mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
fix: разрешить перекрытия целей маркеров (#564)
Issue: #564 User-Visible: yes
This commit is contained in:
@@ -26,7 +26,10 @@ const CAPS = {
|
||||
// 2026-09-11, #534: +1 for `import { keyed }`. The line buys back the 80 ms
|
||||
// that #525 put on every space switch, and there is nothing to move out —
|
||||
// the render itself did not grow by a character.
|
||||
'src/houseplan-card.ts': 13651,
|
||||
// 2026-09-14, #564: the geometry/index/latch/DOM ownership implementation is
|
||||
// isolated in device-hit-owner.ts. These 81 lines are the remaining card
|
||||
// lifecycle invalidations and the presentation/action integration seams.
|
||||
'src/houseplan-card.ts': 13732,
|
||||
// #478 removed the persisted room-draft editor branch. Keep that reduction.
|
||||
// #485 keeps its large setup surface in editors/radar-section.ts; these are
|
||||
// only the dialog state/save seam and the thin lazy-render adapter.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const card = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
||||
const owner = readFileSync(new URL('../src/device-hit-owner.ts', import.meta.url), 'utf8');
|
||||
const styles = readFileSync(new URL('../src/styles/devices.styles.ts', import.meta.url), 'utf8');
|
||||
|
||||
test('#564 wires one semantic owner through pointer, click and hover paths', () => {
|
||||
assert.match(card, /private _deviceForPointerEvent[\s\S]*?_deviceHits\.pointer/);
|
||||
assert.match(card, /private _deviceForClickEvent[\s\S]*?_deviceHits\.click/);
|
||||
assert.match(card, /private _showDeviceTip[\s\S]*?_deviceHits\.hover/);
|
||||
assert.match(card, /private _pointerDown[\s\S]*?_deviceHits\.begin/);
|
||||
assert.match(card, /private _pointerCancel[\s\S]*?_deviceHits\.cancel/);
|
||||
assert.match(owner, /class DeviceHitController[\s\S]*?consumeClick/);
|
||||
assert.match(owner, /hover\([\s\S]*?data-hp-device-hover/);
|
||||
});
|
||||
|
||||
test('#564 keeps pointermove on cached screen geometry', () => {
|
||||
const move = card.match(/private _pointerMove\([\s\S]*?\n }\n\n private _pointerMoveNow/)?.[0] || '';
|
||||
assert.match(move, /_deviceForPointerEvent/);
|
||||
assert.doesNotMatch(move, /getBoundingClientRect|getComputedStyle|querySelector/);
|
||||
assert.match(owner, /private indexFor[\s\S]*?new DeviceHitIndex/);
|
||||
});
|
||||
|
||||
test('#564 paints every real marker face above every invisible 44px floor', () => {
|
||||
const root = styles.match(/ \.dev \{[\s\S]*?\n }/)?.[0] || '';
|
||||
const floor = styles.match(/ \.dev::before \{[\s\S]*?\n }/)?.[0] || '';
|
||||
const shell = styles.match(/ \.device-shell \{[\s\S]*?\n }/)?.[0] || '';
|
||||
assert.match(root, /pointer-events: none/);
|
||||
assert.match(floor, /pointer-events: auto/);
|
||||
assert.match(floor, /z-index: 1/);
|
||||
assert.match(shell, /z-index: 2/);
|
||||
assert.match(styles, /\.dev\[data-hp-device-hover\]/);
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
DeviceHitIndex, DevicePointerOwnerLatch, pointInDeviceCapsule, resolveDeviceHitOwner,
|
||||
} from '../test-build/device-hit-owner.js';
|
||||
|
||||
const candidate = (id, x, y, width = 12, height = 12, floorRadius = 22) => ({
|
||||
id,
|
||||
center: { x, y },
|
||||
painted: {
|
||||
left: x - width / 2,
|
||||
top: y - height / 2,
|
||||
right: x + width / 2,
|
||||
bottom: y + height / 2,
|
||||
},
|
||||
floorRadius,
|
||||
});
|
||||
|
||||
test('#564 a visible core wins the neighbour invisible 44px floor', () => {
|
||||
const left = candidate('left', 100, 100);
|
||||
const right = candidate('right', 112, 100);
|
||||
assert.equal(resolveDeviceHitOwner([right, left], { x: 100, y: 100 })?.id, 'left');
|
||||
assert.equal(resolveDeviceHitOwner([left, right], { x: 112, y: 100 })?.id, 'right');
|
||||
});
|
||||
|
||||
test('#564 painted overlap and floor-only overlap use the nearest core', () => {
|
||||
const left = candidate('left', 100, 100, 16, 16);
|
||||
const right = candidate('right', 112, 100, 16, 16);
|
||||
assert.equal(resolveDeviceHitOwner([right, left], { x: 105, y: 100 })?.id, 'left');
|
||||
assert.equal(resolveDeviceHitOwner([left, right], { x: 107, y: 100 })?.id, 'right');
|
||||
|
||||
const separatedLeft = candidate('left', 100, 100);
|
||||
const separatedRight = candidate('right', 130, 100);
|
||||
assert.equal(resolveDeviceHitOwner([separatedRight, separatedLeft], { x: 113, y: 100 })?.id, 'left');
|
||||
assert.equal(resolveDeviceHitOwner([separatedLeft, separatedRight], { x: 117, y: 100 })?.id, 'right');
|
||||
});
|
||||
|
||||
test('#564 a capsule does not own invisible bbox corners', () => {
|
||||
const horizontal = candidate('pill', 100, 100, 40, 12);
|
||||
assert.equal(pointInDeviceCapsule({ x: 100, y: 100 }, horizontal.painted), true);
|
||||
assert.equal(pointInDeviceCapsule({ x: 119, y: 105 }, horizontal.painted), false);
|
||||
assert.equal(pointInDeviceCapsule({ x: 118, y: 100 }, horizontal.painted), true);
|
||||
});
|
||||
|
||||
test('#564 tie-break is stable and independent of candidate/DOM order', () => {
|
||||
const a = candidate('a', 90, 100);
|
||||
const b = candidate('b', 110, 100);
|
||||
assert.equal(resolveDeviceHitOwner([b, a], { x: 100, y: 100 })?.id, 'a');
|
||||
assert.equal(resolveDeviceHitOwner([a, b], { x: 100, y: 100 })?.id, 'a');
|
||||
const sameA = candidate('a', 100, 100);
|
||||
const sameB = candidate('b', 100, 100);
|
||||
assert.equal(resolveDeviceHitOwner([sameB, sameA], { x: 100, y: 100 })?.id, 'a');
|
||||
});
|
||||
|
||||
test('#564 spatial index resolves transformed screen coordinates locally', () => {
|
||||
const many = Array.from({ length: 200 }, (_, index) =>
|
||||
candidate(`d${String(index).padStart(3, '0')}`, 1000 + index * 60, 800));
|
||||
const target = candidate('target', 312.5, 487.25, 48, 16);
|
||||
const index = new DeviceHitIndex([...many, target]);
|
||||
assert.equal(index.resolve({ x: 330, y: 487.25 })?.id, 'target');
|
||||
assert.equal(index.resolve({ x: -500, y: -500 }), null);
|
||||
});
|
||||
|
||||
test('#564 pointer owner is held through release and consumed by click', () => {
|
||||
const latch = new DevicePointerOwnerLatch();
|
||||
latch.begin(7, 'left');
|
||||
assert.equal(latch.owner(7), 'left');
|
||||
assert.equal(latch.release(7), 'left');
|
||||
assert.equal(latch.owner(7), null);
|
||||
assert.equal(latch.consumeClick(7), 'left');
|
||||
assert.equal(latch.consumeClick(7), null);
|
||||
|
||||
latch.begin(8, 'right');
|
||||
assert.equal(latch.cancel(8), 'right');
|
||||
assert.equal(latch.release(8), null);
|
||||
latch.clear();
|
||||
});
|
||||
Reference in New Issue
Block a user