merge: frame resize handle (bottom-right corner)
Mirrors picasso's device-resize pattern. 10x10 handle at frame's bottom-right; pointerdown→drag→PATCH width+height on release. Min 200x150. Doesn't interfere with frame-body drag or label-as-grip selection.
This commit is contained in:
@@ -480,6 +480,21 @@ function renderCanvas() {
|
||||
});
|
||||
label.textContent = f.name;
|
||||
g.append(rect, label);
|
||||
|
||||
// Bottom-right resize handle. Mirrors the device pattern — sits on
|
||||
// top of the rect so its pointerdown wins, with stopPropagation in
|
||||
// startFrameResize blocking the rect's startDrag underneath.
|
||||
const FHSZ = 10;
|
||||
const fHandle = svgEl("rect", {
|
||||
x: f.x + f.width - FHSZ,
|
||||
y: f.y + f.height - FHSZ,
|
||||
width: FHSZ, height: FHSZ,
|
||||
class: "frame-resize-handle",
|
||||
"data-frame-id": f.id,
|
||||
});
|
||||
fHandle.addEventListener("pointerdown", (e) => startFrameResize(e, f.id));
|
||||
g.append(fHandle);
|
||||
|
||||
gFrames.append(g);
|
||||
rect.addEventListener("pointerdown", (e) => startDrag(e, "frame", f.id));
|
||||
label.addEventListener("pointerdown", (e) => startDrag(e, "frame", f.id));
|
||||
@@ -2385,6 +2400,51 @@ function startResize(e, deviceID) {
|
||||
svg.addEventListener("pointercancel", onUp);
|
||||
}
|
||||
|
||||
// Frame bottom-right resize gesture. Mirrors startResize for devices,
|
||||
// but PATCHes /frames/:id and uses a larger minimum (frames host
|
||||
// devices + IO markers + clamps, so 200×150 is the smallest useful
|
||||
// canvas). Contained children stay at their absolute positions — the
|
||||
// frame body drag is what moves them; resize only changes the frame's
|
||||
// own bounds.
|
||||
function startFrameResize(e, frameID) {
|
||||
if (!state.active) return;
|
||||
// Hard-stop so the rect's pointerdown doesn't also fire startDrag.
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const f = state.frames.find((x) => x.id === frameID);
|
||||
if (!f) return;
|
||||
const startWidth = f.width, startHeight = f.height;
|
||||
const startWorld = svgPoint(e);
|
||||
const svg = /** @type {SVGSVGElement} */ ($("#canvas"));
|
||||
try { svg.setPointerCapture(e.pointerId); } catch {}
|
||||
const MIN_FRAME_W = 200, MIN_FRAME_H = 150;
|
||||
const onMove = (ev) => {
|
||||
const p = svgPoint(ev);
|
||||
f.width = Math.max(MIN_FRAME_W, startWidth + (p.x - startWorld.x));
|
||||
f.height = Math.max(MIN_FRAME_H, startHeight + (p.y - startWorld.y));
|
||||
renderCanvas();
|
||||
};
|
||||
const onUp = async (ev) => {
|
||||
svg.removeEventListener("pointermove", onMove);
|
||||
svg.removeEventListener("pointerup", onUp);
|
||||
svg.removeEventListener("pointercancel", onUp);
|
||||
try { svg.releasePointerCapture(ev.pointerId); } catch {}
|
||||
if (f.width === startWidth && f.height === startHeight) return;
|
||||
try {
|
||||
const updated = await patchFrame(state.active.id, f.id, {
|
||||
width: f.width, height: f.height,
|
||||
});
|
||||
Object.assign(f, updated);
|
||||
renderCanvas();
|
||||
} catch (err) {
|
||||
alert(`Resize failed: ${err.message}`);
|
||||
}
|
||||
};
|
||||
svg.addEventListener("pointermove", onMove);
|
||||
svg.addEventListener("pointerup", onUp);
|
||||
svg.addEventListener("pointercancel", onUp);
|
||||
}
|
||||
|
||||
// Find the topmost canvas element under (clientX, clientY) that maps to
|
||||
// a cable endpoint target. Returns { kind, id } for port / device / IO,
|
||||
// or null when m dropped on empty canvas.
|
||||
|
||||
@@ -183,6 +183,19 @@ body {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
/* Frame bottom-right resize affordance. Mirrors .device-resize-handle
|
||||
but uses the accent-on-frame palette so it reads as part of the frame
|
||||
chrome rather than the device. */
|
||||
.frame-resize-handle {
|
||||
fill: rgba(0, 0, 0, 0.15);
|
||||
stroke: rgba(0, 0, 0, 0.25);
|
||||
stroke-width: 1;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
.frame-resize-handle:hover {
|
||||
fill: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Stroke + fill come from the device's user-set colour, written as
|
||||
inline style in renderCanvas — leaving them out of .device-rect so
|
||||
the author CSS doesn't override the inline style. */
|
||||
|
||||
Reference in New Issue
Block a user