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 +5 -2
- package/dist/index.js +288 -6
- package/package.json +2 -2
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
|
-
|
|
642
|
-
|
|
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
|
-
|
|
659
|
-
|
|
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.
|
|
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.
|
|
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",
|