Compare commits

...
Author SHA1 Message Date
Stewart Allen
d138275bbe update docs workflow 2026-07-26 13:52:55 -04:00
Stewart Allen
5f1f77add9 update docs workflow 2026-07-26 13:52:55 -04:00
Stewart Allen
ed1009fd15 update release notes 2026-07-26 13:52:55 -04:00
Stewart Allen
ff29a8c221 fix tool change dwell time 2026-07-26 13:52:55 -04:00
Stewart Allen
3be111af43 add volume rendering and use that for sla by default now 2026-07-26 13:52:55 -04:00
Stewart Allen
41629b2c76 improve sla supports 2026-07-26 13:52:55 -04:00
Stewart Allen
a0c9428d6c change sla supports 2026-07-26 13:52:55 -04:00
Stewart Allen
7e105f255e update .ctb options 2026-07-26 13:52:55 -04:00
Stewart Allen
b65d4ae57e add tilt tray for .goo and new elegoo targets 2026-07-26 13:52:55 -04:00
Stewart Allen
f32af7eeb1 fix widget + tab assignment when slicing 2026-07-26 13:52:55 -04:00
25 changed files with 1994 additions and 434 deletions

View file

@ -24,15 +24,14 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup bun
uses: oven-sh/setup-bun@v2
- name: Setup Node
uses: actions/setup-node@v4
with:
bun-version: latest
node-version: '22'
- name: Install dependencies
run: bun install
- name: Build docs
run:
bun docs-build
run: npm install
- name: Build docs
run: npm run docs-build
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Upload artifact
@ -43,4 +42,3 @@ jobs:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View file

