@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
@@ -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
+ }
@@ -104,15 +104,24 @@ function getMeasurementGuidePoints(width: number, height: number, depth: number)
104
104
  }
105
105
 
106
106
  function MeasurementPill({
107
+ active,
107
108
  label,
109
+ onSelect,
108
110
  position,
109
111
  }: {
112
+ active?: boolean
110
113
  label: string
114
+ onSelect?: () => void
111
115
  position: [number, number, number]
112
116
  }) {
113
117
  return (
114
- <Html center position={position} style={{ pointerEvents: 'none' }}>
115
- <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
+ }}
116
125
  style={{
117
126
  background: 'rgba(15, 23, 42, 0.86)',
118
127
  border: '1px solid rgba(15, 23, 42, 0.65)',
@@ -123,12 +132,13 @@ function MeasurementPill({
123
132
  fontWeight: 600,
124
133
  lineHeight: 1,
125
134
  padding: '4px 8px',
126
- pointerEvents: 'none',
135
+ pointerEvents: onSelect ? 'auto' : 'none',
127
136
  whiteSpace: 'nowrap',
128
137
  }}
138
+ type="button"
129
139
  >
130
140
  {label}
131
- </div>
141
+ </button>
132
142
  </Html>
133
143
  )
134
144
  }
@@ -147,8 +157,12 @@ function MeasurementPill({
147
157
  * a shelf — lines up without an extra offset.
148
158
  */
149
159
  export function PlacementBox({
160
+ activeDimensionId,
150
161
  dimensions,
162
+ dimensionInput = '',
151
163
  measurements,
164
+ measurementValues,
165
+ onDimensionSelect,
152
166
  position,
153
167
  rotationY = 0,
154
168
  valid,
@@ -157,14 +171,20 @@ export function PlacementBox({
157
171
  dimensions: [number, number, number]
158
172
  /** Optional dimension guide labels matching the GLB item placement cursor. */
159
173
  measurements?: PlacementBoxMeasurements
174
+ /** Values represented by the editable dimension pills; defaults to the box dimensions. */
175
+ measurementValues?: [width: number, height: number, depth: number]
160
176
  /** World-plan position of the footprint centre (floor level). */
161
177
  position: [number, number, number]
162
178
  /** Y-rotation in radians, applied to the whole box. */
163
179
  rotationY?: number
164
180
  /** Drives the colour: green when placeable, red otherwise. */
165
181
  valid: boolean
182
+ activeDimensionId?: string | null
183
+ dimensionInput?: string
184
+ onDimensionSelect?: (id: string) => void
166
185
  }) {
167
186
  const [width, height, depth] = dimensions
187
+ const [measurementWidth, measurementHeight, measurementDepth] = measurementValues ?? dimensions
168
188
 
169
189
  const edgeGeometry = useMemo(
170
190
  () =>
@@ -276,15 +296,45 @@ export function PlacementBox({
276
296
  renderOrder={998}
277
297
  />
278
298
  <MeasurementPill
279
- label={formatLinearMeasurement(width, measurements.unit, measurements.metricNotation)}
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}
280
310
  position={[0, 0.04, depth / 2 + 0.24]}
281
311
  />
282
312
  <MeasurementPill
283
- label={formatLinearMeasurement(depth, measurements.unit, measurements.metricNotation)}
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}
284
324
  position={[width / 2 + 0.24, 0.04, 0]}
285
325
  />
286
326
  <MeasurementPill
287
- label={formatLinearMeasurement(height, measurements.unit, measurements.metricNotation)}
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}
288
338
  position={[-width / 2 - 0.24, height / 2, -depth / 2]}
289
339
  />
290
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
+ }