@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,217 @@
|
|
|
1
|
+
import type { PinState } from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What the chat tells the pin about its ask's turn (the scene/chat boundary agreed with the chat
|
|
5
|
+
* side: queued, working with the live sentence, and one end with what happened).
|
|
6
|
+
*/
|
|
7
|
+
export type PinStatus =
|
|
8
|
+
| { askId: string; turnId?: string; status: 'queued' }
|
|
9
|
+
| { askId: string; turnId?: string; status: 'working'; sentence?: string }
|
|
10
|
+
/** The turn waits on a question to the person; `working` follows once it is answered. */
|
|
11
|
+
| { askId: string; turnId?: string; status: 'asking'; question: string }
|
|
12
|
+
| {
|
|
13
|
+
askId: string
|
|
14
|
+
turnId?: string
|
|
15
|
+
status: 'ended'
|
|
16
|
+
outcome: 'completed' | 'cancelled' | 'errored'
|
|
17
|
+
/** The turn ended on a question to the person. */
|
|
18
|
+
asked: boolean
|
|
19
|
+
/** The turn made at least one scene-writing tool call. */
|
|
20
|
+
wroteScene: boolean
|
|
21
|
+
answer?: string
|
|
22
|
+
error?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type PinEvent =
|
|
26
|
+
| { type: 'sent' }
|
|
27
|
+
| { type: 'status'; status: PinStatus }
|
|
28
|
+
/** A scene commit while the ask works; `isInTargets` supplies subtree or room membership. */
|
|
29
|
+
| {
|
|
30
|
+
type: 'commit'
|
|
31
|
+
changedNodeIds: readonly string[]
|
|
32
|
+
isInTargets?: (id: string) => boolean
|
|
33
|
+
/** A real semantic commit can write materials or collections without changing any node. */
|
|
34
|
+
committed?: boolean
|
|
35
|
+
}
|
|
36
|
+
| { type: 'undo' }
|
|
37
|
+
| { type: 'undone'; ok: boolean }
|
|
38
|
+
/** The person brought the undone ask back (Redo, while nothing else was done). */
|
|
39
|
+
| { type: 'redo' }
|
|
40
|
+
| { type: 'keep' }
|
|
41
|
+
|
|
42
|
+
export type PinModel = {
|
|
43
|
+
askId: string
|
|
44
|
+
/** The ask's words, for the pin's tooltip. */
|
|
45
|
+
text: string
|
|
46
|
+
targetIds: string[]
|
|
47
|
+
state: PinState
|
|
48
|
+
/** The live step words while it works. */
|
|
49
|
+
sentence?: string
|
|
50
|
+
/** What Pascal is waiting to be told, while the turn is paused on a question. */
|
|
51
|
+
question?: string
|
|
52
|
+
turnId?: string
|
|
53
|
+
/** The card's title for a done pin, built by the caller from the step sentences. */
|
|
54
|
+
title?: string
|
|
55
|
+
answer?: string
|
|
56
|
+
error?: string
|
|
57
|
+
/** Steps whose commits touched a target: each is a small pop on the pin. */
|
|
58
|
+
landed: number
|
|
59
|
+
changedTargetIds: string[]
|
|
60
|
+
alsoChangedIds: string[]
|
|
61
|
+
/** An observed durable scene change, not merely a scene-writing tool being called. */
|
|
62
|
+
committed: boolean
|
|
63
|
+
ended: boolean
|
|
64
|
+
undone: boolean
|
|
65
|
+
kept: boolean
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createPin(init: { askId: string; text: string; targetIds: string[] }): PinModel {
|
|
69
|
+
return {
|
|
70
|
+
askId: init.askId,
|
|
71
|
+
text: init.text,
|
|
72
|
+
targetIds: [...init.targetIds],
|
|
73
|
+
state: 'sent',
|
|
74
|
+
landed: 0,
|
|
75
|
+
changedTargetIds: [],
|
|
76
|
+
alsoChangedIds: [],
|
|
77
|
+
committed: false,
|
|
78
|
+
ended: false,
|
|
79
|
+
undone: false,
|
|
80
|
+
kept: false,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const union = (list: string[], more: readonly string[]) => {
|
|
85
|
+
const next = [...list]
|
|
86
|
+
for (const id of more) if (!next.includes(id)) next.push(id)
|
|
87
|
+
return next.length === list.length ? list : next
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const IN_FLIGHT: readonly PinState[] = ['sent', 'queued', 'working']
|
|
91
|
+
/** A turn that waits on a question is still open: it resumes, or ends still asking. */
|
|
92
|
+
const OPEN: readonly PinState[] = [...IN_FLIGHT, 'needs-input']
|
|
93
|
+
|
|
94
|
+
function reduceStatus(pin: PinModel, status: PinStatus): PinModel {
|
|
95
|
+
if (status.askId !== pin.askId || pin.ended) return pin
|
|
96
|
+
const turnId = status.turnId ?? pin.turnId
|
|
97
|
+
if (status.status === 'queued') {
|
|
98
|
+
if (pin.state !== 'sent' && pin.state !== 'queued') return pin
|
|
99
|
+
return { ...pin, state: 'queued', turnId }
|
|
100
|
+
}
|
|
101
|
+
if (status.status === 'asking') {
|
|
102
|
+
if (!OPEN.includes(pin.state)) return pin
|
|
103
|
+
return { ...pin, state: 'needs-input', turnId, question: status.question }
|
|
104
|
+
}
|
|
105
|
+
if (status.status === 'working') {
|
|
106
|
+
if (!OPEN.includes(pin.state)) return pin
|
|
107
|
+
const { question: _answered, ...rest } = pin
|
|
108
|
+
return { ...rest, state: 'working', turnId, sentence: status.sentence ?? pin.sentence }
|
|
109
|
+
}
|
|
110
|
+
if (!OPEN.includes(pin.state)) return pin
|
|
111
|
+
let state: PinState
|
|
112
|
+
if (status.outcome === 'errored') state = 'error'
|
|
113
|
+
else if (pin.committed) state = 'done'
|
|
114
|
+
else if (status.asked) state = 'needs-input'
|
|
115
|
+
else if (status.outcome === 'cancelled') state = 'no-change'
|
|
116
|
+
else if (!status.wroteScene && status.answer) state = 'answered'
|
|
117
|
+
else state = 'no-change'
|
|
118
|
+
return {
|
|
119
|
+
...pin,
|
|
120
|
+
state,
|
|
121
|
+
turnId,
|
|
122
|
+
ended: true,
|
|
123
|
+
sentence: undefined,
|
|
124
|
+
...(pin.sentence ? { title: pin.sentence } : {}),
|
|
125
|
+
...(status.answer === undefined ? {} : { answer: status.answer }),
|
|
126
|
+
...(status.error === undefined ? {} : { error: status.error }),
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The pin's state machine (spec 2.6-2.8): pure, so every transition is a test. */
|
|
131
|
+
export function reducePin(pin: PinModel, event: PinEvent): PinModel {
|
|
132
|
+
switch (event.type) {
|
|
133
|
+
case 'sent':
|
|
134
|
+
return pin
|
|
135
|
+
case 'status':
|
|
136
|
+
return reduceStatus(pin, event.status)
|
|
137
|
+
case 'commit': {
|
|
138
|
+
if (pin.state !== 'working' || (event.changedNodeIds.length === 0 && !event.committed))
|
|
139
|
+
return pin
|
|
140
|
+
const isTarget = (id: string) =>
|
|
141
|
+
pin.targetIds.includes(id) ||
|
|
142
|
+
pin.changedTargetIds.includes(id) ||
|
|
143
|
+
event.isInTargets?.(id) === true
|
|
144
|
+
const hits = event.changedNodeIds.filter(isTarget)
|
|
145
|
+
const others = event.changedNodeIds.filter((id) => !isTarget(id))
|
|
146
|
+
const changedTargetIds = union(pin.changedTargetIds, hits)
|
|
147
|
+
const also = union(pin.alsoChangedIds, others)
|
|
148
|
+
const alsoChangedIds = also.some((id) => changedTargetIds.includes(id))
|
|
149
|
+
? also.filter((id) => !changedTargetIds.includes(id))
|
|
150
|
+
: also
|
|
151
|
+
if (hits.length === 0 && alsoChangedIds === pin.alsoChangedIds && pin.committed) return pin
|
|
152
|
+
return {
|
|
153
|
+
...pin,
|
|
154
|
+
landed: hits.length > 0 ? pin.landed + 1 : pin.landed,
|
|
155
|
+
changedTargetIds,
|
|
156
|
+
alsoChangedIds,
|
|
157
|
+
committed: true,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
case 'undo':
|
|
161
|
+
return pin.state === 'done' ? { ...pin, state: 'undone', undone: true } : pin
|
|
162
|
+
case 'undone':
|
|
163
|
+
// A failed undo hands the pin back; a good one is already shown.
|
|
164
|
+
return !event.ok && pin.state === 'undone' && pin.undone
|
|
165
|
+
? { ...pin, state: 'done', undone: false }
|
|
166
|
+
: pin
|
|
167
|
+
case 'redo':
|
|
168
|
+
return pin.state === 'undone' && pin.undone ? { ...pin, state: 'done', undone: false } : pin
|
|
169
|
+
case 'keep':
|
|
170
|
+
return pin.kept ? pin : { ...pin, kept: true }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const firstLines = (text: string, count: number) => text.split('\n').slice(0, count).join('\n')
|
|
175
|
+
|
|
176
|
+
/** The card's headline: the error's words, an answer's first two lines, "Undone", or the title. */
|
|
177
|
+
export function cardTitle(pin: PinModel, fallback = 'Done'): string {
|
|
178
|
+
if (pin.state === 'error' && pin.error) return pin.error
|
|
179
|
+
if (pin.state === 'answered' && pin.answer) return firstLines(pin.answer, 2)
|
|
180
|
+
if (pin.state === 'undone') return 'Undone'
|
|
181
|
+
if (pin.title) return pin.title
|
|
182
|
+
if (pin.state === 'done' && pin.committed) {
|
|
183
|
+
const count = new Set([...pin.changedTargetIds, ...pin.alsoChangedIds]).size
|
|
184
|
+
return count
|
|
185
|
+
? `Pascal changed ${count} element${count === 1 ? '' : 's'}`
|
|
186
|
+
: 'Pascal updated the scene'
|
|
187
|
+
}
|
|
188
|
+
return fallback
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** "Also changed 2 other elements": the objects a prompt names can differ from the ones it changes. */
|
|
192
|
+
export function alsoChangedLine(pin: PinModel): string | null {
|
|
193
|
+
const count = pin.alsoChangedIds.length
|
|
194
|
+
if (count === 0) return null
|
|
195
|
+
return `Also changed ${count} other element${count === 1 ? '' : 's'}`
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The card rides its pin, so a camera flight (an Undo playing the build backwards, say) can carry it
|
|
200
|
+
* out from under a pointer that has not moved. That is not the person leaving, and the card holds
|
|
201
|
+
* the Redo, so it stays for a while. The leave event of a pointer that walked out is dispatched
|
|
202
|
+
* before the move that caused it, so it is told by position: `at` is where the leave says the
|
|
203
|
+
* pointer is, `last` where the last move left it; a pointer that did not move is where it was.
|
|
204
|
+
* A finger leaves without a move whenever it lifts, so only a mouse can be carried.
|
|
205
|
+
*/
|
|
206
|
+
export function cardCarriedFromPointer(leave: {
|
|
207
|
+
pointerType: string
|
|
208
|
+
at: { x: number; y: number }
|
|
209
|
+
last: { x: number; y: number } | null
|
|
210
|
+
}): boolean {
|
|
211
|
+
return (
|
|
212
|
+
leave.pointerType === 'mouse' &&
|
|
213
|
+
leave.last !== null &&
|
|
214
|
+
Math.abs(leave.at.x - leave.last.x) < 0.5 &&
|
|
215
|
+
Math.abs(leave.at.y - leave.last.y) < 0.5
|
|
216
|
+
)
|
|
217
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { type PlacementInput, type PlacementState, placeBubble } from './placement'
|
|
3
|
+
import type { Rect } from './types'
|
|
4
|
+
|
|
5
|
+
// The bubble sits beside its target and never covers it (spec 2.4, placement). Written first: a
|
|
6
|
+
// bubble over the thing it asks about; a side that flips on every frame at the boundary; a bubble
|
|
7
|
+
// that detaches from its anchor; a bubble that leaves the viewport.
|
|
8
|
+
|
|
9
|
+
const VIEWPORT: Rect = { x0: 12, y0: 56, x1: 1000, y1: 700 }
|
|
10
|
+
const SIZE = { w: 320, h: 180 }
|
|
11
|
+
const FRESH: PlacementState = { side: null, badMs: 0 }
|
|
12
|
+
|
|
13
|
+
const input = (more: Partial<PlacementInput> = {}): PlacementInput => ({
|
|
14
|
+
box: { x0: 400, y0: 300, x1: 500, y1: 400 },
|
|
15
|
+
click: { x: 450, y: 350 },
|
|
16
|
+
size: SIZE,
|
|
17
|
+
viewport: VIEWPORT,
|
|
18
|
+
offset: { x: 0, y: 0 },
|
|
19
|
+
first: true,
|
|
20
|
+
...more,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const bubbleRect = (pos: { x: number; y: number }): Rect => ({
|
|
24
|
+
x0: pos.x,
|
|
25
|
+
y0: pos.y,
|
|
26
|
+
x1: pos.x + SIZE.w,
|
|
27
|
+
y1: pos.y + SIZE.h,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const touches = (a: Rect, b: Rect) => !(a.x1 < b.x0 || a.x0 > b.x1 || a.y1 < b.y0 || a.y0 > b.y1)
|
|
31
|
+
|
|
32
|
+
describe('which side the bubble takes', () => {
|
|
33
|
+
test('is clamped inside the viewport', () => {
|
|
34
|
+
const box = { x0: 30, y0: 60, x1: 90, y1: 120 }
|
|
35
|
+
const placed = placeBubble(input({ box, click: { x: 60, y: 90 } }), FRESH, 16)
|
|
36
|
+
|
|
37
|
+
expect(placed.pos.x).toBeGreaterThanOrEqual(VIEWPORT.x0)
|
|
38
|
+
expect(placed.pos.y).toBeGreaterThanOrEqual(VIEWPORT.y0)
|
|
39
|
+
expect(placed.pos.x + SIZE.w).toBeLessThanOrEqual(VIEWPORT.x1)
|
|
40
|
+
expect(placed.pos.y + SIZE.h).toBeLessThanOrEqual(VIEWPORT.y1)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('never covers the target when a side fits', () => {
|
|
44
|
+
for (const box of [
|
|
45
|
+
{ x0: 400, y0: 300, x1: 500, y1: 400 },
|
|
46
|
+
{ x0: 700, y0: 300, x1: 800, y1: 400 },
|
|
47
|
+
{ x0: 50, y0: 300, x1: 990, y1: 400 },
|
|
48
|
+
{ x0: 50, y0: 560, x1: 990, y1: 690 },
|
|
49
|
+
]) {
|
|
50
|
+
const click = { x: (box.x0 + box.x1) / 2, y: (box.y0 + box.y1) / 2 }
|
|
51
|
+
const placed = placeBubble(input({ box, click }), FRESH, 16)
|
|
52
|
+
|
|
53
|
+
expect(touches(bubbleRect(placed.pos), box)).toBe(false)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('a target filling the view puts the bubble beside the click, outside a 48 px disc', () => {
|
|
58
|
+
const box = { x0: 12, y0: 56, x1: 900, y1: 690 }
|
|
59
|
+
const click = { x: 450, y: 300 }
|
|
60
|
+
const placed = placeBubble(input({ box, click }), FRESH, 16)
|
|
61
|
+
const disc = { x0: click.x - 48, y0: click.y - 48, x1: click.x + 48, y1: click.y + 48 }
|
|
62
|
+
|
|
63
|
+
expect(placed.side).toBe('right')
|
|
64
|
+
expect(placed.pos.x).toBe(click.x + 48 + 16)
|
|
65
|
+
expect(touches(bubbleRect(placed.pos), disc)).toBe(false)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('the user’s drag offset moves it, and the tail follows', () => {
|
|
69
|
+
const plain = placeBubble(input(), FRESH, 16)
|
|
70
|
+
const moved = placeBubble(input({ offset: { x: 30, y: -20 } }), FRESH, 16)
|
|
71
|
+
|
|
72
|
+
expect(moved.pos).toEqual({ x: plain.pos.x + 30, y: plain.pos.y - 20 })
|
|
73
|
+
expect(moved.tail?.y).toBe((plain.tail?.y ?? 0) + 20)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('hysteresis: a side is kept until it has been wrong for 150 ms', () => {
|
|
78
|
+
const blockedRight = input({
|
|
79
|
+
box: { x0: 700, y0: 300, x1: 800, y1: 400 },
|
|
80
|
+
click: { x: 750, y: 350 },
|
|
81
|
+
first: false,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('no switch before 150 ms', () => {
|
|
85
|
+
const first = placeBubble(blockedRight, { side: 'right', badMs: 0 }, 100)
|
|
86
|
+
|
|
87
|
+
expect(first.side).toBe('right')
|
|
88
|
+
expect(first.glide).toBe(false)
|
|
89
|
+
expect(first.state).toEqual({ side: 'right', badMs: 100 })
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('the switch after 150 ms glides, and resets the clock', () => {
|
|
93
|
+
const first = placeBubble(blockedRight, { side: 'right', badMs: 0 }, 100)
|
|
94
|
+
const second = placeBubble(blockedRight, first.state, 60)
|
|
95
|
+
|
|
96
|
+
expect(second.side).toBe('left')
|
|
97
|
+
expect(second.glide).toBe(true)
|
|
98
|
+
expect(second.state).toEqual({ side: 'left', badMs: 0 })
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('a side that is right again clears the count', () => {
|
|
102
|
+
const wrong = placeBubble(blockedRight, { side: 'right', badMs: 0 }, 100)
|
|
103
|
+
const right = placeBubble(input({ first: false }), wrong.state, 16)
|
|
104
|
+
|
|
105
|
+
expect(right.side).toBe('right')
|
|
106
|
+
expect(right.state.badMs).toBe(0)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('with every side blocked it still obeys the 150 ms rule', () => {
|
|
110
|
+
const viewport = { x0: 400, y0: 200, x1: 620, y1: 420 }
|
|
111
|
+
const crowded = input({
|
|
112
|
+
box: { x0: 400, y0: 200, x1: 620, y1: 420 },
|
|
113
|
+
click: { x: 500, y: 300 },
|
|
114
|
+
viewport,
|
|
115
|
+
first: false,
|
|
116
|
+
})
|
|
117
|
+
const early = placeBubble(crowded, { side: 'left', badMs: 0 }, 100)
|
|
118
|
+
const late = placeBubble(crowded, early.state, 100)
|
|
119
|
+
|
|
120
|
+
expect(early.side).toBe('left')
|
|
121
|
+
expect(early.glide).toBe(false)
|
|
122
|
+
expect(late.side).toBe('right')
|
|
123
|
+
expect(late.glide).toBe(true)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('the tail', () => {
|
|
128
|
+
test('is clamped 16 px from the corners', () => {
|
|
129
|
+
const placed = placeBubble(
|
|
130
|
+
input({ box: { x0: 400, y0: 300, x1: 500, y1: 400 }, click: { x: 450, y: 305 } }),
|
|
131
|
+
FRESH,
|
|
132
|
+
16,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
expect(placed.tail?.edge).toBe('left')
|
|
136
|
+
expect(placed.tail?.y).toBeGreaterThanOrEqual(16)
|
|
137
|
+
expect(placed.tail?.y).toBeLessThanOrEqual(180 - 16)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('is hidden when the click is under the bubble', () => {
|
|
141
|
+
const box = { x0: 12, y0: 56, x1: 1000, y1: 700 }
|
|
142
|
+
const placed = placeBubble(
|
|
143
|
+
input({ box, click: { x: 600, y: 300 }, offset: { x: -200, y: 0 } }),
|
|
144
|
+
FRESH,
|
|
145
|
+
16,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
expect(placed.tail).toBeNull()
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
describe('docking', () => {
|
|
153
|
+
test('a click off the viewport docks at the nearest edge, the tail turned to it', () => {
|
|
154
|
+
const placed = placeBubble(
|
|
155
|
+
input({ click: { x: -50, y: 300 }, box: { x0: -80, y0: 250, x1: -20, y1: 350 } }),
|
|
156
|
+
FRESH,
|
|
157
|
+
16,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
expect(placed.side).toBe('dock')
|
|
161
|
+
expect(placed.pos.x).toBe(VIEWPORT.x0)
|
|
162
|
+
expect(placed.tail).toEqual({ edge: 'left', x: 0, y: 90 })
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('docks on the right and at the bottom too', () => {
|
|
166
|
+
const right = placeBubble(
|
|
167
|
+
input({ click: { x: 1300, y: 300 }, box: { x0: 1280, y0: 250, x1: 1320, y1: 350 } }),
|
|
168
|
+
FRESH,
|
|
169
|
+
16,
|
|
170
|
+
)
|
|
171
|
+
const bottom = placeBubble(
|
|
172
|
+
input({ click: { x: 500, y: 900 }, box: { x0: 480, y0: 880, x1: 520, y1: 920 } }),
|
|
173
|
+
FRESH,
|
|
174
|
+
16,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
expect(right.pos.x).toBe(VIEWPORT.x1 - SIZE.w)
|
|
178
|
+
expect(right.tail?.edge).toBe('right')
|
|
179
|
+
expect(bottom.pos.y).toBe(VIEWPORT.y1 - SIZE.h)
|
|
180
|
+
expect(bottom.tail?.edge).toBe('bottom')
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('a click that cannot be projected docks inside the view with no tail', () => {
|
|
184
|
+
const placed = placeBubble(input({ click: null }), FRESH, 16)
|
|
185
|
+
|
|
186
|
+
expect(placed.side).toBe('dock')
|
|
187
|
+
expect(placed.tail).toBeNull()
|
|
188
|
+
expect(placed.pos.x).toBeGreaterThanOrEqual(VIEWPORT.x0)
|
|
189
|
+
expect(placed.pos.x + SIZE.w).toBeLessThanOrEqual(VIEWPORT.x1)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test('coming back from the dock re-chooses a side and glides', () => {
|
|
193
|
+
const docked = placeBubble(
|
|
194
|
+
input({ click: { x: -50, y: 300 }, box: { x0: -80, y0: 250, x1: -20, y1: 350 } }),
|
|
195
|
+
FRESH,
|
|
196
|
+
16,
|
|
197
|
+
)
|
|
198
|
+
const back = placeBubble(input({ first: false }), docked.state, 16)
|
|
199
|
+
|
|
200
|
+
expect(back.side).toBe('right')
|
|
201
|
+
expect(back.glide).toBe(true)
|
|
202
|
+
})
|
|
203
|
+
})
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { BIG_BOX_DISC, BUBBLE_GAP, SIDE_HYSTERESIS_MS } from './choreography'
|
|
2
|
+
import type { BubbleSide, Rect, ScreenPoint } from './types'
|
|
3
|
+
|
|
4
|
+
export type PlacementInput = {
|
|
5
|
+
/** The targets' screen box. */
|
|
6
|
+
box: Rect
|
|
7
|
+
/** Where the finger landed on screen; null when the anchor cannot be projected (behind the camera). */
|
|
8
|
+
click: ScreenPoint | null
|
|
9
|
+
/** The bubble's size. */
|
|
10
|
+
size: { w: number; h: number }
|
|
11
|
+
/** The free viewport: the canvas minus the chat column and toolbars, already inset. */
|
|
12
|
+
viewport: Rect
|
|
13
|
+
/** Where the person dragged the bubble to by its context row, from its placed position. */
|
|
14
|
+
offset: ScreenPoint
|
|
15
|
+
/** The first placement of this bubble: no hysteresis yet. */
|
|
16
|
+
first: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type PlacementState = {
|
|
20
|
+
side: BubbleSide | null
|
|
21
|
+
/** How long the current side has been invalid, in ms. */
|
|
22
|
+
badMs: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type TailEdge = 'left' | 'right' | 'top' | 'bottom'
|
|
26
|
+
|
|
27
|
+
export type Placement = {
|
|
28
|
+
/** The bubble's top-left on screen, drag offset included. */
|
|
29
|
+
pos: ScreenPoint
|
|
30
|
+
side: BubbleSide
|
|
31
|
+
/** Where the tail sits on the bubble's edge, relative to the bubble; null when the click is under it. */
|
|
32
|
+
tail: { edge: TailEdge; x: number; y: number } | null
|
|
33
|
+
/** The side just changed: the caller springs to `pos` instead of snapping. */
|
|
34
|
+
glide: boolean
|
|
35
|
+
/** The click inside the bubble, for `transform-origin` on the open motion. */
|
|
36
|
+
origin: ScreenPoint
|
|
37
|
+
state: PlacementState
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const SIDES = ['right', 'left', 'below', 'above'] as const
|
|
41
|
+
const TAIL_CORNER = 16
|
|
42
|
+
const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max)
|
|
43
|
+
|
|
44
|
+
const area = (rect: Rect) => Math.max(0, rect.x1 - rect.x0) * Math.max(0, rect.y1 - rect.y0)
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Beside the target, never over it; the anchor is tracked 1:1 by the caller each frame, so this is
|
|
48
|
+
* only the choice of side. A side is kept until it has been invalid for 150 ms, then the next
|
|
49
|
+
* free one is taken and the move glides; with the anchor off screen the bubble docks at the nearest
|
|
50
|
+
* edge (spec 2.4, rules 1-5).
|
|
51
|
+
*/
|
|
52
|
+
export function placeBubble(input: PlacementInput, state: PlacementState, dtMs: number): Placement {
|
|
53
|
+
const { box, size, viewport: V, offset } = input
|
|
54
|
+
const { w: bw, h: bh } = size
|
|
55
|
+
const fit = (p: ScreenPoint): ScreenPoint => ({
|
|
56
|
+
x: clamp(p.x, V.x0, Math.max(V.x0, V.x1 - bw)),
|
|
57
|
+
y: clamp(p.y, V.y0, Math.max(V.y0, V.y1 - bh)),
|
|
58
|
+
})
|
|
59
|
+
const anchor =
|
|
60
|
+
input.click ??
|
|
61
|
+
(Number.isFinite(box.x0 + box.x1 + box.y0 + box.y1)
|
|
62
|
+
? { x: (box.x0 + box.x1) / 2, y: (box.y0 + box.y1) / 2 }
|
|
63
|
+
: { x: (V.x0 + V.x1) / 2, y: (V.y0 + V.y1) / 2 })
|
|
64
|
+
const onScreen =
|
|
65
|
+
input.click !== null &&
|
|
66
|
+
anchor.x >= V.x0 &&
|
|
67
|
+
anchor.x <= V.x1 &&
|
|
68
|
+
anchor.y >= V.y0 &&
|
|
69
|
+
anchor.y <= V.y1
|
|
70
|
+
|
|
71
|
+
let pos: ScreenPoint
|
|
72
|
+
let side: BubbleSide
|
|
73
|
+
let badMs = state.badMs
|
|
74
|
+
if (onScreen) {
|
|
75
|
+
// A target filling the view cannot be avoided: stand clear of the click instead.
|
|
76
|
+
const big = area(box) > 0.5 * area(V)
|
|
77
|
+
const R: Rect = big
|
|
78
|
+
? {
|
|
79
|
+
x0: anchor.x - BIG_BOX_DISC,
|
|
80
|
+
x1: anchor.x + BIG_BOX_DISC,
|
|
81
|
+
y0: anchor.y - BIG_BOX_DISC,
|
|
82
|
+
y1: anchor.y + BIG_BOX_DISC,
|
|
83
|
+
}
|
|
84
|
+
: box
|
|
85
|
+
const at: Record<(typeof SIDES)[number], ScreenPoint> = {
|
|
86
|
+
right: { x: R.x1 + BUBBLE_GAP, y: anchor.y - bh / 2 },
|
|
87
|
+
left: { x: R.x0 - BUBBLE_GAP - bw, y: anchor.y - bh / 2 },
|
|
88
|
+
below: { x: anchor.x - bw / 2, y: R.y1 + BUBBLE_GAP },
|
|
89
|
+
above: { x: anchor.x - bw / 2, y: R.y0 - BUBBLE_GAP - bh },
|
|
90
|
+
}
|
|
91
|
+
const free = (s: (typeof SIDES)[number]) => {
|
|
92
|
+
const p = fit(at[s])
|
|
93
|
+
return p.x + bw < R.x0 - 2 || p.x > R.x1 + 2 || p.y + bh < R.y0 - 2 || p.y > R.y1 + 2
|
|
94
|
+
}
|
|
95
|
+
const best = SIDES.find(free) ?? 'right'
|
|
96
|
+
const held = state.side && state.side !== 'dock' && !input.first ? state.side : null
|
|
97
|
+
let chosen: (typeof SIDES)[number]
|
|
98
|
+
if (!held) {
|
|
99
|
+
chosen = best
|
|
100
|
+
badMs = 0
|
|
101
|
+
} else if (!free(held)) {
|
|
102
|
+
badMs += dtMs
|
|
103
|
+
if (badMs > SIDE_HYSTERESIS_MS) {
|
|
104
|
+
chosen = best
|
|
105
|
+
badMs = 0
|
|
106
|
+
} else chosen = held
|
|
107
|
+
} else {
|
|
108
|
+
chosen = held
|
|
109
|
+
badMs = 0
|
|
110
|
+
}
|
|
111
|
+
side = chosen
|
|
112
|
+
pos = fit(at[chosen])
|
|
113
|
+
} else {
|
|
114
|
+
side = 'dock'
|
|
115
|
+
badMs = 0
|
|
116
|
+
pos = fit({ x: anchor.x - bw / 2, y: anchor.y - bh / 2 })
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const glide = !input.first && state.side !== null && side !== state.side
|
|
120
|
+
const placed = { x: pos.x + offset.x, y: pos.y + offset.y }
|
|
121
|
+
|
|
122
|
+
// The tail points at the click from the bubble's nearest edge; under the bubble there is none.
|
|
123
|
+
let tail: Placement['tail'] = null
|
|
124
|
+
const lx = anchor.x - placed.x
|
|
125
|
+
const ly = anchor.y - placed.y
|
|
126
|
+
if (input.click !== null) {
|
|
127
|
+
if (lx < 0) tail = { edge: 'left', x: 0, y: clamp(ly, TAIL_CORNER, bh - TAIL_CORNER) }
|
|
128
|
+
else if (lx > bw) tail = { edge: 'right', x: bw, y: clamp(ly, TAIL_CORNER, bh - TAIL_CORNER) }
|
|
129
|
+
else if (ly < 0) tail = { edge: 'top', x: clamp(lx, TAIL_CORNER, bw - TAIL_CORNER), y: 0 }
|
|
130
|
+
else if (ly > bh) tail = { edge: 'bottom', x: clamp(lx, TAIL_CORNER, bw - TAIL_CORNER), y: bh }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
pos: placed,
|
|
135
|
+
side,
|
|
136
|
+
tail,
|
|
137
|
+
glide,
|
|
138
|
+
origin: { x: clamp(lx, 0, bw), y: clamp(ly, 0, bh) },
|
|
139
|
+
state: { side, badMs },
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// The pointer's last position on the page, shared by whatever follows it (the label chip), so the
|
|
2
|
+
// chip moves on the frame of the pointer event, never after a React render.
|
|
3
|
+
|
|
4
|
+
type PointerFn = (x: number, y: number) => void
|
|
5
|
+
|
|
6
|
+
let x = -1
|
|
7
|
+
let y = -1
|
|
8
|
+
let attached = false
|
|
9
|
+
const subscribers = new Set<PointerFn>()
|
|
10
|
+
|
|
11
|
+
function onMove(event: PointerEvent) {
|
|
12
|
+
x = event.clientX
|
|
13
|
+
y = event.clientY
|
|
14
|
+
for (const fn of subscribers) fn(x, y)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getPointer(): { x: number; y: number } | null {
|
|
18
|
+
return x < 0 ? null : { x, y }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function subscribePointer(fn: PointerFn): () => void {
|
|
22
|
+
subscribers.add(fn)
|
|
23
|
+
if (!attached && typeof window !== 'undefined') {
|
|
24
|
+
window.addEventListener('pointermove', onMove, { passive: true, capture: true })
|
|
25
|
+
attached = true
|
|
26
|
+
}
|
|
27
|
+
return () => {
|
|
28
|
+
subscribers.delete(fn)
|
|
29
|
+
if (subscribers.size === 0 && attached) {
|
|
30
|
+
window.removeEventListener('pointermove', onMove, { capture: true })
|
|
31
|
+
attached = false
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Vector3 } from 'three'
|
|
2
|
+
import { getEditorThreeContext } from '../../components/editor/three-context-bridge'
|
|
3
|
+
import { BUBBLE_BOTTOM_INSET, BUBBLE_TOP_INSET, VIEWPORT_INSET } from './overlay-metrics'
|
|
4
|
+
import type { Rect, ScreenPoint } from './types'
|
|
5
|
+
|
|
6
|
+
// World to screen for the DOM overlay, read from the live editor camera each frame: the overlay
|
|
7
|
+
// lives outside the R3F tree, so it asks the same camera the canvas draws with. Screen units are
|
|
8
|
+
// CSS pixels in viewport coordinates (the overlay is `position: fixed`).
|
|
9
|
+
|
|
10
|
+
const scratch = new Vector3()
|
|
11
|
+
|
|
12
|
+
export type Projected = ScreenPoint & { ok: boolean }
|
|
13
|
+
|
|
14
|
+
/** The canvas the editor draws in, or null before it mounts. */
|
|
15
|
+
export function canvasRect(): DOMRect | null {
|
|
16
|
+
return getEditorThreeContext()?.domElement.getBoundingClientRect() ?? null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A world point on the page; `ok` is false behind the camera or outside the clip volume. */
|
|
20
|
+
export function projectWorld(point: readonly [number, number, number]): Projected {
|
|
21
|
+
const three = getEditorThreeContext()
|
|
22
|
+
if (!three) return { x: 0, y: 0, ok: false }
|
|
23
|
+
const rect = three.domElement.getBoundingClientRect()
|
|
24
|
+
scratch.set(point[0], point[1], point[2])
|
|
25
|
+
three.camera.updateMatrixWorld()
|
|
26
|
+
scratch.project(three.camera)
|
|
27
|
+
const ok = scratch.z > -1 && scratch.z < 1
|
|
28
|
+
return {
|
|
29
|
+
x: rect.left + (scratch.x * 0.5 + 0.5) * rect.width,
|
|
30
|
+
y: rect.top + (-scratch.y * 0.5 + 0.5) * rect.height,
|
|
31
|
+
ok,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The screen box of a world box (its eight corners projected); null when nothing of it is in front of the camera. */
|
|
36
|
+
export function projectBox(box: {
|
|
37
|
+
min: readonly [number, number, number]
|
|
38
|
+
max: readonly [number, number, number]
|
|
39
|
+
}): Rect | null {
|
|
40
|
+
let x0 = Number.POSITIVE_INFINITY
|
|
41
|
+
let y0 = Number.POSITIVE_INFINITY
|
|
42
|
+
let x1 = Number.NEGATIVE_INFINITY
|
|
43
|
+
let y1 = Number.NEGATIVE_INFINITY
|
|
44
|
+
let any = false
|
|
45
|
+
for (const x of [box.min[0], box.max[0]])
|
|
46
|
+
for (const y of [box.min[1], box.max[1]])
|
|
47
|
+
for (const z of [box.min[2], box.max[2]]) {
|
|
48
|
+
const p = projectWorld([x, y, z])
|
|
49
|
+
if (!p.ok) continue
|
|
50
|
+
any = true
|
|
51
|
+
x0 = Math.min(x0, p.x)
|
|
52
|
+
y0 = Math.min(y0, p.y)
|
|
53
|
+
x1 = Math.max(x1, p.x)
|
|
54
|
+
y1 = Math.max(y1, p.y)
|
|
55
|
+
}
|
|
56
|
+
return any ? { x0, y0, x1, y1 } : null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The union of several screen boxes. */
|
|
60
|
+
export function unionRect(rects: readonly Rect[]): Rect | null {
|
|
61
|
+
if (rects.length === 0) return null
|
|
62
|
+
return rects.reduce((all, r) => ({
|
|
63
|
+
x0: Math.min(all.x0, r.x0),
|
|
64
|
+
y0: Math.min(all.y0, r.y0),
|
|
65
|
+
x1: Math.max(all.x1, r.x1),
|
|
66
|
+
y1: Math.max(all.y1, r.y1),
|
|
67
|
+
}))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The free viewport a bubble may sit in: the canvas inset on every side, and clear of the top bar
|
|
72
|
+
* and the action bar the editor floats over it.
|
|
73
|
+
*/
|
|
74
|
+
export function freeViewport(): Rect | null {
|
|
75
|
+
const rect = canvasRect()
|
|
76
|
+
if (!rect) return null
|
|
77
|
+
return {
|
|
78
|
+
x0: rect.left + VIEWPORT_INSET,
|
|
79
|
+
y0: rect.top + BUBBLE_TOP_INSET,
|
|
80
|
+
x1: rect.right - VIEWPORT_INSET,
|
|
81
|
+
y1: rect.bottom - BUBBLE_BOTTOM_INSET,
|
|
82
|
+
}
|
|
83
|
+
}
|