added plots, fixed sessions for sims
This commit is contained in:
parent
880a37ace7
commit
d5e0bc2ef9
|
|
@ -3,6 +3,7 @@ build/ffi/
|
|||
*.o
|
||||
*.a
|
||||
target/
|
||||
examples/vids/
|
||||
assets/old/
|
||||
assets/icons/
|
||||
Cargo.lock
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ pub enum Tool {
|
|||
AddNode,
|
||||
AddBlockLabel,
|
||||
AddSegment,
|
||||
AddProbe,
|
||||
}
|
||||
|
||||
/// composition mode for a pick action, derived from modifier keys at click/release time.
|
||||
|
|
@ -124,9 +125,10 @@ pub fn view<'a>(
|
|||
view_state: ViewState,
|
||||
show_grid: bool,
|
||||
zoom_window_active: bool,
|
||||
probes: &'a [crate::probe::Probe],
|
||||
) -> Element<'a, CanvasMessage> {
|
||||
Canvas::new(DocCanvas {
|
||||
doc, tool, mesh, solution, render_mode, view_state, show_grid, zoom_window_active,
|
||||
doc, tool, mesh, solution, render_mode, view_state, show_grid, zoom_window_active, probes,
|
||||
})
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
|
|
@ -142,6 +144,7 @@ struct DocCanvas<'a> {
|
|||
view_state: ViewState,
|
||||
show_grid: bool,
|
||||
zoom_window_active: bool,
|
||||
probes: &'a [crate::probe::Probe],
|
||||
}
|
||||
|
||||
impl<'a> canvas::Program<CanvasMessage> for DocCanvas<'a> {
|
||||
|
|
@ -519,6 +522,21 @@ impl<'a> canvas::Program<CanvasMessage> for DocCanvas<'a> {
|
|||
});
|
||||
}
|
||||
|
||||
for probe in self.probes {
|
||||
let p = view.map(probe.x, probe.y);
|
||||
let color = Color::from_rgba8(probe.color[0], probe.color[1], probe.color[2], probe.color[3] as f32 / 255.0);
|
||||
frame.fill(&Path::circle(p, 6.0), color);
|
||||
frame.stroke(&Path::circle(p, 6.0),
|
||||
Stroke::default().with_width(1.5).with_color(Color::WHITE));
|
||||
frame.fill_text(Text {
|
||||
content: probe.label.clone(),
|
||||
position: Point::new(p.x + 9.0, p.y - 9.0),
|
||||
color,
|
||||
size: 11.0.into(),
|
||||
..Text::default()
|
||||
});
|
||||
}
|
||||
|
||||
if let (Some(start), true, Some(now)) =
|
||||
(state.marquee_from, state.dragged, state.cursor_canvas)
|
||||
{
|
||||
|
|
@ -549,7 +567,7 @@ 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 | Tool::AddSegment => {
|
||||
Tool::AddNode | Tool::AddBlockLabel | Tool::AddSegment | Tool::AddProbe => {
|
||||
mouse::Interaction::Crosshair
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ pub struct ExportInput {
|
|||
pub height: u32,
|
||||
pub contour: bool,
|
||||
pub crf: u32,
|
||||
pub probes: Vec<crate::probe::Probe>,
|
||||
pub probe_samples: Vec<HashMap<i64, (f64, f64)>>,
|
||||
pub progress: Arc<Mutex<ExportProgress>>,
|
||||
}
|
||||
|
||||
|
|
@ -86,6 +88,9 @@ pub fn export_webm(input: ExportInput) -> Result<ExportReport, String> {
|
|||
"-pix_fmt", "yuv420p",
|
||||
"-crf", crf_arg.as_str(),
|
||||
"-b:v", "0",
|
||||
"-r", fps_arg.as_str(),
|
||||
"-fps_mode", "cfr",
|
||||
"-vsync", "cfr",
|
||||
"-deadline", "good",
|
||||
"-cpu-used", "2",
|
||||
"-row-mt", "1",
|
||||
|
|
@ -135,7 +140,12 @@ pub fn export_webm(input: ExportInput) -> Result<ExportReport, String> {
|
|||
let t = (f as f64) * input.dt;
|
||||
kinematic::apply_tracks(&mut doc, &input.base_doc, &input.tracks, t);
|
||||
|
||||
let pixmap = render_frame(&doc, sol, input.width, input.height, input.contour, xmin, xmax, ymin, ymax);
|
||||
let pixmap = render_frame(
|
||||
&doc, sol, input.width, input.height, input.contour,
|
||||
xmin, xmax, ymin, ymax,
|
||||
&input.probes, &input.probe_samples,
|
||||
f, input.buffer_size,
|
||||
);
|
||||
let frame_bytes = pixmap.data();
|
||||
if frame_bytes.len() != expected_bytes_per_frame {
|
||||
drop(stdin);
|
||||
|
|
@ -276,10 +286,16 @@ fn render_frame(
|
|||
height: u32,
|
||||
contour: bool,
|
||||
xmin: f64, xmax: f64, ymin: f64, ymax: f64,
|
||||
probes: &[crate::probe::Probe],
|
||||
probe_samples: &[HashMap<i64, (f64, f64)>],
|
||||
current_frame: i64,
|
||||
buffer_size: usize,
|
||||
) -> Pixmap {
|
||||
let mut pixmap = Pixmap::new(width, height).expect("allocate pixmap");
|
||||
pixmap.fill(Color::from_rgba8(20, 20, 24, 255));
|
||||
let view = View::fit(width, height, xmin, xmax, ymin, ymax);
|
||||
let plot_strip_h = if probes.is_empty() { 0 } else { (height as f32 * 0.33) as u32 };
|
||||
let canvas_h = height - plot_strip_h;
|
||||
let view = View::fit(width, canvas_h, xmin, xmax, ymin, ymax);
|
||||
|
||||
let (lo, hi) = sol.b_magnitude_range();
|
||||
let span = if hi > lo { hi - lo } else { 1.0 };
|
||||
|
|
@ -366,9 +382,163 @@ fn render_frame(
|
|||
}
|
||||
}
|
||||
|
||||
for probe in probes {
|
||||
let (px, py) = view.map(probe.x, probe.y);
|
||||
paint.set_color(Color::from_rgba8(probe.color[0], probe.color[1], probe.color[2], 255));
|
||||
let mut pb = PathBuilder::new();
|
||||
pb.push_circle(px, py, 6.0);
|
||||
if let Some(path) = pb.finish() {
|
||||
pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None);
|
||||
}
|
||||
paint.set_color(Color::from_rgba8(255, 255, 255, 255));
|
||||
let ring = Stroke { width: 1.6, ..Default::default() };
|
||||
let mut pb = PathBuilder::new();
|
||||
pb.push_circle(px, py, 6.0);
|
||||
if let Some(path) = pb.finish() {
|
||||
pixmap.stroke_path(&path, &paint, &ring, Transform::identity(), None);
|
||||
}
|
||||
}
|
||||
|
||||
if plot_strip_h > 0 && !probes.is_empty() {
|
||||
draw_plot_strip(
|
||||
&mut pixmap,
|
||||
0, canvas_h, width, plot_strip_h,
|
||||
probes, probe_samples,
|
||||
current_frame, buffer_size,
|
||||
);
|
||||
}
|
||||
|
||||
pixmap
|
||||
}
|
||||
|
||||
/// draws one plot pane per probe across a horizontal strip at (x0, y0, w, h), each pane color-matched to its probe and marked with a vertical bar at the current frame.
|
||||
fn draw_plot_strip(
|
||||
pixmap: &mut Pixmap,
|
||||
x0: u32, y0: u32, w: u32, h: u32,
|
||||
probes: &[crate::probe::Probe],
|
||||
probe_samples: &[HashMap<i64, (f64, f64)>],
|
||||
current_frame: i64,
|
||||
buffer_size: usize,
|
||||
) {
|
||||
let n = probes.len().max(1);
|
||||
let pane_w = (w as f32) / (n as f32);
|
||||
let pane_x_top_pad = 4.0;
|
||||
let pane_y_top_pad = 18.0;
|
||||
let pane_bot_pad = 6.0;
|
||||
let pane_lr_pad = 4.0;
|
||||
|
||||
let mut paint = Paint::default();
|
||||
paint.anti_alias = true;
|
||||
paint.set_color(Color::from_rgba8(12, 14, 20, 255));
|
||||
let strip = tiny_skia::Rect::from_xywh(x0 as f32, y0 as f32, w as f32, h as f32);
|
||||
if let Some(rect) = strip {
|
||||
pixmap.fill_rect(rect, &paint, Transform::identity(), None);
|
||||
}
|
||||
|
||||
for (i, probe) in probes.iter().enumerate() {
|
||||
let px0 = x0 as f32 + (i as f32) * pane_w + pane_lr_pad;
|
||||
let py0 = y0 as f32 + pane_x_top_pad;
|
||||
let pw = pane_w - 2.0 * pane_lr_pad;
|
||||
let ph = (h as f32) - pane_x_top_pad - pane_bot_pad;
|
||||
if pw <= 0.0 || ph <= 0.0 { continue; }
|
||||
|
||||
paint.set_color(Color::from_rgba8(18, 20, 26, 255));
|
||||
if let Some(r) = tiny_skia::Rect::from_xywh(px0, py0, pw, ph) {
|
||||
pixmap.fill_rect(r, &paint, Transform::identity(), None);
|
||||
}
|
||||
|
||||
let plot_x = px0 + 4.0;
|
||||
let plot_y = py0 + pane_y_top_pad;
|
||||
let plot_w = pw - 8.0;
|
||||
let plot_h = ph - pane_y_top_pad - 6.0;
|
||||
if plot_w <= 0.0 || plot_h <= 0.0 { continue; }
|
||||
|
||||
let probe_color = Color::from_rgba8(probe.color[0], probe.color[1], probe.color[2], 255);
|
||||
paint.set_color(probe_color);
|
||||
|
||||
let samples = probe_samples.get(i);
|
||||
let mut points: Vec<(i64, f64)> = Vec::new();
|
||||
if let Some(s) = samples {
|
||||
for (&fi, &(bx, by)) in s {
|
||||
points.push((fi, probe.mode.extract(bx, by)));
|
||||
}
|
||||
}
|
||||
if points.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut ymin = f64::INFINITY;
|
||||
let mut ymax = f64::NEG_INFINITY;
|
||||
for &(_, v) in &points {
|
||||
if v < ymin { ymin = v; }
|
||||
if v > ymax { ymax = v; }
|
||||
}
|
||||
if !ymin.is_finite() || !ymax.is_finite() { ymin = -1.0; ymax = 1.0; }
|
||||
if (ymax - ymin).abs() < 1e-12 { ymin -= 1.0; ymax += 1.0; }
|
||||
points.sort_by_key(|p| p.0);
|
||||
|
||||
let buf = buffer_size.max(1) as f64;
|
||||
let x_for = |fi: i64| -> f32 {
|
||||
let t = (fi.rem_euclid(buf as i64) as f64) / buf;
|
||||
plot_x + (t as f32) * plot_w
|
||||
};
|
||||
let y_for = |v: f64| -> f32 {
|
||||
let t = (v - ymin) / (ymax - ymin);
|
||||
plot_y + plot_h - (t as f32) * plot_h
|
||||
};
|
||||
|
||||
let axis_stroke = Stroke { width: 1.0, ..Default::default() };
|
||||
paint.set_color(Color::from_rgba8(60, 65, 75, 255));
|
||||
let mut pb = PathBuilder::new();
|
||||
pb.move_to(plot_x, plot_y + plot_h);
|
||||
pb.line_to(plot_x + plot_w, plot_y + plot_h);
|
||||
if let Some(path) = pb.finish() {
|
||||
pixmap.stroke_path(&path, &paint, &axis_stroke, Transform::identity(), None);
|
||||
}
|
||||
let mid_y = y_for((ymin + ymax) * 0.5);
|
||||
paint.set_color(Color::from_rgba8(50, 55, 65, 255));
|
||||
let mut pb = PathBuilder::new();
|
||||
pb.move_to(plot_x, mid_y);
|
||||
pb.line_to(plot_x + plot_w, mid_y);
|
||||
if let Some(path) = pb.finish() {
|
||||
pixmap.stroke_path(&path, &paint, &axis_stroke, Transform::identity(), None);
|
||||
}
|
||||
|
||||
paint.set_color(probe_color);
|
||||
let line_stroke = Stroke { width: 1.4, ..Default::default() };
|
||||
let mut pb = PathBuilder::new();
|
||||
let mut started = false;
|
||||
for (fi, v) in &points {
|
||||
let x = x_for(*fi);
|
||||
let y = y_for(*v);
|
||||
if !started { pb.move_to(x, y); started = true; }
|
||||
else { pb.line_to(x, y); }
|
||||
}
|
||||
if started {
|
||||
if let Some(path) = pb.finish() {
|
||||
pixmap.stroke_path(&path, &paint, &line_stroke, Transform::identity(), None);
|
||||
}
|
||||
}
|
||||
|
||||
let cx = x_for(current_frame);
|
||||
paint.set_color(Color::from_rgba8(255, 255, 255, 200));
|
||||
let cur_stroke = Stroke { width: 1.2, ..Default::default() };
|
||||
let mut pb = PathBuilder::new();
|
||||
pb.move_to(cx, plot_y);
|
||||
pb.line_to(cx, plot_y + plot_h);
|
||||
if let Some(path) = pb.finish() {
|
||||
pixmap.stroke_path(&path, &paint, &cur_stroke, Transform::identity(), None);
|
||||
}
|
||||
|
||||
let label_dot_r = 4.0;
|
||||
paint.set_color(probe_color);
|
||||
let mut pb = PathBuilder::new();
|
||||
pb.push_circle(px0 + 8.0, py0 + 9.0, label_dot_r);
|
||||
if let Some(path) = pb.finish() {
|
||||
pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// returns a polyline approximating an arc between two endpoints across a CCW degree sweep, capped at max_steps subdivisions.
|
||||
fn arc_polyline(x0: f64, y0: f64, x1: f64, y1: f64, sweep_deg: f64, max_steps: usize) -> Vec<(f64, f64)> {
|
||||
let theta = sweep_deg.to_radians();
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ impl Expression {
|
|||
}
|
||||
}
|
||||
|
||||
/// one continuous-time track: clamped chord, axis, member nodes, the closed-form displacement expression, and the pre-subdivision edge spine used to re-subdivide on demand.
|
||||
/// one continuous-time track: clamped chord, axis, member nodes, the closed-form displacement expression, the pre-subdivision edge spine, and the per-track subdivision count regenerated against a stashed pristine doc.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Track {
|
||||
pub label: String,
|
||||
|
|
@ -97,6 +97,7 @@ pub struct Track {
|
|||
pub expression: Expression,
|
||||
pub edge_endpoints: Vec<(usize, usize)>,
|
||||
pub edge_subdivisions: Vec<Vec<usize>>,
|
||||
pub subdivisions: usize,
|
||||
}
|
||||
|
||||
/// snaps doc nodes to base and layers each track's displacement at simulated time t.
|
||||
|
|
@ -184,148 +185,53 @@ pub fn track_from_selection(
|
|||
expression,
|
||||
edge_endpoints: Vec::new(),
|
||||
edge_subdivisions: Vec::new(),
|
||||
subdivisions: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// removes the chain of segments inside each edge and replaces it with a fresh chain at `count` subdivisions, appending new node indices to the doc. orphans the prior subdivision nodes to preserve every other track's index references.
|
||||
pub fn rebuild_track_subdivisions(track: &mut Track, doc: &mut FemmDoc, count: usize) -> Result<(), String> {
|
||||
if count < 2 {
|
||||
return Err(String::from("subdivision count must be at least 2"));
|
||||
}
|
||||
if track.edge_endpoints.is_empty() {
|
||||
let (eps, subs) = infer_edges_from_positions(track, doc);
|
||||
if eps.is_empty() {
|
||||
return Err(String::from("track has no recorded edges and inference from positions found none - delete and recreate the track"));
|
||||
/// resets the live doc to a clone of the pristine doc, then re-subdivides every track's edges at the track's per-track subdivision count, refreshing each track's member_nodes and edge_subdivisions. orphan-free because no prior subdivision nodes survive the reset.
|
||||
pub fn rebuild_all_tracks_from_pristine(
|
||||
live_doc: &mut FemmDoc,
|
||||
pristine: &FemmDoc,
|
||||
tracks: &mut [Track],
|
||||
) -> Result<(), String> {
|
||||
*live_doc = pristine.clone();
|
||||
for (ti, track) in tracks.iter_mut().enumerate() {
|
||||
if track.edge_endpoints.is_empty() {
|
||||
return Err(format!("track {} has no recorded edges; delete and recreate it", ti + 1));
|
||||
}
|
||||
track.edge_endpoints = eps;
|
||||
track.edge_subdivisions = subs;
|
||||
}
|
||||
while track.edge_subdivisions.len() < track.edge_endpoints.len() {
|
||||
track.edge_subdivisions.push(Vec::new());
|
||||
}
|
||||
|
||||
let mut new_subdivisions: Vec<Vec<usize>> = Vec::with_capacity(track.edge_endpoints.len());
|
||||
for (edge_idx, (a, b)) in track.edge_endpoints.iter().copied().enumerate() {
|
||||
let old_subs = track.edge_subdivisions[edge_idx].clone();
|
||||
let edge_nodes: std::collections::HashSet<usize> = std::iter::once(a)
|
||||
.chain(std::iter::once(b))
|
||||
.chain(old_subs.iter().copied())
|
||||
.collect();
|
||||
doc.segments.retain(|s| {
|
||||
!(edge_nodes.contains(&(s.n0 as usize)) && edge_nodes.contains(&(s.n1 as usize)))
|
||||
});
|
||||
|
||||
let pa = match doc.nodes.get(a) {
|
||||
Some(n) => (n.x, n.y),
|
||||
None => return Err(format!("edge endpoint {a} out of bounds")),
|
||||
};
|
||||
let pb = match doc.nodes.get(b) {
|
||||
Some(n) => (n.x, n.y),
|
||||
None => return Err(format!("edge endpoint {b} out of bounds")),
|
||||
};
|
||||
|
||||
let mut new_subs: Vec<usize> = Vec::with_capacity(count.saturating_sub(1));
|
||||
for i in 1..count {
|
||||
let t = i as f64 / count as f64;
|
||||
let x = pa.0 + (pb.0 - pa.0) * t;
|
||||
let y = pa.1 + (pb.1 - pa.1) * t;
|
||||
let idx = doc.nodes.len();
|
||||
doc.nodes.push(femm_doc_mag::Node {
|
||||
x, y,
|
||||
boundary_marker: String::new(),
|
||||
in_group: 0,
|
||||
selected: false,
|
||||
let count = if track.subdivisions >= 2 { track.subdivisions } else { 20 };
|
||||
let mut new_edge_subs: Vec<Vec<usize>> = Vec::with_capacity(track.edge_endpoints.len());
|
||||
for &(a, b) in &track.edge_endpoints {
|
||||
let seg_idx = live_doc.segments.iter().position(|s| {
|
||||
let n0 = s.n0 as usize;
|
||||
let n1 = s.n1 as usize;
|
||||
(n0 == a && n1 == b) || (n0 == b && n1 == a)
|
||||
});
|
||||
new_subs.push(idx);
|
||||
}
|
||||
|
||||
let mut prev = a;
|
||||
for &mid in &new_subs {
|
||||
doc.segments.push(femm_doc_mag::Segment {
|
||||
n0: prev as i32,
|
||||
n1: mid as i32,
|
||||
max_side_length: -1.0,
|
||||
boundary_marker: String::new(),
|
||||
hidden: false,
|
||||
in_group: 0,
|
||||
selected: false,
|
||||
});
|
||||
prev = mid;
|
||||
}
|
||||
doc.segments.push(femm_doc_mag::Segment {
|
||||
n0: prev as i32,
|
||||
n1: b as i32,
|
||||
max_side_length: -1.0,
|
||||
boundary_marker: String::new(),
|
||||
hidden: false,
|
||||
in_group: 0,
|
||||
selected: false,
|
||||
});
|
||||
new_subdivisions.push(new_subs);
|
||||
}
|
||||
|
||||
let mut corner_set: std::collections::HashSet<usize> = std::collections::HashSet::new();
|
||||
for &(a, b) in &track.edge_endpoints {
|
||||
corner_set.insert(a);
|
||||
corner_set.insert(b);
|
||||
}
|
||||
let mut new_members: Vec<usize> = Vec::new();
|
||||
for &c in &corner_set {
|
||||
if c != track.anchor_a && c != track.anchor_b { new_members.push(c); }
|
||||
}
|
||||
for subs in &new_subdivisions {
|
||||
for &s in subs { new_members.push(s); }
|
||||
}
|
||||
track.member_nodes = new_members;
|
||||
track.edge_subdivisions = new_subdivisions;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// reconstructs a track's edges from its node positions: clusters anchors+members by the lateral coordinate (perpendicular to the displacement axis), and treats each cluster as one edge with extremes as endpoints and middles as subdivisions.
|
||||
fn infer_edges_from_positions(track: &Track, doc: &FemmDoc) -> (Vec<(usize, usize)>, Vec<Vec<usize>>) {
|
||||
let along_x = matches!(track.axis, Axis::PlusY | Axis::MinusY);
|
||||
let pos = |i: usize| doc.nodes.get(i).map(|n| (n.x, n.y));
|
||||
let perp = move |i: usize| pos(i).map(|(x, y)| if along_x { x } else { y });
|
||||
let lat = move |i: usize| pos(i).map(|(x, y)| if along_x { y } else { x });
|
||||
|
||||
let mut all_indices: Vec<usize> = Vec::with_capacity(track.member_nodes.len() + 2);
|
||||
all_indices.push(track.anchor_a);
|
||||
all_indices.push(track.anchor_b);
|
||||
all_indices.extend(&track.member_nodes);
|
||||
|
||||
let tol = 0.05_f64;
|
||||
let mut clusters: Vec<(f64, Vec<usize>)> = Vec::new();
|
||||
for &idx in &all_indices {
|
||||
let v = match lat(idx) { Some(v) => v, None => continue };
|
||||
let mut placed = false;
|
||||
for cluster in &mut clusters {
|
||||
if (v - cluster.0).abs() < tol {
|
||||
cluster.1.push(idx);
|
||||
placed = true;
|
||||
break;
|
||||
match seg_idx {
|
||||
Some(i) => {
|
||||
let subs = live_doc.subdivide_segment(i, count)
|
||||
.into_iter().map(|n| n as usize).collect();
|
||||
new_edge_subs.push(subs);
|
||||
}
|
||||
None => {
|
||||
new_edge_subs.push(Vec::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
if !placed { clusters.push((v, vec![idx])); }
|
||||
}
|
||||
|
||||
let mut endpoints = Vec::new();
|
||||
let mut subdivisions = Vec::new();
|
||||
for (_, cluster) in clusters {
|
||||
if cluster.len() < 2 { continue; }
|
||||
let mut sorted: Vec<(usize, f64)> = cluster.iter()
|
||||
.filter_map(|&i| perp(i).map(|p| (i, p)))
|
||||
let mut corner_set: std::collections::HashSet<usize> = std::collections::HashSet::new();
|
||||
for &(a, b) in &track.edge_endpoints {
|
||||
corner_set.insert(a);
|
||||
corner_set.insert(b);
|
||||
}
|
||||
let mut members: Vec<usize> = corner_set.into_iter()
|
||||
.filter(|&c| c != track.anchor_a && c != track.anchor_b)
|
||||
.collect();
|
||||
sorted.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
let a = sorted.first().unwrap().0;
|
||||
let b = sorted.last().unwrap().0;
|
||||
let subs: Vec<usize> = if sorted.len() >= 3 {
|
||||
sorted[1..sorted.len()-1].iter().map(|(i, _)| *i).collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
endpoints.push((a, b));
|
||||
subdivisions.push(subs);
|
||||
for subs in &new_edge_subs {
|
||||
for &s in subs { members.push(s); }
|
||||
}
|
||||
track.edge_subdivisions = new_edge_subs;
|
||||
track.member_nodes = members;
|
||||
}
|
||||
|
||||
(endpoints, subdivisions)
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
mod doc_canvas;
|
||||
mod export;
|
||||
mod kinematic;
|
||||
mod plot;
|
||||
mod probe;
|
||||
mod session;
|
||||
mod sim_meta;
|
||||
mod spice;
|
||||
|
|
@ -96,6 +98,9 @@ enum Message {
|
|||
SimExportWebM,
|
||||
SimExportComplete(Result<export::ExportReport, ErrorReport>),
|
||||
SimExportPoll,
|
||||
ProbeSetMode(usize, probe::ProbeMode),
|
||||
ProbeRemove(usize),
|
||||
ProbeExportWav(usize),
|
||||
}
|
||||
|
||||
/// copyable diagnostic shown when a pipeline stage fails.
|
||||
|
|
@ -130,6 +135,8 @@ struct Simulation {
|
|||
fundamental_hz: f64,
|
||||
fundamental_hz_text: String,
|
||||
loop_playback: bool,
|
||||
probes: Vec<probe::Probe>,
|
||||
probe_samples: Vec<std::collections::HashMap<i64, (f64, f64)>>,
|
||||
}
|
||||
|
||||
struct App {
|
||||
|
|
@ -149,6 +156,7 @@ struct App {
|
|||
zoom_window_active: bool,
|
||||
canvas_size: Option<(f32, f32)>,
|
||||
export_progress: Option<std::sync::Arc<std::sync::Mutex<export::ExportProgress>>>,
|
||||
pristine_doc: Option<FemmDoc>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
|
|
@ -171,6 +179,7 @@ impl App {
|
|||
zoom_window_active: false,
|
||||
canvas_size: None,
|
||||
export_progress: None,
|
||||
pristine_doc: None,
|
||||
};
|
||||
(app, Task::none())
|
||||
}
|
||||
|
|
@ -199,11 +208,80 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
/// reads a .femsess from path and replaces the doc, simulation, pristine snapshot, and source path on success.
|
||||
fn load_session_from_path(&mut self, path: &Path) -> Task<Message> {
|
||||
let text = match std::fs::read_to_string(path) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
self.error = Some(ErrorReport {
|
||||
title: String::from("read session failed"),
|
||||
body: format!("{path:?}\n\n{e}"),
|
||||
});
|
||||
return Task::none();
|
||||
}
|
||||
};
|
||||
let (base_doc, pristine_doc, meta) = match session::parse(&text) {
|
||||
Ok(triple) => triple,
|
||||
Err(e) => {
|
||||
self.error = Some(ErrorReport {
|
||||
title: String::from("parse session failed"),
|
||||
body: format!("{path:?}\n\n{e}"),
|
||||
});
|
||||
return Task::none();
|
||||
}
|
||||
};
|
||||
let tracks = match sim_meta::resolve_tracks(&meta, &base_doc) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
self.error = Some(ErrorReport {
|
||||
title: String::from("session tracks do not match embedded doc"),
|
||||
body: format!("{path:?}\n\n{e}"),
|
||||
});
|
||||
return Task::none();
|
||||
}
|
||||
};
|
||||
self.doc = base_doc.clone();
|
||||
self.mesh = None;
|
||||
self.solution = None;
|
||||
self.error = None;
|
||||
self.pristine_doc = pristine_doc;
|
||||
let mut sim = new_simulation(base_doc, String::new());
|
||||
if let Some(secs) = spice::parse_spice(&meta.dt_text) { sim.dt = secs; }
|
||||
sim.dt_text = meta.dt_text.clone();
|
||||
if let Some(secs) = spice::parse_spice(&meta.interval_text) {
|
||||
sim.wall_interval = Duration::from_secs_f64(secs);
|
||||
}
|
||||
sim.interval_text = meta.interval_text.clone();
|
||||
sim.subdivisions = meta.subdivisions;
|
||||
sim.subdivisions_text = meta.subdivisions.to_string();
|
||||
sim.buffer_size = meta.buffer_size;
|
||||
sim.buffer_size_text = meta.buffer_size.to_string();
|
||||
sim.match_cycle = meta.match_cycle;
|
||||
sim.loop_playback = meta.loop_playback;
|
||||
sim.fundamental_hz = meta.fundamental_hz;
|
||||
sim.fundamental_hz_text = format!("{}", meta.fundamental_hz);
|
||||
sim.tracks = tracks;
|
||||
if !sim.tracks.is_empty() {
|
||||
sim.selected_track = Some(0);
|
||||
sim.expression_text = sim.tracks[0].expression.source.clone();
|
||||
}
|
||||
maybe_recompute_buffer(&mut sim);
|
||||
self.simulation = Some(sim);
|
||||
self.source_path = meta.source_fem.as_ref().map(PathBuf::from);
|
||||
self.source_label = path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("?")
|
||||
.to_string();
|
||||
self.status = format!("loaded session: {}", path.display());
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn update(&mut self, msg: Message) -> Task<Message> {
|
||||
match msg {
|
||||
Message::OpenFem => {
|
||||
let picked = rfd::FileDialog::new()
|
||||
.add_filter("FEMM magnetostatic", &["fem", "FEM"])
|
||||
.add_filter("FEMM magnetostatic or session", &["fem", "FEM", "femsess"])
|
||||
.add_filter("All files", &["*"])
|
||||
.pick_file();
|
||||
|
||||
|
|
@ -213,11 +291,19 @@ impl App {
|
|||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("?")
|
||||
.to_string();
|
||||
let is_session = path.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|e| e.eq_ignore_ascii_case("femsess"))
|
||||
.unwrap_or(false);
|
||||
if is_session {
|
||||
return self.load_session_from_path(&path);
|
||||
}
|
||||
match FemmDoc::open(&path) {
|
||||
Ok(d) => {
|
||||
self.doc = d;
|
||||
self.mesh = None;
|
||||
self.solution = None;
|
||||
self.pristine_doc = None;
|
||||
self.source_label = label;
|
||||
self.source_path = Some(path);
|
||||
self.status = String::new();
|
||||
|
|
@ -232,6 +318,7 @@ impl App {
|
|||
self.doc = FemmDoc::default();
|
||||
self.mesh = None;
|
||||
self.solution = None;
|
||||
self.pristine_doc = None;
|
||||
self.source_label = String::from("untitled");
|
||||
self.source_path = None;
|
||||
self.error = None;
|
||||
|
|
@ -435,6 +522,28 @@ impl App {
|
|||
self.solution = None;
|
||||
self.status = format!("block label {idx} at ({:.3}, {:.3})", world.0, world.1);
|
||||
}
|
||||
Tool::AddProbe => {
|
||||
if self.simulation.is_none() {
|
||||
self.simulation = Some(new_simulation(self.doc.clone(), String::new()));
|
||||
}
|
||||
let sim = self.simulation.as_mut().unwrap();
|
||||
let idx = sim.probes.len();
|
||||
let p = probe::Probe {
|
||||
x: world.0, y: world.1,
|
||||
label: format!("p{}", idx + 1),
|
||||
color: probe::palette_color(idx),
|
||||
mode: probe::ProbeMode::default(),
|
||||
};
|
||||
let mut samples = std::collections::HashMap::new();
|
||||
for (&fi, sol) in &sim.lut {
|
||||
if let Some(bxby) = sol.sample_b_at(p.x, p.y) {
|
||||
samples.insert(fi, bxby);
|
||||
}
|
||||
}
|
||||
sim.probes.push(p);
|
||||
sim.probe_samples.push(samples);
|
||||
self.status = format!("probe p{} placed at ({:.3}, {:.3})", idx + 1, world.0, world.1);
|
||||
}
|
||||
Tool::Select | Tool::AddSegment => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -495,6 +604,9 @@ impl App {
|
|||
body: String::from("select at least one segment or arc (left-click or marquee). the track moves every node along the selection between its leftmost and rightmost endpoints."),
|
||||
});
|
||||
} else {
|
||||
if self.pristine_doc.is_none() {
|
||||
self.pristine_doc = Some(self.doc.clone());
|
||||
}
|
||||
let subdivisions = self.simulation.as_ref()
|
||||
.map(|s| s.subdivisions)
|
||||
.unwrap_or(SIM_DEFAULT_SUBDIVISIONS);
|
||||
|
|
@ -537,6 +649,7 @@ impl App {
|
|||
Ok(mut track) => {
|
||||
track.edge_endpoints = edge_endpoints;
|
||||
track.edge_subdivisions = edge_subdivisions;
|
||||
track.subdivisions = subdivisions;
|
||||
if self.simulation.is_none() {
|
||||
self.simulation = Some(new_simulation(self.doc.clone(), expr_text));
|
||||
}
|
||||
|
|
@ -671,20 +784,27 @@ impl App {
|
|||
self.status = format!("subdivisions for new tracks set to {n}");
|
||||
return Task::none();
|
||||
};
|
||||
let mut track = sim.tracks[selected].clone();
|
||||
let rebuild = kinematic::rebuild_track_subdivisions(&mut track, &mut self.doc, n);
|
||||
sim.tracks[selected].subdivisions = n;
|
||||
let Some(pristine) = self.pristine_doc.clone() else {
|
||||
self.error = Some(ErrorReport {
|
||||
title: String::from("no pristine doc stashed"),
|
||||
body: String::from("this session was loaded without a pristine snapshot. delete and recreate all tracks (or load from a newer session) to enable subdivision rebuild."),
|
||||
});
|
||||
return Task::none();
|
||||
};
|
||||
let sim_mut = self.simulation.as_mut().unwrap();
|
||||
let rebuild = kinematic::rebuild_all_tracks_from_pristine(&mut self.doc, &pristine, &mut sim_mut.tracks);
|
||||
match rebuild {
|
||||
Ok(()) => {
|
||||
let sim = self.simulation.as_mut().unwrap();
|
||||
sim.tracks[selected] = track;
|
||||
sim.base_doc = self.doc.clone();
|
||||
invalidate_lut(sim);
|
||||
self.status = format!("rebuilt track {} with {n} subdivisions per edge", selected + 1);
|
||||
let sim_mut = self.simulation.as_mut().unwrap();
|
||||
sim_mut.base_doc = self.doc.clone();
|
||||
invalidate_lut(sim_mut);
|
||||
self.status = format!("rebuilt all tracks from pristine; track {} now at {n} subdivisions", selected + 1);
|
||||
}
|
||||
Err(e) => {
|
||||
self.error = Some(ErrorReport {
|
||||
title: String::from("subdivision rebuild failed"),
|
||||
body: format!("{e}\n\ndelete and recreate the track to enable rebuild for tracks created before this feature."),
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -789,6 +909,14 @@ impl App {
|
|||
if frame_idx == sim.frame_idx && sim.current.is_none() {
|
||||
sim.current = Some(sol.clone());
|
||||
}
|
||||
while sim.probe_samples.len() < sim.probes.len() {
|
||||
sim.probe_samples.push(std::collections::HashMap::new());
|
||||
}
|
||||
for (i, p) in sim.probes.iter().enumerate() {
|
||||
if let Some(bxby) = sol.sample_b_at(p.x, p.y) {
|
||||
sim.probe_samples[i].insert(frame_idx, bxby);
|
||||
}
|
||||
}
|
||||
sim.lut.insert(frame_idx, sol);
|
||||
}
|
||||
Err(report) => {
|
||||
|
|
@ -975,7 +1103,7 @@ impl App {
|
|||
.save_file();
|
||||
if let Some(path) = picked {
|
||||
let meta = build_sim_meta(sim, self.source_path.as_deref());
|
||||
let text = session::serialize(&sim.base_doc, &meta);
|
||||
let text = session::serialize(&sim.base_doc, self.pristine_doc.as_ref(), &meta);
|
||||
match std::fs::write(&path, text) {
|
||||
Ok(()) => self.status = format!("saved session: {}", path.display()),
|
||||
Err(e) => self.error = Some(ErrorReport {
|
||||
|
|
@ -990,64 +1118,7 @@ impl App {
|
|||
.add_filter("femm42 session", &["femsess"])
|
||||
.pick_file();
|
||||
let Some(path) = picked else { return Task::none(); };
|
||||
let text = match std::fs::read_to_string(&path) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
self.error = Some(ErrorReport {
|
||||
title: String::from("read session failed"),
|
||||
body: format!("{path:?}\n\n{e}"),
|
||||
});
|
||||
return Task::none();
|
||||
}
|
||||
};
|
||||
let (base_doc, meta) = match session::parse(&text) {
|
||||
Ok(pair) => pair,
|
||||
Err(e) => {
|
||||
self.error = Some(ErrorReport {
|
||||
title: String::from("parse session failed"),
|
||||
body: format!("{path:?}\n\n{e}"),
|
||||
});
|
||||
return Task::none();
|
||||
}
|
||||
};
|
||||
let tracks = match sim_meta::resolve_tracks(&meta, &base_doc) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
self.error = Some(ErrorReport {
|
||||
title: String::from("session tracks do not match embedded doc"),
|
||||
body: format!("{path:?}\n\n{e}"),
|
||||
});
|
||||
return Task::none();
|
||||
}
|
||||
};
|
||||
self.doc = base_doc.clone();
|
||||
self.mesh = None;
|
||||
self.solution = None;
|
||||
self.error = None;
|
||||
let mut sim = new_simulation(base_doc, String::new());
|
||||
if let Some(secs) = spice::parse_spice(&meta.dt_text) { sim.dt = secs; }
|
||||
sim.dt_text = meta.dt_text.clone();
|
||||
if let Some(secs) = spice::parse_spice(&meta.interval_text) {
|
||||
sim.wall_interval = Duration::from_secs_f64(secs);
|
||||
}
|
||||
sim.interval_text = meta.interval_text.clone();
|
||||
sim.subdivisions = meta.subdivisions;
|
||||
sim.subdivisions_text = meta.subdivisions.to_string();
|
||||
sim.buffer_size = meta.buffer_size;
|
||||
sim.buffer_size_text = meta.buffer_size.to_string();
|
||||
sim.match_cycle = meta.match_cycle;
|
||||
sim.loop_playback = meta.loop_playback;
|
||||
sim.fundamental_hz = meta.fundamental_hz;
|
||||
sim.fundamental_hz_text = format!("{}", meta.fundamental_hz);
|
||||
sim.tracks = tracks;
|
||||
if !sim.tracks.is_empty() {
|
||||
sim.selected_track = Some(0);
|
||||
sim.expression_text = sim.tracks[0].expression.source.clone();
|
||||
}
|
||||
maybe_recompute_buffer(&mut sim);
|
||||
self.simulation = Some(sim);
|
||||
self.source_path = meta.source_fem.as_ref().map(PathBuf::from);
|
||||
self.status = format!("loaded session: {}", path.display());
|
||||
return self.load_session_from_path(&path);
|
||||
}
|
||||
Message::SimExportWebM => {
|
||||
let Some(sim) = self.simulation.as_ref() else {
|
||||
|
|
@ -1094,6 +1165,8 @@ impl App {
|
|||
height: 1080,
|
||||
contour,
|
||||
crf: 18,
|
||||
probes: sim.probes.clone(),
|
||||
probe_samples: sim.probe_samples.clone(),
|
||||
progress: std::sync::Arc::clone(&progress),
|
||||
};
|
||||
self.export_progress = Some(progress);
|
||||
|
|
@ -1135,6 +1208,61 @@ impl App {
|
|||
};
|
||||
}
|
||||
}
|
||||
Message::ProbeSetMode(i, mode) => {
|
||||
if let Some(sim) = self.simulation.as_mut() {
|
||||
if let Some(p) = sim.probes.get_mut(i) {
|
||||
p.mode = mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::ProbeRemove(i) => {
|
||||
if let Some(sim) = self.simulation.as_mut() {
|
||||
if i < sim.probes.len() {
|
||||
sim.probes.remove(i);
|
||||
if i < sim.probe_samples.len() {
|
||||
sim.probe_samples.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::ProbeExportWav(i) => {
|
||||
let Some(sim) = self.simulation.as_ref() else { return Task::none(); };
|
||||
let Some(p) = sim.probes.get(i) else { return Task::none(); };
|
||||
let Some(samples) = sim.probe_samples.get(i) else { return Task::none(); };
|
||||
if samples.is_empty() {
|
||||
self.error = Some(ErrorReport {
|
||||
title: String::from("no samples"),
|
||||
body: String::from("fill the buffer (run the sim with loop enabled) before exporting a wav."),
|
||||
});
|
||||
return Task::none();
|
||||
}
|
||||
let default_name = format!("{}_{}.wav", p.label, match p.mode {
|
||||
probe::ProbeMode::Magnitude => "magB",
|
||||
probe::ProbeMode::Bx => "Bx",
|
||||
probe::ProbeMode::By => "By",
|
||||
probe::ProbeMode::Angle => "ang",
|
||||
});
|
||||
let picked = rfd::FileDialog::new()
|
||||
.add_filter("WAV audio", &["wav"])
|
||||
.set_file_name(&default_name)
|
||||
.save_file();
|
||||
let Some(path) = picked else { return Task::none(); };
|
||||
let mut ordered: Vec<(i64, f64)> = samples.iter()
|
||||
.map(|(&fi, &(bx, by))| (fi, p.mode.extract(bx, by)))
|
||||
.collect();
|
||||
ordered.sort_by_key(|s| s.0);
|
||||
let values: Vec<f64> = ordered.into_iter().map(|(_, v)| v).collect();
|
||||
let sample_rate = (1.0 / sim.dt).round().max(1.0) as u32;
|
||||
match write_wav_mono_16bit_normalized(&path, &values, sample_rate) {
|
||||
Ok(()) => self.status = format!(
|
||||
"wrote {}: {} samples at {} Hz", path.display(), values.len(), sample_rate,
|
||||
),
|
||||
Err(e) => self.error = Some(ErrorReport {
|
||||
title: String::from("wav export failed"),
|
||||
body: format!("{path:?}\n\n{e}"),
|
||||
}),
|
||||
}
|
||||
}
|
||||
Message::SimToggleLoop(checked) => {
|
||||
if let Some(sim) = self.simulation.as_mut() {
|
||||
sim.loop_playback = checked;
|
||||
|
|
@ -1174,10 +1302,10 @@ impl App {
|
|||
.size(12);
|
||||
|
||||
let file_group = row![
|
||||
icon_button(ICON_FILE_NEW, "New problem", Some(Message::NewDoc)),
|
||||
icon_button(ICON_FILE_OPEN, "Open .fem", Some(Message::OpenFem)),
|
||||
icon_button(ICON_FILE_SAVE, "Save", Some(Message::SaveDoc)),
|
||||
icon_button(ICON_FILE_SAVE_AS, "Save As", Some(Message::SaveDocAs)),
|
||||
icon_button(ICON_FILE_NEW, "New problem", Some(Message::NewDoc)),
|
||||
icon_button(ICON_FILE_OPEN, "Open .fem or .femsess", Some(Message::OpenFem)),
|
||||
icon_button(ICON_FILE_SAVE, "Save", Some(Message::SaveDoc)),
|
||||
icon_button(ICON_FILE_SAVE_AS, "Save As", Some(Message::SaveDocAs)),
|
||||
].spacing(2);
|
||||
|
||||
let tool_group = row![
|
||||
|
|
@ -1185,6 +1313,7 @@ impl App {
|
|||
tool_icon_button(ICON_TOOL_NODE, "Add Node", Tool::AddNode, self.tool),
|
||||
tool_icon_button(ICON_TOOL_SEGMENT, "Add Segment", Tool::AddSegment, self.tool),
|
||||
tool_icon_button(ICON_TOOL_LABEL, "Add Label", Tool::AddBlockLabel, self.tool),
|
||||
text_button("Probe", Message::SelectTool(Tool::AddProbe)),
|
||||
].spacing(2);
|
||||
|
||||
let edit_group = row![
|
||||
|
|
@ -1247,9 +1376,12 @@ impl App {
|
|||
.and_then(|s| s.current.as_ref())
|
||||
.or(self.solution.as_ref());
|
||||
|
||||
let probes_slice: &[probe::Probe] = self.simulation.as_ref()
|
||||
.map(|s| s.probes.as_slice())
|
||||
.unwrap_or(&[]);
|
||||
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.zoom_window_active,
|
||||
self.view_state, self.show_grid, self.zoom_window_active, probes_slice,
|
||||
).map(Message::Canvas);
|
||||
|
||||
let canvas_row = row![canvas, view_strip]
|
||||
|
|
@ -1268,6 +1400,42 @@ impl App {
|
|||
body = body.push(simulation_panel(sim));
|
||||
}
|
||||
body = body.push(canvas_row);
|
||||
if let Some(sim) = &self.simulation {
|
||||
if !sim.probes.is_empty() {
|
||||
static EMPTY_MAP: std::sync::LazyLock<std::collections::HashMap<i64, (f64, f64)>>
|
||||
= std::sync::LazyLock::new(std::collections::HashMap::new);
|
||||
let mut pane_row = row![].spacing(8);
|
||||
for (i, p) in sim.probes.iter().enumerate() {
|
||||
let samples = sim.probe_samples.get(i).unwrap_or(&*EMPTY_MAP);
|
||||
let mode_row = row![
|
||||
button(text(p.label.clone()).size(11)).style(button::secondary),
|
||||
button(text("|B|").size(10)).on_press(Message::ProbeSetMode(i, probe::ProbeMode::Magnitude))
|
||||
.style(if p.mode == probe::ProbeMode::Magnitude { button::primary } else { button::secondary }),
|
||||
button(text("Bx").size(10)).on_press(Message::ProbeSetMode(i, probe::ProbeMode::Bx))
|
||||
.style(if p.mode == probe::ProbeMode::Bx { button::primary } else { button::secondary }),
|
||||
button(text("By").size(10)).on_press(Message::ProbeSetMode(i, probe::ProbeMode::By))
|
||||
.style(if p.mode == probe::ProbeMode::By { button::primary } else { button::secondary }),
|
||||
button(text("ang").size(10)).on_press(Message::ProbeSetMode(i, probe::ProbeMode::Angle))
|
||||
.style(if p.mode == probe::ProbeMode::Angle { button::primary } else { button::secondary }),
|
||||
button(text("wav").size(10)).on_press(Message::ProbeExportWav(i)).style(button::secondary),
|
||||
button(text("x").size(10)).on_press(Message::ProbeRemove(i)).style(button::danger),
|
||||
].spacing(2);
|
||||
let plot_canvas: Element<'_, Message> =
|
||||
plot::view::<Message>(p, samples, sim.frame_idx, sim.buffer_size.max(1));
|
||||
let pane = column![mode_row, plot_canvas]
|
||||
.spacing(2)
|
||||
.width(Length::FillPortion(1))
|
||||
.height(Length::Fill);
|
||||
pane_row = pane_row.push(pane);
|
||||
}
|
||||
body = body.push(
|
||||
container(pane_row)
|
||||
.width(Length::Fill)
|
||||
.height(Length::FillPortion(1))
|
||||
.padding(4)
|
||||
);
|
||||
}
|
||||
}
|
||||
if !self.status.is_empty() {
|
||||
body = body.push(text(&self.status).size(12));
|
||||
}
|
||||
|
|
@ -1319,6 +1487,8 @@ fn new_simulation(base_doc: FemmDoc, expression_text: String) -> Simulation {
|
|||
fundamental_hz: SIM_DEFAULT_FUNDAMENTAL_HZ,
|
||||
fundamental_hz_text: format!("{SIM_DEFAULT_FUNDAMENTAL_HZ}"),
|
||||
loop_playback: false,
|
||||
probes: Vec::new(),
|
||||
probe_samples: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1339,6 +1509,7 @@ fn build_sim_meta(sim: &Simulation, source: Option<&Path>) -> sim_meta::SimMeta
|
|||
axis: t.axis,
|
||||
expression: t.expression.source.clone(),
|
||||
edges,
|
||||
subdivisions: t.subdivisions,
|
||||
}
|
||||
}).collect();
|
||||
sim_meta::SimMeta {
|
||||
|
|
@ -1370,6 +1541,46 @@ fn invalidate_lut(sim: &mut Simulation) {
|
|||
sim.pending.clear();
|
||||
sim.generation = sim.generation.wrapping_add(1);
|
||||
sim.current = None;
|
||||
for samples in sim.probe_samples.iter_mut() {
|
||||
samples.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// writes mono 16-bit PCM WAV with the given sample rate, normalising the input so the peak magnitude maps to i16 full-scale.
|
||||
fn write_wav_mono_16bit_normalized(path: &Path, values: &[f64], sample_rate: u32) -> std::io::Result<()> {
|
||||
use std::io::Write;
|
||||
let mut peak = 0.0_f64;
|
||||
for &v in values {
|
||||
let a = v.abs();
|
||||
if a.is_finite() && a > peak { peak = a; }
|
||||
}
|
||||
let scale = if peak > 0.0 { (i16::MAX as f64 - 1.0) / peak } else { 0.0 };
|
||||
let mut pcm: Vec<i16> = Vec::with_capacity(values.len());
|
||||
for &v in values {
|
||||
let s = if v.is_finite() { (v * scale).round() } else { 0.0 };
|
||||
pcm.push(s.clamp(i16::MIN as f64, i16::MAX as f64) as i16);
|
||||
}
|
||||
let data_len = (pcm.len() * 2) as u32;
|
||||
let chunk_size = 36 + data_len;
|
||||
let byte_rate = sample_rate * 2;
|
||||
let mut f = std::fs::File::create(path)?;
|
||||
f.write_all(b"RIFF")?;
|
||||
f.write_all(&chunk_size.to_le_bytes())?;
|
||||
f.write_all(b"WAVE")?;
|
||||
f.write_all(b"fmt ")?;
|
||||
f.write_all(&16u32.to_le_bytes())?;
|
||||
f.write_all(&1u16.to_le_bytes())?;
|
||||
f.write_all(&1u16.to_le_bytes())?;
|
||||
f.write_all(&sample_rate.to_le_bytes())?;
|
||||
f.write_all(&byte_rate.to_le_bytes())?;
|
||||
f.write_all(&2u16.to_le_bytes())?;
|
||||
f.write_all(&16u16.to_le_bytes())?;
|
||||
f.write_all(b"data")?;
|
||||
f.write_all(&data_len.to_le_bytes())?;
|
||||
for s in &pcm {
|
||||
f.write_all(&s.to_le_bytes())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// caps concurrent solve jobs at cores x 4, clamped to [8, 64].
|
||||
|
|
|
|||
|
|
@ -0,0 +1,161 @@
|
|||
//! per-probe waveform plot widget rendered as an iced canvas, color-matched to the probe marker on the main canvas.
|
||||
|
||||
use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path, Stroke, Text};
|
||||
use iced::{Color, Element, Length, Point, Rectangle, Renderer, Theme, mouse};
|
||||
|
||||
use crate::probe::Probe;
|
||||
|
||||
/// renders one probe's plot pane: a labeled waveform of the chosen signal across every frame currently in samples, with a vertical bar marking the current playback frame.
|
||||
pub fn view<'a, Msg: 'a + Clone>(
|
||||
probe: &'a Probe,
|
||||
samples: &'a std::collections::HashMap<i64, (f64, f64)>,
|
||||
current_frame: i64,
|
||||
buffer_size: usize,
|
||||
) -> Element<'a, Msg> {
|
||||
Canvas::new(PlotProgram {
|
||||
probe,
|
||||
samples,
|
||||
current_frame,
|
||||
buffer_size,
|
||||
})
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
struct PlotProgram<'a> {
|
||||
probe: &'a Probe,
|
||||
samples: &'a std::collections::HashMap<i64, (f64, f64)>,
|
||||
current_frame: i64,
|
||||
buffer_size: usize,
|
||||
}
|
||||
|
||||
impl<'a, Msg> canvas::Program<Msg> for PlotProgram<'a> {
|
||||
type State = ();
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_state: &Self::State,
|
||||
renderer: &Renderer,
|
||||
_theme: &Theme,
|
||||
bounds: Rectangle,
|
||||
_cursor: mouse::Cursor,
|
||||
) -> Vec<Geometry<Renderer>> {
|
||||
let mut frame = Frame::new(renderer, bounds.size());
|
||||
let bg = Color::from_rgba8(18, 20, 26, 1.0);
|
||||
frame.fill_rectangle(Point::ORIGIN, bounds.size(), bg);
|
||||
|
||||
let pad_l = 8.0_f32;
|
||||
let pad_r = 8.0_f32;
|
||||
let pad_t = 18.0_f32;
|
||||
let pad_b = 16.0_f32;
|
||||
let plot_w = (bounds.width - pad_l - pad_r).max(1.0);
|
||||
let plot_h = (bounds.height - pad_t - pad_b).max(1.0);
|
||||
|
||||
let probe_color = Color::from_rgba8(
|
||||
self.probe.color[0], self.probe.color[1], self.probe.color[2], 1.0,
|
||||
);
|
||||
|
||||
frame.fill_text(Text {
|
||||
content: format!("{} - {}", self.probe.label, self.probe.mode.label()),
|
||||
position: Point::new(pad_l, 2.0),
|
||||
color: probe_color,
|
||||
size: 11.0.into(),
|
||||
..Text::default()
|
||||
});
|
||||
|
||||
let buf = self.buffer_size.max(1);
|
||||
let mut points: Vec<(i64, f64)> = Vec::with_capacity(self.samples.len());
|
||||
for (&fi, &(bx, by)) in self.samples {
|
||||
points.push((fi, self.probe.mode.extract(bx, by)));
|
||||
}
|
||||
if points.is_empty() {
|
||||
frame.fill_text(Text {
|
||||
content: String::from("no samples yet"),
|
||||
position: Point::new(pad_l + 4.0, pad_t + 4.0),
|
||||
color: Color::from_rgba8(140, 140, 150, 1.0),
|
||||
size: 10.0.into(),
|
||||
..Text::default()
|
||||
});
|
||||
return vec![frame.into_geometry()];
|
||||
}
|
||||
let mut ymin = f64::INFINITY;
|
||||
let mut ymax = f64::NEG_INFINITY;
|
||||
for &(_, v) in &points {
|
||||
if v < ymin { ymin = v; }
|
||||
if v > ymax { ymax = v; }
|
||||
}
|
||||
if !ymin.is_finite() || !ymax.is_finite() {
|
||||
ymin = -1.0; ymax = 1.0;
|
||||
}
|
||||
if (ymax - ymin).abs() < 1e-12 {
|
||||
ymin -= 1.0; ymax += 1.0;
|
||||
}
|
||||
|
||||
points.sort_by_key(|p| p.0);
|
||||
|
||||
let x_for = |fi: i64| -> f32 {
|
||||
let t = (fi.rem_euclid(buf as i64) as f64) / (buf as f64).max(1.0);
|
||||
pad_l + (t as f32) * plot_w
|
||||
};
|
||||
let y_for = |v: f64| -> f32 {
|
||||
let t = (v - ymin) / (ymax - ymin);
|
||||
pad_t + plot_h - (t as f32) * plot_h
|
||||
};
|
||||
|
||||
frame.stroke(
|
||||
&Path::line(
|
||||
Point::new(pad_l, pad_t + plot_h),
|
||||
Point::new(pad_l + plot_w, pad_t + plot_h),
|
||||
),
|
||||
Stroke::default().with_width(0.5).with_color(Color::from_rgba8(80, 80, 90, 1.0)),
|
||||
);
|
||||
let mid_y = y_for((ymin + ymax) * 0.5);
|
||||
frame.stroke(
|
||||
&Path::line(
|
||||
Point::new(pad_l, mid_y),
|
||||
Point::new(pad_l + plot_w, mid_y),
|
||||
),
|
||||
Stroke::default().with_width(0.5).with_color(Color::from_rgba8(60, 60, 70, 1.0)),
|
||||
);
|
||||
|
||||
let mut path = canvas::path::Builder::new();
|
||||
let mut started = false;
|
||||
for (fi, v) in &points {
|
||||
let x = x_for(*fi);
|
||||
let y = y_for(*v);
|
||||
if !started { path.move_to(Point::new(x, y)); started = true; }
|
||||
else { path.line_to(Point::new(x, y)); }
|
||||
}
|
||||
if started {
|
||||
frame.stroke(&path.build(),
|
||||
Stroke::default().with_width(1.4).with_color(probe_color));
|
||||
}
|
||||
|
||||
let cx = x_for(self.current_frame);
|
||||
frame.stroke(
|
||||
&Path::line(
|
||||
Point::new(cx, pad_t),
|
||||
Point::new(cx, pad_t + plot_h),
|
||||
),
|
||||
Stroke::default().with_width(1.0).with_color(Color::from_rgba8(255, 255, 255, 0.55)),
|
||||
);
|
||||
|
||||
frame.fill_text(Text {
|
||||
content: format!("{:.3e}", ymax),
|
||||
position: Point::new(pad_l, pad_t - 12.0),
|
||||
color: Color::from_rgba8(150, 150, 160, 1.0),
|
||||
size: 9.0.into(),
|
||||
..Text::default()
|
||||
});
|
||||
frame.fill_text(Text {
|
||||
content: format!("{:.3e}", ymin),
|
||||
position: Point::new(pad_l, pad_t + plot_h + 2.0),
|
||||
color: Color::from_rgba8(150, 150, 160, 1.0),
|
||||
size: 9.0.into(),
|
||||
..Text::default()
|
||||
});
|
||||
|
||||
vec![frame.into_geometry()]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
//! point-sample probes for the magnetic field, time-series plotted per frame in a panel below the canvas.
|
||||
|
||||
/// signal extracted from the (bx, by) sample at a probe location for waveform plotting.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub enum ProbeMode {
|
||||
#[default]
|
||||
Magnitude,
|
||||
Bx,
|
||||
By,
|
||||
Angle,
|
||||
}
|
||||
|
||||
impl ProbeMode {
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
ProbeMode::Magnitude => "|B|",
|
||||
ProbeMode::Bx => "Bx",
|
||||
ProbeMode::By => "By",
|
||||
ProbeMode::Angle => "atan2(By,Bx)",
|
||||
}
|
||||
}
|
||||
pub fn extract(self, bx: f64, by: f64) -> f64 {
|
||||
match self {
|
||||
ProbeMode::Magnitude => (bx * bx + by * by).sqrt(),
|
||||
ProbeMode::Bx => bx,
|
||||
ProbeMode::By => by,
|
||||
ProbeMode::Angle => by.atan2(bx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// one user-placed probe with a world position, display color, label, and plot-mode selector.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Probe {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub label: String,
|
||||
pub color: [u8; 4],
|
||||
pub mode: ProbeMode,
|
||||
}
|
||||
|
||||
/// returns a saturated palette color from a small cycling palette suitable for probe markers and matching plot lines.
|
||||
pub fn palette_color(idx: usize) -> [u8; 4] {
|
||||
const PALETTE: &[[u8; 4]] = &[
|
||||
[231, 76, 60, 255],
|
||||
[241, 196, 15, 255],
|
||||
[ 46, 204, 113, 255],
|
||||
[ 52, 152, 219, 255],
|
||||
[155, 89, 182, 255],
|
||||
[230, 126, 34, 255],
|
||||
[ 26, 188, 156, 255],
|
||||
[255, 105, 180, 255],
|
||||
];
|
||||
PALETTE[idx % PALETTE.len()]
|
||||
}
|
||||
|
|
@ -4,13 +4,22 @@ use femm_doc_mag::FemmDoc;
|
|||
use crate::sim_meta::{self, SimMeta};
|
||||
|
||||
const SESSION_HEADER: &str = "# femm42 session v1\n";
|
||||
const PRISTINE_OPEN: &str = "[pristine-doc]\n";
|
||||
const PRISTINE_CLOSE: &str = "[/pristine-doc]\n";
|
||||
const DOC_OPEN: &str = "[base-doc]\n";
|
||||
const DOC_CLOSE: &str = "[/base-doc]\n";
|
||||
|
||||
/// joins the base .fem text and the sim metadata into one delimited session blob.
|
||||
pub fn serialize(base_doc: &FemmDoc, meta: &SimMeta) -> String {
|
||||
/// joins the optional pristine .fem text, the live base .fem text, and the sim metadata into one delimited session blob.
|
||||
pub fn serialize(base_doc: &FemmDoc, pristine_doc: Option<&FemmDoc>, meta: &SimMeta) -> String {
|
||||
let mut out = String::new();
|
||||
out.push_str(SESSION_HEADER);
|
||||
if let Some(p) = pristine_doc {
|
||||
out.push_str(PRISTINE_OPEN);
|
||||
let pt = p.write();
|
||||
out.push_str(&pt);
|
||||
if !pt.ends_with('\n') { out.push('\n'); }
|
||||
out.push_str(PRISTINE_CLOSE);
|
||||
}
|
||||
out.push_str(DOC_OPEN);
|
||||
let doc_text = base_doc.write();
|
||||
out.push_str(&doc_text);
|
||||
|
|
@ -20,8 +29,19 @@ pub fn serialize(base_doc: &FemmDoc, meta: &SimMeta) -> String {
|
|||
out
|
||||
}
|
||||
|
||||
/// extracts the base FemmDoc and SimMeta from a session blob, failing if the markers or sub-payloads are malformed.
|
||||
pub fn parse(text: &str) -> Result<(FemmDoc, SimMeta), String> {
|
||||
/// extracts the optional pristine FemmDoc, the base FemmDoc, and SimMeta from a session blob.
|
||||
pub fn parse(text: &str) -> Result<(FemmDoc, Option<FemmDoc>, SimMeta), String> {
|
||||
let pristine = if let Some(p_start) = text.find(PRISTINE_OPEN) {
|
||||
let body_start = p_start + PRISTINE_OPEN.len();
|
||||
let after_body = text[body_start..]
|
||||
.find(PRISTINE_CLOSE)
|
||||
.ok_or_else(|| String::from("missing [/pristine-doc] marker"))?;
|
||||
let p_text = &text[body_start..body_start + after_body];
|
||||
Some(FemmDoc::parse(p_text).map_err(|e| format!("parse [pristine-doc] failed: {e}"))?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let doc_start = text.find(DOC_OPEN).ok_or_else(|| String::from("missing [base-doc] section"))?;
|
||||
let body_start = doc_start + DOC_OPEN.len();
|
||||
let after_body = text[body_start..]
|
||||
|
|
@ -33,5 +53,5 @@ pub fn parse(text: &str) -> Result<(FemmDoc, SimMeta), String> {
|
|||
let doc = FemmDoc::parse(doc_text)
|
||||
.map_err(|e| format!("parse [base-doc] failed: {e}"))?;
|
||||
let meta = sim_meta::parse_meta(meta_text)?;
|
||||
Ok((doc, meta))
|
||||
Ok((doc, pristine, meta))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ pub struct TrackMeta {
|
|||
pub axis: Axis,
|
||||
pub expression: String,
|
||||
pub edges: Vec<(usize, usize, Vec<usize>)>,
|
||||
pub subdivisions: usize,
|
||||
}
|
||||
|
||||
/// renders a SimMeta into the on-disk text format.
|
||||
|
|
@ -51,6 +52,7 @@ pub fn serialize_meta(meta: &SimMeta) -> String {
|
|||
s.push_str(&format!("anchor_b = {}\n", track.anchor_b));
|
||||
s.push_str(&format!("axis = {}\n", axis_to_str(track.axis)));
|
||||
s.push_str(&format!("expression = {}\n", track.expression));
|
||||
s.push_str(&format!("subdivisions = {}\n", track.subdivisions));
|
||||
s.push_str("members =");
|
||||
for n in &track.members { s.push_str(&format!(" {n}")); }
|
||||
s.push_str("\n");
|
||||
|
|
@ -95,6 +97,7 @@ pub fn parse_meta(text: &str) -> Result<SimMeta, String> {
|
|||
axis: Axis::PlusY,
|
||||
expression: String::from("0"),
|
||||
edges: Vec::new(),
|
||||
subdivisions: 0,
|
||||
}); Section::Track }
|
||||
other => return Err(format!("unknown section {other:?} at line {}", line_no + 1)),
|
||||
};
|
||||
|
|
@ -134,6 +137,9 @@ pub fn parse_meta(text: &str) -> Result<SimMeta, String> {
|
|||
track.members.push(parse_usize(tok, line_no)?);
|
||||
}
|
||||
}
|
||||
"subdivisions" => {
|
||||
track.subdivisions = parse_usize(value, line_no)?;
|
||||
}
|
||||
"edge" => {
|
||||
let mut iter = value.split_whitespace();
|
||||
let a = iter.next().ok_or_else(|| format!("edge missing first endpoint at line {}", line_no + 1))?;
|
||||
|
|
@ -203,6 +209,7 @@ pub fn resolve_tracks(meta: &SimMeta, doc: &FemmDoc) -> Result<Vec<Track>, Strin
|
|||
expression: expr,
|
||||
edge_endpoints,
|
||||
edge_subdivisions,
|
||||
subdivisions: if tm.subdivisions >= 2 { tm.subdivisions } else { 20 },
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
|
|
|
|||
|
|
@ -175,6 +175,21 @@ impl MagSolution {
|
|||
+ el.b2.re * el.b2.re + el.b2.im * el.b2.im).sqrt()
|
||||
}
|
||||
|
||||
/// looks up the mesh triangle containing world point (x, y) and returns its (bx, by) real components, or None when the point lies outside every element.
|
||||
pub fn sample_b_at(&self, x: f64, y: f64) -> Option<(f64, f64)> {
|
||||
for el in &self.mesh_elements {
|
||||
let (Some(a), Some(b), Some(c)) = (
|
||||
self.mesh_nodes.get(el.n[0] as usize),
|
||||
self.mesh_nodes.get(el.n[1] as usize),
|
||||
self.mesh_nodes.get(el.n[2] as usize),
|
||||
) else { continue };
|
||||
if point_in_triangle((x, y), (a.x, a.y), (b.x, b.y), (c.x, c.y)) {
|
||||
return Some((el.b1.re, el.b2.re));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// (min, max) of |B| across every element, skipping NaN.
|
||||
pub fn b_magnitude_range(&self) -> (f64, f64) {
|
||||
let mut lo = f64::INFINITY;
|
||||
|
|
@ -335,6 +350,19 @@ fn take_i32(s: &str) -> (i32, &str) {
|
|||
(head.parse::<i32>().unwrap_or(0), tail)
|
||||
}
|
||||
|
||||
/// returns true when point p lies inside (or on the boundary of) the triangle a-b-c, via the half-plane sign test.
|
||||
fn point_in_triangle(p: (f64, f64), a: (f64, f64), b: (f64, f64), c: (f64, f64)) -> bool {
|
||||
let sign = |p1: (f64, f64), p2: (f64, f64), p3: (f64, f64)| -> f64 {
|
||||
(p1.0 - p3.0) * (p2.1 - p3.1) - (p2.0 - p3.0) * (p1.1 - p3.1)
|
||||
};
|
||||
let d1 = sign(p, a, b);
|
||||
let d2 = sign(p, b, c);
|
||||
let d3 = sign(p, c, a);
|
||||
let neg = d1 < 0.0 || d2 < 0.0 || d3 < 0.0;
|
||||
let pos = d1 > 0.0 || d2 > 0.0 || d3 > 0.0;
|
||||
!(neg && pos)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,233 @@
|
|||
[Format] = 4.0
|
||||
[Frequency] = 0
|
||||
[Precision] = 1e-008
|
||||
[MinAngle] = 25
|
||||
[Depth] = 5
|
||||
[LengthUnits] = millimeters
|
||||
[ProblemType] = planar
|
||||
[Coordinates] = cartesian
|
||||
[Comment] = "quarter-scale fretboard: six plain steel strings (.009/.011/.016/.026/.036/.046) spanning 162 mm. each string is anchored on the left to its own isolated NdFeB pole-piece magnet (mag_dir 0, N face into the string) and on the right to a shared steel block that ties all six strings together. a bar magnet on the right face of the steel block (mag_dir 0) drives flux through the steel and out into the air return."
|
||||
[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] = 58
|
||||
-100 -100 0 0
|
||||
300 -100 0 0
|
||||
300 100 0 0
|
||||
-100 100 0 0
|
||||
-10 -30.25 0 0
|
||||
0 -30.25 0 0
|
||||
0 -26.8342 0 0
|
||||
0 -25.6658 0 0
|
||||
0 -22.25 0 0
|
||||
-10 -22.25 0 0
|
||||
-10 -19.75 0 0
|
||||
0 -19.75 0 0
|
||||
0 -16.2072 0 0
|
||||
0 -15.2928 0 0
|
||||
0 -11.75 0 0
|
||||
-10 -11.75 0 0
|
||||
-10 -9.25 0 0
|
||||
0 -9.25 0 0
|
||||
0 -5.5802 0 0
|
||||
0 -4.9198 0 0
|
||||
0 -1.25 0 0
|
||||
-10 -1.25 0 0
|
||||
-10 1.25 0 0
|
||||
0 1.25 0 0
|
||||
0 5.0468 0 0
|
||||
0 5.4532 0 0
|
||||
0 9.25 0 0
|
||||
-10 9.25 0 0
|
||||
-10 11.75 0 0
|
||||
0 11.75 0 0
|
||||
0 15.6103 0 0
|
||||
0 15.8897 0 0
|
||||
0 19.75 0 0
|
||||
-10 19.75 0 0
|
||||
-10 22.25 0 0
|
||||
0 22.25 0 0
|
||||
0 26.1357 0 0
|
||||
0 26.3643 0 0
|
||||
0 30.25 0 0
|
||||
-10 30.25 0 0
|
||||
162 -30 0 0
|
||||
162 -26.8342 0 0
|
||||
162 -25.6658 0 0
|
||||
162 -16.2072 0 0
|
||||
162 -15.2928 0 0
|
||||
162 -5.5802 0 0
|
||||
162 -4.9198 0 0
|
||||
162 5.0468 0 0
|
||||
162 5.4532 0 0
|
||||
162 15.6103 0 0
|
||||
162 15.8897 0 0
|
||||
162 26.1357 0 0
|
||||
162 26.3643 0 0
|
||||
162 30 0 0
|
||||
192 -30 0 0
|
||||
192 30 0 0
|
||||
222 -30 0 0
|
||||
222 30 0 0
|
||||
[NumSegments] = 71
|
||||
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 9 -1 0 0 0
|
||||
9 8 -1 0 0 0
|
||||
8 7 -1 0 0 0
|
||||
7 6 -1 0 0 0
|
||||
6 5 -1 0 0 0
|
||||
5 4 -1 0 0 0
|
||||
10 15 -1 0 0 0
|
||||
15 14 -1 0 0 0
|
||||
14 13 -1 0 0 0
|
||||
13 12 -1 0 0 0
|
||||
12 11 -1 0 0 0
|
||||
11 10 -1 0 0 0
|
||||
16 21 -1 0 0 0
|
||||
21 20 -1 0 0 0
|
||||
20 19 -1 0 0 0
|
||||
19 18 -1 0 0 0
|
||||
18 17 -1 0 0 0
|
||||
17 16 -1 0 0 0
|
||||
22 27 -1 0 0 0
|
||||
27 26 -1 0 0 0
|
||||
26 25 -1 0 0 0
|
||||
25 24 -1 0 0 0
|
||||
24 23 -1 0 0 0
|
||||
23 22 -1 0 0 0
|
||||
28 33 -1 0 0 0
|
||||
33 32 -1 0 0 0
|
||||
32 31 -1 0 0 0
|
||||
31 30 -1 0 0 0
|
||||
30 29 -1 0 0 0
|
||||
29 28 -1 0 0 0
|
||||
34 39 -1 0 0 0
|
||||
39 38 -1 0 0 0
|
||||
38 37 -1 0 0 0
|
||||
37 36 -1 0 0 0
|
||||
36 35 -1 0 0 0
|
||||
35 34 -1 0 0 0
|
||||
7 42 -1 0 0 0
|
||||
6 41 -1 0 0 0
|
||||
13 44 -1 0 0 0
|
||||
12 43 -1 0 0 0
|
||||
19 46 -1 0 0 0
|
||||
18 45 -1 0 0 0
|
||||
25 48 -1 0 0 0
|
||||
24 47 -1 0 0 0
|
||||
31 50 -1 0 0 0
|
||||
30 49 -1 0 0 0
|
||||
37 52 -1 0 0 0
|
||||
36 51 -1 0 0 0
|
||||
40 41 -1 0 0 0
|
||||
41 42 -1 0 0 0
|
||||
42 43 -1 0 0 0
|
||||
43 44 -1 0 0 0
|
||||
44 45 -1 0 0 0
|
||||
45 46 -1 0 0 0
|
||||
46 47 -1 0 0 0
|
||||
47 48 -1 0 0 0
|
||||
48 49 -1 0 0 0
|
||||
49 50 -1 0 0 0
|
||||
50 51 -1 0 0 0
|
||||
51 52 -1 0 0 0
|
||||
52 53 -1 0 0 0
|
||||
54 55 -1 0 0 0
|
||||
53 55 -1 0 0 0
|
||||
54 40 -1 0 0 0
|
||||
56 57 -1 0 0 0
|
||||
55 57 -1 0 0 0
|
||||
56 54 -1 0 0 0
|
||||
[NumArcSegments] = 0
|
||||
[NumHoles] = 0
|
||||
[NumBlockLabels] = 15
|
||||
-50 0 1 -1 0 0 0 1 2
|
||||
-5 -26.25 2 5 0 0 0 1 0
|
||||
-5 -15.75 2 5 0 0 0 1 0
|
||||
-5 -5.25 2 5 0 0 0 1 0
|
||||
-5 5.25 2 5 0 0 0 1 0
|
||||
-5 15.75 2 5 0 0 0 1 0
|
||||
-5 26.25 2 5 0 0 0 1 0
|
||||
81 -26.25 3 1 0 0 0 1 0
|
||||
81 -15.75 3 1 0 0 0 1 0
|
||||
81 -5.25 3 1 0 0 0 1 0
|
||||
81 5.25 3 1 0 0 0 1 0
|
||||
81 15.75 3 1 0 0 0 1 0
|
||||
81 26.25 3 1 0 0 0 1 0
|
||||
177 0 3 5 0 0 0 1 0
|
||||
207 0 2 5 0 0 0 1 0
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue