God-Files aufgeteilt (ResourceManager/generatePlan/toWalls3d/App.tsx-Handler), Totcode entfernt (geometry-Crate, OCCT/HLR-Spike), PDF/Bild-Import ergänzt.

ResourceManager.tsx, generatePlan.ts und toWalls3d.ts sind nach Bauteil-
Domäne in src/ui/resourceManager/, src/plan/generatePlan/ und
src/plan/toWalls3d/ aufgeteilt (reine Verschiebung, keine Verhaltens-
änderung). App.tsx verliert weitere Handler-Gruppen an eigene Hooks
(useExportHandlers, useImportHandling, useKeyboardShortcuts) sowie
Hit-Testing an src/viewport/planHitTest.ts.

Unbenutzte Rust-geometry-Crate (durch WASM-Joins abgelöst) sowie der alte
OCCT/HLR-Schnitt-Spike (hlr.ts/occt.ts, vite occt-Aliases) entfernt.

Neu: PDF-Import als Rasterbild (src/io/pdfImport.ts) + gemeinsamer
Bild-Asset-Helfer (src/io/imageAsset.ts) für Plan- und Layout-Bilder.

tsc -b sauber, vitest 891/891 grün.
This commit is contained in:
2026-08-19 22:45:41 +02:00
parent 06c0143d32
commit 1507c2bf00
85 changed files with 12461 additions and 11280 deletions
+162 -84
View File
@@ -15,7 +15,7 @@
// Es werden NUR Eigenschaften angeboten, die der Host-Kontrakt tatsächlich
// setzen kann — keine Stubs/Fake-Setter (Konvention wire-dont-stub). Felder ohne
// echten Setter (Linienstil) werden read-only/informativ gezeigt; Felder, die
// das Modell gar nicht trägt (Deckkraft, Caps, Schlagschatten), werden bewusst
// das Modell gar nicht trägt (Caps, Schlagschatten), werden bewusst
// weggelassen, um die Palette ehrlich und schlank zu halten.
//
// DOSSIER-Tabellen-Look: KEINE wiederholten Feld-Labels pro Zeile — ein kompaktes
@@ -107,10 +107,20 @@ export function AttributesPanel() {
}
const isDrawing = sel.kind === "drawing2d";
// Layout-Annotation (Linie/Rechteck/Text/Bild auf einem Blatt) — kein
// Projekt-Bauteil, daher ohne Ebene/Bauteil-Vererbung: Farbe/Strichstärke
// 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
// (`strokeWeight`/`weightMm`) + Quellen-Dropdown; andere Elementarten erben
// stets aus der Kategorie (kein Setter im Kontrakt → nicht editierbar).
const weightEditable = sel.kind === "wall" || sel.kind === "ceiling" || isDrawing;
// 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" ||
isDrawing ||
(isAnnotation && (sel.annotKind === "line" || sel.annotKind === "rect"));
// Schraffur: Wand/Decke (Schnitt-/Ansichts-Schraffur) oder geschlossene 2D-Form.
const fillEditable =
sel.kind === "wall" || sel.kind === "ceiling" || (isDrawing && sel.closed === true);
@@ -223,97 +233,165 @@ export function AttributesPanel() {
<ColorHexField value={sel.color} onChange={host.onSetSelectionColor} />
</span>
{/* Strichstärke (mm) — Wand/Decke/Drawing2D, mit Nach-Ebene/Nach-Bauteil/
eigener-Wert-Quelle; andere Elementarten erben stets aus der Kategorie. */}
{sourceRow(
"attr.weight",
weightEditable,
weightUi,
onWeightSourceChange,
<>
<input
className="attr-num"
style={{ width: 72 }}
type="number"
step={0.01}
min={0}
list="attr-pen-weights"
value={sel.weightMm}
onChange={(e) => {
const v = Number(e.target.value);
if (Number.isFinite(v)) host.onSetSelectionWeight(v);
}}
/>
<datalist id="attr-pen-weights">
{PEN_WEIGHTS.map((w) => (
<option key={w} value={w} />
))}
</datalist>
</>,
{/* Strichstärke (mm): Wand/Decke/Drawing2D mit Nach-Ebene/Nach-Bauteil/
eigener-Wert-Quelle; eine Layout-Annotation kennt diese Vererbung
nicht (kein Bauteil/keine Ebene) — dort ein schlichtes Zahlenfeld
ohne Quellen-Dropdown, wenn die Art überhaupt eine Strichstärke
trägt (Linie/Rechteck, nicht Text/Bild). */}
{isAnnotation ? (
weightEditable && (
<>
<span className="attr-key">{t("attr.weight")}</span>
<span className="attr-val">
<input
className="attr-num"
style={{ width: 72 }}
type="number"
step={0.01}
min={0}
list="attr-pen-weights"
value={sel.weightMm}
onChange={(e) => {
const v = Number(e.target.value);
if (Number.isFinite(v)) host.onSetSelectionWeight(v);
}}
/>
</span>
</>
)
) : (
sourceRow(
"attr.weight",
weightEditable,
weightUi,
onWeightSourceChange,
<>
<input
className="attr-num"
style={{ width: 72 }}
type="number"
step={0.01}
min={0}
list="attr-pen-weights"
value={sel.weightMm}
onChange={(e) => {
const v = Number(e.target.value);
if (Number.isFinite(v)) host.onSetSelectionWeight(v);
}}
/>
<datalist id="attr-pen-weights">
{PEN_WEIGHTS.map((w) => (
<option key={w} value={w} />
))}
</datalist>
</>,
)
)}
{/* Linienstil — nur Drawing2D trägt ihn (Wand/Decke/etc. read-only). */}
<span className="attr-key">{t("attr.lineStyle")}</span>
<span className="attr-val">
{isDrawing ? (
<Dropdown
value={drawing?.lineStyleId ?? "__none__"}
onChange={(v) =>
host.onSetSelectionLineStyle(v === "__none__" ? undefined : v)
}
options={[
{ value: "__none__", label: t("attr.lineStyle.none") },
...project.lineStyles.map((l) => ({ value: l.id, label: l.name })),
]}
/>
) : (
<span className="attr-readonly">{lineStyle ? lineStyle.name : "—"}</span>
)}
</span>
{/* Deckkraft (0..1) — nur Bild-Elemente (Drawing2D `{shape:"image"}`
im Plan, Layout-Annotation `kind:"image"`). Schlichtes Zahlenfeld
wie die Strichstärke bei Annotationen: kein Bauteil/keine Ebene,
also keine Quellen-Vererbung. */}
{sel.isImage && (
<>
<span className="attr-key">{t("attr.opacity")}</span>
<span className="attr-val">
<input
className="attr-num"
style={{ width: 72 }}
type="number"
step={0.05}
min={0}
max={1}
value={sel.opacity ?? 1}
onChange={(e) => {
const v = Number(e.target.value);
if (Number.isFinite(v)) host.onSetSelectionOpacity(Math.max(0, Math.min(1, v)));
}}
/>
</span>
</>
)}
{/* Linienstil — nur Drawing2D trägt ihn (Wand/Decke/Annotation etc.
read-only), Rest wie gehabt. */}
{!isAnnotation && (
<>
<span className="attr-key">{t("attr.lineStyle")}</span>
<span className="attr-val">
{isDrawing ? (
<Dropdown
value={drawing?.lineStyleId ?? "__none__"}
onChange={(v) =>
host.onSetSelectionLineStyle(v === "__none__" ? undefined : v)
}
options={[
{ value: "__none__", label: t("attr.lineStyle.none") },
...project.lineStyles.map((l) => ({ value: l.id, label: l.name })),
]}
/>
) : (
<span className="attr-readonly">{lineStyle ? lineStyle.name : "—"}</span>
)}
</span>
</>
)}
{/* ── Füllung (Vordergrund/Hintergrund + Schraffur) ─────────── */}
{/* Vordergrund = Muster-/Schraffurfarbe, Hintergrund = Füllfarbe/
Poché. Jedes Feld: Nach Ebene (LayerCategory erzwingt den Wert) /
Nach Bauteil (erbt vom Component-Manager, Default) / eigener Wert. */}
<div className="attr-section-label">{t("attr.fill")}</div>
Nach Bauteil (erbt vom Component-Manager, Default) / eigener Wert.
Eine Layout-Annotation kennt keine Füllung (nur Umriss) — Sektion
entfällt komplett statt drei bedeutungslose Dropdowns zu zeigen. */}
{!isAnnotation && (
<>
<div className="attr-section-label">{t("attr.fill")}</div>
{sourceRow(
"attr.foreground",
pocheEditable,
fgUi,
onForegroundSourceChange,
colorValue(sel.foreground, host.onSetSelectionForeground),
{sourceRow(
"attr.foreground",
pocheEditable,
fgUi,
onForegroundSourceChange,
colorValue(sel.foreground, host.onSetSelectionForeground),
)}
{sourceRow(
"attr.background",
pocheEditable,
bgUi,
onBackgroundSourceChange,
colorValue(sel.background, host.onSetSelectionBackground),
)}
{sourceRow(
"attr.hatch",
fillEditable,
hatchUi,
onHatchSourceChange,
<Dropdown
value={sel.fillHatchId ?? ""}
onChange={(v) => host.onSetSelectionFill(v || null)}
options={[
{ value: "", label: t("attr.none") },
...project.hatches.map((h) => ({ value: h.id, label: h.name })),
]}
/>,
)}
</>
)}
{sourceRow(
"attr.background",
pocheEditable,
bgUi,
onBackgroundSourceChange,
colorValue(sel.background, host.onSetSelectionBackground),
{/* ── Masse (informativ, aus der bbox) — bei einer Layout-Annotation
in Papier-mm statt Modell-Metern geführt (`formatM` passt nicht),
daher hier weggelassen; Grösse steht bereits im Blatt-HUD. */}
{!isAnnotation && (
<>
<div className="attr-section-label">{t("attr.size")}</div>
<span className="attr-key">{t("attr.width")}</span>
<span className="attr-val attr-readonly">{formatM(bboxW)}</span>
<span className="attr-key">{t("attr.height")}</span>
<span className="attr-val attr-readonly">{formatM(bboxH)}</span>
</>
)}
{sourceRow(
"attr.hatch",
fillEditable,
hatchUi,
onHatchSourceChange,
<Dropdown
value={sel.fillHatchId ?? ""}
onChange={(v) => host.onSetSelectionFill(v || null)}
options={[
{ value: "", label: t("attr.none") },
...project.hatches.map((h) => ({ value: h.id, label: h.name })),
]}
/>,
)}
{/* ── Maße (informativ, aus der bbox) ───────────────────────── */}
<div className="attr-section-label">{t("attr.size")}</div>
<span className="attr-key">{t("attr.width")}</span>
<span className="attr-val attr-readonly">{formatM(bboxW)}</span>
<span className="attr-key">{t("attr.height")}</span>
<span className="attr-val attr-readonly">{formatM(bboxH)}</span>
</div>
{/* Element-spezifische Abschnitte (aus ObjectInfo hierher verschoben) —