@tscircuit/rectdiff 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.
@@ -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) => Math.max(lo, Math.min(hi, v))
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 && x <= r.x + r.width + EPS &&
23
- y >= r.y - EPS && y <= r.y + r.height + EPS
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, B = py - y1, C = x2 - x1, D = y2 - y1
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, bounds: XYRect, blockers: XYRect[], maxAspect: number | null | undefined
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 = r.y + r.height > b.y + EPS && b.y + b.height > r.y + EPS
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 (b.x + b.width > r.x + r.width - EPS && b.x < r.x + r.width + EPS) {
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, h = r.height
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, bounds: XYRect, blockers: XYRect[], maxAspect: number | null | undefined
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 (b.y + b.height > r.y + r.height - EPS && b.y < r.y + r.height + EPS) {
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, h = r.height
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, bounds: XYRect, blockers: XYRect[], maxAspect: number | null | undefined
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 = r.y + r.height > b.y + EPS && b.y + b.height > r.y + EPS
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, h = r.height
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, bounds: XYRect, blockers: XYRect[], maxAspect: number | null | undefined
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, h = r.height
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 = { x: startX + s.ox, y: startY + s.oy, width: initialW, height: initialH }
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 (lt(r.x, bounds.x) || lt(r.y, bounds.y) ||
213
- gt(r.x + r.width, bounds.x + bounds.width) ||
214
- gt(r.y + r.height, bounds.y + bounds.height)) {
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) { r = { ...r, width: r.width + eR }; improved = true }
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) { r = { ...r, height: r.height + eD }; improved = true }
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) { r = { x: r.x - eL, y: r.y, width: r.width + eL, height: r.height }; improved = true }
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) { r = { x: r.x, y: r.y - eU, width: r.width, height: r.height + eU }; improved = true }
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) { best = r; bestArea = area }
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: (A.x + A.width) - X1, height: A.height })
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: (A.y + A.height) - Y1 })
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,21 +21,57 @@ export function canonicalizeLayerOrder(names: string[]) {
21
21
  }
22
22
 
23
23
  export function buildZIndexMap(srj: SimpleRouteJson) {
24
- const names = canonicalizeLayerOrder((srj.obstacles ?? []).flatMap((o) => o.layers ?? []))
25
- const fallback = Array.from(
26
- { length: Math.max(1, srj.layerCount || 1) },
27
- (_, i) => (i === 0 ? "top" : i === (srj.layerCount || 1) - 1 ? "bottom" : `inner${i}`),
24
+ const names = canonicalizeLayerOrder(
25
+ (srj.obstacles ?? []).flatMap((o) => o.layers ?? []),
28
26
  )
29
- const layerNames = names.length ? names : fallback
27
+ const declaredLayerCount = Math.max(1, srj.layerCount || names.length || 1)
28
+ const fallback = Array.from({ length: declaredLayerCount }, (_, i) =>
29
+ i === 0 ? "top" : i === declaredLayerCount - 1 ? "bottom" : `inner${i}`,
30
+ )
31
+ const ordered: string[] = []
32
+ const seen = new Set<string>()
33
+ const push = (n: string) => {
34
+ const key = n.toLowerCase()
35
+ if (seen.has(key)) return
36
+ seen.add(key)
37
+ ordered.push(n)
38
+ }
39
+ fallback.forEach(push)
40
+ names.forEach(push)
41
+ const layerNames = ordered.slice(0, declaredLayerCount)
42
+ // Clamp any exotic layer names (extra inner layers, mixed casing, etc.)
43
+ // onto the declared layerCount so every obstacle resolves to a legal z index.
44
+ const clampIndex = (nameLower: string) => {
45
+ if (layerNames.length <= 1) return 0
46
+ if (nameLower === "top") return 0
47
+ if (nameLower === "bottom") return layerNames.length - 1
48
+ const m = /^inner(\d+)$/i.exec(nameLower)
49
+ if (m) {
50
+ if (layerNames.length <= 2) return layerNames.length - 1
51
+ const parsed = parseInt(m[1]!, 10)
52
+ const maxInner = layerNames.length - 2
53
+ const clampedInner = Math.min(
54
+ maxInner,
55
+ Math.max(1, Number.isFinite(parsed) ? parsed : 1),
56
+ )
57
+ return clampedInner
58
+ }
59
+ return 0
60
+ }
30
61
  const map = new Map<string, number>()
31
- layerNames.forEach((n, i) => map.set(n, i))
62
+ layerNames.forEach((n, i) => map.set(n.toLowerCase(), i))
63
+ ordered.slice(layerNames.length).forEach((n) => {
64
+ const key = n.toLowerCase()
65
+ map.set(key, clampIndex(key))
66
+ })
32
67
  return { layerNames, zIndexByName: map }
33
68
  }
34
69
 
35
70
  export function obstacleZs(ob: Obstacle, zIndexByName: Map<string, number>) {
36
- if (ob.zLayers?.length) return Array.from(new Set(ob.zLayers)).sort((a, b) => a - b)
71
+ if (ob.zLayers?.length)
72
+ return Array.from(new Set(ob.zLayers)).sort((a, b) => a - b)
37
73
  const fromNames = (ob.layers ?? [])
38
- .map((n) => zIndexByName.get(n))
74
+ .map((n) => zIndexByName.get(n.toLowerCase()))
39
75
  .filter((v): v is number => typeof v === "number")
40
76
  return Array.from(new Set(fromNames)).sort((a, b) => a - b)
41
77
  }
@@ -16,7 +16,10 @@ export function rectsToMeshNodes(rects: Rect3d[]): CapacityMeshNode[] {
16
16
  height: h,
17
17
  layer: "top",
18
18
  availableZ: r.zLayers.slice(),
19
+ _containsObstacle: r.isObstacle,
20
+ _containsTarget: r.isObstacle,
19
21
  })
20
22
  }
23
+
21
24
  return out
22
25
  }
@@ -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<Omit<GridFill3DOptions, "gridSizes" | "maxMultiLayerSpan">> & {
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.1",
3
+ "version": "0.0.3",
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
+ }
@@ -1,5 +1,5 @@
1
1
  import { GenericSolverDebugger } from "@tscircuit/solver-utils/react"
2
- import simpleRouteJson from "../test-assets/example-simple-route.json"
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"