mirror of
https://github.com/TengerTechnologies/Bricklayers.git
synced 2025-02-07 13:24:45 +00:00
390 lines
18 KiB
Python
390 lines
18 KiB
Python
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
#
|
|
# Copyright (c) [2025] [Roman Tenger]
|
|
import re
|
|
import sys
|
|
import logging
|
|
import os
|
|
import argparse
|
|
import math
|
|
|
|
# Get the directory where the script is located
|
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
# Configure logging to save in the script's directory
|
|
log_file_path = os.path.join(script_dir, "z_shift_log.txt")
|
|
logging.basicConfig(
|
|
filename=log_file_path,
|
|
filemode="w",
|
|
level=logging.INFO,
|
|
format="%(asctime)s - %(message)s"
|
|
)
|
|
|
|
# Add these constants from nonPlanarInfill.py
|
|
DEFAULT_AMPLITUDE = 0.6 # Default Z variation in mm
|
|
DEFAULT_FREQUENCY = 1.1 # Default frequency of the sine wave
|
|
SEGMENT_LENGTH = 1.0 # Split infill lines into segments of this length (mm)
|
|
|
|
# Add these helper functions from nonPlanarInfill.py
|
|
def segment_line(x1, y1, x2, y2, segment_length):
|
|
"""Divide a line into smaller segments."""
|
|
segments = []
|
|
total_length = math.sqrt((x2 - x1)**2 + (y2 - y1)**2)
|
|
num_segments = max(1, int(total_length // segment_length))
|
|
|
|
for i in range(num_segments + 1):
|
|
t = i / num_segments
|
|
x = x1 + t * (x2 - x1)
|
|
y = y1 + t * (y2 - y1)
|
|
segments.append((x, y))
|
|
|
|
logging.debug(f"Segmented line ({x1}, {y1}) -> ({x2}, {y2}) into {len(segments)} segments.")
|
|
return segments
|
|
|
|
def reset_modulation_state():
|
|
"""Reset parameters for Z-modulation to avoid propagating patterns."""
|
|
global last_sx
|
|
last_sx = 0
|
|
|
|
def update_layer_bounds(current_z, solid_infill_heights):
|
|
"""Update the bounds for non-planar processing based on current Z height."""
|
|
global last_bottom_layer, next_top_layer
|
|
lower_layers = [z for z in solid_infill_heights if z < current_z]
|
|
upper_layers = [z for z in solid_infill_heights if z > current_z]
|
|
if lower_layers:
|
|
last_bottom_layer = max(lower_layers)
|
|
if upper_layers:
|
|
next_top_layer = min(upper_layers)
|
|
|
|
def process_nonplanar_infill(lines, current_z, amplitude, frequency, solid_infill_heights):
|
|
"""Process only the non-planar infill modifications."""
|
|
modified_lines = []
|
|
in_infill = False
|
|
last_bottom_layer = 0
|
|
next_top_layer = float('inf')
|
|
processed_indices = set()
|
|
|
|
def update_layer_bounds(current_z):
|
|
nonlocal last_bottom_layer, next_top_layer
|
|
lower_layers = [z for z in solid_infill_heights if z < current_z]
|
|
upper_layers = [z for z in solid_infill_heights if z > current_z]
|
|
if lower_layers:
|
|
last_bottom_layer = max(lower_layers)
|
|
if upper_layers:
|
|
next_top_layer = min(upper_layers)
|
|
|
|
for line_num, line in enumerate(lines):
|
|
if line.startswith('G1') and 'Z' in line:
|
|
z_match = re.search(r'Z([-+]?\d*\.?\d+)', line)
|
|
if z_match:
|
|
current_z = float(z_match.group(1))
|
|
update_layer_bounds(current_z)
|
|
|
|
if ';TYPE:Internal infill' in line:
|
|
in_infill = True
|
|
modified_lines.append(line)
|
|
continue
|
|
elif line.startswith(';TYPE:'):
|
|
in_infill = False
|
|
|
|
if in_infill and line_num not in processed_indices and line.startswith('G1') and 'E' in line:
|
|
processed_indices.add(line_num)
|
|
match = re.search(r'X([-+]?\d*\.?\d+)\s*Y([-+]?\d*\.?\d+)\s*E([-+]?\d*\.?\d+)', line)
|
|
if match:
|
|
x1, y1, e = map(float, match.groups())
|
|
next_line_index = line_num + 1
|
|
|
|
if next_line_index < len(lines):
|
|
next_line = lines[next_line_index]
|
|
next_match = re.search(r'X([-+]?\d*\.?\d+)\s*Y([-+]?\d*\.?\d+)', next_line)
|
|
if next_match:
|
|
x2, y2 = map(float, next_match.groups())
|
|
segments = segment_line(x1, y1, x2, y2, SEGMENT_LENGTH)
|
|
|
|
distance_to_top = next_top_layer - current_z
|
|
distance_to_bottom = current_z - last_bottom_layer
|
|
total_distance = next_top_layer - last_bottom_layer
|
|
if total_distance > 0:
|
|
scaling_factor = min(distance_to_top, distance_to_bottom) / total_distance
|
|
else:
|
|
scaling_factor = 1.0
|
|
|
|
extrusion_per_segment = e / len(segments)
|
|
|
|
for i, (sx, sy) in enumerate(segments):
|
|
z_mod = current_z + amplitude * scaling_factor * math.sin(frequency * sx)
|
|
|
|
# Simple correction factor based on segment height difference
|
|
dz = abs(z_mod - current_z)
|
|
segment_2d = SEGMENT_LENGTH
|
|
segment_3d = math.sqrt(segment_2d**2 + dz**2)
|
|
correction_factor = segment_3d / segment_2d
|
|
|
|
modified_lines.append(
|
|
f"G1 X{sx:.3f} Y{sy:.3f} Z{z_mod:.3f} "
|
|
f"E{(extrusion_per_segment * correction_factor):.5f} ; Correction factor: {correction_factor:.3f} Original E: {extrusion_per_segment:.5f}\n"
|
|
)
|
|
continue
|
|
|
|
modified_lines.append(line)
|
|
|
|
return modified_lines
|
|
|
|
def process_wall_shifting(lines, layer_height, extrusion_multiplier, enable_wall_reorder=True):
|
|
"""Process only the wall shifting modifications."""
|
|
current_layer = 0
|
|
current_z = 0.0
|
|
perimeter_type = None
|
|
perimeter_block_count = 0
|
|
inside_perimeter_block = False
|
|
previous_g1_movement = None
|
|
previous_f_speed = None
|
|
z_shift = layer_height * 0.5
|
|
|
|
# Add buffers for shifted and non-shifted walls (only used if wall_reorder is enabled)
|
|
shifted_wall_buffer = []
|
|
nonshifted_wall_buffer = []
|
|
current_wall_buffer = []
|
|
|
|
total_layers = sum(1 for line in lines if line.startswith(";AFTER_LAYER_CHANGE"))
|
|
modified_lines = []
|
|
|
|
for line in lines:
|
|
# Detect layer changes
|
|
if line.startswith("G1 Z"):
|
|
z_match = re.search(r'Z([-\d.]+)', line)
|
|
if z_match:
|
|
current_z = float(z_match.group(1))
|
|
current_layer = int(current_z / layer_height)
|
|
perimeter_block_count = 0 # Reset block counter for new layer
|
|
logging.info(f"Layer {current_layer} detected at Z={current_z:.3f}")
|
|
modified_lines.append(line)
|
|
continue
|
|
|
|
# Detect perimeter types from PrusaSlicer comments
|
|
if ";TYPE:External perimeter" in line or ";TYPE:Outer wall" in line:
|
|
if enable_wall_reorder:
|
|
# Output any buffered walls when switching to external perimeter
|
|
if shifted_wall_buffer or nonshifted_wall_buffer:
|
|
# Output non-shifted walls first
|
|
for wall in nonshifted_wall_buffer:
|
|
modified_lines.extend(wall)
|
|
# Then output shifted walls
|
|
for wall in shifted_wall_buffer:
|
|
modified_lines.extend(wall)
|
|
# Clear buffers
|
|
shifted_wall_buffer = []
|
|
nonshifted_wall_buffer = []
|
|
|
|
perimeter_type = "external"
|
|
inside_perimeter_block = False
|
|
logging.info(f"External perimeter detected at layer {current_layer}")
|
|
modified_lines.append(line)
|
|
elif ";TYPE:Perimeter" in line or ";TYPE:Inner wall" in line:
|
|
perimeter_type = "internal"
|
|
inside_perimeter_block = False
|
|
if enable_wall_reorder:
|
|
current_wall_buffer = [] # Start a new wall buffer
|
|
logging.info(f"Internal perimeter block started at layer {current_layer}")
|
|
modified_lines.append(line)
|
|
elif ";TYPE:" in line: # Reset for other types
|
|
if enable_wall_reorder:
|
|
# Output any remaining buffered walls
|
|
if shifted_wall_buffer or nonshifted_wall_buffer:
|
|
for wall in nonshifted_wall_buffer:
|
|
modified_lines.extend(wall)
|
|
for wall in shifted_wall_buffer:
|
|
modified_lines.extend(wall)
|
|
shifted_wall_buffer = []
|
|
nonshifted_wall_buffer = []
|
|
|
|
perimeter_type = None
|
|
inside_perimeter_block = False
|
|
modified_lines.append(line)
|
|
|
|
# Group lines into perimeter blocks
|
|
elif perimeter_type == "internal" and line.startswith("G1") and "X" in line and "Y" in line and "E" in line:
|
|
# Start a new perimeter block if not already inside one
|
|
if not inside_perimeter_block:
|
|
perimeter_block_count += 1
|
|
inside_perimeter_block = True
|
|
if enable_wall_reorder:
|
|
current_wall_buffer = [] # Start a new wall buffer
|
|
|
|
# Add the cached movement command first
|
|
if previous_g1_movement:
|
|
if enable_wall_reorder:
|
|
current_wall_buffer.append(f"{previous_g1_movement};Previous position\n")
|
|
current_wall_buffer.append(f"G1 F{previous_f_speed:.3f} ; F speed from previous G1 movement\n")
|
|
|
|
|
|
# Set Z height and determine if wall is shifted
|
|
is_shifted = perimeter_block_count % 2 == 1
|
|
if is_shifted:
|
|
adjusted_z = current_z + z_shift
|
|
z_command = f"G1 Z{adjusted_z:.3f} ; Shifted Z for block #{perimeter_block_count}\n"
|
|
else:
|
|
z_command = f"G1 Z{current_z:.3f} ; Reset Z for block #{perimeter_block_count}\n"
|
|
|
|
if enable_wall_reorder:
|
|
current_wall_buffer.append(z_command)
|
|
else:
|
|
modified_lines.append(z_command)
|
|
|
|
# Process the current line (including extrusion adjustments)
|
|
if is_shifted:
|
|
e_match = re.search(r'E([-\d.]+)', line)
|
|
if e_match:
|
|
e_value = float(e_match.group(1))
|
|
original_line = line
|
|
if current_layer == 1: # First layer
|
|
new_e_value = e_value * 1.5 # 50% more extrusion
|
|
line = re.sub(r'E[-\d.]+', f'E{new_e_value:.5f}', line).strip()
|
|
line += f" ; Adjusted E for first layer (1.5x), block #{perimeter_block_count}\n"
|
|
elif current_layer == total_layers - 1: # Last layer
|
|
new_e_value = e_value * 0.5 # 50% less extrusion
|
|
line = re.sub(r'E[-\d.]+', f'E{new_e_value:.5f}', line).strip()
|
|
line += f" ; Adjusted E for last layer (0.5x), block #{perimeter_block_count}\n"
|
|
else: # Regular layers
|
|
line += f" ; current layer: {current_layer} total layers: {total_layers} \n"
|
|
new_e_value = e_value * extrusion_multiplier
|
|
line = re.sub(r'E[-\d.]+', f'E{new_e_value:.5f}', line).strip()
|
|
line += f" ; Adjusted E for regular layer ({extrusion_multiplier}x), block #{perimeter_block_count}\n"
|
|
|
|
|
|
if enable_wall_reorder:
|
|
current_wall_buffer.append(line)
|
|
else:
|
|
modified_lines.append(line)
|
|
|
|
|
|
|
|
elif perimeter_type == "internal" and line.startswith("G1") and "X" in line and "Y" in line and "F" in line:
|
|
# End of perimeter block
|
|
if inside_perimeter_block:
|
|
if enable_wall_reorder:
|
|
current_wall_buffer.append(line)
|
|
# Add Z reset for shifted blocks
|
|
if is_shifted:
|
|
current_wall_buffer.append(f"G1 Z{current_z:.3f} ; Reset Z after shifted block #{perimeter_block_count}\n")
|
|
# Add completed wall to appropriate buffer
|
|
if is_shifted:
|
|
shifted_wall_buffer.append(current_wall_buffer)
|
|
else:
|
|
nonshifted_wall_buffer.append(current_wall_buffer)
|
|
else:
|
|
modified_lines.append(line)
|
|
if is_shifted:
|
|
modified_lines.append(f"G1 Z{current_z:.3f} ; Reset Z after shifted block #{perimeter_block_count}\n")
|
|
inside_perimeter_block = False
|
|
|
|
elif perimeter_type == "internal" and line.startswith("G1") and "F" in line: #fix for Fspeed movements inside perimeter blocks
|
|
if enable_wall_reorder:
|
|
current_wall_buffer.append(line)
|
|
else:
|
|
modified_lines.append(line)
|
|
# Cache G1 movements with X and Y coordinates and F speeds
|
|
if line.startswith("G1"):
|
|
if "X" in line and "Y" in line:
|
|
previous_g1_movement = line.strip()
|
|
logging.info(f"Cached G1 movement: {previous_g1_movement}")
|
|
if "F" in line:
|
|
f_match = re.search(r'F([\d.]+)', line)
|
|
if f_match:
|
|
previous_f_speed = float(f_match.group(1))
|
|
logging.info(f"Cached F speed: {previous_f_speed}")
|
|
|
|
# Add non-wall lines directly to output
|
|
if not inside_perimeter_block and not perimeter_type == "internal":
|
|
modified_lines.append(line)
|
|
|
|
return modified_lines
|
|
|
|
def get_layer_height(gcode_lines):
|
|
"""Extract layer height from G-code header comments"""
|
|
for line in gcode_lines:
|
|
if "; layer_height =" in line.lower():
|
|
match = re.search(r'layer_height = (\d*\.?\d+)', line, re.IGNORECASE)
|
|
if match:
|
|
return float(match.group(1))
|
|
return None
|
|
|
|
def process_gcode(input_file, extrusion_multiplier, enable_nonplanar=False, enable_wall_reorder=True, amplitude=DEFAULT_AMPLITUDE, frequency=DEFAULT_FREQUENCY):
|
|
logging.info("Starting G-code processing")
|
|
logging.info(f"Input file: {input_file}")
|
|
|
|
# Read the input G-code
|
|
with open(input_file, 'r') as infile:
|
|
lines = infile.readlines()
|
|
|
|
# Get layer height from G-code
|
|
layer_height = get_layer_height(lines)
|
|
if layer_height is None:
|
|
layer_height = 0.2 # Default fallback value
|
|
logging.warning(f"Could not detect layer height from G-code, using default value: {layer_height}mm")
|
|
else:
|
|
logging.info(f"Detected layer height from G-code: {layer_height}mm")
|
|
|
|
# First pass: Process non-planar infill if enabled
|
|
if enable_nonplanar:
|
|
logging.info("Processing non-planar infill modifications...")
|
|
solid_infill_heights = []
|
|
current_z = 0.0
|
|
|
|
# Collect solid infill heights
|
|
for line in lines:
|
|
if line.startswith('G1') and 'Z' in line:
|
|
z_match = re.search(r'Z([-+]?\d*\.?\d+)', line)
|
|
if z_match:
|
|
current_z = float(z_match.group(1))
|
|
if ';TYPE:Solid infill' in line:
|
|
solid_infill_heights.append(current_z)
|
|
logging.info(f"Found solid infill at Z={current_z}")
|
|
|
|
# Process non-planar infill
|
|
lines = process_nonplanar_infill(lines, current_z, amplitude, frequency, solid_infill_heights)
|
|
logging.info("Non-planar infill processing completed")
|
|
|
|
# Second pass: Process wall shifting
|
|
logging.info("Processing wall shifting modifications...")
|
|
modified_lines = process_wall_shifting(lines, layer_height, extrusion_multiplier, enable_wall_reorder)
|
|
logging.info("Wall shifting processing completed")
|
|
|
|
# Write the final modified G-code
|
|
with open(input_file, 'w') as outfile:
|
|
outfile.writelines(modified_lines)
|
|
|
|
logging.info("G-code processing completed")
|
|
logging.info(f"Log file saved at {log_file_path}")
|
|
|
|
# Main execution
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description="Post-process G-code for Z-shifting, extrusion adjustments, and non-planar infill.")
|
|
parser.add_argument("input_file", help="Path to the input G-code file")
|
|
parser.add_argument("-extrusionMultiplier", type=float, default=1, help="Extrusion multiplier (default: 1.0)")
|
|
parser.add_argument("-nonPlanar", type=int, choices=[0, 1], default=0, help="Enable non-planar infill (0=off, 1=on)")
|
|
parser.add_argument("-wallReorder", type=int, choices=[0, 1], default=1, help="Enable wall reordering (0=off, 1=on)")
|
|
parser.add_argument("-amplitude", type=float, default=DEFAULT_AMPLITUDE, help=f"Amplitude of the Z modulation (default: {DEFAULT_AMPLITUDE})")
|
|
parser.add_argument("-frequency", type=float, default=DEFAULT_FREQUENCY, help=f"Frequency of the Z modulation (default: {DEFAULT_FREQUENCY})")
|
|
args = parser.parse_args()
|
|
|
|
process_gcode(
|
|
input_file=args.input_file,
|
|
extrusion_multiplier=args.extrusionMultiplier,
|
|
enable_nonplanar=bool(args.nonPlanar),
|
|
enable_wall_reorder=bool(args.wallReorder),
|
|
amplitude=args.amplitude,
|
|
frequency=args.frequency,
|
|
)
|