@tscircuit/3d-viewer 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bun.lockb ADDED
Binary file
package/index.html ADDED
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Vite + React + TS</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@tscircuit/3d-viewer",
3
+ "version": "0.0.1",
4
+ "main": "./dist/index.cjs",
5
+ "scripts": {
6
+ "start": "bun run dev",
7
+ "dev": "bunx --bun vite",
8
+ "build": "tsup ./src/index.tsx --dts --sourcemap",
9
+ "preview": "vite preview",
10
+ "storybook": "storybook dev -p 6006",
11
+ "build-storybook": "storybook build"
12
+ },
13
+ "dependencies": {
14
+ "@jscad/modeling": "^2.12.2",
15
+ "@jscad/regl-renderer": "^2.6.9",
16
+ "@jscad/stl-serializer": "^2.1.17",
17
+ "@react-three/drei": "^9.107.2",
18
+ "@react-three/fiber": "^8.16.8",
19
+ "@types/three": "^0.165.0",
20
+ "react": "^18.3.1",
21
+ "react-dom": "^18.3.1",
22
+ "react-use-gesture": "^9.1.3",
23
+ "three": "^0.165.0",
24
+ "three-stdlib": "^2.30.3"
25
+ },
26
+ "devDependencies": {
27
+ "@biomejs/biome": "^1.8.3",
28
+ "@chromatic-com/storybook": "^1.5.0",
29
+ "@storybook/addon-essentials": "^8.1.10",
30
+ "@storybook/addon-interactions": "^8.1.10",
31
+ "@storybook/addon-links": "^8.1.10",
32
+ "@storybook/addon-onboarding": "^8.1.10",
33
+ "@storybook/blocks": "^8.1.10",
34
+ "@storybook/builder-vite": "^8.1.10",
35
+ "@storybook/react": "^8.1.10",
36
+ "@storybook/react-vite": "^8.1.10",
37
+ "@storybook/test": "^8.1.10",
38
+ "@tscircuit/soup": "^0.0.38",
39
+ "@tscircuit/soup-util": "^0.0.11",
40
+ "@types/react": "^18.3.3",
41
+ "@types/react-dom": "^18.3.0",
42
+ "@vitejs/plugin-react": "^4.3.1",
43
+ "storybook": "^8.1.10",
44
+ "strip-ansi": "^7.1.0",
45
+ "tsup": "^8.1.0",
46
+ "typescript": "^5.5.3",
47
+ "vite": "^5.3.1",
48
+ "vite-tsconfig-paths": "^4.3.2"
49
+ }
50
+ }
package/src/App.tsx ADDED
@@ -0,0 +1,17 @@
1
+ import { useState } from "react"
2
+ import { Renderer } from "./hooks/render"
3
+ import App2 from "./App2"
4
+ import App3 from "./App3"
5
+ import App4 from "./App4"
6
+
7
+ function App() {
8
+ return (
9
+ <div>
10
+ {/* <App2 /> */}
11
+ {/* <App3 /> */}
12
+ <App4 />
13
+ </div>
14
+ )
15
+ }
16
+
17
+ export default App
package/src/App2.tsx ADDED
@@ -0,0 +1,48 @@
1
+ import {
2
+ intersect,
3
+ subtract,
4
+ union,
5
+ } from "@jscad/modeling/src/operations/booleans"
6
+ import { scale, translate } from "@jscad/modeling/src/operations/transforms"
7
+ import { cube, sphere } from "@jscad/modeling/src/primitives"
8
+ import * as renderingDefaults from "@jscad/regl-renderer/types/rendering/renderDefaults"
9
+ import { light } from "./themes"
10
+ import { useState } from "react"
11
+ import soup from "./plated-hole-board.json"
12
+ // import soup from "./bug-pads-and-traces.json"
13
+
14
+ // import { downloadGeometry } from "./helpers"
15
+ import { Renderer } from "./hooks/render"
16
+ import { createBoardGeomFromSoup } from "./soup-to-3d"
17
+
18
+ const shape = union(
19
+ subtract(cube({ size: 3 }), sphere({ radius: 2 })),
20
+ intersect(sphere({ radius: 1.3 }), cube({ size: 2.1 }))
21
+ )
22
+ const shape2 = translate([0, 0, 1.5], shape)
23
+ const shape3 = scale([3, 3, 3], shape2)
24
+
25
+ function App() {
26
+ const [solids] = useState<any[]>([shape3])
27
+ return (
28
+ <div className="App">
29
+ <Renderer
30
+ solids={createBoardGeomFromSoup(soup as any)}
31
+ height={500}
32
+ width={800}
33
+ options={{
34
+ renderingOptions: {
35
+ background: light.bg,
36
+ meshColor: light.color,
37
+ },
38
+ gridOptions: {
39
+ // color: light.grid1,
40
+ // subColor: light.grid2,
41
+ },
42
+ }}
43
+ />
44
+ </div>
45
+ )
46
+ }
47
+
48
+ export default App
package/src/App3.tsx ADDED
@@ -0,0 +1,209 @@
1
+ import {
2
+ Canvas,
3
+ useFrame,
4
+ extend,
5
+ useThree,
6
+ useLoader,
7
+ } from "@react-three/fiber"
8
+ import { Suspense, useEffect, useMemo, useRef, useState } from "react"
9
+ import { OrbitControls, Grid, Outlines } from "@react-three/drei"
10
+ import * as THREE from "three"
11
+ import { CubeWithLabeledSides } from "./three-components/cube-with-labeled-sides"
12
+ import { createBoardGeomFromSoup } from "./soup-to-3d"
13
+ import soup from "./bug-pads-and-traces.json"
14
+ // import soup from "./plated-hole-board.json"
15
+ import stlSerializer from "@jscad/stl-serializer"
16
+ // import { STLLoader } from "three/examples/jsm/loaders/STLLoader"
17
+ import { MTLLoader, OBJLoader, STLLoader } from "three-stdlib"
18
+
19
+ extend({ OrbitControls })
20
+
21
+ function Box(props) {
22
+ const meshRef = useRef<THREE.Mesh>()
23
+ useFrame((state, delta) => {
24
+ if (!meshRef.current) return
25
+ meshRef.current.rotation.x += delta
26
+ meshRef.current.rotation.y += delta
27
+ })
28
+
29
+ return (
30
+ <mesh ref={meshRef} {...props}>
31
+ <boxGeometry args={[1, 1, 1]} />
32
+ <meshStandardMaterial color="orange" />
33
+ </mesh>
34
+ )
35
+ }
36
+
37
+ function blobToBase64Url(blob: Blob) {
38
+ return new Promise((resolve, reject) => {
39
+ const reader = new FileReader()
40
+ reader.onload = () => {
41
+ resolve(reader.result)
42
+ }
43
+ reader.onerror = reject
44
+ reader.readAsDataURL(blob)
45
+ })
46
+ }
47
+
48
+ const jscadGeom = createBoardGeomFromSoup(soup as any)
49
+
50
+ const stlPromises = jscadGeom.map((a) => {
51
+ const rawData = stlSerializer.serialize({ binary: true }, [a])
52
+
53
+ const blobData = new Blob(rawData)
54
+
55
+ const $urlForStl = blobToBase64Url(blobData)
56
+
57
+ return $urlForStl.then((url) => ({
58
+ url,
59
+ color: a.color,
60
+ }))
61
+ })
62
+
63
+ // const entities = entitiesFromSolids({}, ...soupToJscadShape(soup as any))
64
+ // console.log(entities)
65
+
66
+ function TestStl({
67
+ url,
68
+ color,
69
+ index,
70
+ }: {
71
+ index: number
72
+ url: string
73
+ color: any
74
+ }) {
75
+ const threeGeom = useLoader(STLLoader, url)
76
+ const mesh = useRef<THREE.Mesh>()
77
+
78
+ return (
79
+ <mesh ref={mesh as any}>
80
+ <primitive object={threeGeom} attach="geometry" />
81
+ <meshStandardMaterial
82
+ color={color}
83
+ transparent={index === 0}
84
+ opacity={index === 0 ? 0.8 : 1}
85
+ />
86
+ {/* <Outlines thickness={0.05} color="black" opacity={0.25} /> */}
87
+ </mesh>
88
+ )
89
+ }
90
+
91
+ function TestObj({ url }: { url: string }) {
92
+ // const group = useLoader(OBJLoader, url)
93
+ // const materials = useLoader(MTLLoader, url)
94
+ // const obj = useLoader(OBJLoader, url)
95
+
96
+ const [content, setContent] = useState<string | null>(null)
97
+ const [obj, setObj] = useState<any | null>(null)
98
+ useEffect(() => {
99
+ async function loadUrlContent() {
100
+ const response = await fetch(url)
101
+ const text = await response.text()
102
+ setContent(text)
103
+
104
+ // Extract all the sections of the file that have newmtl...endmtl to
105
+ // separate into mtlContent and objContent
106
+
107
+ const mtlContent = text
108
+ .match(/newmtl[\s\S]*?endmtl/g)
109
+ ?.join("\n")!
110
+ .replace(/d 0\./g, "d 1.")!
111
+ const objContent = text.replace(/newmtl[\s\S]*?endmtl/g, "")
112
+
113
+ // console.log({ mtlContent, objContent })
114
+ console.log("MTL", mtlContent)
115
+ // console.log("OBJ", objContent)
116
+
117
+ const mtlLoader = new MTLLoader()
118
+ mtlLoader.setMaterialOptions({
119
+ invertTrProperty: true,
120
+ })
121
+ const materials = mtlLoader.parse(mtlContent, "test.mtl")
122
+ console.log(materials)
123
+
124
+ const objLoader = new OBJLoader()
125
+ objLoader.setMaterials(materials)
126
+ setObj(objLoader.parse(objContent))
127
+ }
128
+ loadUrlContent()
129
+ }, [url])
130
+
131
+ return <>{obj && <primitive object={obj} />}</>
132
+ }
133
+
134
+ function Scene() {
135
+ const [stls, setStls] = useState<Array<{
136
+ url: string
137
+ color: number[]
138
+ }> | null>(null)
139
+ useEffect(() => {
140
+ async function loadStls() {
141
+ const stls = await Promise.all(stlPromises)
142
+ setStls(stls as any)
143
+ }
144
+ loadStls()
145
+ }, [])
146
+ return (
147
+ <>
148
+ <OrbitControls />
149
+ <ambientLight intensity={Math.PI / 2} />
150
+ <pointLight position={[-10, -10, 10]} decay={0} intensity={Math.PI / 4} />
151
+ {/* <Box /> */}
152
+ {(stls ?? []).map((stl, i) => (
153
+ <TestStl index={i} {...stl} key={stl.url} />
154
+ ))}
155
+ {/* <TestObj url="https://modules.easyeda.com/3dmodel/4ee8413127e64716b804db03d4b340ae" /> */}
156
+ <TestObj url="/easyeda-models/84af7f0f6529479fb6b1c809c61d205f" />
157
+ {/* <axesHelper args={[5]} /> */}
158
+ <Grid
159
+ rotation={[Math.PI / 2, 0, 0]}
160
+ infiniteGrid={true}
161
+ cellSize={1}
162
+ sectionSize={10}
163
+ />
164
+ </>
165
+ )
166
+ }
167
+
168
+ export const RotationTracker = () => {
169
+ useFrame(({ camera }) => {
170
+ window.TSCI_MAIN_CAMERA_ROTATION = camera.rotation
171
+ })
172
+
173
+ return <></>
174
+ }
175
+
176
+ export default function App() {
177
+ return (
178
+ <div style={{ width: "100vw", height: "100vh", position: "relative" }}>
179
+ <div
180
+ style={{
181
+ position: "absolute",
182
+ top: 0,
183
+ left: 0,
184
+ width: 120,
185
+ height: 120,
186
+ }}
187
+ >
188
+ <Canvas
189
+ camera={{
190
+ up: [0, 0, 1],
191
+ // rotation: [-Math.PI / 2, 0, 0],
192
+ // lookAt: new THREE.Vector3(0, 0, 0),
193
+ position: [1, 1, 1],
194
+ }}
195
+ >
196
+ <ambientLight intensity={Math.PI / 2} />
197
+ <CubeWithLabeledSides />
198
+ </Canvas>
199
+ </div>
200
+ <Canvas
201
+ scene={{ up: [0, 0, 1] }}
202
+ camera={{ up: [0, 0, 1], position: [5, 5, 5] }}
203
+ >
204
+ <RotationTracker />
205
+ <Scene />
206
+ </Canvas>
207
+ </div>
208
+ )
209
+ }
package/src/App4.tsx ADDED
@@ -0,0 +1,88 @@
1
+ import {
2
+ Canvas,
3
+ useFrame,
4
+ extend,
5
+ useThree,
6
+ useLoader,
7
+ } from "@react-three/fiber"
8
+ import { Suspense, useEffect, useMemo, useRef, useState } from "react"
9
+ import { OrbitControls, Grid, Outlines } from "@react-three/drei"
10
+ import * as THREE from "three"
11
+ import { CubeWithLabeledSides } from "./three-components/cube-with-labeled-sides"
12
+ import { createBoardGeomFromSoup } from "./soup-to-3d"
13
+ import soup from "./bug-pads-and-traces.json"
14
+ // import soup from "./plated-hole-board.json"
15
+ import stlSerializer from "@jscad/stl-serializer"
16
+ // import { STLLoader } from "three/examples/jsm/loaders/STLLoader"
17
+ import { MTLLoader, OBJLoader, STLLoader } from "three-stdlib"
18
+ import { CommonToThree } from "vendor/@jscadui/format-three"
19
+ import { entitiesFromSolids } from "@jscad/regl-renderer"
20
+
21
+ extend({ OrbitControls })
22
+
23
+ const jscadGeom = createBoardGeomFromSoup(soup as any)
24
+
25
+ const threejs = CommonToThree(THREE)(entitiesFromSolids({}, ...jscadGeom), {
26
+ smooth: false,
27
+ })
28
+ console.log(threejs)
29
+
30
+ function Scene() {
31
+ return (
32
+ <>
33
+ <OrbitControls />
34
+ <ambientLight intensity={Math.PI / 2} />
35
+ <pointLight position={[-10, -10, 10]} decay={0} intensity={Math.PI / 4} />
36
+ <primitive object={threejs} />
37
+ <Grid
38
+ rotation={[Math.PI / 2, 0, 0]}
39
+ infiniteGrid={true}
40
+ cellSize={1}
41
+ sectionSize={10}
42
+ />
43
+ </>
44
+ )
45
+ }
46
+
47
+ export const RotationTracker = () => {
48
+ useFrame(({ camera }) => {
49
+ window.TSCI_MAIN_CAMERA_ROTATION = camera.rotation
50
+ })
51
+
52
+ return <></>
53
+ }
54
+
55
+ export default function App() {
56
+ return (
57
+ <div style={{ width: "100vw", height: "100vh", position: "relative" }}>
58
+ <div
59
+ style={{
60
+ position: "absolute",
61
+ top: 0,
62
+ left: 0,
63
+ width: 120,
64
+ height: 120,
65
+ }}
66
+ >
67
+ <Canvas
68
+ camera={{
69
+ up: [0, 0, 1],
70
+ // rotation: [-Math.PI / 2, 0, 0],
71
+ // lookAt: new THREE.Vector3(0, 0, 0),
72
+ position: [1, 1, 1],
73
+ }}
74
+ >
75
+ <ambientLight intensity={Math.PI / 2} />
76
+ <CubeWithLabeledSides />
77
+ </Canvas>
78
+ </div>
79
+ <Canvas
80
+ scene={{ up: [0, 0, 1] }}
81
+ camera={{ up: [0, 0, 1], position: [5, 5, 5] }}
82
+ >
83
+ <RotationTracker />
84
+ <Scene />
85
+ </Canvas>
86
+ </div>
87
+ )
88
+ }
@@ -0,0 +1,56 @@
1
+ import type { AnySoupElement } from "@tscircuit/soup"
2
+ import { useConvertChildrenToSoup } from "./hooks/use-convert-children-to-soup"
3
+ import { su } from "@tscircuit/soup-util"
4
+ import { useMemo } from "react"
5
+ import { createBoardGeomFromSoup } from "./soup-to-3d"
6
+ import { useStlsFromGeom } from "./hooks/use-stls-from-geom"
7
+ import { STLModel } from "./three-components/STLModel"
8
+ import { CadViewerContainer } from "./CadViewerContainer"
9
+ import { MixedStlModel } from "./three-components/MixedStlModel"
10
+
11
+ interface Props {
12
+ soup?: AnySoupElement[]
13
+ children?: any
14
+ }
15
+
16
+ export const CadViewer = ({ soup, children }: Props) => {
17
+ soup ??= useConvertChildrenToSoup(children, soup)
18
+
19
+ // TODO convert board
20
+
21
+ const boardGeom = useMemo(() => createBoardGeomFromSoup(soup), [soup])
22
+
23
+ const { stls, loading } = useStlsFromGeom(boardGeom)
24
+
25
+ const cad_components = su(soup).cad_component.list()
26
+
27
+ // TODO canvas/camera etc.
28
+ return (
29
+ <CadViewerContainer>
30
+ {stls.map(({ stlUrl, color }, index) => (
31
+ <STLModel
32
+ key={stlUrl}
33
+ stlUrl={stlUrl}
34
+ color={color}
35
+ opacity={index === 0 ? 0.95 : 1}
36
+ />
37
+ ))}
38
+ {/* <MixedStlModel url="/easyeda-models/84af7f0f6529479fb6b1c809c61d205f" /> */}
39
+ <MixedStlModel
40
+ url="/easyeda-models/dc694c23844346e9981bdbac7bb76421"
41
+ position={[0, 0, 0.5]}
42
+ rotation={[0, 0, Math.PI / 2]}
43
+ />
44
+ <MixedStlModel
45
+ url="/easyeda-models/c7acac53bcbc44d68fbab8f60a747688"
46
+ position={[-5.65, 0, 0.5]}
47
+ rotation={[0, 0, Math.PI / 2]}
48
+ />
49
+ <MixedStlModel
50
+ url="/easyeda-models/c7acac53bcbc44d68fbab8f60a747688"
51
+ position={[6.75, 0, 0.5]}
52
+ rotation={[0, 0, 0]}
53
+ />
54
+ </CadViewerContainer>
55
+ )
56
+ }
@@ -0,0 +1,73 @@
1
+ import {
2
+ Canvas,
3
+ useFrame,
4
+ extend,
5
+ useThree,
6
+ useLoader,
7
+ } from "@react-three/fiber"
8
+ import { Suspense, useEffect, useMemo, useRef, useState } from "react"
9
+ import { OrbitControls, Grid, Outlines } from "@react-three/drei"
10
+ import * as THREE from "three"
11
+ import { CubeWithLabeledSides } from "./three-components/cube-with-labeled-sides"
12
+ import { createBoardGeomFromSoup } from "./soup-to-3d"
13
+ import soup from "./bug-pads-and-traces.json"
14
+ // import soup from "./plated-hole-board.json"
15
+ import stlSerializer from "@jscad/stl-serializer"
16
+ // import { STLLoader } from "three/examples/jsm/loaders/STLLoader"
17
+ import { MTLLoader, OBJLoader, STLLoader } from "three-stdlib"
18
+
19
+ export const RotationTracker = () => {
20
+ useFrame(({ camera }) => {
21
+ window.TSCI_MAIN_CAMERA_ROTATION = camera.rotation
22
+ })
23
+
24
+ return <></>
25
+ }
26
+
27
+ export const CadViewerContainer = ({ children }: { children: any }) => {
28
+ return (
29
+ <div style={{ position: "relative", width: "100%", height: "100%" }}>
30
+ <div
31
+ style={{
32
+ position: "absolute",
33
+ top: 0,
34
+ left: 0,
35
+ width: 120,
36
+ height: 120,
37
+ }}
38
+ >
39
+ <Canvas
40
+ camera={{
41
+ up: [0, 0, 1],
42
+ // rotation: [-Math.PI / 2, 0, 0],
43
+ // lookAt: new THREE.Vector3(0, 0, 0),
44
+ position: [1, 1, 1],
45
+ }}
46
+ >
47
+ <ambientLight intensity={Math.PI / 2} />
48
+ <CubeWithLabeledSides />
49
+ </Canvas>
50
+ </div>
51
+ <Canvas
52
+ scene={{ up: [0, 0, 1] }}
53
+ camera={{ up: [0, 0, 1], position: [5, 5, 5] }}
54
+ >
55
+ <RotationTracker />
56
+ <OrbitControls autoRotate autoRotateSpeed={1} />
57
+ <ambientLight intensity={Math.PI / 2} />
58
+ <pointLight
59
+ position={[-10, -10, 10]}
60
+ decay={0}
61
+ intensity={Math.PI / 4}
62
+ />
63
+ <Grid
64
+ rotation={[Math.PI / 2, 0, 0]}
65
+ infiniteGrid={true}
66
+ cellSize={1}
67
+ sectionSize={10}
68
+ />
69
+ {children}
70
+ </Canvas>
71
+ </div>
72
+ )
73
+ }
@@ -0,0 +1,3 @@
1
+ export type GeomContext = {
2
+ pcbThickness: number
3
+ }