@@ -26,6 +26,7 @@ extern "C" {
2626#include " tc.h"
2727#include " tc_types.h"
2828#include " emcpos.h"
29+ #include " emcpose.h" // pmCartesianToEmcPose
2930#include " posemath.h"
3031#include " blendmath.h" // pmCircleEffectiveMinRadius, findIntersectionAngle
3132#include " atomic_9d.h" // atomicStoreDouble
@@ -1035,14 +1036,70 @@ static void tpComputeKinkVelocity_9D(TP_STRUCT *tp, TC_QUEUE_STRUCT *queue,
10351036 TC_PLAN_UNTOUCHED, __ATOMIC_RELEASE);
10361037}
10371038
1039+ /* *
1040+ * @brief Check if two LINE segments can be consolidated (merged).
1041+ *
1042+ * Two adjacent LINE segments are consolidatable when they have:
1043+ * - Same motion type (both TC_LINEAR)
1044+ * - Same canon motion type (don't merge G0 with G1)
1045+ * - Compatible feed rate (same F word)
1046+ * - Collinear direction in all active 9D subspaces (< 1° angle)
1047+ *
1048+ * @return 1 if segments can be merged, 0 otherwise
1049+ */
1050+ static int canConsolidateLines (TC_STRUCT const *prev_tc, TC_STRUCT const *tc)
1051+ {
1052+ if (prev_tc->motion_type != TC_LINEAR || tc->motion_type != TC_LINEAR)
1053+ return 0 ;
1054+
1055+ if (prev_tc->canon_motion_type != tc->canon_motion_type )
1056+ return 0 ;
1057+
1058+ if (fabs (prev_tc->reqvel - tc->reqvel ) > 1e-6 )
1059+ return 0 ;
1060+
1061+ // cos(1°) ≈ 0.99985 — matches BLEND9_MIN_THETA threshold
1062+ double const cos_thresh = 0.99985 ;
1063+ double dot;
1064+
1065+ // XYZ subspace
1066+ if (!prev_tc->coords .line .xyz .tmag_zero && !tc->coords .line .xyz .tmag_zero ) {
1067+ pmCartCartDot (&prev_tc->coords .line .xyz .uVec ,
1068+ &tc->coords .line .xyz .uVec , &dot);
1069+ if (dot < cos_thresh) return 0 ;
1070+ } else if (prev_tc->coords .line .xyz .tmag_zero != tc->coords .line .xyz .tmag_zero ) {
1071+ return 0 ;
1072+ }
1073+
1074+ // ABC subspace
1075+ if (!prev_tc->coords .line .abc .tmag_zero && !tc->coords .line .abc .tmag_zero ) {
1076+ pmCartCartDot (&prev_tc->coords .line .abc .uVec ,
1077+ &tc->coords .line .abc .uVec , &dot);
1078+ if (dot < cos_thresh) return 0 ;
1079+ } else if (prev_tc->coords .line .abc .tmag_zero != tc->coords .line .abc .tmag_zero ) {
1080+ return 0 ;
1081+ }
1082+
1083+ // UVW subspace
1084+ if (!prev_tc->coords .line .uvw .tmag_zero && !tc->coords .line .uvw .tmag_zero ) {
1085+ pmCartCartDot (&prev_tc->coords .line .uvw .uVec ,
1086+ &tc->coords .line .uvw .uVec , &dot);
1087+ if (dot < cos_thresh) return 0 ;
1088+ } else if (prev_tc->coords .line .uvw .tmag_zero != tc->coords .line .uvw .tmag_zero ) {
1089+ return 0 ;
1090+ }
1091+
1092+ return 1 ;
1093+ }
1094+
10381095/* *
10391096 * @brief Add linear move to shared memory queue (userspace planning)
10401097 *
10411098 * - Initialize TC_STRUCT
10421099 * - Set geometry
1100+ * - Consolidate with previous segment if collinear
10431101 * - Write to queue
10441102 * - Trigger optimization
1045- * - NO blending (requires blend geometry implementation)
10461103 */
10471104extern " C" int tpAddLine_9D (
10481105 TP_STRUCT * const tp,
@@ -1098,6 +1155,51 @@ extern "C" int tpAddLine_9D(
10981155 return 0 ;
10991156 }
11001157
1158+ // Collinear segment consolidation: if the new LINE goes the same direction
1159+ // as the previous LINE, extend the previous segment instead of creating a
1160+ // new segment boundary. Eliminates split cycles, blends, and velocity
1161+ // sawtooth in CAM-generated toolpaths with many tiny collinear segments.
1162+ if (tp->termCond != TC_TERM_COND_STOP) { // G61.1 must stop at every junction
1163+ TC_STRUCT *prev_tc = tcqLast_user (queue);
1164+ if (prev_tc && canConsolidateLines (prev_tc, &tc)) {
1165+ // Extend prev_tc geometry: keep current start, use new end
1166+ pmCartLineInit (&prev_tc->coords .line .xyz ,
1167+ &prev_tc->coords .line .xyz .start ,
1168+ &tc.coords .line .xyz .end );
1169+ pmCartLineInit (&prev_tc->coords .line .abc ,
1170+ &prev_tc->coords .line .abc .start ,
1171+ &tc.coords .line .abc .end );
1172+ pmCartLineInit (&prev_tc->coords .line .uvw ,
1173+ &prev_tc->coords .line .uvw .start ,
1174+ &tc.coords .line .uvw .end );
1175+
1176+ // Update lengths
1177+ prev_tc->target = pmLine9Target (&prev_tc->coords .line );
1178+ prev_tc->nominal_length = prev_tc->target ;
1179+
1180+ // Recompute joint-space segment for extended geometry
1181+ if (motion_planning::userspace_kins_is_enabled ()) {
1182+ EmcPose start_pose;
1183+ pmCartesianToEmcPose (&prev_tc->coords .line .xyz .start ,
1184+ &prev_tc->coords .line .abc .start ,
1185+ &prev_tc->coords .line .uvw .start ,
1186+ &start_pose);
1187+ if (motion_planning::userspace_kins_compute_joint_segment (
1188+ &start_pose, &end_pose, prev_tc) != 0 ) {
1189+ prev_tc->joint_space .valid = 0 ;
1190+ }
1191+ }
1192+
1193+ // Mark for re-optimization (geometry changed)
1194+ __atomic_store_n ((int *)&prev_tc->shared_9d .optimization_state ,
1195+ TC_PLAN_UNTOUCHED, __ATOMIC_RELEASE);
1196+
1197+ tp->goalPos = end_pose;
1198+ triggerAdaptiveOptimization_9D (tp);
1199+ return 0 ; // No new segment needed
1200+ }
1201+ }
1202+
11011203 // Set termination condition from G-code mode (G61/G61.1/G64):
11021204 // G61.1 (STOP): decelerate to zero at every junction
11031205 // G61 (EXACT): exact path with kink velocity
0 commit comments