test(2.5D): cover rigid group boundaries and fallback

Issue: #651
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-09-26 04:34:49 +03:00
parent 7cc5423efe
commit e73e56dd94
2 changed files with 87 additions and 0 deletions
+27
View File
@@ -10337,6 +10337,33 @@ const MUTANT_DEFINITIONS = [
replace: ' if (false && boundsIntersect(groupBounds[left], groupBounds[right])) join(left, right);',
}],
},
{
id: 'iso-rigid-groups-cross-room-boundary',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test --test-name-pattern="never join close markers" test/iso-overlays.test.mjs',
because: '#651 groups markers only within one owning room. Removing the owner check lets a '
+ 'close pair across a shared wall move as one group and cross the room boundary',
patches: [{
file: 'src/iso-overlays.ts',
find: ' if (stable[right].placement.owner?.id !== owner) continue;',
replace: ' if (false) continue; // mutant: room ownership no longer separates groups',
}],
},
{
id: 'iso-rigid-fallback-drops-room-wall-overlap-priority',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test --test-name-pattern="rigid fallback prioritizes" test/iso-overlays.test.mjs',
because: '#651 degraded placement must preserve room ownership first, clear walls second and '
+ 'accept inter-group overlap only last. Distance-only fallback can pin a marker in masonry',
patches: [{
file: 'src/iso-overlays.ts',
find: ' return left.roomViolations - right.roomViolations\n'
+ ' || left.wallViolations - right.wallViolations\n'
+ ' || left.overlapPenalty - right.overlapPenalty\n'
+ ' || rigidOffsetOrder(left.offset, right.offset);',
replace: ' return rigidOffsetOrder(left.offset, right.offset);',
}],
},
{
id: 'iso-scene-restores-per-marker-collision-resolver',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+60
View File
@@ -146,6 +146,66 @@ test('rigid overlay groups preserve a row and use one deterministic wall displac
}
});
test('rigid overlay groups never join close markers owned by different rooms', () => {
const left = square('left', 0, 0, 100, 160, [50, 80]);
const right = square('right', 100, 0, 200, 160, [150, 80]);
const wall = { outer: [[94, 0], [106, 0], [106, 160], [94, 160]] };
const make = (id, room, x) => ({
id,
kind: 'device',
placement: placement({
floorAnchor: [x, 80], rooms: [left, right], preferredRoomId: room.id,
wallSilhouettes: [wall], footprintHalfSize: [5, 5], wallHeight: 0,
visualOffset: 0, sceneUnitsPerCssPixel: 1, camera: identityCamera,
}),
screenHalfSize: [7, 7],
});
const leftItem = make('left-device', left, 90);
const rightItem = make('right-device', right, 110);
const result = resolveIsoOverlayRigidGroups({
items: [leftItem, rightItem], rooms: [left, right], wallSilhouettes: [wall],
sceneUnitsPerCssPixel: 1, visualOffset: 0, camera: identityCamera,
});
const leftPlacement = result.placements.get(isoOverlayCollisionKey('device', leftItem.id));
const rightPlacement = result.placements.get(isoOverlayCollisionKey('device', rightItem.id));
assert.ok(leftPlacement.nudgeCss[0] < 0,
'the left-room marker clears the shared wall into its own room');
assert.ok(rightPlacement.nudgeCss[0] > 0,
'the right-room marker clears the shared wall into its own room');
assert.notDeepEqual(leftPlacement.nudgeCss, rightPlacement.nudgeCss,
'close markers across a room boundary must remain separate groups');
});
test('rigid fallback prioritizes room, then wall, then overlap inside the 48px cap', () => {
const obstacleRoom = square('obstacle-room', 40, 0, 60, 40, [50, 30]);
const targetRoom = square('target-room', 40, 0, 60, 40, [50, 30]);
const wall = { outer: [[40, 0], [60, 0], [60, 14], [40, 14]] };
const make = (id, room, y) => ({
id,
kind: 'device',
placement: placement({
floorAnchor: [50, y], rooms: [obstacleRoom, targetRoom], preferredRoomId: room.id,
wallSilhouettes: [wall], footprintHalfSize: [5, 5], wallHeight: 0,
visualOffset: 0, sceneUnitsPerCssPixel: 1, camera: identityCamera,
}),
screenHalfSize: [9, 9],
});
const obstacle = make('a-obstacle', obstacleRoom, 30);
const target = make('b-target', targetRoom, 18);
const result = resolveIsoOverlayRigidGroups({
items: [obstacle, target], rooms: [obstacleRoom, targetRoom], wallSilhouettes: [wall],
sceneUnitsPerCssPixel: 1, visualOffset: 0, camera: identityCamera,
});
const resolved = result.placements.get(isoOverlayCollisionKey('device', target.id));
assert.equal(resolved.status, 'degraded', 'the narrow room has no conflict-free placement');
assert.equal(resolved.reason, 'overlay-collision',
'after preserving room ownership and clearing the wall, overlap is the remaining debt');
assert.equal(resolved.nearWallAfter, false,
'wall clearance outranks overlap with the previously accepted group');
assert.ok(resolved.nudgeCss[1] > 0 && resolved.nudgeDistanceCss <= 48,
'the fallback moves into the room without exceeding the global cap');
});
test('group collision checks room-edge events before accepting a legal farther hint', () => {
const room = {
id: 'diagonal-strip',