three-slicer 0.1.0

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.
Files changed (54) hide show
  1. package/LICENSE.txt +661 -0
  2. package/README.md +44 -0
  3. package/components/README.md +39 -0
  4. package/components/dist/SettingsPanel.js +180 -0
  5. package/components/styles.css +84 -0
  6. package/data/README.md +22 -0
  7. package/data/config-schema-builddump.json +12921 -0
  8. package/data/config-schema.json +12685 -0
  9. package/data/invalidation-map.json +631 -0
  10. package/data/toggle-rules.json +2277 -0
  11. package/data/ui-tree.json +1586 -0
  12. package/engine/README.md +52 -0
  13. package/engine/examples/headless.mjs +37 -0
  14. package/engine/index.d.ts +29 -0
  15. package/engine/index.js +45 -0
  16. package/engine/src/settings.js +130 -0
  17. package/engine/src/slicer.worker.js +73 -0
  18. package/engine/src/slicer_core.js +0 -0
  19. package/engine/src/slicer_core.mt.js +0 -0
  20. package/engine/src/toggle_eval.js +78 -0
  21. package/package.json +74 -0
  22. package/viewer/README.md +68 -0
  23. package/viewer/dist/Viewport.js +1108 -0
  24. package/viewer/dist/make_worker.js +9 -0
  25. package/viewer/dist/model_loaders.js +144 -0
  26. package/viewer/dist/toolpath_gpu.js +386 -0
  27. package/viewer/src/Viewport.jsx +1353 -0
  28. package/viewer/src/assets/add.svg +1 -0
  29. package/viewer/src/assets/arrange.svg +1 -0
  30. package/viewer/src/assets/boolean.svg +1 -0
  31. package/viewer/src/assets/cut.svg +1 -0
  32. package/viewer/src/assets/delete.svg +1 -0
  33. package/viewer/src/assets/deleteall.svg +1 -0
  34. package/viewer/src/assets/duplicate.svg +1 -0
  35. package/viewer/src/assets/measure.svg +1 -0
  36. package/viewer/src/assets/mmu.svg +1 -0
  37. package/viewer/src/assets/move.svg +1 -0
  38. package/viewer/src/assets/negative.svg +1 -0
  39. package/viewer/src/assets/onbed.svg +1 -0
  40. package/viewer/src/assets/open.svg +1 -0
  41. package/viewer/src/assets/orient.svg +1 -0
  42. package/viewer/src/assets/paint.svg +1 -0
  43. package/viewer/src/assets/rotate.svg +1 -0
  44. package/viewer/src/assets/scale.svg +1 -0
  45. package/viewer/src/assets/seam.svg +1 -0
  46. package/viewer/src/assets/split.svg +1 -0
  47. package/viewer/src/assets/textmark.svg +1 -0
  48. package/viewer/src/assets/varlayer.svg +1 -0
  49. package/viewer/src/icons.js +23 -0
  50. package/viewer/src/make_worker.js +9 -0
  51. package/viewer/src/model_loaders.js +178 -0
  52. package/viewer/src/shadow_host.jsx +30 -0
  53. package/viewer/src/toolpath_gpu.js +430 -0
  54. package/viewer/styles.css +231 -0
