Resources Numerical accuracy Calculation policy

Drawing annotations: tolerance frames, datums and surface finish

Lays out feature control frames, datum symbols and surface texture symbols on a drawing sheet and writes their tolerance and roughness values.

How the precision is kept

Symbol shapes are built in sheet millimeters from fixed proportions of the lettering height in 64-bit. The tolerance and roughness values are kept as entered in the part file; only their text on the sheet is rounded, to 4 decimals.

Calculations under this policy

3 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.

gdt.js PartCraft 0.8.1 64-bit 3 calculations Reviewed

Tests bin/test-partcraft.mjs, tools/partcraft-test/test-gdt.mjs

Tolerance and roughness text

64-bit

The text of a tolerance value or an Ra value on the drawing.

text = value rounded to 4 decimals

Written with toFixed(4) and trailing zeros removed. Only the text is rounded: a tolerance entered with more than 4 decimals stays unrounded in the part file but prints rounded.

  • gdt.js
  • line 141
  • PartCraft 0.8.1
const num = (v) => String(Number(v.toFixed(4)));

Circle and arc points

64-bit

The points of a circle or arc in a symbol.

(c_x + r cos t, c_y + r sin t), t = a₀ + sweep × i ÷ n

Each angle is computed from the index, not accumulated. A circle is drawn as a polyline of n chords (32 by default), so the drawn shape lies inside the true circle by up to r (1 − cos(π ÷ n)).

  • gdt.js
  • lines 155 to 161
  • PartCraft 0.8.1
export function circlePts(c, r, n = 32, a0 = 0, sweep = 2 * Math.PI) {
  const pts = [];
  const closed = Math.abs(sweep - 2 * Math.PI) < 1e-12;
  const m = closed ? n : n;
  for (let i = 0; i <= m; i++) { const t = a0 + sweep * i / n; pts.push([c[0] + r * Math.cos(t), c[1] + r * Math.sin(t)]); }
  return pts;
}

Annotation layout

64-bit

The frame, compartments, leader, datum triangle and surface texture legs of one annotation.

frame height = 2h; datum triangle height = side × √3 ÷ 2; texture legs at 60°: offset = leg ÷ tan(π ÷ 3)

Positions are sums of fixed proportions of the lettering height (3.5 mm) and text widths, in 64-bit. The square root and tangent are computed with Math functions, not typed as rounded constants.

  • gdt.js
  • lines 254 to 339
  • PartCraft 0.8.1
/**
 * Geometry of one annotation with its attachment point at `anchor` (sheet mm).
 * → { strokes:[{width, segs}], fills:[{pts}], texts, box:[x0,y0,x1,y1], parts? }
 * `parts` (fcf) lists the compartment rectangles, for tests and hit tests.
 */
