circuit-json-to-gltf 0.0.10 → 0.0.12

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/dist/index.d.ts CHANGED
@@ -76,7 +76,7 @@ interface Box3D {
76
76
  };
77
77
  mesh?: STLMesh | OBJMesh;
78
78
  meshUrl?: string;
79
- meshType?: "stl" | "obj";
79
+ meshType?: "stl" | "obj" | "glb";
80
80
  label?: string;
81
81
  labelColor?: Color;
82
82
  }
@@ -135,6 +135,9 @@ declare function clearSTLCache(): void;
135
135
  declare function loadOBJ(url: string, transform?: CoordinateTransformConfig): Promise<OBJMesh>;
136
136
  declare function clearOBJCache(): void;
137
137
 
138
+ declare function loadGLB(url: string, transform?: CoordinateTransformConfig): Promise<STLMesh>;
139
+ declare function clearGLBCache(): void;
140
+
138
141
  declare function convertCircuitJsonTo3D(circuitJson: CircuitJson, options?: CircuitTo3DOptions): Promise<Scene3D>;
139
142
 
140
143
  declare function convertSceneToGLTF(scene: Scene3D, options?: GLTFExportOptions): Promise<ArrayBuffer | object>;
@@ -165,4 +168,4 @@ declare const COORDINATE_TRANSFORMS: {
165
168
 
166
169
  declare function convertCircuitJsonToGltf(circuitJson: CircuitJson, options?: ConversionOptions): Promise<ArrayBuffer | object>;
167
170
 
168
- export { type BoardRenderOptions, type BoundingBox, type Box3D, COORDINATE_TRANSFORMS, type Camera3D, type CircuitTo3DOptions, type Color, type ConversionOptions, type CoordinateTransformConfig, type GLTFExportOptions, type Light3D, type OBJMaterial, type OBJMesh, type Point3, type STLMesh, type Scene3D, type Size3, type Triangle, applyCoordinateTransform, clearOBJCache, clearSTLCache, convertCircuitJsonTo3D, convertCircuitJsonToGltf, convertSceneToGLTF, loadOBJ, loadSTL, renderBoardLayer, renderBoardTextures, transformTriangles };
171
+ export { type BoardRenderOptions, type BoundingBox, type Box3D, COORDINATE_TRANSFORMS, type Camera3D, type CircuitTo3DOptions, type Color, type ConversionOptions, type CoordinateTransformConfig, type GLTFExportOptions, type Light3D, type OBJMaterial, type OBJMesh, type Point3, type STLMesh, type Scene3D, type Size3, type Triangle, applyCoordinateTransform, clearGLBCache, clearOBJCache, clearSTLCache, convertCircuitJsonTo3D, convertCircuitJsonToGltf, convertSceneToGLTF, loadGLB, loadOBJ, loadSTL, renderBoardLayer, renderBoardTextures, transformTriangles };
package/dist/index.js CHANGED
@@ -480,6 +480,268 @@ function clearOBJCache() {
480
480
  objCache.clear();
481
481
  }
482
482
 
483
+ // lib/loaders/glb.ts
484
+ var glbCache = /* @__PURE__ */ new Map();
485
+ async function loadGLB(url, transform) {
486
+ const cacheKey = `${url}:${JSON.stringify(transform ?? {})}`;
487
+ if (glbCache.has(cacheKey)) {
488
+ return glbCache.get(cacheKey);
489
+ }
490
+ const response = await fetch(url);
491
+ const buffer = await response.arrayBuffer();
492
+ const mesh = parseGLB(buffer, transform);
493
+ glbCache.set(cacheKey, mesh);
494
+ return mesh;
495
+ }
496
+ function parseGLB(buffer, transform) {
497
+ const view = new DataView(buffer);
498
+ let offset = 0;
499
+ const magic = view.getUint32(offset, true);
500
+ offset += 4;
501
+ if (magic !== 1179937895) {
502
+ throw new Error("Invalid GLB file: incorrect magic number");
503
+ }
504
+ const version = view.getUint32(offset, true);
505
+ offset += 4;
506
+ if (version !== 2) {
507
+ throw new Error(`Unsupported GLB version: ${version}`);
508
+ }
509
+ const length = view.getUint32(offset, true);
510
+ offset += 4;
511
+ const jsonChunkLength = view.getUint32(offset, true);
512
+ offset += 4;
513
+ const jsonChunkType = view.getUint32(offset, true);
514
+ offset += 4;
515
+ if (jsonChunkType !== 1313821514) {
516
+ throw new Error("Expected JSON chunk");
517
+ }
518
+ const jsonBytes = new Uint8Array(buffer, offset, jsonChunkLength);
519
+ const jsonString = new TextDecoder().decode(jsonBytes);
520
+ const gltf = JSON.parse(jsonString);
521
+ offset += jsonChunkLength;
522
+ let binaryBuffer;
523
+ if (offset < length) {
524
+ const binaryChunkLength = view.getUint32(offset, true);
525
+ offset += 4;
526
+ const binaryChunkType = view.getUint32(offset, true);
527
+ offset += 4;
528
+ if (binaryChunkType === 5130562) {
529
+ binaryBuffer = buffer.slice(offset, offset + binaryChunkLength);
530
+ }
531
+ }
532
+ const triangles = extractTrianglesFromGLTF(gltf, binaryBuffer);
533
+ const finalConfig = transform ?? {
534
+ axisMapping: { x: "x", y: "z", z: "y" }
535
+ };
536
+ const transformedTriangles = transformTriangles(triangles, finalConfig);
537
+ return {
538
+ triangles: transformedTriangles,
539
+ boundingBox: calculateBoundingBox3(transformedTriangles)
540
+ };
541
+ }
542
+ function extractTrianglesFromGLTF(gltf, binaryBuffer) {
543
+ const triangles = [];
544
+ if (!gltf.meshes || !gltf.accessors || !gltf.bufferViews) {
545
+ return triangles;
546
+ }
547
+ for (const mesh of gltf.meshes) {
548
+ for (const primitive of mesh.primitives) {
549
+ const mode = primitive.mode ?? 4;
550
+ if (mode !== 4) {
551
+ continue;
552
+ }
553
+ const positionAccessorIndex = primitive.attributes.POSITION;
554
+ if (positionAccessorIndex === void 0) {
555
+ continue;
556
+ }
557
+ const positionAccessor = gltf.accessors[positionAccessorIndex];
558
+ const positions = getAccessorData(
559
+ positionAccessor,
560
+ gltf.bufferViews,
561
+ binaryBuffer
562
+ );
563
+ let normals;
564
+ const normalAccessorIndex = primitive.attributes.NORMAL;
565
+ if (normalAccessorIndex !== void 0) {
566
+ const normalAccessor = gltf.accessors[normalAccessorIndex];
567
+ normals = getAccessorData(
568
+ normalAccessor,
569
+ gltf.bufferViews,
570
+ binaryBuffer
571
+ );
572
+ }
573
+ let indices;
574
+ if (primitive.indices !== void 0) {
575
+ const indexAccessor = gltf.accessors[primitive.indices];
576
+ const indexData = getAccessorData(
577
+ indexAccessor,
578
+ gltf.bufferViews,
579
+ binaryBuffer
580
+ );
581
+ indices = indexAccessor.componentType === 5123 ? new Uint16Array(
582
+ indexData.buffer,
583
+ indexData.byteOffset,
584
+ indexData.length
585
+ ) : new Uint32Array(
586
+ indexData.buffer,
587
+ indexData.byteOffset,
588
+ indexData.length
589
+ );
590
+ }
591
+ const vertexCount = positions.length / 3;
592
+ if (indices) {
593
+ for (let i = 0; i < indices.length; i += 3) {
594
+ const i0 = indices[i];
595
+ const i1 = indices[i + 1];
596
+ const i2 = indices[i + 2];
597
+ const v0 = {
598
+ x: positions[i0 * 3],
599
+ y: positions[i0 * 3 + 1],
600
+ z: positions[i0 * 3 + 2]
601
+ };
602
+ const v1 = {
603
+ x: positions[i1 * 3],
604
+ y: positions[i1 * 3 + 1],
605
+ z: positions[i1 * 3 + 2]
606
+ };
607
+ const v2 = {
608
+ x: positions[i2 * 3],
609
+ y: positions[i2 * 3 + 1],
610
+ z: positions[i2 * 3 + 2]
611
+ };
612
+ let normal;
613
+ if (normals) {
614
+ normal = {
615
+ x: (normals[i0 * 3] + normals[i1 * 3] + normals[i2 * 3]) / 3,
616
+ y: (normals[i0 * 3 + 1] + normals[i1 * 3 + 1] + normals[i2 * 3 + 1]) / 3,
617
+ z: (normals[i0 * 3 + 2] + normals[i1 * 3 + 2] + normals[i2 * 3 + 2]) / 3
618
+ };
619
+ } else {
620
+ normal = computeNormal(v0, v1, v2);
621
+ }
622
+ triangles.push({ vertices: [v0, v1, v2], normal });
623
+ }
624
+ } else {
625
+ for (let i = 0; i < vertexCount; i += 3) {
626
+ const v0 = {
627
+ x: positions[i * 3],
628
+ y: positions[i * 3 + 1],
629
+ z: positions[i * 3 + 2]
630
+ };
631
+ const v1 = {
632
+ x: positions[(i + 1) * 3],
633
+ y: positions[(i + 1) * 3 + 1],
634
+ z: positions[(i + 1) * 3 + 2]
635
+ };
636
+ const v2 = {
637
+ x: positions[(i + 2) * 3],
638
+ y: positions[(i + 2) * 3 + 1],
639
+ z: positions[(i + 2) * 3 + 2]
640
+ };
641
+ let normal;
642
+ if (normals) {
643
+ normal = {
644
+ x: (normals[i * 3] + normals[(i + 1) * 3] + normals[(i + 2) * 3]) / 3,
645
+ y: (normals[i * 3 + 1] + normals[(i + 1) * 3 + 1] + normals[(i + 2) * 3 + 1]) / 3,
646
+ z: (normals[i * 3 + 2] + normals[(i + 1) * 3 + 2] + normals[(i + 2) * 3 + 2]) / 3
647
+ };
648
+ } else {
649
+ normal = computeNormal(v0, v1, v2);
650
+ }
651
+ triangles.push({ vertices: [v0, v1, v2], normal });
652
+ }
653
+ }
654
+ }
655
+ }
656
+ return triangles;
657
+ }
658
+ function getAccessorData(accessor, bufferViews, binaryBuffer) {
659
+ const bufferView = bufferViews[accessor.bufferView];
660
+ if (!bufferView || !binaryBuffer) {
661
+ throw new Error("Missing buffer data");
662
+ }
663
+ const byteOffset = (bufferView.byteOffset ?? 0) + (accessor.byteOffset ?? 0);
664
+ const componentType = accessor.componentType;
665
+ const count = accessor.count;
666
+ const type = accessor.type;
667
+ const componentsPerElement = type === "SCALAR" ? 1 : type === "VEC2" ? 2 : type === "VEC3" ? 3 : type === "VEC4" ? 4 : 1;
668
+ const totalComponents = count * componentsPerElement;
669
+ if (componentType === 5126) {
670
+ return new Float32Array(binaryBuffer, byteOffset, totalComponents);
671
+ } else if (componentType === 5123) {
672
+ const uint16Array = new Uint16Array(
673
+ binaryBuffer,
674
+ byteOffset,
675
+ totalComponents
676
+ );
677
+ return new Float32Array(uint16Array);
678
+ } else if (componentType === 5125) {
679
+ const uint32Array = new Uint32Array(
680
+ binaryBuffer,
681
+ byteOffset,
682
+ totalComponents
683
+ );
684
+ return new Float32Array(uint32Array);
685
+ } else if (componentType === 5122) {
686
+ const int16Array = new Int16Array(binaryBuffer, byteOffset, totalComponents);
687
+ return new Float32Array(int16Array);
688
+ } else if (componentType === 5124) {
689
+ const int32Array = new Int32Array(binaryBuffer, byteOffset, totalComponents);
690
+ return new Float32Array(int32Array);
691
+ } else if (componentType === 5121) {
692
+ const uint8Array = new Uint8Array(binaryBuffer, byteOffset, totalComponents);
693
+ return new Float32Array(uint8Array);
694
+ } else if (componentType === 5120) {
695
+ const int8Array = new Int8Array(binaryBuffer, byteOffset, totalComponents);
696
+ return new Float32Array(int8Array);
697
+ }
698
+ throw new Error(`Unsupported component type: ${componentType}`);
699
+ }
700
+ function computeNormal(v0, v1, v2) {
701
+ const edge1 = {
702
+ x: v1.x - v0.x,
703
+ y: v1.y - v0.y,
704
+ z: v1.z - v0.z
705
+ };
706
+ const edge2 = {
707
+ x: v2.x - v0.x,
708
+ y: v2.y - v0.y,
709
+ z: v2.z - v0.z
710
+ };
711
+ return {
712
+ x: edge1.y * edge2.z - edge1.z * edge2.y,
713
+ y: edge1.z * edge2.x - edge1.x * edge2.z,
714
+ z: edge1.x * edge2.y - edge1.y * edge2.x
715
+ };
716
+ }
717
+ function calculateBoundingBox3(triangles) {
718
+ if (triangles.length === 0) {
719
+ return {
720
+ min: { x: 0, y: 0, z: 0 },
721
+ max: { x: 0, y: 0, z: 0 }
722
+ };
723
+ }
724
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
725
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
726
+ for (const triangle of triangles) {
727
+ for (const vertex of triangle.vertices) {
728
+ minX = Math.min(minX, vertex.x);
729
+ minY = Math.min(minY, vertex.y);
730
+ minZ = Math.min(minZ, vertex.z);
731
+ maxX = Math.max(maxX, vertex.x);
732
+ maxY = Math.max(maxY, vertex.y);
733
+ maxZ = Math.max(maxZ, vertex.z);
734
+ }
735
+ }
736
+ return {
737
+ min: { x: minX, y: minY, z: minZ },
738
+ max: { x: maxX, y: maxY, z: maxZ }
739
+ };
740
+ }
741
+ function clearGLBCache() {
742
+ glbCache.clear();
743
+ }
744
+
483
745
  // lib/converters/board-renderer.ts
484
746
  import { convertCircuitJsonToPcbSvg } from "circuit-to-svg";
485
747
  async function renderBoardLayer(circuitJson, options) {
@@ -638,8 +900,12 @@ async function convertCircuitJsonTo3D(circuitJson, options = {}) {
638
900
  const cadComponents = db.cad_component?.list?.() ?? [];
639
901
  const pcbComponentIdsWith3D = /* @__PURE__ */ new Set();
640
902
  for (const cad of cadComponents) {
641
- const { model_stl_url, model_obj_url } = cad;
642
- if (!model_stl_url && !model_obj_url) continue;
903
+ let { model_stl_url, model_obj_url, model_glb_url } = cad;
904
+ const hasModelUrl = Boolean(model_stl_url || model_obj_url || model_glb_url);
905
+ if (!hasModelUrl && cad.footprinter_string) {
906
+ model_glb_url = `https://modelcdn.tscircuit.com/jscad_models/${cad.footprinter_string}.glb`;
907
+ }
908
+ if (!model_stl_url && !model_obj_url && !model_glb_url) continue;
643
909
  pcbComponentIdsWith3D.add(cad.pcb_component_id);
644
910
  const pcbComponent = db.pcb_component.get(cad.pcb_component_id);
645
911
  const size = cad.size ?? {
@@ -655,23 +921,27 @@ async function convertCircuitJsonTo3D(circuitJson, options = {}) {
655
921
  const box = {
656
922
  center,
657
923
  size,
658
- color: componentColor,
659
- meshUrl: model_stl_url || model_obj_url,
660
- meshType: model_stl_url ? "stl" : "obj"
924
+ meshUrl: model_stl_url || model_obj_url || model_glb_url,
925
+ meshType: model_stl_url ? "stl" : model_obj_url ? "obj" : "glb"
661
926
  };
662
927
  if (cad.rotation) {
663
928
  box.rotation = convertRotationFromCadRotation(cad.rotation);
664
929
  }
665
- const defaultTransform = coordinateTransform ?? COORDINATE_TRANSFORMS.Z_UP_TO_Y_UP_USB_FIX;
930
+ const defaultTransform = coordinateTransform ?? (model_glb_url ? void 0 : COORDINATE_TRANSFORMS.Z_UP_TO_Y_UP_USB_FIX);
666
931
  try {
667
932
  if (model_stl_url) {
668
933
  box.mesh = await loadSTL(model_stl_url, defaultTransform);
669
934
  } else if (model_obj_url) {
670
935
  box.mesh = await loadOBJ(model_obj_url, defaultTransform);
936
+ } else if (model_glb_url) {
937
+ box.mesh = await loadGLB(model_glb_url, defaultTransform);
671
938
  }
672
939
  } catch (error) {
673
940
  console.warn(`Failed to load 3D model: ${error}`);
674
941
  }
942
+ if (!box.mesh) {
943
+ box.color = componentColor;
944
+ }
675
945
  boxes.push(box);
676
946
  }
677
947
  for (const component of db.pcb_component.list()) {
@@ -1326,6 +1596,16 @@ var GLTFBuilder = class {
1326
1596
  let materialIndex = defaultMaterialIndex;
1327
1597
  if (box.color) {
1328
1598
  materialIndex = this.addMaterialFromColor(box.color, !box.mesh);
1599
+ } else if (box.mesh) {
1600
+ materialIndex = this.addMaterial({
1601
+ name: `MeshMaterial_${this.materials.length}`,
1602
+ pbrMetallicRoughness: {
1603
+ baseColorFactor: [0.7, 0.7, 0.7, 1],
1604
+ metallicFactor: 0.1,
1605
+ roughnessFactor: 0.9
1606
+ },
1607
+ alphaMode: "OPAQUE"
1608
+ });
1329
1609
  }
1330
1610
  const meshIndex = this.addMesh(meshData, materialIndex, box.label);
1331
1611
  const nodeIndex = this.nodes.length;
@@ -1852,11 +2132,13 @@ async function convertCircuitJsonToGltf(circuitJson, options = {}) {
1852
2132
  export {
1853
2133
  COORDINATE_TRANSFORMS,
1854
2134
  applyCoordinateTransform,
2135
+ clearGLBCache,
1855
2136
  clearOBJCache,
1856
2137
  clearSTLCache,
1857
2138
  convertCircuitJsonTo3D,
1858
2139
  convertCircuitJsonToGltf,
1859
2140
  convertSceneToGLTF,
2141
+ loadGLB,
1860
2142
  loadOBJ,
1861
2143
  loadSTL,
1862
2144
  renderBoardLayer,
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "circuit-json-to-gltf",
3
3
  "main": "dist/index.js",
4
4
  "type": "module",
5
- "version": "0.0.10",
5
+ "version": "0.0.12",
6
6
  "scripts": {
7
7
  "test": "bun test tests/",
8
8
  "format": "biome format --write .",
@@ -28,7 +28,7 @@
28
28
  "circuit-json": "^0.0.267",
29
29
  "circuit-to-svg": "^0.0.175",
30
30
  "looks-same": "^9.0.1",
31
- "poppygl": "^0.0.6",
31
+ "poppygl": "^0.0.9",
32
32
  "react": "^19.1.1",
33
33
  "react-cosmos": "^7.0.0",
34
34
  "react-cosmos-plugin-vite": "^7.0.0",