three-slicer-viewer 0.3.0 → 0.3.1

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.
@@ -0,0 +1,144 @@
1
+ // The fallbacks deriveKernelParams / deriveSlaParams use when neither the settings map nor the schema gives a
2
+ // usable value, written once so every reader of the same key agrees (the per-extruder hole fill and the scalar
3
+ // used to carry separate copies of the same literal). Re-exported from `three-slicer-viewer/settings` for callers
4
+ // that need the same numbers, and frozen so an importer cannot change the derivation for everyone else.
5
+ //
6
+ // These are NOT the schema defaults (config-schema.json) and must not be "fixed" to match them: several
7
+ // disagree on purpose, and the golden check pins every value here — changing one reslices every caller's
8
+ // model (AGENTS.md, "Omission is now the general rule"). The viewer keeps its own copies of the few it draws with
9
+ // (core/viewer_defaults.js, so the react-free toolpath/gcode entries do not load the schema); a test pins them equal.
10
+
11
+ // FFF, keyed by the SCHEMA key each reader asks for (an override pair falls back to its machine key).
12
+ export const FFF_FALLBACKS = Object.freeze({
13
+ line_width: 0,
14
+ prime_tower_width: 0,
15
+ flush_multiplier: 0,
16
+ prime_volume: 0,
17
+ nozzle_diameter: 0.4,
18
+ nozzle_temperature: 200,
19
+ filament_diameter: 1.75,
20
+ filament_flow_ratio: 1.0,
21
+ layer_height: 0.2,
22
+ initial_layer_print_height: 0.2,
23
+ wall_loops: 2,
24
+ sparse_infill_density: 20,
25
+ infill_direction: 45,
26
+ top_shell_layers: 4,
27
+ bottom_shell_layers: 3,
28
+ skirt_loops: 1,
29
+ skirt_distance: 2,
30
+ skirt_height: 1,
31
+ brim_width: 0,
32
+ brim_object_gap: 0,
33
+ resolution: 0.01,
34
+ travel_speed: 120,
35
+ initial_layer_speed: 30,
36
+ outer_wall_speed: 60,
37
+ hot_plate_temp: 45,
38
+ printable_height: 0,
39
+ enable_support: false,
40
+ support_threshold_angle: 30,
41
+ support_top_z_distance: 0.2,
42
+ support_bottom_z_distance: 0.2,
43
+ support_object_xy_distance: 0.35,
44
+ support_interface_top_layers: 2,
45
+ support_angle: 0,
46
+ support_interface_spacing: 0.5,
47
+ support_base_pattern_spacing: 2.5,
48
+ support_remove_small_overhang: true,
49
+ bridge_no_support: false,
50
+ support_expansion: 0,
51
+ support_on_build_plate_only: false,
52
+ support_interface_bottom_layers: 0,
53
+ raft_layers: 0,
54
+ raft_expansion: 1.5,
55
+ raft_contact_distance: 0.1,
56
+ fan_max_speed: 100,
57
+ close_fan_the_first_x_layers: 1,
58
+ full_fan_speed_layer: 0,
59
+ slow_down_layer_time: 5,
60
+ enable_arc_fitting: false,
61
+ spiral_mode: false,
62
+ enable_pressure_advance: false,
63
+ pressure_advance: 0.02,
64
+ bridge_speed: 25,
65
+ ironing_spacing: 0.1,
66
+ ironing_flow: 10,
67
+ ironing_speed: 20,
68
+ reduce_crossing_wall: false,
69
+ max_volumetric_extrusion_rate_slope: 0,
70
+ retraction_length: 0.8,
71
+ retraction_speed: 30,
72
+ z_hop: 0.4,
73
+ retraction_minimum_travel: 2,
74
+ sparse_infill_pattern: 'rectilinear',
75
+ seam_position: 'back',
76
+ support_style: 'default',
77
+ support_type: 'normal(auto)',
78
+ support_base_pattern: 'default',
79
+ support_interface_pattern: 'auto',
80
+ seam_slope_type: 'none',
81
+ ironing_type: 'no ironing',
82
+ wall_generator: 'classic',
83
+ })
84
+
85
+ // What line_width 0 ("auto") resolves to.
86
+ export const AUTO_LINE_WIDTH = 0.42
87
+
88
+ // The bed when printable_area is empty or not a point list. It is the bbox of the schema's own printable_area
89
+ // default, so a malformed value and an absent one land on the same bed; it used to be 256, which no other part of
90
+ // the viewer agreed with (every viewer fallback was 200).
91
+ export const BED_FALLBACK = Object.freeze({ width: 200, depth: 200 })
92
+
93
+ // SLA. Its reader treats any non-positive value as absent, unlike the FFF one. `layer_height` is read from the map
94
+ // only (the schema default is FFF's 0.2), so its fallback is the resin value.
95
+ export const SLA_FALLBACKS = Object.freeze({
96
+ layer_height: 0.05,
97
+ exposure_time: 7,
98
+ initial_exposure_time: 35,
99
+ display_width: 120.96,
100
+ display_height: 68.04,
101
+ display_pixels_x: 2560,
102
+ display_pixels_y: 1440,
103
+ support_pillar_diameter: 1.0,
104
+ support_head_front_diameter: 0.4,
105
+ support_head_width: 1.0,
106
+ support_head_penetration: 0.2,
107
+ support_points_density_relative: 100,
108
+ support_critical_angle: 45,
109
+ support_max_bridge_length: 15,
110
+ support_max_pillar_link_distance: 10,
111
+ support_base_diameter: 4,
112
+ support_base_height: 1,
113
+ support_max_bridges_on_pillar: 3,
114
+ support_max_weight_on_model: 10,
115
+ slice_closing_radius: 0.049,
116
+ pad_brim_size: 1.6,
117
+ pad_wall_thickness: 2,
118
+ pad_wall_slope: 90,
119
+ pad_max_merge_distance: 50,
120
+ pad_object_gap: 1,
121
+ pad_object_connector_width: 0.5,
122
+ pad_object_connector_stride: 10,
123
+ pad_object_connector_penetration: 0.3,
124
+ })
125
+
126
+ // Machine limits (the "Motion ability" printer page) -> kernel time-estimate parameters.
127
+ // Declarative on purpose: the kernel collapses X/Y into one axis, so the mapping cannot be a plain pass-through,
128
+ // but adding a limit stays a data row instead of code. The fallback is the kernel's own default, so an unedited
129
+ // profile produces exactly the previous estimate. Upstream leaves machine_max_*_x/y/z/e without a schema default
130
+ // (they only ever come from a printer preset), hence the explicit fallbacks here.
131
+ export const MACHINE_LIMITS = Object.freeze({
132
+ machine_max_speed_xy: Object.freeze(['machine_max_speed_x', 500]),
133
+ machine_max_speed_z: Object.freeze(['machine_max_speed_z', 12]),
134
+ machine_max_speed_e: Object.freeze(['machine_max_speed_e', 30]),
135
+ machine_max_accel_xy: Object.freeze(['machine_max_acceleration_x', 5000]),
136
+ machine_max_accel_z: Object.freeze(['machine_max_acceleration_z', 500]),
137
+ machine_max_accel_e: Object.freeze(['machine_max_acceleration_e', 5000]),
138
+ machine_jerk_xy: Object.freeze(['machine_max_jerk_x', 9]),
139
+ machine_jerk_z: Object.freeze(['machine_max_jerk_z', 0.4]),
140
+ machine_jerk_e: Object.freeze(['machine_max_jerk_e', 2.5]),
141
+ machine_accel_print: Object.freeze(['default_acceleration', 5000]),
142
+ machine_accel_travel: Object.freeze(['travel_acceleration', 5000]),
143
+ machine_accel_retract: Object.freeze(['machine_max_acceleration_retracting', 5000]),
144
+ })
@@ -11,6 +11,7 @@
11
11
  // The REDUCED schema is enough here: this file reads .type, .default, enum_values and, for the SLA
