Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions src/CadViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
fCu: true,
bCu: true,
fSilkscreen: true,
bSilkscreen: true,
cadComponents: true,
}

export const CadViewer = (props: any) => {
const [engine, setEngine] = useState<"jscad" | "manifold">("manifold")
const containerRef = useRef<HTMLDivElement | null>(null)
const submenuRef = useRef<HTMLDivElement | null>(null)
const [autoRotate, setAutoRotate] = useState(true)
const [autoRotateUserToggled, setAutoRotateUserToggled] = useState(false)
const [layerVisibility, setLayerVisibility] = useState<LayerVisibility>(
defaultLayerVisibility,
)
const [layersSubmenuVisible, setLayersSubmenuVisible] = useState(false)
const [presentLayers, setPresentLayers] = useState<Partial<LayerVisibility>>(
{},
)

const {
menuVisible,
Expand All @@ -19,6 +38,56 @@ 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

Expand All @@ -35,6 +104,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)
Expand Down Expand Up @@ -76,12 +156,14 @@ export const CadViewer = (props: any) => {
{...props}
autoRotateDisabled={props.autoRotateDisabled || !autoRotate}
onUserInteraction={handleUserInteraction}
layerVisibility={layerVisibility}
/>
) : (
<CadViewerManifold
{...props}
autoRotateDisabled={props.autoRotateDisabled || !autoRotate}
onUserInteraction={handleUserInteraction}
layerVisibility={layerVisibility}
/>
)}
<div
Expand Down Expand Up @@ -199,6 +281,28 @@ export const CadViewer = (props: any) => {
>
Download GLTF
</div>
<div
style={{
padding: "12px 18px",
cursor: "pointer",
display: "flex",
alignItems: "center",
gap: 10,
color: "#f5f6fa",
fontWeight: 500,
borderRadius: 6,
transition: "background 0.1s",
position: "relative",
}}
onClick={() => setLayersSubmenuVisible(!layersSubmenuVisible)}
onMouseOver={(e) => (e.currentTarget.style.background = "#2d313a")}
onMouseOut={(e) => {
e.currentTarget.style.background = "transparent"
}}
>
<span>Toggle Layers</span>
<span style={{ marginLeft: "auto", fontSize: 12 }}>▶</span>
</div>
<div
style={{
display: "flex",
Expand All @@ -221,6 +325,16 @@ export const CadViewer = (props: any) => {
</div>
</div>
)}
{layersSubmenuVisible && (
<LayerVisibilitySubmenu
ref={submenuRef}
layerVisibility={layerVisibility}
presentLayers={presentLayers}
onToggleLayer={toggleLayerVisibility}
onShowAllLayers={showAllLayers}
position={menuPos}
/>
)}
</div>
)
}
55 changes: 33 additions & 22 deletions src/CadViewerJscad.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand All @@ -26,6 +26,7 @@ interface Props {
circuitJson?: AnyCircuitElement[]
autoRotateDisabled?: boolean
clickToInteractEnabled?: boolean
layerVisibility?: LayerVisibility
onUserInteraction?: () => void
}

Expand All @@ -40,6 +41,14 @@ export const CadViewerJscad = forwardRef<
children,
autoRotateDisabled,
clickToInteractEnabled,
layerVisibility = {
board: true,
fCu: true,
bCu: true,
fSilkscreen: true,
bSilkscreen: true,
cadComponents: true,
},
onUserInteraction,
},
ref,
Expand Down Expand Up @@ -114,28 +123,30 @@ export const CadViewerJscad = forwardRef<
boardCenter={boardCenter}
onUserInteraction={onUserInteraction}
>
{boardStls.map(({ stlData, color }, index) => (
<STLModel
key={`board-${index - boardStls.length}`}
stlData={stlData}
color={color}
opacity={index === 0 ? 0.95 : 1}
/>
))}
{cad_components.map((cad_component) => (
<ThreeErrorBoundary
key={cad_component.cad_component_id}
fallback={({ error }) => (
<Error3d cad_component={cad_component} error={error} />
)}
>
<AnyCadComponent
key={cad_component.cad_component_id}
cad_component={cad_component}
circuitJson={internalCircuitJson}
{layerVisibility.board &&
boardStls.map(({ stlData, color }, index) => (
<STLModel
key={`board-${index - boardStls.length}`}
stlData={stlData}
color={color}
opacity={index === 0 ? 0.95 : 1}
/>
</ThreeErrorBoundary>
))}
))}
{layerVisibility.cadComponents &&
cad_components.map((cad_component) => (
<ThreeErrorBoundary
key={cad_component.cad_component_id}
fallback={({ error }) => (
<Error3d cad_component={cad_component} error={error} />
)}
>
<AnyCadComponent
key={cad_component.cad_component_id}
cad_component={cad_component}
circuitJson={internalCircuitJson}
/>
</ThreeErrorBoundary>
))}
</CadViewerContainer>
)
},
Expand Down
95 changes: 77 additions & 18 deletions src/CadViewerManifold.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,29 +25,78 @@ 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[] = []

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("via-") &&
layerVisibility.fCu &&
layerVisibility.bCu
) {
meshesToAdd.push(mesh)
}
})

textureMeshes.forEach((mesh) => {
if (mesh.name === "top-trace-texture-plane" && layerVisibility.fCu) {
meshesToAdd.push(mesh)
} else if (
mesh.name === "bottom-trace-texture-plane" &&
layerVisibility.bCu
) {
meshesToAdd.push(mesh)
} else if (
mesh.name === "top-silkscreen-texture-plane" &&
layerVisibility.fSilkscreen
) {
meshesToAdd.push(mesh)
} else if (
mesh.name === "bottom-silkscreen-texture-plane" &&
layerVisibility.bSilkscreen
) {
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
}

import type { LayerVisibility } from "./utils/layerDetection"

type CadViewerManifoldProps = {
autoRotateDisabled?: boolean
clickToInteractEnabled?: boolean
layerVisibility?: LayerVisibility
onUserInteraction?: () => void
} & (
| { circuitJson: AnyCircuitElement[]; children?: React.ReactNode }
Expand All @@ -60,6 +109,14 @@ const CadViewerManifold: React.FC<CadViewerManifoldProps> = ({
circuitJson: circuitJsonProp,
autoRotateDisabled,
clickToInteractEnabled,
layerVisibility = {
board: true,
fCu: true,
bCu: true,
fSilkscreen: true,
bSilkscreen: true,
cadComponents: true,
},
onUserInteraction,
children,
}) => {
Expand Down Expand Up @@ -239,20 +296,22 @@ try {
<BoardMeshes
geometryMeshes={geometryMeshes}
textureMeshes={textureMeshes}
layerVisibility={layerVisibility}
/>
{cadComponents.map((cad_component: CadComponent) => (
<ThreeErrorBoundary
key={cad_component.cad_component_id}
fallback={({ error }) => (
<Error3d cad_component={cad_component} error={error} />
)}
>
<AnyCadComponent
cad_component={cad_component}
circuitJson={circuitJson}
/>
</ThreeErrorBoundary>
))}
{layerVisibility.cadComponents &&
cadComponents.map((cad_component: CadComponent) => (
<ThreeErrorBoundary
key={cad_component.cad_component_id}
fallback={({ error }) => (
<Error3d cad_component={cad_component} error={error} />
)}
>
<AnyCadComponent
cad_component={cad_component}
circuitJson={circuitJson}
/>
</ThreeErrorBoundary>
))}
</CadViewerContainer>
)
}
Expand Down
Loading