circuit-json-to-gltf 0.0.12 → 0.0.14

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
@@ -135,7 +135,7 @@ 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>;
138
+ declare function loadGLB(url: string, transform?: CoordinateTransformConfig): Promise<STLMesh | OBJMesh>;
139
139
  declare function clearGLBCache(): void;
140
140
 
141
141
  declare function convertCircuitJsonTo3D(circuitJson: CircuitJson, options?: CircuitTo3DOptions): Promise<Scene3D>;
package/dist/index.js CHANGED
@@ -534,11 +534,59 @@ function parseGLB(buffer, transform) {
534
534
  axisMapping: { x: "x", y: "z", z: "y" }
535
535
  };
536
536
  const transformedTriangles = transformTriangles(triangles, finalConfig);
537
+ const hasColors = transformedTriangles.some((t) => t.color !== void 0);
538
+ if (hasColors) {
539
+ return convertToOBJMesh(transformedTriangles);
540
+ }
537
541
  return {
538
542
  triangles: transformedTriangles,
539
543
  boundingBox: calculateBoundingBox3(transformedTriangles)
540
544
  };
541
545
  }
546
+ function convertToOBJMesh(triangles) {
547
+ const colorGroups = /* @__PURE__ */ new Map();
548
+ for (const triangle of triangles) {
549
+ const colorKey = triangle.color ? JSON.stringify(triangle.color) : "default";
550
+ if (!colorGroups.has(colorKey)) {
551
+ colorGroups.set(colorKey, []);
552
+ }
553
+ colorGroups.get(colorKey).push(triangle);
554
+ }
555
+ const materials = /* @__PURE__ */ new Map();
556
+ const materialIndexMap = /* @__PURE__ */ new Map();
557
+ let materialIndex = 0;
558
+ const trianglesWithMaterialIndex = [];
559
+ for (const [colorKey, groupTriangles] of colorGroups) {
560
+ const materialName = `Material_${materialIndex}`;
561
+ materialIndexMap.set(materialName, materialIndex);
562
+ if (colorKey === "default") {
563
+ materials.set(materialName, {
564
+ name: materialName,
565
+ color: [179, 179, 179, 1]
566
+ // 0.7 * 255 = 179
567
+ });
568
+ } else {
569
+ const color = JSON.parse(colorKey);
570
+ materials.set(materialName, {
571
+ name: materialName,
572
+ color
573
+ });
574
+ }
575
+ for (const triangle of groupTriangles) {
576
+ trianglesWithMaterialIndex.push({
577
+ ...triangle,
578
+ materialIndex
579
+ });
580
+ }
581
+ materialIndex++;
582
+ }
583
+ return {
584
+ triangles: trianglesWithMaterialIndex,
585
+ boundingBox: calculateBoundingBox3(trianglesWithMaterialIndex),
586
+ materials,
587
+ materialIndexMap
588
+ };
589
+ }
542
590
  function extractTrianglesFromGLTF(gltf, binaryBuffer) {
543
591
  const triangles = [];
544
592
  if (!gltf.meshes || !gltf.accessors || !gltf.bufferViews) {
@@ -570,6 +618,29 @@ function extractTrianglesFromGLTF(gltf, binaryBuffer) {
570
618
  binaryBuffer
571
619
  );
572
620
  }
621
+ let vertexColors;
622
+ const colorAccessorIndex = primitive.attributes.COLOR_0;
623
+ if (colorAccessorIndex !== void 0) {
624
+ const colorAccessor = gltf.accessors[colorAccessorIndex];
625
+ vertexColors = getAccessorData(
626
+ colorAccessor,
627
+ gltf.bufferViews,
628
+ binaryBuffer
629
+ );
630
+ }
631
+ let materialColor;
632
+ if (!vertexColors && primitive.material !== void 0 && gltf.materials) {
633
+ const material = gltf.materials[primitive.material];
634
+ if (material?.pbrMetallicRoughness?.baseColorFactor) {
635
+ const factor = material.pbrMetallicRoughness.baseColorFactor;
636
+ materialColor = [
637
+ Math.round(factor[0] * 255),
638
+ Math.round(factor[1] * 255),
639
+ Math.round(factor[2] * 255),
640
+ factor[3]
641
+ ];
642
+ }
643
+ }
573
644
  let indices;
574
645
  if (primitive.indices !== void 0) {
575
646
  const indexAccessor = gltf.accessors[primitive.indices];
@@ -619,7 +690,44 @@ function extractTrianglesFromGLTF(gltf, binaryBuffer) {
619
690
  } else {
620
691
  normal = computeNormal(v0, v1, v2);
621
692
  }
622
- triangles.push({ vertices: [v0, v1, v2], normal });
693
+ let triangleColor;
694
+ if (vertexColors) {
695
+ const componentsPerColor = vertexColors.length / vertexCount;
696
+ if (componentsPerColor === 3) {
697
+ triangleColor = [
698
+ Math.round(
699
+ (vertexColors[i0 * 3] + vertexColors[i1 * 3] + vertexColors[i2 * 3]) / 3 * 255
700
+ ),
701
+ Math.round(
702
+ (vertexColors[i0 * 3 + 1] + vertexColors[i1 * 3 + 1] + vertexColors[i2 * 3 + 1]) / 3 * 255
703
+ ),
704
+ Math.round(
705
+ (vertexColors[i0 * 3 + 2] + vertexColors[i1 * 3 + 2] + vertexColors[i2 * 3 + 2]) / 3 * 255
706
+ ),
707
+ 1
708
+ ];
709
+ } else if (componentsPerColor === 4) {
710
+ triangleColor = [
711
+ Math.round(
712
+ (vertexColors[i0 * 4] + vertexColors[i1 * 4] + vertexColors[i2 * 4]) / 3 * 255
713
+ ),
714
+ Math.round(
715
+ (vertexColors[i0 * 4 + 1] + vertexColors[i1 * 4 + 1] + vertexColors[i2 * 4 + 1]) / 3 * 255
716
+ ),
717
+ Math.round(
718
+ (vertexColors[i0 * 4 + 2] + vertexColors[i1 * 4 + 2] + vertexColors[i2 * 4 + 2]) / 3 * 255
719
+ ),
720
+ (vertexColors[i0 * 4 + 3] + vertexColors[i1 * 4 + 3] + vertexColors[i2 * 4 + 3]) / 3
721
+ ];
722
+ }
723
+ } else {
724
+ triangleColor = materialColor;
725
+ }
726
+ triangles.push({
727
+ vertices: [v0, v1, v2],
728
+ normal,
729
+ color: triangleColor
730
+ });
623
731
  }
624
732
  } else {
625
733
  for (let i = 0; i < vertexCount; i += 3) {
@@ -648,7 +756,44 @@ function extractTrianglesFromGLTF(gltf, binaryBuffer) {
648
756
  } else {
649
757
  normal = computeNormal(v0, v1, v2);
650
758
  }
651
- triangles.push({ vertices: [v0, v1, v2], normal });
759
+ let triangleColor;
760
+ if (vertexColors) {
761
+ const componentsPerColor = vertexColors.length / vertexCount;
762
+ if (componentsPerColor === 3) {
763
+ triangleColor = [
764
+ Math.round(
765
+ (vertexColors[i * 3] + vertexColors[(i + 1) * 3] + vertexColors[(i + 2) * 3]) / 3 * 255
766
+ ),
767
+ Math.round(
768
+ (vertexColors[i * 3 + 1] + vertexColors[(i + 1) * 3 + 1] + vertexColors[(i + 2) * 3 + 1]) / 3 * 255
769
+ ),
770
+ Math.round(
771
+ (vertexColors[i * 3 + 2] + vertexColors[(i + 1) * 3 + 2] + vertexColors[(i + 2) * 3 + 2]) / 3 * 255
772
+ ),
773
+ 1
774
+ ];
775
+ } else if (componentsPerColor === 4) {
776
+ triangleColor = [
777
+ Math.round(
778
+ (vertexColors[i * 4] + vertexColors[(i + 1) * 4] + vertexColors[(i + 2) * 4]) / 3 * 255
779
+ ),
780
+ Math.round(
781
+ (vertexColors[i * 4 + 1] + vertexColors[(i + 1) * 4 + 1] + vertexColors[(i + 2) * 4 + 1]) / 3 * 255
782
+ ),
783
+ Math.round(
784
+ (vertexColors[i * 4 + 2] + vertexColors[(i + 1) * 4 + 2] + vertexColors[(i + 2) * 4 + 2]) / 3 * 255
785
+ ),
786
+ (vertexColors[i * 4 + 3] + vertexColors[(i + 1) * 4 + 3] + vertexColors[(i + 2) * 4 + 3]) / 3
787
+ ];
788
+ }
789
+ } else {
790
+ triangleColor = materialColor;
791
+ }
792
+ triangles.push({
793
+ vertices: [v0, v1, v2],
794
+ normal,
795
+ color: triangleColor
796
+ });
652
797
  }
653
798
  }
