register photon extensions. add more .ctb target machines

This commit is contained in:
Stewart Allen 2026-05-15 17:10:37 -04:00
commit 304a2a4e24
10 changed files with 329 additions and 89 deletions

View file

@ -0,0 +1,15 @@
{
"no_clone": true,
"cmd": {},
"settings": {
"bed_width": 127,
"bed_depth": 80,
"bed_height": 1,
"max_height": 160,
"resolution_x": 2560,
"resolution_y": 1620,
"sla_format": "ctb",
"sla_file_ext": "ctb",
"sla_format_version": 3
}
}

View file

@ -0,0 +1,15 @@
{
"no_clone": true,
"cmd": {},
"settings": {
"bed_width": 119,
"bed_depth": 65,
"bed_height": 1,
"max_height": 160,
"resolution_x": 2560,
"resolution_y": 1440,
"sla_format": "ctb",
"sla_file_ext": "ctb",
"sla_format_version": 3
}
}

View file

@ -0,0 +1,15 @@
{
"no_clone": true,
"cmd": {},
"settings": {
"bed_width": 129,
"bed_depth": 80,
"bed_height": 1,
"max_height": 160,
"resolution_x": 1620,
"resolution_y": 2560,
"sla_format": "ctb",
"sla_file_ext": "ctb",
"sla_format_version": 3
}
}

View file

@ -0,0 +1,15 @@
{
"no_clone": true,
"cmd": {},
"settings": {
"bed_width": 192,
"bed_depth": 120,
"bed_height": 1,
"max_height": 200,
"resolution_x": 3840,
"resolution_y": 2400,
"sla_format": "ctb",
"sla_file_ext": "ctb",
"sla_format_version": 3
}
}

View file

@ -45,6 +45,41 @@ export const SLA_FORMATS = {
available: true,
priority: 1,
family: 'chitubox'
},
pwmo: {
id: 'pwmo',
ext: 'pwmo',
proprietary: true,
available: false,
family: 'photon-workshop'
},
pwma: {
id: 'pwma',
ext: 'pwma',
proprietary: true,
available: false,
family: 'photon-workshop'
},
pwmb: {
id: 'pwmb',
ext: 'pwmb',
proprietary: true,
available: false,
family: 'photon-workshop'
},
pm3n: {
id: 'pm3n',
ext: 'pm3n',
proprietary: true,
available: false,
family: 'photon-workshop'
},
pm4n: {
id: 'pm4n',
ext: 'pm4n',
proprietary: true,
available: false,
family: 'photon-workshop'
}
};

View file

