fixed probe type nodes (made them selectable and movable)

fixed node selection in edit mode (can specify node select with alt/option key held in either mode now)
This commit is contained in:
jess 2026-05-15 23:45:21 -07:00
parent a2957d907a
commit 94a930fe3b
4 changed files with 1163 additions and 22 deletions

View File

@ -67,7 +67,7 @@ pub enum PickOp {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PickRestrict {
Any,
NodesOnly,
ProbesOnly,
}
/// messages emitted by the canvas back to the app.
@ -211,7 +211,7 @@ impl<'a> canvas::Program<CanvasMessage> for DocCanvas<'a> {
return Some(Action::publish(CanvasMessage::PickAt {
world, pick_radius_world,
op: PickOp::Replace,
restrict: PickRestrict::Any,
restrict: pick_restrict_from(state.modifiers),
}).and_capture());
}
return Some(Action::capture());
@ -599,6 +599,10 @@ impl<'a> canvas::Program<CanvasMessage> for DocCanvas<'a> {
Stroke::default().with_width(1.0)
.with_color(Color { a: 0.55, ..color }));
frame.fill(&Path::circle(p, 6.0), color);
if probe.selected {
frame.stroke(&Path::circle(p, 8.0),
Stroke::default().with_width(2.4).with_color(SELECT_COLOR));
}
frame.stroke(&Path::circle(p, 6.0),
Stroke::default().with_width(1.5).with_color(Color::WHITE));
frame.fill_text(Text {
@ -757,7 +761,7 @@ fn pick_op_from(m: iced::keyboard::Modifiers) -> PickOp {
/// derives the pick-restrict mode from a modifier snapshot.
fn pick_restrict_from(m: iced::keyboard::Modifiers) -> PickRestrict {
if m.alt() { PickRestrict::NodesOnly } else { PickRestrict::Any }
if m.alt() { PickRestrict::ProbesOnly } else { PickRestrict::Any }
}
fn jet(t: f32) -> Color {

View File

@ -601,6 +601,7 @@ impl App {
mode: probe::ProbeMode::default(),
angle_deg: 0.0,
angle_text: String::from("0"),
selected: false,
};
let mut samples = std::collections::HashMap::new();
for (&fi, sol) in &sim.lut {
@ -649,19 +650,39 @@ impl App {
self.zoom_window_active = false;
}
Message::Canvas(CanvasMessage::PickAt { world, pick_radius_world, op, restrict }) => {
let summary = apply_pick_at(&mut self.doc, world.0, world.1, pick_radius_world, op, restrict);
self.status = summary;
if matches!(restrict, PickRestrict::ProbesOnly) {
let probes = self.simulation.as_mut().map(|s| s.probes.as_mut_slice()).unwrap_or(&mut []);
self.status = apply_probe_pick_at(probes, world.0, world.1, pick_radius_world, op);
} else {
self.status = apply_pick_at(&mut self.doc, world.0, world.1, pick_radius_world, op, restrict);
}
}
Message::Canvas(CanvasMessage::PickRect { p0, p1, op, restrict }) => {
let summary = apply_pick_rect(&mut self.doc, p0, p1, op, restrict);
self.status = summary;
}
Message::Canvas(CanvasMessage::TranslateSelection { dx, dy }) => {
let moved = translate_selection(&mut self.doc, dx, dy);
if moved > 0 {
let moved_geom = translate_selection(&mut self.doc, dx, dy);
if moved_geom > 0 {
self.mesh = None;
self.solution = None;
}
if let Some(sim) = self.simulation.as_mut() {
let lut = &sim.lut;
for (i, p) in sim.probes.iter_mut().enumerate() {
if !p.selected { continue; }
p.x += dx;
p.y += dy;
if let Some(samples) = sim.probe_samples.get_mut(i) {
samples.clear();
for (&fi, sol) in lut {
if let Some(bxby) = sol.sample_b_at(p.x, p.y) {
samples.insert(fi, bxby);
}
}
}
}
}
}
Message::Canvas(CanvasMessage::DoubleClickAt { world, pick_radius_world }) => {
let mut best: Option<(usize, f64)> = None;
@ -2671,16 +2692,23 @@ fn apply_op(flag: &mut bool, op: PickOp) {
}
}
/// applies the pick op to the closest entity within pick_radius_world of (x, y), honouring the restrict mode.
/// applies the pick op to the closest entity within pick_radius_world of (x, y); ties within 5% of the pick radius prefer Label > Node > Arc > Segment so point-entities stay grabbable around line entities at the same spot.
fn apply_pick_at(doc: &mut FemmDoc, x: f64, y: f64, pick_radius_world: f64, op: PickOp, restrict: PickRestrict) -> String {
use femm_doc_mag::geom_math::{shortest_distance_from_arc, shortest_distance_from_segment};
let priority = |k: Kind| match k { Kind::Label => 0, Kind::Node => 1, Kind::Arc => 2, Kind::Segment => 3 };
let eps = pick_radius_world * 0.05;
let mut best: Option<(Kind, usize, f64)> = None;
let mut consider = |kind: Kind, idx: usize, d: f64| {
match best {
None => best = Some((kind, idx, d)),
Some((_, _, bd)) if d < bd => best = Some((kind, idx, d)),
_ => {}
Some((bk, _, bd)) => {
if d < bd - eps {
best = Some((kind, idx, d));
} else if (d - bd).abs() <= eps && priority(kind) < priority(bk) {
best = Some((kind, idx, d));
}
}
}
};
@ -2705,18 +2733,7 @@ fn apply_pick_at(doc: &mut FemmDoc, x: f64, y: f64, pick_radius_world: f64, op:
if matches!(op, PickOp::Replace) { clear_selection(doc); }
let mut best_label: Option<(usize, f64)> = None;
for (i, b) in doc.block_labels.iter().enumerate() {
let d = (b.x - x).hypot(b.y - y);
if d <= pick_radius_world && best_label.map(|(_, bd)| d < bd).unwrap_or(true) {
best_label = Some((i, d));
}
}
let hit = if let Some((i, _)) = best_label {
Some((Kind::Label, i))
} else {
best.and_then(|(k, i, d)| if d <= pick_radius_world { Some((k, i)) } else { None })
};
let hit = best.and_then(|(k, i, d)| if d <= pick_radius_world { Some((k, i)) } else { None });
if let Some((kind, idx)) = hit {
let (label, flag) = match kind {
Kind::Node => ("node", &mut doc.nodes[idx].selected),
@ -2734,6 +2751,33 @@ fn apply_pick_at(doc: &mut FemmDoc, x: f64, y: f64, pick_radius_world: f64, op:
}
}
/// applies the pick op to the closest probe within pick_radius_world of (x, y).
fn apply_probe_pick_at(probes: &mut [probe::Probe], x: f64, y: f64, pick_radius_world: f64, op: PickOp) -> String {
let mut best: Option<(usize, f64)> = None;
for (i, p) in probes.iter().enumerate() {
let d = (p.x - x).hypot(p.y - y);
if d <= pick_radius_world && best.map(|(_, bd)| d < bd).unwrap_or(true) {
best = Some((i, d));
}
}
if matches!(op, PickOp::Replace) {
for p in probes.iter_mut() { p.selected = false; }
}
if let Some((idx, _)) = best {
let flag = &mut probes[idx].selected;
match op {
PickOp::Replace | PickOp::Add => *flag = true,
PickOp::Toggle => *flag ^= true,
}
let verb = match op { PickOp::Replace => "selected", PickOp::Add => "added", PickOp::Toggle => "toggled" };
format!("{verb} probe {idx}")
} else if matches!(op, PickOp::Replace) {
String::from("probe selection cleared")
} else {
String::from("nothing at click")
}
}
/// applies the pick op to every entity sitting inside the rectangle spanned by p0 and p1.
fn apply_pick_rect(doc: &mut FemmDoc, p0: (f64, f64), p1: (f64, f64), op: PickOp, restrict: PickRestrict) -> String {
let xmin = p0.0.min(p1.0); let xmax = p0.0.max(p1.0);

View File

@ -52,6 +52,7 @@ pub struct Probe {
pub mode: ProbeMode,
pub angle_deg: f64,
pub angle_text: String,
pub selected: bool,
}
/// returns a saturated palette color from a small cycling palette suitable for probe markers and matching plot lines.

File diff suppressed because it is too large Load Diff