Editieren im Schnitt: Cut-Polygone tragen die Quell-Element-Id

toSection loest je Cut-Band die Quell-Element-Id (Wand/Decke/Dach) ueber die
Besitzer-Listen auf (cp.sourceId) und propagiert sie durch Schicht-Zerlegung,
Terminierung und Boolean-Dominanz. generateSectionPlan schreibt sie je
component.kind als wallId/ceilingId/roofId ans Plan-Polygon — ein Klick im
Schnitt waehlt damit exakt jenes Bauteil im Modell (bestehende PlanView-Pick-/
Highlight-Logik greift), das Attribut-Panel editiert OK/UK/Hoehe, der Schnitt
rechnet neu. Tests fuer Mapping + sourceId-Propagation.
This commit is contained in:
2026-07-11 00:30:01 +02:00
parent ef7eb295d9
commit d43e2c151e
4 changed files with 89 additions and 0 deletions
+9
View File
@@ -995,6 +995,12 @@ export function generateSectionPlan(output: SectionOutput, mono = false): Plan {
const monoFill = solid ? HATCH_INK : HATCH_PAPER; const monoFill = solid ? HATCH_INK : HATCH_PAPER;
const monoHatch = const monoHatch =
solid ? hatch : hatch.pattern !== "none" ? { ...hatch, color: SECTION_INK } : hatch; solid ? hatch : hatch.pattern !== "none" ? { ...hatch, color: SECTION_INK } : hatch;
// Auswahl im Schnitt: das QUELL-Element (via cp.sourceId + component.kind) als
// wallId/ceilingId/roofId ans Polygon hängen — so wählt ein Klick auf das
// geschnittene Bauteil im Schnitt exakt jene Wand/Decke/Dach im Modell, und
// die bestehende PlanView-Pick-/Highlight-Logik greift ohne Sonderfall.
const kind = cp.component.kind;
const sourceId = cp.sourceId;
primitives.push({ primitives.push({
kind: "polygon", kind: "polygon",
pts, pts,
@@ -1002,6 +1008,9 @@ export function generateSectionPlan(output: SectionOutput, mono = false): Plan {
stroke: SECTION_INK, stroke: SECTION_INK,
strokeWidthMm: SECTION_CUT_OUTLINE_MM, strokeWidthMm: SECTION_CUT_OUTLINE_MM,
hatch: mono ? monoHatch : hatch, hatch: mono ? monoHatch : hatch,
wallId: kind === "wall" ? sourceId : undefined,
ceilingId: kind === "slab" ? sourceId : undefined,
roofId: kind === "roof" ? sourceId : undefined,
}); });
} }
@@ -0,0 +1,58 @@
// Auswahl im Schnitt-View: generateSectionPlan hängt jedem Cut-Polygon die
// Quell-Element-Id (cp.sourceId) je nach component.kind als wallId/ceilingId/
// roofId ans Plan-Polygon — damit ein Klick im Schnitt das Bauteil im Modell
// wählt (die bestehende PlanView-Pick-Logik greift dann ohne Sonderfall).
import { describe, it, expect } from "vitest";
import { generateSectionPlan } from "./generatePlan";
import type { SectionCutPolygon, SectionOutput } from "./toSection";
function cut(kind: "wall" | "slab" | "roof", sourceId: string): SectionCutPolygon {
return {
component: { kind, index: 0 },
color: [0.5, 0.5, 0.5],
pts: [
[0, 0],
[1, 0],
[1, 2],
[0, 2],
],
sourceId,
};
}
const output: SectionOutput = {
cutPolygons: [cut("wall", "W1"), cut("slab", "C1"), cut("roof", "R1")],
visibleEdges: [],
hiddenEdges: [],
};
describe("generateSectionPlan — Auswahl-Ids am Cut-Polygon", () => {
it("schreibt sourceId je component.kind als wallId/ceilingId/roofId", () => {
const polys = generateSectionPlan(output).primitives.filter(
(p): p is Extract<typeof p, { kind: "polygon" }> => p.kind === "polygon",
);
const wall = polys.find((p) => p.wallId === "W1");
const ceiling = polys.find((p) => p.ceilingId === "C1");
const roof = polys.find((p) => p.roofId === "R1");
expect(wall).toBeTruthy();
expect(ceiling).toBeTruthy();
expect(roof).toBeTruthy();
// Kanäle schließen sich je Polygon aus (eine Wand trägt keine ceilingId).
expect(wall?.ceilingId).toBeUndefined();
expect(wall?.roofId).toBeUndefined();
expect(ceiling?.wallId).toBeUndefined();
});
it("lässt die Ids weg, wenn cp.sourceId fehlt (unauflösbares Bauteil)", () => {
const noId: SectionOutput = {
cutPolygons: [{ ...cut("wall", "W1"), sourceId: undefined }],
visibleEdges: [],
hiddenEdges: [],
};
const poly = generateSectionPlan(noId).primitives.find(
(p): p is Extract<typeof p, { kind: "polygon" }> => p.kind === "polygon",
);
expect(poly?.wallId).toBeUndefined();
});
});
+7
View File
@@ -112,6 +112,13 @@ describe("splitWallLayers: Aussenputz auf der Aussenseite, Wände spiegelverkehr
expect(bands[3].width).toBeCloseTo(RENDER_EXT_T, 6); expect(bands[3].width).toBeCloseTo(RENDER_EXT_T, 6);
}); });
it("propagiert sourceId (Quell-Element) auf ALLE Schicht-Bänder", () => {
const cpWithId: SectionCutPolygon = { ...cp, sourceId: "W2" };
const bands = splitWallLayers(cpWithId, sampleProject, aw, wall("W2"), uWorld);
expect(bands.length).toBeGreaterThan(1);
expect(bands.every((b) => b.sourceId === "W2")).toBe(true);
});
it("W2 und W4 liegen spiegelverkehrt (nicht identisch)", () => { it("W2 und W4 liegen spiegelverkehrt (nicht identisch)", () => {
const b2 = bandsByU(splitWallLayers(cp, sampleProject, aw, wall("W2"), uWorld)); const b2 = bandsByU(splitWallLayers(cp, sampleProject, aw, wall("W2"), uWorld));
const b4 = bandsByU(splitWallLayers(cp, sampleProject, aw, wall("W4"), uWorld)); const b4 = bandsByU(splitWallLayers(cp, sampleProject, aw, wall("W4"), uWorld));
+15
View File
@@ -76,6 +76,15 @@ export interface SectionCutPolygon {
* Wert (unauflösbares Bauteil), nimmt das Band an keiner Verschmelzung teil. * Wert (unauflösbares Bauteil), nimmt das Band an keiner Verschmelzung teil.
*/ */
componentId?: string; componentId?: string;
/**
* ID des QUELL-Elements dieses Cut-Bands im Dokumentmodell (Wand/Decke/Dach) —
* aufgelöst in `attachCutStyles` über die Besitzer-Listen. Trägt die Auswahl im
* Schnitt-View: `generateSectionPlan` schreibt sie je nach `component.kind` als
* `wallId`/`ceilingId`/`roofId` ins Plan-Polygon, sodass ein Klick auf das
* geschnittene Bauteil im Schnitt exakt jenes Element im Grundriss-Modell wählt.
* Fehlt (unauflösbares Bauteil), bleibt das Band ohne Auswahlbezug.
*/
sourceId?: string;
} }
/** Ein projiziertes Liniensegment in (u, v)-Metern. */ /** Ein projiziertes Liniensegment in (u, v)-Metern. */
@@ -294,11 +303,13 @@ function attachCutStyles(output: SectionOutput, project: Project, plane: Section
// aufgelöst; keine Boolean-Dominanz (joinPriority undefined) — unverändert // aufgelöst; keine Boolean-Dominanz (joinPriority undefined) — unverändert
// übernehmen. WICHTIG vor dem Slab-Zweig (sonst falscher owner-Lookup). // übernehmen. WICHTIG vor dem Slab-Zweig (sonst falscher owner-Lookup).
if (ref.kind === "roof") { if (ref.kind === "roof") {
cp.sourceId = (project.roofs ?? [])[ref.index]?.id;
bands.push(cp); bands.push(cp);
continue; continue;
} }
if (ref.kind === "wall") { if (ref.kind === "wall") {
const owner = walls[ref.index]; const owner = walls[ref.index];
cp.sourceId = owner?.id;
// Wand: bei mehrschichtigem Wandtyp wird das EINE geschnittene Rechteck in // Wand: bei mehrschichtigem Wandtyp wird das EINE geschnittene Rechteck in
// stehende Teil-Rechtecke je Schicht quer zur Dicke (u) aufgeteilt — das // stehende Teil-Rechtecke je Schicht quer zur Dicke (u) aufgeteilt — das
// vertikale Gegenstück zur Decken-Zerlegung (dort quer zur Höhe v), siehe // vertikale Gegenstück zur Decken-Zerlegung (dort quer zur Höhe v), siehe
@@ -327,6 +338,7 @@ function attachCutStyles(output: SectionOutput, project: Project, plane: Section
// Schnitt sichtbar (im Grundriss bleibt die Decke bewusst flächig, siehe // Schnitt sichtbar (im Grundriss bleibt die Decke bewusst flächig, siehe
// `addCeilingPoche`). // `addCeilingPoche`).
const owner = slabs[ref.index]; const owner = slabs[ref.index];
cp.sourceId = owner?.id;
if (owner) { if (owner) {
const wt = getCeilingType(project, owner); const wt = getCeilingType(project, owner);
if (wt.layers.length > 1) { if (wt.layers.length > 1) {
@@ -727,6 +739,7 @@ function bandFromRect(src: SectionCutPolygon, r: Rect): SectionCutPolygon {
hatch: src.hatch, hatch: src.hatch,
joinPriority: src.joinPriority, joinPriority: src.joinPriority,
componentId: src.componentId, componentId: src.componentId,
sourceId: src.sourceId,
}; };
} }
@@ -779,6 +792,7 @@ function splitSlabLayers(
hatch, hatch,
joinPriority: comp.joinPriority, joinPriority: comp.joinPriority,
componentId: comp.id, componentId: comp.id,
sourceId: cp.sourceId,
}); });
} }
return out; return out;
@@ -865,6 +879,7 @@ export function splitWallLayers(
hatch, hatch,
joinPriority: comp.joinPriority, joinPriority: comp.joinPriority,
componentId: comp.id, componentId: comp.id,
sourceId: cp.sourceId,
}); });
} }
return out; return out;