1use crate::foundation::signal::Signal;
8
9#[cfg(feature = "schema")]
10use schemars::JsonSchema;
11#[cfg(feature = "serde")]
12use serde::{Deserialize, Serialize};
13#[cfg(feature = "schema")]
14use std::{borrow::ToOwned, boxed::Box, vec};
15
16#[derive(Copy, Clone, Debug, PartialEq, Eq)]
21#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
22#[cfg_attr(feature = "schema", derive(JsonSchema))]
23pub enum NumericPath {
24 #[cfg_attr(feature = "serde", serde(rename = "f32"))]
26 F32,
27 #[cfg_attr(feature = "serde", serde(rename = "i32q16"))]
29 I32Q16,
30}
31
32#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
37#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
38#[cfg_attr(feature = "schema", derive(JsonSchema))]
39#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
40pub enum SignalDomain {
41 Bool,
43 Level,
45 Count,
47}
48
49impl SignalDomain {
50 pub const fn is_numeric(self) -> bool {
52 matches!(self, SignalDomain::Level | SignalDomain::Count)
53 }
54}
55
56impl NumericPath {
57 pub fn compiled() -> Self {
59 #[cfg(feature = "signal-f32")]
60 {
61 NumericPath::F32
62 }
63 #[cfg(feature = "signal-i32")]
64 {
65 NumericPath::I32Q16
66 }
67 }
68}
69
70#[derive(Copy, Clone, Debug, PartialEq, Eq)]
72#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
73#[cfg_attr(feature = "schema", derive(JsonSchema))]
74pub enum CompareOp {
75 Eq,
77 Ne,
79 Lt,
81 Lte,
83 Gt,
85 Gte,
87}
88
89impl CompareOp {
90 pub const fn supports_domain(self, domain: SignalDomain) -> bool {
95 !matches!(domain, SignalDomain::Bool) || matches!(self, CompareOp::Eq | CompareOp::Ne)
96 }
97}
98
99#[derive(Copy, Clone, Debug, PartialEq, Eq)]
101#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
102#[cfg_attr(feature = "schema", derive(JsonSchema))]
103pub enum TimerMode {
104 FedCountdown,
106 PulseHold,
108}
109
110#[derive(Copy, Clone, Debug, PartialEq, Eq)]
112#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
113#[cfg_attr(feature = "schema", derive(JsonSchema))]
114pub enum CalcOp {
115 Add,
117 Sub,
119 Mul,
121 Div,
123}
124
125#[derive(Copy, Clone, Debug, PartialEq, Eq)]
127#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
128#[cfg_attr(feature = "schema", derive(JsonSchema))]
129pub enum FlagPriority {
130 ResetWins,
132 SetWins,
134}
135
136#[derive(Clone, Debug, PartialEq)]
141#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
142#[cfg_attr(feature = "schema", derive(JsonSchema))]
143pub enum KnotKind {
144 Constant {
146 domain: SignalDomain,
148 value: Signal,
150 },
151 SignalIn {
153 domain: SignalDomain,
155 },
156 OnStart,
158 Not,
160 And {
162 arity: u8,
164 },
165 Or {
167 arity: u8,
169 },
170 Compare {
172 domain: SignalDomain,
174 op: CompareOp,
176 rhs_const: Option<Signal>,
178 },
179 RisingFromZero,
181 Flag {
183 priority: FlagPriority,
185 enable_toggle: bool,
187 },
188 Counter,
190 Timer {
192 mode: TimerMode,
194 ticks: u16,
196 },
197 Delay {
199 ticks: u16,
201 },
202 Calc {
204 domain: SignalDomain,
206 op: CalcOp,
208 },
209 Map {
211 domain: SignalDomain,
213 in_min: Signal,
215 in_max: Signal,
217 out_min: Signal,
219 out_max: Signal,
221 },
222 Abs {
224 domain: SignalDomain,
226 },
227 Neg {
229 domain: SignalDomain,
231 },
232 Select,
234 Digitize {
236 domain: SignalDomain,
238 steps: u16,
240 in_min: Signal,
242 in_max: Signal,
244 out_min: Signal,
246 out_max: Signal,
248 },
249 Threshold {
251 domain: SignalDomain,
253 high: Signal,
255 low: Signal,
257 use_hysteresis: bool,
259 },
260 Random {
262 domain: SignalDomain,
264 require_gate: bool,
266 },
267 Sqrt {
269 domain: SignalDomain,
271 },
272 Xor,
274 FallingToZero,
276 Change,
278 Clamp {
280 domain: SignalDomain,
282 min: Signal,
284 max: Signal,
286 },
287 Convert {
289 from: SignalDomain,
291 to: SignalDomain,
293 },
294 SignalOut {
296 path: std::string::String,
298 domain: SignalDomain,
300 },
301 EmitCommand {
303 name: std::string::String,
305 },
306}
307
308impl KnotKind {
309 pub fn and2() -> Self {
311 KnotKind::And { arity: 2 }
312 }
313
314 pub fn or2() -> Self {
316 KnotKind::Or { arity: 2 }
317 }
318
319 pub fn not() -> Self {
321 KnotKind::Not
322 }
323
324 pub fn signal_in(domain: SignalDomain) -> Self {
326 KnotKind::SignalIn { domain }
327 }
328
329 pub fn constant(value: Signal, domain: SignalDomain) -> Self {
331 KnotKind::Constant { domain, value }
332 }
333
334 pub fn constant_count(n: i32) -> Self {
336 KnotKind::Constant {
337 domain: SignalDomain::Count,
338 value: crate::foundation::signal::from_count(n),
339 }
340 }
341
342 pub fn constant_bool(value: bool) -> Self {
344 KnotKind::Constant {
345 domain: SignalDomain::Bool,
346 value: if value {
347 crate::foundation::signal::ONE
348 } else {
349 crate::foundation::signal::ZERO
350 },
351 }
352 }
353
354 pub fn constant_level(value: f32) -> Self {
356 KnotKind::Constant {
357 domain: SignalDomain::Level,
358 value: crate::foundation::signal::from_level(value),
359 }
360 }
361
362 pub fn signal_out(path: impl Into<std::string::String>, domain: SignalDomain) -> Self {
364 KnotKind::SignalOut {
365 path: path.into(),
366 domain,
367 }
368 }
369
370 pub fn emit_command(name: impl Into<std::string::String>) -> Self {
372 KnotKind::EmitCommand { name: name.into() }
373 }
374
375 pub fn rising_from_zero() -> Self {
377 KnotKind::RisingFromZero
378 }
379
380 pub fn compare(op: CompareOp, rhs_const: Option<Signal>, domain: SignalDomain) -> Self {
382 KnotKind::Compare {
383 domain,
384 op,
385 rhs_const,
386 }
387 }
388
389 pub fn counter() -> Self {
391 KnotKind::Counter
392 }
393
394 pub fn timer(mode: TimerMode, ticks: u16) -> Self {
396 KnotKind::Timer { mode, ticks }
397 }
398
399 pub fn flag(priority: FlagPriority, enable_toggle: bool) -> Self {
401 KnotKind::Flag {
402 priority,
403 enable_toggle,
404 }
405 }
406
407 pub fn select() -> Self {
409 KnotKind::Select
410 }
411
412 pub fn calc(op: CalcOp, domain: SignalDomain) -> Self {
414 KnotKind::Calc { domain, op }
415 }
416
417 pub fn map(
419 in_min: Signal,
420 in_max: Signal,
421 out_min: Signal,
422 out_max: Signal,
423 domain: SignalDomain,
424 ) -> Self {
425 KnotKind::Map {
426 domain,
427 in_min,
428 in_max,
429 out_min,
430 out_max,
431 }
432 }
433
434 pub fn abs(domain: SignalDomain) -> Self {
436 KnotKind::Abs { domain }
437 }
438
439 pub fn neg(domain: SignalDomain) -> Self {
441 KnotKind::Neg { domain }
442 }
443
444 pub fn digitize(steps: u16, domain: SignalDomain) -> Self {
446 KnotKind::Digitize {
447 domain,
448 steps: steps.max(1),
449 in_min: crate::foundation::signal::ZERO,
450 in_max: crate::foundation::signal::ONE,
451 out_min: crate::foundation::signal::ZERO,
452 out_max: crate::foundation::signal::ONE,
453 }
454 }
455
456 pub fn threshold_default(domain: SignalDomain) -> Self {
458 if domain == SignalDomain::Count {
459 return KnotKind::Threshold {
460 domain,
461 high: crate::foundation::signal::from_count(1),
462 low: crate::foundation::signal::from_count(0),
463 use_hysteresis: true,
464 };
465 }
466 #[cfg(feature = "signal-f32")]
467 {
468 KnotKind::Threshold {
469 domain,
470 high: 0.5,
471 low: 0.4,
472 use_hysteresis: true,
473 }
474 }
475 #[cfg(feature = "signal-i32")]
476 {
477 let one = crate::foundation::signal::ONE;
478 KnotKind::Threshold {
479 domain,
480 high: one / 2,
481 low: one * 2 / 5, use_hysteresis: true,
483 }
484 }
485 }
486
487 pub fn random(require_gate: bool, domain: SignalDomain) -> Self {
489 KnotKind::Random {
490 domain,
491 require_gate,
492 }
493 }
494
495 pub fn sqrt(domain: SignalDomain) -> Self {
497 KnotKind::Sqrt { domain }
498 }
499
500 pub fn xor() -> Self {
502 KnotKind::Xor
503 }
504
505 pub fn falling_to_zero() -> Self {
507 KnotKind::FallingToZero
508 }
509
510 pub fn change() -> Self {
512 KnotKind::Change
513 }
514
515 pub fn clamp(min: Signal, max: Signal, domain: SignalDomain) -> Self {
517 KnotKind::Clamp { domain, min, max }
518 }
519
520 pub fn convert(from: SignalDomain, to: SignalDomain) -> Self {
522 KnotKind::Convert { from, to }
523 }
524
525 pub fn has_valid_domains(&self) -> bool {
527 match self {
528 KnotKind::Compare { domain, op, .. } => op.supports_domain(*domain),
529 KnotKind::Calc { domain, .. }
530 | KnotKind::Map { domain, .. }
531 | KnotKind::Abs { domain }
532 | KnotKind::Neg { domain }
533 | KnotKind::Digitize { domain, .. }
534 | KnotKind::Threshold { domain, .. }
535 | KnotKind::Random { domain, .. }
536 | KnotKind::Sqrt { domain }
537 | KnotKind::Clamp { domain, .. } => domain.is_numeric(),
538 KnotKind::Convert { from, to } => from != to,
539 _ => true,
540 }
541 }
542
543 pub fn arity(&self) -> Option<u8> {
545 match self {
546 KnotKind::And { arity } => Some(*arity),
547 KnotKind::Or { arity } => Some(*arity),
548 _ => None,
549 }
550 }
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556 use crate::foundation::signal::{from_count, from_level, ONE, ZERO};
557
558 #[test]
559 fn helpers_and_arity() {
560 assert!(matches!(KnotKind::or2(), KnotKind::Or { arity: 2 }));
561 assert!(matches!(KnotKind::not(), KnotKind::Not));
562 assert!(matches!(
563 KnotKind::constant_count(7),
564 KnotKind::Constant { value, .. } if value == from_count(7)
565 ));
566 assert!(matches!(
567 KnotKind::constant_bool(false),
568 KnotKind::Constant {
569 domain: SignalDomain::Bool,
570 value: ZERO,
571 }
572 ));
573 assert!(matches!(
574 KnotKind::constant_level(0.25),
575 KnotKind::Constant {
576 domain: SignalDomain::Level,
577 value,
578 } if value == from_level(0.25)
579 ));
580 assert!(matches!(
581 KnotKind::emit_command("go"),
582 KnotKind::EmitCommand { name } if name == "go"
583 ));
584 assert_eq!(KnotKind::and2().arity(), Some(2));
585 assert_eq!(KnotKind::or2().arity(), Some(2));
586 assert_eq!(KnotKind::not().arity(), None);
587 assert_eq!(NumericPath::compiled(), NumericPath::compiled());
588 let _ = ONE;
589 let _ = KnotKind::signal_in(SignalDomain::Bool);
590 let _ = KnotKind::signal_out("p", SignalDomain::Bool);
591 let _ = KnotKind::rising_from_zero();
592 let _ = KnotKind::compare(CompareOp::Eq, None, SignalDomain::Bool);
593 let _ = KnotKind::counter();
594 let _ = KnotKind::timer(TimerMode::PulseHold, 1);
595 let _ = KnotKind::flag(FlagPriority::SetWins, false);
596 let _ = KnotKind::constant(ONE, SignalDomain::Bool);
597 let _ = KnotKind::select();
598 let _ = KnotKind::calc(CalcOp::Add, SignalDomain::Count);
599 let _ = KnotKind::map(crate::ZERO, ONE, crate::ZERO, ONE, SignalDomain::Level);
600 let _ = KnotKind::abs(SignalDomain::Level);
601 let _ = KnotKind::neg(SignalDomain::Count);
602 let _ = KnotKind::digitize(4, SignalDomain::Level);
603 let _ = KnotKind::threshold_default(SignalDomain::Level);
604 let _ = KnotKind::random(false, SignalDomain::Count);
605 let _ = KnotKind::sqrt(SignalDomain::Count);
606 let _ = KnotKind::xor();
607 let _ = KnotKind::falling_to_zero();
608 let _ = KnotKind::change();
609 let _ = KnotKind::clamp(crate::ZERO, ONE, SignalDomain::Level);
610 let _ = KnotKind::convert(SignalDomain::Count, SignalDomain::Level);
611 }
612
613 #[test]
614 fn domain_legality_is_catalog_owned() {
615 assert!(KnotKind::compare(CompareOp::Eq, None, SignalDomain::Bool).has_valid_domains());
616 assert!(!KnotKind::compare(CompareOp::Lt, None, SignalDomain::Bool).has_valid_domains());
617 assert!(KnotKind::calc(CalcOp::Mul, SignalDomain::Count).has_valid_domains());
618 assert!(!KnotKind::calc(CalcOp::Mul, SignalDomain::Bool).has_valid_domains());
619 assert!(KnotKind::convert(SignalDomain::Bool, SignalDomain::Level).has_valid_domains());
620 assert!(!KnotKind::convert(SignalDomain::Bool, SignalDomain::Bool).has_valid_domains());
621 assert!(KnotKind::signal_in(SignalDomain::Bool).has_valid_domains());
622
623 let numeric_kinds = [
624 KnotKind::map(ZERO, ONE, ZERO, ONE, SignalDomain::Level),
625 KnotKind::abs(SignalDomain::Level),
626 KnotKind::neg(SignalDomain::Count),
627 KnotKind::digitize(2, SignalDomain::Level),
628 KnotKind::threshold_default(SignalDomain::Level),
629 KnotKind::random(false, SignalDomain::Count),
630 KnotKind::sqrt(SignalDomain::Level),
631 KnotKind::clamp(ZERO, ONE, SignalDomain::Count),
632 ];
633 assert!(numeric_kinds.iter().all(KnotKind::has_valid_domains));
634 }
635}