@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,234 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
area,
|
|
4
|
+
getScaledDimensions,
|
|
5
|
+
getWallCurveLength,
|
|
6
|
+
getWallEffectiveHeightForNodes,
|
|
7
|
+
getWallThickness,
|
|
8
|
+
type ItemNode,
|
|
9
|
+
resolveLevelId,
|
|
10
|
+
type SlabNode,
|
|
11
|
+
type StairNode,
|
|
12
|
+
type WallNode,
|
|
13
|
+
type WindowNode,
|
|
14
|
+
type ZoneNode,
|
|
15
|
+
} from '@pascal-app/core'
|
|
16
|
+
import {
|
|
17
|
+
formatLinearMeasurement,
|
|
18
|
+
getAreaUnitLabel,
|
|
19
|
+
type LinearUnit,
|
|
20
|
+
type MetricNotation,
|
|
21
|
+
squareMetersToAreaUnit,
|
|
22
|
+
} from '../measurements'
|
|
23
|
+
import type { PointKind, PointTarget } from './types'
|
|
24
|
+
|
|
25
|
+
// Point and ask names what the finger is on (the owner, 8 October): the label chip and the
|
|
26
|
+
// bubble's context row read it in the viewer's unit; the agent reads the same element's `sizes` in
|
|
27
|
+
// metres. Pure: the scene is handed in.
|
|
28
|
+
|
|
29
|
+
export type PointUnit = { unit: LinearUnit; metricNotation?: MetricNotation }
|
|
30
|
+
|
|
31
|
+
/** A described element: the chip's fields, plus what the agent's context carries. */
|
|
32
|
+
export type DescribedTarget = PointTarget & {
|
|
33
|
+
levelId: string
|
|
34
|
+
/** The room it is in or bounds. */
|
|
35
|
+
zoneId?: string
|
|
36
|
+
/** The wall an opening is in, the item an item stands on. */
|
|
37
|
+
parentId?: string
|
|
38
|
+
/** Metres: wall {length, height, thickness}; zone {area, width, depth}; window/door {width,
|
|
39
|
+
* height, sill}; item {width, depth, height}. */
|
|
40
|
+
sizes: Record<string, number>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type Nodes = Readonly<Record<string, AnyNode | undefined>>
|
|
44
|
+
|
|
45
|
+
const KINDS: Partial<Record<string, PointKind>> = {
|
|
46
|
+
zone: 'room',
|
|
47
|
+
wall: 'wall',
|
|
48
|
+
window: 'window',
|
|
49
|
+
door: 'door',
|
|
50
|
+
item: 'item',
|
|
51
|
+
roof: 'roof',
|
|
52
|
+
'roof-segment': 'roof',
|
|
53
|
+
stair: 'stair',
|
|
54
|
+
'stair-segment': 'stair',
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const DEFAULT_NAMES: Partial<Record<string, string>> = {
|
|
58
|
+
slab: 'Floor',
|
|
59
|
+
ceiling: 'Ceiling',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const sentence = (type: string) => {
|
|
63
|
+
const words = type.replace(/-/g, ' ')
|
|
64
|
+
return words.charAt(0).toUpperCase() + words.slice(1)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const metres = (value: number, unit: PointUnit) => {
|
|
68
|
+
if (unit.unit === 'imperial') return formatLinearMeasurement(value, 'imperial')
|
|
69
|
+
return unit.metricNotation === 'millimeters' ? `${Math.round(value * 1000)}` : value.toFixed(2)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** "4.80 × 2.70 m", "3'11" × 4'7"", "1200 × 1400 mm". */
|
|
73
|
+
function dimensions(values: number[], unit: PointUnit): string {
|
|
74
|
+
const parts = values.map((value) => metres(value, unit)).join(' × ')
|
|
75
|
+
if (unit.unit === 'imperial') return parts
|
|
76
|
+
return `${parts} ${unit.metricNotation === 'millimeters' ? 'mm' : 'm'}`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** "2.10 m wide". */
|
|
80
|
+
function wide(value: number, unit: PointUnit): string {
|
|
81
|
+
return `${dimensions([value], unit)} wide`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function areaLabel(squareMetres: number, unit: PointUnit): string {
|
|
85
|
+
return `${squareMetersToAreaUnit(squareMetres, unit.unit).toFixed(1)} ${getAreaUnitLabel(unit.unit)}`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function extent(points: readonly (readonly number[])[], axis: 0 | 1): number {
|
|
89
|
+
const values = points.map((point) => point[axis]!)
|
|
90
|
+
return values.length ? Math.max(...values) - Math.min(...values) : 0
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function insidePolygon(point: readonly [number, number], polygon: readonly (readonly number[])[]) {
|
|
94
|
+
let inside = false
|
|
95
|
+
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
|
96
|
+
const [xi, zi] = [polygon[i]![0]!, polygon[i]![1]!]
|
|
97
|
+
const [xj, zj] = [polygon[j]![0]!, polygon[j]![1]!]
|
|
98
|
+
if (
|
|
99
|
+
zi > point[1] !== zj > point[1] &&
|
|
100
|
+
point[0] < ((xj - xi) * (point[1] - zi)) / (zj - zi) + xi
|
|
101
|
+
)
|
|
102
|
+
inside = !inside
|
|
103
|
+
}
|
|
104
|
+
return inside
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const zonesOf = (nodes: Nodes, levelId: string) =>
|
|
108
|
+
Object.values(nodes).filter(
|
|
109
|
+
(node): node is ZoneNode => node?.type === 'zone' && node.parentId === levelId,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
/** The room a wall bounds: the zone that lists it among its boundary walls. */
|
|
113
|
+
const roomOfWall = (nodes: Nodes, levelId: string, wallId: string) =>
|
|
114
|
+
zonesOf(nodes, levelId).find((zone) => zone.boundaryWallIds?.includes(wallId as never))
|
|
115
|
+
|
|
116
|
+
function wallHeight(wall: WallNode, nodes: Nodes): number {
|
|
117
|
+
try {
|
|
118
|
+
const height = getWallEffectiveHeightForNodes(wall, nodes as Record<string, AnyNode>)
|
|
119
|
+
if (Number.isFinite(height) && height > 0) return height
|
|
120
|
+
} catch {
|
|
121
|
+
// No slab election to consult in a bare scene: the wall's own height stands.
|
|
122
|
+
}
|
|
123
|
+
return wall.height ?? 2.5
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function describeTarget(node: AnyNode, nodes: Nodes, unit: PointUnit): DescribedTarget {
|
|
127
|
+
const levelId = resolveLevelId(node, nodes as Record<string, AnyNode>)
|
|
128
|
+
const named = (fallback: string) => (node as { name?: string }).name?.trim() || fallback
|
|
129
|
+
const base = {
|
|
130
|
+
id: node.id,
|
|
131
|
+
type: node.type,
|
|
132
|
+
kind: KINDS[node.type] ?? ('other' as PointKind),
|
|
133
|
+
levelId,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (node.type === 'zone') {
|
|
137
|
+
const zone = node as ZoneNode
|
|
138
|
+
const squareMetres = area([{ outer: zone.polygon, holes: zone.holes }])
|
|
139
|
+
return {
|
|
140
|
+
...base,
|
|
141
|
+
name: named('Room'),
|
|
142
|
+
size: areaLabel(squareMetres, unit),
|
|
143
|
+
sizes: {
|
|
144
|
+
area: squareMetres,
|
|
145
|
+
width: extent(zone.polygon, 0),
|
|
146
|
+
depth: extent(zone.polygon, 1),
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (node.type === 'wall') {
|
|
152
|
+
const wall = node as WallNode
|
|
153
|
+
const room = roomOfWall(nodes, levelId, wall.id)
|
|
154
|
+
const length = getWallCurveLength(wall)
|
|
155
|
+
const height = wallHeight(wall, nodes)
|
|
156
|
+
return {
|
|
157
|
+
...base,
|
|
158
|
+
name: named(room ? `Wall · ${room.name?.trim() || 'Room'}` : 'Wall'),
|
|
159
|
+
size: dimensions([length, height], unit),
|
|
160
|
+
sizes: { length, height, thickness: getWallThickness(wall) },
|
|
161
|
+
...(room ? { zoneId: room.id } : {}),
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (node.type === 'window' || node.type === 'door') {
|
|
166
|
+
const opening = node as WindowNode & { wallId?: string }
|
|
167
|
+
const wallId = opening.wallId ?? opening.parentId ?? undefined
|
|
168
|
+
const room = wallId ? roomOfWall(nodes, levelId, wallId) : undefined
|
|
169
|
+
return {
|
|
170
|
+
...base,
|
|
171
|
+
name: named(node.type === 'window' ? 'Window' : 'Door'),
|
|
172
|
+
size: dimensions([opening.width, opening.height], unit),
|
|
173
|
+
sizes: {
|
|
174
|
+
width: opening.width,
|
|
175
|
+
height: opening.height,
|
|
176
|
+
sill: Math.max(0, opening.position[1] - opening.height / 2),
|
|
177
|
+
},
|
|
178
|
+
...(opening.parentId ? { parentId: opening.parentId } : {}),
|
|
179
|
+
...(room ? { zoneId: room.id } : {}),
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (node.type === 'item') {
|
|
184
|
+
const item = node as ItemNode
|
|
185
|
+
const [width, height, depth] = getScaledDimensions(item)
|
|
186
|
+
const room = zonesOf(nodes, levelId).find((zone) =>
|
|
187
|
+
insidePolygon([item.position[0], item.position[2]], zone.polygon),
|
|
188
|
+
)
|
|
189
|
+
const onItem = item.parentId && nodes[item.parentId]?.type === 'item'
|
|
190
|
+
return {
|
|
191
|
+
...base,
|
|
192
|
+
name: named(item.asset.name?.trim() || 'Item'),
|
|
193
|
+
size: wide(width, unit),
|
|
194
|
+
sizes: { width, depth, height },
|
|
195
|
+
...(onItem ? { parentId: item.parentId as string } : {}),
|
|
196
|
+
...(room ? { zoneId: room.id } : {}),
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (node.type === 'roof-segment') {
|
|
201
|
+
const { width, depth } = node as { width: number; depth: number }
|
|
202
|
+
return {
|
|
203
|
+
...base,
|
|
204
|
+
name: named('Roof segment'),
|
|
205
|
+
size: dimensions([width, depth], unit),
|
|
206
|
+
sizes: { width, depth },
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (node.type === 'stair' || node.type === 'stair-segment') {
|
|
211
|
+
const stair = node as StairNode
|
|
212
|
+
const sizes: Record<string, number> = { width: stair.width ?? 1 }
|
|
213
|
+
if (stair.totalRise !== undefined) sizes.rise = stair.totalRise
|
|
214
|
+
return { ...base, name: named('Stair'), size: wide(sizes.width!, unit), sizes }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (node.type === 'slab' || node.type === 'ceiling') {
|
|
218
|
+
const plate = node as SlabNode
|
|
219
|
+
const squareMetres = area([{ outer: plate.polygon, holes: [] }])
|
|
220
|
+
return {
|
|
221
|
+
...base,
|
|
222
|
+
name: named(DEFAULT_NAMES[node.type] ?? sentence(node.type)),
|
|
223
|
+
size: areaLabel(squareMetres, unit),
|
|
224
|
+
sizes: { area: squareMetres },
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
...base,
|
|
230
|
+
name: named(DEFAULT_NAMES[node.type] ?? sentence(node.type)),
|
|
231
|
+
size: '',
|
|
232
|
+
sizes: {},
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Dictate in the bubble (the owner said yes to voice, 8 October): the browser's own speech
|
|
2
|
+
// recognition where it exists, hidden where it does not. The words fill the field and never send:
|
|
3
|
+
// the person reads them and presses Enter.
|
|
4
|
+
|
|
5
|
+
type RecognitionResult = { 0: { transcript: string }; isFinal: boolean; length: number }
|
|
6
|
+
type RecognitionEvent = { resultIndex: number; results: ArrayLike<RecognitionResult> }
|
|
7
|
+
type Recognition = {
|
|
8
|
+
lang: string
|
|
9
|
+
interimResults: boolean
|
|
10
|
+
continuous: boolean
|
|
11
|
+
onresult: ((event: RecognitionEvent) => void) | null
|
|
12
|
+
onend: (() => void) | null
|
|
13
|
+
onerror: ((event: { error: string }) => void) | null
|
|
14
|
+
start: () => void
|
|
15
|
+
stop: () => void
|
|
16
|
+
abort: () => void
|
|
17
|
+
}
|
|
18
|
+
type RecognitionConstructor = new () => Recognition
|
|
19
|
+
|
|
20
|
+
function constructorOf(): RecognitionConstructor | null {
|
|
21
|
+
if (typeof window === 'undefined') return null
|
|
22
|
+
const w = window as unknown as {
|
|
23
|
+
SpeechRecognition?: RecognitionConstructor
|
|
24
|
+
webkitSpeechRecognition?: RecognitionConstructor
|
|
25
|
+
}
|
|
26
|
+
return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isDictationSupported(): boolean {
|
|
30
|
+
return constructorOf() !== null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type Dictation = { stop: () => void }
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Starts listening. `onText` gets the whole transcript so far (interim words included) each time
|
|
37
|
+
* it changes; `onEnd` fires once when listening stops, by `stop`, by silence or by an error.
|
|
38
|
+
*/
|
|
39
|
+
export function startDictation(options: {
|
|
40
|
+
lang?: string
|
|
41
|
+
onText: (text: string) => void
|
|
42
|
+
onEnd: (reason: 'stopped' | 'error', error?: string) => void
|
|
43
|
+
}): Dictation | null {
|
|
44
|
+
const Ctor = constructorOf()
|
|
45
|
+
if (!Ctor) return null
|
|
46
|
+
const recognition = new Ctor()
|
|
47
|
+
recognition.lang =
|
|
48
|
+
options.lang ?? (typeof navigator === 'undefined' ? 'en-US' : navigator.language)
|
|
49
|
+
recognition.interimResults = true
|
|
50
|
+
recognition.continuous = true
|
|
51
|
+
let finished = false
|
|
52
|
+
const finish = (reason: 'stopped' | 'error', error?: string) => {
|
|
53
|
+
if (finished) return
|
|
54
|
+
finished = true
|
|
55
|
+
options.onEnd(reason, error)
|
|
56
|
+
}
|
|
57
|
+
recognition.onresult = (event) => {
|
|
58
|
+
let text = ''
|
|
59
|
+
for (let i = 0; i < event.results.length; i++) text += event.results[i]?.[0]?.transcript ?? ''
|
|
60
|
+
options.onText(text.trim())
|
|
61
|
+
}
|
|
62
|
+
recognition.onerror = (event) => finish('error', event.error)
|
|
63
|
+
recognition.onend = () => finish('stopped')
|
|
64
|
+
try {
|
|
65
|
+
recognition.start()
|
|
66
|
+
} catch (error) {
|
|
67
|
+
finish('error', error instanceof Error ? error.message : String(error))
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
stop: () => {
|
|
72
|
+
try {
|
|
73
|
+
recognition.stop()
|
|
74
|
+
} catch {
|
|
75
|
+
finish('stopped')
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { edgeMarker } from './edge-marker'
|
|
3
|
+
|
|
4
|
+
// A pin whose element is out of view (spec 2.6, "work outside the view shows an edge marker"):
|
|
5
|
+
// the marker sits on the viewport's edge, on the line from its centre towards the pin, and points
|
|
6
|
+
// at it. Written first: a marker for a pin that is in view; one off the edge on the wrong side;
|
|
7
|
+
// a pin behind the camera sending the marker nowhere.
|
|
8
|
+
const VIEW = { x0: 0, y0: 0, x1: 1000, y1: 600 }
|
|
9
|
+
|
|
10
|
+
describe('edgeMarker', () => {
|
|
11
|
+
test('a pin in view needs none', () => {
|
|
12
|
+
expect(edgeMarker({ x: 400, y: 300 }, VIEW)).toBeNull()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('a pin to the right sits on the right edge, level with the line to it', () => {
|
|
16
|
+
const marker = edgeMarker({ x: 2000, y: 300 }, VIEW)!
|
|
17
|
+
|
|
18
|
+
expect(marker.x).toBe(1000 - 18)
|
|
19
|
+
expect(marker.y).toBeCloseTo(300, 5)
|
|
20
|
+
expect(marker.angle).toBeCloseTo(0, 5)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('a pin above points up from the top edge', () => {
|
|
24
|
+
const marker = edgeMarker({ x: 500, y: -900 }, VIEW)!
|
|
25
|
+
|
|
26
|
+
expect(marker.y).toBe(18)
|
|
27
|
+
expect(marker.x).toBeCloseTo(500, 5)
|
|
28
|
+
expect(marker.angle).toBeCloseTo(-90, 5)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('a pin beyond a corner sits in that corner region, on the line from the centre', () => {
|
|
32
|
+
const marker = edgeMarker({ x: 3000, y: 1800 }, VIEW)!
|
|
33
|
+
|
|
34
|
+
expect(marker.x).toBeLessThanOrEqual(1000 - 18)
|
|
35
|
+
expect(marker.y).toBeLessThanOrEqual(600 - 18)
|
|
36
|
+
// On the line through the centre (500, 300) and the pin: equal slope.
|
|
37
|
+
const slope = (marker.y - 300) / (marker.x - 500)
|
|
38
|
+
expect(slope).toBeCloseTo((1800 - 300) / (3000 - 500), 5)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('is always inside the inset viewport', () => {
|
|
42
|
+
for (const p of [
|
|
43
|
+
{ x: -500, y: 1200 },
|
|
44
|
+
{ x: 1500, y: -300 },
|
|
45
|
+
{ x: 2000, y: 640 },
|
|
46
|
+
{ x: -50, y: 300 },
|
|
47
|
+
]) {
|
|
48
|
+
const marker = edgeMarker(p, VIEW)!
|
|
49
|
+
expect(marker.x).toBeGreaterThanOrEqual(18)
|
|
50
|
+
expect(marker.x).toBeLessThanOrEqual(1000 - 18)
|
|
51
|
+
expect(marker.y).toBeGreaterThanOrEqual(18)
|
|
52
|
+
expect(marker.y).toBeLessThanOrEqual(600 - 18)
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('a pin behind the camera has no direction: the marker sits at the bottom, pointing down', () => {
|
|
57
|
+
const marker = edgeMarker(null, VIEW)!
|
|
58
|
+
|
|
59
|
+
expect(marker).toEqual({ x: 500, y: 600 - 18, angle: 90 })
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Rect, ScreenPoint } from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Where the edge marker of an off-screen pin goes: on the viewport's inset edge, on the line from
|
|
5
|
+
* the viewport's centre towards the pin, turned to point at it. Null when the pin is in view; a
|
|
6
|
+
* pin behind the camera (`point` null) has no direction, so its marker sits at the bottom.
|
|
7
|
+
*/
|
|
8
|
+
export function edgeMarker(
|
|
9
|
+
point: ScreenPoint | null,
|
|
10
|
+
viewport: Rect,
|
|
11
|
+
inset = 18,
|
|
12
|
+
): { x: number; y: number; angle: number } | null {
|
|
13
|
+
const cx = (viewport.x0 + viewport.x1) / 2
|
|
14
|
+
const cy = (viewport.y0 + viewport.y1) / 2
|
|
15
|
+
const hw = (viewport.x1 - viewport.x0) / 2 - inset
|
|
16
|
+
const hh = (viewport.y1 - viewport.y0) / 2 - inset
|
|
17
|
+
if (!point) return { x: cx, y: cy + hh, angle: 90 }
|
|
18
|
+
const dx = point.x - cx
|
|
19
|
+
const dy = point.y - cy
|
|
20
|
+
if (Math.abs(dx) <= hw + inset && Math.abs(dy) <= hh + inset) return null
|
|
21
|
+
// Scale the direction until it touches the inset rectangle.
|
|
22
|
+
const scale = Math.min(
|
|
23
|
+
dx === 0 ? Number.POSITIVE_INFINITY : hw / Math.abs(dx),
|
|
24
|
+
dy === 0 ? Number.POSITIVE_INFINITY : hh / Math.abs(dy),
|
|
25
|
+
)
|
|
26
|
+
return { x: cx + dx * scale, y: cy + dy * scale, angle: (Math.atan2(dy, dx) * 180) / Math.PI }
|
|
27
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// One requestAnimationFrame loop for everything that follows the camera (the outline corners, the
|
|
2
|
+
// bubble, the pins): it runs only while something subscribes, and each subscriber gets the frame's
|
|
3
|
+
// delta in seconds (capped, so a hidden tab does not fling springs on return).
|
|
4
|
+
|
|
5
|
+
type FrameFn = (dt: number, now: number) => void
|
|
6
|
+
|
|
7
|
+
const subscribers = new Set<FrameFn>()
|
|
8
|
+
let frame = 0
|
|
9
|
+
let last = 0
|
|
10
|
+
|
|
11
|
+
function tick(now: number) {
|
|
12
|
+
const dt = Math.min(0.05, last ? (now - last) / 1000 : 0.016)
|
|
13
|
+
last = now
|
|
14
|
+
for (const fn of [...subscribers]) fn(dt, now)
|
|
15
|
+
frame = subscribers.size > 0 ? requestAnimationFrame(tick) : 0
|
|
16
|
+
if (!frame) last = 0
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function subscribeFrame(fn: FrameFn): () => void {
|
|
20
|
+
subscribers.add(fn)
|
|
21
|
+
if (!frame && typeof requestAnimationFrame === 'function') frame = requestAnimationFrame(tick)
|
|
22
|
+
return () => {
|
|
23
|
+
subscribers.delete(fn)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { createdSince, markHistory, stateAfterMark, stepsSince } from './history-marks'
|
|
3
|
+
|
|
4
|
+
// Undo from a pin steps back through what the ask's turn wrote. The scene history keeps only the
|
|
5
|
+
// last 50 steps and a Cmd+Z shrinks it, so a length alone lies: the mark holds the last step that
|
|
6
|
+
// stood when the turn began, and the steps since are counted from where that step is now.
|
|
7
|
+
const steps = (n: number, from = 0) => Array.from({ length: n }, (_, i) => ({ id: from + i }))
|
|
8
|
+
|
|
9
|
+
describe('stepsSince', () => {
|
|
10
|
+
test('counts the steps pushed after the mark', () => {
|
|
11
|
+
const past = steps(3)
|
|
12
|
+
const mark = markHistory(past)
|
|
13
|
+
const later = [...past, ...steps(2, 3)]
|
|
14
|
+
|
|
15
|
+
expect(stepsSince(later, mark)).toBe(2)
|
|
16
|
+
expect(stepsSince(past, mark)).toBe(0)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('an empty history at the mark counts everything since', () => {
|
|
20
|
+
const mark = markHistory([])
|
|
21
|
+
|
|
22
|
+
expect(stepsSince(steps(4), mark)).toBe(4)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('still counts once old steps fall off the front (the history limit)', () => {
|
|
26
|
+
const past = steps(50)
|
|
27
|
+
const mark = markHistory(past)
|
|
28
|
+
const later = [...past, ...steps(3, 50)].slice(3)
|
|
29
|
+
|
|
30
|
+
expect(stepsSince(later, mark)).toBe(3)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('is null when the marked step itself fell off, so the whole turn cannot be undone', () => {
|
|
34
|
+
const past = steps(50)
|
|
35
|
+
const mark = markHistory(past)
|
|
36
|
+
const later = [...past, ...steps(60, 50)].slice(-50)
|
|
37
|
+
|
|
38
|
+
expect(stepsSince(later, mark)).toBeNull()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('is null after the person undid back past the mark', () => {
|
|
42
|
+
const past = steps(5)
|
|
43
|
+
const mark = markHistory(past)
|
|
44
|
+
|
|
45
|
+
expect(stepsSince(past.slice(0, 2), mark)).toBeNull()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('a Cmd+Z that only took part of the turn leaves the rest countable', () => {
|
|
49
|
+
const past = steps(3)
|
|
50
|
+
const mark = markHistory(past)
|
|
51
|
+
const written = [...past, ...steps(4, 3)]
|
|
52
|
+
const afterOneUndo = written.slice(0, -1)
|
|
53
|
+
|
|
54
|
+
expect(stepsSince(afterOneUndo, mark)).toBe(3)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('stateAfterMark', () => {
|
|
59
|
+
test('is the state the first step since the mark was taken from', () => {
|
|
60
|
+
const past = steps(3)
|
|
61
|
+
const mark = markHistory(past)
|
|
62
|
+
const later = [...past, ...steps(2, 3)]
|
|
63
|
+
|
|
64
|
+
expect(stateAfterMark(later, mark)).toBe(later[3] ?? null)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('with an empty history at the mark it is the oldest state', () => {
|
|
68
|
+
const mark = markHistory([])
|
|
69
|
+
const later = steps(2)
|
|
70
|
+
|
|
71
|
+
expect(stateAfterMark(later, mark)).toBe(later[0] ?? null)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('is null when nothing was pushed since the mark, or the marked step is gone', () => {
|
|
75
|
+
const past = steps(3)
|
|
76
|
+
const mark = markHistory(past)
|
|
77
|
+
|
|
78
|
+
expect(stateAfterMark(past, mark)).toBeNull()
|
|
79
|
+
expect(stateAfterMark(steps(2, 10), mark)).toBeNull()
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('createdSince', () => {
|
|
84
|
+
test('is the nodes the scene has now that the earlier state did not', () => {
|
|
85
|
+
const before = { wall_a: {}, window_a: {} }
|
|
86
|
+
const now = { wall_a: {}, window_a: {}, window_b: {}, wall_b: {} }
|
|
87
|
+
|
|
88
|
+
expect(createdSince(before, now).sort()).toEqual(['wall_b', 'window_b'])
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('a node only changed is not created, and one deleted is not either', () => {
|
|
92
|
+
expect(createdSince({ a: {}, b: {} }, { a: { width: 2 } })).toEqual([])
|
|
93
|
+
})
|
|
94
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Counting what an ask's turn wrote in the scene's undo history. The history is bounded (the last
|
|
2
|
+
// 50 steps) and the person can Cmd+Z into it, so a length cannot say how many steps are the turn's:
|
|
3
|
+
// a mark holds the last step standing when the turn began, and the steps since are counted from
|
|
4
|
+
// where that step is now.
|
|
5
|
+
|
|
6
|
+
export type HistoryMark = { ref: unknown | null; length: number }
|
|
7
|
+
|
|
8
|
+
export function markHistory(past: readonly unknown[]): HistoryMark {
|
|
9
|
+
return { ref: past.at(-1) ?? null, length: past.length }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* How many steps were pushed since `mark`, or null when the marked step is gone from the history
|
|
14
|
+
* (it fell off the front, or the person undid back past it): the turn can no longer be undone as a
|
|
15
|
+
* whole.
|
|
16
|
+
*/
|
|
17
|
+
export function stepsSince(past: readonly unknown[], mark: HistoryMark): number | null {
|
|
18
|
+
if (mark.ref === null) return past.length
|
|
19
|
+
const at = past.lastIndexOf(mark.ref)
|
|
20
|
+
return at < 0 ? null : past.length - 1 - at
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The history state the turn's first step was taken from: the scene as it stood when the turn began.
|
|
25
|
+
* Null when nothing was pushed since the mark, or the marked step has gone from the history.
|
|
26
|
+
*/
|
|
27
|
+
export function stateAfterMark<T>(past: readonly T[], mark: HistoryMark): T | null {
|
|
28
|
+
if (mark.ref === null) return past[0] ?? null
|
|
29
|
+
const at = past.lastIndexOf(mark.ref as T)
|
|
30
|
+
return at < 0 ? null : (past[at + 1] ?? null)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The nodes in `now` that `before` had no entry for: what an undo of the steps in between will remove. */
|
|
34
|
+
export function createdSince(
|
|
35
|
+
before: Record<string, unknown>,
|
|
36
|
+
now: Record<string, unknown>,
|
|
37
|
+
): string[] {
|
|
38
|
+
return Object.keys(now).filter((id) => !(id in before))
|
|
39
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { usePointAsk } from '../../store/use-point-ask'
|
|
2
|
+
import { chatWentIdle, retryAsk } from './session'
|
|
3
|
+
|
|
4
|
+
// What a host app (the hosted chat) tells Point and ask about itself, without importing the store.
|
|
5
|
+
|
|
6
|
+
/** The host's "Playful touches" setting: false turns the region flash, the ripples and the sounds off. */
|
|
7
|
+
export function setPointAskPlayful(playful: boolean) {
|
|
8
|
+
usePointAsk.getState().setPlayful(playful)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Pascal is on a turn: the bubble's Send says Queue, and a new ask waits its turn. */
|
|
12
|
+
export function setPointAskBusy(busy: boolean) {
|
|
13
|
+
usePointAsk.getState().setBusy(busy)
|
|
14
|
+
if (!busy) chatWentIdle()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Messages already wait in the chat's queue (or are held there after an error): a new ask goes
|
|
19
|
+
* behind them, and the bubble says Queue, though Pascal is on no turn.
|
|
20
|
+
*/
|
|
21
|
+
export function setPointAskBehindQueue(behindQueue: boolean) {
|
|
22
|
+
usePointAsk.getState().setBehindQueue(behindQueue)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The chat's Retry on a message that was a Point and ask: the same ask again, with its own pin. False
|
|
27
|
+
* when the editor no longer has the pin (a reload, a collaborator's ask): the chat sends the words
|
|
28
|
+
* again as a plain message.
|
|
29
|
+
*/
|
|
30
|
+
export function retryPointAsk(askId: string): Promise<boolean> {
|
|
31
|
+
return retryAsk(askId)
|
|
32
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { outlineBox } from './outline-box'
|
|
3
|
+
|
|
4
|
+
// A room is a floor to the person pointing: its corner brackets frame the footprint, not the
|
|
5
|
+
// storey-high box that would leave two of them floating in the sky (seen on a two-storey house).
|
|
6
|
+
describe('outlineBox', () => {
|
|
7
|
+
const tall = {
|
|
8
|
+
min: [0, 0, 0] as [number, number, number],
|
|
9
|
+
max: [6, 4.5, 6] as [number, number, number],
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
test('a room is its footprint at the floor', () => {
|
|
13
|
+
expect(outlineBox('zone', tall)).toEqual({ min: [0, 0, 0], max: [6, 0.02, 6] })
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('everything else keeps its own box', () => {
|
|
17
|
+
for (const type of ['wall', 'window', 'door', 'item', 'stair', 'roof']) {
|
|
18
|
+
expect(outlineBox(type, tall)).toEqual(tall)
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('a room that already is flat is left as it is', () => {
|
|
23
|
+
const flat = {
|
|
24
|
+
min: [0, 2.55, 0] as [number, number, number],
|
|
25
|
+
max: [4, 2.55, 4] as [number, number, number],
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
expect(outlineBox('zone', flat)).toEqual({ min: [0, 2.55, 0], max: [4, 2.57, 4] })
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { WorldBox } from '../../store/use-point-ask'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The box the overlay frames for a pointed element: its own world box, except a room, whose box
|
|
5
|
+
* spans the storey but which the person means as a floor. A flat footprint keeps the corner
|
|
6
|
+
* brackets on the ground and the bubble beside the room, not up in the air.
|
|
7
|
+
*/
|
|
8
|
+
export function outlineBox(type: string, box: WorldBox): WorldBox {
|
|
9
|
+
if (type !== 'zone') return box
|
|
10
|
+
return { min: [...box.min], max: [box.max[0], box.min[1] + 0.02, box.max[2]] }
|
|
11
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Where the editor floats its own chrome over the canvas, so a bubble keeps clear of it.
|
|
2
|
+
export const VIEWPORT_INSET = 12
|
|
3
|
+
/** The top bar and the camera controls strip. */
|
|
4
|
+
export const BUBBLE_TOP_INSET = 56
|
|
5
|
+
/** The action bar at the bottom centre. */
|
|
6
|
+
export const BUBBLE_BOTTOM_INSET = 84
|