@pascal-app/editor 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/package.json +13 -9
  2. package/src/components/editor/bottom-sheet.tsx +149 -0
  3. package/src/components/editor/custom-camera-controls.tsx +74 -5
  4. package/src/components/editor/editor-layout-mobile.tsx +264 -0
  5. package/src/components/editor/editor-layout-v2.tsx +24 -3
  6. package/src/components/editor/first-person/build-collider-world.ts +363 -0
  7. package/src/components/editor/first-person/bvh-ecctrl.tsx +860 -0
  8. package/src/components/editor/first-person-controls.tsx +496 -143
  9. package/src/components/editor/floating-action-menu.tsx +32 -55
  10. package/src/components/editor/floorplan-background-selection.ts +113 -0
  11. package/src/components/editor/floorplan-panel.tsx +9861 -3297
  12. package/src/components/editor/index.tsx +295 -32
  13. package/src/components/editor/selection-manager.tsx +575 -13
  14. package/src/components/editor/snapshot-capture-overlay.tsx +465 -0
  15. package/src/components/editor/thumbnail-generator.tsx +56 -68
  16. package/src/components/editor/use-floorplan-background-placement.ts +257 -0
  17. package/src/components/editor/use-floorplan-hit-testing.ts +171 -0
  18. package/src/components/editor/use-floorplan-scene-data.ts +189 -0
  19. package/src/components/editor/wall-measurement-label.tsx +267 -36
  20. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +95 -0
  21. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +160 -0
  22. package/src/components/editor-2d/floorplan-hotkey-handlers.tsx +92 -0
  23. package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +124 -0
  24. package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
  25. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +202 -0
  26. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +113 -0
  27. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +474 -0
  28. package/src/components/editor-2d/svg-paths.ts +119 -0
  29. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +10 -12
  30. package/src/components/systems/roof/roof-edit-system.tsx +1 -1
  31. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  32. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
  33. package/src/components/systems/zone/zone-system.tsx +0 -0
  34. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +1 -0
  35. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +1 -0
  36. package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
  37. package/src/components/tools/ceiling/move-ceiling-tool.tsx +9 -2
  38. package/src/components/tools/column/column-tool.tsx +97 -0
  39. package/src/components/tools/column/move-column-tool.tsx +105 -0
  40. package/src/components/tools/door/door-tool.tsx +7 -0
  41. package/src/components/tools/door/move-door-tool.tsx +28 -8
  42. package/src/components/tools/fence/curve-fence-tool.tsx +4 -5
  43. package/src/components/tools/fence/fence-drafting.ts +10 -3
  44. package/src/components/tools/fence/fence-tool.tsx +160 -4
  45. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +139 -25
  46. package/src/components/tools/fence/move-fence-tool.tsx +111 -40
  47. package/src/components/tools/item/move-tool.tsx +7 -1
  48. package/src/components/tools/item/placement-math.ts +32 -5
  49. package/src/components/tools/item/placement-strategies.ts +110 -31
  50. package/src/components/tools/item/placement-types.ts +7 -0
  51. package/src/components/tools/item/use-draft-node.ts +1 -0
  52. package/src/components/tools/item/use-placement-coordinator.tsx +558 -52
  53. package/src/components/tools/roof/move-roof-tool.tsx +29 -17
  54. package/src/components/tools/select/box-select-tool.tsx +12 -17
  55. package/src/components/tools/shared/polygon-editor.tsx +153 -28
  56. package/src/components/tools/shared/segment-angle.ts +156 -0
  57. package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
  58. package/src/components/tools/slab/slab-hole-editor.tsx +1 -0
  59. package/src/components/tools/spawn/move-spawn-tool.tsx +101 -0
  60. package/src/components/tools/spawn/spawn-tool.tsx +130 -0
  61. package/src/components/tools/tool-manager.tsx +20 -5
  62. package/src/components/tools/wall/curve-wall-tool.tsx +8 -6
  63. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +131 -27
  64. package/src/components/tools/wall/move-wall-tool.tsx +6 -4
  65. package/src/components/tools/wall/wall-drafting.ts +18 -9
  66. package/src/components/tools/wall/wall-tool.tsx +136 -4
  67. package/src/components/tools/window/move-window-tool.tsx +18 -0
  68. package/src/components/tools/window/window-tool.tsx +5 -0
  69. package/src/components/tools/zone/zone-tool.tsx +20 -5
  70. package/src/components/ui/action-menu/camera-actions.tsx +37 -33
  71. package/src/components/ui/action-menu/control-modes.tsx +34 -1
  72. package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
  73. package/src/components/ui/action-menu/index.tsx +98 -59
  74. package/src/components/ui/action-menu/structure-tools.tsx +2 -0
  75. package/src/components/ui/action-menu/view-toggles.tsx +418 -41
  76. package/src/components/ui/command-palette/editor-commands.tsx +24 -5
  77. package/src/components/ui/command-palette/index.tsx +4 -255
  78. package/src/components/ui/controls/material-picker.tsx +154 -164
  79. package/src/components/ui/controls/slider-control.tsx +66 -18
  80. package/src/components/ui/floating-level-selector.tsx +286 -55
  81. package/src/components/ui/helpers/helper-manager.tsx +10 -0
  82. package/src/components/ui/item-catalog/catalog-items.tsx +2563 -1239
  83. package/src/components/ui/item-catalog/item-catalog.tsx +96 -187
  84. package/src/components/ui/level-duplicate-dialog.tsx +113 -0
  85. package/src/components/ui/panels/ceiling-panel.tsx +3 -28
  86. package/src/components/ui/panels/column-panel.tsx +759 -0
  87. package/src/components/ui/panels/door-panel.tsx +989 -290
  88. package/src/components/ui/panels/fence-panel.tsx +2 -49
  89. package/src/components/ui/panels/mobile-panel-sheet.tsx +108 -0
  90. package/src/components/ui/panels/mobile-selection-bar.tsx +100 -0
  91. package/src/components/ui/panels/node-display.ts +39 -0
  92. package/src/components/ui/panels/paint-panel.tsx +163 -0
  93. package/src/components/ui/panels/panel-manager.tsx +208 -28
  94. package/src/components/ui/panels/panel-wrapper.tsx +48 -39
  95. package/src/components/ui/panels/reference-panel.tsx +253 -5
  96. package/src/components/ui/panels/roof-panel.tsx +13 -64
  97. package/src/components/ui/panels/roof-segment-panel.tsx +0 -25
  98. package/src/components/ui/panels/slab-panel.tsx +4 -30
  99. package/src/components/ui/panels/spawn-panel.tsx +161 -0
  100. package/src/components/ui/panels/stair-panel.tsx +20 -74
  101. package/src/components/ui/panels/stair-segment-panel.tsx +0 -25
  102. package/src/components/ui/panels/wall-panel.tsx +10 -8
  103. package/src/components/ui/panels/window-panel.tsx +668 -139
  104. package/src/components/ui/primitives/number-input.tsx +1 -1
  105. package/src/components/ui/primitives/sidebar.tsx +0 -0
  106. package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
  107. package/src/components/ui/sidebar/icon-rail.tsx +0 -0
  108. package/src/components/ui/sidebar/mobile-tab-bar.tsx +46 -0
  109. package/src/components/ui/sidebar/panels/items-panel/index.tsx +330 -0
  110. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +0 -0
  111. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
  112. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -2
  113. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +0 -0
  114. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
  115. package/src/components/ui/sidebar/panels/site-panel/index.tsx +105 -22
  116. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -2
  117. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +0 -0
  118. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +76 -0
  119. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +11 -3
  120. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +10 -5
  121. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +1 -1
  122. package/src/components/ui/sidebar/tab-bar.tsx +3 -0
  123. package/src/components/ui/slider.tsx +1 -1
  124. package/src/components/viewer-overlay.tsx +0 -0
  125. package/src/components/viewer-zone-system.tsx +0 -0
  126. package/src/hooks/use-auto-frame.ts +45 -0
  127. package/src/hooks/use-auto-save.ts +14 -0
  128. package/src/hooks/use-keyboard.ts +74 -7
  129. package/src/hooks/use-mobile.ts +12 -12
  130. package/src/index.tsx +8 -1
  131. package/src/lib/door-interaction.ts +88 -0
  132. package/src/lib/floorplan/geometry.ts +263 -0
  133. package/src/lib/floorplan/index.ts +38 -0
  134. package/src/lib/floorplan/items.ts +179 -0
  135. package/src/lib/floorplan/selection-tool.ts +231 -0
  136. package/src/lib/floorplan/stairs.ts +478 -0
  137. package/src/lib/floorplan/types.ts +57 -0
  138. package/src/lib/floorplan/walls.ts +23 -0
  139. package/src/lib/guide-events.ts +10 -0
  140. package/src/lib/level-duplication.test.ts +70 -0
  141. package/src/lib/level-duplication.ts +153 -0
  142. package/src/lib/local-guide-image.ts +42 -0
  143. package/src/lib/material-paint.ts +284 -0
  144. package/src/lib/roof-duplication.ts +214 -0
  145. package/src/lib/scene-bounds.test.ts +183 -0
  146. package/src/lib/scene-bounds.ts +169 -0
  147. package/src/lib/scene.ts +0 -0
  148. package/src/lib/sfx-bus.ts +2 -0
  149. package/src/lib/sfx-player.ts +5 -5
  150. package/src/lib/stair-duplication.ts +126 -0
  151. package/src/lib/window-interaction.ts +86 -0
  152. package/src/store/use-editor.tsx +186 -62
  153. package/tsconfig.json +2 -1
  154. package/src/components/feedback-dialog.tsx +0 -265
  155. package/src/components/pascal-radio.tsx +0 -280
  156. package/src/components/preview-button.tsx +0 -16
  157. package/src/components/ui/viewer-toolbar.tsx +0 -395
