arc metadata

This commit is contained in:
jess 2026-05-12 22:10:52 -07:00
parent c3495eee92
commit 07fbaa4165
3 changed files with 97 additions and 8 deletions

View File

@ -17,6 +17,7 @@ const CLICK_DRAG_THRESHOLD_PX: f32 = 4.0;
const BG: Color = Color::WHITE;
const GEOM: Color = Color::BLACK;
const LABEL_COLOR: Color = Color::from_rgb(0.25, 0.45, 0.85);
const PENDING_COLOR: Color = Color::from_rgb(0.85, 0.30, 0.30);
/// active editing mode on the canvas.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
@ -25,16 +26,19 @@ pub enum Tool {
Select,
AddNode,
AddBlockLabel,
AddSegment,
}
/// messages emitted by the canvas back to the app.
#[derive(Debug, Clone)]
pub enum CanvasMessage {
/// left-click at the given doc-world coordinate, intent depends on the active tool.
/// click at a doc-world coordinate.
Click { world: (f64, f64), tool: Tool },
/// two-point segment request from the canvas.
SegmentBetween { from: (f64, f64), to: (f64, f64) },
}
/// pan offset and zoom factor applied on top of fit-to-view.
/// pan offset, zoom factor, and click-gesture bookkeeping for the canvas.
#[derive(Debug, Default, Clone, Copy)]
pub struct ViewState {
pan: Vector,
@ -42,6 +46,8 @@ pub struct ViewState {
drag_origin: Option<Point>,
press_origin: Option<Point>,
dragged: bool,
pending_segment_start: Option<(f64, f64)>,
cursor_world: Option<(f64, f64)>,
}
/// constructs the canvas widget for a doc reference.
@ -89,6 +95,15 @@ impl<'a> canvas::Program<CanvasMessage> for DocCanvas<'a> {
{
let view = ViewTransform::fit(self.doc, bounds, state);
let world = view.inverse_map(now);
if self.tool == Tool::AddSegment {
if let Some(from) = state.pending_segment_start.take() {
return Some(Action::publish(
CanvasMessage::SegmentBetween { from, to: world },
));
}
state.pending_segment_start = Some(world);
return Some(Action::request_redraw().and_capture());
}
return Some(Action::publish(CanvasMessage::Click {
world,
tool: self.tool,
@ -101,6 +116,12 @@ impl<'a> canvas::Program<CanvasMessage> for DocCanvas<'a> {
}
}
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
if let Some(now) = cursor.position_in(bounds) {
let view = ViewTransform::fit(self.doc, bounds, state);
state.cursor_world = Some(view.inverse_map(now));
} else {
state.cursor_world = None;
}
if let (Some(prev), Some(now)) = (state.drag_origin, cursor.position_in(bounds)) {
state.pan = state.pan + Vector::new(now.x - prev.x, now.y - prev.y);
state.drag_origin = Some(now);
@ -114,6 +135,9 @@ impl<'a> canvas::Program<CanvasMessage> for DocCanvas<'a> {
state.dragged = true;
}
}
if state.pending_segment_start.is_some() {
return Some(Action::request_redraw());
}
}
Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
if let Some(focus) = cursor.position_in(bounds) {
@ -138,6 +162,11 @@ impl<'a> canvas::Program<CanvasMessage> for DocCanvas<'a> {
return Some(Action::request_redraw());
}
}
if matches!(key, iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape)) {
if state.pending_segment_start.take().is_some() {
return Some(Action::request_redraw());
}
}
}
_ => {}
}
@ -200,6 +229,20 @@ impl<'a> canvas::Program<CanvasMessage> for DocCanvas<'a> {
frame.fill(&Path::circle(p, NODE_RADIUS), GEOM);
}
if let Some(start_world) = state.pending_segment_start {
let sp = view.map(start_world.0, start_world.1);
let ring = Path::new(|b| {
b.circle(sp, NODE_RADIUS + 2.0);
});
frame.stroke(&ring,
Stroke::default().with_width(STROKE_WIDTH).with_color(PENDING_COLOR));
if let Some(cursor_world) = state.cursor_world {
let cp = view.map(cursor_world.0, cursor_world.1);
frame.stroke(&Path::line(sp, cp),
Stroke::default().with_width(STROKE_WIDTH).with_color(PENDING_COLOR));
}
}
for label in &self.doc.block_labels {
let p = view.map(label.x, label.y);
let cross = Path::new(|b| {
@ -234,7 +277,9 @@ impl<'a> canvas::Program<CanvasMessage> for DocCanvas<'a> {
if cursor.position_in(bounds).is_some() {
return match self.tool {
Tool::Select => mouse::Interaction::Grab,
Tool::AddNode | Tool::AddBlockLabel => mouse::Interaction::Crosshair,
Tool::AddNode | Tool::AddBlockLabel | Tool::AddSegment => {
mouse::Interaction::Crosshair
}
};
}
mouse::Interaction::default()

View File

@ -79,7 +79,19 @@ impl App {
let idx = self.doc.add_block_label(world.0, world.1, ADD_TOLERANCE);
self.status = format!("block label {idx} at ({:.3}, {:.3})", world.0, world.1);
}
Tool::Select => {}
Tool::Select | Tool::AddSegment => {}
}
}
Message::Canvas(CanvasMessage::SegmentBetween { from, to }) => {
let n0 = self.doc.add_node(from.0, from.1, ADD_TOLERANCE) as i32;
let n1 = self.doc.add_node(to.0, to.1, ADD_TOLERANCE) as i32;
if self.doc.add_segment(n0, n1) {
self.status = format!(
"segment {n0} -> {n1} ({} total)",
self.doc.segments.len(),
);
} else {
self.status = format!("rejected segment {n0} -> {n1}");
}
}
}
@ -98,9 +110,10 @@ impl App {
let toolbar = row![
button("Open .fem...").on_press(Message::OpenFem),
tool_button("Select", Tool::Select, self.tool),
tool_button("Add Node", Tool::AddNode, self.tool),
tool_button("Add Label", Tool::AddBlockLabel, self.tool),
tool_button("Select", Tool::Select, self.tool),
tool_button("Add Node", Tool::AddNode, self.tool),
tool_button("Add Segment", Tool::AddSegment, self.tool),
tool_button("Add Label", Tool::AddBlockLabel, self.tool),
text(&self.source_label).size(13),
stats,
]

View File

@ -186,7 +186,7 @@ impl FemmDoc {
self.closest_node(p0.0, p0.1),
self.closest_node(p1.0, p1.1),
) else { continue };
self.add_arc_segment(n0 as i32, n1 as i32, a.arc_length);
self.add_arc_segment_with_template(n0 as i32, n1 as i32, a.arc_length, &a);
}
self.block_labels = old_block_labels;
@ -508,6 +508,37 @@ mod tests {
assert_eq!(d.segments[0].boundary_marker, "outer");
}
#[test]
fn enforce_pslg_preserves_arc_metadata() {
// arc carrying non-default marker, side length, group, and direction.
// enforce_pslg round-trips every field intact.
let mut d = FemmDoc::default();
d.add_node(1.0, 0.0, 0.0);
d.add_node(0.0, 1.0, 0.0);
let template = ArcSegment {
n0: 0,
n1: 1,
arc_length: 90.0,
max_side_length: 5.0,
boundary_marker: String::from("outer"),
hidden: true,
in_group: 7,
normal_direction: false,
selected: false,
};
assert!(d.add_arc_segment_with_template(0, 1, 90.0, &template));
d.enforce_pslg();
assert_eq!(d.arcs.len(), 1);
let a = &d.arcs[0];
assert_eq!(a.boundary_marker, "outer");
assert!((a.max_side_length - 5.0).abs() < 1e-12);
assert!(a.hidden);
assert_eq!(a.in_group, 7);
assert!(!a.normal_direction);
}
#[test]
fn duplicate_arc_rejected() {
let mut d = FemmDoc::default();