add .rsla and .vsla generic raster and vector sla output formats
This commit is contained in:
parent
61d977e169
commit
95eadc60df
12 changed files with 427 additions and 35 deletions
|
|
@ -48,6 +48,8 @@
|
|||
"kiri/mode/sla/driver",
|
||||
"kiri/mode/sla/x_cxdlp",
|
||||
"kiri/mode/sla/x_photon",
|
||||
"kiri/mode/sla/x_rsla",
|
||||
"kiri/mode/sla/x_vsla",
|
||||
"kiri/mode/sla/export",
|
||||
"kiri/mode/sla/slice",
|
||||
"kiri/core/codec",
|
||||
|
|
|
|||
|
|
@ -99,6 +99,9 @@ function device_from_code(code,mode) {
|
|||
bedBelt: valueOf(set.bed_belt, false),
|
||||
resolutionX: valueOf(set.resolution_x, 1600),
|
||||
resolutionY: valueOf(set.resolution_y, 900),
|
||||
slaFormat: valueOf(set.sla_format, ''),
|
||||
slaFileExt: valueOf(set.sla_file_ext, ''),
|
||||
slaFormatVersion: valueOf(set.sla_format_version, 1),
|
||||
deviceZMax: valueOf(set.z_move_max, 0),
|
||||
gcodeTime: valueOf(set.time_factor, 1),
|
||||
maxHeight: valueOf(set.build_height, 150),
|
||||
|
|
@ -367,7 +370,10 @@ export const conf = {
|
|||
bedHeight: 1.5,
|
||||
maxHeight: 150,
|
||||
resolutionX: 1600,
|
||||
resolutionY: 900
|
||||
resolutionY: 900,
|
||||
slaFormat: "",
|
||||
slaFileExt: "",
|
||||
slaFormatVersion: 1
|
||||
},
|
||||
// process defaults SLA:Process
|
||||
p:{
|
||||
|
|
|
|||
|
|
@ -185,6 +185,8 @@ function setDeviceCode(code, devicename) {
|
|||
}
|
||||
}
|
||||
|
||||
let allowSLADeviceEdit = mode !== 'SLA' || dev.slaFormat === 'vsla' || dev.slaFormat === 'rsla';
|
||||
|
||||
// disable editing for non-local devices
|
||||
[
|
||||
// ui.deviceName,
|
||||
|
|
@ -226,14 +228,14 @@ function setDeviceCode(code, devicename) {
|
|||
ui.extOffsetX,
|
||||
ui.extOffsetY
|
||||
].forEach(function(e) {
|
||||
e.disabled = !local;
|
||||
e.disabled = !local || !allowSLADeviceEdit;
|
||||
});
|
||||
|
||||
ui.devices.save.disabled = !local;
|
||||
ui.devices.save.disabled = !local || !allowSLADeviceEdit;
|
||||
ui.devices.delete.disabled = !local;
|
||||
ui.devices.rename.disabled = !local;
|
||||
ui.devices.export.disabled = !local;
|
||||
ui.devices.add.style.display = mode === 'SLA' ? 'none' : '';
|
||||
ui.devices.add.style.display = allowSLADeviceEdit ? '' : 'none';
|
||||
|
||||
if (local) {
|
||||
ui.devices.add.innerText = "copy";
|
||||
|
|
|
|||
15
src/kiri/dev/sla/Any.Generic.Raster.SLA.json
Normal file
15
src/kiri/dev/sla/Any.Generic.Raster.SLA.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"no_clone": false,
|
||||
"cmd": {},
|
||||
"settings": {
|
||||
"bed_width": 120,
|
||||
"bed_depth": 68,
|
||||
"bed_height": 1,
|
||||
"max_height": 150,
|
||||
"resolution_x": 2560,
|
||||
"resolution_y": 1440,
|
||||
"sla_format": "rsla",
|
||||
"sla_file_ext": "rsla",
|
||||
"sla_format_version": 1
|
||||
}
|
||||
}
|
||||
15
src/kiri/dev/sla/Any.Generic.Vector.SLA.json
Normal file
15
src/kiri/dev/sla/Any.Generic.Vector.SLA.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"no_clone": false,
|
||||
"cmd": {},
|
||||
"settings": {
|
||||
"bed_width": 120,
|
||||
"bed_depth": 68,
|
||||
"bed_height": 1,
|
||||
"max_height": 150,
|
||||
"resolution_x": 2560,
|
||||
"resolution_y": 1440,
|
||||
"sla_format": "vsla",
|
||||
"sla_file_ext": "vsla",
|
||||
"sla_format_version": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -63,21 +63,14 @@ export function printDownload(output, api, names) {
|
|||
$('print-layers').value = layers;
|
||||
$('print-time').value = `${print_hrs}:${print_min}:${print_sec}`;
|
||||
|
||||
switch (device.deviceName) {
|
||||
case 'Anycubic.Photon':
|
||||
download.innerText += " .photon";
|
||||
download.onclick = () => { saveFile(api, file, ".photon") };
|
||||
break;
|
||||
case 'Anycubic.Photon.S':
|
||||
download.innerText += " .photons";
|
||||
download.onclick = () => { saveFile(api, file, ".photons") };
|
||||
break;
|
||||
case 'Creality.Halot.Sky':
|
||||
default:
|
||||
download.innerText += " .cxdlp";
|
||||
download.onclick = () => { saveFile(api, file, ".cxdlp") };
|
||||
break;
|
||||
}
|
||||
let ext = device.slaFileExt || ({
|
||||
'Anycubic.Photon': 'photon',
|
||||
'Anycubic.Photon.S': 'photons'
|
||||
}[device.deviceName]) || 'cxdlp';
|
||||
|
||||
ext = ext.startsWith(".") ? ext : `.${ext}`;
|
||||
download.innerText = `${api.language.current.ex_dwnl || 'download'} ${ext}`;
|
||||
download.onclick = () => { saveFile(api, file, ext) };
|
||||
}
|
||||
|
||||
function saveFile(api, file, ext) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
import { CXDLP } from './x_cxdlp.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';
|
||||
|
||||
/**
|
||||
* DRIVER CONTRACT - runs in worker
|
||||
|
|
@ -33,16 +35,30 @@ export function sla_export(print, online, ondone) {
|
|||
layermax = Math.max(widget.slices.length);
|
||||
});
|
||||
|
||||
let isPhoton = false;
|
||||
let format = device.slaFormat || ({
|
||||
'Anycubic.Photon': 'photon',
|
||||
'Anycubic.Photon.S': 'photons',
|
||||
}[device.deviceName]) || 'cxdlp';
|
||||
|
||||
switch (device.deviceName) {
|
||||
case 'Anycubic.Photon':
|
||||
case 'Anycubic.Photon.S':
|
||||
isPhoton = true;
|
||||
break;
|
||||
if (format === 'vsla') {
|
||||
return VSLA.encode(print, (progress, message) => {
|
||||
online({ progress, message });
|
||||
}).then(output => {
|
||||
let { file, layers, volume } = output;
|
||||
ondone({ width, height, file, layers, volume }, [file]);
|
||||
});
|
||||
}
|
||||
|
||||
if (isPhoton) {
|
||||
if (format === 'rsla') {
|
||||
return RSLA.encode(print, (progress, message) => {
|
||||
online({ progress, message });
|
||||
}).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,
|
||||
part2 = (1 - part1),
|
||||
|
|
@ -73,9 +89,9 @@ export function sla_export(print, online, ondone) {
|
|||
}
|
||||
|
||||
let exp_func = {
|
||||
'Anycubic.Photon': photon.generatePhoton,
|
||||
'Anycubic.Photon.S': photon.generatePhotons,
|
||||
}[device.deviceName] || photon.generatePhoton;
|
||||
photon: photon.generatePhoton,
|
||||
photons: photon.generatePhotons,
|
||||
}[format] || photon.generatePhoton;
|
||||
|
||||
let file = exp_func(print, {
|
||||
width: width,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ function init(worker) {
|
|||
async function sla_prepare(widgets, settings, update) {
|
||||
self.kiri_worker.current.print = newPrint(settings, widgets);
|
||||
if (!SLA.wasm) {
|
||||
fetch('/wasm/kiri-sla.wasm')
|
||||
SLA.wasmPromise = SLA.wasmPromise || fetch('/wasm/kiri-sla.wasm')
|
||||
.then(response => response.arrayBuffer())
|
||||
.then(bytes => WebAssembly.instantiate(bytes, {
|
||||
env: {
|
||||
|
|
@ -44,6 +44,7 @@ async function sla_prepare(widgets, settings, update) {
|
|||
rle_encode: exports.rle_encode
|
||||
};
|
||||
});
|
||||
await SLA.wasmPromise;
|
||||
}
|
||||
update(1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { SLA } from './init-work.js';
|
||||
|
||||
const default_values = {
|
||||
magic1: 'CXSW3DV2',
|
||||
magic2: 'CXSW3DV2',
|
||||
|
|
@ -392,7 +394,7 @@ CXDLP.render = function(params) {
|
|||
}
|
||||
});
|
||||
|
||||
let wasm = kiri.driver.SLA.wasm;
|
||||
let wasm = SLA.wasm;
|
||||
let imagelen = width * height;
|
||||
let writer = new self.DataWriter(new DataView(wasm.memory.buffer), imagelen);
|
||||
writer.writeU16(width, true);
|
||||
|
|
|
|||
144
src/kiri/mode/sla/work/x_rsla.js
Normal file
144
src/kiri/mode/sla/work/x_rsla.js
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { JSZip } from '../../../../ext/jszip-esm.js';
|
||||
import { PNG } from '../../../../ext/pngjs.esm.js';
|
||||
import { photon } from './x_photon.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 file = `layers/${index.toString().padStart(6, "0")}.png`;
|
||||
let layer = {
|
||||
index,
|
||||
z: round(process.slaFirstOffset + process.slaSlice * index),
|
||||
area: round(rendered.area),
|
||||
file
|
||||
};
|
||||
|
||||
volume += rendered.area * process.slaSlice;
|
||||
layers.push(layer);
|
||||
zip.file(file, imageToPNG(rendered.image, width, height));
|
||||
|
||||
if (progress) progress((index / layermax) * 0.85, "raster_gen");
|
||||
}
|
||||
|
||||
let manifest = createManifest({ device, process, layers, volume });
|
||||
zip.file("manifest.json", JSON.stringify(manifest, null, 2));
|
||||
|
||||
return zip.generateAsync({
|
||||
type: "arraybuffer",
|
||||
compression: "DEFLATE",
|
||||
compressionOptions: { level: 6 },
|
||||
streamFiles: true,
|
||||
mimeType: MIME
|
||||
}, meta => {
|
||||
if (progress) progress(0.85 + (meta.percent / 100) * 0.15, "zip_gen");
|
||||
}).then(file => {
|
||||
return { file, layers: layers.length, volume };
|
||||
});
|
||||
}
|
||||
|
||||
function createManifest(params) {
|
||||
let { device, process, layers, volume } = params;
|
||||
|
||||
return {
|
||||
format: FORMAT,
|
||||
version: device.slaFormatVersion || 1,
|
||||
units: "mm",
|
||||
coordinateSystem: {
|
||||
layerImage: "top-left",
|
||||
model: "bed-center",
|
||||
x: "right",
|
||||
y: "back",
|
||||
z: "up"
|
||||
},
|
||||
bed: {
|
||||
width: device.bedWidth,
|
||||
depth: device.bedDepth,
|
||||
height: device.maxHeight || device.bedHeight
|
||||
},
|
||||
resolution: {
|
||||
x: device.resolutionX,
|
||||
y: device.resolutionY
|
||||
},
|
||||
layerHeight: process.slaSlice,
|
||||
layerCount: layers.length,
|
||||
volume: round(volume),
|
||||
raster: {
|
||||
encoding: "png",
|
||||
color: "grayscale",
|
||||
cure: "nonzero",
|
||||
white: "cure",
|
||||
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
|
||||
},
|
||||
layers
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
return PNG.sync.write({ width, height, data }, {
|
||||
colorType: 0,
|
||||
inputColorType: 0,
|
||||
inputHasAlpha: false
|
||||
});
|
||||
}
|
||||
|
||||
function round(value) {
|
||||
return Number.parseFloat(Number(value || 0).toFixed(5));
|
||||
}
|
||||
|
||||
export const RSLA = {
|
||||
encode
|
||||
};
|
||||
193
src/kiri/mode/sla/work/x_vsla.js
Normal file
193
src/kiri/mode/sla/work/x_vsla.js
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { JSZip } from '../../../../ext/jszip-esm.js';
|
||||
|
||||
const FORMAT = "vsla";
|
||||
const MIME = "application/vnd.gridspace.vsla+zip";
|
||||
|
||||
function encode(print, progress) {
|
||||
let { settings, widgets } = print;
|
||||
let { device, process } = settings;
|
||||
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;
|
||||
for (let index=0; index<layermax; index++) {
|
||||
let layer = collectLayer({ index, widgets, process });
|
||||
volume += layer.area * process.slaSlice;
|
||||
layers.push(layer);
|
||||
if (progress) progress((index / layermax) * 0.7, "vector_gen");
|
||||
}
|
||||
|
||||
while (layers.length && layers.last().polygons.length === 0) {
|
||||
layers.pop();
|
||||
}
|
||||
|
||||
let manifest = createManifest({ device, process, layers, volume });
|
||||
let zip = new JSZip();
|
||||
zip.file("manifest.json", JSON.stringify(manifest, null, 2));
|
||||
layers.forEach(layer => {
|
||||
zip.file(layer.file, createLayerSVG({ device, layer }));
|
||||
});
|
||||
|
||||
return zip.generateAsync({
|
||||
type: "arraybuffer",
|
||||
compression: "DEFLATE",
|
||||
compressionOptions: { level: 6 },
|
||||
streamFiles: true,
|
||||
mimeType: MIME
|
||||
}, meta => {
|
||||
if (progress) progress(0.7 + (meta.percent / 100) * 0.3, "zip_gen");
|
||||
}).then(file => {
|
||||
return { file, layers: layers.length, volume };
|
||||
});
|
||||
}
|
||||
|
||||
function collectLayer(params) {
|
||||
let { index, widgets, process } = params;
|
||||
let polygons = [];
|
||||
let area = 0;
|
||||
let z = process.slaFirstOffset + process.slaSlice * index;
|
||||
|
||||
widgets.forEach(widget => {
|
||||
let slice = widget.slices[index];
|
||||
if (!slice) return;
|
||||
|
||||
let polys = slice.unioned;
|
||||
if (!polys) polys = slice.tops.map(t => t.poly);
|
||||
polys = polys ? polys.slice() : [];
|
||||
if (slice.supports) polys.appendAll(slice.supports);
|
||||
|
||||
polygons.appendAll(polys.map(poly => {
|
||||
let moved = poly.clone(true).move(widget.track.pos);
|
||||
area += moved.areaDeep();
|
||||
return moved;
|
||||
}));
|
||||
|
||||
if (slice.z !== undefined) {
|
||||
z = slice.z;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
index,
|
||||
z: round(z),
|
||||
area: round(area),
|
||||
file: `layers/${index.toString().padStart(6, "0")}.svg`,
|
||||
polygons
|
||||
};
|
||||
}
|
||||
|
||||
function createManifest(params) {
|
||||
let { device, process, layers, volume } = params;
|
||||
let layerList = layers.map(layer => {
|
||||
return {
|
||||
index: layer.index,
|
||||
z: layer.z,
|
||||
area: layer.area,
|
||||
file: layer.file,
|
||||
empty: layer.polygons.length === 0
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
format: FORMAT,
|
||||
version: device.slaFormatVersion || 1,
|
||||
units: "mm",
|
||||
coordinateSystem: {
|
||||
layerSvg: "top-left",
|
||||
model: "bed-center",
|
||||
x: "right",
|
||||
y: "back",
|
||||
z: "up"
|
||||
},
|
||||
cure: {
|
||||
geometry: "filled-svg-paths",
|
||||
fill: "#000",
|
||||
fillRule: "evenodd",
|
||||
background: "transparent"
|
||||
},
|
||||
bed: {
|
||||
width: device.bedWidth,
|
||||
depth: device.bedDepth,
|
||||
height: device.maxHeight || device.bedHeight
|
||||
},
|
||||
resolution: {
|
||||
x: device.resolutionX,
|
||||
y: device.resolutionY
|
||||
},
|
||||
layerHeight: process.slaSlice,
|
||||
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
|
||||
},
|
||||
layers: layerList
|
||||
};
|
||||
}
|
||||
|
||||
function createLayerSVG(params) {
|
||||
let { device, layer } = params;
|
||||
let width = round(device.bedWidth);
|
||||
let depth = round(device.bedDepth);
|
||||
let body = layer.polygons.map(poly => {
|
||||
return ` <path fill="#000" fill-rule="evenodd" d="${polyToPath(poly, device)}"/>`;
|
||||
}).join("\n");
|
||||
|
||||
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}">`,
|
||||
body,
|
||||
`</svg>`,
|
||||
``
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function polyToPath(poly, device) {
|
||||
let parts = [];
|
||||
appendPoly(parts, poly, device);
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
function appendPoly(parts, poly, device) {
|
||||
appendPath(parts, poly, device);
|
||||
if (poly.inner) {
|
||||
poly.inner.forEach(inner => appendPoly(parts, inner, device));
|
||||
}
|
||||
}
|
||||
|
||||
function appendPath(parts, poly, device) {
|
||||
let points = poly.points || [];
|
||||
if (points.length === 0) return;
|
||||
|
||||
points.forEach((point, index) => {
|
||||
let x = round(point.x + device.bedWidth / 2);
|
||||
let y = round(device.bedDepth - (point.y + device.bedDepth / 2));
|
||||
parts.push(`${index === 0 ? "M" : "L"}${x},${y}`);
|
||||
});
|
||||
parts.push("Z");
|
||||
}
|
||||
|
||||
function round(value) {
|
||||
return Number.parseFloat(Number(value || 0).toFixed(5));
|
||||
}
|
||||
|
||||
export const VSLA = {
|
||||
encode
|
||||
};
|
||||
|
|
@ -556,7 +556,7 @@ const dispatch = {
|
|||
});
|
||||
},
|
||||
|
||||
export(data, send) {
|
||||
async export(data, send) {
|
||||
const mode = data.settings.mode;
|
||||
const driver = drivers[mode];
|
||||
|
||||
|
|
@ -566,7 +566,7 @@ const dispatch = {
|
|||
}
|
||||
|
||||
let output;
|
||||
driver.export(current.print, (line, direct) => {
|
||||
let result = driver.export(current.print, (line, direct) => {
|
||||
send.data({line}, direct);
|
||||
}, (done) => {
|
||||
// SLA workaround
|
||||
|
|
@ -574,6 +574,9 @@ const dispatch = {
|
|||
}, (debug) => {
|
||||
send.data({debug});
|
||||
});
|
||||
if (result && result.then) {
|
||||
await result;
|
||||
}
|
||||
|
||||
const {
|
||||
bounds,
|
||||
|
|
|
|||
Loading…
Reference in a new issue