update .ctb options

This commit is contained in:
Stewart Allen 2026-05-17 07:34:27 -04:00
commit 7e105f255e
11 changed files with 573 additions and 128 deletions

View file

@ -210,6 +210,23 @@ Same header structure as CTB v3, with additional support for:
- PageNumber field in layer table - PageNumber field in layer table
- Transition layers - Transition layers
#### 1.4 CTB Encrypted v4-5
**Extensions:** `.ctb`, `.encrypted.ctb`
**Magic:** `0x12FD0107` (LE)
**Versions:** 4, 5 (default: 5)
**Settings block:** 288-byte slicer-settings table encrypted with AES-256-CBC, no padding, zero-padded to 16-byte blocks
**Layer data:** CTB variable-length 7-bit grayscale RLE, then CTB layer XOR using `LayerXorKey` (`0xEFBEADDE` by default); some files additionally AES-encrypt layer-data subranges
Encrypted CTB is a different container from plain CTB v3/v4/v5, not just a flag on the v3 layout. The fixed 48-byte header points at the encrypted settings block and a final encrypted SHA-256 signature. The AES key/IV are derived from UVtools-compatible Chitubox constants with the `UVtools` XOR mask. Kiri keeps these primitives native in JS in `x_ctb_crypto.js`; full writer support still needs the encrypted settings table, layer pointer table, resin parameters, previews, and final signature wired into `x_ctb`.
Key tables:
- File header: 48 bytes
- Slicer settings: 288 bytes
- Layer pointer: 16 bytes
- Layer definition: 88 bytes
- Per-layer settings flag: `0x40` allow, `0x00` disallow
### 2. Anycubic Family ### 2. Anycubic Family
#### 2.1 Legacy Anycubic Formats (v1) #### 2.1 Legacy Anycubic Formats (v1)
@ -654,6 +671,46 @@ PrinterModel = SL1
} }
``` ```
### RSLA / VSLA (Grid.Space Generic)
**Extensions:** `.rsla`, `.vsla`
**Type:** ZIP with JSON manifest
**Layer Format:** RSLA stores raster PNG layers; VSLA stores SVG-like vector layer geometry
These formats are intended as a vendor-neutral interchange and debugging target. They carry enough process metadata to preserve behavior before conversion to a proprietary printer format.
**Shared manifest process fields:**
```json
{
"process": {
"bottomLayers": 5,
"transitionLayers": 8,
"exposure": 2.4,
"bottomExposure": 28,
"lightOffDelay": 0,
"bottomLightOffDelay": 0,
"waitBeforeCure": 0.5,
"bottomWaitBeforeCure": 2,
"waitAfterCure": 0,
"waitAfterLift": 0,
"liftHeight": 5,
"bottomLiftHeight": 6,
"liftSpeed": 180,
"bottomLiftSpeed": 60,
"retractHeight": 5,
"retractSpeed": 180,
"lightPWM": 255,
"bottomLightPWM": 255,
"motion": "normal",
"antiAlias": 1
}
}
```
Each layer also carries a resolved per-layer `process` object with `bottom`, `transition`, `transitionIndex`, effective exposure, waits, motion, and PWM. Proprietary writers should prefer this resolved per-layer model where their format supports it, then fall back to global process fields where it does not.
VSLA layer SVG roots additionally include process data attributes such as `data-exposure`, `data-light-off-delay`, `data-light-pwm`, `data-bottom`, and `data-transition` so the per-layer intent is visible without parsing the manifest.
--- ---
## Implementation Guidance for Sandboxed Environments ## Implementation Guidance for Sandboxed Environments
@ -898,6 +955,7 @@ def decode_ctb_variable_rle(data: bytes, width: int, height: int) -> bytes:
**CTB:** **CTB:**
- `UVtools.Core/FileFormats/ChituboxFile.cs`: `DecodeCtbImage`, `EncodeCtbImage`, `LayerRleCryptBuffer` - `UVtools.Core/FileFormats/ChituboxFile.cs`: `DecodeCtbImage`, `EncodeCtbImage`, `LayerRleCryptBuffer`
- `UVtools.Core/FileFormats/CTBEncryptedFile.cs`: encrypted CTB v4-v5 container and AES handling - `UVtools.Core/FileFormats/CTBEncryptedFile.cs`: encrypted CTB v4-v5 container and AES handling
- `src/kiri/mode/sla/work/x_ctb_crypto.js`: native JS AES-256-CBC/no-padding, CTB layer XOR, encrypted CTB constants
**Anycubic:** **Anycubic:**
- `UVtools.Core/FileFormats/AnycubicFile.cs`: `FileMark`, `Header`, `LayerDef`, `GetAvailableVersionsForExtension`, `PrinterModel` - `UVtools.Core/FileFormats/AnycubicFile.cs`: `FileMark`, `Header`, `LayerDef`, `GetAvailableVersionsForExtension`, `PrinterModel`

View file

@ -7,6 +7,8 @@
"bed_height": 1, "bed_height": 1,
"max_height": 300, "max_height": 300,
"resolution_x": 3840, "resolution_x": 3840,
"resolution_y": 2160 "resolution_y": 2160,
"sla_format": "cxdlp",
"sla_file_ext": "cxdlp"
} }
} }

