From 44aeca4e5273694c9b09cf137bf07d4a0a77badc Mon Sep 17 00:00:00 2001 From: David Fyfe Date: Thu, 16 Oct 2025 21:52:13 +1100 Subject: [PATCH] Add radius attribute for rounded corners on orthogonal edges Implements rounded corner support for orthogonal edge routing with opt-in behavior to maintain backward compatibility. Features: - Supports radius=N attribute for explicit radius control eg. `edge [radius=5];` - Detects orthogonal corners by analyzing direction changes - Truncates edge segments at corners by the radius distance - Supports mixed rounded and square corners in same graph eg. `A1 -> B3 [color=red,radius=12];` Backward compatibility: - Radius defaults to 0 (square corners) maintaining existing behavior - Only applies rounding when radius > 0 is set - No behavior change for existing graphs without rounded style --- CHANGELOG.md | 7 + lib/common/emit.c | 403 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 409 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53e0f3061c..6e357d207a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `dot_builtins`, when compiled with the CMake build system, now supports the Quartz plugin if it is enabled at build time. +### Added + +- Orthogonal edges now support rounded corners via the `radius` edge attribute. + When `splines=ortho` is set and an edge has `radius > 0`, corners are rendered + as smooth arcs instead of sharp angles. The radius value controls the size of + the corner arc. Default is 0 (square corners). + ### Fixed - Memory corruption when using the Java bindings to render to a string has been diff --git a/lib/common/emit.c b/lib/common/emit.c index fa85e3ba8c..c9c9b456be 100644 --- a/lib/common/emit.c +++ b/lib/common/emit.c @@ -2104,6 +2104,271 @@ taperfun (edge_t* e) return agisdirected(agraphof(aghead(e))) ? forfunc : nonefunc; } +/* find_prev_distinct: + * Find the previous point that is not a duplicate of pts[i]. + * Returns -1 if no such point exists. + */ +static int +find_prev_distinct(const pointf *pts, size_t i) +{ + const double TOLERANCE = 0.01; + for (int j = (int)i - 1; j >= 0; j--) { + double dx = pts[j].x - pts[i].x; + double dy = pts[j].y - pts[i].y; + if (hypot(dx, dy) > TOLERANCE) { + return j; + } + } + return -1; +} + +/* find_next_distinct: + * Find the next point that is not a duplicate of pts[i]. + * Returns -1 if no such point exists. + */ +static int +find_next_distinct(const pointf *pts, size_t i, size_t n) +{ + const double TOLERANCE = 0.01; + for (size_t j = i + 1; j < n; j++) { + double dx = pts[j].x - pts[i].x; + double dy = pts[j].y - pts[i].y; + if (hypot(dx, dy) > TOLERANCE) { + return (int)j; + } + } + return -1; +} + + +/* Structure to hold corner information for truncation */ +typedef struct { + size_t idx; /* Index of corner point */ + pointf trunc_prev; /* Truncation point toward previous segment */ + pointf trunc_next; /* Truncation point toward next segment */ + pointf wedge_center; /* Center of arc wedge */ + double angle1, angle2; /* Arc angles */ +} corner_info_t; + +/* Comparison function for sorting corners by index */ +static int compare_corners(const void *a, const void *b) { + const corner_info_t *ca = a; + const corner_info_t *cb = b; + if (ca->idx < cb->idx) return -1; + if (ca->idx > cb->idx) return 1; + return 0; +} + +/* calculate_wedge_parameters: + * Calculate the wedge center and angles for an orthogonal corner based on + * the normalized direction vectors. + */ +static void calculate_wedge_parameters(corner_info_t *ci, pointf curr, + double dx1, double dy1, double dx2, double dy2, + double radius, bool seg1_horiz, bool seg2_vert) +{ + if (seg1_horiz && seg2_vert) { + if (dx1 > 0 && dy2 < 0) { + /* Right then down */ + ci->wedge_center.x = curr.x - radius; + ci->wedge_center.y = curr.y - radius; + ci->angle1 = 0; + ci->angle2 = M_PI / 2; + } else if (dx1 > 0 && dy2 > 0) { + /* Right then up */ + ci->wedge_center.x = curr.x - radius; + ci->wedge_center.y = curr.y + radius; + ci->angle1 = -M_PI / 2; + ci->angle2 = 0; + } else if (dx1 < 0 && dy2 < 0) { + /* Left then down */ + ci->wedge_center.x = curr.x + radius; + ci->wedge_center.y = curr.y - radius; + ci->angle1 = M_PI / 2; + ci->angle2 = M_PI; + } else { + /* Left then up */ + ci->wedge_center.x = curr.x + radius; + ci->wedge_center.y = curr.y + radius; + ci->angle1 = M_PI; + ci->angle2 = 3 * M_PI / 2; + } + } else { + if (dy1 < 0 && dx2 > 0) { + /* Down then right */ + ci->wedge_center.x = curr.x + radius; + ci->wedge_center.y = curr.y + radius; + ci->angle1 = M_PI; + ci->angle2 = 3 * M_PI / 2; + } else if (dy1 < 0 && dx2 < 0) { + /* Down then left */ + ci->wedge_center.x = curr.x - radius; + ci->wedge_center.y = curr.y + radius; + ci->angle1 = 3 * M_PI / 2; + ci->angle2 = 2 * M_PI; + } else if (dy1 > 0 && dx2 > 0) { + /* Up then right */ + ci->wedge_center.x = curr.x + radius; + ci->wedge_center.y = curr.y - radius; + ci->angle1 = M_PI / 2; + ci->angle2 = M_PI; + } else { + /* Up then left */ + ci->wedge_center.x = curr.x - radius; + ci->wedge_center.y = curr.y - radius; + ci->angle1 = 0; + ci->angle2 = M_PI / 2; + } + } +} + +/* process_corner: + * Process a detected orthogonal corner and add it to the corners array. + * Returns true if the corner was added, false if it was a duplicate. + */ +static bool process_corner(corner_info_t *corners, size_t *num_corners, + const pointf *pts, size_t i, pointf curr, + double dx1, double dy1, double dx2, double dy2, + double radius, bool seg1_horiz, bool seg2_vert) +{ + /* Check if we already detected this corner (skip duplicates) */ + bool already_detected = false; + const double DUP_TOL = 0.01; + for (size_t j = 0; j < *num_corners; j++) { + pointf existing_corner = pts[corners[j].idx]; + if (hypot(curr.x - existing_corner.x, curr.y - existing_corner.y) < DUP_TOL) { + already_detected = true; + break; + } + } + if (already_detected) return false; + + corner_info_t *ci = &corners[(*num_corners)++]; + ci->idx = i; + + /* Normalize direction vectors */ + double len1 = hypot(dx1, dy1); + double len2 = hypot(dx2, dy2); + dx1 /= len1; + dy1 /= len1; + dx2 /= len2; + dy2 /= len2; + + /* Calculate truncation points (move radius distance away from corner) */ + ci->trunc_prev.x = curr.x - dx1 * radius; + ci->trunc_prev.y = curr.y - dy1 * radius; + ci->trunc_next.x = curr.x + dx2 * radius; + ci->trunc_next.y = curr.y + dy2 * radius; + + /* Calculate wedge center and angles */ + calculate_wedge_parameters(ci, curr, dx1, dy1, dx2, dy2, radius, seg1_horiz, seg2_vert); + + return true; +} + +/* find_ortho_corners: + * Identify all orthogonal corners and compute truncation/arc information. + * Fills corners array and sets *num_corners_out to the count. + * Caller must pre-allocate corners array with at least n elements. + */ +static void find_ortho_corners(const pointf *pts, size_t n, double radius, + corner_info_t *corners, size_t *num_corners_out) +{ + const double TOL = 0.1; + size_t num_corners = 0; + + for (size_t i = 0; i < n; i++) { + int prev_idx = find_prev_distinct(pts, i); + int next_idx = find_next_distinct(pts, i, n); + + if (prev_idx < 0 || next_idx < 0) continue; + + pointf prev = pts[prev_idx]; + pointf curr = pts[i]; + pointf next = pts[next_idx]; + + /* Get direction vectors */ + double dx1 = curr.x - prev.x; + double dy1 = curr.y - prev.y; + double dx2 = next.x - curr.x; + double dy2 = next.y - curr.y; + + /* Check if this is an orthogonal corner */ + bool seg1_horiz = fabs(dy1) < TOL && fabs(dx1) > TOL; + bool seg1_vert = fabs(dx1) < TOL && fabs(dy1) > TOL; + bool seg2_horiz = fabs(dy2) < TOL && fabs(dx2) > TOL; + bool seg2_vert = fabs(dx2) < TOL && fabs(dy2) > TOL; + + bool is_corner = (seg1_horiz && seg2_vert) || (seg1_vert && seg2_horiz); + + if (is_corner) { + process_corner(corners, &num_corners, pts, i, curr, + dx1, dy1, dx2, dy2, radius, seg1_horiz, seg2_vert); + } + } + + *num_corners_out = num_corners; +} + +/* render_corner_arc: + * Extract and render the arc portion from a wedge polyline. + * The wedge includes center point and return paths which we skip. + */ +static void render_corner_arc(GVJ_t *job, const Ppolyline_t *wedge, + size_t arc_start_idx, size_t arc_point_count, + char *edge_color) +{ + pointf *arc_points = gv_calloc(arc_point_count, sizeof(pointf)); + for (size_t j = 0; j < arc_point_count; j++) { + arc_points[j] = wedge->ps[arc_start_idx + j]; + } + + /* Draw only the arc curve (no straight edges back to center) */ + if (edge_color && edge_color[0]) { + gvrender_set_pencolor(job, edge_color); + } else { + gvrender_set_pencolor(job, DEFAULT_COLOR); + } + gvrender_polyline(job, arc_points, arc_point_count); + + free(arc_points); +} + +/* draw_ortho_corner_markers: + * Draw 90-degree arc curves at orthogonal edge corners. + * Draws only the arc portion (not the straight wedge edges). + */ +static void +draw_ortho_corner_markers(GVJ_t *job, const corner_info_t *corners, + size_t num_corners, double radius, char *edge_color) +{ + if (num_corners == 0) return; + + for (size_t i = 0; i < num_corners; i++) { + const corner_info_t *ci = &corners[i]; + + /* Generate wedge path */ + Ppolyline_t *wedge = ellipticWedge(ci->wedge_center, + radius, radius, + ci->angle1, ci->angle2); + + if (wedge && wedge->pn > 4) { + /* Extract only the arc portion (skip center at start and close path at end) */ + /* Skip duplicates and straight edges: wedge path includes center and return paths */ + size_t arc_start_idx = 3; /* Skip center, first arc point, AND duplicate */ + size_t arc_end_idx = wedge->pn - 4; /* Skip duplicate endpoint, last arc point, AND center */ + size_t arc_point_count = arc_end_idx >= arc_start_idx ? arc_end_idx - arc_start_idx + 1 : 0; + + if (arc_point_count >= 2) { + render_corner_arc(job, wedge, arc_start_idx, arc_point_count, edge_color); + } + + free(wedge->ps); + free(wedge); + } + } +} + static void emit_edge_graphics(GVJ_t * job, edge_t * e, char** styles) { int cnum, numsemi = 0; @@ -2307,7 +2572,143 @@ static void emit_edge_graphics(GVJ_t * job, edge_t * e, char** styles) } for (size_t i = 0; i < ED_spl(e)->size; i++) { bz = ED_spl(e)->list[i]; - gvrender_beziercurve(job, bz.list, bz.size, 0); + + /* Check if this edge has orthogonal routing and wants rounded corners */ + char *splines_attr = agget(agraphof(aghead(e)), "splines"); + bool is_ortho = splines_attr && streq(splines_attr, "ortho"); + + /* Check for rounded style or explicit radius attribute */ + bool want_rounded = false; + double radius = 0.0; + + /* First check if style=rounded */ + if (styles) { + for (char **sp = styles; *sp; sp++) { + if (streq(*sp, "rounded")) { + want_rounded = true; + break; + } + } + } + + /* Then check for explicit radius attribute (overrides style) */ + char *radius_attr = agget(e, "radius"); + if (radius_attr && radius_attr[0]) { + radius = atof(radius_attr); + want_rounded = (radius > 0); + } + + /* If no explicit radius, use default when style=rounded */ + if (want_rounded && radius == 0.0) { + radius = fmax(12.0, penwidth * 8.0); + } + + if (is_ortho && want_rounded && radius > 0) { + /* Truncate edge at corners and draw arcs */ + corner_info_t *corners = gv_calloc(bz.size, sizeof(corner_info_t)); + size_t num_corners = 0; + find_ortho_corners(bz.list, bz.size, radius, corners, &num_corners); + + if (num_corners > 0) { + /* Sort corners by index */ + qsort(corners, num_corners, sizeof(corner_info_t), compare_corners); + + /* Render segments between corners */ + const double CORNER_TOL = 0.01; + size_t seg_start_idx = 0; + pointf seg_start_pt = bz.list[0]; + + for (size_t c = 0; c <= num_corners; c++) { + size_t seg_end_idx; + pointf seg_end_pt; + + if (c < num_corners) { + /* Segment ends at this corner's trunc_prev */ + seg_end_idx = corners[c].idx; + seg_end_pt = corners[c].trunc_prev; + } else { + /* Last segment ends at final point */ + seg_end_idx = bz.size - 1; + seg_end_pt = bz.list[bz.size - 1]; + } + + /* Count non-corner points between seg_start_idx and seg_end_idx */ + size_t seg_pt_count = 1; /* seg_start_pt */ + for (size_t pt = seg_start_idx + 1; pt < seg_end_idx; pt++) { + /* Skip if this point is AT a corner position (handles duplicates) */ + bool at_corner = false; + for (size_t cc = 0; cc < num_corners; cc++) { + pointf corner_pt = bz.list[corners[cc].idx]; + if (hypot(bz.list[pt].x - corner_pt.x, bz.list[pt].y - corner_pt.y) < CORNER_TOL) { + at_corner = true; + break; + } + } + if (!at_corner) { + seg_pt_count++; + } + } + seg_pt_count++; /* seg_end_pt */ + + /* Build segment */ + pointf *seg_pts = gv_calloc(seg_pt_count, sizeof(pointf)); + seg_pts[0] = seg_start_pt; + size_t seg_idx = 1; + for (size_t pt = seg_start_idx + 1; pt < seg_end_idx; pt++) { + bool at_corner = false; + for (size_t cc = 0; cc < num_corners; cc++) { + pointf corner_pt = bz.list[corners[cc].idx]; + if (hypot(bz.list[pt].x - corner_pt.x, bz.list[pt].y - corner_pt.y) < CORNER_TOL) { + at_corner = true; + break; + } + } + if (!at_corner) { + seg_pts[seg_idx++] = bz.list[pt]; + } + } + seg_pts[seg_idx] = seg_end_pt; + + /* Render this segment as polyline (straight lines, not bezier) */ + if (seg_pt_count > 1) { + gvrender_polyline(job, seg_pts, seg_pt_count); + } + free(seg_pts); + + /* Prepare for next segment */ + if (c < num_corners) { + /* Skip all duplicates of this corner */ + size_t next_idx = corners[c].idx + 1; + while (next_idx < bz.size) { + bool at_corner = false; + for (size_t cc = 0; cc < num_corners; cc++) { + pointf corner_pt = bz.list[corners[cc].idx]; + if (hypot(bz.list[next_idx].x - corner_pt.x, + bz.list[next_idx].y - corner_pt.y) < CORNER_TOL) { + at_corner = true; + break; + } + } + if (!at_corner) break; + next_idx++; + } + seg_start_idx = next_idx; + seg_start_pt = corners[c].trunc_next; + } + } + + /* Draw corner arcs to fill the gaps */ + draw_ortho_corner_markers(job, corners, num_corners, radius, color); + } else { + /* No corners found, render normally */ + gvrender_beziercurve(job, bz.list, bz.size, 0); + } + free(corners); + } else { + /* Non-orthogonal edge, render normally */ + gvrender_beziercurve(job, bz.list, bz.size, 0); + } + if (bz.sflag) { arrow_gen(job, EMIT_TDRAW, bz.sp, bz.list[0], arrowsize, penwidth, bz.sflag); -- GitLab