@tscircuit/rectdiff 0.0.11 → 0.0.13
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 +163 -27
- package/dist/index.js +1885 -1676
- 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/GapFillSolverPipeline.ts +1 -1
- package/lib/solvers/GapFillSolver/visuallyOffsetLine.ts +5 -2
- package/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts +205 -0
- package/lib/solvers/{rectdiff → RectDiffExpansionSolver}/rectsToMeshNodes.ts +1 -2
- package/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts +87 -0
- package/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +516 -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} +39 -220
- 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 +52 -0
- package/lib/utils/buildHardPlacedByLayer.ts +14 -0
- package/lib/{solvers/rectdiff/geometry.ts → utils/expandRectFromSeed.ts} +21 -120
- package/lib/utils/finalizeRects.ts +49 -0
- package/lib/utils/isFullyOccupiedAtPoint.ts +21 -0
- package/lib/utils/rectdiff-geometry.ts +94 -0
- package/lib/utils/resizeSoftOverlaps.ts +74 -0
- package/package.json +1 -1
- package/tests/board-outline.test.ts +2 -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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { XYRect } from "../rectdiff-types"
|
|
2
|
+
import { containsPoint } from "./rectdiff-geometry"
|
|
3
|
+
|
|
4
|
+
export function isFullyOccupiedAtPoint(
|
|
5
|
+
params: {
|
|
6
|
+
layerCount: number
|
|
7
|
+
obstaclesByLayer: XYRect[][]
|
|
8
|
+
placedByLayer: XYRect[][]
|
|
9
|
+
},
|
|
10
|
+
point: { x: number; y: number },
|
|
11
|
+
): boolean {
|
|
12
|
+
for (let z = 0; z < params.layerCount; z++) {
|
|
13
|
+
const obs = params.obstaclesByLayer[z] ?? []
|
|
14
|
+
const placed = params.placedByLayer[z] ?? []
|
|
15
|
+
const occ =
|
|
16
|
+
obs.some((b) => containsPoint(b, point)) ||
|
|
17
|
+
placed.some((b) => containsPoint(b, point))
|
|
18
|
+
if (!occ) return false
|
|
19
|
+
}
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
@@ -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,74 @@
|
|
|
1
|
+
import type { Placed3D, XYRect } from "../rectdiff-types"
|
|
2
|
+
import { overlaps, subtractRect2D, EPS } from "./rectdiff-geometry"
|
|
3
|
+
|
|
4
|
+
export function resizeSoftOverlaps(
|
|
5
|
+
params: {
|
|
6
|
+
layerCount: number
|
|
7
|
+
placed: Placed3D[]
|
|
8
|
+
placedByLayer: XYRect[][]
|
|
9
|
+
options: any
|
|
10
|
+
},
|
|
11
|
+
newIndex: number,
|
|
12
|
+
) {
|
|
13
|
+
const newcomer = params.placed[newIndex]!
|
|
14
|
+
const { rect: newR, zLayers: newZs } = newcomer
|
|
15
|
+
const layerCount = params.layerCount
|
|
16
|
+
|
|
17
|
+
const removeIdx: number[] = []
|
|
18
|
+
const toAdd: typeof params.placed = []
|
|
19
|
+
|
|
20
|
+
for (let i = 0; i < params.placed.length; i++) {
|
|
21
|
+
if (i === newIndex) continue
|
|
22
|
+
const old = params.placed[i]!
|
|
23
|
+
// Protect full-stack nodes
|
|
24
|
+
if (old.zLayers.length >= layerCount) continue
|
|
25
|
+
|
|
26
|
+
const sharedZ = old.zLayers.filter((z) => newZs.includes(z))
|
|
27
|
+
if (sharedZ.length === 0) continue
|
|
28
|
+
if (!overlaps(old.rect, newR)) continue
|
|
29
|
+
|
|
30
|
+
// Carve the overlap on the shared layers
|
|
31
|
+
const parts = subtractRect2D(old.rect, newR)
|
|
32
|
+
|
|
33
|
+
// We will replace `old` entirely; re-add unaffected layers (same rect object).
|
|
34
|
+
removeIdx.push(i)
|
|
35
|
+
|
|
36
|
+
const unaffectedZ = old.zLayers.filter((z) => !newZs.includes(z))
|
|
37
|
+
if (unaffectedZ.length > 0) {
|
|
38
|
+
toAdd.push({ rect: old.rect, zLayers: unaffectedZ })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Re-add carved pieces for affected layers, dropping tiny slivers
|
|
42
|
+
const minW = Math.min(
|
|
43
|
+
params.options.minSingle.width,
|
|
44
|
+
params.options.minMulti.width,
|
|
45
|
+
)
|
|
46
|
+
const minH = Math.min(
|
|
47
|
+
params.options.minSingle.height,
|
|
48
|
+
params.options.minMulti.height,
|
|
49
|
+
)
|
|
50
|
+
for (const p of parts) {
|
|
51
|
+
if (p.width + EPS >= minW && p.height + EPS >= minH) {
|
|
52
|
+
toAdd.push({ rect: p, zLayers: sharedZ.slice() })
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Remove (and clear placedByLayer)
|
|
58
|
+
removeIdx
|
|
59
|
+
.sort((a, b) => b - a)
|
|
60
|
+
.forEach((idx) => {
|
|
61
|
+
const rem = params.placed.splice(idx, 1)[0]!
|
|
62
|
+
for (const z of rem.zLayers) {
|
|
63
|
+
const arr = params.placedByLayer[z]!
|
|
64
|
+
const j = arr.findIndex((r) => r === rem.rect)
|
|
65
|
+
if (j >= 0) arr.splice(j, 1)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// Add replacements
|
|
70
|
+
for (const p of toAdd) {
|
|
71
|
+
params.placed.push(p)
|
|
72
|
+
for (const z of p.zLayers) params.placedByLayer[z]!.push(p.rect)
|
|
73
|
+
}
|
|
74
|
+
}
|
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)
|
|
@@ -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.
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
// lib/solvers/RectDiffSolver.ts
|
|
2
|
-
import { BaseSolver, BasePipelineSolver } from "@tscircuit/solver-utils"
|
|
3
|
-
import type { SimpleRouteJson } from "../types/srj-types"
|
|
4
|
-
import type { GraphicsObject } from "graphics-debug"
|
|
5
|
-
import type { CapacityMeshNode } from "../types/capacity-mesh-types"
|
|
6
|
-
|
|
7
|
-
import type { GridFill3DOptions, RectDiffState } from "./rectdiff/types"
|
|
8
|
-
import {
|
|
9
|
-
initState,
|
|
10
|
-
stepGrid,
|
|
11
|
-
stepExpansion,
|
|
12
|
-
finalizeRects,
|
|
13
|
-
computeProgress,
|
|
14
|
-
} from "./rectdiff/engine"
|
|
15
|
-
import { rectsToMeshNodes } from "./rectdiff/rectsToMeshNodes"
|
|
16
|
-
import { overlaps } from "./rectdiff/geometry"
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* A streaming, one-step-per-iteration solver for capacity mesh generation.
|
|
20
|
-
*/
|
|
21
|
-
export class RectDiffSolver extends BaseSolver {
|
|
22
|
-
private srj: SimpleRouteJson
|
|
23
|
-
private gridOptions: Partial<GridFill3DOptions>
|
|
24
|
-
private state!: RectDiffState
|
|
25
|
-
private _meshNodes: CapacityMeshNode[] = []
|
|
26
|
-
|
|
27
|
-
constructor(opts: {
|
|
28
|
-
simpleRouteJson: SimpleRouteJson
|
|
29
|
-
gridOptions?: Partial<GridFill3DOptions>
|
|
30
|
-
}) {
|
|
31
|
-
super()
|
|
32
|
-
this.srj = opts.simpleRouteJson
|
|
33
|
-
this.gridOptions = opts.gridOptions ?? {}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
override _setup() {
|
|
37
|
-
this.state = initState(this.srj, this.gridOptions)
|
|
38
|
-
this.stats = {
|
|
39
|
-
phase: this.state.phase,
|
|
40
|
-
gridIndex: this.state.gridIndex,
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Exactly ONE small step per call. */
|
|
45
|
-
override _step() {
|
|
46
|
-
if (this.state.phase === "GRID") {
|
|
47
|
-
stepGrid(this.state)
|
|
48
|
-
} else if (this.state.phase === "EXPANSION") {
|
|
49
|
-
stepExpansion(this.state)
|
|
50
|
-
} else if (this.state.phase === "GAP_FILL") {
|
|
51
|
-
this.state.phase = "DONE"
|
|
52
|
-
} else if (this.state.phase === "DONE") {
|
|
53
|
-
// Finalize once
|
|
54
|
-
if (!this.solved) {
|
|
55
|
-
const rects = finalizeRects(this.state)
|
|
56
|
-
this._meshNodes = rectsToMeshNodes(rects)
|
|
57
|
-
this.solved = true
|
|
58
|
-
}
|
|
59
|
-
return
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Lightweight stats for debugger
|
|
63
|
-
this.stats.phase = this.state.phase
|
|
64
|
-
this.stats.gridIndex = this.state.gridIndex
|
|
65
|
-
this.stats.placed = this.state.placed.length
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Compute solver progress (0 to 1). */
|
|
69
|
-
computeProgress(): number {
|
|
70
|
-
if (this.solved || this.state.phase === "DONE") {
|
|
71
|
-
return 1
|
|
72
|
-
}
|
|
73
|
-
return computeProgress(this.state)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
override getOutput(): { meshNodes: CapacityMeshNode[] } {
|
|
77
|
-
return { meshNodes: this._meshNodes }
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Get color based on z layer for visualization. */
|
|
81
|
-
private getColorForZLayer(zLayers: number[]): {
|
|
82
|
-
fill: string
|
|
83
|
-
stroke: string
|
|
84
|
-
} {
|
|
85
|
-
const minZ = Math.min(...zLayers)
|
|
86
|
-
const colors = [
|
|
87
|
-
{ fill: "#dbeafe", stroke: "#3b82f6" },
|
|
88
|
-
{ fill: "#fef3c7", stroke: "#f59e0b" },
|
|
89
|
-
{ fill: "#d1fae5", stroke: "#10b981" },
|
|
90
|
-
{ fill: "#e9d5ff", stroke: "#a855f7" },
|
|
91
|
-
{ fill: "#fed7aa", stroke: "#f97316" },
|
|
92
|
-
{ fill: "#fecaca", stroke: "#ef4444" },
|
|
93
|
-
]
|
|
94
|
-
return colors[minZ % colors.length]!
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Streaming visualization: board + obstacles + current placements. */
|
|
98
|
-
override visualize(): GraphicsObject {
|
|
99
|
-
const rects: NonNullable<GraphicsObject["rects"]> = []
|
|
100
|
-
const points: NonNullable<GraphicsObject["points"]> = []
|
|
101
|
-
const lines: NonNullable<GraphicsObject["lines"]> = [] // Initialize lines array
|
|
102
|
-
|
|
103
|
-
// Board bounds - use srj bounds which is always available
|
|
104
|
-
const boardBounds = {
|
|
105
|
-
minX: this.srj.bounds.minX,
|
|
106
|
-
maxX: this.srj.bounds.maxX,
|
|
107
|
-
minY: this.srj.bounds.minY,
|
|
108
|
-
maxY: this.srj.bounds.maxY,
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// board or outline
|
|
112
|
-
if (this.srj.outline && this.srj.outline.length > 1) {
|
|
113
|
-
lines.push({
|
|
114
|
-
points: [...this.srj.outline, this.srj.outline[0]!], // Close the loop by adding the first point again
|
|
115
|
-
strokeColor: "#111827",
|
|
116
|
-
strokeWidth: 0.01,
|
|
117
|
-
label: "outline",
|
|
118
|
-
})
|
|
119
|
-
} else {
|
|
120
|
-
rects.push({
|
|
121
|
-
center: {
|
|
122
|
-
x: (boardBounds.minX + boardBounds.maxX) / 2,
|
|
123
|
-
y: (boardBounds.minY + boardBounds.maxY) / 2,
|
|
124
|
-
},
|
|
125
|
-
width: boardBounds.maxX - boardBounds.minX,
|
|
126
|
-
height: boardBounds.maxY - boardBounds.minY,
|
|
127
|
-
fill: "none",
|
|
128
|
-
stroke: "#111827",
|
|
129
|
-
label: "board",
|
|
130
|
-
})
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// obstacles (rect & oval as bounding boxes)
|
|
134
|
-
for (const obstacle of this.srj.obstacles ?? []) {
|
|
135
|
-
if (obstacle.type === "rect" || obstacle.type === "oval") {
|
|
136
|
-
rects.push({
|
|
137
|
-
center: { x: obstacle.center.x, y: obstacle.center.y },
|
|
138
|
-
width: obstacle.width,
|
|
139
|
-
height: obstacle.height,
|
|
140
|
-
fill: "#fee2e2",
|
|
141
|
-
stroke: "#ef4444",
|
|
142
|
-
layer: "obstacle",
|
|
143
|
-
label: "obstacle",
|
|
144
|
-
})
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// board void rects
|
|
149
|
-
if (this.state?.boardVoidRects) {
|
|
150
|
-
// If outline exists, compute its bbox to hide outer padding voids
|
|
151
|
-
let outlineBBox: {
|
|
152
|
-
x: number
|
|
153
|
-
y: number
|
|
154
|
-
width: number
|
|
155
|
-
height: number
|
|
156
|
-
} | null = null
|
|
157
|
-
|
|
158
|
-
if (this.srj.outline && this.srj.outline.length > 0) {
|
|
159
|
-
const xs = this.srj.outline.map((p) => p.x)
|
|
160
|
-
const ys = this.srj.outline.map((p) => p.y)
|
|
161
|
-
const minX = Math.min(...xs)
|
|
162
|
-
const minY = Math.min(...ys)
|
|
163
|
-
outlineBBox = {
|
|
164
|
-
x: minX,
|
|
165
|
-
y: minY,
|
|
166
|
-
width: Math.max(...xs) - minX,
|
|
167
|
-
height: Math.max(...ys) - minY,
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
for (const r of this.state.boardVoidRects) {
|
|
172
|
-
// If we have an outline, only show voids that overlap its bbox (hides outer padding)
|
|
173
|
-
if (outlineBBox && !overlaps(r, outlineBBox)) {
|
|
174
|
-
continue
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
rects.push({
|
|
178
|
-
center: { x: r.x + r.width / 2, y: r.y + r.height / 2 },
|
|
179
|
-
width: r.width,
|
|
180
|
-
height: r.height,
|
|
181
|
-
fill: "rgba(0, 0, 0, 0.5)",
|
|
182
|
-
stroke: "none",
|
|
183
|
-
label: "void",
|
|
184
|
-
})
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// candidate positions (where expansion started from)
|
|
189
|
-
if (this.state?.candidates?.length) {
|
|
190
|
-
for (const cand of this.state.candidates) {
|
|
191
|
-
points.push({
|
|
192
|
-
x: cand.x,
|
|
193
|
-
y: cand.y,
|
|
194
|
-
fill: "#9333ea",
|
|
195
|
-
stroke: "#6b21a8",
|
|
196
|
-
label: `z:${cand.z}`,
|
|
197
|
-
} as any)
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// current placements (streaming) if not yet solved
|
|
202
|
-
if (this.state?.placed?.length) {
|
|
203
|
-
for (const p of this.state.placed) {
|
|
204
|
-
const colors = this.getColorForZLayer(p.zLayers)
|
|
205
|
-
rects.push({
|
|
206
|
-
center: {
|
|
207
|
-
x: p.rect.x + p.rect.width / 2,
|
|
208
|
-
y: p.rect.y + p.rect.height / 2,
|
|
209
|
-
},
|
|
210
|
-
width: p.rect.width,
|
|
211
|
-
height: p.rect.height,
|
|
212
|
-
fill: colors.fill,
|
|
213
|
-
stroke: colors.stroke,
|
|
214
|
-
layer: `z${p.zLayers.join(",")}`,
|
|
215
|
-
label: `free\nz:${p.zLayers.join(",")}`,
|
|
216
|
-
})
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return {
|
|
221
|
-
title: `RectDiff (${this.state?.phase ?? "init"})`,
|
|
222
|
-
coordinateSystem: "cartesian",
|
|
223
|
-
rects,
|
|
224
|
-
points,
|
|
225
|
-
lines, // Include lines in the returned GraphicsObject
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Re-export types for convenience
|
|
231
|
-
export type { GridFill3DOptions } from "./rectdiff/types"
|