654
799
  }
@@ -742,6 +887,95 @@ function clearGLBCache() {
742
887
  glbCache.clear();
743
888
  }
744
889
 
890
+ // lib/loaders/gltf.ts
891
+ async function fetchAsArrayBuffer(url) {
892
+ const response = await fetch(url);
893
+ if (!response.ok) {
894
+ throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
895
+ }
896
+ return response.arrayBuffer();
897
+ }
898
+ function dataUriToArrayBuffer(uri) {
899
+ const a = uri.split(",");
900
+ const byteString = atob(a[1]);
901
+ const ab = new ArrayBuffer(byteString.length);
902
+ const ia = new Uint8Array(ab);
903
+ for (let i = 0; i < byteString.length; i++) {
904
+ ia[i] = byteString.charCodeAt(i);
905
+ }
906
+ return ab;
907
+ }
908
+ async function fetchGltfAndConvertToGlb(url) {
909
+ const gltfResponse = await fetch(url);
910
+ if (!gltfResponse.ok) {
911
+ throw new Error(`Failed to fetch glTF file: ${gltfResponse.statusText}`);
912
+ }
913
+ const gltf = await gltfResponse.json();
914
+ const bufferPromises = [];
915
+ if (gltf.buffers) {
916
+ for (const buffer of gltf.buffers) {
917
+ if (buffer.uri) {
918
+ if (buffer.uri.startsWith("data:")) {
919
+ bufferPromises.push(Promise.resolve(dataUriToArrayBuffer(buffer.uri)));
920
+ } else {
921
+ const bufferUrl = new URL(buffer.uri, url).toString();
922
+ bufferPromises.push(fetchAsArrayBuffer(bufferUrl));
923
+ }
924
+ }
925
+ }
926
+ }
927
+ const buffers = await Promise.all(bufferPromises);
928
+ let binaryBuffer = new ArrayBuffer(0);
929
+ if (buffers.length > 0 && buffers[0]) {
930
+ binaryBuffer = buffers[0];
931
+ }
932
+ if (gltf.buffers && gltf.buffers.length > 0) {
933
+ delete gltf.buffers[0].uri;
934
+ gltf.buffers[0].byteLength = binaryBuffer.byteLength;
935
+ }
936
+ const jsonString = JSON.stringify(gltf);
937
+ const jsonBuffer = new TextEncoder().encode(jsonString);
938
+ const jsonPadding = (4 - jsonBuffer.length % 4) % 4;
939
+ const binaryPadding = (4 - binaryBuffer.byteLength % 4) % 4;
940
+ const totalLength = 12 + // header
941
+ (8 + jsonBuffer.length + jsonPadding) + // json chunk
942
+ (binaryBuffer.byteLength > 0 ? 8 + binaryBuffer.byteLength + binaryPadding : 0);
943
+ const glbBuffer = new ArrayBuffer(totalLength);
944
+ const dataView = new DataView(glbBuffer);
945
+ let offset = 0;
946
+ dataView.setUint32(offset, 1179937895, true);
947
+ offset += 4;
948
+ dataView.setUint32(offset, 2, true);
949
+ offset += 4;
950
+ dataView.setUint32(offset, totalLength, true);
951
+ offset += 4;
952
+ dataView.setUint32(offset, jsonBuffer.length + jsonPadding, true);
953
+ offset += 4;
954
+ dataView.setUint32(offset, 1313821514, true);
955
+ offset += 4;
956
+ new Uint8Array(glbBuffer, offset).set(jsonBuffer);
957
+ offset += jsonBuffer.length;
958
+ for (let i = 0; i < jsonPadding; i++) {
959
+ dataView.setUint8(offset++, 32);
960
+ }
961
+ if (binaryBuffer.byteLength > 0) {
962
+ dataView.setUint32(offset, binaryBuffer.byteLength + binaryPadding, true);
963
+ offset += 4;
964
+ dataView.setUint32(offset, 5130562, true);
965
+ offset += 4;
966
+ new Uint8Array(glbBuffer, offset).set(new Uint8Array(binaryBuffer));
967
+ offset += binaryBuffer.byteLength;
968
+ for (let i = 0; i < binaryPadding; i++) {
969
+ dataView.setUint8(offset++, 0);
970
+ }
971
+ }
972
+ return glbBuffer;
973
+ }
974
+ async function loadGLTF(url, transform) {
975
+ const glb_buffer = await fetchGltfAndConvertToGlb(url);
976
+ return parseGLB(glb_buffer, transform);
977
+ }
978
+
745
979
  // lib/converters/board-renderer.ts
