circuit-json-to-gltf 0.0.17 → 0.0.18

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 (2) hide show
  1. package/dist/index.js +366 -6
  2. package/package.json +4 -1
package/dist/index.js CHANGED
@@ -1078,6 +1078,180 @@ async function renderBoardTextures(circuitJson, resolution = 1024) {
1078
1078
  return { top, bottom };
1079
1079
  }
1080
1080
 
1081
+ // lib/utils/pcb-board-geometry.ts
1082
+ import { extrudeLinear } from "@jscad/modeling/src/operations/extrusions";
1083
+ import {
1084
+ polygon,
1085
+ rectangle,
1086
+ roundedRectangle,
1087
+ cylinder
1088
+ } from "@jscad/modeling/src/primitives";
1089
+ import {
1090
+ translate,
1091
+ rotateZ,
1092
+ rotateX
1093
+ } from "@jscad/modeling/src/operations/transforms";
1094
+ import { subtract } from "@jscad/modeling/src/operations/booleans";
1095
+ import * as geom3 from "@jscad/modeling/src/geometries/geom3";
1096
+ import measureBoundingBox from "@jscad/modeling/src/measurements/measureBoundingBox";
1097
+ var DEFAULT_SEGMENTS = 64;
1098
+ var RADIUS_EPSILON = 1e-4;
1099
+ var toVec2 = (point, center) => [
1100
+ point.x - center.x,
1101
+ point.y - center.y
1102
+ ];
1103
+ var arePointsClockwise = (points) => {
1104
+ let area = 0;
1105
+ for (let i = 0; i < points.length; i++) {
1106
+ const j = (i + 1) % points.length;
1107
+ area += points[i][0] * points[j][1];
1108
+ area -= points[j][0] * points[i][1];
1109
+ }
1110
+ const signedArea = area / 2;
1111
+ return signedArea <= 0;
1112
+ };
1113
+ var getNumberProperty = (obj, key) => {
1114
+ const value = obj[key];
1115
+ return typeof value === "number" ? value : void 0;
1116
+ };
1117
+ var createBoardOutlineGeom = (board, center, thickness) => {
1118
+ if (board.outline && board.outline.length >= 3) {
1119
+ let outlinePoints = board.outline.map((pt) => toVec2(pt, center));
1120
+ if (arePointsClockwise(outlinePoints)) {
1121
+ outlinePoints = outlinePoints.slice().reverse();
1122
+ }
1123
+ const shape2d = polygon({ points: outlinePoints });
1124
+ let geom2 = extrudeLinear({ height: thickness }, shape2d);
1125
+ geom2 = translate([0, 0, -thickness / 2], geom2);
1126
+ return geom2;
1127
+ }
1128
+ const baseRect = rectangle({ size: [board.width, board.height] });
1129
+ let geom = extrudeLinear({ height: thickness }, baseRect);
1130
+ geom = translate([0, 0, -thickness / 2], geom);
1131
+ return geom;
1132
+ };
1133
+ var createCircularHole = (x, y, radius, thickness) => cylinder({
1134
+ center: [x, y, 0],
1135
+ height: thickness + 1,
1136
+ radius,
1137
+ segments: DEFAULT_SEGMENTS
1138
+ });
1139
+ var createPillHole = (x, y, width, height, thickness, rotate) => {
1140
+ const minDimension = Math.min(width, height);
1141
+ const maxAllowedRadius = Math.max(0, minDimension / 2 - RADIUS_EPSILON);
1142
+ const roundRadius = maxAllowedRadius <= 0 ? 0 : Math.min(height / 2, maxAllowedRadius);
1143
+ const hole2d = roundedRectangle({
1144
+ size: [width, height],
1145
+ roundRadius,
1146
+ segments: DEFAULT_SEGMENTS
1147
+ });
1148
+ let hole3d = extrudeLinear({ height: thickness + 1 }, hole2d);
1149
+ hole3d = translate([0, 0, -(thickness + 1) / 2], hole3d);
1150
+ if (rotate) {
1151
+ hole3d = rotateZ(Math.PI / 2, hole3d);
1152
+ }
1153
+ return translate([x, y, 0], hole3d);
1154
+ };
1155
+ var createHoleGeoms = (boardCenter, thickness, holes = [], platedHoles = []) => {
1156
+ const holeGeoms = [];
1157
+ for (const hole of holes) {
1158
+ const holeRecord = hole;
1159
+ const diameter = getNumberProperty(holeRecord, "hole_diameter") ?? getNumberProperty(holeRecord, "diameter");
1160
+ if (!diameter) continue;
1161
+ const radius = diameter / 2;
1162
+ const relX = hole.x - boardCenter.x;
1163
+ const relY = -(hole.y - boardCenter.y);
1164
+ holeGeoms.push(createCircularHole(relX, relY, radius, thickness));
1165
+ }
1166
+ for (const plated of platedHoles) {
1167
+ const relX = plated.x - boardCenter.x;
1168
+ const relY = -(plated.y - boardCenter.y);
1169
+ const platedRecord = plated;
1170
+ if (plated.shape === "pill" || plated.shape === "pill_hole_with_rect_pad") {
1171
+ const holeWidth = getNumberProperty(platedRecord, "hole_width") ?? getNumberProperty(platedRecord, "outer_diameter") ?? 0;
1172
+ const holeHeight = getNumberProperty(platedRecord, "hole_height") ?? getNumberProperty(platedRecord, "hole_diameter") ?? 0;
1173
+ if (!holeWidth || !holeHeight) continue;
1174
+ const rotate = holeHeight > holeWidth;
1175
+ const width = rotate ? holeHeight : holeWidth;
1176
+ const height = rotate ? holeWidth : holeHeight;
1177
+ holeGeoms.push(
1178
+ createPillHole(relX, relY, width, height, thickness, rotate)
1179
+ );
1180
+ continue;
1181
+ }
1182
+ const diameter = getNumberProperty(platedRecord, "hole_diameter") ?? getNumberProperty(platedRecord, "outer_diameter");
1183
+ if (!diameter) continue;
1184
+ holeGeoms.push(createCircularHole(relX, relY, diameter / 2, thickness));
1185
+ }
1186
+ return holeGeoms;
1187
+ };
1188
+ var geom3ToTriangles = (geometry, polygons) => {
1189
+ const sourcePolygons = polygons ?? geom3.toPolygons(geometry);
1190
+ const triangles = [];
1191
+ for (const poly of sourcePolygons) {
1192
+ if (!poly || poly.vertices.length < 3) continue;
1193
+ const base = poly.vertices[0];
1194
+ const next = poly.vertices[1];
1195
+ const next2 = poly.vertices[2];
1196
+ const ab = [next[0] - base[0], next[1] - base[1], next[2] - base[2]];
1197
+ const ac = [
1198
+ next2[0] - base[0],
1199
+ next2[1] - base[1],
1200
+ next2[2] - base[2]
1201
+ ];
1202
+ const cross = [
1203
+ ab[1] * ac[2] - ab[2] * ac[1],
1204
+ ab[2] * ac[0] - ab[0] * ac[2],
1205
+ ab[0] * ac[1] - ab[1] * ac[0]
1206
+ ];
1207
+ const length = Math.sqrt(cross[0] ** 2 + cross[1] ** 2 + cross[2] ** 2) || 1;
1208
+ const normal = {
1209
+ x: cross[0] / length,
1210
+ y: cross[1] / length,
1211
+ z: cross[2] / length
1212
+ };
1213
+ for (let i = 1; i < poly.vertices.length - 1; i++) {
1214
+ const v1 = poly.vertices[i];
1215
+ const v2 = poly.vertices[i + 1];
1216
+ const triangle = {
1217
+ vertices: [
1218
+ { x: base[0], y: base[1], z: base[2] },
1219
+ { x: v1[0], y: v1[1], z: v1[2] },
1220
+ { x: v2[0], y: v2[1], z: v2[2] }
1221
+ ],
1222
+ normal
1223
+ };
1224
+ triangles.push(triangle);
1225
+ }
1226
+ }
1227
+ return triangles;
1228
+ };
1229
+ var createBoundingBox = (bbox) => {
1230
+ const [min, max] = bbox;
1231
+ return {
1232
+ min: { x: min[0], y: min[1], z: min[2] },
1233
+ max: { x: max[0], y: max[1], z: max[2] }
1234
+ };
1235
+ };
1236
+ var createBoardMesh = (board, options) => {
1237
+ const { thickness, holes = [], platedHoles = [] } = options;
1238
+ const center = board.center ?? { x: 0, y: 0 };
1239
+ let boardGeom = createBoardOutlineGeom(board, center, thickness);
1240
+ const holeGeoms = createHoleGeoms(center, thickness, holes, platedHoles);
1241
+ if (holeGeoms.length > 0) {
1242
+ boardGeom = subtract(boardGeom, ...holeGeoms);
1243
+ }
1244
+ boardGeom = rotateX(-Math.PI / 2, boardGeom);
1245
+ const polygons = geom3.toPolygons(boardGeom);
1246
+ const triangles = geom3ToTriangles(boardGeom, polygons);
1247
+ const bboxValues = measureBoundingBox(boardGeom);
1248
+ const boundingBox = createBoundingBox(bboxValues);
1249
+ return {
1250
+ triangles,
1251
+ boundingBox
1252
+ };
1253
+ };
1254
+
1081
1255
  // lib/converters/circuit-to-3d.ts
