mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 05:08:53 +00:00
@@ -4,7 +4,7 @@
|
||||
*
|
||||
* npm run invariants -- --config <файл> # экспорт, config/get или сырой config
|
||||
* npm run invariants -- --config <файл> --layout <файл>
|
||||
* npm run invariants -- --config <файл> --json
|
||||
* npm run invariants -- --config <файл> --near-axis --json
|
||||
*
|
||||
* Зачем это существует. Самый дорогой класс дефектов проекта — не ошибки
|
||||
* формул, а потеря согласованности между геометрией и ссылками на неё: #253
|
||||
@@ -19,6 +19,7 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { checkOptimizeGeometry } from '../test-build/plan-geometry-preflight.js';
|
||||
import { classifyNearAxisSegment } from '../test-build/near-axis.js';
|
||||
|
||||
/** Доля шага сетки, в пределах которой запись считается лежащей на ребре. */
|
||||
const EDGE_TOLERANCE = 0.004;
|
||||
@@ -44,6 +45,42 @@ const roomPolygon = (room) => {
|
||||
|
||||
const edgesOf = (poly) => poly.map((a, index) => [a, poly[(index + 1) % poly.length]]);
|
||||
|
||||
/** Deduplicated physical near-axis segments, including both room-owner copies. */
|
||||
export function nearAxisProfile(config) {
|
||||
const spaces = [];
|
||||
let total = 0;
|
||||
const keyOf = (a, b) => {
|
||||
const ka = `${a[0]},${a[1]}`, kb = `${b[0]},${b[1]}`;
|
||||
return ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`;
|
||||
};
|
||||
for (const space of config?.spaces || []) {
|
||||
const found = new Set();
|
||||
for (const room of space?.rooms || []) {
|
||||
const poly = roomPolygon(room);
|
||||
if (!poly) continue;
|
||||
for (const [a, b] of edgesOf(poly)) {
|
||||
if (classifyNearAxisSegment(a, b)) found.add(keyOf(a, b));
|
||||
}
|
||||
}
|
||||
for (const draft of space?.room_drafts || []) {
|
||||
for (let index = 0; index + 1 < (draft?.points || []).length; index++) {
|
||||
const a = point(draft.points[index]), b = point(draft.points[index + 1]);
|
||||
if (a && b && classifyNearAxisSegment(a, b)) found.add(`draft:${draft.id}:${index}`);
|
||||
}
|
||||
}
|
||||
for (const partition of space?.partitions || []) {
|
||||
const a = point(partition?.a), b = point(partition?.b);
|
||||
if (a && b && classifyNearAxisSegment(a, b)) {
|
||||
found.add(`partition:${partition.id || keyOf(a, b)}`);
|
||||
}
|
||||
}
|
||||
const count = found.size;
|
||||
spaces.push({ spaceId: String(space?.id || ''), count });
|
||||
total += count;
|
||||
}
|
||||
return { total, spaces };
|
||||
}
|
||||
|
||||
const distToSegment = (p, a, b) => {
|
||||
const dx = b[0] - a[0], dy = b[1] - a[1];
|
||||
const len2 = dx * dx + dy * dy;
|
||||
@@ -587,7 +624,7 @@ function main(argv) {
|
||||
const configPath = arg('--config');
|
||||
if (!configPath) {
|
||||
console.error('использование: model-invariants.mjs --config <файл> [--layout <файл>]'
|
||||
+ ' [--lattice] [--json]');
|
||||
+ ' [--lattice|--near-axis] [--json]');
|
||||
return 2;
|
||||
}
|
||||
const model = readModel(readFileSync(configPath, 'utf8'));
|
||||
@@ -599,6 +636,17 @@ function main(argv) {
|
||||
else console.log(latticeReport(profile));
|
||||
return 0;
|
||||
}
|
||||
if (argv.includes('--near-axis')) {
|
||||
const profile = nearAxisProfile(model.config);
|
||||
if (argv.includes('--json')) console.log(JSON.stringify({ nearAxis: profile }, null, 2));
|
||||
else {
|
||||
console.log(`Почти осевых физических стен: ${profile.total}.`);
|
||||
for (const space of profile.spaces.filter((item) => item.count)) {
|
||||
console.log(` ${space.spaceId || '(без id)'}: ${space.count}`);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
const violations = [
|
||||
...checkReferences(model, { notes }),
|
||||
...checkWallKeys(model.config, { notes }),
|
||||
|
||||
@@ -697,9 +697,9 @@ export const MUTANTS = [
|
||||
patches: [{
|
||||
file: 'src/plan-optimizer.ts',
|
||||
find: ' const persistedConfig = canonicalizeConfigGeometry(config);\n'
|
||||
+ ' const persistedLayout = canonicalizeLayoutGeometry(aligned.layout);',
|
||||
+ ' const persistedLayout = canonicalizeLayoutGeometry(finalAligned.layout);',
|
||||
replace: ' const persistedConfig = config;\n'
|
||||
+ ' const persistedLayout = aligned.layout;',
|
||||
+ ' const persistedLayout = finalAligned.layout;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
@@ -941,6 +941,40 @@ export const MUTANTS = [
|
||||
replace: ' if (false && pointInOpaquePlanBody(sourcePoint, masonryGeometry, opaqueBodies)) {',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'near-axis-threshold-weakened',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test --test-name-pattern="#290 near-axis boundary" test/near-axis.test.mjs',
|
||||
because: 'the measured 316x1 wall must stay inside the one shared 0.25 degree drafting '
|
||||
+ 'tolerance while 316x2 and a true diagonal remain outside (#290)',
|
||||
patches: [{
|
||||
file: 'src/near-axis.ts',
|
||||
find: 'export const NEAR_AXIS_MAX_DEGREES = 0.25;',
|
||||
replace: 'export const NEAR_AXIS_MAX_DEGREES = 0.1;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'near-axis-inclusive-boundary-disabled',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test --test-name-pattern="#290 near-axis boundary" test/near-axis.test.mjs',
|
||||
because: 'the product contract includes an edge exactly on the 0.25 degree boundary (#290)',
|
||||
patches: [{
|
||||
file: 'src/near-axis.ts',
|
||||
find: ' if (minor / major > NEAR_AXIS_MAX_SLOPE) return null;',
|
||||
replace: ' if (minor / major >= NEAR_AXIS_MAX_SLOPE) return null;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'near-axis-authoring-snap-bypassed',
|
||||
guard: 'npm run bundle:sync && node demo/smoke_plan_drawing_repairs.mjs',
|
||||
because: 'the production Walls hover and click must persist the exact same straight endpoint '
|
||||
+ 'instead of merely repairing old data through Optimize (#290)',
|
||||
patches: [{
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: ' const point = anchor ? snapNearAxisEndpoint(anchor, snapped) : snapped;',
|
||||
replace: ' const point = snapped;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'optimizer-micro-interval-cleanup-disabled',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
|
||||
@@ -27,6 +27,16 @@
|
||||
|
||||
/** @type {SmokeLink[]} */
|
||||
export const SMOKE_LINKS = [
|
||||
{
|
||||
symbols: [
|
||||
'classifyNearAxisSegment', 'snapNearAxisEndpoint', 'repairNearAxisRoomWalls',
|
||||
'NEAR_AXIS_MAX_DEGREES', 'NEAR_AXIS_MAX_SLOPE',
|
||||
],
|
||||
smokes: ['smoke_plan_drawing_repairs.mjs', 'smoke_near_axis_optimize.mjs'],
|
||||
because: 'the production bundle must make the Walls hover/click candidate exact-axis, then '
|
||||
+ 'preview one deduplicated real shared wall, pass production geometry preflight, apply one '
|
||||
+ 'atomic Optimize write, reload idempotently and restore the original through one Undo (#290)',
|
||||
},
|
||||
{
|
||||
// #285: смок не произносит ни одного имени продуктового кода — он подставляет
|
||||
// геометрию реального этажа и спрашивает сам продукт через isPointInFill,
|
||||
|
||||
Reference in New Issue
Block a user