601 lines
24 KiB
Racket
601 lines
24 KiB
Racket
#lang racket/gui
|
|
|
|
(require "check-aux.rkt"
|
|
"timer.rkt"
|
|
"last.rkt"
|
|
"checked-cell.rkt"
|
|
"stop.rkt"
|
|
"universe-image.rkt"
|
|
"pad.rkt"
|
|
(only-in 2htdp/image scale overlay/align)
|
|
htdp/error
|
|
mzlib/runtime-path
|
|
mrlib/bitmap-label
|
|
string-constants
|
|
mrlib/gif)
|
|
|
|
(provide world% aworld%)
|
|
|
|
;
|
|
;
|
|
;
|
|
; ; ; ; ;
|
|
; ; ; ; ;
|
|
; ; ; ; ;
|
|
; ; ; ;;; ; ;; ; ;;;;
|
|
; ; ; ; ; ;; ; ; ; ;
|
|
; ; ; ; ; ; ; ; ; ; ;
|
|
; ;; ;; ; ; ; ; ; ;
|
|
; ; ; ; ; ; ; ; ;
|
|
; ; ; ;;; ; ;; ;;;;
|
|
;
|
|
;
|
|
;
|
|
|
|
(define MIN-WIDT-FOR-GAME-PAD 300)
|
|
|
|
;; -----------------------------------------------------------------------------
|
|
;; packages for broadcasting information to the universe
|
|
|
|
(define-values (make-package package? package-world package-message)
|
|
(let ()
|
|
(struct package (world message) #:transparent)
|
|
(define (make-package w m)
|
|
(check-arg 'make-package (sexp? m) 'sexp "second" m)
|
|
(package w m))
|
|
(values make-package package? package-world package-message)))
|
|
|
|
(provide
|
|
make-package ;; World S-expression -> Package
|
|
package? ;; Any -> Package
|
|
package-world ;; Package -> World
|
|
)
|
|
|
|
(define world%
|
|
(last-mixin
|
|
(clock-mixin
|
|
(class* object% (start-stop<%>)
|
|
(inspect #f)
|
|
(init-field world0)
|
|
(init-field name state register check-with on-key on-release on-pad on-mouse record?)
|
|
(init on-receive on-draw stop-when)
|
|
|
|
;; -----------------------------------------------------------------------
|
|
(field
|
|
[to-draw on-draw]
|
|
[world
|
|
(new checked-cell% [value0 world0] [ok? check-with]
|
|
[display (and state (or name "your world program's state"))])])
|
|
|
|
|
|
;; -----------------------------------------------------------------------
|
|
(field [*out* #f] ;; (U #f OutputPort), where to send messages to
|
|
[*rec* (make-custodian)]) ;; Custodian, monitor traffic)
|
|
|
|
(define/private (register-with-host)
|
|
(define FMT "\nworking off-line\n")
|
|
(define FMTtry
|
|
(string-append "unable to register with ~a after ~s tries"
|
|
FMT))
|
|
(define FMTcom
|
|
(string-append "unable to register with ~a due to protocol problems"
|
|
FMT))
|
|
;; Input-Port -> [-> Void]
|
|
;; create closure (for thread) to receive messages and signal events
|
|
(define (RECEIVE in)
|
|
(define (RECEIVE)
|
|
(sync
|
|
(handle-evt
|
|
in
|
|
(lambda (in)
|
|
(define dis (text "the universe disappeared" 11 'red))
|
|
(with-handlers ((tcp-eof?
|
|
(compose (handler #f)
|
|
(lambda (e)
|
|
(set! draw (lambda (w) dis))
|
|
(pdraw)
|
|
e))))
|
|
;; --- "the universe disconnected" should come from here ---
|
|
(define msg (tcp-receive in))
|
|
(cond
|
|
[(sexp? msg) (prec msg) (RECEIVE)] ;; break loop if EOF
|
|
[#t (error 'RECEIVE "sexp expected, received: ~e" msg)]))))))
|
|
RECEIVE)
|
|
;; --- now register, obtain connection, and spawn a thread for receiving
|
|
(parameterize ([current-custodian *rec*])
|
|
;; try to register with the server n times
|
|
(let try ([n TRIES])
|
|
(printf "trying to register with ~a ...\n" register)
|
|
(with-handlers ((tcp-eof? (lambda (x) (printf FMTcom register)))
|
|
(exn:fail:network?
|
|
(lambda (x)
|
|
(if (= n 1)
|
|
(printf FMTtry register TRIES)
|
|
(begin (sleep PAUSE) (try (- n 1)))))))
|
|
(define-values (in out) (tcp-connect register SQPORT))
|
|
(tcp-register in out name)
|
|
(printf "... successful registered and ready to receive\n")
|
|
(set! *out* out)
|
|
(thread (RECEIVE in))))))
|
|
|
|
(define/private (broadcast msg)
|
|
(when *out*
|
|
(check-result 'send sexp? "Sexp expected; given ~e\n" msg)
|
|
(tcp-send *out* msg)))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
(field
|
|
(draw (cond
|
|
[(procedure? to-draw) to-draw]
|
|
[(pair? to-draw) (first to-draw)]
|
|
[else to-draw]))
|
|
(live (not (boolean? draw)))
|
|
(width (if (pair? to-draw) (second to-draw) #f))
|
|
(height (if (pair? to-draw) (third to-draw) #f)))
|
|
|
|
;; the visible world
|
|
(field [enable-images-button void] ;; used if stop-when call produces #t
|
|
[disable-images-button void]
|
|
[visible (new pasteboard%)])
|
|
|
|
(define/private (show-canvas)
|
|
(send visible set-cursor (make-object cursor% 'arrow))
|
|
(let ([fst-scene (ppdraw)])
|
|
(if (2:image? fst-scene)
|
|
(let ([first-width (+ (image-width fst-scene) 1)]
|
|
[first-height (+ (image-height fst-scene) 1)])
|
|
(unless (and width height)
|
|
(check-scene-dimensions (name-of draw 'your-draw) first-width first-height)
|
|
(set! width first-width)
|
|
(set! height first-height)))
|
|
(let ([first-width (image-width fst-scene)]
|
|
[first-height (image-height fst-scene)])
|
|
(unless (and width height)
|
|
(set! width first-width)
|
|
(set! height first-height))))
|
|
(when pad
|
|
(unless (>= width MIN-WIDT-FOR-GAME-PAD)
|
|
(error 'big-bang
|
|
"a game pad requires a scene whose width is greater or equal to ~a, given ~e"
|
|
MIN-WIDT-FOR-GAME-PAD fst-scene))
|
|
(set! game-pad-image (scale (/ width (image-width game-pad)) game-pad)))
|
|
(create-frame)
|
|
(show fst-scene)))
|
|
|
|
(define/private (add-game-pad scene)
|
|
(if (boolean? pad) scene (overlay/align 'left 'bottom game-pad-image scene)))
|
|
|
|
(define/public (deal-with-key %)
|
|
(if (and (not on-key) (not on-pad) (not on-release))
|
|
%
|
|
(class %
|
|
(super-new)
|
|
(define/override (on-char e)
|
|
(when live
|
|
(let ([e:str (key-event->parts e)])
|
|
(cond
|
|
[(string=? e:str "release") (prelease (key-release->parts e))]
|
|
[(and pad (pad-event? e:str)) (ppad e:str)]
|
|
[else (pkey e:str)])))))))
|
|
|
|
(define/public (deal-with-mouse %)
|
|
(if (not on-mouse)
|
|
;; No mouse handler => discard mouse events (so snip are not selected
|
|
;; in the pasteboard, for example
|
|
(class %
|
|
(super-new)
|
|
(define/override (on-event e)
|
|
(void)))
|
|
;; Mouse handler => handle mouse events
|
|
(class %
|
|
(super-new)
|
|
(define/override (on-event e)
|
|
(define-values (x y me) (mouse-event->parts e))
|
|
(when live
|
|
(cond
|
|
[(and (<= 0 x width) (<= 0 y height)) (pmouse x y me)]
|
|
[(member me '("leave" "enter")) (pmouse x y me)]
|
|
[else (void)]))))))
|
|
|
|
;; allows embedding of the world-canvas in other GUIs
|
|
(define/public (create-frame)
|
|
(create-frame/universe))
|
|
|
|
;; effect: create, show and set the-frame
|
|
(define/pubment (create-frame/universe)
|
|
(define play-back:cust (make-custodian))
|
|
(define frame (new (class frame%
|
|
(super-new)
|
|
(define/augment (on-close)
|
|
(callback-stop! 'frame-stop)
|
|
(custodian-shutdown-all play-back:cust)))
|
|
(label (if name (format "~a" name) "World"))
|
|
(alignment '(center center))
|
|
(style '(no-resize-border metal))))
|
|
|
|
(define editor-canvas
|
|
(new (deal-with-key (deal-with-mouse editor-canvas%))
|
|
(parent frame)
|
|
(editor visible)
|
|
(stretchable-width #f)
|
|
(stretchable-height #f)
|
|
(style '(no-hscroll no-vscroll))
|
|
(horizontal-inset INSET)
|
|
(vertical-inset INSET)))
|
|
(send editor-canvas min-client-width (+ width INSET INSET))
|
|
(send editor-canvas min-client-height (+ height INSET INSET))
|
|
(set!-values (enable-images-button disable-images-button)
|
|
(inner (values void void) create-frame/universe frame play-back:cust))
|
|
(send editor-canvas focus)
|
|
(send frame show #t))
|
|
|
|
;; Image -> Void
|
|
;; show the image in the visible world
|
|
(define/public (show pict0)
|
|
(define pict (add-game-pad pict0))
|
|
(send visible begin-edit-sequence)
|
|
(send visible lock #f)
|
|
(let ([s (send visible find-first-snip)]
|
|
[c (send visible get-canvas)])
|
|
(when s (send visible delete s))
|
|
(send visible insert (disable-cache (send pict copy)) 0 0)
|
|
(send visible lock #t)
|
|
(send visible end-edit-sequence)
|
|
;; The following flush trades streaming performance (where updates
|
|
;; could be skipped if they're replaced fast enough) for
|
|
;; responsiveness (where too many updates might not get
|
|
;; through if the canvas is mostly in suspended-refresh
|
|
;; mode for scene changes):
|
|
#;
|
|
(send c flush)))
|
|
|
|
;; ----------------------------------------------------------------------
|
|
;; callbacks
|
|
(field
|
|
(key (if on-key on-key (lambda (w ke) w)))
|
|
(pad on-pad)
|
|
(game-pad-image #f)
|
|
(release (if on-release on-release (lambda (w ke) w)))
|
|
(mouse on-mouse)
|
|
(rec on-receive))
|
|
|
|
(define drawing #f) ;; Boolean; is a draw callback scheduled?
|
|
(define (set-draw#!) (set! draw# (random 3)) (set! drawing #f))
|
|
(define draw# 0)
|
|
(set-draw#!)
|
|
|
|
(define-syntax def/cback
|
|
(syntax-rules ()
|
|
[(_ pub (name arg ...) transform)
|
|
(def/cback pub (name arg ...) transform (object-name transform))]
|
|
[(_ pub (name arg ...) transform tag)
|
|
;; Any ... -> Boolean
|
|
(begin
|
|
(define/public (name arg ...)
|
|
(queue-callback
|
|
(lambda ()
|
|
(define H (handler #t))
|
|
(with-handlers ([exn? H])
|
|
; (define tag (object-name transform))
|
|
(define nw (transform (send world get) arg ...))
|
|
(define (d)
|
|
(with-handlers ((exn? H))
|
|
(pdraw))
|
|
(set-draw#!))
|
|
;; ---
|
|
;; [Listof (Box [d | void])]
|
|
(define w '())
|
|
;; set all to void, then w to null
|
|
;; when a high priority draw is scheduledd
|
|
;; ---
|
|
(when (package? nw)
|
|
(broadcast (package-message nw))
|
|
(set! nw (package-world nw)))
|
|
(if (stop-the-world? nw)
|
|
(begin
|
|
(set! nw (stop-the-world-world nw))
|
|
(send world set tag nw)
|
|
(last-draw)
|
|
(callback-stop! 'name)
|
|
(enable-images-button))
|
|
(let ([changed-world? (send world set tag nw)]
|
|
[stop? (pstop)])
|
|
;; this is the old "Robby optimization" see checked-cell:
|
|
; unless changed-world?
|
|
(cond
|
|
[(and draw (not stop?))
|
|
(cond
|
|
[(not drawing)
|
|
(set! drawing #t)
|
|
(let ([b (box d)])
|
|
(set! w (cons b w))
|
|
;; low priority, otherwise it's too fast
|
|
(queue-callback (lambda () ((unbox b))) #f))]
|
|
[(< draw# 0)
|
|
(set-draw#!)
|
|
(for-each (lambda (b) (set-box! b void)) w)
|
|
(set! w '())
|
|
;; high!! the scheduled callback didn't fire
|
|
(queue-callback (lambda () (d)) #t)]
|
|
[else
|
|
(set! draw# (- draw# 1))])]
|
|
[stop?
|
|
(last-draw)
|
|
(callback-stop! 'name)
|
|
(enable-images-button)])
|
|
changed-world?)))))))]))
|
|
|
|
;; tick, tock : deal with a tick event for this world
|
|
(def/cback pubment (ptock) (lambda (w) (pptock w)) (name-of-tick-handler))
|
|
(define/public (pptock w) (void))
|
|
(define/public (name-of-tick-handler)
|
|
"the on-tick handler")
|
|
|
|
;; key events
|
|
(def/cback pubment (pkey ke) key)
|
|
|
|
;; key events
|
|
(def/cback pubment (ppad ke) pad)
|
|
|
|
;; release events
|
|
(def/cback pubment (prelease ke) release)
|
|
|
|
;; mouse events
|
|
(def/cback pubment (pmouse x y me) mouse)
|
|
|
|
;; receive revents
|
|
(def/cback pubment (prec msg) rec)
|
|
|
|
;; ----------------------------------------------------------------------
|
|
;; -> Void
|
|
;; draw : render the given world or this world (if #f)
|
|
(define/private (pdraw)
|
|
(show (ppdraw)))
|
|
|
|
;; -> Scene
|
|
;; produce the scene for the this state
|
|
(define/public (ppdraw)
|
|
(check-scene-result (name-of draw 'your-draw) (draw (send world get))))
|
|
|
|
;; -----------------------------------------------------------------------
|
|
;; stop-when
|
|
(field [stop (if (procedure? stop-when) stop-when (first stop-when))]
|
|
[last-picture (if (pair? stop-when) (second stop-when) #f)])
|
|
|
|
(define/private (last-draw)
|
|
(when last-picture (set! draw last-picture))
|
|
(pdraw))
|
|
|
|
(define/private (pstop)
|
|
(define result (stop (send world get)))
|
|
(check-result (name-of stop 'your-stop-when) boolean? "boolean" result)
|
|
result)
|
|
|
|
;; ----------------------------------------------------------------------
|
|
;; start & stop
|
|
(define/public (callback-stop! msg)
|
|
(stop! (send world get)))
|
|
|
|
(define (handler re-raise)
|
|
(lambda (e)
|
|
(disable-images-button)
|
|
(stop! (if re-raise e (send world get)))))
|
|
|
|
(define/public (start!)
|
|
(with-handlers ([exn? (handler #t)])
|
|
(when width ;; and height
|
|
(check-scene-dimensions "your to-draw clause" width height))
|
|
(when register (register-with-host))
|
|
(define w (send world get))
|
|
(cond
|
|
[(stop w)
|
|
(when last-picture (set! draw last-picture))
|
|
(show-canvas)
|
|
(stop! w)]
|
|
[(stop-the-world? w)
|
|
(when last-picture (set! draw last-picture))
|
|
(show-canvas)
|
|
(stop! (stop-the-world-world w))]
|
|
[else (show-canvas)])))
|
|
|
|
(define/public (stop! w)
|
|
(set! live #f)
|
|
(custodian-shutdown-all *rec*))
|
|
|
|
;; -------------------------------------------------------------------------
|
|
;; initialize the world and run
|
|
(super-new)
|
|
(start!)))))
|
|
|
|
; (define make-new-world (new-world world%))
|
|
|
|
;; -----------------------------------------------------------------------------
|
|
(define-runtime-path break-btn:path '(lib "icons/break.png"))
|
|
(define break-button:label
|
|
((bitmap-label-maker (string-constant break-button-label) break-btn:path) '_))
|
|
|
|
(define-runtime-path image-button:path '(lib "icons/file.gif"))
|
|
(define image-button:label ((bitmap-label-maker "Images" image-button:path) '_))
|
|
|
|
(define aworld%
|
|
(class world% (super-new)
|
|
(inherit-field world0 draw rate width height record?)
|
|
(inherit show callback-stop!)
|
|
|
|
;; -> String or false
|
|
(define/private (recordable-directory)
|
|
(and (path-string? record?) (directory-exists? record?) record?))
|
|
|
|
;; Frame Custodian ->* (-> Void) (-> Void)
|
|
;; adds the stop animation and image creation button,
|
|
;; whose callbacks runs as a thread in the custodian
|
|
(define/augment (create-frame/universe frm play-back-custodian)
|
|
(define p (new horizontal-pane% [parent frm][alignment '(center center)]))
|
|
(define (pb)
|
|
(parameterize ([current-custodian play-back-custodian])
|
|
(thread (lambda () (play-back)))
|
|
(stop)))
|
|
(define (switch)
|
|
(send stop-button enable #f)
|
|
(if (recordable-directory) (pb) (send image-button enable #t)))
|
|
(define (stop)
|
|
(send image-button enable #f)
|
|
(send stop-button enable #f))
|
|
(define-syntax-rule (btn l a y ...)
|
|
(new button% [parent p] [label l] [style '(border)]
|
|
[callback (lambda a y ...)]))
|
|
(define stop-button
|
|
(btn break-button:label (b e) (callback-stop! 'stop-images) (switch)))
|
|
(define image-button
|
|
(btn image-button:label (b e) (pb)))
|
|
(send image-button enable #f)
|
|
(values switch stop))
|
|
|
|
;; an argument-recording ppdraw
|
|
(field [image-history '()]) ;; [Listof Evt]
|
|
(define/override (ppdraw)
|
|
(define image (super ppdraw))
|
|
(set! image-history (cons image image-history))
|
|
image)
|
|
|
|
;; --> Void
|
|
;; re-play the history of events; create a png per step; create animated gif
|
|
;; effect: write to user-chosen directory
|
|
(define/private (play-back)
|
|
;; World EventRecord -> World
|
|
(define (world-transition world fst) (apply (car fst) world (cdr fst)))
|
|
;; --- creating images
|
|
(define total (+ (length image-history) 1))
|
|
(define digt# (string-length (number->string total)))
|
|
(define imag# 0)
|
|
(define bmps '())
|
|
;; Image -> Void
|
|
(define (save-image img)
|
|
(define bm (make-object bitmap% width height))
|
|
(define dc (make-object bitmap-dc% bm))
|
|
(send dc clear)
|
|
(send img draw dc 0 0 0 0 width height 0 0 #f)
|
|
(set! imag# (+ imag# 1))
|
|
(send bm save-file (format "i~a.png" (zero-fill imag# digt#)) 'png)
|
|
(set! bmps (cons bm bmps))
|
|
img)
|
|
;; --- choose place
|
|
(define img:dir
|
|
(or (recordable-directory)
|
|
(get-directory "image directory:" #f (current-directory))))
|
|
(when img:dir
|
|
(parameterize ([current-directory img:dir])
|
|
(define imageN
|
|
(if (empty? image-history)
|
|
(save-image (draw world0))
|
|
(first (map save-image image-history))))
|
|
(show (text (format "creating ~a" ANIMATED-GIF-FILE) 18 'red))
|
|
(create-animated-gif rate bmps)
|
|
(show imageN))))))
|
|
|
|
;; Number [Listof (-> bitmap)] -> Void
|
|
;; turn the list of thunks into animated gifs
|
|
;; effect: overwrite the ANIMATED-GIF-FILE (in current directory)
|
|
;; [Listof (-> bitmap)] -> Void
|
|
;; turn the list of thunks into animated gifs
|
|
;; effect: overwrite the ANIMATED-GIF-FILE (in current directory)
|
|
(define (create-animated-gif R bitmap-list)
|
|
(when (file-exists? ANIMATED-GIF-FILE) (delete-file ANIMATED-GIF-FILE))
|
|
(write-animated-gif bitmap-list (if (> +inf.0 R 0) (number->integer R) 5)
|
|
ANIMATED-GIF-FILE
|
|
#:one-at-a-time? #t
|
|
#:loop? #f))
|
|
|
|
(define ANIMATED-GIF-FILE "i-animated.gif")
|
|
|
|
;; the version of aworld below records all events (pointers to functions)
|
|
;; and replays them starting from the initial world. In terms of space, this
|
|
;; is quite efficient because there are only six differente actions (pointers)
|
|
;; BUT, it doesn't work with random or other effectful stuff
|
|
;; EXPLORE: put random into the library and make it an event
|
|
(define aworld-old%
|
|
(class world% (super-new)
|
|
(inherit-field world0 tick key pad release mouse rec draw rate width height record?)
|
|
(inherit show callback-stop!)
|
|
|
|
;; Frame Custodian ->* (-> Void) (-> Void)
|
|
;; adds the stop animation and image creation button,
|
|
;; whose callbacks runs as a thread in the custodian
|
|
(define/augment (create-frame/universe frm play-back-custodian)
|
|
(define p (new horizontal-pane% [parent frm][alignment '(center center)]))
|
|
(define (pb)
|
|
(parameterize ([current-custodian play-back-custodian])
|
|
(thread (lambda () (play-back)))
|
|
(stop)))
|
|
(define (switch)
|
|
(send stop-button enable #f)
|
|
(if (and (string? record?) (directory-exists? record?))
|
|
(pb)
|
|
(send image-button enable #t)))
|
|
(define (stop)
|
|
(send image-button enable #f)
|
|
(send stop-button enable #f))
|
|
(define-syntax-rule (btn l a y ...)
|
|
(new button% [parent p] [label l] [style '(border)]
|
|
[callback (lambda a y ...)]))
|
|
(define stop-button
|
|
(btn break-button:label (b e) (callback-stop! 'stop-images) (switch)))
|
|
(define image-button
|
|
(btn image-button:label (b e) (pb)))
|
|
(send image-button enable #f)
|
|
(values switch stop))
|
|
|
|
(field [event-history '()]) ;; [Listof Evt]
|
|
;; Symbol Any *-> Void
|
|
(define/private (add-event type . stuff)
|
|
(set! event-history (cons (cons type stuff) event-history)))
|
|
|
|
;; --- new callbacks ---
|
|
(define-syntax-rule
|
|
(def/cb ovr (pname name arg ...))
|
|
(define/override (pname arg ...)
|
|
(when (super pname arg ...) (add-event name arg ...))))
|
|
|
|
(def/cb augment (ptock tick))
|
|
(def/cb augment (pkey key e))
|
|
(def/cb augment (ppad pad e))
|
|
(def/cb augment (prelease release e))
|
|
(def/cb augment (pmouse mouse x y me))
|
|
(def/cb augment (prec rec m))
|
|
|
|
;; --> Void
|
|
;; re-play the history of events; create a png per step; create animated gif
|
|
;; effect: write to user-chosen directory
|
|
(define/private (play-back)
|
|
;; World EventRecord -> World
|
|
(define (world-transition world fst) (apply (car fst) world (cdr fst)))
|
|
;; --- creating images
|
|
(define total (+ (length event-history) 1))
|
|
(define digt# (string-length (number->string total)))
|
|
(define imag# 0)
|
|
(define bmps '())
|
|
;; Image -> Void
|
|
(define (save-image img)
|
|
(define bm (make-object bitmap% width height))
|
|
(define dc (make-object bitmap-dc% bm))
|
|
(send dc clear)
|
|
(send img draw dc 0 0 0 0 width height 0 0 #f)
|
|
(set! imag# (+ imag# 1))
|
|
(send bm save-file (format "i~a.png" (zero-fill imag# digt#)) 'png)
|
|
(set! bmps (cons bm bmps)))
|
|
;; --- choose place
|
|
(define img:dir
|
|
(or (and (string? record?) (directory-exists? record?) record?)
|
|
(get-directory "image directory:" #f (current-directory))))
|
|
(when img:dir
|
|
(parameterize ([current-directory img:dir])
|
|
(define worldN
|
|
(let L ([history event-history][world world0])
|
|
(save-image (draw world))
|
|
(if (empty? history)
|
|
world
|
|
(L (rest history) (world-transition world (first history))))))
|
|
(show (text (format "creating ~a" ANIMATED-GIF-FILE) 18 'red))
|
|
(create-animated-gif rate (reverse bmps))
|
|
(show (draw worldN)))))))
|