@pascal-app/editor 0.9.1 → 0.9.2

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 (262) hide show
  1. package/package.json +12 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +47 -0
  4. package/src/components/editor/custom-camera-controls.tsx +63 -25
  5. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  6. package/src/components/editor/editor-layout-v2.tsx +18 -0
  7. package/src/components/editor/export-manager.tsx +71 -63
  8. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +36 -3
  10. package/src/components/editor/first-person/build-collider-world.ts +32 -5
  11. package/src/components/editor/first-person-controls.tsx +118 -7
  12. package/src/components/editor/floating-action-menu.tsx +410 -33
  13. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  14. package/src/components/editor/floorplan-background-selection.test.ts +58 -0
  15. package/src/components/editor/floorplan-background-selection.ts +3 -2
  16. package/src/components/editor/floorplan-panel.tsx +1804 -849
  17. package/src/components/editor/grid.tsx +166 -39
  18. package/src/components/editor/group-actions.ts +457 -0
  19. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  20. package/src/components/editor/group-move-3d.ts +397 -0
  21. package/src/components/editor/group-rotate-handle.tsx +102 -64
  22. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  23. package/src/components/editor/group-transform-shared.test.ts +192 -1
  24. package/src/components/editor/group-transform-shared.ts +240 -7
  25. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  26. package/src/components/editor/handles/use-handle-drag.ts +37 -4
  27. package/src/components/editor/index.tsx +248 -27
  28. package/src/components/editor/measurement-pill.tsx +62 -22
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +366 -173
  31. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  32. package/src/components/editor/quick-measurement-card.tsx +70 -0
  33. package/src/components/editor/quick-measurement-hud.tsx +28 -0
  34. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  35. package/src/components/editor/selection-manager.tsx +812 -466
  36. package/src/components/editor/site-edge-labels.tsx +4 -4
  37. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  38. package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
  39. package/src/components/editor/three-context-bridge.ts +22 -0
  40. package/src/components/editor/thumbnail-generator.tsx +136 -43
  41. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  42. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  43. package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
  44. package/src/components/editor/wall-measurement-label.tsx +3 -13
  45. package/src/components/editor/wall-move-side-handles.tsx +36 -21
  46. package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
  47. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  48. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
  49. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  50. package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
  51. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  52. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
  53. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  54. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +169 -4
  55. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
  56. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  57. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
  58. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
  59. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +4 -1
  60. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
  61. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
  62. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  63. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
  64. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  65. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  66. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  67. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  68. package/src/components/systems/zone/zone-system.tsx +42 -13
  69. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  70. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  71. package/src/components/tools/fence/fence-drafting.ts +80 -8
  72. package/src/components/tools/item/move-tool.tsx +17 -13
  73. package/src/components/tools/item/placement-math.test.ts +13 -1
  74. package/src/components/tools/item/placement-math.ts +28 -2
  75. package/src/components/tools/item/placement-strategies.ts +246 -5
  76. package/src/components/tools/item/placement-types.ts +13 -1
  77. package/src/components/tools/item/use-draft-node.ts +31 -1
  78. package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
  79. package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
  80. package/src/components/tools/roof/roof-tool.tsx +320 -94
  81. package/src/components/tools/select/box-select-state.ts +18 -7
  82. package/src/components/tools/select/box-select-tool.tsx +176 -40
  83. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  84. package/src/components/tools/select/marquee-geometry.ts +155 -0
  85. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  86. package/src/components/tools/select/select-candidates.ts +1 -1
  87. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  88. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  89. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  90. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  91. package/src/components/tools/shared/placement-box.tsx +183 -1
  92. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  93. package/src/components/tools/site/site-boundary-editor.tsx +88 -36
  94. package/src/components/tools/stair/stair-defaults.ts +1 -0
  95. package/src/components/tools/stair/stair-tool.tsx +82 -11
  96. package/src/components/tools/tool-manager.tsx +86 -25
  97. package/src/components/tools/wall/wall-drafting.test.ts +330 -0
  98. package/src/components/tools/wall/wall-drafting.ts +180 -103
  99. package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
  100. package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
  101. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  102. package/src/components/tools/zone/zone-tool.tsx +82 -88
  103. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  104. package/src/components/ui/action-menu/control-modes.tsx +40 -93
  105. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  106. package/src/components/ui/action-menu/index.tsx +2 -4
  107. package/src/components/ui/action-menu/measurement-control.tsx +188 -0
  108. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  109. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  110. package/src/components/ui/command-palette/editor-commands.tsx +2 -2
  111. package/src/components/ui/command-palette/index.tsx +4 -3
  112. package/src/components/ui/controls/material-paint-panel.tsx +86 -17
  113. package/src/components/ui/controls/material-picker.tsx +83 -152
  114. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  115. package/src/components/ui/controls/metric-control.tsx +118 -44
  116. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  117. package/src/components/ui/controls/slider-control.tsx +88 -38
  118. package/src/components/ui/floating-level-selector.tsx +1 -1
  119. package/src/components/ui/helpers/building-helper.tsx +10 -23
  120. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  121. package/src/components/ui/helpers/helper-manager.tsx +227 -14
  122. package/src/components/ui/helpers/item-helper.tsx +28 -32
  123. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  124. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  125. package/src/components/ui/icon-ref.tsx +47 -0
  126. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  127. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/panels/node-display.ts +17 -17
  130. package/src/components/ui/panels/panel-manager.tsx +22 -13
  131. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  132. package/src/components/ui/panels/parametric-inspector.tsx +15 -2
  133. package/src/components/ui/panels/reference-panel.tsx +54 -19
  134. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  135. package/src/components/ui/primitives/sidebar.tsx +1 -0
  136. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  137. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  138. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  139. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +71 -40
  140. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
  141. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  142. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  143. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  144. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  145. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  146. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  147. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  148. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  149. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  150. package/src/components/ui/sidebar/panels/site-panel/index.tsx +40 -17
  151. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  152. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  153. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  154. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  155. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  156. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  157. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  158. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  159. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  160. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  161. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  162. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  163. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  164. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  165. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  166. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  167. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  168. package/src/components/ui/snap-target-badge.tsx +88 -0
  169. package/src/components/viewer-overlay.tsx +85 -10
  170. package/src/components/viewer-zone-system.tsx +6 -1
  171. package/src/hooks/use-auto-save.test.ts +14 -0
  172. package/src/hooks/use-auto-save.ts +54 -7
  173. package/src/hooks/use-ceiling-events.ts +3 -2
  174. package/src/hooks/use-drag-action.ts +4 -1
  175. package/src/hooks/use-keyboard.ts +385 -40
  176. package/src/index.tsx +219 -6
  177. package/src/lib/active-placement-surface.ts +35 -0
  178. package/src/lib/ceiling-plan-snap.ts +24 -0
  179. package/src/lib/contextual-help.test.ts +112 -0
  180. package/src/lib/contextual-help.ts +144 -0
  181. package/src/lib/continuation.ts +64 -0
  182. package/src/lib/direct-manipulation.test.ts +192 -0
  183. package/src/lib/direct-manipulation.ts +109 -0
  184. package/src/lib/editor-api.ts +23 -35
  185. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  186. package/src/lib/floorplan/apply-alignment.ts +18 -12
  187. package/src/lib/floorplan/floorplan-export.tsx +364 -0
  188. package/src/lib/floorplan/geometry.ts +53 -0
  189. package/src/lib/floorplan/index.ts +3 -0
  190. package/src/lib/floorplan/items.ts +5 -2
  191. package/src/lib/floorplan/plan-coords.ts +21 -0
  192. package/src/lib/fresh-planar-placement.test.ts +240 -3
  193. package/src/lib/fresh-planar-placement.ts +68 -1
  194. package/src/lib/glb-export.test.ts +441 -0
  195. package/src/lib/glb-export.ts +874 -0
  196. package/src/lib/history.ts +9 -0
  197. package/src/lib/interaction/hot-set.test.ts +133 -0
  198. package/src/lib/interaction/hot-set.ts +67 -0
  199. package/src/lib/interaction/overlay-policy.test.ts +51 -0
  200. package/src/lib/interaction/overlay-policy.ts +59 -0
  201. package/src/lib/interaction/scope.ts +211 -0
  202. package/src/lib/level-selection.ts +2 -2
  203. package/src/lib/material-paint.ts +36 -49
  204. package/src/lib/measurement-kind.test.ts +30 -0
  205. package/src/lib/measurement-kind.ts +19 -0
  206. package/src/lib/measurement-label.test.ts +47 -0
  207. package/src/lib/measurement-label.ts +68 -0
  208. package/src/lib/measurement-parser.ts +116 -0
  209. package/src/lib/measurements.test.ts +183 -0
  210. package/src/lib/measurements.ts +199 -0
  211. package/src/lib/paint-scope.test.ts +454 -0
  212. package/src/lib/paint-scope.ts +461 -0
  213. package/src/lib/placement-drag-release.ts +23 -0
  214. package/src/lib/planar-cursor-placement.test.ts +57 -0
  215. package/src/lib/plugin-panels.test.ts +19 -0
  216. package/src/lib/plugin-panels.ts +91 -0
  217. package/src/lib/quick-measurement.test.ts +77 -0
  218. package/src/lib/quick-measurement.ts +109 -0
  219. package/src/lib/roof-duplication.ts +6 -6
  220. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  221. package/src/lib/roof-wall-hit.ts +164 -0
  222. package/src/lib/scene-clipboard.test.ts +196 -0
  223. package/src/lib/scene-clipboard.ts +73 -8
  224. package/src/lib/scene.ts +28 -5
  225. package/src/lib/selection-routing.test.ts +298 -0
  226. package/src/lib/selection-routing.ts +175 -0
  227. package/src/lib/sfx/index.ts +1 -0
  228. package/src/lib/sfx/movement-tick.test.ts +65 -0
  229. package/src/lib/sfx/movement-tick.ts +18 -0
  230. package/src/lib/sfx-bus.ts +50 -13
  231. package/src/lib/sfx-player.test.ts +124 -0
  232. package/src/lib/sfx-player.ts +111 -66
  233. package/src/lib/slab-plan-snap.test.ts +93 -0
  234. package/src/lib/slab-plan-snap.ts +108 -0
  235. package/src/lib/snapping-mode.test.ts +138 -0
  236. package/src/lib/snapping-mode.ts +179 -0
  237. package/src/lib/stair-duplication.ts +4 -4
  238. package/src/lib/surface-plan-snap.test.ts +71 -0
  239. package/src/lib/surface-plan-snap.ts +256 -0
  240. package/src/lib/use-linear-display.ts +40 -0
  241. package/src/lib/world-grid-snap.ts +37 -5
  242. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  243. package/src/store/use-editor.tsx +484 -114
  244. package/src/store/use-facing-pose.ts +44 -0
  245. package/src/store/use-fence-curve-draft.ts +21 -0
  246. package/src/store/use-floorplan-draft-preview.ts +101 -0
  247. package/src/store/use-floorplan-marquee.ts +50 -0
  248. package/src/store/use-interaction-scope.test.ts +186 -0
  249. package/src/store/use-interaction-scope.ts +132 -0
  250. package/src/store/use-measurement-draft.test.ts +530 -0
  251. package/src/store/use-measurement-draft.ts +543 -0
  252. package/src/store/use-opening-guides.ts +41 -0
  253. package/src/store/use-placement-preview.ts +11 -3
  254. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  255. package/src/store/use-quick-measurement-hud.ts +77 -0
  256. package/src/store/use-segment-draft-chain.ts +28 -0
  257. package/src/store/use-stair-build-preview.ts +43 -0
  258. package/src/store/use-wall-snap-indicator.ts +2 -0
  259. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  260. package/src/components/editor/group-move-handle.tsx +0 -316
  261. package/src/components/ui/panels/paint-panel.tsx +0 -163
  262. package/src/lib/level-name.ts +0 -11
