@tscircuit/rectdiff 0.0.1 → 0.0.2
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/.github/workflows/bun-formatcheck.yml +1 -1
- package/.github/workflows/bun-pver-release.yml +2 -2
- package/.github/workflows/bun-test.yml +1 -1
- package/.github/workflows/bun-typecheck.yml +1 -1
- package/README.md +139 -0
- package/dist/index.js +195 -37
- package/global.d.ts +2 -2
- package/lib/solvers/RectDiffSolver.ts +15 -3
- package/lib/solvers/rectdiff/candidates.ts +158 -43
- package/lib/solvers/rectdiff/engine.ts +108 -29
- package/lib/solvers/rectdiff/geometry.ts +81 -28
- package/lib/solvers/rectdiff/layers.ts +11 -3
- package/lib/solvers/rectdiff/rectsToMeshNodes.ts +3 -0
- package/lib/solvers/rectdiff/types.ts +4 -1
- package/package.json +6 -2
- package/pages/bugreport11.page.tsx +16 -0
- package/pages/example01.page.tsx +1 -1
- package/test-assets/bugreport11-b2de3c.json +4315 -0
- package/tests/examples/example01.test.tsx +2 -2
- package/tests/fixtures/preload.ts +1 -1
- package/tests/rect-diff-solver.test.ts +6 -5
- package/tests/svg.test.ts +10 -11
- package/.claude/settings.local.json +0 -9
- package/bun.lock +0 -29
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import type { XYRect } from "./types"
|
|
3
3
|
|
|
4
4
|
export const EPS = 1e-9
|
|
5
|
-
export const clamp = (v: number, lo: number, hi: number) =>
|
|
5
|
+
export const clamp = (v: number, lo: number, hi: number) =>
|
|
6
|
+
Math.max(lo, Math.min(hi, v))
|
|
6
7
|
export const gt = (a: number, b: number) => a > b + EPS
|
|
7
8
|
export const gte = (a: number, b: number) => a > b - EPS
|
|
8
9
|
export const lt = (a: number, b: number) => a < b - EPS
|
|
@@ -19,8 +20,10 @@ export function overlaps(a: XYRect, b: XYRect) {
|
|
|
19
20
|
|
|
20
21
|
export function containsPoint(r: XYRect, x: number, y: number) {
|
|
21
22
|
return (
|
|
22
|
-
x >= r.x - EPS &&
|
|
23
|
-
|
|
23
|
+
x >= r.x - EPS &&
|
|
24
|
+
x <= r.x + r.width + EPS &&
|
|
25
|
+
y >= r.y - EPS &&
|
|
26
|
+
y <= r.y + r.height + EPS
|
|
24
27
|
)
|
|
25
28
|
}
|
|
26
29
|
|
|
@@ -33,7 +36,10 @@ export function distancePointToRectEdges(px: number, py: number, r: XYRect) {
|
|
|
33
36
|
]
|
|
34
37
|
let best = Infinity
|
|
35
38
|
for (const [x1, y1, x2, y2] of edges) {
|
|
36
|
-
const A = px - x1,
|
|
39
|
+
const A = px - x1,
|
|
40
|
+
B = py - y1,
|
|
41
|
+
C = x2 - x1,
|
|
42
|
+
D = y2 - y1
|
|
37
43
|
const dot = A * C + B * D
|
|
38
44
|
const lenSq = C * C + D * D
|
|
39
45
|
let t = lenSq !== 0 ? dot / lenSq : 0
|
|
@@ -48,7 +54,10 @@ export function distancePointToRectEdges(px: number, py: number, r: XYRect) {
|
|
|
48
54
|
// --- directional expansion caps (respect board + blockers + aspect) ---
|
|
49
55
|
|
|
50
56
|
function maxExpandRight(
|
|
51
|
-
r: XYRect,
|
|
57
|
+
r: XYRect,
|
|
58
|
+
bounds: XYRect,
|
|
59
|
+
blockers: XYRect[],
|
|
60
|
+
maxAspect: number | null | undefined,
|
|
52
61
|
) {
|
|
53
62
|
// Start with board boundary
|
|
54
63
|
let maxWidth = bounds.x + bounds.width - r.x
|
|
@@ -56,14 +65,18 @@ function maxExpandRight(
|
|
|
56
65
|
// Check all blockers that could limit rightward expansion
|
|
57
66
|
for (const b of blockers) {
|
|
58
67
|
// Only consider blockers that vertically overlap with current rect
|
|
59
|
-
const verticallyOverlaps =
|
|
68
|
+
const verticallyOverlaps =
|
|
69
|
+
r.y + r.height > b.y + EPS && b.y + b.height > r.y + EPS
|
|
60
70
|
if (verticallyOverlaps) {
|
|
61
71
|
// Blocker is to the right - limits how far we can expand
|
|
62
72
|
if (gte(b.x, r.x + r.width)) {
|
|
63
73
|
maxWidth = Math.min(maxWidth, b.x - r.x)
|
|
64
74
|
}
|
|
65
75
|
// Blocker overlaps current position - can't expand at all
|
|
66
|
-
else if (
|
|
76
|
+
else if (
|
|
77
|
+
b.x + b.width > r.x + r.width - EPS &&
|
|
78
|
+
b.x < r.x + r.width + EPS
|
|
79
|
+
) {
|
|
67
80
|
return 0
|
|
68
81
|
}
|
|
69
82
|
}
|
|
@@ -74,14 +87,18 @@ function maxExpandRight(
|
|
|
74
87
|
|
|
75
88
|
// Apply aspect ratio constraint
|
|
76
89
|
if (maxAspect != null) {
|
|
77
|
-
const w = r.width,
|
|
90
|
+
const w = r.width,
|
|
91
|
+
h = r.height
|
|
78
92
|
if (w >= h) e = Math.min(e, maxAspect * h - w)
|
|
79
93
|
}
|
|
80
94
|
return Math.max(0, e)
|
|
81
95
|
}
|
|
82
96
|
|
|
83
97
|
function maxExpandDown(
|
|
84
|
-
r: XYRect,
|
|
98
|
+
r: XYRect,
|
|
99
|
+
bounds: XYRect,
|
|
100
|
+
blockers: XYRect[],
|
|
101
|
+
maxAspect: number | null | undefined,
|
|
85
102
|
) {
|
|
86
103
|
// Start with board boundary
|
|
87
104
|
let maxHeight = bounds.y + bounds.height - r.y
|
|
@@ -96,7 +113,10 @@ function maxExpandDown(
|
|
|
96
113
|
maxHeight = Math.min(maxHeight, b.y - r.y)
|
|
97
114
|
}
|
|
98
115
|
// Blocker overlaps current position - can't expand at all
|
|
99
|
-
else if (
|
|
116
|
+
else if (
|
|
117
|
+
b.y + b.height > r.y + r.height - EPS &&
|
|
118
|
+
b.y < r.y + r.height + EPS
|
|
119
|
+
) {
|
|
100
120
|
return 0
|
|
101
121
|
}
|
|
102
122
|
}
|
|
@@ -107,14 +127,18 @@ function maxExpandDown(
|
|
|
107
127
|
|
|
108
128
|
// Apply aspect ratio constraint
|
|
109
129
|
if (maxAspect != null) {
|
|
110
|
-
const w = r.width,
|
|
130
|
+
const w = r.width,
|
|
131
|
+
h = r.height
|
|
111
132
|
if (h >= w) e = Math.min(e, maxAspect * w - h)
|
|
112
133
|
}
|
|
113
134
|
return Math.max(0, e)
|
|
114
135
|
}
|
|
115
136
|
|
|
116
137
|
function maxExpandLeft(
|
|
117
|
-
r: XYRect,
|
|
138
|
+
r: XYRect,
|
|
139
|
+
bounds: XYRect,
|
|
140
|
+
blockers: XYRect[],
|
|
141
|
+
maxAspect: number | null | undefined,
|
|
118
142
|
) {
|
|
119
143
|
// Start with board boundary
|
|
120
144
|
let minX = bounds.x
|
|
@@ -122,7 +146,8 @@ function maxExpandLeft(
|
|
|
122
146
|
// Check all blockers that could limit leftward expansion
|
|
123
147
|
for (const b of blockers) {
|
|
124
148
|
// Only consider blockers that vertically overlap with current rect
|
|
125
|
-
const verticallyOverlaps =
|
|
149
|
+
const verticallyOverlaps =
|
|
150
|
+
r.y + r.height > b.y + EPS && b.y + b.height > r.y + EPS
|
|
126
151
|
if (verticallyOverlaps) {
|
|
127
152
|
// Blocker is to the left - limits how far we can expand
|
|
128
153
|
if (lte(b.x + b.width, r.x)) {
|
|
@@ -140,14 +165,18 @@ function maxExpandLeft(
|
|
|
140
165
|
|
|
141
166
|
// Apply aspect ratio constraint
|
|
142
167
|
if (maxAspect != null) {
|
|
143
|
-
const w = r.width,
|
|
168
|
+
const w = r.width,
|
|
169
|
+
h = r.height
|
|
144
170
|
if (w >= h) e = Math.min(e, maxAspect * h - w)
|
|
145
171
|
}
|
|
146
172
|
return Math.max(0, e)
|
|
147
173
|
}
|
|
148
174
|
|
|
149
175
|
function maxExpandUp(
|
|
150
|
-
r: XYRect,
|
|
176
|
+
r: XYRect,
|
|
177
|
+
bounds: XYRect,
|
|
178
|
+
blockers: XYRect[],
|
|
179
|
+
maxAspect: number | null | undefined,
|
|
151
180
|
) {
|
|
152
181
|
// Start with board boundary
|
|
153
182
|
let minY = bounds.y
|
|
@@ -173,7 +202,8 @@ function maxExpandUp(
|
|
|
173
202
|
|
|
174
203
|
// Apply aspect ratio constraint
|
|
175
204
|
if (maxAspect != null) {
|
|
176
|
-
const w = r.width,
|
|
205
|
+
const w = r.width,
|
|
206
|
+
h = r.height
|
|
177
207
|
if (h >= w) e = Math.min(e, maxAspect * w - h)
|
|
178
208
|
}
|
|
179
209
|
return Math.max(0, e)
|
|
@@ -206,12 +236,20 @@ export function expandRectFromSeed(
|
|
|
206
236
|
let bestArea = 0
|
|
207
237
|
|
|
208
238
|
STRATS: for (const s of strategies) {
|
|
209
|
-
let r: XYRect = {
|
|
239
|
+
let r: XYRect = {
|
|
240
|
+
x: startX + s.ox,
|
|
241
|
+
y: startY + s.oy,
|
|
242
|
+
width: initialW,
|
|
243
|
+
height: initialH,
|
|
244
|
+
}
|
|
210
245
|
|
|
211
246
|
// keep initial inside board
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
247
|
+
if (
|
|
248
|
+
lt(r.x, bounds.x) ||
|
|
249
|
+
lt(r.y, bounds.y) ||
|
|
250
|
+
gt(r.x + r.width, bounds.x + bounds.width) ||
|
|
251
|
+
gt(r.y + r.height, bounds.y + bounds.height)
|
|
252
|
+
) {
|
|
215
253
|
continue
|
|
216
254
|
}
|
|
217
255
|
|
|
@@ -223,21 +261,36 @@ export function expandRectFromSeed(
|
|
|
223
261
|
while (improved) {
|
|
224
262
|
improved = false
|
|
225
263
|
const eR = maxExpandRight(r, bounds, blockers, maxAspectRatio)
|
|
226
|
-
if (eR > 0) {
|
|
264
|
+
if (eR > 0) {
|
|
265
|
+
r = { ...r, width: r.width + eR }
|
|
266
|
+
improved = true
|
|
267
|
+
}
|
|
227
268
|
|
|
228
269
|
const eD = maxExpandDown(r, bounds, blockers, maxAspectRatio)
|
|
229
|
-
if (eD > 0) {
|
|
270
|
+
if (eD > 0) {
|
|
271
|
+
r = { ...r, height: r.height + eD }
|
|
272
|
+
improved = true
|
|
273
|
+
}
|
|
230
274
|
|
|
231
275
|
const eL = maxExpandLeft(r, bounds, blockers, maxAspectRatio)
|
|
232
|
-
if (eL > 0) {
|
|
276
|
+
if (eL > 0) {
|
|
277
|
+
r = { x: r.x - eL, y: r.y, width: r.width + eL, height: r.height }
|
|
278
|
+
improved = true
|
|
279
|
+
}
|
|
233
280
|
|
|
234
281
|
const eU = maxExpandUp(r, bounds, blockers, maxAspectRatio)
|
|
235
|
-
if (eU > 0) {
|
|
282
|
+
if (eU > 0) {
|
|
283
|
+
r = { x: r.x, y: r.y - eU, width: r.width, height: r.height + eU }
|
|
284
|
+
improved = true
|
|
285
|
+
}
|
|
236
286
|
}
|
|
237
287
|
|
|
238
288
|
if (r.width + EPS >= minReq.width && r.height + EPS >= minReq.height) {
|
|
239
289
|
const area = r.width * r.height
|
|
240
|
-
if (area > bestArea) {
|
|
290
|
+
if (area > bestArea) {
|
|
291
|
+
best = r
|
|
292
|
+
bestArea = area
|
|
293
|
+
}
|
|
241
294
|
}
|
|
242
295
|
}
|
|
243
296
|
|
|
@@ -247,7 +300,7 @@ export function expandRectFromSeed(
|
|
|
247
300
|
export function intersect1D(a0: number, a1: number, b0: number, b1: number) {
|
|
248
301
|
const lo = Math.max(a0, b0)
|
|
249
302
|
const hi = Math.min(a1, b1)
|
|
250
|
-
return hi > lo + EPS ? [lo, hi] as const : null
|
|
303
|
+
return hi > lo + EPS ? ([lo, hi] as const) : null
|
|
251
304
|
}
|
|
252
305
|
|
|
253
306
|
/** Return A \ B as up to 4 non-overlapping rectangles (or [A] if no overlap). */
|
|
@@ -268,7 +321,7 @@ export function subtractRect2D(A: XYRect, B: XYRect): XYRect[] {
|
|
|
268
321
|
}
|
|
269
322
|
// Right strip
|
|
270
323
|
if (A.x + A.width > X1 + EPS) {
|
|
271
|
-
out.push({ x: X1, y: A.y, width:
|
|
324
|
+
out.push({ x: X1, y: A.y, width: A.x + A.width - X1, height: A.height })
|
|
272
325
|
}
|
|
273
326
|
// Top wedge in the middle band
|
|
274
327
|
const midW = Math.max(0, X1 - X0)
|
|
@@ -277,7 +330,7 @@ export function subtractRect2D(A: XYRect, B: XYRect): XYRect[] {
|
|
|
277
330
|
}
|
|
278
331
|
// Bottom wedge in the middle band
|
|
279
332
|
if (midW > EPS && A.y + A.height > Y1 + EPS) {
|
|
280
|
-
out.push({ x: X0, y: Y1, width: midW, height:
|
|
333
|
+
out.push({ x: X0, y: Y1, width: midW, height: A.y + A.height - Y1 })
|
|
281
334
|
}
|
|
282
335
|
|
|
283
336
|
return out.filter((r) => r.width > EPS && r.height > EPS)
|
|
@@ -21,10 +21,17 @@ export function canonicalizeLayerOrder(names: string[]) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export function buildZIndexMap(srj: SimpleRouteJson) {
|
|
24
|
-
const names = canonicalizeLayerOrder(
|
|
24
|
+
const names = canonicalizeLayerOrder(
|
|
25
|
+
(srj.obstacles ?? []).flatMap((o) => o.layers ?? []),
|
|
26
|
+
)
|
|
25
27
|
const fallback = Array.from(
|
|
26
28
|
{ length: Math.max(1, srj.layerCount || 1) },
|
|
27
|
-
(_, i) =>
|
|
29
|
+
(_, i) =>
|
|
30
|
+
i === 0
|
|
31
|
+
? "top"
|
|
32
|
+
: i === (srj.layerCount || 1) - 1
|
|
33
|
+
? "bottom"
|
|
34
|
+
: `inner${i}`,
|
|
28
35
|
)
|
|
29
36
|
const layerNames = names.length ? names : fallback
|
|
30
37
|
const map = new Map<string, number>()
|
|
@@ -33,7 +40,8 @@ export function buildZIndexMap(srj: SimpleRouteJson) {
|
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
export function obstacleZs(ob: Obstacle, zIndexByName: Map<string, number>) {
|
|
36
|
-
if (ob.zLayers?.length)
|
|
43
|
+
if (ob.zLayers?.length)
|
|
44
|
+
return Array.from(new Set(ob.zLayers)).sort((a, b) => a - b)
|
|
37
45
|
const fromNames = (ob.layers ?? [])
|
|
38
46
|
.map((n) => zIndexByName.get(n))
|
|
39
47
|
.filter((v): v is number => typeof v === "number")
|
|
@@ -9,6 +9,7 @@ export type Rect3d = {
|
|
|
9
9
|
maxX: number
|
|
10
10
|
maxY: number
|
|
11
11
|
zLayers: number[] // sorted contiguous integers
|
|
12
|
+
isObstacle?: boolean
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export type GridFill3DOptions = {
|
|
@@ -41,7 +42,9 @@ export type RectDiffState = {
|
|
|
41
42
|
layerNames: string[]
|
|
42
43
|
layerCount: number
|
|
43
44
|
bounds: XYRect
|
|
44
|
-
options: Required<
|
|
45
|
+
options: Required<
|
|
46
|
+
Omit<GridFill3DOptions, "gridSizes" | "maxMultiLayerSpan">
|
|
47
|
+
> & {
|
|
45
48
|
gridSizes: number[]
|
|
46
49
|
maxMultiLayerSpan: number | undefined
|
|
47
50
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tscircuit/rectdiff",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"start": "cosmos",
|
|
8
|
-
"build": "tsup-node ./lib/index.ts --format esm --dts"
|
|
8
|
+
"build": "tsup-node ./lib/index.ts --format esm --dts",
|
|
9
|
+
"build:site": "cosmos-export",
|
|
10
|
+
"format": "biome format --write .",
|
|
11
|
+
"format:check": "biome format ."
|
|
9
12
|
},
|
|
10
13
|
"devDependencies": {
|
|
11
14
|
"@biomejs/biome": "^2.3.5",
|
|
@@ -13,6 +16,7 @@
|
|
|
13
16
|
"@tscircuit/solver-utils": "^0.0.3",
|
|
14
17
|
"@types/bun": "latest",
|
|
15
18
|
"@types/three": "^0.181.0",
|
|
19
|
+
"biome": "^0.3.3",
|
|
16
20
|
"bun-match-svg": "^0.0.14",
|
|
17
21
|
"graphics-debug": "^0.0.70",
|
|
18
22
|
"rbush": "^4.0.1",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import simpleRouteJson from "../test-assets/bugreport11-b2de3c.json"
|
|
2
|
+
import { RectDiffSolver } from "../lib/solvers/RectDiffSolver"
|
|
3
|
+
import { useMemo } from "react"
|
|
4
|
+
import { SolverDebugger3d } from "../components/SolverDebugger3d"
|
|
5
|
+
|
|
6
|
+
export default () => {
|
|
7
|
+
const solver = useMemo(
|
|
8
|
+
() =>
|
|
9
|
+
new RectDiffSolver({
|
|
10
|
+
simpleRouteJson: simpleRouteJson.simple_route_json,
|
|
11
|
+
}),
|
|
12
|
+
[],
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
return <SolverDebugger3d solver={solver} />
|
|
16
|
+
}
|
package/pages/example01.page.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GenericSolverDebugger } from "@tscircuit/solver-utils/react"
|
|
2
|
-
import simpleRouteJson from "../test-assets/
|
|
2
|
+
import simpleRouteJson from "../test-assets/example01.json"
|
|
3
3
|
import { RectDiffSolver } from "../lib/solvers/RectDiffSolver"
|
|
4
4
|
import { useMemo } from "react"
|
|
5
5
|
import { SolverDebugger3d } from "../components/SolverDebugger3d"
|