@tscircuit/rectdiff 0.0.33 → 0.0.34

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.
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
2
2
  import { GenericSolverDebugger } from "@tscircuit/solver-utils/react"
3
3
  import type { SimpleRouteJson } from "../lib/types/srj-types"
4
4
  import type { CapacityMeshNode } from "../lib/types/capacity-mesh-types"
5
+ import { matchesExactZFilter, parseZFilterInput } from "../lib/utils/z-filter"
5
6
  import * as THREE from "three"
6
7
  import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"
7
8
  import type { BaseSolver } from "@tscircuit/solver-utils"
@@ -168,6 +169,7 @@ const ThreeBoardView: React.FC<{
168
169
  shrinkBoxes: boolean
169
170
  boxShrinkAmount: number
170
171
  showBorders: boolean
172
+ selectedZValues: number[] | null
171
173
  }> = ({
172
174
  nodes,
173
175
  srj,
@@ -181,6 +183,7 @@ const ThreeBoardView: React.FC<{
181
183
  shrinkBoxes,
182
184
  boxShrinkAmount,
183
185
  showBorders,
186
+ selectedZValues,
184
187
  }) => {
185
188
  const containerRef = useRef<HTMLDivElement>(null)
186
189
  const destroyRef = useRef<() => void>(() => {})
@@ -203,9 +206,17 @@ const ThreeBoardView: React.FC<{
203
206
 
204
207
  const layerCount = layerNames.length || srj?.layerCount || 1
205
208
 
206
- const prisms = useMemo(
207
- () => buildPrismsFromNodes(nodes, layerCount),
208
- [nodes, layerCount],
209
+ const filteredNodes = useMemo(
210
+ () =>
211
+ nodes.filter((node) =>
212
+ matchesExactZFilter(node.availableZ ?? [], selectedZValues),
213
+ ),
214
+ [nodes, selectedZValues],
215
+ )
216
+
217
+ const filteredPrisms = useMemo(
218
+ () => buildPrismsFromNodes(filteredNodes, layerCount),
219
+ [filteredNodes, layerCount],
209
220
  )
210
221
 
211
222
  useEffect(() => {
@@ -372,6 +383,8 @@ const ThreeBoardView: React.FC<{
372
383
  .map((name) => zIndexByLayerName.get(name))
373
384
  .filter((z): z is number => typeof z === "number")
374
385
 
386
+ if (!matchesExactZFilter(zs, selectedZValues)) continue
387
+
375
388
  for (const z of zs) {
376
389
  if (z < 0 || z >= layerCount) continue
377
390
  obstaclesGroup.add(
@@ -389,7 +402,7 @@ const ThreeBoardView: React.FC<{
389
402
 
390
403
  // Output prisms from nodes (wireframe toggle like the experiment)
391
404
  if (showOutput) {
392
- for (const p of prisms) {
405
+ for (const p of filteredPrisms) {
393
406
  let box = p
394
407
  if (shrinkBoxes && boxShrinkAmount > 0) {
395
408
  const s = boxShrinkAmount
@@ -442,7 +455,7 @@ const ThreeBoardView: React.FC<{
442
455
  z1: layerCount,
443
456
  }
444
457
  : (() => {
445
- if (prisms.length === 0) {
458
+ if (filteredPrisms.length === 0) {
446
459
  return {
447
460
  minX: -10,
448
461
  maxX: 10,
@@ -456,7 +469,7 @@ const ThreeBoardView: React.FC<{
456
469
  minY = Infinity,
457
470
  maxX = -Infinity,
458
471
  maxY = -Infinity
459
- for (const p of prisms) {
472
+ for (const p of filteredPrisms) {
460
473
  minX = Math.min(minX, p.minX)
461
474
  maxX = Math.max(maxX, p.maxX)
462
475
  minY = Math.min(minY, p.minY)
@@ -517,7 +530,7 @@ const ThreeBoardView: React.FC<{
517
530
  }
518
531
  }, [
519
532
  srj,
520
- prisms,
533
+ filteredPrisms,
521
534
  layerCount,
522
535
  layerThickness,
523
536
  height,
@@ -530,6 +543,7 @@ const ThreeBoardView: React.FC<{
530
543
  shrinkBoxes,
531
544
  boxShrinkAmount,
532
545
  showBorders,
546
+ selectedZValues,
533
547
  ])
534
548
 
535
549
  return (
@@ -572,10 +586,18 @@ export const SolverDebugger3d: React.FC<SolverDebugger3dProps> = ({
572
586
  const [shrinkBoxes, setShrinkBoxes] = useState(true)
573
587
  const [boxShrinkAmount, setBoxShrinkAmount] = useState(0.1)
574
588
  const [showBorders, setShowBorders] = useState(true)
589
+ const [zFilterInput, setZFilterInput] = useState("")
575
590
 
576
591
  // Mesh nodes state - updated when solver completes or during stepping
577
592
  const [meshNodes, setMeshNodes] = useState<CapacityMeshNode[]>([])
578
593
 
594
+ const selectedZValues = useMemo(
595
+ () => parseZFilterInput(zFilterInput),
596
+ [zFilterInput],
597
+ )
598
+ const hasInvalidZFilter =
599
+ zFilterInput.trim().length > 0 && selectedZValues === null
600
+
579
601
  // Update mesh nodes from solver output
580
602
  const updateMeshNodes = useCallback(() => {
581
603
  try {
@@ -601,11 +623,16 @@ export const SolverDebugger3d: React.FC<SolverDebugger3dProps> = ({
601
623
  // Poll for updates during stepping (GenericSolverDebugger doesn't have onStep)
602
624
  useEffect(() => {
603
625
  const interval = setInterval(() => {
604
- // Only update if solver has output available
605
- if (solver.solved || solver.stats?.placed > 0) {
626
+ if (solver.solved) {
606
627
  updateMeshNodes()
628
+ clearInterval(interval)
629
+ return
607
630
  }
608
- }, 100) // Poll every 100ms during active solving
631
+
632
+ if (solver.stats?.placed > 0) {
633
+ updateMeshNodes()
634
+ }
635
+ }, 100)
609
636
 
610
637
  return () => clearInterval(interval)
611
638
  }, [updateMeshNodes, solver])
@@ -724,6 +751,32 @@ export const SolverDebugger3d: React.FC<SolverDebugger3dProps> = ({
724
751
  />
725
752
  Wireframe
726
753
  </label>
754
+
755
+ <label
756
+ style={{
757
+ display: "inline-flex",
758
+ gap: 8,
759
+ alignItems: "center",
760
+ fontSize: 13,
761
+ }}
762
+ >
763
+ <span>Z Filter</span>
764
+ <input
765
+ type="text"
766
+ value={zFilterInput}
767
+ onChange={(e) => setZFilterInput(e.target.value)}
768
+ placeholder="1 or 1,2,3"
769
+ style={{
770
+ width: 120,
771
+ padding: "6px 10px",
772
+ borderRadius: 6,
773
+ border: `1px solid ${hasInvalidZFilter ? "#dc2626" : "#cbd5e1"}`,
774
+ background: "white",
775
+ fontSize: 13,
776
+ }}
777
+ title="Show only exact z matches. Example: 1 matches [1], 1,2,3 matches [1,2,3]."
778
+ />
779
+ </label>
727
780
  </>
728
781
  )}
729
782
  </div>
@@ -752,6 +805,7 @@ export const SolverDebugger3d: React.FC<SolverDebugger3dProps> = ({
752
805
  shrinkBoxes={shrinkBoxes}
753
806
  boxShrinkAmount={boxShrinkAmount}
754
807
  showBorders={showBorders}
808
+ selectedZValues={selectedZValues}
755
809
  />
756
810
  )}
757
811
  </div>
@@ -2,7 +2,7 @@ import RBush from "rbush"
2
2
  import type { RectDiffExpansionSolverInput } from "../solvers/RectDiffExpansionSolver/RectDiffExpansionSolver"
3
3
  import type { SimpleRouteJson } from "../types/srj-types"
4
4
  import type { XYRect } from "../rectdiff-types"
5
- import type { RTreeRect } from "lib/types/capacity-mesh-types"
5
+ import type { RTreeRect } from "../types/capacity-mesh-types"
6
6
  import { buildZIndexMap } from "../solvers/RectDiffSeedingSolver/layers"
7
7
 
8
8
  /**
@@ -1,5 +1,5 @@
1
1
  import { BaseSolver } from "@tscircuit/solver-utils"
2
- import type { CapacityMeshNode } from "lib/types/capacity-mesh-types"
2
+ import type { CapacityMeshNode } from "../../types/capacity-mesh-types"
3
3
  import type { SegmentWithAdjacentEmptySpace } from "./FindSegmentsWithAdjacentEmptySpaceSolver"
4
4
  import type { GraphicsObject } from "graphics-debug"
5
5
  import RBush from "rbush"
@@ -7,7 +7,7 @@ import { EDGE_MAP, EDGES } from "./edge-constants"
7
7
  import { getBoundsFromCorners } from "./getBoundsFromCorners"
8
8
  import type { Bounds } from "@tscircuit/math-utils"
9
9
  import { midpoint, segmentToBoxMinDistance } from "@tscircuit/math-utils"
10
- import type { XYRect } from "lib/rectdiff-types"
10
+ import type { XYRect } from "../../rectdiff-types"
11
11
 
12
12
  const EPS = 1e-4
13
13
 
@@ -1,7 +1,7 @@
1
1
  import { BaseSolver } from "@tscircuit/solver-utils"
2
2
  import Flatbush from "flatbush"
3
3
  import type { GraphicsObject, NinePointAnchor } from "graphics-debug"
4
- import type { CapacityMeshNode } from "lib/types/capacity-mesh-types"
4
+ import type { CapacityMeshNode } from "../../types/capacity-mesh-types"
5
5
  import { projectToUncoveredSegments } from "./projectToUncoveredSegments"
6
6
  import { EDGES } from "./edge-constants"
7
7
  import { visuallyOffsetLine } from "./visuallyOffsetLine"
@@ -3,11 +3,11 @@ import {
3
3
  definePipelineStep,
4
4
  type PipelineStep,
5
5
  } from "@tscircuit/solver-utils"
6
- import type { CapacityMeshNode } from "lib/types/capacity-mesh-types"
6
+ import type { CapacityMeshNode } from "../../types/capacity-mesh-types"
7
7
  import type { GraphicsObject } from "graphics-debug"
8
8
  import { FindSegmentsWithAdjacentEmptySpaceSolver } from "./FindSegmentsWithAdjacentEmptySpaceSolver"
9
9
  import { ExpandEdgesToEmptySpaceSolver } from "./ExpandEdgesToEmptySpaceSolver"
10
- import type { XYRect } from "lib/rectdiff-types"
10
+ import type { XYRect } from "../../rectdiff-types"
11
11
 
12
12
  type GapFillSolverInput = {
13
13
  meshNodes: CapacityMeshNode[]
@@ -1,12 +1,15 @@
1
1
  import { BaseSolver } from "@tscircuit/solver-utils"
2
2
  import type { GraphicsObject } from "graphics-debug"
3
- import type { CapacityMeshNode, RTreeRect } from "lib/types/capacity-mesh-types"
3
+ import type {
4
+ CapacityMeshNode,
5
+ RTreeRect,
6
+ } from "../../types/capacity-mesh-types"
4
7
  import { expandRectFromSeed } from "../../utils/expandRectFromSeed"
5
8
  import { finalizeRects } from "../../utils/finalizeRects"
6
9
  import { resizeSoftOverlaps } from "../../utils/resizeSoftOverlaps"
7
10
  import { rectsToMeshNodes } from "./rectsToMeshNodes"
8
11
  import type { XYRect, Candidate3D, Placed3D } from "../../rectdiff-types"
9
- import type { Obstacle } from "lib/types/srj-types"
12
+ import type { Obstacle } from "../../types/srj-types"
10
13
  import RBush from "rbush"
11
14
  import { rectToTree } from "../../utils/rectToTree"
12
15
  import { sameTreeRect } from "../../utils/sameTreeRect"
@@ -7,11 +7,14 @@ import type {
7
7
  Obstacle,
8
8
  SimpleRouteConnection,
9
9
  SimpleRouteJson,
10
- } from "lib/types/srj-types"
11
- import type { GridFill3DOptions, XYRect } from "lib/rectdiff-types"
12
- import type { CapacityMeshNode, RTreeRect } from "lib/types/capacity-mesh-types"
13
- import { RectDiffSeedingSolver } from "lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver"
14
- import { RectDiffExpansionSolver } from "lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver"
10
+ } from "../../types/srj-types"
11
+ import type { GridFill3DOptions, XYRect } from "../../rectdiff-types"
12
+ import type {
13
+ CapacityMeshNode,
14
+ RTreeRect,
15
+ } from "../../types/capacity-mesh-types"
16
+ import { RectDiffSeedingSolver } from "../RectDiffSeedingSolver/RectDiffSeedingSolver"
17
+ import { RectDiffExpansionSolver } from "../RectDiffExpansionSolver/RectDiffExpansionSolver"
15
18
  import type { GraphicsObject } from "graphics-debug"
16
19
  import RBush from "rbush"
17
20
  import { buildObstacleIndexesByLayer } from "./buildObstacleIndexes"
@@ -1,14 +1,14 @@
1
- import type { SimpleRouteJson } from "lib/types/srj-types"
1
+ import type { SimpleRouteJson } from "../../types/srj-types"
2
2
  import RBush from "rbush"
3
- import { computeInverseRects } from "lib/solvers/RectDiffSeedingSolver/computeInverseRects"
3
+ import { computeInverseRects } from "../RectDiffSeedingSolver/computeInverseRects"
4
4
  import {
5
5
  buildZIndexMap,
6
6
  obstacleToXYRect,
7
7
  obstacleZs,
8
- } from "lib/solvers/RectDiffSeedingSolver/layers"
9
- import type { XYRect } from "lib/rectdiff-types"
10
- import type { RTreeRect } from "lib/types/capacity-mesh-types"
11
- import { padRect } from "lib/utils/padRect"
8
+ } from "../RectDiffSeedingSolver/layers"
9
+ import type { XYRect } from "../../rectdiff-types"
10
+ import type { RTreeRect } from "../../types/capacity-mesh-types"
11
+ import { padRect } from "../../utils/padRect"
12
12
 
13
13
  export const buildObstacleIndexesByLayer = (params: {
14
14
  srj: SimpleRouteJson
@@ -16,12 +16,12 @@ import { computeCandidates3D } from "./computeCandidates3D"
16
16
  import { computeEdgeCandidates3D } from "./computeEdgeCandidates3D"
17
17
  import { longestFreeSpanAroundZ } from "./longestFreeSpanAroundZ"
18
18
  import { allLayerNode } from "../../utils/buildHardPlacedByLayer"
19
- import { isFullyOccupiedAtPoint } from "lib/utils/isFullyOccupiedAtPoint"
19
+ import { isFullyOccupiedAtPoint } from "../../utils/isFullyOccupiedAtPoint"
20
20
  import { resizeSoftOverlaps } from "../../utils/resizeSoftOverlaps"
21
- import { getColorForZLayer } from "lib/utils/getColorForZLayer"
21
+ import { getColorForZLayer } from "../../utils/getColorForZLayer"
22
22
  import RBush from "rbush"
23
- import type { RTreeRect } from "lib/types/capacity-mesh-types"
24
- import { rectToTree } from "lib/utils/rectToTree"
23
+ import type { RTreeRect } from "../../types/capacity-mesh-types"
24
+ import { rectToTree } from "../../utils/rectToTree"
25
25
 
26
26
  export type RectDiffSeedingSolverInput = {
27
27
  simpleRouteJson: SimpleRouteJson
@@ -3,7 +3,7 @@ import { EPS, distancePointToRectEdges } from "../../utils/rectdiff-geometry"
3
3
  import { isFullyOccupiedAtPoint } from "../../utils/isFullyOccupiedAtPoint"
4
4
  import { longestFreeSpanAroundZ } from "./longestFreeSpanAroundZ"
5
5
  import type RBush from "rbush"
6
- import type { RTreeRect } from "lib/types/capacity-mesh-types"
6
+ import type { RTreeRect } from "../../types/capacity-mesh-types"
7
7
  const quantize = (value: number, precision = 1e-6) =>
8
8
  Math.round(value / precision) * precision
9
9
 
@@ -3,7 +3,7 @@ import { EPS, distancePointToRectEdges } from "../../utils/rectdiff-geometry"
3
3
  import { isFullyOccupiedAtPoint } from "../../utils/isFullyOccupiedAtPoint"
4
4
  import { longestFreeSpanAroundZ } from "./longestFreeSpanAroundZ"
5
5
  import type RBush from "rbush"
6
- import type { RTreeRect } from "lib/types/capacity-mesh-types"
6
+ import type { RTreeRect } from "../../types/capacity-mesh-types"
7
7
  const quantize = (value: number, precision = 1e-6) =>
8
8
  Math.round(value / precision) * precision
9
9
 
@@ -1,4 +1,4 @@
1
- import type { RTreeRect } from "lib/types/capacity-mesh-types"
1
+ import type { RTreeRect } from "../../types/capacity-mesh-types"
2
2
  import type { XYRect } from "../../rectdiff-types"
3
3
  import { clamp, containsPoint } from "../../utils/rectdiff-geometry"
4
4
  import type RBush from "rbush"
@@ -1,4 +1,4 @@
1
- import type { XYRect } from "lib/rectdiff-types"
1
+ import type { XYRect } from "../rectdiff-types"
2
2
 
3
3
  export type CapacityMeshNodeId = string
4
4
 
@@ -1,7 +1,7 @@
1
1
  import type RBush from "rbush"
2
2
  import type { XYRect } from "../rectdiff-types"
3
3
  import { EPS, gt, gte, lt, lte, overlaps } from "./rectdiff-geometry"
4
- import type { RTreeRect } from "lib/types/capacity-mesh-types"
4
+ import type { RTreeRect } from "../types/capacity-mesh-types"
5
5
  import { isSelfRect } from "./isSelfRect"
6
6
  import {
7
7
  searchStripDown,
@@ -1,4 +1,4 @@
1
- import type { Obstacle } from "lib/types/srj-types"
1
+ import type { Obstacle } from "../types/srj-types"
2
2
  import type { Placed3D, Rect3d, XYRect } from "../rectdiff-types"
3
3
  import {
4
4
  obstacleToXYRect,
@@ -1,4 +1,4 @@
1
- import type { RTreeRect } from "lib/types/capacity-mesh-types"
1
+ import type { RTreeRect } from "../types/capacity-mesh-types"
2
2
  import RBush from "rbush"
3
3
 
4
4
  export type OccupancyParams = {
@@ -1,4 +1,4 @@
1
- import type { XYRect } from "lib/rectdiff-types"
1
+ import type { XYRect } from "../rectdiff-types"
2
2
 
3
3
  const EPS = 1e-9
4
4
 
@@ -1,5 +1,5 @@
1
- import type { XYRect } from "lib/rectdiff-types"
2
- import type { RTreeRect } from "lib/types/capacity-mesh-types"
1
+ import type { XYRect } from "../rectdiff-types"
2
+ import type { RTreeRect } from "../types/capacity-mesh-types"
3
3
 
4
4
  export const rectToTree = (
5
5
  rect: XYRect,
@@ -1,4 +1,4 @@
1
- import type { SimpleRouteJson } from "lib/types/srj-types"
1
+ import type { SimpleRouteJson } from "../types/srj-types"
2
2
  import type { GraphicsObject } from "graphics-debug"
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
1
- import type { RTreeRect } from "lib/types/capacity-mesh-types"
1
+ import type { RTreeRect } from "../types/capacity-mesh-types"
2
2
  import type { Placed3D } from "../rectdiff-types"
3
3
  import { overlaps, subtractRect2D, EPS } from "./rectdiff-geometry"
4
4
  import type RBush from "rbush"
@@ -1,4 +1,4 @@
1
- import type { RTreeRect } from "lib/types/capacity-mesh-types"
1
+ import type { RTreeRect } from "../types/capacity-mesh-types"
2
2
 
3
3
  export const sameTreeRect = (a: RTreeRect, b: RTreeRect) =>
4
4
  a.minX === b.minX &&
@@ -1,4 +1,4 @@
1
- import type { XYRect } from "lib/rectdiff-types"
1
+ import type { XYRect } from "../rectdiff-types"
2
2
 
3
3
  export const searchStripRight = ({
4
4
  rect,
@@ -0,0 +1,43 @@
1
+ export function normalizeZValues(zValues: number[]): number[] {
2
+ return [...new Set(zValues)].sort((a, b) => a - b)
3
+ }
4
+
5
+ export function parseZFilterInput(input: string): number[] | null {
6
+ const zValues: number[] = []
7
+ let current = ""
8
+
9
+ for (const char of input) {
10
+ if (char >= "0" && char <= "9") {
11
+ current += char
12
+ continue
13
+ }
14
+
15
+ if (char === "," || char === " " || char === "\t" || char === "\n") {
16
+ if (char === ",") {
17
+ if (current === "") return null
18
+ zValues.push(Number(current))
19
+ current = ""
20
+ }
21
+ continue
22
+ }
23
+
24
+ return null
25
+ }
26
+
27
+ if (current !== "") zValues.push(Number(current))
28
+ if (zValues.length === 0) return null
29
+
30
+ return normalizeZValues(zValues)
31
+ }
32
+
33
+ export function matchesExactZFilter(
34
+ zValues: number[],
35
+ selectedZValues: number[] | null,
36
+ ): boolean {
37
+ if (!selectedZValues) return true
38
+
39
+ const normalized = normalizeZValues(zValues)
40
+ if (normalized.length !== selectedZValues.length) return false
41
+
42
+ return normalized.every((z, index) => z === selectedZValues[index])
43
+ }
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@tscircuit/rectdiff",
3
- "version": "0.0.33",
3
+ "version": "0.0.34",
4
4
  "type": "module",
5
- "main": "dist/index.js",
5
+ "main": "lib/index.ts",
6
+ "types": "lib/index.ts",
7
+ "exports": {
8
+ ".": "./lib/index.ts"
9
+ },
6
10
  "scripts": {
7
11
  "start": "cosmos",
8
12
  "build": "tsup-node ./lib/index.ts --format esm --dts",
@@ -0,0 +1,33 @@
1
+ import { expect, test } from "bun:test"
2
+ import { matchesExactZFilter, parseZFilterInput } from "../lib/utils/z-filter"
3
+
4
+ test("parseZFilterInput parses a single z", () => {
5
+ expect(parseZFilterInput("1")).toEqual([1])
6
+ })
7
+
8
+ test("parseZFilterInput parses multiple z values", () => {
9
+ expect(parseZFilterInput("1, 2,3")).toEqual([1, 2, 3])
10
+ })
11
+
12
+ test("parseZFilterInput normalizes duplicates and ordering", () => {
13
+ expect(parseZFilterInput("3,1,3,2")).toEqual([1, 2, 3])
14
+ })
15
+
16
+ test("parseZFilterInput returns null for empty input", () => {
17
+ expect(parseZFilterInput(" ")).toBeNull()
18
+ })
19
+
20
+ test("parseZFilterInput returns null for invalid input", () => {
21
+ expect(parseZFilterInput("1,a")).toBeNull()
22
+ })
23
+
24
+ test("matchesExactZFilter only matches exact single-layer selections", () => {
25
+ expect(matchesExactZFilter([1], [1])).toBe(true)
26
+ expect(matchesExactZFilter([1, 2], [1])).toBe(false)
27
+ })
28
+
29
+ test("matchesExactZFilter only matches exact shared-layer selections", () => {
30
+ expect(matchesExactZFilter([1, 2, 3], [1, 2, 3])).toBe(true)
31
+ expect(matchesExactZFilter([1, 2], [1, 2, 3])).toBe(false)
32
+ expect(matchesExactZFilter([1, 2, 3, 4], [1, 2, 3])).toBe(false)
33
+ })