diff --git a/src/io/swissImage.ts b/src/io/swissImage.ts index ea8ece9..34aeafd 100644 --- a/src/io/swissImage.ts +++ b/src/io/swissImage.ts @@ -111,40 +111,6 @@ export function sampleColorsFromImageData( return out; } -/** - * Dekodiert eine Bild-Data-URL und sampelt daraus die Pro-Vertex-Albedo für ein - * Gelände-TIN (siehe {@link sampleColorsFromImageData}). Nutzt `createImageBitmap` - * + `OffscreenCanvas` (im App-/WebView-Kontext vorhanden; in Node/Tests fehlen sie - * — dann `null`, kein Wurf). So bleibt der Render-Pfad synchron: gesampelt wird - * EINMAL beim Import, das Ergebnis lebt auf `TerrainMesh.vertexColors`. - */ -export async function sampleTerrainColors( - dataUrl: string, - positions: readonly number[], - min: { x: number; y: number }, - max: { x: number; y: number }, -): Promise { - if ( - typeof createImageBitmap !== "function" || - typeof OffscreenCanvas !== "function" - ) { - return null; - } - try { - const res = await fetch(dataUrl); - const blob = await res.blob(); - const bmp = await createImageBitmap(blob); - const canvas = new OffscreenCanvas(bmp.width, bmp.height); - const ctx = canvas.getContext("2d"); - if (!ctx) return null; - ctx.drawImage(bmp, 0, 0); - const img = ctx.getImageData(0, 0, bmp.width, bmp.height); - return sampleColorsFromImageData(img.data, bmp.width, bmp.height, positions, min, max); - } catch { - return null; - } -} - /** Dekodiertes Bild: dichte RGBA8-Bytes + Maße (zeilenweise von oben). */ export interface DecodedImage { data: Uint8Array; diff --git a/src/materials/ambientcg.ts b/src/materials/ambientcg.ts index f0a3eee..a183ff5 100644 --- a/src/materials/ambientcg.ts +++ b/src/materials/ambientcg.ts @@ -267,3 +267,26 @@ export async function fetchMaterialMaps( export function revokeMaterial(blobUrls: string[]): void { for (const u of blobUrls) URL.revokeObjectURL(u); } + +const MATERIAL_MAP_KEYS: (keyof ComponentMaterial)[] = [ + "color", + "normal", + "roughness", + "metalness", + "displacement", + "ao", +]; + +/** + * Gibt die Blob-URLs eines bereits zugewiesenen `ComponentMaterial` frei (Karten + * aus ambientCG-Download ODER lokalem Upload — beide nutzen `createObjectURL`). + * Bibliotheks-Assets (`materialFromAsset`) liefern statische Pfade, keine + * Blob-URLs, und bleiben daher unangetastet. + */ +export function revokeMaterialUrls(material: ComponentMaterial | undefined): void { + if (!material) return; + const blobUrls = MATERIAL_MAP_KEYS.map((k) => material[k]).filter( + (u): u is string => typeof u === "string" && u.startsWith("blob:"), + ); + revokeMaterial(blobUrls); +} diff --git a/src/panels/ResourcesPanel.tsx b/src/panels/ResourcesPanel.tsx deleted file mode 100644 index 88e3975..0000000 --- a/src/panels/ResourcesPanel.tsx +++ /dev/null @@ -1,100 +0,0 @@ -// Inhalts-Panel „Ressourcen" — dünner Adapter um den ResourceManager. -// -// Wiederverwendet die bestehende -Komponente (Bauteile / -// Schraffuren / Linien) UNVERÄNDERT, rendert sie aber INLINE als Panel-Körper -// statt als rechte Schublade. Projekt + Handler kommen aus dem Host-Context. -// -// ResourceManager ist als Overlay-Schublade gebaut (.res-overlay fixed/inset:0 -// → .res-drawer mit fester Breite, Rand und Schatten) und erwartet onClose. -// Da styles.css und ResourceManager selbst nicht angefasst werden dürfen, -// zähmt dieser Adapter die Schubladen-Chrome lokal: ein eingebetteter, -// gescopter - - - ); -} - -/** onClose-Platzhalter: als Panel gibt es keinen Schubladen-Schließer. */ -const NO_OP = () => {}; - -/** - * Lokale Override-Regeln, die die fixe Overlay-Schublade in einen normalen, - * im Panel sitzenden Block verwandeln. Klassen-Selektoren sind unter - * `.panel-resources-host` verschachtelt, damit nichts außerhalb des Panels - * betroffen ist. - */ -const PANEL_RESOURCES_CSS = ` -.panel-resources-host { - height: 100%; - min-height: 0; - display: flex; -} -.panel-resources-host .res-overlay { - position: static; - inset: auto; - z-index: auto; - background: transparent; - display: flex; - flex: 1; - min-height: 0; - justify-content: stretch; -} -.panel-resources-host .res-drawer { - width: 100%; - height: 100%; - border-left: none; - box-shadow: none; -} -/* Im Panel ohne Schubladen-Schließer. */ -.panel-resources-host .res-close { - display: none; -} -`; diff --git a/src/panels/layout.ts b/src/panels/layout.ts index 3d9d05b..007548c 100644 --- a/src/panels/layout.ts +++ b/src/panels/layout.ts @@ -347,20 +347,6 @@ export function removeFloat(layout: LayoutState, panelId: string): LayoutState { return { ...layout, floating }; } -/** - * Entfernt ein Panel aus einem Dock (immutabel). Dünne, benannte Hülle um - * `removeFromDock` auf Layout-Ebene; nützlich, um ein Panel ganz zu schließen. - */ -export function removeFromDockLayout( - layout: LayoutState, - dock: DockId, - panelId: string, -): LayoutState { - const next = removeFromDock(layout[dock], panelId); - if (next === layout[dock]) return layout; - return { ...layout, [dock]: next }; -} - /** * Setzt das Panel als aktiven Tab innerhalb seiner Gruppe (immutabel). Sucht * die Gruppe im Dock, die das Panel enthält, und macht es dort aktiv. Liegt die diff --git a/src/panels/registry.ts b/src/panels/registry.ts index cb01d44..8c15c17 100644 --- a/src/panels/registry.ts +++ b/src/panels/registry.ts @@ -42,10 +42,3 @@ export function hasPanel(id: string): boolean { return registry.has(id); } -/** - * Entfernt ein Panel aus der Registry. Liefert `true`, wenn etwas entfernt - * wurde. Vor allem für Tests und das Entladen von Plugins gedacht. - */ -export function unregisterPanel(id: string): boolean { - return registry.delete(id); -} diff --git a/src/text/renderHtml.ts b/src/text/renderHtml.ts index 460bc18..70a71da 100644 --- a/src/text/renderHtml.ts +++ b/src/text/renderHtml.ts @@ -152,7 +152,7 @@ export interface SvgTextOptions { /** * Zerlegt das Dokument in Zeilen/Tspans mit bereits in Benutzereinheiten * umgerechneten Grössen. Der Aufrufer positioniert die Zeilen selbst (Baseline - * der Zeile i = `y + Σ lineHeight[0..i-1]`, s. `docToSvgText`). + * der Zeile i = `y + Σ lineHeight[0..i-1]`). */ export function docToLines(doc: RichTextDoc, opts: SvgTextOptions): SvgLine[] { const basePt = opts.basePt ?? 12; @@ -262,38 +262,3 @@ function runToTspan(run: TextRun, basePt: number, unitPerPt: number, color: stri return tspan; } -/** - * Baut ein komplettes ``-SVG-Element als String. Jede Zeile ist ein - * ``, darin ein `` je Run. `text-anchor` folgt der - * Absatz-Ausrichtung (left→start, center→middle, right→end). - */ -export function docToSvgText(doc: RichTextDoc, opts: SvgTextOptions): string { - const lines = docToLines(doc, opts); - const parts: string[] = []; - lines.forEach((line, i) => { - const dy = i === 0 ? 0 : line.lineHeight; - const anchor = line.align === "center" ? "middle" : line.align === "right" ? "end" : "start"; - const runSpans = line.tspans.map((t) => tspanToSvg(t)).join(""); - // Leere Zeile: ein Zero-Width-Space, damit dy dennoch wirkt. - const content = runSpans || "​"; - parts.push( - `${content}`, - ); - }); - return `${parts.join("")}`; -} - -function tspanToSvg(t: SvgTspan): string { - const attrs: string[] = [`font-size="${t.fontSize}"`, `fill="${escapeAttr(t.fill)}"`]; - if (t.fontWeight) attrs.push(`font-weight="${t.fontWeight}"`); - if (t.fontStyle) attrs.push(`font-style="${t.fontStyle}"`); - if (t.fontFamily) attrs.push(`font-family="${escapeAttr(t.fontFamily)}"`); - if (t.textDecoration) attrs.push(`text-decoration="${t.textDecoration}"`); - if (t.baselineShift) attrs.push(`dy="${-t.baselineShift}"`); - const body = escapeText(t.text); - // Bei baseline-shift den dy nach dem Run zurücksetzen, damit die Grundlinie hält. - if (t.baselineShift) { - return `${body}​`; - } - return `${body}`; -} diff --git a/src/text/richText.ts b/src/text/richText.ts index a707e0f..c4f80b7 100644 --- a/src/text/richText.ts +++ b/src/text/richText.ts @@ -103,11 +103,6 @@ export function plainText(doc: RichTextDoc): string { .join("\n"); } -/** Ist das Dokument (nach Reintext) leer? */ -export function isEmpty(doc: RichTextDoc): boolean { - return plainText(doc).length === 0; -} - /** Gesamtlänge in Zeichen (inkl. Absatzumbrüche als je 1 Zeichen). */ export function docLength(doc: RichTextDoc): number { return plainText(doc).length; @@ -376,41 +371,6 @@ export function serialize(doc: RichTextDoc): string { return JSON.stringify({ version: 1, paragraphs }); } -/** - * Liest ein Dokument aus JSON. Robust gegenüber fehlenden Feldern; unbekannter - * Input ergibt ein leeres Dokument statt eines Fehlers. - */ -export function deserialize(json: string): RichTextDoc { - try { - const raw = JSON.parse(json) as unknown; - return fromJson(raw); - } catch { - return emptyDoc(); - } -} - -/** Wandelt ein bereits geparstes Objekt in ein valides Dokument. */ -export function fromJson(raw: unknown): RichTextDoc { - if (!raw || typeof raw !== "object") return emptyDoc(); - const obj = raw as { paragraphs?: unknown }; - if (!Array.isArray(obj.paragraphs)) return emptyDoc(); - const paragraphs: Paragraph[] = obj.paragraphs.map((p) => { - const pp = (p ?? {}) as { runs?: unknown; align?: unknown }; - const runs: TextRun[] = Array.isArray(pp.runs) - ? pp.runs.map((r) => { - const rr = (r ?? {}) as { text?: unknown; marks?: unknown }; - const text = typeof rr.text === "string" ? rr.text : ""; - const marks = normalizeMarks((rr.marks ?? {}) as Marks); - return { text, marks }; - }) - : []; - const para: Paragraph = { runs }; - if (pp.align === "center" || pp.align === "right") para.align = pp.align; - return para; - }); - return normalizeDoc({ version: 1, paragraphs }); -} - // ── Style-Presets ────────────────────────────────────────────────────────── /** Ein benannter Stil (Marks-Vorlage), z. B. „Titel". */ diff --git a/src/theme/accents.ts b/src/theme/accents.ts index d3ce348..78a48e4 100644 --- a/src/theme/accents.ts +++ b/src/theme/accents.ts @@ -48,7 +48,3 @@ export const DEFAULT_MARQUEE_COLOR = "#CEB188"; */ export const DEFAULT_SNAP_COLOR = "#E0BE5A"; -/** Liefert einen Swatch nach `id`, oder `undefined`. */ -export function findAccentSwatch(id: string): AccentSwatch | undefined { - return ACCENT_SWATCHES.find((s) => s.id === id); -} diff --git a/src/tools/tools.ts b/src/tools/tools.ts index 6e921a5..8ad3a9a 100644 --- a/src/tools/tools.ts +++ b/src/tools/tools.ts @@ -621,21 +621,3 @@ export function getTool(id: ToolId): Tool { return TOOLS[id]; } -/** Reihenfolge der Werkzeuge in der Werkzeugleiste. */ -export const TOOL_ORDER: ToolId[] = [ - "select", - "wall", - "ceiling", - "window", - "door", - "stair", - "room", - "line", - "polyline", - "rect", - "circle", - "arc", - "text", - "textbox", - "image", -]; diff --git a/src/ui/ResourceManager.tsx b/src/ui/ResourceManager.tsx index 5e7d538..c0f15f5 100644 --- a/src/ui/ResourceManager.tsx +++ b/src/ui/ResourceManager.tsx @@ -98,14 +98,14 @@ export interface ResourceManagerHandlers { onImportHatches: (hatches: Omit[]) => void; /** * Grafische Override-Regeln: ersetzt die GANZE Regelliste (Reihenfolge = - * Priorität). Optional, damit bestehende Einbettungen (z. B. ResourcesPanel) - * ohne Änderung weiter kompilieren — ohne Handler ist der Tab nur lesend. + * Priorität). Optional, damit ältere Einbettungen ohne diesen Handler + * weiter kompilieren — ohne Handler ist der Tab nur lesend. */ onSetOverrideRules?: (rules: OverrideRule[]) => void; /** * Bauteil-Typen (Tür/Fenster/Treppe) — Bibliotheks-CRUD, analog Wand-/ - * Deckentypen. Optional, damit bestehende Einbettungen (z. B. ResourcesPanel) - * ohne Änderung weiter kompilieren; ohne Handler bleiben die Tabs verborgen. + * Deckentypen. Optional, damit ältere Einbettungen ohne diesen Handler + * weiter kompilieren; ohne Handler bleiben die Tabs verborgen. */ onAddDoorType?: () => void; onPatchDoorType?: (id: string, patch: Partial) => void; diff --git a/src/ui/TopBar.tsx b/src/ui/TopBar.tsx index d7afb09..1da14a0 100644 --- a/src/ui/TopBar.tsx +++ b/src/ui/TopBar.tsx @@ -1141,8 +1141,7 @@ export interface ViewRibbonTabProps { * „Ansichten"-Tab des Ribbons — trägt die aus der TopBar gemergten Mittelcluster: * 2D/3D-Ansichts-Raster (+ Kamera-FOV), Sichtbarkeits-Kombinationen, Detailgrad, * Massstab/Zoom-Cluster und Darstellungsart. EIN Ribbon für alles; Handler/Setter - * kommen unverändert von App (gleiche Props wie zuvor an die TopBar). Wird von - * `RibbonBar` als Inhalt des „Ansichten"-Tabs gerendert. + * kommen unverändert von App (gleiche Props wie zuvor an die TopBar). */ export function ViewRibbonTab({ viewToggleEnabled, diff --git a/src/ui/resourceManager/ComponentsTab.tsx b/src/ui/resourceManager/ComponentsTab.tsx index 6e4b26e..73f0806 100644 --- a/src/ui/resourceManager/ComponentsTab.tsx +++ b/src/ui/resourceManager/ComponentsTab.tsx @@ -10,6 +10,7 @@ import { materialFromAsset, DEFAULT_TILE_SIZE_M } from "../../materials/runtime" import { searchMaterials, fetchMaterialMaps, + revokeMaterialUrls, AMBIENT_CATEGORIES, type AmbientMaterial, type AmbientResolution, @@ -55,9 +56,13 @@ function MaterialCell({ { + // Vorheriges Material ersetzt: dessen Blob-URLs (Download/Upload) + // sind ab jetzt unerreichbar — sonst leckt jeder Material-Wechsel. + revokeMaterialUrls(material); onAssign(m); }} onClear={() => { + revokeMaterialUrls(material); onClear(); }} onClose={() => setOpen(false)} @@ -726,7 +731,10 @@ export function ComponentsTab({ component={selected} hatches={project.hatches} onPatch={(patch) => onPatchComponent(selected.id, patch)} - onDelete={() => onDeleteComponent(selected.id)} + onDelete={() => { + revokeMaterialUrls(selected.material); + onDeleteComponent(selected.id); + }} /> ) : (
{t("resources.components.empty")}
diff --git a/src/ui/ribbon/RibbonBar.tsx b/src/ui/ribbon/RibbonBar.tsx deleted file mode 100644 index 7747772..0000000 --- a/src/ui/ribbon/RibbonBar.tsx +++ /dev/null @@ -1,188 +0,0 @@ -// Ribbon-Inhaltsleiste (docs/design/ribbon-ui-plan.md) — rendert die Gruppen -// EINES festen Tabs (kein Tab-Umschalter mehr, v12: Werkzeuge leben im -// ToolsPanel). App rendert mehrere ``-Instanzen permanent -// nebeneinander (2d/bim/custom) statt einer einzigen, umschaltbaren — jede -// zeigt nur die Befehls-Gruppen ohne Panel-Entsprechung. -// -// Werkzeug-Elemente laufen über `onSelectTool` (gekoppelte Befehle inklusive, wie -// im ToolsPanel); Befehls-Elemente über `onRunCommand` (engine.start). -// -// Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md). - -import { t } from "../../i18n"; -import { ToolIcon } from "../../panels/ToolsPanel"; -import { getTool } from "../../tools/tools"; -import type { ToolId } from "../../tools/types"; -import { getCommand } from "../../commands/registry"; -import { Dropdown } from "../Dropdown"; -import type { DropdownItem } from "../Dropdown"; -import { RIBBON_TABS, ALL_RIBBON_ITEMS, itemKey, itemFromKey } from "./ribbonItems"; -import type { RibbonItem, RibbonTabId } from "./ribbonItems"; -import { CommandIcon } from "./CommandIcon"; - -/** Aufgelöste Beschriftung eines Items (Werkzeug bzw. Befehl). */ -function itemLabel(item: RibbonItem): string { - if (item.kind === "tool") return t(getTool(item.id).labelKey); - const cmd = getCommand(item.name); - return cmd ? t(cmd.labelKey) : item.name; -} - -export interface RibbonBarProps { - /** Aktiver Tab (State liegt in App, geteilt mit den Reitern in der TopBar). */ - tab: RibbonTabId; - /** Aktives Werkzeug (Highlight der `tool`-Elemente). */ - activeTool: ToolId; - /** Name des laufenden Engine-Befehls (Highlight der `command`-Elemente). */ - activeCommand: string | null; - /** Ob Boden-gebundene (`floorOnly`) Werkzeuge nutzbar sind (aktives Geschoss). */ - toolsEnabled: boolean; - /** Werkzeug aktivieren (gleicher Pfad wie die Sidebar). */ - onSelectTool: (id: ToolId) => void; - /** Engine-Befehl starten (Ändern-Gruppe u. a.). */ - onRunCommand: (name: string) => void; - /** Persistierte Item-Schlüssel der modularen Custom-Bar (Tab „Eigene"). */ - customItems: string[]; - /** Ein Item in der Custom-Bar an-/abwählen (Picker im „Eigene"-Tab). */ - onToggleCustomItem: (key: string) => void; -} - -/** Aktivierungs-/Highlight-Kontext, den jeder Button-Typ braucht. */ -interface ButtonCtx { - activeTool: ToolId; - activeCommand: string | null; - toolsEnabled: boolean; - onSelectTool: (id: ToolId) => void; - onRunCommand: (name: string) => void; -} - -/** Ein einzelner Ribbon-Button (Werkzeug ODER Befehl). */ -function RibbonButton({ item, ctx }: { item: RibbonItem; ctx: ButtonCtx }) { - if (item.kind === "tool") { - const tool = getTool(item.id); - // Nur Boden-gebundene Werkzeuge sind ohne aktives Geschoss gesperrt; freie - // 2D-Werkzeuge (Linie/Kreis/…) bleiben auch auf Zeichnungsebenen nutzbar. - const disabled = !!tool.floorOnly && !ctx.toolsEnabled; - const active = ctx.activeTool === item.id; - const label = t(tool.labelKey); - return ( - - ); - } - // Befehls-Element: Label aus der Befehls-Registry ableiten. Für Aktionen ohne - // eigenes Engine-Kommando (z. B. „rotate", das über die interaktive - // Transformation läuft) auf den i18n-Schlüssel `cmd..label` zurückfallen. - const cmd = getCommand(item.name); - const label = cmd ? t(cmd.labelKey) : t(`cmd.${item.name}.label`); - const active = ctx.activeCommand === item.name; - return ( - - ); -} - -/** - * Modulare Custom-Bar (Tab „Eigene") — rendert die vom Nutzer gewählten Items - * plus einen „+"-Picker, der ALLE verfügbaren Werkzeuge/Befehle mit Häkchen für - * die bereits enthaltenen listet (Klick = an-/abwählen). Gleiche Item-Typen + - * gleiche Aktivierung wie die normalen Tabs — kein Sonderweg. - */ -function CustomBar({ - customItems, - onToggle, - ctx, -}: { - customItems: string[]; - onToggle: (key: string) => void; - ctx: ButtonCtx; -}) { - const pickerItems: DropdownItem[] = ALL_RIBBON_ITEMS.map((it) => { - const key = itemKey(it); - return { - label: itemLabel(it), - active: customItems.includes(key), - onSelect: () => onToggle(key), - }; - }); - const items = customItems - .map(itemFromKey) - .filter((it): it is RibbonItem => it !== null); - return ( -
-
- {items.length === 0 && ( -
{t("ribbon.custom.empty")}
- )} - {items.map((item, i) => ( - - ))} - -
-
{t("ribbon.group.custom")}
-
- ); -} - -export function RibbonBar(props: RibbonBarProps) { - const current = RIBBON_TABS.find((tt) => tt.id === props.tab) ?? RIBBON_TABS[0]; - const ctx: ButtonCtx = { - activeTool: props.activeTool, - activeCommand: props.activeCommand, - toolsEnabled: props.toolsEnabled, - onSelectTool: props.onSelectTool, - onRunCommand: props.onRunCommand, - }; - - return ( -
- {/* Der „Eigene"-Tab rendert die modulare Custom-Bar; sonst die - datengetriebenen Item-Gruppen. */} -
- {current.id === "custom" ? ( - - ) : current.groups.length === 0 ? ( -
{t("ribbon.empty")}
- ) : ( - current.groups.map((g) => ( -
-
- {g.items.map((item) => ( - - ))} -
-
{t(g.titleKey)}
-
- )) - )} -
-
- ); -}