@ -3,6 +3,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 { SLA } from './init-work.js';
import { RSLA } from './x_rsla.js';
import { VSLA } from './x_vsla.js';
@ -66,6 +67,11 @@ export function sla_export(print, online, ondone) {
});
}
if (format === 'pwmo' || format === 'pwma' || format === 'pwmb' ||
format === 'pm3n' || format === 'pm4n') {
return PW.encode(format);
}
if (format === 'photon' || format === 'photons') {
let legacyMode = SLA.legacy || alias > 1,
part1 = legacyMode ? 0.25 : 0.85,

View file

@ -0,0 +1,76 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
export function createRasterContext(print) {
let { settings, widgets } = print;
let { device, process } = settings;
let width = device.resolutionX;
let height = device.resolutionY;
let layermax = 0;
widgets = widgets.filter(w => !w.track.ignore && !w.meta.disabled);
widgets.forEach(widget => {
layermax = Math.max(layermax, widget.slices.length);
});
return {
settings,
device,
process,
widgets,
width,
height,
scaleX: width / device.bedWidth,
scaleY: height / device.bedDepth,
layermax
};
}
export function renderRasterLayers(print, renderer, onlayer, progress) {
let ctx = createRasterContext(print);
let { process, widgets, width, height, scaleX, scaleY, layermax } = ctx;
let volume = 0;
let count = 0;
for (let index=0; index<layermax; index++) {
let rendered = renderer.renderLayerWasm({
index,
width,
height,
widgets,
scaleX,
scaleY,
masks: []
});
if (rendered.end) break;
volume += rendered.area * process.slaSlice;
onlayer({
ctx,
index,
rendered,
bottom: index < process.slaBaseLayers,
z: process.slaFirstOffset + process.slaSlice * (index + 1)
});
count++;
if (progress) progress(index / layermax);
}
return { ctx, layers: count, volume };
}
export function imageToRowMajor(image, width, height, map) {
let data = new Uint8Array(width * height);
for (let x=0; x<width; x++) {
for (let y=0; y<height; y++) {
let value = image[x * height + y] || 0;
data[y * width + x] = map ? map(value) : value;
}
}
return data;
}
export function round(value) {
return Number.parseFloat(Number(value || 0).toFixed(5));
}

View file

@ -2,6 +2,7 @@
import { JSZip } from '../../../../ext/jszip-esm.js';
import { PNG } from '../../../../ext/pngjs.esm.js';
import { imageToRowMajor, renderRasterLayers, round } from './raster.js';
const CTB_V3_MAGIC = 0x12fd0086;
const HEADER_SIZE = 0x70;
@ -20,55 +21,34 @@ function encode(print, progress, renderer) {
}
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);
let previews = [
createPreview(400, 300),
createPreview(200, 125)
];
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 { ctx, volume } = renderRasterLayers(print, photon, params => {
let { index, rendered, bottom, z, ctx } = params;
let { process, width, height } = ctx;
let pixels = renderToCTBPixels(rendered.image, width, height);
let encoded = encipher(seed, index, encodeRLE(pixels));
let bottom = index < process.slaBaseLayers;
previews.forEach(preview => accumulatePreview(preview, rendered.image, width, height));
volume += rendered.area * process.slaSlice;
layers.push({
index,
z: process.slaFirstOffset + process.slaSlice * (index + 1),
z,
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
});
}, progress ? (value) => progress(value * 0.85, "ctb_encode") : null);
if (progress) progress((index / layermax) * 0.85, "ctb_encode");
}
let file = writeCTB({ device, process, layers, volume, seed });
let { device, process } = ctx;
let file = writeCTB({ device, process, layers, volume, seed, previews });
if (progress) progress(1, "ctb_write");
return { file, layers: layers.length, volume };
@ -86,6 +66,7 @@ function read(input) {
header,
print: readPrintParams(view, header),
machine: readMachineName(view, header),
previews: readPreviews(view, header),
layers,
layout: inferLayout(header, layers)
};
@ -141,12 +122,17 @@ function decodeLayer(view, header, layer) {
}
function writeCTB(params) {
let { device, process, layers, volume, seed } = params;
let { device, process, layers, volume, seed, previews } = params;
let machine = device.deviceName || "Generic CTB";
let machineBytes = new TextEncoder().encode(machine);
let previewData = previews.map(encodePreview);
let printTime = Math.round((process.slaBaseLayers * process.slaBaseOn) +
Math.max(0, layers.length - process.slaBaseLayers) * process.slaLayerOn);
let printParamsOffset = HEADER_SIZE;
let previewLargeOffset = HEADER_SIZE;
let previewLargeDataOffset = previewLargeOffset + 32;
let previewSmallOffset = previewLargeDataOffset + previewData[0].length;
let previewSmallDataOffset = previewSmallOffset + 32;
let printParamsOffset = previewSmallDataOffset + previewData[1].length;
let machineInfoOffset = printParamsOffset + PRINT_PARAMS_SIZE;
let nameOffset = machineInfoOffset + MACHINE_INFO_SIZE;
let layerTableOffset = nameOffset + machineBytes.length;
@ -169,10 +155,20 @@ function writeCTB(params) {
layers,
seed,
printTime,
previewLargeOffset,
previewSmallOffset,
printParamsOffset,
machineInfoOffset,
layerTableOffset
});
writer.seek(previewLargeOffset);
writePreviewRecord(writer, previews[0], previewLargeDataOffset, previewData[0]);
writer.seek(previewLargeDataOffset);
writer.writeBytes(previewData[0]);
writer.seek(previewSmallOffset);
writePreviewRecord(writer, previews[1], previewSmallDataOffset, previewData[1]);
writer.seek(previewSmallDataOffset);
writer.writeBytes(previewData[1]);
writer.seek(printParamsOffset);
writePrintParams(writer, { process, volume });
writer.seek(machineInfoOffset);
@ -193,6 +189,7 @@ function writeCTB(params) {
function writeHeader(writer, params) {
let {
device, process, layers, seed, printTime,
previewLargeOffset, previewSmallOffset,
printParamsOffset, machineInfoOffset, layerTableOffset
} = params;
@ -209,10 +206,10 @@ function writeHeader(writer, params) {
writer.writeU32(process.slaBaseLayers);
writer.writeU32(device.resolutionX);
writer.writeU32(device.resolutionY);
writer.writeU32(0);
writer.writeU32(previewLargeOffset);
writer.writeU32(layerTableOffset);
writer.writeU32(layers.length);
writer.writeU32(0);
writer.writeU32(previewSmallOffset);
writer.writeU32(printTime);
writer.writeU32(1);
writer.writeU32(printParamsOffset);
@ -225,6 +222,17 @@ function writeHeader(writer, params) {
writer.writeU32(MACHINE_INFO_SIZE);
}
function writePreviewRecord(writer, preview, dataOffset, data) {
writer.writeU32(preview.width);
writer.writeU32(preview.height);
writer.writeU32(dataOffset);
writer.writeU32(data.length);
writer.writeU32(0);
writer.writeU32(0);
writer.writeU32(0);
writer.writeU32(0);
}
function writePrintParams(writer, params) {
let { process, volume } = params;
@ -316,9 +324,10 @@ function readHeader(view) {
resolutionX: view.getUint32(0x34, true),
resolutionY: view.getUint32(0x38, true),
previewOffset: view.getUint32(0x3c, true),
previewLargeOffset: view.getUint32(0x3c, true),
layerTableOffset: view.getUint32(0x40, true),
layerCount: view.getUint32(0x44, true),
previewOneLength: view.getUint32(0x48, true),
previewSmallOffset: view.getUint32(0x48, true),
printParamsOffset: view.getUint32(0x54, true),
printParamsLength: view.getUint32(0x58, true),
antiAliasLevel: view.getUint32(0x5c, true),
@ -330,6 +339,27 @@ function readHeader(view) {
};
}
function readPreviews(view, header) {
return [header.previewLargeOffset, header.previewSmallOffset]
.filter(offset => offset > 0)
.map((offset, index) => readPreviewRecord(view, offset, index));
}
function readPreviewRecord(view, offset, index) {
return {
index,
offset,
width: view.getUint32(offset + 0, true),
height: view.getUint32(offset + 4, true),
imageOffset: view.getUint32(offset + 8, true),
imageLength: view.getUint32(offset + 12, true),
unknown16: view.getUint32(offset + 16, true),
unknown20: view.getUint32(offset + 20, true),
unknown24: view.getUint32(offset + 24, true),
unknown28: view.getUint32(offset + 28, true)
};
}
function readLayerTable(view, header) {
let layers = [];
let offset = header.layerTableOffset;
@ -500,14 +530,71 @@ function createRSLAManifest(ctb, layers) {
};
}
function createPreview(width, height) {
return {
width,
height,
image: new Uint8Array(width * height)
};
}
function accumulatePreview(preview, image, width, height) {
let { image: target } = preview;
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));
if (image[sx * height + sy]) {
target[y * preview.width + x] = 255;
}
}
}
}
function encodePreview(preview) {
let output = [];
let data = preview.image;
let color = previewColor(data[0]);
let run = 1;
for (let i=1; i<data.length; i++) {
let next = previewColor(data[i]);
if (next === color && run < 0xfff) {
run++;
continue;
}
writePreviewRun(output, color, run);
color = next;
run = 1;
}
writePreviewRun(output, color, run);
return new Uint8Array(output);
}
function writePreviewRun(output, color, length) {
if (length === 1) {
writePreviewU16(output, color);
return;
}
writePreviewU16(output, color | 0x0020);
writePreviewU16(output, 0x3000 | (length - 1));
}
function previewColor(value) {
return value ? 0xffdf : 0x0000;
}
function writePreviewU16(output, value) {
output.push(value & 0xff, (value >> 8) & 0xff);
}
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;
return imageToRowMajor(image, width, height);
}
function encodeRLE(input) {
@ -675,10 +762,6 @@ function toDataView(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;

View file

@ -0,0 +1,14 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
const FAMILY = "Photon Workshop";
function encode(format) {
return Promise.reject(new Error([
`${FAMILY} format .${format} is registered but not implemented.`,
"Add a fixture sliced by Photon Workshop before enabling this output target."
].join(" ")));
}
export const PW = {
encode
};

View file

@ -3,41 +3,18 @@
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';
const FORMAT = "rsla";
const MIME = "application/vnd.gridspace.rsla+zip";
function encode(print, progress) {
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;
widgets = widgets.filter(w => !w.track.ignore && !w.meta.disabled);
widgets.forEach(widget => {
layermax = Math.max(layermax, widget.slices.length);
});
let layers = [];
let volume = 0;
let zip = new JSZip();
for (let index=0; index<layermax; index++) {
let rendered = photon.renderLayerWasm({
index,
width,
height,
widgets,
scaleX,
scaleY,
masks: []
});
if (rendered.end) {
break;
}
let { ctx, volume } = renderRasterLayers(print, photon, params => {
let { index, rendered, ctx } = params;
let { process, width, height } = ctx;
let file = `layers/${index.toString().padStart(6, "0")}.png`;
let layer = {
@ -47,13 +24,11 @@ function encode(print, progress) {
file
};
volume += rendered.area * process.slaSlice;
layers.push(layer);
zip.file(file, imageToPNG(rendered.image, width, height));
}, progress ? (value) => progress(value * 0.85, "raster_gen") : null);
if (progress) progress((index / layermax) * 0.85, "raster_gen");
}
let { device, process } = ctx;
let manifest = createManifest({ device, process, layers, volume });
zip.file("manifest.json", JSON.stringify(manifest, null, 2));
@ -122,12 +97,7 @@ function createManifest(params) {
}
function imageToPNG(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] ? 255 : 0;
}
}
let data = imageToRowMajor(image, width, height, value => value ? 255 : 0);
return PNG.sync.write({ width, height, data }, {
colorType: 0,
inputColorType: 0,
@ -135,10 +105,6 @@ function imageToPNG(image, width, height) {
});
}
function round(value) {
return Number.parseFloat(Number(value || 0).toFixed(5));
}
export const RSLA = {
encode
};