mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 09:19:34 +05:30
feat(flowchart): edge geometry at shape boundary + connect preview (C2+C3)
Three UX/DI wins in one canvas pass:
1. Edge endpoints now land on the source/target shape's BOUNDARY
rather than the centres. Lines no longer cut through nodes. New
pure helpers boundaryPoint() + edgeEndpoints() do the geometry;
nodeCenter() is still used internally for direction vectors.
2. Connect-mode preview line. Alt+drag now draws a dashed line from
the source node's boundary to the live pointer position so the
user can see where the edge will land before releasing. The original
code marked this 'visual feedback deferred to v2'.
3. Selection state is now owned by the canvas (selectedNodeId /
selectedEdgeId variables), not read from .selected DOM classes.
The controller can ask via api.getSelection(). Fixes the fragile
pattern in flowchart-controller.js where Delete/Backspace read
querySelector('.flowchart-node.selected') — that read is now
redundant and the controller can be cleaned up to use getSelection()
directly.
5 new tests cover: edge starts past source centre, edge ends before
target centre, vertical edges connect top/bottom, label background
auto-sizes for long labels, canvas getSelection() reflects clicks.
Amit Haridas
This commit is contained in:
@@ -183,3 +183,88 @@ describe('flowchart-canvas: destroy', () => {
|
||||
expect(() => store.addNode({ kind: 'process', x: 0, y: 0, label: 'X' })).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('flowchart-canvas: edge geometry (v4.13.0)', () => {
|
||||
test('edge starts at source boundary, not centre', () => {
|
||||
const store = makeStore();
|
||||
const from = store.addNode({ kind: 'process', x: 0, y: 0, label: 'A' });
|
||||
const to = store.addNode({ kind: 'process', x: 300, y: 0, label: 'B' });
|
||||
store.connect(from.id, to.id, 'solid');
|
||||
const { container, api } = mount(store);
|
||||
const line = container.querySelector('line[data-edge-id]');
|
||||
expect(line).not.toBeNull();
|
||||
// Source centre is at (70, 30); line should start further right (toward B).
|
||||
const x1 = parseFloat(line.getAttribute('x1'));
|
||||
const y1 = parseFloat(line.getAttribute('y1'));
|
||||
expect(x1).toBeGreaterThan(70); // started past the source centre
|
||||
expect(y1).toBeCloseTo(30, 0); // horizontal edge stays at the centre y
|
||||
api.destroy();
|
||||
});
|
||||
|
||||
test('edge ends at target boundary, not centre', () => {
|
||||
const store = makeStore();
|
||||
const from = store.addNode({ kind: 'process', x: 0, y: 0, label: 'A' });
|
||||
const to = store.addNode({ kind: 'process', x: 300, y: 0, label: 'B' });
|
||||
store.connect(from.id, to.id, 'solid');
|
||||
const { container, api } = mount(store);
|
||||
const line = container.querySelector('line[data-edge-id]');
|
||||
const x2 = parseFloat(line.getAttribute('x2'));
|
||||
// Target centre is at (370, 30); line should end before that.
|
||||
expect(x2).toBeLessThan(370);
|
||||
api.destroy();
|
||||
});
|
||||
|
||||
test('vertical edge connects at top/bottom boundary', () => {
|
||||
const store = makeStore();
|
||||
const from = store.addNode({ kind: 'process', x: 0, y: 0, label: 'A' });
|
||||
const to = store.addNode({ kind: 'process', x: 0, y: 300, label: 'B' });
|
||||
store.connect(from.id, to.id, 'solid');
|
||||
const { container, api } = mount(store);
|
||||
const line = container.querySelector('line[data-edge-id]');
|
||||
const y1 = parseFloat(line.getAttribute('y1'));
|
||||
const y2 = parseFloat(line.getAttribute('y2'));
|
||||
expect(y1).toBeGreaterThan(30); // source's bottom boundary is at y=60
|
||||
expect(y2).toBeLessThan(330); // target's top boundary is at y=300
|
||||
api.destroy();
|
||||
});
|
||||
|
||||
test('edge label background auto-sizes for long labels', () => {
|
||||
const store = makeStore();
|
||||
const from = store.addNode({ kind: 'process', x: 0, y: 0, label: 'A' });
|
||||
const to = store.addNode({ kind: 'process', x: 300, y: 0, label: 'B' });
|
||||
store.connect(from.id, to.id, 'solid');
|
||||
store.setEdgeLabel(store.getGraph().edges[0].id, 'a very long label');
|
||||
const { container, api } = mount(store);
|
||||
const bg = container.querySelector('rect[data-edge-label-bg]');
|
||||
const longBg = bg.getAttribute('width');
|
||||
// Set a short label and re-render — bg should be smaller
|
||||
store.setEdgeLabel(store.getGraph().edges[0].id, 'x');
|
||||
const bg2 = container.querySelector('rect[data-edge-label-bg]');
|
||||
const shortBg = bg2.getAttribute('width');
|
||||
expect(parseFloat(longBg)).toBeGreaterThan(parseFloat(shortBg));
|
||||
api.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('flowchart-canvas: selection ownership (v4.13.0)', () => {
|
||||
test('canvas owns selection state, not DOM', () => {
|
||||
const store = makeStore();
|
||||
const { api, container } = mount(store);
|
||||
expect(api.getSelection()).toEqual({ nodeId: null, edgeId: null });
|
||||
// Simulate pointerdown via dispatching the event on the SVG
|
||||
const node = store.addNode({ kind: 'process', x: 10, y: 10, label: 'X' });
|
||||
store.subscribe(() => {}); // noop; the canvas subscriber re-renders
|
||||
// Manually call internal logic via a re-render trigger: we set selection
|
||||
// through the public pointerdown handler.
|
||||
const svg = container.querySelector('svg.flowchart-canvas');
|
||||
const nodeEl = container.querySelector('g[data-node-id]');
|
||||
// Build a synthetic pointerdown on the node element
|
||||
const ev = new Event('pointerdown', { bubbles: true, cancelable: true });
|
||||
Object.defineProperty(ev, 'target', { value: nodeEl });
|
||||
Object.defineProperty(ev, 'clientX', { value: 10 });
|
||||
Object.defineProperty(ev, 'clientY', { value: 10 });
|
||||
svg.dispatchEvent(ev);
|
||||
expect(api.getSelection().nodeId).toBe(node.id);
|
||||
api.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user