Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ export {
type AutoSlabSyncPlan,
detectSpacesForLevel,
initSpaceDetectionSync,
isSpaceDetectionPaused,
pauseSpaceDetection,
planAutoSlabsForLevel,
resumeSpaceDetection,
type Space,
wallTouchesOthers,
} from './lib/space-detection'
Expand Down
55 changes: 55 additions & 0 deletions packages/core/src/lib/space-detection-pause.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import {
isSpaceDetectionPaused,
pauseSpaceDetection,
resumeSpaceDetection,
} from './space-detection'

// The pause flag is module-level (matching the existing pauseSceneHistory
// refcount in store/history-control.ts). Reset it before/after each test so
// leftover depth from one case can't bleed into another.
function drain() {
for (let i = 0; i < 64 && isSpaceDetectionPaused(); i += 1) {
resumeSpaceDetection()
}
}

beforeEach(drain)
afterEach(drain)

describe('space-detection pause primitive', () => {
test('defaults to not paused', () => {
expect(isSpaceDetectionPaused()).toBe(false)
})

test('pauseSpaceDetection flips the flag, resumeSpaceDetection clears it', () => {
pauseSpaceDetection()
expect(isSpaceDetectionPaused()).toBe(true)
resumeSpaceDetection()
expect(isSpaceDetectionPaused()).toBe(false)
})

test('refcount — pause depth survives mismatched resumes from a second source', () => {
pauseSpaceDetection()
pauseSpaceDetection()
expect(isSpaceDetectionPaused()).toBe(true)

resumeSpaceDetection()
expect(isSpaceDetectionPaused()).toBe(true)

resumeSpaceDetection()
expect(isSpaceDetectionPaused()).toBe(false)
})

test('resume is a no-op when not currently paused', () => {
expect(isSpaceDetectionPaused()).toBe(false)
resumeSpaceDetection()
resumeSpaceDetection()
expect(isSpaceDetectionPaused()).toBe(false)

pauseSpaceDetection()
expect(isSpaceDetectionPaused()).toBe(true)
resumeSpaceDetection()
expect(isSpaceDetectionPaused()).toBe(false)
})
})
34 changes: 34 additions & 0 deletions packages/core/src/lib/space-detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,29 @@ function runSpaceDetection(
editorStore.getState().setSpaces(nextSpaces)
}

// Refcount of outstanding pause requests, matching the pauseSceneHistory
// pattern. The community editor flips this off while the AI is actively
// mutating the scene so the wall-driven auto slab/ceiling sync doesn't race
// `create_room`'s explicit slabs/ceilings (see plan
// `ai-pause-space-detection`).
let spaceDetectionPauseDepth = 0

/** Pause the wall-driven auto slab/ceiling sync. Refcounted — pair with `resumeSpaceDetection`. */
export function pauseSpaceDetection(): void {
spaceDetectionPauseDepth += 1
}

/** Resume the wall-driven auto slab/ceiling sync. No-op if not currently paused. */
export function resumeSpaceDetection(): void {
if (spaceDetectionPauseDepth === 0) return
spaceDetectionPauseDepth -= 1
}

/** True iff the wall-driven auto slab/ceiling sync is currently paused. */
export function isSpaceDetectionPaused(): boolean {
return spaceDetectionPauseDepth > 0
}

export function initSpaceDetectionSync(sceneStore: any, editorStore: any): () => void {
const previousSnapshots = new Map<string, string>()
let isProcessing = false
Expand All @@ -909,6 +932,17 @@ export function initSpaceDetectionSync(sceneStore: any, editorStore: any): () =>
currentSnapshots.set(levelId, levelWallSnapshot(walls))
}

// Paused: roll the snapshot forward so we don't backfill (and re-duplicate)
// every paused change once detection resumes. Whatever the AI built while
// paused becomes the new baseline; only future changes will reconcile.
if (spaceDetectionPauseDepth > 0) {
previousSnapshots.clear()
for (const [levelId, snapshot] of currentSnapshots.entries()) {
previousSnapshots.set(levelId, snapshot)
}
return
}

const levelsToUpdate = new Set<string>()
for (const levelId of new Set([...previousSnapshots.keys(), ...currentSnapshots.keys()])) {
if ((previousSnapshots.get(levelId) ?? '') !== (currentSnapshots.get(levelId) ?? '')) {
Expand Down
Loading