@tscircuit/cli 0.1.126 → 0.1.128

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.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/dist/main.js +663 -6
  3. package/package.json +4 -2
package/README.md CHANGED
@@ -54,7 +54,8 @@ Commands:
54
54
  add <component> Add a tscircuit component package to your project
55
55
  remove <component> Remove a tscircuit component package from your
56
56
  project
57
- snapshot [options] Generate schematic and PCB snapshots
57
+ snapshot [options] Generate schematic and PCB snapshots (add --3d for
58
+ 3d preview)
58
59
  setup Setup utilities like GitHub Actions
59
60
  upgrade Upgrade CLI to the latest version
60
61
  search <query> Search for packages in the tscircuit registry
package/dist/main.js CHANGED
@@ -441906,7 +441906,7 @@ var getGlobalDepsInstallCommand = (packageManager, deps) => {
441906
441906
  import { execSync as execSync2 } from "node:child_process";
441907
441907
  var import_semver2 = __toESM2(require_semver2(), 1);
441908
441908
  // package.json
441909
- var version = "0.1.125";
441909
+ var version = "0.1.127";
441910
441910
  var package_default = {
441911
441911
  name: "@tscircuit/cli",
441912
441912
  version,
@@ -441951,7 +441951,9 @@ var package_default = {
441951
441951
  tempy: "^3.1.0",
441952
441952
  tscircuit: "^0.0.491",
441953
441953
  "@tscircuit/circuit-json-util": "^0.0.47",
441954
- "typed-ky": "^0.0.4"
441954
+ "typed-ky": "^0.0.4",
441955
+ "circuit-json-to-simple-3d": "^0.0.3",
441956
+ "@tscircuit/simple-3d-svg": "^0.0.10"
441955
441957
  },
441956
441958
  peerDependencies: {
441957
441959
  typescript: "^5.0.0"
@@ -442080,7 +442082,17 @@ var registerInit = (program3) => {
442080
442082
  message: "Do you want to initialize a new project in the current directory?"
442081
442083
  });
442082
442084
  if (!continueInCurrentDirectory) {
442083
- return;
442085
+ const { desiredDirectory } = await prompts({
442086
+ type: "text",
442087
+ name: "desiredDirectory",
442088
+ message: "Enter the desired directory name"
442089
+ });
442090
+ if (desiredDirectory) {
442091
+ directory = desiredDirectory;
442092
+ } else {
442093
+ console.log("Project initialization cancelled.");
442094
+ return process.exit(0);
442095
+ }
442084
442096
  }
442085
442097
  }
442086
442098
  const projectDir = directory ? path9.resolve(process.cwd(), directory) : process.cwd();
@@ -495782,9 +495794,648 @@ var registerBuild = (program3) => {
495782
495794
  import fs24 from "node:fs";
495783
495795
  import path24 from "node:path";
495784
495796
  init_dist6();
495797
+
495798
+ // node_modules/circuit-json-to-simple-3d/dist/index.js
495799
+ init_dist4();
495800
+
495801
+ // node_modules/@tscircuit/simple-3d-svg/dist/index.js
495802
+ var stlCache = /* @__PURE__ */ new Map;
495803
+ async function loadSTL(url) {
495804
+ if (stlCache.has(url)) {
495805
+ return stlCache.get(url);
495806
+ }
495807
+ const response = await fetch(url);
495808
+ const buffer = await response.arrayBuffer();
495809
+ const mesh = parseSTL(buffer);
495810
+ stlCache.set(url, mesh);
495811
+ return mesh;
495812
+ }
495813
+ function parseSTL(buffer) {
495814
+ const view = new DataView(buffer);
495815
+ const header = new TextDecoder().decode(buffer.slice(0, 5));
495816
+ if (header.toLowerCase() === "solid") {
495817
+ return parseASCIISTL(buffer);
495818
+ } else {
495819
+ return parseBinarySTL(view);
495820
+ }
495821
+ }
495822
+ function parseASCIISTL(buffer) {
495823
+ const text = new TextDecoder().decode(buffer);
495824
+ const lines = text.split(`
495825
+ `).map((line) => line.trim());
495826
+ const triangles = [];
495827
+ let i = 0;
495828
+ while (i < lines.length) {
495829
+ const line = lines[i];
495830
+ if (line && line.startsWith("facet normal")) {
495831
+ const normalMatch = line.match(/facet normal\s+([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)\s+([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)\s+([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)/);
495832
+ const normal = normalMatch ? {
495833
+ x: parseFloat(normalMatch[1]),
495834
+ y: parseFloat(normalMatch[2]),
495835
+ z: parseFloat(normalMatch[3])
495836
+ } : { x: 0, y: 0, z: 1 };
495837
+ i++;
495838
+ const vertices = [];
495839
+ while (i < lines.length && lines[i] && !lines[i].startsWith("endfacet")) {
495840
+ const vertexLine = lines[i];
495841
+ if (vertexLine.startsWith("vertex")) {
495842
+ const vertexMatch = vertexLine.match(/vertex\s+([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)\s+([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)\s+([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)/);
495843
+ if (vertexMatch) {
495844
+ vertices.push({
495845
+ x: parseFloat(vertexMatch[1]),
495846
+ y: parseFloat(vertexMatch[2]),
495847
+ z: parseFloat(vertexMatch[3])
495848
+ });
495849
+ }
495850
+ }
495851
+ i++;
495852
+ }
495853
+ if (vertices.length === 3) {
495854
+ triangles.push({
495855
+ vertices: [vertices[0], vertices[1], vertices[2]],
495856
+ normal
495857
+ });
495858
+ }
495859
+ }
495860
+ i++;
495861
+ }
495862
+ return {
495863
+ triangles,
495864
+ boundingBox: calculateBoundingBox(triangles)
495865
+ };
495866
+ }
495867
+ function parseBinarySTL(view) {
495868
+ let offset = 80;
495869
+ const numTriangles = view.getUint32(offset, true);
495870
+ offset += 4;
495871
+ const triangles = [];
495872
+ for (let i = 0;i < numTriangles; i++) {
495873
+ const normal = {
495874
+ x: view.getFloat32(offset, true),
495875
+ y: view.getFloat32(offset + 4, true),
495876
+ z: view.getFloat32(offset + 8, true)
495877
+ };
495878
+ offset += 12;
495879
+ const vertices = [
495880
+ {
495881
+ x: view.getFloat32(offset, true),
495882
+ y: view.getFloat32(offset + 4, true),
495883
+ z: view.getFloat32(offset + 8, true)
495884
+ },
495885
+ {
495886
+ x: view.getFloat32(offset + 12, true),
495887
+ y: view.getFloat32(offset + 16, true),
495888
+ z: view.getFloat32(offset + 20, true)
495889
+ },
495890
+ {
495891
+ x: view.getFloat32(offset + 24, true),
495892
+ y: view.getFloat32(offset + 28, true),
495893
+ z: view.getFloat32(offset + 32, true)
495894
+ }
495895
+ ];
495896
+ offset += 36;
495897
+ offset += 2;
495898
+ triangles.push({ vertices, normal });
495899
+ }
495900
+ return {
495901
+ triangles,
495902
+ boundingBox: calculateBoundingBox(triangles)
495903
+ };
495904
+ }
495905
+ function calculateBoundingBox(triangles) {
495906
+ if (triangles.length === 0) {
495907
+ return {
495908
+ min: { x: 0, y: 0, z: 0 },
495909
+ max: { x: 0, y: 0, z: 0 }
495910
+ };
495911
+ }
495912
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
495913
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
495914
+ for (const triangle of triangles) {
495915
+ for (const vertex of triangle.vertices) {
495916
+ minX = Math.min(minX, vertex.x);
495917
+ minY = Math.min(minY, vertex.y);
495918
+ minZ = Math.min(minZ, vertex.z);
495919
+ maxX = Math.max(maxX, vertex.x);
495920
+ maxY = Math.max(maxY, vertex.y);
495921
+ maxZ = Math.max(maxZ, vertex.z);
495922
+ }
495923
+ }
495924
+ return {
495925
+ min: { x: minX, y: minY, z: minZ },
495926
+ max: { x: maxX, y: maxY, z: maxZ }
495927
+ };
495928
+ }
495929
+ function colorToCss(c) {
495930
+ return typeof c === "string" ? c : `rgba(${c[0]},${c[1]},${c[2]},${c[3]})`;
495931
+ }
495932
+ function add(a, b) {
495933
+ return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z };
495934
+ }
495935
+ function sub(a, b) {
495936
+ return { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z };
495937
+ }
495938
+ function dot(a, b) {
495939
+ return a.x * b.x + a.y * b.y + a.z * b.z;
495940
+ }
495941
+ function cross3(a, b) {
495942
+ return {
495943
+ x: a.y * b.z - a.z * b.y,
495944
+ y: a.z * b.x - a.x * b.z,
495945
+ z: a.x * b.y - a.y * b.x
495946
+ };
495947
+ }
495948
+ function scale9(v, k) {
495949
+ return { x: v.x * k, y: v.y * k, z: v.z * k };
495950
+ }
495951
+ function len(v) {
495952
+ return Math.sqrt(dot(v, v));
495953
+ }
495954
+ function norm(v) {
495955
+ const l = len(v) || 1;
495956
+ return scale9(v, 1 / l);
495957
+ }
495958
+ function rotLocal(p, r = { x: 0, y: 0, z: 0 }) {
495959
+ let { x, y, z: z3 } = p;
495960
+ if (r.x) {
495961
+ const c = Math.cos(r.x);
495962
+ const s = Math.sin(r.x);
495963
+ const y2 = y * c - z3 * s;
495964
+ z3 = y * s + z3 * c;
495965
+ y = y2;
495966
+ }
495967
+ if (r.y) {
495968
+ const c = Math.cos(r.y);
495969
+ const s = Math.sin(r.y);
495970
+ const x2 = x * c + z3 * s;
495971
+ z3 = -x * s + z3 * c;
495972
+ x = x2;
495973
+ }
495974
+ if (r.z) {
495975
+ const c = Math.cos(r.z);
495976
+ const s = Math.sin(r.z);
495977
+ const x2 = x * c - y * s;
495978
+ y = x * s + y * c;
495979
+ x = x2;
495980
+ }
495981
+ return { x, y, z: z3 };
495982
+ }
495983
+ var W_DEF = 400;
495984
+ var H_DEF = 400;
495985
+ var FOCAL = 2;
495986
+ function axes(cam) {
495987
+ const f = norm(sub(cam.lookAt, cam.position));
495988
+ const wUp = { x: 0, y: 1, z: 0 };
495989
+ let r = norm(cross3(f, wUp));
495990
+ if (!len(r))
495991
+ r = { x: 1, y: 0, z: 0 };
495992
+ const u = cross3(r, f);
495993
+ return { r, u, f };
495994
+ }
495995
+ function toCam(p, cam) {
495996
+ const { r, u, f } = axes(cam);
495997
+ const d = sub(p, cam.position);
495998
+ return { x: dot(d, r), y: dot(d, u), z: dot(d, f) };
495999
+ }
496000
+ function proj(p, w, h, focal) {
496001
+ if (p.z <= 0)
496002
+ return null;
496003
+ const s = focal / p.z;
496004
+ return { x: p.x * s * w / 2, y: -p.y * s * h / 2, z: p.z };
496005
+ }
496006
+ var FACES = [
496007
+ [0, 1, 2, 3],
496008
+ [4, 5, 6, 7],
496009
+ [0, 1, 5, 4],
496010
+ [3, 2, 6, 7],
496011
+ [1, 2, 6, 5],
496012
+ [0, 3, 7, 4]
496013
+ ];
496014
+ var TOP = [3, 2, 6, 7];
496015
+ function verts(b) {
496016
+ const {
496017
+ size: { x: sx, y: sy, z: sz },
496018
+ center,
496019
+ rotation: rotation4
496020
+ } = b;
496021
+ const offs = [
496022
+ { x: -sx / 2, y: -sy / 2, z: -sz / 2 },
496023
+ { x: sx / 2, y: -sy / 2, z: -sz / 2 },
496024
+ { x: sx / 2, y: sy / 2, z: -sz / 2 },
496025
+ { x: -sx / 2, y: sy / 2, z: -sz / 2 },
496026
+ { x: -sx / 2, y: -sy / 2, z: sz / 2 },
496027
+ { x: sx / 2, y: -sy / 2, z: sz / 2 },
496028
+ { x: sx / 2, y: sy / 2, z: sz / 2 },
496029
+ { x: -sx / 2, y: sy / 2, z: sz / 2 }
496030
+ ];
496031
+ return offs.map((o) => add(center, rotLocal(o, rotation4)));
496032
+ }
496033
+ function inv3(m2) {
496034
+ const a = m2[0][0], d = m2[0][1], g = m2[0][2];
496035
+ const b = m2[1][0], e = m2[1][1], h = m2[1][2];
496036
+ const c = m2[2][0], f = m2[2][1], i = m2[2][2];
496037
+ const A = e * i - f * h;
496038
+ const B = -(d * i - f * g);
496039
+ const C = d * h - e * g;
496040
+ const D = -(b * i - c * h);
496041
+ const E = a * i - c * g;
496042
+ const F = -(a * h - b * g);
496043
+ const G = b * f - c * e;
496044
+ const H = -(a * f - c * d);
496045
+ const I = a * e - b * d;
496046
+ const det = a * A + d * D + g * G;
496047
+ const invDet = det ? 1 / det : 0;
496048
+ return [
496049
+ [A * invDet, B * invDet, C * invDet],
496050
+ [D * invDet, E * invDet, F * invDet],
496051
+ [G * invDet, H * invDet, I * invDet]
496052
+ ];
496053
+ }
496054
+ function mul3(a, b) {
496055
+ const r = [
496056
+ [0, 0, 0],
496057
+ [0, 0, 0],
496058
+ [0, 0, 0]
496059
+ ];
496060
+ for (let i = 0;i < 3; i++) {
496061
+ for (let j = 0;j < 3; j++) {
496062
+ r[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j];
496063
+ }
496064
+ }
496065
+ return r;
496066
+ }
496067
+ function affineMatrix(src, dst) {
496068
+ const S = [
496069
+ [src[0].x, src[1].x, src[2].x],
496070
+ [src[0].y, src[1].y, src[2].y],
496071
+ [1, 1, 1]
496072
+ ];
496073
+ const D = [
496074
+ [dst[0].x, dst[1].x, dst[2].x],
496075
+ [dst[0].y, dst[1].y, dst[2].y],
496076
+ [1, 1, 1]
496077
+ ];
496078
+ const M = mul3(D, inv3(S));
496079
+ return `matrix(${M[0][0]} ${M[1][0]} ${M[0][1]} ${M[1][1]} ${M[0][2]} ${M[1][2]})`;
496080
+ }
496081
+ function scaleAndPositionMesh(mesh, box) {
496082
+ const { boundingBox } = mesh;
496083
+ const meshSize = sub(boundingBox.max, boundingBox.min);
496084
+ const boxSize = box.size;
496085
+ const scaleX = boxSize.x / meshSize.x;
496086
+ const scaleY = boxSize.y / meshSize.y;
496087
+ const scaleZ = boxSize.z / meshSize.z;
496088
+ const uniformScale = Math.min(scaleX, scaleY, scaleZ);
496089
+ const meshCenter = scale9(add(boundingBox.min, boundingBox.max), 0.5);
496090
+ const transformedVertices = [];
496091
+ for (const triangle of mesh.triangles) {
496092
+ for (const vertex of triangle.vertices) {
496093
+ let transformed = sub(vertex, meshCenter);
496094
+ transformed = scale9(transformed, uniformScale);
496095
+ if (box.stlRotation) {
496096
+ transformed = rotLocal(transformed, box.stlRotation);
496097
+ }
496098
+ if (box.stlPosition) {
496099
+ transformed = add(transformed, box.stlPosition);
496100
+ }
496101
+ if (box.rotation) {
496102
+ transformed = rotLocal(transformed, box.rotation);
496103
+ }
496104
+ transformed = add(transformed, box.center);
496105
+ transformedVertices.push(transformed);
496106
+ }
496107
+ }
496108
+ return transformedVertices;
496109
+ }
496110
+ async function renderScene(scene, opt = {}) {
496111
+ const W = opt.width ?? W_DEF;
496112
+ const H = opt.height ?? H_DEF;
496113
+ const focal = scene.camera.focalLength ?? FOCAL;
496114
+ const faces = [];
496115
+ const images = [];
496116
+ const labels = [];
496117
+ let clipSeq = 0;
496118
+ const texId = /* @__PURE__ */ new Map;
496119
+ const stlMeshes = /* @__PURE__ */ new Map;
496120
+ for (const box of scene.boxes) {
496121
+ if (box.stlUrl && !stlMeshes.has(box.stlUrl)) {
496122
+ try {
496123
+ const mesh = await loadSTL(box.stlUrl);
496124
+ stlMeshes.set(box.stlUrl, mesh);
496125
+ } catch (error) {
496126
+ console.warn(`Failed to load STL from ${box.stlUrl}:`, error);
496127
+ }
496128
+ }
496129
+ }
496130
+ for (const box of scene.boxes) {
496131
+ if (box.stlUrl && stlMeshes.has(box.stlUrl)) {
496132
+ const mesh = stlMeshes.get(box.stlUrl);
496133
+ const transformedVertices = scaleAndPositionMesh(mesh, box);
496134
+ for (let i = 0;i < mesh.triangles.length; i++) {
496135
+ const triangle = mesh.triangles[i];
496136
+ const vertexStart = i * 3;
496137
+ const v0w = transformedVertices[vertexStart];
496138
+ const v1w = transformedVertices[vertexStart + 1];
496139
+ const v2w = transformedVertices[vertexStart + 2];
496140
+ const v0c = toCam(v0w, scene.camera);
496141
+ const v1c = toCam(v1w, scene.camera);
496142
+ const v2c = toCam(v2w, scene.camera);
496143
+ const v0p = proj(v0c, W, H, focal);
496144
+ const v1p = proj(v1c, W, H, focal);
496145
+ const v2p = proj(v2c, W, H, focal);
496146
+ if (v0p && v1p && v2p) {
496147
+ const edge1 = sub(v1c, v0c);
496148
+ const edge2 = sub(v2c, v0c);
496149
+ const faceNormal = cross3(edge1, edge2);
496150
+ if (faceNormal.z < 0) {
496151
+ const depth = Math.max(v0c.z, v1c.z, v2c.z);
496152
+ faces.push({
496153
+ pts: [v0p, v1p, v2p],
496154
+ depth,
496155
+ fill: colorToCss(box.color)
496156
+ });
496157
+ }
496158
+ }
496159
+ }
496160
+ } else {
496161
+ const vw = verts(box);
496162
+ const vc = vw.map((v) => toCam(v, scene.camera));
496163
+ const vp = vc.map((v) => proj(v, W, H, focal));
496164
+ for (const idx of FACES) {
496165
+ const p4 = [];
496166
+ let zMax = -Infinity;
496167
+ let behind = false;
496168
+ for (const i of idx) {
496169
+ const p = vp[i];
496170
+ if (!p) {
496171
+ behind = true;
496172
+ break;
496173
+ }
496174
+ p4.push(p);
496175
+ zMax = Math.max(zMax, vc[i].z);
496176
+ }
496177
+ if (behind)
496178
+ continue;
496179
+ const [a, b, c] = idx;
496180
+ const n2 = cross3(sub(vc[b], vc[a]), sub(vc[c], vc[a]));
496181
+ if (n2.z >= 0)
496182
+ continue;
496183
+ faces.push({
496184
+ pts: p4,
496185
+ depth: zMax,
496186
+ fill: colorToCss(box.color)
496187
+ });
496188
+ }
496189
+ if (box.faceImages?.top) {
496190
+ const pts = TOP.map((i) => vp[i]);
496191
+ if (pts.every(Boolean)) {
496192
+ const dst = pts;
496193
+ const cz = Math.max(...TOP.map((i) => vc[i].z));
496194
+ const href = box.faceImages.top;
496195
+ if (!texId.has(href)) {
496196
+ texId.set(href, `tex${texId.size}`);
496197
+ }
496198
+ const sym = texId.get(href);
496199
+ const subdivisions = box.projectionSubdivision ?? 2;
496200
+ const quadsPerSide = subdivisions;
496201
+ for (let row = 0;row < quadsPerSide; row++) {
496202
+ for (let col = 0;col < quadsPerSide; col++) {
496203
+ const u0 = col / quadsPerSide;
496204
+ const u1 = (col + 1) / quadsPerSide;
496205
+ const v0 = row / quadsPerSide;
496206
+ const v1 = (row + 1) / quadsPerSide;
496207
+ const lerp = (a, b, t2) => ({
496208
+ x: a.x * (1 - t2) + b.x * t2,
496209
+ y: a.y * (1 - t2) + b.y * t2,
496210
+ z: a.z * (1 - t2) + b.z * t2
496211
+ });
496212
+ const p00 = lerp(lerp(dst[0], dst[1], u0), lerp(dst[3], dst[2], u0), v0);
496213
+ const p10 = lerp(lerp(dst[0], dst[1], u1), lerp(dst[3], dst[2], u1), v0);
496214
+ const p01 = lerp(lerp(dst[0], dst[1], u0), lerp(dst[3], dst[2], u0), v1);
496215
+ const p11 = lerp(lerp(dst[0], dst[1], u1), lerp(dst[3], dst[2], u1), v1);
496216
+ const tri0Mat = affineMatrix([
496217
+ { x: u0, y: v0 },
496218
+ { x: u1, y: v0 },
496219
+ { x: u1, y: v1 }
496220
+ ], [p00, p10, p11]);
496221
+ const id0 = `clip${clipSeq++}`;
496222
+ images.push({
496223
+ matrix: tri0Mat,
496224
+ depth: cz,
496225
+ href,
496226
+ clip: id0,
496227
+ points: `${u0},${v0} ${u1},${v0} ${u1},${v1}`,
496228
+ sym
496229
+ });
496230
+ const tri1Mat = affineMatrix([
496231
+ { x: u0, y: v0 },
496232
+ { x: u1, y: v1 },
496233
+ { x: u0, y: v1 }
496234
+ ], [p00, p11, p01]);
496235
+ const id1 = `clip${clipSeq++}`;
496236
+ images.push({
496237
+ matrix: tri1Mat,
496238
+ depth: cz,
496239
+ href,
496240
+ clip: id1,
496241
+ points: `${u0},${v0} ${u1},${v1} ${u0},${v1}`,
496242
+ sym
496243
+ });
496244
+ }
496245
+ }
496246
+ }
496247
+ }
496248
+ if (box.topLabel) {
496249
+ const pts = TOP.map((i) => vp[i]);
496250
+ if (pts.every(Boolean)) {
496251
+ const p0 = pts[0];
496252
+ const p1 = pts[1];
496253
+ const p3 = pts[3];
496254
+ const u = sub(p1, p0);
496255
+ const v = sub(p3, p0);
496256
+ const lu = len(u);
496257
+ const lv = len(v);
496258
+ if (lu && lv) {
496259
+ const uN = scale9(u, 1 / lu);
496260
+ const vN = scale9(v, 1 / lv);
496261
+ const cx = pts.reduce((s, p) => s + p.x, 0) / 4;
496262
+ const cy = pts.reduce((s, p) => s + p.y, 0) / 4;
496263
+ const cz = Math.max(...TOP.map((i) => vc[i].z));
496264
+ const m2 = `matrix(${uN.x} ${uN.y} ${vN.x} ${vN.y} ${cx} ${cy})`;
496265
+ const fillCol = box.topLabelColor ?? [0, 0, 0, 1];
496266
+ labels.push({
496267
+ matrix: m2,
496268
+ depth: cz,
496269
+ text: box.topLabel,
496270
+ fill: colorToCss(fillCol)
496271
+ });
496272
+ }
496273
+ }
496274
+ }
496275
+ }
496276
+ }
496277
+ const allElements = [
496278
+ ...faces.map((f) => ({ type: "face", data: f })),
496279
+ ...images.map((i) => ({ type: "image", data: i })),
496280
+ ...labels.map((l) => ({ type: "label", data: l }))
496281
+ ];
496282
+ allElements.sort((a, b) => b.data.depth - a.data.depth);
496283
+ const out = [];
496284
+ out.push(`<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="${-W / 2} ${-H / 2} ${W} ${H}">`);
496285
+ if (opt.backgroundColor) {
496286
+ out.push(` <rect x="${-W / 2}" y="${-H / 2}" width="${W}" height="${H}" fill="${colorToCss(opt.backgroundColor)}" />
496287
+ `);
496288
+ }
496289
+ if (images.length) {
496290
+ out.push(` <defs>
496291
+ `);
496292
+ for (const [href, id] of texId) {
496293
+ out.push(` <image id="${id}" href="${href}" width="1" height="1" preserveAspectRatio="none" style="image-rendering:pixelated"/>
496294
+ `);
496295
+ }
496296
+ for (const img of images) {
496297
+ out.push(` <clipPath id="${img.clip}" clipPathUnits="objectBoundingBox"><polygon points="${img.points}" /></clipPath>
496298
+ `);
496299
+ }
496300
+ out.push(` </defs>
496301
+ `);
496302
+ }
496303
+ let inStrokeGroup = false;
496304
+ for (const element of allElements) {
496305
+ if (element.type === "face" || element.type === "image") {
496306
+ if (!inStrokeGroup) {
496307
+ out.push(` <g stroke="#000" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
496308
+ `);
496309
+ inStrokeGroup = true;
496310
+ }
496311
+ if (element.type === "face") {
496312
+ const f = element.data;
496313
+ out.push(` <polygon fill="${f.fill}" points="${f.pts.map((p) => `${p.x},${p.y}`).join(" ")}" />
496314
+ `);
496315
+ } else {
496316
+ const img = element.data;
496317
+ out.push(` <g transform="${img.matrix}" clip-path="url(#${img.clip})"><use href="#${img.sym}"/></g>
496318
+ `);
496319
+ }
496320
+ } else if (element.type === "label") {
496321
+ if (inStrokeGroup) {
496322
+ out.push(` </g>
496323
+ `);
496324
+ inStrokeGroup = false;
496325
+ }
496326
+ const l = element.data;
496327
+ out.push(` <g font-family="sans-serif" font-size="14" text-anchor="middle" dominant-baseline="central" transform="${l.matrix}"><text x="0" y="0" fill="${l.fill}">${l.text}</text></g>
496328
+ `);
496329
+ }
496330
+ }
496331
+ if (inStrokeGroup) {
496332
+ out.push(` </g>
496333
+ `);
496334
+ }
496335
+ out.push("</svg>");
496336
+ return out.join("");
496337
+ }
496338
+
496339
+ // node_modules/circuit-json-to-simple-3d/dist/index.js
496340
+ init_dist6();
496341
+ function getDefaultCameraForPcbBoard(pcbBoard, anglePreset = "angle1") {
496342
+ const w = pcbBoard.width;
496343
+ const h = pcbBoard.height;
496344
+ const cx = pcbBoard.center?.x;
496345
+ const cz = pcbBoard.center?.y;
496346
+ const dist = Math.max(w, h) * 1.5;
496347
+ let position4;
496348
+ if (anglePreset === "angle1") {
496349
+ position4 = { x: cx - dist, y: dist, z: cz - dist };
496350
+ } else if (anglePreset === "angle2") {
496351
+ position4 = { x: cx + dist, y: dist, z: cz - dist };
496352
+ } else if (anglePreset === "left") {
496353
+ position4 = { x: cx - dist, y: 0, z: 0 };
496354
+ } else if (anglePreset === "right") {
496355
+ position4 = { x: cx + dist, y: 0, z: 0 };
496356
+ } else if (anglePreset === "left-raised") {
496357
+ position4 = { x: cx - dist, y: dist, z: 0 };
496358
+ } else if (anglePreset === "right-raised") {
496359
+ position4 = { x: cx + dist, y: dist, z: 0 };
496360
+ } else {
496361
+ throw new Error(`Unknown angle preset: ${anglePreset}`);
496362
+ }
496363
+ return {
496364
+ position: position4,
496365
+ lookAt: { x: cx, y: 0, z: cz },
496366
+ focalLength: 2
496367
+ };
496368
+ }
496369
+ async function convertCircuitJsonToSimple3dSvg(circuitJson, opts = {}) {
496370
+ const db = cju_default(circuitJson);
496371
+ const boxes = [];
496372
+ const pcbTopSvg = convertCircuitJsonToPcbSvg(circuitJson, {
496373
+ layer: "top",
496374
+ matchBoardAspectRatio: true,
496375
+ backgroundColor: "transparent",
496376
+ drawPaddingOutsideBoard: false,
496377
+ colorOverrides: {
496378
+ copper: {
496379
+ top: "#ffe066",
496380
+ bottom: "#ffe066"
496381
+ },
496382
+ drill: "rgba(0,0,0,0.5)"
496383
+ }
496384
+ }).replace("<svg", "<svg transform='scale(1, -1)'");
496385
+ const pcbBoard = db.pcb_board.list()[0];
496386
+ if (!pcbBoard)
496387
+ throw new Error("No pcb_board, can't render to 3d");
496388
+ const camera = opts.camera ?? getDefaultCameraForPcbBoard(pcbBoard, opts.anglePreset ?? "angle1");
496389
+ if (!camera.focalLength) {
496390
+ camera.focalLength = 1;
496391
+ }
496392
+ boxes.push({
496393
+ center: {
496394
+ x: pcbBoard.center.x,
496395
+ y: 0,
496396
+ z: pcbBoard.center.y
496397
+ },
496398
+ size: {
496399
+ x: pcbBoard.width,
496400
+ y: pcbBoard.thickness,
496401
+ z: pcbBoard.height
496402
+ },
496403
+ faceImages: {
496404
+ top: `data:image/svg+xml;base64,${btoa(pcbTopSvg)}`
496405
+ },
496406
+ projectionSubdivision: 10,
496407
+ color: "rgba(0,140,0,0.8)"
496408
+ });
496409
+ const DEFAULT_COMP_HEIGHT = 2;
496410
+ for (const comp of db.pcb_component.list()) {
496411
+ const sourceComponent = db.source_component.get(comp.source_component_id);
496412
+ const compHeight = Math.min(Math.min(comp.width, comp.height), DEFAULT_COMP_HEIGHT);
496413
+ boxes.push({
496414
+ center: {
496415
+ x: comp.center.x,
496416
+ y: pcbBoard.thickness / 2 + compHeight / 2,
496417
+ z: comp.center.y
496418
+ },
496419
+ size: {
496420
+ x: comp.width,
496421
+ y: compHeight,
496422
+ z: comp.height
496423
+ },
496424
+ color: "rgba(128,128,128,0.5)",
496425
+ topLabel: sourceComponent?.name ?? "?",
496426
+ topLabelColor: "white"
496427
+ });
496428
+ }
496429
+ return await renderScene({ boxes, camera }, {
496430
+ backgroundColor: "lightgray"
496431
+ });
496432
+ }
496433
+
496434
+ // lib/shared/snapshot-project.ts
495785
496435
  var snapshotProject = async ({
495786
496436
  update = false,
495787
496437
  ignored = [],
496438
+ threeD = false,
495788
496439
  onExit = (code) => process.exit(code),
495789
496440
  onError = (msg) => console.error(msg),
495790
496441
  onSuccess = (msg) => console.log(msg)
@@ -495811,13 +496462,18 @@ var snapshotProject = async ({
495811
496462
  const { circuitJson } = await generateCircuitJson({ filePath: file });
495812
496463
  const pcbSvg = convertCircuitJsonToPcbSvg(circuitJson);
495813
496464
  const schSvg = convertCircuitJsonToSchematicSvg(circuitJson);
496465
+ const svg3d = threeD ? await convertCircuitJsonToSimple3dSvg(circuitJson) : null;
495814
496466
  const snapDir = path24.join(path24.dirname(file), "__snapshots__");
495815
496467
  fs24.mkdirSync(snapDir, { recursive: true });
495816
496468
  const base = path24.basename(file).replace(/\.tsx$/, "");
495817
- for (const [type, svg] of [
496469
+ const snapshotPairs = [
495818
496470
  ["pcb", pcbSvg],
495819
496471
  ["schematic", schSvg]
495820
- ]) {
496472
+ ];
496473
+ if (threeD && svg3d) {
496474
+ snapshotPairs.push(["3d", svg3d]);
496475
+ }
496476
+ for (const [type, svg] of snapshotPairs) {
495821
496477
  const snapPath = path24.join(snapDir, `${base}-${type}.snap.svg`);
495822
496478
  if (update || !fs24.existsSync(snapPath)) {
495823
496479
  fs24.writeFileSync(snapPath, svg);
@@ -495844,9 +496500,10 @@ ${mismatches.join(`
495844
496500
 
495845
496501
  // cli/snapshot/register.ts
495846
496502
  var registerSnapshot = (program3) => {
495847
- program3.command("snapshot").description("Generate schematic and PCB snapshots").option("-u, --update", "Update snapshots on disk").action(async (options) => {
496503
+ program3.command("snapshot").description("Generate schematic and PCB snapshots (add --3d for 3d preview)").option("-u, --update", "Update snapshots on disk").option("--3d", "Generate 3d preview snapshots").action(async (options) => {
495848
496504
  await snapshotProject({
495849
496505
  update: options.update ?? false,
496506
+ threeD: options["3d"] ?? false,
495850
496507
  onExit: (code) => process.exit(code),
495851
496508
  onError: (msg) => console.error(msg),
495852
496509
  onSuccess: (msg) => console.log(msg)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/cli",
3
- "version": "0.1.126",
3
+ "version": "0.1.128",
4
4
  "main": "dist/main.js",
5
5
  "devDependencies": {
6
6
  "@babel/standalone": "^7.26.9",
@@ -42,7 +42,9 @@
42
42
  "tempy": "^3.1.0",
43
43
  "tscircuit": "^0.0.491",
44
44
  "@tscircuit/circuit-json-util": "^0.0.47",
45
- "typed-ky": "^0.0.4"
45
+ "typed-ky": "^0.0.4",
46
+ "circuit-json-to-simple-3d": "^0.0.3",
47
+ "@tscircuit/simple-3d-svg": "^0.0.10"
46
48
  },
47
49
  "peerDependencies": {
48
50
  "typescript": "^5.0.0"