@tscircuit/rectdiff 0.0.4 → 0.0.5

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.
Files changed (29) hide show
  1. package/dist/index.d.ts +112 -3
  2. package/dist/index.js +869 -142
  3. package/lib/solvers/RectDiffSolver.ts +125 -24
  4. package/lib/solvers/rectdiff/candidates.ts +150 -104
  5. package/lib/solvers/rectdiff/engine.ts +72 -53
  6. package/lib/solvers/rectdiff/gapfill/detection/deduplicateGaps.ts +28 -0
  7. package/lib/solvers/rectdiff/gapfill/detection/findAllGaps.ts +83 -0
  8. package/lib/solvers/rectdiff/gapfill/detection/findGapsOnLayer.ts +100 -0
  9. package/lib/solvers/rectdiff/gapfill/detection/mergeUncoveredCells.ts +75 -0
  10. package/lib/solvers/rectdiff/gapfill/detection.ts +3 -0
  11. package/lib/solvers/rectdiff/gapfill/engine/addPlacement.ts +27 -0
  12. package/lib/solvers/rectdiff/gapfill/engine/calculateCoverage.ts +44 -0
  13. package/lib/solvers/rectdiff/gapfill/engine/findUncoveredPoints.ts +43 -0
  14. package/lib/solvers/rectdiff/gapfill/engine/getGapFillProgress.ts +42 -0
  15. package/lib/solvers/rectdiff/gapfill/engine/initGapFillState.ts +57 -0
  16. package/lib/solvers/rectdiff/gapfill/engine/stepGapFill.ts +128 -0
  17. package/lib/solvers/rectdiff/gapfill/engine/tryExpandGap.ts +78 -0
  18. package/lib/solvers/rectdiff/gapfill/engine.ts +7 -0
  19. package/lib/solvers/rectdiff/gapfill/types.ts +60 -0
  20. package/lib/solvers/rectdiff/geometry.ts +23 -11
  21. package/lib/solvers/rectdiff/subsolvers/GapFillSubSolver.ts +253 -0
  22. package/lib/solvers/rectdiff/types.ts +1 -1
  23. package/package.json +1 -1
  24. package/tests/obstacle-extra-layers.test.ts +1 -1
  25. package/tests/obstacle-zlayers.test.ts +1 -1
  26. package/tests/rect-diff-solver.test.ts +1 -4
  27. package/utils/README.md +21 -0
  28. package/utils/rectsEqual.ts +18 -0
  29. package/utils/rectsOverlap.ts +18 -0
@@ -13,31 +13,39 @@ import {
13
13
  computeProgress,
14
14
  } from "./rectdiff/engine"
15
15
  import { rectsToMeshNodes } from "./rectdiff/rectsToMeshNodes"
16
+ import type { GapFillOptions } from "./rectdiff/gapfill/types"
17
+ import {
18
+ findUncoveredPoints,
19
+ calculateCoverage,
20
+ } from "./rectdiff/gapfill/engine"
21
+ import { GapFillSubSolver } from "./rectdiff/subsolvers/GapFillSubSolver"
16
22
 
