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.
@@ -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.4",
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",
@@ -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