Reference geometry, sweep paths and extrude extents
Finds points on edges for reference planes, axes and points, chains curves into a sweep path, and works out how far an extrude runs when it stops at a face, a vertex or the next face.
How the precision is kept
The solids themselves are built by OpenCascade; this file prepares the numbers it is given. Points on general curves are found by sampling the edge and then refining (golden section, bisection, Gauss–Legendre quadrature), so their accuracy is set by the sample count and the stopping rule. Circles are handled in closed form. Extents are 64-bit dot products with tolerances scaled to the sketch.
Calculations under this policy
9 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 point of an edge closest to a given point, with the tangent there.
circle: p = c + r × unit(radial part of (q − c)); other curves: minimize |C(t) − q|²
A circular edge is solved in closed form from its center, axis and radius. Other curves are sampled at 201 parameters and the best bracket is refined by 80 golden-section steps that compare squared distances. A minimum found by comparing values is flat near the answer, so when the point is off the curve the parameter cannot be resolved as finely as the distance itself. The tangent is a central difference with step 1e-6 of the parameter range.
brep-design.js
lines 69 to 91
PartCraft 0.8.1
const edgeNear = (ctx, edge) => (p) => {
const { oc, T, H } = ctx;
const ca = circleAxis(ctx, edge);
if (ca) {
// a circle: exactly, from its centre and axis
const d = V.norm(ca.d), w = V.sub(p, ca.o), radial = V.sub(w, V.mul(d, V.dot(w, d)));
if (V.len(radial) < 1e-12) fail(t('model.brep_design.point_on_axis'));
const u = V.norm(radial);
return { p: V.add(ca.o, V.mul(u, ca.r)), t: V.cross(d, u) };
}
const c = T(new oc.BRepAdaptor_Curve(T(oc.TopoDS.Edge(edge))));
const t0 = c.FirstParameter(), t1 = c.LastParameter();
const at = (t) => H.xyz(T(c.EvalD0(t)));
const d2 = (t) => { const q = at(t); return (q[0] - p[0]) ** 2 + (q[1] - p[1]) ** 2 + (q[2] - p[2]) ** 2; };
let bt = t0, bd = Infinity;
for (let i = 0; i <= 200; i++) { const t = t0 + (t1 - t0) * i / 200; const d = d2(t); if (d < bd) { bd = d; bt = t; } }
let lo = Math.max(t0, bt - (t1 - t0) / 200), hi = Math.min(t1, bt + (t1 - t0) / 200);
const g = (Math.sqrt(5) - 1) / 2;
for (let i = 0; i < 80; i++) { const a = hi - g * (hi - lo), b = lo + g * (hi - lo); if (d2(a) < d2(b)) hi = b; else lo = a; }
const tm = (lo + hi) / 2, h = (t1 - t0) * 1e-6;
const tan = V.norm(V.sub(at(Math.min(t1, tm + h)), at(Math.max(t0, tm - h))));
return { p: at(tm), t: tan };
};
Midpoint of an edge by length
64-bit
The point half way along an edge measured along the curve.
length = Σ over 32 pieces of Σ₈ wᵢ |C′(tᵢ)| × h; bisect for length(t) = total ÷ 2
The Gauss–Legendre nodes and weights are written with 16 digits. The 32 piece lengths are added in order, then 100 bisection steps re-integrate the partial piece with one 8-point rule each time. The quadrature error depends on the curve and is not estimated; for a straight or circular edge the speed is constant and the rule is exact up to rounding.
brep-design.js
lines 93 to 110
PartCraft 0.8.1
const GL8 = [[-0.9602898564975363, 0.1012285362903763], [-0.7966664774136267, 0.2223810344533745], [-0.525532409916329, 0.3137066458778873], [-0.1834346424956498, 0.362683783378362],
[0.1834346424956498, 0.362683783378362], [0.525532409916329, 0.3137066458778873], [0.7966664774136267, 0.2223810344533745], [0.9602898564975363, 0.1012285362903763]];
const edgeMid = (ctx, edge) => () => {
const { oc, T, H } = ctx;
const c = T(new oc.BRepAdaptor_Curve(T(oc.TopoDS.Edge(edge))));
const d1 = (t) => { const r = c.EvalD1(t); const out = { p: H.xyz(r.Point), v: [r.D1.X(), r.D1.Y(), r.D1.Z()] }; r.Point.delete(); r.D1.delete(); return out; };
const t0 = c.FirstParameter(), t1 = c.LastParameter();
const len = (a, b) => { let s = 0; const h = (b - a) / 2, m = (a + b) / 2; for (const [x, w] of GL8) s += w * V.len(d1(m + h * x).v); return s * h; };
const cum = [0], ts = [];
for (let k = 0; k <= 32; k++) ts.push(t0 + (t1 - t0) * k / 32);
for (let k = 0; k < 32; k++) cum.push(cum[k] + len(ts[k], ts[k + 1]));
const half = cum[32] / 2;
const k = Math.max(0, cum.findIndex((x) => x >= half) - 1);
let lo = ts[k], hi = ts[k + 1];
for (let i = 0; i < 100; i++) { const m = (lo + hi) / 2; if (cum[k] + len(ts[k], m) < half) lo = m; else hi = m; }
const r = d1((lo + hi) / 2);
return { p: r.p, t: r.v };
};
Where an edge crosses a plane
64-bit
The points at which an edge passes through a plane.
g(t) = (C(t) − o) · n; bisect each sign change of g between 400 samples
Bisection runs until the bracket is at most 1e-15 × max(1, |t|) wide or 200 steps. A sample with |g| below 1e-12 is taken as a crossing; that test is absolute, and the plane normal is used as given, without normalizing it here.
brep-design.js
lines 112 to 132
PartCraft 0.8.1
const edgeCross = (ctx, edge) => (pl) => {
const { oc, T, H } = ctx;
const c = T(new oc.BRepAdaptor_Curve(T(oc.TopoDS.Edge(edge))));
const t0 = c.FirstParameter(), t1 = c.LastParameter();
const at = (t) => H.xyz(T(c.EvalD0(t)));
const g = (t) => V.dot(V.sub(at(t), pl.o), pl.n);
const out = [];
let ta = t0, ga = g(t0);
if (Math.abs(ga) < 1e-12) out.push(at(t0));
for (let k = 1; k <= 400; k++) {
const tb = t0 + (t1 - t0) * k / 400, gb = g(tb);
if (Math.abs(gb) < 1e-12) out.push(at(tb));
else if (ga * gb < 0 && Math.abs(ga) >= 1e-12) {
let lo = ta, hi = tb, glo = ga;
for (let i = 0; i < 200 && hi - lo > 1e-15 * Math.max(1, Math.abs(hi)); i++) { const m = (lo + hi) / 2, gm = g(m); if (gm * glo <= 0) hi = m; else { lo = m; glo = gm; } }
out.push(at((lo + hi) / 2));
}
ta = tb; ga = gb;
}
return out;
};
Arc of a sweep path
64-bit
Turns a sketch arc into the three points OpenCascade builds the path arc from.
r = |s − c|; mid = c + r × (cos, sin)((a₀ + a₁) ÷ 2), a₁ raised by 2π when a₁ ≤ a₀
The radius is taken from the start point only, and the end angle from atan2, so the arc passed on is the circle through start, computed midpoint and end. If the sketch's end point is not exactly at radius r from the center, the built arc's center and radius differ slightly from the sketch's.
brep-design.js
lines 245 to 251
PartCraft 0.8.1
} else if (e.type === 'arc') {
const c = pointXY(sk, e.c), s = pointXY(sk, e.s), en = pointXY(sk, e.e);
const r = Math.hypot(s[0] - c[0], s[1] - c[1]);
let a0 = Math.atan2(s[1] - c[1], s[0] - c[0]), a1 = Math.atan2(en[1] - c[1], en[0] - c[0]);
if (a1 <= a0) a1 += 2 * Math.PI;
const m = [c[0] + r * Math.cos((a0 + a1) / 2), c[1] + r * Math.sin((a0 + a1) / 2)];
edge = T(new oc.BRepBuilderAPI_MakeEdge(T(T(new oc.GC_MakeArcOfCircle(H.pnt(W(s)), H.pnt(W(m)), H.pnt(W(en)))).Value()))).Edge();
Chaining path pieces
64-bit
Decides which curve ends meet when pieces are joined into one path.
same when |p − q| ≤ 1e-6 × max(1, largest |end point|)
The tolerance scales with the distance of the path's ends from the origin, not with the path's own size, so a small path far from the origin gets a looser test.
brep-design.js
lines 261 to 266
PartCraft 0.8.1
// chain: start at a free end when there is one
const ends = edges.map((e) => curveEnds(ctx, e));
const scale = Math.max(1, ...ends.flat().map((p) => V.len(p)));
const tol = 1e-6 * scale;
const same = (p, q) => V.len(V.sub(p, q)) <= tol;
const degree = (p) => ends.reduce((k, [a, b]) => k + (same(a, p) ? 1 : 0) + (same(b, p) ? 1 : 0), 0);
Sweep profile at the path's end
64-bit
Checks that the profile's plane passes through one end of the path, and which end.
|(p − o) · n| ≤ 1e-6 × max(1, |path chord|)
A point-to-plane distance in 64-bit against a tolerance scaled to the straight distance between the path's ends (at least 1).
brep-design.js
lines 318 to 325
PartCraft 0.8.1
// the profile has to sit at one end of the path
const scale = Math.max(1, V.len(V.sub(path.ends[1], path.ends[0])));
const off = (p) => Math.abs(V.dot(V.sub(p, frame.o), frame.n));
let spine = path.wire;
if (off(path.ends[0]) > 1e-6 * scale) {
if (off(path.ends[1]) > 1e-6 * scale) fail(t('model.brep_design.sweep_profile_at_end'));
spine = T(oc.TopoDS.Wire(T(path.wire.Reversed())));
}
Extrude up to a face
64-bit
How far along the extrude direction each profile point is from the target plane.
t = ((o − x) · n) ÷ (u · n); parallel target: depth = min t; otherwise depth = 1.02 × max t + 1, then cut back by the plane
The division is guarded: |u · n| below 1e-9 is refused. When all points give the same t within 1e-9 × scale the depth is that value; otherwise the prism is made 2 % plus 1 mm too long on purpose and trimmed by a half-space at the plane, so the final face lies on the plane itself and the overshoot does not reach the result.
brep-design.js
lines 450 to 460
PartCraft 0.8.1
const toPlane = (o, n, which) => {
const un = V.dot(u, n);
if (Math.abs(un) < 1e-9) fail(t(which === 'next' ? 'model.brep_design.next_parallel' : 'model.brep_design.upto_parallel'));
const ts = polyPts.map((x) => V.dot(V.sub(o, x), n) / un);
const lo = Math.min(...ts), hi = Math.max(...ts);
const sc = Math.max(1, ...polyPts.map((x) => V.len(V.sub(x, frame.o))));
if (lo <= 1e-9 * sc) fail(t(which === 'next' ? 'model.brep_design.next_behind' : 'model.brep_design.upto_behind'));
if (hi - lo <= 1e-9 * sc) { b = lo; return; } // parallel to the sketch: an exact depth
b = hi * 1.02 + 1;
clip = { o, n };
};
Through, offset and vertex extents
64-bit
The extrude depth for through-all cuts, an offset from a face, and up to a vertex.
through: far + 1 + 0.01 × body range; offset: o = face − n × side × offset; vertex: b = (p − o) · u
A through cut is lengthened by 1 mm plus 1 % of the body's extent along the direction, so it clears the far side; the excess is outside the body. The vertex depth is a single dot product and is refused when at most 1e-9 (absolute).
brep-design.js
lines 463 to 480
PartCraft 0.8.1
else if (f.end === 'through') {
if (f.op !== 'cut') fail(t('model.brep.through_all_cuts'));
const r = rg(); if (!r) fail(t('model.brep.nothing_to_cut'));
const far = s > 0 ? r[1] : -r[0];
if (far <= 1e-9) fail(t('model.brep.nothing_to_cut_side'));
b = far + 1 + 0.01 * (r[1] - r[0]);
} else if (f.end === 'upto' || f.end === 'offset') {
if (!f.upto) fail(t('model.brep_design.upto_pick'));
const tp = resolvePlane(f.upto, ctx);
let o = tp.o;
if (f.end === 'offset') { const side = Math.sign(V.dot(u, tp.n)) || 1; o = V.sub(tp.o, V.mul(tp.n, side * f.offset)); }
toPlane(o, tp.n, 'upto');
} else if (f.end === 'vertex') {
if (!f.vertex) fail(t('model.brep_design.vertex_pick'));
const p = pointOf(f.vertex, ctx);
b = V.dot(V.sub(p, frame.o), u);
if (b <= 1e-9) fail(t('model.brep_design.vertex_behind'));
} else if (f.end === 'next') {
Picking the next face
64-bit
Chooses the first face of the body that the extrude meets.
nearest face by its lowest vertex distance along u, ties (within 1e-7 × scale) by centroid distance
Distances come from the face's vertices only, so a curved face's nearest point between vertices is not considered; a face that is not a plane is refused afterwards. Tolerances are 1e-7 × max(1, far) for position and 1e-9 for parallel directions.
brep-design.js
lines 487 to 501
PartCraft 0.8.1
const sc = Math.max(1, far);
for (const fc of H.unique(ov.shape, 'FACE')) {
const ts = H.explore(fc, 'VERTEX').map((v) => V.dot(V.sub(H.xyz(T(oc.BRep_Tool.Pnt(T(oc.TopoDS.Vertex(v))))), frame.o), u));
if (!ts.length) continue;
const lo = Math.min(...ts), hi = Math.max(...ts);
if (hi <= 1e-7 * sc || lo >= far - 1e-7 * sc) continue; // the sketch plane and the far cap
// the prism's own side walls (planes or cylinders along the direction) are not faces it meets
const ad = T(new oc.BRepAdaptor_Surface(T(oc.TopoDS.Face(fc)), true));
const ty = ad.GetType();
if (ty === 'GeomAbs_Plane' && Math.abs(V.dot(H.xyz(T(T(T(ad.Plane()).Axis()).Direction())), u)) < 1e-9) continue;
if (ty === 'GeomAbs_Cylinder' && Math.abs(V.dot(H.xyz(T(T(T(ad.Cylinder()).Axis()).Direction())), u)) > 1 - 1e-9) continue;
const gp = T(new oc.GProp_GProps());
oc.BRepGProp.SurfaceProperties(fc, gp, false, false);
const mid = V.dot(V.sub(H.xyz(T(gp.CentreOfMass())), frame.o), u);
if (!best || lo < best.lo - 1e-7 * sc || (Math.abs(lo - best.lo) <= 1e-7 * sc && mid < best.mid)) best = { lo, mid, fc };
In detail
Method
Nearest point: 201 samples, then 80 golden-section steps on the squared distance. Midpoint by length: 8-point Gauss–Legendre on 32 pieces, then 100 bisection steps. Plane crossings: sign changes over 400 samples, then bisection. Extent to a plane: t = ((o − x) · n) ÷ (u · n) for every profile point.
Number types
64-bit numbers; curve points and derivatives come from OpenCascade as 64-bit values.
Tolerances
Path ends meet within 1e-6 × max(1, largest end-point distance from the origin). Sweep profile on the path end within 1e-6 × max(1, path chord). Direction parallel to a face when |u · n| < 1e-9. Face behind the sketch when t ≤ 1e-9 × scale. Crossing sample counted as on the plane when |distance| < 1e-12 (absolute). Bisection stops at 1e-15 × max(1, |t|). The loft is built with a precision argument of 1e-6.
Limits
Only the bracket around the best of 201 samples is refined, so a nearer point elsewhere on a wavy edge can be missed. Two plane crossings between neighboring samples, or a touch without a sign change, are not found.
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.