From 47bb38627c5230d8a0f9cfb537b75953577d2c5d Mon Sep 17 00:00:00 2001 From: Yona Appletree Date: Fri, 31 Jul 2026 14:55:34 -0700 Subject: [PATCH 1/7] feat(lpc-wire): ButtonEvent + OutputTestPattern node commands, wire proto 5 Two new WireNodeCommand variants riding the runtime node-command channel (docs/adr/2026-07-27-runtime-node-command-channel.md growth policy): ButtonEvent { Click | Press { press_id, ttl_ms } | Release { press_id } } and OutputTestPattern { Clear | Solid { r, g, b }, ttl_ms }. Playlist's exhaustive match gains a wildcard reject arm (normal data-level rejection for unsupported commands). Runtime implementations follow in later commits. Plan: lp2025/2026-07-31-1025-node-actions Co-Authored-By: Claude Fable 5 --- .../src/nodes/playlist/playlist_node.rs | 1 + lp-core/lpc-wire/src/lib.rs | 5 +- lp-core/lpc-wire/src/project_command/mod.rs | 4 +- .../src/project_command/node_command.rs | 106 ++++++++++++++++++ lp-core/lpc-wire/src/server/hello.rs | 4 +- 5 files changed, 116 insertions(+), 4 deletions(-) diff --git a/lp-core/lpc-engine/src/nodes/playlist/playlist_node.rs b/lp-core/lpc-engine/src/nodes/playlist/playlist_node.rs index d21c4f353..26ca82a74 100644 --- a/lp-core/lpc-engine/src/nodes/playlist/playlist_node.rs +++ b/lp-core/lpc-engine/src/nodes/playlist/playlist_node.rs @@ -224,6 +224,7 @@ impl NodeRuntime for PlaylistNode { self.pending_activate = Some(*entry); Ok(()) } + _ => Err(NodeError::msg("playlist does not support this command")), } } diff --git a/lp-core/lpc-wire/src/lib.rs b/lp-core/lpc-wire/src/lib.rs index 54b061830..40aa7b47f 100644 --- a/lp-core/lpc-wire/src/lib.rs +++ b/lp-core/lpc-wire/src/lib.rs @@ -46,8 +46,9 @@ pub use project::{ WireRuntimeBufferPayload, WireTextureFormat, }; pub use project_command::{ - WireCreateNodeRequest, WireCreateNodeResponse, WireNodeCommand, WireNodeCommandResponse, - WireProjectCommand, WireProjectCommandResponse, WireRemoveNodeRequest, WireRemoveNodeResponse, + WireButtonEvent, WireCreateNodeRequest, WireCreateNodeResponse, WireNodeCommand, + WireNodeCommandResponse, WireOutputTestPattern, WireProjectCommand, WireProjectCommandResponse, + WireRemoveNodeRequest, WireRemoveNodeResponse, }; pub use project_inventory::{ WireProjectInventoryReadRequest, WireProjectInventoryReadResponse, diff --git a/lp-core/lpc-wire/src/project_command/mod.rs b/lp-core/lpc-wire/src/project_command/mod.rs index c6ac1e8f1..e822c9205 100644 --- a/lp-core/lpc-wire/src/project_command/mod.rs +++ b/lp-core/lpc-wire/src/project_command/mod.rs @@ -6,6 +6,8 @@ mod project_command; mod remove_node; pub use create_node::{WireCreateNodeRequest, WireCreateNodeResponse}; -pub use node_command::{WireNodeCommand, WireNodeCommandResponse}; +pub use node_command::{ + WireButtonEvent, WireNodeCommand, WireNodeCommandResponse, WireOutputTestPattern, +}; pub use project_command::{WireProjectCommand, WireProjectCommandResponse}; pub use remove_node::{WireRemoveNodeRequest, WireRemoveNodeResponse}; diff --git a/lp-core/lpc-wire/src/project_command/node_command.rs b/lp-core/lpc-wire/src/project_command/node_command.rs index 1ac68699a..7cded2952 100644 --- a/lp-core/lpc-wire/src/project_command/node_command.rs +++ b/lp-core/lpc-wire/src/project_command/node_command.rs @@ -26,6 +26,43 @@ pub enum WireNodeCommand { /// same u32 `PlaylistState.active_entry` reports) the active entry, /// resetting the entry clock exactly as a trigger switch does. PlaylistActivateEntry { entry: u32 }, + /// Synthetic button event addressed to a button node runtime. + ButtonEvent { event: WireButtonEvent }, + /// Put an output node into (or out of) test-pattern mode. Sustained + /// patterns auto-expire at `ttl_ms` unless renewed by re-sending the + /// command. + OutputTestPattern { + pattern: WireOutputTestPattern, + ttl_ms: u32, + }, +} + +/// A synthetic button event addressed to a button node runtime. +/// +/// Externally tagged, snake_case, like every project-command payload. +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum WireButtonEvent { + /// Minimal down-then-up pair (down one produce, up the next). + Click, + /// Begin/renew a sustained synthetic hold. Auto-releases at `ttl_ms` + /// unless renewed by re-sending with the same `press_id`. + Press { press_id: u32, ttl_ms: u32 }, + /// End the synthetic hold started with `press_id`. + Release { press_id: u32 }, +} + +/// An output node's test-pattern payload. +/// +/// Externally tagged, snake_case, like every project-command payload. +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum WireOutputTestPattern { + /// End test-pattern mode immediately (ignores `ttl_ms`). + Clear, + /// Fill every pixel with one color. Auto-expires at `ttl_ms` unless + /// renewed by re-sending the command. + Solid { r: u8, g: u8, b: u8 }, } /// Outcome of a node command. @@ -58,6 +95,75 @@ mod tests { assert_eq!(back, command); } + #[test] + fn button_event_click_round_trips() { + let command = WireNodeCommand::ButtonEvent { + event: WireButtonEvent::Click, + }; + let json = serde_json::to_string(&command).unwrap(); + assert!(json.contains("button_event")); + assert!(json.contains("click")); + let back: WireNodeCommand = serde_json::from_str(&json).unwrap(); + assert_eq!(back, command); + } + + #[test] + fn button_event_press_round_trips() { + let command = WireNodeCommand::ButtonEvent { + event: WireButtonEvent::Press { + press_id: 7, + ttl_ms: 500, + }, + }; + let json = serde_json::to_string(&command).unwrap(); + assert!(json.contains("button_event")); + assert!(json.contains("press")); + let back: WireNodeCommand = serde_json::from_str(&json).unwrap(); + assert_eq!(back, command); + } + + #[test] + fn button_event_release_round_trips() { + let command = WireNodeCommand::ButtonEvent { + event: WireButtonEvent::Release { press_id: 7 }, + }; + let json = serde_json::to_string(&command).unwrap(); + assert!(json.contains("button_event")); + assert!(json.contains("release")); + let back: WireNodeCommand = serde_json::from_str(&json).unwrap(); + assert_eq!(back, command); + } + + #[test] + fn output_test_pattern_clear_round_trips() { + let command = WireNodeCommand::OutputTestPattern { + pattern: WireOutputTestPattern::Clear, + ttl_ms: 0, + }; + let json = serde_json::to_string(&command).unwrap(); + assert!(json.contains("output_test_pattern")); + assert!(json.contains("clear")); + let back: WireNodeCommand = serde_json::from_str(&json).unwrap(); + assert_eq!(back, command); + } + + #[test] + fn output_test_pattern_solid_round_trips() { + let command = WireNodeCommand::OutputTestPattern { + pattern: WireOutputTestPattern::Solid { + r: 255, + g: 128, + b: 0, + }, + ttl_ms: 2000, + }; + let json = serde_json::to_string(&command).unwrap(); + assert!(json.contains("output_test_pattern")); + assert!(json.contains("solid")); + let back: WireNodeCommand = serde_json::from_str(&json).unwrap(); + assert_eq!(back, command); + } + #[test] fn node_command_response_round_trips_both_outcomes() { let accepted = WireNodeCommandResponse::Accepted; diff --git a/lp-core/lpc-wire/src/server/hello.rs b/lp-core/lpc-wire/src/server/hello.rs index f6a127da4..c6209170c 100644 --- a/lp-core/lpc-wire/src/server/hello.rs +++ b/lp-core/lpc-wire/src/server/hello.rs @@ -23,6 +23,8 @@ use serde::{Deserialize, Serialize}; /// /// # History /// +/// - 5: `WireNodeCommand::ButtonEvent` + `OutputTestPattern` node action +/// events (`docs/adr/2026-07-31-node-action-events.md`). /// - 4: `OutputDriverOptionsConfig.brightness` removed. Brightness is a /// fixture-level control only; the output node no longer carries one. /// - 3: merge of the two independent "2" bumps below — one wire surface @@ -35,7 +37,7 @@ use serde::{Deserialize, Serialize}; /// runtime command channel (playlist activate-entry; /// `docs/adr/2026-07-27-runtime-node-command-channel.md`). /// - 1: hello handshake introduced. -pub const WIRE_PROTO_VERSION: u32 = 4; +pub const WIRE_PROTO_VERSION: u32 = 5; /// Unsolicited/boot-time server identity and version report. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] From 25c3581f360fb1a190a55bbd9b4d9dad82c95e67 Mon Sep 17 00:00:00 2001 From: Yona Appletree Date: Fri, 31 Jul 2026 15:09:06 -0700 Subject: [PATCH 2/7] =?UTF-8?q?feat(lpc-engine):=20output=20test-pattern?= =?UTF-8?q?=20mode=20=E2=80=94=20graph=20bypass=20with=20TTL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OutputTestPattern handling on OutputNode: Solid stores (rgb, expires_at) — rejected before a first frame establishes the channel extent; consume skips the graph resolve entirely while the pattern holds and repaints the last-established samples; expiry falls through to the normal resolve the same frame (no black frame); Clear is idempotent. Re-send replaces and renews atomically (TTL-renewal principle, default 2000 ms). Plan: lp2025/2026-07-31-1025-node-actions Co-Authored-By: Claude Fable 5 --- .../src/nodes/output/output_node.rs | 476 +++++++++++++++++- 1 file changed, 454 insertions(+), 22 deletions(-) diff --git a/lp-core/lpc-engine/src/nodes/output/output_node.rs b/lp-core/lpc-engine/src/nodes/output/output_node.rs index 75587dacd..df520f192 100644 --- a/lp-core/lpc-engine/src/nodes/output/output_node.rs +++ b/lp-core/lpc-engine/src/nodes/output/output_node.rs @@ -16,10 +16,32 @@ use crate::resource::{ RuntimeChannelSampleFormat, }; +/// How long a solid test pattern holds when the command carries no explicit TTL. +/// +/// A command channel is fire-and-forget: if the client that lit the pattern +/// goes away, the pattern must expire on its own rather than strand an +/// installation in test mode. +const DEFAULT_TEST_PATTERN_TTL_MS: u32 = 2000; + +/// A live solid test pattern: the wire color plus the frame time it lapses at. +/// +/// Only the `Solid` wire variant is ever stored — `Clear` is the absence of +/// this state, not a value of it. +#[derive(Clone, Copy, Debug, PartialEq)] +struct TestPattern { + /// The commanded 8-bit color, kept in wire units so a re-send with the + /// same color compares equal without round-trip scaling. + rgb: [u8; 3], + /// Engine frame time (seconds) at which the pattern lapses. Same clock as + /// [`TickContext::time_seconds`] and `handle_command`'s `time_s`. + expires_at_s: f32, +} + /// Output node that owns the materialized control sample buffer. pub struct OutputNode { channel_buffer_id: Option, control_samples: Vec, + test_pattern: Option, } impl OutputNode { @@ -28,12 +50,58 @@ impl OutputNode { Self { channel_buffer_id: None, control_samples: Vec::new(), + test_pattern: None, } } pub fn channel_buffer_id(&self) -> Option { self.channel_buffer_id } + + /// Overwrite every established sample with one color, in place. + /// + /// Keeps the LAST-ESTABLISHED sample count: a test pattern never resizes + /// the channel extent, it only repaints it, so the flush path sees exactly + /// the buffer shape the real render produced. + fn fill_solid(&mut self, rgb: [u8; 3]) { + // 8-bit unorm to 16-bit unorm: 0..=255 maps onto 0..=65535 exactly. + let rgb16 = [ + u16::from(rgb[0]) * 257, + u16::from(rgb[1]) * 257, + u16::from(rgb[2]) * 257, + ]; + for (index, sample) in self.control_samples.iter_mut().enumerate() { + *sample = rgb16[index % 3]; + } + } + + /// Copy `control_samples` into the runtime buffer and mark it dirty for this frame. + /// + /// Shared by the render path and the test-pattern bypass so both publish + /// byte-identical buffer kind, metadata, and revision. + fn publish_channel_buffer(&self, ctx: &mut TickContext<'_>) -> Result<(), NodeError> { + let buffer_id = self + .channel_buffer_id + .ok_or_else(|| NodeError::msg("output channel buffer not initialized"))?; + ctx.with_runtime_buffer_mut(buffer_id, ctx.revision(), |buffer| { + buffer.kind = RuntimeBufferKind::OutputChannels; + buffer.metadata = RuntimeBufferMetadata::OutputChannels { + channels: (self.control_samples.len() / 3) as u32, + sample_format: RuntimeChannelSampleFormat::U16, + }; + buffer + .bytes + .resize(self.control_samples.len().saturating_mul(2), 0); + for (chunk, sample) in buffer + .bytes + .chunks_exact_mut(2) + .zip(self.control_samples.iter()) + { + chunk.copy_from_slice(&sample.to_le_bytes()); + } + Ok(()) + }) + } } pub fn output_input_path() -> SlotPath { @@ -57,7 +125,63 @@ impl NodeRuntime for OutputNode { self.channel_buffer_id } + /// Put this output into (or out of) test-pattern mode. + /// + /// Queue-in-command / apply-in-consume: this only records intent against + /// the command's frame time; the bypass itself happens in + /// [`NodeRuntime::consume`]. + fn handle_command( + &mut self, + command: &lpc_wire::WireNodeCommand, + time_s: f32, + ) -> Result<(), NodeError> { + match command { + lpc_wire::WireNodeCommand::OutputTestPattern { pattern, ttl_ms } => match pattern { + // Idempotent, and deliberately accepted with nothing active: + // a "stop testing" click must never fail just because the + // pattern already lapsed on its own. + lpc_wire::WireOutputTestPattern::Clear => { + self.test_pattern = None; + Ok(()) + } + lpc_wire::WireOutputTestPattern::Solid { r, g, b } => { + if self.control_samples.is_empty() { + return Err(NodeError::msg( + "output has no established channel extent yet", + )); + } + let ttl_ms = if *ttl_ms == 0 { + DEFAULT_TEST_PATTERN_TTL_MS + } else { + *ttl_ms + }; + // Replace AND renew atomically: a re-send is how the + // client holds a pattern alive. + self.test_pattern = Some(TestPattern { + rgb: [*r, *g, *b], + expires_at_s: time_s + (ttl_ms as f32 / 1000.0), + }); + Ok(()) + } + }, + _ => Err(NodeError::msg("output does not support this command")), + } + } + fn consume(&mut self, ctx: &mut TickContext<'_>) -> Result<(), NodeError> { + if let Some(pattern) = self.test_pattern { + if ctx.time_seconds() < pattern.expires_at_s { + // Bypass, not overwrite: the graph resolve is skipped + // entirely, so upstream demand from this root stops while the + // pattern holds. Other outputs are unaffected. + self.fill_solid(pattern.rgb); + return self.publish_channel_buffer(ctx); + } + // Lapsed. Drop it and render the graph THIS frame — falling + // through avoids a black frame at the expiry boundary. + self.test_pattern = None; + } + let prod = ctx .resolve(QueryKey::ConsumedSlot { node: ctx.node_id(), @@ -87,28 +211,7 @@ impl NodeRuntime for OutputNode { ); let _layout = ctx.render_control(control, &request, target)?; - let buffer_id = self - .channel_buffer_id - .ok_or_else(|| NodeError::msg("output channel buffer not initialized"))?; - ctx.with_runtime_buffer_mut(buffer_id, ctx.revision(), |buffer| { - buffer.kind = RuntimeBufferKind::OutputChannels; - buffer.metadata = RuntimeBufferMetadata::OutputChannels { - channels: (self.control_samples.len() / 3) as u32, - sample_format: RuntimeChannelSampleFormat::U16, - }; - buffer - .bytes - .resize(self.control_samples.len().saturating_mul(2), 0); - for (chunk, sample) in buffer - .bytes - .chunks_exact_mut(2) - .zip(self.control_samples.iter()) - { - chunk.copy_from_slice(&sample.to_le_bytes()); - } - Ok(()) - })?; - Ok(()) + self.publish_channel_buffer(ctx) } fn destroy(&mut self, _ctx: &mut DestroyCtx<'_>) -> Result<(), NodeError> { @@ -123,3 +226,332 @@ impl NodeRuntime for OutputNode { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + use alloc::string::{String, ToString}; + use alloc::vec; + + use lpc_model::{ + ControlExtent, ControlProduct, ControlSampleLayout, LpValue, NodeId, ProductRef, + SlotShapeRegistry, + }; + use lpc_wire::{WireButtonEvent, WireNodeCommand, WireOutputTestPattern}; + + use crate::dataflow::resolver::{Production, ProductionSource, ResolveError, TickResolver}; + use crate::products::visual::{RenderTextureRequest, TextureRenderProduct, VisualProduct}; + use crate::resource::RuntimeBuffer; + + const BUFFER: RuntimeBufferId = RuntimeBufferId::new(1); + /// One RGB lamp: the smallest extent that still exercises the triplet layout. + const EXTENT: ControlExtent = ControlExtent::new(1, 3); + + fn node_id() -> NodeId { + NodeId::new(1) + } + + /// Minimal [`TickResolver`] standing in for the engine's session bridge. + /// + /// It counts `resolve` calls (that is how "the graph was consulted" is + /// asserted) and paints whatever `graph_color` currently is, so a color + /// change that does NOT reach the buffer proves the bypass. + struct FakeResolver { + buffer: RuntimeBuffer, + buffer_frame: Revision, + resolve_calls: u32, + graph_color: [u16; 3], + } + + impl FakeResolver { + fn new() -> Self { + Self { + buffer: RuntimeBuffer::output_channels_u16(0, Vec::new()), + buffer_frame: Revision::default(), + resolve_calls: 0, + graph_color: [1000, 2000, 3000], + } + } + + /// The published buffer decoded back into u16 samples. + fn published_samples(&self) -> Vec { + self.buffer + .bytes + .chunks_exact(2) + .map(|c| u16::from_le_bytes([c[0], c[1]])) + .collect() + } + } + + impl TickResolver for FakeResolver { + fn resolve(&mut self, _query: QueryKey) -> Result { + self.resolve_calls += 1; + Ok(Production::leaf( + WithRevision::new( + Revision::new(1), + LpValue::Product(ProductRef::Control(ControlProduct::new( + node_id(), + 0, + EXTENT, + ))), + ), + ProductionSource::Literal, + )) + } + + fn publish_produced_slot( + &mut self, + _node: NodeId, + _slot: SlotPath, + _production: Production, + ) -> Result<(), ResolveError> { + Ok(()) + } + + fn render_texture( + &mut self, + _product: VisualProduct, + _request: &RenderTextureRequest, + ) -> Result { + Err(ResolveError::new(String::from( + "fake resolver renders no textures", + ))) + } + + fn render_control( + &mut self, + _product: ControlProduct, + _request: &ControlRenderRequest, + target: ControlRenderTarget<'_>, + ) -> Result { + for (index, sample) in target.samples.iter_mut().enumerate() { + *sample = self.graph_color[index % 3]; + } + Ok(ControlSampleLayout::empty()) + } + + fn runtime_buffer_mut( + &mut self, + id: RuntimeBufferId, + frame: Revision, + ) -> Result<&mut RuntimeBuffer, ResolveError> { + if id != BUFFER { + return Err(ResolveError::new(String::from("unknown runtime buffer"))); + } + self.buffer_frame = frame; + Ok(&mut self.buffer) + } + } + + /// An output whose buffer id is already assigned (stands in for `init_resources`). + fn output_node() -> OutputNode { + let mut node = OutputNode::new(); + node.channel_buffer_id = Some(BUFFER); + node + } + + /// Run one `consume` at `time_s`, as the engine's tick would. + fn consume_at( + node: &mut OutputNode, + resolver: &mut FakeResolver, + frame: Revision, + time_s: f32, + ) -> Result<(), NodeError> { + let shapes = SlotShapeRegistry::default(); + let mut ctx = TickContext::with_render_services( + node_id(), + frame, + resolver, + &shapes, + None, + None, + time_s, + ); + node.consume(&mut ctx) + } + + fn solid(r: u8, g: u8, b: u8, ttl_ms: u32) -> WireNodeCommand { + WireNodeCommand::OutputTestPattern { + pattern: WireOutputTestPattern::Solid { r, g, b }, + ttl_ms, + } + } + + fn clear() -> WireNodeCommand { + WireNodeCommand::OutputTestPattern { + pattern: WireOutputTestPattern::Clear, + ttl_ms: 0, + } + } + + #[test] + fn solid_is_rejected_before_any_frame_has_been_rendered() { + let mut node = output_node(); + + let err = node + .handle_command(&solid(255, 0, 0, 1000), 0.0) + .expect_err("no established extent yet"); + + assert!(err.to_string().contains("channel extent"), "{err}"); + assert_eq!(node.test_pattern, None); + } + + #[test] + fn solid_pattern_replaces_graph_output_at_the_established_sample_count() { + let mut node = output_node(); + let mut resolver = FakeResolver::new(); + + consume_at(&mut node, &mut resolver, Revision::new(1), 0.0).expect("graph frame"); + assert_eq!(resolver.published_samples(), vec![1000, 2000, 3000]); + + node.handle_command(&solid(255, 128, 0, 1000), 0.0) + .expect("accepted after a rendered frame"); + + // A graph color change from here on must NOT reach the buffer. + resolver.graph_color = [4000, 5000, 6000]; + let before = resolver.resolve_calls; + consume_at(&mut node, &mut resolver, Revision::new(2), 0.1).expect("pattern frame"); + + assert_eq!( + resolver.published_samples(), + vec![255 * 257, 128 * 257, 0], + "buffer should hold the solid color as u16 unorm triplets", + ); + assert_eq!( + resolver.resolve_calls, before, + "the graph resolve must be skipped entirely while a pattern is active", + ); + assert_eq!( + resolver.buffer.metadata, + RuntimeBufferMetadata::OutputChannels { + channels: 1, + sample_format: RuntimeChannelSampleFormat::U16, + }, + ); + assert_eq!(resolver.buffer.kind, RuntimeBufferKind::OutputChannels); + assert_eq!( + resolver.buffer_frame, + Revision::new(2), + "the bypass must mark the buffer dirty for this frame, like the render path", + ); + } + + #[test] + fn resending_a_different_color_replaces_the_active_pattern() { + let mut node = output_node(); + let mut resolver = FakeResolver::new(); + consume_at(&mut node, &mut resolver, Revision::new(1), 0.0).expect("graph frame"); + + node.handle_command(&solid(255, 0, 0, 1000), 0.0) + .expect("first accepted"); + node.handle_command(&solid(0, 0, 255, 1000), 0.5) + .expect("second accepted"); + + let pattern = node.test_pattern.expect("pattern active"); + assert_eq!(pattern.rgb, [0, 0, 255]); + assert!((pattern.expires_at_s - 1.5).abs() < 1e-6, "{pattern:?}"); + } + + #[test] + fn resending_the_same_color_renews_the_expiry() { + let mut node = output_node(); + let mut resolver = FakeResolver::new(); + consume_at(&mut node, &mut resolver, Revision::new(1), 0.0).expect("graph frame"); + + node.handle_command(&solid(255, 0, 0, 1000), 0.0) + .expect("first accepted"); + let first = node.test_pattern.expect("pattern active").expires_at_s; + node.handle_command(&solid(255, 0, 0, 1000), 0.75) + .expect("renewal accepted"); + let renewed = node.test_pattern.expect("pattern active"); + + assert_eq!(renewed.rgb, [255, 0, 0]); + assert!( + renewed.expires_at_s > first, + "renewal should push the expiry out: {first} -> {}", + renewed.expires_at_s, + ); + } + + #[test] + fn a_zero_ttl_falls_back_to_the_default_hold() { + let mut node = output_node(); + let mut resolver = FakeResolver::new(); + consume_at(&mut node, &mut resolver, Revision::new(1), 0.0).expect("graph frame"); + + node.handle_command(&solid(255, 0, 0, 0), 1.0) + .expect("accepted"); + + let pattern = node.test_pattern.expect("pattern active"); + let expected = 1.0 + (DEFAULT_TEST_PATTERN_TTL_MS as f32 / 1000.0); + assert!( + (pattern.expires_at_s - expected).abs() < 1e-6, + "{pattern:?}" + ); + } + + #[test] + fn an_expired_pattern_renders_the_graph_again_on_the_same_frame() { + let mut node = output_node(); + let mut resolver = FakeResolver::new(); + consume_at(&mut node, &mut resolver, Revision::new(1), 0.0).expect("graph frame"); + + node.handle_command(&solid(255, 0, 0, 1000), 0.0) + .expect("accepted"); + resolver.graph_color = [4000, 5000, 6000]; + + // t = 2.0 is past the 1.0 s expiry: no black frame, straight back to the graph. + consume_at(&mut node, &mut resolver, Revision::new(2), 2.0).expect("post-expiry frame"); + + assert_eq!(resolver.published_samples(), vec![4000, 5000, 6000]); + assert_eq!(node.test_pattern, None, "the lapsed pattern is dropped"); + } + + #[test] + fn clear_returns_to_graph_output_immediately() { + let mut node = output_node(); + let mut resolver = FakeResolver::new(); + consume_at(&mut node, &mut resolver, Revision::new(1), 0.0).expect("graph frame"); + + node.handle_command(&solid(255, 0, 0, 10_000), 0.0) + .expect("accepted"); + resolver.graph_color = [4000, 5000, 6000]; + node.handle_command(&clear(), 0.1).expect("clear accepted"); + + // Still well inside the TTL: only the Clear can explain graph output. + consume_at(&mut node, &mut resolver, Revision::new(2), 0.2).expect("post-clear frame"); + + assert_eq!(node.test_pattern, None); + assert_eq!(resolver.published_samples(), vec![4000, 5000, 6000]); + } + + #[test] + fn clear_is_accepted_with_no_pattern_active() { + let mut node = output_node(); + + node.handle_command(&clear(), 0.0) + .expect("clear is idempotent, even before any frame"); + + assert_eq!(node.test_pattern, None); + } + + #[test] + fn unrelated_commands_are_rejected() { + let mut node = output_node(); + + assert!( + node.handle_command(&WireNodeCommand::PlaylistActivateEntry { entry: 1 }, 0.0) + .is_err(), + ); + assert!( + node.handle_command( + &WireNodeCommand::ButtonEvent { + event: WireButtonEvent::Click, + }, + 0.0, + ) + .is_err(), + ); + } +} From 935a5321a0c3c9ced00cd3fae56a0477687292e2 Mon Sep 17 00:00:00 2001 From: Yona Appletree Date: Fri, 31 Jul 2026 15:09:35 -0700 Subject: [PATCH 3/7] =?UTF-8?q?feat(lpc-engine):=20synthetic=20button=20ev?= =?UTF-8?q?ents=20=E2=80=94=20click=20queue=20+=20TTL'd=20press=20set?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ButtonEvent handling on ButtonNode: clicks queue and phase one produce at a time; sustained presses live in a fixed 4-slot set keyed by client press_id, renewed by re-send and swept against the produce clock (deadline stamped on first produce — handle_command's frame clock is a different domain). Effective pressed = hardware OR click OR any active press; down/held/up edges derive from changes in effective state, so overlapping sources add no spurious edges and neither source releases the other. Edge sequence is now node-owned: with two sources feeding one logical button, forwarding the hardware event's own sequence could collide on (id, seq) and defeat consumer dedup. Plan: lp2025/2026-07-31-1025-node-actions Co-Authored-By: Claude Fable 5 --- .../src/nodes/button/button_node.rs | 544 +++++++++++++++++- 1 file changed, 518 insertions(+), 26 deletions(-) diff --git a/lp-core/lpc-engine/src/nodes/button/button_node.rs b/lp-core/lpc-engine/src/nodes/button/button_node.rs index 865c331f8..f02acc84e 100644 --- a/lp-core/lpc-engine/src/nodes/button/button_node.rs +++ b/lp-core/lpc-engine/src/nodes/button/button_node.rs @@ -9,12 +9,22 @@ use lpc_model::{ ButtonDefView, ButtonState, ControlMessage, HwEndpointSpec, MapSlot, Revision, SlotAccess, SlotPath, SlotShapeRegistry, SlotShapeRegistryError, }; +use lpc_wire::{WireButtonEvent, WireNodeCommand}; use crate::node::{ DestroyCtx, MemPressureCtx, NodeError, NodeRuntime, PressureLevel, ProduceResult, RuntimeStateShape, TickContext, }; +/// How many synthetic presses a single button node holds at once. +/// +/// Bounded on purpose: the set lives in a fixed array so a misbehaving (or +/// simply chatty) client cannot grow node state on an embedded target. +const SYNTHETIC_PRESS_CAPACITY: usize = 4; + +/// TTL applied when a `Press` command asks for `ttl_ms == 0`. +const DEFAULT_PRESS_TTL_MS: u32 = 5000; + /// Runtime node for `kind = "Button"` artifacts. pub struct ButtonNode { state: ButtonState, @@ -23,6 +33,24 @@ pub struct ButtonNode { opened: Option, held_id_seq: Option<(u32, u32)>, fallback_now_ms: u64, + /// Last debounced hardware level, latched from `poll` edges. + hw_pressed: bool, + /// Merged state published on the previous produce; `down`/`up` edges + /// derive from CHANGES against it. + effective_pressed: bool, + /// Node-owned message sequence, bumped once per effective edge. The + /// hardware event's own sequence is not forwarded any more: with two + /// sources feeding one logical button, only a node-owned counter stays + /// collision-free for consumers that dedup by `(id, seq)`. + edge_seq: u32, + /// Clicks queued by `WireButtonEvent::Click`, applied one produce at a + /// time (queue in `handle_command`, merge in `produce`). + pending_clicks: u8, + /// True while the frame that a queued click made effective is the most + /// recent one, so the next produce releases it. + click_active: bool, + /// Sustained synthetic presses, keyed by client-generated `press_id`. + presses: [Option; SYNTHETIC_PRESS_CAPACITY], } impl ButtonNode { @@ -34,6 +62,12 @@ impl ButtonNode { opened: None, held_id_seq: None, fallback_now_ms: 0, + hw_pressed: false, + effective_pressed: false, + edge_seq: 0, + pending_clicks: 0, + click_active: false, + presses: [None; SYNTHETIC_PRESS_CAPACITY], } } @@ -68,7 +102,12 @@ impl ButtonNode { .map_err(|error| NodeError::msg(format!("open button {}: {error}", config.endpoint)))?; self.input = Some(input); self.opened = Some(opened); + // A reopened endpoint starts from a clean edge state; synthetic + // presses are client-owned and survive, so a live hold re-arms with a + // fresh down edge on the next produce. self.held_id_seq = None; + self.hw_pressed = false; + self.effective_pressed = false; Ok(()) } @@ -80,6 +119,142 @@ impl ButtonNode { self.fallback_now_ms = self.fallback_now_ms.saturating_add(1); self.fallback_now_ms } + + /// One frame of the hardware/synthetic merge. + /// + /// There is ONE logical button: the hardware level, the click phase, and + /// the synthetic press set fold into a single `effective_pressed`, and the + /// `down`/`held`/`up` slots derive from CHANGES in it. Overlapping sources + /// therefore add no spurious edges, and neither source's release ends the + /// other's hold. + /// + /// Split out of [`NodeRuntime::produce`] so the merge rule is unit + /// testable without a live [`TickContext`]. + fn advance_frame( + &mut self, + revision: Revision, + id: u32, + now_ms: u64, + hw_event: Option, + ) { + if let Some(kind) = hw_event { + self.hw_pressed = matches!(kind, ButtonEventKind::Pressed); + } + self.sweep_presses(now_ms); + let click_pressed = self.advance_click_phase(); + let effective = self.hw_pressed || click_pressed || self.has_active_press(); + + let mut down = MapSlot::default(); + let mut up = MapSlot::default(); + if effective != self.effective_pressed { + self.effective_pressed = effective; + self.edge_seq = self.edge_seq.wrapping_add(1); + if effective { + self.held_id_seq = Some((id, self.edge_seq)); + down = one_message_map(revision, id, self.edge_seq); + } else { + self.held_id_seq = None; + up = one_message_map(revision, id, self.edge_seq); + } + } + let held = self + .held_id_seq + .map(|(held_id, seq)| one_message_map(revision, held_id, seq)) + .unwrap_or_default(); + + self.state.down = down; + self.state.held = held; + self.state.up = up; + } + + /// Expire (and lazily stamp) synthetic presses against the produce clock. + /// + /// `handle_command` cannot stamp deadlines itself: its `time_s` is the + /// engine's frame clock, a different domain from the [`TickContext`] time + /// provider `produce` polls the debouncer with. Entries therefore arrive + /// with a TTL budget and get their deadline on the first produce that + /// sees them — which is also what makes a renewal simply clear the stamp. + fn sweep_presses(&mut self, now_ms: u64) { + for slot in &mut self.presses { + let Some(press) = slot.as_mut() else { + continue; + }; + match press.expires_at_ms { + None => { + press.expires_at_ms = Some(now_ms.saturating_add(u64::from(press.ttl_ms))); + } + Some(deadline) if now_ms >= deadline => *slot = None, + Some(_) => {} + } + } + } + + fn has_active_press(&self) -> bool { + self.presses.iter().any(Option::is_some) + } + + /// Click phasing: a queued click is effective for exactly one produce and + /// released on the next, so queued clicks serialize into distinct down/up + /// pairs instead of merging into one long hold. If another source holds + /// the button meanwhile, the click simply dissolves into that hold. + fn advance_click_phase(&mut self) -> bool { + if self.click_active { + self.click_active = false; + return false; + } + if self.pending_clicks == 0 { + return false; + } + self.pending_clicks -= 1; + self.click_active = true; + true + } + + /// Insert or renew a sustained synthetic press. + fn begin_press(&mut self, press_id: u32, ttl_ms: u32) -> Result<(), NodeError> { + let ttl_ms = if ttl_ms == 0 { + DEFAULT_PRESS_TTL_MS + } else { + ttl_ms + }; + if let Some(press) = self + .presses + .iter_mut() + .filter_map(Option::as_mut) + .find(|press| press.press_id == press_id) + { + press.ttl_ms = ttl_ms; + press.expires_at_ms = None; + return Ok(()); + } + let Some(slot) = self.presses.iter_mut().find(|slot| slot.is_none()) else { + return Err(NodeError::msg(format!( + "button already holds {SYNTHETIC_PRESS_CAPACITY} synthetic presses" + ))); + }; + *slot = Some(SyntheticPress { + press_id, + ttl_ms, + expires_at_ms: None, + }); + Ok(()) + } + + /// End a sustained synthetic press. An unknown id is a normal data-level + /// rejection — a Release that lost the race with its own TTL lands here. + fn end_press(&mut self, press_id: u32) -> Result<(), NodeError> { + let Some(slot) = self + .presses + .iter_mut() + .find(|slot| slot.is_some_and(|press| press.press_id == press_id)) + else { + return Err(NodeError::msg(format!( + "button has no synthetic press {press_id}" + ))); + }; + *slot = None; + Ok(()) + } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -95,6 +270,18 @@ struct OpenedButton { stable_ms: u64, } +/// One sustained synthetic press held by the node. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct SyntheticPress { + /// Client-generated id, scoped to this node, pairing Press with Release. + press_id: u32, + /// Lifetime granted by the latest Press command for this id. + ttl_ms: u32, + /// Deadline in the produce clock's domain; `None` until the next produce + /// stamps it (see [`ButtonNode::sweep_presses`]). + expires_at_ms: Option, +} + impl NodeRuntime for ButtonNode { fn produce( &mut self, @@ -105,44 +292,50 @@ impl NodeRuntime for ButtonNode { self.ensure_input(&config, ctx)?; let now_ms = self.next_now_ms(ctx); - let mut down = MapSlot::default(); - let mut up = MapSlot::default(); - let mut held = self - .held_id_seq - .map(|(id, seq)| one_message_map(ctx.revision(), id, seq)) - .unwrap_or_default(); - - if let Some(event) = self + // Poll every frame regardless of synthetic state: the debouncer is a + // sampler, and skipping it would stall the hardware edge detector. + let hw_event = self .input .as_mut() .ok_or_else(|| NodeError::msg("button input missing after open"))? .poll(now_ms) - { - let seq = event.sequence(); - match event.kind() { - ButtonEventKind::Pressed => { - self.held_id_seq = Some((config.id, seq)); - down = one_message_map(ctx.revision(), config.id, seq); - held = one_message_map(ctx.revision(), config.id, seq); - } - ButtonEventKind::Released => { - self.held_id_seq = None; - held = MapSlot::default(); - up = one_message_map(ctx.revision(), config.id, seq); - } - } - } + .map(|event| event.kind()); - self.state.down = down; - self.state.held = held; - self.state.up = up; + self.advance_frame(ctx.revision(), config.id, now_ms, hw_event); Ok(ProduceResult::Produced) } + /// Synthetic button events (the wire runtime command channel): queue the + /// event here, merge it into the one logical button state in `produce`. + /// + /// Writing `down`/`held`/`up` directly would be lost — `produce` rebuilds + /// all three from the poll each frame — so nothing is stamped here. + /// Sustained presses carry a TTL rather than trusting a Release to + /// arrive: a dropped Release would otherwise wedge the button held + /// forever. + fn handle_command(&mut self, command: &WireNodeCommand, _time_s: f32) -> Result<(), NodeError> { + let WireNodeCommand::ButtonEvent { event } = command else { + return Err(NodeError::msg("button does not support this command")); + }; + match event { + WireButtonEvent::Click => { + self.pending_clicks = self.pending_clicks.saturating_add(1); + Ok(()) + } + WireButtonEvent::Press { press_id, ttl_ms } => self.begin_press(*press_id, *ttl_ms), + WireButtonEvent::Release { press_id } => self.end_press(*press_id), + } + } + fn destroy(&mut self, _ctx: &mut DestroyCtx<'_>) -> Result<(), NodeError> { self.input = None; self.opened = None; self.held_id_seq = None; + self.hw_pressed = false; + self.effective_pressed = false; + self.pending_clicks = 0; + self.click_active = false; + self.presses = [None; SYNTHETIC_PRESS_CAPACITY]; Ok(()) } @@ -183,3 +376,302 @@ pub fn button_held_path() -> SlotPath { pub fn button_up_path() -> SlotPath { SlotPath::parse("up").expect("button up path") } + +#[cfg(test)] +mod tests { + use super::*; + use alloc::string::ToString; + + #[test] + fn click_fires_down_and_held_on_one_frame_then_up_on_the_next() { + let mut node = ButtonNode::new(); + node.handle_command(&click(), 0.0).expect("click accepted"); + + frame(&mut node, 0, None); + assert_eq!(down_seq(&node), Some(1)); + assert_eq!(held_seq(&node), Some(1)); + assert!(node.state.up.is_empty()); + + frame(&mut node, 10, None); + assert!(node.state.down.is_empty()); + assert!(node.state.held.is_empty()); + assert_eq!(up_seq(&node), Some(2)); + + frame(&mut node, 20, None); + assert!(node.state.down.is_empty()); + assert!(node.state.held.is_empty()); + assert!(node.state.up.is_empty()); + } + + #[test] + fn queued_clicks_serialize_into_separate_edge_pairs() { + let mut node = ButtonNode::new(); + node.handle_command(&click(), 0.0).expect("first click"); + node.handle_command(&click(), 0.0).expect("second click"); + + frame(&mut node, 0, None); + assert!(!node.state.down.is_empty(), "first click down"); + frame(&mut node, 10, None); + assert!(!node.state.up.is_empty(), "first click up"); + frame(&mut node, 20, None); + assert!(!node.state.down.is_empty(), "second click down"); + frame(&mut node, 30, None); + assert!(!node.state.up.is_empty(), "second click up"); + frame(&mut node, 40, None); + assert!(node.state.down.is_empty()); + assert!(node.state.up.is_empty()); + } + + #[test] + fn press_holds_across_frames_and_release_emits_the_up_edge() { + let mut node = ButtonNode::new(); + node.handle_command(&press(1, 1000), 0.0) + .expect("press accepted"); + + frame(&mut node, 0, None); + assert_eq!(down_seq(&node), Some(1)); + assert_eq!(held_seq(&node), Some(1)); + + for now_ms in [10, 20, 30] { + frame(&mut node, now_ms, None); + assert!(node.state.down.is_empty(), "no repeat down at {now_ms}"); + assert_eq!(held_seq(&node), Some(1), "still held at {now_ms}"); + assert!(node.state.up.is_empty(), "no up at {now_ms}"); + } + + node.handle_command(&release(1), 0.0).expect("release"); + frame(&mut node, 40, None); + assert!(node.state.held.is_empty()); + assert_eq!(up_seq(&node), Some(2)); + } + + #[test] + fn press_auto_releases_at_ttl_expiry() { + let mut node = ButtonNode::new(); + node.handle_command(&press(1, 100), 0.0).expect("press"); + + frame(&mut node, 0, None); + assert_eq!(down_seq(&node), Some(1)); + frame(&mut node, 99, None); + assert_eq!(held_seq(&node), Some(1)); + + frame(&mut node, 100, None); + assert!(node.state.held.is_empty(), "TTL expiry releases the hold"); + assert_eq!(up_seq(&node), Some(2)); + } + + #[test] + fn renewing_a_press_extends_its_ttl() { + let mut node = ButtonNode::new(); + node.handle_command(&press(1, 100), 0.0).expect("press"); + frame(&mut node, 0, None); + + node.handle_command(&press(1, 100), 0.0).expect("renewal"); + frame(&mut node, 50, None); + assert!(node.state.down.is_empty(), "renewal is not a new edge"); + + frame(&mut node, 100, None); + assert_eq!(held_seq(&node), Some(1), "original deadline was extended"); + assert!(node.state.up.is_empty()); + + frame(&mut node, 150, None); + assert!(node.state.held.is_empty(), "renewed deadline expires"); + assert_eq!(up_seq(&node), Some(2)); + } + + #[test] + fn press_with_zero_ttl_uses_the_default_ttl() { + let mut node = ButtonNode::new(); + node.handle_command(&press(1, 0), 0.0).expect("press"); + + frame(&mut node, 0, None); + assert_eq!( + node.presses[0].expect("press stored").expires_at_ms, + Some(u64::from(DEFAULT_PRESS_TTL_MS)) + ); + } + + #[test] + fn hardware_and_synthetic_presses_merge_into_one_logical_button() { + let mut node = ButtonNode::new(); + node.handle_command(&press(1, 10_000), 0.0).expect("press"); + + frame(&mut node, 0, None); + assert_eq!(down_seq(&node), Some(1)); + + // Hardware presses on top of the synthetic hold: no second down. + frame(&mut node, 10, Some(ButtonEventKind::Pressed)); + assert!(node.state.down.is_empty(), "no extra down edge"); + assert_eq!(held_seq(&node), Some(1)); + + // Hardware releases first: the synthetic hold keeps the button down. + frame(&mut node, 20, Some(ButtonEventKind::Released)); + assert!( + node.state.up.is_empty(), + "synthetic hold survives hw release" + ); + assert_eq!(held_seq(&node), Some(1)); + + // Only when the last source ends does the up edge fire. + node.handle_command(&release(1), 0.0).expect("release"); + frame(&mut node, 30, None); + assert!(node.state.held.is_empty()); + assert_eq!(up_seq(&node), Some(2)); + } + + #[test] + fn synthetic_release_during_a_hardware_hold_keeps_the_button_down() { + let mut node = ButtonNode::new(); + + frame(&mut node, 0, Some(ButtonEventKind::Pressed)); + assert_eq!(down_seq(&node), Some(1)); + + node.handle_command(&press(1, 10_000), 0.0).expect("press"); + frame(&mut node, 10, None); + assert!(node.state.down.is_empty(), "no extra down edge"); + + node.handle_command(&release(1), 0.0).expect("release"); + frame(&mut node, 20, None); + assert!( + node.state.up.is_empty(), + "hardware hold survives synthetic release" + ); + assert_eq!(held_seq(&node), Some(1)); + + frame(&mut node, 30, Some(ButtonEventKind::Released)); + assert_eq!(up_seq(&node), Some(2)); + } + + #[test] + fn click_during_an_active_hold_adds_no_edges() { + let mut node = ButtonNode::new(); + node.handle_command(&press(1, 10_000), 0.0).expect("press"); + frame(&mut node, 0, None); + assert_eq!(down_seq(&node), Some(1)); + + node.handle_command(&click(), 0.0).expect("click"); + for now_ms in [10, 20, 30] { + frame(&mut node, now_ms, None); + assert!(node.state.down.is_empty(), "no down at {now_ms}"); + assert!(node.state.up.is_empty(), "no up at {now_ms}"); + assert_eq!(held_seq(&node), Some(1), "still held at {now_ms}"); + } + + node.handle_command(&release(1), 0.0).expect("release"); + frame(&mut node, 40, None); + assert_eq!(up_seq(&node), Some(2), "exactly one up edge for the pair"); + } + + #[test] + fn a_fifth_concurrent_press_is_rejected() { + let mut node = ButtonNode::new(); + for press_id in 1..=4 { + node.handle_command(&press(press_id, 1000), 0.0) + .unwrap_or_else(|e| panic!("press {press_id} accepted: {e}")); + } + + let err = node + .handle_command(&press(5, 1000), 0.0) + .expect_err("overflow rejected"); + assert!(err.to_string().contains("4 synthetic presses"), "{err}"); + + // The full set is untouched by the rejected command. + assert_eq!(node.presses.iter().filter(|slot| slot.is_some()).count(), 4); + assert!( + node.presses + .iter() + .flatten() + .all(|press| press.press_id != 5) + ); + } + + #[test] + fn renewing_an_existing_press_does_not_consume_a_slot() { + let mut node = ButtonNode::new(); + for press_id in 1..=4 { + node.handle_command(&press(press_id, 1000), 0.0) + .expect("press accepted"); + } + + node.handle_command(&press(2, 2000), 0.0) + .expect("renewal of a held id is accepted even when full"); + + assert_eq!(node.presses.iter().filter(|slot| slot.is_some()).count(), 4); + } + + #[test] + fn releasing_an_unknown_press_id_is_rejected_and_leaves_state_untouched() { + let mut node = ButtonNode::new(); + node.handle_command(&press(1, 1000), 0.0).expect("press"); + frame(&mut node, 0, None); + + let err = node + .handle_command(&release(9), 0.0) + .expect_err("unknown id rejected"); + assert!(err.to_string().contains("no synthetic press 9"), "{err}"); + + frame(&mut node, 10, None); + assert_eq!(held_seq(&node), Some(1), "the real hold is untouched"); + assert!(node.state.up.is_empty()); + } + + #[test] + fn non_button_commands_are_rejected() { + let mut node = ButtonNode::new(); + + for command in [ + WireNodeCommand::PlaylistActivateEntry { entry: 1 }, + WireNodeCommand::OutputTestPattern { + pattern: lpc_wire::WireOutputTestPattern::Clear, + ttl_ms: 0, + }, + ] { + let err = node + .handle_command(&command, 0.0) + .expect_err("non-button command rejected"); + assert!(err.to_string().contains("does not support"), "{err}"); + } + } + + const TEST_BUTTON_ID: u32 = 7; + + fn frame(node: &mut ButtonNode, now_ms: u64, hw_event: Option) { + node.advance_frame(Revision::new(1), TEST_BUTTON_ID, now_ms, hw_event); + } + + fn click() -> WireNodeCommand { + WireNodeCommand::ButtonEvent { + event: WireButtonEvent::Click, + } + } + + fn press(press_id: u32, ttl_ms: u32) -> WireNodeCommand { + WireNodeCommand::ButtonEvent { + event: WireButtonEvent::Press { press_id, ttl_ms }, + } + } + + fn release(press_id: u32) -> WireNodeCommand { + WireNodeCommand::ButtonEvent { + event: WireButtonEvent::Release { press_id }, + } + } + + fn down_seq(node: &ButtonNode) -> Option { + message_seq(&node.state.down) + } + + fn held_seq(node: &ButtonNode) -> Option { + message_seq(&node.state.held) + } + + fn up_seq(node: &ButtonNode) -> Option { + message_seq(&node.state.up) + } + + fn message_seq(slot: &MapSlot) -> Option { + slot.entries + .get(&TEST_BUTTON_ID) + .map(|message| message.seq()) + } +} From eb3250c16f620f840bda4cbfe383f2e9e4870ba4 Mon Sep 17 00:00:00 2001 From: Yona Appletree Date: Fri, 31 Jul 2026 16:18:24 -0700 Subject: [PATCH 4/7] feat(lpa-studio): simulate-press and test-pattern face affordances MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ButtonEventOp / OutputTestPatternOp copy the playlist-activate dispatch shape (NodeId resolved at dispatch time, Accepted quiet + verdict-chase, Rejected as warning notice) through a shared dispatch_node_command helper. New hand-built faces: ButtonFace's windowed simulate-press control (tap<300ms → Click; hold → Press + 1s renewals, Release on pointer-up, pointer-capture, TTL-only teardown) and OutputFace's test-pattern toggle (solid white + 1s renewals while on, Clear on off, epoch-guarded loop). Held state wears the live family, pattern-on wears attention — never violet (bound/bus) or green (good/valid), pinned by unit tests. Stories for both faces; baselines stay CI-captured. Plan: lp2025/2026-07-31-1025-node-actions Co-Authored-By: Claude Fable 5 --- .../lpa-studio-core/src/app/node/face/mod.rs | 19 +- .../src/app/node/face/ui_button_face.rs | 107 +++++++ .../src/app/node/face/ui_node_face.rs | 7 +- .../src/app/node/face/ui_output_face.rs | 118 ++++++++ lp-app/lpa-studio-core/src/app/node/mod.rs | 4 +- lp-app/lpa-studio-core/src/app/project/mod.rs | 8 +- .../src/app/project/node/button_event_op.rs | 155 +++++++++++ .../src/app/project/node/mod.rs | 4 + .../src/app/project/node/node_face_builder.rs | 138 +++++++++- .../project/node/output_test_pattern_op.rs | 146 ++++++++++ .../src/app/project/project_controller.rs | 116 +++++++- .../src/app/studio/studio_controller.rs | 46 +++- lp-app/lpa-studio-core/src/lib.rs | 35 +-- .../src/app/node/button_face_stories.rs | 50 ++++ .../src/app/node/face/button_face.rs | 260 ++++++++++++++++++ .../lpa-studio-web/src/app/node/face/mod.rs | 4 + .../src/app/node/face/node_face_body.rs | 30 +- .../src/app/node/face/output_face.rs | 166 +++++++++++ .../src/app/node/face_story_fixtures.rs | 83 +++++- lp-app/lpa-studio-web/src/app/node/mod.rs | 7 +- .../src/app/node/output_face_stories.rs | 50 ++++ 21 files changed, 1488 insertions(+), 65 deletions(-) create mode 100644 lp-app/lpa-studio-core/src/app/node/face/ui_button_face.rs create mode 100644 lp-app/lpa-studio-core/src/app/node/face/ui_output_face.rs create mode 100644 lp-app/lpa-studio-core/src/app/project/node/button_event_op.rs create mode 100644 lp-app/lpa-studio-core/src/app/project/node/output_test_pattern_op.rs create mode 100644 lp-app/lpa-studio-web/src/app/node/button_face_stories.rs create mode 100644 lp-app/lpa-studio-web/src/app/node/face/button_face.rs create mode 100644 lp-app/lpa-studio-web/src/app/node/face/output_face.rs create mode 100644 lp-app/lpa-studio-web/src/app/node/output_face_stories.rs diff --git a/lp-app/lpa-studio-core/src/app/node/face/mod.rs b/lp-app/lpa-studio-core/src/app/node/face/mod.rs index f3c9520a1..d6f91d041 100644 --- a/lp-app/lpa-studio-core/src/app/node/face/mod.rs +++ b/lp-app/lpa-studio-core/src/app/node/face/mod.rs @@ -6,24 +6,33 @@ //! hand-built per node kind; only the front-panel metadata (which slots are //! on the panel, widget kind, range/unit) is data-driven from slot shapes. //! -//! Faces exist for shader, fixture, and playlist nodes, derived from the -//! finished section DTOs in `node_face_builder` (project layer) so a panel -//! control and its backing slot row can never disagree. Every other kind -//! gets `None` and renders today's generic sections — the universal -//! fallback, also always available inside the advanced drawer. +//! Faces exist for shader, fixture, playlist, button, and output nodes, +//! derived from the finished section DTOs in `node_face_builder` (project +//! layer) so a panel control and its backing slot row can never disagree. +//! Every other kind gets `None` and renders today's generic sections — the +//! universal fallback, also always available inside the advanced drawer. +//! +//! The button and output faces are the runtime-command channel's faces: +//! their affordance is not an edit at all but a poke at the live runtime +//! (simulate a press; drive a diagnostic pattern), so they carry a node +//! ADDRESS and action constructors rather than slot addresses. +mod ui_button_face; mod ui_fixture_face; mod ui_fixture_power; mod ui_node_face; +mod ui_output_face; mod ui_panel_control; mod ui_panel_widget; mod ui_playlist_entry; mod ui_playlist_face; mod ui_shader_face; +pub use ui_button_face::UiButtonFace; pub use ui_fixture_face::UiFixtureFace; pub use ui_fixture_power::UiFixturePower; pub use ui_node_face::UiNodeFace; +pub use ui_output_face::UiOutputFace; pub use ui_panel_control::UiPanelControl; pub use ui_panel_widget::UiPanelWidget; pub use ui_playlist_entry::UiPlaylistEntry; diff --git a/lp-app/lpa-studio-core/src/app/node/face/ui_button_face.rs b/lp-app/lpa-studio-core/src/app/node/face/ui_button_face.rs new file mode 100644 index 000000000..d3f9d17c3 --- /dev/null +++ b/lp-app/lpa-studio-core/src/app/node/face/ui_button_face.rs @@ -0,0 +1,107 @@ +//! The button card's permanent face. + +use lpc_wire::WireButtonEvent; + +use crate::{ButtonEventOp, ControllerId, ProjectController, ProjectNodeAddress, UiAction}; + +/// Permanent face for a button node card. +/// +/// The face is one affordance — simulate a press from the Studio, so a show +/// can be rehearsed (and a binding proven) without reaching for the physical +/// button. The endpoint and message id ride along as the button's identity; +/// everything else stays in the advanced drawer's slot view. +/// +/// The face does NOT carry a ready-made action, because a hold's `press_id` +/// is minted per gesture in the view layer. It carries the ADDRESS and the +/// three constructors below, so the op type and its controller routing stay +/// in core while the timing (press window, renewal cadence) stays where the +/// pointer events are. +#[derive(Clone, Debug, PartialEq)] +pub struct UiButtonFace { + /// Stable address of the button node the simulate-press control pokes. + pub node: ProjectNodeAddress, + /// Authored hardware endpoint (`button:gpio:D9`), when the def's row + /// resolved — the button's physical identity, shown beside the control. + pub endpoint: Option, + /// Authored stable message id the button publishes under, when the + /// def's row resolved. + pub id: Option, +} + +impl UiButtonFace { + /// A tap: the minimal down-then-up pair, no hold to manage. + pub fn click_action(&self) -> UiAction { + self.action(WireButtonEvent::Click) + } + + /// Begin — or RENEW — a sustained hold. Renewals re-send this same + /// action (same `press_id`); the runtime auto-releases at `ttl_ms` if + /// they stop, which is what makes an unmounted card safe. + pub fn press_action(&self, press_id: u32, ttl_ms: u32) -> UiAction { + self.action(WireButtonEvent::Press { press_id, ttl_ms }) + } + + /// End the hold started with `press_id`. + pub fn release_action(&self, press_id: u32) -> UiAction { + self.action(WireButtonEvent::Release { press_id }) + } + + fn action(&self, event: WireButtonEvent) -> UiAction { + UiAction::from_op( + ControllerId::new(ProjectController::NODE_ID), + ButtonEventOp { + node: self.node.clone(), + event, + }, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn face() -> UiButtonFace { + UiButtonFace { + node: ProjectNodeAddress::parse("/demo.project/panel.button").unwrap(), + endpoint: Some("button:gpio:D9".to_string()), + id: Some(1), + } + } + + #[test] + fn every_event_targets_the_project_controller_with_the_faces_address() { + let face = face(); + + for action in [ + face.click_action(), + face.press_action(7, 5000), + face.release_action(7), + ] { + assert!(action.is_for_node(ProjectController::NODE_ID)); + let op = action + .op_as::() + .expect("a button event op rides the action"); + assert_eq!(op.node, face.node); + } + } + + #[test] + fn a_renewal_rebuilds_the_identical_press() { + let face = face(); + assert_eq!( + face.press_action(7, 5000).op_as::().unwrap(), + face.press_action(7, 5000).op_as::().unwrap(), + ); + assert_eq!( + face.press_action(7, 5000) + .op_as::() + .unwrap() + .event, + WireButtonEvent::Press { + press_id: 7, + ttl_ms: 5000, + }, + ); + } +} diff --git a/lp-app/lpa-studio-core/src/app/node/face/ui_node_face.rs b/lp-app/lpa-studio-core/src/app/node/face/ui_node_face.rs index 1a5ad89f3..971ee060c 100644 --- a/lp-app/lpa-studio-core/src/app/node/face/ui_node_face.rs +++ b/lp-app/lpa-studio-core/src/app/node/face/ui_node_face.rs @@ -1,6 +1,6 @@ //! The kind-specific face variants a node card can render. -use crate::{UiFixtureFace, UiPlaylistFace, UiShaderFace}; +use crate::{UiButtonFace, UiFixtureFace, UiOutputFace, UiPlaylistFace, UiShaderFace}; /// Kind-specific permanent face for a node card. /// @@ -16,4 +16,9 @@ pub enum UiNodeFace { /// Playlist card: entry strip; the active child's real card renders /// below via the existing [`crate::UiNodeChild`]. Playlist(UiPlaylistFace), + /// Button card: the simulate-press control (a runtime poke, not an + /// edit). + Button(UiButtonFace), + /// Output card: the test-pattern toggle (a runtime poke, not an edit). + Output(UiOutputFace), } diff --git a/lp-app/lpa-studio-core/src/app/node/face/ui_output_face.rs b/lp-app/lpa-studio-core/src/app/node/face/ui_output_face.rs new file mode 100644 index 000000000..2004b1078 --- /dev/null +++ b/lp-app/lpa-studio-core/src/app/node/face/ui_output_face.rs @@ -0,0 +1,118 @@ +//! The output card's permanent face. + +use lpc_wire::WireOutputTestPattern; + +use crate::{ + ControllerId, OutputTestPatternOp, ProjectController, ProjectNodeAddress, UiAction, + UiProducedProduct, +}; + +/// Permanent face for an output node card. +/// +/// The face is one affordance — drive the strip with a diagnostic pattern — +/// answering the first question anyone asks of hardware: are these LEDs +/// wired, addressed, and alive at all? The endpoint rides along as the +/// output's identity; everything else stays in the advanced drawer's slot +/// view. +/// +/// Like [`crate::UiButtonFace`] the face carries the ADDRESS plus the two +/// constructors below rather than a ready-made action: the op type and its +/// controller routing stay in core, the renewal cadence stays where the +/// timers are. +#[derive(Clone, Debug, PartialEq)] +pub struct UiOutputFace { + /// Stable address of the output node the test pattern is aimed at. + pub node: ProjectNodeAddress, + /// Authored hardware endpoint (`ws281x:rmt:D10`), when the def's row + /// resolved — the output's physical identity. + pub endpoint: Option, + /// What the output is currently being fed, when the node publishes a + /// control product to preview. + pub preview: Option, +} + +impl UiOutputFace { + /// Start — or RENEW — full-white test output. The runtime restores the + /// graph's own frames at `ttl_ms` if the renewals stop, which is what + /// makes an unmounted card (or a closed tab) safe. + pub fn test_pattern_action(&self, ttl_ms: u32) -> UiAction { + self.action( + WireOutputTestPattern::Solid { + r: 255, + g: 255, + b: 255, + }, + ttl_ms, + ) + } + + /// End test-pattern mode now, without waiting for the TTL. + pub fn clear_action(&self) -> UiAction { + self.action(WireOutputTestPattern::Clear, 0) + } + + fn action(&self, pattern: WireOutputTestPattern, ttl_ms: u32) -> UiAction { + UiAction::from_op( + ControllerId::new(ProjectController::NODE_ID), + OutputTestPatternOp { + node: self.node.clone(), + pattern, + ttl_ms, + }, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn face() -> UiOutputFace { + UiOutputFace { + node: ProjectNodeAddress::parse("/demo.project/strip.output").unwrap(), + endpoint: Some("ws281x:rmt:D10".to_string()), + preview: None, + } + } + + #[test] + fn both_sends_target_the_project_controller_with_the_faces_address() { + let face = face(); + + for action in [face.test_pattern_action(2000), face.clear_action()] { + assert!(action.is_for_node(ProjectController::NODE_ID)); + let op = action + .op_as::() + .expect("a test-pattern op rides the action"); + assert_eq!(op.node, face.node); + } + } + + #[test] + fn the_pattern_is_full_white_and_clearing_carries_no_ttl() { + let face = face(); + + let on = face + .test_pattern_action(2000) + .op_as::() + .unwrap() + .clone(); + assert_eq!( + on.pattern, + WireOutputTestPattern::Solid { + r: 255, + g: 255, + b: 255, + } + ); + assert_eq!(on.ttl_ms, 2000); + + let off = face + .clear_action() + .op_as::() + .unwrap() + .clone(); + assert_eq!(off.pattern, WireOutputTestPattern::Clear); + assert_eq!(off.ttl_ms, 0); + } +} diff --git a/lp-app/lpa-studio-core/src/app/node/mod.rs b/lp-app/lpa-studio-core/src/app/node/mod.rs index 8ff97e7f3..f694158ee 100644 --- a/lp-app/lpa-studio-core/src/app/node/mod.rs +++ b/lp-app/lpa-studio-core/src/app/node/mod.rs @@ -38,8 +38,8 @@ mod ui_slot_unit; mod ui_slot_value; pub use face::{ - UiFixtureFace, UiFixturePower, UiNodeFace, UiPanelControl, UiPanelWidget, UiPlaylistEntry, - UiPlaylistFace, UiShaderFace, + UiButtonFace, UiFixtureFace, UiFixturePower, UiNodeFace, UiOutputFace, UiPanelControl, + UiPanelWidget, UiPlaylistEntry, UiPlaylistFace, UiShaderFace, }; pub use ui_asset_editor::UiAssetEditor; pub use ui_binding_authoring::{UiBindingAuthoring, UiBindingAuthoringDirection, UiChannelChoice}; diff --git a/lp-app/lpa-studio-core/src/app/project/mod.rs b/lp-app/lpa-studio-core/src/app/project/mod.rs index e14cf6907..bf8928b0c 100644 --- a/lp-app/lpa-studio-core/src/app/project/mod.rs +++ b/lp-app/lpa-studio-core/src/app/project/mod.rs @@ -57,10 +57,10 @@ pub use asset::{ pub use dirty_summary::DirtySummary; pub use loaded_project_choice::LoadedProjectChoice; pub use node::{ - NodeController, NodeControllerState, NodeCopyOp, NodeCreateOp, NodePasteOp, NodeRemoveOp, - NodeRevertOp, PlaylistActivateOp, ProjectNodeAddress, ProjectNodeTarget, - ProjectProductSubscriptionIntent, UiAddNodeMenu, UiAddNodeMenuEntry, UiAttachTarget, - UiNodeRemovePreflight, + ButtonEventOp, NodeController, NodeControllerState, NodeCopyOp, NodeCreateOp, NodePasteOp, + NodeRemoveOp, NodeRevertOp, OutputTestPatternOp, PlaylistActivateOp, ProjectNodeAddress, + ProjectNodeTarget, ProjectProductSubscriptionIntent, UiAddNodeMenu, UiAddNodeMenuEntry, + UiAttachTarget, UiNodeRemovePreflight, }; pub use node_card_ui_state::{NodeCardDrawer, NodeCardUiState, NodeUiOp}; pub use project_connect_result::ProjectConnectResult; diff --git a/lp-app/lpa-studio-core/src/app/project/node/button_event_op.rs b/lp-app/lpa-studio-core/src/app/project/node/button_event_op.rs new file mode 100644 index 000000000..3e4ebaa0d --- /dev/null +++ b/lp-app/lpa-studio-core/src/app/project/node/button_event_op.rs @@ -0,0 +1,155 @@ +//! Synthetic button-event operation (the runtime command channel's +//! simulate-press consumer). + +use core::any::Any; + +use lpc_wire::WireButtonEvent; + +use crate::{ + ActionClass, ActionMeta, ActionPriority, ControllerOp, PROJECT_EDITOR_ACTION_DEADLINE, + ProjectNodeAddress, +}; + +/// Poke a button node's runtime with a SYNTHETIC event, via +/// `WireProjectCommand::NodeCommand` → `ButtonNode` (the button face's +/// simulate-press control). A runtime poke, not an edit: nothing is staged +/// in the overlay, nothing shows in the Save panel, and the event lands on +/// the engine's next frame exactly as a real GPIO transition would. +/// +/// Dispatched to `ProjectController::NODE_ID` like [`crate::SlotEditOp`]; +/// the controller resolves the button's CURRENT runtime `NodeId` from the +/// stable authored address at dispatch time, so a queued click can never +/// address a stale runtime id across a project reload. +/// +/// A sustained hold ([`WireButtonEvent::Press`]) is a REPEATED op, not a +/// long-running one: the face re-sends the same `press_id` on a renewal +/// cadence while held, and the device-side TTL clears the hold on its own +/// if the renewals stop (tab closed, card unmounted). There is therefore no +/// background action class here — every event is the same foreground poke. +#[derive(Clone, Debug, PartialEq)] +pub struct ButtonEventOp { + /// Stable address of the button node the event is addressed to. + pub node: ProjectNodeAddress, + /// The synthetic event: a click, a press (begin/renew), or a release. + pub event: WireButtonEvent, +} + +impl ButtonEventOp { + /// Human label for the event, used in the action meta and in the + /// rejection notice ("Couldn't … the button: …"). + fn verb(&self) -> &'static str { + match self.event { + WireButtonEvent::Click => "Simulate press", + WireButtonEvent::Press { .. } => "Hold button", + WireButtonEvent::Release { .. } => "Release button", + } + } +} + +impl ControllerOp for ButtonEventOp { + fn default_action_meta(&self) -> ActionMeta { + ActionMeta::new( + self.verb(), + "Send a synthetic event to this button's runtime now.", + ActionPriority::Primary, + ) + } + + fn action_class(&self) -> ActionClass { + // Same editor foreground class as the playlist activate poke: a + // press should feel immediate, and a rejection surfaces as a notice. + ActionClass::Foreground { + deadline: PROJECT_EDITOR_ACTION_DEADLINE, + } + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } + + fn eq_op(&self, other: &dyn ControllerOp) -> bool { + other.as_any().downcast_ref::() == Some(self) + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn into_any(self: Box) -> Box { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn op(event: WireButtonEvent) -> ButtonEventOp { + ButtonEventOp { + node: ProjectNodeAddress::parse("/demo.project/panel.button").unwrap(), + event, + } + } + + #[test] + fn button_events_are_editor_foreground_class() { + let click = op(WireButtonEvent::Click); + + assert_eq!( + click.action_class(), + ActionClass::Foreground { + deadline: PROJECT_EDITOR_ACTION_DEADLINE, + } + ); + assert_eq!(click.default_action_meta().label, "Simulate press"); + assert_eq!( + click.default_action_meta().priority, + ActionPriority::Primary + ); + } + + #[test] + fn press_and_release_label_the_hold_they_drive() { + assert_eq!( + op(WireButtonEvent::Press { + press_id: 9, + ttl_ms: 5000, + }) + .default_action_meta() + .label, + "Hold button" + ); + assert_eq!( + op(WireButtonEvent::Release { press_id: 9 }) + .default_action_meta() + .label, + "Release button" + ); + } + + #[test] + fn press_ids_distinguish_otherwise_identical_ops() { + // Renewals re-send the SAME op (equal), a new gesture does not — + // the queue must never coalesce two distinct holds. + assert_eq!( + op(WireButtonEvent::Press { + press_id: 1, + ttl_ms: 5000 + }), + op(WireButtonEvent::Press { + press_id: 1, + ttl_ms: 5000 + }) + ); + assert_ne!( + op(WireButtonEvent::Press { + press_id: 1, + ttl_ms: 5000 + }), + op(WireButtonEvent::Press { + press_id: 2, + ttl_ms: 5000 + }) + ); + } +} diff --git a/lp-app/lpa-studio-core/src/app/project/node/mod.rs b/lp-app/lpa-studio-core/src/app/project/node/mod.rs index 9eb62ff53..3178fb6a1 100644 --- a/lp-app/lpa-studio-core/src/app/project/node/mod.rs +++ b/lp-app/lpa-studio-core/src/app/project/node/mod.rs @@ -5,6 +5,7 @@ //! [`ProjectNodeTarget`] adds the current runtime `NodeId` for actions that //! need to talk back to the server. +pub mod button_event_op; pub mod node_controller; pub mod node_create_op; pub(in crate::app::project) mod node_face_builder; @@ -13,11 +14,13 @@ pub mod node_remove_op; pub mod node_remove_preflight; pub mod node_revert_op; pub mod node_share_op; +pub mod output_test_pattern_op; pub mod playlist_activate_op; pub mod project_node_address; pub mod project_node_target; pub mod ui_add_node_menu; +pub use button_event_op::ButtonEventOp; pub(in crate::app::project) use node_controller::root_slot_key; pub use node_controller::{NodeController, NodeControllerState, ProjectProductSubscriptionIntent}; pub use node_create_op::{NodeCreateOp, UiAttachTarget}; @@ -25,6 +28,7 @@ pub use node_remove_op::NodeRemoveOp; pub use node_remove_preflight::UiNodeRemovePreflight; pub use node_revert_op::NodeRevertOp; pub use node_share_op::{NodeCopyOp, NodePasteOp}; +pub use output_test_pattern_op::OutputTestPatternOp; pub use playlist_activate_op::PlaylistActivateOp; pub use project_node_address::ProjectNodeAddress; pub use project_node_target::ProjectNodeTarget; diff --git a/lp-app/lpa-studio-core/src/app/project/node/node_face_builder.rs b/lp-app/lpa-studio-core/src/app/project/node/node_face_builder.rs index 522cc5caf..fb13f1bba 100644 --- a/lp-app/lpa-studio-core/src/app/project/node/node_face_builder.rs +++ b/lp-app/lpa-studio-core/src/app/project/node/node_face_builder.rs @@ -27,21 +27,29 @@ //! preview snapshot, node-select action). Deriving the face ALSO //! enforces the sibling invariant: the children list keeps ONLY the //! active entry's child (see [`kind_face`]). +//! - **Button** / **Output**: the runtime-poke faces (node-actions P4). +//! Their affordance is a command at the LIVE runtime, not a slot edit, so +//! they derive from the node's stable ADDRESS and always exist; the +//! section DTOs only supply the readouts beside the control (endpoint, +//! message id, the control product the output is fed). Every op the +//! controls dispatch is built by the face DTO itself (`UiButtonFace`, +//! `UiOutputFace`) so the view layer owns only the timing. //! - Every other kind returns `None` — the card keeps today's generic //! sections. use lpc_model::{ - FixtureDef, LpValue, PlaylistDef, ShaderDef, ShaderValueShapeRef, shader_panel_step, + ButtonDef, FixtureDef, LpValue, OutputDef, PlaylistDef, ShaderDef, ShaderValueShapeRef, + shader_panel_step, }; use crate::app::project::format_lp_value; use crate::{ ControllerId, PlaylistActivateOp, ProjectController, ProjectNodeAddress, ProjectSlotAddress, - UiAction, UiAssetEditor, UiAssetEditorKind, UiConfigSlot, UiConfigSlotBody, UiFixtureFace, - UiFixturePower, UiNodeChild, UiNodeFace, UiNodeSection, UiPanelControl, UiPanelWidget, - UiPlaylistEntry, UiPlaylistFace, UiProducedProduct, UiProductKind, UiProductPreview, - UiShaderFace, UiSlotAspect, UiSlotAspectKind, UiSlotEditorHint, UiSlotSourceState, UiSlotValue, - UiSlotValueKind, + UiAction, UiAssetEditor, UiAssetEditorKind, UiButtonFace, UiConfigSlot, UiConfigSlotBody, + UiFixtureFace, UiFixturePower, UiNodeChild, UiNodeFace, UiNodeSection, UiOutputFace, + UiPanelControl, UiPanelWidget, UiPlaylistEntry, UiPlaylistFace, UiProducedProduct, + UiProductKind, UiProductPreview, UiShaderFace, UiSlotAspect, UiSlotAspectKind, + UiSlotEditorHint, UiSlotSourceState, UiSlotValue, UiSlotValueKind, }; /// Build the kind-specific face for a node's card from its projected @@ -82,6 +90,8 @@ pub(in crate::app::project) fn kind_face( // ACTIVE placard is the active-ness presentation. Some(UiNodeFace::Playlist(face)) } + ButtonDef::KIND => Some(UiNodeFace::Button(button_face(address, sections))), + OutputDef::KIND => Some(UiNodeFace::Output(output_face(address, sections))), // Unknown kinds stay on the generic fallback permanently. _ => None, } @@ -137,6 +147,63 @@ fn fixture_power(sections: &[UiNodeSection]) -> Option { }) } +// -- runtime-poke faces (button, output) -------------------------------------- + +/// The button card's face: the simulate-press control, plus the button's +/// hardware identity. Unlike the shader/fixture faces this needs nothing +/// from the section DTOs to exist — the affordance is a poke at the runtime, +/// addressed by the node's own stable address — so it always derives and the +/// endpoint/id readouts degrade to `None` on their own. +fn button_face(address: &ProjectNodeAddress, sections: &[UiNodeSection]) -> UiButtonFace { + UiButtonFace { + node: address.clone(), + endpoint: top_level_string(sections, "endpoint"), + id: top_level_u32(sections, "id"), + } +} + +/// The output card's face: the test-pattern toggle, the endpoint, and the +/// control product the output is being fed (when one is projected). Always +/// derives, same reasoning as [`button_face`]. +fn output_face(address: &ProjectNodeAddress, sections: &[UiNodeSection]) -> UiOutputFace { + UiOutputFace { + node: address.clone(), + endpoint: top_level_string(sections, "endpoint"), + preview: product_of_kind(sections, UiProductKind::Control), + } +} + +/// A top-level config row's string reading, keyed EXACTLY (unlike +/// [`string_field`], which matches a record field by its `.` suffix). +fn top_level_string(sections: &[UiNodeSection], key: &str) -> Option { + match &config_rows(sections) + .into_iter() + .find(|row| row.key == key)? + .body + { + UiConfigSlotBody::Value(UiSlotValue { + kind: UiSlotValueKind::String(value), + .. + }) => Some(value.clone()), + _ => None, + } +} + +/// A top-level config row's u32 reading, keyed exactly. +fn top_level_u32(sections: &[UiNodeSection], key: &str) -> Option { + match &config_rows(sections) + .into_iter() + .find(|row| row.key == key)? + .body + { + UiConfigSlotBody::Value(UiSlotValue { + kind: UiSlotValueKind::U32(value), + .. + }) => Some(*value), + _ => None, + } +} + /// First produced product row of the wanted kind; an `Empty`-kind row (the /// output exists but nothing resolved yet) is the stable-face fallback. fn product_of_kind(sections: &[UiNodeSection], kind: UiProductKind) -> Option { @@ -885,6 +952,65 @@ mod tests { ); } + #[test] + fn button_face_carries_the_nodes_address_and_hardware_identity() { + let sections = vec![UiNodeSection::ConfigSlots(vec![ + UiConfigSlot::value( + "endpoint", + "Endpoint", + UiSlotValue::string("button:gpio:D9"), + ), + UiConfigSlot::value("id", "Id", UiSlotValue::u32(3)), + UiConfigSlot::value("stable_ms", "Stable ms", UiSlotValue::u32(30)), + ])]; + + let Some(UiNodeFace::Button(face)) = + kind_face("button", &test_address(), §ions, &mut Vec::new()) + else { + panic!("expected a button face"); + }; + assert_eq!(face.node, test_address(), "the poke is address-addressed"); + assert_eq!(face.endpoint.as_deref(), Some("button:gpio:D9")); + assert_eq!(face.id, Some(3)); + } + + #[test] + fn output_face_carries_the_address_endpoint_and_fed_product() { + let sections = vec![ + UiNodeSection::ProducedProducts(vec![UiProducedProduct::control("Output")]), + UiNodeSection::ConfigSlots(vec![UiConfigSlot::value( + "endpoint", + "Endpoint", + UiSlotValue::string("ws281x:rmt:D10"), + )]), + ]; + + let Some(UiNodeFace::Output(face)) = + kind_face("output", &test_address(), §ions, &mut Vec::new()) + else { + panic!("expected an output face"); + }; + assert_eq!(face.node, test_address()); + assert_eq!(face.endpoint.as_deref(), Some("ws281x:rmt:D10")); + assert_eq!( + face.preview.as_ref().map(|product| product.kind), + Some(UiProductKind::Control) + ); + } + + #[test] + fn poke_faces_derive_even_with_nothing_to_read_out() { + // The affordance is a runtime command addressed by the node's own + // address: a def whose rows have not projected yet still gets its + // control (unlike shader/fixture, which need their preview row). + for kind in ["button", "output"] { + assert!( + kind_face(kind, &test_address(), &[], &mut Vec::new()).is_some(), + "{kind} face derives from the address alone" + ); + } + } + #[test] fn other_kinds_and_faceless_sections_stay_generic() { assert_eq!( diff --git a/lp-app/lpa-studio-core/src/app/project/node/output_test_pattern_op.rs b/lp-app/lpa-studio-core/src/app/project/node/output_test_pattern_op.rs new file mode 100644 index 000000000..376b1a2b1 --- /dev/null +++ b/lp-app/lpa-studio-core/src/app/project/node/output_test_pattern_op.rs @@ -0,0 +1,146 @@ +//! Output test-pattern operation (the runtime command channel's +//! "is this strip alive?" consumer). + +use core::any::Any; + +use lpc_wire::WireOutputTestPattern; + +use crate::{ + ActionClass, ActionMeta, ActionPriority, ControllerOp, PROJECT_EDITOR_ACTION_DEADLINE, + ProjectNodeAddress, +}; + +/// Put an output node's runtime into (or out of) test-pattern mode, via +/// `WireProjectCommand::NodeCommand` → `OutputNode` (the output face's +/// test-pattern toggle). A runtime poke, not an edit: nothing is staged in +/// the overlay, nothing shows in the Save panel, and the graph's own frames +/// resume the moment the pattern clears. +/// +/// Dispatched to `ProjectController::NODE_ID` like [`crate::SlotEditOp`]; +/// the controller resolves the output's CURRENT runtime `NodeId` from the +/// stable authored address at dispatch time, so a queued toggle can never +/// address a stale runtime id across a project reload. +/// +/// A sustained pattern is a REPEATED op, not a long-running one: the face +/// re-sends it on a renewal cadence while the toggle is on, and the +/// device-side TTL restores normal output on its own if the renewals stop +/// (tab closed, card unmounted). There is therefore no background action +/// class here — every send is the same foreground poke. +#[derive(Clone, Debug, PartialEq)] +pub struct OutputTestPatternOp { + /// Stable address of the output node the pattern is addressed to. + pub node: ProjectNodeAddress, + /// The pattern to hold (or [`WireOutputTestPattern::Clear`] to end it). + pub pattern: WireOutputTestPattern, + /// Auto-expiry for a sustained pattern, in milliseconds. Ignored by the + /// runtime for `Clear`. + pub ttl_ms: u32, +} + +impl OutputTestPatternOp { + /// Human label, used in the action meta and the rejection notice. + fn verb(&self) -> &'static str { + match self.pattern { + WireOutputTestPattern::Clear => "Clear test pattern", + WireOutputTestPattern::Solid { .. } => "Test pattern", + } + } +} + +impl ControllerOp for OutputTestPatternOp { + fn default_action_meta(&self) -> ActionMeta { + ActionMeta::new( + self.verb(), + "Drive this output with a diagnostic pattern instead of the graph.", + ActionPriority::Primary, + ) + } + + fn action_class(&self) -> ActionClass { + // Same editor foreground class as the playlist activate poke: the + // toggle should feel immediate, and a rejection (never-rendered + // output) surfaces as a notice. + ActionClass::Foreground { + deadline: PROJECT_EDITOR_ACTION_DEADLINE, + } + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } + + fn eq_op(&self, other: &dyn ControllerOp) -> bool { + other.as_any().downcast_ref::() == Some(self) + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn into_any(self: Box) -> Box { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn op(pattern: WireOutputTestPattern, ttl_ms: u32) -> OutputTestPatternOp { + OutputTestPatternOp { + node: ProjectNodeAddress::parse("/demo.project/strip.output").unwrap(), + pattern, + ttl_ms, + } + } + + #[test] + fn test_pattern_is_editor_foreground_class_with_pattern_meta() { + let solid = op( + WireOutputTestPattern::Solid { + r: 255, + g: 255, + b: 255, + }, + 2000, + ); + + assert_eq!( + solid.action_class(), + ActionClass::Foreground { + deadline: PROJECT_EDITOR_ACTION_DEADLINE, + } + ); + assert_eq!(solid.default_action_meta().label, "Test pattern"); + assert_eq!( + solid.default_action_meta().priority, + ActionPriority::Primary + ); + } + + #[test] + fn clearing_labels_itself_as_the_way_out() { + assert_eq!( + op(WireOutputTestPattern::Clear, 0) + .default_action_meta() + .label, + "Clear test pattern" + ); + } + + #[test] + fn renewals_are_equal_ops_and_clear_is_not() { + let solid = || { + op( + WireOutputTestPattern::Solid { + r: 255, + g: 255, + b: 255, + }, + 2000, + ) + }; + assert_eq!(solid(), solid(), "a renewal re-sends the identical op"); + assert_ne!(solid(), op(WireOutputTestPattern::Clear, 0)); + } +} diff --git a/lp-app/lpa-studio-core/src/app/project/project_controller.rs b/lp-app/lpa-studio-core/src/app/project/project_controller.rs index 219a789fd..82134ea73 100644 --- a/lp-app/lpa-studio-core/src/app/project/project_controller.rs +++ b/lp-app/lpa-studio-core/src/app/project/project_controller.rs @@ -15,17 +15,18 @@ use crate::app::project::slot::{ use crate::app::studio::refresh_cadence::{VERDICT_CHASE_INTERVAL, VERDICT_CHASE_TICKS}; use crate::core::notice::UiNotices; use crate::{ - AssetEditOp, Controller, ControllerId, DirtySummary, LoadedProjectChoice, MAX_ASSET_BODY_BYTES, - NodeCardUiState, NodeUiOp, PendingAssetEdit, PendingEdit, PendingEditOp, PendingEditPhase, - PlaylistActivateOp, ProgressState, ProjectConnectResult, ProjectEditorOp, ProjectEditorTarget, - ProjectEditorView, ProjectInventorySummary, ProjectNodeAddress, ProjectNodeStatusTone, - ProjectNodeTreeItem, ProjectNodeTreeView, ProjectOp, ProjectSlotAddress, ProjectSlotRoot, - ProjectSnapshot, ProjectState, ProjectSync, ProjectSyncPhase, ProjectSyncRun, - ProjectSyncSummary, SlotEditOp, StudioOverlayMutation, StudioProjectReadOutcome, - StudioServerClient, UiAction, UiAssetContent, UiAssetContentBody, UiAssetEditor, UiError, - UiIssue, UiLogDraft, UiLogLevel, UiLogOrigin, UiMetric, UiNodeView, UiNotice, UiPaneAction, - UiPaneView, UiPendingEdit, UiPendingEditKind, UiPendingEditPhase, UiProductRef, UiResult, - UiShaderError, UiShaderUniform, UiSlotAsset, UiStatus, UiViewContent, UxUpdateSink, + AssetEditOp, ButtonEventOp, Controller, ControllerId, DirtySummary, LoadedProjectChoice, + MAX_ASSET_BODY_BYTES, NodeCardUiState, NodeUiOp, OutputTestPatternOp, PendingAssetEdit, + PendingEdit, PendingEditOp, PendingEditPhase, PlaylistActivateOp, ProgressState, + ProjectConnectResult, ProjectEditorOp, ProjectEditorTarget, ProjectEditorView, + ProjectInventorySummary, ProjectNodeAddress, ProjectNodeStatusTone, ProjectNodeTreeItem, + ProjectNodeTreeView, ProjectOp, ProjectSlotAddress, ProjectSlotRoot, ProjectSnapshot, + ProjectState, ProjectSync, ProjectSyncPhase, ProjectSyncRun, ProjectSyncSummary, SlotEditOp, + StudioOverlayMutation, StudioProjectReadOutcome, StudioServerClient, UiAction, UiAssetContent, + UiAssetContentBody, UiAssetEditor, UiError, UiIssue, UiLogDraft, UiLogLevel, UiLogOrigin, + UiMetric, UiNodeView, UiNotice, UiPaneAction, UiPaneView, UiPendingEdit, UiPendingEditKind, + UiPendingEditPhase, UiProductRef, UiResult, UiShaderError, UiShaderUniform, UiSlotAsset, + UiStatus, UiViewContent, UxUpdateSink, }; use lpc_model::slot::SlotPersistence; use lpc_model::{ @@ -38,8 +39,8 @@ use lpc_model::{ }; use lpc_view::ProjectView; use lpc_wire::{ - WireCreateNodeRequest, WireCreateNodeResponse, WireNodeCommand, WireNodeCommandResponse, - WireRemoveNodeRequest, WireRemoveNodeResponse, + WireButtonEvent, WireCreateNodeRequest, WireCreateNodeResponse, WireNodeCommand, + WireNodeCommandResponse, WireOutputTestPattern, WireRemoveNodeRequest, WireRemoveNodeResponse, }; use super::node::node_naming::{file_stem, node_kind_slug, sanitize_node_name, unique_node_name}; @@ -2581,6 +2582,95 @@ impl ProjectController { }) } + /// Execute a [`ButtonEventOp`]: dispatch a synthetic button event to the + /// button's live runtime (simulate-press on the button card's face). + /// + /// A hold is a repeated op, not a long-running one — the face re-sends + /// the same `press_id` on its renewal cadence and the device-side TTL + /// cleans up if the renewals stop — so every event takes the same + /// foreground path as the playlist activate poke. + pub async fn send_button_event( + &mut self, + server: &mut StudioServerClient, + op: ButtonEventOp, + ) -> Result { + let what = match op.event { + WireButtonEvent::Click => "simulate a button press", + WireButtonEvent::Press { .. } => "hold the button", + WireButtonEvent::Release { .. } => "release the button", + }; + self.dispatch_node_command( + server, + &op.node, + what, + WireNodeCommand::ButtonEvent { event: op.event }, + ) + .await + } + + /// Execute an [`OutputTestPatternOp`]: drive (or stop driving) an output + /// node's runtime with a diagnostic pattern instead of the graph's + /// frames. + /// + /// A sustained pattern is a repeated op — the face re-sends it while the + /// toggle is on and the device-side TTL restores normal output if the + /// renewals stop — so it takes the same foreground path as the playlist + /// activate poke. + pub async fn send_output_test_pattern( + &mut self, + server: &mut StudioServerClient, + op: OutputTestPatternOp, + ) -> Result { + let what = match op.pattern { + WireOutputTestPattern::Clear => "clear the test pattern", + WireOutputTestPattern::Solid { .. } => "start the test pattern", + }; + self.dispatch_node_command( + server, + &op.node, + what, + WireNodeCommand::OutputTestPattern { + pattern: op.pattern, + ttl_ms: op.ttl_ms, + }, + ) + .await + } + + /// Shared dispatch for the runtime command channel (the non-overlay + /// path): resolve the node's CURRENT runtime `NodeId` from the stable + /// authored address HERE, at dispatch time, so a queued poke never + /// addresses a stale runtime id across a reload; then map the response + /// tiers. Acceptance is quiet but borrows the verdict-chase tightened + /// ticks so the effect is visible in UI-time; rejection surfaces as a + /// warning notice reading "Couldn't ``: ``". + async fn dispatch_node_command( + &mut self, + server: &mut StudioServerClient, + node: &ProjectNodeAddress, + what: &str, + command: WireNodeCommand, + ) -> Result { + let handle_id = self.ready_handle_id()?; + let node_id = self + .node(node) + .map(|node| node.target().node_id) + .ok_or_else(|| UiError::Project(format!("no node at {node} to {what} on")))?; + let run = server.node_command(handle_id, node_id, command).await?; + let notices = match run.response { + WireNodeCommandResponse::Accepted => { + self.verdict_chase_ticks = VERDICT_CHASE_TICKS; + UiNotices::new() + } + WireNodeCommandResponse::Rejected { reason } => UiNotices::new() + .with_notice(UiNotice::warning(format!("Couldn't {what}: {reason}"))), + }; + Ok(ProjectEditRun { + notices, + logs: run.logs, + }) + } + /// Commit the pending-edit overlay (persisted edits are written back to /// def artifacts; transient edits stay pending) and re-sync the overlay /// mirror from a follow-up read. diff --git a/lp-app/lpa-studio-core/src/app/studio/studio_controller.rs b/lp-app/lpa-studio-core/src/app/studio/studio_controller.rs index 87ddd93c5..57d1b6d2e 100644 --- a/lp-app/lpa-studio-core/src/app/studio/studio_controller.rs +++ b/lp-app/lpa-studio-core/src/app/studio/studio_controller.rs @@ -22,14 +22,14 @@ use crate::app::studio::ui_console_view::UiConsoleView; use crate::core::log::{LogClock, LogFilter, LogRing}; use crate::core::notice::UiNotices; use crate::{ - AssetContentFetchOp, AssetEditOp, ConnectFlowState, Controller, ControllerContext, - DeviceController, DeviceOp, NodeCopyOp, NodeCreateOp, NodePasteOp, NodeRemoveOp, NodeRevertOp, - PlaylistActivateOp, ProjectConnectResult, ProjectController, ProjectEditRun, ProjectOp, - ProjectRefreshOutcome, ProjectState, ProjectSyncRun, RuntimePayload, RuntimePool, - ServerFailureKind, ServerSnapshot, ServerState, SlotEditOp, StudioSnapshot, UiAction, - UiActions, UiActivityView, UiError, UiLogDraft, UiLogEntry, UiLogLevel, UiLogOrigin, UiNotice, - UiPaneView, UiProgress, UiResult, UiStatus, UiStudioView, UiViewContent, UxActivityTarget, - UxUpdate, UxUpdateSink, + AssetContentFetchOp, AssetEditOp, ButtonEventOp, ConnectFlowState, Controller, + ControllerContext, DeviceController, DeviceOp, NodeCopyOp, NodeCreateOp, NodePasteOp, + NodeRemoveOp, NodeRevertOp, OutputTestPatternOp, PlaylistActivateOp, ProjectConnectResult, + ProjectController, ProjectEditRun, ProjectOp, ProjectRefreshOutcome, ProjectState, + ProjectSyncRun, RuntimePayload, RuntimePool, ServerFailureKind, ServerSnapshot, ServerState, + SlotEditOp, StudioSnapshot, UiAction, UiActions, UiActivityView, UiError, UiLogDraft, + UiLogEntry, UiLogLevel, UiLogOrigin, UiNotice, UiPaneView, UiProgress, UiResult, UiStatus, + UiStudioView, UiViewContent, UxActivityTarget, UxUpdate, UxUpdateSink, }; /// How often the quiet PortHeld retry re-attempts the granted attach @@ -2003,6 +2003,14 @@ impl StudioController { let op = action.into_op::()?; return self.execute_playlist_activate_op(op).await; } + if action.op_as::().is_some() { + let op = action.into_op::()?; + return self.execute_button_event_op(op).await; + } + if action.op_as::().is_some() { + let op = action.into_op::()?; + return self.execute_output_test_pattern_op(op).await; + } if action.op_as::().is_some() { let op = action.into_op::()?; return self.execute_node_create_op(op).await; @@ -3134,6 +3142,28 @@ impl StudioController { self.record_project_edit_run(run) } + /// Button face simulate-press: dispatch a synthetic button event to the + /// node's live runtime. Quiet on acceptance (the button's own produced + /// state follows via the tightened refresh ticks); a rejection comes + /// back as a warning notice. + async fn execute_button_event_op(&mut self, op: ButtonEventOp) -> UiResult { + let run = { + let server = self.pool.lens_session_mut()?.client_mut()?; + self.project.send_button_event(server, op).await + }; + self.record_project_edit_run(run) + } + + /// Output face test-pattern toggle: dispatch (or clear) the diagnostic + /// pattern on the output's live runtime. + async fn execute_output_test_pattern_op(&mut self, op: OutputTestPatternOp) -> UiResult { + let run = { + let server = self.pool.lens_session_mut()?.client_mut()?; + self.project.send_output_test_pattern(server, op).await + }; + self.record_project_edit_run(run) + } + async fn execute_node_create_op(&mut self, op: NodeCreateOp) -> UiResult { let run = { let server = self.pool.lens_session_mut()?.client_mut()?; diff --git a/lp-app/lpa-studio-core/src/lib.rs b/lp-app/lpa-studio-core/src/lib.rs index 72617f836..d80102b3d 100644 --- a/lp-app/lpa-studio-core/src/lib.rs +++ b/lp-app/lpa-studio-core/src/lib.rs @@ -44,16 +44,16 @@ pub use app::home::{ }; pub use app::node::{ UiAssetEditor, UiAssetEditorKind, UiBindingAuthoring, UiBindingAuthoringDirection, - UiBindingEndpoint, UiChannelChoice, UiConfigSlot, UiConfigSlotBody, UiControlProductPreview, - UiControlSampleFormat, UiFixtureFace, UiFixturePower, UiNodeChild, UiNodeDirtyState, - UiNodeFace, UiNodeHeader, UiNodeSection, UiNodeTab, UiNodeTabBody, UiNodeView, UiPanelControl, - UiPanelWidget, UiPlaylistEntry, UiPlaylistFace, UiProducedBinding, UiProducedBindings, - UiProducedProduct, UiProducedValue, UiProductKind, UiProductPreview, UiProductPreviewFrame, - UiProductRef, UiProductTrackingState, UiShaderFace, UiShaderUniform, UiSlotAffordance, - UiSlotAspect, UiSlotAspectKind, UiSlotAspectRow, UiSlotAsset, UiSlotComposite, - UiSlotEditorHint, UiSlotEnumComposite, UiSlotFieldState, UiSlotMapComposite, UiSlotMapKeyKind, - UiSlotOption, UiSlotOptionality, UiSlotRecord, UiSlotShape, UiSlotShapeField, - UiSlotSourceState, UiSlotUnit, UiSlotValue, UiSlotValueKind, + UiBindingEndpoint, UiButtonFace, UiChannelChoice, UiConfigSlot, UiConfigSlotBody, + UiControlProductPreview, UiControlSampleFormat, UiFixtureFace, UiFixturePower, UiNodeChild, + UiNodeDirtyState, UiNodeFace, UiNodeHeader, UiNodeSection, UiNodeTab, UiNodeTabBody, + UiNodeView, UiOutputFace, UiPanelControl, UiPanelWidget, UiPlaylistEntry, UiPlaylistFace, + UiProducedBinding, UiProducedBindings, UiProducedProduct, UiProducedValue, UiProductKind, + UiProductPreview, UiProductPreviewFrame, UiProductRef, UiProductTrackingState, UiShaderFace, + UiShaderUniform, UiSlotAffordance, UiSlotAspect, UiSlotAspectKind, UiSlotAspectRow, + UiSlotAsset, UiSlotComposite, UiSlotEditorHint, UiSlotEnumComposite, UiSlotFieldState, + UiSlotMapComposite, UiSlotMapKeyKind, UiSlotOption, UiSlotOptionality, UiSlotRecord, + UiSlotShape, UiSlotShapeField, UiSlotSourceState, UiSlotUnit, UiSlotValue, UiSlotValueKind, }; #[cfg(all(feature = "browser-worker", target_arch = "wasm32"))] pub use app::preview_host::{PreviewHost, PreviewSlotHandle}; @@ -62,13 +62,14 @@ pub use app::preview_host::{ PreviewTier, }; pub use app::project::{ - AgentEngineStatus, AssetContentFetchOp, AssetEditOp, DirtySummary, LoadedProjectChoice, - MAX_ASSET_BODY_BYTES, NodeCardDrawer, NodeCardUiState, NodeController, NodeControllerState, - NodeCopyOp, NodeCreateOp, NodePasteOp, NodeRemoveOp, NodeRevertOp, NodeUiOp, PendingAssetEdit, - PendingEdit, PendingEditOp, PendingEditPhase, PlaylistActivateOp, ProjectAssetContentRun, - ProjectConnectResult, ProjectController, ProjectEditRun, ProjectEditorOp, ProjectEditorTarget, - ProjectEditorView, ProjectInventorySummary, ProjectNodeAddress, ProjectNodeStatusTone, - ProjectNodeStatusView, ProjectNodeTarget, ProjectNodeTreeItem, ProjectNodeTreeView, ProjectOp, + AgentEngineStatus, AssetContentFetchOp, AssetEditOp, ButtonEventOp, DirtySummary, + LoadedProjectChoice, MAX_ASSET_BODY_BYTES, NodeCardDrawer, NodeCardUiState, NodeController, + NodeControllerState, NodeCopyOp, NodeCreateOp, NodePasteOp, NodeRemoveOp, NodeRevertOp, + NodeUiOp, OutputTestPatternOp, PendingAssetEdit, PendingEdit, PendingEditOp, PendingEditPhase, + PlaylistActivateOp, ProjectAssetContentRun, ProjectConnectResult, ProjectController, + ProjectEditRun, ProjectEditorOp, ProjectEditorTarget, ProjectEditorView, + ProjectInventorySummary, ProjectNodeAddress, ProjectNodeStatusTone, ProjectNodeStatusView, + ProjectNodeTarget, ProjectNodeTreeItem, ProjectNodeTreeView, ProjectOp, ProjectProductSubscriptionIntent, ProjectRefreshOutcome, ProjectRuntimeSummary, ProjectSlotAddress, ProjectSlotRoot, ProjectSnapshot, ProjectState, ProjectSync, ProjectSyncPhase, ProjectSyncRun, ProjectSyncSummary, SlotController, SlotControllerState, diff --git a/lp-app/lpa-studio-web/src/app/node/button_face_stories.rs b/lp-app/lpa-studio-web/src/app/node/button_face_stories.rs new file mode 100644 index 000000000..abcb94aec --- /dev/null +++ b/lp-app/lpa-studio-web/src/app/node/button_face_stories.rs @@ -0,0 +1,50 @@ +//! Stories for the button card face (node-actions P4). +//! +//! The face is one affordance — simulate the transition a finger would +//! make — over the button's hardware identity. Stories are static by +//! definition, so the held state is mounted directly rather than gestured +//! into: the real one is reached by holding past the 300 ms window, and it +//! renews on the wire until pointer-up. + +use dioxus::prelude::*; +use lpa_studio_web_story_macros::story; + +use crate::app::node::face_story_fixtures::{button_face, button_node_view}; +use crate::app::node::{ButtonFace, NodePane}; + +#[component] +#[allow(non_snake_case, reason = "Dioxus components use PascalCase")] +fn ButtonCardCanvas(children: Element) -> Element { + rsx! { + div { class: "tw:w-full tw:max-w-md", {children} } + } +} + +#[story( + description = "Button card at rest: the simulate-press control beside the endpoint and message id, over the collapsed settings drawer. A tap sends a click; holding past ~300 ms becomes a real hold that renews until release." +)] +fn default() -> Element { + rsx! { + ButtonCardCanvas { + NodePane { + view: button_node_view(), + on_action: move |_| {}, + } + } + } +} + +#[story( + description = "Held: the pointer stayed down past the window, so the control is sustaining a real press on the runtime (renewed every second, auto-released by the device if this tab stops asking). Live-blue, the same family as the playlist's ACTIVE placard — something is happening in the runtime right now." +)] +fn held() -> Element { + rsx! { + ButtonCardCanvas { + ButtonFace { + face: button_face(), + held_initially: true, + on_action: move |_| {}, + } + } + } +} diff --git a/lp-app/lpa-studio-web/src/app/node/face/button_face.rs b/lp-app/lpa-studio-web/src/app/node/face/button_face.rs new file mode 100644 index 000000000..8408c15ca --- /dev/null +++ b/lp-app/lpa-studio-web/src/app/node/face/button_face.rs @@ -0,0 +1,260 @@ +//! The button card's permanent face: the simulate-press control. +//! +//! A button node's whole job is a transition someone's finger makes. Studio +//! needs to make that transition without the finger — to rehearse a show, or +//! to prove a binding before the hardware is even on the desk — so the face +//! is one control that behaves like the physical button it stands in for. +//! +//! The gesture is WINDOWED, and the window is what makes one control cover +//! both events the runtime distinguishes: +//! +//! - pointer-up inside [`PRESS_WINDOW_MS`] → `Click` (the minimal +//! down-then-up pair — a tap); +//! - the window elapsing with the pointer still down → `Press`, re-sent +//! every [`RENEWAL_MS`] while held, then `Release` on pointer-up. +//! +//! The renewal cadence and the [`HOLD_TTL_MS`] TTL are a pair: the runtime +//! auto-releases a hold whose renewals stop, so a closed tab, a crashed +//! renderer, or a card that unmounts mid-hold cannot leave a button stuck +//! down. That is why teardown does NOTHING here — the renewal task is +//! scope-owned and simply dies with the component; chasing a guaranteed +//! `Release` on unmount would be a worse mechanism than the one already +//! guarding the wire. +//! +//! Everything in this file is the TIMING half of the affordance. The ops +//! themselves — and which controller they route to — are built by +//! [`lpa_studio_core::UiButtonFace`], so a synthetic press cannot drift from +//! the op that carries it. + +use core::sync::atomic::{AtomicU32, Ordering}; + +use dioxus::prelude::*; +use gloo_timers::future::TimeoutFuture; +use lpa_studio_core::{UiAction, UiButtonFace as UiButtonFaceData}; + +use crate::app::node::NodeCardSection; +use crate::app::node::slot_fields::capture_field_pointer; + +/// How long the pointer must stay down before the gesture becomes a HOLD +/// instead of a tap. +const PRESS_WINDOW_MS: u32 = 300; + +/// Renewal cadence for a sustained hold. Comfortably inside +/// [`HOLD_TTL_MS`], so an ordinary render hitch never drops the hold. +const RENEWAL_MS: u32 = 1000; + +/// A hold's device-side auto-release. The safety net for every teardown +/// path: nothing this component does on unmount, and nothing it could do, +/// matters as long as the renewals stop. +const HOLD_TTL_MS: u32 = 5000; + +/// Per-gesture press id. A hold is identified by this id on the wire, so +/// renewals of the SAME hold repeat it and a new gesture never adopts an +/// old one. Tab-local is the right scope: renewals only ever come from the +/// tab that started the hold, and a reload's stale hold self-clears on TTL. +static NEXT_PRESS_ID: AtomicU32 = AtomicU32::new(1); + +fn next_press_id() -> u32 { + NEXT_PRESS_ID.fetch_add(1, Ordering::Relaxed) +} + +#[component] +#[allow(non_snake_case, reason = "Dioxus components use PascalCase")] +pub fn ButtonFace( + face: UiButtonFaceData, + /// Mount already looking held (stories render the pressed state + /// without a pointer or a timer). + #[props(default = false)] + held_initially: bool, + #[props(default)] on_action: Option>, +) -> Element { + // The live gesture's press id — `None` between gestures. Clearing it is + // ALSO how the renewal task is told to stop: the task compares the id + // it started with on every tick, so a release, a cancel, or a fresh + // gesture all retire the previous loop without any extra channel. + let mut gesture = use_signal(|| None::); + // True once the window has elapsed: the control looks held and the + // pointer-up will be a Release rather than a Click. + let mut held = use_signal(|| held_initially); + let wired = on_action.is_some(); + + let identity = button_identity(&face); + let press_face = face.clone(); + let up_face = face.clone(); + let cancel_face = face; + + rsx! { + NodeCardSection { label: "simulate", first: true, + div { class: "tw:flex tw:min-w-0 tw:items-center tw:gap-3 tw:px-4 tw:py-3", + button { + class: press_button_class(held(), wired), + r#type: "button", + disabled: !wired, + title: "Press and hold to send a real hold; tap for a click. Synthetic events reach the runtime exactly like the physical button.", + onpointerdown: move |event| { + let Some(handler) = on_action else { + return; + }; + event.stop_propagation(); + // Capture so the pointer-up lands here even if the + // finger slides off the control mid-hold. + capture_field_pointer(&event); + let press_id = next_press_id(); + gesture.set(Some(press_id)); + held.set(false); + let face = press_face.clone(); + spawn(async move { + TimeoutFuture::new(PRESS_WINDOW_MS).await; + // Released inside the window (or superseded): + // this gesture was a tap, and the pointer-up + // already sent its Click. + if *gesture.peek() != Some(press_id) { + return; + } + held.set(true); + // Renewal loop. Scope-owned: an unmount drops + // it mid-hold and the TTL does the rest. + while *gesture.peek() == Some(press_id) { + handler.call(face.press_action(press_id, HOLD_TTL_MS)); + TimeoutFuture::new(RENEWAL_MS).await; + } + }); + }, + onpointerup: move |event| { + event.stop_propagation(); + let (Some(press_id), Some(handler)) = (*gesture.peek(), on_action) else { + return; + }; + let was_held = *held.peek(); + gesture.set(None); + held.set(false); + if was_held { + handler.call(up_face.release_action(press_id)); + } else { + handler.call(up_face.click_action()); + } + }, + onpointercancel: move |_| { + // A cancelled gesture is not a tap, so no Click — + // but an established hold must still be let go. + let (Some(press_id), Some(handler)) = (*gesture.peek(), on_action) else { + return; + }; + let was_held = *held.peek(); + gesture.set(None); + held.set(false); + if was_held { + handler.call(cancel_face.release_action(press_id)); + } + }, + if held() { "Holding…" } else { "Simulate press" } + } + if let Some(identity) = identity { + span { class: "tw:min-w-0 tw:truncate tw:font-mono tw:text-[0.7rem] tw:text-dim-foreground", + "{identity}" + } + } + } + } + } +} + +/// The button's hardware identity beside the control: endpoint, message id, +/// or both. `None` when the def rows have not projected yet — the control +/// is the face, the readout is garnish. +fn button_identity(face: &UiButtonFaceData) -> Option { + match (face.endpoint.as_deref(), face.id) { + (Some(endpoint), Some(id)) => Some(format!("{endpoint} · id {id}")), + (Some(endpoint), None) => Some(endpoint.to_string()), + (None, Some(id)) => Some(format!("id {id}")), + (None, None) => None, + } +} + +/// Control chrome. Held takes the LIVE family (blue) — the same family the +/// playlist strip's ACTIVE placard wears, because both say the same thing: +/// something is happening in the runtime right now. Deliberately not green +/// (green is good/valid, never state) and not violet (violet is bound/bus). +fn press_button_class(held: bool, wired: bool) -> String { + let surface = if held { + "tw:border-status-live-border tw:bg-status-live-bg tw:text-status-live-foreground" + } else { + "tw:border-border-strong tw:bg-card-subtle tw:text-strong-foreground tw:hover:bg-card-muted" + }; + let cursor = if wired { + " tw:cursor-pointer" + } else { + " tw:opacity-60" + }; + format!( + "tw:inline-flex tw:flex-none tw:touch-none tw:select-none tw:appearance-none \ + tw:items-center tw:rounded-sm tw:border tw:px-3 tw:py-1.5 tw:text-[0.8rem] \ + tw:font-medium tw:transition-colors tw:motion-reduce:transition-none \ + {surface}{cursor}" + ) +} + +#[cfg(test)] +mod tests { + use lpa_studio_core::{ProjectNodeAddress, UiButtonFace as UiButtonFaceData}; + + use super::{HOLD_TTL_MS, PRESS_WINDOW_MS, RENEWAL_MS, button_identity, press_button_class}; + + fn face(endpoint: Option<&str>, id: Option) -> UiButtonFaceData { + UiButtonFaceData { + node: ProjectNodeAddress::parse("/demo.project/panel.button").unwrap(), + endpoint: endpoint.map(str::to_string), + id, + } + } + + #[test] + fn renewals_stay_well_inside_the_hold_ttl() { + // The whole teardown story rests on this: if a renewal can be later + // than the TTL, an ordinary hitch reads as a release. + assert!( + RENEWAL_MS * 2 < HOLD_TTL_MS, + "a hold must survive a missed renewal" + ); + assert!( + PRESS_WINDOW_MS < RENEWAL_MS, + "the hold must be established before the first renewal is due" + ); + } + + #[test] + fn identity_degrades_one_piece_at_a_time() { + assert_eq!( + button_identity(&face(Some("button:gpio:D9"), Some(3))).as_deref(), + Some("button:gpio:D9 · id 3") + ); + assert_eq!( + button_identity(&face(Some("button:gpio:D9"), None)).as_deref(), + Some("button:gpio:D9") + ); + assert_eq!( + button_identity(&face(None, Some(3))).as_deref(), + Some("id 3") + ); + assert_eq!(button_identity(&face(None, None)), None); + } + + #[test] + fn held_never_borrows_the_bound_or_good_families() { + let held = press_button_class(true, true); + assert!( + held.contains("status-live"), + "held reads as a live runtime state" + ); + assert!( + !held.contains("status-bound"), + "violet is the binding/bus convention, not a diagnostic one" + ); + assert!( + !held.contains("status-good"), + "green means good/valid, never state" + ); + assert!(press_button_class(false, true).contains("cursor-pointer")); + assert!(!press_button_class(false, false).contains("cursor-pointer")); + } +} diff --git a/lp-app/lpa-studio-web/src/app/node/face/mod.rs b/lp-app/lpa-studio-web/src/app/node/face/mod.rs index 3b7d25a1e..858226cac 100644 --- a/lp-app/lpa-studio-web/src/app/node/face/mod.rs +++ b/lp-app/lpa-studio-web/src/app/node/face/mod.rs @@ -13,17 +13,21 @@ use lpa_studio_core::{NodeUiOp, ProjectEditorOp, ProjectEditorTarget, UiAction}; +mod button_face; mod fixture_face; mod node_card_drawers; mod node_card_section; mod node_face_body; +mod output_face; mod playlist_face; mod shader_face; +pub use button_face::ButtonFace; pub use fixture_face::FixtureFace; pub use node_card_drawers::NodeCardDrawers; pub use node_card_section::NodeCardSection; pub use node_face_body::NodeFaceBody; +pub use output_face::OutputFace; pub use playlist_face::PlaylistFace; pub use shader_face::ShaderFace; diff --git a/lp-app/lpa-studio-web/src/app/node/face/node_face_body.rs b/lp-app/lpa-studio-web/src/app/node/face/node_face_body.rs index 9223779d4..335906bee 100644 --- a/lp-app/lpa-studio-web/src/app/node/face/node_face_body.rs +++ b/lp-app/lpa-studio-web/src/app/node/face/node_face_body.rs @@ -2,8 +2,8 @@ //! plus the drawers under it. //! //! [`super::super::NodePane`] renders this when `UiNodeView.face` is `Some` -//! (shader/fixture/playlist today); nodes without a face keep the generic -//! tab/section body. The body owns the full-bleed container: faces and +//! (shader/fixture/playlist/button/output today); nodes without a face keep +//! the generic tab/section body. The body owns the full-bleed container: faces and //! drawers emit flat //! [`super::NodeCardSection`]s that span the card edge-to-edge, divided by //! 1px hairlines — one surface, no inner boxes. Children (the playlist's @@ -22,7 +22,7 @@ use lpa_studio_core::{ use crate::app::node::NodeDirtyTint; use crate::base::Platform; -use super::{FixtureFace, NodeCardDrawers, PlaylistFace, ShaderFace}; +use super::{ButtonFace, FixtureFace, NodeCardDrawers, OutputFace, PlaylistFace, ShaderFace}; #[component] #[allow(non_snake_case, reason = "Dioxus components use PascalCase")] @@ -105,6 +105,30 @@ pub fn NodeFaceBody( on_action, } }, + UiNodeFace::Button(button) => rsx! { + ButtonFace { face: button, on_action } + NodeCardDrawers { + node, + sections, + advanced_open: card_ui.advanced_open, + platform, + pending_edits, + dirty_tint, + on_action, + } + }, + UiNodeFace::Output(output) => rsx! { + OutputFace { face: output, on_action } + NodeCardDrawers { + node, + sections, + advanced_open: card_ui.advanced_open, + platform, + pending_edits, + dirty_tint, + on_action, + } + }, }} } } diff --git a/lp-app/lpa-studio-web/src/app/node/face/output_face.rs b/lp-app/lpa-studio-web/src/app/node/face/output_face.rs new file mode 100644 index 000000000..747397f12 --- /dev/null +++ b/lp-app/lpa-studio-web/src/app/node/face/output_face.rs @@ -0,0 +1,166 @@ +//! The output card's permanent face: the test-pattern toggle. +//! +//! The first question anyone asks of an LED output is not "what is it +//! showing" but "is it wired, addressed, and alive at all". The toggle +//! answers that directly: while it is on, the runtime drives every pixel +//! full white instead of the graph's frames, so a dark strip is a wiring +//! fault rather than an empty show. +//! +//! Sustained the same way a button hold is: the pattern carries +//! [`PATTERN_TTL_MS`] and is re-sent every [`RENEWAL_MS`] while on, so a +//! closed tab or an unmounted card restores normal output within a couple +//! of seconds without this component doing anything on teardown. The +//! renewal task is scope-owned and dies with the component. +//! +//! Visual: the on state takes the ATTENTION family, deliberately not violet +//! (violet is the bound/bus convention) and not green (green means +//! good/valid, never state). Attention is the honest reading — the output +//! is being overridden, which is a temporary abnormal condition someone +//! needs to remember to undo. + +use dioxus::prelude::*; +use gloo_timers::future::TimeoutFuture; +use lpa_studio_core::{UiAction, UiOutputFace as UiOutputFaceData, UiProductKind}; + +use crate::app::node::produced_product_view::ProductPreview; +use crate::app::node::{NodeCardSection, map_view::MapViewOptions}; + +/// Renewal cadence while the pattern is on. +const RENEWAL_MS: u32 = 1000; + +/// The pattern's device-side auto-expiry. Short on purpose: an abandoned +/// override should not outlive the tab that set it by more than a blink. +const PATTERN_TTL_MS: u32 = 2000; + +#[component] +#[allow(non_snake_case, reason = "Dioxus components use PascalCase")] +pub fn OutputFace( + face: UiOutputFaceData, + /// Mount with the toggle already on (stories render the overridden + /// state without running a renewal loop). + #[props(default = false)] + pattern_initially_on: bool, + #[props(default)] on_action: Option>, +) -> Element { + let mut on = use_signal(|| pattern_initially_on); + // Retires the previous renewal loop when the toggle is flipped: a loop + // runs only while it still owns the current epoch, so a fast off/on + // cannot leave two loops renewing the same pattern. + let mut epoch = use_signal(|| 0_u32); + let wired = on_action.is_some(); + + let preview = face.preview.clone(); + let endpoint = face.endpoint.clone(); + let toggle_face = face; + + rsx! { + NodeCardSection { label: "test", first: true, + div { class: "tw:flex tw:min-w-0 tw:items-center tw:gap-3 tw:px-4 tw:py-3", + button { + class: pattern_button_class(on(), wired), + r#type: "button", + role: "switch", + aria_checked: "{on()}", + disabled: !wired, + title: "Drive every pixel white instead of the graph — the wiring check. Auto-clears a couple of seconds after this tab stops asking.", + onclick: move |event| { + event.stop_propagation(); + let Some(handler) = on_action else { + return; + }; + let next = !*on.peek(); + let mine = *epoch.peek() + 1; + epoch.set(mine); + // Optimistic: the toggle follows the click, and a + // refusal (never-rendered output) arrives as the + // op layer's warning notice while the TTL puts the + // runtime back on its own. + on.set(next); + if !next { + handler.call(toggle_face.clear_action()); + return; + } + let face = toggle_face.clone(); + spawn(async move { + while *epoch.peek() == mine && *on.peek() { + handler.call(face.test_pattern_action(PATTERN_TTL_MS)); + TimeoutFuture::new(RENEWAL_MS).await; + } + }); + }, + if on() { "Test pattern on" } else { "Test pattern" } + } + if let Some(endpoint) = endpoint { + span { class: "tw:min-w-0 tw:truncate tw:font-mono tw:text-[0.7rem] tw:text-dim-foreground", + "{endpoint}" + } + } + } + if let Some(preview) = preview { + ProductPreview { + kind: UiProductKind::Control, + preview: preview.preview.clone(), + tracking: preview.tracking, + frame: preview.frame, + focus_action: None, + on_action, + map_view: MapViewOptions::default(), + } + } + } + } +} + +/// Toggle chrome. On takes the ATTENTION family: the output is overridden, +/// which is a temporary abnormal condition, not a fault (error/warning) and +/// not a good state (green). Violet stays reserved for bound/bus. +fn pattern_button_class(on: bool, wired: bool) -> String { + let surface = if on { + "tw:border-status-attention-border tw:bg-status-attention-bg tw:text-status-attention-foreground" + } else { + "tw:border-border-strong tw:bg-card-subtle tw:text-strong-foreground tw:hover:bg-card-muted" + }; + let cursor = if wired { + " tw:cursor-pointer" + } else { + " tw:opacity-60" + }; + format!( + "tw:inline-flex tw:flex-none tw:select-none tw:appearance-none tw:items-center \ + tw:rounded-sm tw:border tw:px-3 tw:py-1.5 tw:text-[0.8rem] tw:font-medium \ + tw:transition-colors tw:motion-reduce:transition-none {surface}{cursor}" + ) +} + +#[cfg(test)] +mod tests { + use super::{PATTERN_TTL_MS, RENEWAL_MS, pattern_button_class}; + + #[test] + fn the_pattern_outlives_a_missed_renewal_but_not_the_tab() { + assert!( + RENEWAL_MS < PATTERN_TTL_MS, + "a renewal must land before the pattern expires" + ); + assert!( + PATTERN_TTL_MS <= 2 * RENEWAL_MS + RENEWAL_MS, + "an abandoned override must clear within a blink, not a minute" + ); + } + + #[test] + fn on_reads_as_a_diagnostic_override_not_a_binding_or_a_blessing() { + let on = pattern_button_class(true, true); + assert!(on.contains("status-attention")); + assert!( + !on.contains("status-bound"), + "violet is the binding/bus convention" + ); + assert!( + !on.contains("status-good"), + "green means good/valid, never state" + ); + assert!(pattern_button_class(false, true).contains("cursor-pointer")); + assert!(!pattern_button_class(false, false).contains("cursor-pointer")); + } +} diff --git a/lp-app/lpa-studio-web/src/app/node/face_story_fixtures.rs b/lp-app/lpa-studio-web/src/app/node/face_story_fixtures.rs index 70cd3baff..c51495582 100644 --- a/lp-app/lpa-studio-web/src/app/node/face_story_fixtures.rs +++ b/lp-app/lpa-studio-web/src/app/node/face_story_fixtures.rs @@ -10,11 +10,11 @@ use lpa_studio_core::{ ArtifactLocation, ControllerId, ProjectEditorOp, ProjectNodeAddress, ProjectSlotAddress, ProjectSlotRoot, SlotPath, UiAction, UiAgentAvailability, UiAgentStatus, UiAgentToolRow, UiAgentTurn, UiAgentUsage, UiAgentView, UiAssetContent, UiAssetEditor, UiAssetEditorKind, - UiBindingEndpoint, UiConfigSlot, UiFixtureFace, UiNodeChild, UiNodeDirtyState, UiNodeFace, - UiNodeHeader, UiNodeSection, UiNodeTab, UiNodeView, UiPanelControl, UiPanelWidget, - UiPlaylistEntry, UiPlaylistFace, UiProducedProduct, UiProductPreview, UiProductPreviewFrame, - UiProductTrackingState, UiShaderFace, UiShaderUniform, UiSlotFieldState, UiSlotSourceState, - UiSlotUnit, UiSlotValue, UiStatus, + UiBindingEndpoint, UiButtonFace, UiConfigSlot, UiFixtureFace, UiNodeChild, UiNodeDirtyState, + UiNodeFace, UiNodeHeader, UiNodeSection, UiNodeTab, UiNodeView, UiOutputFace, UiPanelControl, + UiPanelWidget, UiPlaylistEntry, UiPlaylistFace, UiProducedProduct, UiProductPreview, + UiProductPreviewFrame, UiProductTrackingState, UiShaderFace, UiShaderUniform, UiSlotFieldState, + UiSlotSourceState, UiSlotUnit, UiSlotValue, UiStatus, }; use crate::app::node::node_story_fixtures::{ @@ -448,6 +448,79 @@ pub(crate) fn fixture_node_view_with_face(face: UiFixtureFace) -> UiNodeView { view } +/// The button face: the simulate-press control plus the button's hardware +/// identity. +pub(crate) fn button_face() -> UiButtonFace { + UiButtonFace { + node: ProjectNodeAddress::parse("/fyeah_sign.show/mode.button") + .expect("valid story node address"), + endpoint: Some("button:gpio:D9".to_string()), + id: Some(1), + } +} + +/// Advanced-drawer sections for the button card. +pub(crate) fn button_sections() -> Vec { + vec![UiNodeSection::ConfigSlots(vec![ + UiConfigSlot::value( + "endpoint", + "Endpoint", + UiSlotValue::string("button:gpio:D9"), + ), + UiConfigSlot::value("id", "Id", UiSlotValue::u32(1)), + UiConfigSlot::value("stable_ms", "Stable ms", UiSlotValue::u32(30)), + ])] +} + +/// A full button node card view with the face installed. +pub(crate) fn button_node_view() -> UiNodeView { + let header = UiNodeHeader::new("Mode", "Button", "/fyeah_sign.show/mode.button") + .with_source("mode.json") + .with_status(UiStatus::good("Running")) + .with_summary("D9"); + let mut view = UiNodeView::new(header, vec![UiNodeTab::main(button_sections())]) + .with_node_id("button-mode"); + view.face = Some(UiNodeFace::Button(button_face())); + view +} + +/// The output face: the test-pattern toggle, the endpoint, and the control +/// product the output is currently being fed. +pub(crate) fn output_face() -> UiOutputFace { + UiOutputFace { + node: ProjectNodeAddress::parse("/fyeah_sign.show/strip.output") + .expect("valid story node address"), + endpoint: Some("ws281x:rmt:D10".to_string()), + preview: Some(control_preview_product("output")), + } +} + +/// Advanced-drawer sections for the output card. +pub(crate) fn output_sections() -> Vec { + vec![UiNodeSection::ConfigSlots(vec![ + UiConfigSlot::value( + "endpoint", + "Endpoint", + UiSlotValue::string("ws281x:rmt:D10"), + ), + UiConfigSlot::value("input", "Input", UiSlotValue::string("Halo ring")).with_source( + UiSlotSourceState::Bound(UiBindingEndpoint::new("bus:control.out")), + ), + ])] +} + +/// A full output node card view with the face installed. +pub(crate) fn output_node_view() -> UiNodeView { + let header = UiNodeHeader::new("Strip", "Output", "/fyeah_sign.show/strip.output") + .with_source("strip.json") + .with_status(UiStatus::good("Running")) + .with_summary("241 LEDs"); + let mut view = UiNodeView::new(header, vec![UiNodeTab::main(output_sections())]) + .with_node_id("output-strip"); + view.face = Some(UiNodeFace::Output(output_face())); + view +} + /// Playlist entries: three timed entries plus one cue entry; Aurora (key 1) /// is playing. Every entry carries the child-select action the P4 /// derivation attaches (clicking a chip focuses the entry's child node). diff --git a/lp-app/lpa-studio-web/src/app/node/mod.rs b/lp-app/lpa-studio-web/src/app/node/mod.rs index cd4cf83a0..09dad7057 100644 --- a/lp-app/lpa-studio-web/src/app/node/mod.rs +++ b/lp-app/lpa-studio-web/src/app/node/mod.rs @@ -12,6 +12,8 @@ mod binding_authoring_section; #[cfg(feature = "stories")] pub(crate) mod binding_authoring_stories; mod binding_chip; +#[cfg(feature = "stories")] +pub(crate) mod button_face_stories; mod config_slot_row; #[cfg(feature = "stories")] pub(crate) mod config_slot_row_stories; @@ -33,6 +35,8 @@ mod node_pane; pub(crate) mod node_stories; #[cfg(feature = "stories")] pub(crate) mod node_story_fixtures; +#[cfg(feature = "stories")] +pub(crate) mod output_face_stories; mod panel; #[cfg(feature = "stories")] pub(crate) mod playlist_face_stories; @@ -84,7 +88,8 @@ pub(crate) use binding_authoring_section::BindingAuthoringSection; pub(crate) use binding_chip::{BindingChip, BindingChipDirection}; pub use config_slot_row::ConfigSlotRow; pub use face::{ - FixtureFace, NodeCardDrawers, NodeCardSection, NodeFaceBody, PlaylistFace, ShaderFace, + ButtonFace, FixtureFace, NodeCardDrawers, NodeCardSection, NodeFaceBody, OutputFace, + PlaylistFace, ShaderFace, }; pub use node_children::NodeChildren; pub(crate) use node_detail_popover::{NodeDetailPopover, node_status_label_class}; diff --git a/lp-app/lpa-studio-web/src/app/node/output_face_stories.rs b/lp-app/lpa-studio-web/src/app/node/output_face_stories.rs new file mode 100644 index 000000000..4598349be --- /dev/null +++ b/lp-app/lpa-studio-web/src/app/node/output_face_stories.rs @@ -0,0 +1,50 @@ +//! Stories for the output card face (node-actions P4). +//! +//! One diagnostic affordance — drive every pixel white instead of the +//! graph — over the lamp preview of what the output is actually being fed. +//! The on state takes the attention family: an override is a temporary +//! abnormal condition, not a fault and not a blessing. Violet stays the +//! bound/bus convention and green stays good/valid. + +use dioxus::prelude::*; +use lpa_studio_web_story_macros::story; + +use crate::app::node::face_story_fixtures::{output_face, output_node_view}; +use crate::app::node::{NodePane, OutputFace}; + +#[component] +#[allow(non_snake_case, reason = "Dioxus components use PascalCase")] +fn OutputCardCanvas(children: Element) -> Element { + rsx! { + div { class: "tw:w-full tw:max-w-md", {children} } + } +} + +#[story( + description = "Output card at rest: the test-pattern toggle beside the endpoint, over the lamp preview of what the graph is feeding this output, over the collapsed settings drawer." +)] +fn default() -> Element { + rsx! { + OutputCardCanvas { + NodePane { + view: output_node_view(), + on_action: move |_| {}, + } + } + } +} + +#[story( + description = "Test pattern on: the output is overridden with full white, re-sent every second while the toggle is on. Attention-coloured because it is an override someone has to remember to undo — and if they forget, the device's ~2 s TTL undoes it for them." +)] +fn pattern_on() -> Element { + rsx! { + OutputCardCanvas { + OutputFace { + face: output_face(), + pattern_initially_on: true, + on_action: move |_| {}, + } + } + } +} From c3a09bb59bfe4eb9ca47d84abf485128a20602d6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 23:28:16 +0000 Subject: [PATCH 5/7] chore(studio): auto-refresh story baselines [validate-stories] Captured in the pinned CI environment by run: https://github.com/light-player/lightplayer/actions/runs/30672524955 --- .../studio__node__button-face__default__lg.png | Bin 0 -> 11945 bytes .../studio__node__button-face__default__md.png | Bin 0 -> 11202 bytes .../studio__node__button-face__default__sm.png | Bin 0 -> 9669 bytes .../studio__node__button-face__held__lg.png | Bin 0 -> 5897 bytes .../studio__node__button-face__held__md.png | Bin 0 -> 5327 bytes .../studio__node__button-face__held__sm.png | Bin 0 -> 4881 bytes .../studio__node__output-face__default__lg.png | Bin 0 -> 19563 bytes .../studio__node__output-face__default__md.png | Bin 0 -> 17900 bytes .../studio__node__output-face__default__sm.png | Bin 0 -> 13480 bytes ...tudio__node__output-face__pattern-on__lg.png | Bin 0 -> 13755 bytes ...tudio__node__output-face__pattern-on__md.png | Bin 0 -> 12536 bytes ...tudio__node__output-face__pattern-on__sm.png | Bin 0 -> 9601 bytes 12 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 lp-app/lpa-studio-web/story-images/studio__node__button-face__default__lg.png create mode 100644 lp-app/lpa-studio-web/story-images/studio__node__button-face__default__md.png create mode 100644 lp-app/lpa-studio-web/story-images/studio__node__button-face__default__sm.png create mode 100644 lp-app/lpa-studio-web/story-images/studio__node__button-face__held__lg.png create mode 100644 lp-app/lpa-studio-web/story-images/studio__node__button-face__held__md.png create mode 100644 lp-app/lpa-studio-web/story-images/studio__node__button-face__held__sm.png create mode 100644 lp-app/lpa-studio-web/story-images/studio__node__output-face__default__lg.png create mode 100644 lp-app/lpa-studio-web/story-images/studio__node__output-face__default__md.png create mode 100644 lp-app/lpa-studio-web/story-images/studio__node__output-face__default__sm.png create mode 100644 lp-app/lpa-studio-web/story-images/studio__node__output-face__pattern-on__lg.png create mode 100644 lp-app/lpa-studio-web/story-images/studio__node__output-face__pattern-on__md.png create mode 100644 lp-app/lpa-studio-web/story-images/studio__node__output-face__pattern-on__sm.png diff --git a/lp-app/lpa-studio-web/story-images/studio__node__button-face__default__lg.png b/lp-app/lpa-studio-web/story-images/studio__node__button-face__default__lg.png new file mode 100644 index 0000000000000000000000000000000000000000..e41a2e648ef98fb6664631721e26da22a4ed619d GIT binary patch literal 11945 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 Gy#EKMHH_~7 literal 0 HcmV?d00001 diff --git a/lp-app/lpa-studio-web/story-images/studio__node__button-face__default__md.png b/lp-app/lpa-studio-web/story-images/studio__node__button-face__default__md.png new file mode 100644 index 0000000000000000000000000000000000000000..18e81b3ca6de274cdbea1fcc9de694283c5f2c4f GIT binary patch literal 11202 zcma*NbyQSe^fo?p4@k#QA}I~R(9)8E(kUQaLwCo3NC}8^iL@ZyHImXu3(U|d-Sr#% ze81~mzqQ^!-udI+bN1fn+0Wkl+_TQyd!jYe6$x;uaRC4Tfzm6vHvj zBgk<+JE2GaBzpD+#z`Otse%FT;+WXtmIiki=dl7)g`sPR;dT01f0A?@{&7gMLywof z3<}cSz2P(N)`d7{bQq6`;}Cl5JT^c6#ZpcTQuodyV)o~+nT!+oyT-3lx=2=?rFil_ zJm65(-#ItPvF{`tFwcPPsw*O$>Z5EuBP&efNA z+P`aZnxz_hl6!73By^$?P!@lY z9UE6tVRL#X%t0=v@c~?~AtOVO_tj?Xhm1J3*k08I`1n-ckmoHg^jRRn%@lAg*hCKi{GLccQ}llJbIFX-i?htbGoxc;{VU=o z7@>T8iFC-Ho1^Nma7^jG_6t(uR4F5CE{-772m<4d;N6&LA{?cR_K_G(gG!s~8?bEozvng~*W=w#rTK!ShsW&ZgP8U#37Uz<+5T-<3UFyiy>lh$ZG$t+c z;2eP=uIh*J}50&+e`_bW3S3xej?mPvGBi~7xn#HY#@?f@seAQC@g)1-c z+J_*{QPZ_>rt9;oT}Rgo`xV-fo?BGMDvQCsE8gs{E`7x7e~1BqouWWfYHeDeT^;~H z1ja@_JFN_7c*t9DC-T`cp+m(202m<1?hX_33V7@mOa33B9}(E3Ef5Wv8SMWf{Qq0t zTF&768ARmq-cmSCsN<0uaG)Cg{le1h{qR8Fl5ai#{cFJx`;f5D@fv|h&PIS@ts%ck8)ZfR_PGQpjzuRs1fx)WXA zhci5A!nei!$!Zh!9`}*IOU2@^JJ&82SgqKSST*Le!#($4XN?CRhL$TNRS%Q6BaMEF zNrAnpo$2nmmqYiqYg8`9M*Uwut&@oUdGRdV`Owcy##QsrApewF%fhc}lRsf0K0~86 zh*xV_PW9ywPdvK8@FY{mR)_09JfLJh!!$^Boi=*Su4}*O()BybO&1+?779lbDCK$kp6B6o&^-_=bE|CXjtk=liEt2hoyU zO9{#^eYZyBK*rco;X7FqAMZuX^+ZZs9hkHRoznKYD{-whk@|E|^>Ovn%yRYh?q3*B zX57{k=l=4V^Lx;QWLp^3w-5{#DDh0V5!w1TB))?I7zeE~GJK^8F*o?6;v>RhJR2IC zW7!B-Jt=D;8o¶oqmQ-k#MckR}xgcxs%wG%&Jb*A06XpEQ8n&482{XDC}W@k2^ z!z~OIQ;+$|x50GmUslW!{q9-YcEt;gy)m+PCp3U-o}2Vj^I*i?I{6ueHuAdZYZ}RF)xMIu|1^+Qi?-(!`D3y9B~q z0!O7&sgmP|(M++Q=gheAA}mMog+ZONYa32?RmoUrCtq@Uy2ssLtKKa3$~%Ks*aH+h zrK;;eey}_q7Z}2s65UJn@&>C&HQOIH&a~AcpaJgO{p{Qm{tpd~2!_g5G}(A);4ggW9=GI8%qV~a&u}d_iGdFP4ab_ep9Psj zCy`QFKi4UuKzeZU+51l;4tmAqpOcduMjr78^iL6ihb0a&SVDp#(P_~m(8Rq{NRgx; z-5@0|upTAaWd9@@13;v4RA#qy+8%d9R^B=}@PDH1dsIAn56LWr7~x7hWd3^ZHXxiw zM+!WI7kWGTf@eEnm-Tww`_{sDD{&eNQ;jQlmG_d3S{zQTZ9PH<2%>M2(JAo{2I+f~ z4cPE{xYiO6sb*CI~OA^3$BrxAgTi01F7-<+TEY zvVy&94ZR~V*AF0|EJ)O@)OkkNd<$QmYl$$?l>x)Sf#bK6GZNtW{Hz1#kE5b0#qUm) zmbw(*oUk7hsms0I3!Piq7wIi=ITUWNSnsfap4~3K5=G-`nUV87U?#+TSaM)L z*TRf-+wu{n3k>Hxqn)jKNsZ7rt(3Z>5`BM+AV_wxH_Hr}KQw^x`2zd4L_oC;5LLSh z?yXEI$AJ&u)6r!yl69g!2KdZ;B8yF&7R_s${9K+zAX7tvif5R&R~MT|PE2g@EpNRp z>9cBvM1`Q3o-WF?Xxd_i=AQ-Op#55|k zay&zkFh4CwN7#-<}1!w57t|a_n%c6?GCs(bU|YzL(Dx(`Rsu@?f974#XP`o6UKxdN_kxIjT`# z{0BAXO`>P$cbR}6MUu?S+$_x8^^jxRez0=J1+tD|{sfQZ3@hpy*WD`wtl3?sjF^a3 z@F^@3sf5X*&Ln^M=mOdOFgH2Kr5%E~(=x@GCehNPMp8Pv{OU<@C;iu4>dvLB7NIAR zF1PW#VZD*(XV(J7ZFUw|Z|&c9O)#oGk>~To#?d3zcp7RA6_kzSq2(K$koV7Qowe3(HuX#*Q# zSZ#aQAiHQ@OH7_{dfsPJQcgw%N!Sj4|Cvoo(uV?>XavCQmJkx2xe5I$iwTN311QQ- zfs4Q3ehUQvxAL5s;TlP>aSKEX)ThD~ObNk1JsEi)cfQ`XpZ}9+v_!vx{_ z0|GNu&iy4wctW`qbt- zjEw^!SNOqbzFTJvDus+9AX4zq>CfZMC(4L0Uf=V~Xb})6v|uAexk)Rb?M+_!%b?{H z)6{;G5=g5ztUK6>r@?ePsQu)%Yd^x~mY9@`Uv~*&3V|EgC8m@ z&bMr`Si&0~6e=eF1;#jvz;@b8rV=LYDEFqt5&`|(@JXz|%-^h02{fx<`-Olj{0SLq zK(&b5R~@pZ-}qMxqR79v3+8!H6V^@_BOp{d&-oA^>i@dDRdBoG{=K?@uvXvxouvX?b0nknioBu9P*hKwo(7}HOFUhVs09dDEySQNR)N=1Sq^fdg zQ{%=lyg@qMBNf;6vHso1LdZFl2c0S49vHd=#wXAzWHYu;Z3 zKn0a48l(S=Ll)9rQwtQ%*`oKU+J4Xcje#X)uuK)lSNySkDU=o{2Et|X68AAt+8MS* zSFBj_=Qn%;y0r-q19^h?kN}-(*=@zs8AyO=|N2i7{X>&Y2J@}W)1hJX70yr?5c&vl zc@Yp7ab=J$XfVe&$~QpwHS=rgfg?6B@b@FDN0=&D2qo`rPyA1>$p0Bb1o9938wvv| zhWy79rK@Wb>+U~1PcSJaI+m1^)E|NWq45$QiT14hf4jkO_=y_@QitpRA09SPQYswY zABYQRBtcS(mKuPw4eI~^UfqWjQpBbZkBZQ+QKXfI4Svwk|LUb?x)T2e9b6C!*%jJdz!&0|>?&H1jv61z zQZcVi;bHbubX%RwT;?fS(3T)12}5ytK83J;%;Sb2H1G(c}yZ-y)DTBg>gc%9c@ zUOonOpB!cZ89&I$%FNCo;OW>V#0pcwRI~_qZhVlO|MR=FH&M^9L0OE z@e=-Tm|&R2(IP_gVeNEJv`;ZdEQX{sc6xeZaDmq}lId9sxor*Y1_3QL)nmY;yQU>t zsk)O^tV5|wHn!znCf5!aj}=G^wWw%&tG;8_{#QXoL}^B-#cXLZp;b~4xGEU_^*nU~{KcTGNn)9{}TpgHj zfN^8s0CcRJ2coVcYwz2WT||++|Ey`rz2$+w$-LeS`2IDhF{X*vsLw)1u;@szA=04d zI&5}_z(7Vz8P$>CH~;#qYJ`=>0zP;wSoTY|96p%4e81bOz^Rs|OTJS+mx!W^6JKAj zk*XS}@u*$Enf3nF-chYk?^?(~_mKEOrP9#Jz=`+)K0t8_Hy8~a6%O#Lf+pi)|8Kie z63#nIe`R`R007&)0)QBZ09dnO0T|-h*$(YK4P}X(!jRt{zOv6V22{_tc-!b>28VK^ zf2$`to+`^IRq$o~w)FX9<$P+-Eh+r2RFwe$2ZR8?opgI;l;DrCUW#;A@(A1y3ME#f z{`^HC+MoGmQm_$j=bjW<%bD zWW^Ma96eEi@s(Gk`-eX_d9&9C8~}I_7qnIp5`22we)Y6hE~#yKTt2Y~_{MSMzD0NC zEW_`vn)k)=YpLF~MO|qhb~d?CME_}H%@bi^cafrJjGTC8wky$D&B;{MdloAbr?B&t z7z;=%qC>!K!od$rk}Rdv+LBnVv22urOz{o%kB4^EArTUyKe&u6|=RoLxFcW*f&Jth@P%9RzsV$HW-_SyH2?wd&}l*j9h5`CL0Z z#bosT1qJ5r2r9G$CPov>OiF4lqLsWzxF>;!+w5MIaV6`H{+y9ezg9g`B=bVrV*mSq zyVqjVOOlL&fL}k*cb?@$2bmlqrnX58mXl1V8|gV2N2%MR=54W9Qe@ z?6q$5s}Na_hCID{-inE^v};4S#+|*t8u0Q<=ZZR}$o=L0--gr$DrR~=N9RT`L_9m^ym|*dMaGL;CntZ_Dk1J2uN=G;`oVlGS~zgH;Fbf$&o3d9<%RbF#(6V z2%e+TLO&%>o=aF)nxe$8Tvw2k|MPH+qG4}b@ZnBx-Be;j7B;^xkq!D3jJDTx-;9E#`C5Pf?lo&s zPrPo@n=ZR83*mDJSUOs9lQMo$Ig1b!p%pXmgGfsB=TZ_yIwh#V}l;0F2{ zr3cz6CfWM)GDQ^+4AiqhUH|AX)6C!>^mXQCWqp7c7^1mOb(e`n%1l`QynFxCs+b28 zNW!M1xy+4*)@?M!a>|t7#3CsSP$FG#B zDn)A6T#N?@x}J+9zv--6P;q;HB>utBJ0>{Hfl9vJlpqk!BD(y3+UB`7F8Dm3?pKK` z5osp&*%Ry3;Bm&5VNVi_Rh{b-X`vDYe%_MzNo6&s8eS&@$w@wXOY$LUYH4S)hFm{+ zz9SgY6ZrCB!T5|t1s=1^Q_)&hCzUfu?dA$~A(cZ~#(s_xUKV8#hqJ&$!5;ebM) zYbJ9L=H16o=G&Nm%e%T;al%6(o>+M|^$}n4lI&o5T$p&&k@?j2=geGtGe272r6n2` z%NIA+x03MAxumyAnlPF~ia5OKPE?3i2C@w83VW5itJ^%dAOgqpJTvHLcD7?`MOpZoT@o-v4 zqVOsC8xCoSLSp=a0m>I&qi&6r3uB&*eAB{?X}gdPV$wzjVfvYQq<<97!R`XERH$G9 z?*?Bg7ltAefBwSUR0qz$Il}i~>V?0K2JGH39%UN4w9ba#KLkK_D{Pyu{cV)Svc zRN{1H>?z!43$LeU@H1fxB*0C%nG$@+&Pgr zMCc)6n+r*B!n(bJ3vA&&lmrOD1lAY@OZKPjltD0@23d!#W90juH$e$0Y-2Qsv=;4blL%Qf~EX97T zbD)AUtdTHf?kE{m<`OGR-%h5PXRl|DHX$NkaEYp}j>WuBD}6$0+BpAwQbIQe-fQ~K zl<9mO1Gpfd&S_16b>7G4abv?lo!@51*wg*(`YqiJoE{YjDtq;`l0L(_RglI+#p*mc z^OHCl$Yv|TTQXITaLg$_6(?X;sSuAnWx0IGMqxi>+Ovdc%MzEFhj;<6lBzNlr;;(W z>nL)f(WlJ=rSMl+&#dh!uKX-4uNTwFuKXr*8d^9`#Awk=^-v(e0kN~G zb&+8^Y50*xXgq;B9=$Yb)AfU=EicNX?iX~dZwvT4CZ%&fkOBH0=316#1pfl!R)eAl zogi^48k*MAbg5tHzd#}lGDKVi7z|gM^?%Ho7wR|0+P^CN@ak!_yc{r;(0}EjaZ>-& z-az^%mC>7a#2JIyGRtrCTZqVOUuj=fEGQ{%-&tKn)39pY672=`#d-FVuGx9lwI5kx9(8Ge}Kg^B`#kiO={`DV11B8WT2l8QIZ6d#OBAZJ`U5xTm?vg1@T*JT2Q69${jf#K(o$%bEi!u1& zfTlbG9r(yJW>!oLyq2}I%q2i&g!?si`VEDf*EoM5eY&5iYZV34xz<4pJ@&ERn}*A@0FPAhrp0{2 zWrLQevb1pLq_Eo5wYBpzQcO~r{;{E`^g0CrgTNLe>Wh^0n}R-N)79m^k#69B@fsc6 zmT9din!VtdqkqMi_Uj`mGB}g|?A`yO%tpQj$nHh5{A&Gw6Hwge7nyG11))49{S^OS zxRU~4KY7+WvVvv zaqsLTvpjW#^{%YMv*pT=n{o7hu));^$Xe7z5#_ zcdcB`a-aX+8)f@mx$slY`C?2xNc|{xw0?s!a6gN4uUF#tCVersLFvf#E-UflrPO9j zcuZE3vdr~G{6xW8nN?fyFQ))WK;t2WM4K1)?b%zz)o919OJ9+hpW(-a&1w0EHoBu_ z5Xjm}OsIDEpBmx%DDl6m*51&`&ryRT`&~>dgJ-Eoi|szZVXSH8y8}B)EV7d zg}SuR+(E{M^Y*LOxXxB6*TCrVbPS(9Z%~9=WPP!!xJf>>rau*_LAP{a__O7|9ZLy? zLI`-k(aO#Iv|2nBN!ILbS%CHv0LN z0t4_<2rB|!#I&odKjkjZXL0MTF(-`6NPbhYuJj$Y)GiNBR^`syS<5kJU#x92k`T~@cUY& zUuZ-BMURq%uoBXmM+UTyXixl}a{MFWi;3@vnNe8L9qA-7r(K4ygk$%U42U}S*s^cI zx)e5+6xlCbpcWW)qKJ#E61uB_sM_J9{o$wkBHy+oYwfOTk6g4?D}3&zSH;hwft`7S zY(Apk*Ag&-d(*D2IdIYK>*o*8q_pw6_zYRz};LYy`gVo6Sv)L>hjpzvwlq5WU( ze!)elheiTrFHRTXcvJqmU+^tU!xQ!^^tPryb_z6q1^^H*>3lZAEw-2Jz{gu#cck+( zQHo*iwjI@!LB>C{wP^gl@q zhS|canlt}n9T~prdzf=L2xE$@C>d=d+$S`%D%9gyFL1gqDAMwbiI}n(yOs*DHwtSn zth|n_Nb%-txG@RZs>$Hl@3r9Zi|8z#1HMMa21MWgA1a(UwQ%i-sKT z&N?dVQQhMug%if$A^P96z9IWuDV=u}1UY2S$0!vvq~lQ$FYm+G^q<6a4ijbMU_4;u zubKKKGO|Z}<=>9#U}wue(Qz>3>y;wOkd7A4mQ&k4bM-KtQCi!9O!E4dH6`<@={FwG z|E+3pE`<~;GnP#ay0dvqRiTfN`KOPQzTukLk$Y|}z?ZYji^#ReCXCW4vrn(D_##{2 z{>xQVWw6v_rmR_}Kj!e+MG53fHFF1teRKA#^0#u_zA7(~G>wnD%6mUIdU*wLeaWz~ zj#;0@^W3Ahp&!Ju{Ju>7xwVZF%yTb8=ku-O5)Yu9Dm2M5Kx|y5(*R4NoB{OV{2M`t z+&%!1t$tZB007rR`xkIW2ILi!h4sk%Y-L2H1tDk2h+DA0bbO=HkbGocsFZ??Ikv;6n`s1(LzT9CFKn{2j`_M5X}wd&~%+ z^9bWVG4{XXW4`qN-2=ZvWoKqvFs} zBP;jKIO#`}qwbRS!0dbUf6}n68+-U~=b%gk)W{(ld1?^=@TE0a%3Xfr`{DlPZZr`5 z&tJRPtfsuBt4D`I6)%2n51}%1f(*KuDjT>=88NCwcuY9s>k| zL_wt`G`*I7FFh*k+kpC=A46PsGt*Xt=W88X3Ywf&cBZleH>fxf12Cq690fp|Lpmw} z2QQwQl#u|F4*yz8>|AW^aFT`XN_9uIDu&XF`VePMBbaYIb99}|*|SMFJ~uv)^T%)L zfM+1D7e=}f|pvlq7GO~{?&{TMMR84v68C)WK7=XQ?^krYUscRs1) z6|wFMu1O${uW?|IH~<2P$^!CC{_g&t?ta2za0SOAPSVw)H#(FjV%Y-fb3sX{s3T%08Wu)!kD- z)^yLGgwMsH*Kxe#ImqZO6kKb-WKHh6=rXC!t&i>>@FC=0T_JhDIR|kQp*NkHViw|E zmf{>fTFkaJ7j&N=Utnn}e4msxZpf7A#?bjC_IwFv6nj*o%tu^D(nC{51Ge2$?qHJ` z?A`44k}Y$9)m=o94mre*F@la2JIx3;Kw{hLJ(UNktTjE3nRm4TgCtX5M^9%cwXbo@rJ{-9m}Fbo6X)5fGP8x{1cf3Ue?!iyrgN6;Bp`Usrw8yhI=7lx&{hdihSn3W!lZ$#FIs zmu<)_1&UYsfu=T#G63shtSMt2qsc*swp6L&MQ6arlW6_Zyi#JDWjmN$LCf$sc<=4R z$M4%djV-FMQcC0E!-=;H5wt4jup9YI7IU^&Cg z2#2p-G&&F@-=AP-TjJ3H$1uf!v&coRReHY~aQ3MisA~;d>^OI!UX`rs7_vM zgk$RNwK2QVPrrEY==qB@`Ye%lj`m3m?`=$x79uUlM~pcS}q_7s+jv?KXE*ykg3p%e8w>_5x@NmUAf z(Lqld^rY;tpveDd(347Y6&AVvdw>S&6q0({C{M`Ke(Bu)KaQdGVt4)4|Lfh zD#^2g)`z_CeS=XOM}&%++cn&nqN#10S5B{Lo|;9UsDh7gHSOwL+^u|ra8V){9oIG2 zfo>rwjejsK3wN`J7S)mmmJb|CBFjEoui*iPx!82{&IzE@X~SDdVeuF_k7qB7^HplJ z;cmX=e>=`Oy;A3xzVVfmzO9-@xrT+RN!*G%zB6Ke)-FN(C$Z0i8x>KY^dKC`7yfoPqg~K+glYA)#8ZKDe+m>p*U1CN^6| z(a1cmyP8sMmG7K(X(4vZ0}?-fi{tzoHW$(eR=r&x7Orv1|8vq9?Uj(xlx^ z)-`~)sPWHp5A30uZ#*Eyg}_~sW*6HMz_5@whP$2=QoAjRP3iGvj0Pdh1A>~VKYvba z)&QCO>}NJ{?xPRv`YUX#+Cxh_SkXNorn9joyg24gGW+E0NrR^Pc335aDn#n-(k#42 z;PJ<#@!>an)Bu%Na)>9?E15@6GNs~*Tg*@8uP3(;^_Id>tJO)R<8|MsY*Cd-=Dm2L;{2CNO!vPw0UiE-aX&qosi-o_E88C}qw@c{ zcemu7Ow$0>$rWNo{`Y?OH;17=wv*geTk>|#l8^l#s%`z)?K9q!J(8lDzp{=PJe6|g zP%<6Onbypm(_;8q$QUEBa2Mab_payJvu4T;o5JPFUJs%jPs*ae0Aw|22 zFdF=t8}4Sq&L{j`7aFL4hQS`1FczzB{|bfp4b6U=BzN+!Y{FL2 zhSL0j<}%~!F0ToIDvq^3)C3-pY|2xfydpLDc2m}~_T@gKtmp@CTmkTA)l4>gSCR~a z?5OvLY=OU3oI=w-^KMoJ4@P`MOaC*^B>j`2Nh|_x<`{kYfflh46Kd3vV>JHYEC6>? zXA4iJ_xT*3@u}R69i6OeOU4V@;)}V^p1qj( z;}?M%0|!eMKO6p;DLb3c)+~9|NghKfnKLT!Fp_3Uq5AE2?|CZ=6(Rb6*U3SsDREdF-dydf>9cc6&loyg7Sic zGU{suh?!p`NTCuG8MbWU7XX#U(GqcXsc6Hy#hssX^&7l8_qKA8X{G!91YuuC(Swn1 z<+xbCh9sab!xBCtw3>ey``JimCxh8Y!pOX#^ST|reEao_rAfTDXYt}ulcsBeY{@-a zz*s<@vs@WA+16*#^p`#?P5UhJ8~4*2bdBbxpZ}QnSKW$AzOj)}?)uUnRMa`WCxYB^IiaC1ObkCyET4;jvDqduiu9o^wdC21S zT0SE00wXbjmd%r$?5`4}_xo4^%+znW^fL*;zav=ctpr`Z8~#4CQ9vYb`_b7cd{JNM zupQg>%^=UJ38-QI01`ynJtTpVUA&x~WMQ~c&H}J$v!T;4UVUESCALgdz=cuclmJ|9 zdG5vfFIJ8!4ptRL*mz&Hf~$&|)3+hie{q@NN06-=>E*ALyS)JtzV{zYd4)v+MJH$t z*uu|%u^gb4_R}x0`0MSxnTelqh&10fhemi~nKZuiM>c!dIc=PGIcrw!e)ZkNVG~xm}q(Qm$ z6xB~Hg5)u8X%QFE%ah~#KO9%oFC0l@+MKybfBooQpCA9Sc6r5jlMZDMHv*dX;pfL` zxr85M*+`h8AnqU9^O9QfzFs?B_eDNqH$UaxW@HESe4w_8!MQ%wf3L6q9vRdm_)KI;U;a7dnzG%?>5flg2%R za2n&bfeo$jpRgWZ=s7S`s0Mz*&LEQt>RE*F9zloyk`EgHL$0;L{f~WtsWX_Vx{#a? zk;3^7Ln%sr;-3SXtpl@D9|dh7zIp-6dzf;2Pr-7MLmp@R`xN;%j1aUcP8)JQw=~Yn zCoyRMq;(EV^tGwmQ90-jI&)HelH8EP7S*pKvu~?K{(K0?PJIA zXlfetfNi)LR+i^fjmF|Ik<0wVC)4rR{F?11H31dpqTWY&rX<6L%BnhjhFRVf^{RzI zd00Wgp=Mtt?l*bGu|dLG`pedZ$F;z?GnOxszA<@S3a@OB`rfUp9N1oQ1(Ofup6q>t zqYJRlfN=Y}J*HeTcU42X9anndgAUC$Tlt?crwpZUp<{JL2CO zQ9;Bx_NZi5oz#l&X;8_C-nCRNzTumV{z>!GG#i_!iv>gXO*L^fdgDoE$}eyqxNS|} zk;(q2q1i*a>l*jy&5iJJ%6{F~Q{m1f`CI$k=sb@VM{r&AG9{rsxbD}}L3!YXWAKKz z9)A8ph7@|(*e_rFdgk28r0A9XTR;%?WOvMu&bYC4AR`|aS)f=hQK2Yt0jjVF76@CN zP32jCIzt-w=YFV|)xu|D?#NiBfO1=t4VkzP{u17#+&A-FkiF`!u$~}rU9~#{jA&{R z>%9aL5Vf}JM_0Ga-9Q2zF!|G{!;iHLzk8X?3#3XH&h6LhD-DP!y$)Gl=N|u%d(>WV z#SEVb*Imh6G^g{cTP1ui*}fyS^sX2@1M?d;+{TkOelF;v&7#GYpQB=1G%2gD01vrr z{Qfu1^7Haal$%g`IWyOIJ^|$y~!3L+C~yW zo4T8eUpxD*Zx~;A&=cE`bFXYqnfc#;pKKQ85E~$St3#Je+pV2>;{L4z0wg?okj6S6 zhs`U`4|^wRDXf-B0~yK$LmLrN(|=Igu3VebS1(gavY@8sD#EPE%CTZE$hM8x>zv(y zmR5%JNYb9{_VPrsu#|;iqOmzSAqN_$<9BUbL@kQhQYbjXPD3SHym7JU{`5KjGg{Ut z;zZ>>>s_`k9eQYBr6-S!kxe3+UH(*dyo-^r&g;%aM7VtbBU$?9&`nQm>BBje>7lry zf=Q|At=CSQc4A1iJO+INj)WK@*uA>|p<^f+V4WN=yIRoi12_Hxb_>W$nXe0X?!Xh^=ST`GA&_aHGDIXM@fZjoV>1Wd2!3S(O3zBt* zW0U{Viv@d2$ZBjwrjHOkyNs0ww6nkBcO!ANI;X#|!X8&H$BI2w7r0pa)+7ROyn^$; zpc=$CjuLH1Mf1Xji zmm#5sB99Ceb{zZV41j)L`tq4b%`yh~EDk=rAJ*%%I|b1^jRB5Gw=@whQw z_eD!584Gh#ClZL>z@w`uBFj;sP98NwzbE7<0>04)DE$Ge1jmb6F50;Uo$9o+cz>KJ zcYB@r#Y)*k*TWAU$)gRm@_Zeavrsc}90@jA8Nu2qnZUiuzvwF?NyXV;W=sGBUv(h& zSln~?P%WuK&$7s5B5byaqwpNdoAKYJZSk`4#Uk}=ocZ9H|F7EAb4DiKA`Tk05XoYmxg z<@gD{G;nJsK8|KnJ>I+cCzf@`F(%I@yYJgN;(0`92Rq_|9m(XjebU;-UM0kCh+IbQ z0yA2l!tdRd7AKkJIopjDT~EgDky{Vq7@o8w?h z2RS#etNO`h;oM}|LglY|gzvi6?lt5kqeNjx(oK(p$iE8%Ijqw$RGC4RFs|)nO%xhL z3>79kf5*!_xNUhylrHHrVB-Kbrz8-kQc~IwFcC)?puK&{(z|pguyhrMP}u8cixcb6_g|`zDJC?BLn;*ADqE6BrMUeXJ}jB7FC z25S5!>lN(W57$jKuS%qVIkl7w+~zMDc3=!$-yM1NEXC!&trj;c?F!8-1%^cyH?(= zP4X$LhYV*k(Onf{xcPe;uRRR(h-avMy%LqgS6@Jj`SG{=mGPpCu@>R``_9z~oR}Dv zkEMcMxSSvV$Rx%5ar>&-t`&d(U4IcSeY~GjxU|98Z%aWiSf{ zVbq8Od+)ke8NiN1(X%M&bC>%U4SE~z&HM1Yn_fsRI!krFC)CM1pS=Bu+drst`WM+I ziB6tro9lCL}GK9xH}=0z0Q1LnDxddNfrYr#ku3<(Ib7dP|UAu%I??6 z=knPSFH!W-BaDMK?e?vPqnMSR?i8YNNtRUAEz3zs6quZ)*$L&)x_Io?zf_53-&+s* zEjTJ9{qBFLq`cGjU#_@I_f+Oo5-b~X7LMs!;cIlWawkeD>O-&G!Jt3ez-M%S zGUh7-m&kE6C(Agg|3Tbx{#_L+U~xHswK0-&JY9ZE4WJ0{Jg#>Aq;igZyih4Qqi#dW z;sQoGwlDqgaA-6C%(o@pjIcYdQAC1&Sr{uP)Q-YN*I>#99Q$xH(eFbK*hrI(Ll^>4 zr=ukuQ7Y>PRTNaFs|xSlH@9R^_Y&!(vd zb^Jjk|F7H@lz_=)5X;dD?I3ka{;%{oVIt6P3g`>~(y@-wr@~iJrWfobMgaxr;ev)+ zCgfcPkdh-MaRZKI0J32}eI`ZBMiJ+_ym=t?W|6~n1>|OKFtuxd(Dh3_ze&8)(`PQe zij0n^Co?s`4RBa-Zn==3CZuujHf_=@38T!DFE1jXYx;$nP1*)FN30kOkBs_ilqA$p zYnupbOzuLEcIU1jH@Knqxazj%yeul4uxMFP308D*qT|NFdEdSzFraO*%M0pzVm>Vu z&{OyL)KwDxx1Tg|egf}gr*<`wLCVoaPxd8H>KzC2c@9T;YB!(w0lhfTe|(<$PaU%V z5AF=V6(rU+m}0ziZ>F>8lQN2=AiCuMR@Q!+JO0@e^7&Fzw?BNDBU4=l0z)lplnaB}b3A*AO>~;&jxPu+O}oUyql|;wicPfP8!O zu<25#)kx$)#?wK%7=2;WqUTszvd9^GHo#`D(dPS|ygUyE>8T*>Gfu8fY{efhA_mOF0sLOfePMJ+zdUKlQ zQE_OqS5uyx;KG*7x`iS!h4yFJZW_!{`C@JL21gmonpi&l*+cVR?kb8gYy`uUF?oSz z?Rm=0V`6}EE;mrR#32sOpiA8RdfRHGFj|mYGPTXvon#RYwmh$Atg^@bp83*tHwW1f2(MI z>L5vUFg9eB+`>|RJ9)<&wHj}R9+E*wBbsNMug4}sZ>@ycXn>-FNSkr|CS7V(@C7=U%?4L?|DOzf0&96Rd2a`pTG(;B7OA=H;Q3){?>1SAxX?UjXo zdK+W}AQJNQ_mnojL-h9H$~PQ-^P+clkLOf))wz){Q-_YEA^Ma%4=s@o)H2NDmbxmf zst5-!QeFLrY?`uW!}#O8Q}~?DYww}_>uy&UEga+O9OiJpuhf=Cidn-QY+#3ov@vEx z|4}TXjV-Y`6;U#7!_4|OXR#}*mv6OjQ-4;wn3@w(m$>FLm`Qnrx4g=4o3A+2iA?#!%rw-^wosqe*m^n}Kv(W>VOV)|R*!EC5X z%|K58*C>3?p7Gu#Nqr=7M}vB+V2$73Al>tgWB)?i@ENyi#6F5*Vcu594@Ko49f}I9 zQ=Ehjg#9VoYE#tV!1>$OI7H&B$=)IZQDJ6MYM>55F^RAj^B_*Z&!|;l1ysl*Lb)N| z_N>0Su)$8Ni17FtB5f!j#HP*LI;*5KguQM|O@y22Zf8BwWaW%s-jyx$P`XdcLh6pn z1kAc#&F;_9&gbLuC4FSZzyGDE0U=JaM~9tDM>9S{|qvVkLOZR5ulV#8T!!E`&%H4LS1m^=$igNL!}kpo*(R=*zFG z7BX7|DNB}*W~{l6TM>lWKts{wG|KS!)DNCSEM6zVd^<~j_0pEjzNlfd2$aOK_v&t zcBS)$Dq{ePk4K#*J5jgt`C`nn1fmh=l5_*P%ubqZkTetLQyJUVYtf+tiV{NAI9A~t z40RShrttJEUsm3PJah5!oc$P5H6t!XvKhC2`>MeheYdV(x7`~O27-a8L4d z>`CA9NC`uIOLy@2hW0T?@ESVH<5E0pj}8ww=p6F+M4Vgl06mC9Sz=y0j=EV;U(aX* zVWii&h0GL2#1?r#c*5pKGL9Y3b6OLf<85WaLhtl33(2+5-EZ#VciAfNi}9BI2pR8= ztee=hRLqILY|D~ho!V_qKmD+2+$Oi4TW*krA{&o{zYR|q=fjUx#d=&W9r#tOS@J<` z$UK)qjvE3GI#*zs+s|P*I<@gmOV=ITtOO6ZH!+>PIYucgiiD}Vj~qgCSm;d$YbhYn zjoJXZQHnMV!6y!^twUvy9~1gtGb&Gh>YhT~c&yhT2vP?s_lyd1#C%G}&|Q@YK%lb^q-nqRNUhQUAI7{M!*_)Omajko zDb$Hs6gF|vyoZ{A1tijbCfyYk)S0eRI=1H5D|e`kyZ+gHU_1RyrA~OJ$!jc7Kmqm( z92txWUZBGIMC~ro426?-X}dONjo70J2?QFGxV#IeDN2*-Fl>`mJXeA}y?eS=4tRR( zaE<;nQxNDM;2*=pMH~tOebs^zrX9os&37XLrXY}D(oy^8AeQ>7@<9E|K4*4QzZc9% zpg;Z+AyT!|suLjYmD$xV141BB0HyNPMPNCz|78yK7lHm4pw5bC=T{W{Ug zr_zaGyAAv0-w95_aUs^{=Rd$n)fllUu00@-8-+B#w2-uD^kdikvrffOn?Hl^*PpTi NL1mPs%Oy>M{s$&Cd<;j*_mt6Ok-KUCUnjH@S>28o@n<`Jl zFs?=k;+)~#dHE`0+nTQeLoV%CUODRqH1iS8R8JX_;$)_71V3|oOdLA^Qve1 zY3x@K;@Pvba#ItR&s2mQ4*xM(zw6!;6q4f1;p4`IV?KGSDwdEWFw11J2Jx@$ySa;! ze{*K*ZRr^OA@gQ@Pa4++9BD5o_sq*t8liHBX zbh1tuvXXG?Usn6!m{&Di0Je&g_B}&8 zJynE2rRiFR4&))cu*2&4#0Mj*Km3uer>ohmPnD#T?-Jogc|5Kvr%ok9Y5Cbrotb<1icO2-rmsrn5aE<_lLdZQ4#nvY`*b0hl*!XCatN) zx|P0iM|R|8+Q^s-JLU+6b!)0fOSIQbXsU5~DO^y<>2#ujhxbj?&ub)ygF5^IT>3t3 zP;g@q1!eTpD|A#{!!7n@LeSf4gk%EO{$UM^=4oxO&!070Qv=^Bc zU2{KEyyv}0G6P6}nb!(mhbr6`!#vGQ9+^>q*J&?|yX8a?oS{2AP60?w8d&Td=#tK0 zksLVZhYkBtQlrY3ECr|c$6x&lNNTC{_(B&Ips(F?`n}|7ldD-LK)QZ$gm zGIrzl?8$F?C(BIujr*CM73Z^rP#?Ie!(rL(wn)7M;qGMpbPv+okO>dZd_5{#@X*M( zXP36}$y%Bq3=i)HD~VYE4{o*!!+r8hk+>AE0eEJ~OS9vR! z8l1*D-~SS$|~2RCzIuQM(kS>Ex*O{e*yuLeG9i4>VJkazu34FuIjTJRd?v84n)t1ooJAfj(K*w3MSw3 zlyGpfFZsf}#F$DNC*NMwrvqctt!A z!{p8f>8oVZTs|A=3qD7#-|!dGm1LG(ox2CK~x5?}9!;%=|UcX%^GVC;t`N^&3f|VCy5fGC1>XgVGx0Vlktbo(~WiqvH zax7RlAmRna}C(dyjt~E?MwO8WF1glnRx+xv;CVGUL}`_a7&+%@hVSLM##5v0p37>Hmfc)65^{ zs+=#dJ|>n8%?kd5Du7Jb71fG-BiXJvIjAG_U)k2;NF;YK@p=5cCvCxqmFTxW{KAL; z0?>J0#_3u8zGf=dJw8(BzmRSAD&N}~`jn|+fLG?@L@sEog@+NyC1U5 zJ!54KQbwoK1__7(a}mQs)TqRD)a~M*2+2SzcjIdE&P$^j|6=S}_| zDqw%>g~@1f;-dxuQ;8(mH_%HYK>yc9>-`_7O}@kFLT~(sh9O!5r$aX6-Rfk!?%91h zr>d1@^yR9jqV+5Al)_=xP*un3ilkniJm$Ne+;?~}ouykCtT zv`WLYE9J^J1-&s*C!CcJY)Mo{q+SNhLU&PCKYqkIkAu73g&fAcU-cJV=*`XJ)L!ZL z++LHytUZ$2lLUxko7ElkE_HBBLW`efgsCY-%4YUHmjrH@w1pfPi7{UH|s`JI| z*6r^fFZ%Vd7R0LXXumVMZHr-QybiK8eO5JWQ%f*ZIUMH+QyhvbVAhc-2!I_U51>0Y zA4k?(s<5)YD!SOxif2V`#I^cz zHbVsLw(+W6^2tQdDzeM5z`+5VHZ7VjI?w|n;J|2(j!_nZufXz5GsLsmKN%_v721nz zCW_tbI3lHH>^#)uZC~2xBTy`VUtl8(St}}0c{fhqDWyKh;vZ6_QfC!Yr|v&M-(em@ zZQ8BrtiEhCq4)g_p<5in;=$Z9PhMm*{ud1j-G)`2;r~To2Z% zu2d9IoEvK{j<x^blEk?nN7xZ!@AAUT*C271JA-?05;9>Ql78r{^TOzc&Ar6^`N=Wh94{BWg& zyV_mMz(M29UP{MU=X!}rw0CNG05?-SB=pt$tAxX&EtpdXN_ARR=!G7XQ%J;T?nEXd z8MqfvrQW}iB3>x}#@a8r$v7YSiZ-$1Lib2`_kD*A1wjxf_`rV&%>@vQiij`TLAm%c zq4~^nw-lhyOGyz$npis}^Xz?84lD0f(6^w=Oy0QRBzvoxU#;}B`;bWMSAw+Y)*~-| zHFUMpn&-pHdG6M{(;GK|YoS=SCB4_+i-G#vy5qWKq1!=BcOo(SO;%ejKr2*-vlQ->XzwBa?=KeU) z-O|Ga%tou`E=YY*d=zl2`^t0glS8Wge)o722rDcF$>HuP&IO||N8JD#@X7D2{~Ek_ zfK9D$c13Oa==Ee|fa}VV_9Tr1IrKHuW3Kp-X#gl|>`a4Ii!6`;#exO*d-hIs;<;T@)cacHK4F6k<&Yrk1A zK&yTqEdJ-gZY`F-8f8@C7!l?o!KgXewS2z-Klq_bUEEB{fW= zUoCBP$Q(0HzUC8z$_mG1mqIs+P0$}{_2;!n`{U}mn6@gA%JrWZGZegb55okVj)~Vm z96;S=zuVbk7O|^&~ZbvRAv|B(8-nc3vuEVdC#kJKh&guK$7T$3_MYi@aMb=D6l{r}pBLqJ-O_}OW zcj*hJO4sS{Kz?+jUbW=tW9(xU7iB!}i}{8u51v)+*9}am{KKuQYhF_HDDFEckzhhR zH=8QMkFkjJ&JlG}wplc3T`SYDLt`k!O=GCh4XpRAu4Ac|x)1TlMg29ksN>&eJctUUp0;iBsj+2lbZ@0|u;Xwg zDvqgi+fG=Znj+f9@FT^qs{}5tJuV`^(TU(Z>A2myn&MtL=1EDavHNTefBEPzSa^xSkiJasOfez%Dzr3PaF&=5tb4VU82q zt()&Kbz?K8?n(WHt1yJ^PEt`!!}%d?7!v|EGK?wx3)9!a4UZfD@VhUArY(!MvGRp7 zzFDq=63B;1+k_9%e_D5xg)CTe0vo%jGBs*078MG*JfN$d6+N#^NIiHM$5C&j0->=7nr?gH_&&hb(HD8}_`|6#ZtzXaXH_gaS zhIyRc*&_`e6ruZmugD>>0h-i_{Fe}@BQ=4(z%;1Rn&_t-v1h8Xf$ZH zKYScVPZAY#nIw+yWocXajGl9KzR$D-HibkFR>EoJvAp$X6%Dr`66QI>Pu1`5!kg%uQCdyjX4OK2a<- ztV2)PR#)5Ar~b}qVgDWjaGcuF*uA8rU61yYFe(x?P*O^Z8W`35>yR?6!*(=nNM9IR zuN6?C%kdOWcy3Q61eAI) z!uLO)iG5Ln>-+q4X~>_L)>}P+%w4plUw_%1I8D;^U%$D~#P+ag`;C%ft<0GVts8H` zT>bHv)LZn vQ+*_?32uG{Q#Ypj<|WsI{!yePwEKXl39#5S<;#C`_Jd0Wa$liD&NTRc`^lZ* literal 0 HcmV?d00001 diff --git a/lp-app/lpa-studio-web/story-images/studio__node__button-face__held__md.png b/lp-app/lpa-studio-web/story-images/studio__node__button-face__held__md.png new file mode 100644 index 0000000000000000000000000000000000000000..3cc048250c2ea94919e69dd2028cf3bcb0d38fed GIT binary patch literal 5327 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 DHO+qI literal 0 HcmV?d00001 diff --git a/lp-app/lpa-studio-web/story-images/studio__node__output-face__default__lg.png b/lp-app/lpa-studio-web/story-images/studio__node__output-face__default__lg.png new file mode 100644 index 0000000000000000000000000000000000000000..9d95c55ff49443c8995e9ce8b8dfe12e280e6693 GIT binary patch literal 19563 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 Xq$S8K*Ojvn>HvzeYBCj4=7IkQ7>cFf literal 0 HcmV?d00001 diff --git a/lp-app/lpa-studio-web/story-images/studio__node__output-face__default__sm.png b/lp-app/lpa-studio-web/story-images/studio__node__output-face__default__sm.png new file mode 100644 index 0000000000000000000000000000000000000000..bf483ed14f0e17f41b003487cdaf578bcfbe67aa GIT binary patch literal 13480 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 literal 0 HcmV?d00001 diff --git a/lp-app/lpa-studio-web/story-images/studio__node__output-face__pattern-on__lg.png b/lp-app/lpa-studio-web/story-images/studio__node__output-face__pattern-on__lg.png new file mode 100644 index 0000000000000000000000000000000000000000..0ede00e6b607e77bf6d6cba3f8d389c3806673ca GIT binary patch literal 13755 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 Date: Sat, 1 Aug 2026 09:26:52 -0700 Subject: [PATCH 6/7] =?UTF-8?q?feat(lpa-studio):=20skeuomorphic=20Press=20?= =?UTF-8?q?button=20(SUPERSEDED=20=E2=80=94=20see=20handoff)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate feedback restyle: the button face's control becomes a knob-family momentary push-button — housing ring, radial-gradient cap that depresses under the pointer, live-blue ring only once the hold is real — labeled just "Press" rather than "Simulate press"; section label "controls". Superseded by the ephemeral/debug-slots design (2026-08-01): the button half of node-actions is punted to the future input initiative. Committed so the exploration is not lost. DO NOT MERGE this branch. Plan: lp2025/2026-07-31-1736-ephemeral-slots Co-Authored-By: Claude Opus 5 --- .../src/app/project/node/button_event_op.rs | 4 +- lp-app/lpa-studio-web/assets/tailwind.css | 6 + .../src/app/node/button_face_stories.rs | 4 +- .../src/app/node/face/button_face.rs | 114 ++++++++++++++---- 4 files changed, 103 insertions(+), 25 deletions(-) diff --git a/lp-app/lpa-studio-core/src/app/project/node/button_event_op.rs b/lp-app/lpa-studio-core/src/app/project/node/button_event_op.rs index 3e4ebaa0d..c303328d4 100644 --- a/lp-app/lpa-studio-core/src/app/project/node/button_event_op.rs +++ b/lp-app/lpa-studio-core/src/app/project/node/button_event_op.rs @@ -39,7 +39,7 @@ impl ButtonEventOp { /// rejection notice ("Couldn't … the button: …"). fn verb(&self) -> &'static str { match self.event { - WireButtonEvent::Click => "Simulate press", + WireButtonEvent::Click => "Press", WireButtonEvent::Press { .. } => "Hold button", WireButtonEvent::Release { .. } => "Release button", } @@ -101,7 +101,7 @@ mod tests { deadline: PROJECT_EDITOR_ACTION_DEADLINE, } ); - assert_eq!(click.default_action_meta().label, "Simulate press"); + assert_eq!(click.default_action_meta().label, "Press"); assert_eq!( click.default_action_meta().priority, ActionPriority::Primary diff --git a/lp-app/lpa-studio-web/assets/tailwind.css b/lp-app/lpa-studio-web/assets/tailwind.css index 5c816465d..cc7989282 100644 --- a/lp-app/lpa-studio-web/assets/tailwind.css +++ b/lp-app/lpa-studio-web/assets/tailwind.css @@ -1535,6 +1535,9 @@ .tw\:text-\[0\.7rem\] { font-size: 0.7rem; } + .tw\:text-\[0\.8rem\] { + font-size: 0.8rem; + } .tw\:text-\[0\.64rem\] { font-size: 0.64rem; } @@ -1544,6 +1547,9 @@ .tw\:text-\[0\.68rem\] { font-size: 0.68rem; } + .tw\:text-\[0\.75rem\] { + font-size: 0.75rem; + } .tw\:text-\[0\.78rem\] { font-size: 0.78rem; } diff --git a/lp-app/lpa-studio-web/src/app/node/button_face_stories.rs b/lp-app/lpa-studio-web/src/app/node/button_face_stories.rs index abcb94aec..b70b9be2a 100644 --- a/lp-app/lpa-studio-web/src/app/node/button_face_stories.rs +++ b/lp-app/lpa-studio-web/src/app/node/button_face_stories.rs @@ -21,7 +21,7 @@ fn ButtonCardCanvas(children: Element) -> Element { } #[story( - description = "Button card at rest: the simulate-press control beside the endpoint and message id, over the collapsed settings drawer. A tap sends a click; holding past ~300 ms becomes a real hold that renews until release." + description = "Button card at rest: the press control — a skeuomorphic momentary button in the knob family — beside the endpoint and message id, over the collapsed settings drawer. A tap sends a click; holding past ~300 ms becomes a real hold that renews until release." )] fn default() -> Element { rsx! { @@ -35,7 +35,7 @@ fn default() -> Element { } #[story( - description = "Held: the pointer stayed down past the window, so the control is sustaining a real press on the runtime (renewed every second, auto-released by the device if this tab stops asking). Live-blue, the same family as the playlist's ACTIVE placard — something is happening in the runtime right now." + description = "Held: the pointer stayed down past the window, so the button is sustaining a real press on the runtime (renewed every second, auto-released by the device if this tab stops asking). The cap sits depressed with a live-blue ring — the same family as the playlist's ACTIVE placard, something happening in the runtime right now." )] fn held() -> Element { rsx! { diff --git a/lp-app/lpa-studio-web/src/app/node/face/button_face.rs b/lp-app/lpa-studio-web/src/app/node/face/button_face.rs index 8408c15ca..5da752060 100644 --- a/lp-app/lpa-studio-web/src/app/node/face/button_face.rs +++ b/lp-app/lpa-studio-web/src/app/node/face/button_face.rs @@ -1,4 +1,4 @@ -//! The button card's permanent face: the simulate-press control. +//! The button card's permanent face: the press control (a skeuomorphic momentary button, knob-family). //! //! A button node's whole job is a transition someone's finger makes. Studio //! needs to make that transition without the finger — to rehearse a show, or @@ -84,13 +84,14 @@ pub fn ButtonFace( let cancel_face = face; rsx! { - NodeCardSection { label: "simulate", first: true, + NodeCardSection { label: "controls", first: true, div { class: "tw:flex tw:min-w-0 tw:items-center tw:gap-3 tw:px-4 tw:py-3", button { - class: press_button_class(held(), wired), + class: press_button_class(wired), r#type: "button", + aria_pressed: "{held()}", disabled: !wired, - title: "Press and hold to send a real hold; tap for a click. Synthetic events reach the runtime exactly like the physical button.", + title: "Press and hold for a real hold; tap for a click. Reaches the runtime exactly like the physical button.", onpointerdown: move |event| { let Some(handler) = on_action else { return; @@ -147,7 +148,65 @@ pub fn ButtonFace( handler.call(cancel_face.release_action(press_id)); } }, - if held() { "Holding…" } else { "Simulate press" } + // The physical anatomy, knob-family: a housing ring, a + // radial-gradient cap that visibly travels down while the + // pointer is on it, and the live-blue ring only once the + // hold is real on the runtime. Depression is mechanical + // truth (your finger is on it); blue is runtime truth. + svg { + class: "tw:block", + width: "48", + height: "48", + view_box: "0 0 48 48", + defs { + radialGradient { + id: "lp-press-cap-gradient", + cx: "35%", + cy: "30%", + r: "80%", + stop { + offset: "0%", + stop_color: "var(--studio-color-surface-raised-strong)", + } + stop { + offset: "100%", + stop_color: "var(--studio-color-surface-raised)", + } + } + } + // Housing: the panel-mount ring the cap sits in. + circle { + cx: "24", + cy: "24", + r: "16", + fill: "none", + stroke: press_ring_stroke(held()), + stroke_width: if held() { "2" } else { "1.5" }, + } + // Cap: down and slightly smaller while the pointer is + // on it — a momentary button, mid-travel. + circle { + cx: "24", + cy: if gesture().is_some() || held() { "24.8" } else { "24" }, + r: if gesture().is_some() || held() { "11.75" } else { "12.5" }, + fill: "url(#lp-press-cap-gradient)", + stroke: press_ring_stroke(held()), + } + // Rest-state highlight dies while pressed: the cap + // face tips out of the light. + if gesture().is_none() && !held() { + circle { + cx: "20.5", + cy: "20", + r: "6", + fill: "var(--studio-color-surface-raised-strong)", + opacity: "0.35", + } + } + } + } + span { class: if held() { "tw:text-[0.75rem] tw:font-medium tw:text-status-live-foreground" } else { "tw:text-[0.75rem] tw:font-medium tw:text-strong-foreground" }, + "Press" } if let Some(identity) = identity { span { class: "tw:min-w-0 tw:truncate tw:font-mono tw:text-[0.7rem] tw:text-dim-foreground", @@ -171,16 +230,22 @@ fn button_identity(face: &UiButtonFaceData) -> Option { } } -/// Control chrome. Held takes the LIVE family (blue) — the same family the -/// playlist strip's ACTIVE placard wears, because both say the same thing: -/// something is happening in the runtime right now. Deliberately not green -/// (green is good/valid, never state) and not violet (violet is bound/bus). -fn press_button_class(held: bool, wired: bool) -> String { - let surface = if held { - "tw:border-status-live-border tw:bg-status-live-bg tw:text-status-live-foreground" +/// Ring/cap stroke. A held button takes the LIVE family (blue) — the same +/// family the playlist strip's ACTIVE placard wears, because both say the +/// same thing: something is happening in the runtime right now. Deliberately +/// not green (green is good/valid, never state) and not violet (violet is +/// bound/bus). At rest it sits in the neutral knob-body family. +fn press_ring_stroke(held: bool) -> &'static str { + if held { + "var(--studio-status-live-border)" } else { - "tw:border-border-strong tw:bg-card-subtle tw:text-strong-foreground tw:hover:bg-card-muted" - }; + "var(--studio-color-border-strong)" + } +} + +/// The button element is bare chrome — the SVG is the whole visual — so the +/// class only carries interaction affordances. +fn press_button_class(wired: bool) -> String { let cursor = if wired { " tw:cursor-pointer" } else { @@ -188,9 +253,9 @@ fn press_button_class(held: bool, wired: bool) -> String { }; format!( "tw:inline-flex tw:flex-none tw:touch-none tw:select-none tw:appearance-none \ - tw:items-center tw:rounded-sm tw:border tw:px-3 tw:py-1.5 tw:text-[0.8rem] \ - tw:font-medium tw:transition-colors tw:motion-reduce:transition-none \ - {surface}{cursor}" + tw:items-center tw:rounded-full tw:border-none tw:bg-transparent tw:p-0 \ + tw:outline-none tw:focus-visible:outline tw:focus-visible:outline-1 \ + tw:focus-visible:outline-border-strong{cursor}" ) } @@ -198,7 +263,10 @@ fn press_button_class(held: bool, wired: bool) -> String { mod tests { use lpa_studio_core::{ProjectNodeAddress, UiButtonFace as UiButtonFaceData}; - use super::{HOLD_TTL_MS, PRESS_WINDOW_MS, RENEWAL_MS, button_identity, press_button_class}; + use super::{ + HOLD_TTL_MS, PRESS_WINDOW_MS, RENEWAL_MS, button_identity, press_button_class, + press_ring_stroke, + }; fn face(endpoint: Option<&str>, id: Option) -> UiButtonFaceData { UiButtonFaceData { @@ -241,7 +309,7 @@ mod tests { #[test] fn held_never_borrows_the_bound_or_good_families() { - let held = press_button_class(true, true); + let held = press_ring_stroke(true); assert!( held.contains("status-live"), "held reads as a live runtime state" @@ -254,7 +322,11 @@ mod tests { !held.contains("status-good"), "green means good/valid, never state" ); - assert!(press_button_class(false, true).contains("cursor-pointer")); - assert!(!press_button_class(false, false).contains("cursor-pointer")); + assert!( + press_ring_stroke(false).contains("border-strong"), + "at rest the button sits in the neutral knob-body family" + ); + assert!(press_button_class(true).contains("cursor-pointer")); + assert!(!press_button_class(false).contains("cursor-pointer")); } } From c32cc7f9292bd8fa2a0ae07d4f94db99e40da659 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 1 Aug 2026 16:37:20 +0000 Subject: [PATCH 7/7] chore(studio): auto-refresh story baselines [validate-stories] Captured in the pinned CI environment by run: https://github.com/light-player/lightplayer/actions/runs/30708165875 --- ...studio__node__button-face__default__lg.png | Bin 11945 -> 12697 bytes ...studio__node__button-face__default__md.png | Bin 11202 -> 11877 bytes ...studio__node__button-face__default__sm.png | Bin 9669 -> 10227 bytes .../studio__node__button-face__held__lg.png | Bin 5897 -> 7129 bytes .../studio__node__button-face__held__md.png | Bin 5327 -> 6739 bytes .../studio__node__button-face__held__sm.png | Bin 4881 -> 6107 bytes 6 files changed, 0 insertions(+), 0 deletions(-) diff --git a/lp-app/lpa-studio-web/story-images/studio__node__button-face__default__lg.png b/lp-app/lpa-studio-web/story-images/studio__node__button-face__default__lg.png index e41a2e648ef98fb6664631721e26da22a4ed619d..106de2dff59134e7c1ac8ccf7ea8920e1ca62098 100644 GIT binary patch delta 8083 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 z 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 delta 7326 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 delta 7074 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 delta 6463 zcmX|`WmME%*Tx6LK*<3?q+y0`knWHfx?4b`8wmxGK|(ukFY@?(79eT9tZ>tFNGH2AsQV>UW2+0 zZ@3VOuU29oQy>sGUTF%1WWPS_IUktKO4RsR zFG?Npf)Ra!oEsV1{^c^Sb>c!9(dfocx$>)RX=>=p;^~>tMGBld)D)=OMq&1ov&cR~ zay@4YhkCwg%8skgPB?cx?+m-gZFR)I&dLoK`M@1w1G9X}{+^%zMeLcB$CD=4HkRLZ zTy4+YjQ#x@FM~7%MBH}V*0d|rlxW$8;);ct8X-P#rLoM$0bh83ygWCY+(;gL4COTg zH$q<7lHRd)UpNLB&((z7v190nvQ|17s$JCif99Q<|I8J(;Q#aqzCjNnO=CJf2P#MW zQwb+8{;mK6tTuE{jD8Tchx!`%&u!r=Zr$yc86R{zj_6boUN%KDs=F>L`dl)&%^gNO z`ll}pFx=UwgBm{WkUWnZ$f1j>;F~H zlw}c5&|SdhzbD}Qv(qM&{t@cGG8h=XYz(qB{=&#g8i6f67XksvgF!@a5JVAzMF)WV zVc2vc47eZ^&Of~8f(m|m@}*czih?k4=HelH?MMx;x6Q&wpd7kD4GJKJ2kP&;gd*t~ zaQ)X&mIUeVM$5Hl5Pjrtr-naT3`ORYuh(jdsJrEL+``!6UR9TrRvNQR2+nJjPWQl+ zzJK4<=_~<%gQ*PmkX10BHca0xeu_Hg_#*2Yk=?5N#9^;fZ%KW};fybkx;N`^>l+GJ zgnJwW?rQTIeVewa5!~i7-yR*XYq{DWe2+b`H)Rc%pfxhg5?vAV_ePio;bY#L?0Y(4*DUO%xW6CQ**%BBKeB3%6YNVaOD|@NXWuO zlE6~vF;KHT`Aglh0yQ3SMa3xx6g!wND@46P$aquoUrg~qx5^Tf+5;e!Wu7cbgVuo^Me`#VS+i-)#G?UAW1%Va=Xe{wOc}!FlcFsf9Ort9-`^{ z89Qn`)Nnp+#)`?Wa)Io%Y}1C^tX@83T*LJy-DR-rt z2TUdj5~#rsbzeDCr>nSFtH_-7|hRZ>=_ z5`H&Ga?Ln9$^9q?=_!}CYC|mJrPMDSawraoR!&5JucyYG!U7^=aGR-5{mk2%g@9!@ zQL463BI*^RNQk%0OYOC=lz#Gir;#u~KO=#(4U4bAE#;vt(U4j(LFAE^$jRcjS_y#X z2}<}OT{kULrVLxvbtB6wYa!nUuD2nP^W8Cz6Ng&uZu1lZuX8AnUlAbu+%7S8(n^5H z{r54=YlY|ZaP0ox+~$41%x=)1Q(qx-xd~Q(pP8N;JpmpG>!~b#k(f0@oUM~pLpZj- z*C#_3%a~Q>a#=8zQj^X(O?0k*!nLs>Iz_cu3o8JpCy(zq0!5lj9&e{-F5=ZW5$LaQ z1!?Eurp+iDL#VhaBebt!)gMez)Ao*o^(y904Hr1Ag8jPO@IfT#yXOWDjgx(6H? zG;w%;7%%o9O#5Q1W^U-^hYAzWhugj*jLMuYAKnjxn9uhUY!nQG&vH&Wi=HPDZ_lyD zK!5}T=rsZVBq>~1F85tgSUfoq*YIBGng#IZr}GbhC~!fMh|5!W4LmOYq7%POxlUMu zPM0>9;?>h92rV{SIZ6h&=+&Po_AhLPqsD%3JRkGeRHm1m_c&+}&_k7b2W+`ZKNt-b zqvJ7xQhU46lwRVmE=+r=QF^QPURh%|#1cLmY;f~O%%8n_rRoev1766E?&<=Q7oXQ` zK*SpNZ2NE)p2-7kx9?k66*6g&-f zkc|@Gdb|z}13H2Z9~E?R^^Aaa_|*({)yz~q{L3u;#vjGcYl+iSHT`RyPP(T~M=4($ zj2Urx)mjQOdf$-E!SeBO_%oVM8zIQld5OlLaa#@LIx$G=(Qq+KYmz2!YEPP};7cGD zA>RM#gY!#Vf~gE&uJBJcJ1pAq34V^dg-vr`yJeAFN~a$WRz>E$bb>(9|Yc^KyaFBE=bMn1alOai%{ZkU@>ssq!qQHqILg$>f!+19?ot!vG zN(=ACc22Q@)DW-D;4=B80sm=%8Kz4X3!U! zeT#oLEz)7huhiP?VnO@pQn)w57-+gb&KEuMgo9?jsvt4IzP0TBmhtd|kq1xYgr&1N zP*m^zF$GsXeA4bEmy07pYFa^k8Ei~vxp*FkcV>$>-R;t;nh9r;vZPwmk0O5kZF^!i zqhO{>_WHVIVVF1~g5yJ>=zB2lhXaMUhy#xVohD6xCfZeeMJ6W5zPqBfLJ%pnm!BBjD>Nx90ZyH~4vD z`kqu>Pa1|Y>kq*oW=B4ElHNZ$Pk*{e3javQtFU<=ai5|rprNQ;{&&--y`@z}f|%7e z;!oF;W2%?#;c+UwUo*p5?LjmipWb4@v~H=S3!-rJ9hDY2hL|#OxX3*w&#Xj1`dbKD z@VFR7OYr#fP!b)E#GZ5|D64iD#Cy}T)I@1N7&o1kIcu(K#-yX>+NzVpyY`{%jGJ7` zYchju%*f>ja95AP(O+!)I3^g|I^QozO-ol#q5A}V=-9d#JjOA`{c0Al>bPy&7tXHgQ9g(SKcCgmvMHu~uFU2p z&rPQIs8zsm=}W0p#ps-i``ZsO5hgg$RuQhiM? z=C34aC4HFHnpun|su4n_*N20?3MiRO7b~jN!>V5t^_ZU;a1ra%Zi40h%>A+AOL_oD zMBsLz^*5bcVk;8dy|P zGvV1rSxWxwy&=LHap}bq2|pSrXbyiEhD6*rMhN;KIPL7V5MO>FkH+Sj%|5!|z3?%E zc#?$&3H_*E|3bCyIg?tM_2YbH>HPa)|01IP@0&js`y+F2J@(LgP zaUl>fMqrp7fzn~!-5#6A$XIbCS!#16;W#Lcx-DubjbY%THf0r-F*#5}Be0%Cb!$t$V#vFXz3ryZ3_t z#`tTwNyn6rn`Kf0M0m!>ff1%|OUlajL(YlXeuHd?*w2lDjLSZs>a@sr6qo;g`0-MF zmAH@wp4ynL!9th%lQ)E2ShJZ(SJ&;wLvr;LZ?~HcL~9bQ8zzp1CLX87kE<-+^07i2 zpEd=SL1vs~@rw1x7*+-?1KN-y_Ly)?o7V>ps;iR?706TJ&Szg+_q)xjIyzsp>~>hP zlEs=vhyVm|KP%C4&Gj~XX(2}VW1N*OMW}miy;f;Sl!l2(4c2C7#oX4z`YU)v{$NKI z*G#4jmNY^h{B9m(zyhoutXg={NAHvfuVmHZ_>atiIQHfld+6(PR#GO#$w7Z;;Nt+u zz%Ig=j#OQZS+s)!2jp)A2KChs!`^md#UsLG!Tx&+09Bu#F`Ej0jf7ie_7sqGF~j3N z5Av`w8QnBN8~P<-E@H2YjM?+g!jNC9s4Pst{?7Am^(U|~G6rXFi&~w65ZY{5ao$s= z+LZ?xl#LK8^t?%5|A4P{K}+fYdKg6a;UE=Azw-&WH@o9 z$@gn|rhGeRA%MR1rXZ;E&U*J=KzGsdE^?CnFKn8--NCDuqnkR|AT^}vojwH$y`w-r z_fa@^S@h2D-$4xjr{*r48c_W|+~|8DN}+Eu%6jTq$7J0pXBtLBe#ryOFaEZ2IY6w6 z`Fts_-0nM#6Lp^kTSD&=2xaiV0Fv7#>*yw(D)8*_$?2tjR_7&4@lJ42JhI=joT5gO zxVfr;d;Im(QnX^aK*8l3?908I)wh+pHRN7Y0?m{&$P@c|BbUO08Q#d_ZZ0S7M&F;@ zRbmq5lw#{~NtY0b^>^tFmFiN{PKc=_Su3v9zb%1l2bLDIS+pnKt`D?lje3QV)Y&l6 z=6tn_)Y}cXZ`fL;iVmM&Y_Y1ud^a~+neH6!B^MAhv>vzTdvk(InGZ{!T=#mDd#Q3L z7Wr~ikDj_Q?XQ6TqVJ5>{@z9iX;bautalngua8&w+H(f~&VJBi7W+3F3J9n2#qM@Y z)OL5glV|@J1^g{0Y@^7&GNmqAXD>gv?tN{xqUiLR!jiQL1&#jamu(FA1F(F3t!ftq zoAO8@IS6%wJd(1aF$0JlFAgjnsC`*syy&LzF=T& z?zZY(lY|}QHRz0TzVe;k4#Ft6%$OWUL93uWzoP=jxcaHF>>yjMl9PiI{&OA$Y@85B zb2+BE))&@tu;-@dpZDLFBeIRx@}@?2;v{<_gBGZ*ZD8y18{QAfNXD6=sbmb2*$z2I zTnfx~s`xb~#QHseb?uqriB?S3M5M;4bol1_2_LcQR$%%~HYAfX1tBiF5b0Vd1Meo` z{#e*SQ(rM*b@o|5kSgqXby(iym@Nw~cm8Phd~%`^XVq$^k-8I8L+n#`gMO>%n?w{s z8=QpebO_M(ivJLlBk{Y@!Ts)`HpZ?!9=zD}3hpbIPGsrJv4>-W66Dcrd{fztkviPGO28bGcd(EO~t{*dInDX;2*V9AcXPlT7 z3$SFIyZCy?-v+teT|0655D^)beJ3@s-`*yv{^*!&<1N+IpHQQ1l-dQ4-<0H)<1L1c zq%12;$-=ogu9rFS%1c+`4ZQ;Ukd2{wg9h69J*A=XO$I|z9pY8=e)bV+(tyyrH|M;~ z{jDysSy?7~s9yrTjj2j{Uo#iP`D^kZJGyHxlGWaU!itVO9$Y=X^vzA`%=YmMU2xLx zvbPpiWb_5@IV_fPUY6m2(b*)Gz4(DW_aM^Pm<`@GQxa{xSqmeeq{$t;tPX0>gtw)o z>s{hOT}OZBBes~y%m$Dem>4PJwa`GW2dZ^IyFcFmYRifB`fZ$6f9!@f{o40MVT4J_ zyJs$4(~W({{2E`kaa3}%*P4H+sQqeIQRW=wC37b0N?g|*r4I!#FB+oIu}?-i@=PSf z*(vFPN;J&~+DXcbB8D`zLY)&(r~Vqu5B;`f`|T|kLg}cKoJ6QP%$^2HVNthkut3Q` z*5SccOSYQgaomoiDrWtnS2QO;>pAukTDeywV%hp+VtbNtDhDhS_ko@CdPPMWN|Ee@ ztAvq9vfeu`u_3Vk1}W^233m@pIiu$bKFfT5h$^_DmT6Q{zh9;se(22c{5_vCV{|d^ zZ)Uww#8DX1d~;&0To91x_~}=6zR%fl#%@2*ZJ>N~z9%(ADmnRN-!~5>$N7|yt#UMU zkE6($S_~a88MNo^`>yfZE@P2_vj$j|%vab@GiV|YS`-gMvX-Am&5J78!-F3wY8R0R zX(=!7`NU71IWRMK1anXMQ%nvFJQVtMq?=-dupXIWL3_Fs*;NBax0>+8hw1Egvw09z zgYDz!RClaBD{=Zs7|(q|uYHh8cDCY8b3nmPykp^1uKFOr;p0`Q!%g0%hM9?&lR?)I zpHePk7dgsv^-!h}e=Oo!Bb4mjp(!BKh~gB_#M9ysVhc@4_vI9f$+nUn%G{2i)HFr# zsZfo3bUBswL>POtu3WY)%M?G{kH&gIBEvr-(7YvoDIg~f_buEY5$fH>drnf_QXG}| zu3~R@hlj~ITS&sKJ{vHCIv2%eHxX-D26VO$RFj3h$5<(!oJEtjsk6~&kJm(K}~99PGb7OHF3 zsqH4`s$asf&H6)MgvJaBkw$6|+|CttujK0#ywU8n%A!%^hoS;7${ds1nJjxp_TI@U zhCQn#kZ#Xfw&Q2}IJtRYN?M-%yYNg7W{aK*8Yr?xAHX$D)Mt45k%wSyR}F;lW5fMx zN%u~ezCG74nt%`l#cF;4xt{M7;Ru`9c6NQ-`!e-03oZzx0S19u&h)@yqKk7byc^%c#osa32!7Sy}6RD2sc|3G;<%z?pNj68mskZrO^?qiVuMeXZSe4eT*)~z!s zpTa?0ceSS=(D56}Bkv`+qoB^9Sx$+`FrSK!|_HKg#^AG#ms<(1nvF z??eN2SN$RuAdqO>Ueo6Qj;hk)PsXR6Zrm1r581Im2MC!Uxr#B3VG#fP#KM sFESD8-VOqJ(8vqRi^)qOZ(FbLwWtK!AN0Ilx@!vrS5T8LmNgIfKfNbXVgLXD diff --git a/lp-app/lpa-studio-web/story-images/studio__node__button-face__held__lg.png b/lp-app/lpa-studio-web/story-images/studio__node__button-face__held__lg.png index e7e6a4d0b995b87145a0fd8892e07fcf0496363b..2ddca44b3747f3f2a98086f51369b1fc47318128 100644 GIT binary patch delta 6056 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 delta 4814 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 delta 4799 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 diff --git a/lp-app/lpa-studio-web/story-images/studio__node__button-face__held__sm.png b/lp-app/lpa-studio-web/story-images/studio__node__button-face__held__sm.png index 159ca53a8a9b4e04cbfbf9061ec691c434473d62..a087d1d2644172e7000bb367b124cd2a91c67813 100644 GIT binary patch delta 5557 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 ICTAA>e^xuem;e9( delta 4321 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 P00000NkvXXu0mjf(r|Fd