@tscircuit/rectdiff 0.0.10 → 0.0.11

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.
@@ -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
+ "expandEdgesToEmptySpace",
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
+ }
@@ -211,6 +211,7 @@ export class RectDiffSolver extends BaseSolver {
211
211
  height: p.rect.height,
212
212
  fill: colors.fill,
213
213
  stroke: colors.stroke,
214
+ layer: `z${p.zLayers.join(",")}`,
214
215
  label: `free\nz:${p.zLayers.join(",")}`,
215
216
  })
216
217
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/rectdiff",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
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/solver-utils": "^0.0.7",
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.70",
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
@@ -7,6 +7,10 @@
7
7
  "moduleDetection": "force",
8
8
  "jsx": "react-jsx",
9
9
  "allowJs": true,
10
+ "paths": {
11
+ "lib/*": ["./lib/*"]
12
+ },
13
+ "baseUrl": ".",
10
14
 
11
15
  // Bundler mode
12
16
  "moduleResolution": "bundler",
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
  })