;; ;; starlet/state.scm ;; ;; Copyright © 2020-2022 Thomas White ;; ;; This file is part of Starlet. ;; ;; Starlet is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;; (define-module (starlet state) #:use-module (starlet fixture) #:use-module (starlet utils) #:use-module (starlet attributes) #:use-module (starlet selection) #:use-module (oop goops) #:use-module (ice-9 pretty-print) #:use-module (ice-9 atomic) #:use-module (ice-9 receive) #:use-module (ice-9 exceptions) #:use-module (srfi srfi-1) #:use-module (srfi srfi-26) #:export ( make-empty-state lighting-state? get-state-name state-for-each state-map->list state-map copy-state clear-state! print-state state-source set-in-state! state-find current-state at apply-state combine-states show-state lighting-state programmer-state ps home-fixture! blackout blackout! value->number atomically-overlay-state! update-state! add-update-hook! state-empty? remove-fixtures-from-state! remove-fixture-from-state! remove-selection-from-programmer!)) ;; A "state" is an atomically-updating container for an immutable ;; hash table mapping (fixture-object . attribute-name-object) pairs to values ;; which can be numbers, symbols, colours, boolean values and more ;; depending on the type of attribute. Values can also be ;; functions which provide the value. (define-class () (hash-table-box #:init-form (make-atomic-box (make-hash-table)) #:getter get-ht-box) (update-hook #:init-form (make-hook 1) #:getter get-update-hook)) (define (lighting-state? a) (is-a? a )) ;; The state used to build a new scene for recording (define programmer-state (make )) (define ps programmer-state) (define (add-update-hook! state proc) (add-hook! (get-update-hook state) proc)) (define (find-colour state fix) (let ((col (state-find fix colour state))) (if (eq? 'no-value col) (let ((home-col (get-attr-home-val fix colour))) (if (eq? 'fixture-does-not-have-attribute home-col) (raise-exception (make-exception (make-exception-with-message "Fixture doesn't have colour attribute") (make-exception-with-irritants fix))) home-col)) col))) (define-method (update-state! (state )) ;; Basic state object needs no updates #f) (define-method (set-in-state! (state ) (fix ) (attr ) value source) (let* ((old-ht (atomic-box-ref (get-ht-box state))) (new-ht (copy-hash-table old-ht))) (hash-set! new-ht (cons fix attr) value) (unless (eq? (atomic-box-compare-and-swap! (get-ht-box state) old-ht new-ht) old-ht) (set-in-state! state fix attr)) ;; Try again (run-hook (get-update-hook state) source))) (define-method (set-in-state! (state ) (fix ) (attr ) value) (set-in-state! state fix attr value #f)) ;; Set any intensity attributes in the current state to zero (define (blackout!) (let ((state (current-state))) (state-for-each (lambda (fix attr val) (when (intensity? attr) (set-in-state! state fix attr 0.0))) state))) ;; Set all attributes of the fixture to their home values (define (home-fixture! fix) (for-each (lambda (attr) (at fix attr (get-attr-home-val fix attr))) (get-fixture-attrs fix))) (define (copy-state state) (let ((new-state (make-empty-state))) (state-for-each (lambda (fix attr val) (set-in-state! new-state fix attr val)) state) new-state)) (define (make-empty-state) (make )) (define blackout (make-empty-state)) (define (state-for-each func state) (hash-for-each (lambda (key value) (func (car key) (cdr key) value)) (atomic-box-ref (get-ht-box state)))) (define (state-find fix attr state) (hash-ref (atomic-box-ref (get-ht-box state)) (cons fix attr) 'no-value)) (define (state-map->list func state) (hash-map->list (lambda (key value) (func (car key) (cdr key) value)) (atomic-box-ref (get-ht-box state)))) (define (state-map func state) (let ((out-state (make-empty-state))) (hash-for-each (lambda (key value) (set-in-state! out-state (car key) (cdr key) (func (car key) (cdr key) value))) (atomic-box-ref (get-ht-box state))) out-state)) (define (apply-state state) "Apply the contents of 'state' to the current state, on top of the \ pre-existing contents." (state-for-each at state)) (define (show-state state) "Clear the current state, and apply the contents of 'state'" (clear-state! (current-state)) (state-for-each at state)) (define (atomically-overlay-state! state newbits) "Apply 'newbits' within 'state', as a single atomic operation." (let* ((old-ht (atomic-box-ref (get-ht-box state))) (new-ht (copy-hash-table old-ht))) (state-for-each (lambda (fix attr val) (hash-set! new-ht (cons fix attr) val)) newbits) (unless (eq? (atomic-box-compare-and-swap! (get-ht-box state) old-ht new-ht) old-ht) (atomically-overlay-state! state newbits)))) ;; Try again (define current-state (make-parameter programmer-state)) (define-syntax lighting-state (syntax-rules () ((_ body ...) (parameterize ((current-state (make-empty-state))) body ... (current-state))))) (define (combine-states a b) (lighting-state (apply-state a) (apply-state b))) (define (print-state a) (pretty-print (state-source a))) (define-method (write (st ) port) (write (state-source st) port)) (define (clamp-to-attr-range fix attr val) (if (number? val) (let ((attr-obj (find-attr fix attr))) (if (continuous-attribute? attr-obj) (let ((range (get-attr-range attr-obj))) (max (car range) (min (cadr range) val))) val)) val)) (define (quote-if-symbol a) (if (symbol? a) (list 'quote a) a)) (define (state-source a) (cons 'lighting-state (state-map->list (lambda (fix attr val) (list 'at (get-fixture-name fix) (canonical-name attr) (quote-if-symbol (clamp-to-attr-range fix attr val)))) a))) ;; Coerce something from a state object into a number for scanout (define (value->number val) (if (procedure? val) (value->number (val)) val)) (define (clear-state! state) (let* ((old-ht (atomic-box-ref (get-ht-box state))) (new-ht (make-hash-table))) (unless (eq? (atomic-box-compare-and-swap! (get-ht-box state) old-ht new-ht) old-ht) (clear-state! state))) ;; Try again (run-hook (get-update-hook state) #f)) (define (partition3 pred1 pred2 input) (receive (output1 others) (partition pred1 input) (receive (output2 others) (partition pred2 others) (values output1 output2 others)))) (define (set-fixtures fixtures attribute value) (for-each (lambda (fix) (if (fixture-has-attr? fix attribute) (set-in-state! (current-state) fix attribute (clamp-to-attr-range fix attribute value)) (error "Fixture does not have attribute" (get-fixture-name fix) (canonical-name attribute)))) fixtures)) ;; (at [] [ ...]) ;; (at fix1 100) <-- Set intensity of single fixture ;; (at fix1 intensity 100) <-- Explicit attribute name ;; (at fix1 fix2 100) <-- Multiple fixtures ;; (at fix1 fix2 pan 36) <-- Multiple fixtures + explicit attribute ;; (at group1 fix1 intensity 100) <-- Groups can be used instead of fixtures ;; (at fix1 100 pan 36) <-- Set multiple attributes ;; NB Can't set multiple fixtures and attributes: (at fix1 pan 35 fix2 tilt 22) (define (at . args) (let ((selection (get-selection))) (receive (fixtures attribute value) (partition3 fixture? attribute? (flatten-sublists args)) (cond ((nil? value) (error "at: Value not specified")) ((or (more-than-one value) (more-than-one attribute)) (error "at: Only one attribute or value name")) ((and (nil? fixtures) (nil? attribute)) (if (nil? selection) 'no-fixtures-selected (set-fixtures selection intensity (car value)))) ((nil? attribute) (set-fixtures fixtures intensity (car value))) ((nil? fixtures) (if (nil? selection) 'no-fixtures-selected (set-fixtures selection (car attribute) (car value)))) (else (set-fixtures fixtures (car attribute) (car value))))))) (define (state-empty? st) (hash-table-empty? (atomic-box-ref (get-ht-box st)))) (define (remove-fixtures-from-state! st fixture-list) (let ((new-ht (make-hash-table)) (old-ht (atomic-box-ref (get-ht-box st)))) (state-for-each (lambda (fix attr val) (unless (memq fix fixture-list) (hash-set! new-ht (cons fix attr) val))) st) (if (eq? old-ht (atomic-box-compare-and-swap! (get-ht-box st) old-ht new-ht)) (run-hook (get-update-hook st) #f) (remove-fixtures-from-state! st fixture-list)))) (define (remove-fixture-from-state! st fix) (remove-fixtures-from-state! st (list fix))) (define (remove-selection-from-programmer!) (remove-fixtures-from-state! programmer-state (get-selection)))