@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
@@ -0,0 +1,523 @@
1
+ 'use client'
2
+
3
+ import { type Cursor, emitter } from '@pascal-app/core'
4
+ import type { ThreeEvent } from '@react-three/fiber'
5
+ import { type ReactNode, useEffect, useMemo, useRef } from 'react'
6
+ import {
7
+ BoxGeometry,
8
+ type BufferGeometry,
9
+ CircleGeometry,
10
+ Color,
11
+ CylinderGeometry,
12
+ DoubleSide,
13
+ ExtrudeGeometry,
14
+ type Group,
15
+ Shape,
16
+ TorusGeometry,
17
+ } from 'three'
18
+ import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
19
+ import { MeshBasicNodeMaterial } from 'three/webgpu'
20
+ import { EDITOR_LAYER } from '../../../lib/constants'
21
+
22
+ export const ARROW_SCALE = 0.65
23
+ export const ARROW_COLOR = '#8381ed'
24
+ export const ARROW_HOVER_COLOR = '#a5b4fc'
25
+ export const NO_RAYCAST = () => null
26
+ export const HIT_AREA_MARGIN = 0.035
27
+
28
+ const HIT_AREA_RENDER_ORDER = 1011
29
+ const HIT_AREA_THICKNESS = 0.08
30
+ const CHEVRON_MIN_X = -0.2
31
+ const CHEVRON_MAX_X = 0.22
32
+ const CHEVRON_HALF_WIDTH = 0.12
33
+ const CHEVRON_NOTCH_X = -0.04
34
+ const CHEVRON_SHAFT_HALF_WIDTH = 0.035
35
+ const CHEVRON_DEPTH = 0.08
36
+ const CHEVRON_BEVEL_THICKNESS = 0.035
37
+ const CHEVRON_BEVEL_SIZE = 0.03
38
+ const CHEVRON_BEVEL_SEGMENTS = 10
39
+ const MOVE_CROSS_HALF_LENGTH = 0.36
40
+ const MOVE_CROSS_SHAFT_HALF_WIDTH = 0.03
41
+ const MOVE_CROSS_HEAD_HALF_WIDTH = 0.12
42
+ const MOVE_CROSS_HEAD_INSET = 0.2
43
+ const MOVE_CROSS_DEPTH = 0.06
44
+ const MOVE_CROSS_BEVEL_THICKNESS = 0.018
45
+ const MOVE_CROSS_BEVEL_SIZE = 0.012
46
+ const MOVE_CROSS_BEVEL_SEGMENTS = 6
47
+ const ROTATE_HANDLE_RADIUS = 0.2
48
+ const ROTATE_HANDLE_HALF_SWEEP = Math.PI / 3
49
+ const ROTATE_RIBBON_HALF_WIDTH = 0.02
50
+ const ROTATE_HEAD_HALF_WIDTH = 0.045
51
+ const TRACKER_CUBE_SIZE = 0.16
52
+ export const CORNER_HEX_RADIUS = 0.16
53
+
54
+ export type HandleArrowShape = 'chevron' | 'cross' | 'curved-arrow' | 'tracker' | 'corner-picker'
55
+ export type HandleArrowInputShape = HandleArrowShape | 'arrow' | 'move-cross'
56
+
57
+ export type HandleArrowPlacement = {
58
+ position: readonly [number, number, number]
59
+ rotation?: readonly [number, number, number]
60
+ baseScale: number
61
+ }
62
+
63
+ type PointerHandler = (event: ThreeEvent<PointerEvent>) => void
64
+
65
+ export type HandleArrowProps = {
66
+ shape: HandleArrowInputShape
67
+ placement: HandleArrowPlacement
68
+ hover: boolean
69
+ cursor: Cursor
70
+ onHoverChange: (hovered: boolean) => void
71
+ onPointerDown: PointerHandler
72
+ activeCursor?: Cursor
73
+ children?: ReactNode
74
+ hoverScale?: number
75
+ indicatorRotation?: readonly [number, number, number]
76
+ onPointerEnter?: PointerHandler
77
+ onPointerLeave?: PointerHandler
78
+ }
79
+
80
+ function normalizeHandleArrowShape(shape: HandleArrowInputShape, cursor: Cursor): HandleArrowShape {
81
+ if (shape === 'arrow') return 'chevron'
82
+ if (shape === 'move-cross') return 'cross'
83
+ if (shape === 'chevron' && cursor === 'move') return 'cross'
84
+ return shape
85
+ }
86
+
87
+ // Two-headed curved-arrow silhouette for whole-node rotation handles
88
+ // (today: the elevator's corner rotate gizmo). Symmetric arc centred on
89
+ // +X with sweeps to +/-halfSweep, arrowhead wings + tangentially-extended
90
+ // tips at each end. Drawn in 2D then extruded and rotated to lie in the
91
+ // XZ plane - same final-orientation contract as the chevron, so the
92
+ // outer rotation Y and inner-rotation chain in the renderer are reused
93
+ // unchanged.
94
+ export function createRotateArrowHandleGeometry() {
95
+ const R = 0.2
96
+ const ribbonHalfWidth = 0.02
97
+ const halfSweep = Math.PI / 3
98
+ const headHalfWidth = 0.045
99
+ const headOvershoot = 0.075
100
+ const rIn = R - ribbonHalfWidth
101
+ const rOut = R + ribbonHalfWidth
102
+ const a1 = halfSweep
103
+ const a2 = -halfSweep
104
+
105
+ const tip1: [number, number] = [
106
+ R * Math.cos(a1) - headOvershoot * Math.sin(a1),
107
+ R * Math.sin(a1) + headOvershoot * Math.cos(a1),
108
+ ]
109
+ const tip2: [number, number] = [
110
+ R * Math.cos(a2) + headOvershoot * Math.sin(a2),
111
+ R * Math.sin(a2) - headOvershoot * Math.cos(a2),
112
+ ]
113
+ const innerWing1: [number, number] = [
114
+ (rIn - headHalfWidth) * Math.cos(a1),
115
+ (rIn - headHalfWidth) * Math.sin(a1),
116
+ ]
117
+ const outerWing1: [number, number] = [
118
+ (rOut + headHalfWidth) * Math.cos(a1),
119
+ (rOut + headHalfWidth) * Math.sin(a1),
120
+ ]
121
+ const innerWing2: [number, number] = [
122
+ (rIn - headHalfWidth) * Math.cos(a2),
123
+ (rIn - headHalfWidth) * Math.sin(a2),
124
+ ]
125
+ const outerWing2: [number, number] = [
126
+ (rOut + headHalfWidth) * Math.cos(a2),
127
+ (rOut + headHalfWidth) * Math.sin(a2),
128
+ ]
129
+ const innerCorner1: [number, number] = [rIn * Math.cos(a1), rIn * Math.sin(a1)]
130
+ const outerCorner1: [number, number] = [rOut * Math.cos(a1), rOut * Math.sin(a1)]
131
+ const innerCorner2: [number, number] = [rIn * Math.cos(a2), rIn * Math.sin(a2)]
132
+ const outerCorner2: [number, number] = [rOut * Math.cos(a2), rOut * Math.sin(a2)]
133
+
134
+ const shape = new Shape()
135
+ shape.moveTo(innerCorner1[0], innerCorner1[1])
136
+ shape.lineTo(innerWing1[0], innerWing1[1])
137
+ shape.lineTo(tip1[0], tip1[1])
138
+ shape.lineTo(outerWing1[0], outerWing1[1])
139
+ shape.lineTo(outerCorner1[0], outerCorner1[1])
140
+ shape.absarc(0, 0, rOut, a1, a2, true)
141
+ shape.lineTo(outerWing2[0], outerWing2[1])
142
+ shape.lineTo(tip2[0], tip2[1])
143
+ shape.lineTo(innerWing2[0], innerWing2[1])
144
+ shape.lineTo(innerCorner2[0], innerCorner2[1])
145
+ shape.absarc(0, 0, rIn, a2, a1, false)
146
+ shape.closePath()
147
+
148
+ const geometry = new ExtrudeGeometry(shape, {
149
+ depth: 0.06,
150
+ bevelEnabled: true,
151
+ bevelThickness: 0.018,
152
+ bevelSize: 0.012,
153
+ bevelOffset: 0,
154
+ bevelSegments: 6,
155
+ curveSegments: 24,
156
+ steps: 1,
157
+ })
158
+ geometry.translate(0, 0, -0.03)
159
+ geometry.rotateX(-Math.PI / 2)
160
+ geometry.computeVertexNormals()
161
+ geometry.computeBoundingSphere()
162
+ return geometry
163
+ }
164
+
165
+ // Reused chevron+shaft silhouette. The chevron points along +X by default;
166
+ // callers rotate it around Y for Z-axis handles and into a vertical frame for
167
+ // Y-axis handles.
168
+ export function createArrowHandleGeometry() {
169
+ const shape = new Shape()
170
+ shape.moveTo(CHEVRON_MAX_X, 0)
171
+ shape.lineTo(CHEVRON_NOTCH_X, CHEVRON_HALF_WIDTH)
172
+ shape.lineTo(CHEVRON_NOTCH_X, CHEVRON_SHAFT_HALF_WIDTH)
173
+ shape.lineTo(CHEVRON_MIN_X, CHEVRON_SHAFT_HALF_WIDTH)
174
+ shape.lineTo(CHEVRON_MIN_X, -CHEVRON_SHAFT_HALF_WIDTH)
175
+ shape.lineTo(CHEVRON_NOTCH_X, -CHEVRON_SHAFT_HALF_WIDTH)
176
+ shape.lineTo(CHEVRON_NOTCH_X, -CHEVRON_HALF_WIDTH)
177
+ shape.lineTo(CHEVRON_MAX_X, 0)
178
+ const geometry = new ExtrudeGeometry(shape, {
179
+ depth: CHEVRON_DEPTH,
180
+ bevelEnabled: true,
181
+ bevelThickness: CHEVRON_BEVEL_THICKNESS,
182
+ bevelSize: CHEVRON_BEVEL_SIZE,
183
+ bevelOffset: 0,
184
+ bevelSegments: CHEVRON_BEVEL_SEGMENTS,
185
+ curveSegments: 16,
186
+ steps: 1,
187
+ })
188
+ geometry.translate(0, 0, -CHEVRON_DEPTH / 2)
189
+ geometry.rotateX(-Math.PI / 2)
190
+ geometry.computeVertexNormals()
191
+ geometry.computeBoundingSphere()
192
+ return geometry
193
+ }
194
+
195
+ function createDoubleArrowShape(): Shape {
196
+ const shape = new Shape()
197
+ shape.moveTo(MOVE_CROSS_HALF_LENGTH, 0)
198
+ shape.lineTo(MOVE_CROSS_HEAD_INSET, MOVE_CROSS_HEAD_HALF_WIDTH)
199
+ shape.lineTo(MOVE_CROSS_HEAD_INSET, MOVE_CROSS_SHAFT_HALF_WIDTH)
200
+ shape.lineTo(-MOVE_CROSS_HEAD_INSET, MOVE_CROSS_SHAFT_HALF_WIDTH)
201
+ shape.lineTo(-MOVE_CROSS_HEAD_INSET, MOVE_CROSS_HEAD_HALF_WIDTH)
202
+ shape.lineTo(-MOVE_CROSS_HALF_LENGTH, 0)
203
+ shape.lineTo(-MOVE_CROSS_HEAD_INSET, -MOVE_CROSS_HEAD_HALF_WIDTH)
204
+ shape.lineTo(-MOVE_CROSS_HEAD_INSET, -MOVE_CROSS_SHAFT_HALF_WIDTH)
205
+ shape.lineTo(MOVE_CROSS_HEAD_INSET, -MOVE_CROSS_SHAFT_HALF_WIDTH)
206
+ shape.lineTo(MOVE_CROSS_HEAD_INSET, -MOVE_CROSS_HEAD_HALF_WIDTH)
207
+ shape.closePath()
208
+ return shape
209
+ }
210
+
211
+ export function createMoveCrossHandleGeometry() {
212
+ const shape = createDoubleArrowShape()
213
+ const extrudeOpts = {
214
+ depth: MOVE_CROSS_DEPTH,
215
+ bevelEnabled: true,
216
+ bevelThickness: MOVE_CROSS_BEVEL_THICKNESS,
217
+ bevelSize: MOVE_CROSS_BEVEL_SIZE,
218
+ bevelOffset: 0,
219
+ bevelSegments: MOVE_CROSS_BEVEL_SEGMENTS,
220
+ curveSegments: 8,
221
+ steps: 1,
222
+ }
223
+ const armX = new ExtrudeGeometry(shape, extrudeOpts)
224
+ armX.translate(0, 0, -MOVE_CROSS_DEPTH / 2)
225
+ armX.rotateX(-Math.PI / 2)
226
+ const armZ = armX.clone()
227
+ armZ.rotateY(Math.PI / 2)
228
+ const merged = mergeGeometries([armX, armZ], false)
229
+ if (!merged) {
230
+ armZ.dispose()
231
+ armX.computeVertexNormals()
232
+ armX.computeBoundingSphere()
233
+ return armX
234
+ }
235
+ armX.dispose()
236
+ armZ.dispose()
237
+ merged.computeVertexNormals()
238
+ merged.computeBoundingSphere()
239
+ return merged
240
+ }
241
+
242
+ export function createArrowHitAreaGeometry() {
243
+ const length = CHEVRON_MAX_X - CHEVRON_MIN_X + HIT_AREA_MARGIN * 2
244
+ const centerX = (CHEVRON_MIN_X + CHEVRON_MAX_X) / 2
245
+ const geometry = new CylinderGeometry(
246
+ CHEVRON_HALF_WIDTH + HIT_AREA_MARGIN,
247
+ CHEVRON_HALF_WIDTH + HIT_AREA_MARGIN,
248
+ length,
249
+ 16,
250
+ )
251
+ geometry.rotateZ(-Math.PI / 2)
252
+ geometry.translate(centerX, 0, 0)
253
+ geometry.computeBoundingSphere()
254
+ return geometry
255
+ }
256
+
257
+ function createMoveCrossHitAreaGeometry() {
258
+ const geometry = new CylinderGeometry(
259
+ MOVE_CROSS_HALF_LENGTH + HIT_AREA_MARGIN,
260
+ MOVE_CROSS_HALF_LENGTH + HIT_AREA_MARGIN,
261
+ HIT_AREA_THICKNESS,
262
+ 32,
263
+ )
264
+ geometry.computeBoundingSphere()
265
+ return geometry
266
+ }
267
+
268
+ export function createRotateArrowHitAreaGeometry() {
269
+ const halfSweep = ROTATE_HANDLE_HALF_SWEEP + HIT_AREA_MARGIN / ROTATE_HANDLE_RADIUS
270
+ const geometry = new TorusGeometry(
271
+ ROTATE_HANDLE_RADIUS,
272
+ ROTATE_RIBBON_HALF_WIDTH + ROTATE_HEAD_HALF_WIDTH + HIT_AREA_MARGIN,
273
+ 10,
274
+ 64,
275
+ halfSweep * 2,
276
+ )
277
+ geometry.rotateZ(-halfSweep)
278
+ geometry.rotateX(-Math.PI / 2)
279
+ geometry.computeBoundingSphere()
280
+ return geometry
281
+ }
282
+
283
+ function createTrackerHitAreaGeometry() {
284
+ const size = TRACKER_CUBE_SIZE + HIT_AREA_MARGIN * 2
285
+ const geometry = new BoxGeometry(size, size, size)
286
+ geometry.computeBoundingSphere()
287
+ return geometry
288
+ }
289
+
290
+ export function createEndpointHitAreaGeometry(radius: number) {
291
+ const geometry = new CylinderGeometry(
292
+ radius + HIT_AREA_MARGIN,
293
+ radius + HIT_AREA_MARGIN,
294
+ HIT_AREA_THICKNESS,
295
+ 24,
296
+ )
297
+ geometry.rotateX(Math.PI / 2)
298
+ geometry.computeBoundingSphere()
299
+ return geometry
300
+ }
301
+
302
+ function createHandleArrowGeometry(shape: HandleArrowShape) {
303
+ if (shape === 'chevron') return createArrowHandleGeometry()
304
+ if (shape === 'cross') return createMoveCrossHandleGeometry()
305
+ if (shape === 'curved-arrow') return createRotateArrowHandleGeometry()
306
+ if (shape === 'tracker') {
307
+ const geometry = new BoxGeometry(TRACKER_CUBE_SIZE, TRACKER_CUBE_SIZE, TRACKER_CUBE_SIZE)
308
+ geometry.computeBoundingSphere()
309
+ return geometry
310
+ }
311
+ const geometry = new CircleGeometry(CORNER_HEX_RADIUS, 6)
312
+ geometry.computeBoundingSphere()
313
+ return geometry
314
+ }
315
+
316
+ function createHandleArrowHitGeometry(shape: HandleArrowShape) {
317
+ if (shape === 'chevron') return createArrowHitAreaGeometry()
318
+ if (shape === 'cross') return createMoveCrossHitAreaGeometry()
319
+ if (shape === 'curved-arrow') return createRotateArrowHitAreaGeometry()
320
+ if (shape === 'tracker') return createTrackerHitAreaGeometry()
321
+ const geometry = new CircleGeometry(CORNER_HEX_RADIUS, 6)
322
+ geometry.computeBoundingSphere()
323
+ return geometry
324
+ }
325
+
326
+ let sharedHitAreaMaterial: MeshBasicNodeMaterial | null = null
327
+ let sharedHitAreaMaterialRefs = 0
328
+
329
+ function createInvisibleHitAreaMaterial() {
330
+ return new MeshBasicNodeMaterial({
331
+ color: new Color('#ffffff'),
332
+ colorWrite: false,
333
+ depthTest: false,
334
+ depthWrite: false,
335
+ opacity: 0,
336
+ side: DoubleSide,
337
+ transparent: true,
338
+ })
339
+ }
340
+
341
+ export function useInvisibleHitAreaMaterial(): MeshBasicNodeMaterial {
342
+ const materialRef = useRef<MeshBasicNodeMaterial | null>(null)
343
+ if (!materialRef.current) {
344
+ sharedHitAreaMaterial ??= createInvisibleHitAreaMaterial()
345
+ materialRef.current = sharedHitAreaMaterial
346
+ }
347
+ useEffect(() => {
348
+ sharedHitAreaMaterialRefs += 1
349
+ return () => {
350
+ sharedHitAreaMaterialRefs -= 1
351
+ if (sharedHitAreaMaterialRefs <= 0 && sharedHitAreaMaterial) {
352
+ sharedHitAreaMaterial.dispose()
353
+ sharedHitAreaMaterial = null
354
+ sharedHitAreaMaterialRefs = 0
355
+ }
356
+ }
357
+ }, [])
358
+ return materialRef.current
359
+ }
360
+
361
+ export function InvisibleHandleHitArea({
362
+ geometry,
363
+ material,
364
+ onPointerDown,
365
+ onPointerEnter,
366
+ onPointerLeave,
367
+ scale,
368
+ }: {
369
+ geometry: BufferGeometry
370
+ material: MeshBasicNodeMaterial
371
+ onPointerDown: PointerHandler
372
+ onPointerEnter: PointerHandler
373
+ onPointerLeave: PointerHandler
374
+ scale: number
375
+ }) {
376
+ return (
377
+ <mesh
378
+ frustumCulled={false}
379
+ geometry={geometry}
380
+ layers={EDITOR_LAYER}
381
+ material={material}
382
+ onPointerDown={onPointerDown}
383
+ onPointerEnter={onPointerEnter}
384
+ onPointerLeave={onPointerLeave}
385
+ renderOrder={HIT_AREA_RENDER_ORDER}
386
+ scale={scale}
387
+ />
388
+ )
389
+ }
390
+
391
+ export function useArrowMaterial(): MeshBasicNodeMaterial {
392
+ return useMemo(
393
+ () =>
394
+ new MeshBasicNodeMaterial({
395
+ color: new Color(ARROW_COLOR),
396
+ side: DoubleSide,
397
+ // `depthTest: false` keeps the chevron drawing on top of any
398
+ // geometry under it; `depthWrite: true` puts the chevron's depth
399
+ // into the scenePass buffer so the ink-edge shader's depth
400
+ // Laplacian fires on its silhouette from every angle. Without
401
+ // depthWrite, only the normal-discontinuity branch can detect
402
+ // the chevron, and that signal collapses when the arrow's faces
403
+ // happen to align with whatever sits behind them in screen space
404
+ // - which is why the lines used to drop out depending on the view.
405
+ depthTest: false,
406
+ depthWrite: true,
407
+ transparent: true,
408
+ opacity: 1,
409
+ }),
410
+ [],
411
+ )
412
+ }
413
+
414
+ function useHandleArrowMaterial(shape: HandleArrowShape): MeshBasicNodeMaterial {
415
+ return useMemo(
416
+ () =>
417
+ new MeshBasicNodeMaterial({
418
+ color: new Color(ARROW_COLOR),
419
+ side: DoubleSide,
420
+ transparent: true,
421
+ opacity: shape === 'corner-picker' ? 0.95 : 1,
422
+ depthTest: false,
423
+ depthWrite: shape !== 'corner-picker',
424
+ }),
425
+ [shape],
426
+ )
427
+ }
428
+
429
+ function indicatorRenderOrder(shape: HandleArrowShape) {
430
+ return shape === 'tracker' || shape === 'corner-picker' ? 1003 : 1010
431
+ }
432
+
433
+ export function HandleArrow({
434
+ shape,
435
+ placement,
436
+ hover,
437
+ cursor,
438
+ activeCursor,
439
+ children,
440
+ hoverScale = 1.12,
441
+ indicatorRotation,
442
+ onHoverChange,
443
+ onPointerDown,
444
+ onPointerEnter,
445
+ onPointerLeave,
446
+ }: HandleArrowProps) {
447
+ const visualShape = normalizeHandleArrowShape(shape, cursor)
448
+ const geometry = useMemo(() => createHandleArrowGeometry(visualShape), [visualShape])
449
+ const hitGeometry = useMemo(() => createHandleArrowHitGeometry(visualShape), [visualShape])
450
+ const indicatorMaterial = useHandleArrowMaterial(visualShape)
451
+ const hitMaterial = useInvisibleHitAreaMaterial()
452
+ const rootRef = useRef<Group>(null)
453
+ const rotation: [number, number, number] = placement.rotation
454
+ ? [placement.rotation[0], placement.rotation[1], placement.rotation[2]]
455
+ : [0, 0, 0]
456
+ const localRotation: [number, number, number] = indicatorRotation
457
+ ? [indicatorRotation[0], indicatorRotation[1], indicatorRotation[2]]
458
+ : [0, 0, 0]
459
+ const scale = (hover ? hoverScale : 1) * placement.baseScale
460
+ const hitScale = visualShape === 'corner-picker' ? scale : placement.baseScale
461
+
462
+ useEffect(() => {
463
+ indicatorMaterial.color.set(hover ? ARROW_HOVER_COLOR : ARROW_COLOR)
464
+ }, [indicatorMaterial, hover])
465
+ useEffect(() => {
466
+ const hideForCapture = () => {
467
+ if (rootRef.current) rootRef.current.visible = false
468
+ }
469
+ const restoreAfterCapture = () => {
470
+ if (rootRef.current) rootRef.current.visible = true
471
+ }
472
+ emitter.on('thumbnail:before-capture', hideForCapture)
473
+ emitter.on('thumbnail:after-capture', restoreAfterCapture)
474
+ return () => {
475
+ emitter.off('thumbnail:before-capture', hideForCapture)
476
+ emitter.off('thumbnail:after-capture', restoreAfterCapture)
477
+ }
478
+ }, [])
479
+ useEffect(() => () => geometry.dispose(), [geometry])
480
+ useEffect(() => () => hitGeometry.dispose(), [hitGeometry])
481
+ useEffect(() => () => indicatorMaterial.dispose(), [indicatorMaterial])
482
+
483
+ const handleEnter: PointerHandler = (event) => {
484
+ event.stopPropagation()
485
+ onHoverChange(true)
486
+ if (!activeCursor || document.body.style.cursor !== activeCursor) {
487
+ document.body.style.cursor = cursor
488
+ }
489
+ onPointerEnter?.(event)
490
+ }
491
+ const handleLeave: PointerHandler = (event) => {
492
+ event.stopPropagation()
493
+ onHoverChange(false)
494
+ if (document.body.style.cursor === cursor) {
495
+ document.body.style.cursor = ''
496
+ }
497
+ onPointerLeave?.(event)
498
+ }
499
+
500
+ return (
501
+ <group position={placement.position} ref={rootRef} rotation={rotation}>
502
+ {children}
503
+ <group rotation={localRotation}>
504
+ <InvisibleHandleHitArea
505
+ geometry={hitGeometry}
506
+ material={hitMaterial}
507
+ onPointerDown={onPointerDown}
508
+ onPointerEnter={handleEnter}
509
+ onPointerLeave={handleLeave}
510
+ scale={hitScale}
511
+ />
512
+ <mesh
513
+ frustumCulled={false}
514
+ geometry={geometry}
515
+ material={indicatorMaterial}
516
+ raycast={NO_RAYCAST}
517
+ renderOrder={indicatorRenderOrder(visualShape)}
518
+ scale={scale}
519
+ />
520
+ </group>
521
+ </group>
522
+ )
523
+ }