-
Notifications
You must be signed in to change notification settings - Fork 39
feat added texture support #541
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| 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 | ||
| } |
There was a problem hiding this comment.
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)
Is this helpful? React 👍 or 👎 to let us know.
src/hooks/useCircuitTexture.ts
Outdated
| 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]) |
There was a problem hiding this comment.
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.
| 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
Is this helpful? React 👍 or 👎 to let us know.
| 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
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
Is this helpful? React 👍 or 👎 to let us know.
| // 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) | ||
| } | ||
| } |
There was a problem hiding this comment.
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
Is this helpful? React 👍 or 👎 to let us know.
| if (firstKey) { | ||
| this.cache.delete(firstKey) | ||
| } |
There was a problem hiding this comment.
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.
| 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
Is this helpful? React 👍 or 👎 to let us know.
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| 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
Is this helpful? React 👍 or 👎 to let us know.
nailoo
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
type check failed
|
@nailoo done |
/claim #534
fixes: #534
Enhanced Texture Generation Pipeline:
Circuit JSON → circuit-to-svg → SVG → resvg-wasm → PNG → Three.js TextureNew Features Added:
Visual Enhancements: