/** Copyright Stewart Allen -- All Rights Reserved */ import { imageToRowMajor, renderRasterLayers, round } from './raster.js'; import { createLayerMetadata, createProcessMetadata, estimatePrintTime, motionFor } from './meta.js'; const FAMILY = "GOO"; const FILE_VERSION = "V3.0"; const FILE_MAGIC = [0x07, 0x00, 0x00, 0x00, 0x44, 0x4c, 0x50, 0x00]; const DELIMITER = [0x0d, 0x0a]; const LAYER_MAGIC = 0x55; const HEADER_SIZE = 195477; const LAYER_DEF_SIZE = 70; const FOOTER_SIZE = 11; const MAX_RUN = 0x0fffffff; const FORMAT_RECORDS = { goo: { ext: "goo", machine: "Elegoo GOO", notes: "Elegoo GOO" }, prz: { ext: "prz", machine: "Phrozen Sonic Mini 8K S", notes: "Phrozen PRZ GOO variant" } }; function encode(print, progress, renderer) { let format = print.settings.device.slaFormat || "goo"; let rec = FORMAT_RECORDS[format]; if (!rec) { return Promise.reject(new Error(`${FAMILY} format .${format} is not implemented.`)); } if (renderer) { return Promise.resolve(encodeGOO(print, progress, renderer, rec)); } return import('./x_photon.js').then(({ photon }) => encodeGOO(print, progress, photon, rec)); } function encodeGOO(print, progress, photon, rec) { let layers = []; let previewSmall = createPreview(116, 116); let previewLarge = createPreview(290, 290); let { ctx, volume } = renderRasterLayers(print, photon, params => { let { index, rendered, z, ctx } = params; let { device, process, width, height } = ctx; let layerMeta = createLayerMetadata(device, process, index, z, rendered.area); let raster = imageToRowMajor(rendered.image, width, height, normalizePixel); accumulatePreview(previewSmall, rendered.image, width, height); accumulatePreview(previewLarge, rendered.image, width, height); layers.push({ index, data: encodeRLE(raster), z: layerMeta.z, pausePositionZ: ctx.device.maxHeight || ctx.device.bedHeight || 0, exposure: layerMeta.exposure, lightOffDelay: layerMeta.lightOffDelay, waitTimeAfterCure: layerMeta.waitAfterCure, waitTimeAfterLift: layerMeta.waitAfterLift, waitTimeBeforeCure: layerMeta.waitBeforeCure, liftHeight: layerMeta.liftHeight, liftSpeed: layerMeta.liftSpeed, liftHeight2: 0, liftSpeed2: 0, retractHeight: layerMeta.retractHeight, retractSpeed: layerMeta.retractSpeed, retractHeight2: 0, retractSpeed2: 0, lightPWM: layerMeta.lightPWM }); }, progress ? (value) => progress(value * 0.85, `${rec.ext}_encode`) : null); let file = writeGOO({ ctx, layers, volume, previewSmall, previewLarge, rec }); if (progress) progress(1, `${rec.ext}_write`); return { file, layers: layers.length, volume }; } function writeGOO(params) { let { ctx, layers, volume, previewSmall, previewLarge, rec } = params; let total = HEADER_SIZE + FOOTER_SIZE; layers.forEach(layer => { layer.dataLength = layer.data.length; total += LAYER_DEF_SIZE + layer.dataLength + DELIMITER.length; }); let writer = new BinaryWriter(new ArrayBuffer(total)); writeHeader(writer, { ctx, layers, volume, previewSmall, previewLarge, rec }); writer.seek(HEADER_SIZE); layers.forEach(layer => writeLayer(writer, layer)); writeFooter(writer); if (writer.pos !== total) { throw new Error(`GOO writer size mismatch (${writer.pos} !== ${total})`); } return writer.buffer; } function writeHeader(writer, params) { let { ctx, layers, volume, previewSmall, previewLarge, rec } = params; let { device, process, width, height } = ctx; let meta = createProcessMetadata(device, process); let bottomMotion = motionFor(device, process, true); let layerMotionValue = motionFor(device, process, false); writer.writeString(FILE_VERSION, 4); writer.writeBytes(FILE_MAGIC); writer.writeString("Grid.Space", 32); writer.writeString("1.0", 24); writer.writeString(formatDate(), 24); writer.writeString(device.slaMachineName || device.deviceName || rec.machine, 32); writer.writeString("DLP", 32); writer.writeString("Grid.Space", 32); writer.writeU16(process.slaAntiAlias || 1); writer.writeU16(1); writer.writeU16(0); writer.writeBytes(previewSmall.image); writer.writeBytes(DELIMITER); writer.writeBytes(previewLarge.image); writer.writeBytes(DELIMITER); writer.writeU32(layers.length); writer.writeU16(width); writer.writeU16(height); writer.writeU8(0); writer.writeU8(0); writer.writeF32(device.bedWidth); writer.writeF32(device.bedDepth); writer.writeF32(device.maxHeight || device.bedHeight || 0); writer.writeF32(process.slaSlice); writer.writeF32(meta.exposure); writer.writeU8(1); writer.writeF32(0); writer.writeF32(0); writer.writeF32(0); writer.writeF32(meta.bottomWaitBeforeCure); writer.writeF32(0); writer.writeF32(0); writer.writeF32(meta.waitBeforeCure); writer.writeF32(meta.bottomExposure); writer.writeU32(meta.bottomLayers); 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(meta.bottomLightPWM); writer.writeU16(meta.lightPWM); writer.writeU8(0); writer.writeU32(estimatePrintTime(device, process, layers.length)); writer.writeF32(round(volume)); writer.writeF32(0); writer.writeF32(0); writer.writeString("$", 8); writer.writeU32(HEADER_SIZE); writer.writeU8(1); writer.writeU16(0); if (writer.pos !== HEADER_SIZE) { throw new Error(`GOO header size mismatch (${writer.pos} !== ${HEADER_SIZE})`); } } function writeLayer(writer, layer) { writer.writeU16(0); writer.writeF32(layer.pausePositionZ); writer.writeF32(layer.z); writer.writeF32(layer.exposure); writer.writeF32(layer.lightOffDelay); writer.writeF32(layer.waitTimeAfterCure); writer.writeF32(layer.waitTimeAfterLift); writer.writeF32(layer.waitTimeBeforeCure); writer.writeF32(layer.liftHeight); writer.writeF32(layer.liftSpeed); writer.writeF32(layer.liftHeight2); writer.writeF32(layer.liftSpeed2); writer.writeF32(layer.retractHeight); writer.writeF32(layer.retractSpeed); writer.writeF32(layer.retractHeight2); writer.writeF32(layer.retractSpeed2); writer.writeU16(layer.lightPWM); writer.writeBytes(DELIMITER); writer.writeU32(layer.dataLength); writer.writeBytes(layer.data); writer.writeBytes(DELIMITER); } function writeFooter(writer) { writer.writeU8(0); writer.writeU8(0); writer.writeU8(0); writer.writeBytes(FILE_MAGIC); } function read(input) { let view = toDataView(input); let header = readHeader(view); let layers = readLayers(view, header.layerDefAddress, header.layerCount); let footerOffset = layers.length ? layers[layers.length - 1].dataOffset + layers[layers.length - 1].dataLength + DELIMITER.length : header.layerDefAddress; let footer = { offset: footerOffset, magic: Array.from(new Uint8Array(view.buffer, view.byteOffset + footerOffset + 3, 8)) }; if (!sameBytes(footer.magic, FILE_MAGIC)) { throw new Error("invalid GOO footer magic"); } return { header, layers, footer }; } function readHeader(view) { let pos = 0; let header = {}; header.version = readString(view, pos, 4); pos += 4; header.magic = readBytes(view, pos, 8); pos += 8; header.softwareName = readString(view, pos, 32); pos += 32; header.softwareVersion = readString(view, pos, 24); pos += 24; header.fileCreateTime = readString(view, pos, 24); pos += 24; header.machineName = readString(view, pos, 32); pos += 32; header.machineType = readString(view, pos, 32); pos += 32; header.profileName = readString(view, pos, 32); pos += 32; header.antiAliasingLevel = view.getUint16(pos, false); pos += 2; header.greyLevel = view.getUint16(pos, false); pos += 2; header.blurLevel = view.getUint16(pos, false); pos += 2; pos += 116 * 116 * 2 + 2 + 290 * 290 * 2 + 2; header.layerCount = view.getUint32(pos, false); pos += 4; header.resolutionX = view.getUint16(pos, false); pos += 2; header.resolutionY = view.getUint16(pos, false); pos += 2; header.mirrorX = view.getUint8(pos++) !== 0; header.mirrorY = view.getUint8(pos++) !== 0; header.displayWidth = view.getFloat32(pos, false); pos += 4; header.displayHeight = view.getFloat32(pos, false); pos += 4; header.machineZ = view.getFloat32(pos, false); pos += 4; header.layerHeight = view.getFloat32(pos, false); pos += 4; header.exposureTime = view.getFloat32(pos, false); pos += 4; header.delayMode = view.getUint8(pos++); header.lightOffDelay = view.getFloat32(pos, false); pos += 4; pos += 6 * 4; header.bottomExposureTime = view.getFloat32(pos, false); pos += 4; header.bottomLayerCount = view.getUint32(pos, false); pos += 4; pos += 16 * 4 + 2 + 2 + 1; header.printTime = view.getUint32(pos, false); pos += 4; header.volume = view.getFloat32(pos, false); pos += 4; header.materialGrams = view.getFloat32(pos, false); pos += 4; header.materialCost = view.getFloat32(pos, false); pos += 4; header.priceCurrencySymbol = readString(view, pos, 8); pos += 8; header.layerDefAddress = view.getUint32(pos, false); pos += 4; header.grayScaleLevel = view.getUint8(pos++); header.transitionLayerCount = view.getUint16(pos, false); pos += 2; if (header.version !== FILE_VERSION) { throw new Error(`invalid GOO version ${header.version}`); } if (!sameBytes(header.magic, FILE_MAGIC)) { throw new Error("invalid GOO magic"); } return header; } function readLayers(view, offset, count) { let layers = []; let pos = offset; for (let index=0; index 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 : 3; let type = color === 0 ? 0 : color === 255 ? 3 : 1; let first = (type << 6) | (lengthCode << 4) | (stride & 0x0f); let ext = []; if (lengthCode >= 1) ext.unshift((stride >> 4) & 0xff); if (lengthCode >= 2) ext.unshift((stride >> 12) & 0xff); if (lengthCode >= 3) ext.unshift((stride >> 20) & 0xff); output.push(first); if (type === 1) { output.push(color); } output.push(...ext); run -= stride; } } 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) { throw new Error(`invalid GOO RLE layer magic ${rle[0]}`); } if (rle[rle.length - 1] !== checksum(rle.subarray(0, rle.length - 1))) { throw new Error("invalid GOO RLE checksum"); } let output = new Uint8Array(pixelCount); let pixel = 0; let color = 0; let last = rle.length - 1; for (let i=1; i> 6; let stride = 0; let strideIndex0 = i; let strideIndex1 = i + 1; let strideIndex2 = i + 2; let strideIndex3 = i + 3; if (chunkType === 0) { color = 0; } else if (chunkType === 1) { color = rle[++i]; strideIndex1++; strideIndex2++; strideIndex3++; } else if (chunkType === 2) { let diffType = (rle[i] >> 4) & 0x03; let diffValue = rle[i] & 0x0f; if (diffType === 0) { color += diffValue; stride = 1; } else if (diffType === 1) { color += diffValue; stride = rle[++i]; } else if (diffType === 2) { color -= diffValue; stride = 1; } else { color -= diffValue; stride = rle[++i]; } color &= 0xff; } else { color = 255; } if (chunkType !== 2) { let chunkLength = (rle[strideIndex0] >> 4) & 0x03; if (chunkLength === 0) { stride = rle[strideIndex0] & 0x0f; } else if (chunkLength === 1) { stride = (rle[strideIndex1] << 4) + (rle[strideIndex0] & 0x0f); i += 1; } else if (chunkLength === 2) { stride = (rle[strideIndex1] << 12) + (rle[strideIndex2] << 4) + (rle[strideIndex0] & 0x0f); i += 2; } else { stride = (rle[strideIndex1] << 20) + (rle[strideIndex2] << 12) + (rle[strideIndex3] << 4) + (rle[strideIndex0] & 0x0f); i += 3; } } output.fill(color, pixel, pixel + stride); pixel += stride; } if (pixel !== pixelCount) { throw new Error(`GOO RLE pixel count mismatch (${pixel} !== ${pixelCount})`); } return output; } function createPreview(width, height) { return { width, height, image: new Uint8Array(width * height * 2) }; } function accumulatePreview(preview, image, width, height) { let xscale = width / preview.width; let yscale = height / preview.height; for (let y=0; y value.toString().padStart(2, "0"); return [ date.getFullYear(), pad(date.getMonth() + 1), pad(date.getDate()) ].join("-") + " " + [ pad(date.getHours()), pad(date.getMinutes()), pad(date.getSeconds()) ].join(":"); } function readString(view, offset, length) { let chars = []; for (let i=0; i>> 0, false); this.pos += 4; } writeF32(value) { this.view.setFloat32(this.pos, Number(value || 0), false); this.pos += 4; } } function supports(format) { return FORMAT_RECORDS[format] !== undefined; } export const GOO = { encode, supports, read, decodeLayer, encodeRLE, decodeRLE, formats: FORMAT_RECORDS };