Takes the numbers a person types in the sketch editor (a dimension's value, the sizes of a shape being drawn) and turns them into stored values and point coordinates.
How the precision is kept
Typed text is converted with Number, which gives the nearest 64-bit number to the decimal typed, and the points of a shape drawn from typed sizes are one 64-bit expression each. The input box is filled with the current value shown to at most 4 decimals, and Enter or leaving the box applies whatever text it holds, so a value accepted without retyping is stored as that 4-decimal text.
Open item. Fill the input box with the full value, or leave the stored value untouched when the text was not edited, so that accepting a dimension does not round it to 4 decimals.
Calculations under this policy
8 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.
Reads a typed size such as 12.5, ⌀20, R5, 30° or 12 mm as a number.
After the spaces, a leading ⌀, Ø or R and a trailing mm or ° are removed, the text must match a plain decimal or exponent pattern and is converted with Number, which rounds correctly to the nearest 64-bit value. Anything else gives null and nothing is stored.
sketchview.js
lines 1271 to 1277
PartCraft 0.8.1
export function parseNumber(s) {
if (typeof s !== 'string') return null;
const t = s.replace(/\s+/g, '').replace(/^[⌀ØR]/i, '').replace(/(mm|°)$/i, '');
if (!/^[-+]?(\d+\.?\d*|\.\d+)(e[-+]?\d+)?$/i.test(t)) return null;
const v = Number(t);
return Number.isFinite(v) ? v : null;
}
Value shown in the input box
64-bit
Opens the input box at a value for the person to accept or change.
box text = fmt(value) (at most 4 decimals)
The box is filled with fmt(value), which vec.js rounds to at most 4 decimals. The number that comes back is parsed from this text, so an unchanged box returns the rounded value, not the one passed in.
sketchview.js
lines 747 to 764
PartCraft 0.8.1
askValue([x, y], value, label, done, typed = null) {
this.inputRaw = null;
this.inputTab = null; this.inputBlur = null;
const inp = this.input;
inp.hidden = false;
inp.value = fmt(value);
inp.setAttribute('aria-label', label);
const { w } = this.size();
inp.style.left = Math.max(4, Math.min(x - 45, w - 100)) + 'px';
inp.style.top = Math.max(4, y - 14) + 'px';
this.inputCb = done;
this.inputClosing = false;
// a key already typed (0.6 PU.1): the box takes the focus now, so the next keys land in it, after that one;
// `true` = focus now with the value selected (Tab to the next size box)
if (typed === true) { inp.focus(); inp.select(); return; }
if (typed !== null) { inp.value = typed; inp.focus(); inp.setSelectionRange(typed.length, typed.length); return; }
setTimeout(() => { inp.focus(); inp.select(); }, 0);
}
Applying the typed value
64-bit
Takes the text of the input box when it closes and hands the number to whatever asked for it.
The text is applied on Enter and also when the box loses focus; only Escape leaves the old value. A dimension driven by an equation is handed to the host as raw text first.
sketchview.js
lines 766 to 782
PartCraft 0.8.1
inputDone(apply) {
const cb = this.inputCb;
if (!cb) return;
this.inputCb = null;
this.inputClosing = true;
const raw = this.input.value.trim();
const rawCb = this.inputRaw;
this.inputRaw = null;
this.hideInput();
if (apply && rawCb && rawCb(raw)) { /* handled as an equation */ }
else if (apply) {
const v = parseNumber(raw);
if (v === null) this.host.status(t('ui.sketch.not_number'));
else cb(v);
}
this.host.refocus?.();
}
Changing a dimension's value
64-bit
Stores the typed number as the dimension's value.
0 ≤ value; angle ≤ 180; radius and diameter > 0
The stored value is exactly the number parsed from the box. Because the box opens on a 4-decimal text of the current value and a new dimension opens it at once, a measured value such as 12.34567891 becomes 12.3457 unless the box is closed with Escape; the solver then moves the geometry to the rounded value.
sketchview.js
lines 731 to 745
PartCraft 0.8.1
/** Type a dimension's value. `fresh`: the dimension was just made, so the value joins that undo step. */
editDimension(id, fresh = false) {
const c = this.sk.constraints.find((k) => k.id === id);
if (!c) return;
const box = this.dimBoxes.find((b) => b.id === id);
const at = box ? [box.x + box.w / 2, box.y + box.h / 2] : this.toScreen(dimGeometry(this.sk, c)?.at || [0, 0]);
this.askValue(at, c.value, S.CONSTRAINTS[c.type].label, (v) => {
try {
if (!(v >= 0) || (c.type === 'angle' && v > 180) || ((c.type === 'radius' || c.type === 'diameter') && v <= 0)) throw new Error(c.type === 'angle' ? t('ui.sketch.dim.bad_angle') : t('ui.sketch.dim.positive'));
this.host.commit(t('ui.sketch.op.change_dimension'), (sk) => { const k = sk.constraints.find((q) => q.id === id); if (k) k.value = v; }, { squash: fresh });
} catch (err) { this.host.status(err.message); }
});
// "=width / 2", or a number for a dimension an equation drives: the parameters handle it (params-panel.js)
this.inputRaw = this.host.dimTyped ? (raw) => this.host.dimTyped(id, raw) : null;
}
Keeping a typed size
64-bit
Checks a size typed while drawing and keeps it for the shape.
angle ← ((v mod 360) + 360) mod 360; sweep must be in (0, 360); other sizes > 0
Lengths are kept as typed. An angle is brought into [0, 360) with two remainders, which is exact for whole degrees and can change the last bit of other values.
sketchview.js
lines 842 to 852
PartCraft 0.8.1
/** Keep a typed size for the shape being drawn. → false (and why, in the status bar) when it can't be one. */
ddSet(k, v) {
const st = this.step;
if (!st) return false;
if (k === 'ang') { st.dd.ang = ((v % 360) + 360) % 360; this.draw(); return true; }
if (k === 'sweep' && !(v > 0 && v < 360)) { this.host.status(t('ui.sketch.dd.bad_sweep')); return false; }
if (!(v > 0)) { this.host.status(t('ui.sketch.dim.positive')); return false; }
st.dd[k] = v;
this.draw();
return true;
}
Point from typed sizes
64-bit
Where the typed length and angle, width and height, diameter, radius or sweep put the next point of the shape being drawn.
line: a + (cos r, sin r) × len, r = angle × π ÷ 180; rectangle: a ± (w, h); arc end: c + R(sweep) · (s − c)
Each coordinate is one 64-bit expression. A typed angle of exactly 0 or 90 modulo 180 also adds a horizontal or vertical relation, which the solver then holds; cos and sin of other angles carry the rounding of the conversion with Math.PI. The click handler also adds the typed length, width, height, diameter or radius as a driving dimension.
sketchview.js
lines 853 to 882
PartCraft 0.8.1
/** Where the typed sizes (and the pointer, for the rest) put the shape's next point, in sketch coordinates. */
ddPoint() {
const st = this.step, tl = this.tool, dd = st.dd || {}, live = this.ddLive();
const cur = this.snapAt?.xy || this.cursor || [0, 0];
const dirFrom = (o, q = cur) => { const dx = q[0] - o[0], dy = q[1] - o[1], l = Math.hypot(dx, dy); return l > 1e-12 ? [dx / l, dy / l] : [1, 0]; };
if (tl === 'line' || tl === 'centerline') {
const a = st.a.xy;
let dir, hv = null;
if (dd.ang !== undefined) {
const r = dd.ang * Math.PI / 180, m = dd.ang % 180;
dir = [Math.cos(r), Math.sin(r)];
if (this.autoRelations) hv = m === 0 ? 'horizontal' : m === 90 ? 'vertical' : null;
} else { const q = this.hvSnap(st.a, { xy: cur }); hv = q.hv; dir = dirFrom(a, q.b.xy); }
st.ddHv = hv;
const len = dd.len ?? live.len ?? 0;
return [a[0] + dir[0] * len, a[1] + dir[1] * len];
}
if (tl === 'rect') {
const a = st.a.xy, sx = cur[0] >= a[0] ? 1 : -1, sy = cur[1] >= a[1] ? 1 : -1;
return [a[0] + sx * (dd.w ?? live.w ?? 0), a[1] + sy * (dd.h ?? live.h ?? 0)];
}
if (tl === 'circle') { const c = st.c.xy, r = (dd.d ?? live.d ?? 0) / 2, u = dirFrom(c); return [c[0] + u[0] * r, c[1] + u[1] * r]; }
if (tl === 'arc' && !st.s) { const c = st.c.xy, r = dd.r ?? live.r ?? 0, u = dirFrom(c); return [c[0] + u[0] * r, c[1] + u[1] * r]; }
if (tl === 'arc') {
const c = st.c.xy, s = st.s.xy, sw = (dd.sweep ?? live.sweep ?? 0) * Math.PI / 180 * ((st.turn || 0) < 0 ? -1 : 1);
const vx = s[0] - c[0], vy = s[1] - c[1];
return [c[0] + vx * Math.cos(sw) - vy * Math.sin(sw), c[1] + vx * Math.sin(sw) + vy * Math.cos(sw)];
}
return cur;
}
Snap to horizontal or vertical
64-bit
Makes a line drawn within 3° of horizontal or vertical exactly so.
angle° = |atan2(Δy, Δx)| × 180 ÷ π; within 3° of 0, 180 or 90: copy the start's y or x to the end
The end point takes the start point's coordinate unchanged, so the two are bit-for-bit equal, and a horizontal or vertical relation is added. Not applied when the end snapped to an existing point or when automatic relations are off.
sketchview.js
lines 621 to 629
PartCraft 0.8.1
/** Lines within 3° of horizontal or vertical snap to it (unless the end snapped to a point). */
hvSnap(a, b) {
if (b.ref || !this.autoRelations) return { b, hv: null };
const dx = b.xy[0] - a.xy[0], dy = b.xy[1] - a.xy[1];
const ang = Math.abs(Math.atan2(dy, dx) * 180 / Math.PI);
if (Math.min(ang, 180 - ang) < 3) return { b: { ...b, xy: [b.xy[0], a.xy[1]], on: null }, hv: 'horizontal' };
if (Math.abs(ang - 90) < 3) return { b: { ...b, xy: [a.xy[0], b.xy[1]], on: null }, hv: 'vertical' };
return { b, hv: null };
}
Point snapped onto a curve
64-bit
The nearest point on a line, circle, arc or spline to the pointer, used when a new point snaps onto a curve.
line: a + t·d, t = ((p − a) · d) ÷ (d · d); circle: c + (p − c) × r ÷ |p − c|
One projection in 64-bit. The line is treated as unbounded; a zero-length line divides by 1e-300 in place of zero and a point at a circle's center divides by 1.
sketchview.js
lines 1313 to 1324
PartCraft 0.8.1
function project(sk, e, p) {
if (e.type === 'spline') return splineNearest(sk, e, p) || p;
if (e.type === 'line') {
const a = S.pointXY(sk, e.p1), b = S.pointXY(sk, e.p2);
const dx = b[0] - a[0], dy = b[1] - a[1], l2 = dx * dx + dy * dy || 1e-300;
const t = ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / l2;
return [a[0] + t * dx, a[1] + t * dy];
}
const c = S.pointXY(sk, e.c), r = S.radiusOf(sk, e);
const d = Math.hypot(p[0] - c[0], p[1] - c[1]) || 1;
return [c[0] + (p[0] - c[0]) / d * r, c[1] + (p[1] - c[1]) / d * r];
}
In detail
Method
Text checked against a number pattern, then Number(text); a point at a typed length and angle is a + (cos, sin) × length; an arc's end is the start vector rotated by the typed sweep.
Number types
64-bit numbers.
Tolerances
Lines within 3° of horizontal or vertical are snapped to it. A direction shorter than 1e-12 falls back to the x axis.
Limits
The box shows fmt(value) from vec.js: at most 4 decimals, and a size under 1e-9 as 0. A new dimension opens this box at once with its measured value.
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.