core(space-detection): add pause/resume refcount primitive (#336)
initSpaceDetectionSync subscribes to every scene mutation and auto-derives slabs/ceilings from wall topology. When a host app drives explicit slab/ceiling creation (e.g. the community editor's AI create_room flow), the auto-sync races those nodes and the polygon-signature de-dupe is fragile enough that duplicates leak through. Expose pauseSpaceDetection / resumeSpaceDetection / isSpaceDetectionPaused mirroring the existing pauseSceneHistory refcount in store/history-control. While paused, the subscriber rolls previousSnapshots forward so resume does NOT trigger a backfill that would re-introduce the very duplicates the host paused to avoid. No behavior change for callers that don't touch the new functions.
This commit is contained in:
@@ -56,7 +56,10 @@ export {
|
|||||||
type AutoSlabSyncPlan,
|
type AutoSlabSyncPlan,
|
||||||
detectSpacesForLevel,
|
detectSpacesForLevel,
|
||||||
initSpaceDetectionSync,
|
initSpaceDetectionSync,
|
||||||
|
isSpaceDetectionPaused,
|
||||||
|
pauseSpaceDetection,
|
||||||
planAutoSlabsForLevel,
|
planAutoSlabsForLevel,
|
||||||
|
resumeSpaceDetection,
|
||||||
type Space,
|
type Space,
|
||||||
wallTouchesOthers,
|
wallTouchesOthers,
|
||||||
} from './lib/space-detection'
|
} from './lib/space-detection'
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -884,6 +884,29 @@ function runSpaceDetection(
|
|||||||
editorStore.getState().setSpaces(nextSpaces)
|
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 {
|
export function initSpaceDetectionSync(sceneStore: any, editorStore: any): () => void {
|
||||||
const previousSnapshots = new Map<string, string>()
|
const previousSnapshots = new Map<string, string>()
|
||||||
let isProcessing = false
|
let isProcessing = false
|
||||||
@@ -909,6 +932,17 @@ export function initSpaceDetectionSync(sceneStore: any, editorStore: any): () =>
|
|||||||
currentSnapshots.set(levelId, levelWallSnapshot(walls))
|
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>()
|
const levelsToUpdate = new Set<string>()
|
||||||
for (const levelId of new Set([...previousSnapshots.keys(), ...currentSnapshots.keys()])) {
|
for (const levelId of new Set([...previousSnapshots.keys(), ...currentSnapshots.keys()])) {
|
||||||
if ((previousSnapshots.get(levelId) ?? '') !== (currentSnapshots.get(levelId) ?? '')) {
|
if ((previousSnapshots.get(levelId) ?? '') !== (currentSnapshots.get(levelId) ?? '')) {
|
||||||
|
|||||||
Reference in New Issue
Block a user