Works out the bent profile of a sheet metal base, the bend allowance of every bend, and the flat pattern with its bend lines, size and area.
How the precision is kept
Bend geometry and the developed length are closed-form 64-bit arithmetic; the bend allowance is θ × (r + k × t) with the K-factor the person gives, so the flat length is as good as that K-factor. The flat pattern of a plate with flanges is built by the OpenCascade kernel from 64-bit numbers handed to it. Its box is taken from segment end points and 65 sampled points per arc or circle, and curves that are neither lines nor circles are sampled at 65 points.
Open item. Take the flat pattern's box from each arc's exact extreme points instead of 65 samples.
Calculations under this policy
13 calculations in 2 source files. 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.
Orders the lines of an open profile end to end, or reports that they are not one chain.
two ends are the same point when their distance ≤ 1e-6 × max(1, largest coordinate size)
The matching tolerance is scaled to the largest coordinate of the profile with a floor of 1e-6. The points themselves are passed through unchanged; nothing is averaged or snapped.
sheetmetal.js
lines 85 to 104
PartCraft 0.8.1
/** One open chain through segments [[a, b], …] (2-D, any order and direction) → its points end to end, or null when they
* are not one chain (a branch, a gap, a loop, two pieces). Shared with the rib (brep-rib.js). */
export function lineChain(segs) {
if (!segs.length) return null;
const tol = 1e-6 * Math.max(1, ...segs.flat().map((p) => Math.hypot(p[0], p[1])));
const same = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]) <= tol;
const deg = (p) => segs.reduce((k, s) => k + (same(s[0], p) ? 1 : 0) + (same(s[1], p) ? 1 : 0), 0);
const start = segs.flat().find((p) => deg(p) === 1);
if (!start) return null;
const used = new Set(), pts = [start];
let cur = start;
while (used.size < segs.length) {
const k = segs.findIndex((s, i) => !used.has(i) && (same(s[0], cur) || same(s[1], cur)));
if (k < 0) return null;
used.add(k);
cur = same(segs[k][0], cur) ? segs[k][1] : segs[k][0];
pts.push(cur);
}
return pts.every((p, i) => i === 0 || deg(p) <= 2) ? pts : null;
}
Bend angle, center and tangent length
64-bit
For every corner of the profile: the signed bend angle, the radius on the path side and the offset side, the distance the bend takes from each leg, and the bend's center.
θ = atan2(u × w, u · w); d = r_path × tan(|θ| ÷ 2); c = P + left(u) × sign(θ) × r_path − u × d
The angle comes from atan2 of the cross and dot products of unit directions, which keeps its digits at small and at near-straight angles alike. The tangent of half the angle grows without bound as the angle nears π; corners within 1e-6 rad of a fold-back are refused before it is used.
sheetmetal.js
lines 127 to 138
PartCraft 0.8.1
const bends = [], trim = new Array(n).fill(0);
for (let i = 1; i + 1 < n; i++) {
const u = dirs[i - 1], w = dirs[i], theta = Math.atan2(cross(u, w), dot(u, w)); // signed turn, CCW positive
if (Math.abs(theta) < 1e-9) throw new Error(tr('sheet.err.straight_on'));
if (Math.abs(Math.abs(theta) - Math.PI) < 1e-6) throw new Error(tr('sheet.err.fold_back'));
const toward = side * Math.sign(theta) > 0; // the thickness grows toward the bend's centre: the path is its outer face
const rPath = toward ? r + t : r, rOff = toward ? r : r + t;
const d = rPath * Math.tan(Math.abs(theta) / 2);
const c = add(pts[i], add(mul(left(u), Math.sign(theta) * rPath), mul(u, -d)));
bends.push({ i, theta, rIn: r, rOut: r + t, rPath, rOff, d, c });
trim[i] = d;
}
Arc of a bend in the profile
64-bit
The arc of a bend between its two tangent points, with its middle point and 17 points along it.
sweep = a₁ − a₀ brought to (0, 2π] or [−2π, 0); point = c + r × (cos t, sin t)
Start and end angles come from atan2 on the end points; the sweep is brought into range by adding or subtracting 2π. The exact end points a and b are kept on the segment next to the sampled points.
sheetmetal.js
lines 111 to 120
PartCraft 0.8.1
/** An arc segment in profileFaces' form, from a to b about c (radius r), the short way unless sweep says otherwise. */
function arcSeg(id, c, r, a, b, ccw) {
const a0 = Math.atan2(a[1] - c[1], a[0] - c[0]);
let a1 = Math.atan2(b[1] - c[1], b[0] - c[0]), sw = a1 - a0;
if (ccw) { while (sw <= 0) sw += 2 * Math.PI; } else { while (sw >= 0) sw -= 2 * Math.PI; }
const mid = [c[0] + r * Math.cos(a0 + sw / 2), c[1] + r * Math.sin(a0 + sw / 2)];
const pts = [];
for (let i = 0; i <= 16; i++) { const t = a0 + sw * i / 16; pts.push([c[0] + r * Math.cos(t), c[1] + r * Math.sin(t)]); }
return { id, type: 'arc', a, b, c, r, mid, pts };
}
Straight runs between bends
64-bit
The flat length of each leg: its drawn length less what the bends at its ends take.
straightⱼ = |Pⱼ₊₁ − Pⱼ| − dⱼ − dⱼ₊₁
A 64-bit subtraction of the tangent lengths from the leg length. A leg that is only just long enough for its bends gives a small difference of larger numbers; the earlier check lets the bends exceed the leg by up to 1e-9, so a run can be very slightly negative.
The total flat length of a bent profile and where each bend line lies on the flat.
length = Σ straights + Σ BA; bend line at x + BA ÷ 2; angle° = θ × 180 ÷ π
The length is a running 64-bit sum of the straight runs and the allowances in order along the profile. The angle is converted to degrees with Math.PI.
sheetmetal.js
lines 175 to 188
PartCraft 0.8.1
/** The flat length of a bent profile and its bend centre-lines (distances from the start of the flat). */
export function flatOfProfile(bp, t, k) {
let x = 0;
const lines = [];
bp.straights.forEach((s, j) => {
x += s;
const b = bp.bends[j];
if (!b) return;
const ba = bendAllowance(Math.abs(b.theta), b.rIn, t, k);
lines.push({ at: x + ba / 2, from: x, to: x + ba, angle: b.theta * 180 / Math.PI, r: b.rIn, ba });
x += ba;
});
return { length: x, lines };
}
Joins two straight segments of the flat outline that continue each other through a point no third segment touches.
collinear when |d₁ × d₂| ≤ 1e-9 × |d₁| × |d₂| and d₁ · d₂ > 0
End points are matched by rounding their coordinates to a grid of the tolerance, so two points closer than the tolerance can still fall in different cells and not match. The joined line keeps the two outer end points unchanged; the collinearity test is relative to the two lengths.
sheet-flat.js
lines 28 to 53
PartCraft 0.8.1
/** Join straight segments that continue each other through a point no third segment touches (the seams between the plate,
* the bend strips and the legs that face merging leaves behind). */
export function mergeLines(segs, tol = 1e-6) {
const out = segs.slice();
const key = (p) => Math.round(p[0] / tol) + ',' + Math.round(p[1] / tol);
for (let changed = true; changed;) {
changed = false;
const at = new Map();
out.forEach((s, i) => { const ends = s.type === 'line' ? [s.a, s.b] : s.type === 'arc' ? arcPoints(s, 1) : s.type === 'poly' ? [s.pts[0], s.pts.at(-1)] : []; for (const p of ends) { const k = key(p); if (!at.has(k)) at.set(k, []); at.get(k).push(i); } });
for (const [, ids] of at) {
if (ids.length !== 2) continue;
const [i, j] = ids, A = out[i], B = out[j];
if (A.type !== 'line' || B.type !== 'line' || i === j) continue;
const shared = key(A.a) === key(B.a) || key(A.a) === key(B.b) ? A.a : A.b;
const pa = key(A.a) === key(shared) ? A.b : A.a, pb = key(B.a) === key(shared) ? B.b : B.a;
const d1 = [shared[0] - pa[0], shared[1] - pa[1]], d2 = [pb[0] - shared[0], pb[1] - shared[1]];
const l1 = Math.hypot(...d1), l2 = Math.hypot(...d2);
if (Math.abs(d1[0] * d2[1] - d1[1] * d2[0]) > 1e-9 * l1 * l2 || d1[0] * d2[0] + d1[1] * d2[1] <= 0) continue;
out[i] = { type: 'line', a: pa, b: pb };
out.splice(j, 1);
changed = true;
break;
}
}
return out;
}
Points along an arc for the extent
64-bit
Sampled points on an arc or circle of the flat, used to find its box.
point = c + r × (cos a, sin a), a = (a₀ + sweep × k ÷ n) × π ÷ 180
Angles are stored in degrees and converted with Math.PI. With the default 64 steps the extreme of an arc lies between samples unless it falls on one, so the box is a sampled value.
sheet-flat.js
lines 55 to 61
PartCraft 0.8.1
/** Points along an arc or circle of a flat (for its extent). */
export function arcPoints(s, n = 64) {
const a0 = s.type === 'circle' ? 0 : s.a0, sw = s.type === 'circle' ? 360 : ((s.a1 - s.a0) % 360 + 360) % 360 || 360;
const out = [];
for (let k = 0; k <= n; k++) { const a = (a0 + sw * k / n) * Math.PI / 180; out.push([s.c[0] + s.r * Math.cos(a), s.c[1] + s.r * Math.sin(a)]); }
return out;
}
Flat of a bent base
64-bit
The developed rectangle of an open bent profile: flat length by depth, with its bend lines and area.
box = L × D; area = L × D
The rectangle's corners are the developed length and the depth themselves, and the area is their one 64-bit product. Bend line positions are copied from the profile's flat.
sheet-flat.js
lines 77 to 84
PartCraft 0.8.1
if (base.kind === 'bent') {
if (flanges.length) throw new Error(t('sheet.flat.bent_flanges'));
if (after.length) notes.push(t('sheet.flat.bent_cuts'));
const L = base.length, D = base.depth;
const segs = [[[0, 0], [L, 0]], [[L, 0], [L, D]], [[L, D], [0, D]], [[0, D], [0, 0]]].map(([a, b]) => ({ type: 'line', a, b }));
const bends = base.bends.map((b, i) => ({ a: [b.at, 0], b: [b.at, D], angle: b.angle, r: b.r, up: b.up, name: `${baseF.name} ${i + 1}` })); // i18n-ignore: a bend's name
return { kind: 'bent', segs, bends, box: { x0: 0, y0: 0, x1: L, y1: D }, area: L * D, notes };
}
Faces that lie on the plate's plane
64-bit
Decides which flat faces of the body belong to the plate, by distance from the sketch plane and by direction.
size = max(1, largest distance of a face centroid from the origin); tol = 1e-6 × size; on the plane when |n · (c − o)| ≤ tol and | |n_face · n| − 1 | ≤ 1e-9
The distance tolerance scales with the part, with a floor of 1e-6. The direction test compares a dot product with 1 to within 1e-9, which accepts normals up to about 4.5e-5 rad apart.
Turns a flange's leg back into the plate's plane and adds the bend strip between the plate and the leg.
leg: rotate by −θ about the bend axis through C, then move by BA along u; strip: S₀, S₀ + e·run, + u·BA; bend line at S₀ + u × BA ÷ 2
The rotation and translation are OpenCascade transforms built from 64-bit numbers; the strip's four corners and the bend line are plain 64-bit vector sums. The flange's own values (S₀, e, u, run, BA, C, θ) are computed in brep-sheet.js.
sheet-flat.js
lines 139 to 151
PartCraft 0.8.1
// turn the leg back about the bend axis (through C along e) by θ, then out by the bend allowance
const axis = V.norm(V.cross(u, i.v));
const rot = T(new oc.gp_Trsf()); rot.SetRotation(T(new oc.gp_Ax1(pnt(C), dir(axis))), -theta);
const mv = T(new oc.gp_Trsf()); mv.SetTranslation(T(new oc.gp_Vec(...V.mul(u, ba))));
const both = T(mv.Multiplied(rot));
for (const lf of legs) pieces.push(facing(toPlane(T(T(new oc.BRepBuilderAPI_Transform(lf.face, both, true)).Shape()), S0)));
// the bend strip: from the plate's edge out by the allowance, along the run
const q = [S0, V.add(S0, V.mul(e, run)), V.add(V.add(S0, V.mul(e, run)), V.mul(u, ba)), V.add(S0, V.mul(u, ba))];
const mw = T(new oc.BRepBuilderAPI_MakeWire());
for (let k = 0; k < 4; k++) mw.Add(T(T(new oc.BRepBuilderAPI_MakeEdge(pnt(q[k]), pnt(q[(k + 1) % 4]))).Edge()));
pieces.push(facing(toPlane(T(T(new oc.BRepBuilderAPI_MakeFace(T(mw.Wire()), true)).Face()), S0)));
const m0 = V.add(S0, V.mul(u, ba / 2)), m1 = V.add(m0, V.mul(e, run));
bends.push({ a: to2(m0), b: to2(m1), angle: f.angle, r: i.r, up: V.dot(i.v, n) > 0, name: f.name });
Outline edges as lines, arcs and circles
64-bit
Reads each edge of the fused flat as a 2-D line, arc, circle or sampled curve in the sketch plane.
Lines keep their two end points and circles their center and radius from the kernel. Arc angles are stored in degrees, derived from the end points with atan2. An edge that is neither a line nor a circle becomes 65 sampled points, so its shape between samples is not kept.
sheet-flat.js
lines 177 to 194
PartCraft 0.8.1
const P = (s) => to2(xyz(T(c.EvalD0(s))));
const type = c.GetType();
if (type === 'GeomAbs_Line') segs.push({ type: 'line', a: P(t0), b: P(t1) });
else if (type === 'GeomAbs_Circle') {
const ci = T(c.Circle()), cc = to2(xyz(T(ci.Location()))), rr = ci.Radius();
if (Math.abs(t1 - t0 - 2 * Math.PI) < 1e-9) segs.push({ type: 'circle', c: cc, r: rr });
else {
// CCW in the plane from a0 to a1: the axis along n keeps the parameter's sense, against n reverses it
const ccw = V.dot(xyz(T(T(ci.Axis()).Direction())), n) > 0;
const ang = (p) => Math.atan2(p[1] - cc[1], p[0] - cc[0]) * 180 / Math.PI;
const [s0, s1] = ccw ? [P(t0), P(t1)] : [P(t1), P(t0)];
segs.push({ type: 'arc', c: cc, r: rr, a0: (ang(s0) + 360) % 360, a1: (ang(s1) + 360) % 360 });
}
} else {
const pts = [];
for (let k = 0; k <= 64; k++) pts.push(P(t0 + (t1 - t0) * k / 64));
segs.push({ type: 'poly', pts });
}
Box and area of the flat pattern
64-bit
The overall size of the flat pattern and its area.
box = min and max over line ends, sampled curve points and 65 points per arc; area = surface area of the fused flat
The area is read from OpenCascade's surface properties of the fused face, not from the 2-D segments. The box of an arc or circle comes from 65 sampled points, so it can fall short of the true extreme.
sheet-flat.js
lines 196 to 205
PartCraft 0.8.1
const merged = mergeLines(segs, 1e-7 * size);
const gp = T(new oc.GProp_GProps());
oc.BRepGProp.SurfaceProperties(flat, gp, false, false);
// the box from the segments themselves
const xs = [], ys = [];
for (const s of merged) {
const ps = s.type === 'line' ? [s.a, s.b] : s.type === 'poly' ? s.pts : arcPoints(s);
for (const p of ps) { xs.push(p[0]); ys.push(p[1]); }
}
return { kind: 'plate', segs: merged, bends, box: { x0: Math.min(...xs), y0: Math.min(...ys), x1: Math.max(...xs), y1: Math.max(...ys) }, area: gp.Mass(), notes };
In detail
Method
Bend allowance θ·(r + k·t) (neutral-layer length); tangent length r·tan(θ ÷ 2) at each corner; flange legs rotated back about the bend axis and moved out by the allowance, then fused and merged in the plane (OpenCascade BRepAlgoAPI_Fuse and ShapeUpgrade_UnifySameDomain); area from OpenCascade surface properties.
Number types
64-bit numbers in plain arrays; OpenCascade works in double precision.
Tolerances
Zero-length line 1e-9; straight-on corner |θ| < 1e-9 rad; fold-back within 1e-6 rad of π; leg too short when the bends need more than its length + 1e-9. Chain end points match within 1e-6 × max(1, largest coordinate). Flat pattern: faces on a plane within 1e-6 × size, normals parallel within 1e-9, end-point matching on a grid of 1e-7 × size, collinear within 1e-9 × the two lengths, full circle when the parameter range is within 1e-9 of 2π.
Limits
The K-factor is an empirical input. The box of a flat with arcs is sampled, so an arc's extreme can be missed by up to r × (1 − cos(sweep ÷ 128)). Cuts made after an open bent base are not unfolded (the result carries a note).
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.