add msla .goo .prz support
This commit is contained in:
parent
db37c13100
commit
3255ce7bb7
14 changed files with 770 additions and 12 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
16
src/kiri/dev/sla/Elegoo.Mars.4.DLP.GOO.json
Normal file
16
src/kiri/dev/sla/Elegoo.Mars.4.DLP.GOO.json
Normal 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
|
||||
}
|
||||
}
|
||||
16
src/kiri/dev/sla/Elegoo.Mars.4.GOO.json
Normal file
16
src/kiri/dev/sla/Elegoo.Mars.4.GOO.json
Normal 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
|
||||
}
|
||||
}
|
||||
16
src/kiri/dev/sla/Elegoo.Mars.4.Max.GOO.json
Normal file
16
src/kiri/dev/sla/Elegoo.Mars.4.Max.GOO.json
Normal 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
|
||||
}
|
||||
}
|
||||
16
src/kiri/dev/sla/Elegoo.Mars.4.Ultra.GOO.json
Normal file
16
src/kiri/dev/sla/Elegoo.Mars.4.Ultra.GOO.json
Normal 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
|
||||
}
|
||||
}
|
||||
16
src/kiri/dev/sla/Elegoo.Mars.5.GOO.json
Normal file
16
src/kiri/dev/sla/Elegoo.Mars.5.GOO.json
Normal 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
|
||||
}
|
||||
}
|
||||
16
src/kiri/dev/sla/Elegoo.Saturn.3.GOO.json
Normal file
16
src/kiri/dev/sla/Elegoo.Saturn.3.GOO.json
Normal 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
|
||||
}
|
||||
}
|
||||
16
src/kiri/dev/sla/Elegoo.Saturn.3.Ultra.GOO.json
Normal file
16
src/kiri/dev/sla/Elegoo.Saturn.3.Ultra.GOO.json
Normal 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
|
||||
}
|
||||
}
|
||||
16
src/kiri/dev/sla/Elegoo.Saturn.4.GOO.json
Normal file
16
src/kiri/dev/sla/Elegoo.Saturn.4.GOO.json
Normal 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
|
||||
}
|
||||
}
|
||||
16
src/kiri/dev/sla/Phrozen.Sonic.Mini.8K.S.PRZ.json
Normal file
16
src/kiri/dev/sla/Phrozen.Sonic.Mini.8K.S.PRZ.json
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
575
src/kiri/mode/sla/work/x_goo.js
Normal file
575
src/kiri/mode/sla/work/x_goo.js
Normal 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
|
||||
};
|
||||
Loading…
Reference in a new issue