parent
4e6a655683
commit
74ab29ee8d
|
|
@ -4,4 +4,5 @@ build/ffi/
|
||||||
*.a
|
*.a
|
||||||
target/
|
target/
|
||||||
assets/old/
|
assets/old/
|
||||||
|
assets/icons/
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
//! draws a FemmDoc on an iced canvas: nodes, segments, arcs, block labels, with pan/zoom and click-to-add.
|
//! draws a FemmDoc on an iced canvas: nodes, segments, arcs, block labels, with pan/zoom and click-to-add.
|
||||||
|
|
||||||
use femm_doc_mag::FemmDoc;
|
use femm_doc_mag::FemmDoc;
|
||||||
|
use femm_doc_mag::mesh::Mesh;
|
||||||
use iced::widget::canvas::{
|
use iced::widget::canvas::{
|
||||||
self, Action, Canvas, Event, Frame, Geometry, Path, Stroke, Text, path::Builder,
|
self, Action, Canvas, Event, Frame, Geometry, Path, Stroke, Text, path::Builder,
|
||||||
};
|
};
|
||||||
|
|
@ -20,6 +21,8 @@ const LABEL_COLOR: Color = Color::from_rgb(0.25, 0.45, 0.85);
|
||||||
const PENDING_COLOR: Color = Color::from_rgb(0.85, 0.30, 0.30);
|
const PENDING_COLOR: Color = Color::from_rgb(0.85, 0.30, 0.30);
|
||||||
const SELECT_COLOR: Color = Color::from_rgb(0.95, 0.20, 0.20);
|
const SELECT_COLOR: Color = Color::from_rgb(0.95, 0.20, 0.20);
|
||||||
const SELECT_STROKE: f32 = 2.4;
|
const SELECT_STROKE: f32 = 2.4;
|
||||||
|
const MESH_COLOR: Color = Color::from_rgba(0.55, 0.55, 0.55, 0.55);
|
||||||
|
const MESH_STROKE: f32 = 0.5;
|
||||||
|
|
||||||
/// active editing mode on the canvas.
|
/// active editing mode on the canvas.
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
|
|
@ -56,9 +59,13 @@ pub struct ViewState {
|
||||||
cursor_world: Option<(f64, f64)>,
|
cursor_world: Option<(f64, f64)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// constructs the canvas widget for a doc reference.
|
/// constructs the canvas widget for a doc reference and an optional mesh overlay.
|
||||||
pub fn view<'a>(doc: &'a FemmDoc, tool: Tool) -> Element<'a, CanvasMessage> {
|
pub fn view<'a>(
|
||||||
Canvas::new(DocCanvas { doc, tool })
|
doc: &'a FemmDoc,
|
||||||
|
tool: Tool,
|
||||||
|
mesh: Option<&'a Mesh>,
|
||||||
|
) -> Element<'a, CanvasMessage> {
|
||||||
|
Canvas::new(DocCanvas { doc, tool, mesh })
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.into()
|
.into()
|
||||||
|
|
@ -67,6 +74,7 @@ pub fn view<'a>(doc: &'a FemmDoc, tool: Tool) -> Element<'a, CanvasMessage> {
|
||||||
struct DocCanvas<'a> {
|
struct DocCanvas<'a> {
|
||||||
doc: &'a FemmDoc,
|
doc: &'a FemmDoc,
|
||||||
tool: Tool,
|
tool: Tool,
|
||||||
|
mesh: Option<&'a Mesh>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> canvas::Program<CanvasMessage> for DocCanvas<'a> {
|
impl<'a> canvas::Program<CanvasMessage> for DocCanvas<'a> {
|
||||||
|
|
@ -214,6 +222,27 @@ impl<'a> canvas::Program<CanvasMessage> for DocCanvas<'a> {
|
||||||
|
|
||||||
let view = ViewTransform::fit(self.doc, bounds, state);
|
let view = ViewTransform::fit(self.doc, bounds, state);
|
||||||
|
|
||||||
|
if let Some(mesh) = self.mesh {
|
||||||
|
for el in &mesh.elements {
|
||||||
|
let (Some(a), Some(b), Some(c)) = (
|
||||||
|
mesh.nodes.get(el.v0 as usize),
|
||||||
|
mesh.nodes.get(el.v1 as usize),
|
||||||
|
mesh.nodes.get(el.v2 as usize),
|
||||||
|
) else { continue };
|
||||||
|
let pa = view.map(a.x, a.y);
|
||||||
|
let pb = view.map(b.x, b.y);
|
||||||
|
let pc = view.map(c.x, c.y);
|
||||||
|
let tri = Path::new(|p| {
|
||||||
|
p.move_to(pa);
|
||||||
|
p.line_to(pb);
|
||||||
|
p.line_to(pc);
|
||||||
|
p.close();
|
||||||
|
});
|
||||||
|
frame.stroke(&tri,
|
||||||
|
Stroke::default().with_width(MESH_STROKE).with_color(MESH_COLOR));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for s in &self.doc.segments {
|
for s in &self.doc.segments {
|
||||||
if let (Some(p0), Some(p1)) =
|
if let (Some(p0), Some(p1)) =
|
||||||
(self.doc.nodes.get(s.n0 as usize), self.doc.nodes.get(s.n1 as usize))
|
(self.doc.nodes.get(s.n0 as usize), self.doc.nodes.get(s.n1 as usize))
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,25 @@ mod doc_canvas;
|
||||||
|
|
||||||
use doc_canvas::{CanvasMessage, Tool};
|
use doc_canvas::{CanvasMessage, Tool};
|
||||||
use femm_doc_mag::FemmDoc;
|
use femm_doc_mag::FemmDoc;
|
||||||
|
use femm_doc_mag::mesh::Mesh;
|
||||||
use iced::widget::{button, column, container, row, text};
|
use iced::widget::{button, column, container, row, text};
|
||||||
use iced::{Element, Length, Task};
|
use iced::{Element, Length, Task};
|
||||||
|
|
||||||
const DEMO_FEM: &str = include_str!("../assets/demo.fem");
|
const DEMO_FEM: &str = include_str!("../assets/demo.fem");
|
||||||
const ADD_TOLERANCE: f64 = 0.5;
|
const ADD_TOLERANCE: f64 = 0.5;
|
||||||
|
const MIN_ANGLE_DEG: f64 = 30.0;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum Message {
|
enum Message {
|
||||||
OpenFem,
|
OpenFem,
|
||||||
SelectTool(Tool),
|
SelectTool(Tool),
|
||||||
Canvas(CanvasMessage),
|
Canvas(CanvasMessage),
|
||||||
|
RunMesh,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct App {
|
struct App {
|
||||||
doc: FemmDoc,
|
doc: FemmDoc,
|
||||||
|
mesh: Option<Mesh>,
|
||||||
source_label: String,
|
source_label: String,
|
||||||
status: String,
|
status: String,
|
||||||
tool: Tool,
|
tool: Tool,
|
||||||
|
|
@ -29,6 +33,7 @@ impl App {
|
||||||
let doc = FemmDoc::parse(DEMO_FEM).unwrap_or_default();
|
let doc = FemmDoc::parse(DEMO_FEM).unwrap_or_default();
|
||||||
let app = App {
|
let app = App {
|
||||||
doc,
|
doc,
|
||||||
|
mesh: None,
|
||||||
source_label: String::from("demo.fem (embedded)"),
|
source_label: String::from("demo.fem (embedded)"),
|
||||||
status: String::new(),
|
status: String::new(),
|
||||||
tool: Tool::Select,
|
tool: Tool::Select,
|
||||||
|
|
@ -57,6 +62,7 @@ impl App {
|
||||||
match FemmDoc::open(&path) {
|
match FemmDoc::open(&path) {
|
||||||
Ok(d) => {
|
Ok(d) => {
|
||||||
self.doc = d;
|
self.doc = d;
|
||||||
|
self.mesh = None;
|
||||||
self.source_label = label;
|
self.source_label = label;
|
||||||
self.status = String::new();
|
self.status = String::new();
|
||||||
}
|
}
|
||||||
|
|
@ -69,14 +75,28 @@ impl App {
|
||||||
Message::SelectTool(t) => {
|
Message::SelectTool(t) => {
|
||||||
self.tool = t;
|
self.tool = t;
|
||||||
}
|
}
|
||||||
|
Message::RunMesh => {
|
||||||
|
match run_mesh(&self.doc) {
|
||||||
|
Ok(m) => {
|
||||||
|
self.status = format!("meshed: {} nodes, {} elements", m.nodes.len(), m.elements.len());
|
||||||
|
self.mesh = Some(m);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.mesh = None;
|
||||||
|
self.status = format!("mesh failed: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Message::Canvas(CanvasMessage::Click { world, tool }) => {
|
Message::Canvas(CanvasMessage::Click { world, tool }) => {
|
||||||
match tool {
|
match tool {
|
||||||
Tool::AddNode => {
|
Tool::AddNode => {
|
||||||
let idx = self.doc.add_node(world.0, world.1, ADD_TOLERANCE);
|
let idx = self.doc.add_node(world.0, world.1, ADD_TOLERANCE);
|
||||||
|
self.mesh = None;
|
||||||
self.status = format!("node {idx} at ({:.3}, {:.3})", world.0, world.1);
|
self.status = format!("node {idx} at ({:.3}, {:.3})", world.0, world.1);
|
||||||
}
|
}
|
||||||
Tool::AddBlockLabel => {
|
Tool::AddBlockLabel => {
|
||||||
let idx = self.doc.add_block_label(world.0, world.1, ADD_TOLERANCE);
|
let idx = self.doc.add_block_label(world.0, world.1, ADD_TOLERANCE);
|
||||||
|
self.mesh = None;
|
||||||
self.status = format!("block label {idx} at ({:.3}, {:.3})", world.0, world.1);
|
self.status = format!("block label {idx} at ({:.3}, {:.3})", world.0, world.1);
|
||||||
}
|
}
|
||||||
Tool::Select | Tool::AddSegment => {}
|
Tool::Select | Tool::AddSegment => {}
|
||||||
|
|
@ -86,6 +106,7 @@ impl App {
|
||||||
let n0 = self.doc.add_node(from.0, from.1, ADD_TOLERANCE) as i32;
|
let n0 = self.doc.add_node(from.0, from.1, ADD_TOLERANCE) as i32;
|
||||||
let n1 = self.doc.add_node(to.0, to.1, ADD_TOLERANCE) as i32;
|
let n1 = self.doc.add_node(to.0, to.1, ADD_TOLERANCE) as i32;
|
||||||
if self.doc.add_segment(n0, n1) {
|
if self.doc.add_segment(n0, n1) {
|
||||||
|
self.mesh = None;
|
||||||
self.status = format!(
|
self.status = format!(
|
||||||
"segment {n0} -> {n1} ({} total)",
|
"segment {n0} -> {n1} ({} total)",
|
||||||
self.doc.segments.len(),
|
self.doc.segments.len(),
|
||||||
|
|
@ -104,6 +125,7 @@ impl App {
|
||||||
let a = self.doc.delete_selected_arcs();
|
let a = self.doc.delete_selected_arcs();
|
||||||
let b = self.doc.delete_selected_block_labels();
|
let b = self.doc.delete_selected_block_labels();
|
||||||
let total = n + s + a + b;
|
let total = n + s + a + b;
|
||||||
|
if total > 0 { self.mesh = None; }
|
||||||
self.status = if total == 0 {
|
self.status = if total == 0 {
|
||||||
String::from("nothing selected")
|
String::from("nothing selected")
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -130,12 +152,14 @@ impl App {
|
||||||
tool_button("Add Node", Tool::AddNode, self.tool),
|
tool_button("Add Node", Tool::AddNode, self.tool),
|
||||||
tool_button("Add Segment", Tool::AddSegment, self.tool),
|
tool_button("Add Segment", Tool::AddSegment, self.tool),
|
||||||
tool_button("Add Label", Tool::AddBlockLabel, self.tool),
|
tool_button("Add Label", Tool::AddBlockLabel, self.tool),
|
||||||
|
button("Mesh").on_press(Message::RunMesh),
|
||||||
text(&self.source_label).size(13),
|
text(&self.source_label).size(13),
|
||||||
stats,
|
stats,
|
||||||
]
|
]
|
||||||
.spacing(8);
|
.spacing(8);
|
||||||
|
|
||||||
let canvas = doc_canvas::view(&self.doc, self.tool).map(Message::Canvas);
|
let canvas = doc_canvas::view(&self.doc, self.tool, self.mesh.as_ref())
|
||||||
|
.map(Message::Canvas);
|
||||||
|
|
||||||
let mut body = column![toolbar].spacing(8).padding(12);
|
let mut body = column![toolbar].spacing(8).padding(12);
|
||||||
body = body.push(canvas);
|
body = body.push(canvas);
|
||||||
|
|
@ -150,6 +174,48 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// saves the doc to a temp .fem and matching .poly, invokes Triangle, and reads back the mesh.
|
||||||
|
fn run_mesh(doc: &FemmDoc) -> Result<Mesh, String> {
|
||||||
|
let dir = std::env::temp_dir().join("femm42-mesh");
|
||||||
|
std::fs::create_dir_all(&dir).map_err(|e| format!("temp dir: {e}"))?;
|
||||||
|
let stem = dir.join("active");
|
||||||
|
let fem_path = stem.with_extension("fem");
|
||||||
|
let poly_path = stem.with_extension("poly");
|
||||||
|
|
||||||
|
doc.save(&fem_path).map_err(|e| format!("save .fem: {e}"))?;
|
||||||
|
doc.save_poly(&poly_path).map_err(|e| format!("save .poly: {e}"))?;
|
||||||
|
|
||||||
|
let triangle = locate_triangle()
|
||||||
|
.ok_or_else(|| String::from("triangle binary not found at build/triangle/triangle - run scripts/macos/build_triangle.sh"))?;
|
||||||
|
|
||||||
|
let stem_str = stem.to_str().ok_or_else(|| String::from("non-utf8 path"))?;
|
||||||
|
let status = std::process::Command::new(&triangle)
|
||||||
|
.args([
|
||||||
|
"-p", "-P", "-e", "-A", "-a", "-z", "-Q", "-I",
|
||||||
|
&format!("-q{MIN_ANGLE_DEG}"),
|
||||||
|
])
|
||||||
|
.arg(stem_str)
|
||||||
|
.status()
|
||||||
|
.map_err(|e| format!("exec triangle: {e}"))?;
|
||||||
|
if !status.success() {
|
||||||
|
return Err(format!("triangle exited with {status}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Mesh::load(&stem).map_err(|e| format!("read mesh: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// resolves the Triangle binary path, preferring the build dir then $PATH.
|
||||||
|
fn locate_triangle() -> Option<std::path::PathBuf> {
|
||||||
|
let here = std::env::current_dir().ok()?;
|
||||||
|
let built = here.join("build/triangle/triangle");
|
||||||
|
if built.is_file() { return Some(built); }
|
||||||
|
if let Ok(env_path) = std::env::var("FEMM_TRIANGLE") {
|
||||||
|
let p = std::path::PathBuf::from(env_path);
|
||||||
|
if p.is_file() { return Some(p); }
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// flips the selected flag on whichever entity sits nearest to (x, y) in doc world coords.
|
/// flips the selected flag on whichever entity sits nearest to (x, y) in doc world coords.
|
||||||
fn toggle_closest(doc: &mut FemmDoc, x: f64, y: f64) {
|
fn toggle_closest(doc: &mut FemmDoc, x: f64, y: f64) {
|
||||||
use femm_doc_mag::geom_math::{
|
use femm_doc_mag::geom_math::{
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ pub mod parser;
|
||||||
pub mod writer;
|
pub mod writer;
|
||||||
pub mod edit;
|
pub mod edit;
|
||||||
pub mod poly;
|
pub mod poly;
|
||||||
|
pub mod mesh;
|
||||||
|
|
||||||
use num_complex::Complex64;
|
use num_complex::Complex64;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
//! readers for Triangle's .node and .ele output files.
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// 2D triangular mesh consumed by the canvas overlay and by the post-processor.
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct Mesh {
|
||||||
|
pub nodes: Vec<MeshNode>,
|
||||||
|
pub elements: Vec<MeshElement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// position and optional boundary marker for a single mesh node.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct MeshNode {
|
||||||
|
pub x: f64,
|
||||||
|
pub y: f64,
|
||||||
|
pub marker: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// triangle element with three zero-based node indices and an optional region attribute.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct MeshElement {
|
||||||
|
pub v0: u32,
|
||||||
|
pub v1: u32,
|
||||||
|
pub v2: u32,
|
||||||
|
pub attribute: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum MeshLoadError {
|
||||||
|
#[error("io error: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
#[error("malformed header in {file}: {reason}")]
|
||||||
|
BadHeader { file: &'static str, reason: String },
|
||||||
|
#[error("malformed row in {file}: {reason}")]
|
||||||
|
BadRow { file: &'static str, reason: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mesh {
|
||||||
|
/// loads .1.node and .1.ele next to the given path stem (Triangle's default output).
|
||||||
|
pub fn load(stem: impl AsRef<Path>) -> Result<Self, MeshLoadError> {
|
||||||
|
let stem = stem.as_ref();
|
||||||
|
let node_path = with_extension(stem, "1.node");
|
||||||
|
let ele_path = with_extension(stem, "1.ele");
|
||||||
|
let nodes = parse_node(&std::fs::read_to_string(&node_path)?)?;
|
||||||
|
let elements = parse_ele(&std::fs::read_to_string(&ele_path)?)?;
|
||||||
|
Ok(Mesh { nodes, elements })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// parses a .node file: header `count 2 0 nbm`, then count rows of `idx x y [marker]`.
|
||||||
|
pub fn parse_node(src: &str) -> Result<Vec<MeshNode>, MeshLoadError> {
|
||||||
|
let mut lines = src.lines().filter(|l| !l.trim_start().starts_with('#') && !l.trim().is_empty());
|
||||||
|
let header = lines.next().ok_or(MeshLoadError::BadHeader {
|
||||||
|
file: ".node",
|
||||||
|
reason: "empty file".into(),
|
||||||
|
})?;
|
||||||
|
let mut hparts = header.split_whitespace();
|
||||||
|
let count: usize = hparts.next()
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.ok_or(MeshLoadError::BadHeader { file: ".node", reason: format!("expected count, got {header:?}") })?;
|
||||||
|
let _dim = hparts.next();
|
||||||
|
let _attrs = hparts.next();
|
||||||
|
let has_marker = hparts.next().map(|s| s.trim() == "1").unwrap_or(false);
|
||||||
|
|
||||||
|
let mut nodes = Vec::with_capacity(count);
|
||||||
|
for line in lines.take(count) {
|
||||||
|
let mut parts = line.split_whitespace();
|
||||||
|
let _idx: u32 = parts.next().and_then(|s| s.parse().ok())
|
||||||
|
.ok_or(MeshLoadError::BadRow { file: ".node", reason: line.into() })?;
|
||||||
|
let x: f64 = parts.next().and_then(|s| s.parse().ok())
|
||||||
|
.ok_or(MeshLoadError::BadRow { file: ".node", reason: line.into() })?;
|
||||||
|
let y: f64 = parts.next().and_then(|s| s.parse().ok())
|
||||||
|
.ok_or(MeshLoadError::BadRow { file: ".node", reason: line.into() })?;
|
||||||
|
let marker = if has_marker {
|
||||||
|
parts.next().and_then(|s| s.parse().ok()).unwrap_or(0)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
nodes.push(MeshNode { x, y, marker });
|
||||||
|
}
|
||||||
|
Ok(nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// parses a .ele file: header `count 3 natt`, then count rows of `idx v0 v1 v2 [attr0 ...]`.
|
||||||
|
pub fn parse_ele(src: &str) -> Result<Vec<MeshElement>, MeshLoadError> {
|
||||||
|
let mut lines = src.lines().filter(|l| !l.trim_start().starts_with('#') && !l.trim().is_empty());
|
||||||
|
let header = lines.next().ok_or(MeshLoadError::BadHeader {
|
||||||
|
file: ".ele",
|
||||||
|
reason: "empty file".into(),
|
||||||
|
})?;
|
||||||
|
let mut hparts = header.split_whitespace();
|
||||||
|
let count: usize = hparts.next()
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.ok_or(MeshLoadError::BadHeader { file: ".ele", reason: format!("expected count, got {header:?}") })?;
|
||||||
|
let _nodes_per = hparts.next();
|
||||||
|
let natt: usize = hparts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||||
|
|
||||||
|
let mut elements = Vec::with_capacity(count);
|
||||||
|
for line in lines.take(count) {
|
||||||
|
let mut parts = line.split_whitespace();
|
||||||
|
let _idx: u32 = parts.next().and_then(|s| s.parse().ok())
|
||||||
|
.ok_or(MeshLoadError::BadRow { file: ".ele", reason: line.into() })?;
|
||||||
|
let v0: u32 = parts.next().and_then(|s| s.parse().ok())
|
||||||
|
.ok_or(MeshLoadError::BadRow { file: ".ele", reason: line.into() })?;
|
||||||
|
let v1: u32 = parts.next().and_then(|s| s.parse().ok())
|
||||||
|
.ok_or(MeshLoadError::BadRow { file: ".ele", reason: line.into() })?;
|
||||||
|
let v2: u32 = parts.next().and_then(|s| s.parse().ok())
|
||||||
|
.ok_or(MeshLoadError::BadRow { file: ".ele", reason: line.into() })?;
|
||||||
|
let attribute = if natt > 0 {
|
||||||
|
parts.next().and_then(|s| s.parse::<f64>().ok()).map(|f| f as i32).unwrap_or(0)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
elements.push(MeshElement { v0, v1, v2, attribute });
|
||||||
|
}
|
||||||
|
Ok(elements)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_extension(stem: &Path, ext: &str) -> std::path::PathBuf {
|
||||||
|
let mut s = stem.to_path_buf();
|
||||||
|
s.set_extension(ext);
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_node_with_markers() {
|
||||||
|
// 3 nodes, 2D, no attributes, with markers; comment line at the start is ignored.
|
||||||
|
let src = "# triangle .node\n3 2 0 1\n0 0.0 0.0 2\n1 1.0 0.0 0\n2 0.0 1.0 0\n";
|
||||||
|
let nodes = parse_node(src).unwrap();
|
||||||
|
assert_eq!(nodes.len(), 3);
|
||||||
|
assert_eq!(nodes[0].marker, 2);
|
||||||
|
assert!((nodes[2].y - 1.0).abs() < 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_ele_with_region_attribute() {
|
||||||
|
// 2 triangles, 3 nodes per element, 1 region attribute column.
|
||||||
|
let src = "2 3 1\n0 0 1 2 1.0\n1 1 2 3 2.0\n";
|
||||||
|
let els = parse_ele(src).unwrap();
|
||||||
|
assert_eq!(els.len(), 2);
|
||||||
|
assert_eq!(els[0].v0, 0);
|
||||||
|
assert_eq!(els[0].v1, 1);
|
||||||
|
assert_eq!(els[0].v2, 2);
|
||||||
|
assert_eq!(els[0].attribute, 1);
|
||||||
|
assert_eq!(els[1].attribute, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_node_without_marker_column() {
|
||||||
|
let src = "2 2 0 0\n0 0.0 0.0\n1 1.0 0.0\n";
|
||||||
|
let nodes = parse_node(src).unwrap();
|
||||||
|
assert_eq!(nodes.len(), 2);
|
||||||
|
assert_eq!(nodes[0].marker, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
|
BUILD="${BUILD:-$ROOT/build/triangle}"
|
||||||
|
|
||||||
|
mkdir -p "$BUILD"
|
||||||
|
|
||||||
|
CC=${CC:-clang}
|
||||||
|
CFLAGS=${CFLAGS:-"-O2 -w"}
|
||||||
|
|
||||||
|
$CC $CFLAGS -o "$BUILD/triangle" "$ROOT/triangle/triangle.c" -lm
|
||||||
|
|
||||||
|
echo "built: $BUILD/triangle"
|
||||||
|
|
@ -1469,7 +1469,7 @@ VOID *memptr;
|
||||||
|
|
||||||
void syntax()
|
void syntax()
|
||||||
{
|
{
|
||||||
ShellExecute(NULL, "open", "https://www.cs.cmu.edu/~quake/triangle.switch.html", NULL, NULL, SW_SHOWNORMAL);
|
printf("Triangle switches: https://www.cs.cmu.edu/~quake/triangle.switch.html\n");
|
||||||
triexit(0);
|
triexit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1485,7 +1485,7 @@ void syntax()
|
||||||
|
|
||||||
void info()
|
void info()
|
||||||
{
|
{
|
||||||
ShellExecute(NULL, "open", "https://www.cs.cmu.edu/~quake/triangle.help.html", NULL, NULL, SW_SHOWNORMAL);
|
printf("Triangle help: https://www.cs.cmu.edu/~quake/triangle.help.html\n");
|
||||||
triexit(0);
|
triexit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue