Resources Numerical accuracy Calculation policy

SHX font text: strokes and width

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.js CadCraft 0.8.2 64-bit 5 calculations Reviewed

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.

  • shx.js
  • lines 121 to 125
  • CadCraft 0.8.2
const move = (dx, dy) => {
  const nx = st.x + dx * st.mul / st.div, ny = st.y + dy * st.mul / st.div;
  if (st.down) { if (!st.cur) { st.cur = [st.x, st.y]; st.strokes.push(st.cur); } st.cur.push(nx, ny); } else st.cur = null;
  st.x = nx; st.y = ny;
};

Arcs as chords

64-bit

Draws an arc of a shape as short straight pieces.

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 };
}
shx-text.js CadCraft 0.8.2 64-bit 2 calculations Reviewed

Tests bin/test-cadcraft.mjs

Width of a text in an SHX font

64-bit

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.

Every policy that takes this approach