@pascal-app/editor 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/README.md +106 -0
  2. package/package.json +39 -37
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/bake-thumbnail.tsx +6 -15
  5. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  6. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  7. package/src/components/editor/custom-camera-controls.tsx +81 -8
  8. package/src/components/editor/editor-layout-mobile.tsx +1 -1
  9. package/src/components/editor/editor-layout-v2.tsx +1 -1
  10. package/src/components/editor/export-manager.tsx +14 -19
  11. package/src/components/editor/first-person/build-collider-world.test.ts +269 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +22 -5
  13. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  14. package/src/components/editor/first-person/ground-holes.ts +130 -0
  15. package/src/components/editor/first-person/mechanism-targets.test.ts +89 -2
  16. package/src/components/editor/first-person/mechanism-targets.ts +52 -0
  17. package/src/components/editor/first-person-controls.tsx +19 -11
  18. package/src/components/editor/floor-aware-camera-controls.test.ts +24 -0
  19. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  20. package/src/components/editor/floorplan-panel.tsx +72 -23
  21. package/src/components/editor/follow-pascal.test.tsx +912 -0
  22. package/src/components/editor/follow-pascal.tsx +354 -0
  23. package/src/components/editor/group-actions.ts +2 -0
  24. package/src/components/editor/group-move-3d.ts +2 -0
  25. package/src/components/editor/group-rotate-handle.tsx +2 -0
  26. package/src/components/editor/group-transform-shared.test.ts +460 -0
  27. package/src/components/editor/index.tsx +166 -79
  28. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  29. package/src/components/editor/room-highlight.tsx +13 -5
  30. package/src/components/editor/room-labels.test.ts +621 -128
  31. package/src/components/editor/room-labels.tsx +1090 -190
  32. package/src/components/editor/selection-manager.tsx +38 -117
  33. package/src/components/editor/slab-hole-highlights.tsx +2 -2
  34. package/src/components/editor/snapshot-capture-overlay.tsx +3 -2
  35. package/src/components/editor/snapshot-capture.test.ts +69 -58
  36. package/src/components/editor/snapshot-capture.ts +7 -1
  37. package/src/components/editor/thumbnail-generator.test.ts +350 -4
  38. package/src/components/editor/thumbnail-generator.tsx +128 -54
  39. package/src/components/editor/use-floorplan-background-placement.ts +3 -1
  40. package/src/components/editor/views/plugins-view.tsx +1 -8
  41. package/src/components/editor/views/view-bar.test.tsx +54 -0
  42. package/src/components/editor/views/view-bar.tsx +140 -17
  43. package/src/components/editor/wall-snap-beacon-layer.tsx +5 -4
  44. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  45. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  46. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +3 -4
  47. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +1 -1
  48. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +29 -7
  49. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  50. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  51. package/src/components/point-ask/bubble.tsx +538 -0
  52. package/src/components/point-ask/hover-chip.tsx +91 -0
  53. package/src/components/point-ask/mode-pill.tsx +92 -0
  54. package/src/components/point-ask/optional-motion.ts +22 -0
  55. package/src/components/point-ask/outline-corners.tsx +93 -0
  56. package/src/components/point-ask/pins.tsx +711 -0
  57. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  58. package/src/components/point-ask/point-ask-styles.ts +105 -0
  59. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  60. package/src/components/point-ask/region-marquee.tsx +64 -0
  61. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  62. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  63. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  64. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  65. package/src/components/tools/distribution/distribution-public.ts +9 -0
  66. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  67. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  68. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  69. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  70. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  71. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  72. package/src/components/tools/distribution/ports.test.ts +56 -0
  73. package/src/components/tools/distribution/ports.ts +378 -0
  74. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  75. package/src/components/tools/distribution/run-cursor.ts +36 -0
  76. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  77. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  78. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  79. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  80. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  81. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  82. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  83. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  84. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  85. package/src/components/tools/item/placement-history.test.tsx +38 -0
  86. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  87. package/src/components/tools/item/placement-strategies.ts +3 -6
  88. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  89. package/src/components/tools/item/use-draft-node.test.tsx +94 -94
  90. package/src/components/tools/item/use-placement-coordinator.tsx +55 -49
  91. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  92. package/src/components/tools/registry/item-surface-move.ts +2 -0
  93. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  94. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +63 -29
  96. package/src/components/tools/tool-manager.tsx +14 -5
  97. package/src/components/tools/wall/wall-drafting.test.ts +170 -57
  98. package/src/components/ui/action-menu/action-button.tsx +6 -1
  99. package/src/components/ui/action-menu/camera-actions.tsx +7 -3
  100. package/src/components/ui/action-menu/control-modes.tsx +13 -7
  101. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  102. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  103. package/src/components/ui/action-menu/index.tsx +56 -33
  104. package/src/components/ui/action-menu/measurement-control.tsx +11 -9
  105. package/src/components/ui/action-menu/placement.ts +20 -0
  106. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  107. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  108. package/src/components/ui/build-panel/build-panel.tsx +10 -3
  109. package/src/components/ui/command-palette/editor-commands.tsx +3 -12
  110. package/src/components/ui/command-palette/index.tsx +45 -15
  111. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  112. package/src/components/ui/controls/action-button.tsx +5 -2
  113. package/src/components/ui/controls/metric-control.tsx +1 -1
  114. package/src/components/ui/controls/segmented-control.tsx +21 -8
  115. package/src/components/ui/controls/select-control.tsx +1 -1
  116. package/src/components/ui/controls/slider-control.tsx +1 -1
  117. package/src/components/ui/controls/toggle-control.tsx +3 -3
  118. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  119. package/src/components/ui/floating-level-selector.tsx +150 -45
  120. package/src/components/ui/helpers/contextual-helper-panel.tsx +32 -7
  121. package/src/components/ui/helpers/helper-manager.test.tsx +54 -2
  122. package/src/components/ui/helpers/helper-manager.tsx +7 -1
  123. package/src/components/ui/helpers/item-helper.tsx +9 -1
  124. package/src/components/ui/panels/collections/collections-popover.tsx +8 -8
  125. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  126. package/src/components/ui/panels/mobile-selection-bar.tsx +6 -3
  127. package/src/components/ui/panels/multi-selection-panel.tsx +120 -3
  128. package/src/components/ui/panels/panel-wrapper.tsx +53 -27
  129. package/src/components/ui/panels/parametric-field-control.tsx +2 -2
  130. package/src/components/ui/panels/parametric-inspector.tsx +3 -3
  131. package/src/components/ui/panels/room-panel.tsx +65 -0
  132. package/src/components/ui/primitives/badge.tsx +8 -0
  133. package/src/components/ui/primitives/sidebar.tsx +1 -1
  134. package/src/components/ui/right-stack.tsx +21 -2
  135. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  136. package/src/components/ui/sidebar/icon-rail.tsx +3 -3
  137. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  138. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  139. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  140. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  141. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  142. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  143. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +107 -23
  144. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  145. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  146. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  147. package/src/components/ui/sidebar/panels/site-panel/index.tsx +5 -12
  148. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -10
  149. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -2
  150. package/src/components/ui/sidebar/tab-bar.tsx +4 -4
  151. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  152. package/src/components/viewer/viewer-rooms.tsx +16 -0
  153. package/src/hooks/use-auto-save.integration.test.tsx +96 -38
  154. package/src/hooks/use-auto-save.ts +26 -1
  155. package/src/hooks/use-ceiling-events.test.ts +123 -118
  156. package/src/hooks/use-grid-events.test.ts +105 -105
  157. package/src/hooks/use-keyboard.test.ts +65 -16
  158. package/src/hooks/use-keyboard.ts +29 -12
  159. package/src/hooks/use-placement-notice.test.ts +24 -1
  160. package/src/hooks/use-placement-notice.ts +37 -6
  161. package/src/index.tsx +71 -24
  162. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  163. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  164. package/src/lib/add-storey.test.ts +177 -0
  165. package/src/lib/add-storey.ts +59 -0
  166. package/src/lib/below-grade.test.tsx +171 -0
  167. package/src/lib/below-grade.ts +133 -0
  168. package/src/lib/camera-facing.test.ts +24 -0
  169. package/src/lib/camera-facing.ts +24 -0
  170. package/src/lib/direct-manipulation.ts +2 -0
  171. package/src/lib/download-project-file.ts +17 -0
  172. package/src/lib/editor-level-display.ts +9 -2
  173. package/src/lib/editor-views.ts +35 -1
  174. package/src/lib/export-texture-utils.ts +2 -4
  175. package/src/lib/floor-foundation-support.test.ts +1 -0
  176. package/src/lib/floorplan/floorplan-pdfkit-document.ts +31 -16
  177. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +39 -0
  178. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +4 -0
  179. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  180. package/src/lib/floorplan/sheet-export.test.ts +24 -0
  181. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +34 -45
  182. package/src/lib/floorplan/site-plan/coverage.ts +2 -4
  183. package/src/lib/floorplan/site-plan/front-edge.ts +2 -1
  184. package/src/lib/floorplan/site-plan/geometry.test.ts +9 -15
  185. package/src/lib/floorplan/site-plan/geometry.ts +1 -416
  186. package/src/lib/floorplan/site-plan/index.ts +0 -16
  187. package/src/lib/floorplan/site-plan/site-annotations.test.ts +117 -0
  188. package/src/lib/floorplan/site-plan/site-annotations.ts +3 -2
  189. package/src/lib/floorplan/site-plan/site-parts.ts +14 -106
  190. package/src/lib/floorplan/site-plan/yards.test.ts +2 -1
  191. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  192. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  193. package/src/lib/follow-pascal-exterior.ts +39 -0
  194. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  195. package/src/lib/follow-pascal-presentation.ts +129 -0
  196. package/src/lib/follow-pascal-sight.ts +84 -0
  197. package/src/lib/follow-pascal-targets.test.ts +406 -0
  198. package/src/lib/follow-pascal-targets.ts +198 -0
  199. package/src/lib/follow-pascal.test.ts +350 -0
  200. package/src/lib/follow-pascal.ts +284 -0
  201. package/src/lib/fresh-planar-placement.test.ts +112 -0
  202. package/src/lib/fresh-planar-placement.ts +57 -3
  203. package/src/lib/geometry-script/author.ts +6 -1
  204. package/src/lib/geometry-script/client.ts +8 -1
  205. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  206. package/src/lib/geometry-script/protocol.ts +1 -0
  207. package/src/lib/glb-export.test.ts +187 -162
  208. package/src/lib/glb-export.ts +4 -1
  209. package/src/lib/history.test.ts +12 -7
  210. package/src/lib/interaction/scope.ts +4 -0
  211. package/src/lib/level-selection.test.ts +57 -0
  212. package/src/lib/level-selection.ts +16 -1
  213. package/src/lib/lot/clean-ring.test.ts +2 -1
  214. package/src/lib/lot/drop-in.ts +9 -24
  215. package/src/lib/lot/lot-patch.test.ts +1 -1
  216. package/src/lib/lot/lot-patch.ts +2 -2
  217. package/src/lib/measurement-kind.test.ts +6 -8
  218. package/src/lib/measurements.test.ts +23 -0
  219. package/src/lib/orbit-floor-clearance.test.ts +36 -0
  220. package/src/lib/orbit-floor-clearance.ts +48 -0
  221. package/src/lib/paint-pass-through.test.ts +11 -0
  222. package/src/lib/paint-pass-through.ts +4 -2
  223. package/src/lib/paint-scope.ts +1 -0
  224. package/src/lib/parametric-node-update.ts +16 -7
  225. package/src/lib/plugin-chat.ts +15 -0
  226. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  227. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  228. package/src/lib/point-ask/ask-created.test.ts +195 -0
  229. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  230. package/src/lib/point-ask/build-context.test.ts +173 -0
  231. package/src/lib/point-ask/build-context.ts +94 -0
  232. package/src/lib/point-ask/capture-crop.ts +129 -0
  233. package/src/lib/point-ask/choreography.ts +69 -0
  234. package/src/lib/point-ask/compare.test.ts +145 -0
  235. package/src/lib/point-ask/compare.ts +73 -0
  236. package/src/lib/point-ask/describe-target.test.ts +165 -0
  237. package/src/lib/point-ask/describe-target.ts +234 -0
  238. package/src/lib/point-ask/dictation.ts +79 -0
  239. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  240. package/src/lib/point-ask/edge-marker.ts +27 -0
  241. package/src/lib/point-ask/frame-loop.ts +25 -0
  242. package/src/lib/point-ask/history-marks.test.ts +94 -0
  243. package/src/lib/point-ask/history-marks.ts +39 -0
  244. package/src/lib/point-ask/host-api.ts +32 -0
  245. package/src/lib/point-ask/outline-box.test.ts +30 -0
  246. package/src/lib/point-ask/outline-box.ts +11 -0
  247. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  248. package/src/lib/point-ask/pick.test.ts +220 -0
  249. package/src/lib/point-ask/pick.ts +121 -0
  250. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  251. package/src/lib/point-ask/pin-machine.ts +217 -0
  252. package/src/lib/point-ask/placement.test.ts +203 -0
  253. package/src/lib/point-ask/placement.ts +141 -0
  254. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  255. package/src/lib/point-ask/projector.ts +83 -0
  256. package/src/lib/point-ask/region.test.ts +157 -0
  257. package/src/lib/point-ask/region.ts +136 -0
  258. package/src/lib/point-ask/retry.test.ts +217 -0
  259. package/src/lib/point-ask/retry.ts +37 -0
  260. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  261. package/src/lib/point-ask/scene-commit.ts +32 -0
  262. package/src/lib/point-ask/session.ts +1391 -0
  263. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  264. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  265. package/src/lib/point-ask/spring.test.ts +63 -0
  266. package/src/lib/point-ask/spring.ts +107 -0
  267. package/src/lib/point-ask/suggestions.test.ts +80 -0
  268. package/src/lib/point-ask/suggestions.ts +125 -0
  269. package/src/lib/point-ask/target-scope.test.ts +622 -0
  270. package/src/lib/point-ask/target-scope.ts +338 -0
  271. package/src/lib/point-ask/types.ts +48 -0
  272. package/src/lib/portable-export.test.ts +34 -1
  273. package/src/lib/portable-export.ts +119 -53
  274. package/src/lib/print-feature-thickness.ts +10 -1
  275. package/src/lib/print-roof-solids.test.ts +39 -1
  276. package/src/lib/print-roof-solids.ts +43 -10
  277. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  278. package/src/lib/room-assembly-overlay.ts +2 -3
  279. package/src/lib/room-selection.test.ts +45 -1
  280. package/src/lib/scene-clipboard.test.ts +246 -1
  281. package/src/lib/scene-clipboard.ts +44 -14
  282. package/src/lib/selection-routing.ts +1 -1
  283. package/src/lib/sfx-bus.ts +13 -0
  284. package/src/lib/sfx-player.test.ts +70 -9
  285. package/src/lib/sfx-player.ts +6 -0
  286. package/src/lib/stair-levels.test.ts +25 -0
  287. package/src/lib/stair-levels.ts +22 -3
  288. package/src/lib/terrace-draft.ts +2 -1
  289. package/src/lib/viewer-selection.ts +22 -11
  290. package/src/lib/wall-placement-pointer.test.ts +42 -0
  291. package/src/lib/wall-placement-pointer.ts +57 -0
  292. package/src/lib/window-changes.ts +35 -0
  293. package/src/store/use-audio.test.ts +146 -0
  294. package/src/store/use-audio.tsx +127 -3
  295. package/src/store/use-construction-reveal.test.ts +173 -0
  296. package/src/store/use-construction-reveal.ts +145 -0
  297. package/src/store/use-corner-snap-hint.test.ts +24 -0
  298. package/src/store/use-corner-snap-hint.ts +47 -0
  299. package/src/store/use-follow-pascal.test.ts +62 -0
  300. package/src/store/use-follow-pascal.ts +53 -0
  301. package/src/store/use-point-ask.ts +247 -0
  302. package/src/store/use-stair-build-preview.ts +25 -5
  303. package/src/store/view-layout.test.ts +27 -1
  304. package/src/store/view-layout.ts +4 -1
  305. package/src/test-utils/render-current-markup.tsx +16 -0
  306. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  307. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  308. package/src/components/tools/stair/stair-defaults.ts +0 -15
  309. package/src/components/tools/stair/stair-tool.tsx +0 -682
