fluidcad 0.0.6 → 0.0.8
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/bin/fluidcad.js +16 -0
- package/lib/dist/core/2d/arc.d.ts +15 -0
- package/lib/dist/core/2d/arc.js +10 -0
- package/lib/dist/core/2d/index.d.ts +0 -1
- package/lib/dist/core/2d/index.js +0 -1
- package/lib/dist/core/2d/tarc.d.ts +26 -2
- package/lib/dist/core/2d/tcircle.d.ts +26 -2
- package/lib/dist/core/2d/tline.d.ts +26 -6
- package/lib/dist/core/axis.d.ts +11 -4
- package/lib/dist/core/chamfer.d.ts +6 -6
- package/lib/dist/core/chamfer.js +12 -9
- package/lib/dist/core/cut.d.ts +27 -6
- package/lib/dist/core/extrude.d.ts +15 -3
- package/lib/dist/core/plane.d.ts +11 -4
- package/lib/dist/core/repeat.d.ts +16 -1
- package/lib/dist/core/repeat.js +39 -3
- package/lib/dist/core/sketch.d.ts +9 -3
- package/lib/dist/core/subtract.d.ts +5 -4
- package/lib/dist/core/subtract.js +9 -2
- package/lib/dist/features/2d/arc-three-points.d.ts +19 -0
- package/lib/dist/features/2d/arc-three-points.js +75 -0
- package/lib/dist/features/2d/constraints/geometry-qualifier.d.ts +16 -0
- package/lib/dist/features/2d/constraints/geometry-qualifier.js +16 -0
- package/lib/dist/features/2d/offset.js +10 -0
- package/lib/dist/features/2d/projection.js +9 -0
- package/lib/dist/features/2d/rect.js +19 -16
- package/lib/dist/features/chamfer.d.ts +4 -4
- package/lib/dist/features/chamfer.js +33 -15
- package/lib/dist/features/common2d.js +26 -8
- package/lib/dist/features/extrude-base.js +2 -1
- package/lib/dist/features/fuse2d.js +24 -6
- package/lib/dist/features/loft.js +2 -1
- package/lib/dist/features/repeat-matrix.d.ts +14 -0
- package/lib/dist/features/repeat-matrix.js +41 -0
- package/lib/dist/features/select.d.ts +1 -0
- package/lib/dist/features/select.js +19 -0
- package/lib/dist/features/subtract2d.d.ts +14 -0
- package/lib/dist/features/subtract2d.js +80 -0
- package/lib/dist/filters/edge/belongs-to-face.d.ts +22 -0
- package/lib/dist/filters/edge/belongs-to-face.js +67 -0
- package/lib/dist/filters/edge/belongs-to-object.d.ts +18 -0
- package/lib/dist/filters/edge/belongs-to-object.js +37 -0
- package/lib/dist/filters/edge/edge-filter.d.ts +88 -3
- package/lib/dist/filters/edge/edge-filter.js +101 -4
- package/lib/dist/filters/edge/line-filter.d.ts +4 -1
- package/lib/dist/filters/edge/line-filter.js +14 -7
- package/lib/dist/filters/face/edge-count.d.ts +17 -0
- package/lib/dist/filters/face/edge-count.js +33 -0
- package/lib/dist/filters/face/face-filter.d.ts +93 -0
- package/lib/dist/filters/face/face-filter.js +112 -0
- package/lib/dist/filters/face/has-edge.d.ts +18 -0
- package/lib/dist/filters/face/has-edge.js +59 -0
- package/lib/dist/filters/face/has-object.d.ts +18 -0
- package/lib/dist/filters/face/has-object.js +37 -0
- package/lib/dist/filters/index.d.ts +6 -0
- package/lib/dist/filters/index.js +6 -0
- package/lib/dist/oc/edge-query.d.ts +2 -2
- package/lib/dist/oc/edge-query.js +13 -4
- package/lib/dist/tests/features/2d/arc.test.js +15 -1
- package/lib/dist/tests/features/select.test.js +249 -0
- package/lib/dist/tests/features/subtract2d.test.d.ts +1 -0
- package/lib/dist/tests/features/subtract2d.test.js +63 -0
- package/lib/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -2
- package/server/dist/vite-manager.js +23 -5
- package/lib/dist/core/2d/wire.d.ts +0 -11
- package/lib/dist/core/2d/wire.js +0 -29
- package/lib/dist/features/2d/wire.d.ts +0 -12
- package/lib/dist/features/2d/wire.js +0 -76
|
@@ -296,6 +296,99 @@ describe("select", () => {
|
|
|
296
296
|
expect(sel.getShapes()).toHaveLength(4);
|
|
297
297
|
});
|
|
298
298
|
});
|
|
299
|
+
describe("hasEdge / notHasEdge", () => {
|
|
300
|
+
it("should select faces that have circular edges", () => {
|
|
301
|
+
cylinder(30, 50);
|
|
302
|
+
// All 3 faces have circular edges (top, bottom, and cylindrical side bounded by circles)
|
|
303
|
+
const sel = select(face().hasEdge(edge().circle()));
|
|
304
|
+
render();
|
|
305
|
+
expect(sel.getShapes()).toHaveLength(3);
|
|
306
|
+
});
|
|
307
|
+
it("should select faces that have line edges", () => {
|
|
308
|
+
sketch("xy", () => {
|
|
309
|
+
rect(100, 50);
|
|
310
|
+
});
|
|
311
|
+
extrude(30);
|
|
312
|
+
// All 6 faces of a box have line edges
|
|
313
|
+
const sel = select(face().hasEdge(edge().line()));
|
|
314
|
+
render();
|
|
315
|
+
expect(sel.getShapes()).toHaveLength(6);
|
|
316
|
+
});
|
|
317
|
+
it("should select faces with multiple edge criteria (AND)", () => {
|
|
318
|
+
sketch("xy", () => {
|
|
319
|
+
rect(100, 50);
|
|
320
|
+
});
|
|
321
|
+
extrude(30);
|
|
322
|
+
// Faces that have both a line edge on XY AND a line edge on XY offset 30
|
|
323
|
+
// The 4 side faces each have edges on both bottom and top planes
|
|
324
|
+
const sel = select(face().hasEdge(edge().onPlane("xy"), edge().onPlane("xy", 30)));
|
|
325
|
+
render();
|
|
326
|
+
expect(sel.getShapes()).toHaveLength(4);
|
|
327
|
+
});
|
|
328
|
+
it("should select faces with vertical and horizontal edges", () => {
|
|
329
|
+
sketch("xy", () => {
|
|
330
|
+
rect(100, 50);
|
|
331
|
+
});
|
|
332
|
+
extrude(30);
|
|
333
|
+
// Side faces have both vertical and horizontal edges
|
|
334
|
+
const sel = select(face().hasEdge(edge().verticalTo("xy"), edge().parallelTo("xy")));
|
|
335
|
+
render();
|
|
336
|
+
// 4 side faces
|
|
337
|
+
expect(sel.getShapes()).toHaveLength(4);
|
|
338
|
+
});
|
|
339
|
+
it("should combine hasEdge with other face filters", () => {
|
|
340
|
+
cylinder(30, 50);
|
|
341
|
+
// Circle faces that also have circular edges
|
|
342
|
+
const sel = select(face().circle().hasEdge(edge().circle()));
|
|
343
|
+
render();
|
|
344
|
+
expect(sel.getShapes()).toHaveLength(2);
|
|
345
|
+
});
|
|
346
|
+
it("should exclude faces with notHasEdge", () => {
|
|
347
|
+
sketch("xy", () => {
|
|
348
|
+
rect(100, 50);
|
|
349
|
+
});
|
|
350
|
+
extrude(30);
|
|
351
|
+
// Exclude faces that have vertical edges → only top and bottom faces remain
|
|
352
|
+
const sel = select(face().notHasEdge(edge().verticalTo("xy")));
|
|
353
|
+
render();
|
|
354
|
+
expect(sel.getShapes()).toHaveLength(2);
|
|
355
|
+
});
|
|
356
|
+
it("should have complementary results between hasEdge and notHasEdge", () => {
|
|
357
|
+
sketch("xy", () => {
|
|
358
|
+
rect(100, 50);
|
|
359
|
+
});
|
|
360
|
+
extrude(30);
|
|
361
|
+
// Side faces have vertical edges, top/bottom do not
|
|
362
|
+
const has = select(face().hasEdge(edge().verticalTo("xy")));
|
|
363
|
+
const notHas = select(face().notHasEdge(edge().verticalTo("xy")));
|
|
364
|
+
render();
|
|
365
|
+
expect(has.getShapes()).toHaveLength(4);
|
|
366
|
+
expect(notHas.getShapes()).toHaveLength(2);
|
|
367
|
+
// No overlap
|
|
368
|
+
for (const s of has.getShapes()) {
|
|
369
|
+
expect(notHas.getShapes().some(ns => ns.isSame(s))).toBe(false);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
it("should select faces matching edges from a scene object", () => {
|
|
373
|
+
cylinder(30, 50);
|
|
374
|
+
const circleEdges = select(edge().circle());
|
|
375
|
+
const sel = select(face().hasEdge(circleEdges));
|
|
376
|
+
render();
|
|
377
|
+
// All 3 cylinder faces share a circular edge
|
|
378
|
+
expect(sel.getShapes()).toHaveLength(3);
|
|
379
|
+
});
|
|
380
|
+
it("should exclude faces with notHasEdge using a scene object", () => {
|
|
381
|
+
sketch("xy", () => {
|
|
382
|
+
rect(100, 50);
|
|
383
|
+
});
|
|
384
|
+
extrude(30);
|
|
385
|
+
const bottomEdges = select(edge().belongsToFace(face().onPlane("xy")));
|
|
386
|
+
const sel = select(face().notHasEdge(bottomEdges));
|
|
387
|
+
render();
|
|
388
|
+
// Top face has no edges from the bottom face selection
|
|
389
|
+
expect(sel.getShapes()).toHaveLength(1);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
299
392
|
describe("OR logic (multiple builders)", () => {
|
|
300
393
|
it("should match faces satisfying any builder", () => {
|
|
301
394
|
cylinder(30, 50);
|
|
@@ -334,4 +427,160 @@ describe("select", () => {
|
|
|
334
427
|
expect(sel.getShapes()).toHaveLength(5);
|
|
335
428
|
});
|
|
336
429
|
});
|
|
430
|
+
describe("belongsToFace / notBelongsToFace", () => {
|
|
431
|
+
it("should select edges belonging to circular faces", () => {
|
|
432
|
+
cylinder(30, 50);
|
|
433
|
+
// Circular faces (top and bottom discs) each have 1 circular edge
|
|
434
|
+
const sel = select(edge().belongsToFace(face().circle()));
|
|
435
|
+
render();
|
|
436
|
+
expect(sel.getShapes()).toHaveLength(2);
|
|
437
|
+
expect(sel.getShapes().every(s => s.getType() === "edge")).toBe(true);
|
|
438
|
+
});
|
|
439
|
+
it("should select edges belonging to faces on a specific plane", () => {
|
|
440
|
+
sketch("xy", () => {
|
|
441
|
+
rect(100, 50);
|
|
442
|
+
});
|
|
443
|
+
extrude(30);
|
|
444
|
+
// Bottom face (on XY) has 4 edges
|
|
445
|
+
const sel = select(edge().belongsToFace(face().onPlane("xy")));
|
|
446
|
+
render();
|
|
447
|
+
expect(sel.getShapes()).toHaveLength(4);
|
|
448
|
+
});
|
|
449
|
+
it("should select edges belonging to faces on offset plane", () => {
|
|
450
|
+
sketch("xy", () => {
|
|
451
|
+
rect(100, 50);
|
|
452
|
+
});
|
|
453
|
+
extrude(30);
|
|
454
|
+
// Top face (on XY offset 30) has 4 edges
|
|
455
|
+
const sel = select(edge().belongsToFace(face().onPlane("xy", 30)));
|
|
456
|
+
render();
|
|
457
|
+
expect(sel.getShapes()).toHaveLength(4);
|
|
458
|
+
});
|
|
459
|
+
it("should select edges with multiple face criteria (AND)", () => {
|
|
460
|
+
sketch("xy", () => {
|
|
461
|
+
rect(100, 50);
|
|
462
|
+
});
|
|
463
|
+
extrude(30);
|
|
464
|
+
// Edges belonging to a face that is both parallel to XY AND on the bottom plane
|
|
465
|
+
// Only the bottom face matches both criteria → 4 edges
|
|
466
|
+
const sel = select(edge().belongsToFace(face().parallelTo("xy").onPlane("xy")));
|
|
467
|
+
render();
|
|
468
|
+
expect(sel.getShapes()).toHaveLength(4);
|
|
469
|
+
});
|
|
470
|
+
it("should select edges with multiple face filter builders (AND across builders)", () => {
|
|
471
|
+
sketch("xy", () => {
|
|
472
|
+
rect(100, 50);
|
|
473
|
+
});
|
|
474
|
+
extrude(30);
|
|
475
|
+
// Edges that belong to a face on XY AND also belong to a face NOT parallel to XY
|
|
476
|
+
// Bottom face edges: the 4 bottom edges each also belong to a side face (not parallel to XY)
|
|
477
|
+
const sel = select(edge().belongsToFace(face().onPlane("xy"), face().notParallelTo("xy")));
|
|
478
|
+
render();
|
|
479
|
+
// All 4 bottom edges belong to the bottom face (on XY) AND each belongs to a side face (not parallel to XY)
|
|
480
|
+
expect(sel.getShapes()).toHaveLength(4);
|
|
481
|
+
});
|
|
482
|
+
it("should combine belongsToFace with other edge filters", () => {
|
|
483
|
+
sketch("xy", () => {
|
|
484
|
+
rect(100, 50);
|
|
485
|
+
});
|
|
486
|
+
extrude(30);
|
|
487
|
+
// Line edges that belong to faces parallel to XY (top and bottom)
|
|
488
|
+
// Top face has 4 edges, bottom face has 4 edges → 8 edges total
|
|
489
|
+
const sel = select(edge().line().belongsToFace(face().parallelTo("xy")));
|
|
490
|
+
render();
|
|
491
|
+
expect(sel.getShapes()).toHaveLength(8);
|
|
492
|
+
});
|
|
493
|
+
it("should combine belongsToFace with circle edge filter", () => {
|
|
494
|
+
cylinder(30, 50);
|
|
495
|
+
// Circular edges that belong to circular faces (discs)
|
|
496
|
+
// Top and bottom discs each have 1 circular edge → 2 edges
|
|
497
|
+
const sel = select(edge().circle().belongsToFace(face().circle()));
|
|
498
|
+
render();
|
|
499
|
+
expect(sel.getShapes()).toHaveLength(2);
|
|
500
|
+
});
|
|
501
|
+
it("should select edges belonging to cylindrical faces", () => {
|
|
502
|
+
cylinder(30, 50);
|
|
503
|
+
// The cylindrical side face has 3 edges: 2 circular (top/bottom) + 1 seam edge
|
|
504
|
+
const sel = select(edge().belongsToFace(face().cylinder()));
|
|
505
|
+
render();
|
|
506
|
+
expect(sel.getShapes()).toHaveLength(3);
|
|
507
|
+
});
|
|
508
|
+
it("should exclude edges with notBelongsToFace", () => {
|
|
509
|
+
cylinder(30, 50);
|
|
510
|
+
// Exclude edges belonging to circular faces → only edges on the cylindrical face that aren't shared with discs
|
|
511
|
+
// Cylinder has 3 total edges: 2 circles + 1 seam. The 2 circles belong to circular faces.
|
|
512
|
+
// The seam edge only belongs to the cylindrical face.
|
|
513
|
+
const sel = select(edge().notBelongsToFace(face().circle()));
|
|
514
|
+
render();
|
|
515
|
+
// Only the seam edge doesn't belong to any circular face
|
|
516
|
+
expect(sel.getShapes()).toHaveLength(1);
|
|
517
|
+
});
|
|
518
|
+
it("should have complementary results between belongsToFace and notBelongsToFace", () => {
|
|
519
|
+
sketch("xy", () => {
|
|
520
|
+
rect(100, 50);
|
|
521
|
+
});
|
|
522
|
+
extrude(30);
|
|
523
|
+
// Edges belonging to the bottom face vs edges NOT belonging to the bottom face
|
|
524
|
+
const belongs = select(edge().belongsToFace(face().onPlane("xy")));
|
|
525
|
+
const notBelongs = select(edge().notBelongsToFace(face().onPlane("xy")));
|
|
526
|
+
render();
|
|
527
|
+
expect(belongs.getShapes()).toHaveLength(4);
|
|
528
|
+
// Box has 12 edges total, 4 on bottom face → 8 not on bottom
|
|
529
|
+
expect(notBelongs.getShapes()).toHaveLength(8);
|
|
530
|
+
// No overlap
|
|
531
|
+
for (const s of belongs.getShapes()) {
|
|
532
|
+
expect(notBelongs.getShapes().some(ns => ns.isSame(s))).toBe(false);
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
it("should cover all edges between belongsToFace and notBelongsToFace", () => {
|
|
536
|
+
cylinder(30, 50);
|
|
537
|
+
const belongs = select(edge().belongsToFace(face().cylinder()));
|
|
538
|
+
const notBelongs = select(edge().notBelongsToFace(face().cylinder()));
|
|
539
|
+
render();
|
|
540
|
+
// Cylinder has 3 edges total, all belong to the cylindrical face
|
|
541
|
+
expect(belongs.getShapes().length + notBelongs.getShapes().length).toBe(3);
|
|
542
|
+
// No overlap
|
|
543
|
+
for (const s of belongs.getShapes()) {
|
|
544
|
+
expect(notBelongs.getShapes().some(ns => ns.isSame(s))).toBe(false);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
it("should select all edges when face filter matches all faces", () => {
|
|
548
|
+
sketch("xy", () => {
|
|
549
|
+
rect(100, 50);
|
|
550
|
+
});
|
|
551
|
+
extrude(30);
|
|
552
|
+
// Every edge belongs to at least one face that has line edges (all 6 faces of a box)
|
|
553
|
+
const sel = select(edge().belongsToFace(face().hasEdge(edge().line())));
|
|
554
|
+
render();
|
|
555
|
+
// All 12 edges of the box
|
|
556
|
+
expect(sel.getShapes()).toHaveLength(12);
|
|
557
|
+
});
|
|
558
|
+
it("should use OR logic with multiple builders", () => {
|
|
559
|
+
sketch("xy", () => {
|
|
560
|
+
rect(100, 50);
|
|
561
|
+
});
|
|
562
|
+
extrude(30);
|
|
563
|
+
// Edges on bottom face OR edges on top face
|
|
564
|
+
const sel = select(edge().belongsToFace(face().onPlane("xy")), edge().belongsToFace(face().onPlane("xy", 30)));
|
|
565
|
+
render();
|
|
566
|
+
// 4 bottom + 4 top = 8 edges
|
|
567
|
+
expect(sel.getShapes()).toHaveLength(8);
|
|
568
|
+
});
|
|
569
|
+
it("should select edges belonging to faces from a scene object", () => {
|
|
570
|
+
cylinder(30, 50);
|
|
571
|
+
const circleFaces = select(face().circle());
|
|
572
|
+
const sel = select(edge().belongsToFace(circleFaces));
|
|
573
|
+
render();
|
|
574
|
+
// Top and bottom disc faces each have 1 circular edge = 2 edges
|
|
575
|
+
expect(sel.getShapes()).toHaveLength(2);
|
|
576
|
+
});
|
|
577
|
+
it("should exclude edges with notBelongsToFace using a scene object", () => {
|
|
578
|
+
cylinder(30, 50);
|
|
579
|
+
const circleFaces = select(face().circle());
|
|
580
|
+
const sel = select(edge().notBelongsToFace(circleFaces));
|
|
581
|
+
render();
|
|
582
|
+
// Cylinder has 3 edges; 2 belong to circle faces, so 1 remains (the seam)
|
|
583
|
+
expect(sel.getShapes()).toHaveLength(1);
|
|
584
|
+
});
|
|
585
|
+
});
|
|
337
586
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { setupOC, render } from "../setup.js";
|
|
3
|
+
import sketch from "../../core/sketch.js";
|
|
4
|
+
import extrude from "../../core/extrude.js";
|
|
5
|
+
import subtract from "../../core/subtract.js";
|
|
6
|
+
import { circle, rect } from "../../core/2d/index.js";
|
|
7
|
+
import { ShapeOps } from "../../oc/shape-ops.js";
|
|
8
|
+
describe("subtract2d", () => {
|
|
9
|
+
setupOC();
|
|
10
|
+
it("should subtract one circle from another", () => {
|
|
11
|
+
sketch("xy", () => {
|
|
12
|
+
const c1 = circle(60);
|
|
13
|
+
const c2 = circle([30, 0], 40);
|
|
14
|
+
subtract(c1, c2);
|
|
15
|
+
});
|
|
16
|
+
const e = extrude(20);
|
|
17
|
+
render();
|
|
18
|
+
const shapes = e.getShapes();
|
|
19
|
+
expect(shapes).toHaveLength(1);
|
|
20
|
+
expect(shapes[0].getType()).toBe("solid");
|
|
21
|
+
// Result should be narrower than the base circle
|
|
22
|
+
const bbox = ShapeOps.getBoundingBox(shapes[0]);
|
|
23
|
+
const width = bbox.maxX - bbox.minX;
|
|
24
|
+
expect(width).toBeLessThan(120);
|
|
25
|
+
});
|
|
26
|
+
it("should subtract a rect from a circle", () => {
|
|
27
|
+
sketch("xy", () => {
|
|
28
|
+
const c1 = circle(60);
|
|
29
|
+
const r1 = rect([0, 0], 40, 40);
|
|
30
|
+
subtract(c1, r1);
|
|
31
|
+
});
|
|
32
|
+
const e = extrude(20);
|
|
33
|
+
render();
|
|
34
|
+
const shapes = e.getShapes();
|
|
35
|
+
expect(shapes).toHaveLength(1);
|
|
36
|
+
expect(shapes[0].getType()).toBe("solid");
|
|
37
|
+
});
|
|
38
|
+
it("should remove original edges from both operands", () => {
|
|
39
|
+
const s = sketch("xy", () => {
|
|
40
|
+
const c1 = circle(60);
|
|
41
|
+
const c2 = circle([30, 0], 40);
|
|
42
|
+
subtract(c1, c2);
|
|
43
|
+
});
|
|
44
|
+
render();
|
|
45
|
+
// Original circles should have their edges removed
|
|
46
|
+
const c1Shapes = s.getChildren()[0].getShapes();
|
|
47
|
+
const c2Shapes = s.getChildren()[1].getShapes();
|
|
48
|
+
expect(c1Shapes).toHaveLength(0);
|
|
49
|
+
expect(c2Shapes).toHaveLength(0);
|
|
50
|
+
});
|
|
51
|
+
it("should not modify shapes when there is no overlap", () => {
|
|
52
|
+
sketch("xy", () => {
|
|
53
|
+
const c1 = circle(40);
|
|
54
|
+
const c2 = circle([200, 0], 40);
|
|
55
|
+
subtract(c1, c2);
|
|
56
|
+
});
|
|
57
|
+
const e = extrude(20);
|
|
58
|
+
render();
|
|
59
|
+
// Base circle remains since tool doesn't overlap
|
|
60
|
+
const shapes = e.getShapes();
|
|
61
|
+
expect(shapes).toHaveLength(1);
|
|
62
|
+
});
|
|
63
|
+
});
|