@faicad/cq-compat 0.16.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/gear-test-harness.d.ts +7 -4
- package/dist/gear-test-harness.d.ts.map +1 -1
- package/dist/gear-test-harness.js +10 -6
- package/dist/gear-test-harness.js.map +1 -1
- package/dist/gears.d.ts +8 -3
- package/dist/gears.d.ts.map +1 -1
- package/dist/gears.js.map +1 -1
- package/dist/workplane.d.ts.map +1 -1
- package/dist/workplane.js +125 -105
- package/dist/workplane.js.map +1 -1
- package/package.json +2 -2
package/dist/workplane.js
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
* Design doc: docs/plans/2026-09-06-cadquery-compat-and-multifile-faijs.md
|
|
10
10
|
*/
|
|
11
11
|
import { createApiNamespace } from '@faicad/faijs/api/api-namespace';
|
|
12
|
-
import { brepjsCompat } from '@faicad/faijs/api';
|
|
13
|
-
import { borrowBrepjsShape, adoptBrepjsProduct } from '@faicad/faijs/api/internal/l3-bridge';
|
|
14
12
|
import { fromHandle } from '@faicad/faijs/sdk';
|
|
15
13
|
import { brepOf, isShape } from '@faicad/faijs/shape';
|
|
16
14
|
import { setName, nameOf } from '@faicad/faijs';
|
|
17
15
|
import { getKernel } from '@faicad/faijs/occt-kernel/occtKernel';
|
|
16
|
+
import { getBrepApi } from '@faicad/faijs/brep/handle-bridge';
|
|
17
|
+
import { applyMatrixBrep } from '@faicad/faijs/api/brep-mirror/topologyFns';
|
|
18
18
|
// ── cad namespace singleton (created once at module load) ──────────────────
|
|
19
19
|
const cad = createApiNamespace();
|
|
20
20
|
// ── compatOp 提升边界归一(GOTCHA:borrowDeep 把实参 Shape 换成借用视图)──
|
|
@@ -33,9 +33,9 @@ const cad = createApiNamespace();
|
|
|
33
33
|
//
|
|
34
34
|
// 所有权:归一出的新 Shape 与原 part Shape 的 slot 指向同一 OCCT 句柄,但 slot
|
|
35
35
|
// 按 Shape 对象各自持有(fromBrep 写的是新 Shape 的 slot),无共享释放路径——
|
|
36
|
-
// 与
|
|
36
|
+
// 与 core fromHandle 收编模式同构,不引入双重释放。
|
|
37
37
|
// 视图 `.wrapped` 是 OcctWasmHandle 对象({ id, type, __occtWasm }),内核只收
|
|
38
|
-
// 数字 id → 解包方式与
|
|
38
|
+
// 数字 id → 解包方式与 core fromHandle 一致。
|
|
39
39
|
const borrowedShapeCache = new WeakMap();
|
|
40
40
|
/**
|
|
41
41
|
* 把可能是借用视图的几何输入归一为真实 faijs Shape(见上方注释)。
|
|
@@ -215,17 +215,37 @@ function unwrapBrepResult(result) {
|
|
|
215
215
|
}
|
|
216
216
|
return result;
|
|
217
217
|
}
|
|
218
|
-
/**
|
|
219
|
-
function
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return
|
|
218
|
+
/** core 直连 helpers(cq-compat 改写:vendored 兼容面 → getBrepApi/getKernel,裁决 3)。 */
|
|
219
|
+
function kern() {
|
|
220
|
+
return getBrepApi();
|
|
221
|
+
}
|
|
222
|
+
function ownHandle(shape) {
|
|
223
|
+
return brepOf(shape);
|
|
224
|
+
}
|
|
225
|
+
function toShape(h) {
|
|
226
|
+
return fromHandle(h);
|
|
227
|
+
}
|
|
228
|
+
function vec3(p) {
|
|
229
|
+
if (Array.isArray(p))
|
|
230
|
+
return { x: p[0], y: p[1], z: p[2] };
|
|
231
|
+
return p;
|
|
232
|
+
}
|
|
233
|
+
function polygonShape(pts) {
|
|
234
|
+
const k = getKernel();
|
|
235
|
+
const edges = pts.map((p, i) => k.makeLineEdge(vec3(p), vec3(pts[(i + 1) % pts.length])));
|
|
236
|
+
const w = k.makeWire(edges);
|
|
237
|
+
const f = k.makeFace(w);
|
|
238
|
+
return fromHandle(f);
|
|
239
|
+
}
|
|
240
|
+
function faceWithHoles(outer, holes) {
|
|
241
|
+
const f = toShape(kern().makeFace(outer));
|
|
242
|
+
if (holes.length > 0)
|
|
243
|
+
kern().addHolesInFace(brepOf(f), holes);
|
|
244
|
+
return f;
|
|
224
245
|
}
|
|
225
246
|
/** Merge same-domain faces/edges after a boolean (CadQuery `clean=True`). */
|
|
226
247
|
async function cleanShapes(shape) {
|
|
227
|
-
|
|
228
|
-
return adoptBrepjsProduct(simplified);
|
|
248
|
+
return toShape(getKernel().simplify(ownHandle(shape)));
|
|
229
249
|
}
|
|
230
250
|
/**
|
|
231
251
|
* Fuse two shapes into ONE solid via the vendored brepjs fuse.
|
|
@@ -237,8 +257,7 @@ async function cleanShapes(shape) {
|
|
|
237
257
|
* stays a single solid. See docs/analysis/2026-09-08-cq-compat-union-compound-bug.md.
|
|
238
258
|
*/
|
|
239
259
|
async function fuseShapes(a, b, clean = true) {
|
|
240
|
-
const
|
|
241
|
-
const fused = adoptBrepjsProduct(product);
|
|
260
|
+
const fused = toShape(kern().fuse(ownHandle(a), ownHandle(b)));
|
|
242
261
|
// CadQuery ops take a `clean` flag (default True); clean=False preserves the
|
|
243
262
|
// boolean splitter faces (verified vs 2.8.0: testNoClean wedge vol 10.650718
|
|
244
263
|
// vs testClean 9.079922 — the kernel unify pass is NOT volume-preserving).
|
|
@@ -246,13 +265,13 @@ async function fuseShapes(a, b, clean = true) {
|
|
|
246
265
|
}
|
|
247
266
|
/** Cut a tool shape out of a base shape via the vendored brepjs cut. */
|
|
248
267
|
async function cutShapes(base, tool) {
|
|
249
|
-
const
|
|
250
|
-
return cleanShapes(
|
|
268
|
+
const cutShape = toShape(kern().cut(ownHandle(base), ownHandle(tool)));
|
|
269
|
+
return cleanShapes(cutShape);
|
|
251
270
|
}
|
|
252
271
|
/** Intersect two shapes via the vendored brepjs intersect. */
|
|
253
272
|
async function intersectShapes(a, b) {
|
|
254
|
-
const
|
|
255
|
-
return cleanShapes(
|
|
273
|
+
const isect = toShape(kern().intersect(ownHandle(a), ownHandle(b)));
|
|
274
|
+
return cleanShapes(isect);
|
|
256
275
|
}
|
|
257
276
|
/** Get bbox max of a shape (via cad.bboxMax — synchronous). */
|
|
258
277
|
function bboxMax(shape) {
|
|
@@ -556,11 +575,11 @@ export async function resolveFaceSelector(shape, sel, centerOption) {
|
|
|
556
575
|
// Indexed selector — enumerate real faces and sort by bbox center along axis.
|
|
557
576
|
// CadQuery semantics: '>A[k]' ascending, '<A[k]' descending (see doc above).
|
|
558
577
|
const idx = parseInt(idxStr, 10);
|
|
559
|
-
const faces =
|
|
578
|
+
const faces = kern().getSubShapes(ownHandle(shape), 'face');
|
|
560
579
|
const entries = faces.map((f) => {
|
|
561
|
-
const b =
|
|
580
|
+
const b = kern().getBoundingBox(f);
|
|
562
581
|
return {
|
|
563
|
-
c: [(b.
|
|
582
|
+
c: [(b.xmin + b.xmax) / 2, (b.ymin + b.ymax) / 2, (b.zmin + b.zmax) / 2],
|
|
564
583
|
bounds: b,
|
|
565
584
|
};
|
|
566
585
|
});
|
|
@@ -574,9 +593,9 @@ export async function resolveFaceSelector(shape, sel, centerOption) {
|
|
|
574
593
|
}
|
|
575
594
|
const fb = entries[pick].bounds;
|
|
576
595
|
const faceCenter = [
|
|
577
|
-
(fb.
|
|
578
|
-
(fb.
|
|
579
|
-
(fb.
|
|
596
|
+
(fb.xmin + fb.xmax) / 2,
|
|
597
|
+
(fb.ymin + fb.ymax) / 2,
|
|
598
|
+
(fb.zmin + fb.zmax) / 2,
|
|
580
599
|
];
|
|
581
600
|
// Outward normal: away from the shape bbox center along the axis.
|
|
582
601
|
normal[axis] = faceCenter[axis] >= center[axis] ? 1 : -1;
|
|
@@ -593,10 +612,10 @@ async function makePolygonPrismAt(wp, poly, length, dir) {
|
|
|
593
612
|
const a = (2 * Math.PI * i) / poly.n;
|
|
594
613
|
pts.push(localToWorld(wp, Math.cos(a) * (poly.d / 2), Math.sin(a) * (poly.d / 2)));
|
|
595
614
|
}
|
|
596
|
-
const face =
|
|
615
|
+
const face = polygonShape(pts);
|
|
597
616
|
const vec = [dir[0] * length, dir[1] * length, dir[2] * length];
|
|
598
|
-
const prism =
|
|
599
|
-
return
|
|
617
|
+
const prism = toShape(kern().extrude(brepOf(face), vec[0], vec[1], vec[2]));
|
|
618
|
+
return prism;
|
|
600
619
|
}
|
|
601
620
|
/**
|
|
602
621
|
* Rotate a Z-axis-aligned primitive so its local +Z maps to `d` — ANY direction,
|
|
@@ -670,8 +689,7 @@ function resolveCentered(c) {
|
|
|
670
689
|
function makeCompoundShape(shapes) {
|
|
671
690
|
if (shapes.length === 1)
|
|
672
691
|
return shapes[0];
|
|
673
|
-
|
|
674
|
-
return adoptBrepjsProduct(product);
|
|
692
|
+
return toShape(kern().makeCompound(shapes.map((s) => ownHandle(s))));
|
|
675
693
|
}
|
|
676
694
|
/**
|
|
677
695
|
* Finish an eachpoint-style op (box/sphere/cylinder): `combine=True` (CadQuery
|
|
@@ -822,9 +840,9 @@ export async function wedge(wp, dx, dy, dz, xmin, zmin, xmax, zmax, opts) {
|
|
|
822
840
|
const rectWire = (pts) => {
|
|
823
841
|
const edges = [];
|
|
824
842
|
for (let i = 0; i < 4; i++) {
|
|
825
|
-
edges.push(
|
|
843
|
+
edges.push(kern().makeLineEdge(vec3(pts[i]), v3(pts[(i + 1) % 4])));
|
|
826
844
|
}
|
|
827
|
-
return
|
|
845
|
+
return kern().makeWire(edges);
|
|
828
846
|
};
|
|
829
847
|
const c = resolveCentered(opts?.centered ?? true);
|
|
830
848
|
const ox = c[0] ? -dx / 2 : 0;
|
|
@@ -844,7 +862,7 @@ export async function wedge(wp, dx, dy, dz, xmin, zmin, xmax, zmax, opts) {
|
|
|
844
862
|
p3(ox + xmax, oy + dy, oz + zmax),
|
|
845
863
|
p3(ox + xmin, oy + dy, oz + zmax),
|
|
846
864
|
]);
|
|
847
|
-
const solid =
|
|
865
|
+
const solid = toShape(getKernel().loft([bottom, top], true, true));
|
|
848
866
|
const points = eachPoints(wp);
|
|
849
867
|
const shapes = [];
|
|
850
868
|
for (const [px, py] of points) {
|
|
@@ -941,8 +959,7 @@ export async function cylinder(wp, height, radius, opts) {
|
|
|
941
959
|
* @returns Promise<Workplane> carrying the torus solid.
|
|
942
960
|
*/
|
|
943
961
|
export async function torus(wp, d1, d2, opts) {
|
|
944
|
-
const
|
|
945
|
-
const shape = adoptBrepjsProduct(product);
|
|
962
|
+
const shape = toShape(kern().makeTorus(d1 / 2, d2 / 2));
|
|
946
963
|
return combineEachpoint(wp, [shape], opts?.combine ?? true);
|
|
947
964
|
}
|
|
948
965
|
/**
|
|
@@ -1511,7 +1528,7 @@ export function spline(wp, points, opts) {
|
|
|
1511
1528
|
return clone(wp, { currentPoint: end });
|
|
1512
1529
|
}
|
|
1513
1530
|
const world = all.map(([x, y]) => localToWorld(wp, x, y));
|
|
1514
|
-
const builtEdge =
|
|
1531
|
+
const builtEdge = toShape(getKernel().interpolatePoints(world.map((p) => vec3(p)), false));
|
|
1515
1532
|
const endTgt = splineEndTangent(builtEdge, wp);
|
|
1516
1533
|
const edges = [
|
|
1517
1534
|
...(wp.pendingEdges ?? []),
|
|
@@ -1532,7 +1549,9 @@ function splineEndTangent(edge, wp) {
|
|
|
1532
1549
|
// curveTangentAt returns a plain [x, y, z] ARRAY (vendored curveFns →
|
|
1533
1550
|
// curveOps.curveTangent(...).tangent), not an {x,y,z} vector — indexing it
|
|
1534
1551
|
// with .x yields undefined → NaN → a corrupt tangent-arc edge downstream.
|
|
1535
|
-
const
|
|
1552
|
+
const edgeH = brepOf(edge);
|
|
1553
|
+
const cp = kern().curveParameters(edgeH);
|
|
1554
|
+
const tRaw = kern().curveTangent(edgeH, cp.last);
|
|
1536
1555
|
const t = Array.isArray(tRaw)
|
|
1537
1556
|
? { x: tRaw[0], y: tRaw[1], z: tRaw[2] }
|
|
1538
1557
|
: tRaw;
|
|
@@ -1797,8 +1816,8 @@ async function buildProfileWire(wp, w) {
|
|
|
1797
1816
|
const n = Array.isArray(pl.normal) ? pl.normal : [0, 0, 1];
|
|
1798
1817
|
if (w.kind === 'circle') {
|
|
1799
1818
|
const center = localToWorld(pl, w.cx, w.cy);
|
|
1800
|
-
const edge =
|
|
1801
|
-
return
|
|
1819
|
+
const edge = kern().makeCircleEdge(vec3(center), vec3(n), w.radius);
|
|
1820
|
+
return kern().makeWire([edge]);
|
|
1802
1821
|
}
|
|
1803
1822
|
if (w.kind === 'ellipse') {
|
|
1804
1823
|
const center = localToWorld(pl, w.cx, w.cy);
|
|
@@ -1815,8 +1834,8 @@ async function buildProfileWire(wp, w) {
|
|
|
1815
1834
|
// Failing loudly instead of silently emitting an approximated ellipse.
|
|
1816
1835
|
throw new Error('[cq-compat] ellipse: y_radius > x_radius is not reproducible (kernel ellipse is always major-on-X and rotations re-approximate it)');
|
|
1817
1836
|
}
|
|
1818
|
-
const edge =
|
|
1819
|
-
return
|
|
1837
|
+
const edge = getKernel().makeEllipseEdge(vec3(center), vec3(n), w.majorRadius, w.minorRadius);
|
|
1838
|
+
return kern().makeWire([edge]);
|
|
1820
1839
|
}
|
|
1821
1840
|
if (w.kind === 'path') {
|
|
1822
1841
|
// Drafted ring. When edge descriptors are present (arcs/splines), rebuild
|
|
@@ -1832,10 +1851,10 @@ async function buildProfileWire(wp, w) {
|
|
|
1832
1851
|
const descs = reorderForWireAssembly(w.edges.filter((e) => e.kind !== 'line' || Math.hypot(e.to[0] - e.from[0], e.to[1] - e.from[1]) >= 1e-9));
|
|
1833
1852
|
for (const e of descs) {
|
|
1834
1853
|
if (e.kind === 'line') {
|
|
1835
|
-
edges.push(
|
|
1854
|
+
edges.push(kern().makeLineEdge(v3(localToWorld(pl, e.from[0], e.from[1])), v3(localToWorld(pl, e.to[0], e.to[1]))));
|
|
1836
1855
|
}
|
|
1837
1856
|
else if (e.kind === 'arc3') {
|
|
1838
|
-
edges.push(
|
|
1857
|
+
edges.push(getKernel().makeArcEdge(vec3(localToWorld(pl, e.from[0], e.from[1])), vec3(localToWorld(pl, e.mid[0], e.mid[1])), vec3(localToWorld(pl, e.to[0], e.to[1]))));
|
|
1839
1858
|
}
|
|
1840
1859
|
else if (e.kind === 'tangentArc') {
|
|
1841
1860
|
const tLen = Math.hypot(e.tgt[0], e.tgt[1]);
|
|
@@ -1844,16 +1863,16 @@ async function buildProfileWire(wp, w) {
|
|
|
1844
1863
|
(pl.xDir[1] * e.tgt[0] + pl.yDir[1] * e.tgt[1]) / tLen,
|
|
1845
1864
|
(pl.xDir[2] * e.tgt[0] + pl.yDir[2] * e.tgt[1]) / tLen,
|
|
1846
1865
|
];
|
|
1847
|
-
edges.push(
|
|
1866
|
+
edges.push(getKernel().makeTangentArc(vec3(localToWorld(pl, e.from[0], e.from[1])), vec3(t), vec3(localToWorld(pl, e.to[0], e.to[1]))));
|
|
1848
1867
|
}
|
|
1849
1868
|
else if (e.builtEdge) {
|
|
1850
1869
|
// Reuse the edge built at op time (strongly held — see spline()).
|
|
1851
|
-
edges.push(e.builtEdge);
|
|
1870
|
+
edges.push(brepOf(e.builtEdge));
|
|
1852
1871
|
}
|
|
1853
1872
|
else {
|
|
1854
1873
|
// spline
|
|
1855
1874
|
const world = e.pts.map(([x, y]) => localToWorld(pl, x, y));
|
|
1856
|
-
edges.push(
|
|
1875
|
+
edges.push(getKernel().interpolatePoints(world.map((p) => vec3(p)), false));
|
|
1857
1876
|
}
|
|
1858
1877
|
}
|
|
1859
1878
|
}
|
|
@@ -1863,12 +1882,12 @@ async function buildProfileWire(wp, w) {
|
|
|
1863
1882
|
const b = w.pts[(i + 1) % w.pts.length];
|
|
1864
1883
|
if (Math.hypot(a[0] - b[0], a[1] - b[1]) < 1e-9)
|
|
1865
1884
|
continue;
|
|
1866
|
-
edges.push(
|
|
1885
|
+
edges.push(kern().makeLineEdge(v3(localToWorld(pl, a[0], a[1])), v3(localToWorld(pl, b[0], b[1]))));
|
|
1867
1886
|
}
|
|
1868
1887
|
}
|
|
1869
1888
|
if (edges.length === 0)
|
|
1870
1889
|
throw new Error('[cq-compat] buildProfileWire: degenerate path wire');
|
|
1871
|
-
return
|
|
1890
|
+
return kern().makeWire(edges);
|
|
1872
1891
|
}
|
|
1873
1892
|
const ring = w.kind === 'rect'
|
|
1874
1893
|
? [
|
|
@@ -1888,9 +1907,9 @@ async function buildProfileWire(wp, w) {
|
|
|
1888
1907
|
for (let i = 0; i < ring.length; i++) {
|
|
1889
1908
|
const a = ring[i];
|
|
1890
1909
|
const b = ring[(i + 1) % ring.length];
|
|
1891
|
-
edges.push(
|
|
1910
|
+
edges.push(kern().makeLineEdge(v3(localToWorld(pl, a[0], a[1])), v3(localToWorld(pl, b[0], b[1]))));
|
|
1892
1911
|
}
|
|
1893
|
-
return
|
|
1912
|
+
return kern().makeWire(edges);
|
|
1894
1913
|
}
|
|
1895
1914
|
/**
|
|
1896
1915
|
* Extrude the pending wire LIST: one face per outermost wire, enclosed wires
|
|
@@ -1926,10 +1945,9 @@ async function pendingPathPrism(wp, vec, shift) {
|
|
|
1926
1945
|
for (const h of g.holes) {
|
|
1927
1946
|
holeWires.push(await buildProfileWire(wp, shift ? shiftWire(h, wp, shift) : h));
|
|
1928
1947
|
}
|
|
1929
|
-
const face =
|
|
1930
|
-
const prism =
|
|
1931
|
-
|
|
1932
|
-
result = result ? await fuseShapes(result, solid) : solid;
|
|
1948
|
+
const face = faceWithHoles(outer, holeWires);
|
|
1949
|
+
const prism = toShape(kern().extrude(brepOf(face), vec[0], vec[1], vec[2]));
|
|
1950
|
+
result = result ? await fuseShapes(result, prism) : prism;
|
|
1933
1951
|
}
|
|
1934
1952
|
if (!result)
|
|
1935
1953
|
throw new Error('[cq-compat] extrude: no pending wire to extrude');
|
|
@@ -1998,7 +2016,7 @@ export async function extrude(wp, height, combine = true, opts) {
|
|
|
1998
2016
|
// exact body is sewn from its faces.
|
|
1999
2017
|
return outwardTaperPrism(wp, base, profile, height, taper, combine);
|
|
2000
2018
|
}
|
|
2001
|
-
const face =
|
|
2019
|
+
const face = toShape(kern().makeFace(profile));
|
|
2002
2020
|
const kernel = getKernel();
|
|
2003
2021
|
const n = Array.isArray(wp.normal) ? wp.normal : [0, 0, 1];
|
|
2004
2022
|
const raw = kernel.draftPrism(rawShapeId(face), n[0] * height, n[1] * height, n[2] * height, taper);
|
|
@@ -2100,8 +2118,8 @@ export async function extrude(wp, height, combine = true, opts) {
|
|
|
2100
2118
|
// (shifted boss \ base) ∩ half-space below the face plane.
|
|
2101
2119
|
// Profiles fully inside the base's cross-section can never overhang,
|
|
2102
2120
|
// so the cheap bbox test skips the two extra booleans entirely.
|
|
2103
|
-
const bossB =
|
|
2104
|
-
const baseB =
|
|
2121
|
+
const bossB = kern().getBoundingBox(ownHandle(boss));
|
|
2122
|
+
const baseB = kern().getBoundingBox(ownHandle(base));
|
|
2105
2123
|
const PAD_EPS = 1e-6;
|
|
2106
2124
|
const axisOf = (v) => Math.abs(v[0]) > 0.5 ? 0 : Math.abs(v[1]) > 0.5 ? 1 : 2;
|
|
2107
2125
|
// In-plane axes = the two axes perpendicular to the face normal.
|
|
@@ -2117,7 +2135,7 @@ export async function extrude(wp, height, combine = true, opts) {
|
|
|
2117
2135
|
// Slab covering the half-space below the face plane (local z <= 0).
|
|
2118
2136
|
const o = Array.isArray(wp.origin) ? wp.origin : [0, 0, 0];
|
|
2119
2137
|
const BIG = 2 *
|
|
2120
|
-
Math.max(baseB.
|
|
2138
|
+
Math.max(baseB.xmax - baseB.xmin, baseB.ymax - baseB.ymin, baseB.zmax - baseB.zmin, bossB.xmax - bossB.xmin, bossB.ymax - bossB.ymin, bossB.zmax - bossB.zmin) +
|
|
2121
2139
|
10;
|
|
2122
2140
|
const belowWp = {
|
|
2123
2141
|
...wp,
|
|
@@ -2335,10 +2353,9 @@ export async function revolve(wp, angleDegrees = 360, axisStart, axisEnd, combin
|
|
|
2335
2353
|
const holeWires = [];
|
|
2336
2354
|
for (const h of g.holes)
|
|
2337
2355
|
holeWires.push(await buildProfileWire(wp, h));
|
|
2338
|
-
const face =
|
|
2339
|
-
const revolved =
|
|
2340
|
-
|
|
2341
|
-
result = result ? await fuseShapes(result, solid) : solid;
|
|
2356
|
+
const face = faceWithHoles(outer, holeWires);
|
|
2357
|
+
const revolved = toShape(kern().revolveVec(brepOf(face), v3(startW), v3(dir), (rad * 180) / Math.PI));
|
|
2358
|
+
result = result ? await fuseShapes(result, revolved) : revolved;
|
|
2342
2359
|
}
|
|
2343
2360
|
const base = wp.shape;
|
|
2344
2361
|
let shape = result;
|
|
@@ -2362,7 +2379,7 @@ export async function revolve(wp, angleDegrees = 360, axisStart, axisEnd, combin
|
|
|
2362
2379
|
/** True when the carrier holds at least one solid (upstream `findSolid()` gate). */
|
|
2363
2380
|
function hasSolidBase(shape) {
|
|
2364
2381
|
try {
|
|
2365
|
-
return
|
|
2382
|
+
return kern().getSubShapes(ownHandle(shape), 'solid').length > 0;
|
|
2366
2383
|
}
|
|
2367
2384
|
catch {
|
|
2368
2385
|
return false;
|
|
@@ -2378,9 +2395,9 @@ async function collectLoftSections(wp, sections) {
|
|
|
2378
2395
|
// Upstream `Workplane().add(face)...loft()` lofts the stacked faces of
|
|
2379
2396
|
// wp.shape: each face's outer wire becomes a loft section (holes are
|
|
2380
2397
|
// intentionally dropped — upstream section extraction is outerWire-only).
|
|
2381
|
-
const shapeFaces =
|
|
2398
|
+
const shapeFaces = kern().getSubShapes(ownHandle(wp.shape), 'face');
|
|
2382
2399
|
for (const f of shapeFaces) {
|
|
2383
|
-
sections.push(
|
|
2400
|
+
sections.push(kern().getSubShapes(f, 'wire')[0]);
|
|
2384
2401
|
}
|
|
2385
2402
|
}
|
|
2386
2403
|
}
|
|
@@ -2416,7 +2433,15 @@ export async function loft(wp, ...rest) {
|
|
|
2416
2433
|
loftCfg.startPoint = opts.startPoint;
|
|
2417
2434
|
if (opts?.endPoint)
|
|
2418
2435
|
loftCfg.endPoint = opts.endPoint;
|
|
2419
|
-
const
|
|
2436
|
+
const wireHandles = sections.map((w) => w);
|
|
2437
|
+
const startPt = loftCfg.startPoint !== undefined ? v3(loftCfg.startPoint) : null;
|
|
2438
|
+
const endPt = loftCfg.endPoint !== undefined ? v3(loftCfg.endPoint) : null;
|
|
2439
|
+
const ruled = loftCfg?.ruled ?? false;
|
|
2440
|
+
const startV = startPt !== null ? getKernel().makeVertex(startPt.x, startPt.y, startPt.z) : 0;
|
|
2441
|
+
const endV = endPt !== null ? getKernel().makeVertex(endPt.x, endPt.y, endPt.z) : 0;
|
|
2442
|
+
const solid = (startV !== 0 || endV !== 0
|
|
2443
|
+
? toShape(getKernel().loftWithVertices(wireHandles, true, ruled, startV, endV))
|
|
2444
|
+
: toShape(getKernel().loft(wireHandles, true, ruled)));
|
|
2420
2445
|
const base = wp.shape;
|
|
2421
2446
|
let shape = solid;
|
|
2422
2447
|
const combine = opts?.combine ?? true;
|
|
@@ -2466,7 +2491,7 @@ export async function cutBlind(wp, depth, opts) {
|
|
|
2466
2491
|
throw new Error('[cq-compat] cutBlind: taper requires exactly one pending profile wire');
|
|
2467
2492
|
}
|
|
2468
2493
|
const profile = await buildProfileWire(wp, solidWires[0]);
|
|
2469
|
-
const face =
|
|
2494
|
+
const face = toShape(kern().makeFace(profile));
|
|
2470
2495
|
const kernel = getKernel();
|
|
2471
2496
|
const raw = kernel.draftPrism(rawShapeId(face), invNormal[0] * absDepth, invNormal[1] * absDepth, invNormal[2] * absDepth, opts.taper);
|
|
2472
2497
|
const tool = fromHandle(raw);
|
|
@@ -3231,12 +3256,11 @@ export async function faceCompound(wp, sel) {
|
|
|
3231
3256
|
return wp;
|
|
3232
3257
|
const s = NAMED_VIEW_TO_AXIS[sel.trim().toLowerCase()] ?? sel;
|
|
3233
3258
|
if (s.trim().toLowerCase() === 'all') {
|
|
3234
|
-
const faces =
|
|
3259
|
+
const faces = kern().getSubShapes(ownHandle(wp.shape), 'face');
|
|
3235
3260
|
if (faces.length === 0) {
|
|
3236
3261
|
throw new Error('[cq-compat] faceCompound "all": shape has no faces');
|
|
3237
3262
|
}
|
|
3238
|
-
const
|
|
3239
|
-
const shape = adoptBrepjsProduct(unwrapBrepResult(product));
|
|
3263
|
+
const shape = toShape(kern().makeCompound(faces));
|
|
3240
3264
|
return clone(wp, { shape, faceSel: null, edgeSel: null, vertexSel: null });
|
|
3241
3265
|
}
|
|
3242
3266
|
const m = /^([<>])([XYZ])(?:\[-?\d+\])?$/.exec(s.trim());
|
|
@@ -3245,25 +3269,24 @@ export async function faceCompound(wp, sel) {
|
|
|
3245
3269
|
}
|
|
3246
3270
|
const axis = m[2] === 'X' ? 0 : m[2] === 'Y' ? 1 : 2;
|
|
3247
3271
|
const sign = m[1] === '>' ? 1 : -1;
|
|
3248
|
-
const bounds = (h) =>
|
|
3249
|
-
const faces =
|
|
3272
|
+
const bounds = (h) => kern().getBoundingBox(h);
|
|
3273
|
+
const faces = kern().getSubShapes(ownHandle(wp.shape), 'face');
|
|
3250
3274
|
// DirectionMinMaxSelector: among faces perpendicular to the axis, take ALL
|
|
3251
3275
|
// faces whose center sits at the extremum (ties included — the two-boxes
|
|
3252
3276
|
// compound exports BOTH top faces).
|
|
3253
3277
|
const perp = faces.filter((f) => {
|
|
3254
3278
|
const b = bounds(f);
|
|
3255
|
-
return [b.
|
|
3279
|
+
return [b.xmax - b.xmin, b.ymax - b.ymin, b.zmax - b.zmin][axis] <= 0.1;
|
|
3256
3280
|
});
|
|
3257
3281
|
if (perp.length === 0) {
|
|
3258
3282
|
throw new Error(`[cq-compat] no planar face for selector "${sel}"`);
|
|
3259
3283
|
}
|
|
3260
|
-
const center = (b) => [(b.
|
|
3284
|
+
const center = (b) => [(b.xmin + b.xmax) / 2, (b.ymin + b.ymax) / 2, (b.zmin + b.zmax) / 2][axis];
|
|
3261
3285
|
const extremum = perp
|
|
3262
3286
|
.map((f) => center(bounds(f)))
|
|
3263
3287
|
.reduce((best, c) => (sign * c > sign * best ? c : best));
|
|
3264
3288
|
const picked = perp.filter((f) => Math.abs(center(bounds(f)) - extremum) <= 1e-6);
|
|
3265
|
-
const
|
|
3266
|
-
const shape = adoptBrepjsProduct(unwrapBrepResult(product));
|
|
3289
|
+
const shape = toShape(kern().makeCompound(picked));
|
|
3267
3290
|
return clone(wp, { shape, faceSel: null, edgeSel: null, vertexSel: null });
|
|
3268
3291
|
}
|
|
3269
3292
|
/**
|
|
@@ -3292,15 +3315,14 @@ export async function edgeCompound(wp, sel) {
|
|
|
3292
3315
|
}
|
|
3293
3316
|
const axis = m[2] === 'X' ? 0 : m[2] === 'Y' ? 1 : 2;
|
|
3294
3317
|
const sign = m[1] === '>' ? 1 : -1;
|
|
3295
|
-
const bounds = (h) =>
|
|
3296
|
-
const edges =
|
|
3297
|
-
const center = (b) => [(b.
|
|
3318
|
+
const bounds = (h) => kern().getBoundingBox(h);
|
|
3319
|
+
const edges = kern().getSubShapes(ownHandle(wp.shape), 'edge');
|
|
3320
|
+
const center = (b) => [(b.xmin + b.xmax) / 2, (b.ymin + b.ymax) / 2, (b.zmin + b.zmax) / 2][axis];
|
|
3298
3321
|
const extremum = edges
|
|
3299
3322
|
.map((e) => center(bounds(e)))
|
|
3300
3323
|
.reduce((best, c) => (sign * c > sign * best ? c : best));
|
|
3301
3324
|
const picked = edges.filter((e) => Math.abs(center(bounds(e)) - extremum) <= 1e-6);
|
|
3302
|
-
const
|
|
3303
|
-
const shape = adoptBrepjsProduct(unwrapBrepResult(product));
|
|
3325
|
+
const shape = toShape(kern().makeCompound(picked));
|
|
3304
3326
|
return clone(wp, { shape, faceSel: null, edgeSel: null, vertexSel: null });
|
|
3305
3327
|
}
|
|
3306
3328
|
function locNum(v) {
|
|
@@ -3510,7 +3532,7 @@ async function applyLocation(shape, loc) {
|
|
|
3510
3532
|
// `moved` (they fold the locations with composeLocations instead).
|
|
3511
3533
|
const solids = (() => {
|
|
3512
3534
|
try {
|
|
3513
|
-
return
|
|
3535
|
+
return kern().getSubShapes(ownHandle(shape), 'solid').length;
|
|
3514
3536
|
}
|
|
3515
3537
|
catch {
|
|
3516
3538
|
return 0;
|
|
@@ -3526,11 +3548,11 @@ async function applyLocation(shape, loc) {
|
|
|
3526
3548
|
}
|
|
3527
3549
|
return s;
|
|
3528
3550
|
}
|
|
3529
|
-
const product =
|
|
3551
|
+
const product = applyMatrixBrep(shape, {
|
|
3530
3552
|
linear: rotationMatrixDeg(loc.rot),
|
|
3531
3553
|
translation: [x, y, z],
|
|
3532
|
-
})
|
|
3533
|
-
return
|
|
3554
|
+
});
|
|
3555
|
+
return toShape(unwrapBrepResult(product));
|
|
3534
3556
|
}
|
|
3535
3557
|
/**
|
|
3536
3558
|
* moved — apply one or more Locations to the carried geometry.
|
|
@@ -3562,8 +3584,8 @@ export async function moved(wp, ...locs) {
|
|
|
3562
3584
|
// Upstream `_compound_or_shape` groups the copies without any boolean or
|
|
3563
3585
|
// clean pass — mirroring that keeps the topology (face/solid counts) equal
|
|
3564
3586
|
// to upstream, which the STEP comparison gates on.
|
|
3565
|
-
const handles = copies.map((c) =>
|
|
3566
|
-
shape =
|
|
3587
|
+
const handles = copies.map((c) => ownHandle(c));
|
|
3588
|
+
shape = toShape(kern().makeCompound(handles));
|
|
3567
3589
|
}
|
|
3568
3590
|
return clone(wp, {
|
|
3569
3591
|
shape,
|
|
@@ -3654,7 +3676,7 @@ export async function face(wp) {
|
|
|
3654
3676
|
for (const h of g.holes) {
|
|
3655
3677
|
holeWires.push(await buildProfileWire(wp, h));
|
|
3656
3678
|
}
|
|
3657
|
-
faces.push(
|
|
3679
|
+
faces.push(faceWithHoles(outer, holeWires));
|
|
3658
3680
|
}
|
|
3659
3681
|
const shape = faces.length === 1 ? faces[0] : makeCompoundShape(faces);
|
|
3660
3682
|
return clone(wp, {
|
|
@@ -3680,7 +3702,7 @@ export async function face(wp) {
|
|
|
3680
3702
|
* @returns Shape holding the vertex
|
|
3681
3703
|
*/
|
|
3682
3704
|
export function vertex(x = 0, y = 0, z = 0) {
|
|
3683
|
-
return
|
|
3705
|
+
return toShape(kern().makeVertex(x, y, z));
|
|
3684
3706
|
}
|
|
3685
3707
|
/**
|
|
3686
3708
|
* compound — upstream module-level `compound(*shapes)` free function: bundle
|
|
@@ -3733,7 +3755,7 @@ export async function intersect(wp, other) {
|
|
|
3733
3755
|
* is out of scope for this layer.
|
|
3734
3756
|
*/
|
|
3735
3757
|
function resolveEdgeSelection(shape, sel) {
|
|
3736
|
-
const edges =
|
|
3758
|
+
const edges = kern().getSubShapes(ownHandle(shape), 'edge');
|
|
3737
3759
|
if (!sel || sel === '')
|
|
3738
3760
|
return edges;
|
|
3739
3761
|
const m = /^\|([XYZ])$/.exec(sel.trim());
|
|
@@ -3747,9 +3769,9 @@ function resolveEdgeSelection(shape, sel) {
|
|
|
3747
3769
|
// must dominate the two perpendicular extents (which stay padding-sized).
|
|
3748
3770
|
const PAD = 0.5; // mm — max perpendicular extent for an axis-parallel edge
|
|
3749
3771
|
return edges.filter((e) => {
|
|
3750
|
-
const b =
|
|
3751
|
-
const min = [b.
|
|
3752
|
-
const max = [b.
|
|
3772
|
+
const b = kern().getBoundingBox(e);
|
|
3773
|
+
const min = [b.xmin, b.ymin, b.zmin];
|
|
3774
|
+
const max = [b.xmax, b.ymax, b.zmax];
|
|
3753
3775
|
const extents = [max[0] - min[0], max[1] - min[1], max[2] - min[2]];
|
|
3754
3776
|
const axisExtent = extents[axisIdx];
|
|
3755
3777
|
return (perp.every((i) => extents[i] <= PAD) &&
|
|
@@ -3785,9 +3807,8 @@ export async function fillet(wp, radius) {
|
|
|
3785
3807
|
else {
|
|
3786
3808
|
edges = resolveEdgeSelection(wp.shape, undefined);
|
|
3787
3809
|
}
|
|
3788
|
-
const
|
|
3789
|
-
const
|
|
3790
|
-
const shape = adoptBrepjsProduct(product);
|
|
3810
|
+
const product = kern().fillet(ownHandle(wp.shape), edges, radius);
|
|
3811
|
+
const shape = toShape(product);
|
|
3791
3812
|
return clone(wp, { shape, edgeSel: null, faceSel: null });
|
|
3792
3813
|
}
|
|
3793
3814
|
/**
|
|
@@ -3822,9 +3843,8 @@ export async function chamfer(wp, length, length2) {
|
|
|
3822
3843
|
else {
|
|
3823
3844
|
edges = resolveEdgeSelection(wp.shape, undefined);
|
|
3824
3845
|
}
|
|
3825
|
-
const
|
|
3826
|
-
const
|
|
3827
|
-
const shape = adoptBrepjsProduct(product);
|
|
3846
|
+
const product = kern().chamfer(ownHandle(wp.shape), edges, length);
|
|
3847
|
+
const shape = toShape(product);
|
|
3828
3848
|
return clone(wp, { shape, edgeSel: null, faceSel: null });
|
|
3829
3849
|
}
|
|
3830
3850
|
/**
|
|
@@ -3844,16 +3864,16 @@ function resolveFaceEdgeSelection(shape, sel) {
|
|
|
3844
3864
|
}
|
|
3845
3865
|
const axis = m[2] === 'X' ? 0 : m[2] === 'Y' ? 1 : 2;
|
|
3846
3866
|
const sign = m[1] === '>' || m[1] === '+' ? 1 : -1;
|
|
3847
|
-
const bounds = (h) =>
|
|
3848
|
-
const faces =
|
|
3867
|
+
const bounds = (h) => kern().getBoundingBox(h);
|
|
3868
|
+
const faces = kern().getSubShapes(ownHandle(shape), 'face');
|
|
3849
3869
|
const perp = faces.filter((f) => {
|
|
3850
3870
|
const b = bounds(f);
|
|
3851
|
-
return [b.
|
|
3871
|
+
return [b.xmax - b.xmin, b.ymax - b.ymin, b.zmax - b.zmin][axis] <= 0.1;
|
|
3852
3872
|
});
|
|
3853
3873
|
if (perp.length === 0) {
|
|
3854
3874
|
throw new Error(`[cq-compat] no planar face for selector "${sel}"`);
|
|
3855
3875
|
}
|
|
3856
|
-
const faceCenter = (b) => [(b.
|
|
3876
|
+
const faceCenter = (b) => [(b.xmin + b.xmax) / 2, (b.ymin + b.ymax) / 2, (b.zmin + b.zmax) / 2][axis];
|
|
3857
3877
|
const target = perp
|
|
3858
3878
|
.map((f) => ({ f, b: bounds(f) }))
|
|
3859
3879
|
.reduce((best, cur) => (sign * (faceCenter(cur.b) - faceCenter(best.b)) > 0 ? cur : best));
|
|
@@ -3864,11 +3884,11 @@ function resolveFaceEdgeSelection(shape, sel) {
|
|
|
3864
3884
|
// padding) and its center coincides with the face center.
|
|
3865
3885
|
const EDGE_AXIS_MAX = 0.25;
|
|
3866
3886
|
const EDGE_CENTER_TOL = 0.15;
|
|
3867
|
-
const edges =
|
|
3887
|
+
const edges = kern().getSubShapes(ownHandle(shape), 'edge');
|
|
3868
3888
|
return edges.filter((e) => {
|
|
3869
3889
|
const b = bounds(e);
|
|
3870
|
-
const ext = [b.
|
|
3871
|
-
const c = [(b.
|
|
3890
|
+
const ext = [b.xmax - b.xmin, b.ymax - b.ymin, b.zmax - b.zmin][axis];
|
|
3891
|
+
const c = [(b.xmin + b.xmax) / 2, (b.ymin + b.ymax) / 2, (b.zmin + b.zmax) / 2][axis];
|
|
3872
3892
|
return ext <= EDGE_AXIS_MAX && Math.abs(c - fc) <= EDGE_CENTER_TOL;
|
|
3873
3893
|
});
|
|
3874
3894
|
}
|