@@ -159,6 +159,59 @@ def point_along_line_backward(from_pos, to_pos, distance):
159159 new_y = to_pos .y + direction [1 ] * scale
160160 return Point (new_x , new_y )
161161
162+ @staticmethod
163+ def arc_length (s_from , s_to , i_offset , j_offset , clockwise ):
164+ """Compute the arc path length for a G2/G3 move."""
165+ cx = s_from .x + i_offset
166+ cy = s_from .y + j_offset
167+ r = math .sqrt (i_offset * i_offset + j_offset * j_offset )
168+ if r < 1e-9 :
169+ return Point .distance_between_points (s_from , s_to )
170+ a_start = math .atan2 (s_from .y - cy , s_from .x - cx )
171+ a_end = math .atan2 (s_to .y - cy , s_to .x - cx )
172+ if clockwise :
173+ sweep = a_start - a_end
174+ if sweep <= 0 :
175+ sweep += 2 * math .pi
176+ else :
177+ sweep = a_end - a_start
178+ if sweep <= 0 :
179+ sweep += 2 * math .pi
180+ return r * sweep
181+
182+ @staticmethod
183+ def point_along_arc (s_from , i_offset , j_offset , clockwise , distance , total_arc_length ):
184+ """Find a point at `distance` along an arc from s_from."""
185+ cx = s_from .x + i_offset
186+ cy = s_from .y + j_offset
187+ r = math .sqrt (i_offset * i_offset + j_offset * j_offset )
188+ if r < 1e-9 :
189+ return Point (s_from .x , s_from .y )
190+ a_start = math .atan2 (s_from .y - cy , s_from .x - cx )
191+ fraction = distance / total_arc_length if total_arc_length > 0 else 0
192+ sweep_angle = total_arc_length / r
193+ if clockwise :
194+ a_target = a_start - fraction * sweep_angle
195+ else :
196+ a_target = a_start + fraction * sweep_angle
197+ return Point (cx + r * math .cos (a_target ), cy + r * math .sin (a_target ))
198+
199+ @staticmethod
200+ def parse_arc_ij (gcode_str ):
201+ """Extract I, J offsets from a G2/G3 gcode string. Returns (i, j) or None."""
202+ parts = gcode_str .split ()
203+ if not parts or parts [0 ] not in ('G2' , 'G3' ):
204+ return None
205+ i_val , j_val = 0.0 , 0.0
206+ for p in parts [1 :]:
207+ if p [0 ] == 'I' :
208+ i_val = float (p [1 :])
209+ elif p [0 ] == 'J' :
210+ j_val = float (p [1 :])
211+ if i_val == 0.0 and j_val == 0.0 :
212+ return None
213+ return (i_val , j_val , parts [0 ] == 'G2' )
214+
162215
163216class GCodeState (NamedTuple ):
164217 """Printing State"""
@@ -228,6 +281,82 @@ def compute(self, state: GCodeState):
228281 self .min_y = min (self .min_y , y )
229282 self .max_y = max (self .max_y , y )
230283
284+ def compute_arc (self , prev_state , cur_state , i_offset , j_offset , clockwise ):
285+ """Computes the bounding box of a G2/G3 arc and feeds it into this bbox.
286+
287+ Args:
288+ prev_state: start point (has .x, .y)
289+ cur_state: end point (has .x, .y)
290+ i_offset: I parameter (center X offset from start, relative)
291+ j_offset: J parameter (center Y offset from start, relative)
292+ clockwise: True for G2 (CW), False for G3 (CCW)
293+ """
294+ cx = prev_state .x + i_offset
295+ cy = prev_state .y + j_offset
296+
297+ # Angles from center to start and end
298+ a_start = math .atan2 (prev_state .y - cy , prev_state .x - cx )
299+ a_end = math .atan2 (cur_state .y - cy , cur_state .x - cx )
300+ r = math .sqrt (i_offset * i_offset + j_offset * j_offset )
301+
302+ if r < 1e-9 :
303+ self .compute (cur_state )
304+ return
305+
306+ # Normalize sweep: CW is negative, CCW is positive
307+ if clockwise :
308+ sweep = a_start - a_end
309+ if sweep <= 0 :
310+ sweep += 2 * math .pi
311+ sweep = - sweep # negative for CW
312+ else :
313+ sweep = a_end - a_start
314+ if sweep <= 0 :
315+ sweep += 2 * math .pi
316+
317+ # Collect candidate extreme points: start, end, plus any cardinal
318+ # angles (0, pi/2, pi, 3pi/2) crossed during the sweep.
319+ xs = [prev_state .x , cur_state .x ]
320+ ys = [prev_state .y , cur_state .y ]
321+
322+ cardinals = [0.0 , math .pi / 2 , math .pi , - math .pi , - math .pi / 2 ]
323+ for card in cardinals :
324+ # Check if this cardinal angle is within the arc sweep
325+ diff = card - a_start
326+ # Normalize diff into the sweep direction
327+ if clockwise : # sweep is negative
328+ while diff > 0 :
329+ diff -= 2 * math .pi
330+ while diff < - 2 * math .pi :
331+ diff += 2 * math .pi
332+ if diff >= sweep : # sweep is negative, so >= means "within"
333+ xs .append (cx + r * math .cos (card ))
334+ ys .append (cy + r * math .sin (card ))
335+ else : # CCW, sweep is positive
336+ while diff < 0 :
337+ diff += 2 * math .pi
338+ while diff > 2 * math .pi :
339+ diff -= 2 * math .pi
340+ if diff <= sweep :
341+ xs .append (cx + r * math .cos (card ))
342+ ys .append (cy + r * math .sin (card ))
343+
344+ # Feed all extreme points into the bbox
345+ for x in xs :
346+ self .min_x = min (self .min_x , x )
347+ self .max_x = max (self .max_x , x )
348+ for y in ys :
349+ self .min_y = min (self .min_y , y )
350+ self .max_y = max (self .max_y , y )
351+
352+ # Ensure nonzero size on first use
353+ if self .min_x == self .max_x :
354+ self .min_x -= 0.1
355+ self .max_x += 0.1
356+ if self .min_y == self .max_y :
357+ self .min_y -= 0.1
358+ self .max_y += 0.1
359+
231360 def contains (self , other ) -> bool :
232361 """Checks if this bounding box fully contains another bounding box."""
233362 return (
@@ -1136,25 +1265,14 @@ def wipe(self, loop, simulator, feature):
11361265
11371266 for line in path :
11381267 if line .current .is_extruding :
1139- if wipe_mode == 'forward' :
1140- from_pos = line .previous
1141- to_pos = line .current
1142- else : # backward mode
1143- from_pos = line .current
1144- to_pos = line .previous
1145-
1146- segment_length = Point .distance_between_points (from_pos , to_pos )
1268+ from_pos , to_pos , segment_length , is_arc , arc_params = BrickLayersProcessor ._wipe_segment_info (line , wipe_mode )
11471269
11481270 if segment_length <= 1e-6 :
11491271 continue
11501272
11511273 if traveled + segment_length >= wipe_distance :
11521274 needed_distance = wipe_distance - traveled
1153- target_point = (
1154- Point .point_along_line_forward (from_pos , to_pos , needed_distance )
1155- if wipe_mode == 'forward'
1156- else Point .point_along_line_backward (from_pos , to_pos , needed_distance )
1157- )
1275+ target_point = BrickLayersProcessor ._wipe_interpolate (from_pos , to_pos , needed_distance , wipe_mode , is_arc , arc_params , segment_length )
11581276
11591277 moving_points .append (target_point )
11601278 moving_distances .append (needed_distance )
@@ -1193,6 +1311,45 @@ def wipe(self, loop, simulator, feature):
11931311
11941312
11951313 # TODO: salvage `experimental_arcflick` into the new wipe feature and remove this:
1314+
1315+ @staticmethod
1316+ def _wipe_segment_info (line , wipe_mode ):
1317+ """Compute segment length and arc info for a wipe path segment.
1318+ Returns (from_pos, to_pos, segment_length, is_arc, arc_params) where
1319+ arc_params is (i_val, j_val, clockwise) or None."""
1320+ if wipe_mode == 'forward' :
1321+ from_pos = line .previous
1322+ to_pos = line .current
1323+ else :
1324+ from_pos = line .current
1325+ to_pos = line .previous
1326+
1327+ arc_info = Point .parse_arc_ij (line .gcode )
1328+ if arc_info and line .previous and line .current :
1329+ i_val , j_val , clockwise = arc_info
1330+ if wipe_mode == 'backward' :
1331+ cx = line .previous .x + i_val
1332+ cy = line .previous .y + j_val
1333+ i_val = cx - line .current .x
1334+ j_val = cy - line .current .y
1335+ clockwise = not clockwise
1336+ seg_len = Point .arc_length (from_pos , to_pos , i_val , j_val , clockwise )
1337+ return from_pos , to_pos , seg_len , True , (i_val , j_val , clockwise )
1338+ else :
1339+ seg_len = Point .distance_between_points (from_pos , to_pos )
1340+ return from_pos , to_pos , seg_len , False , None
1341+
1342+ @staticmethod
1343+ def _wipe_interpolate (from_pos , to_pos , needed_distance , wipe_mode , is_arc , arc_params , segment_length ):
1344+ """Find a point at needed_distance along a segment (line or arc)."""
1345+ if is_arc :
1346+ i_val , j_val , clockwise = arc_params
1347+ return Point .point_along_arc (from_pos , i_val , j_val , clockwise , needed_distance , segment_length )
1348+ elif wipe_mode == 'forward' :
1349+ return Point .point_along_line_forward (from_pos , to_pos , needed_distance )
1350+ else :
1351+ return Point .point_along_line_backward (from_pos , to_pos , needed_distance )
1352+
11961353 def wipe_movement (self , loop , target_state , simulator , feature , z = None ):
11971354 from_gcode = GCodeLine .from_gcode
11981355
@@ -1234,25 +1391,14 @@ def wipe_movement(self, loop, target_state, simulator, feature, z = None):
12341391
12351392 for line in path :
12361393 if line .current .is_extruding :
1237- if wipe_mode == 'forward' :
1238- from_pos = line .previous
1239- to_pos = line .current
1240- else : # backward mode
1241- from_pos = line .current
1242- to_pos = line .previous
1243-
1244- segment_length = Point .distance_between_points (from_pos , to_pos )
1394+ from_pos , to_pos , segment_length , is_arc , arc_params = BrickLayersProcessor ._wipe_segment_info (line , wipe_mode )
12451395
12461396 if segment_length <= 1e-6 :
12471397 continue
12481398
12491399 if traveled + segment_length >= wipe_distance :
12501400 needed_distance = wipe_distance - traveled
1251- target_point = (
1252- Point .point_along_line_forward (from_pos , to_pos , needed_distance )
1253- if wipe_mode == 'forward'
1254- else Point .point_along_line_backward (from_pos , to_pos , needed_distance )
1255- )
1401+ target_point = BrickLayersProcessor ._wipe_interpolate (from_pos , to_pos , needed_distance , wipe_mode , is_arc , arc_params , segment_length )
12561402
12571403 moving_points .append (target_point )
12581404 moving_distances .append (needed_distance )
@@ -1409,7 +1555,26 @@ def calculate_loop_depth(group_perimeter):
14091555 bb = GCodeStateBBox ()
14101556 for pline in ploop :
14111557 if pline .current .is_extruding : # Only compute movements that are extruding, ignore wipes or travels
1412- bb .compute (pline .current ) # compute the bounding box that surrounds the current loop
1558+ gcode = pline .gcode .strip ()
1559+ # Check for arc commands (G2/G3) and compute arc bounding box
1560+ if pline .previous and gcode and gcode [:2 ] in ('G2' , 'G3' ):
1561+ parts = gcode .split ()
1562+ cmd = parts [0 ]
1563+ if cmd in ('G2' , 'G3' ):
1564+ i_val , j_val = 0.0 , 0.0
1565+ for arg in parts [1 :]:
1566+ if arg [0 ] == 'I' :
1567+ i_val = float (arg [1 :])
1568+ elif arg [0 ] == 'J' :
1569+ j_val = float (arg [1 :])
1570+ if i_val != 0.0 or j_val != 0.0 :
1571+ bb .compute_arc (pline .previous , pline .current , i_val , j_val , cmd == 'G2' )
1572+ else :
1573+ bb .compute (pline .current )
1574+ else :
1575+ bb .compute (pline .current )
1576+ else :
1577+ bb .compute (pline .current ) # compute the bounding box that surrounds the current loop
14131578 # print(node)
14141579 # print(bb)
14151580 nodes .append (LoopNode (loop_index , bb , ploop ))
@@ -1610,7 +1775,11 @@ def generate_deffered_perimeters(self, myline, deffered, extrusion_multiplier, e
16101775 # If the gcode was using absolute extrusion, insert an M82 to return to Absolute Extrusion
16111776 buffer .append (from_gcode ("M82 ; BRICK: Return to Absolute Extrusion\n " ))
16121777 # Resets the correct absolute extrusion register for the next feature:
1613- buffer .append (from_gcode (f"G92 E{ myline .previous .e } ; BRICK: Resets the Extruder absolute position\n " ))
1778+ # Use the E value from before the deferred perimeters, not after —
1779+ # the deferred block was replayed with relative extrusion so the
1780+ # firmware E register did not advance in absolute terms.
1781+ e_reset = self .last_noninternalperimeter_state .e if self .last_noninternalperimeter_state else myline .previous .e
1782+ buffer .append (from_gcode (f"G92 E{ e_reset } ; BRICK: Resets the Extruder absolute position\n " ))
16141783 ##########
16151784
16161785 if previous_perimeter != perimeter_index :
@@ -1937,7 +2106,8 @@ def process_gcode(self, gcode_stream):
19372106 # If the gcode was using absolute extrusion, insert an M82 to return to Absolute Extrusion
19382107 buffer_lines .append (from_gcode ("M82 ; BRICK: Return to Absolute Extrusion\n " ))
19392108 # Resets the correct absolute extrusion register for the next feature:
1940- buffer_lines .append (from_gcode (f"G92 E{ myline .previous .e } ; BRICK: Resets the Extruder absolute position\n " ))
2109+ e_reset = self .last_noninternalperimeter_state .e if self .last_noninternalperimeter_state else myline .previous .e
2110+ buffer_lines .append (from_gcode (f"G92 E{ e_reset } ; BRICK: Resets the Extruder absolute position\n " ))
19412111 self .last_internalperimeter_state = calculated_line .current
19422112 #if myline.previous.width != kept_line.current.width:
19432113 buffer_lines .append (from_gcode (f"{ simulator .const_width } { myline .previous .width } \n " )) # For the Preview
0 commit comments