@pascal-app/editor 1.1.0 → 1.1.2

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.
Files changed (262) hide show
  1. package/package.json +9 -15
  2. package/src/components/editor/action-menu-button.tsx +1 -1
  3. package/src/components/editor/alignment-3d-guide-layer.tsx +1 -1
  4. package/src/components/editor/custom-camera-controls.tsx +11 -11
  5. package/src/components/editor/editor-layout-v2.tsx +1 -1
  6. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  7. package/src/components/editor/first-person-controls.tsx +20 -1981
  8. package/src/components/editor/floating-action-menu.tsx +1 -1
  9. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  10. package/src/components/editor/floor-region-controls.tsx +6 -2
  11. package/src/components/editor/floorplan-navigation-presentation.test.ts +0 -40
  12. package/src/components/editor/floorplan-navigation-presentation.ts +0 -66
  13. package/src/components/editor/floorplan-panel.tsx +26 -22
  14. package/src/components/editor/floorplan-wall-typing-keys.ts +1 -1
  15. package/src/components/editor/follow-pascal.test.tsx +1 -2
  16. package/src/components/editor/follow-pascal.tsx +1 -1
  17. package/src/components/editor/footprint-height-handle.tsx +1 -1
  18. package/src/components/editor/group-actions.ts +1 -2
  19. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  20. package/src/components/editor/group-move-3d.ts +1 -2
  21. package/src/components/editor/group-rotate-handle.tsx +1 -2
  22. package/src/components/editor/handles/use-handle-drag.ts +1 -2
  23. package/src/components/editor/index.tsx +24 -14
  24. package/src/components/editor/inspector-extension-actions.tsx +1 -1
  25. package/src/components/editor/node-arrow-handles.tsx +1 -1
  26. package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
  27. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  28. package/src/components/editor/room-controls.tsx +7 -4
  29. package/src/components/editor/room-handles-2d.tsx +2 -3
  30. package/src/components/editor/room-handles.tsx +1 -2
  31. package/src/components/editor/room-highlight.tsx +6 -105
  32. package/src/components/editor/room-labels.test.ts +8 -822
  33. package/src/components/editor/room-labels.tsx +40 -1671
  34. package/src/components/editor/room-webgpu-materials.test.ts +1 -1
  35. package/src/components/editor/selection-manager.tsx +5 -3
  36. package/src/components/editor/site-edge-labels.tsx +1 -1
  37. package/src/components/editor/snapshot-capture-overlay.tsx +2 -5
  38. package/src/components/editor/use-floorplan-background-placement.ts +2 -1
  39. package/src/components/editor/views/plugins-view.tsx +1 -1
  40. package/src/components/editor/views/view-bar.tsx +9 -9
  41. package/src/components/editor/wall-measurement-label.tsx +1 -1
  42. package/src/components/editor/wall-move-side-handles.tsx +1 -1
  43. package/src/components/editor/wall-pull-grip.tsx +2 -2
  44. package/src/components/editor-2d/floorplan-group-move.tsx +2 -3
  45. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1 -1
  46. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +1 -1
  47. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +1 -2
  48. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +0 -37
  49. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +0 -30
  50. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +13 -3
  51. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +1 -2
  52. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +8 -10
  53. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +25 -33
  54. package/src/components/systems/roof/roof-edit-system.tsx +4 -3
  55. package/src/components/tools/distribution/distribution-run-tool.tsx +1 -1
  56. package/src/components/tools/item/placement-history.test.tsx +1 -1
  57. package/src/components/tools/item/use-placement-coordinator.tsx +2 -3
  58. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +6 -2
  59. package/src/components/tools/registry/move-registry-node-tool.tsx +1 -2
  60. package/src/components/tools/shared/cursor-sphere.tsx +1 -1
  61. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  62. package/src/components/tools/shared/placement-box.tsx +1 -1
  63. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  64. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  65. package/src/components/ui/action-menu/camera-actions.tsx +2 -2
  66. package/src/components/ui/action-menu/control-modes.tsx +1 -1
  67. package/src/components/ui/action-menu/follow-pascal-pill.tsx +1 -1
  68. package/src/components/ui/action-menu/index.tsx +3 -3
  69. package/src/components/ui/action-menu/measurement-control.tsx +3 -3
  70. package/src/components/ui/action-menu/placement.ts +0 -18
  71. package/src/components/ui/action-menu/point-ask-button.tsx +1 -1
  72. package/src/components/ui/action-menu/view-toggles.tsx +5 -5
  73. package/src/components/ui/controls/finish-swatch.tsx +1 -1
  74. package/src/components/ui/controls/material-paint-panel.tsx +2 -2
  75. package/src/components/ui/controls/scene-material-list.tsx +2 -2
  76. package/src/components/ui/controls/select-control.tsx +1 -1
  77. package/src/components/ui/controls/terrain-sculpt-panel.tsx +1 -1
  78. package/src/components/ui/floating-level-selector.tsx +3 -3
  79. package/src/components/ui/helpers/contextual-helper-panel.tsx +1 -1
  80. package/src/components/ui/helpers/helper-manager.test.tsx +1 -1
  81. package/src/components/ui/helpers/helper-manager.tsx +1 -1
  82. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  83. package/src/components/ui/node-icon-image.test.tsx +46 -0
  84. package/src/components/ui/node-icon-image.tsx +33 -1
  85. package/src/components/ui/panels/collections/collections-popover.tsx +2 -2
  86. package/src/components/ui/panels/floor-foundation-panel.tsx +2 -2
  87. package/src/components/ui/panels/floor-opening-panel.tsx +4 -4
  88. package/src/components/ui/panels/node-display.ts +1 -1
  89. package/src/components/ui/panels/panel-manager.tsx +1 -1
  90. package/src/components/ui/panels/panel-wrapper.tsx +1 -1
  91. package/src/components/ui/panels/parametric-inspector.tsx +1 -1
  92. package/src/components/ui/panels/room-construction-rows.tsx +6 -6
  93. package/src/components/ui/panels/room-name-options.ts +1 -5
  94. package/src/components/ui/panels/room-panel.tsx +6 -8
  95. package/src/components/ui/primitives/color-dot.tsx +2 -2
  96. package/src/components/ui/primitives/opacity-control.tsx +2 -2
  97. package/src/components/ui/primitives/sidebar.tsx +3 -3
  98. package/src/components/ui/right-stack.tsx +1 -1
  99. package/src/components/ui/sidebar/icon-rail.tsx +1 -1
  100. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  101. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  102. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +1 -1
  103. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +2 -2
  104. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +1 -1
  105. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -1
  106. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  107. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +1 -1
  108. package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -2
  109. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +1 -1
  110. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  111. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +46 -7
  112. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +1 -1
  113. package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +14 -5
  114. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -2
  115. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +3 -3
  116. package/src/components/ui/sidebar/tab-bar.tsx +1 -1
  117. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +1 -1
  118. package/src/components/ui/wall-type/wall-type-menu.tsx +1 -1
  119. package/src/components/viewer-overlay.tsx +1 -2
  120. package/src/hooks/use-auto-frame.ts +1 -2
  121. package/src/hooks/use-keyboard.ts +1 -2
  122. package/src/hooks/use-selected-room.ts +5 -7
  123. package/src/i18n/floor-footprint-name.ts +1 -1
  124. package/src/i18n/locales/en/messages.po +180 -182
  125. package/src/i18n/locales/es/messages.po +180 -182
  126. package/src/i18n/locales/fr/messages.po +180 -182
  127. package/src/i18n/locales/ja/messages.po +180 -182
  128. package/src/i18n/locales/zh-CN/messages.po +180 -182
  129. package/src/i18n/locales.test.ts +1 -1
  130. package/src/index.tsx +109 -131
  131. package/src/lib/below-grade.test.tsx +4 -4
  132. package/src/lib/camera-facing.test.ts +1 -1
  133. package/src/lib/camera-facing.ts +2 -3
  134. package/src/lib/editor-level-display.ts +1 -1
  135. package/src/lib/floor-footprints.test.ts +2 -2
  136. package/src/lib/floor-opening-draft.ts +1 -1
  137. package/src/lib/floorplan/annotation-visibility.test.ts +1 -1
  138. package/src/lib/floorplan/annotation-visibility.ts +1 -1
  139. package/src/lib/floorplan/floorplan-export.test.ts +1 -1
  140. package/src/lib/floorplan/floorplan-export.tsx +8 -11
  141. package/src/lib/floorplan/floorplan-extension.ts +4 -157
  142. package/src/lib/floorplan/floorplan-mode.ts +2 -1
  143. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +1 -1
  144. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +8 -6
  145. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +3 -4
  146. package/src/lib/floorplan/geometry.ts +2 -112
  147. package/src/lib/floorplan/index.ts +0 -9
  148. package/src/lib/floorplan/items.ts +2 -1
  149. package/src/lib/floorplan/schedules.ts +6 -3
  150. package/src/lib/floorplan/sheet-export.ts +7 -3
  151. package/src/lib/floorplan/site-plan/site-parts.ts +7 -1
  152. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +1 -1
  153. package/src/lib/follow-pascal-presentation.ts +1 -1
  154. package/src/lib/glb-export.ts +1 -1
  155. package/src/lib/material-paint.ts +1 -1
  156. package/src/lib/measurements.test.ts +2 -1
  157. package/src/lib/mezzanine-selection.test.ts +1 -1
  158. package/src/lib/paint-region-mode.ts +1 -1
  159. package/src/lib/point-ask/session.ts +1 -2
  160. package/src/lib/roof-duplication.test.ts +100 -0
  161. package/src/lib/roof-duplication.ts +30 -5
  162. package/src/lib/room-handle-drag.ts +2 -12
  163. package/src/lib/room-name-catalog.test.ts +2 -1
  164. package/src/lib/room-name-messages.test.ts +2 -1
  165. package/src/lib/room-name-messages.ts +1 -118
  166. package/src/lib/room-name-storage.test.ts +2 -1
  167. package/src/lib/room-selection-commands.ts +2 -2
  168. package/src/lib/room-selection.test.ts +6 -6
  169. package/src/lib/room-transform-session.ts +1 -2
  170. package/src/lib/room-zone-routing.ts +2 -2
  171. package/src/lib/selection-routing.test.ts +0 -28
  172. package/src/lib/selection-routing.ts +1 -40
  173. package/src/lib/sfx-bus.ts +8 -32
  174. package/src/lib/sfx-player.test.ts +72 -1
  175. package/src/lib/sfx-player.ts +49 -337
  176. package/src/lib/viewer-selection.test.ts +38 -10
  177. package/src/lib/wall-pull.test.ts +1 -1
  178. package/src/lib/wall-pull.ts +1 -27
  179. package/src/store/use-editor.tsx +17 -26
  180. package/src/store/use-floorplan-annotation-visibility.ts +5 -5
  181. package/src/store/use-wall-region-handles-selection.ts +1 -2
  182. package/src/components/editor/__bench__/room-label-placement.bench.ts +0 -108
  183. package/src/components/editor/first-person/build-collider-world.test.ts +0 -596
  184. package/src/components/editor/first-person/build-collider-world.ts +0 -549
  185. package/src/components/editor/first-person/ground-holes.test.ts +0 -128
  186. package/src/components/editor/first-person/ground-holes.ts +0 -130
  187. package/src/components/editor/first-person/mechanism-targets.test.ts +0 -162
  188. package/src/components/editor/first-person/mechanism-targets.ts +0 -88
  189. package/src/components/editor/first-person/terrain-collider.test.ts +0 -132
  190. package/src/components/editor/first-person/terrain-collider.ts +0 -212
  191. package/src/components/editor/floorplan-camera-sync.test.ts +0 -249
  192. package/src/components/editor/floorplan-camera-sync.ts +0 -213
  193. package/src/components/editor/localized-html.tsx +0 -22
  194. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +0 -236
  195. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +0 -708
  196. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +0 -385
  197. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +0 -670
  198. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +0 -89
  199. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +0 -106
  200. package/src/components/editor-2d/svg-paths.ts +0 -124
  201. package/src/components/ui/action-menu/action-button.tsx +0 -89
  202. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +0 -42
  203. package/src/components/ui/primitives/button.tsx +0 -69
  204. package/src/components/ui/primitives/dropdown-menu.tsx +0 -232
  205. package/src/components/ui/primitives/popover.tsx +0 -42
  206. package/src/components/ui/primitives/switch.tsx +0 -29
  207. package/src/components/ui/primitives/tooltip.tsx +0 -57
  208. package/src/components/ui/scene-loader.tsx +0 -73
  209. package/src/components/viewer/floorplan-compass-button.tsx +0 -67
  210. package/src/components/viewer/floorplan-preview-geometry.test.ts +0 -107
  211. package/src/components/viewer/floorplan-preview-geometry.ts +0 -281
  212. package/src/components/viewer/floorplan-preview-navigation.test.ts +0 -43
  213. package/src/components/viewer/floorplan-preview-navigation.ts +0 -62
  214. package/src/components/viewer/floorplan-preview-visibility.test.ts +0 -35
  215. package/src/components/viewer/floorplan-preview-visibility.ts +0 -19
  216. package/src/components/viewer/floorplan-preview.tsx +0 -1065
  217. package/src/components/viewer/render-settings-menu.tsx +0 -294
  218. package/src/components/viewer/sky-control.tsx +0 -710
  219. package/src/components/viewer/use-viewer-camera-navigation-sync.ts +0 -140
  220. package/src/components/viewer/use-viewer-ui-sounds.test.ts +0 -15
  221. package/src/components/viewer/use-viewer-ui-sounds.ts +0 -46
  222. package/src/components/viewer/viewer-controls-bar.tsx +0 -496
  223. package/src/components/viewer/viewer-hover-label.tsx +0 -42
  224. package/src/components/viewer/viewer-rooms.tsx +0 -248
  225. package/src/components/viewer/viewer-scene-header.tsx +0 -286
  226. package/src/components/viewer/viewer-selection-manager.tsx +0 -124
  227. package/src/components/viewer/viewer-stage-modes.test.ts +0 -38
  228. package/src/components/viewer/viewer-stage-modes.ts +0 -49
  229. package/src/components/viewer/viewer-stage-switcher.tsx +0 -97
  230. package/src/components/viewer/viewer-stage.test.tsx +0 -47
  231. package/src/components/viewer/viewer-stage.tsx +0 -221
  232. package/src/components/walkthrough-hud.tsx +0 -193
  233. package/src/hooks/use-mobile.ts +0 -19
  234. package/src/i18n/level-name.ts +0 -39
  235. package/src/i18n/scene-theme-name.ts +0 -16
  236. package/src/lib/below-grade.ts +0 -133
  237. package/src/lib/camera-pose.test.ts +0 -237
  238. package/src/lib/camera-pose.ts +0 -172
  239. package/src/lib/floorplan/floorplan-extension.test.ts +0 -42
  240. package/src/lib/floorplan/floorplan-readonly.test.ts +0 -47
  241. package/src/lib/floorplan/floorplan-readonly.ts +0 -95
  242. package/src/lib/floorplan/marks.test.ts +0 -213
  243. package/src/lib/floorplan/marks.ts +0 -238
  244. package/src/lib/measurements.ts +0 -8
  245. package/src/lib/node-display-label.ts +0 -37
  246. package/src/lib/room-assembly-overlay.test.ts +0 -275
  247. package/src/lib/room-assembly-overlay.ts +0 -265
  248. package/src/lib/room-selection.ts +0 -417
  249. package/src/lib/scene-bounds.test.ts +0 -327
  250. package/src/lib/scene-bounds.ts +0 -233
  251. package/src/lib/typing-target.ts +0 -9
  252. package/src/lib/viewer-selection.ts +0 -595
  253. package/src/lib/zone-content.ts +0 -133
  254. package/src/store/camera-pose-store.test.ts +0 -29
  255. package/src/store/camera-pose-store.ts +0 -27
  256. package/src/store/navigation-sync-pose-store.test.ts +0 -34
  257. package/src/store/navigation-sync-pose-store.ts +0 -27
  258. package/src/store/use-audio.test.ts +0 -146
  259. package/src/store/use-audio.tsx +0 -169
  260. package/src/store/use-first-person-hud.ts +0 -40
  261. package/src/store/use-follow-pascal.test.ts +0 -62
  262. 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 { Polygon } from '@pascal-app/core'
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
- type Timer = ReturnType<typeof setTimeout>
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(roomLabelsVisible(resting)).toBe(true)
89
- for (const type of ['site', 'building', 'level']) {
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 any other selection, a room, a zone, a unit, a tool, a gesture or site', () => {
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(roomLabelsVisible({ ...resting, ...change })).toBe(false)
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
  })