1 (in-package #:clnl-model)
3 (defvar *separator* "@#$#@#$#@")
5 (defvar *current-interface* nil)
6 (defvar *current-callback* nil)
22 (defun set-callback (callback)
23 "SET-CALLBACK CALLBACK => RESULT
27 CALLBACK: a function that can take netlogo code
32 Sets the means by which the interface can call arbitrary netlogo code."
33 (setf *current-callback* callback))
35 (defun set-current-interface (interface)
36 "SET-CURRENT-INTERFACE INTERFACE => RESULT
40 INTERFACE: a list of widgets for display
45 Sets the currently running model to INTERFACE.
47 The widgets set here are comprised of the bare necessary
48 to run the engine with or without an actual visual component."
49 (setf *current-interface* interface))
51 (defun interface (model)
52 "INTERFACE MODEL => INTERFACE
56 MODEL: an object representing the model
57 INTERFACE: a list of widgets for display
61 INTERFACE returns the widgets in MODEL, used for display, or
62 setting with SET-CURRENT-INTERFACE."
63 (model-interface model))
65 (defun default-model ()
66 "DEFAULT-MODEL => MODEL
70 MODEL: an object representing the model
74 Returns the default startup model."
78 (make-view :left 10 :top 10 :min-pxcor -5 :max-pxcor 5 :min-pycor -5 :max-pycor 5 :patch-size 13d0))))
80 (defun read-from-nlogo (str)
81 "READ-FROM-NLOGO STR => MODEL
85 STR: a readable stream
86 MODEL: an object representing the model
90 Takes a stream STR, reads in a nlogo file, parses it, and then
91 returns the model object."
95 ((read-sections (&optional section)
97 ((line (read-line str nil)))
99 (if (string= *separator* line)
100 (cons section (read-sections))
101 (read-sections (append section (list line))))))))
104 :code (format nil "~{~A~^~%~}" (nth 0 sections))
105 :interface (parse-interface (nth 1 sections))
106 :info (nth 2 sections)
107 :turtle-shapes (nth 3 sections)
108 :version (nth 4 sections)
109 :preview-commands (nth 5 sections)
110 :system-dynamics (nth 6 sections)
111 :behavior-space (nth 7 sections)
112 :hub-net-client (nth 8 sections)
113 :link-shapes (nth 9 sections)
114 :model-settings (nth 10 sections)
115 :delta-tick (nth 11 sections))))
119 (defparameter *widget-parsers* nil)
121 (defmacro defwidget-definition (type &rest definitions)
130 (find (car def) (list :int :double :tnil-boolean :inverted-boolean :boolean :choice :string :option :code))
135 (lambda (,lines) ; Validator
141 ((line `(nth ,n ,lines)))
143 (:specified `(string= ,(second def) ,line))
144 (:int `(parse-integer ,line :junk-allowed t))
145 (:double `(ignore-errors (coerce (read-from-string ,line) 'double-float)))
146 (:boolean `(or (string= "1" ,line) (string= "0" ,line)))
147 (:inverted-boolean `(or (string= "0" ,line) (string= "1" ,line)))
148 (:tnil-boolean `(or (string= "T" ,line) (string= "NIL" ,line)))
149 (:choice `(find ,line ',(mapcar #'car (third def)) :test #'string=)))))
150 definitions (loop for i to (length definitions) collect i)))))
151 (lambda (,lines) ; Parser
152 (,(read-from-string (format nil "make-~A" type))
157 ((line `(nth ,n ,lines))
160 (:int `(parse-integer ,line))
161 (:double `(coerce (read-from-string ,line) 'double-float))
162 (:boolean `(string= "1" ,line))
163 (:inverted-boolean `(string= "0" ,line))
164 (:tnil-boolean `(string= "NIL" ,line))
165 (:choice `(cadr (find ,line ',(third def) :key #'car :test #'string=)))
166 (:option `(when (string/= ,line ,(third def)) ,line))
167 (:code `(unescape-code ,line))
169 (when val-getter (list (intern (symbol-name (cadr def)) :keyword) val-getter))))
170 definitions (loop for i to (length definitions) collect i))))))
173 (defwidget-definition view
174 (:specified "GRAPHICS-WINDOW")
188 (:boolean wrapping-allowed-in-x)
189 (:boolean wrapping-allowed-in-y)
195 (:choice update-mode (("0" :continuous) ("1" :tick-based)))
197 (:boolean show-tick-counter)
198 (:string tick-counter-label)
199 (:double frame-rate 30))
201 (defwidget-definition slider
202 (:specified "SLIDER")
214 (:option units "NIL")
215 (:choice direction (("HORIZONTAL" :horizontal) ("VERTICAL" :vertical))))
217 (defwidget-definition switch
218 (:specified "SWITCH")
225 (:inverted-boolean on)
229 (defwidget-definition button
230 (:specified "BUTTON")
235 (:option display "NIL")
237 (:tnil-boolean forever)
240 (:string button-type)
245 (:boolean go-time)) ; should it wait for ticks to be initialized
247 (defun parse-interface (interface-as-strings)
251 ((separate-widgets-as-strings (lines &optional widget-as-strings)
253 (if (string= "" (car lines))
254 (cons widget-as-strings (separate-widgets-as-strings (cdr lines)))
255 (separate-widgets-as-strings (cdr lines) (append widget-as-strings (list (car lines))))))))
256 (separate-widgets-as-strings interface-as-strings))))
260 (lambda (widget-as-strings)
262 ((parser (find-if (lambda (validator) (funcall validator widget-as-strings)) *widget-parsers* :key #'car)))
263 (when parser (funcall (cadr parser) widget-as-strings))))
264 widgets-as-strings))))
266 ; With authoring, idx here needs to be looked at again.
267 (defun execute-button (name &optional (idx 0))
268 "EXECUTE-BUTTON NAME &optional IDX => RESULT
270 ARGUMENTS AND VALUES:
272 NAME: the name of the button
273 IDX: the instance of the button, defaults to 0
278 Executes the code in the button referenced by NAME and IDX.
280 NAME refers to the display name for the button, which is usually
281 set by the model, but sometimes defaults to the code inside.
283 Because NAME is not guaranteed to be unique, IDX is available
284 as a specifier. The index is in the order that the buttons are
285 loaded, and cannot be guaranteed to be stable from run to run."
286 (when *current-callback*
292 (lambda (widget) (and (button-p widget) (string= (button-display-name widget) name)))
293 *current-interface*))))
296 (funcall *current-callback* (button-code button))
297 (error "Couldn't find button with name ~A (idx: ~A)" name idx)))))
299 ;; INFORMATION ABOUT MODEL
301 (defun world-dimensions (model)
302 "WORLD-DIMENSIONS MODEL => DIMS
304 DIMS: (:xmin XMIN :xmax XMAX :ymin YMIN :ymax YMAX)
306 ARGUMENTS AND VALUES:
308 MODEL: A valid model containing a view
309 XMIN: An integer representing the minimum patch coord in X
310 XMAX: An integer representing the maximum patch coord in X
311 YMIN: An integer representing the minimum patch coord in Y
312 YMAX: An integer representing the maximum patch coord in Y
316 Returns the dimensions of MODEL. MODEL must be a valid model
317 as parsed by CLNL, and have a valid view in it."
319 ((view (find-if #'view-p (model-interface model))))
321 :xmin (view-min-pxcor view)
322 :xmax (view-max-pxcor view)
323 :ymin (view-min-pycor view)
324 :ymax (view-max-pycor view)
325 :patch-size (view-patch-size view))))
327 (defun widget-globals (model)
328 "WIDGET-GLOBALS MODEL => GLOBALS
331 GLOBAL: (NAME DEFAULT)
333 ARGUMENTS AND VALUES:
336 NAME: A symbol interned in the keyworkd package
337 DEFAULT: The widget default value
341 Returns the globals that get declared in the model from widgets.
342 They are interned in the keyword package package set for clnl, so
343 that they can later be used for multiple purposes."
348 (slider (list (intern (string-upcase (slider-varname widget)) :keyword) (slider-default widget)))
349 (switch (list (intern (string-upcase (switch-varname widget)) :keyword) (switch-on widget)))))
350 (model-interface model))))
352 (defun buttons (model)
353 "BUTTONS MODEL => BUTTON-DEFS
355 BUTTON-DEFS: BUTTON-DEF*
356 BUTTON-DEF: (:left LEFT :top TOP :height HEIGHT :width WIDTH :display DISPLAY)
358 ARGUMENTS AND VALUES:
361 LEFT: An integer representing the left position
362 TOP: An integer representing the top position
363 HEIGHT: An integer representing height
364 WIDTH: An integer representing width
365 DISPLAY: A string representing display name
369 Returns button definitions that get declared in the buttons of the
370 MODEL. This is used to initialize the interface."
377 :left (button-left widget)
378 :top (button-top widget)
379 :width (- (button-right widget) (button-left widget))
380 :height (- (button-bottom widget) (button-top widget))
381 :display (button-display-name widget)))))
382 (model-interface model))))
385 "BUTTONS MODEL => VIEW-DEF
387 VIEW-DEF: (:left LEFT :top TOP)
389 ARGUMENTS AND VALUES:
392 LEFT: An integer representing the left position
393 TOP: An integer representing the top position
397 Returns the view definition that get declared in the view of the
398 MODEL. This is used to initialize the interface."
400 ((view (find-if #'view-p (model-interface model))))
401 (list :left (view-left view) :top (view-top view))))
406 ARGUMENTS AND VALUES:
409 CODE: The string representing the netlogo code in this model
413 Returns the code from the model."
416 ; This should get cached eventually, though maybe just cached via a display list is good enough
417 (defun button-display-name (button)
419 (button-display button)
420 (cl-ppcre:regex-replace-all "\\s+" (button-code button) " ")))
422 (defun unescape-code (code)
423 (with-output-to-string (out)
424 (with-input-from-string (in code)
426 :for c := (read-char in nil)
428 :for aux := (when (eql #\\ c)
435 (t (error "Invalid escape sequence"))))
436 :do (write-char (or aux c) out)))))