@pascal-app/editor 0.7.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 (103) hide show
  1. package/package.json +6 -6
  2. package/src/components/editor/custom-camera-controls.tsx +2 -1
  3. package/src/components/editor/editor-layout-v2.tsx +4 -3
  4. package/src/components/editor/first-person/build-collider-world.ts +5 -7
  5. package/src/components/editor/first-person/bvh-ecctrl.tsx +119 -54
  6. package/src/components/editor/first-person-controls.tsx +11 -11
  7. package/src/components/editor/floating-action-menu.tsx +0 -0
  8. package/src/components/editor/floorplan-panel.tsx +44 -37
  9. package/src/components/editor/index.tsx +68 -53
  10. package/src/components/editor/selection-manager.tsx +2 -2
  11. package/src/components/editor/snapshot-capture-overlay.tsx +465 -0
  12. package/src/components/editor/thumbnail-generator.tsx +18 -61
  13. package/src/components/editor/use-floorplan-background-placement.ts +3 -3
  14. package/src/components/editor/wall-measurement-label.tsx +0 -0
  15. package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
  16. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +6 -1
  17. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +5 -5
  18. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +10 -12
  19. package/src/components/systems/roof/roof-edit-system.tsx +1 -1
  20. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  21. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
  22. package/src/components/systems/zone/zone-system.tsx +0 -0
  23. package/src/components/tools/ceiling/move-ceiling-tool.tsx +9 -2
  24. package/src/components/tools/fence/curve-fence-tool.tsx +4 -5
  25. package/src/components/tools/fence/fence-tool.tsx +2 -2
  26. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +11 -8
  27. package/src/components/tools/fence/move-fence-tool.tsx +13 -9
  28. package/src/components/tools/item/move-tool.tsx +3 -6
  29. package/src/components/tools/item/placement-math.ts +2 -4
  30. package/src/components/tools/item/placement-strategies.ts +11 -10
  31. package/src/components/tools/item/use-draft-node.ts +0 -1
  32. package/src/components/tools/item/use-placement-coordinator.tsx +9 -111
  33. package/src/components/tools/roof/move-roof-tool.tsx +7 -2
  34. package/src/components/tools/select/box-select-tool.tsx +12 -17
  35. package/src/components/tools/shared/segment-angle.ts +1 -1
  36. package/src/components/tools/tool-manager.tsx +12 -12
  37. package/src/components/tools/wall/curve-wall-tool.tsx +8 -6
  38. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +11 -8
  39. package/src/components/tools/wall/move-wall-tool.tsx +6 -4
  40. package/src/components/tools/wall/wall-drafting.ts +0 -0
  41. package/src/components/tools/wall/wall-tool.tsx +3 -3
  42. package/src/components/tools/zone/zone-tool.tsx +20 -5
  43. package/src/components/ui/action-menu/camera-actions.tsx +0 -0
  44. package/src/components/ui/action-menu/control-modes.tsx +7 -1
  45. package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
  46. package/src/components/ui/action-menu/index.tsx +35 -86
  47. package/src/components/ui/action-menu/view-toggles.tsx +19 -31
  48. package/src/components/ui/command-palette/editor-commands.tsx +6 -4
  49. package/src/components/ui/command-palette/index.tsx +4 -255
  50. package/src/components/ui/controls/material-picker.tsx +8 -5
  51. package/src/components/ui/floating-level-selector.tsx +1 -1
  52. package/src/components/ui/helpers/helper-manager.tsx +5 -0
  53. package/src/components/ui/item-catalog/catalog-items.tsx +1742 -315
  54. package/src/components/ui/item-catalog/item-catalog.tsx +88 -46
  55. package/src/components/ui/level-duplicate-dialog.tsx +3 -5
  56. package/src/components/ui/panels/ceiling-panel.tsx +2 -3
  57. package/src/components/ui/panels/column-panel.tsx +62 -18
  58. package/src/components/ui/panels/door-panel.tsx +272 -265
  59. package/src/components/ui/panels/fence-panel.tsx +0 -5
  60. package/src/components/ui/panels/paint-panel.tsx +66 -41
  61. package/src/components/ui/panels/panel-manager.tsx +3 -32
  62. package/src/components/ui/panels/reference-panel.tsx +28 -13
  63. package/src/components/ui/panels/roof-panel.tsx +52 -2
  64. package/src/components/ui/panels/roof-segment-panel.tsx +0 -0
  65. package/src/components/ui/panels/slab-panel.tsx +0 -0
  66. package/src/components/ui/panels/spawn-panel.tsx +10 -4
  67. package/src/components/ui/panels/stair-panel.tsx +66 -14
  68. package/src/components/ui/panels/wall-panel.tsx +97 -1
  69. package/src/components/ui/panels/window-panel.tsx +13 -5
  70. package/src/components/ui/primitives/number-input.tsx +1 -1
  71. package/src/components/ui/primitives/sidebar.tsx +0 -0
  72. package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
  73. package/src/components/ui/sidebar/icon-rail.tsx +0 -0
  74. package/src/components/ui/sidebar/panels/items-panel/index.tsx +330 -0
  75. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +0 -0
  76. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +0 -0
  77. package/src/components/ui/sidebar/panels/site-panel/index.tsx +4 -6
  78. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +0 -0
  79. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -7
  80. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +3 -1
  81. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
  82. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +1 -1
  83. package/src/components/ui/slider.tsx +1 -1
  84. package/src/components/viewer-overlay.tsx +0 -0
  85. package/src/components/viewer-zone-system.tsx +0 -0
  86. package/src/hooks/use-auto-save.ts +14 -0
  87. package/src/hooks/use-keyboard.ts +10 -0
  88. package/src/index.tsx +8 -1
  89. package/src/lib/level-duplication.test.ts +0 -2
  90. package/src/lib/level-duplication.ts +1 -1
  91. package/src/lib/material-paint.ts +1 -1
  92. package/src/lib/roof-duplication.ts +1 -1
  93. package/src/lib/scene-bounds.ts +1 -1
  94. package/src/lib/scene.ts +0 -0
  95. package/src/lib/sfx-bus.ts +2 -0
  96. package/src/lib/sfx-player.ts +5 -5
  97. package/src/lib/stair-duplication.ts +2 -2
  98. package/src/store/use-editor.tsx +27 -59
  99. package/tsconfig.json +2 -1
  100. package/src/components/feedback-dialog.tsx +0 -265
  101. package/src/components/pascal-radio.tsx +0 -280
  102. package/src/components/preview-button.tsx +0 -16
  103. package/src/components/ui/viewer-toolbar.tsx +0 -436
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/editor",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Pascal building editor component",
5
5
  "type": "module",
