@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,30 @@
1
+ 'use client'
2
+
3
+ import type { AnyNodeId, LevelNode, SceneApi } from '@pascal-app/core'
4
+ import { createContext, type ReactNode, useContext } from 'react'
5
+
6
+ export type RegistryToolContextValue = {
7
+ activeLevelId: LevelNode['id'] | null
8
+ isCameraDragging: () => boolean
9
+ sceneApi: SceneApi
10
+ selectNode: (nodeId: AnyNodeId) => void
11
+ unit: 'metric' | 'imperial'
12
+ }
13
+
14
+ const RegistryToolContext = createContext<RegistryToolContextValue | null>(null)
15
+
16
+ export function RegistryToolProvider({
17
+ children,
18
+ value,
19
+ }: {
20
+ children: ReactNode
21
+ value: RegistryToolContextValue
22
+ }) {
23
+ return <RegistryToolContext.Provider value={value}>{children}</RegistryToolContext.Provider>
24
+ }
25
+
26
+ export function useRegistryToolContext(): RegistryToolContextValue {
27
+ const value = useContext(RegistryToolContext)
28
+ if (!value) throw new Error('Registry tools must be mounted by ToolManager')
29
+ return value
30
+ }
@@ -19,6 +19,7 @@ import {
19
19
  isBoxSelectPointerSuppressed,
20
20
  markBoxSelectHandled,
21
21
  } from './box-select-state'
