@pascal-app/editor 1.0.4 → 1.0.6

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 +40 -38
  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,354 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNodeId,
5
+ type BaseNode,
6
+ type BuildingNode,
7
+ type CameraPose,
8
+ emitter,
9
+ type LevelNode,
10
+ resolveBuildingForLevel,
11
+ useScene,
12
+ type ZoneNode,
13
+ } from '@pascal-app/core'
14
+ import {
15
+ groundVeiled,
16
+ type RevealEvent,
17
+ revealChangedNodeIds,
18
+ subscribeRevealEvents,
19
+ useViewer,
20
+ } from '@pascal-app/viewer'
21
+ import { useFrame, useThree } from '@react-three/fiber'
22
+ import { useEffect, useMemo, useRef } from 'react'
23
+ import { Vector3 } from 'three'
24
+ import {
25
+ clearFrame,
26
+ Follower,
27
+ type FollowWant,
28
+ frameOf,
29
+ OrbitSpring,
30
+ type OrbitValue,
31
+ } from '../../lib/follow-pascal'
32
+ import { anyExterior } from '../../lib/follow-pascal-exterior'
33
+ import { FollowPresentation, type PresentationPort } from '../../lib/follow-pascal-presentation'
34
+ import { FollowSight } from '../../lib/follow-pascal-sight'
35
+ import { stayOnLevel, wantFromEvent } from '../../lib/follow-pascal-targets'
36
+ import useEditor from '../../store/use-editor'
37
+ import useFollowPascal from '../../store/use-follow-pascal'
38
+ import { usePointAsk } from '../../store/use-point-ask'
39
+
40
+ /**
41
+ * Follow Pascal: while a build plays, the camera eases to what the current step is working on, framed
42
+ * at the distance its scale needs. It listens to the reveal's own words (`subscribeRevealEvents`), so
43
+ * a step and the camera share one rhythm, and moves like a critically damped spring on an orbit about
44
+ * what it looks at, so the horizon stays level. Touching the camera, or choosing a view, pauses it; the
45
+ * person resumes it from the pill, or the next build takes it again. Under reduced motion it cuts.
46
+ *
47
+ * To see a build it changes the presentation: it cuts the walls away when no eye sees the target,
48
+ * selects the floor being built and hides the floors above it. An edit or a move seen from outside
49
+ * (an exterior wall bent, a window moved on one, a roof, the ground) is looked at from outdoors with
50
+ * the walls up instead: a cutaway would hide the very wall that moves. A commit with one such change
51
+ * in it takes the outside view for all of it. A build is followed as it always was. When Follow ends (switched off, the person takes the
52
+ * camera, the build is over and no turn is running) it puts back what it changed, and leaves what
53
+ * the person changed since (`FollowPresentation`).
54
+ */
55
+
56
+ /** A build is over this long after its last word; the pill goes with it. */
57
+ const BUILD_OVER_MS = 4000
58
+ /** Close enough to its goal that the flight is over. */
59
+ const ARRIVED = { position: 0.01, angle: 0.002 }
60
+
61
+ /** Every way a person takes the camera. */
62
+ const TAKEN = [
63
+ 'camera-controls:interaction-start',
64
+ 'camera-controls:view',
65
+ 'camera-controls:focus',
66
+ 'camera-controls:top-view',
67
+ 'camera-controls:orbit-cw',
68
+ 'camera-controls:orbit-ccw',
69
+ 'camera-controls:fit-scene',
70
+ ] as const
71
+
72
+ /** The viewer and the editor as `FollowPresentation` reads and writes them. */
73
+ const viewPort: PresentationPort = {
74
+ read: () => {
75
+ const viewer = useViewer.getState()
76
+ const { selection } = viewer
77
+ return {
78
+ wall: viewer.wallMode,
79
+ levels: viewer.levelMode,
80
+ hideAbove: viewer.hideLevelsAboveSelection,
81
+ floor: {
82
+ buildingId: selection.buildingId,
83
+ levelId: selection.levelId,
84
+ zoneId: selection.zoneId,
85
+ selectedIds: selection.selectedIds,
86
+ room: useEditor.getState().room,
87
+ },
88
+ }
89
+ },
90
+ setWall: (mode) => useViewer.getState().setWallMode(mode),
91
+ setLevels: (mode) => useViewer.getState().setLevelMode(mode),
92
+ setHideAbove: (hide) => useViewer.setState({ hideLevelsAboveSelection: hide }),
93
+ setFloor: ({ buildingId, levelId }) => {
94
+ useViewer.getState().setSelection({
95
+ ...(buildingId ? { buildingId: buildingId as BuildingNode['id'] } : {}),
96
+ levelId: levelId as LevelNode['id'],
97
+ })
98
+ useEditor.getState().clearRoom()
99
+ },
100
+ putFloor: (floor) => {
101
+ const nodes = useScene.getState().nodes
102
+ const exists = (id: string | null) => id !== null && !!nodes[id as AnyNodeId]
103
+ useViewer.getState().setSelection({
104
+ buildingId: exists(floor.buildingId) ? (floor.buildingId as BuildingNode['id']) : null,
105
+ levelId: exists(floor.levelId) ? (floor.levelId as LevelNode['id']) : null,
106
+ zoneId: exists(floor.zoneId) ? (floor.zoneId as ZoneNode['id']) : null,
107
+ selectedIds: floor.selectedIds.filter(exists) as BaseNode['id'][],
108
+ })
109
+ const { room } = floor
110
+ useEditor.setState({
111
+ room: room && exists(room.zoneId) && exists(room.levelId) ? room : null,
112
+ hoveredRoom: null,
113
+ })
114
+ },
115
+ }
116
+
117
+ const prefersReducedMotion = () =>
118
+ typeof window !== 'undefined' &&
119
+ typeof window.matchMedia === 'function' &&
120
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches
121
+
122
+ /** The canvas dips and comes back, so a cut reads as a cross-fade rather than a jump. */
123
+ function dipCanvas() {
124
+ const canvas = document.querySelector<HTMLElement>('[data-pascal-viewer-3d]')
125
+ canvas?.animate([{ opacity: 0.35 }, { opacity: 1 }], { duration: 240, easing: 'ease-out' })
126
+ }
127
+
128
+ const arrived = (value: OrbitValue, goal: OrbitValue) =>
129
+ Math.abs(value.x - goal.x) < ARRIVED.position &&
130
+ Math.abs(value.y - goal.y) < ARRIVED.position &&
131
+ Math.abs(value.z - goal.z) < ARRIVED.position &&
132
+ Math.abs(value.r - goal.r) < ARRIVED.position &&
133
+ Math.abs(value.az - goal.az) < ARRIVED.angle &&
134
+ Math.abs(value.el - goal.el) < ARRIVED.angle
135
+
136
+ export function FollowPascal() {
137
+ const camera = useThree((state) => state.camera)
138
+ const scene = useThree((state) => state.scene)
139
+ const sight = useMemo(() => new FollowSight(scene), [scene])
140
+ const presentation = useMemo(() => new FollowPresentation(viewPort), [])
141
+ const controls = useThree((state) => state.controls) as {
142
+ getTarget?: (out: Vector3) => Vector3
143
+ } | null
144
+ const size = useThree((state) => state.size)
145
+ const follower = useRef(new Follower())
146
+ const spring = useRef<OrbitSpring | null>(null)
147
+ const clock = useRef(0)
148
+ const flying = useRef(false)
149
+ const synced = useRef(false)
150
+ const last = useRef<FollowWant | null>(null)
151
+ const resuming = useRef(false)
152
+ const over = useRef<ReturnType<typeof setTimeout> | null>(null)
153
+ const scratch = useRef(new Vector3())
154
+ const nextSightCheck = useRef(0)
155
+
156
+ /** The camera as it is now, so a flight starts from the view the person has. */
157
+ const currentPose = (): CameraPose => {
158
+ const target = controls?.getTarget?.(scratch.current) ?? scratch.current.set(0, 0, 0)
159
+ return {
160
+ position: [camera.position.x, camera.position.y, camera.position.z],
161
+ target: [target.x, target.y, target.z],
162
+ projection: 'perspective',
163
+ }
164
+ }
165
+
166
+ /** The build in hand has a change seen from outside in it: all of it is looked at from outdoors. */
167
+ const outsideBuild = useRef(false)
168
+ /** The last word was that the play is over: the next edit is another commit's. */
169
+ const played = useRef(false)
170
+
171
+ const unobstructed = (want: FollowWant, goal: OrbitValue): OrbitValue => {
172
+ sight.refresh(want)
173
+ const clear = clearFrame(goal, (pose) => sight.clear(pose))
174
+ if (clear) return clear
175
+ // Seen from outside the walls stay up: a cutaway would take the wall that changes away.
176
+ if (want.exterior || outsideBuild.current) {
177
+ presentation.wall('up')
178
+ return goal
179
+ }
180
+ presentation.wall('cutaway')
181
+ return goal
182
+ }
183
+
184
+ const moveTo = (goal: OrbitValue) => {
185
+ const orbit = spring.current!
186
+ if (prefersReducedMotion()) {
187
+ orbit.snap(goal)
188
+ emitter.emit('camera-controls:apply-pose', orbit.pose())
189
+ dipCanvas()
190
+ flying.current = false
191
+ return
192
+ }
193
+ orbit.to(goal)
194
+ flying.current = true
195
+ }
196
+
197
+ const fly = (asked: FollowWant) => {
198
+ const nodes = useScene.getState().nodes
199
+ // A level below grade is shown, its ground open over it: the look at the whole house that follows
200
+ // furniture landing, and every finished commit, stays on that level. Showing the floors above would
201
+ // close the ground, and the next piece on the basement open it again, once for every commit.
202
+ const { selection, hideLevelsAboveSelection } = useViewer.getState()
203
+ const staying = hideLevelsAboveSelection && groundVeiled(useViewer.getState())
204
+ const want =
205
+ asked.kind === 'group' && !asked.levelId && staying && selection.levelId
206
+ ? stayOnLevel(asked, nodes, selection.levelId)
207
+ : asked
208
+ last.current = want
209
+ const level = want.levelId ? nodes[want.levelId as AnyNodeId] : undefined
210
+ if (level?.type === 'level') {
211
+ const buildingId = resolveBuildingForLevel(level.id as AnyNodeId, nodes)
212
+ const building = buildingId ? nodes[buildingId] : undefined
213
+ presentation.floor({
214
+ buildingId: building?.type === 'building' ? building.id : null,
215
+ levelId: level.id,
216
+ })
217
+ presentation.levels('stacked')
218
+ presentation.hideAbove(true)
219
+ } else if (want.kind === 'group' && !want.levelId) {
220
+ // The finished house is looked at whole: its upper floors are not hidden.
221
+ presentation.levels('stacked')
222
+ presentation.hideAbove(false)
223
+ }
224
+ // A change seen from outside: the walls up for it, even if an earlier step cut them away.
225
+ if (want.exterior || outsideBuild.current) presentation.wall('up')
226
+ if (!spring.current) {
227
+ spring.current = new OrbitSpring({ x: 0, y: 1, z: 0, r: 20, az: 0.6, el: 0.5 })
228
+ synced.current = false
229
+ }
230
+ if (!synced.current) {
231
+ spring.current.fromPose(currentPose())
232
+ synced.current = true
233
+ }
234
+ const perspective = camera as { fov?: number }
235
+ const goal = frameOf(want, {
236
+ fov: perspective.fov ?? 45,
237
+ aspect: size.width / Math.max(1, size.height),
238
+ azimuth: spring.current.value().az,
239
+ })
240
+ moveTo(unobstructed(want, goal))
241
+ nextSightCheck.current = clock.current + 0.5
242
+ }
243
+
244
+ // What the build says it is doing: where to look, and when a build begins and ends.
245
+ useEffect(() => {
246
+ const hear = (event: RevealEvent) => {
247
+ const store = useFollowPascal.getState()
248
+ if (over.current) clearTimeout(over.current)
249
+ if (!store.building) {
250
+ store.beginBuild()
251
+ follower.current.reset()
252
+ synced.current = false
253
+ outsideBuild.current = false
254
+ }
255
+ // An edit or a move is read before its first piece plays: one change seen from outside takes the
256
+ // whole commit outdoors, until the play is over. What is waiting in the queue belongs to a later
257
+ // commit, so a build, a deletion, or the edit that follows a finished play never inherits it.
258
+ const edit = 'changed' in event && event.changed === true
259
+ if (!edit) outsideBuild.current = false
260
+ else {
261
+ if (played.current) outsideBuild.current = false
262
+ const changed = outsideBuild.current ? [] : revealChangedNodeIds()
263
+ if (changed.length && anyExterior(useScene.getState().nodes, changed))
264
+ outsideBuild.current = true
265
+ }
266
+ played.current = event.type === 'complete'
267
+ over.current = setTimeout(() => useFollowPascal.getState().endBuild(), BUILD_OVER_MS)
268
+ if (event.type === 'complete') return
269
+ const want = wantFromEvent(event, useScene.getState().nodes, clock.current)
270
+ if (want) follower.current.want(want)
271
+ }
272
+ const stop = subscribeRevealEvents(hear)
273
+ return () => {
274
+ stop()
275
+ if (over.current) clearTimeout(over.current)
276
+ }
277
+ }, [])
278
+
279
+ // The person takes the camera: it stays theirs, and the next flight starts from where they leave it.
280
+ useEffect(() => {
281
+ const take = () => {
282
+ flying.current = false
283
+ synced.current = false
284
+ useFollowPascal.getState().pause()
285
+ }
286
+ for (const name of TAKEN) emitter.on(name, take)
287
+ return () => {
288
+ for (const name of TAKEN) emitter.off(name, take)
289
+ }
290
+ }, [])
291
+
292
+ // Resuming flies back to what it was last looking at; switching it off ends a flight.
293
+ // Follow ends when it is switched off and when the person takes the camera: what it changed to see
294
+ // the build goes back. A build being over ends it only when no turn is running: a turn runs several
295
+ // programs, and the view holds steady between them (the host's busy flag, below).
296
+ useEffect(
297
+ () =>
298
+ useFollowPascal.subscribe((state, before) => {
299
+ if (before.paused && !state.paused && state.on) resuming.current = true
300
+ if (before.on && !state.on) flying.current = false
301
+ if ((before.on && !state.on) || (!before.paused && state.paused)) presentation.restore()
302
+ // The build is over: that ends it when no turn is running to build more.
303
+ if (before.building && !state.building && !usePointAsk.getState().busy)
304
+ presentation.restore()
305
+ }),
306
+ [presentation],
307
+ )
308
+
309
+ // The turn ends, or the person stops it: the host's busy flag is the editor's word on it. What the
310
+ // reveal still plays after that is still followed, so it ends with the build, not with the turn.
311
+ useEffect(
312
+ () =>
313
+ usePointAsk.subscribe((state, before) => {
314
+ if (before.busy && !state.busy && !useFollowPascal.getState().building)
315
+ presentation.restore()
316
+ }),
317
+ [presentation],
318
+ )
319
+
320
+ // Leaving the editor ends it too.
321
+ useEffect(() => () => presentation.restore(), [presentation])
322
+
323
+ useFrame((_, delta) => {
324
+ clock.current += Math.min(delta, 0.1)
325
+ const { on, paused, building } = useFollowPascal.getState()
326
+ if (!on || paused) return
327
+ if (useViewer.getState().cameraMode !== 'perspective' || useEditor.getState().isFirstPersonMode)
328
+ return
329
+ if (resuming.current) {
330
+ resuming.current = false
331
+ if (last.current) fly(last.current)
332
+ }
333
+ const due = follower.current.due(clock.current)
334
+ if (due) fly(due)
335
+ const orbit = spring.current
336
+ // Recheck as geometry lands, even after arrival; retain a clear orbit instead of hunting each frame.
337
+ if (
338
+ orbit &&
339
+ last.current &&
340
+ (building || flying.current) &&
341
+ clock.current >= nextSightCheck.current
342
+ ) {
343
+ nextSightCheck.current = clock.current + 0.5
344
+ const goal = unobstructed(last.current, orbit.destination())
345
+ if (!arrived(goal, orbit.destination())) moveTo(goal)
346
+ }
347
+ if (!(flying.current && orbit)) return
348
+ orbit.step(Math.min(delta, 0.05))
349
+ emitter.emit('camera-controls:apply-pose', orbit.pose())
350
+ if (arrived(orbit.value(), orbit.destination())) flying.current = false
351
+ })
352
+
353
+ return null
354
+ }
@@ -28,6 +28,7 @@ import {
28
28
  pasteSystemEditorClipboardToLevel,
29
29
  } from '../../lib/scene-clipboard'
