@pascal-app/editor 1.0.1 → 1.0.4
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 +16 -9
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +315 -4
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/custom-camera-controls.tsx +221 -128
- package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
- package/src/components/editor/duplicate-node.ts +244 -0
- package/src/components/editor/editor-layout-mobile.tsx +37 -25
- package/src/components/editor/editor-layout-v2.tsx +131 -58
- package/src/components/editor/export-manager.tsx +34 -8
- package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
- package/src/components/editor/first-person/build-collider-world.ts +6 -2
- package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
- package/src/components/editor/first-person/mechanism-targets.ts +36 -0
- package/src/components/editor/first-person-controls.tsx +205 -2
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
- package/src/components/editor/floor-aware-camera-controls.ts +324 -0
- package/src/components/editor/floor-openings-overlay.tsx +243 -0
- package/src/components/editor/floor-region-controls.tsx +846 -0
- package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
- package/src/components/editor/floor-surface.test.ts +165 -0
- package/src/components/editor/floor-surface.ts +182 -0
- package/src/components/editor/floorplan-background-selection.test.ts +11 -2
- package/src/components/editor/floorplan-background-selection.ts +1 -3
- package/src/components/editor/floorplan-panel.tsx +536 -487
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/grid.tsx +31 -7
- package/src/components/editor/group-actions.ts +59 -11
- package/src/components/editor/group-floating-action-menu.tsx +19 -9
- package/src/components/editor/group-move-3d.ts +4 -6
- package/src/components/editor/group-rotate-handle.tsx +81 -35
- package/src/components/editor/group-selection-box-3d.tsx +25 -17
- package/src/components/editor/group-transform-shared.test.ts +126 -1
- package/src/components/editor/group-transform-shared.ts +79 -73
- package/src/components/editor/handles/handle-arrow.tsx +72 -2
- package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
- package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
- package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
- package/src/components/editor/handles/use-handle-drag.ts +272 -131
- package/src/components/editor/index.tsx +316 -176
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +70 -70
- package/src/components/editor/node-arrow-handles.tsx +167 -14
- package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
- package/src/components/editor/open-wall-ends-hint.tsx +89 -0
- package/src/components/editor/quick-measurement-hud.tsx +3 -2
- package/src/components/editor/registry-action-contributions.test.tsx +63 -0
- package/src/components/editor/registry-action-contributions.tsx +132 -0
- package/src/components/editor/room-controls.tsx +847 -0
- package/src/components/editor/room-floor-highlight.tsx +128 -0
- package/src/components/editor/room-handles-2d.tsx +354 -0
- package/src/components/editor/room-handles.tsx +675 -0
- package/src/components/editor/room-highlight.tsx +225 -0
- package/src/components/editor/room-labels.test.ts +357 -0
- package/src/components/editor/room-labels.tsx +990 -0
- package/src/components/editor/room-transform-ghost.tsx +256 -0
- package/src/components/editor/room-webgpu-materials.test.ts +27 -0
- package/src/components/editor/selection-manager.tsx +451 -317
- package/src/components/editor/slab-hole-highlights.tsx +3 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
- package/src/components/editor/snapshot-capture.test.ts +52 -4
- package/src/components/editor/snapshot-capture.ts +30 -5
- package/src/components/editor/thumbnail-generator.test.ts +308 -0
- package/src/components/editor/thumbnail-generator.tsx +1035 -229
- package/src/components/editor/use-floorplan-background-placement.ts +31 -11
- package/src/components/editor/vector-edge-extractor.tsx +121 -0
- package/src/components/editor/views/host-view.tsx +61 -0
- package/src/components/editor/views/plugins-view.tsx +670 -0
- package/src/components/editor/views/scene-region.tsx +59 -0
- package/src/components/editor/views/use-editor-views.ts +57 -0
- package/src/components/editor/views/view-bar.tsx +220 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +114 -163
- package/src/components/editor/wall-opening-highlights.tsx +2 -3
- package/src/components/editor/wall-push-arrow.tsx +92 -0
- package/src/components/editor/wall-region-handles.tsx +509 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
- package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +31 -66
- package/src/components/tools/__fixtures__/hosting.ts +10 -0
- package/src/components/tools/item/item-surface-placement.test.ts +3 -9
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +63 -47
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +207 -97
- package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
- package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
- package/src/components/tools/registry/item-surface-move.test.ts +10 -2
- package/src/components/tools/registry/item-surface-move.ts +46 -24
- package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
- package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
- package/src/components/tools/select/box-select-tool.tsx +2 -12
- package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
- package/src/components/tools/select/select-candidates.test.ts +37 -4
- package/src/components/tools/select/select-candidates.ts +15 -11
- package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
- package/src/components/tools/shared/affordance-dispatch.ts +13 -0
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
- package/src/components/tools/shared/pointer-support-cap.ts +17 -5
- package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
- package/src/components/tools/shared/surface-rejection.tsx +14 -0
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/stair/stair-defaults.ts +0 -3
- package/src/components/tools/stair/stair-tool.tsx +90 -42
- package/src/components/tools/tool-manager.tsx +82 -23
- package/src/components/tools/wall/wall-drafting.test.ts +19 -4
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +3 -0
- package/src/components/ui/action-menu/camera-actions.tsx +2 -1
- package/src/components/ui/action-menu/control-modes.tsx +2 -35
- package/src/components/ui/action-menu/measurement-control.tsx +3 -10
- package/src/components/ui/build-panel/build-panel.tsx +235 -0
- package/src/components/ui/command-palette/editor-commands.tsx +4 -30
- package/src/components/ui/command-palette/index.tsx +5 -24
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +101 -91
- package/src/components/ui/controls/material-picker.tsx +8 -67
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/controls/slider-control.tsx +76 -31
- package/src/components/ui/floating-level-selector.tsx +9 -14
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
- package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
- package/src/components/ui/helpers/helper-manager.tsx +348 -19
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.tsx +16 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
- package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
- package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
- package/src/components/ui/panels/multi-field-value.test.ts +26 -3
- package/src/components/ui/panels/multi-field-value.ts +6 -3
- package/src/components/ui/panels/multi-height-mode.tsx +27 -21
- package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
- package/src/components/ui/panels/node-display.ts +8 -3
- package/src/components/ui/panels/panel-manager.tsx +103 -68
- package/src/components/ui/panels/panel-wrapper.tsx +78 -36
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +7 -19
- package/src/components/ui/panels/reference-panel.tsx +2 -3
- package/src/components/ui/panels/room-construction-rows.tsx +774 -0
- package/src/components/ui/panels/room-name-options.test.ts +55 -0
- package/src/components/ui/panels/room-name-options.ts +47 -0
- package/src/components/ui/panels/room-openings-field.tsx +110 -0
- package/src/components/ui/panels/room-panel.tsx +725 -0
- package/src/components/ui/panels/shape-choice.tsx +58 -0
- package/src/components/ui/panels/wall-region-list.tsx +109 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/shortcut-token.tsx +4 -0
- package/src/components/ui/primitives/sidebar.tsx +2 -0
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +145 -0
- package/src/components/ui/sidebar/icon-rail.tsx +5 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
- package/src/components/ui/sidebar/tab-bar.tsx +10 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
- package/src/components/viewer/floorplan-compass-button.tsx +9 -1
- package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
- package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
- package/src/components/viewer/floorplan-preview.tsx +6 -18
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +175 -0
- package/src/components/viewer/viewer-scene-header.tsx +181 -145
- package/src/components/viewer/viewer-selection-manager.tsx +124 -0
- package/src/components/viewer/viewer-stage.tsx +2 -2
- package/src/components/viewer-zone-system.tsx +9 -9
- package/src/hooks/use-auto-save.integration.test.tsx +311 -0
- package/src/hooks/use-auto-save.ts +55 -14
- package/src/hooks/use-ceiling-events.test.ts +1 -0
- package/src/hooks/use-ceiling-events.ts +2 -0
- package/src/hooks/use-grid-events.test.ts +2 -0
- package/src/hooks/use-grid-events.ts +16 -11
- package/src/hooks/use-keyboard.test.ts +182 -0
- package/src/hooks/use-keyboard.ts +122 -91
- package/src/hooks/use-open-wall-ends.test.ts +99 -0
- package/src/hooks/use-open-wall-ends.ts +238 -0
- package/src/hooks/use-placement-notice.test.ts +48 -0
- package/src/hooks/use-placement-notice.ts +42 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +306 -39
- package/src/lib/catalog-panel-model.ts +39 -0
- package/src/lib/catalog-selection.test.ts +27 -0
- package/src/lib/catalog-selection.ts +8 -0
- package/src/lib/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/contextual-help.test.ts +27 -1
- package/src/lib/contextual-help.ts +16 -9
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +48 -0
- package/src/lib/editor-views.ts +144 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/floor-edit-session.ts +153 -0
- package/src/lib/floor-footprints.test.ts +563 -0
- package/src/lib/floor-footprints.ts +395 -0
- package/src/lib/floor-foundation-support.test.ts +283 -0
- package/src/lib/floor-opening-draft.ts +218 -0
- package/src/lib/floor-openings.test.ts +216 -0
- package/src/lib/floor-region-geometry.ts +191 -0
- package/src/lib/floor-region-session.test.ts +485 -0
- package/src/lib/floor-region-session.ts +589 -0
- package/src/lib/floor-region-snap.ts +101 -0
- package/src/lib/floor-step-hover.test.ts +32 -0
- package/src/lib/floorplan/annotation-visibility.ts +17 -0
- package/src/lib/floorplan/dimension-drive.test.ts +360 -0
- package/src/lib/floorplan/dimension-drive.ts +479 -0
- package/src/lib/floorplan/floorplan-export.test.ts +15 -9
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +48 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
- package/src/lib/floorplan/floorplan-mode.ts +2 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
- package/src/lib/floorplan/floorplan-readonly.ts +6 -0
- package/src/lib/floorplan/geometry.ts +13 -6
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/marks.test.ts +213 -0
- package/src/lib/floorplan/marks.ts +238 -0
- package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
- package/src/lib/floorplan/open-wall-ends.ts +162 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
- package/src/lib/floorplan/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +100 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
- package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
- package/src/lib/floorplan/site-plan/contributors.ts +97 -0
- package/src/lib/floorplan/site-plan/coverage.ts +232 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
- package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
- package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
- package/src/lib/floorplan/site-plan/geometry.ts +557 -0
- package/src/lib/floorplan/site-plan/index.ts +71 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
- package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
- package/src/lib/floorplan/site-plan/street-name.ts +302 -0
- package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/fresh-planar-placement.test.ts +36 -0
- package/src/lib/fresh-planar-placement.ts +191 -36
- package/src/lib/geometry-script/author.ts +152 -0
- package/src/lib/geometry-script/client.ts +152 -0
- package/src/lib/geometry-script/protocol.ts +12 -0
- package/src/lib/gesture-lifecycle.test.ts +595 -0
- package/src/lib/gesture-lifecycle.ts +290 -0
- package/src/lib/glb-export-zone-holes.test.ts +41 -0
- package/src/lib/glb-export.test.ts +544 -16
- package/src/lib/glb-export.ts +256 -125
- package/src/lib/grid-frame.test.ts +19 -0
- package/src/lib/grid-frame.ts +18 -0
- package/src/lib/history.test.ts +6 -0
- package/src/lib/history.ts +7 -0
- package/src/lib/hud-title.ts +109 -0
- package/src/lib/hydration-gated-storage.ts +49 -0
- package/src/lib/ifc-export.test.ts +174 -0
- package/src/lib/ifc-export.ts +201 -0
- package/src/lib/ifc-parts.ts +15 -0
- package/src/lib/inspector-expanded.test.ts +44 -0
- package/src/lib/inspector-expanded.ts +36 -0
- package/src/lib/interaction/scope.ts +27 -1
- package/src/lib/keyboard-pan.test.ts +184 -0
- package/src/lib/keyboard-pan.ts +238 -0
- package/src/lib/live-gesture-round-trip.test.ts +360 -0
- package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
- package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
- package/src/lib/lot/clean-ring.test.ts +186 -0
- package/src/lib/lot/clean-ring.ts +217 -0
- package/src/lib/lot/dossier.test.ts +202 -0
- package/src/lib/lot/dossier.ts +498 -0
- package/src/lib/lot/drop-in.test.ts +326 -0
- package/src/lib/lot/drop-in.ts +407 -0
- package/src/lib/lot/index.ts +43 -0
- package/src/lib/lot/lot-patch.test.ts +340 -0
- package/src/lib/lot/lot-patch.ts +394 -0
- package/src/lib/lot/parcel-provider.ts +43 -0
- package/src/lib/lot/terrain.test.ts +161 -0
- package/src/lib/lot/terrain.ts +266 -0
- package/src/lib/material-catalog-model.ts +74 -0
- package/src/lib/material-paint-panel-model.ts +80 -0
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/mezzanine-actions.test.ts +342 -0
- package/src/lib/mezzanine-draft.test.ts +501 -0
- package/src/lib/mezzanine-draft.ts +278 -0
- package/src/lib/mezzanine-messages.ts +26 -0
- package/src/lib/mezzanine-selection.test.ts +207 -0
- package/src/lib/mezzanine-stairs.ts +46 -0
- package/src/lib/model-export.ts +3 -1
- package/src/lib/node-action-movement.ts +17 -6
- package/src/lib/node-display-label.ts +37 -0
- package/src/lib/orbit-floor-clearance.test.ts +165 -0
- package/src/lib/orbit-floor-clearance.ts +106 -0
- package/src/lib/overlay-transform.test.ts +28 -0
- package/src/lib/overlay-transform.ts +12 -0
- package/src/lib/paint-commit-route.test.ts +37 -0
- package/src/lib/paint-commit-route.ts +17 -0
- package/src/lib/paint-eyedropper.test.tsx +165 -0
- package/src/lib/paint-eyedropper.ts +99 -0
- package/src/lib/paint-pass-through.test.ts +111 -0
- package/src/lib/paint-pass-through.ts +76 -0
- package/src/lib/paint-region-hover.ts +25 -0
- package/src/lib/paint-region-mode.ts +154 -0
- package/src/lib/paint-regions.test.ts +166 -0
- package/src/lib/paint-regions.ts +192 -0
- package/src/lib/paint-scope.test.ts +317 -35
- package/src/lib/paint-scope.ts +266 -25
- package/src/lib/panel-rows.ts +96 -0
- package/src/lib/panel-tool-options.test.ts +34 -0
- package/src/lib/panel-tool-options.ts +46 -0
- package/src/lib/parametric-node-update.ts +37 -0
- package/src/lib/plate-paint-affected.test.ts +300 -0
- package/src/lib/plate-paint-affected.ts +177 -0
- package/src/lib/plugin-chat.ts +50 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/print-shell-compiler.ts +1 -0
- package/src/lib/registered-draft-snap.ts +12 -0
- package/src/lib/roof-duplication.ts +10 -2
- package/src/lib/room-assembly-overlay.test.ts +275 -0
- package/src/lib/room-assembly-overlay.ts +266 -0
- package/src/lib/room-built-on.test.ts +379 -0
- package/src/lib/room-built-on.ts +138 -0
- package/src/lib/room-construction-commands.ts +712 -0
- package/src/lib/room-construction.test.ts +746 -0
- package/src/lib/room-construction.ts +385 -0
- package/src/lib/room-controls-phase6.test.ts +630 -0
- package/src/lib/room-divide-session.ts +523 -0
- package/src/lib/room-handle-drag.ts +1068 -0
- package/src/lib/room-name-catalog.test.ts +90 -0
- package/src/lib/room-name-catalog.ts +168 -0
- package/src/lib/room-push-dimensions.test.ts +311 -0
- package/src/lib/room-push-dimensions.ts +251 -0
- package/src/lib/room-selection-commands.ts +62 -0
- package/src/lib/room-selection.test.ts +973 -0
- package/src/lib/room-selection.ts +417 -0
- package/src/lib/room-structure-commands.ts +94 -0
- package/src/lib/room-structure.test.ts +921 -0
- package/src/lib/room-transform-session.ts +570 -0
- package/src/lib/room-zone-routing.test.ts +294 -0
- package/src/lib/room-zone-routing.ts +197 -0
- package/src/lib/scene-clipboard.test.ts +364 -0
- package/src/lib/scene-clipboard.ts +229 -48
- package/src/lib/scene-material-refs.test.ts +79 -0
- package/src/lib/scene-material-refs.ts +79 -0
- package/src/lib/scene.test.ts +87 -0
- package/src/lib/scene.ts +39 -45
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +53 -74
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sidebar-panel.ts +62 -0
- package/src/lib/site-boundary.test.ts +4 -6
- package/src/lib/site-boundary.ts +1 -1
- package/src/lib/snapping-mode.test.ts +17 -2
- package/src/lib/snapping-mode.ts +19 -7
- package/src/lib/spatial-drag-plane.test.ts +53 -0
- package/src/lib/spatial-drag-plane.ts +19 -0
- package/src/lib/spatial-pointer-input.test.ts +72 -0
- package/src/lib/spatial-pointer-input.ts +62 -0
- package/src/lib/spatial-snap.test.ts +47 -0
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -0
- package/src/lib/terrain-panel-model.ts +117 -0
- package/src/lib/tool-cursor-icon.test.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +11 -0
- package/src/lib/typing-target.ts +9 -0
- package/src/lib/units.ts +77 -39
- package/src/lib/vector-edges.test.ts +58 -0
- package/src/lib/vector-edges.ts +400 -0
- package/src/lib/viewer-selection.test.ts +577 -0
- package/src/lib/viewer-selection.ts +569 -0
- package/src/lib/wall-draw-variant.test.ts +71 -0
- package/src/lib/wall-draw-variant.ts +91 -0
- package/src/lib/wall-paint-region-session.ts +246 -0
- package/src/lib/wall-paint-region.test.ts +373 -0
- package/src/lib/wall-polygon-draft.test.ts +262 -0
- package/src/lib/wall-polygon-draft.ts +286 -0
- package/src/lib/wall-push.test.ts +453 -0
- package/src/lib/wall-region-face.ts +352 -0
- package/src/lib/wall-region-handles.test.ts +304 -0
- package/src/lib/wall-region-handles.ts +368 -0
- package/src/lib/wall-region-list.test.ts +119 -0
- package/src/lib/wall-region-list.ts +136 -0
- package/src/lib/wall-region-snap.ts +110 -0
- package/src/lib/window-interaction.ts +14 -0
- package/src/lib/world-grid-snap.test.ts +48 -0
- package/src/lib/world-grid-snap.ts +9 -5
- package/src/lib/zone-content.ts +4 -1
- package/src/lib/zone-name.test.ts +17 -0
- package/src/lib/zone-name.ts +17 -0
- package/src/store/opens-in-select-mode.test.ts +223 -0
- package/src/store/paint-mode.test.ts +188 -0
- package/src/store/rotation-snapping.test.ts +53 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-delete-confirmation.ts +28 -1
- package/src/store/use-drawing-view.test.ts +24 -3
- package/src/store/use-drawing-view.ts +29 -1
- package/src/store/use-editor.tsx +442 -323
- package/src/store/use-floorplan-draft-preview.ts +23 -1
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-interaction-scope.ts +155 -6
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +13 -5
- package/src/store/use-wall-move-ghosts.ts +4 -2
- package/src/store/use-wall-region-handles-selection.test.ts +222 -0
- package/src/store/use-wall-region-handles-selection.ts +139 -0
- package/src/store/view-layout.test.ts +175 -0
- package/src/store/view-layout.ts +196 -0
- package/src/test-utils/immediate-animation-frames.ts +16 -0
- package/src/test-utils/live-gesture-round-trip.ts +224 -0
- package/src/test-utils/selection-harness.ts +82 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
- package/src/components/tools/fence/fence-drafting.test.ts +0 -100
- package/src/components/tools/fence/fence-drafting.ts +0 -266
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
- package/src/components/viewer/viewer-units-panel.tsx +0 -116
- package/src/lib/floorplan/stairs.ts +0 -489
- package/src/lib/level-duplication.test.ts +0 -144
- package/src/lib/level-duplication.ts +0 -195
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pascal-app/editor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Pascal building editor component",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
"test": "bun test src"
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
|
17
|
-
"@pascal-app/core": "^1.0.
|
|
18
|
-
"@pascal-app/viewer": "^1.0.
|
|
17
|
+
"@pascal-app/core": "^1.0.4",
|
|
18
|
+
"@pascal-app/viewer": "^1.0.4",
|
|
19
19
|
"@react-three/drei": "^10",
|
|
20
20
|
"@react-three/fiber": "^9",
|
|
21
21
|
"next": ">=15",
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
"@dnd-kit/utilities": "^3.2.2",
|
|
30
30
|
"@iconify/react": "^6.0.2",
|
|
31
31
|
"@number-flow/react": "^0.6.0",
|
|
32
|
+
"@pascal-app/geometry-script": "^1.0.4",
|
|
33
|
+
"@pascal-app/ifc-converter": "^1.0.4",
|
|
32
34
|
"@pascal-app/lingo": "^0.2.0",
|
|
33
35
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
|
34
36
|
"@radix-ui/react-context-menu": "^2.2.16",
|
|
@@ -62,18 +64,23 @@
|
|
|
62
64
|
"zustand": "^5.0.11"
|
|
63
65
|
},
|
|
64
66
|
"devDependencies": {
|
|
65
|
-
"@pascal-app/core": "^1.0.
|
|
66
|
-
"@pascal-app/viewer": "^1.0.
|
|
67
|
+
"@pascal-app/core": "^1.0.4",
|
|
68
|
+
"@pascal-app/viewer": "^1.0.4",
|
|
67
69
|
"@pascal/typescript-config": "*",
|
|
68
70
|
"@react-three/test-renderer": "^9.1.0",
|
|
69
71
|
"@types/blob-stream": "^0.1.33",
|
|
70
|
-
"@types/bun": "^1.
|
|
72
|
+
"@types/bun": "^1.4.2",
|
|
71
73
|
"@types/howler": "^2.2.12",
|
|
72
74
|
"@types/pdfkit": "^0.17.6",
|
|
73
|
-
"@types/react": "19.
|
|
74
|
-
"@types/react-dom": "19.
|
|
75
|
+
"@types/react": "19.3.0",
|
|
76
|
+
"@types/react-dom": "19.3.0",
|
|
75
77
|
"@types/three": "^0.184.0",
|
|
76
|
-
"fast-xml-parser": "^5.
|
|
78
|
+
"fast-xml-parser": "^5.11.2",
|
|
77
79
|
"typescript": "6.0.3"
|
|
80
|
+
},
|
|
81
|
+
"repository": {
|
|
82
|
+
"type": "git",
|
|
83
|
+
"url": "git+https://github.com/pascalorg/editor.git",
|
|
84
|
+
"directory": "packages/editor"
|
|
78
85
|
}
|
|
79
86
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { MouseEventHandler, ReactNode } from 'react'
|
|
4
|
+
import { cn } from '../../lib/utils'
|
|
5
|
+
import { shortcutDisplayValue } from '../ui/primitives/shortcut-token'
|
|
6
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/primitives/tooltip'
|
|
7
|
+
|
|
8
|
+
const BUTTON =
|
|
9
|
+
'rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground'
|
|
10
|
+
const DESTRUCTIVE = 'hover:bg-destructive/10 hover:text-destructive'
|
|
11
|
+
const DISABLED = 'cursor-not-allowed opacity-40 hover:bg-transparent hover:text-muted-foreground'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* One button of an action pill, with the editor's tooltip: its name, and the
|
|
15
|
+
* keys that do the same where there are any. A disabled button still shows
|
|
16
|
+
* its tooltip (it stays hoverable) so it can say why it is off.
|
|
17
|
+
*/
|
|
18
|
+
export function ActionMenuButton({
|
|
19
|
+
label,
|
|
20
|
+
keys,
|
|
21
|
+
onClick,
|
|
22
|
+
destructive = false,
|
|
23
|
+
disabled = false,
|
|
24
|
+
disabledReason,
|
|
25
|
+
pressed,
|
|
26
|
+
children,
|
|
27
|
+
}: {
|
|
28
|
+
label: string
|
|
29
|
+
/** Keys pressed together, as `ShortcutToken` names them (`Cmd/Ctrl`, `G`). */
|
|
30
|
+
keys?: string[]
|
|
31
|
+
onClick?: MouseEventHandler<HTMLButtonElement>
|
|
32
|
+
destructive?: boolean
|
|
33
|
+
disabled?: boolean
|
|
34
|
+
/** The tooltip while disabled; the label otherwise. */
|
|
35
|
+
disabledReason?: string
|
|
36
|
+
/** A toggle's state: sets `aria-pressed` and the active look. */
|
|
37
|
+
pressed?: boolean
|
|
38
|
+
children: ReactNode
|
|
39
|
+
}) {
|
|
40
|
+
return (
|
|
41
|
+
<Tooltip>
|
|
42
|
+
<TooltipTrigger asChild>
|
|
43
|
+
<button
|
|
44
|
+
aria-disabled={disabled || undefined}
|
|
45
|
+
aria-label={label}
|
|
46
|
+
aria-pressed={pressed}
|
|
47
|
+
className={cn(
|
|
48
|
+
BUTTON,
|
|
49
|
+
destructive && DESTRUCTIVE,
|
|
50
|
+
disabled && DISABLED,
|
|
51
|
+
pressed && 'bg-accent text-foreground',
|
|
52
|
+
)}
|
|
53
|
+
data-action-disabled={disabled || undefined}
|
|
54
|
+
onClick={(event) => {
|
|
55
|
+
if (disabled) {
|
|
56
|
+
event.stopPropagation()
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
onClick?.(event)
|
|
60
|
+
}}
|
|
61
|
+
type="button"
|
|
62
|
+
>
|
|
63
|
+
{children}
|
|
64
|
+
</button>
|
|
65
|
+
</TooltipTrigger>
|
|
66
|
+
<TooltipContent className="flex items-center gap-2" side="top" sideOffset={6}>
|
|
67
|
+
<span>{disabled && disabledReason ? disabledReason : label}</span>
|
|
68
|
+
{!disabled && keys?.length ? (
|
|
69
|
+
<span className="flex items-center gap-0.5">
|
|
70
|
+
{keys.map((key) => (
|
|
71
|
+
<kbd
|
|
72
|
+
className="rounded border border-background/30 px-1 font-mono text-[10px] leading-4 opacity-80"
|
|
73
|
+
key={key}
|
|
74
|
+
>
|
|
75
|
+
{shortcutDisplayValue(key)}
|
|
76
|
+
</kbd>
|
|
77
|
+
))}
|
|
78
|
+
</span>
|
|
79
|
+
) : null}
|
|
80
|
+
</TooltipContent>
|
|
81
|
+
</Tooltip>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
@@ -1,20 +1,206 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { type AnyNodeId, sceneRegistry, useInteractive, useScene } from '@pascal-app/core'
|
|
4
|
+
import { evaluateRecipe } from '@pascal-app/core/procedural-items'
|
|
3
5
|
import {
|
|
4
6
|
computeHeroFraming,
|
|
5
7
|
createSnapshotPipeline,
|
|
6
8
|
GRID_LAYER,
|
|
9
|
+
type HeroFraming,
|
|
7
10
|
heroCameraPose,
|
|
11
|
+
proceduralSlotMeshes,
|
|
12
|
+
setKeyLightDirectionOverride,
|
|
13
|
+
setProceduralEmission,
|
|
14
|
+
setSlotDefaultOverrides,
|
|
8
15
|
temporarilyHideNodeTypes,
|
|
9
16
|
useSceneAtmosphere,
|
|
10
17
|
useViewer,
|
|
11
18
|
} from '@pascal-app/viewer'
|
|
12
19
|
import { useThree } from '@react-three/fiber'
|
|
13
20
|
import { useEffect, useRef } from 'react'
|
|
14
|
-
import {
|
|
15
|
-
|
|
21
|
+
import {
|
|
22
|
+
Box3,
|
|
23
|
+
CanvasTexture,
|
|
24
|
+
type Material,
|
|
25
|
+
Mesh,
|
|
26
|
+
type Object3D,
|
|
27
|
+
PerspectiveCamera,
|
|
28
|
+
PlaneGeometry,
|
|
29
|
+
Vector3,
|
|
30
|
+
} from 'three'
|
|
31
|
+
import { MeshBasicNodeMaterial, ShadowNodeMaterial, type WebGPURenderer } from 'three/webgpu'
|
|
16
32
|
import { EDITOR_LAYER } from '../../lib/constants'
|
|
17
33
|
|
|
34
|
+
// The published-thumbnail look. One look for every project, independent of
|
|
35
|
+
// the scene's own display preferences.
|
|
36
|
+
const THUMBNAIL_FOV_DEG = 40
|
|
37
|
+
const THUMBNAIL_PADDING = 1.12
|
|
38
|
+
/** Roofless scenes look down into the rooms; roofed ones show the roofline. */
|
|
39
|
+
const THUMBNAIL_ELEVATION_DEG = { roofed: 24, roofless: 35 }
|
|
40
|
+
/** Key light relative to the hero camera: 30° off its azimuth, 45° up — both
|
|
41
|
+
* facades lit, shadows raking across the ground. */
|
|
42
|
+
const THUMBNAIL_KEY_AZIMUTH_OFFSET_DEG = 30
|
|
43
|
+
const THUMBNAIL_KEY_ELEVATION_DEG = 45
|
|
44
|
+
const THUMBNAIL_BACKDROP = { inner: '#FAFAFA', outer: '#E4E4E4' }
|
|
45
|
+
const THUMBNAIL_SHADOW_CATCHER_OPACITY = 0.7
|
|
46
|
+
/** Soft shade hugging the ground-floor walls so the building never floats. */
|
|
47
|
+
const THUMBNAIL_WALL_CONTACT = { opacity: 0.5, spreadM: 0.9 }
|
|
48
|
+
/** Unpainted walls render as smooth plaster rather than the screw-dotted
|
|
49
|
+
* prepared-drywall default, which reads as unfinished at card size. */
|
|
50
|
+
const THUMBNAIL_SLOT_DEFAULT_OVERRIDES = {
|
|
51
|
+
'library:concrete-drywall': 'library:concrete-plaster',
|
|
52
|
+
} as const
|
|
53
|
+
// Past the capture camera's far plane (1000): a catcher edge inside the frame
|
|
54
|
+
// leaves an SSGI AO line along its depth discontinuity.
|
|
55
|
+
const SHADOW_CATCHER_EXTENT = 4000
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Forces the published-thumbnail look on the bake page. Call before the scene
|
|
59
|
+
* builds (the finish swap applies as materials are created); the returned
|
|
60
|
+
* cleanup restores the declared finishes.
|
|
61
|
+
*/
|
|
62
|
+
export function prepareBakeThumbnailLook(): () => void {
|
|
63
|
+
const viewer = useViewer.getState()
|
|
64
|
+
viewer.setSceneTheme('studio')
|
|
65
|
+
viewer.setEdges('soft')
|
|
66
|
+
viewer.setShadows(true)
|
|
67
|
+
setSlotDefaultOverrides(THUMBNAIL_SLOT_DEFAULT_OVERRIDES)
|
|
68
|
+
return () => setSlotDefaultOverrides(null)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Scene-ready fires before every system has finished its geometry: roofs
|
|
72
|
+
// rebuild their merged mesh a few frames later (measured: Starter House ridge
|
|
73
|
+
// 5.75 m at scene-ready, 6.24 m at capture), so a pose fit at scene-ready
|
|
74
|
+
// clips the roof. Re-measure until the framing bounds hold still.
|
|
75
|
+
async function settledHeroFraming() {
|
|
76
|
+
const boundsKey = (framing: HeroFraming | null) => {
|
|
77
|
+
if (!framing) return ''
|
|
78
|
+
const union = new Box3()
|
|
79
|
+
for (const box of framing.boxes) union.union(box)
|
|
80
|
+
return [...union.min.toArray(), ...union.max.toArray()].map((v) => v.toFixed(3)).join(',')
|
|
81
|
+
}
|
|
82
|
+
let framing = computeHeroFraming({ sitePlates: false })
|
|
83
|
+
let previous = boundsKey(framing)
|
|
84
|
+
let stableChecks = 0
|
|
85
|
+
for (let i = 0; i < 40 && stableChecks < 3; i++) {
|
|
86
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 100))
|
|
87
|
+
framing = computeHeroFraming({ sitePlates: false })
|
|
88
|
+
const next = boundsKey(framing)
|
|
89
|
+
stableChecks = next === previous ? stableChecks + 1 : 0
|
|
90
|
+
previous = next
|
|
91
|
+
}
|
|
92
|
+
return framing
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The site's own surfaces (ground fill, horizon disc, property ribbons) —
|
|
96
|
+
* not its registered children (buildings, items), which are groups. */
|
|
97
|
+
function siteSurfaces(): Object3D[] {
|
|
98
|
+
const surfaces: Object3D[] = []
|
|
99
|
+
for (const siteId of sceneRegistry.byType.site ?? []) {
|
|
100
|
+
for (const child of sceneRegistry.nodes.get(siteId)?.children ?? []) {
|
|
101
|
+
if (child.visible && !(child.type === 'Group' && child.children.length > 0)) {
|
|
102
|
+
surfaces.push(child)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return surfaces
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A soft contact shade on the ground along every ground-floor wall: the wall
|
|
111
|
+
* centrelines stroked onto a canvas in the building's facade axes, then
|
|
112
|
+
* blurred. Per wall rather than per footprint, so a walled lot shades its
|
|
113
|
+
* boundary walls and leaves the open courtyard clean.
|
|
114
|
+
*/
|
|
115
|
+
function wallContactShade(yaw: number, groundY: number): Mesh | null {
|
|
116
|
+
const u = new Vector3(Math.cos(yaw), 0, Math.sin(yaw))
|
|
117
|
+
const v = new Vector3(-Math.sin(yaw), 0, Math.cos(yaw))
|
|
118
|
+
const segments: Array<{ a: [number, number]; b: [number, number]; thickness: number }> = []
|
|
119
|
+
const point = new Vector3()
|
|
120
|
+
// A wall mesh sits at its start, rotated along the run: `start`/`end` are
|
|
121
|
+
// in its parent's (the level's) frame.
|
|
122
|
+
const toUv = (x: number, z: number, object: Object3D): [number, number] => {
|
|
123
|
+
point.set(x, 0, z)
|
|
124
|
+
if (object.parent) point.applyMatrix4(object.parent.matrixWorld)
|
|
125
|
+
return [point.dot(u), point.dot(v)]
|
|
126
|
+
}
|
|
127
|
+
for (const wallId of sceneRegistry.byType.wall ?? []) {
|
|
128
|
+
const object = sceneRegistry.nodes.get(wallId)
|
|
129
|
+
const node = useScene.getState().nodes[wallId as AnyNodeId]
|
|
130
|
+
if (!(object && node?.type === 'wall' && node.visible !== false)) continue
|
|
131
|
+
const bounds = new Box3().setFromObject(object)
|
|
132
|
+
if (bounds.isEmpty() || bounds.min.y > groundY + 0.5) continue
|
|
133
|
+
segments.push({
|
|
134
|
+
a: toUv(node.start[0], node.start[1], object),
|
|
135
|
+
b: toUv(node.end[0], node.end[1], object),
|
|
136
|
+
thickness: node.thickness ?? 0.2,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
if (segments.length === 0) return null
|
|
140
|
+
|
|
141
|
+
const { spreadM, opacity } = THUMBNAIL_WALL_CONTACT
|
|
142
|
+
let uMin = Number.POSITIVE_INFINITY
|
|
143
|
+
let uMax = Number.NEGATIVE_INFINITY
|
|
144
|
+
let vMin = Number.POSITIVE_INFINITY
|
|
145
|
+
let vMax = Number.NEGATIVE_INFINITY
|
|
146
|
+
for (const { a, b } of segments) {
|
|
147
|
+
uMin = Math.min(uMin, a[0], b[0])
|
|
148
|
+
uMax = Math.max(uMax, a[0], b[0])
|
|
149
|
+
vMin = Math.min(vMin, a[1], b[1])
|
|
150
|
+
vMax = Math.max(vMax, a[1], b[1])
|
|
151
|
+
}
|
|
152
|
+
const margin = spreadM * 2
|
|
153
|
+
const width = uMax - uMin + margin * 2
|
|
154
|
+
const depth = vMax - vMin + margin * 2
|
|
155
|
+
const scale = 1024 / Math.max(width, depth)
|
|
156
|
+
const canvas = document.createElement('canvas')
|
|
157
|
+
canvas.width = Math.max(8, Math.round(width * scale))
|
|
158
|
+
canvas.height = Math.max(8, Math.round(depth * scale))
|
|
159
|
+
const ctx = canvas.getContext('2d')
|
|
160
|
+
if (!ctx) return null
|
|
161
|
+
// alphaMap samples the green channel: white strokes on opaque black.
|
|
162
|
+
ctx.fillStyle = '#000'
|
|
163
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
|
164
|
+
ctx.filter = `blur(${Math.max(2, spreadM * scale * 0.35)}px)`
|
|
165
|
+
ctx.strokeStyle = '#fff'
|
|
166
|
+
ctx.lineCap = 'square'
|
|
167
|
+
for (const { a, b, thickness } of segments) {
|
|
168
|
+
ctx.lineWidth = (thickness + spreadM * 0.6) * scale
|
|
169
|
+
ctx.beginPath()
|
|
170
|
+
ctx.moveTo((a[0] - uMin + margin) * scale, (a[1] - vMin + margin) * scale)
|
|
171
|
+
ctx.lineTo((b[0] - uMin + margin) * scale, (b[1] - vMin + margin) * scale)
|
|
172
|
+
ctx.stroke()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const shade = new Mesh(
|
|
176
|
+
new PlaneGeometry(width, depth),
|
|
177
|
+
new MeshBasicNodeMaterial({
|
|
178
|
+
color: '#000000',
|
|
179
|
+
alphaMap: new CanvasTexture(canvas),
|
|
180
|
+
transparent: true,
|
|
181
|
+
depthWrite: false,
|
|
182
|
+
opacity,
|
|
183
|
+
}),
|
|
184
|
+
)
|
|
185
|
+
// Plane x → u; plane y → −v, so the canvas's downward rows run along +v.
|
|
186
|
+
shade.rotation.set(-Math.PI / 2, 0, -yaw)
|
|
187
|
+
shade.position
|
|
188
|
+
.copy(u)
|
|
189
|
+
.multiplyScalar((uMin + uMax) / 2)
|
|
190
|
+
.addScaledVector(v, (vMin + vMax) / 2)
|
|
191
|
+
.setY(groundY - 0.005)
|
|
192
|
+
shade.renderOrder = -1
|
|
193
|
+
return shade
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function disposeMesh(mesh: Mesh) {
|
|
197
|
+
mesh.removeFromParent()
|
|
198
|
+
mesh.geometry.dispose()
|
|
199
|
+
const material = mesh.material as MeshBasicNodeMaterial
|
|
200
|
+
material.alphaMap?.dispose()
|
|
201
|
+
material.dispose()
|
|
202
|
+
}
|
|
203
|
+
|
|
18
204
|
export function BakeThumbnail({
|
|
19
205
|
active,
|
|
20
206
|
onComplete,
|
|
@@ -35,10 +221,37 @@ export function BakeThumbnail({
|
|
|
35
221
|
|
|
36
222
|
const run = async () => {
|
|
37
223
|
const restoreNodeVisibility = temporarilyHideNodeTypes(['scan', 'guide', 'spawn'])
|
|
224
|
+
const priorLights = new Map<string, boolean>()
|
|
225
|
+
const restGroups = new Map<string, [number, number, number]>()
|
|
226
|
+
for (const node of Object.values(useScene.getState().nodes)) {
|
|
227
|
+
if (node.type !== 'procedural-item') continue
|
|
228
|
+
const evaluation = evaluateRecipe(node.recipe, node.parameters)
|
|
229
|
+
if (evaluation.lights.length) {
|
|
230
|
+
priorLights.set(
|
|
231
|
+
node.id,
|
|
232
|
+
useInteractive.getState().procedural[node.id]?.lightsOn ??
|
|
233
|
+
useInteractive.getState().lampDefault,
|
|
234
|
+
)
|
|
235
|
+
useInteractive.getState().setProceduralLights(node.id, true)
|
|
236
|
+
}
|
|
237
|
+
for (const motion of evaluation.motions)
|
|
238
|
+
restGroups.set(`${node.id}:${motion.id}`, motion.pivot)
|
|
239
|
+
}
|
|
240
|
+
const transforms: Array<{
|
|
241
|
+
object: import('three').Object3D
|
|
242
|
+
position: import('three').Vector3
|
|
243
|
+
rotation: import('three').Euler
|
|
244
|
+
}> = []
|
|
245
|
+
const emission = new Map<Material, number>()
|
|
38
246
|
let pipeline: Awaited<ReturnType<typeof createSnapshotPipeline>> = null
|
|
247
|
+
// The studio backdrop replaces the site's ground and horizon: a shadow
|
|
248
|
+
// catcher keeps cast shadows, the wall shade keeps contact. Items the
|
|
249
|
+
// framing leaves out are hidden so what's framed is what's shown.
|
|
250
|
+
const hidden: Object3D[] = []
|
|
251
|
+
const added: Mesh[] = []
|
|
39
252
|
|
|
40
253
|
try {
|
|
41
|
-
const framing =
|
|
254
|
+
const framing = await settledHeroFraming()
|
|
42
255
|
if (!framing) {
|
|
43
256
|
onError('scene has no framable content')
|
|
44
257
|
return
|
|
@@ -46,24 +259,73 @@ export function BakeThumbnail({
|
|
|
46
259
|
|
|
47
260
|
const { width, height } = renderer.domElement
|
|
48
261
|
const aspect = width / height
|
|
49
|
-
const camera = new PerspectiveCamera(
|
|
262
|
+
const camera = new PerspectiveCamera(THUMBNAIL_FOV_DEG, aspect, 0.1, 1000)
|
|
50
263
|
camera.layers.disable(EDITOR_LAYER)
|
|
51
264
|
camera.layers.disable(GRID_LAYER)
|
|
265
|
+
const hasRoof = (sceneRegistry.byType.roof?.size ?? 0) > 0
|
|
266
|
+
const elevationDeg = hasRoof
|
|
267
|
+
? THUMBNAIL_ELEVATION_DEG.roofed
|
|
268
|
+
: THUMBNAIL_ELEVATION_DEG.roofless
|
|
52
269
|
const pose = heroCameraPose({
|
|
53
270
|
boxes: framing.boxes,
|
|
54
271
|
aim: framing.aim,
|
|
55
272
|
azimuthRad: framing.azimuthRad,
|
|
56
273
|
aspect,
|
|
274
|
+
fovDeg: THUMBNAIL_FOV_DEG,
|
|
275
|
+
elevationRad: (elevationDeg * Math.PI) / 180,
|
|
276
|
+
padding: THUMBNAIL_PADDING,
|
|
277
|
+
tightFit: true,
|
|
57
278
|
})
|
|
58
279
|
camera.position.set(pose.position[0], pose.position[1], pose.position[2])
|
|
59
280
|
camera.lookAt(pose.target[0], pose.target[1], pose.target[2])
|
|
60
281
|
camera.updateMatrixWorld()
|
|
61
282
|
|
|
283
|
+
const keyAzimuth = framing.azimuthRad + (THUMBNAIL_KEY_AZIMUTH_OFFSET_DEG * Math.PI) / 180
|
|
284
|
+
const keyElevation = (THUMBNAIL_KEY_ELEVATION_DEG * Math.PI) / 180
|
|
285
|
+
setKeyLightDirectionOverride([
|
|
286
|
+
Math.sin(keyAzimuth) * Math.cos(keyElevation),
|
|
287
|
+
Math.sin(keyElevation),
|
|
288
|
+
Math.cos(keyAzimuth) * Math.cos(keyElevation),
|
|
289
|
+
])
|
|
290
|
+
|
|
291
|
+
for (const object of [
|
|
292
|
+
...siteSurfaces(),
|
|
293
|
+
...framing.detachedItemIds.flatMap((id) => sceneRegistry.nodes.get(id) ?? []),
|
|
294
|
+
]) {
|
|
295
|
+
if (!object.visible) continue
|
|
296
|
+
object.visible = false
|
|
297
|
+
hidden.push(object)
|
|
298
|
+
}
|
|
299
|
+
const union = new Box3()
|
|
300
|
+
for (const box of framing.boxes) union.union(box)
|
|
301
|
+
const groundY = Math.max(0, union.min.y)
|
|
302
|
+
const catcher = new Mesh(
|
|
303
|
+
new PlaneGeometry(SHADOW_CATCHER_EXTENT, SHADOW_CATCHER_EXTENT),
|
|
304
|
+
new ShadowNodeMaterial({ opacity: THUMBNAIL_SHADOW_CATCHER_OPACITY }),
|
|
305
|
+
)
|
|
306
|
+
catcher.rotation.x = -Math.PI / 2
|
|
307
|
+
catcher.position.set(framing.aim[0], groundY - 0.01, framing.aim[2])
|
|
308
|
+
catcher.receiveShadow = true
|
|
309
|
+
added.push(catcher)
|
|
310
|
+
const contact = wallContactShade(Math.PI / 4 - framing.azimuthRad, groundY)
|
|
311
|
+
if (contact) added.push(contact)
|
|
312
|
+
for (const mesh of added) scene.add(mesh)
|
|
313
|
+
|
|
314
|
+
// Backdrop centred on the building's mid-height, where it's lightest.
|
|
315
|
+
const center = new Vector3(
|
|
316
|
+
framing.aim[0],
|
|
317
|
+
(groundY + framing.aim[1]) / 2,
|
|
318
|
+
framing.aim[2],
|
|
319
|
+
).project(camera)
|
|
62
320
|
pipeline = await createSnapshotPipeline({
|
|
63
321
|
renderer: renderer as unknown as WebGPURenderer,
|
|
64
322
|
scene,
|
|
65
323
|
camera,
|
|
66
324
|
atmosphere,
|
|
325
|
+
studioBackdrop: {
|
|
326
|
+
...THUMBNAIL_BACKDROP,
|
|
327
|
+
center: [(center.x + 1) / 2, (1 - center.y) / 2],
|
|
328
|
+
},
|
|
67
329
|
})
|
|
68
330
|
if (!pipeline) {
|
|
69
331
|
onError('thumbnail pipeline failed to build')
|
|
@@ -77,6 +339,41 @@ export function BakeThumbnail({
|
|
|
77
339
|
edges: useViewer.getState().edges,
|
|
78
340
|
camera,
|
|
79
341
|
})
|
|
342
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 250))
|
|
343
|
+
for (const node of Object.values(useScene.getState().nodes)) {
|
|
344
|
+
if (node.type !== 'procedural-item') continue
|
|
345
|
+
const slots = new Set(
|
|
346
|
+
evaluateRecipe(node.recipe, node.parameters).lights.flatMap((light) =>
|
|
347
|
+
light.emissiveSlot ? [light.emissiveSlot] : [],
|
|
348
|
+
),
|
|
349
|
+
)
|
|
350
|
+
const object = sceneRegistry.nodes.get(node.id)
|
|
351
|
+
for (const mesh of object ? proceduralSlotMeshes(object, slots) : []) {
|
|
352
|
+
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
|
|
353
|
+
for (const material of materials) {
|
|
354
|
+
const current = material as Material & { emissiveIntensity?: number }
|
|
355
|
+
if (typeof current.emissiveIntensity !== 'number') continue
|
|
356
|
+
emission.set(material, current.emissiveIntensity)
|
|
357
|
+
setProceduralEmission(material, true)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
scene.traverse((object) => {
|
|
362
|
+
const motion = object.userData.proceduralMotion as
|
|
363
|
+
| { nodeId?: string; groupId?: string }
|
|
364
|
+
| undefined
|
|
365
|
+
if (!motion) return
|
|
366
|
+
const pivot = restGroups.get(`${motion.nodeId}:${motion.groupId}`)
|
|
367
|
+
if (!pivot) return
|
|
368
|
+
transforms.push({
|
|
369
|
+
object,
|
|
370
|
+
position: object.position.clone(),
|
|
371
|
+
rotation: object.rotation.clone(),
|
|
372
|
+
})
|
|
373
|
+
object.position.set(...pivot)
|
|
374
|
+
object.rotation.set(0, 0, 0)
|
|
375
|
+
})
|
|
376
|
+
scene.updateMatrixWorld(true)
|
|
80
377
|
const { blob, outW, outH } = await pipeline.capture({ captureMode: 'standard' })
|
|
81
378
|
onComplete(blob, { w: outW, h: outH })
|
|
82
379
|
} catch (error) {
|
|
@@ -87,6 +384,20 @@ export function BakeThumbnail({
|
|
|
87
384
|
onError(error instanceof Error ? error.message : String(error))
|
|
88
385
|
} finally {
|
|
89
386
|
pipeline?.dispose()
|
|
387
|
+
setKeyLightDirectionOverride(null)
|
|
388
|
+
for (const mesh of added) disposeMesh(mesh)
|
|
389
|
+
for (const object of hidden) object.visible = true
|
|
390
|
+
for (const [material, intensity] of emission)
|
|
391
|
+
(material as Material & { emissiveIntensity: number }).emissiveIntensity = intensity
|
|
392
|
+
for (const { object, position, rotation } of transforms) {
|
|
393
|
+
object.position.copy(position)
|
|
394
|
+
object.rotation.copy(rotation)
|
|
395
|
+
}
|
|
396
|
+
scene.updateMatrixWorld(true)
|
|
397
|
+
for (const [id, on] of priorLights)
|
|
398
|
+
useInteractive
|
|
399
|
+
.getState()
|
|
400
|
+
.setProceduralLights(id as import('@pascal-app/core').AnyNodeId, on)
|
|
90
401
|
restoreNodeVisibility()
|
|
91
402
|
}
|
|
92
403
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type BuildingNode, getLevelDisplayName, type LevelNode, useScene } from '@pascal-app/core'
|
|
4
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
6
|
+
import { cn } from '../../lib/utils'
|
|
7
|
+
import useEditor from '../../store/use-editor'
|
|
8
|
+
|
|
9
|
+
const rowClass = (selected: boolean) =>
|
|
10
|
+
cn(
|
|
11
|
+
'flex w-full min-w-0 items-center rounded-lg px-2.5 py-1.5 text-left font-medium text-xs transition-colors',
|
|
12
|
+
selected
|
|
13
|
+
? 'bg-white/10 text-foreground'
|
|
14
|
+
: 'text-muted-foreground/70 hover:bg-white/5 hover:text-muted-foreground',
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The capture view's level picker: the editor's level list, top floor first,
|
|
19
|
+
* under a "Whole building" row that shows every level (the default on entering
|
|
20
|
+
* capture). A level shows itself and the levels below it, as while editing.
|
|
21
|
+
*/
|
|
22
|
+
export function CaptureLevelPicker() {
|
|
23
|
+
const captureLevelId = useEditor((s) => s.captureLevelId)
|
|
24
|
+
const setCaptureLevel = useEditor((s) => s.setCaptureLevel)
|
|
25
|
+
const buildingId = useViewer((s) => s.selection.buildingId)
|
|
26
|
+
const levels = useScene(
|
|
27
|
+
useShallow((state) => {
|
|
28
|
+
const building = (
|
|
29
|
+
buildingId
|
|
30
|
+
? state.nodes[buildingId]
|
|
31
|
+
: Object.values(state.nodes).find((n) => n?.type === 'building')
|
|
32
|
+
) as BuildingNode | undefined
|
|
33
|
+
if (building?.type !== 'building') return [] as LevelNode[]
|
|
34
|
+
return building.children
|
|
35
|
+
.map((id) => state.nodes[id])
|
|
36
|
+
.filter((node): node is LevelNode => node?.type === 'level')
|
|
37
|
+
.sort((a, b) => b.level - a.level)
|
|
38
|
+
}),
|
|
39
|
+
)
|
|
40
|
+
if (levels.length < 2) return null
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
aria-label="Levels in the shot"
|
|
45
|
+
className="pointer-events-auto absolute top-4 left-4 flex w-36 flex-col gap-0.5 rounded-xl border border-border bg-background/90 p-1 shadow-2xl backdrop-blur-md"
|
|
46
|
+
data-capture-level-picker
|
|
47
|
+
role="group"
|
|
48
|
+
>
|
|
49
|
+
<button
|
|
50
|
+
aria-pressed={captureLevelId === null}
|
|
51
|
+
className={rowClass(captureLevelId === null)}
|
|
52
|
+
onClick={() => setCaptureLevel(null)}
|
|
53
|
+
type="button"
|
|
54
|
+
>
|
|
55
|
+
<span className="truncate">Whole building</span>
|
|
56
|
+
</button>
|
|
57
|
+
<div className="mx-1.5 my-0.5 h-px bg-border/60" />
|
|
58
|
+
{levels.map((level) => (
|
|
59
|
+
<button
|
|
60
|
+
aria-pressed={captureLevelId === level.id}
|
|
61
|
+
className={rowClass(captureLevelId === level.id)}
|
|
62
|
+
key={level.id}
|
|
63
|
+
onClick={() => setCaptureLevel(level.id)}
|
|
64
|
+
title={getLevelDisplayName(level)}
|
|
65
|
+
type="button"
|
|
66
|
+
>
|
|
67
|
+
<span className="truncate">{getLevelDisplayName(level)}</span>
|
|
68
|
+
</button>
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
}
|