///|
/// G-Code synthesis and volumetric extrusion calculations for Marlin/RepRap firmware.
/// Implements PrusaSlicer-compatible multi-island, hole perimeters, and solid infill.
///|
/// Formatted G-code result containing the output string and comprehensive physical slicing statistics.
pub(all) struct GCodeResult {
gcode : String
total_filament_mm : Double
total_filament_grams : Double
estimated_cost : Double
total_layers : Int32
estimated_print_time_sec : Double
travel_time_sec : Double
extrusion_time_sec : Double
total_collisions_resolved : Int32
} derive(Debug)
///|
/// Calculate extrusion cross-sectional area based on nozzle line width and layer height.
/// Uses stadium cross-section (flat rectangle core with two semi-circular rounded caps).
pub fn calculate_extrusion_area(
line_width : Double,
layer_height : Double,
) -> Double {
let rect_area = (line_width - layer_height) * layer_height
let semi_area = 3.141592653589793 *
(layer_height * 0.5) *
(layer_height * 0.5)
rect_area + semi_area
}
///|
/// Calculate extrusion length (delta E) for moving distance L with given filament diameter.
pub fn calculate_delta_e(
move_length : Double,
line_width : Double,
layer_height : Double,
filament_diameter : Double,
) -> Double {
let bead_area = calculate_extrusion_area(line_width, layer_height)
let fil_radius = filament_diameter * 0.5
let fil_area = 3.141592653589793 * fil_radius * fil_radius
bead_area * move_length / fil_area
}
///|
/// Helper to format a Double to 3 decimal places without exponential notation.
pub fn format_f3(val : Double) -> String {
let is_neg = val < 0.0
let abs_v = if is_neg { -val } else { val }
let mut int_part = abs_v.to_int()
let mut frac_part = ((abs_v - int_part.to_double()) * 1000.0 + 0.5000001).to_int()
if frac_part >= 1000 {
int_part += 1
frac_part -= 1000
}
let sb = StringBuilder()
if is_neg && (int_part > 0 || frac_part > 0) {
sb.write_char('-')
}
sb.write_string(int_part.to_string())
sb.write_char('.')
if frac_part < 10 {
sb.write_string("00")
} else if frac_part < 100 {
sb.write_char('0')
}
sb.write_string(frac_part.to_string())
sb.to_string()
}
///|
/// Synthesize standard Marlin/RepRap G-code from sliced layers with PrusaSlicer feature alignment.
pub fn generate_gcode(
layers : Array[SliceLayer],
config : SlicerConfig,
bed_center_x~ : Double = 100.0,
bed_center_y~ : Double = 100.0,
) -> GCodeResult {
let sb = StringBuilder()
let mut current_e = 0.0
let mut total_travel_sec = 0.0
let mut total_extrusion_sec = 0.0
let mut total_collisions = 0
let mut current_pos_x = bed_center_x
let mut current_pos_y = bed_center_y
// Header comments
sb.write_string("; ==========================================\n")
sb.write_string("; Generated by MoonSlicer (Pure MoonBit 3D Slicer)\n")
sb.write_string("; Total Layers: \{layers.length()}\n")
sb.write_string("; Layer Height: \{config.layer_height} mm\n")
sb.write_string("; Nozzle Line Width: \{config.line_width} mm\n")
sb.write_string("; Wall Count: \{config.wall_count}\n")
sb.write_string("; Infill Pattern: \{config.infill_pattern}\n")
sb.write_string("; Seam Mode: \{config.seam_mode}\n")
sb.write_string("; Material: \{config.material}\n")
sb.write_string("; Infill Density: \{config.infill_density * 100.0}%\n")
sb.write_string("; Target Bed Center: X\{bed_center_x} Y\{bed_center_y}\n")
sb.write_string("; ==========================================\n\n")
// Start G-code initialization
sb.write_string("G21 ; set units to millimeters\n")
sb.write_string("G90 ; use absolute coordinates\n")
sb.write_string("M82 ; use absolute distances for extrusion\n")
sb.write_string("M140 S\{config.bed_temp} ; set bed temp\n")
sb.write_string("M104 S\{config.nozzle_temp} ; set extruder temp\n")
sb.write_string("M190 S\{config.bed_temp} ; wait for bed temp\n")
sb.write_string("M109 S\{config.nozzle_temp} ; wait for extruder temp\n")
sb.write_string("G28 ; home all axes\n")
sb.write_string("M106 S\{config.fan_speed} ; enable cooling fan\n")
sb.write_string("G92 E0 ; reset extruder position\n")
sb.write_string("G1 Z0.300 F3000 ; lift nozzle\n")
sb.write_string("G1 X10.000 Y10.000 F6000 ; move to prime start\n")
sb.write_string("G1 X60.000 Y10.000 E2.000 F1500 ; prime nozzle\n")
sb.write_string("G92 E0 ; reset extruder\n\n")
let default_print_feedrate = (config.print_speed * 60.0).to_int()
let solid_infill_feedrate = (config.solid_infill_speed * 60.0).to_int()
let bridge_feedrate = (config.bridge_speed * 60.0).to_int()
let travel_feedrate = (config.travel_speed * 60.0).to_int()
let retract_feedrate = (config.retract_speed * 60.0).to_int()
for layer_idx, layer in layers {
let layer_z = layer.z
let current_h = if layer_idx == 0 {
config.initial_layer_height
} else {
config.layer_height
}
let layer_surface = classify_layer(layer_idx, layers.length(), config)
sb.write_string("; ------------------------------------------\n")
sb.write_string("; LAYER: \{layer_idx}\n")
sb.write_string("; Z: \{format_f3(layer_z)}\n")
sb.write_string("; LAYER_TYPE: \{layer_surface}\n")
sb.write_string("G0 Z\{format_f3(layer_z)} F\{travel_feedrate}\n")
let expolys = if !layer.expolygons.is_empty() {
layer.expolygons
} else {
build_expolygons(layer.polygons)
}
for _island_idx, expoly in expolys {
let island_surface = classify_island_surface(
expoly, layer_idx, layers, config,
)
// 1. Generate multi-wall perimeters (outer contours + hole contours)
let perims = generate_expolygon_perimeters(
expoly,
config.wall_count,
config.line_width,
)
total_collisions += perims.collisions.length()
for c in perims.collisions {
sb.write_string(
"; [COLLISION RESOLVED] Hole \{c.hole_a_idx} - \{c.action} (clearance: \{format_f3(c.clearance)} mm)\n",
)
}
// 1a. Outer walls with seam placement
for wall_idx, wall in perims.outer_walls {
let pts_raw = wall.points
if pts_raw.length() < 3 {
continue
}
// Apply SeamMode
let seam_idx = match config.seam_mode {
SharpestCorner => wall.sharpest_corner_idx()
Nearest =>
wall.nearest_vertex_idx(
Vec2::new(
current_pos_x - bed_center_x,
current_pos_y - bed_center_y,
),
)
Aligned => wall.nearest_vertex_idx(Vec2::new(0.0, 100.0))
Random => (layer_idx * 17 + wall_idx * 31) % pts_raw.length()
}
let aligned_wall = wall.rotate_start(seam_idx)
let pts = aligned_wall.points
let start_x = pts[0].x + bed_center_x
let start_y = pts[0].y + bed_center_y
let travel_dist = Vec2::new(current_pos_x, current_pos_y).distance(
Vec2::new(start_x, start_y),
)
total_travel_sec += travel_dist / config.travel_speed
current_pos_x = start_x
current_pos_y = start_y
sb.write_string(
"G0 X\{format_f3(start_x)} Y\{format_f3(start_y)} F\{travel_feedrate}\n",
)
let wall_type_label = if wall_idx == 0 {
"; TYPE: Outer perimeter"
} else {
"; TYPE: Perimeter"
}
let m = pts.length()
for k = 0; k < m; k = k + 1 {
let curr = pts[k]
let next = pts[(k + 1) % m]
let move_dist = curr.distance(next)
let delta_e = calculate_delta_e(
move_dist,
config.line_width,
current_h,
config.filament_diameter,
)
current_e += delta_e
total_extrusion_sec += move_dist / config.print_speed
let nx = next.x + bed_center_x
let ny = next.y + bed_center_y
current_pos_x = nx
current_pos_y = ny
sb.write_string(
"G1 X\{format_f3(nx)} Y\{format_f3(ny)} E\{format_f3(current_e)} F\{default_print_feedrate} \{wall_type_label}\n",
)
}
}
// 1b. Hole walls with seam placement
for _hole_idx, hwalls in perims.hole_walls {
for _wall_idx, hwall in hwalls {
let pts_raw = hwall.points
if pts_raw.length() < 3 {
continue
}
let seam_idx = match config.seam_mode {
SharpestCorner => hwall.sharpest_corner_idx()
Nearest =>
hwall.nearest_vertex_idx(
Vec2::new(
current_pos_x - bed_center_x,
current_pos_y - bed_center_y,
),
)
Aligned => hwall.nearest_vertex_idx(Vec2::new(0.0, 100.0))
Random => (layer_idx * 13 + _wall_idx * 19) % pts_raw.length()
}
let aligned_hwall = hwall.rotate_start(seam_idx)
let pts = aligned_hwall.points
let start_x = pts[0].x + bed_center_x
let start_y = pts[0].y + bed_center_y
let travel_dist = Vec2::new(current_pos_x, current_pos_y).distance(
Vec2::new(start_x, start_y),
)
total_travel_sec += travel_dist / config.travel_speed
current_pos_x = start_x
current_pos_y = start_y
sb.write_string(
"G0 X\{format_f3(start_x)} Y\{format_f3(start_y)} F\{travel_feedrate}\n",
)
let m = pts.length()
for k = 0; k < m; k = k + 1 {
let curr = pts[k]
let next = pts[(k + 1) % m]
let move_dist = curr.distance(next)
let delta_e = calculate_delta_e(
move_dist,
config.line_width,
current_h,
config.filament_diameter,
)
current_e += delta_e
total_extrusion_sec += move_dist / config.print_speed
let nx = next.x + bed_center_x
let ny = next.y + bed_center_y
current_pos_x = nx
current_pos_y = ny
sb.write_string(
"G1 X\{format_f3(nx)} Y\{format_f3(ny)} E\{format_f3(current_e)} F\{default_print_feedrate} ; TYPE: Hole perimeter\n",
)
}
}
}
// 2. Generate infill lines within infill boundary
match perims.infill_boundary {
Some(ib) => {
let is_bridge = island_surface == BottomSolid && layer_idx > 0
if is_bridge {
sb.write_string(
"M106 S\{config.bridge_fan_speed} ; bridge fan boost\n",
)
}
let (density, print_feedrate, speed_val, pattern_to_use, infill_label) = match
island_surface {
TopSolid =>
(
1.0,
solid_infill_feedrate,
config.solid_infill_speed,
InfillPattern::Rectilinear,
"; TYPE: Top solid infill",
)
BottomSolid =>
if is_bridge {
(
1.0,
bridge_feedrate,
config.bridge_speed,
InfillPattern::Rectilinear,
"; TYPE: Bridge infill",
)
} else {
(
1.0,
solid_infill_feedrate,
config.solid_infill_speed,
InfillPattern::Rectilinear,
"; TYPE: Bottom solid infill",
)
}
InternalSparse =>
(
config.infill_density,
default_print_feedrate,
config.print_speed,
config.infill_pattern,
"; TYPE: Internal infill",
)
}
let infill_lines = generate_expolygon_infill(
ib,
pattern_to_use,
density,
config.line_width,
layer_idx,
config.infill_angle,
)
for seg in infill_lines {
let p1x = seg.p1.x + bed_center_x
let p1y = seg.p1.y + bed_center_y
let p2x = seg.p2.x + bed_center_x
let p2y = seg.p2.y + bed_center_y
let travel_dist = Vec2::new(current_pos_x, current_pos_y).distance(
Vec2::new(p1x, p1y),
)
total_travel_sec += travel_dist / config.travel_speed
current_pos_x = p1x
current_pos_y = p1y
sb.write_string(
"G0 X\{format_f3(p1x)} Y\{format_f3(p1y)} F\{travel_feedrate}\n",
)
let move_dist = seg.length()
let flow_scale = if is_bridge {
config.bridge_flow_ratio
} else {
1.0
}
let delta_e = calculate_delta_e(
move_dist,
config.line_width,
current_h,
config.filament_diameter,
) *
flow_scale
current_e += delta_e
total_extrusion_sec += move_dist / speed_val
current_pos_x = p2x
current_pos_y = p2y
sb.write_string(
"G1 X\{format_f3(p2x)} Y\{format_f3(p2y)} E\{format_f3(current_e)} F\{print_feedrate} \{infill_label}\n",
)
}
if is_bridge {
sb.write_string(
"M106 S\{config.fan_speed} ; restore standard fan\n",
)
}
}
None => ()
}
}
// Layer-end retraction
if config.retract_distance > 0.0 {
let ret_e = current_e - config.retract_distance
sb.write_string(
"G1 E\{format_f3(ret_e)} F\{retract_feedrate} ; retract\n",
)
}
}
// End G-code sequence
sb.write_string("\n; End G-code sequence\n")
sb.write_string("M104 S0 ; turn off hotend\n")
sb.write_string("M140 S0 ; turn off heatbed\n")
sb.write_string("G91 ; relative coordinates\n")
sb.write_string("G1 Z+5.000 F3000 ; lift nozzle\n")
sb.write_string("G90 ; absolute coordinates\n")
sb.write_string("G1 X0.000 Y200.000 F6000 ; present print\n")
sb.write_string("M84 ; disable stepper motors\n")
// Volumetric & physical mass calculations
let fil_radius_cm = config.filament_diameter * 0.5 * 0.1 // mm to cm
let fil_area_cm2 = 3.141592653589793 * fil_radius_cm * fil_radius_cm
let fil_len_cm = current_e * 0.1
let vol_cm3 = fil_area_cm2 * fil_len_cm
let filament_grams = vol_cm3 * config.material.density()
let est_cost = filament_grams / 1000.0 * config.filament_cost_per_kg
let total_time_sec = total_travel_sec + total_extrusion_sec
sb.write_string("; Total Filament Used: \{format_f3(current_e)} mm\n")
sb.write_string("; Total Filament Mass: \{format_f3(filament_grams)} g\n")
sb.write_string("; Estimated Print Cost: \{format_f3(est_cost)}\n")
{
gcode: sb.to_string(),
total_filament_mm: current_e,
total_filament_grams: filament_grams,
estimated_cost: est_cost,
total_layers: layers.length(),
estimated_print_time_sec: total_time_sec,
travel_time_sec: total_travel_sec,
extrusion_time_sec: total_extrusion_sec,
total_collisions_resolved: total_collisions,
}
}