12
12
  // contract, defined_in/line — never a label or a tooltip. Aliased so the call sites read as before.
13
13
  import { leanSchema as schema } from './data.js'
14
+ import { FFF_FALLBACKS, AUTO_LINE_WIDTH, SLA_FALLBACKS, MACHINE_LIMITS, BED_FALLBACK } from './kernel_defaults.js'
14
15
 
15
16
  export function schemaDefault(key) { return schema[key]?.default }
16
17
  export function settingRaw(settings, key) { return (settings && key in settings) ? settings[key] : schemaDefault(key) }
@@ -68,25 +69,6 @@ const PASSTHROUGH_NUM_VECTOR = ['filament_map', 'filament_density', 'filament_co
68
69
  // extra unretract, TPU on the first layer) and the settings id for the footer.
69
70
  const PASSTHROUGH_STR_VECTOR = ['filament_type', 'filament_settings_id']
70
71
 
71
- // Machine limits (the "Motion ability" printer page) -> kernel time-estimate parameters.
72
- // Declarative on purpose: the kernel collapses X/Y into one axis, so the mapping cannot be a plain pass-through,
73
- // but adding a limit stays a data row instead of code. The fallback is the kernel's own default, so an unedited
74
- // profile produces exactly the previous estimate. Upstream leaves machine_max_*_x/y/z/e without a schema default
75
- // (they only ever come from a printer preset), hence the explicit fallbacks here.
76
- const MACHINE_LIMITS = {
77
- machine_max_speed_xy: ['machine_max_speed_x', 500],
78
- machine_max_speed_z: ['machine_max_speed_z', 12],
79
- machine_max_speed_e: ['machine_max_speed_e', 30],
80
- machine_max_accel_xy: ['machine_max_acceleration_x', 5000],
81
- machine_max_accel_z: ['machine_max_acceleration_z', 500],
82
- machine_max_accel_e: ['machine_max_acceleration_e', 5000],
83
- machine_jerk_xy: ['machine_max_jerk_x', 9],
84
- machine_jerk_z: ['machine_max_jerk_z', 0.4],
85
- machine_jerk_e: ['machine_max_jerk_e', 2.5],
86
- machine_accel_print: ['default_acceleration', 5000],
87
- machine_accel_travel: ['travel_acceleration', 5000],
88
- machine_accel_retract: ['machine_max_acceleration_retracting', 5000],
89
- }
90
72
  // The schema keys the machine limits are read from — for UI that has to show "which printer characteristics are in play"
91
73
  // without hardcoding key strings of its own.
92
74
  export const machineLimitKeys = Object.values(MACHINE_LIMITS).map(([key]) => key)
@@ -216,24 +198,25 @@ export function serializeProjectSettings(settings) {
216
198
  export function deriveKernelParams(settings, opts) {
217
199
  const plate = opts?.plate ?? 0
218
200
  const S = k => settingScalar(settings, k)
219
- const num = (k, d) => { const v = Number(S(k)); return Number.isFinite(v) ? v : d }
220
- const bool = (k, d) => { const v = settingRaw(settings, k); return typeof v === 'boolean' ? v : (Array.isArray(v) ? !!v[0] : (v == null ? d : !!v)) }
201
+ const str = k => String(S(k) ?? FFF_FALLBACKS[k])
202
+ const num = (k, d = FFF_FALLBACKS[k]) => { const v = Number(S(k)); return Number.isFinite(v) ? v : d }
203
+ const bool = (k, d = FFF_FALLBACKS[k]) => { const v = settingRaw(settings, k); return typeof v === 'boolean' ? v : (Array.isArray(v) ? !!v[0] : (v == null ? d : !!v)) }
221
204
  // Filament retraction overrides: upstream lets a material override the machine's retraction (TPU wants a
222
205
  // different pull-back than PLA on the same printer). These schema keys are nullable and carry no default of
223
206
  // their own — "unset" means keep the machine value — so this reads the settings map directly rather than
224
207
  // through settingScalar, whose schema fallback would not distinguish the two.
225
- const override = (filamentKey, machineKey, fallback) => {
208
+ const override = (filamentKey, machineKey, fallback = FFF_FALLBACKS[machineKey]) => {
226
209
  const raw = settings?.[filamentKey]
227
210
  const value = Number(Array.isArray(raw) ? raw[0] : raw)
228
211
  return Number.isFinite(value) ? value : num(machineKey, fallback)
229
212
  }
230
213
 
231
- let line_width = num('line_width', 0); if (!line_width) line_width = 0.42 // 0=auto -> default 0.42
214
+ let line_width = num('line_width'); if (!line_width) line_width = AUTO_LINE_WIDTH // 0 = auto
232
215
 
233
- let pat = String(S('sparse_infill_pattern') ?? 'rectilinear')
216
+ let pat = str('sparse_infill_pattern')
234
217
  if (!KERNEL_PATTERNS.includes(pat)) pat = 'rectilinear' // pattern unsupported by the kernel -> rectilinear
235
218
 
236
- let seam = String(S('seam_position') ?? 'back')
219
+ let seam = str('seam_position')
237
220
  if (seam === 'aligned_back') seam = 'back'
238
221
  if (!KERNEL_SEAMS.includes(seam)) seam = 'back'
239
222
 
@@ -243,14 +226,14 @@ export function deriveKernelParams(settings, opts) {
243
226
  // support_type participates too, as upstream's UI does: a tree(auto)/tree(manual) TYPE whose style is
244
227
  // still 'default' is a tree request — before this, an imported Orca project saved with tree(auto) and
245
228
  // the default style silently sliced grid. An explicit non-tree style keeps winning over the type.
246
- const styleRaw = String(S('support_style') ?? 'default')
247
- const typeRaw = String(S('support_type') ?? 'normal(auto)')
229
+ const styleRaw = str('support_style')
230
+ const typeRaw = str('support_type')
248
231
  const support_style =
249
232
  /tree|organic/i.test(styleRaw) || (/tree/i.test(typeRaw) && styleRaw === 'default') ? 'tree' : 'grid'
250
233
 
251
234
  // Bed: bounding box of the first rectangle in printable_area
252
235
  const pa = settingRaw(settings, 'printable_area')
253
- let bed_width = 256, bed_depth = 256
236
+ let bed_width = BED_FALLBACK.width, bed_depth = BED_FALLBACK.depth
254
237
  if (Array.isArray(pa) && pa.length) {
255
238
  const xs = pa.map(p => p[0]), ys = pa.map(p => p[1])
256
239
  bed_width = Math.max(...xs) - Math.min(...xs); bed_depth = Math.max(...ys) - Math.min(...ys)
@@ -288,7 +271,7 @@ export function deriveKernelParams(settings, opts) {
288
271
  // plate (the pre-array form, kept readable so old saved settings keep meaning what they meant).
289
272
  const towerSettings = {}
290
273
  {
291
- const width = num('prime_tower_width', 0)
274
+ const width = num('prime_tower_width')
292
275
  if (width > 0) towerSettings.prime_tower_width = width
293
276
  // Read the map itself, NOT settingRaw: wipe_tower_x/y carry an upstream schema default of (15, 220), which is
294
277
  // a coordinate off the front of a 200mm bed. Taking it as "the user chose a position" put the tower outside
@@ -309,9 +292,9 @@ export function deriveKernelParams(settings, opts) {
309
292
  // untouched project keeps whatever the kernel decides.
310
293
  if ('enable_prime_tower' in (settings ?? {})) towerSettings.enable_prime_tower = !!settings.enable_prime_tower
311
294
  if ('flush_into_infill' in (settings ?? {})) towerSettings.flush_into_infill = !!settings.flush_into_infill
312
- const multiplier = num('flush_multiplier', 0)
295
+ const multiplier = num('flush_multiplier')
313
296
  if (multiplier > 0) towerSettings.flush_multiplier = multiplier
314
- const prime = num('prime_volume', 0)
297
+ const prime = num('prime_volume')
315
298
  if (prime > 0) towerSettings.prime_volume = prime
316
299
  }
317
300
 
@@ -342,19 +325,19 @@ export function deriveKernelParams(settings, opts) {
342
325
  // thing jwidth_raw does for the widths that do get the percent-aware reader.
343
326
  if (present('support_line_width')) {
344
327
  const text = String(settingScalar(settings, 'support_line_width')).trim()
345
- const width = text.endsWith('%') ? (parseFloat(text) / 100) * num('nozzle_diameter', 0.4) : Number(text)
328
+ const width = text.endsWith('%') ? (parseFloat(text) / 100) * num('nozzle_diameter') : Number(text)
346
329
  if (Number.isFinite(width) && width > 0) passthrough.support_line_width = width
347
330
  }
348
331
  }
349
332
 
350
333
  const perExtruder = {}
351
334
  for (const [param, key, scalar] of [
352
- ['extruder_nozzle_temp', 'nozzle_temperature', num('nozzle_temperature', 200)],
353
- ['extruder_filament_diameter', 'filament_diameter', num('filament_diameter', 1.75)],
354
- ['extruder_flow_ratio', 'filament_flow_ratio', num('filament_flow_ratio', 1.0)],
355
- ['extruder_retract_length', 'filament_retraction_length', override('filament_retraction_length', 'retraction_length', 0.8)],
356
- ['extruder_retract_speed', 'filament_retraction_speed', override('filament_retraction_speed', 'retraction_speed', 30)],
357
- ['extruder_z_hop', 'filament_z_hop', override('filament_z_hop', 'z_hop', 0.4)],
335
+ ['extruder_nozzle_temp', 'nozzle_temperature', num('nozzle_temperature')],
336
+ ['extruder_filament_diameter', 'filament_diameter', num('filament_diameter')],
337
+ ['extruder_flow_ratio', 'filament_flow_ratio', num('filament_flow_ratio')],
338
+ ['extruder_retract_length', 'filament_retraction_length', override('filament_retraction_length', 'retraction_length')],
339
+ ['extruder_retract_speed', 'filament_retraction_speed', override('filament_retraction_speed', 'retraction_speed')],
340
+ ['extruder_z_hop', 'filament_z_hop', override('filament_z_hop', 'z_hop')],
358
341
  ]) {
359
342
  const raw = settings?.[key]
360
343
  if (!Array.isArray(raw) || raw.length < 2) continue
@@ -369,92 +352,92 @@ export function deriveKernelParams(settings, opts) {
369
352
  ...widths,
370
353
  ...machine,
371
354
  ...passthrough, // present only for keys the settings map actually holds (see PASSTHROUGH_* above)
372
- layer_height: num('layer_height', 0.2),
373
- first_layer_height: num('initial_layer_print_height', 0.2),
355
+ layer_height: num('layer_height'),
356
+ first_layer_height: num('initial_layer_print_height'),
374
357
  line_width,
375
- wall_loops: num('wall_loops', 2),
376
- infill_density: num('sparse_infill_density', 20) / 100, // % → 0~1
358
+ wall_loops: num('wall_loops'),
359
+ infill_density: num('sparse_infill_density') / 100, // % → 0~1
377
360
  sparse_infill_pattern: pat,
378
- infill_angle: num('infill_direction', 45),
379
- top_shell_layers: num('top_shell_layers', 4),
380
- bottom_shell_layers: num('bottom_shell_layers', 3),
361
+ infill_angle: num('infill_direction'),
362
+ top_shell_layers: num('top_shell_layers'),
363
+ bottom_shell_layers: num('bottom_shell_layers'),
381
364
  seam_position: seam,
382
- skirt_loops: num('skirt_loops', 1),
383
- skirt_distance: num('skirt_distance', 2),
384
- skirt_height: num('skirt_height', 1), // stage 33: the kernel used to hardcode the first layer
385
- brim_width: num('brim_width', 0),
386
- brim_object_gap: num('brim_object_gap', 0), // stage 33: the kernel used to hardcode w*0.5
365
+ skirt_loops: num('skirt_loops'),
366
+ skirt_distance: num('skirt_distance'),
367
+ skirt_height: num('skirt_height'), // stage 33: the kernel used to hardcode the first layer
368
+ brim_width: num('brim_width'),
369
+ brim_object_gap: num('brim_object_gap'), // stage 33: the kernel used to hardcode w*0.5
387
370
  ...perExtruder,
388
371
  // Prime tower. These were the one part of multi-material the settings map could not reach: the kernel read its
389
372
  // own prime_tower_* parameters and nothing mapped the upstream keys onto them, so the tower's size and place
390
373
  // were whatever the viewer decided. Only sent when the user actually set them — an unset width keeps the
391
374
  // kernel's own default, and an unset position lets the viewer's auto-placement stand.
392
375
  ...towerSettings,
393
- retract_length: override('filament_retraction_length', 'retraction_length', 0.8), // vector[0]
394
- retraction_minimum_travel: override('filament_retraction_minimum_travel', 'retraction_minimum_travel', 2), // stage 33: used to be the kernel constant 2.0
395
- gcode_resolution: num('resolution', 0.01), // stage 33: tree-support path simplification tolerance
396
- retract_speed: override('filament_retraction_speed', 'retraction_speed', 30),
397
- z_hop: override('filament_z_hop', 'z_hop', 0.4),
398
- travel_speed: num('travel_speed', 120),
399
- first_layer_speed: num('initial_layer_speed', 30),
400
- print_speed: num('outer_wall_speed', 60),
401
- nozzle_diameter: num('nozzle_diameter', 0.4),
402
- filament_diameter: num('filament_diameter', 1.75),
403
- flow_ratio: num('filament_flow_ratio', 1.0),
404
- nozzle_temp: num('nozzle_temperature', 200),
405
- bed_temp: num('hot_plate_temp', 45), // no bed_temperature key -> hot_plate_temp
376
+ retract_length: override('filament_retraction_length', 'retraction_length'), // vector[0]
377
+ retraction_minimum_travel: override('filament_retraction_minimum_travel', 'retraction_minimum_travel'), // stage 33: used to be the kernel constant 2.0
378
+ gcode_resolution: num('resolution'), // stage 33: tree-support path simplification tolerance
379
+ retract_speed: override('filament_retraction_speed', 'retraction_speed'),
380
+ z_hop: override('filament_z_hop', 'z_hop'),
381
+ travel_speed: num('travel_speed'),
382
+ first_layer_speed: num('initial_layer_speed'),
383
+ print_speed: num('outer_wall_speed'),
384
+ nozzle_diameter: num('nozzle_diameter'),
385
+ filament_diameter: num('filament_diameter'),
386
+ flow_ratio: num('filament_flow_ratio'),
387
+ nozzle_temp: num('nozzle_temperature'),
388
+ bed_temp: num('hot_plate_temp'), // no bed_temperature key -> hot_plate_temp
406
389
  bed_width, bed_depth,
407
- bed_height: num('printable_height', 0), // 0 = profile states no ceiling -> kernel skips the check
390
+ bed_height: num('printable_height'), // 0 = profile states no ceiling -> kernel skips the check
408
391
  // Explicitly-set only: the schema default here is upstream's generic "G28 / G1 Z5" preamble, and falling back
409
392
  // to it would rewrite the emitted G-code for every caller. The kernel keeps its own preamble when these are empty.
410
393
  machine_start_gcode: String(settings?.machine_start_gcode ?? ''),
411
394
  machine_end_gcode: String(settings?.machine_end_gcode ?? ''),
412
- enable_support: bool('enable_support', false),
413
- support_threshold_angle: num('support_threshold_angle', 30),
414
- support_top_z_distance: num('support_top_z_distance', 0.2),
415
- support_bottom_z_distance: num('support_bottom_z_distance', 0.2), // stage 32: support bottom z-gap (default 0.2 = equivalent to current behavior)
416
- support_xy_distance: num('support_object_xy_distance', 0.35),
417
- support_interface_top_layers: num('support_interface_top_layers', 2),
395
+ enable_support: bool('enable_support'),
396
+ support_threshold_angle: num('support_threshold_angle'),
397
+ support_top_z_distance: num('support_top_z_distance'),
398
+ support_bottom_z_distance: num('support_bottom_z_distance'), // stage 32: support bottom z-gap (default 0.2 = equivalent to current behavior)
399
+ support_xy_distance: num('support_object_xy_distance'),
400
+ support_interface_top_layers: num('support_interface_top_layers'),
418
401
  // Stage 33: support keys added when the kernel hardcoding was removed (upstream schema key names kept)
419
- support_angle: num('support_angle', 0),
420
- support_base_pattern: String(S('support_base_pattern') ?? 'default'),
421
- support_interface_pattern: String(S('support_interface_pattern') ?? 'auto'),
422
- support_interface_spacing: num('support_interface_spacing', 0.5),
423
- support_base_pattern_spacing: num('support_base_pattern_spacing', 2.5),
424
- support_remove_small_overhang: bool('support_remove_small_overhang', true),
425
- bridge_no_support: bool('bridge_no_support', false),
426
- support_expansion: num('support_expansion', 0),
402
+ support_angle: num('support_angle'),
403
+ support_base_pattern: str('support_base_pattern'),
404
+ support_interface_pattern: str('support_interface_pattern'),
405
+ support_interface_spacing: num('support_interface_spacing'),
406
+ support_base_pattern_spacing: num('support_base_pattern_spacing'),
407
+ support_remove_small_overhang: bool('support_remove_small_overhang'),
408
+ bridge_no_support: bool('bridge_no_support'),
409
+ support_expansion: num('support_expansion'),
427
410
  support_threshold_overlap: (() => { const v = settingRaw(settings, 'support_threshold_overlap')
428
411
  if (typeof v === 'string' && v.trim().endsWith('%')) return parseFloat(v) / 100
429
412
  const n = Number(v); return Number.isFinite(n) ? n : 0.5 })(), // "50%" -> 0.5 (ratio of extrusion width)
430
- support_on_build_plate_only: bool('support_on_build_plate_only', false),
431
- support_interface_bottom_layers: num('support_interface_bottom_layers', 0),
413
+ support_on_build_plate_only: bool('support_on_build_plate_only'),
414
+ support_interface_bottom_layers: num('support_interface_bottom_layers'),
432
415
  ...supportTools, // present only when a support extruder is mapped (see above)
433
- raft_layers: num('raft_layers', 0),
434
- raft_expansion: num('raft_expansion', 1.5), // stage 33: the kernel used to hardcode +3.0
435
- raft_contact_distance: num('raft_contact_distance', 0.1), // stage 33: previously ignored by the kernel
436
- fan_speed: num('fan_max_speed', 100), // no fan_speed key -> fan_max_speed
437
- close_fan_the_first_x_layers: num('close_fan_the_first_x_layers', 1),
438
- full_fan_speed_layer: num('full_fan_speed_layer', 0),
439
- slow_down_layer_time: num('slow_down_layer_time', 5),
440
- enable_arc_fitting: bool('enable_arc_fitting', false),
441
- spiral_mode: bool('spiral_mode', false),
416
+ raft_layers: num('raft_layers'),
417
+ raft_expansion: num('raft_expansion'), // stage 33: the kernel used to hardcode +3.0
418
+ raft_contact_distance: num('raft_contact_distance'), // stage 33: previously ignored by the kernel
419
+ fan_speed: num('fan_max_speed'), // no fan_speed key -> fan_max_speed
420
+ close_fan_the_first_x_layers: num('close_fan_the_first_x_layers'),
421
+ full_fan_speed_layer: num('full_fan_speed_layer'),
422
+ slow_down_layer_time: num('slow_down_layer_time'),
423
+ enable_arc_fitting: bool('enable_arc_fitting'),
424
+ spiral_mode: bool('spiral_mode'),
442
425
  // New in stage 5 (derived from the matching schema keys)
443
- seam_slope_type: String(S('seam_slope_type') ?? 'none'), // none|external|all -> scarf seam
444
- enable_pressure_advance: bool('enable_pressure_advance', false),
445
- pressure_advance: num('pressure_advance', 0.02),
426
+ seam_slope_type: str('seam_slope_type'), // none|external|all -> scarf seam
427
+ enable_pressure_advance: bool('enable_pressure_advance'),
428
+ pressure_advance: num('pressure_advance'),
446
429
  support_style, // grid|tree_lite (mapped above)
447
- bridge_speed: num('bridge_speed', 25),
430
+ bridge_speed: num('bridge_speed'),
448
431
  // New in stage 6 (derived from the matching schema keys). The MM parameters (extruder_count/mm_group_split) are
449
432
  // injected at onSlice time from the viewer's per-object extruder assignment (not included here).
450
- ironing_type: String(S('ironing_type') ?? 'no ironing'), // no ironing|top|topmost|solid (the top family = on)
451
- ironing_spacing: num('ironing_spacing', 0.1),
452
- ironing_flow: num('ironing_flow', 10),
453
- ironing_speed: num('ironing_speed', 20),
454
- reduce_crossing_wall: bool('reduce_crossing_wall', false),
455
- max_volumetric_extrusion_rate_slope: num('max_volumetric_extrusion_rate_slope', 0),
433
+ ironing_type: str('ironing_type'), // no ironing|top|topmost|solid (the top family = on)
434
+ ironing_spacing: num('ironing_spacing'),
435
+ ironing_flow: num('ironing_flow'),
436
+ ironing_speed: num('ironing_speed'),
437
+ reduce_crossing_wall: bool('reduce_crossing_wall'),
438
+ max_volumetric_extrusion_rate_slope: num('max_volumetric_extrusion_rate_slope'),
456
439
  // Stage 7: wall generator classic|arachne (arachne = the real ported OrcaSlicer WallToolPaths, variable width)
457
- wall_generator: (String(S('wall_generator') ?? 'classic') === 'arachne') ? 'arachne' : 'classic',
440
+ wall_generator: (str('wall_generator') === 'arachne') ? 'arachne' : 'classic',
458
441
  // support_density has no matching schema key, so the kernel default (0.15) is used; scarf_length likewise uses the kernel default (10mm)
459
442
  }
460
443
  }
@@ -504,13 +487,13 @@ export const slaSettingKeys = Object.freeze(Object.keys(SLA_SETTING_CONTRACT))
504
487
  // are read from the map directly, so they follow the same omission-friendly rule: absent means the default.
505
488
  export function deriveSlaParams(settings) {
506
489
  const S = k => settingScalar(settings, k)
507
- const num = (k, d) => { const v = Number(S(k)); return Number.isFinite(v) && v > 0 ? v : d }
490
+ const num = (k, d = SLA_FALLBACKS[k]) => { const v = Number(S(k)); return Number.isFinite(v) && v > 0 ? v : d }
508
491
 
509
492
  // Read from the MAP, not through the schema fallback: the key is shared with FFF and its schema default is that
510
493
  // technology's opinion (0.2mm) — four resin layers thick. Absent means the resin default here.
511
494
  const rawLayerHeight = settings?.layer_height
512
495
  const mapLayerHeight = Number(Array.isArray(rawLayerHeight) ? rawLayerHeight[0] : rawLayerHeight)
513
- const layer_height = Number.isFinite(mapLayerHeight) && mapLayerHeight > 0 ? mapLayerHeight : 0.05
496
+ const layer_height = Number.isFinite(mapLayerHeight) && mapLayerHeight > 0 ? mapLayerHeight : SLA_FALLBACKS.layer_height
514
497
  // Zero is a legal fade count (no fade band), so this one cannot go through the positives-only reader.
515
498
  const fadedRaw = Number(S('faded_layers'))
516
499
  const requestedSupportTree = String(S('support_tree_type') ?? 'default').toLowerCase()
@@ -520,15 +503,15 @@ export function deriveSlaParams(settings) {
520
503
  return {
521
504
  layer_height,
522
505
  initial_layer_height: num('initial_layer_height', layer_height),
523
- exposure_time: num('exposure_time', 7),
524
- initial_exposure_time: num('initial_exposure_time', 35),
506
+ exposure_time: num('exposure_time'),
507
+ initial_exposure_time: num('initial_exposure_time'),
525
508
  faded_layers: Number.isFinite(fadedRaw) && fadedRaw >= 0 ? fadedRaw : 10,
526
509
  // The physical display and its pixel grid — what turns millimetres into raster pixels at export time.
527
510
  // Fallbacks are the SL1's own panel, the reference machine of the format this exports to.
528
- display_width: num('display_width', 120.96),
529
- display_height: num('display_height', 68.04),
530
- display_pixels_x: num('display_pixels_x', 2560),
531
- display_pixels_y: num('display_pixels_y', 1440),
511
+ display_width: num('display_width'),
512
+ display_height: num('display_height'),
513
+ display_pixels_x: num('display_pixels_x'),
514
+ display_pixels_y: num('display_pixels_y'),
532
515
  // Panel mounting: the whole SL1 family is portrait with X mirrored (projection through the vat floor);
533
516
  // the raster writer swaps the image axes for portrait exactly like upstream SL1.cpp create_raster.
534
517
  display_orientation: String(S('display_orientation') ?? 'portrait') === 'landscape' ? 'landscape' : 'portrait',
@@ -545,8 +528,8 @@ export function deriveSlaParams(settings) {
545
528
  sla_material_settings_id: String(S('sla_material_settings_id') ?? ''),
546
529
  // The kernel's over-bed check and centring read bed dimensions; for a resin printer the printable area IS
547
530
  // the display, so the display doubles as the bed the shared parser sees.
548
- bed_width: num('display_width', 120.96),
549
- bed_depth: num('display_height', 68.04),
531
+ bed_width: num('display_width'),
532
+ bed_depth: num('display_height'),
550
533
  // Support generation (kernel slice_sla): the grid-pillar generator's shape parameters, all schema keys the
551
534
  // SLA extraction pass brought in — schema defaults rule, an edit wins.
552
535
  supports_enable: !!settingRaw(settings, 'supports_enable'),
@@ -555,20 +538,20 @@ export function deriveSlaParams(settings) {
555
538
  // the OpenVDB chain hollowing needs is not ported, and a solid print labeled hollowed is the worst outcome.
556
539
  hollowing_enable: !!settingRaw(settings, 'hollowing_enable'),
557
540
  support_buildplate_only: !!settingRaw(settings, 'support_buildplate_only'),
558
- support_pillar_diameter: num('support_pillar_diameter', 1.0),
559
- support_head_front_diameter: num('support_head_front_diameter', 0.4),
560
- support_head_width: num('support_head_width', 1.0),
561
- support_head_penetration: num('support_head_penetration', 0.2),
562
- support_points_density_relative: num('support_points_density_relative', 100),
563
- support_critical_angle: num('support_critical_angle', 45),
541
+ support_pillar_diameter: num('support_pillar_diameter'),
542
+ support_head_front_diameter: num('support_head_front_diameter'),
543
+ support_head_width: num('support_head_width'),
544
+ support_head_penetration: num('support_head_penetration'),
545
+ support_points_density_relative: num('support_points_density_relative'),
546
+ support_critical_angle: num('support_critical_angle'),
564
547
  support_tree_type,
565
548
  // The tree shape (upstream SupportTreeConfig): bridge reach, bracing distance, the base foot cone, the
566
549
  // pillar connection style, and how far the object is lifted onto its supports. Elevation may legally be
567
550
  // ZERO (print directly on the plate), so it cannot go through the positives-only reader.
568
- support_max_bridge_length: num('support_max_bridge_length', 15),
569
- support_max_pillar_link_distance: num('support_max_pillar_link_distance', 10),
570
- support_base_diameter: num('support_base_diameter', 4),
571
- support_base_height: num('support_base_height', 1),
551
+ support_max_bridge_length: num('support_max_bridge_length'),
552
+ support_max_pillar_link_distance: num('support_max_pillar_link_distance'),
553
+ support_base_diameter: num('support_base_diameter'),
554
+ support_base_height: num('support_base_height'),
572
555
  // The rest of upstream make_support_cfg (SLAPrint.cpp:50) — small-pillar percent arrives as "50%" so the
573
556
  // percent sign is stripped here; widening factor and base safety distance are legally ZERO (zero safety
574
557
  // falls back to the kernel's own safety_distance), so neither can go through the positives-only reader.
@@ -578,28 +561,28 @@ export function deriveSlaParams(settings) {
578
561
  (() => { const v = Number(S('support_pillar_widening_factor')); return Number.isFinite(v) && v >= 0 ? v : 0 })(),
579
562
  support_base_safety_distance:
580
563
  (() => { const v = Number(S('support_base_safety_distance')); return Number.isFinite(v) && v >= 0 ? v : 1 })(),
581
- support_max_bridges_on_pillar: num('support_max_bridges_on_pillar', 3),
582
- support_max_weight_on_model: num('support_max_weight_on_model', 10),
564
+ support_max_bridges_on_pillar: num('support_max_bridges_on_pillar'),
565
+ support_max_weight_on_model: num('support_max_weight_on_model'),
583
566
  // The generator's input slices are gap-closed with this radius, exactly like upstream slice_mesh_ex.
584
- slice_closing_radius: num('slice_closing_radius', 0.049),
567
+ slice_closing_radius: num('slice_closing_radius'),
585
568
  support_object_elevation: (() => { const v = Number(S('support_object_elevation')); return Number.isFinite(v) && v >= 0 ? v : 5 })(),
586
569
  support_pillar_connection_mode: String(S('support_pillar_connection_mode') ?? 'dynamic'),
587
570
  // The pad (upstream make_pad_cfg): wall height is legally ZERO (flat pad, no cavity), so it cannot go
588
571
  // through the positives-only reader.
589
- pad_brim_size: num('pad_brim_size', 1.6),
590
- pad_wall_thickness: num('pad_wall_thickness', 2),
572
+ pad_brim_size: num('pad_brim_size'),
573
+ pad_wall_thickness: num('pad_wall_thickness'),
591
574
  pad_wall_height:
592
575
  (() => { const v = Number(S('pad_wall_height')); return Number.isFinite(v) && v >= 0 ? v : 0 })(),
593
- pad_wall_slope: num('pad_wall_slope', 90),
594
- pad_max_merge_distance: num('pad_max_merge_distance', 50),
576
+ pad_wall_slope: num('pad_wall_slope'),
577
+ pad_max_merge_distance: num('pad_max_merge_distance'),
595
578
  pad_around_object: !!settingRaw(settings, 'pad_around_object'),
596
579
  // Embed mode (upstream PadConfig::EmbedObject): the ring's gap and the connector sticks that keep the
597
580
  // object attached to the pad. Read even when embed is off — the kernel ignores them then.
598
581
  pad_around_object_everywhere: !!settingRaw(settings, 'pad_around_object_everywhere'),
599
- pad_object_gap: num('pad_object_gap', 1),
600
- pad_object_connector_width: num('pad_object_connector_width', 0.5),
601
- pad_object_connector_stride: num('pad_object_connector_stride', 10),
602
- pad_object_connector_penetration: num('pad_object_connector_penetration', 0.3),
582
+ pad_object_gap: num('pad_object_gap'),
583
+ pad_object_connector_width: num('pad_object_connector_width'),
584
+ pad_object_connector_stride: num('pad_object_connector_stride'),
585
+ pad_object_connector_penetration: num('pad_object_connector_penetration'),
603
586
  }
604
587
  }
605
588
 
@@ -147,6 +147,18 @@ export function readPresetFile(raw: Record<string, unknown> | null | undefined,
147
147
  /** The schema keys the kernel's machine limits are read from — for UI that shows which ones are in play. */
148
148
  export const machineLimitKeys: string[]
149
149
 
150
+ /** deriveKernelParams' fallbacks when neither the settings map nor the schema gives a usable value, keyed by schema
151
+ * key. NOT the schema defaults — several differ on purpose. Frozen. */
152
+ export const FFF_FALLBACKS: Readonly<Record<string, number | boolean | string>>
153
+ /** deriveSlaParams' fallbacks (its reader treats non-positive values as absent). Frozen. */
154
+ export const SLA_FALLBACKS: Readonly<Record<string, number>>
155
+ /** What `line_width` 0 ("auto") resolves to. */
156
+ export const AUTO_LINE_WIDTH: number
157
+ /** The bed when `printable_area` is empty or malformed — the bbox of the schema's own default. */
158
+ export const BED_FALLBACK: Readonly<{ width: number; depth: number }>
159
+ /** Kernel machine-limit parameter -> [schema key, fallback]. Frozen. */
160
+ export const MACHINE_LIMITS: Readonly<Record<string, readonly [string, number]>>
161
+
150
162
  export interface ProcessPresetsApi {
151
163
  /** Every key a process preset can set — clear these before applying a different one */
152
164
  keys: string[]