#lang syndicate (require "../../widgets.rkt") (require racket/list racket/gui/base (except-in racket/class field)) ;; a circle drawer with undo/redo facilities (unclear spec for resizing) (message-struct circle-canvas-event (type x y)) (message-struct resize (circ d)) (message-struct draw-circles (closest others)) ;; --------------------------------------------------------------------------------------------------- (define Default-Diameter 20) (struct circle (x y d action) #:transparent) (define (draw-1-circle dc brush c) (match-define (circle x y d _a) c) (send dc set-brush brush) (define r (/ d 2)) (send dc draw-ellipse (- x r) (- y r) d d)) ;; N N (Circle -> Real] (define ((distance xm ym) c) (match-define (circle xc yc _d _a) c) (sqrt (+ (expt (- xc xm) 2) (expt (- yc ym) 2)))) ;; --------------------------------------------------------------------------------------------------- (define solid-gray (new brush% [color "gray"])) (define white-brush (new brush% [color "white"])) (define circle-canvas% (class canvas% (inherit on-paint get-dc) (define/override (on-event evt) (define type (send evt get-event-type)) (define x (send evt get-x)) (define y (send evt get-y)) (send-ground-message (circle-canvas-event type x y))) (define (paint-callback _self _evt) (draw-circles *last-closest *last-others)) (define *last-closest #f) (define *last-others #f) (define/public (draw-circles closest (others-without-closest #f)) (set! *last-closest closest) (set! *last-others others-without-closest) (define dc (get-dc)) (send dc clear) (when others-without-closest (for ((c others-without-closest)) (draw-1-circle dc white-brush c))) (when closest (draw-1-circle dc solid-gray closest))) (super-new [paint-callback paint-callback]))) (define (spawn-circle-canvas parent frame undo-but redo-but) (define cc (new circle-canvas% [parent (seal-contents parent)][style '(border)])) (spawn (field [*circles '()] [*history '()] [*x 0] [*y 0] [*in-adjuster #f]) (define (add-circle! x y) (define added (circle x y Default-Diameter 'added)) (*circles (cons added (*circles)))) (define (resize! old-closest new-d) (match-define (circle x y d a) old-closest) (define resized (match a ['added (circle x y new-d `(resized (,d)))] [`(resized . ,old-sizes) (circle x y new-d `(resized ,(cons d old-sizes)))])) (*circles (cons resized (remq old-closest (*circles))))) (define (undo) (when (cons? (*circles)) (define fst (first (*circles))) (match fst [(circle x y d 'added) (*circles (rest (*circles)))] [(circle x y d `(resized (,r0 . ,sizes))) (*circles (cons (circle x y r0 `(resized (,d))) (rest (*circles))))]) (*history (cons fst (*history))))) (define (redo) (when (cons? (*history)) (define fst (first (*history))) (if (eq? (circle-action fst) 'added) (begin (*circles (cons fst (*circles))) (*history (rest (*history)))) (begin (*circles (cons fst (rest (*circles)))) (*history (rest (*history))))))) (define (the-closest xm ym (circles (*circles))) (define cdistance (distance xm ym)) (define-values (good-circles distance*) (for*/fold ([good-circles '()][distance* '()]) ((c circles) (d (in-value (cdistance c))) #:when (< d (/ (circle-d c) 2))) (values (cons c good-circles) (cons d distance*)))) (and (cons? distance*) (first (argmin second (map list good-circles distance*))))) (define (is-empty-area xm ym (circles (*circles))) (define dist (distance xm ym)) (for/and ((c circles)) (> (dist c) (/ (+ (circle-d c) Default-Diameter) 2)))) (on (message 'unlock-canvas) (*in-adjuster #f)) (on (message 'lock-canvas) (*in-adjuster #t)) ;; no closest (define (draw!) (send cc draw-circles #f (*circles))) (on (message (resize $old-closest $new-d)) (resize! old-closest new-d) (draw!)) (on (message (draw-circles $close $others)) (send cc draw-circles close others)) (on (message (button-press undo-but)) (undo) (draw!)) (on (message (button-press redo-but)) (redo) (draw!)) (on (message (inbound (circle-canvas-event $type $x $y))) (unless (*in-adjuster) (*x x) (*y y) (cond [(eq? 'leave type) (*x #f)] [(eq? 'enter type) (*x 0)] [(and (eq? 'left-down type) (is-empty-area (*x) (*y))) (add-circle! (*x) (*y)) (draw!)] [(and (eq? 'right-down type) (the-closest (*x) (*y))) => (λ (tc) (*in-adjuster #t) (popup-adjuster tc *circles frame) (send cc draw-circles tc (*circles)))]))) )) (define (popup-adjuster closest-circle *circles frame) (define pid (gensym 'popup)) (send! (popup-menu frame pid "adjuster" 100 100 (list "adjust radius"))) (react (stop-when (message (no-popdown-selected pid)) (send! 'unlock-canvas)) (stop-when (message (popdown-item-selected pid _)) (adjuster! closest-circle *circles)))) (define (adjuster! closest-circle *circles) (define d0 (circle-d closest-circle)) (define frame (spawn-adjuster-dialog closest-circle (remq closest-circle (*circles)))) (spawn-adjuster-slider #:parent frame #:init-value d0)) (define adjuster-dialog% (class frame% (init-field closest-circle) (match-define (circle x* y* _d _a) closest-circle) (define/augment (on-close) (send-ground-message 'adjuster-closed)) (super-new [label (format "Adjust radius of circle at (~a,~a)" x* y*)]))) (define (spawn-adjuster-dialog closest-circle others) (match-define (circle x* y* old-d _a) closest-circle) (define dialog (parameterize ((current-eventspace (make-eventspace))) (new adjuster-dialog% [closest-circle closest-circle]))) (send dialog show #t) (spawn ;; well, there's only one slider (define/query-value *d old-d (slider@ _ $v) v) (on (message (slider-update _ $v)) ;; resize locally while adjusting (send! (draw-circles (circle x* y* (*d) '_dummy_) others))) (on (message (inbound 'adjuster-closed)) ;; resize globally (send! 'unlock-canvas) (send! (resize closest-circle (*d))) (stop-current-facet))) (seal dialog)) (define (spawn-adjuster-slider #:parent parent #:init-value init-value) (spawn-slider #:parent parent #:label "" #:min-value 10 #:max-value 100 #:init-value init-value)) ;; --------------------------------------------------------------------------------------------------- (define frame (spawn-frame #:label "Circle Drawer" #:width 400)) (define hpane1 (spawn-horizontal-pane #:parent frame #:min-height 20 #:alignment '(center center))) (define undo-but (spawn-button #:label "Undo" #:parent hpane1)) (define redo-but (spawn-button #:label "Redo" #:parent hpane1)) (define hpane2 (spawn-horizontal-panel #:parent frame #:min-height 400 #:alignment '(center center))) (define canvas (spawn-circle-canvas hpane2 frame undo-but redo-but)) (spawn (on (asserted (frame@ frame)) (send! (show frame #t)) (stop-current-facet)))