add msla .goo .prz support

This commit is contained in:
Stewart Allen 2026-05-16 12:25:06 -04:00
commit 3255ce7bb7
14 changed files with 770 additions and 12 deletions

View file

@ -491,13 +491,13 @@ Each section starts with a 12-byte null-padded table name followed by a 32-bit t
| RLE4 (PW0) | Nibble-coded | 1-2 (variable) | 4095 (B/W), 15 (gray) | 8 | Optimized variable-length |
| CTB variable RLE | 7-bit grayscale variable RLE | 1-5 (variable) | 268,435,455 | 7 stored / 8 expanded | CTB v3+ layer data |
| RLE128 (PhotonS) | Run-Length 128 | 2 (fixed) | 128 | 8 | Length, Color (BE) |
| RLE+Delim (GOO) | Delimited | 5 (+ delim) | 65535 | 8 | 0x55 + Color + Length16 + 0x0D0A |
| GOO RLE | Chunked | 1-5 (+ checksum) | 268435455 | 8 | 0x55 stream + inverted checksum, layer CRLF-delimited |
**Performance Comparison:**
- **RLE1/RLE125/RLE128:** Simple, predictable, easy to implement, but PWS/CBDDLP are bit-plane encodings rather than full grayscale runs
- **CTB variable RLE:** Better compression and grayscale AA, plus optional seed-derived XOR
- **Nibble RLE4:** Best compression for grayscale AA, complex encoding
- **RLE+Delim:** Highest max repeat, overhead from delimiters
- **GOO RLE:** High max repeat with grayscale support and a per-layer checksum; CRLF only delimits the encoded payload
---
@ -533,18 +533,30 @@ For each run (2 bytes):
**Extensions:** `.goo`, `.prz`
**Magic:** "V3.0" + `0x07000000` (BE) + "DLP\0"
**RLE:** Delimiter-based
**RLE:** Chunked stream with checksum. The layer payload is followed by a CRLF delimiter, but CRLF is not part of the RLE chunk grammar.
**RLE Format:**
```
Each run (5 bytes):
0x55 Magic byte
[color] 0x00 or 0xFF
[length_high] Big-endian uint16
[length_low]
0x0D 0x0A CRLF delimiter
Layer payload:
0x55 layer RLE magic
chunks... chunk stream
checksum bitwise NOT of byte sum from chunks
Maximum repeat: 65535 pixels
Chunk byte0:
bits 7:6 type
0 = black run (0x00)
1 = grayscale run; gray byte follows byte0
2 = delta from previous pixel
3 = white run (0xFF)
bits 5:4 length encoding for non-delta chunks
0 = byte0 low nibble
1 = next byte + low nibble
2 = next two bytes + low nibble
3 = next three bytes + low nibble
bits 3:0 low length nibble or delta value
Non-delta maximum repeat: 0x0FFFFFFF pixels.
Layer record delimiter after payload: 0x0D 0x0A.
```
### CXDLP (Creality)
@ -659,7 +671,7 @@ PrinterModel = SL1
**Tier 3 - Advanced Binary:**
1. **PW0** - Nibble-coded RLE4 (complex but efficient)
2. **PhotonS** - Big-endian RLE128
3. **GOO** - Delimiter-based RLE
3. **GOO** - Chunked RLE with layer checksum
**Tier 4 - Specialized:**
1. **CTBEncrypted** - Requires AES-256-CBC

View file

