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

@ -7,6 +7,8 @@
"bed_height": 1,
"max_height": 300,
"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,
"max_height": 160,
"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,
"max_height": 200,
"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,
family: 'chitubox'
},
ctbe: {
id: 'ctbe',
ext: 'encrypted.ctb',
proprietary: true,
available: false,
priority: 1,
family: 'chitubox-encrypted'
},
goo: {
id: '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 { PNG } from '../../../../ext/pngjs.esm.js';
import { imageToRowMajor, renderRasterLayers, round } from './raster.js';
import { ctbLayerCrypt } from './x_ctb_crypto.js';
const CTB_V3_MAGIC = 0x12fd0086;
const HEADER_SIZE = 0x70;
@ -715,37 +716,11 @@ function requireBytes(input, offset, length) {
}
function decipher(seed, layerIndex, input) {
if (!seed) return input.slice();
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;
return ctbLayerCrypt(seed, layerIndex, input);
}
function encipher(seed, layerIndex, input) {
return decipher(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;
}
};
return ctbLayerCrypt(seed, layerIndex, input);
}
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 */
import { imageToRowMajor, renderRasterLayers, round } from './raster.js';
import { createLayerMetadata, createProcessMetadata, estimatePrintTime, motionFor } from './meta.js';
const FAMILY = "GOO";
const FILE_VERSION = "V3.0";
@ -11,13 +12,6 @@ const HEADER_SIZE = 195477;
const LAYER_DEF_SIZE = 70;
const FOOTER_SIZE = 11;
const MAX_RUN = 0x0fffffff;
const TILT_MOTION = {
liftHeight: 0.05,
liftSpeed: 0.05,
retractHeight: 0,
retractSpeed: 0.05,
motorTime: 4
};
const FORMAT_RECORDS = {
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 { 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 motion = layerMotion(device, process, bottom);
let layerMeta = createLayerMetadata(device, process, index, z, rendered.area);
let raster = imageToRowMajor(rendered.image, width, height, normalizePixel);
accumulatePreview(previewSmall, rendered.image, width, height);
@ -57,22 +51,22 @@ function encodeGOO(print, progress, photon, rec) {
layers.push({
index,
data: encodeRLE(raster),
z: round(z),
z: layerMeta.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: motion.liftHeight,
liftSpeed: motion.liftSpeed,
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: motion.retractHeight,
retractSpeed: motion.retractSpeed,
retractHeight: layerMeta.retractHeight,
retractSpeed: layerMeta.retractSpeed,
retractHeight2: 0,
retractSpeed2: 0,
lightPWM: 255
lightPWM: layerMeta.lightPWM
});
}, progress ? (value) => progress(value * 0.85, `${rec.ext}_encode`) : null);
@ -107,10 +101,9 @@ function writeGOO(params) {
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;
let bottomMotion = layerMotion(device, process, true);
let layerMotionValue = layerMotion(device, process, false);
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);
@ -136,17 +129,17 @@ function writeHeader(writer, params) {
writer.writeF32(device.bedDepth);
writer.writeF32(device.maxHeight || device.bedHeight || 0);
writer.writeF32(process.slaSlice);
writer.writeF32(process.slaLayerOn);
writer.writeF32(meta.exposure);
writer.writeU8(1);
writer.writeF32(0);
writer.writeF32(0);
writer.writeF32(0);
writer.writeF32(bottomOff);
writer.writeF32(meta.bottomWaitBeforeCure);
writer.writeF32(0);
writer.writeF32(0);
writer.writeF32(layerOff);
writer.writeF32(process.slaBaseOn);
writer.writeU32(process.slaBaseLayers || 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);
@ -156,8 +149,8 @@ function writeHeader(writer, params) {
writer.writeF32(layerMotionValue.retractHeight);
writer.writeF32(layerMotionValue.retractSpeed);
for (let i=0; i<8; i++) writer.writeF32(0);
writer.writeU16(255);
writer.writeU16(255);
writer.writeU16(meta.bottomLightPWM);
writer.writeU16(meta.lightPWM);
writer.writeU8(0);
writer.writeU32(estimatePrintTime(device, process, layers.length));
writer.writeF32(round(volume));
@ -197,20 +190,6 @@ function writeLayer(writer, layer) {
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) {
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) {
let sum = 0;
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 { photon } from './x_photon.js';
import { imageToRowMajor, renderRasterLayers, round } from './raster.js';
import { createLayerMetadata, createProcessMetadata } from './meta.js';
const FORMAT = "rsla";
const MIME = "application/vnd.gridspace.rsla+zip";
@ -14,14 +15,17 @@ function encode(print, progress) {
let { ctx, volume } = renderRasterLayers(print, photon, 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 layer = {
index,
z: round(process.slaFirstOffset + process.slaSlice * index),
area: round(rendered.area),
file
z: layerMeta.z,
area: layerMeta.area,
file,
process: layerMeta
};
layers.push(layer);
@ -79,18 +83,7 @@ function createManifest(params) {
black: "off"
},
process: {
bottomLayers: process.slaBaseLayers,
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
...createProcessMetadata(device, process)
},
layers
};

View file

@ -1,6 +1,7 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { JSZip } from '../../../../ext/jszip-esm.js';
import { createLayerMetadata, createProcessMetadata, round } from './meta.js';
const FORMAT = "vsla";
const MIME = "application/vnd.gridspace.vsla+zip";
@ -18,7 +19,7 @@ function encode(print, progress) {
let layers = [];
let volume = 0;
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;
layers.push(layer);
if (progress) progress((index / layermax) * 0.7, "vector_gen");
@ -49,7 +50,7 @@ function encode(print, progress) {
}
function collectLayer(params) {
let { index, widgets, process } = params;
let { index, widgets, device, process } = params;
let polygons = [];
let area = 0;
let z = process.slaFirstOffset + process.slaSlice * index;
@ -74,11 +75,14 @@ function collectLayer(params) {
}
});
let layerMeta = createLayerMetadata(device, process, index, z, area);
return {
index,
z: round(z),
area: round(area),
z: layerMeta.z,
area: layerMeta.area,
file: `layers/${index.toString().padStart(6, "0")}.svg`,
process: layerMeta,
polygons
};
}
@ -91,7 +95,8 @@ function createManifest(params) {
z: layer.z,
area: layer.area,
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,
volume: round(volume),
process: {
bottomLayers: process.slaBaseLayers,
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
...createProcessMetadata(device, process)
},
layers: layerList
};
@ -150,9 +144,10 @@ function createLayerSVG(params) {
return ` <path fill="#000" fill-rule="evenodd" d="${polyToPath(poly, device)}"/>`;
}).join("\n");
let proc = layer.process;
return [
`<?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,
`</svg>`,
``
@ -184,10 +179,6 @@ function appendPath(parts, poly, device) {
parts.push("Z");
}
function round(value) {
return Number.parseFloat(Number(value || 0).toFixed(5));
}
export const VSLA = {
encode
};