30
30
  import { emitDeleteSFX, sfxEmitter } from '../../lib/sfx-bus'
31
+ import { isTypingTarget } from '../../lib/typing-target'
31
32
  import useAlignmentGuides from '../../store/use-alignment-guides'
32
33
  import useDeleteConfirmation from '../../store/use-delete-confirmation'
33
34
  import useEditor, {
@@ -370,6 +371,7 @@ export function startGroupPickUp(
370
371
  }
371
372
 
372
373
  const onKeyDown = (e: KeyboardEvent) => {
374
+ if (isTypingTarget(e.target)) return
373
375
  const key = e.key.toLowerCase()
374
376
  if ((e.metaKey || e.ctrlKey) && (key === 'c' || key === 'v' || key === 'x')) {
375
377
  // A clipboard chord replaces the current carry. Let the global keyboard
@@ -17,6 +17,7 @@ import { type Camera, Plane, type Raycaster, Vector2, Vector3 } from 'three'
17
17
  import { GROUP_MOVE_DRAG_LABEL } from '../../lib/contextual-help'
18
18
  import { isHistoryShortcut } from '../../lib/history'
19
19
  import { sfxEmitter } from '../../lib/sfx-bus'
20
+ import { isTypingTarget } from '../../lib/typing-target'
20
21
  import useAlignmentGuides from '../../store/use-alignment-guides'
21
22
  import useEditor, {
22
23
  isAlignmentGuideActive,
@@ -380,6 +381,7 @@ export function armGroupMove3d(args: {
380
381
  // `use-keyboard` arms, which would otherwise act on stale store state
381
382
  // (or clear the multi-selection) mid-session.
382
383
  const onKeyDown = (e: KeyboardEvent) => {
384
+ if (isTypingTarget(e.target)) return
383
385
  const key = e.key.toLowerCase()
384
386
  if ((key === 'r' || key === 't') && !e.metaKey && !e.ctrlKey && !e.altKey && !e.shiftKey) {
385
387
  // Armed but still under the drag threshold: engage first (exactly what
@@ -19,6 +19,7 @@ import { isHistoryShortcut } from '../../lib/history'
19
19
  import { sfxEmitter } from '../../lib/sfx-bus'
20
20
  import { intersectSpatialDragPlane } from '../../lib/spatial-drag-plane'
21
21
  import { getSpatialPointerId, spatialPointerInput } from '../../lib/spatial-pointer-input'
22
+ import { isTypingTarget } from '../../lib/typing-target'
22
23
  import useEditor, { isAngleSnapActive } from '../../store/use-editor'
23
24
  import useInteractionScope, {
24
25
  useActiveHandleDrag,
@@ -377,6 +378,7 @@ function GroupRotateHandleInner({ ids, meshEpoch }: { ids: string[]; meshEpoch:
377
378
  // Escape / ⌘Z abort the rotate — capture phase so they win over the global
378
379
  // use-keyboard arms (⌘Z must never history-jump under a live pointer).
379
380
  const onKeyDown = (e: KeyboardEvent) => {
381
+ if (isTypingTarget(e.target)) return
380
382
  if (e.key === 'Alt') {
381
383
  altKey = true
382
384
  return