work on sims and and button functions, added some more examples too

This commit is contained in:
jess 2026-05-13 23:42:18 -07:00
parent 749a9d3e43
commit ffd04c1905
6 changed files with 697 additions and 88 deletions

View File

@ -83,10 +83,14 @@ pub enum CanvasMessage {
DeleteSelected,
/// drag delta in canvas pixels accumulating into app-side pan.
PanBy { dx: f32, dy: f32 },
/// wheel-zoom factor focused on a canvas-pixel point.
ZoomAt { factor: f32, focus: Point },
/// wheel-zoom factor centred on a cursor offset measured from canvas centre.
ZoomAt { factor: f32, focus_rel: Point },
/// resets pan and zoom to the natural fit.
ResetView,
/// canvas reports its current pixel bounds for app-side view math.
ViewportSize { width: f32, height: f32 },
/// canvas-computed view replacement, used by zoom-window and other bounds-aware operations.
SetView { pan: Vector, zoom: f32 },
}
/// pan offset and zoom factor owned by the app shell.
@ -107,6 +111,7 @@ pub struct CanvasState {
cursor_world: Option<(f64, f64)>,
cursor_canvas: Option<Point>,
modifiers: iced::keyboard::Modifiers,
last_reported_size: Option<(f32, f32)>,
}
/// constructs the canvas widget for a doc reference, optional mesh overlay, and optional solution.
@ -118,8 +123,11 @@ pub fn view<'a>(
render_mode: RenderMode,
view_state: ViewState,
show_grid: bool,
zoom_window_active: bool,
) -> Element<'a, CanvasMessage> {
Canvas::new(DocCanvas { doc, tool, mesh, solution, render_mode, view_state, show_grid })
Canvas::new(DocCanvas {
doc, tool, mesh, solution, render_mode, view_state, show_grid, zoom_window_active,
})
.width(Length::Fill)
.height(Length::Fill)
.into()
@ -133,6 +141,7 @@ struct DocCanvas<'a> {
render_mode: RenderMode,
view_state: ViewState,
show_grid: bool,
zoom_window_active: bool,
}
impl<'a> canvas::Program<CanvasMessage> for DocCanvas<'a> {
@ -163,7 +172,7 @@ impl<'a> canvas::Program<CanvasMessage> for DocCanvas<'a> {
if let Some(p) = cursor.position_in(bounds) {
state.press_origin = Some(p);
state.dragged = false;
if self.tool == Tool::Select {
if self.tool == Tool::Select || self.zoom_window_active {
state.marquee_from = Some(p);
}
return Some(Action::capture());
@ -175,6 +184,22 @@ impl<'a> canvas::Program<CanvasMessage> for DocCanvas<'a> {
let was_dragged = std::mem::take(&mut state.dragged);
let now_opt = cursor.position_in(bounds);
if self.zoom_window_active {
if let (Some(start), Some(now)) = (marquee_start, now_opt) {
if was_dragged {
let view = ViewTransform::fit(self.doc, bounds, &self.view_state);
let p0 = view.inverse_map(start);
let p1 = view.inverse_map(now);
let (xmin, xmax) = (p0.0.min(p1.0), p0.0.max(p1.0));
let (ymin, ymax) = (p0.1.min(p1.1), p0.1.max(p1.1));
if let Some((pan, zoom)) = zoom_window_view(self.doc, bounds, xmin, xmax, ymin, ymax) {
return Some(Action::publish(CanvasMessage::SetView { pan, zoom }).and_capture());
}
}
}
return Some(Action::capture());
}
if self.tool == Tool::Select {
if let (Some(start), Some(now)) = (marquee_start, now_opt) {
let view = ViewTransform::fit(self.doc, bounds, &self.view_state);
@ -225,6 +250,13 @@ impl<'a> canvas::Program<CanvasMessage> for DocCanvas<'a> {
}
}
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
let current_size = (bounds.width, bounds.height);
if state.last_reported_size != Some(current_size) {
state.last_reported_size = Some(current_size);
return Some(Action::publish(CanvasMessage::ViewportSize {
width: bounds.width, height: bounds.height,
}));
}
if let Some(now) = cursor.position_in(bounds) {
let view = ViewTransform::fit(self.doc, bounds, &self.view_state);
state.cursor_world = Some(view.inverse_map(now));
@ -265,7 +297,11 @@ impl<'a> canvas::Program<CanvasMessage> for DocCanvas<'a> {
mouse::ScrollDelta::Pixels { y, .. } => *y / 40.0,
};
let factor = ZOOM_STEP.powf(lines);
return Some(Action::publish(CanvasMessage::ZoomAt { factor, focus }).and_capture());
let focus_rel = Point::new(
focus.x - bounds.width * 0.5,
focus.y - bounds.height * 0.5,
);
return Some(Action::publish(CanvasMessage::ZoomAt { factor, focus_rel }).and_capture());
}
}
Event::Keyboard(iced::keyboard::Event::KeyPressed { key, .. }) => {
@ -567,6 +603,36 @@ impl ViewTransform {
}
}
/// computes the pan and zoom values that frame a world rectangle inside the canvas with default padding.
pub fn zoom_window_view(
doc: &FemmDoc,
bounds: Rectangle,
xmin: f64, xmax: f64, ymin: f64, ymax: f64,
) -> Option<(Vector, f32)> {
let (dxmin, dxmax, dymin, dymax) = doc_bounds(doc);
let dx = (dxmax - dxmin).max(1e-9);
let dy = (dymax - dymin).max(1e-9);
let avail_w = (bounds.width as f64 - 2.0 * PADDING_PX as f64).max(1.0);
let avail_h = (bounds.height as f64 - 2.0 * PADDING_PX as f64).max(1.0);
let base_scale = (avail_w / dx).min(avail_h / dy);
let rw = (xmax - xmin).max(1e-9);
let rh = (ymax - ymin).max(1e-9);
if !rw.is_finite() || !rh.is_finite() { return None; }
let target_scale = (avail_w / rw).min(avail_h / rh);
let zoom = (target_scale / base_scale) as f32;
let zoom = zoom.clamp(ZOOM_MIN, ZOOM_MAX);
let doc_cx = (dxmin + dxmax) * 0.5;
let doc_cy = (dymin + dymax) * 0.5;
let r_cx = (xmin + xmax) * 0.5;
let r_cy = (ymin + ymax) * 0.5;
let scale_effective = base_scale * (zoom as f64);
let pan_x = (doc_cx - r_cx) * scale_effective;
let pan_y = (r_cy - doc_cy) * scale_effective;
Some((Vector::new(pan_x as f32, pan_y as f32), zoom))
}
fn doc_bounds(doc: &FemmDoc) -> (f64, f64, f64, f64) {
let mut xmin = f64::INFINITY;
let mut xmax = f64::NEG_INFINITY;

View File

@ -9,7 +9,7 @@ use femm_doc_mag::{ArcSegment, BlockLabel, FemmDoc, Node, Segment};
use femm_doc_mag::ans::MagSolution;
use femm_doc_mag::mesh::Mesh;
use iced::widget::{button, column, container, row, scrollable, svg, text, text_input, tooltip};
use iced::{Alignment, Background, Border, Color, Element, Length, Point, Subscription, Task, Theme, Vector, clipboard, time};
use iced::{Alignment, Background, Border, Color, Element, Length, Point, Rectangle, Subscription, Task, Theme, Vector, clipboard, time};
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::path::{Path, PathBuf};
use std::time::Duration;
@ -51,6 +51,8 @@ enum Message {
ZoomIn,
ZoomOut,
ZoomFit,
ZoomWindowToggle,
ZoomSelection,
ToggleGrid,
ToggleSnap,
SelectTool(Tool),
@ -117,6 +119,8 @@ struct App {
view_state: ViewState,
show_grid: bool,
snap_to_grid: bool,
zoom_window_active: bool,
canvas_size: Option<(f32, f32)>,
}
impl App {
@ -136,6 +140,8 @@ impl App {
view_state: ViewState::default(),
show_grid: false,
snap_to_grid: false,
zoom_window_active: false,
canvas_size: None,
};
(app, Task::none())
}
@ -144,6 +150,16 @@ impl App {
format!("femm42 - {}", self.source_label)
}
/// applies a zoom multiplier around a focus offset measured from canvas centre, adjusting pan to keep that world point fixed.
fn apply_zoom_factor(&mut self, factor: f32, focus_rel: Point) {
let prev = if self.view_state.zoom <= 0.0 { 1.0 } else { self.view_state.zoom };
let next = (prev * factor).clamp(doc_canvas::ZOOM_MIN, doc_canvas::ZOOM_MAX);
let real = next / prev;
self.view_state.pan.x = focus_rel.x * (1.0 - real) + real * self.view_state.pan.x;
self.view_state.pan.y = focus_rel.y * (1.0 - real) + real * self.view_state.pan.y;
self.view_state.zoom = next;
}
/// rounds a click world coordinate to the 1-cm grid step under snap-to-grid mode.
fn maybe_snap(&self, w: (f64, f64)) -> (f64, f64) {
if self.snap_to_grid {
@ -238,16 +254,44 @@ impl App {
self.status = format!("duplicated {added} entities, offset +10mm");
}
}
Message::ZoomIn => {
let z = if self.view_state.zoom <= 0.0 { 1.0 } else { self.view_state.zoom };
self.view_state.zoom = (z * 1.25).clamp(doc_canvas::ZOOM_MIN, doc_canvas::ZOOM_MAX);
}
Message::ZoomOut => {
let z = if self.view_state.zoom <= 0.0 { 1.0 } else { self.view_state.zoom };
self.view_state.zoom = (z * 0.8).clamp(doc_canvas::ZOOM_MIN, doc_canvas::ZOOM_MAX);
}
Message::ZoomIn => self.apply_zoom_factor(1.25, Point::ORIGIN),
Message::ZoomOut => self.apply_zoom_factor(0.80, Point::ORIGIN),
Message::ZoomFit => {
self.view_state = ViewState::default();
self.zoom_window_active = false;
}
Message::ZoomWindowToggle => {
self.zoom_window_active = !self.zoom_window_active;
self.status = if self.zoom_window_active {
String::from("zoom-window mode: drag a rectangle on the canvas")
} else {
String::from("zoom-window cancelled")
};
}
Message::ZoomSelection => {
let Some((cw, ch)) = self.canvas_size else {
self.error = Some(ErrorReport {
title: String::from("canvas size unknown"),
body: String::from("move the cursor over the canvas once, then try Zoom Selection again."),
});
return Task::none();
};
match selection_bbox(&self.doc) {
Some((xmin, xmax, ymin, ymax)) => {
let rect = Rectangle { x: 0.0, y: 0.0, width: cw, height: ch };
if let Some((pan, zoom)) = doc_canvas::zoom_window_view(&self.doc, rect, xmin, xmax, ymin, ymax) {
self.view_state.pan = pan;
self.view_state.zoom = zoom;
self.status = format!("zoomed to selection bbox: x [{xmin:.3}, {xmax:.3}], y [{ymin:.3}, {ymax:.3}]");
}
}
None => {
self.error = Some(ErrorReport {
title: String::from("nothing selected"),
body: String::from("select at least one node, segment, arc, or label, then click Zoom Selection."),
});
}
}
}
Message::ToggleGrid => {
self.show_grid = !self.show_grid;
@ -379,16 +423,20 @@ impl App {
Message::Canvas(CanvasMessage::PanBy { dx, dy }) => {
self.view_state.pan = self.view_state.pan + Vector::new(dx, dy);
}
Message::Canvas(CanvasMessage::ZoomAt { factor, focus }) => {
let prev = if self.view_state.zoom <= 0.0 { 1.0 } else { self.view_state.zoom };
let next = (prev * factor).clamp(doc_canvas::ZOOM_MIN, doc_canvas::ZOOM_MAX);
let real = next / prev;
self.view_state.pan.x = focus.x - real * (focus.x - self.view_state.pan.x);
self.view_state.pan.y = focus.y - real * (focus.y - self.view_state.pan.y);
self.view_state.zoom = next;
Message::Canvas(CanvasMessage::ZoomAt { factor, focus_rel }) => {
self.apply_zoom_factor(factor, focus_rel);
}
Message::Canvas(CanvasMessage::ResetView) => {
self.view_state = ViewState::default();
self.zoom_window_active = false;
}
Message::Canvas(CanvasMessage::ViewportSize { width, height }) => {
self.canvas_size = Some((width, height));
}
Message::Canvas(CanvasMessage::SetView { pan, zoom }) => {
self.view_state.pan = pan;
self.view_state.zoom = zoom;
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);
@ -402,13 +450,13 @@ impl App {
self.render_mode = mode;
}
Message::SimAddTrackFromSelection => {
let selected: Vec<usize> = self.doc.nodes.iter().enumerate()
.filter_map(|(i, n)| if n.selected { Some(i) } else { None })
let sel_segs: Vec<usize> = self.doc.segments.iter().enumerate()
.filter_map(|(i, s)| if s.selected { Some(i) } else { None })
.collect();
if selected.is_empty() {
if sel_segs.is_empty() {
self.error = Some(ErrorReport {
title: String::from("no nodes selected"),
body: String::from("select at least 3 nodes: two anchors and one or more members the track will displace."),
title: String::from("no segments selected"),
body: String::from("select at least one segment (left-click or marquee). the track moves every node along selected segments between their leftmost and rightmost endpoints."),
});
} else {
let subdivisions = self.simulation.as_ref()
@ -417,17 +465,14 @@ impl App {
let expr_text = self.simulation.as_ref()
.map(|s| s.expression_text.clone())
.unwrap_or_else(|| String::from(SIM_DEFAULT_EXPRESSION));
// subdivides every selected segment with both endpoints selected, producing interior nodes for chord displacement.
let seg_indices: Vec<usize> = self.doc.segments.iter().enumerate()
.filter_map(|(i, s)| {
if self.doc.nodes.get(s.n0 as usize).map(|n| n.selected).unwrap_or(false)
&& self.doc.nodes.get(s.n1 as usize).map(|n| n.selected).unwrap_or(false) {
Some(i)
} else { None }
})
.collect();
let mut all_members: Vec<usize> = selected.clone();
let mut to_subdivide = seg_indices;
let mut endpoint_nodes: Vec<usize> = Vec::new();
for &i in &sel_segs {
let seg = &self.doc.segments[i];
if !endpoint_nodes.contains(&(seg.n0 as usize)) { endpoint_nodes.push(seg.n0 as usize); }
if !endpoint_nodes.contains(&(seg.n1 as usize)) { endpoint_nodes.push(seg.n1 as usize); }
}
let mut all_members: Vec<usize> = endpoint_nodes.clone();
let mut to_subdivide = sel_segs;
to_subdivide.sort_by(|a, b| b.cmp(a));
for idx in to_subdivide {
let new_nodes = self.doc.subdivide_segment(idx, subdivisions);
@ -561,13 +606,29 @@ impl App {
}
}
Message::SimToggleRun => {
if let Some(sim) = self.simulation.as_mut() {
sim.running = !sim.running;
self.status = if sim.running {
format!("running: t = {}", spice::format_spice_time(sim.t_now))
} else {
format!("paused: t = {}", spice::format_spice_time(sim.t_now))
};
let Some(sim) = self.simulation.as_mut() else {
self.error = Some(ErrorReport {
title: String::from("no simulation"),
body: String::from("create at least one track before running. select a segment and click \"Track from Selection\"."),
});
return Task::none();
};
if sim.tracks.is_empty() {
self.error = Some(ErrorReport {
title: String::from("no tracks defined"),
body: String::from("the simulation has no tracks to displace. select a segment, click \"Track from Selection\", then Run."),
});
return Task::none();
}
sim.running = !sim.running;
let status = if sim.running {
format!("running: t = {}", spice::format_spice_time(sim.t_now))
} else {
format!("paused: t = {}", spice::format_spice_time(sim.t_now))
};
self.status = status;
if sim.running {
return Task::done(Message::SimTick);
}
}
Message::SimTick => {
@ -735,14 +796,20 @@ impl App {
let grid_tip = if self.show_grid { "Toggle grid (on)" } else { "Toggle grid (off)" };
let snap_tip = if self.snap_to_grid { "Toggle grid snap (on)" } else { "Toggle grid snap (off)" };
let zoom_window_tip = if self.zoom_window_active {
"Zoom to window: drag a rectangle (click again to cancel)"
} else {
"Zoom to window"
};
let view_strip = column![
icon_button(ICON_ZOOM_IN, "Zoom in", Some(Message::ZoomIn)),
icon_button(ICON_ZOOM_OUT, "Zoom out", Some(Message::ZoomOut)),
icon_button(ICON_ZOOM_FIT, "Zoom to fit", Some(Message::ZoomFit)),
icon_button(ICON_ZOOM_WINDOW, "Zoom to window (rubber-band not yet implemented)", None),
strip_icon_button(ICON_ZOOM_IN, "Zoom in (scroll up)", Some(Message::ZoomIn)),
strip_icon_button(ICON_ZOOM_OUT, "Zoom out (scroll down)", Some(Message::ZoomOut)),
strip_icon_button(ICON_ZOOM_FIT, "Zoom to fit", Some(Message::ZoomFit)),
strip_icon_button(ICON_ZOOM_WINDOW, zoom_window_tip, Some(Message::ZoomWindowToggle)),
strip_icon_button(ICON_ZOOM_FIT, "Zoom to selection", Some(Message::ZoomSelection)),
horizontal_separator(),
icon_button(ICON_GRID, grid_tip, Some(Message::ToggleGrid)),
icon_button(ICON_GRID_SNAP, snap_tip, Some(Message::ToggleSnap)),
strip_icon_button(ICON_GRID, grid_tip, Some(Message::ToggleGrid)),
strip_icon_button(ICON_GRID_SNAP, snap_tip, Some(Message::ToggleSnap)),
]
.spacing(4)
.align_x(Alignment::Center);
@ -753,7 +820,7 @@ impl App {
let canvas = doc_canvas::view(
&self.doc, self.tool, self.mesh.as_ref(), active_solution, self.render_mode,
self.view_state, self.show_grid,
self.view_state, self.show_grid, self.zoom_window_active,
).map(Message::Canvas);
let canvas_row = row![canvas, view_strip].spacing(6);
@ -1238,6 +1305,40 @@ fn offset_for_unit(unit: femm_doc_mag::LengthUnit) -> (f64, f64) {
(v, v)
}
/// computes the (xmin, xmax, ymin, ymax) bounding box of every selected entity, or None when nothing is selected.
fn selection_bbox(doc: &FemmDoc) -> Option<(f64, f64, f64, f64)> {
let mut xmin = f64::INFINITY;
let mut xmax = f64::NEG_INFINITY;
let mut ymin = f64::INFINITY;
let mut ymax = f64::NEG_INFINITY;
let mut hit = false;
let mut grow = |x: f64, y: f64| {
if x < xmin { xmin = x; } if x > xmax { xmax = x; }
if y < ymin { ymin = y; } if y > ymax { ymax = y; }
};
for n in &doc.nodes {
if n.selected { grow(n.x, n.y); hit = true; }
}
for s in &doc.segments {
if s.selected {
if let (Some(a), Some(b)) = (doc.nodes.get(s.n0 as usize), doc.nodes.get(s.n1 as usize)) {
grow(a.x, a.y); grow(b.x, b.y); hit = true;
}
}
}
for a in &doc.arcs {
if a.selected {
if let (Some(p0), Some(p1)) = (doc.nodes.get(a.n0 as usize), doc.nodes.get(a.n1 as usize)) {
grow(p0.x, p0.y); grow(p1.x, p1.y); hit = true;
}
}
}
for l in &doc.block_labels {
if l.selected { grow(l.x, l.y); hit = true; }
}
if hit { Some((xmin, xmax, ymin, ymax)) } else { None }
}
#[derive(Clone, Copy)]
enum Kind { Node, Segment, Arc, Label }
@ -1345,6 +1446,19 @@ fn apply_pick_rect(doc: &mut FemmDoc, p0: (f64, f64), p1: (f64, f64), op: PickOp
format!("{verb}: {n_nodes} nodes, {n_segs} segs, {n_arcs} arcs, {n_labs} labels")
}
/// renders an SVG icon button with its tooltip placed to the left, suitable for a vertical right-edge strip.
fn strip_icon_button<'a>(svg_bytes: &'static [u8], tip: &'a str, msg: Option<Message>) -> Element<'a, Message> {
let handle = svg::Handle::from_memory(svg_bytes);
let glyph = svg(handle)
.width(Length::Fixed(ICON_PX))
.height(Length::Fixed(ICON_PX));
let mut btn = button(glyph).padding(4).style(button::secondary);
if let Some(m) = msg {
btn = btn.on_press(m);
}
tooltip(btn, text(tip).size(11), tooltip::Position::Left).into()
}
/// renders an SVG icon inside a square button with a hover tooltip.
fn icon_button<'a>(svg_bytes: &'static [u8], tip: &'a str, msg: Option<Message>) -> Element<'a, Message> {
let handle = svg::Handle::from_memory(svg_bytes);

View File

@ -6,7 +6,7 @@
[LengthUnits] = millimeters
[ProblemType] = planar
[Coordinates] = cartesian
[Comment] = "guitar pickup geometry: 25.5-inch scale, 6 plain steel strings (0.010-0.046 inch, real gauges), NdFeB bar magnets above and below the string plane with opposite faces presented."
[Comment] = "guitar pickup geometry: 25.5-inch scale, 6 plain steel strings at real gauges 0.010-0.046 inch, NdFeB bar magnets at the neck and bridge ends. Both magnets present their N face inward toward the strings so flux is injected into each end of every string and cancels at the midpoint."
[PointProps] = 0
[BdryProps] = 1
<BeginBdry>
@ -83,85 +83,97 @@
<EndBlock>
[CircuitProps] = 0
[NumPoints] = 36
-50 -80 0 0
700 -80 0 0
700 80 0 0
-50 80 0 0
0 30 0 0
647.7 30 0 0
647.7 35 0 0
0 35 0 0
0 -35 0 0
647.7 -35 0 0
647.7 -30 0 0
-100 -100 0 0
750 -100 0 0
750 100 0 0
-100 100 0 0
-30 -30 0 0
-30 30 0 0
0 -30 0 0
0 -26.377 0 0
647.7 -26.377 0 0
647.7 -26.123 0 0
0 -26.123 0 0
0 -15.915 0 0
647.7 -15.915 0 0
647.7 -15.585 0 0
0 -15.585 0 0
0 -5.466 0 0
647.7 -5.466 0 0
647.7 -5.034 0 0
0 -5.034 0 0
0 4.920 0 0
647.7 4.920 0 0
647.7 5.580 0 0
0 5.580 0 0
0 15.293 0 0
647.7 15.293 0 0
647.7 16.207 0 0
0 16.207 0 0
0 25.666 0 0
0 26.834 0 0
0 30 0 0
647.7 -30 0 0
647.7 -26.377 0 0
647.7 -26.123 0 0
647.7 -15.915 0 0
647.7 -15.585 0 0
647.7 -5.466 0 0
647.7 -5.034 0 0
647.7 4.920 0 0
647.7 5.580 0 0
647.7 15.293 0 0
647.7 16.207 0 0
647.7 25.666 0 0
647.7 26.834 0 0
0 26.834 0 0
[NumSegments] = 36
647.7 30 0 0
677.7 -30 0 0
677.7 30 0 0
[NumSegments] = 48
0 1 -1 1 0 0
1 2 -1 1 0 0
2 3 -1 1 0 0
3 0 -1 1 0 0
4 5 -1 0 0 0
5 6 -1 0 0 0
5 19 -1 0 0 0
4 6 -1 0 0 0
6 7 -1 0 0 0
7 4 -1 0 0 0
7 8 -1 0 0 0
8 9 -1 0 0 0
9 10 -1 0 0 0
10 11 -1 0 0 0
11 8 -1 0 0 0
11 12 -1 0 0 0
12 13 -1 0 0 0
13 14 -1 0 0 0
14 15 -1 0 0 0
15 12 -1 0 0 0
15 16 -1 0 0 0
16 17 -1 0 0 0
17 18 -1 0 0 0
18 19 -1 0 0 0
19 16 -1 0 0 0
20 21 -1 0 0 0
21 22 -1 0 0 0
22 23 -1 0 0 0
23 20 -1 0 0 0
23 24 -1 0 0 0
24 25 -1 0 0 0
25 26 -1 0 0 0
26 27 -1 0 0 0
27 24 -1 0 0 0
27 28 -1 0 0 0
28 29 -1 0 0 0
29 30 -1 0 0 0
30 31 -1 0 0 0
31 28 -1 0 0 0
31 32 -1 0 0 0
32 33 -1 0 0 0
33 34 -1 0 0 0
20 34 -1 0 0 0
34 35 -1 0 0 0
35 32 -1 0 0 0
33 35 -1 0 0 0
7 21 -1 0 0 0
8 22 -1 0 0 0
9 23 -1 0 0 0
10 24 -1 0 0 0
11 25 -1 0 0 0
12 26 -1 0 0 0
13 27 -1 0 0 0
14 28 -1 0 0 0
15 29 -1 0 0 0
16 30 -1 0 0 0
17 31 -1 0 0 0
18 32 -1 0 0 0
[NumArcSegments] = 0
[NumHoles] = 0
[NumBlockLabels] = 9
325 70 1 -1 0 0 0 1 2
325 32.5 2 5 0 270 0 1 0
325 -32.5 2 5 0 270 0 1 0
325 75 1 -1 0 0 0 1 2
-15 0 2 5 0 0 0 1 0
662.7 0 2 5 0 180 0 1 0
325 -26.25 3 1 0 0 0 1 0
325 -15.75 3 1 0 0 0 1 0
325 -5.25 3 1 0 0 0 1 0

View File

@ -0,0 +1,127 @@
[Format] = 4.0
[Frequency] = 0
[Precision] = 1e-008
[MinAngle] = 25
[Depth] = 5
[LengthUnits] = millimeters
[ProblemType] = planar
[Coordinates] = cartesian
[Comment] = "single low-E steel string (1.168 mm gauge, 25.5-inch scale). NdFeB bar magnets touch each end of the string with N faces facing inward. Flux is injected from both ends, the two flows cancel at the midpoint, producing a null point in the centre of the rod."
[PointProps] = 0
[BdryProps] = 1
<BeginBdry>
<BdryName> = "A=0"
<BdryType> = 0
<A_0> = 0
<A_1> = 0
<A_2> = 0
<Phi> = 0
<c0> = 0
<c0i> = 0
<c1> = 0
<c1i> = 0
<Mu_ssd> = 0
<Sigma_ssd> = 0
<EndBdry>
[BlockProps] = 3
<BeginBlock>
<BlockName> = "Air"
<Mu_x> = 1
<Mu_y> = 1
<H_c> = 0
<H_cAngle> = 0
<J_re> = 0
<J_im> = 0
<Sigma> = 0
<d_lam> = 0
<Phi_h> = 0
<Phi_hx> = 0
<Phi_hy> = 0
<LamType> = 0
<LamFill> = 1
<NStrands> = 0
<WireD> = 0
<BHPoints> = 0
<EndBlock>
<BeginBlock>
<BlockName> = "NdFeB"
<Mu_x> = 1.05
<Mu_y> = 1.05
<H_c> = 915000
<H_cAngle> = 0
<J_re> = 0
<J_im> = 0
<Sigma> = 0.667
<d_lam> = 0
<Phi_h> = 0
<Phi_hx> = 0
<Phi_hy> = 0
<LamType> = 0
<LamFill> = 1
<NStrands> = 0
<WireD> = 0
<BHPoints> = 0
<EndBlock>
<BeginBlock>
<BlockName> = "Steel"
<Mu_x> = 2500
<Mu_y> = 2500
<H_c> = 0
<H_cAngle> = 0
<J_re> = 0
<J_im> = 0
<Sigma> = 5.8
<d_lam> = 0
<Phi_h> = 0
<Phi_hx> = 0
<Phi_hy> = 0
<LamType> = 0
<LamFill> = 1
<NStrands> = 0
<WireD> = 0
<BHPoints> = 0
<EndBlock>
[CircuitProps] = 0
[NumPoints] = 16
-100 -30 0 0
750 -30 0 0
750 30 0 0
-100 30 0 0
-30 -5 0 0
-30 5 0 0
0 -5 0 0
0 -0.584 0 0
0 0.584 0 0
0 5 0 0
647.7 -5 0 0
647.7 -0.584 0 0
647.7 0.584 0 0
647.7 5 0 0
677.7 5 0 0
677.7 -5 0 0
[NumSegments] = 18
0 1 -1 1 0 0
1 2 -1 1 0 0
2 3 -1 1 0 0
3 0 -1 1 0 0
4 5 -1 0 0 0
5 9 -1 0 0 0
4 6 -1 0 0 0
6 7 -1 0 0 0
7 8 -1 0 0 0
8 9 -1 0 0 0
10 11 -1 0 0 0
11 12 -1 0 0 0
12 13 -1 0 0 0
13 14 -1 0 0 0
14 15 -1 0 0 0
15 10 -1 0 0 0
7 11 -1 0 0 0
8 12 -1 0 0 0
[NumArcSegments] = 0
[NumHoles] = 0
[NumBlockLabels] = 4
325 20 1 -1 0 0 0 1 2
-15 0 2 5 0 0 0 1 0
662.7 0 2 5 0 180 0 1 0
325 0 3 1 0 0 0 1 0

View File

@ -0,0 +1,144 @@
[Format] = 4.0
[Frequency] = 0
[Precision] = 1e-008
[MinAngle] = 25
[Depth] = 5
[LengthUnits] = millimeters
[ProblemType] = planar
[Coordinates] = cartesian
[Comment] = "single 18 AWG steel wire (1.024 mm diameter) acting as a solenoid core, wrapped above and below by counter-circulating coil regions at an exaggerated current density chosen to produce a polarising field on the order of NdFeB remanence."
[PointProps] = 0
[BdryProps] = 1
<BeginBdry>
<BdryName> = "A=0"
<BdryType> = 0
<A_0> = 0
<A_1> = 0
<A_2> = 0
<Phi> = 0
<c0> = 0
<c0i> = 0
<c1> = 0
<c1i> = 0
<Mu_ssd> = 0
<Sigma_ssd> = 0
<EndBdry>
[BlockProps] = 4
<BeginBlock>
<BlockName> = "Air"
<Mu_x> = 1
<Mu_y> = 1
<H_c> = 0
<H_cAngle> = 0
<J_re> = 0
<J_im> = 0
<Sigma> = 0
<d_lam> = 0
<Phi_h> = 0
<Phi_hx> = 0
<Phi_hy> = 0
<LamType> = 0
<LamFill> = 1
<NStrands> = 0
<WireD> = 0
<BHPoints> = 0
<EndBlock>
<BeginBlock>
<BlockName> = "Steel"
<Mu_x> = 2500
<Mu_y> = 2500
<H_c> = 0
<H_cAngle> = 0
<J_re> = 0
<J_im> = 0
<Sigma> = 5.8
<d_lam> = 0
<Phi_h> = 0
<Phi_hx> = 0
<Phi_hy> = 0
<LamType> = 0
<LamFill> = 1
<NStrands> = 0
<WireD> = 0
<BHPoints> = 0
<EndBlock>
<BeginBlock>
<BlockName> = "J+"
<Mu_x> = 1
<Mu_y> = 1
<H_c> = 0
<H_cAngle> = 0
<J_re> = 50
<J_im> = 0
<Sigma> = 0
<d_lam> = 0
<Phi_h> = 0
<Phi_hx> = 0
<Phi_hy> = 0
<LamType> = 0
<LamFill> = 1
<NStrands> = 0
<WireD> = 0
<BHPoints> = 0
<EndBlock>
<BeginBlock>
<BlockName> = "J-"
<Mu_x> = 1
<Mu_y> = 1
<H_c> = 0
<H_cAngle> = 0
<J_re> = -50
<J_im> = 0
<Sigma> = 0
<d_lam> = 0
<Phi_h> = 0
<Phi_hx> = 0
<Phi_hy> = 0
<LamType> = 0
<LamFill> = 1
<NStrands> = 0
<WireD> = 0
<BHPoints> = 0
<EndBlock>
[CircuitProps] = 0
[NumPoints] = 16
-130 -20 0 0
130 -20 0 0
130 20 0 0
-130 20 0 0
-100 -0.512 0 0
100 -0.512 0 0
100 0.512 0 0
-100 0.512 0 0
-100 1 0 0
100 1 0 0
100 6 0 0
-100 6 0 0
-100 -6 0 0
100 -6 0 0
100 -1 0 0
-100 -1 0 0
[NumSegments] = 16
0 1 -1 1 0 0
1 2 -1 1 0 0
2 3 -1 1 0 0
3 0 -1 1 0 0
4 5 -1 0 0 0
5 6 -1 0 0 0
6 7 -1 0 0 0
7 4 -1 0 0 0
8 9 -1 0 0 0
9 10 -1 0 0 0
10 11 -1 0 0 0
11 8 -1 0 0 0
12 13 -1 0 0 0
13 14 -1 0 0 0
14 15 -1 0 0 0
15 12 -1 0 0 0
[NumArcSegments] = 0
[NumHoles] = 0
[NumBlockLabels] = 4
0 15 1 -1 0 0 0 1 2
0 0 2 0.5 0 0 0 1 0
0 3.5 3 2 0 0 0 1 0
0 -3.5 4 2 0 0 0 1 0

146
examples/tube.fem Normal file
View File

@ -0,0 +1,146 @@
[Format] = 4.0
[Frequency] = 1
[Precision] = 1e-008
[MinAngle] = 30
[Depth] = 39.370078740157481
[LengthUnits] = inches
[ProblemType] = planar
[Coordinates] = cartesian
[Comment] = "Add comments here."
[PointProps] = 0
[BdryProps] = 1
<BeginBdry>
<BdryName> = "A=0"
<BdryType> = 0
<A_0> = 0
<A_1> = 0
<A_2> = 0
<Phi> = 0
<c0> = 0
<c1> = 0
<Mu_ssd> = 0
<Sigma_ssd> = 0
<EndBdry>
[BlockProps] = 4
<BeginBlock>
<BlockName> = "Air"
<Mu_x> = 1
<Mu_y> = 1
<H_c> = 0
<H_cAngle> = 0
<J_re> = 0
<J_im> = 0
<Sigma> = 0
<d_lam> = 0
<Phi_h> = 0
<Phi_hx> = 0
<Phi_hy> = 0
<LamType> = 0
<LamFill> = 1
<NStrands> = 0
<WireD> = 0
<BHPoints> = 0
<EndBlock>
<BeginBlock>
<BlockName> = "Iron"
<Mu_x> = 1000
<Mu_y> = 1000
<H_c> = 0
<H_cAngle> = 0
<J_re> = 0
<J_im> = 0
<Sigma> = 10
<d_lam> = 0
<Phi_h> = 0
<Phi_hx> = 0
<Phi_hy> = 0
<LamType> = 0
<LamFill> = 1
<NStrands> = 0
<WireD> = 0
<BHPoints> = 0
<EndBlock>
<BeginBlock>
<BlockName> = "Copper"
<Mu_x> = 1
<Mu_y> = 1
<H_c> = 0
<H_cAngle> = 0
<J_re> = 0
<J_im> = 0
<Sigma> = 58
<d_lam> = 0
<Phi_h> = 0
<Phi_hx> = 0
<Phi_hy> = 0
<LamType> = 0
<LamFill> = 1
<NStrands> = 0
<WireD> = 0
<BHPoints> = 0
<EndBlock>
<BeginBlock>
<BlockName> = "M-19 Steel"
<Mu_x> = 4416
<Mu_y> = 4416
<H_c> = 0
<H_cAngle> = 0
<J_re> = 0
<J_im> = 0
<Sigma> = 3
<d_lam> = 0
<Phi_h> = 0
<Phi_hx> = 0
<Phi_hy> = 0
<LamType> = 0
<LamFill> = 1
<NStrands> = 0
<WireD> = 0
<BHPoints> = 13
0 0
0.29999999999999999 39.78875
0.80000000000000004 79.577500000000001
1.1200000000000001 159.155
1.3200000000000001 318.31
1.46 795.77499999999998
1.54 1591.55
1.6187499999999999 3376.6669999999999
1.74 7957.75
1.8700000000000001 15915.5
1.99 31831
2.0459640000000001 55102.040000000001
2.0800000000000001 79577.5
<EndBlock>
[CircuitProps] = 2
<BeginCircuit>
<CircuitName> = "Current"
<TotalAmps_re> = 100
<TotalAmps_im> = 0
<CircuitType> = 0
<EndCircuit>
<BeginCircuit>
<CircuitName> = "Zero Net Current"
<TotalAmps_re> = 0
<TotalAmps_im> = 0
<CircuitType> = 0
<EndCircuit>
[NumPoints] = 6
0.5 0 0 0
1 0 0 0
-0.5 6.1230317691118863e-017 0 0
-1 1.2246063538223773e-016 0 0
0.10000000000000001 0 0 0
-0.10000000000000001 0 0 0
[NumSegments] = 0
[NumArcSegments] = 6
0 2 180 5 0 0 0
1 3 180 5 1 0 0
2 0 180 5 0 0 0
3 1 180 5 1 0 0
4 5 180 10 0 0 0
5 4 180 10 0 0 0
[NumHoles] = 0
[NumBlockLabels] = 3
0 0.75 2 0.025000000000000001 2 0 0 1 0
0 0.34999999999999998 1 0.025000000000000001 0 0 0 1 0
0 0 3 0.025000000000000001 1 0 0 1 0