@tscircuit/rectdiff 0.0.13 → 0.0.15

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.
@@ -1,5 +1,7 @@
1
+ import type { RTreeRect } from "lib/types/capacity-mesh-types"
1
2
  import type { XYRect } from "../../rectdiff-types"
2
3
  import { clamp, containsPoint } from "../../utils/rectdiff-geometry"
4
+ import type RBush from "rbush"
3
5
 
4
6
  /**
5
7
  * Find the longest contiguous free span around z (optionally capped).
@@ -11,8 +13,8 @@ export function longestFreeSpanAroundZ(params: {
11
13
  layerCount: number
12
14
  minSpan: number
13
15
  maxSpan: number | undefined
14
- obstaclesByLayer: XYRect[][]
15
- placedByLayer: XYRect[][]
16
+ obstacleIndexByLayer: Array<RBush<RTreeRect> | undefined>
17
+ additionalBlockersByLayer?: XYRect[][]
16
18
  }): number[] {
17
19
  const {
18
20
  x,
@@ -21,16 +23,22 @@ export function longestFreeSpanAroundZ(params: {
21
23
  layerCount,
22
24
  minSpan,
23
25
  maxSpan,
24
- obstaclesByLayer,
25
- placedByLayer,
26
+ obstacleIndexByLayer,
27
+ additionalBlockersByLayer,
26
28
  } = params
27
29
 
28
30
  const isFreeAt = (layer: number) => {
29
- const blockers = [
30
- ...(obstaclesByLayer[layer] ?? []),
31
- ...(placedByLayer[layer] ?? []),
32
- ]
33
- return !blockers.some((b) => containsPoint(b, { x, y }))
31
+ const query = {
32
+ minX: x,
33
+ minY: y,
34
+ maxX: x,
35
+ maxY: y,
36
+ }
37
+ const obstacleIdx = obstacleIndexByLayer[layer]
38
+ if (obstacleIdx && obstacleIdx.search(query).length > 0) return false
39
+
40
+ const extras = additionalBlockersByLayer?.[layer] ?? []
41
+ return !extras.some((b) => containsPoint(b, { x, y }))
34
42
  }
35
43
  let lo = z
36
44
  let hi = z
@@ -1,3 +1,5 @@
1
+ import type { XYRect } from "lib/rectdiff-types"
2
+
1
3
  export type CapacityMeshNodeId = string
2
4
 
3
5
  export interface CapacityMesh {
@@ -31,3 +33,10 @@ export interface CapacityMeshEdge {
31
33
  capacityMeshEdgeId: string
32
34
  nodeIds: [CapacityMeshNodeId, CapacityMeshNodeId]
33
35
  }
36
+
37
+ export type RTreeRect = XYRect & {
38
+ minX: number
39
+ minY: number
40
+ maxX: number
41
+ maxY: number
42
+ }
@@ -1,8 +1,14 @@
1
1
  import type { Placed3D, Rect3d, XYRect } from "../rectdiff-types"
2
+ import type { SimpleRouteJson } from "../types/srj-types"
3
+ import {
4
+ buildZIndexMap,
5
+ obstacleToXYRect,
6
+ obstacleZs,
7
+ } from "../solvers/RectDiffSeedingSolver/layers"
2
8
 
3
9
  export function finalizeRects(params: {
4
10
  placed: Placed3D[]
5
- obstaclesByLayer: XYRect[][]
11
+ srj: SimpleRouteJson
6
12
  boardVoidRects: XYRect[]
7
13
  }): Rect3d[] {
8
14
  // Convert all placed (free space) nodes to output format
@@ -14,33 +20,32 @@ export function finalizeRects(params: {
14
20
  zLayers: [...p.zLayers].sort((a, b) => a - b),
15
21
  }))
16
22
 
17
- /**
18
- * Recover obstacles as mesh nodes.
19
- * Obstacles are stored per-layer in `obstaclesByLayer`, but we want to emit
20
- * single 3D nodes for multi-layer obstacles if they share the same rect.
21
- * We use the `XYRect` object reference identity to group layers.
22
- */
23
- const layersByObstacleRect = new Map<XYRect, number[]>()
23
+ const { zIndexByName } = buildZIndexMap(params.srj)
24
+ const layersByKey = new Map<string, { rect: XYRect; layers: Set<number> }>()
24
25
 
