@pascal-app/editor 1.0.7 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -15
- package/src/components/editor/action-menu-button.tsx +1 -1
- package/src/components/editor/alignment-3d-guide-layer.tsx +1 -1
- package/src/components/editor/custom-camera-controls.tsx +101 -45
- package/src/components/editor/duplicate-node.ts +2 -1
- package/src/components/editor/editor-layout-v2.tsx +1 -1
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +20 -1983
- package/src/components/editor/fit-scene-framing.test.ts +318 -0
- package/src/components/editor/fit-scene-framing.ts +142 -0
- package/src/components/editor/floating-action-menu.tsx +5 -3
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floor-openings-overlay.tsx +1 -1
- package/src/components/editor/floor-region-controls.tsx +8 -4
- package/src/components/editor/floorplan-navigation-presentation.test.ts +0 -40
- package/src/components/editor/floorplan-navigation-presentation.ts +0 -66
- package/src/components/editor/floorplan-panel.tsx +199 -30
- package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
- package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
- package/src/components/editor/follow-pascal.test.tsx +1 -2
- package/src/components/editor/follow-pascal.tsx +1 -1
- package/src/components/editor/footprint-height-handle.tsx +2 -2
- package/src/components/editor/group-actions.ts +1 -2
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-move-3d.ts +1 -2
- package/src/components/editor/group-rotate-handle.tsx +1 -2
- package/src/components/editor/group-transform-shared.ts +3 -1
- package/src/components/editor/handles/use-handle-drag.ts +1 -2
- package/src/components/editor/index.tsx +43 -13
- package/src/components/editor/inspector-extension-actions.tsx +1 -1
- package/src/components/editor/measurement-pill.tsx +1 -1
- package/src/components/editor/node-arrow-handles.tsx +2 -2
- package/src/components/editor/open-wall-ends-3d-layer.tsx +2 -3
- package/src/components/editor/open-wall-ends-hint.tsx +1 -1
- package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
- package/src/components/editor/quick-measurement-card.tsx +1 -1
- package/src/components/editor/room-controls.tsx +10 -6
- package/src/components/editor/room-handles-2d.tsx +3 -3
- package/src/components/editor/room-handles.tsx +4 -5
- package/src/components/editor/room-highlight.tsx +6 -105
- package/src/components/editor/room-labels.test.ts +8 -822
- package/src/components/editor/room-labels.tsx +41 -1670
- package/src/components/editor/room-webgpu-materials.test.ts +1 -1
- package/src/components/editor/selection-manager.tsx +5 -3
- package/src/components/editor/site-edge-labels.tsx +7 -3
- package/src/components/editor/snapshot-capture-overlay.tsx +2 -5
- package/src/components/editor/use-floorplan-background-placement.ts +2 -1
- package/src/components/editor/views/plugins-view.tsx +1 -1
- package/src/components/editor/views/view-bar.tsx +9 -9
- package/src/components/editor/wall-measurement-label.tsx +31 -15
- package/src/components/editor/wall-move-side-handles.tsx +11 -7
- package/src/components/editor/wall-pull-grip.tsx +2 -2
- package/src/components/editor/wall-region-handles.tsx +2 -2
- package/src/components/editor/wall-snap-beacon-layer.tsx +53 -32
- package/src/components/editor-2d/floorplan-group-move.tsx +3 -4
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +13 -13
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +2 -3
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +23 -6
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +0 -37
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +0 -30
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +13 -3
- package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +14 -3
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +30 -10
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +39 -35
- package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
- package/src/components/systems/roof/roof-edit-system.tsx +33 -21
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
- package/src/components/tools/distribution/distribution-run-tool.tsx +151 -33
- package/src/components/tools/item/move-tool.tsx +1 -6
- package/src/components/tools/item/placement-history.test.tsx +1 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +3 -4
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +7 -3
- package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +39 -16
- package/src/components/tools/shared/cursor-sphere.tsx +1 -1
- package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
- package/src/components/tools/shared/placement-box.tsx +2 -6
- package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
- package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
- package/src/components/tools/shared/segment-angle.ts +0 -4
- package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
- package/src/components/tools/tool-manager.tsx +1 -24
- package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
- package/src/components/tools/wall/wall-typed-entry.ts +70 -0
- package/src/components/ui/action-menu/camera-actions.tsx +2 -2
- package/src/components/ui/action-menu/control-modes.tsx +1 -1
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +1 -1
- package/src/components/ui/action-menu/index.tsx +3 -3
- package/src/components/ui/action-menu/measurement-control.tsx +3 -3
- package/src/components/ui/action-menu/placement.ts +0 -18
- package/src/components/ui/action-menu/point-ask-button.tsx +1 -1
- package/src/components/ui/action-menu/view-toggles.tsx +5 -5
- package/src/components/ui/controls/finish-swatch.tsx +1 -1
- package/src/components/ui/controls/material-paint-panel.tsx +2 -2
- package/src/components/ui/controls/scene-material-list.tsx +2 -2
- package/src/components/ui/controls/select-control.tsx +1 -1
- package/src/components/ui/controls/slider-control.tsx +18 -6
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +1 -1
- package/src/components/ui/controls/tool-options-panel.tsx +74 -44
- package/src/components/ui/floating-level-selector.tsx +3 -3
- package/src/components/ui/helpers/contextual-helper-panel.tsx +1 -1
- package/src/components/ui/helpers/helper-manager.test.tsx +54 -1
- package/src/components/ui/helpers/helper-manager.tsx +55 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/collections/collections-popover.tsx +2 -2
- package/src/components/ui/panels/floor-foundation-panel.tsx +3 -3
- package/src/components/ui/panels/floor-opening-panel.tsx +5 -5
- package/src/components/ui/panels/mobile-selection-bar.tsx +28 -24
- package/src/components/ui/panels/multi-height-mode.tsx +3 -3
- package/src/components/ui/panels/multi-selection-panel.tsx +1 -1
- package/src/components/ui/panels/node-display.ts +1 -1
- package/src/components/ui/panels/panel-manager.tsx +5 -3
- package/src/components/ui/panels/panel-wrapper.tsx +42 -19
- package/src/components/ui/panels/parametric-inspector.tsx +1 -1
- package/src/components/ui/panels/room-construction-rows.tsx +7 -7
- package/src/components/ui/panels/room-name-options.ts +1 -5
- package/src/components/ui/panels/room-openings-field.tsx +1 -1
- package/src/components/ui/panels/room-panel.tsx +7 -9
- package/src/components/ui/primitives/color-dot.tsx +2 -2
- package/src/components/ui/primitives/opacity-control.tsx +2 -2
- package/src/components/ui/primitives/sidebar.tsx +3 -3
- package/src/components/ui/right-stack.test.ts +13 -6
- package/src/components/ui/right-stack.tsx +22 -11
- package/src/components/ui/sidebar/icon-rail.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +2 -6
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +3 -10
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +46 -7
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +14 -5
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +1 -1
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +2 -2
- package/src/components/ui/wall-type/wall-type-menu.tsx +1 -1
- package/src/components/viewer-overlay.tsx +1 -2
- package/src/hooks/use-auto-frame.ts +1 -2
- package/src/hooks/use-keyboard.ts +1 -2
- package/src/hooks/use-selected-room.ts +5 -7
- package/src/i18n/floor-footprint-name.ts +1 -1
- package/src/i18n/locales/en/messages.po +1036 -306
- package/src/i18n/locales/en/messages.ts +1 -1
- package/src/i18n/locales/en-XA/messages.ts +1 -1
- package/src/i18n/locales/es/messages.po +2267 -439
- package/src/i18n/locales/fr/messages.po +2267 -439
- package/src/i18n/locales/ja/messages.po +2267 -439
- package/src/i18n/locales/zh-CN/messages.po +2267 -439
- package/src/i18n/locales.test.ts +1 -1
- package/src/index.tsx +183 -178
- package/src/lib/below-grade.test.tsx +4 -4
- package/src/lib/camera-facing.test.ts +1 -1
- package/src/lib/camera-facing.ts +2 -3
- package/src/lib/contextual-help.ts +6 -0
- package/src/lib/editor-level-display.ts +1 -1
- package/src/lib/elevation-guides.ts +21 -6
- package/src/lib/elevator-support.ts +1 -72
- package/src/lib/floor-footprints.test.ts +3 -3
- package/src/lib/floor-footprints.ts +1 -9
- package/src/lib/floor-foundation-support.test.ts +8 -3
- package/src/lib/floor-opening-draft.ts +2 -2
- package/src/lib/floor-region-session.ts +1 -1
- package/src/lib/floorplan/annotation-visibility.test.ts +1 -1
- package/src/lib/floorplan/annotation-visibility.ts +1 -1
- package/src/lib/floorplan/floorplan-export.test.ts +1 -1
- package/src/lib/floorplan/floorplan-export.tsx +8 -11
- package/src/lib/floorplan/floorplan-extension.ts +4 -157
- package/src/lib/floorplan/floorplan-mode.ts +2 -1
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +1 -1
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +8 -6
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +3 -4
- package/src/lib/floorplan/geometry.ts +2 -112
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +2 -1
- package/src/lib/floorplan/open-wall-ends.ts +2 -1
- package/src/lib/floorplan/schedules.ts +6 -3
- package/src/lib/floorplan/sheet-export.ts +7 -3
- package/src/lib/floorplan/site-plan/site-parts.ts +7 -1
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +1 -1
- package/src/lib/follow-pascal-presentation.ts +1 -1
- package/src/lib/gesture-lifecycle.ts +3 -1
- package/src/lib/glb-export.ts +1 -1
- package/src/lib/interaction/scope.ts +1 -0
- package/src/lib/layer-thickness.ts +1 -1
- package/src/lib/level-selection.test.ts +18 -2
- package/src/lib/level-selection.ts +31 -8
- package/src/lib/linear-display.ts +1 -1
- package/src/lib/live-gesture-round-trip.test.ts +1 -1
- package/src/lib/material-paint.ts +1 -1
- package/src/lib/measurement-label.test.ts +1 -1
- package/src/lib/measurements.test.ts +7 -5
- package/src/lib/mezzanine-selection.test.ts +1 -1
- package/src/lib/node-action-movement.test.tsx +115 -0
- package/src/lib/node-action-movement.ts +2 -0
- package/src/lib/paint-region-mode.ts +1 -1
- package/src/lib/placement-metadata.ts +4 -0
- package/src/lib/point-ask/describe-target.ts +6 -8
- package/src/lib/point-ask/region.ts +1 -1
- package/src/lib/point-ask/session.ts +1 -2
- package/src/lib/print-shell-compiler.ts +12 -1
- package/src/lib/print-wall-solids.test.ts +11 -0
- package/src/lib/print-wall-solids.ts +9 -0
- package/src/lib/roof-duplication.test.ts +100 -0
- package/src/lib/roof-duplication.ts +30 -5
- package/src/lib/room-built-on.test.ts +2 -5
- package/src/lib/room-built-on.ts +1 -15
- package/src/lib/room-construction-commands.ts +3 -2
- package/src/lib/room-controls-phase6.test.ts +1 -1
- package/src/lib/room-handle-drag.ts +8 -37
- package/src/lib/room-name-catalog.test.ts +2 -1
- package/src/lib/room-name-messages.test.ts +2 -1
- package/src/lib/room-name-messages.ts +1 -118
- package/src/lib/room-name-storage.test.ts +2 -1
- package/src/lib/room-push-dimensions.test.ts +1 -1
- package/src/lib/room-selection-commands.ts +2 -2
- package/src/lib/room-selection.test.ts +6 -6
- package/src/lib/room-transform-session.ts +1 -2
- package/src/lib/room-zone-routing.ts +2 -2
- package/src/lib/scene-clipboard.ts +2 -1
- package/src/lib/selection-routing.test.ts +0 -28
- package/src/lib/selection-routing.ts +1 -40
- package/src/lib/sfx-bus.ts +8 -32
- package/src/lib/sfx-player.test.ts +72 -1
- package/src/lib/sfx-player.ts +49 -337
- package/src/lib/snapping-mode.test.ts +23 -1
- package/src/lib/viewer-selection.test.ts +38 -10
- package/src/lib/wall-pull.test.ts +1 -1
- package/src/lib/wall-pull.ts +1 -27
- package/src/lib/wall-region-list.ts +3 -1
- package/src/lib/window-interaction.test.ts +4 -2
- package/src/store/use-editor.tsx +22 -26
- package/src/store/use-floorplan-annotation-visibility.ts +5 -5
- package/src/store/use-placement-typing.test.ts +165 -0
- package/src/store/use-placement-typing.ts +186 -0
- package/src/store/use-wall-region-handles-selection.ts +1 -2
- package/src/components/editor/__bench__/room-label-placement.bench.ts +0 -108
- package/src/components/editor/first-person/build-collider-world.test.ts +0 -596
- package/src/components/editor/first-person/build-collider-world.ts +0 -549
- package/src/components/editor/first-person/ground-holes.test.ts +0 -128
- package/src/components/editor/first-person/ground-holes.ts +0 -130
- package/src/components/editor/first-person/mechanism-targets.test.ts +0 -162
- package/src/components/editor/first-person/mechanism-targets.ts +0 -88
- package/src/components/editor/first-person/terrain-collider.test.ts +0 -132
- package/src/components/editor/first-person/terrain-collider.ts +0 -212
- package/src/components/editor/floorplan-camera-sync.test.ts +0 -249
- package/src/components/editor/floorplan-camera-sync.ts +0 -213
- package/src/components/editor/localized-html.tsx +0 -22
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +0 -236
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +0 -708
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +0 -385
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +0 -670
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +0 -89
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +0 -106
- package/src/components/editor-2d/svg-paths.ts +0 -124
- package/src/components/tools/elevator/elevator-defaults.ts +0 -8
- package/src/components/tools/elevator/elevator-tool.tsx +0 -372
- package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
- package/src/components/ui/action-menu/action-button.tsx +0 -89
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +0 -42
- package/src/components/ui/primitives/button.tsx +0 -69
- package/src/components/ui/primitives/dropdown-menu.tsx +0 -232
- package/src/components/ui/primitives/popover.tsx +0 -42
- package/src/components/ui/primitives/switch.tsx +0 -29
- package/src/components/ui/primitives/tooltip.tsx +0 -57
- package/src/components/ui/scene-loader.tsx +0 -73
- package/src/components/viewer/floorplan-compass-button.tsx +0 -67
- package/src/components/viewer/floorplan-preview-geometry.test.ts +0 -107
- package/src/components/viewer/floorplan-preview-geometry.ts +0 -281
- package/src/components/viewer/floorplan-preview-navigation.test.ts +0 -43
- package/src/components/viewer/floorplan-preview-navigation.ts +0 -62
- package/src/components/viewer/floorplan-preview-visibility.test.ts +0 -35
- package/src/components/viewer/floorplan-preview-visibility.ts +0 -19
- package/src/components/viewer/floorplan-preview.tsx +0 -1065
- package/src/components/viewer/render-settings-menu.tsx +0 -294
- package/src/components/viewer/sky-control.tsx +0 -710
- package/src/components/viewer/use-viewer-camera-navigation-sync.ts +0 -140
- package/src/components/viewer/use-viewer-ui-sounds.test.ts +0 -15
- package/src/components/viewer/use-viewer-ui-sounds.ts +0 -46
- package/src/components/viewer/viewer-controls-bar.tsx +0 -496
- package/src/components/viewer/viewer-hover-label.tsx +0 -42
- package/src/components/viewer/viewer-rooms.tsx +0 -248
- package/src/components/viewer/viewer-scene-header.tsx +0 -286
- package/src/components/viewer/viewer-selection-manager.tsx +0 -124
- package/src/components/viewer/viewer-stage-modes.test.ts +0 -38
- package/src/components/viewer/viewer-stage-modes.ts +0 -49
- package/src/components/viewer/viewer-stage-switcher.tsx +0 -97
- package/src/components/viewer/viewer-stage.test.tsx +0 -47
- package/src/components/viewer/viewer-stage.tsx +0 -221
- package/src/components/walkthrough-hud.tsx +0 -193
- package/src/hooks/use-mobile.ts +0 -19
- package/src/i18n/level-name.ts +0 -39
- package/src/i18n/scene-theme-name.ts +0 -16
- package/src/lib/below-grade.ts +0 -133
- package/src/lib/camera-pose.test.ts +0 -237
- package/src/lib/camera-pose.ts +0 -172
- package/src/lib/door-interaction.ts +0 -102
- package/src/lib/floorplan/floorplan-extension.test.ts +0 -42
- package/src/lib/floorplan/floorplan-readonly.test.ts +0 -47
- package/src/lib/floorplan/floorplan-readonly.ts +0 -95
- package/src/lib/floorplan/marks.test.ts +0 -213
- package/src/lib/floorplan/marks.ts +0 -238
- package/src/lib/measurement-label.ts +0 -68
- package/src/lib/measurements.ts +0 -225
- package/src/lib/node-display-label.ts +0 -37
- package/src/lib/room-assembly-overlay.test.ts +0 -275
- package/src/lib/room-assembly-overlay.ts +0 -265
- package/src/lib/room-selection.ts +0 -417
- package/src/lib/scene-bounds.test.ts +0 -183
- package/src/lib/scene-bounds.ts +0 -169
- package/src/lib/typing-target.ts +0 -9
- package/src/lib/viewer-selection.ts +0 -595
- package/src/lib/window-interaction.ts +0 -142
- package/src/lib/zone-content.ts +0 -133
- package/src/store/camera-pose-store.test.ts +0 -29
- package/src/store/camera-pose-store.ts +0 -27
- package/src/store/navigation-sync-pose-store.test.ts +0 -34
- package/src/store/navigation-sync-pose-store.ts +0 -27
- package/src/store/use-audio.test.ts +0 -146
- package/src/store/use-audio.tsx +0 -169
- package/src/store/use-first-person-hud.ts +0 -40
- package/src/store/use-follow-pascal.test.ts +0 -62
- package/src/store/use-follow-pascal.ts +0 -53
|
@@ -1,34 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import type
|
|
3
|
-
import { ZoneNode } from '@pascal-app/core/schema'
|
|
4
|
-
import { Object3D, PerspectiveCamera, Vector3 } from 'three'
|
|
5
|
-
import type { RoomSelectionRecord } from '../../lib/room-selection'
|
|
6
|
-
import {
|
|
7
|
-
candidates3D,
|
|
8
|
-
DeclutterClock,
|
|
9
|
-
formatRoomArea,
|
|
10
|
-
type LabelRect,
|
|
11
|
-
LabelReveal,
|
|
12
|
-
measureFullPill,
|
|
13
|
-
PILL_GAP,
|
|
14
|
-
type PillCandidate,
|
|
15
|
-
type PillPlacement,
|
|
16
|
-
PillPlacer,
|
|
17
|
-
type PillSize,
|
|
18
|
-
pillRect,
|
|
19
|
-
placedRect,
|
|
20
|
-
placePills,
|
|
21
|
-
REVEAL_DWELL_MS,
|
|
22
|
-
REVEAL_GRACE_MS,
|
|
23
|
-
RoomAnchors,
|
|
24
|
-
type RoomHit,
|
|
25
|
-
type RoomLabelEntry,
|
|
26
|
-
type RoomLabelVisibilityState,
|
|
27
|
-
roomAt,
|
|
28
|
-
roomLabelEntries,
|
|
29
|
-
roomLabelsVisible,
|
|
30
|
-
suppressedBy,
|
|
31
|
-
} from './room-labels'
|
|
2
|
+
import { editorRoomLabelsVisible, type RoomLabelVisibilityState } from './room-labels'
|
|
32
3
|
|
|
33
4
|
const resting: RoomLabelVisibilityState = {
|
|
34
5
|
phase: 'building',
|
|
@@ -42,809 +13,24 @@ const resting: RoomLabelVisibilityState = {
|
|
|
42
13
|
isThumbnailCapture: false,
|
|
43
14
|
}
|
|
44
15
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
/** Timers on a clock the test advances by hand. */
|
|
48
|
-
function manualClock() {
|
|
49
|
-
let now = 0
|
|
50
|
-
let nextId = 1
|
|
51
|
-
const due = new Map<number, { at: number; run: () => void }>()
|
|
52
|
-
return {
|
|
53
|
-
schedule: (run: () => void, ms: number) => {
|
|
54
|
-
const id = nextId++
|
|
55
|
-
due.set(id, { at: now + ms, run })
|
|
56
|
-
return id as unknown as Timer
|
|
57
|
-
},
|
|
58
|
-
cancel: (timer: Timer) => {
|
|
59
|
-
due.delete(timer as unknown as number)
|
|
60
|
-
},
|
|
61
|
-
pending: () => due.size,
|
|
62
|
-
advance(ms: number) {
|
|
63
|
-
const end = now + ms
|
|
64
|
-
for (;;) {
|
|
65
|
-
const [id, timer] = [...due].sort((a, b) => a[1].at - b[1].at)[0] ?? []
|
|
66
|
-
if (id === undefined || !timer || timer.at > end) break
|
|
67
|
-
due.delete(id)
|
|
68
|
-
now = timer.at
|
|
69
|
-
timer.run()
|
|
70
|
-
}
|
|
71
|
-
now = end
|
|
72
|
-
},
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const square = (cx: number, cy: number, side: number): Polygon => ({
|
|
77
|
-
outer: [
|
|
78
|
-
[cx - side / 2, cy - side / 2],
|
|
79
|
-
[cx + side / 2, cy - side / 2],
|
|
80
|
-
[cx + side / 2, cy + side / 2],
|
|
81
|
-
[cx - side / 2, cy + side / 2],
|
|
82
|
-
],
|
|
83
|
-
holes: [],
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
describe('room label pills', () => {
|
|
16
|
+
describe('room label pills in the editor', () => {
|
|
87
17
|
test('show on the resting level, with only the site, building or level picked', () => {
|
|
88
|
-
expect(
|
|
89
|
-
|
|
90
|
-
expect(roomLabelsVisible({ ...resting, selectedTypes: [type] })).toBe(true)
|
|
91
|
-
}
|
|
18
|
+
expect(editorRoomLabelsVisible(resting)).toBe(true)
|
|
19
|
+
expect(editorRoomLabelsVisible({ ...resting, selectedTypes: ['level'] })).toBe(true)
|
|
92
20
|
})
|
|
93
21
|
|
|
94
|
-
test('step back for
|
|
22
|
+
test('step back for a kept room, a tool, a gesture, site, a capture, or the viewer rule', () => {
|
|
95
23
|
for (const change of [
|
|
96
|
-
{ selectedTypes: ['wall'] },
|
|
97
|
-
{ selectedTypes: ['level', 'item'] },
|
|
98
|
-
{ selectedTypes: [undefined] },
|
|
99
24
|
{ room: { levelId: 'level_1', zoneId: 'zone_1' } },
|
|
100
|
-
{ zoneId: 'zone_1' },
|
|
101
|
-
{ focusedUnitId: 'unit_1' },
|
|
102
25
|
{ mode: 'build' },
|
|
103
26
|
{ scopeIdle: false },
|
|
104
27
|
{ phase: 'site' },
|
|
105
28
|
{ isCaptureMode: true },
|
|
106
29
|
{ isThumbnailCapture: true },
|
|
30
|
+
{ zoneId: 'zone_1' },
|
|
31
|
+
{ selectedTypes: ['wall'] },
|
|
107
32
|
] satisfies Partial<RoomLabelVisibilityState>[]) {
|
|
108
|
-
expect(
|
|
109
|
-
}
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
test('read the area in the active unit system', () => {
|
|
113
|
-
expect(formatRoomArea(33, 'metric')).toBe('33.0 m²')
|
|
114
|
-
expect(formatRoomArea(10, 'imperial')).toBe('107.6 ft²')
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
test('a pill is placed centred on where its anchor lands', () => {
|
|
118
|
-
expect(pillRect(100, 50, [80, 20])).toEqual({ left: 60, top: 40, right: 140, bottom: 60 })
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
test('a pill showing less measures its whole label, and its width with the name alone', () => {
|
|
122
|
-
const areaLabel = {}
|
|
123
|
-
const element = {
|
|
124
|
-
dataset: { roomLabelTier: 'dot' },
|
|
125
|
-
querySelector: () => areaLabel,
|
|
126
|
-
} as unknown as HTMLElement
|
|
127
|
-
let width = 161.5
|
|
128
|
-
const read = (target: Element) => {
|
|
129
|
-
if (target === areaLabel) return { width: '40px', height: '16px', columnGap: 'normal' }
|
|
130
|
-
const dot = 'roomLabelTier' in (target as HTMLElement).dataset
|
|
131
|
-
return {
|
|
132
|
-
width: dot ? '16px' : `${width}px`,
|
|
133
|
-
height: dot ? '16px' : '26px',
|
|
134
|
-
columnGap: '6px',
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
expect(measureFullPill(element, read)).toEqual([161.5, 26, 115.5])
|
|
138
|
-
width = 176.25
|
|
139
|
-
expect(measureFullPill(element, read)).toEqual([176.25, 26, 130.25])
|
|
140
|
-
expect(element.dataset.roomLabelTier).toBe('dot')
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
test('invisible rooms and drawn zones do not reserve space for a pill', () => {
|
|
144
|
-
const polygon: [number, number][] = [
|
|
145
|
-
[0, 0],
|
|
146
|
-
[3, 0],
|
|
147
|
-
[3, 3],
|
|
148
|
-
[0, 3],
|
|
149
|
-
]
|
|
150
|
-
const room = {
|
|
151
|
-
zoneId: 'zone_room',
|
|
152
|
-
polygon,
|
|
153
|
-
holes: [],
|
|
154
|
-
area: 9,
|
|
155
|
-
} as unknown as RoomSelectionRecord
|
|
156
|
-
const zones = [
|
|
157
|
-
ZoneNode.parse({ id: 'zone_room', name: 'Room', polygon, visible: false }),
|
|
158
|
-
ZoneNode.parse({ id: 'zone_hidden', name: 'Hidden', polygon, visible: false }),
|
|
159
|
-
ZoneNode.parse({ id: 'zone_visible', name: 'Visible', polygon }),
|
|
160
|
-
]
|
|
161
|
-
expect(roomLabelEntries([room], zones).map((entry) => entry.zoneId)).toEqual(['zone_visible'])
|
|
162
|
-
expect(roomLabelEntries([room], [{ ...zones[0]!, visible: true }])[0]).toEqual({
|
|
163
|
-
zoneId: room.zoneId,
|
|
164
|
-
polygon,
|
|
165
|
-
holes: [],
|
|
166
|
-
area: 9,
|
|
167
|
-
})
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
test('a zone drawn inside a room is a hole in it, so their labels and the pointer agree', () => {
|
|
171
|
-
const living = square(5, 5, 10)
|
|
172
|
-
const nook = square(7, 7, 3)
|
|
173
|
-
const entries = roomLabelEntries(
|
|
174
|
-
[],
|
|
175
|
-
[
|
|
176
|
-
ZoneNode.parse({ id: 'zone_living', name: 'Living room', polygon: living.outer }),
|
|
177
|
-
ZoneNode.parse({ id: 'zone_nook', name: 'Reading nook', polygon: nook.outer }),
|
|
178
|
-
ZoneNode.parse({ id: 'zone_hall', name: 'Hall', polygon: square(15, 5, 10).outer }),
|
|
179
|
-
],
|
|
180
|
-
)
|
|
181
|
-
const [livingEntry, nookEntry, hallEntry] = entries
|
|
182
|
-
expect(livingEntry!.holes).toEqual([nook.outer])
|
|
183
|
-
expect(livingEntry!.area).toBe(100)
|
|
184
|
-
expect(nookEntry!.holes).toEqual([])
|
|
185
|
-
expect(hallEntry!.holes).toEqual([])
|
|
186
|
-
// At 40 px per metre, the living room's label next to the nook moves off it.
|
|
187
|
-
const onScreen = (zoneId: string, x: number, y: number): PillCandidate => {
|
|
188
|
-
const entry = entries.find((candidate) => candidate.zoneId === zoneId)!
|
|
189
|
-
const scale = ([px, py]: [number, number]): [number, number] => [px * 40, py * 40]
|
|
190
|
-
return {
|
|
191
|
-
id: zoneId,
|
|
192
|
-
x: x * 40,
|
|
193
|
-
y: y * 40,
|
|
194
|
-
size: [80, 24, 50],
|
|
195
|
-
area: entry.area,
|
|
196
|
-
outline: { outer: entry.polygon.map(scale), holes: entry.holes.map((h) => h.map(scale)) },
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
const placements = placePills([onScreen('zone_living', 7, 8.7), onScreen('zone_nook', 7, 7)], {
|
|
200
|
-
width: 1000,
|
|
201
|
-
height: 1000,
|
|
202
|
-
})
|
|
203
|
-
expect(placements.get('zone_living')).toEqual({ tier: 'full', dy: 8 })
|
|
204
|
-
expect(placements.get('zone_nook')).toEqual({ tier: 'full', dy: 0 })
|
|
205
|
-
expect(roomAt(entries, () => [7, 7, 0])).toBe('zone_nook')
|
|
206
|
-
expect(roomAt(entries, () => [2, 2, 0])).toBe('zone_living')
|
|
207
|
-
})
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
describe('pill placement', () => {
|
|
211
|
-
const pane = { width: 1000, height: 1000 }
|
|
212
|
-
const everywhere = square(0, 0, 1e5)
|
|
213
|
-
const pill = (
|
|
214
|
-
id: string,
|
|
215
|
-
x: number,
|
|
216
|
-
y: number,
|
|
217
|
-
area: number,
|
|
218
|
-
size: PillSize = [120, 24, 70],
|
|
219
|
-
outline: Polygon = everywhere,
|
|
220
|
-
): PillCandidate => ({ id, x, y, size, area, outline })
|
|
221
|
-
const near = (a: LabelRect, b: LabelRect) =>
|
|
222
|
-
a.left < b.right + PILL_GAP &&
|
|
223
|
-
b.left < a.right + PILL_GAP &&
|
|
224
|
-
a.top < b.bottom + PILL_GAP &&
|
|
225
|
-
b.top < a.bottom + PILL_GAP
|
|
226
|
-
|
|
227
|
-
test('apart, every pill shows in full at its anchor', () => {
|
|
228
|
-
const placements = placePills([pill('a', 100, 100, 20), pill('b', 400, 100, 10)], pane)
|
|
229
|
-
expect([...placements.values()]).toEqual([
|
|
230
|
-
{ tier: 'full', dy: 0 },
|
|
231
|
-
{ tier: 'full', dy: 0 },
|
|
232
|
-
])
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
test('two overlapping pills: the larger room keeps its place, the other moves the least it can', () => {
|
|
236
|
-
const big = pill('big', 100, 100, 30)
|
|
237
|
-
const small = pill('small', 110, 106, 10)
|
|
238
|
-
const placements = placePills([small, big], pane)
|
|
239
|
-
expect(placements.get('big')).toEqual({ tier: 'full', dy: 0 })
|
|
240
|
-
// 24 px tall, 6 px apart, 4 px gap: 24 px down is the nearest clear slot (up needs 34).
|
|
241
|
-
expect(placements.get('small')).toEqual({ tier: 'full', dy: 24 })
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
test('how much of a label shows follows how large its room is on screen', () => {
|
|
245
|
-
// A 4 m square room at 40, 25, 10 and 3 px per metre.
|
|
246
|
-
const tiers = [40, 25, 10, 3].map(
|
|
247
|
-
(scale) =>
|
|
248
|
-
placePills(
|
|
249
|
-
[pill('room', 500, 500, 16, [120, 24, 70], square(500, 500, 4 * scale))],
|
|
250
|
-
pane,
|
|
251
|
-
).get('room')!.tier,
|
|
252
|
-
)
|
|
253
|
-
expect(tiers).toEqual(['full', 'name', 'dot', 'hidden'])
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
test('a thin room at an angle gets its dot, however large its bounding box', () => {
|
|
257
|
-
// A 1.2 m corridor 8 m long at 45°, at 40 px per metre.
|
|
258
|
-
const along = [160 / Math.SQRT2, 160 / Math.SQRT2]
|
|
259
|
-
const across = [-24 / Math.SQRT2, 24 / Math.SQRT2]
|
|
260
|
-
const corner = (a: number, b: number): [number, number] => [
|
|
261
|
-
500 + a * along[0]! + b * across[0]!,
|
|
262
|
-
500 + a * along[1]! + b * across[1]!,
|
|
263
|
-
]
|
|
264
|
-
const corridor = {
|
|
265
|
-
outer: [corner(-1, -1), corner(1, -1), corner(1, 1), corner(-1, 1)],
|
|
266
|
-
holes: [],
|
|
267
|
-
}
|
|
268
|
-
expect(
|
|
269
|
-
placePills([pill('hall', 500, 500, 9.6, [120, 24, 70], corridor)], pane).get('hall'),
|
|
270
|
-
).toEqual({ tier: 'dot', dy: 0 })
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
test('a label next to a hole in its room moves clear of the hole', () => {
|
|
274
|
-
const atrium = { ...square(200, 200, 200), holes: [square(200, 200, 80).outer] }
|
|
275
|
-
// At its anchor the pill's bottom edge crosses the hole's top edge (y 160).
|
|
276
|
-
expect(
|
|
277
|
-
placePills([pill('room', 200, 150, 34, [120, 24, 70], atrium)], pane).get('room'),
|
|
278
|
-
).toEqual({
|
|
279
|
-
tier: 'full',
|
|
280
|
-
dy: -4,
|
|
281
|
-
})
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
test('a label never leaves the pane: near its edge it shows less', () => {
|
|
285
|
-
const placements = placePills([pill('a', 50, 500, 10), pill('b', 5, 500, 10)], pane)
|
|
286
|
-
expect(placements.get('a')).toEqual({ tier: 'name', dy: 0 })
|
|
287
|
-
expect(placements.get('b')).toEqual({ tier: 'hidden', dy: 0 })
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
test('a pill that has to move keeps to the side it took last time; one with room goes back', () => {
|
|
291
|
-
const bar = pill('bar', 100, 100, 30, [120, 8, 60])
|
|
292
|
-
const moved = pill('moved', 100, 100, 10)
|
|
293
|
-
expect(placePills([bar, moved], pane).get('moved')).toEqual({ tier: 'full', dy: -20 })
|
|
294
|
-
const previous = new Map<string, PillPlacement>([['moved', { tier: 'full', dy: 20 }]])
|
|
295
|
-
expect(placePills([bar, moved], pane, previous).get('moved')).toEqual({ tier: 'full', dy: 20 })
|
|
296
|
-
expect(placePills([moved], pane, previous).get('moved')).toEqual({ tier: 'full', dy: 0 })
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
test('the order is fixed by room size and id, not by input order', () => {
|
|
300
|
-
const candidates = [pill('b', 100, 100, 10), pill('a', 105, 100, 10), pill('c', 110, 100, 10)]
|
|
301
|
-
const first = placePills(candidates, pane)
|
|
302
|
-
const shuffled = placePills([candidates[2]!, candidates[0]!, candidates[1]!], pane)
|
|
303
|
-
expect(shuffled).toEqual(first)
|
|
304
|
-
expect(first.get('a')).toEqual({ tier: 'full', dy: 0 })
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
test('placed labels never come within the gap of each other, and each stays in its room', () => {
|
|
308
|
-
let seed = 7
|
|
309
|
-
const random = () => {
|
|
310
|
-
seed = (seed * 1_103_515_245 + 12_345) % 2 ** 31
|
|
311
|
-
return seed / 2 ** 31
|
|
312
|
-
}
|
|
313
|
-
// A plan of 8 × 5 rooms of random widths and depths, filling the pane.
|
|
314
|
-
const cuts = (count: number, length: number) => {
|
|
315
|
-
const weights = Array.from({ length: count }, () => 0.2 + random())
|
|
316
|
-
const total = weights.reduce((sum, weight) => sum + weight, 0)
|
|
317
|
-
let at = 0
|
|
318
|
-
return [0, ...weights.map((weight) => (at += (weight / total) * length))]
|
|
319
|
-
}
|
|
320
|
-
const xs = cuts(8, 1200)
|
|
321
|
-
const ys = cuts(5, 800)
|
|
322
|
-
const rooms = xs.slice(1).flatMap((right, i) =>
|
|
323
|
-
ys.slice(1).map((bottom, j) => {
|
|
324
|
-
const left = xs[i]!
|
|
325
|
-
const top = ys[j]!
|
|
326
|
-
const outline: Polygon = {
|
|
327
|
-
outer: [
|
|
328
|
-
[left, top],
|
|
329
|
-
[right, top],
|
|
330
|
-
[right, bottom],
|
|
331
|
-
[left, bottom],
|
|
332
|
-
],
|
|
333
|
-
holes: [],
|
|
334
|
-
}
|
|
335
|
-
const width = 40 + random() * 140
|
|
336
|
-
return {
|
|
337
|
-
box: { left, top, right, bottom },
|
|
338
|
-
candidate: pill(
|
|
339
|
-
`r${i}-${j}`,
|
|
340
|
-
left + (right - left) * (0.3 + random() * 0.4),
|
|
341
|
-
top + (bottom - top) * (0.3 + random() * 0.4),
|
|
342
|
-
(right - left) * (bottom - top),
|
|
343
|
-
[width, 24, width * 0.6],
|
|
344
|
-
outline,
|
|
345
|
-
),
|
|
346
|
-
}
|
|
347
|
-
}),
|
|
348
|
-
)
|
|
349
|
-
const placements = placePills(
|
|
350
|
-
rooms.map((room) => room.candidate),
|
|
351
|
-
{ width: 1200, height: 800 },
|
|
352
|
-
)
|
|
353
|
-
const placed = rooms.flatMap(({ box, candidate }) => {
|
|
354
|
-
const rect = placedRect(candidate, placements.get(candidate.id)!)
|
|
355
|
-
return rect ? [{ box, rect }] : []
|
|
356
|
-
})
|
|
357
|
-
expect(placed.length).toBeGreaterThan(30)
|
|
358
|
-
for (const { box, rect } of placed) {
|
|
359
|
-
expect(rect.left).toBeGreaterThanOrEqual(box.left)
|
|
360
|
-
expect(rect.right).toBeLessThanOrEqual(box.right)
|
|
361
|
-
expect(rect.top).toBeGreaterThanOrEqual(box.top)
|
|
362
|
-
expect(rect.bottom).toBeLessThanOrEqual(box.bottom)
|
|
363
|
-
}
|
|
364
|
-
for (let i = 0; i < placed.length; i++) {
|
|
365
|
-
for (let j = 0; j < i; j++) expect(near(placed[i]!.rect, placed[j]!.rect)).toBe(false)
|
|
366
|
-
}
|
|
367
|
-
})
|
|
368
|
-
})
|
|
369
|
-
|
|
370
|
-
describe('the room under the pointer', () => {
|
|
371
|
-
const room = (zoneId: string, polygon: Polygon, area: number): RoomLabelEntry => ({
|
|
372
|
-
zoneId,
|
|
373
|
-
polygon: polygon.outer,
|
|
374
|
-
holes: polygon.holes,
|
|
375
|
-
area,
|
|
376
|
-
})
|
|
377
|
-
const hall = room('hall', square(5, 5, 10), 100)
|
|
378
|
-
const nook = room('nook', square(2, 2, 3), 9)
|
|
379
|
-
const atrium = room('atrium', { ...square(25, 5, 10), holes: [square(25, 5, 4).outer] }, 84)
|
|
380
|
-
|
|
381
|
-
test('is the smallest room holding the point: a zone drawn inside the room it stands in', () => {
|
|
382
|
-
const at = (x: number, y: number) => roomAt([hall, nook, atrium], () => [x, y, 0])
|
|
383
|
-
expect(at(2, 2)).toBe('nook')
|
|
384
|
-
expect(at(8, 8)).toBe('hall')
|
|
385
|
-
expect(at(22, 2)).toBe('atrium')
|
|
386
|
-
})
|
|
387
|
-
|
|
388
|
-
test('is no room outside every room or inside a hole', () => {
|
|
389
|
-
expect(roomAt([hall, atrium], () => [15, 5, 0])).toBeNull()
|
|
390
|
-
expect(roomAt([hall, atrium], () => [25, 5, 0])).toBeNull()
|
|
391
|
-
})
|
|
392
|
-
|
|
393
|
-
test('only tests a room where the pointer meets its own plane', () => {
|
|
394
|
-
expect(roomAt([hall, nook], (entry) => (entry === nook ? null : [2, 2, 0]))).toBe('hall')
|
|
395
|
-
})
|
|
396
|
-
|
|
397
|
-
test('is the floor the pointer reaches first, before a smaller one further down', () => {
|
|
398
|
-
// A raised floor over the whole hall, and a small room on the floor below it.
|
|
399
|
-
const raised = room('raised', square(5, 5, 10), 100)
|
|
400
|
-
const below = room('below', square(5, 5, 2), 4)
|
|
401
|
-
const hits = new Map<RoomLabelEntry, RoomHit>([
|
|
402
|
-
[raised, [5, 5, 3]],
|
|
403
|
-
[below, [5, 5, 6]],
|
|
404
|
-
])
|
|
405
|
-
expect(roomAt([raised, below], (entry) => hits.get(entry) ?? null)).toBe('raised')
|
|
406
|
-
// Met at the same distance, they are one floor: the smaller wins.
|
|
407
|
-
hits.set(raised, [5, 5, 6 + 1e-4])
|
|
408
|
-
expect(roomAt([raised, below], (entry) => hits.get(entry) ?? null)).toBe('below')
|
|
409
|
-
})
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
describe('pills in 3D', () => {
|
|
413
|
-
test('a corridor running behind the camera still holds its pill to its width', () => {
|
|
414
|
-
// 0.3 m wide and 20 m long, half of it behind a camera standing in it.
|
|
415
|
-
const corridor: RoomLabelEntry = {
|
|
416
|
-
zoneId: 'corridor',
|
|
417
|
-
polygon: [
|
|
418
|
-
[-0.15, -10],
|
|
419
|
-
[0.15, -10],
|
|
420
|
-
[0.15, 10],
|
|
421
|
-
[-0.15, 10],
|
|
422
|
-
],
|
|
423
|
-
holes: [],
|
|
424
|
-
area: 6,
|
|
425
|
-
}
|
|
426
|
-
const camera = new PerspectiveCamera(50, 1440 / 900, 0.1, 1000)
|
|
427
|
-
camera.position.set(0, 1.6, 0)
|
|
428
|
-
camera.updateMatrixWorld()
|
|
429
|
-
const level = new Object3D()
|
|
430
|
-
level.updateMatrixWorld()
|
|
431
|
-
const size = { width: 1440, height: 900 }
|
|
432
|
-
const [candidate] = candidates3D(
|
|
433
|
-
[corridor],
|
|
434
|
-
new Map([['corridor', new Vector3(0, 1.25, -5)]]),
|
|
435
|
-
new Map<string, PillSize>([['corridor', [120, 24, 70]]]),
|
|
436
|
-
camera,
|
|
437
|
-
level,
|
|
438
|
-
size,
|
|
439
|
-
)
|
|
440
|
-
expect(
|
|
441
|
-
candidate!.outline.outer.every(([x, y]) => Number.isFinite(x) && Number.isFinite(y)),
|
|
442
|
-
).toBe(true)
|
|
443
|
-
// About 58 px wide where the pill would sit: only its dot fits.
|
|
444
|
-
expect(placePills([candidate!], size).get('corridor')).toEqual({ tier: 'dot', dy: 0 })
|
|
445
|
-
})
|
|
446
|
-
})
|
|
447
|
-
|
|
448
|
-
describe('when pills re-read their overlaps', () => {
|
|
449
|
-
// Camera damping after a zoom: each frame moves the camera by a shrinking
|
|
450
|
-
// fraction of a millimetre for a second or more after it visibly stops.
|
|
451
|
-
const settling = (frame: number) => {
|
|
452
|
-
const view = new Float64Array(50)
|
|
453
|
-
view[12] = 30 + 1e-5 * 0.8 ** frame
|
|
454
|
-
view[48] = 1500
|
|
455
|
-
view[49] = 900
|
|
456
|
-
return view
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
test('once the camera has visibly stopped, not once damping stops nudging it', () => {
|
|
460
|
-
const clock = new DeclutterClock()
|
|
461
|
-
const frame = 1 / 60
|
|
462
|
-
let firstRead = -1
|
|
463
|
-
for (let i = 0; i < 120; i++) {
|
|
464
|
-
if (clock.shouldRead(settling(i), i * frame) && i > 0) {
|
|
465
|
-
firstRead = i
|
|
466
|
-
break
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
// Within the rest delay (0.12 s ≈ 7 frames at 60 fps), not two seconds later.
|
|
470
|
-
expect(firstRead).toBeGreaterThan(0)
|
|
471
|
-
expect(firstRead * frame).toBeLessThan(0.2)
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
test('once per rest, not while moving, and again when the pills change at rest', () => {
|
|
475
|
-
const clock = new DeclutterClock()
|
|
476
|
-
const at = (x: number) => {
|
|
477
|
-
const view = new Float64Array(50)
|
|
478
|
-
view[12] = x
|
|
479
|
-
return view
|
|
480
|
-
}
|
|
481
|
-
expect(clock.shouldRead(at(0), 0)).toBe(false)
|
|
482
|
-
expect(clock.shouldRead(at(0), 0.2)).toBe(true)
|
|
483
|
-
expect(clock.atRest).toBe(true)
|
|
484
|
-
expect(clock.shouldRead(at(0), 0.4)).toBe(false)
|
|
485
|
-
// Moving: pills ride their rooms, nothing is re-placed until it rests.
|
|
486
|
-
const moving = [0.5, 0.55, 0.6, 0.65, 0.7].map((t) => clock.shouldRead(at(t), t))
|
|
487
|
-
expect(moving.some(Boolean)).toBe(false)
|
|
488
|
-
expect(clock.atRest).toBe(false)
|
|
489
|
-
expect(clock.shouldRead(at(0.7), 0.85)).toBe(true)
|
|
490
|
-
clock.invalidate()
|
|
491
|
-
expect(clock.shouldRead(at(0.7), 0.86)).toBe(true)
|
|
492
|
-
})
|
|
493
|
-
|
|
494
|
-
test('a room whose anchor moves has the pills placed again', () => {
|
|
495
|
-
const anchors = new RoomAnchors()
|
|
496
|
-
let moves = 0
|
|
497
|
-
anchors.onMove = () => moves++
|
|
498
|
-
anchors.set('kitchen', new Vector3(1, 1.25, 2))
|
|
499
|
-
anchors.set('kitchen', new Vector3(1, 1.25, 2))
|
|
500
|
-
expect(moves).toBe(1)
|
|
501
|
-
anchors.set('kitchen', new Vector3(1, 1.75, 2))
|
|
502
|
-
expect(moves).toBe(2)
|
|
503
|
-
})
|
|
504
|
-
})
|
|
505
|
-
|
|
506
|
-
describe('applying placements', () => {
|
|
507
|
-
const element = () =>
|
|
508
|
-
({
|
|
509
|
-
dataset: {} as Record<string, string>,
|
|
510
|
-
style: { translate: '' },
|
|
511
|
-
}) as unknown as HTMLElement
|
|
512
|
-
function setup() {
|
|
513
|
-
const elements = new Map([
|
|
514
|
-
['near', element()],
|
|
515
|
-
['far', element()],
|
|
516
|
-
])
|
|
517
|
-
const clock = manualClock()
|
|
518
|
-
const changes: ReadonlySet<string>[] = []
|
|
519
|
-
const placer = new PillPlacer(
|
|
520
|
-
elements,
|
|
521
|
-
(gone) => changes.push(gone),
|
|
522
|
-
clock.schedule,
|
|
523
|
-
clock.cancel,
|
|
524
|
-
)
|
|
525
|
-
return { elements, clock, changes, placer }
|
|
526
|
-
}
|
|
527
|
-
const has = (element: HTMLElement | undefined, flag: string) => flag in element!.dataset
|
|
528
|
-
const full = (dy = 0): PillPlacement => ({ tier: 'full', dy })
|
|
529
|
-
const hidden: PillPlacement = { tier: 'hidden', dy: 0 }
|
|
530
|
-
const placements = (entries: [string, PillPlacement][]) => new Map(entries)
|
|
531
|
-
|
|
532
|
-
test('a nudge is a translate, a tier is a flag, both undone when placed back', () => {
|
|
533
|
-
const { elements, placer } = setup()
|
|
534
|
-
placer.apply(
|
|
535
|
-
placements([
|
|
536
|
-
['near', full(-8)],
|
|
537
|
-
['far', { tier: 'dot', dy: 0 }],
|
|
538
|
-
]),
|
|
539
|
-
180,
|
|
540
|
-
)
|
|
541
|
-
expect(elements.get('near')!.style.translate).toBe('0 -8px')
|
|
542
|
-
expect(elements.get('far')!.dataset.roomLabelTier).toBe('dot')
|
|
543
|
-
placer.apply(
|
|
544
|
-
placements([
|
|
545
|
-
['near', full()],
|
|
546
|
-
['far', { tier: 'name', dy: 0 }],
|
|
547
|
-
]),
|
|
548
|
-
180,
|
|
549
|
-
)
|
|
550
|
-
expect(elements.get('near')!.style.translate).toBe('')
|
|
551
|
-
expect(elements.get('far')!.dataset.roomLabelTier).toBe('name')
|
|
552
|
-
placer.apply(
|
|
553
|
-
placements([
|
|
554
|
-
['near', full()],
|
|
555
|
-
['far', full()],
|
|
556
|
-
]),
|
|
557
|
-
180,
|
|
558
|
-
)
|
|
559
|
-
expect(has(elements.get('far'), 'roomLabelTier')).toBe(false)
|
|
560
|
-
})
|
|
561
|
-
|
|
562
|
-
test('a hidden pill fades out without taking the pointer, then leaves the DOM', () => {
|
|
563
|
-
const { elements, clock, changes, placer } = setup()
|
|
564
|
-
placer.apply(
|
|
565
|
-
placements([
|
|
566
|
-
['near', full()],
|
|
567
|
-
['far', hidden],
|
|
568
|
-
]),
|
|
569
|
-
180,
|
|
570
|
-
)
|
|
571
|
-
expect(has(elements.get('far'), 'roomLabelHidden')).toBe(true)
|
|
572
|
-
clock.advance(179)
|
|
573
|
-
expect(changes).toEqual([])
|
|
574
|
-
clock.advance(1)
|
|
575
|
-
expect(placer.gone).toEqual(new Set(['far']))
|
|
576
|
-
})
|
|
577
|
-
|
|
578
|
-
test('a pill given room again mounts in its new place, or turns back mid-fade', () => {
|
|
579
|
-
const { elements, clock, placer } = setup()
|
|
580
|
-
placer.apply(placements([['far', hidden]]), 180)
|
|
581
|
-
clock.advance(180)
|
|
582
|
-
const far = elements.get('far')!
|
|
583
|
-
elements.delete('far')
|
|
584
|
-
placer.apply(placements([['far', full(12)]]), 180)
|
|
585
|
-
expect(placer.gone).toEqual(new Set())
|
|
586
|
-
placer.mount('far', far)
|
|
587
|
-
expect(far.style.translate).toBe('0 12px')
|
|
588
|
-
expect(has(far, 'roomLabelHidden')).toBe(false)
|
|
589
|
-
|
|
590
|
-
const again = setup()
|
|
591
|
-
again.placer.apply(placements([['far', hidden]]), 180)
|
|
592
|
-
again.placer.apply(placements([['far', full()]]), 180)
|
|
593
|
-
again.clock.advance(500)
|
|
594
|
-
expect(again.placer.gone).toEqual(new Set())
|
|
595
|
-
expect(has(again.elements.get('far'), 'roomLabelHidden')).toBe(false)
|
|
596
|
-
})
|
|
597
|
-
|
|
598
|
-
test('a pill no pass has placed waits hidden, still mounted, until one places it', () => {
|
|
599
|
-
const { elements, clock, placer } = setup()
|
|
600
|
-
const near = elements.get('near')!
|
|
601
|
-
placer.mount('near', near)
|
|
602
|
-
expect(has(near, 'roomLabelHidden')).toBe(true)
|
|
603
|
-
// Not measured yet, so not a candidate: it stays hidden and is not removed.
|
|
604
|
-
placer.apply(placements([['far', full()]]), 180)
|
|
605
|
-
expect(has(near, 'roomLabelHidden')).toBe(true)
|
|
606
|
-
expect(clock.pending()).toBe(0)
|
|
607
|
-
placer.apply(
|
|
608
|
-
placements([
|
|
609
|
-
['near', full()],
|
|
610
|
-
['far', full()],
|
|
611
|
-
]),
|
|
612
|
-
180,
|
|
613
|
-
)
|
|
614
|
-
expect(has(near, 'roomLabelHidden')).toBe(false)
|
|
615
|
-
// Its anchor leaves the screen: hidden again, and still mounted.
|
|
616
|
-
placer.apply(placements([['far', full()]]), 180)
|
|
617
|
-
expect(has(near, 'roomLabelHidden')).toBe(true)
|
|
618
|
-
clock.advance(500)
|
|
619
|
-
expect(placer.gone).toEqual(new Set())
|
|
620
|
-
})
|
|
621
|
-
|
|
622
|
-
test('hidden pills can refresh their text measurements without becoming visible', () => {
|
|
623
|
-
const { elements, clock, placer } = setup()
|
|
624
|
-
placer.apply(placements([['far', hidden]]), 180)
|
|
625
|
-
clock.advance(180)
|
|
626
|
-
elements.delete('far')
|
|
627
|
-
placer.remeasure()
|
|
628
|
-
expect(placer.gone.size).toBe(0)
|
|
629
|
-
const fresh = element()
|
|
630
|
-
elements.set('far', fresh)
|
|
631
|
-
placer.mount('far', fresh)
|
|
632
|
-
expect(has(fresh, 'roomLabelHidden')).toBe(true)
|
|
633
|
-
placer.apply(placements([['far', full()]]), 180)
|
|
634
|
-
expect(has(fresh, 'roomLabelHidden')).toBe(false)
|
|
635
|
-
})
|
|
636
|
-
|
|
637
|
-
test('the whole layer fades out and back in, except pills still hidden', () => {
|
|
638
|
-
const { elements, placer } = setup()
|
|
639
|
-
placer.apply(
|
|
640
|
-
placements([
|
|
641
|
-
['near', full()],
|
|
642
|
-
['far', hidden],
|
|
643
|
-
]),
|
|
644
|
-
180,
|
|
645
|
-
)
|
|
646
|
-
placer.showAll(false)
|
|
647
|
-
expect(has(elements.get('near'), 'roomLabelHidden')).toBe(true)
|
|
648
|
-
placer.showAll(true)
|
|
649
|
-
expect(has(elements.get('near'), 'roomLabelHidden')).toBe(false)
|
|
650
|
-
expect(has(elements.get('far'), 'roomLabelHidden')).toBe(true)
|
|
651
|
-
})
|
|
652
|
-
|
|
653
|
-
test('the pills under the detail chip fade out of its way, and come back', () => {
|
|
654
|
-
const { elements, placer } = setup()
|
|
655
|
-
placer.suppress(new Set(['near', 'far']), 'far')
|
|
656
|
-
expect(has(elements.get('near'), 'roomLabelSuppressed')).toBe(true)
|
|
657
|
-
// The open room's own pill fades under its chip but stays the control.
|
|
658
|
-
expect(has(elements.get('far'), 'roomLabelSuppressed')).toBe(false)
|
|
659
|
-
expect(has(elements.get('far'), 'roomLabelOpen')).toBe(true)
|
|
660
|
-
const remounted = element()
|
|
661
|
-
placer.mount('near', remounted)
|
|
662
|
-
expect(has(remounted, 'roomLabelSuppressed')).toBe(true)
|
|
663
|
-
placer.suppress(new Set(), null)
|
|
664
|
-
expect(has(elements.get('near'), 'roomLabelSuppressed')).toBe(false)
|
|
665
|
-
expect(has(elements.get('far'), 'roomLabelOpen')).toBe(false)
|
|
666
|
-
})
|
|
667
|
-
|
|
668
|
-
test('with the default timers, a pill hiding never stops the pills after it', () => {
|
|
669
|
-
// Browser timers throw when called as a method of anything but the window.
|
|
670
|
-
const realSetTimeout = globalThis.setTimeout
|
|
671
|
-
const realClearTimeout = globalThis.clearTimeout
|
|
672
|
-
const clock = manualClock()
|
|
673
|
-
function strictSetTimeout(this: unknown, run: () => void, ms: number) {
|
|
674
|
-
if (this !== undefined && this !== globalThis) throw new TypeError('Illegal invocation')
|
|
675
|
-
return clock.schedule(run, ms)
|
|
676
|
-
}
|
|
677
|
-
function strictClearTimeout(this: unknown, timer: Timer) {
|
|
678
|
-
if (this !== undefined && this !== globalThis) throw new TypeError('Illegal invocation')
|
|
679
|
-
clock.cancel(timer)
|
|
33
|
+
expect(editorRoomLabelsVisible({ ...resting, ...change })).toBe(false)
|
|
680
34
|
}
|
|
681
|
-
globalThis.setTimeout = strictSetTimeout as unknown as typeof setTimeout
|
|
682
|
-
globalThis.clearTimeout = strictClearTimeout as unknown as typeof clearTimeout
|
|
683
|
-
try {
|
|
684
|
-
const elements = new Map([
|
|
685
|
-
['a', element()],
|
|
686
|
-
['b', element()],
|
|
687
|
-
])
|
|
688
|
-
const placer = new PillPlacer(elements, () => {})
|
|
689
|
-
placer.apply(
|
|
690
|
-
placements([
|
|
691
|
-
['a', hidden],
|
|
692
|
-
['b', full(8)],
|
|
693
|
-
]),
|
|
694
|
-
180,
|
|
695
|
-
)
|
|
696
|
-
expect(elements.get('b')!.style.translate).toBe('0 8px')
|
|
697
|
-
clock.advance(180)
|
|
698
|
-
expect(placer.gone).toEqual(new Set(['a']))
|
|
699
|
-
|
|
700
|
-
placer.apply(placements([['b', hidden]]), 180)
|
|
701
|
-
placer.apply(placements([['b', full()]]), 180)
|
|
702
|
-
clock.advance(180)
|
|
703
|
-
expect(placer.gone).toEqual(new Set(['a']))
|
|
704
|
-
expect(has(elements.get('b'), 'roomLabelHidden')).toBe(false)
|
|
705
|
-
|
|
706
|
-
const reveal = new LabelReveal(() => {})
|
|
707
|
-
reveal.pick('b')
|
|
708
|
-
reveal.pick('a')
|
|
709
|
-
reveal.clear()
|
|
710
|
-
expect(clock.pending()).toBe(0)
|
|
711
|
-
} finally {
|
|
712
|
-
globalThis.setTimeout = realSetTimeout
|
|
713
|
-
globalThis.clearTimeout = realClearTimeout
|
|
714
|
-
}
|
|
715
|
-
})
|
|
716
|
-
})
|
|
717
|
-
|
|
718
|
-
describe("a room's detail", () => {
|
|
719
|
-
function setup() {
|
|
720
|
-
const clock = manualClock()
|
|
721
|
-
const changes: (string | null)[] = []
|
|
722
|
-
const focused: boolean[] = []
|
|
723
|
-
const reveal = new LabelReveal(
|
|
724
|
-
(id, isFocused) => {
|
|
725
|
-
changes.push(id)
|
|
726
|
-
focused.push(isFocused)
|
|
727
|
-
},
|
|
728
|
-
clock.schedule,
|
|
729
|
-
clock.cancel,
|
|
730
|
-
)
|
|
731
|
-
return { clock, changes, focused, reveal }
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
test('opens at once for a pill under the pointer, after a short rest for a room under it', () => {
|
|
735
|
-
const { clock, changes, reveal } = setup()
|
|
736
|
-
reveal.hover('closet')
|
|
737
|
-
expect(changes).toEqual(['closet'])
|
|
738
|
-
reveal.hover(null)
|
|
739
|
-
clock.advance(REVEAL_GRACE_MS)
|
|
740
|
-
expect(changes).toEqual(['closet', null])
|
|
741
|
-
reveal.pick('bath')
|
|
742
|
-
clock.advance(REVEAL_DWELL_MS - 1)
|
|
743
|
-
expect(reveal.active).toBeNull()
|
|
744
|
-
clock.advance(1)
|
|
745
|
-
expect(reveal.active).toBe('bath')
|
|
746
|
-
})
|
|
747
|
-
|
|
748
|
-
test('hands over from room to room with one detail open, and opens nothing for a room passed over', () => {
|
|
749
|
-
const { clock, changes, reveal } = setup()
|
|
750
|
-
reveal.pick('hall')
|
|
751
|
-
clock.advance(REVEAL_DWELL_MS)
|
|
752
|
-
reveal.pick('bath')
|
|
753
|
-
clock.advance(REVEAL_DWELL_MS - 1)
|
|
754
|
-
expect(reveal.active).toBe('hall')
|
|
755
|
-
clock.advance(1)
|
|
756
|
-
reveal.pick('closet')
|
|
757
|
-
clock.advance(REVEAL_DWELL_MS / 2)
|
|
758
|
-
reveal.pick('bedroom')
|
|
759
|
-
clock.advance(REVEAL_DWELL_MS / 2)
|
|
760
|
-
expect(reveal.active).toBe('bath')
|
|
761
|
-
clock.advance(REVEAL_DWELL_MS / 2)
|
|
762
|
-
expect(changes).toEqual(['hall', 'bath', 'bedroom'])
|
|
763
|
-
})
|
|
764
|
-
|
|
765
|
-
test('stays a moment after the pointer leaves its room, and stays if it comes back', () => {
|
|
766
|
-
const { clock, changes, reveal } = setup()
|
|
767
|
-
reveal.pick('bath')
|
|
768
|
-
clock.advance(REVEAL_DWELL_MS)
|
|
769
|
-
reveal.pick(null)
|
|
770
|
-
clock.advance(REVEAL_GRACE_MS - 1)
|
|
771
|
-
reveal.pick('bath')
|
|
772
|
-
clock.advance(1000)
|
|
773
|
-
expect(changes).toEqual(['bath'])
|
|
774
|
-
reveal.pick(null)
|
|
775
|
-
clock.advance(REVEAL_GRACE_MS)
|
|
776
|
-
expect(changes).toEqual(['bath', null])
|
|
777
|
-
})
|
|
778
|
-
|
|
779
|
-
test('opens at once for keyboard focus, and says so while that pill keeps it', () => {
|
|
780
|
-
const { clock, changes, focused, reveal } = setup()
|
|
781
|
-
reveal.focus('kitchen')
|
|
782
|
-
reveal.hover('kitchen')
|
|
783
|
-
reveal.hover(null)
|
|
784
|
-
reveal.focus(null)
|
|
785
|
-
clock.advance(REVEAL_GRACE_MS)
|
|
786
|
-
expect(changes).toEqual(['kitchen', null])
|
|
787
|
-
expect(focused).toEqual([true, false])
|
|
788
|
-
})
|
|
789
|
-
|
|
790
|
-
test('with the pointer on one pill and focus on another, the one that moved last wins', () => {
|
|
791
|
-
const { changes, focused, reveal } = setup()
|
|
792
|
-
reveal.hover('kitchen')
|
|
793
|
-
reveal.focus('bath')
|
|
794
|
-
expect(reveal.active).toBe('bath')
|
|
795
|
-
reveal.hover('kitchen')
|
|
796
|
-
expect(reveal.active).toBe('bath')
|
|
797
|
-
reveal.hover('closet')
|
|
798
|
-
reveal.focus('hall')
|
|
799
|
-
reveal.hover(null)
|
|
800
|
-
expect(changes).toEqual(['kitchen', 'bath', 'closet', 'hall'])
|
|
801
|
-
expect(focused).toEqual([false, true, false, true])
|
|
802
|
-
})
|
|
803
|
-
|
|
804
|
-
test('Escape closes it until the pointer or focus moves on to another room', () => {
|
|
805
|
-
const { clock, changes, reveal } = setup()
|
|
806
|
-
reveal.pick('bath')
|
|
807
|
-
clock.advance(REVEAL_DWELL_MS)
|
|
808
|
-
reveal.dismiss()
|
|
809
|
-
reveal.pick('bath')
|
|
810
|
-
clock.advance(1000)
|
|
811
|
-
reveal.hover('bath')
|
|
812
|
-
expect(changes).toEqual(['bath', null])
|
|
813
|
-
reveal.hover(null)
|
|
814
|
-
reveal.pick('closet')
|
|
815
|
-
clock.advance(REVEAL_DWELL_MS)
|
|
816
|
-
reveal.pick('bath')
|
|
817
|
-
clock.advance(REVEAL_DWELL_MS)
|
|
818
|
-
expect(changes).toEqual(['bath', null, 'closet', 'bath'])
|
|
819
|
-
})
|
|
820
|
-
|
|
821
|
-
test('a focused pill opens again once the view rests', () => {
|
|
822
|
-
const { changes, reveal } = setup()
|
|
823
|
-
reveal.focus('kitchen')
|
|
824
|
-
reveal.clear()
|
|
825
|
-
reveal.pick(null)
|
|
826
|
-
expect(changes).toEqual(['kitchen', null, 'kitchen'])
|
|
827
|
-
})
|
|
828
|
-
|
|
829
|
-
test('closes at once when the view moves, and forgets the pointer', () => {
|
|
830
|
-
const { clock, reveal } = setup()
|
|
831
|
-
reveal.pick('bath')
|
|
832
|
-
clock.advance(REVEAL_DWELL_MS)
|
|
833
|
-
reveal.clear()
|
|
834
|
-
expect(reveal.active).toBeNull()
|
|
835
|
-
clock.advance(1000)
|
|
836
|
-
expect(reveal.active).toBeNull()
|
|
837
|
-
expect(clock.pending()).toBe(0)
|
|
838
|
-
})
|
|
839
|
-
|
|
840
|
-
test('fades out exactly the pills within the gap of its chip', () => {
|
|
841
|
-
const chip = { left: 0, top: 0, right: 100, bottom: 24 }
|
|
842
|
-
const rects = new Map([
|
|
843
|
-
['under', { left: 50, top: 10, right: 80, bottom: 30 }],
|
|
844
|
-
['touching', { left: 100 + PILL_GAP - 1, top: 0, right: 140, bottom: 20 }],
|
|
845
|
-
['clear', { left: 100 + PILL_GAP, top: 0, right: 140, bottom: 20 }],
|
|
846
|
-
['far', { left: 400, top: 400, right: 450, bottom: 420 }],
|
|
847
|
-
])
|
|
848
|
-
expect(suppressedBy(chip, rects)).toEqual(new Set(['under', 'touching']))
|
|
849
35
|
})
|
|
850
36
|
})
|