@pauloricca/spagbol 0.1.1 → 0.1.3
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 +33 -3
- package/dist/core/controller.js +4 -4
- package/dist/core/controller.js.map +1 -1
- package/dist/core/model.d.ts +2 -1
- package/dist/core/model.d.ts.map +1 -1
- package/dist/core/model.js.map +1 -1
- package/dist/editors.css +157 -0
- package/dist/react/SpagbolCanvas.d.ts.map +1 -1
- package/dist/react/SpagbolCanvas.js +53 -14
- package/dist/react/SpagbolCanvas.js.map +1 -1
- package/dist/react/SpagbolCanvasControls.d.ts.map +1 -1
- package/dist/react/SpagbolCanvasControls.js +5 -5
- package/dist/react/SpagbolCanvasControls.js.map +1 -1
- package/dist/react/SpagbolControlJoystick.d.ts +9 -0
- package/dist/react/SpagbolControlJoystick.d.ts.map +1 -0
- package/dist/react/SpagbolControlJoystick.js +111 -0
- package/dist/react/SpagbolControlJoystick.js.map +1 -0
- package/dist/react/SpagbolControlZoom.d.ts.map +1 -1
- package/dist/react/SpagbolControlZoom.js +3 -2
- package/dist/react/SpagbolControlZoom.js.map +1 -1
- package/dist/react/SpagbolCurveEditor.d.ts +45 -0
- package/dist/react/SpagbolCurveEditor.d.ts.map +1 -0
- package/dist/react/SpagbolCurveEditor.js +142 -0
- package/dist/react/SpagbolCurveEditor.js.map +1 -0
- package/dist/react/SpagbolNodeFrame.d.ts.map +1 -1
- package/dist/react/SpagbolNodeFrame.js +5 -5
- package/dist/react/SpagbolNodeFrame.js.map +1 -1
- package/dist/react/SpagbolNodeFrameTypePicker.js +1 -1
- package/dist/react/SpagbolNodeFrameTypePicker.js.map +1 -1
- package/dist/react/SpagbolSequencer.d.ts +35 -0
- package/dist/react/SpagbolSequencer.d.ts.map +1 -0
- package/dist/react/SpagbolSequencer.js +166 -0
- package/dist/react/SpagbolSequencer.js.map +1 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +3 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/zoom.d.ts +5 -0
- package/dist/react/zoom.d.ts.map +1 -0
- package/dist/react/zoom.js +19 -0
- package/dist/react/zoom.js.map +1 -0
- package/dist/styles.css +112 -41
- package/docs/language/standard-node-comparison.md +92 -0
- package/docs/language/standard-node-contracts.md +237 -0
- package/docs/language/standard-nodes.md +198 -0
- package/docs/language/ui-components.md +79 -0
- package/package.json +5 -3
- package/src/core/controller.ts +4 -4
- package/src/core/model.ts +2 -1
- package/src/react/SpagbolCanvas.tsx +74 -18
- package/src/react/SpagbolCanvasControls.tsx +2 -7
- package/src/react/SpagbolControlJoystick.tsx +147 -0
- package/src/react/SpagbolControlZoom.tsx +3 -2
- package/src/react/SpagbolCurveEditor.tsx +166 -0
- package/src/react/SpagbolNodeFrame.tsx +18 -13
- package/src/react/SpagbolNodeFrameTypePicker.tsx +1 -1
- package/src/react/SpagbolSequencer.tsx +265 -0
- package/src/react/editors.css +157 -0
- package/src/react/index.ts +4 -0
- package/src/react/styles.css +112 -41
- package/src/react/zoom.ts +18 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# Recommended standard node contracts
|
|
2
|
+
|
|
3
|
+
**Status:** Proposed baseline 0.4, 2026-11-09. **Scope:** 26 optional vocabulary contracts, not a runtime API or a claim of runtime implementation. Envelope, Playhead and Time are app-specific. See the [proposal](standard-nodes.md) and [source comparison](standard-node-comparison.md).
|
|
4
|
+
|
|
5
|
+
Phases 1–3 establish recommendations and review examples. They do not add catalogue entries, rename existing pins, change saved patches or mandate one execution engine. Applications can omit nodes and publish explicit execution profiles or extensions. The six semantic anchors are Expression, Selector, Conditional Execution, Spread, Spawn and Accumulator. The other twenty contracts complete the vocabulary. Phase 4 provides controlled Curve/Sequencer/Joystick UI; applications retain their own data layouts and edit operations. See [UI components](ui-components.md). Executable evaluator conformance remains future application/tooling work.
|
|
6
|
+
|
|
7
|
+
## Shared conventions
|
|
8
|
+
|
|
9
|
+
### Pins, configuration and naming
|
|
10
|
+
|
|
11
|
+
Every connection carries one numeric float signal, including triggers, gates and link-weight modulation. Objects, strings, textures, asset references, expression source and editor structures are configuration, never values carried by links. Numeric parameters remain float pins unless explicitly described as configuration. A discrete numeric choice can be a float pin with a documented conversion; a mode stored as configuration is not modulatable.
|
|
12
|
+
|
|
13
|
+
Names in backticks below are recommended **display labels**, in lowercase English with spaces. Stable serialized IDs are separate from labels and need not change when labels change. Dynamic inputs use labels such as `value 1`; Sequence outputs use `row 1`. Expression symbol bindings map source identifiers to independently named ports (for example `left_value` to `left value`). Do not rename existing app IDs or serialize these suggestions as a new patch format without a migration.
|
|
14
|
+
|
|
15
|
+
Signatures list `input = default -> output`; unspecified output units match the input. Dynamic data-input defaults are 0. A signature describes effective node inputs after the application's existing weighted-link resolution. A Multiply node does not replace or reinterpret Multiply link mode. These contracts do not redefine link arithmetic, reduction order or feedback delay; an execution profile must specify them.
|
|
16
|
+
|
|
17
|
+
### Numeric policy
|
|
18
|
+
|
|
19
|
+
The proposed portable baseline replaces a non-finite effective input with that input's default (0 if none), and replaces non-finite arithmetic results with 0. Finite values are not globally clipped. Invalid configuration produces a diagnostic and prevents that node/scope from executing; it is not silently accepted. Configuration numbers must be finite JSON numbers. Overflow must not poison persistent state. Runtime-specific epsilon guards, saturation or raw GPU undefined results are deviations to document, not implied by this baseline.
|
|
20
|
+
|
|
21
|
+
Comparisons in examples are exact for integers and logical states. For finite arithmetic, a future conformance runner should accept absolute error `1e-6` plus relative error `1e-5 * abs(expected)`; profiles may document precision limits and narrower valid ranges. Examples here are specification vectors, not executed tests of either application.
|
|
22
|
+
|
|
23
|
+
### Evaluation, state and triggers
|
|
24
|
+
|
|
25
|
+
An evaluation is one logical tick with duration `delta time` in seconds supplied by the application; it is not a graph pin. Pure nodes have no state. Stateful nodes update **once** per tick per instance even with multiple consumers. On-demand runtimes must distinguish reading an output from advancing a tick. A GPU evaluation may be per pixel; any per-pixel state ownership must be explicit. A trigger is high at `>= 0.5`, low below it. A rising edge is low-to-high; initial previous level is low, so an initially high signal fires on its first executed tick. Gate means level, not edge. A pulse lasts one executed tick unless an application specifies an event-latching profile for different producer/consumer rates.
|
|
26
|
+
|
|
27
|
+
State belongs to `(graph instance path, execution instance identity, node ID)`, never just a shared graph definition. New instances start clean; deletion disposes their mutable resources. Unchanged identities should preserve state across edits where supported; incompatible node changes reset affected state with a documented policy. Shared immutable assets are allowed. Saving graph data does not implicitly snapshot running state. Stopping, seeking, recompilation and reload policies are execution-profile responsibilities.
|
|
28
|
+
|
|
29
|
+
### Scope and lifecycle profile requirements
|
|
30
|
+
|
|
31
|
+
Use **areas** for the umbrella and **Container** for a plain visual area. Spread, Spawn and Conditional Execution are **execution areas**; a Container has no execution effect. Group remains a nested-graph construct. See [area kinds](standard-nodes.md#areas-and-their-kinds).
|
|
32
|
+
|
|
33
|
+
Spread, Spawn and Conditional Execution describe logical scopes, independent of their visual representation as areas or nested graphs. Input sources outside a scope continue normally. Internal-only ports cannot connect outside their owning scope. A scope output means each outgoing boundary contribution, even where an app renders no explicit output pin. Link modes still apply at the boundary.
|
|
34
|
+
|
|
35
|
+
An application must specify tick/block sampling, ordering, boundary reduction (including zero instances), feedback latency, state migration, membership, supported nesting/overlap, and resource limits. Unsupported combinations must be diagnosed before execution. For the portable examples below use ordered evaluations, no feedback, Add boundary reduction with empty sum 0, and stable IDs. Other link modes need profile-specific fixtures; no generic empty-product or last-writer rule is assumed here.
|
|
36
|
+
|
|
37
|
+
Resource allocation failure must yield an explicit diagnostic rather than silent partial creation; a bounded runtime may instead publish a capacity/drop policy as a named deviation. Scope inactivity does not imply destroying scene entities. Persistent objects may be retained with inactive visibility/update status; rendering, audio and external side effects must obey the scope's execution gate.
|
|
38
|
+
|
|
39
|
+
## Six semantic anchors
|
|
40
|
+
|
|
41
|
+
### Expression
|
|
42
|
+
|
|
43
|
+
**Purpose/signature:** evaluate a numeric formula. Dynamic named float inputs default to 0; output `value`. No clock or persistent evaluation state.
|
|
44
|
+
|
|
45
|
+
**Configuration:** expression source (default `0`), stable symbol-to-port bindings and declared dialect/version. The portable subset is finite decimal/scientific literals, parentheses, identifiers, unary `+`/`-`, binary `+`, `-`, `*`, `/`, and `abs`, `min`, `max`, `clamp`. Calls have arity 1, 2, 2 and 3 respectively. Precedence is calls/parentheses, unary, multiplication/division, addition/subtraction; binary operators associate left. Names are case-sensitive. `clamp` sorts its two bounds as in Clamp. Division by exactly zero returns 0; sanitize each intermediate result using the numeric policy. Evaluation has no side effects and cannot create a scope.
|
|
46
|
+
|
|
47
|
+
**Errors/extensions:** empty source acts as `0`. Unknown symbols, invalid syntax, wrong arity or duplicate bindings produce a compilation diagnostic. Ports and bindings must agree; deleting a bound input requires an explicit edit, not unnoticed rebinding by position. An app may infer bindings from source, but must keep IDs stable across harmless edits. Trigonometry, comparisons, logical operators, power, constants and GPU syntax are declared dialect extensions. Specify radians/units and boolean-to-float conversion when adding them. A numeric branch does not promise short-circuit execution of upstream nodes.
|
|
48
|
+
|
|
49
|
+
**Examples:** `left + right * 2`, inputs 3 and 4 → 11; `(left + right) * 2` → 14; `1 / 0` → 0; `clamp(-2, 1, -1)` → -1; `unknown(1)` → diagnostic. Expression text/bindings remain configuration; no shared specialized editor format is required.
|
|
50
|
+
|
|
51
|
+
### Selector
|
|
52
|
+
|
|
53
|
+
**Purpose/signature:** `select = 0`, `slide = 0` seconds, dynamic `value 1` … `value n` (at least one) → `signal`. Choose a live input, retaining the previous selection between selection pulses.
|
|
54
|
+
|
|
55
|
+
**Behaviour/state:** floor `select`. Valid indices are one-based `1..n`; zero, negative and out-of-range values retain the previous selection. Initially no selection exists and output is 0. A first valid selection immediately outputs that input. Subsequent index changes capture the current output as a transition start. With `slide <= 0` switch immediately; otherwise on the switch tick elapsed is 0, and on later ticks it advances by `delta time`, with output `start + (current selected input - start) * min(elapsed / slide, 1)`. The selected input stays live; slide is temporal, not interpolation between adjacent indices. A new change during a slide starts from the current output. Changing slide changes the current transition duration; reaching 0 completes it immediately.
|
|
56
|
+
|
|
57
|
+
**Configuration/edges/extensions:** stable ordered input IDs; negative slide becomes 0. Removing the selected port clears selection and outputs 0; reordering preserves selected port identity, while future numeric indices follow the new order. Each scope instance owns selection and transition state. An app can offer clamped, wrapped, non-latching or adjacent-input interpolation modes, but must name their different behaviour. No required shared specialized editor data.
|
|
58
|
+
|
|
59
|
+
**Examples:** values `[10, 20]`, slide 0, selections `[0, 2.9, 0, 99, 1]` → `[0, 20, 20, 20, 10]`. After selecting 10, switch to 20 with slide 1 second → 10 on switch tick, 15 after 0.5 seconds, 20 after 1 second. Selecting an input does not suppress computation of the others; use Conditional Execution for that.
|
|
60
|
+
|
|
61
|
+
### Conditional Execution
|
|
62
|
+
|
|
63
|
+
**Purpose/signature:** `gate = 0` → no fixed public output; enables one contained scope.
|
|
64
|
+
|
|
65
|
+
**Behaviour/state:** high gate executes the scope once on that tick. Low gate skips all contained updates and side effects, freezes local state and makes every outgoing boundary contribution zero, including Multiply-mode contributions. It does not merely stop reading results. Gate must originate outside the controlled scope; a dependency cycle through its own gated work is invalid. First opening creates clean state if not already allocated. Closing retains resources; reopening resumes them. Deletion disposes resources. Skipped ticks do not accumulate local elapsed time, and contained edge detectors retain their previous executed levels.
|
|
66
|
+
|
|
67
|
+
**Configuration/edges/extensions:** scope membership and boundary wiring are configuration. Nested/overlapping scope support must be explicit; rejection is allowed. No automatic fade, tail draining, note-off, reset or resource destruction on close. A retained scene entity must cease active rendering/side effects while closed according to the app profile. Reset-on-open and destroy-on-close are named extensions. No dedicated editor data beyond membership is standardized here.
|
|
68
|
+
|
|
69
|
+
**Examples:** a contained Accumulator in continuous mode, range 0..10, increment 1, gates `[1, 0, 0, 1]` → boundary outputs `[1, 0, 0, 2]`; internal values `[1, 1, 1, 2]`. An external counter advances on all four ticks. A contained sender emits nothing on the two closed ticks. A trigger held high across close/reopen does not retrigger an already-high local edge detector.
|
|
70
|
+
|
|
71
|
+
### Spread
|
|
72
|
+
|
|
73
|
+
**Purpose/signature:** external `count = 1` → internal-only `item index`. Evaluate an indexed set of independently stateful scope instances.
|
|
74
|
+
|
|
75
|
+
**Behaviour/state:** `n = max(0, floor(count))`. Active indices are one-based `1..n`, evaluated in increasing order. Zero count performs no contained work. Inputs are broadcast; boundary outputs contribute in item order using the profile's link semantics. Instance identity is the stable Spread ID plus index, independent of screen location or count changes. Increasing count allocates new indices cleanly; decreasing count suspends removed indices and retains their state/resources until the Spread is deleted. Regrowth resumes them; suspended indices do not advance clocks. Apps with disposal-on-shrink must document that deviation and its memory policy.
|
|
76
|
+
|
|
77
|
+
**Configuration/edges/extensions:** membership and wiring are configuration. No universal maximum is imposed; capacity and sampling cadence must be declared. Non-finite count uses default 1 under the numeric policy, not an unbounded allocation. Fractional and negative counts are handled before allocation. Nested scope support is profile-dependent. Counts must not recreate existing scene objects every tick. No specialized shared editor format is mandated.
|
|
78
|
+
|
|
79
|
+
**Examples:** counts `[2.9, 0, 1, 2]` expose indices `[[1, 2], [], [1], [1, 2]]`. A continuous Accumulator in each item (0..10, increment 1) produces Add boundary sums `[2, 0, 2, 5]`: item 1 progresses 1→2→3, item 2 resumes at 2. The zero-count scope sends no external events.
|
|
80
|
+
|
|
81
|
+
### Spawn
|
|
82
|
+
|
|
83
|
+
**Purpose/signature:** external `trigger = 0`, `release trigger = 0`; internal-only `kill trigger = 0` → internal-only `instance gate`. Create independently lived instances; this is not a value-distribution node.
|
|
84
|
+
|
|
85
|
+
**Behaviour/state:** a trigger rising edge creates one fresh instance tagged with that tick's numeric trigger value. Creation order defines stable monotonic identities and evaluation order; tags are not identities and duplicates are allowed. New `instance gate` is 1. A release rising edge sets the gate to 0 on every live instance with an exactly equal tag. Release does not destroy or stop evaluation. Each instance has its own kill edge detector; a kill rising edge destroys only that instance and its mutable resources. Immutable assets and external input sources can be shared.
|
|
86
|
+
|
|
87
|
+
**Tick order:** sample external lifecycle inputs; create; apply releases (including to a just-created matching tag); evaluate live instances in creation order; collect their boundary contributions; then dispose instances with kill edges. Thus a killed instance contributes on its final tick. New high kill signals can kill immediately after that first evaluation. A trigger held high does not repeatedly spawn, and changing its positive tag without dropping below threshold does not create another instance. With no instances, there is no contained work; boundary empty reduction follows the profile.
|
|
88
|
+
|
|
89
|
+
**Configuration/edges/extensions:** membership/wiring are configuration. A tag below 0.5 cannot trigger; zero is not an event tag in this baseline. Equality is exact, not epsilon matching. Retriggering never resets existing instances. Release-before-create, pre-collection kill, voice stealing, capacity drops and external kill-all are profile extensions/deviations to document. No built-in timeout; applications must expose capacity policy and dispose on scope deletion. Shared specialized editor data is not required.
|
|
90
|
+
|
|
91
|
+
**Examples:** external trigger `[7, 0, 7, 0]`, release `[0, 0, 0, 7]` yields live counts `[1, 1, 2, 2]` and gates `[[1], [1], [1, 1], [0, 0]]`. A later local kill on the first instance leaves the second alive with gate 0. Trigger/release 7 on the same tick creates an instance whose first gate is 0. Two independent Accumulators prove overlapping instances do not share state.
|
|
92
|
+
|
|
93
|
+
### Accumulator
|
|
94
|
+
|
|
95
|
+
**Purpose/signature:** `trigger = 0`, `reset = 0`, `increment = 1`, `min = 0`, `max = 1` → `signal`. Maintain one numeric value per instance. Configuration `mode` is `trigger` (default) or `continuous`; it is not a numeric mode pin in the baseline.
|
|
96
|
+
|
|
97
|
+
**Behaviour/state:** sort bounds into low/high. Initialize value to low; on each tick clamp stored value into current bounds. A reset rising edge assigns low and suppresses increment on that tick. Otherwise increment on a trigger rising edge, or every tick in continuous mode. Add increment once; if the result exceeds high assign low, if below low assign high; equal endpoints are retained. Overshoot is discarded, not modulo-wrapped. Output is the resulting value. Record both input levels even on reset ticks. Continuous increment is per tick, not per second.
|
|
98
|
+
|
|
99
|
+
**Edges/extensions:** equal bounds always output that bound. Finite addition overflow resets value to 0 under numeric policy, then clamps it to the bounds. Negative increments are valid. Bounds changes alone clamp; they do not count as trigger/reset. Trigger and reset initially high produce only a reset. Each scope owns value and edge history. Clamp/modulo/unbounded/integration modes must be named extensions. No required shared editor structure.
|
|
100
|
+
|
|
101
|
+
**Examples:** bounds 0..2, increment 1, trigger `[0, 1, 1, 0, 1, 0, 1]` → `[0, 1, 1, 1, 2, 2, 0]`. From 2, increment 4 → 0; from 0, increment -1 → 2. Simultaneous reset/trigger rise → 0 and a held trigger on the next tick does not advance. Continuous mode for three ticks → `[1, 2, 0]`.
|
|
102
|
+
|
|
103
|
+
## Remaining twenty contracts
|
|
104
|
+
|
|
105
|
+
These pure maths nodes have no non-pin configuration, clocks, instance state or specialized shared editor data. They accept extra named modes only as documented extensions. The shared numeric policy applies to all examples and dynamic modulation.
|
|
106
|
+
|
|
107
|
+
### Constant
|
|
108
|
+
|
|
109
|
+
**Purpose/signature:** `value = 1` → `signal`; output the effective value, including modulation, on every evaluation. No implicit sample-and-hold. **Edges:** non-finite value falls back to 1. **Extensions:** UI presets or display formatting do not change the numeric contract. **Examples:** value 3 → 3; modulated values `[2, -1]` → `[2, -1]`.
|
|
110
|
+
|
|
111
|
+
### Pass
|
|
112
|
+
|
|
113
|
+
**Purpose/signature:** `signal = 0` → `signal`; forward the effective input unchanged. Observation/UI does not delay or filter it. **Edges:** non-finite input becomes 0. **Extensions:** metering is allowed if it does not change the signal. **Examples:** -2 → -2; no input → 0.
|
|
114
|
+
|
|
115
|
+
### Abs
|
|
116
|
+
|
|
117
|
+
**Purpose/signature:** `signal = 0` → `signal`; `abs(signal)`. **Edges:** signed zero may normalize to 0; non-finite input becomes 0. **Extensions:** display only. **Examples:** -3 → 3; 2 → 2.
|
|
118
|
+
|
|
119
|
+
### Add
|
|
120
|
+
|
|
121
|
+
**Purpose/signature:** `signal = 0`, `amount = 0` → `signal`; `signal + amount`. Pure arithmetic; no configuration or state. **Edges/extensions:** overflow follows the shared result policy; additional summands must be explicit pins. **Examples:** 3 + -2 → 1; unconnected → 0. This node is independent of Add link mode.
|
|
122
|
+
|
|
123
|
+
### Subtract
|
|
124
|
+
|
|
125
|
+
**Purpose/signature:** `signal = 0`, `amount = 0` → `signal`; `signal - amount`. Pure arithmetic; no configuration or state. **Edges/extensions:** operand order is fixed, and overflow follows shared policy. **Examples:** 3 - 5 → -2; -2 - -4 → 2.
|
|
126
|
+
|
|
127
|
+
### Divide
|
|
128
|
+
|
|
129
|
+
**Purpose/signature:** `signal = 0`, `divisor = 1` → `signal`; `signal / divisor`. Pure arithmetic; no configuration or state. **Edges/extensions:** exactly zero divisor (including negative zero) returns 0. Tiny nonzero divisors use the actual value, with non-finite results replaced by 0. An epsilon guard is a named deviation. **Examples:** 6 / -2 → -3; 1 / 0 → 0; 0 / 0 → 0; 1 / 0.000001 → 1000000.
|
|
130
|
+
|
|
131
|
+
### Modulo
|
|
132
|
+
|
|
133
|
+
**Purpose/signature:** `signal = 0`, `divisor = 1` → `signal`; `signal - divisor * floor(signal / divisor)`. Pure float arithmetic; no configuration or state. This is floor modulo, not truncating remainder. **Edges/extensions:** exactly zero divisor returns 0; intermediate non-finite arithmetic returns 0 for the operation. For a positive divisor, ordinary finite results lie in `[0, divisor)`; a negative divisor gives results in `(divisor, 0]`. Near boundaries, finite precision can affect the endpoint; no integer-only conversion is implied. **Examples:** 5.5 modulo 2 → 1.5; -0.25 modulo 1 → 0.75; 5 modulo -2 → -1; 4 modulo 2 → 0; modulo zero → 0.
|
|
134
|
+
|
|
135
|
+
### Min
|
|
136
|
+
|
|
137
|
+
**Purpose/signature:** `signal = 0`, `other = 0` → `signal`; the lesser of the two effective floats. Pure arithmetic; no configuration or state. **Edges/extensions:** sanitize each input using the shared policy before comparison; signed zeros are equivalent. Additional inputs must be explicit extensions. **Examples:** min(3, -2) → -2; min(4, 4) → 4.
|
|
138
|
+
|
|
139
|
+
### Max
|
|
140
|
+
|
|
141
|
+
**Purpose/signature:** `signal = 0`, `other = 0` → `signal`; the greater of the two effective floats. Pure arithmetic; no configuration or state. **Edges/extensions:** same input sanitization and signed-zero policy as Min. Additional inputs must be explicit extensions. **Examples:** max(3, -2) → 3; max(-4, -1) → -1.
|
|
142
|
+
|
|
143
|
+
### Multiply
|
|
144
|
+
|
|
145
|
+
**Purpose/signature:** `signal = 0`, `factor = 1` → `signal`; `signal * factor`. Both parameters are floats and may be modulated independently. **Edges:** overflow becomes 0; negative factor inverts sign. **Extensions:** multiple factors may be explicit extra pins; do not reinterpret link weights. **Examples:** 3 × -2 → -6; factor 0 → 0.
|
|
146
|
+
|
|
147
|
+
### Pow
|
|
148
|
+
|
|
149
|
+
**Purpose/signature:** `signal = 0`, `exponent = 1` → `signal`; real arithmetic power. Negative bases are valid only for integer exponents. `0^0 = 1`; zero to negative exponent, negative base with non-integer exponent and non-finite results produce 0. No implicit absolute value, sign-preserving distortion or complex result.
|
|
150
|
+
|
|
151
|
+
**Extensions:** Teia's sign-preserving power must be identified as a distinct mode/profile rather than assumed mathematically equivalent. **Examples:** `2^3 = 8`, `(-2)^2 = 4`, `(-2)^3 = -8`, `(-2)^0.5 = 0`, `0^-1 = 0`, `0^0 = 1`.
|
|
152
|
+
|
|
153
|
+
### Map
|
|
154
|
+
|
|
155
|
+
**Purpose/signature:** `signal = 0`, `source min = 0`, `source max = 1`, `target min = 0`, `target max = 1` → `signal`. Compute `target min + (signal - source min) / (source max - source min) * (target max - target min)`. Do not clamp or sort either interval; reversed intervals reverse direction. Equal source bounds output target min. There is no epsilon substitution in the baseline.
|
|
156
|
+
|
|
157
|
+
**Edges/extensions:** arbitrarily small nonzero spans use the same formula, subject to numeric precision/overflow policy. Clamping and epsilon regularization are explicit extensions. **Examples:** 0.25 from 0..1 to 10..20 → 12.5; 2 → 30; 0.25 from 1..0 to 10..20 → 17.5; equal source bounds and target min 10 → 10.
|
|
158
|
+
|
|
159
|
+
### Clamp
|
|
160
|
+
|
|
161
|
+
**Purpose/signature:** `signal = 0`, `min = 0`, `max = 1` → `signal`. Sort the bounds, then `min(high, max(low, signal))`. **Edges:** equal bounds output that bound. **Extensions:** rejecting reversed bounds instead must be documented. **Examples:** 3 clamped to 0..1 → 1; -2 to 1..-1 → -1; 8 to 2..2 → 2.
|
|
162
|
+
|
|
163
|
+
### Random
|
|
164
|
+
|
|
165
|
+
**Purpose/signature:** `trigger = 0`, `range min = -1`, `range max = 1` → `signal`. Draw a normalized uniform pseudorandom value `u` in `[0, 1)` once on first executed tick, then once per subsequent trigger rising edge; an initially high trigger does not cause two draws. Hold `u` between events, and compute `range min + (range max - range min) * u` using the live bounds. Reversed ranges are valid; equal bounds yield a constant while the random stream still advances on triggers.
|
|
166
|
+
|
|
167
|
+
**State/configuration:** each instance owns its held value, generator state and previous trigger level. Optional deterministic seed is non-pin configuration; changing it explicitly reinitializes that instance. Publish algorithm/version and seed-to-instance derivation before promising repeatability. An unseeded restart may reseed; outputs are not required to match across apps.
|
|
168
|
+
|
|
169
|
+
**Edges/extensions/data:** no trigger pulse queue is implicit. Non-finite inputs use defaults; numeric output sanitization applies. Distribution, per-event resampling of already-mapped values, runtime reseeding and shared streams are named extensions. No required shared editor format.
|
|
170
|
+
|
|
171
|
+
**Examples:** with an injected draw stream `[0.25, 0.75]`, range -1..1, trigger `[0, 1, 1, 0]` → `[-0.5, 0.5, 0.5, 0.5]` using two draws. Changing range to 0..4 while holding `u = 0.75` → 3 without a draw. This fixture tests lifecycle/mapping, not a particular PRNG.
|
|
172
|
+
|
|
173
|
+
### Noise
|
|
174
|
+
|
|
175
|
+
**Purpose/signature:** `range min = -1`, `range max = 1` → `signal`. Baseline is temporal white uniform noise: draw one `u` in `[0, 1)` per executed tick and map it as Random does. Re-reading in one tick does not redraw. No implicit elapsed-time clock is required.
|
|
176
|
+
|
|
177
|
+
**State/configuration:** instance-local generator; optional seed configuration and determinism rules as Random. Configuration may name algorithm/version. **Edges:** reversed/equal ranges and non-finite inputs follow Random; suspension pauses stream advancement. Sample and frame rates give different spectra. **Extensions:** coordinate-driven coherent noise, octaves, Gaussian distributions and smoothing require named profiles with their own pin/range contracts. They are not equivalent to temporal white noise. No required shared editor format.
|
|
178
|
+
|
|
179
|
+
**Examples:** injected draws `[0, 0.25, 0.75]` with range -1..1 → `[-1, -0.5, 0.5]` over three ticks; a second consumer gets the same tick's value. A suspended instance consumes no draws. Statistical quality requires separate testing; these examples do not establish it.
|
|
180
|
+
|
|
181
|
+
### Slider
|
|
182
|
+
|
|
183
|
+
**Purpose/signature:** `value = 0.5` normalized, `min = 0`, `max = 1` → `signal`, optional `inverse`. Set `u = clamp(value, 0, 1)`; output `min + (max - min) * u` and inverse `min + (max - min) * (1 - u)`. Unlike Clamp, do not sort range bounds.
|
|
184
|
+
|
|
185
|
+
**State/configuration:** saved normalized manual value is node data/parameter state. Orientation and UI presentation are non-pin configuration. Connected/modulated value drives the effective output; a UI drag updates the base manual value through normal graph edits, not by deleting links. Output computation is pure; a headless instance can evaluate a saved value. No evaluation clock is needed.
|
|
186
|
+
|
|
187
|
+
**Edges/extensions/data:** reversed range, equal bounds and non-finite inputs follow shared policy. Drag cancellation must release pointer/gesture state. Log curves, signal crossfading, pickup and MIDI are named extensions. Shared Spagbol slider control is usable but does not itself implement this signal contract. No new serialized editor model is mandated.
|
|
188
|
+
|
|
189
|
+
**Examples:** value 0.25 and range 10..20 → signal 12.5, inverse 17.5; value 2 → 20 and 10; range 20..10 at 0.25 → 17.5 and 12.5.
|
|
190
|
+
|
|
191
|
+
### Button
|
|
192
|
+
|
|
193
|
+
**Purpose/signature:** no required numeric inputs → `signal`, optional `inverse`. Produce a numeric interactive control with configuration `mode = temporary` by default; `toggle` and `trigger` are other baseline modes. Output is 0/1 and inverse is `1 - signal`.
|
|
194
|
+
|
|
195
|
+
**Behaviour/state:** temporary outputs 1 while pressed, then 0 on release, cancellation, lost pointer capture or focus. Toggle flips the saved state once per accepted activation; initial state is 0 unless a saved toggle value says otherwise. Trigger queues one pulse for each accepted press activation: each pulse is one executed tick high with at least one low tick between pulses so edge consumers see each event. Release alone does not add a trigger. Initially idle temporary/trigger states are not restored as held presses on reload. Each instance owns its state and pending activations.
|
|
196
|
+
|
|
197
|
+
**Edges/extensions/data:** a UI-only flash duration is not the signal pulse length. Apps must state event delivery when UI and execution rates differ, and queue capacity/overflow policy. A suspended scope retains queued activations unless its profile says otherwise. Numeric signal routing, MIDI, smoothing and retrigger counters are extensions. Shared button UI callbacks require a host scheduling adapter; they are not a signal runtime or queue implementation. No new specialized editor format is required.
|
|
198
|
+
|
|
199
|
+
**Examples:** temporary press/release → 1 then 0; two toggle activations → 1 then 0; two queued trigger activations → `[1, 0, 1, 0]`. A cancelled temporary press → 0; ordinary activation does not mutate graph topology.
|
|
200
|
+
|
|
201
|
+
### Joystick
|
|
202
|
+
|
|
203
|
+
**Purpose/signature:** `x min = 0`, `x max = 1`, `y min = 0`, `y max = 1` → `x`, `y`, optional `x inverse`, `y inverse`. Saved normalized positions initially `(0.5, 0.5)` are interactive parameter state. Clamp each normalized coordinate to 0..1 and map each axis and inverse using Slider's formulas. Left/bottom mean 0; right/top mean 1, regardless of screen Y direction.
|
|
204
|
+
|
|
205
|
+
**State/configuration:** orientation/appearance are configuration; no required runtime clock. Holding position after release is baseline. Instances own positions; projected controls must target the correct instance. Non-finite saved positions use 0.5; numeric range inputs use their declared defaults. **Edges/extensions/data:** reversed/equal ranges work independently per axis. Pointer cancellation ends the gesture and retains the latest valid position. Elastic return, MIDI, snapping and numeric coordinate input pins are extensions. The shared Joystick component emits normalized x/y callbacks without defining a saved format.
|
|
206
|
+
|
|
207
|
+
**Examples:** position `(0, 1)` with both ranges 0..1 → x 0, y 1, x inverse 1, y inverse 0; x range -2..2 at x 0.75 → x 1, inverse -1. Releasing without elasticity leaves values unchanged.
|
|
208
|
+
|
|
209
|
+
### Curve
|
|
210
|
+
|
|
211
|
+
**Purpose/signature:** `position = 0` → `value`. Stateless lookup of configured numeric curve data, independent of clock, tempo, trigger, playback or evaluation rate.
|
|
212
|
+
|
|
213
|
+
**Logical configuration:** normalized positions 0..1, finite ordered points/segments, interpolation (baseline linear), and boundary mode (default clamp; repeat and mirror optional). Default shape is the constant-zero curve, with points `(0, 0)` and `(1, 0)`. Baseline points must include both endpoints and have unique strictly increasing positions. Duplicate positions, missing endpoints or non-finite data are invalid configuration; import adapters may normalize legacy data explicitly. Values need not be restricted to -1..1.
|
|
214
|
+
|
|
215
|
+
**Behaviour/edges:** clamp position to 0..1, find the containing segment and interpolate linearly. Exact point positions produce their point value. Repeat uses `p - floor(p)`, so 1 maps to 0 and -0.25 to 0.75. Mirror uses `q = p - 2 * floor(p / 2)`, then `q` if `q <= 1`, otherwise `2 - q`; 1 remains 1. Non-finite position uses 0. No state or hidden oscillator phase.
|
|
216
|
+
|
|
217
|
+
**Editor/data/extensions:** phase 4 extracts Teia's advanced UI with host callbacks, leaving model/edit operations in Teia, preserving curve banks, base-level endpoints, sustain regions, grid/group controls, gesture history and playback metadata through app wrappers. These logical rules do not define a shared wire schema; do not discard existing serialized data to fit a new two-point-only editor. Additional interpolation must define segment parameters and evaluation rules. Teia frequency/phase/trigger and playback modes are extensions, not requirements for arbitrary position evaluation.
|
|
218
|
+
|
|
219
|
+
**Examples:** points `[(0, 0), (0.5, 1), (1, 0)]`, linear clamp: positions `[-1, 0.25, 0.5, 1, 2]` → `[0, 0.5, 1, 0, 0]`. Ramp `[(0, 0), (1, 1)]`: position 1 → clamp 1, repeat 0, mirror 1; position -0.25 → repeat 0.75, mirror 0.25.
|
|
220
|
+
|
|
221
|
+
### Sequence
|
|
222
|
+
|
|
223
|
+
**Purpose/signature:** `position = 0` → `row 1` … `row n` (default one row). Stateless lookup of stored numeric rows. Baseline position uses normalized 0..1, not a clock pulse, musical beat or raw step index.
|
|
224
|
+
|
|
225
|
+
**Logical configuration:** positive integer step count (default 8), one or more stable-ID rows with finite numeric cells (default all zero), and boundary mode (default clamp; repeat/mirror optional as Curve). Each row has exactly the configured number of cells. Numeric cell values are unrestricted by musical notes, gates or velocity. Duplicate row IDs, zero steps/rows, mismatched lengths and non-finite cells produce configuration diagnostics. Resizing must be an explicit editor operation with a documented data-preservation policy.
|
|
226
|
+
|
|
227
|
+
**Behaviour/edges:** transform position with the chosen boundary mode; for `N` steps use `index = min(N - 1, floor(p * N))`. Cells hold their value until the next boundary; an exact internal boundary selects the following step. Clamp at 1 selects the last step; repeat at 1 selects the first. No implicit interpolation, trigger emission or skipped-step event playback when position jumps. Evaluation can run backward or revisit any position without mutation. Non-finite position uses 0. No instance playback state is required.
|
|
228
|
+
|
|
229
|
+
**Editor/data/extensions:** phase 4 reuses Teia's row/step editing, labels, moving/resizing gates, non-overlap rules, fractional positions, per-step velocities, mode conversion and hidden-data retention through a lossless adapter/extension model. These requirements preserve advanced app data; they do not force the generic numeric baseline to emit musical events. There is no Spagbol-owned serialized schema; applications supply view projections and handle edit callbacks. App clocks, trigger/reset inputs, multiple event outputs and beat length are wrappers/extensions. Step-index position must be an explicit mode or adapter, not silently substituted for normalized position.
|
|
230
|
+
|
|
231
|
+
**Examples:** row `[10, 20, 30, 40]`, clamp, positions `[-1, 0, 0.249, 0.25, 0.999, 1, 2]` → `[10, 10, 10, 20, 40, 40, 40]`. Repeat at 1 → 10; mirror at 1.25 → 40 (mapped position 0.75). A second row `[1, 2, 3, 4]` at 0.25 outputs 2 independently. The shared Sequencer UI is an editor surface, not this lookup evaluator.
|
|
232
|
+
|
|
233
|
+
## Adopting a contract
|
|
234
|
+
|
|
235
|
+
An application's profile should list each implemented node, its label/ID mapping, baseline revision and deviations. State the numeric precision and non-finite policy, evaluation cadence, time origin where app-specific clocks exist, trigger delivery, feedback, lifecycle order, resource limits and serialization adapter. Then translate the examples into that runtime's own checks. A matching label alone does not certify conformance; unsupported nodes and unevaluated edge cases remain explicit.
|
|
236
|
+
|
|
237
|
+
No conformance tests or application functionality were executed to produce this specification. The [comparison](standard-node-comparison.md) separates source observations from recommendations and records work still needed. Documentation completion does not establish web/firmware parity, hardware verification or identical sample/frame scheduling.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Spagbol Standard Node Conventions
|
|
2
|
+
|
|
3
|
+
> Phases 1–3 documentation, 2026-11-09: read the [26 baseline contracts](standard-node-contracts.md) and [source-based application comparison](standard-node-comparison.md). Envelope, Playhead and Time are app-specific by request. No catalogue or runtime migration is included; phase 4 now provides reusable UI; runtime conformance and cross-app validation remain future work.
|
|
4
|
+
|
|
5
|
+
**Status:** Proposal / draft 0.4
|
|
6
|
+
**Scope:** A recommended vocabulary of 26 nodes for Spagbol-based applications
|
|
7
|
+
**Origin:** Common concepts identified in Teia's node catalogue, intended for reuse in Visual Visual and future applications.
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
Spagbol is an opinionated **float-signal graph language**, not a generic typed-node workflow system. All graph connections carry numeric values and share Spagbol's established connection, weighting and modulation semantics. Nodes may also hold non-numeric configuration (dropdowns, text, file references, etc.), but that configuration is **not connected or modulated through pins**.
|
|
12
|
+
|
|
13
|
+
This proposal establishes a shared **vocabulary and behavioural conventions** for 26 commonly useful nodes. It does **not** require applications to install a particular catalogue or use a shared runtime. Teia can compile to its audio DSP engine; Visual Visual can compile to shaders or update a scene; other applications supply their own evaluators. Spagbol supplies specifications and reusable UI.
|
|
14
|
+
|
|
15
|
+
The aim is for a user moving between Spagbol applications to recognise familiar node names and behaviours, without constraining domain-specific implementations.
|
|
16
|
+
|
|
17
|
+
## Three levels of standardisation
|
|
18
|
+
|
|
19
|
+
1. **Language invariants (core Spagbol):** Float-only connections; link modes and weighted modulation; graph and scope semantics; the distinction between numeric pins and non-modulatable node configuration. These describe language intent, not optional catalogue nodes. The current editor does not enforce all of these runtime semantics; see the implementation boundary in the application comparison.
|
|
20
|
+
2. **Recommended standard nodes (this document):** Names, intent, baseline inputs/outputs and behavioural contracts. Applications are encouraged—not required—to implement them.
|
|
21
|
+
3. **Optional conformance tooling:** Applications may implement the examples as executable fixtures. Runtime evaluation and saved models remain app-owned.
|
|
22
|
+
|
|
23
|
+
Applications may add pins, modes, UI and domain-specific functionality. Where behaviour differs materially, they should document the variation rather than silently using a standard name for incompatible semantics.
|
|
24
|
+
|
|
25
|
+
## Areas and their kinds
|
|
26
|
+
|
|
27
|
+
**Area** is the generic structural term, not a selectable plain kind. The four area kinds are:
|
|
28
|
+
|
|
29
|
+
| Kind | Meaning | Execution effect |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| **Container** | Plain visual organisation, membership, movement and collapse. Previously displayed as Area. | None: it does not create instances, reset state or suppress work. |
|
|
32
|
+
| **Spread** | A count-driven set of indexed instances of the contained graph. | Evaluate one instance per active index. |
|
|
33
|
+
| **Spawn** | Independently lived instances created by triggers. | Create, release and kill instances according to lifecycle inputs. |
|
|
34
|
+
| **Conditional Execution** | A gate-controlled contained scope. | Execute or skip the contained work. |
|
|
35
|
+
|
|
36
|
+
Spread, Spawn and Conditional Execution are collectively **execution areas**. Container is a **visual area**. They share area editing interactions where supported: header, bounds, membership, locking, moving and collapsing. Sharing the visual structure does not imply shared execution semantics or support for every nested combination.
|
|
37
|
+
|
|
38
|
+
Container is a core editing construct outside the 26 recommended node contracts. The three execution kinds are already counted among those contracts. **Group** is separate: it encapsulates a nested graph and its interface; it is not another area kind.
|
|
39
|
+
|
|
40
|
+
Spagbol represents the umbrella through `GraphArea` and saved `areas`. Optional `ownerNodeId` connects an area to an app node that supplies its execution semantics. The UI calls the ownerless visual kind Container, including creation/type pickers and default titles. Existing serialized keys, `spagbol.area`, `addArea()`/`convertNodeToArea()` APIs, IDs and user-supplied titles remain compatible; no saved graph migration or title rewrite is required. Teia calls the behavioural subset execution areas in its helper names and diagnostics.
|
|
41
|
+
|
|
42
|
+
## The 26 recommended nodes
|
|
43
|
+
|
|
44
|
+
The following are **conventions**, not a mandate to copy Teia's current node implementations verbatim. Names below reflect Teia where possible. The six nodes that most strongly shape execution and programming semantics are marked **★**.
|
|
45
|
+
|
|
46
|
+
### Maths (14)
|
|
47
|
+
|
|
48
|
+
| Node | Recommended role | Behavioural notes |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| **Expression** ★ | Evaluate a numeric expression with named float inputs and a float result. | Expressions and their input names can be non-pin configuration; define supported operators/functions and error behaviour per runtime. |
|
|
51
|
+
| **Constant** | Produce a fixed or modulated numeric value. | Numeric value should remain a float parameter. |
|
|
52
|
+
| **Pass** | Forward a float unchanged. | Useful for routing, graph organisation and observation. |
|
|
53
|
+
| **Abs** | Return the absolute value of a float. | `abs(x)`. |
|
|
54
|
+
| **Add** | Add a float to an incoming signal. | `signal + amount`. |
|
|
55
|
+
| **Subtract** | Subtract a float from an incoming signal. | `signal - amount`; operand order matters. |
|
|
56
|
+
| **Divide** | Divide a signal by a divisor. | Zero divisor returns 0 in the proposed baseline. |
|
|
57
|
+
| **Modulo** | Wrap a float using floor-based modulo. | Result follows divisor sign; zero divisor returns 0. |
|
|
58
|
+
| **Min** | Return the smaller of two floats. | Numeric minimum; no state. |
|
|
59
|
+
| **Max** | Return the larger of two floats. | Numeric maximum; no state. |
|
|
60
|
+
| **Multiply** | Scale an incoming float by a factor. | `signal × factor`; independent of Spagbol's link-level Multiply mode. |
|
|
61
|
+
| **Pow** | Raise an incoming float to an exponent. | Specify handling of negative bases with non-integer exponents and other non-finite results. |
|
|
62
|
+
| **Map** | Remap from a source interval to a target interval. | Agree on behaviour when source min equals source max; do not silently assume clamping. |
|
|
63
|
+
| **Clamp** | Limit a float to a minimum and maximum. | Agree on reversed bounds. |
|
|
64
|
+
|
|
65
|
+
### Logic (2)
|
|
66
|
+
|
|
67
|
+
| Node | Recommended role | Behavioural notes |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| **Selector** ★ | Choose among numeric inputs using a numeric selection parameter. | Define indexing, rounding, out-of-range selection and any interpolation/slide option. Teia's existing `select` and `slide` controls are a useful starting point. |
|
|
70
|
+
| **Conditional Execution** ★ | Enable or suppress execution of a contained scope using a numeric gate. | Unlike a numeric conditional selector, this affects *whether work happens*. Specify gate threshold, instance state and resource lifecycle. |
|
|
71
|
+
|
|
72
|
+
A future **Condition** (numeric if/else) node could be a useful addition, but it is **not** counted among these 26: the current Teia catalogue has `ConditionalExecution` and `Selector`, not a separate `Condition` node.
|
|
73
|
+
|
|
74
|
+
### Instantiation (2)
|
|
75
|
+
|
|
76
|
+
| Node | Recommended role | Behavioural notes |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| **Spread** ★ | Evaluate a contained scope across an indexed set of instances. | Teia currently exposes a numeric `count` input and `item index` output. Define zero count, instance indices, ordering, and stable identity. |
|
|
79
|
+
| **Spawn** ★ | Create and manage independently lived instances of a contained scope. | Teia currently uses trigger, release trigger, kill trigger and instance gate. Preserve the distinction between spawning and distributing values; applications may specialise lifecycle controls. |
|
|
80
|
+
|
|
81
|
+
**Key distinction:** Spread is about a set of indexed evaluations; Spawn is about instances with lifecycles. Neither needs object-valued connections. A Visual Visual scene object can exist simply because its node exists within an active graph/instance.
|
|
82
|
+
|
|
83
|
+
### State (3)
|
|
84
|
+
|
|
85
|
+
| Node | Recommended role | Behavioural notes |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| **Accumulator** ★ | Maintain a numeric value across evaluations, optionally incremented, reset or bounded. | Define reset precedence, trigger semantics, wrapping/clamping and state ownership per instance. |
|
|
88
|
+
| **Random** | Generate numeric random values. | Define when a new value is generated, range and optional deterministic seeding. Teia uses a trigger. |
|
|
89
|
+
| **Noise** | Generate a numeric noise signal. | Define distribution and whether output is time-dependent; algorithms may differ. |
|
|
90
|
+
|
|
91
|
+
### Interaction (3)
|
|
92
|
+
|
|
93
|
+
| Node | Recommended role | Behavioural notes |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| **Slider** | Interactive one-dimensional numeric control. | Standardise the meaning of value, range and optional inverse output; Teia-specific MIDI binding remains an extension. |
|
|
96
|
+
| **Button** | Interactive momentary, toggle or trigger-style numeric control. | Define numeric outputs and transitions; UI appearance can vary. |
|
|
97
|
+
| **Joystick** | Interactive two-axis numeric control. | Standardise X/Y outputs and range mapping; elasticity and MIDI mapping are optional extensions. |
|
|
98
|
+
|
|
99
|
+
### Structured modulators (2)
|
|
100
|
+
|
|
101
|
+
**Curve** and **Sequence** are recommended standard nodes with an additional goal: Spagbol should provide **shared, reusable controlled UI editors**, not merely document their behaviour. They are general-purpose float modulators, independent of any musical clock. The shared UI should be extracted from Teia's existing sophisticated editors, not recreated from scratch.
|
|
102
|
+
|
|
103
|
+
| Node | Teia origin | Recommended baseline | Shared UI |
|
|
104
|
+
|---|---|---|---|
|
|
105
|
+
| **Curve** | `CustomWave` (renamed conceptually) | `position` float input → `value` float output; evaluate a stored curve at the requested position. | Curve drawing/editing, points or segments, interpolation and shape controls. |
|
|
106
|
+
| **Sequence** | `Sequencer` | `position` float input → numeric value output(s); evaluate stored steps at the requested position. | Step/row editing, sequence data manipulation and visual feedback. |
|
|
107
|
+
|
|
108
|
+
#### Position-driven baseline
|
|
109
|
+
|
|
110
|
+
The **standard forms are time-independent**: neither node requires its own clock, tempo, trigger or playback engine. A position is supplied by any float source (an app-specific clock or Playhead, mouse, an LFO, another Curve, etc.). This makes them suitable for per-sample DSP, per-frame animation, GLSL compilation and arbitrary evaluation.
|
|
111
|
+
|
|
112
|
+
- **Curve:** Define a documented position domain (suggested default: normalised `0..1`), interpolation, out-of-range behaviour (clamp/repeat/mirror), and numeric output. Its shape and editor metadata are non-pin configuration. A reference evaluator may be supplied, but specialised runtimes can evaluate the application data differently for performance, subject to the shared behavioural contract.
|
|
113
|
+
- **Sequence:** Define how position maps to step index, boundary behaviour, and how values are read (including optional multiple rows/outputs). The editor's steps and rows are structured node data, **not** values travelling over graph links. Avoid baking BPM or musical note concepts into the baseline. The baseline uses normalized position; apps may adapt step-index or musical positions explicitly.
|
|
114
|
+
- **Teia extensions:** Curve may add frequency, phase, trigger, subdivisions and other waveform/musical controls; Sequence may add beat-length, clock/trigger, reset, multiple rows and musical sequencing behaviour. These extensions should preserve compatibility with the shared view prop contract wherever possible.
|
|
115
|
+
- **Visual Visual usage:** Drive either node from arbitrary floats to animate transforms, shader uniforms, opacity, lighting or other numeric scene parameters.
|
|
116
|
+
|
|
117
|
+
**Separation of responsibilities:**
|
|
118
|
+
|
|
119
|
+
| Spagbol may provide | Each application provides |
|
|
120
|
+
|---|---|
|
|
121
|
+
| Recommended node contracts and stable UI prop contracts | Runtime evaluation / compilation strategy |
|
|
122
|
+
| Shared React Curve, Sequence and Joystick editors | Musical or visual extensions to controls |
|
|
123
|
+
| Typed view projections and edit-intent callbacks | Clock, transport and scheduling integrations |
|
|
124
|
+
| Documented conformance examples | Optimised DSP / GPU implementations |
|
|
125
|
+
|
|
126
|
+
Shared UI does **not** require a shared runtime. The editors should support extension points rather than being hardwired to Teia-specific controls. Extract existing Teia behaviour incrementally; do not discard advanced editing features merely to make the baseline generic.
|
|
127
|
+
|
|
128
|
+
**Total: 14 + 2 + 2 + 3 + 3 + 2 = 26.**
|
|
129
|
+
|
|
130
|
+
## Not part of the 26
|
|
131
|
+
|
|
132
|
+
- **Core graph constructs:** Container (the plain visual area), Group, Ins and Outs are Spagbol-owned editing/interface constructs, not ordinary catalogue nodes. Teia's Params deserves a separate review of group/interface conventions.
|
|
133
|
+
- **Potential later standard extensions:** Oscillators, Perlin noise, follower/smoothing, Send/Receive, tempo, scope/meter and generic numeric Condition.
|
|
134
|
+
- **App-specific time and shaping nodes:** Envelope, Playhead and Time. Applications own their clocks, shapes, transport and playback semantics; none has a shared standard-node contract.
|
|
135
|
+
- **Domain-specific Teia nodes:** Audio I/O, MIDI, musical quantisation, sample playback, filters, effects, distortion, FFT and other DSP-specific components.
|
|
136
|
+
- **Visual Visual scene nodes:** Sphere, Camera, Light, Painty, Image and Video belong to Visual Visual's catalogue. Their existence declares persistent scene entities; their numeric pins remain modulatable. Non-numeric file paths and modes remain node configuration.
|
|
137
|
+
|
|
138
|
+
## Runtime and semantic boundaries
|
|
139
|
+
|
|
140
|
+
**What Spagbol should specify:**
|
|
141
|
+
|
|
142
|
+
- The common meaning of float pins, weighted links and link modulation.
|
|
143
|
+
- The recommended contract for each standard node, including defaults, numeric units and edge cases.
|
|
144
|
+
- Scope and instance semantics for Spread, Spawn and Conditional Execution.
|
|
145
|
+
- Which operations are stateful, and how state relates to graph and instance identity.
|
|
146
|
+
|
|
147
|
+
**What applications should decide:**
|
|
148
|
+
|
|
149
|
+
- Evaluation frequency: per sample, per frame, on demand or on events.
|
|
150
|
+
- Execution backend: Rust/WASM DSP, GLSL/GPU, TypeScript or another engine.
|
|
151
|
+
- Domain-specific node implementations, controls, optimisations and additional pins.
|
|
152
|
+
- How non-numeric node configuration is stored and acted upon.
|
|
153
|
+
|
|
154
|
+
The same graph language need not imply identical scheduling. However, scheduling differences that alter observable results—especially around triggers, feedback, state and instance lifecycle—must be documented as execution profiles rather than assumed away.
|
|
155
|
+
|
|
156
|
+
## Proposed documentation contract for each node
|
|
157
|
+
|
|
158
|
+
A standard-node entry should eventually specify:
|
|
159
|
+
|
|
160
|
+
1. **Canonical name and purpose.**
|
|
161
|
+
2. **Recommended float inputs and outputs**, including defaults and units.
|
|
162
|
+
3. **Non-pin configuration**, where relevant.
|
|
163
|
+
4. **Observable behaviour** and formulas.
|
|
164
|
+
5. **State, clock, trigger and instance semantics**, if applicable.
|
|
165
|
+
6. **Edge cases** (NaN, infinity, division by zero, zero instances, reset order, etc.).
|
|
166
|
+
7. **Allowed extension points** and known domain-specific differences.
|
|
167
|
+
8. **Shared editor data/UI contracts**, where applicable (notably Curve and Sequence).
|
|
168
|
+
9. **Small conformance examples**, ideally executable in a reference runtime.
|
|
169
|
+
|
|
170
|
+
Avoid freezing Teia's exact parameter naming or DSP-specific assumptions until compared against Visual Visual's needs.
|
|
171
|
+
|
|
172
|
+
## Implementation phases
|
|
173
|
+
|
|
174
|
+
**Phase 1 — Document, don't migrate (documented).** Publish this proposal under `docs/language/standard-nodes.md`. Compare Teia's actual definitions and execution behaviour with Visual Visual's equivalents. Record disagreements explicitly.
|
|
175
|
+
|
|
176
|
+
**Phase 2 — Specify the six semantic anchors (documented).** Start with Expression, Selector, Conditional Execution, Spread, Spawn and Accumulator. These carry the most consequential language and lifecycle assumptions.
|
|
177
|
+
|
|
178
|
+
**Phase 3 — Complete the remaining twenty contracts (documented).** Establish defaults, formulas, pin conventions and edge-case behaviour for maths, state, interaction and structured modulator nodes.
|
|
179
|
+
|
|
180
|
+
**Phase 4 — Extract controlled Curve, Sequencer and Joystick UI (implemented, functional validation pending).** Spagbol owns view components and callbacks. Teia retains persistence, model edits, musical wrappers, clocks and runtime evaluation; its piano roll also reuses the Sequencer surface. No shared saved-data schema or edit-operation engine is introduced. See [UI ownership and integration](ui-components.md). Visual Visual adoption remains future work.
|
|
181
|
+
|
|
182
|
+
**Phase 5 — Optional conformance tooling.** Provide sample UI integrations and executable contract fixtures where useful. Applications retain models and evaluators; this plan does not add a shared runtime or persistence model to Spagbol.
|
|
183
|
+
|
|
184
|
+
**Phase 6 — Validate across applications.** Exercise the conventions with Teia's DSP runtime, Visual Visual's shader/scene runtime and a minimal standalone app. Measure performance separately from semantic correctness.
|
|
185
|
+
|
|
186
|
+
## Design principles
|
|
187
|
+
|
|
188
|
+
- **One numeric signal language.** Do not introduce object-, texture- or event-valued connections just to make standard nodes more general. Triggers and gates are represented numerically.
|
|
189
|
+
- **Convention over enforcement.** Applications can omit, extend or specialise recommended nodes.
|
|
190
|
+
- **Stable meaning over identical implementation.** Shared names should imply recognisably shared behaviour.
|
|
191
|
+
- **Persistent instances, cheap updates.** Scene entities and DSP components should not be recreated merely because a modulated float changes.
|
|
192
|
+
- **Reuse complex editors without coupling runtimes.** Curve and Sequence share UI contracts while Teia and Visual Visual retain their own evaluation and domain-specific controls.
|
|
193
|
+
- **No runtime or data-model unification.** Spagbol supplies specifications and controlled UI; Teia and Visual Visual retain execution engines, serialized data and model editing.
|
|
194
|
+
- **Ground the specification in working applications.** Extract the common language from real Teia and Visual Visual behaviour, not a hypothetical universal graph system.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
*This is a proposal for a recommended standard vocabulary, not a claim that Spagbol currently implements these nodes or that all their edge cases have already been standardised.*
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Controlled node editor components
|
|
2
|
+
|
|
3
|
+
Stage 4 ships three React components in Spagbol 0.1.3, extracted from Teia. Spagbol owns reusable rendering and pointer interaction. **Applications own saved data, validation/normalization, edit operations, undo transactions, transport, assets, MIDI and evaluation.** The TypeScript point/item types below are temporary view projections, not a shared document schema. Spagbol introduces no curve/sequence persistence format, reducer, musical clock or reference evaluator.
|
|
4
|
+
|
|
5
|
+
Import components from `@pauloricca/spagbol` and styling from `@pauloricca/spagbol/editors.css` (already included by `styles.css`). React/React DOM remain the shared package's peer dependencies. These components need no canvas/controller, provider, audio engine or Teia import. They include `nodrag nopan` classes for use inside graph editors. The existing Spagbol graph editor/model API remains available; this ownership decision concerns the new domain editors, not removing established graph APIs.
|
|
6
|
+
|
|
7
|
+
## Curve
|
|
8
|
+
|
|
9
|
+
`SpagbolCurveEditor` renders a controlled set of normalized view points: `x` in 0..1 and `y` in -1..1. `locked` prevents dragging/deleting individual points; Teia locks its two endpoints. Applications can map any numeric domain into these coordinates without changing their saved data. Point order and normalization are host responsibilities.
|
|
10
|
+
|
|
11
|
+
Required props are `points`, on-screen `displaySize`, and `onInsertPoint`, `onMovePoint`, `onRemovePoint`. Insert applies a host edit and returns the new index (or null to reject); move/remove callbacks request host edits by index. `onGestureEnd` lets an adapter flush throttled updates. Keep point ordering stable during a drag; replace/remount the component when switching the edited curve. The host may reject any requested edit.
|
|
12
|
+
|
|
13
|
+
The extracted interactions include click-to-insert, dragging points between neighbors, double-click removal of unlocked points, locked endpoints, pointer capture/cancellation, hover/drag value labels and screen-sized hit targets as the canvas zooms. The default point spacing is 0.001. `screenEmphasis` and `pointScale` preserve Teia's zoom adjustments; `formatValue` maps view values to displayed application units.
|
|
14
|
+
|
|
15
|
+
`backgroundCurves` and `pathClassName` support multi-curve previews and colors. `toolbar`, `underlay`, SVG `overlay` and `footer` slots preserve complex app UI without putting its model or runtime into Spagbol. SVG overlays use the fixed 300 × 128 viewBox with 12-unit padding. Overlays are decorative; interactive controls belong in toolbar/footer slots. Applications supply grid legends, modes, sustain regions, range controls, playback markers and bank-selection shortcuts.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<SpagbolCurveEditor
|
|
19
|
+
points={projectCurve(appCurve)}
|
|
20
|
+
displaySize={{ width: 400, height: 180 }}
|
|
21
|
+
onInsertPoint={(point) => insertAppPoint(point)}
|
|
22
|
+
onMovePoint={(index, point) => moveAppPoint(index, point)}
|
|
23
|
+
onRemovePoint={(index) => removeAppPoint(index)}
|
|
24
|
+
/>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
These example functions are host adapters, not Spagbol API. The component performs no curve lookup, DSP compilation or clock advancement.
|
|
28
|
+
|
|
29
|
+
## Sequencer
|
|
30
|
+
|
|
31
|
+
`SpagbolSequencer` is a controlled step/interval editing surface. Required props include `ariaLabel`, `rows`, `steps`, `rowLabel`, `itemsForRow`, `onCreateItem`, `onUpdateItem` and `onDeleteItem`. Each view item has a row-local stable numeric `id`, `start`, `end` in **step coordinates**, and normalized `level` for its displayed height. This UI coordinate system is independent of the standard Sequence evaluator's normalized position input. There is no evaluator in this component.
|
|
32
|
+
|
|
33
|
+
Adapters choose how their own model becomes intervals or step bars. The host handles allocation of new IDs, storage keys, row data, non-overlap validation and edit application. The component constrains pointer proposals against neighboring intervals; the host still validates them. Supply positive finite row/step counts and valid finite view values. A numeric-cell editor can map cells to bars; a musical app can map trigger/gate data without changing the shared component.
|
|
34
|
+
|
|
35
|
+
`mode` chooses trigger painting or interval editing. Trigger mode supports drawing across multiple cells in either direction; `onCreateItems` batches that operation when supplied. Gate mode supports click-to-create, drag-to-extend a new interval, movement, edge resizing, neighbor collision bounds and level adjustment. Clicking an existing item's body without dragging requests deletion. Defaults preserve Teia's minimum interval length of 0.05 steps and `minLevel=0.1`; set `minLevel=0` for zero-height numeric bars. `allowMove`, `allowResize` and `allowLevel` independently disable the associated gestures. Cancelled creation discards its preview; cancellation of an existing drag leaves already accepted host edits in place, as in Teia's original editor.
|
|
36
|
+
|
|
37
|
+
`groupSize` (default 4) is a visual grouping width, not BPM or a transport. `currentStep` is supplied visual feedback. `renderRowLabel`, `labelPosition` and `onRowRef` let an application insert editable labels, graph ports, pitch names or other row chrome without shared knowledge of those systems. Host CSS sets height; `--spagbol-sequencer-label-width` controls the right label column. All internal classes extend `spagbol-sequencer`.
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
<SpagbolSequencer
|
|
41
|
+
ariaLabel="My pattern" rows={2} steps={8}
|
|
42
|
+
rowLabel={(row) => `row ${row + 1}`}
|
|
43
|
+
itemsForRow={(row) => projectAppRow(pattern, row)}
|
|
44
|
+
onCreateItem={(row, start, end) => addAppItem(row, start, end)}
|
|
45
|
+
onUpdateItem={(row, id, patch) => updateAppItem(row, id, patch)}
|
|
46
|
+
onDeleteItem={(row, id) => deleteAppItem(row, id)}
|
|
47
|
+
/>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Joystick
|
|
51
|
+
|
|
52
|
+
`SpagbolControlJoystick` accepts controlled normalized `x`, `y`, and `onChange(x, y)`. Left/bottom are 0; right/top are 1. Optional `elasticity` defaults to 0 (hold position); a positive value animates return to centre at that normalized-unit-per-second speed. This is UI animation, not a graph clock. Pointer release, cancellation and lost capture finish dragging; unmount cancels its animation. The adapter owns saved positions, range/inverse mapping, MIDI and history.
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
<SpagbolControlJoystick
|
|
56
|
+
x={appPosition.x} y={appPosition.y}
|
|
57
|
+
elasticity={0}
|
|
58
|
+
onChange={(x, y) => saveAppPosition(x, y)}
|
|
59
|
+
/>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Each component keeps its canonical root class when a host supplies `className`. Colors support existing Teia tokens with Spagbol/default fallbacks. Host wrappers remain responsible for read-only policy, graph history and projected-instance identity, as with the existing Slider/Button controls.
|
|
63
|
+
|
|
64
|
+
## Teia integration
|
|
65
|
+
|
|
66
|
+
`editor/src/editor/TeiaNodeContent.tsx` adapts Teia's existing data directly:
|
|
67
|
+
|
|
68
|
+
- Custom Wave retains `CustomWaveSettings`, normalization, bank selection (including keys 1–8), base-level/range mapping, sustain/playback modes, grid grouping, live playhead overlays and throttled history-aware audio edits. Shared UI receives only view points, chrome slots and callbacks.
|
|
69
|
+
- Sequencer retains numeric parameter keys, row labels/ports, Trigger/Gate conversion, fractional step positions, velocities and hidden-data retention. Its adapter maps slot/velocity to view id/level and maps callbacks back into the original parameter edits.
|
|
70
|
+
- Roll uses the same Sequencer surface with Teia's pitch labels and note storage adapter. No musical data or scale system moves into Spagbol.
|
|
71
|
+
- Joystick retains saved x/y, ranges/inverses, MIDI and elasticity configuration; its shared component handles the pointer and return animation.
|
|
72
|
+
|
|
73
|
+
Teia consumes the published `@pauloricca/spagbol@0.1.3` registry package, pinned with matching lockfile integrity. The minimal Spagbol example is updated to consume the same package version. The package includes these language/UI docs.
|
|
74
|
+
|
|
75
|
+
## Validation and handoff boundary
|
|
76
|
+
|
|
77
|
+
Static library compilation, Teia compilation/build, package integrity, documentation coverage and the FM-1 source audit are the validation targets. No browser, audio or functional interaction test is implied by those checks. No Rust/WASM code changes; FM-1 status/version claims are unchanged because data adapters still issue the same existing edits and compile the same application models.
|
|
78
|
+
|
|
79
|
+
For Teia's Docker development setup, update the installed dependency before exercising the controls: its startup `npm ci` installs the vendored package. A user-initiated `docker compose restart editor` performs that startup step; this task does not restart the container. After it is ready, use an ordinary refresh if the existing tab does not recover through hot reload. Check point insertion/drag/deletion, wave bank/sustain edits, sequence painting/movement/resizing/velocity/row labels, piano-roll edits, joystick hold/elastic return, undo and saved-patch reload. Visual Visual adoption and interactive cross-app validation remain future work.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pauloricca/spagbol",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "node scripts/dev.mjs",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"dist",
|
|
24
24
|
"src",
|
|
25
25
|
"README.md",
|
|
26
|
-
"LICENSE"
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"docs/language"
|
|
27
28
|
],
|
|
28
29
|
"main": "./dist/index.js",
|
|
29
30
|
"types": "./dist/index.d.ts",
|
|
@@ -33,7 +34,8 @@
|
|
|
33
34
|
"import": "./dist/index.js"
|
|
34
35
|
},
|
|
35
36
|
"./styles.css": "./dist/styles.css",
|
|
36
|
-
"./controls.css": "./dist/controls.css"
|
|
37
|
+
"./controls.css": "./dist/controls.css",
|
|
38
|
+
"./editors.css": "./dist/editors.css"
|
|
37
39
|
},
|
|
38
40
|
"sideEffects": [
|
|
39
41
|
"**/*.css"
|
package/src/core/controller.ts
CHANGED
|
@@ -773,9 +773,9 @@ export class GraphEditor {
|
|
|
773
773
|
convertNodeToArea(nodeId: string): string | undefined {
|
|
774
774
|
if (!this.editing || !this.graph.nodes.some(node => node.id === nodeId)) return;
|
|
775
775
|
const id = createId('area');
|
|
776
|
-
this.edit('Convert node to
|
|
776
|
+
this.edit('Convert node to Container', 'structure', graph => {
|
|
777
777
|
const node = graph.nodes.find(node => node.id === nodeId)!;
|
|
778
|
-
graph.areas.push({ id, title: node.label ?? '
|
|
778
|
+
graph.areas.push({ id, title: node.label ?? 'Container', position: { ...node.position }, size: node.size ?? { width: 420, height: 280 } });
|
|
779
779
|
removeNodes(graph, [nodeId]);
|
|
780
780
|
});
|
|
781
781
|
this.select({ areaIds: [id] });
|
|
@@ -787,10 +787,10 @@ export class GraphEditor {
|
|
|
787
787
|
): string | undefined {
|
|
788
788
|
if (!this.editing) return;
|
|
789
789
|
const id = createId("area");
|
|
790
|
-
this.edit("Add
|
|
790
|
+
this.edit("Add Container", "layout", (graph) => {
|
|
791
791
|
graph.areas.push({
|
|
792
792
|
id,
|
|
793
|
-
title: "
|
|
793
|
+
title: "Container",
|
|
794
794
|
position: { ...position },
|
|
795
795
|
size: { ...size },
|
|
796
796
|
});
|
package/src/core/model.ts
CHANGED
|
@@ -66,9 +66,10 @@ export interface GraphLink {
|
|
|
66
66
|
enabled?: boolean;
|
|
67
67
|
data?: NodeData;
|
|
68
68
|
}
|
|
69
|
+
/** Shared area structure: plain visual Containers or application-owned execution areas. */
|
|
69
70
|
export interface GraphArea {
|
|
70
71
|
id: string;
|
|
71
|
-
/**
|
|
72
|
+
/** Optional owner supplies execution-area behaviour; no owner means a visual Container. */
|
|
72
73
|
ownerNodeId?: string;
|
|
73
74
|
title: string;
|
|
74
75
|
position: Point;
|