add sla .ctb format and a few related printer targets
This commit is contained in:
parent
95eadc60df
commit
a1af70b496
11 changed files with 937 additions and 11 deletions
39
bin/ctb2rsla.mjs
Executable file
39
bin/ctb2rsla.mjs
Executable file
|
|
@ -0,0 +1,39 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { CTB } from '../src/kiri/mode/sla/work/x_ctb.js';
|
||||
|
||||
let [,, input, output] = process.argv;
|
||||
|
||||
if (!input) {
|
||||
console.error("usage: node bin/ctb2rsla.mjs input.ctb [output.rsla]");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
output = output || input.replace(/\.ctb$/i, "") + ".rsla";
|
||||
|
||||
let data = await fs.readFile(input);
|
||||
let ctb = CTB.read(data);
|
||||
let { header, machine } = ctb;
|
||||
let lastPercent = -1;
|
||||
|
||||
console.log([
|
||||
"ctb",
|
||||
machine || "unknown-machine",
|
||||
`${header.resolutionX}x${header.resolutionY}`,
|
||||
`${header.layerCount} layers`,
|
||||
`${path.basename(output)}`
|
||||
].join(" "));
|
||||
|
||||
let file = await CTB.toRSLA(data, (progress, message) => {
|
||||
let percent = Math.floor(progress * 100);
|
||||
if (percent !== lastPercent) {
|
||||
lastPercent = percent;
|
||||
process.stderr.write(`\r${message} ${percent}%`);
|
||||
}
|
||||
});
|
||||
|
||||
if (lastPercent >= 0) process.stderr.write("\n");
|
||||
await fs.writeFile(output, Buffer.from(file));
|
||||
console.log(`wrote ${output}`);
|
||||
|
|
@ -102,6 +102,7 @@ function device_from_code(code,mode) {
|
|||
slaFormat: valueOf(set.sla_format, ''),
|
||||
slaFileExt: valueOf(set.sla_file_ext, ''),
|
||||
slaFormatVersion: valueOf(set.sla_format_version, 1),
|
||||
slaMachineEditable: valueOf(set.sla_machine_editable, false),
|
||||
deviceZMax: valueOf(set.z_move_max, 0),
|
||||
gcodeTime: valueOf(set.time_factor, 1),
|
||||
maxHeight: valueOf(set.build_height, 150),
|
||||
|
|
@ -373,7 +374,8 @@ export const conf = {
|
|||
resolutionY: 900,
|
||||
slaFormat: "",
|
||||
slaFileExt: "",
|
||||
slaFormatVersion: 1
|
||||
slaFormatVersion: 1,
|
||||
slaMachineEditable: false
|
||||
},
|
||||
// process defaults SLA:Process
|
||||
p:{
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { $, h } from '../../moto/webui.js';
|
||||
import { api } from './api.js';
|
||||
import { conf } from './conf/defaults.js';
|
||||
import { canEditSLAMachine } from '../mode/sla/core/formats.js';
|
||||
import { space } from '../../moto/space.js';
|
||||
import { devices as devlist } from '../../pack/kiri-devs.js';
|
||||
import { settings, conf as setconf } from './conf/manager.js';
|
||||
|
|
@ -185,7 +186,7 @@ function setDeviceCode(code, devicename) {
|
|||
}
|
||||
}
|
||||
|
||||
let allowSLADeviceEdit = mode !== 'SLA' || dev.slaFormat === 'vsla' || dev.slaFormat === 'rsla';
|
||||
let allowSLADeviceEdit = mode !== 'SLA' || canEditSLAMachine(dev);
|
||||
|
||||
// disable editing for non-local devices
|
||||
[
|
||||
|
|
|
|||
16
src/kiri/dev/sla/Any.Generic.Chitubox.CTB.json
Normal file
16
src/kiri/dev/sla/Any.Generic.Chitubox.CTB.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"no_clone": false,
|
||||
"cmd": {},
|
||||
"settings": {
|
||||
"bed_width": 192,
|
||||
"bed_depth": 120,
|
||||
"bed_height": 1,
|
||||
"max_height": 250,
|
||||
"resolution_x": 3840,
|
||||
"resolution_y": 2400,
|
||||
"sla_format": "ctb",
|
||||
"sla_file_ext": "ctb",
|
||||
"sla_format_version": 3,
|
||||
"sla_machine_editable": true
|
||||
}
|
||||
}
|
||||
15
src/kiri/dev/sla/Creality.LD-006.json
Normal file
15
src/kiri/dev/sla/Creality.LD-006.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"no_clone": true,
|
||||
"cmd": {},
|
||||
"settings": {
|
||||
"bed_width": 192,
|
||||
"bed_depth": 120,
|
||||
"bed_height": 1,
|
||||
"max_height": 250,
|
||||
"resolution_x": 3840,
|
||||
"resolution_y": 2400,
|
||||
"sla_format": "ctb",
|
||||
"sla_file_ext": "ctb",
|
||||
"sla_format_version": 3
|
||||
}
|
||||
}
|
||||
15
src/kiri/dev/sla/Elegoo.Mars.3.CTB.json
Normal file
15
src/kiri/dev/sla/Elegoo.Mars.3.CTB.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"no_clone": true,
|
||||
"cmd": {},
|
||||
"settings": {
|
||||
"bed_width": 143,
|
||||
"bed_depth": 89.6,
|
||||
"bed_height": 1,
|
||||
"max_height": 175,
|
||||
"resolution_x": 4098,
|
||||
"resolution_y": 2560,
|
||||
"sla_format": "ctb",
|
||||
"sla_file_ext": "ctb",
|
||||
"sla_format_version": 3
|
||||
}
|
||||
}
|
||||
15
src/kiri/dev/sla/Elegoo.Saturn.2.CTB.json
Normal file
15
src/kiri/dev/sla/Elegoo.Saturn.2.CTB.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"no_clone": true,
|
||||
"cmd": {},
|
||||
"settings": {
|
||||
"bed_width": 218.88,
|
||||
"bed_depth": 123.12,
|
||||
"bed_height": 1,
|
||||
"max_height": 250,
|
||||
"resolution_x": 7680,
|
||||
"resolution_y": 4320,
|
||||
"sla_format": "ctb",
|
||||
"sla_file_ext": "ctb",
|
||||
"sla_format_version": 3
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { $ } from '../../../../moto/webui.js';
|
||||
import { api } from '../../../app/api.js';
|
||||
import { getSLAFileExt } from '../core/formats.js';
|
||||
|
||||
export const SLA = {
|
||||
init,
|
||||
|
|
@ -63,11 +64,7 @@ export function printDownload(output, api, names) {
|
|||
$('print-layers').value = layers;
|
||||
$('print-time').value = `${print_hrs}:${print_min}:${print_sec}`;
|
||||
|
||||
let ext = device.slaFileExt || ({
|
||||
'Anycubic.Photon': 'photon',
|
||||
'Anycubic.Photon.S': 'photons'
|
||||
}[device.deviceName]) || 'cxdlp';
|
||||
|
||||
let ext = getSLAFileExt(device);
|
||||
ext = ext.startsWith(".") ? ext : `.${ext}`;
|
||||
download.innerText = `${api.language.current.ex_dwnl || 'download'} ${ext}`;
|
||||
download.onclick = () => { saveFile(api, file, ext) };
|
||||
|
|
|
|||
68
src/kiri/mode/sla/core/formats.js
Normal file
68
src/kiri/mode/sla/core/formats.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
const DEVICE_FORMAT = {
|
||||
'Anycubic.Photon': 'photon',
|
||||
'Anycubic.Photon.S': 'photons'
|
||||
};
|
||||
|
||||
export const SLA_FORMATS = {
|
||||
photon: {
|
||||
id: 'photon',
|
||||
ext: 'photon',
|
||||
proprietary: true,
|
||||
available: true
|
||||
},
|
||||
photons: {
|
||||
id: 'photons',
|
||||
ext: 'photons',
|
||||
proprietary: true,
|
||||
available: true
|
||||
},
|
||||
cxdlp: {
|
||||
id: 'cxdlp',
|
||||
ext: 'cxdlp',
|
||||
proprietary: true,
|
||||
available: true
|
||||
},
|
||||
vsla: {
|
||||
id: 'vsla',
|
||||
ext: 'vsla',
|
||||
proprietary: false,
|
||||
available: true,
|
||||
editableMachine: true
|
||||
},
|
||||
rsla: {
|
||||
id: 'rsla',
|
||||
ext: 'rsla',
|
||||
proprietary: false,
|
||||
available: true,
|
||||
editableMachine: true
|
||||
},
|
||||
ctb: {
|
||||
id: 'ctb',
|
||||
ext: 'ctb',
|
||||
proprietary: true,
|
||||
available: true,
|
||||
priority: 1,
|
||||
family: 'chitubox'
|
||||
}
|
||||
};
|
||||
|
||||
export function getSLAFormat(device) {
|
||||
return device.slaFormat || DEVICE_FORMAT[device.deviceName] || 'cxdlp';
|
||||
}
|
||||
|
||||
export function getSLAFormatRecord(device) {
|
||||
let id = getSLAFormat(device);
|
||||
return SLA_FORMATS[id] || SLA_FORMATS.cxdlp;
|
||||
}
|
||||
|
||||
export function getSLAFileExt(device) {
|
||||
let rec = getSLAFormatRecord(device);
|
||||
return device.slaFileExt || rec.ext || rec.id;
|
||||
}
|
||||
|
||||
export function canEditSLAMachine(device) {
|
||||
return device.slaMachineEditable === true ||
|
||||
getSLAFormatRecord(device).editableMachine === true;
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { CXDLP } from './x_cxdlp.js';
|
||||
import { CTB } from './x_ctb.js';
|
||||
import { photon } from './x_photon.js';
|
||||
import { SLA } from './init-work.js';
|
||||
import { RSLA } from './x_rsla.js';
|
||||
import { VSLA } from './x_vsla.js';
|
||||
import { getSLAFormat } from '../core/formats.js';
|
||||
|
||||
/**
|
||||
* DRIVER CONTRACT - runs in worker
|
||||
|
|
@ -35,10 +37,7 @@ export function sla_export(print, online, ondone) {
|
|||
layermax = Math.max(widget.slices.length);
|
||||
});
|
||||
|
||||
let format = device.slaFormat || ({
|
||||
'Anycubic.Photon': 'photon',
|
||||
'Anycubic.Photon.S': 'photons',
|
||||
}[device.deviceName]) || 'cxdlp';
|
||||
let format = getSLAFormat(device);
|
||||
|
||||
if (format === 'vsla') {
|
||||
return VSLA.encode(print, (progress, message) => {
|
||||
|
|
@ -58,6 +57,15 @@ export function sla_export(print, online, ondone) {
|
|||
});
|
||||
}
|
||||
|
||||
if (format === 'ctb') {
|
||||
return CTB.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,
|
||||
|
|
|
|||
750
src/kiri/mode/sla/work/x_ctb.js
Normal file
750
src/kiri/mode/sla/work/x_ctb.js
Normal file
|
|
@ -0,0 +1,750 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { JSZip } from '../../../../ext/jszip-esm.js';
|
||||
import { PNG } from '../../../../ext/pngjs.esm.js';
|
||||
|
||||
const CTB_V3_MAGIC = 0x12fd0086;
|
||||
const HEADER_SIZE = 0x70;
|
||||
const LAYER_TABLE_RECORD_SIZE = 36;
|
||||
const LAYER_FULL_RECORD_SIZE = 84;
|
||||
const PRINT_PARAMS_SIZE = 60;
|
||||
const MACHINE_INFO_SIZE = 76;
|
||||
const RSLA_MIME = "application/vnd.gridspace.rsla+zip";
|
||||
|
||||
function encode(print, progress, renderer) {
|
||||
if (renderer) {
|
||||
return Promise.resolve(encodeWithPhoton(print, progress, renderer));
|
||||
}
|
||||
return import('./x_photon.js').then(({ photon }) =>
|
||||
encodeWithPhoton(print, progress, photon));
|
||||
}
|
||||
|
||||
function encodeWithPhoton(print, progress, photon) {
|
||||
let { settings, widgets } = print;
|
||||
let { device, process } = settings;
|
||||
let width = device.resolutionX;
|
||||
let height = device.resolutionY;
|
||||
let scaleX = width / device.bedWidth;
|
||||
let scaleY = height / device.bedDepth;
|
||||
let layermax = 0;
|
||||
let layers = [];
|
||||
let volume = 0;
|
||||
let seed = makeSeed(print);
|
||||
|
||||
widgets = widgets.filter(w => !w.track.ignore && !w.meta.disabled);
|
||||
widgets.forEach(widget => {
|
||||
layermax = Math.max(layermax, widget.slices.length);
|
||||
});
|
||||
|
||||
for (let index=0; index<layermax; index++) {
|
||||
let rendered = photon.renderLayerWasm({
|
||||
index,
|
||||
width,
|
||||
height,
|
||||
widgets,
|
||||
scaleX,
|
||||
scaleY,
|
||||
masks: []
|
||||
});
|
||||
if (rendered.end) {
|
||||
break;
|
||||
}
|
||||
|
||||
let pixels = renderToCTBPixels(rendered.image, width, height);
|
||||
let encoded = encipher(seed, index, encodeRLE(pixels));
|
||||
let bottom = index < process.slaBaseLayers;
|
||||
|
||||
volume += rendered.area * process.slaSlice;
|
||||
layers.push({
|
||||
index,
|
||||
z: process.slaFirstOffset + process.slaSlice * (index + 1),
|
||||
exposure: bottom ? process.slaBaseOn : process.slaLayerOn,
|
||||
lightOffDelay: bottom ? process.slaBaseOff : process.slaLayerOff,
|
||||
liftDistance: bottom ? process.slaBasePeelDist : process.slaPeelDist,
|
||||
liftSpeed: (bottom ? process.slaBasePeelLiftRate : process.slaPeelLiftRate) * 60,
|
||||
data: encoded
|
||||
});
|
||||
|
||||
if (progress) progress((index / layermax) * 0.85, "ctb_encode");
|
||||
}
|
||||
|
||||
let file = writeCTB({ device, process, layers, volume, seed });
|
||||
if (progress) progress(1, "ctb_write");
|
||||
|
||||
return { file, layers: layers.length, volume };
|
||||
}
|
||||
|
||||
function read(input) {
|
||||
let view = input instanceof DataView
|
||||
? input
|
||||
: new DataView(input.buffer || input, input.byteOffset || 0, input.byteLength);
|
||||
|
||||
let header = readHeader(view);
|
||||
let layers = readLayerTable(view, header);
|
||||
|
||||
return {
|
||||
header,
|
||||
print: readPrintParams(view, header),
|
||||
machine: readMachineName(view, header),
|
||||
layers,
|
||||
layout: inferLayout(header, layers)
|
||||
};
|
||||
}
|
||||
|
||||
function toRSLA(input, progress) {
|
||||
let view = toDataView(input);
|
||||
let ctb = read(view);
|
||||
let { header, layers } = ctb;
|
||||
let zip = new JSZip();
|
||||
let rslaLayers = [];
|
||||
|
||||
layers.forEach((layer, index) => {
|
||||
let pixels = decodeLayer(view, header, layer);
|
||||
let file = `layers/${index.toString().padStart(6, "0")}.png`;
|
||||
zip.file(file, imageToPNG(pixels, header.resolutionX, header.resolutionY));
|
||||
rslaLayers.push({
|
||||
index,
|
||||
z: round(layer.z),
|
||||
exposure: round(layer.exposure),
|
||||
lightOffDelay: round(layer.lightOffDelay),
|
||||
source: {
|
||||
offset: layer.imageOffset,
|
||||
length: layer.imageLength
|
||||
},
|
||||
file
|
||||
});
|
||||
if (progress) progress((index / layers.length) * 0.85, "ctb_decode");
|
||||
});
|
||||
|
||||
zip.file("manifest.json", JSON.stringify(createRSLAManifest(ctb, rslaLayers), null, 2));
|
||||
|
||||
return zip.generateAsync({
|
||||
type: "arraybuffer",
|
||||
compression: "DEFLATE",
|
||||
compressionOptions: { level: 6 },
|
||||
streamFiles: true,
|
||||
mimeType: RSLA_MIME
|
||||
}, meta => {
|
||||
if (progress) progress(0.85 + (meta.percent / 100) * 0.15, "zip_gen");
|
||||
});
|
||||
}
|
||||
|
||||
function decodeLayer(view, header, layer) {
|
||||
let data = new Uint8Array(
|
||||
view.buffer,
|
||||
view.byteOffset + layer.imageOffset,
|
||||
layer.imageLength
|
||||
);
|
||||
let decoded = decodeRLE(decipher(header.encryptionSeed, layer.index, data),
|
||||
header.resolutionX * header.resolutionY);
|
||||
return decoded;
|
||||
}
|
||||
|
||||
function writeCTB(params) {
|
||||
let { device, process, layers, volume, seed } = params;
|
||||
let machine = device.deviceName || "Generic CTB";
|
||||
let machineBytes = new TextEncoder().encode(machine);
|
||||
let printTime = Math.round((process.slaBaseLayers * process.slaBaseOn) +
|
||||
Math.max(0, layers.length - process.slaBaseLayers) * process.slaLayerOn);
|
||||
let printParamsOffset = HEADER_SIZE;
|
||||
let machineInfoOffset = printParamsOffset + PRINT_PARAMS_SIZE;
|
||||
let nameOffset = machineInfoOffset + MACHINE_INFO_SIZE;
|
||||
let layerTableOffset = nameOffset + machineBytes.length;
|
||||
let imageOffset = layerTableOffset + layers.length * LAYER_TABLE_RECORD_SIZE;
|
||||
|
||||
layers.forEach(layer => {
|
||||
layer.fullOffset = imageOffset;
|
||||
layer.imageOffset = imageOffset + LAYER_FULL_RECORD_SIZE;
|
||||
layer.imageLength = layer.data.length;
|
||||
layer.blockLength = LAYER_FULL_RECORD_SIZE + layer.imageLength;
|
||||
imageOffset = layer.imageOffset + layer.imageLength;
|
||||
});
|
||||
|
||||
let buffer = new ArrayBuffer(imageOffset);
|
||||
let writer = new BinaryWriter(buffer);
|
||||
|
||||
writeHeader(writer, {
|
||||
device,
|
||||
process,
|
||||
layers,
|
||||
seed,
|
||||
printTime,
|
||||
printParamsOffset,
|
||||
machineInfoOffset,
|
||||
layerTableOffset
|
||||
});
|
||||
writer.seek(printParamsOffset);
|
||||
writePrintParams(writer, { process, volume });
|
||||
writer.seek(machineInfoOffset);
|
||||
writeMachineInfo(writer, { machineBytes, nameOffset });
|
||||
writer.seek(nameOffset);
|
||||
writer.writeBytes(machineBytes);
|
||||
writer.seek(layerTableOffset);
|
||||
layers.forEach(layer => writeLayerRecord(writer, layer));
|
||||
layers.forEach(layer => {
|
||||
writer.seek(layer.fullOffset);
|
||||
writeFullLayerRecord(writer, layer);
|
||||
writer.writeBytes(layer.data);
|
||||
});
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
function writeHeader(writer, params) {
|
||||
let {
|
||||
device, process, layers, seed, printTime,
|
||||
printParamsOffset, machineInfoOffset, layerTableOffset
|
||||
} = params;
|
||||
|
||||
writer.writeU32(CTB_V3_MAGIC);
|
||||
writer.writeU32(3);
|
||||
writer.writeF32(device.bedWidth);
|
||||
writer.writeF32(device.bedDepth);
|
||||
writer.writeF32(device.maxHeight || device.bedHeight);
|
||||
writer.skip(12);
|
||||
writer.writeF32(process.slaSlice);
|
||||
writer.writeF32(process.slaLayerOn);
|
||||
writer.writeF32(process.slaBaseOn);
|
||||
writer.writeF32(process.slaLayerOff);
|
||||
writer.writeU32(process.slaBaseLayers);
|
||||
writer.writeU32(device.resolutionX);
|
||||
writer.writeU32(device.resolutionY);
|
||||
writer.writeU32(0);
|
||||
writer.writeU32(layerTableOffset);
|
||||
writer.writeU32(layers.length);
|
||||
writer.writeU32(0);
|
||||
writer.writeU32(printTime);
|
||||
writer.writeU32(1);
|
||||
writer.writeU32(printParamsOffset);
|
||||
writer.writeU32(PRINT_PARAMS_SIZE);
|
||||
writer.writeU32(process.slaAntiAlias || 1);
|
||||
writer.writeU16(0x00ff);
|
||||
writer.writeU16(0x00ff);
|
||||
writer.writeU32(seed);
|
||||
writer.writeU32(machineInfoOffset);
|
||||
writer.writeU32(MACHINE_INFO_SIZE);
|
||||
}
|
||||
|
||||
function writePrintParams(writer, params) {
|
||||
let { process, volume } = params;
|
||||
|
||||
writer.writeF32(process.slaBasePeelDist);
|
||||
writer.writeF32(process.slaBasePeelLiftRate * 60);
|
||||
writer.writeF32(process.slaPeelDist);
|
||||
writer.writeF32(process.slaPeelLiftRate * 60);
|
||||
writer.writeF32(process.slaPeelDropRate * 60);
|
||||
writer.writeF32(volume);
|
||||
writer.writeF32(0);
|
||||
writer.writeF32(0);
|
||||
writer.writeF32(process.slaBaseOff);
|
||||
writer.writeF32(process.slaLayerOff);
|
||||
writer.writeU32(process.slaBaseLayers);
|
||||
writer.writeU32(0);
|
||||
writer.writeU32(0);
|
||||
writer.writeU32(0);
|
||||
writer.writeU32(0);
|
||||
}
|
||||
|
||||
function writeMachineInfo(writer, params) {
|
||||
let { machineBytes, nameOffset } = params;
|
||||
|
||||
writer.skip(0x1c);
|
||||
writer.writeU32(nameOffset);
|
||||
writer.writeU32(machineBytes.length);
|
||||
writer.writeU32(machineBytes.length);
|
||||
writer.writeU32(0x2000000f);
|
||||
writer.writeU32(0x01c45dd1);
|
||||
writer.writeU32(8);
|
||||
writer.writeU32(0x01080000);
|
||||
writer.writeF32(6.8);
|
||||
writer.writeU32(0);
|
||||
writer.writeU32(10);
|
||||
writer.writeU32(0);
|
||||
writer.writeU32(0);
|
||||
writer.writeU32(0);
|
||||
}
|
||||
|
||||
function writeLayerRecord(writer, layer) {
|
||||
writer.writeF32(layer.z);
|
||||
writer.writeF32(layer.exposure);
|
||||
writer.writeF32(layer.lightOffDelay);
|
||||
writer.writeU32(layer.imageOffset);
|
||||
writer.writeU32(layer.imageLength);
|
||||
writer.writeU32(0);
|
||||
writer.writeU32(LAYER_FULL_RECORD_SIZE);
|
||||
writer.writeU32(0);
|
||||
writer.writeU32(0);
|
||||
}
|
||||
|
||||
function writeFullLayerRecord(writer, layer) {
|
||||
writeLayerRecord(writer, layer);
|
||||
writer.writeU32(layer.blockLength);
|
||||
writer.writeF32(layer.liftDistance);
|
||||
writer.writeF32(layer.liftSpeed);
|
||||
writer.writeU32(0);
|
||||
writer.writeU32(0);
|
||||
writer.writeU32(0);
|
||||
writer.writeU32(0);
|
||||
writer.writeU32(0);
|
||||
writer.writeU32(0);
|
||||
writer.writeU32(0);
|
||||
writer.writeF32(0);
|
||||
writer.writeF32(0x00ff);
|
||||
}
|
||||
|
||||
function readHeader(view) {
|
||||
let magic = view.getUint32(0, true);
|
||||
let version = view.getUint32(4, true);
|
||||
if (magic !== CTB_V3_MAGIC) {
|
||||
throw new Error(`invalid CTB magic 0x${magic.toString(16)}`);
|
||||
}
|
||||
if (version !== 3) {
|
||||
throw new Error(`unsupported CTB version ${version}`);
|
||||
}
|
||||
|
||||
return {
|
||||
magic,
|
||||
version,
|
||||
bedWidth: view.getFloat32(0x08, true),
|
||||
bedDepth: view.getFloat32(0x0c, true),
|
||||
bedHeight: view.getFloat32(0x10, true),
|
||||
layerHeight: view.getFloat32(0x20, true),
|
||||
exposure: view.getFloat32(0x24, true),
|
||||
bottomExposure: view.getFloat32(0x28, true),
|
||||
lightOffDelay: view.getFloat32(0x2c, true),
|
||||
bottomLayers: view.getUint32(0x30, true),
|
||||
resolutionX: view.getUint32(0x34, true),
|
||||
resolutionY: view.getUint32(0x38, true),
|
||||
previewOffset: view.getUint32(0x3c, true),
|
||||
layerTableOffset: view.getUint32(0x40, true),
|
||||
layerCount: view.getUint32(0x44, true),
|
||||
previewOneLength: view.getUint32(0x48, true),
|
||||
printParamsOffset: view.getUint32(0x54, true),
|
||||
printParamsLength: view.getUint32(0x58, true),
|
||||
antiAliasLevel: view.getUint32(0x5c, true),
|
||||
lightPWM: view.getUint16(0x60, true),
|
||||
bottomLightPWM: view.getUint16(0x62, true),
|
||||
encryptionSeed: view.getUint32(0x64, true),
|
||||
machineInfoOffset: view.getUint32(0x68, true),
|
||||
machineInfoLength: view.getUint32(0x6c, true)
|
||||
};
|
||||
}
|
||||
|
||||
function readLayerTable(view, header) {
|
||||
let layers = [];
|
||||
let offset = header.layerTableOffset;
|
||||
|
||||
for (let index=0; index<header.layerCount; index++) {
|
||||
layers.push(readLayerRecord(view, header, offset, index));
|
||||
offset += LAYER_TABLE_RECORD_SIZE;
|
||||
}
|
||||
|
||||
return layers;
|
||||
}
|
||||
|
||||
function readLayerRecord(view, header, offset, index) {
|
||||
let record = {
|
||||
index,
|
||||
tableOffset: offset,
|
||||
z: view.getFloat32(offset + 0, true),
|
||||
exposure: view.getFloat32(offset + 4, true),
|
||||
lightOffDelay: view.getFloat32(offset + 8, true),
|
||||
imageOffset: view.getUint32(offset + 12, true),
|
||||
imageLength: view.getUint32(offset + 16, true),
|
||||
unknown20: view.getUint32(offset + 20, true),
|
||||
fullRecordLength: view.getUint32(offset + 24, true),
|
||||
unknown28: view.getUint32(offset + 28, true),
|
||||
unknown32: view.getUint32(offset + 32, true)
|
||||
};
|
||||
let fullOffset = record.imageOffset - LAYER_FULL_RECORD_SIZE;
|
||||
if (fullOffset >= header.layerTableOffset) {
|
||||
record.full = readFullLayerRecord(view, fullOffset);
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
function readFullLayerRecord(view, offset) {
|
||||
return {
|
||||
offset,
|
||||
z: view.getFloat32(offset + 0, true),
|
||||
exposure: view.getFloat32(offset + 4, true),
|
||||
lightOffDelay: view.getFloat32(offset + 8, true),
|
||||
imageOffset: view.getUint32(offset + 12, true),
|
||||
imageLength: view.getUint32(offset + 16, true),
|
||||
unknown20: view.getUint32(offset + 20, true),
|
||||
fullRecordLength: view.getUint32(offset + 24, true),
|
||||
unknown28: view.getUint32(offset + 28, true),
|
||||
unknown32: view.getUint32(offset + 32, true),
|
||||
blockLength: view.getUint32(offset + 36, true),
|
||||
liftDistance: view.getFloat32(offset + 40, true),
|
||||
liftSpeed: view.getFloat32(offset + 44, true),
|
||||
unknown48: view.getUint32(offset + 48, true),
|
||||
unknown52: view.getUint32(offset + 52, true),
|
||||
unknown56: view.getUint32(offset + 56, true),
|
||||
unknown60: view.getUint32(offset + 60, true),
|
||||
unknown64: view.getUint32(offset + 64, true),
|
||||
unknown68: view.getUint32(offset + 68, true),
|
||||
unknown72: view.getUint32(offset + 72, true),
|
||||
unknown76: view.getFloat32(offset + 76, true),
|
||||
lightPWM: view.getFloat32(offset + 80, true)
|
||||
};
|
||||
}
|
||||
|
||||
function readPrintParams(view, header) {
|
||||
let offset = header.printParamsOffset;
|
||||
if (!offset || header.printParamsLength < 60) return {};
|
||||
|
||||
return {
|
||||
bottomLiftDistance: view.getFloat32(offset + 0, true),
|
||||
bottomLiftSpeed: view.getFloat32(offset + 4, true),
|
||||
liftDistance: view.getFloat32(offset + 8, true),
|
||||
liftSpeed: view.getFloat32(offset + 12, true),
|
||||
retractSpeed: view.getFloat32(offset + 16, true),
|
||||
volume: view.getFloat32(offset + 20, true),
|
||||
weight: view.getFloat32(offset + 24, true),
|
||||
cost: view.getFloat32(offset + 28, true),
|
||||
bottomLightOffDelay: view.getFloat32(offset + 32, true),
|
||||
lightOffDelay: view.getFloat32(offset + 36, true),
|
||||
bottomLayers: view.getUint32(offset + 40, true),
|
||||
unknown44: view.getUint32(offset + 44, true),
|
||||
unknown48: view.getUint32(offset + 48, true),
|
||||
unknown52: view.getUint32(offset + 52, true),
|
||||
unknown56: view.getUint32(offset + 56, true)
|
||||
};
|
||||
}
|
||||
|
||||
function readMachineName(view, header) {
|
||||
if (!header.machineInfoOffset || !header.machineInfoLength) return "";
|
||||
|
||||
let nameOffset = view.getUint32(header.machineInfoOffset + 0x1c, true);
|
||||
let nameLength = view.getUint32(header.machineInfoOffset + 0x20, true);
|
||||
if (!nameOffset || !nameLength) return "";
|
||||
|
||||
let chars = [];
|
||||
for (let i=0; i<nameLength; i++) {
|
||||
let c = view.getUint8(nameOffset + i);
|
||||
if (c === 0) break;
|
||||
chars.push(String.fromCharCode(c));
|
||||
}
|
||||
return chars.join("");
|
||||
}
|
||||
|
||||
function inferLayout(header, layers) {
|
||||
let tableEnd = header.layerTableOffset + header.layerCount * LAYER_TABLE_RECORD_SIZE;
|
||||
let firstLayer = layers[0];
|
||||
let firstFullRecordOffset = firstLayer ? firstLayer.imageOffset - LAYER_FULL_RECORD_SIZE : 0;
|
||||
|
||||
return {
|
||||
headerSize: HEADER_SIZE,
|
||||
layerTableRecordSize: LAYER_TABLE_RECORD_SIZE,
|
||||
layerFullRecordSize: LAYER_FULL_RECORD_SIZE,
|
||||
tableEnd,
|
||||
firstFullRecordOffset,
|
||||
hasFullRecordsBeforeImages: firstFullRecordOffset === tableEnd
|
||||
};
|
||||
}
|
||||
|
||||
function createRSLAManifest(ctb, layers) {
|
||||
let { header, print, machine } = ctb;
|
||||
|
||||
return {
|
||||
format: "rsla",
|
||||
version: 1,
|
||||
units: "mm",
|
||||
coordinateSystem: {
|
||||
layerImage: "top-left",
|
||||
model: "bed-center",
|
||||
x: "right",
|
||||
y: "back",
|
||||
z: "up"
|
||||
},
|
||||
source: {
|
||||
format: "ctb",
|
||||
version: header.version,
|
||||
machine
|
||||
},
|
||||
bed: {
|
||||
width: round(header.bedWidth),
|
||||
depth: round(header.bedDepth),
|
||||
height: round(header.bedHeight)
|
||||
},
|
||||
resolution: {
|
||||
x: header.resolutionX,
|
||||
y: header.resolutionY
|
||||
},
|
||||
layerHeight: round(header.layerHeight),
|
||||
layerCount: layers.length,
|
||||
raster: {
|
||||
encoding: "png",
|
||||
color: "grayscale",
|
||||
cure: "nonzero",
|
||||
white: "cure",
|
||||
black: "off"
|
||||
},
|
||||
process: {
|
||||
bottomLayers: print.bottomLayers ?? header.bottomLayers,
|
||||
exposure: round(header.exposure),
|
||||
bottomExposure: round(header.bottomExposure),
|
||||
lightOffDelay: round(print.lightOffDelay ?? header.lightOffDelay),
|
||||
bottomLightOffDelay: round(print.bottomLightOffDelay),
|
||||
liftHeight: round(print.liftDistance),
|
||||
bottomLiftHeight: round(print.bottomLiftDistance),
|
||||
liftSpeed: round(print.liftSpeed),
|
||||
bottomLiftSpeed: round(print.bottomLiftSpeed),
|
||||
retractSpeed: round(print.retractSpeed),
|
||||
antiAlias: header.antiAliasLevel,
|
||||
lightPWM: header.lightPWM,
|
||||
bottomLightPWM: header.bottomLightPWM
|
||||
},
|
||||
layers
|
||||
};
|
||||
}
|
||||
|
||||
function renderToCTBPixels(image, width, height) {
|
||||
let data = new Uint8Array(width * height);
|
||||
for (let x=0; x<width; x++) {
|
||||
for (let y=0; y<height; y++) {
|
||||
data[y * width + x] = image[x * height + y] || 0;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function encodeRLE(input) {
|
||||
let output = [];
|
||||
let color = quantize(input[0]);
|
||||
let run = 1;
|
||||
|
||||
for (let offset=1; offset<input.length; offset++) {
|
||||
let next = quantize(input[offset]);
|
||||
if (next === color) {
|
||||
run++;
|
||||
continue;
|
||||
}
|
||||
writeRun(output, color, run);
|
||||
color = next;
|
||||
run = 1;
|
||||
}
|
||||
|
||||
writeRun(output, color, run);
|
||||
return new Uint8Array(output);
|
||||
}
|
||||
|
||||
function writeRun(output, color, length) {
|
||||
if (length === 1) {
|
||||
output.push(color);
|
||||
return;
|
||||
}
|
||||
|
||||
output.push(color | 0x80);
|
||||
if (length < 0x80) {
|
||||
output.push(length);
|
||||
} else if (length < 0x4000) {
|
||||
output.push(0x80 | (length >> 8), length & 0xff);
|
||||
} else if (length < 0x200000) {
|
||||
output.push(0xc0 | (length >> 16), (length >> 8) & 0xff, length & 0xff);
|
||||
} else if (length < 0x10000000) {
|
||||
output.push(
|
||||
0xe0 | (length >> 24),
|
||||
(length >> 16) & 0xff,
|
||||
(length >> 8) & 0xff,
|
||||
length & 0xff
|
||||
);
|
||||
} else {
|
||||
throw new Error(`CTB RLE run too long (${length})`);
|
||||
}
|
||||
}
|
||||
|
||||
function quantize(value) {
|
||||
return value ? Math.max(1, Math.min(0x7f, Math.round(value / 2))) : 0;
|
||||
}
|
||||
|
||||
function decodeRLE(input, pixelCount) {
|
||||
let output = new Uint8Array(pixelCount);
|
||||
let offset = 0;
|
||||
let out = 0;
|
||||
|
||||
while (offset < input.length && out < pixelCount) {
|
||||
let code = input[offset++];
|
||||
let repeat = (code & 0x80) !== 0;
|
||||
code &= 0x7f;
|
||||
|
||||
let stride = repeat ? readRunLength(input, offset) : 1;
|
||||
if (repeat) offset += stride.bytes;
|
||||
|
||||
let value = code === 0 ? 0 : (code << 1) | 1;
|
||||
let end = out + (repeat ? stride.length : stride);
|
||||
if (end > pixelCount) {
|
||||
throw new Error(`CTB RLE overrun in decoded layer (${end} > ${pixelCount})`);
|
||||
}
|
||||
output.fill(value, out, end);
|
||||
out = end;
|
||||
}
|
||||
|
||||
if (out !== pixelCount) {
|
||||
throw new Error(`CTB RLE underrun in decoded layer (${out} < ${pixelCount})`);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function readRunLength(input, offset) {
|
||||
let slen = input[offset];
|
||||
if (slen === undefined) throw new Error("truncated CTB RLE run length");
|
||||
|
||||
if ((slen & 0x80) === 0) {
|
||||
return { length: slen, bytes: 1 };
|
||||
}
|
||||
if ((slen & 0xc0) === 0x80) {
|
||||
requireBytes(input, offset, 2);
|
||||
return {
|
||||
length: ((slen & 0x3f) << 8) | input[offset + 1],
|
||||
bytes: 2
|
||||
};
|
||||
}
|
||||
if ((slen & 0xe0) === 0xc0) {
|
||||
requireBytes(input, offset, 3);
|
||||
return {
|
||||
length: ((slen & 0x1f) << 16) | (input[offset + 1] << 8) | input[offset + 2],
|
||||
bytes: 3
|
||||
};
|
||||
}
|
||||
if ((slen & 0xf0) === 0xe0) {
|
||||
requireBytes(input, offset, 4);
|
||||
return {
|
||||
length: (((slen & 0x0f) << 24) | (input[offset + 1] << 16) |
|
||||
(input[offset + 2] << 8) | input[offset + 3]) >>> 0,
|
||||
bytes: 4
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`unsupported CTB RLE run length prefix 0x${slen.toString(16)}`);
|
||||
}
|
||||
|
||||
function requireBytes(input, offset, length) {
|
||||
if (offset + length > input.length) {
|
||||
throw new Error("truncated CTB RLE run 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function imageToPNG(image, width, height) {
|
||||
return PNG.sync.write({ width, height, data: image }, {
|
||||
colorType: 0,
|
||||
inputColorType: 0,
|
||||
inputHasAlpha: false
|
||||
});
|
||||
}
|
||||
|
||||
function toDataView(input) {
|
||||
return input instanceof DataView
|
||||
? input
|
||||
: new DataView(input.buffer || input, input.byteOffset || 0, input.byteLength);
|
||||
}
|
||||
|
||||
function round(value) {
|
||||
return Number.parseFloat(Number(value || 0).toFixed(5));
|
||||
}
|
||||
|
||||
function makeSeed(print) {
|
||||
let { device, process } = print.settings;
|
||||
let hash = 0x811c9dc5;
|
||||
let text = [
|
||||
device.deviceName,
|
||||
device.resolutionX,
|
||||
device.resolutionY,
|
||||
device.bedWidth,
|
||||
device.bedDepth,
|
||||
process.slaSlice,
|
||||
process.slaLayerOn,
|
||||
process.slaBaseOn
|
||||
].join("|");
|
||||
|
||||
for (let i=0; i<text.length; i++) {
|
||||
hash ^= text.charCodeAt(i);
|
||||
hash = Math.imul(hash, 0x01000193) >>> 0;
|
||||
}
|
||||
|
||||
return hash || 0x5eed1234;
|
||||
}
|
||||
|
||||
class BinaryWriter {
|
||||
constructor(buffer) {
|
||||
this.view = new DataView(buffer);
|
||||
this.pos = 0;
|
||||
}
|
||||
|
||||
seek(pos) {
|
||||
this.pos = pos;
|
||||
}
|
||||
|
||||
skip(length) {
|
||||
this.pos += length;
|
||||
}
|
||||
|
||||
writeBytes(bytes) {
|
||||
new Uint8Array(this.view.buffer, this.pos, bytes.length).set(bytes);
|
||||
this.pos += bytes.length;
|
||||
}
|
||||
|
||||
writeU8(value) {
|
||||
this.view.setUint8(this.pos, value);
|
||||
this.pos += 1;
|
||||
}
|
||||
|
||||
writeU16(value) {
|
||||
this.view.setUint16(this.pos, value, true);
|
||||
this.pos += 2;
|
||||
}
|
||||
|
||||
writeU32(value) {
|
||||
this.view.setUint32(this.pos, value >>> 0, true);
|
||||
this.pos += 4;
|
||||
}
|
||||
|
||||
writeF32(value) {
|
||||
this.view.setFloat32(this.pos, Number(value || 0), true);
|
||||
this.pos += 4;
|
||||
}
|
||||
}
|
||||
|
||||
export const CTB = {
|
||||
encode,
|
||||
read,
|
||||
toRSLA,
|
||||
decodeLayer,
|
||||
encodeRLE
|
||||
};
|
||||
Loading…
Reference in a new issue