Turns a part's lengths, angles, points and check values into the numbers written in the exported .swb macro text, in meters and radians.
How the precision is kept
Every value is computed as a 64-bit number and then written as text. The text format sets the accuracy: all numbers are cut to 15 significant digits, and lengths and points are first rounded to a whole 1e-6 mm before the change to meters. Angles are converted with Math.PI. The body check the macro runs uses fixed tolerances written into the macro text.
Open item. Write lengths and points with the 15-digit formatter alone (or 17 digits) instead of rounding them to 1e-6 mm first.
Calculations under this policy
11 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 value is printed with toPrecision(15), so the 16th and 17th significant digits of a 64-bit number are not written. Anything smaller than 1e-14 in absolute size is written as 0, whatever the unit. A value that is not finite throws instead of being written.
swmacro.js
lines 169 to 173
PartCraft 0.8.1
function n(v) {
if (!Number.isFinite(v)) throw new Error(t('xch.swm.not_finite'));
if (Math.abs(v) < 1e-14) return '0';
return String(Number(v.toPrecision(15))).replace('e', 'E').replace('E+', 'E');
}
Millimeters to meters
64-bit
Converts a length or a coordinate from millimeters to the meters the macro uses.
m = round(mm × 10⁶) ÷ 10⁹
The value is rounded to a whole number of 1e-6 mm with Math.round before the division, so the exported value is not the 64-bit value: a 1000 mm coordinate keeps 10 significant digits. The division of that integer by 1e9 is a single correctly rounded operation. The code comment gives the reason as dropping kernel and solver noise under 1e-6 mm.
swmacro.js
line 174
PartCraft 0.8.1
const m = (mm) => n(Math.round(mm * 1e6) / 1e9); // mm → metres (kernel and solver noise under 1e-6 mm dropped)
Degrees to radians
64-bit
Converts an angle in degrees to the radians the macro uses.
rad = deg × π ÷ 180
One 64-bit expression with Math.PI, then written with 15 significant digits. No rounding of the angle before the conversion.
swmacro.js
line 176
PartCraft 0.8.1
const rad = (deg) => n(deg * Math.PI / 180);
Unit-vector component
64-bit
Writes a component of a sketch plane's normal or x direction.
u = round(v × 10¹²) ÷ 10¹²
Each component is rounded to 12 decimals before it is written, so the written vector is unit length only to about 1e-12. The macro uses these components to find the sketch's sign and axis with a 1e-6 test.
swmacro.js
line 177
PartCraft 0.8.1
const u = (v) => n(Math.round(v * 1e12) / 1e12); // a unit-vector component
Sketch dimension value and text position
64-bit
Works out the value of each sketch dimension in meters or radians and where its text sits.
length: value ÷ 1000; angle: value × π ÷ 180; arc radius = |start − center|
The dimension value is the stored constraint value divided by 1000 (or the radius measured from the solved points with Math.hypot) and is written with the 15-digit formatter, not with the 1e-6 mm rounding used for the sketch points. The text position uses fixed offsets (15 % of the value, at least 5 mm) and is only a placement. The line intersection used to place an angle dimension treats unit directions with a cross product below 1e-12 as parallel.
swmacro.js
lines 1139 to 1191
PartCraft 0.8.1
function dimensionLine(f, c, ents, kinds, k, frame) {
const sk = f.sketch;
const P = (e) => [e.x, e.y];
const L = (e) => [pointXY(sk, e.p1), pointXY(sk, e.p2)];
const mid = (a, b) => [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
const perp = (a, b, d) => { const dx = b[0] - a[0], dy = b[1] - a[1], l = Math.hypot(dx, dy) || 1; return [-dy / l * d, dx / l * d]; };
let at, kind = 0, sys = c.value / 1000, k1 = k[0], k2 = k[1] || '', factor = 1;
const size = Math.max(5, Math.abs(c.value) * 0.15);
if (c.type === 'radius' || c.type === 'diameter') {
const e = ents[0];
const r = e.type === 'circle' ? e.r : Math.hypot(...sub2(pointXY(sk, e.s), pointXY(sk, e.c)));
const cc = pointXY(sk, e.c);
let ang = Math.PI / 4;
if (e.type === 'arc') { const a = arcAngles(sk, e); ang = a.a0 + a.sweep / 2; }
at = [cc[0] + 1.4 * r * Math.cos(ang), cc[1] + 1.4 * r * Math.sin(ang)];
// SolidWorks dimensions a circle by its diameter and an arc by its radius
const swDiam = e.type === 'circle';
sys = (swDiam ? 2 * r : r) / 1000;
factor = c.type === 'radius' ? (swDiam ? 2 : 1) : (swDiam ? 1 : 0.5);
} else if (c.type === 'angle') {
const [a1, b1] = L(ents[0]), [a2, b2] = L(ents[1]);
const u = norm2(sub2(b1, a1)), v = norm2(sub2(b2, a2));
const X = intersect2(a1, u, a2, v) || mid(mid(a1, b1), mid(a2, b2));
let bis = [u[0] + v[0], u[1] + v[1]];
if (Math.hypot(...bis) < 1e-9) bis = [-u[1], u[0]];
bis = norm2(bis);
const reach = 0.6 * Math.min(Math.hypot(...sub2(b1, a1)), Math.hypot(...sub2(b2, a2)));
at = [X[0] + bis[0] * reach, X[1] + bis[1] * reach];
sys = c.value * Math.PI / 180;
} else if (kinds === 'L') {
const [a, b] = L(ents[0]);
at = c.type === 'distance' ? add2(mid(a, b), perp(a, b, size)) : c.type === 'hdistance' ? [(a[0] + b[0]) / 2, Math.max(a[1], b[1]) + size] : [Math.max(a[0], b[0]) + size, (a[1] + b[1]) / 2];
kind = c.type === 'hdistance' ? 1 : c.type === 'vdistance' ? 2 : 0;
} else if (kinds === 'PP') {
const a = ents[0].type === 'point' ? P(ents[0]) : null, b = P(ents[1]);
const aa = a || [0, 0];
at = c.type === 'distance' ? add2(mid(aa, b), perp(aa, b, size)) : c.type === 'hdistance' ? [(aa[0] + b[0]) / 2, Math.max(aa[1], b[1]) + size] : [Math.max(aa[0], b[0]) + size, (aa[1] + b[1]) / 2];
kind = c.type === 'hdistance' ? 1 : c.type === 'vdistance' ? 2 : 0;
} else if (kinds === 'PL') {
const p = P(ents[0]), [a, b] = L(ents[1]);
const d = norm2(sub2(b, a)), t = dot2(sub2(p, a), d), foot = [a[0] + d[0] * t, a[1] + d[1] * t];
at = add2(mid(p, foot), [d[0] * size, d[1] * size]);
} else if (kinds === 'LL') {
const [a, b] = L(ents[0]), [c2, d2] = L(ents[1]);
const m1 = mid(a, b), d = norm2(sub2(d2, c2)), t = dot2(sub2(m1, c2), d), foot = [c2[0] + d[0] * t, c2[1] + d[1] * t];
at = mid(m1, foot);
} else return null;
if (Array.isArray(c.off) && c.off.length === 2 && c.off.every(Number.isFinite) && c.type !== 'angle') at = add2(at, [c.off[0] * 0.25, c.off[1] * 0.25]);
const w = toWorld(frame, at[0], at[1]);
const nm = swName(c.id);
dimInfo.set(`${f.id}.${c.id}`, { full: `${nm}@${swNames.get(f.id)}`, factor });
return [`Call SkDim(${q(nm)}, ${kind}, ${q(k1)}, ${q(k2)}, ${xyz(w)}, ${n(sys)})`]; // i18n-ignore: VBA macro text
}
Pick point on a face
64-bit
Finds a point inside a face, used by the macro to select that face.
centroid of the largest triangle = (a + b + c) ÷ 3, moved onto the plane: p − ((p − o)·n) n
Triangle areas come from the cross product of two edges and the centroid from a sum times 1 ÷ 3, in 64-bit arithmetic on the vertex array handed in; this file does not set that array's number type. For a planar face the centroid is put back on the face plane, which assumes the stored normal is unit length. The point is then written through the millimeter-to-meter conversion.
swmacro.js
lines 926 to 943
PartCraft 0.8.1
/** A point inside a face (largest of its triangles, its centroid put back on the face's plane), or null. */
function facePoint(rg, test) {
if (!rg || !rg.faces || !rg.mesh || !rg.mesh.triFace) return null;
const ok = new Set(rg.faces.map((f, i) => (test(f, i) ? i : -1)).filter((i) => i >= 0));
if (!ok.size) return null;
const P = rg.mesh.positions, I = rg.mesh.indices, TF = rg.mesh.triFace;
let best = -1, ba = 0, bc = null;
for (let t = 0; t < TF.length; t++) {
if (!ok.has(TF[t])) continue;
const a = [P[I[3 * t] * 3], P[I[3 * t] * 3 + 1], P[I[3 * t] * 3 + 2]], b = [P[I[3 * t + 1] * 3], P[I[3 * t + 1] * 3 + 1], P[I[3 * t + 1] * 3 + 2]], c = [P[I[3 * t + 2] * 3], P[I[3 * t + 2] * 3 + 1], P[I[3 * t + 2] * 3 + 2]];
const area = len(cross(sub(b, a), sub(c, a))) / 2;
if (area > ba) { ba = area; best = t; bc = mul(add(add(a, b), c), 1 / 3); }
}
if (best < 0) return null;
const face = rg.faces[TF[best]];
if (face.type === 'plane' && face.n && face.o) bc = sub(bc, mul(face.n, dot(sub(bc, face.o), face.n)));
return bc;
}
Countersink depth and taper
64-bit
The depth and half angle of the tapered cut that stands in for a countersink.
One 64-bit expression each. The division by the tangent is not guarded: a countersink angle of 0 gives a non-finite depth, which the number formatter then refuses to write.
Writes the part's volume and bounding box after a feature, for the macro to compare with what it built.
volume in m³ = volume in mm³ × 10⁻⁹; box corners through the millimeter-to-meter conversion
The volume is multiplied by 1e-9, which is not exactly representable, so the product carries one rounding; it is written with 15 digits. The six box values are rounded to 1e-6 mm like every other length.
The check is written as VBA text with Double variables. The volume tolerance is relative (1e-4) with a 1e-15 m³ floor; the box tolerance is 1e-5 m plus 2 % of the expected box diagonal, so the box test is loose by design. A feature whose direction is wrong is flipped once when this check fails.
swmacro.js
lines 775 to 794
PartCraft 0.8.1
'Function Expect(ByVal vol As Double, ByVal x0 As Double, ByVal y0 As Double, ByVal z0 As Double, ByVal x1 As Double, ByVal y1 As Double, ByVal z1 As Double) As Integer', // i18n-ignore: VBA macro text
' Dim mProp As Object', // i18n-ignore: VBA macro text
' Dim b As Variant', // i18n-ignore: VBA macro text
' Dim v As Double', // i18n-ignore: VBA macro text
' Dim tol As Double', // i18n-ignore: VBA macro text
' Expect = -1',
' On Error GoTo NoCheck',
' Set mProp = swExt.CreateMassProperty',
' If mProp Is Nothing Then Exit Function', // i18n-ignore: VBA macro text
' v = mProp.Volume',
' b = swModel.GetPartBox(True)',
' tol = 0.00001 + 0.02 * Sqr((x1 - x0) ^ 2 + (y1 - y0) ^ 2 + (z1 - z0) ^ 2)',
' Expect = 1',
' If Abs(v - vol) > 0.0001 * Abs(vol) + 1E-15 Then Expect = 0',
' If Abs(b(0) - x0) > tol Or Abs(b(1) - y0) > tol Or Abs(b(2) - z0) > tol Then Expect = 0',
' If Abs(b(3) - x1) > tol Or Abs(b(4) - y1) > tol Or Abs(b(5) - z1) > tol Then Expect = 0',
' Exit Function', // i18n-ignore: VBA macro text
'NoCheck:',
' Expect = -1',
'End Function', // i18n-ignore: VBA macro text
Final volume comparison
64-bit
Writes the finished part's volume into the macro and the test that reports whether the rebuilt part matches.
The expected volume is written in mm³ with 15 digits. The macro multiplies its own volume by 1e9 and compares with a relative tolerance of 1e-4; the message prints its volume with three decimals.
swmacro.js
lines 1600 to 1612
PartCraft 0.8.1
const vol = ctx.regen?.stats?.volume;
fin.push('Call swModel.ClearSelection2(True)', 'ok = swModel.ForceRebuild3(False)', 'Call swModel.ViewZoomtofit2');
if (Number.isFinite(vol) && checksOn) {
fin.push('Set mProp = swExt.CreateMassProperty', 'If Not mProp Is Nothing Then', // i18n-ignore: VBA macro text
` v = mProp.Volume * 1000000000#`,
` summary = "Volume in SolidWorks: " & Format(v, "0.000") & " mm3; in PartCraft: ${n(vol)} mm3"`,
` If Abs(v - ${n(vol)}) <= 0.0001 * ${n(vol)} Then`,
' summary = summary & " (the same)."',
' Else',
' summary = summary & " (different: see the log)."',
' End If', // i18n-ignore: VBA macro text
' Call Note(summary)', // i18n-ignore: VBA macro text
'End If'); // i18n-ignore: VBA macro text
Numbers in equations
64-bit
Writes a parameter expression as equation text for the macro, including its numeric constants.
Numeric constants are written with formatNumber from expr.js, which uses toPrecision(12), so constants in equations carry 12 significant digits while geometry carries 15. The same formatter writes a variable's value when its expression has no counterpart, and the factor applied to a linked dimension.
swmacro.js
lines 888 to 911
PartCraft 0.8.1
/** SolidWorks equation text for a parsed expression, or throws Error(the reason it has no counterpart). */
export function swExpr(ast, varName = (v) => `"${v}"`) {
const w = (x, inCond = false) => {
switch (x.k) {
case 'num': return x.name === 'pi' ? 'pi' : formatNumber(x.v);
case 'var': return varName(x.name);
case 'neg': return `-(${w(x.a)})`;
case 'bin':
if (x.op === '%') throw new Error(t('xch.swm.expr.percent'));
return `(${w(x.a)} ${x.op} ${w(x.b)})`;
case 'cmp':
if (!inCond) throw new Error(t('xch.swm.expr.comparison'));
return `${w(x.a)} ${CMP[x.op]} ${w(x.b)}`;
case 'call':
if (x.name === 'if') return `iif(${bare(w(x.args[0], true))}, ${bare(w(x.args[1]))}, ${bare(w(x.args[2]))})`;
if (x.name === 'pow') return `((${bare(w(x.args[0]))}) ^ (${bare(w(x.args[1]))}))`;
if (x.name === 'log') return `(log(${bare(w(x.args[0]))}) / log(10))`; // i18n-ignore: VBA macro text
if (SW_FN[x.name]) return `${SW_FN[x.name]}(${bare(w(x.args[0]))})`;
throw new Error(t('xch.swm.expr.function', { name: x.name }));
default: throw new Error(t('xch.swm.expr.unknown'));
}
};
return bare(w(ast));
}
In detail
Method
Millimeters to meters by rounding to 1e-6 mm and dividing by 1e9; degrees to radians by × π ÷ 180; unit-vector components rounded to 12 decimals; all numbers printed with toPrecision(15).
Number types
64-bit numbers, written as decimal text with up to 15 significant digits.
Tolerances
A value below 1e-14 in size is written as 0 (absolute). Lengths rounded to 1e-6 mm (absolute). Unit-vector components rounded to 1e-12. Macro body check: volume within 1e-4 of the expected volume plus 1e-15 m³; box corners within 1e-5 m plus 2 % of the box diagonal. A revolve or circular pattern counts as full when its angle is within 1e-9° of 360. Parallel-line test in the angle dimension placement: cross product below 1e-12 for unit directions.
Checked by
bin/test-partcraft-sw.mjs calls partToSwb on a flange part and checks that the sketch values are in meters and that the body-check line carries the volume 302160π mm³ in m³ and the box in meters. The file's own header says the macro has not been run in the target program.
Limits
A length of 1000 mm keeps 10 significant digits after the 1e-6 mm rounding. Dimension values are written without that rounding (15 digits), so a dimension and the points it measures can differ by up to 1e-6 mm. A spline that is not an open fit-point spline is written as sampled points.
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.