@@ -481,6 +481,56 @@ static int tcGetTangentAtProgress(TC_STRUCT const * const tc,
481481 return TP_ERR_OK;
482482}
483483
484+ /* ----------------------------------------------------------------
485+ * Static helpers: curvature at blend boundary (for C2 matching)
486+ * ---------------------------------------------------------------- */
487+
488+ /* *
489+ * Compute the XYZ curvature and unit curvature normal of a segment
490+ * at a given progress. Used to set up C2-continuous Bezier blends
491+ * that match the curvature of adjacent arcs at the junction.
492+ *
493+ * For TC_LINEAR: κ = 0, n = {0,0,0}
494+ * For TC_CIRCULAR: κ = 1/R, n = (center − point) / |center − point|
495+ *
496+ * @param tc Segment to query
497+ * @param progress Arc-length progress on the segment
498+ * @param kappa [out] Curvature magnitude (≥ 0)
499+ * @param normal [out] Unit curvature normal in XYZ (toward center)
500+ */
501+ static void tcGetCurvatureAtProgress (TC_STRUCT const * const tc,
502+ double progress,
503+ double * const kappa,
504+ PmCartesian * const normal)
505+ {
506+ *kappa = 0.0 ;
507+ normal->x = normal->y = normal->z = 0.0 ;
508+
509+ if (tc->motion_type == TC_CIRCULAR) {
510+ double radius = tc->coords .circle .xyz .radius ;
511+ if (radius > TP_POS_EPSILON) {
512+ *kappa = 1.0 / radius;
513+
514+ /* Normal toward center: (center − point) / |center − point| */
515+ double angle = 0.0 ;
516+ pmCircleAngleFromProgress (&tc->coords .circle .xyz ,
517+ &tc->coords .circle .fit ,
518+ progress, &angle);
519+ PmCartesian point;
520+ pmCirclePoint (&tc->coords .circle .xyz , angle, &point);
521+
522+ PmCartesian diff;
523+ pmCartCartSub (&tc->coords .circle .xyz .center , &point, &diff);
524+ double mag;
525+ pmCartMag (&diff, &mag);
526+ if (mag > TP_POS_EPSILON) {
527+ pmCartScalMult (&diff, 1.0 / mag, normal);
528+ }
529+ }
530+ }
531+ /* TC_LINEAR, TC_BEZIER, TC_SPHERICAL: κ = 0 (safe default) */
532+ }
533+
484534/* ----------------------------------------------------------------
485535 * Public API
486536 * ---------------------------------------------------------------- */
@@ -634,6 +684,13 @@ int findBlendPointsAndTangents9(double Rb,
634684 tcGetIntersectionPoint (prev_tc, tc, &boundary->intersection_point );
635685 }
636686
687+ /* Curvature at blend boundaries (for C2 matching in Bezier blends).
688+ * For arcs, κ = 1/R with normal toward center. For lines, κ = 0. */
689+ tcGetCurvatureAtProgress (prev_tc, boundary->s_prev ,
690+ &boundary->kappa_start , &boundary->n_start_xyz );
691+ tcGetCurvatureAtProgress (tc, boundary->s_tc ,
692+ &boundary->kappa_end , &boundary->n_end_xyz );
693+
637694 return TP_ERR_OK;
638695}
639696
@@ -740,19 +797,25 @@ static double findOptimalAlpha9(double Rb,
740797 Bezier9 trial1, trial2;
741798 double v1 = 0.0 , v2 = 0.0 ;
742799
800+ /* Curvature boundary conditions for C2 matching */
801+ double ks = boundary->kappa_start ;
802+ PmCartesian const *ns = (ks > BEZIER9_CURVATURE_EPSILON) ? &boundary->n_start_xyz : NULL ;
803+ double ke = boundary->kappa_end ;
804+ PmCartesian const *ne = (ke > BEZIER9_CURVATURE_EPSILON) ? &boundary->n_end_xyz : NULL ;
805+
743806 if (bezier9Init (&trial1, &boundary->P_start , &boundary->P_end ,
744807 &boundary->u_start_xyz , &boundary->u_end_xyz ,
745808 &boundary->u_start_abc , &boundary->u_end_abc ,
746809 &boundary->u_start_uvw , &boundary->u_end_uvw ,
747- a1) == TP_ERR_OK) {
810+ ks, ns, ke, ne, a1) == TP_ERR_OK) {
748811 v1 = bezier9AccLimit (&trial1, v_goal, a_max, j_max);
749812 }
750813
751814 if (bezier9Init (&trial2, &boundary->P_start , &boundary->P_end ,
752815 &boundary->u_start_xyz , &boundary->u_end_xyz ,
753816 &boundary->u_start_abc , &boundary->u_end_abc ,
754817 &boundary->u_start_uvw , &boundary->u_end_uvw ,
755- a2) == TP_ERR_OK) {
818+ ks, ns, ke, ne, a2) == TP_ERR_OK) {
756819 v2 = bezier9AccLimit (&trial2, v_goal, a_max, j_max);
757820 }
758821
@@ -768,7 +831,7 @@ static double findOptimalAlpha9(double Rb,
768831 &boundary->u_start_xyz , &boundary->u_end_xyz ,
769832 &boundary->u_start_abc , &boundary->u_end_abc ,
770833 &boundary->u_start_uvw , &boundary->u_end_uvw ,
771- a2) == TP_ERR_OK) {
834+ ks, ns, ke, ne, a2) == TP_ERR_OK) {
772835 v2 = bezier9AccLimit (&trial2, v_goal, a_max, j_max);
773836 } else {
774837 v2 = 0.0 ;
@@ -784,7 +847,7 @@ static double findOptimalAlpha9(double Rb,
784847 &boundary->u_start_xyz , &boundary->u_end_xyz ,
785848 &boundary->u_start_abc , &boundary->u_end_abc ,
786849 &boundary->u_start_uvw , &boundary->u_end_uvw ,
787- a1) == TP_ERR_OK) {
850+ ks, ns, ke, ne, a1) == TP_ERR_OK) {
788851 v1 = bezier9AccLimit (&trial1, v_goal, a_max, j_max);
789852 } else {
790853 v1 = 0.0 ;
@@ -894,8 +957,13 @@ int optimizeBlendSize9(TC_STRUCT const * const prev_tc,
894957 continue ;
895958 }
896959
897- /* Check deviation against tolerance */
898- double deviation = bezier9Deviation (&trial, &boundary.intersection_point );
960+ /* Check deviation against tolerance.
961+ * bezier9PathDeviation measures true distance to the two-segment
962+ * programmed path (P_start → corner → P_end), which is correct
963+ * for both symmetric and asymmetric blends. */
964+ double deviation = bezier9PathDeviation (&trial,
965+ &boundary.P_start .tran , &boundary.intersection_point ,
966+ &boundary.P_end .tran , 9 );
899967
900968 tp_debug_print (" iter=%d Rb=%g alpha=%g dev=%g tol=%g v_limit=%g v_goal=%g len=%g\n " ,
901969 iter, Rb, alpha, deviation, tolerance, v_limit, params.v_goal ,
@@ -954,7 +1022,9 @@ int optimizeBlendSize9(TC_STRUCT const * const prev_tc,
9541022 j_max_blend, params.v_goal ,
9551023 &trial, &alpha);
9561024 if (v_limit > 0.0 ) {
957- double deviation = bezier9Deviation (&trial, &boundary.intersection_point );
1025+ double deviation = bezier9PathDeviation (&trial,
1026+ &boundary.P_start .tran , &boundary.intersection_point ,
1027+ &boundary.P_end .tran , 9 );
9581028 if (deviation <= tolerance) {
9591029 result->Rb = Rb;
9601030 result->bezier = trial;
@@ -1027,9 +1097,15 @@ int createBlendSegment9(TC_STRUCT const * const prev_tc,
10271097 tcSetupMotion (blend_tc, v_req, v_plan, a_max, jerk);
10281098
10291099 /* Hard cap: curvature-rate jerk limit (like kink_vel, ignores feed override).
1030- * Centripetal jerk = v³ · dκ/ds must stay within the jerk budget. */
1100+ * Centripetal jerk = v³ · dκ/ds must stay within the jerk budget.
1101+ * Also cap maxvel so the backward pass limits entry velocity from
1102+ * adjacent segments — prevents high-override arcs from entering
1103+ * the blend faster than the curvature allows. */
10311104 double v_jerk_limit = bezier9AccLimit (&solution->bezier , v_req, a_max, jerk);
10321105 blend_tc->kink_vel = v_jerk_limit;
1106+ if (v_jerk_limit < blend_tc->maxvel ) {
1107+ blend_tc->maxvel = v_jerk_limit;
1108+ }
10331109
10341110 /* Store bezier curve in coords union */
10351111 blend_tc->coords .bezier = solution->bezier ;
0 commit comments