@@ -0,0 +1,254 @@
1
+ import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test'
2
+ import {
3
+ type AnyNode,
4
+ type AnyNodeDefinition,
5
+ type AnyNodeId,
6
+ BaseNode,
7
+ clearSceneHistory,
8
+ type FloorplanGeometry,
9
+ LevelNode,
10
+ loadPlugin,
11
+ nodeRegistry,
12
+ nodeType,
13
+ objectId,
14
+ type RevealConfig,
15
+ runAsSceneCommitAuthor,
16
+ useScene,
17
+ } from '@pascal-app/core'
18
+ import {
19
+ type ConstructionRevealDriver,
20
+ getRevealPlanState,
21
+ startConstructionReveal,
22
+ useViewer,
23
+ } from '@pascal-app/viewer'
24
+ import React from 'react'
25
+ import { renderToStaticMarkup } from 'react-dom/server'
26
+ import useEditor from '../../../store/use-editor'
27
+ import { FloorplanRegistryLayer } from './floorplan-registry-layer'
28
+ import { floorplanRevealAxis, floorplanRevealLook } from './floorplan-reveal'
29
+
30
+ // The 2D plan follows the construction reveal (E-006): a node still waiting
31
+ // for its turn is not drawn, then a wall grows along its centreline and the
32
+ // rest fades in, on the reveal's own clock.
33
+
34
+ const PLUGIN_ID = 'fixture:plan-reveal'
35
+ const base = { object: 'node', parentId: null, visible: true, metadata: {} } as const
36
+
37
+ function planKind(
38
+ kind: string,
39
+ prefix: string,
40
+ reveal: RevealConfig,
41
+ floorplan: () => FloorplanGeometry,
42
+ ): { schema: typeof BaseNode; definition: AnyNodeDefinition } {
43
+ const schema = BaseNode.extend({
44
+ id: objectId(prefix),
45
+ type: nodeType(kind),
46
+ children: LevelNode.shape.children,
47
+ })
48
+ const definition = {
49
+ kind,
50
+ schemaVersion: 1,
51
+ schema,
52
+ category: 'structure',
53
+ defaults: () => ({ ...base, children: [] }),
54
+ capabilities: { reveal },
55
+ floorplan,
56
+ }
57
+ return {
58
+ schema: schema as unknown as typeof BaseNode,
59
+ definition: definition as unknown as AnyNodeDefinition,
60
+ }
61
+ }
62
+
63
+ const WALL_LINE = { x1: 0, y1: 0, x2: 4, y2: 0 }
64
+ const Wall = planKind(
65
+ 'fixture:plan-wall',
66
+ 'fxpwall',
67
+ { phase: 'structure', style: 'rise' },
68
+ () => ({
69
+ kind: 'group',
70
+ children: [
71
+ {
72
+ kind: 'polygon',
73
+ points: [
74
+ [0, -0.1],
75
+ [4, -0.1],
76
+ [4, 0.1],
77
+ [0, 0.1],
78
+ ],
79
+ fill: '#222',
80
+ },
81
+ { kind: 'hit-line', ...WALL_LINE, strokeWidthPx: 18 },
82
+ ],
83
+ }),
84
+ )
85
+ const Crate = planKind(
86
+ 'fixture:plan-crate',
87
+ 'fxpcrate',
88
+ { phase: 'furnishing', style: 'drop', height: 0.3 },
89
+ () => ({ kind: 'rect', x: 1, y: 1, width: 0.5, height: 0.5, fill: '#888' }),
90
+ )
91
+
92
+ const make = (kind: { schema: typeof BaseNode }) => kind.schema.parse({}) as unknown as AnyNode
93
+
94
+ let driver: ConstructionRevealDriver | null = null
95
+ let restoreRegistry: () => void = () => {}
96
+ const savedScene = useScene.getState()
97
+ const savedViewer = useViewer.getState()
98
+ const savedEditor = useEditor.getState()
99
+
100
+ function plan(): string {
101
+ // Server rendering reads Zustand's startup state; render the current scene instead.
102
+ const useSyncExternalStore = React.useSyncExternalStore
103
+ const serverSnapshot = spyOn(React, 'useSyncExternalStore').mockImplementation(
104
+ (subscribe, getSnapshot) => useSyncExternalStore(subscribe, getSnapshot, getSnapshot),
105
+ )
106
+ try {
107
+ return renderToStaticMarkup(
108
+ <svg>
109
+ <FloorplanRegistryLayer />
110
+ </svg>,
111
+ )
112
+ } finally {
113
+ serverSnapshot.mockRestore()
114
+ }
115
+ }
116
+
117
+ function openLevel(): AnyNodeId {
118
+ const level = LevelNode.parse({ level: 0, children: [] })
119
+ useScene.getState().setScene({ [level.id]: level as AnyNode }, [level.id as AnyNodeId], {
120
+ installedPlugins: [PLUGIN_ID],
121
+ hasExplicitPluginInstallState: true,
122
+ })
123
+ clearSceneHistory()
124
+ useViewer.setState({
125
+ selection: { ...useViewer.getState().selection, levelId: level.id, selectedIds: [] },
126
+ } as never)
127
+ useEditor.setState({ viewMode: '2d' })
128
+ driver = startConstructionReveal({
129
+ reveals: (commit) => commit.author === 'agent',
130
+ prefersReducedMotion: () => false,
131
+ })
132
+ return level.id as AnyNodeId
133
+ }
134
+
135
+ beforeEach(async () => {
136
+ restoreRegistry = nodeRegistry._snapshot()
137
+ nodeRegistry._reset()
138
+ await loadPlugin({ id: PLUGIN_ID, apiVersion: 1, nodes: [Wall.definition, Crate.definition] })
139
+ })
140
+
141
+ afterEach(() => {
142
+ driver?.stop()
143
+ driver = null
144
+ restoreRegistry()
145
+ useScene.setState(savedScene)
146
+ useViewer.setState(savedViewer)
147
+ useEditor.setState(savedEditor)
148
+ })
149
+
150
+ describe('the plan during a construction reveal', () => {
151
+ test('a node waiting for its turn is not drawn; it is once its turn comes', () => {
152
+ const levelId = openLevel()
153
+ const wall = make(Wall)
154
+ const crate = make(Crate)
155
+ runAsSceneCommitAuthor('agent', () =>
156
+ useScene.getState().createNodes([
157
+ { node: wall, parentId: levelId },
158
+ { node: crate, parentId: levelId },
159
+ ]),
160
+ )
161
+ let markup = plan()
162
+ expect(markup).not.toContain(`data-node-id="${wall.id}"`)
163
+ expect(markup).not.toContain(`data-node-id="${crate.id}"`)
164
+
165
+ driver!.tick(0)
166
+ expect(getRevealPlanState(wall.id)).not.toBeNull()
167
+ // Nothing draws in a test with no canvas, so the build does not wait for a first draw to settle.
168
+ let now = 0
169
+ while (getRevealPlanState(wall.id)?.progress === 0 && now < 3000) {
170
+ now += 20
171
+ driver!.tick(now)
172
+ }
173
+ markup = plan()
174
+ expect(markup).toContain(`data-node-id="${wall.id}"`)
175
+ expect(markup).not.toContain(`data-node-id="${crate.id}"`)
176
+
177
+ while (getRevealPlanState(crate.id)?.progress === 0 && now < 6000) {
178
+ now += 20
179
+ driver!.tick(now)
180
+ }
181
+ expect(plan()).toContain(`data-node-id="${crate.id}"`)
182
+ })
183
+
184
+ test("a person's own edit is drawn at once", () => {
185
+ const levelId = openLevel()
186
+ const wall = make(Wall)
187
+ useScene.getState().createNode(wall, levelId)
188
+ expect(plan()).toContain(`data-node-id="${wall.id}"`)
189
+ })
190
+ })
191
+
192
+ describe('how a revealing entry draws', () => {
193
+ test('a wall grows along its centreline from its start', () => {
194
+ const axis = floorplanRevealAxis([Wall.definition.floorplan!(make(Wall), {} as never)])
195
+ expect(axis).toEqual(WALL_LINE)
196
+
197
+ const start = floorplanRevealLook({ style: 'rise', progress: 0 }, axis)
198
+ const middle = floorplanRevealLook({ style: 'rise', progress: 0.5 }, axis)
199
+ const end = floorplanRevealLook({ style: 'rise', progress: 1 }, axis)
200
+ expect(start.opacity).toBe(1)
201
+ expect(start.clip!.width).toBeLessThan(middle.clip!.width)
202
+ expect(middle.clip!.width).toBeLessThan(end.clip!.width)
203
+ // Grown, it covers the whole wall and its mitred corners.
204
+ expect(start.clip!.x + end.clip!.width).toBeGreaterThan(WALL_LINE.x2)
205
+ expect(start.clip!.x).toBeLessThan(WALL_LINE.x1)
206
+ expect(end.clip!.rotateDeg).toBe(0)
207
+ expect(
208
+ floorplanRevealLook({ style: 'rise', progress: 0.5 }, { x1: 0, y1: 0, x2: 0, y2: 3 }).clip!
209
+ .rotateDeg,
210
+ ).toBe(90)
211
+ })
212
+
213
+ test("an undo's reverse play is the same look read backwards: a wall's clip shrinks, the rest fades out", () => {
214
+ const axis = floorplanRevealAxis([Wall.definition.floorplan!(make(Wall), {} as never)])
215
+ const whole = floorplanRevealLook({ style: 'rise', progress: 1 }, axis)
216
+ const half = floorplanRevealLook({ style: 'rise', progress: 0.5 }, axis)
217
+ const gone = floorplanRevealLook({ style: 'rise', progress: 0 }, axis)
218
+ expect(whole.clip!.width).toBeGreaterThan(half.clip!.width)
219
+ expect(half.clip!.width).toBeGreaterThan(gone.clip!.width)
220
+ // Gone, the clip covers nothing of the wall (only the pad before its start).
221
+ expect(gone.clip!.width).toBeLessThanOrEqual(1)
222
+ expect(floorplanRevealLook({ style: 'drop', progress: 1 }, null).opacity).toBe(1)
223
+ expect(floorplanRevealLook({ style: 'drop', progress: 0.4 }, null).opacity).toBeLessThan(1)
224
+ expect(floorplanRevealLook({ style: 'drop', progress: 0 }, null).opacity).toBe(0)
225
+ })
226
+
227
+ test('everything else fades in', () => {
228
+ for (const style of ['drop', 'settle', 'cut', 'scale', 'assemble'] as const) {
229
+ const faint = floorplanRevealLook({ style, progress: 0.1 }, null)
230
+ const strong = floorplanRevealLook({ style, progress: 0.8 }, null)
231
+ expect(faint.clip).toBeNull()
232
+ expect(faint.opacity).toBeLessThan(strong.opacity)
233
+ expect(floorplanRevealLook({ style, progress: 1 }, null).opacity).toBe(1)
234
+ }
235
+ // A rising kind with no centreline fades too.
236
+ expect(floorplanRevealLook({ style: 'rise', progress: 0.1 }, null).clip).toBeNull()
237
+ })
238
+
239
+ test('only a centreline outside any transformed group is an axis', () => {
240
+ expect(floorplanRevealAxis([{ kind: 'rect', x: 0, y: 0, width: 1, height: 1 }])).toBeNull()
241
+ expect(
242
+ floorplanRevealAxis([
243
+ {
244
+ kind: 'group',
245
+ transform: { rotate: 1 },
246
+ children: [{ kind: 'hit-line', ...WALL_LINE, strokeWidthPx: 18 }],
247
+ },
248
+ ]),
249
+ ).toBeNull()
250
+ expect(
251
+ floorplanRevealAxis([{ kind: 'hit-line', x1: 1, y1: 1, x2: 1, y2: 1, strokeWidthPx: 18 }]),
252
+ ).toBeNull()
253
+ })
254
+ })
@@ -0,0 +1,141 @@
1
+ 'use client'
2
+
3
+ import type { FloorplanGeometry } from '@pascal-app/core'
4
+ import { getRevealPlanState, type RevealPlanState, subscribeRevealTicks } from '@pascal-app/viewer'
5
+ import { type RefObject, useEffect } from 'react'
6
+
7
+ /** A node's plan centreline, from its start: what a growing reveal follows. */
8
+ export type FloorplanRevealAxis = { x1: number; y1: number; x2: number; y2: number }
9
+
10
+ /** An entry's plan geometry, by pass. */
11
+ export type FloorplanRevealEntry = {
12
+ base: FloorplanGeometry | null
13
+ overlay: FloorplanGeometry | null
14
+ handles: FloorplanGeometry | null
15
+ }
16
+
17
+ /** How a revealing entry draws: its opacity, and for a growing one the clip that uncovers it. */
18
+ export type FloorplanRevealLook = {
19
+ opacity: number
20
+ clip: { x: number; y: number; width: number; height: number; rotateDeg: number } | null
21
+ }
22
+
23
+ /** The clip reaches this far before the start and past the end, so mitred corners show whole. */
24
+ const CLIP_PAD_M = 1
25
+ /** And this far to either side of the centreline: the wall, its layers and its labels. */
26
+ const CLIP_HALF_WIDTH_M = 50
27
+
28
+ const easeOutCubic = (t: number) => 1 - (1 - t) ** 3
29
+
30
+ /**
31
+ * The centreline a linear kind (a wall, a fence) draws as its `hit-line`, if
32
+ * its plan geometry has one outside any transformed group.
33
+ */
34
+ export function floorplanRevealAxis(
35
+ geometries: readonly (FloorplanGeometry | null | undefined)[],
36
+ ): FloorplanRevealAxis | null {
37
+ const visit = (geometry: FloorplanGeometry): FloorplanRevealAxis | null => {
38
+ if (geometry.kind === 'hit-line') {
39
+ const { x1, y1, x2, y2 } = geometry
40
+ return Math.hypot(x2 - x1, y2 - y1) > 1e-6 ? { x1, y1, x2, y2 } : null
41
+ }
42
+ if (geometry.kind !== 'group' || geometry.transform) return null
43
+ for (const child of geometry.children) {
44
+ const axis = visit(child)
45
+ if (axis) return axis
46
+ }
47
+ return null
48
+ }
49
+ for (const geometry of geometries) {
50
+ const axis = geometry ? visit(geometry) : null
51
+ if (axis) return axis
52
+ }
53
+ return null
54
+ }
55
+
56
+ /**
57
+ * The plan's twin of a 3D reveal: a node that rises in 3D and has a
58
+ * centreline grows along it from its start; everything else fades in. An undo's
59
+ * reverse play reads the same look backwards: a wall's clip shrinks toward its
60
+ * start, everything else fades out.
61
+ */
62
+ export function floorplanRevealLook(
63
+ state: RevealPlanState,
64
+ axis: FloorplanRevealAxis | null,
65
+ ): FloorplanRevealLook {
66
+ const eased = easeOutCubic(Math.min(1, Math.max(0, state.progress)))
67
+ if (state.style !== 'rise' || !axis) return { opacity: eased, clip: null }
68
+ const length = Math.hypot(axis.x2 - axis.x1, axis.y2 - axis.y1)
69
+ return {
70
+ opacity: 1,
71
+ clip: {
72
+ x: axis.x1 - CLIP_PAD_M,
73
+ y: axis.y1 - CLIP_HALF_WIDTH_M,
74
+ width: CLIP_PAD_M + eased * (length + CLIP_PAD_M),
75
+ height: CLIP_HALF_WIDTH_M * 2,
76
+ rotateDeg: (Math.atan2(axis.y2 - axis.y1, axis.x2 - axis.x1) * 180) / Math.PI,
77
+ },
78
+ }
79
+ }
80
+
81
+ const SVG_NS = 'http://www.w3.org/2000/svg'
82
+ let clipIds = 0
83
+
84
+ /**
85
+ * Plays a node's construction reveal on its plan entry: while the reveal
86
+ * runs, each tick sets the entry group's opacity or its growing clip, then
87
+ * clears them when the node is done. Imperative, so a reveal re-renders no
88
+ * React tree; an entry with nothing revealing subscribes to nothing.
89
+ */
90
+ export function useFloorplanReveal(
91
+ nodeId: string,
92
+ groupRef: RefObject<SVGGElement | null>,
93
+ entry: FloorplanRevealEntry | null,
94
+ waiting: boolean,
95
+ /** The reveal has something to play for the node (it moves in, or an undo takes it back): the effect runs again. */
96
+ active = false,
97
+ ) {
98
+ useEffect(() => {
99
+ const group = groupRef.current
100
+ if (waiting || !group || !getRevealPlanState(nodeId)) return
101
+ const axis = entry ? floorplanRevealAxis([entry.base, entry.overlay, entry.handles]) : null
102
+ let clip: SVGClipPathElement | null = null
103
+ let rect: SVGRectElement | null = null
104
+ let stop = () => {}
105
+ const clear = () => {
106
+ stop()
107
+ group.removeAttribute('opacity')
108
+ group.removeAttribute('clip-path')
109
+ clip?.remove()
110
+ clip = null
111
+ }
112
+ const apply = () => {
113
+ const state = getRevealPlanState(nodeId)
114
+ if (!state) return clear()
115
+ const look = floorplanRevealLook(state, axis)
116
+ group.setAttribute('opacity', String(look.opacity))
117
+ if (!look.clip) return
118
+ if (!(clip && rect)) {
119
+ clipIds += 1
120
+ const id = `floorplan-reveal-${clipIds}`
121
+ clip = document.createElementNS(SVG_NS, 'clipPath')
122
+ clip.setAttribute('id', id)
123
+ clip.setAttribute('clipPathUnits', 'userSpaceOnUse')
124
+ rect = document.createElementNS(SVG_NS, 'rect')
125
+ clip.appendChild(rect)
126
+ group.appendChild(clip)
127
+ group.setAttribute('clip-path', `url(#${id})`)
128
+ }
129
+ const { x, y, width, height, rotateDeg } = look.clip
130
+ rect.setAttribute('x', String(x))
131
+ rect.setAttribute('y', String(y))
132
+ rect.setAttribute('width', String(width))
133
+ rect.setAttribute('height', String(height))
134
+ rect.setAttribute('transform', `rotate(${rotateDeg} ${axis?.x1 ?? 0} ${axis?.y1 ?? 0})`)
135
+ }
136
+ stop = subscribeRevealTicks(apply)
137
+ apply()
138
+ return clear
139
+ // biome-ignore lint/correctness/useExhaustiveDependencies: `active` only says the reveal's state may have changed
140
+ }, [nodeId, groupRef, entry, waiting, active])
141
+ }