@pascal-app/editor 1.0.0-beta.5 → 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 (245) hide show
  1. package/LICENSE +21 -0
  2. package/bunfig.toml +4 -0
  3. package/package.json +13 -7
  4. package/scripts/generate-print-golden-house.ts +89 -0
  5. package/src/components/editor/bake-exporter.tsx +1 -0
  6. package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
  7. package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
  8. package/src/components/editor/capture-camera-rig.tsx +59 -0
  9. package/src/components/editor/custom-camera-controls.tsx +20 -11
  10. package/src/components/editor/editor-layout-mobile.tsx +6 -6
  11. package/src/components/editor/editor-layout-v2.tsx +9 -2
  12. package/src/components/editor/export-manager.tsx +175 -33
  13. package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
  14. package/src/components/editor/first-person-controls.tsx +180 -12
  15. package/src/components/editor/floating-action-menu.tsx +42 -51
  16. package/src/components/editor/floorplan-panel.tsx +76 -12
  17. package/src/components/editor/grid.tsx +12 -7
  18. package/src/components/editor/group-actions.ts +34 -50
  19. package/src/components/editor/group-move-3d.ts +51 -21
  20. package/src/components/editor/group-transform-shared.test.ts +78 -0
  21. package/src/components/editor/group-transform-shared.ts +74 -7
  22. package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
  23. package/src/components/editor/handles/handle-arrow.tsx +109 -48
  24. package/src/components/editor/handles/linear-resize-drag.ts +67 -0
  25. package/src/components/editor/handles/resize-snap.test.ts +46 -0
  26. package/src/components/editor/handles/resize-snap.ts +10 -1
  27. package/src/components/editor/handles/use-handle-drag.ts +18 -1
  28. package/src/components/editor/index.tsx +140 -21
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +47 -58
  31. package/src/components/editor/selection-manager.tsx +104 -42
  32. package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
  33. package/src/components/editor/snapshot-capture.test.ts +422 -0
  34. package/src/components/editor/snapshot-capture.ts +145 -0
  35. package/src/components/editor/thumbnail-generator.tsx +284 -237
  36. package/src/components/editor/use-floorplan-background-placement.ts +53 -5
  37. package/src/components/editor/wall-move-side-handles.tsx +299 -7
  38. package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
  39. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
  40. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
  41. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
  42. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
  43. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
  44. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
  45. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
  46. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +180 -32
  47. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
  48. package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
  49. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
  50. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
  51. package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
  52. package/src/components/systems/roof/roof-edit-system.tsx +10 -19
  53. package/src/components/systems/selection-affordance-manager.tsx +59 -9
  54. package/src/components/systems/selection-affordance-services.ts +19 -0
  55. package/src/components/systems/stair/stair-edit-system.tsx +5 -2
  56. package/src/components/tools/fence/fence-drafting.test.ts +100 -0
  57. package/src/components/tools/fence/fence-drafting.ts +6 -25
  58. package/src/components/tools/item/draft-creation.test.ts +12 -0
  59. package/src/components/tools/item/draft-creation.ts +10 -0
  60. package/src/components/tools/item/face-host-commit.test.ts +65 -0
  61. package/src/components/tools/item/face-host-commit.ts +56 -0
  62. package/src/components/tools/item/face-host-preview.test.ts +77 -0
  63. package/src/components/tools/item/face-host-preview.ts +82 -0
  64. package/src/components/tools/item/move-tool.tsx +4 -3
  65. package/src/components/tools/item/placement-math.test.ts +11 -1
  66. package/src/components/tools/item/placement-math.ts +0 -20
  67. package/src/components/tools/item/placement-strategies.test.ts +331 -4
  68. package/src/components/tools/item/placement-strategies.ts +135 -2
  69. package/src/components/tools/item/placement-surface.test.ts +61 -0
  70. package/src/components/tools/item/placement-surface.ts +24 -0
  71. package/src/components/tools/item/placement-types.ts +6 -0
  72. package/src/components/tools/item/test-face-host.ts +121 -0
  73. package/src/components/tools/item/use-draft-node.test.tsx +336 -0
  74. package/src/components/tools/item/use-draft-node.ts +26 -4
  75. package/src/components/tools/item/use-placement-coordinator.tsx +428 -153
  76. package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
  77. package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
  78. package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
  79. package/src/components/tools/registry-tool-context.tsx +30 -0
  80. package/src/components/tools/select/box-select-tool.tsx +3 -1
  81. package/src/components/tools/select/marquee-footprint.test.ts +53 -0
  82. package/src/components/tools/select/marquee-footprint.ts +29 -0
  83. package/src/components/tools/shared/placement-box.tsx +57 -7
  84. package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
  85. package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
  86. package/src/components/tools/shared/pointer-support-cap.ts +45 -8
  87. package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
  88. package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
  89. package/src/components/tools/stair/stair-tool.tsx +127 -74
  90. package/src/components/tools/tool-manager.tsx +25 -8
  91. package/src/components/tools/wall/wall-drafting.test.ts +320 -2
  92. package/src/components/tools/wall/wall-drafting.ts +23 -114
  93. package/src/components/ui/action-menu/control-modes.tsx +9 -6
  94. package/src/components/ui/action-menu/index.tsx +11 -1
  95. package/src/components/ui/command-palette/editor-commands.tsx +9 -12
  96. package/src/components/ui/command-palette/index.tsx +7 -2
  97. package/src/components/ui/controls/material-paint-panel.tsx +6 -4
  98. package/src/components/ui/controls/material-picker.tsx +4 -4
  99. package/src/components/ui/controls/scene-material-list.tsx +6 -5
  100. package/src/components/ui/controls/segmented-control.tsx +8 -1
  101. package/src/components/ui/controls/slider-control.tsx +19 -3
  102. package/src/components/ui/controls/toggle-control.tsx +18 -5
  103. package/src/components/ui/controls/tool-options-panel.tsx +126 -0
  104. package/src/components/ui/floating-level-selector.tsx +22 -5
  105. package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
  106. package/src/components/ui/helpers/helper-manager.tsx +47 -11
  107. package/src/components/ui/helpers/item-helper.tsx +1 -1
  108. package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
  109. package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
  110. package/src/components/ui/panels/homogeneous-selection.ts +47 -0
  111. package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
  112. package/src/components/ui/panels/multi-field-value.test.ts +209 -0
  113. package/src/components/ui/panels/multi-field-value.ts +163 -0
  114. package/src/components/ui/panels/multi-height-mode.tsx +185 -0
  115. package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
  116. package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
  117. package/src/components/ui/panels/node-display.ts +4 -0
  118. package/src/components/ui/panels/panel-manager.tsx +77 -3
  119. package/src/components/ui/panels/parametric-field-control.tsx +173 -0
  120. package/src/components/ui/panels/parametric-field-utils.ts +18 -0
  121. package/src/components/ui/panels/parametric-inspector.tsx +29 -162
  122. package/src/components/ui/panels/reference-panel.tsx +31 -1
  123. package/src/components/ui/primitives/shortcut-token.tsx +23 -1
  124. package/src/components/ui/scene-loader.tsx +35 -0
  125. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
  126. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
  127. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
  128. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
  129. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
  130. package/src/components/ui/sidebar/panels/site-panel/index.tsx +116 -46
  131. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
  132. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
  133. package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
  134. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +14 -6
  135. package/src/components/ui/sidebar/tab-bar.tsx +2 -0
  136. package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
  137. package/src/components/viewer/viewer-scene-header.tsx +2 -1
  138. package/src/components/viewer/viewer-stage.tsx +4 -1
  139. package/src/components/viewer-overlay.tsx +1 -22
  140. package/src/components/viewer-zone-system.tsx +6 -2
  141. package/src/components/walkthrough-hud.tsx +19 -10
  142. package/src/hooks/use-auto-save.test.ts +73 -1
  143. package/src/hooks/use-auto-save.ts +76 -5
  144. package/src/hooks/use-ceiling-events.test.ts +122 -0
  145. package/src/hooks/use-drag-action.ts +17 -3
  146. package/src/hooks/use-grid-events.test.ts +240 -0
  147. package/src/hooks/use-grid-events.ts +190 -11
  148. package/src/hooks/use-keyboard.test.ts +244 -0
  149. package/src/hooks/use-keyboard.ts +129 -62
  150. package/src/hooks/use-registered-tool-enabled.ts +20 -0
  151. package/src/hooks/use-save-shortcut.ts +33 -0
  152. package/src/index.tsx +82 -4
  153. package/src/lib/active-placement-surface.test.ts +16 -0
  154. package/src/lib/active-placement-surface.ts +14 -0
  155. package/src/lib/ceiling-surface-raycast.test.ts +64 -0
  156. package/src/lib/ceiling-surface-raycast.ts +12 -0
  157. package/src/lib/contextual-help-extension.ts +17 -0
  158. package/src/lib/continuation.test.ts +11 -0
  159. package/src/lib/continuation.ts +8 -1
  160. package/src/lib/direct-manipulation.test.ts +132 -0
  161. package/src/lib/direct-manipulation.ts +38 -1
  162. package/src/lib/editor-api.ts +6 -3
  163. package/src/lib/elevation-guides.test.ts +28 -1
  164. package/src/lib/elevation-guides.ts +22 -7
  165. package/src/lib/floating-menu-scale.test.ts +29 -0
  166. package/src/lib/floating-menu-scale.ts +14 -0
  167. package/src/lib/floorplan/apply-alignment.test.ts +47 -2
  168. package/src/lib/floorplan/apply-alignment.ts +10 -1
  169. package/src/lib/floorplan/floorplan-export.test.ts +397 -1
  170. package/src/lib/floorplan/floorplan-export.tsx +111 -16
  171. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
  172. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
  173. package/src/lib/floorplan/floorplan-readonly.ts +1 -0
  174. package/src/lib/floorplan-grid-event-point.test.ts +29 -0
  175. package/src/lib/floorplan-grid-event-point.ts +13 -0
  176. package/src/lib/glb-export.test.ts +1169 -21
  177. package/src/lib/glb-export.ts +629 -101
  178. package/src/lib/grid-event-presentation.ts +16 -0
  179. package/src/lib/history.test.ts +528 -147
  180. package/src/lib/history.ts +81 -7
  181. package/src/lib/host-tree-children.test.ts +27 -0
  182. package/src/lib/host-tree-children.ts +79 -0
  183. package/src/lib/interaction/hot-set.test.ts +10 -1
  184. package/src/lib/interaction/hot-set.ts +3 -1
  185. package/src/lib/interaction/overlay-policy.test.ts +33 -1
  186. package/src/lib/interaction/overlay-policy.ts +16 -0
  187. package/src/lib/interaction/registered-drafting.ts +36 -0
  188. package/src/lib/interaction/scope.ts +31 -0
  189. package/src/lib/level-print-export.test.ts +484 -0
  190. package/src/lib/level-print-export.ts +526 -0
  191. package/src/lib/local-project-presentation-persistence.test.ts +302 -0
  192. package/src/lib/local-project-presentation-persistence.ts +335 -0
  193. package/src/lib/material-paint.ts +2 -0
  194. package/src/lib/model-export.ts +29 -0
  195. package/src/lib/paint-preview-owner.ts +71 -0
  196. package/src/lib/planar-cursor-placement.test.ts +178 -1
  197. package/src/lib/planar-cursor-placement.ts +61 -5
  198. package/src/lib/plugin-panels.test.ts +92 -1
  199. package/src/lib/plugin-panels.ts +48 -0
  200. package/src/lib/portable-export.test.ts +366 -0
  201. package/src/lib/portable-export.ts +969 -0
  202. package/src/lib/print-3mf.test.ts +86 -0
  203. package/src/lib/print-3mf.ts +161 -0
  204. package/src/lib/print-content-scope.test.ts +107 -0
  205. package/src/lib/print-content-scope.ts +90 -0
  206. package/src/lib/print-export.test.ts +294 -0
  207. package/src/lib/print-export.ts +735 -0
  208. package/src/lib/print-feature-thickness.test.ts +147 -0
  209. package/src/lib/print-feature-thickness.ts +180 -0
  210. package/src/lib/print-golden-house.test-fixture.ts +352 -0
  211. package/src/lib/print-golden-house.test.ts +249 -0
  212. package/src/lib/print-roof-solids.test.ts +133 -0
  213. package/src/lib/print-roof-solids.ts +630 -0
  214. package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
  215. package/src/lib/print-shell-compiler-baseline.ts +225 -0
  216. package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
  217. package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
  218. package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
  219. package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
  220. package/src/lib/print-shell-compiler-protocol.ts +30 -0
  221. package/src/lib/print-shell-compiler.ts +273 -0
  222. package/src/lib/print-wall-solids.test.ts +194 -0
  223. package/src/lib/print-wall-solids.ts +328 -0
  224. package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
  225. package/src/lib/rigid-plan-svg-transform.ts +24 -0
  226. package/src/lib/scene.ts +22 -8
  227. package/src/lib/selection-routing.test.ts +105 -1
  228. package/src/lib/selection-routing.ts +19 -0
  229. package/src/lib/sfx-bus.ts +4 -0
  230. package/src/lib/sfx-player.ts +8 -0
  231. package/src/lib/snapping-mode.test.ts +32 -4
  232. package/src/lib/snapping-mode.ts +11 -7
  233. package/src/lib/usdz-export.ts +38 -0
  234. package/src/lib/walkthrough-pointer-lock.ts +47 -0
  235. package/src/store/live-draft-preview-stores.test.ts +3 -0
  236. package/src/store/terrain-sculpt-mode.test.ts +3 -6
  237. package/src/store/tool-mode.test.ts +87 -0
  238. package/src/store/use-camera-hint-focus.ts +48 -0
  239. package/src/store/use-editor.tsx +299 -188
  240. package/src/store/use-floorplan-draft-preview.ts +9 -0
  241. package/src/store/use-interaction-scope.test.ts +23 -1
  242. package/src/store/use-interaction-scope.ts +63 -2
  243. package/src/store/use-placement-preview.ts +56 -3
  244. package/src/components/tools/roof/roof-tool.tsx +0 -656
  245. package/src/components/ui/helpers/roof-helper.tsx +0 -14
