@tscircuit/rectdiff 0.0.9 → 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.
Files changed (30) hide show
  1. package/dist/index.d.ts +97 -12
  2. package/dist/index.js +714 -81
  3. package/lib/RectDiffPipeline.ts +79 -13
  4. package/lib/solvers/GapFillSolver/ExpandEdgesToEmptySpaceSolver.ts +284 -0
  5. package/lib/solvers/GapFillSolver/FindSegmentsWithAdjacentEmptySpaceSolver.ts +213 -0
  6. package/lib/solvers/GapFillSolver/GapFillSolverPipeline.ts +129 -0
  7. package/lib/solvers/GapFillSolver/edge-constants.ts +48 -0
  8. package/lib/solvers/GapFillSolver/getBoundsFromCorners.ts +10 -0
  9. package/lib/solvers/GapFillSolver/projectToUncoveredSegments.ts +92 -0
  10. package/lib/solvers/GapFillSolver/visuallyOffsetLine.ts +32 -0
  11. package/lib/solvers/RectDiffSolver.ts +1 -33
  12. package/package.json +9 -6
  13. package/tests/board-outline.test.ts +1 -1
  14. package/tsconfig.json +4 -0
  15. package/vite.config.ts +6 -0
  16. package/lib/solvers/rectdiff/gapfill/detection/deduplicateGaps.ts +0 -28
  17. package/lib/solvers/rectdiff/gapfill/detection/findAllGaps.ts +0 -83
  18. package/lib/solvers/rectdiff/gapfill/detection/findGapsOnLayer.ts +0 -100
  19. package/lib/solvers/rectdiff/gapfill/detection/mergeUncoveredCells.ts +0 -75
  20. package/lib/solvers/rectdiff/gapfill/detection.ts +0 -3
  21. package/lib/solvers/rectdiff/gapfill/engine/addPlacement.ts +0 -27
  22. package/lib/solvers/rectdiff/gapfill/engine/calculateCoverage.ts +0 -44
  23. package/lib/solvers/rectdiff/gapfill/engine/findUncoveredPoints.ts +0 -43
  24. package/lib/solvers/rectdiff/gapfill/engine/getGapFillProgress.ts +0 -42
  25. package/lib/solvers/rectdiff/gapfill/engine/initGapFillState.ts +0 -57
  26. package/lib/solvers/rectdiff/gapfill/engine/stepGapFill.ts +0 -128
  27. package/lib/solvers/rectdiff/gapfill/engine/tryExpandGap.ts +0 -78
  28. package/lib/solvers/rectdiff/gapfill/engine.ts +0 -7
  29. package/lib/solvers/rectdiff/gapfill/types.ts +0 -60
  30. package/lib/solvers/rectdiff/subsolvers/GapFillSubSolver.ts +0 -253
