;;; -*- mode:lisp; package: graph; base:10.; -*- ;;; ;;; LAYOUT ;;; This file contains code to implement layout algorithms for the ;;; graph editor. ;;; ;;; It includes TWO TYPES of utilities: ;;; 1. Facilities to manipulate already-drawn graphs to make ;;; them look better. ;;; 2. Facilities to draw graphs from existing structure. ;;; This second sort may be useful with existing graphs. You ;;; could, for example, erase a set of nodes and arcs, pass ;;; the information to this feature, and have that part of ;;; the graph re-drawn nicely. ;;; **************************************************************** ;;; Necessary Loads, Constants, Declarations. ;;; **************************************************************** ;;; **************************************************************** ;;; Internal Macros, Structure Definitions, FLAVOR definitions. ;;; **************************************************************** ;;; **************************************************************** ;;; Internal Code ;;; **************************************************************** ;;; **************** ;;; Rearranging existing graphs ;;; **************** ;;; Top level message. (defmethod (graph-display-mixin :improve-layout-all-nodes) () (let ((swap-nodes (improve-layout nodes))) (when swap-nodes (send (car swap-nodes) ':exchange-with (cdr swap-nodes))))) (defmethod (graph-display-mixin :improve-layout-some-nodes) () (let ((swap-nodes (improve-layout (send self ':select-many-nodes)))) (when swap-nodes (send (car swap-nodes) ':exchange-with (cdr swap-nodes))))) ;;; This method leaves the screen pretty messed up if there are ;;; designer arcs so we refresh the screen when we aredone. (defmethod (graph-display-mixin :improve-layout-some-nodes-to-the-max) () (loop with node-list = (send self ':select-many-nodes) for swap = (improve-layout node-list) until (null swap) do (send (car swap) ':exchange-with (cdr swap))) (send self ':redraw)) ;;; Top level code to "clean up" a section of a graph. This will consider ;;; what would happen if you exchanged pairs of nodes in a set. The approach ;;; used here is typical of hill climbing: you may make local improvements that ;;; won't work well overall. Because of this, the implementer should couple ;;; this code with some interaction with the user. ;;; This routine loops over pairs of nodes in a set until a swap is done, ;;; then returns. If a swap was done, this returns CONS of the two nodes that ;;; were swapped. If no swap was done, returns nil. (defun improve-layout (node-list) (loop for node1 in node-list if (loop for node2 in node-list if (maybe-swap node1 node2) return (cons node1 node2) finally nil) return it finally nil)) ;;; This function considers exchanging the position of two nodes, and seeing which ;;; results in fewer overlaps between arcs. If the exchange results in fewer overlaps, ;;; it returns T. (defun maybe-swap (node1 node2) (with-drawing-inhibited (let ((overlaps (+ (send node1 ':compute-arc-overlaps) (send node2 ':compute-arc-overlaps)))) (send node1 ':exchange-with node2) (if (prog1 (< (+ (send node1 ':compute-arc-overlaps) (send node2 ':compute-arc-overlaps)) overlaps) (send node1 ':exchange-with node2)) t nil)))) ;;; Interactive version of below. (defmethod (basic-node :interactive-exchange-node) () (with-object-flashing self (send self ':exchange-with (send window ':select-node)))) ;;; Exchanges two nodes. Doesn't do anything about guaranteeing overlaps. (defmethod (basic-node :exchange-with) (node) (erase self) (erase node) (let ((old-x x) (old-y y)) (setq x (send node ':x) y (send node ':y)) (send node ':set-x old-x) (send node ':set-y old-y) (loop for arc in arcs do (send arc ':compute-joint-points)) (loop for arc in (send node ':arcs) do (send arc ':compute-joint-points))) (draw self) (draw node)) ;;; A method used to compute the number of overlaps between arcs. This ;;; computes the overlaps of arcs touching this node with other arcs. An ;;; arc does NOT overlap itself. Two arcs overlap if any of their segments ;;; intersect. (defmethod (basic-node :compute-arc-overlaps) () (loop for arc1 in arcs sum (loop for arc2 in (send window ':arcs) count (send arc1 ':overlap-p arc2)))) ;;; Returns T if this arc overlaps another arc. Does this by examining segments. (defmethod (basic-arc :overlap-p) (arc) (unless (eq arc self) (loop for (x1 y1 x2 y2 . rest) on joint-points by 'cddr until (null x2) if (loop for (x3 y3 x4 y4 . rest) on (send arc ':joint-points) by 'cddr until (null x4) if (overlap-segment x1 y1 x2 y2 x3 y3 x4 y4) return t finally nil) return t finally nil))) ;;; Finally, the meat. Computes whether two line-segments overlap. (defun overlap-segment (ax1 ay1 ax2 ay2 bx1 by1 bx2 by2) ;; If the lines are parallel, then by definition they don't ;; overlap. (Even if they are colinear, it won't help to swap ;; endpoints. (unless (parallel-p ax1 ay1 ax2 ay2 bx1 by1 bx2 by2) (multiple-value-bind (intx inty) (intersection-point ax1 ay1 ax2 ay2 bx1 by1 bx2 by2) (and (segment-contains-point ax1 ay1 ax2 ay2 intx inty) (segment-contains-point bx1 by1 bx2 by2 intx inty))))) ;;; Parallel lines have the same slope. Careful not to divide by zero. (defun parallel-p (x1 y1 x2 y2 x3 y3 x4 y4) (cond ;; If both segments have y = 0, return t. ((and (= y1 y2) (= y3 y4)) t) ;; If only one has y = 0, nil ((or (= y1 y2) (= y3 y4)) nil) ;; Otherwise, compare slopes (t (= (// (- x2 x1) (float (- y2 y1))) (// (- x4 x3) (float (- y4 y3))))))) ;;; Computes the intersection-point of two lines defined by points. ;;; We know the two lines are NOT parallel at this point. (defun intersection-point (x1 y1 x2 y2 x3 y3 x4 y4) (cond ((eq x1 x2) ;If first line is vertical (values x1 (+ (* (// (- y4 y3) (float (- x4 x3))) (- x1 x3)) y3))) ((eq x3 x4) ;If second line is vertical (values x3 (+ (* (// (- y2 y1) (float (- x2 x1))) (- x3 x1)) y1))) (t ;Otherwise, safe to calc slopes. (let* ((slope1 (// (- y2 y1) (float (- x2 x1)))) (slope2 (// (- y4 y3) (float (- x4 x3)))) (x (// (- (+ (* slope1 x1) y3) (* slope2 x3) y1) (- slope1 slope2)))) (values x (+ (* slope1 (- x x1)) y1)))))) ;;; Returns T if the segment contains the point indicated. We can ;;; cheat, since we know that the point is on the line defined by ;;; the segment. (defun segment-contains-point (x1 y1 x2 y2 x y) (and (<= (min x1 x2) x (max x1 x2)) (<= (min y1 y2) y (max y1 y2))))