@pascal-app/editor 1.1.0 → 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.
Files changed (260) 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/panels/collections/collections-popover.tsx +2 -2
  84. package/src/components/ui/panels/floor-foundation-panel.tsx +2 -2
  85. package/src/components/ui/panels/floor-opening-panel.tsx +4 -4
  86. package/src/components/ui/panels/node-display.ts +1 -1
  87. package/src/components/ui/panels/panel-manager.tsx +1 -1
  88. package/src/components/ui/panels/panel-wrapper.tsx +1 -1
  89. package/src/components/ui/panels/parametric-inspector.tsx +1 -1
  90. package/src/components/ui/panels/room-construction-rows.tsx +6 -6
  91. package/src/components/ui/panels/room-name-options.ts +1 -5
  92. package/src/components/ui/panels/room-panel.tsx +6 -8
  93. package/src/components/ui/primitives/color-dot.tsx +2 -2
  94. package/src/components/ui/primitives/opacity-control.tsx +2 -2
  95. package/src/components/ui/primitives/sidebar.tsx +3 -3
  96. package/src/components/ui/right-stack.tsx +1 -1
  97. package/src/components/ui/sidebar/icon-rail.tsx +1 -1
  98. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  99. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  100. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +1 -1
  101. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +2 -2
  102. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +1 -1
  103. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -1
  104. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  105. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +1 -1
  106. package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -2
  107. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +1 -1
  108. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  109. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +46 -7
  110. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +1 -1
  111. package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +14 -5
  112. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -2
  113. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +3 -3
  114. package/src/components/ui/sidebar/tab-bar.tsx +1 -1
  115. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +1 -1
  116. package/src/components/ui/wall-type/wall-type-menu.tsx +1 -1
  117. package/src/components/viewer-overlay.tsx +1 -2
  118. package/src/hooks/use-auto-frame.ts +1 -2
  119. package/src/hooks/use-keyboard.ts +1 -2
  120. package/src/hooks/use-selected-room.ts +5 -7
  121. package/src/i18n/floor-footprint-name.ts +1 -1
  122. package/src/i18n/locales/en/messages.po +180 -182
  123. package/src/i18n/locales/es/messages.po +180 -182
  124. package/src/i18n/locales/fr/messages.po +180 -182
  125. package/src/i18n/locales/ja/messages.po +180 -182
  126. package/src/i18n/locales/zh-CN/messages.po +180 -182
  127. package/src/i18n/locales.test.ts +1 -1
  128. package/src/index.tsx +109 -131
  129. package/src/lib/below-grade.test.tsx +4 -4
  130. package/src/lib/camera-facing.test.ts +1 -1
  131. package/src/lib/camera-facing.ts +2 -3
  132. package/src/lib/editor-level-display.ts +1 -1
  133. package/src/lib/floor-footprints.test.ts +2 -2
  134. package/src/lib/floor-opening-draft.ts +1 -1
  135. package/src/lib/floorplan/annotation-visibility.test.ts +1 -1
  136. package/src/lib/floorplan/annotation-visibility.ts +1 -1
  137. package/src/lib/floorplan/floorplan-export.test.ts +1 -1
  138. package/src/lib/floorplan/floorplan-export.tsx +8 -11
  139. package/src/lib/floorplan/floorplan-extension.ts +4 -157
  140. package/src/lib/floorplan/floorplan-mode.ts +2 -1
  141. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +1 -1
  142. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +8 -6
  143. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +3 -4
  144. package/src/lib/floorplan/geometry.ts +2 -112
  145. package/src/lib/floorplan/index.ts +0 -9
  146. package/src/lib/floorplan/items.ts +2 -1
  147. package/src/lib/floorplan/schedules.ts +6 -3
  148. package/src/lib/floorplan/sheet-export.ts +7 -3
  149. package/src/lib/floorplan/site-plan/site-parts.ts +7 -1
  150. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +1 -1
  151. package/src/lib/follow-pascal-presentation.ts +1 -1
  152. package/src/lib/glb-export.ts +1 -1
  153. package/src/lib/material-paint.ts +1 -1
  154. package/src/lib/measurements.test.ts +2 -1
  155. package/src/lib/mezzanine-selection.test.ts +1 -1
  156. package/src/lib/paint-region-mode.ts +1 -1
  157. package/src/lib/point-ask/session.ts +1 -2
  158. package/src/lib/roof-duplication.test.ts +100 -0
  159. package/src/lib/roof-duplication.ts +30 -5
  160. package/src/lib/room-handle-drag.ts +2 -12
  161. package/src/lib/room-name-catalog.test.ts +2 -1
  162. package/src/lib/room-name-messages.test.ts +2 -1
  163. package/src/lib/room-name-messages.ts +1 -118
  164. package/src/lib/room-name-storage.test.ts +2 -1
  165. package/src/lib/room-selection-commands.ts +2 -2
  166. package/src/lib/room-selection.test.ts +6 -6
  167. package/src/lib/room-transform-session.ts +1 -2
  168. package/src/lib/room-zone-routing.ts +2 -2
  169. package/src/lib/selection-routing.test.ts +0 -28
  170. package/src/lib/selection-routing.ts +1 -40
  171. package/src/lib/sfx-bus.ts +8 -32
  172. package/src/lib/sfx-player.test.ts +72 -1
  173. package/src/lib/sfx-player.ts +49 -337
  174. package/src/lib/viewer-selection.test.ts +38 -10
  175. package/src/lib/wall-pull.test.ts +1 -1
  176. package/src/lib/wall-pull.ts +1 -27
  177. package/src/store/use-editor.tsx +17 -26
  178. package/src/store/use-floorplan-annotation-visibility.ts +5 -5
  179. package/src/store/use-wall-region-handles-selection.ts +1 -2
  180. package/src/components/editor/__bench__/room-label-placement.bench.ts +0 -108
  181. package/src/components/editor/first-person/build-collider-world.test.ts +0 -596
  182. package/src/components/editor/first-person/build-collider-world.ts +0 -549
  183. package/src/components/editor/first-person/ground-holes.test.ts +0 -128
  184. package/src/components/editor/first-person/ground-holes.ts +0 -130
  185. package/src/components/editor/first-person/mechanism-targets.test.ts +0 -162
  186. package/src/components/editor/first-person/mechanism-targets.ts +0 -88
  187. package/src/components/editor/first-person/terrain-collider.test.ts +0 -132
  188. package/src/components/editor/first-person/terrain-collider.ts +0 -212
  189. package/src/components/editor/floorplan-camera-sync.test.ts +0 -249
  190. package/src/components/editor/floorplan-camera-sync.ts +0 -213
  191. package/src/components/editor/localized-html.tsx +0 -22
  192. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +0 -236
  193. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +0 -708
  194. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +0 -385
  195. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +0 -670
  196. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +0 -89
  197. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +0 -106
  198. package/src/components/editor-2d/svg-paths.ts +0 -124
  199. package/src/components/ui/action-menu/action-button.tsx +0 -89
  200. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +0 -42
  201. package/src/components/ui/primitives/button.tsx +0 -69
  202. package/src/components/ui/primitives/dropdown-menu.tsx +0 -232
  203. package/src/components/ui/primitives/popover.tsx +0 -42
  204. package/src/components/ui/primitives/switch.tsx +0 -29
  205. package/src/components/ui/primitives/tooltip.tsx +0 -57
  206. package/src/components/ui/scene-loader.tsx +0 -73
  207. package/src/components/viewer/floorplan-compass-button.tsx +0 -67
  208. package/src/components/viewer/floorplan-preview-geometry.test.ts +0 -107
  209. package/src/components/viewer/floorplan-preview-geometry.ts +0 -281
  210. package/src/components/viewer/floorplan-preview-navigation.test.ts +0 -43
  211. package/src/components/viewer/floorplan-preview-navigation.ts +0 -62
  212. package/src/components/viewer/floorplan-preview-visibility.test.ts +0 -35
  213. package/src/components/viewer/floorplan-preview-visibility.ts +0 -19
  214. package/src/components/viewer/floorplan-preview.tsx +0 -1065
  215. package/src/components/viewer/render-settings-menu.tsx +0 -294
  216. package/src/components/viewer/sky-control.tsx +0 -710
  217. package/src/components/viewer/use-viewer-camera-navigation-sync.ts +0 -140
  218. package/src/components/viewer/use-viewer-ui-sounds.test.ts +0 -15
  219. package/src/components/viewer/use-viewer-ui-sounds.ts +0 -46
  220. package/src/components/viewer/viewer-controls-bar.tsx +0 -496
  221. package/src/components/viewer/viewer-hover-label.tsx +0 -42
  222. package/src/components/viewer/viewer-rooms.tsx +0 -248
  223. package/src/components/viewer/viewer-scene-header.tsx +0 -286
  224. package/src/components/viewer/viewer-selection-manager.tsx +0 -124
  225. package/src/components/viewer/viewer-stage-modes.test.ts +0 -38
  226. package/src/components/viewer/viewer-stage-modes.ts +0 -49
  227. package/src/components/viewer/viewer-stage-switcher.tsx +0 -97
  228. package/src/components/viewer/viewer-stage.test.tsx +0 -47
  229. package/src/components/viewer/viewer-stage.tsx +0 -221
  230. package/src/components/walkthrough-hud.tsx +0 -193
  231. package/src/hooks/use-mobile.ts +0 -19
  232. package/src/i18n/level-name.ts +0 -39
  233. package/src/i18n/scene-theme-name.ts +0 -16
  234. package/src/lib/below-grade.ts +0 -133
  235. package/src/lib/camera-pose.test.ts +0 -237
  236. package/src/lib/camera-pose.ts +0 -172
  237. package/src/lib/floorplan/floorplan-extension.test.ts +0 -42
  238. package/src/lib/floorplan/floorplan-readonly.test.ts +0 -47
  239. package/src/lib/floorplan/floorplan-readonly.ts +0 -95
  240. package/src/lib/floorplan/marks.test.ts +0 -213
  241. package/src/lib/floorplan/marks.ts +0 -238
  242. package/src/lib/measurements.ts +0 -8
  243. package/src/lib/node-display-label.ts +0 -37
  244. package/src/lib/room-assembly-overlay.test.ts +0 -275
  245. package/src/lib/room-assembly-overlay.ts +0 -265
  246. package/src/lib/room-selection.ts +0 -417
  247. package/src/lib/scene-bounds.test.ts +0 -327
  248. package/src/lib/scene-bounds.ts +0 -233
  249. package/src/lib/typing-target.ts +0 -9
  250. package/src/lib/viewer-selection.ts +0 -595
  251. package/src/lib/zone-content.ts +0 -133
  252. package/src/store/camera-pose-store.test.ts +0 -29
  253. package/src/store/camera-pose-store.ts +0 -27
  254. package/src/store/navigation-sync-pose-store.test.ts +0 -34
  255. package/src/store/navigation-sync-pose-store.ts +0 -27
  256. package/src/store/use-audio.test.ts +0 -146
  257. package/src/store/use-audio.tsx +0 -169
  258. package/src/store/use-first-person-hud.ts +0 -40
  259. package/src/store/use-follow-pascal.test.ts +0 -62
  260. 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
  })