@pascal-app/editor 1.0.3 → 1.0.5
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/README.md +106 -0
- package/package.json +42 -38
- package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +307 -5
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
- package/src/components/editor/construction-reveal-audio.tsx +40 -0
- package/src/components/editor/custom-camera-controls.tsx +275 -57
- 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 -26
- package/src/components/editor/editor-layout-v2.tsx +132 -59
- package/src/components/editor/export-manager.tsx +47 -26
- package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
- package/src/components/editor/first-person/build-collider-world.ts +28 -7
- package/src/components/editor/first-person/ground-holes.test.ts +128 -0
- package/src/components/editor/first-person/ground-holes.ts +130 -0
- package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
- package/src/components/editor/first-person/mechanism-targets.ts +88 -0
- package/src/components/editor/first-person-controls.tsx +220 -9
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +466 -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-mode-coordinator.tsx +2 -2
- package/src/components/editor/floorplan-panel.tsx +414 -363
- package/src/components/editor/follow-pascal.test.tsx +912 -0
- package/src/components/editor/follow-pascal.tsx +354 -0
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/group-actions.ts +56 -2
- package/src/components/editor/group-move-3d.ts +2 -0
- package/src/components/editor/group-rotate-handle.tsx +2 -0
- package/src/components/editor/group-transform-shared.test.ts +460 -0
- 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 +269 -197
- package/src/components/editor/index.tsx +409 -197
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +68 -70
- package/src/components/editor/node-arrow-handles.tsx +25 -6
- 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 +80 -1
- package/src/components/editor/riser-diagram-panel.tsx +14 -7
- 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 +233 -0
- package/src/components/editor/room-labels.test.ts +850 -0
- package/src/components/editor/room-labels.tsx +1890 -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 +474 -419
- package/src/components/editor/slab-hole-highlights.tsx +5 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
- package/src/components/editor/snapshot-capture.test.ts +119 -60
- package/src/components/editor/snapshot-capture.ts +36 -5
- package/src/components/editor/thumbnail-generator.test.ts +654 -0
- package/src/components/editor/thumbnail-generator.tsx +1103 -223
- package/src/components/editor/use-floorplan-background-placement.ts +34 -12
- 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 +663 -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.test.tsx +54 -0
- package/src/components/editor/views/view-bar.tsx +343 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +43 -142
- 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 +8 -4
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
- 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 +2 -2
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
- 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-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
- package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
- package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/point-ask/bubble.tsx +538 -0
- package/src/components/point-ask/hover-chip.tsx +91 -0
- package/src/components/point-ask/mode-pill.tsx +92 -0
- package/src/components/point-ask/optional-motion.ts +22 -0
- package/src/components/point-ask/outline-corners.tsx +93 -0
- package/src/components/point-ask/pins.tsx +711 -0
- package/src/components/point-ask/point-ask-layer.tsx +83 -0
- package/src/components/point-ask/point-ask-styles.ts +105 -0
- package/src/components/point-ask/region-marquee-styles.ts +15 -0
- package/src/components/point-ask/region-marquee.tsx +64 -0
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
- 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/roof/roof-edit-system.tsx +1 -0
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +29 -68
- package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
- package/src/components/tools/distribution/connection-compatibility.ts +43 -0
- package/src/components/tools/distribution/distribution-public.ts +9 -0
- package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
- package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
- package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
- package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
- package/src/components/tools/distribution/draw-alignment.ts +35 -0
- package/src/components/tools/distribution/ports.test.ts +56 -0
- package/src/components/tools/distribution/ports.ts +378 -0
- package/src/components/tools/distribution/run-cursor.test.ts +135 -0
- package/src/components/tools/distribution/run-cursor.ts +36 -0
- package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
- package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
- package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
- package/src/components/tools/distribution/run-port-snap.ts +25 -0
- package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
- package/src/components/tools/distribution/run-screen-direction.ts +30 -0
- package/src/components/tools/elevator/elevator-tool.tsx +3 -6
- package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
- package/src/components/tools/item/placement-history.test.tsx +512 -0
- package/src/components/tools/item/placement-strategies.test.ts +46 -1
- package/src/components/tools/item/placement-strategies.ts +11 -10
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +166 -95
- package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
- 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 +62 -63
- package/src/components/tools/registry/item-surface-move.ts +2 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
- package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
- 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/cursor-sphere.tsx +36 -12
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/tool-manager.tsx +59 -24
- package/src/components/tools/wall/wall-drafting.test.ts +175 -47
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +9 -1
- package/src/components/ui/action-menu/camera-actions.tsx +9 -4
- package/src/components/ui/action-menu/control-modes.tsx +15 -42
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
- package/src/components/ui/action-menu/index.tsx +56 -33
- package/src/components/ui/action-menu/measurement-control.tsx +14 -19
- package/src/components/ui/action-menu/placement.ts +20 -0
- package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
- package/src/components/ui/action-menu/view-toggles.tsx +47 -42
- package/src/components/ui/build-panel/build-panel.tsx +242 -0
- package/src/components/ui/command-palette/editor-commands.tsx +7 -42
- package/src/components/ui/command-palette/index.tsx +50 -39
- package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
- package/src/components/ui/controls/action-button.tsx +5 -2
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +97 -26
- package/src/components/ui/controls/material-picker.tsx +4 -1
- package/src/components/ui/controls/metric-control.tsx +1 -1
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/segmented-control.tsx +21 -8
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/controls/slider-control.tsx +1 -1
- package/src/components/ui/controls/toggle-control.tsx +3 -3
- package/src/components/ui/controls/tool-options-panel.tsx +1 -1
- package/src/components/ui/floating-level-selector.tsx +159 -59
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
- package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
- package/src/components/ui/helpers/helper-manager.tsx +331 -14
- package/src/components/ui/helpers/item-helper.tsx +17 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
- package/src/components/ui/icon-ref.tsx +45 -1
- 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 +28 -22
- 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 +4 -4
- package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
- 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-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
- 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 +130 -62
- package/src/components/ui/panels/parametric-field-control.tsx +3 -3
- package/src/components/ui/panels/parametric-inspector.tsx +7 -7
- 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 +790 -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/badge.tsx +8 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/sidebar.tsx +3 -1
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +164 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
- package/src/components/ui/sidebar/icon-rail.tsx +8 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
- package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
- package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
- 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 +181 -223
- 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 +13 -37
- 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 +2 -4
- package/src/components/ui/sidebar/tab-bar.tsx +14 -4
- 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.tsx +5 -6
- package/src/components/viewer/viewer-controls-bar.tsx +18 -6
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +191 -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 +3 -8
- package/src/hooks/use-auto-save.integration.test.tsx +215 -43
- package/src/hooks/use-auto-save.ts +60 -9
- package/src/hooks/use-ceiling-events.test.ts +123 -118
- package/src/hooks/use-grid-events.test.ts +105 -105
- package/src/hooks/use-keyboard.test.ts +243 -19
- package/src/hooks/use-keyboard.ts +118 -83
- 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 +71 -0
- package/src/hooks/use-placement-notice.ts +73 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +312 -58
- package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
- package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
- package/src/lib/add-storey.test.ts +177 -0
- package/src/lib/add-storey.ts +59 -0
- package/src/lib/below-grade.test.tsx +171 -0
- package/src/lib/below-grade.ts +133 -0
- package/src/lib/camera-facing.test.ts +24 -0
- package/src/lib/camera-facing.ts +24 -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/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/direct-manipulation.ts +2 -0
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/download-project-file.ts +17 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +55 -0
- package/src/lib/editor-views.ts +178 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/export-texture-utils.ts +2 -4
- 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 +284 -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 +12 -8
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +32 -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 +363 -27
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -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/pdfkit-standard-fonts.d.ts +5 -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 +124 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -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 +230 -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 +157 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
- package/src/lib/floorplan/site-plan/geometry.ts +142 -0
- package/src/lib/floorplan/site-plan/index.ts +55 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +287 -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 +70 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
- package/src/lib/follow-pascal-exterior.test.ts +171 -0
- package/src/lib/follow-pascal-exterior.ts +39 -0
- package/src/lib/follow-pascal-presentation.test.ts +203 -0
- package/src/lib/follow-pascal-presentation.ts +129 -0
- package/src/lib/follow-pascal-sight.ts +84 -0
- package/src/lib/follow-pascal-targets.test.ts +406 -0
- package/src/lib/follow-pascal-targets.ts +198 -0
- package/src/lib/follow-pascal.test.ts +350 -0
- package/src/lib/follow-pascal.ts +284 -0
- package/src/lib/fresh-planar-placement.test.ts +148 -0
- package/src/lib/fresh-planar-placement.ts +114 -37
- package/src/lib/geometry-script/author.ts +157 -0
- package/src/lib/geometry-script/client.ts +159 -0
- package/src/lib/geometry-script/finish-scripts.ts +127 -0
- package/src/lib/geometry-script/protocol.ts +13 -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 +636 -126
- package/src/lib/glb-export.ts +241 -120
- package/src/lib/history.test.ts +18 -7
- 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 +22 -0
- package/src/lib/keyboard-pan.test.ts +129 -7
- package/src/lib/keyboard-pan.ts +161 -12
- package/src/lib/level-selection.test.ts +57 -0
- package/src/lib/level-selection.ts +16 -1
- 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 +187 -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 +392 -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 +3 -1
- package/src/lib/material-paint-panel-model.ts +9 -5
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/measurement-kind.test.ts +6 -8
- package/src/lib/measurements.test.ts +23 -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 +201 -0
- package/src/lib/orbit-floor-clearance.ts +154 -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 +122 -0
- package/src/lib/paint-pass-through.ts +78 -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 +267 -25
- package/src/lib/panel-rows.ts +21 -0
- package/src/lib/parametric-node-update.ts +16 -7
- 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 +65 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
- package/src/lib/point-ask/__fixtures__/house.ts +81 -0
- package/src/lib/point-ask/ask-created.test.ts +195 -0
- package/src/lib/point-ask/ask-flow.test.ts +380 -0
- package/src/lib/point-ask/build-context.test.ts +173 -0
- package/src/lib/point-ask/build-context.ts +94 -0
- package/src/lib/point-ask/capture-crop.ts +129 -0
- package/src/lib/point-ask/choreography.ts +69 -0
- package/src/lib/point-ask/compare.test.ts +145 -0
- package/src/lib/point-ask/compare.ts +73 -0
- package/src/lib/point-ask/describe-target.test.ts +165 -0
- package/src/lib/point-ask/describe-target.ts +234 -0
- package/src/lib/point-ask/dictation.ts +79 -0
- package/src/lib/point-ask/edge-marker.test.ts +61 -0
- package/src/lib/point-ask/edge-marker.ts +27 -0
- package/src/lib/point-ask/frame-loop.ts +25 -0
- package/src/lib/point-ask/history-marks.test.ts +94 -0
- package/src/lib/point-ask/history-marks.ts +39 -0
- package/src/lib/point-ask/host-api.ts +32 -0
- package/src/lib/point-ask/outline-box.test.ts +30 -0
- package/src/lib/point-ask/outline-box.ts +11 -0
- package/src/lib/point-ask/overlay-metrics.ts +6 -0
- package/src/lib/point-ask/pick.test.ts +220 -0
- package/src/lib/point-ask/pick.ts +121 -0
- package/src/lib/point-ask/pin-machine.test.ts +494 -0
- package/src/lib/point-ask/pin-machine.ts +217 -0
- package/src/lib/point-ask/placement.test.ts +203 -0
- package/src/lib/point-ask/placement.ts +141 -0
- package/src/lib/point-ask/pointer-tracker.ts +34 -0
- package/src/lib/point-ask/projector.ts +83 -0
- package/src/lib/point-ask/region.test.ts +157 -0
- package/src/lib/point-ask/region.ts +136 -0
- package/src/lib/point-ask/retry.test.ts +217 -0
- package/src/lib/point-ask/retry.ts +37 -0
- package/src/lib/point-ask/scene-commit.test.ts +312 -0
- package/src/lib/point-ask/scene-commit.ts +32 -0
- package/src/lib/point-ask/session.ts +1391 -0
- package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
- package/src/lib/point-ask/spring-keyframes.ts +38 -0
- package/src/lib/point-ask/spring.test.ts +63 -0
- package/src/lib/point-ask/spring.ts +107 -0
- package/src/lib/point-ask/suggestions.test.ts +80 -0
- package/src/lib/point-ask/suggestions.ts +125 -0
- package/src/lib/point-ask/target-scope.test.ts +622 -0
- package/src/lib/point-ask/target-scope.ts +338 -0
- package/src/lib/point-ask/types.ts +48 -0
- package/src/lib/portable-export.test.ts +34 -1
- package/src/lib/portable-export.ts +119 -53
- package/src/lib/print-feature-thickness.ts +10 -1
- package/src/lib/print-roof-solids.test.ts +39 -1
- package/src/lib/print-roof-solids.ts +43 -10
- package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
- 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 +265 -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 +1017 -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 +610 -1
- package/src/lib/scene-clipboard.ts +269 -58
- 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 +24 -36
- package/src/lib/scene.ts +33 -57
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +54 -75
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sfx-bus.ts +13 -0
- package/src/lib/sfx-player.test.ts +70 -9
- package/src/lib/sfx-player.ts +6 -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 +4 -0
- package/src/lib/snapping-mode.ts +8 -1
- package/src/lib/stair-levels.test.ts +25 -0
- package/src/lib/stair-levels.ts +22 -3
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +214 -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 +580 -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-placement-pointer.test.ts +42 -0
- package/src/lib/wall-placement-pointer.ts +57 -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-changes.ts +35 -0
- package/src/lib/window-interaction.ts +14 -0
- 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/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-audio.test.ts +146 -0
- package/src/store/use-audio.tsx +127 -3
- package/src/store/use-construction-reveal.test.ts +173 -0
- package/src/store/use-construction-reveal.ts +145 -0
- package/src/store/use-corner-snap-hint.test.ts +24 -0
- package/src/store/use-corner-snap-hint.ts +47 -0
- 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 +432 -321
- package/src/store/use-floorplan-draft-preview.ts +13 -0
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-follow-pascal.test.ts +62 -0
- package/src/store/use-follow-pascal.ts +53 -0
- package/src/store/use-point-ask.ts +247 -0
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +33 -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 +201 -0
- package/src/store/view-layout.ts +199 -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/render-current-markup.tsx +16 -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/tools/stair/stair-click-guard.test.ts +0 -58
- package/src/components/tools/stair/stair-click-guard.ts +0 -72
- package/src/components/tools/stair/stair-defaults.ts +0 -18
- package/src/components/tools/stair/stair-tool.tsx +0 -634
- 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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
applySceneOperationPatch,
|
|
6
|
+
BuildingNode,
|
|
7
|
+
clearSceneHistory,
|
|
8
|
+
LevelNode,
|
|
9
|
+
useScene,
|
|
10
|
+
WallNode,
|
|
11
|
+
} from '@pascal-app/core'
|
|
12
|
+
import { createSessionWrites } from './session-writes'
|
|
13
|
+
|
|
14
|
+
// A live session (a floor-plan drag, a height scrub) takes back only what it
|
|
15
|
+
// wrote itself: an edit a collaborator lands on the same node mid-drag survives
|
|
16
|
+
// the cancel and the commit.
|
|
17
|
+
|
|
18
|
+
globalThis.requestAnimationFrame ??= (callback) => {
|
|
19
|
+
callback(0)
|
|
20
|
+
return 0
|
|
21
|
+
}
|
|
22
|
+
globalThis.cancelAnimationFrame ??= () => {}
|
|
23
|
+
|
|
24
|
+
const level = LevelNode.parse({ id: 'level_writes', parentId: 'building_writes' })
|
|
25
|
+
const building = BuildingNode.parse({ id: 'building_writes', children: [level.id] })
|
|
26
|
+
const wall = WallNode.parse({ id: 'wall_writes', parentId: level.id, start: [0, 0], end: [4, 0] })
|
|
27
|
+
const nodes = () => useScene.getState().nodes as Record<string, AnyNode>
|
|
28
|
+
const data = () => JSON.parse(JSON.stringify(nodes()))
|
|
29
|
+
const id = wall.id as AnyNodeId
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
useScene.setState({
|
|
33
|
+
nodes: {
|
|
34
|
+
[building.id]: building,
|
|
35
|
+
[level.id]: { ...level, children: [wall.id] },
|
|
36
|
+
[wall.id]: wall,
|
|
37
|
+
},
|
|
38
|
+
rootNodeIds: [building.id],
|
|
39
|
+
dirtyNodes: new Set(),
|
|
40
|
+
collections: {},
|
|
41
|
+
materials: {},
|
|
42
|
+
readOnly: false,
|
|
43
|
+
})
|
|
44
|
+
clearSceneHistory()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
/** A collaborator's change, applied outside the session like a remote operation. */
|
|
48
|
+
const remote = (patch: Partial<WallNode>) =>
|
|
49
|
+
useScene.setState({ nodes: { ...nodes(), [id]: { ...nodes()[id]!, ...patch } as AnyNode } })
|
|
50
|
+
|
|
51
|
+
describe('session writes', () => {
|
|
52
|
+
test('cancel takes back the session’s fields and keeps a collaborator’s edit', () => {
|
|
53
|
+
const writes = createSessionWrites()
|
|
54
|
+
writes.record(() => useScene.getState().updateNodes([{ id, data: { end: [6, 0] } }]))
|
|
55
|
+
remote({ name: 'Kitchen wall' })
|
|
56
|
+
writes.record(() => useScene.getState().updateNodes([{ id, data: { end: [7, 0] } }]))
|
|
57
|
+
writes.revert()
|
|
58
|
+
expect(nodes()[id]).toMatchObject({ end: [4, 0], name: 'Kitchen wall' })
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('commit re-applies only the session’s fields', () => {
|
|
62
|
+
const writes = createSessionWrites()
|
|
63
|
+
writes.record(() => useScene.getState().updateNodes([{ id, data: { end: [6, 0] } }]))
|
|
64
|
+
remote({ name: 'Kitchen wall' })
|
|
65
|
+
const changes = writes.changes()
|
|
66
|
+
expect(changes.update).toEqual([{ id, data: { end: [6, 0] } }])
|
|
67
|
+
writes.revert()
|
|
68
|
+
useScene.getState().applyNodeChanges(changes)
|
|
69
|
+
expect(nodes()[id]).toMatchObject({ end: [6, 0], name: 'Kitchen wall' })
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('a session that previews through overrides writes nothing back', () => {
|
|
73
|
+
const writes = createSessionWrites()
|
|
74
|
+
writes.record(() => {})
|
|
75
|
+
remote({ name: 'Kitchen wall' })
|
|
76
|
+
const before = data()
|
|
77
|
+
writes.revert()
|
|
78
|
+
expect(data()).toEqual(before)
|
|
79
|
+
expect(writes.changes()).toEqual({ create: [], update: [], delete: [] })
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('nodes the session created go, nodes it deleted come back, outside history', () => {
|
|
83
|
+
const writes = createSessionWrites()
|
|
84
|
+
const extra = WallNode.parse({
|
|
85
|
+
id: 'wall_extra',
|
|
86
|
+
parentId: level.id,
|
|
87
|
+
start: [4, 0],
|
|
88
|
+
end: [4, 4],
|
|
89
|
+
})
|
|
90
|
+
const before = data()
|
|
91
|
+
writes.record(() => {
|
|
92
|
+
useScene.getState().createNode(extra, level.id as AnyNodeId)
|
|
93
|
+
useScene.getState().deleteNode(id)
|
|
94
|
+
})
|
|
95
|
+
const past = useScene.temporal.getState().pastStates.length
|
|
96
|
+
writes.revert()
|
|
97
|
+
expect(data()).toEqual(before)
|
|
98
|
+
expect(useScene.temporal.getState().pastStates).toHaveLength(past)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('a watched node: another local writer’s draft goes back, a collaborator’s change stays', () => {
|
|
102
|
+
const writes = createSessionWrites()
|
|
103
|
+
writes.watch([id])
|
|
104
|
+
// The 3D tool mounted beside the plan drafts the same node…
|
|
105
|
+
useScene.getState().updateNodes([{ id, data: { metadata: { isTransient: true } } }])
|
|
106
|
+
// …while a collaborator's rename arrives from the host.
|
|
107
|
+
applySceneOperationPatch({
|
|
108
|
+
materialChanges: [],
|
|
109
|
+
nodeUpdates: [{ id, data: { name: 'Kitchen wall' }, removeFields: [] }],
|
|
110
|
+
nodeCreates: [],
|
|
111
|
+
nodeDeletes: [],
|
|
112
|
+
})
|
|
113
|
+
writes.revert()
|
|
114
|
+
expect(nodes()[id]).toMatchObject({ metadata: {}, name: 'Kitchen wall' })
|
|
115
|
+
// The revert ended the watch: later writes are no longer the session's.
|
|
116
|
+
useScene.getState().updateNodes([{ id, data: { end: [5, 0] } }])
|
|
117
|
+
writes.revert()
|
|
118
|
+
expect(nodes()[id]).toMatchObject({ end: [5, 0] })
|
|
119
|
+
})
|
|
120
|
+
})
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
isApplyingRemoteSceneChange,
|
|
5
|
+
pauseSceneHistory,
|
|
6
|
+
resumeSceneHistory,
|
|
7
|
+
useScene,
|
|
8
|
+
} from '@pascal-app/core'
|
|
9
|
+
|
|
10
|
+
type NodeMap = Record<string, AnyNode>
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* What a live session wrote to the scene: for each node it touched, the value
|
|
14
|
+
* before its first write and the value it last wrote (`undefined` for
|
|
15
|
+
* "absent"). Only writes made inside `record` count, so a collaborator's change
|
|
16
|
+
* applied between two pointer moves is never mistaken for the session's.
|
|
17
|
+
*/
|
|
18
|
+
export type SessionWrites = {
|
|
19
|
+
/** Runs one step of the session and records what it wrote. */
|
|
20
|
+
record: <T>(run: () => T) => T
|
|
21
|
+
/**
|
|
22
|
+
* Also records every local write to `ids` until the session ends, whoever
|
|
23
|
+
* makes it (the 3D tool mounted beside a plan session in split view drafts
|
|
24
|
+
* the same node) — never a collaborator's change applied from the host.
|
|
25
|
+
*/
|
|
26
|
+
watch: (ids: readonly string[]) => void
|
|
27
|
+
/** Stops watching (the revert stops too). */
|
|
28
|
+
stop: () => void
|
|
29
|
+
/**
|
|
30
|
+
* Takes back what the session wrote, outside history. A node changed again
|
|
31
|
+
* since by someone else keeps that change: only the session's own fields go
|
|
32
|
+
* back. A session that never wrote the scene touches nothing.
|
|
33
|
+
*/
|
|
34
|
+
revert: () => void
|
|
35
|
+
/** The session's writes as one change batch from the originals (the commit a legacy session makes). */
|
|
36
|
+
changes: () => {
|
|
37
|
+
create: { node: AnyNode; parentId?: AnyNodeId }[]
|
|
38
|
+
update: { id: AnyNodeId; data: Partial<AnyNode> }[]
|
|
39
|
+
delete: AnyNodeId[]
|
|
40
|
+
}
|
|
41
|
+
readonly size: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The value a field had before the session first wrote it, and the value it last wrote. */
|
|
45
|
+
type FieldWrite = { had: boolean; from: unknown; to: unknown }
|
|
46
|
+
|
|
47
|
+
type Entry = {
|
|
48
|
+
original: AnyNode | undefined
|
|
49
|
+
wrote: AnyNode | undefined
|
|
50
|
+
/** Fields the session changed on a node that existed before and after its write. */
|
|
51
|
+
fields: Map<string, FieldWrite>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createSessionWrites(): SessionWrites {
|
|
55
|
+
const writes = new Map<string, Entry>()
|
|
56
|
+
let unwatch: (() => void) | null = null
|
|
57
|
+
|
|
58
|
+
const note = (before: NodeMap, after: NodeMap, only?: ReadonlySet<string>) => {
|
|
59
|
+
if (before === after) return
|
|
60
|
+
const ids = only ?? new Set([...Object.keys(before), ...Object.keys(after)])
|
|
61
|
+
for (const id of ids) {
|
|
62
|
+
const prev = before[id]
|
|
63
|
+
const next = after[id]
|
|
64
|
+
if (prev === next) continue
|
|
65
|
+
let entry = writes.get(id)
|
|
66
|
+
if (!entry) {
|
|
67
|
+
entry = { original: prev, wrote: next, fields: new Map() }
|
|
68
|
+
writes.set(id, entry)
|
|
69
|
+
} else entry.wrote = next
|
|
70
|
+
if (!(prev && next)) continue
|
|
71
|
+
const from = prev as Record<string, unknown>
|
|
72
|
+
const to = next as Record<string, unknown>
|
|
73
|
+
for (const key of new Set([...Object.keys(from), ...Object.keys(to)])) {
|
|
74
|
+
if (Object.is(from[key], to[key])) continue
|
|
75
|
+
const field = entry.fields.get(key)
|
|
76
|
+
if (field) field.to = to[key]
|
|
77
|
+
else entry.fields.set(key, { had: Object.hasOwn(from, key), from: from[key], to: to[key] })
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const created = (id: string) => {
|
|
83
|
+
const entry = writes.get(id)
|
|
84
|
+
return !!entry && !entry.original && !!entry.wrote
|
|
85
|
+
}
|
|
86
|
+
const deleted = (id: string) => {
|
|
87
|
+
const entry = writes.get(id)
|
|
88
|
+
return !!entry && !!entry.original && !entry.wrote
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Field by field: a field the session wrote goes back unless someone else
|
|
92
|
+
// has written it since; every other field keeps its current value. A child
|
|
93
|
+
// list drops the session's creations and gets back what it deleted.
|
|
94
|
+
const revertFields = (current: AnyNode, entry: Entry): AnyNode | null => {
|
|
95
|
+
const merged: Record<string, unknown> = { ...current }
|
|
96
|
+
const cur = current as Record<string, unknown>
|
|
97
|
+
let changed = false
|
|
98
|
+
for (const [key, { had, from, to }] of entry.fields) {
|
|
99
|
+
if (key === 'children' && Array.isArray(cur[key]) && Array.isArray(from)) {
|
|
100
|
+
const children = (cur[key] as string[]).filter((id) => !created(id))
|
|
101
|
+
for (const id of from as string[])
|
|
102
|
+
if (deleted(id) && !children.includes(id)) children.push(id)
|
|
103
|
+
if (children.join('|') !== (cur[key] as string[]).join('|')) {
|
|
104
|
+
merged[key] = children
|
|
105
|
+
changed = true
|
|
106
|
+
}
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
if (!Object.is(cur[key], to)) continue
|
|
110
|
+
if (had) merged[key] = from
|
|
111
|
+
else delete merged[key]
|
|
112
|
+
changed = true
|
|
113
|
+
}
|
|
114
|
+
if (!changed) return null
|
|
115
|
+
// Nothing else moved: the node the session found, as it was (same object).
|
|
116
|
+
const original = entry.original as Record<string, unknown> | undefined
|
|
117
|
+
if (
|
|
118
|
+
original &&
|
|
119
|
+
Object.keys(merged).length === Object.keys(original).length &&
|
|
120
|
+
Object.keys(merged).every((key) => Object.is(merged[key], original[key]))
|
|
121
|
+
)
|
|
122
|
+
return entry.original!
|
|
123
|
+
return merged as AnyNode
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const stop = () => {
|
|
127
|
+
unwatch?.()
|
|
128
|
+
unwatch = null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
watch(ids) {
|
|
133
|
+
stop()
|
|
134
|
+
const scope = new Set(ids)
|
|
135
|
+
unwatch = useScene.subscribe((state, previous) => {
|
|
136
|
+
if (isApplyingRemoteSceneChange()) return
|
|
137
|
+
note(previous.nodes as NodeMap, state.nodes as NodeMap, scope)
|
|
138
|
+
})
|
|
139
|
+
},
|
|
140
|
+
stop,
|
|
141
|
+
record(run) {
|
|
142
|
+
const before = useScene.getState().nodes as NodeMap
|
|
143
|
+
try {
|
|
144
|
+
return run()
|
|
145
|
+
} finally {
|
|
146
|
+
note(before, useScene.getState().nodes as NodeMap)
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
revert() {
|
|
150
|
+
stop()
|
|
151
|
+
if (writes.size === 0) return
|
|
152
|
+
const nodes = useScene.getState().nodes as NodeMap
|
|
153
|
+
const next: NodeMap = { ...nodes }
|
|
154
|
+
const touched: AnyNode[] = []
|
|
155
|
+
for (const [id, entry] of writes) {
|
|
156
|
+
const current = nodes[id]
|
|
157
|
+
if (!entry.original) {
|
|
158
|
+
if (!current) continue
|
|
159
|
+
delete next[id]
|
|
160
|
+
} else if (!current) {
|
|
161
|
+
next[id] = entry.original
|
|
162
|
+
} else {
|
|
163
|
+
const reverted = revertFields(current, entry)
|
|
164
|
+
if (!reverted) continue
|
|
165
|
+
next[id] = reverted
|
|
166
|
+
}
|
|
167
|
+
touched.push(entry.original ?? current!)
|
|
168
|
+
}
|
|
169
|
+
writes.clear()
|
|
170
|
+
if (touched.length === 0) return
|
|
171
|
+
pauseSceneHistory(useScene)
|
|
172
|
+
try {
|
|
173
|
+
useScene.setState({ nodes: next as ReturnType<typeof useScene.getState>['nodes'] })
|
|
174
|
+
} finally {
|
|
175
|
+
resumeSceneHistory(useScene)
|
|
176
|
+
}
|
|
177
|
+
const { markDirty } = useScene.getState()
|
|
178
|
+
for (const node of touched) {
|
|
179
|
+
if (next[node.id]) markDirty(node.id as AnyNodeId)
|
|
180
|
+
if (node.parentId && next[node.parentId]) markDirty(node.parentId as AnyNodeId)
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
changes() {
|
|
184
|
+
const batch: ReturnType<SessionWrites['changes']> = { create: [], update: [], delete: [] }
|
|
185
|
+
for (const [id, { original, wrote, fields }] of writes) {
|
|
186
|
+
if (!original && wrote) {
|
|
187
|
+
batch.create.push({ node: wrote, parentId: (wrote.parentId ?? undefined) as AnyNodeId })
|
|
188
|
+
} else if (original && !wrote) {
|
|
189
|
+
batch.delete.push(id as AnyNodeId)
|
|
190
|
+
} else if (original && wrote) {
|
|
191
|
+
const data: Record<string, unknown> = {}
|
|
192
|
+
// Child lists follow the creates and deletes.
|
|
193
|
+
for (const [key, { from, to }] of fields)
|
|
194
|
+
if (key !== 'children' && JSON.stringify(from) !== JSON.stringify(to)) data[key] = to
|
|
195
|
+
if (Object.keys(data).length)
|
|
196
|
+
batch.update.push({ id: id as AnyNodeId, data: data as Partial<AnyNode> })
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return batch
|
|
200
|
+
},
|
|
201
|
+
get size() {
|
|
202
|
+
return writes.size
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
}
|
package/src/lib/sfx-bus.ts
CHANGED
|
@@ -28,6 +28,9 @@ type SFXEvents = {
|
|
|
28
28
|
'sfx:menu-click': undefined
|
|
29
29
|
'sfx:paint-apply': undefined
|
|
30
30
|
'sfx:success': undefined
|
|
31
|
+
'sfx:turn-done': undefined
|
|
32
|
+
'sfx:turn-failed': undefined
|
|
33
|
+
'sfx:turn-needs-input': undefined
|
|
31
34
|
'sfx:terrain-sculpt-start': TerrainVerb
|
|
32
35
|
'sfx:terrain-sculpt-stop': undefined
|
|
33
36
|
}
|
|
@@ -58,6 +61,9 @@ const handleMenuHover = () => playSFX('menuHover')
|
|
|
58
61
|
const handleMenuClick = () => playSFX('menuClick')
|
|
59
62
|
const handlePaintApply = () => playSFX('paintApply')
|
|
60
63
|
const handleSuccess = () => playSFX('success')
|
|
64
|
+
const handleTurnDone = () => playSFX('turnDone')
|
|
65
|
+
const handleTurnFailed = () => playSFX('turnFailed')
|
|
66
|
+
const handleTurnNeedsInput = () => playSFX('turnNeedsInput')
|
|
61
67
|
const TERRAIN_LOOP_BY_VERB = {
|
|
62
68
|
raise: 'terrainRaise',
|
|
63
69
|
lower: 'terrainLower',
|
|
@@ -89,9 +95,13 @@ export function initSFXBus() {
|
|
|
89
95
|
sfxEmitter.on('sfx:menu-click', handleMenuClick)
|
|
90
96
|
sfxEmitter.on('sfx:paint-apply', handlePaintApply)
|
|
91
97
|
sfxEmitter.on('sfx:success', handleSuccess)
|
|
98
|
+
sfxEmitter.on('sfx:turn-done', handleTurnDone)
|
|
99
|
+
sfxEmitter.on('sfx:turn-failed', handleTurnFailed)
|
|
100
|
+
sfxEmitter.on('sfx:turn-needs-input', handleTurnNeedsInput)
|
|
92
101
|
sfxEmitter.on('sfx:terrain-sculpt-start', handleTerrainSculptStart)
|
|
93
102
|
sfxEmitter.on('sfx:terrain-sculpt-stop', handleTerrainSculptStop)
|
|
94
103
|
unsubscribeAudio = useAudio.subscribe(updateSFXVolumes)
|
|
104
|
+
updateSFXVolumes()
|
|
95
105
|
}
|
|
96
106
|
|
|
97
107
|
export function disposeSFXBus() {
|
|
@@ -110,6 +120,9 @@ export function disposeSFXBus() {
|
|
|
110
120
|
sfxEmitter.off('sfx:menu-click', handleMenuClick)
|
|
111
121
|
sfxEmitter.off('sfx:paint-apply', handlePaintApply)
|
|
112
122
|
sfxEmitter.off('sfx:success', handleSuccess)
|
|
123
|
+
sfxEmitter.off('sfx:turn-done', handleTurnDone)
|
|
124
|
+
sfxEmitter.off('sfx:turn-failed', handleTurnFailed)
|
|
125
|
+
sfxEmitter.off('sfx:turn-needs-input', handleTurnNeedsInput)
|
|
113
126
|
sfxEmitter.off('sfx:terrain-sculpt-start', handleTerrainSculptStart)
|
|
114
127
|
sfxEmitter.off('sfx:terrain-sculpt-stop', handleTerrainSculptStop)
|
|
115
128
|
unsubscribeAudio?.()
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test'
|
|
2
|
+
import useAudio from '../store/use-audio'
|
|
2
3
|
|
|
3
4
|
type FakeContext = { id: string }
|
|
4
5
|
|
|
5
6
|
let activeContext: FakeContext = { id: 'first' }
|
|
6
7
|
let initialState: 'loaded' | 'loading' = 'loaded'
|
|
7
|
-
let muted = false
|
|
8
8
|
let throwOnPlay = false
|
|
9
9
|
const instances: FakeHowl[] = []
|
|
10
10
|
|
|
@@ -92,12 +92,6 @@ const fakeHowler = {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
mock.module('howler', () => ({ Howl: FakeHowl, Howler: fakeHowler }))
|
|
95
|
-
mock.module('../store/use-audio', () => ({
|
|
96
|
-
default: {
|
|
97
|
-
getState: () => ({ masterVolume: 100, muted, sfxVolume: 100 }),
|
|
98
|
-
subscribe: () => () => {},
|
|
99
|
-
},
|
|
100
|
-
}))
|
|
101
95
|
|
|
102
96
|
const { disposeSFX, playSFX, preloadSFX, startLoopSFX, stopLoopSFX, updateSFXVolumes } =
|
|
103
97
|
await import('./sfx-player')
|
|
@@ -107,7 +101,7 @@ beforeEach(() => {
|
|
|
107
101
|
disposeSFXBus()
|
|
108
102
|
activeContext = { id: 'first' }
|
|
109
103
|
initialState = 'loaded'
|
|
110
|
-
muted
|
|
104
|
+
useAudio.setState({ masterVolume: 100, muted: false, sfxVolume: 100 })
|
|
111
105
|
throwOnPlay = false
|
|
112
106
|
instances.length = 0
|
|
113
107
|
})
|
|
@@ -118,6 +112,51 @@ afterAll(() => {
|
|
|
118
112
|
})
|
|
119
113
|
|
|
120
114
|
describe('SFX audio context lifecycle', () => {
|
|
115
|
+
test('editor startup warms assets without playing and repeated init does not duplicate delivery', () => {
|
|
116
|
+
initSFXBus()
|
|
117
|
+
const warmed = [...instances]
|
|
118
|
+
expect(warmed.length).toBeGreaterThan(0)
|
|
119
|
+
expect(warmed.every((sound) => sound.playCount === 0)).toBe(true)
|
|
120
|
+
initSFXBus()
|
|
121
|
+
expect(instances).toHaveLength(warmed.length)
|
|
122
|
+
triggerSFX('sfx:item-place')
|
|
123
|
+
expect(instances.reduce((sum, sound) => sum + sound.playCount, 0)).toBe(1)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('a one-shot requested before loading is dropped, not replayed on load', () => {
|
|
127
|
+
initialState = 'loading'
|
|
128
|
+
initSFXBus()
|
|
129
|
+
triggerSFX('sfx:item-place')
|
|
130
|
+
for (const sound of instances) sound.finishLoading()
|
|
131
|
+
expect(instances.every((sound) => sound.playCount === 0)).toBe(true)
|
|
132
|
+
// A different fresh cue avoids relying on the per-name repeat clock.
|
|
133
|
+
triggerSFX('sfx:structure-build')
|
|
134
|
+
expect(instances.reduce((sum, sound) => sum + sound.playCount, 0)).toBe(1)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('muted startup neither starts audio nor consumes a later fresh cue', () => {
|
|
138
|
+
useAudio.setState({ muted: true })
|
|
139
|
+
initSFXBus()
|
|
140
|
+
triggerSFX('sfx:item-place')
|
|
141
|
+
expect(instances.every((sound) => sound.playCount === 0)).toBe(true)
|
|
142
|
+
useAudio.setState({ muted: false })
|
|
143
|
+
updateSFXVolumes()
|
|
144
|
+
triggerSFX('sfx:item-place')
|
|
145
|
+
expect(instances.reduce((sum, sound) => sum + sound.playCount, 0)).toBe(1)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('cleanup detaches delivery and unloads resources once, then remount plays once', () => {
|
|
149
|
+
initSFXBus()
|
|
150
|
+
const warmed = [...instances]
|
|
151
|
+
disposeSFXBus()
|
|
152
|
+
disposeSFXBus()
|
|
153
|
+
triggerSFX('sfx:item-place')
|
|
154
|
+
expect(warmed.every((sound) => sound.playCount === 0 && sound.unloadCount === 1)).toBe(true)
|
|
155
|
+
initSFXBus()
|
|
156
|
+
triggerSFX('sfx:item-place')
|
|
157
|
+
expect(instances.reduce((sum, sound) => sum + sound.playCount, 0)).toBe(1)
|
|
158
|
+
})
|
|
159
|
+
|
|
121
160
|
test('reuses the preloaded cache while the Howler context is unchanged', () => {
|
|
122
161
|
preloadSFX()
|
|
123
162
|
const initialCount = instances.length
|
|
@@ -234,7 +273,7 @@ describe('SFX audio context lifecycle', () => {
|
|
|
234
273
|
startLoopSFX('terrainLower')
|
|
235
274
|
const lower = instances.find((sound) => sound.loop && sound.playCount === 1)
|
|
236
275
|
|
|
237
|
-
muted
|
|
276
|
+
useAudio.setState({ muted: true })
|
|
238
277
|
updateSFXVolumes()
|
|
239
278
|
|
|
240
279
|
expect(lower?.fadeCalls.at(-1)).toEqual([0.2, 0, 90, 1])
|
|
@@ -263,4 +302,26 @@ describe('SFX audio context lifecycle', () => {
|
|
|
263
302
|
|
|
264
303
|
expect(lower?.playCount).toBe(0)
|
|
265
304
|
})
|
|
305
|
+
|
|
306
|
+
test('turn cues play at the master and effects volume, and not in a muted tab', () => {
|
|
307
|
+
const cues = ['sfx:turn-done', 'sfx:turn-failed', 'sfx:turn-needs-input'] as const
|
|
308
|
+
useAudio.setState({ masterVolume: 50, muted: true, sfxVolume: 40 })
|
|
309
|
+
initSFXBus()
|
|
310
|
+
|
|
311
|
+
for (const cue of cues) triggerSFX(cue)
|
|
312
|
+
expect(instances.every((sound) => sound.playCount === 0)).toBe(true)
|
|
313
|
+
|
|
314
|
+
useAudio.setState({ muted: false })
|
|
315
|
+
for (const cue of cues) triggerSFX(cue)
|
|
316
|
+
|
|
317
|
+
const played = instances.filter((sound) => sound.playCount === 1)
|
|
318
|
+
expect(played.map((sound) => sound.src)).toEqual([
|
|
319
|
+
'/audios/sfx/turn_done.mp3',
|
|
320
|
+
'/audios/sfx/turn_failed.mp3',
|
|
321
|
+
'/audios/sfx/turn_needs_input.mp3',
|
|
322
|
+
])
|
|
323
|
+
for (const sound of played) {
|
|
324
|
+
expect(sound.volumeCalls.find(([, id]) => id === 1)?.[0]).toBeCloseTo(0.2)
|
|
325
|
+
}
|
|
326
|
+
})
|
|
266
327
|
})
|
package/src/lib/sfx-player.ts
CHANGED
|
@@ -120,6 +120,12 @@ export const SFX: Record<string, SFXConfig> = {
|
|
|
120
120
|
src: '/audios/sfx/success.mp3',
|
|
121
121
|
minIntervalMs: 3_000,
|
|
122
122
|
},
|
|
123
|
+
// An agent's turn, heard from another tab or another app: it ended with its work done, it failed,
|
|
124
|
+
// or it waits on the person. Two-note chimes (`scripts/synth-turn-cues.ts`), so no jitter, as for
|
|
125
|
+
// `success`. The host plays each once per turn; no gap here could tell two turns from one.
|
|
126
|
+
turnDone: { src: '/audios/sfx/turn_done.mp3' },
|
|
127
|
+
turnFailed: { src: '/audios/sfx/turn_failed.mp3' },
|
|
128
|
+
turnNeedsInput: { src: '/audios/sfx/turn_needs_input.mp3' },
|
|
123
129
|
} as const
|
|
124
130
|
|
|
125
131
|
export type SFXName = keyof typeof SFX
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
import { useSidebarStore } from '../components/ui/primitives/sidebar'
|
|
3
|
+
import type { SidebarTab } from '../components/ui/sidebar/tab-bar'
|
|
4
|
+
import useEditor from '../store/use-editor'
|
|
5
|
+
|
|
6
|
+
/** The narrowest a reopened sidebar panel may be (the layout's resize floor). */
|
|
7
|
+
export const SIDEBAR_MIN_WIDTH = 300
|
|
8
|
+
|
|
9
|
+
// The desktop layout's rail tabs, published while it is mounted. Hosts pick
|
|
10
|
+
// their own tabs, so a shortcut names the panels it would like, in order, and
|
|
11
|
+
// gets the first one the host has.
|
|
12
|
+
let sidebarTabIds: readonly string[] = []
|
|
13
|
+
|
|
14
|
+
export function setSidebarTabIds(ids: readonly string[]): void {
|
|
15
|
+
sidebarTabIds = ids
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Pick panel `id` and expand a collapsed sidebar onto it, at a usable width.
|
|
20
|
+
* The pick goes first: expanding first would reopen the panel left open for a
|
|
21
|
+
* moment, and a host may arm that panel's tool as it opens (Paint).
|
|
22
|
+
*/
|
|
23
|
+
export function showSidebarPanel(id: string): void {
|
|
24
|
+
useEditor.getState().setActiveSidebarPanel(id)
|
|
25
|
+
const sidebar = useSidebarStore.getState()
|
|
26
|
+
if (!sidebar.isCollapsed) return
|
|
27
|
+
sidebar.setIsCollapsed(false)
|
|
28
|
+
if (sidebar.width < SIDEBAR_MIN_WIDTH) sidebar.setWidth(SIDEBAR_MIN_WIDTH)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Show the first of `preferred` the host provides (`showSidebarPanel`).
|
|
33
|
+
* Returns the panel shown, or null when the host has none of them (e.g. on
|
|
34
|
+
* mobile).
|
|
35
|
+
*/
|
|
36
|
+
export function openSidebarPanel(preferred: readonly string[]): string | null {
|
|
37
|
+
const id = preferred.find((candidate) => sidebarTabIds.includes(candidate))
|
|
38
|
+
if (!id) return null
|
|
39
|
+
showSidebarPanel(id)
|
|
40
|
+
return id
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The rail tab on screen: the panel picked in this workspace when the rail has
|
|
45
|
+
* it, else the first tab with a panel. `activeSidebarPanel` follows what shows,
|
|
46
|
+
* so every reader agrees with the screen, while the pick (`sidebarPanels`)
|
|
47
|
+
* stays: a plugin's panel joins the rail only once the scene's plugins load,
|
|
48
|
+
* so a panel restored on reload is missing for a moment and comes back then.
|
|
49
|
+
*/
|
|
50
|
+
export function useShownSidebarPanel(tabs: readonly SidebarTab[]): string {
|
|
51
|
+
const activePanel = useEditor((s) => s.activeSidebarPanel)
|
|
52
|
+
const pickedPanel = useEditor((s) => s.sidebarPanels[s.workspaceMode] ?? s.activeSidebarPanel)
|
|
53
|
+
const shownPanel = tabs.some((tab) => tab.id === pickedPanel)
|
|
54
|
+
? pickedPanel
|
|
55
|
+
: ((tabs.find((tab) => !tab.noPanel) ?? tabs[0])?.id ?? pickedPanel)
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (shownPanel !== activePanel) useEditor.setState({ activeSidebarPanel: shownPanel })
|
|
59
|
+
}, [shownPanel, activePanel])
|
|
60
|
+
|
|
61
|
+
return shownPanel
|
|
62
|
+
}
|
|
@@ -9,7 +9,7 @@ import { siteBoundaryHandlesEnabled } from './site-boundary'
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const MODES: Mode[] = ['select', 'edit', 'delete', 'build', 'material-paint', 'terrain-sculpt']
|
|
12
|
-
const PHASES: Phase[] = ['site', '
|
|
12
|
+
const PHASES: Phase[] = ['site', 'building']
|
|
13
13
|
|
|
14
14
|
describe('siteBoundaryHandlesEnabled', () => {
|
|
15
15
|
test('sculpt hides the handles in every phase', () => {
|
|
@@ -29,15 +29,13 @@ describe('siteBoundaryHandlesEnabled', () => {
|
|
|
29
29
|
|
|
30
30
|
test('select mode offers them outside the site phase', () => {
|
|
31
31
|
// The flags double as the affordance that *enters* site editing, so select mode
|
|
32
|
-
// has to show them from
|
|
33
|
-
expect(siteBoundaryHandlesEnabled({ mode: 'select', phase: '
|
|
34
|
-
expect(siteBoundaryHandlesEnabled({ mode: 'select', phase: 'furnish' })).toBe(true)
|
|
32
|
+
// has to show them from the building or the lot becomes unreachable.
|
|
33
|
+
expect(siteBoundaryHandlesEnabled({ mode: 'select', phase: 'building' })).toBe(true)
|
|
35
34
|
})
|
|
36
35
|
|
|
37
36
|
test('a drafting or painting mode outside the site phase hides them', () => {
|
|
38
37
|
for (const mode of ['edit', 'delete', 'build', 'material-paint'] as Mode[]) {
|
|
39
|
-
expect(siteBoundaryHandlesEnabled({ mode, phase: '
|
|
40
|
-
expect(siteBoundaryHandlesEnabled({ mode, phase: 'furnish' })).toBe(false)
|
|
38
|
+
expect(siteBoundaryHandlesEnabled({ mode, phase: 'building' })).toBe(false)
|
|
41
39
|
}
|
|
42
40
|
})
|
|
43
41
|
|
package/src/lib/site-boundary.ts
CHANGED
|
@@ -11,7 +11,7 @@ export const SITE_BOUNDARY_DRAG_LABEL = 'site-boundary'
|
|
|
11
11
|
* clause was added to only one of them.
|
|
12
12
|
*
|
|
13
13
|
* The disagreement was reachable in exactly one configuration, which is why it was
|
|
14
|
-
* easy to miss: arming sculpt from the 2D view
|
|
14
|
+
* easy to miss: arming sculpt from the 2D view opens 3D beside the plan *and*
|
|
15
15
|
* the phase to `site`, so the floorplan's `phase === 'site'` term went true at the
|
|
16
16
|
* same moment the 3D editor's `!sculpting` term went false — handles gone in one
|
|
17
17
|
* pane, live in the other. Grabbing one then calls `begin({kind: 'handle-drag'})`,
|
|
@@ -130,6 +130,10 @@ describe('snapContextOf (profile-driven, node-declared)', () => {
|
|
|
130
130
|
tool: string | null = null,
|
|
131
131
|
) => snapContextOf({ scope, mode, tool, profileOf, profileOfNode })
|
|
132
132
|
|
|
133
|
+
it('dividing a room draws like a wall draft (wall, with angles)', () => {
|
|
134
|
+
expect(ctx({ kind: 'room-divide' })).toBe('wall')
|
|
135
|
+
})
|
|
136
|
+
|
|
133
137
|
it('translating a whole structural node has no angle (polygon, not wall)', () => {
|
|
134
138
|
expect(ctx({ kind: 'moving', nodeType: 'wall' })).toBe('polygon')
|
|
135
139
|
expect(ctx({ kind: 'moving', nodeType: 'slab' })).toBe('polygon')
|
package/src/lib/snapping-mode.ts
CHANGED
|
@@ -161,8 +161,12 @@ export function snapContextOf(args: {
|
|
|
161
161
|
// like `profileOf` so `snapping-mode` need not import the registry; defaults
|
|
162
162
|
// to `true` (the structural draw default) when not supplied.
|
|
163
163
|
draftDirectionalOf?: (typeOrTool: string) => boolean
|
|
164
|
+
// The paint tool's region sub-mode draws lines and boxes on a surface: the
|
|
165
|
+
// no-angle 'polygon' set (grid / lines / off), idle or mid-gesture.
|
|
166
|
+
paintRegion?: boolean
|
|
164
167
|
}): SnapContext | null {
|
|
165
|
-
const { scope, mode, tool, profileOf, profileOfNode, draftDirectionalOf } = args
|
|
168
|
+
const { scope, mode, tool, profileOf, profileOfNode, draftDirectionalOf, paintRegion } = args
|
|
169
|
+
if (paintRegion && mode === 'material-paint') return 'polygon'
|
|
166
170
|
// The group-move gizmo translates the whole selection — same no-angle
|
|
167
171
|
// treatment as a single-node move, so Shift cycles the 'item' modes and the
|
|
168
172
|
// HUD shows the item snapping chips for the drag.
|
|
@@ -170,6 +174,9 @@ export function snapContextOf(args: {
|
|
|
170
174
|
return 'item'
|
|
171
175
|
}
|
|
172
176
|
switch (scope.kind) {
|
|
177
|
+
// Dividing a room draws a segment like a wall draft: same modes, same keys.
|
|
178
|
+
case 'room-divide':
|
|
179
|
+
return 'wall'
|
|
173
180
|
case 'mesh-editing':
|
|
174
181
|
return scope.nodeId
|
|
175
182
|
? contextForProfile(profileOfNode?.(scope.nodeId), scope.operator === 'rotate')
|
|
@@ -37,6 +37,31 @@ describe('stair level helpers', () => {
|
|
|
37
37
|
expect(plan?.createdLevel?.parentId).toBe(building.id)
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
+
// Every generated one-storey house has a Roof level: the stair tool sent its flight up into it.
|
|
41
|
+
test('climbs to a new storey under the roof level, which moves up over it', () => {
|
|
42
|
+
const ground = LevelNode.parse({ level: 0, children: [] })
|
|
43
|
+
const roofLevel = LevelNode.parse({ level: 1, children: [], metadata: { role: 'roof' } })
|
|
44
|
+
const building = BuildingNode.parse({ children: [ground.id, roofLevel.id] })
|
|
45
|
+
const nodes = {
|
|
46
|
+
[building.id]: building,
|
|
47
|
+
[ground.id]: { ...ground, parentId: building.id },
|
|
48
|
+
[roofLevel.id]: { ...roofLevel, parentId: building.id },
|
|
49
|
+
} as Record<AnyNodeId, AnyNode>
|
|
50
|
+
|
|
51
|
+
const plan = resolveStairDestinationLevel({
|
|
52
|
+
createMissing: true,
|
|
53
|
+
fromLevelId: ground.id,
|
|
54
|
+
nodes,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
expect(plan?.toLevel.id).toBe(plan?.createdLevel?.id)
|
|
58
|
+
expect(plan?.createdLevel?.level).toBe(1)
|
|
59
|
+
expect(plan?.liftedLevels).toEqual([{ id: roofLevel.id, data: { level: 2 } }])
|
|
60
|
+
expect(resolveStairDestinationLevel({ fromLevelId: ground.id, nodes })?.toLevel.id).toBe(
|
|
61
|
+
ground.id,
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
|
|
40
65
|
test('uses the nearest higher sibling level instead of creating one', () => {
|
|
41
66
|
const building = BuildingNode.parse({})
|
|
42
67
|
const ground = LevelNode.parse({ level: 0, parentId: building.id })
|