nx_estimate.nx source
↩ module page · 149 lines · 6303 B
1// nx_estimate.nx -- pre-print operator visibility (filament + time).
2//
3// Closes the operator question "is this a 10-min print or 18 hours?"
4// BEFORE committing to a slice + upload. Industry slicers show
5// time + filament in their preview pane; substrate provides the
6// same info without the full slice cost.
7//
8// Algorithm (rough but fast, sub-millisecond):
9// 1. Compute mesh bbox volume (mm³)
10// 2. Estimate solid printed volume = bbox × (perim_factor + (1 -
11// perim_factor) × infill_pct / 100)
12// 3. Filament_mm = solid_volume / filament_cross_section_mm²
13// 4. Print_minutes = solid_volume / typical_flow_mm³/s / 60
14//
15// More accurate version (queued v2): run nx_print_session_run + read
16// emitter.E_acc_q14 + count G-code lines. v2 is exact but slow;
17// v1 is rough but instantaneous (operator can call BEFORE deciding
18// to commit to the full slice).
19//
20// Verdicts surface operator-actionable warnings:
21// OK -- print is reasonable
22// WARN_LONG -- > 24h estimated (operator should reconsider scope)
23// WARN_FIL -- > 250m estimated (typical PLA spool is ~330m for 1kg)
24// WARN_TINY -- < 1g of filament (probably too small to print)
25//
26// Per [[feedback-engineering-sciences-bits-up-3d-print-first]]:
27// pre-print visibility is operator's right; substrate composes
28// nx_mesh bbox + nx_gcode_extrude filament-cross-section helper to
29// answer without third-party slicer dependency.
30//
31// license_tier: ORIGINAL
32
33import "nx_syscalls.nx"
34import "nx_mesh.nx"
35import "nx_mesh_print_check.nx"
36import "nx_machine_graph.nx"
37import "nx_material_profile.nx"
38import "nx_gcode_extrude.nx"
39const NX_MAGIC_1440: i64 = 1440
40const NX_MAGIC_250000: i64 = 250000
41
42const NX_EST_Q14: i64 = 16384
43
44// ===== sealed-enum verdicts ========================================
45
46const NX_EST_OK: i64 = 0
47const NX_EST_WARN_LONG: i64 = 1
48const NX_EST_WARN_FIL: i64 = 2
49const NX_EST_WARN_TINY: i64 = 3
50const NX_EST_ERR_INPUT: i64 = 4
51
52func nx_est_verdict_name(v: i64) -> *u8 {
53 if v == NX_EST_OK { return "OK" }
54 if v == NX_EST_WARN_LONG { return "WARN_LONG" }
55 if v == NX_EST_WARN_FIL { return "WARN_FIL" }
56 if v == NX_EST_WARN_TINY { return "WARN_TINY" }
57 if v == NX_EST_ERR_INPUT { return "ERR_INPUT" }
58 return "UNKNOWN"
59}
60
61// ===== result struct ===============================================
62
63struct NxPrintEstimate {
64 filament_mm: i64, // estimated filament needed (integer mm)
65 print_minutes: i64, // estimated print time (integer minutes)
66 volume_mm3: i64, // estimated solid printed volume
67 verdict: i64,
68}
69
70const NX_ESTIMATE_BYTES: i64 = 32
71
72// ===== main entry =================================================
73//
74// Returns rough pre-print estimate without running the full slicer.
75// For a typical print:
76// shell takes ~5-10% of bbox volume (perim_factor 0.08 default)
77// interior infill takes the rest at infill_pct density
78//
79// Composes nx_mesh_bbox_compute + nx_gcex_filament_xsect_q14.
80
81func nx_estimate_print(mesh: *NxMesh,
82 machine: *NxMachineGraph,
83 material: *NxMaterialProfile,
84 infill_density_pct: i64) -> *NxPrintEstimate {
85 let r: *NxPrintEstimate = (sys_mmap(NX_ESTIMATE_BYTES)) as *NxPrintEstimate
86 r.filament_mm = 0
87 r.print_minutes = 0
88 r.volume_mm3 = 0
89 r.verdict = NX_EST_OK
90
91 if (mesh as i64) == 0 { r.verdict = NX_EST_ERR_INPUT; return r }
92 if (machine as i64) == 0 { r.verdict = NX_EST_ERR_INPUT; return r }
93 if (material as i64) == 0 { r.verdict = NX_EST_ERR_INPUT; return r }
94 if infill_density_pct < 0 { r.verdict = NX_EST_ERR_INPUT; return r }
95 if infill_density_pct > 100 { r.verdict = NX_EST_ERR_INPUT; return r }
96
97 // Bbox dimensions in mm.
98 let bbox: *NxMeshBBox = nx_mesh_bbox_compute(mesh)
99 if bbox.valid != 1 { r.verdict = NX_EST_ERR_INPUT; return r }
100
101 let extent_x_mm: i64 = (bbox.max_x - bbox.min_x) / NX_EST_Q14
102 let extent_y_mm: i64 = (bbox.max_y - bbox.min_y) / NX_EST_Q14
103 let extent_z_mm: i64 = (bbox.max_z - bbox.min_z) / NX_EST_Q14
104 let bbox_volume_mm3: i64 = extent_x_mm * extent_y_mm * extent_z_mm
105
106 // Solid volume estimate. Shell ~8% of bbox + infill fraction of
107 // remainder. Heuristic, accurate within ~30% for typical prints.
108 let perim_fraction_pct: i64 = 8
109 let infill_fraction_pct: i64 = (100 - perim_fraction_pct) * infill_density_pct / 100
110 let solid_pct: i64 = perim_fraction_pct + infill_fraction_pct
111 let solid_volume_mm3: i64 = bbox_volume_mm3 * solid_pct / 100
112 r.volume_mm3 = solid_volume_mm3
113
114 // Filament length: volume / cross-section. filament_xsect_q14
115 // is in Q14 mm²; convert to mm² for division.
116 let xsect_q14: i64 = nx_gcex_filament_xsect_q14(machine.filament_dia_um)
117 if xsect_q14 <= 0 { r.verdict = NX_EST_ERR_INPUT; return r }
118 // xsect_mm2_scaled = xsect_q14 / Q14 (~2 mm² for 1.75mm filament)
119 // filament_mm = solid_volume_mm3 / xsect_mm2_scaled
120 // = solid_volume_mm3 * Q14 / xsect_q14
121 if solid_volume_mm3 > 0 {
122 r.filament_mm = (solid_volume_mm3 * NX_EST_Q14) / xsect_q14
123 }
124
125 // Print time estimate. Volume / sustainable flow gives total
126 // extrusion seconds; add ~25% overhead for travels + heating +
127 // first layer. Use min(material.typical_max_flow_mm3s,
128 // machine.max_flow_mm3s) as the sustainable flow rate.
129 var flow_mm3s: i64 = machine.max_flow_mm3s
130 if material.typical_max_flow_mm3s < flow_mm3s {
131 flow_mm3s = material.typical_max_flow_mm3s
132 }
133 if flow_mm3s > 0 {
134 let extrude_sec: i64 = solid_volume_mm3 / flow_mm3s
135 let with_overhead_sec: i64 = (extrude_sec * 125) / 100
136 r.print_minutes = with_overhead_sec / 60
137 }
138
139 // Verdict tiers:
140 if r.print_minutes > NX_MAGIC_1440 { r.verdict = NX_EST_WARN_LONG; return r }
141 if r.filament_mm > NX_MAGIC_250000 { r.verdict = NX_EST_WARN_FIL; return r }
142 // Honest "not worth firing up the printer" cutoff: < 50mm filament
143 // (~0.15g for PLA). A 10mm calibration cube is ~870mm so well
144 // above; only sub-finger-sized doodads register WARN_TINY.
145 if r.filament_mm < 50 {
146 if r.volume_mm3 > 0 { r.verdict = NX_EST_WARN_TINY; return r }
147 }
148 return r
149}