@pascal-app/editor 0.8.0 → 0.9.1

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 (223) hide show
  1. package/package.json +8 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
  3. package/src/components/editor/custom-camera-controls.tsx +754 -25
  4. package/src/components/editor/editor-layout-mobile.tsx +3 -3
  5. package/src/components/editor/editor-layout-v2.tsx +58 -51
  6. package/src/components/editor/export-manager.tsx +32 -3
  7. package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
  8. package/src/components/editor/first-person/build-collider-world.ts +201 -83
  9. package/src/components/editor/first-person/bvh-ecctrl.tsx +8 -8
  10. package/src/components/editor/first-person-controls.tsx +825 -20
  11. package/src/components/editor/floating-action-menu.tsx +287 -196
  12. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  13. package/src/components/editor/floorplan-panel.tsx +5829 -12005
  14. package/src/components/editor/grid.tsx +51 -17
  15. package/src/components/editor/group-move-handle.tsx +316 -0
  16. package/src/components/editor/group-rotate-handle.tsx +382 -0
  17. package/src/components/editor/group-transform-shared.test.ts +205 -0
  18. package/src/components/editor/group-transform-shared.ts +256 -0
  19. package/src/components/editor/handles/handle-arrow.tsx +523 -0
  20. package/src/components/editor/handles/use-handle-drag.ts +213 -0
  21. package/src/components/editor/index.tsx +178 -149
  22. package/src/components/editor/measurement-pill.tsx +75 -0
  23. package/src/components/editor/node-arrow-handles.tsx +1453 -0
  24. package/src/components/editor/selection-manager.tsx +433 -156
  25. package/src/components/editor/site-edge-labels.tsx +44 -20
  26. package/src/components/editor/slab-hole-highlights.tsx +466 -0
  27. package/src/components/editor/snapshot-capture-overlay.tsx +136 -62
  28. package/src/components/editor/thumbnail-generator.tsx +9 -3
  29. package/src/components/editor/use-floorplan-background-placement.ts +119 -32
  30. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  31. package/src/components/editor/use-floorplan-scene-data.ts +14 -2
  32. package/src/components/editor/wall-measurement-label.tsx +11 -6
  33. package/src/components/editor/wall-move-side-handles.tsx +855 -0
  34. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  35. package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
  36. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
  37. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  38. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  39. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
  40. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
  41. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  42. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
  43. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  44. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  45. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
  46. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
  47. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
  48. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
  49. package/src/components/editor-2d/svg-paths.ts +10 -2
  50. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +26 -3
  51. package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
  52. package/src/components/systems/roof/roof-edit-system.tsx +86 -12
  53. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  54. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  55. package/src/components/tools/elevator/elevator-tool.tsx +349 -0
  56. package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
  57. package/src/components/tools/fence/fence-drafting.ts +64 -22
  58. package/src/components/tools/item/move-tool.tsx +41 -105
  59. package/src/components/tools/item/placement-math.test.ts +10 -0
  60. package/src/components/tools/item/placement-math.ts +31 -5
  61. package/src/components/tools/item/placement-strategies.ts +190 -15
  62. package/src/components/tools/item/placement-types.ts +10 -1
  63. package/src/components/tools/item/use-draft-node.ts +20 -1
  64. package/src/components/tools/item/use-placement-coordinator.tsx +694 -191
  65. package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
  66. package/src/components/tools/roof/roof-tool.tsx +108 -12
  67. package/src/components/tools/select/box-select-state.ts +77 -0
  68. package/src/components/tools/select/box-select-tool.tsx +324 -538
  69. package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
  70. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  71. package/src/components/tools/select/select-candidates.test.ts +109 -0
  72. package/src/components/tools/select/select-candidates.ts +85 -0
  73. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  74. package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
  75. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  76. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  77. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  78. package/src/components/tools/shared/placement-box.tsx +123 -0
  79. package/src/components/tools/shared/polygon-editor.tsx +617 -167
  80. package/src/components/tools/shared/segment-angle.ts +56 -0
  81. package/src/components/tools/site/site-boundary-editor.tsx +464 -12
  82. package/src/components/tools/site/site-flag-model.ts +18 -0
  83. package/src/components/tools/stair/stair-defaults.ts +1 -1
  84. package/src/components/tools/stair/stair-tool.tsx +269 -35
  85. package/src/components/tools/tool-manager.tsx +172 -56
  86. package/src/components/tools/wall/wall-drafting.ts +98 -98
  87. package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
  88. package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
  89. package/src/components/tools/zone/zone-tool.tsx +16 -6
  90. package/src/components/ui/action-menu/action-button.tsx +21 -1
  91. package/src/components/ui/action-menu/control-modes.tsx +5 -82
  92. package/src/components/ui/action-menu/index.tsx +1 -113
  93. package/src/components/ui/action-menu/structure-tools.tsx +13 -88
  94. package/src/components/ui/action-menu/view-toggles.tsx +243 -6
  95. package/src/components/ui/command-palette/editor-commands.tsx +18 -13
  96. package/src/components/ui/command-palette/index.tsx +2 -1
  97. package/src/components/ui/controls/material-paint-panel.tsx +83 -0
  98. package/src/components/ui/controls/material-picker.tsx +11 -32
  99. package/src/components/ui/controls/metric-control.tsx +33 -11
  100. package/src/components/ui/floating-level-selector.tsx +47 -10
  101. package/src/components/ui/helpers/helper-manager.tsx +16 -17
  102. package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
  103. package/src/components/ui/item-catalog/catalog-items.tsx +1 -0
  104. package/src/components/ui/item-catalog/item-catalog.tsx +3 -0
  105. package/src/components/ui/level-duplicate-dialog.tsx +2 -1
  106. package/src/components/ui/panels/node-display.ts +1 -0
  107. package/src/components/ui/panels/panel-manager.tsx +35 -47
  108. package/src/components/ui/panels/panel-wrapper.tsx +217 -12
  109. package/src/components/ui/panels/parametric-inspector.tsx +469 -0
  110. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  111. package/src/components/ui/scene-loader.tsx +1 -3
  112. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  113. package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
  114. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
  115. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
  116. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
  117. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  118. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
  119. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
  120. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
  121. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  122. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
  123. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
  124. package/src/components/ui/sidebar/panels/site-panel/index.tsx +4 -6
  125. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  126. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
  127. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
  128. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
  129. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
  130. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
  131. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
  132. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -36
  133. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
  134. package/src/components/ui/sidebar/tab-bar.tsx +61 -1
  135. package/src/components/viewer-overlay.tsx +169 -52
  136. package/src/components/viewer-zone-system.tsx +1 -1
  137. package/src/hooks/use-auto-save.ts +10 -3
  138. package/src/hooks/use-ceiling-events.ts +175 -0
  139. package/src/hooks/use-drag-action.ts +124 -0
  140. package/src/hooks/use-keyboard.ts +115 -62
  141. package/src/hooks/use-selection.ts +64 -0
  142. package/src/index.tsx +237 -5
  143. package/src/lib/constants.ts +6 -2
  144. package/src/lib/editor-api.ts +62 -0
  145. package/src/lib/elevator-support.ts +96 -0
  146. package/src/lib/floorplan/apply-alignment.ts +111 -0
  147. package/src/lib/floorplan/index.ts +7 -0
  148. package/src/lib/floorplan/selection-tool.ts +40 -0
  149. package/src/lib/floorplan/stairs.ts +12 -1
  150. package/src/lib/fresh-planar-placement.test.ts +102 -0
  151. package/src/lib/fresh-planar-placement.ts +63 -0
  152. package/src/lib/history.ts +2 -1
  153. package/src/lib/level-duplication.ts +54 -42
  154. package/src/lib/level-name.ts +11 -0
  155. package/src/lib/material-paint.ts +211 -19
  156. package/src/lib/placement-metadata.ts +29 -0
  157. package/src/lib/planar-cursor-placement.test.ts +43 -0
  158. package/src/lib/planar-cursor-placement.ts +42 -0
  159. package/src/lib/roof-duplication.ts +1 -1
  160. package/src/lib/scene-clipboard.ts +267 -0
  161. package/src/lib/scene.ts +13 -4
  162. package/src/lib/sfx-bus.ts +23 -1
  163. package/src/lib/sfx-player.ts +70 -22
  164. package/src/lib/site-boundary.ts +1 -0
  165. package/src/lib/stair-levels.test.ts +146 -0
  166. package/src/lib/stair-levels.ts +177 -0
  167. package/src/lib/world-grid-snap.ts +199 -0
  168. package/src/lib/zone-content.ts +130 -0
  169. package/src/store/use-alignment-guides.ts +21 -0
  170. package/src/store/use-editor.tsx +320 -37
  171. package/src/store/use-placement-preview.ts +31 -0
  172. package/src/store/use-wall-move-ghosts.ts +36 -0
  173. package/src/store/use-wall-snap-indicator.ts +32 -0
  174. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  175. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  176. package/src/components/tools/building/move-building-tool.tsx +0 -157
  177. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  178. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  179. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  180. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
  181. package/src/components/tools/column/column-tool.tsx +0 -97
  182. package/src/components/tools/column/move-column-tool.tsx +0 -105
  183. package/src/components/tools/door/door-math.ts +0 -110
  184. package/src/components/tools/door/door-tool.tsx +0 -324
  185. package/src/components/tools/door/move-door-tool.tsx +0 -412
  186. package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
  187. package/src/components/tools/fence/fence-tool.tsx +0 -346
  188. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
  189. package/src/components/tools/fence/move-fence-tool.tsx +0 -302
  190. package/src/components/tools/item/item-tool.tsx +0 -26
  191. package/src/components/tools/roof/move-roof-tool.tsx +0 -364
  192. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  193. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  194. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  195. package/src/components/tools/slab/slab-tool.tsx +0 -322
  196. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  197. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  198. package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
  199. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
  200. package/src/components/tools/wall/move-wall-tool.tsx +0 -358
  201. package/src/components/tools/wall/wall-tool.tsx +0 -332
  202. package/src/components/tools/window/move-window-tool.tsx +0 -447
  203. package/src/components/tools/window/window-math.ts +0 -117
  204. package/src/components/tools/window/window-tool.tsx +0 -332
  205. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  206. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  207. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  208. package/src/components/ui/panels/ceiling-panel.tsx +0 -249
  209. package/src/components/ui/panels/column-panel.tsx +0 -759
  210. package/src/components/ui/panels/door-panel.tsx +0 -1299
  211. package/src/components/ui/panels/fence-panel.tsx +0 -222
  212. package/src/components/ui/panels/item-panel.tsx +0 -306
  213. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  214. package/src/components/ui/panels/roof-panel.tsx +0 -280
  215. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  216. package/src/components/ui/panels/slab-panel.tsx +0 -250
  217. package/src/components/ui/panels/spawn-panel.tsx +0 -161
  218. package/src/components/ui/panels/stair-panel.tsx +0 -577
  219. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  220. package/src/components/ui/panels/wall-panel.tsx +0 -273
  221. package/src/components/ui/panels/window-panel.tsx +0 -968
  222. package/src/contexts/presets-context.tsx +0 -121
  223. package/src/hooks/use-contextual-tools.ts +0 -61
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/editor",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "Pascal building editor component",
5
5
  "type": "module",
