docs(debug): sherlock frame-select — not a regression, render-order clutter
Playwright drives the deployed image four ways. The frame <rect> has its own pointerdown handler that calls startDrag; startDrag calls e.stopPropagation() before the pan-start handler runs; the selector at main.js:2087 already includes [data-frame-id]. Click that lands on the rect → frame selected, inspector switches to the Frame panel. Verified. m's complaint is real but cause is render order: frames paint first; devices, ports, cables, clamps, IO markers all paint on top. Any click on the frame interior that happens to land on one of those elements hits that element, not the frame. For LOFT's big 'Entertainment' frame the visible canvas portion is ~180×424 px and partly covered by yellow cable polylines. Not a one-line bug. Three UX options ordered by effort in the doc: make the frame label the selection grip (drop pointer-events:none + add pointerdown), parent-frame breadcrumb on the device inspector, modifier key to escape to the enclosing frame. None applied — picasso/perseus call.
This commit is contained in:
99
docs/sherlock-frame-select.md
Normal file
99
docs/sherlock-frame-select.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# "I cannot change frames" — repro & verdict
|
||||
|
||||
**Reporter:** sherlock (Playwright debug shift 3, 2026-05-16 ~19:30)
|
||||
**Target:** http://mdock:7777 (CableGUI, deployed at 79e17a5)
|
||||
**Hypothesis under test:** kandinsky's pan-on-left-drag (2933bb8) regressed frame select because the "isEmpty" selector missed `[data-frame-id]`.
|
||||
|
||||
**Verdict:** **Hypothesis is wrong.** The selector at main.js:2087 already lists `[data-frame-id]`, the frame `<rect>` has its own pointerdown handler that calls `startDrag`, and `startDrag` calls `e.stopPropagation()` synchronously before the pan-start handler could fire. Frame selection works mechanically on the deployed image — verified four ways. m's complaint is real, but it's a **render-order / clutter** problem, not a click-handler regression.
|
||||
|
||||
## What I ran (4 Playwright drives against the deployed image)
|
||||
|
||||
### Selection works on a click that lands on the frame rect
|
||||
|
||||
`frame_repro2.py` TEST 2 — Esc to deselect, then click 2 px inside frame 1's left edge:
|
||||
```
|
||||
before: selected_frames=[] inspector=None
|
||||
after: selected_frames=['1'] inspector='Frame'
|
||||
pointerdowns: svg-capture target=rect.frame-rect frame=1
|
||||
pan changes: []
|
||||
```
|
||||
Frame 1's inspector panel appears with name="Entertainment", x/y/w/h editable, "Delete frame" button. No pan-start fired.
|
||||
|
||||
`frame_clean_test.py` — Esc, then click frame 3 (Network) bottom-left corner where nothing else is rendered:
|
||||
```
|
||||
top element at click point: rect.frame-rect (frame=3)
|
||||
after click: selected_frames=['3'] inspector='Frame'
|
||||
```
|
||||
|
||||
### Selection works on a click + small drag too
|
||||
|
||||
`frame_repro2.py` TEST 3 — 5 px drag (past the 3 px pan threshold) on frame interior:
|
||||
- `startDrag(e, "frame", f.id)` fires at the frame rect's `pointerdown`. It is the **first** pointerdown handler in the chain.
|
||||
- `startDrag` (main.js:2839) calls `e.stopPropagation()` synchronously when no tool is armed → pan-start handler never gets the event.
|
||||
- The 5 px movement is handled by `startDrag`'s own `onMove` (frame drag), not by the empty-canvas pan path.
|
||||
- Result: frame stays selected, frame nudges 5 px to the right. Inspector panel stays correct.
|
||||
|
||||
50 px drag (TEST 4): same — frame stays selected, moves further.
|
||||
|
||||
## Why m experiences "doesn't select"
|
||||
|
||||
The frame `<rect>` paints **first**; everything else paints on top of it in this order (from index.html):
|
||||
|
||||
```
|
||||
canvas-frames ← frame rects (BOTTOM)
|
||||
canvas-devices ← device rects + port circles inside each device <g>
|
||||
canvas-ports
|
||||
canvas-cables ← cable polylines (often crossing through frame interior)
|
||||
canvas-clamps
|
||||
canvas-io ← IO marker diamonds
|
||||
```
|
||||
|
||||
So any click on the frame's visible interior that happens to land on a device, port, port circle, cable polyline, clamp, or IO diamond hits THAT element, not the frame. The frame rect only "wins" the hit-test on pixels that are not covered by anything else.
|
||||
|
||||
Two concrete observations from the LOFT snapshot:
|
||||
|
||||
- Frame 1 "Entertainment" is huge (SVG 1340×848 at viewBox 2000×1500). Most of it renders past the canvas-wrap's right edge (x > 1220 px on a 1500 px viewport) and is occluded by the inspector `<aside>`. Only a ~180 × 424 px strip on the visible canvas right side is selectable. That strip is partly covered by devices and yellow cable polylines crossing diagonally.
|
||||
- Frame 3 "Network": grid hit-test, 9 sample points → 5 hit the frame, 2 hit cable polylines that cross through its centre. `frame_clean_test.py` confirmed: bottom-left corner click selects; centre click selects the cable.
|
||||
|
||||
For m, "I click on the frame and it doesn't select" is **literally true at the click point he's choosing** — but the click is going to the cable/device on top, not the frame underneath. There is no regression in the dispatch.
|
||||
|
||||
## Recommendation (no code change applied)
|
||||
|
||||
Since the click-handler chain is correct, this is a UX call for picasso/perseus to make, not a one-line patch sherlock should apply. Three options ordered by effort:
|
||||
|
||||
1. **Frame label is the selection grip.** Drop `pointer-events: none` from `.frame-label` (style.css:183) and add a pointerdown handler on the label that calls `startDrag(e, "frame", id)`. The label sits in the top-left of the frame, never overlaps a device, and is always visible — m gets a deterministic spot to click. This is a 2-line patch.
|
||||
|
||||
2. **Selection breadcrumb on the device inspector.** When a selected device has `frame_id`, render `parent frame: <name>` as a clickable chip that calls `state.selection = {kind: "frame", id: frame.id}; render()`. Lets m drill from "I selected the wrong thing" → frame in one click.
|
||||
|
||||
3. **Modifier-click to escape to frame.** Alt-click (or Ctrl-click) on any element promotes the hit-test to "select the enclosing frame, if any". Discoverable via tooltip on the inspector header.
|
||||
|
||||
Option (1) is the cheapest and matches user mental model ("click the label to select that thing"). Probably worth pairing with the empty-canvas pan UX so m can hold Shift to disable pan when he wants to clear selection — but that's a separate refinement.
|
||||
|
||||
## What the head's hypothesis *would* look like if it were the bug
|
||||
|
||||
For completeness — if `[data-frame-id]` were missing from the selector in `onCanvasPointerDown`:
|
||||
|
||||
```js
|
||||
// (hypothetical broken version)
|
||||
if (e.target instanceof Element && e.target.closest("[data-device-id], [data-io-id]")) return;
|
||||
// ^^ no frame-id
|
||||
if (e.button === 0 && state.cableDrawFromPortID == null) {
|
||||
startEmptyCanvasGesture(e);
|
||||
}
|
||||
```
|
||||
|
||||
…then frame clicks would fall into `startEmptyCanvasGesture`. Sub-3 px clicks would still deselect (no good for m), and any tiny hand-jitter would promote to a pan. m would see "click frame → view shifts, no selection." That symptom matches m's wording — but the actual code is correct, so this isn't what's happening.
|
||||
|
||||
The current code (main.js:2087):
|
||||
```js
|
||||
if (e.target instanceof Element && e.target.closest("[data-device-id], [data-frame-id], [data-io-id], [data-clamp-id], [data-port-id], [data-cable-id]")) return;
|
||||
```
|
||||
All six known target types are listed. No regression here.
|
||||
|
||||
## Artifacts
|
||||
|
||||
Under `/tmp/sherlock/`:
|
||||
- `frame_repro.py`, `frame_repro2.py`, `frame_repro3.py`, `frame_clean_test.py` — staged repros
|
||||
- `canvas_layout.py` — layout dump confirming canvas-wrap vs inspector aside geometry
|
||||
- `frame_run.log`, `frame_run2.log`, `frame_run3.log`, `clean.log`, `layout.log` — full transcripts
|
||||
- `frame2_01_interior.png`, `frame2_02_edge.png`, `frame_clean_result.png` — pre/post-click screenshots showing inspector switching to **Frame** panel on a clean rect-hit click
|
||||
Reference in New Issue
Block a user