diff --git a/src/cli/kiri-source.json b/src/cli/kiri-source.json index 0d346a9a..a5e6bbc9 100644 --- a/src/cli/kiri-source.json +++ b/src/cli/kiri-source.json @@ -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", @@ -56,4 +58,4 @@ "kiri/run/worker", "kiri/run/engine", "main/kiri" -] \ No newline at end of file +] diff --git a/src/kiri/app/conf/defaults.js b/src/kiri/app/conf/defaults.js index 1efecc0b..1428728b 100644 --- a/src/kiri/app/conf/defaults.js +++ b/src/kiri/app/conf/defaults.js @@ -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:{ diff --git a/src/kiri/app/devices.js b/src/kiri/app/devices.js index 2714a37b..a66a224b 100644 --- a/src/kiri/app/devices.js +++ b/src/kiri/app/devices.js @@ -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"; @@ -426,4 +428,4 @@ function updateDeviceSelector(devices, selector, selected) { api.platform.layout(); updateDeviceList(); } -} \ No newline at end of file +} diff --git a/src/kiri/dev/sla/Any.Generic.Raster.SLA.json b/src/kiri/dev/sla/Any.Generic.Raster.SLA.json new file mode 100644 index 00000000..8b6e73a4 --- /dev/null +++ b/src/kiri/dev/sla/Any.Generic.Raster.SLA.json @@ -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 + } +} diff --git a/src/kiri/dev/sla/Any.Generic.Vector.SLA.json b/src/kiri/dev/sla/Any.Generic.Vector.SLA.json new file mode 100644 index 00000000..20c9d030 --- /dev/null +++ b/src/kiri/dev/sla/Any.Generic.Vector.SLA.json @@ -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 + } +} diff --git a/src/kiri/mode/sla/app/init-ui.js b/src/kiri/mode/sla/app/init-ui.js index 48a20bbd..234e2b22 100644 --- a/src/kiri/mode/sla/app/init-ui.js +++ b/src/kiri/mode/sla/app/init-ui.js @@ -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) { diff --git a/src/kiri/mode/sla/work/export.js b/src/kiri/mode/sla/work/export.js index d296aea7..86ecd904 100644 --- a/src/kiri/mode/sla/work/export.js +++ b/src/kiri/mode/sla/work/export.js @@ -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, diff --git a/src/kiri/mode/sla/work/init-work.js b/src/kiri/mode/sla/work/init-work.js index 6994cc22..86eacd09 100644 --- a/src/kiri/mode/sla/work/init-work.js +++ b/src/kiri/mode/sla/work/init-work.js @@ -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); } diff --git a/src/kiri/mode/sla/work/x_cxdlp.js b/src/kiri/mode/sla/work/x_cxdlp.js index c4834ae9..b653503a 100644 --- a/src/kiri/mode/sla/work/x_cxdlp.js +++ b/src/kiri/mode/sla/work/x_cxdlp.js @@ -1,5 +1,7 @@ /** Copyright Stewart Allen -- 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); diff --git a/src/kiri/mode/sla/work/x_rsla.js b/src/kiri/mode/sla/work/x_rsla.js new file mode 100644 index 00000000..1e0054ef --- /dev/null +++ b/src/kiri/mode/sla/work/x_rsla.js @@ -0,0 +1,144 @@ +/** Copyright Stewart Allen -- 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 { + 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 -- 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 { + 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 ` `; + }).join("\n"); + + return [ + ``, + ``, + body, + ``, + `` + ].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 +}; diff --git a/src/kiri/run/worker.js b/src/kiri/run/worker.js index 83c6620a..50570278 100644 --- a/src/kiri/run/worker.js +++ b/src/kiri/run/worker.js @@ -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,