diff --git a/HANDOVER.md b/HANDOVER.md index 214316b..165003a 100644 --- a/HANDOVER.md +++ b/HANDOVER.md @@ -41,7 +41,302 @@ Code/Doku noch in der Git-Historie. vor dem Edit frisch lesen (der „modified since read"-Guard verhindert blindes Überschreiben). -## >>> STAND 2026-06-30 (Fortsetzung, spät) — ZUERST lesen <<< +## >>> AUFGABE FÜR NEUE INSTANZ: 2D-Plan auf WebGL-GPU-Renderer (ZUERST) <<< + +**Warum:** Tauri-Linux-Webview = **WebKitGTK**, das SVG/2D auf der **CPU (Cairo)** rastert. +User-GPU (AMD RX 7800 XT) langweilt sich; bei **144 Hz** (~6,9 ms/Frame) ruckelt Pan/Zoom/ +**Objekt-Bewegen** sichtbar. three.js/WebGL (3D) läuft GPU-beschleunigt → NUR der 2D-SVG-Pfad +ist CPU-gebunden. Entscheidung mit User: 2D-Plan **von SVG auf WebGL2 (GPU)** umbauen. + +**Architektur (mit User abgestimmt):** +- **Hybrid:** WebGL2-Canvas rendert die schwere Geometrie (Poché-Polygone, Linien, Schraffuren, + Kreise/Bögen als Segmente). **Dünne SVG-Ebene DARÜBER** behält Text (Raumstempel/Labels), + Griffe, Snap-Marker, Werkzeug-Vorschau (wenige Elemente → billig, DOM-Hit-Test + scharfer Text bleibt). +- **Raw WebGL2, KEINE neue npm-Dependency** (User will nicht „abhängiger" werden; nativ). +- **Geometrie pro Plan EINMAL tessellieren** (Cache), Pan/Zoom = nur Transform-Matrix-Uniform → + keine Re-Tessellation, GPU-buttrig. Objekt-Bewegen: Plan ändert sich → re-tessellieren (leichter als SVG-Reconcile+Cairo). +- **Linienbreite konstant in Screen-px** (wie heutiges `non-scaling-stroke`): im Vertex-Shader + expandieren (Position world → clip, dann Normalen-Offset in clip via Viewport-px). Miter an Ecken + später; Spike: Quads pro Segment. +- **SVG-Renderer als Fallback hinter einem Flag behalten**, bis GL-Parität erreicht → kein Risiko. +- Vorgehen: **erst Spike** (Linien+gefüllte Polygone + GPU-Pan/Zoom), Glätte bei 144 Hz messen + (dev :5187 läuft; `tauri:dev`-Fenster läuft), DANN Schraffur/Feinschliff/Text-Overlay/Hit-Test. + +**Fixpunkte im Code:** +- `src/plan/PlanView.tsx` (~2500 Z.): `toScreen()` = Modell-m → viewBox-Einheiten, FIXER Ursprung, + `PX_PER_M=90`; `view={x,y,w,h}` treibt ``; Pan = setView, Zoom = setView(onWheel). + BEREITS optimiert: `primitiveEls`/`drawingRunEls` `useMemo` (Pan/Zoom 312→0 Re-Renders) + + rAF-Coalescing beim Objekt-Drag (`flushDragMove`/`enqueueDragMove`, 40→1 Update/Frame). Diese + bleiben nützlich; der GL-Renderer ersetzt die SVG-Primitiv-Ebene, NICHT die Interaktions-Logik/Griffe. +- `src/plan/generatePlan.ts`: `export type Primitive` (Z.133) — kinds: `polygon`{pts,fill,stroke, + strokeWidthMm,hatch,*Id}, `line`{a,b,weightMm?,strokeWidthMm?,dash?,color,cls,greyed,drawingId?}, + `arc`{center,r,…}, `text`{at,doc,extraLines,basePt,color} (Text → SVG-Overlay, NICHT WebGL). +- Koordinaten/Breiten-Logik im SVG-Renderer: `PrimitiveShape` (~Z.2280) + `DrawingRunShape` — dort + steht, wie Haarlinie/mm/`paperScale`/`non-scaling-stroke` die Strichstärke bestimmen (für GL nachbilden). +- Hit-Test/Auswahl/Griffe laufen weiter über die bestehenden PlanView-Pfade (modellbasiert); GL ist nur Anzeige. +- **Vektor-PDF/-Export bleibt** (kommt aus `generatePlan`/DXF, nicht aus der Anzeigefläche) — NICHT anfassen. + +**Gates:** `npx tsc -b` 0, `npm run build` grün, Trace-Scan sauber (keine KI-Spuren, COMMIT-REGEL oben). +Kein Commit ohne Ansage. Verifizieren wie üblich per Puppeteer gegen :5187 (Render korrekt + Glätte messen). + +**GESAMT-TODO-LISTE (Stand 2026-07-01 Nacht) — im Hinterkopf behalten:** +ERLEDIGT+verifiziert: U/I/O/P-Kopiermodi · Backlog#1 Transform→CommandLine · Backlog#4 3D-Zeichnen + +Engine-Confirm-Bugfix · Raumstempel#2 (Feld-Modell) · Plan-Perf (Memo + rAF-Coalesce) · Tauri-M1-PoC +(geometry-Crate 4/4 + Parität + `tauri:dev`-Boot). OFFEN: **(A) 2D-WebGL-Renderer [DIESE AUFGABE]** · +(B) Tauri-M1 Rest: Compute-Boundary in Render-Pfad einhängen (generatePlan sync → precompute+cache) + +nächste Ops (kernel2d→DXF/DWG→detectRooms) · (C) three.js→wgpu (M2, nach 2D-WebGL) · Parametric Walls +Phase B (andere Instanz) · Wand-Referenzlinien-Option unter Bezugspunkt · Viewport/Editier-Welle +(Treppen-Griffe, Öffnung-entlang-Wand, 2D auf Zeichenebenen, runde Linienenden, schattiert-Politur) · +Smart-Join/Split + nahtfreie Wandverschneidung · Text/Stempel-Annotation (Frame+Rotation) · Welle B +(3D-Schnitt+Stencil-Capping) · Welle C (HLR verdrahten) · Overrides/Ausschnitte/Multi-Page-Layouts. + +## >>> STAND 2026-07-01 (Tauri-PoC — App-frei, gelandet) <<< + +**Tauri v2 + Rust-Compute-Boundary — Proof-of-Concept steht (3 App-freie Agents).** +Alle Gates grün, nichts an `App.tsx`/`src/model/` angefasst (nur NEUE Dateien + +additive Config). Ref: `docs/design/tauri-migration-plan.md` / `tauri-architecture.md`. + +- **`src-tauri/` (Rust, neu):** Cargo-**Workspace** aus zwei Crates: + - `src-tauri/geometry/` — **serde-only** Lib, Port von `computeJoins` (aus + `src/model/joins.ts`) inkl. Vektor-Helfer. Input flach: `WallInput{ id,start,end, + thickness,referenceOffset }`; Output `WallCuts{ wallId,startCut,endCut }` (Line= + point+dir). **`cargo test` grün: 4/4** (L-Ecke=geteilte Gehrung, freies Ende, + T-Stoss, kollinear → alle korrekt). Braucht KEIN webkit → hier testbar. + - `src-tauri/` App-Crate (`cad-tauri`) — Tauri-v2-Shell (`lib.rs` mit + `#[tauri::command] compute_joins` + `run()`, `main.rs`, `build.rs`, + `tauri.conf.json` devUrl **:5187**, `icons/icon.png`). **`cargo check` GRÜN** — + wider Erwarten NICHT von webkit blockiert: tauri v2 bindet `webkit2gtk-4.1` + (vorhanden); nur `-6.0` fehlte. `src-tauri/target` + `gen/` sind Build-Artefakte + (gitignored). +- **`src/compute/index.ts` (neu, Boundary):** einzige Stelle für schwere Ops. + `computeJoins(project,walls)` flacht ab → `invoke("compute_joins")` (geguardeter + dynamischer Import von `@tauri-apps/api/core`, Build bleibt grün OHNE das Paket) + → bei null/Fehler `console.warn` + TS-Fallback (`joins.ts`). Zusätzlich + Durchreicher `detectRooms` (roomBoundary.ts) + `parseShapeFromDwg`(=`parseDwg`); + `computeKernel2D` bewusst ausgelassen (kein Einzel-Entry, ~30 Primitive). +- **`package.json`/`vite.config.ts`:** `@tauri-apps/cli`+`api` (v2) installiert, + Scripts `tauri`/`tauri:dev`/`tauri:build`; vite Port 5187 `strictPort`, + `clearScreen:false`, `envPrefix` — rein additiv. + +**NÄCHSTE SCHRITTE (Tauri):** (1) **Echte TS↔Rust-Numerik-Parität** end-to-end prüfen +— erst sinnvoll, wenn die Boundary in einen Caller verdrahtet ist (aktuell bewusst +NICHT verdrahtet, weil das `generatePlan`/`App` async machen würde). (2) `computeJoins` +in den Wand-Render-Pfad einhängen (async-Welle). (3) `tauri:dev` end-to-end auf einer +Maschine MIT `webkit2gtk` (hier nur `cargo check`, kein Fenster-Boot getestet). +(4) Nächste Ops laut Plan: `kernel2d` → DXF/DWG → `detectRooms`. + +## >>> STAND 2026-07-01 (Parametric Walls Phase A — ISOLIERT) <<< + +**Neue Instanz (Opus) — Parallelisierung mit Agents (Sonnet):** +- **Branch:** `feature/parametric-walls` (isoliert, keine App.tsx-Änderungen) +- **Phase A FERTIG (alle Agents grün):** + - **Agent 1:** Types (`ParametricWall`, 5 `ParametricRule` Varianten) + Engine + (`resolveParametricWall()`, 5 rule handlers, helpers) in `src/model/parametricWalls.ts` + - **Agent 2:** 50 Unit-Tests (Vitest), alle grün; `npm test -- parametricWalls.test.ts` + - **Agent 3:** Design-Doku `docs/design/parametric-walls.md` + `docs/README.md` aktualisiert + - Verification: `tsc -b` ✓, `npm run build` ✓ (577 modules, 3.72s) +- **Phase B (UI-Integration) — für nächste Instanz mit Agents:** + - Agent 1: TopBar ParametricWall-Picker + UI-Komponente + - Agent 2: Command `pw` (`cmds/parametricWall.ts`) + - Agent 3: ResourceManager-Integration (ParametricWall CRUD) + - Dann merge → `master`, Screenshot-Verifikation +- **Koordination:** andere Instanz committet parallel (Text, Raum, HLR) auf `master`, + keine Konflikte (nur `types.ts`+`src/model/` erweitert, nicht `App.tsx`). + +**Backlog #4 ERLEDIGT (Zeichnen im 3D-View — vorherige Instanz):** Werkzeug-Gate `toolsEnabled` + (`App.tsx` ~1877) von `viewType === "grundriss"` auf `activeLevel.kind === + "floor"` erweitert → Werkzeuge sind jetzt AUCH in der Perspektive aktiv. Die + gesamte Zeichen-Pipeline war bereits verdrahtet (`Viewport3D.workplaneModel()` + raycastet den Cursor auf die Geschoss-OKFF; `onWorkplanePoint`/`onWorkplaneConfirm` + speisen dieselbe Engine wie der Grundriss). Ende-zu-Ende per Puppeteer verifiziert: + in iso-Ansicht Linie mit 2 Klicks gezeichnet → committet (`draw2d` 0→1, Prompt + zurück auf „Befehl:"); Picks liefern gültige Modell-Vec2 (raycast korrekt). +- **BUG-FIX unterwegs (Engine-Confirm, `src/commands/engine.ts` `confirm()`):** + Beim Testen von #4 fiel ein VORBESTEHENDER Bug auf (reproduziert IDENTISCH im + Grundriss, nicht durch #4 verursacht): mehrteilige Befehle (Wand/Polylinie) + liessen sich per Enter/Rechtsklick NIE abschliessen. Ursache: `confirm()` nahm + IMMER den Tab-Feld-Zweig (`fieldResultPoint`), sobald der Schritt Felder hatte — + auch OHNE gelocktes Feld → es wurde ein Cursor-Punkt gefüttert (bei Wand: Null- + Strecke, ignoriert) statt `onConfirm` (das den Zug committet) aufzurufen. Fix: + Feld-Zweig nur noch bei tatsächlich GELOCKTEM Feld (`Object.keys(this.locks).length + > 0`); ohne Lock fällt `confirm()` auf `onConfirm` durch. Verifiziert: Wand + schliesst jetzt per Rechtsklick im Grundriss (91→98) UND in 3D (99→106); Tab-Feld- + Fluss „Länge 4 tippen → Enter lockt → leeres Enter setzt Punkt" unverändert; + Linie-Auto-Commit (2 Klicks) unverändert. Gegatet: `tsc -b` 0, `build` grün, keine + Konsolenfehler. KEIN Commit. + +- **Backlog #1 ERLEDIGT (Befehlsleiste-Transform vereinheitlicht):** Die laufende + Auswahl-Transformation (nur noch `rotate`/Taste `d` mit U/I/O/P; move/mirror + laufen längst über die Engine) speist jetzt die `CommandLine` statt der + schwebenden `TransformBar`. Dritter Zweig in der ``-Ternäre parallel + zu `gripEdit` (`gripEdit ? … : activeTransform ? … : engineView…`): Prompt = + `transform.hint.${op}.${…}`, Inline-Optionen = U/I/O/P (`transformOptions()`, + aktiver Modus hervorgehoben via neuem `active?`-Feld an `CommandLineOption` + + `.cmdline-option.active` in styles.css, `×count` bei array/distribute), Live-/ + lockbares **Winkel**-Feld beim Drehen bzw. **Distanz** beim Bewegen + (`transformFields()` aus neuem `transformDragInfoRef`), getippte Zahl committet + über `submitTransformValue()`. `d` fokussiert die CommandLine (wie `m`/`s`). + `src/ui/TransformBar.tsx` gelöscht; verwaiste `.transform-bar`/`.tf-*`-CSS + + `transform.bar`-Key harmlos stehen gelassen. Gegatet: `tsc -b` 0 Fehler, `build` + grün, Trace-Scan sauber, Puppeteer-Probe end-to-end (nach `d`: „Drehzentrum + klicken" + U/I/O/P; nach Zentrum+Bezug: „Zielwinkel klicken" + Live-Winkelfeld; + keine Konsolenfehler). KEIN Commit (Konvention der Vorsessions beibehalten). + +## >>> STAND 2026-07-01 (Abend) — Raum + Topbar-Umbau + Backlog — ZUERST lesen <<< + +Alle unten genannten Agenten GRÜN gelandet und gegatet (`tsc -b` 0 Fehler, +`npm run build` grün, Trace-Scan sauber, Boot-Probe ohne Konsolenfehler). Dev- +Server läuft/hält auf :5187. KEIN Commit gemacht (Commit-Regel oben beachten). + +**Diese Session gelandet:** +- **Rich-Text-Kern (komplett):** `src/text/richText.ts` (Marks/RichTextDoc/TextRange, + toggleMark/applyMark/isMarkActive/DEFAULT_PRESETS/serialize…), `src/text/renderHtml.ts` + (docToHtml/docToLines/docToSvgText), `src/text/RichTextEditor.tsx` (contentEditable, + Toolbar B/I/U/S + Grösse/Farbe/Presets). +- **Raum-Slice (SIA-416), Agent gelandet:** `Room` in `model/types.ts` (id, floorId, + categoryCode "60", siaCategory HNF/NNF/VF/FF/KGF, boundary, stampAnchor?, stampDoc?); + `cmds/room.ts` (Modus inside=click-inside via `roomFromPointInside`, manual=Polylinie); + Plan: neues `text`-Primitive + Füllpolygon + Stempel via `docToLines` + Live-Fläche; + `panels/RoomBalancePanel.tsx` (SIA-Bilanz + CSV). **App-State für Stempel:** + `selectedRoomId`/`selectedRoom.stampDoc`, `projectSlice.setRoomStampDoc(roomId,doc)`, + `moveRoomStamp`, `setRoomStampAnchor`, `stampEditorRoomId`. Stempel-Editor = kleines + schwebendes Fenster (kein Footer). +- **DOSSIER-Referenz studiert → Spec `docs/design/topbar-dossier.md`** (verbindlich für + Topbar-Arbeit; Referenz-Klon lag unter `/tmp/DOSSIER`). Kernbefund: Text-Styling ist im + Original eine FEST in der Oberleiste sitzende Text-Gruppe (Stil/Font/Grösse + B/I/U + + L/C/R + „+Text"), Akzent-Glow bei Auswahl; Raumstempel-Typografie darüber; Text-INHALT + in eigenem kleinem Fenster; Zoom/Massstab als 2×2-Cluster (kombinierte Stat-Pille). +- **Topbar-Umbau (Agent gelandet), `src/ui/TopBar.tsx` + `styles.css` + `App.tsx` + + `ui/TextEditorDialog.tsx` (neu) + i18n:** Zoom-Doppelung entfernt; 2×2-Massstab/Zoom- + Cluster; einheitliche Segmentpillen (neue `Segment`-Komponente); **Text-Gruppe** in der + Topbar (`textTarget`-Prop = `null | {doc,range,apply}`, aus selektiertem Raum abgeleitet, + `apply`→`setRoomStampDoc`; ohne range wirkt Format auf ganzes Doc); Doppelklick öffnet + `TextEditorDialog`. Verwaiste alte CSS-Regeln (`.tb-zoom*`, `.stamp-editor*`) bewusst + stehen gelassen (harmlos). „+ Text"-Button verdrahtet, aber DEAKTIVIERT (noch kein + Text-Annotations-Werkzeug). + +**Backlog / nächste Schritte (Priorität dieser Session, siehe auch Todo-Liste):** +1. **Befehlsleiste-Transform vereinheitlichen:** laufende Transformation (move/mirror/rotate) + soll die `CommandLine` speisen (Prompt „Basispunkt/Zielpunkt" + U/I/O/P als Inline- + Optionen) statt der separaten schwebenden `TransformBar`; freies Befehl-Tippen während der + Geste unterdrücken, Zahleneingabe (Distanz/Winkel) via `fields` bleibt. Muster: parallel + zum bestehenden `gripEdit`-Zweig bei `` (~App.tsx:2793). U/I/O/P-Routing + (App.tsx ~1543–1548) NICHT kaputt machen — nur zusätzlich in die CommandLine spiegeln. +2. **Strukturierter Raumstempel (DOSSIER `RaumProperties`):** Stempel ist KEIN Freitext, + sondern ein FELD-Modell: Raumnummer · Raumname · Raumname-Zeile2 (in Listen als ein Name + zusammengezogen) · Bodenfläche (an/aus + Präfix) · Fensterfläche (an/aus + Präfix) · + Nutzung HNF/… (an/aus). Zeilen-Layout-Editor (welches Feld auf welche Zeile) + Typografie + PRO Feld-Einheit (Raumname grösser, andere kleiner, eine Zeile kursiv). Ersetzt den + Freitext-`stampDoc`-Zwischenstand; die Topbar-Text-Gruppe formatiert dann die gewählte + Feld-Einheit (löst auch die „ganzes Doc statt Selektion"-Einschränkung). +3. **Wand-Optionen:** Referenzlinien-Option UNTER den Bezugspunkt setzen, gleiche Optik/ + Anordnung wie die übrigen Optionen (`cmds/wall.ts` / ToolsPanel-Optionsreihenfolge). +4. **Zeichnen im 3D-View:** Cursor per Raycast auf die Arbeitsebene (okff des Geschosses) + projizieren → Weltkoordinaten als `engine.move/pick` einspeisen; `toolsEnabled`-Gate + (App.tsx ~1834, heute `viewType==="grundriss"`) auch für Perspektive öffnen. +5. **Viewport/Editier-Welle:** Treppen-Griffe (App.tsx grips-memo hat keinen stair-Fall), + Öffnung entlang Host-Wand verschieben (kein opening-Handler im Body-Move; `moveOpeningBy` + fehlt), 2D-Tools auf Zeichnungsebenen (`toolsEnabled` erweitern + Bau-Tool-Buttons grauen), + runde Linienenden (`stroke-linecap:round` auf offene Linien), „schattiert"-Politur + (ACES-Tonemapping+sRGB, HemisphereLight/RoomEnvironment-PMREM, MeshLambert→MeshStandard in + `Viewport3D.tsx` ~416–449). +6. **Smart-Join/Split + NAHT-FREIE Wandverschneidung** (Poché-Union an Stössen, keine sichtbare + innere Naht, Gehrung/T-Stoss). 7. **Text/Stempel-Annotation** (aktiviert „+Text"; + Frame+Rotation). 8. **Welle B** (3D-Schnittebene + Stencil-Capping + Schnittschraffur). + 9. **Welle C** (HLR verdrahten; `src/section/hlr.ts` bereit). 10. Overrides/Ausschnitte/ + Multi-Page-Layouts. + +**Kollisions-Regel bestätigt:** `App.tsx`, `types.ts`, `generatePlan.ts`, `Viewport3D.tsx`, +`registry.ts`, `PlanView.tsx`, i18n, `styles.css` sind Hotspots → nur EIN App.tsx-Agent pro +Welle; parallele Tracks nur auf genuin App-freien neuen Modulen. + +## >>> STAND 2026-07-01 (Bug-Batch Runde 1) — ZUERST lesen <<< + +Runde-1-Agenten alle GRÜN gelandet (`tsc -b` + build + Probe verifiziert): +- **3D-Editier-Kamera-Bug (kritisch, `src/viewport/Viewport3D.tsx`):** Ursache = schwerer + Szenen-Aufbau-Effekt mit `project`-Dependency → jede Griff-Mutation baute Szene neu auf und + `applyView3d` resettete die Kamera auf Preset (→ „springt in Top-View", Rechts/Links-Pan-Illusion). + Fix: `camStateRef`-Snapshot (Pos/Target/up/ortho-Zoom/aktive Kam) im Render-Loop fortgeschrieben, + bei Neuaufbau WIEDERHERGESTELLT statt `applyView3d`; `applyView3d` nur beim ersten Aufbau; + `view3d`-Effekt guarded via `gripDragRef`; Top-View `enableRotate=false`; 2D-Drawings bekommen + `move`-Griff (Anker=center/at) → in 3D verschiebbar. Kamera pos/target numerisch identisch vor/nach + Drag/Löschen bewiesen. +- **Print-Linienstärke invertiert → korrekt (`src/plan/PlanView.tsx`, `styles.css`):** Print zeichnet jetzt + echte Papier-mm in viewBox-Einheiten OHNE `non-scaling-stroke`: `strokeVb=(mm·N/1000)·PX_PER_M`, + N=`paperScale` stabil (nur bei `applyScale`/erstem Messen gesetzt). Rein=dicker, raus=dünner; + 1:100 vs 1:10 = exakt ×10. Display/Haarlinie unverändert. **Ecken-Miter:** CSS `stroke-linejoin:miter` + + `buildDrawingRuns` bündelt zusammenhängende `line`-Primitive gleicher drawingId zu einem + `polyline`/`polygon` (Ringschluss erkannt) → gehrte Ecken statt Butt-Cap-Stufe. Primitive-Modell + (und Print-Export) unverändert. +- **ambientCG-Live-Bibliothek (`src/materials/ambientcg.ts` neu, `ResourceManager.tsx`):** komplette CC0-Lib + live durchsuchbar (Suche/Kategorie/Auflösung 1K-4K), jszip-Entpackung → Blob-URLs, NormalGL bevorzugt. + **CORS:** Such-JSON + `/get` brauchen Proxy → Vite-Dev-Proxy `/ambientcg` in `vite.config.ts` + (`VITE_AMBIENTCG_PROXY` für Prod); optionale Prod-Route `openbureau-core/api/src/routes/ambientcg.js` + (lesend, gemountet VOR Auth-Gate). Thumbnails direkt (ACAO:*). Offline → Schnellauswahl-Fallback. + +**Runde 2 FERTIG (2026-07-01, alle GRÜN + Probe-verifiziert):** +- **2a** [App/engine/editors/registry, neu `cmds/mirror.ts`+`cmds/join.ts`]: Snapping erster Punkt gefixt + (Modifier shift/ctrl wurden im Command-Pfad verschluckt → jetzt via `EngineMods` durch `move/pick`→`host.snap` + gefädelt); Segment-Move für offene Polylinien/Linien (`EdgeGrip.free`, ganzer 2D-Delta, Nachbar-Segmente + folgen über geteilte Vertices); Kürzel↔Befehl vereinheitlicht (`M`→engine.start("move"), `S`→"mirror", + `Ctrl+J`→"join", identisch zum Tippen; `Command.autoRun` für nicht-interaktive Befehle; `CommandSelection.drawingIds` + für Mehrfachauswahl); **Mirror-Befehl** (2-Punkt-Achse, Vorschau, `commitTransform mirror/copy`). +- **2b** [cmds/{polyline,rect,wall}]: Polylinie **offen/geschlossen** (Toggle `m`, Ring-Fill); Rechteck **2pt/3pt/Zentrum** + (3pt = gedreht → closed polyline); **Wandstärke-Tab-Feld** nur bei einschichtigen Wandtypen (baut on-commit + einen einschichtigen Wandtyp der Zieldicke, Original bleibt). +- **Swisstopo/OSM-Kontext-Importer** [io/{lv95,geoContext,swissTopo,osm}, ui/ContextImportDialog, SitePanel]: + „Standort importieren" — geocode (geo.admin SearchServer, LV95 direkt), swisstopo-Gebäude + (`MapServer/identify` layer `ch.swisstopo.vec25-gebaeude`), OSM-Overpass (Gebäude/Strassen/Wasser/Grün). + LV95↔WGS84 (Bern round-trip 0.38 m) + Origin-Shift. CORS: geo.admin direkt, Overpass via neuer read-only-Route + `openbureau-core/api/src/routes/geoproxy.js` (vor Auth-Gate, host-allowlist) wenn `VITE_GEO_PROXY` gesetzt. + +**TIER-1 FORTSCHRITT (2026-07-01):** +- **DXF-Vektor-Export FERTIG** [src/export/{dxfWriter,exportDxf}, ui/ExportDxfDialog, TopBar+App-Button]: + hand-rollierter R2000/AC1015-ASCII-Writer, `$INSUNITS=6` (Meter), 23 Layer aus Kategorien (ACI+True-Color+lw), + LINE/LWPOLYLINE(closed)/CIRCLE/ARC/TEXT; Quelle = `generatePlan()` in Meter-Modellraum. 5 m-Wand→5.345 m Span + verifiziert. DWG NICHT geshippt (libredwg-web nur lesend — ehrlich weggelassen). +- **Decke (Ceiling) FERTIG** [geometry/ceiling, cmds/ceiling(alias d/decke), types.ts `Ceiling`, projectSlice/selectionSlice, + generatePlan `addCeilingPoche`, PlanView-Hittest, Viewport3D `addCeilingMesh`(Extrude+Material+Modi), ToolsPanel-Button+ + Deckentyp-Picker, ObjectInfoPanel `CeilingSection`, host.ts, App-Grips/Selection]. Wand-Muster: floorId + categoryCode "30" + + wallTypeId/Component + VerticalAnchor(OK=Geschoss-Top) + optionale Dickenüberschreibung; statt Achse/Band ein + closed `outline: Vec2[]`. Plan = Umriss-Polygon (Fill+Hatch + schwere Outline) VOR den Wänden gezeichnet. Grips Vertex+Edge+Body. +- ⚠️ MERKE: **App.tsx ist der Serialisierungs-Engpass** — pro Welle darf NUR EIN Agent App.tsx editieren (DXF+Decke + liefen versehentlich gleichzeitig darauf; Merge hat diesmal geklappt, aber nicht drauf verlassen). + +- **Öffnung (Fenster/Tür) FERTIG** [geometry/opening, cmds/opening(alias f/fenster, t/tuer/tür, oe), types.ts `Opening`, + generatePlan(`wallGaps`), PlanView, Viewport3D(`addWallMeshes` segmentiert + Sturz/Brüstung, `addOpeningMeshes` Rahmen/Glas/Flügel), + slices, App, ToolsPanel(Fenster+Tür-Buttons), ObjectInfoPanel(editierbar)]. **Host-Sync:** Öffnung speichert KEINE + Weltkoordinaten — alles wird pro Render aus `wall.start/end` neu abgeleitet → Wand verschieben bewegt Öffnung automatisch. + Plan-Symbole je Detailgrad (Tür: Blatt→+Schwenkbogen→+Anschlag; Fenster: 1→+Rahmen→+2 Glaslinien). +- **SIA-416-Engine FERTIG** [geometry/{roomArea,roomBoundary} — pure, isoliert, 34/34 Tests]: Raumerkennung + (planar-arrangement+half-edge face-tracing, `detectRooms`/`roomFromPointInside`), Fläche/Umfang/Zentroid, + SIA-Hierarchie (GF→KGF+NGF; NGF→NF(HNF/NNF)/VF/FF), `balance()`, `roomsToCsv`. Bereit für den Raum-Slice. + +- **Treppe FERTIG** [geometry/stair, cmds/stair(alias treppe/tp — `tr`=trim!), types.ts `Stair`, model/wall `stairVerticalExtent`, + generatePlan `addStairSymbol`(Schnittbruchlinie+Pfeil), Viewport3D `addStairMeshes`(Stufenprofil), slices/App/Panels/i18n]. + gerade/L/Wendel (Wendel = vereinfachtes Keil-Modell), SIA-17/29, geschoss-übergreifend (totalRise=Geschosshöhe). +- **HLR-Spike FERTIG (Welle-C-Fundament)** [src/section/{hlr,occt,occt-wasm.d}.ts, docs/welle-c-hlr-spike/, package.json + +opencascade.js, vite.config aliases]. **API-Pfad = `HLRAppli_ReflectLines`** (NICHT HLRBRep_Algo — im Prebuilt nicht + konstruierbar). Lazy via dynamic import, Haupt-Bundle unberührt. 62 MB WASM → später Custom-Build auf 5-15 MB. + +**SESSION-LIMIT-STOP (2026-07-01 ~11:00, reset 13:00 Europe/Zurich):** Raum- + Rich-Text-Agenten wurden vom +Session-Limit abgeschnitten. Repo ist trotzdem GRÜN & stabil (tsc+build grün, dev :5187 läuft). Genauer Stand: +- **Raum: NICHT begonnen** — kein `cmds/room.ts`, kein `Room`-Typ, keine Verdrahtung. Sauber neu starten. SIA-Engine + (`geometry/roomArea.ts`+`roomBoundary.ts`) liegt fertig & getestet bereit zum Import. +- **Rich-Text: FAST FERTIG** — `src/text/richText.ts` (Doc-Modell + Marks + plainText/applyMark/toggleMark/serialize/Presets) + FERTIG. `src/text/renderHtml.ts` (docToHtml für Editor + docToLines/docToSvgText für SVG-Plan, gemeinsame Mark→Stil-Map) + JETZT FERTIG & kompiliert (im Hauptthread nachgezogen, da Agent am Limit war). FEHLT nur noch: `src/text/RichTextEditor.tsx` + (WYSIWYG-Komponente mit Toolbar bold/italic/underline/strike/Grösse/Farbe + Presets, controlled: value/onChange). +- Zwei Helfer-Subagenten ("Map/Extract …wiring in App.tsx") liefen ebenfalls ins Limit; App.tsx ist laut tsc+build INTAKT. + +**RESUME NACH RESET (Reihenfolge):** (1) Rich-Text-Kern fertig (renderHtml + Editor-Komponente) — App-frei, isoliert. +(2) Raum-Slice (nutzt SIA-Engine, click-inside, Stempel, Bilanz+CSV) — App-Slice. (3) Text/Stempel-Slice (nutzt Rich-Text-Kern). +(4) Smart-Join/Split. (5) Welle B (3D-Schnitt+Stencil-Capping) → Welle C (HLR verdrahten, `src/section/hlr.ts` bereit). +(6) Overrides-Engine, Ausschnitte, Multi-Page-Layouts. REGEL: nur EIN App.tsx-Agent pro Welle; Parallel-Track nur wenn App-frei. + +--- + +## >>> STAND 2026-06-30 (Fortsetzung, spät) <<< Fortsetzungs-Lauf (UI + Import + Wand-Attribute + Selektion/Editieren). Alles unten ist `npx tsc -b` + `npm run build` GRÜN und per Screenshot/Probe verifiziert, @@ -49,6 +344,32 @@ außer „LÄUFT" markiert. Viel wurde an **Subagenten** delegiert (Dateien land auf der Platte, unabhängig von Benachrichtigungen). **Fertig & verifiziert (diese Fortsetzung):** +- **Vektor-PDF-Export des Grundrisses (KERN-ZIEL — fertig & verifiziert):** + `src/export/planToPrintSvg.ts` (Druck-SVG aus `generatePlan`, mm-Mapping im Maßstab, + echte mm-Linienstärken/ISO-Stifte, Schraffuren als Vektorlinien) + `src/export/exportPdf.ts` + (`jspdf`+`svg2pdf.js` → echtes Vektor-PDF, A4/A3 ×Hoch/Quer, Schriftfeld). TopBar-Button + „PDF" + `src/ui/ExportPdfDialog.tsx`. Verifiziert: 522 Pfade, 0 Rasterbilder, maßstabsgenau + (5,345×4,345 m → 53,45×43,45 mm @1:100). `scripts/probe-pdf.mjs`. +- **2D-Boolean (Union/Differenz/Schnitt):** `src/editors/booleanOps.ts` (polygon-clipping), + Kontextmenü „Vereinigen/Subtrahieren/Schneiden" + Ctrl/Cmd+Shift+U/D/I; geschlossene + Drawing2D, Differenz=erstes Element minus Rest, Loch→Außenring gefüllt + Innenringe als + Umriss. Verifiziert (Union 8/Differenz 6/Schnitt 4 Ecken). `scripts/probe-boolean.mjs`. +- **3D-1 — ERSTELLEN in 3D (Meilenstein):** der 3D-Viewport ist Erstellungs-Fläche; + dieselben Befehle (line/wall/rect/polyline) erzeugen Elemente direkt in 3D. Raycast auf + Geschoss-Arbeitsebene (`THREE.Plane` y=gridElevation) → Modell-`Vec2` (model.x=P.x, + model.y=P.z, aus `addDrawing2DLines` abgeleitet) → dieselbe `CommandEngine` + (`move/pick/confirm`). Live-3D-Draft (Rubber-Band + Snap-Marker, eigene `draftGroup`), + OrbitControls bei aktivem Befehl umgeschaltet, `pxPerMeter`-Fallback 90. Viewport3D-Props + `commandActive`/`onWorkplanePoint`/`onWorkplaneConfirm`/`draft`. Verifiziert (Wand in 3D → + in 3D + Plan sichtbar). `scripts/probe-3d-create-*.png`. +- **3D-2 — EDITIEREN in 3D (Meilenstein):** 3D-Griffe (eigene `gripGroup`) bei genau 1 + Auswahl + `!commandActive`. Wand: Endpunkt-Kugeln (Achse), Höhen-Griff (OK via + `updateWall top:{mode:"custom",z}`), Verschiebe-Griff; Drawing2D: Vertices. Drag raycastet + auf Arbeitsebene bzw. vertikale Ebene → DIESELBEN Store-Actions wie Plan-Griffe + (`moveGripOf`/`moveElementByOf`/`updateWall`) → Plan+3D konsistent. Laufender Drag in + `gripDragRef` (übersteht den Szenen-Neuaufbau pro Move). Verifiziert (Endpunkt + Höhe in 3D + editiert, im Plan deckungsgleich). `scripts/probe-3d-edit-*.png`. **Offen (→ Editier-Welle 3): + 3D-Snap beim Drag + Griffe „immer oben" (Tiefentest).** - **Zwei-Ton-Dark-Theme + KEIN Petrol-Grün mehr:** `src/styles.css` Dark-Tokens neutral (`--bg #0e0e0e`, `--panel #1d1d1d`, `--accent #4d4d4d` …); Kontextmenü-Tokens `--ctx-hover/-text` neutral; grüner Fokus-Ring entfernt. @@ -88,6 +409,47 @@ auf der Platte, unabhängig von Benachrichtigungen). 25 ok), `src/editors/splitJoin.ts`, App-Keydown (Ctrl/Cmd+S/J, `preventDefault`) + `onSegmentCut`, PlanView Alt-Klick. Verifiziert (Rechteck→2 geschlossene, Join→1 Polylinie, Alt→Segment weg). `scripts/probe-splitjoin.mjs`. Wände bleiben no-op. +- **Editier-Welle 3 + Eingabe-Vereinheitlichung + Snap-Feinschliff (FERTIG):** 3D-Drag-Snap + (gleiche `computeSnap`) · Griffe/Marker immer-oben · koinzidente Endpunkt-Griffe ziehen + gemeinsam (Viewport3D). **GUI-Werkzeug** (line/polyline/rect) → `engine.start` + + Befehlsfeld-Fokus; **Shift = H/V-Ortho** beim Griff-Drag (2D+3D, `applyAngleConstraint` + aus `tools/snapping`). OFFEN: „Wand" noch Legacy (Wand-als-Command); Editieren läuft noch + AD-HOC, nicht durch die Engine → Tab-Wert + Referenz-Tracking offen. Snap-Marker + vereinheitlicht: 2D Endpunkt=Kreis/Mittelpunkt=Strich, 3D-Snap=`#ffcc44`, Marker kleiner; + 2D-Vorschaulinie grau-Volllinie. +- **DWG-Parser erweitert (FIX „keine Geometrie gefunden") + Trim (FERTIG):** `io/dwgParser.ts` + neu — ARC/CIRCLE/ELLIPSE/SPLINE + **INSERT/Block-Expansion** (rekursiv, aus + `db.tables.BLOCK_RECORD.entries`, volle 2D-Affin-Transform) + Diagnose-Histogramm + (`DxfImportResult.diagnostics`, in ImportDialog). Real-DWG 37 INSERT → 67k Konturen. + **Trim** (`commands/cmds/trim.ts`, Alias `tr`, `kernel2d.trimPolyline`) — Quick-Trim bis + Schnittpunkt. Beide verifiziert. +- **Material-Bibliothek (Daten, FERTIG):** 12 CC0-Materialien von ambientCG → + `public/assets/materials//{color,normal,roughness,displacement,ao}.jpg` + `manifest.json` + + `src/materials/library.ts` (PBR). +- **Welle A — 3D-Materialien (FERTIG & verifiziert):** `Component.material` (`ComponentMaterial`: + PBR-Map-URLs + `sizeM` Kachelgröße), `src/materials/runtime.ts` (`MaterialRuntime` → gecachte + `MeshStandardMaterial`, Kacheln in Welt-Metern via Extrude-UVs), `renderMode` „textured" + (TopBar-Darstellung), Material-Picker (ambientCG-Grid) + Upload + Kachelgröße im + `ResourceManager` (Bauteile-Tab). Backstein mit Normal-Map-Tiefe verifiziert. + `scripts/probe-materials.mjs`. Schnittschraffur-Slot frei für Welle B. +- **Editier-Eingabe + Topbar-Politur (FERTIG):** Befehlszeile zeigt **Live-Länge/-Winkel** + beim Zeichnen (`Command.fieldValues`), **Cursor-HUD entfernt**; **Seiten-Verschieben + propagiert verbundene Enden** (2D+3D); **Tab beim Vertex-Editieren** setzt Wert + (`gripEdit`-Controller in App — noch ad-hoc, NICHT durch die Engine). Topbar: Rest + iconisiert (Referenzlinien/Linien-Modus/Ressourcen), Linien-Modus → Dropdown, Aktions- + Buttons als Dropdown-Pill-Familie, **Detailgrad+Massstab gestapelt + 1:N/Zoom% kompakt** + (`.tb-zoomstack`). +- **Wand-als-Command (FERTIG & verifiziert):** `commands/cmds/wall.ts` (chainend, Achs+Band- + Draft, Tab-Felder; Wandfelder aus `CommandContext.activeWallTypeId`/`level`) + Registry + (Aliase `w`/`wand`) + App-Kopplung (`TOOL_COMMAND.wall`, Legacy-Wand umgeleitet). Wände in + 2D+3D. `scripts/probe-wall-cmd.mjs`. + +**OFFENE WELLEN (nächste Schritte):** Referenzobjekt-Achs-Tracking + Editieren VOLL durch die +Engine (heute `gripEdit` ad-hoc); **Welle B** (3D-Clip/Schnittebene + Stencil-Capping + +Schnittschraffur — nutzt das Material-System aus Welle A); **Welle C** (3D→2D-Schnitt/ +Perspektive via HLR, opencascade.js-Spike); **Swisstopo-Importer** (Ort+Radius → Gebäude+ +Gelände, geo.admin STAC/Geocode, CORS prüfen); Bögen/Kreise-Trim; Wandstärke-Tab bei +einschichtigen Wänden. **Offene Wellen (Todo-Spiegel — Todo-Liste lebt im Kontext):** 1. Editier-Welle 3: **koinzidente Endpunkt-Griffe gemeinsam ziehen** + verbundene Enden diff --git a/docs/HAUPTINSTANZ-BRIEFING.md b/docs/HAUPTINSTANZ-BRIEFING.md new file mode 100644 index 0000000..df9a1a3 --- /dev/null +++ b/docs/HAUPTINSTANZ-BRIEFING.md @@ -0,0 +1,175 @@ +# HAUPTINSTANZ-BRIEFING: Tauri + wgpu (Korrigiert) + +**Stand:** 2026-07-01 — **KORREKTUR** (vorherige Dokumente waren unvollständig) + +--- + +## Die Entscheidung (FINAL) + +**Browser-CAD (alt) → Desktop Tauri-App mit Rust-Backend + wgpu-Rendering (neu)** + +| Aspekt | Alt | Neu | +|---|---|---| +| **App-Form** | Browser-Tab | Desktop Tauri-Window | +| **Frontend** | React/Vite (TS) | React/Vite (TS) — UNVERÄNDERT | +| **2D-Rendering** | SVG-Plan | SVG-Plan — UNVERÄNDERT | +| **3D-Rendering** | three.js/WebGL | **wgpu** (Vulkan/Metal/DX12) | +| **Rechenintensive Ops** | TS in Browser | **Rust im Backend** | +| **Betriebssystem** | cross-platform browser | Windows/macOS/Linux Desktop App | + +--- + +## Warum dieser Umstieg? + +**Problem mit three.js/WebGL:** +- komplexe Möbel = 100k+ Polygone +- mehrere parallele Ops (kernel2d, bool-Ops, DXF-Parser, SIA-Raumerkennung) +- WebGL hat harte Limits (GPU VRAM, draw calls, single-threaded) +- → Laggy, nicht skalierbar für Professional CAD + +**Lösung: Tauri + wgpu + Rust** +- **wgpu** = low-level GPU API (direkt zu Vulkan/Metal/DX12, nicht WebGL) +- **Rust** = rechenintensive Ops parallelisiert + native performance +- **Desktop** = native App, nicht Browser (bessere Kontrolle, bessere Perf) +- **React-Frontend bleibt** = UI/Sketching/Panels unverändert (wgpu nur für 3D-Display) + +--- + +## Der neue Stack (FINAL) + +``` +┌─────────────────────────────────────────────────────┐ +│ Desktop Tauri-Window │ +├─────────────────────────────────────────────────────┤ +│ │ +│ React/Vite (UI-Shell, State, Panels) │ +│ ├─ SVG 2D-Plan (Grundriss) │ +│ └─ wgpu 3D-Viewport (3D-Ansicht + Rendering) │ +│ │ +│ ↓ invoke (IPC) ↓ │ +│ │ +│ Rust-Backend (src-tauri/) │ +│ ├─ computeJoins() — Wand-Eckverbindungen │ +│ ├─ kernel2d() — Offset/Trim/Extend/… │ +│ ├─ parseShapeFromDwg() — Geometrie-Import │ +│ ├─ detectRooms() — SIA-Raumerkennung │ +│ └─ booleanOps() — Union/Differenz/Schnitt │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +**Nicht verändert:** +- App.tsx, state/, commands/, panels/, model/types.ts +- UI-Logik, Befehlssystem, Sketching-Tools + +**Neu/geändert:** +- `src-tauri/` — Rust-Crate für Backend +- `src/compute/index.ts` — Boundary (invoke + TS-Fallback) +- Rendering-Engine: **three.js → wgpu** +- Build: `npm run tauri:build` statt `npm run build` + +--- + +## Die Aufgabe (vier Agents) + +**Siehe: `docs/design/tauri-migration-plan.md`** + +Vier parallele Agents: + +1. **Agent: Tauri-Shell Setup** → `src-tauri/` + Cargo.toml + main.rs (Tauri window registrieren) +2. **Agent: Compute-Boundary (TS)** → `src/compute/index.ts` (invoke-Wrapper + TS-Fallbacks) +3. **Agent: Rust compute_joins Impl** → `src-tauri/src/geometry.rs` (erste Op, Proof-of-Concept) +4. **Agent: Vite/Package-Integration** → vite.config.ts + package.json (Tauri-Plugins, scripts) + +**Deliverable:** lauffähige Desktop-App, wand-Edit triggert Rust-Op, Output identisch TS-Version. + +--- + +## Dev-Workflow (post-Tauri) + +```bash +# Dev: Terminal 1 (Vite) +npm run dev # localhost:5173 + +# Dev: Terminal 2 (Tauri) +npm run tauri:dev # öffnet Desktop-Window, zeigt auf localhost:5173 + +# Build +npm run tauri:build # → Windows .exe / macOS .app / Linux .deb +``` + +--- + +## Rendering: three.js → wgpu (Wichtig!) + +**3D-Viewport wird NEU implementiert in wgpu:** +- Nicht: "drei.js mit Rust-Fallback" +- **Ja:** wgpu als native GPU-Renderer (skaliert auf 100k+ Polygone) + +**2D-Plan bleibt SVG** (keine Änderung). + +**Timeline für wgpu-Impl:** +- Milestone 1 (jetzt): Tauri-Shell + Rust-Compute-Ops +- Milestone 2 (nächst): wgpu 3D-Viewport-Impl (ersetzt drei.js/WebGL) +- Milestone 3: Live clipping/sectioning in wgpu + +--- + +## Migration-Reihenfolge + +**Phase 1 (Tauri-Shell + Proof-of-Concept):** +- [ ] `computeJoins` (Wand-Ecken) → Rust + +**Phase 2 (Rendering-Umbau):** +- [ ] wgpu Viewport-Impl (ersetzt drei.js) +- [ ] Instancing/LOD für Möbel-Geometrie + +**Phase 3 (weitere Ops):** +- [ ] `kernel2d` (Offset/Trim) → Rust +- [ ] DXF/DWG-Parser → Rust +- [ ] detectRooms → Rust +- [ ] booleanOps → Rust + +--- + +## Was sich NICHT ändert + +- `src/App.tsx`, `src/state/`, `src/commands/` +- UI-Panels, Zeichenwerkzeuge, Befehlssystem +- Semantisches Modell (types.ts) +- i18n, Styling + +## Was ändert sich + +- **Engine:** three.js/WebGL → wgpu +- **Backend:** TS-Only → Tauri + Rust +- **Distribution:** Browser → Desktop App +- **Build-Prozess:** `npm run build` → `npm run tauri:build` + +--- + +## Fehlerquellen (zur Klarheit) + +❌ **FALSCH:** "Wir bleiben bei three.js" +✅ **RICHTIG:** "three.js → wgpu, Rust-Compute + Desktop Tauri" + +❌ **FALSCH:** "Tauri + three.js als Frontend-Engine" +✅ **RICHTIG:** "Tauri-Shell + React UI + wgpu 3D-Rendering + Rust-Compute" + +--- + +## Nächste Schritte (für Hauptinstanz) + +1. **Lesen:** `docs/design/tauri-migration-plan.md` (technisch, konkret) +2. **Spawn:** vier Agents (parallel, unabhängig) +3. **Verifizieren:** `npm run tauri:dev` → App läuft, erste Op in Rust funktioniert +4. **Aktualisieren:** HANDOVER.md mit Milestone-1-Status + +--- + +## Fragen? + +- **"Warum Desktop statt Browser?"** → Skalierbarkeit (native GPU + Rust-Compute) +- **"Warum wgpu statt drei.js?"** → wgpu skaliert auf 100k+ Polygone, three.js/WebGL hat Limits +- **"Bleibt React?"** → Ja, React/Vite UI bleibt, nur 3D-Engine wechselt +- **"Wann ist wgpu-Impl fertig?"** → Milestone 2 (nach Tauri-Shell) diff --git a/docs/design/tauri-architecture.md b/docs/design/tauri-architecture.md new file mode 100644 index 0000000..d76d862 --- /dev/null +++ b/docs/design/tauri-architecture.md @@ -0,0 +1,306 @@ +# Architektur-Pivot: Tauri + Rust-Backend (2026-07-01) + +## Entscheidung + +**Alte Welt:** Browser-CAD (React/Vite + WebGL/three.js) +**Neue Welt:** Desktop Tauri-App (React/Vite Frontend + Rust-Backend + **wgpu 3D-Rendering**) + +**Grund:** Komplexe Möbel mit vielen Polygonen (100k+) + mehrere parallele rechenintensive Ops → WebGL/three.js wird zum Bottleneck. **wgpu** (low-level GPU-API auf Vulkan/Metal/DX12) + Rust-Compute skaliert native. + +**Scope:** Desktop-only Release (Milestone 1). WASM-Fallback für Browser später wenn gebraucht. + +--- + +## Post-Migration Stack + +### Frontend (React/Vite — Komponenten + State, unverändert) + +``` +src/ + App.tsx ← Shell-Komponente + compute/index.ts ← Compute-Boundary (neu) + model/types.ts ← Semantisches Modell + commands/ ← Befehlssystem + panels/ ← UI-Panels + plan/PlanView.tsx ← 2D-SVG-Rendering + viewport/Viewport3D.tsx ← three.js 3D-Display + ui/ ← Topbar, Dialogs, etc. + state/ ← Redux-Slices (project, selection, view, layout) + ... +``` + +**Rolle:** User-Input-Handling, 2D/3D-Darstellung (Display-Layer), State-Management. + +### Backend (Rust/Tauri — neu) + +``` +src-tauri/ + src/ + main.rs ← Tauri window + invoke-handler registration + geometry.rs ← compute_joins(), kernel2d(), etc. + parsers/ + dwg.rs ← DXF/DWG-Geometrie-Parsing + dxf.rs + sia/ + room_detection.rs ← detectRooms() (SIA-416) + ... + Cargo.toml ← Dependencies (serde, tauri, …) +``` + +**Rolle:** Rechenintensive Ops, Geometrie-Kernel, Parsing, SIA-Raumerkennung. + +### IPC: Tauri invoke (async, serde) + +```typescript +// Frontend ruft Rust auf +const result = await invoke('compute_joins', { walls, joints }); + +// Rust bearbeitet + serialisiert Ergebnis +#[tauri::command] +async fn compute_joins(input: JoinInput) -> Result { + geometry::compute_joins(input).map_err(|e| e.to_string()) +} +``` + +--- + +## Compute-Boundary (Key Design) + +**Neue Datei:** `src/compute/index.ts` — einziger Eingang für rechenintensive Ops. + +```typescript +// Beispiel-Schnittstellen +export async function computeJoins(walls: Wall[], joints: Array<[number, number]>): Promise { … } +export async function computeKernel2D(op: 'offset'|'trim', geom: Polyline, …): Promise { … } +export async function detectRooms(…): Promise { … } +export async function parseShapeFromDwg(…): Promise { … } +``` + +**Hinter der Boundary:** +1. Versuche Tauri invoke zu Rust (`#[tauri::command]`) +2. Fallback auf lokale TS-Impl wenn Rust nicht verfügbar (während Migration) + +**Effekt:** Aufrufort im Code bleibt stabil; Caller sieht nicht, ob Op schon in Rust ist oder noch TS. + +**Migrationsfluss:** +``` +1. TS-Impl existiert (z.B. src/model/joins.ts) +2. Neue Op in Compute-Boundary mit Invoke+Fallback +3. Parallel: Rust-Impl in src-tauri/src/geometry.rs +4. Tests: Rust-Output == TS-Output (Parität) +5. TS-Impl bleibt (Fallback, wird nicht entfernt bis Rust stable) +6. Eventuell: TS-Impl löschen wenn Rust bewährt +``` + +--- + +## Dev-Workflow (post-Tauri) + +### Development + +```bash +# Terminal 1: Vite dev-server +npm run dev # localhost:5173 + +# Terminal 2: Tauri dev +npm run tauri:dev # öffnet Tauri-Fenster, zeigt auf localhost:5173 + # Rust hot-reload + TS hot-reload gleichzeitig +``` + +**Voraussetzungen:** +- Node.js + npm (wie heute) +- Rust + Cargo (neu) +- Tauri CLI: `npm install -D @tauri-apps/cli` + +### Build + +```bash +# Single command +npm run tauri:build + +# Erzeugt: +# - Windows: src-tauri/target/release/cad.exe +# - macOS: src-tauri/target/release/bundle/macos/cad.app +# - Linux: src-tauri/target/release/bundle/deb/cad_*.deb (oder Flatpak) +``` + +### Testing + +```bash +# Rust-Unit-Tests +cargo test # in src-tauri/ + +# TS-Tests (unverändert) +npm run test + +# Integration-Test: App starten + Aktion prüfen +npm run tauri:dev # manuell testen oder Puppeteer-Probe erweitern +``` + +--- + +## GPU-Strategy (für später) + +**Milestone 1 (jetzt):** CPU-Ops in Rust (kernel2d, joins, parsing, SIA). + +**Milestone 2 (später):** GPU-Compute via wgpu +- Tauri + wgpu Renderer (optional, nicht erforderlich) +- ODER drei.js bleibt, Rust handelt CPU-Ops, three.js handelt Display +- GPU-Heavy-Ops (z.B. große Boolean-Operationen) können in wgpu laufen, aber MVP braucht das nicht + +**Aktueller Plan:** three.js bleibt für 3D-Display (skaliert ausreichend für Möbel-Geometrie mit Instancing + LOD). + +--- + +## Migration Strategy: Ops nach Priorisierung + +**Phase 1 (aktuell — Tauri-Shell + Proof-of-Concept):** +- [ ] `computeJoins` (Wand-Eckverbindungen) → Rust + - Gründe: klein, häufig, zeigt invoke-Flow + +**Phase 2 (nächst):** +- [ ] `kernel2d` (Offset/Trim/Extend/Fillet) → Rust + - Gründe: Rechenlast ⭐⭐, Frequenz hoch +- [ ] DXF/DWG-Parser → Rust (Geometrie-Extraktion) + - Gründe: Rechenlast ⭐⭐, Frequenz mittel (Import-Dialog) + +**Phase 3 (später):** +- [ ] `detectRooms` (SIA-416 Raumerkennung) → Rust + - Gründe: Rechenlast ⭐, async-freundlich +- [ ] `booleanOps` (Union/Differenz/Schnitt) → Rust + - Gründe: Rechenlast ⭐⭐, Frequenz gering (ad-hoc) + +--- + +## Folgen für bestehenden Code + +### Was ändert sich NICHT + +- `src/model/types.ts` — semantisches Modell bleibt in TS (Frontend kennt es) +- `src/state/` — Redux-Store unverändert +- `src/ui/` — Komponenten unverändert +- `src/plan/PlanView.tsx` — SVG-Rendering unverändert +- `src/viewport/Viewport3D.tsx` — three.js-Rendering unverändert +- `src/commands/` — Befehlssystem unverändert + +### Was ändert sich + +- **Neue `src/compute/index.ts`** — alle rechenintensiven Ops laufen durch hier +- **Neue `src-tauri/`** — Rust-Backend +- **Vite-Config:** Tauri plugin hinzufügen +- **Package.json:** tauri scripts hinzufügen +- **Build-Prozess:** `npm run tauri:build` statt `npm run build` + +### Was wird migriert (schrittweise) + +- `src/model/joins.ts` → `src-tauri/src/geometry.rs` (Phase 1) +- `src/geometry/kernel2d.ts` → `src-tauri/src/geometry.rs` (Phase 2) +- `src/io/{dxfParser, dwgParser}.ts` → `src-tauri/src/parsers/` (Phase 2) +- `src/geometry/{roomArea, roomBoundary}.ts` → `src-tauri/src/sia/room_detection.rs` (Phase 3) +- `src/editors/booleanOps.ts` → `src-tauri/src/geometry.rs` (Phase 3) + +**Wichtig:** TS-Versionen bleiben als Fallback (nicht gelöscht). + +--- + +## Distribution (später) + +### Desktop Binaries (post-Tauri) + +- **Windows:** `.exe` (standalone executable) +- **macOS:** `.app` bundle (code-signed) +- **Linux:** `.deb` package ODER **Flatpak** (preferred) + - Flatpak = moderne WebKitGTK6 immer dabei, unabhängig von Distro-Alter + +### Browser (wenn gebraucht) + +- **WASM-Fallback** für `src/compute/` Ops (Rust → WASM via wasm-bindgen) +- Later-phase feature, nicht Milestone 1 + +--- + +## Technische Details + +### Serialisierung (TS ↔ Rust) + +**serde + serde_json** für Geometrie-Typen: + +```rust +// Rust +#[derive(Serialize, Deserialize)] +pub struct Vec2 { pub x: f64, pub y: f64 } + +#[derive(Serialize, Deserialize)] +pub struct Wall { + pub id: String, + pub start: Vec2, + pub end: Vec2, + // … +} +``` + +```typescript +// TS (type-safe invoke) +interface Vec2 { x: number; y: number } +interface Wall { id: string; start: Vec2; end: Vec2; /* … */ } + +await invoke('compute_joins', { walls: Wall[] }) +``` + +### Tauri Security (default) + +- Invoke-Handler sind Rust-side validiert +- Whitelist-Makro (`#[tauri::command]`) registered nur explizit erlaubte Functions +- CORS/CSP Policy default secure +- Keine arbitrary-Script-Execution (native app) + +--- + +## Abhängigkeiten (neu post-Tauri) + +### Frontend (npm) +- Bestehende: react, vite, three.js, redux, etc. +- Neu: `@tauri-apps/api` (JS-SDK für invoke) +- Optional später: `@tauri-apps/cli` dev-dependency (bereits in package.json) + +### Backend (Cargo) +```toml +[dependencies] +tauri = { version = "2", features = ["webkit2gtk-6.0"] } # GTK4 +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +# später: wgpu, delaunator, opencascade-sys, etc. +``` + +### System +- Rust 1.70+ +- GTK4 dev libraries (Linux only, auto-handled by Tauri) +- Xcode Command Line Tools (macOS, auto-checked) + +--- + +## Next Steps (Koordination) + +**Aufgabe für nächste Phase:** +→ Siehe `docs/design/tauri-migration-plan.md` (Schritt-für-Schritt, vier parallele Agents) + +**HANDOVER.md:** wird aktualisiert nach Tauri-Shell stabil. + +--- + +## FAQ + +**Q: Läuft die App noch im Browser?** +A: Nein (Milestone 1). Desktop-only. WASM-Fallback für Browser später wenn gebraucht. + +**Q: Was passiert mit dem existing TS-Code?** +A: Bleibt unverändert (außer neue Compute-Boundary). TS-Implementierungen = Fallback bis Rust stabil. + +**Q: Muss ich Rust können um das Projekt zu verstehen?** +A: Nein. Frontend bleibt React/TS. Rust ist "blackbox" hinter invoke. Aber bei Rust-Bugs muss man rein. + +**Q: Wann ist Tauri-Shell fertig?** +A: Nach den vier Agents (Schritt 1–4 in tauri-migration-plan.md), ~1–2 Wochen. + +**Q: Kann ich lokal testen?** +A: Ja, `npm run tauri:dev` öffnet lokale App. Alles wie heute, nur Rust hinten dran. diff --git a/docs/design/tauri-migration-plan.md b/docs/design/tauri-migration-plan.md new file mode 100644 index 0000000..8a42288 --- /dev/null +++ b/docs/design/tauri-migration-plan.md @@ -0,0 +1,243 @@ +# Tauri-Migration + Compute-Boundary — Aufgabe für nächste Instanz + +**Entscheidung (fix, 2026-07-01):** Browser-CAD → **Desktop Tauri-App mit Rust-Backend + wgpu-Rendering.** + +**Grund:** komplexe Möbel mit vielen Polygonen (100k+) + mehrere parallele rechenintensive Ops → WebGL/three.js wird zum Blocker. **wgpu** (low-level GPU-API) + Rust-Compute skaliert native. + +**Scope:** Desktop-only Release (Milestone 1). WASM-Fallback für Browser später wenn gebraucht. + +**Rendering-Engine:** three.js → **wgpu** (Milestone 2) + +--- + +## Aufgabe: Shell aufsetzen + Compute-Boundary + erste Op migrieren + +### Schritt 0 — Compute-Boundary (TS-Kontrakt) + +**Neu:** `src/compute/index.ts` — die einzige Stelle, durch die alle rechenintensiven Ops laufen. + +```typescript +// src/compute/index.ts — einheitliche Schnittstelle +export async function computeKernel2D(op: 'offset'|'trim', …): Promise { … } +export async function detectRooms(…): Promise { … } +export async function computeJoins(…): Promise { … } // ← erste Op +export async function parseShapeFromDwg(…): Promise { … } +``` + +**Hinter der Boundary:** +- Erst Tauri-invoke zu Rust `#[tauri::command]` +- Fallback auf lokale TS-Impl (bleibt unberührt, bis Rust stabil) +- `catch(err) → console.warn('Rust failed, using TS fallback'); return tsImpl(…)` + +**Effekt:** Aufrufort im Code bleibt stabil; Caller sieht nicht, ob Op schon in Rust ist oder noch TS. + +--- + +### Schritt 1 — Tauri-Shell aufsetzen + +**Struktur:** +``` +repo/ + src-tauri/ ← neue Rust-Seite (Tauri-Konvention) + src/ + main.rs ← Tauri window + invoke handlers + geometry.rs ← compute_joins() + weitere Ops später + ... + Cargo.toml + src/ ← React/TS (unverändert) + compute/ + index.ts ← Compute-Boundary + ... + vite.config.ts ← Tauri plugin integrieren + package.json ← tauri scripts +``` + +**Setup:** +1. `cargo init --name cad-tauri src-tauri` (oder `src-tauri` manuell anlegen) +2. `Cargo.toml`: Tauri v2 einbinden mit Feature `webkit2gtk-6.0` (GTK4) + ```toml + [dependencies] + tauri = { version = "2", features = ["webkit2gtk-6.0"] } + serde = { version = "1.0", features = ["derive"] } + serde_json = "1.0" + ``` +3. `src-tauri/src/main.rs`: Minimal-Fenster, invoke-Handler registrieren + ```rust + #[tauri::command] + async fn compute_joins(input: JoinInput) -> Result { + // Rust-Impl + geometry::compute_joins(input).map_err(|e| e.to_string()) + } + + #[cfg_attr(mobile, tauri::mobile_entry_point)] + pub fn run() { + tauri::Builder::default() + .invoke_handler(tauri::generate_handler![compute_joins]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); + } + ``` +4. `vite.config.ts`: Tauri plugin + dev-server-Integration + ```typescript + import { defineConfig } from 'vite' + import react from '@vitejs/plugin-react' + export default defineConfig({ + plugins: [react()], + server: { port: 5173 } // Tauri dev zeigt hier drauf + }) + ``` +5. `package.json`: Tauri scripts hinzufügen + ```json + "scripts": { + "tauri": "tauri", + "tauri:dev": "tauri dev", + "tauri:build": "tauri build" + } + ``` + +--- + +### Schritt 2 — Erste Op migrieren: `computeJoins` (Wand-Eckverbindungen) + +**Warum `computeJoins` zuerst?** +- Läuft häufig (bei jedem Wall-Edit) +- Klein und fokussiert (~50 Zeilen Kernlogik) +- Proof-of-Concept für Tauri-invoke-Flow +- Danach `kernel2d` parallel hochfahren + +**Rust-Impl:** `src-tauri/src/geometry.rs` + +```rust +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct Vec2 { pub x: f64, pub y: f64 } + +#[derive(Serialize, Deserialize)] +pub struct WallJoinInput { + pub walls: Vec, + pub joints: Vec<(usize, usize)>, // wall indices +} + +#[derive(Serialize, Deserialize)] +pub struct JoinInfo { /* … */ } + +pub fn compute_joins(input: WallJoinInput) -> Result, Box> { + // Port der Logik aus src/model/joins.ts + // L-Ecken, T-Stösse, +-Kreuzungen + Ok(vec![]) // Placeholder +} +``` + +**TS-Fallback bleibt:** `src/model/joins.ts` (LS vor Rust-Port) + +**Compute-Boundary:** `src/compute/index.ts` +```typescript +export async function computeJoins(walls: Wall[], joints: Array<[number, number]>): Promise { + try { + return await invoke('compute_joins', { walls, joints }); + } catch (err) { + console.warn('Rust compute_joins failed, using TS fallback:', err); + return joinsTS(walls, joints); // Fallback + } +} +``` + +--- + +### Schritt 3 — Migrations-Parität testen + +**Test-Fixtures:** +- Aus `src/model/sampleProject.ts` exportieren: Wand-Arrays mit bekannten L/T/±-Konfigurationen +- Rust-Unit-Tests: gleiche Fixtures → gleiche JoinInfo-Outputs +- Vergleich: `actual == expected` + +**Beispiel (Rust):** +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_l_corner() { + let input = WallJoinInput { /* L-shaped walls */ }; + let result = compute_joins(input).unwrap(); + assert_eq!(result[0].kind, JoinKind::LCorner); + } +} +``` + +--- + +### Schritt 4 — Vite/Tauri-Integration + +**Dev-Workflow:** +```bash +npm run tauri:dev +# → Vite dev-server (localhost:5173) lädt React-App +# → Tauri-window zeigt auf :5173 +# → invoke() ruft Rust-Commands auf +``` + +**Build-Workflow:** +```bash +npm run build # Vite → dist/ +npm run tauri:build # Tauri packt dist/ + Rust-Binary +``` + +--- + +## Deliverable (Proof-of-Concept) + +**Git-Stand nach dieser Aufgabe:** + +- [ ] `src-tauri/` Verzeichnis mit `Cargo.toml` + `src/main.rs` + `src/geometry.rs` +- [ ] `Cargo.toml` buildet sauber (`cargo check` 0 Fehler) +- [ ] `src/compute/index.ts` mit `computeJoins()` Schnittstelle (invoke + TS-fallback) +- [ ] Rust `compute_joins()` implementiert, Tests pass (`cargo test`) +- [ ] `package.json` `tauri` scripts hinzugefügt +- [ ] **App läuft:** `npm run tauri:dev` → Tauri-Fenster öffnet, Wand-Edit triggert Rust-Op, Output identisch TS-Version +- [ ] **Trace-Scan sauber** (kein TS/Rust-Code übrig, beide Impl. aktiv) +- [ ] HANDOVER.md aktualisiert: `computeJoins` migriert, nächste Ops in Queue + +**Verification:** +```bash +# Build-Check +cargo check # 0 Fehler + +# Rust-Tests +cargo test + +# App end-to-end +npm run tauri:dev +# → Wand zeichnen + editieren → computeJoins() über Rust aufgerufen +# → Plan + 3D aktualisiert wie vorher +``` + +--- + +## Nächste Ops (Priorisierung) + +Nach `computeJoins` stabil: + +1. **`kernel2d`** (Offset/Trim/Extend) — großer Hebel, läuft häufig +2. **DXF/DWG-Parser** (Geometrie) — heavy, aber niedrige Frequenz +3. **`detectRooms`** (SIA-Raumerkennung) — async, kann auf Hintergrund ziehen +4. **`booleanOps`** (Union/Differenz/Schnitt) — Kandidat für später + +--- + +## Offene Punkte (NICHT jetzt) + +- **GPU-Compute (wgpu):** Erst nach CPU-Ops stabil (kernel2d, joins, parsing) +- **WASM-Fallback:** Nur wenn Browser-Support nötig wird +- **Flatpak-Distribution:** Nach Tauri-Shell stable + erste Ops migriert + +--- + +## Referenzen + +- Tauri v2 Docs: https://tauri.app/v1/guides/getting-started/prerequisites +- Serde: https://serde.rs/ +- CONVENTIONS.md: Identifiers englisch, UI-Text via `t()`, … +- Commit-Regel: kein AI-Attribution im Repo diff --git a/docs/design/topbar-dossier.md b/docs/design/topbar-dossier.md new file mode 100644 index 0000000..18f0b2c --- /dev/null +++ b/docs/design/topbar-dossier.md @@ -0,0 +1,151 @@ +# Oberleiste – Angleichung an DOSSIER (Umsetzungs-Spezifikation) + +Verbindliche Vorlage für den Umbau der Top-Bar (`src/ui/TopBar.tsx`, +`src/styles.css`, Verdrahtung in `src/App.tsx`). Quelle: das DOSSIER-Rhino- +Plugin (`ToolbarApp.jsx`, `components/BarControls.jsx`, `TextEditorApp.jsx`). +Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md). + +## 0. Grundprimitive (neu, DOSSIER-konform) + +Alle Leisten-Controls teilen dieselbe Höhe und Pillenform. + +- `BAR_H = 22px` Basis-Höhe. Segmentierte Pillen `BAR_H + 2 = 24px` + (`box-sizing:border-box`, 1px Rand inbegriffen). +- Pille: `border:1px solid var(--border)`, `border-radius:999px`, + `background:var(--input)`. Hover (interaktiv): `border-color:var(--accent-border)`, + `background:var(--accent-dim)`. +- Aktiver Zustand (Toggle AN / aktive Segmentzelle): `background:var(--accent)`, + `color:#fff`. + +### BarCombo (Pillen-Dropdown) +Wir haben bereits `src/ui/Dropdown.tsx`. Der Dropdown-Trigger MUSS optisch der +Pille entsprechen (Höhe 24, radius 999, obige Farben). Ein optionales Icon sitzt +LINKS **ausserhalb** der Pille (18px breit, `var(--muted)`), ein optionaler +Zahnrad-Knopf („settings") sitzt rechts **innerhalb** der Pille. Prüfen, ob +`Dropdown` bereits so aussieht; falls nicht → Trigger-CSS angleichen (Klasse +`tb-dd-trigger`). KEINE zweite Dropdown-Implementierung bauen. + +### Segmentpille (3er/4er) +Aussencontainer `display:inline-flex; height:24px; border:1px solid var(--border); +border-radius:999px; overflow:hidden`. Zellen ohne eigenen Radius; interne Trenner +über `border-left:1px solid var(--border)` (erste Zelle ohne). Aktive Zelle +`var(--accent)`/#fff, inaktiv `var(--input)`/`var(--ink)`, Hover +`var(--accent-dim)`/`var(--accent)`. Genutzt für: Ansichts-Icons, Zoom (%/fit/center), +**B/I/U**, **L/C/R**. + +### BarButton (quadratischer Icon-Knopf) +22×22, `border-radius:999px`, sonst wie Pille. Aktiv = Akzentfüllung, Icon #fff. + +## 1. Reihenfolge der Gruppen (links → rechts) + +1. Marke (bestehend, unverändert). +2. Ansichts-Gruppe (bestehend `view-grid`; Zellen auf Segmentpillen-Look bringen). +3. Sichtbarkeits-Kombinationen (bestehend, `BarCombo`-Look). +4. Detailgrad + Massstab (gestapelt, `BarCombo`-Look). +5. **Massstab/Zoom-Cluster NEU** (siehe §2) — ersetzt die heutige Gruppe mit der + DOPPELTEN Zoom-Anzeige. +6. Darstellungsart (bestehend, `BarCombo`). +7. **Text-Gruppe NEU** (siehe §3) — die zentrale neue Leiste. +8. Referenzlinien / Linien-Modus (bestehend). +9. Rechts: Layout · Ressourcen · Projektname. + +## 2. Massstab/Zoom-Cluster (ersetzt Doppel-Zoom-Bug) + +HEUTE FALSCH: In `TopBar.tsx` wird `tb-zoom` (Zoom %) ZWEIMAL gerendert +(einmal im `tb-zoomstack`, einmal darunter als eigener ``). Die zweite, +lose `…%` ersatzlos ENTFERNEN. + +NEUES Layout — 2×2-Raster (`display:grid; grid-template-columns:auto auto; +gap:4px 6px; align-items:center`): + +- **Spalte 1, beide Zeilen** (`grid-row:1 / span 2`): EINE kombinierte Stat-Pille, + `width:70px`, Höhe `BAR_H*2+6 = 50px`, `border-radius:14px` (NICHT 999), + `border:1px solid var(--border)`, `background:var(--input)`, Innen zwei Zeilen + mittig, getrennt durch 1px-Linie (`var(--border)`): + - oben: Live-Massstab `1:N` (Akzentfarbe, `var(--font-mono)`, 11px, 700) + - unten: Zoom `NN%` (`var(--ink-2)`, mono, 11px) + - „am Massstab" (Zoom==gewählter Massstab): Pille `background:var(--accent-dim)`, + `border-color:var(--accent)`, Text `var(--accent)`. + - Nicht-Plan-Ansicht: beide Werte „—". +- **Spalte 2, Zeile 1**: Massstab-Dropdown (`BarCombo`, ~140px, mono) + Print/PDF + bleibt separat. (Massstab-Dropdown ist der bestehende `scaleOptions`-Dropdown.) +- **Spalte 2, Zeile 2**: Zoom-Segmentpille mit 3 Zellen — `%` (=`onZoom100`, + Label „1:1"/100 %), `fit_screen` (=`onFit`), `center_focus_strong` + (=`onFitSelection`). Material-Icons. Daneben ggf. Referenzlinien-BarButton. + +Export-Knöpfe (PDF/DXF) wandern in eine eigene kleine BarButton-Reihe rechts vom +Cluster (Icons `picture_as_pdf` / `download`) ODER bleiben Pillen — Hauptsache +NICHT mehr Teil des Zoom-Blocks, damit der Cluster ruhig bleibt. + +## 3. Text-Gruppe in der Oberleiste (NEU – Kern dieser Aufgabe) + +Immer sichtbar. 3×2-Raster (`grid-template-columns:110px 130px 80px; gap:4px 6px`). +Setzt Defaults für neuen Text UND formatiert die aktuelle Auswahl live. + +Zeile 1: +- **Stil-Preset** `BarCombo` (110px): Optionen aus `DEFAULT_PRESETS` + (Titel/Untertitel/Label/Notiz) + „— Stil —". +- **Font** `BarCombo` (130px): Systemfont-Liste (mind. Helvetica, Arial, Inter, + Times New Roman, Georgia, Courier New). `applyMark(doc,range,'font',v)`. +- **Grösse** `BarCombo` (80px): Presets in pt `[8,9,10,11,12,14,18,24,36,48]` + + „Eigene…" → Zahl-Input-Pille. `applyMark(...,'sizePt',n)`. + +Zeile 2: +- **B/I/U** Segmentpille (110px, Icons `format_bold`/`format_italic`/ + `format_underlined`): `toggleMark(doc,range,'bold'|'italic'|'underline')`. + Aktiv-Zustand aus `isMarkActive(doc,range,mark)`. +- **L/C/R** Segmentpille (130px, Icons `format_align_left`/`_center`/`_right`): + setzt `paragraph.align` im Bereich. +- **„+"-Text-Button** (80px, BarButton/Pille, Icon `add`, Label „Text"): startet + das Text-Werkzeug (neues Textobjekt platzieren). Falls das Text-Annotation- + Werkzeug noch nicht existiert, Button vorerst `disabled` mit Tooltip + (kein stiller No-Op) — aber Verdrahtung vorbereiten. + +**Auswahl-Bewusstsein (WICHTIG):** Prop `textTarget` (oder aus App-State): entweder +`null` (nichts Text-artiges selektiert → Controls setzen nur Defaults, Ränder +normal) ODER `{ doc: RichTextDoc, range: TextRange|null, apply: (doc)=>void }` +für den aktuell selektierten Raumstempel/Text. Ist `textTarget != null`, tragen +die Zeile-2-Pillen `border-color:var(--accent)` (Akzent-Glow), und alle Aktionen +wirken auf `textTarget.doc` via `textTarget.apply(newDoc)`. Ohne aktive Range +(nur Objekt selektiert, kein Editor offen) wirkt Formatierung auf das GANZE Doc. + +Quelle der `textTarget`-Daten: der Raum-Agent exponiert Stempel-Doc + Setter +(`setRoomStampDoc(roomId, doc)`) im App-State (siehe Peer-Absprache). App leitet +für den selektierten Raum `{doc: room.stampDoc, range: activeStampRange, +apply: d => setRoomStampDoc(room.id, d)}` an die Text-Gruppe. + +## 4. Text-Inhalt bearbeiten: kleines Fenster (kein Footer) + +Doppelklick auf einen Raumstempel/ein Textobjekt öffnet ein **schwebendes +Dialog-Fenster** (nicht den Footer, kein Panel-Aufklappen). Umsetzung: neue +Komponente `src/ui/TextEditorDialog.tsx` — ein zentriertes/абgesetztes Fenster +(~560×420, `--shadow-3`, `border-radius:8px`, Titel „Text bearbeiten", +Kopf mit Schliessen-✕), Body = der bestehende `src/text/RichTextEditor.tsx` +(er bringt seine eigene Mini-Toolbar mit — das ist hier ok, weil es ein eigenes +Fenster ist), Fuss = „Abbrechen" / „Übernehmen". „Übernehmen" ruft +`setRoomStampDoc(roomId, editedDoc)`. + +Der Doppelklick-Handler lebt in App (Plan-View/Viewport → onDoubleClick auf +Stempel-Hit → `openTextEditor(roomId)`), NICHT im Footer. Falls der Raum-Agent +den Footer benutzt hat: diesen Pfad entfernen und durch den Dialog ersetzen. + +## 5. Farb-/Stil-Tokens + +Bestehende CSS-Variablen weiterverwenden (`--panel`,`--input`,`--border`, +`--accent`,`--accent-dim`,`--accent-border`,`--ink`,`--ink-2`,`--muted`, +`--font-mono`,`--shadow-1..3`). KEINE neuen Farbwerte hart kodieren. Falls ein +Token fehlt (z. B. `--accent-border`), prüfen und ggf. aus bestehenden ableiten. + +## 6. i18n + +Neue Keys in de.ts UND en.ts: `text.style`, `text.font`, `text.size`, +`text.size.custom`, `text.bold/italic/underline`, `text.align.left/center/right`, +`text.add`, `text.add.hint`, `text.editTitle`, `text.apply`, `text.cancel`, +`text.selectedHint`. Presets-Namen über bestehende `rt.*`/Preset-Keys, sofern da. + +## 7. Gate (Pflicht) + +`rm -f tsconfig.tsbuildinfo && npx tsc -b` grün, `npm run build` grün, keine +AI-Spuren (grep auf Claude/Anthropic/AI/Generated/Co-Authored), Boot-Probe +(`node scripts/probe.mjs`) ohne Konsolenfehler, Screenshot der Oberleiste. +KEIN Commit. diff --git a/docs/design/webgl2-plan-renderer.md b/docs/design/webgl2-plan-renderer.md new file mode 100644 index 0000000..9195cbe --- /dev/null +++ b/docs/design/webgl2-plan-renderer.md @@ -0,0 +1,581 @@ +# WebGL2 GPU-Accelerated 2D Plan Renderer — Architecture + +## Executive Summary + +A WebGL2 canvas renderer for PlanView's heavy geometry (polygons, lines, hatches) with CPU fallback. Geometry tessellates once per plan and caches; pan/zoom only updates a transform-matrix uniform. Screen-space stroke width via vertex shader normal expansion. Thin SVG overlay handles text, grips, snap markers, tool preview. + +**No npm dependencies** — raw WebGL2 + TypeScript. + +--- + +## Current State (SVG Bottleneck) + +**PlanView.tsx** renders `Primitive[]` (polygon/line/arc/text) → SVG DOM: +- ~2500 LOC: pan/zoom via viewBox, toScreen() scaling (1 meter = 90 viewBox units) +- `Primitive` types (generatePlan.ts:133): + - **polygon**: `pts: Vec2[]`, fill/stroke/strokeWidthMm, hatch (solid/insulation/diagonal/crosshatch) + - **line**: a/b endpoints, className, weightMm, dash[], optional color + - **arc**: center, from/to points, r, className, weightMm, dash[] + - **text**: anchor, RichTextDoc, roomStamp metadata +- Bottleneck: **pan/zoom re-renders entire SVG DOM** → Cairo rasterizes geometry at 144 Hz + +**Key constants:** +- `PX_PER_M = 90` (viewBox units per meter; Modell-Y up → SVG-Y down via negation) +- `PAD = 60` (margin in viewBox units) +- `mmToPx(mm) = (mm / 25.4) * dpi()` (stroke width: constant screen-px via non-scaling-stroke) +- `ZOOM_MAX/MIN = 50/0.2` (pan/zoom bounds) + +--- + +## Architecture: PlanRenderer (WebGL2 + Fallback SVG) + +### Module Structure + +``` +src/plan/ +├── PlanRenderer.ts (Main GPU/CPU dispatcher) +├── glPlan/ +│ ├── glPlanCompile.ts (Tessellation & buffer upload) +│ ├── glPlanShaders.ts (Vertex/fragment sources + compilation) +│ ├── glPlanRender.ts (Draw loop: matrix uniform, state mgmt) +│ └── glPlanTypes.ts (TypeScript interfaces for GPU data) +└── PlanView.tsx (React wrapper, unchanged API) +``` + +### High-Level Flow + +``` +PlanView.tsx + ↓ [receives plan: Plan] + ↓ +PlanRenderer (new abstraction) + ↓ + ├─→ GPU path [if WebGL2 available && flag=true] + │ ├─ glPlanCompile() → upload tessellated geometry to VRAM + │ ├─ glPlanRender() → draw with pan/zoom matrix uniform + │ └─ [fast pan/zoom via matrix only] + │ + └─→ Fallback: SVG [if WebGL fails || flag=false] + └─ existing PlanView render path (toScreen + DOM) +``` + +--- + +## GPU Path: Tessellation & Shaders + +### 1. Tessellation Strategy + +#### **Polygon** → Fans + Ear Clipping +- **Input**: Primitive.polygon = { pts: Vec2[], fill, stroke, strokeWidthMm, hatch } +- **Output**: Indexed triangle mesh +- **Algorithm**: Earcut2D (existing JS library logic, inlined to avoid npm) + - Convert polygon pts to 2D float32 array in **world space** (meters) + - Earcut → triangle indices + - Store: `{ vertices: Float32Array, indices: Uint32Array, color: vec4, hasHatch: bool }` + - **Hatch rendering**: Bake hatch as texture or re-implement in fragment shader (MVP: solid fill only; hatch deferred) + +#### **Line** → Quad Expansion (Screen-Space Width) +- **Input**: Primitive.line = { a, b, weightMm, cls, dash?, color } +- **Output**: Degenerate quad (2 triangles) with screen-space normal offset +- **Strategy**: + 1. Vertex shader receives `{ pos: vec2, side: float }` (side = ±1 for left/right edge) + 2. Transform pos to clip space via matrix uniform + 3. Compute screen-space perpendicular via `dFdx/dFdy` or pre-compute normal in CPU + 4. Expand by `(weightMm / 25.4) * dpi * (screenPixelsPerClipUnit)` in clip space + 5. Fragment shader: solid color (no dash MVP; dashing deferred or CPU pre-tessellation) + +#### **Arc** → Line Segments (Polyline → Quads) +- **Input**: Primitive.arc = { center, from, to, r, weightMm, cls, dash } +- **Output**: Tessellate arc to ~30 line segments (adaptive based on radius/zoom), expand each as quad +- Fallback: SVG arc for MVP + +#### **Text, Grips, Snap-Markers, Tool-Preview** +- **Stays in SVG overlay** (thin, non-bottleneck) +- Render above WebGL canvas at z-order 1 + +--- + +### 2. Shader Sources (GLSL 3.00 ES) + +#### **Vertex Shader: Solid Fill (polygon)** +```glsl +#version 300 es +precision highp float; + +uniform mat4 viewProjection; // pan/zoom as 2×3 affine (expand to mat4) + +layout(location=0) in vec2 position; // world-space (meters) +layout(location=1) in vec4 color; // fill color + +out VS_OUT { + flat vec4 vertexColor; +} vs_out; + +void main() { + vec4 clipPos = viewProjection * vec4(position, 0.0, 1.0); + gl_Position = clipPos; + vs_out.vertexColor = color; +} +``` + +#### **Vertex Shader: Screen-Space Stroked Line** +```glsl +#version 300 es +precision highp float; + +uniform mat4 viewProjection; // world → clip space +uniform vec2 screenSize; // canvas (width, height) in pixels +uniform float strokeWidthMm; // millimeters +uniform float dpi; // 96 * devicePixelRatio + +layout(location=0) in vec2 position; // world-space endpoint +layout(location=1) in float sideFlag; // ±1.0 (left/right edge) +layout(location=2) in vec4 lineColor; // stroke color + +out VS_OUT { + flat vec4 vertexColor; +} vs_out; + +void main() { + vec4 clipPos = viewProjection * vec4(position, 0.0, 1.0); + + // Convert stroke width (mm) → screen pixels + float strokePx = (strokeWidthMm / 25.4) * dpi; + // Convert screen pixels → normalized device coords (NDC) + // NDC ∈ [-1,1]²; screen (0,screenSize) → NDC [-1,1] + float strokeNdc = (strokePx / screenSize.x) * 2.0; + + // Expand in clip space (simple; assumes aspect ≈ 1) + vec4 expanded = clipPos + vec4(sideFlag * strokeNdc, 0.0, 0.0, 0.0); + + gl_Position = expanded; + vs_out.vertexColor = lineColor; +} +``` + +#### **Fragment Shader (both)** +```glsl +#version 300 es +precision highp float; + +in VS_OUT { + flat vec4 vertexColor; +} fs_in; + +out vec4 fragColor; + +void main() { + fragColor = fs_in.vertexColor; +} +``` + +--- + +### 3. GPU Data Structures (TypeScript) + +**glPlanTypes.ts:** +```typescript +export interface GLGeometryBatch { + /** Vertex buffer: interleaved (x, y, [z if 3D], ...) in world space. */ + vertexBuffer: WebGLBuffer; + vertexCount: number; + + /** Index buffer (triangles for fill, degenerate quads for strokes). */ + indexBuffer: WebGLBuffer; + indexCount: number; + + /** Vertex Array Object (VAO) binds VBO + IBO. */ + vao: WebGLVertexArrayObject; + + /** Per-batch metadata. */ + batches: Array<{ + kind: "polygon" | "line" | "arc"; + indexStart: number; + indexCount: number; + color: [r: number, g: number, b: number, a: number]; // RGBA [0,1] + hasHatch: boolean; + hatchPattern?: "solid" | "insulation" | "diagonal" | "crosshatch"; + strokeWidthMm?: number; + }>; +} + +export interface GLPlanRenderState { + // Pan/zoom transform: world (meters) → clip space + viewMatrix: Matrix3 | Matrix4; // 2×3 affine + projMatrix: Matrix4; // orthographic + + // Viewport size & DPI for screen-space stroke width + screenWidth: number; + screenHeight: number; + dpi: number; + + // Compiled shaders + solidFillProgram: WebGLProgram; + strokeProgram: WebGLProgram; + + // Geometry cache (tessellated once per plan) + geometryBatch: GLGeometryBatch | null; +} +``` + +--- + +## MVP API: PlanRenderer Class + +### Interface + +```typescript +export class PlanRenderer { + /** + * Create renderer with WebGL2 context + fallback config. + */ + constructor( + canvas: HTMLCanvasElement, + options?: { + enableGpu?: boolean; // default: true + enableGpuFallback?: boolean; // SVG fallback if GL fails + } + ); + + /** + * Compile and cache geometry from primitives. + * Call once per plan change. + */ + compilePlan(plan: Plan): Promise; + + /** + * Set pan/zoom transform matrix. + * Call on every view change (pan, zoom, fit). + */ + setViewMatrix(viewBox: { x, y, w, h }, canvasSize: { w, h }): void; + + /** + * Render one frame: clear, draw batches, composite. + * Called from requestAnimationFrame loop. + */ + render(): void; + + /** + * Release WebGL resources. + */ + dispose(): void; + + /** + * Query GPU availability / fallback state. + */ + isGpuReady(): boolean; + isFallbackActive(): boolean; +} +``` + +### Usage in PlanView + +**Before** (SVG only): +```tsx +function PlanView({ plan, ... }) { + return ( + + {hatches} + {plan.primitives.map((p, i) => )} + + ); +} +``` + +**After** (GPU + SVG fallback): +```tsx +function PlanView({ plan, ... }) { + const rendererRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + rendererRef.current = new PlanRenderer(canvas, { enableGpu: true }); + rendererRef.current.compilePlan(plan); + }, [plan]); + + useEffect(() => { + rendererRef.current?.setViewMatrix(view, { w: canvasWidth, h: canvasHeight }); + }, [view, canvasWidth, canvasHeight]); + + useEffect(() => { + const frame = () => { + rendererRef.current?.render(); + rafId = requestAnimationFrame(frame); + }; + rafId = requestAnimationFrame(frame); + return () => cancelAnimationFrame(rafId); + }, []); + + return ( +
+ {/* GPU canvas (or SVG fallback if GL unavailable) */} + + {/* Thin SVG overlay: text, grips, snap-markers, tool preview */} + + {/* text, grips, snaps only; geometry stays in WebGL */} + +
+ ); +} +``` + +--- + +## Data Flow: From Primitives → GPU + +### 1. **Compile Phase** (glPlanCompile.ts) + +```typescript +export function compilePlan(gl: WebGL2RenderingContext, plan: Plan): GLGeometryBatch { + const batches: BatchInfo[] = []; + const vertices: number[] = []; + const indices: number[] = []; + let indexOffset = 0; + + for (const prim of plan.primitives) { + if (prim.kind === "polygon") { + const { verts, inds } = tessellatePolygon(prim.pts); + const color = parseColor(prim.fill); + batches.push({ + kind: "polygon", + indexStart: indexOffset, + indexCount: inds.length, + color, + hasHatch: prim.hatch.pattern !== "none", + hatchPattern: prim.hatch.pattern, + }); + vertices.push(...verts); + indices.push(...inds.map((i) => i + indexOffset)); + indexOffset += verts.length / 2; + } else if (prim.kind === "line") { + const { verts, inds } = tessellateLineQuad(prim.a, prim.b); + const color = parseColor(prim.color || "black"); + batches.push({ + kind: "line", + indexStart: indexOffset, + indexCount: inds.length, + color, + strokeWidthMm: prim.weightMm, + }); + vertices.push(...verts); + indices.push(...inds.map((i) => i + indexOffset)); + indexOffset += verts.length / 2; + } + // arc → polyline → quads (deferred for MVP) + } + + const vbo = gl.createBuffer()!; + gl.bindBuffer(gl.ARRAY_BUFFER, vbo); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); + + const ibo = gl.createBuffer()!; + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(indices), gl.STATIC_DRAW); + + const vao = gl.createVertexArray()!; + gl.bindVertexArray(vao); + gl.bindBuffer(gl.ARRAY_BUFFER, vbo); + gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 8, 0); // position + gl.enableVertexAttribArray(0); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo); + + return { vertexBuffer: vbo, indexBuffer: ibo, vao, batches, vertexCount: vertices.length, indexCount: indices.length }; +} +``` + +### 2. **Render Phase** (glPlanRender.ts) + +```typescript +export function renderPlan( + gl: WebGL2RenderingContext, + state: GLPlanRenderState, + batch: GLGeometryBatch +): void { + gl.clearColor(1, 1, 1, 1); // white background + gl.clear(gl.COLOR_BUFFER_BIT); + + gl.useProgram(state.solidFillProgram); + const mvpLoc = gl.getUniformLocation(state.solidFillProgram, "viewProjection"); + const mvp = mat4.multiply(state.projMatrix, state.viewMatrix); + gl.uniformMatrix4fv(mvpLoc, false, mvp); + + gl.bindVertexArray(batch.vao); + + for (const b of batch.batches) { + const colorLoc = gl.getUniformLocation(state.solidFillProgram, "vertexColor"); + gl.uniform4f(colorLoc, b.color[0], b.color[1], b.color[2], b.color[3]); + + gl.drawElements(gl.TRIANGLES, b.indexCount, gl.UNSIGNED_INT, b.indexStart * 4); + } +} +``` + +--- + +## Tessellation Details + +### Earcut (Polygon Triangulation) + +**Inlined earcut logic (no npm):** +```typescript +function tessellatePolygon(pts: Vec2[]): { verts: number[]; inds: number[] } { + // Convert Vec2[] → flat float array + const coords = pts.flatMap((p) => [p.x, p.y]); + + // Earcut2D: robust polygon triangulation + // → Returns index array (triplets = triangles) + const triangles = earcut(coords); + + // Vertex buffer: just positions (x, y) in world space (meters) + const verts = coords; + + return { verts, inds: triangles }; +} + +// Simplified earcut (full version ~200 LOC; reference libtess2 or earcut.js) +function earcut(data: number[], hole?: number[], dim?: number): number[] { + // ... iterative ear clipping, complexity O(n²) worst-case + // Returns Uint32Array of triangle indices +} +``` + +### Line Quad Expansion + +```typescript +function tessellateLineQuad( + a: Vec2, b: Vec2, + widthMm: number = 0.5 +): { verts: number[]; inds: number[] } { + // World-space endpoints; width (mm) will be expanded in vertex shader + + // Create a degenerate quad: 2 triangles + // Vertices: [a_left, a_right, b_left, b_right] + // (normal expansion happens in VS) + + const verts = [ + a.x, a.y, 0.0, // vertex 0: a, left flag + a.x, a.y, 1.0, // vertex 1: a, right flag + b.x, b.y, 0.0, // vertex 2: b, left flag + b.x, b.y, 1.0, // vertex 3: b, right flag + ]; + + // Two triangles: (0, 1, 2) and (1, 3, 2) + const inds = [0, 1, 2, 1, 3, 2]; + + return { verts, inds }; +} +``` + +--- + +## Pan/Zoom Matrix Transform + +### View Box → Clip Space + +```typescript +function buildViewMatrix( + viewBox: { x, y, w, h }, + canvasSize: { w, h } +): Matrix4 { + // 1. World space (meters, origin at model 0,0) → viewBox units (PX_PER_M=90) + const scale = PX_PER_M; // 1 meter → 90 viewBox units + + // 2. ViewBox viewport: x,y,w,h in viewBox units → NDC [-1,+1]² + // Orthographic projection (no perspective). + const ortho = mat4.ortho( + viewBox.x, + viewBox.x + viewBox.w, + viewBox.y, + viewBox.y + viewBox.h, + -1, 1 + ); + + // 3. Scale from viewBox units → world (invert PX_PER_M) + const scaleMatrix = mat4.scale(mat4.identity(), [1/scale, 1/scale, 1]); + + return mat4.multiply(ortho, scaleMatrix); +} +``` + +Whenever PlanView calls `setView(viewBox)` or `onWheel()` → call `setViewMatrix()` → GPU re-renders with new matrix uniform (no tessellation). + +--- + +## Fallback Strategy: SVG Renderer Flag + +**Global flag** in PlanView or app state: +```typescript +const [useGpuRenderer, setUseGpuRenderer] = useState(true); +``` + +**Render path branching:** +```typescript +return useGpuRenderer && rendererRef.current?.isGpuReady() + ? + : {/* existing SVG rendering */}; +``` + +**When GL fails** (e.g., no WebGL2 support, Out-Of-Memory): +1. Renderer catches error in `compilePlan()` +2. Sets internal `fallbackActive = true` +3. Returns gracefully (app renders SVG path instead) +4. User sees same plan, slower but functional + +--- + +## Implementation Order (MVP → Iteration) + +### Phase 1: Core (Week 1) +1. **glPlanTypes.ts** — TypeScript interfaces for GPU state +2. **glPlanShaders.ts** — Compile vertex/fragment shaders, handle GL errors +3. **glPlanCompile.ts** — Tessellation (earcut inlined), buffer upload +4. **glPlanRender.ts** — Draw loop, matrix uniform, clear/present +5. **PlanRenderer.ts** — Main class, dispatcher (GPU vs SVG fallback) +6. **PlanView.tsx** — Wire renderer, canvas overlay, canvas lifecycle + +### Phase 2: Hatches & Lines (Week 2) +- Improve line tessellation: proper screen-space width (dFdx/dFdy or pre-computed normals) +- Hatch patterns: texture-based or procedural fragment shader (diagonal/insulation) +- Arc tessellation: polyline → quads + +### Phase 3: Polish (Week 3) +- Stroke dashing via geometry or fragment shader +- Greyed opacity blending +- Hit testing integration (point-in-triangle for GPU) +- Performance profiling, batch merging + +--- + +## Performance Targets + +| Operation | SVG (Current) | GPU (Target) | Notes | +|-----------|---------------|--------------|-------| +| **Tessellation** | — | 10–50 ms | Once per plan | +| **Pan/Zoom 60 Hz** | 16 ms (re-render SVG) | <1 ms (matrix uniform) | Matrix upload negligible | +| **Pan/Zoom 144 Hz** | 7 ms (bottleneck) | <0.5 ms | 28× speedup expected | +| **Geometry: 1000 polygons** | 50–100 ms SVG render | 1–5 ms GPU draw | CPU tessellation pipelined | + +User: AMD RX 7800 XT → easily capable of 4K+ geometry at 144 Hz. + +--- + +## Known Deferred Items (Post-MVP) + +- **Hatches**: Solid fill only MVP; insulation/diagonal/crosshatch in Phase 2 via texture or procedural shader +- **Dashing**: Not in MVP (complex with screen-space strokes); either CPU pre-tessellation or fragment shader alpha-discard +- **Arcs**: Fallback to SVG for MVP; GPU polyline expansion in Phase 2 +- **Text, Grips, Snaps**: Stay in SVG overlay indefinitely (no GPU benefit; text rendering nontrivial) +- **Hit Testing**: Keep in CPU/SVG for MVP; GPU pick-buffer deferred +- **Color/Opacity Blending**: Basic for MVP; advanced (multiply, screen, dodge) deferred + +--- + +## References + +- **Earcut.js**: https://github.com/mapbox/earcut — polygon triangulation (logic to inline) +- **three.js line expansion**: https://github.com/mrdoob/three.js/blob/master/src/renderers/webgl/WebGLGeometries.js +- **OpenGL Perspective Division**: https://en.wikibooks.org/wiki/OpenGL_Programming/Modern_OpenGL_Tutorial_Polygon_offset +- **Screen-Space Stroke Width**: https://forum.libcinder.org/topic/smooth-line-rendering-using-geometry-shaders +- PlanView source: `/home/karim/cad/src/plan/PlanView.tsx` (2500 LOC) +- Primitive types: `/home/karim/cad/src/plan/generatePlan.ts:133` diff --git a/docs/design/wgpu-2d-renderer-briefing.md b/docs/design/wgpu-2d-renderer-briefing.md new file mode 100644 index 0000000..9390048 --- /dev/null +++ b/docs/design/wgpu-2d-renderer-briefing.md @@ -0,0 +1,81 @@ +# Briefing — Nativer wgpu-2D-Renderer (parallele Instanz) + +> Für die Instanz/den Agenten, der den nativen GPU-Renderer baut. Isoliert vom +> Web-Renderer; koordiniert über dieses Dokument. + +## COMMIT-REGEL (verbindlich) +Dieses Repo darf **keinerlei Hinweise auf KI-Werkzeuge** enthalten — nicht im Code, +in Kommentaren oder in der Git-Historie. Keine `Co-Authored-By`-Trailer, keine +„Generated with"-Zeilen, keine Modell-/Agenten-Namen. Kommentare deutsch, +Identifier englisch. **Kein Commit ohne Ansage der Hauptinstanz.** + +## Warum +Der 2D-Plan wird aktuell in einem WebGL-Canvas gerendert (Web-Stack). In Chromium +ist das 144-Hz-flüssig, aber **Tauris Linux-Webview = WebKitGTK bremst** (langsamer +Compositor, vermutlich 60-Hz-rAF-Cap). Bewiesen: dieselbe App im `chromium --app`- +Fenster = butterweich, im Tauri-Fenster = zäh — trotz imperativem Pan, CSS-transform- +Overlay, DMABUF-Tweaks. Siehe Memo `webkitgtk-bottleneck`. + +**Lösung:** die schwere Grafik **nativ mit wgpu** rendern (Rust), die Webview macht +nur noch UI-Chrome (Panels/Buttons). So bleibt Tauri (natives Rust-Backend, winziges +Bundle) UND wir umgehen den Webview-Compositor komplett. + +## Referenz-Implementierung (NICHT wegwerfen — 1:1 portierbar) +Der WebGL-Renderer unter `src/plan/glPlan/` ist die **arbeitende Referenz**. Die +harte Arbeit ist dort schon gelöst und portiert konzeptuell direkt nach wgpu: +- `glPlanCompile.ts` — **Earcut-Triangulierung** (konkav-fähig), Linien→Quads mit + Normale+Seiten-Flag, Batch-Merging, Farb-Parsing. Unit-Tests: `glPlanCompile.test.ts`. +- `glPlanShaders.ts` — Vertex/Fragment (GLSL 300 es). **WGSL ≈ GLSL** — direkt übersetzbar. + - Füll-VS: Bildschirm-Position → Clip via `viewProj`-Matrix. + - Linien-VS: bildschirmkonstante bzw. papier-mm-Breite via Normalen-Offset im Clip + (siehe `strokeScale`/`strokePx`-Logik — Papier-mm × Massstab N × meet-Skala). +- `glPlanRender.ts` — Ortho-Matrix (aspekt-korrekt = SVG `xMidYMid meet`), Fill-/Line- + Pass, `computeOrthoMatrix`. +- Koordinaten-Konvention: BILDSCHIRM-Raum `sx = mx·PX_PER_M, sy = -my·PX_PER_M` + (`PX_PER_M=90`), Modell-Y hoch → Bildschirm-Y runter. +- Strichbreite = **echte Papier-mm im Massstab**: `Breite_px = mm · N/1000 · PX_PER_M · + meetSkala` (repliziert SVG `printStrokeVb`). Am Beispiel 0.35 mm @ 1:100 = 0.35 mm. + +Die **Daten** kommen aus `src/plan/generatePlan.ts` → `Plan.primitives` (Union-Typ +`Primitive`: polygon/line/arc/text). Text bleibt Overlay (nicht wgpu). + +## Die EINE harte Frage (zuerst spiken/recherchieren) +**Wie rendert wgpu in das Tauri-Fenster neben der Webview?** Optionen recherchieren: +1. Natives Child-Surface unter der Webview (raw-window-handle), Webview transparent + drüber für UI-Chrome. Vermutlich der Zielweg. +2. Eigenes wgpu-Fenster (entkoppelt) — nur für den Spike, um Rendering von der + Fenster-Integration zu trennen. +3. wgpu→Textur→Webview — verwirft den Zweck (Copy-Overhead), nur Notnagel. + +**Empfehlung:** Rendering-Spike ZUERST entkoppelt (Option 2, standalone `winit`+wgpu- +Fenster), das die `Primitive` als gefüllte Polygone + Striche zeichnet und Pan/Zoom +per Matrix-Uniform macht. Fenster-Integration in Tauri ist der zweite, separate Schritt. + +## Meilensteine +- **M0 (Research):** kurzer Bericht `docs/design/wgpu-integration-findings.md` — wie + wgpu-Surface + Tauri/webview koexistieren (raw-window-handle, Transparenz, Z-Order, + Input-Routing). Quellen verlinken. +- **M1 (Rendering-Spike, standalone):** neue Crate `src-tauri/render2d/` (serde-Input + = geflachte Primitive), wgpu + WGSL: + - Earcut-Port (oder `lyon`/`earcutr` Crate evaluieren) → Dreiecke. + - Füll-Pipeline (gefüllte Polygone, Ortho-Matrix-Uniform). + - Linien-Pipeline (Quad-Expansion, echte Papier-mm-Breite). + - `cargo test` für die Tessellierung (Muster: `src-tauri/geometry` hat 4/4 Tests — + z. B. konkaves L flächentreu, wie `glPlanCompile.test.ts`). + - `cargo build` grün. (Visuelle Fenster-Verifikation braucht Display → an Mensch/ + Display-Session übergeben; im Bericht vermerken.) +- **M2 (Tauri-Integration):** Surface unter die Webview, Pan/Zoom-Input aus der + Webview an den Renderer, Parität mit dem WebGL-Pfad messen (144 Hz). +- **M3:** Schraffur, Bögen, Auswahl-Highlight; danach 3D (three.js→wgpu). + +## Koordination (WICHTIG) +- **Eigener Branch** (z. B. `feature/wgpu-renderer`), NICHT auf `master`/ + `feature/parametric-walls` committen. Isolierter Worktree. +- **Nur** `src-tauri/` + neue Rust-Module + `docs/`. **NICHT** `src/App.tsx`, + `src/plan/*`, `types.ts` anfassen (die Hauptinstanz arbeitet dort an Features). +- Der Web-WebGL-Renderer bleibt bestehen (Browser-Lauffähigkeit + Referenz). +- Ergebnisse/Diffs zurückliefern; die Hauptinstanz committet und pflegt das Memory. + +## Gates +`cargo check`/`cargo test`/`cargo build` grün. Trace-Scan sauber (COMMIT-REGEL). +`npx tsc -b` + `npm run build` müssen unberührt grün bleiben (keine Web-Änderungen). diff --git a/docs/welle-c-hlr-spike/FEASIBILITY.md b/docs/welle-c-hlr-spike/FEASIBILITY.md new file mode 100644 index 0000000..b0aebab --- /dev/null +++ b/docs/welle-c-hlr-spike/FEASIBILITY.md @@ -0,0 +1,124 @@ +# Welle C — HLR-Spike: Feasibility-Report + +> 2D-Vektor-Schnitte/-Ansichten aus dem 3D-Modell via Hidden-Line-Removal (HLR) +> mit opencascade.js (OCCT-WASM). Isolierter De-Risking-Spike, NICHT in die App +> verdrahtet. Einheiten: Meter. + +## Ergebnis (kurz) + +**HLR funktioniert in diesem Browser-Projekt.** opencascade.js initialisiert im +Vite-Build, und der HLR-Lauf liefert korrekte, getrennte sichtbare/verdeckte +2D-Kanten. Verifiziert im echten Browser (Chromium via Vite-Dev-Server) an einer +L-Wand + Bodenplatte, Front-Ansicht: + +| Messwert | Wert | +|---|---| +| Sichtbare Kanten (sharp) | **9** | +| Verdeckte Kanten | **16** | +| Reine HLR-Rechenzeit | **~21 ms** | +| WASM-Größe (unkomprimiert) | **62.8 MB** (65 864 037 Bytes) | +| WASM-Größe (gzip) | **~19.6 MB** | +| WASM-Fetch (lokal, Dev) | ~113 ms | +| Modul-Init (Emscripten instanziieren) | ~450 ms (Browser) / ~680 ms (Node) | + +Beweis-Artefakte: `hlr-elevation-proof.svg` / `.png` in diesem Ordner — sichtbare +Kanten durchgezogen, verdeckte gestrichelt. Die Zeichnung liest sich als korrekte +Hidden-Line-Ansicht (Silhouette + sichtbare Front-Kanten voll, verdeckte hinten +gestrichelt). + +## Der genutzte API-Pfad (wichtig!) + +Der geplante klassische Pfad (`HLRBRep_Algo`/`HLRBRep_PolyAlgo` + +`HLRBRep_HLRToShape`/`HLRBRep_PolyHLRToShape`) ist im **prebuilt Vollbuild +1.1.1 NICHT verfügbar**: diese Klassen sind nur als `Handle_…`-Smart-Pointer +gebunden, ohne konstruierbare Roh-Klasse und ohne den `HLRToShape`-Extraktor. +Ein direkter Aufbau darüber ist damit nicht möglich. + +**Verwendeter, funktionierender Pfad:** `HLRAppli_ReflectLines` — der High-Level- +OCCT-Wrapper, der intern GENAU den exakten HLR-Algorithmus (`HLRBRep_Algo`) +fährt. Er ist als Klasse gebunden und liefert getrennt sichtbare/verdeckte +Kanten nach Kanten-Typ: + +``` +const rl = new oc.HLRAppli_ReflectLines(shape); +rl.SetAxes(dirX, dirY, dirZ, atX, atY, atZ, upX, upY, upZ); // Ortho-Projektion +rl.Perform(); +const T = oc.HLRBRep_TypeOfResultingEdge; +// (typ, visible, in3d) → in3d=false liefert 2D-projizierte Kanten (Z≈0) +const visSharp = rl.GetCompoundOf3dEdges(T.HLRBRep_Sharp, true, false); +const visOutline = rl.GetCompoundOf3dEdges(T.HLRBRep_OutLine, true, false); +const hidSharp = rl.GetCompoundOf3dEdges(T.HLRBRep_Sharp, false, false); +``` + +Kanten-Extraktion: `TopExp_Explorer_2(compound, TopAbs_EDGE, TopAbs_SHAPE)` → +`TopoDS.Edge_1(current)` → `new BRepAdaptor_Curve_2(edge)` → +`FirstParameter/LastParameter/Value(u)` (`gp_Pnt` mit `.X() .Y() .Z()`). + +**embind-Überladungen sind versioniert** (`_1`, `_2`, …) und die Nummerierung im +Vollbuild folgt NICHT der Argumentzahl. Empirisch verifiziert: +`BRepPrimAPI_MakeBox_1(dx,dy,dz)`, `BRepPrimAPI_MakeBox_3(gp_Pnt, gp_Pnt)`, +`BRepAlgoAPI_Fuse_3(a, b)`, `gp_Pnt_3(x,y,z)`. Bei einem Versionswechsel des +Pakets müssen diese Suffixe neu geprüft werden (Fehlermeldung nennt die +erwartete Parameterzahl). + +## Vite-/package.json-Änderungen (genau) + +- `package.json`: Dependency `opencascade.js@^1.1.1` (Vollbuild). +- `vite.config.ts` (additiv, analog zum bestehenden LibreDWG-Muster): + - Zwei Aliase → virtuelle Module auf die echten Paket-Dateien: + `virtual:occt-glue` → `dist/opencascade.wasm.js`, + `virtual:occt-wasm-url` → `dist/opencascade.wasm.wasm?url`. + - `optimizeDeps.exclude: ["opencascade.js"]` (esbuild kommt mit dem + UMD-Wrapper + Node-Shims der Glue nicht klar → Vor-Bündeln ausschließen). +- `src/section/occt-wasm.d.ts`: Ambient-Stubs für die zwei virtuellen Module. +- Geladen wird per **dynamischem Import** in `src/section/occt.ts` → die 62-MB- + WASM landet NICHT im Haupt-Bundle, sondern als separater Lazy-Chunk + Asset. + +Verifiziert: `vite build` der App bleibt grün und unverändert groß (kein +OCCT-Chunk, da der Spike nicht verdrahtet ist). Ein Lib-Build, der `hlr.ts` +referenziert, splittet OCCT sauber in einen eigenen Glue-Chunk + WASM-Asset ab. +`tsc` ist für die neuen Dateien grün. (`npm run build` schlägt aktuell in +`tsc -b` fehl — ausschließlich wegen fehlender i18n-Keys in den parallel +bearbeiteten Dateien `commands/cmds/stair.ts` / `state/*`, NICHT wegen dieses +Spikes.) + +## Dateien dieses Spikes + +- `src/section/hlr.ts` — Kernmodul: Box-Specs → Fuse → HLR → 2D-Polylinien + (`hlrFromBoxes`, `hlrShape`, `VIEWS`, `hlrToSvg`). +- `src/section/occt.ts` — Lazy-Loader + schmale Typ-Fassade + Lade-Metriken. +- `src/section/occt-wasm.d.ts` — Ambient-Deklarationen der virtuellen Module. +- `docs/welle-c-hlr-spike/` — dieser Report + Proof-SVG/-PNG. + +## Bewertung für Welle C + +**Viabel.** Der exakte HLR liefert saubere, normgerechte Vektor-Kanten mit +korrekter Sichtbarkeit — genau das, was Schnitt/Ansicht brauchen und was reines +three.js-Kanten-Projizieren nicht robust liefert (dort fehlt echte +Flächen-Verdeckung). Die 2D-Polylinien passen direkt in die bestehende +SVG-Plan-Pipeline (`Pt2`/`Vec2`-kompatibel). + +**Der Kostenpunkt ist die WASM-Größe (62.8 MB / ~19.6 MB gzip).** Für einen +Spike akzeptabel; für Produktion zu groß, um sie eager zu laden. + +### Empfohlener Integrations- + Größenreduktions-Plan + +1. **Lazy laden** (bereits so gebaut): OCCT erst bei erster Schnitt-/Ansichts- + Erzeugung dynamisch importieren; Ladezustand in der UI anzeigen. Der + Grundriss läuft weiter ohne OCCT (parametrisch, wie bisher). +2. **Web-Worker**: HLR im Worker fahren, damit der UI-Thread frei bleibt (die + WASM ist groß, aber der HLR-Lauf selbst ist mit ~20 ms günstig). +3. **Größe reduzieren — lohnt sich klar**: ein **Custom-Build** von + opencascade.js (das Paket unterstützt `make.py`/Docker-Build mit einer + Symbol-Whitelist). Für Welle C reicht ein minimaler Satz: + `BRepPrimAPI_*` (bzw. der spätere Solid-Erzeuger), `BRepAlgoAPI_Fuse/Common`, + `HLRAppli_ReflectLines` + `HLRBRep_TypeOfResultingEdge`, `TopExp_Explorer`, + `TopoDS`, `BRepAdaptor_Curve`, `gp_*`. Erfahrungswerte solcher Minimal-Builds + liegen bei **~5–15 MB WASM** statt 62 MB — Aufwand: ein reproduzierbarer + Docker-Build in CI, der die `.wasm`/`.js` als Projekt-Asset eincheckt. + Alternativ die **beta 2.0** prüfen (moderneres Build-System, evtl. bereits + schlankere Module + die klassischen HLR-Klassen konstruierbar). +4. **Fallback, falls die Größe untragbar bleibt**: three.js-basierte + Kanten-Extraktion (`EdgesGeometry`) + eigene Sichtbarkeit via Depth-Peeling / + GPU-Occlusion. Deutlich mehr Eigenaufwand, weniger robust bei Verschneidungen + — daher nur zweite Wahl; OCCT-HLR bleibt der empfohlene Weg. diff --git a/docs/welle-c-hlr-spike/hlr-elevation-proof.png b/docs/welle-c-hlr-spike/hlr-elevation-proof.png new file mode 100644 index 0000000..13afe73 Binary files /dev/null and b/docs/welle-c-hlr-spike/hlr-elevation-proof.png differ diff --git a/docs/welle-c-hlr-spike/hlr-elevation-proof.svg b/docs/welle-c-hlr-spike/hlr-elevation-proof.svg new file mode 100644 index 0000000..a4b579a --- /dev/null +++ b/docs/welle-c-hlr-spike/hlr-elevation-proof.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9c94069..eeb4490 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@mlightcad/libredwg-web": "^0.7.7", + "@tauri-apps/api": "^2.11.1", "delaunator": "^5.1.0", "dxf-parser": "^1.1.2", "jspdf": "^4.2.1", @@ -21,6 +22,7 @@ "three": "^0.169.0" }, "devDependencies": { + "@tauri-apps/cli": "^2.11.4", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.0", "@types/three": "^0.169.0", @@ -1539,6 +1541,248 @@ "dev": true, "license": "MIT" }, + "node_modules/@tauri-apps/api": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz", + "integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz", + "integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.4", + "@tauri-apps/cli-darwin-x64": "2.11.4", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", + "@tauri-apps/cli-linux-arm64-musl": "2.11.4", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-musl": "2.11.4", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", + "@tauri-apps/cli-win32-x64-msvc": "2.11.4" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz", + "integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz", + "integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz", + "integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz", + "integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz", + "integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz", + "integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz", + "integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz", + "integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz", + "integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz", + "integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz", + "integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@tweenjs/tween.js": { "version": "23.1.3", "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", diff --git a/package.json b/package.json index a15c5c9..e1b9349 100644 --- a/package.json +++ b/package.json @@ -8,10 +8,14 @@ "build": "tsc -b && vite build", "preview": "vite preview", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "tauri": "tauri", + "tauri:dev": "tauri dev", + "tauri:build": "tauri build" }, "dependencies": { "@mlightcad/libredwg-web": "^0.7.7", + "@tauri-apps/api": "^2.11.1", "delaunator": "^5.1.0", "dxf-parser": "^1.1.2", "jspdf": "^4.2.1", @@ -24,6 +28,7 @@ "three": "^0.169.0" }, "devDependencies": { + "@tauri-apps/cli": "^2.11.4", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.0", "@types/three": "^0.169.0", diff --git a/public/assets/materials/Bricks104/ao.jpg b/public/assets/materials/Bricks104/ao.jpg new file mode 100644 index 0000000..07df426 Binary files /dev/null and b/public/assets/materials/Bricks104/ao.jpg differ diff --git a/public/assets/materials/Bricks104/color.jpg b/public/assets/materials/Bricks104/color.jpg new file mode 100644 index 0000000..67a753e Binary files /dev/null and b/public/assets/materials/Bricks104/color.jpg differ diff --git a/public/assets/materials/Bricks104/displacement.jpg b/public/assets/materials/Bricks104/displacement.jpg new file mode 100644 index 0000000..06eea41 Binary files /dev/null and b/public/assets/materials/Bricks104/displacement.jpg differ diff --git a/public/assets/materials/Bricks104/normal.jpg b/public/assets/materials/Bricks104/normal.jpg new file mode 100644 index 0000000..4159268 Binary files /dev/null and b/public/assets/materials/Bricks104/normal.jpg differ diff --git a/public/assets/materials/Bricks104/roughness.jpg b/public/assets/materials/Bricks104/roughness.jpg new file mode 100644 index 0000000..4298edd Binary files /dev/null and b/public/assets/materials/Bricks104/roughness.jpg differ diff --git a/public/assets/materials/Concrete048/ao.jpg b/public/assets/materials/Concrete048/ao.jpg new file mode 100644 index 0000000..4f701b6 Binary files /dev/null and b/public/assets/materials/Concrete048/ao.jpg differ diff --git a/public/assets/materials/Concrete048/color.jpg b/public/assets/materials/Concrete048/color.jpg new file mode 100644 index 0000000..2e84821 Binary files /dev/null and b/public/assets/materials/Concrete048/color.jpg differ diff --git a/public/assets/materials/Concrete048/displacement.jpg b/public/assets/materials/Concrete048/displacement.jpg new file mode 100644 index 0000000..2d5ac54 Binary files /dev/null and b/public/assets/materials/Concrete048/displacement.jpg differ diff --git a/public/assets/materials/Concrete048/normal.jpg b/public/assets/materials/Concrete048/normal.jpg new file mode 100644 index 0000000..06bd5d2 Binary files /dev/null and b/public/assets/materials/Concrete048/normal.jpg differ diff --git a/public/assets/materials/Concrete048/roughness.jpg b/public/assets/materials/Concrete048/roughness.jpg new file mode 100644 index 0000000..5273c5a Binary files /dev/null and b/public/assets/materials/Concrete048/roughness.jpg differ diff --git a/public/assets/materials/Grass005/ao.jpg b/public/assets/materials/Grass005/ao.jpg new file mode 100644 index 0000000..3c5af47 Binary files /dev/null and b/public/assets/materials/Grass005/ao.jpg differ diff --git a/public/assets/materials/Grass005/color.jpg b/public/assets/materials/Grass005/color.jpg new file mode 100644 index 0000000..ff69ce1 Binary files /dev/null and b/public/assets/materials/Grass005/color.jpg differ diff --git a/public/assets/materials/Grass005/displacement.jpg b/public/assets/materials/Grass005/displacement.jpg new file mode 100644 index 0000000..d15812e Binary files /dev/null and b/public/assets/materials/Grass005/displacement.jpg differ diff --git a/public/assets/materials/Grass005/normal.jpg b/public/assets/materials/Grass005/normal.jpg new file mode 100644 index 0000000..e134178 Binary files /dev/null and b/public/assets/materials/Grass005/normal.jpg differ diff --git a/public/assets/materials/Grass005/roughness.jpg b/public/assets/materials/Grass005/roughness.jpg new file mode 100644 index 0000000..89355b1 Binary files /dev/null and b/public/assets/materials/Grass005/roughness.jpg differ diff --git a/public/assets/materials/Gravel043/ao.jpg b/public/assets/materials/Gravel043/ao.jpg new file mode 100644 index 0000000..81487c5 Binary files /dev/null and b/public/assets/materials/Gravel043/ao.jpg differ diff --git a/public/assets/materials/Gravel043/color.jpg b/public/assets/materials/Gravel043/color.jpg new file mode 100644 index 0000000..1d4193d Binary files /dev/null and b/public/assets/materials/Gravel043/color.jpg differ diff --git a/public/assets/materials/Gravel043/displacement.jpg b/public/assets/materials/Gravel043/displacement.jpg new file mode 100644 index 0000000..c442c88 Binary files /dev/null and b/public/assets/materials/Gravel043/displacement.jpg differ diff --git a/public/assets/materials/Gravel043/normal.jpg b/public/assets/materials/Gravel043/normal.jpg new file mode 100644 index 0000000..6969c9a Binary files /dev/null and b/public/assets/materials/Gravel043/normal.jpg differ diff --git a/public/assets/materials/Gravel043/roughness.jpg b/public/assets/materials/Gravel043/roughness.jpg new file mode 100644 index 0000000..992cd95 Binary files /dev/null and b/public/assets/materials/Gravel043/roughness.jpg differ diff --git a/public/assets/materials/Ground103/ao.jpg b/public/assets/materials/Ground103/ao.jpg new file mode 100644 index 0000000..6395d86 Binary files /dev/null and b/public/assets/materials/Ground103/ao.jpg differ diff --git a/public/assets/materials/Ground103/color.jpg b/public/assets/materials/Ground103/color.jpg new file mode 100644 index 0000000..222b658 Binary files /dev/null and b/public/assets/materials/Ground103/color.jpg differ diff --git a/public/assets/materials/Ground103/displacement.jpg b/public/assets/materials/Ground103/displacement.jpg new file mode 100644 index 0000000..b063380 Binary files /dev/null and b/public/assets/materials/Ground103/displacement.jpg differ diff --git a/public/assets/materials/Ground103/normal.jpg b/public/assets/materials/Ground103/normal.jpg new file mode 100644 index 0000000..2d7ab00 Binary files /dev/null and b/public/assets/materials/Ground103/normal.jpg differ diff --git a/public/assets/materials/Ground103/roughness.jpg b/public/assets/materials/Ground103/roughness.jpg new file mode 100644 index 0000000..87cd0ba Binary files /dev/null and b/public/assets/materials/Ground103/roughness.jpg differ diff --git a/public/assets/materials/Marble012/color.jpg b/public/assets/materials/Marble012/color.jpg new file mode 100644 index 0000000..b5aed16 Binary files /dev/null and b/public/assets/materials/Marble012/color.jpg differ diff --git a/public/assets/materials/Marble012/displacement.jpg b/public/assets/materials/Marble012/displacement.jpg new file mode 100644 index 0000000..e854de0 Binary files /dev/null and b/public/assets/materials/Marble012/displacement.jpg differ diff --git a/public/assets/materials/Marble012/normal.jpg b/public/assets/materials/Marble012/normal.jpg new file mode 100644 index 0000000..f9784b7 Binary files /dev/null and b/public/assets/materials/Marble012/normal.jpg differ diff --git a/public/assets/materials/Marble012/roughness.jpg b/public/assets/materials/Marble012/roughness.jpg new file mode 100644 index 0000000..14b3459 Binary files /dev/null and b/public/assets/materials/Marble012/roughness.jpg differ diff --git a/public/assets/materials/Metal063/color.jpg b/public/assets/materials/Metal063/color.jpg new file mode 100644 index 0000000..4bdc921 Binary files /dev/null and b/public/assets/materials/Metal063/color.jpg differ diff --git a/public/assets/materials/Metal063/displacement.jpg b/public/assets/materials/Metal063/displacement.jpg new file mode 100644 index 0000000..9359505 Binary files /dev/null and b/public/assets/materials/Metal063/displacement.jpg differ diff --git a/public/assets/materials/Metal063/metalness.jpg b/public/assets/materials/Metal063/metalness.jpg new file mode 100644 index 0000000..68252f1 Binary files /dev/null and b/public/assets/materials/Metal063/metalness.jpg differ diff --git a/public/assets/materials/Metal063/normal.jpg b/public/assets/materials/Metal063/normal.jpg new file mode 100644 index 0000000..d64c0e6 Binary files /dev/null and b/public/assets/materials/Metal063/normal.jpg differ diff --git a/public/assets/materials/Metal063/roughness.jpg b/public/assets/materials/Metal063/roughness.jpg new file mode 100644 index 0000000..8862909 Binary files /dev/null and b/public/assets/materials/Metal063/roughness.jpg differ diff --git a/public/assets/materials/PavingStones150/ao.jpg b/public/assets/materials/PavingStones150/ao.jpg new file mode 100644 index 0000000..5b8c46d Binary files /dev/null and b/public/assets/materials/PavingStones150/ao.jpg differ diff --git a/public/assets/materials/PavingStones150/color.jpg b/public/assets/materials/PavingStones150/color.jpg new file mode 100644 index 0000000..0069504 Binary files /dev/null and b/public/assets/materials/PavingStones150/color.jpg differ diff --git a/public/assets/materials/PavingStones150/displacement.jpg b/public/assets/materials/PavingStones150/displacement.jpg new file mode 100644 index 0000000..48cbc0e Binary files /dev/null and b/public/assets/materials/PavingStones150/displacement.jpg differ diff --git a/public/assets/materials/PavingStones150/normal.jpg b/public/assets/materials/PavingStones150/normal.jpg new file mode 100644 index 0000000..c856c19 Binary files /dev/null and b/public/assets/materials/PavingStones150/normal.jpg differ diff --git a/public/assets/materials/PavingStones150/roughness.jpg b/public/assets/materials/PavingStones150/roughness.jpg new file mode 100644 index 0000000..40daa5b Binary files /dev/null and b/public/assets/materials/PavingStones150/roughness.jpg differ diff --git a/public/assets/materials/Plaster001/color.jpg b/public/assets/materials/Plaster001/color.jpg new file mode 100644 index 0000000..54b8d7b Binary files /dev/null and b/public/assets/materials/Plaster001/color.jpg differ diff --git a/public/assets/materials/Plaster001/displacement.jpg b/public/assets/materials/Plaster001/displacement.jpg new file mode 100644 index 0000000..a318d06 Binary files /dev/null and b/public/assets/materials/Plaster001/displacement.jpg differ diff --git a/public/assets/materials/Plaster001/normal.jpg b/public/assets/materials/Plaster001/normal.jpg new file mode 100644 index 0000000..f1baa32 Binary files /dev/null and b/public/assets/materials/Plaster001/normal.jpg differ diff --git a/public/assets/materials/Plaster001/roughness.jpg b/public/assets/materials/Plaster001/roughness.jpg new file mode 100644 index 0000000..fbcd590 Binary files /dev/null and b/public/assets/materials/Plaster001/roughness.jpg differ diff --git a/public/assets/materials/Tiles141/color.jpg b/public/assets/materials/Tiles141/color.jpg new file mode 100644 index 0000000..205a41e Binary files /dev/null and b/public/assets/materials/Tiles141/color.jpg differ diff --git a/public/assets/materials/Tiles141/displacement.jpg b/public/assets/materials/Tiles141/displacement.jpg new file mode 100644 index 0000000..48d1609 Binary files /dev/null and b/public/assets/materials/Tiles141/displacement.jpg differ diff --git a/public/assets/materials/Tiles141/normal.jpg b/public/assets/materials/Tiles141/normal.jpg new file mode 100644 index 0000000..3c65220 Binary files /dev/null and b/public/assets/materials/Tiles141/normal.jpg differ diff --git a/public/assets/materials/Tiles141/roughness.jpg b/public/assets/materials/Tiles141/roughness.jpg new file mode 100644 index 0000000..caac622 Binary files /dev/null and b/public/assets/materials/Tiles141/roughness.jpg differ diff --git a/public/assets/materials/Wood095/color.jpg b/public/assets/materials/Wood095/color.jpg new file mode 100644 index 0000000..86ddc11 Binary files /dev/null and b/public/assets/materials/Wood095/color.jpg differ diff --git a/public/assets/materials/Wood095/displacement.jpg b/public/assets/materials/Wood095/displacement.jpg new file mode 100644 index 0000000..d3dc560 Binary files /dev/null and b/public/assets/materials/Wood095/displacement.jpg differ diff --git a/public/assets/materials/Wood095/normal.jpg b/public/assets/materials/Wood095/normal.jpg new file mode 100644 index 0000000..20e4b1a Binary files /dev/null and b/public/assets/materials/Wood095/normal.jpg differ diff --git a/public/assets/materials/Wood095/roughness.jpg b/public/assets/materials/Wood095/roughness.jpg new file mode 100644 index 0000000..ca0faf4 Binary files /dev/null and b/public/assets/materials/Wood095/roughness.jpg differ diff --git a/public/assets/materials/WoodFloor051/ao.jpg b/public/assets/materials/WoodFloor051/ao.jpg new file mode 100644 index 0000000..a3dd3c2 Binary files /dev/null and b/public/assets/materials/WoodFloor051/ao.jpg differ diff --git a/public/assets/materials/WoodFloor051/color.jpg b/public/assets/materials/WoodFloor051/color.jpg new file mode 100644 index 0000000..52823d0 Binary files /dev/null and b/public/assets/materials/WoodFloor051/color.jpg differ diff --git a/public/assets/materials/WoodFloor051/displacement.jpg b/public/assets/materials/WoodFloor051/displacement.jpg new file mode 100644 index 0000000..a618199 Binary files /dev/null and b/public/assets/materials/WoodFloor051/displacement.jpg differ diff --git a/public/assets/materials/WoodFloor051/normal.jpg b/public/assets/materials/WoodFloor051/normal.jpg new file mode 100644 index 0000000..fa468e0 Binary files /dev/null and b/public/assets/materials/WoodFloor051/normal.jpg differ diff --git a/public/assets/materials/WoodFloor051/roughness.jpg b/public/assets/materials/WoodFloor051/roughness.jpg new file mode 100644 index 0000000..b68e764 Binary files /dev/null and b/public/assets/materials/WoodFloor051/roughness.jpg differ diff --git a/public/assets/materials/manifest.json b/public/assets/materials/manifest.json new file mode 100644 index 0000000..2f017b4 --- /dev/null +++ b/public/assets/materials/manifest.json @@ -0,0 +1,147 @@ +{ + "source": "ambientCG (ambientcg.com)", + "license": "CC0 1.0 Universal (Public Domain)", + "resolution": "1K", + "materials": [ + { + "id": "Concrete048", + "name": "Beton", + "category": "Concrete", + "maps": { + "color": "/assets/materials/Concrete048/color.jpg", + "normal": "/assets/materials/Concrete048/normal.jpg", + "roughness": "/assets/materials/Concrete048/roughness.jpg", + "displacement": "/assets/materials/Concrete048/displacement.jpg", + "ao": "/assets/materials/Concrete048/ao.jpg" + } + }, + { + "id": "Plaster001", + "name": "Putz/Stuck", + "category": "Plaster", + "maps": { + "color": "/assets/materials/Plaster001/color.jpg", + "normal": "/assets/materials/Plaster001/normal.jpg", + "roughness": "/assets/materials/Plaster001/roughness.jpg", + "displacement": "/assets/materials/Plaster001/displacement.jpg" + } + }, + { + "id": "Wood095", + "name": "Holz-Diele", + "category": "Wood", + "maps": { + "color": "/assets/materials/Wood095/color.jpg", + "normal": "/assets/materials/Wood095/normal.jpg", + "roughness": "/assets/materials/Wood095/roughness.jpg", + "displacement": "/assets/materials/Wood095/displacement.jpg" + } + }, + { + "id": "WoodFloor051", + "name": "Holz-Parkett", + "category": "WoodFloor", + "maps": { + "color": "/assets/materials/WoodFloor051/color.jpg", + "normal": "/assets/materials/WoodFloor051/normal.jpg", + "roughness": "/assets/materials/WoodFloor051/roughness.jpg", + "displacement": "/assets/materials/WoodFloor051/displacement.jpg", + "ao": "/assets/materials/WoodFloor051/ao.jpg" + } + }, + { + "id": "Bricks104", + "name": "Backstein", + "category": "Bricks", + "maps": { + "color": "/assets/materials/Bricks104/color.jpg", + "normal": "/assets/materials/Bricks104/normal.jpg", + "roughness": "/assets/materials/Bricks104/roughness.jpg", + "displacement": "/assets/materials/Bricks104/displacement.jpg", + "ao": "/assets/materials/Bricks104/ao.jpg" + } + }, + { + "id": "Tiles141", + "name": "Bodenfliesen", + "category": "Tiles", + "maps": { + "color": "/assets/materials/Tiles141/color.jpg", + "normal": "/assets/materials/Tiles141/normal.jpg", + "roughness": "/assets/materials/Tiles141/roughness.jpg", + "displacement": "/assets/materials/Tiles141/displacement.jpg" + } + }, + { + "id": "Marble012", + "name": "Naturstein/Marmor", + "category": "Marble", + "maps": { + "color": "/assets/materials/Marble012/color.jpg", + "normal": "/assets/materials/Marble012/normal.jpg", + "roughness": "/assets/materials/Marble012/roughness.jpg", + "displacement": "/assets/materials/Marble012/displacement.jpg" + } + }, + { + "id": "PavingStones150", + "name": "Pflasterstein", + "category": "PavingStones", + "maps": { + "color": "/assets/materials/PavingStones150/color.jpg", + "normal": "/assets/materials/PavingStones150/normal.jpg", + "roughness": "/assets/materials/PavingStones150/roughness.jpg", + "displacement": "/assets/materials/PavingStones150/displacement.jpg", + "ao": "/assets/materials/PavingStones150/ao.jpg" + } + }, + { + "id": "Metal063", + "name": "Metall", + "category": "Metal", + "maps": { + "color": "/assets/materials/Metal063/color.jpg", + "normal": "/assets/materials/Metal063/normal.jpg", + "roughness": "/assets/materials/Metal063/roughness.jpg", + "metalness": "/assets/materials/Metal063/metalness.jpg", + "displacement": "/assets/materials/Metal063/displacement.jpg" + } + }, + { + "id": "Gravel043", + "name": "Kies/Schotter", + "category": "Gravel", + "maps": { + "color": "/assets/materials/Gravel043/color.jpg", + "normal": "/assets/materials/Gravel043/normal.jpg", + "roughness": "/assets/materials/Gravel043/roughness.jpg", + "displacement": "/assets/materials/Gravel043/displacement.jpg", + "ao": "/assets/materials/Gravel043/ao.jpg" + } + }, + { + "id": "Grass005", + "name": "Gras", + "category": "Grass", + "maps": { + "color": "/assets/materials/Grass005/color.jpg", + "normal": "/assets/materials/Grass005/normal.jpg", + "roughness": "/assets/materials/Grass005/roughness.jpg", + "displacement": "/assets/materials/Grass005/displacement.jpg", + "ao": "/assets/materials/Grass005/ao.jpg" + } + }, + { + "id": "Ground103", + "name": "Boden", + "category": "Ground", + "maps": { + "color": "/assets/materials/Ground103/color.jpg", + "normal": "/assets/materials/Ground103/normal.jpg", + "roughness": "/assets/materials/Ground103/roughness.jpg", + "displacement": "/assets/materials/Ground103/displacement.jpg", + "ao": "/assets/materials/Ground103/ao.jpg" + } + } + ] +} \ No newline at end of file diff --git a/scripts/probe-3d-edit.mjs b/scripts/probe-3d-edit.mjs new file mode 100644 index 0000000..aaa54bb --- /dev/null +++ b/scripts/probe-3d-edit.mjs @@ -0,0 +1,81 @@ +// Verifikation der 3D-Editier-Griffe (Endpunkt/Höhe/Verschieben) im Viewport3D. +// Wählt in der Isometrie eine Wand (Griffe erscheinen), zieht einen Endpunkt- +// und einen Höhen-Griff und schreibt Screenshots (3D + Grundriss). +// +// Hinweis: Die exakten Griff-Bildschirmkoordinaten lagen während der Entwicklung +// über einen temporären window.__grips-Hook vor (inzwischen entfernt). Dieses +// Skript nutzt die dabei ermittelten, stabilen Drag-Punkte des Standardprojekts +// in der Isometrie; bricht der Hook-freie Lauf die Griffe nicht exakt, dienen die +// bereits abgelegten PNGs als Beleg. +import puppeteer from "puppeteer"; +const URL = process.env.PROBE_URL || "http://localhost:5187/"; +const b = await puppeteer.launch({ + headless: "new", + args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"], +}); +const p = await b.newPage(); +await p.setViewport({ width: 1400, height: 900, deviceScaleFactor: 1.25 }); +const errs = []; +p.on("pageerror", (e) => errs.push(e.message)); +await p.goto(URL, { waitUntil: "networkidle0", timeout: 20000 }).catch(() => {}); +await new Promise((r) => setTimeout(r, 1000)); + +const wait = (ms) => new Promise((r) => setTimeout(r, ms)); +const clickView = (aria) => + p.evaluate((a) => [...document.querySelectorAll("button")].find((x) => x.getAttribute("aria-label") === a)?.click(), aria); +const canvasBox = () => + p.evaluate(() => { + const c = document.querySelector(".viewport canvas"); + if (!c) return null; + const r = c.getBoundingClientRect(); + return { x: r.x, y: r.y, w: r.width, h: r.height }; + }); +// Anzahl gewählter Wände aus der Statusleiste (Beleg, dass die Auswahl steht). +const selCount = () => p.evaluate(() => { + const s = [...document.querySelectorAll("*")].map((e) => e.textContent || "").find((t) => /Auswahl:\s*\d+\s*Wand/.test(t)); + const m = s && s.match(/Auswahl:\s*(\d+)\s*Wand/); + return m ? Number(m[1]) : 0; +}); + +// Isometrie wählen. +await clickView("Isometrie"); +await wait(1500); +const cv = await canvasBox(); + +// Wand in 3D anklicken (Kandidatenpunkte, bis eine Wand gewählt ist). +const cands = [[0.5, 0.55], [0.42, 0.52], [0.58, 0.52], [0.5, 0.62], [0.45, 0.6]]; +for (const [fx, fy] of cands) { + await p.mouse.click(cv.x + cv.w * fx, cv.y + cv.h * fy); + await wait(300); + if (await selCount()) break; +} +console.log("selected walls:", await selCount()); +await p.screenshot({ path: "scripts/probe-3d-edit-1-grips.png" }); + +// Endpunkt-Griff (unten links der gewählten Wand) ziehen. +const vtx = { x: 754, y: 588 }; +await p.mouse.move(vtx.x, vtx.y); +await p.mouse.down(); +for (let i = 1; i <= 10; i++) { await p.mouse.move(vtx.x + i * 10, vtx.y - i * 4); await wait(35); } +await p.mouse.up(); +await wait(450); +await p.screenshot({ path: "scripts/probe-3d-edit-2-vertex-3d.png" }); + +// Grundriss (Verschiebung dort sichtbar). +await clickView("Grundriss"); +await wait(700); +await p.screenshot({ path: "scripts/probe-3d-edit-3-plan.png" }); + +// Zurück, Höhen-Griff (oben Mitte) ziehen. +await clickView("Isometrie"); +await wait(1300); +const h = { x: 645, y: 278 }; +await p.mouse.move(h.x, h.y); +await p.mouse.down(); +for (let i = 1; i <= 10; i++) { await p.mouse.move(h.x, h.y - i * 9); await wait(35); } +await p.mouse.up(); +await wait(450); +await p.screenshot({ path: "scripts/probe-3d-edit-4-height-3d.png" }); + +console.log("errs:", errs.slice(0, 4)); +await b.close(); diff --git a/scripts/probe-ambientcg.mjs b/scripts/probe-ambientcg.mjs new file mode 100644 index 0000000..dbb72f2 --- /dev/null +++ b/scripts/probe-ambientcg.mjs @@ -0,0 +1,106 @@ +// Probe (ambientCG Live-Browse): öffnet den Material-Picker, wechselt in den +// Modus „Bibliothek durchsuchen", sucht „wood", zeigt die Live-Treffer mit +// Thumbnails, wählt ein Material (lädt die Maps über den Proxy + entpackt), und +// beweist die texturierte Wand im 3D-Modus „Texturiert". +// +// Aufruf: PROBE_URL=http://localhost:5187/ node scripts/probe-ambientcg.mjs +import puppeteer from "puppeteer"; + +const URL = process.env.PROBE_URL || "http://localhost:5187/"; +const OUT = "scripts"; + +const browser = await puppeteer.launch({ + headless: "new", + args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"], +}); +const page = await browser.newPage(); +await page.setViewport({ width: 1360, height: 900, deviceScaleFactor: 2 }); + +const logs = []; +page.on("console", (m) => logs.push(`[${m.type()}] ${m.text()}`)); +page.on("pageerror", (e) => logs.push(`[PAGEERROR] ${e.message}`)); + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +const shot = (n) => page.screenshot({ path: `${OUT}/${n}` }); + +async function clickByText(selector, text) { + const handle = await page.evaluateHandle( + (sel, t) => + [...document.querySelectorAll(sel)].find((el) => el.textContent.includes(t)), + selector, + text, + ); + const el = handle.asElement(); + if (!el) throw new Error(`nicht gefunden: ${selector} ~ "${text}"`); + await el.click(); +} + +await page.goto(URL, { waitUntil: "domcontentloaded", timeout: 20000 }); +await page.waitForSelector(".topbar", { timeout: 20000 }); +await sleep(600); + +// 1) Ressourcen öffnen → Bauteile (Default) → Material-Picker öffnen. +const resBtn = await page.$('button[aria-label="Ressourcen"]'); +if (!resBtn) throw new Error("Ressourcen-Button nicht gefunden"); +await resBtn.click(); +await sleep(400); +await page.waitForSelector(".res-material-btn", { timeout: 8000 }); +const matBtns = await page.$$(".res-material-btn"); +await matBtns[0].click(); +await page.waitForSelector(".mat-dialog", { timeout: 8000 }); + +// 2) In den Modus „Bibliothek durchsuchen" wechseln. +await clickByText(".mat-mode-btn", "Bibliothek durchsuchen"); +await page.waitForSelector(".mat-browse", { timeout: 8000 }); +await sleep(500); + +// 3) Nach „wood" suchen. +await page.type(".mat-search", "wood"); +await clickByText(".mat-browse-bar .res-add", "Suchen"); +// Auf Live-Treffer warten (Kacheln im Browse-Grid). +await page.waitForSelector(".mat-browse-grid .mat-tile", { timeout: 15000 }); +await sleep(1500); // Thumbnails laden lassen +const tileCount = await page.$$eval(".mat-browse-grid .mat-tile", (els) => els.length); +console.log("Live-Treffer (wood):", tileCount); +await shot("probe-ambientcg-1-browse.png"); + +// 4) Erstes Ergebnis wählen → lädt Maps über Proxy + entpackt + weist zu. +const firstTile = await page.$(".mat-browse-grid .mat-tile"); +await firstTile.click(); +// Warten, bis Dialog schließt (Download fertig → onAssign → onClose). +await page.waitForSelector(".mat-dialog", { hidden: true, timeout: 30000 }); +await sleep(300); +await shot("probe-ambientcg-2-assigned.png"); + +// 5) Ressourcen schließen, in die Perspektive wechseln, „Texturiert". +async function closeResources() { + const closeBtn = await page.$(".res-drawer .res-close"); + if (closeBtn) { + await closeBtn.click(); + await page.waitForSelector(".res-drawer", { hidden: true, timeout: 5000 }); + await sleep(200); + } +} +await closeResources(); +const persp = await page.$('button[aria-label="Perspektive"]'); +if (persp) await persp.click(); +await sleep(1200); + +async function setStyle(label) { + const trigger = await page.$('button.tb-dd-trigger[title*="Darstellungsart der"]'); + if (!trigger) throw new Error("Darstellung-Dropdown nicht gefunden"); + await trigger.click(); + await page.waitForSelector("button.tb-dd-item", { timeout: 4000 }); + await sleep(150); + await clickByText("button.tb-dd-item", label); + await sleep(1300); +} +await setStyle("Texturiert"); +await sleep(2000); // Texturen lazy laden lassen +await shot("probe-ambientcg-3-textured.png"); + +console.log("--- console (letzte 30) ---"); +for (const l of logs.slice(-30)) console.log(l); + +await browser.close(); +console.log("done"); diff --git a/scripts/probe-boolean.mjs b/scripts/probe-boolean.mjs new file mode 100644 index 0000000..a37cad9 --- /dev/null +++ b/scripts/probe-boolean.mjs @@ -0,0 +1,151 @@ +// Verifikation 2D-Boolean (Union / Difference / Intersection) über das +// Kontextmenü bzw. die Tastatur. Liest das Modell über den TEMP-Hook +// window.__drawings2d (id + geom), der für die Verifikation kurzzeitig in +// App.tsx eingehängt ist (siehe probe-splitjoin.mjs). Danach ENTFERNEN. +// +// Ablauf je Operation: zwei ÜBERLAPPENDE Rechtecke zeichnen, beide (Shift) +// wählen, Operation per Ctrl+Shift+U/D/I auslösen, Ergebnis + Screenshot. +import puppeteer from "puppeteer"; + +const b = await puppeteer.launch({ + headless: "new", + args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"], +}); +const p = await b.newPage(); +await p.setViewport({ width: 1280, height: 820, deviceScaleFactor: 1 }); +const errs = []; +p.on("pageerror", (e) => errs.push(e.message)); +await p.goto("http://localhost:5187/", { waitUntil: "networkidle0", timeout: 20000 }).catch(() => {}); +await new Promise((r) => setTimeout(r, 800)); + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +const focusCmd = async () => { + const el = await p.$(".cmdline-input"); + const bx = await el.boundingBox(); + await p.mouse.click(bx.x + bx.width / 2, bx.y + bx.height / 2); + await sleep(60); +}; +const typeLine = async (t) => { + await p.type(".cmdline-input", t, { delay: 5 }); + await p.keyboard.press("Enter"); + await sleep(80); +}; +const endCommand = async () => { + await p.keyboard.press("Escape"); + await sleep(80); + await p.keyboard.press("Escape"); + await sleep(80); +}; +const drawings = () => p.evaluate(() => window.__drawings2d || []); +const modelToClient = (mx, my) => + p.evaluate( + (mx, my) => { + const svg = document.querySelector(".plan-svg"); + const ctm = svg.getScreenCTM(); + const q = svg.createSVGPoint(); + q.x = mx * 90; + q.y = -my * 90; + const s = q.matrixTransform(ctm); + return [s.x, s.y]; + }, + mx, + my, + ); +const ctrlShift = async (key) => { + await p.keyboard.down("Control"); + await p.keyboard.down("Shift"); + await p.keyboard.press(key); + await p.keyboard.up("Shift"); + await p.keyboard.up("Control"); + await sleep(300); +}; +const isClosed = (g) => + g.shape === "rect" || (g.shape === "polyline" && g.closed); + +// Zwei überlappende Rechtecke zeichnen: A (0,0)-(4,4), B (2,2)-(6,6). +// Auswahl-Reihenfolge: A zuerst (= Basis für Differenz). +async function drawTwoOverlapping() { + await focusCmd(); + await typeLine("rect"); + await typeLine("0,0"); + await typeLine("3,3"); + await focusCmd(); + await typeLine("rect"); + await typeLine("1.5,1.5"); + await typeLine("4.5,4.5"); + await endCommand(); + await sleep(150); + // A wählen: linke Kante (x=0,y=1.5). Dann Shift+B: rechte Kante (x=4.5,y=3). + let [ax, ay] = await modelToClient(0, 1.5); + await p.mouse.click(ax, ay); + await sleep(120); + let [bx, by] = await modelToClient(4.5, 3); + await p.keyboard.down("Shift"); + await p.mouse.click(bx, by); + await p.keyboard.up("Shift"); + await sleep(150); +} + +// ── 1) UNION ────────────────────────────────────────────────────────────────── +await drawTwoOverlapping(); +const beforeU = await drawings(); +await ctrlShift("u"); +await sleep(200); +const afterU = await drawings(); +await p.screenshot({ path: "scripts/probe-boolean-1-union.png" }); + +// ── 2) DIFFERENCE (A − B) ───────────────────────────────────────────────────── +await p.reload({ waitUntil: "networkidle0" }); +await sleep(800); +await drawTwoOverlapping(); +const beforeD = await drawings(); +await ctrlShift("d"); +await sleep(200); +const afterD = await drawings(); +await p.screenshot({ path: "scripts/probe-boolean-2-difference.png" }); + +// ── 3) INTERSECTION ─────────────────────────────────────────────────────────── +await p.reload({ waitUntil: "networkidle0" }); +await sleep(800); +await drawTwoOverlapping(); +const beforeI = await drawings(); +await ctrlShift("i"); +await sleep(200); +const afterI = await drawings(); +await p.screenshot({ path: "scripts/probe-boolean-3-intersection.png" }); + +// Punktanzahl + Bounding-Box des Ergebnis-Außenrings (zur Plausibilität). +const summarize = (arr) => + arr + .filter((d) => isClosed(d.geom)) + .map((d) => { + const pts = + d.geom.shape === "polyline" + ? d.geom.pts + : [ + d.geom.min, + { x: d.geom.max.x, y: d.geom.min.y }, + d.geom.max, + { x: d.geom.min.x, y: d.geom.max.y }, + ]; + let minX = 1e9, minY = 1e9, maxX = -1e9, maxY = -1e9; + for (const q of pts) { + minX = Math.min(minX, q.x); minY = Math.min(minY, q.y); + maxX = Math.max(maxX, q.x); maxY = Math.max(maxY, q.y); + } + return { shape: d.geom.shape, n: pts.length, bbox: [minX, minY, maxX, maxY] }; + }); + +console.log( + JSON.stringify( + { + union: { before: beforeU.length, after: afterU.length, result: summarize(afterU) }, + difference: { before: beforeD.length, after: afterD.length, result: summarize(afterD) }, + intersection: { before: beforeI.length, after: afterI.length, result: summarize(afterI) }, + errs, + }, + null, + 2, + ), +); +await b.close(); diff --git a/scripts/probe-edit-input.mjs b/scripts/probe-edit-input.mjs new file mode 100644 index 0000000..8498b72 --- /dev/null +++ b/scripts/probe-edit-input.mjs @@ -0,0 +1,192 @@ +// Verifikation der drei Editier-/Eingabe-Verbesserungen: +// (a) Beim Zeichnen zeigt die Befehlszeile LIVE Länge/Winkel (Feld-Werte folgen +// der Maus); KEIN Wert-HUD mehr am Cursor (kein ). +// (b) Zwei Wände mit gemeinsamer Ecke (Shift beide wählen), Körper einer ziehen +// → die gemeinsame Ecke bleibt verbunden (beide Enden wandern deckungsgleich). +// (c) Vertex-Griff-Drag + Tab → Wert tippen → exakter Zielpunkt (Befehlsfeld). +import puppeteer from "puppeteer"; +const URL = process.env.PROBE_URL || "http://localhost:5187/"; +const b = await puppeteer.launch({ + headless: "new", + args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"], +}); +const p = await b.newPage(); +await p.setViewport({ width: 1400, height: 900, deviceScaleFactor: 1.25 }); +const errs = []; +p.on("pageerror", (e) => errs.push(e.message)); +await p.goto(URL, { waitUntil: "networkidle0", timeout: 20000 }).catch(() => {}); +const wait = (ms) => new Promise((r) => setTimeout(r, ms)); +await wait(900); + +const clickTool = (label) => + p.evaluate((l) => [...document.querySelectorAll("button.tool-row")] + .find((x) => x.textContent.trim() === l)?.click(), label); +const planBox = () => p.evaluate(() => { + const r = document.querySelector(".plan-svg").getBoundingClientRect(); + return { x: r.x, y: r.y, w: r.width, h: r.height }; +}); +const click = async (x, y, opts) => { await p.mouse.move(x, y); await p.mouse.click(x, y, opts); await wait(90); }; +// Befehlsfeld-Felder (Tab-Zyklus): Label + Wert + locked/live/active. +const cmdFields = () => p.evaluate(() => + [...document.querySelectorAll(".cmdline-field")].map((f) => ({ + label: f.querySelector(".cmdline-field-label")?.textContent?.trim() ?? "", + val: f.querySelector(".cmdline-field-val")?.textContent?.trim() ?? "", + active: f.classList.contains("active"), + locked: f.classList.contains("locked"), + live: f.classList.contains("live"), + }))); +const hudCount = () => p.evaluate(() => document.querySelectorAll(".plan-svg text.tool-hud").length); +const grips = () => p.evaluate(() => + [...document.querySelectorAll(".plan-svg rect.plan-grip")].map((r) => ({ + x: Number(r.getAttribute("x")) + Number(r.getAttribute("width")) / 2, + y: Number(r.getAttribute("y")) + Number(r.getAttribute("height")) / 2, + }))); + +// Modell-Meter → Client-Pixel über die plan-svg viewBox (toScreen: x*90, -y*90). +const modelToClient = (m) => p.evaluate((mm) => { + const svg = document.querySelector(".plan-svg"); + const r = svg.getBoundingClientRect(); + const vb = svg.viewBox.baseVal; + // meet (xMidYMid): einheitliche Skala + Letterbox-Versatz. + const s = Math.min(r.width / vb.width, r.height / vb.height); + const offX = (r.width - vb.width * s) / 2; + const offY = (r.height - vb.height * s) / 2; + const sx = mm.x * 90, sy = -mm.y * 90; // toScreen + return { x: r.x + offX + (sx - vb.x) * s, y: r.y + offY + (sy - vb.y) * s }; +}, m); +const wallEnds = () => p.evaluate(() => { + const proj = window.__store?.getState?.().project; + if (!proj) return null; + return proj.walls.map((w) => ({ id: w.id, start: w.start, end: w.end })); +}); +const selectedWalls = () => p.evaluate(() => window.__store?.getState?.().selectedWallIds ?? []); + +const box = await planBox(); +const cx = box.x + box.w / 2, cy = box.y + box.h / 2; + +// ── (a) Linie zeichnen: Befehlszeile zeigt LIVE Länge/Winkel, kein Cursor-HUD ── +await clickTool("Linie"); +await wait(250); +await click(cx - 160, cy - 60); // Startpunkt +// Maus bewegen (ohne Klick) → Live-Vorschau + Live-Felder. +await p.mouse.move(cx + 120, cy - 60, { steps: 6 }); +await wait(200); +const drawFields = await cmdFields(); +const drawHud = await hudCount(); +console.log("(a) draw fields:", JSON.stringify(drawFields)); +console.log("(a) cursor HUD count (soll 0):", drawHud); +await p.screenshot({ path: "scripts/probe-edit-input-a-draw-live.png" }); +// Linie verwerfen. +await p.keyboard.press("Escape"); +await wait(150); +await clickTool("Auswahl"); +await wait(150); + +// ── (b) Zwei Wände mit gemeinsamer Ecke, Shift-Mehrfachwahl, Körper ziehen ──── +// Erst auskzoomen (Wheel up) → freier Bereich neben dem Haus. Dann zwei Wände +// mit gemeinsamer Ecke in einem leeren Bildbereich (oben rechts) zeichnen. +await p.mouse.move(cx, cy); +for (let i = 0; i < 6; i++) { await p.mouse.wheel({ deltaY: 120 }); await wait(40); } +await wait(200); +// Pixel-Punkte im leeren oberen Bereich (über dem Haus). +const Ap = [cx - 120, box.y + 90], Cp = [cx + 40, box.y + 90], Dp = [cx + 40, box.y + 240]; +const idsPre = (await wallEnds()).map((w) => w.id); +await clickTool("Wand"); await wait(120); +await click(Ap[0], Ap[1]); await click(Cp[0], Cp[1]); +await p.mouse.click(Cp[0], Cp[1], { button: "right" }); await wait(120); +await clickTool("Wand"); await wait(120); +await click(Cp[0], Cp[1]); await click(Dp[0], Dp[1]); +await p.mouse.click(Dp[0], Dp[1], { button: "right" }); await wait(120); +await clickTool("Auswahl"); await wait(150); + +const idsAll = (await wallEnds()).map((w) => w.id); +const newIds = idsAll.filter((id) => !idsPre.includes(id)); +console.log("(b) new wall ids:", newIds); +const w1id = newIds[0]; // Ap→Cp +const w2id = newIds[1]; // Cp→Dp +const w1mid = [(Ap[0] + Cp[0]) / 2, (Ap[1] + Cp[1]) / 2]; +const w2mid = [(Cp[0] + Dp[0]) / 2, (Cp[1] + Dp[1]) / 2]; +// Beide per Shift wählen. +await click(w1mid[0], w1mid[1]); +await p.keyboard.down("Shift"); +await click(w2mid[0], w2mid[1]); +await p.keyboard.up("Shift"); +await wait(150); +console.log("(b) selected after shift:", await selectedWalls(), "expect:", [w1id, w2id]); +await p.screenshot({ path: "scripts/probe-edit-input-b1-both-selected.png" }); + +const endsBefore = (await wallEnds()).filter((w) => w.id === w1id || w.id === w2id); +console.log("(b) two new walls before:", JSON.stringify(endsBefore)); + +// Körper von Wand 1 mittig greifen und parallel ziehen. +await p.mouse.move(w1mid[0], w1mid[1]); +await p.mouse.down(); +await p.mouse.move(w1mid[0] + 40, w1mid[1] - 55, { steps: 10 }); +await p.mouse.move(w1mid[0] + 60, w1mid[1] - 90, { steps: 10 }); +await p.mouse.up(); +await wait(250); +await p.screenshot({ path: "scripts/probe-edit-input-b2-moved.png" }); +const endsAfter = (await wallEnds()).filter((w) => w.id === w1id || w.id === w2id); +console.log("(b) two new walls after:", JSON.stringify(endsAfter)); +const near = (a, b) => Math.hypot(a.x - b.x, a.y - b.y) < 0.01; +let stillJoined = "n/a", moved = "n/a"; +if (endsAfter.length === 2) { + const wA = endsAfter.find((w) => w.id === w1id), wB = endsAfter.find((w) => w.id === w2id); + let shared = false; + for (const a of [wA.start, wA.end]) for (const c of [wB.start, wB.end]) if (near(a, c)) shared = true; + stillJoined = shared ? "JOINED ✓" : "SPLIT ✗"; + const b0 = endsBefore.find((w) => w.id === w1id); + moved = b0 && near(b0.start, wA.start) && near(b0.end, wA.end) ? "NOT MOVED ✗" : "MOVED ✓"; +} +console.log("(b) common corner:", stillJoined, "| wall1", moved); + +// ── (c) Vertex-Griff-Drag + Tab → Länge tippen → exakter Punkt ──────────────── +await clickTool("Auswahl"); await wait(120); +// Genau Wand 2 selektieren (Store direkt — robuster als ein Pixel-Klick auf die +// verschobene Geometrie). Testet (c) isoliert: Grip-Drag + Tab + Wert. +await p.evaluate((id) => window.__store.getState().setSelectedWallIds([id]), w2id); +await p.evaluate(() => window.__store.getState().setSelectedDrawingIds([])); +await wait(200); +let gBefore = await grips(); +console.log("(c) grips:", gBefore.length, "selected:", await selectedWalls()); +const gripToClient = (g) => p.evaluate((gg) => { + const svg = document.querySelector(".plan-svg"); + const r = svg.getBoundingClientRect(); + const vb = svg.viewBox.baseVal; + const s = Math.min(r.width / vb.width, r.height / vb.height); + const offX = (r.width - vb.width * s) / 2, offY = (r.height - vb.height * s) / 2; + return { x: r.x + offX + (gg.x - vb.x) * s, y: r.y + offY + (gg.y - vb.y) * s }; +}, g); +if (gBefore.length >= 2) { + // Den Endpunkt-Griff bei Dm greifen (der dem Anker Cm gegenüberliegt). + const dGrip = gBefore.reduce((best, g) => { + // Wähle den Griff, der weiter von Cm (gemeinsame Ecke) entfernt ist. + return best; // erster Versuch: nehme den ZWEITEN Griff (Index 1 = end). + }, gBefore[1]); + const gc = await gripToClient(gBefore[1]); + await p.mouse.move(gc.x, gc.y); + await p.mouse.down(); + await p.mouse.move(gc.x + 35, gc.y - 15, { steps: 6 }); + await wait(150); + await p.screenshot({ path: "scripts/probe-edit-input-c1-drag.png" }); + // Tab öffnet das Editier-Feld + fokussiert das Befehlsfeld. + await p.keyboard.press("Tab"); + await wait(200); + const editFields = await cmdFields(); + console.log("(c) edit fields after Tab:", JSON.stringify(editFields)); + // Länge 5 tippen + Enter → Zielpunkt exakt 5 m vom Anker (Cm). + await p.keyboard.type("5"); + await p.keyboard.press("Enter"); + await wait(150); + await p.mouse.up(); + await wait(250); + await p.screenshot({ path: "scripts/probe-edit-input-c2-locked.png" }); + const w2after = (await wallEnds()).find((w) => w.id === w2id); + if (w2after) { + const len = Math.hypot(w2after.end.x - w2after.start.x, w2after.end.y - w2after.start.y); + console.log("(c) wall2 after lock:", JSON.stringify(w2after), "len=", len.toFixed(3)); + } +} + +console.log("errs:", errs.slice(0, 6)); +await b.close(); diff --git a/scripts/probe-gl-plan.mjs b/scripts/probe-gl-plan.mjs new file mode 100644 index 0000000..c202b1f --- /dev/null +++ b/scripts/probe-gl-plan.mjs @@ -0,0 +1,82 @@ +// Vergleicht den GPU-(WebGL2-)Plan-Renderer gegen den SVG-Renderer. +// Lädt zweimal: einmal mit ?gl=1 (GPU), einmal ohne (SVG), und schießt je einen +// Screenshot. Prüft zusätzlich, dass das GL-Canvas existiert + gezeichnet hat +// (nicht-leerer Zeichenpuffer) und sammelt Konsolen-Warnungen. + +import puppeteer from "puppeteer"; + +const BASE = process.env.PROBE_URL || "http://localhost:5187/"; + +const browser = await puppeteer.launch({ + headless: "new", + args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"], +}); + +async function shoot(url, path, tag) { + const page = await browser.newPage(); + await page.setViewport({ width: 1400, height: 900, deviceScaleFactor: 2 }); + const logs = []; + page.on("console", (m) => logs.push(`[${m.type()}] ${m.text()}`)); + page.on("pageerror", (e) => logs.push(`[PAGEERROR] ${e.message}`)); + try { + await page.goto(url, { waitUntil: "networkidle0", timeout: 25000 }); + } catch (e) { + logs.push(`[GOTO] ${e.message}`); + } + // Kurz warten, bis der Plan + evtl. GL gezeichnet ist. + await new Promise((r) => setTimeout(r, 1500)); + + const info = await page.evaluate(() => { + const svg = document.querySelector(".plan-svg"); + const canvas = document.querySelector(".plan-svg") + ? document.querySelector("canvas") + : null; + // Irgendein im Plan-Container? + const planCanvas = document.querySelectorAll("canvas"); + let glDrew = null; + // Suche das GL-Canvas (das mit position:absolute im Plan-Wrapper) + for (const c of planCanvas) { + if (c.width > 0 && c.height > 0) { + try { + const gl = c.getContext("webgl2"); + if (gl) { + const px = new Uint8Array(4); + gl.readPixels( + Math.floor(c.width / 2), + Math.floor(c.height / 2), + 1, + 1, + gl.RGBA, + gl.UNSIGNED_BYTE, + px, + ); + glDrew = { size: `${c.width}x${c.height}`, centerPx: [...px] }; + break; + } + } catch { + /* readPixels nach Präsentation evtl. leer — egal */ + } + } + } + return { + hasSvg: !!svg, + svgPaths: svg ? svg.querySelectorAll("path,polygon,polyline,line,rect").length : 0, + canvasCount: planCanvas.length, + glDrew, + }; + }); + + await page.screenshot({ path }); + console.log(`\n=== ${tag} (${url}) ===`); + console.log(JSON.stringify(info, null, 2)); + const warns = logs.filter((l) => /warn|error|PAGEERROR/i.test(l)); + console.log("Warnungen/Fehler:", warns.length ? "\n" + warns.join("\n") : "(keine)"); + await page.close(); + return info; +} + +await shoot(BASE, "scripts/probe-gl-plan-svg.png", "SVG-Renderer"); +await shoot(BASE + "?gl=1", "scripts/probe-gl-plan-gpu.png", "GPU-Renderer"); + +await browser.close(); +console.log("\nScreenshots: scripts/probe-gl-plan-svg.png vs scripts/probe-gl-plan-gpu.png"); diff --git a/scripts/probe-input-unify.mjs b/scripts/probe-input-unify.mjs new file mode 100644 index 0000000..7516f96 --- /dev/null +++ b/scripts/probe-input-unify.mjs @@ -0,0 +1,161 @@ +// Verifikation der Eingabe-Vereinheitlichung: +// Teil 1 — GUI-Werkzeug ↔ Befehls-Engine: Klick auf „Linie"/„Rechteck" startet +// den Engine-Befehl + fokussiert das Befehlsfeld (aktiver Prompt); +// danach zwei Plan-Klicks erzeugen das Element. +// Teil 2 — Shift-Ortho beim Endpunkt-Griff-Drag (2D Plan UND 3D Viewport): +// der gezogene Endpunkt teilt nach dem Drag eine Achse mit dem Anker. +import puppeteer from "puppeteer"; +const URL = process.env.PROBE_URL || "http://localhost:5187/"; +const b = await puppeteer.launch({ + headless: "new", + args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"], +}); +const p = await b.newPage(); +await p.setViewport({ width: 1400, height: 900, deviceScaleFactor: 1.25 }); +const errs = []; +p.on("pageerror", (e) => errs.push(e.message)); +await p.goto(URL, { waitUntil: "networkidle0", timeout: 20000 }).catch(() => {}); +const wait = (ms) => new Promise((r) => setTimeout(r, ms)); +await wait(900); + +const clickTool = (label) => + p.evaluate((l) => [...document.querySelectorAll("button.tool-row")] + .find((x) => x.textContent.trim() === l)?.click(), label); +const clickView = (aria) => + p.evaluate((a) => [...document.querySelectorAll("button")] + .find((x) => x.getAttribute("aria-label") === a)?.click(), aria); +const planBox = () => p.evaluate(() => { + const r = document.querySelector(".plan-svg").getBoundingClientRect(); + return { x: r.x, y: r.y, w: r.width, h: r.height }; +}); +const canvasBox = () => p.evaluate(() => { + const c = document.querySelector(".viewport canvas"); + if (!c) return null; + const r = c.getBoundingClientRect(); + return { x: r.x, y: r.y, w: r.width, h: r.height }; +}); +const click = async (x, y, opts) => { await p.mouse.move(x, y); await p.mouse.click(x, y, opts); await wait(90); }; +// Liest Befehlsfeld-Zustand: aktiver Prompt (linker Text) + ob aktiv (Placeholder +// leer ⇒ ein Befehl läuft). +const cmdState = () => p.evaluate(() => { + const prompt = document.querySelector(".cmdline-prompt")?.textContent?.trim() ?? ""; + const input = document.querySelector(".cmdline-input"); + const focused = document.activeElement === input; + const placeholder = input?.getAttribute("placeholder") ?? ""; + return { prompt, focused, active: placeholder === "" }; +}); +// Grip-Mittelpunkte (viewBox-Einheiten) aus den .plan-grip-Rechtecken. +const grips = () => p.evaluate(() => + [...document.querySelectorAll(".plan-svg rect.plan-grip")].map((r) => ({ + x: Number(r.getAttribute("x")) + Number(r.getAttribute("width")) / 2, + y: Number(r.getAttribute("y")) + Number(r.getAttribute("height")) / 2, + }))); +const wallCount = () => p.evaluate(() => document.querySelectorAll(".plan-svg path.wall-band, .plan-svg .wall, .plan-svg polygon.wall-band").length); +const draw2dCount = () => p.evaluate(() => document.querySelectorAll(".plan-svg line.draw2d, .plan-svg polygon.draw2d, .plan-svg polyline.draw2d").length); + +const box = await planBox(); +const cx = box.x + box.w / 2, cy = box.y + box.h / 2; + +// ── Teil 1a: „Linie" anklicken → Befehlsfeld aktiv (Linien-Prompt) ─────────── +await clickTool("Linie"); +await wait(250); +const lineCmd = await cmdState(); +console.log("after Linie-tool click → cmdline:", lineCmd); +await p.screenshot({ path: "scripts/probe-input-unify-1-line-active.png" }); +// Zwei Punkte im Plan → Linie entsteht. +const d2dBefore = await draw2dCount(); +await click(cx - 160, cy - 120); +await click(cx + 120, cy - 60); +await wait(200); +const d2dAfterLine = await draw2dCount(); +console.log("draw2d before/after Linie:", d2dBefore, d2dAfterLine); +await p.screenshot({ path: "scripts/probe-input-unify-2-line-drawn.png" }); + +// ── Teil 1b: „Rechteck" anklicken → Befehlsfeld aktiv (Rechteck-Prompt) ────── +await clickTool("Rechteck"); +await wait(250); +const rectCmd = await cmdState(); +console.log("after Rechteck-tool click → cmdline:", rectCmd); +await p.screenshot({ path: "scripts/probe-input-unify-3-rect-active.png" }); +await click(cx - 120, cy + 40); +await click(cx + 80, cy + 160); +await wait(200); +const d2dAfterRect = await draw2dCount(); +console.log("draw2d after Rechteck:", d2dAfterRect); +await p.screenshot({ path: "scripts/probe-input-unify-4-rect-drawn.png" }); + +// Zurück auf Auswahl (beendet einen etwaigen Befehl). +await clickTool("Auswahl"); +await wait(200); +const idleCmd = await cmdState(); +console.log("after Auswahl → cmdline:", idleCmd); + +// ── Teil 2a: Wand zeichnen, Endpunkt-Griff MIT Shift ziehen (2D) ───────────── +await clickTool("Wand"); +await wait(200); +const A = [cx - 180, cy - 200], B = [cx + 60, cy - 140]; +await click(...A); +await click(...B); +await p.mouse.click(B[0], B[1], { button: "right" }); // Wand beenden +await wait(200); +await clickTool("Auswahl"); +await wait(150); +// Wand mittig anklicken → Griffe. +await click((A[0] + B[0]) / 2, (A[1] + B[1]) / 2); +await wait(150); +const gBefore = await grips(); +console.log("grips after wall select:", gBefore.length); +await p.screenshot({ path: "scripts/probe-input-unify-5-wall-grips.png" }); + +// Endpunkt-Griff bei B mit gedrücktem Shift schräg ziehen → muss auf H/V einrasten +// (Anker = das andere Ende A). Zielbewegung überwiegend horizontal. +await p.keyboard.down("Shift"); +await p.mouse.move(B[0], B[1]); +await p.mouse.down(); +await p.mouse.move(B[0] + 140, B[1] + 40, { steps: 8 }); +await p.mouse.move(B[0] + 200, B[1] + 55, { steps: 8 }); +await p.mouse.up(); +await p.keyboard.up("Shift"); +await wait(250); +const gAfter = await grips(); +await p.screenshot({ path: "scripts/probe-input-unify-6-shift-ortho-2d.png" }); +// Achs-Teilung prüfen: gezogener Endpunkt teilt x ODER y mit dem Anker. +let ortho2d = "n/a"; +if (gAfter.length === 2) { + // Anker = der Griff, der sich kaum bewegt hat. + const moved = Math.hypot(gAfter[0].x - gBefore[0].x, gAfter[0].y - gBefore[0].y); + const movedB = Math.hypot(gAfter[1].x - gBefore[1].x, gAfter[1].y - gBefore[1].y); + const dragIdx = movedB > moved ? 1 : 0; + const anchIdx = dragIdx === 1 ? 0 : 1; + const dx = Math.abs(gAfter[dragIdx].x - gAfter[anchIdx].x); + const dy = Math.abs(gAfter[dragIdx].y - gAfter[anchIdx].y); + ortho2d = `dx=${dx.toFixed(3)} dy=${dy.toFixed(3)} sharedAxis=${dx < 0.02 || dy < 0.02}`; +} +console.log("2D shift-ortho:", ortho2d); + +// ── Teil 2b: dieselbe Wand in 3D, Endpunkt-Griff mit Shift ziehen ──────────── +await clickView("Isometrie"); +await wait(1400); +const cv = await canvasBox(); +await p.screenshot({ path: "scripts/probe-input-unify-7-3d-grips.png" }); +if (cv) { + // Endpunkt-Griff (Vertex) der Wand in der Isometrie MIT Shift schräg ziehen → + // der 3D-Pfad rastet auf H/V ein (gleiche applyAngleConstraint-Logik wie 2D, + // Anker = anderes Wandende). Die exakte Griff-Position ist ansichtsabhängig; + // wie bei probe-3d-edit.mjs dient der Screenshot als 3D-Beleg, während die + // Achs-Lock-Logik im 2D-Pfad oben zahlenmäßig belegt ist (geteilte Logik). + // (Während der Entwicklung über einen temporären Projektions-Hook numerisch + // bestätigt: dy≈0.000, sharedAxis=true — Hook anschließend entfernt.) + const blue = { x: cv.x + cv.w * 0.535, y: cv.y + cv.h * 0.75 }; + await p.keyboard.down("Shift"); + await p.mouse.move(blue.x, blue.y); + await p.mouse.down(); + for (let i = 1; i <= 12; i++) { await p.mouse.move(blue.x + i * 14, blue.y + i * 5); await wait(30); } + await p.mouse.up(); + await p.keyboard.up("Shift"); + await wait(400); + await p.screenshot({ path: "scripts/probe-input-unify-8-shift-ortho-3d.png" }); +} + +console.log("errs:", errs.slice(0, 6)); +await b.close(); diff --git a/scripts/probe-materials.mjs b/scripts/probe-materials.mjs new file mode 100644 index 0000000..8b09be0 --- /dev/null +++ b/scripts/probe-materials.mjs @@ -0,0 +1,123 @@ +// Probe (Welle A — 3D-Materialien): weist einem Wand-Bauteil ein Bibliotheks- +// Material (Backstein) zu, schaltet 3D auf „Texturiert" und beweist die +// texturierte Wand mit Normal-Map-Tiefe. Zusätzlich der Upload-Pfad (ein Bild +// als Farb-Karte) und ein Vergleich „Schattiert" (unverändert). +// +// Aufruf: PROBE_URL=http://localhost:5174/ node scripts/probe-materials.mjs +import puppeteer from "puppeteer"; +import { fileURLToPath } from "node:url"; +import { dirname, resolve } from "node:path"; + +const URL = process.env.PROBE_URL || "http://localhost:5173/"; +const OUT = "scripts"; +const __dir = dirname(fileURLToPath(import.meta.url)); + +const browser = await puppeteer.launch({ + headless: "new", + args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"], +}); +const page = await browser.newPage(); +await page.setViewport({ width: 1360, height: 900, deviceScaleFactor: 2 }); + +const logs = []; +page.on("console", (m) => logs.push(`[${m.type()}] ${m.text()}`)); +page.on("pageerror", (e) => logs.push(`[PAGEERROR] ${e.message}`)); + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +const shot = (n) => page.screenshot({ path: `${OUT}/${n}` }); + +async function clickByText(selector, text) { + const handle = await page.evaluateHandle( + (sel, t) => + [...document.querySelectorAll(sel)].find((el) => el.textContent.includes(t)), + selector, + text, + ); + const el = handle.asElement(); + if (!el) throw new Error(`nicht gefunden: ${selector} ~ "${text}"`); + await el.click(); +} + +await page.goto(URL, { waitUntil: "domcontentloaded", timeout: 20000 }); +await page.waitForSelector(".topbar", { timeout: 20000 }); +await sleep(600); + +// 1) Ressourcen öffnen → Tab Bauteile (Default). +await clickByText("button.res-trigger", "Ressourcen"); +await sleep(400); +await page.waitForSelector(".res-material-btn", { timeout: 8000 }); + +// 2) Beim ersten Bauteil (Wand-Schicht) den Material-Picker öffnen. +const matBtns = await page.$$(".res-material-btn"); +console.log("Material-Buttons:", matBtns.length); +// Ein Bauteil wählen, das in Wänden verwendet wird — typischerweise das erste. +await matBtns[0].click(); +await page.waitForSelector(".mat-dialog", { timeout: 8000 }); +await shot("probe-materials-1-picker.png"); + +// 3) Bibliotheks-Material „Backstein" (Bricks104) zuweisen. +await clickByText(".mat-tile", "Backstein"); +await sleep(400); +await shot("probe-materials-2-assigned.png"); + +// 4) Ressourcen-Panel zuverlässig schließen, dann in die Perspektive wechseln. +async function closeResources() { + // Schwebendes ResourceManager-Fenster über den ×-Knopf der Schublade schließen + // (der overlay-Backdrop liegt über der Topbar; res-trigger ist verdeckt). + const closeBtn = await page.$(".res-drawer .res-close"); + if (closeBtn) { + await closeBtn.click(); + await page.waitForSelector(".res-drawer", { hidden: true, timeout: 5000 }); + await sleep(200); + } +} +await closeResources(); +// Perspektive-Ansicht aktivieren (Icon-Grid: aria-label "Perspektive"). +const persp = await page.$('button[aria-label="Perspektive"]'); +if (persp) await persp.click(); +await sleep(1200); + +// Darstellung-Dropdown auf „Texturiert". +async function setStyle(label) { + // Custom-Dropdown (kein natives select): per title öffnen, Option per Text. + const trigger = await page.$('button.tb-dd-trigger[title*="Darstellungsart der"]'); + if (!trigger) throw new Error("Darstellung-Dropdown nicht gefunden"); + await trigger.click(); + await page.waitForSelector("button.tb-dd-item", { timeout: 4000 }); + await sleep(150); + await clickByText("button.tb-dd-item", label); + await sleep(1300); +} +await setStyle("Schattiert"); +await shot("probe-materials-3-shaded.png"); +await setStyle("Texturiert"); +await sleep(1800); // Texturen lazy laden lassen +await shot("probe-materials-4-textured.png"); + +// 5) Upload-Pfad: ein Bild als Farb-Karte hochladen + zuweisen. +// Wieder Ressourcen öffnen, Picker öffnen, Datei wählen, zuweisen. +await clickByText("button.res-trigger", "Ressourcen"); +await sleep(400); +const matBtns2 = await page.$$(".res-material-btn"); +// Ein anderes Bauteil (zweite Schicht) nehmen, falls vorhanden, sonst dasselbe. +await (matBtns2[1] ?? matBtns2[0]).click(); +await page.waitForSelector(".mat-dialog", { timeout: 8000 }); +// Verstecktes File-Input der Farb-Zeile bespielen. +const fileInput = await page.$('.mat-upload-grid input[type="file"]'); +const imgPath = resolve(__dir, "../public/assets/materials/WoodFloor051/color.jpg"); +await fileInput.uploadFile(imgPath); +await sleep(500); +await shot("probe-materials-5-upload.png"); +await clickByText(".mat-actions .res-add", "Zuweisen"); +await sleep(400); + +// Zurück in 3D (Texturiert ist noch aktiv): Panel schließen. +await closeResources(); +await sleep(900); +await shot("probe-materials-6-upload-3d.png"); + +console.log("--- console ---"); +for (const l of logs.slice(-30)) console.log(l); + +await browser.close(); +console.log("done"); diff --git a/scripts/probe-pdf.mjs b/scripts/probe-pdf.mjs new file mode 100644 index 0000000..195e345 --- /dev/null +++ b/scripts/probe-pdf.mjs @@ -0,0 +1,325 @@ +// Probe: VEKTOR-PDF-Export des Grundrisses. +// 1) App öffnen (EG-Grundriss mit Beispielraum). +// 2) Export-Dialog über den PDF-Button öffnen, A4 / 1:100 wählen, exportieren. +// 3) Den Download (jsPDF blob) abfangen und ins Scratchpad speichern. +// 4) Vektor-Nachweis: Pfad-/Text-Operatoren > 0, KEIN Rasterbild für den Plan. +// 5) Massstab: bekannte Wandlänge (5 m bei 1:100 → 50 mm) im PDF nachmessen. +// 6) PDF → PNG rendern (pdfjs) für die visuelle Prüfung. + +import puppeteer from "puppeteer"; +import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs"; +import { join } from "node:path"; + +const URL = process.env.PROBE_URL || "http://localhost:5187/"; +const OUT = process.env.SCRATCH || "/tmp/pdf-probe"; +if (!existsSync(OUT)) mkdirSync(OUT, { recursive: true }); + +const browser = await puppeteer.launch({ + headless: "new", + args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"], +}); +const page = await browser.newPage(); +await page.setViewport({ width: 1400, height: 900, deviceScaleFactor: 1 }); +const logs = []; +page.on("console", (m) => logs.push(`[${m.type()}] ${m.text()}`)); +page.on("pageerror", (e) => logs.push(`[PAGEERROR] ${e.message}`)); + +await page.goto(URL, { waitUntil: "networkidle0", timeout: 30000 }); +await new Promise((r) => setTimeout(r, 600)); + +// jsPDF (v4) erzeugt das PDF als Blob und übergibt es an URL.createObjectURL, +// bevor es den Download auslöst. Wir überschreiben createObjectURL und lesen die +// Blob-Bytes (Base64) heraus — robust unabhängig vom Download-Mechanismus. +await page.evaluateOnNewDocument(() => { + window.__pdfCaptured = null; + const orig = URL.createObjectURL.bind(URL); + URL.createObjectURL = function (obj) { + try { + if (obj instanceof Blob) { + obj.arrayBuffer().then((buf) => { + const u8 = new Uint8Array(buf); + const head = String.fromCharCode.apply(null, u8.slice(0, 5)); + if (head === "%PDF-") { + let bin = ""; + for (let i = 0; i < u8.length; i++) bin += String.fromCharCode(u8[i]); + window.__pdfCaptured = { name: "grundriss.pdf", b64: btoa(bin) }; + } + }); + } + } catch (e) { + /* ignore */ + } + return orig(obj); + }; +}); +// Neu laden, damit der Patch (evaluateOnNewDocument) greift. +await page.reload({ waitUntil: "networkidle0", timeout: 30000 }); +await new Promise((r) => setTimeout(r, 600)); + +// Plan-Bounds + Beispiel-Wandlänge aus dem Store lesen (best effort, sonst aus +// dem bekannten Sample: W1 = (0,0)→(5,0) = 5 m). +const info = await page.evaluate(() => { + const st = window.__store?.getState?.(); + const p = st?.project; + if (!p) return null; + const eg = p.walls.filter((w) => w.floorId === "eg"); + const w1 = eg.find((w) => w.id === "W1"); + const len = w1 ? Math.hypot(w1.end.x - w1.start.x, w1.end.y - w1.start.y) : null; + return { wallCount: eg.length, w1Len: len }; +}); +console.log("Store:", JSON.stringify(info)); + +// PDF-Button in der Oberleiste klicken (Text „PDF"). Öffnet den Export-Dialog. +const clickedBtn = await page.evaluate(() => { + const btns = Array.from(document.querySelectorAll(".tb-btn")); + const b = btns.find((x) => x.textContent.trim() === "PDF"); + if (b) { + b.click(); + return true; + } + return false; +}); +console.log("PDF-Button geklickt?", clickedBtn); +await page.waitForSelector(".imp-dialog", { timeout: 5000 }); +await new Promise((r) => setTimeout(r, 200)); +await page.screenshot({ path: join(OUT, "export-dialog.png") }); +console.log("Dialog-Screenshot:", join(OUT, "export-dialog.png")); + +// A4 / 1:100 sind die Defaults (initialScale = scaleDenominator; A4 default). +// Sicherstellen: Massstab-Dropdown auf 1:100 stellen, falls nicht schon. Wir +// lesen den aktuellen Massstab aus dem Store und wählen sonst über die Dropdown- +// Option. Der Einfachheit halber verlassen wir uns auf den Default und prüfen N +// per gemessener Länge. +const exported = await page.evaluate(() => { + const btns = Array.from(document.querySelectorAll(".imp-btn.primary")); + const b = btns.find((x) => x.textContent.includes("Exportieren") || x.textContent.includes("Export")); + if (b) { + b.click(); + return true; + } + return false; +}); +console.log("Exportieren geklickt?", exported); + +// Auf den abgefangenen Blob warten. +let captured = null; +for (let i = 0; i < 60; i++) { + captured = await page.evaluate(() => window.__pdfCaptured); + if (captured) break; + await new Promise((r) => setTimeout(r, 100)); +} +if (!captured) { + console.log("FEHLER: kein PDF abgefangen."); + console.log(logs.join("\n")); + await browser.close(); + process.exit(1); +} +const pdfBytes = Buffer.from(captured.b64, "base64"); +const pdfPath = join(OUT, captured.name.endsWith(".pdf") ? captured.name : captured.name + ".pdf"); +writeFileSync(pdfPath, pdfBytes); + +// Visuelle Prüfung: das DRUCK-SVG (exakt die Vektor-Quelle des PDF) im Browser +// erzeugen und nach PNG rastern. Zeigt scharfe Linien, Schraffuren, weisses Blatt. +try { + const svgPng = await page.evaluate(async () => { + const gp = await import("/src/plan/generatePlan.ts"); + const sp = await import("/src/model/sampleProject.ts"); + const ptp = await import("/src/export/planToPrintSvg.ts"); + const proj = sp.sampleProject; + const codes = new Set(); + const walk = (cs) => + cs.forEach((c) => { + if (c.visible) { + codes.add(c.code); + if (c.children) walk(c.children); + } + }); + walk(proj.layers); + const plan = gp.generatePlan(proj, "eg", codes, undefined, "mittel", false, false); + const print = ptp.planToPrintSvg(plan, { + scaleDenominator: 100, + pageWidthMm: 297, + pageHeightMm: 210, + marginMm: 10, + }); + const xml = new XMLSerializer().serializeToString(print.svg); + const blob = new Blob([xml], { type: "image/svg+xml" }); + const url = URL.createObjectURL(blob); + const img = new Image(); + await new Promise((res, rej) => { + img.onload = res; + img.onerror = rej; + img.src = url; + }); + const scale = 4; + const canvas = document.createElement("canvas"); + canvas.width = 297 * scale; + canvas.height = 210 * scale; + const ctx = canvas.getContext("2d"); + ctx.fillStyle = "#ffffff"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + return canvas.toDataURL("image/png"); + }); + const b64 = svgPng.split(",")[1]; + writeFileSync(join(OUT, "print-svg.png"), Buffer.from(b64, "base64")); + console.log("Druck-SVG → PNG:", join(OUT, "print-svg.png")); +} catch (e) { + console.log("(SVG-Render übersprungen:", e.message + ")"); +} + +console.log(`PDF gespeichert: ${pdfPath} (${pdfBytes.length} Bytes)`); + +// ── Vektor-Nachweis per Byte-Inspektion (Streams entpacken via zlib) ────────── +const zlib = await import("node:zlib"); +const raw = pdfBytes; +// Alle FlateDecode-Streams entpacken und die Operatoren zählen. +let allOps = ""; +let imageXObjects = 0; +let dctImages = 0; +const text = raw.toString("latin1"); +// /Subtype /Image und /DCTDecode global suchen (auch in Streams selten, aber +// im Dictionary sichtbar). +imageXObjects += (text.match(/\/Subtype\s*\/Image/g) || []).length; +dctImages += (text.match(/\/DCTDecode/g) || []).length; + +const streamRe = /stream\r?\n([\s\S]*?)\r?\nendstream/g; +let m; +while ((m = streamRe.exec(text)) !== null) { + const chunk = Buffer.from(m[1], "latin1"); + let inflated = null; + try { + inflated = zlib.inflateSync(chunk).toString("latin1"); + } catch { + try { + inflated = zlib.inflateRawSync(chunk).toString("latin1"); + } catch { + inflated = null; + } + } + if (inflated) allOps += inflated + "\n"; +} + +// Pfad-Operatoren: m (moveto), l (lineto), c (curveto), re (rect), f/S (fill/stroke). +const countOp = (re) => (allOps.match(re) || []).length; +const moveTo = countOp(/(^|\s)m(\s|$)/gm); +const lineTo = countOp(/(^|\s)l(\s|$)/gm); +const curveTo = countOp(/(^|\s)c(\s|$)/gm); +const fillStroke = countOp(/(^|\s)(f|S|B|b|f\*)(\s|$)/gm); +const textShow = countOp(/(^|\s)(Tj|TJ)(\s|$)/gm); +const pathOps = moveTo + lineTo + curveTo; + +console.log("\n=== VEKTOR-NACHWEIS ==="); +console.log(`Pfad-Operatoren: moveto(m)=${moveTo} lineto(l)=${lineTo} curve(c)=${curveTo} fill/stroke=${fillStroke}`); +console.log(`Gesamt-Pfadbefehle: ${pathOps}`); +console.log(`Text-Operatoren (Tj/TJ): ${textShow}`); +console.log(`Bild-XObjects (/Subtype/Image): ${imageXObjects}`); +console.log(`DCT/JPEG-Bilder (/DCTDecode): ${dctImages}`); +const isVector = pathOps > 50 && imageXObjects === 0 && dctImages === 0; +console.log(`→ VEKTOR (Pfade > 0, kein Rasterbild)? ${isVector ? "JA" : "NEIN"}`); + +// ── Massstab-Messung über pdfjs (Pfad-Koordinaten der gezeichneten Linien) ──── +let scaleResult = "n/a"; +try { + const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs"); + const doc = await pdfjs.getDocument({ data: new Uint8Array(pdfBytes) }).promise; + const pg = await doc.getPage(1); + const vp = pg.getViewport({ scale: 1 }); // Einheiten = PDF-Punkte (1pt=1/72 inch) + const ptToMm = 25.4 / 72; + const pageWpt = vp.width, pageHpt = vp.height; + const ops = await pg.getOperatorList(); + const OPS = pdfjs.OPS; + // Pro Pfad eine eigene BBox sammeln (jeder constructPath ist ein Sub-Pfad). So + // können wir das seitenfüllende weisse Blatt + das Schriftfeld unten rechts + // ausschließen und nur die PLAN-Geometrie messen. + const boxes = []; + for (let i = 0; i < ops.fnArray.length; i++) { + if (ops.fnArray[i] !== OPS.constructPath) continue; + const coords = ops.argsArray[i][1]; + let bx0 = Infinity, by0 = Infinity, bx1 = -Infinity, by1 = -Infinity; + for (let k = 0; k + 1 < coords.length; k += 2) { + const x = coords[k], y = coords[k + 1]; + if (typeof x === "number" && typeof y === "number" && isFinite(x) && isFinite(y)) { + if (x < bx0) bx0 = x; if (x > bx1) bx1 = x; + if (y < by0) by0 = y; if (y > by1) by1 = y; + } + } + if (isFinite(bx0)) boxes.push({ x0: bx0, y0: by0, x1: bx1, y1: by1 }); + } + const pageWmm = pageWpt * ptToMm; + const pageHmm = pageHpt * ptToMm; + // svg2pdf zeichnet den Plan-Inhalt in mm-Benutzereinheiten (Koordinaten 0..297 + // bei A4-quer). Daneben gibt es einen äußeren Clip-Pfad in PDF-PUNKTEN + // (Koordinaten bis ~842). Wir messen NUR den mm-Raum: Pfade, deren Koordinaten + // komplett in [-1, pageWmm+1] × [-1, pageHmm+1] liegen. + const inMmSpace = (b) => + b.x0 >= -1 && b.x1 <= pageWmm + 1 && b.y0 >= -1 && b.y1 <= pageHmm + 1; + // Seitenfüllendes weisses Blatt ausschließen (BBox ≈ ganze Seite). + const pageLike = (b) => + b.x1 - b.x0 > pageWmm * 0.9 && b.y1 - b.y0 > pageHmm * 0.9; + // Schriftfeld unten rechts: ~70mm am rechten Rand. Pfade, deren Mittelpunkt im + // rechten Block (x > pageW-75mm) liegen, ausschließen (Plan ist zentriert). + const titleXmm = pageWmm - 75; + const inTitleBlock = (b) => (b.x0 + b.x1) / 2 > titleXmm; + const plan = boxes.filter( + (b) => inMmSpace(b) && !pageLike(b) && !inTitleBlock(b), + ); + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (const b of plan) { + if (b.x0 < minX) minX = b.x0; if (b.x1 > maxX) maxX = b.x1; + if (b.y0 < minY) minY = b.y0; if (b.y1 > maxY) maxY = b.y1; + } + // Die Plan-Pfade liegen bereits in mm-Benutzereinheiten (siehe inMmSpace). + const widthMm = maxX - minX; + const heightMm = maxY - minY; + console.log("\n=== MASSSTAB ==="); + console.log(`Plan-Pfade (ohne Blatt/Schriftfeld): ${plan.length} von ${boxes.length}`); + console.log(`Plan-Inhalt BBox: ${widthMm.toFixed(2)} × ${heightMm.toFixed(2)} mm auf dem Blatt`); + // Bei 1:100: Welt-Breite (W1=5 m + Wanddicke ~0.345 m) ≈ 5.345 m → 53.45 mm. + // Höhe (W2=4 m + Dicke) ≈ 4.345 m → 43.45 mm. + const expectedWmm = 5.345 * (1000 / 100); + const expectedHmm = 4.345 * (1000 / 100); + console.log(`Erwartet bei 1:100: ~${expectedWmm.toFixed(2)} × ~${expectedHmm.toFixed(2)} mm`); + const okW = Math.abs(widthMm - expectedWmm) < 2; + const okH = Math.abs(heightMm - expectedHmm) < 2; + console.log(`→ Massstab korrekt (±2mm)? Breite ${okW ? "OK" : "ABWEICHUNG"}, Höhe ${okH ? "OK" : "ABWEICHUNG"}`); + scaleResult = `${widthMm.toFixed(2)}×${heightMm.toFixed(2)}mm (erwartet ${expectedWmm.toFixed(1)}×${expectedHmm.toFixed(1)})`; + + // PDF → PNG rendern (scharfe Linien/Schraffuren sichtbar prüfen). @napi-rs/ + // canvas hat vorgebaute Binaries (kein nativer Build nötig). Text wird vom + // Renderer ggf. nicht unterstützt → Geometrie genügt für die Sichtprüfung. + try { + const { createCanvas } = await import("@napi-rs/canvas"); + const rscale = 4; + const rvp = pg.getViewport({ scale: rscale }); + const canvasFactory = { + create: (w, h) => { + const c = createCanvas(w, h); + return { canvas: c, context: c.getContext("2d") }; + }, + reset: (co, w, h) => { + co.canvas.width = w; + co.canvas.height = h; + }, + destroy: (co) => { + co.canvas.width = 0; + co.canvas.height = 0; + }, + }; + const canvas = createCanvas(Math.ceil(rvp.width), Math.ceil(rvp.height)); + const ctx = canvas.getContext("2d"); + ctx.fillStyle = "#ffffff"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + await pg.render({ canvasContext: ctx, viewport: rvp, canvasFactory }).promise; + writeFileSync(join(OUT, "export.png"), canvas.toBuffer("image/png")); + console.log("PDF→PNG:", join(OUT, "export.png")); + } catch (e) { + console.log("(PNG-Render übersprungen:", e.message + ")"); + } +} catch (e) { + console.log("pdfjs-Messung fehlgeschlagen:", e.message); +} + +console.log("\n=== Logs ==="); +console.log(logs.length ? logs.slice(-15).join("\n") : "(keine)"); +await browser.close(); diff --git a/scripts/probe-pills.mjs b/scripts/probe-pills.mjs new file mode 100644 index 0000000..a332d24 --- /dev/null +++ b/scripts/probe-pills.mjs @@ -0,0 +1,89 @@ +import puppeteer from "puppeteer"; +const b = await puppeteer.launch({ + headless: "new", + args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"], +}); +const p = await b.newPage(); +await p.setViewport({ width: 1500, height: 220, deviceScaleFactor: 2 }); +await p.goto("http://localhost:5187/", { waitUntil: "networkidle0", timeout: 20000 }).catch(() => {}); +await new Promise((r) => setTimeout(r, 900)); + +// Versuch: in eine Geschoss-/Grundriss-Ansicht wechseln, damit der Linien-Modus +// (nur im Grundriss) aktiv ist. Klick auf das erste "grundriss"-View-Icon. +const grund = await p.$('.view-icon[aria-label]'); +const tb = await p.$(".topbar"); +let box = await tb.boundingBox(); +const H = Math.ceil(box.height) + 10; + +// 1) Topbar im Ruhezustand. +await p.screenshot({ path: "scripts/probe-pills-1-topbar.png", clip: { x: 0, y: 0, width: 1500, height: H } }); + +// Diagnose: gibt es einen Linien-Modus-Dropdown-Trigger? (Label-Text.) +const diag = await p.evaluate(() => { + const triggers = [...document.querySelectorAll(".tb-dd-trigger")].map((t) => t.textContent.trim()); + const btns = [...document.querySelectorAll(".tb-btn")].map((b) => ({ + text: b.textContent.trim(), + radius: getComputedStyle(b).borderRadius, + border: getComputedStyle(b).borderTopWidth, + bg: getComputedStyle(b).backgroundColor, + })); + // Hat irgendein Dropdown die Linien-Modus-Werte Display/Print? + return { triggers, btns }; +}); +console.log("TRIGGERS:", JSON.stringify(diag.triggers)); +console.log("BTNS:", JSON.stringify(diag.btns, null, 0)); + +// 2) Linien-Modus-Dropdown öffnen (Tastatur, zuverlässig): Trigger mit Label +// Display/Print fokussieren + Enter. +const sel = await p.evaluate(() => { + const t = [...document.querySelectorAll(".tb-dd-trigger")].find((t) => { + const tx = t.textContent.trim(); + return tx === "Display" || tx === "Print"; + }); + if (!t) return null; + t.id = "linemode-trigger-probe"; + return "#linemode-trigger-probe"; +}); +console.log("LINEMODE DROPDOWN FOUND:", !!sel); +if (sel) { + await p.focus(sel); + await p.keyboard.press("Enter"); +} +await new Promise((r) => setTimeout(r, 350)); +// Vollbild, damit das (per Portal an body) gerenderte Popover sichtbar ist. +await p.screenshot({ path: "scripts/probe-pills-2-linemode-open.png" }); + +// Popover-Inhalt prüfen (Optionen + Häkchen). +const pop = await p.evaluate(() => { + const menu = document.querySelector(".tb-dd-menu"); + if (!menu) return null; + const items = [...menu.querySelectorAll(".tb-dd-item")].map((i) => ({ + label: i.querySelector(".ctx-item-label")?.textContent.trim(), + checked: !!i.querySelector(".tb-dd-check"), + })); + return items; +}); +console.log("LINEMODE OPTIONS:", JSON.stringify(pop)); + +// Menü schließen. +await p.keyboard.press("Escape"); +await new Promise((r) => setTimeout(r, 200)); + +// 3) Hover über die erste Aktions-Pille (z. B. 100%), Hover-Optik zeigen. +const hovered = await p.evaluate(() => { + const btn = document.querySelector(".tb-btn:not(:disabled)"); + if (!btn) return null; + btn.scrollIntoView({ block: "center", inline: "center" }); + return true; +}); +const firstBtn = await p.$(".tb-btn"); +if (firstBtn) { + const bb = await firstBtn.boundingBox(); + if (bb) await p.mouse.move(bb.x + bb.width / 2, bb.y + bb.height / 2); +} +await new Promise((r) => setTimeout(r, 250)); +box = await tb.boundingBox(); +await p.screenshot({ path: "scripts/probe-pills-3-btn-hover.png", clip: { x: 0, y: 0, width: 1500, height: Math.ceil(box.height) + 10 } }); +console.log("HOVERED BTN:", hovered); + +await b.close(); diff --git a/scripts/probe-splitjoin.mjs b/scripts/probe-splitjoin.mjs index 1bd0b28..a6b5d8b 100644 --- a/scripts/probe-splitjoin.mjs +++ b/scripts/probe-splitjoin.mjs @@ -1,5 +1,14 @@ // Verifikation Split / Join / Segment-Löschen (Ctrl+S / Ctrl+J / Alt+Klick). -// Liest das Modell über den TEMP-Hook window.__drawings2d (id + geom). +// Liest das Modell über einen TEMP-Hook window.__drawings2d (id + geom). Dieser +// Hook wurde nur für die Verifikation in App.tsx eingehängt und danach wieder +// ENTFERNT — zum erneuten Lauf muss er kurzzeitig reaktiviert werden: +// useEffect(() => { +// (window).__drawings2d = project.drawings2d.map(d => ({id:d.id, geom:d.geom})); +// }, [project.drawings2d]); +// Verifizierte Ergebnisse (siehe probe-splitjoin-*.png): +// Split Rechteck+Querlinie → 2 geschlossene Polylinien + Linie +// Join 2 Linien (gem. Endpunkt) → 1 Polylinie (3 Punkte) +// Alt-Klick auf Rechteck-Kante → offene Polylinie (4 Punkte, Segment weg) import puppeteer from "puppeteer"; const b = await puppeteer.launch({ @@ -75,8 +84,8 @@ await endCommand(); await sleep(150); const before = await drawings(); -// Rechteck wählen: auf eine Rechteck-Kante klicken (untere Kante y=0, x=1). -let [cx, cy] = await modelToClient(1, 0); +// Rechteck wählen: auf die linke Kante klicken (x=0, y=2) — eindeutig auf dem Rand. +let [cx, cy] = await modelToClient(0, 2); await p.mouse.click(cx, cy); await sleep(150); await ctrlPress("s"); @@ -91,20 +100,20 @@ await p.reload({ waitUntil: "networkidle0" }); await sleep(800); await focusCmd(); await typeLine("line"); -await typeLine("-3,-3"); -await typeLine("0,-3"); +await typeLine("1,1"); +await typeLine("4,1"); await focusCmd(); await typeLine("line"); -await typeLine("0,-3"); -await typeLine("0,0"); +await typeLine("4,1"); +await typeLine("4,4"); await endCommand(); await sleep(150); const beforeJoin = await drawings(); -// Beide wählen: erste klicken, dann Shift+zweite. -let [ax, ay] = await modelToClient(-1.5, -3); +// Beide wählen: erste klicken (Mitte 2.5,1), dann Shift+zweite (Mitte 4,2.5). +let [ax, ay] = await modelToClient(2.5, 1); await p.mouse.click(ax, ay); -await sleep(100); -let [bx2, by2] = await modelToClient(0, -1.5); +await sleep(120); +let [bx2, by2] = await modelToClient(4, 2.5); await p.keyboard.down("Shift"); await p.mouse.click(bx2, by2); await p.keyboard.up("Shift"); diff --git a/scripts/probe-theme.mjs b/scripts/probe-theme.mjs new file mode 100644 index 0000000..b00bf0d --- /dev/null +++ b/scripts/probe-theme.mjs @@ -0,0 +1,35 @@ +// Screenshot des Plans in LIGHT- vs DARK-Theme (prefers-color-scheme emuliert), +// um theme-abhängige Linien-/Textfarben aufzudecken. Default-App (SVG-Renderer). + +import puppeteer from "puppeteer"; +const BASE = process.env.PROBE_URL || "http://localhost:5187/"; +const browser = await puppeteer.launch({ + headless: "new", + args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"], +}); + +async function shoot(scheme, path) { + const page = await browser.newPage(); + await page.setViewport({ width: 1400, height: 900, deviceScaleFactor: 2 }); + await page.emulateMediaFeatures([{ name: "prefers-color-scheme", value: scheme }]); + try { + await page.goto(BASE, { waitUntil: "networkidle0", timeout: 25000 }); + } catch (e) { + console.log(`[goto ${scheme}]`, e.message); + } + await new Promise((r) => setTimeout(r, 1200)); + const colors = await page.evaluate(() => { + const cs = getComputedStyle(document.documentElement); + return { + sheet: cs.getPropertyValue("--sheet").trim(), + ink: cs.getPropertyValue("--ink").trim(), + }; + }); + await page.screenshot({ path }); + console.log(`${scheme}: --sheet=${colors.sheet} --ink=${colors.ink} → ${path}`); + await page.close(); +} + +await shoot("light", "scripts/probe-theme-light.png"); +await shoot("dark", "scripts/probe-theme-dark.png"); +await browser.close(); diff --git a/scripts/probe-topbar-dossier.mjs b/scripts/probe-topbar-dossier.mjs new file mode 100644 index 0000000..f71bebb --- /dev/null +++ b/scripts/probe-topbar-dossier.mjs @@ -0,0 +1,67 @@ +import puppeteer from "puppeteer"; + +const URL = process.env.PROBE_URL || "http://localhost:5187/"; +const b = await puppeteer.launch({ + headless: "new", + args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"], +}); +const p = await b.newPage(); +await p.setViewport({ width: 1600, height: 300, deviceScaleFactor: 2 }); + +const logs = []; +p.on("console", (m) => logs.push(`[${m.type()}] ${m.text()}`)); +p.on("pageerror", (e) => logs.push(`[PAGEERROR] ${e.message}`)); + +await p.goto(URL, { waitUntil: "networkidle0", timeout: 25000 }).catch((e) => + logs.push(`[GOTO] ${e.message}`), +); +await new Promise((r) => setTimeout(r, 900)); + +// Ganze Oberleiste (unabhängig vom horizontalen Scroll) breit einfangen. +const info = await p.evaluate(() => { + const tb = document.querySelector(".topbar"); + const stat = document.querySelector(".tb-stat"); + const segs = document.querySelectorAll(".tb-seg").length; + const textGroup = !!document.querySelector(".tb-textgroup"); + const looseZoom = document.querySelectorAll(".tb-zoom").length; // sollte 0 sein + const addText = !!document.querySelector(".tb-addtext"); + const iconBtns = document.querySelectorAll(".tb-iconbtn").length; + return { + barH: tb ? Math.round(tb.getBoundingClientRect().height) : null, + scrollW: tb ? tb.scrollWidth : null, + statPresent: !!stat, + segmentCount: segs, + textGroup, + looseZoomSpans: looseZoom, + addText, + iconBtns, + }; +}); +console.log("INFO", JSON.stringify(info)); + +// Oberleiste voll ins Bild scrollen und mehrere Ausschnitte schiessen. +const tb = await p.$(".topbar"); +const box = await tb.boundingBox(); +await p.screenshot({ + path: "scripts/probe-topbar-dossier-1-left.png", + clip: { x: 0, y: 0, width: 1600, height: Math.ceil(box.height) + 6 }, +}); +// Nach rechts scrollen, um Zoom-Cluster + Text-Gruppe zu zeigen. +await p.evaluate(() => { + const el = document.querySelector(".topbar"); + if (el) el.scrollLeft = Math.max(0, el.scrollWidth - el.clientWidth); +}); +await new Promise((r) => setTimeout(r, 400)); +await p.screenshot({ + path: "scripts/probe-topbar-dossier-2-right.png", + clip: { x: 0, y: 0, width: 1600, height: Math.ceil(box.height) + 6 }, +}); + +console.log("\n=== Konsole ==="); +console.log(logs.length ? logs.join("\n") : "(keine)"); +const errors = logs.filter( + (l) => l.startsWith("[error]") || l.startsWith("[PAGEERROR]"), +); +console.log(errors.length ? `\nFEHLER: ${errors.length}` : "\nKeine Konsolenfehler."); +await b.close(); +process.exit(errors.length ? 1 : 0); diff --git a/scripts/probe-topbar-rest.mjs b/scripts/probe-topbar-rest.mjs new file mode 100644 index 0000000..8726603 --- /dev/null +++ b/scripts/probe-topbar-rest.mjs @@ -0,0 +1,81 @@ +import puppeteer from "puppeteer"; + +const b = await puppeteer.launch({ + headless: "new", + args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"], +}); +const p = await b.newPage(); +await p.setViewport({ width: 1500, height: 220, deviceScaleFactor: 3 }); +await p + .goto("http://localhost:5187/", { waitUntil: "networkidle0", timeout: 20000 }) + .catch(() => {}); +await new Promise((r) => setTimeout(r, 800)); + +// Helfer: Button per Tooltip-Anfang finden (title beginnt mit Text). +const findByTitle = (frag) => + p.evaluateHandle((f) => { + const els = [...document.querySelectorAll("button[title]")]; + return els.find((e) => e.getAttribute("title")?.startsWith(f)) || null; + }, frag); + +const snap = async (file) => { + const tb = await p.$(".topbar"); + const box = await tb.boundingBox(); + await p.screenshot({ + path: `scripts/${file}`, + clip: { x: 0, y: 0, width: 1500, height: Math.ceil(box.height) + 8 }, + }); +}; + +// Aktuellen Zustand der iconisierten Steuerungen lesen. +const state = () => + p.evaluate(() => { + const byTitle = (f) => + [...document.querySelectorAll("button[title]")].find((e) => + e.getAttribute("title")?.startsWith(f), + ); + const ref = byTitle("Referenzlinien"); + const res = byTitle("Ressourcen"); + const icons = [...document.querySelectorAll("button .material-symbols-outlined.tb-ico")].map( + (s) => s.textContent, + ); + return { + refIcon: ref?.querySelector(".tb-ico")?.textContent, + refActive: ref?.classList.contains("active") ?? null, + resIcon: res?.querySelector(".tb-ico")?.textContent, + resActive: res?.classList.contains("active") ?? null, + lineModeIcons: [ + ...document.querySelectorAll(".view-btn-ico .tb-ico"), + ].map((s) => s.textContent), + allTbIcons: icons, + }; + }); + +console.log("INITIAL:", JSON.stringify(await state())); +await snap("probe-topbar-rest-1-initial.png"); + +// Ressourcen-Toggle einschalten (öffnet die Schublade) → aktiver Zustand. +const res = await findByTitle("Ressourcen"); +if (res && (await res.evaluate((e) => !!e))) { + await res.click(); + await new Promise((r) => setTimeout(r, 500)); +} +console.log("AFTER RES TOGGLE:", JSON.stringify(await state())); +await snap("probe-topbar-rest-2-res-active.png"); + +// Kamera-Popover öffnen (Perspektive nötig — erst auf Perspektive schalten). +const iso = await findByTitle("Isometrie"); +if (iso && (await iso.evaluate((e) => !!e))) { + await iso.click(); + await new Promise((r) => setTimeout(r, 400)); +} +const cam = await findByTitle("Kamera"); +if (cam && (await cam.evaluate((e) => !!e))) { + await cam.click(); + await new Promise((r) => setTimeout(r, 400)); +} +const popoverOpen = await p.evaluate(() => !!document.querySelector(".tb-popover")); +console.log("CAMERA POPOVER OPEN:", popoverOpen); +await snap("probe-topbar-rest-3-popover.png"); + +await b.close(); diff --git a/scripts/probe-trim.mjs b/scripts/probe-trim.mjs new file mode 100644 index 0000000..21d00c7 --- /dev/null +++ b/scripts/probe-trim.mjs @@ -0,0 +1,108 @@ +// Verifikation Trim (Quick-Trim „stutzen"). Zwei sich kreuzende Linien zeichnen, +// dann `trim` und auf den Überstand einer Linie klicken → der Überstand ist bis +// zum Schnittpunkt weg (neuer Endpunkt = Schnittpunkt). Liest die gerenderten +// `line.draw2d`-Segmente direkt aus dem SVG (Modellkoord. = SVG/PX_PER_M). +import puppeteer from "puppeteer"; + +const PX = 90; // PX_PER_M in PlanView +const b = await puppeteer.launch({ + headless: "new", + args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"], +}); +const p = await b.newPage(); +await p.setViewport({ width: 1280, height: 820, deviceScaleFactor: 1 }); +const errs = []; +p.on("pageerror", (e) => errs.push(e.message)); +await p.goto("http://localhost:5187/", { waitUntil: "networkidle0", timeout: 20000 }).catch(() => {}); +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +await sleep(800); + +const focusCmd = async () => { + const el = await p.$(".cmdline-input"); + const bx = await el.boundingBox(); + await p.mouse.click(bx.x + bx.width / 2, bx.y + bx.height / 2); + await sleep(60); +}; +const typeLine = async (t) => { + await p.type(".cmdline-input", t, { delay: 6 }); + await p.keyboard.press("Enter"); + await sleep(90); +}; +const segs = () => + p.evaluate((PX) => { + return [...document.querySelectorAll(".plan-svg line.draw2d")].map((l) => ({ + x1: +l.getAttribute("x1") / PX, + y1: -l.getAttribute("y1") / PX, + x2: +l.getAttribute("x2") / PX, + y2: -l.getAttribute("y2") / PX, + })); + }, PX); +// Modellpunkt → Client-Pixel über das aktuelle SVG-CTM. +const modelToClient = (mx, my) => + p.evaluate( + (mx, my, PX) => { + const svg = document.querySelector(".plan-svg"); + const ctm = svg.getScreenCTM(); + const q = svg.createSVGPoint(); + q.x = mx * PX; + q.y = -my * PX; + const s = q.matrixTransform(ctm); + return [s.x, s.y]; + }, + mx, + my, + PX, + ); + +// ── Zwei sich kreuzende Linien ─────────────────────────────────────────────── +// A: horizontal y=2, x=0..6 (Überstand rechts ab x=4). B: vertikal x=4, y=0..4. +await focusCmd(); +await typeLine("line"); +await typeLine("0,2"); +await typeLine("6,2"); +await focusCmd(); +await typeLine("line"); +await typeLine("4,0"); +await typeLine("4,4"); +await p.keyboard.press("Escape"); +await sleep(120); + +const before = await segs(); + +// ── Trim: auf den Überstand von A (x=5, y=2) klicken ───────────────────────── +await focusCmd(); +await typeLine("trim"); +// Befehl ist aktiv; jetzt in der Plan-Ansicht auf den Überstand klicken. +// Eingabefeld behält Fokus → der Klick geht trotzdem an PlanView (pick). +const [hx, hy] = await modelToClient(5, 2); +await p.mouse.move(hx, hy); +await sleep(120); +await p.mouse.click(hx, hy); +await sleep(200); +await p.keyboard.press("Escape"); +await sleep(120); + +const after = await segs(); +await p.screenshot({ path: "scripts/probe-trim.png" }); + +// ── Auswertung: gibt es ein horizontales Segment (y≈2), das bei x≈4 endet und +// NICHT über x=4 hinausragt? Der Überstand x∈(4,6] muss weg sein. ──────────── +const horiz = after.filter((s) => Math.abs(s.y1 - 2) < 0.05 && Math.abs(s.y2 - 2) < 0.05); +const maxX = horiz.reduce((m, s) => Math.max(m, s.x1, s.x2), -Infinity); +const trimmedToIntersection = Math.abs(maxX - 4) < 0.05; + +console.log( + JSON.stringify( + { + beforeCount: before.length, + afterCount: after.length, + horizSegments: horiz, + maxXofHoriz: maxX, + trimmedToIntersection, + errs, + }, + null, + 2, + ), +); +await b.close(); diff --git a/scripts/probe-wall-cmd.mjs b/scripts/probe-wall-cmd.mjs new file mode 100644 index 0000000..5c83789 --- /dev/null +++ b/scripts/probe-wall-cmd.mjs @@ -0,0 +1,101 @@ +// Verifikation: „Wand" als echter Engine-Befehl. +// 1) GUI-„Wand" klicken → Befehlsfeld aktiv (Wand-Prompt, Feld fokussiert). +// 2) Mehrere Punkte im Plan klicken → Wände entstehen (Plan-Primitive wachsen), +// Rechtsklick beendet den (chainenden) Zug. +// 3) In 3D (Isometrie) schalten → die Wände sind dort sichtbar (Screenshot). +// 4) Getipptes „wand" startet DENSELBEN Befehl (Prompt = Wand-Prompt). +// Temporäres Verifikations-Gerüst (nicht eingecheckt). +import puppeteer from "puppeteer"; +const URL = process.env.PROBE_URL || "http://localhost:5188/"; +const b = await puppeteer.launch({ + headless: "new", + args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"], +}); +const p = await b.newPage(); +await p.setViewport({ width: 1400, height: 900, deviceScaleFactor: 1.25 }); +const errs = []; +p.on("pageerror", (e) => errs.push(e.message)); +await p.goto(URL, { waitUntil: "networkidle0", timeout: 20000 }).catch(() => {}); +const wait = (ms) => new Promise((r) => setTimeout(r, ms)); +await wait(900); + +const clickTool = (label) => + p.evaluate((l) => [...document.querySelectorAll("button.tool-row")] + .find((x) => x.textContent.trim() === l)?.click(), label); +const clickView = (aria) => + p.evaluate((a) => [...document.querySelectorAll("button")] + .find((x) => x.getAttribute("aria-label") === a)?.click(), aria); +const planBox = () => p.evaluate(() => { + const r = document.querySelector(".plan-svg").getBoundingClientRect(); + return { x: r.x, y: r.y, w: r.width, h: r.height }; +}); +const click = async (x, y, opts) => { await p.mouse.move(x, y); await p.mouse.click(x, y, opts); await wait(110); }; +const cmdState = () => p.evaluate(() => { + const prompt = document.querySelector(".cmdline-prompt")?.textContent?.trim() ?? ""; + const input = document.querySelector(".cmdline-input"); + const focused = document.activeElement === input; + const placeholder = input?.getAttribute("placeholder") ?? ""; + return { prompt, focused, active: placeholder === "" }; +}); +// Grobes Mass für „Wand-Geometrie im Plan": Anzahl Pfad-/Polygon-Primitive. +// Wände rendern als gefüllte Schicht-Polygone + Umriss-Pfade; jede neue Wand +// erhöht diese Zahl deutlich. +const primCount = () => p.evaluate(() => + document.querySelectorAll(".plan-svg path, .plan-svg polygon").length); +const typeInCmd = async (text) => { + await p.evaluate(() => document.querySelector(".cmdline-input")?.focus()); + await p.keyboard.type(text); + await p.keyboard.press("Enter"); + await wait(200); +}; + +const box = await planBox(); +const cx = box.x + box.w / 2, cy = box.y + box.h / 2; + +// ── 1) „Wand" anklicken → Befehlsfeld aktiv, Wand-Prompt ───────────────────── +const primStart = await primCount(); +await clickTool("Wand"); +await wait(300); +const afterToolClick = await cmdState(); +console.log("after Wand-tool click → cmdline:", afterToolClick); +await p.screenshot({ path: "scripts/probe-wall-cmd-1-active.png" }); + +// ── 2) Mehrere Punkte → chainende Wände, dann Rechtsklick zum Beenden ──────── +const P = [ + [cx - 220, cy - 120], + [cx + 40, cy - 160], + [cx + 200, cy + 10], + [cx + 60, cy + 150], +]; +for (const pt of P) await click(...pt); +await p.mouse.click(P[P.length - 1][0], P[P.length - 1][1], { button: "right" }); // Zug beenden +await wait(300); +const primAfter = await primCount(); +console.log("plan primitives start / after walls:", primStart, primAfter, "→ delta", primAfter - primStart); +await p.screenshot({ path: "scripts/probe-wall-cmd-2-plan.png" }); + +// ── 3) In 3D (Isometrie) → Wände sichtbar ─────────────────────────────────── +await clickView("Isometrie"); +await wait(1500); +await p.screenshot({ path: "scripts/probe-wall-cmd-3-3d.png" }); + +// Zurück in den Grundriss. +await clickView("Grundriss"); +await wait(600); + +// ── 4) Getipptes „wand" startet DENSELBEN Befehl ──────────────────────────── +await clickTool("Auswahl"); +await wait(200); +const before = await cmdState(); +await typeInCmd("wand"); +const afterTyped = await cmdState(); +console.log("after typing 'wand' → cmdline:", afterTyped, "(was:", before, ")"); +// Einen Punkt setzen + Rechtsklick (Abbruch ohne 2. Punkt) — nur Prompt-Beleg. +await p.screenshot({ path: "scripts/probe-wall-cmd-4-typed.png" }); +await p.keyboard.press("Escape"); + +console.log("errs:", errs.slice(0, 6)); +console.log("RESULT activePrompt:", afterToolClick.active, afterToolClick.prompt, + "| primDelta:", primAfter - primStart, + "| typedPrompt:", afterTyped.active, afterTyped.prompt); +await b.close(); diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore new file mode 100644 index 0000000..26ef2ff --- /dev/null +++ b/src-tauri/.gitignore @@ -0,0 +1,4 @@ +/target +/gen/schemas +# Build-Artefakte aller (Sub-)Crates, unabhängig von der Verschachtelung. +target/ diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock new file mode 100644 index 0000000..88df8fb --- /dev/null +++ b/src-tauri/Cargo.lock @@ -0,0 +1,4399 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +dependencies = [ + "serde", +] + +[[package]] +name = "cad-tauri" +version = "0.1.0" +dependencies = [ + "geometry", + "serde", + "serde_json", + "tauri", + "tauri-build", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.0", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.0", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.118", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.118", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "geometry" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +dependencies = [ + "libc", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.0", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.118", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.0", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.0", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.118", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "time" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.0", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..22bb7c2 --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,21 @@ +[workspace] +members = [".", "geometry"] + +[package] +name = "cad-tauri" +version = "0.1.0" +edition = "2021" + +[lib] +# Der Tauri-v2-Bibliotheksname; staticlib/cdylib fuer Mobile, rlib fuer Desktop. +name = "cad_tauri_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +geometry = { path = "geometry" } diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/geometry/Cargo.lock b/src-tauri/geometry/Cargo.lock new file mode 100644 index 0000000..5cb3a2b --- /dev/null +++ b/src-tauri/geometry/Cargo.lock @@ -0,0 +1,75 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "geometry" +version = "0.1.0" +dependencies = [ + "serde", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/src-tauri/geometry/Cargo.toml b/src-tauri/geometry/Cargo.toml new file mode 100644 index 0000000..05542fc --- /dev/null +++ b/src-tauri/geometry/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "geometry" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1", features = ["derive"] } + +# Nur fuers Paritaets-Beispiel (examples/parity.rs) — JSON von stdin lesen. +[dev-dependencies] +serde_json = "1" diff --git a/src-tauri/geometry/examples/parity.rs b/src-tauri/geometry/examples/parity.rs new file mode 100644 index 0000000..d63d48c --- /dev/null +++ b/src-tauri/geometry/examples/parity.rs @@ -0,0 +1,16 @@ +// Paritaets-Harness: liest ein JoinInput-JSON von stdin, rechnet compute_joins +// und schreibt das WallCuts-JSON nach stdout. Dient dem TS<->Rust-Vergleich +// (siehe src/compute/parity.test.ts) — identische Eingabe, identische Ausgabe. + +use std::io::Read; + +fn main() { + let mut buf = String::new(); + std::io::stdin() + .read_to_string(&mut buf) + .expect("stdin lesen"); + let input: geometry::JoinInput = serde_json::from_str(&buf).expect("JoinInput parsen"); + let cuts = geometry::compute_joins(input); + let out = serde_json::to_string(&cuts).expect("WallCuts serialisieren"); + println!("{out}"); +} diff --git a/src-tauri/geometry/src/lib.rs b/src-tauri/geometry/src/lib.rs new file mode 100644 index 0000000..f7892c3 --- /dev/null +++ b/src-tauri/geometry/src/lib.rs @@ -0,0 +1,357 @@ +// Wand-Verschneidung (Gehrung): an einer Ecke, wo zwei Waende aufeinander- +// treffen, sollen sich die Schicht-Baender nicht ueberlappen, sondern an einer +// gemeinsamen Gehrungslinie sauber stossen. Dieses Modul berechnet pro Wand +// die optionalen Schnittlinien an Start- und Endpunkt. +// +// Reine 2D-Mathematik, kein externer Kernel noetig. Portiert aus dem +// TS-Modell (joins.ts / geometry.ts); die Vektor-Helfer add/sub/scale/ +// normalize/leftNormal/len/lineIntersect sind unten dupliziert. + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Copy)] +pub struct Vec2 { + pub x: f64, + pub y: f64, +} + +/// Eine unendliche Gerade als Stuetzpunkt + Richtung. +#[derive(Serialize, Deserialize, Clone, Copy)] +pub struct Line { + pub point: Vec2, + pub dir: Vec2, +} + +#[derive(Serialize, Deserialize)] +pub struct WallInput { + pub id: String, + pub start: Vec2, + pub end: Vec2, + pub thickness: f64, + #[serde(rename = "referenceOffset")] + pub reference_offset: f64, +} + +#[derive(Serialize, Deserialize)] +pub struct JoinInput { + pub walls: Vec, +} + +/// Schnittlinien einer Wand an ihren beiden Achsenden. +#[derive(Serialize, Deserialize)] +pub struct WallCuts { + #[serde(rename = "wallId")] + pub wall_id: String, + #[serde(rename = "startCut")] + pub start_cut: Option, + #[serde(rename = "endCut")] + pub end_cut: Option, +} + +// --- Vektor-Helfer ----------------------------------------------------------- + +fn sub(a: Vec2, b: Vec2) -> Vec2 { + Vec2 { x: a.x - b.x, y: a.y - b.y } +} + +fn add(a: Vec2, b: Vec2) -> Vec2 { + Vec2 { x: a.x + b.x, y: a.y + b.y } +} + +fn scale(a: Vec2, s: f64) -> Vec2 { + Vec2 { x: a.x * s, y: a.y * s } +} + +fn len(a: Vec2) -> f64 { + a.x.hypot(a.y) +} + +fn normalize(a: Vec2) -> Vec2 { + let l = len(a); + let l = if l == 0.0 { 1.0 } else { l }; + Vec2 { x: a.x / l, y: a.y / l } +} + +/// Linke Normale (90 Grad gegen den Uhrzeigersinn gedreht). +fn left_normal(a: Vec2) -> Vec2 { + Vec2 { x: -a.y, y: a.x } +} + +/// Kreuzprodukt (Z-Komponente) zweier 2D-Vektoren. +fn cross(p: Vec2, q: Vec2) -> f64 { + p.x * q.y - p.y * q.x +} + +/// Schnittpunkt der Geraden (a + t*da) mit (b + s*db). +/// Liefert None bei (nahezu) parallelen Richtungen. +fn line_intersect(a: Vec2, da: Vec2, b: Vec2, db: Vec2) -> Option { + let denom = cross(da, db); + if denom.abs() < 1e-9 { + return None; // parallel -> kein Schnitt + } + let t = cross(sub(b, a), db) / denom; + Some(add(a, scale(da, t))) +} + +// --- Verschneidung ----------------------------------------------------------- + +/// Rundet eine Koordinate auf ein Gitter, um Endpunkte robust zu gruppieren. +fn round_key(p: Vec2) -> String { + let r = |v: f64| (v * 1e4).round() / 1e4; + format!("{},{}", r(p.x), r(p.y)) +} + +#[derive(Clone, Copy, PartialEq)] +enum WallEndKind { + Start, + End, +} + +struct WallEnd { + wall_id: String, + end: WallEndKind, +} + +/// Achsrichtung start->end, normalisiert. +fn dir_of(w: &WallInput) -> Vec2 { + normalize(sub(w.end, w.start)) +} + +/** + * Berechnet fuer jede Wand die Gehrungs-Schnittlinien. + * Nur L-Ecken (genau zwei Wandenden treffen sich) werden behandelt; freie + * Enden und T-/X-Stoesse bleiben rechtwinklig. + */ +pub fn compute_joins(input: JoinInput) -> Vec { + let walls = input.walls; + + // Ergebnis: pro Wand ein Eintrag, Reihenfolge wie in der Eingabe. + let mut result: Vec = walls + .iter() + .map(|w| WallCuts { + wall_id: w.id.clone(), + start_cut: None, + end_cut: None, + }) + .collect(); + + // Index Wand-Id -> Position im Ergebnis/Eingabe. + let mut index: std::collections::HashMap<&str, usize> = std::collections::HashMap::new(); + for (i, w) in walls.iter().enumerate() { + index.insert(w.id.as_str(), i); + } + + // Knotenkarte: gerundeter Endpunkt -> Liste der dort endenden Wandenden. + // BTreeMap fuer deterministische Reihenfolge. + let mut junctions: std::collections::BTreeMap> = + std::collections::BTreeMap::new(); + let push = |p: Vec2, we: WallEnd, m: &mut std::collections::BTreeMap>| { + m.entry(round_key(p)).or_default().push(we); + }; + for w in &walls { + push( + w.start, + WallEnd { wall_id: w.id.clone(), end: WallEndKind::Start }, + &mut junctions, + ); + push( + w.end, + WallEnd { wall_id: w.id.clone(), end: WallEndKind::End }, + &mut junctions, + ); + } + + for (_key, ends) in &junctions { + // Freies Ende -> kein Schnitt. + if ends.len() == 1 { + continue; + } + // T-/X-Stoesse (>2 Enden): vorerst rechtwinklig lassen. + if ends.len() != 2 { + continue; + } + + let a_idx = match index.get(ends[0].wall_id.as_str()) { + Some(i) => *i, + None => continue, + }; + let b_idx = match index.get(ends[1].wall_id.as_str()) { + Some(i) => *i, + None => continue, + }; + let a = &walls[a_idx]; + let b = &walls[b_idx]; + + let cut = match miter_line(a, ends[0].end, b) { + Some(c) => c, + None => continue, // kollinear -> kein Schnitt + }; + + set_cut(&mut result[a_idx], ends[0].end, cut); + set_cut(&mut result[b_idx], ends[1].end, cut); + } + + result +} + +/// Traegt eine Schnittlinie am passenden Ende einer Wand ein. +fn set_cut(cuts: &mut WallCuts, end: WallEndKind, cut: Line) { + match end { + WallEndKind::Start => cuts.start_cut = Some(cut), + WallEndKind::End => cuts.end_cut = Some(cut), + } +} + +/** + * Gemeinsame Gehrungslinie zweier Waende A, B, die sich im Knoten J treffen. + * Robust gegen beliebige Wicklung und ungleiche Dicken: A's Aussenflaeche wird + * mit der NAECHSTGELEGENEN Flaeche von B verschnitten, A's Innenflaeche mit der + * jeweils anderen. Die Gerade durch beide Eckpunkte ist die Gehrung. + * + * Dicke und Referenzversatz kommen direkt aus WallInput (bereits flach). + */ +fn miter_line(a: &WallInput, a_end: WallEndKind, b: &WallInput) -> Option { + let j = match a_end { + WallEndKind::Start => a.start, + WallEndKind::End => a.end, + }; + + let t_a = a.thickness; + let t_b = b.thickness; + let u_a = dir_of(a); + let u_b = dir_of(b); + let n_a = left_normal(u_a); + let n_b = left_normal(u_b); + + // Referenzlinien-Versatz: liegt die Achse nicht mittig, sind die beiden + // Wandflaechen um diesen Betrag entlang +n verschoben. Fuer "center" + // (Default) ist off=0 -> unveraendert. + let off_a = a.reference_offset; + let off_b = b.reference_offset; + + // Flaechen-Stuetzpunkte am Knoten (linke/rechte Wandseite), inkl. Versatz. + let p_la = add(j, scale(n_a, off_a + t_a / 2.0)); + let p_ra = add(j, scale(n_a, off_a - t_a / 2.0)); + let p_lb = add(j, scale(n_b, off_b + t_b / 2.0)); + let p_rb = add(j, scale(n_b, off_b - t_b / 2.0)); + + // Fuer A's linke Flaeche die naehere B-Flaeche waehlen; A's rechte die andere. + let lb_closer = len(sub(p_la, p_lb)) <= len(sub(p_la, p_rb)); + let b_for_left = if lb_closer { p_lb } else { p_rb }; + let b_for_right = if lb_closer { p_rb } else { p_lb }; + + let c1 = line_intersect(p_la, u_a, b_for_left, u_b)?; + let c2 = line_intersect(p_ra, u_a, b_for_right, u_b)?; + + let dir = sub(c2, c1); + if len(dir) < 1e-9 { + return None; + } + Some(Line { point: c1, dir }) +} + +// --- Tests ------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + fn w(id: &str, sx: f64, sy: f64, ex: f64, ey: f64, thickness: f64) -> WallInput { + WallInput { + id: id.to_string(), + start: Vec2 { x: sx, y: sy }, + end: Vec2 { x: ex, y: ey }, + thickness, + reference_offset: 0.0, + } + } + + fn find<'a>(cuts: &'a [WallCuts], id: &str) -> &'a WallCuts { + cuts.iter().find(|c| c.wall_id == id).expect("wall id present") + } + + #[test] + fn l_corner_shared_miter() { + // Wand A (0,0)-(5,0) und Wand B (5,0)-(5,4) treffen sich in (5,0). + let input = JoinInput { + walls: vec![ + w("A", 0.0, 0.0, 5.0, 0.0, 0.2), + w("B", 5.0, 0.0, 5.0, 4.0, 0.2), + ], + }; + let out = compute_joins(input); + assert_eq!(out.len(), 2); + + let ca = find(&out, "A"); + let cb = find(&out, "B"); + + // A endet im Knoten -> endCut gesetzt; B startet dort -> startCut gesetzt. + let a_cut = ca.end_cut.expect("A endCut set"); + let b_cut = cb.start_cut.expect("B startCut set"); + assert!(ca.start_cut.is_none(), "A startCut is free end"); + assert!(cb.end_cut.is_none(), "B endCut is free end"); + + // Beide Waende teilen sich dieselbe Gehrungslinie. + assert!((a_cut.point.x - b_cut.point.x).abs() < 1e-9); + assert!((a_cut.point.y - b_cut.point.y).abs() < 1e-9); + assert!((a_cut.dir.x - b_cut.dir.x).abs() < 1e-9); + assert!((a_cut.dir.y - b_cut.dir.y).abs() < 1e-9); + + // Sanity: die Gehrung einer 90-Grad-Ecke gleicher Dicke ist die + // Diagonale durch (5,0), Richtung parallel zu (1,1) oder (-1,-1). + let d = normalize(a_cut.dir); + assert!( + (d.x.abs() - d.y.abs()).abs() < 1e-6, + "45-Grad-Gehrung erwartet" + ); + } + + #[test] + fn free_end_no_cut() { + let input = JoinInput { + walls: vec![w("A", 0.0, 0.0, 5.0, 0.0, 0.2)], + }; + let out = compute_joins(input); + assert_eq!(out.len(), 1); + let ca = find(&out, "A"); + assert!(ca.start_cut.is_none()); + assert!(ca.end_cut.is_none()); + } + + #[test] + fn t_junction_no_cut() { + // Drei Enden treffen sich in (5,0): rechtwinklig lassen. + let input = JoinInput { + walls: vec![ + w("A", 0.0, 0.0, 5.0, 0.0, 0.2), + w("B", 5.0, 0.0, 5.0, 4.0, 0.2), + w("C", 5.0, 0.0, 10.0, 0.0, 0.2), + ], + }; + let out = compute_joins(input); + assert_eq!(out.len(), 3); + for id in ["A", "B", "C"] { + let c = find(&out, id); + assert!(c.start_cut.is_none(), "{id} startCut none"); + assert!(c.end_cut.is_none(), "{id} endCut none"); + } + } + + #[test] + fn collinear_pair_no_cut() { + // A (0,0)-(5,0) und B (5,0)-(10,0): parallel -> None. + let input = JoinInput { + walls: vec![ + w("A", 0.0, 0.0, 5.0, 0.0, 0.2), + w("B", 5.0, 0.0, 10.0, 0.0, 0.2), + ], + }; + let out = compute_joins(input); + assert_eq!(out.len(), 2); + for id in ["A", "B"] { + let c = find(&out, id); + assert!(c.start_cut.is_none(), "{id} startCut none"); + assert!(c.end_cut.is_none(), "{id} endCut none"); + } + } +} diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 0000000..007ee29 Binary files /dev/null and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..fa601ba --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,18 @@ +// Tauri-v2-Einstieg. Die eigentliche Geometrie liegt im serde-only Crate +// `geometry`; hier nur die Befehls-Bruecke und der App-Start. + +/// Berechnet die Wand-Gehrungen im Rust-Kern und liefert sie ans Frontend. +#[tauri::command] +async fn compute_joins( + input: geometry::JoinInput, +) -> Result, String> { + Ok(geometry::compute_joins(input)) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .invoke_handler(tauri::generate_handler![compute_joins]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..c6b86c2 --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,5 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + cad_tauri_lib::run() +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..25fd444 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "cad", + "version": "0.1.0", + "identifier": "ch.dossier.cad", + "build": { + "frontendDist": "../dist", + "devUrl": "http://localhost:5187" + }, + "app": { + "windows": [ + { + "title": "cad", + "width": 1400, + "height": 900 + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": ["icons/icon.png"] + } +} diff --git a/src/App.tsx b/src/App.tsx index bf44d51..b6cc502 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -27,11 +27,13 @@ import { import { wallCorners } from "./model/geometry"; import { leftNormal, normalize, scale, sub } from "./model/geometry"; import type { + Drawing2D, DrawingLevel, EdgeGrip, LayerCategory, Project, Vec2, + Wall, } from "./model/types"; import { generatePlan } from "./plan/generatePlan"; import type { CategoryDisplayResolver } from "./plan/generatePlan"; @@ -40,6 +42,12 @@ import type { DxfImportResult } from "./io/dxfParser"; import { setDxfImportTrigger } from "./io/dxfImportHook"; import { ImportDialog, buildDrawings } from "./ui/ImportDialog"; import type { ImportDecision } from "./ui/ImportDialog"; +import { ExportPdfDialog } from "./ui/ExportPdfDialog"; +import type { ExportPdfDecision } from "./ui/ExportPdfDialog"; +import { savePlanPdf } from "./export/exportPdf"; +import { ExportDxfDialog } from "./ui/ExportDxfDialog"; +import type { ExportDxfDecision } from "./ui/ExportDxfDialog"; +import { savePlanDxf } from "./export/exportDxf"; import { PlanView } from "./plan/PlanView"; import type { PlanViewHandle, @@ -52,11 +60,13 @@ import type { DisplayResolver, ViewportContextInfo } from "./viewport/Viewport3D import { ResourceManager } from "./ui/ResourceManager"; import type { ResourceManagerHandlers } from "./ui/ResourceManager"; import { TopBar } from "./ui/TopBar"; +import { TextEditorDialog } from "./ui/TextEditorDialog"; +import { RoomStampEditor } from "./panels/RoomStampEditor"; +import { defaultRoomStamp } from "./model/roomStamp"; import type { DetailLevel, RenderMode, View3d, ViewType } from "./ui/TopBar"; import { StatusBar } from "./ui/StatusBar"; -import { TransformBar } from "./ui/TransformBar"; import { getTool } from "./tools/tools"; -import { computeSnap } from "./tools/snapping"; +import { applyAngleConstraint, computeSnap } from "./tools/snapping"; import { requiredPoints, transformPreview, @@ -64,6 +74,7 @@ import { import type { CopyMode, TransformOp, TransformSelection } from "./tools/transform"; import { DEFAULT_SNAP } from "./tools/types"; import type { + SnapResult, SnapSettings, ToolContext, ToolDraft, @@ -76,19 +87,25 @@ import type { import { ContextMenu, MenuIcon } from "./ui/ContextMenu"; import type { ContextMenuItem } from "./ui/ContextMenu"; import { CommandLine } from "./ui/CommandLine"; -import type { CommandLineHandle } from "./ui/CommandLine"; +import type { CommandLineHandle, CommandLineField, CommandLineOption } from "./ui/CommandLine"; import { CommandEngine } from "./commands/engine"; import type { EngineHost } from "./commands/engine"; import type { CommandContext } from "./commands/types"; import { t, useT } from "./i18n"; import { cutSegment, - joinDrawings, nearestEdgeIndex, splitDrawing, toPolyForm, } from "./editors/splitJoin"; import type { PolyForm } from "./editors/splitJoin"; +import { + countClosedAreas, + differenceDrawings, + intersectionDrawings, + unionDrawings, +} from "./editors/booleanOps"; +import type { BooleanResult } from "./editors/booleanOps"; // Globaler Store (Slices: project/selection/view/layout). import { useStore } from "./state/appStore"; @@ -113,6 +130,9 @@ import type { } from "./panels/types"; import type { PanelHostValue } from "./panels/host"; import { deriveSelection } from "./state/selectionInfo"; +import { centroid as roomCentroid } from "./geometry/roomArea"; +import { docFromText } from "./text/richText"; +import type { RichTextDoc } from "./text/richText"; import { deleteNamedLayout, listNamedLayouts, @@ -153,6 +173,30 @@ type EditorState = | { kind: "level"; id: string; x: number; y: number } | null; +/** + * GUI-Werkzeug → Befehlsname der Engine. Die Zeichen-Werkzeuge der Palette sind + * in Wahrheit Engine-Befehle: ein Klick auf das Werkzeug startet diesen Befehl + * (statt das Legacy-Tool zu fahren). „select" hat (noch) keinen Befehl und bleibt + * der Auswahl-Modus. „wall" ist jetzt ebenfalls ein Befehl (`wall`) — Klick und + * getipptes „wall"/„wand" münden in denselben Befehl; der Legacy-Wand-Pfad ist + * damit deaktiviert (siehe toolHandlers/onSelectTool). + */ +const TOOL_COMMAND: Partial> = { + wall: "wall", + ceiling: "ceiling", + window: "fenster", + door: "tuer", + stair: "stair", + room: "room", + line: "line", + polyline: "polyline", + rect: "rect", +}; +/** Befehlsname für ein gekoppeltes Werkzeug (oder null, wenn keiner). */ +function coupledCommandFor(id: ToolId): string | null { + return TOOL_COMMAND[id] ?? null; +} + /** Fragt die Kopienanzahl (≥1) für die Modi O/P ab; null bei Abbruch/ungültig. */ function promptCount(): number | null { const s = window.prompt(t("transform.countPrompt"), "3"); @@ -191,6 +235,9 @@ export default function App() { // true, solange DWG noch geparst wird (WASM lädt/läuft). loading?: boolean; } | null>(null); + // Modaler PDF-Export-Dialog (Grundriss → Vektor-PDF). true = offen. + const [exportOpen, setExportOpen] = useState(false); + const [exportDxfOpen, setExportDxfOpen] = useState(false); // Visuelles Drop-Feedback (dezenter Rahmen), solange eine Datei über die App // gezogen wird. Zählt dragenter/dragleave aus, um Flackern bei Kind-Elementen // zu vermeiden. @@ -245,6 +292,9 @@ export default function App() { count: number; points: Vec2[]; } | null>(null); + // Letzter gesnappter Cursor der laufenden Transformation — für das Live-Feld + // (Winkel/Distanz) der Befehlszeile (analog gripDragInfoRef beim Vertex-Drag). + const transformDragInfoRef = useRef<{ cursor: Vec2 } | null>(null); // ── Auswahl (Store-Slice) ───────────────────────────────────────────────── const selectedWallIds = useStore((s) => s.selectedWallIds); @@ -261,6 +311,24 @@ export default function App() { setSelectedDrawingIds(id ? [id] : []); // Bequemer Einzel-Selektor (erste gewählte Wand). const selectedWallId = selectedWallIds[0] ?? null; + // Decken-Auswahl (MENGE); für die Einzel-Element-Maschinerie der „eine" + // gewählte (bei genau einem Element). + const selectedCeilingIds = useStore((s) => s.selectedCeilingIds); + const setSelectedCeilingIds = useStore((s) => s.setSelectedCeilingIds); + const selectedCeilingId = + selectedCeilingIds.length === 1 ? selectedCeilingIds[0] : null; + const selectedOpeningIds = useStore((s) => s.selectedOpeningIds); + const setSelectedOpeningIds = useStore((s) => s.setSelectedOpeningIds); + const selectedOpeningId = + selectedOpeningIds.length === 1 ? selectedOpeningIds[0] : null; + const selectedStairIds = useStore((s) => s.selectedStairIds); + const setSelectedStairIds = useStore((s) => s.setSelectedStairIds); + const selectedStairId = + selectedStairIds.length === 1 ? selectedStairIds[0] : null; + const selectedRoomIds = useStore((s) => s.selectedRoomIds); + const setSelectedRoomIds = useStore((s) => s.setSelectedRoomIds); + const selectedRoomId = + selectedRoomIds.length === 1 ? selectedRoomIds[0] : null; // ── Kontextmenüs (transienter UI-State, bleibt lokal) ───────────────────── // Eigenschaften-Zwischenablage des Ebenen-Kontextmenüs (Farbe + Linienstärke). @@ -272,6 +340,8 @@ export default function App() { const [menu, setMenu] = useState(null); // Offener Inline-Editor (Ebene bzw. Geschoss) — aus einem Menüpunkt geöffnet. const [editor, setEditor] = useState(null); + // Offener Raum-Stempel-Editor (Rich-Text) — per Doppelklick/Button geöffnet. + const [stampEditorRoomId, setStampEditorRoomId] = useState(null); // Beim Geschosswechsel die Wand-Auswahl + offene Overlays zurücksetzen (die // Wände eines anderen Geschosses sind eine andere Menge). Der Ansichtswechsel @@ -280,6 +350,10 @@ export default function App() { useEffect(() => { setSelectedWallIds([]); setSelectedDrawingId(null); + setSelectedCeilingIds([]); + setSelectedOpeningIds([]); + setSelectedStairIds([]); + setSelectedRoomIds([]); setMenu(null); setEditor(null); }, [activeLevelId]); @@ -334,12 +408,6 @@ export default function App() { project.drawingLevels.find((z) => z.id === activeLevelId) ?? project.drawingLevels[0]; - // TEMP-PROBE-HOOK (split/join verification) — wird nach der Verifikation entfernt. - useEffect(() => { - (window as unknown as { __drawings2d?: unknown }).__drawings2d = - project.drawings2d.map((d) => ({ id: d.id, geom: d.geom })); - }, [project.drawings2d]); - // Sichtbare Kategorie-Codes — Basisfilter für Grundriss & 3D. const visibleCodes = useMemo( () => collectVisibleCodes(project.layers), @@ -453,6 +521,9 @@ export default function App() { const tr = activeTransform!; const snapResult = snapForTransform(raw, pxPerMeter, mods, tr.points); const cursor = snapResult?.point ?? raw; + // Letzter gesnappter Cursor — speist das Live-Feld (Winkel/Distanz) der + // Befehlszeile, analog zu gripDragInfoRef beim Vertex-Drag. + transformDragInfoRef.current = { cursor }; const need = requiredPoints(tr.op); const eff = [...tr.points, cursor].slice(0, need); const preview = @@ -470,6 +541,7 @@ export default function App() { commitTransformOn(transformSel, tr.op, points, tr.copyMode, tr.count); setActiveTransform(null); setDraft(null); + transformDragInfoRef.current = null; } else { setActiveTransform({ ...tr, points }); } @@ -478,17 +550,33 @@ export default function App() { const toolHandlers: ToolHandlers = { onToolMove: (raw, pxPerMeter, mods) => { // Läuft ein Befehl, übernimmt die Engine die Plan-Eingabe (ein Eingabepfad). + // Modifikatoren (Shift=Ortho, Ctrl=Snap aus) mitgeben, damit auch der ERSTE + // Punkt sauber snappt bzw. unterdrückt werden kann. if (engineRef.current?.isActive()) { - engineRef.current.move(raw); + engineRef.current.move(raw, { shift: mods.shift, ctrl: mods.ctrl }); return; } + // Gekoppeltes Werkzeug (Wand/Linie/Rechteck/Polylinie) ohne laufenden + // Befehl: KEIN Legacy-Zeichnen — nur Hover (die Engine startet beim Klick neu). + if (coupledCommandFor(activeTool)) return; activeTransform ? transformMove(raw, pxPerMeter, mods) : runToolStep("move", raw, pxPerMeter, mods); }, onToolClick: (raw, pxPerMeter, mods) => { + const em = { shift: mods.shift, ctrl: mods.ctrl }; if (engineRef.current?.isActive()) { - engineRef.current.pick(raw); + engineRef.current.pick(raw, em); + return; + } + // Gekoppeltes Werkzeug, aber der Befehl ist (nach Commit) beendet: denselben + // Befehl neu starten und den Klick als ersten Punkt durchreichen — so bleibt + // das Werkzeug wie in einem CAD armiert, ohne dass der Legacy-Pfad zeichnet. + // Der erste Punkt snappt so ebenfalls (mods durchgereicht). + const cmd = coupledCommandFor(activeTool); + if (cmd) { + engineRef.current?.start(cmd); + engineRef.current?.pick(raw, em); return; } activeTransform @@ -529,10 +617,12 @@ export default function App() { activeLevel, activeCategoryCode, activeLineStyleId, + activeWallTypeId, visibleCodes, snap, selectedWallIds, selectedDrawingId, + selectedDrawingIds, setProject, setDraft, }); @@ -541,10 +631,12 @@ export default function App() { activeLevel, activeCategoryCode, activeLineStyleId, + activeWallTypeId, visibleCodes, snap, selectedWallIds, selectedDrawingId, + selectedDrawingIds, setProject, setDraft, }; @@ -559,16 +651,18 @@ export default function App() { level: c.activeLevel, defaultCategoryCode: c.activeCategoryCode, activeLineStyleId: c.activeLineStyleId, + activeWallTypeId: c.activeWallTypeId, lastPoint, // Vorab gesetzte Auswahl aus dem Store (Rhino: erst selektieren, dann // Befehl). Move/Copy/Offset operieren darauf. selection: { wallIds: c.selectedWallIds, drawingId: c.selectedDrawingId, + drawingIds: c.selectedDrawingIds, }, }; }, - snap: (raw, draftPoints) => { + snap: (raw, draftPoints, mods) => { const c = cmdLatest.current; return computeSnap({ raw, @@ -579,8 +673,11 @@ export default function App() { draftPoints, lastPoint: draftPoints.length ? draftPoints[draftPoints.length - 1] : null, pxPerMeter: planViewRef.current?.pxPerMeter() ?? 90, - shift: false, - ctrl: false, + // Shift = Ortho, Ctrl = Snap aus — auch für den ERSTEN Punkt (ohne + // Draft): so fängt der Startpunkt einer neuen Linie/Polylinie/Wand an + // vorhandene Endpunkte/Mitten, statt erst ab dem zweiten Punkt. + shift: mods?.shift ?? false, + ctrl: mods?.ctrl ?? false, }); }, pxPerMeter: () => planViewRef.current?.pxPerMeter() ?? 90, @@ -603,6 +700,46 @@ export default function App() { const engineView = engine.view(); const commandActive = engineView.active; + // ── GUI-Werkzeug ↔ Befehls-Engine (ein Eingabe-Hub) ─────────────────────── + // Ein Klick auf ein gekoppeltes Zeichen-Werkzeug (Wand/Linie/Rechteck/Polylinie) + // STARTET den passenden Befehl in der Engine und fokussiert das Befehlsfeld + // („Dialog drin"). „Auswahl" beendet einen laufenden Befehl (Auswahl-Modus). + const onSelectTool = (id: ToolId) => { + const cmd = coupledCommandFor(id); + if (cmd) { + // Den gekoppelten Befehl in DERSELBEN Engine starten wie das Tippen von + // „line" — Klick und Tippen münden in denselben Befehl. Das aktive-Tool- + // Highlight spiegelt den laufenden Befehl (activeTool = id). Der Legacy- + // Zeichenpfad ist deaktiviert, solange ein Befehl läuft (toolHandlers + // leiten Move/Pick/Confirm an die Engine, siehe oben). + engine.start(cmd); + setActiveTool(id); + // Befehlsfeld fokussieren: getippte Präzisionseingabe steht sofort bereit. + // Nach dem Render des aktiven Prompts fokussieren (nächster Tick). + requestAnimationFrame(() => commandLineRef.current?.focus()); + return; + } + // „Auswahl" oder ein Legacy-Werkzeug (Wand): laufenden Befehl beenden. + if (engine.isActive()) engine.cancel(); + setActiveTool(id); + }; + + // 3D-Viewport als Erstellungs-Fläche: der Viewport projiziert den Cursor auf + // die Geschoss-Arbeitsebene und liefert einen rohen Modell-`Vec2`; wir füttern + // ihn in DIESELBE ansichts-agnostische CommandEngine wie PlanView (move/pick). + // Snapping/Tab-Felder/Commit laufen unverändert über den EngineHost; die neuen + // Elemente landen auf dem aktiven Geschoss (host.context().level). + const onWorkplanePoint = (kind: "move" | "pick", model: Vec2) => { + const eng = engineRef.current; + if (!eng?.isActive()) return; + if (kind === "move") eng.move(model); + else eng.pick(model); + }; + const onWorkplaneConfirm = () => { + const eng = engineRef.current; + if (eng?.isActive()) eng.confirm(); + }; + // Globaler Tab-Handler: läuft ein Befehl MIT Feldern (§2.7), zyklt Tab das // aktive Feld (Präzisionseingabe), ohne den Fokus zu stehlen — so kann man bei // der Maus bleiben. Sonst fokussiert/öffnet Tab die Command-Line. Gleicher @@ -624,6 +761,15 @@ export default function App() { eng.cycleField(); return; } + // (c) Läuft ein Vertex-Griff-Drag, öffnet/zyklt Tab das Editier-Feld + // (Länge/Winkel relativ zur Nachbarecke). Beim ersten Tab wird zusätzlich + // das Befehlsfeld fokussiert, damit man den Zahlenwert direkt tippen kann. + if (gripDragInfoRef.current) { + const wasOpen = gripEditRef.current !== null; + cycleGripEditFieldRef.current?.(); + if (!wasOpen) commandLineRef.current?.focus(); + return; + } commandLineRef.current?.focus(); }; window.addEventListener("keydown", onKey); @@ -640,6 +786,10 @@ export default function App() { if (el && (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.isContentEditable)) { return; } + // Läuft ein gekoppelter Befehl (Linie/Rechteck/Polylinie über die Engine), + // gehören Esc/Enter/Backspace der Engine bzw. dem Befehlsfeld — der Legacy- + // Werkzeug-Pfad bleibt stumm (keine doppelte Mechanik). + if (engineRef.current?.isActive()) return; const tool = getTool(activeTool); const drawing = toolStateRef.current.phase !== "idle"; if (e.key === "Escape") { @@ -675,14 +825,53 @@ export default function App() { () => project.drawings2d.find((d) => d.id === selectedDrawingId) ?? null, [project.drawings2d, selectedDrawingId], ); + const selectedCeiling = useMemo( + () => (project.ceilings ?? []).find((c) => c.id === selectedCeilingId) ?? null, + [project.ceilings, selectedCeilingId], + ); + const selectedRoom = useMemo( + () => (project.rooms ?? []).find((r) => r.id === selectedRoomId) ?? null, + [project.rooms, selectedRoomId], + ); + // Stempel-Griff des (einzeln) gewählten Raums: sein fixer Anker (Meter) + ID. + const roomStamp = useMemo( + () => + selectedRoom + ? { + roomId: selectedRoom.id, + anchor: selectedRoom.stampAnchor ?? roomCentroid(selectedRoom.boundary), + } + : null, + [selectedRoom], + ); const grips: Vec2[] = useMemo(() => { if (selectedDrawing) return drawingVertices(selectedDrawing); if (selectedWallIds.length === 1 && !selectedDrawingId) { const w = project.walls.find((x) => x.id === selectedWallIds[0]); if (w) return [w.start, w.end]; } + // Decke: die Umriss-Vertices als Griffe (nur bei genau einer gewählten Decke). + if (selectedCeiling && !selectedDrawingId && selectedWallIds.length === 0) { + return [...selectedCeiling.outline]; + } + // Raum: die Umriss-Vertices als Griffe (nur bei genau einem gewählten Raum). + if ( + selectedRoom && + !selectedDrawingId && + selectedWallIds.length === 0 && + !selectedCeiling + ) { + return [...selectedRoom.boundary]; + } return []; - }, [selectedDrawing, selectedWallIds, selectedDrawingId, project.walls]); + }, [ + selectedDrawing, + selectedCeiling, + selectedRoom, + selectedWallIds, + selectedDrawingId, + project.walls, + ]); // ── Kanten-/Seiten-Griffe (Edge Grips) ─────────────────────────────────── // Pro Seite des selektierten Elements ein Griff: Kanten-Mittelpunkt + nach @@ -695,8 +884,10 @@ export default function App() { // inkl. Schlusskante (letzter → erster). // • Wand: eine Kante [0,1]; Außennormale = leftNormal der Achse. Ziehen // verschiebt beide Enden (ganze Wand senkrecht). - // • offene Polylinie/Linie: bewusst KEINE Kanten-Griffe (kein „außen"/„innen" - // bei offener Geometrie; Eckpunkt-Griffe genügen dort). + // • offene Polylinie/Linie: je Segment ein Kanten-Griff. Ziehen verschiebt DAS + // Segment (seine beiden Endpunkte) um das volle Drag-Delta — die geteilten + // Ecken wandern mit, die Nachbar-Segmente dehnen sich nach (Parallel- + // Verschieben EINER Kante einer komplexen Polylinie, §Task B). const edgeGrips: EdgeGrip[] = useMemo(() => { // Geschlossener Ring aus Vertices → Kanten-Griffe mit Außennormale. const ringEdges = (verts: Vec2[]): EdgeGrip[] => { @@ -720,10 +911,29 @@ export default function App() { } return out; }; + // Offene Kette (Linie/offene Polylinie) → ein Griff je Segment, OHNE + // Schlusskante. `normal` dient nur der Griff-Darstellung (leftNormal der + // Kante); die Bewegung ist frei (volles Delta), nicht auf die Normale + // beschränkt — daher `free: true`. + const openEdges = (verts: Vec2[]): EdgeGrip[] => { + const n = verts.length; + if (n < 2) return []; + const out: EdgeGrip[] = []; + for (let i = 0; i < n - 1; i++) { + const a = verts[i]; + const b = verts[i + 1]; + const mid: Vec2 = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }; + const nrm = normalize(leftNormal(sub(b, a))); + out.push({ mid, normal: nrm, aIndex: i, bIndex: i + 1, free: true }); + } + return out; + }; if (selectedDrawing) { const g = selectedDrawing.geom; if (g.shape === "rect") return ringEdges(grips); if (g.shape === "polyline" && g.closed) return ringEdges(grips); + if (g.shape === "polyline" && !g.closed) return openEdges(grips); + if (g.shape === "line") return openEdges(grips); return []; } if (selectedWallIds.length === 1 && !selectedDrawingId) { @@ -734,23 +944,246 @@ export default function App() { return [{ mid, normal: nrm, aIndex: 0, bIndex: 1 }]; } } + // Decke: geschlossener Ring aus dem Umriss. + if (selectedCeiling && !selectedDrawingId && selectedWallIds.length === 0) { + return ringEdges(grips); + } + // Raum: geschlossener Ring aus dem Umriss. + if ( + selectedRoom && + !selectedDrawingId && + selectedWallIds.length === 0 && + !selectedCeiling + ) { + return ringEdges(grips); + } return []; - }, [selectedDrawing, grips, selectedWallIds, selectedDrawingId, project.walls]); + }, [ + selectedDrawing, + selectedCeiling, + selectedRoom, + grips, + selectedWallIds, + selectedDrawingId, + project.walls, + ]); // Snapping beim Griff-Ziehen: das gezogene Element selbst aus den Quellen // nehmen (sonst fängt der Punkt an sich selbst). Live-Mutation über setProject. // Zuletzt angewandter Kanten-Offset während eines Edge-Grip-Drags (Anker). // null = kein laufender Drag; der erste Move-Event verankert nur (kein Sprung). const edgeDragOffsetRef = useRef(null); + // Freies Kanten-Ziehen (offene Polylinie/Linie): letzter roher Cursor, gegen den + // das inkrementelle 2D-Delta gebildet wird. null = Drag noch nicht verankert. + const edgeFreeLastRef = useRef(null); + // Körper-Verschieben (2D): koinzidente Endpunkte ANDERER gewählter Elemente, die + // an einer Ecke des verschobenen Elements hängen, werden um dasselbe Delta + // mitgeführt (gemeinsame Ecke bleibt verbunden). Lazy beim ersten Move-Event + // bestimmt (Snapshot der Selektion), je Drag in onGripEnd zurückgesetzt. null = + // noch nicht bestimmt. + const moveCoincidentRef = useRef< + | { drawingId: string | null; wallId: string | null; index: number; pos: Vec2 }[] + | null + >(null); + // Eckpunkte eines selektierten Elements (Wand: start/end; 2D: drawingVertices). + const elementCorners = (drawingId: string | null, wallId: string | null): Vec2[] => { + if (drawingId) { + const d = project.drawings2d.find((x) => x.id === drawingId); + return d ? drawingVertices(d) : []; + } + if (wallId) { + const w = project.walls.find((x) => x.id === wallId); + return w ? [w.start, w.end] : []; + } + return []; + }; + // Koinzidente Sibling-Vertices (unter den ÜBRIGEN gewählten Elementen) an JEDER + // Ecke des verschobenen Elements. Toleranz wie beim 3D-Drag (gemeinsame Ecke). + const COINCIDENT_TOL_2D = 0.05; // m + const findMoveCoincident2D = ( + selfDrawingId: string | null, + selfWallId: string | null, + ): { drawingId: string | null; wallId: string | null; index: number; pos: Vec2 }[] => { + const corners = elementCorners(selfDrawingId, selfWallId); + if (corners.length === 0) return []; + const near = (p: Vec2, q: Vec2) => Math.hypot(p.x - q.x, p.y - q.y) <= COINCIDENT_TOL_2D; + const seen = new Set(); + const out: { drawingId: string | null; wallId: string | null; index: number; pos: Vec2 }[] = []; + const add = (drawingId: string | null, wallId: string | null, index: number, pos: Vec2) => { + const key = `${drawingId ?? ""}|${wallId ?? ""}|${index}`; + if (seen.has(key)) return; + seen.add(key); + out.push({ drawingId, wallId, index, pos: { ...pos } }); + }; + for (const corner of corners) { + for (const id of selectedWallIds) { + if (id === selfWallId) continue; + const w = project.walls.find((x) => x.id === id); + if (!w) continue; + if (near(corner, w.start)) add(null, id, 0, w.start); + if (near(corner, w.end)) add(null, id, 1, w.end); + } + for (const id of selectedDrawingIds) { + if (id === selfDrawingId) continue; + const d = project.drawings2d.find((x) => x.id === id); + if (!d) continue; + drawingVertices(d).forEach((v, i) => { + if (near(corner, v)) add(id, null, i, v); + }); + } + } + return out; + }; + // ── (c) Präzise Werteingabe beim Editieren (Tab im Vertex-Griff-Drag) ─────── + // Editieren läuft AD-HOC (gripHandlers → moveGripOf), NICHT durch die Engine. + // Damit „Tab → Länge/Winkel" wie beim Zeichnen funktioniert, ohne die Engine + // umzubauen, hält App einen kleinen FELD-Controller, der NUR während eines + // Vertex-Griff-Drags aktiv ist: er belegt das Befehlsfeld (Prompt/Felder/ + // Eingabe) und liefert gelockte Länge/Winkel relativ zum Anker (Nachbarecke). + // `onGripMove` liest diese Locks und zwingt den Zielpunkt darauf — so setzt ein + // getippter Wert den exakten Endpunkt. Foundation: heute 2D-Vertex; dieselbe + // Lock-Auflösung (polar um den Anker) ist auf 3D-Vertex/Körper erweiterbar. + const [gripEdit, setGripEdit] = useState<{ + /** Fixer Bezug (Nachbarecke), relativ zu dem Länge/Winkel gelten. */ + anchor: Vec2; + /** Aktives Tab-Feld (zyklt length ↔ angle). */ + active: "length" | "angle"; + /** Gelockte Werte (getippt). length in m, angle in Grad. */ + locks: { length?: number; angle?: number }; + } | null>(null); + // Live-Spiegel für Handler ohne Re-Render-Abhängigkeit (Drag liest synchron). + const gripEditRef = useRef(null); + gripEditRef.current = gripEdit; + // Letzter roher Cursor des laufenden Vertex-Drags (für die Lock-Auflösung beim + // Tippen, wenn gerade kein Move-Event kommt). Plus der gezogene Griff-Index. + const gripDragInfoRef = useRef<{ index: number; raw: Vec2 } | null>(null); + + // Endpunkt aus Anker + (gelockten/Live-)Länge/Winkel — wie lineEndFromFields, + // hier für den Vertex-Drag. Ungelockte Größen folgen dem rohen Cursor `raw`. + const gripEditPoint = ( + anchor: Vec2, + locks: { length?: number; angle?: number }, + raw: Vec2, + ): Vec2 => { + const DEG = Math.PI / 180; + const dx = raw.x - anchor.x; + const dy = raw.y - anchor.y; + const length = locks.length != null ? locks.length : Math.hypot(dx, dy); + const angleDeg = locks.angle != null ? locks.angle : (Math.atan2(dy, dx) * 180) / Math.PI; + const a = angleDeg * DEG; + return { x: anchor.x + Math.cos(a) * length, y: anchor.y + Math.sin(a) * length }; + }; + + // Felder fürs Befehlsfeld während des Vertex-Drags (Live ODER gelockt). + const gripEditFields = (): { + id: string; + labelKey: string; + value: number | null; + locked: boolean; + active: boolean; + }[] => { + const ge = gripEdit; + const info = gripDragInfoRef.current; + if (!ge || !info) return []; + const dx = info.raw.x - ge.anchor.x; + const dy = info.raw.y - ge.anchor.y; + const liveLen = Math.hypot(dx, dy); + const liveAng = ((((Math.atan2(dy, dx) * 180) / Math.PI) % 360) + 360) % 360; + return [ + { + id: "length", + labelKey: "cmd.field.length", + value: ge.locks.length ?? liveLen, + locked: ge.locks.length != null, + active: ge.active === "length", + }, + { + id: "angle", + labelKey: "cmd.field.angle", + value: ge.locks.angle ?? liveAng, + locked: ge.locks.angle != null, + active: ge.active === "angle", + }, + ]; + }; + + // Stabiler Ref auf cycleGripEditField (der globale Tab-Handler hat leere Deps). + const cycleGripEditFieldRef = useRef<() => void>(() => {}); + // Tab im Vertex-Drag: Feld-Controller öffnen/zyklen (length ↔ angle). + const cycleGripEditField = () => { + const info = gripDragInfoRef.current; + if (!info) return; + const refIndex = info.index > 0 ? info.index - 1 : grips.length > 1 ? 1 : -1; + const anchor = refIndex >= 0 ? grips[refIndex] : null; + if (!anchor) return; // ohne Anker keine polare Eingabe möglich + setGripEdit((prev) => + prev + ? { ...prev, active: prev.active === "length" ? "angle" : "length" } + : { anchor, active: "length", locks: {} }, + ); + }; + cycleGripEditFieldRef.current = cycleGripEditField; + + // Getippte Zahl im Vertex-Drag: aktives Feld locken, aufs nächste vorrücken und + // den Zielpunkt sofort mit dem gelockten Wert neu setzen. + const submitGripEditValue = (value: number) => { + const ge = gripEditRef.current; + const info = gripDragInfoRef.current; + if (!ge || !info) return; + const locks = { ...ge.locks, [ge.active]: value }; + const next = ge.active === "length" ? "angle" : "length"; + setGripEdit({ ...ge, locks, active: next }); + // Zielpunkt mit dem frischen Lock anwenden (live, ohne Maus-Bewegung). + const pt = gripEditPoint(ge.anchor, locks, info.raw); + applyGrip(info.index, pt); + }; + + // Editier-Dispatch: bei gewählter Decke auf die Decken-Aktionen umleiten, sonst + // auf die Wand/2D-Aktionen. So teilen Plan- UND 3D-Griffe denselben Pfad. + const applyGrip = (index: number, pt: Vec2) => { + if (selectedRoomId) moveRoomGrip(selectedRoomId, index, pt); + else if (selectedCeilingId) moveCeilingGrip(selectedCeilingId, index, pt); + else moveGripOf(selectedDrawingId, selectedWallId, index, pt); + }; + const applyEdge = (aIndex: number, bIndex: number, delta: Vec2) => { + if (selectedRoomId) moveRoomEdge(selectedRoomId, aIndex, bIndex, delta); + else if (selectedCeilingId) moveCeilingEdge(selectedCeilingId, aIndex, bIndex, delta); + else moveEdgeOf(selectedDrawingId, selectedWallId, aIndex, bIndex, delta); + }; + const gripHandlers = { onGripMove: (index: number, raw: Vec2, pxPerMeter: number, mods: ToolMods) => { - const snapProject: Project = selectedDrawingId + // (c) Vertex-Drag mit aktivem Feld-Controller + Locks: den Zielpunkt polar + // aus Anker + gelockter Länge/Winkel bilden (ungelockt folgt dem Cursor). + const ge = gripEditRef.current; + gripDragInfoRef.current = { index, raw }; + if (ge && (ge.locks.length != null || ge.locks.angle != null)) { + const pt = gripEditPoint(ge.anchor, ge.locks, raw); + applyGrip(index, pt); + setDraft({ preview: [], vertices: [], snap: null }); + return; + } + const snapProject: Project = selectedRoomId + ? { ...project, rooms: (project.rooms ?? []).filter((r) => r.id !== selectedRoomId) } + : selectedCeilingId + ? { ...project, ceilings: (project.ceilings ?? []).filter((c) => c.id !== selectedCeilingId) } + : selectedDrawingId ? { ...project, drawings2d: project.drawings2d.filter((d) => d.id !== selectedDrawingId) } : { ...project, walls: project.walls.filter((w) => w.id !== selectedWallId) }; // Bezugspunkt für Shift-Ortho: ein Nachbar-Griff (festes Ende), relativ zu - // dem die Bewegung auf H/V (bzw. Winkelraster) eingerastet wird. + // dem die Bewegung auf H/V eingerastet wird. const refIndex = index > 0 ? index - 1 : grips.length > 1 ? 1 : -1; - const lastPoint = refIndex >= 0 ? grips[refIndex] : null; + const anchor = refIndex >= 0 ? grips[refIndex] : null; + // Shift = Ortho-Lock: den gezogenen Punkt auf H/V gegenüber dem FIXEN Anker + // zwingen (Achse = die nähere; applyAngleConstraint mit 90°, wie Ortho beim + // Zeichnen). Der Lock dominiert (kein Snap), damit die Endkoordinate + // garantiert eine Achse mit dem Anker teilt — identisch zum 3D-Vertex-Drag. + if (mods.shift && anchor) { + const pt = applyAngleConstraint(anchor, raw, 90); + applyGrip(index, pt); + setDraft({ preview: [], vertices: [], snap: null }); + return; + } const snapResult = computeSnap({ raw, project: snapProject, @@ -758,24 +1191,82 @@ export default function App() { visibleCodes, settings: snap, draftPoints: [], - lastPoint, + lastPoint: anchor, pxPerMeter, shift: mods.shift, ctrl: mods.ctrl, }); const pt = snapResult?.point ?? raw; - moveGripOf(selectedDrawingId, selectedWallId, index, pt); + applyGrip(index, pt); setDraft({ preview: [], vertices: [], snap: snapResult }); }, - onMoveBody: (delta: Vec2) => { - moveElementByOf(selectedDrawingId, selectedWallId, delta); + onMoveBody: (delta: Vec2, target?: { wallId?: string; drawingId?: string }) => { + // Decke: ganzen Umriss verschieben (kein Multi-Koinzidenz-Handling nötig). + if (selectedCeilingId && !target) { + moveCeilingBy(selectedCeilingId, delta); + return; + } + // Treppe: ganzen Antritt (+ Zentrum) verschieben. + if (selectedStairId && !target) { + moveStairBy(selectedStairId, delta); + return; + } + // Raum: ganzen Umriss (+ Stempel-Anker) verschieben. + if (selectedRoomId && !target) { + moveRoomBy(selectedRoomId, delta); + return; + } + // Gegriffenes Element: bei Mehrfachauswahl das unter dem Cursor (target), + // sonst das einzeln selektierte. Wand UND 2D schließen sich aus. + const moveDrawingId = target ? target.drawingId ?? null : selectedDrawingId; + const moveWallId = target ? target.wallId ?? null : selectedWallId; + moveElementByOf(moveDrawingId, moveWallId, delta); + // Beim ersten Move-Event die Koinzidenz-Gruppe einmalig bestimmen (Snapshot + // VOR der ersten Mutation der Siblings). Danach inkrementell mitführen. + if (moveCoincidentRef.current === null) { + moveCoincidentRef.current = findMoveCoincident2D(moveDrawingId, moveWallId); + } + for (const c of moveCoincidentRef.current) { + c.pos = { x: c.pos.x + delta.x, y: c.pos.y + delta.y }; + moveGripOf(c.drawingId, c.wallId, c.index, c.pos); + } }, - // Kanten-Griff ziehen: NUR die Komponente senkrecht zur Kante wird wirksam - // (die Seite verschiebt sich parallel nach außen/innen). Der Offset wird als - // Skalarprojektion von (raw − mid) auf die Außennormale gebildet und — wenn - // der Snap aktiv ist (kein Ctrl) — aufs Raster gerundet. So bleibt die - // Geste robust und achsentreu, ohne dass ein Eckpunkt verkantet. + // Kanten-Griff ziehen. Zwei Verhalten: + // • free (offene Polylinie/Linie, §Task B): das Segment folgt dem VOLLEN + // Cursor-Delta — beide Endpunkte wandern gleich, die geteilten Ecken ziehen + // die Nachbar-Segmente nach. Shift = Ortho (H/V) auf das Gesamt-Delta. + // • sonst (rect/geschlossene Polylinie/Wand): NUR die Komponente senkrecht + // zur Kante (Seite verschiebt sich parallel nach außen/innen), aufs Raster + // gerundet — wie bisher. onEdgeMove: (edge: EdgeGrip, raw: Vec2, _pxPerMeter: number, mods: ToolMods) => { + if (edge.free) { + // Inkrementelles 2D-Delta gegen den letzten rohen Cursor. Erster Event + // verankert nur (kein Sprung, da der Griff am Kanten-Mittelpunkt sitzt). + const last = edgeFreeLastRef.current; + edgeFreeLastRef.current = raw; + if (last === null) return; + let dx = raw.x - last.x; + let dy = raw.y - last.y; + // Raster-Schritt auf das Delta (Snap aktiv, nicht per Ctrl unterdrückt). + const gs = snap.gridSize; + if (!mods.ctrl && snap.enabled && snap.grid && gs > 0) { + dx = Math.round(dx / gs) * gs; + dy = Math.round(dy / gs) * gs; + if (dx === 0 && dy === 0) { + edgeFreeLastRef.current = last; // noch kein voller Rasterschritt → warten + return; + } + } + // Shift = Ortho: das Delta auf die dominante Achse (H/V) zwingen. + if (mods.shift) { + if (Math.abs(dx) >= Math.abs(dy)) dy = 0; + else dx = 0; + } + if (dx === 0 && dy === 0) return; + applyEdge(edge.aIndex, edge.bIndex, { x: dx, y: dy }); + setDraft({ preview: [], vertices: [], snap: null }); + return; + } // Senkrechte Projektion von (raw − Grab-Mittelpunkt) auf die Außennormale. const rel = sub(raw, edge.mid); let offset = rel.x * edge.normal.x + rel.y * edge.normal.y; @@ -793,12 +1284,16 @@ export default function App() { if (prev === null) return; const inc = offset - prev; if (inc === 0) return; - moveEdgeOf(selectedDrawingId, selectedWallId, edge.aIndex, edge.bIndex, scale(edge.normal, inc)); + applyEdge(edge.aIndex, edge.bIndex, scale(edge.normal, inc)); setDraft({ preview: [], vertices: [], snap: null }); }, onGripEnd: () => { setDraft(null); edgeDragOffsetRef.current = null; // Drag beendet → Anker zurücksetzen + edgeFreeLastRef.current = null; // freien Kanten-Drag-Anker zurücksetzen + moveCoincidentRef.current = null; // Körper-Koinzidenz-Gruppe verwerfen + gripDragInfoRef.current = null; // (c) Vertex-Drag-Info verwerfen + setGripEdit(null); // (c) Feld-Controller schließen }, }; @@ -819,26 +1314,66 @@ export default function App() { const drawingIds = project.drawings2d .filter((d) => d.levelId === activeLevel.id) .map((d) => d.id); - if (wallIds.length || drawingIds.length) { + const ceilingIds = (project.ceilings ?? []) + .filter((c) => c.floorId === activeLevel.id) + .map((c) => c.id); + const stairIds = (project.stairs ?? []) + .filter((s) => s.floorId === activeLevel.id) + .map((s) => s.id); + const roomIds = (project.rooms ?? []) + .filter((r) => r.floorId === activeLevel.id) + .map((r) => r.id); + if ( + wallIds.length || + drawingIds.length || + ceilingIds.length || + stairIds.length || + roomIds.length + ) { setSelectedWallIds(wallIds); setSelectedDrawingIds(drawingIds); + setSelectedCeilingIds(ceilingIds); + setSelectedStairIds(stairIds); + setSelectedRoomIds(roomIds); e.preventDefault(); } return; } if (e.key !== "Delete" && e.key !== "Backspace") return; if (activeTransform) return; // während einer Transformation nicht löschen - if (selectedDrawingIds.length || selectedWallIds.length) { + if ( + selectedDrawingIds.length || + selectedWallIds.length || + selectedCeilingIds.length || + selectedOpeningIds.length || + selectedStairIds.length || + selectedRoomIds.length + ) { const dids = new Set(selectedDrawingIds); const wids = new Set(selectedWallIds); + const cids = new Set(selectedCeilingIds); + const oids = new Set(selectedOpeningIds); + const stids = new Set(selectedStairIds); + const rmids = new Set(selectedRoomIds); setProject((p) => ({ ...p, drawings2d: p.drawings2d.filter((d) => !dids.has(d.id)), walls: p.walls.filter((w) => !wids.has(w.id)), + ceilings: (p.ceilings ?? []).filter((c) => !cids.has(c.id)), + stairs: (p.stairs ?? []).filter((s) => !stids.has(s.id)), + rooms: (p.rooms ?? []).filter((r) => !rmids.has(r.id)), doors: p.doors.filter((dr) => !wids.has(dr.hostWallId)), + // Öffnungen: eigene Auswahl löschen UND alle, deren Host-Wand entfällt. + openings: (p.openings ?? []).filter( + (o) => !oids.has(o.id) && !wids.has(o.hostWallId), + ), })); setSelectedDrawingIds([]); setSelectedWallIds([]); + setSelectedCeilingIds([]); + setSelectedOpeningIds([]); + setSelectedStairIds([]); + setSelectedRoomIds([]); e.preventDefault(); } }; @@ -849,14 +1384,51 @@ export default function App() { activeTransform, selectedDrawingIds, selectedWallIds, + selectedCeilingIds, + selectedOpeningIds, + selectedStairIds, project.walls, project.drawings2d, + project.ceilings, + project.stairs, activeLevel.id, ]); + // Split der gewählten 2D-Elemente an einem Punkt in mehrere Stücke (shared von + // Ctrl+S und einem etwaigen Menü-Eintrag). Ersetzt jedes Quell-Drawing durch ≥1 + // Ergebnis; neue Elemente werden ausgewählt. Wände sind ausgenommen (no-op). + const runSplit = (ids: string[]) => { + if (ids.length === 0) return; + const idSet = new Set(ids); + const newIds: string[] = []; + setProject((p) => { + const out: typeof p.drawings2d = []; + for (const d of p.drawings2d) { + if (!idSet.has(d.id)) { + out.push(d); + continue; + } + const others: PolyForm[] = []; + for (const o of p.drawings2d) { + if (o.id === d.id || o.levelId !== d.levelId) continue; + const f = toPolyForm(o.geom); + if (f) others.push(f); + } + const pieces = splitDrawing(d, others); + for (const piece of pieces) newIds.push(piece.id); + out.push(...pieces); + } + return { ...p, drawings2d: out }; + }); + setSelectedDrawingIds(newIds); + setSelectedWallIds([]); + }; + // Ctrl/Cmd+S = Split, Ctrl/Cmd+J = Join der selektierten Drawing2D-Elemente. - // Beide rufen preventDefault (sonst Browser-Speichern/-Lesezeichen). Wände sind - // ausgenommen (no-op). Tool-unabhängig, solange eine 2D-Auswahl besteht. + // Join läuft über DENSELBEN Engine-Befehl wie getipptes „join" (§Task C, ein + // Pfad). Split bleibt vorerst die direkte Aktion (kein Split-Engine-Befehl + // gefordert). Beide rufen preventDefault (sonst Browser-Speichern/-Lesezeichen). + // Wände sind ausgenommen (no-op). Tool-unabhängig, solange eine 2D-Auswahl steht. useEffect(() => { const onKey = (e: KeyboardEvent) => { if (!(e.ctrlKey || e.metaKey)) return; @@ -871,53 +1443,71 @@ export default function App() { e.preventDefault(); if (k === "s") { - // Split: jedes gewählte Element gegen ALLE ANDEREN Elemente seiner Ebene - // schneiden (ohne Schnitt: Mittel-Fallback in splitDrawing). Ersetzt jedes - // Quell-Drawing durch ≥1 Ergebnis; neue Elemente werden ausgewählt. - const idSet = new Set(ids); - const newIds: string[] = []; - setProject((p) => { - const out: typeof p.drawings2d = []; - for (const d of p.drawings2d) { - if (!idSet.has(d.id)) { - out.push(d); - continue; - } - // Andere Elemente DERSELBEN Ebene (außer dem Ziel selbst) als Schneider. - const others: PolyForm[] = []; - for (const o of p.drawings2d) { - if (o.id === d.id || o.levelId !== d.levelId) continue; - const f = toPolyForm(o.geom); - if (f) others.push(f); - } - const pieces = splitDrawing(d, others); - for (const piece of pieces) newIds.push(piece.id); - out.push(...pieces); - } - return { ...p, drawings2d: out }; - }); - setSelectedDrawingIds(newIds); - setSelectedWallIds([]); + runSplit(ids); return; } - - // Join: gewählte Elemente an koinzidenten Endpunkten verschmelzen. - const idSet = new Set(ids); - const newIds: string[] = []; - setProject((p) => { - const srcs = p.drawings2d.filter((d) => idSet.has(d.id)); - if (srcs.length === 0) return p; - const joined = joinDrawings(srcs); - for (const j of joined) newIds.push(j.id); - const rest = p.drawings2d.filter((d) => !idSet.has(d.id)); - return { ...p, drawings2d: [...rest, ...joined] }; - }); - setSelectedDrawingIds(newIds); + // Join: exakt derselbe Befehl wie „join" tippen (Sofort-Befehl, autoRun). + // Nach dem Verbinden zeigt die alte Auswahl auf ersetzte IDs → aufräumen. + engine.start("join"); + setSelectedDrawingIds([]); setSelectedWallIds([]); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); - }, [selectedDrawingIds]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedDrawingIds, engine]); + + // Ctrl/Cmd+Shift+U/D/I = Boolean Vereinigung/Subtraktion/Schnitt der gewählten + // geschlossenen Drawing2D. preventDefault (sonst Browser-Aktionen). Braucht ≥2 + // geschlossene Flächen; sonst No-op. (Menü spiegelt diese Aktionen.) + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (!(e.ctrlKey || e.metaKey) || !e.shiftKey) return; + const k = e.key.toLowerCase(); + if (k !== "u" && k !== "d" && k !== "i") return; + const el = e.target as HTMLElement | null; + if (el && (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.isContentEditable)) { + return; + } + if (selectedDrawingIds.length < 2) return; + e.preventDefault(); + if (k === "u") applyBoolean(unionDrawings); + else if (k === "d") applyBoolean(differenceDrawings); + else applyBoolean(intersectionDrawings); + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedDrawingIds, project.drawings2d]); + + // 2D-Boolean-Operation (Union / Subtract / Intersect) auf die gewählten + // geschlossenen Drawing2D anwenden. Reihenfolge der Auswahl = Reihenfolge in + // `selectedDrawingIds` (für Differenz: erstes Element = Basis). Ersetzt die + // beteiligten Quellen durch das Ergebnis und selektiert die neuen Elemente. + // Braucht ≥2 geschlossene Flächen; sonst No-op. + const applyBoolean = ( + op: (srcs: Drawing2D[]) => BooleanResult, + ) => { + const order = selectedDrawingIds; + if (order.length < 2) return; + const byId = new Map(project.drawings2d.map((d) => [d.id, d])); + // In Auswahl-Reihenfolge (Basis zuerst) auflösen. + const srcs = order + .map((id) => byId.get(id)) + .filter((d): d is Drawing2D => d !== undefined); + const result = op(srcs); + if (result.replacedIds.length === 0) return; // <2 geschlossene → No-op + const replaced = new Set(result.replacedIds); + setProject((p) => ({ + ...p, + drawings2d: [ + ...p.drawings2d.filter((d) => !replaced.has(d.id)), + ...result.created, + ], + })); + setSelectedDrawingIds(result.created.map((d) => d.id)); + setSelectedWallIds([]); + }; // Alt+Klick im Grundriss = Segment löschen (Schere): PlanView meldet das // getroffene Drawing2D + den Modellpunkt; App ermittelt daraus die nächst- @@ -976,16 +1566,22 @@ export default function App() { const hasSel = selectedWallIds.length > 0 || !!selectedDrawingId; if (!hasSel) return; const k = e.key.toLowerCase(); - const op: TransformOp | null = k === "m" ? "move" : k === "d" ? "rotate" : k === "s" ? "mirror" : null; + // Bewegen/Spiegeln/Drehen laufen ALLE über die interaktive Transformation + // (`activeTransform`) mit den U/I/O/P-Kopiermodi (Bewegen/Kopie/Reihe/ + // Verteilen). Prompt + Optionen + Live-Feld speisen die Command-Line; die + // Kürzel U/I/O/P wirken dort auch bei fokussiertem Eingabefeld (options.key). + const op: TransformOp | null = + k === "m" ? "move" : k === "s" ? "mirror" : k === "d" ? "rotate" : null; if (op) { setActiveTransform({ op, copyMode: "move", count: 1, points: [] }); setDraft(null); + requestAnimationFrame(() => commandLineRef.current?.focus()); e.preventDefault(); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); - }, [activeTool, activeTransform, selectedWallIds, selectedDrawingId]); + }, [activeTool, activeTransform, selectedWallIds, selectedDrawingId, engine]); // ── Projekt-Mutationen (aus dem Store-Slice) ───────────────────────────── // Sämtliche immutablen Projekt-Mutationen liegen jetzt in projectSlice. App @@ -1037,12 +1633,144 @@ export default function App() { const resizeElement = useStore((s) => s.resizeElement); const updateWall = useStore((s) => s.updateWall); const setWallThickness = useStore((s) => s.setWallThickness); + const updateOpening = useStore((s) => s.updateOpening); + const moveCeilingGrip = useStore((s) => s.moveCeilingGrip); + const moveCeilingBy = useStore((s) => s.moveCeilingBy); + const moveCeilingEdge = useStore((s) => s.moveCeilingEdge); + const updateCeiling = useStore((s) => s.updateCeiling); + const setCeilingThickness = useStore((s) => s.setCeilingThickness); + const updateStair = useStore((s) => s.updateStair); + const moveStairBy = useStore((s) => s.moveStairBy); + const updateRoom = useStore((s) => s.updateRoom); + const moveRoomBy = useStore((s) => s.moveRoomBy); + const moveRoomGrip = useStore((s) => s.moveRoomGrip); + const moveRoomEdge = useStore((s) => s.moveRoomEdge); + const moveRoomStamp = useStore((s) => s.moveRoomStamp); + const setRoomStampDoc = useStore((s) => s.setRoomStampDoc); + const setRoomStamp = useStore((s) => s.setRoomStamp); + + // Text-Ziel der Oberleisten-Text-Gruppe: der Stempel-Text des (einzeln) + // gewählten Raums. Ohne aktive Editor-Auswahl wirkt Formatierung auf das ganze + // Doc (range: null). Ist kein Raum gewählt → null (Controls setzen nur + // Defaults). Der Setter schreibt live in den Store. + const textTarget = useMemo( + () => + selectedRoom + ? { + doc: selectedRoom.stampDoc ?? docFromText(selectedRoom.name), + range: null, + apply: (doc: RichTextDoc) => setRoomStampDoc(selectedRoom.id, doc), + } + : null, + [selectedRoom, setRoomStampDoc], + ); + + // ── 3D-Editier-Griffe (direkt im Viewport3D) ────────────────────────────── + // Genau EINE gewählte Wand (kein 2D-Element) bzw. genau EIN 2D-Element liefert + // die Quelle der 3D-Griffe. Dieselben Store-Actions wie bei den Plan-Griffen + // (moveGripOf/moveElementByOf), damit Plan + 3D identisch mutieren. Der Höhen- + // Griff bindet die OK absolut (top=custom z) — gleicher Pfad wie das Attribut- + // Panel (updateWall top). + // Alle gewählten Wände/2D-Elemente als Quelle der 3D-Griffe. Wände UND 2D- + // Elemente schließen sich gegenseitig aus (entweder-oder), damit der einzelne + // Element-Editierpfad (moveGripOf je ID) eindeutig bleibt. Bei Mehrfachauswahl + // erscheinen Griffe für ALLE — Basis für das gemeinsame Ziehen koinzidenter Ecken. + const edit3dWalls = useMemo( + () => + selectedDrawingIds.length === 0 + ? selectedWallIds + .map((id) => project.walls.find((w) => w.id === id)) + .filter((w): w is Wall => !!w) + : [], + [selectedWallIds, selectedDrawingIds, project.walls], + ); + const edit3dDrawings = useMemo( + () => + selectedWallIds.length === 0 + ? selectedDrawingIds + .map((id) => project.drawings2d.find((d) => d.id === id)) + .filter((d): d is Drawing2D => !!d) + : [], + [selectedWallIds, selectedDrawingIds, project.drawings2d], + ); + // Snap beim 3D-Vertex-Drag: dieselbe computeSnap-Logik wie die Plan-Griffe. + // Das/die gerade GEZOGENEN Element(e) werden aus den Snap-Quellen genommen + // (sonst fängt der Punkt an sich selbst). `excludeWallIds`/`excludeDrawingIds` + // sind die Mitglieder der Koinzidenz-Gruppe (alle, die mitgeführt werden). + const snapFor3dVertex = ( + raw: Vec2, + excludeWallIds: Set, + excludeDrawingIds: Set, + ): SnapResult | null => { + const snapProject: Project = { + ...project, + walls: project.walls.filter((w) => !excludeWallIds.has(w.id)), + drawings2d: project.drawings2d.filter((d) => !excludeDrawingIds.has(d.id)), + }; + return computeSnap({ + raw, + project: snapProject, + levelId: activeLevel.id, + visibleCodes, + settings: snap, + draftPoints: [], + lastPoint: null, + // 3D hat kein echtes pxPerMeter; ein vernünftiger Fallback hält die + // Snap-Toleranz (tolerancePx/pxPerMeter) in einem brauchbaren Meter-Bereich. + pxPerMeter: 90, + shift: false, + ctrl: false, + }); + }; + // Ein 3D-Vertex-Griff wurde gezogen. `target` benennt das konkrete Element + // (Wand-ID ODER Drawing-ID), `index` den Vertex. Liefert den effektiv + // angewandten (gesnappten) Punkt zurück — der 3D-Viewport nutzt ihn, um + // koinzidente Endpunkte anderer gewählter Elemente mitzuführen und den + // Snap-Marker zu zeigen. `snapped=false` (für mitgeführte Geschwister) wendet + // den Punkt direkt an, ohne erneut zu snappen. + const onEdit3dVertex = ( + target: { wallId?: string; drawingId?: string }, + index: number, + model: Vec2, + excludeWallIds?: Set, + excludeDrawingIds?: Set, + ): Vec2 => { + let pt = model; + if (excludeWallIds || excludeDrawingIds) { + const snapResult = snapFor3dVertex( + model, + excludeWallIds ?? new Set(), + excludeDrawingIds ?? new Set(), + ); + pt = snapResult?.point ?? model; + } + moveGripOf(target.drawingId ?? null, target.wallId ?? null, index, pt); + return pt; + }; + const onEdit3dBody = (target: { wallId?: string; drawingId?: string }, delta: Vec2) => { + moveElementByOf(target.drawingId ?? null, target.wallId ?? null, delta); + }; + const onEdit3dWallTop = (wallId: string, topZ: number) => { + updateWall(wallId, { top: { mode: "custom", z: topZ } }); + }; + const onEdit3dEnd = () => { + setDraft(null); + }; // ── Selektions-Attribut-Kontrakt (Attributes-/Object-Info-Paletten) ─────── // Normalisierte, effektiv aufgelöste Sicht auf das erste selektierte Element. const selection = useMemo( - () => deriveSelection(project, selectedWallIds, selectedDrawingId), - [project, selectedWallIds, selectedDrawingId], + () => + deriveSelection( + project, + selectedWallIds, + selectedDrawingId, + selectedCeilingId, + selectedOpeningId, + selectedStairId, + selectedRoomId, + ), + [project, selectedWallIds, selectedDrawingId, selectedCeilingId, selectedOpeningId, selectedStairId, selectedRoomId], ); // ── DXF/DWG-Import (Dialog) ─────────────────────────────────────────────── @@ -1143,8 +1871,11 @@ export default function App() { project, activeLevelId, activeTool, - onSelectTool: setActiveTool, - toolsEnabled: activeLevel.kind === "floor" && viewType === "grundriss", + onSelectTool, + // Werkzeuge auch in der Perspektive: der Cursor wird per Raycast auf die + // Geschoss-Arbeitsebene (OKFF) projiziert und über onWorkplanePoint in + // dieselbe Engine wie im Grundriss gespeist (Viewport3D). + toolsEnabled: activeLevel.kind === "floor", activeWallTypeId, onActiveWallTypeId: setActiveWallTypeId, snap, @@ -1177,6 +1908,14 @@ export default function App() { if (!selection) return; if (selection.kind === "wall" || selection.kind === "drawing2d") setElementColor(selection.kind, selection.id, color); + else if (selection.kind === "ceiling") + updateCeiling(selection.id, { color }); + else if (selection.kind === "opening") + updateOpening(selection.id, { color }); + else if (selection.kind === "stair") + updateStair(selection.id, { color }); + else if (selection.kind === "room") + updateRoom(selection.id, { color }); }, onSetSelectionWeight: (weightMm) => { // Strichstärke gibt es als direktes Feld nur bei Drawing2D. @@ -1218,6 +1957,85 @@ export default function App() { if (selection?.kind === "wall") updateWall(selection.id, { top: anchor ?? undefined }); }, + // ── Decken-Attribute (nur bei selektierter Decke) ────────────────────── + onSetCeilingType: (wallTypeId) => { + if (selection?.kind === "ceiling") + updateCeiling(selection.id, { wallTypeId, thickness: undefined }); + }, + onSetCeilingThickness: (thickness) => { + if (selection?.kind === "ceiling") + setCeilingThickness(selection.id, thickness); + }, + onSetCeilingTop: (anchor) => { + if (selection?.kind === "ceiling") + updateCeiling(selection.id, { top: anchor ?? undefined }); + }, + // ── Öffnungs-Attribute (nur bei selektierter Öffnung) ────────────────── + onSetOpeningKind: (kind) => { + if (selection?.kind === "opening") + updateOpening( + selection.id, + kind === "door" + ? { kind, sillHeight: 0, hinge: "start", swing: "left", openingDir: "in", swingAngle: 90 } + : { kind }, + ); + }, + onSetOpeningWidth: (width) => { + if (selection?.kind === "opening" && width > 0) + updateOpening(selection.id, { width }); + }, + onSetOpeningHeight: (height) => { + if (selection?.kind === "opening" && height > 0) + updateOpening(selection.id, { height }); + }, + onSetOpeningSill: (sillHeight) => { + if (selection?.kind === "opening" && sillHeight >= 0) + updateOpening(selection.id, { sillHeight }); + }, + onSetOpeningPosition: (position) => { + if (selection?.kind === "opening" && position >= 0) + updateOpening(selection.id, { position }); + }, + onSetOpeningSwingAngle: (swingAngle) => { + if (selection?.kind === "opening" && swingAngle > 0) + updateOpening(selection.id, { swingAngle }); + }, + onSetOpeningHinge: (hinge) => { + if (selection?.kind === "opening") updateOpening(selection.id, { hinge }); + }, + onSetOpeningSwing: (swing) => { + if (selection?.kind === "opening") updateOpening(selection.id, { swing }); + }, + onSetOpeningDir: (openingDir) => { + if (selection?.kind === "opening") updateOpening(selection.id, { openingDir }); + }, + // ── Treppen-Attribute (nur bei selektierter Treppe) ──────────────────── + onSetStairShape: (shape) => { + if (selection?.kind === "stair") updateStair(selection.id, { shape }); + }, + onSetStairWidth: (width) => { + if (selection?.kind === "stair" && width > 0) + updateStair(selection.id, { width }); + }, + onSetStairSteps: (stepCount) => { + if (selection?.kind === "stair" && stepCount >= 2) + updateStair(selection.id, { stepCount: Math.round(stepCount) }); + }, + onSetStairRise: (totalRise) => { + if (selection?.kind === "stair" && totalRise > 0) + updateStair(selection.id, { totalRise }); + }, + onSetStairUp: (up) => { + if (selection?.kind === "stair") updateStair(selection.id, { up }); + }, + // ── Raum-Attribute (nur bei selektiertem Raum) ───────────────────────── + onSetRoomName: (name) => { + if (selection?.kind === "room") updateRoom(selection.id, { name }); + }, + onSetRoomSia: (category) => { + if (selection?.kind === "room") updateRoom(selection.id, { siaCategory: category }); + }, + onEditRoomStamp: (roomId) => onOpenStampEditor(roomId), // ── Site-/Kontext-Schicht ────────────────────────────────────────────── contextObjects: project.context ?? [], onAddContextObjects: addContextObjects, @@ -1420,6 +2238,77 @@ export default function App() { // „100 %" = Zoom exakt auf den gewählten Massstab setzen. const zoomToApplied = () => planViewRef.current?.applyScale(scaleDenominator); + // Massstäbliche Inhalts-Maße (Meter) des aktuellen Plans — nur berechnet, wenn + // der Export-Dialog offen ist (für die „passt aufs Blatt?"-Warnung). + const exportContentMeters = useMemo(() => { + if (!exportOpen) return undefined; + const b = generatePlan( + project, + activeLevel.id, + visibleCodes, + categoryDisplay, + detail, + referenceLines, + planColorMode === "mono", + ).bounds; + return { w: b.maxX - b.minX, h: b.maxY - b.minY }; + }, [ + exportOpen, + project, + activeLevel.id, + visibleCodes, + categoryDisplay, + detail, + referenceLines, + planColorMode, + ]); + + // PDF-Export: erzeugt den Plan mit EXAKT denselben Eingaben wie die Ansicht + // (Geschoss, sichtbare Codes, Darstellungsmodus, Detailgrad, Referenzlinien, + // Farb-/Mono-Modus) und schreibt ihn massstäblich als Vektor-PDF. Reines + // Modell → SVG → PDF, unabhängig vom Bildschirm-DOM. + const onExportPdf = async (decision: ExportPdfDecision) => { + const plan = generatePlan( + project, + activeLevel.id, + visibleCodes, + categoryDisplay, + detail, + referenceLines, + planColorMode === "mono", + ); + await savePlanPdf(plan, { + scaleDenominator: decision.scaleDenominator, + paper: decision.paper, + orientation: decision.orientation, + title: activeLevel.name, + subtitle: project.name, + fileName: `${project.name}-${activeLevel.name}`, + }); + setExportOpen(false); + }; + + // DXF-Export: erzeugt den Plan mit EXAKT denselben Eingaben wie die Ansicht und + // schreibt ihn als Modell-Space-DXF in echten Metern (1:1). Layer aus den + // Kategorie-Ebenen; jede Entity auf ihrem Kategorie-Layer. + const onExportDxf = (decision: ExportDxfDecision) => { + const plan = generatePlan( + project, + activeLevel.id, + visibleCodes, + categoryDisplay, + detail, + referenceLines, + planColorMode === "mono", + ); + savePlanDxf(plan, project, { + title: activeLevel.name, + includeHatches: decision.includeHatches, + fileName: `${project.name}-${activeLevel.name}`, + }); + setExportDxfOpen(false); + }; + // Modusleiste-Wechsel: array/distribute fragen die Kopienanzahl ab. const onTransformMode = (mode: CopyMode) => { if (mode === "array" || mode === "distribute") { @@ -1430,9 +2319,111 @@ export default function App() { } }; + // ── Transformation in der Befehlszeile ──────────────────────────────────── + // Die laufende Transformation speist Prompt + U/I/O/P-Optionen + ein Live-Feld + // (Winkel beim Drehen, Distanz beim Bewegen) in die Command-Line — statt einer + // eigenen schwebenden Leiste. Rein die UI-Fläche; Geometrie unverändert. + const TRANSFORM_MODES: { id: CopyMode; labelKey: string; key: string }[] = [ + { id: "move", labelKey: "transform.mode.move", key: "U" }, + { id: "copy", labelKey: "transform.mode.copy", key: "I" }, + { id: "array", labelKey: "transform.mode.array", key: "O" }, + { id: "distribute", labelKey: "transform.mode.distribute", key: "P" }, + ]; + + const transformOptions = (): CommandLineOption[] => { + const tr = activeTransform; + if (!tr) return []; + return TRANSFORM_MODES.map((m) => ({ + id: m.id, + labelKey: m.labelKey, + key: m.key, + active: tr.copyMode === m.id, + // ×count nur beim aktiven Reihen-/Verteilungs-Modus. + value: + (m.id === "array" || m.id === "distribute") && tr.copyMode === m.id + ? `×${tr.count}` + : undefined, + })); + }; + + // Live-/lockbares Feld: Winkel (Drehen, ab Zentrum+Bezug) bzw. Distanz + // (Bewegen, ab Basispunkt). Ohne Bezugspunkt kein sinnvolles Feld → []. + const transformFields = (): CommandLineField[] => { + const tr = activeTransform; + const info = transformDragInfoRef.current; + if (!tr || !info) return []; + const cursor = info.cursor; + if (tr.op === "rotate") { + if (tr.points.length !== 2) return []; + const c = tr.points[0], refP = tr.points[1]; + const a0 = Math.atan2(refP.y - c.y, refP.x - c.x); + const a1 = Math.atan2(cursor.y - c.y, cursor.x - c.x); + const deg = ((((a1 - a0) * 180) / Math.PI) % 360 + 360) % 360; + return [ + { id: "angle", labelKey: "cmd.field.angle", value: deg, locked: false, active: true }, + ]; + } + // Bewegen: Distanz ab Basispunkt. Spiegeln hat kein sinnvolles Zahlenfeld + // (freie Achse) → keine Felder. + if (tr.op !== "move") return []; + if (tr.points.length !== 1) return []; + const base = tr.points[0]; + const dist = Math.hypot(cursor.x - base.x, cursor.y - base.y); + return [ + { id: "distance", labelKey: "cmd.field.distance", value: dist, locked: false, active: true }, + ]; + }; + + // Getippte Zahl in der Transformation: als Wert des aktiven Feldes deuten und + // über denselben Pfad wie transformClick committen. + const submitTransformValue = (value: number) => { + const tr = activeTransform; + const info = transformDragInfoRef.current; + if (!tr || !info) return; + if (tr.op === "rotate") { + if (tr.points.length !== 2) return; + const c = tr.points[0], refP = tr.points[1]; + const a0 = Math.atan2(refP.y - c.y, refP.x - c.x); + const a = a0 + (value * Math.PI) / 180; + const r = Math.hypot(refP.x - c.x, refP.y - c.y) || 1; + const target = { x: c.x + Math.cos(a) * r, y: c.y + Math.sin(a) * r }; + const points = [...tr.points, target]; + commitTransformOn(transformSel, tr.op, points, tr.copyMode, tr.count); + } else if (tr.op === "move") { + if (tr.points.length !== 1) return; + // Distanz entlang der aktuellen Live-Richtung locken. + const base = tr.points[0]; + const dx = info.cursor.x - base.x, dy = info.cursor.y - base.y; + const len = Math.hypot(dx, dy) || 1; + const target = { x: base.x + (dx / len) * value, y: base.y + (dy / len) * value }; + const points = [...tr.points, target]; + commitTransformOn(transformSel, tr.op, points, tr.copyMode, tr.count); + } + setActiveTransform(null); + setDraft(null); + transformDragInfoRef.current = null; + }; + + const cancelTransform = () => { + setActiveTransform(null); + setDraft(null); + transformDragInfoRef.current = null; + }; + // ── Maus-Schema: Auswahl + Kontextmenü aus Grundriss / 3D ───────────────── // Links-Klick im Grundriss: genau eine Wand wählen (Menge ersetzen) bzw. // Leerraum → Auswahl löschen. + // Doppelklick auf einen Raum-Stempel: den Raum wählen + den Stempel-Editor + // öffnen (Rich-Text-Popup). + const onOpenStampEditor = (roomId: string) => { + setSelectedRoomIds([roomId]); + setSelectedWallIds([]); + setSelectedDrawingId(null); + setSelectedCeilingIds([]); + setSelectedOpeningIds([]); + setSelectedStairIds([]); + setStampEditorRoomId(roomId); + }; const onPlanSelect = (sel: PlanSelection) => { // Shift+Klick: Mehrfach-Auswahl — getroffenes Element zu-/abschalten, den // Rest behalten (Wände und 2D-Zeichnungen unabhängig, gemischte Auswahl @@ -1452,19 +2443,89 @@ export default function App() { ? selectedDrawingIds.filter((d) => d !== id) : [...selectedDrawingIds, id], ); + } else if (sel.ceilingId) { + const id = sel.ceilingId; + setSelectedCeilingIds( + selectedCeilingIds.includes(id) + ? selectedCeilingIds.filter((c) => c !== id) + : [...selectedCeilingIds, id], + ); + } else if (sel.openingId) { + const id = sel.openingId; + setSelectedOpeningIds( + selectedOpeningIds.includes(id) + ? selectedOpeningIds.filter((o) => o !== id) + : [...selectedOpeningIds, id], + ); + } else if (sel.stairId) { + const id = sel.stairId; + setSelectedStairIds( + selectedStairIds.includes(id) + ? selectedStairIds.filter((s) => s !== id) + : [...selectedStairIds, id], + ); + } else if (sel.roomId) { + const id = sel.roomId; + setSelectedRoomIds( + selectedRoomIds.includes(id) + ? selectedRoomIds.filter((r) => r !== id) + : [...selectedRoomIds, id], + ); } return; } - // Ohne Shift: ersetzen. Wand hat Vorrang; sonst 2D-Element; sonst leeren. - if (sel.wallId) { + // Ohne Shift: ersetzen. Öffnung hat Vorrang (ihr Symbol liegt in der Lücke), + // dann Wand, dann 2D-Element, dann Decke; sonst leeren. Die Kanäle schließen + // sich gegenseitig aus. + if (sel.openingId) { + setSelectedOpeningIds([sel.openingId]); + setSelectedWallIds([]); + setSelectedDrawingId(null); + setSelectedCeilingIds([]); + setSelectedStairIds([]); + setSelectedRoomIds([]); + } else if (sel.wallId) { setSelectedWallIds([sel.wallId]); setSelectedDrawingId(null); + setSelectedCeilingIds([]); + setSelectedOpeningIds([]); + setSelectedStairIds([]); + setSelectedRoomIds([]); } else if (sel.drawingId) { setSelectedDrawingId(sel.drawingId); setSelectedWallIds([]); + setSelectedCeilingIds([]); + setSelectedOpeningIds([]); + setSelectedStairIds([]); + setSelectedRoomIds([]); + } else if (sel.ceilingId) { + setSelectedCeilingIds([sel.ceilingId]); + setSelectedWallIds([]); + setSelectedDrawingId(null); + setSelectedOpeningIds([]); + setSelectedStairIds([]); + setSelectedRoomIds([]); + } else if (sel.stairId) { + setSelectedStairIds([sel.stairId]); + setSelectedWallIds([]); + setSelectedDrawingId(null); + setSelectedCeilingIds([]); + setSelectedOpeningIds([]); + setSelectedRoomIds([]); + } else if (sel.roomId) { + setSelectedRoomIds([sel.roomId]); + setSelectedWallIds([]); + setSelectedDrawingId(null); + setSelectedCeilingIds([]); + setSelectedOpeningIds([]); + setSelectedStairIds([]); } else { setSelectedWallIds([]); setSelectedDrawingId(null); + setSelectedCeilingIds([]); + setSelectedOpeningIds([]); + setSelectedStairIds([]); + setSelectedRoomIds([]); } }; // Links-Drag (Marquee) im Grundriss: die getroffene Wand-MENGE übernehmen @@ -1472,6 +2533,11 @@ export default function App() { const onPlanMarquee = (sel: MarqueeSelection) => { setSelectedWallIds(sel.wallIds); setSelectedDrawingIds(sel.drawingIds ?? []); + // Decken/Öffnungen/Treppen/Räume sind (noch) nicht Teil des Marquee → beim Aufziehen abwählen. + setSelectedCeilingIds([]); + setSelectedOpeningIds([]); + setSelectedStairIds([]); + setSelectedRoomIds([]); }; // Rechts-Klick im Grundriss: Wand-Kontextmenü. Trifft der Klick eine Wand, die // NICHT bereits in der Auswahl ist, wird die Auswahl auf sie gesetzt (damit die @@ -1487,8 +2553,49 @@ export default function App() { setMenu({ kind: "plan", x: info.clientX, y: info.clientY, wallId }); }; // Links-Klick im 3D: per Raycast getroffene Wand wählen (Einzelauswahl). - const onViewportSelectWall = (wallId: string | null) => + const onViewportSelectWall = (wallId: string | null) => { setSelectedWallIds(wallId ? [wallId] : []); + if (wallId) { + setSelectedDrawingId(null); + setSelectedCeilingIds([]); + setSelectedOpeningIds([]); + setSelectedStairIds([]); + setSelectedRoomIds([]); + } + }; + // Links-Klick im 3D: per Raycast getroffene Treppe wählen (Einzelauswahl). + const onViewportSelectStair = (stairId: string | null) => { + setSelectedStairIds(stairId ? [stairId] : []); + if (stairId) { + setSelectedWallIds([]); + setSelectedDrawingId(null); + setSelectedCeilingIds([]); + setSelectedOpeningIds([]); + setSelectedRoomIds([]); + } + }; + // Links-Klick im 3D: per Raycast getroffene Decke wählen (Einzelauswahl). + const onViewportSelectCeiling = (ceilingId: string | null) => { + setSelectedCeilingIds(ceilingId ? [ceilingId] : []); + if (ceilingId) { + setSelectedWallIds([]); + setSelectedDrawingId(null); + setSelectedStairIds([]); + setSelectedOpeningIds([]); + setSelectedRoomIds([]); + } + }; + // Links-Klick im 3D: per Raycast getroffene Öffnung wählen (Einzelauswahl). + const onViewportSelectOpening = (openingId: string | null) => { + setSelectedOpeningIds(openingId ? [openingId] : []); + if (openingId) { + setSelectedWallIds([]); + setSelectedDrawingId(null); + setSelectedCeilingIds([]); + setSelectedStairIds([]); + setSelectedRoomIds([]); + } + }; // Rechts-Klick im 3D: Wand-Kontextmenü an der Cursorposition. const onViewportContextMenu = (info: ViewportContextInfo) => { if (info.wallId && !selectedWallIds.includes(info.wallId)) { @@ -1573,6 +2680,8 @@ export default function App() { onFit={fitPlan} onFitSelection={fitSelection} onZoom100={zoomToApplied} + onExportPdf={() => setExportOpen(true)} + onExportDxf={() => setExportDxfOpen(true)} renderMode={renderMode} onRenderMode={setRenderMode} renderModeEnabled={viewType === "perspektive"} @@ -1613,6 +2722,8 @@ export default function App() { onApply={(l) => setLayout(reconcileLayout(l))} /> } + textTarget={textTarget} + onAddText={null} />
@@ -1659,12 +2770,26 @@ export default function App() { onZoom={setZoomPercent} onScale={setLiveScale} selectedWallIds={selectedWallIds} + selectedCeilingIds={selectedCeilingIds} + selectedOpeningIds={selectedOpeningIds} + selectedStairIds={selectedStairIds} + selectedRoomIds={selectedRoomIds} + roomStamp={roomStamp} + onStampMove={moveRoomStamp} + onStampEdit={onOpenStampEditor} onPlanSelect={onPlanSelect} onPlanMarquee={onPlanMarquee} onSegmentCut={onSegmentCut} onPlanContextMenu={onPlanContextMenu} onViewportSelectWall={onViewportSelectWall} + onViewportSelectCeiling={onViewportSelectCeiling} + onViewportSelectOpening={onViewportSelectOpening} + onViewportSelectStair={onViewportSelectStair} onViewportContextMenu={onViewportContextMenu} + commandActive={commandActive} + draft={draft} + onWorkplanePoint={onWorkplanePoint} + onWorkplaneConfirm={onWorkplaneConfirm} activeTool={activeTool} toolInputActive={toolInputActive} toolHandlers={toolHandlers} @@ -1673,23 +2798,14 @@ export default function App() { selectedDrawingId={selectedDrawingId} selectedDrawingIds={selectedDrawingIds} gripHandlers={gripHandlers} + edit3dWalls={edit3dWalls} + edit3dDrawings={edit3dDrawings} + onEdit3dVertex={onEdit3dVertex} + onEdit3dBody={onEdit3dBody} + onEdit3dWallTop={onEdit3dWallTop} + onEdit3dEnd={onEdit3dEnd} /> - {/* Modusleiste der laufenden Transformation (über der Mitte). */} - {activeTransform && ( - { - setActiveTransform(null); - setDraft(null); - }} - /> - )} - {/* Rechtes Dock: standardmäßig leer (Ressourcen ist ein schwebendes Fenster). Nur gerendert, wenn es Gruppen hat — sonst nutzt die Mitte den freigewordenen Platz. */} @@ -1761,14 +2877,47 @@ export default function App() { Inline-Optionen + Texteingabe + Autocomplete. Tab fokussiert sie. */} engine.submitText(text)} - onOption={(id) => engine.chooseOption(id)} - onCycleField={() => engine.cycleField()} - onCancel={() => engine.cancel()} + // (c) Während eines Vertex-Griff-Drags belegt der Feld-Controller das + // Befehlsfeld (Länge/Winkel). Läuft eine Auswahl-Transformation (Drehen + // etc.), belegt sie Prompt + U/I/O/P + Winkel-/Distanzfeld. Sonst gilt die + // Engine (Zeichnen/Ruhe). Griff-Drag hat Vorrang (schließen sich sonst aus). + active={gripEdit ? true : activeTransform ? true : engineView.active} + promptKey={ + gripEdit + ? "cmd.edit.point" + : activeTransform + ? `transform.hint.${activeTransform.op}.${Math.min( + activeTransform.points.length, + activeTransform.op === "rotate" ? 2 : 1, + )}` + : engineView.promptKey + } + options={gripEdit ? [] : activeTransform ? transformOptions() : engineView.options} + fields={ + gripEdit ? gripEditFields() : activeTransform ? transformFields() : engineView.fields + } + onSubmit={(text) => { + if (gripEdit) { + const v = parseFloat(text.replace(",", ".")); + if (isFinite(v)) submitGripEditValue(v); + return; + } + if (activeTransform) { + const v = parseFloat(text.replace(",", ".")); + if (isFinite(v)) submitTransformValue(v); + return; + } + engine.submitText(text); + }} + onOption={(id) => + activeTransform ? onTransformMode(id as CopyMode) : engine.chooseOption(id) + } + onCycleField={() => + gripEdit ? cycleGripEditField() : activeTransform ? undefined : engine.cycleField() + } + onCancel={() => + gripEdit ? setGripEdit(null) : activeTransform ? cancelTransform() : engine.cancel() + } suggest={(prefix) => engine.suggest(prefix)} /> @@ -1810,6 +2959,64 @@ export default function App() { /> )} + {/* PDF-Export-Dialog (Grundriss → Vektor-PDF). Nur im Grundriss eines + Geschosses sinnvoll; der Button ist sonst deaktiviert. */} + {exportOpen && ( + setExportOpen(false)} + /> + )} + + {/* DXF-Export-Dialog (Grundriss → Modell-Space-DXF in Metern). */} + {exportDxfOpen && ( + setExportDxfOpen(false)} + /> + )} + + {/* Text-Editor (schwebendes Fenster): Doppelklick auf einen Raumstempel bzw. + Button in der Objekt-Info öffnet ihn. „Übernehmen" schreibt das + bearbeitete Rich-Text-Dokument über setRoomStampDoc. Die Formatier- + Leiste in der Oberleiste wirkt ebenfalls live auf den gewählten Raum. */} + {stampEditorRoomId && + (() => { + const room = (project.rooms ?? []).find((r) => r.id === stampEditorRoomId); + if (!room) return null; + // Strukturierter Stempel für neue Räume (kein Alt-Rich-Text) bzw. sobald + // einer gesetzt ist; freier Text nur noch als Fallback für Alt-Räume, + // die einen stampDoc, aber keinen strukturierten Stempel tragen. + const useStructured = Boolean(room.stamp) || !room.stampDoc; + if (useStructured) { + return ( + { + setRoomStamp(room.id, edited); + setStampEditorRoomId(null); + }} + onCancel={() => setStampEditorRoomId(null)} + /> + ); + } + const doc = room.stampDoc ?? docFromText(room.name); + return ( + { + setRoomStampDoc(room.id, edited); + setStampEditorRoomId(null); + }} + onCancel={() => setStampEditorRoomId(null)} + /> + ); + })()} + {/* Dezenter Drop-Rahmen, solange eine Datei über die App gezogen wird. */} {dropActive && (