@lovelace_lol/polymer 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +226 -0
- package/dist/index.d.ts +2603 -0
- package/dist/index.js +1031 -0
- package/dist/test.cjs +1519 -0
- package/dist/worker/agency-worker.js +972 -0
- package/dist/worker/manifest.edn +1 -0
- package/package.json +57 -0
- package/shadow-cljs.edn +173 -0
- package/src/polymer/animation/agency.cljs +84 -0
- package/src/polymer/animation/domain.cljs +110 -0
- package/src/polymer/animation/planner.cljs +88 -0
- package/src/polymer/animation/runtime.cljs +137 -0
- package/src/polymer/animation/scheduler.cljs +178 -0
- package/src/polymer/animation/service.cljs +981 -0
- package/src/polymer/animation/state.cljs +112 -0
- package/src/polymer/blink/agency.cljs +131 -0
- package/src/polymer/blink/planner.cljs +115 -0
- package/src/polymer/blink/scheduler.cljs +90 -0
- package/src/polymer/blink/snippet.cljs +50 -0
- package/src/polymer/blink/state.cljs +102 -0
- package/src/polymer/camera_context/agency.cljs +132 -0
- package/src/polymer/camera_context/domain.cljs +186 -0
- package/src/polymer/camera_context/planner.cljs +50 -0
- package/src/polymer/camera_context/scheduler.cljs +113 -0
- package/src/polymer/camera_context/state.cljs +121 -0
- package/src/polymer/character.cljs +471 -0
- package/src/polymer/conversation/agency.cljs +83 -0
- package/src/polymer/conversation/domain.cljs +35 -0
- package/src/polymer/conversation/effector.cljs +130 -0
- package/src/polymer/conversation/planner.cljs +178 -0
- package/src/polymer/conversation/scheduler.cljs +202 -0
- package/src/polymer/conversation/service.cljs +329 -0
- package/src/polymer/conversation/state.cljs +186 -0
- package/src/polymer/core.cljs +172 -0
- package/src/polymer/eye_head/agency.cljs +75 -0
- package/src/polymer/eye_head/domain.cljs +133 -0
- package/src/polymer/eye_head/goap.cljs +106 -0
- package/src/polymer/eye_head/scheduler.cljs +176 -0
- package/src/polymer/eye_head/service.cljs +259 -0
- package/src/polymer/eye_head/snippet.cljs +92 -0
- package/src/polymer/eye_head/state.cljs +167 -0
- package/src/polymer/gaze/agency.cljs +85 -0
- package/src/polymer/gaze/domain.cljs +211 -0
- package/src/polymer/gaze/goap.cljs +208 -0
- package/src/polymer/gaze/scheduler.cljs +184 -0
- package/src/polymer/gaze/state.cljs +202 -0
- package/src/polymer/gesture/agency.cljs +185 -0
- package/src/polymer/gesture/goap.cljs +461 -0
- package/src/polymer/gesture/scheduler.cljs +122 -0
- package/src/polymer/gesture/snippet.cljs +198 -0
- package/src/polymer/gesture/state.cljs +273 -0
- package/src/polymer/lipsync/agency.cljs +593 -0
- package/src/polymer/lipsync/articulation/jaw.cljs +258 -0
- package/src/polymer/lipsync/articulation/snippet.cljs +605 -0
- package/src/polymer/lipsync/articulation/tongue.cljs +418 -0
- package/src/polymer/lipsync/articulation/visemes.cljs +429 -0
- package/src/polymer/lipsync/goap.cljs +132 -0
- package/src/polymer/lipsync/pronunciation/effector.cljs +51 -0
- package/src/polymer/lipsync/scheduler.cljs +302 -0
- package/src/polymer/lipsync/state.cljs +202 -0
- package/src/polymer/nlp/phonemes.cljs +164 -0
- package/src/polymer/prosodic/agency.cljs +196 -0
- package/src/polymer/prosodic/goap.cljs +122 -0
- package/src/polymer/prosodic/scheduler.cljs +99 -0
- package/src/polymer/prosodic/snippet.cljs +106 -0
- package/src/polymer/prosodic/state.cljs +139 -0
- package/src/polymer/runtime/embody.cljs +117 -0
- package/src/polymer/runtime/embody_wasm.cljs +177 -0
- package/src/polymer/runtime/rust_embody_host.cljs +645 -0
- package/src/polymer/society.cljs +239 -0
- package/src/polymer/stream.cljs +130 -0
- package/src/polymer/transcription/agency.cljs +69 -0
- package/src/polymer/transcription/domain.cljs +68 -0
- package/src/polymer/transcription/effector.cljs +611 -0
- package/src/polymer/transcription/planner.cljs +203 -0
- package/src/polymer/transcription/scheduler.cljs +225 -0
- package/src/polymer/transcription/service.cljs +209 -0
- package/src/polymer/transcription/state.cljs +239 -0
- package/src/polymer/tts/agency.cljs +503 -0
- package/src/polymer/tts/azure.cljs +290 -0
- package/src/polymer/tts/planner.cljs +178 -0
- package/src/polymer/tts/runtime.cljs +624 -0
- package/src/polymer/tts/scheduler.cljs +162 -0
- package/src/polymer/tts/state.cljs +191 -0
- package/src/polymer/worker.cljs +70 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
(ns polymer.camera-context.agency
|
|
2
|
+
(:require [polymer.camera-context.domain :as domain]
|
|
3
|
+
[polymer.camera-context.planner :as planner]
|
|
4
|
+
[polymer.camera-context.scheduler :as scheduler]
|
|
5
|
+
[polymer.camera-context.state :as state]
|
|
6
|
+
[polymer.stream :as stream]))
|
|
7
|
+
|
|
8
|
+
;; Camera Context publishes stable camera-relative facts. It does not own gaze
|
|
9
|
+
;; behavior and does not import a rendering engine. Runtime-specific code can
|
|
10
|
+
;; observe the camera and send plain pose/target facts into this agency.
|
|
11
|
+
|
|
12
|
+
(defn now-ms
|
|
13
|
+
[]
|
|
14
|
+
(.now js/Date))
|
|
15
|
+
|
|
16
|
+
(defn create-camera-context-agency
|
|
17
|
+
[config]
|
|
18
|
+
(let [input-stream (stream/create-stream)
|
|
19
|
+
event-stream (stream/create-stream)
|
|
20
|
+
state-atom (atom (state/config->state config))
|
|
21
|
+
sequence (atom 0)
|
|
22
|
+
disposed? (atom false)
|
|
23
|
+
emit-input (:emit input-stream)
|
|
24
|
+
emit-event (:emit event-stream)
|
|
25
|
+
scheduler-atom (atom nil)]
|
|
26
|
+
(letfn [(publish-fact! [fact]
|
|
27
|
+
(let [published-at (now-ms)
|
|
28
|
+
payload (assoc fact
|
|
29
|
+
:type "camera.fact"
|
|
30
|
+
:publishedAt published-at)]
|
|
31
|
+
(swap! state-atom state/record-published fact published-at)
|
|
32
|
+
(emit-event payload)))
|
|
33
|
+
|
|
34
|
+
(invalidate-stale! [reason]
|
|
35
|
+
(when (state/can-invalidate? @state-atom)
|
|
36
|
+
(let [invalidated-at (now-ms)]
|
|
37
|
+
(swap! state-atom state/record-stale invalidated-at reason)
|
|
38
|
+
(emit-event (assoc (:lastInvalidation @state-atom)
|
|
39
|
+
:type "camera.stale")))))
|
|
40
|
+
|
|
41
|
+
(emit-status! [status reason]
|
|
42
|
+
(emit-event (cond-> {:type "camera.status"
|
|
43
|
+
:agency "cameraContext"
|
|
44
|
+
:status status
|
|
45
|
+
:stale (:stale @state-atom)
|
|
46
|
+
:at (now-ms)}
|
|
47
|
+
reason (assoc :reason reason))))
|
|
48
|
+
|
|
49
|
+
(schedule-camera-update! [payload]
|
|
50
|
+
(let [id (swap! sequence inc)
|
|
51
|
+
observed-at (now-ms)
|
|
52
|
+
fact (domain/normalize-camera-fact payload
|
|
53
|
+
(:config @state-atom)
|
|
54
|
+
observed-at
|
|
55
|
+
id)]
|
|
56
|
+
(swap! state-atom state/record-camera-update fact)
|
|
57
|
+
(when-let [agency-scheduler @scheduler-atom]
|
|
58
|
+
((:schedule-camera-update agency-scheduler) fact))))
|
|
59
|
+
|
|
60
|
+
(dispatch! [command]
|
|
61
|
+
(when-not @disposed?
|
|
62
|
+
(let [payload (domain/data-map command)
|
|
63
|
+
plan (planner/plan-command payload @state-atom (now-ms))
|
|
64
|
+
failure (first (filter #(= "fail" (:op %)) (:steps plan)))]
|
|
65
|
+
(emit-input {:type "command"
|
|
66
|
+
:agency "cameraContext"
|
|
67
|
+
:command payload})
|
|
68
|
+
(swap! state-atom state/record-plan plan)
|
|
69
|
+
(if failure
|
|
70
|
+
(emit-event {:type "error"
|
|
71
|
+
:agency "cameraContext"
|
|
72
|
+
:message (:reason failure)
|
|
73
|
+
:commandType (:commandType failure)})
|
|
74
|
+
(case (:type payload)
|
|
75
|
+
"configure"
|
|
76
|
+
(do
|
|
77
|
+
(swap! state-atom state/configure (:config payload))
|
|
78
|
+
(emit-status! (:status @state-atom) "configured"))
|
|
79
|
+
|
|
80
|
+
"updateCamera"
|
|
81
|
+
(schedule-camera-update! payload)
|
|
82
|
+
|
|
83
|
+
"publishCameraFacts"
|
|
84
|
+
(when-let [agency-scheduler @scheduler-atom]
|
|
85
|
+
((:publish-now agency-scheduler)))
|
|
86
|
+
|
|
87
|
+
"invalidateStale"
|
|
88
|
+
(when-let [agency-scheduler @scheduler-atom]
|
|
89
|
+
((:invalidate-stale agency-scheduler)
|
|
90
|
+
(or (:reason payload) "manual")))
|
|
91
|
+
|
|
92
|
+
"reset"
|
|
93
|
+
(do
|
|
94
|
+
(when-let [agency-scheduler @scheduler-atom]
|
|
95
|
+
((:reset agency-scheduler)))
|
|
96
|
+
(swap! state-atom state/reset-state)
|
|
97
|
+
(emit-status! "idle" "reset"))
|
|
98
|
+
|
|
99
|
+
nil)))))
|
|
100
|
+
|
|
101
|
+
(dispatch-type! [type]
|
|
102
|
+
(dispatch! #js {:type type}))]
|
|
103
|
+
(let [agency-scheduler (scheduler/create-scheduler
|
|
104
|
+
{:state-atom state-atom
|
|
105
|
+
:publish-fact! publish-fact!
|
|
106
|
+
:invalidate-stale! invalidate-stale!})]
|
|
107
|
+
(reset! scheduler-atom agency-scheduler)
|
|
108
|
+
#js {:dispatch dispatch!
|
|
109
|
+
:snapshot (fn [] (state/visible-state @state-atom))
|
|
110
|
+
:input (stream/writable-port input-stream dispatch!)
|
|
111
|
+
:events (stream/readable-port event-stream)
|
|
112
|
+
:subscribeInput (fn [listener] ((:subscribe input-stream) listener))
|
|
113
|
+
:subscribeEvents (fn [listener] ((:subscribe event-stream) listener))
|
|
114
|
+
:subscribe (fn [listener] ((:subscribe event-stream) listener))
|
|
115
|
+
:subscribeStatus (fn [listener] ((:subscribe event-stream) listener))
|
|
116
|
+
:updateCamera (fn [facts]
|
|
117
|
+
(dispatch! #js {:type "updateCamera"
|
|
118
|
+
:facts facts}))
|
|
119
|
+
:invalidateStale (fn
|
|
120
|
+
([] (dispatch-type! "invalidateStale"))
|
|
121
|
+
([reason] (dispatch! #js {:type "invalidateStale"
|
|
122
|
+
:reason reason})))
|
|
123
|
+
:reset (fn [] (dispatch-type! "reset"))
|
|
124
|
+
:queue (fn []
|
|
125
|
+
(when-let [agency-scheduler @scheduler-atom]
|
|
126
|
+
(clj->js ((:queue agency-scheduler)))))
|
|
127
|
+
:dispose (fn []
|
|
128
|
+
(when-not @disposed?
|
|
129
|
+
(reset! disposed? true)
|
|
130
|
+
((:dispose agency-scheduler))
|
|
131
|
+
((:dispose input-stream))
|
|
132
|
+
((:dispose event-stream))))}))))
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
(ns polymer.camera-context.domain)
|
|
2
|
+
|
|
3
|
+
;; Camera Context intentionally works with plain data, not Three.js objects.
|
|
4
|
+
;; The host/runtime boundary can read camera, controls, and model handles, then
|
|
5
|
+
;; pass stable vector/quaternion facts into Polymer. Everything below is pure
|
|
6
|
+
;; math over those facts so Gaze, Eye/Head, and future agencies can consume the
|
|
7
|
+
;; same camera-relative context without inheriting a rendering-engine import.
|
|
8
|
+
|
|
9
|
+
(def default-epsilon 1.0e-4)
|
|
10
|
+
(def default-yaw-weight 0.35)
|
|
11
|
+
(def default-pitch-weight 0.2)
|
|
12
|
+
|
|
13
|
+
(def zero-vector3 {:x 0 :y 0 :z 0})
|
|
14
|
+
(def identity-quaternion {:x 0 :y 0 :z 0 :w 1})
|
|
15
|
+
(def zero-offset {:x 0 :y 0})
|
|
16
|
+
|
|
17
|
+
(defn finite-number?
|
|
18
|
+
[value]
|
|
19
|
+
(and (number? value) (js/isFinite value)))
|
|
20
|
+
|
|
21
|
+
(defn number-or
|
|
22
|
+
[value fallback]
|
|
23
|
+
(if (finite-number? value) value fallback))
|
|
24
|
+
|
|
25
|
+
(defn clamp
|
|
26
|
+
[lo hi value]
|
|
27
|
+
(min hi (max lo value)))
|
|
28
|
+
|
|
29
|
+
(defn data-map
|
|
30
|
+
[value]
|
|
31
|
+
(cond
|
|
32
|
+
(map? value) value
|
|
33
|
+
value (js->clj value :keywordize-keys true)
|
|
34
|
+
:else {}))
|
|
35
|
+
|
|
36
|
+
(defn normalize-vector3
|
|
37
|
+
([value] (normalize-vector3 value zero-vector3))
|
|
38
|
+
([value fallback]
|
|
39
|
+
(let [input (data-map value)
|
|
40
|
+
fallback (merge zero-vector3 fallback)]
|
|
41
|
+
{:x (number-or (:x input) (:x fallback))
|
|
42
|
+
:y (number-or (:y input) (:y fallback))
|
|
43
|
+
:z (number-or (:z input) (:z fallback))})))
|
|
44
|
+
|
|
45
|
+
(defn normalize-quaternion
|
|
46
|
+
[value]
|
|
47
|
+
(let [input (data-map value)
|
|
48
|
+
raw {:x (number-or (:x input) 0)
|
|
49
|
+
:y (number-or (:y input) 0)
|
|
50
|
+
:z (number-or (:z input) 0)
|
|
51
|
+
:w (number-or (:w input) 1)}
|
|
52
|
+
length (js/Math.hypot (:x raw) (:y raw) (:z raw) (:w raw))]
|
|
53
|
+
(if (< length default-epsilon)
|
|
54
|
+
identity-quaternion
|
|
55
|
+
{:x (/ (:x raw) length)
|
|
56
|
+
:y (/ (:y raw) length)
|
|
57
|
+
:z (/ (:z raw) length)
|
|
58
|
+
:w (/ (:w raw) length)})))
|
|
59
|
+
|
|
60
|
+
(defn subtract-vector3
|
|
61
|
+
[a b]
|
|
62
|
+
{:x (- (:x a) (:x b))
|
|
63
|
+
:y (- (:y a) (:y b))
|
|
64
|
+
:z (- (:z a) (:z b))})
|
|
65
|
+
|
|
66
|
+
(defn length-sq
|
|
67
|
+
[v]
|
|
68
|
+
(+ (* (:x v) (:x v))
|
|
69
|
+
(* (:y v) (:y v))
|
|
70
|
+
(* (:z v) (:z v))))
|
|
71
|
+
|
|
72
|
+
(defn normalize-direction
|
|
73
|
+
[v epsilon]
|
|
74
|
+
(let [length (js/Math.sqrt (length-sq v))]
|
|
75
|
+
(if (< length epsilon)
|
|
76
|
+
zero-vector3
|
|
77
|
+
{:x (/ (:x v) length)
|
|
78
|
+
:y (/ (:y v) length)
|
|
79
|
+
:z (/ (:z v) length)})))
|
|
80
|
+
|
|
81
|
+
(defn invert-unit-quaternion
|
|
82
|
+
[q]
|
|
83
|
+
{:x (- (:x q))
|
|
84
|
+
:y (- (:y q))
|
|
85
|
+
:z (- (:z q))
|
|
86
|
+
:w (:w q)})
|
|
87
|
+
|
|
88
|
+
(defn apply-quaternion
|
|
89
|
+
[v q]
|
|
90
|
+
;; This mirrors Three.js Vector3.applyQuaternion, but stays independent from
|
|
91
|
+
;; Three so the agency can run in workers and tests with only CLJS data.
|
|
92
|
+
(let [x (:x v)
|
|
93
|
+
y (:y v)
|
|
94
|
+
z (:z v)
|
|
95
|
+
qx (:x q)
|
|
96
|
+
qy (:y q)
|
|
97
|
+
qz (:z q)
|
|
98
|
+
qw (:w q)
|
|
99
|
+
ix (+ (* qw x) (* qy z) (- (* qz y)))
|
|
100
|
+
iy (+ (* qw y) (* qz x) (- (* qx z)))
|
|
101
|
+
iz (+ (* qw z) (* qx y) (- (* qy x)))
|
|
102
|
+
iw (- 0 (* qx x) (* qy y) (* qz z))]
|
|
103
|
+
{:x (+ (* ix qw) (* iw (- qx)) (* iy (- qz)) (- (* iz (- qy))))
|
|
104
|
+
:y (+ (* iy qw) (* iw (- qy)) (* iz (- qx)) (- (* ix (- qz))))
|
|
105
|
+
:z (+ (* iz qw) (* iw (- qz)) (* ix (- qy)) (- (* iy (- qx))))}))
|
|
106
|
+
|
|
107
|
+
(defn compute-camera-relative-offset
|
|
108
|
+
"Return a normalized gaze offset from camera position relative to a target.
|
|
109
|
+
|
|
110
|
+
Positive x means the camera is toward the model's local right side. Positive y
|
|
111
|
+
means the camera is above the target. The weights match the Latticework helper
|
|
112
|
+
defaults, while the inputs are portable data facts instead of Three objects."
|
|
113
|
+
([camera-position target-position model-quaternion]
|
|
114
|
+
(compute-camera-relative-offset camera-position target-position model-quaternion {}))
|
|
115
|
+
([camera-position target-position model-quaternion options]
|
|
116
|
+
(let [options (data-map options)
|
|
117
|
+
epsilon (number-or (:epsilon options) default-epsilon)
|
|
118
|
+
yaw-weight (number-or (:yawWeight options)
|
|
119
|
+
(number-or (:yaw-weight options) default-yaw-weight))
|
|
120
|
+
pitch-weight (number-or (:pitchWeight options)
|
|
121
|
+
(number-or (:pitch-weight options) default-pitch-weight))
|
|
122
|
+
camera-position (normalize-vector3 camera-position)
|
|
123
|
+
target-position (normalize-vector3 target-position)
|
|
124
|
+
world-offset (subtract-vector3 camera-position target-position)]
|
|
125
|
+
(if (< (length-sq world-offset) epsilon)
|
|
126
|
+
zero-offset
|
|
127
|
+
(let [world-direction (normalize-direction world-offset epsilon)
|
|
128
|
+
local-direction (normalize-direction
|
|
129
|
+
(apply-quaternion world-direction
|
|
130
|
+
(invert-unit-quaternion
|
|
131
|
+
(normalize-quaternion model-quaternion)))
|
|
132
|
+
epsilon)
|
|
133
|
+
yaw-angle (js/Math.atan2 (:x local-direction) (:z local-direction))
|
|
134
|
+
pitch-angle (js/Math.atan2
|
|
135
|
+
(:y local-direction)
|
|
136
|
+
(max (js/Math.hypot (:x local-direction) (:z local-direction))
|
|
137
|
+
epsilon))
|
|
138
|
+
half-pi (/ (.-PI js/Math) 2)
|
|
139
|
+
third-pi (/ (.-PI js/Math) 3)]
|
|
140
|
+
{:x (* (clamp -1 1 (/ yaw-angle half-pi)) yaw-weight)
|
|
141
|
+
:y (* (clamp -1 1 (/ pitch-angle third-pi)) pitch-weight)})))))
|
|
142
|
+
|
|
143
|
+
(defn camera-payload
|
|
144
|
+
[command]
|
|
145
|
+
(let [payload (data-map command)
|
|
146
|
+
facts (data-map (:facts payload))]
|
|
147
|
+
(merge facts payload)))
|
|
148
|
+
|
|
149
|
+
(defn normalize-camera-fact
|
|
150
|
+
"Create the stable fact payload published by the agency.
|
|
151
|
+
|
|
152
|
+
The command can either put facts at the top level or under :facts. Keeping the
|
|
153
|
+
shape forgiving lets tests, workers, and runtime adapters evolve without
|
|
154
|
+
changing the internal fact contract."
|
|
155
|
+
[command config observed-at sequence]
|
|
156
|
+
(let [payload (camera-payload command)
|
|
157
|
+
camera-position (normalize-vector3
|
|
158
|
+
(or (:cameraPosition payload)
|
|
159
|
+
(:camera-position payload)
|
|
160
|
+
(get-in payload [:camera :position])
|
|
161
|
+
(:position payload)))
|
|
162
|
+
target-position (normalize-vector3
|
|
163
|
+
(or (:targetPosition payload)
|
|
164
|
+
(:target-position payload)
|
|
165
|
+
(get-in payload [:controls :target])
|
|
166
|
+
(:target payload)))
|
|
167
|
+
model-quaternion (normalize-quaternion
|
|
168
|
+
(or (:modelQuaternion payload)
|
|
169
|
+
(:model-quaternion payload)
|
|
170
|
+
(get-in payload [:model :quaternion])
|
|
171
|
+
(get-in payload [:model :worldQuaternion])
|
|
172
|
+
(:worldQuaternion payload)))
|
|
173
|
+
offset (compute-camera-relative-offset camera-position
|
|
174
|
+
target-position
|
|
175
|
+
model-quaternion
|
|
176
|
+
config)]
|
|
177
|
+
{:kind "camera.relative"
|
|
178
|
+
:agency "cameraContext"
|
|
179
|
+
:sequence sequence
|
|
180
|
+
:source (or (:source payload) "runtime")
|
|
181
|
+
:observedAt observed-at
|
|
182
|
+
:cameraPosition camera-position
|
|
183
|
+
:targetPosition target-position
|
|
184
|
+
:modelQuaternion model-quaternion
|
|
185
|
+
:relativeOffset offset
|
|
186
|
+
:stale false}))
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
(ns polymer.camera-context.planner)
|
|
2
|
+
|
|
3
|
+
;; The first Camera Context planner is deliberately small. It validates the
|
|
4
|
+
;; command vocabulary and produces auditable action data; the agency and
|
|
5
|
+
;; scheduler still execute those actions explicitly.
|
|
6
|
+
|
|
7
|
+
(def supported-command-types
|
|
8
|
+
#{"configure"
|
|
9
|
+
"updateCamera"
|
|
10
|
+
"publishCameraFacts"
|
|
11
|
+
"invalidateStale"
|
|
12
|
+
"reset"})
|
|
13
|
+
|
|
14
|
+
(defn command-goal
|
|
15
|
+
[command state now-ms]
|
|
16
|
+
{:type (:type command)
|
|
17
|
+
:hasPublishedFact (boolean (:lastPublishedAt state))
|
|
18
|
+
:stale (:stale state)
|
|
19
|
+
:nowMs now-ms})
|
|
20
|
+
|
|
21
|
+
(defn failure-step
|
|
22
|
+
[goal]
|
|
23
|
+
(when-not (contains? supported-command-types (:type goal))
|
|
24
|
+
{:op "fail"
|
|
25
|
+
:reason "unsupported-command"
|
|
26
|
+
:commandType (:type goal)}))
|
|
27
|
+
|
|
28
|
+
(defn command-steps
|
|
29
|
+
[goal]
|
|
30
|
+
(if-let [failure (failure-step goal)]
|
|
31
|
+
[failure]
|
|
32
|
+
(case (:type goal)
|
|
33
|
+
"configure" [{:op "apply-config"}]
|
|
34
|
+
"updateCamera" [{:op "stabilize/coalesce-update"}
|
|
35
|
+
{:op "publish-camera-facts"}]
|
|
36
|
+
"publishCameraFacts" [{:op "publish-camera-facts"}]
|
|
37
|
+
"invalidateStale" [{:op "invalidate-stale"}]
|
|
38
|
+
"reset" [{:op "reset-state"}])))
|
|
39
|
+
|
|
40
|
+
(defn plan-ok?
|
|
41
|
+
[steps]
|
|
42
|
+
(not= "fail" (:op (first steps))))
|
|
43
|
+
|
|
44
|
+
(defn plan-command
|
|
45
|
+
[command state now-ms]
|
|
46
|
+
(let [goal (command-goal command state now-ms)
|
|
47
|
+
steps (command-steps goal)]
|
|
48
|
+
{:goal goal
|
|
49
|
+
:steps steps
|
|
50
|
+
:ok (plan-ok? steps)}))
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
(ns polymer.camera-context.scheduler)
|
|
2
|
+
|
|
3
|
+
;; The scheduler is the only Camera Context namespace that owns timers. Runtime
|
|
4
|
+
;; camera notifications can arrive at frame cadence, but the agency should only
|
|
5
|
+
;; publish stabilized facts after this coalescing boundary.
|
|
6
|
+
|
|
7
|
+
(defn now-ms
|
|
8
|
+
[]
|
|
9
|
+
(.now js/Date))
|
|
10
|
+
|
|
11
|
+
(defn clear-timeout!
|
|
12
|
+
[timer-atom]
|
|
13
|
+
(when-let [timer @timer-atom]
|
|
14
|
+
(js/clearTimeout timer)
|
|
15
|
+
(reset! timer-atom nil)))
|
|
16
|
+
|
|
17
|
+
(defn queue-entry
|
|
18
|
+
[effect index]
|
|
19
|
+
(assoc effect
|
|
20
|
+
:queueIndex index
|
|
21
|
+
:queuedAt (now-ms)))
|
|
22
|
+
|
|
23
|
+
(defn create-scheduler
|
|
24
|
+
[{:keys [state-atom publish-fact! invalidate-stale!]}]
|
|
25
|
+
(let [publish-timer (atom nil)
|
|
26
|
+
stale-timer (atom nil)
|
|
27
|
+
pending-fact (atom nil)
|
|
28
|
+
queue (atom [])
|
|
29
|
+
disposed? (atom false)]
|
|
30
|
+
(letfn [(enqueue! [effect]
|
|
31
|
+
(let [entry (queue-entry effect (count @queue))]
|
|
32
|
+
(swap! queue conj entry)
|
|
33
|
+
entry))
|
|
34
|
+
|
|
35
|
+
(schedule-stale! []
|
|
36
|
+
(clear-timeout! stale-timer)
|
|
37
|
+
(when-not @disposed?
|
|
38
|
+
(let [delay-ms (get-in @state-atom [:config :staleAfterMs])
|
|
39
|
+
published-at (:lastPublishedAt @state-atom)]
|
|
40
|
+
(when (pos? delay-ms)
|
|
41
|
+
(reset! stale-timer
|
|
42
|
+
(js/setTimeout
|
|
43
|
+
(fn []
|
|
44
|
+
(reset! stale-timer nil)
|
|
45
|
+
(when (and (not @disposed?)
|
|
46
|
+
(nil? @pending-fact)
|
|
47
|
+
(= published-at (:lastPublishedAt @state-atom)))
|
|
48
|
+
(enqueue! {:type "invalidateStale"
|
|
49
|
+
:agency "cameraContext"
|
|
50
|
+
:reason "timeout"})
|
|
51
|
+
(invalidate-stale! "timeout")))
|
|
52
|
+
delay-ms))))))
|
|
53
|
+
|
|
54
|
+
(publish-now! []
|
|
55
|
+
(when-not @disposed?
|
|
56
|
+
(clear-timeout! publish-timer)
|
|
57
|
+
(when-let [fact @pending-fact]
|
|
58
|
+
(reset! pending-fact nil)
|
|
59
|
+
(enqueue! {:type "publishCameraFact"
|
|
60
|
+
:agency "cameraContext"
|
|
61
|
+
:sequence (:sequence fact)})
|
|
62
|
+
(publish-fact! fact)
|
|
63
|
+
(schedule-stale!))))
|
|
64
|
+
|
|
65
|
+
(schedule-publish! [fact]
|
|
66
|
+
(when-not @disposed?
|
|
67
|
+
(reset! pending-fact fact)
|
|
68
|
+
(clear-timeout! publish-timer)
|
|
69
|
+
(clear-timeout! stale-timer)
|
|
70
|
+
(let [delay-ms (get-in @state-atom [:config :coalesceMs])]
|
|
71
|
+
(enqueue! {:type "coalesceCameraFact"
|
|
72
|
+
:agency "cameraContext"
|
|
73
|
+
:sequence (:sequence fact)
|
|
74
|
+
:coalesceMs delay-ms})
|
|
75
|
+
(if (pos? delay-ms)
|
|
76
|
+
(reset! publish-timer
|
|
77
|
+
(js/setTimeout publish-now! delay-ms))
|
|
78
|
+
(publish-now!)))))]
|
|
79
|
+
{:schedule-camera-update
|
|
80
|
+
(fn [fact]
|
|
81
|
+
(schedule-publish! fact))
|
|
82
|
+
|
|
83
|
+
:publish-now
|
|
84
|
+
(fn []
|
|
85
|
+
(publish-now!))
|
|
86
|
+
|
|
87
|
+
:invalidate-stale
|
|
88
|
+
(fn [reason]
|
|
89
|
+
(when-not @disposed?
|
|
90
|
+
(clear-timeout! stale-timer)
|
|
91
|
+
(enqueue! {:type "invalidateStale"
|
|
92
|
+
:agency "cameraContext"
|
|
93
|
+
:reason reason})
|
|
94
|
+
(invalidate-stale! reason)))
|
|
95
|
+
|
|
96
|
+
:reset
|
|
97
|
+
(fn []
|
|
98
|
+
(clear-timeout! publish-timer)
|
|
99
|
+
(clear-timeout! stale-timer)
|
|
100
|
+
(reset! pending-fact nil)
|
|
101
|
+
(reset! queue []))
|
|
102
|
+
|
|
103
|
+
:queue
|
|
104
|
+
(fn []
|
|
105
|
+
@queue)
|
|
106
|
+
|
|
107
|
+
:dispose
|
|
108
|
+
(fn []
|
|
109
|
+
(reset! disposed? true)
|
|
110
|
+
(clear-timeout! publish-timer)
|
|
111
|
+
(clear-timeout! stale-timer)
|
|
112
|
+
(reset! pending-fact nil)
|
|
113
|
+
(reset! queue []))})))
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
(ns polymer.camera-context.state
|
|
2
|
+
(:require [polymer.camera-context.domain :as domain]))
|
|
3
|
+
|
|
4
|
+
;; Camera Context state is the local ledger of stable runtime facts. The agency
|
|
5
|
+
;; may observe many camera updates, but only scheduler-approved facts become
|
|
6
|
+
;; published stream data for peer agencies.
|
|
7
|
+
|
|
8
|
+
(def default-config
|
|
9
|
+
{:coalesceMs 16
|
|
10
|
+
:staleAfterMs 1000
|
|
11
|
+
:epsilon domain/default-epsilon
|
|
12
|
+
:yawWeight domain/default-yaw-weight
|
|
13
|
+
:pitchWeight domain/default-pitch-weight})
|
|
14
|
+
|
|
15
|
+
(defn normalize-config
|
|
16
|
+
[config]
|
|
17
|
+
(let [input (merge default-config (domain/data-map config))]
|
|
18
|
+
{:coalesceMs (int (domain/clamp 0 1000
|
|
19
|
+
(domain/number-or (:coalesceMs input)
|
|
20
|
+
(:coalesceMs default-config))))
|
|
21
|
+
:staleAfterMs (int (domain/clamp 0 60000
|
|
22
|
+
(domain/number-or (:staleAfterMs input)
|
|
23
|
+
(:staleAfterMs default-config))))
|
|
24
|
+
:epsilon (domain/clamp 1.0e-9 1
|
|
25
|
+
(domain/number-or (:epsilon input)
|
|
26
|
+
(:epsilon default-config)))
|
|
27
|
+
:yawWeight (domain/clamp 0 1
|
|
28
|
+
(domain/number-or (:yawWeight input)
|
|
29
|
+
(:yawWeight default-config)))
|
|
30
|
+
:pitchWeight (domain/clamp 0 1
|
|
31
|
+
(domain/number-or (:pitchWeight input)
|
|
32
|
+
(:pitchWeight default-config)))}))
|
|
33
|
+
|
|
34
|
+
(defn default-state
|
|
35
|
+
[config]
|
|
36
|
+
{:agency "cameraContext"
|
|
37
|
+
:status "idle"
|
|
38
|
+
:cameraPosition nil
|
|
39
|
+
:targetPosition nil
|
|
40
|
+
:modelQuaternion domain/identity-quaternion
|
|
41
|
+
:relativeOffset domain/zero-offset
|
|
42
|
+
:lastUpdatedAt nil
|
|
43
|
+
:lastPublishedAt nil
|
|
44
|
+
:lastFact nil
|
|
45
|
+
:lastPlan nil
|
|
46
|
+
:lastInvalidation nil
|
|
47
|
+
:stale false
|
|
48
|
+
:invalidated false
|
|
49
|
+
:updateCount 0
|
|
50
|
+
:publishedCount 0
|
|
51
|
+
:config (normalize-config config)})
|
|
52
|
+
|
|
53
|
+
(defn config->state
|
|
54
|
+
[config]
|
|
55
|
+
(default-state config))
|
|
56
|
+
|
|
57
|
+
(defn visible-state
|
|
58
|
+
[state]
|
|
59
|
+
(clj->js state))
|
|
60
|
+
|
|
61
|
+
(defn configure
|
|
62
|
+
[state config]
|
|
63
|
+
(assoc state :config (normalize-config (merge (:config state)
|
|
64
|
+
(domain/data-map config)))))
|
|
65
|
+
|
|
66
|
+
(defn reset-state
|
|
67
|
+
[state]
|
|
68
|
+
;; Reset clears camera facts and counters while preserving the normalized
|
|
69
|
+
;; runtime cadence supplied when this agency was created or configured.
|
|
70
|
+
(default-state (:config state)))
|
|
71
|
+
|
|
72
|
+
(defn record-plan
|
|
73
|
+
[state plan]
|
|
74
|
+
(assoc state :lastPlan plan))
|
|
75
|
+
|
|
76
|
+
(defn record-camera-update
|
|
77
|
+
[state fact]
|
|
78
|
+
(-> state
|
|
79
|
+
(assoc :status "pending"
|
|
80
|
+
:cameraPosition (:cameraPosition fact)
|
|
81
|
+
:targetPosition (:targetPosition fact)
|
|
82
|
+
:modelQuaternion (:modelQuaternion fact)
|
|
83
|
+
:relativeOffset (:relativeOffset fact)
|
|
84
|
+
:lastUpdatedAt (:observedAt fact)
|
|
85
|
+
:stale false
|
|
86
|
+
:invalidated false)
|
|
87
|
+
(update :updateCount inc)))
|
|
88
|
+
|
|
89
|
+
(defn record-published
|
|
90
|
+
[state fact published-at]
|
|
91
|
+
(-> state
|
|
92
|
+
(assoc :status "fresh"
|
|
93
|
+
:lastFact (assoc fact :publishedAt published-at)
|
|
94
|
+
:lastPublishedAt published-at
|
|
95
|
+
:stale false
|
|
96
|
+
:invalidated false)
|
|
97
|
+
(update :publishedCount inc)))
|
|
98
|
+
|
|
99
|
+
(defn stale-fact
|
|
100
|
+
[state invalidated-at reason]
|
|
101
|
+
{:kind "camera.stale"
|
|
102
|
+
:agency "cameraContext"
|
|
103
|
+
:status "stale"
|
|
104
|
+
:reason reason
|
|
105
|
+
:invalidatedAt invalidated-at
|
|
106
|
+
:lastPublishedAt (:lastPublishedAt state)
|
|
107
|
+
:lastObservedAt (:lastUpdatedAt state)
|
|
108
|
+
:lastRelativeOffset (:relativeOffset state)})
|
|
109
|
+
|
|
110
|
+
(defn can-invalidate?
|
|
111
|
+
[state]
|
|
112
|
+
(and (:lastPublishedAt state) (not (:stale state))))
|
|
113
|
+
|
|
114
|
+
(defn record-stale
|
|
115
|
+
[state invalidated-at reason]
|
|
116
|
+
(let [fact (stale-fact state invalidated-at reason)]
|
|
117
|
+
(assoc state
|
|
118
|
+
:status "stale"
|
|
119
|
+
:stale true
|
|
120
|
+
:invalidated true
|
|
121
|
+
:lastInvalidation fact)))
|