mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 04:09:17 +00:00
File diff suppressed because one or more lines are too long
Vendored
+7
-6
File diff suppressed because one or more lines are too long
+4
-1
@@ -10,7 +10,10 @@
|
||||
into stable model-v9 wall segments; all existing `cm:0` segments deliberately
|
||||
receive the same semantics, so a small number of old plans may change
|
||||
appearance or light transmission. Zero walls create no masonry, area or
|
||||
opening tunnel and cannot host an opening
|
||||
opening tunnel and cannot host an opening. Export a backup before the first
|
||||
structural save, import or **Optimize plans** that writes model v9: downgrade
|
||||
to an older version does not understand the new wall model, and restoring
|
||||
that backup is the supported way back
|
||||
([#306](https://github.com/Matysh/houseplan-card/issues/306)).
|
||||
|
||||
## v1.68.0-beta.2 — 2026-08-26
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
мигрируют в стабильные сегменты model v9; все прежние `cm:0` намеренно
|
||||
получают ту же семантику, поэтому у небольшой части старых планов может
|
||||
измениться вид или прохождение света. Нулевая стена не создаёт кладку,
|
||||
площадь или тоннель и не может содержать проём
|
||||
площадь или тоннель и не может содержать проём. Перед первым структурным
|
||||
сохранением, импортом или **«Оптимизировать планы»**, записывающим model v9,
|
||||
экспортируйте резервную копию: старая версия не понимает новую модель стен,
|
||||
и поддерживаемый путь назад — восстановление этой копии
|
||||
([#306](https://github.com/Matysh/houseplan-card/issues/306)).
|
||||
|
||||
## v1.68.0-beta.2 — 2026-08-26
|
||||
|
||||
+34
-14
@@ -7347,6 +7347,25 @@ class HouseplanCard extends LitElement {
|
||||
return this._t(`wall_model.reason.${reason}`);
|
||||
}
|
||||
|
||||
/** A legacy virtual-wall projection is still present only until its first
|
||||
* structural v9 write. Keep its atomic-failure copy distinct from generic
|
||||
* wall-model validation without persisting provenance on the new atoms. */
|
||||
private _hasLegacyZeroWallFields(config: any = this._serverCfg): boolean {
|
||||
return (config?.spaces || []).some((space: any) => (
|
||||
(Array.isArray(space?.open_spans) && space.open_spans.length > 0)
|
||||
|| (space?.rooms || []).some((room: any) => (
|
||||
Array.isArray(room?.open_to) && room.open_to.length > 0
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
private _showWallModelMigrationBlocked(error: unknown): void {
|
||||
const key = this._hasLegacyZeroWallFields()
|
||||
? 'toast.zero_wall_migration_blocked'
|
||||
: 'toast.wall_model_migration_blocked';
|
||||
this._showToast(this._t(key, { reason: this._wallModelBlockerLabel(error) }));
|
||||
}
|
||||
|
||||
private _commitPhysicalGeometry(
|
||||
name: string,
|
||||
before: SpaceGeometryState | null,
|
||||
@@ -7417,9 +7436,7 @@ class HouseplanCard extends LitElement {
|
||||
} catch (error) {
|
||||
this._clearGeometryGesture();
|
||||
this._restoreGeometryStateLocal(before);
|
||||
this._showToast(this._t('toast.wall_model_migration_blocked', {
|
||||
reason: this._wallModelBlockerLabel(error),
|
||||
}));
|
||||
this._showWallModelMigrationBlocked(error);
|
||||
return false;
|
||||
}
|
||||
let safe = false;
|
||||
@@ -7551,9 +7568,7 @@ class HouseplanCard extends LitElement {
|
||||
committedCandidate = commitWallSegmentModel(restoredCandidate).config;
|
||||
} catch (error) {
|
||||
this._restoreGeometryStateLocal(before);
|
||||
this._showToast(this._t('toast.wall_model_migration_blocked', {
|
||||
reason: this._wallModelBlockerLabel(error),
|
||||
}));
|
||||
this._showWallModelMigrationBlocked(error);
|
||||
return false;
|
||||
}
|
||||
if (physicalChanged) {
|
||||
@@ -8124,7 +8139,9 @@ class HouseplanCard extends LitElement {
|
||||
});
|
||||
if (repair.kind === 'ambiguous') {
|
||||
this._wallRepairDiagnostic = repair.proposals[0] || null;
|
||||
this._showToast(this._t('toast.wall_repair_ambiguous'));
|
||||
this._showToast(this._t(
|
||||
this._drawWallCm === 0 ? 'toast.zero_wall_ambiguous' : 'toast.wall_repair_ambiguous',
|
||||
));
|
||||
return;
|
||||
}
|
||||
if (repair.kind === 'repair' && !this._overlapRoom(repair.face.ring)) {
|
||||
@@ -8200,7 +8217,9 @@ class HouseplanCard extends LitElement {
|
||||
});
|
||||
if (result.kind === 'ambiguous') {
|
||||
this._wallRepairDiagnostic = result.proposals[0] || null;
|
||||
this._showToast(this._t('toast.wall_repair_ambiguous'));
|
||||
this._showToast(this._t(
|
||||
this._drawWallCm === 0 ? 'toast.zero_wall_ambiguous' : 'toast.wall_repair_ambiguous',
|
||||
));
|
||||
return true;
|
||||
}
|
||||
if (result.kind === 'repair') {
|
||||
@@ -8327,7 +8346,7 @@ class HouseplanCard extends LitElement {
|
||||
if (p && cmRaw === 0 && zeroWallHasOpening(sp.openings, {
|
||||
kind: 'partition', id: d.id,
|
||||
})) {
|
||||
this._showToast(this._t('toast.zero_wall_opening'));
|
||||
this._showToast(this._t('toast.zero_wall_opening_conflict'));
|
||||
return;
|
||||
}
|
||||
if (p) p.cm = cmRaw;
|
||||
@@ -11699,7 +11718,7 @@ class HouseplanCard extends LitElement {
|
||||
if (cmRaw === 0 && zeroWallHasOpening(sp.openings, {
|
||||
kind: 'partition', id: partition.id,
|
||||
})) {
|
||||
this._showToast(this._t('toast.zero_wall_opening'));
|
||||
this._showToast(this._t('toast.zero_wall_opening_conflict'));
|
||||
return;
|
||||
}
|
||||
partition.cm = cmRaw;
|
||||
@@ -11739,7 +11758,7 @@ class HouseplanCard extends LitElement {
|
||||
return point.every(Number.isFinite) && distToSegment(point, target) <= eps;
|
||||
});
|
||||
if (blocked) {
|
||||
this._showToast(this._t('toast.zero_wall_opening'));
|
||||
this._showToast(this._t('toast.zero_wall_opening_conflict'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -15657,9 +15676,7 @@ class HouseplanCard extends LitElement {
|
||||
this._optimizeReferenceContext(removeLiveMissingPositions),
|
||||
);
|
||||
} catch (error) {
|
||||
this._showToast(this._t('toast.wall_model_migration_blocked', {
|
||||
reason: this._wallModelBlockerLabel(error),
|
||||
}));
|
||||
this._showWallModelMigrationBlocked(error);
|
||||
return;
|
||||
}
|
||||
const preflight = r.changed ? this._checkOptimizeGeometry(r.config) : null;
|
||||
@@ -16917,6 +16934,9 @@ class HouseplanCard extends LitElement {
|
||||
${r.wallSegmentsMigrated ? html`<p class="alignmsg">${this._t(
|
||||
'gs.wall_segments_migrated', { n: String(r.wallSegmentsMigrated) },
|
||||
)}</p>` : nothing}
|
||||
${r.legacyZeroWallsMigrated ? html`<p class="alignmsg">${this._t(
|
||||
'gs.zero_walls_migrated', { n: String(r.legacyZeroWallsMigrated) },
|
||||
)}</p>` : nothing}
|
||||
${modelMaintenance ? html`<p class="alignmsg">${this._t('gs.optimize_changes', {
|
||||
m: String(r.migrated), c: String(r.canonicalized),
|
||||
p: String(r.coordsCanonicalized), w: String(r.wallsMerged),
|
||||
|
||||
+5
-2
@@ -534,7 +534,9 @@
|
||||
"toast.wallthick_set": "Wall thickness set",
|
||||
"toast.wallthick_cleared": "Wall thickness removed",
|
||||
"toast.physical_range": "Enter a value from {min} to {max} {unit}",
|
||||
"toast.zero_wall_opening": "Remove the opening on this wall stretch first",
|
||||
"toast.zero_wall_opening_conflict": "Remove the opening on this wall segment first.",
|
||||
"toast.zero_wall_ambiguous": "The wall segment is ambiguous. Simplify or adjust the junction.",
|
||||
"toast.zero_wall_migration_blocked": "The space was not converted: {reason}. No data was changed.",
|
||||
"toast.physical_angle": "Enter a rotation angle from 0° up to, but not including, 90°",
|
||||
"toast.physical_limit": "The space has reached the limit for this type of geometry",
|
||||
"toast.geometry_unsafe": "Change canceled: wall geometry could not be built safely.",
|
||||
@@ -815,7 +817,8 @@
|
||||
"gs.preflight_update_hint": "The card and integration versions differ — update House Plan and retry.",
|
||||
"gs.align_preflight_space": "Space {n}",
|
||||
"gs.align_preflight_more": ", and {n} more",
|
||||
"gs.optimize_changes": "Model migrations: {m}; spaces updated: {c}; noisy coordinate values removed: {p}; merged real-wall fragments: {w}; virtual fragments: {s}; independent walls: {i}.",
|
||||
"gs.optimize_changes": "Model migrations: {m}; spaces updated: {c}; noisy coordinate values removed: {p}; merged real-wall fragments: {w}; merged zero-thickness wall fragments: {s}; independent walls: {i}.",
|
||||
"gs.zero_walls_migrated": "Virtual wall spans converted: {n}.",
|
||||
"gs.wall_segments_migrated": "Wall segments stabilised: {n}.",
|
||||
"gs.optimize_lattice_summary": "Noisy coordinate values canonicalized: {n}; maximum movement: {cm} cm.",
|
||||
"gs.optimize_lattice_space": "{space}: coordinate values canonicalized: {n}; off-grid values left unchanged: {far}.",
|
||||
|
||||
+5
-2
@@ -534,7 +534,9 @@
|
||||
"toast.wallthick_set": "Толщина стены задана",
|
||||
"toast.wallthick_cleared": "Толщина стены убрана",
|
||||
"toast.physical_range": "Введите значение от {min} до {max} {unit}",
|
||||
"toast.zero_wall_opening": "Сначала удалите проём на этом участке стены",
|
||||
"toast.zero_wall_opening_conflict": "Сначала удалите проём на этом участке стены.",
|
||||
"toast.zero_wall_ambiguous": "Не удалось однозначно выбрать участок стены. Уточните геометрию узла.",
|
||||
"toast.zero_wall_migration_blocked": "Пространство не преобразовано: {reason}. Данные не изменены.",
|
||||
"toast.physical_angle": "Введите угол поворота от 0° до 90°, не включая 90°",
|
||||
"toast.physical_limit": "В пространстве достигнут лимит объектов этого типа",
|
||||
"toast.geometry_unsafe": "Изменение отменено: геометрию стен нельзя безопасно построить.",
|
||||
@@ -815,7 +817,8 @@
|
||||
"gs.preflight_update_hint": "Версии карточки и интеграции различаются — обновите House Plan и повторите.",
|
||||
"gs.align_preflight_space": "Пространство {n}",
|
||||
"gs.align_preflight_more": " и ещё {n}",
|
||||
"gs.optimize_changes": "Миграций модели: {m}; обновлено пространств: {c}; устранён шум координат: {p}; объединено отрезков реальных стен: {w}; виртуальных: {s}; независимых: {i}.",
|
||||
"gs.optimize_changes": "Миграций модели: {m}; обновлено пространств: {c}; устранён шум координат: {p}; объединено отрезков реальных стен: {w}; объединено отрезков стен нулевой толщины: {s}; независимых: {i}.",
|
||||
"gs.zero_walls_migrated": "Преобразовано виртуальных участков: {n}.",
|
||||
"gs.wall_segments_migrated": "Стабилизировано сегментов стен: {n}.",
|
||||
"gs.optimize_lattice_summary": "Канонизировано шумовых значений координат: {n}; максимальный сдвиг: {cm} см.",
|
||||
"gs.optimize_lattice_space": "{space}: канонизировано значений координат: {n}; оставлено значений вне сетки: {far}.",
|
||||
|
||||
@@ -57,9 +57,11 @@ export interface OptimizeReport extends AlignReport, SpaceReferenceReport {
|
||||
canonicalized: number;
|
||||
/** Contour-wall atoms materialised into the stable v8 catalogue. */
|
||||
wallSegmentsMigrated: number;
|
||||
/** Read-compatible legacy virtual spans converted into zero-wall atoms. */
|
||||
legacyZeroWallsMigrated: number;
|
||||
/** Redundant equal-thickness wall entries removed by canonicalisation. */
|
||||
wallsMerged: number;
|
||||
/** Touching/overlapping virtual pieces merged on the same room pair. */
|
||||
/** Touching/overlapping zero-wall atoms merged on the same room pair. */
|
||||
spansMerged: number;
|
||||
/** Collinear independent-wall records collapsed into one (#229). */
|
||||
partitionsMerged: number;
|
||||
@@ -480,6 +482,7 @@ export function optimizePlans(
|
||||
} : { ...aligned.report };
|
||||
|
||||
let wallsMerged = 0;
|
||||
let legacyZeroWallsMigrated = 0;
|
||||
let spansMerged = 0;
|
||||
let partitionsMerged = 0;
|
||||
let partitionsReconciled = 0;
|
||||
@@ -513,7 +516,9 @@ export function optimizePlans(
|
||||
// the final identity barrier assign stable catalogue IDs. No production
|
||||
// maintenance pass writes open_spans/open_to back into the candidate.
|
||||
const knownZeroKeys = new Set(sourceZeroWalls.map((wall) => wall.key));
|
||||
for (const line of legacyZeroContourLines(before, oldModel.rooms, NORM_W, eps)) {
|
||||
const legacyZeroLines = legacyZeroContourLines(before, oldModel.rooms, NORM_W, eps);
|
||||
legacyZeroWallsMigrated += legacyZeroLines.length;
|
||||
for (const line of legacyZeroLines) {
|
||||
const a = [line[0] / NORM_W, line[1] / NORM_W];
|
||||
const b = [line[2] / NORM_W, line[3] / NORM_W];
|
||||
const key = wallKey(a, b, GRID_STEP_N);
|
||||
@@ -693,6 +698,7 @@ export function optimizePlans(
|
||||
glowRoomsMigrated: changed ? migration.glowRooms : 0,
|
||||
canonicalized: changed ? canonicalized : 0,
|
||||
wallSegmentsMigrated: changed ? wallSegmentsMigrated : 0,
|
||||
legacyZeroWallsMigrated: changed ? legacyZeroWallsMigrated : 0,
|
||||
wallsMerged: changed ? wallsMerged : 0,
|
||||
spansMerged: changed ? spansMerged : 0,
|
||||
partitionsMerged: changed ? partitionsMerged : 0,
|
||||
|
||||
+27
-2
@@ -51,12 +51,15 @@ test('issue 251 unavailable controls toast has exact singular and plural copy',
|
||||
test('Optimize distinguishes updated spaces from cleaned coordinate noise', () => {
|
||||
assert.equal(
|
||||
en['gs.optimize_changes'],
|
||||
'Model migrations: {m}; spaces updated: {c}; noisy coordinate values removed: {p}; merged real-wall fragments: {w}; virtual fragments: {s}; independent walls: {i}.',
|
||||
'Model migrations: {m}; spaces updated: {c}; noisy coordinate values removed: {p}; merged real-wall fragments: {w}; merged zero-thickness wall fragments: {s}; independent walls: {i}.',
|
||||
);
|
||||
assert.equal(
|
||||
ru['gs.optimize_changes'],
|
||||
'Миграций модели: {m}; обновлено пространств: {c}; устранён шум координат: {p}; объединено отрезков реальных стен: {w}; виртуальных: {s}; независимых: {i}.',
|
||||
'Миграций модели: {m}; обновлено пространств: {c}; устранён шум координат: {p}; объединено отрезков реальных стен: {w}; объединено отрезков стен нулевой толщины: {s}; независимых: {i}.',
|
||||
);
|
||||
assert.equal(en['gs.zero_walls_migrated'], 'Virtual wall spans converted: {n}.');
|
||||
assert.equal(ru['gs.zero_walls_migrated'], 'Преобразовано виртуальных участков: {n}.');
|
||||
assert.match(cardSource, /gs\.zero_walls_migrated/);
|
||||
assert.match(cardSource, /p: String\(r\.coordsCanonicalized\)/);
|
||||
// #229: the independent-wall counter is reported, not silently accumulated
|
||||
assert.match(cardSource, /i: String\(r\.partitionsMerged\)/);
|
||||
@@ -66,6 +69,28 @@ test('Optimize distinguishes updated spaces from cleaned coordinate noise', () =
|
||||
/d\.report\.coordsCanonicalized \+ d\.report\.latticeCoordinatesCanonicalized/);
|
||||
});
|
||||
|
||||
test('issue 306 zero-wall failures have dedicated symmetric copy', () => {
|
||||
const expected = {
|
||||
'toast.zero_wall_opening_conflict': [
|
||||
'Remove the opening on this wall segment first.',
|
||||
'Сначала удалите проём на этом участке стены.',
|
||||
],
|
||||
'toast.zero_wall_ambiguous': [
|
||||
'The wall segment is ambiguous. Simplify or adjust the junction.',
|
||||
'Не удалось однозначно выбрать участок стены. Уточните геометрию узла.',
|
||||
],
|
||||
'toast.zero_wall_migration_blocked': [
|
||||
'The space was not converted: {reason}. No data was changed.',
|
||||
'Пространство не преобразовано: {reason}. Данные не изменены.',
|
||||
],
|
||||
};
|
||||
for (const [key, [english, russian]] of Object.entries(expected)) {
|
||||
assert.equal(en[key], english);
|
||||
assert.equal(ru[key], russian);
|
||||
assert.match(cardSource, new RegExp(key.replaceAll('.', '\\.')));
|
||||
}
|
||||
});
|
||||
|
||||
test('issue 291 Optimize reports lattice cleanup separately in both languages', () => {
|
||||
assert.equal(
|
||||
en['gs.optimize_lattice_summary'],
|
||||
|
||||
@@ -47,7 +47,8 @@ const assertNoPersistedChanges = (result) => {
|
||||
for (const field of [
|
||||
'moved', 'coordsCanonicalized', 'rotated', 'removedDrafts', 'migrated',
|
||||
'glowSpacesMigrated', 'glowRoomsMigrated', 'canonicalized', 'wallsMerged',
|
||||
'spansMerged', 'partitionsMerged', 'partitionsReconciled', 'openingsRehosted',
|
||||
'legacyZeroWallsMigrated', 'spansMerged', 'partitionsMerged',
|
||||
'partitionsReconciled', 'openingsRehosted',
|
||||
'redundantDraftsRemoved',
|
||||
'wallsStraightened',
|
||||
'spaceRefsRemapped', 'roomRefsRemapped',
|
||||
@@ -596,6 +597,7 @@ test('optimizePlans migrates, aligns and canonicalises idempotently', () => {
|
||||
const sharedIds = left.wall_ids.filter((id) => right.wall_ids.includes(id));
|
||||
assert.ok(sharedIds.some((id) => first.config.spaces[0].wall_segments
|
||||
.some((segment) => segment.id === id && segment.cm === 0)));
|
||||
assert.equal(first.report.legacyZeroWallsMigrated, 1);
|
||||
assert.equal(first.config.spaces[0].walls.length, 1);
|
||||
assert.equal(first.layout.m1.x % S, 0);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user