Schnittführungs-Symbol im Grundriss + Schnittlinien-Auswahl

generatePlan zeichnet fuer jede Schnitt-/Ansichtsebene mit linePoints das
klassische Symbol: Strichpunkt-Linie, Endmarken + Richtungspfeile (directionSign),
Kurz-Label an beiden Enden und ein unsichtbares Pick-Band (Polygon mit
sectionLineId). PlanView pickt die Linie (hoechste Prioritaet) und hebt die
Auswahl hervor. Neuer Selektionskanal selectedSectionLineId (Einzel-DrawingLevel-
Id) in selectionSlice; onPlanSelect + alle Reset-Stellen in App gepflegt.
Primitive.sectionLineId, PlanSelection.sectionLineId, Tests.
This commit is contained in:
2026-07-11 00:21:29 +02:00
parent 340f950f9c
commit 39bceedc17
5 changed files with 364 additions and 1 deletions
+41 -1
View File
@@ -581,6 +581,9 @@ export default function App() {
const selectedRoofIds = useStore((s) => s.selectedRoofIds); const selectedRoofIds = useStore((s) => s.selectedRoofIds);
const setSelectedRoofIds = useStore((s) => s.setSelectedRoofIds); const setSelectedRoofIds = useStore((s) => s.setSelectedRoofIds);
const selectedRoofId = selectedRoofIds.length === 1 ? selectedRoofIds[0] : null; 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 updateRoof = useStore((s) => s.updateRoof);
const moveRoofGrip = useStore((s) => s.moveRoofGrip); const moveRoofGrip = useStore((s) => s.moveRoofGrip);
const moveRoofBy = useStore((s) => s.moveRoofBy); const moveRoofBy = useStore((s) => s.moveRoofBy);
@@ -634,6 +637,7 @@ export default function App() {
setSelectedExtrudedSolidIds([]); setSelectedExtrudedSolidIds([]);
setSelectedColumnIds([]); setSelectedColumnIds([]);
setSelectedRoofIds([]); setSelectedRoofIds([]);
setSelectedSectionLineId(null);
setMenu(null); setMenu(null);
setEditor(null); setEditor(null);
}, [activeLevelId]); }, [activeLevelId]);
@@ -1897,6 +1901,7 @@ export default function App() {
setSelectedExtrudedSolidIds([]); setSelectedExtrudedSolidIds([]);
setSelectedColumnIds([]); setSelectedColumnIds([]);
setSelectedRoofIds([]); setSelectedRoofIds([]);
setSelectedSectionLineId(null);
e.preventDefault(); e.preventDefault();
} }
}; };
@@ -3474,6 +3479,7 @@ export default function App() {
setSelectedExtrudedSolidIds([]); setSelectedExtrudedSolidIds([]);
setSelectedColumnIds([]); setSelectedColumnIds([]);
setSelectedRoofIds([]); setSelectedRoofIds([]);
setSelectedSectionLineId(null);
switch (kind) { switch (kind) {
case "Wand": case "Wand":
setSelectedWallIds([id]); setSelectedWallIds([id]);
@@ -4333,6 +4339,7 @@ export default function App() {
setSelectedExtrudedSolidIds([]); setSelectedExtrudedSolidIds([]);
setSelectedColumnIds([]); setSelectedColumnIds([]);
setSelectedRoofIds([]); setSelectedRoofIds([]);
setSelectedSectionLineId(null);
setStampEditorRoomId(roomId); setStampEditorRoomId(roomId);
}; };
// Doppelklick auf ein Text-2D-Element: selektieren + Inhalts-Editor öffnen. // Doppelklick auf ein Text-2D-Element: selektieren + Inhalts-Editor öffnen.
@@ -4348,6 +4355,7 @@ export default function App() {
setSelectedExtrudedSolidIds([]); setSelectedExtrudedSolidIds([]);
setSelectedColumnIds([]); setSelectedColumnIds([]);
setSelectedRoofIds([]); setSelectedRoofIds([]);
setSelectedSectionLineId(null);
setEditTextId(drawingId); setEditTextId(drawingId);
}; };
// Bearbeiteten Text-Inhalt übernehmen (immutabel; nur bei Text-Geometrie). // 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 // Rest behalten (Wände und 2D-Zeichnungen unabhängig, gemischte Auswahl
// erlaubt). Leerraum-Klick mit Shift lässt die Auswahl unverändert. // erlaubt). Leerraum-Klick mit Shift lässt die Auswahl unverändert.
if (sel.shift) { 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) { if (sel.wallId) {
const id = sel.wallId; const id = sel.wallId;
setSelectedWallIds( setSelectedWallIds(
@@ -4453,8 +4468,12 @@ export default function App() {
if (keep !== "extrudedSolid") setSelectedExtrudedSolidIds([]); if (keep !== "extrudedSolid") setSelectedExtrudedSolidIds([]);
if (keep !== "column") setSelectedColumnIds([]); if (keep !== "column") setSelectedColumnIds([]);
if (keep !== "roof") setSelectedRoofIds([]); 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]); setSelectedOpeningIds([sel.openingId]);
clearOthers("opening"); clearOthers("opening");
} else if (sel.wallId) { } else if (sel.wallId) {
@@ -4499,6 +4518,7 @@ export default function App() {
setSelectedExtrudedSolidIds([]); setSelectedExtrudedSolidIds([]);
setSelectedColumnIds([]); setSelectedColumnIds([]);
setSelectedRoofIds([]); setSelectedRoofIds([]);
setSelectedSectionLineId(null);
}; };
// Rechts-Klick im Grundriss: Wand-Kontextmenü. Trifft der Klick eine Wand, die // 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 // NICHT bereits in der Auswahl ist, wird die Auswahl auf sie gesetzt (damit die
@@ -4525,6 +4545,7 @@ export default function App() {
setSelectedExtrudedSolidIds([]); setSelectedExtrudedSolidIds([]);
setSelectedColumnIds([]); setSelectedColumnIds([]);
setSelectedRoofIds([]); setSelectedRoofIds([]);
setSelectedSectionLineId(null);
} }
}; };
// Links-Klick im 3D: per Raycast getroffene Treppe wählen (Einzelauswahl). // Links-Klick im 3D: per Raycast getroffene Treppe wählen (Einzelauswahl).
@@ -4539,6 +4560,7 @@ export default function App() {
setSelectedExtrudedSolidIds([]); setSelectedExtrudedSolidIds([]);
setSelectedColumnIds([]); setSelectedColumnIds([]);
setSelectedRoofIds([]); setSelectedRoofIds([]);
setSelectedSectionLineId(null);
} }
}; };
// Links-Klick im 3D: per Raycast getroffene Decke wählen (Einzelauswahl). // Links-Klick im 3D: per Raycast getroffene Decke wählen (Einzelauswahl).
@@ -4553,6 +4575,7 @@ export default function App() {
setSelectedExtrudedSolidIds([]); setSelectedExtrudedSolidIds([]);
setSelectedColumnIds([]); setSelectedColumnIds([]);
setSelectedRoofIds([]); setSelectedRoofIds([]);
setSelectedSectionLineId(null);
} }
}; };
// Links-Klick im 3D: per Raycast getroffene Öffnung wählen (Einzelauswahl). // Links-Klick im 3D: per Raycast getroffene Öffnung wählen (Einzelauswahl).
@@ -4567,6 +4590,7 @@ export default function App() {
setSelectedExtrudedSolidIds([]); setSelectedExtrudedSolidIds([]);
setSelectedColumnIds([]); setSelectedColumnIds([]);
setSelectedRoofIds([]); setSelectedRoofIds([]);
setSelectedSectionLineId(null);
} }
}; };
// Links-KLICK im Nordstern-3D-Viewport (WASM, TS-Raycast): getroffenes Bauteil // Links-KLICK im Nordstern-3D-Viewport (WASM, TS-Raycast): getroffenes Bauteil
@@ -4586,6 +4610,7 @@ export default function App() {
setSelectedExtrudedSolidIds([]); setSelectedExtrudedSolidIds([]);
setSelectedColumnIds([]); setSelectedColumnIds([]);
setSelectedRoofIds([]); setSelectedRoofIds([]);
setSelectedSectionLineId(null);
return; return;
} }
// Öffnung (Fenster/Tür) und Treppe sind jetzt auch im WASM-Viewport pickbar // Öffnung (Fenster/Tür) und Treppe sind jetzt auch im WASM-Viewport pickbar
@@ -4601,6 +4626,7 @@ export default function App() {
setSelectedExtrudedSolidIds([]); setSelectedExtrudedSolidIds([]);
setSelectedColumnIds([]); setSelectedColumnIds([]);
setSelectedRoofIds([]); setSelectedRoofIds([]);
setSelectedSectionLineId(null);
return; return;
} }
if (hit.kind === "stair") { if (hit.kind === "stair") {
@@ -4613,6 +4639,7 @@ export default function App() {
setSelectedExtrudedSolidIds([]); setSelectedExtrudedSolidIds([]);
setSelectedColumnIds([]); setSelectedColumnIds([]);
setSelectedRoofIds([]); setSelectedRoofIds([]);
setSelectedSectionLineId(null);
return; return;
} }
if (hit.kind === "roof") { if (hit.kind === "roof") {
@@ -4645,6 +4672,7 @@ export default function App() {
setSelectedExtrudedSolidIds([]); setSelectedExtrudedSolidIds([]);
setSelectedColumnIds([]); setSelectedColumnIds([]);
setSelectedRoofIds([]); setSelectedRoofIds([]);
setSelectedSectionLineId(null);
}; };
// Rechts-Klick im 3D: Wand-Kontextmenü an der Cursorposition. // Rechts-Klick im 3D: Wand-Kontextmenü an der Cursorposition.
const onViewportContextMenu = (info: ViewportContextInfo) => { const onViewportContextMenu = (info: ViewportContextInfo) => {
@@ -4930,6 +4958,7 @@ export default function App() {
selectedColumnIds={selectedColumnIds} selectedColumnIds={selectedColumnIds}
selectedRoomIds={selectedRoomIds} selectedRoomIds={selectedRoomIds}
selectedRoofIds={selectedRoofIds} selectedRoofIds={selectedRoofIds}
selectedSectionLineId={selectedSectionLineId}
roomStamp={roomStamp} roomStamp={roomStamp}
onStampMove={moveRoomStamp} onStampMove={moveRoomStamp}
onStampEdit={onOpenStampEditor} onStampEdit={onOpenStampEditor}
@@ -6063,6 +6092,7 @@ function Content({
selectedColumnIds, selectedColumnIds,
selectedRoomIds, selectedRoomIds,
selectedRoofIds, selectedRoofIds,
selectedSectionLineId,
roomStamp, roomStamp,
onStampMove, onStampMove,
onStampEdit, onStampEdit,
@@ -6141,6 +6171,7 @@ function Content({
selectedColumnIds: string[]; selectedColumnIds: string[];
selectedRoomIds: string[]; selectedRoomIds: string[];
selectedRoofIds: string[]; selectedRoofIds: string[];
selectedSectionLineId: string | null;
roomStamp: { roomId: string; anchor: Vec2 } | null; roomStamp: { roomId: string; anchor: Vec2 } | null;
onStampMove: (roomId: string, delta: Vec2) => void; onStampMove: (roomId: string, delta: Vec2) => void;
onStampEdit: (roomId: string) => void; onStampEdit: (roomId: string) => void;
@@ -6258,6 +6289,7 @@ function Content({
selectedColumnIds={selectedColumnIds} selectedColumnIds={selectedColumnIds}
selectedRoomIds={selectedRoomIds} selectedRoomIds={selectedRoomIds}
selectedRoofIds={selectedRoofIds} selectedRoofIds={selectedRoofIds}
selectedSectionLineId={selectedSectionLineId}
roomStamp={roomStamp} roomStamp={roomStamp}
onStampMove={onStampMove} onStampMove={onStampMove}
onStampEdit={onStampEdit} onStampEdit={onStampEdit}
@@ -6313,6 +6345,7 @@ function Content({
selectedColumnIds={selectedColumnIds} selectedColumnIds={selectedColumnIds}
selectedRoomIds={selectedRoomIds} selectedRoomIds={selectedRoomIds}
selectedRoofIds={selectedRoofIds} selectedRoofIds={selectedRoofIds}
selectedSectionLineId={selectedSectionLineId}
roomStamp={roomStamp} roomStamp={roomStamp}
onStampMove={onStampMove} onStampMove={onStampMove}
onStampEdit={onStampEdit} onStampEdit={onStampEdit}
@@ -6408,6 +6441,7 @@ function Content({
selectedColumnIds={selectedColumnIds} selectedColumnIds={selectedColumnIds}
selectedRoomIds={selectedRoomIds} selectedRoomIds={selectedRoomIds}
selectedRoofIds={selectedRoofIds} selectedRoofIds={selectedRoofIds}
selectedSectionLineId={selectedSectionLineId}
roomStamp={roomStamp} roomStamp={roomStamp}
onStampMove={onStampMove} onStampMove={onStampMove}
onStampEdit={onStampEdit} onStampEdit={onStampEdit}
@@ -6453,6 +6487,7 @@ function LevelPlanView({
selectedColumnIds, selectedColumnIds,
selectedRoomIds, selectedRoomIds,
selectedRoofIds, selectedRoofIds,
selectedSectionLineId,
roomStamp, roomStamp,
onStampMove, onStampMove,
onStampEdit, onStampEdit,
@@ -6491,6 +6526,7 @@ function LevelPlanView({
selectedColumnIds: string[]; selectedColumnIds: string[];
selectedRoomIds: string[]; selectedRoomIds: string[];
selectedRoofIds: string[]; selectedRoofIds: string[];
selectedSectionLineId: string | null;
roomStamp: { roomId: string; anchor: Vec2 } | null; roomStamp: { roomId: string; anchor: Vec2 } | null;
onStampMove: (roomId: string, delta: Vec2) => void; onStampMove: (roomId: string, delta: Vec2) => void;
onStampEdit: (roomId: string) => void; onStampEdit: (roomId: string) => void;
@@ -6568,6 +6604,7 @@ function LevelPlanView({
selectedColumnIds={selectedColumnIds} selectedColumnIds={selectedColumnIds}
selectedRoomIds={selectedRoomIds} selectedRoomIds={selectedRoomIds}
selectedRoofIds={selectedRoofIds} selectedRoofIds={selectedRoofIds}
selectedSectionLineIds={selectedSectionLineId ? [selectedSectionLineId] : []}
roomStamp={roomStamp} roomStamp={roomStamp}
onStampMove={onStampMove} onStampMove={onStampMove}
onStampEdit={onStampEdit} onStampEdit={onStampEdit}
@@ -6621,6 +6658,7 @@ function SectionPlanView({
selectedColumnIds, selectedColumnIds,
selectedRoomIds, selectedRoomIds,
selectedRoofIds, selectedRoofIds,
selectedSectionLineId,
roomStamp, roomStamp,
onStampMove, onStampMove,
onStampEdit, onStampEdit,
@@ -6655,6 +6693,7 @@ function SectionPlanView({
selectedColumnIds: string[]; selectedColumnIds: string[];
selectedRoomIds: string[]; selectedRoomIds: string[];
selectedRoofIds: string[]; selectedRoofIds: string[];
selectedSectionLineId: string | null;
roomStamp: { roomId: string; anchor: Vec2 } | null; roomStamp: { roomId: string; anchor: Vec2 } | null;
onStampMove: (roomId: string, delta: Vec2) => void; onStampMove: (roomId: string, delta: Vec2) => void;
onStampEdit: (roomId: string) => void; onStampEdit: (roomId: string) => void;
@@ -6742,6 +6781,7 @@ function SectionPlanView({
selectedColumnIds={selectedColumnIds} selectedColumnIds={selectedColumnIds}
selectedRoomIds={selectedRoomIds} selectedRoomIds={selectedRoomIds}
selectedRoofIds={selectedRoofIds} selectedRoofIds={selectedRoofIds}
selectedSectionLineIds={selectedSectionLineId ? [selectedSectionLineId] : []}
roomStamp={roomStamp} roomStamp={roomStamp}
onStampMove={onStampMove} onStampMove={onStampMove}
onStampEdit={onStampEdit} onStampEdit={onStampEdit}
+75
View File
@@ -197,6 +197,12 @@ export interface PlanSelection {
* Element höherer Priorität (alle anderen gehen vor); sonst `null`. * Element höherer Priorität (alle anderen gehen vor); sonst `null`.
*/ */
roofId: string | 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 * 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 * Auswahl um das getroffene Element (bzw. schaltet es ab/zu), statt sie zu
@@ -287,6 +293,11 @@ export interface PlanViewProps {
selectedRoomIds?: string[]; selectedRoomIds?: string[];
/** Aktuell ausgewählte Dach-IDs (von App); die Ansicht markiert die Traufe. */ /** Aktuell ausgewählte Dach-IDs (von App); die Ansicht markiert die Traufe. */
selectedRoofIds?: string[]; 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 * Aktuell ausgewählte IDs extrudierter Körper (truck-Integration; kontrolliert
* von App). Die Ansicht markiert den Footprint-Umriss JEDES dieser Körper mit * von App). Die Ansicht markiert den Footprint-Umriss JEDES dieser Körper mit
@@ -416,6 +427,7 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
selectedStairIds, selectedStairIds,
selectedRoomIds, selectedRoomIds,
selectedRoofIds, selectedRoofIds,
selectedSectionLineIds,
selectedExtrudedSolidIds, selectedExtrudedSolidIds,
selectedColumnIds, selectedColumnIds,
roomStamp, roomStamp,
@@ -1007,6 +1019,20 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
return null; 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 // Getroffener extrudierter Körper (truck-Integration): der Footprint-Umriss
// ist ein reiner Haarlinien-Umriss (fill:"none"), aber per Punkt-in-Polygon // ist ein reiner Haarlinien-Umriss (fill:"none"), aber per Punkt-in-Polygon
// trotzdem als FLÄCHE anklickbar (analog Raum, nicht nur auf der Linie). // trotzdem als FLÄCHE anklickbar (analog Raum, nicht nur auf der Linie).
@@ -1518,6 +1544,28 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
const hit = hitIndex !== null ? plan.primitives[hitIndex] : undefined; const hit = hitIndex !== null ? plan.primitives[hitIndex] : undefined;
// Öffnung (Fenster/Tür) zuerst: ihr Symbol sitzt in der ausgesparten Lücke // Ö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. // 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 openingId = pickOpening(e.clientX, e.clientY);
const wallId = const wallId =
openingId ? null : hit && hit.kind === "polygon" ? hit.wallId ?? null : null; openingId ? null : hit && hit.kind === "polygon" ? hit.wallId ?? null : null;
@@ -1581,6 +1629,7 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
extrudedSolidId, extrudedSolidId,
columnId, columnId,
roofId, roofId,
sectionLineId: null,
shift: e.shiftKey, shift: e.shiftKey,
}); });
} }
@@ -1780,6 +1829,20 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
); );
}, [plan, selectedRoofIds]); }, [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<Primitive, { kind: "polygon" }> =>
p.kind === "polygon" && p.sectionLineId != null && ids.has(p.sectionLineId),
);
}, [plan, selectedSectionLineIds]);
// Hervorhebung der selektierten extrudierten Körper (truck-Integration): der // Hervorhebung der selektierten extrudierten Körper (truck-Integration): der
// Footprint-Umriss (Polygon mit extrudedSolidId) mit Akzentkontur überzeichnen // Footprint-Umriss (Polygon mit extrudedSolidId) mit Akzentkontur überzeichnen
// — analog zur Raum-Auswahl. // — analog zur Raum-Auswahl.
@@ -2024,6 +2087,18 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
pointerEvents="none" pointerEvents="none"
/> />
))} ))}
{/* Auswahl-Hervorhebung der gewählten Schnitt-/Ansichtslinie (Pick-Band). */}
{highlightSectionLinePolys.map((poly, i) => (
<polygon
key={`selsl-${i}`}
className="plan-selected"
points={poly.pts
.map(toScreen)
.map((s) => `${s.x},${s.y}`)
.join(" ")}
pointerEvents="none"
/>
))}
{/* Auswahl-Hervorhebung der gewählten extrudierten Körper (truck-Integration). */} {/* Auswahl-Hervorhebung der gewählten extrudierten Körper (truck-Integration). */}
{highlightExtrudedSolidPolys.map((poly, i) => ( {highlightExtrudedSolidPolys.map((poly, i) => (
<polygon <polygon
+98
View File
@@ -0,0 +1,98 @@
/**
* Schnittführungs-Symbol im Grundriss (generatePlan → addSectionLines): jede
* Schnitt-/Ansichtsebene MIT `linePoints` erzeugt eine Strichpunkt-Linie
* (cls "section-line"), Endmarken + ein unsichtbares Pick-Band (Polygon mit
* `sectionLineId`). Ebenen OHNE Linie erzeugen nichts.
*/
import { describe, it, expect } from "vitest";
import { generatePlan } from "./generatePlan";
import type { DrawingLevel, Project } from "../model/types";
const visible = new Set(["20", "35"]);
function baseProject(levels: DrawingLevel[]): Project {
return {
id: "t",
name: "T",
lineStyles: [],
hatches: [],
components: [],
wallTypes: [],
drawingLevels: levels,
layers: [{ code: "20", name: "Wände", color: "#333", lw: 0.18, visible: true, locked: false }],
walls: [],
doors: [],
openings: [],
ceilings: [],
stairs: [],
rooms: [],
drawings2d: [],
context: [],
} as unknown as Project;
}
const floor: DrawingLevel = {
id: "eg",
name: "EG",
kind: "floor",
visible: true,
locked: false,
floorHeight: 2.6,
cutHeight: 1.0,
baseElevation: 0,
};
describe("generatePlan — Schnittführungs-Symbol", () => {
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);
});
});
+143
View File
@@ -265,6 +265,12 @@ export type Primitive =
columnId?: string; columnId?: string;
/** ID des Dachs (Roof), falls dieses (unsichtbare) Polygon der Traufe-Umriss ist (Auswahl). */ /** ID des Dachs (Roof), falls dieses (unsichtbare) Polygon der Traufe-Umriss ist (Auswahl). */
roofId?: string; 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"; kind: "line";
@@ -892,6 +898,10 @@ export function generatePlan(
// die Reihenfolge unkritisch; wir hängen sie als Referenz an. // die Reihenfolge unkritisch; wir hängen sie als Referenz an.
addContextContours(primitives, project); 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. // Schwarz-Weiss (mono): nachträglich alle Farben auf reine Tinte zwingen.
// Element-/Kategorie-/Hatch-Farben werden ignoriert, Füllungen werden weiss, // Element-/Kategorie-/Hatch-Farben werden ignoriert, Füllungen werden weiss,
// Musterlinien/Striche schwarz — so entsteht ein reiner S/W-Plan. // 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 (AA).
*/
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 * Deckkraft (0..255) der Raum-Füllung → 2-stelliges Hex-Suffix. Transluzente
* Farbfüllung entfällt — Plandarstellung schwarz/grau; Farbe bleibt späteren * Farbfüllung entfällt — Plandarstellung schwarz/grau; Farbe bleibt späteren
+7
View File
@@ -25,6 +25,9 @@ export interface SelectionSlice {
selectedColumnIds: string[]; selectedColumnIds: string[];
// Gewählte Dächer als MENGE von IDs — getrennt von den übrigen Kanälen. // Gewählte Dächer als MENGE von IDs — getrennt von den übrigen Kanälen.
selectedRoofIds: string[]; 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; setSelectedWallIds: (ids: string[]) => void;
setSelectedDrawingIds: (ids: string[]) => void; setSelectedDrawingIds: (ids: string[]) => void;
@@ -35,6 +38,7 @@ export interface SelectionSlice {
setSelectedExtrudedSolidIds: (ids: string[]) => void; setSelectedExtrudedSolidIds: (ids: string[]) => void;
setSelectedColumnIds: (ids: string[]) => void; setSelectedColumnIds: (ids: string[]) => void;
setSelectedRoofIds: (ids: string[]) => void; setSelectedRoofIds: (ids: string[]) => void;
setSelectedSectionLineId: (id: string | null) => void;
/** Auswahl (Wände + Decken + Öffnungen + Treppen + Dächer + 2D-Elemente) leeren. */ /** Auswahl (Wände + Decken + Öffnungen + Treppen + Dächer + 2D-Elemente) leeren. */
clearSelection: () => void; clearSelection: () => void;
} }
@@ -53,6 +57,7 @@ export function createSelectionSlice(
selectedExtrudedSolidIds: [], selectedExtrudedSolidIds: [],
selectedColumnIds: [], selectedColumnIds: [],
selectedRoofIds: [], selectedRoofIds: [],
selectedSectionLineId: null,
setSelectedWallIds: (ids) => set({ selectedWallIds: ids }), setSelectedWallIds: (ids) => set({ selectedWallIds: ids }),
setSelectedDrawingIds: (ids) => set({ selectedDrawingIds: ids }), setSelectedDrawingIds: (ids) => set({ selectedDrawingIds: ids }),
setSelectedCeilingIds: (ids) => set({ selectedCeilingIds: ids }), setSelectedCeilingIds: (ids) => set({ selectedCeilingIds: ids }),
@@ -62,6 +67,7 @@ export function createSelectionSlice(
setSelectedExtrudedSolidIds: (ids) => set({ selectedExtrudedSolidIds: ids }), setSelectedExtrudedSolidIds: (ids) => set({ selectedExtrudedSolidIds: ids }),
setSelectedColumnIds: (ids) => set({ selectedColumnIds: ids }), setSelectedColumnIds: (ids) => set({ selectedColumnIds: ids }),
setSelectedRoofIds: (ids) => set({ selectedRoofIds: ids }), setSelectedRoofIds: (ids) => set({ selectedRoofIds: ids }),
setSelectedSectionLineId: (id) => set({ selectedSectionLineId: id }),
clearSelection: () => clearSelection: () =>
set({ set({
selectedWallIds: [], selectedWallIds: [],
@@ -73,6 +79,7 @@ export function createSelectionSlice(
selectedExtrudedSolidIds: [], selectedExtrudedSolidIds: [],
selectedColumnIds: [], selectedColumnIds: [],
selectedRoofIds: [], selectedRoofIds: [],
selectedSectionLineId: null,
}), }),
}; };
} }