circuit-json-to-lbrn 0.0.1 → 0.0.3

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.
@@ -0,0 +1,26 @@
1
+ # Created using @tscircuit/plop (npm install -g @tscircuit/plop)
2
+ name: Format Check
3
+
4
+ on:
5
+ push:
6
+ branches: [main]
7
+ pull_request:
8
+ branches: [main]
9
+
10
+ jobs:
11
+ format-check:
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - name: Setup bun
18
+ uses: oven-sh/setup-bun@v2
19
+ with:
20
+ bun-version: latest
21
+
22
+ - name: Install dependencies
23
+ run: bun install
24
+
25
+ - name: Run format check
26
+ run: bun run format:check
@@ -0,0 +1,71 @@
1
+ # Created using @tscircuit/plop (npm install -g @tscircuit/plop)
2
+ name: Publish to npm
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ workflow_dispatch:
8
+
9
+ env:
10
+ UPSTREAM_REPOS: "" # comma-separated list, e.g. "eval,tscircuit,docs"
11
+ UPSTREAM_PACKAGES_TO_UPDATE: "" # comma-separated list, e.g. "@tscircuit/core,@tscircuit/protos"
12
+
13
+ jobs:
14
+ publish:
15
+ runs-on: ubuntu-latest
16
+ if: ${{ !(github.event_name == 'push' && startsWith(github.event.head_commit.message, 'v')) }}
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ with:
20
+ token: ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN }}
21
+ - name: Setup bun
22
+ uses: oven-sh/setup-bun@v2
23
+ with:
24
+ bun-version: latest
25
+ - uses: actions/setup-node@v3
26
+ with:
27
+ node-version: 20
28
+ registry-url: https://registry.npmjs.org/
29
+ - run: npm install -g pver
30
+ - run: bun install --frozen-lockfile
31
+ - run: bun run build
32
+ - run: pver release --no-push-main
33
+ env:
34
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
35
+ GITHUB_TOKEN: ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN }}
36
+
37
+ - name: Create Pull Request
38
+ id: create-pr
39
+ uses: peter-evans/create-pull-request@v5
40
+ with:
41
+ commit-message: "chore: bump version"
42
+ title: "chore: bump version"
43
+ body: "Automated package update"
44
+ branch: bump-version-${{ github.run_number }}
45
+ base: main
46
+ token: ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN }}
47
+ committer: tscircuitbot <githubbot@tscircuit.com>
48
+ author: tscircuitbot <githubbot@tscircuit.com>
49
+
50
+ - name: Enable auto-merge
51
+ if: steps.create-pr.outputs.pull-request-number != ''
52
+ run: |
53
+ gh pr merge ${{ steps.create-pr.outputs.pull-request-number }} --auto --squash --delete-branch
54
+ env:
55
+ GH_TOKEN: ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN }}
56
+
57
+ # - name: Trigger upstream repo updates
58
+ # if: env.UPSTREAM_REPOS && env.UPSTREAM_PACKAGES_TO_UPDATE
59
+ # run: |
60
+ # IFS=',' read -ra REPOS <<< "${{ env.UPSTREAM_REPOS }}"
61
+ # for repo in "${REPOS[@]}"; do
62
+ # if [[ -n "$repo" ]]; then
63
+ # echo "Triggering update for repo: $repo"
64
+ # curl -X POST \
65
+ # -H "Accept: application/vnd.github.v3+json" \
66
+ # -H "Authorization: token ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN }}" \
67
+ # -H "Content-Type: application/json" \
68
+ # "https://api.github.com/repos/tscircuit/$repo/actions/workflows/update-package.yml/dispatches" \
69
+ # -d "{\"ref\":\"main\",\"inputs\":{\"package_names\":\"${{ env.UPSTREAM_PACKAGES_TO_UPDATE }}\"}}"
70
+ # fi
71
+ # done
@@ -0,0 +1,31 @@
1
+ # Created using @tscircuit/plop (npm install -g @tscircuit/plop)
2
+ name: Bun Test
3
+
4
+ on:
5
+ pull_request:
6
+ push:
7
+ branches:
8
+ - main
9
+
10
+ jobs:
11
+ test:
12
+ runs-on: ubuntu-latest
13
+ timeout-minutes: 5
14
+
15
+ # Skip test for PRs that not chore: bump version
16
+ if: "${{ github.event_name != 'pull_request' || github.event.pull_request.title != 'chore: bump version' }}"
17
+
18
+ steps:
19
+ - name: Checkout code
20
+ uses: actions/checkout@v4
21
+
22
+ - name: Setup bun
23
+ uses: oven-sh/setup-bun@v2
24
+ with:
25
+ bun-version: 1.3.1
26
+
27
+ - name: Install dependencies
28
+ run: bun install
29
+
30
+ - name: Run tests
31
+ run: bun test
@@ -0,0 +1,26 @@
1
+ # Created using @tscircuit/plop (npm install -g @tscircuit/plop)
2
+ name: Type Check
3
+
4
+ on:
5
+ push:
6
+ branches: [main]
7
+ pull_request:
8
+ branches: [main]
9
+
10
+ jobs:
11
+ type-check:
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - name: Setup bun
18
+ uses: oven-sh/setup-bun@v2
19
+ with:
20
+ bun-version: latest
21
+
22
+ - name: Install dependencies
23
+ run: bun i
24
+
25
+ - name: Run type check
26
+ run: bunx tsc --noEmit
package/dist/index.d.ts CHANGED
@@ -3,6 +3,10 @@ import { LightBurnProject } from 'lbrnts';
3
3
 
