From ffd04c19057178a4b33efa450c9f614ae80f9fea Mon Sep 17 00:00:00 2001 From: jess Date: Wed, 13 May 2026 23:42:18 -0700 Subject: [PATCH] work on sims and and button functions, added some more examples too --- crates/femm-app/src/doc_canvas.rs | 76 ++++++++++- crates/femm-app/src/main.rs | 206 +++++++++++++++++++++++------- examples/guitar_strings.fem | 86 +++++++------ examples/magnetized_string.fem | 127 ++++++++++++++++++ examples/solenoid_string.fem | 144 +++++++++++++++++++++ examples/tube.fem | 146 +++++++++++++++++++++ 6 files changed, 697 insertions(+), 88 deletions(-) create mode 100644 examples/magnetized_string.fem create mode 100644 examples/solenoid_string.fem create mode 100644 examples/tube.fem diff --git a/crates/femm-app/src/doc_canvas.rs b/crates/femm-app/src/doc_canvas.rs index 0f45046..8af57f7 100644 --- a/crates/femm-app/src/doc_canvas.rs +++ b/crates/femm-app/src/doc_canvas.rs @@ -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, 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 for DocCanvas<'a> { @@ -163,7 +172,7 @@ impl<'a> canvas::Program 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 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 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 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; diff --git a/crates/femm-app/src/main.rs b/crates/femm-app/src/main.rs index ce67648..29e52ce 100644 --- a/crates/femm-app/src/main.rs +++ b/crates/femm-app/src/main.rs @@ -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 = self.doc.nodes.iter().enumerate() - .filter_map(|(i, n)| if n.selected { Some(i) } else { None }) + let sel_segs: Vec = 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 = 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 = selected.clone(); - let mut to_subdivide = seg_indices; + let mut endpoint_nodes: Vec = 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 = 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) -> 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) -> Element<'a, Message> { let handle = svg::Handle::from_memory(svg_bytes); diff --git a/examples/guitar_strings.fem b/examples/guitar_strings.fem index 89345d5..f610d64 100644 --- a/examples/guitar_strings.fem +++ b/examples/guitar_strings.fem @@ -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 @@ -83,85 +83,97 @@ [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 diff --git a/examples/magnetized_string.fem b/examples/magnetized_string.fem new file mode 100644 index 0000000..42db87e --- /dev/null +++ b/examples/magnetized_string.fem @@ -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 + + = "A=0" + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + +[BlockProps] = 3 + + = "Air" + = 1 + = 1 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 1 + = 0 + = 0 + = 0 + + + = "NdFeB" + = 1.05 + = 1.05 + = 915000 + = 0 + = 0 + = 0 + = 0.667 + = 0 + = 0 + = 0 + = 0 + = 0 + = 1 + = 0 + = 0 + = 0 + + + = "Steel" + = 2500 + = 2500 + = 0 + = 0 + = 0 + = 0 + = 5.8 + = 0 + = 0 + = 0 + = 0 + = 0 + = 1 + = 0 + = 0 + = 0 + +[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 diff --git a/examples/solenoid_string.fem b/examples/solenoid_string.fem new file mode 100644 index 0000000..9afdc6f --- /dev/null +++ b/examples/solenoid_string.fem @@ -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 + + = "A=0" + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + +[BlockProps] = 4 + + = "Air" + = 1 + = 1 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 1 + = 0 + = 0 + = 0 + + + = "Steel" + = 2500 + = 2500 + = 0 + = 0 + = 0 + = 0 + = 5.8 + = 0 + = 0 + = 0 + = 0 + = 0 + = 1 + = 0 + = 0 + = 0 + + + = "J+" + = 1 + = 1 + = 0 + = 0 + = 50 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 1 + = 0 + = 0 + = 0 + + + = "J-" + = 1 + = 1 + = 0 + = 0 + = -50 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 1 + = 0 + = 0 + = 0 + +[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 diff --git a/examples/tube.fem b/examples/tube.fem new file mode 100644 index 0000000..f77588e --- /dev/null +++ b/examples/tube.fem @@ -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 + + = "A=0" + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + +[BlockProps] = 4 + + = "Air" + = 1 + = 1 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 0 + = 1 + = 0 + = 0 + = 0 + + + = "Iron" + = 1000 + = 1000 + = 0 + = 0 + = 0 + = 0 + = 10 + = 0 + = 0 + = 0 + = 0 + = 0 + = 1 + = 0 + = 0 + = 0 + + + = "Copper" + = 1 + = 1 + = 0 + = 0 + = 0 + = 0 + = 58 + = 0 + = 0 + = 0 + = 0 + = 0 + = 1 + = 0 + = 0 + = 0 + + + = "M-19 Steel" + = 4416 + = 4416 + = 0 + = 0 + = 0 + = 0 + = 3 + = 0 + = 0 + = 0 + = 0 + = 0 + = 1 + = 0 + = 0 + = 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 + +[CircuitProps] = 2 + + = "Current" + = 100 + = 0 + = 0 + + + = "Zero Net Current" + = 0 + = 0 + = 0 + +[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