@pascal-app/editor 1.0.0-beta.4 → 1.0.0

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 (283) hide show
  1. package/LICENSE +21 -0
  2. package/bunfig.toml +4 -0
  3. package/package.json +15 -8
  4. package/scripts/generate-print-golden-house.ts +89 -0
  5. package/src/components/editor/alignment-3d-guide-layer.tsx +12 -3
  6. package/src/components/editor/bake-exporter.tsx +1 -0
  7. package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
  8. package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
  9. package/src/components/editor/capture-camera-rig.tsx +59 -0
  10. package/src/components/editor/custom-camera-controls.tsx +20 -11
  11. package/src/components/editor/editor-layout-mobile.tsx +6 -6
  12. package/src/components/editor/editor-layout-v2.tsx +17 -5
  13. package/src/components/editor/elevation-3d-guide-layer.tsx +3 -2
  14. package/src/components/editor/export-manager.tsx +175 -33
  15. package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
  16. package/src/components/editor/first-person-controls.tsx +182 -57
  17. package/src/components/editor/floating-action-menu.tsx +55 -55
  18. package/src/components/editor/floorplan-background-selection.test.ts +31 -4
  19. package/src/components/editor/floorplan-background-selection.ts +27 -7
  20. package/src/components/editor/floorplan-panel.tsx +81 -57
  21. package/src/components/editor/grid.tsx +12 -7
  22. package/src/components/editor/group-actions.ts +34 -50
  23. package/src/components/editor/group-floating-action-menu.tsx +30 -15
  24. package/src/components/editor/group-move-3d.ts +51 -21
  25. package/src/components/editor/group-transform-shared.test.ts +78 -0
  26. package/src/components/editor/group-transform-shared.ts +74 -7
  27. package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
  28. package/src/components/editor/handles/handle-arrow.tsx +109 -48
  29. package/src/components/editor/handles/linear-resize-drag.ts +67 -0
  30. package/src/components/editor/handles/resize-snap.test.ts +46 -0
  31. package/src/components/editor/handles/resize-snap.ts +10 -1
  32. package/src/components/editor/handles/use-handle-drag.ts +18 -1
  33. package/src/components/editor/index.tsx +252 -46
  34. package/src/components/editor/measurement-pill.tsx +12 -12
  35. package/src/components/editor/node-action-menu.tsx +42 -1
  36. package/src/components/editor/node-arrow-handles.tsx +47 -58
  37. package/src/components/editor/opening-guides-3d-layer.tsx +13 -4
  38. package/src/components/editor/selection-manager.tsx +132 -47
  39. package/src/components/editor/site-edge-labels.tsx +2 -1
  40. package/src/components/editor/snapshot-capture-overlay.tsx +309 -41
  41. package/src/components/editor/snapshot-capture.test.ts +422 -0
  42. package/src/components/editor/snapshot-capture.ts +145 -0
  43. package/src/components/editor/thumbnail-generator.tsx +295 -232
  44. package/src/components/editor/use-floorplan-background-placement.ts +53 -5
  45. package/src/components/editor/wall-measurement-label.tsx +3 -2
  46. package/src/components/editor/wall-move-side-handles.tsx +299 -7
  47. package/src/components/editor-2d/floorplan-group-action-menu.tsx +23 -16
  48. package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
  49. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
  50. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
  51. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
  52. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
  53. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
  54. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
  55. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
  56. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +204 -114
  57. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
  58. package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
  59. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
  60. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
  61. package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
  62. package/src/components/systems/roof/roof-edit-system.tsx +10 -19
  63. package/src/components/systems/selection-affordance-manager.tsx +59 -9
  64. package/src/components/systems/selection-affordance-services.ts +19 -0
  65. package/src/components/systems/stair/stair-edit-system.tsx +5 -2
  66. package/src/components/tools/fence/fence-drafting.test.ts +100 -0
  67. package/src/components/tools/fence/fence-drafting.ts +6 -25
  68. package/src/components/tools/item/draft-creation.test.ts +12 -0
  69. package/src/components/tools/item/draft-creation.ts +10 -0
  70. package/src/components/tools/item/face-host-commit.test.ts +65 -0
  71. package/src/components/tools/item/face-host-commit.ts +56 -0
  72. package/src/components/tools/item/face-host-preview.test.ts +77 -0
  73. package/src/components/tools/item/face-host-preview.ts +82 -0
  74. package/src/components/tools/item/move-tool.tsx +4 -3
  75. package/src/components/tools/item/placement-math.test.ts +11 -1
  76. package/src/components/tools/item/placement-math.ts +0 -20
  77. package/src/components/tools/item/placement-strategies.test.ts +494 -0
  78. package/src/components/tools/item/placement-strategies.ts +192 -24
  79. package/src/components/tools/item/placement-surface.test.ts +61 -0
  80. package/src/components/tools/item/placement-surface.ts +24 -0
  81. package/src/components/tools/item/placement-types.ts +6 -0
  82. package/src/components/tools/item/test-face-host.ts +121 -0
  83. package/src/components/tools/item/use-draft-node.test.tsx +336 -0
  84. package/src/components/tools/item/use-draft-node.ts +26 -4
  85. package/src/components/tools/item/use-placement-coordinator.tsx +447 -180
  86. package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
  87. package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
  88. package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
  89. package/src/components/tools/registry-tool-context.tsx +30 -0
  90. package/src/components/tools/select/box-select-tool.tsx +3 -1
  91. package/src/components/tools/select/marquee-footprint.test.ts +53 -0
  92. package/src/components/tools/select/marquee-footprint.ts +29 -0
  93. package/src/components/tools/shared/placement-box.tsx +63 -8
  94. package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
  95. package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
  96. package/src/components/tools/shared/pointer-support-cap.ts +45 -8
  97. package/src/components/tools/site/terrain-brush-cursor.tsx +2 -1
  98. package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
  99. package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
  100. package/src/components/tools/stair/stair-tool.tsx +127 -74
  101. package/src/components/tools/tool-manager.tsx +25 -8
  102. package/src/components/tools/wall/wall-drafting.test.ts +527 -4
  103. package/src/components/tools/wall/wall-drafting.ts +55 -462
  104. package/src/components/ui/action-menu/control-modes.tsx +9 -6
  105. package/src/components/ui/action-menu/index.tsx +11 -1
  106. package/src/components/ui/command-palette/editor-commands.tsx +12 -13
  107. package/src/components/ui/command-palette/index.tsx +7 -2
  108. package/src/components/ui/controls/material-paint-panel.tsx +6 -4
  109. package/src/components/ui/controls/material-picker.tsx +4 -4
  110. package/src/components/ui/controls/scene-material-list.tsx +6 -5
  111. package/src/components/ui/controls/segmented-control.tsx +8 -1
  112. package/src/components/ui/controls/slider-control.tsx +19 -3
  113. package/src/components/ui/controls/toggle-control.tsx +18 -5
  114. package/src/components/ui/controls/tool-options-panel.tsx +126 -0
  115. package/src/components/ui/floating-level-selector.tsx +23 -6
  116. package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
  117. package/src/components/ui/helpers/helper-manager.tsx +47 -11
  118. package/src/components/ui/helpers/item-helper.tsx +1 -1
  119. package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
  120. package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
  121. package/src/components/ui/panels/homogeneous-selection.ts +47 -0
  122. package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
  123. package/src/components/ui/panels/multi-field-value.test.ts +209 -0
  124. package/src/components/ui/panels/multi-field-value.ts +163 -0
  125. package/src/components/ui/panels/multi-height-mode.tsx +185 -0
  126. package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
  127. package/src/components/ui/panels/multi-selection-panel.tsx +78 -26
  128. package/src/components/ui/panels/node-display.ts +4 -0
  129. package/src/components/ui/panels/panel-manager.tsx +77 -3
  130. package/src/components/ui/panels/panel-wrapper.tsx +228 -12
  131. package/src/components/ui/panels/parametric-field-control.tsx +173 -0
  132. package/src/components/ui/panels/parametric-field-utils.ts +18 -0
  133. package/src/components/ui/panels/parametric-inspector.tsx +29 -162
  134. package/src/components/ui/panels/reference-panel.tsx +31 -1
  135. package/src/components/ui/primitives/shortcut-token.tsx +23 -1
  136. package/src/components/ui/scene-loader.tsx +35 -0
  137. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +7 -2
  138. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
  139. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +31 -23
  140. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
  141. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
  142. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
  143. package/src/components/ui/sidebar/panels/site-panel/index.tsx +128 -46
  144. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
  145. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
  146. package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
  147. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +22 -7
  148. package/src/components/ui/sidebar/tab-bar.tsx +9 -1
  149. package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
  150. package/src/components/viewer/floorplan-compass-button.tsx +50 -0
  151. package/src/components/viewer/floorplan-preview-geometry.test.ts +107 -0
  152. package/src/components/viewer/floorplan-preview-geometry.ts +281 -0
  153. package/src/components/viewer/floorplan-preview-navigation.test.ts +43 -0
  154. package/src/components/viewer/floorplan-preview-navigation.ts +62 -0
  155. package/src/components/viewer/floorplan-preview.tsx +1069 -0
  156. package/src/components/viewer/use-viewer-camera-navigation-sync.ts +140 -0
  157. package/src/components/viewer/viewer-scene-header.tsx +2 -1
  158. package/src/components/viewer/viewer-stage-modes.test.ts +38 -0
  159. package/src/components/viewer/viewer-stage-modes.ts +49 -0
  160. package/src/components/viewer/viewer-stage-switcher.tsx +95 -0
  161. package/src/components/viewer/viewer-stage.test.tsx +47 -0
  162. package/src/components/viewer/viewer-stage.tsx +221 -0
  163. package/src/components/viewer-overlay.tsx +13 -30
  164. package/src/components/viewer-zone-system.tsx +6 -2
  165. package/src/components/walkthrough-hud.tsx +19 -10
  166. package/src/hooks/use-auto-save.test.ts +122 -1
  167. package/src/hooks/use-auto-save.ts +119 -13
  168. package/src/hooks/use-ceiling-events.test.ts +122 -0
  169. package/src/hooks/use-drag-action.ts +17 -3
  170. package/src/hooks/use-grid-events.test.ts +240 -0
  171. package/src/hooks/use-grid-events.ts +190 -11
  172. package/src/hooks/use-keyboard.test.ts +244 -0
  173. package/src/hooks/use-keyboard.ts +164 -63
  174. package/src/hooks/use-registered-tool-enabled.ts +20 -0
  175. package/src/hooks/use-save-shortcut.ts +33 -0
  176. package/src/index.tsx +109 -4
  177. package/src/lib/active-placement-surface.test.ts +16 -0
  178. package/src/lib/active-placement-surface.ts +14 -0
  179. package/src/lib/ceiling-surface-raycast.test.ts +64 -0
  180. package/src/lib/ceiling-surface-raycast.ts +12 -0
  181. package/src/lib/contextual-help-extension.ts +17 -0
  182. package/src/lib/contextual-help.test.ts +8 -0
  183. package/src/lib/contextual-help.ts +8 -0
  184. package/src/lib/continuation.test.ts +11 -0
  185. package/src/lib/continuation.ts +8 -1
  186. package/src/lib/direct-manipulation.test.ts +132 -0
  187. package/src/lib/direct-manipulation.ts +38 -1
  188. package/src/lib/editor-api.ts +6 -3
  189. package/src/lib/elevation-guides.test.ts +28 -1
  190. package/src/lib/elevation-guides.ts +22 -7
  191. package/src/lib/floating-menu-scale.test.ts +29 -0
  192. package/src/lib/floating-menu-scale.ts +14 -0
  193. package/src/lib/floorplan/apply-alignment.test.ts +47 -2
  194. package/src/lib/floorplan/apply-alignment.ts +10 -1
  195. package/src/lib/floorplan/floorplan-export.test.ts +397 -1
  196. package/src/lib/floorplan/floorplan-export.tsx +111 -16
  197. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
  198. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
  199. package/src/lib/floorplan/floorplan-readonly.test.ts +47 -0
  200. package/src/lib/floorplan/floorplan-readonly.ts +85 -0
  201. package/src/lib/floorplan-grid-event-point.test.ts +29 -0
  202. package/src/lib/floorplan-grid-event-point.ts +13 -0
  203. package/src/lib/glb-export.test.ts +1169 -21
  204. package/src/lib/glb-export.ts +629 -101
  205. package/src/lib/grid-event-presentation.ts +16 -0
  206. package/src/lib/history.test.ts +523 -77
  207. package/src/lib/history.ts +133 -14
  208. package/src/lib/host-tree-children.test.ts +27 -0
  209. package/src/lib/host-tree-children.ts +79 -0
  210. package/src/lib/inspector-card-mode.test.ts +92 -0
  211. package/src/lib/inspector-card-mode.ts +60 -0
  212. package/src/lib/interaction/hot-set.test.ts +10 -1
  213. package/src/lib/interaction/hot-set.ts +3 -1
  214. package/src/lib/interaction/overlay-policy.test.ts +33 -1
  215. package/src/lib/interaction/overlay-policy.ts +16 -0
  216. package/src/lib/interaction/registered-drafting.ts +36 -0
  217. package/src/lib/interaction/scope.ts +31 -0
  218. package/src/lib/level-print-export.test.ts +484 -0
  219. package/src/lib/level-print-export.ts +526 -0
  220. package/src/lib/local-project-presentation-persistence.test.ts +302 -0
  221. package/src/lib/local-project-presentation-persistence.ts +335 -0
  222. package/src/lib/material-paint.ts +2 -0
  223. package/src/lib/measurement-parser.ts +3 -0
  224. package/src/lib/measurements.test.ts +4 -0
  225. package/src/lib/model-export.ts +29 -0
  226. package/src/lib/paint-preview-owner.ts +71 -0
  227. package/src/lib/paint-scope.test.ts +58 -2
  228. package/src/lib/paint-scope.ts +8 -2
  229. package/src/lib/planar-cursor-placement.test.ts +178 -1
  230. package/src/lib/planar-cursor-placement.ts +61 -5
  231. package/src/lib/plugin-panels.test.ts +92 -1
  232. package/src/lib/plugin-panels.ts +48 -0
  233. package/src/lib/portable-export.test.ts +366 -0
  234. package/src/lib/portable-export.ts +969 -0
  235. package/src/lib/print-3mf.test.ts +86 -0
  236. package/src/lib/print-3mf.ts +161 -0
  237. package/src/lib/print-content-scope.test.ts +107 -0
  238. package/src/lib/print-content-scope.ts +90 -0
  239. package/src/lib/print-export.test.ts +294 -0
  240. package/src/lib/print-export.ts +735 -0
  241. package/src/lib/print-feature-thickness.test.ts +147 -0
  242. package/src/lib/print-feature-thickness.ts +180 -0
  243. package/src/lib/print-golden-house.test-fixture.ts +352 -0
  244. package/src/lib/print-golden-house.test.ts +249 -0
  245. package/src/lib/print-roof-solids.test.ts +133 -0
  246. package/src/lib/print-roof-solids.ts +630 -0
  247. package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
  248. package/src/lib/print-shell-compiler-baseline.ts +225 -0
  249. package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
  250. package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
  251. package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
  252. package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
  253. package/src/lib/print-shell-compiler-protocol.ts +30 -0
  254. package/src/lib/print-shell-compiler.ts +273 -0
  255. package/src/lib/print-wall-solids.test.ts +194 -0
  256. package/src/lib/print-wall-solids.ts +328 -0
  257. package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
  258. package/src/lib/rigid-plan-svg-transform.ts +24 -0
  259. package/src/lib/scene.ts +22 -8
  260. package/src/lib/selection-routing.test.ts +125 -6
  261. package/src/lib/selection-routing.ts +34 -1
  262. package/src/lib/session-groups.test.ts +88 -0
  263. package/src/lib/session-groups.ts +201 -0
  264. package/src/lib/sfx-bus.ts +4 -0
  265. package/src/lib/sfx-player.ts +8 -0
  266. package/src/lib/snapping-mode.test.ts +32 -4
  267. package/src/lib/snapping-mode.ts +11 -7
  268. package/src/lib/structured-clone-fallback.test.ts +45 -0
  269. package/src/lib/structured-clone-fallback.ts +24 -0
  270. package/src/lib/usdz-export.ts +38 -0
  271. package/src/lib/walkthrough-pointer-lock.ts +47 -0
  272. package/src/store/live-draft-preview-stores.test.ts +3 -0
  273. package/src/store/terrain-sculpt-mode.test.ts +3 -6
  274. package/src/store/tool-mode.test.ts +87 -0
  275. package/src/store/use-camera-hint-focus.ts +48 -0
  276. package/src/store/use-editor.tsx +299 -188
  277. package/src/store/use-floorplan-draft-preview.ts +9 -0
  278. package/src/store/use-interaction-scope.test.ts +23 -1
  279. package/src/store/use-interaction-scope.ts +63 -2
  280. package/src/store/use-placement-preview.ts +56 -3
  281. package/src/store/use-session-groups.ts +103 -0
  282. package/src/components/tools/roof/roof-tool.tsx +0 -656
  283. package/src/components/ui/helpers/roof-helper.tsx +0 -14
