@tscircuit/rectdiff 0.0.10 → 0.0.12
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 +97 -4
- package/dist/index.js +714 -13
- package/lib/RectDiffPipeline.ts +79 -13
- package/lib/solvers/GapFillSolver/ExpandEdgesToEmptySpaceSolver.ts +284 -0
- package/lib/solvers/GapFillSolver/FindSegmentsWithAdjacentEmptySpaceSolver.ts +213 -0
- package/lib/solvers/GapFillSolver/GapFillSolverPipeline.ts +129 -0
- package/lib/solvers/GapFillSolver/edge-constants.ts +48 -0
- package/lib/solvers/GapFillSolver/getBoundsFromCorners.ts +10 -0
- package/lib/solvers/GapFillSolver/projectToUncoveredSegments.ts +92 -0
- package/lib/solvers/GapFillSolver/visuallyOffsetLine.ts +32 -0
- package/lib/solvers/RectDiffSolver.ts +1 -0
- package/package.json +9 -6
- package/tests/board-outline.test.ts +1 -1
- package/tsconfig.json +4 -0
- package/vite.config.ts +6 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BasePipelineSolver,
|
|
3
|
+
definePipelineStep,
|
|
4
|
+
type PipelineStep,
|
|
5
|
+
} from "@tscircuit/solver-utils"
|
|
6
|
+
import type { SimpleRouteJson } from "lib/types/srj-types"
|
|
7
|
+
import type { CapacityMeshNode } from "lib/types/capacity-mesh-types"
|
|
8
|
+
import type { GraphicsObject } from "graphics-debug"
|
|
9
|
+
import { FindSegmentsWithAdjacentEmptySpaceSolver } from "./FindSegmentsWithAdjacentEmptySpaceSolver"
|
|
10
|
+
import { ExpandEdgesToEmptySpaceSolver } from "./ExpandEdgesToEmptySpaceSolver"
|
|
11
|
+
|
|
12
|
+
export class GapFillSolverPipeline extends BasePipelineSolver<{
|
|
13
|
+
meshNodes: CapacityMeshNode[]
|
|
14
|
+
}> {
|
|
15
|
+
findSegmentsWithAdjacentEmptySpaceSolver?: FindSegmentsWithAdjacentEmptySpaceSolver
|
|
16
|
+
expandEdgesToEmptySpaceSolver?: ExpandEdgesToEmptySpaceSolver
|
|
17
|
+
|
|
18
|
+
override pipelineDef: PipelineStep<any>[] = [
|
|
19
|
+
definePipelineStep(
|
|
20
|
+
"findSegmentsWithAdjacentEmptySpaceSolver",
|
|
21
|
+
FindSegmentsWithAdjacentEmptySpaceSolver,
|
|
22
|
+
(gapFillPipeline) => [
|
|
23
|
+
{
|
|
24
|
+
meshNodes: gapFillPipeline.inputProblem.meshNodes,
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
{
|
|
28
|
+
onSolved: () => {
|
|
29
|
+
// Gap fill solver completed
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
),
|
|
33
|
+
definePipelineStep(
|
|
34
|
+
"expandEdgesToEmptySpaceSolver",
|
|
35
|
+
ExpandEdgesToEmptySpaceSolver,
|
|
36
|
+
(gapFillPipeline: GapFillSolverPipeline) => [
|
|
37
|
+
{
|
|
38
|
+
inputMeshNodes: gapFillPipeline.inputProblem.meshNodes,
|
|
39
|
+
segmentsWithAdjacentEmptySpace:
|
|
40
|
+
gapFillPipeline.findSegmentsWithAdjacentEmptySpaceSolver!.getOutput()
|
|
41
|
+
.segmentsWithAdjacentEmptySpace,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
{
|
|
45
|
+
onSolved: () => {
|
|
46
|
+
// Gap fill solver completed
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
),
|
|
50
|
+
] as const
|
|
51
|
+
|
|
52
|
+
override getOutput(): { outputNodes: CapacityMeshNode[] } {
|
|
53
|
+
const expandedSegments =
|
|
54
|
+
this.expandEdgesToEmptySpaceSolver?.getOutput().expandedSegments ?? []
|
|
55
|
+
const expandedNodes = expandedSegments.map((es) => es.newNode)
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
outputNodes: [...this.inputProblem.meshNodes, ...expandedNodes],
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override initialVisualize(): GraphicsObject {
|
|
63
|
+
const graphics: Required<GraphicsObject> = {
|
|
64
|
+
title: "GapFillSolverPipeline - Initial",
|
|
65
|
+
coordinateSystem: "cartesian" as const,
|
|
66
|
+
rects: [],
|
|
67
|
+
points: [],
|
|
68
|
+
lines: [],
|
|
69
|
+
circles: [],
|
|
70
|
+
arrows: [],
|
|
71
|
+
texts: [],
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const node of this.inputProblem.meshNodes) {
|
|
75
|
+
graphics.rects.push({
|
|
76
|
+
center: node.center,
|
|
77
|
+
width: node.width,
|
|
78
|
+
height: node.height,
|
|
79
|
+
stroke: "rgba(0, 0, 0, 0.3)",
|
|
80
|
+
fill: "rgba(100, 100, 100, 0.1)",
|
|
81
|
+
layer: `z${node.availableZ.join(",")}`,
|
|
82
|
+
label: [
|
|
83
|
+
`node ${node.capacityMeshNodeId}`,
|
|
84
|
+
`z:${node.availableZ.join(",")}`,
|
|
85
|
+
].join("\n"),
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return graphics
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
override finalVisualize(): GraphicsObject {
|
|
93
|
+
const graphics: Required<GraphicsObject> = {
|
|
94
|
+
title: "GapFillSolverPipeline - Final",
|
|
95
|
+
coordinateSystem: "cartesian" as const,
|
|
96
|
+
rects: [],
|
|
97
|
+
points: [],
|
|
98
|
+
lines: [],
|
|
99
|
+
circles: [],
|
|
100
|
+
arrows: [],
|
|
101
|
+
texts: [],
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const { outputNodes } = this.getOutput()
|
|
105
|
+
const expandedSegments =
|
|
106
|
+
this.expandEdgesToEmptySpaceSolver?.getOutput().expandedSegments ?? []
|
|
107
|
+
const expandedNodeIds = new Set(
|
|
108
|
+
expandedSegments.map((es) => es.newNode.capacityMeshNodeId),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
for (const node of outputNodes) {
|
|
112
|
+
const isExpanded = expandedNodeIds.has(node.capacityMeshNodeId)
|
|
113
|
+
graphics.rects.push({
|
|
114
|
+
center: node.center,
|
|
115
|
+
width: node.width,
|
|
116
|
+
height: node.height,
|
|
117
|
+
stroke: isExpanded ? "rgba(0, 128, 0, 0.8)" : "rgba(0, 0, 0, 0.3)",
|
|
118
|
+
fill: isExpanded ? "rgba(0, 200, 0, 0.3)" : "rgba(100, 100, 100, 0.1)",
|
|
119
|
+
layer: `z${node.availableZ.join(",")}`,
|
|
120
|
+
label: [
|
|
121
|
+
`${isExpanded ? "[expanded] " : ""}node ${node.capacityMeshNodeId}`,
|
|
122
|
+
`z:${node.availableZ.join(",")}`,
|
|
123
|
+
].join("\n"),
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return graphics
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The four normalized edges of a rect
|
|
3
|
+
*/
|
|
4
|
+
export const EDGES = [
|
|
5
|
+
{
|
|
6
|
+
facingDirection: "x-",
|
|
7
|
+
dx: -1,
|
|
8
|
+
dy: 0,
|
|
9
|
+
startX: -0.5,
|
|
10
|
+
startY: 0.5,
|
|
11
|
+
endX: -0.5,
|
|
12
|
+
endY: -0.5,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
facingDirection: "x+",
|
|
16
|
+
dx: 1,
|
|
17
|
+
dy: 0,
|
|
18
|
+
startX: 0.5,
|
|
19
|
+
startY: 0.5,
|
|
20
|
+
endX: 0.5,
|
|
21
|
+
endY: -0.5,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
facingDirection: "y-",
|
|
25
|
+
dx: 0,
|
|
26
|
+
dy: -1,
|
|
27
|
+
startX: -0.5,
|
|
28
|
+
startY: -0.5,
|
|
29
|
+
endX: 0.5,
|
|
30
|
+
endY: -0.5,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
facingDirection: "y+",
|
|
34
|
+
dx: 0,
|
|
35
|
+
dy: 1,
|
|
36
|
+
startX: 0.5,
|
|
37
|
+
startY: 0.5,
|
|
38
|
+
endX: -0.5,
|
|
39
|
+
endY: 0.5,
|
|
40
|
+
},
|
|
41
|
+
] as const
|
|
42
|
+
|
|
43
|
+
export const EDGE_MAP = {
|
|
44
|
+
"x-": EDGES.find((e) => e.facingDirection === "x-")!,
|
|
45
|
+
"x+": EDGES.find((e) => e.facingDirection === "x+")!,
|
|
46
|
+
"y-": EDGES.find((e) => e.facingDirection === "y-")!,
|
|
47
|
+
"y+": EDGES.find((e) => e.facingDirection === "y+")!,
|
|
48
|
+
} as const
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const getBoundsFromCorners = (
|
|
2
|
+
corners: { x: number; y: number }[],
|
|
3
|
+
): { minX: number; minY: number; maxX: number; maxY: number } => {
|
|
4
|
+
return {
|
|
5
|
+
minX: Math.min(...corners.map((c) => c.x)),
|
|
6
|
+
minY: Math.min(...corners.map((c) => c.y)),
|
|
7
|
+
maxX: Math.max(...corners.map((c) => c.x)),
|
|
8
|
+
maxY: Math.max(...corners.map((c) => c.y)),
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { SegmentWithAdjacentEmptySpace } from "./FindSegmentsWithAdjacentEmptySpaceSolver"
|
|
2
|
+
|
|
3
|
+
const EPS = 1e-4
|
|
4
|
+
|
|
5
|
+
export function projectToUncoveredSegments(
|
|
6
|
+
primaryEdge: SegmentWithAdjacentEmptySpace,
|
|
7
|
+
overlappingEdges: SegmentWithAdjacentEmptySpace[],
|
|
8
|
+
): Array<SegmentWithAdjacentEmptySpace> {
|
|
9
|
+
const isHorizontal = Math.abs(primaryEdge.start.y - primaryEdge.end.y) < EPS
|
|
10
|
+
const isVertical = Math.abs(primaryEdge.start.x - primaryEdge.end.x) < EPS
|
|
11
|
+
if (!isHorizontal && !isVertical) return []
|
|
12
|
+
|
|
13
|
+
const axis: "x" | "y" = isHorizontal ? "x" : "y"
|
|
14
|
+
const perp: "x" | "y" = isHorizontal ? "y" : "x"
|
|
15
|
+
const lineCoord = primaryEdge.start[perp]
|
|
16
|
+
|
|
17
|
+
const p0 = primaryEdge.start[axis]
|
|
18
|
+
const p1 = primaryEdge.end[axis]
|
|
19
|
+
const pMin = Math.min(p0, p1)
|
|
20
|
+
const pMax = Math.max(p0, p1)
|
|
21
|
+
|
|
22
|
+
const clamp = (v: number) => Math.max(pMin, Math.min(pMax, v))
|
|
23
|
+
|
|
24
|
+
// 1) project each overlapping edge to an interval on the primary edge
|
|
25
|
+
const intervals: Array<{ s: number; e: number }> = []
|
|
26
|
+
for (const e of overlappingEdges) {
|
|
27
|
+
if (e === primaryEdge) continue
|
|
28
|
+
|
|
29
|
+
// only consider edges parallel + colinear (within EPS) with the primary edge
|
|
30
|
+
const eIsHorizontal = Math.abs(e.start.y - e.end.y) < EPS
|
|
31
|
+
const eIsVertical = Math.abs(e.start.x - e.end.x) < EPS
|
|
32
|
+
if (axis === "x" && !eIsHorizontal) continue
|
|
33
|
+
if (axis === "y" && !eIsVertical) continue
|
|
34
|
+
if (Math.abs(e.start[perp] - lineCoord) > EPS) continue
|
|
35
|
+
|
|
36
|
+
const eMin = Math.min(e.start[axis], e.end[axis])
|
|
37
|
+
const eMax = Math.max(e.start[axis], e.end[axis])
|
|
38
|
+
|
|
39
|
+
const s = clamp(eMin)
|
|
40
|
+
const t = clamp(eMax)
|
|
41
|
+
if (t - s > EPS) intervals.push({ s, e: t })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (intervals.length === 0) {
|
|
45
|
+
// nothing covers the primary edge -> entire edge is uncovered
|
|
46
|
+
return [
|
|
47
|
+
{
|
|
48
|
+
...primaryEdge,
|
|
49
|
+
start: { ...primaryEdge.start },
|
|
50
|
+
end: { ...primaryEdge.end },
|
|
51
|
+
},
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 2) merge cover intervals
|
|
56
|
+
intervals.sort((a, b) => a.s - b.s)
|
|
57
|
+
const merged: Array<{ s: number; e: number }> = []
|
|
58
|
+
for (const it of intervals) {
|
|
59
|
+
const last = merged[merged.length - 1]
|
|
60
|
+
if (!last || it.s > last.e + EPS) merged.push({ ...it })
|
|
61
|
+
else last.e = Math.max(last.e, it.e)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 3) compute uncovered intervals (complement of merged within [pMin,pMax])
|
|
65
|
+
const uncovered: Array<{ s: number; e: number }> = []
|
|
66
|
+
let cursor = pMin
|
|
67
|
+
for (const m of merged) {
|
|
68
|
+
if (m.s > cursor + EPS) uncovered.push({ s: cursor, e: m.s })
|
|
69
|
+
cursor = Math.max(cursor, m.e)
|
|
70
|
+
if (cursor >= pMax - EPS) break
|
|
71
|
+
}
|
|
72
|
+
if (pMax > cursor + EPS) uncovered.push({ s: cursor, e: pMax })
|
|
73
|
+
if (uncovered.length === 0) return []
|
|
74
|
+
|
|
75
|
+
// 4) convert uncovered intervals back to segments on the primary edge
|
|
76
|
+
return uncovered
|
|
77
|
+
.filter((u) => u.e - u.s > EPS)
|
|
78
|
+
.map((u) => {
|
|
79
|
+
const start =
|
|
80
|
+
axis === "x" ? { x: u.s, y: lineCoord } : { x: lineCoord, y: u.s }
|
|
81
|
+
const end =
|
|
82
|
+
axis === "x" ? { x: u.e, y: lineCoord } : { x: lineCoord, y: u.e }
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
parent: primaryEdge.parent,
|
|
86
|
+
facingDirection: primaryEdge.facingDirection,
|
|
87
|
+
start,
|
|
88
|
+
end,
|
|
89
|
+
z: primaryEdge.z,
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const OFFSET_DIR_MAP = {
|
|
2
|
+
"x-": {
|
|
3
|
+
x: -1,
|
|
4
|
+
y: 0,
|
|
5
|
+
},
|
|
6
|
+
"x+": {
|
|
7
|
+
x: 1,
|
|
8
|
+
y: 0,
|
|
9
|
+
},
|
|
10
|
+
"y-": {
|
|
11
|
+
x: 0,
|
|
12
|
+
y: -1,
|
|
13
|
+
},
|
|
14
|
+
"y+": {
|
|
15
|
+
x: 0,
|
|
16
|
+
y: 1,
|
|
17
|
+
},
|
|
18
|
+
} as const
|
|
19
|
+
/**
|
|
20
|
+
* Visually offset a line by a given amount in a given direction
|
|
21
|
+
*/
|
|
22
|
+
export const visuallyOffsetLine = (
|
|
23
|
+
line: Array<{ x: number; y: number }>,
|
|
24
|
+
dir: "x-" | "x+" | "y-" | "y+",
|
|
25
|
+
amt: number,
|
|
26
|
+
) => {
|
|
27
|
+
const offset = OFFSET_DIR_MAP[dir]
|
|
28
|
+
return line.map((p) => ({
|
|
29
|
+
x: p.x + offset.x * amt,
|
|
30
|
+
y: p.y + offset.y * amt,
|
|
31
|
+
}))
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tscircuit/rectdiff",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -13,14 +13,17 @@
|
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"@biomejs/biome": "^2.3.5",
|
|
15
15
|
"@react-hook/resize-observer": "^2.0.2",
|
|
16
|
-
"@tscircuit/
|
|
16
|
+
"@tscircuit/math-utils": "^0.0.29",
|
|
17
|
+
"@tscircuit/solver-utils": "^0.0.9",
|
|
17
18
|
"@types/bun": "latest",
|
|
19
|
+
"@types/rbush": "^4.0.0",
|
|
18
20
|
"@types/react": "^18",
|
|
19
21
|
"@types/react-dom": "^18",
|
|
20
22
|
"@types/three": "^0.181.0",
|
|
23
|
+
"@vitejs/plugin-react": "^4",
|
|
21
24
|
"biome": "^0.3.3",
|
|
22
25
|
"bun-match-svg": "^0.0.14",
|
|
23
|
-
"graphics-debug": "^0.0.
|
|
26
|
+
"graphics-debug": "^0.0.76",
|
|
24
27
|
"rbush": "^4.0.1",
|
|
25
28
|
"react": "18",
|
|
26
29
|
"react-cosmos": "^6.2.3",
|
|
@@ -28,13 +31,13 @@
|
|
|
28
31
|
"react-dom": "18",
|
|
29
32
|
"three": "^0.181.1",
|
|
30
33
|
"tsup": "^8.5.1",
|
|
31
|
-
"vite": "^6.0.11"
|
|
32
|
-
"@vitejs/plugin-react": "^4"
|
|
34
|
+
"vite": "^6.0.11"
|
|
33
35
|
},
|
|
34
36
|
"peerDependencies": {
|
|
35
37
|
"typescript": "^5"
|
|
36
38
|
},
|
|
37
39
|
"dependencies": {
|
|
38
|
-
"D": "^1.0.0"
|
|
40
|
+
"D": "^1.0.0",
|
|
41
|
+
"flatbush": "^4.5.0"
|
|
39
42
|
}
|
|
40
43
|
}
|
|
@@ -11,7 +11,7 @@ test("board outline snapshot", async () => {
|
|
|
11
11
|
// Run to completion
|
|
12
12
|
solver.solve()
|
|
13
13
|
|
|
14
|
-
const viz = solver.visualize()
|
|
14
|
+
const viz = solver.rectDiffSolver!.visualize()
|
|
15
15
|
const svg = getSvgFromGraphicsObject(viz)
|
|
16
16
|
|
|
17
17
|
await expect(svg).toMatchSvgSnapshot(import.meta.path)
|
package/tsconfig.json
CHANGED
package/vite.config.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { defineConfig } from "vite"
|
|
2
|
+
import path from "node:path"
|
|
2
3
|
import react from "@vitejs/plugin-react"
|
|
3
4
|
|
|
4
5
|
// https://vitejs.dev/config/
|
|
5
6
|
export default defineConfig({
|
|
6
7
|
plugins: [react()],
|
|
8
|
+
resolve: {
|
|
9
|
+
alias: {
|
|
10
|
+
lib: path.resolve(__dirname, "lib"),
|
|
11
|
+
},
|
|
12
|
+
},
|
|
7
13
|
})
|