@ -103,9 +103,10 @@ function device_from_code(code,mode) {
slaFileExt: valueOf(set.sla_file_ext, ''),
slaFormatVersion: valueOf(set.sla_format_version, 1),
slaMachineEditable: valueOf(set.sla_machine_editable, false),
slaMachineName: valueOf(set.sla_machine_name, ''),
deviceZMax: valueOf(set.z_move_max, 0),
gcodeTime: valueOf(set.time_factor, 1),
maxHeight: valueOf(set.build_height, 150),
maxHeight: valueOf(set.build_height || set.max_height, 150),
originCenter: valueOf(set.origin_center, false),
extrudeAbs: valueOf(set.extrude_abs, false),
spindleMax: valueOf(set.spindle_max, 0),

View file

@ -0,0 +1,16 @@
{
"no_clone": true,
"cmd": {},
"settings": {
"bed_width": 132.8,
"bed_depth": 74.7,
"bed_height": 1,
"max_height": 150,
"resolution_x": 4098,
"resolution_y": 2560,
"sla_format": "goo",
"sla_file_ext": "goo",
"sla_machine_name": "Mars 4 DLP",
"sla_format_version": 3
}
}

View file

@ -0,0 +1,16 @@
{
"no_clone": true,
"cmd": {},
"settings": {
"bed_width": 153.36,
"bed_depth": 77.76,
"bed_height": 1,
"max_height": 175,
"resolution_x": 8520,
"resolution_y": 4320,
"sla_format": "goo",
"sla_file_ext": "goo",
"sla_machine_name": "Mars 4",
"sla_format_version": 3
}
}

View file

@ -0,0 +1,16 @@
{
"no_clone": true,
"cmd": {},
"settings": {
"bed_width": 195.84,
"bed_depth": 122.4,
"bed_height": 1,
"max_height": 150,
"resolution_x": 5760,
"resolution_y": 3600,
"sla_format": "goo",
"sla_file_ext": "goo",
"sla_machine_name": "Mars 4 Max",
"sla_format_version": 3
}
}

View file

@ -0,0 +1,16 @@
{
"no_clone": true,
"cmd": {},
"settings": {
"bed_width": 153.36,
"bed_depth": 77.76,
"bed_height": 1,
"max_height": 165,
"resolution_x": 8520,
"resolution_y": 4320,
"sla_format": "goo",
"sla_file_ext": "goo",
"sla_machine_name": "Mars 4 Ultra",
"sla_format_version": 3
}
}

View file

@ -0,0 +1,16 @@
{
"no_clone": true,
"cmd": {},
"settings": {
"bed_width": 143.43,
"bed_depth": 89.6,
"bed_height": 1,
"max_height": 150,
"resolution_x": 4098,
"resolution_y": 2560,
"sla_format": "goo",
"sla_file_ext": "goo",
"sla_machine_name": "Mars 5",
"sla_format_version": 3
}
}

View file

@ -0,0 +1,16 @@
{
"no_clone": true,
"cmd": {},
"settings": {
"bed_width": 218.88,
"bed_depth": 122.88,
"bed_height": 1,
"max_height": 250,
"resolution_x": 11520,
"resolution_y": 5120,
"sla_format": "goo",
"sla_file_ext": "goo",
"sla_machine_name": "Saturn 3",
"sla_format_version": 3
}
}

View file

@ -0,0 +1,16 @@
{
"no_clone": true,
"cmd": {},
"settings": {
"bed_width": 218.88,
"bed_depth": 122.88,
"bed_height": 1,
"max_height": 260,
"resolution_x": 11520,
"resolution_y": 5120,
"sla_format": "goo",
"sla_file_ext": "goo",
"sla_machine_name": "Saturn 3 Ultra",
"sla_format_version": 3
}
}

View file

@ -0,0 +1,16 @@
{
"no_clone": true,
"cmd": {},
"settings": {
"bed_width": 218.88,
"bed_depth": 122.88,
"bed_height": 1,
"max_height": 220,
"resolution_x": 11520,
"resolution_y": 5120,
"sla_format": "goo",
"sla_file_ext": "goo",
"sla_machine_name": "Saturn 4",
"sla_format_version": 3
}
}

View file

@ -0,0 +1,16 @@
{
"no_clone": true,
"cmd": {},
"settings": {
"bed_width": 165.792,
"bed_depth": 71.28,
"bed_height": 1,
"max_height": 170,
"resolution_x": 7536,
"resolution_y": 3240,
"sla_format": "prz",
"sla_file_ext": "prz",
"sla_machine_name": "Sonic Mini 8K S",
"sla_format_version": 3
}
}

View file

@ -46,6 +46,22 @@ export const SLA_FORMATS = {
priority: 1,
family: 'chitubox'
},
goo: {
id: 'goo',
ext: 'goo',
proprietary: true,
available: true,
priority: 2,
family: 'goo'
},
prz: {
id: 'prz',
ext: 'prz',
proprietary: true,
available: true,
priority: 2,
family: 'goo'
},
pw0: {
id: 'pw0',
ext: 'pw0',

View file

@ -4,6 +4,7 @@ import { CXDLP } from './x_cxdlp.js';
import { CTB } from './x_ctb.js';
import { photon } from './x_photon.js';
import { PW } from './x_pw.js';
import { GOO } from './x_goo.js';
import { SLA } from './init-work.js';
import { RSLA } from './x_rsla.js';
import { VSLA } from './x_vsla.js';
@ -76,6 +77,15 @@ export function sla_export(print, online, ondone) {
});
}
if (GOO.supports(format)) {
return GOO.encode(print, (progress, message) => {
online({ progress, message });
}, photon).then(output => {
let { file, layers, volume } = output;
ondone({ width, height, file, layers, volume }, [file]);
});
}
if (format === 'photon' || format === 'photons') {
let legacyMode = SLA.legacy || alias > 1,
part1 = legacyMode ? 0.25 : 0.85,

View file

@ -0,0 +1,575 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { imageToRowMajor, renderRasterLayers, round } from './raster.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, bottom, z, ctx } = params;
let { process, width, height } = ctx;
let raster = imageToRowMajor(rendered.image, width, height, value => value ? 255 : 0);
accumulatePreview(previewSmall, rendered.image, width, height);
accumulatePreview(previewLarge, rendered.image, width, height);
layers.push({
index,
data: encodeRLE(raster),
z: round(z),
pausePositionZ: ctx.device.maxHeight || ctx.device.bedHeight || 0,
exposure: bottom ? process.slaBaseOn : process.slaLayerOn,
lightOffDelay: 0,
waitTimeAfterCure: 0,
waitTimeAfterLift: 0,
waitTimeBeforeCure: bottom ? process.slaBaseOff : process.slaLayerOff,
liftHeight: bottom ? process.slaBasePeelDist : process.slaPeelDist,
liftSpeed: (bottom ? process.slaBasePeelLiftRate : process.slaPeelLiftRate) * 60,
liftHeight2: 0,
liftSpeed2: 0,
retractHeight: bottom ? process.slaBasePeelDist : process.slaPeelDist,
retractSpeed: (bottom ? process.slaBasePeelDropRate : process.slaPeelDropRate) * 60,
retractHeight2: 0,
retractSpeed2: 0,
lightPWM: 255
});
}, 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 bottomOff = process.slaBaseOff || 0;
let layerOff = process.slaLayerOff || 0;
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(process.slaLayerOn);
writer.writeU8(1);
writer.writeF32(0);
writer.writeF32(0);
writer.writeF32(0);
writer.writeF32(bottomOff);
writer.writeF32(0);
writer.writeF32(0);
writer.writeF32(layerOff);
writer.writeF32(process.slaBaseOn);
writer.writeU32(process.slaBaseLayers || 0);
writer.writeF32(process.slaBasePeelDist);
writer.writeF32(process.slaBasePeelLiftRate * 60);
writer.writeF32(process.slaPeelDist);
writer.writeF32(process.slaPeelLiftRate * 60);
writer.writeF32(process.slaBasePeelDist);
writer.writeF32(process.slaBasePeelDropRate * 60);
writer.writeF32(process.slaPeelDist);
writer.writeF32(process.slaPeelDropRate * 60);
for (let i=0; i<8; i++) writer.writeF32(0);
writer.writeU16(255);
writer.writeU16(255);
writer.writeU8(0);
writer.writeU32(estimatePrintTime(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 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, run);
current = value;
run = 1;
}
}
writeRun(output, current, run);
output.push(checksum(output));
return Uint8Array.from(output);
}
function writeRun(output, color, run) {
while (run > 0) {
let stride = Math.min(run, MAX_RUN);
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 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 estimatePrintTime(process, layers) {
let bottom = Math.min(process.slaBaseLayers || 0, layers);
let normal = Math.max(0, layers - bottom);
let bottomTime = number(process.slaBaseOn) + number(process.slaBaseOff);
let normalTime = number(process.slaLayerOn) + number(process.slaLayerOff);
return Math.ceil(bottom * bottomTime + normal * normalTime);
}
function number(value) {
value = Number(value);
return Number.isFinite(value) ? value : 0;
}
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
};