three-slicer 0.1.7 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +59 -3
- package/THIRD-PARTY-NOTICES.md +37 -0
- package/components/README.md +2 -0
- package/components/dist/SettingsPanel.js +92 -92
- package/data/config-schema.json +698 -0
- package/data/printers.json +1 -1
- package/data/ui-tree.json +342 -0
- package/engine/README.md +20 -0
- package/engine/examples/sla_headless.mjs +51 -0
- package/engine/index.d.ts +3 -0
- package/engine/index.js +6 -0
- package/engine/src/client.js +31 -2
- package/engine/src/settings.js +157 -0
- package/engine/src/sla_core.js +188 -0
- package/engine/src/sla_request.js +185 -0
- package/engine/src/slicer.worker.js +63 -1
- package/engine/src/slicer_core.js +0 -0
- package/engine/src/slicer_core.mt.js +0 -0
- package/package.json +8 -3
- package/types/client.d.ts +14 -0
- package/types/settings-keys.d.ts +107 -1
- package/types/settings.d.ts +34 -0
- package/types/sla.d.ts +167 -0
- package/types/viewer.d.ts +2 -0
- package/types/worker.d.ts +9 -1
- package/viewer/README.md +7 -1
- package/viewer/dist/Viewport.js +3454 -3138
- package/viewer/dist/model_loaders.js +1 -1
- package/viewer/dist/parse_3mf-C2X_W442.js +300 -0
- package/viewer/dist/parse_3mf.worker.js +1 -1
- package/viewer/dist/parse_3mf-8oCnCJNK.js +0 -216
package/engine/src/settings.js
CHANGED
|
@@ -91,6 +91,26 @@ export const printerKeys = printers.keys
|
|
|
91
91
|
/** Vendor -> profile name -> `[nozzle, setIndex, model]`, straight from the data (for building a picker). */
|
|
92
92
|
export const printersByVendor = printers.byVendor
|
|
93
93
|
|
|
94
|
+
/** Vendor -> slicing technology. Absent means FFF — only the resin vendor bundles are marked, so a printer
|
|
95
|
+
* picker can keep FFF machines and SLA machines apart with one lookup. */
|
|
96
|
+
export const printerTechByVendor = printers.techByVendor ?? {}
|
|
97
|
+
|
|
98
|
+
/** The resin material catalog (SLA vendor bundles, inherits flattened): {name, bundle, type, vendor, colour,
|
|
99
|
+
* exposure_time, initial_exposure_time, initial_layer_height, layerHeight}. layerHeight is the preset's
|
|
100
|
+
* compatibility condition reduced to a number — the picker filters on it without an expression engine. */
|
|
101
|
+
export const resinCatalog = printers.resins ?? []
|
|
102
|
+
|
|
103
|
+
/** The settings a resin material applies, ready to merge — the exposure family plus the remembered pick.
|
|
104
|
+
* `null` when unknown. The material never touches support/pad keys (upstream keeps those in sla_print). */
|
|
105
|
+
export function resinSettingsFor(name) {
|
|
106
|
+
const entry = resinCatalog.find(r => r.name === name)
|
|
107
|
+
if (!entry) return null
|
|
108
|
+
const out = { sla_material_settings_id: name }
|
|
109
|
+
for (const key of ['exposure_time', 'initial_exposure_time', 'initial_layer_height'])
|
|
110
|
+
if (Number.isFinite(entry[key])) out[key] = entry[key]
|
|
111
|
+
return out
|
|
112
|
+
}
|
|
113
|
+
|
|
94
114
|
// Process (print) presets live in the ~800 KB processes.json, so they load on demand — the first call fetches,
|
|
95
115
|
// later ones reuse the same promise. Returns a small facade so no caller has to know the column layout.
|
|
96
116
|
let processesPromise = null
|
|
@@ -534,6 +554,143 @@ export function deriveKernelParams(settings, opts) {
|
|
|
534
554
|
}
|
|
535
555
|
}
|
|
536
556
|
|
|
557
|
+
// ---- Printer technology (FFF vs SLA routing) ----------------------------------------------------------------
|
|
558
|
+
// The technology is a property of the printer profile, exactly as upstream models it: one settings map, two
|
|
559
|
+
// disjoint key families, and the profile's technology key decides which family the UI shows and which slicing
|
|
560
|
+
// path runs. The schema already carries the key (inherited from the upstream lineage), so an imported preset
|
|
561
|
+
// that names a resin printer routes itself.
|
|
562
|
+
export function printerTechnology(settings) {
|
|
563
|
+
return String(settingRaw(settings, 'printer_technology') ?? 'FFF').toUpperCase() === 'SLA' ? 'SLA' : 'FFF'
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// SLA settings provenance table. Rows come from the schema's `defined_in` marker so a new resin option cannot
|
|
567
|
+
// silently leave the UI/bridge without a documented omission/default decision. Explicit rows cover upstream
|
|
568
|
+
// expression defaults the extractor cannot evaluate (support_tree_type and branching's bridge limits).
|
|
569
|
+
const SLA_SETTING_OVERRIDES = {
|
|
570
|
+
layer_height: { omission: 'resin-default', default: 0.05, provenance: 'shared key; FFF schema default is not used' },
|
|
571
|
+
support_tree_type: { default: 'default', enum: ['default', 'branching', 'organic'], provenance: 'upstream enum expression' },
|
|
572
|
+
branchingsupport_pillar_connection_mode: { default: 'dynamic', enum: ['zigzag', 'cross', 'dynamic'], provenance: 'upstream enum expression' },
|
|
573
|
+
sla_archive_format: { default: 'SL1', enum: ['SL1'], provenance: 'Original Prusa SL1 archive' },
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const SLA_SCHEMA_ROWS = Object.fromEntries(Object.entries(schema)
|
|
577
|
+
.filter(([key, meta]) => key === 'printer_technology' || String(meta.defined_in ?? '').includes('sla_'))
|
|
578
|
+
.map(([key, meta]) => [key, {
|
|
579
|
+
source: 'config-schema.json',
|
|
580
|
+
omission: 'schema-default',
|
|
581
|
+
default: meta.default,
|
|
582
|
+
...(meta.enum_values ? { enum: [...meta.enum_values] } : {}),
|
|
583
|
+
provenance: `${meta.defined_in}:${meta.line}`,
|
|
584
|
+
}]))
|
|
585
|
+
|
|
586
|
+
export const SLA_SETTING_CONTRACT = Object.freeze(Object.fromEntries(
|
|
587
|
+
Object.entries({ ...SLA_SCHEMA_ROWS, ...SLA_SETTING_OVERRIDES }).map(([key, row]) => [key, Object.freeze({
|
|
588
|
+
...row,
|
|
589
|
+
...(SLA_SETTING_OVERRIDES[key] ?? {}),
|
|
590
|
+
})]),
|
|
591
|
+
))
|
|
592
|
+
export const slaSettingContract = SLA_SETTING_CONTRACT
|
|
593
|
+
export const slaSettingKeys = Object.freeze(Object.keys(SLA_SETTING_CONTRACT))
|
|
594
|
+
|
|
595
|
+
// Right-panel settings values -> SLA slice parameters (sla_core.js) + the raster/export geometry the viewer's
|
|
596
|
+
// SL1 writer needs. A separate function, NOT a branch inside deriveKernelParams: that one feeds the WASM kernel
|
|
597
|
+
// and its empty-map key count is a pinned invariant — this one feeds the JS contour slicer and owes bytes to
|
|
598
|
+
// nobody yet. Keys the schema does not define (the resin display's pixel grid, the per-layer height override)
|
|
599
|
+
// are read from the map directly, so they follow the same omission-friendly rule: absent means the default.
|
|
600
|
+
export function deriveSlaParams(settings) {
|
|
601
|
+
const S = k => settingScalar(settings, k)
|
|
602
|
+
const num = (k, d) => { const v = Number(S(k)); return Number.isFinite(v) && v > 0 ? v : d }
|
|
603
|
+
|
|
604
|
+
// Read from the MAP, not through the schema fallback: the key is shared with FFF and its schema default is that
|
|
605
|
+
// technology's opinion (0.2mm) — four resin layers thick. Absent means the resin default here.
|
|
606
|
+
const rawLayerHeight = settings?.layer_height
|
|
607
|
+
const mapLayerHeight = Number(Array.isArray(rawLayerHeight) ? rawLayerHeight[0] : rawLayerHeight)
|
|
608
|
+
const layer_height = Number.isFinite(mapLayerHeight) && mapLayerHeight > 0 ? mapLayerHeight : 0.05
|
|
609
|
+
// Zero is a legal fade count (no fade band), so this one cannot go through the positives-only reader.
|
|
610
|
+
const fadedRaw = Number(S('faded_layers'))
|
|
611
|
+
const requestedSupportTree = String(S('support_tree_type') ?? 'default').toLowerCase()
|
|
612
|
+
const support_tree_type = ['default', 'branching', 'organic'].includes(requestedSupportTree)
|
|
613
|
+
? requestedSupportTree
|
|
614
|
+
: 'default'
|
|
615
|
+
return {
|
|
616
|
+
layer_height,
|
|
617
|
+
initial_layer_height: num('initial_layer_height', layer_height),
|
|
618
|
+
exposure_time: num('exposure_time', 7),
|
|
619
|
+
initial_exposure_time: num('initial_exposure_time', 35),
|
|
620
|
+
faded_layers: Number.isFinite(fadedRaw) && fadedRaw >= 0 ? fadedRaw : 10,
|
|
621
|
+
// The physical display and its pixel grid — what turns millimetres into raster pixels at export time.
|
|
622
|
+
// Fallbacks are the SL1's own panel, the reference machine of the format this exports to.
|
|
623
|
+
display_width: num('display_width', 120.96),
|
|
624
|
+
display_height: num('display_height', 68.04),
|
|
625
|
+
display_pixels_x: num('display_pixels_x', 2560),
|
|
626
|
+
display_pixels_y: num('display_pixels_y', 1440),
|
|
627
|
+
// Panel mounting: the whole SL1 family is portrait with X mirrored (projection through the vat floor);
|
|
628
|
+
// the raster writer swaps the image axes for portrait exactly like upstream SL1.cpp create_raster.
|
|
629
|
+
display_orientation: String(S('display_orientation') ?? 'portrait') === 'landscape' ? 'landscape' : 'portrait',
|
|
630
|
+
display_mirror_x: settingRaw(settings, 'display_mirror_x') === undefined ? true : !!settingRaw(settings, 'display_mirror_x'),
|
|
631
|
+
display_mirror_y: !!settingRaw(settings, 'display_mirror_y'),
|
|
632
|
+
// SL1 archive identity fields (config.ini): written as-is, empty when the host never set a named
|
|
633
|
+
// profile — the same thing upstream produces then. material_print_speed defaults to 'fast', the value
|
|
634
|
+
// every Prusa vendor SL1 material carries (-> expUserProfile 0 in the archive).
|
|
635
|
+
material_print_speed: String(S('material_print_speed') ?? 'fast'),
|
|
636
|
+
printer_model: String(S('printer_model') ?? ''),
|
|
637
|
+
printer_variant: String(S('printer_variant') ?? ''),
|
|
638
|
+
printer_settings_id: String(S('printer_settings_id') ?? ''),
|
|
639
|
+
sla_print_settings_id: String(S('sla_print_settings_id') ?? ''),
|
|
640
|
+
sla_material_settings_id: String(S('sla_material_settings_id') ?? ''),
|
|
641
|
+
// The kernel's over-bed check and centring read bed dimensions; for a resin printer the printable area IS
|
|
642
|
+
// the display, so the display doubles as the bed the shared parser sees.
|
|
643
|
+
bed_width: num('display_width', 120.96),
|
|
644
|
+
bed_depth: num('display_height', 68.04),
|
|
645
|
+
// Support generation (kernel slice_sla): the grid-pillar generator's shape parameters, all schema keys the
|
|
646
|
+
// SLA extraction pass brought in — schema defaults rule, an edit wins.
|
|
647
|
+
supports_enable: !!settingRaw(settings, 'supports_enable'),
|
|
648
|
+
pad_enable: !!settingRaw(settings, 'pad_enable'),
|
|
649
|
+
// Passed through so a requested hollow is REFUSED (typed kernel error) instead of silently sliced solid —
|
|
650
|
+
// the OpenVDB chain hollowing needs is not ported, and a solid print labeled hollowed is the worst outcome.
|
|
651
|
+
hollowing_enable: !!settingRaw(settings, 'hollowing_enable'),
|
|
652
|
+
support_buildplate_only: !!settingRaw(settings, 'support_buildplate_only'),
|
|
653
|
+
support_pillar_diameter: num('support_pillar_diameter', 1.0),
|
|
654
|
+
support_head_front_diameter: num('support_head_front_diameter', 0.4),
|
|
655
|
+
support_head_width: num('support_head_width', 1.0),
|
|
656
|
+
support_head_penetration: num('support_head_penetration', 0.2),
|
|
657
|
+
support_points_density_relative: num('support_points_density_relative', 100),
|
|
658
|
+
support_critical_angle: num('support_critical_angle', 45),
|
|
659
|
+
support_tree_type,
|
|
660
|
+
// The tree shape (upstream SupportTreeConfig): bridge reach, bracing distance, the base foot cone, the
|
|
661
|
+
// pillar connection style, and how far the object is lifted onto its supports. Elevation may legally be
|
|
662
|
+
// ZERO (print directly on the plate), so it cannot go through the positives-only reader.
|
|
663
|
+
support_max_bridge_length: num('support_max_bridge_length', 15),
|
|
664
|
+
support_max_pillar_link_distance: num('support_max_pillar_link_distance', 10),
|
|
665
|
+
support_base_diameter: num('support_base_diameter', 4),
|
|
666
|
+
support_base_height: num('support_base_height', 1),
|
|
667
|
+
// The rest of upstream make_support_cfg (SLAPrint.cpp:50) — small-pillar percent arrives as "50%" so the
|
|
668
|
+
// percent sign is stripped here; widening factor and base safety distance are legally ZERO (zero safety
|
|
669
|
+
// falls back to the kernel's own safety_distance), so neither can go through the positives-only reader.
|
|
670
|
+
support_small_pillar_diameter_percent:
|
|
671
|
+
(() => { const v = Number(String(S('support_small_pillar_diameter_percent') ?? '').replace('%', '')); return Number.isFinite(v) && v > 0 ? v : 50 })(),
|
|
672
|
+
support_pillar_widening_factor:
|
|
673
|
+
(() => { const v = Number(S('support_pillar_widening_factor')); return Number.isFinite(v) && v >= 0 ? v : 0 })(),
|
|
674
|
+
support_base_safety_distance:
|
|
675
|
+
(() => { const v = Number(S('support_base_safety_distance')); return Number.isFinite(v) && v >= 0 ? v : 1 })(),
|
|
676
|
+
support_max_bridges_on_pillar: num('support_max_bridges_on_pillar', 3),
|
|
677
|
+
support_max_weight_on_model: num('support_max_weight_on_model', 10),
|
|
678
|
+
// The generator's input slices are gap-closed with this radius, exactly like upstream slice_mesh_ex.
|
|
679
|
+
slice_closing_radius: num('slice_closing_radius', 0.049),
|
|
680
|
+
support_object_elevation: (() => { const v = Number(S('support_object_elevation')); return Number.isFinite(v) && v >= 0 ? v : 5 })(),
|
|
681
|
+
support_pillar_connection_mode: String(S('support_pillar_connection_mode') ?? 'dynamic'),
|
|
682
|
+
// The pad (upstream make_pad_cfg): wall height is legally ZERO (flat pad, no cavity), so it cannot go
|
|
683
|
+
// through the positives-only reader.
|
|
684
|
+
pad_brim_size: num('pad_brim_size', 1.6),
|
|
685
|
+
pad_wall_thickness: num('pad_wall_thickness', 2),
|
|
686
|
+
pad_wall_height:
|
|
687
|
+
(() => { const v = Number(S('pad_wall_height')); return Number.isFinite(v) && v >= 0 ? v : 0 })(),
|
|
688
|
+
pad_wall_slope: num('pad_wall_slope', 90),
|
|
689
|
+
pad_max_merge_distance: num('pad_max_merge_distance', 50),
|
|
690
|
+
pad_around_object: !!settingRaw(settings, 'pad_around_object'),
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
537
694
|
// ---- Preset files (the .json OrcaSlicer imports/exports) ----------------------------------------------------
|
|
538
695
|
// Re-exported here rather than given their own subpath: they are settings-map transforms like the 3mf project
|
|
539
696
|
// codecs above, and a consumer that has one hand on `settings` should not need a second import path for them.
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Pure-JS SLA (resin) slicer: binary STL -> per-layer closed contours, in the SAME layer-stream contract the
|
|
2
|
+
// FFF kernel emits ({z, paths stride 8 [x0,y0,z0,enc, x1,y1,z1,enc], widths one per segment, enc = role+tool*16})
|
|
3
|
+
// — so the viewer's GPU toolpath preview, layer slider and plate cache consume an SLA slice unmodified.
|
|
4
|
+
//
|
|
5
|
+
// Why JS and not the WASM kernel: an SLA slice is contours only — no walls, no infill, no G-code — and a
|
|
6
|
+
// plane/triangle sweep is deterministic in doubles, so nothing here needs the C++ side. The raster/export half
|
|
7
|
+
// (PNG per layer, SL1 zip) is DELIBERATELY not in this module: rasterization needs a canvas, which Node does not
|
|
8
|
+
// have, and the engine package keeps zero runtime dependencies. The viewer owns that half (sl1_write.js).
|
|
9
|
+
//
|
|
10
|
+
// Determinism: iteration order is the STL's own triangle order, stitching maps insert in encounter order, and
|
|
11
|
+
// vertices on a slicing plane are pushed up by a fixed epsilon — same input bytes, same output bytes.
|
|
12
|
+
|
|
13
|
+
const ROLE_CONTOUR = 1 // rendered as 'wall' by the preview's feature colouring
|
|
14
|
+
const CONTOUR_WIDTH = 0.2 // display width of a contour segment (SLA has no bead width)
|
|
15
|
+
const PLANE_EPS = 1e-9 // a vertex exactly on a plane counts as above it — deterministic tie-break
|
|
16
|
+
const KEY_SCALE = 1e4 // endpoint welding: 0.1 micrometre grid
|
|
17
|
+
|
|
18
|
+
/** Binary STL -> Float32Array of 9 floats per triangle (vertices only; normals are recomputed). */
|
|
19
|
+
export function parseBinarySTL(bytes) {
|
|
20
|
+
if (!(bytes instanceof Uint8Array)) bytes = new Uint8Array(bytes)
|
|
21
|
+
if (bytes.byteLength < 84) return { error: 'not a binary STL (shorter than its own header)' }
|
|
22
|
+
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
23
|
+
const count = dv.getUint32(80, true)
|
|
24
|
+
if (84 + 50 * count !== bytes.byteLength) return { error: 'not a binary STL (triangle count does not match the byte length)' }
|
|
25
|
+
const tris = new Float32Array(count * 9)
|
|
26
|
+
for (let i = 0; i < count; i++) {
|
|
27
|
+
const base = 84 + 50 * i + 12 // skip the stored normal — recomputed from the winding below
|
|
28
|
+
for (let f = 0; f < 9; f++) tris[i * 9 + f] = dv.getFloat32(base + 4 * f, true)
|
|
29
|
+
}
|
|
30
|
+
return { tris, count }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ptKey = (x, y) => Math.round(x * KEY_SCALE) + ',' + Math.round(y * KEY_SCALE)
|
|
34
|
+
|
|
35
|
+
// One plane through the triangle soup -> oriented segments. Direction is Z×N (N = winding normal), which walks
|
|
36
|
+
// outer boundaries counter-clockwise and holes clockwise — that orientation is what makes the summed shoelace
|
|
37
|
+
// area the NET cross-section (outer minus holes) without any containment test.
|
|
38
|
+
function sliceAtPlane(tris, z) {
|
|
39
|
+
const segs = [] // [x0,y0,x1,y1] oriented
|
|
40
|
+
const n = tris.length / 9
|
|
41
|
+
for (let t = 0; t < n; t++) {
|
|
42
|
+
const o = t * 9
|
|
43
|
+
const vx = [tris[o], tris[o + 3], tris[o + 6]]
|
|
44
|
+
const vy = [tris[o + 1], tris[o + 4], tris[o + 7]]
|
|
45
|
+
const vz = [tris[o + 2], tris[o + 5], tris[o + 8]]
|
|
46
|
+
for (let i = 0; i < 3; i++) if (Math.abs(vz[i] - z) < PLANE_EPS) vz[i] = z + PLANE_EPS
|
|
47
|
+
const below = [vz[0] < z, vz[1] < z, vz[2] < z]
|
|
48
|
+
const nb = (below[0] ? 1 : 0) + (below[1] ? 1 : 0) + (below[2] ? 1 : 0)
|
|
49
|
+
if (nb === 0 || nb === 3) continue
|
|
50
|
+
// The lone vertex (on the minority side) and its two crossing edges
|
|
51
|
+
const lone = nb === 1 ? below.indexOf(true) : below.indexOf(false)
|
|
52
|
+
const a = (lone + 1) % 3, b = (lone + 2) % 3
|
|
53
|
+
const ta = (z - vz[lone]) / (vz[a] - vz[lone])
|
|
54
|
+
const tb = (z - vz[lone]) / (vz[b] - vz[lone])
|
|
55
|
+
let px = vx[lone] + ta * (vx[a] - vx[lone]), py = vy[lone] + ta * (vy[a] - vy[lone])
|
|
56
|
+
let qx = vx[lone] + tb * (vx[b] - vx[lone]), qy = vy[lone] + tb * (vy[b] - vy[lone])
|
|
57
|
+
// Winding normal N = (B-A)×(C-A); in-plane direction Z×N = (-Ny, Nx)
|
|
58
|
+
const ux = vx[1] - vx[0], uy = vy[1] - vy[0], uz = vz[1] - vz[0]
|
|
59
|
+
const wx = vx[2] - vx[0], wy = vy[2] - vy[0], wz = vz[2] - vz[0]
|
|
60
|
+
const nx = uy * wz - uz * wy, ny = uz * wx - ux * wz
|
|
61
|
+
const dirx = -ny, diry = nx
|
|
62
|
+
if ((qx - px) * dirx + (qy - py) * diry < 0) { const sx = px, sy = py; px = qx; py = qy; qx = sx; qy = sy }
|
|
63
|
+
if (Math.abs(px - qx) > 1e-12 || Math.abs(py - qy) > 1e-12) segs.push(px, py, qx, qy)
|
|
64
|
+
}
|
|
65
|
+
return segs
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Oriented segments -> closed loops (walk start-key -> next segment). Open chains (non-manifold geometry) are
|
|
69
|
+
// dropped whole and counted, mirroring how the paint importer drops a malformed facet rather than guessing.
|
|
70
|
+
function stitchLoops(segs) {
|
|
71
|
+
const byStart = new Map()
|
|
72
|
+
const segCount = segs.length / 4
|
|
73
|
+
for (let s = 0; s < segCount; s++) {
|
|
74
|
+
const k = ptKey(segs[s * 4], segs[s * 4 + 1])
|
|
75
|
+
let list = byStart.get(k)
|
|
76
|
+
if (!list) { list = []; byStart.set(k, list) }
|
|
77
|
+
list.push(s)
|
|
78
|
+
}
|
|
79
|
+
const used = new Uint8Array(segCount)
|
|
80
|
+
const loops = []
|
|
81
|
+
let dropped = 0
|
|
82
|
+
for (let s0 = 0; s0 < segCount; s0++) {
|
|
83
|
+
if (used[s0]) continue
|
|
84
|
+
const startKey = ptKey(segs[s0 * 4], segs[s0 * 4 + 1])
|
|
85
|
+
const pts = []
|
|
86
|
+
let cur = s0, closed = false
|
|
87
|
+
while (true) {
|
|
88
|
+
used[cur] = 1
|
|
89
|
+
pts.push(segs[cur * 4], segs[cur * 4 + 1])
|
|
90
|
+
const endKey = ptKey(segs[cur * 4 + 2], segs[cur * 4 + 3])
|
|
91
|
+
if (endKey === startKey) { closed = true; break }
|
|
92
|
+
const nexts = byStart.get(endKey)
|
|
93
|
+
let next = -1
|
|
94
|
+
if (nexts) for (const cand of nexts) if (!used[cand]) { next = cand; break }
|
|
95
|
+
if (next < 0) break
|
|
96
|
+
cur = next
|
|
97
|
+
}
|
|
98
|
+
if (closed && pts.length >= 6) loops.push(pts)
|
|
99
|
+
else dropped++
|
|
100
|
+
}
|
|
101
|
+
return { loops, dropped }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const loopArea = (pts) => { // signed shoelace, CCW positive
|
|
105
|
+
let area = 0
|
|
106
|
+
for (let i = 0, n = pts.length / 2; i < n; i++) {
|
|
107
|
+
const j = (i + 1) % n
|
|
108
|
+
area += pts[i * 2] * pts[j * 2 + 1] - pts[j * 2] * pts[i * 2 + 1]
|
|
109
|
+
}
|
|
110
|
+
return area / 2
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Slice a binary STL for a resin print.
|
|
115
|
+
* params: layer_height, initial_layer_height, exposure_time, initial_exposure_time, faded_layers,
|
|
116
|
+
* sla_layer_overhead (per-layer tilt/lift seconds) — all optional.
|
|
117
|
+
* hooks: {onProgress(done,total), onLayer({z,idx,paths,widths,area})} — layer buffers belong to the callee.
|
|
118
|
+
* Returns {stats, layers} (layers omitted when onLayer took them, mirroring the kernel's streamed mode).
|
|
119
|
+
*/
|
|
120
|
+
export function sliceSla(stlBytes, params = {}, hooks = {}) {
|
|
121
|
+
const parsed = parseBinarySTL(stlBytes)
|
|
122
|
+
if (parsed.error) return { error: parsed.error }
|
|
123
|
+
const { tris, count } = parsed
|
|
124
|
+
if (!count) return { error: 'empty STL (no triangles)' }
|
|
125
|
+
|
|
126
|
+
const lh = Number(params.layer_height) > 0 ? Number(params.layer_height) : 0.05
|
|
127
|
+
const ilh = Number(params.initial_layer_height) > 0 ? Number(params.initial_layer_height) : lh
|
|
128
|
+
const exposure = Number(params.exposure_time) > 0 ? Number(params.exposure_time) : 7
|
|
129
|
+
const initialExposure = Number(params.initial_exposure_time) > 0 ? Number(params.initial_exposure_time) : 35
|
|
130
|
+
const faded = Number.isFinite(Number(params.faded_layers)) ? Math.max(0, Number(params.faded_layers)) : 10
|
|
131
|
+
const overhead = Number(params.sla_layer_overhead) >= 0 ? Number(params.sla_layer_overhead) : 6
|
|
132
|
+
|
|
133
|
+
let minZ = Infinity, maxZ = -Infinity
|
|
134
|
+
for (let i = 2; i < tris.length; i += 3) { const v = tris[i]; if (v < minZ) minZ = v; if (v > maxZ) maxZ = v }
|
|
135
|
+
const height = maxZ - minZ
|
|
136
|
+
if (!(height > 0)) return { error: 'model has no height to slice' }
|
|
137
|
+
|
|
138
|
+
const layerCount = height <= ilh ? 1 : 1 + Math.ceil((height - ilh) / lh)
|
|
139
|
+
const layers = hooks.onLayer ? null : []
|
|
140
|
+
let segTotal = 0, volume = 0, droppedTotal = 0
|
|
141
|
+
|
|
142
|
+
for (let i = 0; i < layerCount; i++) {
|
|
143
|
+
const thickness = i === 0 ? ilh : lh
|
|
144
|
+
const top = minZ + (i === 0 ? ilh : ilh + i * lh)
|
|
145
|
+
const mid = top - thickness / 2
|
|
146
|
+
const { loops, dropped } = stitchLoops(sliceAtPlane(tris, mid))
|
|
147
|
+
droppedTotal += dropped
|
|
148
|
+
let area = 0, segCount = 0
|
|
149
|
+
for (const pts of loops) { area += loopArea(pts); segCount += pts.length / 2 }
|
|
150
|
+
area = Math.max(0, area)
|
|
151
|
+
volume += area * thickness
|
|
152
|
+
const paths = new Float32Array(segCount * 8)
|
|
153
|
+
const widths = new Float32Array(segCount)
|
|
154
|
+
let w = 0
|
|
155
|
+
for (const pts of loops) {
|
|
156
|
+
const n = pts.length / 2
|
|
157
|
+
for (let p = 0; p < n; p++) {
|
|
158
|
+
const q = (p + 1) % n
|
|
159
|
+
paths[w * 8] = pts[p * 2]; paths[w * 8 + 1] = pts[p * 2 + 1]; paths[w * 8 + 2] = top; paths[w * 8 + 3] = ROLE_CONTOUR
|
|
160
|
+
paths[w * 8 + 4] = pts[q * 2]; paths[w * 8 + 5] = pts[q * 2 + 1]; paths[w * 8 + 6] = top; paths[w * 8 + 7] = ROLE_CONTOUR
|
|
161
|
+
widths[w] = CONTOUR_WIDTH
|
|
162
|
+
w++
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
segTotal += segCount
|
|
166
|
+
const layer = { z: top, idx: i, paths, widths, area }
|
|
167
|
+
if (hooks.onLayer) hooks.onLayer(layer)
|
|
168
|
+
else layers.push(layer)
|
|
169
|
+
if (hooks.onProgress && (i % 16 === 0 || i === layerCount - 1)) hooks.onProgress(i + 1, layerCount)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Exposure fades linearly from the initial to the regular value over the faded band, upstream's model.
|
|
173
|
+
let time = layerCount * overhead
|
|
174
|
+
for (let i = 0; i < layerCount; i++) {
|
|
175
|
+
const blend = faded > 0 ? Math.min(1, i / faded) : 1
|
|
176
|
+
time += initialExposure + (exposure - initialExposure) * blend
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const stats = {
|
|
180
|
+
sla: true, streamed: !!hooks.onLayer,
|
|
181
|
+
layers: layerCount, path_segments: segTotal, filament_mm: 0,
|
|
182
|
+
resin_ml: volume / 1000, volume_mm3: volume,
|
|
183
|
+
time_estimate: Math.round(time),
|
|
184
|
+
open_chains_dropped: droppedTotal,
|
|
185
|
+
layer_height: lh, initial_layer_height: ilh,
|
|
186
|
+
}
|
|
187
|
+
return layers ? { stats, layers } : { stats }
|
|
188
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
export const SLA_JOB_VERSION = 1
|
|
2
|
+
|
|
3
|
+
export const SLA_CAPABILITIES = Object.freeze({
|
|
4
|
+
defaultSupport: 'wasm-required',
|
|
5
|
+
branchingSupport: 'wasm-required',
|
|
6
|
+
organicSupport: 'unsupported',
|
|
7
|
+
manualSupportPoints: 'wasm-required',
|
|
8
|
+
modifierVolumes: 'wasm-required',
|
|
9
|
+
pad: 'wasm-required',
|
|
10
|
+
correction: 'wasm-required',
|
|
11
|
+
hollowing: 'unsupported',
|
|
12
|
+
drainHoles: 'unsupported',
|
|
13
|
+
jsFallback: 'single-object-contours-only',
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export class SlaRequestError extends Error {
|
|
17
|
+
constructor(code, message) {
|
|
18
|
+
super(message)
|
|
19
|
+
this.name = 'SlaRequestError'
|
|
20
|
+
this.code = code
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const fail = (code, message) => { throw new SlaRequestError(code, message) }
|
|
25
|
+
const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof ArrayBuffer) && !ArrayBuffer.isView(value)
|
|
26
|
+
const isBinary = (value) => value instanceof ArrayBuffer || value instanceof Uint8Array
|
|
27
|
+
const has = (value, key) => Object.prototype.hasOwnProperty.call(value, key)
|
|
28
|
+
const finite = (value) => typeof value === 'number' && Number.isFinite(value)
|
|
29
|
+
const nonEmptyText = (value) => typeof value === 'string' && value.length > 0
|
|
30
|
+
const optionalRecord = (value, path) => {
|
|
31
|
+
if (value === undefined) return undefined
|
|
32
|
+
if (!isRecord(value)) fail('SLA_INVALID_REQUEST', `${path} must be an object`)
|
|
33
|
+
return value
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const matrixOf = (value, path) => {
|
|
37
|
+
if (value === undefined) return undefined
|
|
38
|
+
if (!Array.isArray(value) || value.length !== 16 || value.some(v => !finite(v)))
|
|
39
|
+
fail('SLA_INVALID_REQUEST', `${path} must be 16 finite numbers`)
|
|
40
|
+
return value
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const isIdentity = (matrix) => !matrix || matrix.every((value, index) => value === (index % 5 === 0 ? 1 : 0))
|
|
44
|
+
|
|
45
|
+
const pointOf = (value, path) => {
|
|
46
|
+
if (!isRecord(value)) fail('SLA_INVALID_REQUEST', `${path} must be an object`)
|
|
47
|
+
if (!['manual', 'automatic', 'island', 'slope'].includes(value.type))
|
|
48
|
+
fail('SLA_INVALID_REQUEST', `${path}.type must be 'manual', 'automatic', 'island', or 'slope'`)
|
|
49
|
+
if (!Array.isArray(value.position) || value.position.length !== 3 || value.position.some(v => !finite(v)))
|
|
50
|
+
fail('SLA_INVALID_REQUEST', `${path}.position must be three finite numbers`)
|
|
51
|
+
for (const key of ['radius', 'elevation'])
|
|
52
|
+
if (has(value, key) && (!finite(value[key]) || value[key] < 0)) fail('SLA_INVALID_REQUEST', `${path}.${key} must be a non-negative finite number`)
|
|
53
|
+
for (const key of ['permanent', 'manual'])
|
|
54
|
+
if (has(value, key) && typeof value[key] !== 'boolean') fail('SLA_INVALID_REQUEST', `${path}.${key} must be boolean`)
|
|
55
|
+
return value
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const drainHoleOf = (value, path) => {
|
|
59
|
+
if (!isRecord(value)) fail('SLA_INVALID_REQUEST', `${path} must be an object`)
|
|
60
|
+
for (const key of ['position', 'normal'])
|
|
61
|
+
if (!Array.isArray(value[key]) || value[key].length !== 3 || value[key].some(v => !finite(v)))
|
|
62
|
+
fail('SLA_INVALID_REQUEST', `${path}.${key} must be three finite numbers`)
|
|
63
|
+
for (const key of ['radius', 'height'])
|
|
64
|
+
if (!finite(value[key]) || value[key] < 0) fail('SLA_INVALID_REQUEST', `${path}.${key} must be a non-negative finite number`)
|
|
65
|
+
return value
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const modifierOf = (value, path) => {
|
|
69
|
+
if (!isRecord(value)) fail('SLA_INVALID_REQUEST', `${path} must be an object`)
|
|
70
|
+
if (!nonEmptyText(value.id)) fail('SLA_INVALID_REQUEST', `${path}.id must be a non-empty string`)
|
|
71
|
+
if (value.kind !== 'blocker' && value.kind !== 'enforcer') fail('SLA_INVALID_REQUEST', `${path}.kind must be 'blocker' or 'enforcer'`)
|
|
72
|
+
if (!isBinary(value.stl)) fail('SLA_INVALID_REQUEST', `${path}.stl must be an ArrayBuffer or Uint8Array`)
|
|
73
|
+
matrixOf(value.transform, `${path}.transform`)
|
|
74
|
+
optionalRecord(value.settings, `${path}.settings`)
|
|
75
|
+
return value
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const instanceOf = (value, path) => {
|
|
79
|
+
if (!isRecord(value)) fail('SLA_INVALID_REQUEST', `${path} must be an object`)
|
|
80
|
+
if (!nonEmptyText(value.id)) fail('SLA_INVALID_REQUEST', `${path}.id must be a non-empty string`)
|
|
81
|
+
matrixOf(value.transform, `${path}.transform`)
|
|
82
|
+
optionalRecord(value.settings, `${path}.settings`)
|
|
83
|
+
return value
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const objectOf = (value, index, ids) => {
|
|
87
|
+
const path = `objects[${index}]`
|
|
88
|
+
if (!isRecord(value)) fail('SLA_INVALID_REQUEST', `${path} must be an object`)
|
|
89
|
+
if (!nonEmptyText(value.id) || ids.has(value.id)) fail('SLA_INVALID_REQUEST', `${path}.id must be a unique non-empty string`)
|
|
90
|
+
if (!isBinary(value.stl)) fail('SLA_INVALID_REQUEST', `${path}.stl must be an ArrayBuffer or Uint8Array`)
|
|
91
|
+
ids.add(value.id)
|
|
92
|
+
matrixOf(value.transform, `${path}.transform`)
|
|
93
|
+
optionalRecord(value.settings, `${path}.settings`)
|
|
94
|
+
if (value.instances !== undefined) {
|
|
95
|
+
if (!Array.isArray(value.instances) || value.instances.length === 0) fail('SLA_INVALID_REQUEST', `${path}.instances must be a non-empty array when present`)
|
|
96
|
+
const instanceIds = new Set()
|
|
97
|
+
value.instances.forEach((instance, i) => {
|
|
98
|
+
instanceOf(instance, `${path}.instances[${i}]`)
|
|
99
|
+
if (instanceIds.has(instance.id)) fail('SLA_INVALID_REQUEST', `${path}.instances ids must be unique`)
|
|
100
|
+
instanceIds.add(instance.id)
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
if (value.supportPoints !== undefined) {
|
|
104
|
+
if (!Array.isArray(value.supportPoints)) fail('SLA_INVALID_REQUEST', `${path}.supportPoints must be an array`)
|
|
105
|
+
value.supportPoints.forEach((point, i) => pointOf(point, `${path}.supportPoints[${i}]`))
|
|
106
|
+
}
|
|
107
|
+
if (value.modifierVolumes !== undefined) {
|
|
108
|
+
if (!Array.isArray(value.modifierVolumes)) fail('SLA_INVALID_REQUEST', `${path}.modifierVolumes must be an array`)
|
|
109
|
+
value.modifierVolumes.forEach((modifier, i) => modifierOf(modifier, `${path}.modifierVolumes[${i}]`))
|
|
110
|
+
}
|
|
111
|
+
if (value.drainHoles !== undefined) {
|
|
112
|
+
if (!Array.isArray(value.drainHoles)) fail('SLA_INVALID_REQUEST', `${path}.drainHoles must be an array`)
|
|
113
|
+
value.drainHoles.forEach((hole, i) => drainHoleOf(hole, `${path}.drainHoles[${i}]`))
|
|
114
|
+
}
|
|
115
|
+
return value
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const supportOf = (value) => {
|
|
119
|
+
const support = optionalRecord(value, 'support')
|
|
120
|
+
if (!support) return undefined
|
|
121
|
+
if (has(support, 'enabled') && typeof support.enabled !== 'boolean') fail('SLA_INVALID_REQUEST', 'support.enabled must be boolean')
|
|
122
|
+
if (has(support, 'enforcersOnly') && typeof support.enforcersOnly !== 'boolean') fail('SLA_INVALID_REQUEST', 'support.enforcersOnly must be boolean')
|
|
123
|
+
if (has(support, 'strategy') && !['default', 'branching', 'organic'].includes(support.strategy))
|
|
124
|
+
fail('SLA_INVALID_REQUEST', "support.strategy must be 'default', 'branching', or 'organic'")
|
|
125
|
+
if (support.strategy === 'organic') fail('SLA_UNSUPPORTED_ORGANIC', 'Organic SLA supports are not available')
|
|
126
|
+
return support
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const hollowingOf = (value) => {
|
|
130
|
+
const hollowing = optionalRecord(value, 'hollowing')
|
|
131
|
+
if (!hollowing) return undefined
|
|
132
|
+
if (has(hollowing, 'enabled') && typeof hollowing.enabled !== 'boolean') fail('SLA_INVALID_REQUEST', 'hollowing.enabled must be boolean')
|
|
133
|
+
if (hollowing.enabled) fail('SLA_UNSUPPORTED_HOLLOWING', 'SLA hollowing is not available')
|
|
134
|
+
return hollowing
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function parseSlaJob(value) {
|
|
138
|
+
if (!isRecord(value)) fail('SLA_INVALID_REQUEST', 'SLA job must be an object')
|
|
139
|
+
if (has(value, 'version') && value.version !== SLA_JOB_VERSION) fail('SLA_INVALID_REQUEST', `SLA job version must be ${SLA_JOB_VERSION}`)
|
|
140
|
+
if (!Array.isArray(value.objects) || value.objects.length === 0) fail('SLA_INVALID_REQUEST', 'SLA job.objects must be a non-empty array')
|
|
141
|
+
const ids = new Set()
|
|
142
|
+
value.objects.forEach((object, index) => objectOf(object, index, ids))
|
|
143
|
+
if (value.objects.some(object => object.drainHoles?.length))
|
|
144
|
+
fail('SLA_UNSUPPORTED_HOLLOWING', 'SLA drain holes were preserved but require unsupported hollowing geometry')
|
|
145
|
+
optionalRecord(value.settings, 'settings')
|
|
146
|
+
const support = supportOf(value.support)
|
|
147
|
+
const pad = optionalRecord(value.pad, 'pad')
|
|
148
|
+
const correction = optionalRecord(value.correction, 'correction')
|
|
149
|
+
const hollowing = hollowingOf(value.hollowing)
|
|
150
|
+
if (value.drainHoles !== undefined) {
|
|
151
|
+
if (!Array.isArray(value.drainHoles)) fail('SLA_INVALID_REQUEST', 'drainHoles must be an array')
|
|
152
|
+
if (value.drainHoles.length) fail('SLA_UNSUPPORTED_HOLLOWING', 'SLA drain holes require unsupported hollowing geometry')
|
|
153
|
+
}
|
|
154
|
+
if (value.requiredCapabilities !== undefined) {
|
|
155
|
+
if (!Array.isArray(value.requiredCapabilities) || value.requiredCapabilities.some(key => !has(SLA_CAPABILITIES, key)))
|
|
156
|
+
fail('SLA_INVALID_REQUEST', 'requiredCapabilities must name known SLA capabilities')
|
|
157
|
+
if (value.requiredCapabilities.includes('organicSupport')) fail('SLA_UNSUPPORTED_ORGANIC', 'Organic SLA supports are not available')
|
|
158
|
+
if (value.requiredCapabilities.includes('hollowing') || value.requiredCapabilities.includes('drainHoles'))
|
|
159
|
+
fail('SLA_UNSUPPORTED_HOLLOWING', 'SLA hollowing is not available')
|
|
160
|
+
}
|
|
161
|
+
return { version: SLA_JOB_VERSION, ...value, support, pad, correction, hollowing }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const wantsSupports = (job, object) => job.support?.enabled === true || job.settings?.supports_enable === true || object.settings?.supports_enable === true
|
|
165
|
+
const wantsPad = (job, object) => job.pad?.enabled === true || job.settings?.pad_enable === true || object.settings?.pad_enable === true
|
|
166
|
+
|
|
167
|
+
export function slaFallbackParams(job) {
|
|
168
|
+
if (job.objects.length !== 1) fail('SLA_FALLBACK_MULTI_OBJECT', 'The JS SLA fallback accepts exactly one object; object-aware jobs require WASM support')
|
|
169
|
+
const object = job.objects[0]
|
|
170
|
+
if (!isIdentity(object.transform) || object.instances !== undefined || object.supportPoints?.length || object.modifierVolumes?.length || object.settings?.hollowing_enable === true)
|
|
171
|
+
fail('SLA_FALLBACK_OBJECT_CONTEXT', 'The JS SLA fallback cannot preserve object transforms, instances, points, modifiers, or hollowing')
|
|
172
|
+
if (wantsSupports(job, object)) fail('SLA_FALLBACK_SUPPORTS', 'The JS SLA fallback cannot generate requested supports')
|
|
173
|
+
if (wantsPad(job, object)) fail('SLA_FALLBACK_PAD', 'The JS SLA fallback cannot generate a requested pad')
|
|
174
|
+
if (job.correction || job.hollowing || job.drainHoles?.length)
|
|
175
|
+
fail('SLA_FALLBACK_OBJECT_CONTEXT', 'The JS SLA fallback cannot apply correction, hollowing, or drain-hole context')
|
|
176
|
+
return { stl: object.stl, params: { ...(job.settings ?? {}), ...(object.settings ?? {}) } }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function assertLegacySlaFallback(params) {
|
|
180
|
+
if (!isRecord(params)) fail('SLA_INVALID_REQUEST', 'legacy SLA params must be an object')
|
|
181
|
+
if (params.support_tree_type === 'organic') fail('SLA_UNSUPPORTED_ORGANIC', 'Organic SLA supports are not available')
|
|
182
|
+
if (params.hollowing_enable === true) fail('SLA_UNSUPPORTED_HOLLOWING', 'SLA hollowing is not available')
|
|
183
|
+
if (params.supports_enable === true) fail('SLA_FALLBACK_SUPPORTS', 'The JS SLA fallback cannot generate requested supports')
|
|
184
|
+
if (params.pad_enable === true) fail('SLA_FALLBACK_PAD', 'The JS SLA fallback cannot generate a requested pad')
|
|
185
|
+
}
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
// A host embedding the viewer in its own product can switch it off (`<Viewport features={{ logs: false }} />`,
|
|
16
16
|
// which sets `quiet` on the messages it sends): this is a separate module instance from the viewer's log.js and
|
|
17
17
|
// cannot read a flag set over there.
|
|
18
|
+
import { assertLegacySlaFallback, parseSlaJob } from './sla_request.js'
|
|
19
|
+
|
|
18
20
|
let quiet = false
|
|
19
21
|
const say = (level, ...args) => { if (!quiet) console[level](...args) }
|
|
20
22
|
|
|
@@ -125,6 +127,66 @@ self.onmessage = async (e) => {
|
|
|
125
127
|
if (d.cmd === 'exportPaint' && !modPromise) {
|
|
126
128
|
self.postMessage({ type: 'paintExport', supported: false, facets: [], hex: '' }); return
|
|
127
129
|
}
|
|
130
|
+
if (d.cmd === 'slaJob') {
|
|
131
|
+
const job = parseSlaJob(d.job)
|
|
132
|
+
if (!modPromise) modPromise = loadCore()
|
|
133
|
+
const Kernel = await modPromise
|
|
134
|
+
const onProgress = (done, total) => self.postMessage({ type: 'progress', done, total })
|
|
135
|
+
if (Kernel.slice_sla_job) {
|
|
136
|
+
Kernel.set_layer_sink((z, idx, gcode, paths, widths) => {
|
|
137
|
+
const transfer = []
|
|
138
|
+
if (paths?.buffer) transfer.push(paths.buffer)
|
|
139
|
+
if (widths?.buffer) transfer.push(widths.buffer)
|
|
140
|
+
self.postMessage({ type: 'layer', z, idx, gcode: '', paths, widths }, transfer)
|
|
141
|
+
})
|
|
142
|
+
let r
|
|
143
|
+
try { r = Kernel.slice_sla_job(job, onProgress) }
|
|
144
|
+
finally { Kernel.clear_layer_sink() }
|
|
145
|
+
if (r?.error) { self.postMessage({ type: 'error', error: String(r.error) }); return }
|
|
146
|
+
self.postMessage({ type: 'done', result: r }); return
|
|
147
|
+
}
|
|
148
|
+
self.postMessage({ type: 'error', code: 'SLA_UNSUPPORTED_OBJECT_AWARE',
|
|
149
|
+
error: 'Object-aware SLA slicing requires WASM kernel support' })
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
// SLA slicing runs in the WASM kernel (slice_sla — contours + generated supports, C++ Clipper speed), through
|
|
153
|
+
// the same layer-sink streaming a kernel slice uses. The pure-JS contour slicer (sla_core.js) stays as the
|
|
154
|
+
// fallback for a loaded kernel that predates the binding — it produces the same stream, minus the supports.
|
|
155
|
+
if (d.cmd === 'sla') {
|
|
156
|
+
const params = typeof d.params === 'string' ? d.params : JSON.stringify(d.params ?? {})
|
|
157
|
+
if (!modPromise) modPromise = loadCore()
|
|
158
|
+
const Kernel = await modPromise
|
|
159
|
+
const onProgress = (done, total) => self.postMessage({ type: 'progress', done, total })
|
|
160
|
+
if (Kernel.slice_sla) {
|
|
161
|
+
Kernel.set_layer_sink((z, idx, gcode, paths, widths) => {
|
|
162
|
+
const transfer = []
|
|
163
|
+
if (paths && paths.buffer) transfer.push(paths.buffer)
|
|
164
|
+
if (widths && widths.buffer) transfer.push(widths.buffer)
|
|
165
|
+
self.postMessage({ type: 'layer', z, idx, gcode: '', paths, widths }, transfer)
|
|
166
|
+
})
|
|
167
|
+
let r
|
|
168
|
+
try { r = Kernel.slice_sla(new Uint8Array(d.stl), params, onProgress) }
|
|
169
|
+
finally { Kernel.clear_layer_sink() }
|
|
170
|
+
if (r && r.error) { self.postMessage({ type: 'error', error: String(r.error) }); return }
|
|
171
|
+
self.postMessage({ type: 'done', result: r })
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
const { sliceSla } = await import('./sla_core.js')
|
|
175
|
+
const fallbackParams = JSON.parse(params)
|
|
176
|
+
assertLegacySlaFallback(fallbackParams)
|
|
177
|
+
const r = sliceSla(new Uint8Array(d.stl), fallbackParams, {
|
|
178
|
+
onProgress,
|
|
179
|
+
onLayer: (L) => {
|
|
180
|
+
const transfer = []
|
|
181
|
+
if (L.paths?.buffer) transfer.push(L.paths.buffer)
|
|
182
|
+
if (L.widths?.buffer) transfer.push(L.widths.buffer)
|
|
183
|
+
self.postMessage({ type: 'layer', z: L.z, idx: L.idx, gcode: '', paths: L.paths, widths: L.widths }, transfer)
|
|
184
|
+
},
|
|
185
|
+
})
|
|
186
|
+
if (r && r.error) { self.postMessage({ type: 'error', error: String(r.error) }); return }
|
|
187
|
+
self.postMessage({ type: 'done', result: r })
|
|
188
|
+
return
|
|
189
|
+
}
|
|
128
190
|
if (!modPromise) modPromise = loadCore()
|
|
129
191
|
const Module = await modPromise
|
|
130
192
|
// Warmup: only load the kernel (+ spawn the mt pthread pool) ahead of time — removes the perceived load on the first slice
|
|
@@ -247,6 +309,6 @@ self.onmessage = async (e) => {
|
|
|
247
309
|
self.postMessage({ type: 'done', result: r })
|
|
248
310
|
} catch (err) {
|
|
249
311
|
// Includes the WASM abort("memory access out of bounds") — the main thread's OOM ladder decides on re-creation / economy retry.
|
|
250
|
-
self.postMessage({ type: 'error', error: String((err && err.message) || err) })
|
|
312
|
+
self.postMessage({ type: 'error', error: String((err && err.message) || err), ...(err?.code ? { code: err.code } : {}) })
|
|
251
313
|
}
|
|
252
314
|
}
|
|
Binary file
|
|
Binary file
|