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;
|
label.textContent = f.name;
|
||||||
g.append(rect, label);
|
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);
|
gFrames.append(g);
|
||||||
rect.addEventListener("pointerdown", (e) => startDrag(e, "frame", f.id));
|
rect.addEventListener("pointerdown", (e) => startDrag(e, "frame", f.id));
|
||||||
label.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);
|
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
|
// Find the topmost canvas element under (clientX, clientY) that maps to
|
||||||
// a cable endpoint target. Returns { kind, id } for port / device / IO,
|
// a cable endpoint target. Returns { kind, id } for port / device / IO,
|
||||||
// or null when m dropped on empty canvas.
|
// or null when m dropped on empty canvas.
|
||||||
|
|||||||
@@ -183,6 +183,19 @@ body {
|
|||||||
cursor: grab;
|
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
|
/* Stroke + fill come from the device's user-set colour, written as
|
||||||
inline style in renderCanvas — leaving them out of .device-rect so
|
inline style in renderCanvas — leaving them out of .device-rect so
|
||||||
the author CSS doesn't override the inline style. */
|
the author CSS doesn't override the inline style. */
|
||||||
|
|||||||
Reference in New Issue
Block a user