Skip to content
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

<Pcb3D /> component #137

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
26 changes: 10 additions & 16 deletions src/AnyCadComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,18 @@
import type { AnyCircuitElement, CadComponent } from "circuit-json"
import { useConvertChildrenToSoup } from "./hooks/use-convert-children-to-soup"
import { su } from "@tscircuit/soup-util"
import { useMemo, useState } from "react"
import { createBoardGeomFromSoup } from "./soup-to-3d"
import { useStlsFromGeom } from "./hooks/use-stls-from-geom"
import { STLModel } from "./three-components/STLModel"
import { CadViewerContainer } from "./CadViewerContainer"
import type { CadComponent } from "circuit-json"
import type { HoverProps } from "./ContainerWithTooltip"
import { MixedStlModel } from "./three-components/MixedStlModel"
import { Euler } from "three"
import { JscadModel } from "./three-components/JscadModel"
import { Footprinter3d } from "jscad-electronics"
import { FootprinterModel } from "./three-components/FootprinterModel"
import { tuple } from "./utils/tuple"

export const AnyCadComponent = ({
export type * as tooltip from "./ContainerWithTooltip"

export function AnyCadComponent({
cad_component,
onHover = () => {},
onUnhover = () => {},
isHovered = false,
}: {
cad_component: CadComponent
onHover?: (e: any) => void
isHovered?: boolean
}) => {
}: HoverProps & { cad_component: CadComponent }) {
const url = cad_component.model_obj_url ?? cad_component.model_stl_url
const rotationOffset = cad_component.rotation
? tuple(
Expand All @@ -47,6 +38,7 @@ export const AnyCadComponent = ({
}
rotation={rotationOffset}
onHover={onHover}
onUnhover={onUnhover}
isHovered={isHovered}
/>
)
Expand All @@ -59,6 +51,7 @@ export const AnyCadComponent = ({
jscadPlan={cad_component.model_jscad as any}
rotationOffset={rotationOffset}
onHover={onHover}
onUnhover={onUnhover}
isHovered={isHovered}
/>
)
Expand All @@ -79,6 +72,7 @@ export const AnyCadComponent = ({
rotationOffset={rotationOffset}
footprint={cad_component.footprinter_string}
onHover={onHover}
onUnhover={onUnhover}
isHovered={isHovered}
/>
)
Expand Down
89 changes: 20 additions & 69 deletions src/CadViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,10 @@
import type { AnySoupElement } from "@tscircuit/soup"
import type * as React from "react"
import type * as THREE from "three"
import { useConvertChildrenToSoup } from "./hooks/use-convert-children-to-soup"
import { su } from "@tscircuit/soup-util"
import { useEffect, useMemo, useState, forwardRef } from "react"
import { createBoardGeomFromSoup } from "./soup-to-3d"
import { useStlsFromGeom } from "./hooks/use-stls-from-geom"
import { STLModel } from "./three-components/STLModel"
import { useState, forwardRef } from "react"
import { CadViewerContainer } from "./CadViewerContainer"
import { MixedStlModel } from "./three-components/MixedStlModel"
import { Euler } from "three"
import { JscadModel } from "./three-components/JscadModel"
import { Footprinter3d } from "jscad-electronics"
import { FootprinterModel } from "./three-components/FootprinterModel"
import { tuple } from "./utils/tuple"
import { AnyCadComponent } from "./AnyCadComponent"
import { Text } from "@react-three/drei"
import { ThreeErrorBoundary } from "./three-components/ThreeErrorBoundary"
import { Error3d } from "./three-components/Error3d"
import { Pcb3D } from "./Pcb3D"

interface Props {
soup?: AnySoupElement[]
Expand All @@ -30,64 +17,28 @@ export const CadViewer = forwardRef<
const [hoveredComponent, setHoveredComponent] = useState<null | {
cad_component_id: string
name: string
mousePosition: [number, number, number]
point: THREE.Vector3
}>(null)
soup ??= useConvertChildrenToSoup(children, soup) as any

if (!soup) return null

const boardGeom = useMemo(() => {
if (!soup.some((e) => e.type === "pcb_board")) return null
return createBoardGeomFromSoup(soup)
}, [soup])

const { stls: boardStls, loading } = useStlsFromGeom(boardGeom)

const cad_components = su(soup).cad_component.list()

return (
<CadViewerContainer ref={ref} hoveredComponent={hoveredComponent}>
{boardStls.map(({ stlUrl, color }, index) => (
<STLModel
key={stlUrl}
stlUrl={stlUrl}
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}
onHover={(e) => {
// TODO this should be done by onUnhover
if (!e) {
setHoveredComponent(null)
}
if (!e.mousePosition) return

const componentName = su(soup as any).source_component.getUsing({
source_component_id: cad_component.source_component_id,
})?.name
setHoveredComponent({
cad_component_id: cad_component.cad_component_id,
name: componentName ?? "<unknown>",
mousePosition: e.mousePosition,
})
}}
cad_component={cad_component}
isHovered={
hoveredComponent?.cad_component_id ===
cad_component.cad_component_id
}
/>
</ThreeErrorBoundary>
))}
<Pcb3D
soup={soup}
onHover={(e) => {
const componentName = su(soup as any).source_component.getUsing({
source_component_id: e.cad_component.source_component_id,
})?.name
setHoveredComponent({
cad_component_id: e.cad_component.cad_component_id,
name: componentName ?? "<unknown>",
point: e.point,
})
}}
onUnhover={() => setHoveredComponent(null)}
hoverAt={hoveredComponent?.cad_component_id}
>
{children}
</Pcb3D>
</CadViewerContainer>
)
})
4 changes: 2 additions & 2 deletions src/CadViewerContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface Props {
hoveredComponent: {
cad_component_id: string
name: string
mousePosition: [number, number, number]
point: THREE.Vector3
} | null
}

Expand Down Expand Up @@ -71,7 +71,7 @@ export const CadViewerContainer = forwardRef<
<object3D ref={ref}>{children}</object3D>
{hoveredComponent && (
<Html
position={hoveredComponent.mousePosition}
position={hoveredComponent.point}
style={{
fontFamily: "sans-serif",
transform: "translate3d(50%, 50%, 0)",
Expand Down
47 changes: 25 additions & 22 deletions src/ContainerWithTooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,60 +1,65 @@
import { Html } from "@react-three/drei"
import { GroupProps, useThree } from "@react-three/fiber"
import type { GroupProps, ThreeEvent } from "@react-three/fiber"
import { useRef, useCallback } from "react"
import type { Vector3 } from "three"
import * as THREE from "three"
import type * as React from "react"

const Group = (props: GroupProps) => <group {...props} />

const ContainerWithTooltip = ({
export interface RaycastEvent {
point: THREE.Vector3
}

export interface HoverProps<E extends RaycastEvent = RaycastEvent> {
onHover: (event: E) => void
onUnhover: (event: E) => void
isHovered: boolean
}

export function ContainerWithTooltip({
children,
isHovered,
onHover,
onUnhover,
position,
}: {
children: React.ReactNode
position?: Vector3 | [number, number, number]
onHover: (e: any) => void
isHovered: boolean
}) => {
}: React.PropsWithChildren<
HoverProps & { position?: THREE.Vector3 | [number, number, number] }
>) {
const lastValidPointRef = useRef<THREE.Vector3 | null>(null)

const handlePointerEnter = useCallback(
(e: any) => {
(e: ThreeEvent<PointerEvent>) => {
e.stopPropagation()

try {
// Fallback to event position if raycaster fails
const point =
e.point ||
(e.intersections && e.intersections.length > 0
? e.intersections[0].point
? e.intersections[0]!.point
: null) ||
(position
? new THREE.Vector3(...(position as [number, number, number]))
: null)

if (point) {
lastValidPointRef.current = point
onHover({ mousePosition: [point.x, point.y, point.z] })
} else {
onHover({})
onHover({ point })
}
} catch (error) {
console.warn("Hover event error:", error)
onHover({})
}
},
[position],
)

const handlePointerLeave = useCallback(
(e: any) => {
(e: ThreeEvent<PointerEvent>) => {
const point = lastValidPointRef.current
if (!point) return

e.stopPropagation()
lastValidPointRef.current = null

// TODO REPLACE WITH onUnhover
onHover(null)
onUnhover({ point })
},
[onHover],
)
Expand All @@ -69,5 +74,3 @@ const ContainerWithTooltip = ({
</Group>
)
}

export default ContainerWithTooltip
89 changes: 89 additions & 0 deletions src/Pcb3D.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import type * as React from "react"
import type { AnySoupElement } from "@tscircuit/soup"
import { useMemo } from "react"
import { su, type SoupUtilObjects } from "@tscircuit/soup-util"
import { hash } from "./utils/buffer.ts"
import { geom2stl } from "./geoms/converter.ts"
import { createBoardGeomFromSoup } from "./soup-to-3d"
import { useConvertChildrenToSoup } from "./hooks/use-convert-children-to-soup"
import { STLModel } from "./three-components/STLModel"
import { ThreeErrorBoundary } from "./three-components/ThreeErrorBoundary"
import { AnyCadComponent, type tooltip } from "./AnyCadComponent"
import { Error3d } from "./three-components/Error3d"

export interface RaycastEvent extends tooltip.RaycastEvent {
source_component: SoupUtilObjects["source_component"]
cad_component: ReturnType<SoupUtilObjects["cad_component"]["list"]>[0]
}

export interface HoverProps
extends Omit<tooltip.HoverProps<RaycastEvent>, "isHovered"> {
hoverAt?: RaycastEvent["cad_component"]["cad_component_id"]
}

export function Pcb3D({
soup,
children,
onHover,
onUnhover,
hoverAt,
}: React.PropsWithChildren<HoverProps & { soup?: AnySoupElement[] }>) {
soup ??= useConvertChildrenToSoup(children, soup) as any

if (!soup) return null

const boardStls = useMemo(() => {
if (!soup.some((e) => e.type === "pcb_board")) return []
// TODO: dedupe works cause by createBoardGeomFromSoup() call su(soup)
return createBoardGeomFromSoup(soup).map((g) => ({
stlData: geom2stl(g),
color: g.color,
}))
}, [soup])

const soupUtil = su(soup)
const cad_components = soupUtil.cad_component.list()

return [
...boardStls.map(({ stlData, color }, index) => (
<STLModel
key={hash(stlData)}
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}
onHover={
onHover &&
((e) =>
onHover({
source_component: soupUtil.source_component,
cad_component,
...e,
}))
}
onUnhover={
onUnhover &&
((e) =>
onUnhover({
source_component: soupUtil.source_component,
cad_component,
...e,
}))
}
cad_component={cad_component}
isHovered={hoverAt === cad_component.cad_component_id}
/>
</ThreeErrorBoundary>
)),
]
}
8 changes: 8 additions & 0 deletions src/geoms/converter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Geom3 } from "@jscad/modeling/src/geometries/types"
import stlSerializer from "@jscad/stl-serializer"
import { join } from "../utils/buffer.ts"

export function geom2stl(geom: Geom3) {
const rawData = stlSerializer.serialize({ binary: true }, [geom])
return join(rawData)
}
Loading