Skip to content

Conversation

@saurabhraghuvanshii
Copy link

/claim #534
fixes: #534

Enhanced Texture Generation Pipeline:

Circuit JSON → circuit-to-svg → SVG → resvg-wasm → PNG → Three.js Texture

New Features Added:

  • Quality Presets - low, medium, high quality options
  • Texture Caching - LRU cache to avoid regenerating identical textures
  • Better Fallback - More realistic PCB patterns when resvg-wasm fails

Visual Enhancements:

  • Realistic PCB Pattern - Copper traces, gold pads, brown vias
  • Better Canvas Fallback - More detailed PCB-like patterns
  • Optimized Texture Settings - Better visual quality

@vercel
Copy link

vercel bot commented Oct 23, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
3d-viewer Ready Ready Preview Comment Oct 25, 2025 8:02pm

Comment on lines 1 to 37
import { useState, useEffect } from "react"
import * as THREE from "three"
import type { AnyCircuitElement } from "circuit-json"
import { createTopSideTexture } from "../utils/circuit-to-texture"

export function useCircuitTexture(
circuitJson: AnyCircuitElement[] | undefined,
enabled: boolean = true
): THREE.Texture | null {
const [texture, setTexture] = useState<THREE.Texture | null>(null)
const [isLoading, setIsLoading] = useState(false)

useEffect(() => {
if (!enabled || !circuitJson) {
setTexture(null)
return
}

setIsLoading(true)
createTopSideTexture(circuitJson, {
width: 1024,
height: 1024,
backgroundColor: "#ffffff",
})
.then((newTexture) => {
setTexture(newTexture)
setIsLoading(false)
})
.catch((error) => {
console.warn('Failed to create circuit texture:', error)
setTexture(null)
setIsLoading(false)
})
}, [circuitJson, enabled])

return texture
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File names should be consistent with project. Generally kebab-case or should match at least one export name. The file name useCircuitTexture.ts does not follow the project's kebab-case naming convention. Looking at other hook files in the project like use-convert-children-to-soup, use-stls-from-geom, and useBoardGeomBuilder, there's a mix of kebab-case and camelCase. However, the export name is useCircuitTexture which matches the filename, so this could be acceptable. But for consistency with the kebab-case pattern seen in other files, consider renaming to use-circuit-texture.ts.

Spotted by Graphite Agent (based on custom rule: Custom rule)

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Comment on lines 10 to 34
const [texture, setTexture] = useState<THREE.Texture | null>(null)
const [isLoading, setIsLoading] = useState(false)

useEffect(() => {
if (!enabled || !circuitJson) {
setTexture(null)
return
}

setIsLoading(true)
createTopSideTexture(circuitJson, {
width: 1024,
height: 1024,
backgroundColor: "#ffffff",
})
.then((newTexture) => {
setTexture(newTexture)
setIsLoading(false)
})
.catch((error) => {
console.warn('Failed to create circuit texture:', error)
setTexture(null)
setIsLoading(false)
})
}, [circuitJson, enabled])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The texture resources aren't being properly cleaned up when the component unmounts or when dependencies change. This can lead to memory leaks, especially in applications where components mount and unmount frequently.

Consider adding a cleanup function to the useEffect hook:

useEffect(() => {
    if (!enabled || !circuitJson) {
        setTexture(null);
        return;
    }

    setIsLoading(true);
    createTopSideTexture(circuitJson, {
        width: 1024,
        height: 1024,
        backgroundColor: "#ffffff",
    })
        .then((newTexture) => {
            setTexture(newTexture);
            setIsLoading(false);
        })
        .catch((error) => {
            console.warn('Failed to create circuit texture:', error);
            setTexture(null);
            setIsLoading(false);
        });
        
    // Cleanup function to dispose textures when unmounting or dependencies change
    return () => {
        if (texture) {
            texture.dispose();
            setTexture(null);
        }
    };
}, [circuitJson, enabled, texture]);

Note that texture should be added to the dependency array to ensure the cleanup function has access to the current texture.

Suggested change
const [texture, setTexture] = useState<THREE.Texture | null>(null)
const [isLoading, setIsLoading] = useState(false)
useEffect(() => {
if (!enabled || !circuitJson) {
setTexture(null)
return
}
setIsLoading(true)
createTopSideTexture(circuitJson, {
width: 1024,
height: 1024,
backgroundColor: "#ffffff",
})
.then((newTexture) => {
setTexture(newTexture)
setIsLoading(false)
})
.catch((error) => {
console.warn('Failed to create circuit texture:', error)
setTexture(null)
setIsLoading(false)
})
}, [circuitJson, enabled])
const [texture, setTexture] = useState<THREE.Texture | null>(null)
const [isLoading, setIsLoading] = useState(false)
const textureRef = useRef<THREE.Texture | null>(null)
useEffect(() => {
if (!enabled || !circuitJson) {
setTexture(null)
return
}
setIsLoading(true)
createTopSideTexture(circuitJson, {
width: 1024,
height: 1024,
backgroundColor: "#ffffff",
})
.then((newTexture) => {
// Dispose of the previous texture if it exists
if (textureRef.current) {
textureRef.current.dispose()
}
textureRef.current = newTexture
setTexture(newTexture)
setIsLoading(false)
})
.catch((error) => {
console.warn('Failed to create circuit texture:', error)
setTexture(null)
setIsLoading(false)
})
// Cleanup function to dispose textures when unmounting or dependencies change
return () => {
if (textureRef.current) {
textureRef.current.dispose()
textureRef.current = null
setTexture(null)
}
}
}, [circuitJson, enabled])

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Comment on lines 10 to 24
async function loadResvg(): Promise<any> {
if (resvgLoaded) return Resvg
resvgLoading = true
try {
const resvgModule = await import("resvg-wasm")
Resvg = resvgModule.Resvg
resvgLoaded = true
return Resvg
} catch (error) {
console.warn('Failed to load resvg-wasm:', error)
return null
} finally {
resvgLoading = false
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current implementation of loadResvg() has a potential race condition when multiple calls occur simultaneously. Consider using a promise-based loading mechanism to ensure the module is loaded only once:

let resvgLoadPromise = null;

async function loadResvg() {
  if (resvgLoaded) return Resvg;
  if (resvgLoadPromise) return resvgLoadPromise;
  
  resvgLoadPromise = import('resvg-wasm')
    .then(module => {
      Resvg = module.Resvg;
      resvgLoaded = true;
      return Resvg;
    })
    .catch(error => {
      console.warn('Failed to load resvg-wasm:', error);
      return null;
    });
    
  return resvgLoadPromise;
}

This approach eliminates the need for the resvgLoading flag while ensuring that concurrent calls wait for the same loading process to complete.

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Comment on lines +341 to +347
// Implement LRU eviction if cache is full
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value
if (firstKey) {
this.cache.delete(firstKey)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current implementation doesn't maintain true LRU (Least Recently Used) behavior. It simply removes the first key inserted into the cache, which is more FIFO than LRU. For proper LRU functionality, the implementation should track access order and evict the least recently accessed item.

A more accurate implementation would use a data structure that maintains access order, such as:

// Example of a more accurate LRU implementation
private cache = new Map<string, THREE.Texture>()
private accessOrder: string[] = []

get(key: string): THREE.Texture | null {
  const texture = this.cache.get(key)
  if (texture) {
    // Update access order
    this.accessOrder = this.accessOrder.filter(k => k !== key)
    this.accessOrder.push(key)
  }
  return texture || null
}

set(key: string, texture: THREE.Texture): void {
  if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
    // Remove least recently used item
    const lruKey = this.accessOrder.shift()
    if (lruKey) this.cache.delete(lruKey)
  }
  
  this.cache.set(key, texture)
  
  // Update access order
  this.accessOrder = this.accessOrder.filter(k => k !== key)
  this.accessOrder.push(key)
}

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Comment on lines +344 to +346
if (firstKey) {
this.cache.delete(firstKey)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When removing textures from the cache, Three.js textures should be properly disposed to free GPU memory. Consider adding texture.dispose() before deleting the key:

if (firstKey) {
  const texture = this.cache.get(firstKey);
  if (texture) {
    texture.dispose();
  }
  this.cache.delete(firstKey);
}

This prevents memory leaks by ensuring WebGL resources are properly released when textures are evicted from the cache.

Suggested change
if (firstKey) {
this.cache.delete(firstKey)
}
if (firstKey) {
const texture = this.cache.get(firstKey)
if (texture) {
texture.dispose()
}
this.cache.delete(firstKey)
}

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Comment on lines +147 to +183
function createCanvasTexture(
svgString: string,
width: number,
height: number,
backgroundColor: string,
): Uint8Array {
const canvas = document.createElement("canvas")
canvas.width = width
canvas.height = height
const ctx = canvas.getContext("2d")!

if (!ctx) {
throw new Error("Failed to get canvas 2D context")
}

// Fill background
ctx.fillStyle = backgroundColor
ctx.fillRect(0, 0, width, height)

// Create a more realistic PCB pattern
createPCBPattern(ctx, width, height)

// Convert canvas to PNG data
const dataURL = canvas.toDataURL("image/png")
const base64 = dataURL.split(",")[1]
if (!base64) {
throw new Error("Failed to generate canvas data URL")
}

const binaryString = atob(base64)
const bytes = new Uint8Array(binaryString.length)
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i)
}

return bytes
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding error handling for canvas operations to ensure robustness in different environments. The current implementation may fail in server-side rendering contexts where the DOM is unavailable. A try/catch block would allow graceful fallback:

try {
  const canvas = document.createElement("canvas");
  // existing canvas operations
} catch (error) {
  console.warn("Canvas operations failed:", error);
  // Return a simple fallback texture or rethrow with more context
}

This would improve reliability across different rendering contexts.

Suggested change
function createCanvasTexture(
svgString: string,
width: number,
height: number,
backgroundColor: string,
): Uint8Array {
const canvas = document.createElement("canvas")
canvas.width = width
canvas.height = height
const ctx = canvas.getContext("2d")!
if (!ctx) {
throw new Error("Failed to get canvas 2D context")
}
// Fill background
ctx.fillStyle = backgroundColor
ctx.fillRect(0, 0, width, height)
// Create a more realistic PCB pattern
createPCBPattern(ctx, width, height)
// Convert canvas to PNG data
const dataURL = canvas.toDataURL("image/png")
const base64 = dataURL.split(",")[1]
if (!base64) {
throw new Error("Failed to generate canvas data URL")
}
const binaryString = atob(base64)
const bytes = new Uint8Array(binaryString.length)
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i)
}
return bytes
}
function createCanvasTexture(
svgString: string,
width: number,
height: number,
backgroundColor: string,
): Uint8Array {
try {
const canvas = document.createElement("canvas")
canvas.width = width
canvas.height = height
const ctx = canvas.getContext("2d")!
if (!ctx) {
throw new Error("Failed to get canvas 2D context")
}
// Fill background
ctx.fillStyle = backgroundColor
ctx.fillRect(0, 0, width, height)
// Create a more realistic PCB pattern
createPCBPattern(ctx, width, height)
// Convert canvas to PNG data
const dataURL = canvas.toDataURL("image/png")
const base64 = dataURL.split(",")[1]
if (!base64) {
throw new Error("Failed to generate canvas data URL")
}
const binaryString = atob(base64)
const bytes = new Uint8Array(binaryString.length)
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i)
}
return bytes
} catch (error) {
console.warn("Canvas operations failed:", error)
// Return a simple fallback texture or rethrow with more context
throw new Error(`Failed to create canvas texture: ${error instanceof Error ? error.message : String(error)}`)
}
}

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link
Contributor

@nailoo nailoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type check failed

@saurabhraghuvanshii
Copy link
Author

@nailoo done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Texture Support (PCB should have texture on box)

2 participants