@tscircuit/rectdiff 0.0.5 → 0.0.7
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/index.d.ts +0 -97
- package/dist/index.js +168 -655
- package/lib/solvers/RectDiffSolver.ts +71 -79
- package/lib/solvers/rectdiff/engine.ts +26 -7
- package/lib/solvers/rectdiff/geometry/computeInverseRects.ts +108 -0
- package/lib/solvers/rectdiff/geometry/isPointInPolygon.ts +20 -0
- package/lib/solvers/rectdiff/types.ts +1 -0
- package/package.json +1 -1
- package/pages/board-with-cutout.page.tsx +11 -0
- package/test-assets/board-with-cutout.json +148 -0
- package/tests/__snapshots__/board-outline.snap.svg +59 -0
- package/tests/board-outline.test.ts +18 -0
|
@@ -13,12 +13,12 @@ import {
|
|
|
13
13
|
computeProgress,
|
|
14
14
|
} from "./rectdiff/engine"
|
|
15
15
|
import { rectsToMeshNodes } from "./rectdiff/rectsToMeshNodes"
|
|
16
|
+
import { overlaps } from "./rectdiff/geometry"
|
|
16
17
|
import type { GapFillOptions } from "./rectdiff/gapfill/types"
|
|
17
18
|
import {
|
|
18
19
|
findUncoveredPoints,
|
|
19
20
|
calculateCoverage,
|
|
20
21
|
} from "./rectdiff/gapfill/engine"
|
|
21
|
-
import { GapFillSubSolver } from "./rectdiff/subsolvers/GapFillSubSolver"
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* A streaming, one-step-per-iteration solver for capacity mesh generation.
|
|
@@ -26,23 +26,16 @@ import { GapFillSubSolver } from "./rectdiff/subsolvers/GapFillSubSolver"
|
|
|
26
26
|
export class RectDiffSolver extends BaseSolver {
|
|
27
27
|
private srj: SimpleRouteJson
|
|
28
28
|
private gridOptions: Partial<GridFill3DOptions>
|
|
29
|
-
private gapFillOptions: Partial<GapFillOptions>
|
|
30
29
|
private state!: RectDiffState
|
|
31
30
|
private _meshNodes: CapacityMeshNode[] = []
|
|
32
31
|
|
|
33
|
-
/** Active subsolver for GAP_FILL phases. */
|
|
34
|
-
declare activeSubSolver: GapFillSubSolver | null
|
|
35
|
-
|
|
36
32
|
constructor(opts: {
|
|
37
33
|
simpleRouteJson: SimpleRouteJson
|
|
38
34
|
gridOptions?: Partial<GridFill3DOptions>
|
|
39
|
-
gapFillOptions?: Partial<GapFillOptions>
|
|
40
35
|
}) {
|
|
41
36
|
super()
|
|
42
37
|
this.srj = opts.simpleRouteJson
|
|
43
38
|
this.gridOptions = opts.gridOptions ?? {}
|
|
44
|
-
this.gapFillOptions = opts.gapFillOptions ?? {}
|
|
45
|
-
this.activeSubSolver = null
|
|
46
39
|
}
|
|
47
40
|
|
|
48
41
|
override _setup() {
|
|
@@ -60,44 +53,7 @@ export class RectDiffSolver extends BaseSolver {
|
|
|
60
53
|
} else if (this.state.phase === "EXPANSION") {
|
|
61
54
|
stepExpansion(this.state)
|
|
62
55
|
} else if (this.state.phase === "GAP_FILL") {
|
|
63
|
-
|
|
64
|
-
if (
|
|
65
|
-
!this.activeSubSolver ||
|
|
66
|
-
!(this.activeSubSolver instanceof GapFillSubSolver)
|
|
67
|
-
) {
|
|
68
|
-
const minTrace = this.srj.minTraceWidth || 0.15
|
|
69
|
-
const minGapSize = Math.max(0.01, minTrace / 10)
|
|
70
|
-
const boundsSize = Math.min(
|
|
71
|
-
this.state.bounds.width,
|
|
72
|
-
this.state.bounds.height,
|
|
73
|
-
)
|
|
74
|
-
this.activeSubSolver = new GapFillSubSolver({
|
|
75
|
-
placed: this.state.placed,
|
|
76
|
-
options: {
|
|
77
|
-
minWidth: minGapSize,
|
|
78
|
-
minHeight: minGapSize,
|
|
79
|
-
scanResolution: Math.max(0.05, boundsSize / 100),
|
|
80
|
-
...this.gapFillOptions,
|
|
81
|
-
},
|
|
82
|
-
layerCtx: {
|
|
83
|
-
bounds: this.state.bounds,
|
|
84
|
-
layerCount: this.state.layerCount,
|
|
85
|
-
obstaclesByLayer: this.state.obstaclesByLayer,
|
|
86
|
-
placedByLayer: this.state.placedByLayer,
|
|
87
|
-
},
|
|
88
|
-
})
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
this.activeSubSolver.step()
|
|
92
|
-
|
|
93
|
-
if (this.activeSubSolver.solved) {
|
|
94
|
-
// Transfer results back to main state
|
|
95
|
-
const output = this.activeSubSolver.getOutput()
|
|
96
|
-
this.state.placed = output.placed
|
|
97
|
-
this.state.placedByLayer = output.placedByLayer
|
|
98
|
-
this.activeSubSolver = null
|
|
99
|
-
this.state.phase = "DONE"
|
|
100
|
-
}
|
|
56
|
+
this.state.phase = "DONE"
|
|
101
57
|
} else if (this.state.phase === "DONE") {
|
|
102
58
|
// Finalize once
|
|
103
59
|
if (!this.solved) {
|
|
@@ -112,10 +68,6 @@ export class RectDiffSolver extends BaseSolver {
|
|
|
112
68
|
this.stats.phase = this.state.phase
|
|
113
69
|
this.stats.gridIndex = this.state.gridIndex
|
|
114
70
|
this.stats.placed = this.state.placed.length
|
|
115
|
-
if (this.activeSubSolver instanceof GapFillSubSolver) {
|
|
116
|
-
const output = this.activeSubSolver.getOutput()
|
|
117
|
-
this.stats.gapsFilled = output.filledCount
|
|
118
|
-
}
|
|
119
71
|
}
|
|
120
72
|
|
|
121
73
|
/** Compute solver progress (0 to 1). */
|
|
@@ -123,13 +75,7 @@ export class RectDiffSolver extends BaseSolver {
|
|
|
123
75
|
if (this.solved || this.state.phase === "DONE") {
|
|
124
76
|
return 1
|
|
125
77
|
}
|
|
126
|
-
|
|
127
|
-
this.state.phase === "GAP_FILL" &&
|
|
128
|
-
this.activeSubSolver instanceof GapFillSubSolver
|
|
129
|
-
) {
|
|
130
|
-
return 0.85 + 0.1 * this.activeSubSolver.computeProgress()
|
|
131
|
-
}
|
|
132
|
-
return computeProgress(this.state) * 0.85
|
|
78
|
+
return computeProgress(this.state)
|
|
133
79
|
}
|
|
134
80
|
|
|
135
81
|
override getOutput(): { meshNodes: CapacityMeshNode[] } {
|
|
@@ -183,13 +129,9 @@ export class RectDiffSolver extends BaseSolver {
|
|
|
183
129
|
|
|
184
130
|
/** Streaming visualization: board + obstacles + current placements. */
|
|
185
131
|
override visualize(): GraphicsObject {
|
|
186
|
-
// If a subsolver is active, delegate to its visualization
|
|
187
|
-
if (this.activeSubSolver) {
|
|
188
|
-
return this.activeSubSolver.visualize()
|
|
189
|
-
}
|
|
190
|
-
|
|
191
132
|
const rects: NonNullable<GraphicsObject["rects"]> = []
|
|
192
133
|
const points: NonNullable<GraphicsObject["points"]> = []
|
|
134
|
+
const lines: NonNullable<GraphicsObject["lines"]> = [] // Initialize lines array
|
|
193
135
|
|
|
194
136
|
// Board bounds - use srj bounds which is always available
|
|
195
137
|
const boardBounds = {
|
|
@@ -199,26 +141,35 @@ export class RectDiffSolver extends BaseSolver {
|
|
|
199
141
|
maxY: this.srj.bounds.maxY,
|
|
200
142
|
}
|
|
201
143
|
|
|
202
|
-
// board
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
144
|
+
// board or outline
|
|
145
|
+
if (this.srj.outline && this.srj.outline.length > 1) {
|
|
146
|
+
lines.push({
|
|
147
|
+
points: [...this.srj.outline, this.srj.outline[0]!], // Close the loop by adding the first point again
|
|
148
|
+
strokeColor: "#111827",
|
|
149
|
+
strokeWidth: 0.01,
|
|
150
|
+
label: "outline",
|
|
151
|
+
})
|
|
152
|
+
} else {
|
|
153
|
+
rects.push({
|
|
154
|
+
center: {
|
|
155
|
+
x: (boardBounds.minX + boardBounds.maxX) / 2,
|
|
156
|
+
y: (boardBounds.minY + boardBounds.maxY) / 2,
|
|
157
|
+
},
|
|
158
|
+
width: boardBounds.maxX - boardBounds.minX,
|
|
159
|
+
height: boardBounds.maxY - boardBounds.minY,
|
|
160
|
+
fill: "none",
|
|
161
|
+
stroke: "#111827",
|
|
162
|
+
label: "board",
|
|
163
|
+
})
|
|
164
|
+
}
|
|
214
165
|
|
|
215
166
|
// obstacles (rect & oval as bounding boxes)
|
|
216
|
-
for (const
|
|
217
|
-
if (
|
|
167
|
+
for (const obstacle of this.srj.obstacles ?? []) {
|
|
168
|
+
if (obstacle.type === "rect" || obstacle.type === "oval") {
|
|
218
169
|
rects.push({
|
|
219
|
-
center: { x:
|
|
220
|
-
width:
|
|
221
|
-
height:
|
|
170
|
+
center: { x: obstacle.center.x, y: obstacle.center.y },
|
|
171
|
+
width: obstacle.width,
|
|
172
|
+
height: obstacle.height,
|
|
222
173
|
fill: "#fee2e2",
|
|
223
174
|
stroke: "#ef4444",
|
|
224
175
|
layer: "obstacle",
|
|
@@ -227,6 +178,46 @@ export class RectDiffSolver extends BaseSolver {
|
|
|
227
178
|
}
|
|
228
179
|
}
|
|
229
180
|
|
|
181
|
+
// board void rects
|
|
182
|
+
if (this.state?.boardVoidRects) {
|
|
183
|
+
// If outline exists, compute its bbox to hide outer padding voids
|
|
184
|
+
let outlineBBox: {
|
|
185
|
+
x: number
|
|
186
|
+
y: number
|
|
187
|
+
width: number
|
|
188
|
+
height: number
|
|
189
|
+
} | null = null
|
|
190
|
+
|
|
191
|
+
if (this.srj.outline && this.srj.outline.length > 0) {
|
|
192
|
+
const xs = this.srj.outline.map((p) => p.x)
|
|
193
|
+
const ys = this.srj.outline.map((p) => p.y)
|
|
194
|
+
const minX = Math.min(...xs)
|
|
195
|
+
const minY = Math.min(...ys)
|
|
196
|
+
outlineBBox = {
|
|
197
|
+
x: minX,
|
|
198
|
+
y: minY,
|
|
199
|
+
width: Math.max(...xs) - minX,
|
|
200
|
+
height: Math.max(...ys) - minY,
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const r of this.state.boardVoidRects) {
|
|
205
|
+
// If we have an outline, only show voids that overlap its bbox (hides outer padding)
|
|
206
|
+
if (outlineBBox && !overlaps(r, outlineBBox)) {
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
rects.push({
|
|
211
|
+
center: { x: r.x + r.width / 2, y: r.y + r.height / 2 },
|
|
212
|
+
width: r.width,
|
|
213
|
+
height: r.height,
|
|
214
|
+
fill: "rgba(0, 0, 0, 0.5)",
|
|
215
|
+
stroke: "none",
|
|
216
|
+
label: "void",
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
230
221
|
// candidate positions (where expansion started from)
|
|
231
222
|
if (this.state?.candidates?.length) {
|
|
232
223
|
for (const cand of this.state.candidates) {
|
|
@@ -263,6 +254,7 @@ export class RectDiffSolver extends BaseSolver {
|
|
|
263
254
|
coordinateSystem: "cartesian",
|
|
264
255
|
rects,
|
|
265
256
|
points,
|
|
257
|
+
lines, // Include lines in the returned GraphicsObject
|
|
266
258
|
}
|
|
267
259
|
}
|
|
268
260
|
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
overlaps,
|
|
21
21
|
subtractRect2D,
|
|
22
22
|
} from "./geometry"
|
|
23
|
+
import { computeInverseRects } from "./geometry/computeInverseRects"
|
|
23
24
|
import { buildZIndexMap, obstacleToXYRect, obstacleZs } from "./layers"
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -44,11 +45,24 @@ export function initState(
|
|
|
44
45
|
{ length: layerCount },
|
|
45
46
|
() => [],
|
|
46
47
|
)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
|
|
49
|
+
// Compute void rects from outline if present
|
|
50
|
+
let boardVoidRects: XYRect[] = []
|
|
51
|
+
if (srj.outline && srj.outline.length > 2) {
|
|
52
|
+
boardVoidRects = computeInverseRects(bounds, srj.outline)
|
|
53
|
+
// Add void rects as obstacles to ALL layers
|
|
54
|
+
for (const voidR of boardVoidRects) {
|
|
55
|
+
for (let z = 0; z < layerCount; z++) {
|
|
56
|
+
obstaclesByLayer[z]!.push(voidR)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const obstacle of srj.obstacles ?? []) {
|
|
62
|
+
const rect = obstacleToXYRect(obstacle)
|
|
63
|
+
if (!rect) continue
|
|
64
|
+
const zLayers = obstacleZs(obstacle, zIndexByName)
|
|
65
|
+
const invalidZs = zLayers.filter((z) => z < 0 || z >= layerCount)
|
|
52
66
|
if (invalidZs.length) {
|
|
53
67
|
throw new Error(
|
|
54
68
|
`RectDiffSolver: obstacle uses z-layer indices ${invalidZs.join(
|
|
@@ -57,8 +71,9 @@ export function initState(
|
|
|
57
71
|
)
|
|
58
72
|
}
|
|
59
73
|
// Persist normalized zLayers back onto the shared SRJ so downstream solvers see them.
|
|
60
|
-
if ((!
|
|
61
|
-
|
|
74
|
+
if ((!obstacle.zLayers || obstacle.zLayers.length === 0) && zLayers.length)
|
|
75
|
+
obstacle.zLayers = zLayers
|
|
76
|
+
for (const z of zLayers) obstaclesByLayer[z]!.push(rect)
|
|
62
77
|
}
|
|
63
78
|
|
|
64
79
|
const trace = Math.max(0.01, srj.minTraceWidth || 0.15)
|
|
@@ -97,6 +112,7 @@ export function initState(
|
|
|
97
112
|
bounds,
|
|
98
113
|
options,
|
|
99
114
|
obstaclesByLayer,
|
|
115
|
+
boardVoidRects,
|
|
100
116
|
phase: "GRID",
|
|
101
117
|
gridIndex: 0,
|
|
102
118
|
candidates: [],
|
|
@@ -426,7 +442,10 @@ export function finalizeRects(state: RectDiffState): Rect3d[] {
|
|
|
426
442
|
})
|
|
427
443
|
|
|
428
444
|
// Append obstacle nodes to the output
|
|
445
|
+
const voidSet = new Set(state.boardVoidRects || [])
|
|
429
446
|
for (const [rect, layerIndices] of layersByObstacleRect.entries()) {
|
|
447
|
+
if (voidSet.has(rect)) continue // Skip void rects
|
|
448
|
+
|
|
430
449
|
out.push({
|
|
431
450
|
minX: rect.x,
|
|
432
451
|
minY: rect.y,
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { XYRect } from "../types"
|
|
2
|
+
import { isPointInPolygon } from "./isPointInPolygon"
|
|
3
|
+
import { EPS } from "../geometry" // Import EPS from common geometry file
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Decompose the empty space inside 'bounds' but outside 'polygon' into rectangles.
|
|
7
|
+
* This uses a coordinate grid approach, ideal for rectilinear polygons.
|
|
8
|
+
*/
|
|
9
|
+
export function computeInverseRects(
|
|
10
|
+
bounds: XYRect,
|
|
11
|
+
polygon: Array<{ x: number; y: number }>,
|
|
12
|
+
): XYRect[] {
|
|
13
|
+
if (!polygon || polygon.length < 3) return []
|
|
14
|
+
|
|
15
|
+
// 1. Collect unique sorted X and Y coordinates
|
|
16
|
+
const xs = new Set<number>([bounds.x, bounds.x + bounds.width])
|
|
17
|
+
const ys = new Set<number>([bounds.y, bounds.y + bounds.height])
|
|
18
|
+
for (const p of polygon) {
|
|
19
|
+
xs.add(p.x)
|
|
20
|
+
ys.add(p.y)
|
|
21
|
+
}
|
|
22
|
+
const xSorted = Array.from(xs).sort((a, b) => a - b)
|
|
23
|
+
const ySorted = Array.from(ys).sort((a, b) => a - b)
|
|
24
|
+
|
|
25
|
+
// 2. Generate grid cells and classify them
|
|
26
|
+
const rawRects: XYRect[] = []
|
|
27
|
+
for (let i = 0; i < xSorted.length - 1; i++) {
|
|
28
|
+
for (let j = 0; j < ySorted.length - 1; j++) {
|
|
29
|
+
const x0 = xSorted[i]!
|
|
30
|
+
const x1 = xSorted[i + 1]!
|
|
31
|
+
const y0 = ySorted[j]!
|
|
32
|
+
const y1 = ySorted[j + 1]!
|
|
33
|
+
|
|
34
|
+
// Check center point
|
|
35
|
+
const cx = (x0 + x1) / 2
|
|
36
|
+
const cy = (y0 + y1) / 2
|
|
37
|
+
|
|
38
|
+
// If NOT in polygon, it's a void rect
|
|
39
|
+
if (
|
|
40
|
+
cx >= bounds.x &&
|
|
41
|
+
cx <= bounds.x + bounds.width &&
|
|
42
|
+
cy >= bounds.y &&
|
|
43
|
+
cy <= bounds.y + bounds.height
|
|
44
|
+
) {
|
|
45
|
+
if (!isPointInPolygon({ x: cx, y: cy }, polygon)) {
|
|
46
|
+
rawRects.push({ x: x0, y: y0, width: x1 - x0, height: y1 - y0 })
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 3. Simple merge pass (horizontal)
|
|
53
|
+
const finalRects: XYRect[] = []
|
|
54
|
+
|
|
55
|
+
// Sort by y then x
|
|
56
|
+
rawRects.sort((a, b) => {
|
|
57
|
+
if (Math.abs(a.y - b.y) > EPS) return a.y - b.y
|
|
58
|
+
return a.x - b.x
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
let current: XYRect | null = null
|
|
62
|
+
for (const r of rawRects) {
|
|
63
|
+
if (!current) {
|
|
64
|
+
current = r
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const sameY = Math.abs(current.y - r.y) < EPS
|
|
69
|
+
const sameHeight = Math.abs(current.height - r.height) < EPS
|
|
70
|
+
const touchesX = Math.abs(current.x + current.width - r.x) < EPS
|
|
71
|
+
|
|
72
|
+
if (sameY && sameHeight && touchesX) {
|
|
73
|
+
current.width += r.width
|
|
74
|
+
} else {
|
|
75
|
+
finalRects.push(current)
|
|
76
|
+
current = r
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (current) finalRects.push(current)
|
|
80
|
+
|
|
81
|
+
// 4. Vertical merge pass
|
|
82
|
+
finalRects.sort((a, b) => {
|
|
83
|
+
if (Math.abs(a.x - b.x) > EPS) return a.x - b.x
|
|
84
|
+
return a.y - b.y
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const mergedVertical: XYRect[] = []
|
|
88
|
+
current = null
|
|
89
|
+
for (const r of finalRects) {
|
|
90
|
+
if (!current) {
|
|
91
|
+
current = r
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
const sameX = Math.abs(current.x - r.x) < EPS
|
|
95
|
+
const sameWidth = Math.abs(current.width - r.width) < EPS
|
|
96
|
+
const touchesY = Math.abs(current.y + current.height - r.y) < EPS
|
|
97
|
+
|
|
98
|
+
if (sameX && sameWidth && touchesY) {
|
|
99
|
+
current.height += r.height
|
|
100
|
+
} else {
|
|
101
|
+
mergedVertical.push(current)
|
|
102
|
+
current = r
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (current) mergedVertical.push(current)
|
|
106
|
+
|
|
107
|
+
return mergedVertical
|
|
108
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if a point is inside a polygon using ray casting.
|
|
3
|
+
*/
|
|
4
|
+
export function isPointInPolygon(
|
|
5
|
+
p: { x: number; y: number },
|
|
6
|
+
polygon: Array<{ x: number; y: number }>,
|
|
7
|
+
): boolean {
|
|
8
|
+
let inside = false
|
|
9
|
+
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
|
10
|
+
const xi = polygon[i]!.x,
|
|
11
|
+
yi = polygon[i]!.y
|
|
12
|
+
const xj = polygon[j]!.x,
|
|
13
|
+
yj = polygon[j]!.y
|
|
14
|
+
|
|
15
|
+
const intersect =
|
|
16
|
+
yi > p.y !== yj > p.y && p.x < ((xj - xi) * (p.y - yi)) / (yj - yi) + xi
|
|
17
|
+
if (intersect) inside = !inside
|
|
18
|
+
}
|
|
19
|
+
return inside
|
|
20
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { GenericSolverDebugger } from "@tscircuit/solver-utils/react"
|
|
2
|
+
import simpleRouteJson from "../test-assets/board-with-cutout.json"
|
|
3
|
+
import { RectDiffSolver } from "../lib/solvers/RectDiffSolver"
|
|
4
|
+
import { useMemo } from "react"
|
|
5
|
+
import { SolverDebugger3d } from "../components/SolverDebugger3d"
|
|
6
|
+
|
|
7
|
+
export default () => {
|
|
8
|
+
const solver = useMemo(() => new RectDiffSolver({ simpleRouteJson }), [])
|
|
9
|
+
|
|
10
|
+
return <SolverDebugger3d solver={solver} />
|
|
11
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bounds": {
|
|
3
|
+
"minX": -9,
|
|
4
|
+
"maxX": 9,
|
|
5
|
+
"minY": -7,
|
|
6
|
+
"maxY": 7
|
|
7
|
+
},
|
|
8
|
+
"obstacles": [
|
|
9
|
+
{
|
|
10
|
+
"type": "rect",
|
|
11
|
+
"layers": ["top"],
|
|
12
|
+
"center": {
|
|
13
|
+
"x": -5.51,
|
|
14
|
+
"y": -4
|
|
15
|
+
},
|
|
16
|
+
"width": 0.54,
|
|
17
|
+
"height": 0.64,
|
|
18
|
+
"connectedTo": [
|
|
19
|
+
"pcb_smtpad_0",
|
|
20
|
+
"connectivity_net11",
|
|
21
|
+
"source_port_0",
|
|
22
|
+
"pcb_smtpad_0",
|
|
23
|
+
"pcb_port_0"
|
|
24
|
+
],
|
|
25
|
+
"zLayers": [0]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"type": "rect",
|
|
29
|
+
"layers": ["top"],
|
|
30
|
+
"center": {
|
|
31
|
+
"x": -4.49,
|
|
32
|
+
"y": -4
|
|
33
|
+
},
|
|
34
|
+
"width": 0.54,
|
|
35
|
+
"height": 0.64,
|
|
36
|
+
"connectedTo": [
|
|
37
|
+
"pcb_smtpad_1",
|
|
38
|
+
"connectivity_net0",
|
|
39
|
+
"source_trace_0",
|
|
40
|
+
"source_port_1",
|
|
41
|
+
"source_port_3",
|
|
42
|
+
"pcb_smtpad_1",
|
|
43
|
+
"pcb_port_1",
|
|
44
|
+
"pcb_smtpad_3",
|
|
45
|
+
"pcb_port_3"
|
|
46
|
+
],
|
|
47
|
+
"zLayers": [0]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"type": "rect",
|
|
51
|
+
"layers": ["top"],
|
|
52
|
+
"center": {
|
|
53
|
+
"x": 4.49,
|
|
54
|
+
"y": -4
|
|
55
|
+
},
|
|
56
|
+
"width": 0.54,
|
|
57
|
+
"height": 0.64,
|
|
58
|
+
"connectedTo": [
|
|
59
|
+
"pcb_smtpad_2",
|
|
60
|
+
"connectivity_net12",
|
|
61
|
+
"source_port_2",
|
|
62
|
+
"pcb_smtpad_2",
|
|
63
|
+
"pcb_port_2"
|
|
64
|
+
],
|
|
65
|
+
"zLayers": [0]
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"type": "rect",
|
|
69
|
+
"layers": ["top"],
|
|
70
|
+
"center": {
|
|
71
|
+
"x": 5.51,
|
|
72
|
+
"y": -4
|
|
73
|
+
},
|
|
74
|
+
"width": 0.54,
|
|
75
|
+
"height": 0.64,
|
|
76
|
+
"connectedTo": [
|
|
77
|
+
"pcb_smtpad_3",
|
|
78
|
+
"connectivity_net0",
|
|
79
|
+
"source_trace_0",
|
|
80
|
+
"source_port_1",
|
|
81
|
+
"source_port_3",
|
|
82
|
+
"pcb_smtpad_1",
|
|
83
|
+
"pcb_port_1",
|
|
84
|
+
"pcb_smtpad_3",
|
|
85
|
+
"pcb_port_3"
|
|
86
|
+
],
|
|
87
|
+
"zLayers": [0]
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
"connections": [
|
|
91
|
+
{
|
|
92
|
+
"name": "source_trace_0",
|
|
93
|
+
"source_trace_id": "source_trace_0",
|
|
94
|
+
"pointsToConnect": [
|
|
95
|
+
{
|
|
96
|
+
"x": -4.49,
|
|
97
|
+
"y": -4,
|
|
98
|
+
"layer": "top",
|
|
99
|
+
"pointId": "pcb_port_1",
|
|
100
|
+
"pcb_port_id": "pcb_port_1"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"x": 5.51,
|
|
104
|
+
"y": -4,
|
|
105
|
+
"layer": "top",
|
|
106
|
+
"pointId": "pcb_port_3",
|
|
107
|
+
"pcb_port_id": "pcb_port_3"
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
"layerCount": 2,
|
|
113
|
+
"minTraceWidth": 0.15,
|
|
114
|
+
"outline": [
|
|
115
|
+
{
|
|
116
|
+
"x": -8,
|
|
117
|
+
"y": -6
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"x": -2,
|
|
121
|
+
"y": -6
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"x": -2,
|
|
125
|
+
"y": 2
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"x": 2,
|
|
129
|
+
"y": 2
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"x": 2,
|
|
133
|
+
"y": -6
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"x": 8,
|
|
137
|
+
"y": -6
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"x": 8,
|
|
141
|
+
"y": 6
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"x": -8,
|
|
145
|
+
"y": 6
|
|
146
|
+
}
|
|
147
|
+
]
|
|
148
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<svg width="640" height="640" viewBox="0 0 640 640" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="white"/><g><polyline data-points="-8,-6 -2,-6 -2,2 2,2 2,-6 8,-6 8,6 -8,6 -8,-6" data-type="line" data-label="outline" points="40,530 250,530 250,250.00000000000006 390,250.00000000000006 390,530 600,530 600,110.00000000000006 40,110.00000000000006 40,530" fill="none" stroke="#111827" stroke-width="0.35000000000000003"/></g><g><rect data-type="rect" data-label="obstacle" data-x="-5.51" data-y="-4" x="117.70000000000002" y="448.80000000000007" width="18.899999999999977" height="22.399999999999977" fill="#fee2e2" stroke="#ef4444" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="obstacle" data-x="-4.49" data-y="-4" x="153.4" y="448.80000000000007" width="18.899999999999977" height="22.399999999999977" fill="#fee2e2" stroke="#ef4444" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="obstacle" data-x="4.49" data-y="-4" x="467.70000000000005" y="448.80000000000007" width="18.899999999999977" height="22.399999999999977" fill="#fee2e2" stroke="#ef4444" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="obstacle" data-x="5.51" data-y="-4" x="503.4" y="448.80000000000007" width="18.899999999999977" height="22.399999999999977" fill="#fee2e2" stroke="#ef4444" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="void" data-x="0" data-y="-2" x="250" y="250.00000000000006" width="140" height="279.99999999999994" fill="rgba(0, 0, 0, 0.5)" stroke="none" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="free
|
|
2
|
+
z:0,1" data-x="-5" data-y="1.1600000000000006" x="40" y="110" width="210" height="338.80000000000007" fill="#dbeafe" stroke="#3b82f6" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="free
|
|
3
|
+
z:0,1" data-x="5" data-y="1.1600000000000006" x="390" y="110" width="210" height="338.80000000000007" fill="#dbeafe" stroke="#3b82f6" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="free
|
|
4
|
+
z:0,1" data-x="0" data-y="4" x="250" y="110.00000000000006" width="140" height="140" fill="#dbeafe" stroke="#3b82f6" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="free
|
|
5
|
+
z:0,1" data-x="-5.48" data-y="-5.16" x="40" y="471.20000000000005" width="176.39999999999998" height="58.799999999999955" fill="#dbeafe" stroke="#3b82f6" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="free
|
|
6
|
+
z:0,1" data-x="5.48" data-y="-5.16" x="423.6" y="471.20000000000005" width="176.39999999999998" height="58.799999999999955" fill="#dbeafe" stroke="#3b82f6" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="free
|
|
7
|
+
z:0,1" data-x="-2.4800000000000004" data-y="-4.84" x="216.39999999999998" y="448.80000000000007" width="33.60000000000002" height="81.19999999999993" fill="#dbeafe" stroke="#3b82f6" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="free
|
|
8
|
+
z:0,1" data-x="2.4800000000000004" data-y="-4.84" x="390" y="448.80000000000007" width="33.60000000000002" height="81.19999999999993" fill="#dbeafe" stroke="#3b82f6" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="free
|
|
9
|
+
z:1" data-x="-5.48" data-y="-4" x="40" y="448.80000000000007" width="176.39999999999998" height="22.399999999999977" fill="#fef3c7" stroke="#f59e0b" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="free
|
|
10
|
+
z:1" data-x="5.48" data-y="-4" x="423.6" y="448.80000000000007" width="176.39999999999998" height="22.399999999999977" fill="#fef3c7" stroke="#f59e0b" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="free
|
|
11
|
+
z:0" data-x="6.890000000000001" data-y="-4" x="522.3" y="448.80000000000007" width="77.70000000000005" height="22.399999999999977" fill="#dbeafe" stroke="#3b82f6" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="free
|
|
12
|
+
z:0" data-x="-6.89" data-y="-4" x="40" y="448.80000000000007" width="77.70000000000002" height="22.399999999999977" fill="#dbeafe" stroke="#3b82f6" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="free
|
|
13
|
+
z:0" data-x="-5" data-y="-4" x="136.6" y="448.80000000000007" width="16.80000000000001" height="22.399999999999977" fill="#dbeafe" stroke="#3b82f6" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="free
|
|
14
|
+
z:0" data-x="-3.5900000000000003" data-y="-4" x="172.3" y="448.80000000000007" width="44.099999999999966" height="22.399999999999977" fill="#dbeafe" stroke="#3b82f6" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="free
|
|
15
|
+
z:0" data-x="3.5900000000000007" data-y="-4" x="423.6" y="448.80000000000007" width="44.10000000000002" height="22.399999999999977" fill="#dbeafe" stroke="#3b82f6" stroke-width="0.02857142857142857"/></g><g><rect data-type="rect" data-label="free
|
|
16
|
+
z:0" data-x="5" data-y="-4" x="486.6" y="448.80000000000007" width="16.799999999999955" height="22.399999999999977" fill="#dbeafe" stroke="#3b82f6" stroke-width="0.02857142857142857"/></g><g id="crosshair" style="display: none"><line id="crosshair-h" y1="0" y2="640" stroke="#666" stroke-width="0.5"/><line id="crosshair-v" x1="0" x2="640" stroke="#666" stroke-width="0.5"/><text id="coordinates" font-family="monospace" font-size="12" fill="#666"></text></g><script><![CDATA[
|
|
17
|
+
document.currentScript.parentElement.addEventListener('mousemove', (e) => {
|
|
18
|
+
const svg = e.currentTarget;
|
|
19
|
+
const rect = svg.getBoundingClientRect();
|
|
20
|
+
const x = e.clientX - rect.left;
|
|
21
|
+
const y = e.clientY - rect.top;
|
|
22
|
+
const crosshair = svg.getElementById('crosshair');
|
|
23
|
+
const h = svg.getElementById('crosshair-h');
|
|
24
|
+
const v = svg.getElementById('crosshair-v');
|
|
25
|
+
const coords = svg.getElementById('coordinates');
|
|
26
|
+
|
|
27
|
+
crosshair.style.display = 'block';
|
|
28
|
+
h.setAttribute('x1', '0');
|
|
29
|
+
h.setAttribute('x2', '640');
|
|
30
|
+
h.setAttribute('y1', y);
|
|
31
|
+
h.setAttribute('y2', y);
|
|
32
|
+
v.setAttribute('x1', x);
|
|
33
|
+
v.setAttribute('x2', x);
|
|
34
|
+
v.setAttribute('y1', '0');
|
|
35
|
+
v.setAttribute('y2', '640');
|
|
36
|
+
|
|
37
|
+
// Calculate real coordinates using inverse transformation
|
|
38
|
+
const matrix = {"a":35,"c":0,"e":320,"b":0,"d":-35,"f":320.00000000000006};
|
|
39
|
+
// Manually invert and apply the affine transform
|
|
40
|
+
// Since we only use translate and scale, we can directly compute:
|
|
41
|
+
// x' = (x - tx) / sx
|
|
42
|
+
// y' = (y - ty) / sy
|
|
43
|
+
const sx = matrix.a;
|
|
44
|
+
const sy = matrix.d;
|
|
45
|
+
const tx = matrix.e;
|
|
46
|
+
const ty = matrix.f;
|
|
47
|
+
const realPoint = {
|
|
48
|
+
x: (x - tx) / sx,
|
|
49
|
+
y: (y - ty) / sy // Flip y back since we used negative scale
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
coords.textContent = `(${realPoint.x.toFixed(2)}, ${realPoint.y.toFixed(2)})`;
|
|
53
|
+
coords.setAttribute('x', (x + 5).toString());
|
|
54
|
+
coords.setAttribute('y', (y - 5).toString());
|
|
55
|
+
});
|
|
56
|
+
document.currentScript.parentElement.addEventListener('mouseleave', () => {
|
|
57
|
+
document.currentScript.parentElement.getElementById('crosshair').style.display = 'none';
|
|
58
|
+
});
|
|
59
|
+
]]></script></svg>
|