From f491c1313ed5a0aaf6c4f7f02322c15eaade5042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Mon, 17 Nov 2025 23:51:21 +0530 Subject: [PATCH 01/14] adding test --- AGENTS.md | 920 +++++++++++++++++++++++++++++ test-assets/example01.json | 1124 ++++++++++++++++++++++++++++++++++++ 2 files changed, 2044 insertions(+) create mode 100644 AGENTS.md create mode 100644 test-assets/example01.json diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..19c143a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,920 @@ +we are pulling some code from another project to make +a new compoenent called `` and we are using `*.page.tsx` files to view exmaples please create an example page and setup code to be organized and tidy + + +check out ./test-assets/example01.json for example data + +here are the reference files from the other project +``` +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { GenericSolverDebugger } from "@tscircuit/solver-utils/react" +import type { SimpleRouteJson } from "../lib/types/srj-types" +import type { CapacityMeshNode } from "../lib/types/capacity-mesh-types" +import * as THREE from "three" +import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js" +import type { BaseSolver } from "@tscircuit/solver-utils" + +type SolverDebugger3dProps = { + solver: BaseSolver + /** Optional SRJ to show board bounds & rectangular obstacles */ + simpleRouteJson?: SimpleRouteJson + /** Visual Z thickness per layer (world units) */ + layerThickness?: number + /** Canvas height */ + height?: number + /** Initial toggles (user can change in UI) */ + defaultShowRoot?: boolean + defaultShowObstacles?: boolean + defaultShowOutput?: boolean + defaultWireframeOutput?: boolean + /** Wrap styles */ + style?: React.CSSProperties +} + +/* ----------------------------- helpers ----------------------------- */ + +function contiguousRuns(nums: number[]) { + const zs = [...new Set(nums)].sort((a, b) => a - b) + if (zs.length === 0) return [] as number[][] + const groups: number[][] = [] + let run: number[] = [zs[0]!] + for (let i = 1; i < zs.length; i++) { + if (zs[i] === zs[i - 1]! + 1) run.push(zs[i]!) + else { + groups.push(run) + run = [zs[i]!] + } + } + groups.push(run) + return groups +} + +/** Canonical layer order to mirror the solver & experiment */ +function layerSortKey(name: string) { + const n = name.toLowerCase() + if (n === "top") return -1_000_000 + if (n === "bottom") return 1_000_000 + const m = /^inner(\d+)$/i.exec(n) + if (m) return parseInt(m[1]!, 10) || 0 + return 100 + n.charCodeAt(0) +} +function canonicalizeLayerOrder(names: string[]) { + return [...new Set(names)].sort((a, b) => { + const ka = layerSortKey(a) + const kb = layerSortKey(b) + if (ka !== kb) return ka - kb + return a.localeCompare(b) + }) +} + +/** Build prisms by grouping identical XY nodes across contiguous Z */ +function buildPrismsFromNodes( + nodes: CapacityMeshNode[], + fallbackLayerCount: number, +): Array<{ + minX: number + maxX: number + minY: number + maxY: number + z0: number + z1: number +}> { + const xyKey = (n: CapacityMeshNode) => + `${n.center.x.toFixed(8)}|${n.center.y.toFixed(8)}|${n.width.toFixed(8)}|${n.height.toFixed(8)}` + const azKey = (n: CapacityMeshNode) => { + const zs = ( + n.availableZ && n.availableZ.length ? [...new Set(n.availableZ)] : [0] + ).sort((a, b) => a - b) + return `zset:${zs.join(",")}` + } + const key = (n: CapacityMeshNode) => `${xyKey(n)}|${azKey(n)}` + + const groups = new Map< + string, + { cx: number; cy: number; w: number; h: number; zs: number[] } + >() + for (const n of nodes) { + const k = key(n) + const zlist = n.availableZ?.length ? n.availableZ : [0] + const g = groups.get(k) + if (g) g.zs.push(...zlist) + else + groups.set(k, { + cx: n.center.x, + cy: n.center.y, + w: n.width, + h: n.height, + zs: [...zlist], + }) + } + + const prisms: Array<{ + minX: number + maxX: number + minY: number + maxY: number + z0: number + z1: number + }> = [] + for (const g of groups.values()) { + const minX = g.cx - g.w / 2 + const maxX = g.cx + g.w / 2 + const minY = g.cy - g.h / 2 + const maxY = g.cy + g.h / 2 + const runs = contiguousRuns(g.zs) + if (runs.length === 0) { + prisms.push({ + minX, + maxX, + minY, + maxY, + z0: 0, + z1: Math.max(1, fallbackLayerCount), + }) + } else { + for (const r of runs) { + prisms.push({ + minX, + maxX, + minY, + maxY, + z0: r[0]!, + z1: r[r.length - 1]! + 1, + }) + } + } + } + return prisms +} + +function clamp01(x: number) { + return Math.max(0, Math.min(1, x)) +} + +function darkenColor(hex: number, factor = 0.6): number { + const r = ((hex >> 16) & 0xff) * factor + const g = ((hex >> 8) & 0xff) * factor + const b = (hex & 0xff) * factor + const cr = Math.max(0, Math.min(255, Math.round(r))) + const cg = Math.max(0, Math.min(255, Math.round(g))) + const cb = Math.max(0, Math.min(255, Math.round(b))) + return (cr << 16) | (cg << 8) | cb +} + +/* ---------------------------- 3D Canvas ---------------------------- */ + +const ThreeBoardView: React.FC<{ + nodes: CapacityMeshNode[] + srj?: SimpleRouteJson + layerThickness: number + height: number + showRoot: boolean + showObstacles: boolean + showOutput: boolean + wireframeOutput: boolean + meshOpacity: number + shrinkBoxes: boolean + boxShrinkAmount: number + showBorders: boolean +}> = ({ + nodes, + srj, + layerThickness, + height, + showRoot, + showObstacles, + showOutput, + wireframeOutput, + meshOpacity, + shrinkBoxes, + boxShrinkAmount, + showBorders, +}) => { + const containerRef = useRef(null) + const destroyRef = useRef<() => void>(() => {}) + + const layerNames = useMemo(() => { + // Build from nodes (preferred, matches solver) and fall back to SRJ obstacle names + const fromNodes = canonicalizeLayerOrder(nodes.map((n) => n.layer)) + if (fromNodes.length) return fromNodes + const fromObs = canonicalizeLayerOrder( + (srj?.obstacles ?? []).flatMap((o) => o.layers ?? []), + ) + return fromObs.length ? fromObs : ["top"] + }, [nodes, srj]) + + const zIndexByLayerName = useMemo(() => { + const m = new Map() + layerNames.forEach((n, i) => m.set(n, i)) + return m + }, [layerNames]) + + const layerCount = layerNames.length || srj?.layerCount || 1 + + const prisms = useMemo( + () => buildPrismsFromNodes(nodes, layerCount), + [nodes, layerCount], + ) + + useEffect(() => { + let mounted = true + ;(async () => { + const el = containerRef.current + if (!el) return + if (!mounted) return + + destroyRef.current?.() + + const w = el.clientWidth || 800 + const h = el.clientHeight || height + + const renderer = new THREE.WebGLRenderer({ + antialias: true, + alpha: true, + premultipliedAlpha: false, + }) + // Increase pixel ratio for better alphaHash quality + renderer.setPixelRatio(window.devicePixelRatio) + renderer.setSize(w, h) + el.innerHTML = "" + el.appendChild(renderer.domElement) + + const scene = new THREE.Scene() + scene.background = new THREE.Color(0xf7f8fa) + + const camera = new THREE.PerspectiveCamera(45, w / h, 0.1, 10000) + camera.position.set(80, 80, 120) + + const controls = new OrbitControls(camera, renderer.domElement) + controls.enableDamping = true + + const amb = new THREE.AmbientLight(0xffffff, 0.9) + scene.add(amb) + const dir = new THREE.DirectionalLight(0xffffff, 0.6) + dir.position.set(1, 2, 3) + scene.add(dir) + + const rootGroup = new THREE.Group() + const obstaclesGroup = new THREE.Group() + const outputGroup = new THREE.Group() + scene.add(rootGroup, obstaclesGroup, outputGroup) + + // Axes helper for orientation (similar to experiment) + const axes = new THREE.AxesHelper(50) + scene.add(axes) + + const colorRoot = 0x111827 + const colorOb = 0xef4444 + + // Palette for layer-span-based coloring + const spanPalette = [ + 0x0ea5e9, // cyan-ish + 0x22c55e, // green + 0xf97316, // orange + 0xa855f7, // purple + 0xfacc15, // yellow + 0x38bdf8, // light blue + 0xec4899, // pink + 0x14b8a6, // teal + ] + const spanColorMap = new Map() + let spanColorIndex = 0 + const getSpanColor = (z0: number, z1: number) => { + const key = `${z0}-${z1}` + let c = spanColorMap.get(key) + if (c == null) { + c = spanPalette[spanColorIndex % spanPalette.length]! + spanColorMap.set(key, c) + spanColorIndex++ + } + return c + } + + function makeBoxMesh( + b: { + minX: number + maxX: number + minY: number + maxY: number + z0: number + z1: number + }, + color: number, + wire: boolean, + opacity = 0.45, + borders = false, + ) { + const dx = b.maxX - b.minX + const dz = b.maxY - b.minY // map board Y -> three Z + const dy = (b.z1 - b.z0) * layerThickness + const cx = -((b.minX + b.maxX) / 2) // negate X to match expected orientation + const cz = (b.minY + b.maxY) / 2 + // Negate Y so z=0 is at top, higher z goes down + const cy = -((b.z0 + b.z1) / 2) * layerThickness + + const geom = new THREE.BoxGeometry(dx, dy, dz) + if (wire) { + const edges = new THREE.EdgesGeometry(geom) + const line = new THREE.LineSegments( + edges, + new THREE.LineBasicMaterial({ color }), + ) + line.position.set(cx, cy, cz) + return line + } + const clampedOpacity = clamp01(opacity) + const mat = new THREE.MeshPhongMaterial({ + color, + opacity: clampedOpacity, + transparent: clampedOpacity < 1, + alphaHash: clampedOpacity < 1, + alphaToCoverage: true, + }) + + const mesh = new THREE.Mesh(geom, mat) + mesh.position.set(cx, cy, cz) + + if (!borders) return mesh + + const edges = new THREE.EdgesGeometry(geom) + const borderColor = darkenColor(color, 0.6) + const line = new THREE.LineSegments( + edges, + new THREE.LineBasicMaterial({ color: borderColor }), + ) + line.position.set(cx, cy, cz) + + const group = new THREE.Group() + group.add(mesh) + group.add(line) + return group + } + + // Root wireframe from SRJ bounds + if (srj && showRoot) { + const rootBox = { + minX: srj.bounds.minX, + maxX: srj.bounds.maxX, + minY: srj.bounds.minY, + maxY: srj.bounds.maxY, + z0: 0, + z1: layerCount, + } + rootGroup.add(makeBoxMesh(rootBox, colorRoot, true, 1)) + } + + // Obstacles — rectangular only — one slab per declared layer + if (srj && showObstacles) { + for (const ob of srj.obstacles ?? []) { + if (ob.type !== "rect") continue + const minX = ob.center.x - ob.width / 2 + const maxX = ob.center.x + ob.width / 2 + const minY = ob.center.y - ob.height / 2 + const maxY = ob.center.y + ob.height / 2 + + // Prefer explicit zLayers; otherwise map layer names to indices + const zs = + ob.zLayers && ob.zLayers.length + ? [...new Set(ob.zLayers)] + : (ob.layers ?? []) + .map((name) => zIndexByLayerName.get(name)) + .filter((z): z is number => typeof z === "number") + + for (const z of zs) { + if (z < 0 || z >= layerCount) continue + obstaclesGroup.add( + makeBoxMesh( + { minX, maxX, minY, maxY, z0: z, z1: z + 1 }, + colorOb, + false, + 0.35, + false, + ), + ) + } + } + } + + // Output prisms from nodes (wireframe toggle like the experiment) + if (showOutput) { + for (const p of prisms) { + let box = p + if (shrinkBoxes && boxShrinkAmount > 0) { + const s = boxShrinkAmount + + const widthX = p.maxX - p.minX + const widthY = p.maxY - p.minY + + // Never shrink more on a side than allowed by the configured shrink amount + // while ensuring we don't shrink past a minimum dimension of "s" + const maxShrinkEachSideX = Math.max(0, (widthX - s) / 2) + const maxShrinkEachSideY = Math.max(0, (widthY - s) / 2) + + const shrinkX = Math.min(s, maxShrinkEachSideX) + const shrinkY = Math.min(s, maxShrinkEachSideY) + + const minX = p.minX + shrinkX + const maxX = p.maxX - shrinkX + const minY = p.minY + shrinkY + const maxY = p.maxY - shrinkY + + // Guard against any degenerate box + if (minX >= maxX || minY >= maxY) { + continue + } + + box = { ...p, minX, maxX, minY, maxY } + } + + const color = getSpanColor(p.z0, p.z1) + outputGroup.add( + makeBoxMesh( + box, + color, + wireframeOutput, + meshOpacity, + showBorders && !wireframeOutput, + ), + ) + } + } + + // Fit camera + const fitBox = srj + ? { + minX: srj.bounds.minX, + maxX: srj.bounds.maxX, + minY: srj.bounds.minY, + maxY: srj.bounds.maxY, + z0: 0, + z1: layerCount, + } + : (() => { + if (prisms.length === 0) { + return { + minX: -10, + maxX: 10, + minY: -10, + maxY: 10, + z0: 0, + z1: layerCount, + } + } + let minX = Infinity, + minY = Infinity, + maxX = -Infinity, + maxY = -Infinity + for (const p of prisms) { + minX = Math.min(minX, p.minX) + maxX = Math.max(maxX, p.maxX) + minY = Math.min(minY, p.minY) + maxY = Math.max(maxY, p.maxY) + } + return { minX, maxX, minY, maxY, z0: 0, z1: layerCount } + })() + + const dx = fitBox.maxX - fitBox.minX + const dz = fitBox.maxY - fitBox.minY + const dy = (fitBox.z1 - fitBox.z0) * layerThickness + const size = Math.max(dx, dz, dy) + const dist = size * 2.0 + // Camera looks from above-right-front, with negative Y being "up" (z=0 at top) + camera.position.set( + -(fitBox.maxX + dist * 0.6), // negate X to account for flipped axis + -dy / 2 + dist, // negative Y is up, so position above the center + fitBox.maxY + dist * 0.6, + ) + camera.near = Math.max(0.1, size / 100) + camera.far = dist * 10 + size * 10 + camera.updateProjectionMatrix() + controls.target.set( + -((fitBox.minX + fitBox.maxX) / 2), // negate X to account for flipped axis + -dy / 2, // center of the inverted Y range + (fitBox.minY + fitBox.maxY) / 2, + ) + controls.update() + + const onResize = () => { + const W = el.clientWidth || w + const H = el.clientHeight || h + camera.aspect = W / H + camera.updateProjectionMatrix() + renderer.setSize(W, H) + } + window.addEventListener("resize", onResize) + + let raf = 0 + const animate = () => { + raf = requestAnimationFrame(animate) + controls.update() + renderer.render(scene, camera) + } + animate() + + destroyRef.current = () => { + cancelAnimationFrame(raf) + window.removeEventListener("resize", onResize) + renderer.dispose() + el.innerHTML = "" + } + })() + + return () => { + mounted = false + destroyRef.current?.() + } + }, [ + srj, + prisms, + layerCount, + layerThickness, + height, + showRoot, + showObstacles, + showOutput, + wireframeOutput, + zIndexByLayerName, + meshOpacity, + shrinkBoxes, + boxShrinkAmount, + showBorders, + ]) + + return ( +
+ ) +} + +/* ----------------------- Public wrapper component ----------------------- */ + +export const SolverDebugger3d: React.FC = ({ + solver, + simpleRouteJson, + layerThickness = 1, + height = 600, + defaultShowRoot = true, + defaultShowObstacles = false, // don't show obstacles by default + defaultShowOutput = true, + defaultWireframeOutput = false, + style, +}) => { + const [show3d, setShow3d] = useState(false) + const [rebuildKey, setRebuildKey] = useState(0) + + const [showRoot, setShowRoot] = useState(defaultShowRoot) + const [showObstacles, setShowObstacles] = useState(defaultShowObstacles) + const [showOutput, setShowOutput] = useState(defaultShowOutput) + const [wireframeOutput, setWireframeOutput] = useState(defaultWireframeOutput) + + const [meshOpacity, setMeshOpacity] = useState(0.6) + const [shrinkBoxes, setShrinkBoxes] = useState(true) + const [boxShrinkAmount, setBoxShrinkAmount] = useState(0.1) + const [showBorders, setShowBorders] = useState(true) + + // Mesh nodes state - updated when solver completes or during stepping + const [meshNodes, setMeshNodes] = useState([]) + + // Update mesh nodes from solver output + const updateMeshNodes = useCallback(() => { + try { + const output = solver.getOutput() + const nodes = output.meshNodes ?? [] + setMeshNodes(nodes) + } catch { + setMeshNodes([]) + } + }, [solver]) + + // Initialize mesh nodes on mount (in case solver is already solved) + useEffect(() => { + updateMeshNodes() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + // Handle solver completion + const handleSolverCompleted = useCallback(() => { + updateMeshNodes() + }, [updateMeshNodes]) + + // Poll for updates during stepping (GenericSolverDebugger doesn't have onStep) + useEffect(() => { + const interval = setInterval(() => { + // Only update if solver has output available + if (solver.solved || (solver as any).stats?.placed > 0) { + updateMeshNodes() + } + }, 100) // Poll every 100ms during active solving + + return () => clearInterval(interval) + }, [updateMeshNodes, solver]) + + const toggle3d = useCallback(() => setShow3d((s) => !s), []) + const rebuild = useCallback(() => setRebuildKey((k) => k + 1), []) + + return ( + <> +
+ + +
+ + {show3d && ( + + )} + + {/* experiment-like toggles */} + + + + + + {/* Mesh opacity slider */} + {show3d && ( + + )} + + {/* Shrink boxes option */} + {show3d && ( + <> + + {shrinkBoxes && ( + + )} + + )} + + {/* Show borders option */} + {show3d && ( + + )} + +
+ Drag to orbit · Wheel to zoom · Right-drag to pan +
+
+ + {show3d && ( + + )} +
+ + {/* White margin at bottom of the page */} +
+ + ) +} +``` + +``` +export type CapacityMeshNodeId = string + +export interface CapacityMesh { + nodes: CapacityMeshNode[] + edges: CapacityMeshEdge[] +} + +export interface CapacityMeshNode { + capacityMeshNodeId: string + center: { x: number; y: number } + width: number + height: number + layer: string + availableZ: number[] + + _depth?: number + + _completelyInsideObstacle?: boolean + _containsObstacle?: boolean + _containsTarget?: boolean + _targetConnectionName?: string + _strawNode?: boolean + _strawParentCapacityMeshNodeId?: CapacityMeshNodeId + + _adjacentNodeIds?: CapacityMeshNodeId[] + + _parent?: CapacityMeshNode +} + +export interface CapacityMeshEdge { + capacityMeshEdgeId: string + nodeIds: [CapacityMeshNodeId, CapacityMeshNodeId] +} +``` + + +``` +export type TraceId = string + +export interface SimpleRouteJson { + layerCount: number + minTraceWidth: number + minViaDiameter?: number + obstacles: Obstacle[] + connections: Array + bounds: { minX: number; maxX: number; minY: number; maxY: number } + outline?: Array<{ x: number; y: number }> +} + +export interface Obstacle { + type: "rect" + layers: string[] + zLayers?: number[] + center: { x: number; y: number } + width: number + height: number + connectedTo: TraceId[] + netIsAssignable?: boolean + offBoardConnectsTo?: TraceId[] +} + +export interface SimpleRouteConnection { + name: string + netConnectionName?: string + nominalTraceWidth?: number + pointsToConnect: Array<{ + x: number + y: number + layer: string + pointId?: string + pcb_port_id?: string + }> + externallyConnectedPointIds?: string[][] +} +``` + diff --git a/test-assets/example01.json b/test-assets/example01.json new file mode 100644 index 0000000..8a81179 --- /dev/null +++ b/test-assets/example01.json @@ -0,0 +1,1124 @@ +{ + "meshNodes": [ + { + "capacityMeshNodeId": "cmn_0", + "center": { + "x": 3.7847720000000002, + "y": -1.7996026029034198 + }, + "width": 9.93, + "height": 12.82920520580684, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_1", + "center": { + "x": -2.9227279999999993, + "y": -5.3575 + }, + "width": 3.4849999999999994, + "height": 19.285, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_2", + "center": { + "x": -2.3852279999999992, + "y": 11.145 + }, + "width": 4.56, + "height": 7.709999999999999, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_3", + "center": { + "x": 6.442272, + "y": 9.807500000000001 + }, + "width": 4.615, + "height": 10.385, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_4", + "center": { + "x": -7.321306647149842, + "y": -6.179968050960356 + }, + "width": 5.312157294299686, + "height": 4.025936101920712, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_5", + "center": { + "x": -9.957614, + "y": 7.372841999999999 + }, + "width": 5.084772000000001, + "height": 15.254316000000003, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_6", + "center": { + "x": -7.321306647149842, + "y": -11.903968050960355 + }, + "width": 5.312157294299686, + "height": 6.192063898079288, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_7", + "center": { + "x": 8.469281586933102, + "y": -11.60710260290342 + }, + "width": 8.061436826133797, + "height": 6.78579479419316, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_8", + "center": { + "x": -8.696306647149843, + "y": -2.2106580000000013 + }, + "width": 2.562157294299686, + "height": 3.912683999999997, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_9", + "center": { + "x": 0.8791675869331019, + "y": -11.60710260290342 + }, + "width": 4.118791173866203, + "height": 6.78579479419316, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_10", + "center": { + "x": 2.8747719999999974, + "y": 9.274999999999999 + }, + "width": 2.520000000000005, + "height": 0.8400000000000016, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_11", + "center": { + "x": 10.624886, + "y": -6.19060260290342 + }, + "width": 3.750228, + "height": 4.04720520580684, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_12", + "center": { + "x": 0.7547719999999978, + "y": 9.274999999999999 + }, + "width": 1.7199999999999944, + "height": 0.8400000000000016, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_13", + "center": { + "x": -11.238692647149843, + "y": -13.664726821657975 + }, + "width": 2.522614705700315, + "height": 2.670546356684051, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_14", + "center": { + "x": -11.238692647149843, + "y": -9.829453643315949 + }, + "width": 2.522614705700315, + "height": 2, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_15", + "center": { + "x": -11.238692647149843, + "y": -6.329453643315949 + }, + "width": 2.522614705700315, + "height": 2, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_16", + "center": { + "x": -11.238692647149843, + "y": -2.829453643315949 + }, + "width": 2.522614705700315, + "height": 2, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_17", + "center": { + "x": 3.6885631738662035, + "y": -13.35710260290342 + }, + "width": 1.5, + "height": 3.2857947941931602, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_18", + "center": { + "x": 2.6799999999999997, + "y": 11.815000000000001 + }, + "width": 2.520000000000005, + "height": 0.8400000000000016, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_19", + "center": { + "x": 10.624886, + "y": 14.0365 + }, + "width": 3.750228, + "height": 1.9269999999999996, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_20", + "center": { + "x": -4.172727999999999, + "y": 5.812500000000002 + }, + "width": 0.9849999999999994, + "height": 2.9549999999999983, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_21", + "center": { + "x": -2.4302279999999996, + "y": 5.7875000000000005 + }, + "width": 2.5, + "height": 3.005000000000001, + "layer": "top", + "availableZ": [ + 1, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_22", + "center": { + "x": -6.040227999999999, + "y": 14.0365 + }, + "width": 2.75, + "height": 1.9269999999999996, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_23", + "center": { + "x": -2.4302279999999996, + "y": 5.7875000000000005 + }, + "width": 2.5, + "height": 3.005000000000001, + "layer": "top", + "availableZ": [ + 2 + ] + }, + { + "capacityMeshNodeId": "cmn_24", + "center": { + "x": -0.6427279999999997, + "y": 5.952500000000001 + }, + "width": 1.0749999999999997, + "height": 2.6750000000000007, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_25", + "center": { + "x": 0.6573859999999989, + "y": 11.815000000000001 + }, + "width": 1.5252279999999967, + "height": 0.8400000000000016, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_26", + "center": { + "x": 0.32477200000000095, + "y": 13.6175 + }, + "width": 0.8600000000000008, + "height": 2.7650000000000006, + "layer": "top", + "availableZ": [ + 2 + ] + }, + { + "capacityMeshNodeId": "cmn_27", + "center": { + "x": -11.988692647149843, + "y": -11.579453643315949 + }, + "width": 1.022614705700315, + "height": 1.5, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_28", + "center": { + "x": -11.988692647149843, + "y": -8.079453643315949 + }, + "width": 1.022614705700315, + "height": 1.5, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_29", + "center": { + "x": -11.988692647149843, + "y": -4.579453643315949 + }, + "width": 1.022614705700315, + "height": 1.5, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_30", + "center": { + "x": -11.988692647149843, + "y": -1.041884821657976 + }, + "width": 1.022614705700315, + "height": 1.575137643315946, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_31", + "center": { + "x": 2.0147720000000002, + "y": 7.494999999999999 + }, + "width": 0.8399999999999999, + "height": 2.719999999999999, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_32", + "center": { + "x": 3.2847720000000002, + "y": 6.734999999999999 + }, + "width": 1.6999999999999997, + "height": 4.239999999999998, + "layer": "top", + "availableZ": [ + 1, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_33", + "center": { + "x": 3.2847720000000002, + "y": 6.734999999999999 + }, + "width": 1.6999999999999997, + "height": 4.239999999999998, + "layer": "top", + "availableZ": [ + 2 + ] + }, + { + "capacityMeshNodeId": "cmn_34", + "center": { + "x": 0.7447720000000004, + "y": 6.734999999999999 + }, + "width": 1.6999999999999997, + "height": 4.239999999999998, + "layer": "top", + "availableZ": [ + 1 + ] + }, + { + "capacityMeshNodeId": "cmn_35", + "center": { + "x": 0.7447720000000004, + "y": 6.734999999999999 + }, + "width": 1.6999999999999997, + "height": 4.239999999999998, + "layer": "top", + "availableZ": [ + 2 + ] + }, + { + "capacityMeshNodeId": "cmn_36", + "center": { + "x": 0.7447720000000004, + "y": 6.734999999999999 + }, + "width": 1.6999999999999997, + "height": 4.239999999999998, + "layer": "top", + "availableZ": [ + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_37", + "center": { + "x": 2.0147720000000002, + "y": 10.545 + }, + "width": 0.8399999999999999, + "height": 1.6999999999999993, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_38", + "center": { + "x": 2.3522720000000006, + "y": 14.467500000000001 + }, + "width": 3.1949999999999985, + "height": 1.0649999999999995, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_39", + "center": { + "x": 1.0873859999999993, + "y": 13.085 + }, + "width": 0.6652279999999959, + "height": 1.6999999999999993, + "layer": "top", + "availableZ": [ + 2 + ] + }, + { + "capacityMeshNodeId": "cmn_40", + "center": { + "x": 2.0147720000000002, + "y": 13.085 + }, + "width": 0.8399999999999999, + "height": 1.6999999999999993, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_41", + "center": { + "x": -6.040227999999999, + "y": 0.643 + }, + "width": 2.75, + "height": 0.5399999999999996, + "layer": "top", + "availableZ": [ + 0 + ] + }, + { + "capacityMeshNodeId": "cmn_42", + "center": { + "x": 0.324772000000001, + "y": 14.467500000000001 + }, + "width": 0.8600000000000009, + "height": 1.0650000000000013, + "layer": "top", + "availableZ": [ + 0 + ] + }, + { + "capacityMeshNodeId": "cmn_43", + "center": { + "x": 0.7447720000000002, + "y": 6.734999999999999 + }, + "width": 1.7000000000000002, + "height": 0.8399999999999999, + "layer": "top", + "availableZ": [ + 0 + ] + }, + { + "capacityMeshNodeId": "cmn_44", + "center": { + "x": 11.999886, + "y": 8.463000000000001 + }, + "width": 1.000228, + "height": 9.22, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_45", + "center": { + "x": 11.999886, + "y": -0.09699999999999953 + }, + "width": 1.000228, + "height": 7.9, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_46", + "center": { + "x": 10.124772, + "y": 0.643 + }, + "width": 2.7500000000000018, + "height": 0.5399999999999996, + "layer": "top", + "availableZ": [ + 0 + ] + }, + { + "capacityMeshNodeId": "cmn_47", + "center": { + "x": -6.356893060790263, + "y": -8.500436101920712 + }, + "width": 0.39999999999999947, + "height": 0.615000000000002, + "layer": "top", + "availableZ": [ + 0 + ] + }, + { + "capacityMeshNodeId": "cmn_48", + "center": { + "x": -6.040227999999999, + "y": 10.803 + }, + "width": 2.75, + "height": 0.5399999999999991, + "layer": "top", + "availableZ": [ + 0 + ] + }, + { + "capacityMeshNodeId": "cmn_49", + "center": { + "x": -6.040227999999999, + "y": 8.263000000000002 + }, + "width": 2.75, + "height": 0.5400000000000009, + "layer": "top", + "availableZ": [ + 0 + ] + }, + { + "capacityMeshNodeId": "cmn_50", + "center": { + "x": -6.040227999999999, + "y": 5.723000000000001 + }, + "width": 2.75, + "height": 0.5400000000000009, + "layer": "top", + "availableZ": [ + 0 + ] + }, + { + "capacityMeshNodeId": "cmn_51", + "center": { + "x": -6.040227999999999, + "y": 3.183 + }, + "width": 2.75, + "height": 0.5400000000000009, + "layer": "top", + "availableZ": [ + 0 + ] + }, + { + "capacityMeshNodeId": "cmn_52", + "center": { + "x": -6.040227999999999, + "y": -1.8969999999999998 + }, + "width": 2.75, + "height": 0.54, + "layer": "top", + "availableZ": [ + 0 + ] + }, + { + "capacityMeshNodeId": "cmn_53", + "center": { + "x": 10.124772, + "y": -1.8969999999999998 + }, + "width": 2.75, + "height": 0.54, + "layer": "top", + "availableZ": [ + 0 + ] + }, + { + "capacityMeshNodeId": "cmn_54", + "center": { + "x": 10.124772, + "y": 3.183 + }, + "width": 2.75, + "height": 0.5400000000000009, + "layer": "top", + "availableZ": [ + 0 + ] + }, + { + "capacityMeshNodeId": "cmn_55", + "center": { + "x": 10.124772, + "y": 5.723000000000001 + }, + "width": 2.75, + "height": 0.5400000000000009, + "layer": "top", + "availableZ": [ + 0 + ] + }, + { + "capacityMeshNodeId": "cmn_56", + "center": { + "x": 10.124772, + "y": 8.263000000000002 + }, + "width": 2.75, + "height": 0.5400000000000009, + "layer": "top", + "availableZ": [ + 0 + ] + }, + { + "capacityMeshNodeId": "cmn_57", + "center": { + "x": 10.124772, + "y": 10.803 + }, + "width": 2.75, + "height": 0.5399999999999991, + "layer": "top", + "availableZ": [ + 0 + ] + }, + { + "capacityMeshNodeId": "cmn_58", + "center": { + "x": 2.0147720000000002, + "y": 5.375 + }, + "width": 0.8399999999999999, + "height": 1.5199999999999996, + "layer": "top", + "availableZ": [ + 0, + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_59", + "center": { + "x": 3.6885631738662035, + "y": -9.96420520580684 + }, + "width": 1.5, + "height": 0.5, + "layer": "top", + "availableZ": [ + 0 + ] + }, + { + "capacityMeshNodeId": "cmn_60", + "center": { + "x": -2.4302279999999996, + "y": 5.7875 + }, + "width": 2.5, + "height": 0.8050000000000006, + "layer": "top", + "availableZ": [ + 0 + ] + }, + { + "capacityMeshNodeId": "cmn_61", + "center": { + "x": 3.2847720000000002, + "y": 6.734999999999999 + }, + "width": 1.6999999999999997, + "height": 0.8399999999999999, + "layer": "top", + "availableZ": [ + 0 + ] + }, + { + "capacityMeshNodeId": "cmn_62", + "center": { + "x": -8.567139177544973, + "y": -8.500436101920712 + }, + "width": 2.820492233509423, + "height": 0.615000000000002, + "layer": "top", + "availableZ": [ + 0 + ] + }, + { + "capacityMeshNodeId": "cmn_63", + "center": { + "x": -5.111060530395131, + "y": -8.500436101920712 + }, + "width": 0.8916650607902632, + "height": 0.615000000000002, + "layer": "top", + "availableZ": [ + 0 + ] + }, + { + "capacityMeshNodeId": "cmn_64", + "center": { + "x": 3.6885631738662035, + "y": -9.96420520580684 + }, + "width": 1.5, + "height": 0.5, + "layer": "top", + "availableZ": [ + 1 + ] + }, + { + "capacityMeshNodeId": "cmn_65", + "center": { + "x": -7.321306647149842, + "y": -8.500436101920712 + }, + "width": 5.312157294299686, + "height": 0.615000000000002, + "layer": "top", + "availableZ": [ + 1 + ] + }, + { + "capacityMeshNodeId": "cmn_66", + "center": { + "x": 3.6885631738662035, + "y": -9.96420520580684 + }, + "width": 1.5, + "height": 0.5, + "layer": "top", + "availableZ": [ + 2 + ] + }, + { + "capacityMeshNodeId": "cmn_67", + "center": { + "x": -7.321306647149842, + "y": -8.500436101920712 + }, + "width": 5.312157294299686, + "height": 0.615000000000002, + "layer": "top", + "availableZ": [ + 2 + ] + }, + { + "capacityMeshNodeId": "cmn_68", + "center": { + "x": 3.6885631738662035, + "y": -9.96420520580684 + }, + "width": 1.5, + "height": 0.5, + "layer": "top", + "availableZ": [ + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_69", + "center": { + "x": -7.321306647149842, + "y": -8.500436101920712 + }, + "width": 5.312157294299686, + "height": 0.615000000000002, + "layer": "top", + "availableZ": [ + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_70", + "center": { + "x": 0.7447720000000004, + "y": 10.545 + }, + "width": 1.6999999999999997, + "height": 1.6999999999999993, + "layer": "top", + "availableZ": [ + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_71", + "center": { + "x": 3.2847720000000002, + "y": 10.545 + }, + "width": 1.6999999999999997, + "height": 1.6999999999999993, + "layer": "top", + "availableZ": [ + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_72", + "center": { + "x": 3.2847720000000002, + "y": 13.085 + }, + "width": 1.6999999999999997, + "height": 1.6999999999999993, + "layer": "top", + "availableZ": [ + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_73", + "center": { + "x": -6.040227999999999, + "y": 4.453000000000001 + }, + "width": 2.75, + "height": 17.240000000000002, + "layer": "top", + "availableZ": [ + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_74", + "center": { + "x": 10.124772, + "y": 4.453000000000001 + }, + "width": 2.75, + "height": 17.240000000000002, + "layer": "top", + "availableZ": [ + 1, + 2, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_75", + "center": { + "x": 0.32477200000000095, + "y": 13.617500000000001 + }, + "width": 0.8600000000000008, + "height": 2.7650000000000006, + "layer": "top", + "availableZ": [ + 1, + 3 + ] + }, + { + "capacityMeshNodeId": "cmn_76", + "center": { + "x": 1.1747720000000008, + "y": 13.085 + }, + "width": 0.839999999999999, + "height": 1.6999999999999993, + "layer": "top", + "availableZ": [ + 1, + 3 + ] + } + ] +} \ No newline at end of file From 5356bf434ec2b4c4a1d18dd32ca4cf94e3d3843e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Tue, 18 Nov 2025 00:03:50 +0530 Subject: [PATCH 02/14] added view --- .gitignore | 1 + IMPLEMENTATION_SUMMARY.md | 126 ++++++ lib/CapacityNode3dDebugger.tsx | 783 +++++++++++++++++++++++++++++++++ lib/README.md | 77 ++++ lib/index.ts | 11 + lib/types.ts | 71 +++ package.json | 2 + pages/example01.page.tsx | 192 +++++++- test-component.tsx | 48 ++ test.html | 87 ++++ tsconfig.json | 10 +- 11 files changed, 1404 insertions(+), 4 deletions(-) create mode 100644 IMPLEMENTATION_SUMMARY.md create mode 100644 lib/CapacityNode3dDebugger.tsx create mode 100644 lib/README.md create mode 100644 lib/index.ts create mode 100644 lib/types.ts create mode 100644 test-component.tsx create mode 100644 test.html diff --git a/.gitignore b/.gitignore index a14702c..dad331a 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Finder (MacOS) folder config .DS_Store +package-lock.json diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..95eb700 --- /dev/null +++ b/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,126 @@ +# CapacityNode3dDebugger Implementation Summary + +## Overview +Successfully implemented the `CapacityNode3dDebugger` component as requested, pulling code from the reference project and organizing it in a clean, modular structure. + +## Files Created + +### Core Component +- **`lib/CapacityNode3dDebugger.tsx`** - Main 3D visualization component with full functionality +- **`lib/types.ts`** - TypeScript type definitions for all data structures +- **`lib/index.ts`** - Export barrel for clean imports + +### Example and Documentation +- **`pages/example01.page.tsx`** - Complete example page that loads data from test-assets +- **`lib/README.md`** - Comprehensive documentation with usage examples +- **`test.html`** - Simple test page for verification +- **`IMPLEMENTATION_SUMMARY.md`** - This summary document + +### Configuration Updates +- **`package.json`** - Added Three.js dependencies (`three` and `@types/three`) +- **`tsconfig.json`** - Updated TypeScript configuration for better compatibility + +## Key Features Implemented + +### 3D Visualization +- Interactive 3D scene using Three.js +- WebGL renderer with antialiasing +- OrbitControls for camera manipulation +- Proper lighting setup (ambient + directional) + +### Node Visualization +- Converts capacity mesh nodes to 3D prisms +- Groups identical XY nodes across contiguous Z layers +- Layer-span-based coloring with palette +- Optional wireframe rendering +- Configurable opacity and transparency + +### Interactive Controls +- Show/Hide 3D toggle +- Rebuild 3D scene button +- Toggle options: Root, Obstacles, Output, Wireframe +- Opacity slider (0-1 range) +- Box shrinking with configurable amount +- Border highlighting option + +### Data Handling +- Loads example data from `/test-assets/example01.json` +- Calculates bounds from node data +- Supports optional SimpleRouteJson for obstacles +- Proper TypeScript typing throughout + +### User Experience +- Clean, organized UI with intuitive controls +- Responsive design with proper styling +- Loading states and error handling +- Usage instructions and documentation +- Mouse control instructions + +## Technical Details + +### Dependencies +- **React** - Component framework +- **Three.js** - 3D graphics library +- **@types/three** - TypeScript definitions for Three.js + +### Architecture +- Functional React components with hooks +- Memoized calculations for performance +- Proper cleanup with useEffect +- TypeScript for type safety + +### Code Organization +- Modular structure with separate files for types and main component +- Clean imports/exports via barrel file +- Comprehensive documentation +- Example implementation + +## Usage Example + +```tsx +import { CapacityNode3dDebugger } from '../lib' +import type { CapacityMeshNode } from '../lib' + +const nodes: CapacityMeshNode[] = [ + { + capacityMeshNodeId: "node1", + center: { x: 0, y: 0 }, + width: 10, + height: 10, + layer: "top", + availableZ: [0, 1, 2] + } + // ... more nodes +] + +function App() { + return ( + + ) +} +``` + +## Testing and Verification + +✅ **TypeScript Compilation** - All components compile without errors +✅ **Dependencies** - Three.js and types properly installed +✅ **Data Loading** - Example page loads data from test-assets +✅ **Component Structure** - Clean, modular organization +✅ **Documentation** - Comprehensive README and examples + +## Next Steps + +1. Start the development server: `npm start` +2. Navigate to the example page in the browser +3. Click "Show 3D" to render the visualization +4. Interact with the 3D scene using mouse controls +5. Experiment with different visualization options + +The implementation is complete and ready for use! \ No newline at end of file diff --git a/lib/CapacityNode3dDebugger.tsx b/lib/CapacityNode3dDebugger.tsx new file mode 100644 index 0000000..0176249 --- /dev/null +++ b/lib/CapacityNode3dDebugger.tsx @@ -0,0 +1,783 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react" +import type { CapacityMeshNode, SimpleRouteJson } from "./types" +import * as THREE from "three" +import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js" + +type CapacityNode3dDebuggerProps = { + nodes: CapacityMeshNode[] + simpleRouteJson?: SimpleRouteJson + layerThickness?: number + height?: number + defaultShowRoot?: boolean + defaultShowObstacles?: boolean + defaultShowOutput?: boolean + defaultWireframeOutput?: boolean + style?: React.CSSProperties +} + +/* ----------------------------- helpers ----------------------------- */ + +function contiguousRuns(nums: number[]) { + const zs = Array.from(new Set(nums)).sort((a, b) => a - b) + if (zs.length === 0) return [] as number[][] + const groups: number[][] = [] + let run: number[] = [zs[0]!] + for (let i = 1; i < zs.length; i++) { + if (zs[i] === zs[i - 1]! + 1) run.push(zs[i]!) + else { + groups.push(run) + run = [zs[i]!] + } + } + groups.push(run) + return groups +} + +/** Canonical layer order to mirror the solver & experiment */ +function layerSortKey(name: string) { + const n = name.toLowerCase() + if (n === "top") return -1_000_000 + if (n === "bottom") return 1_000_000 + const m = /^inner(\d+)$/i.exec(n) + if (m) return parseInt(m[1]!, 10) || 0 + return 100 + n.charCodeAt(0) +} +function canonicalizeLayerOrder(names: string[]) { + return Array.from(new Set(names)).sort((a, b) => { + const ka = layerSortKey(a) + const kb = layerSortKey(b) + if (ka !== kb) return ka - kb + return a.localeCompare(b) + }) +} + +/** Build prisms by grouping identical XY nodes across contiguous Z */ +function buildPrismsFromNodes( + nodes: CapacityMeshNode[], + fallbackLayerCount: number, +): Array<{ + minX: number + maxX: number + minY: number + maxY: number + z0: number + z1: number +}> { + const xyKey = (n: CapacityMeshNode) => + `${n.center.x.toFixed(8)}|${n.center.y.toFixed(8)}|${n.width.toFixed(8)}|${n.height.toFixed(8)}` + const azKey = (n: CapacityMeshNode) => { + const zs = ( + n.availableZ && n.availableZ.length ? Array.from(new Set(n.availableZ)) : [0] + ).sort((a, b) => a - b) + return `zset:${zs.join(",")}` + } + const key = (n: CapacityMeshNode) => `${xyKey(n)}|${azKey(n)}` + + const groups = new Map< + string, + { cx: number; cy: number; w: number; h: number; zs: number[] } + >() + for (const n of nodes) { + const k = key(n) + const zlist = n.availableZ?.length ? n.availableZ : [0] + const g = groups.get(k) + if (g) g.zs.push(...zlist) + else + groups.set(k, { + cx: n.center.x, + cy: n.center.y, + w: n.width, + h: n.height, + zs: [...zlist], + }) + } + + const prisms: Array<{ + minX: number + maxX: number + minY: number + maxY: number + z0: number + z1: number + }> = [] + for (const g of Array.from(groups.values())) { + const minX = g.cx - g.w / 2 + const maxX = g.cx + g.w / 2 + const minY = g.cy - g.h / 2 + const maxY = g.cy + g.h / 2 + const runs = contiguousRuns(g.zs) + if (runs.length === 0) { + prisms.push({ + minX, + maxX, + minY, + maxY, + z0: 0, + z1: Math.max(1, fallbackLayerCount), + }) + } else { + for (const r of runs) { + prisms.push({ + minX, + maxX, + minY, + maxY, + z0: r[0]!, + z1: r[r.length - 1]! + 1, + }) + } + } + } + return prisms +} + +function clamp01(x: number) { + return Math.max(0, Math.min(1, x)) +} + +function darkenColor(hex: number, factor = 0.6): number { + const r = ((hex >> 16) & 0xff) * factor + const g = ((hex >> 8) & 0xff) * factor + const b = (hex & 0xff) * factor + const cr = Math.max(0, Math.min(255, Math.round(r))) + const cg = Math.max(0, Math.min(255, Math.round(g))) + const cb = Math.max(0, Math.min(255, Math.round(b))) + return (cr << 16) | (cg << 8) | cb +} + +/* ---------------------------- 3D Canvas ---------------------------- */ + +const ThreeBoardView: React.FC<{ + nodes: CapacityMeshNode[] + srj?: SimpleRouteJson + layerThickness: number + height: number + showRoot: boolean + showObstacles: boolean + showOutput: boolean + wireframeOutput: boolean + meshOpacity: number + shrinkBoxes: boolean + boxShrinkAmount: number + showBorders: boolean +}> = ({ + nodes, + srj, + layerThickness, + height, + showRoot, + showObstacles, + showOutput, + wireframeOutput, + meshOpacity, + shrinkBoxes, + boxShrinkAmount, + showBorders, +}) => { + const containerRef = useRef(null) + const destroyRef = useRef<() => void>(() => {}) + + const layerNames = useMemo(() => { + // Build from nodes (preferred, matches solver) and fall back to SRJ obstacle names + const fromNodes = canonicalizeLayerOrder(nodes.map((n) => n.layer)) + if (fromNodes.length) return fromNodes + const fromObs = canonicalizeLayerOrder( + (srj?.obstacles ?? []).flatMap((o) => o.layers ?? []), + ) + return fromObs.length ? fromObs : ["top"] + }, [nodes, srj]) + + const zIndexByLayerName = useMemo(() => { + const m = new Map() + layerNames.forEach((n, i) => m.set(n, i)) + return m + }, [layerNames]) + + const layerCount = layerNames.length || srj?.layerCount || 1 + + const prisms = useMemo( + () => buildPrismsFromNodes(nodes, layerCount), + [nodes, layerCount], + ) + + useEffect(() => { + let mounted = true + ;(async () => { + const el = containerRef.current + if (!el) return + if (!mounted) return + + destroyRef.current?.() + + const w = el.clientWidth || 800 + const h = el.clientHeight || height + + const renderer = new THREE.WebGLRenderer({ + antialias: true, + alpha: true, + premultipliedAlpha: false, + }) + // Increase pixel ratio for better alphaHash quality + renderer.setPixelRatio(window.devicePixelRatio) + renderer.setSize(w, h) + el.innerHTML = "" + el.appendChild(renderer.domElement) + + const scene = new THREE.Scene() + scene.background = new THREE.Color(0xf7f8fa) + + const camera = new THREE.PerspectiveCamera(45, w / h, 0.1, 10000) + camera.position.set(80, 80, 120) + + const controls = new OrbitControls(camera, renderer.domElement) + controls.enableDamping = true + + const amb = new THREE.AmbientLight(0xffffff, 0.9) + scene.add(amb) + const dir = new THREE.DirectionalLight(0xffffff, 0.6) + dir.position.set(1, 2, 3) + scene.add(dir) + + const rootGroup = new THREE.Group() + const obstaclesGroup = new THREE.Group() + const outputGroup = new THREE.Group() + scene.add(rootGroup, obstaclesGroup, outputGroup) + + // Axes helper for orientation (similar to experiment) + const axes = new THREE.AxesHelper(50) + scene.add(axes) + + const colorRoot = 0x111827 + const colorOb = 0xef4444 + + // Palette for layer-span-based coloring + const spanPalette = [ + 0x0ea5e9, // cyan-ish + 0x22c55e, // green + 0xf97316, // orange + 0xa855f7, // purple + 0xfacc15, // yellow + 0x38bdf8, // light blue + 0xec4899, // pink + 0x14b8a6, // teal + ] + const spanColorMap = new Map() + let spanColorIndex = 0 + const getSpanColor = (z0: number, z1: number) => { + const key = `${z0}-${z1}` + let c = spanColorMap.get(key) + if (c == null) { + c = spanPalette[spanColorIndex % spanPalette.length]! + spanColorMap.set(key, c) + spanColorIndex++ + } + return c + } + + function makeBoxMesh( + b: { + minX: number + maxX: number + minY: number + maxY: number + z0: number + z1: number + }, + color: number, + wire: boolean, + opacity = 0.45, + borders = false, + ) { + const dx = b.maxX - b.minX + const dz = b.maxY - b.minY // map board Y -> three Z + const dy = (b.z1 - b.z0) * layerThickness + const cx = -((b.minX + b.maxX) / 2) // negate X to match expected orientation + const cz = (b.minY + b.maxY) / 2 + // Negate Y so z=0 is at top, higher z goes down + const cy = -((b.z0 + b.z1) / 2) * layerThickness + + const geom = new THREE.BoxGeometry(dx, dy, dz) + if (wire) { + const edges = new THREE.EdgesGeometry(geom) + const line = new THREE.LineSegments( + edges, + new THREE.LineBasicMaterial({ color }), + ) + line.position.set(cx, cy, cz) + return line + } + const clampedOpacity = clamp01(opacity) + const mat = new THREE.MeshPhongMaterial({ + color, + opacity: clampedOpacity, + transparent: clampedOpacity < 1, + alphaHash: clampedOpacity < 1, + alphaToCoverage: true, + }) + + const mesh = new THREE.Mesh(geom, mat) + mesh.position.set(cx, cy, cz) + + if (!borders) return mesh + + const edges = new THREE.EdgesGeometry(geom) + const borderColor = darkenColor(color, 0.6) + const line = new THREE.LineSegments( + edges, + new THREE.LineBasicMaterial({ color: borderColor }), + ) + line.position.set(cx, cy, cz) + + const group = new THREE.Group() + group.add(mesh) + group.add(line) + return group + } + + // Root wireframe from SRJ bounds + if (srj && showRoot) { + const rootBox = { + minX: srj.bounds.minX, + maxX: srj.bounds.maxX, + minY: srj.bounds.minY, + maxY: srj.bounds.maxY, + z0: 0, + z1: layerCount, + } + rootGroup.add(makeBoxMesh(rootBox, colorRoot, true, 1)) + } + + // Obstacles — rectangular only — one slab per declared layer + if (srj && showObstacles) { + for (const ob of srj.obstacles ?? []) { + if (ob.type !== "rect") continue + const minX = ob.center.x - ob.width / 2 + const maxX = ob.center.x + ob.width / 2 + const minY = ob.center.y - ob.height / 2 + const maxY = ob.center.y + ob.height / 2 + + // Prefer explicit zLayers; otherwise map layer names to indices + const zs = + ob.zLayers && ob.zLayers.length + ? Array.from(new Set(ob.zLayers)) + : (ob.layers ?? []) + .map((name) => zIndexByLayerName.get(name)) + .filter((z): z is number => typeof z === "number") + + for (const z of zs) { + if (z < 0 || z >= layerCount) continue + obstaclesGroup.add( + makeBoxMesh( + { minX, maxX, minY, maxY, z0: z, z1: z + 1 }, + colorOb, + false, + 0.35, + false, + ), + ) + } + } + } + + // Output prisms from nodes (wireframe toggle like the experiment) + if (showOutput) { + for (const p of prisms) { + let box = p + if (shrinkBoxes && boxShrinkAmount > 0) { + const s = boxShrinkAmount + + const widthX = p.maxX - p.minX + const widthY = p.maxY - p.minY + + // Never shrink more on a side than allowed by the configured shrink amount + // while ensuring we don't shrink past a minimum dimension of "s" + const maxShrinkEachSideX = Math.max(0, (widthX - s) / 2) + const maxShrinkEachSideY = Math.max(0, (widthY - s) / 2) + + const shrinkX = Math.min(s, maxShrinkEachSideX) + const shrinkY = Math.min(s, maxShrinkEachSideY) + + const minX = p.minX + shrinkX + const maxX = p.maxX - shrinkX + const minY = p.minY + shrinkY + const maxY = p.maxY - shrinkY + + // Guard against any degenerate box + if (minX >= maxX || minY >= maxY) { + continue + } + + box = { ...p, minX, maxX, minY, maxY } + } + + const color = getSpanColor(p.z0, p.z1) + outputGroup.add( + makeBoxMesh( + box, + color, + wireframeOutput, + meshOpacity, + showBorders && !wireframeOutput, + ), + ) + } + } + + // Fit camera + const fitBox = srj + ? { + minX: srj.bounds.minX, + maxX: srj.bounds.maxX, + minY: srj.bounds.minY, + maxY: srj.bounds.maxY, + z0: 0, + z1: layerCount, + } + : (() => { + if (prisms.length === 0) { + return { + minX: -10, + maxX: 10, + minY: -10, + maxY: 10, + z0: 0, + z1: layerCount, + } + } + let minX = Infinity, + minY = Infinity, + maxX = -Infinity, + maxY = -Infinity + for (const p of prisms) { + minX = Math.min(minX, p.minX) + maxX = Math.max(maxX, p.maxX) + minY = Math.min(minY, p.minY) + maxY = Math.max(maxY, p.maxY) + } + return { minX, maxX, minY, maxY, z0: 0, z1: layerCount } + })() + + const dx = fitBox.maxX - fitBox.minX + const dz = fitBox.maxY - fitBox.minY + const dy = (fitBox.z1 - fitBox.z0) * layerThickness + const size = Math.max(dx, dz, dy) + const dist = size * 2.0 + // Camera looks from above-right-front, with negative Y being "up" (z=0 at top) + camera.position.set( + -(fitBox.maxX + dist * 0.6), // negate X to account for flipped axis + -dy / 2 + dist, // negative Y is up, so position above the center + fitBox.maxY + dist * 0.6, + ) + camera.near = Math.max(0.1, size / 100) + camera.far = dist * 10 + size * 10 + camera.updateProjectionMatrix() + controls.target.set( + -((fitBox.minX + fitBox.maxX) / 2), // negate X to account for flipped axis + -dy / 2, // center of the inverted Y range + (fitBox.minY + fitBox.maxY) / 2, + ) + controls.update() + + const onResize = () => { + const W = el.clientWidth || w + const H = el.clientHeight || h + camera.aspect = W / H + camera.updateProjectionMatrix() + renderer.setSize(W, H) + } + window.addEventListener("resize", onResize) + + let raf = 0 + const animate = () => { + raf = requestAnimationFrame(animate) + controls.update() + renderer.render(scene, camera) + } + animate() + + destroyRef.current = () => { + cancelAnimationFrame(raf) + window.removeEventListener("resize", onResize) + renderer.dispose() + el.innerHTML = "" + } + })() + + return () => { + mounted = false + destroyRef.current?.() + } + }, [ + srj, + prisms, + layerCount, + layerThickness, + height, + showRoot, + showObstacles, + showOutput, + wireframeOutput, + zIndexByLayerName, + meshOpacity, + shrinkBoxes, + boxShrinkAmount, + showBorders, + ]) + + return ( +
+ ) +} + +/* ----------------------- Public wrapper component ----------------------- */ + +export const CapacityNode3dDebugger: React.FC = ({ + nodes, + simpleRouteJson, + layerThickness = 1, + height = 600, + defaultShowRoot = true, + defaultShowObstacles = false, // don't show obstacles by default + defaultShowOutput = true, + defaultWireframeOutput = false, + style, +}) => { + const [show3d, setShow3d] = useState(false) + const [rebuildKey, setRebuildKey] = useState(0) + + const [showRoot, setShowRoot] = useState(defaultShowRoot) + const [showObstacles, setShowObstacles] = useState(defaultShowObstacles) + const [showOutput, setShowOutput] = useState(defaultShowOutput) + const [wireframeOutput, setWireframeOutput] = useState(defaultWireframeOutput) + + const [meshOpacity, setMeshOpacity] = useState(0.6) + const [shrinkBoxes, setShrinkBoxes] = useState(true) + const [boxShrinkAmount, setBoxShrinkAmount] = useState(0.1) + const [showBorders, setShowBorders] = useState(true) + + const toggle3d = useCallback(() => setShow3d((s) => !s), []) + const rebuild = useCallback(() => setRebuildKey((k) => k + 1), []) + + return ( + <> +
+
+ + {show3d && ( + + )} + + {/* experiment-like toggles */} + + + + + + {/* Mesh opacity slider */} + {show3d && ( + + )} + + {/* Shrink boxes option */} + {show3d && ( + <> + + {shrinkBoxes && ( + + )} + + )} + + {/* Show borders option */} + {show3d && ( + + )} + +
+ Drag to orbit · Wheel to zoom · Right-drag to pan +
+
+ + {show3d && ( + + )} +
+ + {/* White margin at bottom of the page */} +
+ + ) +} \ No newline at end of file diff --git a/lib/README.md b/lib/README.md new file mode 100644 index 0000000..90ae421 --- /dev/null +++ b/lib/README.md @@ -0,0 +1,77 @@ +# CapacityNode3dDebugger + +A React component for visualizing capacity mesh nodes in 3D space using Three.js. + +## Features + +- 3D visualization of capacity mesh nodes +- Interactive controls for orbiting, zooming, and panning +- Layer-based visualization with customizable thickness +- Toggle options for different visual elements (root, obstacles, output) +- Wireframe and solid rendering modes +- Opacity controls for mesh visualization +- Box shrinking for better spatial visualization +- Border highlighting for mesh boxes + +## Usage + +```tsx +import { CapacityNode3dDebugger } from './lib/CapacityNode3dDebugger' +import type { CapacityMeshNode } from './lib/types' + +const nodes: CapacityMeshNode[] = [ + { + capacityMeshNodeId: "node1", + center: { x: 0, y: 0 }, + width: 10, + height: 10, + layer: "top", + availableZ: [0, 1, 2] + } + // ... more nodes +] + +function App() { + return ( + + ) +} +``` + +## Props + +- `nodes`: Array of capacity mesh nodes to visualize +- `simpleRouteJson`: Optional SimpleRouteJson data for obstacles and bounds +- `layerThickness`: Visual Z thickness per layer (default: 1) +- `height`: Canvas height (default: 600) +- `defaultShowRoot`: Show root bounds initially (default: true) +- `defaultShowObstacles`: Show obstacles initially (default: false) +- `defaultShowOutput`: Show output mesh initially (default: true) +- `defaultWireframeOutput`: Use wireframe for output initially (default: false) +- `style`: Optional CSS styles for the container + +## Controls + +- **Show 3D/Hide 3D**: Toggle 3D visualization +- **Rebuild 3D**: Rebuild the 3D scene +- **Root**: Toggle root bounds visibility +- **Obstacles**: Toggle obstacles visibility +- **Output**: Toggle output mesh visibility +- **Wireframe Output**: Toggle between solid and wireframe rendering +- **Opacity**: Adjust mesh opacity (0-1) +- **Shrink boxes**: Enable box shrinking for better visualization +- **Show borders**: Toggle border highlighting on mesh boxes + +## Mouse Controls + +- **Drag**: Orbit camera +- **Wheel**: Zoom in/out +- **Right-drag**: Pan camera \ No newline at end of file diff --git a/lib/index.ts b/lib/index.ts new file mode 100644 index 0000000..748e830 --- /dev/null +++ b/lib/index.ts @@ -0,0 +1,11 @@ +export { CapacityNode3dDebugger } from './CapacityNode3dDebugger' +export type { + CapacityMeshNode, + CapacityMeshEdge, + CapacityMesh, + CapacityMeshNodeId, + SimpleRouteJson, + Obstacle, + SimpleRouteConnection, + TraceId +} from './types' \ No newline at end of file diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 0000000..60d15b9 --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,71 @@ +export type CapacityMeshNodeId = string + +export interface CapacityMesh { + nodes: CapacityMeshNode[] + edges: CapacityMeshEdge[] +} + +export interface CapacityMeshNode { + capacityMeshNodeId: string + center: { x: number; y: number } + width: number + height: number + layer: string + availableZ: number[] + + _depth?: number + + _completelyInsideObstacle?: boolean + _containsObstacle?: boolean + _containsTarget?: boolean + _targetConnectionName?: string + _strawNode?: boolean + _strawParentCapacityMeshNodeId?: CapacityMeshNodeId + + _adjacentNodeIds?: CapacityMeshNodeId[] + + _parent?: CapacityMeshNode +} + +export interface CapacityMeshEdge { + capacityMeshEdgeId: string + nodeIds: [CapacityMeshNodeId, CapacityMeshNodeId] +} + +export type TraceId = string + +export interface SimpleRouteJson { + layerCount: number + minTraceWidth: number + minViaDiameter?: number + obstacles: Obstacle[] + connections: Array + bounds: { minX: number; maxX: number; minY: number; maxY: number } + outline?: Array<{ x: number; y: number }> +} + +export interface Obstacle { + type: "rect" + layers: string[] + zLayers?: number[] + center: { x: number; y: number } + width: number + height: number + connectedTo: TraceId[] + netIsAssignable?: boolean + offBoardConnectsTo?: TraceId[] +} + +export interface SimpleRouteConnection { + name: string + netConnectionName?: string + nominalTraceWidth?: number + pointsToConnect: Array<{ + x: number + y: number + layer: string + pointId?: string + pcb_port_id?: string + }> + externallyConnectedPointIds?: string[][] +} \ No newline at end of file diff --git a/package.json b/package.json index dbf6892..1bb0f83 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,12 @@ }, "devDependencies": { "@types/bun": "latest", + "@types/three": "^0.170.0", "react": "^19.2.0", "react-cosmos": "^7.0.0", "react-cosmos-plugin-vite": "^7.0.0", "react-dom": "^19.2.0", + "three": "^0.171.0", "vite": "^7.2.2" }, "peerDependencies": { diff --git a/pages/example01.page.tsx b/pages/example01.page.tsx index cb57e36..349227f 100644 --- a/pages/example01.page.tsx +++ b/pages/example01.page.tsx @@ -1,3 +1,193 @@ +import { useState, useEffect } from "react" +import { CapacityNode3dDebugger } from "../lib/CapacityNode3dDebugger" +import type { CapacityMeshNode, SimpleRouteJson } from "../lib/types" + export default function Example01() { - return "hi" + const [nodes, setNodes] = useState([]) + const [simpleRouteJson, setSimpleRouteJson] = useState(undefined) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + // Load the example data from test-assets + fetch('/test-assets/example01.json') + .then(response => { + if (!response.ok) { + throw new Error(`Failed to load example data: ${response.status} ${response.statusText}`) + } + return response.json() + }) + .then(data => { + // The example data contains meshNodes directly + if (data.meshNodes && Array.isArray(data.meshNodes)) { + setNodes(data.meshNodes) + + // Create a simple route json with bounds based on the nodes + if (data.meshNodes.length > 0) { + const bounds = calculateBoundsFromNodes(data.meshNodes) + const mockSimpleRouteJson: SimpleRouteJson = { + layerCount: 4, // Based on availableZ values in the data + minTraceWidth: 0.1, + obstacles: [], + connections: [], + bounds: bounds + } + setSimpleRouteJson(mockSimpleRouteJson) + } + } else { + throw new Error('Invalid data format: meshNodes array not found') + } + setLoading(false) + }) + .catch(err => { + console.error('Error loading example data:', err) + setError(err.message) + setLoading(false) + }) + }, []) + + const calculateBoundsFromNodes = (nodes: CapacityMeshNode[]) => { + let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity + + nodes.forEach(node => { + const halfWidth = node.width / 2 + const halfHeight = node.height / 2 + + minX = Math.min(minX, node.center.x - halfWidth) + maxX = Math.max(maxX, node.center.x + halfWidth) + minY = Math.min(minY, node.center.y - halfHeight) + maxY = Math.max(maxY, node.center.y + halfHeight) + }) + + // Add some padding + const padding = 2 + return { + minX: minX - padding, + maxX: maxX + padding, + minY: minY - padding, + maxY: maxY + padding + } + } + + if (loading) { + return ( +
+ Loading example data... +
+ ) + } + + if (error) { + return ( +
+
+

Error Loading Data

+

{error}

+

Make sure the test-assets directory contains example01.json

+
+
+ ) + } + + return ( +
+
+

+ Capacity Node 3D Debugger - Example 01 +

+ +
+

Dataset Information

+

+ Total Nodes: {nodes.length} +

+

+ Layers: 4 (based on availableZ values) +

+

+ Bounds: + {simpleRouteJson ? + ` X: ${simpleRouteJson.bounds.minX.toFixed(2)} to ${simpleRouteJson.bounds.maxX.toFixed(2)}, ` + + `Y: ${simpleRouteJson.bounds.minY.toFixed(2)} to ${simpleRouteJson.bounds.maxY.toFixed(2)}` + : ' Calculating...' + } +

+
+ + + +
+

Usage Instructions

+
    +
  • Click "Show 3D" to render the 3D visualization
  • +
  • Use mouse to orbit, wheel to zoom, right-drag to pan
  • +
  • Toggle different layers and visualization options using the controls
  • +
  • Adjust opacity and box shrinking for better visualization
  • +
+
+
+
+ ) } \ No newline at end of file diff --git a/test-component.tsx b/test-component.tsx new file mode 100644 index 0000000..535c79a --- /dev/null +++ b/test-component.tsx @@ -0,0 +1,48 @@ + +import { CapacityNode3dDebugger } from './lib/CapacityNode3dDebugger' +import type { CapacityMeshNode } from './lib/types' + +// Simple test data +const testNodes: CapacityMeshNode[] = [ + { + capacityMeshNodeId: "test_node_1", + center: { x: 0, y: 0 }, + width: 10, + height: 10, + layer: "top", + availableZ: [0, 1, 2] + }, + { + capacityMeshNodeId: "test_node_2", + center: { x: 15, y: 0 }, + width: 8, + height: 8, + layer: "top", + availableZ: [0, 1] + }, + { + capacityMeshNodeId: "test_node_3", + center: { x: 0, y: 15 }, + width: 12, + height: 6, + layer: "inner1", + availableZ: [1, 2, 3] + } +] + +function TestComponent() { + return ( +
+

Capacity Node 3D Debugger Test

+ +
+ ) +} + +export default TestComponent \ No newline at end of file diff --git a/test.html b/test.html new file mode 100644 index 0000000..4dd1f02 --- /dev/null +++ b/test.html @@ -0,0 +1,87 @@ + + + + + + CapacityNode3dDebugger Test + + + +
+

Capacity Node 3D Debugger - Test Page

+ +
+

Component Status

+

Status: ✅ Component created successfully

+

Types: ✅ TypeScript compilation successful

+

Dependencies: ✅ Three.js and @types/three installed

+

Features: 3D visualization, interactive controls, layer management

+
+ +
+

Implementation Summary

+
    +
  • CapacityNode3dDebugger.tsx - Main 3D visualization component
  • +
  • types.ts - TypeScript type definitions
  • +
  • index.ts - Export barrel for clean imports
  • +
  • example01.page.tsx - Example page with data loading
  • +
  • README.md - Documentation and usage instructions
  • +
+
+ +
+

Next Steps

+
    +
  1. Start the development server: npm start
  2. +
  3. Navigate to the example page in your browser
  4. +
  5. Click "Show 3D" to render the visualization
  6. +
  7. Use mouse controls to interact with the 3D scene
  8. +
+
+ +
+

Usage Instructions

+
    +
  • Click "Show 3D" to render the 3D visualization
  • +
  • Use mouse to orbit, wheel to zoom, right-drag to pan
  • +
  • Toggle different layers and visualization options using the controls
  • +
  • Adjust opacity and box shrinking for better visualization
  • +
+
+
+ + \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index bfa0fea..3ec8bb3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { // Environment setup & latest features - "lib": ["ESNext"], - "target": "ESNext", + "lib": ["ESNext", "DOM"], + "target": "ES2015", "module": "Preserve", "moduleDetection": "force", "jsx": "react-jsx", @@ -24,6 +24,10 @@ // Some stricter flags (disabled by default) "noUnusedLocals": false, "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false + "noPropertyAccessFromIndexSignature": false, + + // Enable ES module interop and iteration + "esModuleInterop": true, + "downlevelIteration": true } } From a1d550e7b234fbd424ebb8a93405768f3a3709ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Tue, 18 Nov 2025 00:17:23 +0530 Subject: [PATCH 03/14] added scripts --- .gitignore | 2 + bun.lock | 583 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 17 +- 3 files changed, 596 insertions(+), 6 deletions(-) create mode 100644 bun.lock diff --git a/.gitignore b/.gitignore index dad331a..bcf622c 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Finder (MacOS) folder config .DS_Store package-lock.json +.vercel +cosmos-export \ No newline at end of file diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..8eddad1 --- /dev/null +++ b/bun.lock @@ -0,0 +1,583 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "trace-capacity-visualizer", + "dependencies": { + "tsup": "^8.5.1", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/three": "^0.170.0", + "react": "^19.2.0", + "react-cosmos": "^7.0.0", + "react-cosmos-plugin-vite": "^7.0.0", + "react-dom": "^19.2.0", + "three": "^0.171.0", + "vite": "^7.2.2", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.0", "", { "os": "android", "cpu": "arm" }, "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.0", "", { "os": "android", "cpu": "arm64" }, "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.0", "", { "os": "android", "cpu": "x64" }, "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, ""], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.0", "", { "os": "none", "cpu": "x64" }, "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg=="], + + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, ""], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, ""], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.53.2", "", { "os": "linux", "cpu": "x64" }, ""], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.53.2", "", { "os": "linux", "cpu": "x64" }, ""], + + "@skidding/launch-editor": ["@skidding/launch-editor@2.2.3", "", { "dependencies": { "chalk": "^2.3.0", "shell-quote": "^1.6.1" } }, ""], + + "@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="], + + "@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, ""], + + "@types/http-proxy": ["@types/http-proxy@1.17.17", "", { "dependencies": { "@types/node": "*" } }, ""], + + "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, ""], + + "@types/react": ["@types/react@19.2.5", "", { "dependencies": { "csstype": "^3.0.2" } }, ""], + + "@types/stats.js": ["@types/stats.js@0.17.4", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="], + + "@types/three": ["@types/three@0.170.0", "", { "dependencies": { "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": "*", "@webgpu/types": "*", "fflate": "~0.8.2", "meshoptimizer": "~0.18.1" } }, "sha512-CUm2uckq+zkCY7ZbFpviRttY+6f9fvwm6YqSqPfA5K22s9w7R4VnA3rzJse8kHVvuzLcTx+CjNCs2NYe0QFAyg=="], + + "@types/webxr": ["@types/webxr@0.5.24", "", {}, "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg=="], + + "@webgpu/types": ["@webgpu/types@0.1.66", "", {}, "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA=="], + + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, ""], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, ""], + + "ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, ""], + + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, ""], + + "array-flatten": ["array-flatten@1.1.1", "", {}, ""], + + "balanced-match": ["balanced-match@1.0.2", "", {}, ""], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, ""], + + "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, ""], + + "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, ""], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, ""], + + "bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, ""], + + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, ""], + + "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="], + + "bytes": ["bytes@3.1.2", "", {}, ""], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, ""], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, ""], + + "chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, ""], + + "charenc": ["charenc@0.0.2", "", {}, ""], + + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" } }, ""], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, ""], + + "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, ""], + + "color-name": ["color-name@1.1.3", "", {}, ""], + + "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + + "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + + "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, ""], + + "content-type": ["content-type@1.0.5", "", {}, ""], + + "cookie": ["cookie@0.7.1", "", {}, ""], + + "cookie-signature": ["cookie-signature@1.0.6", "", {}, ""], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, ""], + + "crypt": ["crypt@0.0.2", "", {}, ""], + + "csstype": ["csstype@3.2.3", "", {}, ""], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, ""], + + "default-browser-id": ["default-browser-id@5.0.1", "", {}, ""], + + "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, ""], + + "depd": ["depd@2.0.0", "", {}, ""], + + "destroy": ["destroy@1.2.0", "", {}, ""], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, ""], + + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, ""], + + "ee-first": ["ee-first@1.1.1", "", {}, ""], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, ""], + + "encodeurl": ["encodeurl@2.0.0", "", {}, ""], + + "entities": ["entities@6.0.1", "", {}, ""], + + "es-define-property": ["es-define-property@1.0.1", "", {}, ""], + + "es-errors": ["es-errors@1.3.0", "", {}, ""], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, ""], + + "es6-promisify": ["es6-promisify@7.0.0", "", {}, ""], + + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/linux-x64": "0.25.12" }, "bin": "bin/esbuild" }, ""], + + "escalade": ["escalade@3.2.0", "", {}, ""], + + "escape-html": ["escape-html@1.0.3", "", {}, ""], + + "escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, ""], + + "etag": ["etag@1.8.1", "", {}, ""], + + "eventemitter3": ["eventemitter3@4.0.7", "", {}, ""], + + "express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, ""], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, ""], + + "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, ""], + + "finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, ""], + + "fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="], + + "follow-redirects": ["follow-redirects@1.15.11", "", {}, ""], + + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, ""], + + "forwarded": ["forwarded@0.2.0", "", {}, ""], + + "fresh": ["fresh@0.5.2", "", {}, ""], + + "function-bind": ["function-bind@1.1.2", "", {}, ""], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, ""], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, ""], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, ""], + + "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, ""], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, ""], + + "gopd": ["gopd@1.2.0", "", {}, ""], + + "has-flag": ["has-flag@3.0.0", "", {}, ""], + + "has-symbols": ["has-symbols@1.1.0", "", {}, ""], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, ""], + + "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, ""], + + "http-proxy": ["http-proxy@1.18.1", "", { "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" } }, ""], + + "http-proxy-middleware": ["http-proxy-middleware@2.0.9", "", { "dependencies": { "@types/http-proxy": "^1.17.8", "http-proxy": "^1.18.1", "is-glob": "^4.0.1", "is-plain-obj": "^3.0.0", "micromatch": "^4.0.2" }, "peerDependencies": { "@types/express": "^4.17.13" }, "optionalPeers": ["@types/express"] }, ""], + + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, ""], + + "inherits": ["inherits@2.0.4", "", {}, ""], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, ""], + + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, ""], + + "is-buffer": ["is-buffer@1.1.6", "", {}, ""], + + "is-docker": ["is-docker@3.0.0", "", { "bin": "cli.js" }, ""], + + "is-extglob": ["is-extglob@2.1.1", "", {}, ""], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, ""], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, ""], + + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": "cli.js" }, ""], + + "is-number": ["is-number@7.0.0", "", {}, ""], + + "is-plain-obj": ["is-plain-obj@3.0.0", "", {}, ""], + + "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, ""], + + "isexe": ["isexe@2.0.0", "", {}, ""], + + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, ""], + + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + + "js-base64": ["js-base64@3.7.7", "", {}, ""], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="], + + "lodash-es": ["lodash-es@4.17.21", "", {}, ""], + + "lru-cache": ["lru-cache@10.4.3", "", {}, ""], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, ""], + + "md5": ["md5@2.3.0", "", { "dependencies": { "charenc": "0.0.2", "crypt": "0.0.2", "is-buffer": "~1.1.6" } }, ""], + + "media-typer": ["media-typer@0.3.0", "", {}, ""], + + "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, ""], + + "meshoptimizer": ["meshoptimizer@0.18.1", "", {}, "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw=="], + + "methods": ["methods@1.1.2", "", {}, ""], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, ""], + + "mime": ["mime@1.6.0", "", { "bin": "cli.js" }, ""], + + "mime-db": ["mime-db@1.52.0", "", {}, ""], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, ""], + + "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, ""], + + "minipass": ["minipass@7.1.2", "", {}, ""], + + "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], + + "ms": ["ms@2.1.3", "", {}, ""], + + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, ""], + + "negotiator": ["negotiator@0.6.3", "", {}, ""], + + "normalize-path": ["normalize-path@3.0.0", "", {}, ""], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, ""], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, ""], + + "open": ["open@10.1.1", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "is-wsl": "^3.1.0" } }, ""], + + "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, ""], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, ""], + + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, ""], + + "parseurl": ["parseurl@1.3.3", "", {}, ""], + + "path-key": ["path-key@3.1.1", "", {}, ""], + + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, ""], + + "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, ""], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pem": ["pem@1.14.8", "", { "dependencies": { "es6-promisify": "^7.0.0", "md5": "^2.3.0", "os-tmpdir": "^1.0.2", "which": "^2.0.2" } }, ""], + + "picocolors": ["picocolors@1.1.1", "", {}, ""], + + "picomatch": ["picomatch@4.0.3", "", {}, ""], + + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, ""], + + "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, ""], + + "qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, ""], + + "range-parser": ["range-parser@1.2.1", "", {}, ""], + + "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, ""], + + "react": ["react@19.2.0", "", {}, ""], + + "react-cosmos": ["react-cosmos@7.0.0", "", { "dependencies": { "@skidding/launch-editor": "2.2.3", "chokidar": "3.6.0", "express": "4.21.2", "glob": "10.4.5", "http-proxy-middleware": "2.0.9", "lodash-es": "4.17.21", "micromatch": "4.0.8", "open": "10.1.1", "pem": "1.14.8", "react-cosmos-core": "^7.0.0", "react-cosmos-renderer": "^7.0.0", "react-cosmos-ui": "^7.0.0", "ws": "8.18.1", "yargs": "17.7.2" }, "bin": { "cosmos": "bin/cosmos.js", "cosmos-export": "bin/cosmos-export.js", "cosmos-native": "bin/cosmos-native.js" } }, ""], + + "react-cosmos-core": ["react-cosmos-core@7.0.0", "", { "dependencies": { "js-base64": "3.7.7", "lodash-es": "4.17.21" } }, ""], + + "react-cosmos-dom": ["react-cosmos-dom@7.0.0", "", { "dependencies": { "lodash-es": "4.17.21", "react-cosmos-core": "^7.0.0", "react-cosmos-renderer": "^7.0.0" } }, ""], + + "react-cosmos-plugin-vite": ["react-cosmos-plugin-vite@7.0.0", "", { "dependencies": { "parse5": "7.3.0", "react-cosmos-core": "^7.0.0", "react-cosmos-dom": "^7.0.0" }, "peerDependencies": { "vite": "*" } }, ""], + + "react-cosmos-renderer": ["react-cosmos-renderer@7.0.0", "", { "dependencies": { "lodash-es": "4.17.21", "react-cosmos-core": "^7.0.0" } }, ""], + + "react-cosmos-ui": ["react-cosmos-ui@7.0.0", "", { "dependencies": { "lodash-es": "4.17.21", "react-cosmos-core": "^7.0.0" } }, ""], + + "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, ""], + + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, ""], + + "require-directory": ["require-directory@2.1.1", "", {}, ""], + + "requires-port": ["requires-port@1.0.0", "", {}, ""], + + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "rollup": ["rollup@4.53.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "4.53.2", "@rollup/rollup-linux-x64-musl": "4.53.2" }, "bin": "dist/bin/rollup" }, ""], + + "run-applescript": ["run-applescript@7.1.0", "", {}, ""], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, ""], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, ""], + + "scheduler": ["scheduler@0.27.0", "", {}, ""], + + "send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, ""], + + "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, ""], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, ""], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, ""], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, ""], + + "shell-quote": ["shell-quote@1.8.3", "", {}, ""], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, ""], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, ""], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, ""], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, ""], + + "signal-exit": ["signal-exit@4.1.0", "", {}, ""], + + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, ""], + + "statuses": ["statuses@2.0.1", "", {}, ""], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, ""], + + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, ""], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, ""], + + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, ""], + + "sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="], + + "supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, ""], + + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + + "three": ["three@0.171.0", "", {}, "sha512-Y/lAXPaKZPcEdkKjh0JOAHVv8OOnv/NDJqm0wjfCzyQmfKxV7zvkwsnBgPBKTzJHToSOhRGQAGbPJObT59B/PQ=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, ""], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, ""], + + "toidentifier": ["toidentifier@1.0.1", "", {}, ""], + + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + + "tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="], + + "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, ""], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, ""], + + "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], + + "undici-types": ["undici-types@7.16.0", "", {}, ""], + + "unpipe": ["unpipe@1.0.0", "", {}, ""], + + "utils-merge": ["utils-merge@1.0.1", "", {}, ""], + + "vary": ["vary@1.1.2", "", {}, ""], + + "vite": ["vite@7.2.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": "bin/vite.js" }, ""], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, ""], + + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, ""], + + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, ""], + + "ws": ["ws@8.18.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, ""], + + "y18n": ["y18n@5.0.8", "", {}, ""], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, ""], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, ""], + + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, ""], + + "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, ""], + + "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, ""], + + "anymatch/picomatch": ["picomatch@2.3.1", "", {}, ""], + + "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, ""], + + "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, ""], + + "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, ""], + + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, ""], + + "readdirp/picomatch": ["picomatch@2.3.1", "", {}, ""], + + "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, ""], + + "send/encodeurl": ["encodeurl@1.0.2", "", {}, ""], + + "tsup/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "tsup/esbuild": ["esbuild@0.27.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.0", "@esbuild/android-arm": "0.27.0", "@esbuild/android-arm64": "0.27.0", "@esbuild/android-x64": "0.27.0", "@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-x64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-x64": "0.27.0", "@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-x64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-x64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-x64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.0", "@esbuild/sunos-x64": "0.27.0", "@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-x64": "0.27.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA=="], + + "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, ""], + + "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, ""], + + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, ""], + + "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, ""], + + "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, ""], + + "body-parser/debug/ms": ["ms@2.0.0", "", {}, ""], + + "express/debug/ms": ["ms@2.0.0", "", {}, ""], + + "finalhandler/debug/ms": ["ms@2.0.0", "", {}, ""], + + "send/debug/ms": ["ms@2.0.0", "", {}, ""], + + "tsup/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "tsup/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.0", "", { "os": "linux", "cpu": "x64" }, "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw=="], + + "wrap-ansi-cjs/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, ""], + + "wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, ""], + + "wrap-ansi-cjs/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, ""], + + "wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, ""], + } +} diff --git a/package.json b/package.json index 1bb0f83..37bb730 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,13 @@ { - "name": "trace-capacity-visualizer", - "private": true, - "scripts":{ - "start": "cosmos" - }, + "name": "@tscircuit/trace-capacity-visualizer", + "version": "0.0.1", + "type": "module", + "main": "dist/index.js", + "scripts": { + "start": "cosmos", + "build:site": "cosmos-export", + "build": "tsup-node lib/index.ts --format esm --dts" + }, "devDependencies": { "@types/bun": "latest", "@types/three": "^0.170.0", @@ -12,7 +16,8 @@ "react-cosmos-plugin-vite": "^7.0.0", "react-dom": "^19.2.0", "three": "^0.171.0", - "vite": "^7.2.2" + "vite": "^7.2.2", + "tsup": "^8.5.1" }, "peerDependencies": { "typescript": "^5" From e877d32aea2cc7217a661e8ab401ae2480ed4918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Tue, 18 Nov 2025 00:19:21 +0530 Subject: [PATCH 04/14] added workflows --- .github/workflows/bun-formatcheck.yml | 26 ++++++++++ .github/workflows/bun-pver-release.yml | 71 ++++++++++++++++++++++++++ .github/workflows/bun-typecheck.yml | 26 ++++++++++ 3 files changed, 123 insertions(+) create mode 100644 .github/workflows/bun-formatcheck.yml create mode 100644 .github/workflows/bun-pver-release.yml create mode 100644 .github/workflows/bun-typecheck.yml diff --git a/.github/workflows/bun-formatcheck.yml b/.github/workflows/bun-formatcheck.yml new file mode 100644 index 0000000..be449cf --- /dev/null +++ b/.github/workflows/bun-formatcheck.yml @@ -0,0 +1,26 @@ +# Created using @tscircuit/plop (npm install -g @tscircuit/plop) +name: Format Check + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + format-check: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Run format check + run: bun run format:check diff --git a/.github/workflows/bun-pver-release.yml b/.github/workflows/bun-pver-release.yml new file mode 100644 index 0000000..11105c4 --- /dev/null +++ b/.github/workflows/bun-pver-release.yml @@ -0,0 +1,71 @@ +# Created using @tscircuit/plop (npm install -g @tscircuit/plop) +name: Publish to npm +on: + push: + branches: + - main + workflow_dispatch: + +env: + UPSTREAM_REPOS: "" # comma-separated list, e.g. "eval,tscircuit,docs" + UPSTREAM_PACKAGES_TO_UPDATE: "" # comma-separated list, e.g. "@tscircuit/core,@tscircuit/protos" + +jobs: + publish: + runs-on: ubuntu-latest + if: ${{ !(github.event_name == 'push' && startsWith(github.event.head_commit.message, 'v')) }} + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN }} + - name: Setup bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - uses: actions/setup-node@v3 + with: + node-version: 20 + registry-url: https://registry.npmjs.org/ + - run: npm install -g pver + - run: bun install --frozen-lockfile + - run: bun run build + - run: pver release --no-push-main + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + GITHUB_TOKEN: ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN }} + + - name: Create Pull Request + id: create-pr + uses: peter-evans/create-pull-request@v5 + with: + commit-message: "chore: bump version" + title: "chore: bump version" + body: "Automated package update" + branch: bump-version-${{ github.run_number }} + base: main + token: ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN }} + committer: tscircuitbot + author: tscircuitbot + + - name: Enable auto-merge + if: steps.create-pr.outputs.pull-request-number != '' + run: | + gh pr merge ${{ steps.create-pr.outputs.pull-request-number }} --auto --squash --delete-branch + env: + GH_TOKEN: ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN }} + + # - name: Trigger upstream repo updates + # if: env.UPSTREAM_REPOS && env.UPSTREAM_PACKAGES_TO_UPDATE + # run: | + # IFS=',' read -ra REPOS <<< "${{ env.UPSTREAM_REPOS }}" + # for repo in "${REPOS[@]}"; do + # if [[ -n "$repo" ]]; then + # echo "Triggering update for repo: $repo" + # curl -X POST \ + # -H "Accept: application/vnd.github.v3+json" \ + # -H "Authorization: token ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN }}" \ + # -H "Content-Type: application/json" \ + # "https://api.github.com/repos/tscircuit/$repo/actions/workflows/update-package.yml/dispatches" \ + # -d "{\"ref\":\"main\",\"inputs\":{\"package_names\":\"${{ env.UPSTREAM_PACKAGES_TO_UPDATE }}\"}}" + # fi + # done \ No newline at end of file diff --git a/.github/workflows/bun-typecheck.yml b/.github/workflows/bun-typecheck.yml new file mode 100644 index 0000000..68acb7d --- /dev/null +++ b/.github/workflows/bun-typecheck.yml @@ -0,0 +1,26 @@ +# Created using @tscircuit/plop (npm install -g @tscircuit/plop) +name: Type Check + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + type-check: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun i + + - name: Run type check + run: bunx tsc --noEmit From 1238c6da5ffff6c7afe2430aa95f310d78e923c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Tue, 18 Nov 2025 08:56:03 +0530 Subject: [PATCH 05/14] feat: simplify CapacityNode3dDebugger UI - Always show 3D visualization by default - Remove Hide 3D/Show 3D toggle button - Remove Rebuild 3D button - Remove Root and Output checkboxes (always enabled) - Remove Dataset Information and Usage Instructions sections - Keep Obstacles toggle and advanced controls (opacity, wireframe, etc.) - Clean up component props and state management --- lib/CapacityNode3dDebugger.tsx | 71 ++-------------------------------- pages/example01.page.tsx | 42 +------------------- 2 files changed, 5 insertions(+), 108 deletions(-) diff --git a/lib/CapacityNode3dDebugger.tsx b/lib/CapacityNode3dDebugger.tsx index 0176249..92b3a53 100644 --- a/lib/CapacityNode3dDebugger.tsx +++ b/lib/CapacityNode3dDebugger.tsx @@ -8,9 +8,7 @@ type CapacityNode3dDebuggerProps = { simpleRouteJson?: SimpleRouteJson layerThickness?: number height?: number - defaultShowRoot?: boolean defaultShowObstacles?: boolean - defaultShowOutput?: boolean defaultWireframeOutput?: boolean style?: React.CSSProperties } @@ -513,9 +511,7 @@ const ThreeBoardView: React.FC<{ layerCount, layerThickness, height, - showRoot, showObstacles, - showOutput, wireframeOutput, zIndexByLayerName, meshOpacity, @@ -546,18 +542,14 @@ export const CapacityNode3dDebugger: React.FC = ({ simpleRouteJson, layerThickness = 1, height = 600, - defaultShowRoot = true, defaultShowObstacles = false, // don't show obstacles by default - defaultShowOutput = true, defaultWireframeOutput = false, style, }) => { - const [show3d, setShow3d] = useState(false) + const [show3d, setShow3d] = useState(true) const [rebuildKey, setRebuildKey] = useState(0) - const [showRoot, setShowRoot] = useState(defaultShowRoot) const [showObstacles, setShowObstacles] = useState(defaultShowObstacles) - const [showOutput, setShowOutput] = useState(defaultShowOutput) const [wireframeOutput, setWireframeOutput] = useState(defaultWireframeOutput) const [meshOpacity, setMeshOpacity] = useState(0.6) @@ -565,7 +557,6 @@ export const CapacityNode3dDebugger: React.FC = ({ const [boxShrinkAmount, setBoxShrinkAmount] = useState(0.1) const [showBorders, setShowBorders] = useState(true) - const toggle3d = useCallback(() => setShow3d((s) => !s), []) const rebuild = useCallback(() => setRebuildKey((k) => k + 1), []) return ( @@ -579,52 +570,6 @@ export const CapacityNode3dDebugger: React.FC = ({ flexWrap: "wrap", }} > - - {show3d && ( - - )} - - {/* experiment-like toggles */} - -
) From a60ca80d97cc13b8e30b500aa22c2951944ac952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Tue, 18 Nov 2025 09:46:32 +0530 Subject: [PATCH 06/14] feat: Implement orthographic view in 3D debugger Adds a toggle for orthographic camera view in the CapacityNode3dDebugger component. - Introduces an 'isOrthographic' state and a corresponding checkbox. - Modifies ThreeBoardView to dynamically switch between PerspectiveCamera and OrthographicCamera. - Adjusts camera positioning and resize handling for the orthographic view, providing a top-down initial perspective. --- lib/CapacityNode3dDebugger.tsx | 148 ++++++++++++++++++++++----------- 1 file changed, 98 insertions(+), 50 deletions(-) diff --git a/lib/CapacityNode3dDebugger.tsx b/lib/CapacityNode3dDebugger.tsx index 92b3a53..5f729f5 100644 --- a/lib/CapacityNode3dDebugger.tsx +++ b/lib/CapacityNode3dDebugger.tsx @@ -158,6 +158,7 @@ const ThreeBoardView: React.FC<{ shrinkBoxes: boolean boxShrinkAmount: number showBorders: boolean + isOrthographic: boolean }> = ({ nodes, srj, @@ -171,6 +172,7 @@ const ThreeBoardView: React.FC<{ shrinkBoxes, boxShrinkAmount, showBorders, + isOrthographic, }) => { const containerRef = useRef(null) const destroyRef = useRef<() => void>(() => {}) @@ -224,8 +226,59 @@ const ThreeBoardView: React.FC<{ const scene = new THREE.Scene() scene.background = new THREE.Color(0xf7f8fa) - const camera = new THREE.PerspectiveCamera(45, w / h, 0.1, 10000) - camera.position.set(80, 80, 120) + const fitBox = srj + ? { + minX: srj.bounds.minX, + maxX: srj.bounds.maxX, + minY: srj.bounds.minY, + maxY: srj.bounds.maxY, + z0: 0, + z1: layerCount, + } + : (() => { + if (prisms.length === 0) { + return { + minX: -10, + maxX: 10, + minY: -10, + maxY: 10, + z0: 0, + z1: layerCount, + } + } + let minX = Infinity, + minY = Infinity, + maxX = -Infinity, + maxY = -Infinity + for (const p of prisms) { + minX = Math.min(minX, p.minX) + maxX = Math.max(maxX, p.maxX) + minY = Math.min(minY, p.minY) + maxY = Math.max(maxY, p.maxY) + } + return { minX, maxX, minY, maxY, z0: 0, z1: layerCount } + })() + + const dx = fitBox.maxX - fitBox.minX + const dz = fitBox.maxY - fitBox.minY + const dy = (fitBox.z1 - fitBox.z0) * layerThickness + const size = Math.max(dx, dz, dy) + + let camera: THREE.PerspectiveCamera | THREE.OrthographicCamera + if (isOrthographic) { + const aspect = w / h + const frustumSize = size * 1.2 + camera = new THREE.OrthographicCamera( + (frustumSize * aspect) / -2, + (frustumSize * aspect) / 2, + frustumSize / 2, + frustumSize / -2, + 0.1, + size * 20, + ) + } else { + camera = new THREE.PerspectiveCamera(45, w / h, 0.1, 10000) + } const controls = new OrbitControls(camera, renderer.domElement) controls.enableDamping = true @@ -422,64 +475,46 @@ const ThreeBoardView: React.FC<{ } // Fit camera - const fitBox = srj - ? { - minX: srj.bounds.minX, - maxX: srj.bounds.maxX, - minY: srj.bounds.minY, - maxY: srj.bounds.maxY, - z0: 0, - z1: layerCount, - } - : (() => { - if (prisms.length === 0) { - return { - minX: -10, - maxX: 10, - minY: -10, - maxY: 10, - z0: 0, - z1: layerCount, - } - } - let minX = Infinity, - minY = Infinity, - maxX = -Infinity, - maxY = -Infinity - for (const p of prisms) { - minX = Math.min(minX, p.minX) - maxX = Math.max(maxX, p.maxX) - minY = Math.min(minY, p.minY) - maxY = Math.max(maxY, p.maxY) - } - return { minX, maxX, minY, maxY, z0: 0, z1: layerCount } - })() + const target = new THREE.Vector3( + -((fitBox.minX + fitBox.maxX) / 2), // negate X to account for flipped axis + -dy / 2, // center of the inverted Y range + (fitBox.minY + fitBox.maxY) / 2, + ) + controls.target.copy(target) - const dx = fitBox.maxX - fitBox.minX - const dz = fitBox.maxY - fitBox.minY - const dy = (fitBox.z1 - fitBox.z0) * layerThickness - const size = Math.max(dx, dz, dy) const dist = size * 2.0 - // Camera looks from above-right-front, with negative Y being "up" (z=0 at top) - camera.position.set( - -(fitBox.maxX + dist * 0.6), // negate X to account for flipped axis - -dy / 2 + dist, // negative Y is up, so position above the center - fitBox.maxY + dist * 0.6, - ) + if (isOrthographic) { + // Top-down view + camera.position.copy(target).add(new THREE.Vector3(0, dist, 0)) + camera.lookAt(target) + } else { + // Camera looks from above-right-front, with negative Y being "up" (z=0 at top) + camera.position.set( + -(fitBox.maxX + dist * 0.6), // negate X to account for flipped axis + -dy / 2 + dist, // negative Y is up, so position above the center + fitBox.maxY + dist * 0.6, + ) + } + camera.near = Math.max(0.1, size / 100) camera.far = dist * 10 + size * 10 camera.updateProjectionMatrix() - controls.target.set( - -((fitBox.minX + fitBox.maxX) / 2), // negate X to account for flipped axis - -dy / 2, // center of the inverted Y range - (fitBox.minY + fitBox.maxY) / 2, - ) controls.update() const onResize = () => { const W = el.clientWidth || w const H = el.clientHeight || h - camera.aspect = W / H + if (isOrthographic) { + const aspect = W / H + const frustumSize = size * 1.2 + const ocam = camera as THREE.OrthographicCamera + ocam.left = (frustumSize * aspect) / -2 + ocam.right = (frustumSize * aspect) / 2 + ocam.top = frustumSize / 2 + ocam.bottom = frustumSize / -2 + } else { + ;(camera as THREE.PerspectiveCamera).aspect = W / H + } camera.updateProjectionMatrix() renderer.setSize(W, H) } @@ -518,6 +553,7 @@ const ThreeBoardView: React.FC<{ shrinkBoxes, boxShrinkAmount, showBorders, + isOrthographic, ]) return ( @@ -551,6 +587,7 @@ export const CapacityNode3dDebugger: React.FC = ({ const [showObstacles, setShowObstacles] = useState(defaultShowObstacles) const [wireframeOutput, setWireframeOutput] = useState(defaultWireframeOutput) + const [isOrthographic, setIsOrthographic] = useState(false) const [meshOpacity, setMeshOpacity] = useState(0.6) const [shrinkBoxes, setShrinkBoxes] = useState(true) @@ -590,6 +627,16 @@ export const CapacityNode3dDebugger: React.FC = ({ /> Wireframe Output + {/* Mesh opacity slider */} {show3d && ( @@ -707,6 +754,7 @@ export const CapacityNode3dDebugger: React.FC = ({ shrinkBoxes={shrinkBoxes} boxShrinkAmount={boxShrinkAmount} showBorders={showBorders} + isOrthographic={isOrthographic} /> )}
From 3705acd10f5675fe93ea65aa48f417f2edf0fb4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Tue, 18 Nov 2025 11:02:44 +0530 Subject: [PATCH 07/14] impliment hiding rects --- lib/CapacityNode3dDebugger.tsx | 273 +++++++++++++++++++++++++++------ 1 file changed, 229 insertions(+), 44 deletions(-) diff --git a/lib/CapacityNode3dDebugger.tsx b/lib/CapacityNode3dDebugger.tsx index 5f729f5..c5d4cda 100644 --- a/lib/CapacityNode3dDebugger.tsx +++ b/lib/CapacityNode3dDebugger.tsx @@ -1,4 +1,10 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react" import type { CapacityMeshNode, SimpleRouteJson } from "./types" import * as THREE from "three" import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js" @@ -10,6 +16,8 @@ type CapacityNode3dDebuggerProps = { height?: number defaultShowObstacles?: boolean defaultWireframeOutput?: boolean + defaultShowRoot?: boolean + defaultShowOutput?: boolean style?: React.CSSProperties } @@ -60,12 +68,17 @@ function buildPrismsFromNodes( maxY: number z0: number z1: number + nodes: CapacityMeshNode[] }> { const xyKey = (n: CapacityMeshNode) => - `${n.center.x.toFixed(8)}|${n.center.y.toFixed(8)}|${n.width.toFixed(8)}|${n.height.toFixed(8)}` + `${n.center.x.toFixed(8)}|${n.center.y.toFixed(8)}|${n.width.toFixed( + 8, + )}|${n.height.toFixed(8)}` const azKey = (n: CapacityMeshNode) => { const zs = ( - n.availableZ && n.availableZ.length ? Array.from(new Set(n.availableZ)) : [0] + n.availableZ && n.availableZ.length + ? Array.from(new Set(n.availableZ)) + : [0] ).sort((a, b) => a - b) return `zset:${zs.join(",")}` } @@ -73,20 +86,30 @@ function buildPrismsFromNodes( const groups = new Map< string, - { cx: number; cy: number; w: number; h: number; zs: number[] } + { + cx: number + cy: number + w: number + h: number + zs: number[] + nodes: CapacityMeshNode[] + } >() for (const n of nodes) { const k = key(n) const zlist = n.availableZ?.length ? n.availableZ : [0] const g = groups.get(k) - if (g) g.zs.push(...zlist) - else + if (g) { + g.zs.push(...zlist) + g.nodes.push(n) + } else groups.set(k, { cx: n.center.x, cy: n.center.y, w: n.width, h: n.height, zs: [...zlist], + nodes: [n], }) } @@ -97,6 +120,7 @@ function buildPrismsFromNodes( maxY: number z0: number z1: number + nodes: CapacityMeshNode[] }> = [] for (const g of Array.from(groups.values())) { const minX = g.cx - g.w / 2 @@ -112,6 +136,7 @@ function buildPrismsFromNodes( maxY, z0: 0, z1: Math.max(1, fallbackLayerCount), + nodes: g.nodes, }) } else { for (const r of runs) { @@ -122,6 +147,7 @@ function buildPrismsFromNodes( maxY, z0: r[0]!, z1: r[r.length - 1]! + 1, + nodes: g.nodes, }) } } @@ -159,6 +185,7 @@ const ThreeBoardView: React.FC<{ boxShrinkAmount: number showBorders: boolean isOrthographic: boolean + onDeleteNodes: (nodes: CapacityMeshNode[]) => void }> = ({ nodes, srj, @@ -173,9 +200,71 @@ const ThreeBoardView: React.FC<{ boxShrinkAmount, showBorders, isOrthographic, + onDeleteNodes, }) => { const containerRef = useRef(null) const destroyRef = useRef<() => void>(() => {}) + const sceneRef = useRef(null) + const cameraRef = + useRef(null) + const outputGroupRef = useRef(null) + const controlsStateRef = + useRef<{ position: THREE.Vector3; target: THREE.Vector3; zoom: number } | null>( + null, + ) + + const [contextMenu, setContextMenu] = useState<{ + x: number + y: number + nodes: CapacityMeshNode[] + } | null>(null) + + const handleContextMenu = (event: React.MouseEvent) => { + event.preventDefault() + event.stopPropagation() + setContextMenu(null) // Close any existing menu + + const scene = sceneRef.current + const camera = cameraRef.current + const outputGroup = outputGroupRef.current + const el = containerRef.current + if (!el || !scene || !camera || !outputGroup) return + + const rect = el.getBoundingClientRect() + const pointer = new THREE.Vector2() + pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1 + pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1 + + const raycaster = new THREE.Raycaster() + raycaster.setFromCamera(pointer, camera) + const intersects = raycaster.intersectObjects(outputGroup.children, true) + + if (intersects.length > 0 && intersects[0]) { + let intersectedObject: THREE.Object3D | null = intersects[0].object + while (intersectedObject && !intersectedObject.userData.nodes) { + intersectedObject = intersectedObject.parent + } + + if (intersectedObject && intersectedObject.userData.nodes) { + setContextMenu({ + x: event.clientX, + y: event.clientY, + nodes: intersectedObject.userData.nodes, + }) + } + } + } + + const handleDelete = (nodesToDelete: CapacityMeshNode[]) => { + onDeleteNodes(nodesToDelete) + setContextMenu(null) + } + + useEffect(() => { + const handleClick = () => setContextMenu(null) + window.addEventListener("click", handleClick) + return () => window.removeEventListener("click", handleClick) + }, []) const layerNames = useMemo(() => { // Build from nodes (preferred, matches solver) and fall back to SRJ obstacle names @@ -224,6 +313,7 @@ const ThreeBoardView: React.FC<{ el.appendChild(renderer.domElement) const scene = new THREE.Scene() + sceneRef.current = scene scene.background = new THREE.Color(0xf7f8fa) const fitBox = srj @@ -279,6 +369,7 @@ const ThreeBoardView: React.FC<{ } else { camera = new THREE.PerspectiveCamera(45, w / h, 0.1, 10000) } + cameraRef.current = camera const controls = new OrbitControls(camera, renderer.domElement) controls.enableDamping = true @@ -292,6 +383,7 @@ const ThreeBoardView: React.FC<{ const rootGroup = new THREE.Group() const obstaclesGroup = new THREE.Group() const outputGroup = new THREE.Group() + outputGroupRef.current = outputGroup scene.add(rootGroup, obstaclesGroup, outputGroup) // Axes helper for orientation (similar to experiment) @@ -336,6 +428,7 @@ const ThreeBoardView: React.FC<{ }, color: number, wire: boolean, + nodes: CapacityMeshNode[], opacity = 0.45, borders = false, ) { @@ -355,6 +448,7 @@ const ThreeBoardView: React.FC<{ new THREE.LineBasicMaterial({ color }), ) line.position.set(cx, cy, cz) + line.userData = { nodes } return line } const clampedOpacity = clamp01(opacity) @@ -368,6 +462,7 @@ const ThreeBoardView: React.FC<{ const mesh = new THREE.Mesh(geom, mat) mesh.position.set(cx, cy, cz) + mesh.userData = { nodes } if (!borders) return mesh @@ -382,6 +477,7 @@ const ThreeBoardView: React.FC<{ const group = new THREE.Group() group.add(mesh) group.add(line) + group.userData = { nodes } return group } @@ -395,7 +491,7 @@ const ThreeBoardView: React.FC<{ z0: 0, z1: layerCount, } - rootGroup.add(makeBoxMesh(rootBox, colorRoot, true, 1)) + rootGroup.add(makeBoxMesh(rootBox, colorRoot, true, [])) } // Obstacles — rectangular only — one slab per declared layer @@ -422,6 +518,7 @@ const ThreeBoardView: React.FC<{ { minX, maxX, minY, maxY, z0: z, z1: z + 1 }, colorOb, false, + [], 0.35, false, ), @@ -467,6 +564,7 @@ const ThreeBoardView: React.FC<{ box, color, wireframeOutput, + p.nodes, meshOpacity, showBorders && !wireframeOutput, ), @@ -474,32 +572,43 @@ const ThreeBoardView: React.FC<{ } } - // Fit camera - const target = new THREE.Vector3( - -((fitBox.minX + fitBox.maxX) / 2), // negate X to account for flipped axis - -dy / 2, // center of the inverted Y range - (fitBox.minY + fitBox.maxY) / 2, - ) - controls.target.copy(target) - - const dist = size * 2.0 - if (isOrthographic) { - // Top-down view - camera.position.copy(target).add(new THREE.Vector3(0, dist, 0)) - camera.lookAt(target) + if (controlsStateRef.current) { + camera.position.copy(controlsStateRef.current.position) + controls.target.copy(controlsStateRef.current.target) + if (isOrthographic) { + ;(camera as THREE.OrthographicCamera).zoom = + controlsStateRef.current.zoom + } + camera.updateProjectionMatrix() + controls.update() } else { - // Camera looks from above-right-front, with negative Y being "up" (z=0 at top) - camera.position.set( - -(fitBox.maxX + dist * 0.6), // negate X to account for flipped axis - -dy / 2 + dist, // negative Y is up, so position above the center - fitBox.maxY + dist * 0.6, + // Fit camera + const target = new THREE.Vector3( + -((fitBox.minX + fitBox.maxX) / 2), // negate X to account for flipped axis + -dy / 2, // center of the inverted Y range + (fitBox.minY + fitBox.maxY) / 2, ) - } + controls.target.copy(target) - camera.near = Math.max(0.1, size / 100) - camera.far = dist * 10 + size * 10 - camera.updateProjectionMatrix() - controls.update() + const dist = size * 2.0 + if (isOrthographic) { + // Top-down view + camera.position.copy(target).add(new THREE.Vector3(0, dist, 0)) + camera.lookAt(target) + } else { + // Camera looks from above-right-front, with negative Y being "up" (z=0 at top) + camera.position.set( + -(fitBox.maxX + dist * 0.6), // negate X to account for flipped axis + -dy / 2 + dist, // negative Y is up, so position above the center + fitBox.maxY + dist * 0.6, + ) + } + + camera.near = Math.max(0.1, size / 100) + camera.far = dist * 10 + size * 10 + camera.updateProjectionMatrix() + controls.update() + } const onResize = () => { const W = el.clientWidth || w @@ -529,10 +638,18 @@ const ThreeBoardView: React.FC<{ animate() destroyRef.current = () => { + controlsStateRef.current = { + position: camera.position.clone(), + target: controls.target.clone(), + zoom: (camera as any).zoom ?? 1, + } cancelAnimationFrame(raf) window.removeEventListener("resize", onResize) renderer.dispose() el.innerHTML = "" + sceneRef.current = null + cameraRef.current = null + outputGroupRef.current = null } })() @@ -557,31 +674,71 @@ const ThreeBoardView: React.FC<{ ]) return ( -
+ <> + {contextMenu && ( +
e.stopPropagation()} // Prevent closing when clicking inside + > + +
+ )} +
+ ) } /* ----------------------- Public wrapper component ----------------------- */ export const CapacityNode3dDebugger: React.FC = ({ - nodes, + nodes: initialNodes, simpleRouteJson, layerThickness = 1, height = 600, defaultShowObstacles = false, // don't show obstacles by default defaultWireframeOutput = false, + defaultShowRoot = false, + defaultShowOutput = true, style, }) => { + const [nodes, setNodes] = useState(initialNodes) + useEffect(() => { + setNodes(initialNodes) + }, [initialNodes]) + const [show3d, setShow3d] = useState(true) const [rebuildKey, setRebuildKey] = useState(0) @@ -596,6 +753,17 @@ export const CapacityNode3dDebugger: React.FC = ({ const rebuild = useCallback(() => setRebuildKey((k) => k + 1), []) + const handleDeleteNodes = useCallback((nodesToDelete: CapacityMeshNode[]) => { + const nodesToDeleteSet = new Set(nodesToDelete) + setNodes((currentNodes) => + currentNodes.filter((n) => !nodesToDeleteSet.has(n)), + ) + }, []) + + const handleReset = () => { + setNodes(initialNodes) + } + return ( <>
@@ -734,6 +902,22 @@ export const CapacityNode3dDebugger: React.FC = ({ )} + {nodes.length !== initialNodes.length && ( + + )} +
Drag to orbit · Wheel to zoom · Right-drag to pan
@@ -746,15 +930,16 @@ export const CapacityNode3dDebugger: React.FC = ({ srj={simpleRouteJson} layerThickness={layerThickness} height={height} - showRoot={true} + showRoot={defaultShowRoot} showObstacles={showObstacles} - showOutput={true} + showOutput={defaultShowOutput} wireframeOutput={wireframeOutput} meshOpacity={meshOpacity} shrinkBoxes={shrinkBoxes} boxShrinkAmount={boxShrinkAmount} showBorders={showBorders} isOrthographic={isOrthographic} + onDeleteNodes={handleDeleteNodes} /> )}
From 754e3d8678db181bae9bdce28c953b53ca7ea7ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Tue, 18 Nov 2025 11:29:46 +0530 Subject: [PATCH 08/14] WIP --- pages/example01.page.tsx | 163 ++++++++++++--------------------------- 1 file changed, 49 insertions(+), 114 deletions(-) diff --git a/pages/example01.page.tsx b/pages/example01.page.tsx index d630677..db0f2c2 100644 --- a/pages/example01.page.tsx +++ b/pages/example01.page.tsx @@ -1,137 +1,74 @@ -import { useState, useEffect } from "react" import { CapacityNode3dDebugger } from "../lib/CapacityNode3dDebugger" import type { CapacityMeshNode, SimpleRouteJson } from "../lib/types" -export default function Example01() { - const [nodes, setNodes] = useState([]) - const [simpleRouteJson, setSimpleRouteJson] = useState(undefined) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - // Load the example data from test-assets - fetch('/test-assets/example01.json') - .then(response => { - if (!response.ok) { - throw new Error(`Failed to load example data: ${response.status} ${response.statusText}`) - } - return response.json() - }) - .then(data => { - // The example data contains meshNodes directly - if (data.meshNodes && Array.isArray(data.meshNodes)) { - setNodes(data.meshNodes) - - // Create a simple route json with bounds based on the nodes - if (data.meshNodes.length > 0) { - const bounds = calculateBoundsFromNodes(data.meshNodes) - const mockSimpleRouteJson: SimpleRouteJson = { - layerCount: 4, // Based on availableZ values in the data - minTraceWidth: 0.1, - obstacles: [], - connections: [], - bounds: bounds - } - setSimpleRouteJson(mockSimpleRouteJson) - } - } else { - throw new Error('Invalid data format: meshNodes array not found') - } - setLoading(false) - }) - .catch(err => { - console.error('Error loading example data:', err) - setError(err.message) - setLoading(false) - }) - }, []) +import example01 from "../test-assets/example01.json" +export default function Example01() { const calculateBoundsFromNodes = (nodes: CapacityMeshNode[]) => { - let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity - - nodes.forEach(node => { + let minX = Infinity, + maxX = -Infinity, + minY = Infinity, + maxY = -Infinity + + nodes.forEach((node) => { const halfWidth = node.width / 2 const halfHeight = node.height / 2 - + minX = Math.min(minX, node.center.x - halfWidth) maxX = Math.max(maxX, node.center.x + halfWidth) minY = Math.min(minY, node.center.y - halfHeight) maxY = Math.max(maxY, node.center.y + halfHeight) }) - + // Add some padding const padding = 2 return { minX: minX - padding, maxX: maxX + padding, minY: minY - padding, - maxY: maxY + padding + maxY: maxY + padding, } } - if (loading) { - return ( -
- Loading example data... -
- ) - } - - if (error) { - return ( -
-
-

Error Loading Data

-

{error}

-

Make sure the test-assets directory contains example01.json

-
-
- ) + const nodes = (example01.meshNodes || []) as CapacityMeshNode[] + const bounds = calculateBoundsFromNodes(nodes) + const simpleRouteJson: SimpleRouteJson = { + layerCount: 4, // Based on availableZ values in the data + minTraceWidth: 0.1, + obstacles: [], + connections: [], + bounds: bounds, } return ( -
-
-

+
+
+

Capacity Node 3D Debugger - Example 01

- - - -
) From 626dbf5acccdee7b5acb20fb61b38e6e87e8f98c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Tue, 18 Nov 2025 11:47:17 +0530 Subject: [PATCH 09/14] WIP --- AGENTS.md | 1 - README.md | 79 ++++++++++++++++++++++++++++++++++++++++++++-- lib/README.md | 77 -------------------------------------------- test-component.tsx | 48 ---------------------------- 4 files changed, 77 insertions(+), 128 deletions(-) delete mode 100644 lib/README.md delete mode 100644 test-component.tsx diff --git a/AGENTS.md b/AGENTS.md index 19c143a..f7e425e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,3 @@ -we are pulling some code from another project to make a new compoenent called `` and we are using `*.page.tsx` files to view exmaples please create an example page and setup code to be organized and tidy diff --git a/README.md b/README.md index 217c62f..90ae421 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,77 @@ -# trace-capacity-visualizer -A react component and poppygl layer for rendering capacity nodes and 3d traces for viewing routing +# CapacityNode3dDebugger + +A React component for visualizing capacity mesh nodes in 3D space using Three.js. + +## Features + +- 3D visualization of capacity mesh nodes +- Interactive controls for orbiting, zooming, and panning +- Layer-based visualization with customizable thickness +- Toggle options for different visual elements (root, obstacles, output) +- Wireframe and solid rendering modes +- Opacity controls for mesh visualization +- Box shrinking for better spatial visualization +- Border highlighting for mesh boxes + +## Usage + +```tsx +import { CapacityNode3dDebugger } from './lib/CapacityNode3dDebugger' +import type { CapacityMeshNode } from './lib/types' + +const nodes: CapacityMeshNode[] = [ + { + capacityMeshNodeId: "node1", + center: { x: 0, y: 0 }, + width: 10, + height: 10, + layer: "top", + availableZ: [0, 1, 2] + } + // ... more nodes +] + +function App() { + return ( + + ) +} +``` + +## Props + +- `nodes`: Array of capacity mesh nodes to visualize +- `simpleRouteJson`: Optional SimpleRouteJson data for obstacles and bounds +- `layerThickness`: Visual Z thickness per layer (default: 1) +- `height`: Canvas height (default: 600) +- `defaultShowRoot`: Show root bounds initially (default: true) +- `defaultShowObstacles`: Show obstacles initially (default: false) +- `defaultShowOutput`: Show output mesh initially (default: true) +- `defaultWireframeOutput`: Use wireframe for output initially (default: false) +- `style`: Optional CSS styles for the container + +## Controls + +- **Show 3D/Hide 3D**: Toggle 3D visualization +- **Rebuild 3D**: Rebuild the 3D scene +- **Root**: Toggle root bounds visibility +- **Obstacles**: Toggle obstacles visibility +- **Output**: Toggle output mesh visibility +- **Wireframe Output**: Toggle between solid and wireframe rendering +- **Opacity**: Adjust mesh opacity (0-1) +- **Shrink boxes**: Enable box shrinking for better visualization +- **Show borders**: Toggle border highlighting on mesh boxes + +## Mouse Controls + +- **Drag**: Orbit camera +- **Wheel**: Zoom in/out +- **Right-drag**: Pan camera \ No newline at end of file diff --git a/lib/README.md b/lib/README.md deleted file mode 100644 index 90ae421..0000000 --- a/lib/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# CapacityNode3dDebugger - -A React component for visualizing capacity mesh nodes in 3D space using Three.js. - -## Features - -- 3D visualization of capacity mesh nodes -- Interactive controls for orbiting, zooming, and panning -- Layer-based visualization with customizable thickness -- Toggle options for different visual elements (root, obstacles, output) -- Wireframe and solid rendering modes -- Opacity controls for mesh visualization -- Box shrinking for better spatial visualization -- Border highlighting for mesh boxes - -## Usage - -```tsx -import { CapacityNode3dDebugger } from './lib/CapacityNode3dDebugger' -import type { CapacityMeshNode } from './lib/types' - -const nodes: CapacityMeshNode[] = [ - { - capacityMeshNodeId: "node1", - center: { x: 0, y: 0 }, - width: 10, - height: 10, - layer: "top", - availableZ: [0, 1, 2] - } - // ... more nodes -] - -function App() { - return ( - - ) -} -``` - -## Props - -- `nodes`: Array of capacity mesh nodes to visualize -- `simpleRouteJson`: Optional SimpleRouteJson data for obstacles and bounds -- `layerThickness`: Visual Z thickness per layer (default: 1) -- `height`: Canvas height (default: 600) -- `defaultShowRoot`: Show root bounds initially (default: true) -- `defaultShowObstacles`: Show obstacles initially (default: false) -- `defaultShowOutput`: Show output mesh initially (default: true) -- `defaultWireframeOutput`: Use wireframe for output initially (default: false) -- `style`: Optional CSS styles for the container - -## Controls - -- **Show 3D/Hide 3D**: Toggle 3D visualization -- **Rebuild 3D**: Rebuild the 3D scene -- **Root**: Toggle root bounds visibility -- **Obstacles**: Toggle obstacles visibility -- **Output**: Toggle output mesh visibility -- **Wireframe Output**: Toggle between solid and wireframe rendering -- **Opacity**: Adjust mesh opacity (0-1) -- **Shrink boxes**: Enable box shrinking for better visualization -- **Show borders**: Toggle border highlighting on mesh boxes - -## Mouse Controls - -- **Drag**: Orbit camera -- **Wheel**: Zoom in/out -- **Right-drag**: Pan camera \ No newline at end of file diff --git a/test-component.tsx b/test-component.tsx deleted file mode 100644 index 535c79a..0000000 --- a/test-component.tsx +++ /dev/null @@ -1,48 +0,0 @@ - -import { CapacityNode3dDebugger } from './lib/CapacityNode3dDebugger' -import type { CapacityMeshNode } from './lib/types' - -// Simple test data -const testNodes: CapacityMeshNode[] = [ - { - capacityMeshNodeId: "test_node_1", - center: { x: 0, y: 0 }, - width: 10, - height: 10, - layer: "top", - availableZ: [0, 1, 2] - }, - { - capacityMeshNodeId: "test_node_2", - center: { x: 15, y: 0 }, - width: 8, - height: 8, - layer: "top", - availableZ: [0, 1] - }, - { - capacityMeshNodeId: "test_node_3", - center: { x: 0, y: 15 }, - width: 12, - height: 6, - layer: "inner1", - availableZ: [1, 2, 3] - } -] - -function TestComponent() { - return ( -
-

Capacity Node 3D Debugger Test

- -
- ) -} - -export default TestComponent \ No newline at end of file From 6a99d4bb833162f0747de8a049e0094870112468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Tue, 18 Nov 2025 11:58:06 +0530 Subject: [PATCH 10/14] WIP --- lib/CapacityNode3dDebugger.tsx | 709 +------------------------------- lib/ThreeBoardView.tsx | 558 +++++++++++++++++++++++++ package.json | 9 +- utils/buildPrismsFromNodes.ts | 100 +++++ utils/canonicalizeLayerOrder.ts | 10 + utils/clamp01.ts | 3 + utils/contiguousRuns.ts | 15 + utils/darkenColor.ts | 9 + utils/layerSortKey.ts | 8 + 9 files changed, 711 insertions(+), 710 deletions(-) create mode 100644 lib/ThreeBoardView.tsx create mode 100644 utils/buildPrismsFromNodes.ts create mode 100644 utils/canonicalizeLayerOrder.ts create mode 100644 utils/clamp01.ts create mode 100644 utils/contiguousRuns.ts create mode 100644 utils/darkenColor.ts create mode 100644 utils/layerSortKey.ts diff --git a/lib/CapacityNode3dDebugger.tsx b/lib/CapacityNode3dDebugger.tsx index c5d4cda..3fc83ef 100644 --- a/lib/CapacityNode3dDebugger.tsx +++ b/lib/CapacityNode3dDebugger.tsx @@ -1,13 +1,10 @@ import { useCallback, useEffect, - useMemo, - useRef, useState, } from "react" import type { CapacityMeshNode, SimpleRouteJson } from "./types" -import * as THREE from "three" -import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js" +import { ThreeBoardView } from "./ThreeBoardView" type CapacityNode3dDebuggerProps = { nodes: CapacityMeshNode[] @@ -21,708 +18,6 @@ type CapacityNode3dDebuggerProps = { style?: React.CSSProperties } -/* ----------------------------- helpers ----------------------------- */ - -function contiguousRuns(nums: number[]) { - const zs = Array.from(new Set(nums)).sort((a, b) => a - b) - if (zs.length === 0) return [] as number[][] - const groups: number[][] = [] - let run: number[] = [zs[0]!] - for (let i = 1; i < zs.length; i++) { - if (zs[i] === zs[i - 1]! + 1) run.push(zs[i]!) - else { - groups.push(run) - run = [zs[i]!] - } - } - groups.push(run) - return groups -} - -/** Canonical layer order to mirror the solver & experiment */ -function layerSortKey(name: string) { - const n = name.toLowerCase() - if (n === "top") return -1_000_000 - if (n === "bottom") return 1_000_000 - const m = /^inner(\d+)$/i.exec(n) - if (m) return parseInt(m[1]!, 10) || 0 - return 100 + n.charCodeAt(0) -} -function canonicalizeLayerOrder(names: string[]) { - return Array.from(new Set(names)).sort((a, b) => { - const ka = layerSortKey(a) - const kb = layerSortKey(b) - if (ka !== kb) return ka - kb - return a.localeCompare(b) - }) -} - -/** Build prisms by grouping identical XY nodes across contiguous Z */ -function buildPrismsFromNodes( - nodes: CapacityMeshNode[], - fallbackLayerCount: number, -): Array<{ - minX: number - maxX: number - minY: number - maxY: number - z0: number - z1: number - nodes: CapacityMeshNode[] -}> { - const xyKey = (n: CapacityMeshNode) => - `${n.center.x.toFixed(8)}|${n.center.y.toFixed(8)}|${n.width.toFixed( - 8, - )}|${n.height.toFixed(8)}` - const azKey = (n: CapacityMeshNode) => { - const zs = ( - n.availableZ && n.availableZ.length - ? Array.from(new Set(n.availableZ)) - : [0] - ).sort((a, b) => a - b) - return `zset:${zs.join(",")}` - } - const key = (n: CapacityMeshNode) => `${xyKey(n)}|${azKey(n)}` - - const groups = new Map< - string, - { - cx: number - cy: number - w: number - h: number - zs: number[] - nodes: CapacityMeshNode[] - } - >() - for (const n of nodes) { - const k = key(n) - const zlist = n.availableZ?.length ? n.availableZ : [0] - const g = groups.get(k) - if (g) { - g.zs.push(...zlist) - g.nodes.push(n) - } else - groups.set(k, { - cx: n.center.x, - cy: n.center.y, - w: n.width, - h: n.height, - zs: [...zlist], - nodes: [n], - }) - } - - const prisms: Array<{ - minX: number - maxX: number - minY: number - maxY: number - z0: number - z1: number - nodes: CapacityMeshNode[] - }> = [] - for (const g of Array.from(groups.values())) { - const minX = g.cx - g.w / 2 - const maxX = g.cx + g.w / 2 - const minY = g.cy - g.h / 2 - const maxY = g.cy + g.h / 2 - const runs = contiguousRuns(g.zs) - if (runs.length === 0) { - prisms.push({ - minX, - maxX, - minY, - maxY, - z0: 0, - z1: Math.max(1, fallbackLayerCount), - nodes: g.nodes, - }) - } else { - for (const r of runs) { - prisms.push({ - minX, - maxX, - minY, - maxY, - z0: r[0]!, - z1: r[r.length - 1]! + 1, - nodes: g.nodes, - }) - } - } - } - return prisms -} - -function clamp01(x: number) { - return Math.max(0, Math.min(1, x)) -} - -function darkenColor(hex: number, factor = 0.6): number { - const r = ((hex >> 16) & 0xff) * factor - const g = ((hex >> 8) & 0xff) * factor - const b = (hex & 0xff) * factor - const cr = Math.max(0, Math.min(255, Math.round(r))) - const cg = Math.max(0, Math.min(255, Math.round(g))) - const cb = Math.max(0, Math.min(255, Math.round(b))) - return (cr << 16) | (cg << 8) | cb -} - -/* ---------------------------- 3D Canvas ---------------------------- */ - -const ThreeBoardView: React.FC<{ - nodes: CapacityMeshNode[] - srj?: SimpleRouteJson - layerThickness: number - height: number - showRoot: boolean - showObstacles: boolean - showOutput: boolean - wireframeOutput: boolean - meshOpacity: number - shrinkBoxes: boolean - boxShrinkAmount: number - showBorders: boolean - isOrthographic: boolean - onDeleteNodes: (nodes: CapacityMeshNode[]) => void -}> = ({ - nodes, - srj, - layerThickness, - height, - showRoot, - showObstacles, - showOutput, - wireframeOutput, - meshOpacity, - shrinkBoxes, - boxShrinkAmount, - showBorders, - isOrthographic, - onDeleteNodes, -}) => { - const containerRef = useRef(null) - const destroyRef = useRef<() => void>(() => {}) - const sceneRef = useRef(null) - const cameraRef = - useRef(null) - const outputGroupRef = useRef(null) - const controlsStateRef = - useRef<{ position: THREE.Vector3; target: THREE.Vector3; zoom: number } | null>( - null, - ) - - const [contextMenu, setContextMenu] = useState<{ - x: number - y: number - nodes: CapacityMeshNode[] - } | null>(null) - - const handleContextMenu = (event: React.MouseEvent) => { - event.preventDefault() - event.stopPropagation() - setContextMenu(null) // Close any existing menu - - const scene = sceneRef.current - const camera = cameraRef.current - const outputGroup = outputGroupRef.current - const el = containerRef.current - if (!el || !scene || !camera || !outputGroup) return - - const rect = el.getBoundingClientRect() - const pointer = new THREE.Vector2() - pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1 - pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1 - - const raycaster = new THREE.Raycaster() - raycaster.setFromCamera(pointer, camera) - const intersects = raycaster.intersectObjects(outputGroup.children, true) - - if (intersects.length > 0 && intersects[0]) { - let intersectedObject: THREE.Object3D | null = intersects[0].object - while (intersectedObject && !intersectedObject.userData.nodes) { - intersectedObject = intersectedObject.parent - } - - if (intersectedObject && intersectedObject.userData.nodes) { - setContextMenu({ - x: event.clientX, - y: event.clientY, - nodes: intersectedObject.userData.nodes, - }) - } - } - } - - const handleDelete = (nodesToDelete: CapacityMeshNode[]) => { - onDeleteNodes(nodesToDelete) - setContextMenu(null) - } - - useEffect(() => { - const handleClick = () => setContextMenu(null) - window.addEventListener("click", handleClick) - return () => window.removeEventListener("click", handleClick) - }, []) - - const layerNames = useMemo(() => { - // Build from nodes (preferred, matches solver) and fall back to SRJ obstacle names - const fromNodes = canonicalizeLayerOrder(nodes.map((n) => n.layer)) - if (fromNodes.length) return fromNodes - const fromObs = canonicalizeLayerOrder( - (srj?.obstacles ?? []).flatMap((o) => o.layers ?? []), - ) - return fromObs.length ? fromObs : ["top"] - }, [nodes, srj]) - - const zIndexByLayerName = useMemo(() => { - const m = new Map() - layerNames.forEach((n, i) => m.set(n, i)) - return m - }, [layerNames]) - - const layerCount = layerNames.length || srj?.layerCount || 1 - - const prisms = useMemo( - () => buildPrismsFromNodes(nodes, layerCount), - [nodes, layerCount], - ) - - useEffect(() => { - let mounted = true - ;(async () => { - const el = containerRef.current - if (!el) return - if (!mounted) return - - destroyRef.current?.() - - const w = el.clientWidth || 800 - const h = el.clientHeight || height - - const renderer = new THREE.WebGLRenderer({ - antialias: true, - alpha: true, - premultipliedAlpha: false, - }) - // Increase pixel ratio for better alphaHash quality - renderer.setPixelRatio(window.devicePixelRatio) - renderer.setSize(w, h) - el.innerHTML = "" - el.appendChild(renderer.domElement) - - const scene = new THREE.Scene() - sceneRef.current = scene - scene.background = new THREE.Color(0xf7f8fa) - - const fitBox = srj - ? { - minX: srj.bounds.minX, - maxX: srj.bounds.maxX, - minY: srj.bounds.minY, - maxY: srj.bounds.maxY, - z0: 0, - z1: layerCount, - } - : (() => { - if (prisms.length === 0) { - return { - minX: -10, - maxX: 10, - minY: -10, - maxY: 10, - z0: 0, - z1: layerCount, - } - } - let minX = Infinity, - minY = Infinity, - maxX = -Infinity, - maxY = -Infinity - for (const p of prisms) { - minX = Math.min(minX, p.minX) - maxX = Math.max(maxX, p.maxX) - minY = Math.min(minY, p.minY) - maxY = Math.max(maxY, p.maxY) - } - return { minX, maxX, minY, maxY, z0: 0, z1: layerCount } - })() - - const dx = fitBox.maxX - fitBox.minX - const dz = fitBox.maxY - fitBox.minY - const dy = (fitBox.z1 - fitBox.z0) * layerThickness - const size = Math.max(dx, dz, dy) - - let camera: THREE.PerspectiveCamera | THREE.OrthographicCamera - if (isOrthographic) { - const aspect = w / h - const frustumSize = size * 1.2 - camera = new THREE.OrthographicCamera( - (frustumSize * aspect) / -2, - (frustumSize * aspect) / 2, - frustumSize / 2, - frustumSize / -2, - 0.1, - size * 20, - ) - } else { - camera = new THREE.PerspectiveCamera(45, w / h, 0.1, 10000) - } - cameraRef.current = camera - - const controls = new OrbitControls(camera, renderer.domElement) - controls.enableDamping = true - - const amb = new THREE.AmbientLight(0xffffff, 0.9) - scene.add(amb) - const dir = new THREE.DirectionalLight(0xffffff, 0.6) - dir.position.set(1, 2, 3) - scene.add(dir) - - const rootGroup = new THREE.Group() - const obstaclesGroup = new THREE.Group() - const outputGroup = new THREE.Group() - outputGroupRef.current = outputGroup - scene.add(rootGroup, obstaclesGroup, outputGroup) - - // Axes helper for orientation (similar to experiment) - const axes = new THREE.AxesHelper(50) - scene.add(axes) - - const colorRoot = 0x111827 - const colorOb = 0xef4444 - - // Palette for layer-span-based coloring - const spanPalette = [ - 0x0ea5e9, // cyan-ish - 0x22c55e, // green - 0xf97316, // orange - 0xa855f7, // purple - 0xfacc15, // yellow - 0x38bdf8, // light blue - 0xec4899, // pink - 0x14b8a6, // teal - ] - const spanColorMap = new Map() - let spanColorIndex = 0 - const getSpanColor = (z0: number, z1: number) => { - const key = `${z0}-${z1}` - let c = spanColorMap.get(key) - if (c == null) { - c = spanPalette[spanColorIndex % spanPalette.length]! - spanColorMap.set(key, c) - spanColorIndex++ - } - return c - } - - function makeBoxMesh( - b: { - minX: number - maxX: number - minY: number - maxY: number - z0: number - z1: number - }, - color: number, - wire: boolean, - nodes: CapacityMeshNode[], - opacity = 0.45, - borders = false, - ) { - const dx = b.maxX - b.minX - const dz = b.maxY - b.minY // map board Y -> three Z - const dy = (b.z1 - b.z0) * layerThickness - const cx = -((b.minX + b.maxX) / 2) // negate X to match expected orientation - const cz = (b.minY + b.maxY) / 2 - // Negate Y so z=0 is at top, higher z goes down - const cy = -((b.z0 + b.z1) / 2) * layerThickness - - const geom = new THREE.BoxGeometry(dx, dy, dz) - if (wire) { - const edges = new THREE.EdgesGeometry(geom) - const line = new THREE.LineSegments( - edges, - new THREE.LineBasicMaterial({ color }), - ) - line.position.set(cx, cy, cz) - line.userData = { nodes } - return line - } - const clampedOpacity = clamp01(opacity) - const mat = new THREE.MeshPhongMaterial({ - color, - opacity: clampedOpacity, - transparent: clampedOpacity < 1, - alphaHash: clampedOpacity < 1, - alphaToCoverage: true, - }) - - const mesh = new THREE.Mesh(geom, mat) - mesh.position.set(cx, cy, cz) - mesh.userData = { nodes } - - if (!borders) return mesh - - const edges = new THREE.EdgesGeometry(geom) - const borderColor = darkenColor(color, 0.6) - const line = new THREE.LineSegments( - edges, - new THREE.LineBasicMaterial({ color: borderColor }), - ) - line.position.set(cx, cy, cz) - - const group = new THREE.Group() - group.add(mesh) - group.add(line) - group.userData = { nodes } - return group - } - - // Root wireframe from SRJ bounds - if (srj && showRoot) { - const rootBox = { - minX: srj.bounds.minX, - maxX: srj.bounds.maxX, - minY: srj.bounds.minY, - maxY: srj.bounds.maxY, - z0: 0, - z1: layerCount, - } - rootGroup.add(makeBoxMesh(rootBox, colorRoot, true, [])) - } - - // Obstacles — rectangular only — one slab per declared layer - if (srj && showObstacles) { - for (const ob of srj.obstacles ?? []) { - if (ob.type !== "rect") continue - const minX = ob.center.x - ob.width / 2 - const maxX = ob.center.x + ob.width / 2 - const minY = ob.center.y - ob.height / 2 - const maxY = ob.center.y + ob.height / 2 - - // Prefer explicit zLayers; otherwise map layer names to indices - const zs = - ob.zLayers && ob.zLayers.length - ? Array.from(new Set(ob.zLayers)) - : (ob.layers ?? []) - .map((name) => zIndexByLayerName.get(name)) - .filter((z): z is number => typeof z === "number") - - for (const z of zs) { - if (z < 0 || z >= layerCount) continue - obstaclesGroup.add( - makeBoxMesh( - { minX, maxX, minY, maxY, z0: z, z1: z + 1 }, - colorOb, - false, - [], - 0.35, - false, - ), - ) - } - } - } - - // Output prisms from nodes (wireframe toggle like the experiment) - if (showOutput) { - for (const p of prisms) { - let box = p - if (shrinkBoxes && boxShrinkAmount > 0) { - const s = boxShrinkAmount - - const widthX = p.maxX - p.minX - const widthY = p.maxY - p.minY - - // Never shrink more on a side than allowed by the configured shrink amount - // while ensuring we don't shrink past a minimum dimension of "s" - const maxShrinkEachSideX = Math.max(0, (widthX - s) / 2) - const maxShrinkEachSideY = Math.max(0, (widthY - s) / 2) - - const shrinkX = Math.min(s, maxShrinkEachSideX) - const shrinkY = Math.min(s, maxShrinkEachSideY) - - const minX = p.minX + shrinkX - const maxX = p.maxX - shrinkX - const minY = p.minY + shrinkY - const maxY = p.maxY - shrinkY - - // Guard against any degenerate box - if (minX >= maxX || minY >= maxY) { - continue - } - - box = { ...p, minX, maxX, minY, maxY } - } - - const color = getSpanColor(p.z0, p.z1) - outputGroup.add( - makeBoxMesh( - box, - color, - wireframeOutput, - p.nodes, - meshOpacity, - showBorders && !wireframeOutput, - ), - ) - } - } - - if (controlsStateRef.current) { - camera.position.copy(controlsStateRef.current.position) - controls.target.copy(controlsStateRef.current.target) - if (isOrthographic) { - ;(camera as THREE.OrthographicCamera).zoom = - controlsStateRef.current.zoom - } - camera.updateProjectionMatrix() - controls.update() - } else { - // Fit camera - const target = new THREE.Vector3( - -((fitBox.minX + fitBox.maxX) / 2), // negate X to account for flipped axis - -dy / 2, // center of the inverted Y range - (fitBox.minY + fitBox.maxY) / 2, - ) - controls.target.copy(target) - - const dist = size * 2.0 - if (isOrthographic) { - // Top-down view - camera.position.copy(target).add(new THREE.Vector3(0, dist, 0)) - camera.lookAt(target) - } else { - // Camera looks from above-right-front, with negative Y being "up" (z=0 at top) - camera.position.set( - -(fitBox.maxX + dist * 0.6), // negate X to account for flipped axis - -dy / 2 + dist, // negative Y is up, so position above the center - fitBox.maxY + dist * 0.6, - ) - } - - camera.near = Math.max(0.1, size / 100) - camera.far = dist * 10 + size * 10 - camera.updateProjectionMatrix() - controls.update() - } - - const onResize = () => { - const W = el.clientWidth || w - const H = el.clientHeight || h - if (isOrthographic) { - const aspect = W / H - const frustumSize = size * 1.2 - const ocam = camera as THREE.OrthographicCamera - ocam.left = (frustumSize * aspect) / -2 - ocam.right = (frustumSize * aspect) / 2 - ocam.top = frustumSize / 2 - ocam.bottom = frustumSize / -2 - } else { - ;(camera as THREE.PerspectiveCamera).aspect = W / H - } - camera.updateProjectionMatrix() - renderer.setSize(W, H) - } - window.addEventListener("resize", onResize) - - let raf = 0 - const animate = () => { - raf = requestAnimationFrame(animate) - controls.update() - renderer.render(scene, camera) - } - animate() - - destroyRef.current = () => { - controlsStateRef.current = { - position: camera.position.clone(), - target: controls.target.clone(), - zoom: (camera as any).zoom ?? 1, - } - cancelAnimationFrame(raf) - window.removeEventListener("resize", onResize) - renderer.dispose() - el.innerHTML = "" - sceneRef.current = null - cameraRef.current = null - outputGroupRef.current = null - } - })() - - return () => { - mounted = false - destroyRef.current?.() - } - }, [ - srj, - prisms, - layerCount, - layerThickness, - height, - showObstacles, - wireframeOutput, - zIndexByLayerName, - meshOpacity, - shrinkBoxes, - boxShrinkAmount, - showBorders, - isOrthographic, - ]) - - return ( - <> - {contextMenu && ( -
e.stopPropagation()} // Prevent closing when clicking inside - > - -
- )} -
- - ) -} - -/* ----------------------- Public wrapper component ----------------------- */ - export const CapacityNode3dDebugger: React.FC = ({ nodes: initialNodes, simpleRouteJson, @@ -948,4 +243,4 @@ export const CapacityNode3dDebugger: React.FC = ({
) -} \ No newline at end of file +} diff --git a/lib/ThreeBoardView.tsx b/lib/ThreeBoardView.tsx new file mode 100644 index 0000000..cf4eaee --- /dev/null +++ b/lib/ThreeBoardView.tsx @@ -0,0 +1,558 @@ +import { useMemo, useRef, useState, useEffect } from "react" +import type { CapacityMeshNode, SimpleRouteJson } from "./types" +import * as THREE from "three" +import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js" +import { canonicalizeLayerOrder } from "../utils/canonicalizeLayerOrder" +import { buildPrismsFromNodes } from "../utils/buildPrismsFromNodes" +import { clamp01 } from "../utils/clamp01" +import { darkenColor } from "../utils/darkenColor" + +export const ThreeBoardView: React.FC<{ + nodes: CapacityMeshNode[] + srj?: SimpleRouteJson + layerThickness: number + height: number + showRoot: boolean + showObstacles: boolean + showOutput: boolean + wireframeOutput: boolean + meshOpacity: number + shrinkBoxes: boolean + boxShrinkAmount: number + showBorders: boolean + isOrthographic: boolean + onDeleteNodes: (nodes: CapacityMeshNode[]) => void +}> = ({ + nodes, + srj, + layerThickness, + height, + showRoot, + showObstacles, + showOutput, + wireframeOutput, + meshOpacity, + shrinkBoxes, + boxShrinkAmount, + showBorders, + isOrthographic, + onDeleteNodes, +}) => { + const containerRef = useRef(null) + const destroyRef = useRef<() => void>(() => {}) + const sceneRef = useRef(null) + const cameraRef = + useRef(null) + const outputGroupRef = useRef(null) + const controlsStateRef = + useRef<{ position: THREE.Vector3; target: THREE.Vector3; zoom: number } | null>( + null, + ) + + const [contextMenu, setContextMenu] = useState<{ + x: number + y: number + nodes: CapacityMeshNode[] + } | null>(null) + + const handleContextMenu = (event: React.MouseEvent) => { + event.preventDefault() + event.stopPropagation() + setContextMenu(null) // Close any existing menu + + const scene = sceneRef.current + const camera = cameraRef.current + const outputGroup = outputGroupRef.current + const el = containerRef.current + if (!el || !scene || !camera || !outputGroup) return + + const rect = el.getBoundingClientRect() + const pointer = new THREE.Vector2() + pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1 + pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1 + + const raycaster = new THREE.Raycaster() + raycaster.setFromCamera(pointer, camera) + const intersects = raycaster.intersectObjects(outputGroup.children, true) + + if (intersects.length > 0 && intersects[0]) { + let intersectedObject: THREE.Object3D | null = intersects[0].object + while (intersectedObject && !intersectedObject.userData.nodes) { + intersectedObject = intersectedObject.parent + } + + if (intersectedObject && intersectedObject.userData.nodes) { + setContextMenu({ + x: event.clientX, + y: event.clientY, + nodes: intersectedObject.userData.nodes, + }) + } + } + } + + const handleDelete = (nodesToDelete: CapacityMeshNode[]) => { + onDeleteNodes(nodesToDelete) + setContextMenu(null) + } + + useEffect(() => { + const handleClick = () => setContextMenu(null) + window.addEventListener("click", handleClick) + return () => window.removeEventListener("click", handleClick) + }, []) + + const layerNames = useMemo(() => { + // Build from nodes (preferred, matches solver) and fall back to SRJ obstacle names + const fromNodes = canonicalizeLayerOrder(nodes.map((n) => n.layer)) + if (fromNodes.length) return fromNodes + const fromObs = canonicalizeLayerOrder( + (srj?.obstacles ?? []).flatMap((o) => o.layers ?? []), + ) + return fromObs.length ? fromObs : ["top"] + }, [nodes, srj]) + + const zIndexByLayerName = useMemo(() => { + const m = new Map() + layerNames.forEach((n, i) => m.set(n, i)) + return m + }, [layerNames]) + + const layerCount = layerNames.length || srj?.layerCount || 1 + + const prisms = useMemo( + () => buildPrismsFromNodes(nodes, layerCount), + [nodes, layerCount], + ) + + useEffect(() => { + let mounted = true + ;(async () => { + const el = containerRef.current + if (!el) return + if (!mounted) return + + destroyRef.current?.() + + const w = el.clientWidth || 800 + const h = el.clientHeight || height + + const renderer = new THREE.WebGLRenderer({ + antialias: true, + alpha: true, + premultipliedAlpha: false, + }) + // Increase pixel ratio for better alphaHash quality + renderer.setPixelRatio(window.devicePixelRatio) + renderer.setSize(w, h) + el.innerHTML = "" + el.appendChild(renderer.domElement) + + const scene = new THREE.Scene() + sceneRef.current = scene + scene.background = new THREE.Color(0xf7f8fa) + + const fitBox = srj + ? { + minX: srj.bounds.minX, + maxX: srj.bounds.maxX, + minY: srj.bounds.minY, + maxY: srj.bounds.maxY, + z0: 0, + z1: layerCount, + } + : (() => { + if (prisms.length === 0) { + return { + minX: -10, + maxX: 10, + minY: -10, + maxY: 10, + z0: 0, + z1: layerCount, + } + } + let minX = Infinity, + minY = Infinity, + maxX = -Infinity, + maxY = -Infinity + for (const p of prisms) { + minX = Math.min(minX, p.minX) + maxX = Math.max(maxX, p.maxX) + minY = Math.min(minY, p.minY) + maxY = Math.max(maxY, p.maxY) + } + return { minX, maxX, minY, maxY, z0: 0, z1: layerCount } + })() + + const dx = fitBox.maxX - fitBox.minX + const dz = fitBox.maxY - fitBox.minY + const dy = (fitBox.z1 - fitBox.z0) * layerThickness + const size = Math.max(dx, dz, dy) + + let camera: THREE.PerspectiveCamera | THREE.OrthographicCamera + if (isOrthographic) { + const aspect = w / h + const frustumSize = size * 1.2 + camera = new THREE.OrthographicCamera( + (frustumSize * aspect) / -2, + (frustumSize * aspect) / 2, + frustumSize / 2, + frustumSize / -2, + 0.1, + size * 20, + ) + } else { + camera = new THREE.PerspectiveCamera(45, w / h, 0.1, 10000) + } + cameraRef.current = camera + + const controls = new OrbitControls(camera, renderer.domElement) + controls.enableDamping = true + + const amb = new THREE.AmbientLight(0xffffff, 0.9) + scene.add(amb) + const dir = new THREE.DirectionalLight(0xffffff, 0.6) + dir.position.set(1, 2, 3) + scene.add(dir) + + const rootGroup = new THREE.Group() + const obstaclesGroup = new THREE.Group() + const outputGroup = new THREE.Group() + outputGroupRef.current = outputGroup + scene.add(rootGroup, obstaclesGroup, outputGroup) + + // Axes helper for orientation (similar to experiment) + const axes = new THREE.AxesHelper(50) + scene.add(axes) + + const colorRoot = 0x111827 + const colorOb = 0xef4444 + + // Palette for layer-span-based coloring + const spanPalette = [ + 0x0ea5e9, // cyan-ish + 0x22c55e, // green + 0xf97316, // orange + 0xa855f7, // purple + 0xfacc15, // yellow + 0x38bdf8, // light blue + 0xec4899, // pink + 0x14b8a6, // teal + ] + const spanColorMap = new Map() + let spanColorIndex = 0 + const getSpanColor = (z0: number, z1: number) => { + const key = `${z0}-${z1}` + let c = spanColorMap.get(key) + if (c == null) { + c = spanPalette[spanColorIndex % spanPalette.length]! + spanColorMap.set(key, c) + spanColorIndex++ + } + return c + } + + function makeBoxMesh( + b: { + minX: number + maxX: number + minY: number + maxY: number + z0: number + z1: number + }, + color: number, + wire: boolean, + nodes: CapacityMeshNode[], + opacity = 0.45, + borders = false, + ) { + const dx = b.maxX - b.minX + const dz = b.maxY - b.minY // map board Y -> three Z + const dy = (b.z1 - b.z0) * layerThickness + const cx = -((b.minX + b.maxX) / 2) // negate X to match expected orientation + const cz = (b.minY + b.maxY) / 2 + // Negate Y so z=0 is at top, higher z goes down + const cy = -((b.z0 + b.z1) / 2) * layerThickness + + const geom = new THREE.BoxGeometry(dx, dy, dz) + if (wire) { + const edges = new THREE.EdgesGeometry(geom) + const line = new THREE.LineSegments( + edges, + new THREE.LineBasicMaterial({ color }), + ) + line.position.set(cx, cy, cz) + line.userData = { nodes } + return line + } + const clampedOpacity = clamp01(opacity) + const mat = new THREE.MeshPhongMaterial({ + color, + opacity: clampedOpacity, + transparent: clampedOpacity < 1, + alphaHash: clampedOpacity < 1, + alphaToCoverage: true, + }) + + const mesh = new THREE.Mesh(geom, mat) + mesh.position.set(cx, cy, cz) + mesh.userData = { nodes } + + if (!borders) return mesh + + const edges = new THREE.EdgesGeometry(geom) + const borderColor = darkenColor(color, 0.6) + const line = new THREE.LineSegments( + edges, + new THREE.LineBasicMaterial({ color: borderColor }), + ) + line.position.set(cx, cy, cz) + + const group = new THREE.Group() + group.add(mesh) + group.add(line) + group.userData = { nodes } + return group + } + + // Root wireframe from SRJ bounds + if (srj && showRoot) { + const rootBox = { + minX: srj.bounds.minX, + maxX: srj.bounds.maxX, + minY: srj.bounds.minY, + maxY: srj.bounds.maxY, + z0: 0, + z1: layerCount, + } + rootGroup.add(makeBoxMesh(rootBox, colorRoot, true, [])) + } + + // Obstacles — rectangular only — one slab per declared layer + if (srj && showObstacles) { + for (const ob of srj.obstacles ?? []) { + if (ob.type !== "rect") continue + const minX = ob.center.x - ob.width / 2 + const maxX = ob.center.x + ob.width / 2 + const minY = ob.center.y - ob.height / 2 + const maxY = ob.center.y + ob.height / 2 + + // Prefer explicit zLayers; otherwise map layer names to indices + const zs = + ob.zLayers && ob.zLayers.length + ? Array.from(new Set(ob.zLayers)) + : (ob.layers ?? []) + .map((name) => zIndexByLayerName.get(name)) + .filter((z): z is number => typeof z === "number") + + for (const z of zs) { + if (z < 0 || z >= layerCount) continue + obstaclesGroup.add( + makeBoxMesh( + { minX, maxX, minY, maxY, z0: z, z1: z + 1 }, + colorOb, + false, + [], + 0.35, + false, + ), + ) + } + } + } + + // Output prisms from nodes (wireframe toggle like the experiment) + if (showOutput) { + for (const p of prisms) { + let box = p + if (shrinkBoxes && boxShrinkAmount > 0) { + const s = boxShrinkAmount + + const widthX = p.maxX - p.minX + const widthY = p.maxY - p.minY + + // Never shrink more on a side than allowed by the configured shrink amount + // while ensuring we don't shrink past a minimum dimension of "s" + const maxShrinkEachSideX = Math.max(0, (widthX - s) / 2) + const maxShrinkEachSideY = Math.max(0, (widthY - s) / 2) + + const shrinkX = Math.min(s, maxShrinkEachSideX) + const shrinkY = Math.min(s, maxShrinkEachSideY) + + const minX = p.minX + shrinkX + const maxX = p.maxX - shrinkX + const minY = p.minY + shrinkY + const maxY = p.maxY - shrinkY + + // Guard against any degenerate box + if (minX >= maxX || minY >= maxY) { + continue + } + + box = { ...p, minX, maxX, minY, maxY } + } + + const color = getSpanColor(p.z0, p.z1) + outputGroup.add( + makeBoxMesh( + box, + color, + wireframeOutput, + p.nodes, + meshOpacity, + showBorders && !wireframeOutput, + ), + ) + } + } + + if (controlsStateRef.current) { + camera.position.copy(controlsStateRef.current.position) + controls.target.copy(controlsStateRef.current.target) + if (isOrthographic) { + ;(camera as THREE.OrthographicCamera).zoom = + controlsStateRef.current.zoom + } + camera.updateProjectionMatrix() + controls.update() + } else { + // Fit camera + const target = new THREE.Vector3( + -((fitBox.minX + fitBox.maxX) / 2), // negate X to account for flipped axis + -dy / 2, // center of the inverted Y range + (fitBox.minY + fitBox.maxY) / 2, + ) + controls.target.copy(target) + + const dist = size * 2.0 + if (isOrthographic) { + // Top-down view + camera.position.copy(target).add(new THREE.Vector3(0, dist, 0)) + camera.lookAt(target) + } else { + // Camera looks from above-right-front, with negative Y being "up" (z=0 at top) + camera.position.set( + -(fitBox.maxX + dist * 0.6), // negate X to account for flipped axis + -dy / 2 + dist, // negative Y is up, so position above the center + fitBox.maxY + dist * 0.6, + ) + } + + camera.near = Math.max(0.1, size / 100) + camera.far = dist * 10 + size * 10 + camera.updateProjectionMatrix() + controls.update() + } + + const onResize = () => { + const W = el.clientWidth || w + const H = el.clientHeight || h + if (isOrthographic) { + const aspect = W / H + const frustumSize = size * 1.2 + const ocam = camera as THREE.OrthographicCamera + ocam.left = (frustumSize * aspect) / -2 + ocam.right = (frustumSize * aspect) / 2 + ocam.top = frustumSize / 2 + ocam.bottom = frustumSize / -2 + } else { + ;(camera as THREE.PerspectiveCamera).aspect = W / H + } + camera.updateProjectionMatrix() + renderer.setSize(W, H) + } + window.addEventListener("resize", onResize) + + let raf = 0 + const animate = () => { + raf = requestAnimationFrame(animate) + controls.update() + renderer.render(scene, camera) + } + animate() + + destroyRef.current = () => { + controlsStateRef.current = { + position: camera.position.clone(), + target: controls.target.clone(), + zoom: (camera as any).zoom ?? 1, + } + cancelAnimationFrame(raf) + window.removeEventListener("resize", onResize) + renderer.dispose() + el.innerHTML = "" + sceneRef.current = null + cameraRef.current = null + outputGroupRef.current = null + } + })() + + return () => { + mounted = false + destroyRef.current?.() + } + }, [ + srj, + prisms, + layerCount, + layerThickness, + height, + showObstacles, + wireframeOutput, + zIndexByLayerName, + meshOpacity, + shrinkBoxes, + boxShrinkAmount, + showBorders, + isOrthographic, + ]) + + return ( + <> + {contextMenu && ( +
e.stopPropagation()} // Prevent closing when clicking inside + > + +
+ )} +
+ + ) +} diff --git a/package.json b/package.json index 37bb730..4465a0d 100644 --- a/package.json +++ b/package.json @@ -6,18 +6,21 @@ "scripts": { "start": "cosmos", "build:site": "cosmos-export", - "build": "tsup-node lib/index.ts --format esm --dts" + "build": "tsup-node lib/index.ts --format esm --dts", + "format": "biome format --write .", + "format:check": "biome format ." }, "devDependencies": { "@types/bun": "latest", "@types/three": "^0.170.0", + "biome": "^0.3.3", "react": "^19.2.0", "react-cosmos": "^7.0.0", "react-cosmos-plugin-vite": "^7.0.0", "react-dom": "^19.2.0", "three": "^0.171.0", - "vite": "^7.2.2", - "tsup": "^8.5.1" + "tsup": "^8.5.1", + "vite": "^7.2.2" }, "peerDependencies": { "typescript": "^5" diff --git a/utils/buildPrismsFromNodes.ts b/utils/buildPrismsFromNodes.ts new file mode 100644 index 0000000..b13fdd2 --- /dev/null +++ b/utils/buildPrismsFromNodes.ts @@ -0,0 +1,100 @@ +import type { CapacityMeshNode } from "../lib/types" +import { contiguousRuns } from "./contiguousRuns" + +/** Build prisms by grouping identical XY nodes across contiguous Z */ +export function buildPrismsFromNodes( + nodes: CapacityMeshNode[], + fallbackLayerCount: number, +): Array<{ + minX: number + maxX: number + minY: number + maxY: number + z0: number + z1: number + nodes: CapacityMeshNode[] +}> { + const xyKey = (n: CapacityMeshNode) => + `${n.center.x.toFixed(8)}|${n.center.y.toFixed(8)}|${n.width.toFixed( + 8, + )}|${n.height.toFixed(8)}` + const azKey = (n: CapacityMeshNode) => { + const zs = ( + n.availableZ && n.availableZ.length + ? Array.from(new Set(n.availableZ)) + : [0] + ).sort((a, b) => a - b) + return `zset:${zs.join(",")}` + } + const key = (n: CapacityMeshNode) => `${xyKey(n)}|${azKey(n)}` + + const groups = new Map< + string, + { + cx: number + cy: number + w: number + h: number + zs: number[] + nodes: CapacityMeshNode[] + } + >() + for (const n of nodes) { + const k = key(n) + const zlist = n.availableZ?.length ? n.availableZ : [0] + const g = groups.get(k) + if (g) { + g.zs.push(...zlist) + g.nodes.push(n) + } else + groups.set(k, { + cx: n.center.x, + cy: n.center.y, + w: n.width, + h: n.height, + zs: [...zlist], + nodes: [n], + }) + } + + const prisms: Array<{ + minX: number + maxX: number + minY: number + maxY: number + z0: number + z1: number + nodes: CapacityMeshNode[] + }> = [] + for (const g of Array.from(groups.values())) { + const minX = g.cx - g.w / 2 + const maxX = g.cx + g.w / 2 + const minY = g.cy - g.h / 2 + const maxY = g.cy + g.h / 2 + const runs = contiguousRuns(g.zs) + if (runs.length === 0) { + prisms.push({ + minX, + maxX, + minY, + maxY, + z0: 0, + z1: Math.max(1, fallbackLayerCount), + nodes: g.nodes, + }) + } else { + for (const r of runs) { + prisms.push({ + minX, + maxX, + minY, + maxY, + z0: r[0]!, + z1: r[r.length - 1]! + 1, + nodes: g.nodes, + }) + } + } + } + return prisms +} diff --git a/utils/canonicalizeLayerOrder.ts b/utils/canonicalizeLayerOrder.ts new file mode 100644 index 0000000..1820aa9 --- /dev/null +++ b/utils/canonicalizeLayerOrder.ts @@ -0,0 +1,10 @@ +import { layerSortKey } from "./layerSortKey" + +export function canonicalizeLayerOrder(names: string[]) { + return Array.from(new Set(names)).sort((a, b) => { + const ka = layerSortKey(a) + const kb = layerSortKey(b) + if (ka !== kb) return ka - kb + return a.localeCompare(b) + }) +} diff --git a/utils/clamp01.ts b/utils/clamp01.ts new file mode 100644 index 0000000..4ef2a0e --- /dev/null +++ b/utils/clamp01.ts @@ -0,0 +1,3 @@ +export function clamp01(x: number) { + return Math.max(0, Math.min(1, x)) +} diff --git a/utils/contiguousRuns.ts b/utils/contiguousRuns.ts new file mode 100644 index 0000000..f378f4e --- /dev/null +++ b/utils/contiguousRuns.ts @@ -0,0 +1,15 @@ +export function contiguousRuns(nums: number[]) { + const zs = Array.from(new Set(nums)).sort((a, b) => a - b) + if (zs.length === 0) return [] as number[][] + const groups: number[][] = [] + let run: number[] = [zs[0]!] + for (let i = 1; i < zs.length; i++) { + if (zs[i] === zs[i - 1]! + 1) run.push(zs[i]!) + else { + groups.push(run) + run = [zs[i]!] + } + } + groups.push(run) + return groups +} diff --git a/utils/darkenColor.ts b/utils/darkenColor.ts new file mode 100644 index 0000000..c647db8 --- /dev/null +++ b/utils/darkenColor.ts @@ -0,0 +1,9 @@ +export function darkenColor(hex: number, factor = 0.6): number { + const r = ((hex >> 16) & 0xff) * factor + const g = ((hex >> 8) & 0xff) * factor + const b = (hex & 0xff) * factor + const cr = Math.max(0, Math.min(255, Math.round(r))) + const cg = Math.max(0, Math.min(255, Math.round(g))) + const cb = Math.max(0, Math.min(255, Math.round(b))) + return (cr << 16) | (cg << 8) | cb +} diff --git a/utils/layerSortKey.ts b/utils/layerSortKey.ts new file mode 100644 index 0000000..692e6c4 --- /dev/null +++ b/utils/layerSortKey.ts @@ -0,0 +1,8 @@ +export function layerSortKey(name: string) { + const n = name.toLowerCase() + if (n === "top") return -1_000_000 + if (n === "bottom") return 1_000_000 + const m = /^inner(\d+)$/i.exec(n) + if (m) return parseInt(m[1]!, 10) || 0 + return 100 + n.charCodeAt(0) +} From 1f15e6e4c3e9d2aab82fbb89b212032a5f996364 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Tue, 18 Nov 2025 11:59:26 +0530 Subject: [PATCH 11/14] WIP --- bun.lock | 583 ------------------------------------------------------- 1 file changed, 583 deletions(-) delete mode 100644 bun.lock diff --git a/bun.lock b/bun.lock deleted file mode 100644 index 8eddad1..0000000 --- a/bun.lock +++ /dev/null @@ -1,583 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "name": "trace-capacity-visualizer", - "dependencies": { - "tsup": "^8.5.1", - }, - "devDependencies": { - "@types/bun": "latest", - "@types/three": "^0.170.0", - "react": "^19.2.0", - "react-cosmos": "^7.0.0", - "react-cosmos-plugin-vite": "^7.0.0", - "react-dom": "^19.2.0", - "three": "^0.171.0", - "vite": "^7.2.2", - }, - "peerDependencies": { - "typescript": "^5", - }, - }, - }, - "packages": { - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.0", "", { "os": "android", "cpu": "arm" }, "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.0", "", { "os": "android", "cpu": "arm64" }, "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.0", "", { "os": "android", "cpu": "x64" }, "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, ""], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.0", "", { "os": "none", "cpu": "x64" }, "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg=="], - - "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, ""], - - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - - "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, ""], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.53.2", "", { "os": "linux", "cpu": "x64" }, ""], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.53.2", "", { "os": "linux", "cpu": "x64" }, ""], - - "@skidding/launch-editor": ["@skidding/launch-editor@2.2.3", "", { "dependencies": { "chalk": "^2.3.0", "shell-quote": "^1.6.1" } }, ""], - - "@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="], - - "@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, ""], - - "@types/http-proxy": ["@types/http-proxy@1.17.17", "", { "dependencies": { "@types/node": "*" } }, ""], - - "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, ""], - - "@types/react": ["@types/react@19.2.5", "", { "dependencies": { "csstype": "^3.0.2" } }, ""], - - "@types/stats.js": ["@types/stats.js@0.17.4", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="], - - "@types/three": ["@types/three@0.170.0", "", { "dependencies": { "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": "*", "@webgpu/types": "*", "fflate": "~0.8.2", "meshoptimizer": "~0.18.1" } }, "sha512-CUm2uckq+zkCY7ZbFpviRttY+6f9fvwm6YqSqPfA5K22s9w7R4VnA3rzJse8kHVvuzLcTx+CjNCs2NYe0QFAyg=="], - - "@types/webxr": ["@types/webxr@0.5.24", "", {}, "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg=="], - - "@webgpu/types": ["@webgpu/types@0.1.66", "", {}, "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA=="], - - "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, ""], - - "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], - - "ansi-regex": ["ansi-regex@5.0.1", "", {}, ""], - - "ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, ""], - - "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], - - "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, ""], - - "array-flatten": ["array-flatten@1.1.1", "", {}, ""], - - "balanced-match": ["balanced-match@1.0.2", "", {}, ""], - - "binary-extensions": ["binary-extensions@2.3.0", "", {}, ""], - - "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, ""], - - "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, ""], - - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, ""], - - "bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, ""], - - "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, ""], - - "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="], - - "bytes": ["bytes@3.1.2", "", {}, ""], - - "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], - - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, ""], - - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, ""], - - "chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, ""], - - "charenc": ["charenc@0.0.2", "", {}, ""], - - "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" } }, ""], - - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, ""], - - "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, ""], - - "color-name": ["color-name@1.1.3", "", {}, ""], - - "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - - "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], - - "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], - - "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, ""], - - "content-type": ["content-type@1.0.5", "", {}, ""], - - "cookie": ["cookie@0.7.1", "", {}, ""], - - "cookie-signature": ["cookie-signature@1.0.6", "", {}, ""], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, ""], - - "crypt": ["crypt@0.0.2", "", {}, ""], - - "csstype": ["csstype@3.2.3", "", {}, ""], - - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, ""], - - "default-browser-id": ["default-browser-id@5.0.1", "", {}, ""], - - "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, ""], - - "depd": ["depd@2.0.0", "", {}, ""], - - "destroy": ["destroy@1.2.0", "", {}, ""], - - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, ""], - - "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, ""], - - "ee-first": ["ee-first@1.1.1", "", {}, ""], - - "emoji-regex": ["emoji-regex@8.0.0", "", {}, ""], - - "encodeurl": ["encodeurl@2.0.0", "", {}, ""], - - "entities": ["entities@6.0.1", "", {}, ""], - - "es-define-property": ["es-define-property@1.0.1", "", {}, ""], - - "es-errors": ["es-errors@1.3.0", "", {}, ""], - - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, ""], - - "es6-promisify": ["es6-promisify@7.0.0", "", {}, ""], - - "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/linux-x64": "0.25.12" }, "bin": "bin/esbuild" }, ""], - - "escalade": ["escalade@3.2.0", "", {}, ""], - - "escape-html": ["escape-html@1.0.3", "", {}, ""], - - "escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, ""], - - "etag": ["etag@1.8.1", "", {}, ""], - - "eventemitter3": ["eventemitter3@4.0.7", "", {}, ""], - - "express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, ""], - - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, ""], - - "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], - - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, ""], - - "finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, ""], - - "fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="], - - "follow-redirects": ["follow-redirects@1.15.11", "", {}, ""], - - "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, ""], - - "forwarded": ["forwarded@0.2.0", "", {}, ""], - - "fresh": ["fresh@0.5.2", "", {}, ""], - - "function-bind": ["function-bind@1.1.2", "", {}, ""], - - "get-caller-file": ["get-caller-file@2.0.5", "", {}, ""], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, ""], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, ""], - - "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, ""], - - "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, ""], - - "gopd": ["gopd@1.2.0", "", {}, ""], - - "has-flag": ["has-flag@3.0.0", "", {}, ""], - - "has-symbols": ["has-symbols@1.1.0", "", {}, ""], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, ""], - - "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, ""], - - "http-proxy": ["http-proxy@1.18.1", "", { "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" } }, ""], - - "http-proxy-middleware": ["http-proxy-middleware@2.0.9", "", { "dependencies": { "@types/http-proxy": "^1.17.8", "http-proxy": "^1.18.1", "is-glob": "^4.0.1", "is-plain-obj": "^3.0.0", "micromatch": "^4.0.2" }, "peerDependencies": { "@types/express": "^4.17.13" }, "optionalPeers": ["@types/express"] }, ""], - - "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, ""], - - "inherits": ["inherits@2.0.4", "", {}, ""], - - "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, ""], - - "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, ""], - - "is-buffer": ["is-buffer@1.1.6", "", {}, ""], - - "is-docker": ["is-docker@3.0.0", "", { "bin": "cli.js" }, ""], - - "is-extglob": ["is-extglob@2.1.1", "", {}, ""], - - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, ""], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, ""], - - "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": "cli.js" }, ""], - - "is-number": ["is-number@7.0.0", "", {}, ""], - - "is-plain-obj": ["is-plain-obj@3.0.0", "", {}, ""], - - "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, ""], - - "isexe": ["isexe@2.0.0", "", {}, ""], - - "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, ""], - - "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], - - "js-base64": ["js-base64@3.7.7", "", {}, ""], - - "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], - - "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], - - "load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="], - - "lodash-es": ["lodash-es@4.17.21", "", {}, ""], - - "lru-cache": ["lru-cache@10.4.3", "", {}, ""], - - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, ""], - - "md5": ["md5@2.3.0", "", { "dependencies": { "charenc": "0.0.2", "crypt": "0.0.2", "is-buffer": "~1.1.6" } }, ""], - - "media-typer": ["media-typer@0.3.0", "", {}, ""], - - "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, ""], - - "meshoptimizer": ["meshoptimizer@0.18.1", "", {}, "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw=="], - - "methods": ["methods@1.1.2", "", {}, ""], - - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, ""], - - "mime": ["mime@1.6.0", "", { "bin": "cli.js" }, ""], - - "mime-db": ["mime-db@1.52.0", "", {}, ""], - - "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, ""], - - "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, ""], - - "minipass": ["minipass@7.1.2", "", {}, ""], - - "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], - - "ms": ["ms@2.1.3", "", {}, ""], - - "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, ""], - - "negotiator": ["negotiator@0.6.3", "", {}, ""], - - "normalize-path": ["normalize-path@3.0.0", "", {}, ""], - - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - - "object-inspect": ["object-inspect@1.13.4", "", {}, ""], - - "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, ""], - - "open": ["open@10.1.1", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "is-wsl": "^3.1.0" } }, ""], - - "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, ""], - - "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, ""], - - "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, ""], - - "parseurl": ["parseurl@1.3.3", "", {}, ""], - - "path-key": ["path-key@3.1.1", "", {}, ""], - - "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, ""], - - "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, ""], - - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - - "pem": ["pem@1.14.8", "", { "dependencies": { "es6-promisify": "^7.0.0", "md5": "^2.3.0", "os-tmpdir": "^1.0.2", "which": "^2.0.2" } }, ""], - - "picocolors": ["picocolors@1.1.1", "", {}, ""], - - "picomatch": ["picomatch@4.0.3", "", {}, ""], - - "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], - - "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], - - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, ""], - - "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], - - "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, ""], - - "qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, ""], - - "range-parser": ["range-parser@1.2.1", "", {}, ""], - - "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, ""], - - "react": ["react@19.2.0", "", {}, ""], - - "react-cosmos": ["react-cosmos@7.0.0", "", { "dependencies": { "@skidding/launch-editor": "2.2.3", "chokidar": "3.6.0", "express": "4.21.2", "glob": "10.4.5", "http-proxy-middleware": "2.0.9", "lodash-es": "4.17.21", "micromatch": "4.0.8", "open": "10.1.1", "pem": "1.14.8", "react-cosmos-core": "^7.0.0", "react-cosmos-renderer": "^7.0.0", "react-cosmos-ui": "^7.0.0", "ws": "8.18.1", "yargs": "17.7.2" }, "bin": { "cosmos": "bin/cosmos.js", "cosmos-export": "bin/cosmos-export.js", "cosmos-native": "bin/cosmos-native.js" } }, ""], - - "react-cosmos-core": ["react-cosmos-core@7.0.0", "", { "dependencies": { "js-base64": "3.7.7", "lodash-es": "4.17.21" } }, ""], - - "react-cosmos-dom": ["react-cosmos-dom@7.0.0", "", { "dependencies": { "lodash-es": "4.17.21", "react-cosmos-core": "^7.0.0", "react-cosmos-renderer": "^7.0.0" } }, ""], - - "react-cosmos-plugin-vite": ["react-cosmos-plugin-vite@7.0.0", "", { "dependencies": { "parse5": "7.3.0", "react-cosmos-core": "^7.0.0", "react-cosmos-dom": "^7.0.0" }, "peerDependencies": { "vite": "*" } }, ""], - - "react-cosmos-renderer": ["react-cosmos-renderer@7.0.0", "", { "dependencies": { "lodash-es": "4.17.21", "react-cosmos-core": "^7.0.0" } }, ""], - - "react-cosmos-ui": ["react-cosmos-ui@7.0.0", "", { "dependencies": { "lodash-es": "4.17.21", "react-cosmos-core": "^7.0.0" } }, ""], - - "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, ""], - - "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, ""], - - "require-directory": ["require-directory@2.1.1", "", {}, ""], - - "requires-port": ["requires-port@1.0.0", "", {}, ""], - - "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], - - "rollup": ["rollup@4.53.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "4.53.2", "@rollup/rollup-linux-x64-musl": "4.53.2" }, "bin": "dist/bin/rollup" }, ""], - - "run-applescript": ["run-applescript@7.1.0", "", {}, ""], - - "safe-buffer": ["safe-buffer@5.2.1", "", {}, ""], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, ""], - - "scheduler": ["scheduler@0.27.0", "", {}, ""], - - "send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, ""], - - "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, ""], - - "setprototypeof": ["setprototypeof@1.2.0", "", {}, ""], - - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, ""], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, ""], - - "shell-quote": ["shell-quote@1.8.3", "", {}, ""], - - "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, ""], - - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, ""], - - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, ""], - - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, ""], - - "signal-exit": ["signal-exit@4.1.0", "", {}, ""], - - "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, ""], - - "statuses": ["statuses@2.0.1", "", {}, ""], - - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, ""], - - "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, ""], - - "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, ""], - - "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, ""], - - "sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="], - - "supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, ""], - - "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], - - "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], - - "three": ["three@0.171.0", "", {}, "sha512-Y/lAXPaKZPcEdkKjh0JOAHVv8OOnv/NDJqm0wjfCzyQmfKxV7zvkwsnBgPBKTzJHToSOhRGQAGbPJObT59B/PQ=="], - - "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], - - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, ""], - - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, ""], - - "toidentifier": ["toidentifier@1.0.1", "", {}, ""], - - "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], - - "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], - - "tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="], - - "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, ""], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, ""], - - "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], - - "undici-types": ["undici-types@7.16.0", "", {}, ""], - - "unpipe": ["unpipe@1.0.0", "", {}, ""], - - "utils-merge": ["utils-merge@1.0.1", "", {}, ""], - - "vary": ["vary@1.1.2", "", {}, ""], - - "vite": ["vite@7.2.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": "bin/vite.js" }, ""], - - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, ""], - - "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, ""], - - "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, ""], - - "ws": ["ws@8.18.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, ""], - - "y18n": ["y18n@5.0.8", "", {}, ""], - - "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, ""], - - "yargs-parser": ["yargs-parser@21.1.1", "", {}, ""], - - "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, ""], - - "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, ""], - - "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, ""], - - "anymatch/picomatch": ["picomatch@2.3.1", "", {}, ""], - - "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, ""], - - "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, ""], - - "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, ""], - - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, ""], - - "readdirp/picomatch": ["picomatch@2.3.1", "", {}, ""], - - "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, ""], - - "send/encodeurl": ["encodeurl@1.0.2", "", {}, ""], - - "tsup/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], - - "tsup/esbuild": ["esbuild@0.27.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.0", "@esbuild/android-arm": "0.27.0", "@esbuild/android-arm64": "0.27.0", "@esbuild/android-x64": "0.27.0", "@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-x64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-x64": "0.27.0", "@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-x64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-x64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-x64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.0", "@esbuild/sunos-x64": "0.27.0", "@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-x64": "0.27.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA=="], - - "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, ""], - - "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, ""], - - "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, ""], - - "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, ""], - - "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, ""], - - "body-parser/debug/ms": ["ms@2.0.0", "", {}, ""], - - "express/debug/ms": ["ms@2.0.0", "", {}, ""], - - "finalhandler/debug/ms": ["ms@2.0.0", "", {}, ""], - - "send/debug/ms": ["ms@2.0.0", "", {}, ""], - - "tsup/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], - - "tsup/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.0", "", { "os": "linux", "cpu": "x64" }, "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw=="], - - "wrap-ansi-cjs/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, ""], - - "wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, ""], - - "wrap-ansi-cjs/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, ""], - - "wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, ""], - } -} From 1f03dcf83d82e371300d8be0c0187c5b6bfe16fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Tue, 18 Nov 2025 11:59:30 +0530 Subject: [PATCH 12/14] WIP --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index bcf622c..0898819 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,5 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json .DS_Store package-lock.json .vercel -cosmos-export \ No newline at end of file +cosmos-export +bun.lock \ No newline at end of file From 3e8eb454857c16b8c659386993e83a69b151b39b Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Mon, 17 Nov 2025 23:54:57 -0800 Subject: [PATCH 13/14] Delete AGENTS.md --- AGENTS.md | 919 ------------------------------------------------------ 1 file changed, 919 deletions(-) delete mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index f7e425e..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,919 +0,0 @@ -a new compoenent called `` and we are using `*.page.tsx` files to view exmaples please create an example page and setup code to be organized and tidy - - -check out ./test-assets/example01.json for example data - -here are the reference files from the other project -``` -import React, { useCallback, useEffect, useMemo, useRef, useState } from "react" -import { GenericSolverDebugger } from "@tscircuit/solver-utils/react" -import type { SimpleRouteJson } from "../lib/types/srj-types" -import type { CapacityMeshNode } from "../lib/types/capacity-mesh-types" -import * as THREE from "three" -import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js" -import type { BaseSolver } from "@tscircuit/solver-utils" - -type SolverDebugger3dProps = { - solver: BaseSolver - /** Optional SRJ to show board bounds & rectangular obstacles */ - simpleRouteJson?: SimpleRouteJson - /** Visual Z thickness per layer (world units) */ - layerThickness?: number - /** Canvas height */ - height?: number - /** Initial toggles (user can change in UI) */ - defaultShowRoot?: boolean - defaultShowObstacles?: boolean - defaultShowOutput?: boolean - defaultWireframeOutput?: boolean - /** Wrap styles */ - style?: React.CSSProperties -} - -/* ----------------------------- helpers ----------------------------- */ - -function contiguousRuns(nums: number[]) { - const zs = [...new Set(nums)].sort((a, b) => a - b) - if (zs.length === 0) return [] as number[][] - const groups: number[][] = [] - let run: number[] = [zs[0]!] - for (let i = 1; i < zs.length; i++) { - if (zs[i] === zs[i - 1]! + 1) run.push(zs[i]!) - else { - groups.push(run) - run = [zs[i]!] - } - } - groups.push(run) - return groups -} - -/** Canonical layer order to mirror the solver & experiment */ -function layerSortKey(name: string) { - const n = name.toLowerCase() - if (n === "top") return -1_000_000 - if (n === "bottom") return 1_000_000 - const m = /^inner(\d+)$/i.exec(n) - if (m) return parseInt(m[1]!, 10) || 0 - return 100 + n.charCodeAt(0) -} -function canonicalizeLayerOrder(names: string[]) { - return [...new Set(names)].sort((a, b) => { - const ka = layerSortKey(a) - const kb = layerSortKey(b) - if (ka !== kb) return ka - kb - return a.localeCompare(b) - }) -} - -/** Build prisms by grouping identical XY nodes across contiguous Z */ -function buildPrismsFromNodes( - nodes: CapacityMeshNode[], - fallbackLayerCount: number, -): Array<{ - minX: number - maxX: number - minY: number - maxY: number - z0: number - z1: number -}> { - const xyKey = (n: CapacityMeshNode) => - `${n.center.x.toFixed(8)}|${n.center.y.toFixed(8)}|${n.width.toFixed(8)}|${n.height.toFixed(8)}` - const azKey = (n: CapacityMeshNode) => { - const zs = ( - n.availableZ && n.availableZ.length ? [...new Set(n.availableZ)] : [0] - ).sort((a, b) => a - b) - return `zset:${zs.join(",")}` - } - const key = (n: CapacityMeshNode) => `${xyKey(n)}|${azKey(n)}` - - const groups = new Map< - string, - { cx: number; cy: number; w: number; h: number; zs: number[] } - >() - for (const n of nodes) { - const k = key(n) - const zlist = n.availableZ?.length ? n.availableZ : [0] - const g = groups.get(k) - if (g) g.zs.push(...zlist) - else - groups.set(k, { - cx: n.center.x, - cy: n.center.y, - w: n.width, - h: n.height, - zs: [...zlist], - }) - } - - const prisms: Array<{ - minX: number - maxX: number - minY: number - maxY: number - z0: number - z1: number - }> = [] - for (const g of groups.values()) { - const minX = g.cx - g.w / 2 - const maxX = g.cx + g.w / 2 - const minY = g.cy - g.h / 2 - const maxY = g.cy + g.h / 2 - const runs = contiguousRuns(g.zs) - if (runs.length === 0) { - prisms.push({ - minX, - maxX, - minY, - maxY, - z0: 0, - z1: Math.max(1, fallbackLayerCount), - }) - } else { - for (const r of runs) { - prisms.push({ - minX, - maxX, - minY, - maxY, - z0: r[0]!, - z1: r[r.length - 1]! + 1, - }) - } - } - } - return prisms -} - -function clamp01(x: number) { - return Math.max(0, Math.min(1, x)) -} - -function darkenColor(hex: number, factor = 0.6): number { - const r = ((hex >> 16) & 0xff) * factor - const g = ((hex >> 8) & 0xff) * factor - const b = (hex & 0xff) * factor - const cr = Math.max(0, Math.min(255, Math.round(r))) - const cg = Math.max(0, Math.min(255, Math.round(g))) - const cb = Math.max(0, Math.min(255, Math.round(b))) - return (cr << 16) | (cg << 8) | cb -} - -/* ---------------------------- 3D Canvas ---------------------------- */ - -const ThreeBoardView: React.FC<{ - nodes: CapacityMeshNode[] - srj?: SimpleRouteJson - layerThickness: number - height: number - showRoot: boolean - showObstacles: boolean - showOutput: boolean - wireframeOutput: boolean - meshOpacity: number - shrinkBoxes: boolean - boxShrinkAmount: number - showBorders: boolean -}> = ({ - nodes, - srj, - layerThickness, - height, - showRoot, - showObstacles, - showOutput, - wireframeOutput, - meshOpacity, - shrinkBoxes, - boxShrinkAmount, - showBorders, -}) => { - const containerRef = useRef(null) - const destroyRef = useRef<() => void>(() => {}) - - const layerNames = useMemo(() => { - // Build from nodes (preferred, matches solver) and fall back to SRJ obstacle names - const fromNodes = canonicalizeLayerOrder(nodes.map((n) => n.layer)) - if (fromNodes.length) return fromNodes - const fromObs = canonicalizeLayerOrder( - (srj?.obstacles ?? []).flatMap((o) => o.layers ?? []), - ) - return fromObs.length ? fromObs : ["top"] - }, [nodes, srj]) - - const zIndexByLayerName = useMemo(() => { - const m = new Map() - layerNames.forEach((n, i) => m.set(n, i)) - return m - }, [layerNames]) - - const layerCount = layerNames.length || srj?.layerCount || 1 - - const prisms = useMemo( - () => buildPrismsFromNodes(nodes, layerCount), - [nodes, layerCount], - ) - - useEffect(() => { - let mounted = true - ;(async () => { - const el = containerRef.current - if (!el) return - if (!mounted) return - - destroyRef.current?.() - - const w = el.clientWidth || 800 - const h = el.clientHeight || height - - const renderer = new THREE.WebGLRenderer({ - antialias: true, - alpha: true, - premultipliedAlpha: false, - }) - // Increase pixel ratio for better alphaHash quality - renderer.setPixelRatio(window.devicePixelRatio) - renderer.setSize(w, h) - el.innerHTML = "" - el.appendChild(renderer.domElement) - - const scene = new THREE.Scene() - scene.background = new THREE.Color(0xf7f8fa) - - const camera = new THREE.PerspectiveCamera(45, w / h, 0.1, 10000) - camera.position.set(80, 80, 120) - - const controls = new OrbitControls(camera, renderer.domElement) - controls.enableDamping = true - - const amb = new THREE.AmbientLight(0xffffff, 0.9) - scene.add(amb) - const dir = new THREE.DirectionalLight(0xffffff, 0.6) - dir.position.set(1, 2, 3) - scene.add(dir) - - const rootGroup = new THREE.Group() - const obstaclesGroup = new THREE.Group() - const outputGroup = new THREE.Group() - scene.add(rootGroup, obstaclesGroup, outputGroup) - - // Axes helper for orientation (similar to experiment) - const axes = new THREE.AxesHelper(50) - scene.add(axes) - - const colorRoot = 0x111827 - const colorOb = 0xef4444 - - // Palette for layer-span-based coloring - const spanPalette = [ - 0x0ea5e9, // cyan-ish - 0x22c55e, // green - 0xf97316, // orange - 0xa855f7, // purple - 0xfacc15, // yellow - 0x38bdf8, // light blue - 0xec4899, // pink - 0x14b8a6, // teal - ] - const spanColorMap = new Map() - let spanColorIndex = 0 - const getSpanColor = (z0: number, z1: number) => { - const key = `${z0}-${z1}` - let c = spanColorMap.get(key) - if (c == null) { - c = spanPalette[spanColorIndex % spanPalette.length]! - spanColorMap.set(key, c) - spanColorIndex++ - } - return c - } - - function makeBoxMesh( - b: { - minX: number - maxX: number - minY: number - maxY: number - z0: number - z1: number - }, - color: number, - wire: boolean, - opacity = 0.45, - borders = false, - ) { - const dx = b.maxX - b.minX - const dz = b.maxY - b.minY // map board Y -> three Z - const dy = (b.z1 - b.z0) * layerThickness - const cx = -((b.minX + b.maxX) / 2) // negate X to match expected orientation - const cz = (b.minY + b.maxY) / 2 - // Negate Y so z=0 is at top, higher z goes down - const cy = -((b.z0 + b.z1) / 2) * layerThickness - - const geom = new THREE.BoxGeometry(dx, dy, dz) - if (wire) { - const edges = new THREE.EdgesGeometry(geom) - const line = new THREE.LineSegments( - edges, - new THREE.LineBasicMaterial({ color }), - ) - line.position.set(cx, cy, cz) - return line - } - const clampedOpacity = clamp01(opacity) - const mat = new THREE.MeshPhongMaterial({ - color, - opacity: clampedOpacity, - transparent: clampedOpacity < 1, - alphaHash: clampedOpacity < 1, - alphaToCoverage: true, - }) - - const mesh = new THREE.Mesh(geom, mat) - mesh.position.set(cx, cy, cz) - - if (!borders) return mesh - - const edges = new THREE.EdgesGeometry(geom) - const borderColor = darkenColor(color, 0.6) - const line = new THREE.LineSegments( - edges, - new THREE.LineBasicMaterial({ color: borderColor }), - ) - line.position.set(cx, cy, cz) - - const group = new THREE.Group() - group.add(mesh) - group.add(line) - return group - } - - // Root wireframe from SRJ bounds - if (srj && showRoot) { - const rootBox = { - minX: srj.bounds.minX, - maxX: srj.bounds.maxX, - minY: srj.bounds.minY, - maxY: srj.bounds.maxY, - z0: 0, - z1: layerCount, - } - rootGroup.add(makeBoxMesh(rootBox, colorRoot, true, 1)) - } - - // Obstacles — rectangular only — one slab per declared layer - if (srj && showObstacles) { - for (const ob of srj.obstacles ?? []) { - if (ob.type !== "rect") continue - const minX = ob.center.x - ob.width / 2 - const maxX = ob.center.x + ob.width / 2 - const minY = ob.center.y - ob.height / 2 - const maxY = ob.center.y + ob.height / 2 - - // Prefer explicit zLayers; otherwise map layer names to indices - const zs = - ob.zLayers && ob.zLayers.length - ? [...new Set(ob.zLayers)] - : (ob.layers ?? []) - .map((name) => zIndexByLayerName.get(name)) - .filter((z): z is number => typeof z === "number") - - for (const z of zs) { - if (z < 0 || z >= layerCount) continue - obstaclesGroup.add( - makeBoxMesh( - { minX, maxX, minY, maxY, z0: z, z1: z + 1 }, - colorOb, - false, - 0.35, - false, - ), - ) - } - } - } - - // Output prisms from nodes (wireframe toggle like the experiment) - if (showOutput) { - for (const p of prisms) { - let box = p - if (shrinkBoxes && boxShrinkAmount > 0) { - const s = boxShrinkAmount - - const widthX = p.maxX - p.minX - const widthY = p.maxY - p.minY - - // Never shrink more on a side than allowed by the configured shrink amount - // while ensuring we don't shrink past a minimum dimension of "s" - const maxShrinkEachSideX = Math.max(0, (widthX - s) / 2) - const maxShrinkEachSideY = Math.max(0, (widthY - s) / 2) - - const shrinkX = Math.min(s, maxShrinkEachSideX) - const shrinkY = Math.min(s, maxShrinkEachSideY) - - const minX = p.minX + shrinkX - const maxX = p.maxX - shrinkX - const minY = p.minY + shrinkY - const maxY = p.maxY - shrinkY - - // Guard against any degenerate box - if (minX >= maxX || minY >= maxY) { - continue - } - - box = { ...p, minX, maxX, minY, maxY } - } - - const color = getSpanColor(p.z0, p.z1) - outputGroup.add( - makeBoxMesh( - box, - color, - wireframeOutput, - meshOpacity, - showBorders && !wireframeOutput, - ), - ) - } - } - - // Fit camera - const fitBox = srj - ? { - minX: srj.bounds.minX, - maxX: srj.bounds.maxX, - minY: srj.bounds.minY, - maxY: srj.bounds.maxY, - z0: 0, - z1: layerCount, - } - : (() => { - if (prisms.length === 0) { - return { - minX: -10, - maxX: 10, - minY: -10, - maxY: 10, - z0: 0, - z1: layerCount, - } - } - let minX = Infinity, - minY = Infinity, - maxX = -Infinity, - maxY = -Infinity - for (const p of prisms) { - minX = Math.min(minX, p.minX) - maxX = Math.max(maxX, p.maxX) - minY = Math.min(minY, p.minY) - maxY = Math.max(maxY, p.maxY) - } - return { minX, maxX, minY, maxY, z0: 0, z1: layerCount } - })() - - const dx = fitBox.maxX - fitBox.minX - const dz = fitBox.maxY - fitBox.minY - const dy = (fitBox.z1 - fitBox.z0) * layerThickness - const size = Math.max(dx, dz, dy) - const dist = size * 2.0 - // Camera looks from above-right-front, with negative Y being "up" (z=0 at top) - camera.position.set( - -(fitBox.maxX + dist * 0.6), // negate X to account for flipped axis - -dy / 2 + dist, // negative Y is up, so position above the center - fitBox.maxY + dist * 0.6, - ) - camera.near = Math.max(0.1, size / 100) - camera.far = dist * 10 + size * 10 - camera.updateProjectionMatrix() - controls.target.set( - -((fitBox.minX + fitBox.maxX) / 2), // negate X to account for flipped axis - -dy / 2, // center of the inverted Y range - (fitBox.minY + fitBox.maxY) / 2, - ) - controls.update() - - const onResize = () => { - const W = el.clientWidth || w - const H = el.clientHeight || h - camera.aspect = W / H - camera.updateProjectionMatrix() - renderer.setSize(W, H) - } - window.addEventListener("resize", onResize) - - let raf = 0 - const animate = () => { - raf = requestAnimationFrame(animate) - controls.update() - renderer.render(scene, camera) - } - animate() - - destroyRef.current = () => { - cancelAnimationFrame(raf) - window.removeEventListener("resize", onResize) - renderer.dispose() - el.innerHTML = "" - } - })() - - return () => { - mounted = false - destroyRef.current?.() - } - }, [ - srj, - prisms, - layerCount, - layerThickness, - height, - showRoot, - showObstacles, - showOutput, - wireframeOutput, - zIndexByLayerName, - meshOpacity, - shrinkBoxes, - boxShrinkAmount, - showBorders, - ]) - - return ( -
- ) -} - -/* ----------------------- Public wrapper component ----------------------- */ - -export const SolverDebugger3d: React.FC = ({ - solver, - simpleRouteJson, - layerThickness = 1, - height = 600, - defaultShowRoot = true, - defaultShowObstacles = false, // don't show obstacles by default - defaultShowOutput = true, - defaultWireframeOutput = false, - style, -}) => { - const [show3d, setShow3d] = useState(false) - const [rebuildKey, setRebuildKey] = useState(0) - - const [showRoot, setShowRoot] = useState(defaultShowRoot) - const [showObstacles, setShowObstacles] = useState(defaultShowObstacles) - const [showOutput, setShowOutput] = useState(defaultShowOutput) - const [wireframeOutput, setWireframeOutput] = useState(defaultWireframeOutput) - - const [meshOpacity, setMeshOpacity] = useState(0.6) - const [shrinkBoxes, setShrinkBoxes] = useState(true) - const [boxShrinkAmount, setBoxShrinkAmount] = useState(0.1) - const [showBorders, setShowBorders] = useState(true) - - // Mesh nodes state - updated when solver completes or during stepping - const [meshNodes, setMeshNodes] = useState([]) - - // Update mesh nodes from solver output - const updateMeshNodes = useCallback(() => { - try { - const output = solver.getOutput() - const nodes = output.meshNodes ?? [] - setMeshNodes(nodes) - } catch { - setMeshNodes([]) - } - }, [solver]) - - // Initialize mesh nodes on mount (in case solver is already solved) - useEffect(() => { - updateMeshNodes() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - // Handle solver completion - const handleSolverCompleted = useCallback(() => { - updateMeshNodes() - }, [updateMeshNodes]) - - // Poll for updates during stepping (GenericSolverDebugger doesn't have onStep) - useEffect(() => { - const interval = setInterval(() => { - // Only update if solver has output available - if (solver.solved || (solver as any).stats?.placed > 0) { - updateMeshNodes() - } - }, 100) // Poll every 100ms during active solving - - return () => clearInterval(interval) - }, [updateMeshNodes, solver]) - - const toggle3d = useCallback(() => setShow3d((s) => !s), []) - const rebuild = useCallback(() => setRebuildKey((k) => k + 1), []) - - return ( - <> -
- - -
- - {show3d && ( - - )} - - {/* experiment-like toggles */} - - - - - - {/* Mesh opacity slider */} - {show3d && ( - - )} - - {/* Shrink boxes option */} - {show3d && ( - <> - - {shrinkBoxes && ( - - )} - - )} - - {/* Show borders option */} - {show3d && ( - - )} - -
- Drag to orbit · Wheel to zoom · Right-drag to pan -
-
- - {show3d && ( - - )} -
- - {/* White margin at bottom of the page */} -
- - ) -} -``` - -``` -export type CapacityMeshNodeId = string - -export interface CapacityMesh { - nodes: CapacityMeshNode[] - edges: CapacityMeshEdge[] -} - -export interface CapacityMeshNode { - capacityMeshNodeId: string - center: { x: number; y: number } - width: number - height: number - layer: string - availableZ: number[] - - _depth?: number - - _completelyInsideObstacle?: boolean - _containsObstacle?: boolean - _containsTarget?: boolean - _targetConnectionName?: string - _strawNode?: boolean - _strawParentCapacityMeshNodeId?: CapacityMeshNodeId - - _adjacentNodeIds?: CapacityMeshNodeId[] - - _parent?: CapacityMeshNode -} - -export interface CapacityMeshEdge { - capacityMeshEdgeId: string - nodeIds: [CapacityMeshNodeId, CapacityMeshNodeId] -} -``` - - -``` -export type TraceId = string - -export interface SimpleRouteJson { - layerCount: number - minTraceWidth: number - minViaDiameter?: number - obstacles: Obstacle[] - connections: Array - bounds: { minX: number; maxX: number; minY: number; maxY: number } - outline?: Array<{ x: number; y: number }> -} - -export interface Obstacle { - type: "rect" - layers: string[] - zLayers?: number[] - center: { x: number; y: number } - width: number - height: number - connectedTo: TraceId[] - netIsAssignable?: boolean - offBoardConnectsTo?: TraceId[] -} - -export interface SimpleRouteConnection { - name: string - netConnectionName?: string - nominalTraceWidth?: number - pointsToConnect: Array<{ - x: number - y: number - layer: string - pointId?: string - pcb_port_id?: string - }> - externallyConnectedPointIds?: string[][] -} -``` - From 9604f17b65d5e9f9fca356dc20e8e40ade22d69b Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Mon, 17 Nov 2025 23:55:11 -0800 Subject: [PATCH 14/14] Delete IMPLEMENTATION_SUMMARY.md --- IMPLEMENTATION_SUMMARY.md | 126 -------------------------------------- 1 file changed, 126 deletions(-) delete mode 100644 IMPLEMENTATION_SUMMARY.md diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index 95eb700..0000000 --- a/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,126 +0,0 @@ -# CapacityNode3dDebugger Implementation Summary - -## Overview -Successfully implemented the `CapacityNode3dDebugger` component as requested, pulling code from the reference project and organizing it in a clean, modular structure. - -## Files Created - -### Core Component -- **`lib/CapacityNode3dDebugger.tsx`** - Main 3D visualization component with full functionality -- **`lib/types.ts`** - TypeScript type definitions for all data structures -- **`lib/index.ts`** - Export barrel for clean imports - -### Example and Documentation -- **`pages/example01.page.tsx`** - Complete example page that loads data from test-assets -- **`lib/README.md`** - Comprehensive documentation with usage examples -- **`test.html`** - Simple test page for verification -- **`IMPLEMENTATION_SUMMARY.md`** - This summary document - -### Configuration Updates -- **`package.json`** - Added Three.js dependencies (`three` and `@types/three`) -- **`tsconfig.json`** - Updated TypeScript configuration for better compatibility - -## Key Features Implemented - -### 3D Visualization -- Interactive 3D scene using Three.js -- WebGL renderer with antialiasing -- OrbitControls for camera manipulation -- Proper lighting setup (ambient + directional) - -### Node Visualization -- Converts capacity mesh nodes to 3D prisms -- Groups identical XY nodes across contiguous Z layers -- Layer-span-based coloring with palette -- Optional wireframe rendering -- Configurable opacity and transparency - -### Interactive Controls -- Show/Hide 3D toggle -- Rebuild 3D scene button -- Toggle options: Root, Obstacles, Output, Wireframe -- Opacity slider (0-1 range) -- Box shrinking with configurable amount -- Border highlighting option - -### Data Handling -- Loads example data from `/test-assets/example01.json` -- Calculates bounds from node data -- Supports optional SimpleRouteJson for obstacles -- Proper TypeScript typing throughout - -### User Experience -- Clean, organized UI with intuitive controls -- Responsive design with proper styling -- Loading states and error handling -- Usage instructions and documentation -- Mouse control instructions - -## Technical Details - -### Dependencies -- **React** - Component framework -- **Three.js** - 3D graphics library -- **@types/three** - TypeScript definitions for Three.js - -### Architecture -- Functional React components with hooks -- Memoized calculations for performance -- Proper cleanup with useEffect -- TypeScript for type safety - -### Code Organization -- Modular structure with separate files for types and main component -- Clean imports/exports via barrel file -- Comprehensive documentation -- Example implementation - -## Usage Example - -```tsx -import { CapacityNode3dDebugger } from '../lib' -import type { CapacityMeshNode } from '../lib' - -const nodes: CapacityMeshNode[] = [ - { - capacityMeshNodeId: "node1", - center: { x: 0, y: 0 }, - width: 10, - height: 10, - layer: "top", - availableZ: [0, 1, 2] - } - // ... more nodes -] - -function App() { - return ( - - ) -} -``` - -## Testing and Verification - -✅ **TypeScript Compilation** - All components compile without errors -✅ **Dependencies** - Three.js and types properly installed -✅ **Data Loading** - Example page loads data from test-assets -✅ **Component Structure** - Clean, modular organization -✅ **Documentation** - Comprehensive README and examples - -## Next Steps - -1. Start the development server: `npm start` -2. Navigate to the example page in the browser -3. Click "Show 3D" to render the visualization -4. Interact with the 3D scene using mouse controls -5. Experiment with different visualization options - -The implementation is complete and ready for use! \ No newline at end of file