6
6
  "exports": {
@@ -44,24 +44,24 @@
44
44
  "clsx": "^2.1.1",
45
45
  "cmdk": "^1.1.1",
46
46
  "howler": "^2.2.4",
47
- "lucide-react": "^0.562.0",
47
+ "lucide-react": "^1.7.0",
48
48
  "mitt": "^3.0.1",
49
49
  "motion": "^12.34.3",
50
50
  "nanoid": "^5.1.6",
51
51
  "tailwind-merge": "^3.5.0",
52
- "three-mesh-bvh": "^0.9.8",
53
52
  "zod": "^4.3.6",
54
- "zustand": "^5.0.11"
53
+ "zustand": "^5.0.11",
54
+ "three-mesh-bvh": "~0.9.8"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@pascal-app/core": "^0.7.0",
58
58
  "@pascal-app/viewer": "^0.7.0",
59
59
  "@pascal/typescript-config": "*",
60
+ "@types/bun": "^1.3.0",
60
61
  "@types/howler": "^2.2.12",
61
- "@types/node": "^22.19.12",
62
62
  "@types/react": "19.2.2",
63
63
  "@types/react-dom": "19.2.2",
64
64
  "@types/three": "^0.184.0",
65
- "typescript": "5.9.3"
65
+ "typescript": "6.0.2"
66
66
  }
