From 8e1ae5fc9c3bc203b84247fd535c269283969cd4 Mon Sep 17 00:00:00 2001 From: quanta-naut <123290216+Quanta-Naut@users.noreply.github.com> Date: Wed, 15 Oct 2025 14:59:23 +0530 Subject: [PATCH 1/7] feat: add layer visibility toggling feature --- src/CadViewer.tsx | 114 +++++++++++++++++++++++++++++++- src/CadViewerJscad.tsx | 28 +++++++- src/CadViewerManifold.tsx | 70 ++++++++++++++++++-- src/LayerVisibilitySubmenu.tsx | 117 +++++++++++++++++++++++++++++++++ src/utils/layerDetection.ts | 74 +++++++++++++++++++++ 5 files changed, 394 insertions(+), 9 deletions(-) create mode 100644 src/LayerVisibilitySubmenu.tsx create mode 100644 src/utils/layerDetection.ts diff --git a/src/CadViewer.tsx b/src/CadViewer.tsx index f4bce971..b029efbc 100644 --- a/src/CadViewer.tsx +++ b/src/CadViewer.tsx @@ -3,13 +3,32 @@ import { CadViewerJscad } from "./CadViewerJscad" import CadViewerManifold from "./CadViewerManifold" import { useContextMenu } from "./hooks/useContextMenu" import { useGlobalDownloadGltf } from "./hooks/useGlobalDownloadGltf" +import { LayerVisibilitySubmenu } from "./LayerVisibilitySubmenu" +import { getPresentLayers, type LayerVisibility } from "./utils/layerDetection" import packageJson from "../package.json" +const defaultLayerVisibility: LayerVisibility = { + board: true, + platedHoles: true, + smtPads: true, + vias: true, + copperPours: true, + topTrace: true, + bottomTrace: true, + topSilkscreen: true, + bottomSilkscreen: true, + cadComponents: true, +} + export const CadViewer = (props: any) => { const [engine, setEngine] = useState<"jscad" | "manifold">("manifold") const containerRef = useRef(null) + const submenuRef = useRef(null) const [autoRotate, setAutoRotate] = useState(true) const [autoRotateUserToggled, setAutoRotateUserToggled] = useState(false) + const [layerVisibility, setLayerVisibility] = useState(defaultLayerVisibility) + const [layersSubmenuVisible, setLayersSubmenuVisible] = useState(false) + const [presentLayers, setPresentLayers] = useState>({}) const { menuVisible, @@ -19,6 +38,54 @@ export const CadViewer = (props: any) => { setMenuVisible, } = useContextMenu({ containerRef }) + // Override menuRef.contains to include both main menu and submenu + useEffect(() => { + if (menuRef.current) { + const originalContains = menuRef.current.contains + menuRef.current.contains = (node: Node) => { + const isInMainMenu = originalContains.call(menuRef.current!, node) + const isInSubmenu = submenuRef.current?.contains(node) || false + return isInMainMenu || isInSubmenu + } + } + }, [menuVisible]) // Re-run when menu becomes visible + + // Handle clicks outside both menus to close them + useEffect(() => { + const handleClickOutside = (e: MouseEvent | TouchEvent) => { + const target = e.target as Node + const isClickInsideMainMenu = menuRef.current && menuRef.current.contains(target) + const isClickInsideSubmenu = submenuRef.current && submenuRef.current.contains(target) + + if (!isClickInsideMainMenu && !isClickInsideSubmenu) { + setMenuVisible(false) + setLayersSubmenuVisible(false) + } else { + // If click is inside either menu, prevent the useContextMenu's click outside from running + e.stopPropagation() + } + } + + if (menuVisible) { + // Add our handler first (higher priority) + document.addEventListener("mousedown", handleClickOutside, true) // use capture phase + document.addEventListener("touchstart", handleClickOutside, true) + return () => { + document.removeEventListener("mousedown", handleClickOutside, true) + document.removeEventListener("touchstart", handleClickOutside, true) + } + } + }, [menuVisible]) + + // Update present layers when circuitJson changes + useEffect(() => { + if (props.circuitJson) { + setPresentLayers(getPresentLayers(props.circuitJson)) + } else { + setPresentLayers({}) + } + }, [props.circuitJson]) + const autoRotateUserToggledRef = useRef(autoRotateUserToggled) autoRotateUserToggledRef.current = autoRotateUserToggled @@ -35,6 +102,17 @@ export const CadViewer = (props: any) => { const downloadGltf = useGlobalDownloadGltf() + const toggleLayerVisibility = useCallback((layer: keyof LayerVisibility) => { + setLayerVisibility(prev => ({ + ...prev, + [layer]: !prev[layer] + })) + }, []) + + const showAllLayers = useCallback(() => { + setLayerVisibility(defaultLayerVisibility) + }, []) + const handleMenuClick = (newEngine: "jscad" | "manifold") => { setEngine(newEngine) setMenuVisible(false) @@ -76,12 +154,14 @@ export const CadViewer = (props: any) => { {...props} autoRotateDisabled={props.autoRotateDisabled || !autoRotate} onUserInteraction={handleUserInteraction} + layerVisibility={layerVisibility} /> ) : ( )}
{ > Download GLTF
+
setLayersSubmenuVisible(!layersSubmenuVisible)} + onMouseOver={(e) => (e.currentTarget.style.background = "#2d313a")} + onMouseOut={(e) => { + (e.currentTarget.style.background = "transparent") + }} + > + Toggle Layers + +
{
- )} + )} + {layersSubmenuVisible && ( + + )} ) } diff --git a/src/CadViewerJscad.tsx b/src/CadViewerJscad.tsx index d10f359d..6cf9dc5c 100644 --- a/src/CadViewerJscad.tsx +++ b/src/CadViewerJscad.tsx @@ -26,6 +26,18 @@ interface Props { circuitJson?: AnyCircuitElement[] autoRotateDisabled?: boolean clickToInteractEnabled?: boolean + layerVisibility?: { + board: boolean + platedHoles: boolean + smtPads: boolean + vias: boolean + copperPours: boolean + topTrace: boolean + bottomTrace: boolean + topSilkscreen: boolean + bottomSilkscreen: boolean + cadComponents: boolean + } onUserInteraction?: () => void } @@ -40,6 +52,18 @@ export const CadViewerJscad = forwardRef< children, autoRotateDisabled, clickToInteractEnabled, + layerVisibility = { + board: true, + platedHoles: true, + smtPads: true, + vias: true, + copperPours: true, + topTrace: true, + bottomTrace: true, + topSilkscreen: true, + bottomSilkscreen: true, + cadComponents: true, + }, onUserInteraction, }, ref, @@ -114,7 +138,7 @@ export const CadViewerJscad = forwardRef< boardCenter={boardCenter} onUserInteraction={onUserInteraction} > - {boardStls.map(({ stlData, color }, index) => ( + {layerVisibility.board && boardStls.map(({ stlData, color }, index) => ( ))} - {cad_components.map((cad_component) => ( + {layerVisibility.cadComponents && cad_components.map((cad_component) => ( ( diff --git a/src/CadViewerManifold.tsx b/src/CadViewerManifold.tsx index 85e54744..f7a7e3c9 100644 --- a/src/CadViewerManifold.tsx +++ b/src/CadViewerManifold.tsx @@ -25,29 +25,74 @@ declare global { const BoardMeshes = ({ geometryMeshes, textureMeshes, + layerVisibility, }: { geometryMeshes: THREE.Mesh[] textureMeshes: THREE.Mesh[] + layerVisibility: LayerVisibility }) => { const { rootObject } = useThree() useEffect(() => { if (!rootObject) return - geometryMeshes.forEach((mesh) => rootObject.add(mesh)) - textureMeshes.forEach((mesh) => rootObject.add(mesh)) + + const meshesToAdd: THREE.Mesh[] = [] + + // Add geometry meshes based on visibility + geometryMeshes.forEach((mesh) => { + if (mesh.name === "board-geom" && layerVisibility.board) { + meshesToAdd.push(mesh) + } else if (mesh.name.startsWith("plated-hole-") && layerVisibility.platedHoles) { + meshesToAdd.push(mesh) + } else if (mesh.name.startsWith("smt-pad-") && layerVisibility.smtPads) { + meshesToAdd.push(mesh) + } else if (mesh.name.startsWith("via-") && layerVisibility.vias) { + meshesToAdd.push(mesh) + } else if (mesh.name.startsWith("copper-pour-") && layerVisibility.copperPours) { + meshesToAdd.push(mesh) + } + }) + + // Add texture meshes based on visibility + textureMeshes.forEach((mesh) => { + if (mesh.name === "top-trace-texture-plane" && layerVisibility.topTrace) { + meshesToAdd.push(mesh) + } else if (mesh.name === "bottom-trace-texture-plane" && layerVisibility.bottomTrace) { + meshesToAdd.push(mesh) + } else if (mesh.name === "top-silkscreen-texture-plane" && layerVisibility.topSilkscreen) { + meshesToAdd.push(mesh) + } else if (mesh.name === "bottom-silkscreen-texture-plane" && layerVisibility.bottomSilkscreen) { + meshesToAdd.push(mesh) + } + }) + + meshesToAdd.forEach((mesh) => rootObject.add(mesh)) return () => { - geometryMeshes.forEach((mesh) => rootObject.remove(mesh)) - textureMeshes.forEach((mesh) => rootObject.remove(mesh)) + meshesToAdd.forEach((mesh) => rootObject.remove(mesh)) } - }, [rootObject, geometryMeshes, textureMeshes]) + }, [rootObject, geometryMeshes, textureMeshes, layerVisibility]) return null } +type LayerVisibility = { + board: boolean + platedHoles: boolean + smtPads: boolean + vias: boolean + copperPours: boolean + topTrace: boolean + bottomTrace: boolean + topSilkscreen: boolean + bottomSilkscreen: boolean + cadComponents: boolean +} + type CadViewerManifoldProps = { autoRotateDisabled?: boolean clickToInteractEnabled?: boolean + layerVisibility?: LayerVisibility onUserInteraction?: () => void } & ( | { circuitJson: AnyCircuitElement[]; children?: React.ReactNode } @@ -60,6 +105,18 @@ const CadViewerManifold: React.FC = ({ circuitJson: circuitJsonProp, autoRotateDisabled, clickToInteractEnabled, + layerVisibility = { + board: true, + platedHoles: true, + smtPads: true, + vias: true, + copperPours: true, + topTrace: true, + bottomTrace: true, + topSilkscreen: true, + bottomSilkscreen: true, + cadComponents: true, + }, onUserInteraction, children, }) => { @@ -239,8 +296,9 @@ try { - {cadComponents.map((cad_component: CadComponent) => ( + {layerVisibility.cadComponents && cadComponents.map((cad_component: CadComponent) => ( ( diff --git a/src/LayerVisibilitySubmenu.tsx b/src/LayerVisibilitySubmenu.tsx new file mode 100644 index 00000000..40b13505 --- /dev/null +++ b/src/LayerVisibilitySubmenu.tsx @@ -0,0 +1,117 @@ +import React, { forwardRef } from "react" + +export type LayerVisibility = { + board: boolean + platedHoles: boolean + smtPads: boolean + vias: boolean + copperPours: boolean + topTrace: boolean + bottomTrace: boolean + topSilkscreen: boolean + bottomSilkscreen: boolean + cadComponents: boolean +} + +interface LayerVisibilitySubmenuProps { + layerVisibility: LayerVisibility + presentLayers: Partial + onToggleLayer: (layer: keyof LayerVisibility) => void + onShowAllLayers: () => void + position: { x: number; y: number } +} + +export const LayerVisibilitySubmenu = forwardRef(({ + layerVisibility, + presentLayers, + onToggleLayer, + onShowAllLayers, + position, +}, ref) => { + return ( +
+
+ Layer Visibility +
+
(e.currentTarget.style.background = "#2d313a")} + onMouseOut={(e) => (e.currentTarget.style.background = "transparent")} + > + Show All Layers +
+ {Object.entries(layerVisibility) + .filter(([layer]) => presentLayers[layer as keyof LayerVisibility]) + .map(([layer, visible]) => ( +
onToggleLayer(layer as keyof LayerVisibility)} + onMouseOver={(e) => (e.currentTarget.style.background = "#2d313a")} + onMouseOut={(e) => (e.currentTarget.style.background = "transparent")} + > + {visible ? "✔" : ""} + {layer === "cadComponents" ? "CAD Components" : + layer === "platedHoles" ? "Plated Holes" : + layer === "smtPads" ? "SMT Pads" : + layer === "copperPours" ? "Copper Pours" : + layer === "topTrace" ? "Top Traces" : + layer === "bottomTrace" ? "Bottom Traces" : + layer === "topSilkscreen" ? "Top Silkscreen" : + layer === "bottomSilkscreen" ? "Bottom Silkscreen" : + layer.charAt(0).toUpperCase() + layer.slice(1)} +
+ ))} +
+ ) +}) \ No newline at end of file diff --git a/src/utils/layerDetection.ts b/src/utils/layerDetection.ts new file mode 100644 index 00000000..c9dcf00b --- /dev/null +++ b/src/utils/layerDetection.ts @@ -0,0 +1,74 @@ +export type LayerVisibility = { + board: boolean + platedHoles: boolean + smtPads: boolean + vias: boolean + copperPours: boolean + topTrace: boolean + bottomTrace: boolean + topSilkscreen: boolean + bottomSilkscreen: boolean + cadComponents: boolean +} + +export const getPresentLayers = (circuitJson: any[]): Partial => { + const presentLayers: Partial = {} + + // Always show board if there's a PCB + if (circuitJson.some(e => e.type === "pcb_board")) { + presentLayers.board = true + } + + // Check for plated holes + if (circuitJson.some(e => e.type === "pcb_plated_hole")) { + presentLayers.platedHoles = true + } + + // Check for SMT pads + if (circuitJson.some(e => e.type === "pcb_smtpad")) { + presentLayers.smtPads = true + } + + // Check for vias + if (circuitJson.some(e => e.type === "pcb_via")) { + presentLayers.vias = true + } + + // Check for copper pours + if (circuitJson.some(e => e.type === "pcb_copper_pour")) { + presentLayers.copperPours = true + } + + // Check for traces (need to check route points for layer) + const traces = circuitJson.filter(e => e.type === "pcb_trace") + const hasTopTraces = traces.some(t => + t.route && t.route.some((point: any) => point.layer === "top") + ) + const hasBottomTraces = traces.some(t => + t.route && t.route.some((point: any) => point.layer === "bottom") + ) + if (hasTopTraces) { + presentLayers.topTrace = true + } + if (hasBottomTraces) { + presentLayers.bottomTrace = true + } + + // Check for silkscreen + const silkscreenTexts = circuitJson.filter(e => + e.type === "pcb_silkscreen_text" || e.type === "pcb_silkscreen_path" + ) + if (silkscreenTexts.some(s => s.layer === "top")) { + presentLayers.topSilkscreen = true + } + if (silkscreenTexts.some(s => s.layer === "bottom")) { + presentLayers.bottomSilkscreen = true + } + + // Check for CAD components + if (circuitJson.some(e => e.type === "cad_component")) { + presentLayers.cadComponents = true + } + + return presentLayers +} \ No newline at end of file From 08f9d7b437d93888b175a8752aee93852ff99138 Mon Sep 17 00:00:00 2001 From: quanta-naut <123290216+Quanta-Naut@users.noreply.github.com> Date: Wed, 15 Oct 2025 15:07:03 +0530 Subject: [PATCH 2/7] feat: add layer visibility toggling feature --- src/CadViewer.tsx | 44 +++++---- src/CadViewerJscad.tsx | 44 ++++----- src/CadViewerManifold.tsx | 52 ++++++---- src/LayerVisibilitySubmenu.tsx | 168 ++++++++++++++++++--------------- src/utils/layerDetection.ts | 36 +++---- 5 files changed, 195 insertions(+), 149 deletions(-) diff --git a/src/CadViewer.tsx b/src/CadViewer.tsx index b029efbc..47263fc6 100644 --- a/src/CadViewer.tsx +++ b/src/CadViewer.tsx @@ -26,9 +26,13 @@ export const CadViewer = (props: any) => { const submenuRef = useRef(null) const [autoRotate, setAutoRotate] = useState(true) const [autoRotateUserToggled, setAutoRotateUserToggled] = useState(false) - const [layerVisibility, setLayerVisibility] = useState(defaultLayerVisibility) + const [layerVisibility, setLayerVisibility] = useState( + defaultLayerVisibility, + ) const [layersSubmenuVisible, setLayersSubmenuVisible] = useState(false) - const [presentLayers, setPresentLayers] = useState>({}) + const [presentLayers, setPresentLayers] = useState>( + {}, + ) const { menuVisible, @@ -54,9 +58,11 @@ export const CadViewer = (props: any) => { useEffect(() => { const handleClickOutside = (e: MouseEvent | TouchEvent) => { const target = e.target as Node - const isClickInsideMainMenu = menuRef.current && menuRef.current.contains(target) - const isClickInsideSubmenu = submenuRef.current && submenuRef.current.contains(target) - + const isClickInsideMainMenu = + menuRef.current && menuRef.current.contains(target) + const isClickInsideSubmenu = + submenuRef.current && submenuRef.current.contains(target) + if (!isClickInsideMainMenu && !isClickInsideSubmenu) { setMenuVisible(false) setLayersSubmenuVisible(false) @@ -103,9 +109,9 @@ export const CadViewer = (props: any) => { const downloadGltf = useGlobalDownloadGltf() const toggleLayerVisibility = useCallback((layer: keyof LayerVisibility) => { - setLayerVisibility(prev => ({ + setLayerVisibility((prev) => ({ ...prev, - [layer]: !prev[layer] + [layer]: !prev[layer], })) }, []) @@ -295,7 +301,7 @@ export const CadViewer = (props: any) => { onClick={() => setLayersSubmenuVisible(!layersSubmenuVisible)} onMouseOver={(e) => (e.currentTarget.style.background = "#2d313a")} onMouseOut={(e) => { - (e.currentTarget.style.background = "transparent") + e.currentTarget.style.background = "transparent" }} > Toggle Layers @@ -322,17 +328,17 @@ export const CadViewer = (props: any) => { - )} - {layersSubmenuVisible && ( - - )} + )} + {layersSubmenuVisible && ( + + )} ) } diff --git a/src/CadViewerJscad.tsx b/src/CadViewerJscad.tsx index 6cf9dc5c..18a5647a 100644 --- a/src/CadViewerJscad.tsx +++ b/src/CadViewerJscad.tsx @@ -138,28 +138,30 @@ export const CadViewerJscad = forwardRef< boardCenter={boardCenter} onUserInteraction={onUserInteraction} > - {layerVisibility.board && boardStls.map(({ stlData, color }, index) => ( - - ))} - {layerVisibility.cadComponents && cad_components.map((cad_component) => ( - ( - - )} - > - ( + - - ))} + ))} + {layerVisibility.cadComponents && + cad_components.map((cad_component) => ( + ( + + )} + > + + + ))} ) }, diff --git a/src/CadViewerManifold.tsx b/src/CadViewerManifold.tsx index f7a7e3c9..ba932148 100644 --- a/src/CadViewerManifold.tsx +++ b/src/CadViewerManifold.tsx @@ -42,13 +42,19 @@ const BoardMeshes = ({ geometryMeshes.forEach((mesh) => { if (mesh.name === "board-geom" && layerVisibility.board) { meshesToAdd.push(mesh) - } else if (mesh.name.startsWith("plated-hole-") && layerVisibility.platedHoles) { + } else if ( + mesh.name.startsWith("plated-hole-") && + layerVisibility.platedHoles + ) { meshesToAdd.push(mesh) } else if (mesh.name.startsWith("smt-pad-") && layerVisibility.smtPads) { meshesToAdd.push(mesh) } else if (mesh.name.startsWith("via-") && layerVisibility.vias) { meshesToAdd.push(mesh) - } else if (mesh.name.startsWith("copper-pour-") && layerVisibility.copperPours) { + } else if ( + mesh.name.startsWith("copper-pour-") && + layerVisibility.copperPours + ) { meshesToAdd.push(mesh) } }) @@ -57,11 +63,20 @@ const BoardMeshes = ({ textureMeshes.forEach((mesh) => { if (mesh.name === "top-trace-texture-plane" && layerVisibility.topTrace) { meshesToAdd.push(mesh) - } else if (mesh.name === "bottom-trace-texture-plane" && layerVisibility.bottomTrace) { + } else if ( + mesh.name === "bottom-trace-texture-plane" && + layerVisibility.bottomTrace + ) { meshesToAdd.push(mesh) - } else if (mesh.name === "top-silkscreen-texture-plane" && layerVisibility.topSilkscreen) { + } else if ( + mesh.name === "top-silkscreen-texture-plane" && + layerVisibility.topSilkscreen + ) { meshesToAdd.push(mesh) - } else if (mesh.name === "bottom-silkscreen-texture-plane" && layerVisibility.bottomSilkscreen) { + } else if ( + mesh.name === "bottom-silkscreen-texture-plane" && + layerVisibility.bottomSilkscreen + ) { meshesToAdd.push(mesh) } }) @@ -298,19 +313,20 @@ try { textureMeshes={textureMeshes} layerVisibility={layerVisibility} /> - {layerVisibility.cadComponents && cadComponents.map((cad_component: CadComponent) => ( - ( - - )} - > - - - ))} + {layerVisibility.cadComponents && + cadComponents.map((cad_component: CadComponent) => ( + ( + + )} + > + + + ))} ) } diff --git a/src/LayerVisibilitySubmenu.tsx b/src/LayerVisibilitySubmenu.tsx index 40b13505..06e5fbc7 100644 --- a/src/LayerVisibilitySubmenu.tsx +++ b/src/LayerVisibilitySubmenu.tsx @@ -21,97 +21,117 @@ interface LayerVisibilitySubmenuProps { position: { x: number; y: number } } -export const LayerVisibilitySubmenu = forwardRef(({ - layerVisibility, - presentLayers, - onToggleLayer, - onShowAllLayers, - position, -}, ref) => { - return ( -
+export const LayerVisibilitySubmenu = forwardRef< + HTMLDivElement, + LayerVisibilitySubmenuProps +>( + ( + { + layerVisibility, + presentLayers, + onToggleLayer, + onShowAllLayers, + position, + }, + ref, + ) => { + return (
- Layer Visibility -
-
(e.currentTarget.style.background = "#2d313a")} - onMouseOut={(e) => (e.currentTarget.style.background = "transparent")} > - Show All Layers -
- {Object.entries(layerVisibility) - .filter(([layer]) => presentLayers[layer as keyof LayerVisibility]) - .map(([layer, visible]) => (
+ Layer Visibility +
+
onToggleLayer(layer as keyof LayerVisibility)} + onClick={onShowAllLayers} onMouseOver={(e) => (e.currentTarget.style.background = "#2d313a")} onMouseOut={(e) => (e.currentTarget.style.background = "transparent")} > - {visible ? "✔" : ""} - {layer === "cadComponents" ? "CAD Components" : - layer === "platedHoles" ? "Plated Holes" : - layer === "smtPads" ? "SMT Pads" : - layer === "copperPours" ? "Copper Pours" : - layer === "topTrace" ? "Top Traces" : - layer === "bottomTrace" ? "Bottom Traces" : - layer === "topSilkscreen" ? "Top Silkscreen" : - layer === "bottomSilkscreen" ? "Bottom Silkscreen" : - layer.charAt(0).toUpperCase() + layer.slice(1)} + Show All Layers
- ))} -
- ) -}) \ No newline at end of file + {Object.entries(layerVisibility) + .filter(([layer]) => presentLayers[layer as keyof LayerVisibility]) + .map(([layer, visible]) => ( +
onToggleLayer(layer as keyof LayerVisibility)} + onMouseOver={(e) => + (e.currentTarget.style.background = "#2d313a") + } + onMouseOut={(e) => + (e.currentTarget.style.background = "transparent") + } + > + {visible ? "✔" : ""} + {layer === "cadComponents" + ? "CAD Components" + : layer === "platedHoles" + ? "Plated Holes" + : layer === "smtPads" + ? "SMT Pads" + : layer === "copperPours" + ? "Copper Pours" + : layer === "topTrace" + ? "Top Traces" + : layer === "bottomTrace" + ? "Bottom Traces" + : layer === "topSilkscreen" + ? "Top Silkscreen" + : layer === "bottomSilkscreen" + ? "Bottom Silkscreen" + : layer.charAt(0).toUpperCase() + layer.slice(1)} +
+ ))} + + ) + }, +) diff --git a/src/utils/layerDetection.ts b/src/utils/layerDetection.ts index c9dcf00b..c37964ce 100644 --- a/src/utils/layerDetection.ts +++ b/src/utils/layerDetection.ts @@ -11,41 +11,43 @@ export type LayerVisibility = { cadComponents: boolean } -export const getPresentLayers = (circuitJson: any[]): Partial => { +export const getPresentLayers = ( + circuitJson: any[], +): Partial => { const presentLayers: Partial = {} // Always show board if there's a PCB - if (circuitJson.some(e => e.type === "pcb_board")) { + if (circuitJson.some((e) => e.type === "pcb_board")) { presentLayers.board = true } // Check for plated holes - if (circuitJson.some(e => e.type === "pcb_plated_hole")) { + if (circuitJson.some((e) => e.type === "pcb_plated_hole")) { presentLayers.platedHoles = true } // Check for SMT pads - if (circuitJson.some(e => e.type === "pcb_smtpad")) { + if (circuitJson.some((e) => e.type === "pcb_smtpad")) { presentLayers.smtPads = true } // Check for vias - if (circuitJson.some(e => e.type === "pcb_via")) { + if (circuitJson.some((e) => e.type === "pcb_via")) { presentLayers.vias = true } // Check for copper pours - if (circuitJson.some(e => e.type === "pcb_copper_pour")) { + if (circuitJson.some((e) => e.type === "pcb_copper_pour")) { presentLayers.copperPours = true } // Check for traces (need to check route points for layer) - const traces = circuitJson.filter(e => e.type === "pcb_trace") - const hasTopTraces = traces.some(t => - t.route && t.route.some((point: any) => point.layer === "top") + const traces = circuitJson.filter((e) => e.type === "pcb_trace") + const hasTopTraces = traces.some( + (t) => t.route && t.route.some((point: any) => point.layer === "top"), ) - const hasBottomTraces = traces.some(t => - t.route && t.route.some((point: any) => point.layer === "bottom") + const hasBottomTraces = traces.some( + (t) => t.route && t.route.some((point: any) => point.layer === "bottom"), ) if (hasTopTraces) { presentLayers.topTrace = true @@ -55,20 +57,20 @@ export const getPresentLayers = (circuitJson: any[]): Partial = } // Check for silkscreen - const silkscreenTexts = circuitJson.filter(e => - e.type === "pcb_silkscreen_text" || e.type === "pcb_silkscreen_path" + const silkscreenTexts = circuitJson.filter( + (e) => e.type === "pcb_silkscreen_text" || e.type === "pcb_silkscreen_path", ) - if (silkscreenTexts.some(s => s.layer === "top")) { + if (silkscreenTexts.some((s) => s.layer === "top")) { presentLayers.topSilkscreen = true } - if (silkscreenTexts.some(s => s.layer === "bottom")) { + if (silkscreenTexts.some((s) => s.layer === "bottom")) { presentLayers.bottomSilkscreen = true } // Check for CAD components - if (circuitJson.some(e => e.type === "cad_component")) { + if (circuitJson.some((e) => e.type === "cad_component")) { presentLayers.cadComponents = true } return presentLayers -} \ No newline at end of file +} From 56673302b1d96c0b328a9ebc8f0b00b803c02cb0 Mon Sep 17 00:00:00 2001 From: Tarun <123290216+Quanta-Naut@users.noreply.github.com> Date: Wed, 15 Oct 2025 16:14:03 +0530 Subject: [PATCH 3/7] chore: removed hard-coded position of the layer-submenu Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> --- src/LayerVisibilitySubmenu.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/LayerVisibilitySubmenu.tsx b/src/LayerVisibilitySubmenu.tsx index 06e5fbc7..4f3414c0 100644 --- a/src/LayerVisibilitySubmenu.tsx +++ b/src/LayerVisibilitySubmenu.tsx @@ -40,8 +40,12 @@ export const LayerVisibilitySubmenu = forwardRef< ref={ref} style={{ position: "fixed", + top: position.y, - left: position.x + 220, + left: (submenuRef.current && window.innerWidth < position.x + submenuRef.current.offsetWidth + 220) + ? position.x - submenuRef.current?.offsetWidth + : position.x + 220, + background: "#23272f", color: "#f5f6fa", borderRadius: 6, From 2ad49966ec4c59f7dede567ed9b55675287b19e6 Mon Sep 17 00:00:00 2001 From: quanta-naut <123290216+Quanta-Naut@users.noreply.github.com> Date: Wed, 15 Oct 2025 16:20:28 +0530 Subject: [PATCH 4/7] chore: simplify submenu positioning logic in LayerVisibilitySubmenu --- src/LayerVisibilitySubmenu.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/LayerVisibilitySubmenu.tsx b/src/LayerVisibilitySubmenu.tsx index 4f3414c0..69a4cb8f 100644 --- a/src/LayerVisibilitySubmenu.tsx +++ b/src/LayerVisibilitySubmenu.tsx @@ -35,17 +35,19 @@ export const LayerVisibilitySubmenu = forwardRef< }, ref, ) => { + const submenuWidth = 200 + const left = + position.x + 220 > window.innerWidth - submenuWidth + ? position.x - submenuWidth - 20 + : position.x + 220 + return (
Date: Thu, 16 Oct 2025 18:48:09 +0530 Subject: [PATCH 5/7] chore: inherited the kicad layer toggles --- src/CadViewer.tsx | 12 ++--- src/CadViewerManifold.tsx | 58 +++++++++++------------- src/LayerVisibilitySubmenu.tsx | 62 +++++++++++++------------- src/utils/layerDetection.ts | 40 ++++------------- src/utils/manifold/process-smt-pads.ts | 2 +- 5 files changed, 70 insertions(+), 104 deletions(-) diff --git a/src/CadViewer.tsx b/src/CadViewer.tsx index 47263fc6..9b1c3b3f 100644 --- a/src/CadViewer.tsx +++ b/src/CadViewer.tsx @@ -9,14 +9,10 @@ import packageJson from "../package.json" const defaultLayerVisibility: LayerVisibility = { board: true, - platedHoles: true, - smtPads: true, - vias: true, - copperPours: true, - topTrace: true, - bottomTrace: true, - topSilkscreen: true, - bottomSilkscreen: true, + fCu: true, + bCu: true, + fSilkscreen: true, + bSilkscreen: true, cadComponents: true, } diff --git a/src/CadViewerManifold.tsx b/src/CadViewerManifold.tsx index ba932148..0b7e721f 100644 --- a/src/CadViewerManifold.tsx +++ b/src/CadViewerManifold.tsx @@ -38,44 +38,44 @@ const BoardMeshes = ({ const meshesToAdd: THREE.Mesh[] = [] - // Add geometry meshes based on visibility geometryMeshes.forEach((mesh) => { if (mesh.name === "board-geom" && layerVisibility.board) { meshesToAdd.push(mesh) + } else if (mesh.name.startsWith("pad-")) { + const isTopPad = mesh.name.startsWith("pad-top-") + const isBottomPad = mesh.name.startsWith("pad-bottom-") + + if ( + (isTopPad && layerVisibility.fCu) || + (isBottomPad && layerVisibility.bCu) + ) { + meshesToAdd.push(mesh) + } } else if ( - mesh.name.startsWith("plated-hole-") && - layerVisibility.platedHoles - ) { - meshesToAdd.push(mesh) - } else if (mesh.name.startsWith("smt-pad-") && layerVisibility.smtPads) { - meshesToAdd.push(mesh) - } else if (mesh.name.startsWith("via-") && layerVisibility.vias) { - meshesToAdd.push(mesh) - } else if ( - mesh.name.startsWith("copper-pour-") && - layerVisibility.copperPours + mesh.name.startsWith("via-") && + layerVisibility.fCu && + layerVisibility.bCu ) { meshesToAdd.push(mesh) } }) - // Add texture meshes based on visibility textureMeshes.forEach((mesh) => { - if (mesh.name === "top-trace-texture-plane" && layerVisibility.topTrace) { + if (mesh.name === "top-trace-texture-plane" && layerVisibility.fCu) { meshesToAdd.push(mesh) } else if ( mesh.name === "bottom-trace-texture-plane" && - layerVisibility.bottomTrace + layerVisibility.bCu ) { meshesToAdd.push(mesh) } else if ( mesh.name === "top-silkscreen-texture-plane" && - layerVisibility.topSilkscreen + layerVisibility.fSilkscreen ) { meshesToAdd.push(mesh) } else if ( mesh.name === "bottom-silkscreen-texture-plane" && - layerVisibility.bottomSilkscreen + layerVisibility.bSilkscreen ) { meshesToAdd.push(mesh) } @@ -93,14 +93,10 @@ const BoardMeshes = ({ type LayerVisibility = { board: boolean - platedHoles: boolean - smtPads: boolean - vias: boolean - copperPours: boolean - topTrace: boolean - bottomTrace: boolean - topSilkscreen: boolean - bottomSilkscreen: boolean + fCu: boolean + bCu: boolean + fSilkscreen: boolean + bSilkscreen: boolean cadComponents: boolean } @@ -122,14 +118,10 @@ const CadViewerManifold: React.FC = ({ clickToInteractEnabled, layerVisibility = { board: true, - platedHoles: true, - smtPads: true, - vias: true, - copperPours: true, - topTrace: true, - bottomTrace: true, - topSilkscreen: true, - bottomSilkscreen: true, + fCu: true, + bCu: true, + fSilkscreen: true, + bSilkscreen: true, cadComponents: true, }, onUserInteraction, diff --git a/src/LayerVisibilitySubmenu.tsx b/src/LayerVisibilitySubmenu.tsx index 69a4cb8f..4b391d69 100644 --- a/src/LayerVisibilitySubmenu.tsx +++ b/src/LayerVisibilitySubmenu.tsx @@ -2,14 +2,10 @@ import React, { forwardRef } from "react" export type LayerVisibility = { board: boolean - platedHoles: boolean - smtPads: boolean - vias: boolean - copperPours: boolean - topTrace: boolean - bottomTrace: boolean - topSilkscreen: boolean - bottomSilkscreen: boolean + fCu: boolean + bCu: boolean + fSilkscreen: boolean + bSilkscreen: boolean cadComponents: boolean } @@ -35,18 +31,28 @@ export const LayerVisibilitySubmenu = forwardRef< }, ref, ) => { - const submenuWidth = 200 + const SUBMENU_WIDTH = 200 + const SUBMENU_HEIGHT = 300 + const HORIZONTAL_OFFSET = 220 + const MARGIN = 20 + + // Calculate position to keep submenu within viewport bounds const left = - position.x + 220 > window.innerWidth - submenuWidth - ? position.x - submenuWidth - 20 - : position.x + 220 + position.x + HORIZONTAL_OFFSET + SUBMENU_WIDTH > window.innerWidth + ? position.x - SUBMENU_WIDTH - MARGIN + : position.x + HORIZONTAL_OFFSET + + const top = + position.y + SUBMENU_HEIGHT > window.innerHeight + ? window.innerHeight - SUBMENU_HEIGHT - MARGIN + : position.y return (
{visible ? "✔" : ""} - {layer === "cadComponents" - ? "CAD Components" - : layer === "platedHoles" - ? "Plated Holes" - : layer === "smtPads" - ? "SMT Pads" - : layer === "copperPours" - ? "Copper Pours" - : layer === "topTrace" - ? "Top Traces" - : layer === "bottomTrace" - ? "Bottom Traces" - : layer === "topSilkscreen" - ? "Top Silkscreen" - : layer === "bottomSilkscreen" - ? "Bottom Silkscreen" - : layer.charAt(0).toUpperCase() + layer.slice(1)} + {layer === "board" + ? "Board Body" + : layer === "cadComponents" + ? "CAD Components" + : layer === "fCu" + ? "F.Cu" + : layer === "bCu" + ? "B.Cu" + : layer === "fSilkscreen" + ? "F.Silkscreen" + : layer === "bSilkscreen" + ? "B.Silkscreen" + : layer.charAt(0).toUpperCase() + layer.slice(1)}
))}
diff --git a/src/utils/layerDetection.ts b/src/utils/layerDetection.ts index c37964ce..2e9845ed 100644 --- a/src/utils/layerDetection.ts +++ b/src/utils/layerDetection.ts @@ -1,13 +1,9 @@ export type LayerVisibility = { board: boolean - platedHoles: boolean - smtPads: boolean - vias: boolean - copperPours: boolean - topTrace: boolean - bottomTrace: boolean - topSilkscreen: boolean - bottomSilkscreen: boolean + fCu: boolean + bCu: boolean + fSilkscreen: boolean + bSilkscreen: boolean cadComponents: boolean } @@ -21,26 +17,6 @@ export const getPresentLayers = ( presentLayers.board = true } - // Check for plated holes - if (circuitJson.some((e) => e.type === "pcb_plated_hole")) { - presentLayers.platedHoles = true - } - - // Check for SMT pads - if (circuitJson.some((e) => e.type === "pcb_smtpad")) { - presentLayers.smtPads = true - } - - // Check for vias - if (circuitJson.some((e) => e.type === "pcb_via")) { - presentLayers.vias = true - } - - // Check for copper pours - if (circuitJson.some((e) => e.type === "pcb_copper_pour")) { - presentLayers.copperPours = true - } - // Check for traces (need to check route points for layer) const traces = circuitJson.filter((e) => e.type === "pcb_trace") const hasTopTraces = traces.some( @@ -50,10 +26,10 @@ export const getPresentLayers = ( (t) => t.route && t.route.some((point: any) => point.layer === "bottom"), ) if (hasTopTraces) { - presentLayers.topTrace = true + presentLayers.fCu = true } if (hasBottomTraces) { - presentLayers.bottomTrace = true + presentLayers.bCu = true } // Check for silkscreen @@ -61,10 +37,10 @@ export const getPresentLayers = ( (e) => e.type === "pcb_silkscreen_text" || e.type === "pcb_silkscreen_path", ) if (silkscreenTexts.some((s) => s.layer === "top")) { - presentLayers.topSilkscreen = true + presentLayers.fSilkscreen = true } if (silkscreenTexts.some((s) => s.layer === "bottom")) { - presentLayers.bottomSilkscreen = true + presentLayers.bSilkscreen = true } // Check for CAD components diff --git a/src/utils/manifold/process-smt-pads.ts b/src/utils/manifold/process-smt-pads.ts index 69ca8755..3b3e9104 100644 --- a/src/utils/manifold/process-smt-pads.ts +++ b/src/utils/manifold/process-smt-pads.ts @@ -65,7 +65,7 @@ export function processSmtPadsForManifold( } const threeGeom = manifoldMeshToThreeGeometry(finalPadOp.getMesh()) smtPadGeoms.push({ - key: `pad-${pad.pcb_smtpad_id || index}`, + key: `pad-${pad.layer}-${pad.pcb_smtpad_id || index}`, geometry: threeGeom, color: COPPER_COLOR, }) From 51a044004a00ea8e8bbc1bbd6611dd7445ea9c6c Mon Sep 17 00:00:00 2001 From: quanta-naut <123290216+Quanta-Naut@users.noreply.github.com> Date: Thu, 16 Oct 2025 19:40:17 +0530 Subject: [PATCH 6/7] refactor: fixed inconsistencies --- src/CadViewerJscad.tsx | 27 ++++++--------------------- src/CadViewerManifold.tsx | 9 +-------- 2 files changed, 7 insertions(+), 29 deletions(-) diff --git a/src/CadViewerJscad.tsx b/src/CadViewerJscad.tsx index 18a5647a..fae21974 100644 --- a/src/CadViewerJscad.tsx +++ b/src/CadViewerJscad.tsx @@ -16,7 +16,7 @@ import { JscadModel } from "./three-components/JscadModel" import { MixedStlModel } from "./three-components/MixedStlModel" import { STLModel } from "./three-components/STLModel" import { ThreeErrorBoundary } from "./three-components/ThreeErrorBoundary" -import { tuple } from "./utils/tuple" +import type { LayerVisibility } from "./utils/layerDetection" interface Props { /** @@ -26,18 +26,7 @@ interface Props { circuitJson?: AnyCircuitElement[] autoRotateDisabled?: boolean clickToInteractEnabled?: boolean - layerVisibility?: { - board: boolean - platedHoles: boolean - smtPads: boolean - vias: boolean - copperPours: boolean - topTrace: boolean - bottomTrace: boolean - topSilkscreen: boolean - bottomSilkscreen: boolean - cadComponents: boolean - } + layerVisibility?: LayerVisibility onUserInteraction?: () => void } @@ -54,14 +43,10 @@ export const CadViewerJscad = forwardRef< clickToInteractEnabled, layerVisibility = { board: true, - platedHoles: true, - smtPads: true, - vias: true, - copperPours: true, - topTrace: true, - bottomTrace: true, - topSilkscreen: true, - bottomSilkscreen: true, + fCu: true, + bCu: true, + fSilkscreen: true, + bSilkscreen: true, cadComponents: true, }, onUserInteraction, diff --git a/src/CadViewerManifold.tsx b/src/CadViewerManifold.tsx index 0b7e721f..bdcf8435 100644 --- a/src/CadViewerManifold.tsx +++ b/src/CadViewerManifold.tsx @@ -91,14 +91,7 @@ const BoardMeshes = ({ return null } -type LayerVisibility = { - board: boolean - fCu: boolean - bCu: boolean - fSilkscreen: boolean - bSilkscreen: boolean - cadComponents: boolean -} +import type { LayerVisibility } from "./utils/layerDetection" type CadViewerManifoldProps = { autoRotateDisabled?: boolean From b0497c443f10a5658a722461c6f630c42199dcaf Mon Sep 17 00:00:00 2001 From: quanta-naut <123290216+Quanta-Naut@users.noreply.github.com> Date: Thu, 16 Oct 2025 19:56:34 +0530 Subject: [PATCH 7/7] fix: enhance layer detection for SMT pads and vias --- src/utils/layerDetection.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/utils/layerDetection.ts b/src/utils/layerDetection.ts index 2e9845ed..576c81cd 100644 --- a/src/utils/layerDetection.ts +++ b/src/utils/layerDetection.ts @@ -32,6 +32,24 @@ export const getPresentLayers = ( presentLayers.bCu = true } + // Add detection for pads and vias + const smtPads = circuitJson.filter((e) => e.type === "pcb_smtpad") + if (smtPads.some((p) => p.layer === "top")) { + presentLayers.fCu = true + } + if (smtPads.some((p) => p.layer === "bottom")) { + presentLayers.bCu = true + } + // Also check for vias + if ( + circuitJson.some( + (e) => e.type === "pcb_via" || e.type === "pcb_plated_hole", + ) + ) { + presentLayers.fCu = true + presentLayers.bCu = true + } + // Check for silkscreen const silkscreenTexts = circuitJson.filter( (e) => e.type === "pcb_silkscreen_text" || e.type === "pcb_silkscreen_path",