4
4
  declare const convertCircuitJsonToLbrn: (circuitJson: CircuitJson, options?: {
5
5
  includeSilkscreen?: boolean;
6
+ origin?: {
7
+ x: number;
8
+ y: number;
9
+ };
6
10
  }) => LightBurnProject;
7
11
 
8
12
  export { convertCircuitJsonToLbrn };
package/dist/index.js CHANGED
@@ -1,172 +1,27 @@
1
1
  // lib/index.ts
2
- import { LightBurnProject, CutSetting } from "lbrnts";
2
+ import { LightBurnProject, CutSetting, ShapePath as ShapePath3 } from "lbrnts";
3
3
  import { cju } from "@tscircuit/circuit-json-util";
4
4
 
5
5
  // lib/element-handlers/addPlatedHole/addCirclePlatedHole.ts
6
6
  import { ShapePath } from "lbrnts";
7
- var addCirclePlatedHole = (platedHole, ctx) => {
8
- const { project, copperCutSetting } = ctx;
9
- const radius = platedHole.outer_diameter / 2;
10
- const centerX = platedHole.x;
11
- const centerY = platedHole.y;
12
- const kappa = 0.5522847498;
13
- const controlOffset = radius * kappa;
14
- const verts = [
15
- // Right point (3 o'clock)
16
- { x: centerX + radius, y: centerY },
17
- // Bottom point (6 o'clock) with control points
18
- {
19
- x: centerX,
20
- y: centerY + radius,
21
- c: 1,
22
- c0x: radius,
23
- c0y: controlOffset,
24
- c1x: controlOffset,
25
- c1y: radius
26
- },
27
- // Left point (9 o'clock) with control points
28
- {
29
- x: centerX - radius,
30
- y: centerY,
31
- c: 1,
32
- c0x: -controlOffset,
33
- c0y: radius,
34
- c1x: -radius,
35
- c1y: controlOffset
36
- },
37
- // Top point (12 o'clock) with control points
38
- {
39
- x: centerX,
40
- y: centerY - radius,
41
- c: 1,
42
- c0x: -radius,
43
- c0y: -controlOffset,
44
- c1x: -controlOffset,
45
- c1y: -radius
46
- },
47
- // Back to right point (close path) with control points
48
- {
49
- x: centerX + radius,
50
- y: centerY,
51
- c: 1,
52
- c0x: controlOffset,
53
- c0y: -radius,
54
- c1x: radius,
55
- c1y: -controlOffset
56
- }
57
- ];
58
- const prims = [
59
- { type: 1 },
60
- // BezierTo for bottom
61
- { type: 1 },
62
- // BezierTo for left
63
- { type: 1 },
64
- // BezierTo for top
65
- { type: 1 }
66
- // BezierTo for right (closing)
67
- ];
68
- project.children.push(
69
- new ShapePath({
70
- cutIndex: copperCutSetting.index,
71
- verts,
72
- prims,
73
- isClosed: true
74
- })
75
- );
76
- };
77
-
78
- // lib/element-handlers/addPlatedHole/addOvalPlatedHole.ts
79
- var addOvalPlatedHole = (platedHole, ctx) => {
80
- console.warn(
81
- `Oval/pill plated hole not yet implemented: ${platedHole.pcb_plated_hole_id}`
82
- );
83
- };
84
-
85
- // lib/element-handlers/addPlatedHole/addCircularHoleWithRectPad.ts
86
- var addCircularHoleWithRectPad = (platedHole, ctx) => {
87
- console.warn(
88
- `Circular hole with rect pad not yet implemented: ${platedHole.pcb_plated_hole_id}`
89
- );
90
- };
91
-
92
- // lib/element-handlers/addPlatedHole/addPillHoleWithRectPad.ts
93
- var addPillHoleWithRectPad = (platedHole, ctx) => {
94
- console.warn(
95
- `Pill hole with rect pad not yet implemented: ${platedHole.pcb_plated_hole_id}`
96
- );
97
- };
98
-
99
- // lib/element-handlers/addPlatedHole/addRotatedPillHoleWithRectPad.ts
100
- var addRotatedPillHoleWithRectPad = (platedHole, ctx) => {
101
- console.warn(
102
- `Rotated pill hole with rect pad not yet implemented: ${platedHole.pcb_plated_hole_id}`
103
- );
104
- };
105
-
106
- // lib/element-handlers/addPlatedHole/addHoleWithPolygonPad.ts
107
- var addHoleWithPolygonPad = (platedHole, ctx) => {
108
- console.warn(
109
- `Hole with polygon pad not yet implemented: ${platedHole.pcb_plated_hole_id}`
110
- );
111
- };
112
-
113
- // lib/element-handlers/addPlatedHole/index.ts
114
- var addPlatedHole = (platedHole, ctx) => {
115
- switch (platedHole.shape) {
116
- case "circle":
117
- return addCirclePlatedHole(platedHole, ctx);
118
- case "oval":
119
- case "pill":
120
- return addOvalPlatedHole(platedHole, ctx);
121
- case "circular_hole_with_rect_pad":
122
- return addCircularHoleWithRectPad(platedHole, ctx);
123
- case "pill_hole_with_rect_pad":
124
- return addPillHoleWithRectPad(platedHole, ctx);
125
- case "rotated_pill_hole_with_rect_pad":
126
- return addRotatedPillHoleWithRectPad(platedHole, ctx);
127
- case "hole_with_polygon_pad":
128
- return addHoleWithPolygonPad(platedHole, ctx);
129
- default:
130
- const _exhaustive = platedHole;
131
- console.warn(`Unknown plated hole shape: ${platedHole.shape}`);
132
- }
133
- };
134
7
 
