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

View file

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

View file

@ -32,7 +32,7 @@
| **Other Binary Formats** | | **Other Binary Formats** |
| AnetFile | ✓ | ✓ | n4, n7 | 3 | Yes (N4/N7) | RLE | Anet printers | | AnetFile | ✓ | ✓ | n4, n7 | 3 | Yes (N4/N7) | RLE | Anet printers |
| FDGFile | ✓ | ✓ | fdg | 2 | No | RLE | Voxelab | | 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 | | LGSFile | ✓ | ✓ | lgs, lgs30, lgs120, lgs4k | - | Yes | Struct | Longer Orange |
| MDLPFile | ✓ | ✓ | mdlp | 1 | No | Vector | Makerbase MKS | | MDLPFile | ✓ | ✓ | mdlp | 1 | No | Vector | Makerbase MKS |
| GR1File | ✓ | ✓ | gr1 | - | No | Vector | GR Workshop | | 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 - PageNumber field in layer table
- Transition layers - 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. Anycubic Family
#### 2.1 Legacy Anycubic Formats (v1) #### 2.1 Legacy Anycubic Formats (v1)
@ -559,6 +576,8 @@ Non-delta maximum repeat: 0x0FFFFFFF pixels.
Layer record delimiter after payload: 0x0D 0x0A. 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) ### CXDLP (Creality)
**Extension:** `.cxdlp` **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 ## Implementation Guidance for Sandboxed Environments
@ -896,6 +955,7 @@ def decode_ctb_variable_rle(data: bytes, width: int, height: int) -> bytes:
**CTB:** **CTB:**
- `UVtools.Core/FileFormats/ChituboxFile.cs`: `DecodeCtbImage`, `EncodeCtbImage`, `LayerRleCryptBuffer` - `UVtools.Core/FileFormats/ChituboxFile.cs`: `DecodeCtbImage`, `EncodeCtbImage`, `LayerRleCryptBuffer`
- `UVtools.Core/FileFormats/CTBEncryptedFile.cs`: encrypted CTB v4-v5 container and AES handling - `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:** **Anycubic:**
- `UVtools.Core/FileFormats/AnycubicFile.cs`: `FileMark`, `Header`, `LayerDef`, `GetAvailableVersionsForExtension`, `PrinterModel` - `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 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 # Release 4.4.0
## General ## General

View file

