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