6
6
  "exports": {
@@ -11,8 +11,8 @@
11
11
  "check-types": "tsc --noEmit"
12
12
  },
13
13
  "peerDependencies": {
14
- "@pascal-app/core": "^0.7.0",
15
- "@pascal-app/viewer": "^0.7.0",
14
+ "@pascal-app/core": "^0.9.1",
15
+ "@pascal-app/viewer": "^0.9.1",
16
16
  "@react-three/drei": "^10",
17
17
  "@react-three/fiber": "^9",
18
18
  "next": ">=15",
@@ -25,7 +25,7 @@
25
25
  "@dnd-kit/sortable": "^10.0.0",
26
26
  "@dnd-kit/utilities": "^3.2.2",
27
27
  "@iconify/react": "^6.0.2",
28
- "@number-flow/react": "^0.5.14",
28
+ "@number-flow/react": "^0.6.0",
29
29
  "@radix-ui/react-alert-dialog": "^1.1.15",
30
30
  "@radix-ui/react-context-menu": "^2.2.16",
31
31
  "@radix-ui/react-dialog": "^1.1.15",
@@ -38,8 +38,7 @@
38
38
  "@radix-ui/react-switch": "^1.2.6",
39
39
  "@radix-ui/react-tabs": "^1.1.13",
40
40
  "@radix-ui/react-tooltip": "^1.2.8",
41
- "@react-three/uikit-lucide": "^1.0.62",
42
- "@visual-json/react": "latest",
41
+ "@visual-json/react": "^0.4.0",
43
42
  "class-variance-authority": "^0.7.1",
44
43
  "clsx": "^2.1.1",
45
44
  "cmdk": "^1.1.1",
@@ -54,14 +53,14 @@
54
53
  "three-mesh-bvh": "~0.9.8"
55
54
  },