@ -1,6 +1,7 @@
# DXF Import Implementation Review
## Overview
Added DXF import functionality to complement the existing DXF export feature, following the same architecture as SVG import.
## Implementation Summary
@ -28,6 +29,7 @@ Added DXF import functionality to complement the existing DXF export feature, fo
### Export Format (from `src/kiri/mode/laser/init-work.js`)
The existing `exportDXF()` function generates:
```dxf
0
SECTION
@ -58,12 +60,14 @@ ENDSEC
### Import Implementation Compatibility
✅ **Fully Compatible** with exported format:
- Parses POLYLINE entities with VERTEX sub-entities
- Reads group code 70 (closed flag)
- Reads group codes 10, 20, 30 (X, Y, Z coordinates)
- Handles SEQEND terminator
✅ **Additional Entity Support**:
- LWPOLYLINE (modern compact format)
- LINE (simple two-point segments)
- CIRCLE (converted to polygon)
@ -74,23 +78,27 @@ ENDSEC
### Entity Parsing
#### POLYLINE (Traditional Format)
- Uses VERTEX sub-entities for each point
- Flag 70 bit 0: closed (1) or open (0)
- Group codes: 10=X, 20=Y, 30=Z
- Terminated by SEQEND
#### LWPOLYLINE (Lightweight Format)
- Direct vertex coordinates (no VERTEX entities)
- More compact than POLYLINE
- Flag 70 bit 0: closed/open
- **Limitation**: Bulge values (arcs) not yet supported
#### LINE
- Simple two-point entity
- Group codes: 10,20,30 = start, 11,21,31 = end
- Always treated as open polyline
#### CIRCLE
- Center point (10,20,30) + radius (40)
- Converted to polygon based on segment size
- **Default**: 1mm segments (e.g., 10mm radius circle = ~63 segments)
@ -98,6 +106,7 @@ ENDSEC
- Calculation: `segments = max(minSegments, ceil(2πr / segmentSize))`
#### ARC
- Center (10,20,30) + radius (40)
- Start angle (50) and end angle (51) in degrees
- Converted to polyline based on segment size
@ -138,7 +147,7 @@ Load into scene
✅ **Line Ending Handling**: Normalizes \r\n, \r, \n
✅ **Whitespace**: Trims all lines (handles varying spacing in group codes)
✅ **Bounds Checking**: Prevents index out of bounds errors
✅ **Empty Entity Skip**: Ignores entities with < 2 points
✅ **Empty Entity Skip**: Ignores entities with fewer than 2 points
✅ **Default Values**: Safe defaults for missing Z coordinates
✅ **Error Handling**: Try/catch in parseAsync wrapper
@ -231,6 +240,7 @@ The DXF import dialog provides the following options:
## Comparison with SVG Import
### Similarities
- Both use polygon-based pipeline
- Both support nesting for holes
- Both extrude 2D to 3D
@ -238,18 +248,19 @@ The DXF import dialog provides the following options:
### Differences
| Feature | SVG Import | DXF Import |
|---------|-----------|------------|
| Parser | THREE.SVGLoader | Custom implementation |
| Dependency | three.js library | None (pure JS) |
| Arc Resolution | User-configurable | Fixed algorithm |
| DPI Setting | Yes | No (DXF is unitless) |
| Curve Types | Bezier, arcs, ellipses | Lines, arcs, circles |
| Y-Axis | Inverted (SVG quirk) | Standard (CAD) |
| Feature | SVG Import | DXF Import |
| -------------- | ---------------------- | --------------------- |
| Parser | THREE.SVGLoader | Custom implementation |
| Dependency | three.js library | None (pure JS) |
| Arc Resolution | User-configurable | Fixed algorithm |
| DPI Setting | Yes | No (DXF is unitless) |
| Curve Types | Bezier, arcs, ellipses | Lines, arcs, circles |
| Y-Axis | Inverted (SVG quirk) | Standard (CAD) |
## Code Quality
### Strengths
✅ Follows existing architecture patterns
✅ No external dependencies
✅ Clear, readable code with comments
@ -257,6 +268,7 @@ The DXF import dialog provides the following options:
✅ Consistent with SVG import style
### Areas for Improvement
⚠️ No comprehensive unit tests yet
⚠️ Could add SPLINE support for advanced CAD
⚠️ Could expose layer information to UI
@ -265,12 +277,14 @@ The DXF import dialog provides the following options:
## Security Considerations
✅ **Safe Parsing**:
- No `eval()` or code execution
- No file system access
- Bounds checking prevents array overflow
- String operations only (no binary exploits)
✅ **Input Validation**:
- parseFloat() for numeric values (NaN-safe)
- Trim whitespace (prevents injection)
- Length checks before array access
@ -278,11 +292,13 @@ The DXF import dialog provides the following options:
## Performance
**Expected Performance**:
- Small files (<100 entities): <10ms
- Medium files (100-1000 entities): <100ms
- Large files (1000-10000 entities): <1s
- Small files (fewer than 100 entities): under 10ms
- Medium files (100-1000 entities): under 100ms
- Large files (1000-10000 entities): under 1s
**Optimization Opportunities**:
- Could use worker thread for large files
- Could stream parse (currently loads entire file)
- Could skip unknown entities faster (regex match)

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 |
@ -210,6 +210,23 @@ Same header structure as CTB v3, with additional support for:
- PageNumber field in layer table
- Transition layers
#### 1.4 CTB Encrypted v4-5
**Extensions:** `.ctb`, `.encrypted.ctb`
**Magic:** `0x12FD0107` (LE)
**Versions:** 4, 5 (default: 5)
**Settings block:** 288-byte slicer-settings table encrypted with AES-256-CBC, no padding, zero-padded to 16-byte blocks
**Layer data:** CTB variable-length 7-bit grayscale RLE, then CTB layer XOR using `LayerXorKey` (`0xEFBEADDE` by default); some files additionally AES-encrypt layer-data subranges
Encrypted CTB is a different container from plain CTB v3/v4/v5, not just a flag on the v3 layout. The fixed 48-byte header points at the encrypted settings block and a final encrypted SHA-256 signature. The AES key/IV are derived from UVtools-compatible Chitubox constants with the `UVtools` XOR mask. Kiri keeps these primitives native in JS in `x_ctb_crypto.js`; full writer support still needs the encrypted settings table, layer pointer table, resin parameters, previews, and final signature wired into `x_ctb`.
Key tables:
- File header: 48 bytes
- Slicer settings: 288 bytes
- Layer pointer: 16 bytes
- Layer definition: 88 bytes
- Per-layer settings flag: `0x40` allow, `0x00` disallow
### 2. Anycubic Family
#### 2.1 Legacy Anycubic Formats (v1)
@ -559,6 +576,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`
@ -652,6 +671,46 @@ PrinterModel = SL1
}
```
### RSLA / VSLA (Grid.Space Generic)
**Extensions:** `.rsla`, `.vsla`
**Type:** ZIP with JSON manifest
**Layer Format:** RSLA stores raster PNG layers; VSLA stores SVG-like vector layer geometry
These formats are intended as a vendor-neutral interchange and debugging target. They carry enough process metadata to preserve behavior before conversion to a proprietary printer format.
**Shared manifest process fields:**
```json
{
"process": {
"bottomLayers": 5,
"transitionLayers": 8,
"exposure": 2.4,
"bottomExposure": 28,
"lightOffDelay": 0,
"bottomLightOffDelay": 0,
"waitBeforeCure": 0.5,
"bottomWaitBeforeCure": 2,
"waitAfterCure": 0,
"waitAfterLift": 0,
"liftHeight": 5,
"bottomLiftHeight": 6,
"liftSpeed": 180,
"bottomLiftSpeed": 60,
"retractHeight": 5,
"retractSpeed": 180,
"lightPWM": 255,
"bottomLightPWM": 255,
"motion": "normal",
"antiAlias": 1
}
}
```
Each layer also carries a resolved per-layer `process` object with `bottom`, `transition`, `transitionIndex`, effective exposure, waits, motion, and PWM. Proprietary writers should prefer this resolved per-layer model where their format supports it, then fall back to global process fields where it does not.
VSLA layer SVG roots additionally include process data attributes such as `data-exposure`, `data-light-off-delay`, `data-light-pwm`, `data-bottom`, and `data-transition` so the per-layer intent is visible without parsing the manifest.
---
## Implementation Guidance for Sandboxed Environments
@ -896,6 +955,7 @@ def decode_ctb_variable_rle(data: bytes, width: int, height: int) -> bytes:
**CTB:**
- `UVtools.Core/FileFormats/ChituboxFile.cs`: `DecodeCtbImage`, `EncodeCtbImage`, `LayerRleCryptBuffer`
- `UVtools.Core/FileFormats/CTBEncryptedFile.cs`: encrypted CTB v4-v5 container and AES handling
- `src/kiri/mode/sla/work/x_ctb_crypto.js`: native JS AES-256-CBC/no-padding, CTB layer XOR, encrypted CTB constants
**Anycubic:**
- `UVtools.Core/FileFormats/AnycubicFile.cs`: `FileMark`, `Header`, `LayerDef`, `GetAvailableVersionsForExtension`, `PrinterModel`

View file

@ -2,6 +2,399 @@
Full docs @ https://docs.grid.space/projects/kiri-moto
# Release 4.7.0
A broad update focused on SLA/MSLA export coverage, DXF import, CAM routing and drilling fixes, and a substantial menu/theme cleanup across Kiri:Moto.
## SLA / MSLA
- add CTB export support with Chitubox machine profiles
- add Goo and PRZ export support for Elegoo and Phrozen targets
- add Photon Workshop family export support for newer Anycubic Photon machines
- add generic raster SLA and vector SLA output formats
- add volume rendering and make it the default SLA rendering path
- improve SLA support generation and support geometry handling
- add SLA raster, metadata, and process timing helpers for exposure, lift, retract, waits, PWM, and transition layers
- add tilt-motion support for Goo/Elegoo targets
- add memory growth and fallback handling for SLA wasm slicing
- add and enable new Anycubic Photon, Creality, Elegoo, and Phrozen SLA printer profiles
- add CTB conversion utility and MSLA format reference documentation
## CAM
- add basic finish cut capability to Area clear operations
- improve routing between Area operation regions
- add tool move epsilon to reduce false-positive collision detection
- fix contour filtering errors
- fix CPU contouring with more than one isolated body
- fix contour Y handling that could mutate cached widget vertices
- fix tool change dwell time
- force tool lift between drill operations
- fix drill marking with latent thru settings
- fix widget and tab assignment during slicing
- patch tab record format handling
- remove metric/imperial scaling factor from A axis output
## Import / Export
- add DXF import support
- add DXF spline support
- reconnect DXF open lines with shared endpoints
- normalize the DXF import dialog
- add automatic DXF unit detection and scaling, including Z/depth scaling
- proxy print start commands when needed
- add Bambu SSDP model option
## UI / Workflow
- rebuild the Kiri:Moto menubar from JS instead of static HTML
- restructure top menus and move mode tools into the menubar build
- add a shared palette and theme CSS foundation
- reorganize Kiri CSS and normalize menu colors
- make rotate and scale panels draggable
- make CAM setting groups collapsible
- move device/profile controls to the left menu
- move Save As commands into the file menu
- replace transient alerts with a busy spinner
- add middle-button drag support
- add automatic slideshow behavior for narrow windows
- improve popup positioning, z-index layering, and menu hover behavior
- add missing language keys for new menu items
- fix file New/Open state clearing and rebuild behavior
## Mesh:Tool / Void:Form
- update Mesh:Tool visuals and add document history support
- continue Void:Form boundary provenance work
- add Void:Form constraints and chamfer/offset planning docs
- improve Void:Form solids, geometry store, rebuild, and toolbar internals
## Platform / Build
- add localhost HTTP POST proxy support for embedded HTTPS contexts such as Onshape
- add CORS headers and secure-request handling for proxy routes
- fix Docker build steps and update Docker Node version
- fix esbuild mkdir race and node module external handling
- remove Windows build from GitHub workflow
- update Electron hide/show handling and Windows link workaround
# Release 4.6.3
## General
- merge experimental Void:Form app code
- add permissive graphics card flags for Linux Electron builds
## CAM
- fix register operation cutout modes
# Release 4.6.2
## CAM
- add Carvera Air device support
- add spindle spin-up delay for Carvera Air
- reduce max spindle speed for Carvera Air
- add trace offset Z option
- allow drill operations to obey Z bottom
## Features
- add install progress bar
- add OPFS support and API link
- add engine workspace export
- add layer diff support
## UI / Workflow
- resize engine code and preview boxes
- fix Mesh:Tool window close behavior so it returns to Kiri:Moto
## Bug Fixes
- fix leave XY offset for roughing cutout
- fix API URL object loading
- fix engine stock origin and center handling
- fix engine setMode()
- fix floating-point noise in Z vertices with shadow clipping
## Development
- expose mesh worker log function
- add slicePre() and slicePost() engine requirements
- update shared-array-buffer origin trial token
# Release 4.6.1
## CAM
- fix deep ease-down paths that wrap a polygon more than once
- add ease-down safeguards for bad geometry
- cap ease-down speed by engage setting
- correct tool shadow epsilon
- raise Z safe when Z top is above it
- improve outline strategy for depth-first operations
- add trace option to ignore part boundaries
- disable trace merge until Area can handle it properly
## FDM
- async FDM slicing layer diffs for roughly 3x speedup
- trim support shadow to overhangs
- improve support clipping
- change support gap offset from numeric to boolean
- refactor slice progress tracking
- tighten FDM slice shadow range
## Bug Fixes
- fix unit scale when visual Z is set
- fix G-code import origin
- fix negative level Z order
- fix wireframe opacity
- fix missing macro token handling
- filter invalid area polygons
- fix LOCAL const handling
- remove input filter on iOS mobile
## UI / Platform
- invert dolly sign
- remove beta tag
- update Electron app base
- fix docs formatting for controls.md
# Release 4.6.0
A major update focused on FDM support system improvements, belt printer support, CAM loop and selection workflows, and significant code modernization.
## Major Features
- redesign FDM support generation with manual support painting and erase mode
- add angle-aware support painting with automatic overhang filtering
- extract support structures into synthetic widgets for independent extruder assignment
- integrate manual support painting with belt printer coordinate transforms
- fix purge tower allocation for sparse, non-contiguous extruder configurations
## FDM
- add manual support painting visual overlay
- add automatic support generation using shadow projection
- support dedicated extruder selection for support structures
- clip belt-mode support to the angled platform
- fix support slice height propagation
- improve nested top fill offset calculations
- fix arc output center point calculations
- add delta thickness filtering for variable layer heights
- add Moonraker auto-start remote print option
- fix finish speed ratio calculations for flats and bridges
## CAM
- add Loop operation for rotary and indexing workflows
- add SVG trace path loading for arbitrary vector import
- add Area operation vertical-wall clearing option
- improve lathe step-down output for rotary operations
- add alternating/zigzag options for Area linear surfacing
- add shadow selection mode for Area operations
- improve drag knife routing
- add coastline move routes for contour operations
- support manual slice ordering
- improve progress reporting for shadow generation, Area surfacing, and lathe operations
- parallelize widget shadow generation
- add optional clip-to-stock for non-GPU topo3 contouring
## UI / Workflow
- add visual selection of Z top and Z bottom constraints
- show Z top/bottom overrides in operation summaries
- add collapsible operations list
- add slicing and preview statistics in developer mode hover
- persist widget transparency across sessions
- add expandable preview in developer mode
- add W shortcut for ghost/wire toggle
- improve colors, opacity, and edge rendering
- expose file type parsers as api.load
## Architecture
- document the FDM slicing pipeline
- reorganize UI code into app/init and app/ui modules
- extract keyboard, slider, and modal handling into dedicated modules
- consolidate STL/OBJ loading into unified load classes
- split widget into core and app-ui subclasses
- add async initialization for startup
- remove legacy code and unused dependencies
- add JSDoc developer documentation
## Bug Fixes
- normalize mouse wheel scrolling across Chrome, Firefox, and Safari
- fix widget cloning issues
- fix broken API sync link
- fix Z base layer height for parts not on bed
- fix ghost/wire/solid toggle states
- fix Area operation rotation handling
- fix Loop operation expansion and rendering
- fix shadow and lathe progress reporting
- fix trace outside with drape duplication
- fix starting offsets with multiple widgets
- fix import slider max-value click
- fix widget edge updates after mesh moves
## Platform
- add Snapmaker U1 belt printer profile
- remove deprecated Gridbots devices
- update FlashForge Adventurer 5M profile
- update Three.js and Manifold libraries
- add QuickJS evaluation engine for macros
# Release 4.5.1
## Bug Fixes
- fix findConnectedSurface for pocket operations in indexed mode
- fix use of flatness when dropping points in CPU contour processing
- fix erroneous tab matching in CPU contouring
- fix GPU contour Y coordinate handling with inside-only mode
- fix trace-through backward compatibility
- fix order-of-operations issue with tabs and inner-only processing
## Improvements
- increase Bambu FTP timeout for more reliable connectivity
- update FlashForge Adventurer 5M printer profile
- add support for exported formats in packed devices
- reorganize help menu layout
- expose file type parsers as api.load
- distinguish fit contents from resize operations
- improve visibility for hover-only UI elements
- update Three.js and Manifold libraries
# Release 4.5.0
## Major Features
- rewrite CNC path routing engine for improved collision detection
- add unified Area operation combining roughing, outlining, tracing, and surfacing workflows
- add taper ball tool support for CNC operations
- add on-the-fly switching between orthographic and perspective cameras
- preserve and restore camera projection settings across workspace import
- add Shift+F zoom to contents and F zoom to fit shortcuts
## CAM
- add Area clear, trace, outline, and surface modes
- add depth-first pocket routing
- add roughing flat passes option
- improve leave stock handling in XY and Z
- enable inside/outside trace for open polylines
- add trace-through option
- improve dogbone generation
- add wide cutout support for outlines
- add contour grace factor and stock clip-to restoration
- add linear surface output mode
- merge overlapping tabs automatically
- make tabs respect travel boundaries
- ensure ease-down paths avoid tabs
- improve tab cutting for indexed rotations
- speed up tool shadow computation with grid-bucketed recursive union
- improve travel boundary handling and outside travel safeties
- add rotational interpolation for G-code parsing
- improve lathe preview rendering
- add lathe axis reset macro
- improve drilling hole selection and circularity detection
- add milling direction per operation
- tighten arc tolerances for G-code output
## FDM
- add Solidify option to remove inner voids at slice time
- fix support on-demand detection
- improve bridge detection and handling
## Laser
- add stacking STL reconstruction option for multi-layer engraving
## UI / Workflow
- improve CAM render colors
- soften edge traces in light mode
- mute dark part outlines for better visibility
- improve indexed path visualization
- fix colors and layer-first shadow rendering
- hide outline omit-through option when not applicable
- improve settings mutex handling for legacy operation import
- fix safe moves between operations with widget platform offsets
- improve stock clipping for multiple models with offset centers
- fix origin selection with offset origin
- fix first-save behavior when moving between widgets
## Bug Fixes
- fix Z bottom validation and related error messages
- warn when Z bottom is below Z through
- fix trace tool shadow clone
- fix clamp min/max swap
- fix stock clear all functionality
- fix pocket bottom for slab slicing
- clamp single trace to Z top/bottom
- fix Area trace early termination conflict with drape
- filter empty slices from drape artifacts
- fix offsetting open polygons with array offsets
- skip raster when no paths are produced
- fix orthographic aspect ratio and zoom behavior
- fix ball taper rendering in indexed mode
- fix new tool units bug
- prevent double registration of frame listeners
- fix async/await issue in CAM prepare
- convert slice bounds errors to alerts
## Documentation / Build
- add geometry library JSDoc documentation
- update Kiri:Moto UI documentation and interface images
- add docs redirects and fix broken links
- update Docusaurus to 3.9
- improve cross-platform build workflow with cross-env
- update raster-path dependency
- improve Electron build process
- improve Onshape and Bambu Lab integration behavior
# Release 4.4.1
## CAM
- fix contour Y bounds swap
- fix tab cuts and stock clip-to in roughing operations
- fix outline stock clipping with offset origin
- fix origin selection with offset origin
- fix stock clipping for multiple models with offset centers
- fix multi-widget safe moves between operations
- fix thru cuts with new up/over tab paths
## FDM
- fix FDM support on-demand detection
- fix asset append missing module root
## Core
- fix bad worker reference
- add install pass-through for root requests
## Improvements
- improve smoothing and pocket developer-mode visuals
- change Electron build context detection
- move mods package dependencies to the main package
- move Bambu initialization to load-done
- remove beta tag
- backport cross-env dependency
# Release 4.4.0
## General

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),
@ -399,11 +400,12 @@ export const conf = {
slaFillDensity: 0,
slaFillLine: 0.5,
slaFirstOffset: 0,
slaSupportLayers: 10,
slaSupportDensity: 0.5,
slaSupportSize: 0.6,
slaSupportLayers: 0,
slaSupportDensity: 0.1,
slaSupportSize: 0.4,
slaSupportPoints: 4,
slaSupportGap: 10,
slaSupportAngle: 35,
slaSupportGap: 0,
slaSupportEnable: false
}
},

View file

@ -212,6 +212,42 @@ export class Layers {
}
}
/**
* Add closed polygons as filled Z volumes.
* By default, the polygon Z is treated as the volume center. Pass
* { base:true } to treat polygon Z as the bottom face instead.
* @param {Polygon|Array<Polygon>} polys - Closed polygon(s), with inners preserved as voids
* @param {object} [options] - Options: {height, z, base, outline}
* @returns {Layers} This instance for chaining
*/
addZVolume(polys, options) {
const opts = options || {};
const height = opts.height || 1;
const faces = this.current.faces;
polys = Array.isArray(polys) ? polys.clone(true) : [ polys.clone(true) ];
for (let poly of polys) {
if (poly.open || poly.length < 3) {
continue;
}
let z = opts.z !== undefined ? opts.z : poly.getZ(),
zoff = opts.base ? z : z - height / 2,
volume = poly.setZ(0).extrude(height, { zadd: zoff });
faces.appendAll(volume);
if (opts.outline) {
this.addPolys([
poly.clone(true).setZ(zoff),
poly.clone(true).setZ(zoff + height)
]);
}
}
return this;
}
/**
* Add 2D polyline paths with width (usually FDM extrusion paths).
* Creates flat ribbons with vertex normals for lighting.

View file

@ -23,7 +23,7 @@
],
"gcodeSpindle": [
"M3 S{spindle}",
"G4 P4000"
"G4 P4"
],
"gcodeChange": [
"M6 T{tool} ; change tool to '{tool_name}'"

View file

@ -7,6 +7,8 @@
"bed_height": 1,
"max_height": 300,
"resolution_x": 3840,
"resolution_y": 2160
"resolution_y": 2160,
"sla_format": "cxdlp",
"sla_file_ext": "cxdlp"
}
}

View file

@ -7,6 +7,8 @@
"bed_height": 1,
"max_height": 160,
"resolution_x": 1620,
"resolution_y": 2560
"resolution_y": 2560,
"sla_format": "cxdlp",
"sla_file_ext": "cxdlp"
}
}

View file

@ -7,6 +7,8 @@
"bed_height": 1,
"max_height": 200,
"resolution_x": 3840,
"resolution_y": 2400
"resolution_y": 2400,
"sla_format": "cxdlp",
"sla_file_ext": "cxdlp"
}
}

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

@ -906,20 +906,22 @@ export function init() {
clearPops();
recreateTabs();
for (let group of Widget.Groups.list()) {
let root = group[0];
if (root.tabs)
for (let tab of Object.values(root.tabs)) {
let geo = tab.box.geometry.clone();
if (geo.index) geo = geo.toNonIndexed();
geo.translate(tab.x, tab.y, tab.z);
let bbg = BufferGeometryUtils.mergeGeometries([ geo ]);
let sw = newWidget(null, group);
let fwp = group[0].track.pos;
sw.loadGeometry(bbg);
sw._move(fwp.x, fwp.y, fwp.z);
api.widgets.add(sw);
sw.track.synth = true;
sw.track.indexed = root.track.indexed;
for (let widget of group) {
if (widget.tabs)
for (let tab of Object.values(widget.tabs)) {
let geo = tab.box.geometry.clone();
if (geo.index) geo = geo.toNonIndexed();
geo.translate(tab.x, tab.y, tab.z);
let bbg = BufferGeometryUtils.mergeGeometries([ geo ]);
let sw = newWidget(null, group);
let fwp = widget.track.pos;
sw.loadGeometry(bbg);
sw._move(fwp.x, fwp.y, fwp.z);
api.widgets.add(sw);
sw.track.synth = true;
sw.track.indexed = widget.track.indexed;
sw.track.tabOwner = widget.id;
}
}
}
});

View file

@ -35,8 +35,8 @@ export function cam_slice_pre(settings) {
export async function cam_slice(settings, widget, onupdate, ondone) {
if (widget.track.synth) return ondone();
// get tab widgets
let tabW = widget.group.filter(w => w != widget);
// get tab widgets for this specific widget
let tabW = widget.group.filter(w => w != widget && w.track.tabOwner === widget.id);
// merge overlapping tabs
for (let i=0; i<tabW.length; i++) {

View file

@ -43,11 +43,12 @@ export function menu() {
slaFillDensity: newInput(LANG.sa_ifdn_s, {title:LANG.sa_ifdn_l, convert:toFloat, bound:bound(0,1)}),
slaFillLine: newInput(LANG.sa_iflw_s, {title:LANG.sa_iflw_l, convert:toFloat, bound:bound(0,5)}),
slaSupport: newGroup(LANG.sa_supp_m, $('sla-support'), { modes:SLA, group:"sla-support", driven, separator }),
slaSupportLayers: newInput(LANG.sa_slyr_s, {title:LANG.sa_slyr_l, convert:toInt, bound:bound(5,100)}),
slaSupportGap: newInput(LANG.sa_slgp_s, {title:LANG.sa_slgp_l, convert:toInt, bound:bound(3,30)}),
slaSupportLayers: newInput(LANG.sa_slyr_s, {title:LANG.sa_slyr_l, convert:toInt, bound:bound(0,100)}),
slaSupportGap: newInput(LANG.sa_slgp_s, {title:LANG.sa_slgp_l, convert:toInt, bound:bound(0,30)}),
slaSupportDensity: newInput(LANG.sa_sldn_s, {title:LANG.sa_sldn_l, convert:toFloat, bound:bound(0.01,0.9)}),
slaSupportSize: newInput(LANG.sa_slsz_s, {title:LANG.sa_slsz_l, convert:toFloat, bound:bound(0.1,1)}),
slaSupportPoints: newInput(LANG.sa_slpt_s, {title:LANG.sa_slpt_l, convert:toInt, bound:bound(3,10)}),
slaSupportAngle: newInput(LANG.sa_slan_s, {title:LANG.sa_slan_l, convert:toFloat, bound:bound(5,45)}),
slaSupportEnable: newBoolean(LANG.enable, onBooleanClick, {title:LANG.sl_slen_l}),
slaOutput: newGroup(LANG.sa_outp_m, $('sla-output'), { modes:SLA, driven, separator, group:"sla-output" }),
slaFirstOffset: newInput(LANG.sa_opzo_s, {title:LANG.sa_opzo_l, convert:toFloat, bound:bound(0,1)}),

View file

@ -46,6 +46,14 @@ export const SLA_FORMATS = {
priority: 1,
family: 'chitubox'
},
ctbe: {
id: 'ctbe',
ext: 'encrypted.ctb',
proprietary: true,
available: false,
priority: 1,
family: 'chitubox-encrypted'
},
goo: {
id: 'goo',
ext: 'goo',

View file

@ -0,0 +1,128 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
const DEFAULT_PWM = 255;
const TILT_MOTION = {
liftHeight: 0.05,
liftSpeed: 0.05,
retractHeight: 0,
retractSpeed: 0.05,
motorTime: 4
};
export function createProcessMetadata(device, process) {
let transitionLayers = intValue(process.slaTransitionLayers);
let lightPWM = pwmValue(process.slaLightPWM);
let bottomLightPWM = pwmValue(process.slaBaseLightPWM, lightPWM);
let normalMotion = motionFor(device, process, false);
let bottomMotion = motionFor(device, process, true);
return {
bottomLayers: intValue(process.slaBaseLayers),
transitionLayers,
exposure: number(process.slaLayerOn),
bottomExposure: number(process.slaBaseOn),
lightOffDelay: number(process.slaLayerOff),
bottomLightOffDelay: number(process.slaBaseOff),
waitBeforeCure: number(process.slaWaitBeforeCure ?? process.slaLayerOff),
bottomWaitBeforeCure: number(process.slaBaseWaitBeforeCure ?? process.slaBaseOff),
waitAfterCure: number(process.slaWaitAfterCure),
bottomWaitAfterCure: number(process.slaBaseWaitAfterCure),
waitAfterLift: number(process.slaWaitAfterLift),
bottomWaitAfterLift: number(process.slaBaseWaitAfterLift),
liftHeight: normalMotion.liftHeight,
bottomLiftHeight: bottomMotion.liftHeight,
liftSpeed: normalMotion.liftSpeed,
bottomLiftSpeed: bottomMotion.liftSpeed,
retractHeight: normalMotion.retractHeight,
bottomRetractHeight: bottomMotion.retractHeight,
retractSpeed: normalMotion.retractSpeed,
bottomRetractSpeed: bottomMotion.retractSpeed,
lightPWM,
bottomLightPWM,
motion: device.slaMotion || "normal",
firstLayerOffset: number(process.slaFirstOffset),
antiAlias: intValue(process.slaAntiAlias, 1)
};
}
export function createLayerMetadata(device, process, index, z, area) {
let meta = createProcessMetadata(device, process);
let bottom = index < meta.bottomLayers;
let transitionIndex = bottom ? -1 : index - meta.bottomLayers;
let transition = transitionIndex >= 0 && transitionIndex < meta.transitionLayers;
let motion = motionFor(device, process, bottom);
return {
index,
z: round(z),
area: round(area),
bottom,
transition,
transitionIndex: transition ? transitionIndex : -1,
exposure: round(layerExposure(meta, bottom, transitionIndex)),
lightOffDelay: bottom ? meta.bottomLightOffDelay : meta.lightOffDelay,
waitBeforeCure: bottom ? meta.bottomWaitBeforeCure : meta.waitBeforeCure,
waitAfterCure: bottom ? meta.bottomWaitAfterCure : meta.waitAfterCure,
waitAfterLift: bottom ? meta.bottomWaitAfterLift : meta.waitAfterLift,
liftHeight: motion.liftHeight,
liftSpeed: motion.liftSpeed,
retractHeight: motion.retractHeight,
retractSpeed: motion.retractSpeed,
lightPWM: bottom ? meta.bottomLightPWM : meta.lightPWM
};
}
export function motionFor(device, process, bottom) {
if (device.slaMotion === "tilt") {
return TILT_MOTION;
}
let normalDrop = process.slaPeelDropRate;
let bottomDrop = process.slaBasePeelDropRate ?? normalDrop;
return {
liftHeight: number(bottom ? process.slaBasePeelDist : process.slaPeelDist),
liftSpeed: number(bottom ? process.slaBasePeelLiftRate : process.slaPeelLiftRate) * 60,
retractHeight: number(bottom ? process.slaBasePeelDist : process.slaPeelDist),
retractSpeed: number(bottom ? bottomDrop : normalDrop) * 60,
motorTime: 0
};
}
export function estimatePrintTime(device, process, layers) {
let meta = createProcessMetadata(device, process);
let seconds = 0;
for (let index=0; index<layers; index++) {
let layer = createLayerMetadata(device, process, index, 0, 0);
let motor = device.slaMotion === "tilt" ? TILT_MOTION.motorTime : 0;
seconds += layer.exposure + layer.waitBeforeCure + layer.waitAfterCure + layer.waitAfterLift + motor;
}
return Math.ceil(seconds);
}
export function round(value) {
return Number.parseFloat(Number(value || 0).toFixed(5));
}
function layerExposure(meta, bottom, transitionIndex) {
if (bottom || meta.transitionLayers <= 0 || transitionIndex < 0 || transitionIndex >= meta.transitionLayers) {
return bottom ? meta.bottomExposure : meta.exposure;
}
let step = (transitionIndex + 1) / (meta.transitionLayers + 1);
return meta.bottomExposure + (meta.exposure - meta.bottomExposure) * step;
}
function pwmValue(value, fallback = DEFAULT_PWM) {
value = intValue(value, fallback);
return Math.max(0, Math.min(255, value));
}
function intValue(value, fallback = 0) {
value = Number.parseInt(value);
return Number.isFinite(value) ? value : fallback;
}
function number(value) {
value = Number(value);
return Number.isFinite(value) ? value : 0;
}

File diff suppressed because it is too large Load diff

View file

@ -3,6 +3,7 @@
import { JSZip } from '../../../../ext/jszip-esm.js';
import { PNG } from '../../../../ext/pngjs.esm.js';
import { imageToRowMajor, renderRasterLayers, round } from './raster.js';
import { ctbLayerCrypt } from './x_ctb_crypto.js';
const CTB_V3_MAGIC = 0x12fd0086;
const HEADER_SIZE = 0x70;
@ -715,37 +716,11 @@ function requireBytes(input, offset, length) {
}
function decipher(seed, layerIndex, input) {
if (!seed) return input.slice();
let output = new Uint8Array(input.length);
let keyring = newKeyring(seed, layerIndex);
for (let i=0; i<input.length; i++) {
output[i] = input[i] ^ keyring.next();
}
return output;
return ctbLayerCrypt(seed, layerIndex, input);
}
function encipher(seed, layerIndex, input) {
return decipher(seed, layerIndex, input);
}
function newKeyring(seed, layerIndex) {
let init = (Math.imul(seed >>> 0, 0x2d83cdac) + 0xd8a83423) >>> 0;
let keySeed = (Math.imul(layerIndex >>> 0, 0x1e1530cd) + 0xec3d47cd) >>> 0;
let key = Math.imul(keySeed, init) >>> 0;
let index = 0;
return {
next() {
let byte = (key >>> (8 * index)) & 0xff;
index++;
if ((index & 3) === 0) {
key = (key + init) >>> 0;
index = 0;
}
return byte;
}
};
return ctbLayerCrypt(seed, layerIndex, input);
}
function imageToPNG(image, width, height) {

View file

@ -0,0 +1,321 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
const UVTOOLS_KEY = "UVtools";
const SECRET_KEY = "hQ36XB6yTk+zO02ysyiowt8yC1buK+nbLWyfY40EXoU=";
const SECRET_IV = "Wld+ampndVJecmVjYH5cWQ==";
const BLOCK_SIZE = 16;
const SBOX = new Uint8Array(256);
const INV_SBOX = new Uint8Array(256);
const RCON = new Uint8Array(15);
initTables();
export const CTB_ENCRYPTED_MAGIC = 0x12fd0107;
export const CTB_ENCRYPTED_VERSION = 5;
export const CTB_LAYER_XOR_KEY = 0xefbeadde;
export const CTB_ENCRYPTED_HEADER_SIZE = 48;
export const CTB_ENCRYPTED_SETTINGS_SIZE = 288;
export const CTB_ENCRYPTED_LAYER_POINTER_SIZE = 16;
export const CTB_ENCRYPTED_LAYER_DEF_SIZE = 88;
export const CTB_ENCRYPTED_MIN_RLE_AES_LENGTH = 512;
export const CTB_PER_LAYER_SETTINGS_ALLOW = 0x40;
export const CTB_PER_LAYER_SETTINGS_DISALLOW = 0x00;
export const CTB_ENCRYPTED_KEY = xorCipher(base64Bytes(SECRET_KEY), UVTOOLS_KEY);
export const CTB_ENCRYPTED_IV = xorCipher(base64Bytes(SECRET_IV), UVTOOLS_KEY);
export function ctbLayerCrypt(seed, layerIndex, input) {
let output = new Uint8Array(input);
ctbLayerCryptInPlace(seed, layerIndex, output);
return output;
}
export function ctbLayerCryptInPlace(seed, layerIndex, input) {
if (!seed) return input;
let init = (Math.imul(seed >>> 0, 0x2d83cdac) + 0xd8a83423) >>> 0;
let keySeed = (Math.imul(layerIndex >>> 0, 0x1e1530cd) + 0xec3d47cd) >>> 0;
let key = Math.imul(keySeed, init) >>> 0;
let index = 0;
for (let i=0; i<input.length; i++) {
input[i] ^= (key >>> (8 * index)) & 0xff;
index++;
if ((index & 3) === 0) {
key = (key + init) >>> 0;
index = 0;
}
}
return input;
}
export function ctbEncrypt(data) {
return aesCbcCrypt(data, true, CTB_ENCRYPTED_KEY, CTB_ENCRYPTED_IV);
}
export function ctbDecrypt(data) {
return aesCbcCrypt(data, false, CTB_ENCRYPTED_KEY, CTB_ENCRYPTED_IV);
}
export async function ctbSignature(checksumValue = 0xcafebabe) {
let bytes = new Uint8Array(8);
let view = new DataView(bytes.buffer);
view.setUint32(0, checksumValue >>> 0, true);
view.setUint32(4, Math.floor(checksumValue / 0x100000000), true);
return ctbEncrypt(await sha256(bytes));
}
function aesCbcCrypt(data, encrypt, key, iv) {
if (key.length !== 32) {
throw new Error(`CTB AES key must be 32 bytes (${key.length})`);
}
if (iv.length !== BLOCK_SIZE) {
throw new Error(`CTB AES IV must be 16 bytes (${iv.length})`);
}
let keys = expandKey(key);
let input = encrypt ? zeroPad(data) : toBytes(data);
if (input.length % BLOCK_SIZE !== 0) {
throw new Error(`CTB AES input must align to 16 bytes (${input.length})`);
}
let output = new Uint8Array(input.length);
let previous = new Uint8Array(iv);
for (let offset=0; offset<input.length; offset += BLOCK_SIZE) {
let block = input.slice(offset, offset + BLOCK_SIZE);
if (encrypt) {
xorBlock(block, previous);
block = encryptBlock(block, keys);
previous = block;
output.set(block, offset);
continue;
}
let current = block;
block = decryptBlock(block, keys);
xorBlock(block, previous);
previous = current;
output.set(block, offset);
}
return output;
}
function encryptBlock(block, keys) {
let state = new Uint8Array(block);
let rounds = keys.rounds;
addRoundKey(state, keys.bytes, 0);
for (let round=1; round<rounds; round++) {
subBytes(state);
shiftRows(state);
mixColumns(state);
addRoundKey(state, keys.bytes, round);
}
subBytes(state);
shiftRows(state);
addRoundKey(state, keys.bytes, rounds);
return state;
}
function decryptBlock(block, keys) {
let state = new Uint8Array(block);
let rounds = keys.rounds;
addRoundKey(state, keys.bytes, rounds);
for (let round=rounds - 1; round>0; round--) {
invShiftRows(state);
invSubBytes(state);
addRoundKey(state, keys.bytes, round);
invMixColumns(state);
}
invShiftRows(state);
invSubBytes(state);
addRoundKey(state, keys.bytes, 0);
return state;
}
function expandKey(key) {
let nk = 8;
let nb = 4;
let rounds = 14;
let bytes = new Uint8Array(nb * (rounds + 1) * 4);
bytes.set(key);
let temp = new Uint8Array(4);
let words = nb * (rounds + 1);
for (let word=nk; word<words; word++) {
let prev = (word - 1) * 4;
temp[0] = bytes[prev];
temp[1] = bytes[prev + 1];
temp[2] = bytes[prev + 2];
temp[3] = bytes[prev + 3];
if (word % nk === 0) {
let first = temp[0];
temp[0] = SBOX[temp[1]] ^ RCON[word / nk];
temp[1] = SBOX[temp[2]];
temp[2] = SBOX[temp[3]];
temp[3] = SBOX[first];
} else if (word % nk === 4) {
temp[0] = SBOX[temp[0]];
temp[1] = SBOX[temp[1]];
temp[2] = SBOX[temp[2]];
temp[3] = SBOX[temp[3]];
}
let prior = (word - nk) * 4;
let out = word * 4;
bytes[out] = bytes[prior] ^ temp[0];
bytes[out + 1] = bytes[prior + 1] ^ temp[1];
bytes[out + 2] = bytes[prior + 2] ^ temp[2];
bytes[out + 3] = bytes[prior + 3] ^ temp[3];
}
return { bytes, rounds };
}
function addRoundKey(state, keys, round) {
let offset = round * BLOCK_SIZE;
for (let i=0; i<BLOCK_SIZE; i++) {
state[i] ^= keys[offset + i];
}
}
function subBytes(state) {
for (let i=0; i<BLOCK_SIZE; i++) state[i] = SBOX[state[i]];
}
function invSubBytes(state) {
for (let i=0; i<BLOCK_SIZE; i++) state[i] = INV_SBOX[state[i]];
}
function shiftRows(state) {
let t = state.slice();
state[1] = t[5]; state[5] = t[9]; state[9] = t[13]; state[13] = t[1];
state[2] = t[10]; state[6] = t[14]; state[10] = t[2]; state[14] = t[6];
state[3] = t[15]; state[7] = t[3]; state[11] = t[7]; state[15] = t[11];
}
function invShiftRows(state) {
let t = state.slice();
state[1] = t[13]; state[5] = t[1]; state[9] = t[5]; state[13] = t[9];
state[2] = t[10]; state[6] = t[14]; state[10] = t[2]; state[14] = t[6];
state[3] = t[7]; state[7] = t[11]; state[11] = t[15]; state[15] = t[3];
}
function mixColumns(state) {
for (let c=0; c<BLOCK_SIZE; c += 4) {
let a0 = state[c];
let a1 = state[c + 1];
let a2 = state[c + 2];
let a3 = state[c + 3];
let t = a0 ^ a1 ^ a2 ^ a3;
let u = a0;
state[c] ^= t ^ xtime(a0 ^ a1);
state[c + 1] ^= t ^ xtime(a1 ^ a2);
state[c + 2] ^= t ^ xtime(a2 ^ a3);
state[c + 3] ^= t ^ xtime(a3 ^ u);
}
}
function invMixColumns(state) {
for (let c=0; c<BLOCK_SIZE; c += 4) {
let a0 = state[c];
let a1 = state[c + 1];
let a2 = state[c + 2];
let a3 = state[c + 3];
state[c] = mul(a0, 14) ^ mul(a1, 11) ^ mul(a2, 13) ^ mul(a3, 9);
state[c + 1] = mul(a0, 9) ^ mul(a1, 14) ^ mul(a2, 11) ^ mul(a3, 13);
state[c + 2] = mul(a0, 13) ^ mul(a1, 9) ^ mul(a2, 14) ^ mul(a3, 11);
state[c + 3] = mul(a0, 11) ^ mul(a1, 13) ^ mul(a2, 9) ^ mul(a3, 14);
}
}
function initTables() {
RCON[1] = 1;
for (let i=2; i<RCON.length; i++) RCON[i] = xtime(RCON[i - 1]);
for (let i=0; i<256; i++) {
let inv = i ? gfPow(i, 254) : 0;
let s = inv ^ rot(inv, 1) ^ rot(inv, 2) ^ rot(inv, 3) ^ rot(inv, 4) ^ 0x63;
SBOX[i] = s & 0xff;
INV_SBOX[SBOX[i]] = i;
}
}
function xtime(value) {
return ((value << 1) ^ ((value & 0x80) ? 0x1b : 0)) & 0xff;
}
function mul(a, b) {
let out = 0;
while (b) {
if (b & 1) out ^= a;
a = xtime(a);
b >>= 1;
}
return out;
}
function gfPow(value, power) {
let out = 1;
while (power) {
if (power & 1) out = mul(out, value);
value = mul(value, value);
power >>= 1;
}
return out;
}
function rot(value, shift) {
return ((value << shift) | (value >> (8 - shift))) & 0xff;
}
function zeroPad(data) {
data = toBytes(data);
let length = Math.ceil(data.length / BLOCK_SIZE) * BLOCK_SIZE;
if (length === data.length) return data;
let output = new Uint8Array(length);
output.set(data);
return output;
}
function xorBlock(block, key) {
for (let i=0; i<BLOCK_SIZE; i++) block[i] ^= key[i];
}
function xorCipher(bytes, key) {
let output = new Uint8Array(bytes.length);
for (let i=0; i<bytes.length; i++) {
output[i] = bytes[i] ^ key.charCodeAt(i % key.length);
}
return output;
}
function base64Bytes(text) {
let binary = atob(text);
let bytes = new Uint8Array(binary.length);
for (let i=0; i<binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
function toBytes(data) {
return data instanceof Uint8Array ? data : new Uint8Array(data);
}
async function sha256(bytes) {
let subtle = globalThis.crypto && globalThis.crypto.subtle;
if (!subtle) {
throw new Error("CTB encrypted signature requires crypto.subtle SHA-256");
}
return new Uint8Array(await subtle.digest("SHA-256", bytes));
}

View file

@ -1,6 +1,7 @@
/** Copyright Stewart Allen <sa@grid.space> -- 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";
@ -39,9 +40,10 @@ function encodeGOO(print, progress, photon, rec) {
let previewLarge = createPreview(290, 290);
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 { 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);
@ -49,22 +51,22 @@ function encodeGOO(print, progress, photon, rec) {
layers.push({
index,
data: encodeRLE(raster),
z: round(z),
z: layerMeta.z,
pausePositionZ: ctx.device.maxHeight || ctx.device.bedHeight || 0,
exposure: bottom ? process.slaBaseOn : process.slaLayerOn,
lightOffDelay: 0,
waitTimeAfterCure: 0,
waitTimeAfterLift: 0,
waitTimeBeforeCure: bottom ? process.slaBaseOff : process.slaLayerOff,
liftHeight: bottom ? process.slaBasePeelDist : process.slaPeelDist,
liftSpeed: (bottom ? process.slaBasePeelLiftRate : process.slaPeelLiftRate) * 60,
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: bottom ? process.slaBasePeelDist : process.slaPeelDist,
retractSpeed: (bottom ? process.slaBasePeelDropRate : process.slaPeelDropRate) * 60,
retractHeight: layerMeta.retractHeight,
retractSpeed: layerMeta.retractSpeed,
retractHeight2: 0,
retractSpeed2: 0,
lightPWM: 255
lightPWM: layerMeta.lightPWM
});
}, progress ? (value) => progress(value * 0.85, `${rec.ext}_encode`) : null);
@ -99,8 +101,9 @@ function writeGOO(params) {
function writeHeader(writer, params) {
let { ctx, layers, volume, previewSmall, previewLarge, rec } = params;
let { device, process, width, height } = ctx;
let bottomOff = process.slaBaseOff || 0;
let layerOff = process.slaLayerOff || 0;
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);
@ -126,30 +129,30 @@ function writeHeader(writer, params) {
writer.writeF32(device.bedDepth);
writer.writeF32(device.maxHeight || device.bedHeight || 0);
writer.writeF32(process.slaSlice);
writer.writeF32(process.slaLayerOn);
writer.writeF32(meta.exposure);
writer.writeU8(1);
writer.writeF32(0);
writer.writeF32(0);
writer.writeF32(0);
writer.writeF32(bottomOff);
writer.writeF32(meta.bottomWaitBeforeCure);
writer.writeF32(0);
writer.writeF32(0);
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(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(255);
writer.writeU16(255);
writer.writeU16(meta.bottomLightPWM);
writer.writeU16(meta.lightPWM);
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);
@ -307,6 +310,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 +319,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 +372,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,19 +485,6 @@ function accumulatePreview(preview, image, width, height) {
}
}
function estimatePrintTime(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);
return Math.ceil(bottom * bottomTime + normal * normalTime);
}
function number(value) {
value = Number(value);
return Number.isFinite(value) ? value : 0;
}
function checksum(bytes) {
let sum = 0;
for (let i=1; i<bytes.length; i++) {

View file

@ -4,6 +4,7 @@ import { JSZip } from '../../../../ext/jszip-esm.js';
import { PNG } from '../../../../ext/pngjs.esm.js';
import { photon } from './x_photon.js';
import { imageToRowMajor, renderRasterLayers, round } from './raster.js';
import { createLayerMetadata, createProcessMetadata } from './meta.js';
const FORMAT = "rsla";
const MIME = "application/vnd.gridspace.rsla+zip";
@ -14,14 +15,17 @@ function encode(print, progress) {
let { ctx, volume } = renderRasterLayers(print, photon, params => {
let { index, rendered, ctx } = params;
let { process, width, height } = ctx;
let { device, process, width, height } = ctx;
let z = process.slaFirstOffset + process.slaSlice * index;
let layerMeta = createLayerMetadata(device, process, index, z, rendered.area);
let file = `layers/${index.toString().padStart(6, "0")}.png`;
let layer = {
index,
z: round(process.slaFirstOffset + process.slaSlice * index),
area: round(rendered.area),
file
z: layerMeta.z,
area: layerMeta.area,
file,
process: layerMeta
};
layers.push(layer);
@ -79,18 +83,7 @@ function createManifest(params) {
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
...createProcessMetadata(device, process)
},
layers
};

View file

@ -1,6 +1,7 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { JSZip } from '../../../../ext/jszip-esm.js';
import { createLayerMetadata, createProcessMetadata, round } from './meta.js';
const FORMAT = "vsla";
const MIME = "application/vnd.gridspace.vsla+zip";
@ -18,7 +19,7 @@ function encode(print, progress) {
let layers = [];
let volume = 0;
for (let index=0; index<layermax; index++) {
let layer = collectLayer({ index, widgets, process });
let layer = collectLayer({ index, widgets, device, process });
volume += layer.area * process.slaSlice;
layers.push(layer);
if (progress) progress((index / layermax) * 0.7, "vector_gen");
@ -49,7 +50,7 @@ function encode(print, progress) {
}
function collectLayer(params) {
let { index, widgets, process } = params;
let { index, widgets, device, process } = params;
let polygons = [];
let area = 0;
let z = process.slaFirstOffset + process.slaSlice * index;
@ -74,11 +75,14 @@ function collectLayer(params) {
}
});
let layerMeta = createLayerMetadata(device, process, index, z, area);
return {
index,
z: round(z),
area: round(area),
z: layerMeta.z,
area: layerMeta.area,
file: `layers/${index.toString().padStart(6, "0")}.svg`,
process: layerMeta,
polygons
};
}
@ -91,7 +95,8 @@ function createManifest(params) {
z: layer.z,
area: layer.area,
file: layer.file,
empty: layer.polygons.length === 0
empty: layer.polygons.length === 0,
process: layer.process
};
});
@ -125,18 +130,7 @@ function createManifest(params) {
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
...createProcessMetadata(device, process)
},
layers: layerList
};
@ -150,9 +144,10 @@ function createLayerSVG(params) {
return ` <path fill="#000" fill-rule="evenodd" d="${polyToPath(poly, device)}"/>`;
}).join("\n");
let proc = layer.process;
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}">`,
`<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}" data-exposure="${proc.exposure}" data-light-off-delay="${proc.lightOffDelay}" data-light-pwm="${proc.lightPWM}" data-bottom="${proc.bottom}" data-transition="${proc.transition}">`,
body,
`</svg>`,
``
@ -184,10 +179,6 @@ function appendPath(parts, poly, device) {
parts.push("Z");
}
function round(value) {
return Number.parseFloat(Number(value || 0).toFixed(5));
}
export const VSLA = {
encode
};

View file

@ -1007,8 +1007,8 @@ self.lang['en-us'] = {
sa_iflw_l: ["hatch line width","in millimeters"],
sa_supp_m: "support",
sa_slyr_s: "base layers",
sa_slyr_l: ["base support layers","value range 0-10"],
sa_slyr_s: "raft layers",
sa_slyr_l: ["support raft layers","0 disables raft","value range 0-100"],
sa_slgp_s: "gap layers",
sa_slgp_l: ["number of layers between","raft and bottom of object"],
sa_sldn_s: "density",
@ -1017,6 +1017,8 @@ self.lang['en-us'] = {
sa_slsz_l: ["max size of a","support pillar","in millimeters"],
sa_slpt_s: "points",
sa_slpt_l: ["number of points in","each support pillar","in millimeters"],
sa_slan_s: "angle",
sa_slan_l: ["max branch angle","from vertical","5-45 degrees"],
sl_slen_l: "enable supports",
sa_outp_m: "output",