@ -104,6 +104,7 @@ function device_from_code(code,mode) {
slaFormatVersion: valueOf(set.sla_format_version, 1), slaFormatVersion: valueOf(set.sla_format_version, 1),
slaMachineEditable: valueOf(set.sla_machine_editable, false), slaMachineEditable: valueOf(set.sla_machine_editable, false),
slaMachineName: valueOf(set.sla_machine_name, ''), slaMachineName: valueOf(set.sla_machine_name, ''),
slaMotion: valueOf(set.sla_motion, ''),
deviceZMax: valueOf(set.z_move_max, 0), deviceZMax: valueOf(set.z_move_max, 0),
gcodeTime: valueOf(set.time_factor, 1), gcodeTime: valueOf(set.time_factor, 1),
maxHeight: valueOf(set.build_height || set.max_height, 150), maxHeight: valueOf(set.build_height || set.max_height, 150),
@ -399,11 +400,12 @@ export const conf = {
slaFillDensity: 0, slaFillDensity: 0,
slaFillLine: 0.5, slaFillLine: 0.5,
slaFirstOffset: 0, slaFirstOffset: 0,
slaSupportLayers: 10, slaSupportLayers: 0,
slaSupportDensity: 0.5, slaSupportDensity: 0.1,
slaSupportSize: 0.6, slaSupportSize: 0.4,
slaSupportPoints: 4, slaSupportPoints: 4,
slaSupportGap: 10, slaSupportAngle: 35,
slaSupportGap: 0,
slaSupportEnable: false 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). * Add 2D polyline paths with width (usually FDM extrusion paths).
* Creates flat ribbons with vertex normals for lighting. * Creates flat ribbons with vertex normals for lighting.

View file

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

View file

@ -7,6 +7,8 @@
"bed_height": 1, "bed_height": 1,
"max_height": 300, "max_height": 300,
"resolution_x": 3840, "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, "bed_height": 1,
"max_height": 160, "max_height": 160,
"resolution_x": 1620, "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, "bed_height": 1,
"max_height": 200, "max_height": 200,
"resolution_x": 3840, "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(); clearPops();
recreateTabs(); recreateTabs();
for (let group of Widget.Groups.list()) { for (let group of Widget.Groups.list()) {
let root = group[0]; for (let widget of group) {
if (root.tabs) if (widget.tabs)
for (let tab of Object.values(root.tabs)) { for (let tab of Object.values(widget.tabs)) {
let geo = tab.box.geometry.clone(); let geo = tab.box.geometry.clone();
if (geo.index) geo = geo.toNonIndexed(); if (geo.index) geo = geo.toNonIndexed();
geo.translate(tab.x, tab.y, tab.z); geo.translate(tab.x, tab.y, tab.z);
let bbg = BufferGeometryUtils.mergeGeometries([ geo ]); let bbg = BufferGeometryUtils.mergeGeometries([ geo ]);
let sw = newWidget(null, group); let sw = newWidget(null, group);
let fwp = group[0].track.pos; let fwp = widget.track.pos;
sw.loadGeometry(bbg); sw.loadGeometry(bbg);
sw._move(fwp.x, fwp.y, fwp.z); sw._move(fwp.x, fwp.y, fwp.z);
api.widgets.add(sw); api.widgets.add(sw);
sw.track.synth = true; sw.track.synth = true;
sw.track.indexed = root.track.indexed; 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) { export async function cam_slice(settings, widget, onupdate, ondone) {
if (widget.track.synth) return ondone(); if (widget.track.synth) return ondone();
// get tab widgets // get tab widgets for this specific widget
let tabW = widget.group.filter(w => w != widget); let tabW = widget.group.filter(w => w != widget && w.track.tabOwner === widget.id);
// merge overlapping tabs // merge overlapping tabs
for (let i=0; i<tabW.length; i++) { 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)}), 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)}), 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 }), 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)}), 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(3,30)}), 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)}), 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)}), 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)}), 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}), 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" }), 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)}), 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, priority: 1,
family: 'chitubox' family: 'chitubox'
}, },
ctbe: {
id: 'ctbe',
ext: 'encrypted.ctb',
proprietary: true,
available: false,
priority: 1,
family: 'chitubox-encrypted'
},
goo: { goo: {
id: 'goo', id: 'goo',
ext: '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 { JSZip } from '../../../../ext/jszip-esm.js';
import { PNG } from '../../../../ext/pngjs.esm.js'; import { PNG } from '../../../../ext/pngjs.esm.js';
import { imageToRowMajor, renderRasterLayers, round } from './raster.js'; import { imageToRowMajor, renderRasterLayers, round } from './raster.js';
import { ctbLayerCrypt } from './x_ctb_crypto.js';
const CTB_V3_MAGIC = 0x12fd0086; const CTB_V3_MAGIC = 0x12fd0086;
const HEADER_SIZE = 0x70; const HEADER_SIZE = 0x70;
@ -715,37 +716,11 @@ function requireBytes(input, offset, length) {
} }
function decipher(seed, layerIndex, input) { function decipher(seed, layerIndex, input) {
if (!seed) return input.slice(); return ctbLayerCrypt(seed, layerIndex, input);
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;
} }
function encipher(seed, layerIndex, input) { function encipher(seed, layerIndex, input) {
return decipher(seed, layerIndex, input); return ctbLayerCrypt(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;
}
};
} }
function imageToPNG(image, width, height) { 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 */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { imageToRowMajor, renderRasterLayers, round } from './raster.js'; import { imageToRowMajor, renderRasterLayers, round } from './raster.js';
import { createLayerMetadata, createProcessMetadata, estimatePrintTime, motionFor } from './meta.js';
const FAMILY = "GOO"; const FAMILY = "GOO";
const FILE_VERSION = "V3.0"; const FILE_VERSION = "V3.0";
@ -39,9 +40,10 @@ function encodeGOO(print, progress, photon, rec) {
let previewLarge = createPreview(290, 290); let previewLarge = createPreview(290, 290);
let { ctx, volume } = renderRasterLayers(print, photon, params => { let { ctx, volume } = renderRasterLayers(print, photon, params => {
let { index, rendered, bottom, z, ctx } = params; let { index, rendered, z, ctx } = params;
let { process, width, height } = ctx; let { device, process, width, height } = ctx;
let raster = imageToRowMajor(rendered.image, width, height, value => value ? 255 : 0); let layerMeta = createLayerMetadata(device, process, index, z, rendered.area);
let raster = imageToRowMajor(rendered.image, width, height, normalizePixel);
accumulatePreview(previewSmall, rendered.image, width, height); accumulatePreview(previewSmall, rendered.image, width, height);
accumulatePreview(previewLarge, rendered.image, width, height); accumulatePreview(previewLarge, rendered.image, width, height);
@ -49,22 +51,22 @@ function encodeGOO(print, progress, photon, rec) {
layers.push({ layers.push({
index, index,
data: encodeRLE(raster), data: encodeRLE(raster),
z: round(z), z: layerMeta.z,
pausePositionZ: ctx.device.maxHeight || ctx.device.bedHeight || 0, pausePositionZ: ctx.device.maxHeight || ctx.device.bedHeight || 0,
exposure: bottom ? process.slaBaseOn : process.slaLayerOn, exposure: layerMeta.exposure,
lightOffDelay: 0, lightOffDelay: layerMeta.lightOffDelay,
waitTimeAfterCure: 0, waitTimeAfterCure: layerMeta.waitAfterCure,
waitTimeAfterLift: 0, waitTimeAfterLift: layerMeta.waitAfterLift,
waitTimeBeforeCure: bottom ? process.slaBaseOff : process.slaLayerOff, waitTimeBeforeCure: layerMeta.waitBeforeCure,
liftHeight: bottom ? process.slaBasePeelDist : process.slaPeelDist, liftHeight: layerMeta.liftHeight,
liftSpeed: (bottom ? process.slaBasePeelLiftRate : process.slaPeelLiftRate) * 60, liftSpeed: layerMeta.liftSpeed,
liftHeight2: 0, liftHeight2: 0,
liftSpeed2: 0, liftSpeed2: 0,
retractHeight: bottom ? process.slaBasePeelDist : process.slaPeelDist, retractHeight: layerMeta.retractHeight,
retractSpeed: (bottom ? process.slaBasePeelDropRate : process.slaPeelDropRate) * 60, retractSpeed: layerMeta.retractSpeed,
retractHeight2: 0, retractHeight2: 0,
retractSpeed2: 0, retractSpeed2: 0,
lightPWM: 255 lightPWM: layerMeta.lightPWM
}); });
}, progress ? (value) => progress(value * 0.85, `${rec.ext}_encode`) : null); }, progress ? (value) => progress(value * 0.85, `${rec.ext}_encode`) : null);
@ -99,8 +101,9 @@ function writeGOO(params) {
function writeHeader(writer, params) { function writeHeader(writer, params) {
let { ctx, layers, volume, previewSmall, previewLarge, rec } = params; let { ctx, layers, volume, previewSmall, previewLarge, rec } = params;
let { device, process, width, height } = ctx; let { device, process, width, height } = ctx;
let bottomOff = process.slaBaseOff || 0; let meta = createProcessMetadata(device, process);
let layerOff = process.slaLayerOff || 0; let bottomMotion = motionFor(device, process, true);
let layerMotionValue = motionFor(device, process, false);
writer.writeString(FILE_VERSION, 4); writer.writeString(FILE_VERSION, 4);
writer.writeBytes(FILE_MAGIC); writer.writeBytes(FILE_MAGIC);
@ -126,30 +129,30 @@ function writeHeader(writer, params) {
writer.writeF32(device.bedDepth); writer.writeF32(device.bedDepth);
writer.writeF32(device.maxHeight || device.bedHeight || 0); writer.writeF32(device.maxHeight || device.bedHeight || 0);
writer.writeF32(process.slaSlice); writer.writeF32(process.slaSlice);
writer.writeF32(process.slaLayerOn); writer.writeF32(meta.exposure);
writer.writeU8(1); writer.writeU8(1);
writer.writeF32(0); writer.writeF32(0);
writer.writeF32(0); writer.writeF32(0);
writer.writeF32(0); writer.writeF32(0);
writer.writeF32(bottomOff); writer.writeF32(meta.bottomWaitBeforeCure);
writer.writeF32(0); writer.writeF32(0);
writer.writeF32(0); writer.writeF32(0);
writer.writeF32(layerOff); writer.writeF32(meta.waitBeforeCure);
writer.writeF32(process.slaBaseOn); writer.writeF32(meta.bottomExposure);
writer.writeU32(process.slaBaseLayers || 0); writer.writeU32(meta.bottomLayers);
writer.writeF32(process.slaBasePeelDist); writer.writeF32(bottomMotion.liftHeight);
writer.writeF32(process.slaBasePeelLiftRate * 60); writer.writeF32(bottomMotion.liftSpeed);
writer.writeF32(process.slaPeelDist); writer.writeF32(layerMotionValue.liftHeight);
writer.writeF32(process.slaPeelLiftRate * 60); writer.writeF32(layerMotionValue.liftSpeed);
writer.writeF32(process.slaBasePeelDist); writer.writeF32(bottomMotion.retractHeight);
writer.writeF32(process.slaBasePeelDropRate * 60); writer.writeF32(bottomMotion.retractSpeed);
writer.writeF32(process.slaPeelDist); writer.writeF32(layerMotionValue.retractHeight);
writer.writeF32(process.slaPeelDropRate * 60); writer.writeF32(layerMotionValue.retractSpeed);
for (let i=0; i<8; i++) writer.writeF32(0); for (let i=0; i<8; i++) writer.writeF32(0);
writer.writeU16(255); writer.writeU16(meta.bottomLightPWM);
writer.writeU16(255); writer.writeU16(meta.lightPWM);
writer.writeU8(0); writer.writeU8(0);
writer.writeU32(estimatePrintTime(process, layers.length)); writer.writeU32(estimatePrintTime(device, process, layers.length));
writer.writeF32(round(volume)); writer.writeF32(round(volume));
writer.writeF32(0); writer.writeF32(0);
writer.writeF32(0); writer.writeF32(0);
@ -307,6 +310,7 @@ function decodeLayer(view, layer, pixelCount) {
function encodeRLE(input) { function encodeRLE(input) {
let output = [LAYER_MAGIC]; let output = [LAYER_MAGIC];
let previous = 0;
let current = input[0] || 0; let current = input[0] || 0;
let run = 0; let run = 0;
@ -315,20 +319,38 @@ function encodeRLE(input) {
if (value === current && run < MAX_RUN) { if (value === current && run < MAX_RUN) {
run++; run++;
} else { } else {
writeRun(output, current, run); writeRun(output, current, previous, run);
previous = current;
current = value; current = value;
run = 1; run = 1;
} }
} }
writeRun(output, current, run); writeRun(output, current, previous, run);
output.push(checksum(output)); output.push(checksum(output));
return Uint8Array.from(output); return Uint8Array.from(output);
} }
function writeRun(output, color, run) { function writeRun(output, color, previous, run) {
while (run > 0) { while (run > 0) {
let stride = Math.min(run, MAX_RUN); 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 let lengthCode = stride <= 0x0f ? 0
: stride <= 0x0fff ? 1 : stride <= 0x0fff ? 1
: stride <= 0x0fffff ? 2 : 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) { function decodeRLE(input, pixelCount) {
let rle = input instanceof Uint8Array ? input : new Uint8Array(input); let rle = input instanceof Uint8Array ? input : new Uint8Array(input);
if (rle[0] !== LAYER_MAGIC) { 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) { function checksum(bytes) {
let sum = 0; let sum = 0;
for (let i=1; i<bytes.length; i++) { 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 { PNG } from '../../../../ext/pngjs.esm.js';
import { photon } from './x_photon.js'; import { photon } from './x_photon.js';
import { imageToRowMajor, renderRasterLayers, round } from './raster.js'; import { imageToRowMajor, renderRasterLayers, round } from './raster.js';
import { createLayerMetadata, createProcessMetadata } from './meta.js';
const FORMAT = "rsla"; const FORMAT = "rsla";
const MIME = "application/vnd.gridspace.rsla+zip"; const MIME = "application/vnd.gridspace.rsla+zip";
@ -14,14 +15,17 @@ function encode(print, progress) {
let { ctx, volume } = renderRasterLayers(print, photon, params => { let { ctx, volume } = renderRasterLayers(print, photon, params => {
let { index, rendered, ctx } = 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 file = `layers/${index.toString().padStart(6, "0")}.png`;
let layer = { let layer = {
index, index,
z: round(process.slaFirstOffset + process.slaSlice * index), z: layerMeta.z,
area: round(rendered.area), area: layerMeta.area,
file file,
process: layerMeta
}; };
layers.push(layer); layers.push(layer);
@ -79,18 +83,7 @@ function createManifest(params) {
black: "off" black: "off"
}, },
process: { process: {
bottomLayers: process.slaBaseLayers, ...createProcessMetadata(device, process)
exposure: process.slaLayerOn,
bottomExposure: process.slaBaseOn,
lightOffDelay: process.slaLayerOff,
bottomLightOffDelay: process.slaBaseOff,
liftHeight: process.slaPeelDist,
bottomLiftHeight: process.slaBasePeelDist,
liftSpeed: process.slaPeelLiftRate * 60,
bottomLiftSpeed: process.slaBasePeelLiftRate * 60,
retractSpeed: process.slaPeelDropRate * 60,
firstLayerOffset: process.slaFirstOffset,
antiAlias: process.slaAntiAlias
}, },
layers layers
}; };

View file

@ -1,6 +1,7 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { JSZip } from '../../../../ext/jszip-esm.js'; import { JSZip } from '../../../../ext/jszip-esm.js';
import { createLayerMetadata, createProcessMetadata, round } from './meta.js';
const FORMAT = "vsla"; const FORMAT = "vsla";
const MIME = "application/vnd.gridspace.vsla+zip"; const MIME = "application/vnd.gridspace.vsla+zip";
@ -18,7 +19,7 @@ function encode(print, progress) {
let layers = []; let layers = [];
let volume = 0; let volume = 0;
for (let index=0; index<layermax; index++) { 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; volume += layer.area * process.slaSlice;
layers.push(layer); layers.push(layer);
if (progress) progress((index / layermax) * 0.7, "vector_gen"); if (progress) progress((index / layermax) * 0.7, "vector_gen");
@ -49,7 +50,7 @@ function encode(print, progress) {
} }
function collectLayer(params) { function collectLayer(params) {
let { index, widgets, process } = params; let { index, widgets, device, process } = params;
let polygons = []; let polygons = [];
let area = 0; let area = 0;
let z = process.slaFirstOffset + process.slaSlice * index; let z = process.slaFirstOffset + process.slaSlice * index;
@ -74,11 +75,14 @@ function collectLayer(params) {
} }
}); });
let layerMeta = createLayerMetadata(device, process, index, z, area);
return { return {
index, index,
z: round(z), z: layerMeta.z,
area: round(area), area: layerMeta.area,
file: `layers/${index.toString().padStart(6, "0")}.svg`, file: `layers/${index.toString().padStart(6, "0")}.svg`,
process: layerMeta,
polygons polygons
}; };
} }
@ -91,7 +95,8 @@ function createManifest(params) {
z: layer.z, z: layer.z,
area: layer.area, area: layer.area,
file: layer.file, 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, layerCount: layers.length,
volume: round(volume), volume: round(volume),
process: { process: {
bottomLayers: process.slaBaseLayers, ...createProcessMetadata(device, process)
exposure: process.slaLayerOn,
bottomExposure: process.slaBaseOn,
lightOffDelay: process.slaLayerOff,
bottomLightOffDelay: process.slaBaseOff,
liftHeight: process.slaPeelDist,
bottomLiftHeight: process.slaBasePeelDist,
liftSpeed: process.slaPeelLiftRate * 60,
bottomLiftSpeed: process.slaBasePeelLiftRate * 60,
retractSpeed: process.slaPeelDropRate * 60,
firstLayerOffset: process.slaFirstOffset,
antiAlias: process.slaAntiAlias
}, },
layers: layerList layers: layerList
}; };
@ -150,9 +144,10 @@ function createLayerSVG(params) {
return ` <path fill="#000" fill-rule="evenodd" d="${polyToPath(poly, device)}"/>`; return ` <path fill="#000" fill-rule="evenodd" d="${polyToPath(poly, device)}"/>`;
}).join("\n"); }).join("\n");
let proc = layer.process;
return [ return [
`<?xml version="1.0" encoding="UTF-8"?>`, `<?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, body,
`</svg>`, `</svg>`,
`` ``
@ -184,10 +179,6 @@ function appendPath(parts, poly, device) {
parts.push("Z"); parts.push("Z");
} }
function round(value) {
return Number.parseFloat(Number(value || 0).toFixed(5));
}
export const VSLA = { export const VSLA = {
encode encode
}; };

View file

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