fix: keep jamb margin on partition openings

Issue: #186
User-Visible: yes
This commit is contained in:
Sergey Matyunin
2026-08-19 15:43:23 +03:00
parent 804b282f5f
commit 57ba75b9da
25 changed files with 2125 additions and 1515 deletions
+41
View File
@@ -5,6 +5,7 @@ import {
openingPlacementTargets,
passagePlacementPreviewGeometry,
resolveOpeningPlacement,
resolveOpeningPlacementResult,
sameOpeningPlacementInput,
} from '../test-build/opening-placement.js';
@@ -43,6 +44,46 @@ test('Opening presets use the agreed type-specific defaults', () => {
assert.equal(openingPlacementPreset('gate', 3).lengthCm, 300);
});
test('partition placement reserves its physical half-width at both endpoints', () => {
const hosted = interval({
partitionHost: { kind: 'partition', id: 'p1' },
half: 10,
});
const left = resolve({ pointer: [0, 0], intervals: [hosted] });
const right = resolve({ pointer: [100, 0], intervals: [hosted] });
assert.equal(left.x, 25);
assert.equal(left.host.t, 0.25);
assert.equal(left.measure.labels[0].distance, 10);
assert.equal(right.x, 75);
assert.equal(right.host.t, 0.75);
assert.equal(right.measure.labels[1].distance, 10);
});
test('a partition shorter than opening plus both jambs reports a typed block', () => {
const result = resolveOpeningPlacementResult({
pointer: [24.5, 0],
preset: openingPlacementPreset('door', 1),
geometryRevision: 7,
renderedLength: 30,
intervals: [interval({
b: [49, 0], half: 10,
partitionHost: { kind: 'partition', id: 'short' },
})],
baseTolerance: 4,
bodyPointerPadding: 2,
gridStep: 10,
});
assert.equal(result.candidate, null);
assert.equal(result.jambBlockedTarget.partitionHost.id, 'short');
assert.equal(result.jambBlockedTarget.physicalHalfWidth, 10);
});
test('room-wall placement keeps its zero-jamb endpoint contract', () => {
const candidate = resolve({ pointer: [0, 0], intervals: [interval({ half: 10 })] });
assert.equal(candidate.x, 15);
assert.equal(candidate.measure.labels[0].distance, 0);
});
test('passage preview geometry follows the resolved length and standard wall depth', () => {
const geometry = passagePlacementPreviewGeometry({
renderedLength: 90,
+52
View File
@@ -6,7 +6,10 @@ import {
partitionOpeningCut,
partitionOpeningFace,
partitionOpeningHasCompositeRoomWall,
partitionOpeningJambMargin,
partitionOpeningNeedsStrictValidation,
resolvePartitionOpening,
resolvePartitionOpeningStrict,
} from '../test-build/partition-openings.js';
import { cutPartitionBody, partitionBody } from '../test-build/physical-geometry.js';
import { physicalBodyParts } from '../test-build/physical-geometry.js';
@@ -49,6 +52,55 @@ test('missing, out-of-range and too-short hosts fail dark', () => {
'does-not-fit');
});
test('strict writes reserve half the real wall depth while compat reads remain visible', () => {
assert.equal(partitionOpeningJambMargin(partition, 5, 5), 7.5);
for (const type of ['door', 'window', 'gate', 'passage']) {
const atBoundary = opening({ type, host: { kind: 'partition', id: 'p1', t: 0.225 } });
const insideJamb = opening({ type, host: { kind: 'partition', id: 'p1', t: 0.224 } });
assert.ok(resolvePartitionOpeningStrict(atBoundary, [partition], 100, 5, 5).resolved);
assert.equal(
resolvePartitionOpeningStrict(insideJamb, [partition], 100, 5, 5).reason,
'does-not-fit-jamb',
);
assert.ok(resolvePartitionOpening(insideJamb, [partition], 100, 5, 5).resolved,
`compat/read policy must not orphan a stored near-end ${type}`);
}
});
test('strict jamb policy follows thickness, scale and host direction', () => {
for (const cm of [1, 15, 100]) {
for (const [cellCm, gridPitch] of [[5, 5], [2.5, 8]]) {
for (const [a, b] of [[[0, 0], [1000, 0]], [[1000, 1000], [0, 0]]]) {
const host = { id: 'p1', a, b, cm };
const span = Math.hypot(b[0] - a[0], b[1] - a[1]);
const margin = partitionOpeningJambMargin(host, cellCm, gridPitch);
const length = 200;
const exactT = (length / 2 + margin) / span;
const candidate = opening({
length: length / 100,
host: { kind: 'partition', id: 'p1', t: exactT },
});
assert.ok(resolvePartitionOpeningStrict(
candidate, [host], 100, cellCm, gridPitch,
).resolved, `${cm}/${cellCm}/${gridPitch} exact boundary`);
assert.equal(resolvePartitionOpeningStrict({
...candidate, host: { ...candidate.host, t: exactT - 1e-5 },
}, [host], 100, cellCm, gridPitch).reason, 'does-not-fit-jamb');
}
}
}
});
test('only direct hosted geometry changes opt an existing opening into strict validation', () => {
const original = opening();
assert.equal(partitionOpeningNeedsStrictValidation(original, { ...original, type: 'window' }), false);
assert.equal(partitionOpeningNeedsStrictValidation(original, {
...original, host: { ...original.host, t: 0.4 },
}), true);
assert.equal(partitionOpeningNeedsStrictValidation(original, { ...original, length: 0.4 }), true);
assert.equal(partitionOpeningNeedsStrictValidation(null, original), true);
});
test('partition body is split by its own full-depth slot only', () => {
const body = partitionBody(partition.a, partition.b, partition.cm, 5, 5);
const resolved = resolvePartitionOpening(opening(), [partition], 100, 5, 5).resolved;