135
8
  // lib/element-handlers/addSmtPad/addRectSmtPad.ts
136
- import { ShapePath as ShapePath2 } from "lbrnts";
9
+ import { Box } from "@flatten-js/core";
10
+ import "lbrnts";
137
11
  var addRectSmtPad = (smtPad, ctx) => {
138
- const { project, copperCutSetting } = ctx;
139
- const centerX = smtPad.x;
140
- const centerY = smtPad.y;
12
+ const { project, copperCutSetting, connMap, netGeoms, origin } = ctx;
13
+ const centerX = smtPad.x + origin.x;
14
+ const centerY = smtPad.y + origin.y;
141
15
  const halfWidth = smtPad.width / 2;
142
16
  const halfHeight = smtPad.height / 2;
143
- const verts = [
144
- { x: centerX - halfWidth, y: centerY - halfHeight },
145
- // Top-left
146
- { x: centerX + halfWidth, y: centerY - halfHeight },
147
- // Top-right
148
- { x: centerX + halfWidth, y: centerY + halfHeight },
149
- // Bottom-right
150
- { x: centerX - halfWidth, y: centerY + halfHeight }
151
- // Bottom-left
152
- ];
153
- const prims = [
154
- { type: 0 },
155
- // LineTo for top-right
156
- { type: 0 },
157
- // LineTo for bottom-right
158
- { type: 0 },
159
- // LineTo for bottom-left
160
- { type: 0 }
161
- // LineTo back to top-left (closing)
162
- ];
163
- project.children.push(
164
- new ShapePath2({
165
- cutIndex: copperCutSetting.index,
166
- verts,
167
- prims,
168
- isClosed: true
169
- })
17
+ const netId = connMap.getNetConnectedToId(smtPad.pcb_smtpad_id);
18
+ ctx.netGeoms.get(netId)?.push(
19
+ new Box(
20
+ centerX - halfWidth,
21
+ centerY - halfHeight,
22
+ centerX + halfWidth,
23
+ centerY + halfHeight
24
+ )
170
25
  );
171
26
  };
172
27
 
@@ -183,6 +38,116 @@ var addSmtPad = (smtPad, ctx) => {
183
38
  }
184
39
  };
