Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d138275bbe | ||
|
|
5f1f77add9 | ||
|
|
ed1009fd15 | ||
|
|
ff29a8c221 | ||
|
|
3be111af43 | ||
|
|
41629b2c76 | ||
|
|
a0c9428d6c | ||
|
|
7e105f255e | ||
|
|
b65d4ae57e | ||
|
|
f32af7eeb1 |
25 changed files with 1994 additions and 434 deletions
14
.github/workflows/build-docs.yml
vendored
14
.github/workflows/build-docs.yml
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
393
docs/release.md
393
docs/release.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
],
|
||||
"gcodeSpindle": [
|
||||
"M3 S{spindle}",
|
||||
"G4 P4000"
|
||||
"G4 P4"
|
||||
],
|
||||
"gcodeChange": [
|
||||
"M6 T{tool} ; change tool to '{tool_name}'"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
17
src/kiri/dev/sla/Elegoo.Mars.5.Ultra.GOO.json
Normal file
17
src/kiri/dev/sla/Elegoo.Mars.5.Ultra.GOO.json
Normal 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
|
||||
}
|
||||
}
|
||||
17
src/kiri/dev/sla/Elegoo.Saturn.4.Ultra.12K.GOO.json
Normal file
17
src/kiri/dev/sla/Elegoo.Saturn.4.Ultra.12K.GOO.json
Normal 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
|
||||
}
|
||||
}
|
||||
17
src/kiri/dev/sla/Elegoo.Saturn.4.Ultra.16K.GOO.json
Normal file
17
src/kiri/dev/sla/Elegoo.Saturn.4.Ultra.16K.GOO.json
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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++) {
|
||||
|
|
|
|||
|
|
@ -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)}),
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
128
src/kiri/mode/sla/work/meta.js
Normal file
128
src/kiri/mode/sla/work/meta.js
Normal 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
|
|
@ -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) {
|
||||
|
|
|
|||
321
src/kiri/mode/sla/work/x_ctb_crypto.js
Normal file
321
src/kiri/mode/sla/work/x_ctb_crypto.js
Normal 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));
|
||||
}
|
||||
|
|
@ -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++) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue