three-slicer 0.1.1 → 0.1.3
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/components/dist/SettingsPanel.js +112 -112
- package/components/styles.css +9 -9
- package/data/config-schema.json +999 -900
- package/data/invalidation-map.json +6 -5
- package/data/printers.json +1 -0
- package/data/processes.js +2 -0
- package/data/toggle-rules.json +44 -44
- package/data/ui-tree.json +170 -494
- package/engine/examples/headless.mjs +2 -2
- package/engine/index.js +2 -2
- package/engine/src/data.js +13 -7
- package/engine/src/settings.js +120 -35
- package/engine/src/slicer.worker.js +23 -21
- package/engine/src/slicer_core.js +0 -0
- package/engine/src/slicer_core.mt.js +0 -0
- package/engine/src/toggle_eval.js +11 -11
- package/package.json +4 -2
- package/types/components.d.ts +5 -5
- package/types/data/config-schema.json.d.ts +6 -6
- package/types/data/invalidation-map.json.d.ts +5 -5
- package/types/data/printers.json.d.ts +23 -0
- package/types/data/processes.js.d.ts +21 -0
- package/types/data/toggle-rules.json.d.ts +5 -5
- package/types/data/ui-tree.json.d.ts +3 -3
- package/types/data.d.ts +10 -7
- package/types/gen_settings_types.mjs +10 -10
- package/types/settings-keys.d.ts +18 -7
- package/types/settings.d.ts +6 -6
- package/types/toggle.d.ts +6 -6
- package/types/viewer-loaders.d.ts +8 -8
- package/types/viewer-toolpath.d.ts +17 -17
- package/types/viewer.d.ts +3 -3
- package/types/wasm.d.ts +3 -3
- package/types/worker.d.ts +2 -2
- package/viewer/dist/Viewport.js +1760 -966
- package/viewer/dist/make_worker.js +6 -6
- package/viewer/dist/model_loaders.js +35 -35
- package/viewer/dist/toolpath_gpu.js +146 -147
- package/viewer/styles.css +46 -25
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// three-slicer headless SDK example + independence proof: drive the engine from Node using ONLY the
|
|
2
2
|
// public API (no UI, no viewer). Generates a 20mm cube STL, slices it, prints stats + G-code length.
|
|
3
3
|
// run: node packages/engine/examples/headless.mjs
|
|
4
|
-
import { createSlicer } from 'three-slicer' //
|
|
4
|
+
import { createSlicer } from 'three-slicer' // resolved via the workspace symlink — same path an external consumer takes
|
|
5
5
|
|
|
6
6
|
// minimal binary-STL writer for a box (public API takes a binary-STL ArrayBuffer)
|
|
7
7
|
function boxTris(ox, oy, oz, sx, sy, sz) {
|
|
@@ -27,7 +27,7 @@ const slicer = await createSlicer()
|
|
|
27
27
|
const r = slicer.slice(cube, params)
|
|
28
28
|
console.log(`batch : layers=${r.stats.layers} segments=${r.stats.path_segments} filament=${r.stats.filament_mm.toFixed(1)}mm gcode=${r.gcode.length} chars`)
|
|
29
29
|
|
|
30
|
-
// (b) streaming slice via onLayer (30
|
|
30
|
+
// (b) streaming slice via onLayer (stage 30) — assemble G-code from per-layer chunks
|
|
31
31
|
let chunks = 0, gbytes = 0
|
|
32
32
|
const rs = slicer.slice(cube, params, { onLayer: ({ gcode }) => { chunks++; gbytes += gcode.length } })
|
|
33
33
|
console.log(`stream: layers=${chunks} streamed=${rs.stats.streamed} assembled-gcode=${gbytes} chars`)
|
package/engine/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import createModule from './src/slicer_core.js'
|
|
|
7
7
|
const u8 = (b) => (b instanceof Uint8Array ? b : new Uint8Array(b))
|
|
8
8
|
|
|
9
9
|
// Off-main-thread worker URL for the browser: `new Worker(engineWorkerURL(), { type: 'module' })`.
|
|
10
|
-
// The worker speaks the 30
|
|
10
|
+
// The worker speaks the stage-30 streaming protocol ({type:'layer'|'done'|'error'|'progress'}).
|
|
11
11
|
export const engineWorkerURL = () => new URL('./src/slicer.worker.js', import.meta.url)
|
|
12
12
|
|
|
13
13
|
// createSlicer(): load the WASM kernel and return a handle. Works in Node and the browser main thread.
|
|
@@ -15,7 +15,7 @@ export const engineWorkerURL = () => new URL('./src/slicer.worker.js', import.me
|
|
|
15
15
|
// - stl: ArrayBuffer|Uint8Array (binary STL)
|
|
16
16
|
// - params: kernel-params object OR JSON string (see web/GUIDE.md; use deriveKernelParams to build from UI settings)
|
|
17
17
|
// - onProgress(done,total): per-layer progress
|
|
18
|
-
// - onLayer({z,idx,gcode,paths,widths}): 30
|
|
18
|
+
// - onLayer({z,idx,gcode,paths,widths}): stage-30 streaming — emits + frees each layer; when set, the
|
|
19
19
|
// returned result carries stats only (assemble gcode/layers from the callbacks). When omitted, the
|
|
20
20
|
// result carries the whole { gcode, stats, layers }.
|
|
21
21
|
// paintPrepare/paint/paintClear/overlay: manual support enforcer/blocker painting (TriangleSelector).
|
package/engine/src/data.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
//
|
|
1
|
+
// The single import site for the extracted JSON files.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// Vite/esbuild
|
|
5
|
-
//
|
|
6
|
-
// "
|
|
7
|
-
// "inconsistent import attributes"
|
|
8
|
-
//
|
|
3
|
+
// Why funnel them through one module: JSON requires `with { type: 'json' }` in Node 22+ ESM, but
|
|
4
|
+
// Vite/esbuild strip that attribute from bundle output (neither build.target nor importAttributesKey
|
|
5
|
+
// prevents it — measured). So if the same JSON is pointed at by both "the original with the attribute
|
|
6
|
+
// (engine/src/*.js)" and "the bundle without it (components/dist)", the consumer's bundler warns with
|
|
7
|
+
// "inconsistent import attributes". Only this file holds the JSON; everything else imports this module
|
|
8
|
+
// as external -> no JSON import survives in the bundle, so the mismatch cannot arise.
|
|
9
9
|
export { default as schema } from 'three-slicer/data/config-schema.json' with { type: 'json' }
|
|
10
10
|
export { default as uiTree } from 'three-slicer/data/ui-tree.json' with { type: 'json' }
|
|
11
11
|
export { default as toggleRules } from 'three-slicer/data/toggle-rules.json' with { type: 'json' }
|
|
12
12
|
export { default as invalidationMap } from 'three-slicer/data/invalidation-map.json' with { type: 'json' }
|
|
13
|
+
export { default as printers } from 'three-slicer/data/printers.json' with { type: 'json' }
|
|
14
|
+
// processes is the largest artifact (~800 KB) and is only needed once a printer is picked, so it stays a dynamic
|
|
15
|
+
// import. It is generated as a JS module rather than JSON precisely because of the attribute problem above, in its
|
|
16
|
+
// other direction: a dynamic JSON import needs the attribute in Node, and that same attribute makes browsers
|
|
17
|
+
// reject the text/javascript response a dev server gives for .json.
|
|
18
|
+
export const loadProcesses = () => import('three-slicer/data/processes.js').then(m => m.default)
|
package/engine/src/settings.js
CHANGED
|
@@ -1,40 +1,116 @@
|
|
|
1
|
-
//
|
|
2
|
-
// -
|
|
3
|
-
// -
|
|
4
|
-
import { schema } from './data.js'
|
|
1
|
+
// Maps the actual values of the (editable) right-hand settings panel -> kernel slice parameters.
|
|
2
|
+
// - Settings state is a sparse map (key->value): only edited keys are stored, otherwise the config-schema default.
|
|
3
|
+
// - For vector types (coFloats/coInts, …) only the first element is used/edited (simplification).
|
|
4
|
+
import { schema, printers, loadProcesses } from './data.js'
|
|
5
5
|
|
|
6
6
|
export function schemaDefault(key) { return schema[key]?.default }
|
|
7
7
|
export function settingRaw(settings, key) { return (settings && key in settings) ? settings[key] : schemaDefault(key) }
|
|
8
|
-
//
|
|
8
|
+
// Normalize to a scalar ([0] for vectors)
|
|
9
9
|
export function settingScalar(settings, key) { const v = settingRaw(settings, key); return Array.isArray(v) ? v[0] : v }
|
|
10
10
|
|
|
11
|
-
// 8
|
|
11
|
+
// Stage 8: real ported Fill patterns (gyroid TPMS/honeycomb/3dhoneycomb/crosshatch/concentric) + the older approximations
|
|
12
12
|
const KERNEL_PATTERNS = ['rectilinear', 'grid', 'triangles', 'zigzag', 'gyroid', 'gyroid_approx',
|
|
13
13
|
'honeycomb', '3dhoneycomb', 'crosshatch', 'concentric']
|
|
14
14
|
const KERNEL_SEAMS = ['nearest', 'aligned', 'back', 'random']
|
|
15
15
|
|
|
16
|
-
//
|
|
16
|
+
// Machine limits (the "Motion ability" printer page) -> kernel time-estimate parameters.
|
|
17
|
+
// Declarative on purpose: the kernel collapses X/Y into one axis, so the mapping cannot be a plain pass-through,
|
|
18
|
+
// but adding a limit stays a data row instead of code. The fallback is the kernel's own default, so an unedited
|
|
19
|
+
// profile produces exactly the previous estimate. Upstream leaves machine_max_*_x/y/z/e without a schema default
|
|
20
|
+
// (they only ever come from a printer preset), hence the explicit fallbacks here.
|
|
21
|
+
const MACHINE_LIMITS = {
|
|
22
|
+
machine_max_speed_xy: ['machine_max_speed_x', 500],
|
|
23
|
+
machine_max_speed_z: ['machine_max_speed_z', 12],
|
|
24
|
+
machine_max_speed_e: ['machine_max_speed_e', 30],
|
|
25
|
+
machine_max_accel_xy: ['machine_max_acceleration_x', 5000],
|
|
26
|
+
machine_max_accel_z: ['machine_max_acceleration_z', 500],
|
|
27
|
+
machine_max_accel_e: ['machine_max_acceleration_e', 5000],
|
|
28
|
+
machine_jerk_xy: ['machine_max_jerk_x', 9],
|
|
29
|
+
machine_jerk_z: ['machine_max_jerk_z', 0.4],
|
|
30
|
+
machine_jerk_e: ['machine_max_jerk_e', 2.5],
|
|
31
|
+
machine_accel_print: ['default_acceleration', 5000],
|
|
32
|
+
machine_accel_travel: ['travel_acceleration', 5000],
|
|
33
|
+
machine_accel_retract: ['machine_max_acceleration_retracting', 5000],
|
|
34
|
+
}
|
|
35
|
+
// The schema keys the machine limits are read from — for UI that has to show "which printer characteristics are in play"
|
|
36
|
+
// without hardcoding key strings of its own.
|
|
37
|
+
export const machineLimitKeys = Object.values(MACHINE_LIMITS).map(([key]) => key)
|
|
38
|
+
|
|
39
|
+
// ---- Printer profiles -------------------------------------------------------
|
|
40
|
+
// printers.json is stored column-oriented (see its .d.ts). These two hide that layout so no consumer decodes it.
|
|
41
|
+
|
|
42
|
+
/** Every option key a printer profile can set — what to clear before applying a different printer. */
|
|
43
|
+
export const printerKeys = printers.keys
|
|
44
|
+
|
|
45
|
+
/** Vendor -> profile name -> `[nozzle, setIndex, model]`, straight from the data (for building a picker). */
|
|
46
|
+
export const printersByVendor = printers.byVendor
|
|
47
|
+
|
|
48
|
+
// Process (print) presets live in the ~800 KB processes.json, so they load on demand — the first call fetches,
|
|
49
|
+
// later ones reuse the same promise. Returns a small facade so no caller has to know the column layout.
|
|
50
|
+
let processesPromise = null
|
|
51
|
+
export function processPresets() {
|
|
52
|
+
processesPromise ??= loadProcesses().then(data => ({
|
|
53
|
+
/** Every key a process preset can set — clear these before applying a different one */
|
|
54
|
+
keys: data.keys,
|
|
55
|
+
/** Preset names compatible with a printer profile, in upstream order */
|
|
56
|
+
listFor: (printerProfileName) =>
|
|
57
|
+
(data.byPrinter[printerProfileName] ?? []).map(i => data.presets[i][0]),
|
|
58
|
+
/** The settings a preset applies, ready to merge. `null` when unknown. */
|
|
59
|
+
settingsFor: (presetName) => {
|
|
60
|
+
const preset = data.presets.find(([name]) => name === presetName)
|
|
61
|
+
if (!preset) return null
|
|
62
|
+
const row = data.sets[preset[1]], out = {}
|
|
63
|
+
data.keys.forEach((key, i) => { if (row[i] != null) out[key] = row[i] })
|
|
64
|
+
return out
|
|
65
|
+
},
|
|
66
|
+
}))
|
|
67
|
+
return processesPromise
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function printerEntry(profileName) {
|
|
71
|
+
for (const models of Object.values(printers.byVendor)) {
|
|
72
|
+
const entry = models[profileName]
|
|
73
|
+
if (entry) return entry
|
|
74
|
+
}
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The settings a printer profile applies, ready to merge into the settings map. `null` when unknown. */
|
|
79
|
+
export function printerSettings(profileName) {
|
|
80
|
+
const entry = printerEntry(profileName)
|
|
81
|
+
if (!entry) return null
|
|
82
|
+
const row = printers.sets[entry[1]]
|
|
83
|
+
const out = {}
|
|
84
|
+
printers.keys.forEach((key, i) => { if (row[i] != null) out[key] = row[i] })
|
|
85
|
+
return out
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The vendor's recommended process preset for a printer, or '' when the profile names none. */
|
|
89
|
+
export function printerDefaultPreset(profileName) { return printerEntry(profileName)?.[3] ?? '' }
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
// Right-panel settings values -> kernel parameters (derived from schema keys)
|
|
17
93
|
export function deriveKernelParams(settings) {
|
|
18
94
|
const S = k => settingScalar(settings, k)
|
|
19
95
|
const num = (k, d) => { const v = Number(S(k)); return Number.isFinite(v) ? v : d }
|
|
20
96
|
const bool = (k, d) => { const v = settingRaw(settings, k); return typeof v === 'boolean' ? v : (Array.isArray(v) ? !!v[0] : (v == null ? d : !!v)) }
|
|
21
97
|
|
|
22
|
-
let line_width = num('line_width', 0); if (!line_width) line_width = 0.42 // 0=auto
|
|
98
|
+
let line_width = num('line_width', 0); if (!line_width) line_width = 0.42 // 0=auto -> default 0.42
|
|
23
99
|
|
|
24
100
|
let pat = String(S('sparse_infill_pattern') ?? 'rectilinear')
|
|
25
|
-
if (!KERNEL_PATTERNS.includes(pat)) pat = 'rectilinear' //
|
|
101
|
+
if (!KERNEL_PATTERNS.includes(pat)) pat = 'rectilinear' // pattern unsupported by the kernel -> rectilinear
|
|
26
102
|
|
|
27
103
|
let seam = String(S('seam_position') ?? 'back')
|
|
28
104
|
if (seam === 'aligned_back') seam = 'back'
|
|
29
105
|
if (!KERNEL_SEAMS.includes(seam)) seam = 'back'
|
|
30
106
|
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// generate_tree_support_3D
|
|
107
|
+
// Support style: schema enum (default/grid/snug/organic/tree_slim/tree_strong/tree_hybrid)
|
|
108
|
+
// -> kernel grid|tree|tree_lite. Since stage 18 the tree/organic family maps to the real organic TreeSupport ('tree',
|
|
109
|
+
// generate_tree_support_3D -> real type5 branch toolpaths); everything else maps to grid.
|
|
34
110
|
const styleRaw = String(S('support_style') ?? 'default')
|
|
35
111
|
const support_style = /tree|organic/i.test(styleRaw) ? 'tree' : 'grid'
|
|
36
112
|
|
|
37
|
-
//
|
|
113
|
+
// Bed: bounding box of the first rectangle in printable_area
|
|
38
114
|
const pa = settingRaw(settings, 'printable_area')
|
|
39
115
|
let bed_width = 256, bed_depth = 256
|
|
40
116
|
if (Array.isArray(pa) && pa.length) {
|
|
@@ -42,15 +118,19 @@ export function deriveKernelParams(settings) {
|
|
|
42
118
|
bed_width = Math.max(...xs) - Math.min(...xs); bed_depth = Math.max(...ys) - Math.min(...ys)
|
|
43
119
|
}
|
|
44
120
|
|
|
45
|
-
// 21
|
|
46
|
-
// 0
|
|
121
|
+
// Stage 21: per-feature widths — passed to the kernel verbatim (including strings like "120%"). Unedited keys are omitted -> the kernel derives auto(=line_width).
|
|
122
|
+
// The 0=auto semantics are handled by the kernel's resolve_lw (upstream Flow formula). Defaults unchanged (no feature set -> line_width).
|
|
47
123
|
const widths = {}
|
|
48
124
|
for (const k of ['outer_wall_line_width','inner_wall_line_width','top_surface_line_width',
|
|
49
125
|
'sparse_infill_line_width','internal_solid_infill_line_width','initial_layer_line_width']) {
|
|
50
126
|
const v = S(k); if (v != null && v !== '') widths[k] = v
|
|
51
127
|
}
|
|
128
|
+
const machine = {}
|
|
129
|
+
for (const [param, [key, fallback]] of Object.entries(MACHINE_LIMITS)) machine[param] = num(key, fallback)
|
|
130
|
+
|
|
52
131
|
return {
|
|
53
132
|
...widths,
|
|
133
|
+
...machine,
|
|
54
134
|
layer_height: num('layer_height', 0.2),
|
|
55
135
|
first_layer_height: num('initial_layer_print_height', 0.2),
|
|
56
136
|
line_width,
|
|
@@ -63,12 +143,12 @@ export function deriveKernelParams(settings) {
|
|
|
63
143
|
seam_position: seam,
|
|
64
144
|
skirt_loops: num('skirt_loops', 1),
|
|
65
145
|
skirt_distance: num('skirt_distance', 2),
|
|
66
|
-
skirt_height: num('skirt_height', 1), // 33
|
|
146
|
+
skirt_height: num('skirt_height', 1), // stage 33: the kernel used to hardcode the first layer
|
|
67
147
|
brim_width: num('brim_width', 0),
|
|
68
|
-
brim_object_gap: num('brim_object_gap', 0), // 33
|
|
69
|
-
retract_length: num('retraction_length', 0.8), //
|
|
70
|
-
retraction_minimum_travel: num('retraction_minimum_travel', 2), // 33
|
|
71
|
-
gcode_resolution: num('resolution', 0.01), // 33
|
|
148
|
+
brim_object_gap: num('brim_object_gap', 0), // stage 33: the kernel used to hardcode w*0.5
|
|
149
|
+
retract_length: num('retraction_length', 0.8), // vector[0]
|
|
150
|
+
retraction_minimum_travel: num('retraction_minimum_travel', 2), // stage 33: used to be the kernel constant 2.0
|
|
151
|
+
gcode_resolution: num('resolution', 0.01), // stage 33: tree-support path simplification tolerance
|
|
72
152
|
retract_speed: num('retraction_speed', 30),
|
|
73
153
|
z_hop: num('z_hop', 0.4),
|
|
74
154
|
travel_speed: num('travel_speed', 120),
|
|
@@ -78,15 +158,20 @@ export function deriveKernelParams(settings) {
|
|
|
78
158
|
filament_diameter: num('filament_diameter', 1.75),
|
|
79
159
|
flow_ratio: num('filament_flow_ratio', 1.0),
|
|
80
160
|
nozzle_temp: num('nozzle_temperature', 200),
|
|
81
|
-
bed_temp: num('hot_plate_temp', 45), // bed_temperature
|
|
161
|
+
bed_temp: num('hot_plate_temp', 45), // no bed_temperature key -> hot_plate_temp
|
|
82
162
|
bed_width, bed_depth,
|
|
163
|
+
bed_height: num('printable_height', 0), // 0 = profile states no ceiling -> kernel skips the check
|
|
164
|
+
// Explicitly-set only: the schema default here is upstream's generic "G28 / G1 Z5" preamble, and falling back
|
|
165
|
+
// to it would rewrite the emitted G-code for every caller. The kernel keeps its own preamble when these are empty.
|
|
166
|
+
machine_start_gcode: String(settings?.machine_start_gcode ?? ''),
|
|
167
|
+
machine_end_gcode: String(settings?.machine_end_gcode ?? ''),
|
|
83
168
|
enable_support: bool('enable_support', false),
|
|
84
169
|
support_threshold_angle: num('support_threshold_angle', 30),
|
|
85
170
|
support_top_z_distance: num('support_top_z_distance', 0.2),
|
|
86
|
-
support_bottom_z_distance: num('support_bottom_z_distance', 0.2), // 32
|
|
171
|
+
support_bottom_z_distance: num('support_bottom_z_distance', 0.2), // stage 32: support bottom z-gap (default 0.2 = equivalent to current behavior)
|
|
87
172
|
support_xy_distance: num('support_object_xy_distance', 0.35),
|
|
88
173
|
support_interface_top_layers: num('support_interface_top_layers', 2),
|
|
89
|
-
// 33
|
|
174
|
+
// Stage 33: support keys added when the kernel hardcoding was removed (upstream schema key names kept)
|
|
90
175
|
support_angle: num('support_angle', 0),
|
|
91
176
|
support_base_pattern: String(S('support_base_pattern') ?? 'default'),
|
|
92
177
|
support_interface_pattern: String(S('support_interface_pattern') ?? 'auto'),
|
|
@@ -97,34 +182,34 @@ export function deriveKernelParams(settings) {
|
|
|
97
182
|
support_expansion: num('support_expansion', 0),
|
|
98
183
|
support_threshold_overlap: (() => { const v = settingRaw(settings, 'support_threshold_overlap')
|
|
99
184
|
if (typeof v === 'string' && v.trim().endsWith('%')) return parseFloat(v) / 100
|
|
100
|
-
const n = Number(v); return Number.isFinite(n) ? n : 0.5 })(), // "50%"
|
|
185
|
+
const n = Number(v); return Number.isFinite(n) ? n : 0.5 })(), // "50%" -> 0.5 (ratio of extrusion width)
|
|
101
186
|
support_on_build_plate_only: bool('support_on_build_plate_only', false),
|
|
102
187
|
support_interface_bottom_layers: num('support_interface_bottom_layers', 0),
|
|
103
188
|
raft_layers: num('raft_layers', 0),
|
|
104
|
-
raft_expansion: num('raft_expansion', 1.5), // 33
|
|
105
|
-
raft_contact_distance: num('raft_contact_distance', 0.1), // 33
|
|
106
|
-
fan_speed: num('fan_max_speed', 100), // fan_speed
|
|
189
|
+
raft_expansion: num('raft_expansion', 1.5), // stage 33: the kernel used to hardcode +3.0
|
|
190
|
+
raft_contact_distance: num('raft_contact_distance', 0.1), // stage 33: previously ignored by the kernel
|
|
191
|
+
fan_speed: num('fan_max_speed', 100), // no fan_speed key -> fan_max_speed
|
|
107
192
|
close_fan_the_first_x_layers: num('close_fan_the_first_x_layers', 1),
|
|
108
193
|
full_fan_speed_layer: num('full_fan_speed_layer', 0),
|
|
109
194
|
slow_down_layer_time: num('slow_down_layer_time', 5),
|
|
110
195
|
enable_arc_fitting: bool('enable_arc_fitting', false),
|
|
111
196
|
spiral_mode: bool('spiral_mode', false),
|
|
112
|
-
// 5
|
|
113
|
-
seam_slope_type: String(S('seam_slope_type') ?? 'none'), // none|external|all
|
|
197
|
+
// New in stage 5 (derived from the matching schema keys)
|
|
198
|
+
seam_slope_type: String(S('seam_slope_type') ?? 'none'), // none|external|all -> scarf seam
|
|
114
199
|
enable_pressure_advance: bool('enable_pressure_advance', false),
|
|
115
200
|
pressure_advance: num('pressure_advance', 0.02),
|
|
116
|
-
support_style, // grid|tree_lite (
|
|
201
|
+
support_style, // grid|tree_lite (mapped above)
|
|
117
202
|
bridge_speed: num('bridge_speed', 25),
|
|
118
|
-
// 6
|
|
119
|
-
//
|
|
120
|
-
ironing_type: String(S('ironing_type') ?? 'no ironing'), // no ironing|top|topmost|solid (top
|
|
203
|
+
// New in stage 6 (derived from the matching schema keys). The MM parameters (extruder_count/mm_group_split) are
|
|
204
|
+
// injected at onSlice time from the viewer's per-object extruder assignment (not included here).
|
|
205
|
+
ironing_type: String(S('ironing_type') ?? 'no ironing'), // no ironing|top|topmost|solid (the top family = on)
|
|
121
206
|
ironing_spacing: num('ironing_spacing', 0.1),
|
|
122
207
|
ironing_flow: num('ironing_flow', 10),
|
|
123
208
|
ironing_speed: num('ironing_speed', 20),
|
|
124
209
|
reduce_crossing_wall: bool('reduce_crossing_wall', false),
|
|
125
210
|
max_volumetric_extrusion_rate_slope: num('max_volumetric_extrusion_rate_slope', 0),
|
|
126
|
-
// 7
|
|
211
|
+
// Stage 7: wall generator classic|arachne (arachne = the real ported OrcaSlicer WallToolPaths, variable width)
|
|
127
212
|
wall_generator: (String(S('wall_generator') ?? 'classic') === 'arachne') ? 'arachne' : 'classic',
|
|
128
|
-
// support_density
|
|
213
|
+
// support_density has no matching schema key, so the kernel default (0.15) is used; scarf_length likewise uses the kernel default (10mm)
|
|
129
214
|
}
|
|
130
215
|
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Runs slicing off the main thread (non-blocking UI) plus stage-30 layer streaming.
|
|
2
2
|
// Vite module worker: new Worker(new URL('./slicer.worker.js', import.meta.url), { type: 'module' }).
|
|
3
|
-
// SINGLE_FILE
|
|
3
|
+
// SINGLE_FILE means the wasm is inlined into slicer_core.js -> no external fetch from the worker either.
|
|
4
4
|
//
|
|
5
|
-
// 30
|
|
6
|
-
// (Float32Array
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
5
|
+
// Stage 30 (OOM tolerance): set_layer_sink lets the kernel emit layers as it produces them; each is transferred to main
|
|
6
|
+
// immediately (Float32Array buffers moved -> the worker copy is released at once) and the kernel frees that layer buffer
|
|
7
|
+
// from the heap. The resident result (the full gw.s + the full layersArr) disappears, so the WASM heap peak drops sharply
|
|
8
|
+
// -> large models avoid OOM. The final 'done' carries stats only.
|
|
9
|
+
// In economy mode (params.economy) only G-code chunks are emitted, without toolpaths (finishes without a preview).
|
|
10
|
+
// The MM / real-PE paths fall back to batch mode.
|
|
11
|
+
// Automatic multithreaded kernel selection: with crossOriginIsolated (sites serving COOP/COEP) it uses mt (-pthread, PASS1
|
|
12
|
+
// layers in parallel — measured 2.2x), otherwise st (zero-config). The dynamic import makes bundlers emit both as chunks,
|
|
13
|
+
// but only one is loaded at runtime. On mt init failure (SAB blocked, …) it falls back to st.
|
|
12
14
|
const loadCore = async () => {
|
|
13
15
|
const isolated = typeof crossOriginIsolated !== 'undefined' && crossOriginIsolated
|
|
14
16
|
if (isolated) {
|
|
@@ -16,7 +18,7 @@ const loadCore = async () => {
|
|
|
16
18
|
const M = await (await import('./slicer_core.mt.js')).default()
|
|
17
19
|
console.info('[slicer.worker] core: mt (threads)')
|
|
18
20
|
return M
|
|
19
|
-
} catch (e) { console.warn('[slicer.worker] mt
|
|
21
|
+
} catch (e) { console.warn('[slicer.worker] mt load failed — falling back to st:', e) }
|
|
20
22
|
}
|
|
21
23
|
const M = await (await import('./slicer_core.js')).default()
|
|
22
24
|
console.info('[slicer.worker] core: st')
|
|
@@ -30,34 +32,34 @@ self.onmessage = async (e) => {
|
|
|
30
32
|
try {
|
|
31
33
|
if (!modPromise) modPromise = loadCore()
|
|
32
34
|
const Module = await modPromise
|
|
33
|
-
//
|
|
35
|
+
// Warmup: only load the kernel (+ spawn the mt pthread pool) ahead of time — removes the perceived load on the first slice
|
|
34
36
|
if (d.cmd === 'warmup') { self.postMessage({ type: 'warm' }); return }
|
|
35
|
-
// 20
|
|
37
|
+
// Stage 20: manual support painting — selector state persists in this worker Module (slicing uses the same Module).
|
|
36
38
|
if (d.cmd === 'prepare') { Module.selector_prepare(new Uint8Array(d.stl)); self.postMessage({ type: 'prepared', facets: Module.selector_facet_count() }); return }
|
|
37
39
|
if (d.cmd === 'paint') { Module.selector_paint(d.facet, d.hx, d.hy, d.hz, d.cx, d.cy, d.cz, d.radius, d.enforcer);
|
|
38
40
|
self.postMessage({ type: 'painted', enf: Module.selector_painted_count(true), blk: Module.selector_painted_count(false) }); return }
|
|
39
41
|
if (d.cmd === 'clear') { Module.selector_clear(); self.postMessage({ type: 'painted', enf: 0, blk: 0 }); return }
|
|
40
42
|
if (d.cmd === 'overlay') { self.postMessage({ type: 'overlay', enf: Array.from(Module.selector_overlay(true)), blk: Array.from(Module.selector_overlay(false)) }); return }
|
|
41
43
|
|
|
42
|
-
if (d.stall) return // 30
|
|
43
|
-
// mt(SAB):
|
|
44
|
-
// UI
|
|
44
|
+
if (d.stall) return // stage-30 test hook: simulate a hang -> verifies the main-thread watchdog fires (not set in production)
|
|
45
|
+
// mt (SAB): share the address of the real support progress counter (u32) with main once — even while the kernel is
|
|
46
|
+
// blocked inside C++, the UI thread polls the SAB directly to show support progress. st (non-shared buffer) is unsupported -> not sent.
|
|
45
47
|
if (!self.__supSabSent) {
|
|
46
48
|
self.__supSabSent = true
|
|
47
49
|
try {
|
|
48
|
-
// Module.HEAPU8
|
|
50
|
+
// Module.HEAPU8 is not exposed by the emscripten 6.x glue — obtain the buffer via embind typed_memory_view
|
|
49
51
|
const v = Module.sup_progress_view && Module.sup_progress_view()
|
|
50
52
|
const c = Module.cancel_flag_view && Module.cancel_flag_view()
|
|
51
53
|
if (v && v.buffer instanceof SharedArrayBuffer)
|
|
52
54
|
self.postMessage({ type: 'supsab', buf: v.buffer, ptr: v.byteOffset, cancelPtr: c ? c.byteOffset : 0 })
|
|
53
55
|
} catch {}
|
|
54
56
|
}
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
+
// Default: slice. Register the layer sink -> the kernel calls back per layer (z, idx, gcodeChunk, pathsF32, widthsF32).
|
|
58
|
+
// Each layer is transferred to main immediately (toolpath buffers moved) -> the worker copy is freed -> heap headroom before the next layer.
|
|
57
59
|
const onProgress = (done, total) => self.postMessage({ type: 'progress', done, total })
|
|
58
60
|
Module.set_layer_sink((z, idx, gcode, paths, widths) => {
|
|
59
61
|
const transfer = []
|
|
60
|
-
if (paths && paths.buffer) transfer.push(paths.buffer) //
|
|
62
|
+
if (paths && paths.buffer) transfer.push(paths.buffer) // economy mode yields empty arrays (no .buffer) -> nothing to transfer
|
|
61
63
|
if (widths && widths.buffer) transfer.push(widths.buffer)
|
|
62
64
|
self.postMessage({ type: 'layer', z, idx, gcode, paths, widths }, transfer)
|
|
63
65
|
})
|
|
@@ -65,10 +67,10 @@ self.onmessage = async (e) => {
|
|
|
65
67
|
try { r = Module.slice(new Uint8Array(d.stl), d.params, onProgress) }
|
|
66
68
|
finally { Module.clear_layer_sink() }
|
|
67
69
|
if (r && r.error) { self.postMessage({ type: 'error', error: String(r.error) }); return }
|
|
68
|
-
// streamed=true
|
|
70
|
+
// streamed=true -> g-code/layers were already emitted as 'layer' (result holds stats only). batch/MM keep them in result.
|
|
69
71
|
self.postMessage({ type: 'done', result: r })
|
|
70
72
|
} catch (err) {
|
|
71
|
-
// WASM abort("memory access out of bounds")
|
|
73
|
+
// Includes the WASM abort("memory access out of bounds") — the main thread's OOM ladder decides on re-creation / economy retry.
|
|
72
74
|
self.postMessage({ type: 'error', error: String((err && err.message) || err) })
|
|
73
75
|
}
|
|
74
76
|
}
|
|
Binary file
|
|
Binary file
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// 25
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Stage 25 S5.1: partial translation of toggle-rules — upstream C++ toggle_options conditions (enable_if) -> JS.
|
|
2
|
+
// Scope: among the rules touching keys the viewer handles, only those "fully" translatable with the hardcoded locals below.
|
|
3
|
+
// Untranslatable ones (unknown locals, enum comparisons, …) fail open (stay enabled) — avoids wrongly disabling (honest partial implementation).
|
|
4
|
+
// Translating all 907 keys / 231 rules is out of scope (noted in the README). Reference: toggle_print_fff_options.locals in toggle-rules.json.
|
|
5
5
|
import { toggleRules as toggles, schema } from './data.js'
|
|
6
6
|
|
|
7
|
-
//
|
|
7
|
+
// Settings value accessor (settings map first, schema default otherwise). Percentages ("15%") become numbers.
|
|
8
8
|
export function makeCfg(settings) {
|
|
9
9
|
const val = k => {
|
|
10
10
|
let v = settings ? settings[k] : undefined
|
|
@@ -20,7 +20,7 @@ export function makeCfg(settings) {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
//
|
|
23
|
+
// Upstream locals translated inline (viewer-related conditions only). Each value is a boolean.
|
|
24
24
|
function makeLocals(cfg) {
|
|
25
25
|
const have_raft = cfg.int('raft_layers') > 0
|
|
26
26
|
const has_spiral_vase = cfg.bool('spiral_mode')
|
|
@@ -41,24 +41,24 @@ function makeLocals(cfg) {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
// enable_if(
|
|
44
|
+
// Evaluates enable_if (up to the first comma) to a boolean. Returns null (= stays enabled) when not fully translatable.
|
|
45
45
|
export function evalEnableIf(expr, locals, cfg) {
|
|
46
46
|
let s = String(expr).split(',')[0].trim()
|
|
47
47
|
if (!s) return null
|
|
48
|
-
//
|
|
48
|
+
// Direct config access pattern -> value substitution
|
|
49
49
|
s = s.replace(/config->opt_bool\(\s*"([^"]+)"\s*\)/g, (_, k) => cfg.bool(k) ? 'true' : 'false')
|
|
50
50
|
s = s.replace(/config->opt_int\(\s*"([^"]+)"\s*\)/g, (_, k) => String(cfg.int(k)))
|
|
51
51
|
s = s.replace(/config->opt_float\(\s*"([^"]+)"\s*\)/g, (_, k) => String(cfg.float(k)))
|
|
52
|
-
//
|
|
52
|
+
// Substitute known locals (longest name first — prevents partial matches)
|
|
53
53
|
for (const name of Object.keys(locals).sort((a, b) => b.length - a.length))
|
|
54
54
|
s = s.replace(new RegExp('\\b' + name + '\\b', 'g'), locals[name] ? 'true' : 'false')
|
|
55
|
-
//
|
|
55
|
+
// Check the remaining string is a safe boolean expression (only true/false/numbers/operators/parens) — otherwise unknown -> null
|
|
56
56
|
const stripped = s.replace(/\btrue\b|\bfalse\b/g, '1')
|
|
57
57
|
if (!/^[\s()!<>=&|0-9.]*$/.test(stripped)) return null
|
|
58
58
|
try { return !!Function('"use strict";return (' + s + ')')() } catch { return null }
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
//
|
|
61
|
+
// Map of keys disabled under the current settings { key: condition }. Only rules that are unambiguously false.
|
|
62
62
|
export function disabledKeys(cfg) {
|
|
63
63
|
const locals = makeLocals(cfg)
|
|
64
64
|
const groups = ['toggle_print_fff_options', 'toggle_filament_options', 'toggle_printer_options',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "three-slicer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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",
|
|
@@ -78,6 +78,8 @@
|
|
|
78
78
|
"data/ui-tree.json",
|
|
79
79
|
"data/toggle-rules.json",
|
|
80
80
|
"data/invalidation-map.json",
|
|
81
|
+
"data/printers.json",
|
|
82
|
+
"data/processes.js",
|
|
81
83
|
"README.md",
|
|
82
84
|
"LICENSE.txt"
|
|
83
85
|
],
|
|
@@ -86,7 +88,7 @@
|
|
|
86
88
|
"url": "git+https://github.com/kimgh06/Web_Three_Slicer.git",
|
|
87
89
|
"directory": "packages"
|
|
88
90
|
},
|
|
89
|
-
"homepage": "https://
|
|
91
|
+
"homepage": "https://slicer.kimgh06.com",
|
|
90
92
|
"keywords": [
|
|
91
93
|
"3d-printing",
|
|
92
94
|
"slicer",
|
package/types/components.d.ts
CHANGED
|
@@ -3,15 +3,15 @@ import type * as React from 'react'
|
|
|
3
3
|
import type { SlicerSettings } from './settings-keys.d.ts'
|
|
4
4
|
|
|
5
5
|
export interface SettingsPanelProps {
|
|
6
|
-
/**
|
|
6
|
+
/** Sparse map — only edited keys. Missing keys fall back to the config-schema default. */
|
|
7
7
|
settings: SlicerSettings
|
|
8
|
-
/** React setState
|
|
8
|
+
/** React setState shape. The only channel through which state leaves the component. */
|
|
9
9
|
setSettings: React.Dispatch<React.SetStateAction<SlicerSettings>>
|
|
10
|
-
/**
|
|
10
|
+
/** Label click — for deep links / detail views. Omit for a plain label (zero router coupling). */
|
|
11
11
|
onOptionOpen?: (optKey: string) => void
|
|
12
|
-
/**
|
|
12
|
+
/** When embedding inside another panel */
|
|
13
13
|
embedded?: boolean
|
|
14
|
-
/**
|
|
14
|
+
/** Replace schema-driven widgets per key (e.g. `{ printable_area: MyBedEditor }`) */
|
|
15
15
|
customWidgets?: Record<string, React.ComponentType<any>>
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// three-slicer/data/config-schema.json —
|
|
2
|
-
// ponytail:
|
|
3
|
-
//
|
|
1
|
+
// three-slicer/data/config-schema.json — the 907 option definitions extracted from upstream OrcaSlicer.
|
|
2
|
+
// ponytail: values are not narrowed to literals. Inferring 907 keys x 33 fields as literals crawls in consumer tsc.
|
|
3
|
+
// Use SlicerSettings (three-slicer/settings) when you need per-key accuracy.
|
|
4
4
|
|
|
5
|
-
/**
|
|
5
|
+
/** The coXxx type tag from the upstream ConfigOptionDef */
|
|
6
6
|
export type ConfigOptionType =
|
|
7
7
|
| 'coBool' | 'coBools'
|
|
8
8
|
| 'coInt' | 'coInts'
|
|
@@ -13,10 +13,10 @@ export type ConfigOptionType =
|
|
|
13
13
|
| 'coEnum' | 'coEnums'
|
|
14
14
|
| 'coPoint' | 'coPoints' | 'coPointsGroups'
|
|
15
15
|
|
|
16
|
-
/** 907
|
|
16
|
+
/** Of the 907 keys, only `type` and `defined_in` always exist. Everything else is optional. */
|
|
17
17
|
export interface ConfigOption {
|
|
18
18
|
type: ConfigOptionType
|
|
19
|
-
/**
|
|
19
|
+
/** Name of the source function it was extracted from (e.g. 'init_common_params') */
|
|
20
20
|
defined_in: string
|
|
21
21
|
line?: number
|
|
22
22
|
mode?: string
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
// three-slicer/data/invalidation-map.json —
|
|
2
|
-
//
|
|
1
|
+
// three-slicer/data/invalidation-map.json — setting key change -> invalidated slice steps.
|
|
2
|
+
// Top-level key = the kind of invalidation target (print/object/filament, etc.).
|
|
3
3
|
|
|
4
4
|
export interface InvalidationRule {
|
|
5
|
-
/**
|
|
5
|
+
/** The config-schema keys this rule reacts to */
|
|
6
6
|
keys: string[]
|
|
7
|
-
/**
|
|
7
|
+
/** The invalidated steps (e.g. 'psSkirtBrim', 'posSlice') */
|
|
8
8
|
steps: string[]
|
|
9
|
-
/**
|
|
9
|
+
/** Special handling that cannot be expressed as a step */
|
|
10
10
|
special: string[]
|
|
11
11
|
line?: number
|
|
12
12
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// printers.json — per-printer settings extracted from OrcaSlicer's resources/profiles/<Vendor>/machine/.
|
|
2
|
+
// Column layout: `keys` names the columns once and each entry in `sets` is a positional row, because
|
|
3
|
+
// repeating the key names per printer costs more than all the values together. Rows are deduplicated —
|
|
4
|
+
// a printer's nozzle variants usually share one row — so a printer holds an index rather than its own copy.
|
|
5
|
+
// Prefer `printerSettings()` from three-slicer/settings over decoding this by hand.
|
|
6
|
+
|
|
7
|
+
/** A positional row aligned to `keys`; `null` where the profile chain never set that option. */
|
|
8
|
+
export type PrinterRow = (unknown | null)[]
|
|
9
|
+
|
|
10
|
+
/** `[nozzle diameter, index into sets, model name without the nozzle suffix]` */
|
|
11
|
+
export type PrinterEntry = [string, number, string]
|
|
12
|
+
|
|
13
|
+
export interface PrinterData {
|
|
14
|
+
/** Option keys, in column order — the same keys the settings panel edits */
|
|
15
|
+
keys: string[]
|
|
16
|
+
/** Deduplicated value rows */
|
|
17
|
+
sets: PrinterRow[]
|
|
18
|
+
/** Vendor -> printer profile name -> entry */
|
|
19
|
+
byVendor: Record<string, Record<string, PrinterEntry>>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare const printers: PrinterData
|
|
23
|
+
export default printers
|