@tscircuit/rectdiff 0.0.16 → 0.0.18
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/lib/fixtures/twoNodeExpansionFixture.ts +66 -0
- package/package.json +1 -1
- package/pages/features/should-expand-node.page.tsx +13 -0
- package/test-assets/gap-fill-h-shape-should-expand-node.json +72 -0
- package/tests/__snapshots__/should-expand-node.snap.svg +44 -0
- package/tests/should-expand-node.test.ts +25 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import RBush from "rbush"
|
|
2
|
+
import type {
|
|
3
|
+
RectDiffExpansionSolverInput,
|
|
4
|
+
RectDiffExpansionSolverSnapshot,
|
|
5
|
+
} from "../solvers/RectDiffExpansionSolver/RectDiffExpansionSolver"
|
|
6
|
+
import type { SimpleRouteJson } from "../types/srj-types"
|
|
7
|
+
import type { XYRect } from "../rectdiff-types"
|
|
8
|
+
import type { RTreeRect } from "lib/types/capacity-mesh-types"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Builds a minimal RectDiffExpansionSolver snapshot with exactly two nodes
|
|
12
|
+
* separated by empty space. This keeps the data close to the solver’s real
|
|
13
|
+
* input shape so we can reuse it for demos/tests that reproduce the gap issue.
|
|
14
|
+
*/
|
|
15
|
+
export const createTwoNodeExpansionInput = (): RectDiffExpansionSolverInput => {
|
|
16
|
+
const srj: SimpleRouteJson = {
|
|
17
|
+
bounds: { minX: 0, maxX: 12, minY: 0, maxY: 4 },
|
|
18
|
+
layerCount: 1,
|
|
19
|
+
minTraceWidth: 0.2,
|
|
20
|
+
obstacles: [],
|
|
21
|
+
connections: [],
|
|
22
|
+
}
|
|
23
|
+
const layerCount = srj.layerCount ?? 1
|
|
24
|
+
const bounds: XYRect = {
|
|
25
|
+
x: srj.bounds.minX,
|
|
26
|
+
y: srj.bounds.minY,
|
|
27
|
+
width: srj.bounds.maxX - srj.bounds.minX,
|
|
28
|
+
height: srj.bounds.maxY - srj.bounds.minY,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const obstacleIndexByLayer = Array.from(
|
|
32
|
+
{ length: layerCount },
|
|
33
|
+
() => new RBush<RTreeRect>(),
|
|
34
|
+
)
|
|
35
|
+
// Start with all-empty obstacle indexes for a "clean" scenario
|
|
36
|
+
|
|
37
|
+
const initialSnapshot: RectDiffExpansionSolverSnapshot = {
|
|
38
|
+
srj,
|
|
39
|
+
layerNames: ["top"],
|
|
40
|
+
layerCount,
|
|
41
|
+
bounds,
|
|
42
|
+
options: { gridSizes: [1] },
|
|
43
|
+
boardVoidRects: [],
|
|
44
|
+
gridIndex: 0,
|
|
45
|
+
candidates: [],
|
|
46
|
+
placed: [
|
|
47
|
+
{
|
|
48
|
+
rect: { x: 0.5, y: 0.5, width: 2, height: 3 },
|
|
49
|
+
zLayers: [0],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
rect: { x: 9.5, y: 0.5, width: 2, height: 3 },
|
|
53
|
+
zLayers: [0],
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
expansionIndex: 0,
|
|
57
|
+
edgeAnalysisDone: true,
|
|
58
|
+
totalSeedsThisGrid: 0,
|
|
59
|
+
consumedSeedsThisGrid: 0,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
initialSnapshot,
|
|
64
|
+
obstacleIndexByLayer,
|
|
65
|
+
}
|
|
66
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useMemo } from "react"
|
|
2
|
+
import { RectDiffExpansionSolver } from "lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver"
|
|
3
|
+
import { createTwoNodeExpansionInput } from "lib/fixtures/twoNodeExpansionFixture"
|
|
4
|
+
import { SolverDebugger3d } from "../../components/SolverDebugger3d"
|
|
5
|
+
|
|
6
|
+
export default () => {
|
|
7
|
+
const solver = useMemo(
|
|
8
|
+
() => new RectDiffExpansionSolver(createTwoNodeExpansionInput()),
|
|
9
|
+
[],
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
return <SolverDebugger3d solver={solver} />
|
|
13
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "H-shaped configuration - two vertical nodes with a horizontal connector in the middle",
|
|
3
|
+
"expectedBehavior": "Should detect segments on all exposed edges and expand into empty spaces around the H shape",
|
|
4
|
+
"meshNodes": [
|
|
5
|
+
{
|
|
6
|
+
"capacityMeshNodeId": "node-left-vertical",
|
|
7
|
+
"center": { "x": -3, "y": 0 },
|
|
8
|
+
"width": 1,
|
|
9
|
+
"height": 6,
|
|
10
|
+
"layer": "top",
|
|
11
|
+
"availableZ": [0]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"capacityMeshNodeId": "node-right-vertical",
|
|
15
|
+
"center": { "x": 3, "y": 0 },
|
|
16
|
+
"width": 1,
|
|
17
|
+
"height": 6,
|
|
18
|
+
"layer": "top",
|
|
19
|
+
"availableZ": [0]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"capacityMeshNodeId": "node-middle-horizontal",
|
|
23
|
+
"center": { "x": 0, "y": 0 },
|
|
24
|
+
"width": 2,
|
|
25
|
+
"height": 1,
|
|
26
|
+
"layer": "top",
|
|
27
|
+
"availableZ": [0]
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"expectedSegments": [
|
|
31
|
+
{
|
|
32
|
+
"description": "Left vertical - left edge",
|
|
33
|
+
"parentNodeId": "node-left-vertical",
|
|
34
|
+
"facingDirection": "x-"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"description": "Left vertical - top edge (above horizontal)",
|
|
38
|
+
"parentNodeId": "node-left-vertical",
|
|
39
|
+
"facingDirection": "y+"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"description": "Left vertical - bottom edge (below horizontal)",
|
|
43
|
+
"parentNodeId": "node-left-vertical",
|
|
44
|
+
"facingDirection": "y-"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"description": "Right vertical - right edge",
|
|
48
|
+
"parentNodeId": "node-right-vertical",
|
|
49
|
+
"facingDirection": "x+"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"description": "Right vertical - top edge (above horizontal)",
|
|
53
|
+
"parentNodeId": "node-right-vertical",
|
|
54
|
+
"facingDirection": "y+"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"description": "Right vertical - bottom edge (below horizontal)",
|
|
58
|
+
"parentNodeId": "node-right-vertical",
|
|
59
|
+
"facingDirection": "y-"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"description": "Middle horizontal - top edge",
|
|
63
|
+
"parentNodeId": "node-middle-horizontal",
|
|
64
|
+
"facingDirection": "y+"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"description": "Middle horizontal - bottom edge",
|
|
68
|
+
"parentNodeId": "node-middle-horizontal",
|
|
69
|
+
"facingDirection": "y-"
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<svg width="640" height="480" viewBox="0 0 640 480" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="white"/><g><rect data-type="rect" data-label="node" data-x="1.5" data-y="2" x="40.000000000000014" y="163.63636363636363" width="101.8181818181818" height="152.72727272727275" fill="#dbeafe" stroke="black" stroke-width="0.019642857142857142"/></g><g><rect data-type="rect" data-label="node" data-x="10.5" data-y="2" x="498.1818181818182" y="163.63636363636363" width="101.81818181818181" height="152.72727272727275" fill="#dbeafe" stroke="black" stroke-width="0.019642857142857142"/></g><g id="crosshair" style="display: none"><line id="crosshair-h" y1="0" y2="480" stroke="#666" stroke-width="0.5"/><line id="crosshair-v" x1="0" x2="640" stroke="#666" stroke-width="0.5"/><text id="coordinates" font-family="monospace" font-size="12" fill="#666"></text></g><script><![CDATA[
|
|
2
|
+
document.currentScript.parentElement.addEventListener('mousemove', (e) => {
|
|
3
|
+
const svg = e.currentTarget;
|
|
4
|
+
const rect = svg.getBoundingClientRect();
|
|
5
|
+
const x = e.clientX - rect.left;
|
|
6
|
+
const y = e.clientY - rect.top;
|
|
7
|
+
const crosshair = svg.getElementById('crosshair');
|
|
8
|
+
const h = svg.getElementById('crosshair-h');
|
|
9
|
+
const v = svg.getElementById('crosshair-v');
|
|
10
|
+
const coords = svg.getElementById('coordinates');
|
|
11
|
+
|
|
12
|
+
crosshair.style.display = 'block';
|
|
13
|
+
h.setAttribute('x1', '0');
|
|
14
|
+
h.setAttribute('x2', '640');
|
|
15
|
+
h.setAttribute('y1', y);
|
|
16
|
+
h.setAttribute('y2', y);
|
|
17
|
+
v.setAttribute('x1', x);
|
|
18
|
+
v.setAttribute('x2', x);
|
|
19
|
+
v.setAttribute('y1', '0');
|
|
20
|
+
v.setAttribute('y2', '480');
|
|
21
|
+
|
|
22
|
+
// Calculate real coordinates using inverse transformation
|
|
23
|
+
const matrix = {"a":50.90909090909091,"c":0,"e":14.545454545454561,"b":0,"d":-50.90909090909091,"f":341.8181818181818};
|
|
24
|
+
// Manually invert and apply the affine transform
|
|
25
|
+
// Since we only use translate and scale, we can directly compute:
|
|
26
|
+
// x' = (x - tx) / sx
|
|
27
|
+
// y' = (y - ty) / sy
|
|
28
|
+
const sx = matrix.a;
|
|
29
|
+
const sy = matrix.d;
|
|
30
|
+
const tx = matrix.e;
|
|
31
|
+
const ty = matrix.f;
|
|
32
|
+
const realPoint = {
|
|
33
|
+
x: (x - tx) / sx,
|
|
34
|
+
y: (y - ty) / sy // Flip y back since we used negative scale
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
coords.textContent = `(${realPoint.x.toFixed(2)}, ${realPoint.y.toFixed(2)})`;
|
|
38
|
+
coords.setAttribute('x', (x + 5).toString());
|
|
39
|
+
coords.setAttribute('y', (y - 5).toString());
|
|
40
|
+
});
|
|
41
|
+
document.currentScript.parentElement.addEventListener('mouseleave', () => {
|
|
42
|
+
document.currentScript.parentElement.getElementById('crosshair').style.display = 'none';
|
|
43
|
+
});
|
|
44
|
+
]]></script></svg>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { getSvgFromGraphicsObject } from "graphics-debug"
|
|
3
|
+
import { RectDiffExpansionSolver } from "lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver"
|
|
4
|
+
import { createTwoNodeExpansionInput } from "lib/fixtures/twoNodeExpansionFixture"
|
|
5
|
+
import { makeCapacityMeshNodeWithLayerInfo } from "./fixtures/makeCapacityMeshNodeWithLayerInfo"
|
|
6
|
+
|
|
7
|
+
test("RectDiff expansion reproduces the two-node gap fixture", async () => {
|
|
8
|
+
const solver = new RectDiffExpansionSolver(createTwoNodeExpansionInput())
|
|
9
|
+
|
|
10
|
+
solver.solve()
|
|
11
|
+
|
|
12
|
+
const { meshNodes } = solver.getOutput()
|
|
13
|
+
expect(meshNodes.length).toBeGreaterThanOrEqual(2)
|
|
14
|
+
|
|
15
|
+
const finalGraphics = makeCapacityMeshNodeWithLayerInfo(meshNodes)
|
|
16
|
+
const svg = getSvgFromGraphicsObject(
|
|
17
|
+
{ rects: finalGraphics.values().toArray().flat() },
|
|
18
|
+
{
|
|
19
|
+
svgWidth: 640,
|
|
20
|
+
svgHeight: 480,
|
|
21
|
+
},
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
await expect(svg).toMatchSvgSnapshot(import.meta.path)
|
|
25
|
+
})
|