1082
1256
  var DEFAULT_BOARD_THICKNESS = 1.6;
1083
1257
  var DEFAULT_COMPONENT_HEIGHT = 2;
@@ -1101,7 +1275,17 @@ async function convertCircuitJsonTo3D(circuitJson, options = {}) {
1101
1275
  const db = cju(circuitJson);
1102
1276
  const boxes = [];
1103
1277
  const pcbBoard = db.pcb_board?.list?.()[0];
1278
+ const effectiveBoardThickness = pcbBoard?.thickness ?? boardThickness;
1104
1279
  if (pcbBoard) {
1280
+ const pcbHoles = db.pcb_hole?.list?.() ?? [];
1281
+ const pcbPlatedHoles = db.pcb_plated_hole?.list?.() ?? [];
1282
+ const boardMesh = createBoardMesh(pcbBoard, {
1283
+ thickness: effectiveBoardThickness,
1284
+ holes: pcbHoles,
1285
+ platedHoles: pcbPlatedHoles
1286
+ });
1287
+ const meshWidth = boardMesh.boundingBox.max.x - boardMesh.boundingBox.min.x;
1288
+ const meshHeight = boardMesh.boundingBox.max.z - boardMesh.boundingBox.min.z;
1105
1289
  const boardBox = {
1106
1290
  center: {
1107
1291
  x: pcbBoard.center.x,
@@ -1109,10 +1293,12 @@ async function convertCircuitJsonTo3D(circuitJson, options = {}) {
1109
1293
  z: pcbBoard.center.y
1110
1294
  },
1111
1295
  size: {
1112
- x: pcbBoard.width,
1113
- y: boardThickness,
1114
- z: pcbBoard.height
1115
- }
1296
+ x: Number.isFinite(meshWidth) ? meshWidth : pcbBoard.width,
1297
+ y: effectiveBoardThickness,
1298
+ z: Number.isFinite(meshHeight) ? meshHeight : pcbBoard.height
1299
+ },
1300
+ mesh: boardMesh,
1301
+ color: pcbColor
1116
1302
  };
1117
1303
  if (shouldRenderTextures && textureResolution > 0) {
1118
1304
  try {
@@ -1159,7 +1345,7 @@ async function convertCircuitJsonTo3D(circuitJson, options = {}) {
1159
1345
  z: cad.position.y
1160
1346
  } : {
1161
1347
  x: pcbComponent?.center.x ?? 0,
1162
- y: isBottomLayer ? -(boardThickness / 2 + size.y / 2) : boardThickness / 2 + size.y / 2,
1348
+ y: isBottomLayer ? -(effectiveBoardThickness / 2 + size.y / 2) : effectiveBoardThickness / 2 + size.y / 2,
1163
1349
  z: pcbComponent?.center.y ?? 0
1164
1350
  };
1165
1351
  const meshType = model_stl_url ? "stl" : model_obj_url ? "obj" : model_gltf_url ? "gltf" : "glb";
@@ -1228,7 +1414,7 @@ async function convertCircuitJsonTo3D(circuitJson, options = {}) {
1228
1414
  boxes.push({
1229
1415
  center: {
1230
1416
  x: component.center.x,
1231
- y: isBottomLayer ? -(boardThickness / 2 + compHeight / 2) : boardThickness / 2 + compHeight / 2,
1417
+ y: isBottomLayer ? -(effectiveBoardThickness / 2 + compHeight / 2) : effectiveBoardThickness / 2 + compHeight / 2,
1232
1418
  z: component.center.y
1233
1419
  },
1234
1420
  size: {
@@ -2017,7 +2203,181 @@ var GLTFBuilder = class {
2017
2203
  });
2018
2204
  this.gltf.scenes[0].nodes.push(nodeIndex);
2019
2205
  }
2206
+ async addMeshWithFaceTextures(box, defaultMaterialIndex) {
2207
+ const topTriangles = [];
2208
+ const bottomTriangles = [];
2209
+ const sideTriangles = [];
2210
+ const yThreshold = 0.8;
2211
+ for (const triangle of box.mesh.triangles) {
2212
+ const ny = Math.abs(triangle.normal.y);
2213
+ if (ny > yThreshold) {
2214
+ if (triangle.normal.y > 0) {
2215
+ topTriangles.push(triangle);
2216
+ } else {
2217
+ bottomTriangles.push(triangle);
2218
+ }
2219
+ } else {
2220
+ sideTriangles.push(triangle);
2221
+ }
2222
+ }
2223
+ const materials = [];
2224
+ if (topTriangles.length > 0 && box.texture?.top) {
2225
+ const topMaterialIndex = this.addMaterial({
2226
+ name: `TopMaterial_${this.materials.length}`,
2227
+ pbrMetallicRoughness: {
2228
+ baseColorFactor: [1, 1, 1, 1],
2229
+ metallicFactor: 0,
2230
+ roughnessFactor: 0.8
2231
+ },
2232
+ alphaMode: "OPAQUE",
2233
+ doubleSided: true
2234
+ });
2235
+ const textureIndex = await this.addTextureFromDataUrl(box.texture.top);
2236
+ if (textureIndex !== -1) {
2237
+ const material = this.materials[topMaterialIndex];
2238
+ if (material.pbrMetallicRoughness) {
2239
+ material.pbrMetallicRoughness.baseColorTexture = {
2240
+ index: textureIndex
2241
+ };
2242
+ }
2243
+ }
2244
+ materials.push({
2245
+ triangles: topTriangles,
2246
+ materialIndex: topMaterialIndex
2247
+ });
2248
+ }
2249
+ if (bottomTriangles.length > 0 && box.texture?.bottom) {
2250
+ const bottomMaterialIndex = this.addMaterial({
2251
+ name: `BottomMaterial_${this.materials.length}`,
2252
+ pbrMetallicRoughness: {
2253
+ baseColorFactor: [1, 1, 1, 1],
2254
+ metallicFactor: 0,
2255
+ roughnessFactor: 0.8
2256
+ },
2257
+ alphaMode: "OPAQUE",
2258
+ doubleSided: true
2259
+ });
2260
+ const textureIndex = await this.addTextureFromDataUrl(box.texture.bottom);
2261
+ if (textureIndex !== -1) {
2262
+ const material = this.materials[bottomMaterialIndex];
2263
+ if (material.pbrMetallicRoughness) {
2264
+ material.pbrMetallicRoughness.baseColorTexture = {
2265
+ index: textureIndex
2266
+ };
2267
+ }
2268
+ }
2269
+ materials.push({
2270
+ triangles: bottomTriangles,
2271
+ materialIndex: bottomMaterialIndex
2272
+ });
2273
+ }
2274
+ if (sideTriangles.length > 0) {
2275
+ const sideMaterialIndex = this.addMaterial({
2276
+ name: `GreenSideMaterial_${this.materials.length}`,
2277
+ pbrMetallicRoughness: {
2278
+ baseColorFactor: [0, 0.55, 0, 1],
2279
+ metallicFactor: 0,
2280
+ roughnessFactor: 0.8
2281
+ },
2282
+ alphaMode: "OPAQUE",
2283
+ doubleSided: true
2284
+ });
2285
+ materials.push({
2286
+ triangles: sideTriangles,
2287
+ materialIndex: sideMaterialIndex
2288
+ });
2289
+ }
2290
+ const primitives = [];
2291
+ const bounds = getBounds([]);
2292
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
2293
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
2294
+ for (const triangle of box.mesh.triangles) {
2295
+ for (const v of triangle.vertices) {
2296
+ minX = Math.min(minX, v.x);
2297
+ minY = Math.min(minY, v.y);
2298
+ minZ = Math.min(minZ, v.z);
2299
+ maxX = Math.max(maxX, v.x);
2300
+ maxY = Math.max(maxY, v.y);
2301
+ maxZ = Math.max(maxZ, v.z);
2302
+ }
2303
+ }
2304
+ const sizeX = maxX - minX;
2305
+ const sizeZ = maxZ - minZ;
2306
+ for (const { triangles, materialIndex } of materials) {
2307
+ const positions = [];
2308
+ const normals = [];
2309
+ const texcoords = [];
2310
+ const indices = [];
2311
+ let vertexIndex = 0;
2312
+ for (const triangle of triangles) {
2313
+ for (const v of triangle.vertices) {
2314
+ positions.push(v.x, v.y, v.z);
2315
+ normals.push(triangle.normal.x, triangle.normal.y, triangle.normal.z);
2316
+ const u = sizeX > 0 ? (v.x - minX) / sizeX : 0.5;
2317
+ const v_coord = sizeZ > 0 ? (v.z - minZ) / sizeZ : 0.5;
2318
+ texcoords.push(u, 1 - v_coord);
2319
+ }
2320
+ indices.push(vertexIndex, vertexIndex + 1, vertexIndex + 2);
2321
+ vertexIndex += 3;
2322
+ }
2323
+ const meshData = { positions, normals, texcoords, indices };
2324
+ const transformedMeshData = transformMesh(
2325
+ meshData,
2326
+ box.center,
2327
+ box.rotation
2328
+ );
2329
+ const positionAccessorIndex = this.addAccessor(
2330
+ transformedMeshData.positions,
2331
+ "VEC3",
2332
+ COMPONENT_TYPE.FLOAT,
2333
+ TARGET.ARRAY_BUFFER
2334
+ );
2335
+ const normalAccessorIndex = this.addAccessor(
2336
+ transformedMeshData.normals,
2337
+ "VEC3",
2338
+ COMPONENT_TYPE.FLOAT,
2339
+ TARGET.ARRAY_BUFFER
2340
+ );
2341
+ const texcoordAccessorIndex = this.addAccessor(
2342
+ transformedMeshData.texcoords,
2343
+ "VEC2",
2344
+ COMPONENT_TYPE.FLOAT,
2345
+ TARGET.ARRAY_BUFFER
2346
+ );
2347
+ const indicesAccessorIndex = this.addAccessor(
2348
+ transformedMeshData.indices,
2349
+ "SCALAR",
2350
+ COMPONENT_TYPE.UNSIGNED_SHORT,
2351
+ TARGET.ELEMENT_ARRAY_BUFFER
2352
+ );
2353
+ primitives.push({
2354
+ attributes: {
2355
+ POSITION: positionAccessorIndex,
2356
+ NORMAL: normalAccessorIndex,
2357
+ TEXCOORD_0: texcoordAccessorIndex
2358
+ },
2359
+ indices: indicesAccessorIndex,
2360
+ material: materialIndex,
2361
+ mode: PRIMITIVE_MODE.TRIANGLES
2362
+ });
2363
+ }
2364
+ const meshIndex = this.meshes.length;
2365
+ this.meshes.push({
2366
+ name: box.label || `MeshWithTextures${meshIndex}`,
2367
+ primitives
2368
+ });
2369
+ const nodeIndex = this.nodes.length;
2370
+ this.nodes.push({
2371
+ name: box.label || `Box${nodeIndex}`,
2372
+ mesh: meshIndex
2373
+ });
2374
+ this.gltf.scenes[0].nodes.push(nodeIndex);
2375
+ }
2020
2376
  async addBoxWithFaceMaterials(box, defaultMaterialIndex) {
2377
+ if (box.mesh) {
2378
+ await this.addMeshWithFaceTextures(box, defaultMaterialIndex);
2379
+ return;
2380
+ }
2021
2381
  const faceMeshes = createBoxMeshByFaces(box.size);
2022
2382
  const faceMaterials = {};
2023
2383
  if (box.texture?.top) {
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.17",
5
+ "version": "0.0.18",
6
6
  "scripts": {
7
7
  "test": "bun test tests/",
8
8
  "format": "biome format --write .",
@@ -53,5 +53,8 @@
53
53
  "@resvg/resvg-js": {
54
54
  "optional": true
55
55
  }
56
+ },
57
+ "dependencies": {
58
+ "@jscad/modeling": "^2.12.6"
56
59
  }
57
60
  }