three-slicer 0.1.4 → 0.1.5
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/engine/src/settings.js +81 -0
- package/engine/src/slicer.worker.js +12 -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 +12 -0
- package/viewer/dist/Viewport.js +2210 -1992
- 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 +6 -0
package/engine/src/settings.js
CHANGED
|
@@ -136,6 +136,87 @@ 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
|
+
|
|
139
220
|
|
|
140
221
|
// Right-panel settings values -> kernel parameters (derived from schema keys)
|
|
141
222
|
export function deriveKernelParams(settings) {
|
|
@@ -148,6 +148,18 @@ self.onmessage = async (e) => {
|
|
|
148
148
|
else Module.selector_erase(d.facet, d.hx, d.hy, d.hz, d.cx, d.cy, d.cz, d.radius)
|
|
149
149
|
self.postMessage(paintedReply(Module, d, null)); return
|
|
150
150
|
}
|
|
151
|
+
// importPaint: load painting out of a 3mf. `facets` is an Int32Array of facet indices in the CURRENT selector's
|
|
152
|
+
// numbering and `hex` the newline-joined per-facet split-tree strings the 3mf carried, in the same order.
|
|
153
|
+
// It REPLACES every mark (the kernel's deserialize resets first), so it belongs immediately after a 'prepare'
|
|
154
|
+
// and nowhere else — an import in the middle of a session would silently discard what the user had painted.
|
|
155
|
+
// A kernel built before this binding existed simply has no selector_import_paint; the reply says applied:0
|
|
156
|
+
// rather than throwing, because losing a 3mf's paint must not also lose its geometry.
|
|
157
|
+
if (d.cmd === 'importPaint') {
|
|
158
|
+
const applied = Module.selector_import_paint ? Module.selector_import_paint(d.facets, d.hex ?? '') : 0
|
|
159
|
+
const reply = paintedReply(Module, d, null)
|
|
160
|
+
reply.applied = applied
|
|
161
|
+
self.postMessage(reply); return
|
|
162
|
+
}
|
|
151
163
|
// clear wipes every state at once, so each requested count is 0 by construction — no need to ask the kernel back.
|
|
152
164
|
if (d.cmd === 'clear') {
|
|
153
165
|
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.5",
|
|
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
|
@@ -18,6 +18,18 @@ export function settingScalar(settings: SlicerSettings | null | undefined, key:
|
|
|
18
18
|
*/
|
|
19
19
|
export function deriveKernelParams(settings: SlicerSettings | null | undefined): Record<string, unknown>
|
|
20
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
|
+
|
|
21
33
|
// ---- Lazily loaded presets --------------------------------------------------
|
|
22
34
|
// Both facades hide the column layout of the artifact they wrap; the promise is created once and reused.
|
|
23
35
|
|