@@ -40,6 +40,7 @@ type FloorplanDraftPreviewState = {
40
40
  wallDraftStart: WallPlanPoint | null
41
41
  fenceDraftStart: WallPlanPoint | null
42
42
  roofDraftStart: WallPlanPoint | null
43
+ roofDraftQuarterTurn: boolean
43
44
  polygonDraftType: FloorplanPolygonDraftType | null
44
45
  polygonDraftPoints: WallPlanPoint[]
45
46
  /** Set the snapped cursor point. No-ops (skips the store update, so
@@ -54,6 +55,7 @@ type FloorplanDraftPreviewState = {
54
55
  setWallDraftStart(point: WallPlanPoint | null): void
55
56
  setFenceDraftStart(point: WallPlanPoint | null): void
56
57
  setRoofDraftStart(point: WallPlanPoint | null): void
58
+ setRoofDraftQuarterTurn(quarterTurn: boolean): void
57
59
  setPolygonDraft(type: FloorplanPolygonDraftType | null, points: readonly WallPlanPoint[]): void
58
60
  reset(): void
59
61
  }
@@ -94,6 +96,7 @@ export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set)
94
96
  wallDraftStart: null,
95
97
  fenceDraftStart: null,
96
98
  roofDraftStart: null,
99
+ roofDraftQuarterTurn: false,
97
100
  polygonDraftType: null,
98
101
  polygonDraftPoints: [],
99
102
  setCursorPoint: (point) =>
@@ -116,6 +119,10 @@ export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set)
116
119
  setWallDraftStart: (point) => set(setPlanPointField('wallDraftStart', point)),
117
120
  setFenceDraftStart: (point) => set(setPlanPointField('fenceDraftStart', point)),
118
121
  setRoofDraftStart: (point) => set(setPlanPointField('roofDraftStart', point)),
122
+ setRoofDraftQuarterTurn: (quarterTurn) =>
123
+ set((state) =>
124
+ state.roofDraftQuarterTurn === quarterTurn ? state : { roofDraftQuarterTurn: quarterTurn },
125
+ ),
119
126
  setPolygonDraft: (type, points) =>
120
127
  set((state) =>
121
128
  state.polygonDraftType === type && planPointsEqual(state.polygonDraftPoints, points)
@@ -132,6 +139,7 @@ export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set)
132
139
  state.wallDraftStart === null &&
133
140
  state.fenceDraftStart === null &&
134
141
  state.roofDraftStart === null &&
142
+ state.roofDraftQuarterTurn === false &&
135
143
  state.polygonDraftType === null &&
136
144
  state.polygonDraftPoints.length === 0
137
145
  ? state
@@ -144,6 +152,7 @@ export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set)
144
152
  wallDraftStart: null,
145
153
  fenceDraftStart: null,
146
154
  roofDraftStart: null,
155
+ roofDraftQuarterTurn: false,
147
156
  polygonDraftType: null,
148
157
  polygonDraftPoints: [],
149
158
  },
@@ -1,4 +1,4 @@
1
- import { afterEach, describe, expect, test } from 'bun:test'
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
2
  import type { AnyNode } from '@pascal-app/core'
3
3
  import {
4
4
  type ActiveInteractionScope,
@@ -8,6 +8,7 @@ import {
8
8
  isActive,
9
9
  isIdle,
10
10
  isToolDrivenReshape,
11
+ meshEditScope,
11
12
  scopeNodeId,
12
13
  selectionEnabled,
13
14
  } from '../lib/interaction/scope'
@@ -20,6 +21,7 @@ const mockNode = (id: string, type: string): AnyNode => ({ id, type }) as unknow
20
21
  function reset() {
21
22
  useInteractionScope.getState().end()
22
23
  }
24
+ beforeEach(reset)
23
25
  afterEach(reset)
24
26
 
25
27
  describe('use-interaction-scope state machine', () => {
@@ -71,6 +73,7 @@ describe('use-interaction-scope state machine', () => {
71
73
  nodeType: 'item',
72
74
  view: '3d',
73
75
  pressDrag: false,
76
+ driver: 'move-tool',
74
77
  })
75
78
  s.update({ pressDrag: true })
76
79
  const scope = useInteractionScope.getState().scope
@@ -104,6 +107,7 @@ describe('use-interaction-scope state machine', () => {
104
107
  nodeType: 'item',
105
108
  view: '3d',
106
109
  pressDrag: true,
110
+ driver: 'move-tool',
107
111
  })
108
112
  expect(useInteractionScope.getState().scope.kind).toBe('moving')
109
113
  })
@@ -116,6 +120,22 @@ describe('use-interaction-scope state machine', () => {
116
120
  expect(isActive(useInteractionScope.getState().scope)).toBe(true)
117
121
  })
118
122
 
123
+ test('mesh edit mode owns its node and disables scene selection for the full session', () => {
124
+ const s = useInteractionScope.getState()
125
+ s.begin(meshEditScope('block_1'))
126
+ expect(scopeNodeId(useInteractionScope.getState().scope)).toBe('block_1')
127
+ expect(selectionEnabled(useInteractionScope.getState().scope)).toBe(false)
128
+
129
+ s.begin(meshEditScope('block_1', 'operating', 'translate'))
130
+ expect(useInteractionScope.getState().scope).toEqual({
131
+ kind: 'mesh-editing',
132
+ nodeId: 'block_1',
133
+ phase: 'operating',
134
+ operator: 'translate',
135
+ })
136
+ expect(selectionEnabled(useInteractionScope.getState().scope)).toBe(false)
137
+ })
138
+
119
139
  test('end is idempotent', () => {
120
140
  const s = useInteractionScope.getState()
121
141
  s.end()
@@ -188,9 +208,11 @@ describe('derived flag views are leak-free (no parallel flags)', () => {
188
208
  nodeType: 'item',
189
209
  view: '3d',
190
210
  pressDrag: false,
211
+ driver: 'move-tool',
191
212
  },
192
213
  { kind: 'moving', node: mockNode('i', 'item'), nodeId: 'i', nodeType: 'item', view: '3d' },
193
214
  { kind: 'drafting', tool: 'wall' },
215
+ { kind: 'mesh-editing', nodeId: 'mesh_1', phase: 'selecting' },
194
216
  { kind: 'box-select' },
195
217
  { kind: 'painting' },
196
218
  { kind: 'sculpting' },
@@ -1,6 +1,12 @@
1
1
  'use client'
2
2
 
3
3
  import { type AnyNode, type AnyNodeId, useScene } from '@pascal-app/core'
4
+ import {
5
+ beginPerfAction,
6
+ commitPerfAction,
7
+ getActivePerfActionId,
8
+ hasUncommittedPerfAction,
9
+ } from '@pascal-app/viewer'
4
10
  import { useRef } from 'react'
5
11
  import { create } from 'zustand'
6
12
  import { useShallow } from 'zustand/react/shallow'
@@ -45,9 +51,60 @@ export type InteractionScopeState = {
45
51
  endIf: (match: (scope: ActiveInteractionScope) => boolean) => void
46
52
  }
47
53
 
54
+ // Perf action ledger (`?perf`): every 3D gesture funnels through this store, so
55
+ // begin/end are the one generic bracket for action-cost receipts. A more
56
+ // specific call site (use-drag-action, the 2D floorplan layer) may have begun
57
+ // its own action first — yield to it while ITS gesture is in flight, but a
58
+ // merely-settling previous action must not swallow a new gesture. The scope
59
+ // remembers the id it began and commits only that action at `end`, so a
60
+ // specific site's receipt (or a cancelled one finalized via
61
+ // markToolCancelConsumed) is never committed by the generic bracket. Known
62
+ // limit: a scope-begun gesture cancelled through a path that skips
63
+ // markToolCancelConsumed still commits at end and bills its revert as settle.
64
+ let scopePerfActionId: number | null = null
65
+
66
+ function beginScopePerfAction(scope: ActiveInteractionScope): void {
67
+ if (hasUncommittedPerfAction()) return
68
+ switch (scope.kind) {
69
+ case 'moving':
70
+ scopePerfActionId = beginPerfAction('drag:move', scope.nodeId)
71
+ break
72
+ case 'placing':
73
+ scopePerfActionId = beginPerfAction(
74
+ `place:${scope.node?.type ?? 'node'}`,
75
+ scope.node?.id ?? '',
76
+ )
77
+ break
78
+ case 'reshaping': {
79
+ const nodeType = useScene.getState().nodes[scope.nodeId as AnyNodeId]?.type
80
+ scopePerfActionId = beginPerfAction(
81
+ `drag:${nodeType ? `${nodeType}-` : ''}${scope.reshape}`,
82
+ scope.nodeId,
83
+ )
84
+ break
85
+ }
86
+ case 'handle-drag':
87
+ scopePerfActionId = beginPerfAction(`drag:${scope.handle}`, scope.nodeId)
88
+ break
89
+ default:
90
+ // drafting / mesh-editing are long-lived modes, not gestures
91
+ break
92
+ }
93
+ }
94
+
95
+ function commitScopePerfAction(): void {
96
+ if (scopePerfActionId !== null && getActivePerfActionId() === scopePerfActionId) {
97
+ commitPerfAction()
98
+ }
99
+ scopePerfActionId = null
100
+ }
101
+
48
102
  const useInteractionScope = create<InteractionScopeState>((set, get) => ({
49
103
  scope: IDLE_SCOPE,
50
- begin: (scope) => set({ scope }),
104
+ begin: (scope) => {
105
+ beginScopePerfAction(scope)
106
+ set({ scope })
107
+ },
51
108
  update: (patch) =>
52
109
  set((state) => {
53
110
  if (state.scope.kind === 'idle') return state
@@ -56,12 +113,16 @@ const useInteractionScope = create<InteractionScopeState>((set, get) => ({
56
113
  }),
57
114
  end: () => {
58
115
  if (get().scope.kind === 'idle') return
116
+ commitScopePerfAction()
59
117
  set({ scope: IDLE_SCOPE })
60
118
  },
61
119
  endIf: (match) => {
62
120
  const scope = get().scope
63
121
  if (scope.kind === 'idle') return
64
- if (match(scope)) set({ scope: IDLE_SCOPE })
122
+ if (match(scope)) {
123
+ commitScopePerfAction()
124
+ set({ scope: IDLE_SCOPE })
125
+ }
65
126
  },
66
127
  }))
67
128
 
@@ -14,10 +14,22 @@
14
14
  import type { AnyNode } from '@pascal-app/core'
15
15
  import { create } from 'zustand'
16
16
 
17
+ export type PlacementPreviewDimension = {
18
+ id: string
19
+ start: [number, number, number]
20
+ end: [number, number, number]
21
+ offsetNormal: [number, number]
22
+ offsetDistance: number
23
+ value: number
24
+ renderIn3d?: boolean
25
+ renderInFloorplan?: boolean
26
+ }
27
+
17
28
  type PlacementPreviewState = {
18
29
  /** Transient preview node, already positioned + rotated at the (snapped,
19
30
  * aligned) cursor. `null` when no placement is active. */
20
31
  node: AnyNode | null
32
+ contextNodes: AnyNode[]
21
33
  /** Optional synthetic parent for the preview's `def.floorplan` context.
22
34
  * Door / window glyph builders need `ctx.parent` to be a wall to draw their
23
35
  * real symbol (swing arc / panes); off any real wall we hand them a
@@ -25,15 +37,56 @@ type PlacementPreviewState = {
25
37
  * the faithful blueprint symbol instead of a bare rectangle. `null` for
26
38
  * self-contained kinds (column / elevator). */
27
39
  parentNode: AnyNode | null
28
- set(node: AnyNode | null, parentNode?: AnyNode | null): void
40
+ dimensions: PlacementPreviewDimension[]
41
+ activeDimensionId: string | null
42
+ dimensionInput: string
43
+ set(
44
+ node: AnyNode | null,
45
+ parentNode?: AnyNode | null,
46
+ dimensions?: PlacementPreviewDimension[],
47
+ contextNodes?: AnyNode[],
48
+ ): void
49
+ selectDimension(id: string | null): void
50
+ setDimensionInput(value: string): void
51
+ clearDimensionEditor(): void
29
52
  clear(): void
30
53
  }
