Works out the move, turn and scale that carry picked source points onto destination points, and the moves that line objects up or space them evenly.
How the precision is kept
Each move is a difference of bounding-box edges in 64-bit numbers. The two-point align turns by the difference of two atan2 angles and scales by a ratio of two lengths, and builds the transform as translate × rotate × scale × translate about the first source point. Moves smaller than 1e-12 drawing units are dropped.
Calculations under this policy
5 calculations in 1 source file. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.
Lengths come from Math.hypot and the turn is the difference of two atan2 results, so it carries the rounding of both. A pair of points closer than 1e-12 drawing units (absolute) returns no transform. The mirror for a third pair is decided by the signs of two cross products with no tolerance, so a third point exactly on the line gives no mirror.
align.js
lines 24 to 40
CadCraft 0.8.2
export function alignMatrix(pairs, { scale = false } = {}) {
const [a, b, c] = pairs;
if (!a) return null;
if (!b) return mTranslate(a.d.x - a.s.x, a.d.y - a.s.y);
const ls = dist(a.s, b.s), ld = dist(a.d, b.d);
if (ls < 1e-12 || ld < 1e-12) return null;
const turn = Math.atan2(b.d.y - a.d.y, b.d.x - a.d.x) - Math.atan2(b.s.y - a.s.y, b.s.x - a.s.x);
const k = scale && !c ? ld / ls : 1;
// about the first source point: scale, turn, then onto the first destination point
let m = mMul(mTranslate(a.d.x, a.d.y), mMul(mRotate(turn), mMul(mScale(k, k), mTranslate(-a.s.x, -a.s.y))));
if (c) {
const side = (p, q, r) => Math.sign((q.x - p.x) * (r.y - p.y) - (q.y - p.y) * (r.x - p.x));
const ss = side(a.s, b.s, c.s), sd = side(a.d, b.d, c.d);
if (ss && sd && ss !== sd) m = mMul(mMirror(a.d, b.d), m);
}
return m;
}
Bounding boxes in the coordinate system in force
64-bit
Each selected object's bounding box, measured along the axes of the user coordinate system when one is set.
with a UCS: box of R(−angle) · T(−origin) applied to the object
With no user coordinate system the box is geom.js entityBounds of the object as stored. With one, the object is first transformed by rotate(−angle) × translate(−origin), so its box carries the rounding of that sine and cosine.
align.js
lines 42 to 53
CadCraft 0.8.2
const ucsMatrix = (u) => (u ? mMul(mRotate(-u.ang), mTranslate(-u.x, -u.y)) : null);
export function boxes(doc, ids, ucs = null) {
const m = ucsMatrix(ucs), out = [];
for (const id of ids) {
const e = doc.get(id);
if (!e) continue;
const b = entityBounds(m ? txEntity(e, m) : e, doc.blocks);
if (validBounds(b)) out.push({ id, minX: b.minX, minY: b.minY, maxX: b.maxX, maxY: b.maxY });
}
return out;
}
Edge or middle of a box
64-bit
The left, center, right, top, middle or bottom coordinate of a bounding box.
Left, right, top and bottom are the stored box values. Center and middle are one addition and a halving; the halving is exact, so only the addition rounds.
align.js
lines 56 to 65
CadCraft 0.8.2
const edge = (b, side) => {
switch (side) {
case 'left': return b.minX;
case 'right': return b.maxX;
case 'center': return (b.minX + b.maxX) / 2;
case 'bottom': return b.minY;
case 'top': return b.maxY;
default: return (b.minY + b.maxY) / 2; // middle
}
};
Moves that line objects up
64-bit
For each object, the vector that brings its chosen edge or middle onto the reference.
d = ref − edge(box)
The reference is an edge of the whole selection's box, of the first or last object's box, or a coordinate of a picked point; each move is one subtraction. A move below 1e-12 drawing units (absolute) is skipped. In a user coordinate system the move is turned back to world axes by ucs.js vecToWorld.
align.js
lines 67 to 89
CadCraft 0.8.2
export function alignMoves(doc, ids, { side = 'left', to = 'selection', point = null, ucs = null } = {}) {
const list = boxes(doc, ids, ucs), out = new Map();
if (!list.length || !SIDES.includes(side)) return out;
let ref;
if (to === 'point') {
if (!point) return out;
const p = ucs ? toUcs(ucs, point) : point;
ref = along(side) === 'x' ? p.x : p.y;
} else if (to === 'first' || to === 'last') {
ref = edge(to === 'first' ? list[0] : list[list.length - 1], side);
} else {
const all = emptyBounds();
for (const b of list) { all.minX = Math.min(all.minX, b.minX); all.minY = Math.min(all.minY, b.minY); all.maxX = Math.max(all.maxX, b.maxX); all.maxY = Math.max(all.maxY, b.maxY); }
ref = edge(all, side);
}
for (const b of list) {
const d = ref - edge(b, side);
if (Math.abs(d) < 1e-12) continue;
const v = along(side) === 'x' ? { x: d, y: 0 } : { x: 0, y: d };
out.set(b.id, ucs ? vecToWorld(ucs, v) : v);
}
return out;
}
Moves that space objects evenly
64-bit
For each object between the first and the last, the vector that makes the middles or the gaps equal.
Objects are sorted by box middle. In gaps mode the sizes are summed in that order and a running position adds size + gap for each object, so rounding accumulates along the row; the last object's move is the residue of that running sum and is dropped only when it is under 1e-12 drawing units. In centers mode each target is mid_first + step × i, with no running sum.
align.js
lines 91 to 115
CadCraft 0.8.2
export function distributeMoves(doc, ids, { axis = 'h', mode = 'gaps', ucs = null } = {}) {
const out = new Map();
const list = boxes(doc, ids, ucs);
if (list.length < 3) return out;
const lo = axis === 'h' ? 'minX' : 'minY', hi = axis === 'h' ? 'maxX' : 'maxY';
const mid = (b) => (b[lo] + b[hi]) / 2;
list.sort((a, b) => mid(a) - mid(b) || a[lo] - b[lo]);
const n = list.length, first = list[0], last = list[n - 1];
const want = new Map();
if (mode === 'centers') {
const step = (mid(last) - mid(first)) / (n - 1);
list.forEach((b, i) => want.set(b.id, mid(first) + step * i - mid(b)));
} else {
const span = last[hi] - first[lo], sizes = list.reduce((s, b) => s + (b[hi] - b[lo]), 0);
const gap = (span - sizes) / (n - 1);
let at = first[lo];
for (const b of list) { want.set(b.id, at - b[lo]); at += b[hi] - b[lo] + gap; }
}
for (const [id, d] of want) {
if (Math.abs(d) < 1e-12) continue;
const v = axis === 'h' ? { x: d, y: 0 } : { x: 0, y: d };
out.set(id, ucs ? vecToWorld(ucs, v) : v);
}
return out;
}
In detail
Method
ALIGN: a 2D matrix from one, two or three point pairs. Align to an edge: reference edge minus each object's edge. Distribute: equal steps between box middles, or equal gaps between boxes.
Number types
64-bit numbers; a transform is an array of six of them.
Tolerances
1e-12 drawing units, absolute: a pair of source or destination points closer than this gives no transform, and a move smaller than this is skipped.
Limits
The 1e-12 tolerances are absolute and do not grow with the size of the coordinates. With a user coordinate system the boxes are measured after each object is turned by the system's angle, which passes through a sine and a cosine.
Approach: 64-bit, direct
Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.