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:
+162
-84
@@ -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) —
|
||||
|
||||
@@ -158,89 +158,102 @@ export function ObjectInfoPanel() {
|
||||
|
||||
<div className="objinfo-sep" />
|
||||
|
||||
{/* Bezugspunkt-Würfel (3×3, immer quadratisch) links, X/Y/Z als Spalte
|
||||
rechts daneben (Nutzer-Wunsch). */}
|
||||
<div className="objinfo-section-label">{t("objinfo.refpoint")}</div>
|
||||
<div className="objinfo-refrow">
|
||||
<div className="objinfo-cube" role="group" aria-label={t("objinfo.refpoint")}>
|
||||
{FRACTIONS.map((fy) =>
|
||||
FRACTIONS.map((fx) => {
|
||||
const active = anchor.fx === fx && anchor.fy === fy;
|
||||
return (
|
||||
<button
|
||||
key={anchorKey(fx, fy)}
|
||||
type="button"
|
||||
className={"objinfo-anchor" + (active ? " active" : "")}
|
||||
title={t("objinfo.setRefpoint")}
|
||||
aria-pressed={active}
|
||||
onClick={() => setAnchor({ fx, fy })}
|
||||
>
|
||||
<span className="objinfo-dot" />
|
||||
</button>
|
||||
);
|
||||
}),
|
||||
)}
|
||||
</div>
|
||||
{/* Bezugspunkt/X-Y/Maße sind MODELL-Meter (Grundriss) — für eine Layout-
|
||||
Annotation (Papier-Millimeter, eigenes Koordinatensystem) gibt es
|
||||
keinen sinnvollen Bezugspunkt hier; Position/Grösse editiert man
|
||||
direkt am Rahmen auf dem Blatt (Greifpunkte + das dortige HUD in
|
||||
mm — `layout-sheet-hud` in LayoutSheet.tsx). Sonst zeigte dieses
|
||||
Panel für sie fest 0/0 (Selection.bbox ist für sie ein Platzhalter,
|
||||
s. App.tsx) — verwirrend statt nur „nicht anwendbar". */}
|
||||
{sel.kind === "layoutAnnotation" ? (
|
||||
<div className="objinfo-empty">{t("objinfo.layoutAnnotationHint")}</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Bezugspunkt-Würfel (3×3, immer quadratisch) links, X/Y/Z als Spalte
|
||||
rechts daneben (Nutzer-Wunsch). */}
|
||||
<div className="objinfo-section-label">{t("objinfo.refpoint")}</div>
|
||||
<div className="objinfo-refrow">
|
||||
<div className="objinfo-cube" role="group" aria-label={t("objinfo.refpoint")}>
|
||||
{FRACTIONS.map((fy) =>
|
||||
FRACTIONS.map((fx) => {
|
||||
const active = anchor.fx === fx && anchor.fy === fy;
|
||||
return (
|
||||
<button
|
||||
key={anchorKey(fx, fy)}
|
||||
type="button"
|
||||
className={"objinfo-anchor" + (active ? " active" : "")}
|
||||
title={t("objinfo.setRefpoint")}
|
||||
aria-pressed={active}
|
||||
onClick={() => setAnchor({ fx, fy })}
|
||||
>
|
||||
<span className="objinfo-dot" />
|
||||
</button>
|
||||
);
|
||||
}),
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* X / Y des gewählten Bezugspunkts (editierbar) + Z (read-only) + ein
|
||||
Dreh-Feld, als Spalte. */}
|
||||
<div className="objinfo-xyzcol">
|
||||
<DimensionField
|
||||
label={t("objinfo.x")}
|
||||
value={px}
|
||||
min={-Infinity}
|
||||
onCommit={(nx) => host.onMoveSelectionBy(nx - px, 0)}
|
||||
/>
|
||||
<DimensionField
|
||||
label={t("objinfo.y")}
|
||||
value={py}
|
||||
min={-Infinity}
|
||||
onCommit={(ny) => host.onMoveSelectionBy(0, ny - py)}
|
||||
/>
|
||||
{/* Z ehrlich: Das Modell ist 2D pro Ebene; es gibt keine Z am Element.
|
||||
Daher „—" statt einer gefakten 0, mit kurzem Hinweis im Titel. */}
|
||||
<div className="objinfo-field">
|
||||
<span className="objinfo-flabel">{t("objinfo.z")}</span>
|
||||
<span className="objinfo-fval objinfo-muted" title={t("objinfo.zHint")}>
|
||||
—
|
||||
</span>
|
||||
{/* X / Y des gewählten Bezugspunkts (editierbar) + Z (read-only) + ein
|
||||
Dreh-Feld, als Spalte. */}
|
||||
<div className="objinfo-xyzcol">
|
||||
<DimensionField
|
||||
label={t("objinfo.x")}
|
||||
value={px}
|
||||
min={-Infinity}
|
||||
onCommit={(nx) => host.onMoveSelectionBy(nx - px, 0)}
|
||||
/>
|
||||
<DimensionField
|
||||
label={t("objinfo.y")}
|
||||
value={py}
|
||||
min={-Infinity}
|
||||
onCommit={(ny) => host.onMoveSelectionBy(0, ny - py)}
|
||||
/>
|
||||
{/* Z ehrlich: Das Modell ist 2D pro Ebene; es gibt keine Z am Element.
|
||||
Daher „—" statt einer gefakten 0, mit kurzem Hinweis im Titel. */}
|
||||
<div className="objinfo-field">
|
||||
<span className="objinfo-flabel">{t("objinfo.z")}</span>
|
||||
<span className="objinfo-fval objinfo-muted" title={t("objinfo.zHint")}>
|
||||
—
|
||||
</span>
|
||||
</div>
|
||||
{/* Drehen: Delta-Winkel (Grad) um den gewählten Bezugspunkt — kein
|
||||
persistenter Zustand, das Feld zeigt nach dem Commit wieder „0". */}
|
||||
<RotateField onCommit={(deg) => host.onRotateSelectionAround(px, py, deg)} />
|
||||
</div>
|
||||
</div>
|
||||
{/* Drehen: Delta-Winkel (Grad) um den gewählten Bezugspunkt — kein
|
||||
persistenter Zustand, das Feld zeigt nach dem Commit wieder „0". */}
|
||||
<RotateField onCommit={(deg) => host.onRotateSelectionAround(px, py, deg)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="objinfo-sep" />
|
||||
<div className="objinfo-sep" />
|
||||
|
||||
{/* Maße (editierbar) — Commit auf blur/Enter skaliert um den Anker. */}
|
||||
<div className="objinfo-section-label">{t("objinfo.dimensions")}</div>
|
||||
<DimensionField
|
||||
label={t("objinfo.width")}
|
||||
value={width}
|
||||
onCommit={(w) => commitSize(w, height)}
|
||||
/>
|
||||
<DimensionField
|
||||
label={t("objinfo.height")}
|
||||
value={height}
|
||||
onCommit={(h) => commitSize(width, h)}
|
||||
/>
|
||||
{/* Linien-Länge bzw. Kreis-Radius (nur wenn die Selektion genau eine
|
||||
Linie/ein Kreis ist — das Modell gibt das direkt her, andere Formen
|
||||
bleiben ohne Zusatzfeld). */}
|
||||
{drawingShape === "line" && (
|
||||
<DimensionField
|
||||
label={t("objinfo.drawing.length")}
|
||||
value={Math.hypot(width, height)}
|
||||
onCommit={(v) => host.onSetDrawingLineLength(v)}
|
||||
/>
|
||||
)}
|
||||
{drawingShape === "circle" && (
|
||||
<DimensionField
|
||||
label={t("objinfo.drawing.radius")}
|
||||
value={width / 2}
|
||||
onCommit={(v) => host.onSetDrawingCircleRadius(v)}
|
||||
/>
|
||||
{/* Maße (editierbar) — Commit auf blur/Enter skaliert um den Anker. */}
|
||||
<div className="objinfo-section-label">{t("objinfo.dimensions")}</div>
|
||||
<DimensionField
|
||||
label={t("objinfo.width")}
|
||||
value={width}
|
||||
onCommit={(w) => commitSize(w, height)}
|
||||
/>
|
||||
<DimensionField
|
||||
label={t("objinfo.height")}
|
||||
value={height}
|
||||
onCommit={(h) => commitSize(width, h)}
|
||||
/>
|
||||
{/* Linien-Länge bzw. Kreis-Radius (nur wenn die Selektion genau eine
|
||||
Linie/ein Kreis ist — das Modell gibt das direkt her, andere Formen
|
||||
bleiben ohne Zusatzfeld). */}
|
||||
{drawingShape === "line" && (
|
||||
<DimensionField
|
||||
label={t("objinfo.drawing.length")}
|
||||
value={Math.hypot(width, height)}
|
||||
onCommit={(v) => host.onSetDrawingLineLength(v)}
|
||||
/>
|
||||
)}
|
||||
{drawingShape === "circle" && (
|
||||
<DimensionField
|
||||
label={t("objinfo.drawing.radius")}
|
||||
value={width / 2}
|
||||
onCommit={(v) => host.onSetDrawingCircleRadius(v)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* Kontext-Objekt (importiertes Gebäude/Terrain/Konturen): reale LV95-
|
||||
Koordinate des Bbox-Mittelpunkts — direkt sichtbar, ob/wohin
|
||||
|
||||
@@ -156,6 +156,15 @@ export function ToolIcon({ id }: { id: ToolId }) {
|
||||
<line x1="4.5" y1="11" x2="9" y2="11" />
|
||||
</svg>
|
||||
);
|
||||
case "image":
|
||||
// Bild-Rahmen mit Bergsilhouette + Sonne (klassisches Bild-Icon).
|
||||
return (
|
||||
<svg {...common}>
|
||||
<rect x="2.5" y="3" width="11" height="10" fill="none" />
|
||||
<circle cx="6" cy="6.5" r="1" fill="currentColor" stroke="none" />
|
||||
<path d="M 3 12 L 7 8 L 9.5 10.5 L 11 9 L 13 12" fill="none" />
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -166,7 +175,7 @@ export function ToolIcon({ id }: { id: ToolId }) {
|
||||
// und 3D und Bearbeiten"). Befehlsnamen spiegeln die Ribbon-Registry
|
||||
// (ui/ribbon/ribbonItems.ts) — dort liegt die kanonische Liste; hier nur die
|
||||
// Panel-Gruppierung (2D-Zeichnen · 3D/BIM-Bauteile · Bearbeiten-Aktionen).
|
||||
const TOOLS_2D: ToolId[] = ["line", "polyline", "rect", "circle", "arc", "text", "textbox"];
|
||||
const TOOLS_2D: ToolId[] = ["line", "polyline", "rect", "circle", "arc", "text", "textbox", "image"];
|
||||
const TOOLS_3D: ToolId[] = ["wall", "ceiling", "window", "door", "stair", "room"];
|
||||
const COMMANDS_3D = ["column", "ceilingopening", "roof", "extrude", "sectionline", "viewline"];
|
||||
const COMMANDS_EDIT = ["move", "copy", "rotate", "mirror", "offset", "trim", "join", "measure"];
|
||||
|
||||
+31
-1
@@ -13,7 +13,7 @@
|
||||
|
||||
import { useContext } from "react";
|
||||
import { PanelHostContext } from "./types";
|
||||
import type { DisplayMode } from "./types";
|
||||
import type { DisplayMode, PanelHost } from "./types";
|
||||
import type {
|
||||
AttributeSource,
|
||||
CeilingType,
|
||||
@@ -197,6 +197,11 @@ export interface PanelHostValue {
|
||||
* `null` entfernt die Füllfarbe (Fläche wieder transparent).
|
||||
*/
|
||||
onSetSelectionFillColor: (color: string | null) => void;
|
||||
/**
|
||||
* Setzt die Deckkraft (0..1) — wirkt NUR auf Bild-Elemente (Drawing2D
|
||||
* `{shape:"image"}` oder Layout-Annotation `kind:"image"`), sonst No-op.
|
||||
*/
|
||||
onSetSelectionOpacity: (opacity: number) => void;
|
||||
/**
|
||||
* Setzt/entfernt den Vordergrund-Override (Muster-/Schraffurfarbe, „eigener
|
||||
* Wert") der Selektion — Wand, Decke oder geschlossene Drawing2D. `null` =
|
||||
@@ -653,3 +658,28 @@ export function usePanelHost(): PanelHostValue {
|
||||
}
|
||||
return host as unknown as PanelHostValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut den fertigen {@link PanelHost}-Wert aus dem memoisierten `base` +
|
||||
* live injizierten Feldern (Messwerte, aktiver Darstellungsmodus je Tab).
|
||||
*/
|
||||
export function hostWithMode(
|
||||
base: Omit<PanelHostValue, "displayMode">,
|
||||
activeTab: string | null,
|
||||
modeFor: (id: string | null) => DisplayMode,
|
||||
setModeFor: (id: string | null, mode: DisplayMode) => void,
|
||||
measurement: MeasurementReadout | null = null,
|
||||
): PanelHost {
|
||||
const value: PanelHostValue = {
|
||||
...base,
|
||||
// Live-Messwerte NICHT aus dem memoisierten `base` (dort wären sie eingefroren,
|
||||
// da `draft` bewusst kein Memo-Dep ist — sonst würde der ganze Host bei jeder
|
||||
// Mausbewegung neu gebaut). Hier bei jedem Render frisch injiziert.
|
||||
measurement,
|
||||
displayMode: {
|
||||
mode: modeFor(activeTab),
|
||||
setMode: (mode) => setModeFor(activeTab, mode),
|
||||
},
|
||||
};
|
||||
return value as unknown as PanelHost;
|
||||
}
|
||||
|
||||
@@ -163,6 +163,19 @@ describe("createAnnotation — Defaults je Art", () => {
|
||||
const a = createAnnotation("a3", "text", { text: "EG", heightMm: 5 });
|
||||
expect(a).toMatchObject({ text: "EG", heightMm: 5 });
|
||||
});
|
||||
it("text: init mit doc/widthMm (Rich-Text-Frame) wird übernommen", () => {
|
||||
const doc = { version: 1 as const, paragraphs: [{ runs: [{ text: "Hi", marks: {} }] }] };
|
||||
const a = createAnnotation("a3", "text", { doc, widthMm: 50 });
|
||||
expect(a).toMatchObject({ doc, widthMm: 50 });
|
||||
});
|
||||
it("image: Default-Grösse, leere Quelle", () => {
|
||||
const a = createAnnotation("a4", "image");
|
||||
expect(a).toEqual({ id: "a4", kind: "image", xMm: 20, yMm: 20, widthMm: 60, heightMm: 40, src: "" });
|
||||
});
|
||||
it("image: init setzt Quelle + Grösse + Deckkraft", () => {
|
||||
const a = createAnnotation("a4", "image", { src: "data:image/png;base64,x", widthMm: 30, heightMm: 20, opacity: 0.5 });
|
||||
expect(a).toMatchObject({ src: "data:image/png;base64,x", widthMm: 30, heightMm: 20, opacity: 0.5 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Annotations-CRUD (immutabel)", () => {
|
||||
@@ -199,7 +212,7 @@ describe("Annotations-CRUD (immutabel)", () => {
|
||||
const l = addAnnotation(base, createAnnotation("a1", "line", { color: "#111" }));
|
||||
const patched = patchAnnotation(l, "a1", { x1Mm: 42, color: "#0f0" });
|
||||
expect(patched.annotations![0]).toMatchObject({ x1Mm: 42, color: "#0f0" });
|
||||
expect(l.annotations![0].color).toBe("#111");
|
||||
expect((l.annotations![0] as { color?: string }).color).toBe("#111");
|
||||
});
|
||||
|
||||
it("patchAnnotation: unbekannte Id → No-op (gleiche Werte, neue Liste)", () => {
|
||||
|
||||
@@ -546,6 +546,18 @@ export function createAnnotation(
|
||||
...(init.weightMm !== undefined ? { weightMm: init.weightMm } : {}),
|
||||
};
|
||||
}
|
||||
if (kind === "image") {
|
||||
return {
|
||||
id,
|
||||
kind: "image",
|
||||
xMm: init.xMm ?? 20,
|
||||
yMm: init.yMm ?? 20,
|
||||
widthMm: init.widthMm ?? 60,
|
||||
heightMm: init.heightMm ?? 40,
|
||||
src: init.src ?? "",
|
||||
...(init.opacity !== undefined ? { opacity: init.opacity } : {}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
id,
|
||||
kind: "text",
|
||||
@@ -554,6 +566,8 @@ export function createAnnotation(
|
||||
text: init.text ?? "",
|
||||
heightMm: init.heightMm ?? 3.5,
|
||||
...colorPatch,
|
||||
...(init.doc !== undefined ? { doc: init.doc } : {}),
|
||||
...(init.widthMm !== undefined ? { widthMm: init.widthMm } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user