arc metadata
This commit is contained in:
parent
c3495eee92
commit
07fbaa4165
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue