mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 19:28:46 +00:00
fix: keep the active chain axis and nodes above the wall bodies (#307)
Каждый клик персистит сегмент цепочки в room_drafts, и его непрозрачная кладка рисовалась поверх markup-слоя — жёлтая ось и узлы исчезали на уже поставленных частях. Разметка активной цепочки вынесена в свой слой между телами стен и снап-оверлеем: ось и узлы видны, снап-геометрия не тронута, самопривязка по-прежнему запрещена. Смок с пиксельными пробами падает на прежнем порядке слоёв. Issue: #307 User-Visible: yes
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Issue #307: the active wall-chain axis and nodes must stay visible on
|
||||
* already-placed segments. Every click persists the segment into room_drafts,
|
||||
* whose opaque masonry paints above the markup layer; the chain ink therefore
|
||||
* lives in its own layer between the wall bodies and the snap overlay.
|
||||
*/
|
||||
import { launch, check, checkAll, finish } from './serve.mjs';
|
||||
|
||||
const { page, browser } = await launch({ width: 900, height: 820 }, 1);
|
||||
|
||||
const out = await page.evaluate(async () => {
|
||||
const result = {};
|
||||
const card = window.__card;
|
||||
const root = () => card.shadowRoot || card.renderRoot;
|
||||
const update = async () => { card.requestUpdate(); await card.updateComplete; };
|
||||
const NORM_W = 1000;
|
||||
const chain = [[0.2, 0.3], [0.5, 0.3], [0.5, 0.55]];
|
||||
const cfg = {
|
||||
spaces: [{
|
||||
id: 'ink', title: 'Ink', cell_cm: 5, view_box: [0, 0, 1, 0.7],
|
||||
rooms: [{ id: 'room', name: 'Room', area: null,
|
||||
poly: [[0.1, 0.1], [0.9, 0.1], [0.9, 0.25], [0.1, 0.25]] }],
|
||||
room_drafts: [{ id: 'draft-307', points: chain,
|
||||
segments: [{ cm: 20 }, { cm: 20 }] }],
|
||||
}],
|
||||
markers: [], settings: {},
|
||||
};
|
||||
card._serverCfg = JSON.parse(JSON.stringify(cfg));
|
||||
card._layout = {};
|
||||
card._space = 'ink';
|
||||
card._modelCache = null;
|
||||
card._frame = null;
|
||||
card._cfgEpoch++;
|
||||
card._setMode('plan');
|
||||
card._tool = 'draw';
|
||||
card._path = chain.map((p) => [p[0] * NORM_W, p[1] * NORM_W]);
|
||||
card._activeDraftId = 'draft-307';
|
||||
card._draftSegmentCms = [20, 20];
|
||||
card._clearPlanSnapHover();
|
||||
await update();
|
||||
|
||||
const stage = root().querySelector('.stage');
|
||||
const rect = stage.getBoundingClientRect();
|
||||
const view = card._viewOr(card._baseVb());
|
||||
stage.dispatchEvent(new PointerEvent('pointermove', {
|
||||
clientX: rect.left + ((0.75 * NORM_W - view.x) / view.w) * rect.width,
|
||||
clientY: rect.top + ((0.55 * NORM_W - view.y) / view.h) * rect.height,
|
||||
bubbles: true, pointerId: 71, pointerType: 'mouse',
|
||||
}));
|
||||
await update();
|
||||
|
||||
const pathline = root().querySelector('polyline.pathline');
|
||||
const vertices = [...root().querySelectorAll('circle.vertex')];
|
||||
const bodies = [...root().querySelectorAll('.wallbody-fill')];
|
||||
const overlay = root().querySelector('[data-hp="plan-snap-overlay"]');
|
||||
result.pathlinePresent = !!pathline;
|
||||
result.vertexCount = vertices.length === 3;
|
||||
result.rubberBandAxis = !!root().querySelector('line.active-axis');
|
||||
result.rubberBandVertex = !!root().querySelector('circle.active-vertex');
|
||||
// AC2: document order — wall bodies → chain ink → snap overlay.
|
||||
const after = (a, b) => !!(a && b
|
||||
&& (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING));
|
||||
result.draftHasBody = bodies.length > 0;
|
||||
result.inkAboveBodies = bodies.every((body) => after(body, pathline));
|
||||
result.inkBelowSnapOverlay = after(pathline, overlay);
|
||||
// Pixel probes read the composited stage: the persisted segment's midpoint
|
||||
// must show the yellow axis, the shared chain node must show a node dot.
|
||||
return result;
|
||||
});
|
||||
|
||||
// AC1: pixel colours at the persisted segment's axis midpoint and chain node.
|
||||
// The probes are mapped to CSS right before the shot: the draw toolbar mounts
|
||||
// above the stage and shifts the SVG after the first render.
|
||||
const probeInfo = await page.evaluate(async () => {
|
||||
const card = window.__card;
|
||||
const root = card.shadowRoot || card.renderRoot;
|
||||
// The stage camera keeps animating after the first render; wait until the
|
||||
// screen matrix is stable so the probes match the screenshot that follows.
|
||||
const svgEl = root.querySelector('polyline.pathline').ownerSVGElement;
|
||||
let last = '';
|
||||
for (let i = 0; i < 40; i++) {
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
const m = svgEl.getScreenCTM();
|
||||
const key = [m.a, m.b, m.c, m.d, m.e, m.f].join(',');
|
||||
if (key === last) break;
|
||||
last = key;
|
||||
}
|
||||
const stage = root.querySelector('.stage');
|
||||
const rect = stage.getBoundingClientRect();
|
||||
const pathline = root.querySelector('polyline.pathline');
|
||||
const svg = pathline.ownerSVGElement;
|
||||
const toCss = ([ux, uy]) => {
|
||||
const point = new DOMPoint(ux, uy).matrixTransform(svg.getScreenCTM());
|
||||
return [point.x - rect.left, point.y - rect.top];
|
||||
};
|
||||
return {
|
||||
probes: { axisMid: toCss([350, 300]), node: toCss([500, 300]) },
|
||||
rect: { width: rect.width, height: rect.height },
|
||||
};
|
||||
});
|
||||
const shot = await page.locator('.stage').first().screenshot({ animations: 'disabled' });
|
||||
const pixels = await page.evaluate(async ([bytes, info]) => {
|
||||
const bitmap = await createImageBitmap(new Blob([new Uint8Array(bytes)], { type: 'image/png' }));
|
||||
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(bitmap, 0, 0);
|
||||
const scaleX = bitmap.width / info.rect.width;
|
||||
const scaleY = bitmap.height / info.rect.height;
|
||||
const at = ([cx, cy]) => {
|
||||
const d = ctx.getImageData(Math.round(cx * scaleX), Math.round(cy * scaleY), 1, 1).data;
|
||||
return [d[0], d[1], d[2]];
|
||||
};
|
||||
const yellowish = ([r, g, b]) => r > 200 && g > 130 && g < 220 && b < 120;
|
||||
// The node dot is tiny at this zoom: its centre may land on the dark rim
|
||||
// (#4a2800) instead of the yellow fill. Warm ink of either kind passes;
|
||||
// masonry (cold white) and the background (blue-tinted) do not.
|
||||
const warmInk = ([r, g, b]) => r > 110 && r > g && g > b && b < 130;
|
||||
return {
|
||||
axisMidYellow: yellowish(at(info.probes.axisMid)),
|
||||
nodeYellow: warmInk(at(info.probes.node)),
|
||||
axisMidRgb: at(info.probes.axisMid),
|
||||
nodeRgb: at(info.probes.node),
|
||||
};
|
||||
}, [[...shot], probeInfo]);
|
||||
|
||||
checkAll(out);
|
||||
check('axisMidYellow', pixels.axisMidYellow, true);
|
||||
check('nodeYellow', pixels.nodeYellow, true);
|
||||
console.log('pixel probes:', JSON.stringify(pixels));
|
||||
await finish(browser);
|
||||
Vendored
+12
-8
File diff suppressed because one or more lines are too long
@@ -2,6 +2,12 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- The active wall chain keeps its axis and node markers visible on already
|
||||
placed segments while drawing. The chain ink now paints above the wall
|
||||
masonry (and below the snap overlay), so each click no longer buries the
|
||||
yellow axis under the freshly persisted wall body
|
||||
([#307](https://github.com/Matysh/houseplan-card/issues/307)).
|
||||
|
||||
## v1.67.0-rc.3 — 2026-08-25
|
||||
|
||||
- Wall junctions no longer leave white wedges and notches. A node where three
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
|
||||
## Не выпущено
|
||||
|
||||
- Во время рисования цепочки стен осевая линия и узлы остаются видимыми на уже
|
||||
поставленных сегментах. Разметка цепочки теперь рисуется поверх кладки стен
|
||||
(и под снап-оверлеем), поэтому каждый клик больше не прячет жёлтую ось под
|
||||
только что записанным телом стены
|
||||
([#307](https://github.com/Matysh/houseplan-card/issues/307)).
|
||||
|
||||
## v1.67.0-rc.3 — 2026-08-25
|
||||
|
||||
- Стыки стен больше не оставляют белых клиньев и зазубрин. Узел, где сходятся
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"version": 1,
|
||||
"fixture": "synthetic-only",
|
||||
"chromium": "151.0.7922.34",
|
||||
"sourceFingerprint": "fd3fbbc336f92644cfeff177f4d36c9b16cd427ec91977e40432ccd54b735c93",
|
||||
"sourceFingerprint": "e9ccc3f9aecc3ce4de775233b9e31aedca19c8a474d9839d96a82ebd79d0f272",
|
||||
"captureScriptSha256": "ce2e9542fed9dade3085be87d16f69adb2ac8262893ad78ad966b1b9673f2983",
|
||||
"command": "npm run build && node demo/docs/capture.mjs",
|
||||
"scenarios": {
|
||||
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "fd3fbbc336f92644cfeff177f4d36c9b16cd427ec91977e40432ccd54b735c93",
|
||||
"sourceSha256": "e9ccc3f9aecc3ce4de775233b9e31aedca19c8a474d9839d96a82ebd79d0f272",
|
||||
"imageSha256": "0ca982495a7749907c48ebbddfd089c54385a4347f79f64e94af2c5a8d5c03eb"
|
||||
},
|
||||
"view-touch": {
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "fd3fbbc336f92644cfeff177f4d36c9b16cd427ec91977e40432ccd54b735c93",
|
||||
"sourceSha256": "e9ccc3f9aecc3ce4de775233b9e31aedca19c8a474d9839d96a82ebd79d0f272",
|
||||
"imageSha256": "1c8e28ff34736b936ba7c9ec047d1da27aaf6fbb01411155f2d5c05282c02c41"
|
||||
},
|
||||
"space-create": {
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "fd3fbbc336f92644cfeff177f4d36c9b16cd427ec91977e40432ccd54b735c93",
|
||||
"sourceSha256": "e9ccc3f9aecc3ce4de775233b9e31aedca19c8a474d9839d96a82ebd79d0f272",
|
||||
"imageSha256": "329750c81a7a958c4fd105b2c80f85dce55ed87df9ddf0ea08084e4428e85f04"
|
||||
},
|
||||
"room-contour-close": {
|
||||
@@ -47,7 +47,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "fd3fbbc336f92644cfeff177f4d36c9b16cd427ec91977e40432ccd54b735c93",
|
||||
"sourceSha256": "e9ccc3f9aecc3ce4de775233b9e31aedca19c8a474d9839d96a82ebd79d0f272",
|
||||
"imageSha256": "ff2e903e548423aa0aa52680e20b8ad951c704e01130cb4b180222015a241276"
|
||||
},
|
||||
"plan-context-tray": {
|
||||
@@ -58,7 +58,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "fd3fbbc336f92644cfeff177f4d36c9b16cd427ec91977e40432ccd54b735c93",
|
||||
"sourceSha256": "e9ccc3f9aecc3ce4de775233b9e31aedca19c8a474d9839d96a82ebd79d0f272",
|
||||
"imageSha256": "ff55925f079411023442e7e7dd5ed528c331aaa3fd3fc2fbbf3b1aaf34a85718"
|
||||
},
|
||||
"device-editor": {
|
||||
@@ -69,7 +69,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "fd3fbbc336f92644cfeff177f4d36c9b16cd427ec91977e40432ccd54b735c93",
|
||||
"sourceSha256": "e9ccc3f9aecc3ce4de775233b9e31aedca19c8a474d9839d96a82ebd79d0f272",
|
||||
"imageSha256": "6b9cd281450e7782cff292f11efc58d227bedd0df86afc3be987feb2edec9dc3"
|
||||
},
|
||||
"device-display-preview": {
|
||||
@@ -80,7 +80,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "fd3fbbc336f92644cfeff177f4d36c9b16cd427ec91977e40432ccd54b735c93",
|
||||
"sourceSha256": "e9ccc3f9aecc3ce4de775233b9e31aedca19c8a474d9839d96a82ebd79d0f272",
|
||||
"imageSha256": "6f628abe6b89d7c4b352e6d73ac0b126526f6fdba5927bb774c5b884e484ca19"
|
||||
},
|
||||
"background-editor": {
|
||||
@@ -91,7 +91,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "fd3fbbc336f92644cfeff177f4d36c9b16cd427ec91977e40432ccd54b735c93",
|
||||
"sourceSha256": "e9ccc3f9aecc3ce4de775233b9e31aedca19c8a474d9839d96a82ebd79d0f272",
|
||||
"imageSha256": "a1d1a905f1410f73231d2ea5d1dfbd8ae79c2fe8b0a79cff87ebb977ac7a3103"
|
||||
},
|
||||
"room-card": {
|
||||
@@ -102,7 +102,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "fd3fbbc336f92644cfeff177f4d36c9b16cd427ec91977e40432ccd54b735c93",
|
||||
"sourceSha256": "e9ccc3f9aecc3ce4de775233b9e31aedca19c8a474d9839d96a82ebd79d0f272",
|
||||
"imageSha256": "48a5685039a5e0de2a28190d857b193c86e2e63606e3c7a8c40fc9d2b190da4e"
|
||||
},
|
||||
"device-info": {
|
||||
@@ -113,7 +113,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "fd3fbbc336f92644cfeff177f4d36c9b16cd427ec91977e40432ccd54b735c93",
|
||||
"sourceSha256": "e9ccc3f9aecc3ce4de775233b9e31aedca19c8a474d9839d96a82ebd79d0f272",
|
||||
"imageSha256": "29a342811aaedaebc54ba337792d60cb454a7cf4c3063b210f6c4bafe0a4d87b"
|
||||
}
|
||||
}
|
||||
|
||||
+34
-12
@@ -17578,6 +17578,12 @@ class HouseplanCard extends LitElement {
|
||||
${''/* Static architectural axes and endpoints are a Plan-editor
|
||||
invariant. Tool-specific hover/snap state is resolved inside
|
||||
the overlay and remains exclusive to Walls. */}
|
||||
${''/* Active chain ink paints above the wall bodies: persisted
|
||||
draft segments already own opaque masonry, which would
|
||||
otherwise cover the chain axis and nodes (#307). It stays
|
||||
below the snap overlay so snap highlights keep priority. */}
|
||||
${this._markup ? svg`<g class="hp-editor-only-layer"
|
||||
opacity="${modeVisual?.editorWeight ?? 1}">${this._renderActiveChainInk()}</g>` : nothing}
|
||||
${this._markup ? svg`<g class="hp-editor-only-layer"
|
||||
opacity="${modeVisual?.editorWeight ?? 1}">${this._renderPlanSnapOverlay()}</g>` : nothing}
|
||||
${disp.hideOpenings && !this._markup
|
||||
@@ -20025,18 +20031,6 @@ class HouseplanCard extends LitElement {
|
||||
x1=${repairPreview.from[0]} y1=${repairPreview.from[1]}
|
||||
x2=${repairPreview.to[0]} y2=${repairPreview.to[1]}
|
||||
aria-hidden="true" pointer-events="none"></line>` : nothing}
|
||||
${path.length > 1
|
||||
? svg`<polyline class="pathline" points="${path.map((p) => p.join(',')).join(' ')}"></polyline>`
|
||||
: nothing}
|
||||
${path.length && this._cursorPt && this._tool === 'draw' && !this._contourClosed
|
||||
? svg`<line class="active-axis" x1="${path[path.length - 1][0]}" y1="${path[path.length - 1][1]}"
|
||||
x2="${this._cursorPt[0]}" y2="${this._cursorPt[1]}" aria-hidden="true"></line>
|
||||
${!this._activePlanSnapCandidate && !this._activePlanSnapConflicts.length
|
||||
? svg`<circle class="active-vertex" cx="${this._cursorPt[0]}" cy="${this._cursorPt[1]}"
|
||||
r="${gridVisualUnits(g * 0.22, this._cellCm)}" aria-hidden="true"></circle>` : nothing}`
|
||||
: nothing}
|
||||
${path.map((p, i) => svg`<circle class="vertex ${i === 0 ? 'first' : ''}"
|
||||
cx="${p[0]}" cy="${p[1]}" r="${gridVisualUnits(g * 0.22, this._cellCm)}"></circle>`)}
|
||||
${this._tool === 'split' && this._splitSel?.pts?.length
|
||||
? svg`${this._splitSel.pts.length > 1
|
||||
? svg`<polyline class="pathline" points="${this._splitSel.pts.map((p) => p.join(',')).join(' ')}"></polyline>`
|
||||
@@ -20051,6 +20045,34 @@ class HouseplanCard extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Axis and node ink of the active wall/room chain. Every click persists the
|
||||
* segment into `room_drafts`, whose opaque masonry paints ABOVE the markup
|
||||
* layer, so drawing this ink inside `_renderMarkupLayer` left already-placed
|
||||
* segments looking like bare walls (#307). The chain ink therefore paints in
|
||||
* its own layer after the wall bodies and before the snap overlay: yellow
|
||||
* "work in progress" styling stays distinct from finished walls, and the
|
||||
* snap geometry keeps excluding the active draft (no self-snapping).
|
||||
*/
|
||||
private _renderActiveChainInk(): TemplateResult {
|
||||
const path = this._path;
|
||||
const g = this._gridPitch;
|
||||
return svg`
|
||||
${path.length > 1
|
||||
? svg`<polyline class="pathline" points="${path.map((p) => p.join(',')).join(' ')}"></polyline>`
|
||||
: nothing}
|
||||
${path.length && this._cursorPt && this._tool === 'draw' && !this._contourClosed
|
||||
? svg`<line class="active-axis" x1="${path[path.length - 1][0]}" y1="${path[path.length - 1][1]}"
|
||||
x2="${this._cursorPt[0]}" y2="${this._cursorPt[1]}" aria-hidden="true"></line>
|
||||
${!this._activePlanSnapCandidate && !this._activePlanSnapConflicts.length
|
||||
? svg`<circle class="active-vertex" cx="${this._cursorPt[0]}" cy="${this._cursorPt[1]}"
|
||||
r="${gridVisualUnits(g * 0.22, this._cellCm)}" aria-hidden="true"></circle>` : nothing}`
|
||||
: nothing}
|
||||
${path.map((p, i) => svg`<circle class="vertex ${i === 0 ? 'first' : ''}"
|
||||
cx="${p[0]}" cy="${p[1]}" r="${gridVisualUnits(g * 0.22, this._cellCm)}"></circle>`)}
|
||||
` as unknown as TemplateResult;
|
||||
}
|
||||
|
||||
private _renderPartitionDeleteDialog(): TemplateResult {
|
||||
const dialog = this._partitionDeleteDialog!;
|
||||
const imperial = this.hass?.config?.unit_system?.length === 'mi';
|
||||
|
||||
Reference in New Issue
Block a user