grid-apps-cmms/src/kiri/mode/sla/work/x_goo.js
2026-07-26 13:52:55 -04:00

595 lines
20 KiB
JavaScript

/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { imageToRowMajor, renderRasterLayers, round } from './raster.js';
import { createLayerMetadata, createProcessMetadata, estimatePrintTime, motionFor } from './meta.js';
const FAMILY = "GOO";
const FILE_VERSION = "V3.0";
const FILE_MAGIC = [0x07, 0x00, 0x00, 0x00, 0x44, 0x4c, 0x50, 0x00];
const DELIMITER = [0x0d, 0x0a];
const LAYER_MAGIC = 0x55;
const HEADER_SIZE = 195477;
const LAYER_DEF_SIZE = 70;
const FOOTER_SIZE = 11;
const MAX_RUN = 0x0fffffff;
const FORMAT_RECORDS = {
goo: { ext: "goo", machine: "Elegoo GOO", notes: "Elegoo GOO" },
prz: { ext: "prz", machine: "Phrozen Sonic Mini 8K S", notes: "Phrozen PRZ GOO variant" }
};
function encode(print, progress, renderer) {
let format = print.settings.device.slaFormat || "goo";
let rec = FORMAT_RECORDS[format];
if (!rec) {
return Promise.reject(new Error(`${FAMILY} format .${format} is not implemented.`));
}
if (renderer) {
return Promise.resolve(encodeGOO(print, progress, renderer, rec));
}
return import('./x_photon.js').then(({ photon }) =>
encodeGOO(print, progress, photon, rec));
}
function encodeGOO(print, progress, photon, rec) {
let layers = [];
let previewSmall = createPreview(116, 116);
let previewLarge = createPreview(290, 290);
let { ctx, volume } = renderRasterLayers(print, photon, params => {
let { index, rendered, z, ctx } = params;
let { device, process, width, height } = ctx;
let layerMeta = createLayerMetadata(device, process, index, z, rendered.area);
let raster = imageToRowMajor(rendered.image, width, height, normalizePixel);
accumulatePreview(previewSmall, rendered.image, width, height);
accumulatePreview(previewLarge, rendered.image, width, height);
layers.push({
index,
data: encodeRLE(raster),
z: layerMeta.z,
pausePositionZ: ctx.device.maxHeight || ctx.device.bedHeight || 0,
exposure: layerMeta.exposure,
lightOffDelay: layerMeta.lightOffDelay,
waitTimeAfterCure: layerMeta.waitAfterCure,
waitTimeAfterLift: layerMeta.waitAfterLift,
waitTimeBeforeCure: layerMeta.waitBeforeCure,
liftHeight: layerMeta.liftHeight,
liftSpeed: layerMeta.liftSpeed,
liftHeight2: 0,
liftSpeed2: 0,
retractHeight: layerMeta.retractHeight,
retractSpeed: layerMeta.retractSpeed,
retractHeight2: 0,
retractSpeed2: 0,
lightPWM: layerMeta.lightPWM
});
}, progress ? (value) => progress(value * 0.85, `${rec.ext}_encode`) : null);
let file = writeGOO({ ctx, layers, volume, previewSmall, previewLarge, rec });
if (progress) progress(1, `${rec.ext}_write`);
return { file, layers: layers.length, volume };
}
function writeGOO(params) {
let { ctx, layers, volume, previewSmall, previewLarge, rec } = params;
let total = HEADER_SIZE + FOOTER_SIZE;
layers.forEach(layer => {
layer.dataLength = layer.data.length;
total += LAYER_DEF_SIZE + layer.dataLength + DELIMITER.length;
});
let writer = new BinaryWriter(new ArrayBuffer(total));
writeHeader(writer, { ctx, layers, volume, previewSmall, previewLarge, rec });
writer.seek(HEADER_SIZE);
layers.forEach(layer => writeLayer(writer, layer));
writeFooter(writer);
if (writer.pos !== total) {
throw new Error(`GOO writer size mismatch (${writer.pos} !== ${total})`);
}
return writer.buffer;
}
function writeHeader(writer, params) {
let { ctx, layers, volume, previewSmall, previewLarge, rec } = params;
let { device, process, width, height } = ctx;
let meta = createProcessMetadata(device, process);
let bottomMotion = motionFor(device, process, true);
let layerMotionValue = motionFor(device, process, false);
writer.writeString(FILE_VERSION, 4);
writer.writeBytes(FILE_MAGIC);
writer.writeString("Grid.Space", 32);
writer.writeString("1.0", 24);
writer.writeString(formatDate(), 24);
writer.writeString(device.slaMachineName || device.deviceName || rec.machine, 32);
writer.writeString("DLP", 32);
writer.writeString("Grid.Space", 32);
writer.writeU16(process.slaAntiAlias || 1);
writer.writeU16(1);
writer.writeU16(0);
writer.writeBytes(previewSmall.image);
writer.writeBytes(DELIMITER);
writer.writeBytes(previewLarge.image);
writer.writeBytes(DELIMITER);
writer.writeU32(layers.length);
writer.writeU16(width);
writer.writeU16(height);
writer.writeU8(0);
writer.writeU8(0);
writer.writeF32(device.bedWidth);
writer.writeF32(device.bedDepth);
writer.writeF32(device.maxHeight || device.bedHeight || 0);
writer.writeF32(process.slaSlice);
writer.writeF32(meta.exposure);
writer.writeU8(1);
writer.writeF32(0);
writer.writeF32(0);
writer.writeF32(0);
writer.writeF32(meta.bottomWaitBeforeCure);
writer.writeF32(0);
writer.writeF32(0);
writer.writeF32(meta.waitBeforeCure);
writer.writeF32(meta.bottomExposure);
writer.writeU32(meta.bottomLayers);
writer.writeF32(bottomMotion.liftHeight);
writer.writeF32(bottomMotion.liftSpeed);
writer.writeF32(layerMotionValue.liftHeight);
writer.writeF32(layerMotionValue.liftSpeed);
writer.writeF32(bottomMotion.retractHeight);
writer.writeF32(bottomMotion.retractSpeed);
writer.writeF32(layerMotionValue.retractHeight);
writer.writeF32(layerMotionValue.retractSpeed);
for (let i=0; i<8; i++) writer.writeF32(0);
writer.writeU16(meta.bottomLightPWM);
writer.writeU16(meta.lightPWM);
writer.writeU8(0);
writer.writeU32(estimatePrintTime(device, process, layers.length));
writer.writeF32(round(volume));
writer.writeF32(0);
writer.writeF32(0);
writer.writeString("$", 8);
writer.writeU32(HEADER_SIZE);
writer.writeU8(1);
writer.writeU16(0);
if (writer.pos !== HEADER_SIZE) {
throw new Error(`GOO header size mismatch (${writer.pos} !== ${HEADER_SIZE})`);
}
}
function writeLayer(writer, layer) {
writer.writeU16(0);
writer.writeF32(layer.pausePositionZ);
writer.writeF32(layer.z);
writer.writeF32(layer.exposure);
writer.writeF32(layer.lightOffDelay);
writer.writeF32(layer.waitTimeAfterCure);
writer.writeF32(layer.waitTimeAfterLift);
writer.writeF32(layer.waitTimeBeforeCure);
writer.writeF32(layer.liftHeight);
writer.writeF32(layer.liftSpeed);
writer.writeF32(layer.liftHeight2);
writer.writeF32(layer.liftSpeed2);
writer.writeF32(layer.retractHeight);
writer.writeF32(layer.retractSpeed);
writer.writeF32(layer.retractHeight2);
writer.writeF32(layer.retractSpeed2);
writer.writeU16(layer.lightPWM);
writer.writeBytes(DELIMITER);
writer.writeU32(layer.dataLength);
writer.writeBytes(layer.data);
writer.writeBytes(DELIMITER);
}
function writeFooter(writer) {
writer.writeU8(0);
writer.writeU8(0);
writer.writeU8(0);
writer.writeBytes(FILE_MAGIC);
}
function read(input) {
let view = toDataView(input);
let header = readHeader(view);
let layers = readLayers(view, header.layerDefAddress, header.layerCount);
let footerOffset = layers.length
? layers[layers.length - 1].dataOffset + layers[layers.length - 1].dataLength + DELIMITER.length
: header.layerDefAddress;
let footer = {
offset: footerOffset,
magic: Array.from(new Uint8Array(view.buffer, view.byteOffset + footerOffset + 3, 8))
};
if (!sameBytes(footer.magic, FILE_MAGIC)) {
throw new Error("invalid GOO footer magic");
}
return { header, layers, footer };
}
function readHeader(view) {
let pos = 0;
let header = {};
header.version = readString(view, pos, 4); pos += 4;
header.magic = readBytes(view, pos, 8); pos += 8;
header.softwareName = readString(view, pos, 32); pos += 32;
header.softwareVersion = readString(view, pos, 24); pos += 24;
header.fileCreateTime = readString(view, pos, 24); pos += 24;
header.machineName = readString(view, pos, 32); pos += 32;
header.machineType = readString(view, pos, 32); pos += 32;
header.profileName = readString(view, pos, 32); pos += 32;
header.antiAliasingLevel = view.getUint16(pos, false); pos += 2;
header.greyLevel = view.getUint16(pos, false); pos += 2;
header.blurLevel = view.getUint16(pos, false); pos += 2;
pos += 116 * 116 * 2 + 2 + 290 * 290 * 2 + 2;
header.layerCount = view.getUint32(pos, false); pos += 4;
header.resolutionX = view.getUint16(pos, false); pos += 2;
header.resolutionY = view.getUint16(pos, false); pos += 2;
header.mirrorX = view.getUint8(pos++) !== 0;
header.mirrorY = view.getUint8(pos++) !== 0;
header.displayWidth = view.getFloat32(pos, false); pos += 4;
header.displayHeight = view.getFloat32(pos, false); pos += 4;
header.machineZ = view.getFloat32(pos, false); pos += 4;
header.layerHeight = view.getFloat32(pos, false); pos += 4;
header.exposureTime = view.getFloat32(pos, false); pos += 4;
header.delayMode = view.getUint8(pos++);
header.lightOffDelay = view.getFloat32(pos, false); pos += 4;
pos += 6 * 4;
header.bottomExposureTime = view.getFloat32(pos, false); pos += 4;
header.bottomLayerCount = view.getUint32(pos, false); pos += 4;
pos += 16 * 4 + 2 + 2 + 1;
header.printTime = view.getUint32(pos, false); pos += 4;
header.volume = view.getFloat32(pos, false); pos += 4;
header.materialGrams = view.getFloat32(pos, false); pos += 4;
header.materialCost = view.getFloat32(pos, false); pos += 4;
header.priceCurrencySymbol = readString(view, pos, 8); pos += 8;
header.layerDefAddress = view.getUint32(pos, false); pos += 4;
header.grayScaleLevel = view.getUint8(pos++);
header.transitionLayerCount = view.getUint16(pos, false); pos += 2;
if (header.version !== FILE_VERSION) {
throw new Error(`invalid GOO version ${header.version}`);
}
if (!sameBytes(header.magic, FILE_MAGIC)) {
throw new Error("invalid GOO magic");
}
return header;
}
function readLayers(view, offset, count) {
let layers = [];
let pos = offset;
for (let index=0; index<count; index++) {
let layer = { index };
layer.pause = view.getUint16(pos, false); pos += 2;
layer.pausePositionZ = view.getFloat32(pos, false); pos += 4;
layer.z = view.getFloat32(pos, false); pos += 4;
layer.exposure = view.getFloat32(pos, false); pos += 4;
layer.lightOffDelay = view.getFloat32(pos, false); pos += 4;
layer.waitTimeAfterCure = view.getFloat32(pos, false); pos += 4;
layer.waitTimeAfterLift = view.getFloat32(pos, false); pos += 4;
layer.waitTimeBeforeCure = view.getFloat32(pos, false); pos += 4;
layer.liftHeight = view.getFloat32(pos, false); pos += 4;
layer.liftSpeed = view.getFloat32(pos, false); pos += 4;
layer.liftHeight2 = view.getFloat32(pos, false); pos += 4;
layer.liftSpeed2 = view.getFloat32(pos, false); pos += 4;
layer.retractHeight = view.getFloat32(pos, false); pos += 4;
layer.retractSpeed = view.getFloat32(pos, false); pos += 4;
layer.retractHeight2 = view.getFloat32(pos, false); pos += 4;
layer.retractSpeed2 = view.getFloat32(pos, false); pos += 4;
layer.lightPWM = view.getUint16(pos, false); pos += 2;
layer.delimiter = readBytes(view, pos, 2); pos += 2;
layer.dataLength = view.getUint32(pos, false); pos += 4;
layer.dataOffset = pos;
pos += layer.dataLength + DELIMITER.length;
layers.push(layer);
}
return layers;
}
function decodeLayer(view, layer, pixelCount) {
let data = new Uint8Array(
view.buffer,
view.byteOffset + layer.dataOffset,
layer.dataLength
);
return decodeRLE(data, pixelCount);
}
function encodeRLE(input) {
let output = [LAYER_MAGIC];
let previous = 0;
let current = input[0] || 0;
let run = 0;
for (let i=0; i<input.length; i++) {
let value = input[i] || 0;
if (value === current && run < MAX_RUN) {
run++;
} else {
writeRun(output, current, previous, run);
previous = current;
current = value;
run = 1;
}
}
writeRun(output, current, previous, run);
output.push(checksum(output));
return Uint8Array.from(output);
}
function writeRun(output, color, previous, run) {
while (run > 0) {
let stride = Math.min(run, MAX_RUN);
let diff = Math.abs(color - previous);
if (color > 0 && color < 255 && diff <= 0x0f && stride <= 0xff) {
let first = 0x80 | (diff & 0x0f);
if (stride > 1) {
first |= 0x10;
}
if (color < previous) {
first |= 0x20;
}
output.push(first);
if (stride > 1) {
output.push(stride);
}
run -= stride;
continue;
}
let lengthCode = stride <= 0x0f ? 0
: stride <= 0x0fff ? 1
: stride <= 0x0fffff ? 2
: 3;
let type = color === 0 ? 0 : color === 255 ? 3 : 1;
let first = (type << 6) | (lengthCode << 4) | (stride & 0x0f);
let ext = [];
if (lengthCode >= 1) ext.unshift((stride >> 4) & 0xff);
if (lengthCode >= 2) ext.unshift((stride >> 12) & 0xff);
if (lengthCode >= 3) ext.unshift((stride >> 20) & 0xff);
output.push(first);
if (type === 1) {
output.push(color);
}
output.push(...ext);
run -= stride;
}
}
function normalizePixel(value) {
value = value || 0;
if (value === 0 || value === 255) {
return value;
}
// The JS fallback rasterizer paints red at 200, so rescale that range to
// retain binary cure strength while preserving antialiased edge values.
return Math.min(255, Math.round(value * 255 / 200));
}
function decodeRLE(input, pixelCount) {
let rle = input instanceof Uint8Array ? input : new Uint8Array(input);
if (rle[0] !== LAYER_MAGIC) {
throw new Error(`invalid GOO RLE layer magic ${rle[0]}`);
}
if (rle[rle.length - 1] !== checksum(rle.subarray(0, rle.length - 1))) {
throw new Error("invalid GOO RLE checksum");
}
let output = new Uint8Array(pixelCount);
let pixel = 0;
let color = 0;
let last = rle.length - 1;
for (let i=1; i<last; i++) {
let chunkType = rle[i] >> 6;
let stride = 0;
let strideIndex0 = i;
let strideIndex1 = i + 1;
let strideIndex2 = i + 2;
let strideIndex3 = i + 3;
if (chunkType === 0) {
color = 0;
} else if (chunkType === 1) {
color = rle[++i];
strideIndex1++;
strideIndex2++;
strideIndex3++;
} else if (chunkType === 2) {
let diffType = (rle[i] >> 4) & 0x03;
let diffValue = rle[i] & 0x0f;
if (diffType === 0) {
color += diffValue;
stride = 1;
} else if (diffType === 1) {
color += diffValue;
stride = rle[++i];
} else if (diffType === 2) {
color -= diffValue;
stride = 1;
} else {
color -= diffValue;
stride = rle[++i];
}
color &= 0xff;
} else {
color = 255;
}
if (chunkType !== 2) {
let chunkLength = (rle[strideIndex0] >> 4) & 0x03;
if (chunkLength === 0) {
stride = rle[strideIndex0] & 0x0f;
} else if (chunkLength === 1) {
stride = (rle[strideIndex1] << 4) + (rle[strideIndex0] & 0x0f);
i += 1;
} else if (chunkLength === 2) {
stride = (rle[strideIndex1] << 12) + (rle[strideIndex2] << 4) + (rle[strideIndex0] & 0x0f);
i += 2;
} else {
stride = (rle[strideIndex1] << 20) + (rle[strideIndex2] << 12) + (rle[strideIndex3] << 4) + (rle[strideIndex0] & 0x0f);
i += 3;
}
}
output.fill(color, pixel, pixel + stride);
pixel += stride;
}
if (pixel !== pixelCount) {
throw new Error(`GOO RLE pixel count mismatch (${pixel} !== ${pixelCount})`);
}
return output;
}
function createPreview(width, height) {
return {
width,
height,
image: new Uint8Array(width * height * 2)
};
}
function accumulatePreview(preview, image, width, height) {
let xscale = width / preview.width;
let yscale = height / preview.height;
for (let y=0; y<preview.height; y++) {
let sy = Math.min(height - 1, Math.floor(y * yscale));
for (let x=0; x<preview.width; x++) {
let sx = Math.min(width - 1, Math.floor(x * xscale));
let pos = (y * preview.width + x) * 2;
if (image[sx * height + sy]) {
preview.image[pos] = 0xff;
preview.image[pos + 1] = 0xff;
}
}
}
}
function checksum(bytes) {
let sum = 0;
for (let i=1; i<bytes.length; i++) {
sum = (sum + bytes[i]) & 0xff;
}
return (~sum) & 0xff;
}
function formatDate() {
let date = new Date();
let pad = value => value.toString().padStart(2, "0");
return [
date.getFullYear(),
pad(date.getMonth() + 1),
pad(date.getDate())
].join("-") + " " + [
pad(date.getHours()),
pad(date.getMinutes()),
pad(date.getSeconds())
].join(":");
}
function readString(view, offset, length) {
let chars = [];
for (let i=0; i<length; i++) {
let value = view.getUint8(offset + i);
if (value === 0) break;
chars.push(String.fromCharCode(value));
}
return chars.join("");
}
function readBytes(view, offset, length) {
return Array.from(new Uint8Array(view.buffer, view.byteOffset + offset, length));
}
function sameBytes(a, b) {
if (!a || a.length !== b.length) return false;
for (let i=0; i<a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}
function toDataView(input) {
if (input instanceof DataView) return input;
if (input instanceof ArrayBuffer) return new DataView(input);
if (ArrayBuffer.isView(input)) return new DataView(input.buffer, input.byteOffset, input.byteLength);
throw new Error("expected ArrayBuffer or typed array");
}
class BinaryWriter {
constructor(buffer) {
this.buffer = buffer;
this.view = new DataView(buffer);
this.pos = 0;
}
seek(pos) {
this.pos = pos;
}
writeBytes(bytes) {
new Uint8Array(this.buffer, this.pos, bytes.length).set(bytes);
this.pos += bytes.length;
}
writeString(value, length) {
value = value || "";
for (let i=0; i<length; i++) {
this.writeU8(i < value.length ? value.charCodeAt(i) : 0);
}
}
writeU8(value) {
this.view.setUint8(this.pos, Number(value || 0) & 0xff);
this.pos += 1;
}
writeU16(value) {
this.view.setUint16(this.pos, Number(value || 0) & 0xffff, false);
this.pos += 2;
}
writeU32(value) {
this.view.setUint32(this.pos, Number(value || 0) >>> 0, false);
this.pos += 4;
}
writeF32(value) {
this.view.setFloat32(this.pos, Number(value || 0), false);
this.pos += 4;
}
}
function supports(format) {
return FORMAT_RECORDS[format] !== undefined;
}
export const GOO = {
encode,
supports,
read,
decodeLayer,
encodeRLE,
decodeRLE,
formats: FORMAT_RECORDS
};