Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 200 additions & 30 deletions bricklayers.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,59 @@ def point_along_line_backward(from_pos, to_pos, distance):
new_y = to_pos.y + direction[1] * scale
return Point(new_x, new_y)

@staticmethod
def arc_length(s_from, s_to, i_offset, j_offset, clockwise):
"""Compute the arc path length for a G2/G3 move."""
cx = s_from.x + i_offset
cy = s_from.y + j_offset
r = math.sqrt(i_offset * i_offset + j_offset * j_offset)
if r < 1e-9:
return Point.distance_between_points(s_from, s_to)
a_start = math.atan2(s_from.y - cy, s_from.x - cx)
a_end = math.atan2(s_to.y - cy, s_to.x - cx)
if clockwise:
sweep = a_start - a_end
if sweep <= 0:
sweep += 2 * math.pi
else:
sweep = a_end - a_start
if sweep <= 0:
sweep += 2 * math.pi
return r * sweep

@staticmethod
def point_along_arc(s_from, i_offset, j_offset, clockwise, distance, total_arc_length):
"""Find a point at `distance` along an arc from s_from."""
cx = s_from.x + i_offset
cy = s_from.y + j_offset
r = math.sqrt(i_offset * i_offset + j_offset * j_offset)
if r < 1e-9:
return Point(s_from.x, s_from.y)
a_start = math.atan2(s_from.y - cy, s_from.x - cx)
fraction = distance / total_arc_length if total_arc_length > 0 else 0
sweep_angle = total_arc_length / r
if clockwise:
a_target = a_start - fraction * sweep_angle
else:
a_target = a_start + fraction * sweep_angle
return Point(cx + r * math.cos(a_target), cy + r * math.sin(a_target))

@staticmethod
def parse_arc_ij(gcode_str):
"""Extract I, J offsets from a G2/G3 gcode string. Returns (i, j) or None."""
parts = gcode_str.split()
if not parts or parts[0] not in ('G2', 'G3'):
return None
i_val, j_val = 0.0, 0.0
for p in parts[1:]:
if p[0] == 'I':
i_val = float(p[1:])
elif p[0] == 'J':
j_val = float(p[1:])
if i_val == 0.0 and j_val == 0.0:
return None
return (i_val, j_val, parts[0] == 'G2')


class GCodeState(NamedTuple):
"""Printing State"""
Expand Down Expand Up @@ -228,6 +281,82 @@ def compute(self, state: GCodeState):
self.min_y = min(self.min_y, y)
self.max_y = max(self.max_y, y)

def compute_arc(self, prev_state, cur_state, i_offset, j_offset, clockwise):
"""Computes the bounding box of a G2/G3 arc and feeds it into this bbox.

Args:
prev_state: start point (has .x, .y)
cur_state: end point (has .x, .y)
i_offset: I parameter (center X offset from start, relative)
j_offset: J parameter (center Y offset from start, relative)
clockwise: True for G2 (CW), False for G3 (CCW)
"""
cx = prev_state.x + i_offset
cy = prev_state.y + j_offset

# Angles from center to start and end
a_start = math.atan2(prev_state.y - cy, prev_state.x - cx)
a_end = math.atan2(cur_state.y - cy, cur_state.x - cx)
r = math.sqrt(i_offset * i_offset + j_offset * j_offset)

if r < 1e-9:
self.compute(cur_state)
return

# Normalize sweep: CW is negative, CCW is positive
if clockwise:
sweep = a_start - a_end
if sweep <= 0:
sweep += 2 * math.pi
sweep = -sweep # negative for CW
else:
sweep = a_end - a_start
if sweep <= 0:
sweep += 2 * math.pi

# Collect candidate extreme points: start, end, plus any cardinal
# angles (0, pi/2, pi, 3pi/2) crossed during the sweep.
xs = [prev_state.x, cur_state.x]
ys = [prev_state.y, cur_state.y]

cardinals = [0.0, math.pi / 2, math.pi, -math.pi, -math.pi / 2]
for card in cardinals:
# Check if this cardinal angle is within the arc sweep
diff = card - a_start
# Normalize diff into the sweep direction
if clockwise: # sweep is negative
while diff > 0:
diff -= 2 * math.pi
while diff < -2 * math.pi:
diff += 2 * math.pi
if diff >= sweep: # sweep is negative, so >= means "within"
xs.append(cx + r * math.cos(card))
ys.append(cy + r * math.sin(card))
else: # CCW, sweep is positive
while diff < 0:
diff += 2 * math.pi
while diff > 2 * math.pi:
diff -= 2 * math.pi
if diff <= sweep:
xs.append(cx + r * math.cos(card))
ys.append(cy + r * math.sin(card))

