@tscircuit/rectdiff 0.0.3 → 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.
- package/dist/index.d.ts +112 -3
- package/dist/index.js +869 -142
- package/index.html +2 -2
- package/lib/solvers/RectDiffSolver.ts +125 -24
- package/lib/solvers/rectdiff/candidates.ts +150 -104
- package/lib/solvers/rectdiff/engine.ts +72 -53
- package/lib/solvers/rectdiff/gapfill/detection/deduplicateGaps.ts +28 -0
- package/lib/solvers/rectdiff/gapfill/detection/findAllGaps.ts +83 -0
- package/lib/solvers/rectdiff/gapfill/detection/findGapsOnLayer.ts +100 -0
- package/lib/solvers/rectdiff/gapfill/detection/mergeUncoveredCells.ts +75 -0
- package/lib/solvers/rectdiff/gapfill/detection.ts +3 -0
- package/lib/solvers/rectdiff/gapfill/engine/addPlacement.ts +27 -0
- package/lib/solvers/rectdiff/gapfill/engine/calculateCoverage.ts +44 -0
- package/lib/solvers/rectdiff/gapfill/engine/findUncoveredPoints.ts +43 -0
- package/lib/solvers/rectdiff/gapfill/engine/getGapFillProgress.ts +42 -0
- package/lib/solvers/rectdiff/gapfill/engine/initGapFillState.ts +57 -0
- package/lib/solvers/rectdiff/gapfill/engine/stepGapFill.ts +128 -0
- package/lib/solvers/rectdiff/gapfill/engine/tryExpandGap.ts +78 -0
- package/lib/solvers/rectdiff/gapfill/engine.ts +7 -0
- package/lib/solvers/rectdiff/gapfill/types.ts +60 -0
- package/lib/solvers/rectdiff/geometry.ts +23 -11
- package/lib/solvers/rectdiff/subsolvers/GapFillSubSolver.ts +253 -0
- package/lib/solvers/rectdiff/types.ts +1 -1
- package/main.tsx +1 -0
- package/package.json +11 -8
- package/tests/obstacle-extra-layers.test.ts +1 -1
- package/tests/obstacle-zlayers.test.ts +1 -1
- package/tests/rect-diff-solver.test.ts +1 -4
- package/utils/README.md +21 -0
- package/utils/rectsEqual.ts +18 -0
- package/utils/rectsOverlap.ts +18 -0
- package/vite.config.ts +7 -0
package/index.html
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>React Cosmos
|
|
6
|
+
<title>React Cosmos</title>
|
|
7
7
|
</head>
|
|
8
8
|
<body>
|
|
9
9
|
<div id="root"></div>
|
|
10
|
-
<script type="module" src="/
|
|
10
|
+
<script type="module" src="/main.tsx"></script>
|
|
11
11
|
</body>
|
|
12
12
|
</html>
|
|
@@ -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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
121
|
+
/** Compute solver progress (0 to 1). */
|
|
71
122
|
computeProgress(): number {
|
|
72
|
-
|
|
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
|
-
|
|
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" },
|
|
87
|
-
{ fill: "#fef3c7", stroke: "#f59e0b" },
|
|
88
|
-
{ fill: "#d1fae5", stroke: "#10b981" },
|
|
89
|
-
{ fill: "#e9d5ff", stroke: "#a855f7" },
|
|
90
|
-
{ fill: "#fed7aa", stroke: "#f97316" },
|
|
91
|
-
{ fill: "#fecaca", stroke: "#ef4444" },
|
|
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
|
-
|
|
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: (
|
|
105
|
-
y: (
|
|
205
|
+
x: (boardBounds.minX + boardBounds.maxX) / 2,
|
|
206
|
+
y: (boardBounds.minY + boardBounds.maxY) / 2,
|
|
106
207
|
},
|
|
107
|
-
width:
|
|
108
|
-
height:
|
|
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:
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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,
|
|
81
|
+
minSpan: 1,
|
|
82
|
+
maxSpan: undefined,
|
|
68
83
|
obstaclesByLayer,
|
|
69
|
-
hardPlacedByLayer,
|
|
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
|
-
/**
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
/**
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
/**
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
}
|