185
40
 
41
+ // lib/element-handlers/addPcbTrace/index.ts
42
+ import Flatten2, { BooleanOperations } from "@flatten-js/core";
43
+
44
+ // lib/element-handlers/addPcbTrace/circle-to-polygon.ts
45
+ import Flatten from "@flatten-js/core";
46
+ var circleToPolygon = (circle, segments = 32) => {
47
+ const points = [];
48
+ for (let i = 0; i < segments; i++) {
49
+ const angle = i * 2 * Math.PI / segments;
50
+ const x = circle.center.x + circle.r * Math.cos(angle);
51
+ const y = circle.center.y + circle.r * Math.sin(angle);
52
+ points.push(Flatten.point(x, y));
53
+ }
54
+ return new Flatten.Polygon(points);
55
+ };
56
+
57
+ // lib/element-handlers/addPcbTrace/index.ts
58
+ var addPcbTrace = (trace, ctx) => {
59
+ const { netGeoms, connMap, origin } = ctx;
60
+ const netId = connMap.getNetConnectedToId(
61
+ trace.source_trace_id ?? trace.pcb_trace_id
62
+ );
63
+ if (!netId) {
64
+ console.warn(`Trace ${trace.pcb_trace_id} is not connected to any net`);
65
+ return;
66
+ }
67
+ if (!trace.route || trace.route.length < 2) {
68
+ console.warn(`Trace ${trace.pcb_trace_id} has insufficient route points`);
69
+ return;
70
+ }
71
+ const { route } = trace;
72
+ const traceWidth = route.find((point) => point.route_type === "wire")?.width ?? 0.15;
73
+ const polygons = [];
74
+ for (const routePoint of route) {
75
+ const circle = new Flatten2.Circle(
76
+ new Flatten2.Point(routePoint.x + origin.x, routePoint.y + origin.y),
77
+ traceWidth / 2
78
+ );
79
+ polygons.push(circleToPolygon(circle));
80
+ }
81
+ for (let i = 0; i < route.length - 1; i++) {
82
+ const p1 = route[i];
83
+ const p2 = route[i + 1];
84
+ if (!p1 || !p2) continue;
85
+ const segmentLength = Math.hypot(p1.x - p2.x, p1.y - p2.y);
86
+ if (segmentLength === 0) continue;
87
+ const centerX = (p1.x + p2.x) / 2 + origin.x;
88
+ const centerY = (p1.y + p2.y) / 2 + origin.y;
89
+ const rotationDeg = Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180 / Math.PI;
90
+ const w2 = segmentLength / 2;
91
+ const h2 = traceWidth / 2;
92
+ const angleRad = rotationDeg * Math.PI / 180;
93
+ const cosAngle = Math.cos(angleRad);
94
+ const sinAngle = Math.sin(angleRad);
95
+ const corners = [
96
+ { x: -w2, y: -h2 },
97
+ { x: w2, y: -h2 },
98
+ { x: w2, y: h2 },
99
+ { x: -w2, y: h2 }
100
+ ];
101
+ const rotatedCorners = corners.map((p) => ({
102
+ x: centerX + p.x * cosAngle - p.y * sinAngle,
103
+ y: centerY + p.x * sinAngle + p.y * cosAngle
104
+ }));
105
+ polygons.push(
106
+ new Flatten2.Polygon(rotatedCorners.map((p) => Flatten2.point(p.x, p.y)))
107
+ );
108
+ }
109
+ if (polygons.length === 0) return;
110
+ let tracePolygon = polygons[0];
111
+ for (let i = 1; i < polygons.length; i++) {
112
+ const poly = polygons[i];
113
+ if (poly) {
114
+ tracePolygon = BooleanOperations.unify(tracePolygon, poly);
115
+ }
116
+ }
117
+ netGeoms.get(netId)?.push(tracePolygon);
118
+ };
119
+
120
+ // lib/index.ts
121
+ import { getFullConnectivityMapFromCircuitJson } from "circuit-json-to-connectivity-map";
122
+ import { Polygon, Box as Box2, BooleanOperations as BooleanOperations2 } from "@flatten-js/core";
123
+
124
+ // lib/polygon-to-shape-path.ts
125
+ function polygonToShapePathData(polygon) {
126
+ const verts = [];
127
+ const prims = [];
128
+ const svgString = polygon.svg();
129
+ const dAttributeMatch = svgString.match(/\bd="([^"]+)"/);
130
+ if (!dAttributeMatch || !dAttributeMatch[1]) {
131
+ return { verts, prims };
132
+ }
133
+ const pathData = dAttributeMatch[1].trim();
134
+ const commands = pathData.match(/[MLz][^MLz]*/g) || [];
135
+ for (const command of commands) {
136
+ const type = command[0];
137
+ const coords = command.slice(1).trim();
138
+ if (type === "M" || type === "L") {
139
+ const parts = coords.split(",");
140
+ const x = Number(parts[0]);
141
+ const y = Number(parts[1]);
142
+ if (!Number.isNaN(x) && !Number.isNaN(y)) {
143
+ verts.push({ x, y });
144
+ prims.push({ type: 0 });
145
+ }
146
+ }
147
+ }
148
+ return { verts, prims };
149
+ }
150
+
186
151
  // lib/index.ts