@@ -6,24 +6,45 @@ import { furnishTools } from '../../../components/ui/action-menu/furnish-tools'
6
6
  import { tools } from '../../../components/ui/action-menu/structure-tools'
7
7
  import { EDITOR_LAYER } from '../../../lib/constants'
8
8
  import useEditor from '../../../store/use-editor'
9
+ import useWallSnapIndicator from '../../../store/use-wall-snap-indicator'
9
10
 
10
11
  interface CursorSphereProps extends Omit<ThreeElements['group'], 'ref'> {
11
12
  color?: string
12
13
  depthWrite?: boolean
13
14
  showTooltip?: boolean
14
15
  height?: number
16
+ /**
17
+ * Put the bright marker dot at the TIP of the vertical line (y = height)
18
+ * instead of on the ground ring. Used when the point being placed hangs
19
+ * above the floor (e.g. duct drawn against the ceiling): the dot rides at
20
+ * the cursor / placement point while the line drops to a floor ring that
21
+ * keeps the plan position readable.
22
+ */
23
+ dotAtTip?: boolean
15
24
  /** Custom tooltip content — overrides the auto-detected build tool icon */
16
25
  tooltipContent?: React.ReactNode
17
26
  }
18
27
 
19
28
  export const CursorSphere = forwardRef<Group, CursorSphereProps>(function CursorSphere(
20
- { color = '#818cf8', showTooltip = true, height = 2.5, visible = true, tooltipContent, ...props },
29
+ {
30
+ color = '#818cf8',
31
+ showTooltip = true,
32
+ height = 2.5,
33
+ dotAtTip = false,
34
+ visible = true,
35
+ tooltipContent,
36
+ ...props
37
+ },
21
38
  ref,
22
39
  ) {
23
40
  const tool = useEditor((s) => s.tool)
24
41
  const mode = useEditor((s) => s.mode)
25
42
  const catalogCategory = useEditor((s) => s.catalogCategory)
26
43
  const isFloorplanHovered = useEditor((s) => s.isFloorplanHovered)
44
+ // While a wall snap is active the beacon layer already marks the point
45
+ // (green square at a corner); hide the cursor's ground dot/ring so it
46
+ // doesn't sit on top of that glyph.
47
+ const isSnapping = useWallSnapIndicator((s) => s.point !== null)
27
48
 
28
49
  // Find the icon for the current tool
29
50
  let activeToolConfig = null
@@ -39,32 +60,38 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
39
60
 
40
61
  return (
41
62
  <group ref={ref} {...props} visible={isVisible}>
42
- {/* Flat marker on the ground */}
43
- <group rotation={[-Math.PI / 2, 0, 0]}>
44
- {/* Center dot */}
45
- <mesh layers={EDITOR_LAYER} renderOrder={2}>
46
- <circleGeometry args={[0.06, 32]} />
47
- <meshBasicMaterial
48
- color={color}
49
- depthTest={false}
50
- depthWrite={false}
51
- opacity={0.9}
52
- transparent
53
- />
54
- </mesh>
63
+ {/* Flat marker on the ground. The bright center dot moves to the tip
64
+ of the line in `dotAtTip` mode (the placement point hangs above the
65
+ floor), leaving a faint ring here so the plan position stays read. */}
66
+ {!isSnapping && (
67
+ <group rotation={[-Math.PI / 2, 0, 0]}>
68
+ {/* Center dot — at the ground unless the placement point is elevated */}
69
+ {!dotAtTip && (
70
+ <mesh layers={EDITOR_LAYER} renderOrder={2}>
71
+ <circleGeometry args={[0.06, 32]} />
72
+ <meshBasicMaterial
73
+ color={color}
74
+ depthTest={false}
75
+ depthWrite={false}
76
+ opacity={0.9}
77
+ transparent
78
+ />
79
+ </mesh>
80
+ )}
55
81
 
56
- {/* Outer ring / glow */}
57
- <mesh layers={EDITOR_LAYER} renderOrder={2}>
58
- <circleGeometry args={[0.2, 32]} />
59
- <meshBasicMaterial
60
- color={color}
61
- depthTest={false}
62
- depthWrite={false}
63
- opacity={0.25}
64
- transparent
65
- />
66
- </mesh>
67
- </group>
82
+ {/* Outer ring / glow */}
83
+ <mesh layers={EDITOR_LAYER} renderOrder={2}>
84
+ <circleGeometry args={[0.2, 32]} />
85
+ <meshBasicMaterial
86
+ color={color}
87
+ depthTest={false}
88
+ depthWrite={false}
89
+ opacity={dotAtTip ? 0.2 : 0.25}
90
+ transparent
91
+ />
92
+ </mesh>
93
+ </group>
94
+ )}
68
95
 
69
96
  {/* Vertical line */}
70
97
  {height > 0 && (
@@ -80,6 +107,15 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
80
107
  </mesh>
81
108
  )}
82
109
 
110
+ {/* Bright marker dot at the tip of the line — the actual placement
111
+ point, riding at the cursor while the line drops to the floor. */}
112
+ {dotAtTip && height > 0 && (
113
+ <mesh layers={EDITOR_LAYER} position={[0, height, 0]} renderOrder={2}>
114
+ <sphereGeometry args={[0.08, 20, 14]} />
115
+ <meshBasicMaterial color={color} depthTest={false} depthWrite={false} />
116
+ </mesh>
117
+ )}
118
+
83
119
  {/* Tool Icon Tooltip at the top of the line */}
84
120
  {isVisible && showTooltip && (activeToolConfig || tooltipContent) && (
85
121
  <Html
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { sceneRegistry } from '@pascal-app/core'
3
+ import { type AnyNodeId, resolveFacingIndicator, sceneRegistry, useScene } from '@pascal-app/core'
4
4
  import { useEffect, useMemo } from 'react'
5
5
  import {
6
6
  Box3,
@@ -15,6 +15,7 @@ import {
15
15
  import { distance, smoothstep, uv, vec2 } from 'three/tsl'
16
16
  import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
17
17
  import { EDITOR_LAYER } from '../../../lib/constants'
18
+ import useFacingPose from '../../../store/use-facing-pose'
18
19
 
19
20
  const NO_RAYCAST = () => null
20
21
 
@@ -71,6 +72,8 @@ interface DragBoundingBoxProps {
71
72
  * includes per-level landings outside the shaft footprint).
72
73
  */
73
74
  size?: [number, number, number]
75
+ /** Local center of `size`. Defaults to the node origin plus half-height. */
76
+ center?: [number, number, number]
74
77
  /** Y center of the box in the node's local frame. Defaults to `size[1] / 2`. */
75
78
  centerY?: number
76
79
  color?: number
@@ -90,9 +93,13 @@ export function DragBoundingBox({
90
93
  rotationY = 0,
91
94
  fallbackSize = [0, 0, 0],
92
95
  size,
96
+ center,
93
97
  centerY,
94
98
  color = DEFAULT_COLOR,
95
99
  }: DragBoundingBoxProps) {
100
+ const nodeType = useScene((state) => state.nodes[nodeId as AnyNodeId]?.type)
101
+ const facing = nodeType ? resolveFacingIndicator(nodeType) : null
102
+
96
103
  const measured = useMemo(() => {
97
104
  if (size) return null
98
105
  const obj = sceneRegistry.nodes.get(nodeId)
@@ -101,9 +108,10 @@ export function DragBoundingBox({
101
108
 
102
109
  const [w, h, d] = size ?? measured?.size ?? fallbackSize
103
110
  const [cx, cy, cz] = size
104
- ? [0, centerY ?? size[1] / 2, 0]
111
+ ? (center ?? [0, centerY ?? size[1] / 2, 0])
105
112
  : (measured?.center ?? [0, fallbackSize[1] / 2, 0])
106
113
  const minY = cy - h / 2
114
+ const groundY = minY + 0.01
107
115
 
108
116
  const edgeGeometry = useMemo(() => {
109
117
  const box = new BoxGeometry(w, h, d)
@@ -117,9 +125,9 @@ export function DragBoundingBox({
117
125
  const planeGeometry = useMemo(() => {
118
126
  const plane = new PlaneGeometry(w, d)
119
127
  plane.rotateX(-Math.PI / 2)
120
- plane.translate(cx, minY + 0.01, cz)
128
+ plane.translate(cx, groundY, cz)
121
129
  return plane
122
- }, [w, d, cx, minY, cz])
130
+ }, [w, d, cx, groundY, cz])
123
131
 
124
132
  const edgeMaterial = useMemo(
125
133
  () => new LineBasicNodeMaterial({ color, linewidth: 3, depthTest: false, depthWrite: false }),
@@ -147,6 +155,22 @@ export function DragBoundingBox({
147
155
  [edgeGeometry, planeGeometry, edgeMaterial, planeMaterial],
148
156
  )
149
157
 
158
+ // Publish the facing pose to the editor-side overlay (the single triangle
159
+ // renderer) rather than drawing it here. The node origin is `position`; the
160
+ // footprint centre is `[cx, cz]` in the node's local frame. Runs each drag
161
+ // frame so the triangle follows; a separate mount/unmount effect clears it.
162
+ useEffect(() => {
163
+ if (!facing || d <= 0) return
164
+ useFacingPose.getState().set({
165
+ position: [position[0], position[1] + groundY, position[2]],
166
+ rotationY,
167
+ depth: d,
168
+ center: [cx, cz],
169
+ reversed: facing.reversed,
170
+ })
171
+ }, [facing, position, rotationY, d, cx, cz, groundY])
172
+ useEffect(() => () => useFacingPose.getState().clear(), [])
173
+
150
174
  if (w <= 0 || h <= 0 || d <= 0) return null
151
175
 
152
176
  return (
@@ -0,0 +1,87 @@
1
+ import { useEffect, useMemo } from 'react'
2
+ import { BufferGeometry, DoubleSide, Float32BufferAttribute } from 'three'
3
+ import { MeshBasicNodeMaterial } from 'three/webgpu'
4
+ import { EDITOR_LAYER } from '../../../lib/constants'
5
+
6
+ // A flat forward-pointing triangle drawn on the floor just in front of a
7
+ // placement ghost, so the direction the node will face is obvious. Tip at local
8
+ // +Z (every kind's forward face); render it inside the ghost's rotated group so
9
+ // it inherits the node's yaw. Matches the item coordinator / drag-bounding-box
10
+ // indicators (same size, colour, double-sided so it shows from above).
11
+ const FACING_INDICATOR_WIDTH = 0.4
12
+ const FACING_INDICATOR_LENGTH = 0.46
13
+ const FACING_INDICATOR_GAP = 0.45
14
+
15
+ /**
16
+ * @param depth bbox depth (along local Z) of the ghost — positions the
17
+ * triangle just past the front edge.
18
+ * @param center optional [x, z] of the bbox centre in the ghost's local frame.
19
+ * @param reversed point along local -Z (the front is the -Z side, e.g. a stair
20
+ * entry) instead of +Z.
21
+ * @param y small lift off the floor to avoid z-fighting.
22
+ */
23
+ export function FacingIndicator({
24
+ depth,
25
+ center = [0, 0],
26
+ reversed = false,
27
+ y = 0.02,
28
+ }: {
29
+ depth: number
30
+ center?: [number, number]
31
+ reversed?: boolean
32
+ y?: number
33
+ }) {
34
+ const dir = reversed ? -1 : 1
35
+ // Per-instance geometry/material (not module singletons) so this works no
36
+ // matter which package mounts it (the tools live in `nodes`, imported via
37
+ // `@pascal-app/editor`). Disposed on unmount.
38
+ const geometry = useMemo(() => {
39
+ const g = new BufferGeometry()
40
+ g.setAttribute(
41
+ 'position',
42
+ new Float32BufferAttribute(
43
+ [
44
+ 0,
45
+ 0,
46
+ dir * FACING_INDICATOR_LENGTH,
47
+ FACING_INDICATOR_WIDTH / 2,
48
+ 0,
49
+ 0,
50
+ -FACING_INDICATOR_WIDTH / 2,
51
+ 0,
52
+ 0,
53
+ ],
54
+ 3,
55
+ ),
56
+ )
57
+ return g
58
+ }, [dir])
59
+ const material = useMemo(
60
+ () =>
61
+ new MeshBasicNodeMaterial({
62
+ color: 0x22_c5_5e, // green-500 (forward)
63
+ depthTest: false,
64
+ depthWrite: false,
65
+ side: DoubleSide,
66
+ }),
67
+ [],
68
+ )
69
+ useEffect(
70
+ () => () => {
71
+ geometry.dispose()
72
+ material.dispose()
73
+ },
74
+ [geometry, material],
75
+ )
76
+
77
+ return (
78
+ <mesh
79
+ frustumCulled={false}
80
+ geometry={geometry}
81
+ layers={EDITOR_LAYER}
82
+ material={material}
83
+ position={[center[0], y, center[1] + dir * (depth / 2 + FACING_INDICATOR_GAP)]}
84
+ renderOrder={1001}
85
+ />
86
+ )
87
+ }
@@ -0,0 +1,54 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+ import type { Group } from 'three'
3
+ import useFacingPose, { type FacingPose } from '../../../store/use-facing-pose'
4
+ import { FacingIndicator } from './facing-indicator'
5
+
6
+ // The single editor-side renderer for the placement/move facing triangle.
7
+ // Mounted once inside ToolManager's building-local group; every tool publishes
8
+ // its ghost pose to `useFacingPose` and this draws the triangle. The pose
9
+ // (position/yaw) is applied imperatively to a ref so the per-frame cursor
10
+ // updates don't re-render React — only a change in footprint shape (depth /
11
+ // centre), which is constant per tool session, triggers a re-render.
12
+ export function FacingPoseIndicator() {
13
+ const groupRef = useRef<Group>(null)
14
+ const [shape, setShape] = useState<Pick<FacingPose, 'depth' | 'center' | 'reversed'> | null>(null)
15
+
16
+ useEffect(() => {
17
+ const apply = (pose: FacingPose | null) => {
18
+ const group = groupRef.current
19
+ if (group) {
20
+ if (pose) {
21
+ group.visible = true
22
+ group.position.set(...pose.position)
23
+ group.rotation.y = pose.rotationY
24
+ } else {
25
+ group.visible = false
26
+ }
27
+ }
28
+ setShape((prev) => {
29
+ if (!pose) return null
30
+ const center = pose.center ?? [0, 0]
31
+ if (
32
+ prev &&
33
+ prev.depth === pose.depth &&
34
+ prev.reversed === pose.reversed &&
35
+ (prev.center ?? [0, 0])[0] === center[0] &&
36
+ (prev.center ?? [0, 0])[1] === center[1]
37
+ ) {
38
+ return prev
39
+ }
40
+ return { depth: pose.depth, center, reversed: pose.reversed }
41
+ })
42
+ }
43
+ apply(useFacingPose.getState().pose)
44
+ return useFacingPose.subscribe((state) => apply(state.pose))
45
+ }, [])
46
+
47
+ return (
48
+ <group ref={groupRef} visible={false}>
49
+ {shape ? (
50
+ <FacingIndicator center={shape.center} depth={shape.depth} reversed={shape.reversed} />
51
+ ) : null}
52
+ </group>
53
+ )
54
+ }
@@ -2,15 +2,131 @@
2
2
 
3
3
  import '../../../three-types'
4
4
 
5
+ import { Html } from '@react-three/drei'
5
6
  import { useEffect, useMemo } from 'react'
6
7
  import { PlaneGeometry } from 'three'
7
8
  import { distance, smoothstep, uv, vec2 } from 'three/tsl'
8
9
  import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
9
10
  import { EDITOR_LAYER } from '../../../lib/constants'
11
+ import { formatLinearMeasurement, type LinearUnit } from '../../../lib/measurements'
10
12
  import { createLineGeometry, getBoxEdgePoints } from './placement-box-geometry'
11
13
 
12
14
  const VALID_COLOR = 0x22_c5_5e // green-500
13
15
  const INVALID_COLOR = 0xef_44_44 // red-500
16
+ const MEASUREMENT_COLOR = 0x0f_17_2a
17
+
18
+ type PlacementBoxMeasurements = {
19
+ unit: LinearUnit
20
+ }
21
+
22
+ function getMeasurementGuidePoints(width: number, height: number, depth: number) {
23
+ const minX = -width / 2
24
+ const maxX = width / 2
25
+ const minZ = -depth / 2
26
+ const maxZ = depth / 2
27
+ const guideOffset = 0.18
28
+ const tick = 0.08
29
+ const y = 0.02
30
+
31
+ return {
32
+ depth: [
33
+ maxX + guideOffset,
34
+ y,
35
+ minZ,
36
+ maxX + guideOffset,
37
+ y,
38
+ maxZ,
39
+
40
+ maxX + guideOffset - tick,
41
+ y,
42
+ minZ,
43
+ maxX + guideOffset + tick,
44
+ y,
45
+ minZ,
46
+
47
+ maxX + guideOffset - tick,
48
+ y,
49
+ maxZ,
50
+ maxX + guideOffset + tick,
51
+ y,
52
+ maxZ,
53
+ ],
54
+ height: [
55
+ minX - guideOffset,
56
+ 0,
57
+ minZ,
58
+ minX - guideOffset,
59
+ height,
60
+ minZ,
61
+
62
+ minX - guideOffset - tick,
63
+ 0,
64
+ minZ,
65
+ minX - guideOffset + tick,
66
+ 0,
67
+ minZ,
68
+
69
+ minX - guideOffset - tick,
70
+ height,
71
+ minZ,
72
+ minX - guideOffset + tick,
73
+ height,
74
+ minZ,
75
+ ],
76
+ width: [
77
+ minX,
78
+ y,
79
+ maxZ + guideOffset,
80
+ maxX,
81
+ y,
82
+ maxZ + guideOffset,
83
+
84
+ minX,
85
+ y,
86
+ maxZ + guideOffset - tick,
87
+ minX,
88
+ y,
89
+ maxZ + guideOffset + tick,
90
+
91
+ maxX,
92
+ y,
93
+ maxZ + guideOffset - tick,
94
+ maxX,
95
+ y,
96
+ maxZ + guideOffset + tick,
97
+ ],
98
+ }
99
+ }
100
+
101
+ function MeasurementPill({
102
+ label,
103
+ position,
104
+ }: {
105
+ label: string
106
+ position: [number, number, number]
107
+ }) {
108
+ return (
109
+ <Html center position={position} style={{ pointerEvents: 'none' }}>
110
+ <div
111
+ style={{
112
+ background: 'rgba(15, 23, 42, 0.86)',
113
+ border: '1px solid rgba(15, 23, 42, 0.65)',
114
+ borderRadius: '999px',
115
+ color: '#f8fafc',
116
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
117
+ fontSize: '11px',
118
+ fontWeight: 600,
119
+ lineHeight: 1,
120
+ padding: '4px 8px',
121
+ pointerEvents: 'none',
122
+ whiteSpace: 'nowrap',
123
+ }}
124
+ >
125
+ {label}
126
+ </div>
127
+ </Html>
128
+ )
129
+ }
14
130
 
15
131
  /**
16
132
  * Green/red placement footprint shown while a node follows the cursor — the
@@ -27,12 +143,15 @@ const INVALID_COLOR = 0xef_44_44 // red-500
27
143
  */
28
144
  export function PlacementBox({
29
145
  dimensions,
146
+ measurements,
30
147
  position,
31
148
  rotationY = 0,
32
149
  valid,
33
150
  }: {
34
151
  /** Footprint extent `[width, height, depth]` (unrotated). */
35
152
  dimensions: [number, number, number]
153
+ /** Optional dimension guide labels matching the GLB item placement cursor. */
154
+ measurements?: PlacementBoxMeasurements
36
155
  /** World-plan position of the footprint centre (floor level). */
37
156
  position: [number, number, number]
38
157
  /** Y-rotation in radians, applied to the whole box. */
@@ -61,6 +180,14 @@ export function PlacementBox({
61
180
  geometry.translate(0, 0.01, 0)
62
181
  return geometry
63
182
  }, [width, depth])
183
+ const measurementGuideGeometries = useMemo(() => {
184
+ const points = getMeasurementGuidePoints(width, height, depth)
185
+ return {
186
+ depth: createLineGeometry(points.depth),
187
+ height: createLineGeometry(points.height),
188
+ width: createLineGeometry(points.width),
189
+ }
190
+ }, [width, height, depth])
64
191
 
65
192
  const edgeMaterial = useMemo(
66
193
  () => new LineBasicNodeMaterial({ linewidth: 3, depthTest: false, depthWrite: false }),
@@ -77,6 +204,16 @@ export function PlacementBox({
77
204
  material.opacityNode = smoothstep(0, 0.7, distance(uv(), vec2(0.5, 0.5))).mul(0.6)
78
205
  return material
79
206
  }, [])
207
+ const measurementMaterial = useMemo(
208
+ () =>
209
+ new LineBasicNodeMaterial({
210
+ color: MEASUREMENT_COLOR,
211
+ linewidth: 2,
212
+ depthTest: false,
213
+ depthWrite: false,
214
+ }),
215
+ [],
216
+ )
80
217
 
81
218
  useEffect(() => {
82
219
  const color = valid ? VALID_COLOR : INVALID_COLOR
@@ -90,6 +227,14 @@ export function PlacementBox({
90
227
  },
91
228
  [edgeGeometry],
92
229
  )
230
+ useEffect(
231
+ () => () => {
232
+ measurementGuideGeometries.depth.dispose()
233
+ measurementGuideGeometries.height.dispose()
234
+ measurementGuideGeometries.width.dispose()
235
+ },
236
+ [measurementGuideGeometries],
237
+ )
93
238
  useEffect(
94
239
  () => () => {
95
240
  basePlaneGeometry.dispose()
@@ -100,10 +245,46 @@ export function PlacementBox({
100
245
  () => () => {
101
246
  edgeMaterial.dispose()
102
247
  basePlaneMaterial.dispose()
248
+ measurementMaterial.dispose()
103
249
  },
104
- [edgeMaterial, basePlaneMaterial],
250
+ [edgeMaterial, basePlaneMaterial, measurementMaterial],
105
251
  )
106
252
 
253
+ const measurementContent = measurements ? (
254
+ <>
255
+ <lineSegments
256
+ geometry={measurementGuideGeometries.width}
257
+ layers={EDITOR_LAYER}
258
+ material={measurementMaterial}
259
+ renderOrder={998}
260
+ />
261
+ <lineSegments
262
+ geometry={measurementGuideGeometries.depth}
263
+ layers={EDITOR_LAYER}
264
+ material={measurementMaterial}
265
+ renderOrder={998}
266
+ />
267
+ <lineSegments
268
+ geometry={measurementGuideGeometries.height}
269
+ layers={EDITOR_LAYER}
270
+ material={measurementMaterial}
271
+ renderOrder={998}
272
+ />
273
+ <MeasurementPill
274
+ label={formatLinearMeasurement(width, measurements.unit)}
275
+ position={[0, 0.04, depth / 2 + 0.24]}
276
+ />
277
+ <MeasurementPill
278
+ label={formatLinearMeasurement(depth, measurements.unit)}
279
+ position={[width / 2 + 0.24, 0.04, 0]}
280
+ />
281
+ <MeasurementPill
282
+ label={formatLinearMeasurement(height, measurements.unit)}
283
+ position={[-width / 2 - 0.24, height / 2, -depth / 2]}
284
+ />
285
+ </>
286
+ ) : null
287
+
107
288
  return (
108
289
  <group position={position} rotation={[0, rotationY, 0]}>
109
290
  <lineSegments
@@ -112,6 +293,7 @@ export function PlacementBox({
112
293
  material={edgeMaterial}
113
294
  renderOrder={999}
114
295
  />
296
+ {measurementContent}
115
297
  <mesh
116
298
  geometry={basePlaneGeometry}
117
299
  layers={EDITOR_LAYER}