From f450a1aa0853ab7cae24355e151625451d4f6ecc Mon Sep 17 00:00:00 2001 From: Karim Date: Thu, 20 Aug 2026 20:41:28 +0200 Subject: [PATCH] =?UTF-8?q?2D:=20By-Layer/By-Object-Attribute=20f=C3=BCr?= =?UTF-8?q?=20Wand/Decke=20konsistent=20verdrahtet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolveBackground (generatePlan/shared.ts) existierte korrekt implementiert, wurde aber nirgends aufgerufen — Wand-/Decken-Poché nutzte überall fest HATCH_INK/HATCH_PAPER, die By-Layer-Hintergrundfarbe kam nie im Renderer an. Signatur an resolveForeground angeglichen (string | undefined statt erzwungenem Component.color-Fallback), damit "kein Override gesetzt" weiter auf die neutrale SIA-Poché-Konvention fällt statt auf die rohe Bauteilfarbe — sonst hätte jedes bestehende Projekt ohne gesetzten Override optisch umgeschlagen. Verdrahtet in walls.ts/ceilings.ts (Grundriss) und neu auch in splitWallLayers/splitSlabLayers (Schnitt, toSection.ts) — dort fehlte bei MEHRSCHICHTIGEM Wand-/Deckentyp zusätzlich foreground/hatchId komplett (owner.foreground/hatchSource wurden nie an resolveForeground/resolveHatchId übergeben, nur der Default lief); splitSlabLayers bekam dafür einen neuen `owner: Ceiling`-Parameter (die einschichtigen Pfade über resolveWallSectionStyle/resolveCeilingSectionStyle waren bereits korrekt). Ceiling.strokeWeight/strokeWeightSource war komplett unverdrahtet (fixe LAYER_LINE_MM-Konstante) — das Attribut-Panel bietet aber einen Editier- Umschalter dafür (identisch zur Wand). addCeilingPoche bekommt jetzt eine aufgelöste lwMm (resolveStrokeWeight, wie schon bei der Wand); die Deckenkontur bleibt weiterhin die gestrichelte Überkopf-Ansichtslinie, nur die Dicke folgt jetzt dem Override — Verhaltensänderung auch im Default-Fall, wenn eine Kategorie eine von LAYER_LINE_MM abweichende Strichstärke trägt. Bewusst NICHT angefasst: Wand-/Decken-Strichstärke im Schnitt-Umriss (SECTION_CUT_OUTLINE_MM in generateSectionPlan) — eine einzige, uniforme Konstante für ALLE Schnitt-Polygone, kein Per-Bauteil-Wert; das wirkt wie eine bewusste Zeichnungskonvention (analog zur "grob"-Vollschwarz-Poché nach SIA 400 B.9/36), nicht wie der "UI verspricht etwas, Renderer hält es nicht"- Bugmuster der übrigen Funde. Würde ausserdem eine Erweiterung von SectionCutPolygon um ein Gewichtsfeld brauchen (heute keins) — grösserer, separat zu scopender Eingriff. 6 neue Tests (splitWallLayers/splitSlabLayers mit/ohne Override, Grundriss weiterhin über die 907 Gesamttests abgedeckt). tsc -b / vitest run (907/907) / npm run build grün. --- src/plan/generatePlan.ts | 16 ++++-- src/plan/generatePlan/ceilings.ts | 18 ++++-- src/plan/generatePlan/shared.ts | 18 +++--- src/plan/generatePlan/walls.ts | 4 +- src/plan/toSection.layerOverride.test.ts | 73 ++++++++++++++++++++++++ src/plan/toSection.ts | 36 +++++++++--- 6 files changed, 140 insertions(+), 25 deletions(-) create mode 100644 src/plan/toSection.layerOverride.test.ts diff --git a/src/plan/generatePlan.ts b/src/plan/generatePlan.ts index b4a3724..e64b330 100644 --- a/src/plan/generatePlan.ts +++ b/src/plan/generatePlan.ts @@ -58,6 +58,7 @@ import { MONO_INK, SHOW_ALL, WALL_FALLBACK_MM, + resolveBackground, resolveForeground, resolveHatch, resolveHatchId, @@ -81,7 +82,7 @@ import { addReferenceLine, addWallPoche, buildWallFootprints, wallGaps } from ". // Re-Export des gemeinsamen Vokabulars — externe Konsumenten (Export-Pipeline, // PlanView, toSection.ts, toWalls3d.ts, toRenderScene.ts, …) importieren // weiterhin ausschliesslich von hier, unverändert zur Zeit vor dem Domänen-Split. -export { HATCH_INK, HATCH_PAPER, resolveForeground, resolveHatch }; +export { HATCH_INK, HATCH_PAPER, resolveBackground, resolveForeground, resolveHatch, resolveHatchId }; export type { CategoryDisplayResolver, DetailLevel, HatchRender, Primitive }; /** @@ -309,9 +310,16 @@ export function generatePlan( for (const ceiling of ceilings) { const greyed = categoryDisplay(ceiling.categoryCode).greyed; const category = catByCode.get(ceiling.categoryCode); - // Kein lwMm mehr: der Decken-Umriss ist eine feste Überkopf-Haarlinie - // (gestrichelt), unabhängig von der Kategorie-Strichstärke. - addCeilingPoche(primitives, project, ceiling, greyed, detail, category, wallFootprints); + // Strichstärke wie bei der Wand (Attribut-Override > Kategorie > Fallback); + // der Umriss bleibt trotzdem eine gestrichelte Überkopf-Ansichtslinie + // (Dash-Muster unverändert, s. addCeilingPoche) — nur die Dicke ist editierbar. + const ceilingLwMm = resolveStrokeWeight( + ceiling.strokeWeight, + ceiling.strokeWeightSource, + category, + lwByCode.get(ceiling.categoryCode) ?? LAYER_LINE_MM, + ); + addCeilingPoche(primitives, project, ceiling, greyed, detail, category, wallFootprints, ceilingLwMm); } // Dächer dieses Geschosses (Dachaufsicht): reine Grundriss-Linien (Traufe/ diff --git a/src/plan/generatePlan/ceilings.ts b/src/plan/generatePlan/ceilings.ts index f775de7..6e5dfeb 100644 --- a/src/plan/generatePlan/ceilings.ts +++ b/src/plan/generatePlan/ceilings.ts @@ -13,8 +13,8 @@ import { OVERHEAD_DASH, POCHE_STROKE, LAYER_DETAIL_FACTOR, - LAYER_LINE_MM, pocheFill, + resolveBackground, resolveForeground, resolveHatch, resolveHatchId, @@ -38,7 +38,8 @@ import { segmentInsideWallBand, subtractIntervals, type WallFootprint } from "./ * hier. Im Detailgrad „grob" entfällt die Schraffur (nur Fläche + Umriss); * „mittel"/„fein" zeigen die Ansichts-Schraffur des Bauteils (`viewHatchId`, * Default weiss) — die frontal gesehene Aufsicht ist kein Schnitt. `lwMm` ist - * die Kategorie-Strichstärke. + * die aufgelöste Umriss-Strichstärke (Attribut-Override > Kategorie > Fallback, + * vom Aufrufer via `resolveStrokeWeight` bestimmt, analog zur Wand). */ export function addCeilingPoche( out: Primitive[], @@ -48,6 +49,7 @@ export function addCeilingPoche( detail: DetailLevel, category: LayerCategory | undefined, wallFootprints: WallFootprint[], + lwMm: number, ): void { const pts = ceiling.outline; if (pts.length < 3) return; @@ -86,6 +88,11 @@ export function addCeilingPoche( resolveForeground(comp, ceiling.foreground, category, ceiling.foregroundSource), ) : NO_HATCH; + // Poché-Füllfarbe: Attribut-Override (By-Layer/By-Object) hat Vorrang vor der + // neutralen SIA-Fläche, analog zur Wand-Poché (siehe walls.ts). + const bgOverride = comp + ? resolveBackground(comp, ceiling.background, category, ceiling.backgroundSource) + : undefined; // Gefüllte Fläche OHNE eigenen Umriss (stroke:"none"): die Decke liegt unter // der Wand-Poché; ihre Kante wird separat als verdeckungs-geclippte Linie // gezeichnet (s. u.), damit sie nicht durch die darüberstehenden Wände schlägt. @@ -93,7 +100,7 @@ export function addCeilingPoche( out.push({ kind: "polygon", pts: ring, - fill: comp ? pocheFill(hatch.pattern) : "none", + fill: comp ? bgOverride ?? pocheFill(hatch.pattern) : "none", stroke: "none", strokeWidthMm: 0, hatch, @@ -128,8 +135,9 @@ export function addCeilingPoche( b: add(a, scale(d, t1)), cls: "ceiling-outline", // Überkopf-Ansichtslinie (Decke liegt als Slab über der Schnittebene) → - // gestrichelte Haarlinie statt kräftiger Volllinie (BIM-Konvention). - weightMm: LAYER_LINE_MM * LAYER_DETAIL_FACTOR[detail], + // gestrichelt statt durchgezogen (BIM-Konvention) — die DICKE folgt + // aber ceiling.strokeWeight/-Source wie bei der Wand (s. Aufrufer). + weightMm: lwMm * LAYER_DETAIL_FACTOR[detail], dash: OVERHEAD_DASH, color: stroke, greyed, diff --git a/src/plan/generatePlan/shared.ts b/src/plan/generatePlan/shared.ts index f7e1ed1..4e905e4 100644 --- a/src/plan/generatePlan/shared.ts +++ b/src/plan/generatePlan/shared.ts @@ -438,21 +438,25 @@ export function resolveForeground( } /** - * Kollabiert die Hintergrund-/Füllfarbkette (Poché) eines Bauteils, analog zu - * {@link resolveForeground}: + * Kollabiert die Hintergrund-/Füllfarbkette (Poché-Override) eines Bauteils zu + * einem einzelnen Wert, exakt analog zu {@link resolveForeground}: * (Attribut-Override `override`) ?? (`source==="layer"` ⇒ `category.color`) ?? - * (`Component.background`) ?? `Component.color`. - * Immer definiert (endet spätestens bei `Component.color` = heutiges Verhalten). + * (`Component.background`) ?? `undefined`. + * `undefined` als Endergebnis heißt „kein Override gesetzt" — Aufrufer fallen + * dann auf ihren eigenen Default zurück (bei Wand/Decke die neutrale SIA- + * Poché-Farbe HATCH_INK/HATCH_PAPER, NICHT `Component.color` — die reale + * Bauteilfarbe soll die Schnitt-Konvention nicht überschreiben, nur ein + * bewusster Override). */ export function resolveBackground( comp: Component, override?: string, category?: LayerCategory, source?: AttributeSource, -): string { +): string | undefined { if (override != null) return override; - if (source === "layer") return category?.color ?? comp.background ?? comp.color; - return comp.background ?? comp.color; + if (source === "layer") return category?.color ?? comp.background; + return comp.background; } /** diff --git a/src/plan/generatePlan/walls.ts b/src/plan/generatePlan/walls.ts index d75b8b6..bd70b4f 100644 --- a/src/plan/generatePlan/walls.ts +++ b/src/plan/generatePlan/walls.ts @@ -28,6 +28,7 @@ import { NO_HATCH, OUTLINE_DETAIL_FACTOR, POCHE_STROKE, + resolveBackground, resolveForeground, resolveHatch, resolveHatchId, @@ -341,6 +342,7 @@ export function addWallPoche( // Schichtschraffur (fest in HATCH_INK, siehe resolveHatch) liegt darüber; // Vollmuster ("solid") = Tinten-Poché. const layerSolid = layerHatch.pattern === "solid"; + const bgOverride = resolveBackground(comp, wall.background, category, wall.backgroundSource); const layerStartCut = s <= 1e-6 ? (layerCuts ? layerCuts.start[li] : startCut) : null; const layerEndCut = e >= axisLen - 1e-6 ? (layerCuts ? layerCuts.end[li] : endCut) : null; // Durchgangswand-Aussparung: liegt der Quer-Offsetbereich dieses Bandes @@ -363,7 +365,7 @@ export function addWallPoche( out.push({ kind: "polygon", pts: clippedBand(axisPt(a), axisPt(b), off, off + layer.thickness, sc, ec), - fill: layerSolid ? HATCH_INK : HATCH_PAPER, + fill: bgOverride ?? (layerSolid ? HATCH_INK : HATCH_PAPER), // Band ohne Umriss: keine Schichtfuge über die Bandkante, keine 45°-Naht // am Knoten. Die Fugen kommen als separate `line`-Primitive (siehe unten). stroke: "none", diff --git a/src/plan/toSection.layerOverride.test.ts b/src/plan/toSection.layerOverride.test.ts new file mode 100644 index 0000000..c18ed87 --- /dev/null +++ b/src/plan/toSection.layerOverride.test.ts @@ -0,0 +1,73 @@ +/** + * `splitWallLayers`/`splitSlabLayers` — Attribut-Override (By-Layer/By-Object) + * bei MEHRSCHICHTIGEM Wand-/Deckentyp im Schnitt. Vorher wurde `owner.foreground`/ + * `hatchId`/`hatchSource`/`background`/`backgroundSource` hier komplett ignoriert + * (nur der einschichtige Pfad über resolveWallSectionStyle/resolveCeilingSectionStyle + * respektierte den Override) — Regressionsschutz für den Nachzieh-Fix + * (2D-Vervollständigung, Fund D). + */ + +import { describe, it, expect } from "vitest"; +import { sampleProject } from "../model/sampleProject"; +import { getCeilingType, getWallType } from "../model/types"; +import { splitWallLayers, splitSlabLayers, type SectionCutPolygon } from "./toSection"; + +const cp: SectionCutPolygon = { + component: { kind: "wall", index: 0 }, + color: [0.5, 0.5, 0.5], + pts: [ + [0, 0], + [1, 0], + [1, 2], + [0, 2], + ], +}; + +describe("splitWallLayers — Attribut-Override greift auf ALLE Schichten", () => { + const wall = sampleProject.walls.find((w) => w.id === "W1")!; // wallTypeId "aw", 4 Schichten + const wt = getWallType(sampleProject, wall); + + it("Testvoraussetzung: Wandtyp ist tatsächlich mehrschichtig", () => { + expect(wt.layers.length).toBeGreaterThan(1); + }); + + it("ohne Override: Füllung bleibt SIA-neutral, nicht die Override-Testfarbe", () => { + const bands = splitWallLayers(cp, sampleProject, wt, wall, { x: 1, y: 0 }); + expect(bands.length).toBe(wt.layers.length); + for (const b of bands) { + expect(typeof b.fill).toBe("string"); + expect(b.fill).not.toBe("#ff00ff"); + } + }); + + it("mit explizitem background-Override: JEDE Schicht bekommt die Override-Farbe", () => { + const overridden = { ...wall, background: "#ff00ff" }; + const bands = splitWallLayers(cp, sampleProject, wt, overridden, { x: 1, y: 0 }); + expect(bands.length).toBe(wt.layers.length); + for (const b of bands) expect(b.fill).toBe("#ff00ff"); + }); +}); + +describe("splitSlabLayers — Attribut-Override greift auf ALLE Schichten", () => { + const ceiling = (sampleProject.ceilings ?? []).find((c) => c.id === "C1")!; // ceilingTypeId "dg-massiv" + const wt = getCeilingType(sampleProject, ceiling); + + it("Testvoraussetzung: Deckentyp ist tatsächlich mehrschichtig", () => { + expect(wt.layers.length).toBeGreaterThan(1); + }); + + it("mit explizitem background-Override: JEDE Schicht bekommt die Override-Farbe", () => { + const overridden = { ...ceiling, background: "#00ffff" }; + const bands = splitSlabLayers(cp, sampleProject, wt, overridden); + expect(bands.length).toBe(wt.layers.length); + for (const b of bands) expect(b.fill).toBe("#00ffff"); + }); + + it("mit backgroundSource:'layer': category.color wird für ALLE Schichten übernommen", () => { + const category = sampleProject.layers.find((l) => l.code === ceiling.categoryCode)!; + const overridden = { ...ceiling, backgroundSource: "layer" as const }; + const bands = splitSlabLayers(cp, sampleProject, wt, overridden); + expect(bands.length).toBe(wt.layers.length); + for (const b of bands) expect(b.fill).toBe(category.color); + }); +}); diff --git a/src/plan/toSection.ts b/src/plan/toSection.ts index fe95485..e60ef7c 100644 --- a/src/plan/toSection.ts +++ b/src/plan/toSection.ts @@ -18,7 +18,7 @@ // v = absolute Höhe (world.y). Siehe src-tauri/render3d/src/section.rs. import type { Ceiling, DrawingLevel, Project, Vec2, Wall } from "../model/types"; -import { getCeilingType, getComponent, getWallType, openingsOfWall, roofLayers } from "../model/types"; +import { getCeilingType, getComponent, getLayerCategory, getWallType, openingsOfWall, roofLayers } from "../model/types"; import { ceilingVerticalExtent, wallVerticalExtent } from "../model/wall"; import { dot, leftNormal, normalize, sub } from "../model/geometry"; import { openingInterval, openingVerticalExtent } from "../geometry/opening"; @@ -28,9 +28,11 @@ import { loadEngine3d } from "../engine/engine3d"; import { HATCH_INK, HATCH_PAPER, + resolveBackground, resolveCeilingSectionStyle, resolveForeground, resolveHatch, + resolveHatchId, resolveWallSectionStyle, } from "./generatePlan"; import type { HatchRender } from "./generatePlan"; @@ -343,7 +345,7 @@ function attachCutStyles(output: SectionOutput, project: Project, plane: Section if (owner) { const wt = getCeilingType(project, owner); if (wt.layers.length > 1) { - bands.push(...splitSlabLayers(cp, project, wt)); + bands.push(...splitSlabLayers(cp, project, wt, owner)); continue; } const style = resolveCeilingSectionStyle(project, owner); @@ -761,10 +763,11 @@ function bandFromRect(src: SectionCutPolygon, r: Rect): SectionCutPolygon { * aneinanderstoßenden Rechtecken (jedes bekommt in `generateSectionPlan` die * generische Schnitt-Umrisslinie). */ -function splitSlabLayers( +export function splitSlabLayers( cp: SectionCutPolygon, project: Project, wt: ReturnType, + owner: Ceiling, ): SectionCutPolygon[] { const totalT = wt.layers.reduce((s, l) => s + l.thickness, 0); const us = cp.pts.map((p) => p[0]); @@ -776,17 +779,23 @@ function splitSlabLayers( const vSpan = vMax - vMin; if (totalT <= 1e-9 || vSpan <= 1e-9) return [cp]; const scale = vSpan / totalT; + // Attribut-Override der Decke (By-Layer/By-Object) gilt uniform für ALLE + // Schichten — analog zu splitWallLayers und dem Grundriss (addCeilingPoche). + const category = getLayerCategory(project, owner.categoryCode); const out: SectionCutPolygon[] = []; let acc = 0; // kumulierte Dicke von OK (oben) nach UK (unten) for (const layer of wt.layers) { const comp = getComponent(project, layer.componentId); + const hatchId = + resolveHatchId(owner.hatchId, owner.hatchSource, category, comp.hatchId) ?? comp.hatchId; const hatch = resolveHatch( project, - comp.hatchId, + hatchId, SECTION_SLAB_AXIS_ANGLE_DEG, - resolveForeground(comp), + resolveForeground(comp, owner.foreground, category, owner.foregroundSource), ); + const bgOverride = resolveBackground(comp, owner.background, category, owner.backgroundSource); const vTop = vMax - acc * scale; acc += layer.thickness; const vBottom = vMax - acc * scale; @@ -799,7 +808,7 @@ function splitSlabLayers( [uMax, vBottom], [uMin, vBottom], ], - fill: hatch.pattern === "solid" ? HATCH_INK : HATCH_PAPER, + fill: bgOverride ?? (hatch.pattern === "solid" ? HATCH_INK : HATCH_PAPER), hatch, joinPriority: comp.joinPriority, componentId: comp.id, @@ -870,6 +879,9 @@ export function splitWallLayers( // Aussen/Innen-Orientierung: per Default liegt layers[0] an uMin; zeigt die // Wand-Aufbaurichtung gegen die Schnitt-u-Achse, werden die Bänder gespiegelt. const layers = wallLayersReversedInU(owner, uWorld) ? [...wt.layers].reverse() : wt.layers; + // Attribut-Override der Wand (By-Layer/By-Object) gilt uniform für ALLE + // Schichten — analog zum Grundriss (addWallPoche in walls.ts). + const category = getLayerCategory(project, owner.categoryCode); const out: SectionCutPolygon[] = []; let acc = 0; // kumulierte Dicke von uMin (erste einsortierte Schicht) nach uMax @@ -880,7 +892,15 @@ export function splitWallLayers( // entlang v, Modellwinkel 90°). Wie im Grundriss (`addWallPoche` übergibt // die Wandachse) wird der Achswinkel mitgegeben, sonst liefe die Dämmung // vertikal statt quer zur Wanddicke (horizontal). - const hatch = resolveHatch(project, comp.hatchId, SECTION_WALL_AXIS_ANGLE_DEG, resolveForeground(comp)); + const hatchId = + resolveHatchId(owner.hatchId, owner.hatchSource, category, comp.hatchId) ?? comp.hatchId; + const hatch = resolveHatch( + project, + hatchId, + SECTION_WALL_AXIS_ANGLE_DEG, + resolveForeground(comp, owner.foreground, category, owner.foregroundSource), + ); + const bgOverride = resolveBackground(comp, owner.background, category, owner.backgroundSource); const uLeft = uMin + acc * scale; acc += layer.thickness; const uRight = uMin + acc * scale; @@ -893,7 +913,7 @@ export function splitWallLayers( [uRight, vMin], [uLeft, vMin], ], - fill: hatch.pattern === "solid" ? HATCH_INK : HATCH_PAPER, + fill: bgOverride ?? (hatch.pattern === "solid" ? HATCH_INK : HATCH_PAPER), hatch, joinPriority: comp.joinPriority, componentId: comp.id,