diff --git a/src/App.tsx b/src/App.tsx index b8969c9..aca8fae 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -581,6 +581,9 @@ export default function App() { const selectedRoofIds = useStore((s) => s.selectedRoofIds); const setSelectedRoofIds = useStore((s) => s.setSelectedRoofIds); const selectedRoofId = selectedRoofIds.length === 1 ? selectedRoofIds[0] : null; + // Schnitt-/Ansichtslinien-Auswahl (Einzel-DrawingLevel-Id oder null). + const selectedSectionLineId = useStore((s) => s.selectedSectionLineId); + const setSelectedSectionLineId = useStore((s) => s.setSelectedSectionLineId); const updateRoof = useStore((s) => s.updateRoof); const moveRoofGrip = useStore((s) => s.moveRoofGrip); const moveRoofBy = useStore((s) => s.moveRoofBy); @@ -634,6 +637,7 @@ export default function App() { setSelectedExtrudedSolidIds([]); setSelectedColumnIds([]); setSelectedRoofIds([]); + setSelectedSectionLineId(null); setMenu(null); setEditor(null); }, [activeLevelId]); @@ -1897,6 +1901,7 @@ export default function App() { setSelectedExtrudedSolidIds([]); setSelectedColumnIds([]); setSelectedRoofIds([]); + setSelectedSectionLineId(null); e.preventDefault(); } }; @@ -3474,6 +3479,7 @@ export default function App() { setSelectedExtrudedSolidIds([]); setSelectedColumnIds([]); setSelectedRoofIds([]); + setSelectedSectionLineId(null); switch (kind) { case "Wand": setSelectedWallIds([id]); @@ -4333,6 +4339,7 @@ export default function App() { setSelectedExtrudedSolidIds([]); setSelectedColumnIds([]); setSelectedRoofIds([]); + setSelectedSectionLineId(null); setStampEditorRoomId(roomId); }; // Doppelklick auf ein Text-2D-Element: selektieren + Inhalts-Editor öffnen. @@ -4348,6 +4355,7 @@ export default function App() { setSelectedExtrudedSolidIds([]); setSelectedColumnIds([]); setSelectedRoofIds([]); + setSelectedSectionLineId(null); setEditTextId(drawingId); }; // Bearbeiteten Text-Inhalt übernehmen (immutabel; nur bei Text-Geometrie). @@ -4372,6 +4380,13 @@ export default function App() { // Rest behalten (Wände und 2D-Zeichnungen unabhängig, gemischte Auswahl // erlaubt). Leerraum-Klick mit Shift lässt die Auswahl unverändert. if (sel.shift) { + // Schnittlinie ist Einzelauswahl — Shift+Klick togglet sie an/aus. + if (sel.sectionLineId) { + setSelectedSectionLineId( + selectedSectionLineId === sel.sectionLineId ? null : sel.sectionLineId, + ); + return; + } if (sel.wallId) { const id = sel.wallId; setSelectedWallIds( @@ -4453,8 +4468,12 @@ export default function App() { if (keep !== "extrudedSolid") setSelectedExtrudedSolidIds([]); if (keep !== "column") setSelectedColumnIds([]); if (keep !== "roof") setSelectedRoofIds([]); + if (keep !== "sectionLine") setSelectedSectionLineId(null); }; - if (sel.openingId) { + if (sel.sectionLineId) { + setSelectedSectionLineId(sel.sectionLineId); + clearOthers("sectionLine"); + } else if (sel.openingId) { setSelectedOpeningIds([sel.openingId]); clearOthers("opening"); } else if (sel.wallId) { @@ -4499,6 +4518,7 @@ export default function App() { setSelectedExtrudedSolidIds([]); setSelectedColumnIds([]); setSelectedRoofIds([]); + setSelectedSectionLineId(null); }; // Rechts-Klick im Grundriss: Wand-Kontextmenü. Trifft der Klick eine Wand, die // NICHT bereits in der Auswahl ist, wird die Auswahl auf sie gesetzt (damit die @@ -4525,6 +4545,7 @@ export default function App() { setSelectedExtrudedSolidIds([]); setSelectedColumnIds([]); setSelectedRoofIds([]); + setSelectedSectionLineId(null); } }; // Links-Klick im 3D: per Raycast getroffene Treppe wählen (Einzelauswahl). @@ -4539,6 +4560,7 @@ export default function App() { setSelectedExtrudedSolidIds([]); setSelectedColumnIds([]); setSelectedRoofIds([]); + setSelectedSectionLineId(null); } }; // Links-Klick im 3D: per Raycast getroffene Decke wählen (Einzelauswahl). @@ -4553,6 +4575,7 @@ export default function App() { setSelectedExtrudedSolidIds([]); setSelectedColumnIds([]); setSelectedRoofIds([]); + setSelectedSectionLineId(null); } }; // Links-Klick im 3D: per Raycast getroffene Öffnung wählen (Einzelauswahl). @@ -4567,6 +4590,7 @@ export default function App() { setSelectedExtrudedSolidIds([]); setSelectedColumnIds([]); setSelectedRoofIds([]); + setSelectedSectionLineId(null); } }; // Links-KLICK im Nordstern-3D-Viewport (WASM, TS-Raycast): getroffenes Bauteil @@ -4586,6 +4610,7 @@ export default function App() { setSelectedExtrudedSolidIds([]); setSelectedColumnIds([]); setSelectedRoofIds([]); + setSelectedSectionLineId(null); return; } // Öffnung (Fenster/Tür) und Treppe sind jetzt auch im WASM-Viewport pickbar @@ -4601,6 +4626,7 @@ export default function App() { setSelectedExtrudedSolidIds([]); setSelectedColumnIds([]); setSelectedRoofIds([]); + setSelectedSectionLineId(null); return; } if (hit.kind === "stair") { @@ -4613,6 +4639,7 @@ export default function App() { setSelectedExtrudedSolidIds([]); setSelectedColumnIds([]); setSelectedRoofIds([]); + setSelectedSectionLineId(null); return; } if (hit.kind === "roof") { @@ -4645,6 +4672,7 @@ export default function App() { setSelectedExtrudedSolidIds([]); setSelectedColumnIds([]); setSelectedRoofIds([]); + setSelectedSectionLineId(null); }; // Rechts-Klick im 3D: Wand-Kontextmenü an der Cursorposition. const onViewportContextMenu = (info: ViewportContextInfo) => { @@ -4930,6 +4958,7 @@ export default function App() { selectedColumnIds={selectedColumnIds} selectedRoomIds={selectedRoomIds} selectedRoofIds={selectedRoofIds} + selectedSectionLineId={selectedSectionLineId} roomStamp={roomStamp} onStampMove={moveRoomStamp} onStampEdit={onOpenStampEditor} @@ -6063,6 +6092,7 @@ function Content({ selectedColumnIds, selectedRoomIds, selectedRoofIds, + selectedSectionLineId, roomStamp, onStampMove, onStampEdit, @@ -6141,6 +6171,7 @@ function Content({ selectedColumnIds: string[]; selectedRoomIds: string[]; selectedRoofIds: string[]; + selectedSectionLineId: string | null; roomStamp: { roomId: string; anchor: Vec2 } | null; onStampMove: (roomId: string, delta: Vec2) => void; onStampEdit: (roomId: string) => void; @@ -6258,6 +6289,7 @@ function Content({ selectedColumnIds={selectedColumnIds} selectedRoomIds={selectedRoomIds} selectedRoofIds={selectedRoofIds} + selectedSectionLineId={selectedSectionLineId} roomStamp={roomStamp} onStampMove={onStampMove} onStampEdit={onStampEdit} @@ -6313,6 +6345,7 @@ function Content({ selectedColumnIds={selectedColumnIds} selectedRoomIds={selectedRoomIds} selectedRoofIds={selectedRoofIds} + selectedSectionLineId={selectedSectionLineId} roomStamp={roomStamp} onStampMove={onStampMove} onStampEdit={onStampEdit} @@ -6408,6 +6441,7 @@ function Content({ selectedColumnIds={selectedColumnIds} selectedRoomIds={selectedRoomIds} selectedRoofIds={selectedRoofIds} + selectedSectionLineId={selectedSectionLineId} roomStamp={roomStamp} onStampMove={onStampMove} onStampEdit={onStampEdit} @@ -6453,6 +6487,7 @@ function LevelPlanView({ selectedColumnIds, selectedRoomIds, selectedRoofIds, + selectedSectionLineId, roomStamp, onStampMove, onStampEdit, @@ -6491,6 +6526,7 @@ function LevelPlanView({ selectedColumnIds: string[]; selectedRoomIds: string[]; selectedRoofIds: string[]; + selectedSectionLineId: string | null; roomStamp: { roomId: string; anchor: Vec2 } | null; onStampMove: (roomId: string, delta: Vec2) => void; onStampEdit: (roomId: string) => void; @@ -6568,6 +6604,7 @@ function LevelPlanView({ selectedColumnIds={selectedColumnIds} selectedRoomIds={selectedRoomIds} selectedRoofIds={selectedRoofIds} + selectedSectionLineIds={selectedSectionLineId ? [selectedSectionLineId] : []} roomStamp={roomStamp} onStampMove={onStampMove} onStampEdit={onStampEdit} @@ -6621,6 +6658,7 @@ function SectionPlanView({ selectedColumnIds, selectedRoomIds, selectedRoofIds, + selectedSectionLineId, roomStamp, onStampMove, onStampEdit, @@ -6655,6 +6693,7 @@ function SectionPlanView({ selectedColumnIds: string[]; selectedRoomIds: string[]; selectedRoofIds: string[]; + selectedSectionLineId: string | null; roomStamp: { roomId: string; anchor: Vec2 } | null; onStampMove: (roomId: string, delta: Vec2) => void; onStampEdit: (roomId: string) => void; @@ -6742,6 +6781,7 @@ function SectionPlanView({ selectedColumnIds={selectedColumnIds} selectedRoomIds={selectedRoomIds} selectedRoofIds={selectedRoofIds} + selectedSectionLineIds={selectedSectionLineId ? [selectedSectionLineId] : []} roomStamp={roomStamp} onStampMove={onStampMove} onStampEdit={onStampEdit} diff --git a/src/plan/PlanView.tsx b/src/plan/PlanView.tsx index 03bf6b0..bc0add9 100644 --- a/src/plan/PlanView.tsx +++ b/src/plan/PlanView.tsx @@ -197,6 +197,12 @@ export interface PlanSelection { * Element höherer Priorität (alle anderen gehen vor); sonst `null`. */ roofId: string | null; + /** + * ID der getroffenen Schnitt-/Ansichtslinie (DrawingLevel), wenn der Klick ihr + * dünnes Pick-Band traf; sonst `null`. Höchste Priorität (das schmale Band liegt + * über allem — ein Klick genau darauf meint die Linie). + */ + sectionLineId: string | null; /** * War beim Klick Shift gedrückt? Dann ERWEITERT der Aufrufer (App) die * Auswahl um das getroffene Element (bzw. schaltet es ab/zu), statt sie zu @@ -287,6 +293,11 @@ export interface PlanViewProps { selectedRoomIds?: string[]; /** Aktuell ausgewählte Dach-IDs (von App); die Ansicht markiert die Traufe. */ selectedRoofIds?: string[]; + /** + * Aktuell ausgewählte Schnitt-/Ansichtslinien-IDs (DrawingLevel; von App). Die + * Ansicht markiert das Pick-Band JEDER dieser Linien mit einer Akzentkontur. + */ + selectedSectionLineIds?: string[]; /** * Aktuell ausgewählte IDs extrudierter Körper (truck-Integration; kontrolliert * von App). Die Ansicht markiert den Footprint-Umriss JEDES dieser Körper mit @@ -416,6 +427,7 @@ export const PlanView = forwardRef( selectedStairIds, selectedRoomIds, selectedRoofIds, + selectedSectionLineIds, selectedExtrudedSolidIds, selectedColumnIds, roomStamp, @@ -1007,6 +1019,20 @@ export const PlanView = forwardRef( return null; }; + // Getroffene Schnitt-/Ansichtslinie: ihr schmales, unsichtbares Pick-Band + // (Polygon mit sectionLineId) per Punkt-in-Polygon. + const pickSectionLine = (clientX: number, clientY: number): string | null => { + const v = clientToView(clientX, clientY, view); + const m = viewToModel(v.x, v.y); + for (let i = plan.primitives.length - 1; i >= 0; i--) { + const p = plan.primitives[i]; + if (p.kind === "polygon" && p.sectionLineId && pointInPolygon(m, p.pts)) { + return p.sectionLineId; + } + } + return null; + }; + // Getroffener extrudierter Körper (truck-Integration): der Footprint-Umriss // ist ein reiner Haarlinien-Umriss (fill:"none"), aber per Punkt-in-Polygon // trotzdem als FLÄCHE anklickbar (analog Raum, nicht nur auf der Linie). @@ -1518,6 +1544,28 @@ export const PlanView = forwardRef( const hit = hitIndex !== null ? plan.primitives[hitIndex] : undefined; // Öffnung (Fenster/Tür) zuerst: ihr Symbol sitzt in der ausgesparten Lücke // bzw. schlägt über die Wand — ein Klick nahe am Symbol wählt die Öffnung. + // Schnitt-/Ansichtslinie zuerst: ihr schmales Band liegt über allem, ein + // Klick genau darauf meint die Linie (nicht die darunterliegende Wand) — + // dann sofort melden (kein anderes Element konkurriert). + const sectionLineId = pickSectionLine(e.clientX, e.clientY); + if (sectionLineId) { + onSelect({ + model: viewToModel(v.x, v.y), + hitIndex, + wallId: null, + drawingId: null, + ceilingId: null, + openingId: null, + stairId: null, + roomId: null, + extrudedSolidId: null, + columnId: null, + roofId: null, + sectionLineId, + shift: e.shiftKey, + }); + return; + } const openingId = pickOpening(e.clientX, e.clientY); const wallId = openingId ? null : hit && hit.kind === "polygon" ? hit.wallId ?? null : null; @@ -1581,6 +1629,7 @@ export const PlanView = forwardRef( extrudedSolidId, columnId, roofId, + sectionLineId: null, shift: e.shiftKey, }); } @@ -1780,6 +1829,20 @@ export const PlanView = forwardRef( ); }, [plan, selectedRoofIds]); + // Hervorhebung der selektierten Schnitt-/Ansichtslinie: das (unsichtbare) + // Pick-Band (Polygon mit sectionLineId) mit Akzentkontur überzeichnen. + const highlightSectionLinePolys = useMemo(() => { + const ids = + selectedSectionLineIds && selectedSectionLineIds.length + ? new Set(selectedSectionLineIds) + : null; + if (!ids) return []; + return plan.primitives.filter( + (p): p is Extract => + p.kind === "polygon" && p.sectionLineId != null && ids.has(p.sectionLineId), + ); + }, [plan, selectedSectionLineIds]); + // Hervorhebung der selektierten extrudierten Körper (truck-Integration): der // Footprint-Umriss (Polygon mit extrudedSolidId) mit Akzentkontur überzeichnen // — analog zur Raum-Auswahl. @@ -2024,6 +2087,18 @@ export const PlanView = forwardRef( pointerEvents="none" /> ))} + {/* Auswahl-Hervorhebung der gewählten Schnitt-/Ansichtslinie (Pick-Band). */} + {highlightSectionLinePolys.map((poly, i) => ( + `${s.x},${s.y}`) + .join(" ")} + pointerEvents="none" + /> + ))} {/* Auswahl-Hervorhebung der gewählten extrudierten Körper (truck-Integration). */} {highlightExtrudedSolidPolys.map((poly, i) => ( { + it("zeichnet Linie + Pick-Band für eine Schnittebene mit linePoints", () => { + const p = baseProject([ + floor, + { + id: "s-a", + name: "Schnitt A", + kind: "section", + visible: true, + locked: false, + linePoints: [{ x: 0, y: 2 }, { x: 6, y: 2 }], + directionSign: 1, + }, + ]); + const prims = generatePlan(p, "eg", visible).primitives; + // Strichpunkt-Hauptlinie vorhanden. + const lines = prims.filter((pr) => pr.kind === "line" && pr.cls === "section-line"); + expect(lines.length).toBeGreaterThan(0); + // Pick-Band trägt die Level-Id. + const band = prims.find((pr) => pr.kind === "polygon" && pr.sectionLineId === "s-a"); + expect(band).toBeTruthy(); + // Label-Text (Kurz-Tag „A") an den Enden. + const labels = prims.filter((pr) => pr.kind === "drawingText" && pr.text === "A"); + expect(labels.length).toBe(2); + }); + + it("zeichnet NICHTS für eine Schnittebene ohne linePoints", () => { + const p = baseProject([ + floor, + { id: "s-b", name: "Schnitt B", kind: "section", visible: true, locked: false }, + ]); + const prims = generatePlan(p, "eg", visible).primitives; + expect(prims.some((pr) => pr.kind === "polygon" && pr.sectionLineId != null)).toBe(false); + expect(prims.some((pr) => pr.kind === "line" && pr.cls === "section-line")).toBe(false); + }); + + it("behandelt Ansichtsebenen (elevation) gleich", () => { + const p = baseProject([ + floor, + { + id: "e-a", + name: "Ansicht Süd", + kind: "elevation", + visible: true, + locked: false, + linePoints: [{ x: 0, y: 0 }, { x: 4, y: 0 }], + directionSign: -1, + }, + ]); + const prims = generatePlan(p, "eg", visible).primitives; + expect(prims.some((pr) => pr.kind === "polygon" && pr.sectionLineId === "e-a")).toBe(true); + }); +}); diff --git a/src/plan/generatePlan.ts b/src/plan/generatePlan.ts index 92d3ad3..8247e13 100644 --- a/src/plan/generatePlan.ts +++ b/src/plan/generatePlan.ts @@ -265,6 +265,12 @@ export type Primitive = columnId?: string; /** ID des Dachs (Roof), falls dieses (unsichtbare) Polygon der Traufe-Umriss ist (Auswahl). */ roofId?: string; + /** + * ID der Schnitt-/Ansichtslinie (DrawingLevel), falls dieses (unsichtbare) + * Pick-Band das Schnittführungs-Symbol umhüllt (für die Links-Klick-Auswahl + * der Schnittlinie im Grundriss). + */ + sectionLineId?: string; } | { kind: "line"; @@ -892,6 +898,10 @@ export function generatePlan( // die Reihenfolge unkritisch; wir hängen sie als Referenz an. addContextContours(primitives, project); + // Schnittführungs-Symbole (klassische Schnittlinien) aller Schnitt-/Ansichts- + // ebenen mit gesetzter Linie — projektglobale Referenzgeometrie über dem Plan. + addSectionLines(primitives, project); + // Schwarz-Weiss (mono): nachträglich alle Farben auf reine Tinte zwingen. // Element-/Kategorie-/Hatch-Farben werden ignoriert, Füllungen werden weiss, // Musterlinien/Striche schwarz — so entsteht ein reiner S/W-Plan. @@ -2641,6 +2651,139 @@ function addRoof( } } +// ── Schnittführungs-Symbol (klassische Schnittlinie im Grundriss) ─────────── +/** Tinte der Schnittführungs-Linie/Marken (kräftiges Grau). */ +const SECTION_LINE_INK = "#1f242c"; +/** Haarlinie der Strichpunkt-Schnittlinie (mm Papier). */ +const SECTION_LINE_MM = 0.18; +/** Kräftige Endmarken (Querstrich + Pfeil) (mm Papier). */ +const SECTION_LINE_MARK_MM = 0.5; +/** Strichpunkt-Muster der Schnittlinie (lange Striche + Punkt, mm Papier). */ +const SECTION_LINE_DASH: number[] = [6, 1.5, 0.4, 1.5]; +/** Länge der Endmarken (Querstrich/Pfeil) in Modell-Metern. */ +const SECTION_MARK_LEN = 0.6; +/** Halbe Breite des unsichtbaren Pick-Bands über der Linie (Modell-Meter). */ +const SECTION_PICK_HALF = 0.3; +/** Schrifthöhe des Schnitt-Labels (Modell-Meter). */ +const SECTION_LABEL_H = 0.5; + +/** + * Kurz-Tag eines Schnitt-Labels: der letzte „Wort"-Teil des Ebenennamens (z. B. + * „Schnitt A" → „A"); enthält der Name kein Leerzeichen, der ganze Name. Klassisch + * wird der Schnitt an beiden Enden mit diesem Tag beschriftet (A–A). + */ +function sectionLabelTag(name: string): string { + const parts = name.trim().split(/\s+/); + return parts.length > 1 ? parts[parts.length - 1] : name.trim(); +} + +/** + * Zeichnet für jede Schnitt-/Ansichtsebene MIT gesetzter `linePoints` das + * klassische Schnittführungs-Symbol in den Grundriss: eine Strichpunkt-Haarlinie + * über die volle Länge, an beiden Enden einen kräftigen Querstrich + Richtungspfeil + * auf der Blickseite (`directionSign`) und das Schnitt-Label (Kurz-Tag) daneben. + * Zusätzlich ein unsichtbares Pick-Band (Polygon mit `sectionLineId`), damit die + * Linie im Grundriss anklickbar ist (analog dem Dach-Traufe-Pick-Polygon). + */ +function addSectionLines(out: Primitive[], project: Project): void { + for (const level of project.drawingLevels) { + if (level.kind !== "section" && level.kind !== "elevation") continue; + const line = level.linePoints; + if (!line) continue; + const [p0, p1] = line; + const axis = sub(p1, p0); + const len = Math.hypot(axis.x, axis.y); + if (len < 1e-6) continue; + const u = normalize(axis); + const n = leftNormal(u); // (-uy, ux) + const sign = level.directionSign ?? 1; + const bn = { x: n.x * sign, y: n.y * sign }; // Blicknormale + + // Hauptlinie (Strichpunkt-Haarlinie). + out.push({ + kind: "line", + a: p0, + b: p1, + cls: "section-line", + weightMm: SECTION_LINE_MM, + dash: SECTION_LINE_DASH, + color: SECTION_LINE_INK, + }); + + // Endmarken (Querstrich + Richtungspfeil + Label) an beiden Enden. + const half = SECTION_MARK_LEN / 2; + for (const end of [p0, p1]) { + // Querstrich senkrecht zur Linie, mittig am Endpunkt. + const t0 = { x: end.x - n.x * half, y: end.y - n.y * half }; + const t1 = { x: end.x + n.x * half, y: end.y + n.y * half }; + out.push({ + kind: "line", + a: t0, + b: t1, + cls: "section-line", + weightMm: SECTION_LINE_MARK_MM, + color: SECTION_LINE_INK, + }); + // Richtungspfeil: vom Querstrich-Ende auf der Blickseite entlang bn. + const base = { x: end.x + bn.x * half, y: end.y + bn.y * half }; + const tip = { x: base.x + bn.x * SECTION_MARK_LEN, y: base.y + bn.y * SECTION_MARK_LEN }; + const wingBack = { + x: tip.x - bn.x * SECTION_MARK_LEN * 0.4, + y: tip.y - bn.y * SECTION_MARK_LEN * 0.4, + }; + const w = SECTION_MARK_LEN * 0.25; + out.push({ kind: "line", a: base, b: tip, cls: "section-line", weightMm: SECTION_LINE_MARK_MM, color: SECTION_LINE_INK }); + out.push({ + kind: "line", + a: tip, + b: { x: wingBack.x + u.x * w, y: wingBack.y + u.y * w }, + cls: "section-line", + weightMm: SECTION_LINE_MARK_MM, + color: SECTION_LINE_INK, + }); + out.push({ + kind: "line", + a: tip, + b: { x: wingBack.x - u.x * w, y: wingBack.y - u.y * w }, + cls: "section-line", + weightMm: SECTION_LINE_MARK_MM, + color: SECTION_LINE_INK, + }); + // Label (Kurz-Tag) etwas hinter dem Pfeil, horizontal. + const labelAt = { + x: tip.x + bn.x * SECTION_MARK_LEN * 0.4 - SECTION_LABEL_H * 0.3, + y: tip.y + bn.y * SECTION_MARK_LEN * 0.4 + SECTION_LABEL_H * 0.5, + }; + out.push({ + kind: "drawingText", + at: labelAt, + text: sectionLabelTag(level.name), + heightM: SECTION_LABEL_H, + angle: 0, + color: SECTION_LINE_INK, + }); + } + + // Unsichtbares Pick-Band (Rechteck entlang der Linie) — klickbar per + // Punkt-in-Polygon in PlanView, trägt die DrawingLevel-Id. + const off = { x: n.x * SECTION_PICK_HALF, y: n.y * SECTION_PICK_HALF }; + out.push({ + kind: "polygon", + pts: [ + { x: p0.x + off.x, y: p0.y + off.y }, + { x: p1.x + off.x, y: p1.y + off.y }, + { x: p1.x - off.x, y: p1.y - off.y }, + { x: p0.x - off.x, y: p0.y - off.y }, + ], + fill: "none", + stroke: "none", + strokeWidthMm: 0, + hatch: NO_HATCH, + sectionLineId: level.id, + }); + } +} + /** * Deckkraft (0..255) der Raum-Füllung → 2-stelliges Hex-Suffix. Transluzente * Farbfüllung entfällt — Plandarstellung schwarz/grau; Farbe bleibt späteren diff --git a/src/state/selectionSlice.ts b/src/state/selectionSlice.ts index b7bfa4a..3da70a9 100644 --- a/src/state/selectionSlice.ts +++ b/src/state/selectionSlice.ts @@ -25,6 +25,9 @@ export interface SelectionSlice { selectedColumnIds: string[]; // Gewählte Dächer als MENGE von IDs — getrennt von den übrigen Kanälen. selectedRoofIds: string[]; + // Gewählte Schnitt-/Ansichtslinie (DrawingLevel-Id) — EINZELauswahl (eine Linie + // zur Zeit), getrennt von den übrigen Kanälen. `null` = keine Linie gewählt. + selectedSectionLineId: string | null; setSelectedWallIds: (ids: string[]) => void; setSelectedDrawingIds: (ids: string[]) => void; @@ -35,6 +38,7 @@ export interface SelectionSlice { setSelectedExtrudedSolidIds: (ids: string[]) => void; setSelectedColumnIds: (ids: string[]) => void; setSelectedRoofIds: (ids: string[]) => void; + setSelectedSectionLineId: (id: string | null) => void; /** Auswahl (Wände + Decken + Öffnungen + Treppen + Dächer + 2D-Elemente) leeren. */ clearSelection: () => void; } @@ -53,6 +57,7 @@ export function createSelectionSlice( selectedExtrudedSolidIds: [], selectedColumnIds: [], selectedRoofIds: [], + selectedSectionLineId: null, setSelectedWallIds: (ids) => set({ selectedWallIds: ids }), setSelectedDrawingIds: (ids) => set({ selectedDrawingIds: ids }), setSelectedCeilingIds: (ids) => set({ selectedCeilingIds: ids }), @@ -62,6 +67,7 @@ export function createSelectionSlice( setSelectedExtrudedSolidIds: (ids) => set({ selectedExtrudedSolidIds: ids }), setSelectedColumnIds: (ids) => set({ selectedColumnIds: ids }), setSelectedRoofIds: (ids) => set({ selectedRoofIds: ids }), + setSelectedSectionLineId: (id) => set({ selectedSectionLineId: id }), clearSelection: () => set({ selectedWallIds: [], @@ -73,6 +79,7 @@ export function createSelectionSlice( selectedExtrudedSolidIds: [], selectedColumnIds: [], selectedRoofIds: [], + selectedSectionLineId: null, }), }; }