three-slicer-viewer 0.3.0 → 0.3.2
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 -4
- package/TOOLPATH_SPEC.md +2 -2
- package/VIEWER.md +66 -1
- package/dist/components.js +102 -111
- package/dist/gcode_parse.js +190 -146
- package/dist/index.js +5060 -4324
- package/dist/model_loaders-DxlFNFOu.js +196 -0
- package/dist/model_loaders.js +15 -181
- package/dist/parse_3mf-BZISF3eW.js +327 -0
- package/dist/parse_3mf.worker.js +1 -1
- package/dist/shadow_host-CQ3LirzD.js +16 -0
- package/dist/sl1_encode.worker.js +1 -1
- package/dist/{sl1_write-CvrNejbW.js → sl1_write-BovHbg3k.js} +66 -65
- package/dist/sla_reconstruct-CRZ3TrI1.js +178 -0
- package/dist/sla_reconstruct.worker.js +1 -1
- package/dist/sla_slice.worker.js +22 -21
- package/dist/theme-2oyfMuvf.js +97 -0
- package/dist/toolpath.js +138 -213
- package/dist/toolpath_encoding-BuYs95qZ.js +23 -0
- package/dist/toolpath_palette-B8xEl-qa.js +89 -0
- package/dist/viewer_defaults-B7UUZ582.js +28 -0
- package/package.json +30 -5
- package/src/settings/index.js +2 -0
- package/src/settings/kernel_defaults.js +144 -0
- package/src/settings/settings_core.js +114 -131
- package/types/gcode.d.ts +27 -1
- package/types/settings.d.ts +12 -0
- package/types/viewer.d.ts +3 -1
- package/dist/parse_3mf-CQ5rAX6P.js +0 -299
- package/dist/sla_reconstruct-CXkYhux2.js +0 -176
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "three-slicer-viewer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "The three-slicer viewer without the slicer — model loading, G-code parsing, GPU toolpath rendering and the settings transforms, under MIT. Plug a slicer in through the `slicer` prop.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -51,15 +51,40 @@
|
|
|
51
51
|
"dist",
|
|
52
52
|
"src/data.js",
|
|
53
53
|
"src/settings",
|
|
54
|
-
"types"
|
|
54
|
+
"types",
|
|
55
|
+
"!**/.omc"
|
|
55
56
|
],
|
|
56
57
|
"keywords": [
|
|
57
58
|
"3d-printing",
|
|
59
|
+
"3d-printer",
|
|
60
|
+
"viewer",
|
|
58
61
|
"gcode",
|
|
62
|
+
"gcode-viewer",
|
|
59
63
|
"toolpath",
|
|
64
|
+
"stl",
|
|
65
|
+
"obj",
|
|
66
|
+
"3mf",
|
|
67
|
+
"amf",
|
|
68
|
+
"ply",
|
|
69
|
+
"sl1",
|
|
70
|
+
"sla",
|
|
71
|
+
"resin",
|
|
60
72
|
"three",
|
|
61
|
-
"
|
|
73
|
+
"three.js",
|
|
74
|
+
"threejs",
|
|
75
|
+
"react",
|
|
76
|
+
"webgl",
|
|
77
|
+
"model-viewer",
|
|
78
|
+
"three-slicer"
|
|
62
79
|
],
|
|
80
|
+
"homepage": "https://slicer.kimgh06.com",
|
|
81
|
+
"repository": {
|
|
82
|
+
"type": "git",
|
|
83
|
+
"url": "git+https://github.com/kimgh06/three-slicer-viewer.git"
|
|
84
|
+
},
|
|
85
|
+
"bugs": {
|
|
86
|
+
"url": "https://github.com/kimgh06/three-slicer-viewer/issues"
|
|
87
|
+
},
|
|
63
88
|
"peerDependencies": {
|
|
64
89
|
"three": "^0.160.0",
|
|
65
90
|
"react": ">=18",
|
|
@@ -84,7 +109,7 @@
|
|
|
84
109
|
},
|
|
85
110
|
"scripts": {
|
|
86
111
|
"build": "vite build && cp src/make_worker.js dist/make_worker.js",
|
|
87
|
-
"prepublishOnly": "npm run build && node test_version_lockstep.mjs && node test_toolpath_contract.mjs",
|
|
88
|
-
"test": "for f in test_*.mjs; do node $f || exit 1; done"
|
|
112
|
+
"prepublishOnly": "npm run build && node tests/test_version_lockstep.mjs && node tests/test_toolpath_contract.mjs",
|
|
113
|
+
"test": "for f in tests/test_*.mjs; do node $f || exit 1; done"
|
|
89
114
|
}
|
|
90
115
|
}
|
package/src/settings/index.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
// three-slicer-viewer/settings — the catalog-free settings surface. See settings_core.js.
|
|
2
2
|
export * from './settings_core.js'
|
|
3
|
+
// The derivation's own fallbacks, for callers that need the same numbers (frozen; see kernel_defaults.js).
|
|
4
|
+
export { FFF_FALLBACKS, SLA_FALLBACKS, AUTO_LINE_WIDTH, BED_FALLBACK, MACHINE_LIMITS } from './kernel_defaults.js'
|
|
@@ -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
|
|
220
|
-
const
|
|
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'
|
|
214
|
+
let line_width = num('line_width'); if (!line_width) line_width = AUTO_LINE_WIDTH // 0 = auto
|
|
232
215
|
|
|
233
|
-
let pat =
|
|
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 =
|
|
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 =
|
|
247
|
-
const typeRaw =
|
|
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 =
|
|
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'
|
|
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'
|
|
295
|
+
const multiplier = num('flush_multiplier')
|
|
313
296
|
if (multiplier > 0) towerSettings.flush_multiplier = multiplier
|
|
314
|
-
const prime = num('prime_volume'
|
|
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'
|
|
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'
|
|
353
|
-
['extruder_filament_diameter', 'filament_diameter', num('filament_diameter'
|
|
354
|
-
['extruder_flow_ratio', 'filament_flow_ratio', num('filament_flow_ratio'
|
|
355
|
-
['extruder_retract_length', 'filament_retraction_length', override('filament_retraction_length', 'retraction_length'
|
|
356
|
-
['extruder_retract_speed', 'filament_retraction_speed', override('filament_retraction_speed', 'retraction_speed'
|
|
357
|
-
['extruder_z_hop', 'filament_z_hop', override('filament_z_hop', 'z_hop'
|
|
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'
|
|
373
|
-
first_layer_height: num('initial_layer_print_height'
|
|
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'
|
|
376
|
-
infill_density: num('sparse_infill_density'
|
|
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'
|
|
379
|
-
top_shell_layers: num('top_shell_layers'
|
|
380
|
-
bottom_shell_layers: num('bottom_shell_layers'
|
|
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'
|
|
383
|
-
skirt_distance: num('skirt_distance'
|
|
384
|
-
skirt_height: num('skirt_height'
|
|
385
|
-
brim_width: num('brim_width'
|
|
386
|
-
brim_object_gap: num('brim_object_gap'
|
|
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'
|
|
394
|
-
retraction_minimum_travel: override('filament_retraction_minimum_travel', 'retraction_minimum_travel'
|
|
395
|
-
gcode_resolution: num('resolution'
|
|
396
|
-
retract_speed: override('filament_retraction_speed', 'retraction_speed'
|
|
397
|
-
z_hop: override('filament_z_hop', 'z_hop'
|
|
398
|
-
travel_speed: num('travel_speed'
|
|
399
|
-
first_layer_speed: num('initial_layer_speed'
|
|
400
|
-
print_speed: num('outer_wall_speed'
|
|
401
|
-
nozzle_diameter: num('nozzle_diameter'
|
|
402
|
-
filament_diameter: num('filament_diameter'
|
|
403
|
-
flow_ratio: num('filament_flow_ratio'
|
|
404
|
-
nozzle_temp: num('nozzle_temperature'
|
|
405
|
-
bed_temp: num('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'
|
|
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'
|
|
413
|
-
support_threshold_angle: num('support_threshold_angle'
|
|
414
|
-
support_top_z_distance: num('support_top_z_distance'
|
|
415
|
-
support_bottom_z_distance: num('support_bottom_z_distance'
|
|
416
|
-
support_xy_distance: num('support_object_xy_distance'
|
|
417
|
-
support_interface_top_layers: num('support_interface_top_layers'
|
|
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'
|
|
420
|
-
support_base_pattern:
|
|
421
|
-
support_interface_pattern:
|
|
422
|
-
support_interface_spacing: num('support_interface_spacing'
|
|
423
|
-
support_base_pattern_spacing: num('support_base_pattern_spacing'
|
|
424
|
-
support_remove_small_overhang: bool('support_remove_small_overhang'
|
|
425
|
-
bridge_no_support: bool('bridge_no_support'
|
|
426
|
-
support_expansion: num('support_expansion'
|
|
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'
|
|
431
|
-
support_interface_bottom_layers: num('support_interface_bottom_layers'
|
|
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'
|
|
434
|
-
raft_expansion: num('raft_expansion'
|
|
435
|
-
raft_contact_distance: num('raft_contact_distance'
|
|
436
|
-
fan_speed: num('fan_max_speed'
|
|
437
|
-
close_fan_the_first_x_layers: num('close_fan_the_first_x_layers'
|
|
438
|
-
full_fan_speed_layer: num('full_fan_speed_layer'
|
|
439
|
-
slow_down_layer_time: num('slow_down_layer_time'
|
|
440
|
-
enable_arc_fitting: bool('enable_arc_fitting'
|
|
441
|
-
spiral_mode: bool('spiral_mode'
|
|
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:
|
|
444
|
-
enable_pressure_advance: bool('enable_pressure_advance'
|
|
445
|
-
pressure_advance: num('pressure_advance'
|
|
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'
|
|
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:
|
|
451
|
-
ironing_spacing: num('ironing_spacing'
|
|
452
|
-
ironing_flow: num('ironing_flow'
|
|
453
|
-
ironing_speed: num('ironing_speed'
|
|
454
|
-
reduce_crossing_wall: bool('reduce_crossing_wall'
|
|
455
|
-
max_volumetric_extrusion_rate_slope: num('max_volumetric_extrusion_rate_slope'
|
|
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: (
|
|
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 :
|
|
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'
|
|
524
|
-
initial_exposure_time: num('initial_exposure_time'
|
|
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'
|
|
529
|
-
display_height: num('display_height'
|
|
530
|
-
display_pixels_x: num('display_pixels_x'
|
|
531
|
-
display_pixels_y: num('display_pixels_y'
|
|
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'
|
|
549
|
-
bed_depth: num('display_height'
|
|
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'
|
|
559
|
-
support_head_front_diameter: num('support_head_front_diameter'
|
|
560
|
-
support_head_width: num('support_head_width'
|
|
561
|
-
support_head_penetration: num('support_head_penetration'
|
|
562
|
-
support_points_density_relative: num('support_points_density_relative'
|
|
563
|
-
support_critical_angle: num('support_critical_angle'
|
|
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'
|
|
569
|
-
support_max_pillar_link_distance: num('support_max_pillar_link_distance'
|
|
570
|
-
support_base_diameter: num('support_base_diameter'
|
|
571
|
-
support_base_height: num('support_base_height'
|
|
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'
|
|
582
|
-
support_max_weight_on_model: num('support_max_weight_on_model'
|
|
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'
|
|
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'
|
|
590
|
-
pad_wall_thickness: num('pad_wall_thickness'
|
|
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'
|
|
594
|
-
pad_max_merge_distance: num('pad_max_merge_distance'
|
|
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'
|
|
600
|
-
pad_object_connector_width: num('pad_object_connector_width'
|
|
601
|
-
pad_object_connector_stride: num('pad_object_connector_stride'
|
|
602
|
-
pad_object_connector_penetration: num('pad_object_connector_penetration'
|
|
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
|
|