@almadar/std 16.174.0 → 16.176.0
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/behaviors/lolo/ui/core/molecules/ui-table-view.lolo +4 -9
- package/behaviors/lolo/ui/learning/atoms/std-learn-classify.lolo +132 -22
- package/behaviors/lolo/ui/learning/atoms/std-learn-derivation.lolo +428 -0
- package/behaviors/lolo/ui/learning/atoms/std-learn-doser.lolo +520 -0
- package/behaviors/lolo/ui/learning/atoms/std-learn-scatter-fit.lolo +805 -0
- package/behaviors/lolo/ui/learning/atoms/std-learn-tour-3d.lolo +31 -2
- package/behaviors/lolo/ui/learning/atoms/std-learn-transport.lolo +186 -0
- package/behaviors/lolo/ui/learning/atoms/std-learn-tree.lolo +522 -0
- package/behaviors/lolo/ui/learning/atoms/ui-algo-graph-canvas.lolo +192 -0
- package/behaviors/lolo/ui/learning/atoms/ui-algorithm-canvas.lolo +111 -2
- package/behaviors/lolo/ui/learning/atoms/ui-biology-canvas.lolo +150 -1
- package/behaviors/lolo/ui/learning/atoms/ui-chemistry-canvas.lolo +132 -1
- package/behaviors/lolo/ui/learning/atoms/ui-learning-canvas.lolo +48 -1
- package/behaviors/lolo/ui/learning/atoms/ui-math-canvas.lolo +141 -1
- package/behaviors/lolo/ui/learning/atoms/ui-physics-canvas.lolo +199 -1
- package/behaviors/registry/ui/core/molecules/ui-table-view.orb +4 -39
- package/behaviors/registry/ui/learning/atoms/std-learn-classify.orb +1288 -197
- package/behaviors/registry/ui/learning/atoms/std-learn-derivation.orb +2512 -0
- package/behaviors/registry/ui/learning/atoms/std-learn-doser.orb +3678 -0
- package/behaviors/registry/ui/learning/atoms/std-learn-scatter-fit.orb +6539 -0
- package/behaviors/registry/ui/learning/atoms/std-learn-tour-3d.orb +173 -2
- package/behaviors/registry/ui/learning/atoms/std-learn-transport.orb +986 -0
- package/behaviors/registry/ui/learning/atoms/std-learn-tree.orb +3333 -0
- package/behaviors/registry/ui/learning/atoms/ui-algo-graph-canvas.orb +716 -0
- package/behaviors/registry/ui/learning/atoms/ui-algorithm-canvas.orb +506 -1
- package/behaviors/registry/ui/learning/atoms/ui-biology-canvas.orb +684 -0
- package/behaviors/registry/ui/learning/atoms/ui-chemistry-canvas.orb +634 -0
- package/behaviors/registry/ui/learning/atoms/ui-learning-canvas.orb +256 -0
- package/behaviors/registry/ui/learning/atoms/ui-math-canvas.orb +736 -12
- package/behaviors/registry/ui/learning/atoms/ui-physics-canvas.orb +1061 -33
- package/dist/behaviors/exports-reader.js +2448 -2184
- package/dist/behaviors/functions/index.d.ts +800 -13
- package/dist/behaviors/functions/index.js +2431 -2185
- package/dist/behaviors/index.d.ts +1 -1
- package/dist/behaviors/index.js +2449 -2185
- package/dist/behaviors/query.js +2448 -2184
- package/dist/behaviors-embeddings.hash.json +3 -3
- package/dist/behaviors-embeddings.json +318 -294
- package/dist/behaviors-registry.json +4391 -1298
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2449 -2185
- package/dist/knob-embeddings.hash.json +3 -3
- package/dist/knob-embeddings.json +1 -1
- package/dist/registry/factory-signatures.json +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
app std-learn-doser "1.0.0"
|
|
2
|
+
"Generic monotone-accumulation dosing instrument — the learner adds a quantity in large or small config-declared increments (or lets it auto-drip on its own inline clock) and watches an accumulated dose climb through a config-declared roster of bands/zones (thresholds, labels, colors all config), rendered as a reservoir/source vessel pair plus a dose-history trace on a canvas. Crossing into a terminal band is irreversible — done latches true and only Reset revives it. Standalone by default it renders its own control strip AND scene canvas; set showCanvas false to embed only the control strip (via @trait.<name>) beneath a composed sibling trait that owns the domain-accurate scene — see std-chem-titration for the transport-sibling shape. Behind a lab router set matchMode and it renders only when MODE_SELECTED matches."
|
|
3
|
+
orbital LearnDoserOrbital {
|
|
4
|
+
type DoserContainer = {
|
|
5
|
+
x : number
|
|
6
|
+
y : number
|
|
7
|
+
width : number
|
|
8
|
+
height : number
|
|
9
|
+
divider : "none" | "solid" | "dashed" | "dotted"
|
|
10
|
+
level : number
|
|
11
|
+
levelColor : string
|
|
12
|
+
label : string
|
|
13
|
+
}
|
|
14
|
+
type DoserReadout = {
|
|
15
|
+
label : string
|
|
16
|
+
value : string
|
|
17
|
+
}
|
|
18
|
+
type DoserTracePoint = {
|
|
19
|
+
x : number!
|
|
20
|
+
y : number!
|
|
21
|
+
}
|
|
22
|
+
type DoserBand = {
|
|
23
|
+
upperBound : number!
|
|
24
|
+
label : string!
|
|
25
|
+
color : string!
|
|
26
|
+
terminal : boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
entity DoserScene [runtime, shared] {
|
|
30
|
+
id : string!
|
|
31
|
+
active : boolean = false
|
|
32
|
+
title : string
|
|
33
|
+
done : boolean = false
|
|
34
|
+
running : boolean = false
|
|
35
|
+
elapsedMs : number = 0
|
|
36
|
+
dose : number = 0
|
|
37
|
+
bandLabel : string
|
|
38
|
+
bandColor : string
|
|
39
|
+
narration : string
|
|
40
|
+
containers : [DoserContainer]
|
|
41
|
+
readouts : [DoserReadout]
|
|
42
|
+
doseSeries : [DoserTracePoint]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
trait LearnDoserSim -> @rebindable DoserScene [interaction, instance] {
|
|
46
|
+
initial: idle
|
|
47
|
+
|
|
48
|
+
state idle {
|
|
49
|
+
INIT -> idle when (== @config.matchMode "")
|
|
50
|
+
(set @entity.active true)
|
|
51
|
+
(set @entity.title @config.title)
|
|
52
|
+
(set @entity.dose 0)
|
|
53
|
+
(set @entity.done false)
|
|
54
|
+
(set @entity.running @config.autoDrip)
|
|
55
|
+
(set @entity.elapsedMs 0)
|
|
56
|
+
(let (
|
|
57
|
+
(matched (array/filter @config.bands (fn b (<= @entity.dose (object/get @b upperBound)))))
|
|
58
|
+
(band (if (> (array/len @matched) 0) (array/first @matched) (array/nth @config.bands (- (array/len @config.bands) 1))))
|
|
59
|
+
)
|
|
60
|
+
(do
|
|
61
|
+
(set @entity.bandLabel (object/get @band label))
|
|
62
|
+
(set @entity.bandColor (object/get @band color))
|
|
63
|
+
(set @entity.done (if (object/get @band terminal false) true @entity.done))
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
(set @entity.containers [
|
|
67
|
+
{ x: (* @config.width 0.12), y: (* @config.height 0.3), width: (* @config.width 0.28), height: (* @config.height 0.6), divider: "none", level: (math/clamp (/ @entity.dose @config.capacity) 0 1), levelColor: @entity.bandColor, label: @config.reservoirLabel },
|
|
68
|
+
{ x: (* @config.width 0.58), y: (* @config.height 0.05), width: (* @config.width 0.06), height: (* @config.height 0.55), divider: "none", level: (math/clamp (/ (- @config.capacity @entity.dose) @config.capacity) 0 1), levelColor: @config.sourceColor, label: @config.sourceLabel }
|
|
69
|
+
])
|
|
70
|
+
(set @entity.readouts [
|
|
71
|
+
{ label: @config.quantityLabel, value: (str/concat "" (math/round @entity.dose 2)) },
|
|
72
|
+
{ label: @config.zoneReadoutLabel, value: @entity.bandLabel }
|
|
73
|
+
])
|
|
74
|
+
(set @entity.doseSeries [{ x: 0, y: @entity.dose }])
|
|
75
|
+
(set @entity.narration (str/concat @config.quantityLabel (str/concat " at " (str/concat (str/concat "" (math/round @entity.dose 2)) (str/concat " — " @entity.bandLabel)))))
|
|
76
|
+
(if @config.showCanvas
|
|
77
|
+
(render-ui main {
|
|
78
|
+
type: box
|
|
79
|
+
padding: md
|
|
80
|
+
children: [
|
|
81
|
+
{ type: stack, direction: vertical, gap: md, children: [
|
|
82
|
+
{ type: typography, variant: h4, content: @entity.title },
|
|
83
|
+
{ type: stack, direction: horizontal, gap: sm, children: [
|
|
84
|
+
{ type: button, action: ADD_SMALL, label: (str/concat "+ " (str/concat (str/concat "" @config.stepSmall) (if (== @config.unit "") "" (str/concat " " @config.unit)))), variant: primary },
|
|
85
|
+
{ type: button, action: ADD_LARGE, label: (str/concat "+ " (str/concat (str/concat "" @config.stepLarge) (if (== @config.unit "") "" (str/concat " " @config.unit)))), variant: secondary },
|
|
86
|
+
{ type: button, action: TOGGLE_DRIP, label: (if @entity.running "Pause" "Auto-dose"), variant: secondary },
|
|
87
|
+
{ type: button, action: RESET, label: "Reset", variant: ghost }
|
|
88
|
+
] },
|
|
89
|
+
{ type: chemistry-canvas, width: @config.width, height: @config.height, backgroundColor: @config.backgroundColor, containers: @entity.containers, readouts: @entity.readouts, traces: [ { series: [ { samples: @entity.doseSeries, label: @config.quantityLabel } ], xLabel: "step", yLabel: @config.quantityLabel } ] },
|
|
90
|
+
{ type: typography, variant: body, content: @entity.narration }
|
|
91
|
+
] }
|
|
92
|
+
]
|
|
93
|
+
})
|
|
94
|
+
(render-ui main {
|
|
95
|
+
type: box
|
|
96
|
+
padding: md
|
|
97
|
+
children: [
|
|
98
|
+
{ type: stack, direction: vertical, gap: md, children: [
|
|
99
|
+
{ type: typography, variant: h4, content: @entity.title },
|
|
100
|
+
{ type: stack, direction: horizontal, gap: sm, children: [
|
|
101
|
+
{ type: button, action: ADD_SMALL, label: (str/concat "+ " (str/concat (str/concat "" @config.stepSmall) (if (== @config.unit "") "" (str/concat " " @config.unit)))), variant: primary },
|
|
102
|
+
{ type: button, action: ADD_LARGE, label: (str/concat "+ " (str/concat (str/concat "" @config.stepLarge) (if (== @config.unit "") "" (str/concat " " @config.unit)))), variant: secondary },
|
|
103
|
+
{ type: button, action: TOGGLE_DRIP, label: (if @entity.running "Pause" "Auto-dose"), variant: secondary },
|
|
104
|
+
{ type: button, action: RESET, label: "Reset", variant: ghost }
|
|
105
|
+
] }
|
|
106
|
+
] }
|
|
107
|
+
]
|
|
108
|
+
}))
|
|
109
|
+
(emit DOSER_UPDATED { running: @entity.running, dose: @entity.dose, bandLabel: @entity.bandLabel, done: @entity.done })
|
|
110
|
+
|
|
111
|
+
MODE_SELECTED -> idle when (== ?mode @config.matchMode)
|
|
112
|
+
(set @entity.active true)
|
|
113
|
+
(set @entity.title @config.title)
|
|
114
|
+
(set @entity.dose 0)
|
|
115
|
+
(set @entity.done false)
|
|
116
|
+
(set @entity.running @config.autoDrip)
|
|
117
|
+
(set @entity.elapsedMs 0)
|
|
118
|
+
(let (
|
|
119
|
+
(matched (array/filter @config.bands (fn b (<= @entity.dose (object/get @b upperBound)))))
|
|
120
|
+
(band (if (> (array/len @matched) 0) (array/first @matched) (array/nth @config.bands (- (array/len @config.bands) 1))))
|
|
121
|
+
)
|
|
122
|
+
(do
|
|
123
|
+
(set @entity.bandLabel (object/get @band label))
|
|
124
|
+
(set @entity.bandColor (object/get @band color))
|
|
125
|
+
(set @entity.done (if (object/get @band terminal false) true @entity.done))
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
(set @entity.containers [
|
|
129
|
+
{ x: (* @config.width 0.12), y: (* @config.height 0.3), width: (* @config.width 0.28), height: (* @config.height 0.6), divider: "none", level: (math/clamp (/ @entity.dose @config.capacity) 0 1), levelColor: @entity.bandColor, label: @config.reservoirLabel },
|
|
130
|
+
{ x: (* @config.width 0.58), y: (* @config.height 0.05), width: (* @config.width 0.06), height: (* @config.height 0.55), divider: "none", level: (math/clamp (/ (- @config.capacity @entity.dose) @config.capacity) 0 1), levelColor: @config.sourceColor, label: @config.sourceLabel }
|
|
131
|
+
])
|
|
132
|
+
(set @entity.readouts [
|
|
133
|
+
{ label: @config.quantityLabel, value: (str/concat "" (math/round @entity.dose 2)) },
|
|
134
|
+
{ label: @config.zoneReadoutLabel, value: @entity.bandLabel }
|
|
135
|
+
])
|
|
136
|
+
(set @entity.doseSeries [{ x: 0, y: @entity.dose }])
|
|
137
|
+
(set @entity.narration (str/concat @config.quantityLabel (str/concat " at " (str/concat (str/concat "" (math/round @entity.dose 2)) (str/concat " — " @entity.bandLabel)))))
|
|
138
|
+
(if @config.showCanvas
|
|
139
|
+
(render-ui main {
|
|
140
|
+
type: box
|
|
141
|
+
padding: md
|
|
142
|
+
children: [
|
|
143
|
+
{ type: stack, direction: vertical, gap: md, children: [
|
|
144
|
+
{ type: typography, variant: h4, content: @entity.title },
|
|
145
|
+
{ type: stack, direction: horizontal, gap: sm, children: [
|
|
146
|
+
{ type: button, action: ADD_SMALL, label: (str/concat "+ " (str/concat (str/concat "" @config.stepSmall) (if (== @config.unit "") "" (str/concat " " @config.unit)))), variant: primary },
|
|
147
|
+
{ type: button, action: ADD_LARGE, label: (str/concat "+ " (str/concat (str/concat "" @config.stepLarge) (if (== @config.unit "") "" (str/concat " " @config.unit)))), variant: secondary },
|
|
148
|
+
{ type: button, action: TOGGLE_DRIP, label: (if @entity.running "Pause" "Auto-dose"), variant: secondary },
|
|
149
|
+
{ type: button, action: RESET, label: "Reset", variant: ghost }
|
|
150
|
+
] },
|
|
151
|
+
{ type: chemistry-canvas, width: @config.width, height: @config.height, backgroundColor: @config.backgroundColor, containers: @entity.containers, readouts: @entity.readouts, traces: [ { series: [ { samples: @entity.doseSeries, label: @config.quantityLabel } ], xLabel: "step", yLabel: @config.quantityLabel } ] },
|
|
152
|
+
{ type: typography, variant: body, content: @entity.narration }
|
|
153
|
+
] }
|
|
154
|
+
]
|
|
155
|
+
})
|
|
156
|
+
(render-ui main {
|
|
157
|
+
type: box
|
|
158
|
+
padding: md
|
|
159
|
+
children: [
|
|
160
|
+
{ type: stack, direction: vertical, gap: md, children: [
|
|
161
|
+
{ type: typography, variant: h4, content: @entity.title },
|
|
162
|
+
{ type: stack, direction: horizontal, gap: sm, children: [
|
|
163
|
+
{ type: button, action: ADD_SMALL, label: (str/concat "+ " (str/concat (str/concat "" @config.stepSmall) (if (== @config.unit "") "" (str/concat " " @config.unit)))), variant: primary },
|
|
164
|
+
{ type: button, action: ADD_LARGE, label: (str/concat "+ " (str/concat (str/concat "" @config.stepLarge) (if (== @config.unit "") "" (str/concat " " @config.unit)))), variant: secondary },
|
|
165
|
+
{ type: button, action: TOGGLE_DRIP, label: (if @entity.running "Pause" "Auto-dose"), variant: secondary },
|
|
166
|
+
{ type: button, action: RESET, label: "Reset", variant: ghost }
|
|
167
|
+
] }
|
|
168
|
+
] }
|
|
169
|
+
]
|
|
170
|
+
}))
|
|
171
|
+
(emit DOSER_UPDATED { running: @entity.running, dose: @entity.dose, bandLabel: @entity.bandLabel, done: @entity.done })
|
|
172
|
+
|
|
173
|
+
ADD_SMALL -> idle when (and (== @entity.active true) (== @entity.done false))
|
|
174
|
+
(set @entity.dose (math/min (+ @entity.dose @config.stepSmall) @config.capacity))
|
|
175
|
+
(let (
|
|
176
|
+
(matched (array/filter @config.bands (fn b (<= @entity.dose (object/get @b upperBound)))))
|
|
177
|
+
(band (if (> (array/len @matched) 0) (array/first @matched) (array/nth @config.bands (- (array/len @config.bands) 1))))
|
|
178
|
+
)
|
|
179
|
+
(do
|
|
180
|
+
(set @entity.bandLabel (object/get @band label))
|
|
181
|
+
(set @entity.bandColor (object/get @band color))
|
|
182
|
+
(set @entity.done (if (object/get @band terminal false) true @entity.done))
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
(set @entity.containers [
|
|
186
|
+
{ x: (* @config.width 0.12), y: (* @config.height 0.3), width: (* @config.width 0.28), height: (* @config.height 0.6), divider: "none", level: (math/clamp (/ @entity.dose @config.capacity) 0 1), levelColor: @entity.bandColor, label: @config.reservoirLabel },
|
|
187
|
+
{ x: (* @config.width 0.58), y: (* @config.height 0.05), width: (* @config.width 0.06), height: (* @config.height 0.55), divider: "none", level: (math/clamp (/ (- @config.capacity @entity.dose) @config.capacity) 0 1), levelColor: @config.sourceColor, label: @config.sourceLabel }
|
|
188
|
+
])
|
|
189
|
+
(set @entity.readouts [
|
|
190
|
+
{ label: @config.quantityLabel, value: (str/concat "" (math/round @entity.dose 2)) },
|
|
191
|
+
{ label: @config.zoneReadoutLabel, value: @entity.bandLabel }
|
|
192
|
+
])
|
|
193
|
+
(set @entity.doseSeries (array/takeLast (array/append @entity.doseSeries { x: (array/len @entity.doseSeries), y: @entity.dose }) @config.historyLength))
|
|
194
|
+
(set @entity.narration (str/concat @config.quantityLabel (str/concat " at " (str/concat (str/concat "" (math/round @entity.dose 2)) (str/concat " — " (str/concat @entity.bandLabel (if @entity.done " — locked" (if @entity.running " — auto-dosing" " — ready"))))))))
|
|
195
|
+
(if @config.showCanvas
|
|
196
|
+
(render-ui main {
|
|
197
|
+
type: box
|
|
198
|
+
padding: md
|
|
199
|
+
children: [
|
|
200
|
+
{ type: stack, direction: vertical, gap: md, children: [
|
|
201
|
+
{ type: typography, variant: h4, content: @entity.title },
|
|
202
|
+
{ type: stack, direction: horizontal, gap: sm, children: [
|
|
203
|
+
{ type: button, action: ADD_SMALL, label: (str/concat "+ " (str/concat (str/concat "" @config.stepSmall) (if (== @config.unit "") "" (str/concat " " @config.unit)))), variant: primary },
|
|
204
|
+
{ type: button, action: ADD_LARGE, label: (str/concat "+ " (str/concat (str/concat "" @config.stepLarge) (if (== @config.unit "") "" (str/concat " " @config.unit)))), variant: secondary },
|
|
205
|
+
{ type: button, action: TOGGLE_DRIP, label: (if @entity.running "Pause" "Auto-dose"), variant: secondary },
|
|
206
|
+
{ type: button, action: RESET, label: "Reset", variant: ghost }
|
|
207
|
+
] },
|
|
208
|
+
{ type: chemistry-canvas, width: @config.width, height: @config.height, backgroundColor: @config.backgroundColor, containers: @entity.containers, readouts: @entity.readouts, traces: [ { series: [ { samples: @entity.doseSeries, label: @config.quantityLabel } ], xLabel: "step", yLabel: @config.quantityLabel } ] },
|
|
209
|
+
{ type: typography, variant: body, content: @entity.narration }
|
|
210
|
+
] }
|
|
211
|
+
]
|
|
212
|
+
})
|
|
213
|
+
(render-ui main {
|
|
214
|
+
type: box
|
|
215
|
+
padding: md
|
|
216
|
+
children: [
|
|
217
|
+
{ type: stack, direction: vertical, gap: md, children: [
|
|
218
|
+
{ type: typography, variant: h4, content: @entity.title },
|
|
219
|
+
{ type: stack, direction: horizontal, gap: sm, children: [
|
|
220
|
+
{ type: button, action: ADD_SMALL, label: (str/concat "+ " (str/concat (str/concat "" @config.stepSmall) (if (== @config.unit "") "" (str/concat " " @config.unit)))), variant: primary },
|
|
221
|
+
{ type: button, action: ADD_LARGE, label: (str/concat "+ " (str/concat (str/concat "" @config.stepLarge) (if (== @config.unit "") "" (str/concat " " @config.unit)))), variant: secondary },
|
|
222
|
+
{ type: button, action: TOGGLE_DRIP, label: (if @entity.running "Pause" "Auto-dose"), variant: secondary },
|
|
223
|
+
{ type: button, action: RESET, label: "Reset", variant: ghost }
|
|
224
|
+
] }
|
|
225
|
+
] }
|
|
226
|
+
]
|
|
227
|
+
}))
|
|
228
|
+
(emit DOSE_ADDED { dose: @entity.dose, bandLabel: @entity.bandLabel, bandColor: @entity.bandColor, done: @entity.done })
|
|
229
|
+
|
|
230
|
+
ADD_LARGE -> idle when (and (== @entity.active true) (== @entity.done false))
|
|
231
|
+
(set @entity.dose (math/min (+ @entity.dose @config.stepLarge) @config.capacity))
|
|
232
|
+
(let (
|
|
233
|
+
(matched (array/filter @config.bands (fn b (<= @entity.dose (object/get @b upperBound)))))
|
|
234
|
+
(band (if (> (array/len @matched) 0) (array/first @matched) (array/nth @config.bands (- (array/len @config.bands) 1))))
|
|
235
|
+
)
|
|
236
|
+
(do
|
|
237
|
+
(set @entity.bandLabel (object/get @band label))
|
|
238
|
+
(set @entity.bandColor (object/get @band color))
|
|
239
|
+
(set @entity.done (if (object/get @band terminal false) true @entity.done))
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
(set @entity.containers [
|
|
243
|
+
{ x: (* @config.width 0.12), y: (* @config.height 0.3), width: (* @config.width 0.28), height: (* @config.height 0.6), divider: "none", level: (math/clamp (/ @entity.dose @config.capacity) 0 1), levelColor: @entity.bandColor, label: @config.reservoirLabel },
|
|
244
|
+
{ x: (* @config.width 0.58), y: (* @config.height 0.05), width: (* @config.width 0.06), height: (* @config.height 0.55), divider: "none", level: (math/clamp (/ (- @config.capacity @entity.dose) @config.capacity) 0 1), levelColor: @config.sourceColor, label: @config.sourceLabel }
|
|
245
|
+
])
|
|
246
|
+
(set @entity.readouts [
|
|
247
|
+
{ label: @config.quantityLabel, value: (str/concat "" (math/round @entity.dose 2)) },
|
|
248
|
+
{ label: @config.zoneReadoutLabel, value: @entity.bandLabel }
|
|
249
|
+
])
|
|
250
|
+
(set @entity.doseSeries (array/takeLast (array/append @entity.doseSeries { x: (array/len @entity.doseSeries), y: @entity.dose }) @config.historyLength))
|
|
251
|
+
(set @entity.narration (str/concat @config.quantityLabel (str/concat " at " (str/concat (str/concat "" (math/round @entity.dose 2)) (str/concat " — " (str/concat @entity.bandLabel (if @entity.done " — locked" (if @entity.running " — auto-dosing" " — ready"))))))))
|
|
252
|
+
(if @config.showCanvas
|
|
253
|
+
(render-ui main {
|
|
254
|
+
type: box
|
|
255
|
+
padding: md
|
|
256
|
+
children: [
|
|
257
|
+
{ type: stack, direction: vertical, gap: md, children: [
|
|
258
|
+
{ type: typography, variant: h4, content: @entity.title },
|
|
259
|
+
{ type: stack, direction: horizontal, gap: sm, children: [
|
|
260
|
+
{ type: button, action: ADD_SMALL, label: (str/concat "+ " (str/concat (str/concat "" @config.stepSmall) (if (== @config.unit "") "" (str/concat " " @config.unit)))), variant: primary },
|
|
261
|
+
{ type: button, action: ADD_LARGE, label: (str/concat "+ " (str/concat (str/concat "" @config.stepLarge) (if (== @config.unit "") "" (str/concat " " @config.unit)))), variant: secondary },
|
|
262
|
+
{ type: button, action: TOGGLE_DRIP, label: (if @entity.running "Pause" "Auto-dose"), variant: secondary },
|
|
263
|
+
{ type: button, action: RESET, label: "Reset", variant: ghost }
|
|
264
|
+
] },
|
|
265
|
+
{ type: chemistry-canvas, width: @config.width, height: @config.height, backgroundColor: @config.backgroundColor, containers: @entity.containers, readouts: @entity.readouts, traces: [ { series: [ { samples: @entity.doseSeries, label: @config.quantityLabel } ], xLabel: "step", yLabel: @config.quantityLabel } ] },
|
|
266
|
+
{ type: typography, variant: body, content: @entity.narration }
|
|
267
|
+
] }
|
|
268
|
+
]
|
|
269
|
+
})
|
|
270
|
+
(render-ui main {
|
|
271
|
+
type: box
|
|
272
|
+
padding: md
|
|
273
|
+
children: [
|
|
274
|
+
{ type: stack, direction: vertical, gap: md, children: [
|
|
275
|
+
{ type: typography, variant: h4, content: @entity.title },
|
|
276
|
+
{ type: stack, direction: horizontal, gap: sm, children: [
|
|
277
|
+
{ type: button, action: ADD_SMALL, label: (str/concat "+ " (str/concat (str/concat "" @config.stepSmall) (if (== @config.unit "") "" (str/concat " " @config.unit)))), variant: primary },
|
|
278
|
+
{ type: button, action: ADD_LARGE, label: (str/concat "+ " (str/concat (str/concat "" @config.stepLarge) (if (== @config.unit "") "" (str/concat " " @config.unit)))), variant: secondary },
|
|
279
|
+
{ type: button, action: TOGGLE_DRIP, label: (if @entity.running "Pause" "Auto-dose"), variant: secondary },
|
|
280
|
+
{ type: button, action: RESET, label: "Reset", variant: ghost }
|
|
281
|
+
] }
|
|
282
|
+
] }
|
|
283
|
+
]
|
|
284
|
+
}))
|
|
285
|
+
(emit DOSE_ADDED { dose: @entity.dose, bandLabel: @entity.bandLabel, bandColor: @entity.bandColor, done: @entity.done })
|
|
286
|
+
|
|
287
|
+
TOGGLE_DRIP -> idle when (== @entity.active true)
|
|
288
|
+
(set @entity.running (not @entity.running))
|
|
289
|
+
(set @entity.elapsedMs 0)
|
|
290
|
+
(if @config.showCanvas
|
|
291
|
+
(render-ui main {
|
|
292
|
+
type: box
|
|
293
|
+
padding: md
|
|
294
|
+
children: [
|
|
295
|
+
{ type: stack, direction: vertical, gap: md, children: [
|
|
296
|
+
{ type: typography, variant: h4, content: @entity.title },
|
|
297
|
+
{ type: stack, direction: horizontal, gap: sm, children: [
|
|
298
|
+
{ type: button, action: ADD_SMALL, label: (str/concat "+ " (str/concat (str/concat "" @config.stepSmall) (if (== @config.unit "") "" (str/concat " " @config.unit)))), variant: primary },
|
|
299
|
+
{ type: button, action: ADD_LARGE, label: (str/concat "+ " (str/concat (str/concat "" @config.stepLarge) (if (== @config.unit "") "" (str/concat " " @config.unit)))), variant: secondary },
|
|
300
|
+
{ type: button, action: TOGGLE_DRIP, label: (if @entity.running "Pause" "Auto-dose"), variant: secondary },
|
|
301
|
+
{ type: button, action: RESET, label: "Reset", variant: ghost }
|
|
302
|
+
] },
|
|
303
|
+
{ type: chemistry-canvas, width: @config.width, height: @config.height, backgroundColor: @config.backgroundColor, containers: @entity.containers, readouts: @entity.readouts, traces: [ { series: [ { samples: @entity.doseSeries, label: @config.quantityLabel } ], xLabel: "step", yLabel: @config.quantityLabel } ] },
|
|
304
|
+
{ type: typography, variant: body, content: @entity.narration }
|
|
305
|
+
] }
|
|
306
|
+
]
|
|
307
|
+
})
|
|
308
|
+
(render-ui main {
|
|
309
|
+
type: box
|
|
310
|
+
padding: md
|
|
311
|
+
children: [
|
|
312
|
+
{ type: stack, direction: vertical, gap: md, children: [
|
|
313
|
+
{ type: typography, variant: h4, content: @entity.title },
|
|
314
|
+
{ type: stack, direction: horizontal, gap: sm, children: [
|
|
315
|
+
{ type: button, action: ADD_SMALL, label: (str/concat "+ " (str/concat (str/concat "" @config.stepSmall) (if (== @config.unit "") "" (str/concat " " @config.unit)))), variant: primary },
|
|
316
|
+
{ type: button, action: ADD_LARGE, label: (str/concat "+ " (str/concat (str/concat "" @config.stepLarge) (if (== @config.unit "") "" (str/concat " " @config.unit)))), variant: secondary },
|
|
317
|
+
{ type: button, action: TOGGLE_DRIP, label: (if @entity.running "Pause" "Auto-dose"), variant: secondary },
|
|
318
|
+
{ type: button, action: RESET, label: "Reset", variant: ghost }
|
|
319
|
+
] }
|
|
320
|
+
] }
|
|
321
|
+
]
|
|
322
|
+
}))
|
|
323
|
+
(emit DOSER_UPDATED { running: @entity.running, dose: @entity.dose, bandLabel: @entity.bandLabel, done: @entity.done })
|
|
324
|
+
|
|
325
|
+
RESET -> idle when (== @entity.active true)
|
|
326
|
+
(set @entity.title @config.title)
|
|
327
|
+
(set @entity.dose 0)
|
|
328
|
+
(set @entity.done false)
|
|
329
|
+
(set @entity.running @config.autoDrip)
|
|
330
|
+
(set @entity.elapsedMs 0)
|
|
331
|
+
(let (
|
|
332
|
+
(matched (array/filter @config.bands (fn b (<= @entity.dose (object/get @b upperBound)))))
|
|
333
|
+
(band (if (> (array/len @matched) 0) (array/first @matched) (array/nth @config.bands (- (array/len @config.bands) 1))))
|
|
334
|
+
)
|
|
335
|
+
(do
|
|
336
|
+
(set @entity.bandLabel (object/get @band label))
|
|
337
|
+
(set @entity.bandColor (object/get @band color))
|
|
338
|
+
(set @entity.done (if (object/get @band terminal false) true @entity.done))
|
|
339
|
+
)
|
|
340
|
+
)
|
|
341
|
+
(set @entity.containers [
|
|
342
|
+
{ x: (* @config.width 0.12), y: (* @config.height 0.3), width: (* @config.width 0.28), height: (* @config.height 0.6), divider: "none", level: (math/clamp (/ @entity.dose @config.capacity) 0 1), levelColor: @entity.bandColor, label: @config.reservoirLabel },
|
|
343
|
+
{ x: (* @config.width 0.58), y: (* @config.height 0.05), width: (* @config.width 0.06), height: (* @config.height 0.55), divider: "none", level: (math/clamp (/ (- @config.capacity @entity.dose) @config.capacity) 0 1), levelColor: @config.sourceColor, label: @config.sourceLabel }
|
|
344
|
+
])
|
|
345
|
+
(set @entity.readouts [
|
|
346
|
+
{ label: @config.quantityLabel, value: (str/concat "" (math/round @entity.dose 2)) },
|
|
347
|
+
{ label: @config.zoneReadoutLabel, value: @entity.bandLabel }
|
|
348
|
+
])
|
|
349
|
+
(set @entity.doseSeries [{ x: 0, y: @entity.dose }])
|
|
350
|
+
(set @entity.narration (str/concat @config.quantityLabel (str/concat " at " (str/concat (str/concat "" (math/round @entity.dose 2)) (str/concat " — " @entity.bandLabel)))))
|
|
351
|
+
(if @config.showCanvas
|
|
352
|
+
(render-ui main {
|
|
353
|
+
type: box
|
|
354
|
+
padding: md
|
|
355
|
+
children: [
|
|
356
|
+
{ type: stack, direction: vertical, gap: md, children: [
|
|
357
|
+
{ type: typography, variant: h4, content: @entity.title },
|
|
358
|
+
{ type: stack, direction: horizontal, gap: sm, children: [
|
|
359
|
+
{ type: button, action: ADD_SMALL, label: (str/concat "+ " (str/concat (str/concat "" @config.stepSmall) (if (== @config.unit "") "" (str/concat " " @config.unit)))), variant: primary },
|
|
360
|
+
{ type: button, action: ADD_LARGE, label: (str/concat "+ " (str/concat (str/concat "" @config.stepLarge) (if (== @config.unit "") "" (str/concat " " @config.unit)))), variant: secondary },
|
|
361
|
+
{ type: button, action: TOGGLE_DRIP, label: (if @entity.running "Pause" "Auto-dose"), variant: secondary },
|
|
362
|
+
{ type: button, action: RESET, label: "Reset", variant: ghost }
|
|
363
|
+
] },
|
|
364
|
+
{ type: chemistry-canvas, width: @config.width, height: @config.height, backgroundColor: @config.backgroundColor, containers: @entity.containers, readouts: @entity.readouts, traces: [ { series: [ { samples: @entity.doseSeries, label: @config.quantityLabel } ], xLabel: "step", yLabel: @config.quantityLabel } ] },
|
|
365
|
+
{ type: typography, variant: body, content: @entity.narration }
|
|
366
|
+
] }
|
|
367
|
+
]
|
|
368
|
+
})
|
|
369
|
+
(render-ui main {
|
|
370
|
+
type: box
|
|
371
|
+
padding: md
|
|
372
|
+
children: [
|
|
373
|
+
{ type: stack, direction: vertical, gap: md, children: [
|
|
374
|
+
{ type: typography, variant: h4, content: @entity.title },
|
|
375
|
+
{ type: stack, direction: horizontal, gap: sm, children: [
|
|
376
|
+
{ type: button, action: ADD_SMALL, label: (str/concat "+ " (str/concat (str/concat "" @config.stepSmall) (if (== @config.unit "") "" (str/concat " " @config.unit)))), variant: primary },
|
|
377
|
+
{ type: button, action: ADD_LARGE, label: (str/concat "+ " (str/concat (str/concat "" @config.stepLarge) (if (== @config.unit "") "" (str/concat " " @config.unit)))), variant: secondary },
|
|
378
|
+
{ type: button, action: TOGGLE_DRIP, label: (if @entity.running "Pause" "Auto-dose"), variant: secondary },
|
|
379
|
+
{ type: button, action: RESET, label: "Reset", variant: ghost }
|
|
380
|
+
] }
|
|
381
|
+
] }
|
|
382
|
+
]
|
|
383
|
+
}))
|
|
384
|
+
(emit DOSE_RESET { })
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
ticks {
|
|
388
|
+
dripTick every 32ms
|
|
389
|
+
when (and (== @entity.active true) (and (== @entity.running true) (== @entity.done false)))
|
|
390
|
+
(set @entity.elapsedMs (+ @entity.elapsedMs 32))
|
|
391
|
+
(when (>= @entity.elapsedMs @config.dripIntervalMs)
|
|
392
|
+
(do
|
|
393
|
+
(set @entity.elapsedMs 0)
|
|
394
|
+
(emit ADD_SMALL { })
|
|
395
|
+
))
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
listens {
|
|
399
|
+
MODE_SELECTED {
|
|
400
|
+
mode : string
|
|
401
|
+
}
|
|
402
|
+
@description "Lab router broadcast naming the active mode; this instrument renders when the mode equals matchMode, otherwise renders null."
|
|
403
|
+
@tier "domain"
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
emits {
|
|
407
|
+
DOSE_ADDED -> external {
|
|
408
|
+
dose : number
|
|
409
|
+
bandLabel : string
|
|
410
|
+
bandColor : string
|
|
411
|
+
done : boolean
|
|
412
|
+
}
|
|
413
|
+
@description "Fires after every dose increase — from the small/large add buttons or an auto-drip tick — with the accumulated dose and its classified band. A composed sibling trait transitions on it to recompute a domain-specific response (e.g. a chemistry pH curve) and repaint the scene it owns. A harmless no-op when the atom runs standalone."
|
|
414
|
+
@synonyms "dose added, increment, add, drip, titrant added, quantity added"
|
|
415
|
+
@tier "domain"
|
|
416
|
+
DOSE_RESET -> external {
|
|
417
|
+
}
|
|
418
|
+
@description "Fires on Reset after the dose, band, and done flag re-zero. A composed sibling trait reseeds its own domain state from config on it. A harmless no-op when the atom runs standalone."
|
|
419
|
+
@synonyms "reset, reseed, restart, clear"
|
|
420
|
+
@tier "domain"
|
|
421
|
+
DOSER_UPDATED -> external {
|
|
422
|
+
running : boolean
|
|
423
|
+
dose : number
|
|
424
|
+
bandLabel : string
|
|
425
|
+
done : boolean
|
|
426
|
+
}
|
|
427
|
+
@description "Fires on INIT/MODE_SELECTED/auto-drip toggle with the transport-level state — running flag, dose, current band, done — so a composed sibling trait can repaint its own shell. A harmless no-op when the atom runs standalone."
|
|
428
|
+
@synonyms "drip toggle, running state, auto-dose"
|
|
429
|
+
@tier "domain"
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
config {
|
|
433
|
+
matchMode : string = ""
|
|
434
|
+
@label "Match Mode"
|
|
435
|
+
@description "When composed behind a lab router, the MODE_SELECTED mode value this instrument responds to. Empty string means standalone — render immediately on INIT."
|
|
436
|
+
@tier "presentation"
|
|
437
|
+
title : string = "Doser"
|
|
438
|
+
@label "Title"
|
|
439
|
+
@description "Title shown above the instrument."
|
|
440
|
+
@tier "presentation"
|
|
441
|
+
width : number = 600
|
|
442
|
+
@label "Width"
|
|
443
|
+
@description "Canvas width in pixels."
|
|
444
|
+
@tier "presentation"
|
|
445
|
+
height : number = 400
|
|
446
|
+
@label "Height"
|
|
447
|
+
@description "Canvas height in pixels."
|
|
448
|
+
@tier "presentation"
|
|
449
|
+
backgroundColor : string = "#fafafa"
|
|
450
|
+
@label "Background Color"
|
|
451
|
+
@description "Canvas background color."
|
|
452
|
+
@tier "presentation"
|
|
453
|
+
showCanvas : boolean = true
|
|
454
|
+
@label "Show Canvas"
|
|
455
|
+
@description "When true (the default, standalone use) the instrument renders its own reservoir/source canvas and narration below the control strip. Set false when a domain sibling trait composes this atom via @rebindable and owns the accurate scene render itself — then only the control strip (add/drip/reset buttons) embeds via @trait.<name>, avoiding a duplicate canvas."
|
|
456
|
+
@tier "presentation"
|
|
457
|
+
unit : string = ""
|
|
458
|
+
@label "Unit"
|
|
459
|
+
@description "Unit suffix appended to the step-size button labels (e.g. \"mL\", \"mg\", \"units\"); empty means no unit shown."
|
|
460
|
+
@tier "domain"
|
|
461
|
+
quantityLabel : string = "Dose"
|
|
462
|
+
@label "Quantity Label"
|
|
463
|
+
@description "Label used for the accumulated-quantity readout and in the narration line."
|
|
464
|
+
@synonyms "quantity, amount, dose, volume, level"
|
|
465
|
+
@tier "domain"
|
|
466
|
+
zoneReadoutLabel : string = "Zone"
|
|
467
|
+
@label "Zone Readout Label"
|
|
468
|
+
@description "Label used for the current-band readout row."
|
|
469
|
+
@tier "presentation"
|
|
470
|
+
stepSmall : number = 1
|
|
471
|
+
@label "Small Step"
|
|
472
|
+
@description "Quantity added per press of the fine add button, and per auto-drip tick — for precise control near a band boundary."
|
|
473
|
+
@tier "domain"
|
|
474
|
+
stepLarge : number = 5
|
|
475
|
+
@label "Large Step"
|
|
476
|
+
@description "Quantity added per press of the coarse add button."
|
|
477
|
+
@tier "domain"
|
|
478
|
+
capacity : number = 100
|
|
479
|
+
@label "Capacity"
|
|
480
|
+
@description "Maximum total dose the source can deliver; the source vessel drains to empty and additions clamp once accumulated dose reaches this cap."
|
|
481
|
+
@tier "domain"
|
|
482
|
+
autoDrip : boolean = false
|
|
483
|
+
@label "Auto Drip"
|
|
484
|
+
@description "When true, INIT/MODE_SELECTED/Reset start the drip clock adding a small increment each interval; toggle live with the Auto-dose/Pause button."
|
|
485
|
+
@tier "domain"
|
|
486
|
+
dripIntervalMs : number = 500
|
|
487
|
+
@label "Drip Interval (ms)"
|
|
488
|
+
@description "Milliseconds of running clock per auto-drip increment — the 32ms base tick accumulates elapsed time and adds a small step each time this interval elapses."
|
|
489
|
+
@tier "domain"
|
|
490
|
+
historyLength : number = 300
|
|
491
|
+
@label "Dose History"
|
|
492
|
+
@description "Maximum points kept in the dose-history trace; older points drop off once exceeded."
|
|
493
|
+
@tier "domain"
|
|
494
|
+
bands : [DoserBand] = [
|
|
495
|
+
{ upperBound: 50, label: "Low", color: "#94a3b8", terminal: false },
|
|
496
|
+
{ upperBound: 999999999, label: "High", color: "#16a34a", terminal: true }
|
|
497
|
+
]
|
|
498
|
+
@label "Bands"
|
|
499
|
+
@description "Ordered ascending roster of dose bands/zones — each carries the dose upper bound it covers, a label, a color, and whether crossing into it is terminal (irreversible; sets done true until Reset). The accumulated dose is classified into the first band whose upperBound it does not exceed, falling through to the last band beyond all bounds."
|
|
500
|
+
@synonyms "zone, band, threshold, region, stage, phase, tier"
|
|
501
|
+
@tier "domain"
|
|
502
|
+
reservoirLabel : string = "Reservoir"
|
|
503
|
+
@label "Reservoir Label"
|
|
504
|
+
@description "Label for the vessel that fills as dose accumulates."
|
|
505
|
+
@synonyms "reservoir, beaker, tank, container, receiving vessel"
|
|
506
|
+
@tier "presentation"
|
|
507
|
+
sourceLabel : string = "Source"
|
|
508
|
+
@label "Source Label"
|
|
509
|
+
@description "Label for the vessel that drains as dose is delivered from it."
|
|
510
|
+
@synonyms "source, burette, supply, dispenser"
|
|
511
|
+
@tier "presentation"
|
|
512
|
+
sourceColor : string = "#60a5fa"
|
|
513
|
+
@label "Source Color"
|
|
514
|
+
@description "Fill color for the source vessel."
|
|
515
|
+
@tier "presentation"
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
page "/learn-doser" as LearnDoserPage -> LearnDoserSim
|
|
520
|
+
}
|