remove image from sla export dialog. rename halot/cxdlp. improve cxdlp debug tool. layer more export fields
This commit is contained in:
parent
153b8cef64
commit
5748912ef0
7 changed files with 50 additions and 458 deletions
2
app.js
2
app.js
|
|
@ -309,7 +309,7 @@ const script = {
|
|||
"mode/sla/driver",
|
||||
"mode/sla/slice",
|
||||
"mode/sla/export",
|
||||
"mode/sla/x_halot",
|
||||
"mode/sla/x_cxdlp",
|
||||
"mode/sla/x_photon",
|
||||
"mode/cam/driver",
|
||||
"mode/cam/ops",
|
||||
|
|
|
|||
|
|
@ -60,27 +60,19 @@ console.log/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
|||
}
|
||||
|
||||
function callExportSLA(options) {
|
||||
const preview = [];
|
||||
KIRI.client.export(API.conf.get(), (line) => {
|
||||
API.show.progress(line.progress, "exporting");
|
||||
if (line.data) {
|
||||
preview.push(line.data);
|
||||
}
|
||||
}, (output, error) => {
|
||||
API.show.progress(0);
|
||||
if (error) {
|
||||
API.show.alert(error, 5);
|
||||
} else {
|
||||
KIRI.driver.SLA.printDownload(preview, output, API);
|
||||
KIRI.driver.SLA.printDownload(output, API);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function exportLaserDialog(data) {
|
||||
// if (!isPrepared()) {
|
||||
// return API.function.prepare(exportLaser);
|
||||
// }
|
||||
|
||||
const filename = "laser-"+(new Date().getTime().toString(36));
|
||||
const settings = API.conf.get();
|
||||
const driver = KIRI.driver.LASER;
|
||||
|
|
|
|||
|
|
@ -19,9 +19,8 @@
|
|||
});
|
||||
};
|
||||
|
||||
SLA.printDownload = function(preview, output, API) {
|
||||
const lines = preview;
|
||||
const { file, width, height } = output;
|
||||
SLA.printDownload = function(output, API) {
|
||||
const { file, width, height, layers } = output;
|
||||
const filename = `print-${new Date().getTime().toString(36)}`;
|
||||
|
||||
API.ajax("/kiri/output-sla.html", html => {
|
||||
|
|
@ -31,10 +30,10 @@
|
|||
process = settings.process,
|
||||
device = settings.device,
|
||||
print_sec = (process.slaBaseLayers * process.slaBaseOn) +
|
||||
(lines.length - process.slaBaseLayers) * process.slaLayerOn;
|
||||
(layers - process.slaBaseLayers) * process.slaLayerOn;
|
||||
|
||||
// add peel lift/drop times to total print time
|
||||
for (let i=0; i<lines.length; i++) {
|
||||
for (let i=0; i<layers; i++) {
|
||||
let dist = process.slaPeelDist,
|
||||
lift = process.slaPeelLiftRate,
|
||||
drop = process.slaPeelDropRate,
|
||||
|
|
@ -51,7 +50,7 @@
|
|||
|
||||
let print_min = Math.floor(print_sec/60),
|
||||
print_hrs = Math.floor(print_min/60),
|
||||
download = $('print-photon');
|
||||
download = $('print-sla');
|
||||
|
||||
// add lift/drop time
|
||||
print_sec -= (print_min * 60);
|
||||
|
|
@ -61,7 +60,7 @@
|
|||
print_hrs = print_hrs.toString().padStart(2,'0');
|
||||
|
||||
$('print-filename').value = filename;
|
||||
$('print-layers').value = lines.length;
|
||||
$('print-layers').value = layers;
|
||||
$('print-time').value = `${print_hrs}:${print_min}:${print_sec}`;
|
||||
|
||||
switch (device.deviceName) {
|
||||
|
|
@ -80,29 +79,6 @@
|
|||
break;
|
||||
}
|
||||
|
||||
let canvas = $('print-canvas');
|
||||
// rotate 90 degrees for export view
|
||||
canvas.width = height;
|
||||
canvas.height = width;
|
||||
let ctx = canvas.getContext('2d');
|
||||
let img = ctx.createImageData(height, width);
|
||||
let imgDV = new DataView(img.data.buffer);
|
||||
let range = $('print-range');
|
||||
range.value = 0;
|
||||
range.min = 0;
|
||||
range.max = lines.length - 1;
|
||||
range.oninput = function() {
|
||||
let pdata = lines[range.value];
|
||||
if (!pdata) return;
|
||||
let lineDV = new DataView(pdata);
|
||||
for (let i=0; i<lineDV.byteLength; i++) {
|
||||
imgDV.setUint32(i*4, lineDV.getUint8(i));
|
||||
}
|
||||
ctx.putImageData(img,0,0);
|
||||
$('print-layer').innerText = range.value.padStart(4,'0');
|
||||
};
|
||||
|
||||
range.oninput();
|
||||
API.modal.show('print');
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,13 +64,10 @@
|
|||
let {image, layers, end} = render(param);
|
||||
images.push(image);
|
||||
slices.push(layers);
|
||||
// transfer image memory to browser main
|
||||
image = image.buffer;
|
||||
online({
|
||||
progress: (index / layermax) * part1,
|
||||
message: "image_gen",
|
||||
data: image
|
||||
}, [image]);
|
||||
});
|
||||
if (end) break;
|
||||
}
|
||||
|
||||
|
|
@ -89,41 +86,39 @@
|
|||
}, (progress, message) => {
|
||||
online({progress: progress * part2 + part1, message});
|
||||
});
|
||||
ondone({
|
||||
width: width,
|
||||
height: height,
|
||||
file: file
|
||||
},[file]);
|
||||
ondone({ width, height, file, layers: images.length }, [file]);
|
||||
} else {
|
||||
let part1 = 0.25;
|
||||
let part1 = 0.95;
|
||||
let part2 = 1 - part1;
|
||||
let images = [];
|
||||
let slices = [];
|
||||
|
||||
for (let index=0; index < layermax; index++) {
|
||||
let param = { index, width, height, widgets, scaleX, scaleY };
|
||||
let {image, lines} = CXDLP.render(param);
|
||||
images.push(image);
|
||||
let {lines} = CXDLP.render(param);
|
||||
slices.push(lines);
|
||||
// transfer image memory to browser main
|
||||
// it *should* be sampled to save memory
|
||||
image = image ? image.buffer : undefined;
|
||||
online({
|
||||
progress: (index / layermax) * part1,
|
||||
message: "image_gen",
|
||||
data: image
|
||||
}, [image]);
|
||||
message: "image_gen"
|
||||
});
|
||||
// bail on an empty layer
|
||||
if (lines.length === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let file = CXDLP.export({settings, width, height, slices});
|
||||
// generate thumb, preview1, preview2
|
||||
|
||||
let file = CXDLP.export({
|
||||
settings,
|
||||
width,
|
||||
height,
|
||||
slices
|
||||
});
|
||||
ondone({
|
||||
width: width,
|
||||
height: height,
|
||||
file: file
|
||||
file: file,
|
||||
layers: slices.length
|
||||
}, [file]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,386 +0,0 @@
|
|||
(function() {
|
||||
|
||||
const default_values = {
|
||||
magic1: 'CXSW3DV2',
|
||||
magic2: 'CXSW3DV2',
|
||||
model: 'CL-89',
|
||||
version: 1,
|
||||
layer_count: 0,
|
||||
res_x: 3840,
|
||||
res_y: 2400,
|
||||
height: 0,
|
||||
thumb: undefined,
|
||||
preview1: undefined,
|
||||
preview2: undefined,
|
||||
dim_x: '192.000000',
|
||||
dim_y: '120.000000',
|
||||
layer: '0.050000',
|
||||
light_on: 6,
|
||||
light_off: 2,
|
||||
light_pwm: 255,
|
||||
lift_dist: 6,
|
||||
lift_speed: 60,
|
||||
down_speed: 150,
|
||||
base_layers: 8,
|
||||
base_light_on: 60,
|
||||
base_light_pwm: 255,
|
||||
base_lift_dist: 5,
|
||||
base_lift_speed: 60
|
||||
};
|
||||
const data_term = 0x0d0a;
|
||||
|
||||
class CXDLP {
|
||||
constructor() {
|
||||
Object.assign(this, default_values);
|
||||
}
|
||||
|
||||
// takes a DataView Object
|
||||
read(view) {
|
||||
let read = new DataReader(view, 0);
|
||||
|
||||
function read_string(dbl) {
|
||||
let len = read.readU32();
|
||||
let str = '';
|
||||
while (--len > 0) {
|
||||
if (dbl) {
|
||||
read.readU8();
|
||||
len--;
|
||||
}
|
||||
str += String.fromCharCode(read.readU8());
|
||||
}
|
||||
if (!dbl) {
|
||||
read.skip(1); // null term on byte strings
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
let meta = {
|
||||
view,
|
||||
magic1: read_string(),
|
||||
version: read.readU16(),
|
||||
model: read_string(),
|
||||
layer_count: read.readU16(),
|
||||
res_x: read.readU16(),
|
||||
res_y: read.readU16(),
|
||||
height: read.readU32(),
|
||||
skip: read.skip(60),
|
||||
thumb: read.skip(26914),
|
||||
preview1: read.skip(168202),
|
||||
preview2: read.skip(168202),
|
||||
dim_x: read_string(true),
|
||||
dim_y: read_string(true),
|
||||
layer: read_string(true),
|
||||
light_on: read.readU16(),
|
||||
light_off: read.readU16(),
|
||||
base_light_on: read.readU16(),
|
||||
base_layers: read.readU16(),
|
||||
base_lift_dist: read.readU16(),
|
||||
base_lift_speed: read.readU16(),
|
||||
lift_dist: read.readU16(),
|
||||
lift_speed: read.readU16(),
|
||||
down_speed: read.readU16(),
|
||||
base_light_pwm: read.readU16(),
|
||||
light_pwm: read.readU16()
|
||||
};
|
||||
|
||||
// read layer record lengths
|
||||
let layers = this.layers = [];
|
||||
for (let i=0; i<meta.layer_count; i++) {
|
||||
layers.push({ length: read.readU32() });
|
||||
}
|
||||
if (read.readU16() !== data_term) {
|
||||
throw `invalid data term @ ${read.pos - 2}`;
|
||||
}
|
||||
|
||||
// read layer meta data, not line data
|
||||
for (let i=0; i<meta.layer_count; i++) {
|
||||
let size = read.readU32();
|
||||
if (size !== layers[i].length) {
|
||||
throw `layer length mismatch: ${size} != ${layers[i].length} @ i=${i}`;
|
||||
}
|
||||
let lines = read.readU32();
|
||||
layers[i].lines = lines;
|
||||
layers[i].pos = read.pos; // store line data position
|
||||
// skip line data and check data term is valid
|
||||
read.skip(lines * 6);
|
||||
if (read.readU16() !== data_term) {
|
||||
throw `invalid data term @ ${read.pos - 2}`;
|
||||
}
|
||||
}
|
||||
|
||||
// read trailing magic
|
||||
meta.magic2 = read_string();
|
||||
|
||||
// compute checksum and verify
|
||||
let cksum = 0;
|
||||
for (let i=0; i<read.pos; i++) {
|
||||
cksum = cksum ^ view.getUint8(i);
|
||||
}
|
||||
|
||||
meta.checksum = cksum;
|
||||
|
||||
if (cksum !== read.readU32()) {
|
||||
throw `checksum mismatch`;
|
||||
}
|
||||
|
||||
delete meta.skip;
|
||||
Object.assign(this, meta);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
get_layer_lines(layer) {
|
||||
if (this.view) {
|
||||
if (layer > this.layers.length) {
|
||||
throw `layer out of range 0-${this.layers.length}`;
|
||||
}
|
||||
let out = [];
|
||||
let rec = this.layers[layer];
|
||||
let { lines, pos } = rec;
|
||||
for (let j=0; j<lines; j++) {
|
||||
let d1 = this.view.getUint16(pos); pos += 2;
|
||||
let d2 = this.view.getUint16(pos); pos += 2;
|
||||
let d3 = this.view.getUint16(pos); pos += 2;
|
||||
let y_start = d1 >> 3;
|
||||
let y_end = ((d1 & 0b111) << 10) | (d2 >> 6);
|
||||
let x_end = ((d2 & 0b111111) << 8) | (d3 >> 8);
|
||||
let color = d3 & 0xff;
|
||||
out.push({y_start, y_end, x_end, color});
|
||||
}
|
||||
return out;
|
||||
} else {
|
||||
throw "missing data view";
|
||||
}
|
||||
}
|
||||
|
||||
write() {
|
||||
let output = new ArrayWriter();
|
||||
|
||||
output.write_string = function write_string(str, dbl = false) {
|
||||
let len = str.length;
|
||||
output.writeU32(dbl ? len * 2 : len + 1);
|
||||
let pos = 0;
|
||||
while (pos < len) {
|
||||
if (dbl) {
|
||||
output.writeU8(0);
|
||||
}
|
||||
output.writeU8(str.charCodeAt(pos++));
|
||||
}
|
||||
if (!dbl) {
|
||||
output.writeU8(0);
|
||||
}
|
||||
}
|
||||
|
||||
let layers = this.layers;
|
||||
this.layer_count = layers.length;
|
||||
output.write_string(this.magic1);
|
||||
output.writeU16(this.version);
|
||||
output.write_string(this.model);
|
||||
output.writeU16(this.layer_count);
|
||||
output.writeU16(this.res_x);
|
||||
output.writeU16(this.res_y);
|
||||
output.writeU32(this.height);
|
||||
output.skip(60);
|
||||
output.skip(26912); // thumb
|
||||
output.writeU16(data_term);
|
||||
output.skip(168200); // preview1
|
||||
output.writeU16(data_term);
|
||||
output.skip(168200); // preview1
|
||||
output.writeU16(data_term);
|
||||
output.write_string(this.dim_x, true);
|
||||
output.write_string(this.dim_y, true);
|
||||
output.write_string(this.layer, true);
|
||||
output.writeU16(this.light_on);
|
||||
output.writeU16(this.light_off);
|
||||
output.writeU16(this.base_light_on);
|
||||
output.writeU16(this.base_layers);
|
||||
output.writeU16(this.base_lift_dist);
|
||||
output.writeU16(this.base_lift_speed);
|
||||
output.writeU16(this.lift_dist);
|
||||
output.writeU16(this.lift_speed);
|
||||
output.writeU16(this.down_speed);
|
||||
output.writeU16(this.base_light_pwm);
|
||||
output.writeU16(this.light_pwm);
|
||||
|
||||
// write placeholder layer length to capture position
|
||||
for (let layer of layers) {
|
||||
layer.l1 = output.pos;
|
||||
output.writeU32(0);
|
||||
}
|
||||
output.writeU16(data_term);
|
||||
|
||||
// write out encoded layers
|
||||
for (let layer of layers) {
|
||||
layer.l2 = output.pos;
|
||||
// placeholder to be written post
|
||||
output.writeU32(0);
|
||||
output.writeU32(layer.lines.length);
|
||||
let start = output.pos;
|
||||
for (let line of layer.lines) {
|
||||
let b1 = (line.y_start >> 5);
|
||||
let b2 = ((line.y_start << 3) | (line.y_end >> 10)) & 0xff;
|
||||
let b3 = (line.y_end >> 2) & 0xff;
|
||||
let b4 = ((line.y_end << 6) | (line.x_end >> 8)) & 0xff;
|
||||
let b5 = (line.x_end) & 0xff;
|
||||
output.writeU8(b1);
|
||||
output.writeU8(b2);
|
||||
output.writeU8(b3);
|
||||
output.writeU8(b4);
|
||||
output.writeU8(b5);
|
||||
output.writeU8(line.color);
|
||||
}
|
||||
layer.length = output.pos - start;
|
||||
output.writeU16(data_term);
|
||||
}
|
||||
|
||||
output.write_string(this.magic2);
|
||||
let ckpos = output.pos;
|
||||
|
||||
// retrace and write layer lengths
|
||||
for (let layer of layers) {
|
||||
output.seek(layer.l1);
|
||||
output.writeU32(layer.length);
|
||||
output.seek(layer.l2);
|
||||
output.writeU32(layer.length);
|
||||
}
|
||||
|
||||
output.seek(ckpos);
|
||||
// compute xor checksum
|
||||
let cksum = 0;
|
||||
let array = output.array;
|
||||
for (let i=0; i<array.length; i++) {
|
||||
cksum = cksum ^ (array[i] || 0);
|
||||
}
|
||||
output.writeU32(cksum);
|
||||
|
||||
return output.toBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
CXDLP.export = function(params) {
|
||||
let { settings, width, height, slices } = params;
|
||||
|
||||
let cxdlp = new CXDLP();
|
||||
cxdlp.layers = slices.map(a => {return { lines: a }});
|
||||
|
||||
return cxdlp.write();
|
||||
};
|
||||
|
||||
CXDLP.render = function(params) {
|
||||
let { width, height, index, widgets, scaleX, scaleY } = params;
|
||||
let width2 = width / 2, height2 = height / 2;
|
||||
let array = [];
|
||||
let count = 0;
|
||||
|
||||
function scaleMovePoly(poly) {
|
||||
let points = poly.points;
|
||||
poly._bounds = undefined;
|
||||
for (let i=0, il=points.length; i<il; i++) {
|
||||
let p = points[i];
|
||||
p.y = height - (p.y * scaleY + height2);
|
||||
p.x = p.x * scaleX + width2;
|
||||
}
|
||||
if (poly.inner) {
|
||||
for (let i=0, ia=poly.inner, il=poly.inner.length; i<il; i++) {
|
||||
scaleMovePoly(ia[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// serialize poly into wasm heap memory
|
||||
function writePoly(writer, poly) {
|
||||
let pos = writer.skip(2);
|
||||
let inner = poly.inner;
|
||||
writer.writeU16(inner ? inner.length : 0, true);
|
||||
let points = poly.points;
|
||||
let bounds = poly.bounds;
|
||||
writer.writeU16(points.length, true);
|
||||
writer.writeU16(bounds.minx, true);
|
||||
writer.writeU16(bounds.maxx, true);
|
||||
writer.writeU16(bounds.miny, true);
|
||||
writer.writeU16(bounds.maxy, true);
|
||||
for (let j=0, jl=points.length; j<jl; j++) {
|
||||
let point = points[j];
|
||||
writer.writeF32(point.x, true);
|
||||
writer.writeF32(point.y, true);
|
||||
}
|
||||
if (inner && inner.length) {
|
||||
for (let i=0, il=inner.length; i<il; i++) {
|
||||
writePoly(writer, inner[i]);
|
||||
}
|
||||
}
|
||||
// write total struct length at struct head
|
||||
writer.view.setUint16(pos, writer.pos - pos, true);
|
||||
}
|
||||
|
||||
widgets.forEach(widget => {
|
||||
let slice = widget.slices[index];
|
||||
if (slice) {
|
||||
if (slice.synth) count++;
|
||||
let polys = slice.unioned;
|
||||
if (!polys) polys = slice.tops.map(t => t.poly);
|
||||
if (slice.supports) polys.appendAll(slice.supports);
|
||||
array.appendAll(polys.map(poly => {
|
||||
return poly.clone(true).move(widget.track.pos);
|
||||
}));
|
||||
count += polys.length;
|
||||
}
|
||||
});
|
||||
|
||||
let wasm = kiri.driver.SLA.wasm;
|
||||
let imagelen = width * height;
|
||||
let writer = new self.DataWriter(new DataView(wasm.memory.buffer), imagelen);
|
||||
writer.writeU16(width, true);
|
||||
writer.writeU16(height, true);
|
||||
writer.writeU16(array.length, true);
|
||||
|
||||
// scale and move all polys to fit in rendered platform coordinates
|
||||
for (let i=0, il=array.length; i<il; i++) {
|
||||
let poly = array[i];
|
||||
scaleMovePoly(poly);
|
||||
writePoly(writer, poly);
|
||||
}
|
||||
wasm.render(0, imagelen, 0);
|
||||
let image = wasm.heap;//.slice(0, imagelen);
|
||||
let lines = [];
|
||||
for (let x=0; x<width; x++) {
|
||||
let y_start = 0;
|
||||
let lastv = 0;
|
||||
for (let y=0; y<height; y++) {
|
||||
let v = image[x * height + y];
|
||||
if (v !== lastv) {
|
||||
if (lastv) {
|
||||
// emit any non-zero sequence
|
||||
lines.push({y_start, y_end: y, x_end: x, color: lastv});
|
||||
}
|
||||
if (v) {
|
||||
// start a new sequence
|
||||
y_start = y;
|
||||
}
|
||||
}
|
||||
lastv = v;
|
||||
}
|
||||
}
|
||||
return { lines };
|
||||
// return { image, lines };
|
||||
}
|
||||
|
||||
if (!this.navigator && this.process && this.process.env) {
|
||||
let fs = require('fs');
|
||||
let self = this;
|
||||
eval(fs.readFileSync("src/add/class.js").toString());
|
||||
let args = process.argv.slice(2);
|
||||
let file = args.shift();
|
||||
// let fpos = 0;
|
||||
let view = new DataView(fs.readFileSync(file).buffer);
|
||||
let read = new DataReader(view, 0);
|
||||
let cxdlp = new CXDLP().read(view);
|
||||
console.log({
|
||||
cxdlp,
|
||||
layers5: cxdlp.layers.slice(0,5),
|
||||
lines1: cxdlp.get_layer_lines(1)
|
||||
});
|
||||
} else if (this.navigator) {
|
||||
this.CXDLP = CXDLP;
|
||||
}
|
||||
}());
|
||||
|
|
@ -9,6 +9,9 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
#slider {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
|
@ -17,7 +20,7 @@
|
|||
}
|
||||
</style>
|
||||
<script src="/src/add/class.js"></script>
|
||||
<script src="/src/mode/sla/x_halot.js"></script>
|
||||
<script src="/src/mode/sla/x_cxdlp.js"></script>
|
||||
<script>
|
||||
function onload() {
|
||||
document.addEventListener("drop", (ev) => {
|
||||
|
|
@ -30,6 +33,7 @@
|
|||
console.log({cxdlp});
|
||||
slider.max = layers.value = cxdlp.layers.length - 1;
|
||||
render(slider.value = layer.value = 0);
|
||||
meta(cxdlp);
|
||||
};
|
||||
reader.readAsArrayBuffer(files[0]);
|
||||
console.log("drop", files);
|
||||
|
|
@ -53,6 +57,20 @@
|
|||
render(slider.value = layer.value);
|
||||
};
|
||||
}
|
||||
function row(key,value) {
|
||||
return `<tr><th>${key}</th><td>${value}</td></tr>`;
|
||||
}
|
||||
function meta(cxdlp) {
|
||||
let rows = Object.entries(cxdlp).filter(e => {
|
||||
let t = typeof(e[1]);
|
||||
return t === 'number' || t === 'string';
|
||||
}).map(e => row(e[0], e[1]));
|
||||
params.innerHTML = [
|
||||
'<table>',
|
||||
...rows,
|
||||
'</table>'
|
||||
].join('');
|
||||
}
|
||||
function render(layer) {
|
||||
let cxdlp = window.cxdlp;
|
||||
if (!cxdlp || layer > cxdlp.layers.length || layer < 0) {
|
||||
|
|
@ -84,5 +102,11 @@
|
|||
<input id="layers" size=5 disabled=true>
|
||||
</div>
|
||||
<canvas id="canvas" width=4000 height=2000></canvas>
|
||||
<div>
|
||||
<div id="params"></div>
|
||||
<canvas id="thumb"></canvas>
|
||||
<canvas id="pre1"></canvas>
|
||||
<canvas id="pre2"></canvas>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -7,14 +7,5 @@
|
|||
|
||||
<div class="header"><label>download</label></div>
|
||||
<div class="f-row box">
|
||||
<button id="print-photon" class="grow">download</button>
|
||||
</div>
|
||||
|
||||
<div class="header"><label>preview</label></div>
|
||||
<div class="f-col box a-center j-center">
|
||||
<div class="f-row a-center j-center">
|
||||
<input id="print-range" type="range" class="grow">
|
||||
<label id="print-layer"></label>
|
||||
</div>
|
||||
<canvas id="print-canvas" width="1440" height="2560"></canvas>
|
||||
<button id="print-sla" class="grow">download</button>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue