Files
DOSSIER-STANDALONE/src/panels/ToolsPanel.tsx
T
karim 3d2d4d6321 2D-Plan-Renderer auf WebGL2 (GPU) + akkumulierter Funktionsstand
Neuer GPU-Renderer fuer den Grundriss (src/plan/glPlan/): Earcut-Tessellierung
(konkav-faehig), gehrte Linienzuege (Miter), echte Papier-mm-Strichbreiten im
Massstab (repliziert den SVG-printStrokeVb-Pfad), Hybrid mit scharfem SVG-Text-
Overlay. GPU ist der Standardpfad; der SVG-Renderer bleibt automatischer Fallback,
falls WebGL2/Shader nicht verfuegbar sind. Imperativer Pan (rAF + CSS-transform)
fuer fluessige Interaktion ohne React-Re-Render je Frame.

Enthaelt zudem den bisher nicht committeten Arbeitsstand des Browser-BIM
(Oeffnungen, Treppen, Raeume, Decken, DXF-Export, Materialbibliothek, Kontext-
Import, Tauri-Compute-Boundary-PoC).
2026-07-02 00:12:39 +02:00

281 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Inhalts-Panel „Werkzeuge" — die vertikale Werkzeug-Palette (ArchiCAD-/
// Vectorworks-Stil): eine Zeile je Zeichenwerkzeug, jeweils Icon + Name, das
// aktive Werkzeug hervorgehoben. Darunter — wenn das Wand-Werkzeug aktiv ist —
// ein Wandtyp-Auswahlfeld, und ein kompakter Fang-Abschnitt mit Checkboxen.
//
// Daten/Handler kommen über usePanelHost (kein Prop-Drilling). Die Werkzeug-
// Liste und ihre Metadaten stammen aus der Tools-Registry (TOOL_ORDER/getTool).
//
// Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md). Alle sichtbaren
// Texte über t(...).
import { t } from "../i18n";
import { usePanelHost } from "./host";
import type { ToolId } from "./host";
import { TOOL_ORDER, getTool } from "../tools/tools";
// ── Icons ──────────────────────────────────────────────────────────────────
// Keine echten Asset-Icons vorhanden: schlichte Inline-SVG-Glyphen je Werkzeug
// (~16×16, stroke=currentColor, fill=none), damit das Icon die Textfarbe der
// Zeile übernimmt.
function ToolIcon({ id }: { id: ToolId }) {
const common = {
width: 16,
height: 16,
viewBox: "0 0 16 16",
fill: "none",
stroke: "currentColor",
strokeWidth: 1.4,
strokeLinecap: "round" as const,
strokeLinejoin: "round" as const,
};
switch (id) {
case "select":
// Pfeil-Cursor.
return (
<svg {...common} fill="currentColor" stroke="none">
<path d="M3 2l9 5-3.6 1.1L10.4 13 8.5 13.7 6.6 9.4 3 11z" />
</svg>
);
case "wall":
// Zwei parallele Linien (Wandband).
return (
<svg {...common}>
<line x1="2" y1="5.5" x2="14" y2="5.5" />
<line x1="2" y1="10.5" x2="14" y2="10.5" />
</svg>
);
case "ceiling":
// Deckenplatte: gefülltes Rechteck mit Diagonal-Schraffur (Slab-Symbol).
return (
<svg {...common}>
<rect x="2.5" y="4" width="11" height="8" />
<line x1="4" y1="12" x2="8" y2="4" />
<line x1="8" y1="12" x2="12" y2="4" />
</svg>
);
case "window":
// Fenster: Rahmen-Rechteck mit Mittelsprosse (Verglasung).
return (
<svg {...common}>
<rect x="2.5" y="4" width="11" height="8" />
<line x1="8" y1="4" x2="8" y2="12" />
</svg>
);
case "door":
// Tür: Pfosten + Türblatt mit Schwenkbogen.
return (
<svg {...common}>
<line x1="3" y1="13" x2="3" y2="4" />
<line x1="3" y1="4" x2="11" y2="4" />
<path d="M3 13 A9 9 0 0 0 11 4" />
</svg>
);
case "stair":
// Treppensymbol: gestufte Linie (Tritte) + Lauflinie mit Pfeil.
return (
<svg {...common}>
<polyline points="2,14 2,11 6,11 6,8 10,8 10,5 14,5 14,2" />
<line x1="4" y1="12.5" x2="12" y2="3.5" />
<polyline points="10.5,3 12,3.5 11.5,5" />
</svg>
);
case "room":
// Raum: umschlossene Fläche (Rechteck) mit Flächen-Marke (kleines Kreuz).
return (
<svg {...common}>
<rect x="2.5" y="3.5" width="11" height="9" />
<line x1="6" y1="8" x2="10" y2="8" />
<line x1="8" y1="6" x2="8" y2="10" />
</svg>
);
case "line":
// Einzelne Diagonale.
return (
<svg {...common}>
<line x1="3" y1="13" x2="13" y2="3" />
</svg>
);
case "polyline":
// Dreisegmentiger Zickzack.
return (
<svg {...common}>
<polyline points="2,12 6,5 10,11 14,4" />
</svg>
);
case "rect":
// Rechteck-Umriss.
return (
<svg {...common}>
<rect x="2.5" y="3.5" width="11" height="9" />
</svg>
);
default:
return null;
}
}
// ── Panel ────────────────────────────────────────────────────────────────────
export function ToolsPanel() {
const host = usePanelHost();
const snap = host.snap;
return (
<div className="tools-panel">
{/* Werkzeug-Zeilen: Icon + Name, aktives hervorgehoben. */}
{TOOL_ORDER.map((id) => {
const tool = getTool(id);
const active = host.activeTool === id;
const disabled = id !== "select" && !host.toolsEnabled;
const title = disabled
? t("tool.floorOnlyDisabled")
: t(tool.hintKey(tool.init()));
const cls =
"tool-row" + (active ? " active" : "") + (disabled ? " disabled" : "");
return (
<button
key={id}
type="button"
className={cls}
title={title}
disabled={disabled}
onClick={() => host.onSelectTool(id)}
>
<ToolIcon id={id} />
<span>{t(tool.labelKey)}</span>
</button>
);
})}
<div className="tools-sep" />
{/* Wandtyp-Auswahl — nur sichtbar, wenn das Wand-Werkzeug aktiv ist. */}
{host.activeTool === "wall" && (
<label className="tools-field">
<span>{t("tool.wallType")}</span>
<select
value={host.activeWallTypeId}
disabled={!host.toolsEnabled}
onChange={(e) => host.onActiveWallTypeId(e.target.value)}
>
{host.project.wallTypes.map((wt) => (
<option key={wt.id} value={wt.id}>
{wt.name}
</option>
))}
</select>
</label>
)}
{host.activeTool === "wall" && <div className="tools-sep" />}
{/* Deckentyp-Auswahl — nur sichtbar, wenn das Decken-Werkzeug aktiv ist.
Nutzt denselben Aufbau-Typ (WallType) wie die Wand. */}
{host.activeTool === "ceiling" && (
<label className="tools-field">
<span>{t("tool.ceilingType")}</span>
<select
value={host.activeWallTypeId}
disabled={!host.toolsEnabled}
onChange={(e) => host.onActiveWallTypeId(e.target.value)}
>
{host.project.wallTypes.map((wt) => (
<option key={wt.id} value={wt.id}>
{wt.name}
</option>
))}
</select>
</label>
)}
{host.activeTool === "ceiling" && <div className="tools-sep" />}
{/* Fang-Abschnitt. Master-Checkbox steuert die übrigen Fang-Optionen. */}
<label className="tools-snap-row">
<input
type="checkbox"
checked={snap.enabled}
onChange={(e) => host.onSnapChange({ ...snap, enabled: e.target.checked })}
/>
<span>{t("snap.enabled")}</span>
</label>
<label className="tools-snap-row">
<input
type="checkbox"
checked={snap.endpoint}
disabled={!snap.enabled}
onChange={(e) => host.onSnapChange({ ...snap, endpoint: e.target.checked })}
/>
<span>{t("snap.endpoint")}</span>
</label>
<label className="tools-snap-row">
<input
type="checkbox"
checked={snap.midpoint}
disabled={!snap.enabled}
onChange={(e) => host.onSnapChange({ ...snap, midpoint: e.target.checked })}
/>
<span>{t("snap.midpoint")}</span>
</label>
<label className="tools-snap-row">
<input
type="checkbox"
checked={snap.intersection}
disabled={!snap.enabled}
onChange={(e) =>
host.onSnapChange({ ...snap, intersection: e.target.checked })
}
/>
<span>{t("snap.intersection")}</span>
</label>
<label className="tools-snap-row">
<input
type="checkbox"
checked={snap.onEdge}
disabled={!snap.enabled}
onChange={(e) => host.onSnapChange({ ...snap, onEdge: e.target.checked })}
/>
<span>{t("snap.onEdge")}</span>
</label>
<label className="tools-snap-row">
<input
type="checkbox"
checked={snap.grid}
disabled={!snap.enabled}
onChange={(e) => host.onSnapChange({ ...snap, grid: e.target.checked })}
/>
<span>{t("snap.grid")}</span>
</label>
<label className="tools-snap-row">
<input
type="checkbox"
checked={snap.ortho}
disabled={!snap.enabled}
onChange={(e) => host.onSnapChange({ ...snap, ortho: e.target.checked })}
/>
<span>{t("snap.ortho")}</span>
</label>
<label className="tools-field">
<span>{t("snap.gridSize")}</span>
<input
type="number"
step={0.05}
min={0.01}
value={snap.gridSize}
onChange={(e) =>
host.onSnapChange({ ...snap, gridSize: Number(e.target.value) })
}
/>
</label>
</div>
);
}