2D: By-Layer/By-Object-Attribute für Wand/Decke konsistent verdrahtet
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.
This commit is contained in:
@@ -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/
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
+28
-8
@@ -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<typeof getCeilingType>,
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user