circuit-json-to-gltf 0.0.16 → 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.
- package/dist/index.js +458 -50
- package/package.json +7 -3
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;
|
|
@@ -1100,37 +1274,51 @@ async function convertCircuitJsonTo3D(circuitJson, options = {}) {
|
|
|
1100
1274
|
} = options;
|
|
1101
1275
|
const db = cju(circuitJson);
|
|
1102
1276
|
const boxes = [];
|
|
1103
|
-
const pcbBoard = db.pcb_board
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1277
|
+
const pcbBoard = db.pcb_board?.list?.()[0];
|
|
1278
|
+
const effectiveBoardThickness = pcbBoard?.thickness ?? boardThickness;
|
|
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;
|
|
1289
|
+
const boardBox = {
|
|
1290
|
+
center: {
|
|
1291
|
+
x: pcbBoard.center.x,
|
|
1292
|
+
y: 0,
|
|
1293
|
+
z: pcbBoard.center.y
|
|
1294
|
+
},
|
|
1295
|
+
size: {
|
|
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
|
|
1302
|
+
};
|
|
1303
|
+
if (shouldRenderTextures && textureResolution > 0) {
|
|
1304
|
+
try {
|
|
1305
|
+
const textures = await renderBoardTextures(
|
|
1306
|
+
circuitJson,
|
|
1307
|
+
textureResolution
|
|
1308
|
+
);
|
|
1309
|
+
boardBox.texture = {
|
|
1310
|
+
top: textures.top,
|
|
1311
|
+
bottom: textures.bottom
|
|
1312
|
+
};
|
|
1313
|
+
} catch (error) {
|
|
1314
|
+
console.warn("Failed to render board textures:", error);
|
|
1315
|
+
boardBox.color = pcbColor;
|
|
1316
|
+
}
|
|
1317
|
+
} else {
|
|
1128
1318
|
boardBox.color = pcbColor;
|
|
1129
1319
|
}
|
|
1130
|
-
|
|
1131
|
-
boardBox.color = pcbColor;
|
|
1320
|
+
boxes.push(boardBox);
|
|
1132
1321
|
}
|
|
1133
|
-
boxes.push(boardBox);
|
|
1134
1322
|
const cadComponents = db.cad_component?.list?.() ?? [];
|
|
1135
1323
|
const pcbComponentIdsWith3D = /* @__PURE__ */ new Set();
|
|
1136
1324
|
for (const cad of cadComponents) {
|
|
@@ -1157,7 +1345,7 @@ async function convertCircuitJsonTo3D(circuitJson, options = {}) {
|
|
|
1157
1345
|
z: cad.position.y
|
|
1158
1346
|
} : {
|
|
1159
1347
|
x: pcbComponent?.center.x ?? 0,
|
|
1160
|
-
y: isBottomLayer ? -(
|
|
1348
|
+
y: isBottomLayer ? -(effectiveBoardThickness / 2 + size.y / 2) : effectiveBoardThickness / 2 + size.y / 2,
|
|
1161
1349
|
z: pcbComponent?.center.y ?? 0
|
|
1162
1350
|
};
|
|
1163
1351
|
const meshType = model_stl_url ? "stl" : model_obj_url ? "obj" : model_gltf_url ? "gltf" : "glb";
|
|
@@ -1226,7 +1414,7 @@ async function convertCircuitJsonTo3D(circuitJson, options = {}) {
|
|
|
1226
1414
|
boxes.push({
|
|
1227
1415
|
center: {
|
|
1228
1416
|
x: component.center.x,
|
|
1229
|
-
y: isBottomLayer ? -(
|
|
1417
|
+
y: isBottomLayer ? -(effectiveBoardThickness / 2 + compHeight / 2) : effectiveBoardThickness / 2 + compHeight / 2,
|
|
1230
1418
|
z: component.center.y
|
|
1231
1419
|
},
|
|
1232
1420
|
size: {
|
|
@@ -1239,26 +1427,72 @@ async function convertCircuitJsonTo3D(circuitJson, options = {}) {
|
|
|
1239
1427
|
labelColor: "white"
|
|
1240
1428
|
});
|
|
1241
1429
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1430
|
+
let camera;
|
|
1431
|
+
if (pcbBoard) {
|
|
1432
|
+
const boardDiagonal = Math.sqrt(
|
|
1433
|
+
pcbBoard.width * pcbBoard.width + pcbBoard.height * pcbBoard.height
|
|
1434
|
+
);
|
|
1435
|
+
const cameraDistance = boardDiagonal * 1.5;
|
|
1436
|
+
camera = {
|
|
1437
|
+
position: {
|
|
1438
|
+
x: pcbBoard.center.x + cameraDistance * 0.5,
|
|
1439
|
+
y: cameraDistance * 0.7,
|
|
1440
|
+
z: pcbBoard.center.y + cameraDistance * 0.5
|
|
1441
|
+
},
|
|
1442
|
+
target: {
|
|
1443
|
+
x: pcbBoard.center.x,
|
|
1444
|
+
y: 0,
|
|
1445
|
+
z: pcbBoard.center.y
|
|
1446
|
+
},
|
|
1447
|
+
up: { x: 0, y: 1, z: 0 },
|
|
1448
|
+
fov: 50,
|
|
1449
|
+
near: 0.1,
|
|
1450
|
+
far: cameraDistance * 4
|
|
1451
|
+
};
|
|
1452
|
+
} else {
|
|
1453
|
+
const hasBoxes = boxes.length > 0;
|
|
1454
|
+
if (hasBoxes) {
|
|
1455
|
+
let minX = Infinity;
|
|
1456
|
+
let minZ = Infinity;
|
|
1457
|
+
let maxX = -Infinity;
|
|
1458
|
+
let maxZ = -Infinity;
|
|
1459
|
+
for (const box of boxes) {
|
|
1460
|
+
const halfX = (box.size?.x ?? 0) / 2;
|
|
1461
|
+
const halfZ = (box.size?.z ?? 0) / 2;
|
|
1462
|
+
minX = Math.min(minX, box.center.x - halfX);
|
|
1463
|
+
maxX = Math.max(maxX, box.center.x + halfX);
|
|
1464
|
+
minZ = Math.min(minZ, box.center.z - halfZ);
|
|
1465
|
+
maxZ = Math.max(maxZ, box.center.z + halfZ);
|
|
1466
|
+
}
|
|
1467
|
+
const width = Math.max(maxX - minX, 1);
|
|
1468
|
+
const height = Math.max(maxZ - minZ, 1);
|
|
1469
|
+
const diagonal = Math.sqrt(width * width + height * height);
|
|
1470
|
+
const distance = diagonal * 1.5;
|
|
1471
|
+
const centerX = (minX + maxX) / 2;
|
|
1472
|
+
const centerZ = (minZ + maxZ) / 2;
|
|
1473
|
+
camera = {
|
|
1474
|
+
position: {
|
|
1475
|
+
x: centerX + distance * 0.5,
|
|
1476
|
+
y: distance * 0.7,
|
|
1477
|
+
z: centerZ + distance * 0.5
|
|
1478
|
+
},
|
|
1479
|
+
target: { x: centerX, y: 0, z: centerZ },
|
|
1480
|
+
up: { x: 0, y: 1, z: 0 },
|
|
1481
|
+
fov: 50,
|
|
1482
|
+
near: 0.1,
|
|
1483
|
+
far: distance * 4
|
|
1484
|
+
};
|
|
1485
|
+
} else {
|
|
1486
|
+
camera = {
|
|
1487
|
+
position: { x: 30, y: 30, z: 25 },
|
|
1488
|
+
target: { x: 0, y: 0, z: 0 },
|
|
1489
|
+
up: { x: 0, y: 1, z: 0 },
|
|
1490
|
+
fov: 50,
|
|
1491
|
+
near: 0.1,
|
|
1492
|
+
far: 120
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1262
1496
|
const lights = [
|
|
1263
1497
|
{
|
|
1264
1498
|
type: "ambient",
|
|
@@ -1969,7 +2203,181 @@ var GLTFBuilder = class {
|
|
|
1969
2203
|
});
|
|
1970
2204
|
this.gltf.scenes[0].nodes.push(nodeIndex);
|
|
1971
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
|
+
}
|
|
1972
2376
|
async addBoxWithFaceMaterials(box, defaultMaterialIndex) {
|
|
2377
|
+
if (box.mesh) {
|
|
2378
|
+
await this.addMeshWithFaceTextures(box, defaultMaterialIndex);
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
1973
2381
|
const faceMeshes = createBoxMeshByFaces(box.size);
|
|
1974
2382
|
const faceMaterials = {};
|
|
1975
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.
|
|
5
|
+
"version": "0.0.18",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "bun test tests/",
|
|
8
8
|
"format": "biome format --write .",
|
|
@@ -27,15 +27,16 @@
|
|
|
27
27
|
"bun-match-svg": "^0.0.12",
|
|
28
28
|
"circuit-json": "^0.0.267",
|
|
29
29
|
"circuit-to-svg": "^0.0.175",
|
|
30
|
+
"graphics-debug": "^0.0.65",
|
|
30
31
|
"looks-same": "^9.0.1",
|
|
31
32
|
"poppygl": "^0.0.9",
|
|
32
33
|
"react": "^19.1.1",
|
|
33
34
|
"react-cosmos": "^7.0.0",
|
|
34
35
|
"react-cosmos-plugin-vite": "^7.0.0",
|
|
35
36
|
"react-dom": "^19.1.1",
|
|
37
|
+
"tscircuit": "^0.0.701",
|
|
36
38
|
"tsup": "^8.5.0",
|
|
37
|
-
"vite": "^7.1.1"
|
|
38
|
-
"tscircuit": "^0.0.701"
|
|
39
|
+
"vite": "^7.1.1"
|
|
39
40
|
},
|
|
40
41
|
"peerDependencies": {
|
|
41
42
|
"typescript": "^5",
|
|
@@ -52,5 +53,8 @@
|
|
|
52
53
|
"@resvg/resvg-js": {
|
|
53
54
|
"optional": true
|
|
54
55
|
}
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@jscad/modeling": "^2.12.6"
|
|
55
59
|
}
|
|
56
60
|
}
|