@@ -0,0 +1,860 @@
1
+ import '../../../three-types'
2
+ import { TransformControls, useKeyboardControls } from '@react-three/drei'
3
+ import { type ThreeElements, useFrame, useThree } from '@react-three/fiber'
4
+ import type { ReactNode } from 'react'
5
+ import { forwardRef, Suspense, useCallback, useImperativeHandle, useMemo, useRef } from 'react'
6
+ import * as THREE from 'three'
7
+ import { clamp } from 'three/src/math/MathUtils.js'
8
+
9
+ export type MovementInput = {
10
+ forward?: boolean
11
+ backward?: boolean
12
+ leftward?: boolean
13
+ rightward?: boolean
14
+ joystick?: { x: number; y: number }
15
+ run?: boolean
16
+ jump?: boolean
17
+ }
18
+
19
+ export type CharacterAnimationStatus =
20
+ | 'IDLE'
21
+ | 'WALK'
22
+ | 'RUN'
23
+ | 'JUMP_START'
24
+ | 'JUMP_IDLE'
25
+ | 'JUMP_FALL'
26
+ | 'JUMP_LAND'
27
+
28
+ export type FloatCheckType = 'RAYCAST' | 'SHAPECAST' | 'BOTH'
29
+
30
+ export interface BVHEcctrlApi {
31
+ group: THREE.Group | null
32
+ model: THREE.Group | null
33
+ resetLinVel: () => void
34
+ addLinVel: (v: THREE.Vector3) => void
35
+ setLinVel: (v: THREE.Vector3) => void
36
+ setMovement: (input: MovementInput) => void
37
+ }
38
+
39
+ export interface EcctrlProps extends Omit<ThreeElements['group'], 'ref'> {
40
+ children?: ReactNode
41
+ debug?: boolean
42
+ colliderMeshes?: THREE.Mesh[]
43
+ colliderCapsuleArgs?: [
44
+ radius: number,
45
+ length: number,
46
+ capSegments: number,
47
+ radialSegments: number,
48
+ ]
49
+ paused?: boolean
50
+ delay?: number
51
+ gravity?: number
52
+ fallGravityFactor?: number
53
+ maxFallSpeed?: number
54
+ mass?: number
55
+ sleepTimeout?: number
56
+ slowMotionFactor?: number
57
+ turnSpeed?: number
58
+ maxWalkSpeed?: number
59
+ maxRunSpeed?: number
60
+ acceleration?: number
61
+ deceleration?: number
62
+ counterAccFactor?: number
63
+ airDragFactor?: number
64
+ jumpVel?: number
65
+ floatCheckType?: FloatCheckType
66
+ maxSlope?: number
67
+ floatHeight?: number
68
+ floatPullBackHeight?: number
69
+ floatSensorRadius?: number
70
+ floatSpringK?: number
71
+ floatDampingC?: number
72
+ collisionCheckIteration?: number
73
+ collisionPushBackDamping?: number
74
+ collisionPushBackThreshold?: number
75
+ }
76
+
77
+ type CharacterStatus = {
78
+ position: THREE.Vector3
79
+ linvel: THREE.Vector3
80
+ quaternion: THREE.Quaternion
81
+ inputDir: THREE.Vector3
82
+ movingDir: THREE.Vector3
83
+ isOnGround: boolean
84
+ isOnMovingPlatform: boolean
85
+ animationStatus: CharacterAnimationStatus
86
+ }
87
+
88
+ export const characterStatus: CharacterStatus = {
89
+ position: new THREE.Vector3(),
90
+ linvel: new THREE.Vector3(),
91
+ quaternion: new THREE.Quaternion(),
92
+ inputDir: new THREE.Vector3(),
93
+ movingDir: new THREE.Vector3(),
94
+ isOnGround: false,
95
+ isOnMovingPlatform: false,
96
+ animationStatus: 'IDLE',
97
+ }
98
+
99
+ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
100
+ (
101
+ {
102
+ children,
103
+ debug = false,
104
+ colliderMeshes = [],
105
+ colliderCapsuleArgs = [0.3, 0.6, 4, 8],
106
+ paused = false,
107
+ delay = 1.5,
108
+ gravity = 9.81,
109
+ fallGravityFactor = 4,
110
+ maxFallSpeed = 50,
111
+ mass = 1,
112
+ sleepTimeout = 10,
113
+ slowMotionFactor = 1,
114
+ turnSpeed = 15,
115
+ maxWalkSpeed = 3,
116
+ maxRunSpeed = 5,
117
+ acceleration = 30,
118
+ deceleration = 20,
119
+ counterAccFactor = 0.5,
120
+ airDragFactor = 0.3,
121
+ jumpVel = 5,
122
+ floatCheckType = 'BOTH',
123
+ maxSlope = 1,
124
+ floatHeight = 0.2,
125
+ floatPullBackHeight = 0.25,
126
+ floatSensorRadius = 0.12,
127
+ floatSpringK = 600,
128
+ floatDampingC = 28,
129
+ collisionCheckIteration = 3,
130
+ collisionPushBackDamping = 0.1,
131
+ collisionPushBackThreshold = 0.05,
132
+ ...props
133
+ },
134
+ ref,
135
+ ) => {
136
+ const { camera } = useThree()
137
+ const capsuleRadius = useMemo(() => colliderCapsuleArgs[0], [colliderCapsuleArgs])
138
+ const capsuleLength = useMemo(() => colliderCapsuleArgs[1], [colliderCapsuleArgs])
139
+ const characterGroupRef = useRef<THREE.Group | null>(null)
140
+ const characterColliderRef = useRef<THREE.Mesh | null>(null)
141
+ const characterModelRef = useRef<THREE.Group | null>(null)
142
+ const debugLineStart = useRef<THREE.Mesh | null>(null)
143
+ const debugLineEnd = useRef<THREE.Mesh | null>(null)
144
+ const debugRaySensorStart = useRef<THREE.Mesh | null>(null)
145
+ const debugRaySensorEnd = useRef<THREE.Mesh | null>(null)
146
+ const standPointRef = useRef<THREE.Mesh | null>(null)
147
+ const lookDirRef = useRef<THREE.Mesh | null>(null)
148
+ const inputDirRef = useRef<THREE.ArrowHelper | null>(null)
149
+ const moveDirRef = useRef<THREE.ArrowHelper | null>(null)
150
+ const elapsedRef = useRef(0)
151
+
152
+ const [, getKeys] = useKeyboardControls()
153
+ const presetKeys = {
154
+ forward: false,
155
+ backward: false,
156
+ leftward: false,
157
+ rightward: false,
158
+ jump: false,
159
+ run: false,
160
+ }
161
+
162
+ const upAxis = useRef(new THREE.Vector3(0, 1, 0))
163
+ const localUpAxis = useRef(new THREE.Vector3())
164
+ const gravityDir = useRef(new THREE.Vector3(0, -1, 0))
165
+ const currentLinVel = useRef(new THREE.Vector3())
166
+ const currentLinVelOnPlane = useRef(new THREE.Vector3())
167
+ const isFalling = useRef(false)
168
+ const idleTime = useRef(0)
169
+ const isSleeping = useRef(false)
170
+ const camProjDir = useRef(new THREE.Vector3())
171
+ const camRightDir = useRef(new THREE.Vector3())
172
+ const inputDir = useRef(new THREE.Vector3())
173
+ const inputDirOnPlane = useRef(new THREE.Vector3())
174
+ const movingDir = useRef(new THREE.Vector3())
175
+ const deltaLinVel = useRef(new THREE.Vector3())
176
+ const wantToMoveVel = useRef(new THREE.Vector3())
177
+ const forwardState = useRef(false)
178
+ const backwardState = useRef(false)
179
+ const leftwardState = useRef(false)
180
+ const rightwardState = useRef(false)
181
+ const joystickState = useRef(new THREE.Vector2())
182
+ const runState = useRef(false)
183
+ const jumpState = useRef(false)
184
+ const isOnGround = useRef(false)
185
+ const prevIsOnGround = useRef(false)
186
+ const prevAnimation = useRef<CharacterAnimationStatus>('IDLE')
187
+ const characterModelTargetQuat = useRef(new THREE.Quaternion())
188
+ const characterModelLookMatrix = useRef(new THREE.Matrix4())
189
+ const characterOrigin = useMemo(() => new THREE.Vector3(0, 0, 0), [])
190
+ const contactDepth = useRef(0)
191
+ const contactNormal = useRef(new THREE.Vector3())
192
+ const triContactPoint = useRef(new THREE.Vector3())
193
+ const capsuleContactPoint = useRef(new THREE.Vector3())
194
+ const totalDepth = useRef(0)
195
+ const triangleCount = useRef(0)
196
+ const accumulatedContactNormal = useRef(new THREE.Vector3())
197
+ const accumulatedContactPoint = useRef(new THREE.Vector3())
198
+ const absorbVel = useRef(new THREE.Vector3())
199
+ const pushBackVel = useRef(new THREE.Vector3())
200
+ const characterBbox = useRef(new THREE.Box3())
201
+ const characterSegment = useRef(new THREE.Line3())
202
+ const localCharacterBbox = useRef(new THREE.Box3())
203
+ const localCharacterSegment = useRef(new THREE.Line3())
204
+ const collideInvertMatrix = useRef(new THREE.Matrix4())
205
+ const relativeCollideVel = useRef(new THREE.Vector3())
206
+ const scaledContactRadiusVec = useRef(new THREE.Vector3())
207
+ const deltaDist = useRef(new THREE.Vector3())
208
+ const currSlopeAngle = useRef(0)
209
+ const localMinDistance = useRef(Number.POSITIVE_INFINITY)
210
+ const localClosestPoint = useRef(new THREE.Vector3())
211
+ const localHitNormal = useRef(new THREE.Vector3())
212
+ const triNormal = useRef(new THREE.Vector3())
213
+ const globalMinDistance = useRef(Number.POSITIVE_INFINITY)
214
+ const globalClosestPoint = useRef(new THREE.Vector3())
215
+ const triHitPoint = useRef(new THREE.Vector3())
216
+ const segHitPoint = useRef(new THREE.Vector3())
217
+ const floatHitNormal = useRef(new THREE.Vector3())
218
+ const groundFriction = useRef(0.8)
219
+ const floatSensorBbox = useRef(new THREE.Box3())
220
+ const floatSensorBboxExpendPoint = useRef(new THREE.Vector3())
221
+ const floatSensorSegment = useRef(new THREE.Line3())
222
+ const localFloatSensorBbox = useRef(new THREE.Box3())
223
+ const localFloatSensorBboxExpendPoint = useRef(new THREE.Vector3())
224
+ const localFloatSensorSegment = useRef(new THREE.Line3())
225
+ const floatInvertMatrix = useRef(new THREE.Matrix4())
226
+ const floatNormalInverseMatrix = useRef(new THREE.Matrix3())
227
+ const floatNormalMatrix = useRef(new THREE.Matrix3())
228
+ const floatRaycaster = useRef(new THREE.Raycaster())
229
+ const relativeHitPoint = useRef(new THREE.Vector3())
230
+ const totalPlatformDeltaPos = useRef(new THREE.Vector3())
231
+ const isOnMovingPlatform = useRef(false)
232
+ const floatTempPos = useRef(new THREE.Vector3())
233
+ const floatTempQuat = useRef(new THREE.Quaternion())
234
+ const floatTempScale = useRef(new THREE.Vector3())
235
+ const scaledFloatRadiusVec = useRef(new THREE.Vector3())
236
+ const deltaHit = useRef(new THREE.Vector3())
237
+ const rotationDeltaPos = useRef(new THREE.Vector3())
238
+ const yawQuaternion = useRef(new THREE.Quaternion())
239
+ const contactTempPos = useRef(new THREE.Vector3())
240
+ const contactTempQuat = useRef(new THREE.Quaternion())
241
+ const contactTempScale = useRef(new THREE.Vector3())
242
+
243
+ floatRaycaster.current.far = capsuleRadius + floatHeight + floatPullBackHeight
244
+
245
+ const floatRaycastCandidates = useMemo(
246
+ () =>
247
+ colliderMeshes.filter(
248
+ (mesh) => mesh.geometry.boundsTree && !(mesh instanceof THREE.InstancedMesh),
249
+ ),
250
+ [colliderMeshes],
251
+ )
252
+
253
+ const applyGravity = useCallback(
254
+ (delta: number) => {
255
+ gravityDir.current.copy(upAxis.current).negate()
256
+ const fallingSpeed = currentLinVel.current.dot(gravityDir.current)
257
+ isFalling.current = fallingSpeed > 0
258
+ if (fallingSpeed < maxFallSpeed) {
259
+ currentLinVel.current.addScaledVector(
260
+ gravityDir.current,
261
+ gravity * (isFalling.current ? fallGravityFactor : 1) * delta,
262
+ )
263
+ }
264
+ },
265
+ [fallGravityFactor, gravity, maxFallSpeed],
266
+ )
267
+
268
+ const checkCharacterSleep = useCallback(
269
+ (jump: boolean, delta: number) => {
270
+ const moving = currentLinVel.current.lengthSq() > 1e-6
271
+ const platformIsMoving = totalPlatformDeltaPos.current.lengthSq() > 1e-6
272
+
273
+ if (
274
+ !moving &&
275
+ isOnGround.current &&
276
+ !jump &&
277
+ !isOnMovingPlatform.current &&
278
+ !platformIsMoving
279
+ ) {
280
+ idleTime.current += delta
281
+ if (idleTime.current > sleepTimeout) isSleeping.current = true
282
+ } else {
283
+ idleTime.current = 0
284
+ isSleeping.current = false
285
+ }
286
+ },
287
+ [sleepTimeout],
288
+ )
289
+
290
+ const setInputDirection = useCallback(
291
+ (dir: {
292
+ forward?: boolean
293
+ backward?: boolean
294
+ leftward?: boolean
295
+ rightward?: boolean
296
+ joystick?: THREE.Vector2
297
+ }) => {
298
+ inputDir.current.set(0, 0, 0)
299
+
300
+ camera.getWorldDirection(camProjDir.current)
301
+ camProjDir.current.projectOnPlane(upAxis.current).normalize()
302
+ camRightDir.current.crossVectors(camProjDir.current, upAxis.current).normalize()
303
+
304
+ if (dir.joystick && dir.joystick.lengthSq() > 0) {
305
+ inputDir.current
306
+ .addScaledVector(camProjDir.current, dir.joystick.y)
307
+ .addScaledVector(camRightDir.current, dir.joystick.x)
308
+ } else {
309
+ if (dir.forward) inputDir.current.add(camProjDir.current)
310
+ if (dir.backward) inputDir.current.sub(camProjDir.current)
311
+ if (dir.leftward) inputDir.current.sub(camRightDir.current)
312
+ if (dir.rightward) inputDir.current.add(camRightDir.current)
313
+ }
314
+
315
+ inputDir.current.normalize()
316
+ },
317
+ [camera],
318
+ )
319
+
320
+ const handleCharacterMovement = useCallback(
321
+ (run: boolean, delta: number) => {
322
+ const friction = clamp(groundFriction.current, 0, 1)
323
+
324
+ if (inputDir.current.lengthSq() > 0) {
325
+ if (characterModelRef.current) {
326
+ inputDirOnPlane.current.copy(inputDir.current).projectOnPlane(upAxis.current)
327
+ characterModelLookMatrix.current.lookAt(
328
+ inputDirOnPlane.current,
329
+ characterOrigin,
330
+ upAxis.current,
331
+ )
332
+ characterModelTargetQuat.current.setFromRotationMatrix(characterModelLookMatrix.current)
333
+ characterModelRef.current.quaternion.slerp(
334
+ characterModelTargetQuat.current,
335
+ delta * turnSpeed,
336
+ )
337
+ }
338
+
339
+ const maxSpeed = run ? maxRunSpeed : maxWalkSpeed
340
+ wantToMoveVel.current.copy(inputDir.current).multiplyScalar(maxSpeed)
341
+ const dot = movingDir.current.dot(inputDir.current)
342
+
343
+ deltaLinVel.current.subVectors(wantToMoveVel.current, currentLinVelOnPlane.current)
344
+ deltaLinVel.current.clampLength(
345
+ 0,
346
+ (dot <= 0 ? 1 + counterAccFactor : 1) *
347
+ acceleration *
348
+ friction *
349
+ delta *
350
+ (isOnGround.current ? 1 : airDragFactor),
351
+ )
352
+ currentLinVel.current.add(deltaLinVel.current)
353
+ } else if (isOnGround.current) {
354
+ deltaLinVel.current
355
+ .copy(currentLinVelOnPlane.current)
356
+ .clampLength(0, deceleration * friction * delta)
357
+ currentLinVel.current.sub(deltaLinVel.current)
358
+ }
359
+ },
360
+ [
361
+ acceleration,
362
+ airDragFactor,
363
+ counterAccFactor,
364
+ deceleration,
365
+ maxRunSpeed,
366
+ maxWalkSpeed,
367
+ turnSpeed,
368
+ characterOrigin,
369
+ ],
370
+ )
371
+
372
+ const updateSegmentBBox = useCallback(() => {
373
+ if (!characterGroupRef.current) return
374
+
375
+ characterSegment.current.start
376
+ .set(0, capsuleLength / 2, 0)
377
+ .add(characterGroupRef.current.position)
378
+ characterSegment.current.end
379
+ .set(0, -capsuleLength / 2, 0)
380
+ .add(characterGroupRef.current.position)
381
+
382
+ characterBbox.current
383
+ .makeEmpty()
384
+ .expandByPoint(characterSegment.current.start)
385
+ .expandByPoint(characterSegment.current.end)
386
+ .expandByScalar(capsuleRadius)
387
+
388
+ floatSensorSegment.current.start.copy(characterSegment.current.end)
389
+ floatSensorSegment.current.end
390
+ .copy(floatSensorSegment.current.start)
391
+ .addScaledVector(gravityDir.current, floatHeight + capsuleRadius)
392
+ floatSensorBboxExpendPoint.current
393
+ .copy(floatSensorSegment.current.end)
394
+ .addScaledVector(gravityDir.current, floatPullBackHeight)
395
+
396
+ floatSensorBbox.current
397
+ .makeEmpty()
398
+ .expandByPoint(floatSensorSegment.current.start)
399
+ .expandByPoint(floatSensorBboxExpendPoint.current)
400
+ .expandByScalar(floatSensorRadius)
401
+ }, [capsuleLength, capsuleRadius, floatHeight, floatPullBackHeight, floatSensorRadius])
402
+
403
+ const collisionCheck = useCallback(
404
+ (mesh: THREE.Mesh, originMatrix: THREE.Matrix4, delta: number) => {
405
+ if (!(mesh.visible && mesh.geometry.boundsTree) || mesh.userData.excludeCollisionCheck)
406
+ return
407
+
408
+ originMatrix.decompose(
409
+ contactTempPos.current,
410
+ contactTempQuat.current,
411
+ contactTempScale.current,
412
+ )
413
+ collideInvertMatrix.current.copy(originMatrix).invert()
414
+ localCharacterSegment.current
415
+ .copy(characterSegment.current)
416
+ .applyMatrix4(collideInvertMatrix.current)
417
+
418
+ scaledContactRadiusVec.current.set(
419
+ capsuleRadius / contactTempScale.current.x,
420
+ capsuleRadius / contactTempScale.current.y,
421
+ capsuleRadius / contactTempScale.current.z,
422
+ )
423
+
424
+ localCharacterBbox.current
425
+ .makeEmpty()
426
+ .expandByPoint(localCharacterSegment.current.start)
427
+ .expandByPoint(localCharacterSegment.current.end)
428
+ localCharacterBbox.current.min.addScaledVector(scaledContactRadiusVec.current, -1)
429
+ localCharacterBbox.current.max.add(scaledContactRadiusVec.current)
430
+
431
+ contactDepth.current = 0
432
+ contactNormal.current.set(0, 0, 0)
433
+ absorbVel.current.set(0, 0, 0)
434
+ pushBackVel.current.set(0, 0, 0)
435
+ totalDepth.current = 0
436
+ triangleCount.current = 0
437
+ accumulatedContactNormal.current.set(0, 0, 0)
438
+ accumulatedContactPoint.current.set(0, 0, 0)
439
+
440
+ mesh.geometry.boundsTree.shapecast({
441
+ intersectsBounds: (box) => box.intersectsBox(localCharacterBbox.current),
442
+ intersectsTriangle: (tri) => {
443
+ tri.closestPointToSegment(
444
+ localCharacterSegment.current,
445
+ triContactPoint.current,
446
+ capsuleContactPoint.current,
447
+ )
448
+
449
+ deltaDist.current.copy(triContactPoint.current).sub(capsuleContactPoint.current)
450
+ deltaDist.current.divide(scaledContactRadiusVec.current)
451
+
452
+ if (deltaDist.current.lengthSq() < 1) {
453
+ triContactPoint.current.applyMatrix4(originMatrix)
454
+ capsuleContactPoint.current.applyMatrix4(originMatrix)
455
+
456
+ contactNormal.current
457
+ .copy(capsuleContactPoint.current)
458
+ .sub(triContactPoint.current)
459
+ .normalize()
460
+ contactDepth.current =
461
+ capsuleRadius - capsuleContactPoint.current.distanceTo(triContactPoint.current)
462
+
463
+ accumulatedContactNormal.current.addScaledVector(
464
+ contactNormal.current,
465
+ contactDepth.current,
466
+ )
467
+ accumulatedContactPoint.current.add(triContactPoint.current)
468
+ totalDepth.current += contactDepth.current
469
+ triangleCount.current += 1
470
+ }
471
+ },
472
+ })
473
+
474
+ if (triangleCount.current > 0) {
475
+ accumulatedContactNormal.current.normalize()
476
+ accumulatedContactPoint.current.divideScalar(triangleCount.current)
477
+ const avgDepth = totalDepth.current / triangleCount.current
478
+ relativeCollideVel.current.copy(currentLinVel.current)
479
+ const intoSurfaceVel = relativeCollideVel.current.dot(accumulatedContactNormal.current)
480
+
481
+ if (intoSurfaceVel < 0) {
482
+ absorbVel.current
483
+ .copy(accumulatedContactNormal.current)
484
+ .multiplyScalar(-intoSurfaceVel * (1 + (mesh.userData.restitution ?? 0.05)))
485
+ currentLinVel.current.add(absorbVel.current)
486
+ }
487
+
488
+ if (avgDepth > collisionPushBackThreshold) {
489
+ const correction = (collisionPushBackDamping / delta) * avgDepth
490
+ pushBackVel.current.copy(accumulatedContactNormal.current).multiplyScalar(correction)
491
+ currentLinVel.current.add(pushBackVel.current)
492
+ }
493
+ }
494
+ },
495
+ [capsuleRadius, collisionPushBackDamping, collisionPushBackThreshold],
496
+ )
497
+
498
+ const handleCollisionResponse = useCallback(
499
+ (meshes: THREE.Mesh[], delta: number) => {
500
+ if (meshes.length === 0) return
501
+
502
+ for (let iteration = 0; iteration < collisionCheckIteration; iteration += 1) {
503
+ for (const mesh of meshes) {
504
+ collisionCheck(mesh, mesh.matrixWorld, delta)
505
+ }
506
+ }
507
+ },
508
+ [collisionCheck, collisionCheckIteration],
509
+ )
510
+
511
+ const floatingCheck = useCallback(
512
+ (mesh: THREE.Mesh, originMatrix: THREE.Matrix4) => {
513
+ if (!(mesh.visible && mesh.geometry.boundsTree) || mesh.userData.excludeFloatHit) return
514
+
515
+ originMatrix.decompose(floatTempPos.current, floatTempQuat.current, floatTempScale.current)
516
+ floatInvertMatrix.current.copy(originMatrix).invert()
517
+ floatNormalInverseMatrix.current.getNormalMatrix(floatInvertMatrix.current)
518
+ floatNormalMatrix.current.getNormalMatrix(originMatrix)
519
+
520
+ localFloatSensorSegment.current
521
+ .copy(floatSensorSegment.current)
522
+ .applyMatrix4(floatInvertMatrix.current)
523
+ localFloatSensorBboxExpendPoint.current
524
+ .copy(floatSensorBboxExpendPoint.current)
525
+ .applyMatrix4(floatInvertMatrix.current)
526
+
527
+ scaledFloatRadiusVec.current.set(
528
+ floatSensorRadius / floatTempScale.current.x,
529
+ floatSensorRadius / floatTempScale.current.y,
530
+ floatSensorRadius / floatTempScale.current.z,
531
+ )
532
+
533
+ localFloatSensorBbox.current
534
+ .makeEmpty()
535
+ .expandByPoint(localFloatSensorSegment.current.start)
536
+ .expandByPoint(localFloatSensorBboxExpendPoint.current)
537
+ localFloatSensorBbox.current.min.addScaledVector(scaledFloatRadiusVec.current, -1)
538
+ localFloatSensorBbox.current.max.add(scaledFloatRadiusVec.current)
539
+
540
+ localMinDistance.current = Number.POSITIVE_INFINITY
541
+ localClosestPoint.current.set(
542
+ Number.POSITIVE_INFINITY,
543
+ Number.POSITIVE_INFINITY,
544
+ Number.POSITIVE_INFINITY,
545
+ )
546
+
547
+ mesh.geometry.boundsTree.shapecast({
548
+ intersectsBounds: (box) => box.intersectsBox(localFloatSensorBbox.current),
549
+ intersectsTriangle: (tri) => {
550
+ tri.closestPointToSegment(
551
+ localFloatSensorSegment.current,
552
+ triHitPoint.current,
553
+ segHitPoint.current,
554
+ )
555
+ localUpAxis.current
556
+ .copy(upAxis.current)
557
+ .applyMatrix3(floatNormalInverseMatrix.current)
558
+ .normalize()
559
+ deltaHit.current.subVectors(triHitPoint.current, localFloatSensorSegment.current.start)
560
+ deltaHit.current.divide(scaledFloatRadiusVec.current)
561
+
562
+ const totalLengthSq = deltaHit.current.lengthSq()
563
+ const dot = deltaHit.current.dot(localUpAxis.current)
564
+ const verticalLength =
565
+ Math.abs(dot) /
566
+ ((capsuleRadius + floatHeight + floatPullBackHeight) / floatSensorRadius)
567
+ const horizontalLength = Math.sqrt(Math.max(0, totalLengthSq - dot * dot))
568
+
569
+ if (horizontalLength < 1 && verticalLength < 1) {
570
+ tri.getNormal(triNormal.current)
571
+ triNormal.current.applyMatrix3(floatNormalMatrix.current).normalize()
572
+ triHitPoint.current.applyMatrix4(originMatrix)
573
+
574
+ const slopeAngle = triNormal.current.angleTo(upAxis.current)
575
+ if (verticalLength < localMinDistance.current && slopeAngle < maxSlope) {
576
+ localMinDistance.current = verticalLength
577
+ localClosestPoint.current.copy(triHitPoint.current)
578
+ localHitNormal.current.copy(triNormal.current)
579
+ }
580
+ }
581
+ },
582
+ })
583
+
584
+ if (localMinDistance.current < globalMinDistance.current) {
585
+ globalMinDistance.current = localMinDistance.current
586
+ globalClosestPoint.current.copy(localClosestPoint.current)
587
+ floatHitNormal.current.copy(localHitNormal.current)
588
+ }
589
+ },
590
+ [capsuleRadius, floatHeight, floatPullBackHeight, floatSensorRadius, maxSlope],
591
+ )
592
+
593
+ const handleFloatingResponse = useCallback(
594
+ (meshes: THREE.Mesh[], jump: boolean, delta: number) => {
595
+ if (meshes.length === 0) return
596
+ let shouldJump = jump
597
+
598
+ globalMinDistance.current = Number.POSITIVE_INFINITY
599
+ globalClosestPoint.current.set(
600
+ Number.POSITIVE_INFINITY,
601
+ Number.POSITIVE_INFINITY,
602
+ Number.POSITIVE_INFINITY,
603
+ )
604
+ floatHitNormal.current.set(0, 1, 0)
605
+ isOnGround.current = false
606
+ totalPlatformDeltaPos.current.set(0, 0, 0)
607
+ isOnMovingPlatform.current = false
608
+
609
+ if (floatCheckType !== 'RAYCAST') {
610
+ for (const mesh of meshes) {
611
+ floatingCheck(mesh, mesh.matrixWorld)
612
+ }
613
+ }
614
+
615
+ if (
616
+ floatCheckType !== 'SHAPECAST' &&
617
+ floatRaycastCandidates.length > 0 &&
618
+ globalMinDistance.current === Number.POSITIVE_INFINITY
619
+ ) {
620
+ floatRaycaster.current.ray.origin.copy(floatSensorSegment.current.start)
621
+ floatRaycaster.current.ray.direction.copy(gravityDir.current)
622
+ const hits = floatRaycaster.current.intersectObjects(floatRaycastCandidates, false)
623
+ const hit = hits[0]
624
+ if (hit?.point) {
625
+ globalClosestPoint.current.copy(hit.point)
626
+ if (hit.face) {
627
+ floatHitNormal.current
628
+ .copy(hit.face.normal)
629
+ .transformDirection(hit.object.matrixWorld)
630
+ .normalize()
631
+ }
632
+ }
633
+ }
634
+
635
+ if (globalClosestPoint.current.x === Number.POSITIVE_INFINITY) return
636
+
637
+ relativeHitPoint.current
638
+ .copy(globalClosestPoint.current)
639
+ .sub(floatSensorSegment.current.start)
640
+ const currentDistance = relativeHitPoint.current.length()
641
+ currSlopeAngle.current = floatHitNormal.current.angleTo(upAxis.current)
642
+
643
+ if (currentDistance < floatHeight + capsuleRadius) {
644
+ isOnGround.current = true
645
+ shouldJump = false
646
+ }
647
+
648
+ if (!shouldJump) {
649
+ const displacement = floatHeight + capsuleRadius - currentDistance
650
+ const velocityOnHitNormal = currentLinVel.current.dot(floatHitNormal.current)
651
+ const springForce = displacement * floatSpringK
652
+ const dampingForce = -velocityOnHitNormal * floatDampingC
653
+ const totalForce = springForce + dampingForce - mass * gravity
654
+
655
+ currentLinVel.current.addScaledVector(floatHitNormal.current, (totalForce / mass) * delta)
656
+ }
657
+ },
658
+ [
659
+ capsuleRadius,
660
+ floatCheckType,
661
+ floatDampingC,
662
+ floatHeight,
663
+ floatRaycastCandidates,
664
+ floatSpringK,
665
+ floatingCheck,
666
+ gravity,
667
+ mass,
668
+ ],
669
+ )
670
+
671
+ const updateCharacterWithPlatform = useCallback(() => {
672
+ if (!characterGroupRef.current) return
673
+ rotationDeltaPos.current.copy(totalPlatformDeltaPos.current)
674
+ characterGroupRef.current.position.add(rotationDeltaPos.current)
675
+ yawQuaternion.current.setFromUnitVectors(upAxis.current, floatHitNormal.current)
676
+ }, [upAxis])
677
+
678
+ const updateCharacterAnimation = useCallback(
679
+ (run: boolean, jump: boolean): CharacterAnimationStatus => {
680
+ if (prevIsOnGround.current && jump) return 'JUMP_START'
681
+ if (!isOnGround.current && currentLinVel.current.y > 0) return 'JUMP_IDLE'
682
+ if (!isOnGround.current && currentLinVel.current.y <= 0) return 'JUMP_FALL'
683
+ if (!prevIsOnGround.current && isOnGround.current) return 'JUMP_LAND'
684
+ if (inputDir.current.lengthSq() > 0) return run ? 'RUN' : 'WALK'
685
+ return 'IDLE'
686
+ },
687
+ [],
688
+ )
689
+
690
+ const updateCharacterStatus = useCallback(
691
+ (run: boolean, jump: boolean) => {
692
+ characterModelRef.current?.getWorldPosition(characterStatus.position)
693
+ characterModelRef.current?.getWorldQuaternion(characterStatus.quaternion)
694
+ characterStatus.linvel.copy(currentLinVel.current)
695
+ characterStatus.inputDir.copy(inputDir.current)
696
+ characterStatus.movingDir.copy(movingDir.current)
697
+ characterStatus.isOnGround = isOnGround.current
698
+ characterStatus.isOnMovingPlatform = isOnMovingPlatform.current
699
+ characterStatus.animationStatus = updateCharacterAnimation(run, jump)
700
+ prevAnimation.current = characterStatus.animationStatus
701
+ },
702
+ [updateCharacterAnimation],
703
+ )
704
+
705
+ const resetLinVel = useCallback(() => currentLinVel.current.set(0, 0, 0), [])
706
+ const addLinVel = useCallback(
707
+ (velocity: THREE.Vector3) => currentLinVel.current.add(velocity),
708
+ [],
709
+ )
710
+ const setLinVel = useCallback(
711
+ (velocity: THREE.Vector3) => currentLinVel.current.copy(velocity),
712
+ [],
713
+ )
714
+ const setMovement = useCallback((movement: MovementInput) => {
715
+ if (movement.forward !== undefined) forwardState.current = movement.forward
716
+ if (movement.backward !== undefined) backwardState.current = movement.backward
717
+ if (movement.leftward !== undefined) leftwardState.current = movement.leftward
718
+ if (movement.rightward !== undefined) rightwardState.current = movement.rightward
719
+ if (movement.joystick) joystickState.current.set(movement.joystick.x, movement.joystick.y)
720
+ if (movement.run !== undefined) runState.current = movement.run
721
+ if (movement.jump !== undefined) jumpState.current = movement.jump
722
+ }, [])
723
+
724
+ useImperativeHandle(
725
+ ref,
726
+ () => ({
727
+ get group() {
728
+ return characterGroupRef.current
729
+ },
730
+ get model() {
731
+ return characterModelRef.current
732
+ },
733
+ resetLinVel,
734
+ addLinVel,
735
+ setLinVel,
736
+ setMovement,
737
+ }),
738
+ [addLinVel, resetLinVel, setLinVel, setMovement],
739
+ )
740
+
741
+ const updateDebugger = useCallback(() => {
742
+ debugLineStart.current?.position.copy(characterSegment.current.start)
743
+ debugLineEnd.current?.position.copy(characterSegment.current.end)
744
+ debugRaySensorStart.current?.position.copy(floatSensorSegment.current.start)
745
+ debugRaySensorEnd.current?.position.copy(floatSensorSegment.current.end)
746
+ standPointRef.current?.position.copy(globalClosestPoint.current)
747
+ if (characterGroupRef.current) {
748
+ lookDirRef.current?.position
749
+ .copy(characterGroupRef.current.position)
750
+ .addScaledVector(upAxis.current, 0.7)
751
+ }
752
+ lookDirRef.current?.lookAt(lookDirRef.current.position.clone().add(camProjDir.current))
753
+ inputDirRef.current?.position.copy(characterSegment.current.end)
754
+ inputDirRef.current?.setDirection(inputDir.current)
755
+ inputDirRef.current?.setLength(inputDir.current.lengthSq())
756
+ moveDirRef.current?.position.copy(characterSegment.current.end)
757
+ moveDirRef.current?.setDirection(currentLinVel.current)
758
+ moveDirRef.current?.setLength(currentLinVel.current.length() / maxWalkSpeed)
759
+ }, [characterSegment, maxWalkSpeed])
760
+
761
+ useFrame((_, delta) => {
762
+ elapsedRef.current += delta
763
+ if (paused || elapsedRef.current < delay) return
764
+
765
+ const deltaTime = Math.min(1 / 45, delta) * slowMotionFactor
766
+ const keys = getKeys() ?? presetKeys
767
+ const forward = forwardState.current || keys.forward
768
+ const backward = backwardState.current || keys.backward
769
+ const leftward = leftwardState.current || keys.leftward
770
+ const rightward = rightwardState.current || keys.rightward
771
+ const run = runState.current || keys.run
772
+ const jump = jumpState.current || keys.jump
773
+
774
+ setInputDirection({
775
+ forward,
776
+ backward,
777
+ leftward,
778
+ rightward,
779
+ joystick: joystickState.current,
780
+ })
781
+ handleCharacterMovement(run, deltaTime)
782
+ if (jump && isOnGround.current) currentLinVel.current.y = jumpVel
783
+ movingDir.current.copy(currentLinVel.current).normalize()
784
+ currentLinVelOnPlane.current.copy(currentLinVel.current).projectOnPlane(upAxis.current)
785
+
786
+ checkCharacterSleep(jump, deltaTime)
787
+ if (!isSleeping.current) {
788
+ if (!isOnGround.current) applyGravity(deltaTime)
789
+
790
+ updateSegmentBBox()
791
+ handleCollisionResponse(colliderMeshes, deltaTime)
792
+ handleFloatingResponse(colliderMeshes, jump, deltaTime)
793
+ updateCharacterWithPlatform()
794
+
795
+ if (characterGroupRef.current) {
796
+ characterGroupRef.current.position.addScaledVector(currentLinVel.current, deltaTime)
797
+ }
798
+
799
+ updateCharacterStatus(run, jump)
800
+ prevIsOnGround.current = isOnGround.current
801
+ }
802
+
803
+ if (debug) updateDebugger()
804
+ })
805
+
806
+ return (
807
+ <Suspense fallback={null}>
808
+ <group {...props} dispose={null} ref={characterGroupRef}>
809
+ {debug && (
810
+ <mesh ref={characterColliderRef}>
811
+ <capsuleGeometry args={colliderCapsuleArgs} />
812
+ <meshNormalMaterial wireframe />
813
+ </mesh>
814
+ )}
815
+ <group name="BVHEcctrl-Model" ref={characterModelRef}>
816
+ {children}
817
+ </group>
818
+ </group>
819
+
820
+ {debug && (
821
+ <group>
822
+ <TransformControls object={characterGroupRef.current!} />
823
+ <box3Helper args={[characterBbox.current]} />
824
+ <mesh ref={debugLineStart}>
825
+ <octahedronGeometry args={[0.05, 0]} />
826
+ <meshNormalMaterial />
827
+ </mesh>
828
+ <mesh ref={debugLineEnd}>
829
+ <octahedronGeometry args={[0.05, 0]} />
830
+ <meshNormalMaterial />
831
+ </mesh>
832
+ <box3Helper args={[floatSensorBbox.current]} />
833
+ <mesh ref={debugRaySensorStart}>
834
+ <octahedronGeometry args={[0.1, 0]} />
835
+ <meshBasicMaterial color="yellow" wireframe />
836
+ </mesh>
837
+ <mesh ref={debugRaySensorEnd}>
838
+ <octahedronGeometry args={[0.1, 0]} />
839
+ <meshBasicMaterial color="yellow" wireframe />
840
+ </mesh>
841
+ <mesh ref={lookDirRef} scale={[1, 0.5, 4]}>
842
+ <octahedronGeometry args={[0.1, 0]} />
843
+ <meshNormalMaterial />
844
+ </mesh>
845
+ <arrowHelper args={[undefined, undefined, undefined, '#00f']} ref={inputDirRef} />
846
+ <arrowHelper args={[undefined, undefined, undefined, '#f00']} ref={moveDirRef} />
847
+ <mesh ref={standPointRef}>
848
+ <octahedronGeometry args={[0.12, 0]} />
849
+ <meshBasicMaterial color="red" opacity={0.2} transparent />
850
+ </mesh>
851
+ </group>
852
+ )}
853
+ </Suspense>
854
+ )
855
+ },
856
+ )
857
+
858
+ BVHEcctrl.displayName = 'BVHEcctrl'
859
+
860
+ export default BVHEcctrl