@pascal-app/editor 1.0.0-beta.4 → 1.0.0-beta.5

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 (73) hide show
  1. package/package.json +7 -6
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +12 -3
  3. package/src/components/editor/editor-layout-v2.tsx +8 -3
  4. package/src/components/editor/elevation-3d-guide-layer.tsx +3 -2
  5. package/src/components/editor/first-person-controls.tsx +2 -45
  6. package/src/components/editor/floating-action-menu.tsx +13 -4
  7. package/src/components/editor/floorplan-background-selection.test.ts +31 -4
  8. package/src/components/editor/floorplan-background-selection.ts +27 -7
  9. package/src/components/editor/floorplan-panel.tsx +5 -45
  10. package/src/components/editor/group-floating-action-menu.tsx +30 -15
  11. package/src/components/editor/index.tsx +112 -25
  12. package/src/components/editor/measurement-pill.tsx +12 -12
  13. package/src/components/editor/node-action-menu.tsx +29 -1
  14. package/src/components/editor/opening-guides-3d-layer.tsx +13 -4
  15. package/src/components/editor/selection-manager.tsx +28 -5
  16. package/src/components/editor/site-edge-labels.tsx +2 -1
  17. package/src/components/editor/snapshot-capture-overlay.tsx +11 -2
  18. package/src/components/editor/thumbnail-generator.tsx +27 -11
  19. package/src/components/editor/wall-measurement-label.tsx +3 -2
  20. package/src/components/editor-2d/floorplan-group-action-menu.tsx +23 -16
  21. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +26 -84
  22. package/src/components/tools/item/placement-strategies.test.ts +167 -0
  23. package/src/components/tools/item/placement-strategies.ts +57 -22
  24. package/src/components/tools/item/use-placement-coordinator.tsx +19 -27
  25. package/src/components/tools/shared/placement-box.tsx +9 -4
  26. package/src/components/tools/site/terrain-brush-cursor.tsx +2 -1
  27. package/src/components/tools/wall/wall-drafting.test.ts +207 -2
  28. package/src/components/tools/wall/wall-drafting.ts +84 -400
  29. package/src/components/ui/command-palette/editor-commands.tsx +3 -1
  30. package/src/components/ui/floating-level-selector.tsx +1 -1
  31. package/src/components/ui/panels/multi-selection-panel.tsx +54 -13
  32. package/src/components/ui/panels/panel-wrapper.tsx +228 -12
  33. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +7 -2
  34. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +11 -0
  35. package/src/components/ui/sidebar/panels/site-panel/index.tsx +12 -0
  36. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +8 -1
  37. package/src/components/ui/sidebar/tab-bar.tsx +7 -1
  38. package/src/components/viewer/floorplan-compass-button.tsx +50 -0
  39. package/src/components/viewer/floorplan-preview-geometry.test.ts +107 -0
  40. package/src/components/viewer/floorplan-preview-geometry.ts +281 -0
  41. package/src/components/viewer/floorplan-preview-navigation.test.ts +43 -0
  42. package/src/components/viewer/floorplan-preview-navigation.ts +62 -0
  43. package/src/components/viewer/floorplan-preview.tsx +1069 -0
  44. package/src/components/viewer/use-viewer-camera-navigation-sync.ts +140 -0
  45. package/src/components/viewer/viewer-stage-modes.test.ts +38 -0
  46. package/src/components/viewer/viewer-stage-modes.ts +49 -0
  47. package/src/components/viewer/viewer-stage-switcher.tsx +95 -0
  48. package/src/components/viewer/viewer-stage.test.tsx +47 -0
  49. package/src/components/viewer/viewer-stage.tsx +218 -0
  50. package/src/components/viewer-overlay.tsx +12 -8
  51. package/src/hooks/use-auto-save.test.ts +50 -1
  52. package/src/hooks/use-auto-save.ts +44 -9
  53. package/src/hooks/use-keyboard.ts +35 -1
  54. package/src/index.tsx +27 -0
  55. package/src/lib/contextual-help.test.ts +8 -0
  56. package/src/lib/contextual-help.ts +8 -0
  57. package/src/lib/floorplan/floorplan-readonly.test.ts +47 -0
  58. package/src/lib/floorplan/floorplan-readonly.ts +84 -0
  59. package/src/lib/history.test.ts +75 -10
  60. package/src/lib/history.ts +58 -13
  61. package/src/lib/inspector-card-mode.test.ts +92 -0
  62. package/src/lib/inspector-card-mode.ts +60 -0
  63. package/src/lib/measurement-parser.ts +3 -0
  64. package/src/lib/measurements.test.ts +4 -0
  65. package/src/lib/paint-scope.test.ts +58 -2
  66. package/src/lib/paint-scope.ts +8 -2
  67. package/src/lib/selection-routing.test.ts +20 -5
  68. package/src/lib/selection-routing.ts +15 -1
  69. package/src/lib/session-groups.test.ts +88 -0
  70. package/src/lib/session-groups.ts +201 -0
  71. package/src/lib/structured-clone-fallback.test.ts +45 -0
  72. package/src/lib/structured-clone-fallback.ts +24 -0
  73. package/src/store/use-session-groups.ts +103 -0
