three-slicer 0.1.4 → 0.1.6
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/README.md +4 -1
- package/components/README.md +2 -2
- package/data/README.md +2 -2
- package/engine/README.md +2 -2
- package/engine/src/settings.js +126 -3
- package/engine/src/slicer.worker.js +29 -0
- package/engine/src/slicer_core.js +0 -0
- package/engine/src/slicer_core.mt.js +0 -0
- package/package.json +1 -1
- package/types/settings.d.ts +20 -1
- package/viewer/README.md +7 -2
- package/viewer/dist/Viewport.js +3239 -2124
- package/viewer/dist/make_worker.js +7 -0
- package/viewer/dist/model_loaders.js +139 -211
- package/viewer/dist/parse_3mf-8oCnCJNK.js +216 -0
- package/viewer/dist/parse_3mf.worker.js +10 -0
- package/viewer/styles.css +32 -3
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ Browser/WASM 3D-printing slicer package derived from [OrcaSlicer](https://github
|
|
|
4
4
|
|
|
5
5
|
The package is published as a single npm package, `three-slicer`, with subpath exports for the engine, viewer, components, worker, and extracted OrcaSlicer data.
|
|
6
6
|
|
|
7
|
+

|
|
8
|
+
|
|
7
9
|
## Links
|
|
8
10
|
|
|
9
11
|
- Package: [npmjs.com/package/three-slicer](https://www.npmjs.com/package/three-slicer)
|
|
@@ -19,6 +21,7 @@ The package is published as a single npm package, `three-slicer`, with subpath e
|
|
|
19
21
|
- React `<SettingsPanel/>` generated from extracted OrcaSlicer metadata.
|
|
20
22
|
- Vendor printer, print-process and filament preset catalogs, with one material assignable per extruder.
|
|
21
23
|
- Support painting and material painting, plus per-tool filament and purge statistics.
|
|
24
|
+
- 3MF **project** import in the viewer: a slicer-written `.3mf` (OrcaSlicer/BambuStudio save, MakerWorld download) restores its plate layout, project settings, and painted facets — not just the meshes.
|
|
22
25
|
- Extracted data files for custom UIs: config schema, UI tree, toggle rules, invalidation map, and the printer/process/filament catalogs.
|
|
23
26
|
- Automatic multithreaded WASM selection on cross-origin-isolated browser pages.
|
|
24
27
|
|
|
@@ -107,7 +110,7 @@ export default function App() {
|
|
|
107
110
|
}
|
|
108
111
|
```
|
|
109
112
|
|
|
110
|
-
The viewer handles model loading, drag and drop, transform controls, multi-plate layout, worker slicing, GPU toolpath preview, and G-code export. It supports STL, OBJ, 3MF (including the production extension used by Orca/Bambu/Prusa), AMF, and PLY out of the box; other formats such as STEP can be added with `registerLoader()`. Viewer and component styles are bundled into their Shadow DOM roots, so host app CSS does not need to import package CSS.
|
|
113
|
+
The viewer handles model loading, drag and drop, transform controls, multi-plate layout, worker slicing, GPU toolpath preview, and G-code export. It supports STL, OBJ, 3MF (including the production extension used by Orca/Bambu/Prusa), AMF, and PLY out of the box; other formats such as STEP can be added with `registerLoader()`. A `.3mf` written by a slicer is treated as a project, not just geometry: its plate layout, project settings, and support/material painting are restored on import (where a facet carries both paint kinds, material paint wins and the dropped support paint is reported). Viewer and component styles are bundled into their Shadow DOM roots, so host app CSS does not need to import package CSS.
|
|
111
114
|
|
|
112
115
|
## Quick Start: Settings Panel
|
|
113
116
|
|
package/components/README.md
CHANGED
|
@@ -4,10 +4,10 @@ Reusable React components for the browser slicer. Props-driven — no global sta
|
|
|
4
4
|
|
|
5
5
|
## `<SettingsPanel/>`
|
|
6
6
|
|
|
7
|
-
An OrcaSlicer-style settings form generated from
|
|
7
|
+
An OrcaSlicer-style settings form generated from `three-slicer/data` (schema + UI tree + toggle rules): tab/page/group navigation, mode filter (simple/advanced/expert), search across 923 options, dirty markers with per-option reset, and enable/disable rules evaluation.
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm i three-slicer
|
|
10
|
+
npm i three-slicer # react is a peer dependency — npm installs it for you
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
```jsx
|
package/data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# three-slicer/data
|
|
2
2
|
|
|
3
|
-
OrcaSlicer configuration metadata extracted from the original C++ sources, as
|
|
3
|
+
OrcaSlicer configuration metadata extracted from the original C++ sources, shipped inside the `three-slicer` package as its `/data` subpath. Used by `three-slicer` (defaults, settings mapping) and `three-slicer/components` (settings UI), and usable standalone for building slicer UIs.
|
|
4
4
|
|
|
5
5
|
| File | Contents |
|
|
6
6
|
|---|---|
|
package/engine/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Browser/WASM 3D-slicing kernel reverse-engineered from [OrcaSlicer](https://github.com/OrcaSlicer/OrcaSlicer). Slices binary STL to G-code entirely client-side (or in Node) — no server, no native binaries.
|
|
4
4
|
|
|
5
|
-
**No three.js dependency.** This is a headless SDK; the
|
|
5
|
+
**No three.js dependency.** This is a headless SDK with zero runtime dependencies; the config metadata it reads ships inside the same package (`three-slicer/data`). For a ready-made React viewer see `three-slicer/viewer`.
|
|
6
6
|
|
|
7
7
|
Ported from the original C++ sources: Clipper polygon ops, Arachne variable-width walls, real fill patterns (gyroid TPMS / honeycomb / 3D-honeycomb / crosshatch / concentric), tree supports, pressure equalizer, arc fitting (G2/G3), scarf seams, ironing, multi-material with prime tower. Kernel changes are gated by a 120+ invariant test suite and golden byte-identical G-code checks.
|
|
8
8
|
|
|
@@ -38,7 +38,7 @@ Off-main-thread (browser): `new Worker(engineWorkerURL(), { type: 'module' })`
|
|
|
38
38
|
Two levels — use whichever fits:
|
|
39
39
|
|
|
40
40
|
1. **Kernel params (this package's native contract)** — a flat JSON object of ~90 keys (`layer_height`, `wall_loops`, `infill_density` 0–1, `sparse_infill_pattern`, `enable_support`, `wall_generator: 'classic' | 'arachne'`, …). See `src/settings.js` for the full shape.
|
|
41
|
-
2. **OrcaSlicer settings map** — a sparse `{schemaKey: value}` map using original OrcaSlicer option keys (923 defined in
|
|
41
|
+
2. **OrcaSlicer settings map** — a sparse `{schemaKey: value}` map using original OrcaSlicer option keys (923 defined in `three-slicer/data`); unset keys fall back to schema defaults:
|
|
42
42
|
|
|
43
43
|
```js
|
|
44
44
|
import { deriveKernelParams } from 'three-slicer/settings'
|
package/engine/src/settings.js
CHANGED
|
@@ -136,9 +136,128 @@ export function printerSettings(profileName) {
|
|
|
136
136
|
/** The vendor's recommended process preset for a printer, or '' when the profile names none. */
|
|
137
137
|
export function printerDefaultPreset(profileName) { return printerEntry(profileName)?.[3] ?? '' }
|
|
138
138
|
|
|
139
|
+
// ---- Imported project settings (a slicer-written 3mf's Metadata/project_settings.config) --------------------
|
|
140
|
+
// Upstream serializes every option as a STRING (or an array of them, one per extruder) — a bool is "0"/"1" and a
|
|
141
|
+
// float is "0.2". This settings map holds real JS types, and the difference is not cosmetic: `bool()` below reads
|
|
142
|
+
// a value with `!!v`, and `!!"0"` is TRUE — importing raw would turn every disabled option on. So each value is
|
|
143
|
+
// coerced by its schema type, and anything the schema does not know (inherits_group, *_settings_id, version, the
|
|
144
|
+
// whole preset-bookkeeping half of that file) is dropped rather than carried into a map keyed by schema keys.
|
|
145
|
+
// The `inherits` / `different_settings_to_system` machinery upstream applies on top of this (Preset.cpp:2577) is
|
|
146
|
+
// deliberately NOT reproduced: it exists to reconcile a stored preset against a LOCAL vendor preset database of a
|
|
147
|
+
// possibly different version. project_settings.config is already flattened, so taking its values as written is
|
|
148
|
+
// both simpler and closer to what the project author actually sliced.
|
|
149
|
+
const FALSE_STRINGS = new Set(['0', 'false', 'no', 'off', ''])
|
|
150
|
+
|
|
151
|
+
function coerceScalar(type, value) {
|
|
152
|
+
if (typeof value !== 'string') return value
|
|
153
|
+
switch (type) {
|
|
154
|
+
case 'coBool': case 'coBools':
|
|
155
|
+
return !FALSE_STRINGS.has(value.trim().toLowerCase())
|
|
156
|
+
case 'coFloat': case 'coFloats': case 'coInt': case 'coInts':
|
|
157
|
+
return asNumber(value)
|
|
158
|
+
case 'coPercent': case 'coPercents':
|
|
159
|
+
// A percent option's value is the bare number (the schema default of sparse_infill_density is 20, not "20%")
|
|
160
|
+
// and upstream serializes it that way — but the '%' does show up in hand-edited configs, so strip it.
|
|
161
|
+
return asNumber(value.trim().replace(/%$/, ''))
|
|
162
|
+
case 'coFloatOrPercent': case 'coFloatsOrPercents':
|
|
163
|
+
// "50%" must stay a string — the percent IS the value, and Number('50%') is NaN anyway.
|
|
164
|
+
return value.trim().endsWith('%') ? value : asNumber(value)
|
|
165
|
+
case 'coPoint': case 'coPoints':
|
|
166
|
+
return parsePoint(value) ?? value
|
|
167
|
+
case 'coPointsGroups':
|
|
168
|
+
return parsePointGroup(value) ?? value
|
|
169
|
+
default:
|
|
170
|
+
return value // strings and enums — the kernel and the panel both read these as written
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Points are the other shape trap, and a quiet one: upstream writes a point as the STRING "XxY", while every
|
|
175
|
+
// consumer here indexes it as a [x, y] pair — so `printable_area[1][0]` on a raw import is the CHARACTER '2' of
|
|
176
|
+
// "256x0", and the bed comes out 2mm wide (measured on a real MakerWorld project: 2 x NaN).
|
|
177
|
+
// Two separators exist in the wild: ConfigOptionPoint::serialize writes 'x', while some Orca options
|
|
178
|
+
// (best_object_pos) write "X,Y". A points GROUP is one string holding a comma-separated LIST of "XxY" points,
|
|
179
|
+
// so there the comma cannot also separate a point's own coordinates — hence two parsers rather than one regex.
|
|
180
|
+
function parsePoint(text) {
|
|
181
|
+
const split = String(text).split('x')
|
|
182
|
+
const pair = split.length === 2 ? split : String(text).split(',')
|
|
183
|
+
if (pair.length !== 2) return null
|
|
184
|
+
const x = Number(pair[0]), y = Number(pair[1])
|
|
185
|
+
return Number.isFinite(x) && Number.isFinite(y) ? [x, y] : null
|
|
186
|
+
}
|
|
187
|
+
function parsePointGroup(text) {
|
|
188
|
+
const points = String(text).split(',').map(entry => {
|
|
189
|
+
const [a, b] = entry.split('x')
|
|
190
|
+
const x = Number(a), y = Number(b)
|
|
191
|
+
return Number.isFinite(x) && Number.isFinite(y) ? [x, y] : null
|
|
192
|
+
})
|
|
193
|
+
return points.length && points.every(Boolean) ? points : null
|
|
194
|
+
}
|
|
195
|
+
// Number('') is 0 and Number(' ') is 0, neither of which means "the option was set to zero" — an unparsable
|
|
196
|
+
// value is handed back untouched so the schema default keeps applying instead of a fabricated 0.
|
|
197
|
+
function asNumber(value) {
|
|
198
|
+
if (value.trim() === '') return value
|
|
199
|
+
const n = Number(value)
|
|
200
|
+
return Number.isFinite(n) ? n : value
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* A 3mf project's raw `project_settings.config` object -> a settings map this package's own API accepts.
|
|
205
|
+
* Keys the config schema does not define are dropped. Returns `{settings, applied, skipped}`.
|
|
206
|
+
*/
|
|
207
|
+
export function normalizeProjectSettings(raw) {
|
|
208
|
+
const settings = {}
|
|
209
|
+
let applied = 0
|
|
210
|
+
const skipped = []
|
|
211
|
+
for (const [key, value] of Object.entries(raw || {})) {
|
|
212
|
+
const type = schema[key]?.type
|
|
213
|
+
if (!type) { skipped.push(key); continue }
|
|
214
|
+
settings[key] = Array.isArray(value) ? value.map(entry => coerceScalar(type, entry)) : coerceScalar(type, value)
|
|
215
|
+
applied++
|
|
216
|
+
}
|
|
217
|
+
return { settings, applied, skipped }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// The exact inverse, for WRITING a project_settings.config: every value back to the string shape upstream
|
|
221
|
+
// serializes (bool -> "1"/"0", point -> "XxY", a points group -> "X1xY1,X2xY2"), because that is what every other
|
|
222
|
+
// slicer's reader — and normalizeProjectSettings above — expects to coerce FROM. Writing the JS types raw would
|
|
223
|
+
// hand OrcaSlicer a bool it reads with its own string parser and this package a round-trip that only works here.
|
|
224
|
+
// Keys the schema does not define are dropped for the same reason normalize drops them: they could not be typed.
|
|
225
|
+
const pointToText = (point) => (Array.isArray(point) ? `${point[0]}x${point[1]}` : String(point))
|
|
226
|
+
|
|
227
|
+
function serializeScalar(type, value) {
|
|
228
|
+
if (value == null) return ''
|
|
229
|
+
switch (type) {
|
|
230
|
+
case 'coBool': case 'coBools':
|
|
231
|
+
return value ? '1' : '0'
|
|
232
|
+
case 'coPoint': case 'coPoints':
|
|
233
|
+
return pointToText(value)
|
|
234
|
+
case 'coPointsGroups':
|
|
235
|
+
return Array.isArray(value) ? value.map(pointToText).join(',') : String(value)
|
|
236
|
+
default:
|
|
237
|
+
return String(value) // numbers, percents, strings, enums — String() is upstream's own spelling for all of them
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** A settings map (real JS types) -> the all-strings object a 3mf's `project_settings.config` stores. */
|
|
242
|
+
export function serializeProjectSettings(settings) {
|
|
243
|
+
const out = {}
|
|
244
|
+
for (const [key, value] of Object.entries(settings || {})) {
|
|
245
|
+
const type = schema[key]?.type
|
|
246
|
+
if (!type) continue
|
|
247
|
+
// A coPoint's value IS an array ([x, y]) — the generic array path would serialize its two numbers separately.
|
|
248
|
+
out[key] = (type !== 'coPoint' && Array.isArray(value))
|
|
249
|
+
? value.map(entry => serializeScalar(type, entry))
|
|
250
|
+
: serializeScalar(type, value)
|
|
251
|
+
}
|
|
252
|
+
return out
|
|
253
|
+
}
|
|
254
|
+
|
|
139
255
|
|
|
140
256
|
// Right-panel settings values -> kernel parameters (derived from schema keys)
|
|
141
|
-
|
|
257
|
+
// opts.plate: which plate's entry to take from per-plate array options (wipe_tower_x/y — upstream coFloats,
|
|
258
|
+
// one entry per plate). Defaults to 0, so every existing caller reads exactly what it always did.
|
|
259
|
+
export function deriveKernelParams(settings, opts) {
|
|
260
|
+
const plate = opts?.plate ?? 0
|
|
142
261
|
const S = k => settingScalar(settings, k)
|
|
143
262
|
const num = (k, d) => { const v = Number(S(k)); return Number.isFinite(v) ? v : d }
|
|
144
263
|
const bool = (k, d) => { const v = settingRaw(settings, k); return typeof v === 'boolean' ? v : (Array.isArray(v) ? !!v[0] : (v == null ? d : !!v)) }
|
|
@@ -202,7 +321,9 @@ export function deriveKernelParams(settings) {
|
|
|
202
321
|
// A hole (a material that overrides nothing) is filled with the value tool 0 resolved to, because the kernel
|
|
203
322
|
// reads the array positionally and cannot tell "absent" from "0".
|
|
204
323
|
// Prime tower keys, omitted when unset so the kernel/viewer defaults stay in charge (same rule as supportTools).
|
|
205
|
-
// wipe_tower_x/y are coFloats upstream — one entry per plate —
|
|
324
|
+
// wipe_tower_x/y are coFloats upstream — one entry per plate — indexed by opts.plate. A hole (null entry) means
|
|
325
|
+
// nobody chose a position for THAT plate, so the automatic placement stands there; a scalar applies to every
|
|
326
|
+
// plate (the pre-array form, kept readable so old saved settings keep meaning what they meant).
|
|
206
327
|
const towerSettings = {}
|
|
207
328
|
{
|
|
208
329
|
const width = num('prime_tower_width', 0)
|
|
@@ -211,7 +332,9 @@ export function deriveKernelParams(settings) {
|
|
|
211
332
|
// a coordinate off the front of a 200mm bed. Taking it as "the user chose a position" put the tower outside
|
|
212
333
|
// the plate and disabled the automatic placement entirely — measured as a tower at z=-127.5 on a 200mm bed.
|
|
213
334
|
// Absent from the map means nobody chose one, and the placement beside the model stands.
|
|
214
|
-
const chosen = (key) => { const
|
|
335
|
+
const chosen = (key) => { const raw = settings?.[key]
|
|
336
|
+
const v = Array.isArray(raw) ? raw[plate] : raw
|
|
337
|
+
const n = Number(v)
|
|
215
338
|
return (v != null && v !== '' && Number.isFinite(n)) ? n : null }
|
|
216
339
|
const towerX = chosen('wipe_tower_x'), towerY = chosen('wipe_tower_y')
|
|
217
340
|
if (towerX != null) towerSettings.prime_tower_x = towerX
|
|
@@ -109,6 +109,12 @@ const paintedReply = (Module, message, paintedState) => {
|
|
|
109
109
|
self.onmessage = async (e) => {
|
|
110
110
|
const d = e.data
|
|
111
111
|
try {
|
|
112
|
+
// Asking for the painting must not be what LOADS the kernel. Every other command needs it, but if this worker
|
|
113
|
+
// has never loaded it then selector_prepare has never run either, so there is no painting to report — and
|
|
114
|
+
// making a save wait on a 4MB WASM load before the first slice is seconds of nothing, for an empty answer.
|
|
115
|
+
if (d.cmd === 'exportPaint' && !modPromise) {
|
|
116
|
+
self.postMessage({ type: 'paintExport', supported: false, facets: [], hex: '' }); return
|
|
117
|
+
}
|
|
112
118
|
if (!modPromise) modPromise = loadCore()
|
|
113
119
|
const Module = await modPromise
|
|
114
120
|
// Warmup: only load the kernel (+ spawn the mt pthread pool) ahead of time — removes the perceived load on the first slice
|
|
@@ -148,6 +154,29 @@ self.onmessage = async (e) => {
|
|
|
148
154
|
else Module.selector_erase(d.facet, d.hx, d.hy, d.hz, d.cx, d.cy, d.cz, d.radius)
|
|
149
155
|
self.postMessage(paintedReply(Module, d, null)); return
|
|
150
156
|
}
|
|
157
|
+
// importPaint: load painting out of a 3mf. `facets` is an Int32Array of facet indices in the CURRENT selector's
|
|
158
|
+
// numbering and `hex` the newline-joined per-facet split-tree strings the 3mf carried, in the same order.
|
|
159
|
+
// It REPLACES every mark (the kernel's deserialize resets first), so it belongs immediately after a 'prepare'
|
|
160
|
+
// and nowhere else — an import in the middle of a session would silently discard what the user had painted.
|
|
161
|
+
// A kernel built before this binding existed simply has no selector_import_paint; the reply says applied:0
|
|
162
|
+
// rather than throwing, because losing a 3mf's paint must not also lose its geometry.
|
|
163
|
+
if (d.cmd === 'importPaint') {
|
|
164
|
+
const applied = Module.selector_import_paint ? Module.selector_import_paint(d.facets, d.hex ?? '') : 0
|
|
165
|
+
const reply = paintedReply(Module, d, null)
|
|
166
|
+
reply.applied = applied
|
|
167
|
+
self.postMessage(reply); return
|
|
168
|
+
}
|
|
169
|
+
// exportPaint: the reverse of importPaint — every marked facet's split tree as {facets, hex} in the CURRENT
|
|
170
|
+
// selector's numbering, the same parallel-array pairing importPaint takes (hex is one newline-joined blob).
|
|
171
|
+
// The caller (3mf save) rebases the facet indices back onto per-object numbering; this worker cannot, because
|
|
172
|
+
// which objects were merged in what order is the viewer's knowledge. A kernel built before the binding existed
|
|
173
|
+
// reports supported:false rather than throwing — the save then falls back to the paint it imported.
|
|
174
|
+
if (d.cmd === 'exportPaint') {
|
|
175
|
+
const exported = Module.selector_export_paint ? Module.selector_export_paint() : null
|
|
176
|
+
self.postMessage({ type: 'paintExport', supported: !!Module.selector_export_paint,
|
|
177
|
+
facets: exported?.facets ?? [], hex: exported?.hex ?? '' })
|
|
178
|
+
return
|
|
179
|
+
}
|
|
151
180
|
// clear wipes every state at once, so each requested count is 0 by construction — no need to ask the kernel back.
|
|
152
181
|
if (d.cmd === 'clear') {
|
|
153
182
|
Module.selector_clear()
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "three-slicer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Browser/WASM 3D-printing slicer reverse-engineered from OrcaSlicer: slicing engine (Arachne walls, tree/grid supports, multi-material), React viewer with GPU toolpath preview, and schema-driven settings UI.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
package/types/settings.d.ts
CHANGED
|
@@ -16,7 +16,26 @@ export function settingScalar(settings: SlicerSettings | null | undefined, key:
|
|
|
16
16
|
* ponytail: 53 returned keys, so Record instead of listing each — kernel params are passed straight to slice(),
|
|
17
17
|
* not an API consumers read field by field. Expand it when field access is actually needed.
|
|
18
18
|
*/
|
|
19
|
-
export function deriveKernelParams(settings: SlicerSettings | null | undefined): Record<string, unknown>
|
|
19
|
+
export function deriveKernelParams(settings: SlicerSettings | null | undefined, opts?: { plate?: number }): Record<string, unknown>
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A 3mf project's raw `Metadata/project_settings.config` -> a settings map this API accepts.
|
|
23
|
+
* Upstream writes every option as a string ("0"/"1" for a bool, "0.2" for a float, one entry per extruder for a
|
|
24
|
+
* vector); each value is coerced by its config-schema type, and keys the schema does not define are dropped into
|
|
25
|
+
* `skipped` rather than carried into the map. `applied` is how many keys survived.
|
|
26
|
+
*/
|
|
27
|
+
export function normalizeProjectSettings(raw: Record<string, unknown> | null | undefined): {
|
|
28
|
+
settings: SlicerSettings
|
|
29
|
+
applied: number
|
|
30
|
+
skipped: string[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The inverse, for WRITING a 3mf's `Metadata/project_settings.config`: every value back to the string shape
|
|
35
|
+
* upstream serializes (a bool as "1"/"0", a point as "XxY", a points group as "X1xY1,X2xY2"), so another slicer
|
|
36
|
+
* reads what it expects. Keys the config schema does not define are dropped.
|
|
37
|
+
*/
|
|
38
|
+
export function serializeProjectSettings(settings: SlicerSettings | null | undefined): Record<string, string | string[]>
|
|
20
39
|
|
|
21
40
|
// ---- Lazily loaded presets --------------------------------------------------
|
|
22
41
|
// Both facades hide the column layout of the artifact they wrap; the promise is created once and reused.
|
package/viewer/README.md
CHANGED
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
3D slicer viewer as a React component: three.js viewport (orbit/transform gizmos), model import (STL/OBJ/3MF/AMF/PLY + pluggable formats such as STEP, multi-object, drag & drop), Web Worker slicing via `three-slicer`, support and material painting, per-extruder filament presets, multi-plate, and a GPU-instanced volumetric toolpath preview ported from OrcaSlicer's libvgcode (millions of segments in a single draw path, per-feature colors, layer range slider, G-code export).
|
|
4
4
|
|
|
5
|
+

|
|
6
|
+
|
|
5
7
|
```bash
|
|
6
|
-
npm i three-slicer
|
|
8
|
+
npm i three-slicer # react, react-dom and three are peer dependencies — npm installs them for you
|
|
7
9
|
```
|
|
8
10
|
|
|
9
11
|
```jsx
|
|
10
12
|
import { useState } from 'react'
|
|
11
13
|
import Viewport from 'three-slicer/viewer'
|
|
12
|
-
import 'three-slicer/viewer/styles.css'
|
|
13
14
|
|
|
14
15
|
function App() {
|
|
15
16
|
const [settings, setSettings] = useState({}) // OrcaSlicer schema keys; sparse — defaults fill the rest
|
|
@@ -17,8 +18,12 @@ function App() {
|
|
|
17
18
|
}
|
|
18
19
|
```
|
|
19
20
|
|
|
21
|
+
Styles are injected into the component's Shadow DOM root — nothing to import, and host CSS cannot collide.
|
|
22
|
+
|
|
20
23
|
Slice parameters are derived from `settings` via `three-slicer`'s schema mapping — pair it with `three-slicer/components`' `<SettingsPanel/>` sharing the same state for a full slicer UI.
|
|
21
24
|
|
|
25
|
+
A `.3mf` written by a slicer (OrcaSlicer/BambuStudio save, MakerWorld download) imports as a **project**: plate layout, project settings, and support/material painting are restored, not just the meshes. Where a facet carries both paint kinds, material paint wins and the dropped support paint is reported.
|
|
26
|
+
|
|
22
27
|
Props: `settings`, `setSettings`, and three optional React-node slots rendered in the right sidebar —
|
|
23
28
|
`processPanel` (the process card), `motionPanel` (folded into the printer card) and `filamentPanel`
|
|
24
29
|
(folded into the filament card, next to the material picker).
|