add tilt tray for .goo and new elegoo targets

This commit is contained in:
Stewart Allen 2026-05-16 21:22:08 -04:00
commit b65d4ae57e
6 changed files with 131 additions and 22 deletions

View file

@ -32,7 +32,7 @@
| **Other Binary Formats** |
| AnetFile | ✓ | ✓ | n4, n7 | 3 | Yes (N4/N7) | RLE | Anet printers |
| FDGFile | ✓ | ✓ | fdg | 2 | No | RLE | Voxelab |
| GooFile | ✓ | ✓ | goo, prz | - | No | RLE+delim | Elegoo/Phrozen |
| GooFile | ✓ | ✓ | goo, prz | - | No | Chunked RLE + checksum | Elegoo/Phrozen |
| LGSFile | ✓ | ✓ | lgs, lgs30, lgs120, lgs4k | - | Yes | Struct | Longer Orange |
| MDLPFile | ✓ | ✓ | mdlp | 1 | No | Vector | Makerbase MKS |
| GR1File | ✓ | ✓ | gr1 | - | No | Vector | GR Workshop |
@ -559,6 +559,8 @@ Non-delta maximum repeat: 0x0FFFFFFF pixels.
Layer record delimiter after payload: 0x0D 0x0A.
```
**Tilt-vat motion:** Mars 5 Ultra and Saturn 4 Ultra GOO files are treated as tilt-vat machines. UVtools identifies them by machine name and substitutes tiny synthetic motion values before encode: lift height `0.05`, lift speed `0.05`, retract height `0`, retract speed `0.05`, with about 4 seconds of motor time in print-time estimates. Kiri models this as `sla_motion: "tilt"` on the device profile instead of overloading user peel distance/speed fields.
### CXDLP (Creality)
**Extension:** `.cxdlp`

View file

@ -104,6 +104,7 @@ function device_from_code(code,mode) {
slaFormatVersion: valueOf(set.sla_format_version, 1),
slaMachineEditable: valueOf(set.sla_machine_editable, false),
slaMachineName: valueOf(set.sla_machine_name, ''),
slaMotion: valueOf(set.sla_motion, ''),
deviceZMax: valueOf(set.z_move_max, 0),
gcodeTime: valueOf(set.time_factor, 1),
maxHeight: valueOf(set.build_height || set.max_height, 150),

View file

@ -0,0 +1,17 @@
{
"no_clone": true,
"cmd": {},
"settings": {
"bed_width": 153.36,
"bed_depth": 77.76,
"bed_height": 1,
"max_height": 165,
"resolution_x": 8520,
"resolution_y": 4320,
"sla_format": "goo",
"sla_file_ext": "goo",
"sla_machine_name": "Mars 5 Ultra",
"sla_motion": "tilt",
"sla_format_version": 3
}
}

View file

@ -0,0 +1,17 @@
{
"no_clone": true,
"cmd": {},
"settings": {
"bed_width": 218.88,
"bed_depth": 122.88,
"bed_height": 1,
"max_height": 220,
"resolution_x": 11520,
"resolution_y": 5120,
"sla_format": "goo",
"sla_file_ext": "goo",
"sla_machine_name": "Saturn 4 Ultra 12K",
"sla_motion": "tilt",
"sla_format_version": 3
}
}

View file

@ -0,0 +1,17 @@
{
"no_clone": true,
"cmd": {},
"settings": {
"bed_width": 211.68,
"bed_depth": 118.37,
"bed_height": 1,
"max_height": 220,
"resolution_x": 15120,
"resolution_y": 6230,
"sla_format": "goo",
"sla_file_ext": "goo",
"sla_machine_name": "Saturn 4 Ultra 16K",
"sla_motion": "tilt",
"sla_format_version": 3
}
}

View file

@ -11,6 +11,13 @@ const HEADER_SIZE = 195477;
const LAYER_DEF_SIZE = 70;
const FOOTER_SIZE = 11;
const MAX_RUN = 0x0fffffff;
const TILT_MOTION = {
liftHeight: 0.05,
liftSpeed: 0.05,
retractHeight: 0,
retractSpeed: 0.05,
motorTime: 4
};
const FORMAT_RECORDS = {
goo: { ext: "goo", machine: "Elegoo GOO", notes: "Elegoo GOO" },
@ -40,8 +47,9 @@ function encodeGOO(print, progress, photon, rec) {
let { ctx, volume } = renderRasterLayers(print, photon, params => {
let { index, rendered, bottom, z, ctx } = params;
let { process, width, height } = ctx;
let raster = imageToRowMajor(rendered.image, width, height, value => value ? 255 : 0);
let { device, process, width, height } = ctx;
let motion = layerMotion(device, process, bottom);
let raster = imageToRowMajor(rendered.image, width, height, normalizePixel);
accumulatePreview(previewSmall, rendered.image, width, height);
accumulatePreview(previewLarge, rendered.image, width, height);
@ -56,12 +64,12 @@ function encodeGOO(print, progress, photon, rec) {
waitTimeAfterCure: 0,
waitTimeAfterLift: 0,
waitTimeBeforeCure: bottom ? process.slaBaseOff : process.slaLayerOff,
liftHeight: bottom ? process.slaBasePeelDist : process.slaPeelDist,
liftSpeed: (bottom ? process.slaBasePeelLiftRate : process.slaPeelLiftRate) * 60,
liftHeight: motion.liftHeight,
liftSpeed: motion.liftSpeed,
liftHeight2: 0,
liftSpeed2: 0,
retractHeight: bottom ? process.slaBasePeelDist : process.slaPeelDist,
retractSpeed: (bottom ? process.slaBasePeelDropRate : process.slaPeelDropRate) * 60,
retractHeight: motion.retractHeight,
retractSpeed: motion.retractSpeed,
retractHeight2: 0,
retractSpeed2: 0,
lightPWM: 255
@ -101,6 +109,8 @@ function writeHeader(writer, params) {
let { device, process, width, height } = ctx;
let bottomOff = process.slaBaseOff || 0;
let layerOff = process.slaLayerOff || 0;
let bottomMotion = layerMotion(device, process, true);
let layerMotionValue = layerMotion(device, process, false);
writer.writeString(FILE_VERSION, 4);
writer.writeBytes(FILE_MAGIC);
@ -137,19 +147,19 @@ function writeHeader(writer, params) {
writer.writeF32(layerOff);
writer.writeF32(process.slaBaseOn);
writer.writeU32(process.slaBaseLayers || 0);
writer.writeF32(process.slaBasePeelDist);
writer.writeF32(process.slaBasePeelLiftRate * 60);
writer.writeF32(process.slaPeelDist);
writer.writeF32(process.slaPeelLiftRate * 60);
writer.writeF32(process.slaBasePeelDist);
writer.writeF32(process.slaBasePeelDropRate * 60);
writer.writeF32(process.slaPeelDist);
writer.writeF32(process.slaPeelDropRate * 60);
writer.writeF32(bottomMotion.liftHeight);
writer.writeF32(bottomMotion.liftSpeed);
writer.writeF32(layerMotionValue.liftHeight);
writer.writeF32(layerMotionValue.liftSpeed);
writer.writeF32(bottomMotion.retractHeight);
writer.writeF32(bottomMotion.retractSpeed);
writer.writeF32(layerMotionValue.retractHeight);
writer.writeF32(layerMotionValue.retractSpeed);
for (let i=0; i<8; i++) writer.writeF32(0);
writer.writeU16(255);
writer.writeU16(255);
writer.writeU8(0);
writer.writeU32(estimatePrintTime(process, layers.length));
writer.writeU32(estimatePrintTime(device, process, layers.length));
writer.writeF32(round(volume));
writer.writeF32(0);
writer.writeF32(0);
@ -187,6 +197,20 @@ function writeLayer(writer, layer) {
writer.writeBytes(DELIMITER);
}
function layerMotion(device, process, bottom) {
if (device.slaMotion === "tilt") {
return TILT_MOTION;
}
return {
liftHeight: bottom ? process.slaBasePeelDist : process.slaPeelDist,
liftSpeed: (bottom ? process.slaBasePeelLiftRate : process.slaPeelLiftRate) * 60,
retractHeight: bottom ? process.slaBasePeelDist : process.slaPeelDist,
retractSpeed: (bottom ? process.slaBasePeelDropRate : process.slaPeelDropRate) * 60,
motorTime: 0
};
}
function writeFooter(writer) {
writer.writeU8(0);
writer.writeU8(0);
@ -307,6 +331,7 @@ function decodeLayer(view, layer, pixelCount) {
function encodeRLE(input) {
let output = [LAYER_MAGIC];
let previous = 0;
let current = input[0] || 0;
let run = 0;
@ -315,20 +340,38 @@ function encodeRLE(input) {
if (value === current && run < MAX_RUN) {
run++;
} else {
writeRun(output, current, run);
writeRun(output, current, previous, run);
previous = current;
current = value;
run = 1;
}
}
writeRun(output, current, run);
writeRun(output, current, previous, run);
output.push(checksum(output));
return Uint8Array.from(output);
}
function writeRun(output, color, run) {
function writeRun(output, color, previous, run) {
while (run > 0) {
let stride = Math.min(run, MAX_RUN);
let diff = Math.abs(color - previous);
if (color > 0 && color < 255 && diff <= 0x0f && stride <= 0xff) {
let first = 0x80 | (diff & 0x0f);
if (stride > 1) {
first |= 0x10;
}
if (color < previous) {
first |= 0x20;
}
output.push(first);
if (stride > 1) {
output.push(stride);
}
run -= stride;
continue;
}
let lengthCode = stride <= 0x0f ? 0
: stride <= 0x0fff ? 1
: stride <= 0x0fffff ? 2
@ -350,6 +393,17 @@ function writeRun(output, color, run) {
}
}
function normalizePixel(value) {
value = value || 0;
if (value === 0 || value === 255) {
return value;
}
// The JS fallback rasterizer paints red at 200, so rescale that range to
// retain binary cure strength while preserving antialiased edge values.
return Math.min(255, Math.round(value * 255 / 200));
}
function decodeRLE(input, pixelCount) {
let rle = input instanceof Uint8Array ? input : new Uint8Array(input);
if (rle[0] !== LAYER_MAGIC) {
@ -452,11 +506,12 @@ function accumulatePreview(preview, image, width, height) {
}
}
function estimatePrintTime(process, layers) {
function estimatePrintTime(device, process, layers) {
let bottom = Math.min(process.slaBaseLayers || 0, layers);
let normal = Math.max(0, layers - bottom);
let bottomTime = number(process.slaBaseOn) + number(process.slaBaseOff);
let normalTime = number(process.slaLayerOn) + number(process.slaLayerOff);
let motorTime = device.slaMotion === "tilt" ? TILT_MOTION.motorTime : 0;
let bottomTime = number(process.slaBaseOn) + number(process.slaBaseOff) + motorTime;
let normalTime = number(process.slaLayerOn) + number(process.slaLayerOff) + motorTime;
return Math.ceil(bottom * bottomTime + normal * normalTime);
}