demo
This commit is contained in:
parent
74ab29ee8d
commit
520188a82a
|
|
@ -1,9 +1,38 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="278.932 265.471 31.349 31.349" xmlns:bx="https://boxy-svg.com">
|
||||
<path style="stroke: rgb(125, 117, 117); vector-effect: non-scaling-stroke; stroke-width: 3px; fill: rgb(3, 3, 3);" transform="matrix(0.101437, 0, 0, 0.098416, 186.736202, 207.679973)" d="M 1011.929 635.017 L 1038.263 635.017 L 1044.534 706.885 A 48.651 48.651 0 0 1 1054 712.35 L 1119.376 681.847 L 1132.543 704.653 L 1073.439 746.018 A 48.651 48.651 0 0 1 1073.439 756.949 L 1132.543 798.314 L 1119.376 821.12 L 1054 790.617 A 48.651 48.651 0 0 1 1044.534 796.082 L 1038.263 867.951 L 1011.929 867.951 L 1005.658 796.082 A 48.651 48.651 0 0 1 996.191 790.617 L 930.816 821.12 L 917.649 798.314 L 976.753 756.949 A 48.651 48.651 0 0 1 976.753 746.018 L 917.649 704.653 L 930.816 681.847 L 996.191 712.35 A 48.651 48.651 0 0 1 1005.658 706.885 Z M 1025.096 730.844 A 20.64 20.64 0 0 0 1025.096 772.123 A 20.64 20.64 0 0 0 1025.096 730.844" bx:shape="cog 1025.096 751.484 20.64 48.651 116.467 0.57 6 1@6e4f8584"/>
|
||||
<path style="vector-effect: non-scaling-stroke; stroke-width: 4px; stroke: rgb(107, 78, 0); fill: rgb(142, 142, 4);" transform="matrix(0.11202, 0, 0, 0.11202, 205.952327, 195.846981)" d="M 754.573 660.818 L 776.232 660.818 L 781.39 719.928 A 40.014 40.014 0 0 1 789.175 724.423 L 842.945 699.336 L 853.775 718.093 L 805.163 752.115 A 40.014 40.014 0 0 1 805.163 761.105 L 853.775 795.127 L 842.945 813.884 L 789.175 788.796 A 40.014 40.014 0 0 1 781.39 793.291 L 776.232 852.401 L 754.573 852.401 L 749.415 793.291 A 40.014 40.014 0 0 1 741.629 788.796 L 687.859 813.884 L 677.03 795.127 L 725.641 761.105 A 40.014 40.014 0 0 1 725.641 752.115 L 677.03 718.093 L 687.859 699.336 L 741.629 724.423 A 40.014 40.014 0 0 1 749.415 719.928 Z M 765.402 739.634 A 16.976 16.976 0 0 0 765.402 773.585 A 16.976 16.976 0 0 0 765.402 739.634" bx:shape="cog 765.402 756.61 16.976 40.014 95.791 0.57 6 1@b6f3ee13"/>
|
||||
<path d="M 294.354 279.503 L 303.403 279.503 C 303.458 279.503 303.508 279.535 303.541 279.585 L 303.541 281.015 C 303.508 281.065 303.458 281.097 303.403 281.097 L 294.354 281.097 C 294.254 281.097 294.171 280.993 294.171 280.862 L 294.171 279.738 C 294.171 279.607 294.254 279.503 294.354 279.503 Z" style="fill: rgb(255, 255, 4); vector-effect: non-scaling-stroke; paint-order: stroke; stroke-width: 9px; stroke: rgb(0, 0, 0);"/>
|
||||
<path d="M 309.392 270.753 L 309.392 271.338 C 309.392 272.2 308.848 272.9 308.179 272.9 L 306.774 272.9 C 306.538 272.9 306.318 272.812 306.129 272.661 L 306.129 270.373 C 306.129 270.095 306.05 269.841 305.917 269.646 C 306.137 269.365 306.441 269.191 306.774 269.191 L 308.179 269.191 C 308.848 269.191 309.392 269.89 309.392 270.753 Z" style="vector-effect: non-scaling-stroke; paint-order: stroke; stroke-width: 9px; stroke: rgb(0, 0, 0);"/>
|
||||
<path d="M 303.585 279.738 L 303.585 280.862 C 303.585 280.92 303.569 280.974 303.541 281.015 L 303.541 282.749 C 303.541 283.362 303.928 283.859 304.403 283.859 L 305.268 283.859 C 305.742 283.859 306.129 283.362 306.129 282.749 L 306.129 272.661 C 305.787 272.385 305.561 271.895 305.561 271.338 L 305.561 270.753 C 305.561 270.32 305.698 269.928 305.917 269.646 C 305.76 269.411 305.527 269.262 305.268 269.262 L 304.403 269.262 C 303.928 269.262 303.541 269.76 303.541 270.373 L 303.541 279.585 C 303.569 279.626 303.585 279.68 303.585 279.738 Z" style="vector-effect: non-scaling-stroke; paint-order: stroke; stroke-width: 9px; stroke: rgb(0, 0, 0); fill: rgb(207, 23, 8);"/>
|
||||
<path style="vector-effect: non-scaling-stroke; paint-order: stroke; stroke-width: 8px; stroke: rgb(89, 103, 61); fill: rgb(255, 255, 4);" transform="matrix(0.11202, 0, 0, 0.11202, 223.932612, 193.577602)" d="M 598.424 703.833 L 614.878 703.833 L 618.797 748.74 A 30.399 30.399 0 0 1 624.712 752.155 L 665.561 733.095 L 673.789 747.345 L 636.858 773.192 A 30.399 30.399 0 0 1 636.858 780.022 L 673.789 805.869 L 665.561 820.119 L 624.712 801.059 A 30.399 30.399 0 0 1 618.797 804.474 L 614.878 849.381 L 598.424 849.381 L 594.505 804.474 A 30.399 30.399 0 0 1 588.59 801.059 L 547.74 820.119 L 539.513 805.869 L 576.444 780.022 A 30.399 30.399 0 0 1 576.444 773.192 L 539.513 747.345 L 547.74 733.095 L 588.59 752.155 A 30.399 30.399 0 0 1 594.505 748.74 Z M 606.651 763.71 A 12.897 12.897 0 0 0 606.651 789.504 A 12.897 12.897 0 0 0 606.651 763.71" bx:shape="cog 606.651 776.607 12.897 30.399 72.774 0.57 6 1@453a207a"/>
|
||||
</svg>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="278.932 265.471 31.349 31.349"
|
||||
version="1.1"
|
||||
id="svg6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:bx="https://boxy-svg.com">
|
||||
<defs
|
||||
id="defs6" />
|
||||
<path
|
||||
style="vector-effect:non-scaling-stroke;fill:#030303;stroke:#7d7575;stroke-width:0.299745px"
|
||||
d="m 290.12925,269.32323 h 2.67124 l 0.63612,7.07296 a 4.9350115,4.7880368 0 0 1 0.9602,0.53784 l 6.63154,-3.00198 1.33562,2.24447 -5.99533,4.07098 a 4.9350115,4.7880368 0 0 1 0,1.07579 l 5.99533,4.07097 -1.33562,2.24448 -6.63154,-3.00198 a 4.9350115,4.7880368 0 0 1 -0.9602,0.53784 l -0.63612,7.07306 h -2.67124 l -0.63611,-7.07306 a 4.9350115,4.7880368 0 0 1 -0.9603,-0.53784 l -6.63145,3.00198 -1.33562,-2.24448 5.99533,-4.07097 a 4.9350115,4.7880368 0 0 1 0,-1.07579 l -5.99533,-4.07098 1.33562,-2.24447 6.63145,3.00198 a 4.9350115,4.7880368 0 0 1 0.9603,-0.53784 z m 1.33562,9.43091 a 2.0936597,2.0313062 0 0 0 0,4.06251 2.0936597,2.0313062 0 0 0 0,-4.06251"
|
||||
bx:shape="cog 1025.096 751.484 20.64 48.651 116.467 0.57 6 1@6e4f8584"
|
||||
id="path1" />
|
||||
<path
|
||||
style="vector-effect:non-scaling-stroke;fill:#8e8e04;stroke:#6b4e00;stroke-width:0.425409px"
|
||||
d="m 291.70371,269.64525 h 2.16275 l 0.51505,6.69554 a 3.9955751,4.5324927 0 0 1 0.77736,0.50916 l 5.36918,-2.84167 1.08142,2.12466 -4.85412,3.85376 a 3.9955751,4.5324927 0 0 1 0,1.01832 l 4.85412,3.85376 -1.08142,2.12466 -5.36918,-2.84178 a 3.9955751,4.5324927 0 0 1 -0.77736,0.50916 l -0.51505,6.69554 h -2.16275 l -0.51505,-6.69554 a 3.9955751,4.5324927 0 0 1 -0.77747,-0.50916 l -5.36917,2.84178 -1.08132,-2.12466 4.85402,-3.85376 a 3.9955751,4.5324927 0 0 1 0,-1.01832 l -4.85402,-3.85376 1.08132,-2.12466 5.36917,2.84167 a 3.9955751,4.5324927 0 0 1 0.77747,-0.50916 z m 1.08132,8.92769 a 1.6951288,1.9229169 0 0 0 0,3.84572 1.6951288,1.9229169 0 0 0 0,-3.84572"
|
||||
bx:shape="cog 765.402 756.61 16.976 40.014 95.791 0.57 6 1@b6f3ee13"
|
||||
id="path2" />
|
||||
<path
|
||||
d="m 308.53942,270.59314 v 0.585 c 0,0.862 -0.544,1.562 -1.213,1.562 h -1.405 c -0.236,0 -0.456,-0.088 -0.645,-0.239 v -2.288 c 0,-0.278 -0.079,-0.532 -0.212,-0.727 0.22,-0.281 0.524,-0.455 0.857,-0.455 h 1.405 c 0.669,0 1.213,0.699 1.213,1.562 z"
|
||||
style="vector-effect:non-scaling-stroke;stroke:#000000;stroke-width:9px;paint-order:stroke"
|
||||
id="path4" />
|
||||
<path
|
||||
d="m 302.35942,279.68471 v 1.124 c 0,0.058 -0.016,0.112 -0.044,0.153 v 1.734 c 0,0.613 0.387,1.11 0.862,1.11 h 0.865 c 0.474,0 0.861,-0.497 0.861,-1.11 v -10.088 c -0.342,-0.276 -0.568,-0.766 -0.568,-1.323 v -0.585 c 0,-0.433 0.137,-0.825 0.356,-1.107 -0.157,-0.235 -0.39,-0.384 -0.649,-0.384 h -0.865 c -0.475,0 -0.862,0.498 -0.862,1.111 v 9.212 c 0.028,0.041 0.044,0.095 0.044,0.153 z"
|
||||
style="vector-effect:non-scaling-stroke;fill:#cf1708;stroke:#000000;stroke-width:9px;paint-order:stroke"
|
||||
id="path5" />
|
||||
<path
|
||||
style="vector-effect:non-scaling-stroke;fill:#ffff04;stroke:#59673d;stroke-width:0.858263px;paint-order:stroke"
|
||||
d="m 291.89154,272.02442 h 1.64811 l 0.39255,5.1601 a 3.0449237,3.4930369 0 0 1 0.59248,0.39241 l 4.09165,-2.19012 0.82416,1.63742 -3.6992,2.96998 a 3.0449237,3.4930369 0 0 1 0,0.78481 l 3.6992,2.96998 -0.82416,1.63742 -4.09165,-2.19011 a 3.0449237,3.4930369 0 0 1 -0.59248,0.3924 l -0.39255,5.1601 h -1.64811 l -0.39255,-5.1601 a 3.0449237,3.4930369 0 0 1 -0.59248,-0.3924 l -4.09175,2.19011 -0.82406,-1.63742 3.6992,-2.96998 a 3.0449237,3.4930369 0 0 1 0,-0.78481 l -3.6992,-2.96998 0.82406,-1.63742 4.09175,2.19012 a 3.0449237,3.4930369 0 0 1 0.59248,-0.39241 z m 0.82406,6.88025 a 1.2918313,1.4819467 0 0 0 0,2.96389 1.2918313,1.4819467 0 0 0 0,-2.96389"
|
||||
bx:shape="cog 606.651 776.607 12.897 30.399 72.774 0.57 6 1@453a207a"
|
||||
id="path6" />
|
||||
<path
|
||||
d="m 292.40923,279.57662 h 9.36996 c 0.0569,0 0.10872,0.0312 0.14289,0.0799 v 1.39352 c -0.0342,0.0487 -0.0859,0.0799 -0.14289,0.0799 h -9.36996 c -0.10355,0 -0.18949,-0.10135 -0.18949,-0.22901 v -1.09532 c 0,-0.12766 0.0859,-0.22901 0.18949,-0.22901 z"
|
||||
style="vector-effect:non-scaling-stroke;fill:#ffff04;stroke:#000000;stroke-width:9.04066px;paint-order:stroke"
|
||||
id="path3" />
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.1 KiB |
|
|
@ -1,7 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="238.748 141.34 30.063 30.063">
|
||||
<g transform="matrix(1, 0, 0, 1, 238.810587, 139.339917)">
|
||||
<path style="fill: none; stroke: rgb(0, 53, 255); stroke-dashoffset: -3.02px; stroke-miterlimit: 4.56; stroke-linejoin: round; stroke-width: 5px; stroke-linecap: round;" d="M 27.431 24.348 C 26.218 14.419 15.699 -1.564 6.007 17.415"/>
|
||||
<g transform="matrix(1, 0, 0, 1, 238.805415, 139.339917)">
|
||||
<path d="M 0.5 2 L 5.5 2 L 6 2.5 L 6 25.5 L 6.5 26 L 29.5 26 L 30 26.5 L 30 31.5 L 29.5 32 Q 28.25 32.25 28 31.5 L 28 28.5 L 27.5 28 L 18.5 28 L 18 28.5 L 18 31.5 L 17.5 32 Q 16.25 32.25 16 31.5 L 16 28.5 L 15.5 28 L 6.5 28 L 6 28.5 L 6 31.5 L 5.5 32 Q 4.25 32.25 4 31.5 L 4 28.5 L 3.5 28 L 0.5 28 L 0 27.5 Q -0.25 26.25 0.5 26 L 3.5 26 L 4 25.5 L 4 16.5 L 3.5 16 L 0.5 16 L 0 15.5 Q -0.25 14.25 0.5 14 L 3.5 14 L 4 13.5 L 4 4.5 L 3.5 4 L 0.5 4 L 0 3.5 Q -0.25 2.25 0.5 2 Z " style="fill: #000; stroke: #000;"/>
|
||||
<rect x="6.528" y="18.289" width="1.727" height="1.727" style="stroke: rgb(0, 0, 0); fill: rgb(14, 34, 255); vector-effect: non-scaling-stroke; stroke-width: 4px;"/>
|
||||
<rect x="7.863" y="15.654" width="1.727" height="1.727" style="stroke: rgb(0, 0, 0); fill: rgb(14, 34, 255); vector-effect: non-scaling-stroke; stroke-width: 4px;"/>
|
||||
<rect x="8.62" y="13.009" width="1.727" height="1.727" style="stroke: rgb(0, 0, 0); fill: rgb(14, 34, 255); vector-effect: non-scaling-stroke; stroke-width: 4px;"/>
|
||||
<rect x="10.304" y="10.331" width="1.727" height="1.727" style="stroke: rgb(0, 0, 0); fill: rgb(14, 34, 255); vector-effect: non-scaling-stroke; stroke-width: 4px;"/>
|
||||
<rect x="12.068" y="7.837" width="1.727" height="1.727" style="stroke: rgb(0, 0, 0); fill: rgb(14, 34, 255); vector-effect: non-scaling-stroke; stroke-width: 4px;"/>
|
||||
<rect x="-27.558" y="20.993" width="1.727" height="1.727" style="stroke: rgb(0, 0, 0); fill: rgb(14, 34, 255); vector-effect: non-scaling-stroke; stroke-width: 4px;" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||
<rect x="-26.94" y="18.31" width="1.727" height="1.727" style="stroke: rgb(0, 0, 0); fill: rgb(14, 34, 255); vector-effect: non-scaling-stroke; stroke-width: 4px;" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||
<rect x="-26.062" y="15.684" width="1.727" height="1.727" style="stroke: rgb(0, 0, 0); fill: rgb(14, 34, 255); vector-effect: non-scaling-stroke; stroke-width: 4px;" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||
<rect x="-25.197" y="13.078" width="1.727" height="1.727" style="stroke: rgb(0, 0, 0); fill: rgb(14, 34, 255); vector-effect: non-scaling-stroke; stroke-width: 4px;" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||
<rect x="-24.027" y="10.349" width="1.727" height="1.773" style="stroke: rgb(0, 0, 0); fill: rgb(14, 34, 255); vector-effect: non-scaling-stroke; stroke-width: 4px;" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||
<rect x="-28.154" y="23.535" width="1.727" height="1.727" style="stroke: rgb(0, 0, 0); fill: rgb(14, 34, 255); vector-effect: non-scaling-stroke; stroke-width: 4px;" transform="matrix(-1, 0, 0, 1, 0, 0)"/>
|
||||
<rect x="20.967" y="7.965" width="1.727" height="1.727" style="stroke: rgb(0, 0, 0); fill: rgb(14, 34, 255); vector-effect: non-scaling-stroke; stroke-width: 4px;"/>
|
||||
<rect x="14.443" y="5.769" width="1.727" height="1.727" style="stroke: rgb(0, 0, 0); fill: rgb(14, 34, 255); vector-effect: non-scaling-stroke; stroke-width: 4px;"/>
|
||||
<rect x="19.226" y="5.898" width="1.727" height="1.727" style="stroke: rgb(0, 0, 0); fill: rgb(14, 34, 255); vector-effect: non-scaling-stroke; stroke-width: 4px;"/>
|
||||
<rect x="16.725" y="4.318" width="1.727" height="1.727" style="stroke: rgb(0, 0, 0); fill: rgb(14, 34, 255); vector-effect: non-scaling-stroke; stroke-width: 4px;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 948 B After Width: | Height: | Size: 3.4 KiB |
|
|
@ -4,9 +4,12 @@ mod doc_canvas;
|
|||
|
||||
use doc_canvas::{CanvasMessage, Tool};
|
||||
use femm_doc_mag::FemmDoc;
|
||||
use femm_doc_mag::ans::MagSolution;
|
||||
use femm_doc_mag::mesh::Mesh;
|
||||
use iced::widget::{button, column, container, row, text};
|
||||
use iced::{Element, Length, Task};
|
||||
use std::ffi::CString;
|
||||
use std::path::Path;
|
||||
|
||||
const DEMO_FEM: &str = include_str!("../assets/demo.fem");
|
||||
const ADD_TOLERANCE: f64 = 0.5;
|
||||
|
|
@ -18,11 +21,13 @@ enum Message {
|
|||
SelectTool(Tool),
|
||||
Canvas(CanvasMessage),
|
||||
RunMesh,
|
||||
RunAnalyze,
|
||||
}
|
||||
|
||||
struct App {
|
||||
doc: FemmDoc,
|
||||
mesh: Option<Mesh>,
|
||||
solution: Option<MagSolution>,
|
||||
source_label: String,
|
||||
status: String,
|
||||
tool: Tool,
|
||||
|
|
@ -34,6 +39,7 @@ impl App {
|
|||
let app = App {
|
||||
doc,
|
||||
mesh: None,
|
||||
solution: None,
|
||||
source_label: String::from("demo.fem (embedded)"),
|
||||
status: String::new(),
|
||||
tool: Tool::Select,
|
||||
|
|
@ -63,6 +69,7 @@ impl App {
|
|||
Ok(d) => {
|
||||
self.doc = d;
|
||||
self.mesh = None;
|
||||
self.solution = None;
|
||||
self.source_label = label;
|
||||
self.status = String::new();
|
||||
}
|
||||
|
|
@ -83,20 +90,52 @@ impl App {
|
|||
}
|
||||
Err(e) => {
|
||||
self.mesh = None;
|
||||
self.solution = None;
|
||||
self.status = format!("mesh failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::RunAnalyze => {
|
||||
self.solution = None;
|
||||
match run_mesh(&self.doc) {
|
||||
Ok(m) => self.mesh = Some(m),
|
||||
Err(e) => {
|
||||
self.mesh = None;
|
||||
self.status = format!("mesh failed: {e}");
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
let stem = active_stem();
|
||||
if let Err(e) = run_solve(&stem) {
|
||||
self.status = format!("solve failed: {e}");
|
||||
return Task::none();
|
||||
}
|
||||
let ans_path = stem.with_extension("ans");
|
||||
match MagSolution::open(&ans_path) {
|
||||
Ok(sol) => {
|
||||
self.status = format!(
|
||||
"solved: {} mesh nodes, {} elements",
|
||||
sol.mesh_nodes.len(), sol.mesh_elements.len(),
|
||||
);
|
||||
self.solution = Some(sol);
|
||||
}
|
||||
Err(e) => {
|
||||
self.status = format!("read .ans failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Canvas(CanvasMessage::Click { world, tool }) => {
|
||||
match tool {
|
||||
Tool::AddNode => {
|
||||
let idx = self.doc.add_node(world.0, world.1, ADD_TOLERANCE);
|
||||
self.mesh = None;
|
||||
self.solution = None;
|
||||
self.status = format!("node {idx} at ({:.3}, {:.3})", world.0, world.1);
|
||||
}
|
||||
Tool::AddBlockLabel => {
|
||||
let idx = self.doc.add_block_label(world.0, world.1, ADD_TOLERANCE);
|
||||
self.mesh = None;
|
||||
self.solution = None;
|
||||
self.status = format!("block label {idx} at ({:.3}, {:.3})", world.0, world.1);
|
||||
}
|
||||
Tool::Select | Tool::AddSegment => {}
|
||||
|
|
@ -153,6 +192,7 @@ impl App {
|
|||
tool_button("Add Segment", Tool::AddSegment, self.tool),
|
||||
tool_button("Add Label", Tool::AddBlockLabel, self.tool),
|
||||
button("Mesh").on_press(Message::RunMesh),
|
||||
button("Analyze").on_press(Message::RunAnalyze),
|
||||
text(&self.source_label).size(13),
|
||||
stats,
|
||||
]
|
||||
|
|
@ -174,11 +214,16 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
/// stem path used by all mesh and solve operations for the active doc.
|
||||
fn active_stem() -> std::path::PathBuf {
|
||||
let dir = std::env::temp_dir().join("femm42-mesh");
|
||||
std::fs::create_dir_all(&dir).ok();
|
||||
dir.join("active")
|
||||
}
|
||||
|
||||
/// 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 stem = active_stem();
|
||||
let fem_path = stem.with_extension("fem");
|
||||
let poly_path = stem.with_extension("poly");
|
||||
|
||||
|
|
@ -204,11 +249,51 @@ fn run_mesh(doc: &FemmDoc) -> Result<Mesh, String> {
|
|||
Mesh::load(&stem).map_err(|e| format!("read mesh: {e}"))
|
||||
}
|
||||
|
||||
/// resolves the Triangle binary path, preferring the build dir then $PATH.
|
||||
/// runs the magnetostatic solver FFI pipeline against an on-disk .fem + .node + .ele rooted at `stem`.
|
||||
fn run_solve(stem: &Path) -> Result<(), String> {
|
||||
let stem_str = stem.to_str().ok_or_else(|| String::from("non-utf8 path"))?;
|
||||
let cstem = CString::new(stem_str).map_err(|e| format!("path: {e}"))?;
|
||||
|
||||
unsafe {
|
||||
let doc = femm_sys::femm_mag_doc_new();
|
||||
if doc.is_null() {
|
||||
return Err(String::from("femm_mag_doc_new returned null"));
|
||||
}
|
||||
let load = femm_sys::femm_mag_doc_load_fem(doc, cstem.as_ptr());
|
||||
if load == 0 {
|
||||
femm_sys::femm_mag_doc_free(doc);
|
||||
return Err(String::from("load_fem returned 0"));
|
||||
}
|
||||
if femm_sys::femm_mag_doc_load_mesh(doc) == 0 {
|
||||
femm_sys::femm_mag_doc_free(doc);
|
||||
return Err(String::from("load_mesh returned 0"));
|
||||
}
|
||||
if femm_sys::femm_mag_doc_renumber(doc) == 0 {
|
||||
femm_sys::femm_mag_doc_free(doc);
|
||||
return Err(String::from("renumber returned 0"));
|
||||
}
|
||||
if femm_sys::femm_mag_doc_solve(doc) == 0 {
|
||||
femm_sys::femm_mag_doc_free(doc);
|
||||
return Err(String::from("solve returned 0"));
|
||||
}
|
||||
femm_sys::femm_mag_doc_free(doc);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// resolves the Triangle binary path.
|
||||
/// search order: sibling of the running exe (bundled .app), repo build dir under target/.., FEMM_TRIANGLE.
|
||||
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(exe) = std::env::current_exe() {
|
||||
if let Some(parent) = exe.parent() {
|
||||
let bundled = parent.join("triangle");
|
||||
if bundled.is_file() { return Some(bundled); }
|
||||
let dev = parent.join("../../build/triangle/triangle");
|
||||
if let Ok(canon) = dev.canonicalize() {
|
||||
if canon.is_file() { return Some(canon); }
|
||||
}
|
||||
}
|
||||
}
|
||||
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); }
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ pub mod props;
|
|||
pub mod parser;
|
||||
pub mod writer;
|
||||
pub mod edit;
|
||||
pub mod poly;
|
||||
pub mod mesh;
|
||||
|
||||
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 .node and .ele next to the given path stem.
|
||||
pub fn load(stem: impl AsRef<Path>) -> Result<Self, MeshLoadError> {
|
||||
let stem = stem.as_ref();
|
||||
let node_path = with_extension(stem, "node");
|
||||
let ele_path = with_extension(stem, "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,166 @@
|
|||
//! .poly emitter consumed by the Triangle mesher.
|
||||
|
||||
use crate::FemmDoc;
|
||||
use std::fmt::Write;
|
||||
|
||||
impl FemmDoc {
|
||||
/// renders the doc geometry to the Triangle .poly text format, returning the file contents.
|
||||
pub fn write_poly(&self) -> String {
|
||||
let mut out = String::new();
|
||||
let bbox_diag = self.bbox_diagonal();
|
||||
let default_area = if bbox_diag > 0.0 { bbox_diag } else { -1.0 };
|
||||
|
||||
writeln!(out, "{}\t2\t0\t1", self.nodes.len()).unwrap();
|
||||
for (i, n) in self.nodes.iter().enumerate() {
|
||||
let marker = point_marker_index(self, &n.boundary_marker);
|
||||
writeln!(out, "{i}\t{:.17e}\t{:.17e}\t{marker}", n.x, n.y).unwrap();
|
||||
}
|
||||
|
||||
writeln!(out, "{}\t1", self.segments.len()).unwrap();
|
||||
for (i, s) in self.segments.iter().enumerate() {
|
||||
let marker = -boundary_marker_index(self, &s.boundary_marker);
|
||||
writeln!(out, "{i}\t{}\t{}\t{marker}", s.n0, s.n1).unwrap();
|
||||
}
|
||||
|
||||
let holes: Vec<&_> = self.block_labels.iter().filter(|b| b.block_type == "<No Mesh>").collect();
|
||||
writeln!(out, "{}", holes.len()).unwrap();
|
||||
for (i, h) in holes.iter().enumerate() {
|
||||
writeln!(out, "{i}\t{:.17e}\t{:.17e}", h.x, h.y).unwrap();
|
||||
}
|
||||
|
||||
let regions: Vec<&_> = self.block_labels.iter().filter(|b| b.block_type != "<No Mesh>").collect();
|
||||
writeln!(out, "{}", regions.len()).unwrap();
|
||||
for (i, r) in regions.iter().enumerate() {
|
||||
let attr = (i as i32) + 1;
|
||||
let max_area = if r.max_area > 0.0 && r.max_area < default_area {
|
||||
r.max_area
|
||||
} else {
|
||||
default_area
|
||||
};
|
||||
writeln!(out, "{i}\t{:.17e}\t{:.17e}\t{attr}\t{:.17e}", r.x, r.y, max_area).unwrap();
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
/// writes the .poly text to disk at the given path.
|
||||
pub fn save_poly(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
|
||||
std::fs::write(path, self.write_poly())
|
||||
}
|
||||
|
||||
fn bbox_diagonal(&self) -> f64 {
|
||||
if self.nodes.len() < 2 { return 0.0; }
|
||||
let mut xmin = f64::INFINITY;
|
||||
let mut xmax = f64::NEG_INFINITY;
|
||||
let mut ymin = f64::INFINITY;
|
||||
let mut ymax = f64::NEG_INFINITY;
|
||||
for n in &self.nodes {
|
||||
xmin = xmin.min(n.x); xmax = xmax.max(n.x);
|
||||
ymin = ymin.min(n.y); ymax = ymax.max(n.y);
|
||||
}
|
||||
let dx = xmax - xmin;
|
||||
let dy = ymax - ymin;
|
||||
(dx * dx + dy * dy).sqrt()
|
||||
}
|
||||
}
|
||||
|
||||
/// resolves a node's boundary-marker name to Triangle's marker integer (0 for unmarked).
|
||||
fn point_marker_index(doc: &FemmDoc, name: &str) -> i32 {
|
||||
if name.is_empty() { return 0; }
|
||||
for (i, p) in doc.points.iter().enumerate() {
|
||||
if p.name == name { return (i as i32) + 2; }
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
/// resolves a segment's boundary-marker name to Triangle's marker integer, returned positive.
|
||||
/// the caller negates for Triangle's segment-marker convention.
|
||||
fn boundary_marker_index(doc: &FemmDoc, name: &str) -> i32 {
|
||||
if name.is_empty() { return 0; }
|
||||
for (i, b) in doc.boundaries.iter().enumerate() {
|
||||
if b.name == name { return (i as i32) + 2; }
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{BoundaryProp, PointProp, BlockLabel};
|
||||
|
||||
#[test]
|
||||
fn write_poly_header_lines_count_correctly() {
|
||||
// four-corner square with one block label inside; verify the section headers and counts.
|
||||
let mut d = FemmDoc::default();
|
||||
d.points.push(PointProp { name: "V=0".into(), ..PointProp::default() });
|
||||
d.boundaries.push(BoundaryProp { name: "outer".into(), ..BoundaryProp::default() });
|
||||
d.add_node(0.0, 0.0, 0.0);
|
||||
d.add_node(1.0, 0.0, 0.0);
|
||||
d.add_node(1.0, 1.0, 0.0);
|
||||
d.add_node(0.0, 1.0, 0.0);
|
||||
d.nodes[0].boundary_marker = "V=0".into();
|
||||
d.add_segment_with_marker(0, 1, "outer");
|
||||
d.add_segment_with_marker(1, 2, "outer");
|
||||
d.add_segment_with_marker(2, 3, "outer");
|
||||
d.add_segment_with_marker(3, 0, "outer");
|
||||
d.block_labels.push(BlockLabel {
|
||||
x: 0.5, y: 0.5,
|
||||
max_area: 0.0,
|
||||
block_type: "Air".into(),
|
||||
..BlockLabel::default()
|
||||
});
|
||||
|
||||
let poly = d.write_poly();
|
||||
let lines: Vec<&str> = poly.lines().collect();
|
||||
|
||||
assert!(lines[0].starts_with("4\t2\t0\t1"), "node header: {}", lines[0]);
|
||||
assert!(lines[5].starts_with("4\t1"), "segment header: {}", lines[5]);
|
||||
assert_eq!(lines[10], "0", "no holes section: {}", lines[10]);
|
||||
assert_eq!(lines[11], "1", "one region: {}", lines[11]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_poly_marks_nodes_and_segments_by_property_index() {
|
||||
// first point property listed -> marker 2; first boundary property listed -> -2 on segment.
|
||||
let mut d = FemmDoc::default();
|
||||
d.points.push(PointProp { name: "V=0".into(), ..PointProp::default() });
|
||||
d.boundaries.push(BoundaryProp { name: "outer".into(), ..BoundaryProp::default() });
|
||||
d.add_node(0.0, 0.0, 0.0);
|
||||
d.add_node(1.0, 0.0, 0.0);
|
||||
d.nodes[0].boundary_marker = "V=0".into();
|
||||
d.add_segment_with_marker(0, 1, "outer");
|
||||
|
||||
let poly = d.write_poly();
|
||||
let lines: Vec<&str> = poly.lines().collect();
|
||||
|
||||
assert!(lines[1].ends_with("\t2"), "node 0 marker = 2: {}", lines[1]);
|
||||
assert!(lines[2].ends_with("\t0"), "node 1 marker = 0: {}", lines[2]);
|
||||
assert!(lines[4].ends_with("\t-2"), "segment 0 marker = -2: {}", lines[4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_poly_holes_partitioned_from_regions() {
|
||||
// one "<No Mesh>" label counts as a hole; everything else is a region.
|
||||
let mut d = FemmDoc::default();
|
||||
d.add_node(0.0, 0.0, 0.0);
|
||||
d.add_node(1.0, 0.0, 0.0);
|
||||
d.block_labels.push(BlockLabel {
|
||||
x: 0.5, y: 0.5,
|
||||
block_type: "<No Mesh>".into(),
|
||||
..BlockLabel::default()
|
||||
});
|
||||
d.block_labels.push(BlockLabel {
|
||||
x: 0.2, y: 0.2,
|
||||
block_type: "Air".into(),
|
||||
..BlockLabel::default()
|
||||
});
|
||||
|
||||
let poly = d.write_poly();
|
||||
let lines: Vec<&str> = poly.lines().collect();
|
||||
|
||||
// node header (0), 2 node rows (1,2), segment header "0\t1" (3),
|
||||
// hole count (4), 1 hole row (5), region count (6), 1 region row (7).
|
||||
assert_eq!(lines[4], "1", "one hole: {}", lines[4]);
|
||||
assert_eq!(lines[6], "1", "one region: {}", lines[6]);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,8 @@ pub mod props;
|
|||
pub mod parser;
|
||||
pub mod writer;
|
||||
pub mod edit;
|
||||
pub mod poly;
|
||||
pub mod mesh;
|
||||
|
||||
pub use geom::{ArcSegment, BlockLabel, Node, Segment};
|
||||
pub use props::{BoundaryProp, ConductorProp, MaterialProp, PointProp};
|
||||
|
|
|
|||
|
|
@ -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 .node and .ele next to the given path stem.
|
||||
pub fn load(stem: impl AsRef<Path>) -> Result<Self, MeshLoadError> {
|
||||
let stem = stem.as_ref();
|
||||
let node_path = with_extension(stem, "node");
|
||||
let ele_path = with_extension(stem, "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,166 @@
|
|||
//! .poly emitter consumed by the Triangle mesher.
|
||||
|
||||
use crate::FemmDoc;
|
||||
use std::fmt::Write;
|
||||
|
||||
impl FemmDoc {
|
||||
/// renders the doc geometry to the Triangle .poly text format, returning the file contents.
|
||||
pub fn write_poly(&self) -> String {
|
||||
let mut out = String::new();
|
||||
let bbox_diag = self.bbox_diagonal();
|
||||
let default_area = if bbox_diag > 0.0 { bbox_diag } else { -1.0 };
|
||||
|
||||
writeln!(out, "{}\t2\t0\t1", self.nodes.len()).unwrap();
|
||||
for (i, n) in self.nodes.iter().enumerate() {
|
||||
let marker = point_marker_index(self, &n.boundary_marker);
|
||||
writeln!(out, "{i}\t{:.17e}\t{:.17e}\t{marker}", n.x, n.y).unwrap();
|
||||
}
|
||||
|
||||
writeln!(out, "{}\t1", self.segments.len()).unwrap();
|
||||
for (i, s) in self.segments.iter().enumerate() {
|
||||
let marker = -boundary_marker_index(self, &s.boundary_marker);
|
||||
writeln!(out, "{i}\t{}\t{}\t{marker}", s.n0, s.n1).unwrap();
|
||||
}
|
||||
|
||||
let holes: Vec<&_> = self.block_labels.iter().filter(|b| b.block_type == "<No Mesh>").collect();
|
||||
writeln!(out, "{}", holes.len()).unwrap();
|
||||
for (i, h) in holes.iter().enumerate() {
|
||||
writeln!(out, "{i}\t{:.17e}\t{:.17e}", h.x, h.y).unwrap();
|
||||
}
|
||||
|
||||
let regions: Vec<&_> = self.block_labels.iter().filter(|b| b.block_type != "<No Mesh>").collect();
|
||||
writeln!(out, "{}", regions.len()).unwrap();
|
||||
for (i, r) in regions.iter().enumerate() {
|
||||
let attr = (i as i32) + 1;
|
||||
let max_area = if r.max_area > 0.0 && r.max_area < default_area {
|
||||
r.max_area
|
||||
} else {
|
||||
default_area
|
||||
};
|
||||
writeln!(out, "{i}\t{:.17e}\t{:.17e}\t{attr}\t{:.17e}", r.x, r.y, max_area).unwrap();
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
/// writes the .poly text to disk at the given path.
|
||||
pub fn save_poly(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
|
||||
std::fs::write(path, self.write_poly())
|
||||
}
|
||||
|
||||
fn bbox_diagonal(&self) -> f64 {
|
||||
if self.nodes.len() < 2 { return 0.0; }
|
||||
let mut xmin = f64::INFINITY;
|
||||
let mut xmax = f64::NEG_INFINITY;
|
||||
let mut ymin = f64::INFINITY;
|
||||
let mut ymax = f64::NEG_INFINITY;
|
||||
for n in &self.nodes {
|
||||
xmin = xmin.min(n.x); xmax = xmax.max(n.x);
|
||||
ymin = ymin.min(n.y); ymax = ymax.max(n.y);
|
||||
}
|
||||
let dx = xmax - xmin;
|
||||
let dy = ymax - ymin;
|
||||
(dx * dx + dy * dy).sqrt()
|
||||
}
|
||||
}
|
||||
|
||||
/// resolves a node's boundary-marker name to Triangle's marker integer (0 for unmarked).
|
||||
fn point_marker_index(doc: &FemmDoc, name: &str) -> i32 {
|
||||
if name.is_empty() { return 0; }
|
||||
for (i, p) in doc.points.iter().enumerate() {
|
||||
if p.name == name { return (i as i32) + 2; }
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
/// resolves a segment's boundary-marker name to Triangle's marker integer, returned positive.
|
||||
/// the caller negates for Triangle's segment-marker convention.
|
||||
fn boundary_marker_index(doc: &FemmDoc, name: &str) -> i32 {
|
||||
if name.is_empty() { return 0; }
|
||||
for (i, b) in doc.boundaries.iter().enumerate() {
|
||||
if b.name == name { return (i as i32) + 2; }
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{BoundaryProp, PointProp, BlockLabel};
|
||||
|
||||
#[test]
|
||||
fn write_poly_header_lines_count_correctly() {
|
||||
// four-corner square with one block label inside; verify the section headers and counts.
|
||||
let mut d = FemmDoc::default();
|
||||
d.points.push(PointProp { name: "V=0".into(), ..PointProp::default() });
|
||||
d.boundaries.push(BoundaryProp { name: "outer".into(), ..BoundaryProp::default() });
|
||||
d.add_node(0.0, 0.0, 0.0);
|
||||
d.add_node(1.0, 0.0, 0.0);
|
||||
d.add_node(1.0, 1.0, 0.0);
|
||||
d.add_node(0.0, 1.0, 0.0);
|
||||
d.nodes[0].boundary_marker = "V=0".into();
|
||||
d.add_segment_with_marker(0, 1, "outer");
|
||||
d.add_segment_with_marker(1, 2, "outer");
|
||||
d.add_segment_with_marker(2, 3, "outer");
|
||||
d.add_segment_with_marker(3, 0, "outer");
|
||||
d.block_labels.push(BlockLabel {
|
||||
x: 0.5, y: 0.5,
|
||||
max_area: 0.0,
|
||||
block_type: "Air".into(),
|
||||
..BlockLabel::default()
|
||||
});
|
||||
|
||||
let poly = d.write_poly();
|
||||
let lines: Vec<&str> = poly.lines().collect();
|
||||
|
||||
assert!(lines[0].starts_with("4\t2\t0\t1"), "node header: {}", lines[0]);
|
||||
assert!(lines[5].starts_with("4\t1"), "segment header: {}", lines[5]);
|
||||
assert_eq!(lines[10], "0", "no holes section: {}", lines[10]);
|
||||
assert_eq!(lines[11], "1", "one region: {}", lines[11]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_poly_marks_nodes_and_segments_by_property_index() {
|
||||
// first point property listed -> marker 2; first boundary property listed -> -2 on segment.
|
||||
let mut d = FemmDoc::default();
|
||||
d.points.push(PointProp { name: "V=0".into(), ..PointProp::default() });
|
||||
d.boundaries.push(BoundaryProp { name: "outer".into(), ..BoundaryProp::default() });
|
||||
d.add_node(0.0, 0.0, 0.0);
|
||||
d.add_node(1.0, 0.0, 0.0);
|
||||
d.nodes[0].boundary_marker = "V=0".into();
|
||||
d.add_segment_with_marker(0, 1, "outer");
|
||||
|
||||
let poly = d.write_poly();
|
||||
let lines: Vec<&str> = poly.lines().collect();
|
||||
|
||||
assert!(lines[1].ends_with("\t2"), "node 0 marker = 2: {}", lines[1]);
|
||||
assert!(lines[2].ends_with("\t0"), "node 1 marker = 0: {}", lines[2]);
|
||||
assert!(lines[4].ends_with("\t-2"), "segment 0 marker = -2: {}", lines[4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_poly_holes_partitioned_from_regions() {
|
||||
// one "<No Mesh>" label counts as a hole; everything else falls into the regions section.
|
||||
let mut d = FemmDoc::default();
|
||||
d.add_node(0.0, 0.0, 0.0);
|
||||
d.add_node(1.0, 0.0, 0.0);
|
||||
d.block_labels.push(BlockLabel {
|
||||
x: 0.5, y: 0.5,
|
||||
block_type: "<No Mesh>".into(),
|
||||
..BlockLabel::default()
|
||||
});
|
||||
d.block_labels.push(BlockLabel {
|
||||
x: 0.2, y: 0.2,
|
||||
block_type: "Air".into(),
|
||||
..BlockLabel::default()
|
||||
});
|
||||
|
||||
let poly = d.write_poly();
|
||||
let lines: Vec<&str> = poly.lines().collect();
|
||||
|
||||
// node header (0), 2 node rows (1,2), segment header "0\t1" (3),
|
||||
// hole count (4), 1 hole row (5), region count (6), 1 region row (7).
|
||||
assert_eq!(lines[4], "1", "one hole: {}", lines[4]);
|
||||
assert_eq!(lines[6], "1", "one region: {}", lines[6]);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,8 @@ pub mod props;
|
|||
pub mod parser;
|
||||
pub mod writer;
|
||||
pub mod edit;
|
||||
pub mod poly;
|
||||
pub mod mesh;
|
||||
|
||||
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 .node and .ele next to the given path stem.
|
||||
pub fn load(stem: impl AsRef<Path>) -> Result<Self, MeshLoadError> {
|
||||
let stem = stem.as_ref();
|
||||
let node_path = with_extension(stem, "node");
|
||||
let ele_path = with_extension(stem, "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,166 @@
|
|||
//! .poly emitter consumed by the Triangle mesher.
|
||||
|
||||
use crate::FemmDoc;
|
||||
use std::fmt::Write;
|
||||
|
||||
impl FemmDoc {
|
||||
/// renders the doc geometry to the Triangle .poly text format, returning the file contents.
|
||||
pub fn write_poly(&self) -> String {
|
||||
let mut out = String::new();
|
||||
let bbox_diag = self.bbox_diagonal();
|
||||
let default_area = if bbox_diag > 0.0 { bbox_diag } else { -1.0 };
|
||||
|
||||
writeln!(out, "{}\t2\t0\t1", self.nodes.len()).unwrap();
|
||||
for (i, n) in self.nodes.iter().enumerate() {
|
||||
let marker = point_marker_index(self, &n.boundary_marker);
|
||||
writeln!(out, "{i}\t{:.17e}\t{:.17e}\t{marker}", n.x, n.y).unwrap();
|
||||
}
|
||||
|
||||
writeln!(out, "{}\t1", self.segments.len()).unwrap();
|
||||
for (i, s) in self.segments.iter().enumerate() {
|
||||
let marker = -boundary_marker_index(self, &s.boundary_marker);
|
||||
writeln!(out, "{i}\t{}\t{}\t{marker}", s.n0, s.n1).unwrap();
|
||||
}
|
||||
|
||||
let holes: Vec<&_> = self.block_labels.iter().filter(|b| b.block_type == "<No Mesh>").collect();
|
||||
writeln!(out, "{}", holes.len()).unwrap();
|
||||
for (i, h) in holes.iter().enumerate() {
|
||||
writeln!(out, "{i}\t{:.17e}\t{:.17e}", h.x, h.y).unwrap();
|
||||
}
|
||||
|
||||
let regions: Vec<&_> = self.block_labels.iter().filter(|b| b.block_type != "<No Mesh>").collect();
|
||||
writeln!(out, "{}", regions.len()).unwrap();
|
||||
for (i, r) in regions.iter().enumerate() {
|
||||
let attr = (i as i32) + 1;
|
||||
let max_area = if r.max_area > 0.0 && r.max_area < default_area {
|
||||
r.max_area
|
||||
} else {
|
||||
default_area
|
||||
};
|
||||
writeln!(out, "{i}\t{:.17e}\t{:.17e}\t{attr}\t{:.17e}", r.x, r.y, max_area).unwrap();
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
/// writes the .poly text to disk at the given path.
|
||||
pub fn save_poly(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
|
||||
std::fs::write(path, self.write_poly())
|
||||
}
|
||||
|
||||
fn bbox_diagonal(&self) -> f64 {
|
||||
if self.nodes.len() < 2 { return 0.0; }
|
||||
let mut xmin = f64::INFINITY;
|
||||
let mut xmax = f64::NEG_INFINITY;
|
||||
let mut ymin = f64::INFINITY;
|
||||
let mut ymax = f64::NEG_INFINITY;
|
||||
for n in &self.nodes {
|
||||
xmin = xmin.min(n.x); xmax = xmax.max(n.x);
|
||||
ymin = ymin.min(n.y); ymax = ymax.max(n.y);
|
||||
}
|
||||
let dx = xmax - xmin;
|
||||
let dy = ymax - ymin;
|
||||
(dx * dx + dy * dy).sqrt()
|
||||
}
|
||||
}
|
||||
|
||||
/// resolves a node's boundary-marker name to Triangle's marker integer (0 for unmarked).
|
||||
fn point_marker_index(doc: &FemmDoc, name: &str) -> i32 {
|
||||
if name.is_empty() { return 0; }
|
||||
for (i, p) in doc.points.iter().enumerate() {
|
||||
if p.name == name { return (i as i32) + 2; }
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
/// resolves a segment's boundary-marker name to Triangle's marker integer, returned positive.
|
||||
/// the caller negates for Triangle's segment-marker convention.
|
||||
fn boundary_marker_index(doc: &FemmDoc, name: &str) -> i32 {
|
||||
if name.is_empty() { return 0; }
|
||||
for (i, b) in doc.boundaries.iter().enumerate() {
|
||||
if b.name == name { return (i as i32) + 2; }
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{BoundaryProp, PointProp, BlockLabel};
|
||||
|
||||
#[test]
|
||||
fn write_poly_header_lines_count_correctly() {
|
||||
// four-corner square with one block label inside; verify the section headers and counts.
|
||||
let mut d = FemmDoc::default();
|
||||
d.points.push(PointProp { name: "T=0".into(), ..PointProp::default() });
|
||||
d.boundaries.push(BoundaryProp { name: "outer".into(), ..BoundaryProp::default() });
|
||||
d.add_node(0.0, 0.0, 0.0);
|
||||
d.add_node(1.0, 0.0, 0.0);
|
||||
d.add_node(1.0, 1.0, 0.0);
|
||||
d.add_node(0.0, 1.0, 0.0);
|
||||
d.nodes[0].boundary_marker = "T=0".into();
|
||||
d.add_segment_with_marker(0, 1, "outer");
|
||||
d.add_segment_with_marker(1, 2, "outer");
|
||||
d.add_segment_with_marker(2, 3, "outer");
|
||||
d.add_segment_with_marker(3, 0, "outer");
|
||||
d.block_labels.push(BlockLabel {
|
||||
x: 0.5, y: 0.5,
|
||||
max_area: 0.0,
|
||||
block_type: "Air".into(),
|
||||
..BlockLabel::default()
|
||||
});
|
||||
|
||||
let poly = d.write_poly();
|
||||
let lines: Vec<&str> = poly.lines().collect();
|
||||
|
||||
assert!(lines[0].starts_with("4\t2\t0\t1"), "node header: {}", lines[0]);
|
||||
assert!(lines[5].starts_with("4\t1"), "segment header: {}", lines[5]);
|
||||
assert_eq!(lines[10], "0", "no holes section: {}", lines[10]);
|
||||
assert_eq!(lines[11], "1", "one region: {}", lines[11]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_poly_marks_nodes_and_segments_by_property_index() {
|
||||
// first point property listed -> marker 2; first boundary property listed -> -2 on segment.
|
||||
let mut d = FemmDoc::default();
|
||||
d.points.push(PointProp { name: "T=0".into(), ..PointProp::default() });
|
||||
d.boundaries.push(BoundaryProp { name: "outer".into(), ..BoundaryProp::default() });
|
||||
d.add_node(0.0, 0.0, 0.0);
|
||||
d.add_node(1.0, 0.0, 0.0);
|
||||
d.nodes[0].boundary_marker = "T=0".into();
|
||||
d.add_segment_with_marker(0, 1, "outer");
|
||||
|
||||
let poly = d.write_poly();
|
||||
let lines: Vec<&str> = poly.lines().collect();
|
||||
|
||||
assert!(lines[1].ends_with("\t2"), "node 0 marker = 2: {}", lines[1]);
|
||||
assert!(lines[2].ends_with("\t0"), "node 1 marker = 0: {}", lines[2]);
|
||||
assert!(lines[4].ends_with("\t-2"), "segment 0 marker = -2: {}", lines[4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_poly_holes_partitioned_from_regions() {
|
||||
// one "<No Mesh>" label counts as a hole; everything else is a region.
|
||||
let mut d = FemmDoc::default();
|
||||
d.add_node(0.0, 0.0, 0.0);
|
||||
d.add_node(1.0, 0.0, 0.0);
|
||||
d.block_labels.push(BlockLabel {
|
||||
x: 0.5, y: 0.5,
|
||||
block_type: "<No Mesh>".into(),
|
||||
..BlockLabel::default()
|
||||
});
|
||||
d.block_labels.push(BlockLabel {
|
||||
x: 0.2, y: 0.2,
|
||||
block_type: "Air".into(),
|
||||
..BlockLabel::default()
|
||||
});
|
||||
|
||||
let poly = d.write_poly();
|
||||
let lines: Vec<&str> = poly.lines().collect();
|
||||
|
||||
// node header (0), 2 node rows (1,2), segment header "0\t1" (3),
|
||||
// hole count (4), 1 hole row (5), region count (6), 1 region row (7).
|
||||
assert_eq!(lines[4], "1", "one hole: {}", lines[4]);
|
||||
assert_eq!(lines[6], "1", "one region: {}", lines[6]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,422 @@
|
|||
//! .ans solution-file parser: reuses the .fem header grammar and adds the post-[Solution] mesh body.
|
||||
|
||||
use crate::{
|
||||
ACSolver, BoundaryProp, CircuitProp, Complex, Coords, FemmDoc, LengthUnit, MaterialProp,
|
||||
PointProp, ProblemType,
|
||||
};
|
||||
use std::path::Path;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AnsParseError {
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("header parse error: {0}")]
|
||||
Header(#[from] crate::parser::ParseError),
|
||||
#[error("missing [Solution] marker")]
|
||||
MissingSolution,
|
||||
#[error("unexpected end of file while reading {context}")]
|
||||
UnexpectedEof { context: &'static str },
|
||||
#[error("malformed row in {section}: {reason}")]
|
||||
MalformedRow { section: &'static str, reason: String },
|
||||
}
|
||||
|
||||
/// solver header attributes shared by every magnetostatic .ans file.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MagHeader {
|
||||
pub format: f64,
|
||||
pub frequency: f64,
|
||||
pub precision: f64,
|
||||
pub min_angle: f64,
|
||||
pub smart_mesh: bool,
|
||||
pub depth: f64,
|
||||
pub length_units: LengthUnit,
|
||||
pub ac_solver: ACSolver,
|
||||
pub problem_type: ProblemType,
|
||||
pub coords: Coords,
|
||||
pub comment: String,
|
||||
pub ext_ro: f64,
|
||||
pub ext_ri: f64,
|
||||
pub ext_zo: f64,
|
||||
}
|
||||
|
||||
/// one finite-element mesh node with the solved magnetic vector potential.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct SolMeshNode {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub a: Complex,
|
||||
}
|
||||
|
||||
/// one mesh triangle with three node indices, block label, and post-load flux/permeability slots.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct SolMeshElement {
|
||||
pub n: [u32; 3],
|
||||
pub lab: i32,
|
||||
pub b1: Complex,
|
||||
pub b2: Complex,
|
||||
pub mu1: Complex,
|
||||
pub mu2: Complex,
|
||||
}
|
||||
|
||||
/// per-block-label circuit assignment value sourced from the trailing .ans stanza.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct BlockCircuitValue {
|
||||
pub case: i32,
|
||||
pub value: Complex,
|
||||
}
|
||||
|
||||
/// parsed magnetostatic solution file: header, property tables, mesh body, and circuit body.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MagSolution {
|
||||
pub header: MagHeader,
|
||||
pub points: Vec<PointProp>,
|
||||
pub boundaries: Vec<BoundaryProp>,
|
||||
pub materials: Vec<MaterialProp>,
|
||||
pub circuits: Vec<CircuitProp>,
|
||||
pub mesh_nodes: Vec<SolMeshNode>,
|
||||
pub mesh_elements: Vec<SolMeshElement>,
|
||||
pub block_circuits: Vec<BlockCircuitValue>,
|
||||
}
|
||||
|
||||
impl MagSolution {
|
||||
/// parses a .ans text buffer into a magnetostatic solution.
|
||||
pub fn parse(src: &str) -> Result<Self, AnsParseError> {
|
||||
let (head, body) = split_at_solution(src).ok_or(AnsParseError::MissingSolution)?;
|
||||
let doc = FemmDoc::parse(head)?;
|
||||
let header = MagHeader {
|
||||
format: doc.format,
|
||||
frequency: doc.frequency.abs(),
|
||||
precision: doc.precision,
|
||||
min_angle: doc.min_angle,
|
||||
smart_mesh: doc.smart_mesh,
|
||||
depth: doc.depth,
|
||||
length_units: doc.length_units,
|
||||
ac_solver: doc.ac_solver,
|
||||
problem_type: doc.problem_type,
|
||||
coords: doc.coords,
|
||||
comment: doc.comment,
|
||||
ext_ro: doc.ext_ro,
|
||||
ext_ri: doc.ext_ri,
|
||||
ext_zo: doc.ext_zo,
|
||||
};
|
||||
let ac = header.frequency != 0.0;
|
||||
let mut lines = body.lines();
|
||||
|
||||
let nnodes = read_count(&mut lines, "mesh nodes count")?;
|
||||
let mut mesh_nodes = Vec::with_capacity(nnodes);
|
||||
for _ in 0..nnodes {
|
||||
let row = lines.next().ok_or(AnsParseError::UnexpectedEof { context: "mesh nodes" })?;
|
||||
mesh_nodes.push(parse_node_row(row, ac)?);
|
||||
}
|
||||
|
||||
let nels = read_count(&mut lines, "mesh elements count")?;
|
||||
let mut mesh_elements = Vec::with_capacity(nels);
|
||||
for _ in 0..nels {
|
||||
let row = lines.next().ok_or(AnsParseError::UnexpectedEof { context: "mesh elements" })?;
|
||||
mesh_elements.push(parse_element_row(row)?);
|
||||
}
|
||||
|
||||
let ncirc = read_count(&mut lines, "block-circuit rows count")?;
|
||||
let mut block_circuits = Vec::with_capacity(ncirc);
|
||||
for _ in 0..ncirc {
|
||||
let row = lines.next().ok_or(AnsParseError::UnexpectedEof { context: "block-circuit rows" })?;
|
||||
block_circuits.push(parse_block_circuit_row(row, ac)?);
|
||||
}
|
||||
|
||||
Ok(MagSolution {
|
||||
header,
|
||||
points: doc.points,
|
||||
boundaries: doc.boundaries,
|
||||
materials: doc.materials,
|
||||
circuits: doc.circuits,
|
||||
mesh_nodes,
|
||||
mesh_elements,
|
||||
block_circuits,
|
||||
})
|
||||
}
|
||||
|
||||
/// loads a .ans file from disk.
|
||||
pub fn open(path: impl AsRef<Path>) -> Result<Self, AnsParseError> {
|
||||
let text = std::fs::read_to_string(path)?;
|
||||
Self::parse(&text)
|
||||
}
|
||||
}
|
||||
|
||||
/// splits the raw text on the `[Solution]` marker into a header chunk and a body chunk.
|
||||
fn split_at_solution(src: &str) -> Option<(&str, &str)> {
|
||||
let lower_target = "[solution]";
|
||||
let mut offset = 0usize;
|
||||
for line in src.lines() {
|
||||
let line_len = line.len();
|
||||
let trimmed = line.trim_start();
|
||||
if trimmed.to_ascii_lowercase().starts_with(lower_target) {
|
||||
let head_end = offset;
|
||||
let body_start = offset + line_len;
|
||||
let body_start = skip_newline(src, body_start);
|
||||
return Some((&src[..head_end], &src[body_start..]));
|
||||
}
|
||||
offset += line_len;
|
||||
offset = skip_newline(src, offset);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn skip_newline(src: &str, mut i: usize) -> usize {
|
||||
let b = src.as_bytes();
|
||||
if i < b.len() && b[i] == b'\r' { i += 1; }
|
||||
if i < b.len() && b[i] == b'\n' { i += 1; }
|
||||
i
|
||||
}
|
||||
|
||||
/// reads the next non-blank line as a decimal integer count.
|
||||
fn read_count<'a, I: Iterator<Item = &'a str>>(
|
||||
lines: &mut I,
|
||||
ctx: &'static str,
|
||||
) -> Result<usize, AnsParseError> {
|
||||
for line in lines.by_ref() {
|
||||
let t = line.trim();
|
||||
if t.is_empty() { continue; }
|
||||
return t.parse::<usize>().map_err(|e| AnsParseError::MalformedRow {
|
||||
section: ctx,
|
||||
reason: format!("{e}: {t:?}"),
|
||||
});
|
||||
}
|
||||
Err(AnsParseError::UnexpectedEof { context: ctx })
|
||||
}
|
||||
|
||||
/// parses one mesh-node row, picking the column layout from the AC/DC flag.
|
||||
fn parse_node_row(line: &str, ac: bool) -> Result<SolMeshNode, AnsParseError> {
|
||||
let (x, rest) = take_f64(line);
|
||||
let (y, rest) = take_f64(rest);
|
||||
let (a_re, rest) = take_f64(rest);
|
||||
let a_im = if ac {
|
||||
let (v, _) = take_f64(rest);
|
||||
v
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
Ok(SolMeshNode { x, y, a: Complex::new(a_re, a_im) })
|
||||
}
|
||||
|
||||
/// parses one mesh-element row into three node indices plus a block-label index.
|
||||
fn parse_element_row(line: &str) -> Result<SolMeshElement, AnsParseError> {
|
||||
let (p0, rest) = take_i32(line);
|
||||
let (p1, rest) = take_i32(rest);
|
||||
let (p2, rest) = take_i32(rest);
|
||||
let (lab, _) = take_i32(rest);
|
||||
Ok(SolMeshElement {
|
||||
n: [p0 as u32, p1 as u32, p2 as u32],
|
||||
lab,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
/// parses one block-circuit row of `case value [imag]`.
|
||||
fn parse_block_circuit_row(line: &str, ac: bool) -> Result<BlockCircuitValue, AnsParseError> {
|
||||
let (case, rest) = take_i32(line);
|
||||
let (re, rest) = take_f64(rest);
|
||||
let im = if ac {
|
||||
let (v, _) = take_f64(rest);
|
||||
v
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
Ok(BlockCircuitValue { case, value: Complex::new(re, im) })
|
||||
}
|
||||
|
||||
/// peels one f64 off the head of a whitespace/tab/comma-separated row.
|
||||
fn take_f64(s: &str) -> (f64, &str) {
|
||||
let s = s.trim_start_matches(|c: char| c.is_whitespace() || c == ',');
|
||||
let end = s.find(|c: char| c.is_whitespace() || c == ',').unwrap_or(s.len());
|
||||
let (head, tail) = s.split_at(end);
|
||||
(head.parse::<f64>().unwrap_or(0.0), tail)
|
||||
}
|
||||
|
||||
/// peels one i32 off the head of a whitespace/tab/comma-separated row.
|
||||
fn take_i32(s: &str) -> (i32, &str) {
|
||||
let s = s.trim_start_matches(|c: char| c.is_whitespace() || c == ',');
|
||||
let end = s.find(|c: char| c.is_whitespace() || c == ',').unwrap_or(s.len());
|
||||
let (head, tail) = s.split_at(end);
|
||||
(head.parse::<i32>().unwrap_or(0), tail)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const DC_FIXTURE: &str = "\
|
||||
[Format] = 4.0
|
||||
[Frequency] = 0
|
||||
[Precision] = 1e-08
|
||||
[MinAngle] = 30
|
||||
[DoSmartMesh] = 1
|
||||
[Depth] = 1
|
||||
[LengthUnits] = millimeters
|
||||
[ProblemType] = planar
|
||||
[Coordinates] = cartesian
|
||||
[ACSolver] = 0
|
||||
[PrevType] = 0
|
||||
[PrevSoln] = \"\"
|
||||
[Comment] = \"dc fixture\"
|
||||
[PointProps] = 1
|
||||
<BeginPoint>
|
||||
<PointName> = \"A=0\"
|
||||
<I_re> = 0
|
||||
<I_im> = 0
|
||||
<A_re> = 0
|
||||
<A_im> = 0
|
||||
<EndPoint>
|
||||
[BdryProps] = 1
|
||||
<BeginBdry>
|
||||
<BdryName> = \"outer\"
|
||||
<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
|
||||
<innerangle> = 0
|
||||
<outerangle> = 0
|
||||
<EndBdry>
|
||||
[BlockProps] = 1
|
||||
<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>
|
||||
[CircuitProps] = 1
|
||||
<BeginCircuit>
|
||||
<CircuitName> = \"coil\"
|
||||
<TotalAmps_re> = 1
|
||||
<TotalAmps_im> = 0
|
||||
<CircuitType> = 1
|
||||
<EndCircuit>
|
||||
[NumPoints] = 0
|
||||
[NumSegments] = 0
|
||||
[NumArcSegments] = 0
|
||||
[NumHoles] = 0
|
||||
[NumBlockLabels] = 1
|
||||
0.5\t0.5\t1\t-1\t0\t0\t0\t1\t0\t\"\"
|
||||
[Solution]
|
||||
4
|
||||
0.0\t0.0\t0.0
|
||||
1.0\t0.0\t0.125
|
||||
0.0\t1.0\t0.25
|
||||
1.0\t1.0\t0.375
|
||||
2
|
||||
0\t1\t2\t0
|
||||
1\t3\t2\t0
|
||||
1
|
||||
1\t0.5
|
||||
";
|
||||
|
||||
const AC_FIXTURE: &str = "\
|
||||
[Format] = 4.0
|
||||
[Frequency] = 60
|
||||
[Precision] = 1e-08
|
||||
[MinAngle] = 30
|
||||
[DoSmartMesh] = 1
|
||||
[Depth] = 1
|
||||
[LengthUnits] = millimeters
|
||||
[ProblemType] = planar
|
||||
[Coordinates] = cartesian
|
||||
[ACSolver] = 0
|
||||
[PrevType] = 0
|
||||
[PrevSoln] = \"\"
|
||||
[Comment] = \"ac fixture\"
|
||||
[PointProps] = 0
|
||||
[BdryProps] = 0
|
||||
[BlockProps] = 0
|
||||
[CircuitProps] = 0
|
||||
[NumPoints] = 0
|
||||
[NumSegments] = 0
|
||||
[NumArcSegments] = 0
|
||||
[NumHoles] = 0
|
||||
[NumBlockLabels] = 0
|
||||
[Solution]
|
||||
3
|
||||
0.0\t0.0\t0.0\t0.0
|
||||
1.0\t0.0\t0.125\t0.0625
|
||||
0.0\t1.0\t0.25\t0.125
|
||||
1
|
||||
0\t1\t2\t0
|
||||
0
|
||||
";
|
||||
|
||||
#[test]
|
||||
fn parses_dc_header_and_counts() {
|
||||
let sol = MagSolution::parse(DC_FIXTURE).unwrap();
|
||||
assert_eq!(sol.header.frequency, 0.0);
|
||||
assert_eq!(sol.header.problem_type, ProblemType::Planar);
|
||||
assert_eq!(sol.header.length_units, LengthUnit::Millimeters);
|
||||
assert!((sol.header.depth - 1.0).abs() < 1e-12);
|
||||
assert_eq!(sol.points.len(), 1);
|
||||
assert_eq!(sol.boundaries.len(), 1);
|
||||
assert_eq!(sol.materials.len(), 1);
|
||||
assert_eq!(sol.circuits.len(), 1);
|
||||
assert_eq!(sol.mesh_nodes.len(), 4);
|
||||
assert_eq!(sol.mesh_elements.len(), 2);
|
||||
assert_eq!(sol.block_circuits.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_dc_node_a_values_real_only() {
|
||||
let sol = MagSolution::parse(DC_FIXTURE).unwrap();
|
||||
assert!((sol.mesh_nodes[2].a.re - 0.25).abs() < 1e-12);
|
||||
assert!(sol.mesh_nodes[2].a.im.abs() < 1e-12);
|
||||
assert!((sol.mesh_nodes[3].x - 1.0).abs() < 1e-12);
|
||||
assert!((sol.mesh_nodes[3].y - 1.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_element_indices_and_label_with_zero_b_field() {
|
||||
let sol = MagSolution::parse(DC_FIXTURE).unwrap();
|
||||
let e0 = &sol.mesh_elements[0];
|
||||
assert_eq!(e0.n, [0, 1, 2]);
|
||||
assert_eq!(e0.lab, 0);
|
||||
assert_eq!(e0.b1, Complex::new(0.0, 0.0));
|
||||
assert_eq!(e0.b2, Complex::new(0.0, 0.0));
|
||||
assert_eq!(e0.mu1, Complex::new(0.0, 0.0));
|
||||
assert_eq!(e0.mu2, Complex::new(0.0, 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_ac_node_complex_a_values() {
|
||||
let sol = MagSolution::parse(AC_FIXTURE).unwrap();
|
||||
assert_eq!(sol.header.frequency, 60.0);
|
||||
assert_eq!(sol.mesh_nodes.len(), 3);
|
||||
assert!((sol.mesh_nodes[1].a.re - 0.125).abs() < 1e-12);
|
||||
assert!((sol.mesh_nodes[1].a.im - 0.0625).abs() < 1e-12);
|
||||
assert_eq!(sol.mesh_elements.len(), 1);
|
||||
assert_eq!(sol.block_circuits.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_input_missing_solution_marker() {
|
||||
let bad = "[Format] = 4.0\n[Frequency] = 0\n";
|
||||
match MagSolution::parse(bad) {
|
||||
Err(AnsParseError::MissingSolution) => {}
|
||||
other => panic!("expected MissingSolution, got {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ pub mod writer;
|
|||
pub mod edit;
|
||||
pub mod poly;
|
||||
pub mod mesh;
|
||||
pub mod ans;
|
||||
|
||||
use num_complex::Complex64;
|
||||
|
||||
|
|
|
|||
|
|
@ -38,11 +38,11 @@ pub enum MeshLoadError {
|
|||
}
|
||||
|
||||
impl Mesh {
|
||||
/// loads .1.node and .1.ele next to the given path stem (Triangle's default output).
|
||||
/// loads .node and .ele next to the given path stem.
|
||||
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 node_path = with_extension(stem, "node");
|
||||
let ele_path = with_extension(stem, "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 })
|
||||
|
|
|
|||
|
|
@ -20,6 +20,15 @@ ICONSET="$BUILD/AppIcon.iconset"
|
|||
ICNS="$RESOURCES/AppIcon.icns"
|
||||
SVG="$ROOT/assets/femm.svg"
|
||||
|
||||
echo "Building Triangle..."
|
||||
bash "$ROOT/scripts/macos/build_triangle.sh"
|
||||
|
||||
TRI="$ROOT/build/triangle/triangle"
|
||||
if [ ! -f "$TRI" ]; then
|
||||
echo "ERROR: triangle binary missing at $TRI" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Building Rust workspace (release)..."
|
||||
cargo build --release -p femm-app
|
||||
|
||||
|
|
@ -32,6 +41,7 @@ fi
|
|||
rm -rf "$APP"
|
||||
mkdir -p "$MACOS" "$RESOURCES"
|
||||
cp "$BIN" "$MACOS/femm"
|
||||
cp "$TRI" "$MACOS/triangle"
|
||||
|
||||
if [ -f "$SVG" ]; then
|
||||
if ! command -v rsvg-convert >/dev/null; then
|
||||
|
|
|
|||
Loading…
Reference in New Issue