@@ -1,75 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/detection/mergeUncoveredCells.ts
2
- import type { XYRect } from "../../types"
3
- import { EPS } from "../../geometry"
4
-
5
- /**
6
- * Merge adjacent uncovered cells into larger rectangles using a greedy approach.
7
- */
8
- export function mergeUncoveredCells(
9
- cells: Array<{ x: number; y: number; w: number; h: number }>,
10
- ): XYRect[] {
11
- if (cells.length === 0) return []
12
-
13
- // Group cells by their left edge and width
14
- const byXW = new Map<string, typeof cells>()
15
- for (const c of cells) {
16
- const key = `${c.x.toFixed(9)}|${c.w.toFixed(9)}`
17
- const arr = byXW.get(key) ?? []
18
- arr.push(c)
19
- byXW.set(key, arr)
20
- }
21
-
22
- // Within each vertical strip, merge adjacent cells
23
- const verticalStrips: XYRect[] = []
24
- for (const stripCells of byXW.values()) {
25
- // Sort by y
26
- stripCells.sort((a, b) => a.y - b.y)
27
-
28
- let current: XYRect | null = null
29
- for (const c of stripCells) {
30
- if (!current) {
31
- current = { x: c.x, y: c.y, width: c.w, height: c.h }
32
- } else if (Math.abs(current.y + current.height - c.y) < EPS) {
33
- // Adjacent vertically, merge
34
- current.height += c.h
35
- } else {
36
- // Gap, save current and start new
37
- verticalStrips.push(current)
38
- current = { x: c.x, y: c.y, width: c.w, height: c.h }
39
- }
40
- }
41
- if (current) verticalStrips.push(current)
42
- }
43
-
44
- // Now try to merge horizontal strips with same y and height
45
- const byYH = new Map<string, XYRect[]>()
46
- for (const r of verticalStrips) {
47
- const key = `${r.y.toFixed(9)}|${r.height.toFixed(9)}`
48
- const arr = byYH.get(key) ?? []
49
- arr.push(r)
50
- byYH.set(key, arr)
51
- }
52
-
53
- const merged: XYRect[] = []
54
- for (const rowRects of byYH.values()) {
55
- // Sort by x
56
- rowRects.sort((a, b) => a.x - b.x)
57
-
58
- let current: XYRect | null = null
59
- for (const r of rowRects) {
60
- if (!current) {
61
- current = { ...r }
62
- } else if (Math.abs(current.x + current.width - r.x) < EPS) {
63
- // Adjacent horizontally, merge
64
- current.width += r.width
65
- } else {
66
- // Gap, save current and start new
67
- merged.push(current)
68
- current = { ...r }
69
- }
70
- }
71
- if (current) merged.push(current)
72
- }
73
-
74
- return merged
75
- }
@@ -1,3 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/detection.ts
2
- export * from "./detection/findAllGaps"
3
- // findGapsOnLayer is not exported as it's only used by findAllGaps
@@ -1,27 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/engine/addPlacement.ts
2
- import type { Placed3D, XYRect } from "../../types"
3
- import type { GapFillState } from "../types"
4
-
5
- /**
6
- * Add a new placement to the state.
7
- */
8
- export function addPlacement(
9
- state: GapFillState,
10
- {
11
- rect,
12
- zLayers,
13
- }: {
14
- rect: XYRect
15
- zLayers: number[]
16
- },
17
- ): void {
18
- const placed: Placed3D = { rect, zLayers: [...zLayers] }
19
- state.placed.push(placed)
20
-
21
- for (const z of zLayers) {
22
- if (!state.placedByLayer[z]) {
23
- state.placedByLayer[z] = []
24
- }
25
- state.placedByLayer[z]!.push(rect)
26
- }
27
- }
@@ -1,44 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/engine/calculateCoverage.ts
2
- import type { LayerContext } from "../types"
3
-
4
- /**
5
- * Calculate coverage percentage (0-1).
6
- */
7
- export function calculateCoverage(
8
- { sampleResolution = 0.1 }: { sampleResolution?: number },
9
- ctx: LayerContext,
10
- ): number {
11
- const { bounds, layerCount, obstaclesByLayer, placedByLayer } = ctx
12
-
13
- let totalPoints = 0
14
- let coveredPoints = 0
15
-
16
- for (let z = 0; z < layerCount; z++) {
17
- const obstacles = obstaclesByLayer[z] ?? []
18
- const placed = placedByLayer[z] ?? []
19
- const allRects = [...obstacles, ...placed]
20
-
21
- for (
22
- let x = bounds.x;
23
- x <= bounds.x + bounds.width;
24
- x += sampleResolution
25
- ) {
26
- for (
27
- let y = bounds.y;
28
- y <= bounds.y + bounds.height;
29
- y += sampleResolution
30
- ) {
31
- totalPoints++
32
-
33
- const isCovered = allRects.some(
34
- (r) =>
35
- x >= r.x && x <= r.x + r.width && y >= r.y && y <= r.y + r.height,
36
- )
37
-
38
- if (isCovered) coveredPoints++
39
- }
40
- }
41
- }
42
-
43
- return totalPoints > 0 ? coveredPoints / totalPoints : 1
44
- }
@@ -1,43 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/engine/findUncoveredPoints.ts
2
- import type { LayerContext } from "../types"
3
-
4
- /**
5
- * Find uncovered points for debugging gaps.
6
- */
7
- export function findUncoveredPoints(
8
- { sampleResolution = 0.05 }: { sampleResolution?: number },
9
- ctx: LayerContext,
10
- ): Array<{ x: number; y: number; z: number }> {
11
- const { bounds, layerCount, obstaclesByLayer, placedByLayer } = ctx
12
-
13
- const uncovered: Array<{ x: number; y: number; z: number }> = []
14
-
15
- for (let z = 0; z < layerCount; z++) {
16
- const obstacles = obstaclesByLayer[z] ?? []
17
- const placed = placedByLayer[z] ?? []
18
- const allRects = [...obstacles, ...placed]
19
-
20
- for (
21
- let x = bounds.x;
22
- x <= bounds.x + bounds.width;
23
- x += sampleResolution
24
- ) {
25
- for (
26
- let y = bounds.y;
27
- y <= bounds.y + bounds.height;
28
- y += sampleResolution
29
- ) {
30
- const isCovered = allRects.some(
31
- (r) =>
32
- x >= r.x && x <= r.x + r.width && y >= r.y && y <= r.y + r.height,
33
- )
34
-
35
- if (!isCovered) {
36
- uncovered.push({ x, y, z })
37
- }
38
- }
39
- }
40
- }
41
-
42
- return uncovered
43
- }
@@ -1,42 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/engine/getGapFillProgress.ts
2
- import type { GapFillState } from "../types"
3
-
4
- /**
5
- * Get progress as a number between 0 and 1.
6
- * Accounts for four-stage processing (scan → select → expand → place for each gap).
7
- */
8
- export function getGapFillProgress(state: GapFillState): number {
9
- if (state.done) return 1
10
-
11
- const iterationProgress = state.iteration / state.options.maxIterations
12
- const gapProgress =
13
- state.gapsFound.length > 0 ? state.gapIndex / state.gapsFound.length : 0
14
-
15
- // Add sub-progress within current gap based on stage
16
- let stageProgress = 0
17
- switch (state.stage) {
18
- case "scan":
19
- stageProgress = 0
20
- break
21
- case "select":
22
- stageProgress = 0.25
23
- break
24
- case "expand":
25
- stageProgress = 0.5
26
- break
27
- case "place":
28
- stageProgress = 0.75
29
- break
30
- }
31
-
32
- const gapStageProgress =
33
- state.gapsFound.length > 0
34
- ? stageProgress / (state.gapsFound.length * 4) // 4 stages per gap
35
- : 0
36
-
37
- return Math.min(
38
- 0.999,
39
- iterationProgress +
40
- (gapProgress + gapStageProgress) / state.options.maxIterations,
41
- )
42
- }
@@ -1,57 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/engine/initGapFillState.ts
2
- import type { Placed3D } from "../../types"
3
- import type { GapFillState, GapFillOptions, LayerContext } from "../types"
4
-
5
- const DEFAULT_OPTIONS: GapFillOptions = {
6
- minWidth: 0.1,
7
- minHeight: 0.1,
8
- maxIterations: 10,
9
- targetCoverage: 0.999,
10
- scanResolution: 0.5,
11
- }
12
-
13
- /**
14
- * Initialize the gap fill state from existing rectdiff state.
15
- */
16
- export function initGapFillState(
17
- {
18
- placed,
19
- options,
20
- }: {
21
- placed: Placed3D[]
22
- options?: Partial<GapFillOptions>
23
- },
24
- ctx: LayerContext,
25
- ): GapFillState {
26
- const opts = { ...DEFAULT_OPTIONS, ...options }
27
-
28
- // Deep copy placed arrays to avoid mutation issues
29
- const placedCopy = placed.map((p) => ({
30
- rect: { ...p.rect },
31
- zLayers: [...p.zLayers],
32
- }))
33
-
34
- const placedByLayerCopy = ctx.placedByLayer.map((layer) =>
35
- layer.map((r) => ({ ...r })),
36
- )
37
-
38
- return {
39
- bounds: { ...ctx.bounds },
40
- layerCount: ctx.layerCount,
41
- obstaclesByLayer: ctx.obstaclesByLayer,
42
- placed: placedCopy,
43
- placedByLayer: placedByLayerCopy,
44
- options: opts,
45
- iteration: 0,
46
- gapsFound: [],
47
- gapIndex: 0,
48
- done: false,
49
- initialGapCount: 0,
50
- filledCount: 0,
51
- // Four-stage visualization state
52
- stage: "scan",
53
- currentGap: null,
54
- currentSeed: null,
55
- expandedRect: null,
56
- }
57
- }
@@ -1,128 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/engine/stepGapFill.ts
2
- import type { GapFillState } from "../types"
3
- import { findAllGaps } from "../detection"
4
- import { tryExpandGap } from "./tryExpandGap"
5
- import { addPlacement } from "./addPlacement"
6
-
7
- /**
8
- * Perform one step of gap filling with four-stage visualization.
9
- * Stages: scan → select → expand → place
10
- * Returns true if still working, false if done.
11
- */
12
- export function stepGapFill(state: GapFillState): boolean {
13
- if (state.done) return false
14
-
15
- switch (state.stage) {
16
- case "scan": {
17
- // Stage 1: Gap detection/scanning
18
-
19
- // Check if we need to find new gaps
20
- if (
21
- state.gapsFound.length === 0 ||
22
- state.gapIndex >= state.gapsFound.length
23
- ) {
24
- // Check if we've hit max iterations
25
- if (state.iteration >= state.options.maxIterations) {
26
- state.done = true
27
- return false
28
- }
29
-
30
- // Find new gaps
31
- state.gapsFound = findAllGaps(
32
- {
33
- scanResolution: state.options.scanResolution,
34
- minWidth: state.options.minWidth,
35
- minHeight: state.options.minHeight,
36
- },
37
- {
38
- bounds: state.bounds,
39
- layerCount: state.layerCount,
40
- obstaclesByLayer: state.obstaclesByLayer,
41
- placedByLayer: state.placedByLayer,
42
- },
43
- )
44
-
45
- if (state.iteration === 0) {
46
- state.initialGapCount = state.gapsFound.length
47
- }
48
-
49
- state.gapIndex = 0
50
- state.iteration++
51
-
52
- // If no gaps found, we're done
53
- if (state.gapsFound.length === 0) {
54
- state.done = true
55
- return false
56
- }
57
- }
58
-
59
- // Move to select stage
60
- state.stage = "select"
61
- return true
62
- }
63
-
64
- case "select": {
65
- // Stage 2: Show the gap being targeted
66
- if (state.gapIndex >= state.gapsFound.length) {
67
- // No more gaps in this iteration, go back to scan
68
- state.stage = "scan"
69
- return true
70
- }
71
-
72
- state.currentGap = state.gapsFound[state.gapIndex]!
73
- state.currentSeed = {
74
- x: state.currentGap.centerX,
75
- y: state.currentGap.centerY,
76
- }
77
- state.expandedRect = null
78
-
79
- // Move to expand stage
80
- state.stage = "expand"
81
- return true
82
- }
83
-
84
- case "expand": {
85
- // Stage 3: Show expansion attempt
86
- if (!state.currentGap) {
87
- // Shouldn't happen, but handle gracefully
88
- state.stage = "select"
89
- return true
90
- }
91
-
92
- // Try to expand from the current seed
93
- const expandedRect = tryExpandGap(state, {
94
- gap: state.currentGap,
95
- seed: state.currentSeed!,
96
- })
97
- state.expandedRect = expandedRect
98
-
99
- // Move to place stage
100
- state.stage = "place"
101
- return true
102
- }
103
-
104
- case "place": {
105
- // Stage 4: Show the placed result
106
- if (state.expandedRect && state.currentGap) {
107
- // Actually place the rectangle
108
- addPlacement(state, {
109
- rect: state.expandedRect,
110
- zLayers: state.currentGap.zLayers,
111
- })
112
- state.filledCount++
113
- }
114
-
115
- // Move to next gap and reset to select stage
116
- state.gapIndex++
117
- state.currentGap = null
118
- state.currentSeed = null
119
- state.expandedRect = null
120
- state.stage = "select"
121
- return true
122
- }
123
-
124
- default:
125
- state.stage = "scan"
126
- return true
127
- }
128
- }
@@ -1,78 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/engine/tryExpandGap.ts
2
- import type { XYRect } from "../../types"
3
- import type { GapFillState, GapRegion } from "../types"
4
- import { expandRectFromSeed } from "../../geometry"
5
-
6
- /**
7
- * Try to expand a rectangle from a seed point within the gap.
8
- * Returns the expanded rectangle or null if expansion fails.
9
- */
10
- export function tryExpandGap(
11
- state: GapFillState,
12
- {
13
- gap,
14
- seed,
15
- }: {
16
- gap: GapRegion
17
- seed: { x: number; y: number }
18
- },
19
- ): XYRect | null {
20
- // Build blockers for the gap's z-layers
21
- const blockers: XYRect[] = []
22
- for (const z of gap.zLayers) {
23
- blockers.push(...(state.obstaclesByLayer[z] ?? []))
24
- blockers.push(...(state.placedByLayer[z] ?? []))
25
- }
26
-
27
- // Try to expand from the seed point
28
- const rect = expandRectFromSeed({
29
- startX: seed.x,
30
- startY: seed.y,
31
- gridSize: Math.min(gap.rect.width, gap.rect.height),
32
- bounds: state.bounds,
33
- blockers,
34
- initialCellRatio: 0,
35
- maxAspectRatio: null,
36
- minReq: { width: state.options.minWidth, height: state.options.minHeight },
37
- })
38
-
39
- if (!rect) {
40
- // Try additional seed points within the gap
41
- const seeds = [
42
- { x: gap.rect.x + state.options.minWidth / 2, y: gap.centerY },
43
- {
44
- x: gap.rect.x + gap.rect.width - state.options.minWidth / 2,
45
- y: gap.centerY,
46
- },
47
- { x: gap.centerX, y: gap.rect.y + state.options.minHeight / 2 },
48
- {
49
- x: gap.centerX,
50
- y: gap.rect.y + gap.rect.height - state.options.minHeight / 2,
51
- },
52
- ]
53
-
54
- for (const altSeed of seeds) {
55
- const altRect = expandRectFromSeed({
56
- startX: altSeed.x,
57
- startY: altSeed.y,
58
- gridSize: Math.min(gap.rect.width, gap.rect.height),
59
- bounds: state.bounds,
60
- blockers,
61
- initialCellRatio: 0,
62
- maxAspectRatio: null,
63
- minReq: {
64
- width: state.options.minWidth,
65
- height: state.options.minHeight,
66
- },
67
- })
68
-
69
- if (altRect) {
70
- return altRect
71
- }
72
- }
73
-
74
- return null
75
- }
76
-
77
- return rect
78
- }
@@ -1,7 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/engine.ts
2
- export * from "./engine/calculateCoverage"
3
- export * from "./engine/findUncoveredPoints"
4
- export * from "./engine/getGapFillProgress"
5
- export * from "./engine/initGapFillState"
6
-
7
- export * from "./engine/stepGapFill"
@@ -1,60 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/types.ts
2
- import type { XYRect, Placed3D } from "../types"
3
-
4
- export interface GapFillOptions {
5
- /** Minimum width for gap-fill rectangles (can be smaller than main solver) */
6
- minWidth: number
7
- /** Minimum height for gap-fill rectangles */
8
- minHeight: number
9
- /** Maximum iterations to prevent infinite loops */
10
- maxIterations: number
11
- /** Target coverage percentage (0-1) to stop early */
12
- targetCoverage: number
13
- /** Grid resolution for gap detection */
14
- scanResolution: number
15
- }
16
-
17
- export interface GapRegion {
18
- /** Bounding box of the gap */
19
- rect: XYRect
20
- /** Z-layers where this gap exists */
21
- zLayers: number[]
22
- /** Center point for seeding */
23
- centerX: number
24
- centerY: number
25
- /** Approximate area of the gap */
26
- area: number
27
- }
28
-
29
- export interface GapFillState {
30
- bounds: XYRect
31
- layerCount: number
32
- obstaclesByLayer: XYRect[][]
33
- placed: Placed3D[]
34
- placedByLayer: XYRect[][]
35
- options: GapFillOptions
36
-
37
- // Progress tracking
38
- iteration: number
39
- gapsFound: GapRegion[]
40
- gapIndex: number
41
- done: boolean
42
-
43
- // Stats
44
- initialGapCount: number
45
- filledCount: number
46
-
47
- // Four-stage visualization state
48
- stage: "scan" | "select" | "expand" | "place"
49
- currentGap: GapRegion | null
50
- currentSeed: { x: number; y: number } | null
51
- expandedRect: XYRect | null
52
- }
53
-
54
- /** Context for layer-based operations shared across gap fill functions */
55
- export interface LayerContext {
56
- bounds: XYRect
57
- layerCount: number
58
- obstaclesByLayer: XYRect[][]
59
- placedByLayer: XYRect[][]
60
- }