@@ -295,6 +295,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
295
295
  const { canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling } = useSpatialQuery()
296
296
  const { asset, draftNode } = config
297
297
  const unit = useViewer((state) => state.unit)
298
+ const metricNotation = useViewer((state) => state.metricNotation)
298
299
  const gridSnapStep = useEditor((s) => s.gridSnapStep)
299
300
  const updatePreviewGeometry = useCallback((bounds: PreviewBounds) => {
300
301
  const [width, height, depth] = bounds.dimensions
@@ -1129,19 +1130,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1129
1130
  }
1130
1131
  const correctedX = wallDragAnchor.startX + (rawX - wallDragAnchor.rawX)
1131
1132
  const correctedY = wallDragAnchor.startY + (rawY - wallDragAnchor.rawY)
1132
- const wallMesh = sceneRegistry.nodes.get(event.node.id)
1133
- // Derive the world cursor from the corrected wall-local point so the
1134
- // visual cursor (world) and the stored position (wall-local) agree; if
1135
- // the wall mesh is somehow absent, keep the raw world hit unchanged.
1136
- const correctedWorld = wallMesh
1137
- ? wallMesh.localToWorld(new Vector3(correctedX, correctedY, event.localPosition[2]))
1138
- : null
1139
1133
  wallMoveEvent = {
1140
1134
  ...event,
1141
1135
  localPosition: [correctedX, correctedY, event.localPosition[2]],
1142
- position: correctedWorld
1143
- ? [correctedWorld.x, correctedWorld.y, correctedWorld.z]
1144
- : event.position,
1145
1136
  }
1146
1137
  }
1147
1138
  const result = wallStrategy.move(ctx, wallMoveEvent, getActiveValidators())
@@ -1203,22 +1194,11 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
1203
1194
 
1204
1195
  // Publish live transform for the 2D floorplan. The floorplan resolves a
1205
1196
  // wall item's footprint (and its wall-side depth offset) from this
1206
- // rotation as a PLAN-space yaw. `cursorRotationY` is the 3D world cursor
1207
- // yaw, which is π off from the plan rotation on a wall face — feeding it
1208
- // raw flips the footprint to the far side of the wall during placement.
1209
- // Publish the plan rotation (wall angle + the item's wall-local yaw) so
1210
- // the preview matches what the committed node resolves to.
1211
- let liveRotation = result.cursorRotationY
1212
- const liveWallId = placementState.current.wallId
1213
- const liveWall = liveWallId ? useScene.getState().nodes[liveWallId as AnyNodeId] : undefined
1214
- if (liveWall?.type === 'wall') {
1215
- const w = liveWall as WallNode
1216
- const wallPlanRotation = -Math.atan2(w.end[1] - w.start[1], w.end[0] - w.start[0])
1217
- liveRotation = wallPlanRotation + (draft.rotation[1] ?? 0)
1218
- }
1197
+ // rotation as a PLAN-space yaw which is exactly what the wall strategy
1198
+ // composes `cursorRotationY` as (wall yaw + the item's wall-local yaw).
1219
1199
  useLiveTransforms.getState().set(draft.id, {
1220
1200
  position: result.cursorPosition,
1221
- rotation: liveRotation,
1201
+ rotation: result.cursorRotationY,
1222
1202
  })
