mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 09:19:34 +05:30
feat(flowchart): alignment + distribution tools (C15)
8 new toolbar buttons (Align L/R/T/B, Center H/V, Distribute H/V)
plus a Select All button. Each operation takes the current
selection and applies a pure-function transform to the (x, y)
positions, then commits each result via store.moveNode().
Architecture:
- src/flowchart/flowchart-align.js: pure module with 8 helpers
+ nodeWidth / nodeHeight utility. UMD wrapper for browser
global + CommonJS, same pattern as the other pure modules.
- src/renderer/flowchart-bundle.js: pure module inlined so the
standalone window doesn't need a separate bundle build; the
inlined functions are exposed as window.FlowchartAlign for
jsdom tests.
- Bundle's _selectedNodeIds Set tracks the alignment selection.
Empty by default; populated by Select All or by future
canvas-side shift+click (item 1: multi-select).
- Each align button no-ops with a status hint if fewer than 2
nodes are selected. Distribute needs 3+.
- Each moveNode call creates its own undo snapshot, which is
fine for typical 2-10 node selections and lets the user undo
a misalignment one node at a time.
Tests: 14 new (1376 total) covering all 8 functions plus
immutability, no-op for insufficient nodes, and unsorted-input
sorting for distribute.
Amit Haridas
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Pure alignment + distribution helpers for the flowchart editor (v4.13.0).
|
||||
*
|
||||
* Each function takes an array of node objects and returns a new array
|
||||
* with the same nodes but their (x, y) adjusted per the operation.
|
||||
* Pure module — no DOM, no globals, no store mutation — so it's
|
||||
* unit-testable in isolation and the caller decides how to apply the
|
||||
* result (one moveNode per node, or a future batch API).
|
||||
*
|
||||
* All operations are no-ops when given fewer than two nodes (nothing
|
||||
* to align against). Distribute needs three or more to have a meaningful
|
||||
* "even space" between extremes.
|
||||
*
|
||||
* Coordinates are SVG units. A node's height is the canvas default
|
||||
* (60px) since the editor doesn't store height — only width.
|
||||
*
|
||||
* @module flowchart-align
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const DEFAULT_HEIGHT = 60;
|
||||
|
||||
function nodeWidth(node) {
|
||||
return Number(node.width) > 0 ? Number(node.width) : 120;
|
||||
}
|
||||
|
||||
function nodeHeight(_node) {
|
||||
return DEFAULT_HEIGHT;
|
||||
}
|
||||
|
||||
function clone(node) {
|
||||
return { ...node };
|
||||
}
|
||||
|
||||
/** All nodes line up at the leftmost x. */
|
||||
function alignLeft(nodes) {
|
||||
if (!Array.isArray(nodes) || nodes.length < 2) return nodes;
|
||||
const minX = Math.min(...nodes.map((n) => n.x));
|
||||
return nodes.map((n) => Object.assign(clone(n), { x: minX }));
|
||||
}
|
||||
|
||||
/** All nodes line up at the rightmost (x + width). */
|
||||
function alignRight(nodes) {
|
||||
if (!Array.isArray(nodes) || nodes.length < 2) return nodes;
|
||||
const maxRight = Math.max(...nodes.map((n) => n.x + nodeWidth(n)));
|
||||
return nodes.map((n) => Object.assign(clone(n), { x: maxRight - nodeWidth(n) }));
|
||||
}
|
||||
|
||||
/** All nodes line up at the topmost y. */
|
||||
function alignTop(nodes) {
|
||||
if (!Array.isArray(nodes) || nodes.length < 2) return nodes;
|
||||
const minY = Math.min(...nodes.map((n) => n.y));
|
||||
return nodes.map((n) => Object.assign(clone(n), { y: minY }));
|
||||
}
|
||||
|
||||
/** All nodes line up at the bottommost (y + height). */
|
||||
function alignBottom(nodes) {
|
||||
if (!Array.isArray(nodes) || nodes.length < 2) return nodes;
|
||||
const maxBottom = Math.max(...nodes.map((n) => n.y + nodeHeight(n)));
|
||||
return nodes.map((n) => Object.assign(clone(n), { y: maxBottom - nodeHeight(n) }));
|
||||
}
|
||||
|
||||
/** All nodes share the same horizontal center (mean of centers). */
|
||||
function alignCenterHorizontal(nodes) {
|
||||
if (!Array.isArray(nodes) || nodes.length < 2) return nodes;
|
||||
const centers = nodes.map((n) => n.x + nodeWidth(n) / 2);
|
||||
const avg = centers.reduce((a, b) => a + b, 0) / centers.length;
|
||||
return nodes.map((n) => Object.assign(clone(n), { x: avg - nodeWidth(n) / 2 }));
|
||||
}
|
||||
|
||||
/** All nodes share the same vertical center (mean of centers). */
|
||||
function alignCenterVertical(nodes) {
|
||||
if (!Array.isArray(nodes) || nodes.length < 2) return nodes;
|
||||
const centers = nodes.map((n) => n.y + nodeHeight(n) / 2);
|
||||
const avg = centers.reduce((a, b) => a + b, 0) / centers.length;
|
||||
return nodes.map((n) => Object.assign(clone(n), { y: avg - nodeHeight(n) / 2 }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Distribute horizontally — equal gap between consecutive node left edges.
|
||||
* The first and last nodes keep their positions; everything in between
|
||||
* is spaced evenly.
|
||||
*/
|
||||
function distributeHorizontally(nodes) {
|
||||
if (!Array.isArray(nodes) || nodes.length < 3) return nodes;
|
||||
const sorted = [...nodes].sort((a, b) => a.x - b.x);
|
||||
const leftmost = sorted[0].x;
|
||||
const rightmost = sorted[sorted.length - 1].x;
|
||||
const gap = (rightmost - leftmost) / (sorted.length - 1);
|
||||
return sorted.map((n, i) => Object.assign(clone(n), { x: leftmost + gap * i }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Distribute vertically — equal gap between consecutive node top edges.
|
||||
* Same first/last-anchored semantics as the horizontal version.
|
||||
*/
|
||||
function distributeVertically(nodes) {
|
||||
if (!Array.isArray(nodes) || nodes.length < 3) return nodes;
|
||||
const sorted = [...nodes].sort((a, b) => a.y - b.y);
|
||||
const topmost = sorted[0].y;
|
||||
const bottommost = sorted[sorted.length - 1].y;
|
||||
const gap = (bottommost - topmost) / (sorted.length - 1);
|
||||
return sorted.map((n, i) => Object.assign(clone(n), { y: topmost + gap * i }));
|
||||
}
|
||||
|
||||
if (typeof module === 'object' && module.exports) {
|
||||
module.exports = {
|
||||
alignLeft,
|
||||
alignRight,
|
||||
alignTop,
|
||||
alignBottom,
|
||||
alignCenterHorizontal,
|
||||
alignCenterVertical,
|
||||
distributeHorizontally,
|
||||
distributeVertically,
|
||||
nodeWidth,
|
||||
nodeHeight,
|
||||
};
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
window.FlowchartAlign = {
|
||||
alignLeft,
|
||||
alignRight,
|
||||
alignTop,
|
||||
alignBottom,
|
||||
alignCenterHorizontal,
|
||||
alignCenterVertical,
|
||||
distributeHorizontally,
|
||||
distributeVertically,
|
||||
nodeWidth,
|
||||
nodeHeight,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user