25
- params.obstaclesByLayer.forEach((layerObs, z) => {
26
- for (const rect of layerObs) {
27
- const layerIndices = layersByObstacleRect.get(rect) ?? []
28
- layerIndices.push(z)
29
- layersByObstacleRect.set(rect, layerIndices)
26
+ for (const obstacle of params.srj.obstacles ?? []) {
27
+ const rect = obstacleToXYRect(obstacle as any)
28
+ if (!rect) continue
29
+ const zLayers =
30
+ obstacle.zLayers?.length && obstacle.zLayers.length > 0
31
+ ? obstacle.zLayers
32
+ : obstacleZs(obstacle as any, zIndexByName)
33
+ const key = `${rect.x}:${rect.y}:${rect.width}:${rect.height}`
34
+ let entry = layersByKey.get(key)
35
+ if (!entry) {
36
+ entry = { rect, layers: new Set() }
37
+ layersByKey.set(key, entry)
30
38
  }
31
- })
32
-
33
- // Append obstacle nodes to the output
34
- const voidSet = new Set(params.boardVoidRects || [])
35
- for (const [rect, layerIndices] of layersByObstacleRect.entries()) {
36
- if (voidSet.has(rect)) continue // Skip void rects
39
+ zLayers.forEach((layer) => entry!.layers.add(layer))
40
+ }
37
41
 
42
+ for (const { rect, layers } of layersByKey.values()) {
38
43
  out.push({
39
44
  minX: rect.x,
40
45
  minY: rect.y,
41
46
  maxX: rect.x + rect.width,
42
47
  maxY: rect.y + rect.height,
43
- zLayers: layerIndices.sort((a, b) => a - b),
48
+ zLayers: Array.from(layers).sort((a, b) => a - b),
44
49
  isObstacle: true,
45
50
  })
46
51
  }
@@ -0,0 +1,17 @@
1
+ export const getColorForZLayer = (
2
+ zLayers: number[],
3
+ ): {
4
+ fill: string
5
+ stroke: string
6
+ } => {
7
+ const minZ = Math.min(...zLayers)
8
+ const colors = [
9
+ { fill: "#dbeafe", stroke: "#3b82f6" },
10
+ { fill: "#fef3c7", stroke: "#f59e0b" },
11
+ { fill: "#d1fae5", stroke: "#10b981" },
12
+ { fill: "#e9d5ff", stroke: "#a855f7" },
13
+ { fill: "#fed7aa", stroke: "#f97316" },
14
+ { fill: "#fecaca", stroke: "#ef4444" },
15
+ ] as const
16
+ return colors[minZ % colors.length]!
17
+ }
@@ -1,21 +1,28 @@
1
- import type { XYRect } from "../rectdiff-types"
2
- import { containsPoint } from "./rectdiff-geometry"
1
+ import type { RTreeRect } from "lib/types/capacity-mesh-types"
2
+ import RBush from "rbush"
3
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 {
4
+ export type OccupancyParams = {
5
+ layerCount: number
6
+ obstacleIndexByLayer: Array<RBush<RTreeRect> | undefined>
7
+ placedIndexByLayer: Array<RBush<RTreeRect> | undefined>
8
+ point: { x: number; y: number }
9
+ }
10
+
11
+ export function isFullyOccupiedAtPoint(params: OccupancyParams): boolean {
12
+ const query = {
13
+ minX: params.point.x,
14
+ minY: params.point.y,
15
+ maxX: params.point.x,
16
+ maxY: params.point.y,
17
+ }
12
18
  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
+ const obstacleIdx = params.obstacleIndexByLayer[z]
20
+ const hasObstacle = !!obstacleIdx && obstacleIdx.search(query).length > 0
21
+
22
+ const placedIdx = params.placedIndexByLayer[z]
23
+ const hasPlaced = !!placedIdx && placedIdx.search(query).length > 0
24
+
25
+ if (!hasObstacle && !hasPlaced) return false
19
26
  }
20
27
  return true
21
28
  }
@@ -0,0 +1,10 @@
1
+ import type { XYRect } from "lib/rectdiff-types"
2
+ import type { RTreeRect } from "lib/types/capacity-mesh-types"
3
+
4
+ export const rectToTree = (rect: XYRect): RTreeRect => ({
5
+ ...rect,
6
+ minX: rect.x,
7
+ minY: rect.y,
8
+ maxX: rect.x + rect.width,
9
+ maxY: rect.y + rect.height,
10
+ })
@@ -1,12 +1,14 @@
1
+ import type { RTreeRect } from "lib/types/capacity-mesh-types"
1
2
  import type { Placed3D, XYRect } from "../rectdiff-types"
2
3
  import { overlaps, subtractRect2D, EPS } from "./rectdiff-geometry"
4
+ import type RBush from "rbush"
3
5
 
4
6
  export function resizeSoftOverlaps(
5
7
  params: {
6
8
  layerCount: number
7
9
  placed: Placed3D[]
8
- placedByLayer: XYRect[][]
9
10
  options: any
11
+ placedIndexByLayer?: Array<RBush<RTreeRect> | undefined>
10
12
  },
11
13
  newIndex: number,
12
14
  ) {
@@ -54,21 +56,48 @@ export function resizeSoftOverlaps(
54
56
  }
55
57
  }
56
58
 
57
- // Remove (and clear placedByLayer)
59
+ // Remove fully overlapped nodes and keep indexes in sync
60
+ const rectToTree = (rect: XYRect): RTreeRect => ({
61
+ ...rect,
62
+ minX: rect.x,
63
+ minY: rect.y,
64
+ maxX: rect.x + rect.width,
65
+ maxY: rect.y + rect.height,
66
+ })
67
+ const sameRect = (a: RTreeRect, b: RTreeRect) =>
68
+ a.minX === b.minX &&
69
+ a.minY === b.minY &&
70
+ a.maxX === b.maxX &&
71
+ a.maxY === b.maxY
72
+
58
73
  removeIdx
59
74
  .sort((a, b) => b - a)
60
75
  .forEach((idx) => {
61
76
  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)
77
+ if (params.placedIndexByLayer) {
78
+ for (const z of rem.zLayers) {
79
+ const tree = params.placedIndexByLayer[z]
80
+ if (tree) tree.remove(rectToTree(rem.rect), sameRect)
81
+ }
66
82
  }
67
83
  })
68
84
 
69
85
  // Add replacements
70
86
  for (const p of toAdd) {
71
87
  params.placed.push(p)
72
- for (const z of p.zLayers) params.placedByLayer[z]!.push(p.rect)
88
+ for (const z of p.zLayers) {
89
+ if (params.placedIndexByLayer) {
90
+ const idx = params.placedIndexByLayer[z]
91
+ if (idx) {
92
+ idx.insert({
93
+ ...p.rect,
94
+ minX: p.rect.x,
95
+ minY: p.rect.y,
96
+ maxX: p.rect.x + p.rect.width,
97
+ maxY: p.rect.y + p.rect.height,
98
+ })
99
+ }
100
+ }
101
+ }
73
102
  }
74
103
  }