187
152
  var convertCircuitJsonToLbrn = (circuitJson, options = {}) => {
188
153
  const db = cju(circuitJson);
@@ -197,16 +162,51 @@ var convertCircuitJsonToLbrn = (circuitJson, options = {}) => {
197
162
  speed: 100
198
163
  });
199
164
  project.children.push(copperCutSetting);
165
+ const connMap = getFullConnectivityMapFromCircuitJson(circuitJson);
200
166
  const ctx = {
201
167
  db,
202
168
  project,
203
- copperCutSetting
169
+ copperCutSetting,
170
+ connMap,
171
+ netGeoms: /* @__PURE__ */ new Map(),
172
+ origin: options.origin ?? { x: 0, y: 0 }
204
173
  };
174
+ for (const net of Object.keys(connMap.netMap)) {
175
+ ctx.netGeoms.set(net, []);
176
+ }
205
177
  for (const smtpad of db.pcb_smtpad.list()) {
206
178
  addSmtPad(smtpad, ctx);
207
179
  }
208
- for (const platedHole of db.pcb_plated_hole.list()) {
209
- addPlatedHole(platedHole, ctx);
180
+ for (const trace of db.pcb_trace.list()) {
181
+ addPcbTrace(trace, ctx);
182
+ }
183
+ for (const net of Object.keys(connMap.netMap)) {
184
+ const netGeoms = ctx.netGeoms.get(net);
185
+ if (netGeoms.length === 0) {
186
+ continue;
187
+ }
188
+ let union = netGeoms[0];
189
+ if (union instanceof Box2) {
190
+ union = new Polygon(union);
191
+ }
192
+ for (const geom of netGeoms.slice(1)) {
193
+ if (geom instanceof Polygon) {
194
+ union = BooleanOperations2.unify(union, geom);
195
+ } else if (geom instanceof Box2) {
196
+ union = BooleanOperations2.unify(union, new Polygon(geom));
197
+ }
198
+ }
199
+ for (const island of union.splitToIslands()) {
200
+ const { verts, prims } = polygonToShapePathData(island);
201
+ project.children.push(
202
+ new ShapePath3({
203
+ cutIndex: copperCutSetting.index,
204
+ verts,
205
+ prims,
206
+ isClosed: false
207
+ })
208
+ );
209
+ }
210
210
  }
211
211
  return project;
212
212
  };
@@ -1,9 +1,19 @@
1
1
  import type { CircuitJsonUtilObjects } from "@tscircuit/circuit-json-util"
2
2
  import type { CutSetting, LightBurnProject } from "lbrnts"
3
+ import type { AnyShape, Box, Polygon } from "@flatten-js/core"
4
+ import type { ConnectivityMap } from "circuit-json-to-connectivity-map"
5
+
6
+ export type ConnectivityMapKey = string
3
7
 
4
8
  export interface ConvertContext {
5
9
  db: CircuitJsonUtilObjects
6
10
  project: LightBurnProject
7
11
 
8
12
  copperCutSetting: CutSetting
13
+
14
+ connMap: ConnectivityMap
15
+
16
+ netGeoms: Map<ConnectivityMapKey, Array<Polygon | Box>>
17
+
18
+ origin: { x: number; y: number }
9
19
  }
