circuit-json-to-gerber 0.0.82 → 0.0.84

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.
@@ -292,6 +292,8 @@ var ExcellonDrillBuilder = class {
292
292
  var excellonDrill = () => new ExcellonDrillBuilder();
293
293
 
294
294
  // src/excellon-drill/convert-soup-to-excellon-drill-commands.ts
295
+ import polygonClipping from "polygon-clipping";
296
+ import { getBoundFromCenteredRect } from "@tscircuit/math-utils";
295
297
  var getLayerCount = (circuitJson) => {
296
298
  const board = circuitJson.find((element) => element.type === "pcb_board");
297
299
  if (!board || typeof board.num_layers !== "number") {
@@ -426,6 +428,149 @@ var getDrillableElements = (circuitJson) => [
426
428
  ...circuitJson,
427
429
  ...getTraceRouteViaElements(circuitJson)
428
430
  ];
431
+ var circleCutoutPolygonSegmentCount = 64;
432
+ var getBoardOutlinePolygons = (circuitJson) => {
433
+ return circuitJson.flatMap((element) => {
434
+ if (element.type !== "pcb_board") {
435
+ return [];
436
+ }
437
+ if (element.outline && element.outline.length > 2) {
438
+ return [[element.outline.map((point) => [point.x, point.y])]];
439
+ }
440
+ const bounds = getBoundFromCenteredRect({
441
+ center: element.center,
442
+ width: element.width,
443
+ height: element.height
444
+ });
445
+ return [
446
+ [
447
+ [
448
+ [bounds.minX, bounds.minY],
449
+ [bounds.maxX, bounds.minY],
450
+ [bounds.maxX, bounds.maxY],
451
+ [bounds.minX, bounds.maxY]
452
+ ]
453
+ ]
454
+ ];
455
+ });
456
+ };
457
+ var getCircleCutoutPolygon = (cutout) => {
458
+ const points = [];
459
+ for (let i = 0; i < circleCutoutPolygonSegmentCount; i++) {
460
+ const angle = i / circleCutoutPolygonSegmentCount * Math.PI * 2;
461
+ points.push([
462
+ cutout.center.x + cutout.radius * Math.cos(angle),
463
+ cutout.center.y + cutout.radius * Math.sin(angle)
464
+ ]);
465
+ }
466
+ return [points];
467
+ };
468
+ var isInternalCircularCutout = ({
469
+ cutout,
470
+ boardOutlinePolygons
471
+ }) => {
472
+ const cutoutPolygon = getCircleCutoutPolygon(cutout);
473
+ return boardOutlinePolygons.some((boardOutlinePolygon) => {
474
+ const overlapsBoard = polygonClipping.intersection(boardOutlinePolygon, cutoutPolygon).length > 0;
475
+ const extendsOutsideBoard = polygonClipping.difference(cutoutPolygon, boardOutlinePolygon).length > 0;
476
+ return overlapsBoard && !extendsOutsideBoard;
477
+ });
478
+ };
479
+ var getInternalCircularCutoutDrills = (circuitJson) => {
480
+ const boardOutlinePolygons = getBoardOutlinePolygons(circuitJson);
481
+ return circuitJson.filter(
482
+ (element) => {
483
+ if (element.type !== "pcb_cutout") {
484
+ return false;
485
+ }
486
+ if (element.shape !== "circle") {
487
+ return false;
488
+ }
489
+ return isInternalCircularCutout({
490
+ cutout: element,
491
+ boardOutlinePolygons
492
+ });
493
+ }
494
+ );
495
+ };
496
+ var getDrillElements = ({
497
+ circuitJson,
498
+ is_plated
499
+ }) => {
500
+ const drillableElements = getDrillableElements(circuitJson).filter(
501
+ (element) => element.type !== "pcb_cutout"
502
+ );
503
+ if (is_plated) {
504
+ return drillableElements;
505
+ }
506
+ const internalCircularCutoutDrills = getInternalCircularCutoutDrills(circuitJson);
507
+ return [...drillableElements, ...internalCircularCutoutDrills];
508
+ };
509
+ var getHoleDiameter = (element) => {
510
+ if ("hole_diameter" in element && typeof element.hole_diameter === "number") {
511
+ return element.hole_diameter;
512
+ }
513
+ if (element.type === "pcb_cutout") {
514
+ return element.radius * 2;
515
+ }
516
+ if ("hole_width" in element && typeof element.hole_width === "number" && "hole_height" in element && typeof element.hole_height === "number") {
517
+ return Math.min(element.hole_width, element.hole_height);
518
+ }
519
+ return void 0;
520
+ };
521
+ var getDrillCenter = (element) => {
522
+ if (element.type === "pcb_cutout") {
523
+ return element.center;
524
+ }
525
+ let x = 0;
526
+ let y = 0;
527
+ let holeOffsetX = 0;
528
+ let holeOffsetY = 0;
529
+ if ("x" in element && typeof element.x === "number") {
530
+ x = element.x;
531
+ }
532
+ if ("y" in element && typeof element.y === "number") {
533
+ y = element.y;
534
+ }
535
+ if ("hole_offset_x" in element && typeof element.hole_offset_x === "number") {
536
+ holeOffsetX = element.hole_offset_x;
537
+ }
538
+ if ("hole_offset_y" in element && typeof element.hole_offset_y === "number") {
539
+ holeOffsetY = element.hole_offset_y;
540
+ }
541
+ return {
542
+ x: x + holeOffsetX,
543
+ y: y + holeOffsetY
544
+ };
545
+ };
546
+ var getYMultiplier = (flip_y_axis) => {
547
+ if (flip_y_axis) {
548
+ return -1;
549
+ }
550
+ return 1;
551
+ };
552
+ var getSlotEndpoints = ({
553
+ holeWidth,
554
+ holeHeight,
555
+ slotHalfLength
556
+ }) => {
557
+ let startRelative = { x: 0, y: -slotHalfLength };
558
+ let endRelative = { x: 0, y: slotHalfLength };
559
+ if (holeWidth >= holeHeight) {
560
+ startRelative = { x: -slotHalfLength, y: 0 };
561
+ endRelative = { x: slotHalfLength, y: 0 };
562
+ }
563
+ return { startRelative, endRelative };
564
+ };
565
+ var getHoleRotationDegrees = (element) => {
566
+ if ("hole_ccw_rotation" in element && typeof element.hole_ccw_rotation === "number") {
567
+ return element.hole_ccw_rotation;
568
+ }
569
+ if ("ccw_rotation" in element && typeof element.ccw_rotation === "number") {
570
+ return element.ccw_rotation;
571
+ }
572
+ return 0;
573
+ };
429
574
  var convertSoupToExcellonDrillCommands = ({
430
575
  circuitJson,
431
576
  is_plated,
@@ -434,7 +579,10 @@ var convertSoupToExcellonDrillCommands = ({
434
579
  }) => {
435
580
  const builder = excellonDrill();
436
581
  const layerCount = getLayerCount(circuitJson);
437
- const drillableElements = getDrillableElements(circuitJson);
582
+ const drillElements = getDrillElements({
583
+ circuitJson,
584
+ is_plated
585
+ });
438
586
  builder.add("M48", {});
439
587
  const date_str = (/* @__PURE__ */ new Date()).toISOString();
440
588
  builder.add("header_comment", {
@@ -457,8 +605,8 @@ var convertSoupToExcellonDrillCommands = ({
457
605
  }).add("FMAT", { format: 2 }).add("unit_format", { unit: "METRIC", lz: null });
458
606
  let tool_counter = 10;
459
607
  const diameterToToolNumber = {};
460
- for (const element of drillableElements) {
461
- if (!shouldIncludeElement({
608
+ for (const element of drillElements) {
609
+ if (element.type !== "pcb_cutout" && !shouldIncludeElement({
462
610
  element,
463
611
  is_plated,
464
612
  layer_span,
@@ -469,7 +617,7 @@ var convertSoupToExcellonDrillCommands = ({
469
617
  if (is_plated && element.type === "pcb_hole") {
470
618
  continue;
471
619
  }
472
- const holeDiameter = "hole_diameter" in element && typeof element.hole_diameter === "number" ? element.hole_diameter : "hole_width" in element && typeof element.hole_width === "number" && "hole_height" in element && typeof element.hole_height === "number" ? Math.min(element.hole_width, element.hole_height) : void 0;
620
+ const holeDiameter = getHoleDiameter(element);
473
621
  if (!holeDiameter) continue;
474
622
  if (!diameterToToolNumber[holeDiameter]) {
475
623
  builder.add("aper_function_header", {
@@ -488,9 +636,9 @@ var convertSoupToExcellonDrillCommands = ({
488
636
  builder.add("G05", {});
489
637
  for (let i = 10; i < tool_counter; i++) {
490
638
  builder.add("use_tool", { tool_number: i });
491
- for (const element of drillableElements) {
492
- if (element.type === "pcb_plated_hole" || element.type === "pcb_hole" || element.type === "pcb_via") {
493
- if (!shouldIncludeElement({
639
+ for (const element of drillElements) {
640
+ if (element.type === "pcb_plated_hole" || element.type === "pcb_hole" || element.type === "pcb_via" || element.type === "pcb_cutout") {
641
+ if (element.type !== "pcb_cutout" && !shouldIncludeElement({
494
642
  element,
495
643
  is_plated,
496
644
  layer_span,
@@ -501,17 +649,14 @@ var convertSoupToExcellonDrillCommands = ({
501
649
  if (is_plated && element.type === "pcb_hole") {
502
650
  continue;
503
651
  }
504
- const holeDiameter = "hole_diameter" in element && typeof element.hole_diameter === "number" ? element.hole_diameter : "hole_width" in element && typeof element.hole_width === "number" && "hole_height" in element && typeof element.hole_height === "number" ? Math.min(element.hole_width, element.hole_height) : void 0;
652
+ const holeDiameter = getHoleDiameter(element);
505
653
  if (!holeDiameter || diameterToToolNumber[holeDiameter] !== i) {
506
654
  continue;
507
655
  }
508
- const elementX = "x" in element && typeof element.x === "number" ? element.x : 0;
509
- const elementY = "y" in element && typeof element.y === "number" ? element.y : 0;
510
- const offsetX = "hole_offset_x" in element && typeof element.hole_offset_x === "number" ? element.hole_offset_x : 0;
511
- const offsetY = "hole_offset_y" in element && typeof element.hole_offset_y === "number" ? element.hole_offset_y : 0;
512
- const centerX = elementX + offsetX;
513
- const centerY = elementY + offsetY;
514
- const yMultiplier = flip_y_axis ? -1 : 1;
656
+ const drillCenter = getDrillCenter(element);
657
+ const centerX = drillCenter.x;
658
+ const centerY = drillCenter.y;
659
+ const yMultiplier = getYMultiplier(flip_y_axis);
515
660
  if ("hole_width" in element && typeof element.hole_width === "number" && "hole_height" in element && typeof element.hole_height === "number") {
516
661
  const holeWidth = element.hole_width;
517
662
  const holeHeight = element.hole_height;
@@ -524,14 +669,16 @@ var convertSoupToExcellonDrillCommands = ({
524
669
  });
525
670
  continue;
526
671
  }
527
- const rotationDegrees = "hole_ccw_rotation" in element && typeof element.hole_ccw_rotation === "number" ? element.hole_ccw_rotation : "ccw_rotation" in element && typeof element.ccw_rotation === "number" ? element.ccw_rotation : 0;
672
+ const rotationDegrees = getHoleRotationDegrees(element);
528
673
  const rotationRadians = rotationDegrees * Math.PI / 180;
529
674
  const cosTheta = Math.cos(rotationRadians);
530
675
  const sinTheta = Math.sin(rotationRadians);
531
- const isWidthMajor = holeWidth >= holeHeight;
532
676
  const slotHalfLength = (maxDim - minDim) / 2;
533
- const startRelative = isWidthMajor ? { x: -slotHalfLength, y: 0 } : { x: 0, y: -slotHalfLength };
534
- const endRelative = isWidthMajor ? { x: slotHalfLength, y: 0 } : { x: 0, y: slotHalfLength };
677
+ const { startRelative, endRelative } = getSlotEndpoints({
678
+ holeWidth,
679
+ holeHeight,
680
+ slotHalfLength
681
+ });
535
682
  const rotatePoint = ({ x, y }) => ({
536
683
  x: x * cosTheta - y * sinTheta,
537
684
  y: x * sinTheta + y * cosTheta
@@ -600,12 +747,10 @@ var convertSoupToExcellonDrillCommandLayers = ({
600
747
  ])
601
748
  );
602
749
  const hasNonPlatedDrill = circuitJson.some((element) => {
603
- if (element.type !== "pcb_hole") {
604
- return false;
605
- }
606
- return hasDrillGeometry(element);
750
+ return element.type === "pcb_hole" && hasDrillGeometry(element);
607
751
  });
608
- if (!hasNonPlatedDrill) {
752
+ const hasInternalCircularCutoutDrill = getInternalCircularCutoutDrills(circuitJson).length > 0;
753
+ if (!hasNonPlatedDrill && !hasInternalCircularCutoutDrill) {
609
754
  return platedDrillLayers;
610
755
  }
611
756
  return {
@@ -1337,6 +1482,7 @@ import {
1337
1482
  rotate,
1338
1483
  translate
1339
1484
  } from "transformation-matrix";
1485
+ import { getBoundFromCenteredRect as getBoundFromCenteredRect2 } from "@tscircuit/math-utils";
1340
1486
  var SILKSCREEN_SHAPE_TYPES = [
1341
1487
  "pcb_silkscreen_line",
1342
1488
  "pcb_silkscreen_rect",
@@ -1368,11 +1514,12 @@ var rectangleRoute = (center, width, height, cornerRadius, ccwDegrees = 0) => {
1368
1514
  const halfH = height / 2;
1369
1515
  const route = [];
1370
1516
  if (cornerRadius <= 0) {
1517
+ const bounds = getBoundFromCenteredRect2({ center, width, height });
1371
1518
  route.push(
1372
- { x: center.x - halfW, y: center.y - halfH },
1373
- { x: center.x + halfW, y: center.y - halfH },
1374
- { x: center.x + halfW, y: center.y + halfH },
1375
- { x: center.x - halfW, y: center.y + halfH }
1519
+ { x: bounds.minX, y: bounds.minY },
1520
+ { x: bounds.maxX, y: bounds.minY },
1521
+ { x: bounds.maxX, y: bounds.maxY },
1522
+ { x: bounds.minX, y: bounds.maxY }
1376
1523
  );
1377
1524
  } else {
1378
1525
  const insetW = halfW - cornerRadius;
@@ -1481,23 +1628,19 @@ function getAllTraceWidths(soup) {
1481
1628
  }
1482
1629
  }
1483
1630
  }
1484
- return {
1485
- top: Array.from(widths.top || []),
1486
- inner1: Array.from(widths.inner1 || []),
1487
- inner2: Array.from(widths.inner2 || []),
1488
- inner3: Array.from(widths.inner3 || []),
1489
- inner4: Array.from(widths.inner4 || []),
1490
- inner5: Array.from(widths.inner5 || []),
1491
- inner6: Array.from(widths.inner6 || []),
1492
- bottom: Array.from(widths.bottom || [])
1493
- };
1631
+ return Object.fromEntries(
1632
+ Object.entries(widths).map(([layer, layerWidths]) => [
1633
+ layer,
1634
+ Array.from(layerWidths)
1635
+ ])
1636
+ );
1494
1637
  }
1495
1638
 
1496
1639
  // src/gerber/convert-soup-to-gerber-commands/defineAperturesForLayer.ts
1497
1640
  var getLayerRefFromGerberLayerName = (glayer_name) => {
1498
1641
  if (glayer_name.startsWith("F_")) return "top";
1499
1642
  if (glayer_name.startsWith("B_")) return "bottom";
1500
- const innerLayerMatch = glayer_name.match(/^In([1-6])_/);
1643
+ const innerLayerMatch = glayer_name.match(/^In(\d+)_/);
1501
1644
  if (innerLayerMatch) return `inner${innerLayerMatch[1]}`;
1502
1645
  throw new Error(`Could not infer layer ref from ${glayer_name}`);
1503
1646
  };
@@ -1543,7 +1686,7 @@ function defineAperturesForLayer({
1543
1686
  );
1544
1687
  const traceWidths = getAllTraceWidths(circuitJson);
1545
1688
  const layerRef = getLayerRefFromGerberLayerName(glayer_name);
1546
- for (const width of traceWidths[layerRef]) {
1689
+ for (const width of traceWidths[layerRef] ?? []) {
1547
1690
  glayer.push(
1548
1691
  ...gerberBuilder().add("define_aperture_template", {
1549
1692
  aperture_number: getNextApertureNumber(),
@@ -2095,7 +2238,7 @@ var findApertureNumber = (glayer, search_params) => {
2095
2238
  // package.json
2096
2239
  var package_default = {
2097
2240
  name: "circuit-json-to-gerber",
2098
- version: "0.0.81",
2241
+ version: "0.0.83",
2099
2242
  main: "dist/index.js",
2100
2243
  type: "module",
2101
2244
  scripts: {
@@ -2120,8 +2263,9 @@ var package_default = {
2120
2263
  "@types/react-dom": "^19.1.5",
2121
2264
  archiver: "^7.0.1",
2122
2265
  "bun-match-svg": "^0.0.13",
2123
- "circuit-json": "^0.0.426",
2266
+ "circuit-json": "^0.0.450",
2124
2267
  commander: "^12.1.0",
2268
+ "format-si-unit": "^0.0.7",
2125
2269
  "gerber-to-svg": "^4.2.8",
2126
2270
  gerberts: "^0.0.3",
2127
2271
  jszip: "^3.10.1",
@@ -2139,6 +2283,7 @@ var package_default = {
2139
2283
  dependencies: {
2140
2284
  "@tscircuit/alphabet": "^0.0.25",
2141
2285
  "fast-json-stable-stringify": "^2.1.0",
2286
+ "polygon-clipping": "^0.15.7",
2142
2287
  "transformation-matrix": "^3.0.0"
2143
2288
  }
2144
2289
  };
@@ -2201,15 +2346,9 @@ var getCommandHeaders = (opts) => {
2201
2346
  };
2202
2347
 
2203
2348
  // src/gerber/convert-soup-to-gerber-commands/getGerberLayerName.ts
2204
- var layerRefToGerberPrefix = {
2349
+ var outerLayerRefToGerberPrefix = {
2205
2350
  top: "F_",
2206
- bottom: "B_",
2207
- inner1: "In1_",
2208
- inner2: "In2_",
2209
- inner3: "In3_",
2210
- inner4: "In4_",
2211
- inner5: "In5_",
2212
- inner6: "In6_"
2351
+ bottom: "B_"
2213
2352
  };
2214
2353
  var layerTypeToGerberSuffix = {
2215
2354
  copper: "Cu",
@@ -2224,7 +2363,8 @@ var getGerberLayerName = (layer_ref, layer_type) => {
2224
2363
  if (layer_ref.startsWith("inner") && layer_type !== "copper") {
2225
2364
  throw new Error(`Inner layer ${layer_ref} only supports copper gerbers`);
2226
2365
  }
2227
- return `${layerRefToGerberPrefix[layer_ref]}${layerTypeToGerberSuffix[layer_type]}`;
2366
+ const prefix = layer_ref.startsWith("inner") ? `In${layer_ref.slice("inner".length)}_` : outerLayerRefToGerberPrefix[layer_ref];
2367
+ return `${prefix}${layerTypeToGerberSuffix[layer_type]}`;
2228
2368
  };
2229
2369
 
2230
2370
  // src/gerber/convert-soup-to-gerber-commands/offsetPolygonOutline.ts
@@ -2306,11 +2446,11 @@ var offsetPolygonOutline = (points, offset) => {
2306
2446
  // src/gerber/convert-soup-to-gerber-commands/index.ts
2307
2447
  import { lineAlphabet } from "@tscircuit/alphabet";
2308
2448
  import {
2309
- applyToPoint as applyToPoint2,
2310
- compose as compose2,
2311
- identity as identity2,
2312
- rotate as rotate2,
2313
- translate as translate2
2449
+ applyToPoint as applyToPoint4,
2450
+ compose as compose4,
2451
+ identity as identity3,
2452
+ rotate as rotate4,
2453
+ translate as translate4
2314
2454
  } from "transformation-matrix";
2315
2455
 
2316
2456
  // src/gerber/convert-soup-to-gerber-commands/fabricationLayerRefs.ts
@@ -2324,15 +2464,18 @@ var getFabricationLayerRefs = (circuitJson) => outerLayerRefs.filter(
2324
2464
  );
2325
2465
 
2326
2466
  // src/gerber/convert-soup-to-gerber-commands/getFabRectPoints.ts
2467
+ import { getBoundFromCenteredRect as getBoundFromCenteredRect3 } from "@tscircuit/math-utils";
2327
2468
  var getFabRectPoints = (element) => {
2328
- const halfWidth = element.width / 2;
2329
- const halfHeight = element.height / 2;
2330
- const { x, y } = element.center;
2469
+ const bounds = getBoundFromCenteredRect3({
2470
+ center: element.center,
2471
+ width: element.width,
2472
+ height: element.height
2473
+ });
2331
2474
  return [
2332
- { x: x - halfWidth, y: y - halfHeight },
2333
- { x: x + halfWidth, y: y - halfHeight },
2334
- { x: x + halfWidth, y: y + halfHeight },
2335
- { x: x - halfWidth, y: y + halfHeight }
2475
+ { x: bounds.minX, y: bounds.minY },
2476
+ { x: bounds.maxX, y: bounds.minY },
2477
+ { x: bounds.maxX, y: bounds.maxY },
2478
+ { x: bounds.minX, y: bounds.maxY }
2336
2479
  ];
2337
2480
  };
2338
2481
 
@@ -2482,10 +2625,357 @@ var renderFabricationDimension = ({
2482
2625
  };
2483
2626
 
2484
2627
  // src/gerber/convert-soup-to-gerber-commands/index.ts
2628
+ import polygonClipping3 from "polygon-clipping";
2629
+
2630
+ // src/gerber/utils/boardCutoutGeometry.ts
2631
+ import polygonClipping2 from "polygon-clipping";
2632
+ import { applyToPoint as applyToPoint2, compose as compose2, rotate as rotate2, translate as translate2 } from "transformation-matrix";
2633
+ import { getBoundFromCenteredRect as getBoundFromCenteredRect4 } from "@tscircuit/math-utils";
2634
+ var roundedRectCornerArcSegmentCount = 8;
2635
+ var circleCutoutPolygonSegmentCount2 = 64;
2636
+ var getBoardOutlinePolygon = (board) => {
2637
+ if (board.outline && board.outline.length > 2) {
2638
+ return [board.outline.map((point) => [point.x, point.y])];
2639
+ }
2640
+ const bounds = getBoundFromCenteredRect4({
2641
+ center: board.center,
2642
+ width: board.width,
2643
+ height: board.height
2644
+ });
2645
+ return [
2646
+ [
2647
+ [bounds.minX, bounds.minY],
2648
+ [bounds.maxX, bounds.minY],
2649
+ [bounds.maxX, bounds.maxY],
2650
+ [bounds.minX, bounds.maxY]
2651
+ ]
2652
+ ];
2653
+ };
2654
+ var getBoardOutlinePolygons2 = (circuitJson) => circuitJson.filter((element) => element.type === "pcb_board").map(getBoardOutlinePolygon);
2655
+ var getRectCutoutOutlinePoints = (cutout) => {
2656
+ const { center, width, height, rotation, corner_radius } = cutout;
2657
+ const w = width / 2;
2658
+ const h = height / 2;
2659
+ const r = Math.max(0, Math.min(corner_radius ?? 0, Math.abs(w), Math.abs(h)));
2660
+ let transformMatrix = translate2(center.x, center.y);
2661
+ if (rotation) {
2662
+ transformMatrix = compose2(
2663
+ transformMatrix,
2664
+ rotate2(rotation * Math.PI / 180)
2665
+ );
2666
+ }
2667
+ if (r === 0) {
2668
+ return [
2669
+ { x: -w, y: h },
2670
+ { x: w, y: h },
2671
+ { x: w, y: -h },
2672
+ { x: -w, y: -h }
2673
+ ].map((point) => applyToPoint2(transformMatrix, point));
2674
+ }
2675
+ const points = [];
2676
+ const corners = [
2677
+ { center: { x: w - r, y: h - r }, start: Math.PI / 2, end: 0 },
2678
+ { center: { x: w - r, y: -h + r }, start: 0, end: -Math.PI / 2 },
2679
+ { center: { x: -w + r, y: -h + r }, start: -Math.PI / 2, end: -Math.PI },
2680
+ { center: { x: -w + r, y: h - r }, start: Math.PI, end: Math.PI / 2 }
2681
+ ];
2682
+ for (const corner of corners) {
2683
+ for (let i = 0; i <= roundedRectCornerArcSegmentCount; i++) {
2684
+ const t = i / roundedRectCornerArcSegmentCount;
2685
+ const angle = corner.start + (corner.end - corner.start) * t;
2686
+ points.push({
2687
+ x: corner.center.x + r * Math.cos(angle),
2688
+ y: corner.center.y + r * Math.sin(angle)
2689
+ });
2690
+ }
2691
+ }
2692
+ return points.map((point) => applyToPoint2(transformMatrix, point));
2693
+ };
2694
+ var getCircleCutoutOutlinePolygon = (cutout) => {
2695
+ const points = [];
2696
+ for (let i = 0; i < circleCutoutPolygonSegmentCount2; i++) {
2697
+ const angle = i / circleCutoutPolygonSegmentCount2 * Math.PI * 2;
2698
+ points.push([
2699
+ cutout.center.x + cutout.radius * Math.cos(angle),
2700
+ cutout.center.y + cutout.radius * Math.sin(angle)
2701
+ ]);
2702
+ }
2703
+ return [points];
2704
+ };
2705
+ var getSolidCutoutOutlinePolygon = (cutout) => {
2706
+ if (cutout.shape === "rect") {
2707
+ return [
2708
+ getRectCutoutOutlinePoints(cutout).map((point) => [point.x, point.y])
2709
+ ];
2710
+ }
2711
+ if (cutout.shape === "circle") {
2712
+ return getCircleCutoutOutlinePolygon(cutout);
2713
+ }
2714
+ if (cutout.shape === "polygon") {
2715
+ return [cutout.points.map((point) => [point.x, point.y])];
2716
+ }
2717
+ return null;
2718
+ };
2719
+ var doesSolidCutoutOverlapBoardEdge = ({
2720
+ cutout,
2721
+ boardOutlinePolygons
2722
+ }) => {
2723
+ const cutoutOutlinePolygon = getSolidCutoutOutlinePolygon(cutout);
2724
+ if (!cutoutOutlinePolygon) return false;
2725
+ return boardOutlinePolygons.some((boardOutlinePolygon) => {
2726
+ const overlapsBoard = polygonClipping2.intersection(boardOutlinePolygon, cutoutOutlinePolygon).length > 0;
2727
+ const extendsOutsideBoard = polygonClipping2.difference(cutoutOutlinePolygon, boardOutlinePolygon).length > 0;
2728
+ return overlapsBoard && extendsOutsideBoard;
2729
+ });
2730
+ };
2731
+ var isCutoutFullyInternal = ({
2732
+ cutout,
2733
+ boardOutlinePolygons
2734
+ }) => {
2735
+ const cutoutOutlinePolygon = getSolidCutoutOutlinePolygon(cutout);
2736
+ if (!cutoutOutlinePolygon) return false;
2737
+ return boardOutlinePolygons.some(
2738
+ (boardOutlinePolygon) => (
2739
+ // Has overlap with the board interior …
2740
+ polygonClipping2.intersection(boardOutlinePolygon, cutoutOutlinePolygon).length > 0 && // … but does not extend outside it at all.
2741
+ polygonClipping2.difference(cutoutOutlinePolygon, boardOutlinePolygon).length === 0
2742
+ )
2743
+ );
2744
+ };
2745
+
2746
+ // src/gerber/utils/emitCutoutEdgeCuts.ts
2747
+ import {
2748
+ applyToPoint as applyToPoint3,
2749
+ compose as compose3,
2750
+ identity as identity2,
2751
+ rotate as rotate3,
2752
+ translate as translate3
2753
+ } from "transformation-matrix";
2754
+ var emitCutoutEdgeCuts = ({
2755
+ cutout,
2756
+ builder,
2757
+ mfy,
2758
+ drawCw
2759
+ }) => {
2760
+ if (cutout.shape === "circle") {
2761
+ emitCircle({ cutout, builder, mfy, drawCw });
2762
+ } else if (cutout.shape === "rect") {
2763
+ emitRect({ cutout, builder, mfy, drawCw });
2764
+ } else if (cutout.shape === "polygon") {
2765
+ emitPolygon({ cutout, builder, mfy, drawCw });
2766
+ }
2767
+ };
2768
+ var emitCircle = ({
2769
+ cutout,
2770
+ builder,
2771
+ mfy,
2772
+ drawCw
2773
+ }) => {
2774
+ const { center, radius } = cutout;
2775
+ const p1 = { x: center.x + radius, y: center.y };
2776
+ const p2 = { x: center.x - radius, y: center.y };
2777
+ const isYFlipped = mfy(1) < 0;
2778
+ let isClockwise = drawCw;
2779
+ if (isYFlipped) {
2780
+ isClockwise = !isClockwise;
2781
+ }
2782
+ let arcMode = "set_movement_mode_to_counterclockwise_circular";
2783
+ if (isClockwise) {
2784
+ arcMode = "set_movement_mode_to_clockwise_circular";
2785
+ }
2786
+ builder.add("move_operation", { x: p1.x, y: mfy(p1.y) }).add(arcMode, {}).add("plot_operation", { x: p2.x, y: mfy(p2.y), i: -radius, j: 0 }).add("plot_operation", { x: p1.x, y: mfy(p1.y), i: radius, j: 0 }).add("set_movement_mode_to_linear", {});
2787
+ };
2788
+ var emitRect = ({
2789
+ cutout,
2790
+ builder,
2791
+ mfy,
2792
+ drawCw
2793
+ }) => {
2794
+ const { center, width, height, rotation, corner_radius } = cutout;
2795
+ const w = width / 2;
2796
+ const h = height / 2;
2797
+ let cr = 0;
2798
+ if (corner_radius !== void 0) {
2799
+ cr = corner_radius;
2800
+ }
2801
+ const r = Math.max(0, Math.min(cr, Math.abs(w), Math.abs(h)));
2802
+ let rotationTransform = identity2();
2803
+ if (rotation) {
2804
+ rotationTransform = rotate3(rotation * Math.PI / 180);
2805
+ }
2806
+ const transformMatrix = compose3(
2807
+ translate3(center.x, center.y),
2808
+ rotationTransform
2809
+ );
2810
+ if (r > 0) {
2811
+ emitRoundedRect({ w, h, r, transformMatrix, builder, mfy, drawCw });
2812
+ } else {
2813
+ emitSharpRect({ w, h, transformMatrix, builder, mfy, drawCw });
2814
+ }
2815
+ };
2816
+ var emitSharpRect = ({
2817
+ w,
2818
+ h,
2819
+ transformMatrix,
2820
+ builder,
2821
+ mfy,
2822
+ drawCw
2823
+ }) => {
2824
+ const cwPoints = [
2825
+ { x: -w, y: h },
2826
+ { x: w, y: h },
2827
+ { x: w, y: -h },
2828
+ { x: -w, y: -h }
2829
+ ];
2830
+ const isYFlipped = mfy(1) < 0;
2831
+ let shouldReverse = !drawCw;
2832
+ if (isYFlipped) {
2833
+ shouldReverse = !shouldReverse;
2834
+ }
2835
+ let pts = cwPoints;
2836
+ if (shouldReverse) {
2837
+ pts = [...cwPoints].reverse();
2838
+ }
2839
+ const tpts = pts.map((p) => applyToPoint3(transformMatrix, p));
2840
+ builder.add("move_operation", { x: tpts[0].x, y: mfy(tpts[0].y) });
2841
+ for (let i = 1; i < tpts.length; i++) {
2842
+ builder.add("plot_operation", { x: tpts[i].x, y: mfy(tpts[i].y) });
2843
+ }
2844
+ builder.add("plot_operation", { x: tpts[0].x, y: mfy(tpts[0].y) });
2845
+ };
2846
+ var emitRoundedRect = ({
2847
+ w,
2848
+ h,
2849
+ r,
2850
+ transformMatrix,
2851
+ builder,
2852
+ mfy,
2853
+ drawCw
2854
+ }) => {
2855
+ const isYFlipped = mfy(1) < 0;
2856
+ let isClockwise = drawCw;
2857
+ if (isYFlipped) {
2858
+ isClockwise = !isClockwise;
2859
+ }
2860
+ let arcMode = "set_movement_mode_to_counterclockwise_circular";
2861
+ if (isClockwise) {
2862
+ arcMode = "set_movement_mode_to_clockwise_circular";
2863
+ }
2864
+ let currentPoint;
2865
+ const addLine = (pt) => {
2866
+ const tpt = applyToPoint3(transformMatrix, pt);
2867
+ builder.add("plot_operation", { x: tpt.x, y: mfy(tpt.y) });
2868
+ currentPoint = tpt;
2869
+ };
2870
+ const addArc = (endPt, centerPt) => {
2871
+ const tEnd = applyToPoint3(transformMatrix, endPt);
2872
+ const tCenter = applyToPoint3(transformMatrix, centerPt);
2873
+ builder.add(arcMode, {}).add("plot_operation", {
2874
+ x: tEnd.x,
2875
+ y: mfy(tEnd.y),
2876
+ i: tCenter.x - currentPoint.x,
2877
+ j: mfy(tCenter.y) - mfy(currentPoint.y)
2878
+ }).add("set_movement_mode_to_linear", {});
2879
+ currentPoint = tEnd;
2880
+ };
2881
+ if (isClockwise) {
2882
+ const cwSegments = [
2883
+ {
2884
+ lineTo: { x: w - r, y: h },
2885
+ arcEnd: { x: w, y: h - r },
2886
+ arcCenter: { x: w - r, y: h - r }
2887
+ },
2888
+ {
2889
+ lineTo: { x: w, y: -h + r },
2890
+ arcEnd: { x: w - r, y: -h },
2891
+ arcCenter: { x: w - r, y: -h + r }
2892
+ },
2893
+ {
2894
+ lineTo: { x: -w + r, y: -h },
2895
+ arcEnd: { x: -w, y: -h + r },
2896
+ arcCenter: { x: -w + r, y: -h + r }
2897
+ },
2898
+ {
2899
+ lineTo: { x: -w, y: h - r },
2900
+ arcEnd: { x: -w + r, y: h },
2901
+ arcCenter: { x: -w + r, y: h - r }
2902
+ }
2903
+ ];
2904
+ const startPt = { x: -w + r, y: h };
2905
+ const tStart = applyToPoint3(transformMatrix, startPt);
2906
+ builder.add("move_operation", { x: tStart.x, y: mfy(tStart.y) });
2907
+ currentPoint = tStart;
2908
+ for (const seg of cwSegments) {
2909
+ addLine(seg.lineTo);
2910
+ addArc(seg.arcEnd, seg.arcCenter);
2911
+ }
2912
+ } else {
2913
+ const startPt = { x: w - r, y: h };
2914
+ const tStart = applyToPoint3(transformMatrix, startPt);
2915
+ builder.add("move_operation", { x: tStart.x, y: mfy(tStart.y) });
2916
+ currentPoint = tStart;
2917
+ addLine({ x: -w + r, y: h });
2918
+ addArc({ x: -w, y: h - r }, { x: -w + r, y: h - r });
2919
+ addLine({ x: -w, y: -h + r });
2920
+ addArc({ x: -w + r, y: -h }, { x: -w + r, y: -h + r });
2921
+ addLine({ x: w - r, y: -h });
2922
+ addArc({ x: w, y: -h + r }, { x: w - r, y: -h + r });
2923
+ addLine({ x: w, y: h - r });
2924
+ addArc({ x: w - r, y: h }, { x: w - r, y: h - r });
2925
+ }
2926
+ };
2927
+ var emitPolygon = ({
2928
+ cutout,
2929
+ builder,
2930
+ mfy,
2931
+ drawCw
2932
+ }) => {
2933
+ const points = cutout.points;
2934
+ if (points.length === 0) return;
2935
+ const isYFlipped = mfy(1) < 0;
2936
+ let shouldReverse = !drawCw;
2937
+ if (isYFlipped) {
2938
+ shouldReverse = !shouldReverse;
2939
+ }
2940
+ let ordered = points;
2941
+ if (shouldReverse) {
2942
+ ordered = [...points].reverse();
2943
+ }
2944
+ builder.add("move_operation", { x: ordered[0].x, y: mfy(ordered[0].y) });
2945
+ for (let i = 1; i < ordered.length; i++) {
2946
+ builder.add("plot_operation", { x: ordered[i].x, y: mfy(ordered[i].y) });
2947
+ }
2948
+ builder.add("plot_operation", { x: ordered[0].x, y: mfy(ordered[0].y) });
2949
+ };
2950
+
2951
+ // src/gerber/convert-soup-to-gerber-commands/index.ts
2952
+ var closeRing = (ring) => {
2953
+ const first = ring[0];
2954
+ const last = ring[ring.length - 1];
2955
+ if (!first || !last) return ring;
2956
+ if (first[0] === last[0] && first[1] === last[1]) return ring;
2957
+ return [...ring, first];
2958
+ };
2959
+ var emitEdgeCutRing = (gerberBuild, ring, mfy) => {
2960
+ const closedRing = closeRing(ring);
2961
+ const firstPoint = closedRing[0];
2962
+ if (!firstPoint || closedRing.length < 4) return;
2963
+ gerberBuild.add("move_operation", {
2964
+ x: firstPoint[0],
2965
+ y: mfy(firstPoint[1])
2966
+ });
2967
+ for (let i = 1; i < closedRing.length; i++) {
2968
+ const point = closedRing[i];
2969
+ gerberBuild.add("plot_operation", {
2970
+ x: point[0],
2971
+ y: mfy(point[1])
2972
+ });
2973
+ }
2974
+ };
2485
2975
  var getLayerCount2 = (circuitJson) => {
2486
2976
  const board = circuitJson.find((element) => element.type === "pcb_board");
2487
2977
  const numLayers = board?.num_layers ?? 2;
2488
- return Math.max(2, Math.min(8, numLayers));
2978
+ return Math.max(2, Math.min(10, numLayers));
2489
2979
  };
2490
2980
  var getInnerLayerRefs = (layerCount) => Array.from({ length: layerCount - 2 }, (_, i) => `inner${i + 1}`);
2491
2981
  var getGerberInnerLayerName = (layerRef) => {
@@ -2497,6 +2987,7 @@ var getGerberInnerLayerName = (layerRef) => {
2497
2987
  var convertSoupToGerberCommands = (circuitJson, opts = {}) => {
2498
2988
  opts.flip_y_axis ??= false;
2499
2989
  const hasPanel = circuitJson.some((e) => e.type === "pcb_panel");
2990
+ const hasBoard = circuitJson.some((e) => e.type === "pcb_board");
2500
2991
  const layerCount = getLayerCount2(circuitJson);
2501
2992
  const innerLayerRefs = getInnerLayerRefs(layerCount);
2502
2993
  const copperLayerRefs = ["top", ...innerLayerRefs, "bottom"];
@@ -2586,7 +3077,16 @@ var convertSoupToGerberCommands = (circuitJson, opts = {}) => {
2586
3077
  //mm
2587
3078
  }).build()
2588
3079
  );
2589
- const mfy = (y) => opts.flip_y_axis ? -y : y;
3080
+ const mfy = (y) => {
3081
+ if (opts.flip_y_axis) {
3082
+ return -y;
3083
+ }
3084
+ return y;
3085
+ };
3086
+ const boardOutlinePolygons = getBoardOutlinePolygons2(circuitJson);
3087
+ const solidCutoutPolygons = circuitJson.filter((element) => element.type === "pcb_cutout").filter(
3088
+ (cutout) => doesSolidCutoutOverlapBoardEdge({ cutout, boardOutlinePolygons })
3089
+ ).map(getSolidCutoutOutlinePolygon).filter((polygon) => polygon !== null);
2590
3090
  const renderPillFlash = ({
2591
3091
  glayer,
2592
3092
  x,
@@ -2739,18 +3239,18 @@ var convertSoupToGerberCommands = (circuitJson, opts = {}) => {
2739
3239
  const shouldMirror = element.is_mirrored !== void 0 ? element.is_mirrored : element.layer === "bottom";
2740
3240
  if (shouldMirror) {
2741
3241
  transforms.push(
2742
- translate2(cx, cy),
3242
+ translate4(cx, cy),
2743
3243
  { a: -1, b: 0, c: 0, d: 1, e: 0, f: 0 },
2744
- translate2(-cx, -cy)
3244
+ translate4(-cx, -cy)
2745
3245
  );
2746
3246
  rotation = -rotation;
2747
3247
  }
2748
3248
  if (rotation) {
2749
3249
  const rad = rotation * Math.PI / 180;
2750
- transforms.push(translate2(cx, cy), rotate2(rad), translate2(-cx, -cy));
3250
+ transforms.push(translate4(cx, cy), rotate4(rad), translate4(-cx, -cy));
2751
3251
  }
2752
- const transformMatrix = transforms.length > 0 ? compose2(...transforms) : void 0;
2753
- const applyTransform = (point) => transformMatrix ? applyToPoint2(transformMatrix, point) : point;
3252
+ const transformMatrix = transforms.length > 0 ? compose4(...transforms) : void 0;
3253
+ const applyTransform = (point) => transformMatrix ? applyToPoint4(transformMatrix, point) : point;
2754
3254
  if (element.is_knockout) {
2755
3255
  const padding = element.knockout_padding ?? {
2756
3256
  left: 0.2,
@@ -2939,17 +3439,17 @@ var convertSoupToGerberCommands = (circuitJson, opts = {}) => {
2939
3439
  { x: -w, y: -h }
2940
3440
  // Bottom-left
2941
3441
  ];
2942
- let transformMatrix = identity2();
3442
+ let transformMatrix = identity3();
2943
3443
  if (rotation) {
2944
3444
  const angle_rad = rotation * Math.PI / 180;
2945
- transformMatrix = rotate2(angle_rad);
3445
+ transformMatrix = rotate4(angle_rad);
2946
3446
  }
2947
- transformMatrix = compose2(
2948
- translate2(center.x, center.y),
3447
+ transformMatrix = compose4(
3448
+ translate4(center.x, center.y),
2949
3449
  transformMatrix
2950
3450
  );
2951
3451
  const transformedPoints = points.map(
2952
- (p) => applyToPoint2(transformMatrix, p)
3452
+ (p) => applyToPoint4(transformMatrix, p)
2953
3453
  );
2954
3454
  const regionApertureNumber = getRegionApertureNumber(copper_glayer);
2955
3455
  const rect_builder = gerberBuilder().add("select_aperture", { aperture_number: regionApertureNumber }).add("start_region_statement", {});
@@ -3505,36 +4005,42 @@ var convertSoupToGerberCommands = (circuitJson, opts = {}) => {
3505
4005
  aperture_number: 10
3506
4006
  });
3507
4007
  if (outline && outline.length > 2) {
3508
- const outlinePoints = outline.map((point) => ({
3509
- x: point.x,
3510
- y: mfy(point.y)
3511
- }));
3512
- const firstPoint = outlinePoints[0];
3513
- gerberBuild.add("move_operation", firstPoint);
3514
- for (let i = 1; i < outlinePoints.length; i++) {
3515
- gerberBuild.add("plot_operation", outlinePoints[i]);
4008
+ const boardPolygon = [
4009
+ outline.map((point) => [point.x, point.y])
4010
+ ];
4011
+ let boardGeometry = [boardPolygon];
4012
+ if (solidCutoutPolygons.length > 0) {
4013
+ boardGeometry = polygonClipping3.difference(
4014
+ boardPolygon,
4015
+ ...solidCutoutPolygons
4016
+ );
3516
4017
  }
3517
- const lastPoint = outlinePoints[outlinePoints.length - 1];
3518
- if (lastPoint.x !== firstPoint.x || lastPoint.y !== firstPoint.y) {
3519
- gerberBuild.add("plot_operation", firstPoint);
4018
+ for (const polygon of boardGeometry) {
4019
+ for (const ring of polygon) {
4020
+ emitEdgeCutRing(gerberBuild, ring, mfy);
4021
+ }
3520
4022
  }
3521
4023
  } else {
3522
- gerberBuild.add("move_operation", {
3523
- x: center.x - width / 2,
3524
- y: mfy(center.y - height / 2)
3525
- }).add("plot_operation", {
3526
- x: center.x + width / 2,
3527
- y: mfy(center.y - height / 2)
3528
- }).add("plot_operation", {
3529
- x: center.x + width / 2,
3530
- y: mfy(center.y + height / 2)
3531
- }).add("plot_operation", {
3532
- x: center.x - width / 2,
3533
- y: mfy(center.y + height / 2)
3534
- }).add("plot_operation", {
3535
- x: center.x - width / 2,
3536
- y: mfy(center.y - height / 2)
3537
- });
4024
+ const boardPolygon = [
4025
+ [
4026
+ [center.x - width / 2, center.y - height / 2],
4027
+ [center.x + width / 2, center.y - height / 2],
4028
+ [center.x + width / 2, center.y + height / 2],
4029
+ [center.x - width / 2, center.y + height / 2]
4030
+ ]
4031
+ ];
4032
+ let boardGeometry = [boardPolygon];
4033
+ if (solidCutoutPolygons.length > 0) {
4034
+ boardGeometry = polygonClipping3.difference(
4035
+ boardPolygon,
4036
+ ...solidCutoutPolygons
4037
+ );
4038
+ }
4039
+ for (const polygon of boardGeometry) {
4040
+ for (const ring of polygon) {
4041
+ emitEdgeCutRing(gerberBuild, ring, mfy);
4042
+ }
4043
+ }
3538
4044
  }
3539
4045
  glayer.push(...gerberBuild.build());
3540
4046
  } else if (element.type === "pcb_panel" && layer === "edgecut") {
@@ -3562,146 +4068,26 @@ var convertSoupToGerberCommands = (circuitJson, opts = {}) => {
3562
4068
  glayer.push(...gerberBuild.build());
3563
4069
  } else if (element.type === "pcb_cutout") {
3564
4070
  if (layer === "edgecut") {
4071
+ if (hasBoard && !hasPanel && doesSolidCutoutOverlapBoardEdge({
4072
+ cutout: element,
4073
+ boardOutlinePolygons
4074
+ })) {
4075
+ continue;
4076
+ }
3565
4077
  const ec_layer = glayers.Edge_Cuts;
3566
4078
  const cutout_builder = gerberBuilder().add("select_aperture", {
3567
4079
  aperture_number: 10
3568
4080
  });
3569
- const el = element;
3570
- if (el.shape === "rect") {
3571
- const { center, width, height, rotation, corner_radius } = el;
3572
- const w = width / 2;
3573
- const h = height / 2;
3574
- const r = Math.max(
3575
- 0,
3576
- Math.min(corner_radius ?? 0, Math.abs(w), Math.abs(h))
3577
- );
3578
- const makeTransformMatrix = () => {
3579
- let transformMatrix2 = identity2();
3580
- if (rotation) {
3581
- const angle_rad = rotation * Math.PI / 180;
3582
- transformMatrix2 = rotate2(angle_rad);
3583
- }
3584
- return compose2(translate2(center.x, center.y), transformMatrix2);
3585
- };
3586
- const transformMatrix = makeTransformMatrix();
3587
- if (r > 0) {
3588
- const startPoint = { x: -w + r, y: h };
3589
- let currentPoint = applyToPoint2(transformMatrix, startPoint);
3590
- cutout_builder.add("move_operation", {
3591
- x: currentPoint.x,
3592
- y: mfy(currentPoint.y)
3593
- });
3594
- const addLine = (point) => {
3595
- const transformedPoint = applyToPoint2(transformMatrix, point);
3596
- cutout_builder.add("plot_operation", {
3597
- x: transformedPoint.x,
3598
- y: mfy(transformedPoint.y)
3599
- });
3600
- currentPoint = transformedPoint;
3601
- };
3602
- const addArc = (options) => {
3603
- const transformedPoint = applyToPoint2(
3604
- transformMatrix,
3605
- options.point
3606
- );
3607
- const transformedCenter = applyToPoint2(
3608
- transformMatrix,
3609
- options.center
3610
- );
3611
- cutout_builder.add("set_movement_mode_to_clockwise_circular", {}).add("plot_operation", {
3612
- x: transformedPoint.x,
3613
- y: mfy(transformedPoint.y),
3614
- i: transformedCenter.x - currentPoint.x,
3615
- j: mfy(transformedCenter.y) - mfy(currentPoint.y)
3616
- }).add("set_movement_mode_to_linear", {});
3617
- currentPoint = transformedPoint;
3618
- };
3619
- addLine({ x: w - r, y: h });
3620
- addArc({
3621
- point: { x: w, y: h - r },
3622
- center: { x: w - r, y: h - r }
3623
- });
3624
- addLine({ x: w, y: -h + r });
3625
- addArc({
3626
- point: { x: w - r, y: -h },
3627
- center: { x: w - r, y: -h + r }
3628
- });
3629
- addLine({ x: -w + r, y: -h });
3630
- addArc({
3631
- point: { x: -w, y: -h + r },
3632
- center: { x: -w + r, y: -h + r }
3633
- });
3634
- addLine({ x: -w, y: h - r });
3635
- addArc({
3636
- point: { x: -w + r, y: h },
3637
- center: { x: -w + r, y: h - r }
3638
- });
3639
- } else {
3640
- const points = [
3641
- { x: -w, y: h },
3642
- // Top-left
3643
- { x: w, y: h },
3644
- // Top-right
3645
- { x: w, y: -h },
3646
- // Bottom-right
3647
- { x: -w, y: -h }
3648
- // Bottom-left
3649
- ];
3650
- const transformedPoints = points.map(
3651
- (p) => applyToPoint2(transformMatrix, p)
3652
- );
3653
- cutout_builder.add("move_operation", {
3654
- x: transformedPoints[0].x,
3655
- y: mfy(transformedPoints[0].y)
3656
- });
3657
- for (let i = 1; i < transformedPoints.length; i++) {
3658
- cutout_builder.add("plot_operation", {
3659
- x: transformedPoints[i].x,
3660
- y: mfy(transformedPoints[i].y)
3661
- });
3662
- }
3663
- cutout_builder.add("plot_operation", {
3664
- x: transformedPoints[0].x,
3665
- y: mfy(transformedPoints[0].y)
3666
- });
3667
- }
3668
- } else if (el.shape === "circle") {
3669
- const { center, radius } = el;
3670
- const p1 = { x: center.x + radius, y: center.y };
3671
- const p2 = { x: center.x - radius, y: center.y };
3672
- cutout_builder.add("move_operation", {
3673
- x: p1.x,
3674
- y: mfy(p1.y)
3675
- }).add("set_movement_mode_to_counterclockwise_circular", {}).add("plot_operation", {
3676
- x: p2.x,
3677
- y: mfy(p2.y),
3678
- i: -radius,
3679
- j: 0
3680
- }).add("plot_operation", {
3681
- x: p1.x,
3682
- y: mfy(p1.y),
3683
- i: radius,
3684
- j: 0
3685
- }).add("set_movement_mode_to_linear", {});
3686
- } else if (el.shape === "polygon") {
3687
- const { points } = el;
3688
- if (points.length > 0) {
3689
- cutout_builder.add("move_operation", {
3690
- x: points[0].x,
3691
- y: mfy(points[0].y)
3692
- });
3693
- for (let i = 1; i < points.length; i++) {
3694
- cutout_builder.add("plot_operation", {
3695
- x: points[i].x,
3696
- y: mfy(points[i].y)
3697
- });
3698
- }
3699
- cutout_builder.add("plot_operation", {
3700
- x: points[0].x,
3701
- y: mfy(points[0].y)
3702
- });
3703
- }
3704
- }
4081
+ const drawCw = hasBoard && !hasPanel && isCutoutFullyInternal({
4082
+ cutout: element,
4083
+ boardOutlinePolygons
4084
+ });
4085
+ emitCutoutEdgeCuts({
4086
+ cutout: element,
4087
+ builder: cutout_builder,
4088
+ mfy,
4089
+ drawCw
4090
+ });
3705
4091
  ec_layer.push(...cutout_builder.build());
3706
4092
  }
3707
4093
  }
@@ -3726,4 +4112,4 @@ export {
3726
4112
  stringifyGerberCommands,
3727
4113
  convertSoupToGerberCommands
3728
4114
  };
3729
- //# sourceMappingURL=chunk-7LG6NVYJ.js.map
4115
+ //# sourceMappingURL=chunk-23TRHNW3.js.map