export function annotationGeometry(a, anchor) {
  const h = DRAW.h;
  const frame = { width: DRAW.frame, segs: [] }, leader = { width: DRAW.leader, segs: [] };
  const fills = [], texts = [];
  const P = [anchor[0] + a.off[0], anchor[1] + a.off[1]];
  const out = { strokes: [frame, leader], fills, texts };
  const rect = (x0, y0, x1, y1, segs) => segs.push([x0, y0, x1, y0], [x1, y0, x1, y1], [x1, y1, x0, y1], [x0, y1, x0, y0]);
  const leaderTo = (from) => { if (Math.hypot(anchor[0] - from[0], anchor[1] - from[1]) > DRAW.arrowL * 1.2) { leader.segs.push([from[0], from[1], anchor[0], anchor[1]]); arrowHead(fills, anchor, from); } };

  if (a.kind === 'fcf') {
    const H = 2 * h, pad = DRAW.pad, y0 = P[1] - H / 2, y1 = P[1] + H / 2, ty = P[1] - 0.35 * h;
    const parts = [];
    let x = P[0];
    // [symbol]
    parts.push({ kind: 'char', box: [x, y0, x + H, y1] });
    charSymbol(a.char, [x + H / 2, P[1]], frame.segs, fills);
    x += H;
    // [dia tol mod]
    const tolText = num(a.tol);
    let w = pad + (a.dia ? DIA_W : 0) + textW(tolText, h) + (a.mod ? 0.6 + MOD_W : 0) + pad;
    w = Math.max(w, H);
    let cx = x + pad;
    if (a.dia) { diaSymbol([cx + DIA_W / 2, P[1]], frame.segs); cx += DIA_W; }
    texts.push({ x: cx, y: ty, size: h, text: tolText, anchor: 'start' });
    cx += textW(tolText, h);
    if (a.mod) modSymbol(a.mod, [cx + 0.6 + MOD_W / 2, P[1]], frame.segs);
    parts.push({ kind: 'tol', box: [x, y0, x + w, y1] });
    x += w;
    // [datum mod] …
    for (const d of a.datums) {
      const dw = Math.max(H, pad + textW(d.letter, h) + (d.mod ? 0.6 + MOD_W : 0) + pad);
      const inner = textW(d.letter, h) + (d.mod ? 0.6 + MOD_W : 0);
      const sx = x + (dw - inner) / 2;
      texts.push({ x: sx, y: ty, size: h, text: d.letter, anchor: 'start' });
      if (d.mod) modSymbol(d.mod, [sx + textW(d.letter, h) + 0.6 + MOD_W / 2, P[1]], frame.segs);
      parts.push({ kind: 'datum', letter: d.letter, box: [x, y0, x + dw, y1] });
      x += dw;
    }
    rect(P[0], y0, x, y1, frame.segs);
    for (const p of parts.slice(1)) frame.segs.push([p.box[0], y0, p.box[0], y1]);
    const fb = [P[0], y0, x, y1];
    if (!inside(fb, anchor)) leaderTo(nearestOnBox(fb, anchor));
    out.parts = parts;
    out.frameBox = fb;
  } else if (a.kind === 'datum') {
    // square frame 2h centred at P, letter centred; leader to an equilateral filled triangle whose base sits on the feature at the anchor
    const S = 2 * h;
    const fb = [P[0] - S / 2, P[1] - S / 2, P[0] + S / 2, P[1] + S / 2];
    rect(fb[0], fb[1], fb[2], fb[3], frame.segs);
    texts.push({ x: P[0], y: P[1] - 0.35 * h, size: h, text: a.letter, anchor: 'middle' });
    // the triangle sits on the feature (drawn square to the nearer sheet axis, as a surface seen
    // edge-on in an orthographic view); the leader may run at any angle from the frame to its apex
    const dx = P[0] - anchor[0], dy = P[1] - anchor[1];
    const vertical = Math.abs(dx) <= 2 * Math.abs(dy);   // apex up or down unless the leader runs mostly sideways
    const ux = vertical ? 0 : Math.sign(dx), uy = vertical ? (dy < 0 ? -1 : 1) : 0;   // from the feature toward the frame
    const side = 3, th = side * Math.sqrt(3) / 2;
    const apex = [anchor[0] + ux * th, anchor[1] + uy * th];
    fills.push({ pts: [[anchor[0] - uy * side / 2, anchor[1] + ux * side / 2], [anchor[0] + uy * side / 2, anchor[1] - ux * side / 2], apex] });
    const start = nearestOnBox(fb, apex);
    if (!inside(fb, apex) && Math.hypot(start[0] - apex[0], start[1] - apex[1]) > 1e-9) leader.segs.push([start[0], start[1], apex[0], apex[1]]);
    out.frameBox = fb;
    out.triangle = fills[0].pts;
  } else if (a.kind === 'finish') {
    // ISO 1302: legs at 60° to the surface, short leg H1 = 1.4h, long leg H2 = 3h. The requirement
    // (here Ra) is written under the long leg's horizontal extension (ISO 1302 position "a").
    const H1 = 1.4 * h, H2 = 3 * h, k = 1 / Math.tan(Math.PI / 3);
    const tip = P;
    const sTop = [tip[0] - H1 * k, tip[1] + H1], lTop = [tip[0] + H2 * k, tip[1] + H2];
    frame.segs.push([tip[0], tip[1], sTop[0], sTop[1]], [tip[0], tip[1], lTop[0], lTop[1]]);
    if (a.process === 'removal') frame.segs.push([sTop[0], sTop[1], tip[0] + H1 * k, tip[1] + H1]);
    if (a.process === 'noRemoval') { const rc = H1 / 3; poly(circlePts([tip[0], tip[1] + 2 * rc], rc, 32), frame.segs); out.circle = { c: [tip[0], tip[1] + 2 * rc], r: rc }; }
    const raText = 'Ra ' + num(a.ra);
    const ext = Math.max(textW(raText, h), a.note ? textW(a.note, 2.5) : 0) + 2;
    frame.segs.push([lTop[0], lTop[1], lTop[0] + ext, lTop[1]]);
    texts.push({ x: lTop[0] + 1, y: lTop[1] - 1 - 0.75 * h, size: h, text: raText, anchor: 'start' });
    if (a.note) texts.push({ x: lTop[0] + 1, y: lTop[1] + 1, size: 2.5, text: a.note, anchor: 'start' });
    if (Math.hypot(a.off[0], a.off[1]) > 1e-9) leaderTo(tip);
  } else throw new Error(tr('model.gdt.unknown_kind'));
  out.box = boxOf(out.strokes, fills, texts);
  return out;
}

In detail

Method
Polylines and filled polygons at proportions the file header takes from ASME Y14.5-2018 and ISO 1302; circles as 32-segment polylines; text widths from Helvetica character widths.
Number types
64-bit numbers.
Tolerances
A leader is drawn only when it is longer than 1.2 arrow lengths; a datum leader when longer than 1e-9 mm.
Limits
The file header says the shapes are drawn to the stated ratios, not traced from the standards' figures.

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.

Every policy that takes this approach