@@ -0,0 +1,20 @@
1
+ 'use client'
2
+
3
+ import { isNodeKindEnabled, useScene } from '@pascal-app/core'
4
+ import { useEffect } from 'react'
5
+ import useEditor, { type Tool } from '../store/use-editor'
6
+
7
+ export function useRegisteredToolEnabled(tool: Tool | null): boolean {
8
+ const installedPlugins = useScene((state) => state.installedPlugins)
9
+ const enabled = tool === null || isNodeKindEnabled(tool, installedPlugins)
10
+
11
+ useEffect(() => {
12
+ if (enabled || tool === null) return
13
+
14
+ // The render gate has already unmounted the tool, so its own cleanup cancels
15
+ // any draft before clearing the stale selection prevents reinstall remounts.
16
+ if (useEditor.getState().tool === tool) useEditor.getState().setTool(null)
17
+ }, [enabled, tool])
18
+
19
+ return enabled
20
+ }
@@ -0,0 +1,33 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useRef } from 'react'
4
+
5
+ /**
6
+ * Claims Cmd/Ctrl+S for the app's save.
7
+ *
8
+ * Capture phase and ungated on purpose: the browser's "Save page" dialog must
9
+ * never appear anywhere in the editor — including first-person, studio mode and
10
+ * while focus sits in an input, where people still expect the chord to save.
11
+ * `e.code` keeps it on the physical S key across keyboard layouts.
12
+ */
13
+ export function useSaveShortcut(onSave: () => void) {
14
+ const onSaveRef = useRef(onSave)
15
+
16
+ useEffect(() => {
17
+ onSaveRef.current = onSave
18
+ }, [onSave])
19
+
20
+ useEffect(() => {
21
+ const handleKeyDown = (e: KeyboardEvent) => {
22
+ if (!(e.metaKey || e.ctrlKey) || e.altKey || e.shiftKey) return
23
+ if (e.code !== 'KeyS') return
24
+
25
+ e.preventDefault()
26
+ e.stopPropagation()
27
+ onSaveRef.current()
28
+ }
29
+
30
+ window.addEventListener('keydown', handleKeyDown, true)
31
+ return () => window.removeEventListener('keydown', handleKeyDown, true)
32
+ }, [])
33
+ }
package/src/index.tsx CHANGED
@@ -56,6 +56,7 @@ export {
56
56
  formatMeasurement,
57
57
  MeasurementPill,
58
58
  } from './components/editor/measurement-pill'
