nx_gcode_extrude.nx source
↩ module page · 142 lines · 6218 B
1// nx_gcode_extrude.nx -- extrusion-distance math for FDM G-code.
2//
3// Computes how many millimetres of filament must be fed for a given
4// XY move at given line width + layer height to satisfy volume
5// conservation (incompressible deposit):
6//
7// extruded_volume_mm3 = line_width_mm * layer_height_mm * move_length_mm
8// filament_cross_section = π * (filament_radius_mm)^2
9// E_delta_mm = extruded_volume_mm3 / filament_cross_section
10//
11// Per cardinal feedback-engineering-sciences-bits-up-3d-print-first:
12// this primitive is MATH (geometric volume) + PHYSICS (continuity
13// equation: mass-in == mass-out for incompressible flow). Future
14// physics refinements where this composes:
15// - Rheology: shear-rate-dependent viscosity (PLA at 210°C),
16// pressure-drop through nozzle, max sustainable flow rate
17// - Thermal: melt-zone heat budget bounding max_flow_mm3s
18// - Material science: glass-transition temperature gating bridge
19// fan timing
20// All of those would refine THIS primitive's outputs. v1 ships the
21// volume-conservation core; further physics queued.
22//
23// Coordinate convention: Q14 fixed-point throughout (1 unit = 1/16384).
24//
25// Reusability: this primitive is also valid for CNC paste extruders,
26// food printers, concrete printers, bioprinters -- anywhere a moving
27// nozzle deposits a controlled volume per unit length. Hence runtime/
28// (multi-purpose), not nishi-print/ (3D-print-specific) hub.
29//
30// Numerical robustness:
31// - Uses nx_isqrt for move-length computation (no float sqrt).
32// - Uses nx_muldiv_i64 for intermediate products that would
33// overflow plain i64 (filament_xsect computation can reach 2^44).
34//
35// license_tier: ORIGINAL
36
37import "nx_syscalls.nx"
38import "nx_isqrt.nx"
39import "nx_i128.nx"
40
41const NX_PI_Q14: i64 = 51472 // π × 16384, rounded
42const NX_GCEX_Q14_ONE: i64 = 16384
43
44// ===== verdicts ====================================================
45
46const NX_GCEX_OK: i64 = 0
47const NX_GCEX_ERR_BAD_GEOMETRY: i64 = 1
48const NX_GCEX_ERR_BAD_FILAMENT: i64 = 2
49
50func nx_gcex_verdict_name(v: i64) -> *u8 {
51 if v == NX_GCEX_OK { return "OK" }
52 if v == NX_GCEX_ERR_BAD_GEOMETRY { return "BAD_GEOMETRY" }
53 if v == NX_GCEX_ERR_BAD_FILAMENT { return "BAD_FILAMENT" }
54 return "UNKNOWN"
55}
56
57// ===== filament cross-section ======================================
58//
59// Returns the filament's cross-sectional area in Q14 mm². Caller
60// passes filament diameter in micrometres (per nx_machine_graph's
61// filament_dia_um field convention).
62//
63// Algebra (in Q14):
64// radius_q14 = (dia_um * Q14_ONE / 2) / 1000 (µm -> mm in Q14)
65// xsect_q14 = π_q14 * radius_q14 * radius_q14 / (Q14_ONE * Q14_ONE)
66//
67// For 1.75mm filament: dia_um=1750, radius_q14=14336, xsect ≈ 2.405 mm²
68// (Q14 value ≈ 39408).
69
70func nx_gcex_filament_xsect_q14(filament_dia_um: i64) -> i64 {
71 if filament_dia_um <= 0 { return 0 }
72 // radius in Q14 mm: dia_um/2 * Q14_ONE / 1000. Multiply first
73 // to preserve precision (875 * 16384 = 14336000 fits in i64).
74 let half_um: i64 = filament_dia_um / 2
75 let radius_q14: i64 = (half_um * NX_GCEX_Q14_ONE) / 1000
76 // xsect = π * r² / Q14_ONE (because π is Q14, r is Q14, r*r is
77 // Q28, divide by Q14 to get back to Q14). Use muldiv for the
78 // π * r * r path to keep intermediates safe.
79 let r_squared_q28: i64 = radius_q14 * radius_q14
80 let xsect_q14: i64 = nx_muldiv_i64(NX_PI_Q14, r_squared_q28, NX_GCEX_Q14_ONE * NX_GCEX_Q14_ONE)
81 return xsect_q14
82}
83
84// ===== move length =================================================
85//
86// 2D Euclidean distance between two Q14 points. Returns Q14 mm.
87// Uses nx_isqrt; result is exact to integer Q14 (truncated).
88
89func nx_gcex_move_length_q14(x1: i64, y1: i64, x2: i64, y2: i64) -> i64 {
90 let dx: i64 = x2 - x1
91 let dy: i64 = y2 - y1
92 // dx, dy are Q14; squares are Q28; sum is Q28; nx_isqrt of Q28
93 // gives Q14.
94 let lsq: i64 = dx * dx + dy * dy
95 return nx_isqrt(lsq)
96}
97
98// ===== extrusion delta =============================================
99//
100// Returns the additional mm of filament (Q14) to feed for a move of
101// `move_length_q14` mm at the given line width + layer height.
102//
103// E_delta_q14 = volume_q14 * Q14_ONE / filament_xsect_q14
104//
105// where volume_q14 = (line_width_q14 * layer_height_q14 / Q14_ONE)
106// * (move_length_q14 / Q14_ONE)
107// * Q14_ONE
108//
109// Combined: E_delta_q14 = (line_width * layer_height * move_length)
110// / (Q14_ONE * filament_xsect)
111//
112// Uses nx_muldiv pattern to avoid intermediate overflow on the
113// triple product (worst case slicer-scale ~2^42, fits i64 fine, but
114// safer via i128 if line_width or move_length is unusual).
115
116func nx_gcex_E_delta_q14(line_width_q14: i64, layer_height_q14: i64,
117 move_length_q14: i64,
118 filament_xsect_q14: i64) -> i64 {
119 if filament_xsect_q14 <= 0 { return 0 }
120 if move_length_q14 <= 0 { return 0 }
121 // Compute (line_width * layer_height) first; this is mm² in Q28.
122 let cross_area_q28: i64 = line_width_q14 * layer_height_q14
123 // (cross_area_q28 / Q14_ONE) * move_length_q14 = volume in Q28.
124 // We want E_delta in Q14 mm: volume_q28 / (xsect_q14 * Q14_ONE)
125 // = volume_q28 / filament_xsect * 1/Q14_ONE
126 // Equivalent: muldiv(cross_area_q28, move_length_q14, filament_xsect_q14 * Q14_ONE)
127 let denom: i64 = filament_xsect_q14 * NX_GCEX_Q14_ONE
128 return nx_muldiv_i64(cross_area_q28, move_length_q14, denom)
129}
130
131// ===== one-shot helper =============================================
132//
133// Convenience: compute E_delta for a segment between two points.
134// Composes nx_gcex_move_length_q14 + nx_gcex_E_delta_q14.
135
136func nx_gcex_E_delta_for_move(x1: i64, y1: i64, x2: i64, y2: i64,
137 line_width_q14: i64, layer_height_q14: i64,
138 filament_xsect_q14: i64) -> i64 {
139 let move_len: i64 = nx_gcex_move_length_q14(x1, y1, x2, y2)
140 return nx_gcex_E_delta_q14(line_width_q14, layer_height_q14,
141 move_len, filament_xsect_q14)
142}