22
+ import { marqueePolygon } from './marquee-footprint'
22
23
  import {
23
24
  convexHull2D,
24
25
  type Point2,
@@ -223,7 +224,8 @@ function collectNodeIdsInScreenRect(
223
224
  | { start?: unknown; end?: unknown; polygon?: unknown }
224
225
  | undefined
225
226
  if (node) {
226
- const { start, end, polygon } = node
227
+ const { start, end } = node
228
+ const polygon = marqueePolygon(node)
227
229
  if (isVec2(start) && isVec2(end)) {
228
230
  if (segmentIntersectsPolygon(start, end, quad)) result.push(id)
229
231
  continue
@@ -0,0 +1,53 @@
1
+ import { expect, test } from 'bun:test'
2
+ import { marqueePolygon } from './marquee-footprint'
3
+ import { type Point2, polygonsIntersect } from './marquee-geometry'
4
+
5
+ const polygon: Point2[] = [
6
+ [-4, -2],
7
+ [4, -2],
8
+ [4, 2],
9
+ [-4, 2],
10
+ ]
11
+
12
+ test('a translated pool is not selected by a pipe marquee over its untransformed outline', () => {
13
+ const pool = { polygon, position: [12, 0, 0], rotation: [0, 0, 0] }
14
+ const pipeMarquee: Point2[] = [
15
+ [-1, -0.1],
16
+ [1, -0.1],
17
+ [1, 0.1],
18
+ [-1, 0.1],
19
+ ]
20
+ expect(polygonsIntersect(polygon, pipeMarquee)).toBe(true)
21
+ expect(polygonsIntersect(marqueePolygon(pool)!, pipeMarquee)).toBe(false)
22
+ expect(
23
+ polygonsIntersect(marqueePolygon(pool)!, [
24
+ [11, -1],
25
+ [13, -1],
26
+ [13, 1],
27
+ [11, 1],
28
+ ]),
29
+ ).toBe(true)
30
+ })
31
+
32
+ test('pool rotation is applied before its position', () => {
33
+ const footprint = marqueePolygon({
34
+ polygon,
35
+ position: [12, 0, 5],
36
+ rotation: [0, Math.PI / 2, 0],
37
+ })!
38
+ expect(footprint[0]![0]).toBeCloseTo(10)
39
+ expect(footprint[0]![1]).toBeCloseTo(9)
40
+ expect(
41
+ polygonsIntersect(footprint, [
42
+ [15, 4],
43
+ [16, 4],
44
+ [16, 6],
45
+ [15, 6],
46
+ ]),
47
+ ).toBe(false)
48
+ })
49
+
50
+ test('level-space polygons keep their existing coordinates', () => {
51
+ expect(marqueePolygon({ polygon })).toBe(polygon)
52
+ expect(marqueePolygon({ polygon: undefined })).toBeNull()
53
+ })
@@ -0,0 +1,29 @@
1
+ import { Euler, Vector3 } from 'three'
2
+ import type { Point2 } from './marquee-geometry'
3
+
4
+ export function marqueePolygon(node: {
5
+ polygon?: unknown
6
+ position?: unknown
7
+ rotation?: unknown
8
+ }): Point2[] | null {
9
+ const { polygon, position, rotation } = node
10
+ if (
11
+ !Array.isArray(polygon) ||
12
+ polygon.length === 0 ||
13
+ !polygon.every(
14
+ (point) => Array.isArray(point) && point.length === 2 && point.every(Number.isFinite),
15
+ )
16
+ )
17
+ return null
18
+ // Position-based kinds author their footprint locally, unlike slabs and zones.
19
+ if (!Array.isArray(position) || position.length !== 3 || !position.every(Number.isFinite))
20
+ return polygon as Point2[]
21
+ const angles =
22
+ Array.isArray(rotation) && rotation.length === 3 && rotation.every(Number.isFinite)
23
+ ? new Euler(rotation[0], rotation[1], rotation[2])
24
+ : new Euler()
25
+ return polygon.map(([x, z]) => {
26
+ const point = new Vector3(x, 0, z).applyEuler(angles)
27
+ return [point.x + position[0], point.z + position[2]]
28
+ })
29
+ }
@@ -8,7 +8,11 @@ import { PlaneGeometry } from 'three'
8
8
  import { distance, smoothstep, uv, vec2 } from 'three/tsl'
9
9
  import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
10
10
  import { EDITOR_LAYER } from '../../../lib/constants'
11
- import { formatLinearMeasurement, type LinearUnit } from '../../../lib/measurements'
11
+ import {
12
+ formatLinearMeasurement,
13
+ type LinearUnit,
14
+ type MetricNotation,
15
+ } from '../../../lib/measurements'
12
16
  import { createLineGeometry, getBoxEdgePoints } from './placement-box-geometry'
13
17
 
14
18
  const VALID_COLOR = 0x22_c5_5e // green-500
@@ -17,6 +21,7 @@ const MEASUREMENT_COLOR = 0x0f_17_2a
17
21
 
18
22
  type PlacementBoxMeasurements = {
19
23
  unit: LinearUnit
24
+ metricNotation?: MetricNotation
20
25
  }
21
26
 
22
27
  function getMeasurementGuidePoints(width: number, height: number, depth: number) {
@@ -99,15 +104,24 @@ function getMeasurementGuidePoints(width: number, height: number, depth: number)
99
104
  }
100
105
 
101
106
  function MeasurementPill({
107
+ active,
102
108
  label,
109
+ onSelect,
103
110
  position,
104
111
  }: {
112
+ active?: boolean
105
113
  label: string
114
+ onSelect?: () => void
106
115
  position: [number, number, number]
107
116
  }) {
108
117
  return (
109
- <Html center position={position} style={{ pointerEvents: 'none' }}>
110
- <div
118
+ <Html center position={position} style={{ pointerEvents: onSelect ? 'auto' : 'none' }}>
119
+ <button
120
+ className={active ? 'ring-1 ring-indigo-200' : undefined}
121
+ onClick={(event) => {
122
+ event.stopPropagation()
123
+ onSelect?.()
124
+ }}
111
125
  style={{
112
126
  background: 'rgba(15, 23, 42, 0.86)',
113
127
  border: '1px solid rgba(15, 23, 42, 0.65)',
@@ -118,12 +132,13 @@ function MeasurementPill({
118
132
  fontWeight: 600,
119
133
  lineHeight: 1,
120
134
  padding: '4px 8px',
121
- pointerEvents: 'none',
135
+ pointerEvents: onSelect ? 'auto' : 'none',
122
136
  whiteSpace: 'nowrap',
123
137
  }}
138
+ type="button"
124
139
  >
125
140
  {label}
126
- </div>
141
+ </button>
127
142
  </Html>
128
143
  )
129
144
  }
@@ -142,8 +157,12 @@ function MeasurementPill({
142
157
  * a shelf — lines up without an extra offset.
143
158
  */
144
159
  export function PlacementBox({
160
+ activeDimensionId,
145
161
  dimensions,
162
+ dimensionInput = '',
146
163
  measurements,
164
+ measurementValues,
165
+ onDimensionSelect,
147
166
  position,
148
167
  rotationY = 0,
149
168
  valid,
@@ -152,14 +171,20 @@ export function PlacementBox({
152
171
  dimensions: [number, number, number]
153
172
  /** Optional dimension guide labels matching the GLB item placement cursor. */
154
173
  measurements?: PlacementBoxMeasurements
174
+ /** Values represented by the editable dimension pills; defaults to the box dimensions. */
175
+ measurementValues?: [width: number, height: number, depth: number]
155
176
  /** World-plan position of the footprint centre (floor level). */
156
177
  position: [number, number, number]
157
178
  /** Y-rotation in radians, applied to the whole box. */
158
179
  rotationY?: number
159
180
  /** Drives the colour: green when placeable, red otherwise. */
160
181
  valid: boolean
182
+ activeDimensionId?: string | null
183
+ dimensionInput?: string
184
+ onDimensionSelect?: (id: string) => void
161
185
  }) {
162
186
  const [width, height, depth] = dimensions
187
+ const [measurementWidth, measurementHeight, measurementDepth] = measurementValues ?? dimensions
163
188
 
164
189
  const edgeGeometry = useMemo(
165
190
  () =>
@@ -271,15 +296,45 @@ export function PlacementBox({
271
296
  renderOrder={998}
272
297
  />
273
298
  <MeasurementPill
274
- label={formatLinearMeasurement(width, measurements.unit)}
299
+ active={activeDimensionId === 'cabinet-width'}
300
+ label={
301
+ activeDimensionId === 'cabinet-width' && dimensionInput
302
+ ? dimensionInput
303
+ : formatLinearMeasurement(
304
+ measurementWidth,
305
+ measurements.unit,
306
+ measurements.metricNotation,
307
+ )
308
+ }
309
+ onSelect={onDimensionSelect ? () => onDimensionSelect('cabinet-width') : undefined}
275
310
  position={[0, 0.04, depth / 2 + 0.24]}
276
311
  />
277
312
  <MeasurementPill
278
- label={formatLinearMeasurement(depth, measurements.unit)}
313
+ active={activeDimensionId === 'cabinet-depth'}
314
+ label={
315
+ activeDimensionId === 'cabinet-depth' && dimensionInput
316
+ ? dimensionInput
317
+ : formatLinearMeasurement(
318
+ measurementDepth,
319
+ measurements.unit,
320
+ measurements.metricNotation,
321
+ )
322
+ }
323
+ onSelect={onDimensionSelect ? () => onDimensionSelect('cabinet-depth') : undefined}
279
324
  position={[width / 2 + 0.24, 0.04, 0]}
280
325
  />
281
326
  <MeasurementPill
282
- label={formatLinearMeasurement(height, measurements.unit)}
327
+ active={activeDimensionId === 'cabinet-height'}
328
+ label={
329
+ activeDimensionId === 'cabinet-height' && dimensionInput
330
+ ? dimensionInput
331
+ : formatLinearMeasurement(
332
+ measurementHeight,
333
+ measurements.unit,
334
+ measurements.metricNotation,
335
+ )
336
+ }
337
+ onSelect={onDimensionSelect ? () => onDimensionSelect('cabinet-height') : undefined}
283
338
  position={[-width / 2 - 0.24, height / 2, -depth / 2]}
284
339
  />
285
340
  </>
@@ -0,0 +1,121 @@
1
+ 'use client'
2
+
3
+ import { useViewer } from '@pascal-app/viewer'
4
+ import { Html } from '@react-three/drei'
5
+ import { useLayoutEffect, useMemo } from 'react'
6
+ import { BufferGeometry, Float32BufferAttribute, Line as ThreeLine } from 'three'
7
+ import { LineBasicNodeMaterial } from 'three/webgpu'
8
+ import { EDITOR_LAYER } from '../../../lib/constants'
9
+ import type { PlacementPreviewDimension } from '../../../store/use-placement-preview'
10
+ import usePlacementPreview from '../../../store/use-placement-preview'
11
+ import { formatMeasurement } from '../../editor/measurement-pill'
12
+
13
+ const DIMENSION_COLOR = 0x63_66_f1
14
+ const dimensionMaterial = new LineBasicNodeMaterial({
15
+ color: DIMENSION_COLOR,
16
+ depthTest: false,
17
+ depthWrite: false,
18
+ toneMapped: false,
19
+ })
20
+
21
+ export function PlacementDimensionGuides() {
22
+ const dimensions = usePlacementPreview((state) => state.dimensions)
23
+ const activeDimensionId = usePlacementPreview((state) => state.activeDimensionId)
24
+ const dimensionInput = usePlacementPreview((state) => state.dimensionInput)
25
+ const unit = useViewer((state) => state.unit)
26
+ const metricNotation = useViewer((state) => state.metricNotation)
27
+ if (dimensions.length === 0) return null
28
+
29
+ return (
30
+ <>
31
+ {dimensions
32
+ .filter((dimension) => dimension.renderIn3d !== false)
33
+ .map((dimension) => (
34
+ <PlacementDimensionGuide
35
+ dimension={dimension}
36
+ dimensionInput={dimension.id === activeDimensionId ? dimensionInput : null}
37
+ key={dimension.id}
38
+ metricNotation={metricNotation}
39
+ unit={unit}
40
+ />
41
+ ))}
42
+ </>
43
+ )
44
+ }
45
+
46
+ function PlacementDimensionGuide({
47
+ dimension,
48
+ dimensionInput,
49
+ metricNotation,
50
+ unit,
51
+ }: {
52
+ dimension: PlacementPreviewDimension
53
+ dimensionInput: string | null
54
+ metricNotation: 'meters' | 'millimeters'
55
+ unit: 'metric' | 'imperial'
56
+ }) {
57
+ const { line, position } = useMemo(() => {
58
+ const position = new Float32BufferAttribute(new Float32Array(6), 3)
59
+ const geometry = new BufferGeometry()
60
+ geometry.setAttribute('position', position)
61
+ const line = new ThreeLine(geometry, dimensionMaterial)
62
+ line.frustumCulled = false
63
+ line.layers.set(EDITOR_LAYER)
64
+ line.renderOrder = 1000
65
+ return { line, position }
66
+ }, [])
67
+
68
+ const start = useMemo(
69
+ () =>
70
+ [
71
+ dimension.start[0] + dimension.offsetNormal[0] * dimension.offsetDistance,
72
+ dimension.start[1],
73
+ dimension.start[2] + dimension.offsetNormal[1] * dimension.offsetDistance,
74
+ ] as [number, number, number],
75
+ [dimension.offsetDistance, dimension.offsetNormal, dimension.start],
76
+ )
77
+ const end = useMemo(
78
+ () =>
79
+ [
80
+ dimension.end[0] + dimension.offsetNormal[0] * dimension.offsetDistance,
81
+ dimension.end[1],
82
+ dimension.end[2] + dimension.offsetNormal[1] * dimension.offsetDistance,
83
+ ] as [number, number, number],
84
+ [dimension.end, dimension.offsetDistance, dimension.offsetNormal],
85
+ )
86
+ useLayoutEffect(() => {
87
+ position.setXYZ(0, ...start)
88
+ position.setXYZ(1, ...end)
89
+ position.needsUpdate = true
90
+ }, [end, position, start])
91
+
92
+ useLayoutEffect(() => () => line.geometry.dispose(), [line])
93
+ return (
94
+ <>
95
+ <primitive object={line} />
96
+ <Html
97
+ center
98
+ position={[
99
+ (start[0] + end[0]) / 2,
100
+ (start[1] + end[1]) / 2 + 0.015,
101
+ (start[2] + end[2]) / 2,
102
+ ]}
103
+ style={{ pointerEvents: 'auto', userSelect: 'none' }}
104
+ zIndexRange={[100, 0]}
105
+ >
106
+ <div
107
+ className={`rounded-[3px] px-1.5 py-0.5 font-mono text-[10px] font-semibold text-white shadow-sm ${
108
+ dimensionInput !== null ? 'bg-indigo-700 ring-1 ring-indigo-200' : 'bg-indigo-500/90'
109
+ }`}
110
+ onClick={(event) => {
111
+ event.stopPropagation()
112
+ usePlacementPreview.getState().selectDimension(dimension.id)
113
+ }}
114
+ style={{ cursor: 'text', pointerEvents: 'auto' }}
115
+ >
116
+ {dimensionInput || formatMeasurement(dimension.value, unit, metricNotation)}
117
+ </div>
118
+ </Html>
119
+ </>
120
+ )
121
+ }