@@ -18,11 +18,17 @@ This file is part of Universal Gcode Sender (UGS).
1818 */
1919package com .willwinder .universalgcodesender .gcode .processors ;
2020
21+ import com .willwinder .universalgcodesender .gcode .GcodeParser .GcodeMeta ;
22+ import com .willwinder .universalgcodesender .gcode .GcodePreprocessorUtils ;
2123import com .willwinder .universalgcodesender .gcode .GcodeState ;
2224import com .willwinder .universalgcodesender .gcode .GcodeStats ;
25+ import com .willwinder .universalgcodesender .gcode .util .Code ;
2326import com .willwinder .universalgcodesender .gcode .util .GcodeParserException ;
27+ import com .willwinder .universalgcodesender .gcode .util .GcodeParserUtils ;
28+ import com .willwinder .universalgcodesender .gcode .util .PlaneFormatter ;
2429import com .willwinder .universalgcodesender .model .Position ;
2530import com .willwinder .universalgcodesender .model .UnitUtils .Units ;
31+ import com .willwinder .universalgcodesender .types .PointSegment ;
2632
2733import java .util .Collections ;
2834import java .util .List ;
@@ -34,35 +40,174 @@ This file is part of Universal Gcode Sender (UGS).
3440public class Stats implements CommandProcessor , GcodeStats {
3541 private static Units defaultUnits = Units .MM ;
3642
43+ /** Cardinal angles (0, 90, 180, 270 degrees) where a circle reaches its axis-aligned extremes. */
44+ private static final double [] CARDINAL_ANGLES = {0 , Math .PI / 2 , Math .PI , 3 * Math .PI / 2 };
45+ private static final double TWO_PI = 2 * Math .PI ;
46+ private static final double EPSILON = 1e-9 ;
47+
3748 private Position min = new Position (Double .MAX_VALUE , Double .MAX_VALUE , Double .MAX_VALUE , Units .MM );
3849 private Position max = new Position (Double .MIN_VALUE , Double .MIN_VALUE , Double .MIN_VALUE , Units .MM );
3950
4051 private long commandCount = 0 ;
52+ private Position previous = null ;
53+
54+ /** Step size used when sampling a rotary move so its swept cartesian extents are captured. */
55+ private static final double MAX_DEGREES_PER_STEP = 5 ;
4156
4257 @ Override
4358 public List <String > processCommand (String command , GcodeState state ) throws GcodeParserException {
4459 Position c = state .currentPoint ;
4560 if (c != null ) {
46- Position p = new Position (c .x , c .y , c .z , state .isMetric ? Units .MM : Units .INCH )
47- .getPositionIn (defaultUnits );
48-
49- // Update min
50- min .x = getMin (min .x , p .x );
51- min .y = getMin (min .y , p .y );
52- min .z = getMin (min .z , p .z );
53-
54- // Update max
55- max .x = getMax (max .x , p .x );
56- max .y = getMax (max .y , p .y );
57- max .z = getMax (max .z , p .z );
61+ record (c , state .isMetric );
62+ if (isArcMotion (state .currentMotionMode ) && previous != null && !isSamePoint (previous , c )) {
63+ expandArcBounds (command , state , previous );
64+ } else if (previous != null && hasRotationChange (previous , c )) {
65+ expandRotationBounds (previous , c , state .isMetric );
66+ }
5867
5968 // Num commands
6069 commandCount ++;
70+ previous = new Position (c );
6171 }
6272
6373 return Collections .singletonList (command );
6474 }
6575
76+ private void expandRotationBounds (Position start , Position end , boolean metric ) {
77+ double startA = zeroIfNaN (start .a );
78+ double startB = zeroIfNaN (start .b );
79+ double startC = zeroIfNaN (start .c );
80+ double endA = zeroIfNaN (end .a );
81+ double endB = zeroIfNaN (end .b );
82+ double endC = zeroIfNaN (end .c );
83+ double maxDelta = Math .max (Math .abs (endA - startA ), Math .max (Math .abs (endB - startB ), Math .abs (endC - startC )));
84+ int steps = (int ) Math .ceil (maxDelta / MAX_DEGREES_PER_STEP );
85+
86+ for (int i = 1 ; i < steps ; i ++) {
87+ double t = (double ) i / steps ;
88+ Position sample = new Position (
89+ lerp (start .x , end .x , t ),
90+ lerp (start .y , end .y , t ),
91+ lerp (start .z , end .z , t ),
92+ lerp (startA , endA , t ),
93+ lerp (startB , endB , t ),
94+ lerp (startC , endC , t ),
95+ metric ? Units .MM : Units .INCH );
96+ record (sample , metric );
97+ }
98+ }
99+
100+ // An arc only reports its end point, so add the cardinal crossings (0/90/180/270 degrees from
101+ // its center) that fall within the swept angle, which is where it reaches its axis extremes.
102+ private void expandArcBounds (String command , GcodeState endState , Position start ) {
103+ try {
104+ GcodeState startState = endState .copy ();
105+ startState .currentPoint = new Position (start );
106+
107+ PointSegment arc = findArc (GcodeParserUtils .processCommand (command , 0 , startState , true ));
108+ if (arc == null ) {
109+ return ;
110+ }
111+
112+ PlaneFormatter plane = new PlaneFormatter (arc .getPlaneState ());
113+ Position center = arc .center ();
114+ Position end = arc .point ();
115+ double radius = arc .getRadius ();
116+ if (radius <= 0 ) {
117+ radius = Math .hypot (plane .axis0 (start ) - plane .axis0 (center ), plane .axis1 (start ) - plane .axis1 (center ));
118+ }
119+
120+ double startAngle = GcodePreprocessorUtils .getAngle (center , start , plane );
121+ double endAngle = GcodePreprocessorUtils .getAngle (center , end , plane );
122+
123+ for (double cardinal : CARDINAL_ANGLES ) {
124+ if (!arcContainsAngle (startAngle , endAngle , cardinal , arc .isClockwise ())) {
125+ continue ;
126+ }
127+
128+ Position extreme = new Position (start );
129+ plane .setAxis0 (extreme , plane .axis0 (center ) + radius * Math .cos (cardinal ));
130+ plane .setAxis1 (extreme , plane .axis1 (center ) + radius * Math .sin (cardinal ));
131+ record (extreme , endState .isMetric );
132+ }
133+ } catch (GcodeParserException e ) {
134+ // If the arc can't be parsed, fall back to the start/end points already recorded.
135+ }
136+ }
137+
138+ private void record (Position rawPoint , boolean metric ) {
139+ Position p = new Position (rawPoint .x , rawPoint .y , rawPoint .z ,
140+ rawPoint .a , rawPoint .b , rawPoint .c , metric ? Units .MM : Units .INCH )
141+ .getPositionIn (defaultUnits );
142+
143+ // Only fold rotations into cartesian coordinates when the point actually rotates. Doing it
144+ // unconditionally would turn undefined (NaN) axes into 0 and wrongly pull the bounds to the
145+ // origin for plain XYZ moves.
146+ if (p .hasRotation ()) {
147+ p = p .getCartesian ();
148+ }
149+
150+ min .x = getMin (min .x , p .x );
151+ min .y = getMin (min .y , p .y );
152+ min .z = getMin (min .z , p .z );
153+
154+ max .x = getMax (max .x , p .x );
155+ max .y = getMax (max .y , p .y );
156+ max .z = getMax (max .z , p .z );
157+ }
158+
159+ private static boolean isArcMotion (Code motionMode ) {
160+ return motionMode == Code .G2 || motionMode == Code .G3 ;
161+ }
162+
163+ private static boolean isSamePoint (Position a , Position b ) {
164+ return a .x == b .x && a .y == b .y && a .z == b .z ;
165+ }
166+
167+ private static boolean hasRotationChange (Position from , Position to ) {
168+ return zeroIfNaN (from .a ) != zeroIfNaN (to .a )
169+ || zeroIfNaN (from .b ) != zeroIfNaN (to .b )
170+ || zeroIfNaN (from .c ) != zeroIfNaN (to .c );
171+ }
172+
173+ private static double lerp (double from , double to , double t ) {
174+ return from + (to - from ) * t ;
175+ }
176+
177+ private static double zeroIfNaN (double value ) {
178+ return Double .isNaN (value ) ? 0 : value ;
179+ }
180+
181+ private static PointSegment findArc (List <GcodeMeta > commands ) {
182+ if (commands == null ) {
183+ return null ;
184+ }
185+ for (GcodeMeta meta : commands ) {
186+ if (meta .point != null && meta .point .isArc ()) {
187+ return meta .point ;
188+ }
189+ }
190+ return null ;
191+ }
192+
193+ /**
194+ * @return true if {@code angle} is swept when travelling along the arc from {@code startAngle}
195+ * to {@code endAngle} in the given direction. All angles are in radians.
196+ */
197+ private static boolean arcContainsAngle (double startAngle , double endAngle , double angle , boolean clockwise ) {
198+ double sweep = clockwise ? mod2pi (startAngle - endAngle ) : mod2pi (endAngle - startAngle );
199+ if (sweep < EPSILON ) {
200+ sweep = TWO_PI ; // a full circle (start == end) reaches every extreme
201+ }
202+ double toAngle = clockwise ? mod2pi (startAngle - angle ) : mod2pi (angle - startAngle );
203+ return toAngle <= sweep + EPSILON ;
204+ }
205+
206+ private static double mod2pi (double value ) {
207+ double remainder = value % TWO_PI ;
208+ return remainder < 0 ? remainder + TWO_PI : remainder ;
209+ }
210+
66211 private double getMin (double value1 , double value2 ) {
67212 if (Double .isNaN (value1 )) {
68213 return value2 ;
0 commit comments