@tscircuit/rectdiff 0.0.12 → 0.0.14
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 +169 -27
- package/dist/index.js +2012 -1672
- package/lib/RectDiffPipeline.ts +18 -17
- package/lib/{solvers/rectdiff/types.ts → rectdiff-types.ts} +0 -34
- package/lib/{solvers/rectdiff/visualization.ts → rectdiff-visualization.ts} +2 -1
- package/lib/solvers/GapFillSolver/FindSegmentsWithAdjacentEmptySpaceSolver.ts +9 -12
- package/lib/solvers/GapFillSolver/visuallyOffsetLine.ts +5 -2
- package/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts +252 -0
- package/lib/solvers/{rectdiff → RectDiffExpansionSolver}/rectsToMeshNodes.ts +1 -2
- package/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts +106 -0
- package/lib/solvers/RectDiffGridSolverPipeline/buildObstacleIndexes.ts +70 -0
- package/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +487 -0
- package/lib/solvers/RectDiffSeedingSolver/computeCandidates3D.ts +109 -0
- package/lib/solvers/RectDiffSeedingSolver/computeDefaultGridSizes.ts +9 -0
- package/lib/solvers/{rectdiff/candidates.ts → RectDiffSeedingSolver/computeEdgeCandidates3D.ts} +44 -225
- package/lib/solvers/{rectdiff/geometry → RectDiffSeedingSolver}/computeInverseRects.ts +6 -2
- package/lib/solvers/{rectdiff → RectDiffSeedingSolver}/layers.ts +4 -3
- package/lib/solvers/RectDiffSeedingSolver/longestFreeSpanAroundZ.ts +60 -0
- package/lib/types/capacity-mesh-types.ts +9 -0
- package/lib/utils/buildHardPlacedByLayer.ts +14 -0
- package/lib/{solvers/rectdiff/geometry.ts → utils/expandRectFromSeed.ts} +21 -120
- package/lib/utils/finalizeRects.ts +54 -0
- package/lib/utils/isFullyOccupiedAtPoint.ts +28 -0
- package/lib/utils/rectToTree.ts +10 -0
- package/lib/utils/rectdiff-geometry.ts +94 -0
- package/lib/utils/resizeSoftOverlaps.ts +103 -0
- package/lib/utils/sameTreeRect.ts +7 -0
- package/package.json +1 -1
- package/tests/board-outline.test.ts +2 -1
- package/tests/examples/example01.test.tsx +18 -1
- package/tests/obstacle-extra-layers.test.ts +1 -1
- package/tests/obstacle-zlayers.test.ts +1 -1
- package/utils/rectsEqual.ts +2 -2
- package/utils/rectsOverlap.ts +2 -2
- package/lib/solvers/RectDiffSolver.ts +0 -231
- package/lib/solvers/rectdiff/engine.ts +0 -481
- /package/lib/solvers/{rectdiff/geometry → RectDiffSeedingSolver}/isPointInPolygon.ts +0 -0
|
@@ -1,64 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
1
|
+
import type { XYRect } from "../rectdiff-types"
|
|
2
|
+
import { EPS, gt, gte, lt, lte, overlaps } from "./rectdiff-geometry"
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export const lt = (a: number, b: number) => a < b - EPS
|
|
10
|
-
export const lte = (a: number, b: number) => a < b + EPS
|
|
11
|
-
|
|
12
|
-
export function overlaps(a: XYRect, b: XYRect) {
|
|
13
|
-
return !(
|
|
14
|
-
a.x + a.width <= b.x + EPS ||
|
|
15
|
-
b.x + b.width <= a.x + EPS ||
|
|
16
|
-
a.y + a.height <= b.y + EPS ||
|
|
17
|
-
b.y + b.height <= a.y + EPS
|
|
18
|
-
)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function containsPoint(r: XYRect, x: number, y: number) {
|
|
22
|
-
return (
|
|
23
|
-
x >= r.x - EPS &&
|
|
24
|
-
x <= r.x + r.width + EPS &&
|
|
25
|
-
y >= r.y - EPS &&
|
|
26
|
-
y <= r.y + r.height + EPS
|
|
27
|
-
)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function distancePointToRectEdges(px: number, py: number, r: XYRect) {
|
|
31
|
-
const edges: [number, number, number, number][] = [
|
|
32
|
-
[r.x, r.y, r.x + r.width, r.y],
|
|
33
|
-
[r.x + r.width, r.y, r.x + r.width, r.y + r.height],
|
|
34
|
-
[r.x + r.width, r.y + r.height, r.x, r.y + r.height],
|
|
35
|
-
[r.x, r.y + r.height, r.x, r.y],
|
|
36
|
-
]
|
|
37
|
-
let best = Infinity
|
|
38
|
-
for (const [x1, y1, x2, y2] of edges) {
|
|
39
|
-
const A = px - x1,
|
|
40
|
-
B = py - y1,
|
|
41
|
-
C = x2 - x1,
|
|
42
|
-
D = y2 - y1
|
|
43
|
-
const dot = A * C + B * D
|
|
44
|
-
const lenSq = C * C + D * D
|
|
45
|
-
let t = lenSq !== 0 ? dot / lenSq : 0
|
|
46
|
-
t = clamp(t, 0, 1)
|
|
47
|
-
const xx = x1 + t * C
|
|
48
|
-
const yy = y1 + t * D
|
|
49
|
-
best = Math.min(best, Math.hypot(px - xx, py - yy))
|
|
50
|
-
}
|
|
51
|
-
return best
|
|
4
|
+
type ExpandDirectionParams = {
|
|
5
|
+
r: XYRect
|
|
6
|
+
bounds: XYRect
|
|
7
|
+
blockers: XYRect[]
|
|
8
|
+
maxAspect: number | null | undefined
|
|
52
9
|
}
|
|
53
10
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
function maxExpandRight(
|
|
57
|
-
r: XYRect,
|
|
58
|
-
bounds: XYRect,
|
|
59
|
-
blockers: XYRect[],
|
|
60
|
-
maxAspect: number | null | undefined,
|
|
61
|
-
) {
|
|
11
|
+
function maxExpandRight(params: ExpandDirectionParams) {
|
|
12
|
+
const { r, bounds, blockers, maxAspect } = params
|
|
62
13
|
// Start with board boundary
|
|
63
14
|
let maxWidth = bounds.x + bounds.width - r.x
|
|
64
15
|
|
|
@@ -94,12 +45,8 @@ function maxExpandRight(
|
|
|
94
45
|
return Math.max(0, e)
|
|
95
46
|
}
|
|
96
47
|
|
|
97
|
-
function maxExpandDown(
|
|
98
|
-
r
|
|
99
|
-
bounds: XYRect,
|
|
100
|
-
blockers: XYRect[],
|
|
101
|
-
maxAspect: number | null | undefined,
|
|
102
|
-
) {
|
|
48
|
+
function maxExpandDown(params: ExpandDirectionParams) {
|
|
49
|
+
const { r, bounds, blockers, maxAspect } = params
|
|
103
50
|
// Start with board boundary
|
|
104
51
|
let maxHeight = bounds.y + bounds.height - r.y
|
|
105
52
|
|
|
@@ -134,12 +81,8 @@ function maxExpandDown(
|
|
|
134
81
|
return Math.max(0, e)
|
|
135
82
|
}
|
|
136
83
|
|
|
137
|
-
function maxExpandLeft(
|
|
138
|
-
r
|
|
139
|
-
bounds: XYRect,
|
|
140
|
-
blockers: XYRect[],
|
|
141
|
-
maxAspect: number | null | undefined,
|
|
142
|
-
) {
|
|
84
|
+
function maxExpandLeft(params: ExpandDirectionParams) {
|
|
85
|
+
const { r, bounds, blockers, maxAspect } = params
|
|
143
86
|
// Start with board boundary
|
|
144
87
|
let minX = bounds.x
|
|
145
88
|
|
|
@@ -172,12 +115,8 @@ function maxExpandLeft(
|
|
|
172
115
|
return Math.max(0, e)
|
|
173
116
|
}
|
|
174
117
|
|
|
175
|
-
function maxExpandUp(
|
|
176
|
-
r
|
|
177
|
-
bounds: XYRect,
|
|
178
|
-
blockers: XYRect[],
|
|
179
|
-
maxAspect: number | null | undefined,
|
|
180
|
-
) {
|
|
118
|
+
function maxExpandUp(params: ExpandDirectionParams) {
|
|
119
|
+
const { r, bounds, blockers, maxAspect } = params
|
|
181
120
|
// Start with board boundary
|
|
182
121
|
let minY = bounds.y
|
|
183
122
|
|
|
@@ -271,25 +210,27 @@ export function expandRectFromSeed(params: {
|
|
|
271
210
|
let improved = true
|
|
272
211
|
while (improved) {
|
|
273
212
|
improved = false
|
|
274
|
-
const
|
|
213
|
+
const commonParams = { bounds, blockers, maxAspect: maxAspectRatio }
|
|
214
|
+
|
|
215
|
+
const eR = maxExpandRight({ ...commonParams, r })
|
|
275
216
|
if (eR > 0) {
|
|
276
217
|
r = { ...r, width: r.width + eR }
|
|
277
218
|
improved = true
|
|
278
219
|
}
|
|
279
220
|
|
|
280
|
-
const eD = maxExpandDown(
|
|
221
|
+
const eD = maxExpandDown({ ...commonParams, r })
|
|
281
222
|
if (eD > 0) {
|
|
282
223
|
r = { ...r, height: r.height + eD }
|
|
283
224
|
improved = true
|
|
284
225
|
}
|
|
285
226
|
|
|
286
|
-
const eL = maxExpandLeft(
|
|
227
|
+
const eL = maxExpandLeft({ ...commonParams, r })
|
|
287
228
|
if (eL > 0) {
|
|
288
229
|
r = { x: r.x - eL, y: r.y, width: r.width + eL, height: r.height }
|
|
289
230
|
improved = true
|
|
290
231
|
}
|
|
291
232
|
|
|
292
|
-
const eU = maxExpandUp(
|
|
233
|
+
const eU = maxExpandUp({ ...commonParams, r })
|
|
293
234
|
if (eU > 0) {
|
|
294
235
|
r = { x: r.x, y: r.y - eU, width: r.width, height: r.height + eU }
|
|
295
236
|
improved = true
|
|
@@ -307,43 +248,3 @@ export function expandRectFromSeed(params: {
|
|
|
307
248
|
|
|
308
249
|
return best
|
|
309
250
|
}
|
|
310
|
-
|
|
311
|
-
/** Find the intersection of two 1D intervals, or null if they don't overlap. */
|
|
312
|
-
export function intersect1D(a0: number, a1: number, b0: number, b1: number) {
|
|
313
|
-
const lo = Math.max(a0, b0)
|
|
314
|
-
const hi = Math.min(a1, b1)
|
|
315
|
-
return hi > lo + EPS ? ([lo, hi] as const) : null
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/** Return A \ B as up to 4 non-overlapping rectangles (or [A] if no overlap). */
|
|
319
|
-
export function subtractRect2D(A: XYRect, B: XYRect): XYRect[] {
|
|
320
|
-
if (!overlaps(A, B)) return [A]
|
|
321
|
-
|
|
322
|
-
const Xi = intersect1D(A.x, A.x + A.width, B.x, B.x + B.width)
|
|
323
|
-
const Yi = intersect1D(A.y, A.y + A.height, B.y, B.y + B.height)
|
|
324
|
-
if (!Xi || !Yi) return [A]
|
|
325
|
-
|
|
326
|
-
const [X0, X1] = Xi
|
|
327
|
-
const [Y0, Y1] = Yi
|
|
328
|
-
const out: XYRect[] = []
|
|
329
|
-
|
|
330
|
-
// Left strip
|
|
331
|
-
if (X0 > A.x + EPS) {
|
|
332
|
-
out.push({ x: A.x, y: A.y, width: X0 - A.x, height: A.height })
|
|
333
|
-
}
|
|
334
|
-
// Right strip
|
|
335
|
-
if (A.x + A.width > X1 + EPS) {
|
|
336
|
-
out.push({ x: X1, y: A.y, width: A.x + A.width - X1, height: A.height })
|
|
337
|
-
}
|
|
338
|
-
// Top wedge in the middle band
|
|
339
|
-
const midW = Math.max(0, X1 - X0)
|
|
340
|
-
if (midW > EPS && Y0 > A.y + EPS) {
|
|
341
|
-
out.push({ x: X0, y: A.y, width: midW, height: Y0 - A.y })
|
|
342
|
-
}
|
|
343
|
-
// Bottom wedge in the middle band
|
|
344
|
-
if (midW > EPS && A.y + A.height > Y1 + EPS) {
|
|
345
|
-
out.push({ x: X0, y: Y1, width: midW, height: A.y + A.height - Y1 })
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
return out.filter((r) => r.width > EPS && r.height > EPS)
|
|
349
|
-
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Placed3D, Rect3d, XYRect } from "../rectdiff-types"
|
|
2
|
+
import type { SimpleRouteJson } from "../types/srj-types"
|
|
3
|
+
import {
|
|
4
|
+
buildZIndexMap,
|
|
5
|
+
obstacleToXYRect,
|
|
6
|
+
obstacleZs,
|
|
7
|
+
} from "../solvers/RectDiffSeedingSolver/layers"
|
|
8
|
+
|
|
9
|
+
export function finalizeRects(params: {
|
|
10
|
+
placed: Placed3D[]
|
|
11
|
+
srj: SimpleRouteJson
|
|
12
|
+
boardVoidRects: XYRect[]
|
|
13
|
+
}): Rect3d[] {
|
|
14
|
+
// Convert all placed (free space) nodes to output format
|
|
15
|
+
const out: Rect3d[] = params.placed.map((p) => ({
|
|
16
|
+
minX: p.rect.x,
|
|
17
|
+
minY: p.rect.y,
|
|
18
|
+
maxX: p.rect.x + p.rect.width,
|
|
19
|
+
maxY: p.rect.y + p.rect.height,
|
|
20
|
+
zLayers: [...p.zLayers].sort((a, b) => a - b),
|
|
21
|
+
}))
|
|
22
|
+
|
|
23
|
+
const { zIndexByName } = buildZIndexMap(params.srj)
|
|
24
|
+
const layersByKey = new Map<string, { rect: XYRect; layers: Set<number> }>()
|
|
25
|
+
|
|
26
|
+
for (const obstacle of params.srj.obstacles ?? []) {
|
|
27
|
+
const rect = obstacleToXYRect(obstacle as any)
|
|
28
|
+
if (!rect) continue
|
|
29
|
+
const zLayers =
|
|
30
|
+
obstacle.zLayers?.length && obstacle.zLayers.length > 0
|
|
31
|
+
? obstacle.zLayers
|
|
32
|
+
: obstacleZs(obstacle as any, zIndexByName)
|
|
33
|
+
const key = `${rect.x}:${rect.y}:${rect.width}:${rect.height}`
|
|
34
|
+
let entry = layersByKey.get(key)
|
|
35
|
+
if (!entry) {
|
|
36
|
+
entry = { rect, layers: new Set() }
|
|
37
|
+
layersByKey.set(key, entry)
|
|
38
|
+
}
|
|
39
|
+
zLayers.forEach((layer) => entry!.layers.add(layer))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (const { rect, layers } of layersByKey.values()) {
|
|
43
|
+
out.push({
|
|
44
|
+
minX: rect.x,
|
|
45
|
+
minY: rect.y,
|
|
46
|
+
maxX: rect.x + rect.width,
|
|
47
|
+
maxY: rect.y + rect.height,
|
|
48
|
+
zLayers: Array.from(layers).sort((a, b) => a - b),
|
|
49
|
+
isObstacle: true,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return out
|
|
54
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { RTreeRect } from "lib/types/capacity-mesh-types"
|
|
2
|
+
import RBush from "rbush"
|
|
3
|
+
|
|
4
|
+
export type OccupancyParams = {
|
|
5
|
+
layerCount: number
|
|
6
|
+
obstacleIndexByLayer: Array<RBush<RTreeRect> | undefined>
|
|
7
|
+
placedIndexByLayer: Array<RBush<RTreeRect> | undefined>
|
|
8
|
+
point: { x: number; y: number }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isFullyOccupiedAtPoint(params: OccupancyParams): boolean {
|
|
12
|
+
const query = {
|
|
13
|
+
minX: params.point.x,
|
|
14
|
+
minY: params.point.y,
|
|
15
|
+
maxX: params.point.x,
|
|
16
|
+
maxY: params.point.y,
|
|
17
|
+
}
|
|
18
|
+
for (let z = 0; z < params.layerCount; z++) {
|
|
19
|
+
const obstacleIdx = params.obstacleIndexByLayer[z]
|
|
20
|
+
const hasObstacle = !!obstacleIdx && obstacleIdx.search(query).length > 0
|
|
21
|
+
|
|
22
|
+
const placedIdx = params.placedIndexByLayer[z]
|
|
23
|
+
const hasPlaced = !!placedIdx && placedIdx.search(query).length > 0
|
|
24
|
+
|
|
25
|
+
if (!hasObstacle && !hasPlaced) return false
|
|
26
|
+
}
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { XYRect } from "lib/rectdiff-types"
|
|
2
|
+
import type { RTreeRect } from "lib/types/capacity-mesh-types"
|
|
3
|
+
|
|
4
|
+
export const rectToTree = (rect: XYRect): RTreeRect => ({
|
|
5
|
+
...rect,
|
|
6
|
+
minX: rect.x,
|
|
7
|
+
minY: rect.y,
|
|
8
|
+
maxX: rect.x + rect.width,
|
|
9
|
+
maxY: rect.y + rect.height,
|
|
10
|
+
})
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { XYRect } from "../rectdiff-types"
|
|
2
|
+
|
|
3
|
+
export const EPS = 1e-9
|
|
4
|
+
export const clamp = (v: number, lo: number, hi: number) =>
|
|
5
|
+
Math.max(lo, Math.min(hi, v))
|
|
6
|
+
export const gt = (a: number, b: number) => a > b + EPS
|
|
7
|
+
export const gte = (a: number, b: number) => a > b - EPS
|
|
8
|
+
export const lt = (a: number, b: number) => a < b - EPS
|
|
9
|
+
export const lte = (a: number, b: number) => a < b + EPS
|
|
10
|
+
|
|
11
|
+
export function overlaps(a: XYRect, b: XYRect) {
|
|
12
|
+
return !(
|
|
13
|
+
a.x + a.width <= b.x + EPS ||
|
|
14
|
+
b.x + b.width <= a.x + EPS ||
|
|
15
|
+
a.y + a.height <= b.y + EPS ||
|
|
16
|
+
b.y + b.height <= a.y + EPS
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function containsPoint(r: XYRect, p: { x: number; y: number }) {
|
|
21
|
+
return (
|
|
22
|
+
p.x >= r.x - EPS &&
|
|
23
|
+
p.x <= r.x + r.width + EPS &&
|
|
24
|
+
p.y >= r.y - EPS &&
|
|
25
|
+
p.y <= r.y + r.height + EPS
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function distancePointToRectEdges(
|
|
30
|
+
p: { x: number; y: number },
|
|
31
|
+
r: XYRect,
|
|
32
|
+
) {
|
|
33
|
+
const edges: [number, number, number, number][] = [
|
|
34
|
+
[r.x, r.y, r.x + r.width, r.y],
|
|
35
|
+
[r.x + r.width, r.y, r.x + r.width, r.y + r.height],
|
|
36
|
+
[r.x + r.width, r.y + r.height, r.x, r.y + r.height],
|
|
37
|
+
[r.x, r.y + r.height, r.x, r.y],
|
|
38
|
+
]
|
|
39
|
+
let best = Infinity
|
|
40
|
+
for (const [x1, y1, x2, y2] of edges) {
|
|
41
|
+
const A = p.x - x1,
|
|
42
|
+
B = p.y - y1,
|
|
43
|
+
C = x2 - x1,
|
|
44
|
+
D = y2 - y1
|
|
45
|
+
const dot = A * C + B * D
|
|
46
|
+
const lenSq = C * C + D * D
|
|
47
|
+
let t = lenSq !== 0 ? dot / lenSq : 0
|
|
48
|
+
t = clamp(t, 0, 1)
|
|
49
|
+
const xx = x1 + t * C
|
|
50
|
+
const yy = y1 + t * D
|
|
51
|
+
best = Math.min(best, Math.hypot(p.x - xx, p.y - yy))
|
|
52
|
+
}
|
|
53
|
+
return best
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Find the intersection of two 1D intervals, or null if they don't overlap. */
|
|
57
|
+
export function intersect1D(r1: [number, number], r2: [number, number]) {
|
|
58
|
+
const lo = Math.max(r1[0], r2[0])
|
|
59
|
+
const hi = Math.min(r1[1], r2[1])
|
|
60
|
+
return hi > lo + EPS ? ([lo, hi] as const) : null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Return A \ B as up to 4 non-overlapping rectangles (or [A] if no overlap). */
|
|
64
|
+
export function subtractRect2D(A: XYRect, B: XYRect): XYRect[] {
|
|
65
|
+
if (!overlaps(A, B)) return [A]
|
|
66
|
+
|
|
67
|
+
const Xi = intersect1D([A.x, A.x + A.width], [B.x, B.x + B.width])
|
|
68
|
+
const Yi = intersect1D([A.y, A.y + A.height], [B.y, B.y + B.height])
|
|
69
|
+
if (!Xi || !Yi) return [A]
|
|
70
|
+
|
|
71
|
+
const [X0, X1] = Xi
|
|
72
|
+
const [Y0, Y1] = Yi
|
|
73
|
+
const out: XYRect[] = []
|
|
74
|
+
|
|
75
|
+
// Left strip
|
|
76
|
+
if (X0 > A.x + EPS) {
|
|
77
|
+
out.push({ x: A.x, y: A.y, width: X0 - A.x, height: A.height })
|
|
78
|
+
}
|
|
79
|
+
// Right strip
|
|
80
|
+
if (A.x + A.width > X1 + EPS) {
|
|
81
|
+
out.push({ x: X1, y: A.y, width: A.x + A.width - X1, height: A.height })
|
|
82
|
+
}
|
|
83
|
+
// Top wedge in the middle band
|
|
84
|
+
const midW = Math.max(0, X1 - X0)
|
|
85
|
+
if (midW > EPS && Y0 > A.y + EPS) {
|
|
86
|
+
out.push({ x: X0, y: A.y, width: midW, height: Y0 - A.y })
|
|
87
|
+
}
|
|
88
|
+
// Bottom wedge in the middle band
|
|
89
|
+
if (midW > EPS && A.y + A.height > Y1 + EPS) {
|
|
90
|
+
out.push({ x: X0, y: Y1, width: midW, height: A.y + A.height - Y1 })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return out.filter((r) => r.width > EPS && r.height > EPS)
|
|
94
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { RTreeRect } from "lib/types/capacity-mesh-types"
|
|
2
|
+
import type { Placed3D, XYRect } from "../rectdiff-types"
|
|
3
|
+
import { overlaps, subtractRect2D, EPS } from "./rectdiff-geometry"
|
|
4
|
+
import type RBush from "rbush"
|
|
5
|
+
|
|
6
|
+
export function resizeSoftOverlaps(
|
|
7
|
+
params: {
|
|
8
|
+
layerCount: number
|
|
9
|
+
placed: Placed3D[]
|
|
10
|
+
options: any
|
|
11
|
+
placedIndexByLayer?: Array<RBush<RTreeRect> | undefined>
|
|
12
|
+
},
|
|
13
|
+
newIndex: number,
|
|
14
|
+
) {
|
|
15
|
+
const newcomer = params.placed[newIndex]!
|
|
16
|
+
const { rect: newR, zLayers: newZs } = newcomer
|
|
17
|
+
const layerCount = params.layerCount
|
|
18
|
+
|
|
19
|
+
const removeIdx: number[] = []
|
|
20
|
+
const toAdd: typeof params.placed = []
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < params.placed.length; i++) {
|
|
23
|
+
if (i === newIndex) continue
|
|
24
|
+
const old = params.placed[i]!
|
|
25
|
+
// Protect full-stack nodes
|
|
26
|
+
if (old.zLayers.length >= layerCount) continue
|
|
27
|
+
|
|
28
|
+
const sharedZ = old.zLayers.filter((z) => newZs.includes(z))
|
|
29
|
+
if (sharedZ.length === 0) continue
|
|
30
|
+
if (!overlaps(old.rect, newR)) continue
|
|
31
|
+
|
|
32
|
+
// Carve the overlap on the shared layers
|
|
33
|
+
const parts = subtractRect2D(old.rect, newR)
|
|
34
|
+
|
|
35
|
+
// We will replace `old` entirely; re-add unaffected layers (same rect object).
|
|
36
|
+
removeIdx.push(i)
|
|
37
|
+
|
|
38
|
+
const unaffectedZ = old.zLayers.filter((z) => !newZs.includes(z))
|
|
39
|
+
if (unaffectedZ.length > 0) {
|
|
40
|
+
toAdd.push({ rect: old.rect, zLayers: unaffectedZ })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Re-add carved pieces for affected layers, dropping tiny slivers
|
|
44
|
+
const minW = Math.min(
|
|
45
|
+
params.options.minSingle.width,
|
|
46
|
+
params.options.minMulti.width,
|
|
47
|
+
)
|
|
48
|
+
const minH = Math.min(
|
|
49
|
+
params.options.minSingle.height,
|
|
50
|
+
params.options.minMulti.height,
|
|
51
|
+
)
|
|
52
|
+
for (const p of parts) {
|
|
53
|
+
if (p.width + EPS >= minW && p.height + EPS >= minH) {
|
|
54
|
+
toAdd.push({ rect: p, zLayers: sharedZ.slice() })
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Remove fully overlapped nodes and keep indexes in sync
|
|
60
|
+
const rectToTree = (rect: XYRect): RTreeRect => ({
|
|
61
|
+
...rect,
|
|
62
|
+
minX: rect.x,
|
|
63
|
+
minY: rect.y,
|
|
64
|
+
maxX: rect.x + rect.width,
|
|
65
|
+
maxY: rect.y + rect.height,
|
|
66
|
+
})
|
|
67
|
+
const sameRect = (a: RTreeRect, b: RTreeRect) =>
|
|
68
|
+
a.minX === b.minX &&
|
|
69
|
+
a.minY === b.minY &&
|
|
70
|
+
a.maxX === b.maxX &&
|
|
71
|
+
a.maxY === b.maxY
|
|
72
|
+
|
|
73
|
+
removeIdx
|
|
74
|
+
.sort((a, b) => b - a)
|
|
75
|
+
.forEach((idx) => {
|
|
76
|
+
const rem = params.placed.splice(idx, 1)[0]!
|
|
77
|
+
if (params.placedIndexByLayer) {
|
|
78
|
+
for (const z of rem.zLayers) {
|
|
79
|
+
const tree = params.placedIndexByLayer[z]
|
|
80
|
+
if (tree) tree.remove(rectToTree(rem.rect), sameRect)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// Add replacements
|
|
86
|
+
for (const p of toAdd) {
|
|
87
|
+
params.placed.push(p)
|
|
88
|
+
for (const z of p.zLayers) {
|
|
89
|
+
if (params.placedIndexByLayer) {
|
|
90
|
+
const idx = params.placedIndexByLayer[z]
|
|
91
|
+
if (idx) {
|
|
92
|
+
idx.insert({
|
|
93
|
+
...p.rect,
|
|
94
|
+
minX: p.rect.x,
|
|
95
|
+
minY: p.rect.y,
|
|
96
|
+
maxX: p.rect.x + p.rect.width,
|
|
97
|
+
maxY: p.rect.y + p.rect.height,
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
package/package.json
CHANGED
|
@@ -11,7 +11,8 @@ test("board outline snapshot", async () => {
|
|
|
11
11
|
// Run to completion
|
|
12
12
|
solver.solve()
|
|
13
13
|
|
|
14
|
-
const viz =
|
|
14
|
+
const viz =
|
|
15
|
+
solver.rectDiffGridSolverPipeline!.rectDiffSeedingSolver!.visualize()
|
|
15
16
|
const svg = getSvgFromGraphicsObject(viz)
|
|
16
17
|
|
|
17
18
|
await expect(svg).toMatchSvgSnapshot(import.meta.path)
|
|
@@ -2,6 +2,11 @@ import { expect, test } from "bun:test"
|
|
|
2
2
|
import simpleRouteJson from "../../test-assets/example01.json"
|
|
3
3
|
import { RectDiffPipeline } from "../../lib/RectDiffPipeline"
|
|
4
4
|
import { getSvgFromGraphicsObject } from "graphics-debug"
|
|
5
|
+
import {
|
|
6
|
+
buildZIndexMap,
|
|
7
|
+
obstacleToXYRect,
|
|
8
|
+
obstacleZs,
|
|
9
|
+
} from "lib/solvers/RectDiffSeedingSolver/layers"
|
|
5
10
|
|
|
6
11
|
test.skip("example01", () => {
|
|
7
12
|
const solver = new RectDiffPipeline({ simpleRouteJson })
|
|
@@ -17,7 +22,19 @@ test.skip("example01", () => {
|
|
|
17
22
|
const step = 0.004
|
|
18
23
|
const layerCount = simpleRouteJson.layerCount || 2
|
|
19
24
|
const state = (solver as any).state
|
|
20
|
-
const
|
|
25
|
+
const { zIndexByName } = buildZIndexMap(simpleRouteJson as any)
|
|
26
|
+
const obstacles = Array.from({ length: layerCount }, () => [] as any[])
|
|
27
|
+
for (const obstacle of simpleRouteJson.obstacles ?? []) {
|
|
28
|
+
const rect = obstacleToXYRect(obstacle as any)
|
|
29
|
+
if (!rect) continue
|
|
30
|
+
const zLayers =
|
|
31
|
+
obstacle.zLayers?.length && obstacle.zLayers.length > 0
|
|
32
|
+
? obstacle.zLayers
|
|
33
|
+
: obstacleZs(obstacle as any, zIndexByName)
|
|
34
|
+
zLayers.forEach((z: number) => {
|
|
35
|
+
if (z >= 0 && z < layerCount) obstacles[z]!.push(rect)
|
|
36
|
+
})
|
|
37
|
+
}
|
|
21
38
|
const placed = state.placed
|
|
22
39
|
|
|
23
40
|
const gapPoints: Array<{ x: number; y: number; z: number }> = []
|
|
@@ -40,5 +40,5 @@ test("RectDiffSolver clamps extra layer names to available z indices", () => {
|
|
|
40
40
|
expect(output.meshNodes.length).toBeGreaterThan(0)
|
|
41
41
|
|
|
42
42
|
// Verify solver was instantiated and processed obstacles
|
|
43
|
-
expect(pipeline.
|
|
43
|
+
expect(pipeline.rectDiffGridSolverPipeline).toBeDefined()
|
|
44
44
|
})
|
|
@@ -41,5 +41,5 @@ test("RectDiffSolver maps obstacle layers to numeric zLayers", () => {
|
|
|
41
41
|
|
|
42
42
|
// Verify obstacles were processed correctly
|
|
43
43
|
// The internal solver should have mapped layer names to z indices
|
|
44
|
-
expect(pipeline.
|
|
44
|
+
expect(pipeline.rectDiffGridSolverPipeline).toBeDefined()
|
|
45
45
|
})
|
package/utils/rectsEqual.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// utils/rectsEqual.ts
|
|
2
|
-
import type { XYRect } from "../lib/
|
|
3
|
-
import { EPS } from "../lib/
|
|
2
|
+
import type { XYRect } from "../lib/rectdiff-types"
|
|
3
|
+
import { EPS } from "../lib/utils/rectdiff-geometry"
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Checks if two rectangles are equal within a small tolerance (EPS).
|
package/utils/rectsOverlap.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// utils/rectsOverlap.ts
|
|
2
|
-
import type { XYRect } from "../lib/
|
|
3
|
-
import { EPS } from "../lib/
|
|
2
|
+
import type { XYRect } from "../lib/rectdiff-types"
|
|
3
|
+
import { EPS } from "../lib/utils/rectdiff-geometry"
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Checks if two rectangles overlap.
|