@@ -0,0 +1,7 @@
1
+ import type { RTreeRect } from "lib/types/capacity-mesh-types"
2
+
3
+ export const sameTreeRect = (a: RTreeRect, b: RTreeRect) =>
4
+ a.minX === b.minX &&
5
+ a.minY === b.minY &&
6
+ a.maxX === b.maxX &&
7
+ a.maxY === b.maxY
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/rectdiff",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -2,6 +2,11 @@ import { expect, test } from "bun:test"
2
2
  import simpleRouteJson from "../../test-assets/example01.json"
3
3
  import { RectDiffPipeline } from "../../lib/RectDiffPipeline"
4
4
  import { getSvgFromGraphicsObject } from "graphics-debug"
5
+ import {
6
+ buildZIndexMap,
7
+ obstacleToXYRect,
8
+ obstacleZs,
9
+ } from "lib/solvers/RectDiffSeedingSolver/layers"
5
10
 
6
11
  test.skip("example01", () => {
7
12
  const solver = new RectDiffPipeline({ simpleRouteJson })
@@ -17,7 +22,19 @@ test.skip("example01", () => {
17
22
  const step = 0.004
18
23
  const layerCount = simpleRouteJson.layerCount || 2
19
24
  const state = (solver as any).state
20
- const obstacles = state.obstaclesByLayer
25
+ const { zIndexByName } = buildZIndexMap(simpleRouteJson as any)
26
+ const obstacles = Array.from({ length: layerCount }, () => [] as any[])
27
+ for (const obstacle of simpleRouteJson.obstacles ?? []) {
28
+ const rect = obstacleToXYRect(obstacle as any)
29
+ if (!rect) continue
30
+ const zLayers =
31
+ obstacle.zLayers?.length && obstacle.zLayers.length > 0
32
+ ? obstacle.zLayers
33
+ : obstacleZs(obstacle as any, zIndexByName)
34
+ zLayers.forEach((z: number) => {
35
+ if (z >= 0 && z < layerCount) obstacles[z]!.push(rect)
36
+ })
37
+ }
21
38
  const placed = state.placed
22
39
 
23
40
  const gapPoints: Array<{ x: number; y: number; z: number }> = []
@@ -0,0 +1,130 @@
1
+ import type { GraphicsObject, Line, Point, Rect } from "graphics-debug"
2
+
3
+ export function getPerLayerVisualizations(
4
+ graphics: GraphicsObject,
5
+ ): Map<string, GraphicsObject> {
6
+ const rects = (graphics.rects ?? []) as NonNullable<Rect[]>
7
+ const lines = (graphics.lines ?? []) as NonNullable<Line[]>
8
+ const points = (graphics.points ?? []) as NonNullable<Point[]>
9
+
10
+ const zValues = new Set<number>()
11
+
12
+ const addZValuesFromLayer = (layer: string) => {
13
+ if (!layer.startsWith("z")) return
14
+ const rest = layer.slice(1)
15
+ if (!rest) return
16
+ for (const part of rest.split(",")) {
17
+ const value = Number.parseInt(part, 10)
18
+ if (!Number.isNaN(value)) zValues.add(value)
19
+ }
20
+ }
21
+
22
+ for (const rect of rects) addZValuesFromLayer(rect.layer!)
23
+ for (const line of lines) addZValuesFromLayer(line.layer!)
24
+ for (const point of points) addZValuesFromLayer(point.layer!)
25
+
26
+ const result = new Map<string, GraphicsObject>()
27
+ if (!zValues.size) return result
28
+
29
+ const sortedZ = Array.from(zValues).sort((a, b) => a - b)
30
+
31
+ const commonRects: NonNullable<Rect[]> = []
32
+ const perLayerRects: { layers: number[]; rect: Rect }[] = []
33
+
34
+ for (const rect of rects) {
35
+ const layer = rect.layer!
36
+ if (layer.startsWith("z")) {
37
+ const rest = layer.slice(1)
38
+ if (rest) {
39
+ const layers = rest
40
+ .split(",")
41
+ .map((part) => Number.parseInt(part, 10))
42
+ .filter((value) => !Number.isNaN(value))
43
+ if (layers.length) {
44
+ perLayerRects.push({ layers, rect })
45
+ continue
46
+ }
47
+ }
48
+ }
49
+ commonRects.push(rect)
50
+ }
51
+
52
+ const commonLines: NonNullable<Line[]> = []
53
+ const perLayerLines: { layers: number[]; line: Line }[] = []
54
+
55
+ for (const line of lines) {
56
+ const layer = line.layer!
57
+ if (layer.startsWith("z")) {
58
+ const rest = layer.slice(1)
59
+ if (rest) {
60
+ const layers = rest
61
+ .split(",")
62
+ .map((part) => Number.parseInt(part, 10))
63
+ .filter((value) => !Number.isNaN(value))
64
+ if (layers.length) {
65
+ perLayerLines.push({ layers, line })
66
+ continue
67
+ }
68
+ }
69
+ }
70
+ commonLines.push(line)
71
+ }
72
+
73
+ const commonPoints: NonNullable<Point[]> = []
74
+ const perLayerPoints: { layers: number[]; point: Point }[] = []
75
+
76
+ for (const point of points) {
77
+ const layer = point.layer!
78
+ if (layer.startsWith("z")) {
79
+ const rest = layer.slice(1)
80
+ if (rest) {
81
+ const layers = rest
82
+ .split(",")
83
+ .map((part) => Number.parseInt(part, 10))
84
+ .filter((value) => !Number.isNaN(value))
85
+ if (layers.length) {
86
+ perLayerPoints.push({ layers, point })
87
+ continue
88
+ }
89
+ }
90
+ }
91
+ commonPoints.push(point)
92
+ }
93
+
94
+ const allCombos: number[][] = [[]]
95
+ for (const z of sortedZ) {
96
+ const withZ = allCombos.map((combo) => [...combo, z])
97
+ allCombos.push(...withZ)
98
+ }
99
+
100
+ for (const combo of allCombos.filter((c) => c.length > 0)) {
101
+ const key = `z${combo.join(",")}`
102
+
103
+ const layerRects: NonNullable<Rect[]> = [...commonRects]
104
+ const layerLines: NonNullable<Line[]> = [...commonLines]
105
+ const layerPoints: NonNullable<Point[]> = [...commonPoints]
106
+
107
+ const intersects = (layers: number[]) =>
108
+ layers.some((layer) => combo.includes(layer))
109
+
110
+ for (const { layers, rect } of perLayerRects) {
111
+ if (intersects(layers)) layerRects.push(rect)
112
+ }
113
+ for (const { layers, line } of perLayerLines) {
114
+ if (intersects(layers)) layerLines.push(line)
115
+ }
116
+ for (const { layers, point } of perLayerPoints) {
117
+ if (intersects(layers)) layerPoints.push(point)
118
+ }
119
+
120
+ result.set(key, {
121
+ title: `${graphics.title ?? ""} - z${combo.join(",")}`,
122
+ coordinateSystem: graphics.coordinateSystem,
123
+ rects: layerRects,
124
+ lines: layerLines,
125
+ points: layerPoints,
126
+ })
127
+ }
128
+
129
+ return result
130
+ }
@@ -0,0 +1,33 @@
1
+ import type { Rect } from "graphics-debug"
2
+ import type { CapacityMeshNode } from "lib/types/capacity-mesh-types"
3
+ import { getColorForZLayer } from "lib/utils/getColorForZLayer"
4
+
5
+ export const makeCapacityMeshNodeWithLayerInfo = (
6
+ nodes: CapacityMeshNode[],
7
+ ): Map<string, Rect[]> => {
8
+ const map = new Map<string, Rect[]>()
9
+
10
+ for (const node of nodes) {
11
+ if (!node.availableZ.length) continue
12
+ const key = node.availableZ.join(",")
13
+ const colors = getColorForZLayer(node.availableZ)
14
+ const rect: Rect = {
15
+ center: node.center,
16
+ width: node.width,
17
+ height: node.height,
18
+ layer: `z${key}`,
19
+ stroke: "black",
20
+ fill: node._containsObstacle ? "red" : colors.fill,
21
+ label: "node",
22
+ }
23
+
24
+ const existing = map.get(key)
25
+ if (existing) {
26
+ existing.push(rect)
27
+ } else {
28
+ map.set(key, [rect])
29
+ }
30
+ }
31
+
32
+ return map
33
+ }