;;; -*- Mode: LISP; Syntax: Common-Lisp; Package: GARNET-GADGETS; Base: 10 -*- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; The Garnet User Interface Development Environment. ;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; This code was written as part of the Garnet project at ;;; ;;; Carnegie Mellon University, and has been placed in the public ;;; ;;; domain. If you are using this code or any part of Garnet, ;;; ;;; please contact garnet@cs.cmu.edu to be put on the mailing list. ;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; ;;; These functions enable a user to edit a polygon that has AT LEAST ;;; TWO (2) points. If it has only one point, strange things will happen. ;;; ;;; These are the user visible functions: ;;; ;;; toggle-polyline-handles: creates (if necessary) & displays the handles to ;;; allow polyline editing. If handles are already ;;; exposed, turns it off. ;;; destroy-polyline-handles: destroys the polyline handles ;;; ;;; CHANGE LOG: ;;; ;;; 10/18/93 Andrew Mickish - Moved export list to polyline-creator.lisp ;;; 8/02/93 Andrew Mickish - Reordered function declarations ;;; 7/26/93 Andrew Mickish - Added special proclamation to avoid warning ;;; 6/23/93 Andrew Mickish - Exported hide-polyline-handles ;;; 6/17/93 Rajan Parthasarathy - Created! (in-package "GARNET-GADGETS") (declaim (special POLYLINE-POINT-DELETING-INTER)) ;;; polyline-edit-handle: This is the handle with which the user can ;;; drag, create, and delete polyline points. Basically, it's a 7 by 7 ;;; square centered around the point. The point is specified by setting ;;; the :xy slot to be the xy pair. The handle is visible when it is a ;;; member of the :polyline-handles-being-used slot of the polyline creator. ;;; This means that it's being used now, and so, it should be visible. (create-instance 'polyline-edit-handle opal:rectangle (:xy '(0 0)) (:x-coord (o-formula (first (gvl :xy)))) (:y-coord (o-formula (second (gvl :xy)))) (:polyline (o-formula (gvl :polyline-creator :polyline-being-edited))) (:polyline-creator NIL) (:rank (o-formula (position (gv :self) (gvl :polyline-creator :polyline-handles-being-used)))) (:visible (o-formula (member (gv :self) (gvl :polyline-creator :polyline-handles-being-used)))) (:width 7) (:height 7) (:left (o-formula (- (first (gvl :xy)) 3))) (:top (o-formula (- (second (gvl :xy)) 3))) (:draw-function :xor) (:fast-redraw-p T) (:filling-style opal:black-fill) (:line-style NIL)) ;;; polyline-mover-feedback: This is the dashed feedback polyline ;;; that appears when we move a point. :attach-points are the two ;;; points on either side of the point being moved. (create-instance 'polyline-mover-feedback opal:polyline (:polyline (o-formula (gvl :parent :operates-on))) (:attach-points '(0 0 0 0)) (:box '(0 0)) (:draw-function :xor) (:fast-redraw-p T) (:point-list (o-formula (list (first (gvl :attach-points)) (second (gvl :attach-points)) (+ (first (gvl :box)) 3) (+ (second (gvl :box)) 3) (third (gvl :attach-points)) (fourth (gvl :attach-points))))) (:line-style opal:dashed-line)) ;;; polyline-point-moving-inter-start-action: ;;; This is called by the point-moving interactor when it starts ;;; up. Basically, we have to set the feedback object to be ;;; visible, and then set up the correct attach points for the ;;; feedback object. Then, the interactor will set the :box slot ;;; of the feedback-obj and everything will sail smoothly. (defun polyline-point-moving-inter-start-action (inter handle box) (when (g-value handle :visible) (let* ((poly (g-value handle :polyline)) (points (g-value poly :point-list)) (len (length points)) (attach-points NIL) (rank (g-value handle :rank)) (feedback (g-value inter :feedback-obj))) (cond ;; If this is the FIRST point in an open polyline, ;; set the attach-points to be the second point ((AND (eq rank 0) (NOT (g-value poly :closed-polyline-p))) (setf attach-points (list (third points) (fourth points) (third points) (fourth points)))) ;; If this is the LAST point in an open polyline, ;; set the attach-points to be the (last - 1)th point ((AND (eq (* 2 (1+ rank)) len) (NOT (g-value poly :closed-polyline-p))) (setf attach-points (list (nth (- len 4) points) (nth (- len 3) points) (nth (- len 4) points) (nth (- len 3) points)))) ;; If this is the first or last point in a CLOSED polyline, ;; set the attach-points to be the second and (last - 1)th ;; points ((AND (OR (eq rank 0) (eq (* 2 (1+ rank)) len)) (g-value poly :closed-polyline-p)) (setf attach-points (list (third points) (fourth points) (nth (- len 4) points) (nth (- len 3) points)))) ;; If this is NOT an end point, set the attach points to be the ;; points before and after the current point. (T (setf attach-points (list (nth (* 2 (1- rank)) points) (nth (1+ (* 2 (1- rank))) points) (nth (* 2 (1+ rank)) points) (nth (1+ (* 2 (1+ rank))) points))))) (s-value feedback :attach-points attach-points) (s-value feedback :box (list (first box) (second box)))))) ;;; polyline-point-moving-inter-stop-action: ;;; This is called with the point-moving interactor has stopped. ;;; We need to adjust the point-list of the polyline and the xy ;;; coordinates of the current handle to reflect the new position ;;; of the point. (defun polyline-point-moving-inter-stop-action (inter handle box) (when (g-value handle :visible) (let* ((poly (g-value handle :polyline)) (points (copy-list (g-value poly :point-list))) (x (+ 3 (first box))) (y (+ 3 (second box))) (rank (g-value handle :rank)) (len (1- (length points)))) (s-value (g-value inter :feedback-obj) :visible NIL) ;; Change the points in the :point-list (setf (nth (* 2 rank) points) x) (setf (nth (1+ (* 2 rank)) points) y) ;; If we're changing the first point in a closed ;; polyline, remember to adjust the last point so ;; it stays closed! (when (AND (= rank 0) (g-value poly :closed-polyline-p)) (setf (nth len points) y) (setf (nth (1- len) points) x)) ;; s-value the changes. (s-value poly :point-list points) (s-value handle :xy (list x y)) (opal:update (g-value poly :window)) ;; Call the modify-function. If the :added-handle slot is set, ;; want to call it with action :add-move, else call it with action ;; :move. (if (g-value inter :added-handle) (progn (s-value inter :added-handle NIL) (kr-send (g-value inter :operates-on) :modify-function (g-value inter :operates-on) poly (* 2 rank) :add-move)) (kr-send (g-value inter :operates-on) :modify-function (g-value inter :operates-on) poly (* 2 rank) :move))))) ;;; polyline-point-moving-inter: ;;; This is the interactor that moves the points. It's waiting priority ;;; is high so that if the same button is used for both adding/moving a ;;; point, it'll move the point if we are using a handle. The running ;;; action is for the case when someone is dragging a point and hits the ;;; delete key. That point dies. The abort action turns off the feedback ;;; line. start-action and stop-action for this interactor are explained ;;; above. (create-instance 'polyline-point-moving-inter inter:move-grow-interactor (:polyline-creator NIL) (:waiting-priority inter:running-priority-level) (:start-where (o-formula (list :list-element-of (gvl :polyline-creator) :polyline-handles-being-used))) (:window (o-formula (gvl :polyline-creator :polyline-being-edited :window))) (:start-action #'polyline-point-moving-inter-start-action) ;; If someone hits the delete key while dragging a point, the point should ;; be deleted! (:running-action #'(lambda (inter handle loc) (s-value (g-value inter :feedback-obj) :visible T) (when (eq #\delete (inter:event-char inter:*current-event*)) (inter:abort-interactor inter) (polyline-point-deleting-inter-stop-action inter handle)) (call-prototype-method inter handle loc))) (:stop-action #'polyline-point-moving-inter-stop-action) (:abort-action #'(lambda (inter handle) ;; When handle has been added, remove it! ;; This is inefficient, but since this case doesn't ;; come up often, it's okay. (when (g-value inter :added-handle) (polyline-point-deleting-inter-stop-action inter (g-value inter :added-handle)) (s-value inter :added-handle NIL)) (s-value (g-value inter :feedback-obj) :visible NIL) (call-prototype-method inter handle)))) ;;; polyline-point-deleting-inter-stop-action: ;;; This is the stop action for the interactor that handles deleting ;;; a point. Basically, we remove the handle from the ;;; :polyline-handles-being-used list, and remove the point from ;;; the :point-list. We should put the handle for the deleted point ;;; in the :stored-polyline-handles list so that some other polyline ;;; can use that handle. ;;; ;;; We also have to be careful when we delete a point in ;;; a closed polyline, because a closed polyline should STAY CLOSED. ;;; Also, if there are only 2 points in the polyline, no more points ;;; can be deleted. (defun polyline-point-deleting-inter-stop-action (inter handle) (when (g-value handle :visible) (let* ((poly (g-value handle :polyline)) (points (copy-list (g-value poly :point-list))) (poly-creator (g-value poly :polyline-creator-obj)) (handles (copy-list (g-value poly-creator :polyline-handles-being-used))) (len (length points)) (rank (g-value handle :rank))) ;; Polyline should have minimum 2 points (when (> (length handles) 2) ;; If we are deleting the first or last point in ;; a closed polyline, make sure it stays closed by ;; putting a copy of the new first point at the ;; end of the :point-list. (if (AND (g-value poly :closed-polyline-p) (OR (eq rank 0) (eq (* 2 (1+ rank)) len))) (progn (setf handles (remove handle handles)) ;; have to remove handle from aggregate so we can reuse it. ;; otherwise, we get the "already has a parent" error (opal:remove-component (g-value poly :window :aggregate) handle) (push handle (g-value poly-creator :stored-polyline-handles)) (setf points (subseq points 2 (- len 2))) (setf points (append points (list (first points) (second points))))) (progn (setf handles (remove handle handles)) ;; have to remove handle from aggregate so we can reuse it. ;; otherwise, we get the "already has a parent" error (opal:remove-component (g-value poly :window :aggregate) handle) (push handle (g-value poly-creator :stored-polyline-handles)) (setf points (append (subseq points 0 (* 2 rank)) (subseq points (+ 2 (* 2 rank))))))) ;; s-value the changes so that update will notice it (s-value poly-creator :polyline-handles-being-used handles) (s-value poly :point-list points) (opal:update (g-value poly :window)) ;; call modify function with :delete UNLESS it a new handle has been ;; added and control-g has been hit while dragging. (when (is-a-p inter polyline-point-deleting-inter) (kr-send (g-value inter :operates-on) :modify-function (g-value inter :operates-on) poly (* 2 rank) :delete)) )))) ;;; polyline-point-deleting-inter: ;;; This is the point interactor that deletes points. It's just a non- ;;; continuous button interactor which deletes the point when it stops. ;;; The :stop-action function is explained above. (create-instance 'polyline-point-deleting-inter inter:button-interactor (:continuous NIL) (:waiting-priority inter:high-priority-level) (:polyline-creator NIL) (:start-where (o-formula (list :list-element-of (gvl :polyline-creator) :polyline-handles-being-used))) (:window (o-formula (gvl :polyline-creator :polyline-being-edited :window))) (:window (o-formula (gvl :polyline :window))) (:stop-action #'polyline-point-deleting-inter-stop-action)) ;;; polyline-point-adding-inter-stop-action: ;;; This mutha is called by the point adding interactor. To add a point, ;;; the user clicks on a line in the polyline. Since the user can't ;;; ACCURATELY click on the line, there's a threshold so that if you click ;;; so many pixels around the line, it is interpreted as a click on the ;;; line. Basically, we have to figure out between WHAT two points the ;;; new point lies. Once we find that, we just insert the new point and ;;; the new handle for that point in the appropriate places in the :point-list ;;; and the :polyline-handles-being-used list. (defun polyline-point-adding-inter-stop-action (inter poly) (when (g-value poly :polyline-creator-obj :polyline-handles-being-used) (let* ((x (inter:event-x inter:*current-event*)) (y (inter:event-y inter:*current-event*)) (thresh (g-value inter :threshold)) (points (copy-list (g-value poly :point-list))) (len (length points)) (poly-creator (g-value poly :polyline-creator-obj)) (handles (copy-list (g-value poly-creator :polyline-handles-being-used))) (rank NIL)) ;; For each point in the polyline, if the new point lies between ;; it and the next point, we insert it at that place, updating the ;; :point-list and the :polyline-handles-being-used slot. Then we ;; add the new handle to the window and get out of the loop. (dotimes (i (1- (/ len 2))) (when (opal::between-polyline-points (nth (* 2 i) points) (nth (1+ (* 2 i)) points) x y (nth (* 2 (1+ i)) points) (nth (1+ (* 2 (1+ i))) points) thresh) (let ((new-handle (if (g-value poly-creator :stored-polyline-handles) (progn (let ((handle-to-be-reused (pop (g-value poly-creator :stored-polyline-handles)))) (s-value handle-to-be-reused :xy (list x y)) (s-value handle-to-be-reused :polyline-creator poly-creator) handle-to-be-reused)) (create-instance NIL polyline-edit-handle (:xy (list x y)) (:polyline-creator poly-creator))))) (setf points (append (subseq points 0 (+ 2 (* 2 i))) (list x y) (subseq points (+ 2 (* 2 i))))) (setf handles (append (subseq handles 0 (1+ i)) (list new-handle) (subseq handles (1+ i)))) (s-value poly :point-list points) (s-value poly-creator :polyline-handles-being-used handles) (setf rank (* 2 (1+ i))) ;; Add the handle to the window so that it's visible (opal:add-component (g-value poly :window :aggregate) new-handle) (opal:update (g-value poly :window)) ;; When both the adding/moving interactors have the same ;; :start-event, we start the moving interactor. This way, ;; the user can create a point and move it immediately without ;; releasing the mouse button. (if (eq (g-value inter :start-event) (g-value poly :polyline-creator-obj :polyline-point-mover :start-event)) (progn (s-value (g-value poly :polyline-creator-obj :polyline-point-mover) :added-handle new-handle) (inter:start-interactor (g-value poly :polyline-creator-obj :polyline-point-mover))) ;; If not moving point, call modify-function with :add action (kr-send (g-value inter :operates-on) :modify-function (g-value inter :operates-on) poly rank :add)) (return))))))) ;;; polyline-point-adding-inter-start-where: ;;; want the thing to start when you hit on an outline, since a click in the ;;; polyline is not necessarily a click on the outline of the polyline. This ;;; function takes care of that. (defun polyline-point-adding-inter-start-where (obj inter ev) ;; Gotta make sure event is in right window! ;; Then, check that it's on that polyline outline (if (and (eq (g-value obj :window) (inter:event-window ev)) (opal::point-in-polyline (inter:event-x ev) (inter:event-y ev) (g-value obj :polyline-being-edited :point-list) (g-value inter :threshold) T NIL)) (g-value obj :polyline-being-edited) NIL)) ;;; polyline-point-adding-inter: ;;; This is the interactor that adds points to the polyline. It's non- ;;; continuous and all the work is done in the :stop-action, which is ;;; described above. (create-instance 'polyline-point-adding-inter inter:button-interactor (:continuous NIL) (:waiting-priority inter:high-priority-level) (:polyline-creator NIL) (:polyline (o-formula (gvl :polyline-creator :polyline-being-edited))) (:start-where (o-formula (list :custom (gvl :operates-on) #'polyline-point-adding-inter-start-where))) (:window (o-formula (gvl :polyline :window))) (:stop-action #'polyline-point-adding-inter-stop-action)) ;;; make-polyline-handles: ;;; This is the function that sets up the handles and the interactors. ;;; First, we check to see if a polyline is closed or not. When we ;;; assign a handle to a point, we want to check the :stored-polyline-handles ;;; slot to see if there's any handles we can reuse. If the :stored... ;;; list is empty, we create a new handle and put it in the ;;; :polyline-handles-being-used list. (defun make-polyline-handles (poly-creator) (let ((poly (g-value poly-creator :polyline-being-edited))) ;; Decide whether the polyline is closed or not (polyline-closed-or-not poly) (s-value poly :polyline-creator-obj poly-creator) ;; If polyline handles aren't already there, make them! (let* ((points (g-value poly :point-list)) (len (length points)) (closed (g-value poly :closed-polyline-p))) ;; If it is a closed polygon, don't create the last ;; handle, since that'll be 2 handles for one point! (dotimes (i (- (/ len 2) (if closed 1 0))) (let* ((x (nth (* i 2) points)) (y (nth (1+ (* i 2)) points)) ;; Get handle from storage, else create new one. (new-handle (if (g-value poly-creator :stored-polyline-handles) (progn (let ((handle-to-be-reused (pop (g-value poly-creator :stored-polyline-handles)))) (s-value handle-to-be-reused :xy (list x y)) (s-value handle-to-be-reused :polyline-creator poly-creator) handle-to-be-reused)) (create-instance NIL polyline-edit-handle (:xy (list x y)) (:polyline-creator poly-creator))))) ;; update the :polyline-handles-being-used list and put the new handle ;; in the aggregate so that it's visible (push new-handle (g-value poly-creator :polyline-handles-being-used)) (opal:add-component (g-value poly :window :aggregate) new-handle))) ;; since we are pushing the handles in, the list is in REVERSE order, ie ;; the last handle on the list is for the first point. So we reverse ;; the list so that the first handle in the list stands for the first ;; point, second handle for the second point, and so on (and so forth :) (s-value poly-creator :polyline-handles-being-used (reverse (g-value poly-creator :polyline-handles-being-used)))) (s-value poly-creator :mover-feedback-obj :visible NIL) (opal:add-component (g-value poly :window :aggregate) (g-value poly-creator :polyline-point-mover :feedback-obj)) (opal:update (g-value poly :window)) )) ;;; polyline-closed-or-not: ;;; If the polyline is closed (first & last points are equal), ;;; set the :closed-polyline-p slot to T (defun polyline-closed-or-not (poly) (s-value poly :closed-polyline-p NIL) (let* ((points (g-value poly :point-list)) (len (1- (length points)))) (when (AND (eq (first points) (nth (1- len) points)) (eq (second points) (nth len points))) (s-value poly :closed-polyline-p T)))) ;;; hide-polyline-handles: ;;; We have to remove the handles from the aggregate first, and then ;;; remove the feedback object for the mover from the aggregate. ;;; Then, we push all the handles currently being used into storage. (defun hide-polyline-handles (poly-creator) (let ((poly (g-value poly-creator :polyline-being-edited))) ;; Have to first remove the components from the agg -- the handles ;; and the durn feedback object (dolist (handle (g-value poly-creator :polyline-handles-being-used)) (opal:remove-component (g-value poly :window :aggregate) handle)) (when (member (g-value poly-creator :polyline-point-mover :feedback-obj) (g-value poly-creator :window :aggregate :components)) (opal:remove-component (g-value poly :window :aggregate) (g-value poly-creator :polyline-point-mover :feedback-obj))) ;; Now put the stuff back into the storage (s-value poly-creator :stored-polyline-handles (append (g-value poly-creator :stored-polyline-handles) (g-value poly-creator :polyline-handles-being-used))) ;; Now set the :polyline-being-edited slot to NIL (s-value poly-creator :polyline-being-edited NIL) (s-value poly-creator :polyline-handles-being-used NIL) (opal:update (g-value poly :window)))) ;;; toggle-polyline-handles: ;;; this function decides what object to show/hide polyline handles over, and ;;; calls make-polyline-handles, which makes the handles (defun toggle-polyline-handles (polyline-creator-obj obj) (when (is-a-p obj opal:polyline) ;; Turn off feedback, otherwise, it follows you around!! (s-value (g-value polyline-creator-obj :mover-feedback-obj) :visible NIL) (cond ;; If control-r over obj being edited, hide handles ((eq (g-value polyline-creator-obj :polyline-being-edited) obj) (gg::hide-polyline-handles polyline-creator-obj) (s-value polyline-creator-obj :polyline-being-edited NIL)) ;; If control-r over different obj, hide current obj's handles ;; and display new obj's handles ((g-value polyline-creator-obj :polyline-being-edited) (gg::hide-polyline-handles polyline-creator-obj) (s-value polyline-creator-obj :polyline-being-edited obj) (gg::make-polyline-handles polyline-creator-obj)) ;; Otherwise, just display the handles for current obj (T (s-value polyline-creator-obj :polyline-being-edited obj) (gg::make-polyline-handles polyline-creator-obj))))) ;;; destroy-polyline-handles: ;;; Destroys all the handles and interactors, and sets the ;;; slots to NIL, so that you can re-create the handles. ;;; If you don't set them to NIL, they contain the *destroyed* ;;; object and so, crashes. (defun destroy-polyline-handles (creator &optional erase) (s-value creator :polyline-being-edited NIL) (dolist (handle (append (g-value creator :polyline-handles-being-used) (g-value creator :stored-polyline-handles))) (opal:destroy handle erase)) (call-prototype-method creator erase)) ;; Tell the world that polyline-functions has been loaded ;; (setf (get :garnet-modules :polyline-functions) T)