67
67
  }
@@ -1,4 +1,5 @@
1
1
  'use client'
2
+
2
3
  import {
3
4
  type CameraControlEvent,
4
5
  type CameraControlFitSceneEvent,
@@ -6,7 +7,7 @@ import {
6
7
  sceneRegistry,
7
8
  useScene,
8
9
  } from '@pascal-app/core'
9
- import { useViewer, ZONE_LAYER } from '@pascal-app/viewer'
10
+ import { useViewer, WalkthroughControls, ZONE_LAYER } from '@pascal-app/viewer'
10
11
  import { CameraControls, CameraControlsImpl } from '@react-three/drei'
11
12
  import { useThree } from '@react-three/fiber'
12
13
  import { useCallback, useEffect, useMemo, useRef } from 'react'
@@ -205,6 +205,7 @@ export function EditorLayoutV2({
205
205
  viewerContent,
206
206
  overlays,
207
207
  }: EditorLayoutV2Props) {
208
+ const isCaptureMode = useEditor((s) => s.isCaptureMode)
208
209
  const isMobile = useIsMobile()
209
210
 
210
211
  if (isMobile) {
@@ -229,7 +230,7 @@ export function EditorLayoutV2({
229
230
 
230
231
  {/* Main content: left column + right column */}
231
232
  <div className="flex min-h-0 flex-1">
232
- {sidebarTabs.length > 0 && (
233
+ {!isCaptureMode && sidebarTabs.length > 0 && (
233
234
  <LeftColumn
234
235
  renderTabContent={renderTabContent}
235
236
  sidebarOverlay={sidebarOverlay}
@@ -238,8 +239,8 @@ export function EditorLayoutV2({
238
239
  )}
239
240
  <RightColumn
240
241
  overlays={overlays}
241
- toolbarLeft={viewerToolbarLeft}
242
- toolbarRight={viewerToolbarRight}
242
+ toolbarLeft={isCaptureMode ? undefined : viewerToolbarLeft}
243
+ toolbarRight={isCaptureMode ? undefined : viewerToolbarRight}
243
244
  >
244
245
  {viewerContent}
245
246
  </RightColumn>
@@ -1,7 +1,7 @@
1
1
  import {
2
- getGarageVisibleOpeningRatio,
3
2
  type AnyNodeId,
4
3
  type DoorNode,
4
+ getGarageVisibleOpeningRatio,
5
5
  isOperationDoorType,
6
6
  sceneRegistry,
7
7
  useInteractive,
@@ -97,11 +97,11 @@ function shouldSkipColliderNode(nodeId: string, type: (typeof COLLIDER_NODE_TYPE
97
97
  const node = useScene.getState().nodes[nodeId as AnyNodeId]
98
98
  if (!node || node.type !== 'door') return false
99
99
 
100
- if (node.openingKind === 'opening') return true
101
-
102
100
  if (!node.segments.length) return true
103
101
 
104
- return node.segments.every((segment) => segment.type === 'empty')
102
+ if (node.openingKind === 'opening') return true
103
+
104
+ return node.segments.every((segment: { type: string }) => segment.type === 'empty')
105
105
  }
106
106
 
107
107
  function createDoorLeafColliderGeometry(root: THREE.Object3D, node: DoorNode) {
@@ -273,9 +273,7 @@ export function buildFirstPersonColliderWorldFromRegistry(): FirstPersonCollider
273
273
  }
274
274
 
275
275
  const mergedGeometry = mergeGeometries(geometries, false)
276
- geometries.forEach((geometry) => {
277
- geometry.dispose()
278
- })
276
+ geometries.forEach((geometry) => geometry.dispose())
279
277
 
280
278
  if (!mergedGeometry || mergedGeometry.getAttribute('position') == null) {
281
279
  mergedGeometry?.dispose()
@@ -1,15 +1,8 @@
1
1
  import '../../../three-types'
2
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'
3
+ import { type ThreeElements, useFrame, useThree } from '@react-three/fiber'
12
4
  import type { ReactNode } from 'react'
5
+ import { forwardRef, Suspense, useCallback, useImperativeHandle, useMemo, useRef } from 'react'
13
6
  import * as THREE from 'three'
14
7
  import { clamp } from 'three/src/math/MathUtils.js'
15
8
 
@@ -156,16 +149,7 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
156
149
  const moveDirRef = useRef<THREE.ArrowHelper | null>(null)
157
150
  const elapsedRef = useRef(0)
158
151
 
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]
152
+ const [, getKeys] = useKeyboardControls()
169
153
  const presetKeys = {
170
154
  forward: false,
171
155
  backward: false,
@@ -222,11 +206,11 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
222
206
  const scaledContactRadiusVec = useRef(new THREE.Vector3())
223
207
  const deltaDist = useRef(new THREE.Vector3())
224
208
  const currSlopeAngle = useRef(0)
225
- const localMinDistance = useRef(Infinity)
209
+ const localMinDistance = useRef(Number.POSITIVE_INFINITY)
226
210
  const localClosestPoint = useRef(new THREE.Vector3())
227
211
  const localHitNormal = useRef(new THREE.Vector3())
228
212
  const triNormal = useRef(new THREE.Vector3())
229
- const globalMinDistance = useRef(Infinity)
213
+ const globalMinDistance = useRef(Number.POSITIVE_INFINITY)
230
214
  const globalClosestPoint = useRef(new THREE.Vector3())
231
215
  const triHitPoint = useRef(new THREE.Vector3())
232
216
  const segHitPoint = useRef(new THREE.Vector3())
@@ -286,7 +270,13 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
286
270
  const moving = currentLinVel.current.lengthSq() > 1e-6
287
271
  const platformIsMoving = totalPlatformDeltaPos.current.lengthSq() > 1e-6
288
272
 
289
- if (!moving && isOnGround.current && !jump && !isOnMovingPlatform.current && !platformIsMoving) {
273
+ if (
274
+ !moving &&
275
+ isOnGround.current &&
276
+ !jump &&
277
+ !isOnMovingPlatform.current &&
278
+ !platformIsMoving
279
+ ) {
290
280
  idleTime.current += delta
291
281
  if (idleTime.current > sleepTimeout) isSleeping.current = true
292
282
  } else {
@@ -340,7 +330,10 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
340
330
  upAxis.current,
341
331
  )
342
332
  characterModelTargetQuat.current.setFromRotationMatrix(characterModelLookMatrix.current)
343
- characterModelRef.current.quaternion.slerp(characterModelTargetQuat.current, delta * turnSpeed)
333
+ characterModelRef.current.quaternion.slerp(
334
+ characterModelTargetQuat.current,
335
+ delta * turnSpeed,
336
+ )
344
337
  }
345
338
 
346
339
  const maxSpeed = run ? maxRunSpeed : maxWalkSpeed
@@ -358,18 +351,33 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
358
351
  )
359
352
  currentLinVel.current.add(deltaLinVel.current)
360
353
  } else if (isOnGround.current) {
361
- deltaLinVel.current.copy(currentLinVelOnPlane.current).clampLength(0, deceleration * friction * delta)
354
+ deltaLinVel.current
355
+ .copy(currentLinVelOnPlane.current)
356
+ .clampLength(0, deceleration * friction * delta)
362
357
  currentLinVel.current.sub(deltaLinVel.current)
363
358
  }
364
359
  },
365
- [acceleration, airDragFactor, counterAccFactor, deceleration, maxRunSpeed, maxWalkSpeed, turnSpeed, characterOrigin],
360
+ [
361
+ acceleration,
362
+ airDragFactor,
363
+ counterAccFactor,
364
+ deceleration,
365
+ maxRunSpeed,
366
+ maxWalkSpeed,
367
+ turnSpeed,
368
+ characterOrigin,
369
+ ],
366
370
  )
367
371
 
368
372
  const updateSegmentBBox = useCallback(() => {
369
373
  if (!characterGroupRef.current) return
370
374
 
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)
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)
373
381
 
374
382
  characterBbox.current
375
383
  .makeEmpty()
@@ -394,11 +402,18 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
394
402
 
395
403
  const collisionCheck = useCallback(
396
404
  (mesh: THREE.Mesh, originMatrix: THREE.Matrix4, delta: number) => {
397
- if (!mesh.visible || !mesh.geometry.boundsTree || mesh.userData.excludeCollisionCheck) return
405
+ if (!(mesh.visible && mesh.geometry.boundsTree) || mesh.userData.excludeCollisionCheck)
406
+ return
398
407
 
399
- originMatrix.decompose(contactTempPos.current, contactTempQuat.current, contactTempScale.current)
408
+ originMatrix.decompose(
409
+ contactTempPos.current,
410
+ contactTempQuat.current,
411
+ contactTempScale.current,
412
+ )
400
413
  collideInvertMatrix.current.copy(originMatrix).invert()
401
- localCharacterSegment.current.copy(characterSegment.current).applyMatrix4(collideInvertMatrix.current)
414
+ localCharacterSegment.current
415
+ .copy(characterSegment.current)
416
+ .applyMatrix4(collideInvertMatrix.current)
402
417
 
403
418
  scaledContactRadiusVec.current.set(
404
419
  capsuleRadius / contactTempScale.current.x,
@@ -445,7 +460,10 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
445
460
  contactDepth.current =
446
461
  capsuleRadius - capsuleContactPoint.current.distanceTo(triContactPoint.current)
447
462
 
448
- accumulatedContactNormal.current.addScaledVector(contactNormal.current, contactDepth.current)
463
+ accumulatedContactNormal.current.addScaledVector(
464
+ contactNormal.current,
465
+ contactDepth.current,
466
+ )
449
467
  accumulatedContactPoint.current.add(triContactPoint.current)
450
468
  totalDepth.current += contactDepth.current
451
469
  triangleCount.current += 1
@@ -492,14 +510,16 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
492
510
 
493
511
  const floatingCheck = useCallback(
494
512
  (mesh: THREE.Mesh, originMatrix: THREE.Matrix4) => {
495
- if (!mesh.visible || !mesh.geometry.boundsTree || mesh.userData.excludeFloatHit) return
513
+ if (!(mesh.visible && mesh.geometry.boundsTree) || mesh.userData.excludeFloatHit) return
496
514
 
497
515
  originMatrix.decompose(floatTempPos.current, floatTempQuat.current, floatTempScale.current)
498
516
  floatInvertMatrix.current.copy(originMatrix).invert()
499
517
  floatNormalInverseMatrix.current.getNormalMatrix(floatInvertMatrix.current)
500
518
  floatNormalMatrix.current.getNormalMatrix(originMatrix)
501
519
 
502
- localFloatSensorSegment.current.copy(floatSensorSegment.current).applyMatrix4(floatInvertMatrix.current)
520
+ localFloatSensorSegment.current
521
+ .copy(floatSensorSegment.current)
522
+ .applyMatrix4(floatInvertMatrix.current)
503
523
  localFloatSensorBboxExpendPoint.current
504
524
  .copy(floatSensorBboxExpendPoint.current)
505
525
  .applyMatrix4(floatInvertMatrix.current)
@@ -517,20 +537,33 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
517
537
  localFloatSensorBbox.current.min.addScaledVector(scaledFloatRadiusVec.current, -1)
518
538
  localFloatSensorBbox.current.max.add(scaledFloatRadiusVec.current)
519
539
 
520
- localMinDistance.current = Infinity
521
- localClosestPoint.current.set(Infinity, Infinity, Infinity)
540
+ localMinDistance.current = Number.POSITIVE_INFINITY
541
+ localClosestPoint.current.set(
542
+ Number.POSITIVE_INFINITY,
543
+ Number.POSITIVE_INFINITY,
544
+ Number.POSITIVE_INFINITY,
545
+ )
522
546
 
523
547
  mesh.geometry.boundsTree.shapecast({
524
548
  intersectsBounds: (box) => box.intersectsBox(localFloatSensorBbox.current),
525
549
  intersectsTriangle: (tri) => {
526
- tri.closestPointToSegment(localFloatSensorSegment.current, triHitPoint.current, segHitPoint.current)
527
- localUpAxis.current.copy(upAxis.current).applyMatrix3(floatNormalInverseMatrix.current).normalize()
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()
528
559
  deltaHit.current.subVectors(triHitPoint.current, localFloatSensorSegment.current.start)
529
560
  deltaHit.current.divide(scaledFloatRadiusVec.current)
530
561
 
531
562
  const totalLengthSq = deltaHit.current.lengthSq()
532
563
  const dot = deltaHit.current.dot(localUpAxis.current)
533
- const verticalLength = Math.abs(dot) / ((capsuleRadius + floatHeight + floatPullBackHeight) / floatSensorRadius)
564
+ const verticalLength =
565
+ Math.abs(dot) /
566
+ ((capsuleRadius + floatHeight + floatPullBackHeight) / floatSensorRadius)
534
567
  const horizontalLength = Math.sqrt(Math.max(0, totalLengthSq - dot * dot))
535
568
 
536
569
  if (horizontalLength < 1 && verticalLength < 1) {
@@ -560,9 +593,14 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
560
593
  const handleFloatingResponse = useCallback(
561
594
  (meshes: THREE.Mesh[], jump: boolean, delta: number) => {
562
595
  if (meshes.length === 0) return
596
+ let shouldJump = jump
563
597
 
564
- globalMinDistance.current = Infinity
565
- globalClosestPoint.current.set(Infinity, Infinity, Infinity)
598
+ globalMinDistance.current = Number.POSITIVE_INFINITY
599
+ globalClosestPoint.current.set(
600
+ Number.POSITIVE_INFINITY,
601
+ Number.POSITIVE_INFINITY,
602
+ Number.POSITIVE_INFINITY,
603
+ )
566
604
  floatHitNormal.current.set(0, 1, 0)
567
605
  isOnGround.current = false
568
606
  totalPlatformDeltaPos.current.set(0, 0, 0)
@@ -574,7 +612,11 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
574
612
  }
575
613
  }
576
614
 
577
- if (floatCheckType !== 'SHAPECAST' && floatRaycastCandidates.length > 0 && globalMinDistance.current === Infinity) {
615
+ if (
616
+ floatCheckType !== 'SHAPECAST' &&
617
+ floatRaycastCandidates.length > 0 &&
618
+ globalMinDistance.current === Number.POSITIVE_INFINITY
619
+ ) {
578
620
  floatRaycaster.current.ray.origin.copy(floatSensorSegment.current.start)
579
621
  floatRaycaster.current.ray.direction.copy(gravityDir.current)
580
622
  const hits = floatRaycaster.current.intersectObjects(floatRaycastCandidates, false)
@@ -582,23 +624,28 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
582
624
  if (hit?.point) {
583
625
  globalClosestPoint.current.copy(hit.point)
584
626
  if (hit.face) {
585
- floatHitNormal.current.copy(hit.face.normal).transformDirection(hit.object.matrixWorld).normalize()
627
+ floatHitNormal.current
628
+ .copy(hit.face.normal)
629
+ .transformDirection(hit.object.matrixWorld)
630
+ .normalize()
586
631
  }
587
632
  }
588
633
  }
589
634
 
590
- if (globalClosestPoint.current.x === Infinity) return
635
+ if (globalClosestPoint.current.x === Number.POSITIVE_INFINITY) return
591
636
 
592
- relativeHitPoint.current.copy(globalClosestPoint.current).sub(floatSensorSegment.current.start)
637
+ relativeHitPoint.current
638
+ .copy(globalClosestPoint.current)
639
+ .sub(floatSensorSegment.current.start)
593
640
  const currentDistance = relativeHitPoint.current.length()
594
641
  currSlopeAngle.current = floatHitNormal.current.angleTo(upAxis.current)
595
642
 
596
643
  if (currentDistance < floatHeight + capsuleRadius) {
597
644
  isOnGround.current = true
598
- jump = false
645
+ shouldJump = false
599
646
  }
600
647
 
601
- if (!jump) {
648
+ if (!shouldJump) {
602
649
  const displacement = floatHeight + capsuleRadius - currentDistance
603
650
  const velocityOnHitNormal = currentLinVel.current.dot(floatHitNormal.current)
604
651
  const springForce = displacement * floatSpringK
@@ -608,7 +655,17 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
608
655
  currentLinVel.current.addScaledVector(floatHitNormal.current, (totalForce / mass) * delta)
609
656
  }
610
657
  },
611
- [capsuleRadius, floatCheckType, floatDampingC, floatHeight, floatRaycastCandidates, floatSpringK, floatingCheck, gravity, mass],
658
+ [
659
+ capsuleRadius,
660
+ floatCheckType,
661
+ floatDampingC,
662
+ floatHeight,
663
+ floatRaycastCandidates,
664
+ floatSpringK,
665
+ floatingCheck,
666
+ gravity,
667
+ mass,
668
+ ],
612
669
  )
613
670
 
614
671
  const updateCharacterWithPlatform = useCallback(() => {
@@ -646,8 +703,14 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
646
703
  )
647
704
 
648
705
  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), [])
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
+ )
651
714
  const setMovement = useCallback((movement: MovementInput) => {
652
715
  if (movement.forward !== undefined) forwardState.current = movement.forward
653
716
  if (movement.backward !== undefined) backwardState.current = movement.backward
@@ -682,7 +745,9 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
682
745
  debugRaySensorEnd.current?.position.copy(floatSensorSegment.current.end)
683
746
  standPointRef.current?.position.copy(globalClosestPoint.current)
684
747
  if (characterGroupRef.current) {
685
- lookDirRef.current?.position.copy(characterGroupRef.current.position).addScaledVector(upAxis.current, 0.7)
748
+ lookDirRef.current?.position
749
+ .copy(characterGroupRef.current.position)
750
+ .addScaledVector(upAxis.current, 0.7)
686
751
  }
687
752
  lookDirRef.current?.lookAt(lookDirRef.current.position.clone().add(camProjDir.current))
688
753
  inputDirRef.current?.position.copy(characterSegment.current.end)
@@ -698,7 +763,7 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
698
763
  if (paused || elapsedRef.current < delay) return
699
764
 
700
765
  const deltaTime = Math.min(1 / 45, delta) * slowMotionFactor
701
- const keys = isInsideKeyboardControls && getKeys ? getKeys() : presetKeys
766
+ const keys = getKeys() ?? presetKeys
702
767
  const forward = forwardState.current || keys.forward
703
768
  const backward = backwardState.current || keys.backward
704
769
  const leftward = leftwardState.current || keys.leftward
@@ -740,7 +805,7 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
740
805
 
741
806
  return (
742
807
  <Suspense fallback={null}>
743
- <group {...props} ref={characterGroupRef} dispose={null}>
808
+ <group {...props} dispose={null} ref={characterGroupRef}>
744
809
  {debug && (
745
810
  <mesh ref={characterColliderRef}>
746
811
  <capsuleGeometry args={colliderCapsuleArgs} />
@@ -777,8 +842,8 @@ const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
777
842
  <octahedronGeometry args={[0.1, 0]} />
778
843
  <meshNormalMaterial />
779
844
  </mesh>
780
- <arrowHelper ref={inputDirRef} args={[undefined, undefined, undefined, '#00f']} />
781
- <arrowHelper ref={moveDirRef} args={[undefined, undefined, undefined, '#f00']} />
845
+ <arrowHelper args={[undefined, undefined, undefined, '#00f']} ref={inputDirRef} />
846
+ <arrowHelper args={[undefined, undefined, undefined, '#f00']} ref={moveDirRef} />
782
847
  <mesh ref={standPointRef}>
783
848
  <octahedronGeometry args={[0.12, 0]} />
784
849
  <meshBasicMaterial color="red" opacity={0.2} transparent />
@@ -455,14 +455,15 @@ export const FirstPersonControls = () => {
455
455
  {controllerStart && (
456
456
  <KeyboardControls map={keyboardMap}>
457
457
  <BVHEcctrl
458
- ref={controllerRef}
459
- key="first-person-controller"
458
+ acceleration={26}
459
+ airDragFactor={0.3}
460
460
  colliderCapsuleArgs={[0.25, 0.8, 4, 8]}
461
461
  colliderMeshes={[world.mesh]}
462
462
  collisionCheckIteration={3}
463
463
  collisionPushBackDamping={0.1}
464
464
  collisionPushBackThreshold={0.001}
465
465
  debug={false}
466
+ deceleration={30}
466
467
  delay={0}
467
468
  fallGravityFactor={4}
468
469
  floatCheckType="BOTH"
@@ -473,13 +474,12 @@ export const FirstPersonControls = () => {
473
474
  floatSpringK={1200}
474
475
  gravity={9.81}
475
476
  jumpVel={6}
477
+ key="first-person-controller"
476
478
  maxRunSpeed={5.5}
477
479
  maxSlope={1.2}
478
480
  maxWalkSpeed={4}
479
481
  position={controllerStart.position}
480
- acceleration={26}
481
- airDragFactor={0.3}
482
- deceleration={30}
482
+ ref={controllerRef}
483
483
  />
484
484
  </KeyboardControls>
485
485
  )}
@@ -551,12 +551,12 @@ export const FirstPersonOverlay = ({ onExit }: { onExit: () => void }) => {
551
551
  {isLocked && (
552
552
  <div className="pointer-events-none fixed top-1/2 right-6 z-40 -translate-y-1/2">
553
553
  <div className="flex min-w-[148px] flex-col gap-3 rounded-2xl border border-border/35 bg-background/80 px-4 py-4 shadow-lg backdrop-blur-xl">
554
- <ControlHint label="Move" keys={['W', 'A', 'S', 'D']} />
554
+ <ControlHint keys={['W', 'A', 'S', 'D']} label="Move" />
555
555
  <div className="h-px w-full bg-border/30" />
556
- <InlineControlHint label="Jump" keyLabel="Space" />
557
- <InlineControlHint label="Sprint" keyLabel="Shift" />
558
- <InlineControlHint label="Interact" keyLabel="E / R" />
559
- <InlineControlHint label="Close" keyLabel="T" />
556
+ <InlineControlHint keyLabel="Space" label="Jump" />
557
+ <InlineControlHint keyLabel="Shift" label="Sprint" />
558
+ <InlineControlHint keyLabel="E / R" label="Interact" />
559
+ <InlineControlHint keyLabel="T" label="Close" />
560
560
  <div className="h-px w-full bg-border/30" />
561
561
  <span className="text-center text-muted-foreground/60 text-xs">
562
562
  Click to look around
@@ -591,7 +591,7 @@ function ControlHint({ label, keys }: { label: string; keys: string[] }) {
591
591
  function InlineControlHint({ label, keyLabel }: { label: string; keyLabel: string }) {
592
592
  return (
593
593
  <div className="flex items-center justify-between gap-3">
594
- <span className="font-medium text-[10px] text-muted-foreground/60 tracking-[0.03em] uppercase">
594
+ <span className="font-medium text-[10px] text-muted-foreground/60 uppercase tracking-[0.03em]">
595
595
  {label}
596
596
  </span>
597
597
  <kbd className="flex h-5 min-w-5 items-center justify-center rounded border border-border/50 bg-accent/40 px-1.5 font-mono text-[10px] text-foreground/80 leading-none">
File without changes