@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.
@@ -0,0 +1,213 @@
1
+ [
2
+ {
3
+ "type": "source_component",
4
+ "source_component_id": "simple_resistor_0",
5
+ "name": "R1",
6
+ "supplier_part_numbers": {},
7
+ "ftype": "simple_resistor",
8
+ "resistance": "10kohm"
9
+ },
10
+ {
11
+ "type": "schematic_component",
12
+ "source_component_id": "simple_resistor_0",
13
+ "schematic_component_id": "schematic_component_simple_resistor_0",
14
+ "rotation": 0,
15
+ "size": {
16
+ "width": 1,
17
+ "height": 0.3
18
+ },
19
+ "center": {
20
+ "x": 0,
21
+ "y": 0
22
+ }
23
+ },
24
+ {
25
+ "type": "source_port",
26
+ "name": "left",
27
+ "source_port_id": "source_port_0",
28
+ "source_component_id": "simple_resistor_0"
29
+ },
30
+ {
31
+ "type": "schematic_port",
32
+ "schematic_port_id": "schematic_port_0",
33
+ "source_port_id": "source_port_0",
34
+ "center": {
35
+ "x": -0.5,
36
+ "y": 0
37
+ },
38
+ "facing_direction": "left",
39
+ "schematic_component_id": "schematic_component_simple_resistor_0"
40
+ },
41
+ {
42
+ "type": "pcb_port",
43
+ "pcb_port_id": "pcb_port_0",
44
+ "source_port_id": "source_port_0",
45
+ "pcb_component_id": "pcb_component_simple_resistor_0",
46
+ "x": 0,
47
+ "y": 0
48
+ },
49
+ {
50
+ "type": "source_port",
51
+ "name": "right",
52
+ "source_port_id": "source_port_1",
53
+ "source_component_id": "simple_resistor_0"
54
+ },
55
+ {
56
+ "type": "schematic_port",
57
+ "schematic_port_id": "schematic_port_1",
58
+ "source_port_id": "source_port_1",
59
+ "center": {
60
+ "x": 0.5,
61
+ "y": 0
62
+ },
63
+ "facing_direction": "right",
64
+ "schematic_component_id": "schematic_component_simple_resistor_0"
65
+ },
66
+ {
67
+ "type": "pcb_port",
68
+ "pcb_port_id": "pcb_port_1",
69
+ "source_port_id": "source_port_1",
70
+ "pcb_component_id": "pcb_component_simple_resistor_0",
71
+ "x": 0,
72
+ "y": 0
73
+ },
74
+ {
75
+ "type": "schematic_text",
76
+ "text": "R1",
77
+ "schematic_text_id": "schematic_text_0",
78
+ "schematic_component_id": "schematic_component_simple_resistor_0",
79
+ "anchor": "left",
80
+ "position": {
81
+ "x": -0.2,
82
+ "y": -0.5
83
+ },
84
+ "rotation": 0
85
+ },
86
+ {
87
+ "type": "schematic_text",
88
+ "text": "10kohm",
89
+ "schematic_text_id": "schematic_text_1",
90
+ "schematic_component_id": "schematic_component_simple_resistor_0",
91
+ "anchor": "left",
92
+ "position": {
93
+ "x": -0.2,
94
+ "y": -0.3
95
+ },
96
+ "rotation": 0
97
+ },
98
+ {
99
+ "type": "pcb_component",
100
+ "source_component_id": "simple_resistor_0",
101
+ "pcb_component_id": "pcb_component_simple_resistor_0",
102
+ "layer": "top",
103
+ "center": {
104
+ "x": 0,
105
+ "y": 0
106
+ },
107
+ "rotation": 0,
108
+ "width": 4.2,
109
+ "height": 1.2
110
+ },
111
+ {
112
+ "type": "pcb_plated_hole",
113
+ "x": -1.5,
114
+ "y": 0,
115
+ "layers": [
116
+ "top",
117
+ "bottom"
118
+ ],
119
+ "hole_diameter": 1,
120
+ "outer_diameter": 1.2,
121
+ "port_hints": [
122
+ "1"
123
+ ]
124
+ },
125
+ {
126
+ "type": "pcb_plated_hole",
127
+ "x": 1.5,
128
+ "y": 0,
129
+ "layers": [
130
+ "top",
131
+ "bottom"
132
+ ],
133
+ "hole_diameter": 1,
134
+ "outer_diameter": 1.2,
135
+ "port_hints": [
136
+ "2"
137
+ ]
138
+ },
139
+ {
140
+ "type": "pcb_silkscreen_path",
141
+ "layer": "top",
142
+ "pcb_component_id": "pcb_component_simple_resistor_0",
143
+ "pcb_silkscreen_path_id": "pcb_silkscreen_path_0",
144
+ "route": [
145
+ {
146
+ "x": -0.7,
147
+ "y": -0.8
148
+ },
149
+ {
150
+ "x": -0.7,
151
+ "y": 0.8
152
+ },
153
+ {
154
+ "x": -0.2333333333333333,
155
+ "y": 0.8
156
+ },
157
+ {
158
+ "x": -0.2155718909193002,
159
+ "y": 0.7107071991148124
160
+ },
161
+ {
162
+ "x": -0.16499158227686106,
163
+ "y": 0.635008417723139
164
+ },
165
+ {
166
+ "x": -0.0892928008851876,
167
+ "y": 0.5844281090806999
168
+ },
169
+ {
170
+ "x": 1.4287545990052454e-17,
171
+ "y": 0.5666666666666668
172
+ },
173
+ {
174
+ "x": 0.08929280088518761,
175
+ "y": 0.5844281090806999
176
+ },
177
+ {
178
+ "x": 0.1649915822768611,
179
+ "y": 0.635008417723139
180
+ },
181
+ {
182
+ "x": 0.2155718909193002,
183
+ "y": 0.7107071991148124
184
+ },
185
+ {
186
+ "x": 0.2333333333333333,
187
+ "y": 0.8
188
+ },
189
+ {
190
+ "x": 0.7,
191
+ "y": 0.8
192
+ },
193
+ {
194
+ "x": 0.7,
195
+ "y": -0.8
196
+ },
197
+ {
198
+ "x": -0.7,
199
+ "y": -0.8
200
+ }
201
+ ],
202
+ "stroke_width": 0.1
203
+ },
204
+ {
205
+ "type": "pcb_board",
206
+ "center": {
207
+ "x": 0,
208
+ "y": 0
209
+ },
210
+ "width": 5,
211
+ "height": 5
212
+ }
213
+ ]
@@ -0,0 +1,162 @@
1
+ import type { Geom3 } from "@jscad/modeling/src/geometries/types"
2
+ import type { AnySoupElement, PCBPlatedHole } from "@tscircuit/soup"
3
+ import { su } from "@tscircuit/soup-util"
4
+ import { translate } from "@jscad/modeling/src/operations/transforms"
5
+ import { cuboid, cylinder, line } from "@jscad/modeling/src/primitives"
6
+ import { colorize } from "@jscad/modeling/src/colors"
7
+ import { subtract } from "@jscad/modeling/src/operations/booleans"
8
+ import { platedHole } from "../geoms/plated-hole"
9
+ import { M, colors } from "../geoms/constants"
10
+ import { extrudeLinear } from "@jscad/modeling/src/operations/extrusions"
11
+ import { expand } from "@jscad/modeling/src/operations/expansions"
12
+
13
+ export const createBoardGeomFromSoup = (soup: AnySoupElement[]): Geom3[] => {
14
+ const board = su(soup).pcb_board.list()[0]
15
+ if (!board) {
16
+ throw new Error("No pcb_board found")
17
+ }
18
+
19
+ const plated_holes = su(soup).pcb_plated_hole.list()
20
+ const pads = su(soup).pcb_smtpad.list()
21
+ const traces = su(soup).pcb_trace.list()
22
+ const pcb_vias = su(soup).pcb_via.list()
23
+
24
+ // PCB Board
25
+ let boardGeom = cuboid({ size: [board.width, board.height, 1.2] })
26
+
27
+ const platedHoleGeoms: Geom3[] = []
28
+ const padGeoms: Geom3[] = []
29
+ const traceGeoms: Geom3[] = []
30
+ const ctx = {
31
+ pcbThickness: 1.2,
32
+ }
33
+
34
+ const addPlatedHole = (plated_hole: PCBPlatedHole) => {
35
+ if (!(plated_hole as any).shape) plated_hole.shape = "circle"
36
+ if (plated_hole.shape === "circle") {
37
+ const cyGeom = cylinder({
38
+ center: [plated_hole.x, plated_hole.y, 0],
39
+ radius: plated_hole.hole_diameter / 2 + M,
40
+ })
41
+ boardGeom = subtract(boardGeom, cyGeom)
42
+
43
+ const platedHoleGeom = platedHole(plated_hole, ctx)
44
+ platedHoleGeoms.push(platedHoleGeom)
45
+ }
46
+ }
47
+
48
+ for (const plated_hole of plated_holes) {
49
+ addPlatedHole(plated_hole)
50
+ }
51
+
52
+ for (const pad of pads) {
53
+ if (pad.shape === "rect") {
54
+ const padGeom = colorize(
55
+ colors.copper,
56
+ cuboid({
57
+ center: [pad.x, pad.y, 1.2 / 2 + M],
58
+ size: [pad.width, pad.height, M],
59
+ })
60
+ )
61
+ padGeoms.push(padGeom)
62
+ } else if (pad.shape === "circle") {
63
+ const padGeom = colorize(
64
+ colors.copper,
65
+ cylinder({
66
+ center: [pad.x, pad.y, 1.2 / 2 + M],
67
+ radius: pad.radius,
68
+ height: M,
69
+ })
70
+ )
71
+ padGeoms.push(padGeom)
72
+ }
73
+ }
74
+
75
+ for (const { route: mixedRoute } of traces) {
76
+ const subRoutes = mixedRoute.reduce(
77
+ (c, p) => {
78
+ // @ts-ignore
79
+ const lastLayer = c.current?.[c.current.length - 1]?.layer
80
+ if (
81
+ p.route_type === "via" ||
82
+ (p.route_type === "wire" && p.layer !== lastLayer)
83
+ ) {
84
+ if (c.current.length > 2) {
85
+ c.allPrev.push(c.current)
86
+ }
87
+ c.current = p.route_type === "wire" ? [p] : []
88
+ return c
89
+ }
90
+ c.current.push(p)
91
+ return c
92
+ },
93
+ {
94
+ current: [] as typeof mixedRoute,
95
+ allPrev: [] as Array<typeof mixedRoute>,
96
+ }
97
+ )
98
+ for (const route of subRoutes.allPrev.concat([subRoutes.current])) {
99
+ // TODO break into segments based on layers
100
+ const linePath = line(route.map((p) => [p.x, p.y]))
101
+
102
+ const layer = route[0]!.route_type === "wire" ? route[0]!.layer : "top"
103
+ const layerSign = layer === "top" ? 1 : -1
104
+ // traceGeoms.push(traceGeom)
105
+ let traceGeom = translate(
106
+ [0, 0, (layerSign * 1.2) / 2],
107
+ extrudeLinear(
108
+ { height: M * layerSign },
109
+ expand({ delta: 0.1, corners: "edge" }, linePath)
110
+ )
111
+ )
112
+
113
+ // HACK: Subtract all vias from every trace- this mostly is because the
114
+ // vias aren't inside the route- we should probably pre-filter to make sure
115
+ // that vias are only near the route
116
+ for (const via of pcb_vias) {
117
+ traceGeom = subtract(
118
+ traceGeom,
119
+ cylinder({
120
+ center: [via.x, via.y, 0],
121
+ radius: via.outer_diameter / 2,
122
+ height: 5,
123
+ })
124
+ )
125
+ }
126
+
127
+ traceGeom = colorize(colors.fr4GreenSolderWithMask, traceGeom)
128
+
129
+ traceGeoms.push(traceGeom)
130
+ }
131
+ for (const via of mixedRoute.filter((p) => p.route_type === "via")) {
132
+ if (via.route_type !== "via") continue // TODO remove when ts is smart
133
+
134
+ addPlatedHole({
135
+ x: via.x,
136
+ y: via.y,
137
+ hole_diameter: 0.8,
138
+ outer_diameter: 1.6,
139
+ shape: "circle",
140
+ layers: ["top", "bottom"],
141
+ type: "pcb_plated_hole",
142
+ })
143
+ }
144
+ }
145
+
146
+ for (const via of pcb_vias) {
147
+ addPlatedHole({
148
+ x: via.x,
149
+ y: via.y,
150
+ hole_diameter: via.hole_diameter,
151
+ outer_diameter: via.outer_diameter,
152
+ shape: "circle",
153
+ layers: ["top", "bottom"],
154
+ type: "pcb_plated_hole",
155
+ })
156
+ }
157
+
158
+ // Colorize to a PCB green color: #05A32E
159
+ boardGeom = colorize(colors.fr4Green, boardGeom)
160
+
161
+ return [boardGeom, ...platedHoleGeoms, ...padGeoms, ...traceGeoms]
162
+ }
package/src/themes.ts ADDED
@@ -0,0 +1,7 @@
1
+ export const light = {
2
+ name: "Light",
3
+ color: [0, 0.6, 1, 1],
4
+ bg: [0.95, 0.95, 0.95, 1],
5
+ grid1: [0, 0, 0, 0.2],
6
+ grid2: [0, 0, 1, 0.1],
7
+ }
@@ -0,0 +1,13 @@
1
+ import type { Geom3 } from "@jscad/modeling/src/geometries/types"
2
+
3
+ export const GeomModel = ({ geom }: { geom: Geom3[] | Geom3 }) => {
4
+ if (!Array.isArray(geom)) {
5
+ geom = [geom]
6
+ }
7
+
8
+ // TODO useGeomToStl
9
+
10
+ // TODO STLModel
11
+
12
+ return null
13
+ }
@@ -0,0 +1,61 @@
1
+ import { useEffect, useState } from "react"
2
+ import { Euler, Vector3 } from "three"
3
+ import { MTLLoader, OBJLoader } from "three-stdlib"
4
+
5
+ export function MixedStlModel({
6
+ url,
7
+ position,
8
+ rotation,
9
+ }: {
10
+ url: string
11
+ position?: Vector3 | [number, number, number]
12
+ rotation?: Euler | [number, number, number]
13
+ }) {
14
+ // const group = useLoader(OBJLoader, url)
15
+ // const materials = useLoader(MTLLoader, url)
16
+ // const obj = useLoader(OBJLoader, url)
17
+
18
+ const [obj, setObj] = useState<any | null>(null)
19
+ useEffect(() => {
20
+ async function loadUrlContent() {
21
+ const response = await fetch(url)
22
+ const text = await response.text()
23
+
24
+ // Extract all the sections of the file that have newmtl...endmtl to
25
+ // separate into mtlContent and objContent
26
+
27
+ const mtlContent = text
28
+ .match(/newmtl[\s\S]*?endmtl/g)
29
+ ?.join("\n")!
30
+ .replace(/d 0\./g, "d 1.")!
31
+ const objContent = text.replace(/newmtl[\s\S]*?endmtl/g, "")
32
+
33
+ const mtlLoader = new MTLLoader()
34
+ mtlLoader.setMaterialOptions({
35
+ invertTrProperty: true,
36
+ })
37
+ const materials = mtlLoader.parse(
38
+ // Grayscale the colors, for some reason everything from JLCPCB is
39
+ // a bit red, it doesn't look right. The grayscale version looks OK,
40
+ // it's a HACK because we only take the second color rather than
41
+ // averaging the colors
42
+ mtlContent.replace(
43
+ /Kd\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)/g,
44
+ "Kd $2 $2 $2"
45
+ ),
46
+ "test.mtl"
47
+ )
48
+
49
+ const objLoader = new OBJLoader()
50
+ objLoader.setMaterials(materials)
51
+ setObj(objLoader.parse(objContent))
52
+ }
53
+ loadUrlContent()
54
+ }, [url])
55
+
56
+ return (
57
+ <group rotation={rotation} position={position}>
58
+ {obj && <primitive object={obj} />}
59
+ </group>
60
+ )
61
+ }
@@ -0,0 +1,33 @@
1
+ import { useLoader } from "@react-three/fiber"
2
+ import { useRef } from "react"
3
+ import * as THREE from "three"
4
+ import { MTLLoader, OBJLoader, STLLoader } from "three-stdlib"
5
+
6
+ export function STLModel({
7
+ stlUrl,
8
+ mtlUrl,
9
+ color,
10
+ opacity = 1,
11
+ }: {
12
+ stlUrl: string
13
+ color?: any
14
+ mtlUrl?: string
15
+ opacity?: number
16
+ }) {
17
+ const geom = useLoader(STLLoader, stlUrl)
18
+ const mesh = useRef<THREE.Mesh>()
19
+
20
+ // TODO handle mtl url
21
+
22
+ return (
23
+ <mesh ref={mesh as any}>
24
+ <primitive object={geom} attach="geometry" />
25
+ <meshStandardMaterial
26
+ color={color}
27
+ transparent={opacity !== 1}
28
+ opacity={opacity}
29
+ />
30
+ {/* <Outlines thickness={0.05} color="black" opacity={0.25} /> */}
31
+ </mesh>
32
+ )
33
+ }
@@ -0,0 +1,116 @@
1
+ import React, { useRef } from "react"
2
+ import { Canvas, useFrame } from "@react-three/fiber"
3
+ import { OrbitControls, Text } from "@react-three/drei"
4
+ import * as THREE from "three"
5
+
6
+ declare global {
7
+ interface Window {
8
+ TSCI_MAIN_CAMERA_ROTATION: THREE.Euler
9
+ }
10
+ }
11
+ window.TSCI_MAIN_CAMERA_ROTATION = new THREE.Euler(0, 0, 0)
12
+
13
+ function computePointInFront(rotationVector, distance) {
14
+ // Create a quaternion from the rotation vector
15
+ const quaternion = new THREE.Quaternion().setFromEuler(
16
+ new THREE.Euler(rotationVector.x, rotationVector.y, rotationVector.z)
17
+ )
18
+
19
+ // Create a vector pointing forward (along the negative z-axis)
20
+ const forwardVector = new THREE.Vector3(0, 0, 1)
21
+
22
+ // Apply the rotation to the forward vector
23
+ forwardVector.applyQuaternion(quaternion)
24
+
25
+ // Scale the rotated vector by the distance
26
+ const result = forwardVector.multiplyScalar(distance)
27
+
28
+ return result
29
+ }
30
+
31
+ export const CubeWithLabeledSides = ({}: any) => {
32
+ const ref = useRef<THREE.Mesh>()
33
+ const rotationTrackingRef = useRef({ lastRotation: new THREE.Euler() })
34
+ useFrame((state, delta) => {
35
+ if (!ref.current) return
36
+
37
+ const mainRot = window.TSCI_MAIN_CAMERA_ROTATION
38
+
39
+ // Use window.TSCI_CAMERA_ROTATION to compute the position of the camera
40
+ const cameraPosition = computePointInFront(mainRot, 2)
41
+
42
+ state.camera.position.copy(cameraPosition)
43
+ state.camera.lookAt(0, 0, 0)
44
+ })
45
+ return (
46
+ <mesh ref={ref as any} rotation={[Math.PI / 2, 0, 0]}>
47
+ <boxGeometry args={[1, 1, 1]} />
48
+ <meshStandardMaterial color="white" />
49
+ <Text position={[0, 0, 0.51]} fontSize={0.25} color="black">
50
+ Front
51
+ </Text>
52
+ <Text
53
+ position={[0, 0, -0.51]}
54
+ fontSize={0.25}
55
+ color="black"
56
+ rotation={[0, Math.PI, 0]}
57
+ >
58
+ Back
59
+ </Text>
60
+ <Text
61
+ position={[0.51, 0, 0]}
62
+ fontSize={0.25}
63
+ color="black"
64
+ rotation={[0, Math.PI / 2, 0]}
65
+ >
66
+ Right
67
+ </Text>
68
+ <Text
69
+ position={[-0.51, 0, 0]}
70
+ fontSize={0.25}
71
+ color="black"
72
+ rotation={[0, -Math.PI / 2, 0]}
73
+ >
74
+ Left
75
+ </Text>
76
+ <Text
77
+ position={[0, 0.51, 0]}
78
+ fontSize={0.25}
79
+ color="black"
80
+ rotation={[-Math.PI / 2, 0, 0]}
81
+ >
82
+ Top
83
+ </Text>
84
+ <Text
85
+ position={[0, -0.51, 0]}
86
+ fontSize={0.25}
87
+ color="black"
88
+ rotation={[Math.PI / 2, 0, 0]}
89
+ >
90
+ Bottom
91
+ </Text>
92
+ <lineSegments
93
+ args={[new THREE.EdgesGeometry(new THREE.BoxGeometry(1, 1, 1))]}
94
+ material={
95
+ new THREE.LineBasicMaterial({
96
+ color: 0x000000,
97
+ linewidth: 2,
98
+ })
99
+ }
100
+ />
101
+ </mesh>
102
+ )
103
+ }
104
+
105
+ // const LabeledCubeScene = () => {
106
+ // return (
107
+ // <Canvas camera={{ position: [1.5, 1.5, 1.5] }}>
108
+ // <ambientLight intensity={0.5} />
109
+ // <pointLight position={[10, 10, 10]} />
110
+ // <LabeledCube />
111
+ // <OrbitControls />
112
+ // </Canvas>
113
+ // )
114
+ // }
115
+
116
+ // export default LabeledCubeScene
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,10 @@
1
+ import { fn } from "@storybook/test"
2
+ import { CadViewer } from "src/CadViewer"
3
+ import bugsPadsAndTracesSoup from "./assets/soic-with-traces.json"
4
+
5
+ export const Default = () => <CadViewer soup={bugsPadsAndTracesSoup as any} />
6
+
7
+ export default {
8
+ title: "Simple",
9
+ component: Default,
10
+ }