746
980
  import { convertCircuitJsonToPcbSvg } from "circuit-to-svg";
747
981
  async function renderBoardLayer(circuitJson, options) {
@@ -900,12 +1134,15 @@ async function convertCircuitJsonTo3D(circuitJson, options = {}) {
900
1134
  const cadComponents = db.cad_component?.list?.() ?? [];
901
1135
  const pcbComponentIdsWith3D = /* @__PURE__ */ new Set();
902
1136
  for (const cad of cadComponents) {
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);
1137
+ let { model_stl_url, model_obj_url, model_glb_url, model_gltf_url } = cad;
1138
+ const hasModelUrl = Boolean(
1139
+ model_stl_url || model_obj_url || model_glb_url || model_gltf_url
1140
+ );
905
1141
  if (!hasModelUrl && cad.footprinter_string) {
906
1142
  model_glb_url = `https://modelcdn.tscircuit.com/jscad_models/${cad.footprinter_string}.glb`;
907
1143
  }
908
- if (!model_stl_url && !model_obj_url && !model_glb_url) continue;
1144
+ if (!model_stl_url && !model_obj_url && !model_glb_url && !model_gltf_url)
1145
+ continue;
909
1146
  pcbComponentIdsWith3D.add(cad.pcb_component_id);
910
1147
  const pcbComponent = db.pcb_component.get(cad.pcb_component_id);
911
1148
  const size = cad.size ?? {
@@ -918,26 +1155,35 @@ async function convertCircuitJsonTo3D(circuitJson, options = {}) {
918
1155
  y: boardThickness / 2 + size.y / 2,
919
1156
  z: pcbComponent?.center.y ?? 0
920
1157
  };
1158
+ const meshType = model_stl_url ? "stl" : model_obj_url ? "obj" : model_gltf_url ? "gltf" : "glb";
921
1159
  const box = {
922
1160
  center,
923
1161
  size,
924
- meshUrl: model_stl_url || model_obj_url || model_glb_url,
925
- meshType: model_stl_url ? "stl" : model_obj_url ? "obj" : "glb"
1162
+ meshUrl: model_stl_url || model_obj_url || model_glb_url || model_gltf_url,
1163
+ meshType
926
1164
  };
927
1165
  if (cad.rotation) {
928
- box.rotation = convertRotationFromCadRotation(cad.rotation);
929
- }
930
- const defaultTransform = coordinateTransform ?? (model_glb_url ? void 0 : COORDINATE_TRANSFORMS.Z_UP_TO_Y_UP_USB_FIX);
931
- try {
932
- if (model_stl_url) {
933
- box.mesh = await loadSTL(model_stl_url, defaultTransform);
934
- } else if (model_obj_url) {
935
- box.mesh = await loadOBJ(model_obj_url, defaultTransform);
936
- } else if (model_glb_url) {
937
- box.mesh = await loadGLB(model_glb_url, defaultTransform);
1166
+ if (model_glb_url || model_gltf_url) {
1167
+ box.rotation = convertRotationFromCadRotation({
1168
+ x: cad.rotation.x,
1169
+ y: cad.rotation.z,
1170
+ // Circuit Z rotation becomes model Y rotation
1171
+ z: cad.rotation.y
1172
+ // Circuit Y rotation becomes model Z rotation
1173
+ });
1174
+ } else {
1175
+ box.rotation = convertRotationFromCadRotation(cad.rotation);
938
1176
  }
939
- } catch (error) {
940
- console.warn(`Failed to load 3D model: ${error}`);
1177
+ }
1178
+ const defaultTransform = coordinateTransform ?? (model_glb_url || model_gltf_url ? void 0 : COORDINATE_TRANSFORMS.Z_UP_TO_Y_UP_USB_FIX);
1179
+ if (model_stl_url) {
1180
+ box.mesh = await loadSTL(model_stl_url, defaultTransform);
1181
+ } else if (model_obj_url) {
1182
+ box.mesh = await loadOBJ(model_obj_url, defaultTransform);
1183
+ } else if (model_glb_url) {
1184
+ box.mesh = await loadGLB(model_glb_url, defaultTransform);
1185
+ } else if (model_gltf_url) {
1186
+ box.mesh = await loadGLTF(model_gltf_url, defaultTransform);
941
1187
  }
942
1188
  if (!box.mesh) {
943
1189
  box.color = componentColor;
@@ -1619,18 +1865,19 @@ var GLTFBuilder = class {
1619
1865
  const meshDataArray = createMeshFromOBJ(objMesh);
1620
1866
  const objMaterialIndices = /* @__PURE__ */ new Map();
1621
1867
  for (const [name, objMaterial] of objMesh.materials) {
1622
- const dissolve = objMaterial.dissolve ?? 1;
1623
- const alpha = 1 - dissolve;
1624
- let baseColor = [0.3, 0.3, 0.3, alpha];
1868
+ let baseColor = [0.3, 0.3, 0.3, 1];
1625
1869
  if (objMaterial.color) {
1626
1870
  const color = typeof objMaterial.color === "string" ? this.parseColorString(objMaterial.color) : [
1627
1871
  objMaterial.color[0] / 255,
1628
1872
  objMaterial.color[1] / 255,
1629
1873
  objMaterial.color[2] / 255,
1630
- alpha
1874
+ objMaterial.color[3]
1875
+ // Use the alpha from the color
1631
1876
  ];
1632
- baseColor = [color[0], color[1], color[2], alpha];
1877
+ baseColor = [color[0], color[1], color[2], color[3]];
1633
1878
  }
1879
+ const alpha = objMaterial.dissolve !== void 0 ? 1 - objMaterial.dissolve : baseColor[3];
1880
+ baseColor[3] = alpha;
1634
1881
  const gltfMaterialIndex = this.addMaterial({
1635
1882
  name: `OBJ_${name}`,
1636
1883
  pbrMetallicRoughness: {
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.12",
5
+ "version": "0.0.14",
6
6
  "scripts": {
7
7
  "test": "bun test tests/",
8
8
  "format": "biome format --write .",