View file

@ -7,6 +7,8 @@
"bed_height": 1, "bed_height": 1,
"max_height": 160, "max_height": 160,
"resolution_x": 1620, "resolution_x": 1620,
"resolution_y": 2560 "resolution_y": 2560,
"sla_format": "cxdlp",
"sla_file_ext": "cxdlp"
} }
} }

View file

@ -7,6 +7,8 @@
"bed_height": 1, "bed_height": 1,
"max_height": 200, "max_height": 200,
"resolution_x": 3840, "resolution_x": 3840,
"resolution_y": 2400 "resolution_y": 2400,
"sla_format": "cxdlp",
"sla_file_ext": "cxdlp"
} }
} }

View file

@ -46,6 +46,14 @@ export const SLA_FORMATS = {
priority: 1, priority: 1,
family: 'chitubox' family: 'chitubox'
}, },
ctbe: {
id: 'ctbe',
ext: 'encrypted.ctb',
proprietary: true,
available: false,
priority: 1,
family: 'chitubox-encrypted'
},
goo: { goo: {
id: 'goo', id: 'goo',
ext: 'goo', ext: 'goo',

View file

@ -0,0 +1,128 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
const DEFAULT_PWM = 255;
const TILT_MOTION = {
liftHeight: 0.05,
liftSpeed: 0.05,
retractHeight: 0,
retractSpeed: 0.05,
motorTime: 4
};
export function createProcessMetadata(device, process) {
let transitionLayers = intValue(process.slaTransitionLayers);
let lightPWM = pwmValue(process.slaLightPWM);
let bottomLightPWM = pwmValue(process.slaBaseLightPWM, lightPWM);
let normalMotion = motionFor(device, process, false);
let bottomMotion = motionFor(device, process, true);
return {
bottomLayers: intValue(process.slaBaseLayers),
transitionLayers,
exposure: number(process.slaLayerOn),
bottomExposure: number(process.slaBaseOn),
lightOffDelay: number(process.slaLayerOff),
bottomLightOffDelay: number(process.slaBaseOff),
waitBeforeCure: number(process.slaWaitBeforeCure ?? process.slaLayerOff),
bottomWaitBeforeCure: number(process.slaBaseWaitBeforeCure ?? process.slaBaseOff),
waitAfterCure: number(process.slaWaitAfterCure),
bottomWaitAfterCure: number(process.slaBaseWaitAfterCure),
waitAfterLift: number(process.slaWaitAfterLift),
bottomWaitAfterLift: number(process.slaBaseWaitAfterLift),
liftHeight: normalMotion.liftHeight,
bottomLiftHeight: bottomMotion.liftHeight,
liftSpeed: normalMotion.liftSpeed,
bottomLiftSpeed: bottomMotion.liftSpeed,
retractHeight: normalMotion.retractHeight,
bottomRetractHeight: bottomMotion.retractHeight,
retractSpeed: normalMotion.retractSpeed,
bottomRetractSpeed: bottomMotion.retractSpeed,
lightPWM,
bottomLightPWM,
motion: device.slaMotion || "normal",
firstLayerOffset: number(process.slaFirstOffset),
antiAlias: intValue(process.slaAntiAlias, 1)
};
}
export function createLayerMetadata(device, process, index, z, area) {
let meta = createProcessMetadata(device, process);
let bottom = index < meta.bottomLayers;
let transitionIndex = bottom ? -1 : index - meta.bottomLayers;
let transition = transitionIndex >= 0 && transitionIndex < meta.transitionLayers;
let motion = motionFor(device, process, bottom);
return {
index,
z: round(z),
area: round(area),
bottom,
transition,
transitionIndex: transition ? transitionIndex : -1,
exposure: round(layerExposure(meta, bottom, transitionIndex)),
lightOffDelay: bottom ? meta.bottomLightOffDelay : meta.lightOffDelay,
waitBeforeCure: bottom ? meta.bottomWaitBeforeCure : meta.waitBeforeCure,
waitAfterCure: bottom ? meta.bottomWaitAfterCure : meta.waitAfterCure,
waitAfterLift: bottom ? meta.bottomWaitAfterLift : meta.waitAfterLift,
liftHeight: motion.liftHeight,
liftSpeed: motion.liftSpeed,
retractHeight: motion.retractHeight,
retractSpeed: motion.retractSpeed,
lightPWM: bottom ? meta.bottomLightPWM : meta.lightPWM
};
}
export function motionFor(device, process, bottom) {
if (device.slaMotion === "tilt") {
return TILT_MOTION;
}
let normalDrop = process.slaPeelDropRate;
let bottomDrop = process.slaBasePeelDropRate ?? normalDrop;
return {
liftHeight: number(bottom ? process.slaBasePeelDist : process.slaPeelDist),
liftSpeed: number(bottom ? process.slaBasePeelLiftRate : process.slaPeelLiftRate) * 60,
retractHeight: number(bottom ? process.slaBasePeelDist : process.slaPeelDist),
retractSpeed: number(bottom ? bottomDrop : normalDrop) * 60,
motorTime: 0
};
}
export function estimatePrintTime(device, process, layers) {
let meta = createProcessMetadata(device, process);
let seconds = 0;
for (let index=0; index<layers; index++) {
let layer = createLayerMetadata(device, process, index, 0, 0);
let motor = device.slaMotion === "tilt" ? TILT_MOTION.motorTime : 0;
seconds += layer.exposure + layer.waitBeforeCure + layer.waitAfterCure + layer.waitAfterLift + motor;
}
return Math.ceil(seconds);
}
export function round(value) {
return Number.parseFloat(Number(value || 0).toFixed(5));
}
function layerExposure(meta, bottom, transitionIndex) {
if (bottom || meta.transitionLayers <= 0 || transitionIndex < 0 || transitionIndex >= meta.transitionLayers) {
return bottom ? meta.bottomExposure : meta.exposure;
}
let step = (transitionIndex + 1) / (meta.transitionLayers + 1);
return meta.bottomExposure + (meta.exposure - meta.bottomExposure) * step;
}
function pwmValue(value, fallback = DEFAULT_PWM) {
value = intValue(value, fallback);
return Math.max(0, Math.min(255, value));
}
function intValue(value, fallback = 0) {
value = Number.parseInt(value);
return Number.isFinite(value) ? value : fallback;
}
function number(value) {
value = Number(value);
return Number.isFinite(value) ? value : 0;
}

View file

@ -3,6 +3,7 @@
import { JSZip } from '../../../../ext/jszip-esm.js'; import { JSZip } from '../../../../ext/jszip-esm.js';
import { PNG } from '../../../../ext/pngjs.esm.js'; import { PNG } from '../../../../ext/pngjs.esm.js';
import { imageToRowMajor, renderRasterLayers, round } from './raster.js'; import { imageToRowMajor, renderRasterLayers, round } from './raster.js';
import { ctbLayerCrypt } from './x_ctb_crypto.js';
const CTB_V3_MAGIC = 0x12fd0086; const CTB_V3_MAGIC = 0x12fd0086;
const HEADER_SIZE = 0x70; const HEADER_SIZE = 0x70;
@ -715,37 +716,11 @@ function requireBytes(input, offset, length) {
} }
function decipher(seed, layerIndex, input) { function decipher(seed, layerIndex, input) {
if (!seed) return input.slice(); return ctbLayerCrypt(seed, layerIndex, input);
let output = new Uint8Array(input.length);
let keyring = newKeyring(seed, layerIndex);
for (let i=0; i<input.length; i++) {
output[i] = input[i] ^ keyring.next();
}
return output;
} }
function encipher(seed, layerIndex, input) { function encipher(seed, layerIndex, input) {
return decipher(seed, layerIndex, input); return ctbLayerCrypt(seed, layerIndex, input);
}
function newKeyring(seed, layerIndex) {
let init = (Math.imul(seed >>> 0, 0x2d83cdac) + 0xd8a83423) >>> 0;
let keySeed = (Math.imul(layerIndex >>> 0, 0x1e1530cd) + 0xec3d47cd) >>> 0;
let key = Math.imul(keySeed, init) >>> 0;
let index = 0;
return {
next() {
let byte = (key >>> (8 * index)) & 0xff;
index++;
if ((index & 3) === 0) {
key = (key + init) >>> 0;
index = 0;
}
return byte;
}
};
} }
function imageToPNG(image, width, height) { function imageToPNG(image, width, height) {

View file

@ -0,0 +1,321 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
const UVTOOLS_KEY = "UVtools";
const SECRET_KEY = "hQ36XB6yTk+zO02ysyiowt8yC1buK+nbLWyfY40EXoU=";
const SECRET_IV = "Wld+ampndVJecmVjYH5cWQ==";
const BLOCK_SIZE = 16;
const SBOX = new Uint8Array(256);
const INV_SBOX = new Uint8Array(256);
const RCON = new Uint8Array(15);
initTables();
export const CTB_ENCRYPTED_MAGIC = 0x12fd0107;
export const CTB_ENCRYPTED_VERSION = 5;
export const CTB_LAYER_XOR_KEY = 0xefbeadde;
export const CTB_ENCRYPTED_HEADER_SIZE = 48;
export const CTB_ENCRYPTED_SETTINGS_SIZE = 288;
export const CTB_ENCRYPTED_LAYER_POINTER_SIZE = 16;
export const CTB_ENCRYPTED_LAYER_DEF_SIZE = 88;
export const CTB_ENCRYPTED_MIN_RLE_AES_LENGTH = 512;
export const CTB_PER_LAYER_SETTINGS_ALLOW = 0x40;
export const CTB_PER_LAYER_SETTINGS_DISALLOW = 0x00;
export const CTB_ENCRYPTED_KEY = xorCipher(base64Bytes(SECRET_KEY), UVTOOLS_KEY);
export const CTB_ENCRYPTED_IV = xorCipher(base64Bytes(SECRET_IV), UVTOOLS_KEY);
export function ctbLayerCrypt(seed, layerIndex, input) {
let output = new Uint8Array(input);
ctbLayerCryptInPlace(seed, layerIndex, output);
return output;
}
export function ctbLayerCryptInPlace(seed, layerIndex, input) {
if (!seed) return input;
let init = (Math.imul(seed >>> 0, 0x2d83cdac) + 0xd8a83423) >>> 0;
let keySeed = (Math.imul(layerIndex >>> 0, 0x1e1530cd) + 0xec3d47cd) >>> 0;
let key = Math.imul(keySeed, init) >>> 0;
let index = 0;
for (let i=0; i<input.length; i++) {
input[i] ^= (key >>> (8 * index)) & 0xff;
index++;
if ((index & 3) === 0) {
key = (key + init) >>> 0;
index = 0;
}
}
return input;
}
export function ctbEncrypt(data) {
return aesCbcCrypt(data, true, CTB_ENCRYPTED_KEY, CTB_ENCRYPTED_IV);
}
export function ctbDecrypt(data) {
return aesCbcCrypt(data, false, CTB_ENCRYPTED_KEY, CTB_ENCRYPTED_IV);
}
export async function ctbSignature(checksumValue = 0xcafebabe) {
let bytes = new Uint8Array(8);
let view = new DataView(bytes.buffer);
view.setUint32(0, checksumValue >>> 0, true);
view.setUint32(4, Math.floor(checksumValue / 0x100000000), true);
return ctbEncrypt(await sha256(bytes));
}
function aesCbcCrypt(data, encrypt, key, iv) {
if (key.length !== 32) {
throw new Error(`CTB AES key must be 32 bytes (${key.length})`);
}
if (iv.length !== BLOCK_SIZE) {
throw new Error(`CTB AES IV must be 16 bytes (${iv.length})`);
}
let keys = expandKey(key);
let input = encrypt ? zeroPad(data) : toBytes(data);
if (input.length % BLOCK_SIZE !== 0) {
throw new Error(`CTB AES input must align to 16 bytes (${input.length})`);
}
let output = new Uint8Array(input.length);
let previous = new Uint8Array(iv);
for (let offset=0; offset<input.length; offset += BLOCK_SIZE) {
let block = input.slice(offset, offset + BLOCK_SIZE);
if (encrypt) {
xorBlock(block, previous);
block = encryptBlock(block, keys);
previous = block;
output.set(block, offset);
continue;
}
let current = block;
block = decryptBlock(block, keys);
xorBlock(block, previous);
previous = current;
output.set(block, offset);
}
return output;
}
function encryptBlock(block, keys) {
let state = new Uint8Array(block);
let rounds = keys.rounds;
addRoundKey(state, keys.bytes, 0);
for (let round=1; round<rounds; round++) {
subBytes(state);
shiftRows(state);
mixColumns(state);
addRoundKey(state, keys.bytes, round);
}
subBytes(state);
shiftRows(state);
addRoundKey(state, keys.bytes, rounds);
return state;
}
function decryptBlock(block, keys) {
let state = new Uint8Array(block);
let rounds = keys.rounds;
addRoundKey(state, keys.bytes, rounds);
for (let round=rounds - 1; round>0; round--) {
invShiftRows(state);
invSubBytes(state);
addRoundKey(state, keys.bytes, round);
invMixColumns(state);
}
invShiftRows(state);
invSubBytes(state);
addRoundKey(state, keys.bytes, 0);
return state;
}
function expandKey(key) {
let nk = 8;
let nb = 4;
let rounds = 14;
let bytes = new Uint8Array(nb * (rounds + 1) * 4);
bytes.set(key);
let temp = new Uint8Array(4);
let words = nb * (rounds + 1);
for (let word=nk; word<words; word++) {
let prev = (word - 1) * 4;
temp[0] = bytes[prev];
temp[1] = bytes[prev + 1];
temp[2] = bytes[prev + 2];
temp[3] = bytes[prev + 3];
if (word % nk === 0) {
let first = temp[0];
temp[0] = SBOX[temp[1]] ^ RCON[word / nk];
temp[1] = SBOX[temp[2]];
temp[2] = SBOX[temp[3]];
temp[3] = SBOX[first];
} else if (word % nk === 4) {
temp[0] = SBOX[temp[0]];
temp[1] = SBOX[temp[1]];
temp[2] = SBOX[temp[2]];
temp[3] = SBOX[temp[3]];
}
let prior = (word - nk) * 4;
let out = word * 4;
bytes[out] = bytes[prior] ^ temp[0];
bytes[out + 1] = bytes[prior + 1] ^ temp[1];
bytes[out + 2] = bytes[prior + 2] ^ temp[2];
bytes[out + 3] = bytes[prior + 3] ^ temp[3];
}
return { bytes, rounds };
}
function addRoundKey(state, keys, round) {
let offset = round * BLOCK_SIZE;
for (let i=0; i<BLOCK_SIZE; i++) {
state[i] ^= keys[offset + i];
}
}
function subBytes(state) {
for (let i=0; i<BLOCK_SIZE; i++) state[i] = SBOX[state[i]];
}
function invSubBytes(state) {
for (let i=0; i<BLOCK_SIZE; i++) state[i] = INV_SBOX[state[i]];
}
function shiftRows(state) {
let t = state.slice();
state[1] = t[5]; state[5] = t[9]; state[9] = t[13]; state[13] = t[1];
state[2] = t[10]; state[6] = t[14]; state[10] = t[2]; state[14] = t[6];
state[3] = t[15]; state[7] = t[3]; state[11] = t[7]; state[15] = t[11];
}
function invShiftRows(state) {
let t = state.slice();
state[1] = t[13]; state[5] = t[1]; state[9] = t[5]; state[13] = t[9];
state[2] = t[10]; state[6] = t[14]; state[10] = t[2]; state[14] = t[6];
state[3] = t[7]; state[7] = t[11]; state[11] = t[15]; state[15] = t[3];
}
function mixColumns(state) {
for (let c=0; c<BLOCK_SIZE; c += 4) {
let a0 = state[c];
let a1 = state[c + 1];
let a2 = state[c + 2];
let a3 = state[c + 3];
let t = a0 ^ a1 ^ a2 ^ a3;
let u = a0;
state[c] ^= t ^ xtime(a0 ^ a1);
state[c + 1] ^= t ^ xtime(a1 ^ a2);
state[c + 2] ^= t ^ xtime(a2 ^ a3);
state[c + 3] ^= t ^ xtime(a3 ^ u);
}
}
function invMixColumns(state) {
for (let c=0; c<BLOCK_SIZE; c += 4) {
let a0 = state[c];
let a1 = state[c + 1];
let a2 = state[c + 2];
let a3 = state[c + 3];
state[c] = mul(a0, 14) ^ mul(a1, 11) ^ mul(a2, 13) ^ mul(a3, 9);
state[c + 1] = mul(a0, 9) ^ mul(a1, 14) ^ mul(a2, 11) ^ mul(a3, 13);
state[c + 2] = mul(a0, 13) ^ mul(a1, 9) ^ mul(a2, 14) ^ mul(a3, 11);
state[c + 3] = mul(a0, 11) ^ mul(a1, 13) ^ mul(a2, 9) ^ mul(a3, 14);
}
}
function initTables() {
RCON[1] = 1;
for (let i=2; i<RCON.length; i++) RCON[i] = xtime(RCON[i - 1]);
for (let i=0; i<256; i++) {
let inv = i ? gfPow(i, 254) : 0;
let s = inv ^ rot(inv, 1) ^ rot(inv, 2) ^ rot(inv, 3) ^ rot(inv, 4) ^ 0x63;
SBOX[i] = s & 0xff;
INV_SBOX[SBOX[i]] = i;
}
}
function xtime(value) {
return ((value << 1) ^ ((value & 0x80) ? 0x1b : 0)) & 0xff;
}
function mul(a, b) {
let out = 0;
while (b) {
if (b & 1) out ^= a;
a = xtime(a);
b >>= 1;
}
return out;
}
function gfPow(value, power) {
let out = 1;
while (power) {
if (power & 1) out = mul(out, value);
value = mul(value, value);
power >>= 1;
}
return out;
}
function rot(value, shift) {
return ((value << shift) | (value >> (8 - shift))) & 0xff;
}
function zeroPad(data) {
data = toBytes(data);
let length = Math.ceil(data.length / BLOCK_SIZE) * BLOCK_SIZE;
if (length === data.length) return data;
let output = new Uint8Array(length);
output.set(data);
return output;
}
function xorBlock(block, key) {
for (let i=0; i<BLOCK_SIZE; i++) block[i] ^= key[i];
}
function xorCipher(bytes, key) {
let output = new Uint8Array(bytes.length);
for (let i=0; i<bytes.length; i++) {
output[i] = bytes[i] ^ key.charCodeAt(i % key.length);
}
return output;
}
function base64Bytes(text) {
let binary = atob(text);
let bytes = new Uint8Array(binary.length);
for (let i=0; i<binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
function toBytes(data) {
return data instanceof Uint8Array ? data : new Uint8Array(data);
}
async function sha256(bytes) {
let subtle = globalThis.crypto && globalThis.crypto.subtle;
if (!subtle) {
throw new Error("CTB encrypted signature requires crypto.subtle SHA-256");
}
return new Uint8Array(await subtle.digest("SHA-256", bytes));
}

View file

@ -1,6 +1,7 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { imageToRowMajor, renderRasterLayers, round } from './raster.js'; import { imageToRowMajor, renderRasterLayers, round } from './raster.js';
import { createLayerMetadata, createProcessMetadata, estimatePrintTime, motionFor } from './meta.js';
const FAMILY = "GOO"; const FAMILY = "GOO";
const FILE_VERSION = "V3.0"; const FILE_VERSION = "V3.0";
@ -11,13 +12,6 @@ const HEADER_SIZE = 195477;
const LAYER_DEF_SIZE = 70; const LAYER_DEF_SIZE = 70;
const FOOTER_SIZE = 11; const FOOTER_SIZE = 11;
const MAX_RUN = 0x0fffffff; const MAX_RUN = 0x0fffffff;
const TILT_MOTION = {
liftHeight: 0.05,
liftSpeed: 0.05,
retractHeight: 0,
retractSpeed: 0.05,
motorTime: 4
};
const FORMAT_RECORDS = { const FORMAT_RECORDS = {
goo: { ext: "goo", machine: "Elegoo GOO", notes: "Elegoo GOO" }, goo: { ext: "goo", machine: "Elegoo GOO", notes: "Elegoo GOO" },
@ -46,9 +40,9 @@ function encodeGOO(print, progress, photon, rec) {
let previewLarge = createPreview(290, 290); let previewLarge = createPreview(290, 290);
let { ctx, volume } = renderRasterLayers(print, photon, params => { let { ctx, volume } = renderRasterLayers(print, photon, params => {
let { index, rendered, bottom, z, ctx } = params; let { index, rendered, z, ctx } = params;
let { device, process, width, height } = ctx; let { device, process, width, height } = ctx;
let motion = layerMotion(device, process, bottom); let layerMeta = createLayerMetadata(device, process, index, z, rendered.area);
let raster = imageToRowMajor(rendered.image, width, height, normalizePixel); let raster = imageToRowMajor(rendered.image, width, height, normalizePixel);
accumulatePreview(previewSmall, rendered.image, width, height); accumulatePreview(previewSmall, rendered.image, width, height);
@ -57,22 +51,22 @@ function encodeGOO(print, progress, photon, rec) {
layers.push({ layers.push({
index, index,
data: encodeRLE(raster), data: encodeRLE(raster),
z: round(z), z: layerMeta.z,
pausePositionZ: ctx.device.maxHeight || ctx.device.bedHeight || 0, pausePositionZ: ctx.device.maxHeight || ctx.device.bedHeight || 0,
exposure: bottom ? process.slaBaseOn : process.slaLayerOn, exposure: layerMeta.exposure,
lightOffDelay: 0, lightOffDelay: layerMeta.lightOffDelay,
waitTimeAfterCure: 0, waitTimeAfterCure: layerMeta.waitAfterCure,
waitTimeAfterLift: 0, waitTimeAfterLift: layerMeta.waitAfterLift,
waitTimeBeforeCure: bottom ? process.slaBaseOff : process.slaLayerOff, waitTimeBeforeCure: layerMeta.waitBeforeCure,
liftHeight: motion.liftHeight, liftHeight: layerMeta.liftHeight,
liftSpeed: motion.liftSpeed, liftSpeed: layerMeta.liftSpeed,
liftHeight2: 0, liftHeight2: 0,
liftSpeed2: 0, liftSpeed2: 0,
retractHeight: motion.retractHeight, retractHeight: layerMeta.retractHeight,
retractSpeed: motion.retractSpeed, retractSpeed: layerMeta.retractSpeed,
retractHeight2: 0, retractHeight2: 0,
retractSpeed2: 0, retractSpeed2: 0,
lightPWM: 255 lightPWM: layerMeta.lightPWM
}); });
}, progress ? (value) => progress(value * 0.85, `${rec.ext}_encode`) : null); }, progress ? (value) => progress(value * 0.85, `${rec.ext}_encode`) : null);
@ -107,10 +101,9 @@ function writeGOO(params) {
function writeHeader(writer, params) { function writeHeader(writer, params) {
let { ctx, layers, volume, previewSmall, previewLarge, rec } = params; let { ctx, layers, volume, previewSmall, previewLarge, rec } = params;
let { device, process, width, height } = ctx; let { device, process, width, height } = ctx;
let bottomOff = process.slaBaseOff || 0; let meta = createProcessMetadata(device, process);
let layerOff = process.slaLayerOff || 0; let bottomMotion = motionFor(device, process, true);
let bottomMotion = layerMotion(device, process, true); let layerMotionValue = motionFor(device, process, false);
let layerMotionValue = layerMotion(device, process, false);
writer.writeString(FILE_VERSION, 4); writer.writeString(FILE_VERSION, 4);
writer.writeBytes(FILE_MAGIC); writer.writeBytes(FILE_MAGIC);
@ -136,17 +129,17 @@ function writeHeader(writer, params) {
writer.writeF32(device.bedDepth); writer.writeF32(device.bedDepth);
writer.writeF32(device.maxHeight || device.bedHeight || 0); writer.writeF32(device.maxHeight || device.bedHeight || 0);
writer.writeF32(process.slaSlice); writer.writeF32(process.slaSlice);
writer.writeF32(process.slaLayerOn); writer.writeF32(meta.exposure);
writer.writeU8(1); writer.writeU8(1);
writer.writeF32(0); writer.writeF32(0);
writer.writeF32(0); writer.writeF32(0);
writer.writeF32(0); writer.writeF32(0);
writer.writeF32(bottomOff); writer.writeF32(meta.bottomWaitBeforeCure);
writer.writeF32(0); writer.writeF32(0);
writer.writeF32(0); writer.writeF32(0);
writer.writeF32(layerOff); writer.writeF32(meta.waitBeforeCure);
writer.writeF32(process.slaBaseOn); writer.writeF32(meta.bottomExposure);
writer.writeU32(process.slaBaseLayers || 0); writer.writeU32(meta.bottomLayers);
writer.writeF32(bottomMotion.liftHeight); writer.writeF32(bottomMotion.liftHeight);
writer.writeF32(bottomMotion.liftSpeed); writer.writeF32(bottomMotion.liftSpeed);
writer.writeF32(layerMotionValue.liftHeight); writer.writeF32(layerMotionValue.liftHeight);
@ -156,8 +149,8 @@ function writeHeader(writer, params) {
writer.writeF32(layerMotionValue.retractHeight); writer.writeF32(layerMotionValue.retractHeight);
writer.writeF32(layerMotionValue.retractSpeed); writer.writeF32(layerMotionValue.retractSpeed);
for (let i=0; i<8; i++) writer.writeF32(0); for (let i=0; i<8; i++) writer.writeF32(0);
writer.writeU16(255); writer.writeU16(meta.bottomLightPWM);
writer.writeU16(255); writer.writeU16(meta.lightPWM);
writer.writeU8(0); writer.writeU8(0);
writer.writeU32(estimatePrintTime(device, process, layers.length)); writer.writeU32(estimatePrintTime(device, process, layers.length));
writer.writeF32(round(volume)); writer.writeF32(round(volume));
@ -197,20 +190,6 @@ function writeLayer(writer, layer) {
writer.writeBytes(DELIMITER); writer.writeBytes(DELIMITER);
} }
function layerMotion(device, process, bottom) {
if (device.slaMotion === "tilt") {
return TILT_MOTION;
}
return {
liftHeight: bottom ? process.slaBasePeelDist : process.slaPeelDist,
liftSpeed: (bottom ? process.slaBasePeelLiftRate : process.slaPeelLiftRate) * 60,
retractHeight: bottom ? process.slaBasePeelDist : process.slaPeelDist,
retractSpeed: (bottom ? process.slaBasePeelDropRate : process.slaPeelDropRate) * 60,
motorTime: 0
};
}
function writeFooter(writer) { function writeFooter(writer) {
writer.writeU8(0); writer.writeU8(0);
writer.writeU8(0); writer.writeU8(0);
@ -506,20 +485,6 @@ function accumulatePreview(preview, image, width, height) {
} }
} }
function estimatePrintTime(device, process, layers) {
let bottom = Math.min(process.slaBaseLayers || 0, layers);
let normal = Math.max(0, layers - bottom);
let motorTime = device.slaMotion === "tilt" ? TILT_MOTION.motorTime : 0;
let bottomTime = number(process.slaBaseOn) + number(process.slaBaseOff) + motorTime;
let normalTime = number(process.slaLayerOn) + number(process.slaLayerOff) + motorTime;
return Math.ceil(bottom * bottomTime + normal * normalTime);
}
function number(value) {
value = Number(value);
return Number.isFinite(value) ? value : 0;
}
function checksum(bytes) { function checksum(bytes) {
let sum = 0; let sum = 0;
for (let i=1; i<bytes.length; i++) { for (let i=1; i<bytes.length; i++) {

View file

@ -4,6 +4,7 @@ import { JSZip } from '../../../../ext/jszip-esm.js';
import { PNG } from '../../../../ext/pngjs.esm.js'; import { PNG } from '../../../../ext/pngjs.esm.js';
import { photon } from './x_photon.js'; import { photon } from './x_photon.js';
import { imageToRowMajor, renderRasterLayers, round } from './raster.js'; import { imageToRowMajor, renderRasterLayers, round } from './raster.js';
import { createLayerMetadata, createProcessMetadata } from './meta.js';
const FORMAT = "rsla"; const FORMAT = "rsla";
const MIME = "application/vnd.gridspace.rsla+zip"; const MIME = "application/vnd.gridspace.rsla+zip";
@ -14,14 +15,17 @@ function encode(print, progress) {
let { ctx, volume } = renderRasterLayers(print, photon, params => { let { ctx, volume } = renderRasterLayers(print, photon, params => {
let { index, rendered, ctx } = params; let { index, rendered, ctx } = params;
let { process, width, height } = ctx; let { device, process, width, height } = ctx;
let z = process.slaFirstOffset + process.slaSlice * index;
let layerMeta = createLayerMetadata(device, process, index, z, rendered.area);
let file = `layers/${index.toString().padStart(6, "0")}.png`; let file = `layers/${index.toString().padStart(6, "0")}.png`;
let layer = { let layer = {
index, index,
z: round(process.slaFirstOffset + process.slaSlice * index), z: layerMeta.z,
area: round(rendered.area), area: layerMeta.area,
file file,
process: layerMeta
}; };
layers.push(layer); layers.push(layer);
@ -79,18 +83,7 @@ function createManifest(params) {
black: "off" black: "off"
}, },
process: { process: {
bottomLayers: process.slaBaseLayers, ...createProcessMetadata(device, process)
exposure: process.slaLayerOn,
bottomExposure: process.slaBaseOn,
lightOffDelay: process.slaLayerOff,
bottomLightOffDelay: process.slaBaseOff,
liftHeight: process.slaPeelDist,
bottomLiftHeight: process.slaBasePeelDist,
liftSpeed: process.slaPeelLiftRate * 60,
bottomLiftSpeed: process.slaBasePeelLiftRate * 60,
retractSpeed: process.slaPeelDropRate * 60,
firstLayerOffset: process.slaFirstOffset,
antiAlias: process.slaAntiAlias
}, },
layers layers
}; };

View file

@ -1,6 +1,7 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { JSZip } from '../../../../ext/jszip-esm.js'; import { JSZip } from '../../../../ext/jszip-esm.js';
import { createLayerMetadata, createProcessMetadata, round } from './meta.js';
const FORMAT = "vsla"; const FORMAT = "vsla";
const MIME = "application/vnd.gridspace.vsla+zip"; const MIME = "application/vnd.gridspace.vsla+zip";
@ -18,7 +19,7 @@ function encode(print, progress) {
let layers = []; let layers = [];
let volume = 0; let volume = 0;
for (let index=0; index<layermax; index++) { for (let index=0; index<layermax; index++) {
let layer = collectLayer({ index, widgets, process }); let layer = collectLayer({ index, widgets, device, process });
volume += layer.area * process.slaSlice; volume += layer.area * process.slaSlice;
layers.push(layer); layers.push(layer);
if (progress) progress((index / layermax) * 0.7, "vector_gen"); if (progress) progress((index / layermax) * 0.7, "vector_gen");
@ -49,7 +50,7 @@ function encode(print, progress) {
} }
function collectLayer(params) { function collectLayer(params) {
let { index, widgets, process } = params; let { index, widgets, device, process } = params;
let polygons = []; let polygons = [];
let area = 0; let area = 0;
let z = process.slaFirstOffset + process.slaSlice * index; let z = process.slaFirstOffset + process.slaSlice * index;
@ -74,11 +75,14 @@ function collectLayer(params) {
} }
}); });
let layerMeta = createLayerMetadata(device, process, index, z, area);
return { return {
index, index,
z: round(z), z: layerMeta.z,
area: round(area), area: layerMeta.area,
file: `layers/${index.toString().padStart(6, "0")}.svg`, file: `layers/${index.toString().padStart(6, "0")}.svg`,
process: layerMeta,
polygons polygons
}; };
} }
@ -91,7 +95,8 @@ function createManifest(params) {
z: layer.z, z: layer.z,
area: layer.area, area: layer.area,
file: layer.file, file: layer.file,
empty: layer.polygons.length === 0 empty: layer.polygons.length === 0,
process: layer.process
}; };
}); });
@ -125,18 +130,7 @@ function createManifest(params) {
layerCount: layers.length, layerCount: layers.length,
volume: round(volume), volume: round(volume),
process: { process: {
bottomLayers: process.slaBaseLayers, ...createProcessMetadata(device, process)
exposure: process.slaLayerOn,
bottomExposure: process.slaBaseOn,
lightOffDelay: process.slaLayerOff,
bottomLightOffDelay: process.slaBaseOff,
liftHeight: process.slaPeelDist,
bottomLiftHeight: process.slaBasePeelDist,
liftSpeed: process.slaPeelLiftRate * 60,
bottomLiftSpeed: process.slaBasePeelLiftRate * 60,
retractSpeed: process.slaPeelDropRate * 60,
firstLayerOffset: process.slaFirstOffset,
antiAlias: process.slaAntiAlias
}, },
layers: layerList layers: layerList
}; };
@ -150,9 +144,10 @@ function createLayerSVG(params) {
return ` <path fill="#000" fill-rule="evenodd" d="${polyToPath(poly, device)}"/>`; return ` <path fill="#000" fill-rule="evenodd" d="${polyToPath(poly, device)}"/>`;
}).join("\n"); }).join("\n");
let proc = layer.process;
return [ return [
`<?xml version="1.0" encoding="UTF-8"?>`, `<?xml version="1.0" encoding="UTF-8"?>`,
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="${width}mm" height="${depth}mm" viewBox="0 0 ${width} ${depth}" data-layer="${layer.index}" data-z="${layer.z}">`, `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="${width}mm" height="${depth}mm" viewBox="0 0 ${width} ${depth}" data-layer="${layer.index}" data-z="${layer.z}" data-exposure="${proc.exposure}" data-light-off-delay="${proc.lightOffDelay}" data-light-pwm="${proc.lightPWM}" data-bottom="${proc.bottom}" data-transition="${proc.transition}">`,
body, body,
`</svg>`, `</svg>`,
`` ``
@ -184,10 +179,6 @@ function appendPath(parts, poly, device) {
parts.push("Z"); parts.push("Z");
} }
function round(value) {
return Number.parseFloat(Number(value || 0).toFixed(5));
}
export const VSLA = { export const VSLA = {
encode encode
}; };