31
54
 
32
55
  const usePlacementPreview = create<PlacementPreviewState>((set) => ({
33
56
  node: null,
57
+ contextNodes: [],
34
58
  parentNode: null,
35
- set: (node, parentNode = null) => set({ node, parentNode }),
36
- clear: () => set({ node: null, parentNode: null }),
59
+ dimensions: [],
60
+ activeDimensionId: null,
61
+ dimensionInput: '',
62
+ set: (node, parentNode = null, dimensions = [], contextNodes = []) =>
63
+ set((state) => {
64
+ const activeDimensionId = dimensions.some(
65
+ (dimension) => dimension.id === state.activeDimensionId,
66
+ )
67
+ ? state.activeDimensionId
68
+ : null
69
+ return {
70
+ node,
71
+ contextNodes,
72
+ parentNode,
73
+ dimensions,
74
+ activeDimensionId,
75
+ dimensionInput: activeDimensionId ? state.dimensionInput : '',
76
+ }
77
+ }),
78
+ selectDimension: (id) => set({ activeDimensionId: id, dimensionInput: '' }),
79
+ setDimensionInput: (dimensionInput) => set({ dimensionInput }),
80
+ clearDimensionEditor: () => set({ activeDimensionId: null, dimensionInput: '' }),
81
+ clear: () =>
82
+ set({
83
+ node: null,
84
+ contextNodes: [],
85
+ parentNode: null,
86
+ dimensions: [],
87
+ activeDimensionId: null,
88
+ dimensionInput: '',
89
+ }),
37
90
  }))
38
91
 
39
92
  export default usePlacementPreview