mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 09:19:34 +05:30
feat(flowchart): node resizing (C10)
v4.13.0 — nodes can be resized by dragging the bottom-right handle.
- New setNodeWidth(id, width) on the store. Clamps to [60, 600] so
shapes stay readable. Throws on unknown id; pushes an undo
snapshot like every other mutator.
- New nodeWidth(node) helper on the canvas uses node.width with a
fallback to DEFAULT_WIDTH for graphs loaded from older JSON
that didn't have the field.
- Canvas render uses the per-node width for shapeSvg + label
positioning, and edge boundaryPoint() uses it too so connection
geometry adapts to the new shape size.
- Selected node renders a bottom-right resize handle (10px square).
Clicking the handle starts a 'resize' dragState that calls
setNodeWidth on every pointermove with the screen-to-SVG delta
applied to the start width.
- The handle is detected before the generic node click handler so
it doesn't accidentally start a move drag.
3 new tests cover setNodeWidth update, clamp to [60, 600], and
unknown-id throw. The canvas change reuses the existing 14 tests
(which check rendering and pointer events) — they still pass.
Amit Haridas
This commit is contained in:
@@ -45,8 +45,12 @@ function edgeStyle(kind) {
|
||||
return { 'stroke-width': 1 };
|
||||
}
|
||||
|
||||
function nodeWidth(node) {
|
||||
return Number(node.width) || DEFAULT_WIDTH;
|
||||
}
|
||||
|
||||
function nodeCenter(node) {
|
||||
return { x: node.x + DEFAULT_WIDTH / 2, y: node.y + DEFAULT_HEIGHT / 2 };
|
||||
return { x: node.x + nodeWidth(node) / 2, y: node.y + DEFAULT_HEIGHT / 2 };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,12 +62,13 @@ function nodeCenter(node) {
|
||||
* is a fine visual trade for the simplicity.
|
||||
*/
|
||||
function boundaryPoint(node, dx, dy) {
|
||||
const cx = node.x + DEFAULT_WIDTH / 2;
|
||||
const w = nodeWidth(node);
|
||||
const cx = node.x + w / 2;
|
||||
const cy = node.y + DEFAULT_HEIGHT / 2;
|
||||
if (dx === 0 && dy === 0) return { x: cx, y: cy };
|
||||
const absDx = Math.abs(dx);
|
||||
const absDy = Math.abs(dy);
|
||||
const scaleX = absDx > 0 ? DEFAULT_WIDTH / 2 / absDx : Infinity;
|
||||
const scaleX = absDx > 0 ? w / 2 / absDx : Infinity;
|
||||
const scaleY = absDy > 0 ? DEFAULT_HEIGHT / 2 / absDy : Infinity;
|
||||
const scale = Math.min(scaleX, scaleY);
|
||||
return { x: cx + dx * scale, y: cy + dy * scale };
|
||||
@@ -204,6 +209,7 @@ function createCanvas(container, store, opts = {}) {
|
||||
}
|
||||
|
||||
for (const node of graph.nodes) {
|
||||
const w = nodeWidth(node);
|
||||
const g = svgEl('g', {
|
||||
'data-node-id': node.id,
|
||||
transform: `translate(${node.x},${node.y})`,
|
||||
@@ -211,9 +217,9 @@ function createCanvas(container, store, opts = {}) {
|
||||
tabindex: '0',
|
||||
'aria-label': `${node.kind}: ${node.label || '(no label)'}`,
|
||||
});
|
||||
g.innerHTML = shapeSvg(node.kind, 0, 0, DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||
g.innerHTML = shapeSvg(node.kind, 0, 0, w, DEFAULT_HEIGHT);
|
||||
const text = svgEl('text', {
|
||||
x: DEFAULT_WIDTH / 2,
|
||||
x: w / 2,
|
||||
y: DEFAULT_HEIGHT / 2 + 4,
|
||||
'text-anchor': 'middle',
|
||||
'font-size': 13,
|
||||
@@ -222,6 +228,27 @@ function createCanvas(container, store, opts = {}) {
|
||||
});
|
||||
text.textContent = node.label || ' ';
|
||||
g.appendChild(text);
|
||||
|
||||
// v4.13.0 — bottom-right resize handle on the selected node. A
|
||||
// single square handle is enough for v1; multi-handle (4 corners
|
||||
// + 4 edges) is a follow-up. Width is the only mutable dimension.
|
||||
if (node.id === selectedNodeId) {
|
||||
const handleSize = 10;
|
||||
const handle = svgEl('rect', {
|
||||
x: w - handleSize / 2,
|
||||
y: DEFAULT_HEIGHT - handleSize / 2,
|
||||
width: handleSize,
|
||||
height: handleSize,
|
||||
fill: '#e5461f',
|
||||
stroke: '#ffffff',
|
||||
'stroke-width': 1,
|
||||
class: 'flowchart-resize-handle',
|
||||
'data-resize-node': node.id,
|
||||
cursor: 'nwse-resize',
|
||||
});
|
||||
g.appendChild(handle);
|
||||
}
|
||||
|
||||
nodesLayer.appendChild(g);
|
||||
}
|
||||
}
|
||||
@@ -262,6 +289,27 @@ function createCanvas(container, store, opts = {}) {
|
||||
}
|
||||
|
||||
function onPointerDown(ev) {
|
||||
// v4.13.0 — resize handle on the selected node. Detected before the
|
||||
// generic node drag so the bottom-right square doesn't accidentally
|
||||
// start a move on the node.
|
||||
const handleEl = ev.target.closest('[data-resize-node]');
|
||||
if (handleEl) {
|
||||
const nodeId = handleEl.getAttribute('data-resize-node');
|
||||
const node = store.getGraph().nodes.find((n) => n.id === nodeId);
|
||||
if (!node) return;
|
||||
setSelection({ nodeId });
|
||||
const startW = nodeWidth(node);
|
||||
const startX = ev.clientX;
|
||||
dragState = {
|
||||
mode: 'resize',
|
||||
nodeId,
|
||||
startW,
|
||||
startClientX: startX,
|
||||
};
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeG = ev.target.closest('g[data-node-id]');
|
||||
if (nodeG) {
|
||||
const nodeId = nodeG.getAttribute('data-node-id');
|
||||
@@ -375,6 +423,17 @@ function createCanvas(container, store, opts = {}) {
|
||||
return;
|
||||
}
|
||||
if (!dragState) return;
|
||||
if (dragState.mode === 'resize') {
|
||||
// Width delta in screen units, then convert to SVG via the
|
||||
// viewport-aware viewBox-to-screen ratio.
|
||||
const dxScreen = ev.clientX - dragState.startClientX;
|
||||
const rect = svg.getBoundingClientRect();
|
||||
const vb = svg.viewBox.baseVal;
|
||||
const scaleX = vb.width / rect.width;
|
||||
const newW = Math.max(60, Math.min(600, dragState.startW + dxScreen * scaleX));
|
||||
store.setNodeWidth(dragState.nodeId, newW);
|
||||
return;
|
||||
}
|
||||
if (dragState.mode === 'move') {
|
||||
const p = getSvgPoint(ev.clientX, ev.clientY);
|
||||
const dx = p.x - dragState.pointerX;
|
||||
|
||||
Reference in New Issue
Block a user