@@ -0,0 +1,15 @@
1
+ import Flatten from "@flatten-js/core"
2
+
3
+ export const circleToPolygon = (
4
+ circle: Flatten.Circle,
5
+ segments = 32,
6
+ ): Flatten.Polygon => {
7
+ const points: Flatten.Point[] = []
8
+ for (let i = 0; i < segments; i++) {
9
+ const angle = (i * 2 * Math.PI) / segments
10
+ const x = circle.center.x + circle.r * Math.cos(angle)
11
+ const y = circle.center.y + circle.r * Math.sin(angle)
12
+ points.push(Flatten.point(x, y))
13
+ }
14
+ return new Flatten.Polygon(points)
15
+ }
@@ -0,0 +1,90 @@
1
+ import type { PcbTrace } from "circuit-json"
2
+ import type { ConvertContext } from "../../ConvertContext"
3
+ import Flatten, { BooleanOperations } from "@flatten-js/core"
4
+ import { circleToPolygon } from "./circle-to-polygon"
5
+
6
+ export const addPcbTrace = (trace: PcbTrace, ctx: ConvertContext) => {
7
+ const { netGeoms, connMap, origin } = ctx
8
+
9
+ const netId = connMap.getNetConnectedToId(
10
+ trace.source_trace_id ?? trace.pcb_trace_id,
11
+ )!
12
+
13
+ if (!netId) {
14
+ console.warn(`Trace ${trace.pcb_trace_id} is not connected to any net`)
15
+ return
16
+ }
17
+
18
+ if (!trace.route || trace.route.length < 2) {
19
+ console.warn(`Trace ${trace.pcb_trace_id} has insufficient route points`)
20
+ return
21
+ }
22
+
23
+ const { route } = trace
24
+
25
+ const traceWidth =
26
+ route.find((point) => point.route_type === "wire")?.width ?? 0.15
27
+
28
+ const polygons: Flatten.Polygon[] = []
29
+
30
+ // Add circles for each vertex
31
+ for (const routePoint of route) {
32
+ const circle = new Flatten.Circle(
33
+ new Flatten.Point(routePoint.x + origin.x, routePoint.y + origin.y),
34
+ traceWidth / 2,
35
+ )
36
+ polygons.push(circleToPolygon(circle))
37
+ }
38
+
39
+ // Add rectangles for each segment
40
+ for (let i = 0; i < route.length - 1; i++) {
41
+ const p1 = route[i]
42
+ const p2 = route[i + 1]
43
+
44
+ if (!p1 || !p2) continue
45
+
46
+ const segmentLength = Math.hypot(p1.x - p2.x, p1.y - p2.y)
47
+ if (segmentLength === 0) continue
48
+
49
+ const centerX = (p1.x + p2.x) / 2 + origin.x
50
+ const centerY = (p1.y + p2.y) / 2 + origin.y
51
+ const rotationDeg = (Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180) / Math.PI
52
+
53
+ const w2 = segmentLength / 2
54
+ const h2 = traceWidth / 2
55
+
56
+ const angleRad = (rotationDeg * Math.PI) / 180
57
+ const cosAngle = Math.cos(angleRad)
58
+ const sinAngle = Math.sin(angleRad)
59
+
60
+ const corners = [
61
+ { x: -w2, y: -h2 },
62
+ { x: w2, y: -h2 },
63
+ { x: w2, y: h2 },
64
+ { x: -w2, y: h2 },
65
+ ]
66
+
67
+ const rotatedCorners = corners.map((p) => ({
68
+ x: centerX + p.x * cosAngle - p.y * sinAngle,
69
+ y: centerY + p.x * sinAngle + p.y * cosAngle,
70
+ }))
71
+
72
+ polygons.push(
73
+ new Flatten.Polygon(rotatedCorners.map((p) => Flatten.point(p.x, p.y))),
74
+ )
75
+ }
76
+
77
+ // Union all polygons together to create the final trace polygon
78
+ if (polygons.length === 0) return
79
+
80
+ let tracePolygon = polygons[0]!
81
+
82
+ for (let i = 1; i < polygons.length; i++) {
83
+ const poly = polygons[i]
84
+ if (poly) {
85
+ tracePolygon = BooleanOperations.unify(tracePolygon, poly)
86
+ }
87
+ }
88
+
89
+ netGeoms.get(netId)?.push(tracePolygon)
90
+ }