@tscircuit/rectdiff 0.0.17 → 0.0.19

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.
@@ -3,28 +3,21 @@ import type { GraphicsObject } from "graphics-debug"
3
3
  import type { CapacityMeshNode, RTreeRect } from "lib/types/capacity-mesh-types"
4
4
  import { expandRectFromSeed } from "../../utils/expandRectFromSeed"
5
5
  import { finalizeRects } from "../../utils/finalizeRects"
6
- import { allLayerNode } from "../../utils/buildHardPlacedByLayer"
7
6
  import { resizeSoftOverlaps } from "../../utils/resizeSoftOverlaps"
8
7
  import { rectsToMeshNodes } from "./rectsToMeshNodes"
9
8
  import type { XYRect, Candidate3D, Placed3D } from "../../rectdiff-types"
10
9
  import type { SimpleRouteJson } from "lib/types/srj-types"
11
- import {
12
- buildZIndexMap,
13
- obstacleToXYRect,
14
- obstacleZs,
15
- } from "../RectDiffSeedingSolver/layers"
16
10
  import RBush from "rbush"
17
11
  import { rectToTree } from "../../utils/rectToTree"
18
12
  import { sameTreeRect } from "../../utils/sameTreeRect"
19
13
 
20
- export type RectDiffExpansionSolverSnapshot = {
14
+ export type RectDiffExpansionSolverInput = {
21
15
  srj: SimpleRouteJson
22
16
  layerNames: string[]
23
17
  layerCount: number
24
18
  bounds: XYRect
25
19
  options: {
26
20
  gridSizes: number[]
27
- // the engine only uses gridSizes here, other options are ignored
28
21
  [key: string]: any
29
22
  }
30
23
  boardVoidRects: XYRect[]
@@ -35,10 +28,6 @@ export type RectDiffExpansionSolverSnapshot = {
35
28
  edgeAnalysisDone: boolean
36
29
  totalSeedsThisGrid: number
37
30
  consumedSeedsThisGrid: number
38
- }
39
-
40
- export type RectDiffExpansionSolverInput = {
41
- initialSnapshot: RectDiffExpansionSolverSnapshot
42
31
  obstacleIndexByLayer: Array<RBush<RTreeRect>>
43
32
  }
44
33
 
@@ -49,74 +38,25 @@ export type RectDiffExpansionSolverInput = {
49
38
  * and runs the EXPANSION phase, then finalizes to capacity mesh nodes.
50
39
  */
51
40
  export class RectDiffExpansionSolver extends BaseSolver {
52
- // Engine fields (same shape used by rectdiff/engine.ts)
53
- private srj!: SimpleRouteJson
54
- private layerNames!: string[]
55
- private layerCount!: number
56
- private bounds!: XYRect
57
- private options!: {
58
- gridSizes: number[]
59
- // the engine only uses gridSizes here, other options are ignored
60
- [key: string]: any
61
- }
62
- private boardVoidRects!: XYRect[]
63
- private gridIndex!: number
64
- private candidates!: Candidate3D[]
65
- private placed!: Placed3D[]
66
- private placedIndexByLayer!: Array<RBush<RTreeRect>>
67
- private expansionIndex!: number
68
- private edgeAnalysisDone!: boolean
69
- private totalSeedsThisGrid!: number
70
- private consumedSeedsThisGrid!: number
71
-
72
- private _meshNodes: CapacityMeshNode[] = []
73
-
41
+ placedIndexByLayer: Array<RBush<RTreeRect>> = []
42
+ _meshNodes: CapacityMeshNode[] = []
74
43
  constructor(private input: RectDiffExpansionSolverInput) {
75
44
  super()
76
- // Copy engine snapshot fields directly onto this solver instance
77
- Object.assign(this, this.input.initialSnapshot)
78
45
  }
79
46
 
80
47
  override _setup() {
81
48
  this.stats = {
82
- gridIndex: this.gridIndex,
83
- }
84
-
85
- if (this.input.obstacleIndexByLayer) {
86
- } else {
87
- const { zIndexByName } = buildZIndexMap(this.srj)
88
- this.input.obstacleIndexByLayer = Array.from(
89
- { length: this.layerCount },
90
- () => new RBush<RTreeRect>(),
91
- )
92
- const insertObstacle = (rect: XYRect, z: number) => {
93
- const tree = this.input.obstacleIndexByLayer[z]
94
- if (tree) tree.insert(rectToTree(rect))
95
- }
96
- for (const voidRect of this.boardVoidRects ?? []) {
97
- for (let z = 0; z < this.layerCount; z++) insertObstacle(voidRect, z)
98
- }
99
- for (const obstacle of this.srj.obstacles ?? []) {
100
- const rect = obstacleToXYRect(obstacle as any)
101
- if (!rect) continue
102
- const zLayers =
103
- obstacle.zLayers?.length && obstacle.zLayers.length > 0
104
- ? obstacle.zLayers
105
- : obstacleZs(obstacle as any, zIndexByName)
106
- zLayers.forEach((z) => {
107
- if (z >= 0 && z < this.layerCount) insertObstacle(rect, z)
108
- })
109
- }
49
+ gridIndex: this.input.gridIndex,
110
50
  }
111
51
 
112
52
  this.placedIndexByLayer = Array.from(
113
- { length: this.layerCount },
53
+ { length: this.input.layerCount },
114
54
  () => new RBush<RTreeRect>(),
115
55
  )
116
- for (const placement of this.placed ?? []) {
56
+ for (const placement of this.input.placed) {
117
57
  for (const z of placement.zLayers) {
118
- const tree = this.placedIndexByLayer[z]
119
- if (tree) tree.insert(rectToTree(placement.rect))
58
+ const placedIndex = this.placedIndexByLayer[z]
59
+ if (placedIndex) placedIndex.insert(rectToTree(placement.rect))
120
60
  }
121
61
  }
122
62
  }
@@ -126,51 +66,41 @@ export class RectDiffExpansionSolver extends BaseSolver {
126
66
 
127
67
  this._stepExpansion()
128
68
 
129
- this.stats.gridIndex = this.gridIndex
130
- this.stats.placed = this.placed.length
69
+ this.stats.gridIndex = this.input.gridIndex
70
+ this.stats.placed = this.input.placed.length
131
71
 
132
- if (this.expansionIndex >= this.placed.length) {
72
+ if (this.input.expansionIndex >= this.input.placed.length) {
133
73
  this.finalizeIfNeeded()
134
74
  }
135
75
  }
136
76
 
137
77
  private _stepExpansion(): void {
138
- if (this.expansionIndex >= this.placed.length) {
78
+ if (this.input.expansionIndex >= this.input.placed.length) {
139
79
  return
140
80
  }
141
81
 
142
- const idx = this.expansionIndex
143
- const p = this.placed[idx]!
144
- const lastGrid = this.options.gridSizes[this.options.gridSizes.length - 1]!
145
-
146
- const hardPlacedByLayer = allLayerNode({
147
- layerCount: this.layerCount,
148
- placed: this.placed,
149
- })
150
-
151
- // HARD blockers only: obstacles on p.zLayers + full-stack nodes
152
- const hardBlockers: XYRect[] = []
153
- for (const z of p.zLayers) {
154
- const obstacleTree = this.input.obstacleIndexByLayer[z]
155
- if (obstacleTree) hardBlockers.push(...obstacleTree.all())
156
- hardBlockers.push(...(hardPlacedByLayer[z] ?? []))
157
- }
82
+ const idx = this.input.expansionIndex
83
+ const p = this.input.placed[idx]!
84
+ const lastGrid =
85
+ this.input.options.gridSizes[this.input.options.gridSizes.length - 1]!
158
86
 
159
87
  const oldRect = p.rect
160
88
  const expanded = expandRectFromSeed({
161
89
  startX: p.rect.x + p.rect.width / 2,
162
90
  startY: p.rect.y + p.rect.height / 2,
163
91
  gridSize: lastGrid,
164
- bounds: this.bounds,
165
- blockers: hardBlockers,
92
+ bounds: this.input.bounds,
93
+ obsticalIndexByLayer: this.input.obstacleIndexByLayer,
94
+ placedIndexByLayer: this.placedIndexByLayer,
166
95
  initialCellRatio: 0,
167
96
  maxAspectRatio: null,
168
97
  minReq: { width: p.rect.width, height: p.rect.height },
98
+ zLayers: p.zLayers,
169
99
  })
170
100
 
171
101
  if (expanded) {
172
102
  // Update placement + per-layer index (replace old rect object)
173
- this.placed[idx] = { rect: expanded, zLayers: p.zLayers }
103
+ this.input.placed[idx] = { rect: expanded, zLayers: p.zLayers }
174
104
  for (const z of p.zLayers) {
175
105
  const tree = this.placedIndexByLayer[z]
176
106
  if (tree) {
@@ -182,25 +112,25 @@ export class RectDiffExpansionSolver extends BaseSolver {
182
112
  // Carve overlapped soft neighbors (respect full-stack nodes)
183
113
  resizeSoftOverlaps(
184
114
  {
185
- layerCount: this.layerCount,
186
- placed: this.placed,
187
- options: this.options,
115
+ layerCount: this.input.layerCount,
116
+ placed: this.input.placed,
117
+ options: this.input.options,
188
118
  placedIndexByLayer: this.placedIndexByLayer,
189
119
  },
190
120
  idx,
191
121
  )
192
122
  }
193
123
 
194
- this.expansionIndex += 1
124
+ this.input.expansionIndex += 1
195
125
  }
196
126
 
197
127
  private finalizeIfNeeded() {
198
128
  if (this.solved) return
199
129
 
200
130
  const rects = finalizeRects({
201
- placed: this.placed,
202
- srj: this.srj,
203
- boardVoidRects: this.boardVoidRects,
131
+ placed: this.input.placed,
132
+ srj: this.input.srj,
133
+ boardVoidRects: this.input.boardVoidRects,
204
134
  })
205
135
  this._meshNodes = rectsToMeshNodes(rects)
206
136
  this.solved = true
@@ -208,25 +138,39 @@ export class RectDiffExpansionSolver extends BaseSolver {
208
138
 
209
139
  computeProgress(): number {
210
140
  if (this.solved) return 1
211
- const grids = this.options.gridSizes.length
141
+ const grids = this.input.options.gridSizes.length
212
142
  const base = grids / (grids + 1)
213
- const denom = Math.max(1, this.placed.length)
214
- const frac = denom ? this.expansionIndex / denom : 1
143
+ const denom = Math.max(1, this.input.placed.length)
144
+ const frac = denom ? this.input.expansionIndex / denom : 1
215
145
  return Math.min(0.999, base + frac * (1 / (grids + 1)))
216
146
  }
217
147
 
218
148
  override getOutput(): { meshNodes: CapacityMeshNode[] } {
219
- if (!this.solved && this._meshNodes.length === 0) {
220
- this.finalizeIfNeeded()
221
- }
222
- return { meshNodes: this._meshNodes }
149
+ if (this.solved) return { meshNodes: this._meshNodes }
150
+
151
+ // Provide a live preview of the placements before finalization so debuggers
152
+ // can inspect intermediary states without forcing the solver to finish.
153
+ const previewNodes: CapacityMeshNode[] = this.input.placed.map(
154
+ (placement, idx) => ({
155
+ capacityMeshNodeId: `expand-preview-${idx}`,
156
+ center: {
157
+ x: placement.rect.x + placement.rect.width / 2,
158
+ y: placement.rect.y + placement.rect.height / 2,
159
+ },
160
+ width: placement.rect.width,
161
+ height: placement.rect.height,
162
+ availableZ: placement.zLayers.slice(),
163
+ layer: `z${placement.zLayers.join(",")}`,
164
+ }),
165
+ )
166
+ return { meshNodes: previewNodes }
223
167
  }
224
168
 
225
169
  /** Simple visualization of expanded placements. */
226
170
  override visualize(): GraphicsObject {
227
171
  const rects: NonNullable<GraphicsObject["rects"]> = []
228
172
 
229
- for (const placement of this.placed ?? []) {
173
+ for (const placement of this.input.placed ?? []) {
230
174
  rects.push({
231
175
  center: {
232
176
  x: placement.rect.x + placement.rect.width / 2,
@@ -48,15 +48,30 @@ export class RectDiffGridSolverPipeline extends BasePipelineSolver<RectDiffGridS
48
48
  definePipelineStep(
49
49
  "rectDiffExpansionSolver",
50
50
  RectDiffExpansionSolver,
51
- (pipeline: RectDiffGridSolverPipeline) => [
52
- {
53
- initialSnapshot: {
54
- ...pipeline.rectDiffSeedingSolver!.getOutput(),
51
+ (pipeline: RectDiffGridSolverPipeline) => {
52
+ const output = pipeline.rectDiffSeedingSolver?.getOutput()
53
+ if (!output) {
54
+ throw new Error("RectDiffSeedingSolver did not produce output")
55
+ }
56
+ return [
57
+ {
58
+ srj: pipeline.inputProblem.simpleRouteJson,
59
+ layerNames: output.layerNames ?? [],
55
60
  boardVoidRects: pipeline.inputProblem.boardVoidRects ?? [],
61
+ layerCount: pipeline.inputProblem.simpleRouteJson.layerCount,
62
+ bounds: output.bounds!,
63
+ candidates: output.candidates,
64
+ consumedSeedsThisGrid: output.placed.length,
65
+ totalSeedsThisGrid: output.candidates.length,
66
+ placed: output.placed,
67
+ edgeAnalysisDone: output.edgeAnalysisDone,
68
+ gridIndex: output.gridIndex,
69
+ expansionIndex: output.expansionIndex,
70
+ obstacleIndexByLayer: pipeline.obstacleIndexByLayer,
71
+ options: output.options,
56
72
  },
57
- obstacleIndexByLayer: pipeline.obstacleIndexByLayer,
58
- },
59
- ],
73
+ ]
74
+ },
60
75
  ),
61
76
  ]
62
77
 
@@ -238,23 +238,17 @@ export class RectDiffSeedingSolver extends BaseSolver {
238
238
  const ordered = preferMultiLayer ? attempts : attempts.reverse()
239
239
 
240
240
  for (const attempt of ordered) {
241
- // HARD blockers only: obstacles on those layers + full-stack nodes
242
- const hardBlockers: XYRect[] = []
243
- for (const z of attempt.layers) {
244
- const obstacleLayer = this.input.obstacleIndexByLayer[z]
245
- if (obstacleLayer) hardBlockers.push(...obstacleLayer.all())
246
- if (hardPlacedByLayer[z]) hardBlockers.push(...hardPlacedByLayer[z]!)
247
- }
248
-
249
241
  const rect = expandRectFromSeed({
250
242
  startX: cand.x,
251
243
  startY: cand.y,
252
244
  gridSize: grid,
253
245
  bounds: this.bounds,
254
- blockers: hardBlockers,
246
+ obsticalIndexByLayer: this.input.obstacleIndexByLayer,
247
+ placedIndexByLayer: this.placedIndexByLayer,
255
248
  initialCellRatio,
256
249
  maxAspectRatio,
257
250
  minReq: attempt.minReq,
251
+ zLayers: attempt.layers,
258
252
  })
259
253
  if (!rect) continue
260
254
 
@@ -1,5 +1,14 @@
1
+ import type RBush from "rbush"
1
2
  import type { XYRect } from "../rectdiff-types"
2
3
  import { EPS, gt, gte, lt, lte, overlaps } from "./rectdiff-geometry"
4
+ import type { RTreeRect } from "lib/types/capacity-mesh-types"
5
+ import { isSelfRect } from "./isSelfRect"
6
+ import {
7
+ searchStripDown,
8
+ searchStripLeft,
9
+ searchStripRight,
10
+ searchStripUp,
11
+ } from "./searchStrip"
3
12
 
4
13
  type ExpandDirectionParams = {
5
14
  r: XYRect
@@ -148,23 +157,55 @@ function maxExpandUp(params: ExpandDirectionParams) {
148
157
  return Math.max(0, e)
149
158
  }
150
159
 
160
+ const toRect = (tree: RTreeRect): XYRect => ({
161
+ x: tree.minX,
162
+ y: tree.minY,
163
+ width: tree.maxX - tree.minX,
164
+ height: tree.maxY - tree.minY,
165
+ })
166
+
167
+ const addBlocker = (params: {
168
+ rect: XYRect
169
+ seen: Set<string>
170
+ blockers: XYRect[]
171
+ }) => {
172
+ const { rect, seen, blockers } = params
173
+ const key = `${rect.x}|${rect.y}|${rect.width}|${rect.height}`
174
+ if (seen.has(key)) return
175
+ seen.add(key)
176
+ blockers.push(rect)
177
+ }
178
+
179
+ const toQueryRect = (params: { bounds: XYRect; rect: XYRect }) => {
180
+ const { bounds, rect } = params
181
+ const minX = Math.max(bounds.x, rect.x)
182
+ const minY = Math.max(bounds.y, rect.y)
183
+ const maxX = Math.min(bounds.x + bounds.width, rect.x + rect.width)
184
+ const maxY = Math.min(bounds.y + bounds.height, rect.y + rect.height)
185
+ if (maxX <= minX + EPS || maxY <= minY + EPS) return null
186
+ return { minX, minY, maxX, maxY }
187
+ }
188
+
151
189
  /** Grow a rect around a seed point, honoring bounds/blockers/aspect/min sizes. */
152
190
  export function expandRectFromSeed(params: {
153
191
  startX: number
154
192
  startY: number
155
193
  gridSize: number
156
194
  bounds: XYRect
157
- blockers: XYRect[]
195
+ obsticalIndexByLayer: Array<RBush<RTreeRect>>
196
+ placedIndexByLayer: Array<RBush<RTreeRect>>
158
197
  initialCellRatio: number
159
198
  maxAspectRatio: number | null | undefined
160
199
  minReq: { width: number; height: number }
200
+ zLayers: number[]
161
201
  }): XYRect | null {
162
202
  const {
163
203
  startX,
164
204
  startY,
165
205
  gridSize,
166
206
  bounds,
167
- blockers,
207
+ obsticalIndexByLayer,
208
+ placedIndexByLayer,
168
209
  initialCellRatio,
169
210
  maxAspectRatio,
170
211
  minReq,
@@ -173,6 +214,40 @@ export function expandRectFromSeed(params: {
173
214
  const minSide = Math.max(1e-9, gridSize * initialCellRatio)
174
215
  const initialW = Math.max(minSide, minReq.width)
175
216
  const initialH = Math.max(minSide, minReq.height)
217
+ const blockers: XYRect[] = []
218
+ const seen = new Set<string>()
219
+
220
+ // Ignore the existing placement we are expanding so it doesn't self-block.
221
+
222
+ const collectBlockers = (searchRect: XYRect) => {
223
+ const query = toQueryRect({ bounds, rect: searchRect })
224
+ if (!query) return
225
+ for (const z of params.zLayers) {
226
+ const blockersIndex = obsticalIndexByLayer[z]
227
+ if (blockersIndex) {
228
+ for (const entry of blockersIndex.search(query))
229
+ addBlocker({ rect: toRect(entry), seen, blockers })
230
+ }
231
+
232
+ const placedLayer = placedIndexByLayer[z]
233
+ if (placedLayer) {
234
+ for (const entry of placedLayer.search(query)) {
235
+ const rect = toRect(entry)
236
+ if (
237
+ isSelfRect({
238
+ rect,
239
+ startX,
240
+ startY,
241
+ initialW,
242
+ initialH,
243
+ })
244
+ )
245
+ continue
246
+ addBlocker({ rect, seen, blockers })
247
+ }
248
+ }
249
+ }
250
+ }
176
251
 
177
252
  const strategies = [
178
253
  { ox: 0, oy: 0 },
@@ -192,6 +267,7 @@ export function expandRectFromSeed(params: {
192
267
  width: initialW,
193
268
  height: initialH,
194
269
  }
270
+ collectBlockers(r)
195
271
 
196
272
  // keep initial inside board
197
273
  if (
@@ -212,27 +288,35 @@ export function expandRectFromSeed(params: {
212
288
  improved = false
213
289
  const commonParams = { bounds, blockers, maxAspect: maxAspectRatio }
214
290
 
291
+ collectBlockers(searchStripRight({ rect: r, bounds }))
215
292
  const eR = maxExpandRight({ ...commonParams, r })
216
293
  if (eR > 0) {
217
294
  r = { ...r, width: r.width + eR }
295
+ collectBlockers(r)
218
296
  improved = true
219
297
  }
220
298
 
299
+ collectBlockers(searchStripDown({ rect: r, bounds }))
221
300
  const eD = maxExpandDown({ ...commonParams, r })
222
301
  if (eD > 0) {
223
302
  r = { ...r, height: r.height + eD }
303
+ collectBlockers(r)
224
304
  improved = true
225
305
  }
226
306
 
307
+ collectBlockers(searchStripLeft({ rect: r, bounds }))
227
308
  const eL = maxExpandLeft({ ...commonParams, r })
228
309
  if (eL > 0) {
229
310
  r = { x: r.x - eL, y: r.y, width: r.width + eL, height: r.height }
311
+ collectBlockers(r)
230
312
  improved = true
231
313
  }
232
314
 
315
+ collectBlockers(searchStripUp({ rect: r, bounds }))
233
316
  const eU = maxExpandUp({ ...commonParams, r })
234
317
  if (eU > 0) {
235
318
  r = { x: r.x, y: r.y - eU, width: r.width, height: r.height + eU }
319
+ collectBlockers(r)
236
320
  improved = true
237
321
  }
238
322
  }
@@ -0,0 +1,15 @@
1
+ import type { XYRect } from "lib/rectdiff-types"
2
+
3
+ const EPS = 1e-9
4
+
5
+ export const isSelfRect = (params: {
6
+ rect: XYRect
7
+ startX: number
8
+ startY: number
9
+ initialW: number
10
+ initialH: number
11
+ }) =>
12
+ Math.abs(params.rect.x + params.rect.width / 2 - params.startX) < EPS &&
13
+ Math.abs(params.rect.y + params.rect.height / 2 - params.startY) < EPS &&
14
+ Math.abs(params.rect.width - params.initialW) < EPS &&
15
+ Math.abs(params.rect.height - params.initialH) < EPS
@@ -0,0 +1,50 @@
1
+ import type { XYRect } from "lib/rectdiff-types"
2
+
3
+ export const searchStripRight = ({
4
+ rect,
5
+ bounds,
6
+ }: {
7
+ rect: XYRect
8
+ bounds: XYRect
9
+ }): XYRect => ({
10
+ x: rect.x,
11
+ y: rect.y,
12
+ width: bounds.x + bounds.width - rect.x,
13
+ height: rect.height,
14
+ })
15
+ export const searchStripDown = ({
16
+ rect,
17
+ bounds,
18
+ }: {
19
+ rect: XYRect
20
+ bounds: XYRect
21
+ }): XYRect => ({
22
+ x: rect.x,
23
+ y: rect.y,
24
+ width: rect.width,
25
+ height: bounds.y + bounds.height - rect.y,
26
+ })
27
+ export const searchStripLeft = ({
28
+ rect,
29
+ bounds,
30
+ }: {
31
+ rect: XYRect
32
+ bounds: XYRect
33
+ }): XYRect => ({
34
+ x: bounds.x,
35
+ y: rect.y,
36
+ width: rect.x - bounds.x,
37
+ height: rect.height,
38
+ })
39
+ export const searchStripUp = ({
40
+ rect,
41
+ bounds,
42
+ }: {
43
+ rect: XYRect
44
+ bounds: XYRect
45
+ }): XYRect => ({
46
+ x: rect.x,
47
+ y: bounds.y,
48
+ width: rect.width,
49
+ height: rect.y - bounds.y,
50
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/rectdiff",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -0,0 +1,13 @@
1
+ import { useMemo } from "react"
2
+ import { RectDiffExpansionSolver } from "lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver"
3
+ import { createTwoNodeExpansionInput } from "lib/fixtures/twoNodeExpansionFixture"
4
+ import { SolverDebugger3d } from "../../components/SolverDebugger3d"
5
+
6
+ export default () => {
7
+ const solver = useMemo(
8
+ () => new RectDiffExpansionSolver(createTwoNodeExpansionInput()),
9
+ [],
10
+ )
11
+
12
+ return <SolverDebugger3d solver={solver} />
13
+ }
@@ -1,4 +1,4 @@
1
- <svg width="640" height="480" viewBox="0 0 640 480" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="white"/><g><rect data-type="rect" data-label="node" data-x="-3" data-y="0" x="86.66666666666666" y="40" width="66.66666666666666" height="400" fill="#dbeafe" stroke="black" stroke-width="0.015"/></g><g><rect data-type="rect" data-label="node" data-x="3" data-y="0" x="486.6666666666667" y="40" width="66.66666666666669" height="400" fill="#dbeafe" stroke="black" stroke-width="0.015"/></g><g><rect data-type="rect" data-label="node" data-x="0" data-y="0" x="253.33333333333331" y="206.66666666666666" width="133.33333333333337" height="66.66666666666666" fill="#dbeafe" stroke="black" stroke-width="0.015"/></g><g><rect data-type="rect" data-label="node" data-x="-1.75" data-y="0" x="153.33333333333331" y="40" width="100" height="400" fill="#dbeafe" stroke="black" stroke-width="0.015"/></g><g><rect data-type="rect" data-label="node" data-x="1.75" data-y="0" x="386.6666666666667" y="40" width="100" height="400" fill="#dbeafe" stroke="black" stroke-width="0.015"/></g><g id="crosshair" style="display: none"><line id="crosshair-h" y1="0" y2="480" 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[
1
+ <svg width="640" height="480" viewBox="0 0 640 480" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="white"/><g><polyline data-points="0,0 12,0 12,4 0,4 0,0" data-type="line" data-label="bounds" points="40,333.3333333333333 600,333.3333333333333 600,146.66666666666666 40,146.66666666666666 40,333.3333333333333" fill="none" stroke="#111827" stroke-width="4.666666666666667"/></g><g><rect data-type="rect" data-label="node" data-x="4.75" data-y="2" x="40" y="146.66666666666666" width="443.3333333333333" height="186.66666666666666" fill="#dbeafe" stroke="black" stroke-width="0.02142857142857143"/></g><g><rect data-type="rect" data-label="node" data-x="10.75" data-y="2" x="483.3333333333333" y="146.66666666666666" width="116.66666666666669" height="186.66666666666666" fill="#dbeafe" stroke="black" stroke-width="0.02142857142857143"/></g><g id="crosshair" style="display: none"><line id="crosshair-h" y1="0" y2="480" 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[
2
2
  document.currentScript.parentElement.addEventListener('mousemove', (e) => {
3
3
  const svg = e.currentTarget;
4
4
  const rect = svg.getBoundingClientRect();
@@ -20,7 +20,7 @@
20
20
  v.setAttribute('y2', '480');
21
21
 
22
22
  // Calculate real coordinates using inverse transformation
23
- const matrix = {"a":66.66666666666667,"c":0,"e":320,"b":0,"d":-66.66666666666667,"f":240};
23
+ const matrix = {"a":46.666666666666664,"c":0,"e":40,"b":0,"d":-46.666666666666664,"f":333.3333333333333};
24
24
  // Manually invert and apply the affine transform
25
25
  // Since we only use translate and scale, we can directly compute:
26
26
  // x' = (x - tx) / sx
@@ -0,0 +1,41 @@
1
+ import type { GraphicsObject, Line } from "graphics-debug"
2
+ import type { SimpleRouteJson } from "lib/types/srj-types"
3
+
4
+ /**
5
+ * Creates a GraphicsObject that draws the SRJ outline (or bounds fallback).
6
+ */
7
+ export const makeSimpleRouteOutlineGraphics = (
8
+ srj: SimpleRouteJson,
9
+ ): GraphicsObject => {
10
+ const lines: NonNullable<Line[]> = []
11
+
12
+ if (srj.outline && srj.outline.length > 1) {
13
+ lines.push({
14
+ points: [...srj.outline, srj.outline[0]!],
15
+ strokeColor: "#111827",
16
+ strokeWidth: 0.1,
17
+ label: "outline",
18
+ })
19
+ } else {
20
+ const { minX, maxX, minY, maxY } = srj.bounds
21
+ lines.push({
22
+ points: [
23
+ { x: minX, y: minY },
24
+ { x: maxX, y: minY },
25
+ { x: maxX, y: maxY },
26
+ { x: minX, y: maxY },
27
+ { x: minX, y: minY },
28
+ ],
29
+ strokeColor: "#111827",
30
+ strokeWidth: 0.1,
31
+ label: "bounds",
32
+ })
33
+ }
34
+
35
+ return {
36
+ title: "SimpleRoute Outline",
37
+ coordinateSystem: "cartesian",
38
+ lines,
39
+ points: [],
40
+ }
41
+ }
@@ -28,7 +28,7 @@ test("RectDiffSolver supports incremental stepping", () => {
28
28
 
29
29
  // Step advances one candidate at a time
30
30
  let stepCount = 0
31
- const maxSteps = 1000 // safety limit
31
+ const maxSteps = 5000 // safety limit
32
32
 
33
33
  while (!pipeline.solved && stepCount < maxSteps) {
34
34
  pipeline.step()