586 lines
19 KiB
JavaScript
586 lines
19 KiB
JavaScript
const NativeBuffer = globalThis.Buffer;
|
|
const textEncoder = typeof TextEncoder !== "undefined" ? new TextEncoder() : null;
|
|
|
|
function finiteNumber(value, fallback = 0) {
|
|
const numeric = Number(value);
|
|
return Number.isFinite(numeric) ? numeric : fallback;
|
|
}
|
|
|
|
function clamp01(value) {
|
|
return Math.min(Math.max(finiteNumber(value), 0), 1);
|
|
}
|
|
|
|
function bytesFromString(value, encoding = "utf-8") {
|
|
if (NativeBuffer?.from) {
|
|
return NativeBuffer.from(String(value), encoding);
|
|
}
|
|
if (encoding !== "utf-8" && encoding !== "utf8") {
|
|
const text = String(value);
|
|
const bytes = new Uint8Array(text.length);
|
|
for (let index = 0; index < text.length; index += 1) {
|
|
bytes[index] = text.charCodeAt(index) & 0xff;
|
|
}
|
|
return bytes;
|
|
}
|
|
return textEncoder.encode(String(value));
|
|
}
|
|
|
|
function allocBytes(length, fill = 0) {
|
|
if (NativeBuffer?.alloc) {
|
|
return NativeBuffer.alloc(length, fill);
|
|
}
|
|
const bytes = new Uint8Array(length);
|
|
if (fill) {
|
|
bytes.fill(fill);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
function concatBytes(parts, totalLength = undefined) {
|
|
if (NativeBuffer?.concat) {
|
|
return NativeBuffer.concat(parts, totalLength);
|
|
}
|
|
const length = totalLength ?? parts.reduce((sum, part) => sum + part.length, 0);
|
|
const bytes = new Uint8Array(length);
|
|
let offset = 0;
|
|
for (const part of parts) {
|
|
bytes.set(part, offset);
|
|
offset += part.length;
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
function typedArrayBytes(array) {
|
|
return new Uint8Array(array.buffer, array.byteOffset, array.byteLength);
|
|
}
|
|
|
|
function viewFor(bytes) {
|
|
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
}
|
|
|
|
function writeUInt16LE(bytes, offset, value) {
|
|
viewFor(bytes).setUint16(offset, value, true);
|
|
}
|
|
|
|
function writeUInt32LE(bytes, offset, value) {
|
|
viewFor(bytes).setUint32(offset, value, true);
|
|
}
|
|
|
|
function writeFloatLE(bytes, offset, value) {
|
|
viewFor(bytes).setFloat32(offset, value, true);
|
|
}
|
|
|
|
function writeAscii(bytes, offset, text, maxLength) {
|
|
const source = String(text).slice(0, maxLength);
|
|
for (let index = 0; index < source.length; index += 1) {
|
|
bytes[offset + index] = source.charCodeAt(index) & 0x7f;
|
|
}
|
|
}
|
|
|
|
function align4Buffer(buffer, fill = 0x20) {
|
|
const padding = (4 - (buffer.length % 4)) % 4;
|
|
return padding ? concatBytes([buffer, allocBytes(padding, fill)]) : buffer;
|
|
}
|
|
|
|
function hexToRgb01(hex, fallback = "#d4d4d8") {
|
|
const raw = String(hex || fallback).trim();
|
|
const value = /^#(?:[0-9a-fA-F]{3}){1,2}$/.test(raw) ? raw : fallback;
|
|
const expanded = value.length === 4
|
|
? `${value[1]}${value[1]}${value[2]}${value[2]}${value[3]}${value[3]}`
|
|
: value.slice(1);
|
|
return [
|
|
parseInt(expanded.slice(0, 2), 16) / 255,
|
|
parseInt(expanded.slice(2, 4), 16) / 255,
|
|
parseInt(expanded.slice(4, 6), 16) / 255,
|
|
];
|
|
}
|
|
|
|
function hexTo3mfDisplayColor(hex, fallback = "#d4d4d8") {
|
|
const toChannel = (component) => Math.round(clamp01(component) * 255)
|
|
.toString(16)
|
|
.padStart(2, "0")
|
|
.toUpperCase();
|
|
const rgb = hexToRgb01(hex, fallback);
|
|
return `#${toChannel(rgb[0])}${toChannel(rgb[1])}${toChannel(rgb[2])}FF`;
|
|
}
|
|
|
|
function triangleNormal(positions, offset) {
|
|
const ax = positions[offset];
|
|
const ay = positions[offset + 1];
|
|
const az = positions[offset + 2];
|
|
const bx = positions[offset + 3];
|
|
const by = positions[offset + 4];
|
|
const bz = positions[offset + 5];
|
|
const cx = positions[offset + 6];
|
|
const cy = positions[offset + 7];
|
|
const cz = positions[offset + 8];
|
|
const ux = bx - ax;
|
|
const uy = by - ay;
|
|
const uz = bz - az;
|
|
const vx = cx - ax;
|
|
const vy = cy - ay;
|
|
const vz = cz - az;
|
|
const nx = uy * vz - uz * vy;
|
|
const ny = uz * vx - ux * vz;
|
|
const nz = ux * vy - uy * vx;
|
|
const length = Math.hypot(nx, ny, nz);
|
|
return length > 1e-12 ? [nx / length, ny / length, nz / length] : [0, 0, 1];
|
|
}
|
|
|
|
function sanitizeName(value, fallback = "implicit-cad") {
|
|
return String(value || fallback).trim().replace(/[\x00-\x1f<>:"/\\|?*]+/g, "-") || fallback;
|
|
}
|
|
|
|
function boundsForPositions(positions) {
|
|
const min = [Infinity, Infinity, Infinity];
|
|
const max = [-Infinity, -Infinity, -Infinity];
|
|
for (let index = 0; index < positions.length; index += 3) {
|
|
min[0] = Math.min(min[0], positions[index]);
|
|
min[1] = Math.min(min[1], positions[index + 1]);
|
|
min[2] = Math.min(min[2], positions[index + 2]);
|
|
max[0] = Math.max(max[0], positions[index]);
|
|
max[1] = Math.max(max[1], positions[index + 1]);
|
|
max[2] = Math.max(max[2], positions[index + 2]);
|
|
}
|
|
return {
|
|
min: min.map((value) => Number.isFinite(value) ? value : 0),
|
|
max: max.map((value) => Number.isFinite(value) ? value : 0),
|
|
};
|
|
}
|
|
|
|
function buildGlb(gltf, binaryParts) {
|
|
const binaryChunk = align4Buffer(concatBytes(binaryParts), 0);
|
|
gltf.buffers = [{ byteLength: binaryChunk.length }];
|
|
const jsonChunk = align4Buffer(bytesFromString(JSON.stringify(gltf)), 0x20);
|
|
const totalLength = 12 + 8 + jsonChunk.length + 8 + binaryChunk.length;
|
|
const header = allocBytes(12);
|
|
writeUInt32LE(header, 0, 0x46546c67);
|
|
writeUInt32LE(header, 4, 2);
|
|
writeUInt32LE(header, 8, totalLength);
|
|
const jsonHeader = allocBytes(8);
|
|
writeUInt32LE(jsonHeader, 0, jsonChunk.length);
|
|
writeUInt32LE(jsonHeader, 4, 0x4e4f534a);
|
|
const binHeader = allocBytes(8);
|
|
writeUInt32LE(binHeader, 0, binaryChunk.length);
|
|
writeUInt32LE(binHeader, 4, 0x004e4942);
|
|
return concatBytes([header, jsonHeader, jsonChunk, binHeader, binaryChunk], totalLength);
|
|
}
|
|
|
|
export function meshToBinaryStl(mesh, { name = "implicit-cad" } = {}) {
|
|
const positions = mesh.positions || new Float32Array();
|
|
const triangleCount = Math.floor(positions.length / 9);
|
|
const buffer = allocBytes(84 + triangleCount * 50);
|
|
writeAscii(buffer, 0, `implicit-cad ${sanitizeName(name)}`, 80);
|
|
writeUInt32LE(buffer, 80, triangleCount);
|
|
let offset = 84;
|
|
for (let triangle = 0; triangle < triangleCount; triangle += 1) {
|
|
const positionOffset = triangle * 9;
|
|
const normal = triangleNormal(positions, positionOffset);
|
|
for (const component of normal) {
|
|
writeFloatLE(buffer, offset, component);
|
|
offset += 4;
|
|
}
|
|
for (let vertex = 0; vertex < 9; vertex += 1) {
|
|
writeFloatLE(buffer, offset, positions[positionOffset + vertex]);
|
|
offset += 4;
|
|
}
|
|
writeUInt16LE(buffer, offset, 0);
|
|
offset += 2;
|
|
}
|
|
return buffer;
|
|
}
|
|
|
|
export function meshToGlb(mesh, { name = "Implicit CAD", color = "#d4d4d8" } = {}) {
|
|
const positions = mesh.positions || new Float32Array();
|
|
const normals = mesh.normals && mesh.normals.length === positions.length
|
|
? mesh.normals
|
|
: new Float32Array(positions.length);
|
|
const vertexCount = Math.floor(positions.length / 3);
|
|
const positionBuffer = typedArrayBytes(positions);
|
|
const normalBuffer = typedArrayBytes(normals);
|
|
const positionBounds = boundsForPositions(positions);
|
|
const baseColor = hexToRgb01(color);
|
|
const gltf = {
|
|
asset: { version: "2.0", generator: "implicitjs exporter" },
|
|
scene: 0,
|
|
scenes: [{ nodes: [0] }],
|
|
nodes: [{
|
|
mesh: 0,
|
|
name: sanitizeName(name, "Implicit CAD"),
|
|
extras: {
|
|
cadOccurrenceId: "implicit-cad:0",
|
|
cadSourceKind: "implicit-cad",
|
|
cadUnits: "mm",
|
|
},
|
|
}],
|
|
meshes: [{
|
|
primitives: [{
|
|
attributes: { POSITION: 0, NORMAL: 1 },
|
|
material: 0,
|
|
mode: 4,
|
|
}],
|
|
}],
|
|
materials: [{
|
|
name: "Implicit material",
|
|
doubleSided: true,
|
|
extras: { cadSourceColor: true },
|
|
pbrMetallicRoughness: {
|
|
baseColorFactor: [...baseColor.map(clamp01), 1],
|
|
roughnessFactor: 0.72,
|
|
metallicFactor: 0.02,
|
|
},
|
|
}],
|
|
bufferViews: [
|
|
{ buffer: 0, byteOffset: 0, byteLength: positionBuffer.length, target: 34962 },
|
|
{ buffer: 0, byteOffset: positionBuffer.length, byteLength: normalBuffer.length, target: 34962 },
|
|
],
|
|
accessors: [
|
|
{
|
|
bufferView: 0,
|
|
byteOffset: 0,
|
|
componentType: 5126,
|
|
count: vertexCount,
|
|
type: "VEC3",
|
|
min: positionBounds.min,
|
|
max: positionBounds.max,
|
|
},
|
|
{
|
|
bufferView: 1,
|
|
byteOffset: 0,
|
|
componentType: 5126,
|
|
count: vertexCount,
|
|
type: "VEC3",
|
|
},
|
|
],
|
|
};
|
|
return buildGlb(gltf, [positionBuffer, normalBuffer]);
|
|
}
|
|
|
|
export function meshToAnimatedGlb(mesh, {
|
|
name = "Implicit CAD",
|
|
color = "#d4d4d8",
|
|
duration = 4,
|
|
times = [],
|
|
targetPositions = [],
|
|
} = {}) {
|
|
const positions = mesh.positions || new Float32Array();
|
|
const normals = mesh.normals && mesh.normals.length === positions.length
|
|
? mesh.normals
|
|
: new Float32Array(positions.length);
|
|
const vertexCount = Math.floor(positions.length / 3);
|
|
const positionBuffer = typedArrayBytes(positions);
|
|
const normalBuffer = typedArrayBytes(normals);
|
|
const positionBounds = boundsForPositions(positions);
|
|
const cleanTargets = targetPositions
|
|
.filter((target) => target && target.length === positions.length)
|
|
.map((target) => {
|
|
const delta = new Float32Array(positions.length);
|
|
for (let index = 0; index < positions.length; index += 1) {
|
|
delta[index] = finiteNumber(target[index], positions[index]) - positions[index];
|
|
}
|
|
return delta;
|
|
});
|
|
if (!cleanTargets.length) {
|
|
throw new Error("Animated GLB export needs at least one compatible animation frame.");
|
|
}
|
|
|
|
const keyTimes = Array.isArray(times) && times.length >= 2
|
|
? times.map((value) => Math.max(finiteNumber(value, 0), 0))
|
|
: [0, Math.max(finiteNumber(duration, 4), 0.001)];
|
|
const targetCount = cleanTargets.length;
|
|
const weightValues = new Float32Array(keyTimes.length * targetCount);
|
|
for (let key = 0; key < keyTimes.length; key += 1) {
|
|
const targetIndex = key - 1;
|
|
if (targetIndex >= 0 && targetIndex < targetCount) {
|
|
weightValues[key * targetCount + targetIndex] = 1;
|
|
}
|
|
}
|
|
const targetBuffers = cleanTargets.map((target) => typedArrayBytes(target));
|
|
const timeValues = new Float32Array(keyTimes);
|
|
const timeBuffer = typedArrayBytes(timeValues);
|
|
const weightBuffer = typedArrayBytes(weightValues);
|
|
const offsets = [];
|
|
let byteOffset = 0;
|
|
for (const part of [positionBuffer, normalBuffer, ...targetBuffers, timeBuffer, weightBuffer]) {
|
|
offsets.push(byteOffset);
|
|
byteOffset += part.length;
|
|
}
|
|
const baseColor = hexToRgb01(color);
|
|
const targetAccessorStart = 2;
|
|
const timeAccessor = targetAccessorStart + targetCount;
|
|
const weightAccessor = timeAccessor + 1;
|
|
const gltf = {
|
|
asset: { version: "2.0", generator: "implicitjs animated exporter" },
|
|
scene: 0,
|
|
scenes: [{ nodes: [0] }],
|
|
nodes: [{
|
|
mesh: 0,
|
|
name: sanitizeName(name, "Implicit CAD"),
|
|
weights: Array.from({ length: targetCount }, () => 0),
|
|
extras: {
|
|
cadOccurrenceId: "implicit-cad:0",
|
|
cadSourceKind: "implicit-cad",
|
|
cadUnits: "mm",
|
|
implicitjsAnimated: true,
|
|
},
|
|
}],
|
|
meshes: [{
|
|
weights: Array.from({ length: targetCount }, () => 0),
|
|
primitives: [{
|
|
attributes: { POSITION: 0, NORMAL: 1 },
|
|
targets: cleanTargets.map((_, index) => ({ POSITION: targetAccessorStart + index })),
|
|
material: 0,
|
|
mode: 4,
|
|
}],
|
|
}],
|
|
materials: [{
|
|
name: "Implicit material",
|
|
doubleSided: true,
|
|
extras: { cadSourceColor: true },
|
|
pbrMetallicRoughness: {
|
|
baseColorFactor: [...baseColor.map(clamp01), 1],
|
|
roughnessFactor: 0.72,
|
|
metallicFactor: 0.02,
|
|
},
|
|
}],
|
|
animations: [{
|
|
name: "Implicit parameter animation",
|
|
channels: [{ sampler: 0, target: { node: 0, path: "weights" } }],
|
|
samplers: [{ input: timeAccessor, output: weightAccessor, interpolation: "LINEAR" }],
|
|
}],
|
|
bufferViews: [
|
|
{ buffer: 0, byteOffset: offsets[0], byteLength: positionBuffer.length, target: 34962 },
|
|
{ buffer: 0, byteOffset: offsets[1], byteLength: normalBuffer.length, target: 34962 },
|
|
...targetBuffers.map((target, index) => ({
|
|
buffer: 0,
|
|
byteOffset: offsets[2 + index],
|
|
byteLength: target.length,
|
|
target: 34962,
|
|
})),
|
|
{ buffer: 0, byteOffset: offsets[2 + targetCount], byteLength: timeBuffer.length },
|
|
{ buffer: 0, byteOffset: offsets[3 + targetCount], byteLength: weightBuffer.length },
|
|
],
|
|
accessors: [
|
|
{
|
|
bufferView: 0,
|
|
byteOffset: 0,
|
|
componentType: 5126,
|
|
count: vertexCount,
|
|
type: "VEC3",
|
|
min: positionBounds.min,
|
|
max: positionBounds.max,
|
|
},
|
|
{
|
|
bufferView: 1,
|
|
byteOffset: 0,
|
|
componentType: 5126,
|
|
count: vertexCount,
|
|
type: "VEC3",
|
|
},
|
|
...cleanTargets.map((target, index) => {
|
|
const bounds = boundsForPositions(target);
|
|
return {
|
|
bufferView: targetAccessorStart + index,
|
|
byteOffset: 0,
|
|
componentType: 5126,
|
|
count: vertexCount,
|
|
type: "VEC3",
|
|
min: bounds.min,
|
|
max: bounds.max,
|
|
};
|
|
}),
|
|
{
|
|
bufferView: timeAccessor,
|
|
byteOffset: 0,
|
|
componentType: 5126,
|
|
count: timeValues.length,
|
|
type: "SCALAR",
|
|
min: [Math.min(...keyTimes)],
|
|
max: [Math.max(...keyTimes)],
|
|
},
|
|
{
|
|
bufferView: weightAccessor,
|
|
byteOffset: 0,
|
|
componentType: 5126,
|
|
count: weightValues.length,
|
|
type: "SCALAR",
|
|
min: [0],
|
|
max: [1],
|
|
},
|
|
],
|
|
};
|
|
return buildGlb(gltf, [positionBuffer, normalBuffer, ...targetBuffers, timeBuffer, weightBuffer]);
|
|
}
|
|
|
|
function xmlEscape(value) {
|
|
return String(value ?? "")
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
}
|
|
|
|
function crc32(buffer) {
|
|
let table = crc32.table;
|
|
if (!table) {
|
|
table = new Uint32Array(256);
|
|
for (let index = 0; index < 256; index += 1) {
|
|
let c = index;
|
|
for (let bit = 0; bit < 8; bit += 1) {
|
|
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
}
|
|
table[index] = c >>> 0;
|
|
}
|
|
crc32.table = table;
|
|
}
|
|
let crc = 0xffffffff;
|
|
for (const byte of buffer) {
|
|
crc = table[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
}
|
|
return (crc ^ 0xffffffff) >>> 0;
|
|
}
|
|
|
|
function dosDateTime(date = new Date()) {
|
|
const year = Math.max(date.getFullYear(), 1980);
|
|
const dosTime = (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2);
|
|
const dosDate = ((year - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate();
|
|
return { dosTime, dosDate };
|
|
}
|
|
|
|
function zipStore(files) {
|
|
const localParts = [];
|
|
const centralParts = [];
|
|
let offset = 0;
|
|
const { dosTime, dosDate } = dosDateTime();
|
|
for (const file of files) {
|
|
const nameBuffer = bytesFromString(file.name);
|
|
const body = file.body instanceof Uint8Array ? file.body : bytesFromString(String(file.body || ""));
|
|
const crc = crc32(body);
|
|
const local = allocBytes(30);
|
|
writeUInt32LE(local, 0, 0x04034b50);
|
|
writeUInt16LE(local, 4, 20);
|
|
writeUInt16LE(local, 6, 0);
|
|
writeUInt16LE(local, 8, 0);
|
|
writeUInt16LE(local, 10, dosTime);
|
|
writeUInt16LE(local, 12, dosDate);
|
|
writeUInt32LE(local, 14, crc);
|
|
writeUInt32LE(local, 18, body.length);
|
|
writeUInt32LE(local, 22, body.length);
|
|
writeUInt16LE(local, 26, nameBuffer.length);
|
|
writeUInt16LE(local, 28, 0);
|
|
localParts.push(local, nameBuffer, body);
|
|
|
|
const central = allocBytes(46);
|
|
writeUInt32LE(central, 0, 0x02014b50);
|
|
writeUInt16LE(central, 4, 20);
|
|
writeUInt16LE(central, 6, 20);
|
|
writeUInt16LE(central, 8, 0);
|
|
writeUInt16LE(central, 10, 0);
|
|
writeUInt16LE(central, 12, dosTime);
|
|
writeUInt16LE(central, 14, dosDate);
|
|
writeUInt32LE(central, 16, crc);
|
|
writeUInt32LE(central, 20, body.length);
|
|
writeUInt32LE(central, 24, body.length);
|
|
writeUInt16LE(central, 28, nameBuffer.length);
|
|
writeUInt16LE(central, 30, 0);
|
|
writeUInt16LE(central, 32, 0);
|
|
writeUInt16LE(central, 34, 0);
|
|
writeUInt16LE(central, 36, 0);
|
|
writeUInt32LE(central, 38, 0);
|
|
writeUInt32LE(central, 42, offset);
|
|
centralParts.push(central, nameBuffer);
|
|
offset += local.length + nameBuffer.length + body.length;
|
|
}
|
|
const centralStart = offset;
|
|
const centralDirectory = concatBytes(centralParts);
|
|
const end = allocBytes(22);
|
|
writeUInt32LE(end, 0, 0x06054b50);
|
|
writeUInt16LE(end, 4, 0);
|
|
writeUInt16LE(end, 6, 0);
|
|
writeUInt16LE(end, 8, files.length);
|
|
writeUInt16LE(end, 10, files.length);
|
|
writeUInt32LE(end, 12, centralDirectory.length);
|
|
writeUInt32LE(end, 16, centralStart);
|
|
writeUInt16LE(end, 20, 0);
|
|
return concatBytes([...localParts, centralDirectory, end]);
|
|
}
|
|
|
|
export function meshTo3mf(mesh, { name = "Implicit CAD", color = "#d4d4d8" } = {}) {
|
|
const positions = mesh.positions || new Float32Array();
|
|
const vertexCount = Math.floor(positions.length / 3);
|
|
const triangleCount = Math.floor(positions.length / 9);
|
|
const displayColor = hexTo3mfDisplayColor(color);
|
|
const vertices = [];
|
|
for (let index = 0; index < vertexCount; index += 1) {
|
|
const offset = index * 3;
|
|
vertices.push(`<vertex x="${positions[offset]}" y="${positions[offset + 1]}" z="${positions[offset + 2]}"/>`);
|
|
}
|
|
const triangles = [];
|
|
for (let index = 0; index < triangleCount; index += 1) {
|
|
const base = index * 3;
|
|
triangles.push(`<triangle v1="${base}" v2="${base + 1}" v3="${base + 2}" pid="2" p1="0"/>`);
|
|
}
|
|
const modelXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<model unit="millimeter" xml:lang="en-US" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
|
|
<metadata name="Title">${xmlEscape(name)}</metadata>
|
|
<resources>
|
|
<basematerials id="2">
|
|
<base name="Implicit material" displaycolor="${displayColor}"/>
|
|
</basematerials>
|
|
<object id="1" type="model" name="${xmlEscape(name)}">
|
|
<mesh>
|
|
<vertices>
|
|
${vertices.join("\n ")}
|
|
</vertices>
|
|
<triangles>
|
|
${triangles.join("\n ")}
|
|
</triangles>
|
|
</mesh>
|
|
</object>
|
|
</resources>
|
|
<build>
|
|
<item objectid="1"/>
|
|
</build>
|
|
</model>`;
|
|
const contentTypes = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
|
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
|
<Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/>
|
|
</Types>`;
|
|
const relationships = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
<Relationship Target="/3D/3dmodel.model" Id="rel0" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/>
|
|
</Relationships>`;
|
|
return zipStore([
|
|
{ name: "[Content_Types].xml", body: contentTypes },
|
|
{ name: "_rels/.rels", body: relationships },
|
|
{ name: "3D/3dmodel.model", body: modelXml },
|
|
]);
|
|
}
|
|
|
|
export function meshToFormat(mesh, format, options = {}) {
|
|
const normalized = String(format || "").trim().toLowerCase().replace(/^\./, "");
|
|
if (normalized === "stl") {
|
|
return {
|
|
body: meshToBinaryStl(mesh, options),
|
|
contentType: "model/stl",
|
|
extension: ".stl",
|
|
};
|
|
}
|
|
if (normalized === "glb" || normalized === "gltf") {
|
|
return {
|
|
body: meshToGlb(mesh, options),
|
|
contentType: "model/gltf-binary",
|
|
extension: ".glb",
|
|
};
|
|
}
|
|
if (normalized === "3mf") {
|
|
return {
|
|
body: meshTo3mf(mesh, options),
|
|
contentType: "model/3mf",
|
|
extension: ".3mf",
|
|
};
|
|
}
|
|
throw new Error(`Unsupported implicit CAD export format: ${format || "(missing)"}`);
|
|
}
|