Akkumulierten grünen Arbeitsstand landen (Basis für Weiterarbeit)

Bündelt den über mehrere Sessions gewachsenen, uncommitteten Stand in
einem Basis-Commit, damit Folge-Features isoliert darauf aufsetzen.
Verifikation: tsc --noEmit sauber, vitest 600/600 grün.

Enthalten (Details in PENDENZEN.md -Liste / HANDOVER.md):
- truck-Integration: Profil-Extrusion + Verjüngung + Boolean-CSG (csgrs),
  Crate src-tauri/trucksolid, Werkzeug `extrude`, ExtrudedSolid-Modell.
- kernel2d-Port nach Rust/WASM (Phasen 1–5, Diff-Harness).
- render3d 3D-Live-Schnitt = 2D-Schnitt: geschichteter Bodenaufbau,
  Prioritäts-Verschneidung (section_boolean.rs), einstellbare
  Schichttrennlinien, per-Hatch-Strichstärke, relativeToWall-Orientierung.
- Interop-Export IFC4/STL/OBJ (Loch-Ausschnitt wallMeshCut), Schnellexport.
- Projektdatei .obp + OS-Lock (lock.rs, LockConflictDialog).
- Layout-Blätter (Modell/Editor/Panel/PDF), Ausschnitte, Override-Engine,
  Tragwerk-Stützen (Column), BIM-Tree-Panel.
- Bauteil-Typsystem (Tür/Fenster/Treppe-Typen), Betontreppe mit schräger
  Laufplatte, Text-/Textbox-Werkzeug, Mess-Werkzeug, 2D/3D-Griffe für
  Öffnungen/Treppen, Snap-Symbol-Restyle.
