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