@@ -0,0 +1,52 @@
1
+ # three-slicer
2
+
3
+ Browser/WASM 3D-slicing kernel reverse-engineered from [OrcaSlicer](https://github.com/OrcaSlicer/OrcaSlicer). Slices binary STL to G-code entirely client-side (or in Node) — no server, no native binaries.
4
+
5
+ **No three.js dependency.** This is a headless SDK; the only dependency is `@three-slicer/data` (config metadata JSON). For a ready-made React viewer see `three-slicer/viewer`.
6
+
7
+ Ported from the original C++ sources: Clipper polygon ops, Arachne variable-width walls, real fill patterns (gyroid TPMS / honeycomb / 3D-honeycomb / crosshatch / concentric), tree supports, pressure equalizer, arc fitting (G2/G3), scarf seams, ironing, multi-material with prime tower. Kernel changes are gated by a 120+ invariant test suite and golden byte-identical G-code checks.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm i three-slicer
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```js
18
+ import { createSlicer } from 'three-slicer'
19
+
20
+ const slicer = await createSlicer() // loads the WASM kernel (~3.4 MB, base64-inlined)
21
+
22
+ // Batch: full result at once
23
+ const r = slicer.slice(stlArrayBuffer, { layer_height: 0.2, wall_loops: 2 })
24
+ console.log(r.stats.layers, r.gcode.length)
25
+
26
+ // Streaming: per-layer callbacks, frees each layer (OOM-resilient); result carries stats only
27
+ slicer.slice(stlArrayBuffer, params, {
28
+ onProgress: (done, total) => {},
29
+ onLayer: ({ z, idx, gcode, paths, widths }) => { /* accumulate */ },
30
+ })
31
+ slicer.dispose()
32
+ ```
33
+
34
+ Off-main-thread (browser): `new Worker(engineWorkerURL(), { type: 'module' })` — the worker speaks a streaming protocol (`{type: 'layer' | 'done' | 'error' | 'progress'}`). Subpath `three-slicer/worker` exposes the worker entry for bundler-specific setups.
35
+
36
+ ## Parameters
37
+
38
+ Two levels — use whichever fits:
39
+
40
+ 1. **Kernel params (this package's native contract)** — a flat JSON object of ~50 keys (`layer_height`, `wall_loops`, `infill_density` 0–1, `sparse_infill_pattern`, `enable_support`, `wall_generator: 'classic' | 'arachne'`, …). See `src/settings.js` for the full shape.
41
+ 2. **OrcaSlicer settings map** — a sparse `{schemaKey: value}` map using original OrcaSlicer option keys (907 defined in `@three-slicer/data`); unset keys fall back to schema defaults:
42
+
43
+ ```js
44
+ import { deriveKernelParams } from 'three-slicer/settings'
45
+ slicer.slice(stl, deriveKernelParams({ layer_height: 0.25, sparse_infill_density: 15 }))
46
+ ```
47
+
48
+ **Known limit:** `deriveKernelParams` maps a curated whitelist of ~50 schema keys to kernel params. Editing other schema keys has no slicing effect (they exist for UI/metadata). Vector options use their first element only.
49
+
50
+ ## License
51
+
52
+ AGPL-3.0-or-later (derived from OrcaSlicer). A web app embedding this package must comply with AGPL — including offering its source to network users.
@@ -0,0 +1,37 @@
1
+ // three-slicer headless SDK example + independence proof: drive the engine from Node using ONLY the
2
+ // public API (no UI, no viewer). Generates a 20mm cube STL, slices it, prints stats + G-code length.
3
+ // run: node packages/engine/examples/headless.mjs
4
+ import { createSlicer } from 'three-slicer' // 워크스페이스 심링크로 해석 — 외부 소비자와 동일한 경로
5
+
6
+ // minimal binary-STL writer for a box (public API takes a binary-STL ArrayBuffer)
7
+ function boxTris(ox, oy, oz, sx, sy, sz) {
8
+ const c = [[0,0,0],[sx,0,0],[sx,sy,0],[0,sy,0],[0,0,sz],[sx,0,sz],[sx,sy,sz],[0,sy,sz]].map(v => [v[0]+ox, v[1]+oy, v[2]+oz])
9
+ const q = (a,b,cc,d) => [[c[a],c[b],c[cc]],[c[a],c[cc],c[d]]]
10
+ return [...q(0,1,2,3), ...q(4,5,6,7), ...q(0,1,5,4), ...q(1,2,6,5), ...q(2,3,7,6), ...q(3,0,4,7)]
11
+ }
12
+ function trisToSTL(tris) {
13
+ const buf = Buffer.alloc(84 + tris.length*50); buf.writeUInt32LE(tris.length, 80); let off = 84
14
+ for (const t of tris) { off += 12; for (const p of t){ buf.writeFloatLE(p[0],off); buf.writeFloatLE(p[1],off+4); buf.writeFloatLE(p[2],off+8); off += 12 } buf.writeUInt16LE(0,off); off += 2 }
15
+ return buf
16
+ }
17
+ const cube = trisToSTL(boxTris(-10,-10,0, 20,20,20))
18
+ const params = {
19
+ layer_height: 0.2, first_layer_height: 0.2, line_width: 0.42, wall_loops: 2, infill_density: 0.15,
20
+ nozzle_diameter: 0.4, filament_diameter: 1.75, print_speed: 60, first_layer_speed: 20, travel_speed: 150,
21
+ nozzle_temp: 210, bed_temp: 60, top_shell_layers: 3, bottom_shell_layers: 3, skirt_loops: 1,
22
+ }
23
+
24
+ const slicer = await createSlicer()
25
+
26
+ // (a) batch slice
27
+ const r = slicer.slice(cube, params)
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
+
30
+ // (b) streaming slice via onLayer (30단계) — assemble G-code from per-layer chunks
31
+ let chunks = 0, gbytes = 0
32
+ const rs = slicer.slice(cube, params, { onLayer: ({ gcode }) => { chunks++; gbytes += gcode.length } })
33
+ console.log(`stream: layers=${chunks} streamed=${rs.stats.streamed} assembled-gcode=${gbytes} chars`)
34
+
35
+ if (r.gcode.length > 0 && chunks > 0) console.log('OK — three-slicer drives headlessly via the public API')
36
+ else { console.error('FAIL'); process.exit(1) }
37
+ slicer.dispose()
@@ -0,0 +1,29 @@
1
+ // Type definitions for @three-slicer/engine
2
+ export interface SliceLayer { z: number; idx: number; gcode: string; paths: Float32Array; widths: Float32Array }
3
+ export interface SliceCallbacks {
4
+ onProgress?: (done: number, total: number) => void
5
+ onLayer?: (layer: SliceLayer) => void
6
+ }
7
+ export interface SliceStats {
8
+ layers: number; model_layers: number; raft_layers: number;
9
+ path_segments: number; filament_mm: number; time_estimate: number;
10
+ streamed?: boolean; economy?: boolean; [k: string]: unknown
11
+ }
12
+ export interface SliceResult { gcode?: string; stats: SliceStats; layers?: unknown[]; error?: string }
13
+ export interface PaintArgs {
14
+ facet: number; hx: number; hy: number; hz: number;
15
+ cx: number; cy: number; cz: number; radius: number; enforcer: boolean
16
+ }
17
+ export interface Slicer {
18
+ slice(stl: ArrayBuffer | Uint8Array, params: object | string, cb?: SliceCallbacks): SliceResult
19
+ paintPrepare(stl: ArrayBuffer | Uint8Array): number
20
+ paint(args: PaintArgs): { enf: number; blk: number }
21
+ paintClear(): void
22
+ overlay(enforcer: boolean): number[]
23
+ heapSize(): number
24
+ module: unknown
25
+ dispose(): void
26
+ }
27
+ export function createSlicer(): Promise<Slicer>
28
+ export function engineWorkerURL(): URL
29
+ export default createSlicer
@@ -0,0 +1,45 @@
1
+ // three-slicer — browser/WASM 3D-slicing SDK (reverse-engineered from OrcaSlicer).
2
+ // Public API. Node-safe (imports only the pure-JS WASM factory; no JSON so it loads in plain Node ESM).
3
+ // Schema-driven helpers (deriveKernelParams etc.) live in the "three-slicer/settings" subpath because
4
+ // they import JSON — use them in a bundler (vite) or with Node JSON import attributes.
5
+ import createModule from './src/slicer_core.js'
6
+
7
+ const u8 = (b) => (b instanceof Uint8Array ? b : new Uint8Array(b))
8
+
9
+ // Off-main-thread worker URL for the browser: `new Worker(engineWorkerURL(), { type: 'module' })`.
10
+ // The worker speaks the 30단계 streaming protocol ({type:'layer'|'done'|'error'|'progress'}).
11
+ export const engineWorkerURL = () => new URL('./src/slicer.worker.js', import.meta.url)
12
+
13
+ // createSlicer(): load the WASM kernel and return a handle. Works in Node and the browser main thread.
14
+ // slice(stl, params, { onProgress, onLayer }):
15
+ // - stl: ArrayBuffer|Uint8Array (binary STL)
16
+ // - params: kernel-params object OR JSON string (see web/GUIDE.md; use deriveKernelParams to build from UI settings)
17
+ // - onProgress(done,total): per-layer progress
18
+ // - onLayer({z,idx,gcode,paths,widths}): 30단계 streaming — emits + frees each layer; when set, the
19
+ // returned result carries stats only (assemble gcode/layers from the callbacks). When omitted, the
20
+ // result carries the whole { gcode, stats, layers }.
21
+ // paintPrepare/paint/paintClear/overlay: manual support enforcer/blocker painting (TriangleSelector).
22
+ // heapSize(): current WASM heap bytes (peak, monotonic). module: escape hatch. dispose(): drop the module.
23
+ export async function createSlicer() {
24
+ const M = await createModule()
25
+ return {
26
+ slice(stl, params, { onProgress, onLayer } = {}) {
27
+ const p = typeof params === 'string' ? params : JSON.stringify(params || {})
28
+ if (onLayer) M.set_layer_sink((z, idx, gcode, paths, widths) => onLayer({ z, idx, gcode, paths, widths }))
29
+ try { return M.slice(u8(stl), p, onProgress || (() => {})) }
30
+ finally { if (onLayer) M.clear_layer_sink() }
31
+ },
32
+ paintPrepare(stl) { M.selector_prepare(u8(stl)); return M.selector_facet_count() },
33
+ paint(a) {
34
+ M.selector_paint(a.facet, a.hx, a.hy, a.hz, a.cx, a.cy, a.cz, a.radius, a.enforcer)
35
+ return { enf: M.selector_painted_count(true), blk: M.selector_painted_count(false) }
36
+ },
37
+ paintClear() { M.selector_clear() },
38
+ overlay(enforcer) { return Array.from(M.selector_overlay(!!enforcer)) },
39
+ heapSize() { return M.heap_size() },
40
+ module: M,
41
+ dispose() { /* emscripten module is GC'd when dropped; no explicit teardown required */ },
42
+ }
43
+ }
44
+
45
+ export default createSlicer
@@ -0,0 +1,130 @@
1
+ // 우측 설정 패널(편집 가능)의 실제 값 → 커널 슬라이스 파라미터 매핑.
2
+ // - 설정 상태는 sparse 맵(key→value): 편집한 키만 저장, 없으면 config-schema default.
3
+ // - 벡터형(coFloats/coInts 등)은 첫 원소만 사용/편집(단순화).
4
+ import schema from 'three-slicer/data/config-schema.json'
5
+
6
+ export function schemaDefault(key) { return schema[key]?.default }
7
+ export function settingRaw(settings, key) { return (settings && key in settings) ? settings[key] : schemaDefault(key) }
8
+ // 스칼라(벡터면 [0]) 로 정규화
9
+ export function settingScalar(settings, key) { const v = settingRaw(settings, key); return Array.isArray(v) ? v[0] : v }
10
+
11
+ // 8단계: 이식된 실제 Fill 패턴(gyroid TPMS/honeycomb/3dhoneycomb/crosshatch/concentric) + 기존 근사 패턴
12
+ const KERNEL_PATTERNS = ['rectilinear', 'grid', 'triangles', 'zigzag', 'gyroid', 'gyroid_approx',
13
+ 'honeycomb', '3dhoneycomb', 'crosshatch', 'concentric']
14
+ const KERNEL_SEAMS = ['nearest', 'aligned', 'back', 'random']
15
+
16
+ // 우측 패널 설정값 → 커널 파라미터 (스키마 키에서 유도)
17
+ export function deriveKernelParams(settings) {
18
+ const S = k => settingScalar(settings, k)
19
+ const num = (k, d) => { const v = Number(S(k)); return Number.isFinite(v) ? v : d }
20
+ const bool = (k, d) => { const v = settingRaw(settings, k); return typeof v === 'boolean' ? v : (Array.isArray(v) ? !!v[0] : (v == null ? d : !!v)) }
21
+
22
+ let line_width = num('line_width', 0); if (!line_width) line_width = 0.42 // 0=auto → 기본 0.42
23
+
24
+ let pat = String(S('sparse_infill_pattern') ?? 'rectilinear')
25
+ if (!KERNEL_PATTERNS.includes(pat)) pat = 'rectilinear' // 커널 미지원 패턴 → rectilinear
26
+
27
+ let seam = String(S('seam_position') ?? 'back')
28
+ if (seam === 'aligned_back') seam = 'back'
29
+ if (!KERNEL_SEAMS.includes(seam)) seam = 'back'
30
+
31
+ // 서포트 스타일: 스키마 enum(default/grid/snug/organic/tree_slim/tree_strong/tree_hybrid)
32
+ // → 커널 grid|tree|tree_lite. 18단계부터 tree/organic 계열은 실 오가닉 TreeSupport('tree',
33
+ // generate_tree_support_3D → 실 type5 브랜치 툴패스)로 매핑, 그 외 grid.
34
+ const styleRaw = String(S('support_style') ?? 'default')
35
+ const support_style = /tree|organic/i.test(styleRaw) ? 'tree' : 'grid'
36
+
37
+ // 베드: printable_area 첫 사각형의 바운딩박스
38
+ const pa = settingRaw(settings, 'printable_area')
39
+ let bed_width = 256, bed_depth = 256
40
+ if (Array.isArray(pa) && pa.length) {
41
+ const xs = pa.map(p => p[0]), ys = pa.map(p => p[1])
42
+ bed_width = Math.max(...xs) - Math.min(...xs); bed_depth = Math.max(...ys) - Math.min(...ys)
43
+ }
44
+
45
+ // 21단계: 피처별 폭 — 원본(문자열 "120%" 포함) 그대로 커널로 전달. 미편집 키는 생략 → 커널이 auto(=line_width) 유도.
46
+ // 0=자동 시맨틱은 커널 resolve_lw(원본 Flow 산식)이 처리. 기본값 불변(모든 피처 미설정 → line_width).
47
+ const widths = {}
48
+ for (const k of ['outer_wall_line_width','inner_wall_line_width','top_surface_line_width',
49
+ 'sparse_infill_line_width','internal_solid_infill_line_width','initial_layer_line_width']) {
50
+ const v = S(k); if (v != null && v !== '') widths[k] = v
51
+ }
52
+ return {
53
+ ...widths,
54
+ layer_height: num('layer_height', 0.2),
55
+ first_layer_height: num('initial_layer_print_height', 0.2),
56
+ line_width,
57
+ wall_loops: num('wall_loops', 2),
58
+ infill_density: num('sparse_infill_density', 20) / 100, // % → 0~1
59
+ sparse_infill_pattern: pat,
60
+ infill_angle: num('infill_direction', 45),
61
+ top_shell_layers: num('top_shell_layers', 4),
62
+ bottom_shell_layers: num('bottom_shell_layers', 3),
63
+ seam_position: seam,
64
+ skirt_loops: num('skirt_loops', 1),
65
+ skirt_distance: num('skirt_distance', 2),
66
+ skirt_height: num('skirt_height', 1), // 33단계: 커널이 첫 레이어 고정이었음
67
+ brim_width: num('brim_width', 0),
68
+ brim_object_gap: num('brim_object_gap', 0), // 33단계: 커널이 w*0.5 고정이었음
69
+ retract_length: num('retraction_length', 0.8), // 벡터[0]
70
+ retraction_minimum_travel: num('retraction_minimum_travel', 2), // 33단계: 커널 상수 2.0 이었음
71
+ gcode_resolution: num('resolution', 0.01), // 33단계: 트리 서포트 경로 단순화 허용오차
72
+ retract_speed: num('retraction_speed', 30),
73
+ z_hop: num('z_hop', 0.4),
74
+ travel_speed: num('travel_speed', 120),
75
+ first_layer_speed: num('initial_layer_speed', 30),
76
+ print_speed: num('outer_wall_speed', 60),
77
+ nozzle_diameter: num('nozzle_diameter', 0.4),
78
+ filament_diameter: num('filament_diameter', 1.75),
79
+ flow_ratio: num('filament_flow_ratio', 1.0),
80
+ nozzle_temp: num('nozzle_temperature', 200),
81
+ bed_temp: num('hot_plate_temp', 45), // bed_temperature 키 없음 → hot_plate_temp
82
+ bed_width, bed_depth,
83
+ enable_support: bool('enable_support', false),
84
+ support_threshold_angle: num('support_threshold_angle', 30),
85
+ support_top_z_distance: num('support_top_z_distance', 0.2),
86
+ support_bottom_z_distance: num('support_bottom_z_distance', 0.2), // 32단계: 서포트 바닥 z-gap(기본 0.2=현행 등가)
87
+ support_xy_distance: num('support_object_xy_distance', 0.35),
88
+ support_interface_top_layers: num('support_interface_top_layers', 2),
89
+ // 33단계: 커널 하드코딩 제거로 신설된 서포트 키 배선(원본 스키마 키 그대로)
90
+ support_angle: num('support_angle', 0),
91
+ support_base_pattern: String(S('support_base_pattern') ?? 'default'),
92
+ support_interface_pattern: String(S('support_interface_pattern') ?? 'auto'),
93
+ support_interface_spacing: num('support_interface_spacing', 0.5),
94
+ support_base_pattern_spacing: num('support_base_pattern_spacing', 2.5),
95
+ support_remove_small_overhang: bool('support_remove_small_overhang', true),
96
+ bridge_no_support: bool('bridge_no_support', false),
97
+ support_expansion: num('support_expansion', 0),
98
+ support_threshold_overlap: (() => { const v = settingRaw(settings, 'support_threshold_overlap')
99
+ if (typeof v === 'string' && v.trim().endsWith('%')) return parseFloat(v) / 100
100
+ const n = Number(v); return Number.isFinite(n) ? n : 0.5 })(), // "50%" → 0.5 (압출폭 비율)
101
+ support_on_build_plate_only: bool('support_on_build_plate_only', false),
102
+ support_interface_bottom_layers: num('support_interface_bottom_layers', 0),
103
+ raft_layers: num('raft_layers', 0),
104
+ raft_expansion: num('raft_expansion', 1.5), // 33단계: 커널이 +3.0 고정이었음
105
+ raft_contact_distance: num('raft_contact_distance', 0.1), // 33단계: 커널에 미반영이었음
106
+ fan_speed: num('fan_max_speed', 100), // fan_speed 키 없음 → fan_max_speed
107
+ close_fan_the_first_x_layers: num('close_fan_the_first_x_layers', 1),
108
+ full_fan_speed_layer: num('full_fan_speed_layer', 0),
109
+ slow_down_layer_time: num('slow_down_layer_time', 5),
110
+ enable_arc_fitting: bool('enable_arc_fitting', false),
111
+ spiral_mode: bool('spiral_mode', false),
112
+ // 5단계 신규 (스키마 대응 키에서 유도)
113
+ seam_slope_type: String(S('seam_slope_type') ?? 'none'), // none|external|all → scarf 심
114
+ enable_pressure_advance: bool('enable_pressure_advance', false),
115
+ pressure_advance: num('pressure_advance', 0.02),
116
+ support_style, // grid|tree_lite (위에서 매핑)
117
+ bridge_speed: num('bridge_speed', 25),
118
+ // 6단계 신규 (스키마 대응 키에서 유도). MM 파라미터(extruder_count/mm_group_split)는
119
+ // 뷰어 오브젝트별 익스트루더 지정에서 onSlice 시 주입(여기선 미포함).
120
+ ironing_type: String(S('ironing_type') ?? 'no ironing'), // no ironing|top|topmost|solid (top류=on)
121
+ ironing_spacing: num('ironing_spacing', 0.1),
122
+ ironing_flow: num('ironing_flow', 10),
123
+ ironing_speed: num('ironing_speed', 20),
124
+ reduce_crossing_wall: bool('reduce_crossing_wall', false),
125
+ max_volumetric_extrusion_rate_slope: num('max_volumetric_extrusion_rate_slope', 0),
126
+ // 7단계: 벽 생성기 classic|arachne (arachne = 이식된 실제 OrcaSlicer WallToolPaths, 가변폭)
127
+ wall_generator: (String(S('wall_generator') ?? 'classic') === 'arachne') ? 'arachne' : 'classic',
128
+ // support_density 는 스키마에 대응 키가 없어 커널 기본(0.15), scarf_length 도 커널 기본(10mm) 사용
129
+ }
130
+ }
@@ -0,0 +1,73 @@
1
+ // 슬라이스를 메인 스레드 밖에서 실행 (UI 논블록) + 30단계 레이어 스트리밍.
2
+ // Vite module worker: new Worker(new URL('./slicer.worker.js', import.meta.url), { type: 'module' }).
3
+ // SINGLE_FILE 이라 slicer_core.js 안에 wasm 이 인라인 → worker 에서도 외부 fetch 없음.
4
+ //
5
+ // 30단계(OOM 내성): set_layer_sink 로 커널이 레이어를 만드는 대로 방출받아 즉시 메인으로 transfer 하고
6
+ // (Float32Array 버퍼 이전 → worker 사본 즉시 해제) 커널은 그 레이어 버퍼를 힙에서 해제한다. 결과 상주
7
+ // (전체 gw.s + 전체 layersArr)가 사라져 WASM 힙 피크가 급감 → 대형 모델 OOM 회피. 최종 'done' 은 stats 만.
8
+ // 절약 모드(params.economy)면 툴패스 없이 g-code 청크만 방출(프리뷰 없이 완주). MM/실PE 경로는 배치 폴백.
9
+ // 멀티스레드 커널 자동 선택: crossOriginIsolated(COOP/COEP 제공 사이트)면 mt(-pthread, PASS1 레이어
10
+ // 병렬 — 실측 2.2×), 아니면 st(zero-config). 동적 import 라 번들러는 둘 다 청크로 내지만 런타임엔
11
+ // 하나만 로드. mt 초기화 실패(SAB 차단 등) 시 st 폴백.
12
+ const loadCore = async () => {
13
+ const isolated = typeof crossOriginIsolated !== 'undefined' && crossOriginIsolated
14
+ if (isolated) {
15
+ try {
16
+ const M = await (await import('./slicer_core.mt.js')).default()
17
+ console.info('[slicer.worker] core: mt (threads)')
18
+ return M
19
+ } catch (e) { console.warn('[slicer.worker] mt 로드 실패 — st 폴백:', e) }
20
+ }
21
+ const M = await (await import('./slicer_core.js')).default()
22
+ console.info('[slicer.worker] core: st')
23
+ return M
24
+ }
25
+
26
+ let modPromise = null
27
+
28
+ self.onmessage = async (e) => {
29
+ const d = e.data
30
+ try {
31
+ if (!modPromise) modPromise = loadCore()
32
+ const Module = await modPromise
33
+ // 워밍업: 커널 로드(+mt pthread 풀 스폰)만 미리 수행 — 첫 슬라이스의 체감 로딩 제거
34
+ if (d.cmd === 'warmup') { self.postMessage({ type: 'warm' }); return }
35
+ // 20단계: 수동 서포트 페인팅 — selector 상태는 이 worker Module 에 지속(슬라이스도 같은 Module).
36
+ if (d.cmd === 'prepare') { Module.selector_prepare(new Uint8Array(d.stl)); self.postMessage({ type: 'prepared', facets: Module.selector_facet_count() }); return }
37
+ 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
+ self.postMessage({ type: 'painted', enf: Module.selector_painted_count(true), blk: Module.selector_painted_count(false) }); return }
39
+ if (d.cmd === 'clear') { Module.selector_clear(); self.postMessage({ type: 'painted', enf: 0, blk: 0 }); return }
40
+ if (d.cmd === 'overlay') { self.postMessage({ type: 'overlay', enf: Array.from(Module.selector_overlay(true)), blk: Array.from(Module.selector_overlay(false)) }); return }
41
+
42
+ if (d.stall) return // 30단계 테스트 훅: 무응답(행) 시뮬 → 메인 워치독 발동 검증(프로덕션 미설정)
43
+ // mt(SAB): 서포트 실진행 카운터(u32) 주소를 메인에 1회 공유 — 커널이 C++ 안에 블록된 동안에도
44
+ // UI 스레드가 SAB 를 직접 폴링해 서포트 진행률을 표시한다. st(비공유 버퍼)는 미지원 → 미전송.
45
+ if (!self.__supSabSent) {
46
+ self.__supSabSent = true
47
+ try {
48
+ // Module.HEAPU8 은 emscripten 6.x 글루가 노출하지 않음 — embind typed_memory_view 로 버퍼 획득
49
+ const v = Module.sup_progress_view && Module.sup_progress_view()
50
+ if (v && v.buffer instanceof SharedArrayBuffer)
51
+ self.postMessage({ type: 'supsab', buf: v.buffer, ptr: v.byteOffset })
52
+ } catch {}
53
+ }
54
+ // 기본: 슬라이스. 레이어 싱크 등록 → 커널이 레이어마다 콜백(z, idx, gcodeChunk, pathsF32, widthsF32).
55
+ // 각 레이어를 즉시 메인으로 transfer(툴패스 버퍼 이전) → worker 사본 해제 → 다음 레이어 전 힙 여유.
56
+ const onProgress = (done, total) => self.postMessage({ type: 'progress', done, total })
57
+ Module.set_layer_sink((z, idx, gcode, paths, widths) => {
58
+ const transfer = []
59
+ if (paths && paths.buffer) transfer.push(paths.buffer) // 절약 모드는 빈 배열(.buffer 없음) → 미이전
60
+ if (widths && widths.buffer) transfer.push(widths.buffer)
61
+ self.postMessage({ type: 'layer', z, idx, gcode, paths, widths }, transfer)
62
+ })
63
+ let r
64
+ try { r = Module.slice(new Uint8Array(d.stl), d.params, onProgress) }
65
+ finally { Module.clear_layer_sink() }
66
+ if (r && r.error) { self.postMessage({ type: 'error', error: String(r.error) }); return }
67
+ // streamed=true → g-code/layers 는 'layer' 로 이미 방출됨(result 엔 stats 만). batch/MM 는 result 에 그대로.
68
+ self.postMessage({ type: 'done', result: r })
69
+ } catch (err) {
70
+ // WASM abort("memory access out of bounds") 포함 — 메인의 OOM 사다리가 재생성/절약 재시도 판정.
71
+ self.postMessage({ type: 'error', error: String((err && err.message) || err) })
72
+ }
73
+ }
Binary file
Binary file
@@ -0,0 +1,78 @@
1
+ // 25단계 S5.1: toggle-rules 부분 번역 — 원본 C++ toggle_options 조건식(enable_if) → JS.
2
+ // 범위: 뷰어가 다루는 키에 관련된 규칙 중, 아래 하드코딩 로컬로 "완전히" 번역 가능한 것만 적용.
3
+ // 번역 불가(모르는 로컬/enum 비교 등)는 fail-open(활성 유지) — 잘못된 비활성 방지(정직한 부분 구현).
4
+ // 전체 907키·231규칙 완역은 범위 외(README 기록). 참조: toggle-rules.json 의 toggle_print_fff_options.locals.
5
+ import toggles from 'three-slicer/data/toggle-rules.json'
6
+ import schema from 'three-slicer/data/config-schema.json'
7
+
8
+ // 설정값 접근기 (settings 맵 우선, 없으면 스키마 default). 퍼센트("15%")는 숫자로.
9
+ export function makeCfg(settings) {
10
+ const val = k => {
11
+ let v = settings ? settings[k] : undefined
12
+ if (v === undefined || v === '') { const d = schema[k]?.default; v = Array.isArray(d) ? d[0] : d }
13
+ return v
14
+ }
15
+ const num = k => { const v = val(k); const n = parseFloat(v); return Number.isFinite(n) ? n : 0 }
16
+ return {
17
+ bool: k => { const v = val(k); return v === true || v === 1 || v === '1' || v === 'true' },
18
+ int: k => Math.trunc(num(k)),
19
+ float: k => num(k),
20
+ has: k => k in schema,
21
+ }
22
+ }
23
+
24
+ // 원본 locals 를 인라인 번역(뷰어 관련 조건만). 각 값은 boolean.
25
+ function makeLocals(cfg) {
26
+ const have_raft = cfg.int('raft_layers') > 0
27
+ const has_spiral_vase = cfg.bool('spiral_mode')
28
+ const has_top_shell = cfg.int('top_shell_layers') > 0 || (has_spiral_vase && cfg.int('bottom_shell_layers') > 1)
29
+ const has_bottom_shell = cfg.int('bottom_shell_layers') > 0
30
+ return {
31
+ have_raft,
32
+ have_perimeters: cfg.int('wall_loops') > 0,
33
+ have_infill: cfg.float('sparse_infill_density') > 0,
34
+ has_spiral_vase,
35
+ has_top_shell,
36
+ has_bottom_shell,
37
+ has_solid_infill: has_top_shell || has_bottom_shell,
38
+ have_skirt: cfg.int('skirt_loops') > 0,
39
+ have_support_material: cfg.bool('enable_support') || have_raft,
40
+ have_support_interface: cfg.int('support_interface_top_layers') > 0 || cfg.int('support_interface_bottom_layers') > 0,
41
+ have_prime_tower: cfg.bool('enable_prime_tower'),
42
+ }
43
+ }
44
+
45
+ // enable_if(첫 콤마부) 를 boolean 으로 평가. 완전 번역 불가하면 null(=활성 유지).
46
+ export function evalEnableIf(expr, locals, cfg) {
47
+ let s = String(expr).split(',')[0].trim()
48
+ if (!s) return null
49
+ // 직접 config 접근 패턴 → 값 치환
50
+ s = s.replace(/config->opt_bool\(\s*"([^"]+)"\s*\)/g, (_, k) => cfg.bool(k) ? 'true' : 'false')
51
+ s = s.replace(/config->opt_int\(\s*"([^"]+)"\s*\)/g, (_, k) => String(cfg.int(k)))
52
+ s = s.replace(/config->opt_float\(\s*"([^"]+)"\s*\)/g, (_, k) => String(cfg.float(k)))
53
+ // 알려진 로컬 치환(긴 이름 먼저 — 부분매칭 방지)
54
+ for (const name of Object.keys(locals).sort((a, b) => b.length - a.length))
55
+ s = s.replace(new RegExp('\\b' + name + '\\b', 'g'), locals[name] ? 'true' : 'false')
56
+ // 남은 문자열이 안전한 boolean 식인지(true/false/숫자/연산자/괄호만) — 아니면 미지 → null
57
+ const stripped = s.replace(/\btrue\b|\bfalse\b/g, '1')
58
+ if (!/^[\s()!<>=&|0-9.]*$/.test(stripped)) return null
59
+ try { return !!Function('"use strict";return (' + s + ')')() } catch { return null }
60
+ }
61
+
62
+ // 현재 설정에서 비활성 키 맵 { key: 조건식 }. 조건이 명확히 false 인 규칙만.
63
+ export function disabledKeys(cfg) {
64
+ const locals = makeLocals(cfg)
65
+ const groups = ['toggle_print_fff_options', 'toggle_filament_options', 'toggle_printer_options',
66
+ 'TabFilament::toggle_options', 'TabPrinter::toggle_options']
67
+ const out = {}
68
+ for (const gname of groups) {
69
+ const g = toggles[gname]
70
+ if (!g || !Array.isArray(g.rules)) continue
71
+ for (const rule of g.rules) {
72
+ if (!rule.enable_if || !Array.isArray(rule.fields)) continue
73
+ const en = evalEnableIf(rule.enable_if, locals, cfg)
74
+ if (en === false) for (const k of rule.fields) if (!(k in out)) out[k] = rule.enable_if
75
+ }
76
+ }
77
+ return out
78
+ }
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "three-slicer",
3
+ "version": "0.1.0",
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
+ "license": "AGPL-3.0-or-later",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./engine/index.d.ts",
10
+ "import": "./engine/index.js"
11
+ },
12
+ "./settings": "./engine/src/settings.js",
13
+ "./toggle": "./engine/src/toggle_eval.js",
14
+ "./worker": "./engine/src/slicer.worker.js",
15
+ "./wasm": "./engine/src/slicer_core.js",
16
+ "./wasm-mt": "./engine/src/slicer_core.mt.js",
17
+ "./viewer": "./viewer/dist/Viewport.js",
18
+ "./viewer/toolpath": "./viewer/dist/toolpath_gpu.js",
19
+ "./viewer/loaders": "./viewer/dist/model_loaders.js",
20
+ "./components": "./components/dist/SettingsPanel.js",
21
+ "./data/*": "./data/*"
22
+ },
23
+ "scripts": {
24
+ "build": "vite build --config components/vite.config.js && vite build --config viewer/vite.config.js && cp viewer/src/make_worker.js viewer/dist/make_worker.js"
25
+ },
26
+ "peerDependencies": {
27
+ "react": ">=18",
28
+ "react-dom": ">=18",
29
+ "three": "^0.160.0"
30
+ },
31
+ "peerDependenciesMeta": {
32
+ "react": {
33
+ "optional": true
34
+ },
35
+ "react-dom": {
36
+ "optional": true
37
+ },
38
+ "three": {
39
+ "optional": true
40
+ }
41
+ },
42
+ "devDependencies": {
43
+ "vite": "^5.4.11"
44
+ },
45
+ "files": [
46
+ "engine",
47
+ "components/dist",
48
+ "viewer/dist",
49
+ "viewer/src",
50
+ "viewer/styles.css",
51
+ "components/styles.css",
52
+ "data/*.json",
53
+ "README.md",
54
+ "LICENSE.txt"
55
+ ],
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "git+https://github.com/kimgh06/Web_Three_Slicer.git",
59
+ "directory": "packages"
60
+ },
61
+ "homepage": "https://github.com/kimgh06/Web_Three_Slicer#readme",
62
+ "keywords": [
63
+ "3d-printing",
64
+ "slicer",
65
+ "gcode",
66
+ "stl",
67
+ "wasm",
68
+ "orcaslicer",
69
+ "three",
70
+ "react",
71
+ "arachne",
72
+ "tree-support"
73
+ ]
74
+ }
@@ -0,0 +1,68 @@
1
+ # three-slicer/viewer
2
+
3
+ 3D slicer viewer as a React component: three.js viewport (orbit/transform gizmos), model import (STL/OBJ/3MF/AMF/PLY, multi-object, drag & drop), Web Worker slicing via `three-slicer`, support painting, multi-plate, and a GPU-instanced volumetric toolpath preview ported from OrcaSlicer's libvgcode (millions of segments in a single draw path, per-feature colors, layer range slider, G-code export).
4
+
5
+ ```bash
6
+ npm i three-slicer/viewer react react-dom three@^0.160.0
7
+ ```
8
+
9
+ ```jsx
10
+ import { useState } from 'react'
11
+ import Viewport from 'three-slicer/viewer'
12
+ import 'three-slicer/viewer/styles.css'
13
+
14
+ function App() {
15
+ const [settings, setSettings] = useState({}) // OrcaSlicer schema keys; sparse — defaults fill the rest
16
+ return <Viewport settings={settings} setSettings={setSettings} />
17
+ }
18
+ ```
19
+
20
+ Slice parameters are derived from `settings` via `three-slicer`'s schema mapping — pair it with `three-slicer/components`' `<SettingsPanel/>` sharing the same state for a full slicer UI.
21
+
22
+ Props: `settings`, `setSettings`, `processPanel` (optional React node rendered in the right sidebar).
23
+
24
+ Subpath exports for custom UIs (framework-free, no React):
25
+ - `three-slicer/viewer/toolpath` — GPU toolpath renderer (`buildSegmentData`, `makeToolpath`, view-type colorers)
26
+ - `three-slicer/viewer/loaders` — model loaders returning unified triangle soup
27
+
28
+ ## Multithreaded slicing (automatic)
29
+
30
+ The engine ships two WASM kernels: single-threaded (default) and `-pthread` multithreaded (~2× wall-clock on layer-parallel wall generation, measured 12× on the wall pass itself). The worker picks the mt kernel automatically when the page is [cross-origin isolated](https://developer.mozilla.org/en-US/docs/Web/API/Window/crossOriginIsolated) — i.e. served with:
31
+
32
+ ```
33
+ Cross-Origin-Opener-Policy: same-origin
34
+ Cross-Origin-Embedder-Policy: require-corp
35
+ ```
36
+
37
+ No headers → falls back to the single-threaded kernel. Nothing else to change.
38
+
39
+ ## Bundler notes
40
+
41
+ - **Vite:** two config lines (worker code-splitting + top-level await in the mt glue; Chrome 89+/Safari 15+):
42
+
43
+ ```js
44
+ // vite.config.js
45
+ export default defineConfig({
46
+ worker: { format: 'es' },
47
+ build: { target: 'es2022' },
48
+ })
49
+ ```
50
+
51
+ - **Next.js / webpack:** render client-side (`dynamic(..., { ssr: false })`) and alias the emscripten glue's Node-only guards away:
52
+
53
+ ```js
54
+ // next.config.js
55
+ module.exports = {
56
+ webpack: (config) => {
57
+ for (const m of ['node:module', 'node:fs', 'node:path', 'node:url', 'node:crypto', 'node:worker_threads'])
58
+ config.resolve.alias[m] = false
59
+ return config
60
+ },
61
+ }
62
+ ```
63
+
64
+ `three` is a peer dependency pinned to `^0.160` (TransformControls API; r169+ changed its Object3D contract).
65
+
66
+ ## License
67
+
68
+ AGPL-3.0-or-later (derived from OrcaSlicer). A web app embedding this package must comply with AGPL — including offering its source to network users.