From a917e4149a14e6cf361ceb52bc01f20f60ab71bf Mon Sep 17 00:00:00 2001 From: Karim Date: Sat, 22 Aug 2026 01:13:47 +0200 Subject: [PATCH] =?UTF-8?q?St=C3=BCtze=20(Column)=20bekommt=20dasselbe=20B?= =?UTF-8?q?y-Layer/By-Object-Attributsystem=20wie=20Wand/Decke?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nutzer-Wunsch: "generell sollten alle BIM Teile also 3D die weiteren Attribute haben." Untersucht: Dach/Treppe/Raum/Öffnung haben HEUTE keine Poché-Füllung/Schraffur im Grundriss (reine Linien-/Symbol- Darstellung, bei Raum sogar bewusst so entschieden -- "keine Farbwäsche im Plan"); das Attribut-System dort anzuschliessen würde neue Poché-/Schraffur-Fähigkeiten erfordern, die es dort noch gar nicht gibt. Stütze dagegen hat bereits eine Poché UND nutzte bereits dieselben Resolver wie Wand/Decke (nur mit fest verdrahtetem override=source=undefined) -- Nutzer-Entscheidung nach Rückfrage: nur Stütze jetzt fertig verdrahten, der Rest bleibt vorerst unverändert. model/types.ts: Column bekommt dieselben acht Felder wie Wall (foreground/background/hatchId/strokeWeight × je einer *Source). plan/generatePlan/structures.ts (addColumnPoche): resolveHatchId/ resolveForeground lesen jetzt column.hatchId/column.hatchSource bzw. column.foreground/column.foregroundSource statt hartcodiertem undefined; neuer resolveBackground-Aufruf lässt einen expliziten Hintergrund-Override vor die neutrale SIA-Poché (Tinte/Papier je nach Schraffur-Muster) treten -- exakt das addWallPoche-Muster (bgOverride ?? neutrale Poché). plan/generatePlan.ts: die Strichstärke der Stütze lief bisher IMMER mit hartcodiertem override=source=undefined durch resolveStrokeWeight -- liest jetzt column.strokeWeight/column.strokeWeightSource. state/selectionInfo.ts (columnSelection): effectiveForeground/ effectiveBackground/effectiveHatchId analog wallSelection (Bauteil- Repräsentant ist hier `column.componentId` statt der äussersten Schicht); weightMm war bisher hart auf WALL_FALLBACK_MM gesetzt, läuft jetzt korrekt durch resolveStrokeWeight. panels/AttributesPanel.tsx: weightEditable/fillEditable/pocheEditable um `sel.kind === "column"` erweitert -- Stütze zeigt jetzt dieselben vier Attribut-Zeilen mit vollem 3-Quellen-Dropdown (Nach Ebene/Nach Bauteil/eigener Wert) wie Wand/Decke. +4 Tests (structures.column.test.ts: Default bleibt Bauteil-Schraffur, Hintergrund-Override greift, hatchSource "layer" nutzt die Kategorie- Schraffur, expliziter hatchId-Override gewinnt). tsc/vitest 938/938 grün. --- src/model/types.ts | 30 +++++ src/panels/AttributesPanel.tsx | 19 ++- src/plan/generatePlan.ts | 8 +- .../generatePlan/structures.column.test.ts | 119 ++++++++++++++++++ src/plan/generatePlan/structures.ts | 11 +- src/state/selectionInfo.ts | 38 +++++- 6 files changed, 213 insertions(+), 12 deletions(-) create mode 100644 src/plan/generatePlan/structures.column.test.ts diff --git a/src/model/types.ts b/src/model/types.ts index 7291cf5..928c799 100644 --- a/src/model/types.ts +++ b/src/model/types.ts @@ -1721,6 +1721,36 @@ export interface Column { * Kategorie-Farbe. */ color?: string; + /** + * Attribut-Override der Muster-/Schraffurfarbe (Vordergrund) DIESER Stütze. + * `undefined` = „Nach System" (erben → Component.foreground), analog `Wall. + * foreground`. + */ + foreground?: string; + /** + * Attribut-Override der Füllfarbe (Hintergrund/Poché) DIESER Stütze. + * `undefined` = „Nach System" (neutrale SIA-Poché HATCH_INK/HATCH_PAPER je + * nach Schraffur-Muster), analog `Wall.background`. + */ + background?: string; + /** + * Attribut-Override der Strichstärke (mm Papier) DIESER Stütze. `undefined` + * = „Nach System" (erben → `strokeWeightSource`), analog `Wall.strokeWeight`. + */ + strokeWeight?: number; + /** + * Attribut-Override der Schnitt-Schraffur (Hatch Manager) DIESER Stütze. + * `undefined` = „Nach System" (erben → `hatchSource`), analog `Wall.hatchId`. + */ + hatchId?: string; + /** Quelle des Vordergrunds, wenn kein explizites `foreground` gesetzt ist — analog `Wall.foregroundSource`. */ + foregroundSource?: AttributeSource; + /** Quelle des Hintergrunds, analog {@link Column.foregroundSource}. */ + backgroundSource?: AttributeSource; + /** Quelle der Strichstärke, analog {@link Column.foregroundSource}. */ + strokeWeightSource?: AttributeSource; + /** Quelle der Schraffur, analog {@link Column.foregroundSource}. */ + hatchSource?: AttributeSource; } // ── Kontext-Geometrie (importiert / abgeleitet, NICHT semantisch) ─────────── diff --git a/src/panels/AttributesPanel.tsx b/src/panels/AttributesPanel.tsx index 3e240b8..61d3aee 100644 --- a/src/panels/AttributesPanel.tsx +++ b/src/panels/AttributesPanel.tsx @@ -121,29 +121,36 @@ export function AttributesPanel() { // sind IMMER ein eigener Wert (kein 3-Quellen-Dropdown, keine Füllung/ // Schraffur/Masse — s. Ausblendungen weiter unten). const isAnnotation = sel.kind === "layoutAnnotation"; - // Strichstärke: Wand/Decke/Drawing2D tragen einen eigenen Override + // Strichstärke: Wand/Decke/Stütze/Drawing2D tragen einen eigenen Override // (`strokeWeight`/`weightMm`) + Quellen-Dropdown; andere Elementarten erben // stets aus der Kategorie (kein Setter im Kontrakt → nicht editierbar). Bei // Annotationen nur Linie/Rechteck (Text/Bild kennen keine Strichstärke). const weightEditable = sel.kind === "wall" || sel.kind === "ceiling" || + sel.kind === "column" || isDrawing || (isAnnotation && (sel.annotKind === "line" || sel.annotKind === "rect")); - // Schraffur: Wand/Decke (Schnitt-/Ansichts-Schraffur) oder geschlossene 2D-Form. + // Schraffur: Wand/Decke/Stütze (Schnitt-/Ansichts-Schraffur) oder + // geschlossene 2D-Form. const fillEditable = - sel.kind === "wall" || sel.kind === "ceiling" || (isDrawing && sel.closed === true); - // Vordergrund/Hintergrund (Muster-/Füllfarbe) tragen Wand, Decke und + sel.kind === "wall" || + sel.kind === "ceiling" || + sel.kind === "column" || + (isDrawing && sel.closed === true); + // Vordergrund/Hintergrund (Muster-/Füllfarbe) tragen Wand, Decke, Stütze und // geschlossene 2D-Formen als Override; `undefined` = „Nach System". const pocheEditable = sel.kind === "wall" || sel.kind === "ceiling" || + sel.kind === "column" || (isDrawing && sel.closed === true); // Ein Drawing2D hat KEIN eigenes Bauteil (Component) — „Nach Bauteil" wäre // dort bedeutungslos (resolveForeground & Co. fallen dann auf gar keinen - // Wert zurück, s. selectionInfo.ts `noComponent`). Nur Wand/Decke bieten - // die Option daher an. + // Wert zurück, s. selectionInfo.ts `noComponent`). Wand/Decke/Stütze haben + // alle ein echtes Bauteil (Schicht-Component bzw. `Column.componentId`) und + // bieten die Option daher an. const allowObjectSource = !isDrawing; // Der Quellen-Dropdown (Nach Ebene/Nach Bauteil/eigener Wert), gemeinsam für diff --git a/src/plan/generatePlan.ts b/src/plan/generatePlan.ts index e64b330..546c046 100644 --- a/src/plan/generatePlan.ts +++ b/src/plan/generatePlan.ts @@ -437,7 +437,13 @@ export function generatePlan( const category = catByCode.get(column.categoryCode); const ov = overrideFor(column.categoryCode, columnLabel(column)); const lwMm = - ov.lineweight ?? resolveStrokeWeight(undefined, undefined, category, lwByCode.get(column.categoryCode) ?? WALL_FALLBACK_MM); + ov.lineweight ?? + resolveStrokeWeight( + column.strokeWeight, + column.strokeWeightSource, + category, + lwByCode.get(column.categoryCode) ?? WALL_FALLBACK_MM, + ); const effCol = ov.color !== undefined ? { ...column, color: ov.color } : column; addColumnPoche(primitives, project, effCol, greyed, lwMm, category); } diff --git a/src/plan/generatePlan/structures.column.test.ts b/src/plan/generatePlan/structures.column.test.ts new file mode 100644 index 0000000..b28d2b6 --- /dev/null +++ b/src/plan/generatePlan/structures.column.test.ts @@ -0,0 +1,119 @@ +/** + * `addColumnPoche` — By-Layer/By-Object-Override für Stützen (Nutzer-Wunsch: + * "generell sollten alle BIM Teile ... die weiteren Attribute haben" — Stütze + * hat bereits eine Poché UND nutzt dieselben Resolver wie Wand/Decke, daher + * hier zuerst fertig verdrahtet). Regressionsschutz: die neuen + * foreground/background/hatchId/strokeWeight(+Source)-Felder auf `Column` + * müssen tatsächlich bis in die gerenderte Poché durchschlagen. + */ + +import { describe, it, expect } from "vitest"; +import { addColumnPoche } from "./structures"; +import type { Column, Component, LayerCategory, Project } from "../../model/types"; +import type { Primitive } from "./shared"; + +const comp: Component = { + id: "beton", + name: "Beton", + color: "#888888", + hatchId: "sia-concrete", + joinPriority: 10, +}; + +const category: LayerCategory = { + code: "50", + name: "Tragwerk", + color: "#333333", + lw: 0.35, + visible: true, + locked: false, +}; + +function project(overrides: Partial = {}): Project { + return { + id: "t", + name: "T", + lineStyles: [], + hatches: [ + { id: "sia-concrete", name: "Beton", pattern: "solid" }, + { id: "sia-other", name: "Sonstiges", pattern: "none" }, + ] as unknown as Project["hatches"], + components: [comp], + wallTypes: [], + drawingLevels: [], + layers: [category], + walls: [], + doors: [], + openings: [], + ceilings: [], + stairs: [], + rooms: [], + drawings2d: [], + context: [], + columns: [], + ...overrides, + }; +} + +function column(overrides: Partial = {}): Column { + return { + id: "col1", + type: "column", + floorId: "eg", + categoryCode: "50", + position: { x: 0, y: 0 }, + profile: { kind: "rect", width: 0.3, depth: 0.3 }, + rotation: 0, + height: 2.6, + componentId: "beton", + ...overrides, + }; +} + +function polygonOf(out: Primitive[]): Extract { + const p = out.find((x) => x.kind === "polygon"); + if (!p || p.kind !== "polygon") throw new Error("kein Poché-Polygon gefunden"); + return p; +} + +describe("addColumnPoche — Attribut-Override greift", () => { + it("ohne Override: Bauteil-Schraffur/-Farbe bestimmen die Poché (Default 'Nach Bauteil')", () => { + const out: Primitive[] = []; + addColumnPoche(out, project(), column(), false, 0.35, category); + const poly = polygonOf(out); + expect(poly.hatch.pattern).toBe("solid"); + }); + + it("mit explizitem background-Override: die Poché-Füllung wird die Override-Farbe", () => { + const out: Primitive[] = []; + addColumnPoche(out, project(), column({ background: "#ff00ff" }), false, 0.35, category); + expect(polygonOf(out).fill).toBe("#ff00ff"); + }); + + it("mit hatchSource:'layer': die Kategorie-Schraffur wird verwendet, nicht die Bauteil-Schraffur", () => { + const out: Primitive[] = []; + const catWithHatch: LayerCategory = { ...category, hatch: "sia-other" }; + addColumnPoche( + out, + project({ layers: [catWithHatch] }), + column({ hatchSource: "layer" }), + false, + 0.35, + catWithHatch, + ); + expect(polygonOf(out).hatch.pattern).toBe("none"); + }); + + it("mit explizitem hatchId-Override: gewinnt gegenüber Bauteil UND Kategorie", () => { + const out: Primitive[] = []; + addColumnPoche( + out, + project(), + column({ hatchId: "sia-other" }), + false, + 0.35, + category, + ); + expect(polygonOf(out).hatch.pattern).toBe("none"); + }); +}); diff --git a/src/plan/generatePlan/structures.ts b/src/plan/generatePlan/structures.ts index fd6b91f..e0be2b5 100644 --- a/src/plan/generatePlan/structures.ts +++ b/src/plan/generatePlan/structures.ts @@ -12,6 +12,7 @@ import { MONO_INK, NO_HATCH, SYMBOL_HAIRLINE_MM, + resolveBackground, resolveForeground, resolveHatch, resolveHatchId, @@ -67,15 +68,19 @@ export function addColumnPoche( try { const comp = getComponent(project, column.componentId); const hatchId = - resolveHatchId(undefined, undefined, category, comp.hatchId) ?? comp.hatchId; + resolveHatchId(column.hatchId, column.hatchSource, category, comp.hatchId) ?? comp.hatchId; const resolved = resolveHatch( project, hatchId, undefined, - resolveForeground(comp, undefined, category, undefined), + resolveForeground(comp, column.foreground, category, column.foregroundSource), ); hatch = resolved; - fill = resolved.pattern === "solid" ? HATCH_INK : HATCH_PAPER; + // SIA-Poché: neutraler Hintergrund (Tinte/Papier je nach Muster) statt + // Bauteil-Albedo, es sei denn ein expliziter Hintergrund-Override greift + // (analog addWallPoche: bgOverride ?? neutrale Poché). + const bgOverride = resolveBackground(comp, column.background, category, column.backgroundSource); + fill = bgOverride ?? (resolved.pattern === "solid" ? HATCH_INK : HATCH_PAPER); } catch { // Bauteil nicht auflösbar → neutrale Poché (Fallback bleibt bestehen). } diff --git a/src/state/selectionInfo.ts b/src/state/selectionInfo.ts index adbabbd..7ea1f10 100644 --- a/src/state/selectionInfo.ts +++ b/src/state/selectionInfo.ts @@ -43,7 +43,12 @@ import type { WallReferenceLine, } from "../model/types"; import { localToLv95 } from "../io/lv95"; -import { resolveForeground, resolveBackground, resolveHatchId } from "../plan/generatePlan/shared"; +import { + resolveForeground, + resolveBackground, + resolveHatchId, + resolveStrokeWeight, +} from "../plan/generatePlan/shared"; import { columnVerticalExtent } from "../model/wall"; import { columnFootprint } from "../geometry/column"; import { evaluateRoom, polygonArea } from "../geometry/roomArea"; @@ -985,13 +990,42 @@ function columnSelection(project: Project, col: Column): Selection { zBottom, zTop, }; + // Effektive Vordergrund-/Hintergrund-/Schraffur-/Strichstärke-Vorschau — s. + // Kommentar in wallSelection (dieselbe Kette, das zugewiesene Bauteil als + // Repräsentant statt der äussersten Schicht, da Column nur EIN componentId + // trägt statt eines geschichteten Aufbaus). + // `getComponent` wirft bei unbekannter ID (z. B. gelöschtes Bauteil) — die + // Panel-Vorschau soll dabei nicht abstürzen (analog dem try/catch in + // structures.ts addColumnPoche), einfach auf "kein Bauteil" zurückfallen. + const comp = col.componentId + ? project.components.find((c) => c.id === col.componentId) + : undefined; + const effectiveForeground = comp + ? resolveForeground(comp, col.foreground, category, col.foregroundSource) + : col.foreground; + const effectiveBackground = comp + ? resolveBackground(comp, col.background, category, col.backgroundSource) + : col.background; + const effectiveHatchId = resolveHatchId(col.hatchId, col.hatchSource, category, comp?.hatchId); + const weightMm = resolveStrokeWeight(col.strokeWeight, col.strokeWeightSource, category, WALL_FALLBACK_MM); return { kind: "column", id: col.id, categoryCode: col.categoryCode, floorId: col.floorId, color: col.color ?? category?.color ?? COLUMN_COLOR, - weightMm: WALL_FALLBACK_MM, + weightMm, + fillHatchId: col.hatchId, + foreground: col.foreground, + background: col.background, + foregroundSource: col.foregroundSource, + backgroundSource: col.backgroundSource, + effectiveForeground, + effectiveBackground, + effectiveHatchId, + strokeWeightOverride: col.strokeWeight, + strokeWeightSource: col.strokeWeightSource, + hatchSource: col.hatchSource, bbox: { minX, minY, maxX, maxY }, column: info, };