feat(ui): bottom-right resize handle on devices
m: 'I want the size of devices to be customizable. A resize function at the bottom right corner would be good.' - 10×10 SVG handle drawn at each device's bottom-right corner with class .device-resize-handle + cursor: nwse-resize. Subtle grey by default, darker on hover so m can find it without it dominating the rect. - startResize captures the pointer, stops propagation so the rect's pointerdown (= startDrag) doesn't also fire, and updates the local device.width / .height on every pointermove using svgPoint deltas — works at any zoom level via the same world-coord conversion the rest of the canvas uses. - Clamps to 60×30 minimum during the drag so the rect can't collapse. - On pointerup: PATCH /devices/:id with the new width + height, then relayoutAllEdges(deviceID) so ports on every edge redistribute to their i/(N+1) positions against the new dimensions. Right- and bottom-edge ports get the visible adjustment; top/left re-space too but their absolute positions don't change.
This commit is contained in:
@@ -474,6 +474,20 @@ function renderCanvas() {
|
||||
g.append(c);
|
||||
}
|
||||
|
||||
// Bottom-right resize handle. Drawn last so it sits on top of the rect
|
||||
// and any port circles that might overlap the corner. Visible always
|
||||
// but subtle; cursor signals resize affordance.
|
||||
const HSZ = 10;
|
||||
const handle = svgEl("rect", {
|
||||
x: d.x + d.width - HSZ,
|
||||
y: d.y + d.height - HSZ,
|
||||
width: HSZ, height: HSZ,
|
||||
class: "device-resize-handle",
|
||||
"data-device-id": d.id,
|
||||
});
|
||||
handle.addEventListener("pointerdown", (e) => startResize(e, d.id));
|
||||
g.append(handle);
|
||||
|
||||
gDevices.append(g);
|
||||
rect.addEventListener("pointerdown", (e) => startDrag(e, "device", d.id));
|
||||
}
|
||||
@@ -1903,6 +1917,64 @@ async function relayoutEdge(deviceID, edge) {
|
||||
}
|
||||
}
|
||||
|
||||
// Re-space ports on every edge of `deviceID`. Used after the device's
|
||||
// width / height change so all four edges recompute the i/(N+1)
|
||||
// positions against the new dimensions.
|
||||
async function relayoutAllEdges(deviceID) {
|
||||
await Promise.all([
|
||||
relayoutEdge(deviceID, "top"),
|
||||
relayoutEdge(deviceID, "right"),
|
||||
relayoutEdge(deviceID, "bottom"),
|
||||
relayoutEdge(deviceID, "left"),
|
||||
]);
|
||||
}
|
||||
|
||||
// Bottom-right resize handle gesture. Updates width / height in local
|
||||
// state on each move (renderCanvas redraws the rect + ports), clamps to
|
||||
// a minimum so the device can't collapse, then PATCHes the new size on
|
||||
// pointerup and re-spaces every edge's ports.
|
||||
function startResize(e, deviceID) {
|
||||
if (!state.active) return;
|
||||
// Hard-stop so the rect's pointerdown doesn't also fire startDrag.
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const d = state.devices.find((x) => x.id === deviceID);
|
||||
if (!d) return;
|
||||
const startWidth = d.width, startHeight = d.height;
|
||||
const startWorld = svgPoint(e);
|
||||
const svg = /** @type {SVGSVGElement} */ ($("#canvas"));
|
||||
try { svg.setPointerCapture(e.pointerId); } catch {}
|
||||
const MIN_W = 60, MIN_H = 30;
|
||||
const onMove = (ev) => {
|
||||
const p = svgPoint(ev);
|
||||
d.width = Math.max(MIN_W, startWidth + (p.x - startWorld.x));
|
||||
d.height = Math.max(MIN_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 (d.width === startWidth && d.height === startHeight) return;
|
||||
try {
|
||||
const updated = await patchDevice(state.active.id, d.id, {
|
||||
width: d.width, height: d.height,
|
||||
});
|
||||
Object.assign(d, updated);
|
||||
// Ports may have been on an edge that just moved (right or bottom)
|
||||
// — re-distribute everything to the new dims.
|
||||
await relayoutAllEdges(d.id);
|
||||
renderCanvas();
|
||||
} catch (err) {
|
||||
alert(`Resize failed: ${err.message}`);
|
||||
}
|
||||
};
|
||||
svg.addEventListener("pointermove", onMove);
|
||||
svg.addEventListener("pointerup", onUp);
|
||||
svg.addEventListener("pointercancel", onUp);
|
||||
}
|
||||
|
||||
/** Port-click flow:
|
||||
* - A cable draw is in progress (cableDrawFromPortID set):
|
||||
* same port → cancel; another port → finish the cable.
|
||||
|
||||
@@ -192,6 +192,18 @@ body {
|
||||
.device-rect.selected { stroke-width: 3; }
|
||||
.device-rect:hover { filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.15)); }
|
||||
|
||||
/* Bottom-right resize affordance per device. Subtle grey by default,
|
||||
stronger on hover so m can find it without it dominating the rect. */
|
||||
.device-resize-handle {
|
||||
fill: rgba(120, 120, 120, 0.35);
|
||||
stroke: rgba(60, 60, 60, 0.45);
|
||||
stroke-width: 1;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
.device-resize-handle:hover {
|
||||
fill: rgba(60, 60, 60, 0.65);
|
||||
}
|
||||
|
||||
.device-label {
|
||||
fill: var(--text);
|
||||
font-size: 12px;
|
||||
|
||||
Reference in New Issue
Block a user