1223
1203
  }
1224
1204
  }
@@ -2558,9 +2538,21 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
2558
2538
  depth: currentDimensionBounds.dimensions[2],
2559
2539
  center: [currentDimensionBounds.center[0], currentDimensionBounds.center[2]],
2560
2540
  }
2561
- const widthLabel = formatLinearMeasurement(currentDimensionBounds.dimensions[0], unit)
2562
- const depthLabel = formatLinearMeasurement(currentDimensionBounds.dimensions[2], unit)
2563
- const heightLabel = formatLinearMeasurement(currentDimensionBounds.dimensions[1], unit)
2541
+ const widthLabel = formatLinearMeasurement(
2542
+ currentDimensionBounds.dimensions[0],
2543
+ unit,
2544
+ metricNotation,
2545
+ )
2546
+ const depthLabel = formatLinearMeasurement(
2547
+ currentDimensionBounds.dimensions[2],
2548
+ unit,
2549
+ metricNotation,
2550
+ )
2551
+ const heightLabel = formatLinearMeasurement(
2552
+ currentDimensionBounds.dimensions[1],
2553
+ unit,
2554
+ metricNotation,
2555
+ )
2564
2556
  const widthLabelPosition: [number, number, number] = [
2565
2557
  currentDimensionBounds.center[0],
2566
2558
  0.04,
@@ -8,7 +8,11 @@ import { PlaneGeometry } from 'three'
8
8
  import { distance, smoothstep, uv, vec2 } from 'three/tsl'
9
9
  import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
10
10
  import { EDITOR_LAYER } from '../../../lib/constants'
11
- import { formatLinearMeasurement, type LinearUnit } from '../../../lib/measurements'
11
+ import {
12
+ formatLinearMeasurement,
13
+ type LinearUnit,
14
+ type MetricNotation,
15
+ } from '../../../lib/measurements'
12
16
  import { createLineGeometry, getBoxEdgePoints } from './placement-box-geometry'
13
17
 
14
18
  const VALID_COLOR = 0x22_c5_5e // green-500
@@ -17,6 +21,7 @@ const MEASUREMENT_COLOR = 0x0f_17_2a
17
21
 
18
22
  type PlacementBoxMeasurements = {
19
23
  unit: LinearUnit
24
+ metricNotation?: MetricNotation
20
25
  }
21
26
 
22
27
  function getMeasurementGuidePoints(width: number, height: number, depth: number) {
@@ -271,15 +276,15 @@ export function PlacementBox({
271
276
  renderOrder={998}
272
277
  />
273
278
  <MeasurementPill
274
- label={formatLinearMeasurement(width, measurements.unit)}
279
+ label={formatLinearMeasurement(width, measurements.unit, measurements.metricNotation)}
275
280
  position={[0, 0.04, depth / 2 + 0.24]}
276
281
  />
277
282
  <MeasurementPill
278
- label={formatLinearMeasurement(depth, measurements.unit)}
283
+ label={formatLinearMeasurement(depth, measurements.unit, measurements.metricNotation)}
279
284
  position={[width / 2 + 0.24, 0.04, 0]}
280
285
  />
281
286
  <MeasurementPill
282
- label={formatLinearMeasurement(height, measurements.unit)}
287
+ label={formatLinearMeasurement(height, measurements.unit, measurements.metricNotation)}
283
288
  position={[-width / 2 - 0.24, height / 2, -depth / 2]}
284
289
  />
285
290
  </>
@@ -79,6 +79,7 @@ export const TerrainBrushCursor: React.FC<{
79
79
  const sampling = useEditor((state) => state.terrainSampling)
80
80
  const color = brushRingColor(verb, sampling)
81
81
  const unit = useViewer((state) => state.unit)
82
+ const metricNotation = useViewer((state) => state.metricNotation)
82
83
 
83
84
  const lineRef = useRef<Line>(null)
84
85
  const centerRef = useRef<Group>(null)
@@ -195,7 +196,7 @@ export const TerrainBrushCursor: React.FC<{
195
196
 
196
197
  const centerHeight = surfaceHeightAt(surface, hit.x, hit.z)
197
198
  center.position.set(hit.x, centerHeight + RING_LIFT, hit.z)
198
- const heightLabel = `H ${formatMeasurement(centerHeight, unit)}`
199
+ const heightLabel = `H ${formatMeasurement(centerHeight, unit, metricNotation)}`
199
200
  if (heightRef.current) {
200
201
  heightRef.current.style.display = ''
201
202
  if (heightLabel !== lastHeightLabelRef.current) {
@@ -3,12 +3,15 @@ import {
3
3
  type AnyNode,
4
4
  type AnyNodeId,
5
5
  applyHeightPatch,
6
+ CeilingNode,
6
7
  createTerrainField,
7
8
  DoorNode as DoorSchema,
8
9
  encodeTerrainField,
9
10
  flattenPatch,
10
11
  GROUND_SUPPORT_ID,
12
+ initSpaceDetectionSync,
11
13
  runAsSingleSceneHistoryStep,
14
+ SlabNode,
12
15
  spatialGridManager,
13
16
  useScene,
14
17
  type WallNode,
@@ -56,7 +59,7 @@ function seedLevel(walls: WallNode[], extraNodes: AnyNode[] = []) {
56
59
  parentId: null,
57
60
  visible: true,
58
61
  metadata: {},
59
- children: walls.map((wall) => wall.id),
62
+ children: [...walls.map((wall) => wall.id), ...extraNodes.map((node) => node.id)],
60
63
  level: 0,
61
64
  } as AnyNode,
62
65
  ],
@@ -69,6 +72,66 @@ function seedLevel(walls: WallNode[], extraNodes: AnyNode[] = []) {
69
72
  } as never)
70
73
  }
71
74
 
75
+ // Seeds a site/building/level chain whose terrain field carries a sculpted
76
+ // patch raised to `liftTo` in the far corner — enough for ground drafts to be
77
+ // terrain chains, while the walls under test stand where the ground is 0.
78
+ function seedTerrainLevel(walls: WallNode[], liftTo: number) {
79
+ const field = createTerrainField({ cols: 9, rows: 9, spacing: 1, origin: [-4, -4] })
80
+ const patch = flattenPatch(field, { maxX: -2, maxZ: -2, minX: -4, minZ: -4 }, liftTo)
81
+ if (!patch) throw new Error('Expected terrain patch')
82
+ const terrain = applyHeightPatch(field, patch)
83
+ useScene.setState({
84
+ nodes: Object.fromEntries([
85
+ [
86
+ 'site_test',
87
+ {
88
+ id: 'site_test',
89
+ type: 'site',
90
+ object: 'node',
91
+ parentId: null,
92
+ visible: true,
93
+ metadata: {},
94
+ children: ['building_test'],
95
+ terrain: encodeTerrainField(terrain),
96
+ } as unknown as AnyNode,
97
+ ],
98
+ [
99
+ 'building_test',
100
+ {
101
+ id: 'building_test',
102
+ type: 'building',
103
+ object: 'node',
104
+ parentId: 'site_test',
105
+ visible: true,
106
+ metadata: {},
107
+ children: [LEVEL_ID],
108
+ position: [0, 0, 0],
109
+ rotation: [0, 0, 0],
110
+ } as AnyNode,
111
+ ],
112
+ [
113
+ LEVEL_ID,
114
+ {
115
+ id: LEVEL_ID,
116
+ type: 'level',
117
+ object: 'node',
118
+ parentId: 'building_test',
119
+ visible: true,
120
+ metadata: {},
121
+ children: walls.map((wall) => wall.id),
122
+ level: 0,
123
+ baseElevation: 0,
124
+ height: 2.5,
125
+ } as AnyNode,
126
+ ],
127
+ ...walls.map((wall) => [wall.id, wall] as const),
128
+ ]),
129
+ rootNodeIds: ['site_test' as AnyNodeId],
130
+ dirtyNodes: new Set(),
131
+ collections: {},
132
+ } as never)
133
+ }
134
+
72
135
  function levelWalls(): WallNode[] {
73
136
  return Object.values(useScene.getState().nodes).filter(
74
137
  (node): node is WallNode => node?.type === 'wall',
@@ -109,7 +172,9 @@ describe('createWallOnCurrentLevel', () => {
109
172
  expect(levelWalls()).toHaveLength(2)
110
173
  })
111
174
 
112
- test('committed wall preserves the ghost construction elevation on ground', () => {
175
+ test('committed wall preserves the ghost construction elevation on terrain', () => {
176
+ seedTerrainLevel([makeWall([0, 0], [4, 0], 'wall_a')], 1.75)
177
+
113
178
  const created = createWallOnCurrentLevel([2, 2], [3, 2], {
114
179
  supportCap: 1.75,
115
180
  preferredSupportSlabId: GROUND_SUPPORT_ID,
@@ -133,6 +198,69 @@ describe('createWallOnCurrentLevel', () => {
133
198
  expect(support.elevation).toBe(1.75)
134
199
  })
135
200
 
201
+ test('a flat-ground draft (no sculpted terrain) commits plane-bound', () => {
202
+ // Pointing at bare ground freezes a GROUND construction plane at 0. With
203
+ // no terrain field in the scene that plane is just the backdrop — none of
204
+ // the draft options may reach the committed node: no stamped height, no
205
+ // persisted ground host (a slab drawn later must lift the wall), no
206
+ // election cap.
207
+ const created = createWallOnCurrentLevel([2, 2], [3, 2], {
208
+ supportCap: 0,
209
+ preferredSupportSlabId: GROUND_SUPPORT_ID,
210
+ constructionElevation: 0,
211
+ constructionHeight: 2.5,
212
+ })
213
+
214
+ expect(created).not.toBeNull()
215
+ expect(created?.height).toBeUndefined()
216
+ expect(created?.supportOffset).toBeUndefined()
217
+ expect(created?.supportSlabId).toBeUndefined()
218
+ })
219
+
220
+ test('a wall started on a slab stays plane-bound (no stamped height or offset)', () => {
221
+ const slab = SlabNode.parse({
222
+ id: 'slab_floor',
223
+ parentId: LEVEL_ID,
224
+ polygon: [
225
+ [-1, -1],
226
+ [5, -1],
227
+ [5, 5],
228
+ [-1, 5],
229
+ ],
230
+ elevation: 0.05,
231
+ thickness: 0.05,
232
+ })
233
+ seedLevel([makeWall([0, 0], [4, 0], 'wall_a')], [slab])
234
+
235
+ // Mirrors the 3D tool's first click on the slab top: frozen plane at the
236
+ // slab elevation, ghost drawn at the level height. None of it may reach
237
+ // the committed node — plane-bound is the default.
238
+ const created = createWallOnCurrentLevel([2, 2], [3, 2], {
239
+ supportCap: 0.05,
240
+ preferredSupportSlabId: slab.id,
241
+ constructionElevation: 0.05,
242
+ constructionHeight: 2.55,
243
+ })
244
+
245
+ expect(created).not.toBeNull()
246
+ expect(created?.height).toBeUndefined()
247
+ expect(created?.supportOffset).toBeUndefined()
248
+ expect(created?.supportSlabId).not.toBe(GROUND_SUPPORT_ID)
249
+ })
250
+
251
+ test('a non-ground draft never freezes the ghost height, even at a raised plane', () => {
252
+ const created = createWallOnCurrentLevel([2, 2], [3, 2], {
253
+ supportCap: 1.2,
254
+ preferredSupportSlabId: null,
255
+ constructionElevation: 1.2,
256
+ constructionHeight: 2.5,
257
+ })
258
+
259
+ expect(created).not.toBeNull()
260
+ expect(created?.height).toBeUndefined()
261
+ expect(created?.supportOffset).toBeUndefined()
262
+ })
263
+
136
264
  test('2D terrain construction options freeze the first-point elevation and wall height', () => {
137
265
  const field = createTerrainField({ cols: 5, rows: 5, spacing: 1, origin: [-2, -2] })
138
266
  const patch = flattenPatch(field, { minX: -2, minZ: -2, maxX: 2, maxZ: 2 }, 1.5)
@@ -168,6 +296,7 @@ describe('createWallOnCurrentLevel', () => {
168
296
  metadata: {},
169
297
  children: [],
170
298
  level: 0,
299
+ baseElevation: 0,
171
300
  height: 3,
172
301
  } as AnyNode
173
302
  const nodes = Object.fromEntries([site, building, level].map((node) => [node.id, node]))
@@ -261,6 +390,82 @@ describe('createWallOnCurrentLevel', () => {
261
390
  expect(created).not.toBeNull()
262
391
  expect(useScene.temporal.getState().pastStates.length - before).toBe(1)
263
392
  })
393
+
394
+ test('a crossing splits both the host and the inserted wall in one undo step', () => {
395
+ const before = useScene.temporal.getState().pastStates.length
396
+
397
+ const created = createWallOnCurrentLevel([2, -2], [2, 2])
398
+
399
+ expect(created?.start).toEqual([2, 0])
400
+ expect(created?.end).toEqual([2, 2])
401
+ expect(useScene.getState().nodes['wall_a' as AnyNodeId]).toBeUndefined()
402
+ expect(levelWalls()).toHaveLength(4)
403
+ expect(useScene.temporal.getState().pastStates.length - before).toBe(1)
404
+ })
405
+
406
+ test('a room divider splits customized auto slabs and ceilings', () => {
407
+ const walls = [
408
+ makeWall([0, 0], [8, 0], 'wall_bottom'),
409
+ makeWall([8, 0], [8, 6], 'wall_right'),
410
+ makeWall([8, 6], [0, 6], 'wall_top'),
411
+ makeWall([0, 6], [0, 0], 'wall_left'),
412
+ ]
413
+ const slab = SlabNode.parse({
414
+ id: 'slab_auto',
415
+ parentId: LEVEL_ID,
416
+ polygon: [
417
+ [0, 0],
418
+ [8, 0],
419
+ [8, 6],
420
+ [0, 6],
421
+ ],
422
+ elevation: 0.18,
423
+ thickness: 0.32,
424
+ autoFromWalls: true,
425
+ })
426
+ const ceiling = CeilingNode.parse({
427
+ id: 'ceiling_auto',
428
+ parentId: LEVEL_ID,
429
+ polygon: slab.polygon,
430
+ autoFromWalls: true,
431
+ })
432
+ seedLevel(walls, [slab, ceiling])
433
+ const stopDetection = initSpaceDetectionSync(useScene, useEditor)
434
+
435
+ try {
436
+ expect(createWallOnCurrentLevel([4, 0], [4, 6])).not.toBeNull()
437
+
438
+ const nodes = Object.values(useScene.getState().nodes)
439
+ const slabs = nodes.filter((node) => node.type === 'slab')
440
+ const ceilings = nodes.filter((node) => node.type === 'ceiling')
441
+ expect(slabs).toHaveLength(2)
442
+ expect(ceilings).toHaveLength(2)
443
+ expect(
444
+ slabs.every(
445
+ (node) => node.autoFromWalls && node.elevation === 0.18 && node.thickness === 0.32,
446
+ ),
447
+ ).toBe(true)
448
+ expect(ceilings.every((node) => node.autoFromWalls)).toBe(true)
449
+ } finally {
450
+ stopDetection()
451
+ }
452
+ })
453
+
454
+ test('close crossings reject the whole insertion without mutating the scene', () => {
455
+ seedLevel([
456
+ makeWall([2, -2], [2, 2], 'wall_close_a'),
457
+ makeWall([2.0055, -2], [2.0055, 2], 'wall_close_b'),
458
+ ])
459
+ useScene.temporal.getState().clear()
460
+ const beforeNodes = useScene.getState().nodes
461
+
462
+ const created = createWallOnCurrentLevel([0, 0], [4, 0])
463
+
464
+ expect(created).toBeNull()
465
+ expect(useScene.getState().nodes).toBe(beforeNodes)
466
+ expect(levelWalls()).toHaveLength(2)
467
+ expect(useScene.temporal.getState().pastStates).toHaveLength(0)
468
+ })
264
469
  })
265
470
 
266
471
  describe('resolveEndpointWallSplit', () => {