17
- // A streaming, one-step-per-iteration solver.
18
- // Tests that call `solver.solve()` still work because BaseSolver.solve()
19
- // loops until this.solved flips true.
20
-
23
+ /**
24
+ * A streaming, one-step-per-iteration solver for capacity mesh generation.
25
+ */
21
26
  export class RectDiffSolver extends BaseSolver {
22
27
  private srj: SimpleRouteJson
23
- private mode: "grid" | "exact"
24
28
  private gridOptions: Partial<GridFill3DOptions>
29
+ private gapFillOptions: Partial<GapFillOptions>
25
30
  private state!: RectDiffState
26
31
  private _meshNodes: CapacityMeshNode[] = []
27
32
 
33
+ /** Active subsolver for GAP_FILL phases. */
34
+ declare activeSubSolver: GapFillSubSolver | null
35
+
28
36
  constructor(opts: {
29
37
  simpleRouteJson: SimpleRouteJson
30
- mode?: "grid" | "exact"
31
38
  gridOptions?: Partial<GridFill3DOptions>
39
+ gapFillOptions?: Partial<GapFillOptions>
32
40
  }) {
33
41
  super()
34
42
  this.srj = opts.simpleRouteJson
35
- this.mode = opts.mode ?? "grid"
36
43
  this.gridOptions = opts.gridOptions ?? {}
44
+ this.gapFillOptions = opts.gapFillOptions ?? {}
45
+ this.activeSubSolver = null
37
46
  }
38
47
 
39
48
  override _setup() {
40
- // For now "exact" mode falls back to grid; keep switch if you add exact later.
41
49
  this.state = initState(this.srj, this.gridOptions)
42
50
  this.stats = {
43
51
  phase: this.state.phase,
@@ -45,12 +53,51 @@ export class RectDiffSolver extends BaseSolver {
45
53
  }
46
54
  }
47
55
 
48
- /** IMPORTANT: exactly ONE small step per call */
56
+ /** Exactly ONE small step per call. */
49
57
  override _step() {
50
58
  if (this.state.phase === "GRID") {
51
59
  stepGrid(this.state)
52
60
  } else if (this.state.phase === "EXPANSION") {
53
61
  stepExpansion(this.state)
62
+ } else if (this.state.phase === "GAP_FILL") {
63
+ // Initialize gap fill subsolver if needed
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
+ }
54
101
  } else if (this.state.phase === "DONE") {
55
102
  // Finalize once
56
103
  if (!this.solved) {
@@ -65,47 +112,101 @@ export class RectDiffSolver extends BaseSolver {
65
112
  this.stats.phase = this.state.phase
66
113
  this.stats.gridIndex = this.state.gridIndex
67
114
  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
+ }
68
119
  }
69
120
 
70
- // Let BaseSolver update this.progress automatically if present.
121
+ /** Compute solver progress (0 to 1). */
71
122
  computeProgress(): number {
72
- return computeProgress(this.state)
123
+ if (this.solved || this.state.phase === "DONE") {
124
+ return 1
125
+ }
126
+ if (
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
73
133
  }
74
134
 
75
135
  override getOutput(): { meshNodes: CapacityMeshNode[] } {
76
136
  return { meshNodes: this._meshNodes }
77
137
  }
78
138
 
79
- // Helper to get color based on z layer
139
+ /** Get coverage percentage (0-1). */
140
+ getCoverage(sampleResolution: number = 0.05): number {
141
+ return calculateCoverage(
142
+ { sampleResolution },
143
+ {
144
+ bounds: this.state.bounds,
145
+ layerCount: this.state.layerCount,
146
+ obstaclesByLayer: this.state.obstaclesByLayer,
147
+ placedByLayer: this.state.placedByLayer,
148
+ },
149
+ )
150
+ }
151
+
152
+ /** Find uncovered points for debugging gaps. */
153
+ getUncoveredPoints(
154
+ sampleResolution: number = 0.05,
155
+ ): Array<{ x: number; y: number; z: number }> {
156
+ return findUncoveredPoints(
157
+ { sampleResolution },
158
+ {
159
+ bounds: this.state.bounds,
160
+ layerCount: this.state.layerCount,
161
+ obstaclesByLayer: this.state.obstaclesByLayer,
162
+ placedByLayer: this.state.placedByLayer,
163
+ },
164
+ )
165
+ }
166
+
167
+ /** Get color based on z layer for visualization. */
80
168
  private getColorForZLayer(zLayers: number[]): {
81
169
  fill: string
82
170
  stroke: string
83
171
  } {
84
172
  const minZ = Math.min(...zLayers)
85
173
  const colors = [
86
- { fill: "#dbeafe", stroke: "#3b82f6" }, // blue (z=0)
87
- { fill: "#fef3c7", stroke: "#f59e0b" }, // amber (z=1)
88
- { fill: "#d1fae5", stroke: "#10b981" }, // green (z=2)
89
- { fill: "#e9d5ff", stroke: "#a855f7" }, // purple (z=3)
90
- { fill: "#fed7aa", stroke: "#f97316" }, // orange (z=4)
91
- { fill: "#fecaca", stroke: "#ef4444" }, // red (z=5)
174
+ { fill: "#dbeafe", stroke: "#3b82f6" },
175
+ { fill: "#fef3c7", stroke: "#f59e0b" },
176
+ { fill: "#d1fae5", stroke: "#10b981" },
177
+ { fill: "#e9d5ff", stroke: "#a855f7" },
178
+ { fill: "#fed7aa", stroke: "#f97316" },
179
+ { fill: "#fecaca", stroke: "#ef4444" },
92
180
  ]
93
181
  return colors[minZ % colors.length]!
94
182
  }
95
183
 
96
- // Streaming visualization: board + obstacles + current placements.
184
+ /** Streaming visualization: board + obstacles + current placements. */
97
185
  override visualize(): GraphicsObject {
186
+ // If a subsolver is active, delegate to its visualization
187
+ if (this.activeSubSolver) {
188
+ return this.activeSubSolver.visualize()
189
+ }
190
+
98
191
  const rects: NonNullable<GraphicsObject["rects"]> = []
99
192
  const points: NonNullable<GraphicsObject["points"]> = []
100
193
 
194
+ // Board bounds - use srj bounds which is always available
195
+ const boardBounds = {
196
+ minX: this.srj.bounds.minX,
197
+ maxX: this.srj.bounds.maxX,
198
+ minY: this.srj.bounds.minY,
199
+ maxY: this.srj.bounds.maxY,
200
+ }
201
+
101
202
  // board
102
203
  rects.push({
103
204
  center: {
104
- x: (this.srj.bounds.minX + this.srj.bounds.maxX) / 2,
105
- y: (this.srj.bounds.minY + this.srj.bounds.maxY) / 2,
205
+ x: (boardBounds.minX + boardBounds.maxX) / 2,
206
+ y: (boardBounds.minY + boardBounds.maxY) / 2,
106
207
  },
107
- width: this.srj.bounds.maxX - this.srj.bounds.minX,
108
- height: this.srj.bounds.maxY - this.srj.bounds.minY,
208
+ width: boardBounds.maxX - boardBounds.minX,
209
+ height: boardBounds.maxY - boardBounds.minY,
109
210
  fill: "none",
110
211
  stroke: "#111827",
111
212
  label: "board",
@@ -158,7 +259,7 @@ export class RectDiffSolver extends BaseSolver {
158
259
  }
159
260
 
160
261
  return {
161
- title: "RectDiff (incremental)",
262
+ title: `RectDiff (${this.state?.phase ?? "init"})`,
162
263
  coordinateSystem: "cartesian",
163
264
  rects,
164
265
  points,
@@ -2,13 +2,17 @@
2
2
  import type { Candidate3D, XYRect } from "./types"
3
3
  import { EPS, clamp, containsPoint, distancePointToRectEdges } from "./geometry"
4
4
 
5
- function isFullyOccupiedAllLayers(
6
- x: number,
7
- y: number,
8
- layerCount: number,
9
- obstaclesByLayer: XYRect[][],
10
- placedByLayer: XYRect[][],
11
- ): boolean {
5
+ /**
6
+ * Check if a point is occupied on all layers.
7
+ */
8
+ function isFullyOccupiedAllLayers(params: {
9
+ x: number
10
+ y: number
11
+ layerCount: number
12
+ obstaclesByLayer: XYRect[][]
13
+ placedByLayer: XYRect[][]
14
+ }): boolean {
15
+ const { x, y, layerCount, obstaclesByLayer, placedByLayer } = params
12
16
  for (let z = 0; z < layerCount; z++) {
13
17
  const obs = obstaclesByLayer[z] ?? []
14
18
  const placed = placedByLayer[z] ?? []
@@ -20,14 +24,25 @@ function isFullyOccupiedAllLayers(
20
24
  return true
21
25
  }
22
26
 
23
- export function computeCandidates3D(
24
- bounds: XYRect,
25
- gridSize: number,
26
- layerCount: number,
27
- obstaclesByLayer: XYRect[][],
28
- placedByLayer: XYRect[][], // all current nodes (soft + hard)
29
- hardPlacedByLayer: XYRect[][], // only full-stack nodes, treated as hard
30
- ): Candidate3D[] {
27
+ /**
28
+ * Compute candidate seed points for a given grid size.
29
+ */
30
+ export function computeCandidates3D(params: {
31
+ bounds: XYRect
32
+ gridSize: number
33
+ layerCount: number
34
+ obstaclesByLayer: XYRect[][]
35
+ placedByLayer: XYRect[][]
36
+ hardPlacedByLayer: XYRect[][]
37
+ }): Candidate3D[] {
38
+ const {
39
+ bounds,
40
+ gridSize,
41
+ layerCount,
42
+ obstaclesByLayer,
43
+ placedByLayer,
44
+ hardPlacedByLayer,
45
+ } = params
31
46
  const out = new Map<string, Candidate3D>() // key by (x,y)
32
47
 
33
48
  for (let x = bounds.x; x < bounds.x + bounds.width; x += gridSize) {
@@ -44,13 +59,13 @@ export function computeCandidates3D(
44
59
 
45
60
  // New rule: Only drop if EVERY layer is occupied (by obstacle or node)
46
61
  if (
47
- isFullyOccupiedAllLayers(
62
+ isFullyOccupiedAllLayers({
48
63
  x,
49
64
  y,
50
65
  layerCount,
51
66
  obstaclesByLayer,
52
67
  placedByLayer,
53
- )
68
+ })
54
69
  )
55
70
  continue
56
71
 
@@ -58,16 +73,16 @@ export function computeCandidates3D(
58
73
  let bestSpan: number[] = []
59
74
  let bestZ = 0
60
75
  for (let z = 0; z < layerCount; z++) {
61
- const s = longestFreeSpanAroundZ(
76
+ const s = longestFreeSpanAroundZ({
62
77
  x,
63
78
  y,
64
79
  z,
65
80
  layerCount,
66
- 1,
67
- undefined, // no cap here
81
+ minSpan: 1,
82
+ maxSpan: undefined,
68
83
  obstaclesByLayer,
69
- hardPlacedByLayer, // IMPORTANT: ignore soft nodes
70
- )
84
+ placedByLayer: hardPlacedByLayer,
85
+ })
71
86
  if (s.length > bestSpan.length) {
72
87
  bestSpan = s
73
88
  bestZ = z
@@ -113,17 +128,30 @@ export function computeCandidates3D(
113
128
  return arr
114
129
  }
115
130
 
116
- /** Longest contiguous free span around z (optionally capped) */
117
- export function longestFreeSpanAroundZ(
118
- x: number,
119
- y: number,
120
- z: number,
121
- layerCount: number,
122
- minSpan: number,
123
- maxSpan: number | undefined,
124
- obstaclesByLayer: XYRect[][],
125
- placedByLayer: XYRect[][],
126
- ): number[] {
131
+ /**
132
+ * Find the longest contiguous free span around z (optionally capped).
133
+ */
134
+ export function longestFreeSpanAroundZ(params: {
135
+ x: number
136
+ y: number
137
+ z: number
138
+ layerCount: number
139
+ minSpan: number
140
+ maxSpan: number | undefined
141
+ obstaclesByLayer: XYRect[][]
142
+ placedByLayer: XYRect[][]
143
+ }): number[] {
144
+ const {
145
+ x,
146
+ y,
147
+ z,
148
+ layerCount,
149
+ minSpan,
150
+ maxSpan,
151
+ obstaclesByLayer,
152
+ placedByLayer,
153
+ } = params
154
+
127
155
  const isFreeAt = (layer: number) => {
128
156
  const blockers = [
129
157
  ...(obstaclesByLayer[layer] ?? []),
@@ -150,18 +178,25 @@ export function longestFreeSpanAroundZ(
150
178
  return res.length >= minSpan ? res : []
151
179
  }
152
180
 
181
+ /**
182
+ * Compute default grid sizes based on bounds.
183
+ */
153
184
  export function computeDefaultGridSizes(bounds: XYRect): number[] {
154
185
  const ref = Math.max(bounds.width, bounds.height)
155
186
  return [ref / 8, ref / 16, ref / 32]
156
187
  }
157
188
 
158
- /** Compute exact uncovered segments along a 1D line given a list of covering intervals */
159
- function computeUncoveredSegments(
160
- lineStart: number,
161
- lineEnd: number,
162
- coveringIntervals: Array<{ start: number; end: number }>,
163
- minSegmentLength: number,
164
- ): Array<{ start: number; end: number; center: number }> {
189
+ /**
190
+ * Compute exact uncovered segments along a 1D line.
191
+ */
192
+ function computeUncoveredSegments(params: {
193
+ lineStart: number
194
+ lineEnd: number
195
+ coveringIntervals: Array<{ start: number; end: number }>
196
+ minSegmentLength: number
197
+ }): Array<{ start: number; end: number; center: number }> {
198
+ const { lineStart, lineEnd, coveringIntervals, minSegmentLength } = params
199
+
165
200
  if (coveringIntervals.length === 0) {
166
201
  const center = (lineStart + lineEnd) / 2
167
202
  return [{ start: lineStart, end: lineEnd, center }]
@@ -220,15 +255,26 @@ function computeUncoveredSegments(
220
255
  return uncovered
221
256
  }
222
257
 
223
- /** Exact edge analysis: find uncovered segments along board edges and blocker edges */
224
- export function computeEdgeCandidates3D(
225
- bounds: XYRect,
226
- minSize: number,
227
- layerCount: number,
228
- obstaclesByLayer: XYRect[][],
229
- placedByLayer: XYRect[][], // all nodes
230
- hardPlacedByLayer: XYRect[][], // full-stack nodes
231
- ): Candidate3D[] {
258
+ /**
259
+ * Compute edge candidates using exact edge analysis.
260
+ */
261
+ export function computeEdgeCandidates3D(params: {
262
+ bounds: XYRect
263
+ minSize: number
264
+ layerCount: number
265
+ obstaclesByLayer: XYRect[][]
266
+ placedByLayer: XYRect[][]
267
+ hardPlacedByLayer: XYRect[][]
268
+ }): Candidate3D[] {
269
+ const {
270
+ bounds,
271
+ minSize,
272
+ layerCount,
273
+ obstaclesByLayer,
274
+ placedByLayer,
275
+ hardPlacedByLayer,
276
+ } = params
277
+
232
278
  const out: Candidate3D[] = []
233
279
  // Use small inset from edges for placement
234
280
  const δ = Math.max(minSize * 0.15, EPS * 3)
@@ -237,13 +283,13 @@ export function computeEdgeCandidates3D(
237
283
  `${z}|${x.toFixed(6)}|${y.toFixed(6)}`
238
284
 
239
285
  function fullyOcc(x: number, y: number) {
240
- return isFullyOccupiedAllLayers(
286
+ return isFullyOccupiedAllLayers({
241
287
  x,
242
288
  y,
243
289
  layerCount,
244
290
  obstaclesByLayer,
245
291
  placedByLayer,
246
- )
292
+ })
247
293
  }
248
294
 
249
295
  function pushIfFree(x: number, y: number, z: number) {
@@ -273,16 +319,16 @@ export function computeEdgeCandidates3D(
273
319
  dedup.add(k)
274
320
 
275
321
  // Approximate z-span strength at this z (ignoring soft nodes)
276
- const span = longestFreeSpanAroundZ(
322
+ const span = longestFreeSpanAroundZ({
277
323
  x,
278
324
  y,
279
325
  z,
280
326
  layerCount,
281
- 1,
282
- undefined,
327
+ minSpan: 1,
328
+ maxSpan: undefined,
283
329
  obstaclesByLayer,
284
- hardPlacedByLayer,
285
- )
330
+ placedByLayer: hardPlacedByLayer,
331
+ })
286
332
  out.push({ x, y, z, distance: d, zSpanLen: span.length, isEdgeSeed: true })
287
333
  }
288
334
 
@@ -314,12 +360,12 @@ export function computeEdgeCandidates3D(
314
360
  end: Math.min(bounds.x + bounds.width, b.x + b.width),
315
361
  }))
316
362
  // Find uncovered segments that are large enough to potentially fill
317
- const topUncovered = computeUncoveredSegments(
318
- bounds.x + δ,
319
- bounds.x + bounds.width - δ,
320
- topCovering,
321
- minSize * 0.5,
322
- )
363
+ const topUncovered = computeUncoveredSegments({
364
+ lineStart: bounds.x + δ,
365
+ lineEnd: bounds.x + bounds.width - δ,
366
+ coveringIntervals: topCovering,
367
+ minSegmentLength: minSize * 0.5,
368
+ })
323
369
  for (const seg of topUncovered) {
324
370
  const segLen = seg.end - seg.start
325
371
  if (segLen >= minSize) {
@@ -340,12 +386,12 @@ export function computeEdgeCandidates3D(
340
386
  start: Math.max(bounds.x, b.x),
341
387
  end: Math.min(bounds.x + bounds.width, b.x + b.width),
342
388
  }))
343
- const bottomUncovered = computeUncoveredSegments(
344
- bounds.x + δ,
345
- bounds.x + bounds.width - δ,
346
- bottomCovering,
347
- minSize * 0.5,
348
- )
389
+ const bottomUncovered = computeUncoveredSegments({
390
+ lineStart: bounds.x + δ,
391
+ lineEnd: bounds.x + bounds.width - δ,
392
+ coveringIntervals: bottomCovering,
393
+ minSegmentLength: minSize * 0.5,
394
+ })
349
395
  for (const seg of bottomUncovered) {
350
396
  const segLen = seg.end - seg.start
351
397
  if (segLen >= minSize) {
@@ -365,12 +411,12 @@ export function computeEdgeCandidates3D(
365
411
  start: Math.max(bounds.y, b.y),
366
412
  end: Math.min(bounds.y + bounds.height, b.y + b.height),
367
413
  }))
368
- const leftUncovered = computeUncoveredSegments(
369
- bounds.y + δ,
370
- bounds.y + bounds.height - δ,
371
- leftCovering,
372
- minSize * 0.5,
373
- )
414
+ const leftUncovered = computeUncoveredSegments({
415
+ lineStart: bounds.y + δ,
416
+ lineEnd: bounds.y + bounds.height - δ,
417
+ coveringIntervals: leftCovering,
418
+ minSegmentLength: minSize * 0.5,
419
+ })
374
420
  for (const seg of leftUncovered) {
375
421
  const segLen = seg.end - seg.start
376
422
  if (segLen >= minSize) {
@@ -390,12 +436,12 @@ export function computeEdgeCandidates3D(
390
436
  start: Math.max(bounds.y, b.y),
391
437
  end: Math.min(bounds.y + bounds.height, b.y + b.height),
392
438
  }))
393
- const rightUncovered = computeUncoveredSegments(
394
- bounds.y + δ,
395
- bounds.y + bounds.height - δ,
396
- rightCovering,
397
- minSize * 0.5,
398
- )
439
+ const rightUncovered = computeUncoveredSegments({
440
+ lineStart: bounds.y + δ,
441
+ lineEnd: bounds.y + bounds.height - δ,
442
+ coveringIntervals: rightCovering,
443
+ minSegmentLength: minSize * 0.5,
444
+ })
399
445
  for (const seg of rightUncovered) {
400
446
  const segLen = seg.end - seg.start
401
447
  if (segLen >= minSize) {
@@ -420,12 +466,12 @@ export function computeEdgeCandidates3D(
420
466
  start: Math.max(b.y, bl.y),
421
467
  end: Math.min(b.y + b.height, bl.y + bl.height),
422
468
  }))
423
- const obLeftUncovered = computeUncoveredSegments(
424
- b.y,
425
- b.y + b.height,
426
- obLeftCovering,
427
- minSize * 0.5,
428
- )
469
+ const obLeftUncovered = computeUncoveredSegments({
470
+ lineStart: b.y,
471
+ lineEnd: b.y + b.height,
472
+ coveringIntervals: obLeftCovering,
473
+ minSegmentLength: minSize * 0.5,
474
+ })
429
475
  for (const seg of obLeftUncovered) {
430
476
  pushIfFree(obLeftX, seg.center, z)
431
477
  }
@@ -445,12 +491,12 @@ export function computeEdgeCandidates3D(
445
491
  start: Math.max(b.y, bl.y),
446
492
  end: Math.min(b.y + b.height, bl.y + bl.height),
447
493
  }))
448
- const obRightUncovered = computeUncoveredSegments(
449
- b.y,
450
- b.y + b.height,
451
- obRightCovering,
452
- minSize * 0.5,
453
- )
494
+ const obRightUncovered = computeUncoveredSegments({
495
+ lineStart: b.y,
496
+ lineEnd: b.y + b.height,
497
+ coveringIntervals: obRightCovering,
498
+ minSegmentLength: minSize * 0.5,
499
+ })
454
500
  for (const seg of obRightUncovered) {
455
501
  pushIfFree(obRightX, seg.center, z)
456
502
  }
@@ -467,12 +513,12 @@ export function computeEdgeCandidates3D(
467
513
  start: Math.max(b.x, bl.x),
468
514
  end: Math.min(b.x + b.width, bl.x + bl.width),
469
515
  }))
470
- const obTopUncovered = computeUncoveredSegments(
471
- b.x,
472
- b.x + b.width,
473
- obTopCovering,
474
- minSize * 0.5,
475
- )
516
+ const obTopUncovered = computeUncoveredSegments({
517
+ lineStart: b.x,
518
+ lineEnd: b.x + b.width,
519
+ coveringIntervals: obTopCovering,
520
+ minSegmentLength: minSize * 0.5,
521
+ })
476
522
  for (const seg of obTopUncovered) {
477
523
  pushIfFree(seg.center, obTopY, z)
478
524
  }
@@ -493,12 +539,12 @@ export function computeEdgeCandidates3D(
493
539
  start: Math.max(b.x, bl.x),
494
540
  end: Math.min(b.x + b.width, bl.x + bl.width),
495
541
  }))
496
- const obBottomUncovered = computeUncoveredSegments(
497
- b.x,
498
- b.x + b.width,
499
- obBottomCovering,
500
- minSize * 0.5,
501
- )
542
+ const obBottomUncovered = computeUncoveredSegments({
543
+ lineStart: b.x,
544
+ lineEnd: b.x + b.width,
545
+ coveringIntervals: obBottomCovering,
546
+ minSegmentLength: minSize * 0.5,
547
+ })
502
548
  for (const seg of obBottomUncovered) {
503
549
  pushIfFree(seg.center, obBottomY, z)
504
550
  }