Turns the shapes of a loaded .shx font into strokes, and gives a text's width and the position of each stroke in the drawing.
How the precision is kept
Pen moves are whole numbers from the font times a scale that the shape multiplies and divides by whole numbers, all in 64-bit. Arcs in a shape are drawn as chords of at most 7.5°, so a round letter is a polygon. Text is scaled by its height over the font's capital height.
Calculations under this policy
7 calculations in 2 source files. 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.
shx.jsCadCraft 0.8.264-bit5 calculationsReviewed
Tests bin/test-cadcraft.mjs
Pen moves
64-bit
Moves the pen by a displacement of the shape.
x′ = x + dx × mul ÷ div
dx and dy are whole numbers (or halves in the 16-direction table); mul and div are products of the shape's whole-number scale bytes. The product is formed before the division.
n = max(2, ⌈|sweep| ÷ 7.5°⌉); pointₖ = c + r(cos aₖ, sin aₖ), aₖ = a₀ + sweep × k ÷ n
Chord ends lie on the arc and each angle is computed from the whole sweep, not added up. The curve between the ends is replaced by straight pieces of at most 7.5°.
shx.js
lines 126 to 130
CadCraft 0.8.2
const arc = (cx, cy, r, a0, a1) => { // a1 − a0 signed; the pen ends on the arc
const n = Math.max(2, Math.ceil(Math.abs(a1 - a0) / ARC_STEP));
for (let k = 1; k <= n; k++) { const a = a0 + (a1 - a0) * k / n; const x = cx + r * Math.cos(a), y = cy + r * Math.sin(a); if (st.down) put(x, y); st.x = x; st.y = y; }
if (!st.down) st.cur = null;
};
Bulge arcs
64-bit
An arc given by its chord and a bulge value.
θ = 4·atan(b); r = chord ÷ (2·sin(|θ| ÷ 2)); center = chord middle ± r·cos(θ ÷ 2) across the chord
The bulge is a signed byte divided by 127. After the chords are drawn the pen is set to the exact end of the displacement, so rounding in the arc does not carry into the next move. A zero chord is skipped.
shx.js
lines 131 to 142
CadCraft 0.8.2
const bulge = (dx, dy, bl) => {
const x0 = st.x, y0 = st.y, x1 = x0 + dx * st.mul / st.div, y1 = y0 + dy * st.mul / st.div;
if (!bl) { move(dx, dy); return; }
const chord = Math.hypot(x1 - x0, y1 - y0);
if (chord === 0) return;
const th = 4 * Math.atan(bl), r = chord / (2 * Math.sin(Math.abs(th) / 2));
const mx = (x0 + x1) / 2, my = (y0 + y1) / 2, hh = r * Math.cos(th / 2) * Math.sign(bl); // the centre lies this far to the left of the chord (to the right for a clockwise arc)
const cx = mx - (y1 - y0) / chord * hh, cy = my + (x1 - x0) / chord * hh;
const a0 = Math.atan2(y0 - cy, x0 - cx);
arc(cx, cy, r, a0, a0 + th);
st.x = x1; st.y = y1;
};
Octant and fractional arcs
64-bit
Arcs given by a radius, a starting octant and a span of octants, with optional fractions of an octant.
a₀ = (s₀ + so ÷ 256) × π ÷ 4; center = pen − r(cos a₀, sin a₀)
The radius is a whole number from the font times mul ÷ div. Octant angles are whole multiples of π ÷ 4; a fractional arc adds its start and end offsets in 256ths of an octant.
shx.js
lines 165 to 180
CadCraft 0.8.2
case 0x0a: {
const r = (bytes[i] ?? 0) * st.mul / st.div, o = bytes[i + 1] ?? 0;
i += 2;
const cw = (o & 0x80) !== 0, s0 = (o >> 4) & 7, span = (o & 7) || 8;
const a0 = s0 * Math.PI / 4, a1 = a0 + (cw ? -1 : 1) * span * Math.PI / 4;
arc(st.x - r * Math.cos(a0), st.y - r * Math.sin(a0), r, a0, a1);
break;
}
case 0x0b: {
const so = bytes[i] ?? 0, eo = bytes[i + 1] ?? 0, r = (((bytes[i + 2] ?? 0) << 8) | (bytes[i + 3] ?? 0)) * st.mul / st.div, o = bytes[i + 4] ?? 0;
i += 5;
const cw = (o & 0x80) !== 0, s0 = (o >> 4) & 7, span = (o & 7) || 8;
const a0 = (s0 + so / 256) * Math.PI / 4, a1 = ((s0 + (cw ? -span : span)) + (cw ? -eo : eo) / 256) * Math.PI / 4;
arc(st.x - r * Math.cos(a0), st.y - r * Math.sin(a0), r, a0, a1);
break;
}
Text strokes and width
64-bit
The strokes of a text for capitals of a given height, and the text's width.
k = h ÷ capital height of the font; width = Σ glyph advance × k
Each glyph's coordinates are multiplied by k and offset by the running width, summed left to right. A font with no stated capital height is scaled as if it were 1. A missing character adds 0.6 × h and a space without a shape 0.5 × h.
shx.js
lines 219 to 235
CadCraft 0.8.2
export function textStrokes(font, text, h, o = {}) {
const chars = [...String(text)];
if (o.reverse) chars.reverse();
const above = font.above > 0 ? font.above : 1;
const k = h / above;
const strokes = [];
let x = 0, missing = 0;
for (const ch of chars) {
if (ch === ' ' || ch === '\t') { const sp = pick(font, ' ', o); x += sp ? sp.g.endX * k : h * 0.5; continue; }
const r = pick(font, ch, o);
if (!r) { missing++; x += h * 0.6; continue; }
const kk = h / (r.font.above > 0 ? r.font.above : 1);
for (const s of r.g.strokes) { const out = new Array(s.length); for (let i = 0; i < s.length; i += 2) { out[i] = x + s[i] * kk; out[i + 1] = s[i + 1] * kk; } strokes.push(out); }
x += r.g.endX * kk;
}
return { strokes, advance: x, missing };
}
The width the text layout uses for a text whose style names a loaded .shx font.
width = advance × width factor
The advance from textStrokes times the text's width factor (1 when none is set); nothing is rounded.
shx-text.js
lines 44 to 47
CadCraft 0.8.2
setTextMeasure((s, e) => {
const f = activeDoc && e && typeof e === 'object' ? styleFont(activeDoc, e) : null;
return f ? textAdvance(f.font, s, e.h, { reverse: f.reverse, fallback: f.big }) * (e.wf || 1) : null;
});
Strokes placed in the drawing
64-bit
Each stroke point of a text taken to drawing coordinates with alignment, width factor, slant and rotation.
lx = x₀ + (gx + gy·tan(oblique)) × wf; X = x + lx·cos − gy·sin; Y = y + lx·sin + gy·cos
One tangent, cosine and sine per text; each point takes a few 64-bit products and sums. Centered and right-aligned text start at minus half or minus the whole width.
shx-text.js
lines 49 to 71
CadCraft 0.8.2
setShxHook((doc, te) => {
const f = styleFont(doc, te);
if (!f || richOf(te)) return null;
const lay = textLayout(te);
const wf = te.wf || 1, tan = Math.tan(te.obl || 0), co = Math.cos(te.rot || 0), si = Math.sin(te.rot || 0);
const prims = [];
lay.lines.forEach((line, i) => {
if (!line) return;
const r = textStrokes(f.font, line, te.h, { reverse: f.reverse, fallback: f.big });
const adv = r.advance * wf, base = lay.baselines[i];
const x0 = te.ha === 'center' ? -adv / 2 : te.ha === 'right' ? -adv : 0;
for (const s of r.strokes) {
let px = 0, py = 0;
for (let k = 0; k < s.length; k += 2) {
const gy = base + s[k + 1], lx = x0 + (s[k] + gy * tan) * wf;
const X = te.x + lx * co - gy * si, Y = te.y + lx * si + gy * co;
if (k) prims.push({ k: 'L', x1: px, y1: py, x2: X, y2: Y });
px = X; py = Y;
}
}
});
return prims;
});
In detail
Method
Shape byte codes as the file header lists them: vectors in 16 directions, displacements, octant arcs, fractional arcs and bulge arcs.
Number types
64-bit numbers; byte values from the font file.
Tolerances
Arc chord step π ÷ 24 (7.5°), at least 2 chords per arc.
Limits
Arcs are polygons. A character the font lacks advances 0.6 × the text height, and a space with no shape 0.5 × the height. The header says the reader was tested on fonts made for the purpose.
Approach: 64-bit, stated limit
The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.