56
55
  "devDependencies": {
57
- "@pascal-app/core": "^0.7.0",
58
- "@pascal-app/viewer": "^0.7.0",
56
+ "@pascal-app/core": "^0.9.1",
57
+ "@pascal-app/viewer": "^0.9.1",
59
58
  "@pascal/typescript-config": "*",
60
59
  "@types/bun": "^1.3.0",
61
60
  "@types/howler": "^2.2.12",
62
61
  "@types/react": "19.2.2",
63
62
  "@types/react-dom": "19.2.2",
64
63
  "@types/three": "^0.184.0",
65
- "typescript": "6.0.2"
64
+ "typescript": "6.0.3"
66
65
  }
67
66
  }
@@ -0,0 +1,160 @@
1
+ 'use client'
2
+
3
+ import { type AlignmentGuide, sceneRegistry } from '@pascal-app/core'
4
+ import { useAlignmentGuides } from '@pascal-app/editor'
5
+ import { useViewer } from '@pascal-app/viewer'
6
+ import { Html } from '@react-three/drei'
7
+ import { useFrame } from '@react-three/fiber'
8
+ import { memo, useMemo, useRef } from 'react'
9
+ import { BoxGeometry, CircleGeometry, type Group, Vector3 } from 'three'
10
+ import { MeshBasicNodeMaterial } from 'three/webgpu'
11
+ import { EDITOR_LAYER } from '../../lib/constants'
12
+ import { formatMeasurement } from './measurement-pill'
13
+
14
+ /**
15
+ * Figma-style alignment guides for the 3D editor — the spatial twin of
16
+ * `FloorplanAlignmentGuideLayer`. Subscribes to the shared
17
+ * `useAlignmentGuides` store (published by the move / placement / wall tools
18
+ * during a drag) and draws each guide as a dashed ribbon on the floor with a
19
+ * flat circular marker at each endpoint and a distance pill.
20
+ *
21
+ * The dashes + dots lie flat on the floor plane (XZ) — a ground ribbon, like
22
+ * the design reference — so they're real 3D geometry, not screen billboards.
23
+ * Only the distance pill is screen-space (`<Html>`).
24
+ *
25
+ * Guide coordinates are XZ meters in the WORLD frame — alignment now resolves
26
+ * on the world axes (via `resolveAlignmentForActiveBuilding`) so the guides
27
+ * stay parallel to the visible world grid even when the active building is
28
+ * rotated. This layer is mounted OUTSIDE the building-local ToolManager group
29
+ * for the same reason. The whole ribbon is lifted to the active level's WORLD
30
+ * Y each frame so it lies on the floor being edited — not the building base —
31
+ * when floors are stacked.
32
+ */
33
+
34
+ const LINE_COLOR = 0x81_8c_f8 // indigo-400 — matches the editor's selection accent (box-select / wall highlights)
35
+ const PILL_COLOR = '#6366f1' // indigo-500 — same hue, darker for white-text contrast
36
+ const GUIDE_Y = 0.03 // small lift so guides read above the floor grid
37
+ const DASH_LEN = 0.18 // world-meter dash length
38
+ const DASH_GAP = 0.12 // world-meter gap between dashes
39
+ const LINE_WIDTH = 0.06 // world-meter ribbon thickness
40
+ const DOT_RADIUS = 0.11 // world-meter radius of the endpoint markers
41
+ const MAX_DASHES = 80 // cap so a very long guide can't spawn thousands of quads
42
+
43
+ // Shared resources — one violet material + unit geometries scaled per
44
+ // instance, so guide churn during a drag doesn't rebuild GPU buffers.
45
+ const guideMaterial = new MeshBasicNodeMaterial({
46
+ color: LINE_COLOR,
47
+ depthTest: false,
48
+ depthWrite: false,
49
+ toneMapped: false,
50
+ transparent: true,
51
+ })
52
+ const DASH_GEOMETRY = new BoxGeometry(1, 1, 1)
53
+ const DOT_GEOMETRY = new CircleGeometry(1, 24)
54
+
55
+ type Vec3 = [number, number, number]
56
+
57
+ export const Alignment3DGuideLayer = memo(function Alignment3DGuideLayer() {
58
+ const guides = useAlignmentGuides((s) => s.guides)
59
+ const levelId = useViewer((s) => s.selection.levelId)
60
+ const unit = useViewer((s) => s.unit)
61
+ const groupRef = useRef<Group>(null)
62
+
63
+ // Guides carry only XZ in WORLD coords; their Y has to track the active
64
+ // level's world Y so the ground ribbon lies on the floor being edited,
65
+ // not the building base. `getWorldPosition` walks the level mesh's
66
+ // parents (building / site) so it stays correct even if the building has
67
+ // a Y offset.
68
+ const worldYWork = useMemo(() => new Vector3(), [])
69
+ useFrame(() => {
70
+ const group = groupRef.current
71
+ if (!group) return
72
+ const levelMesh = levelId ? sceneRegistry.nodes.get(levelId) : null
73
+ group.position.y = levelMesh ? levelMesh.getWorldPosition(worldYWork).y : 0
74
+ })
75
+
76
+ if (guides.length === 0) return null
77
+ return (
78
+ <group ref={groupRef}>
79
+ {guides.map((guide, i) => (
80
+ <GuideLine guide={guide} key={i} unit={unit} />
81
+ ))}
82
+ </group>
83
+ )
84
+ })
85
+
86
+ function GuideLine({ guide, unit }: { guide: AlignmentGuide; unit: 'metric' | 'imperial' }) {
87
+ const { x: fx, z: fz } = guide.from
88
+ const { x: tx, z: tz } = guide.to
89
+ const distLabel = formatMeasurement(guide.distance, unit)
90
+
91
+ // Lay out the dash centres along the from→to direction. The ribbon
92
+ // stretches the dash period up if the line is long enough to exceed the
93
+ // dash cap, so it always reads as a continuous dashed line.
94
+ const { dashes, angleY } = useMemo(() => {
95
+ const dx = tx - fx
96
+ const dz = tz - fz
97
+ const length = Math.hypot(dx, dz)
98
+ const angle = -Math.atan2(dz, dx)
99
+ if (length < 1e-4) return { dashes: [] as Vec3[], angleY: angle }
100
+ const ux = dx / length
101
+ const uz = dz / length
102
+ const period = Math.max(DASH_LEN + DASH_GAP, length / MAX_DASHES)
103
+ const centres: Vec3[] = []
104
+ for (let d = period / 2; d - DASH_LEN / 2 <= length; d += period) {
105
+ centres.push([fx + ux * d, GUIDE_Y, fz + uz * d])
106
+ }
107
+ return { dashes: centres, angleY: angle }
108
+ }, [fx, fz, tx, tz])
109
+
110
+ const mid: Vec3 = [(fx + tx) / 2, GUIDE_Y, (fz + tz) / 2]
111
+
112
+ return (
113
+ <>
114
+ {dashes.map((centre, i) => (
115
+ <mesh
116
+ geometry={DASH_GEOMETRY}
117
+ key={i}
118
+ layers={EDITOR_LAYER}
119
+ material={guideMaterial}
120
+ position={centre}
121
+ renderOrder={1000}
122
+ rotation={[0, angleY, 0]}
123
+ scale={[DASH_LEN, 0.002, LINE_WIDTH]}
124
+ />
125
+ ))}
126
+ <Dot position={[fx, GUIDE_Y + 0.001, fz]} />
127
+ <Dot position={[tx, GUIDE_Y + 0.001, tz]} />
128
+ {guide.distance > 1e-4 && (
129
+ <Html
130
+ center
131
+ position={mid}
132
+ style={{ pointerEvents: 'none', userSelect: 'none' }}
133
+ zIndexRange={[20, 0]}
134
+ >
135
+ <div
136
+ className="whitespace-nowrap rounded-[3px] px-[5px] py-[2px] font-medium font-sans text-[11px] text-white"
137
+ style={{ backgroundColor: PILL_COLOR }}
138
+ >
139
+ {distLabel}
140
+ </div>
141
+ </Html>
142
+ )}
143
+ </>
144
+ )
145
+ }
146
+
147
+ /** Flat circular marker lying on the floor plane at a guide endpoint. */
148
+ function Dot({ position }: { position: Vec3 }) {
149
+ return (
150
+ <mesh
151
+ geometry={DOT_GEOMETRY}
152
+ layers={EDITOR_LAYER}
153
+ material={guideMaterial}
154
+ position={position}
155
+ renderOrder={1001}
156
+ rotation={[-Math.PI / 2, 0, 0]}
157
+ scale={[DOT_RADIUS, DOT_RADIUS, DOT_RADIUS]}
158
+ />
159
+ )
160
+ }