mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-18 20:59:55 -05:00
109 lines
4.5 KiB
TypeScript
109 lines
4.5 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import zlib from "node:zlib";
|
|
import { strToU8, zipSync } from "fflate";
|
|
import { CUBE_STL } from "./helpers";
|
|
|
|
/** Upload fixtures, built in code so every variation stays readable next to the test using it. */
|
|
|
|
export const stl = (): Uint8Array => fs.readFileSync(CUBE_STL);
|
|
|
|
const CRC_TABLE = Array.from({ length: 256 }, (_, n) => {
|
|
let c = n;
|
|
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
return c >>> 0;
|
|
});
|
|
|
|
function crc32(buf: Buffer): number {
|
|
let c = 0xffffffff;
|
|
for (const byte of buf) c = CRC_TABLE[(c ^ byte) & 0xff] ^ (c >>> 8);
|
|
return (c ^ 0xffffffff) >>> 0;
|
|
}
|
|
|
|
function pngChunk(type: string, data: Buffer): Buffer {
|
|
const len = Buffer.alloc(4);
|
|
len.writeUInt32BE(data.length);
|
|
const body = Buffer.concat([Buffer.from(type, "ascii"), data]);
|
|
const crc = Buffer.alloc(4);
|
|
crc.writeUInt32BE(crc32(body));
|
|
return Buffer.concat([len, body, crc]);
|
|
}
|
|
|
|
/** A solid-colour RGB PNG; real enough for the server to decode and thumbnail. */
|
|
export function png([r, g, b]: [number, number, number] = [200, 80, 40], size = 64): Uint8Array {
|
|
const ihdr = Buffer.alloc(13);
|
|
ihdr.writeUInt32BE(size, 0);
|
|
ihdr.writeUInt32BE(size, 4);
|
|
ihdr[8] = 8; // bit depth
|
|
ihdr[9] = 2; // RGB
|
|
const row = Buffer.concat([Buffer.from([0]), Buffer.from(Array.from({ length: size }, () => [r, g, b]).flat())]);
|
|
const raw = Buffer.concat(Array.from({ length: size }, () => row));
|
|
return Buffer.concat([
|
|
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
pngChunk("IHDR", ihdr),
|
|
pngChunk("IDAT", zlib.deflateSync(raw)),
|
|
pngChunk("IEND", Buffer.alloc(0)),
|
|
]);
|
|
}
|
|
|
|
const CUBE_MODEL = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<model unit="millimeter" xml:lang="en-US" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
|
|
<resources>
|
|
<object id="1" type="model">
|
|
<mesh>
|
|
<vertices>
|
|
<vertex x="0" y="0" z="0"/><vertex x="10" y="0" z="0"/><vertex x="10" y="10" z="0"/><vertex x="0" y="10" z="0"/>
|
|
<vertex x="0" y="0" z="10"/><vertex x="10" y="0" z="10"/><vertex x="10" y="10" z="10"/><vertex x="0" y="10" z="10"/>
|
|
</vertices>
|
|
<triangles>
|
|
<triangle v1="0" v2="2" v3="1"/><triangle v1="0" v2="3" v3="2"/><triangle v1="4" v2="5" v3="6"/><triangle v1="4" v2="6" v3="7"/>
|
|
<triangle v1="0" v2="1" v3="5"/><triangle v1="0" v2="5" v3="4"/><triangle v1="1" v2="2" v3="6"/><triangle v1="1" v2="6" v3="5"/>
|
|
<triangle v1="2" v2="3" v3="7"/><triangle v1="2" v2="7" v3="6"/><triangle v1="3" v2="0" v3="4"/><triangle v1="3" v2="4" v3="7"/>
|
|
</triangles>
|
|
</mesh>
|
|
</object>
|
|
</resources>
|
|
<build><item objectid="1"/></build>
|
|
</model>`;
|
|
|
|
/** A 10mm cube as a 3MF package; with `thumbnail`, it carries the embedded preview slicers write. */
|
|
export function threeMf({ thumbnail = true } = {}): Uint8Array {
|
|
const files: Record<string, Uint8Array> = {
|
|
"[Content_Types].xml": strToU8(
|
|
`<?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"/>` +
|
|
`<Default Extension="png" ContentType="image/png"/></Types>`,
|
|
),
|
|
"_rels/.rels": strToU8(
|
|
`<?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>`,
|
|
),
|
|
"3D/3dmodel.model": strToU8(CUBE_MODEL),
|
|
};
|
|
if (thumbnail) files["Metadata/thumbnail.png"] = png([40, 120, 220]);
|
|
return zipSync(files);
|
|
}
|
|
|
|
export const zip = (entries: Record<string, Uint8Array>): Uint8Array => zipSync(entries);
|
|
|
|
export const text = (s: string): Uint8Array => strToU8(s);
|
|
|
|
/** Writes `files` (relative path -> content) under `dir` and returns `dir`, ready for a folder pick. */
|
|
export function writeTree(dir: string, files: Record<string, Uint8Array>): string {
|
|
for (const [rel, content] of Object.entries(files)) {
|
|
const abs = path.join(dir, rel);
|
|
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
fs.writeFileSync(abs, content);
|
|
}
|
|
return dir;
|
|
}
|
|
|
|
/** Shape `setFiles` takes for files that never touch disk. */
|
|
export const file = (name: string, buffer: Uint8Array, mimeType = "application/octet-stream") => ({
|
|
name,
|
|
mimeType,
|
|
buffer: Buffer.from(buffer),
|
|
});
|