1 ; Copyright 2022 Frank Duncan (frank@consxy.com) under AGPL3. See distributed LICENSE.txt.
2 (in-package #:clnl-model)
4 (defvar *separator* "@#$#@#$#@")
6 (defvar *current-interface* nil)
7 (defvar *current-callback* nil)
9 ; At this time, this is the only stateful part of the model. If more get added,
10 ; a more general concept can be introduced.
11 (defvar *enabled-forever-buttons* nil)
27 (defun set-callback (callback)
28 "SET-CALLBACK CALLBACK => RESULT
32 CALLBACK: a function that can take netlogo code
37 Sets the means by which the interface can call arbitrary netlogo code."
38 (setf *current-callback* callback))
40 (defun set-current-interface (interface)
41 "SET-CURRENT-INTERFACE INTERFACE => RESULT
45 INTERFACE: a list of widgets for display
50 Sets the currently running model to INTERFACE.
52 The widgets set here are comprised of the bare necessary
53 to run the engine with or without an actual visual component."
54 (setf *current-interface* interface))
56 (defun interface (model)
57 "INTERFACE MODEL => INTERFACE
61 MODEL: an object representing the model
62 INTERFACE: a list of widgets for display
66 INTERFACE returns the widgets in MODEL, used for display, or
67 setting with SET-CURRENT-INTERFACE."
68 (model-interface model))
70 (defun default-model ()
71 "DEFAULT-MODEL => MODEL
75 MODEL: an object representing the model
79 Returns the default startup model."
83 (make-view :left 10 :top 10 :min-pxcor -5 :max-pxcor 5 :min-pycor -5 :max-pycor 5 :patch-size 13d0))))
85 (defun read-from-nlogo (str)
86 "READ-FROM-NLOGO STR => MODEL
90 STR: a readable stream
91 MODEL: an object representing the model
95 Takes a stream STR, reads in a nlogo file, parses it, and then
96 returns the model object."
100 ((read-sections (&optional section)
102 ((line (read-line str nil)))
104 (if (string= *separator* line)
105 (cons section (read-sections))
106 (read-sections (append section (list line))))))))
109 :code (format nil "~{~A~^~%~}" (nth 0 sections))
110 :interface (parse-interface (nth 1 sections))
111 :info (nth 2 sections)
112 :turtle-shapes (nth 3 sections)
113 :version (nth 4 sections)
114 :preview-commands (nth 5 sections)
115 :system-dynamics (nth 6 sections)
116 :behavior-space (nth 7 sections)
117 :hub-net-client (nth 8 sections)
118 :link-shapes (nth 9 sections)
119 :model-settings (nth 10 sections)
120 :delta-tick (nth 11 sections))))
124 (defparameter *widget-parsers* nil)
126 (defmacro defwidget-definition (type &rest definitions)
135 (find (car def) (list :int :double :tnil-boolean :inverted-boolean :boolean :choice :string :option :code))
140 (lambda (,lines) ; Validator
146 ((line `(nth ,n ,lines)))
148 (:specified `(string= ,(second def) ,line))
149 (:int `(parse-integer ,line :junk-allowed t))
150 (:double `(ignore-errors (coerce (read-from-string ,line) 'double-float)))
151 (:boolean `(or (string= "1" ,line) (string= "0" ,line)))
152 (:inverted-boolean `(or (string= "0" ,line) (string= "1" ,line)))
153 (:tnil-boolean `(or (string= "T" ,line) (string= "NIL" ,line)))
154 (:choice `(find ,line ',(mapcar #'car (third def)) :test #'string=)))))
155 definitions (loop for i to (length definitions) collect i)))))
156 (lambda (,lines) ; Parser
157 (,(read-from-string (format nil "make-~A" type))
162 ((line `(nth ,n ,lines))
165 (:int `(parse-integer ,line))
166 (:double `(coerce (read-from-string ,line) 'double-float))
167 (:boolean `(string= "1" ,line))
168 (:inverted-boolean `(string= "0" ,line))
169 (:tnil-boolean `(string/= "NIL" ,line))
170 (:choice `(cadr (find ,line ',(third def) :key #'car :test #'string=)))
171 (:option `(when (string/= ,line ,(third def)) ,line))
172 (:code `(unescape-code ,line))
174 (when val-getter (list (intern (symbol-name (cadr def)) :keyword) val-getter))))
175 definitions (loop for i to (length definitions) collect i))))))
178 (defwidget-definition view
179 (:specified "GRAPHICS-WINDOW")
193 (:boolean wrapping-allowed-in-x)
194 (:boolean wrapping-allowed-in-y)
200 (:choice update-mode (("0" :continuous) ("1" :tick-based)))
202 (:boolean show-tick-counter)
203 (:string tick-counter-label)
204 (:double frame-rate 30))
206 (defwidget-definition slider
207 (:specified "SLIDER")
219 (:option units "NIL")
220 (:choice direction (("HORIZONTAL" :horizontal) ("VERTICAL" :vertical))))
222 (defwidget-definition switch
223 (:specified "SWITCH")
230 (:inverted-boolean on)
234 (defwidget-definition button
235 (:specified "BUTTON")
240 (:option display "NIL")
242 (:tnil-boolean forever)
245 (:string button-type)
250 (:boolean go-time)) ; should it wait for ticks to be initialized
252 (defwidget-definition textbox
253 (:specified "TEXTBOX")
258 (:code display) ; We use code here because the original netlogo treats this display like it does code
261 (:boolean transparent))
263 (defun parse-interface (interface-as-strings)
267 ((separate-widgets-as-strings (lines &optional widget-as-strings)
269 (if (string= "" (car lines))
270 (cons widget-as-strings (separate-widgets-as-strings (cdr lines)))
271 (separate-widgets-as-strings (cdr lines) (append widget-as-strings (list (car lines))))))))
272 (separate-widgets-as-strings interface-as-strings))))
276 (lambda (widget-as-strings)
278 ((parser (find-if (lambda (validator) (funcall validator widget-as-strings)) *widget-parsers* :key #'car)))
279 (when parser (funcall (cadr parser) widget-as-strings))))
280 widgets-as-strings))))
282 (defun find-button (name idx)
286 (lambda (widget) (and (button-p widget) (string= (button-display-name widget) name)))
287 *current-interface*)))
289 ; With authoring, idx here needs to be looked at again.
290 (defun execute-button (name &optional (idx 0))
291 "EXECUTE-BUTTON NAME &optional IDX => RESULT
293 ARGUMENTS AND VALUES:
295 NAME: the name of the button
296 IDX: the instance of the button, defaults to 0
301 Executes the code in the button referenced by NAME and IDX.
303 NAME refers to the display name for the button, which is usually
304 set by the model, but sometimes defaults to the code inside.
306 Because NAME is not guaranteed to be unique, IDX is available
307 as a specifier. The index is in the order that the buttons are
308 loaded, and cannot be guaranteed to be stable from run to run."
309 (when *current-callback*
311 ((button (find-button name (round idx))))
313 ((not button) (error "Couldn't find button with name ~A (idx: ~A)" name idx))
314 ((and (button-forever button) (find button *enabled-forever-buttons* :test #'equal))
315 (setf *enabled-forever-buttons* (remove button *enabled-forever-buttons* :test #'equal)))
316 ((button-forever button)
317 (setf *enabled-forever-buttons* (cons button *enabled-forever-buttons*))
318 (sb-thread:make-thread
321 :while (find button *enabled-forever-buttons* :test #'equal)
322 ; The sleep is necessary so that it gives other threads time
325 ((result (funcall *current-callback* (button-code button))))
326 (when (eql :stop result)
327 (setf *enabled-forever-buttons* (remove button *enabled-forever-buttons* :test #'equal)))
329 :name (format nil "Forever button: ~A" (button-display button))))
330 (t (funcall *current-callback* (button-code button)))))))
332 (defun forever-button-on (name &optional (idx 0))
333 "FOREVER-BUTTON-ON NAME &optional IDX => ON
335 ARGUMENTS AND VALUES:
337 NAME: the name of the button
338 IDX: the instance of the button, defaults to 0
343 Returns whether the button identified by NAME and IDX is currently on.
345 NAME refers to the display name for the button, which is usually
346 set by the model, but sometimes defaults to the code inside.
348 Because NAME is not guaranteed to be unique, IDX is available
349 as a specifier. The index is in the order that the buttons are
350 loaded, and cannot be guaranteed to be stable from run to run."
351 (and (find (find-button name (round idx)) *enabled-forever-buttons* :test #'equal) t))
353 ;; INFORMATION ABOUT MODEL
355 (defun world-dimensions (model)
356 "WORLD-DIMENSIONS MODEL => DIMS
358 DIMS: (:xmin XMIN :xmax XMAX :ymin YMIN :ymax YMAX)
360 ARGUMENTS AND VALUES:
362 MODEL: A valid model containing a view
363 XMIN: An integer representing the minimum patch coord in X
364 XMAX: An integer representing the maximum patch coord in X
365 YMIN: An integer representing the minimum patch coord in Y
366 YMAX: An integer representing the maximum patch coord in Y
370 Returns the dimensions of MODEL. MODEL must be a valid model
371 as parsed by CLNL, and have a valid view in it."
373 ((view (find-if #'view-p (model-interface model))))
375 :xmin (view-min-pxcor view)
376 :xmax (view-max-pxcor view)
377 :ymin (view-min-pycor view)
378 :ymax (view-max-pycor view)
379 :patch-size (view-patch-size view))))
381 (defun widget-globals (model)
382 "WIDGET-GLOBALS MODEL => GLOBALS
385 GLOBAL: (NAME DEFAULT)
387 ARGUMENTS AND VALUES:
390 NAME: A symbol interned in the keyworkd package
391 DEFAULT: The widget default value
395 Returns the globals that get declared in the model from widgets.
396 They are interned in the keyword package package set for clnl, so
397 that they can later be used for multiple purposes."
402 (slider (list (intern (string-upcase (slider-varname widget)) :keyword) (slider-default widget)))
403 (switch (list (intern (string-upcase (switch-varname widget)) :keyword) (switch-on widget)))))
404 (model-interface model))))
406 (defun buttons (model)
407 "BUTTONS MODEL => BUTTON-DEFS
409 BUTTON-DEFS: BUTTON-DEF*
410 BUTTON-DEF: (:left LEFT :top TOP :height HEIGHT :width WIDTH :forever FOREVER :display DISPLAY)
412 ARGUMENTS AND VALUES:
415 LEFT: An integer representing the left position
416 TOP: An integer representing the top position
417 HEIGHT: An integer representing height
418 WIDTH: An integer representing width
419 FOREVER: A boolean representing whether this button is a forever button
420 DISPLAY: A string representing display name
424 Returns button definitions that get declared in the buttons of the
425 MODEL. This is used to initialize the interface."
432 :left (button-left widget)
433 :top (button-top widget)
434 :width (- (button-right widget) (button-left widget))
435 :height (- (button-bottom widget) (button-top widget))
436 :forever (button-forever widget)
437 :display (button-display-name widget)))))
438 (model-interface model))))
440 (defun textboxes (model)
441 "TEXTBOXES MODEL => TEXTBOX-DEFS
443 TEXTBOX-DEFS: TEXTBOX-DEF*
444 TEXTBOX-DEF: (:left LEFT :top TOP :height HEIGHT :width WIDTH :display DISPLAY)
446 ARGUMENTS AND VALUES:
449 LEFT: An integer representing the left position
450 TOP: An integer representing the top position
451 HEIGHT: An integer representing height, in characters
452 WIDTH: An integer representing width, in characters
453 DISPLAY: A string representing display name
457 Returns textbox definitions that get declared in the textboxes of the
458 MODEL. This is used to initialize the interface."
465 :left (textbox-left widget)
466 :top (textbox-top widget)
467 :width (truncate (- (textbox-right widget) (textbox-left widget)) clnl-gltk:*font-width*)
468 :height (truncate (- (textbox-bottom widget) (textbox-top widget)) clnl-gltk:*font-height*)
469 :display (textbox-display widget)))))
470 (model-interface model))))
472 (defun switches (model)
473 "SWITCHES MODEL => SWITCH-DEFS
475 SWITCH-DEFS: SWITCH-DEF*
476 SWITCH-DEF: (:left LEFT :top TOP :width WIDTH :var VAR :display DISPLAY :initial-value INITIAL-VALUE)
478 ARGUMENTS AND VALUES:
481 LEFT: An integer representing the left position
482 TOP: An integer representing the top position
483 WIDTH: An integer representing width
484 VAR: A symbole representing variable
485 DISPLAY: A string representing variable name
486 INITIAL-VALUE: The initial value
490 Returns switch definitions that get declared in the switches of the
491 MODEL. This is used to initialize the interface."
498 :left (switch-left widget)
499 :top (switch-top widget)
500 :width (- (switch-right widget) (switch-left widget))
501 :var (intern (string-upcase (switch-varname widget)) :keyword)
502 :display (switch-varname widget)
503 :initial-value (switch-on widget) ))))
504 (model-interface model))))
506 (defun sliders (model)
507 "SLIDERS MODEL => SLIDER-DEFS
509 SLIDER-DEFS: SLIDER-DEF*
510 SLIDER-DEF: (:left LEFT :top TOP :width WIDTH :var VAR :display DISPLAY :initial-value INITIAL-VALUE)
512 ARGUMENTS AND VALUES:
515 LEFT: An integer representing the left position
516 TOP: An integer representing the top position
517 WIDTH: An integer representing width
518 VAR: A symbole representing variable
519 DISPLAY: A string representing variable name
520 INITIAL-VALUE: The initial value
524 Returns slider definitions that get declared in the sliders of the
525 MODEL. This is used to initialize the interface."
532 :left (slider-left widget)
533 :top (slider-top widget)
534 :width (- (slider-right widget) (slider-left widget))
535 :var (intern (string-upcase (slider-varname widget)) :keyword)
536 :display (slider-varname widget)
537 :min (slider-min widget)
538 :max (slider-max widget)
539 :step (slider-step widget)
540 :initial-value (slider-default widget)))))
541 (model-interface model))))
544 "VIEW MODEL => VIEW-DEF
546 VIEW-DEF: (:left LEFT :top TOP)
548 ARGUMENTS AND VALUES:
551 LEFT: An integer representing the left position
552 TOP: An integer representing the top position
556 Returns the view definition that get declared in the view of the
557 MODEL. This is used to initialize the interface."
559 ((view (find-if #'view-p (model-interface model))))
560 (list :left (view-left view) :top (view-top view))))
565 ARGUMENTS AND VALUES:
568 CODE: The string representing the netlogo code in this model
572 Returns the code from the model."
575 ; This should get cached eventually, though maybe just cached via a display list is good enough
576 (defun button-display-name (button)
578 (button-display button)
579 (cl-ppcre:regex-replace-all "\\s+" (button-code button) " ")))
581 (defun unescape-code (code)
582 (with-output-to-string (out)
583 (with-input-from-string (in code)
585 :for c := (read-char in nil)
587 :for aux := (when (eql #\\ c)
594 (t (error "Invalid escape sequence"))))
595 :do (write-char (or aux c) out)))))