# Feed all extreme points into the bbox
for x in xs:
self.min_x = min(self.min_x, x)
self.max_x = max(self.max_x, x)
for y in ys:
self.min_y = min(self.min_y, y)
self.max_y = max(self.max_y, y)

# Ensure nonzero size on first use
if self.min_x == self.max_x:
self.min_x -= 0.1
self.max_x += 0.1
if self.min_y == self.max_y:
self.min_y -= 0.1
self.max_y += 0.1

def contains(self, other) -> bool:
"""Checks if this bounding box fully contains another bounding box."""
return (
Expand Down Expand Up @@ -1136,25 +1265,14 @@ def wipe(self, loop, simulator, feature):

for line in path:
if line.current.is_extruding:
if wipe_mode == 'forward':
from_pos = line.previous
to_pos = line.current
else: # backward mode
from_pos = line.current
to_pos = line.previous

segment_length = Point.distance_between_points(from_pos, to_pos)
from_pos, to_pos, segment_length, is_arc, arc_params = BrickLayersProcessor._wipe_segment_info(line, wipe_mode)

if segment_length <= 1e-6:
continue

if traveled + segment_length >= wipe_distance:
needed_distance = wipe_distance - traveled
target_point = (
Point.point_along_line_forward(from_pos, to_pos, needed_distance)
if wipe_mode == 'forward'
else Point.point_along_line_backward(from_pos, to_pos, needed_distance)
)
target_point = BrickLayersProcessor._wipe_interpolate(from_pos, to_pos, needed_distance, wipe_mode, is_arc, arc_params, segment_length)

moving_points.append(target_point)
moving_distances.append(needed_distance)
Expand Down Expand Up @@ -1193,6 +1311,45 @@ def wipe(self, loop, simulator, feature):


# TODO: salvage `experimental_arcflick` into the new wipe feature and remove this:

@staticmethod
def _wipe_segment_info(line, wipe_mode):
"""Compute segment length and arc info for a wipe path segment.
Returns (from_pos, to_pos, segment_length, is_arc, arc_params) where
arc_params is (i_val, j_val, clockwise) or None."""
if wipe_mode == 'forward':
from_pos = line.previous
to_pos = line.current
else:
from_pos = line.current
to_pos = line.previous

arc_info = Point.parse_arc_ij(line.gcode)
if arc_info and line.previous and line.current:
i_val, j_val, clockwise = arc_info
if wipe_mode == 'backward':
cx = line.previous.x + i_val
cy = line.previous.y + j_val
i_val = cx - line.current.x
j_val = cy - line.current.y
clockwise = not clockwise
seg_len = Point.arc_length(from_pos, to_pos, i_val, j_val, clockwise)
return from_pos, to_pos, seg_len, True, (i_val, j_val, clockwise)
else:
seg_len = Point.distance_between_points(from_pos, to_pos)
return from_pos, to_pos, seg_len, False, None

@staticmethod
def _wipe_interpolate(from_pos, to_pos, needed_distance, wipe_mode, is_arc, arc_params, segment_length):
"""Find a point at needed_distance along a segment (line or arc)."""
if is_arc:
i_val, j_val, clockwise = arc_params
return Point.point_along_arc(from_pos, i_val, j_val, clockwise, needed_distance, segment_length)
elif wipe_mode == 'forward':
return Point.point_along_line_forward(from_pos, to_pos, needed_distance)
else:
return Point.point_along_line_backward(from_pos, to_pos, needed_distance)

def wipe_movement(self, loop, target_state, simulator, feature, z = None):
from_gcode = GCodeLine.from_gcode

Expand Down Expand Up @@ -1234,25 +1391,14 @@ def wipe_movement(self, loop, target_state, simulator, feature, z = None):

for line in path:
if line.current.is_extruding:
if wipe_mode == 'forward':
from_pos = line.previous
to_pos = line.current
else: # backward mode
from_pos = line.current
to_pos = line.previous

segment_length = Point.distance_between_points(from_pos, to_pos)
from_pos, to_pos, segment_length, is_arc, arc_params = BrickLayersProcessor._wipe_segment_info(line, wipe_mode)

if segment_length <= 1e-6:
continue

if traveled + segment_length >= wipe_distance:
needed_distance = wipe_distance - traveled
target_point = (
Point.point_along_line_forward(from_pos, to_pos, needed_distance)
if wipe_mode == 'forward'
else Point.point_along_line_backward(from_pos, to_pos, needed_distance)
)
target_point = BrickLayersProcessor._wipe_interpolate(from_pos, to_pos, needed_distance, wipe_mode, is_arc, arc_params, segment_length)

moving_points.append(target_point)
moving_distances.append(needed_distance)
Expand Down Expand Up @@ -1409,7 +1555,26 @@ def calculate_loop_depth(group_perimeter):
bb = GCodeStateBBox()
for pline in ploop:
if pline.current.is_extruding: # Only compute movements that are extruding, ignore wipes or travels
bb.compute(pline.current) # compute the bounding box that surrounds the current loop
gcode = pline.gcode.strip()
# Check for arc commands (G2/G3) and compute arc bounding box
if pline.previous and gcode and gcode[:2] in ('G2', 'G3'):
parts = gcode.split()
cmd = parts[0]
if cmd in ('G2', 'G3'):
i_val, j_val = 0.0, 0.0
for arg in parts[1:]:
if arg[0] == 'I':
i_val = float(arg[1:])
elif arg[0] == 'J':
j_val = float(arg[1:])
if i_val != 0.0 or j_val != 0.0:
bb.compute_arc(pline.previous, pline.current, i_val, j_val, cmd == 'G2')
else:
bb.compute(pline.current)
else:
bb.compute(pline.current)
else:
bb.compute(pline.current) # compute the bounding box that surrounds the current loop
# print(node)
# print(bb)
nodes.append(LoopNode(loop_index, bb, ploop))
Expand Down Expand Up @@ -1610,7 +1775,11 @@ def generate_deffered_perimeters(self, myline, deffered, extrusion_multiplier, e
# If the gcode was using absolute extrusion, insert an M82 to return to Absolute Extrusion
buffer.append(from_gcode("M82 ; BRICK: Return to Absolute Extrusion\n"))
# Resets the correct absolute extrusion register for the next feature:
buffer.append(from_gcode(f"G92 E{myline.previous.e} ; BRICK: Resets the Extruder absolute position\n"))
# Use the E value from before the deferred perimeters, not after —
# the deferred block was replayed with relative extrusion so the
# firmware E register did not advance in absolute terms.
e_reset = self.last_noninternalperimeter_state.e if self.last_noninternalperimeter_state else myline.previous.e
buffer.append(from_gcode(f"G92 E{e_reset} ; BRICK: Resets the Extruder absolute position\n"))
##########

if previous_perimeter != perimeter_index:
Expand Down Expand Up @@ -1937,7 +2106,8 @@ def process_gcode(self, gcode_stream):
# If the gcode was using absolute extrusion, insert an M82 to return to Absolute Extrusion
buffer_lines.append(from_gcode("M82 ; BRICK: Return to Absolute Extrusion\n"))
# Resets the correct absolute extrusion register for the next feature:
buffer_lines.append(from_gcode(f"G92 E{myline.previous.e} ; BRICK: Resets the Extruder absolute position\n"))
e_reset = self.last_noninternalperimeter_state.e if self.last_noninternalperimeter_state else myline.previous.e
buffer_lines.append(from_gcode(f"G92 E{e_reset} ; BRICK: Resets the Extruder absolute position\n"))
self.last_internalperimeter_state = calculated_line.current
#if myline.previous.width != kept_line.current.width:
buffer_lines.append(from_gcode(f"{simulator.const_width}{myline.previous.width}\n")) # For the Preview
Expand Down Expand Up @@ -2013,7 +2183,7 @@ def process_gcode(self, gcode_stream):
buffer_lines.append(myline)

self.last_noninternalperimeter_state = current_state
if simulator.moved_in_xy:
if simulator.moved_in_xy and not feature.wiping:
myline.current = current_state
self.last_noninternalperimeter_xy_line = myline

Expand Down
Loading