/* * COPYRIGHT * * cschem - modular/flexible schematics editor - libcschem (core library) * Copyright (C) 2018,2022,2023 Tibor 'Igor2' Palinkas * * (Supported by NLnet NGI0 PET Fund in 2022, Entrust in 2023) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version.* * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 31 Milk Street, # 960789 Boston, MA 02196 USA * * Contact: * Project page: http://repo.hu/projects/sch-rnd * contact lead developer: http://www.repo.hu/projects/sch-rnd/contact.html * mailing list: http://www.repo.hu/projects/sch-rnd/contact.html */ #include "config.h" #include #include #include "event.h" #include "concrete.h" #include "cnc_obj.h" #include "cnc_conn.h" #include "cnc_line.h" #include "cnc_arc.h" #include "op_common.h" #include "operation.h" #include "intersect.h" #include "undo.h" #include #include #include csch_conn_t *csch_conn_alloc(csch_sheet_t *sheet, csch_cgrp_t *parent, csch_oid_t oid) { csch_conn_t *conn; conn = htip_get(&parent->id2obj, oid); if (conn != NULL) return NULL; conn = calloc(sizeof(csch_conn_t), 1); csch_cobj_init(&conn->hdr, sheet, parent, oid, CSCH_CTYPE_CONN); return conn; } void csch_conn_free(csch_conn_t *conn) { long n; for(n = 0; n < conn->conn_path.used; n++) csch_oidpath_free(&conn->conn_path.array[n]); vtp0_uninit(&conn->conn); vtl0_uninit(&conn->coords); csch_vtoidpath_uninit(&conn->conn_path); csch_cobj_uninit(&conn->hdr); free(conn); } csch_conn_t *csch_conn_get(csch_sheet_t *sheet, csch_oid_t oid) { csch_conn_t *conn = htip_get(&sheet->direct.id2obj, oid); if ((conn != NULL) && (conn->hdr.type != CSCH_CTYPE_CONN)) return NULL; return conn; } void csch_conn_text2ptr(csch_sheet_t *sheet, csch_conn_t *conn) { long n; vtp0_enlarge(&conn->conn, conn->conn_path.used); conn->conn.used = 0; for(n = 0; n < conn->conn_path.used; n++) { csch_chdr_t *obj = csch_oidpath_resolve(sheet, &conn->conn_path.array[n]); csch_chdr_t *grp; if (obj != NULL) { grp = (csch_chdr_t *)obj->parent; if (csch_obj_is_grp(grp)) csch_conn_add_obj(sheet, conn, obj, 0); else rnd_msg_error("Invalid oid path in connection %ld: not a group or group-ref\n", conn->hdr.oid); } else rnd_msg_error("Can not resolve oid path in connection %ld\n", conn->hdr.oid); csch_oidpath_free(&conn->conn_path.array[n]); } csch_vtoidpath_truncate(&conn->conn_path, 0); } /* append one or more connection points of o to tmp in x;y form */ RND_INLINE void conn_append_pts(csch_conn_t *conn, csch_chdr_t *o, vtl0_t *tmp) { switch(o->type) { case CSCH_CTYPE_LINE: { csch_line_t *l = (csch_line_t *)o; vtl0_append(tmp, l->inst.c.p1.x); vtl0_append(tmp, l->inst.c.p1.y); vtl0_append(tmp, l->inst.c.p2.x); vtl0_append(tmp, l->inst.c.p2.y); vtl0_append(tmp, (l->inst.c.p1.x + l->inst.c.p2.x)/2); vtl0_append(tmp, (l->inst.c.p1.y + l->inst.c.p2.y)/2); } break; case CSCH_CTYPE_ARC: { csch_arc_t *a = (csch_arc_t *)o; g2d_vect_t v; v = g2d_carc_offs(&a->inst.c, 0); vtl0_append(tmp, v.x); vtl0_append(tmp, v.y); if (a->inst.c.delta < 1.8 * G2D_PI) { v = g2d_carc_offs(&a->inst.c, 1); vtl0_append(tmp, v.x); vtl0_append(tmp, v.y); } if (a->inst.c.delta > 0.5*G2D_PI) { v = g2d_carc_offs(&a->inst.c, 0.5); vtl0_append(tmp, v.x); vtl0_append(tmp, v.y); } } break; default: /* fallback: bbox center */ vtl0_append(tmp, (o->bbox.x1 + o->bbox.x2)/2); vtl0_append(tmp, (o->bbox.y1 + o->bbox.y2)/2); break; } } /* append a single line of shortest connection between o1 and o2 notable points to tmp in x1;y1;x2;y2 line form */ RND_INLINE void conn_recalc_add_line_npts(csch_conn_t *conn, csch_chdr_t *o1, csch_chdr_t *o2, vtl0_t *tmp) { int o2_start, besti1 = -1, besti2 = -1, i1, i2; double best = (double)CSCH_COORD_MAX * (double)CSCH_COORD_MAX; /* collect o1 and o2 points in tmp */ tmp->used = 0; conn_append_pts(conn, o1, tmp); o2_start = tmp->used; conn_append_pts(conn, o2, tmp); /* find shortest line */ for(i1 = 0; i1 < o2_start; i1+=2) { csch_coord_t x1 = tmp->array[i1], y1 = tmp->array[i1+1]; for(i2 = o2_start; i2 < tmp->used; i2+=2) { csch_coord_t x2 = tmp->array[i2], y2 = tmp->array[i2+1]; double dx = x2 - x1, dy = y2 - y1, dist = dx*dx + dy*dy; if (dist < best) { best = dist; besti1 = i1; besti2 = i2; } } } /* append results */ assert(besti1 >= 0); assert(besti2 > besti1); vtl0_append(&conn->coords, tmp->array[besti1]); vtl0_append(&conn->coords, tmp->array[besti1+1]); vtl0_append(&conn->coords, tmp->array[besti2]); vtl0_append(&conn->coords, tmp->array[besti2+1]); csch_bbox_bump(&conn->hdr.bbox, x, tmp->array[besti1]); csch_bbox_bump(&conn->hdr.bbox, y, tmp->array[besti1+1]); csch_bbox_bump(&conn->hdr.bbox, x, tmp->array[besti2]); csch_bbox_bump(&conn->hdr.bbox, y, tmp->array[besti2+1]); } /* Search for centerline intersections between o1 and o2, create zero-length conn lines in those points and return the number of points found. */ RND_INLINE int conn_recalc_add_line_cent_isc(csch_conn_t *conn, csch_chdr_t *o1, csch_chdr_t *o2, vtl0_t *tmp) { csch_sheet_t *sheet = o1->sheet; g2d_vect_t iscp[32]; int maxlen = sizeof(iscp) / sizeof(iscp[0]); long n, len; if (o1->parent == o2->parent) return 0; len = csch_obj_intersect_obj(sheet, o1, o2, iscp, maxlen); if (len > maxlen) len = maxlen; for(n = 0; n < len; n++) { vtl0_append(&conn->coords, iscp[n].x); vtl0_append(&conn->coords, iscp[n].y); vtl0_append(&conn->coords, iscp[n].x); vtl0_append(&conn->coords, iscp[n].y); csch_bbox_bump(&conn->hdr.bbox, x, iscp[n].x); csch_bbox_bump(&conn->hdr.bbox, y, iscp[n].y); } return len; } /* Append new connection line(s) between o1 and o2; prefer zero-length conns at center-line intersections, fall back to non-zero-length conns between notable points when there's no centerline intersection */ RND_INLINE void conn_recalc_add_line(csch_conn_t *conn, csch_chdr_t *o1, csch_chdr_t *o2, vtl0_t *tmp) { if (o1->parent == o2->parent) return; /* corner case: never indicate connection within the same group */ if (conn_recalc_add_line_cent_isc(conn, o1, o2, tmp) == 0) conn_recalc_add_line_npts(conn, o1, o2, tmp); /* fallback */ } void csch_conn_recalc_coords(csch_conn_t *conn) { int i1, i2, readd_rtree; vtl0_t tmp = {0}; csch_sheet_t *sheet = conn->hdr.sheet; /* try removing the object from its rtree; if it succeeds, add it back after the recalculation */ readd_rtree = (sheet != NULL) && (csch_cobj_rtree_del(sheet, &conn->hdr) == 0); csch_bbox_reset(&conn->hdr.bbox); conn->coords.used = 0; for(i1 = 0; i1 < conn->conn.used; i1++) for(i2 = i1+1; i2 < conn->conn.used; i2++) conn_recalc_add_line(conn, conn->conn.array[i1], conn->conn.array[i2], &tmp); if (readd_rtree) csch_cobj_rtree_add(sheet, &conn->hdr); vtl0_uninit(&tmp); } void csch_conn_update(csch_sheet_t *sheet, csch_conn_t *conn, int do_xform) { /* live data has pointers, not oid paths */ if ((conn->conn_path.used > 0) && (conn->conn.used == 0)) { csch_conn_text2ptr(sheet, conn); } csch_cobj_rtree_del(sheet, &conn->hdr); csch_conn_recalc_coords(conn); csch_cobj_rtree_add(sheet, &conn->hdr); csch_cobj_bbox_changed(sheet, &conn->hdr); conn->dirty = 0; } static long find_obj_in_conn(const csch_chdr_t *obj, const csch_conn_t *conn) { long n; for(n = 0; n < conn->conn.used; n++) if (conn->conn.array[n] == obj) return n; return -1; } static long find_conn_in_obj(const csch_conn_t *conn, const csch_chdr_t *obj) { long n; for(n = 0; n < obj->conn.used; n++) if (obj->conn.array[n] == conn) return n; return -1; } RND_INLINE long csch_conn_add_obj_(csch_sheet_t *sheet, csch_conn_t *conn, csch_chdr_t *obj, long *cidx) { long ci, gi; ci = find_obj_in_conn(obj, conn); if (ci >= 0) { if (cidx != NULL) *cidx = -1; return -1; } gi = find_conn_in_obj(conn, obj); if (gi >= 0) { if (cidx != NULL) *cidx = gi; return -1; } vtp0_append(&conn->conn, obj); vtp0_append(&obj->conn, conn); if (cidx != NULL) *cidx = obj->conn.used - 1; return 0; } RND_INLINE int csch_conn_del_obj_(csch_sheet_t *sheet, csch_conn_t *conn, csch_chdr_t *obj) { long ci, gi; ci = find_obj_in_conn(obj, conn); gi = find_conn_in_obj(conn, obj); if (ci >= 0) vtp0_remove(&conn->conn, ci, 1); if (gi >= 0) vtp0_remove(&obj->conn, gi, 1); if ((ci < 0) || (gi < 0)) return -1; return 0; } void csch_conn_ptr2text(csch_sheet_t *sheet, csch_conn_t *conn) { long n; csch_vtoidpath_enlarge(&conn->conn_path, conn->conn.used); conn->conn_path.used = conn->conn.used; for(n = 0; n < conn->conn.used; n++) csch_oidpath_from_obj(&conn->conn_path.array[n], conn->conn.array[n]); /* this changes conn->conn, so go backward */ for(n = conn->conn.used-1; n >= 0; n--) csch_conn_del_obj_(sheet, conn, conn->conn.array[n]); conn->conn.used = 0; /* just in case... to make sure the code resolves the oidpaths */ conn->dirty = 1; } /*** undoable conn-obj add/del ***/ typedef struct { csch_sheet_t *sheet; csch_conn_t *conn; csch_chdr_t *obj; long last_cidx; unsigned add:1; /* if 1, swap should add, else swap should del (swap always inverts afterwards) */ } undo_conn_obj_t; static int undo_conn_obj_swap_(void *udata, int update) { undo_conn_obj_t *u = udata; if (u->add) csch_conn_add_obj_(u->sheet, u->conn, u->obj, &u->last_cidx); else csch_conn_del_obj_(u->sheet, u->conn, u->obj); if (update) /* recalc coords (the graphics) when called from undo/redo */ csch_conn_update(u->sheet, u->conn, 0); u->add = !u->add; return 0; } static int undo_conn_obj_swap(void *udata) { return undo_conn_obj_swap_(udata, 1); } static void undo_conn_obj_print(void *udata, char *dst, size_t dst_len) { undo_conn_obj_t *u = udata; gds_t tmp; csch_oidpath_t oidp = {0}; tmp.used = 0; tmp.alloced = dst_len; tmp.array = dst; tmp.no_realloc = 1; csch_oidpath_from_obj(&oidp, u->obj); rnd_append_printf(&tmp, "%s object ", u->add ? "add" : "del"); csch_oidpath_to_str_append(&tmp, &oidp); rnd_append_printf(&tmp, " in conn #%ld", (long)u->conn->hdr.oid); csch_oidpath_free(&oidp); } static const char core_conn_cookie[] = "libcschem/core/cnc_conn.c"; static const uundo_oper_t undo_conn_obj_modify = { core_conn_cookie, NULL, undo_conn_obj_swap, undo_conn_obj_swap, undo_conn_obj_print }; RND_INLINE int csch_conn_do_obj(csch_sheet_t *sheet, csch_conn_t *conn, csch_chdr_t *obj, int add, long *cidx, int undoable) { int res; undo_conn_obj_t utmp, *u = &utmp; if (undoable) u = uundo_append(&sheet->undo, &undo_conn_obj_modify, sizeof(undo_conn_obj_t)); u->sheet = sheet; u->conn = conn; u->obj = obj; u->add = add; res = undo_conn_obj_swap_(u, 0); /* update 0: expect the caller to do the update */ if (undoable) csch_undo_inc_serial(sheet); if (cidx != NULL) *cidx = u->last_cidx; return res; } int csch_conn_del_obj(csch_sheet_t *sheet, csch_conn_t *conn, csch_chdr_t *obj, int undoable) { return csch_conn_do_obj(sheet, conn, obj, 0, NULL, undoable); } int csch_conn_add_obj(csch_sheet_t *sheet, csch_conn_t *conn, csch_chdr_t *obj, int undoable) { return csch_conn_do_obj(sheet, conn, obj, 1, NULL, undoable); } /* returns conn index in obj or -1 on error */ long csch_conn_add_obji(csch_sheet_t *sheet, csch_conn_t *conn, csch_chdr_t *obj, int undoable) { long res; csch_conn_do_obj(sheet, conn, obj, 1, &res, undoable); return res; } /* Return conn where: obj1 is in the connection and another object from obj2's parent is in too (or return NULL if not found) */ RND_INLINE csch_conn_t *conn_find_common(csch_chdr_t *obj1, csch_chdr_t *obj2, long *o1i, csch_conn_t *ignore) { long n, m; csch_conn_t *conn; csch_chdr_t *o; for(n = 0; n < obj1->conn.used; n++) { conn = obj1->conn.array[n]; if (conn == ignore) continue; for(m = 0; m < conn->conn.used; m++) { o = conn->conn.array[m]; if (o->parent == obj2->parent) { if (o1i != NULL) *o1i = n; return conn; } } } if (o1i != NULL) *o1i = -1; return NULL; } csch_conn_t *csch_conn_find_mergable(csch_sheet_t *sheet, csch_conn_t *conn, csch_chdr_t *owner) { long n; for(n = 0; n < conn->conn.used; n++) { csch_chdr_t *co = conn->conn.array[n]; csch_conn_t *res = conn_find_common(co, owner, NULL, conn); if (res != NULL) return res; } return NULL; } RND_INLINE csch_conn_t *csch_conn_auto_add_(csch_sheet_t *sheet, csch_chdr_t *obj1, csch_chdr_t *obj2, long *obj1i) { csch_conn_t *conn; /* check if there's an existing connection we can extend; requirement is: obj1/2 is in the connection and another object from obj2/1's parent is in too */ conn = conn_find_common(obj1, obj2, obj1i, NULL); if (conn != NULL) { if (find_obj_in_conn(obj2, conn) < 0) { csch_conn_add_obj(sheet, conn, obj2, 1); csch_conn_update(sheet, conn, 1); } return conn; } conn = conn_find_common(obj2, obj1, NULL, NULL); if (conn != NULL) { *obj1i = find_obj_in_conn(obj1, conn); if (*obj1i < 0) { *obj1i = csch_conn_add_obji(sheet, conn, obj1, 1); csch_conn_update(sheet, conn, 1); } return conn; } /* there was no common conn, create a new one */ conn = (csch_conn_t *)csch_op_create(sheet, &sheet->direct, CSCH_CTYPE_CONN); *obj1i = csch_conn_add_obji(sheet, conn, obj1, 1); csch_conn_add_obj(sheet, conn, obj2, 1); csch_conn_update(sheet, conn, 1); return conn; } csch_conn_t *csch_conn_auto_add(csch_sheet_t *sheet, csch_chdr_t *obj1, csch_chdr_t *obj2) { long tmp; return csch_conn_auto_add_(sheet, obj1, obj2, &tmp); } long csch_conn_auto_addi(csch_sheet_t *sheet, csch_chdr_t *obj1, csch_chdr_t *obj2) { long tmp = -1; csch_conn_auto_add_(sheet, obj1, obj2, &tmp); return tmp; } /* Return 1 if the connection lists at least 2 objects with different parent groups (so that the connection is valid) */ static int conn_has_multiple_groups(csch_sheet_t *sheet, csch_conn_t *conn) { long n; csch_chdr_t *obj; csch_cgrp_t *par; if (conn->conn.used < 2) return 0; obj = conn->conn.array[0]; par = obj->parent; for(n = 1; n < conn->conn.used; n++) { obj = conn->conn.array[n]; if (obj->parent != par) return 1; } return 0; } int csch_conn_auto_del_obj(csch_sheet_t *sheet, csch_conn_t *conn, csch_chdr_t *obj) { int res; TODO("This may not be undoable; check with 3 objects crossing"); res = csch_conn_del_obj(sheet, conn, obj, 1); if (res != 0) return res; /* if connection doesn't connect two groups anymore, remove it */ if (!conn_has_multiple_groups(sheet, conn)) { long n; res = 0; for(n = conn->conn.used-1; n >= 0; n--) res |= csch_conn_del_obj(sheet, conn, conn->conn.array[n], 1); if (res != 0) return res; } if (conn->conn.used == 0) csch_op_remove(sheet, &conn->hdr); else csch_conn_update(sheet, conn, 1); return 0; } int csch_conn_auto_del_obj_all(csch_sheet_t *sheet, csch_chdr_t *obj) { long n; int res = 0; for(n = obj->conn.used-1; n >= 0; n--) res |= csch_conn_auto_del_obj(sheet, obj->conn.array[n], obj); return res; } static csch_chdr_t *conn_create(csch_sheet_t *sheet, csch_cgrp_t *parent) { csch_conn_t *conn = csch_conn_alloc(sheet, parent, csch_oid_new(sheet, parent)); if (conn == NULL) return NULL; return &conn->hdr; } static void conn_remove_alloc(csch_undo_remove_t *slot) { } static void conn_remove_redo(csch_undo_remove_t *slot) { csch_cnc_common_remove_redo(slot, CSCH_REM_FROM_RTREE | CSCH_REM_FROM_PARENT | CSCH_REM_DEL_EMPTY_PARENT); } static void conn_remove_undo(csch_undo_remove_t *slot) { csch_cnc_common_remove_undo(slot, CSCH_REM_FROM_RTREE | CSCH_REM_FROM_PARENT | CSCH_REM_DEL_EMPTY_PARENT); } static int conn_isc_with_box(csch_chdr_t *obj, csch_rtree_box_t *box) { return 0; /* a connection is a purely logical concept, has no geometry */ } const csch_ops_t csch_ops_conn = { conn_create, conn_remove_alloc, conn_remove_redo, conn_remove_undo, conn_isc_with_box }; /*** conn recalc ***/ typedef struct { csch_sheet_t *sheet; csch_chdr_t *wobj; gds_t keep; } find_conn_t; static csch_rtree_dir_t find_conn_cb(void *ctx_, void *obj_, const csch_rtree_box_t *box) { find_conn_t *ctx = ctx_; csch_chdr_t *obj = obj_; long ci; char *keep; if ((obj == ctx->wobj) || (obj->parent == ctx->wobj->parent)) return csch_RTREE_DIR_NOT_FOUND_CONT; if (csch_obj_is_grp(obj)) return csch_RTREE_DIR_NOT_FOUND_CONT; if (csch_obj_intersect_obj(obj->sheet, ctx->wobj, obj, NULL, 0) <= 0) return csch_RTREE_DIR_NOT_FOUND_CONT; ci = csch_conn_auto_addi(obj->sheet, ctx->wobj, obj); keep = gds_get(&ctx->keep, ci, 1); *keep = 1; return csch_RTREE_DIR_FOUND_CONT; } /* Remove objects from conn that are not intersecting with main_obj */ static void csch_conn_remove_disconnected_objs(csch_sheet_t *sheet, csch_conn_t *conn, csch_chdr_t *main_obj) { long n; for(n = conn->conn.used-1; n >= 0; n--) { csch_chdr_t *obj = conn->conn.array[n]; if (csch_obj_intersect_obj(obj->sheet, main_obj, obj, NULL, 0) <= 0) csch_conn_auto_del_obj(sheet, conn, obj); } } void csch_recalc_obj_conn(csch_sheet_t *sheet, csch_chdr_t *wobj, csch_displayer_t target, csch_displayer_t target2) { find_conn_t ctx = {0}; long n; ctx.sheet = sheet; ctx.wobj = (csch_chdr_t *)wobj; if (wobj->conn.used != 0) { gds_enlarge(&ctx.keep, wobj->conn.used+4); memset(ctx.keep.array, 0, wobj->conn.used+4); ctx.keep.used = wobj->conn.used; } /* we may be called on deleted objects to get their conns removed */ if (!csch_obj_is_deleted(wobj)) { csch_rtree_search_obj(&sheet->dsply[target], &wobj->bbox, find_conn_cb, &ctx); if (target2 != CSCH_DSPLY_invalid) csch_rtree_search_obj(&sheet->dsply[target2], &wobj->bbox, find_conn_cb, &ctx); } for(n = ctx.keep.used-1; n >= 0; n--) { if (ctx.keep.array[n]) { csch_conn_remove_disconnected_objs(sheet, wobj->conn.array[n], wobj); if (wobj->conn.used > n) csch_conn_update(sheet, wobj->conn.array[n], 1); continue; } rnd_trace("! remove conn %ld\n", n); csch_conn_auto_del_obj(sheet, wobj->conn.array[n], wobj); } } static void csch_conn_auto_recalc_(csch_sheet_t *sheet, csch_chdr_t *obj, int in_term, int is_deleted) { if (csch_obj_is_grp(obj)) { csch_cgrp_t *grp = (csch_cgrp_t *)obj; htip_entry_t *e; if (grp->role == CSCH_ROLE_TERMINAL) in_term = 1; for(e = htip_first(&grp->id2obj); e != NULL; e = htip_next(&grp->id2obj, e)) csch_conn_auto_recalc_(sheet, e->value, in_term, is_deleted); } else { if ((in_term) || ((obj->parent != NULL) && (obj->parent->role == CSCH_ROLE_WIRE_NET))) { if (is_deleted) csch_conn_auto_del_obj_all(sheet, obj); else csch_recalc_obj_conn(sheet, obj, in_term ? CSCH_DSPLY_WIRE : CSCH_DSPLY_HUBTERM, in_term ? CSCH_DSPLY_HUBTERM : CSCH_DSPLY_invalid); } } } void csch_conn_auto_recalc(csch_sheet_t *sheet, csch_chdr_t *obj) { csch_conn_auto_recalc_(sheet, obj, 0, csch_obj_is_deleted(obj)); } void csch_conn_auto_recalc_geo(csch_sheet_t *sheet, csch_chdr_t *obj) { long n; for(n = 0; n < obj->conn.used; n++) csch_conn_recalc_coords((csch_conn_t *)obj->conn.array[n]); /* recurse into children */ if (csch_obj_is_grp(obj)) { csch_cgrp_t *grp = (csch_cgrp_t *)obj; htip_entry_t *e; for(e = htip_first(&grp->id2obj); e != NULL; e = htip_next(&grp->id2obj, e)) csch_conn_auto_recalc_geo(sheet, e->value); } else if (obj->type == CSCH_CTYPE_CONN) csch_conn_recalc_coords((csch_conn_t *)obj); } void csch_conn_update_dirties(csch_sheet_t *sheet, csch_cgrp_t *grp, int do_xform) { htip_entry_t *e; for(e = htip_first(&grp->id2obj); e != NULL; e = htip_next(&grp->id2obj, e)) { csch_cgrp_t *g = e->value; csch_conn_t *c = e->value; if ((c->hdr.type == CSCH_CTYPE_CONN) && c->dirty) csch_conn_update(sheet, c, do_xform); else if (csch_obj_is_grp(&g->hdr)) csch_conn_update_dirties(sheet, g, do_xform); } }