Files
houseplan-card/demo/wall-draw-click-harness.mjs
Claudeandclaude[bot] 66ae952c2a refactor(editor): диалог «Оптимизировать планы» — свой модуль с узким портом (#642)
Первый вынос из монолита по образцу live-*/RadarSetupController.
src/optimize-plans-dialog.ts: OptimizePlansDialogState, чистые
preflightDiagnostics/preflightVersionsDiffer и класс OptimizePlansDialog
(open/preview/toggleLivePositions/run/copyDiagnostics/
reportPreflightFailure/render, clipboardFallback) за портом из 18 членов.
Модуль не знает HouseplanEditorHostPort; импортирует его только рантайм,
карточка берёт тип.

Инлайн-фолбэк буфера обмена — WeakMap по объекту диалога
(CODE-REVIEW-295-r1 M2), дедуп dev-лога — поле класса: из карточки и порта
ушли _preflightClipboardFallback и _reportedPreflightFingerprint, пять
делегатов и две стрелки-заглушки. Харнесс (13 смоков, wall-draw-click,
golden) зовёт card._editorRuntime.optimizePlans.* — меняется только адрес.

11 текстовых утверждений i18n.test.mjs о разметке и тосте диалога и гварды
8 мутантов переехали в юнит test/optimize-plans-dialog.test.mjs
(test-build); новый мутант optimize-dialog-imports-host-port.

monolith-baseline: delegates 159→154, portMembers 350→348,
hostRefs 4948→4869, portPrivates 96→94, harnessPrivates 107→101,
bundleBytes 2499647→2499182. Потолки ядер опущены на выигрыш;
потолок lazy editor gzip пересчитан (+555 Б gzip при −465 Б сырых).

Issue: #642
User-Visible: no
2026-09-24 08:00:58 +00:00

220 lines
9.7 KiB
JavaScript

import { makeWallDrawClickFixture, WALL_DRAW_CLICK_POINTS } from './fixtures/wall-draw-click.mjs';
export async function installWallDrawClickHarness(page) {
await page.evaluate(() => {
const card = window.__card;
if (!window.__wallDrawOriginals) {
window.__wallDrawOriginals = {
physical: card._checkSpacePhysicalGeometry.bind(card),
junction: card._junctionLimitViolations.bind(card),
save: card._saveConfigDebounced,
genericCommit: card._editorRuntime._commitPhysicalGeometry.bind(card._editorRuntime),
finalize: card._editorRuntime._finalizeWallChainPartitions.bind(card._editorRuntime),
};
card._editorRuntime._finalizeWallChainPartitions = (...args) => {
if (window.__wallDrawMetrics) window.__wallDrawMetrics.wallFinishBarriers += 1;
return window.__wallDrawOriginals.finalize(...args);
};
card._editorRuntime._commitPhysicalGeometry = (name, before, ...rest) => {
const metrics = window.__wallDrawMetrics;
if (metrics && name === card._t('history.wall_segment')) {
const finishCommit = rest[1] === false;
if (finishCommit) metrics.wallFinishCommits += 1;
else metrics.wallGenericFallbacks += 1;
if (!finishCommit && !metrics.fallbackSample) {
const space = card._serverCfg?.spaces?.find((item) => item.id === before?.spaceId);
const stable = (value) => JSON.stringify(value ?? null);
const arrays = [
'rooms', 'openings', 'walls', 'wall_segments', 'open_spans',
'partitions', 'wall_columns', 'decor',
];
metrics.fallbackSample = JSON.parse(JSON.stringify({
modelVersion: card._serverCfg?.model_version,
chainId: card._activeWallChainId,
activePartitionIds: card._activeWallChainPartitionIds,
beforePartitions: before?.partitions,
afterPartitions: space?.partitions,
beforeTransform: before?.plan_transform,
afterTransform: Object.fromEntries([
'plan_x', 'plan_y', 'plan_scale', 'plan_scale_x', 'plan_scale_y', 'plan_angle',
].filter((key) => space?.[key] !== undefined).map((key) => [key, space[key]])),
changedArrays: arrays.filter((key) =>
stable(before?.[key] ?? []) !== stable(space?.[key] ?? [])),
}));
}
}
return window.__wallDrawOriginals.genericCommit(name, before, ...rest);
};
card._checkSpacePhysicalGeometry = (config, ...rest) => {
const metrics = window.__wallDrawMetrics;
if (metrics) {
const local = config?.spaces?.length === 1;
metrics[local ? 'localPhysicalChecks' : 'fullSpacePhysicalChecks'] += 1;
if (local) {
const space = config.spaces[0] || {};
const count = ['wall_segments', 'partitions', 'wall_columns']
.reduce((sum, key) => sum + (space[key]?.length || 0), space.rooms?.length || 0);
metrics.localProofMaxObjects = Math.max(metrics.localProofMaxObjects, count);
}
}
return window.__wallDrawOriginals.physical(config, ...rest);
};
card._junctionLimitViolations = (config, spaceId, geometry, roomIds) => {
const metrics = window.__wallDrawMetrics;
if (metrics) {
metrics.junctionPasses += 1;
if (geometry) metrics.junctionArtifactPasses += 1;
}
return window.__wallDrawOriginals.junction(config, spaceId, geometry, roomIds);
};
const countedSave = () => {
if (window.__wallDrawMetrics) window.__wallDrawMetrics.configWrites += 1;
};
countedSave.cancel = () => {};
countedSave.flush = () => {};
card._saveConfigDebounced = countedSave;
}
});
}
export async function resetWallDrawClickHarness(page, remoteVariant = false) {
const fixture = makeWallDrawClickFixture(remoteVariant);
return page.evaluate(async (config) => {
const card = window.__card;
card._clearGeometryGesture();
card._serverCfg = JSON.parse(JSON.stringify(config));
card._space = 'edited'; card._layout = {};
card._cfgEpoch += 1; card._modelCache = null; card._wallUnionCache = null;
card._physicalBodiesCache = null; card._frame = null;
card._pendingPhysicalWrites.clear(); card._geometryHistory.clear();
card._activeWallChainId = null; card._activeWallChainPartitionIds = [];
card._path = []; card._wallChainSegmentCms = []; card._wallChainRedo = [];
card._toast = ''; card._mode = 'plan'; card._tool = 'draw'; card._drawWallField = '15';
window.__wallDrawMetrics = {
fullSpacePhysicalChecks: 0, localPhysicalChecks: 0,
junctionPasses: 0, junctionArtifactPasses: 0,
configWrites: 0, localProofMaxObjects: 0,
wallGenericFallbacks: 0, fallbackSample: null,
wallFinishCommits: 0,
wallFinishBarriers: 0,
};
card.requestUpdate(); await card.updateComplete;
return {
spaces: card._serverCfg.spaces.length,
rooms: card._curSpaceCfg.rooms.length,
positiveSegments: card._curSpaceCfg.wall_segments.filter((item) => item.cm > 0).length,
savedPartitions: card._curSpaceCfg.partitions.filter(
(item) => item.id.startsWith('saved-partition-'),
).length,
};
}, fixture);
}
export async function runWallDrawClickChain(page) {
return page.evaluate(async (normalizedPoints) => {
const card = window.__card;
const metrics = window.__wallDrawMetrics;
const stage = (card.shadowRoot || card.renderRoot).querySelector('.stage');
const eventAt = (normalized) => {
const point = normalized.map((value) => value * 1000);
const rect = stage.getBoundingClientRect();
const view = card._viewOr(card._baseVb());
return new MouseEvent('click', {
clientX: rect.left + ((point[0] - view.x) / view.w) * rect.width,
clientY: rect.top + ((point[1] - view.y) / view.h) * rect.height,
bubbles: true,
});
};
const snapshots = () => ({
...metrics, history: card._geometryHistory.size,
pathPoints: card._path.length,
});
const delta = (after, before) => Object.fromEntries(
Object.keys(after).map((key) => [key, after[key] - before[key]]),
);
const firstBefore = snapshots();
card._markupClick(eventAt(normalizedPoints[0]));
const first = delta(snapshots(), firstBefore);
const times = []; const clicks = [];
for (const normalized of normalizedPoints.slice(1)) {
const before = snapshots();
const started = performance.now();
card._markupClick(eventAt(normalized));
times.push(performance.now() - started);
clicks.push(delta(snapshots(), before));
}
const ids = [...card._activeWallChainPartitionIds];
const terminalBefore = snapshots();
card._activateMarkupTool('select');
const terminal = delta(snapshots(), terminalBefore);
await card.updateComplete;
return {
first, clicks, times, ids,
terminal,
terminalPartitionCount: card._curSpaceCfg.partitions?.length || 0,
activeCleared: !card._activeWallChainId
&& card._activeWallChainPartitionIds.length === 0 && card._path.length === 0,
metrics: { ...metrics },
};
}, WALL_DRAW_CLICK_POINTS);
}
/** A separate completed straight chain: unlike terminal clicks, finish owns
* the bounded lossless merge/reconcile barrier introduced by #477. */
export async function runWallDrawFinishProfile(page) {
return page.evaluate(async (normalizedPoints) => {
const card = window.__card;
const metrics = window.__wallDrawMetrics;
const stage = (card.shadowRoot || card.renderRoot).querySelector('.stage');
const eventAt = (normalized) => {
const point = normalized.map((value) => value * 1000);
const rect = stage.getBoundingClientRect();
const view = card._viewOr(card._baseVb());
return new MouseEvent('click', {
clientX: rect.left + ((point[0] - view.x) / view.w) * rect.width,
clientY: rect.top + ((point[1] - view.y) / view.h) * rect.height,
bubbles: true,
});
};
const snapshots = () => ({
fullSpacePhysicalChecks: metrics.fullSpacePhysicalChecks,
localPhysicalChecks: metrics.localPhysicalChecks,
junctionPasses: metrics.junctionPasses,
junctionArtifactPasses: metrics.junctionArtifactPasses,
configWrites: metrics.configWrites,
wallGenericFallbacks: metrics.wallGenericFallbacks,
wallFinishCommits: metrics.wallFinishCommits,
wallFinishBarriers: metrics.wallFinishBarriers,
history: card._geometryHistory.size,
});
const delta = (after, before) => Object.fromEntries(
Object.keys(after).map((key) => [key, after[key] - before[key]]),
);
for (const point of normalizedPoints) card._markupClick(eventAt(point));
const chainIds = [...card._activeWallChainPartitionIds];
const before = snapshots();
const started = performance.now();
card._activateMarkupTool('select');
const finishMs = performance.now() - started;
const terminal = delta(snapshots(), before);
card._editorRuntime.optimizePlans.preview(false);
const optimizeChanged = card._alignDialog?.changed;
card._alignDialog = null;
await card.updateComplete;
const surviving = (card._curSpaceCfg.partitions || []).filter(
(partition) => chainIds.includes(partition.id),
);
return {
finishMs, terminal, optimizeChanged,
chainIds, surviving,
terminalPartitionCount: card._curSpaceCfg.partitions?.length || 0,
activeCleared: !card._activeWallChainId
&& card._activeWallChainPartitionIds.length === 0 && card._path.length === 0,
metrics: { ...metrics },
};
}, [[152, 200], [176, 200], [200, 200], [224, 200]]
.map(([x, y]) => [x / 240, y / 240]));
}