This commit is contained in:
2026-07-09 00:57:29 +02:00
parent 889cbb2c12
commit 35299307d6
131 changed files with 26501 additions and 852 deletions
+394
View File
@@ -2470,6 +2470,223 @@ pub fn room_from_point_inside_faces(
best
}
// --- Oeffnungs-Geometrie (Port von src/geometry/opening.ts, Slice 4) --------
// `Wall`/`Opening`/`Project`-Kopplung durch Vec2/number-Parameter ersetzt
// (PORT_PLAN §1 "Teilweise im Scope"): `thickness`/`refOffset` werden hier als
// bereits aufgeloeste Zahlen uebergeben (TS: `wallTypeThickness(getWallType(...))`
// bzw. `wallReferenceOffset(wall, total)` gegen ein `Project`). Nur
// `openingVerticalExtent` bleibt TS (haengt an `Project.drawingLevels` via
// `wallVerticalExtent`) und wird hier NICHT portiert.
//
// `DEG` (Grad→Radiant, Port des lokalen `DEG` in opening.ts) ist bereits als
// modul-weite Konstante aus dem stair-Slice vorhanden (identischer Wert).
/// Punkt entlang der Strecke p1→p2 im Abstand d vom Start (Port von `along`
/// aus src/model/geometry.ts).
pub fn along(p1: Vec2, p2: Vec2, d: f64) -> Vec2 {
add(p1, scale(normalize(sub(p2, p1)), d))
}
/// Achslaenge einer Wand (Port von `wallAxisLength`; Wand → start/end).
pub fn wall_axis_length(start: Vec2, end: Vec2) -> f64 {
(end.x - start.x).hypot(end.y - start.y)
}
/// Oeffnungs-Intervall entlang der Achse (Port von `openingInterval`).
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Interval {
pub from: f64,
pub to: f64,
}
pub fn opening_interval(start: Vec2, end: Vec2, position: f64, width: f64) -> Option<Interval> {
let axis = wall_axis_length(start, end);
let from = position.min(axis).max(0.0);
let to = (position + width).min(axis).max(from);
if to - from < 1e-4 {
return None; // entartet: Breite <= 0 oder vollstaendig ausserhalb der Achse
}
Some(Interval { from, to })
}
/// Einheits-Laufrichtung (u) und linke Normale (n) der Wandachse (Port von
/// `wallAxisFrame`).
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
pub struct AxisFrame {
pub u: Vec2,
pub n: Vec2,
}
pub fn wall_axis_frame(start: Vec2, end: Vec2) -> AxisFrame {
let u = normalize(sub(end, start));
AxisFrame { u, n: left_normal(u) }
}
/// Die beiden Pfosten-Punkte einer Oeffnung im Grundriss (Port von `openingJambs`).
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Jambs {
pub jamb_start: Vec2,
pub jamb_end: Vec2,
}
pub fn opening_jambs(start: Vec2, end: Vec2, position: f64, width: f64) -> Option<Jambs> {
let iv = opening_interval(start, end, position, width)?;
Some(Jambs {
jamb_start: along(start, end, iv.from),
jamb_end: along(start, end, iv.to),
})
}
/// Luecken-Rechteck-Ecken ueber die volle Wanddicke (Port von `openingGapQuad`).
/// Reihenfolge: jambStart(+n/2) → jambEnd(+n/2) → jambEnd(n/2) → jambStart(n/2).
pub fn opening_gap_quad(
start: Vec2,
end: Vec2,
position: f64,
width: f64,
thickness: f64,
ref_offset: f64,
) -> Option<Vec<Vec2>> {
let jambs = opening_jambs(start, end, position, width)?;
let n = wall_axis_frame(start, end).n;
let outer = ref_offset + thickness / 2.0;
let inner = ref_offset - thickness / 2.0;
Some(vec![
add(jambs.jamb_start, scale(n, outer)),
add(jambs.jamb_end, scale(n, outer)),
add(jambs.jamb_end, scale(n, inner)),
add(jambs.jamb_start, scale(n, inner)),
])
}
/// Mittelpunkt einer Oeffnung im Grundriss (Port von `openingCenter`).
pub fn opening_center(start: Vec2, end: Vec2, position: f64, width: f64) -> Option<Vec2> {
let iv = opening_interval(start, end, position, width)?;
Some(along(start, end, (iv.from + iv.to) / 2.0))
}
/// Aufgeloeste Tuer-Symbol-Geometrie im Grundriss (Port von `DoorSymbol`).
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct DoorSymbol {
pub hinge: Vec2,
pub open_end: Vec2,
pub closed_end: Vec2,
pub radius: f64,
pub jamb_start: Vec2,
pub jamb_end: Vec2,
pub normal: Vec2,
}
/// Port von `doorSymbol`. `swing`/`opening_dir`/`hinge_side` folgen der TS-
/// `??`-Fallback-Semantik (`None` → TS-Default); `swing_angle` `None` → 90°.
pub fn door_symbol(
start: Vec2,
end: Vec2,
position: f64,
width: f64,
swing: Option<&str>,
opening_dir: Option<&str>,
hinge_side: Option<&str>,
swing_angle: Option<f64>,
) -> Option<DoorSymbol> {
let jambs = opening_jambs(start, end, position, width)?;
let frame = wall_axis_frame(start, end);
let (jamb_start, jamb_end) = (jambs.jamb_start, jambs.jamb_end);
// TS berechnet die Breite NEU aus den geklemmten Pfosten (nicht das rohe
// `o.width`) — bewusst repliziert.
let w = (jamb_end.x - jamb_start.x).hypot(jamb_end.y - jamb_start.y);
let swing_sign = if swing.unwrap_or("left") == "left" { 1.0 } else { -1.0 };
let dir_sign = if opening_dir.unwrap_or("in") == "in" { 1.0 } else { -1.0 };
let swing_dir = scale(frame.n, swing_sign * dir_sign);
let hinge_at_start = hinge_side.unwrap_or("start") == "start";
let hinge = if hinge_at_start { jamb_start } else { jamb_end };
let closed_dir = if hinge_at_start { frame.u } else { scale(frame.u, -1.0) };
let angle = swing_angle.unwrap_or(90.0) * DEG;
let open_dir = Vec2 {
x: closed_dir.x * angle.cos() + swing_dir.x * angle.sin(),
y: closed_dir.y * angle.cos() + swing_dir.y * angle.sin(),
};
Some(DoorSymbol {
hinge,
open_end: add(hinge, scale(open_dir, w)),
closed_end: add(hinge, scale(closed_dir, w)),
radius: w,
jamb_start,
jamb_end,
normal: frame.n,
})
}
/// Aufgeloeste Fenster-Linien im Grundriss (Port von `WindowSymbol`).
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct WindowSymbol {
pub frame: Vec<Vec2>,
pub glass_lines: Vec<(Vec2, Vec2)>,
pub mullion_lines: Vec<(Vec2, Vec2)>,
}
/// Port von `windowSymbol`. `thickness`/`ref_offset` sind bereits aufgeloeste
/// Werte (siehe Modul-Kommentar); `wing_count` `None` → TS-Default `1`.
pub fn window_symbol(
start: Vec2,
end: Vec2,
position: f64,
width: f64,
thickness: f64,
ref_offset: f64,
glass_count: f64,
wing_count: Option<f64>,
) -> Option<WindowSymbol> {
let jambs = opening_jambs(start, end, position, width)?;
let (jamb_start, jamb_end) = (jambs.jamb_start, jambs.jamb_end);
let n = wall_axis_frame(start, end).n;
let outer = ref_offset + thickness / 2.0;
let inner = ref_offset - thickness / 2.0;
let frame_quad = vec![
add(jamb_start, scale(n, outer)),
add(jamb_end, scale(n, outer)),
add(jamb_end, scale(n, inner)),
add(jamb_start, scale(n, inner)),
];
let mut glass_lines: Vec<(Vec2, Vec2)> = Vec::new();
let count = glass_count.max(1.0);
let mut i: i64 = 0;
while (i as f64) < count {
let spread = thickness / 8.0;
let off = if count == 1.0 {
ref_offset
} else {
ref_offset + ((i as f64) - (count - 1.0) / 2.0) * spread * 2.0
};
glass_lines.push((add(jamb_start, scale(n, off)), add(jamb_end, scale(n, off))));
i += 1;
}
let mut mullion_lines: Vec<(Vec2, Vec2)> = Vec::new();
let wings = wing_count.unwrap_or(1.0).round().min(4.0).max(1.0);
if wings > 1.0 {
let post_depth = thickness * 0.6;
let p_outer = ref_offset + post_depth / 2.0;
let p_inner = ref_offset - post_depth / 2.0;
let wings_n = wings as i64;
for k in 1..wings_n {
let t = k as f64 / wings;
let on_axis = add(jamb_start, scale(sub(jamb_end, jamb_start), t));
mullion_lines.push((add(on_axis, scale(n, p_outer)), add(on_axis, scale(n, p_inner))));
}
}
Some(WindowSymbol { frame: frame_quad, glass_lines, mullion_lines })
}
// --- Batch-Fassaden: roomArea / ceiling (Slice 1) ----------------------------
#[cfg(feature = "web")]
@@ -2741,6 +2958,183 @@ pub fn rb_point_in_polygon_batch_json(input_json: &str) -> Result<String, wasm_b
to_js(&out)
}
// --- Batch-Fassaden: opening (Slice 4) ---------------------------------------
#[cfg(feature = "web")]
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct WallAxisLengthQuery {
start: Vec2,
end: Vec2,
}
#[cfg(feature = "web")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn wall_axis_length_batch_json(input_json: &str) -> Result<String, wasm_bindgen::JsValue> {
console_error_panic_hook::set_once();
let qs: Vec<WallAxisLengthQuery> = from_js(input_json)?;
let out: Vec<f64> = qs.iter().map(|q| wall_axis_length(q.start, q.end)).collect();
to_js(&out)
}
#[cfg(feature = "web")]
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct OpeningQuery {
start: Vec2,
end: Vec2,
position: f64,
width: f64,
}
#[cfg(feature = "web")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn opening_interval_batch_json(input_json: &str) -> Result<String, wasm_bindgen::JsValue> {
console_error_panic_hook::set_once();
let qs: Vec<OpeningQuery> = from_js(input_json)?;
let out: Vec<Option<Interval>> = qs
.iter()
.map(|q| opening_interval(q.start, q.end, q.position, q.width))
.collect();
to_js(&out)
}
#[cfg(feature = "web")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn wall_axis_frame_batch_json(input_json: &str) -> Result<String, wasm_bindgen::JsValue> {
console_error_panic_hook::set_once();
let qs: Vec<WallAxisLengthQuery> = from_js(input_json)?;
let out: Vec<AxisFrame> = qs.iter().map(|q| wall_axis_frame(q.start, q.end)).collect();
to_js(&out)
}
#[cfg(feature = "web")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn opening_jambs_batch_json(input_json: &str) -> Result<String, wasm_bindgen::JsValue> {
console_error_panic_hook::set_once();
let qs: Vec<OpeningQuery> = from_js(input_json)?;
let out: Vec<Option<Jambs>> = qs
.iter()
.map(|q| opening_jambs(q.start, q.end, q.position, q.width))
.collect();
to_js(&out)
}
#[cfg(feature = "web")]
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct OpeningGapQuadQuery {
start: Vec2,
end: Vec2,
position: f64,
width: f64,
thickness: f64,
ref_offset: f64,
}
#[cfg(feature = "web")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn opening_gap_quad_batch_json(input_json: &str) -> Result<String, wasm_bindgen::JsValue> {
console_error_panic_hook::set_once();
let qs: Vec<OpeningGapQuadQuery> = from_js(input_json)?;
let out: Vec<Option<Vec<Vec2>>> = qs
.iter()
.map(|q| opening_gap_quad(q.start, q.end, q.position, q.width, q.thickness, q.ref_offset))
.collect();
to_js(&out)
}
#[cfg(feature = "web")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn opening_center_batch_json(input_json: &str) -> Result<String, wasm_bindgen::JsValue> {
console_error_panic_hook::set_once();
let qs: Vec<OpeningQuery> = from_js(input_json)?;
let out: Vec<Option<Vec2>> = qs
.iter()
.map(|q| opening_center(q.start, q.end, q.position, q.width))
.collect();
to_js(&out)
}
#[cfg(feature = "web")]
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct DoorSymbolQuery {
start: Vec2,
end: Vec2,
position: f64,
width: f64,
#[serde(default)]
swing: Option<String>,
#[serde(default)]
opening_dir: Option<String>,
#[serde(default)]
hinge: Option<String>,
#[serde(default)]
swing_angle: Option<f64>,
}
#[cfg(feature = "web")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn door_symbol_batch_json(input_json: &str) -> Result<String, wasm_bindgen::JsValue> {
console_error_panic_hook::set_once();
let qs: Vec<DoorSymbolQuery> = from_js(input_json)?;
let out: Vec<Option<DoorSymbol>> = qs
.iter()
.map(|q| {
door_symbol(
q.start,
q.end,
q.position,
q.width,
q.swing.as_deref(),
q.opening_dir.as_deref(),
q.hinge.as_deref(),
q.swing_angle,
)
})
.collect();
to_js(&out)
}
#[cfg(feature = "web")]
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct WindowSymbolQuery {
start: Vec2,
end: Vec2,
position: f64,
width: f64,
thickness: f64,
ref_offset: f64,
glass_count: f64,
#[serde(default)]
wing_count: Option<f64>,
}
#[cfg(feature = "web")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn window_symbol_batch_json(input_json: &str) -> Result<String, wasm_bindgen::JsValue> {
console_error_panic_hook::set_once();
let qs: Vec<WindowSymbolQuery> = from_js(input_json)?;
let out: Vec<Option<WindowSymbol>> = qs
.iter()
.map(|q| {
window_symbol(
q.start,
q.end,
q.position,
q.width,
q.thickness,
q.ref_offset,
q.glass_count,
q.wing_count,
)
})
.collect();
to_js(&out)
}
#[cfg(test)]
mod tests {
use super::*;