diff --git a/Cargo.toml b/Cargo.toml index db719db..bb01ec9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,3 +35,8 @@ wasm-bindgen-futures = "0.4" [[example]] name = "demo" required-features = ["serde", "egui-probe"] + +# [patch.crates-io] +# egui = { path = "../egui/crates/egui" } +# eframe = { path = "../egui/crates/eframe" } +# egui_extras = { path = "../egui/crates/egui_extras" } \ No newline at end of file diff --git a/examples/demo.rs b/examples/demo.rs index 1ffa142..ae4fc86 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -173,6 +173,7 @@ impl SnarlViewer for DemoViewer { } #[allow(clippy::too_many_lines)] + #[allow(refining_impl_trait)] fn show_input( &mut self, pin: &InPin, @@ -388,6 +389,7 @@ impl SnarlViewer for DemoViewer { } } + #[allow(refining_impl_trait)] fn show_output( &mut self, pin: &OutPin, @@ -411,6 +413,7 @@ impl SnarlViewer for DemoViewer { .desired_width(0.0) .margin(ui.spacing().item_spacing); ui.add(edit); + ui.label("AAAAA"); PinInfo::circle() .with_fill(STRING_COLOR) .with_wire_style(WireStyle::AxisAligned { diff --git a/src/ui.rs b/src/ui.rs index cb5e1af..79a1a36 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -26,12 +26,12 @@ use self::{ pub use self::{ background_pattern::{BackgroundPattern, Grid, Viewport}, - pin::{AnyPins, PinInfo, PinShape}, + pin::{AnyPins, PinInfo, PinShape, PinWireInfo, SnarlPin}, viewer::SnarlViewer, wire::{WireLayer, WireStyle}, }; -/// Controls how header, pins, body and footer are laid out in the node. +/// Controls how header, pins, body and footer are placed in the node. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "egui-probe", derive(egui_probe::EguiProbe))] @@ -42,7 +42,10 @@ pub enum NodeLayout { /// +---------------------+ /// | Header | /// +----+-----------+----+ + /// | In | | Out| /// | In | Body | Out| + /// | In | | Out| + /// | In | | | /// +----+-----------+----+ /// | Footer | /// +---------------------+ @@ -57,10 +60,15 @@ pub enum NodeLayout { /// | Header | /// +---------------------+ /// | In | + /// | In | + /// | In | + /// | In | /// +---------------------+ /// | Body | /// +---------------------+ /// | Out | + /// | Out | + /// | Out | /// +---------------------+ /// | Footer | /// +---------------------+ @@ -73,14 +81,20 @@ pub enum NodeLayout { /// | Header | /// +---------------------+ /// | Out | + /// | Out | + /// | Out | /// +---------------------+ /// | Body | /// +---------------------+ /// | In | + /// | In | + /// | In | + /// | In | /// +---------------------+ /// | Footer | /// +---------------------+ FlippedSandwich, + // TODO: Add vertical layouts. } /// Controls style of node selection rect. @@ -612,8 +626,8 @@ struct DrawBodyResponse { struct PinResponse { pos: Pos2, - pin_color: Color32, - wire_style: Option, + wire_color: Color32, + wire_style: WireStyle, } impl Snarl { @@ -670,7 +684,7 @@ impl Snarl { let mut bg_r = ui.allocate_rect(ui.max_rect(), Sense::click_and_drag()); let viewport = bg_r.rect; - ui.set_clip_rect(viewport); + ui.set_clip_rect(rect_round(viewport)); let pivot = input.hover_pos.unwrap_or_else(|| viewport.center()); @@ -699,11 +713,12 @@ impl Snarl { if viewport.contains(hover_pos) && ui.rect_contains_pointer(viewport) => { if input.zoom_delta != 1.0 { - let new_scale = (snarl_state.scale() - * input.zoom_delta.powf(style.get_scale_velocity())) - .clamp(style.get_min_scale(), style.get_max_scale()); - - snarl_state.set_scale(new_scale); + snarl_state.zoom_delta( + input.zoom_delta, + style.get_scale_velocity(), + style.get_min_scale(), + style.get_max_scale(), + ); } } _ => {} @@ -775,7 +790,7 @@ impl Snarl { if !wire_hit && !snarl_state.has_new_wires() && bg_r.hovered() && !bg_r.dragged() { // Try to find hovered wire - // If not draggin new wire + // If not dragging new wire // And not hovering over item above. if let Some(interact_pos) = input.interact_pos { @@ -787,12 +802,7 @@ impl Snarl { from_r.pos, to_r.pos, wire_width.max(1.5), - pick_wire_style( - style.get_wire_style(snarl_state.scale()), - from_r.wire_style, - to_r.wire_style, - ) - .zoomed(snarl_state.scale()), + pick_wire_style(from_r.wire_style, to_r.wire_style), ); if wire_hit { @@ -808,7 +818,7 @@ impl Snarl { } } - let color = mix_colors(from_r.pin_color, to_r.pin_color); + let color = mix_colors(from_r.wire_color, to_r.wire_color); let mut draw_width = wire_width; if hovered_wire == Some(wire) { @@ -824,11 +834,7 @@ impl Snarl { from_r.pos, to_r.pos, Stroke::new(draw_width, color), - pick_wire_style( - style.get_wire_style(snarl_state.scale()), - from_r.wire_style.zoomed(snarl_state.scale()), - to_r.wire_style.zoomed(snarl_state.scale()), - ), + pick_wire_style(from_r.wire_style, to_r.wire_style), ); } @@ -1032,10 +1038,8 @@ impl Snarl { style.get_downscale_wire_frame(), from_pos, to_r.pos, - Stroke::new(wire_width, to_r.pin_color), - to_r.wire_style - .zoomed(snarl_state.scale()) - .unwrap_or_else(|| style.get_wire_style(snarl_state.scale())), + Stroke::new(wire_width, to_r.wire_color), + to_r.wire_style.zoomed(snarl_state.scale()), ); } } @@ -1052,11 +1056,8 @@ impl Snarl { style.get_downscale_wire_frame(), from_r.pos, to_pos, - Stroke::new(wire_width, from_r.pin_color), - from_r - .wire_style - .zoomed(snarl_state.scale()) - .unwrap_or_else(|| style.get_wire_style(snarl_state.scale())), + Stroke::new(wire_width, from_r.wire_color), + from_r.wire_style.zoomed(snarl_state.scale()), ); } } @@ -1127,12 +1128,12 @@ impl Snarl { // Input pins on the left. let inputs_ui = &mut ui.new_child( UiBuilder::new() - .max_rect(inputs_rect) + .max_rect(rect_round(inputs_rect)) .layout(Layout::top_down(Align::Min)) .id_salt("inputs"), ); - inputs_ui.set_clip_rect(clip_rect.intersect(viewport)); + inputs_ui.set_clip_rect(rect_round(clip_rect.intersect(viewport))); for in_pin in inputs { // Show input pin. @@ -1144,7 +1145,7 @@ impl Snarl { let y0 = ui.cursor().min.y; // Show input content - let pin_info = viewer.show_input(in_pin, ui, snarl_state.scale(), self); + let snarl_pin = viewer.show_input(in_pin, ui, snarl_state.scale(), self); if !self.nodes.contains(node.0) { // If removed return; @@ -1160,7 +1161,7 @@ impl Snarl { let pin_pos = pos2(input_x, y); // Interact with pin shape. - ui.set_clip_rect(viewport); + ui.set_clip_rect(rect_round(viewport)); let r = ui.interact( Rect::from_center_size(pin_pos, vec2(pin_size, pin_size)), @@ -1199,7 +1200,7 @@ impl Snarl { drag_released = true; } - let mut visual_pin_size = pin_size; + let mut visual_pin_rect = r.rect; match input.hover_pos { Some(hover_pos) if r.rect.contains(hover_pos) => { @@ -1209,32 +1210,28 @@ impl Snarl { snarl_state.remove_new_wire_in(in_pin.id); } pin_hovered = Some(AnyPin::In(in_pin.id)); - visual_pin_size *= 1.2; + visual_pin_rect = visual_pin_rect.scale_from_center(1.2); } _ => {} } let mut pin_painter = ui.painter().clone(); - pin_painter.set_clip_rect(viewport); + pin_painter.set_clip_rect(rect_round(viewport)); - let pin_color = viewer.draw_input_pin( - in_pin, - &pin_info, - r.rect.center(), - visual_pin_size, + let wire_info = snarl_pin.draw( + snarl_state.scale(), style, ui.style(), + visual_pin_rect, &pin_painter, - snarl_state.scale(), - self, ); input_positions.insert( in_pin.id, PinResponse { pos: r.rect.center(), - pin_color, - wire_style: pin_info.wire_style, + wire_color: wire_info.color, + wire_style: wire_info.style, }, ); }); @@ -1277,12 +1274,12 @@ impl Snarl { let outputs_ui = &mut ui.new_child( UiBuilder::new() - .max_rect(outputs_rect) + .max_rect(rect_round(outputs_rect)) .layout(Layout::top_down(Align::Max)) .id_salt("outputs"), ); - outputs_ui.set_clip_rect(clip_rect.intersect(viewport)); + outputs_ui.set_clip_rect(rect_round(clip_rect.intersect(viewport))); // Output pins on the right. for out_pin in outputs { @@ -1296,7 +1293,7 @@ impl Snarl { let y0 = ui.cursor().min.y; // Show output content - let pin_info = viewer.show_output(out_pin, ui, snarl_state.scale(), self); + let snarl_pin = viewer.show_output(out_pin, ui, snarl_state.scale(), self); if !self.nodes.contains(node.0) { // If removed return; @@ -1311,7 +1308,7 @@ impl Snarl { let pin_pos = pos2(output_x, y); - ui.set_clip_rect(viewport); + ui.set_clip_rect(rect_round(viewport)); let r = ui.interact( Rect::from_center_size(pin_pos, vec2(pin_size, pin_size)), @@ -1351,7 +1348,7 @@ impl Snarl { drag_released = true; } - let mut visual_pin_size = pin_size; + let mut visual_pin_rect = r.rect; match input.hover_pos { Some(hover_pos) if r.rect.contains(hover_pos) => { if input.modifiers.shift { @@ -1360,32 +1357,28 @@ impl Snarl { snarl_state.remove_new_wire_out(out_pin.id); } pin_hovered = Some(AnyPin::Out(out_pin.id)); - visual_pin_size *= 1.2; + visual_pin_rect = visual_pin_rect.scale_from_center(1.2); } _ => {} } let mut pin_painter = ui.painter().clone(); - pin_painter.set_clip_rect(viewport); + pin_painter.set_clip_rect(rect_round(viewport)); - let pin_color = viewer.draw_output_pin( - out_pin, - &pin_info, - r.rect.center(), - visual_pin_size, + let wire_info = snarl_pin.draw( + snarl_state.scale(), style, ui.style(), + visual_pin_rect, &pin_painter, - snarl_state.scale(), - self, ); output_positions.insert( out_pin.id, PinResponse { pos: r.rect.center(), - pin_color, - wire_style: pin_info.wire_style, + wire_color: wire_info.color, + wire_style: wire_info.style, }, ); }); @@ -1418,11 +1411,11 @@ impl Snarl { { let mut body_ui = ui.new_child( UiBuilder::new() - .max_rect(body_rect) + .max_rect(rect_round(body_rect)) .layout(Layout::left_to_right(Align::Min)) .id_salt("body"), ); - body_ui.set_clip_rect(clip_rect.intersect(viewport)); + body_ui.set_clip_rect(rect_round(clip_rect.intersect(viewport))); viewer.show_body( node, @@ -1480,7 +1473,7 @@ impl Snarl { .map(|idx| OutPin::new(self, OutPinId { node, output: idx })) .collect::>(); - let node_pos = snarl_state.graph_pos_to_screen(pos, viewport); + let node_pos = snarl_state.graph_pos_to_screen(pos, viewport).round(); // Generate persistent id for the node. let node_id = snarl_id.with(("snarl-node", node)); @@ -1589,7 +1582,7 @@ impl Snarl { let node_ui = &mut ui.new_child( UiBuilder::new() - .max_rect(node_frame_rect) + .max_rect(rect_round(node_frame_rect)) .layout(Layout::top_down(Align::Center)) .id_salt(node_id), ); @@ -1749,14 +1742,15 @@ impl Snarl { // Show body if there's one. if viewer.has_body(&self.nodes.get(node.0).unwrap().value) { - let body_left = inputs_rect.right() + ui.spacing().item_spacing.x; - let body_right = outputs_rect.left() - ui.spacing().item_spacing.x; - let body_top = payload_rect.top(); - let body_bottom = payload_rect.bottom(); - let body_rect = Rect::from_min_max( - pos2(body_left, body_top), - pos2(body_right, body_bottom), + pos2( + inputs_rect.right() + ui.spacing().item_spacing.x, + payload_rect.top(), + ), + pos2( + outputs_rect.left() - ui.spacing().item_spacing.x, + payload_rect.bottom(), + ), ); let r = self.draw_body( @@ -1787,7 +1781,6 @@ impl Snarl { NodeLayout::Sandwich => { // Show input pins. - let inputs_rect = payload_rect; let r = self.draw_inputs( viewer, node, @@ -1795,7 +1788,7 @@ impl Snarl { pin_size, style, ui, - inputs_rect, + payload_rect, payload_clip_rect, viewport, input_x, @@ -2013,19 +2006,17 @@ impl Snarl { }; if viewer.has_footer(&self.nodes[node.0].value) { - let footer_left = node_rect.left(); - let footer_right = node_rect.right(); - let footer_top = pins_rect.bottom() + ui.spacing().item_spacing.y; - let footer_bottom = node_rect.bottom(); - let footer_rect = Rect::from_min_max( - pos2(footer_left, footer_top), - pos2(footer_right, footer_bottom), + pos2( + node_rect.left(), + pins_rect.bottom() + ui.spacing().item_spacing.y, + ), + pos2(node_rect.right(), node_rect.bottom()), ); let mut footer_ui = ui.new_child( UiBuilder::new() - .max_rect(footer_rect) + .max_rect(rect_round(footer_rect)) .layout(Layout::left_to_right(Align::Min)) .id_salt("footer"), ); @@ -2061,7 +2052,7 @@ impl Snarl { // Show node's header let header_ui: &mut Ui = &mut ui.new_child( UiBuilder::new() - .max_rect(node_rect + header_frame.total_margin()) + .max_rect(rect_round(node_rect + header_frame.total_margin())) .layout(Layout::top_down(Align::Center)) .id_salt("header"), ); @@ -2222,3 +2213,8 @@ const fn snarl_style_is_send_sync() { const fn is_send_sync() {} is_send_sync::(); } + +fn rect_round(r: Rect) -> Rect { + // return r; + Rect::from_min_max(r.min.round(), r.max.round()) +} diff --git a/src/ui/pin.rs b/src/ui/pin.rs index cf6d880..1ae5912 100644 --- a/src/ui/pin.rs +++ b/src/ui/pin.rs @@ -1,4 +1,4 @@ -use egui::{epaint::PathShape, vec2, Color32, Painter, Pos2, Shape, Stroke, Style, Vec2}; +use egui::{epaint::PathShape, vec2, Color32, Painter, Rect, Shape, Stroke, Style, Vec2}; use crate::{InPinId, OutPinId}; @@ -20,6 +20,39 @@ pub enum AnyPins<'a> { In(&'a [InPinId]), } +/// Contains information about a pin's wire. +/// Used to draw the wire. +/// When two pins are connected, the wire is drawn between them, +/// using merged `PinWireInfo` from both pins. +pub struct PinWireInfo { + /// Desired color of the wire. + pub color: Color32, + + /// Desired style of the wire. + /// Zoomed with current scale. + pub style: WireStyle, +} + +/// Uses `Painter` to draw a pin. +pub trait SnarlPin { + /// Draws the pin. + /// + /// `rect` is the interaction rectangle of the pin. + /// Pin should fit in it. + /// `painter` is used to add pin's shapes to the UI. + /// + /// Returns the color + #[must_use] + fn draw( + self, + scale: f32, + snarl_style: &SnarlStyle, + style: &Style, + rect: Rect, + painter: &Painter, + ) -> PinWireInfo; +} + /// Shape of a pin. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -48,15 +81,16 @@ pub struct PinInfo { /// Shape of the pin. pub shape: Option, - /// Size of the pin. - pub size: Option, - /// Fill color of the pin. pub fill: Option, /// Outline stroke of the pin. pub stroke: Option, + /// Color of the wire connected to the pin. + /// If `None`, the pin's fill color is used. + pub wire_color: Option, + /// Style of the wire connected to the pin. pub wire_style: Option, } @@ -69,13 +103,6 @@ impl PinInfo { self } - /// Sets the size of the pin. - #[must_use] - pub const fn with_size(mut self, size: f32) -> Self { - self.size = Some(size); - self - } - /// Sets the fill color of the pin. #[must_use] pub const fn with_fill(mut self, fill: Color32) -> Self { @@ -97,6 +124,13 @@ impl PinInfo { self } + /// Sets the color of the wire connected to the pin. + #[must_use] + pub const fn with_wire_color(mut self, wire_color: Color32) -> Self { + self.wire_color = Some(wire_color); + self + } + /// Creates a circle pin. #[must_use] pub fn circle() -> Self { @@ -159,41 +193,54 @@ impl PinInfo { #[must_use] pub fn draw( &self, - pos: Pos2, - size: f32, + scale: f32, snarl_style: &SnarlStyle, style: &Style, + rect: Rect, painter: &Painter, - scale: f32, - ) -> Color32 { + ) -> PinWireInfo { let shape = self.get_shape(snarl_style); let fill = self.get_fill(snarl_style, style); let stroke = self.get_stroke(snarl_style, style, scale); - let size = self.size.zoomed(scale).unwrap_or(size); - draw_pin(painter, shape, fill, stroke, pos, size); + draw_pin(painter, shape, fill, stroke, rect); + + PinWireInfo { + color: self.wire_color.unwrap_or(fill), + style: self + .wire_style + .zoomed(scale) + .unwrap_or(snarl_style.get_wire_style(scale)), + } + } +} - fill +impl SnarlPin for PinInfo { + fn draw( + self, + scale: f32, + snarl_style: &SnarlStyle, + style: &Style, + rect: Rect, + painter: &Painter, + ) -> PinWireInfo { + Self::draw(&self, scale, snarl_style, style, rect, painter) } } -pub fn draw_pin( - painter: &Painter, - shape: PinShape, - fill: Color32, - stroke: Stroke, - pos: Pos2, - size: f32, -) { +pub fn draw_pin(painter: &Painter, shape: PinShape, fill: Color32, stroke: Stroke, rect: Rect) { + let center = rect.center(); + let size = f32::min(rect.width(), rect.height()); + match shape { PinShape::Circle => { - painter.circle(pos, size * 2.0 / std::f32::consts::PI, fill, stroke); + painter.circle(center, size / 2.0, fill, stroke); } PinShape::Triangle => { const A: Vec2 = vec2(-0.649_519, 0.4875); const B: Vec2 = vec2(0.649_519, 0.4875); const C: Vec2 = vec2(0.0, -0.6375); - let points = vec![pos + A * size, pos + B * size, pos + C * size]; + let points = vec![center + A * size, center + B * size, center + C * size]; painter.add(Shape::Path(PathShape { points, @@ -204,10 +251,10 @@ pub fn draw_pin( } PinShape::Square => { let points = vec![ - pos + vec2(-0.5, -0.5) * size, - pos + vec2(0.5, -0.5) * size, - pos + vec2(0.5, 0.5) * size, - pos + vec2(-0.5, 0.5) * size, + center + vec2(-0.5, -0.5) * size, + center + vec2(0.5, -0.5) * size, + center + vec2(0.5, 0.5) * size, + center + vec2(-0.5, 0.5) * size, ]; painter.add(Shape::Path(PathShape { @@ -220,16 +267,16 @@ pub fn draw_pin( PinShape::Star => { let points = vec![ - pos + size * 0.700_000 * vec2(0.0, -1.0), - pos + size * 0.267_376 * vec2(-0.587_785, -0.809_017), - pos + size * 0.700_000 * vec2(-0.951_057, -0.309_017), - pos + size * 0.267_376 * vec2(-0.951_057, 0.309_017), - pos + size * 0.700_000 * vec2(-0.587_785, 0.809_017), - pos + size * 0.267_376 * vec2(0.0, 1.0), - pos + size * 0.700_000 * vec2(0.587_785, 0.809_017), - pos + size * 0.267_376 * vec2(0.951_057, 0.309_017), - pos + size * 0.700_000 * vec2(0.951_057, -0.309_017), - pos + size * 0.267_376 * vec2(0.587_785, -0.809_017), + center + size * 0.700_000 * vec2(0.0, -1.0), + center + size * 0.267_376 * vec2(-0.587_785, -0.809_017), + center + size * 0.700_000 * vec2(-0.951_057, -0.309_017), + center + size * 0.267_376 * vec2(-0.951_057, 0.309_017), + center + size * 0.700_000 * vec2(-0.587_785, 0.809_017), + center + size * 0.267_376 * vec2(0.0, 1.0), + center + size * 0.700_000 * vec2(0.587_785, 0.809_017), + center + size * 0.267_376 * vec2(0.951_057, 0.309_017), + center + size * 0.700_000 * vec2(0.951_057, -0.309_017), + center + size * 0.267_376 * vec2(0.587_785, -0.809_017), ]; painter.add(Shape::Path(PathShape { diff --git a/src/ui/state.rs b/src/ui/state.rs index 06aad52..15727b1 100644 --- a/src/ui/state.rs +++ b/src/ui/state.rs @@ -290,15 +290,15 @@ impl SnarlState { let mut offset = Vec2::ZERO; let mut scale = 1.0f32.clamp(style.get_min_scale(), style.get_max_scale()); - if bb.is_positive() { + if bb.is_finite() { bb = bb.expand(100.0); let bb_size = bb.size(); let viewport_size = viewport.size(); - scale = (viewport_size.x / bb_size.x) + scale = (viewport_size.x / bb_size.x.min(1.0)) .min(1.0) - .min(viewport_size.y / bb_size.y) + .min(viewport_size.y / bb_size.y.min(1.0)) .min(style.get_max_scale()) .max(style.get_min_scale()); @@ -338,36 +338,48 @@ impl SnarlState { } } + pub fn set_offset(&mut self, offset: Vec2) { + if self.offset != offset { + self.offset = offset; + self.dirty = true; + } + } + #[inline(always)] pub fn pan(&mut self, delta: Vec2) { - self.offset += delta; - self.dirty = true; + if delta != Vec2::ZERO { + self.offset += delta; + self.dirty = true; + } } #[inline(always)] - pub const fn scale(&self) -> f32 { - self.scale + pub fn scale(&self) -> f32 { + (self.scale * 1024.0).round() / 1024.0 } #[inline(always)] - pub const fn offset(&self) -> Vec2 { + pub fn offset(&self) -> Vec2 { self.offset } #[inline(always)] - pub fn set_scale(&mut self, scale: f32) { - self.target_scale = scale; + pub fn zoom_delta(&mut self, zoom_delta: f32, velocity: f32, min: f32, max: f32) { + if zoom_delta == 1.0 { + return; + } + self.target_scale = (self.target_scale * zoom_delta.powf(velocity)).clamp(min, max); self.dirty = true; } #[inline(always)] pub fn screen_pos_to_graph(&self, pos: Pos2, viewport: Rect) -> Pos2 { - (pos + self.offset - viewport.center().to_vec2()) / self.scale + (pos + self.offset() - viewport.center().to_vec2()) / self.scale() } #[inline(always)] pub fn graph_pos_to_screen(&self, pos: Pos2, viewport: Rect) -> Pos2 { - pos * self.scale - self.offset + viewport.center().to_vec2() + pos * self.scale() - self.offset() + viewport.center().to_vec2() } #[inline(always)] @@ -393,7 +405,7 @@ impl SnarlState { #[inline(always)] pub fn screen_vec_to_graph(&self, size: Vec2) -> Vec2 { - size / self.scale + size / self.scale() } // #[inline(always)] @@ -524,11 +536,6 @@ impl SnarlState { self.dirty = true; } - pub fn set_offset(&mut self, offset: Vec2) { - self.offset = offset; - self.dirty = true; - } - pub fn selected_nodes(&self) -> &[NodeId] { &self.selected_nodes } diff --git a/src/ui/viewer.rs b/src/ui/viewer.rs index ec37d81..7e9d7d7 100644 --- a/src/ui/viewer.rs +++ b/src/ui/viewer.rs @@ -1,8 +1,11 @@ -use egui::{Color32, Painter, Pos2, Rect, Style, Ui}; +use egui::{Painter, Pos2, Rect, Style, Ui}; use crate::{InPin, InPinId, NodeId, OutPin, OutPinId, Snarl}; -use super::{pin::AnyPins, BackgroundPattern, NodeLayout, PinInfo, SnarlStyle, Viewport}; +use super::{ + pin::{AnyPins, SnarlPin}, + BackgroundPattern, NodeLayout, SnarlStyle, Viewport, +}; /// `SnarlViewer` is a trait for viewing a Snarl. /// @@ -13,6 +16,13 @@ pub trait SnarlViewer { fn title(&mut self, node: &T) -> String; /// Returns the node's frame. + /// All node's elements will be rendered inside this frame. + /// Except for pins if they are configured to be rendered outside of the frame. + /// + /// Returns `default` by default. + /// `default` frame is taken from the [`SnarlStyle::node_frame`] or constructed if it's `None`. + /// + /// Override this method to customize the frame for specific nodes. fn node_frame( &mut self, default: egui::Frame, @@ -26,6 +36,14 @@ pub trait SnarlViewer { } /// Returns the node's header frame. + /// + /// This frame would be placed on top of the node's frame. + /// And header UI (see [`show_header`]) will be placed inside this frame. + /// + /// Returns `default` by default. + /// `default` frame is taken from the [`SnarlStyle::header_frame`], + /// or [`SnarlStyle::node_frame`] with removed shadow if `None`, + /// or constructed if both are `None`. fn header_frame( &mut self, default: egui::Frame, @@ -38,10 +56,14 @@ pub trait SnarlViewer { default } - /// Returns layout override for the node. + /// Returns elements layout for the node. + /// + /// Node consists of 5 parts: header, body, footer, input pins and output pins. + /// See [`NodeLayout`] for available placements. /// - /// This method can be used to override the default layout of the node. - /// By default it returns `None` and layout from the style is used. + /// Returns `default` by default. + /// `default` layout is taken from the [`SnarlStyle::node_layout`] or constructed if it's `None`. + /// Override this method to customize the layout for specific nodes. #[inline] fn node_layout( &mut self, @@ -55,7 +77,11 @@ pub trait SnarlViewer { default } - /// Renders the node's header. + /// Renders elements inside the node's header frame. + /// + /// This is the good place to show the node's title and controls related to the whole node. + /// + /// By default it shows the node's title. #[inline] fn show_header( &mut self, @@ -75,9 +101,14 @@ pub trait SnarlViewer { /// [`SnarlViewer::show_input`] and [`SnarlViewer::draw_input_pin`] will be called for each input in range `0..inputs()`. fn inputs(&mut self, node: &T) -> usize; - /// Renders the node's input. - fn show_input(&mut self, pin: &InPin, ui: &mut Ui, scale: f32, snarl: &mut Snarl) - -> PinInfo; + /// Renders one specified node's input element and returns drawer for the corresponding pin. + fn show_input( + &mut self, + pin: &InPin, + ui: &mut Ui, + scale: f32, + snarl: &mut Snarl, + ) -> impl SnarlPin + 'static; /// Returns number of output pins of the node. /// @@ -91,7 +122,7 @@ pub trait SnarlViewer { ui: &mut Ui, scale: f32, snarl: &mut Snarl, - ) -> PinInfo; + ) -> impl SnarlPin + 'static; /// Checks if node has something to show in body - between input and output pins. #[inline] @@ -288,54 +319,6 @@ pub trait SnarlViewer { snarl.drop_inputs(pin.id); } - /// Draws the node's input pin. - /// - /// This method is called after [`SnarlViewer::show_input`] and can be used to draw the pin shape. - /// By default it draws a pin with the shape and style returned by [`SnarlViewer::show_input`]. - /// - /// If you want to draw the pin yourself, you can override this method. - #[allow(clippy::too_many_arguments)] - fn draw_input_pin( - &mut self, - pin: &InPin, - pin_info: &PinInfo, - pos: Pos2, - size: f32, - snarl_style: &SnarlStyle, - style: &Style, - painter: &Painter, - scale: f32, - snarl: &Snarl, - ) -> Color32 { - let _ = (pin, snarl); - - pin_info.draw(pos, size, snarl_style, style, painter, scale) - } - - /// Draws the node's output pin. - /// - /// This method is called after [`SnarlViewer::show_output`] and can be used to draw the pin shape. - /// By default it draws a pin with the shape and style returned by [`SnarlViewer::show_output`]. - /// - /// If you want to draw the pin yourself, you can override this method. - #[allow(clippy::too_many_arguments)] - fn draw_output_pin( - &mut self, - pin: &OutPin, - pin_info: &PinInfo, - pos: Pos2, - size: f32, - snarl_style: &SnarlStyle, - style: &Style, - painter: &Painter, - scale: f32, - snarl: &Snarl, - ) -> Color32 { - let _ = (pin, snarl); - - pin_info.draw(pos, size, snarl_style, style, painter, scale) - } - /// Draws background of the snarl view. /// /// By default it draws the background pattern using [`BackgroundPattern::draw`]. diff --git a/src/ui/wire.rs b/src/ui/wire.rs index bc1c938..2411256 100644 --- a/src/ui/wire.rs +++ b/src/ui/wire.rs @@ -1,4 +1,4 @@ -use std::f32; +use core::f32; use egui::{epaint::PathShape, pos2, Color32, Pos2, Rect, Shape, Stroke, Ui}; @@ -18,17 +18,15 @@ pub enum WireLayer { } /// Controls style in which wire is rendered. +/// +/// Variants are given in order of precedence when two pins require different styles. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "egui-probe", derive(egui_probe::EguiProbe))] #[derive(Default)] pub enum WireStyle { - /// Draw wire as 3rd degree Bezier curve. - Bezier3, - - /// Draw wire as 5th degree Bezier curve. - #[default] - Bezier5, + /// Straight line from one endpoint to another. + Line, /// Draw wire as straight lines with 90 degree turns. /// Corners has radius of `corner_radius`. @@ -36,29 +34,28 @@ pub enum WireStyle { /// Radius of corners in wire. corner_radius: f32, }, + + /// Draw wire as 3rd degree Bezier curve. + Bezier3, + + /// Draw wire as 5th degree Bezier curve. + #[default] + Bezier5, } -pub fn pick_wire_style( - default: WireStyle, - left: Option, - right: Option, -) -> WireStyle { +pub fn pick_wire_style(left: WireStyle, right: WireStyle) -> WireStyle { match (left, right) { - (None, None) => default, - (Some(one), None) | (None, Some(one)) => one, - (Some(WireStyle::Bezier5), Some(WireStyle::Bezier5)) => WireStyle::Bezier5, - (Some(WireStyle::Bezier3 | WireStyle::Bezier5), Some(WireStyle::Bezier3)) - | (Some(WireStyle::Bezier3), Some(WireStyle::Bezier5)) => WireStyle::Bezier3, + (WireStyle::Line, _) | (_, WireStyle::Line) => WireStyle::Line, ( - Some(WireStyle::AxisAligned { corner_radius: a }), - Some(WireStyle::AxisAligned { corner_radius: b }), + WireStyle::AxisAligned { corner_radius: a }, + WireStyle::AxisAligned { corner_radius: b }, ) => WireStyle::AxisAligned { - corner_radius: a.max(b), + corner_radius: f32::max(a, b), }, - (Some(WireStyle::AxisAligned { corner_radius }), Some(_)) - | (Some(_), Some(WireStyle::AxisAligned { corner_radius })) => { - WireStyle::AxisAligned { corner_radius } - } + (WireStyle::AxisAligned { corner_radius }, _) + | (_, WireStyle::AxisAligned { corner_radius }) => WireStyle::AxisAligned { corner_radius }, + (WireStyle::Bezier3, _) | (_, WireStyle::Bezier3) => WireStyle::Bezier3, + (WireStyle::Bezier5, WireStyle::Bezier5) => WireStyle::Bezier5, } } @@ -223,6 +220,12 @@ pub fn draw_wire( let frame_size = adjust_frame_size(frame_size, upscale, downscale, from, to); match style { + WireStyle::Line => { + let bb = Rect::from_two_pos(from, to); + if ui.is_rect_visible(bb) { + shapes.push(Shape::line_segment([from, to], stroke)); + } + } WireStyle::Bezier3 => { let [a, _, b, c, _, d] = wire_bezier_5(frame_size, from, to); let points = [a, b, c, d]; @@ -261,6 +264,21 @@ pub fn hit_wire( ) -> bool { let frame_size = adjust_frame_size(frame_size, upscale, downscale, from, to); match style { + WireStyle::Line => { + let aabb = Rect::from_two_pos(from, to); + let aabb_e = aabb.expand(threshold); + if !aabb_e.contains(pos) { + return false; + } + + let a = to - from; + let b = pos - from; + + let dot = b.dot(a); + let dist2 = b.length_sq() - dot * dot / a.length_sq(); + + dist2 < threshold * threshold + } WireStyle::Bezier3 => { let [a, _, b, c, _, d] = wire_bezier_5(frame_size, from, to); let points = [a, b, c, d]; @@ -277,7 +295,7 @@ pub fn hit_wire( } #[inline] -fn bezier_reference_size(points: &[Pos2]) -> f32 { +fn curve_reference_size(points: &[Pos2]) -> f32 { let mut size = 0.0; for i in 1..points.len() { size += (points[i] - points[i - 1]).length(); @@ -288,7 +306,7 @@ fn bezier_reference_size(points: &[Pos2]) -> f32 { const MAX_CURVE_SAMPLES: usize = 100; fn bezier_samples_number(points: &[Pos2], threshold: f32) -> usize { - let reference_size = bezier_reference_size(points); + let reference_size = curve_reference_size(points); #[allow(clippy::cast_sign_loss)] #[allow(clippy::cast_possible_truncation)] @@ -512,135 +530,145 @@ fn hit_bezier_5(pos: Pos2, points: &[Pos2; 6], threshold: f32) -> bool { } struct AxisAlignedWire { - points: [Pos2; 5], - turns: [(Pos2, f32); 4], + aabb: Rect, + turns: usize, + segments: [(Pos2, Pos2); 5], + turn_centers: [Pos2; 4], + turn_radii: [f32; 4], } #[allow(clippy::too_many_lines)] fn wire_axis_aligned(corner_radius: f32, frame_size: f32, from: Pos2, to: Pos2) -> AxisAlignedWire { - if from.x + frame_size <= to.x - frame_size { - let mid = pos2((from.x + to.x) / 2.0, (from.y + to.y) / 2.0); + let corner_radius = corner_radius.max(0.0); - let from_turn_radius = f32::abs(mid.x - from.x) - .min(f32::abs(mid.y - from.y)) - .min(corner_radius); + let half_height = f32::abs(from.y - to.y) / 2.0; + let max_radius = (half_height / 2.0).min(corner_radius); - let to_turn_radius = f32::abs(to.x - mid.x) - .min(f32::abs(mid.y - to.y)) - .min(corner_radius); + let frame_size = frame_size.max(max_radius * 2.0); - let from_turn_x = mid.x - from_turn_radius; - let from_turn_y = if from.y < to.y { - from.y + from_turn_radius - } else { - from.y - from_turn_radius - }; + let zero_segment = (Pos2::ZERO, Pos2::ZERO); - let to_turn_x = mid.x + to_turn_radius; - let to_turn_y = if from.y < to.y { - to.y - to_turn_radius + if from.x + frame_size <= to.x - frame_size { + if from.y == to.y { + // Single segment case. + AxisAlignedWire { + aabb: Rect::from_two_pos(from, to), + segments: [ + (from, to), + zero_segment, + zero_segment, + zero_segment, + zero_segment, + ], + turns: 0, + turn_centers: [Pos2::ZERO; 4], + turn_radii: [f32::NAN; 4], + } } else { - to.y + to_turn_radius - }; + // Two turns case. + let mid_x = (from.x + to.x) / 2.0; + let half_width = (to.x - from.x) / 2.0; - AxisAlignedWire { - points: [from, mid, mid, mid, to], - turns: [ - (pos2(from_turn_x, from_turn_y), from_turn_radius), - (mid, 0.0), - (mid, 0.0), - (pos2(to_turn_x, to_turn_y), to_turn_radius), - ], - } - } else { - let from_2nd = pos2( - from.x + frame_size, - if from.y + frame_size + corner_radius <= to.y - frame_size - corner_radius { - from.y + frame_size - } else if from.y <= to.y { - from.y + (to.y - from.y) / 4.0 - } else if from.y - frame_size - corner_radius >= to.y + frame_size + corner_radius { - from.y - frame_size - } else { - from.y - (from.y - to.y) / 4.0 - }, - ); + let turn_radius = max_radius.min(half_width); - let to_2nd = pos2( - to.x - frame_size, - if from.y + frame_size + corner_radius <= to.y - frame_size - corner_radius { - to.y - frame_size - } else if from.y <= to.y { - to.y - (to.y - from.y) / 4.0 - } else if from.y - frame_size - corner_radius >= to.y + frame_size + corner_radius { - to.y + frame_size + let turn_vert_len = if from.y < to.y { + turn_radius } else { - to.y + (from.y - to.y) / 4.0 - }, - ); - - let mid = pos2((from_2nd.x + to_2nd.x) / 2.0, (from_2nd.y + to_2nd.y) / 2.0); - - let from_turn_radius = f32::abs(from_2nd.x - from.x) - .min(f32::abs(from_2nd.y - from.y)) - .min(corner_radius); - - let from_turn_x = from_2nd.x - from_turn_radius; - let from_turn_y = if from.y < from_2nd.y { - from.y + from_turn_radius - } else { - from.y - from_turn_radius - }; - - let from_turn = pos2(from_turn_x, from_turn_y); - - let from_2nd_turn_radius = f32::abs(mid.x - from_2nd.x) - .min(f32::abs(mid.y - from_2nd.y)) - .min(corner_radius); + -turn_radius + }; + + let segments = [ + (from, pos2(mid_x - turn_radius, from.y)), + ( + pos2(mid_x, from.y + turn_vert_len), + pos2(mid_x, to.y - turn_vert_len), + ), + (pos2(mid_x + turn_radius, to.y), to), + zero_segment, + zero_segment, + ]; + + let turn_centers = [ + pos2(mid_x - turn_radius, from.y + turn_vert_len), + pos2(mid_x + turn_radius, to.y - turn_vert_len), + Pos2::ZERO, + Pos2::ZERO, + ]; + + let turn_radii = [turn_radius, turn_radius, f32::NAN, f32::NAN]; + + AxisAlignedWire { + aabb: Rect::from_two_pos(from, to), + turns: 2, + segments, + turn_centers, + turn_radii, + } + } + } else { + // Four turns case. + let mid = f32::abs(from.y + to.y) / 2.0; - let from_2nd_turn_x = from_2nd.x - from_2nd_turn_radius; - let from_2nd_turn_y = if from_2nd.y < mid.y { - mid.y - from_2nd_turn_radius - } else { - mid.y + from_2nd_turn_radius - }; + let right = from.x + frame_size; + let left = to.x - frame_size; - let from_2nd_turn = pos2(from_2nd_turn_x, from_2nd_turn_y); + let half_width = f32::abs(right - left) / 2.0; - let to_turn_radius = f32::abs(to_2nd.x - to.x) - .min(f32::abs(to_2nd.y - to.y)) - .min(corner_radius); + let ends_turn_radius = max_radius; + let middle_turn_radius = max_radius.min(half_width); - let to_turn_x = to_2nd.x + to_turn_radius; - let to_turn_y = if to.y < to_2nd.y { - to.y + to_turn_radius + let ends_turn_vert_len = if from.y < to.y { + ends_turn_radius } else { - to.y - to_turn_radius + -ends_turn_radius }; - let to_turn = pos2(to_turn_x, to_turn_y); - - let to_2nd_turn_radius = f32::abs(mid.x - to_2nd.x) - .min(f32::abs(mid.y - to_2nd.y)) - .min(corner_radius); - - let to_2nd_turn_x = to_2nd.x + to_2nd_turn_radius; - let to_2nd_turn_y = if to_2nd.y < mid.y { - mid.y - to_2nd_turn_radius + let middle_turn_vert_len = if from.y < to.y { + middle_turn_radius } else { - mid.y + to_2nd_turn_radius + -middle_turn_radius }; - let to_2nd_turn = pos2(to_2nd_turn_x, to_2nd_turn_y); + let segments = [ + (from, pos2(right - ends_turn_radius, from.y)), + ( + pos2(right, from.y + ends_turn_vert_len), + pos2(right, mid - middle_turn_vert_len), + ), + ( + pos2(right - middle_turn_radius, mid), + pos2(left + middle_turn_radius, mid), + ), + ( + pos2(left, mid + middle_turn_vert_len), + pos2(left, to.y - ends_turn_vert_len), + ), + (pos2(left + ends_turn_radius, to.y), to), + ]; + + let turn_centers = [ + pos2(right - ends_turn_radius, from.y + ends_turn_vert_len), + pos2(right - middle_turn_radius, mid - middle_turn_vert_len), + pos2(left + middle_turn_radius, mid + middle_turn_vert_len), + pos2(left + ends_turn_radius, to.y - ends_turn_vert_len), + ]; + + let turn_radii = [ + ends_turn_radius, + middle_turn_radius, + middle_turn_radius, + ends_turn_radius, + ]; AxisAlignedWire { - points: [from, from_2nd, mid, to_2nd, to], - turns: [ - (from_turn, from_turn_radius), - (from_2nd_turn, from_2nd_turn_radius), - (to_2nd_turn, to_2nd_turn_radius), - (to_turn, to_turn_radius), - ], + aabb: Rect::from_min_max( + pos2(f32::min(left, from.x), f32::min(from.y, to.y)), + pos2(f32::max(right, to.x), f32::max(from.y, to.y)), + ), + turns: 4, + segments, + turn_centers, + turn_radii, } } } @@ -655,45 +683,43 @@ fn hit_axis_aligned( ) -> bool { let wire = wire_axis_aligned(corner_radius, frame_size, from, to); - let aabb = Rect::from_points(&wire.points); - let aabb_e = aabb.expand(threshold); - if !aabb_e.contains(pos) { + // Check AABB first + if !wire.aabb.expand(threshold).contains(pos) { return false; } // Check all straight segments first - for i in 0..5 { - let start = if i == 0 { - wire.points[0] - } else if i % 2 == 0 { - pos2(wire.turns[i - 1].0.x, wire.points[i].y) - } else { - pos2(wire.points[i].x, wire.turns[i - 1].0.y) - }; - - let end = if i == 4 { - wire.points[4] - } else if i % 2 == 0 { - pos2(wire.turns[i].0.x, wire.points[i].y) - } else { - pos2(wire.points[i].x, wire.turns[i].0.y) - }; - - let aabb = Rect::from_two_pos(start, end); - let aabb_e = aabb.expand(threshold); - if aabb_e.contains(pos) { + // Number of segments is number of turns + 1 + for i in 0..wire.turns + 1 { + let (start, end) = wire.segments[i]; + + // Segments are always axis aligned + // So we can use AABB for checking + if Rect::from_two_pos(start, end) + .expand(threshold) + .contains(pos) + { return true; } } // Check all turns - for i in 0..4 { - let (turn, radius) = wire.turns[i]; - if radius <= 0.0 { - continue; - } - if f32::abs((turn - pos).length() - radius) <= threshold { - return true; + for i in 0..wire.turns { + if wire.turn_radii[i] > 0.0 { + let turn = wire.turn_centers[i]; + let turn_aabb = Rect::from_two_pos(wire.segments[i].1, wire.segments[i + 1].0); + if !turn_aabb.contains(pos) { + continue; + } + + // Avoid sqrt + let dist2 = (turn - pos).length_sq(); + let min = wire.turn_radii[i] - threshold; + let max = wire.turn_radii[i] + threshold; + + if dist2 <= max * max && dist2 >= min * min { + return true; + } } } @@ -727,39 +753,52 @@ fn draw_axis_aligned( let mut path = Vec::new(); - path.push(wire.points[0]); + for i in 0..wire.turns { + // shapes.push(Shape::line_segment( + // [wire.segments[i].0, wire.segments[i].1], + // stroke, + // )); - for i in 0..4 { - let (turn, radius) = wire.turns[i]; - if radius <= 0.0 { - path.push(wire.points[i + 1]); - continue; - } + // Draw segment first + path.push(wire.segments[i].0); + path.push(wire.segments[i].1); - let samples = turn_samples_number(radius, stroke.width); + if wire.turn_radii[i] > 0.0 { + let turn = wire.turn_centers[i]; + let samples = turn_samples_number(wire.turn_radii[i], stroke.width); - for j in 1..samples { - #[allow(clippy::cast_precision_loss)] - let a = std::f32::consts::FRAC_PI_2 * (j as f32 / samples as f32); + let start = wire.segments[i].1; + let end = wire.segments[i + 1].0; - let (sin_a, cos_a) = a.sin_cos(); + let sin_x = end.x - turn.x; + let cos_x = start.x - turn.x; - if i % 2 == 0 { - path.push(pos2( - turn.x.mul_add(1.0 - sin_a, wire.points[i + 1].x * sin_a), - wire.points[i].y.mul_add(cos_a, turn.y * (1.0 - cos_a)), - )); - } else { - path.push(pos2( - wire.points[i].x.mul_add(cos_a, turn.x * (1.0 - cos_a)), - turn.y.mul_add(1.0 - sin_a, wire.points[i + 1].y * sin_a), - )); + let sin_y = end.y - turn.y; + let cos_y = start.y - turn.y; + + for j in 1..samples { + #[allow(clippy::cast_precision_loss)] + let a = std::f32::consts::FRAC_PI_2 * (j as f32 / samples as f32); + + let (sin_a, cos_a) = a.sin_cos(); + + let point: Pos2 = pos2( + turn.x + sin_x * sin_a + cos_x * cos_a, + turn.y + sin_y * sin_a + cos_y * cos_a, + ); + path.push(point); } } - - path.push(wire.points[i + 1]); } + // shapes.push(Shape::line_segment( + // [wire.segments[wire.turns].0, wire.segments[wire.turns].1], + // stroke, + // )); + + path.push(wire.segments[wire.turns].0); + path.push(wire.segments[wire.turns].1); + let shape = Shape::Path(PathShape { points: path, closed: false, diff --git a/src/ui/zoom.rs b/src/ui/zoom.rs index 62bb6e6..ce795cd 100644 --- a/src/ui/zoom.rs +++ b/src/ui/zoom.rs @@ -206,7 +206,7 @@ impl Zoom for WireStyle { #[inline(always)] fn zoom(&mut self, zoom: f32) { match self { - WireStyle::Bezier3 | WireStyle::Bezier5 => {} + WireStyle::Line | WireStyle::Bezier3 | WireStyle::Bezier5 => {} WireStyle::AxisAligned { corner_radius } => { corner_radius.zoom(zoom); }