59
+ export { NodeActionMenu } from './components/editor/node-action-menu'
59
60
  // In-world arrow handle primitives (chevron geometry, invisible hit area,
60
61
  // shared material, palette + scale constants). Re-exported so kind-owned
61
62
  // 3D selection affordances in `@pascal-app/nodes` (duct side-move / height /
@@ -98,6 +99,11 @@ export {
98
99
  buildSvgArrowHeadPoints,
99
100
  getArcPlanPoint,
100
101
  } from './components/editor-2d/svg-paths'
102
+ export type {
103
+ SelectionAffordanceHistoryApi,
104
+ SelectionAffordanceInteractionApi,
105
+ SelectionAffordanceProps,
106
+ } from './components/systems/selection-affordance-services'
101
107
  // Phase 5 Stage D transitional exports — pure drafting / angle helpers
102
108
  // consumed by kind-owned drag actions in @pascal-app/nodes. Stage F
103
109
  // cleanup moves these into @pascal-app/nodes (fence/drafting.ts +
@@ -113,7 +119,6 @@ export { MoveTool } from './components/tools/item/move-tool'
113
119
  // `@pascal-app/nodes` (wall curve sagitta snap, door / window placement,
114
120
  // item drop) so kinds don't reach into editor internals.
115
121
  export {
116
- calculateCursorRotation,
117
122
  calculateItemRotation,
118
123
  getSideFromNormal,
119
124
  isValidWallSideFace,
@@ -132,6 +137,7 @@ export {
132
137
  type PlacementCoordinatorConfig,
133
138
  usePlacementCoordinator,
134
139
  } from './components/tools/item/use-placement-coordinator'
140
+ export { useRegistryToolContext } from './components/tools/registry-tool-context'
135
141
  export { CursorSphere } from './components/tools/shared/cursor-sphere'
136
142
  export { DragBoundingBox } from './components/tools/shared/drag-bounding-box'
137
143
  export { getFloorStackPreviewPosition } from './components/tools/shared/floor-stack-preview'
@@ -144,6 +150,7 @@ export {
144
150
  resolveLevelConstructionPlane,
145
151
  } from './components/tools/shared/horizontal-construction-plane'
146
152
  export { PlacementBox } from './components/tools/shared/placement-box'
153
+ export { PlacementDimensionGuides } from './components/tools/shared/placement-dimension-guides'
147
154
  // Pointer-decided support surface (deck top vs floor underneath) — the
148
155
  // draw tools (wall / fence) ride their grid plane and commit cap on it.
149
156
  export {
@@ -234,6 +241,7 @@ export { SegmentedControl } from './components/ui/controls/segmented-control'
234
241
  export { SliderControl } from './components/ui/controls/slider-control'
235
242
  export { TerrainSculptPanel } from './components/ui/controls/terrain-sculpt-panel'
236
243
  export { ToggleControl } from './components/ui/controls/toggle-control'
244
+ export { ToolOptionsPanel } from './components/ui/controls/tool-options-panel'
237
245
  export { FloatingLevelSelector } from './components/ui/floating-level-selector'
238
246
  export { CATALOG_ITEMS } from './components/ui/item-catalog/catalog-items'
239
247
  // Item collections UI — used by the kind-owned ItemPanel in nodes/.
@@ -254,6 +262,10 @@ export {
254
262
  DropdownMenuSubTrigger,
255
263
  DropdownMenuTrigger,
256
264
  } from './components/ui/primitives/dropdown-menu'
265
+ export {
266
+ ShortcutToken,
267
+ shortcutDisplayValue,
268
+ } from './components/ui/primitives/shortcut-token'
257
269
  export { useSidebarStore } from './components/ui/primitives/sidebar'
258
270
  export { Slider } from './components/ui/primitives/slider'
259
271
  export { SceneLoader } from './components/ui/scene-loader'
@@ -274,6 +286,16 @@ export {
274
286
  SnapTargetBadge,
275
287
  SnapTargetIcon,
276
288
  } from './components/ui/snap-target-badge'
289
+ export {
290
+ FloorplanCompassButton,
291
+ type FloorplanCompassButtonProps,
292
+ } from './components/viewer/floorplan-compass-button'
293
+ export {
294
+ FloorplanPreview,
295
+ type FloorplanPreviewProps,
296
+ type FloorplanPreviewScene,
297
+ } from './components/viewer/floorplan-preview'
298
+ export { useViewerCameraNavigationSync } from './components/viewer/use-viewer-camera-navigation-sync'
277
299
  export {
278
300
  ViewerControlsBar,
279
301
  type ViewerControlsBarProps,
@@ -282,6 +304,19 @@ export {
282
304
  ViewerSceneHeader,
283
305
  type ViewerSceneHeaderProps,
284
306
  } from './components/viewer/viewer-scene-header'
307
+ export { ViewerStage, type ViewerStageProps } from './components/viewer/viewer-stage'
308
+ export {
309
+ normalizeViewerStageModes,
310
+ resolveMobileViewerStageMode,
311
+ resolveViewerStageMode,
312
+ VIEWER_STAGE_MODES,
313
+ viewerStageIncludes3D,
314
+ } from './components/viewer/viewer-stage-modes'
315
+ export {
316
+ type ViewerStageMode,
317
+ ViewerStageSwitcher,
318
+ type ViewerStageSwitcherProps,
319
+ } from './components/viewer/viewer-stage-switcher'
285
320
  export {
286
321
  WalkthroughHud,
287
322
  type WalkthroughHudProps,
@@ -310,6 +345,12 @@ export {
310
345
  resolveCeilingPlanPointSnap,
311
346
  } from './lib/ceiling-plan-snap'
312
347
  export { EDITOR_LAYER } from './lib/constants'
348
+ export type { ContextualShortcutHint } from './lib/contextual-help'
349
+ export {
350
+ CONTEXTUAL_HELP_NODE_EXTENSION_KEY,
351
+ type ContextualHelpNodeExtension,
352
+ getContextualHelpNodeExtension,
353
+ } from './lib/contextual-help-extension'
313
354
  // Helper libs used by the kind-owned roof / stair / elevator panels.
314
355
  export {
315
356
  CONTINUATION_PROFILES,
@@ -318,6 +359,7 @@ export {
318
359
  continuationContextOf,
319
360
  nextContinuation,
320
361
  } from './lib/continuation'
362
+ export { createEditorApi } from './lib/editor-api'
321
363
  export {
322
364
  clearStructuralElevationGuide,
323
365
  collectElevationSnapTargets,
@@ -325,6 +367,7 @@ export {
325
367
  type ElevationGuideSource,
326
368
  type ElevationSnapMatch,
327
369
  type ElevationSnapTarget,
370
+ publishResolvedElevationGuide,
328
371
  publishStructuralElevationGuide,
329
372
  resolveElevationSnapMatch,
330
373
  resolveStructuralElevationSnap,
@@ -335,6 +378,7 @@ export {
335
378
  resolveElevatorSupportLevelId,
336
379
  resolveElevatorSupportY,
337
380
  } from './lib/elevator-support'
381
+ export { getFloatingMenuScale } from './lib/floating-menu-scale'
338
382
  // Floor-plan stair helpers — the cumulative-transform walk
339
383
  // (`computeFloorplanStairSegmentTransforms`) and the rich segment-entry
340
384
  // builder (`buildFloorplanStairEntry`) used by the kind-owned stair
@@ -359,6 +403,10 @@ export type {
359
403
  FloorplanAnnotationCategory,
360
404
  FloorplanAnnotationVisibility,
361
405
  } from './lib/floorplan/annotation-visibility'
406
+ export {
407
+ exportFloorplanPdf,
408
+ type FloorplanExportScope,
409
+ } from './lib/floorplan/floorplan-export'
362
410
  export {
363
411
  createFloorplanContextExtensions,
364
412
  FLOORPLAN_CONTEXT_EXTENSION_KEY,
@@ -384,23 +432,47 @@ export {
384
432
  type FloorplanMode,
385
433
  isFloorplanToolAvailableInMode,
386
434
  } from './lib/floorplan/floorplan-mode'
387
- export { commitFreshPlacementSubtree } from './lib/fresh-planar-placement'
388
- export { exportSceneToGlb } from './lib/glb-export'
389
435
  export {
436
+ commitFreshPlacementSubtree,
437
+ createFreshPlacementSubtree,
438
+ } from './lib/fresh-planar-placement'
439
+ export { exportSceneToGlb, type GlbExportOptions } from './lib/glb-export'
440
+ export {
441
+ type EditorGridEvent,
442
+ type GridEventScreenProjection,
443
+ getGridEventScreenProjection,
444
+ } from './lib/grid-event-presentation'
445
+ export {
446
+ getHistoryCommandState,
390
447
  type HistoryCommandDelegate,
448
+ type HistoryCommandResult,
449
+ type HistoryCommandState,
391
450
  installHistoryCommandDelegate,
392
451
  runRedo,
393
452
  runUndo,
453
+ subscribeHistoryCommandState,
394
454
  } from './lib/history'
455
+ export {
456
+ type EditorHostTreeChildren,
457
+ type EditorHostTreeChildrenProps,
458
+ editorHostTreeChildrenRegistry,
459
+ registerEditorHostTreeChildren,
460
+ } from './lib/host-tree-children'
461
+ export {
462
+ DRAFTING_SURFACE_EXTENSION_KEY,
463
+ type DraftingSurfaceExtension,
464
+ } from './lib/interaction/registered-drafting'
395
465
  export {
396
466
  boundaryReshapeScope,
397
467
  curveReshapeScope,
398
468
  endpointReshapeScope,
399
469
  holeEditScope,
470
+ meshEditScope,
400
471
  movingNodeOf,
401
472
  scopeNodeId,
402
473
  } from './lib/interaction/scope'
403
474
  export {
475
+ type ActivePaintMaterial,
404
476
  buildResetSurfaceMaterialUpdates,
405
477
  buildRoofSurfaceMaterialPatch,
406
478
  buildSingleSurfaceMaterialPatch,
@@ -419,6 +491,14 @@ export {
419
491
  measurementPolygonLabelAnchor,
420
492
  triangulateMeasurementPolygon,
421
493
  } from './lib/measurement-label'
494
+ export {
495
+ type LingoUnitSpec,
496
+ lingoUnitSpec,
497
+ type MeasurementHintOptions,
498
+ measurementHint,
499
+ type ParseMeasurementOptions,
500
+ parseMeasurement,
501
+ } from './lib/measurement-parser'
422
502
  export {
423
503
  buildMeasurementAngleArcPoints,
424
504
  cubicMetersToVolumeUnit,
@@ -440,6 +520,12 @@ export {
440
520
  metersToLinearUnit,
441
521
  squareMetersToAreaUnit,
442
522
  } from './lib/measurements'
523
+ export type {
524
+ ModelExport,
525
+ ModelExportArtifact,
526
+ ModelExportFormat,
527
+ ModelExportOptions,
528
+ } from './lib/model-export'
443
529
  export { consumePlacementDragRelease } from './lib/placement-drag-release'
444
530
  export {
445
531
  addFreshPlacementMetadata,
@@ -483,6 +569,13 @@ export {
483
569
  type SlabPlanSnapInput,
484
570
  type SlabPlanSnapResult,
485
571
  } from './lib/slab-plan-snap'
572
+ export {
573
+ getSnappingModeLabel,
574
+ resolveSnapFlags,
575
+ type SnapContext,
576
+ type SnapFlags,
577
+ type SnappingMode,
578
+ } from './lib/snapping-mode'
486
579
  export { duplicateStairSubtree } from './lib/stair-duplication'
487
580
  export {
488
581
  getBuildingLevelsForLevel,
@@ -506,6 +599,7 @@ export {
506
599
  resolveFlattenTarget,
507
600
  sculptFieldForSite,
508
601
  } from './lib/terrain-sculpt'
602
+ export { exportSceneToUsdz, type UsdzExportOptions } from './lib/usdz-export'
509
603
  // `cn` (twMerge + clsx) — used by kind-owned panels in `@pascal-app/
510
604
  // nodes` so they don't need their own copy / their own tailwind-merge
511
605
  // dependency.
@@ -521,6 +615,7 @@ export {
521
615
  export { subscribeCameraPose } from './store/camera-pose-store'
522
616
  export { default as useAlignmentGuides } from './store/use-alignment-guides'
523
617
  export { default as useAudio } from './store/use-audio'
618
+ export { type CameraHintAction, useCameraHintFocus } from './store/use-camera-hint-focus'
524
619
  export { type CommandAction, useCommandRegistry } from './store/use-command-registry'
525
620
  export {
526
621
  DRAWING_TYPE_OPTIONS,
@@ -529,17 +624,24 @@ export {
529
624
  export type {
530
625
  CaptureMode,
531
626
  FloorplanSelectionTool,
627
+ Mode,
532
628
  SnapshotCropMode,
533
629
  SnapshotStandardAspect,
534
630
  SplitOrientation,
631
+ StructureTool,
535
632
  Tool,
536
633
  ToolDefaults,
634
+ ToolMode,
537
635
  ViewMode,
538
636
  WorkspaceMode,
539
637
  } from './store/use-editor'
540
638
  export {
639
+ armMaterialPaint,
640
+ armToolMode,
541
641
  default as useEditor,
542
642
  getActiveContinuationContext,
643
+ getActiveSnapContext,
644
+ getActiveSnappingMode,
543
645
  getContinuation,
544
646
  isAlignmentGuideActive,
545
647
  isAngleSnapActive,
@@ -596,7 +698,10 @@ export {
596
698
  type PathDraftPoint,
597
699
  usePathDraftPreview,
598
700
  } from './store/use-path-draft-preview'
599
- export { default as usePlacementPreview } from './store/use-placement-preview'
701
+ export {
702
+ default as usePlacementPreview,
703
+ type PlacementPreviewDimension,
704
+ } from './store/use-placement-preview'
600
705
  export {
601
706
  activateQuickMeasurementHudSource,
602
707
  clearQuickMeasurementHudSource,
@@ -4,6 +4,7 @@ import {
4
4
  clearPlacementSurface,
5
5
  getPlacementSurface,
6
6
  publishPlacementSurface,
7
+ usesOrientedPlacementPlane,
7
8
  } from './active-placement-surface'
8
9
 
9
10
  describe('active placement surface', () => {
@@ -21,4 +22,19 @@ describe('active placement surface', () => {
21
22
 
22
23
  expect(getPlacementSurface()?.projection).toBe('surface')
23
24
  })
25
+
26
+ test('copies a stable lattice anchor when one is provided', () => {
27
+ publishPlacementSurface(
28
+ new Vector3(4, 2, 3),
29
+ new Vector3(0, 0, 1),
30
+ 'surface',
31
+ new Vector3(1, 2, 3),
32
+ )
33
+
34
+ expect(getPlacementSurface()?.anchor?.toArray()).toEqual([1, 2, 3])
35
+ })
36
+
37
+ test('orients the grid to a sloped placement surface', () => {
38
+ expect(usesOrientedPlacementPlane(new Vector3(0, 0.6, 0.8))).toBe(true)
39
+ })
24
40
  })
@@ -11,12 +11,15 @@ import { Vector3 } from 'three'
11
11
  // readers must consume them within the same frame.
12
12
  export type PlacementSurface = {
13
13
  point: Vector3
14
+ /** Optional stable origin for the construction lattice. */
15
+ anchor?: Vector3
14
16
  normal: Vector3
15
17
  projection: 'surface' | 'fixed-plane'
16
18
  }
17
19
 
18
20
  const surface: PlacementSurface = {
19
21
  point: new Vector3(),
22
+ anchor: new Vector3(),
20
23
  normal: new Vector3(0, 1, 0),
21
24
  projection: 'surface',
22
25
  }
@@ -26,8 +29,15 @@ export function publishPlacementSurface(
26
29
  point: Vector3,
27
30
  normal: Vector3,
28
31
  projection: PlacementSurface['projection'] = 'surface',
32
+ anchor?: Vector3,
29
33
  ): void {
30
34
  surface.point.copy(point)
35
+ if (anchor) {
36
+ surface.anchor ??= new Vector3()
37
+ surface.anchor.copy(anchor)
38
+ } else {
39
+ surface.anchor = undefined
40
+ }
31
41
  surface.normal.copy(normal)
32
42
  surface.projection = projection
33
43
  active = true
@@ -40,3 +50,7 @@ export function clearPlacementSurface(): void {
40
50
  export function getPlacementSurface(): PlacementSurface | null {
41
51
  return active ? surface : null
42
52
  }
53
+
54
+ export function usesOrientedPlacementPlane(normal: Vector3): boolean {
55
+ return Math.abs(normal.y) < 0.95
56
+ }
@@ -0,0 +1,64 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ BackSide,
4
+ Mesh,
5
+ MeshBasicMaterial,
6
+ Path,
7
+ Raycaster,
8
+ Shape,
9
+ ShapeGeometry,
10
+ Vector3,
11
+ } from 'three'
12
+ import { raycastCeilingUnderside } from './ceiling-surface-raycast'
13
+
14
+ function ceilingMesh() {
15
+ const shape = new Shape()
16
+ shape.moveTo(-2, -2)
17
+ shape.lineTo(2, -2)
18
+ shape.lineTo(2, 2)
19
+ shape.lineTo(-2, 2)
20
+ shape.closePath()
21
+ const hole = new Path()
22
+ hole.moveTo(-0.5, -0.5)
23
+ hole.lineTo(-0.5, 0.5)
24
+ hole.lineTo(0.5, 0.5)
25
+ hole.lineTo(0.5, -0.5)
26
+ hole.closePath()
27
+ shape.holes.push(hole)
28
+ const geometry = new ShapeGeometry(shape).rotateX(-Math.PI / 2)
29
+ const mesh = new Mesh(geometry, new MeshBasicMaterial({ side: BackSide }))
30
+ mesh.position.y = 3
31
+ mesh.updateMatrixWorld(true)
32
+ return mesh
33
+ }
34
+
35
+ describe('ceiling drawing surfaces', () => {
36
+ test('lets drawing pass through the transparent top while picking the underside', () => {
37
+ const ceiling = ceilingMesh()
38
+ for (const side of [-1, 1]) {
39
+ const ray = new Raycaster(new Vector3(1, 3 + side * 2, 0), new Vector3(0, -side, 0))
40
+ const hit = raycastCeilingUnderside(ray, ceiling)[0]
41
+ if (side > 0) {
42
+ expect(hit).toBeUndefined()
43
+ } else {
44
+ expect(hit?.point.y).toBeCloseTo(3)
45
+ expect(hit?.object).toBe(ceiling)
46
+ }
47
+ expect(ceiling.material.side).toBe(BackSide)
48
+ }
49
+ ceiling.geometry.dispose()
50
+ ceiling.material.dispose()
51
+ })
52
+
53
+ test('respects ceiling holes and the polygon boundary from either side', () => {
54
+ const ceiling = ceilingMesh()
55
+ for (const side of [-1, 1]) {
56
+ for (const x of [0, 3]) {
57
+ const ray = new Raycaster(new Vector3(x, 3 + side * 2, 0), new Vector3(0, -side, 0))
58
+ expect(raycastCeilingUnderside(ray, ceiling)).toHaveLength(0)
59
+ }
60
+ }
61
+ ceiling.geometry.dispose()
62
+ ceiling.material.dispose()
63
+ })
64
+ })
@@ -0,0 +1,12 @@
1
+ import { BackSide, Mesh, MeshBasicMaterial, type Object3D, type Raycaster } from 'three'
2
+
3
+ const pickingMaterial = new MeshBasicMaterial({ side: BackSide })
4
+
5
+ export function raycastCeilingUnderside(raycaster: Raycaster, ceiling: Object3D) {
6
+ if (!(ceiling instanceof Mesh)) return []
7
+ // Ignore the transparent top and its grid overlay so drawing can reach surfaces below.
8
+ const proxy = new Mesh(ceiling.geometry, pickingMaterial)
9
+ proxy.matrixWorld.copy(ceiling.matrixWorld)
10
+ proxy.layers.mask = ceiling.layers.mask
11
+ return raycaster.intersectObject(proxy, false).map((hit) => ({ ...hit, object: ceiling }))
12
+ }
@@ -0,0 +1,17 @@
1
+ import type { NodeDefinition } from '@pascal-app/core'
2
+ import type { ContextualShortcutHint } from './contextual-help'
3
+
4
+ export const CONTEXTUAL_HELP_NODE_EXTENSION_KEY = 'pascal:editor/contextual-help'
5
+
6
+ export type ContextualHelpNodeExtension = {
7
+ subscribe: (onChange: () => void) => () => void
8
+ getHints: (nodeId: string) => ContextualShortcutHint[]
9
+ }
10
+
11
+ export function getContextualHelpNodeExtension(
12
+ definition: NodeDefinition<any> | undefined,
13
+ ): ContextualHelpNodeExtension | undefined {
14
+ return definition?.extensions?.[CONTEXTUAL_HELP_NODE_EXTENSION_KEY] as
15
+ | ContextualHelpNodeExtension
16
+ | undefined
17
+ }
@@ -73,6 +73,14 @@ describe('resolveSelectModeHelpHints', () => {
73
73
  label: 'Click or drag the selection to move it as one',
74
74
  },
75
75
  { keys: ['R / T'], label: 'Rotate the selection ±45°' },
76
+ {
77
+ keys: ['Cmd/Ctrl', 'G'],
78
+ label: 'Group selection (session only)',
79
+ },
80
+ {
81
+ keys: ['Cmd/Ctrl', 'Shift', 'G'],
82
+ label: 'Ungroup session selection',
83
+ },
76
84
  {
77
85
  keys: [['Cmd/Ctrl', 'Shift'], 'Left click'],
78
86
  label: 'Add or remove objects from the selection',
@@ -93,6 +93,14 @@ export function resolveSelectModeHelpHints({
93
93
  label: 'Click or drag the selection to move it as one',
94
94
  })
95
95
  hints.push({ keys: [ROTATE_KEYS], label: 'Rotate the selection ±45°' })
96
+ hints.push({
97
+ keys: [COMMAND_KEY, 'G'],
98
+ label: 'Group selection (session only)',
99
+ })
100
+ hints.push({
101
+ keys: [COMMAND_KEY, SHIFT_KEY, 'G'],
102
+ label: 'Ungroup session selection',
103
+ })
96
104
  hints.push({
97
105
  keys: [[COMMAND_KEY, SHIFT_KEY], LEFT_CLICK],
98
106
  label: 'Add or remove objects from the selection',
@@ -0,0 +1,11 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { CONTINUATION_PROFILES, continuationContextOf, nextContinuation } from './continuation'
3
+
4
+ describe('canopy continuation', () => {
5
+ test('maps the canopy tool to its own single and continuous profile', () => {
6
+ expect(continuationContextOf('lean-to-extension')).toBe('canopy')
7
+ expect(CONTINUATION_PROFILES.canopy.default).toBe('single')
8
+ expect(nextContinuation('canopy', 'single')).toBe('continuous')
9
+ expect(nextContinuation('canopy', 'continuous')).toBe('single')
10
+ })
11
+ })
@@ -1,4 +1,4 @@
1
- export type ContinuationContext = 'wall' | 'fence' | 'point' | 'cabinet'
1
+ export type ContinuationContext = 'wall' | 'fence' | 'point' | 'cabinet' | 'canopy'
2
2
  export type ContinuationMode = string
3
3
 
4
4
  export const CONTINUATION_PROFILES: Record<
@@ -42,6 +42,12 @@ export const CONTINUATION_PROFILES: Record<
42
42
  labels: { single: 'Single cabinet', continuous: 'Continuous run' },
43
43
  icons: { single: 'lucide:minus', continuous: 'lucide:waypoints' },
44
44
  },
45
+ canopy: {
46
+ options: ['single', 'continuous'],
47
+ default: 'single',
48
+ labels: { single: 'Single canopy', continuous: 'Continuous canopy' },
49
+ icons: { single: 'lucide:minus', continuous: 'lucide:waypoints' },
50
+ },
45
51
  }
46
52
 
47
53
  const POINT_KINDS = new Set(['item', 'door', 'window', 'shelf', 'column'])
@@ -60,5 +66,6 @@ export function continuationContextOf(kind: string): ContinuationContext | null
60
66
  if (kind === 'wall') return 'wall'
61
67
  if (kind === 'fence') return 'fence'
62
68
  if (kind === 'cabinet') return 'cabinet'
69
+ if (kind === 'lean-to-extension') return 'canopy'
63
70
  return POINT_KINDS.has(kind) ? 'point' : null
64
71
  }