@pascal-app/editor 0.9.1 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +49 -0
- package/src/components/editor/bake-thumbnail.tsx +95 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
- package/src/components/editor/custom-camera-controls.tsx +332 -199
- package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -0
- package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
- package/src/components/editor/export-manager.tsx +71 -63
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
- package/src/components/editor/first-person/build-collider-world.ts +52 -5
- package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
- package/src/components/editor/first-person/terrain-collider.ts +212 -0
- package/src/components/editor/first-person-controls.tsx +410 -136
- package/src/components/editor/floating-action-menu.tsx +435 -64
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-background-selection.test.ts +72 -0
- package/src/components/editor/floorplan-background-selection.ts +7 -20
- package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
- package/src/components/editor/floorplan-camera-sync.ts +213 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
- package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
- package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
- package/src/components/editor/floorplan-panel.tsx +2517 -1492
- package/src/components/editor/grid.tsx +166 -39
- package/src/components/editor/group-actions.ts +597 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +102 -64
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +192 -1
- package/src/components/editor/group-transform-shared.ts +240 -7
- package/src/components/editor/handles/handle-arrow.tsx +81 -23
- package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
- package/src/components/editor/handles/handle-drag-history.ts +14 -0
- package/src/components/editor/handles/preview-overrides.test.ts +26 -0
- package/src/components/editor/handles/preview-overrides.ts +13 -0
- package/src/components/editor/handles/resize-snap.test.ts +52 -0
- package/src/components/editor/handles/resize-snap.ts +23 -0
- package/src/components/editor/handles/use-handle-drag.ts +49 -6
- package/src/components/editor/index.tsx +289 -35
- package/src/components/editor/measurement-pill.tsx +68 -23
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +409 -180
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +76 -0
- package/src/components/editor/quick-measurement-hud.tsx +34 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +814 -466
- package/src/components/editor/site-edge-labels.tsx +28 -6
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +118 -253
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
- package/src/components/editor/wall-measurement-label.tsx +10 -28
- package/src/components/editor/wall-move-side-handles.tsx +229 -39
- package/src/components/editor/wall-opening-highlights.tsx +63 -32
- package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
- package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
- package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
- package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
- package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-tool.tsx +34 -8
- package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
- package/src/components/tools/fence/fence-drafting.ts +107 -8
- package/src/components/tools/item/move-tool.tsx +17 -13
- package/src/components/tools/item/placement-math.test.ts +13 -1
- package/src/components/tools/item/placement-math.ts +28 -2
- package/src/components/tools/item/placement-strategies.ts +246 -5
- package/src/components/tools/item/placement-types.ts +13 -1
- package/src/components/tools/item/use-draft-node.ts +146 -57
- package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
- package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
- package/src/components/tools/roof/roof-tool.tsx +336 -94
- package/src/components/tools/select/box-select-state.ts +18 -7
- package/src/components/tools/select/box-select-tool.tsx +176 -40
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
- package/src/components/tools/select/select-candidates.ts +1 -1
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/floor-stack-preview.ts +4 -0
- package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +183 -1
- package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
- package/src/components/tools/shared/pointer-support-cap.ts +273 -0
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +101 -42
- package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
- package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
- package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
- package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
- package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
- package/src/components/tools/stair/stair-click-guard.ts +72 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +164 -42
- package/src/components/tools/tool-manager.tsx +149 -30
- package/src/components/tools/wall/wall-drafting.test.ts +447 -0
- package/src/components/tools/wall/wall-drafting.ts +304 -108
- package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +98 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +44 -94
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +2 -4
- package/src/components/ui/action-menu/measurement-control.tsx +267 -0
- package/src/components/ui/action-menu/structure-tools.tsx +22 -13
- package/src/components/ui/action-menu/view-toggles.tsx +42 -90
- package/src/components/ui/command-palette/editor-commands.tsx +16 -3
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +93 -18
- package/src/components/ui/controls/material-picker.tsx +162 -152
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +118 -44
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
- package/src/components/ui/floating-level-selector.tsx +74 -20
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +273 -14
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
- package/src/components/ui/panels/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +36 -14
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +19 -4
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
- package/src/components/ui/panels/selection-breakdown.ts +20 -0
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +42 -28
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer/viewer-controls-bar.tsx +466 -0
- package/src/components/viewer/viewer-scene-header.tsx +235 -0
- package/src/components/viewer-overlay.tsx +36 -596
- package/src/components/viewer-zone-system.tsx +6 -1
- package/src/components/walkthrough-hud.tsx +111 -0
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +54 -7
- package/src/hooks/use-ceiling-events.ts +3 -2
- package/src/hooks/use-drag-action.ts +4 -1
- package/src/hooks/use-grid-events.ts +24 -0
- package/src/hooks/use-keyboard.ts +438 -70
- package/src/index.tsx +355 -9
- package/src/lib/active-placement-surface.test.ts +24 -0
- package/src/lib/active-placement-surface.ts +42 -0
- package/src/lib/camera-pose.test.ts +237 -0
- package/src/lib/camera-pose.ts +172 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +275 -0
- package/src/lib/direct-manipulation.ts +118 -0
- package/src/lib/door-interaction.ts +1 -1
- package/src/lib/editor-api.ts +23 -35
- package/src/lib/elevation-guides.test.ts +107 -0
- package/src/lib/elevation-guides.ts +236 -0
- package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
- package/src/lib/floorplan/annotation-visibility.ts +133 -0
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
- package/src/lib/floorplan/drawing-coordination.ts +13 -0
- package/src/lib/floorplan/floorplan-export.test.ts +319 -0
- package/src/lib/floorplan/floorplan-export.tsx +978 -0
- package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
- package/src/lib/floorplan/floorplan-extension.ts +181 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
- package/src/lib/floorplan/floorplan-mode.ts +73 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
- package/src/lib/floorplan/geometry.ts +52 -0
- package/src/lib/floorplan/index.ts +3 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/fresh-planar-placement.test.ts +283 -3
- package/src/lib/fresh-planar-placement.ts +85 -1
- package/src/lib/glb-export.test.ts +556 -0
- package/src/lib/glb-export.ts +1071 -0
- package/src/lib/ground-surface.ts +95 -0
- package/src/lib/history.test.ts +96 -0
- package/src/lib/history.ts +31 -0
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +52 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +243 -0
- package/src/lib/level-duplication.test.ts +2 -1
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +36 -49
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +188 -0
- package/src/lib/measurements.ts +210 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/planar-cursor-placement.test.ts +57 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-action-feedback.ts +33 -0
- package/src/lib/quick-action-nodes.test.ts +94 -0
- package/src/lib/quick-action-nodes.ts +47 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +6 -6
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +325 -0
- package/src/lib/scene-clipboard.ts +310 -21
- package/src/lib/scene.ts +28 -5
- package/src/lib/selection-routing.test.ts +313 -0
- package/src/lib/selection-routing.ts +180 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +82 -14
- package/src/lib/sfx-player.test.ts +266 -0
- package/src/lib/sfx-player.ts +262 -69
- package/src/lib/site-boundary.test.ts +53 -0
- package/src/lib/site-boundary.ts +27 -0
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +165 -0
- package/src/lib/snapping-mode.ts +191 -0
- package/src/lib/stair-duplication.test.ts +103 -0
- package/src/lib/stair-duplication.ts +58 -86
- package/src/lib/stair-levels.ts +2 -0
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/terrain-sculpt.test.ts +429 -0
- package/src/lib/terrain-sculpt.ts +301 -0
- package/src/lib/terrain-verb-color.test.ts +51 -0
- package/src/lib/terrain-verb-color.ts +58 -0
- package/src/lib/touch-gesture-priority.test.ts +45 -0
- package/src/lib/touch-gesture-priority.ts +30 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/window-interaction.ts +1 -1
- package/src/lib/world-grid-snap.ts +37 -5
- package/src/store/camera-pose-store.test.ts +29 -0
- package/src/store/camera-pose-store.ts +27 -0
- package/src/store/live-draft-preview-stores.test.ts +136 -0
- package/src/store/navigation-sync-pose-store.test.ts +34 -0
- package/src/store/navigation-sync-pose-store.ts +27 -0
- package/src/store/terrain-sculpt-mode.test.ts +223 -0
- package/src/store/use-delete-confirmation.ts +27 -0
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-drawing-view.test.ts +23 -0
- package/src/store/use-drawing-view.ts +84 -0
- package/src/store/use-editor.tsx +665 -126
- package/src/store/use-elevation-guides.ts +40 -0
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +51 -0
- package/src/store/use-first-person-hud.ts +40 -0
- package/src/store/use-floorplan-annotation-visibility.ts +60 -0
- package/src/store/use-floorplan-draft-preview.ts +151 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-floorplan-mode.ts +110 -0
- package/src/store/use-interaction-scope.test.ts +206 -0
- package/src/store/use-interaction-scope.ts +140 -0
- package/src/store/use-measurement-draft.test.ts +559 -0
- package/src/store/use-measurement-draft.ts +570 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-path-draft-preview.ts +90 -0
- package/src/store/use-placement-preview.ts +11 -3
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +53 -0
- package/src/store/use-wall-snap-indicator.ts +2 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/group-move-handle.tsx +0 -316
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/lib/floorplan/selection-tool.ts +0 -271
- package/src/lib/level-name.ts +0 -11
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AnyNode,
|
|
5
|
+
type AnyNodeId,
|
|
6
|
+
type ConstructionDrawingType,
|
|
7
|
+
type FloorplanGeometry,
|
|
8
|
+
type FloorplanPalette,
|
|
9
|
+
type FloorplanPoint,
|
|
10
|
+
type LiveNodeOverrides,
|
|
11
|
+
nodeRegistry,
|
|
12
|
+
resolveBuildingForLevel,
|
|
13
|
+
useScene,
|
|
14
|
+
} from '@pascal-app/core'
|
|
15
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
16
|
+
import { createElement } from 'react'
|
|
17
|
+
import { flushSync } from 'react-dom'
|
|
18
|
+
import { createRoot } from 'react-dom/client'
|
|
19
|
+
import { resolveSvgAnnotationCollisions } from '../../components/editor-2d/renderers/floorplan-annotation-layout'
|
|
20
|
+
import { FloorplanGeometryRenderer } from '../../components/editor-2d/renderers/floorplan-geometry-renderer'
|
|
21
|
+
import {
|
|
22
|
+
buildContext,
|
|
23
|
+
collectFloorplanLinkedLevelNodes,
|
|
24
|
+
floorplanLayerRank,
|
|
25
|
+
getFloorplanLevelData,
|
|
26
|
+
isFloorplanNodeVisible,
|
|
27
|
+
splitFloorplanOverlay,
|
|
28
|
+
} from '../../components/editor-2d/renderers/floorplan-registry-layer'
|
|
29
|
+
import useDrawingView, { DRAWING_TYPE_OPTIONS } from '../../store/use-drawing-view'
|
|
30
|
+
import useEditor from '../../store/use-editor'
|
|
31
|
+
import useFloorplanAnnotationVisibility from '../../store/use-floorplan-annotation-visibility'
|
|
32
|
+
import useFloorplanMode from '../../store/use-floorplan-mode'
|
|
33
|
+
import {
|
|
34
|
+
type FloorplanAnnotationVisibility,
|
|
35
|
+
filterFloorplanAnnotationGeometry,
|
|
36
|
+
} from './annotation-visibility'
|
|
37
|
+
import { resolveNodeForDrawingType } from './drawing-coordination'
|
|
38
|
+
import {
|
|
39
|
+
type FloorplanMetricNotation,
|
|
40
|
+
type FloorplanSchedule,
|
|
41
|
+
type FloorplanWallDimensionReference,
|
|
42
|
+
getFloorplanNodeExtension,
|
|
43
|
+
readFloorplanGeometryMetadata,
|
|
44
|
+
} from './floorplan-extension'
|
|
45
|
+
import {
|
|
46
|
+
type FloorplanMode,
|
|
47
|
+
resolveFloorplanAnnotationVisibility,
|
|
48
|
+
resolveFloorplanWallDimensionReference,
|
|
49
|
+
} from './floorplan-mode'
|
|
50
|
+
import { createFloorplanPdfDocument, type FloorplanPdfDocument } from './floorplan-pdfkit-document'
|
|
51
|
+
import { renderFloorplanGeometryToPdfKit } from './floorplan-pdfkit-renderer'
|
|
52
|
+
import { FLOORPLAN_VIEW_ROTATION_DEG } from './geometry'
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Floorplan PDF export.
|
|
56
|
+
*
|
|
57
|
+
* Re-runs the same registry-driven geometry pipeline the live 2D layer uses
|
|
58
|
+
* (`def.floorplan(node, ctx)` → `FloorplanGeometryRenderer`) headlessly, with
|
|
59
|
+
* a neutral `viewState` so nodes render in their default, unselected form.
|
|
60
|
+
* Every level of the active building becomes its own page, titled with the
|
|
61
|
+
* level's label, with the plan fit to the page (independent of the live
|
|
62
|
+
* pan/zoom). PDFKit is dynamically imported so it only loads when an export
|
|
63
|
+
* actually runs. Geometry and labels are emitted as native PDF vectors and
|
|
64
|
+
* text instead of being reinterpreted from browser SVG.
|
|
65
|
+
*
|
|
66
|
+
* `scope: 'structure'` keeps only `category === 'structure'` nodes (walls,
|
|
67
|
+
* slabs, ceilings, doors, windows, stairs, columns, roofs…); `'full'` keeps
|
|
68
|
+
* every node that has a floorplan builder and is visible.
|
|
69
|
+
*/
|
|
70
|
+
export type FloorplanExportScope = 'full' | 'structure'
|
|
71
|
+
|
|
72
|
+
const SVG_NS = 'http://www.w3.org/2000/svg'
|
|
73
|
+
/** Minimum and proportional margin around the structural drawing bounds. */
|
|
74
|
+
const MIN_PLAN_PADDING_M = 1
|
|
75
|
+
const PLAN_PADDING_RATIO = 0.2
|
|
76
|
+
/** PDF page margin + title band, in pt. */
|
|
77
|
+
const PAGE_MARGIN_PT = 36
|
|
78
|
+
const TITLE_BAND_PT = 28
|
|
79
|
+
const A4_LANDSCAPE_WIDTH_PT = (297 / 25.4) * 72
|
|
80
|
+
const A4_LANDSCAPE_HEIGHT_PT = (210 / 25.4) * 72
|
|
81
|
+
|
|
82
|
+
const NEUTRAL_PALETTE: FloorplanPalette = {
|
|
83
|
+
selectedStroke: '#334155',
|
|
84
|
+
selectedFill: '#ffffff',
|
|
85
|
+
selectedHatch: '#334155',
|
|
86
|
+
wallHoverStroke: '#334155',
|
|
87
|
+
endpointHandleFill: '#ffffff',
|
|
88
|
+
endpointHandleStroke: '#334155',
|
|
89
|
+
endpointHandleHoverStroke: '#334155',
|
|
90
|
+
endpointHandleActiveFill: '#334155',
|
|
91
|
+
endpointHandleActiveStroke: '#334155',
|
|
92
|
+
curveHandleFill: '#ffffff',
|
|
93
|
+
curveHandleStroke: '#334155',
|
|
94
|
+
curveHandleHoverStroke: '#334155',
|
|
95
|
+
measurementStroke: '#334155',
|
|
96
|
+
measurementLabelBackground: '#ffffff',
|
|
97
|
+
measurementLabelText: '#111827',
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Neutral view state — no selection / hover. A neutral palette keeps the
|
|
101
|
+
// full view state (including unit preference) available to node builders.
|
|
102
|
+
const NEUTRAL_VIEW_STATE = {
|
|
103
|
+
selected: false,
|
|
104
|
+
purpose: 'edit',
|
|
105
|
+
highlighted: false,
|
|
106
|
+
hovered: false,
|
|
107
|
+
moving: false,
|
|
108
|
+
palette: NEUTRAL_PALETTE,
|
|
109
|
+
} as const
|
|
110
|
+
|
|
111
|
+
export function resolveFloorplanExportViewState(
|
|
112
|
+
unit: 'metric' | 'imperial',
|
|
113
|
+
metricNotation: FloorplanMetricNotation,
|
|
114
|
+
wallDimensionReference?: FloorplanWallDimensionReference,
|
|
115
|
+
automaticDimensions = true,
|
|
116
|
+
) {
|
|
117
|
+
return {
|
|
118
|
+
...NEUTRAL_VIEW_STATE,
|
|
119
|
+
automaticDimensions,
|
|
120
|
+
unit,
|
|
121
|
+
metricNotation,
|
|
122
|
+
wallDimensionReference,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
type ExportLevel = { id: AnyNodeId; label: string }
|
|
127
|
+
|
|
128
|
+
type ExportGeometry = {
|
|
129
|
+
id: AnyNodeId
|
|
130
|
+
model: FloorplanGeometry | null
|
|
131
|
+
annotations: FloorplanGeometry | null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export type FloorplanPageLayout = {
|
|
135
|
+
planBox: { x: number; y: number; width: number; height: number }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function exportFloorplanPdf(scope: FloorplanExportScope): Promise<void> {
|
|
139
|
+
const nodes = useScene.getState().nodes
|
|
140
|
+
const viewer = useViewer.getState()
|
|
141
|
+
const unit = viewer.unit
|
|
142
|
+
const metricNotation = viewer.metricNotation
|
|
143
|
+
const floorplanMode = useFloorplanMode.getState().mode
|
|
144
|
+
const expertAnnotationState = useFloorplanAnnotationVisibility.getState()
|
|
145
|
+
const annotationVisibility = resolveFloorplanExportAnnotationVisibility(
|
|
146
|
+
floorplanMode,
|
|
147
|
+
expertAnnotationState.visibility,
|
|
148
|
+
)
|
|
149
|
+
const wallDimensionReference = resolveFloorplanWallDimensionReference(
|
|
150
|
+
floorplanMode,
|
|
151
|
+
expertAnnotationState.wallDimensionReference,
|
|
152
|
+
)
|
|
153
|
+
const navigationAzimuth = useEditor.getState().navigationSyncPose?.azimuth
|
|
154
|
+
const drawingType = useDrawingView.getState().drawingType
|
|
155
|
+
const annotationLayoutOverrides = useDrawingView.getState().annotationLayoutOverrides
|
|
156
|
+
const drawingLabel =
|
|
157
|
+
DRAWING_TYPE_OPTIONS.find((option) => option.id === drawingType)?.label ?? 'Floor plan'
|
|
158
|
+
const levels = resolveExportLevels(nodes)
|
|
159
|
+
if (levels.length === 0) {
|
|
160
|
+
console.warn('[floorplan-export] no level to export')
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const { doc, save } = await createFloorplanPdfDocument([
|
|
165
|
+
A4_LANDSCAPE_WIDTH_PT,
|
|
166
|
+
A4_LANDSCAPE_HEIGHT_PT,
|
|
167
|
+
])
|
|
168
|
+
|
|
169
|
+
const host = document.createElement('div')
|
|
170
|
+
host.style.cssText =
|
|
171
|
+
'position:fixed;left:-10000px;top:0;width:1px;height:1px;overflow:hidden;pointer-events:none;'
|
|
172
|
+
document.body.appendChild(host)
|
|
173
|
+
|
|
174
|
+
let pageCount = 0
|
|
175
|
+
try {
|
|
176
|
+
for (const level of levels) {
|
|
177
|
+
const geometries = collectFloorplanGeometry(
|
|
178
|
+
nodes,
|
|
179
|
+
level.id,
|
|
180
|
+
scope,
|
|
181
|
+
unit,
|
|
182
|
+
metricNotation,
|
|
183
|
+
annotationVisibility,
|
|
184
|
+
drawingType,
|
|
185
|
+
wallDimensionReference,
|
|
186
|
+
)
|
|
187
|
+
const schedules = collectFloorplanSchedules(nodes, level.id, unit)
|
|
188
|
+
if (geometries.length === 0 && schedules.length === 0) continue
|
|
189
|
+
const layout = resolveFloorplanPageLayout(A4_LANDSCAPE_WIDTH_PT, A4_LANDSCAPE_HEIGHT_PT)
|
|
190
|
+
|
|
191
|
+
if (geometries.length > 0) {
|
|
192
|
+
// Preserve the live floor-plan orientation rather than forcing north-up.
|
|
193
|
+
const buildingId = resolveBuildingForLevel(level.id, nodes as Record<AnyNodeId, AnyNode>)
|
|
194
|
+
const building = buildingId ? nodes[buildingId] : undefined
|
|
195
|
+
const buildingRotationY = building?.type === 'building' ? (building.rotation[1] ?? 0) : 0
|
|
196
|
+
const rotationDeg = resolveFloorplanExportRotationDeg(buildingRotationY, navigationAzimuth)
|
|
197
|
+
|
|
198
|
+
const mounted = await mountFloorplanSvg(
|
|
199
|
+
host,
|
|
200
|
+
geometries,
|
|
201
|
+
rotationDeg,
|
|
202
|
+
annotationLayoutOverrides,
|
|
203
|
+
)
|
|
204
|
+
if (mounted) {
|
|
205
|
+
try {
|
|
206
|
+
doc.addPage([A4_LANDSCAPE_WIDTH_PT, A4_LANDSCAPE_HEIGHT_PT], 'landscape')
|
|
207
|
+
pageCount++
|
|
208
|
+
|
|
209
|
+
const screenUnitsPerPixel = resolveFloorplanScreenUnitsPerPixel(
|
|
210
|
+
mounted.width,
|
|
211
|
+
mounted.height,
|
|
212
|
+
layout.planBox.width,
|
|
213
|
+
layout.planBox.height,
|
|
214
|
+
)
|
|
215
|
+
await mounted.setScreenUnitsPerPixel(screenUnitsPerPixel)
|
|
216
|
+
const fitted = resolveFloorplanExportPlacement(
|
|
217
|
+
mounted.width,
|
|
218
|
+
mounted.height,
|
|
219
|
+
layout.planBox.x,
|
|
220
|
+
layout.planBox.y,
|
|
221
|
+
layout.planBox.width,
|
|
222
|
+
layout.planBox.height,
|
|
223
|
+
)
|
|
224
|
+
drawFloorplanPageHeader(doc, level.label, drawingLabel)
|
|
225
|
+
const model = combineGeometryList(geometries.map((geometry) => geometry.model))
|
|
226
|
+
if (model) {
|
|
227
|
+
await renderFloorplanGeometryToPdfKit(doc, model, {
|
|
228
|
+
annotationLayer: false,
|
|
229
|
+
placement: fitted,
|
|
230
|
+
rotationDeg,
|
|
231
|
+
viewport: mounted.viewport,
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
const annotations = combineGeometryList(
|
|
235
|
+
geometries.map((geometry) => geometry.annotations),
|
|
236
|
+
)
|
|
237
|
+
if (annotations) {
|
|
238
|
+
await renderFloorplanGeometryToPdfKit(doc, annotations, {
|
|
239
|
+
annotationLabelShifts: mounted.annotationLabelShifts,
|
|
240
|
+
annotationLayer: true,
|
|
241
|
+
placement: fitted,
|
|
242
|
+
rotationDeg,
|
|
243
|
+
viewport: mounted.viewport,
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
} finally {
|
|
247
|
+
mounted.cleanup()
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (schedules.length > 0) {
|
|
253
|
+
pageCount = drawFloorplanSchedulePages(doc, level.label, schedules, pageCount)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (pageCount === 0) {
|
|
258
|
+
console.warn(`[floorplan-export] nothing to export for scope "${scope}"`)
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const date = new Date().toISOString().split('T')[0]
|
|
263
|
+
await save(`${drawingType}_${scope}_${date}.pdf`)
|
|
264
|
+
} finally {
|
|
265
|
+
host.remove()
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function collectFloorplanSchedules(
|
|
270
|
+
nodes: Record<string, AnyNode>,
|
|
271
|
+
levelId: AnyNodeId,
|
|
272
|
+
unit: 'metric' | 'imperial',
|
|
273
|
+
): FloorplanSchedule[] {
|
|
274
|
+
const siblingsByType = new Map<string, AnyNode[]>()
|
|
275
|
+
const visit = (id: AnyNodeId) => {
|
|
276
|
+
const node = nodes[id]
|
|
277
|
+
if (!node) return
|
|
278
|
+
if (node.visible !== false) {
|
|
279
|
+
const siblings = siblingsByType.get(node.type)
|
|
280
|
+
if (siblings) siblings.push(node)
|
|
281
|
+
else siblingsByType.set(node.type, [node])
|
|
282
|
+
}
|
|
283
|
+
const children = (node as { children?: AnyNodeId[] }).children
|
|
284
|
+
if (Array.isArray(children)) for (const childId of children) visit(childId)
|
|
285
|
+
}
|
|
286
|
+
visit(levelId)
|
|
287
|
+
|
|
288
|
+
const schedules: FloorplanSchedule[] = []
|
|
289
|
+
for (const [kind, definition] of nodeRegistry.entries()) {
|
|
290
|
+
const scheduleContribution = getFloorplanNodeExtension(definition)?.schedule
|
|
291
|
+
if (!scheduleContribution) continue
|
|
292
|
+
const siblings = siblingsByType.get(kind) ?? []
|
|
293
|
+
const schedule = scheduleContribution({ siblings, nodes, levelId, unit })
|
|
294
|
+
if (schedule && schedule.rows.length > 0) schedules.push(schedule)
|
|
295
|
+
}
|
|
296
|
+
return schedules
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function resolveFloorplanPageLayout(
|
|
300
|
+
pageWidth: number,
|
|
301
|
+
pageHeight: number,
|
|
302
|
+
): FloorplanPageLayout {
|
|
303
|
+
const planY = PAGE_MARGIN_PT + TITLE_BAND_PT
|
|
304
|
+
return {
|
|
305
|
+
planBox: {
|
|
306
|
+
x: PAGE_MARGIN_PT,
|
|
307
|
+
y: planY,
|
|
308
|
+
width: pageWidth - PAGE_MARGIN_PT * 2,
|
|
309
|
+
height: pageHeight - planY - PAGE_MARGIN_PT,
|
|
310
|
+
},
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function drawFloorplanPageHeader(
|
|
315
|
+
doc: FloorplanPdfDocument,
|
|
316
|
+
levelLabel: string,
|
|
317
|
+
drawingLabel: string,
|
|
318
|
+
): void {
|
|
319
|
+
doc.setTextColor('#111827')
|
|
320
|
+
doc.setFont('helvetica', 'bold')
|
|
321
|
+
doc.setFontSize(14)
|
|
322
|
+
doc.text(`${levelLabel} - ${drawingLabel}`, PAGE_MARGIN_PT, PAGE_MARGIN_PT + 12)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function drawFloorplanSchedulePages(
|
|
326
|
+
doc: FloorplanPdfDocument,
|
|
327
|
+
levelLabel: string,
|
|
328
|
+
schedules: readonly FloorplanSchedule[],
|
|
329
|
+
initialPageCount: number,
|
|
330
|
+
): number {
|
|
331
|
+
const pageW = doc.internal.pageSize.getWidth()
|
|
332
|
+
const pageH = doc.internal.pageSize.getHeight()
|
|
333
|
+
const tableWidth = pageW - PAGE_MARGIN_PT * 2
|
|
334
|
+
const bottom = pageH - PAGE_MARGIN_PT
|
|
335
|
+
const headerHeight = 22
|
|
336
|
+
const rowHeight = 20
|
|
337
|
+
let pageCount = initialPageCount
|
|
338
|
+
let y = 0
|
|
339
|
+
|
|
340
|
+
const startPage = () => {
|
|
341
|
+
doc.addPage([pageW, pageH])
|
|
342
|
+
pageCount++
|
|
343
|
+
doc.setTextColor('#111827')
|
|
344
|
+
doc.setFont('helvetica', 'bold')
|
|
345
|
+
doc.setFontSize(14)
|
|
346
|
+
doc.setLineWidth(0.5)
|
|
347
|
+
doc.text(`${levelLabel} - Construction Schedules`, PAGE_MARGIN_PT, PAGE_MARGIN_PT + 12)
|
|
348
|
+
y = PAGE_MARGIN_PT + TITLE_BAND_PT + 8
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const drawHeader = (schedule: FloorplanSchedule, continued: boolean) => {
|
|
352
|
+
doc.setTextColor('#111827')
|
|
353
|
+
doc.setFont('helvetica', 'bold')
|
|
354
|
+
doc.setFontSize(11)
|
|
355
|
+
doc.text(`${schedule.title}${continued ? ' (CONTINUED)' : ''}`, PAGE_MARGIN_PT, y + 11)
|
|
356
|
+
y += 18
|
|
357
|
+
doc.setFillColor('#334155')
|
|
358
|
+
doc.rect(PAGE_MARGIN_PT, y, tableWidth, headerHeight, 'F')
|
|
359
|
+
doc.setTextColor('#ffffff')
|
|
360
|
+
doc.setFontSize(8)
|
|
361
|
+
const widths = scheduleColumnWidths(schedule, tableWidth)
|
|
362
|
+
doc.setDrawColor('#64748b')
|
|
363
|
+
drawScheduleColumnDividers(doc, widths, y, headerHeight)
|
|
364
|
+
let x = PAGE_MARGIN_PT
|
|
365
|
+
schedule.columns.forEach((column, index) => {
|
|
366
|
+
const width = widths[index] ?? 0
|
|
367
|
+
doc.text(column.label, x + 4, y + 14, { maxWidth: Math.max(0, width - 8) })
|
|
368
|
+
x += width
|
|
369
|
+
})
|
|
370
|
+
y += headerHeight
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
startPage()
|
|
374
|
+
for (const schedule of schedules) {
|
|
375
|
+
const issueHeight = (schedule.issues?.length ?? 0) * 13
|
|
376
|
+
const minimumTableHeight = 18 + issueHeight + headerHeight + rowHeight
|
|
377
|
+
if (y + minimumTableHeight > bottom) startPage()
|
|
378
|
+
|
|
379
|
+
if (schedule.issues?.length) {
|
|
380
|
+
doc.setTextColor('#b45309')
|
|
381
|
+
doc.setFont('helvetica', 'normal')
|
|
382
|
+
doc.setFontSize(8)
|
|
383
|
+
for (const issue of schedule.issues) {
|
|
384
|
+
doc.text(`WARNING: ${issue}`, PAGE_MARGIN_PT, y + 9)
|
|
385
|
+
y += 13
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
drawHeader(schedule, false)
|
|
390
|
+
const widths = scheduleColumnWidths(schedule, tableWidth)
|
|
391
|
+
schedule.rows.forEach((row, rowIndex) => {
|
|
392
|
+
if (y + rowHeight > bottom) {
|
|
393
|
+
startPage()
|
|
394
|
+
drawHeader(schedule, true)
|
|
395
|
+
}
|
|
396
|
+
if (rowIndex % 2 === 1) {
|
|
397
|
+
doc.setFillColor('#f1f5f9')
|
|
398
|
+
doc.rect(PAGE_MARGIN_PT, y, tableWidth, rowHeight, 'F')
|
|
399
|
+
}
|
|
400
|
+
doc.setDrawColor('#cbd5e1')
|
|
401
|
+
doc.rect(PAGE_MARGIN_PT, y, tableWidth, rowHeight)
|
|
402
|
+
drawScheduleColumnDividers(doc, widths, y, rowHeight)
|
|
403
|
+
doc.setTextColor('#111827')
|
|
404
|
+
doc.setFont('helvetica', 'normal')
|
|
405
|
+
doc.setFontSize(8)
|
|
406
|
+
let x = PAGE_MARGIN_PT
|
|
407
|
+
schedule.columns.forEach((column, columnIndex) => {
|
|
408
|
+
const width = widths[columnIndex] ?? 0
|
|
409
|
+
const text = truncatePdfText(doc, row.cells[column.key] ?? '', Math.max(0, width - 8))
|
|
410
|
+
doc.text(text, x + 4, y + 13)
|
|
411
|
+
x += width
|
|
412
|
+
})
|
|
413
|
+
y += rowHeight
|
|
414
|
+
})
|
|
415
|
+
y += 20
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return pageCount
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function scheduleColumnWidths(schedule: FloorplanSchedule, tableWidth: number): number[] {
|
|
422
|
+
const totalWeight = schedule.columns.reduce((sum, column) => sum + (column.weight ?? 1), 0)
|
|
423
|
+
return schedule.columns.map((column) => (tableWidth * (column.weight ?? 1)) / totalWeight)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function drawScheduleColumnDividers(
|
|
427
|
+
doc: FloorplanPdfDocument,
|
|
428
|
+
widths: readonly number[],
|
|
429
|
+
y: number,
|
|
430
|
+
height: number,
|
|
431
|
+
) {
|
|
432
|
+
let x = PAGE_MARGIN_PT
|
|
433
|
+
for (const width of widths.slice(0, -1)) {
|
|
434
|
+
x += width
|
|
435
|
+
doc.line(x, y, x, y + height)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function truncatePdfText(doc: FloorplanPdfDocument, value: string, maxWidth: number): string {
|
|
440
|
+
if (doc.getTextWidth(value) <= maxWidth) return value
|
|
441
|
+
let truncated = value
|
|
442
|
+
while (truncated.length > 0 && doc.getTextWidth(`${truncated}...`) > maxWidth) {
|
|
443
|
+
truncated = truncated.slice(0, -1)
|
|
444
|
+
}
|
|
445
|
+
return `${truncated}...`
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
type MountedFloorplan = {
|
|
449
|
+
svg: SVGSVGElement
|
|
450
|
+
annotationLabelShifts: readonly FloorplanPoint[]
|
|
451
|
+
/** Padded viewBox dimensions, in meters — used for aspect-preserving fit. */
|
|
452
|
+
width: number
|
|
453
|
+
height: number
|
|
454
|
+
viewport: FloorplanExportBounds
|
|
455
|
+
setScreenUnitsPerPixel: (value: number) => Promise<void>
|
|
456
|
+
cleanup: () => void
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export type FloorplanExportBounds = {
|
|
460
|
+
x: number
|
|
461
|
+
y: number
|
|
462
|
+
width: number
|
|
463
|
+
height: number
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export function fitPlanToBox(
|
|
467
|
+
planWidth: number,
|
|
468
|
+
planHeight: number,
|
|
469
|
+
boxX: number,
|
|
470
|
+
boxY: number,
|
|
471
|
+
boxWidth: number,
|
|
472
|
+
boxHeight: number,
|
|
473
|
+
) {
|
|
474
|
+
const aspect = planWidth / planHeight
|
|
475
|
+
let width = boxWidth
|
|
476
|
+
let height = width / aspect
|
|
477
|
+
if (height > boxHeight) {
|
|
478
|
+
height = boxHeight
|
|
479
|
+
width = height * aspect
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
x: boxX + (boxWidth - width) / 2,
|
|
483
|
+
y: boxY + (boxHeight - height) / 2,
|
|
484
|
+
width,
|
|
485
|
+
height,
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export function resolveFloorplanExportPlacement(
|
|
490
|
+
planWidth: number,
|
|
491
|
+
planHeight: number,
|
|
492
|
+
boxX: number,
|
|
493
|
+
boxY: number,
|
|
494
|
+
boxWidth: number,
|
|
495
|
+
boxHeight: number,
|
|
496
|
+
) {
|
|
497
|
+
return fitPlanToBox(planWidth, planHeight, boxX, boxY, boxWidth, boxHeight)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export function resolveFloorplanExportViewport(
|
|
501
|
+
modelBounds: FloorplanExportBounds,
|
|
502
|
+
): FloorplanExportBounds {
|
|
503
|
+
const padding = Math.max(
|
|
504
|
+
MIN_PLAN_PADDING_M,
|
|
505
|
+
Math.max(modelBounds.width, modelBounds.height) * PLAN_PADDING_RATIO,
|
|
506
|
+
)
|
|
507
|
+
return {
|
|
508
|
+
x: modelBounds.x - padding,
|
|
509
|
+
y: modelBounds.y - padding,
|
|
510
|
+
width: modelBounds.width + padding * 2,
|
|
511
|
+
height: modelBounds.height + padding * 2,
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function rotateFloorplanExportBounds(
|
|
516
|
+
bounds: FloorplanExportBounds,
|
|
517
|
+
rotationDeg: number,
|
|
518
|
+
): FloorplanExportBounds {
|
|
519
|
+
const radians = (rotationDeg * Math.PI) / 180
|
|
520
|
+
const cosine = Math.cos(radians)
|
|
521
|
+
const sine = Math.sin(radians)
|
|
522
|
+
const corners = [
|
|
523
|
+
[bounds.x, bounds.y],
|
|
524
|
+
[bounds.x + bounds.width, bounds.y],
|
|
525
|
+
[bounds.x + bounds.width, bounds.y + bounds.height],
|
|
526
|
+
[bounds.x, bounds.y + bounds.height],
|
|
527
|
+
] as const
|
|
528
|
+
const rotated = corners.map(([x, y]) => ({
|
|
529
|
+
x: x * cosine - y * sine,
|
|
530
|
+
y: x * sine + y * cosine,
|
|
531
|
+
}))
|
|
532
|
+
const minX = Math.min(...rotated.map((point) => point.x))
|
|
533
|
+
const minY = Math.min(...rotated.map((point) => point.y))
|
|
534
|
+
const maxX = Math.max(...rotated.map((point) => point.x))
|
|
535
|
+
const maxY = Math.max(...rotated.map((point) => point.y))
|
|
536
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export function resolveFloorplanScreenUnitsPerPixel(
|
|
540
|
+
modelWidth: number,
|
|
541
|
+
modelHeight: number,
|
|
542
|
+
boxWidth: number,
|
|
543
|
+
boxHeight: number,
|
|
544
|
+
): number {
|
|
545
|
+
return Math.max(modelWidth / boxWidth, modelHeight / boxHeight)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export function resolveFloorplanExportAnnotationVisibility(
|
|
549
|
+
mode: FloorplanMode,
|
|
550
|
+
liveVisibility: FloorplanAnnotationVisibility,
|
|
551
|
+
): FloorplanAnnotationVisibility {
|
|
552
|
+
return resolveFloorplanAnnotationVisibility(mode, liveVisibility, { target: 'export' })
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export function resolveFloorplanExportRotationDeg(
|
|
556
|
+
buildingRotationY: number,
|
|
557
|
+
navigationAzimuth?: number,
|
|
558
|
+
): number {
|
|
559
|
+
const userRotationDeg =
|
|
560
|
+
navigationAzimuth === undefined
|
|
561
|
+
? 0
|
|
562
|
+
: (navigationAzimuth * 180) / Math.PI - FLOORPLAN_VIEW_ROTATION_DEG
|
|
563
|
+
return FLOORPLAN_VIEW_ROTATION_DEG + userRotationDeg - (buildingRotationY * 180) / Math.PI
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async function mountFloorplanSvg(
|
|
567
|
+
parent: HTMLElement,
|
|
568
|
+
geometries: ExportGeometry[],
|
|
569
|
+
rotationDeg: number,
|
|
570
|
+
annotationLayoutOverrides = useDrawingView.getState().annotationLayoutOverrides,
|
|
571
|
+
): Promise<MountedFloorplan | null> {
|
|
572
|
+
const container = document.createElement('div')
|
|
573
|
+
parent.appendChild(container)
|
|
574
|
+
const root = createRoot(container)
|
|
575
|
+
const cleanup = () => {
|
|
576
|
+
root.unmount()
|
|
577
|
+
container.remove()
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const render = (screenUnitsPerPixel?: number) => {
|
|
581
|
+
flushSync(() => {
|
|
582
|
+
root.render(
|
|
583
|
+
createElement(
|
|
584
|
+
'svg',
|
|
585
|
+
{ xmlns: SVG_NS },
|
|
586
|
+
createElement(
|
|
587
|
+
'g',
|
|
588
|
+
{ 'data-floorplan-content': '' },
|
|
589
|
+
createElement(
|
|
590
|
+
'g',
|
|
591
|
+
{ transform: `rotate(${rotationDeg})` },
|
|
592
|
+
createElement(
|
|
593
|
+
'g',
|
|
594
|
+
{ 'data-floorplan-model': '' },
|
|
595
|
+
geometries.map(({ id, model }) =>
|
|
596
|
+
model
|
|
597
|
+
? createElement(FloorplanGeometryRenderer, {
|
|
598
|
+
key: id,
|
|
599
|
+
geometry: model,
|
|
600
|
+
sceneRotationDeg: rotationDeg,
|
|
601
|
+
})
|
|
602
|
+
: null,
|
|
603
|
+
),
|
|
604
|
+
),
|
|
605
|
+
createElement(
|
|
606
|
+
'g',
|
|
607
|
+
{ 'data-floorplan-annotations': '' },
|
|
608
|
+
geometries.map(({ id, annotations }) =>
|
|
609
|
+
annotations
|
|
610
|
+
? createElement(FloorplanGeometryRenderer, {
|
|
611
|
+
key: id,
|
|
612
|
+
geometry: annotations,
|
|
613
|
+
renderMode: 'pdf',
|
|
614
|
+
sceneRotationDeg: rotationDeg,
|
|
615
|
+
screenUnitsPerPixel,
|
|
616
|
+
})
|
|
617
|
+
: null,
|
|
618
|
+
),
|
|
619
|
+
),
|
|
620
|
+
),
|
|
621
|
+
),
|
|
622
|
+
),
|
|
623
|
+
)
|
|
624
|
+
})
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
render()
|
|
628
|
+
|
|
629
|
+
// Give async asset images (item icons) a couple of frames to resolve so
|
|
630
|
+
// they're included in the measured bounds and the rendered output.
|
|
631
|
+
await nextFrames(2)
|
|
632
|
+
|
|
633
|
+
const svg = container.querySelector('svg')
|
|
634
|
+
if (!svg) {
|
|
635
|
+
cleanup()
|
|
636
|
+
return null
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const modelBounds = measureFloorplanBounds(svg, '[data-floorplan-model]')
|
|
640
|
+
if (!modelBounds) {
|
|
641
|
+
cleanup()
|
|
642
|
+
return null
|
|
643
|
+
}
|
|
644
|
+
const viewport = resolveFloorplanExportViewport(
|
|
645
|
+
rotateFloorplanExportBounds(modelBounds, rotationDeg),
|
|
646
|
+
)
|
|
647
|
+
const mounted: MountedFloorplan = {
|
|
648
|
+
svg,
|
|
649
|
+
annotationLabelShifts: [],
|
|
650
|
+
width: viewport.width,
|
|
651
|
+
height: viewport.height,
|
|
652
|
+
viewport,
|
|
653
|
+
cleanup,
|
|
654
|
+
setScreenUnitsPerPixel: async (value) => {
|
|
655
|
+
render(value)
|
|
656
|
+
applyFloorplanViewport(mounted, viewport, value)
|
|
657
|
+
await nextFrames(1)
|
|
658
|
+
resolveSvgAnnotationCollisions(svg, { layoutOverrides: annotationLayoutOverrides })
|
|
659
|
+
mounted.annotationLabelShifts = readSvgAnnotationLabelShifts(svg)
|
|
660
|
+
},
|
|
661
|
+
}
|
|
662
|
+
applyFloorplanViewport(mounted, viewport)
|
|
663
|
+
return mounted
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function readSvgAnnotationLabelShifts(svg: SVGSVGElement): FloorplanPoint[] {
|
|
667
|
+
return Array.from(svg.querySelectorAll<SVGGElement>('[data-floorplan-annotation-label]')).map(
|
|
668
|
+
(label) => {
|
|
669
|
+
const x = Number(label.dataset.floorplanAnnotationLayoutDx ?? 0)
|
|
670
|
+
const y = Number(label.dataset.floorplanAnnotationLayoutDy ?? 0)
|
|
671
|
+
return [Number.isFinite(x) ? x : 0, Number.isFinite(y) ? y : 0]
|
|
672
|
+
},
|
|
673
|
+
)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function measureFloorplanBounds(
|
|
677
|
+
svg: SVGSVGElement,
|
|
678
|
+
selector: string,
|
|
679
|
+
): FloorplanExportBounds | null {
|
|
680
|
+
const content = svg.querySelector(selector) as SVGGraphicsElement | null
|
|
681
|
+
const bbox = content?.getBBox()
|
|
682
|
+
if (!bbox || bbox.width === 0 || bbox.height === 0) return null
|
|
683
|
+
return { x: bbox.x, y: bbox.y, width: bbox.width, height: bbox.height }
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
export function resolveFloorplanMeasurementSize(
|
|
687
|
+
viewport: FloorplanExportBounds,
|
|
688
|
+
screenUnitsPerPixel: number,
|
|
689
|
+
): { width: number; height: number } {
|
|
690
|
+
return {
|
|
691
|
+
width: viewport.width / screenUnitsPerPixel,
|
|
692
|
+
height: viewport.height / screenUnitsPerPixel,
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function applyFloorplanViewport(
|
|
697
|
+
mounted: MountedFloorplan,
|
|
698
|
+
viewport: FloorplanExportBounds,
|
|
699
|
+
screenUnitsPerPixel?: number,
|
|
700
|
+
): void {
|
|
701
|
+
mounted.width = viewport.width
|
|
702
|
+
mounted.height = viewport.height
|
|
703
|
+
mounted.svg.setAttribute(
|
|
704
|
+
'viewBox',
|
|
705
|
+
`${viewport.x} ${viewport.y} ${viewport.width} ${viewport.height}`,
|
|
706
|
+
)
|
|
707
|
+
const measurementSize = screenUnitsPerPixel
|
|
708
|
+
? resolveFloorplanMeasurementSize(viewport, screenUnitsPerPixel)
|
|
709
|
+
: { width: mounted.width, height: mounted.height }
|
|
710
|
+
mounted.svg.setAttribute('width', `${measurementSize.width}`)
|
|
711
|
+
mounted.svg.setAttribute('height', `${measurementSize.height}`)
|
|
712
|
+
|
|
713
|
+
mounted.svg.querySelector('[data-floorplan-background]')?.remove()
|
|
714
|
+
const background = document.createElementNS(SVG_NS, 'rect')
|
|
715
|
+
background.setAttribute('data-floorplan-background', '')
|
|
716
|
+
background.setAttribute('x', `${viewport.x}`)
|
|
717
|
+
background.setAttribute('y', `${viewport.y}`)
|
|
718
|
+
background.setAttribute('width', `${mounted.width}`)
|
|
719
|
+
background.setAttribute('height', `${mounted.height}`)
|
|
720
|
+
background.setAttribute('fill', '#ffffff')
|
|
721
|
+
mounted.svg.insertBefore(background, mounted.svg.firstChild)
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function collectFloorplanGeometry(
|
|
725
|
+
nodes: Record<string, AnyNode>,
|
|
726
|
+
levelId: AnyNodeId,
|
|
727
|
+
scope: FloorplanExportScope,
|
|
728
|
+
unit: 'metric' | 'imperial',
|
|
729
|
+
metricNotation: FloorplanMetricNotation,
|
|
730
|
+
annotationVisibility: FloorplanAnnotationVisibility,
|
|
731
|
+
drawingType: ConstructionDrawingType,
|
|
732
|
+
wallDimensionReference: FloorplanWallDimensionReference,
|
|
733
|
+
): ExportGeometry[] {
|
|
734
|
+
const noLiveOverrides = new Map<string, LiveNodeOverrides>()
|
|
735
|
+
const levelNodeIdsByType = new Map<string, AnyNodeId[]>()
|
|
736
|
+
const entries: { id: AnyNodeId; node: AnyNode; parentOverride?: AnyNode }[] = []
|
|
737
|
+
|
|
738
|
+
const visit = (id: AnyNodeId) => {
|
|
739
|
+
const node = nodes[id]
|
|
740
|
+
if (!node) return
|
|
741
|
+
const def = nodeRegistry.get(node.type)
|
|
742
|
+
if (def?.computeFloorplanLevelData) {
|
|
743
|
+
const ids = levelNodeIdsByType.get(node.type)
|
|
744
|
+
if (ids) ids.push(id)
|
|
745
|
+
else levelNodeIdsByType.set(node.type, [id])
|
|
746
|
+
}
|
|
747
|
+
if (
|
|
748
|
+
def?.floorplan &&
|
|
749
|
+
isFloorplanNodeVisible(node) &&
|
|
750
|
+
(scope === 'full' || def.category === 'structure')
|
|
751
|
+
) {
|
|
752
|
+
const drawingNode = resolveNodeForDrawingType(node, nodes, drawingType)
|
|
753
|
+
if (drawingNode) entries.push({ id, node: drawingNode })
|
|
754
|
+
}
|
|
755
|
+
const childIds = (node as { children?: AnyNodeId[] }).children
|
|
756
|
+
if (Array.isArray(childIds)) for (const cid of childIds) visit(cid)
|
|
757
|
+
}
|
|
758
|
+
visit(levelId)
|
|
759
|
+
|
|
760
|
+
const activeLevelNode = nodes[levelId]
|
|
761
|
+
if (activeLevelNode) {
|
|
762
|
+
const collectedIds = new Set(entries.map((entry) => entry.id))
|
|
763
|
+
for (const linked of collectFloorplanLinkedLevelNodes(nodes, levelId, collectedIds)) {
|
|
764
|
+
const definition = nodeRegistry.get(linked.node.type)
|
|
765
|
+
if (
|
|
766
|
+
isFloorplanNodeVisible(linked.node) &&
|
|
767
|
+
(scope === 'full' || definition?.category === 'structure')
|
|
768
|
+
) {
|
|
769
|
+
const drawingNode = resolveNodeForDrawingType(linked.node, nodes, drawingType)
|
|
770
|
+
if (drawingNode) {
|
|
771
|
+
entries.push({ id: linked.id, node: drawingNode, parentOverride: activeLevelNode })
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Document order is paint order — sort the same way the live layer does so
|
|
778
|
+
// zones sit under walls/slabs/furniture rather than on top of them.
|
|
779
|
+
entries.sort((a, b) => floorplanLayerRank(a.node.type) - floorplanLayerRank(b.node.type))
|
|
780
|
+
|
|
781
|
+
// One-shot per-type cache for `computeFloorplanLevelData`; value type is
|
|
782
|
+
// module-private to the registry layer, so let it infer.
|
|
783
|
+
const levelDataCache = new Map()
|
|
784
|
+
const out: ExportGeometry[] = []
|
|
785
|
+
for (const { id, node, parentOverride } of entries) {
|
|
786
|
+
const builder = nodeRegistry.get(node.type)?.floorplan
|
|
787
|
+
if (!builder) continue
|
|
788
|
+
const levelData = getFloorplanLevelData(
|
|
789
|
+
node.type,
|
|
790
|
+
nodes,
|
|
791
|
+
noLiveOverrides,
|
|
792
|
+
levelNodeIdsByType,
|
|
793
|
+
levelDataCache,
|
|
794
|
+
)
|
|
795
|
+
const baseContext = buildContext(
|
|
796
|
+
node,
|
|
797
|
+
nodes,
|
|
798
|
+
resolveFloorplanExportViewState(
|
|
799
|
+
unit,
|
|
800
|
+
metricNotation,
|
|
801
|
+
wallDimensionReference,
|
|
802
|
+
annotationVisibility.automaticDimensions,
|
|
803
|
+
),
|
|
804
|
+
levelData,
|
|
805
|
+
)
|
|
806
|
+
const ctx = parentOverride ? { ...baseContext, parent: parentOverride } : baseContext
|
|
807
|
+
const geometry = builder(node, ctx)
|
|
808
|
+
if (!geometry) continue
|
|
809
|
+
const visibleGeometry = filterFloorplanAnnotationGeometry(geometry, annotationVisibility)
|
|
810
|
+
if (!visibleGeometry) continue
|
|
811
|
+
const { base, overlay } = splitFloorplanOverlay(visibleGeometry)
|
|
812
|
+
const exportOverlay = overlay ? filterFloorplanExportOverlay(overlay) : null
|
|
813
|
+
const annotationOnly = isFloorplanExportAnnotationGeometry(visibleGeometry)
|
|
814
|
+
const { model, annotations } = resolveFloorplanExportNodeGeometry(
|
|
815
|
+
base,
|
|
816
|
+
exportOverlay,
|
|
817
|
+
annotationOnly,
|
|
818
|
+
)
|
|
819
|
+
if (model || annotations) out.push({ id, model, annotations })
|
|
820
|
+
}
|
|
821
|
+
return out
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
export function filterFloorplanExportOverlay(
|
|
825
|
+
geometry: FloorplanGeometry,
|
|
826
|
+
): FloorplanGeometry | null {
|
|
827
|
+
if (FLOORPLAN_EXPORT_EDITING_KINDS.has(geometry.kind)) return null
|
|
828
|
+
if (geometry.kind !== 'group') return geometry
|
|
829
|
+
|
|
830
|
+
const children = geometry.children
|
|
831
|
+
.map(filterFloorplanExportOverlay)
|
|
832
|
+
.filter((child): child is FloorplanGeometry => child !== null)
|
|
833
|
+
if (children.length === 0) return null
|
|
834
|
+
return { ...geometry, children }
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const FLOORPLAN_EXPORT_EDITING_KINDS = new Set<FloorplanGeometry['kind']>([
|
|
838
|
+
'endpoint-handle',
|
|
839
|
+
'midpoint-handle',
|
|
840
|
+
'edge-handle',
|
|
841
|
+
'move-handle',
|
|
842
|
+
'move-arrow',
|
|
843
|
+
'rotate-arrow',
|
|
844
|
+
])
|
|
845
|
+
|
|
846
|
+
type FloorplanExportOverlayPartition = {
|
|
847
|
+
model: FloorplanGeometry | null
|
|
848
|
+
annotations: FloorplanGeometry | null
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
export function resolveFloorplanExportNodeGeometry(
|
|
852
|
+
base: FloorplanGeometry | null,
|
|
853
|
+
overlay: FloorplanGeometry | null,
|
|
854
|
+
annotationOnly: boolean,
|
|
855
|
+
): FloorplanExportOverlayPartition {
|
|
856
|
+
const combined = combineGeometry(base, overlay)
|
|
857
|
+
if (annotationOnly) return { model: null, annotations: combined }
|
|
858
|
+
return combined ? partitionFloorplanExportOverlay(combined) : { model: null, annotations: null }
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
export function partitionFloorplanExportOverlay(
|
|
862
|
+
geometry: FloorplanGeometry,
|
|
863
|
+
): FloorplanExportOverlayPartition {
|
|
864
|
+
if (FLOORPLAN_EXPORT_EDITING_KINDS.has(geometry.kind)) {
|
|
865
|
+
return { model: null, annotations: null }
|
|
866
|
+
}
|
|
867
|
+
if (isFloorplanExportAnnotationGeometry(geometry)) {
|
|
868
|
+
return { model: null, annotations: filterFloorplanExportOverlay(geometry) }
|
|
869
|
+
}
|
|
870
|
+
if (geometry.kind !== 'group') {
|
|
871
|
+
return { model: geometry, annotations: null }
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const modelChildren: FloorplanGeometry[] = []
|
|
875
|
+
const annotationChildren: FloorplanGeometry[] = []
|
|
876
|
+
for (const child of geometry.children) {
|
|
877
|
+
const partition = partitionFloorplanExportOverlay(child)
|
|
878
|
+
if (partition.model) modelChildren.push(partition.model)
|
|
879
|
+
if (partition.annotations) annotationChildren.push(partition.annotations)
|
|
880
|
+
}
|
|
881
|
+
return {
|
|
882
|
+
model:
|
|
883
|
+
modelChildren.length > 0
|
|
884
|
+
? { kind: 'group', children: modelChildren, transform: geometry.transform }
|
|
885
|
+
: null,
|
|
886
|
+
annotations:
|
|
887
|
+
annotationChildren.length > 0
|
|
888
|
+
? { kind: 'group', children: annotationChildren, transform: geometry.transform }
|
|
889
|
+
: null,
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
export function isFloorplanExportAnnotationGeometry(geometry: FloorplanGeometry): boolean {
|
|
894
|
+
if (
|
|
895
|
+
geometry.kind === 'text' ||
|
|
896
|
+
geometry.kind === 'dimension' ||
|
|
897
|
+
geometry.kind === 'dimension-string' ||
|
|
898
|
+
geometry.kind === 'dimension-label' ||
|
|
899
|
+
geometry.kind === 'equal-spacing-badge'
|
|
900
|
+
) {
|
|
901
|
+
return true
|
|
902
|
+
}
|
|
903
|
+
if (readFloorplanGeometryMetadata(geometry).annotationRole) return true
|
|
904
|
+
return false
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function combineGeometry(
|
|
908
|
+
base: FloorplanGeometry | null,
|
|
909
|
+
overlay: FloorplanGeometry | null,
|
|
910
|
+
): FloorplanGeometry | null {
|
|
911
|
+
if (!base) return overlay
|
|
912
|
+
if (!overlay) return base
|
|
913
|
+
return { kind: 'group', children: [base, overlay] }
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function combineGeometryList(
|
|
917
|
+
geometries: readonly (FloorplanGeometry | null)[],
|
|
918
|
+
): FloorplanGeometry | null {
|
|
919
|
+
const children = geometries.filter((geometry): geometry is FloorplanGeometry => geometry !== null)
|
|
920
|
+
if (children.length === 0) return null
|
|
921
|
+
if (children.length === 1) return children[0] ?? null
|
|
922
|
+
return { kind: 'group', children }
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Levels to export, ordered bottom-to-top. The active building (the building
|
|
927
|
+
* owning the selected level, or the first one found) contributes all of its
|
|
928
|
+
* level children; if there is no building wrapper we fall back to the single
|
|
929
|
+
* resolved level.
|
|
930
|
+
*/
|
|
931
|
+
function resolveExportLevels(nodes: Record<string, AnyNode>): ExportLevel[] {
|
|
932
|
+
const selected = useViewer.getState().selection.levelId as AnyNodeId | null | undefined
|
|
933
|
+
const activeLevelId = selected && nodes[selected] ? selected : firstLevelId(nodes)
|
|
934
|
+
if (!activeLevelId) return []
|
|
935
|
+
|
|
936
|
+
const buildingId = resolveBuildingForLevel(activeLevelId, nodes as Record<AnyNodeId, AnyNode>)
|
|
937
|
+
let levelNodes: AnyNode[]
|
|
938
|
+
if (buildingId) {
|
|
939
|
+
const childIds = (nodes[buildingId] as { children?: AnyNodeId[] }).children ?? []
|
|
940
|
+
levelNodes = childIds.map((id) => nodes[id]).filter((n): n is AnyNode => n?.type === 'level')
|
|
941
|
+
} else {
|
|
942
|
+
const node = nodes[activeLevelId]
|
|
943
|
+
levelNodes = node ? [node] : []
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
levelNodes.sort((a, b) => levelIndexOf(a) - levelIndexOf(b))
|
|
947
|
+
return levelNodes.map((n) => ({ id: n.id as AnyNodeId, label: levelLabelOf(n) }))
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function firstLevelId(nodes: Record<string, AnyNode>): AnyNodeId | null {
|
|
951
|
+
for (const node of Object.values(nodes)) {
|
|
952
|
+
if (node.type === 'level') return node.id as AnyNodeId
|
|
953
|
+
}
|
|
954
|
+
return null
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function levelIndexOf(node: AnyNode): number {
|
|
958
|
+
return (node as { level?: number }).level ?? 0
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function levelLabelOf(node: AnyNode): string {
|
|
962
|
+
const name = node.name?.trim()
|
|
963
|
+
if (name) return name
|
|
964
|
+
return `Level ${levelIndexOf(node)}`
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function nextFrames(count: number): Promise<void> {
|
|
968
|
+
return new Promise((resolve) => {
|
|
969
|
+
const tick = (remaining: number) => {
|
|
970
|
+
if (remaining <= 0) {
|
|
971
|
+
resolve()
|
|
972
|
+
return
|
|
973
|
+
}
|
|
974
|
+
requestAnimationFrame(() => tick(remaining - 1))
|
|
975
|
+
}
|
|
976
|
+
tick(count)
|
|
977
|
+
})
|
|
978
|
+
}
|