fix GUI file-open pipeline: replace buggy sdf_to_source with cord_sdf::sdf_to_cordial, reset viewport on parse error
This commit is contained in:
parent
97653580e5
commit
91241e44b0
|
|
@ -466,6 +466,8 @@ impl App {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
self.info = None;
|
self.info = None;
|
||||||
self.error = Some(e);
|
self.error = Some(e);
|
||||||
|
self.viewport.set_graph(&default_sphere_graph());
|
||||||
|
self.viewport.set_bounds(2.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -545,6 +547,8 @@ impl App {
|
||||||
self.scene_objects.clear();
|
self.scene_objects.clear();
|
||||||
self.needs_cast = false;
|
self.needs_cast = false;
|
||||||
self.needs_plot = false;
|
self.needs_plot = false;
|
||||||
|
self.viewport.set_graph(&default_sphere_graph());
|
||||||
|
self.viewport.set_bounds(2.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -598,7 +602,7 @@ impl App {
|
||||||
|
|
||||||
let config = DecompileConfig::default();
|
let config = DecompileConfig::default();
|
||||||
let source = match decompile(&mesh, &config) {
|
let source = match decompile(&mesh, &config) {
|
||||||
Ok(result) => sdf_to_source(&result.sdf, "imported"),
|
Ok(result) => cord_sdf::sdf_to_cordial(&result.sdf),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
self.status = Some(format!("decompose error: {e}"));
|
self.status = Some(format!("decompose error: {e}"));
|
||||||
return;
|
return;
|
||||||
|
|
@ -638,6 +642,7 @@ impl App {
|
||||||
.to_lowercase();
|
.to_lowercase();
|
||||||
|
|
||||||
let is_mesh = matches!(ext.as_str(), "obj" | "stl" | "3mf");
|
let is_mesh = matches!(ext.as_str(), "obj" | "stl" | "3mf");
|
||||||
|
let mut fallback_note: Option<&str> = None;
|
||||||
|
|
||||||
let result = match ext.as_str() {
|
let result = match ext.as_str() {
|
||||||
"zcd" => load_zcd(path),
|
"zcd" => load_zcd(path),
|
||||||
|
|
@ -646,7 +651,15 @@ impl App {
|
||||||
Err("binary format (.cord) — no source to edit".into())
|
Err("binary format (.cord) — no source to edit".into())
|
||||||
}
|
}
|
||||||
"obj" | "stl" | "3mf" => {
|
"obj" | "stl" | "3mf" => {
|
||||||
import_mesh(path)
|
match import_mesh(path) {
|
||||||
|
Ok(imp) => {
|
||||||
|
if imp.is_fallback {
|
||||||
|
fallback_note = Some(" (bounding box — full decompose failed)");
|
||||||
|
}
|
||||||
|
Ok(imp.source)
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"step" | "stp" => {
|
"step" | "stp" => {
|
||||||
Err(format!("import for .{ext} not yet implemented"))
|
Err(format!("import for .{ext} not yet implemented"))
|
||||||
|
|
@ -666,7 +679,8 @@ impl App {
|
||||||
self.reparse();
|
self.reparse();
|
||||||
self.update_markdown();
|
self.update_markdown();
|
||||||
self.push_recent(path);
|
self.push_recent(path);
|
||||||
self.status = Some(format!("opened: {}", path.display()));
|
let note = fallback_note.unwrap_or("");
|
||||||
|
self.status = Some(format!("opened: {}{note}", path.display()));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
self.status = Some(format!("error: {e}"));
|
self.status = Some(format!("error: {e}"));
|
||||||
|
|
@ -1649,6 +1663,20 @@ fn estimate_bounds(graph: &cord_trig::TrigGraph) -> f64 {
|
||||||
max_r.clamp(0.5, 10000.0)
|
max_r.clamp(0.5, 10000.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_sphere_graph() -> cord_trig::TrigGraph {
|
||||||
|
use cord_trig::ir::{TrigGraph, TrigOp};
|
||||||
|
let mut g = TrigGraph::new();
|
||||||
|
let x = g.push(TrigOp::InputX);
|
||||||
|
let y = g.push(TrigOp::InputY);
|
||||||
|
let z = g.push(TrigOp::InputZ);
|
||||||
|
let xy = g.push(TrigOp::Hypot(x, y));
|
||||||
|
let mag = g.push(TrigOp::Hypot(xy, z));
|
||||||
|
let r = g.push(TrigOp::Const(2.0));
|
||||||
|
let out = g.push(TrigOp::Sub(mag, r));
|
||||||
|
g.set_output(out);
|
||||||
|
g
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_scad(src: &str) -> Result<(cord_trig::TrigGraph, f64), String> {
|
fn parse_scad(src: &str) -> Result<(cord_trig::TrigGraph, f64), String> {
|
||||||
use cord_parse::lexer::Lexer;
|
use cord_parse::lexer::Lexer;
|
||||||
use cord_parse::parser::Parser;
|
use cord_parse::parser::Parser;
|
||||||
|
|
@ -1683,15 +1711,26 @@ fn load_zcd(path: &std::path::Path) -> Result<String, String> {
|
||||||
Err("no readable layers in .zcd".into())
|
Err("no readable layers in .zcd".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn import_mesh(path: &std::path::Path) -> Result<String, String> {
|
struct MeshImport {
|
||||||
|
source: String,
|
||||||
|
is_fallback: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn import_mesh(path: &std::path::Path) -> Result<MeshImport, String> {
|
||||||
use cord_decompile::mesh::TriangleMesh;
|
use cord_decompile::mesh::TriangleMesh;
|
||||||
use cord_decompile::{decompile, DecompileConfig};
|
use cord_decompile::{decompile, DecompileConfig};
|
||||||
|
|
||||||
let mesh = TriangleMesh::load(path).map_err(|e| e.to_string())?;
|
let mesh = TriangleMesh::load(path).map_err(|e| e.to_string())?;
|
||||||
let config = DecompileConfig::default();
|
let config = DecompileConfig::default();
|
||||||
match decompile(&mesh, &config) {
|
match decompile(&mesh, &config) {
|
||||||
Ok(result) => Ok(sdf_to_source(&result.sdf, "imported")),
|
Ok(result) => Ok(MeshImport {
|
||||||
Err(_) => Ok(mesh_bounding_source(&mesh)),
|
source: cord_sdf::sdf_to_cordial(&result.sdf),
|
||||||
|
is_fallback: false,
|
||||||
|
}),
|
||||||
|
Err(_) => Ok(MeshImport {
|
||||||
|
source: mesh_bounding_source(&mesh),
|
||||||
|
is_fallback: true,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1710,98 +1749,6 @@ fn mesh_bounding_source(mesh: &cord_decompile::mesh::TriangleMesh) -> String {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sdf_to_source(node: &cord_sdf::SdfNode, prefix: &str) -> String {
|
|
||||||
let mut out = String::new();
|
|
||||||
let mut counter = 0u32;
|
|
||||||
let final_expr = sdf_node_emit(node, prefix, &mut counter, &mut out);
|
|
||||||
use std::fmt::Write;
|
|
||||||
let _ = writeln!(out, "let result: Obj = {final_expr}");
|
|
||||||
let _ = writeln!(out, "cast(result)");
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sdf_node_emit(
|
|
||||||
node: &cord_sdf::SdfNode,
|
|
||||||
prefix: &str,
|
|
||||||
counter: &mut u32,
|
|
||||||
out: &mut String,
|
|
||||||
) -> String {
|
|
||||||
use cord_sdf::SdfNode;
|
|
||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
match node {
|
|
||||||
SdfNode::Sphere { radius } => format!("sphere({radius:.4})"),
|
|
||||||
SdfNode::Box { half_extents: h } => format!("box({:.4}, {:.4}, {:.4})", h[0], h[1], h[2]),
|
|
||||||
SdfNode::Cylinder { radius, height } => format!("cylinder({radius:.4}, {:.4})", height / 2.0),
|
|
||||||
|
|
||||||
SdfNode::Translate { offset, child } => {
|
|
||||||
let inner = sdf_node_emit(child, prefix, counter, out);
|
|
||||||
format!("translate({inner}, {:.4}, {:.4}, {:.4})", offset[0], offset[1], offset[2])
|
|
||||||
}
|
|
||||||
SdfNode::Rotate { axis: _, angle_deg, child } => {
|
|
||||||
let inner = sdf_node_emit(child, prefix, counter, out);
|
|
||||||
let rad = angle_deg * std::f64::consts::PI / 180.0;
|
|
||||||
format!("rotate_z({inner}, {rad:.6})")
|
|
||||||
}
|
|
||||||
SdfNode::Scale { factor, child } => {
|
|
||||||
let inner = sdf_node_emit(child, prefix, counter, out);
|
|
||||||
let s = factor[0].max(factor[1]).max(factor[2]);
|
|
||||||
format!("scale({inner}, {s:.4})")
|
|
||||||
}
|
|
||||||
|
|
||||||
SdfNode::Union(children) => {
|
|
||||||
let mut names = Vec::new();
|
|
||||||
for child in children {
|
|
||||||
let name = format!("{prefix}_{counter}");
|
|
||||||
*counter += 1;
|
|
||||||
let expr = sdf_node_emit(child, prefix, counter, out);
|
|
||||||
let _ = writeln!(out, "let {name}: Obj = {expr}");
|
|
||||||
names.push(name);
|
|
||||||
}
|
|
||||||
if names.len() == 1 {
|
|
||||||
return names[0].clone();
|
|
||||||
}
|
|
||||||
let mut result = format!("union({}, {})", names[0], names[1]);
|
|
||||||
for n in &names[2..] {
|
|
||||||
result = format!("union({result}, {n})");
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
SdfNode::Intersection(children) => {
|
|
||||||
let exprs: Vec<String> = children.iter()
|
|
||||||
.map(|c| sdf_node_emit(c, prefix, counter, out))
|
|
||||||
.collect();
|
|
||||||
let mut result = format!("intersect({}, {})", exprs[0], exprs[1]);
|
|
||||||
for e in &exprs[2..] {
|
|
||||||
result = format!("intersect({result}, {e})");
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
SdfNode::Difference { base, subtract } => {
|
|
||||||
let base_expr = sdf_node_emit(base, prefix, counter, out);
|
|
||||||
let sub_exprs: Vec<String> = subtract.iter()
|
|
||||||
.map(|c| sdf_node_emit(c, prefix, counter, out))
|
|
||||||
.collect();
|
|
||||||
let mut result = base_expr;
|
|
||||||
for s in &sub_exprs {
|
|
||||||
result = format!("diff({result}, {s})");
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
SdfNode::SmoothUnion { children, .. } => {
|
|
||||||
let exprs: Vec<String> = children.iter()
|
|
||||||
.map(|c| sdf_node_emit(c, prefix, counter, out))
|
|
||||||
.collect();
|
|
||||||
if exprs.len() == 1 { return exprs[0].clone(); }
|
|
||||||
let mut result = format!("union({}, {})", exprs[0], exprs[1]);
|
|
||||||
for e in &exprs[2..] {
|
|
||||||
result = format!("union({result}, {e})");
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_object_line(lines: &[&str], name: &str) -> Option<usize> {
|
fn find_object_line(lines: &[&str], name: &str) -> Option<usize> {
|
||||||
for (i, line) in lines.iter().enumerate() {
|
for (i, line) in lines.iter().enumerate() {
|
||||||
let t = line.trim();
|
let t = line.trim();
|
||||||
|
|
@ -2280,3 +2227,72 @@ fn setup_native_menu() {
|
||||||
|
|
||||||
std::mem::forget(menu);
|
std::mem::forget(menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use cord_expr::parse_expr_scene;
|
||||||
|
use cord_sdf::SdfNode;
|
||||||
|
use cord_trig::eval::evaluate;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cordial_roundtrip_sphere() {
|
||||||
|
let node = SdfNode::Sphere { radius: 5.0 };
|
||||||
|
let src = cord_sdf::sdf_to_cordial(&node);
|
||||||
|
let scene = parse_expr_scene(&src).unwrap_or_else(|e| panic!("{e}\n{src}"));
|
||||||
|
let graph = cord_expr::resolve_scene(scene);
|
||||||
|
assert!((evaluate(&graph, 5.0, 0.0, 0.0)).abs() < 1e-4);
|
||||||
|
assert!(evaluate(&graph, 0.0, 0.0, 0.0) < 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cordial_roundtrip_translated_box() {
|
||||||
|
let node = SdfNode::Translate {
|
||||||
|
offset: [3.0, 0.0, 0.0],
|
||||||
|
child: Box::new(SdfNode::Box { half_extents: [2.0, 2.0, 2.0] }),
|
||||||
|
};
|
||||||
|
let src = cord_sdf::sdf_to_cordial(&node);
|
||||||
|
let scene = parse_expr_scene(&src).unwrap_or_else(|e| panic!("{e}\n{src}"));
|
||||||
|
let graph = cord_expr::resolve_scene(scene);
|
||||||
|
assert!(evaluate(&graph, 3.0, 0.0, 0.0) < 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cordial_roundtrip_rotated_cylinder() {
|
||||||
|
let node = SdfNode::Rotate {
|
||||||
|
axis: [1.0, 0.0, 0.0],
|
||||||
|
angle_deg: 90.0,
|
||||||
|
child: Box::new(SdfNode::Cylinder { radius: 2.0, height: 10.0 }),
|
||||||
|
};
|
||||||
|
let src = cord_sdf::sdf_to_cordial(&node);
|
||||||
|
assert!(src.contains("rotate_x("), "axis=X should emit rotate_x, got:\n{src}");
|
||||||
|
let scene = parse_expr_scene(&src).unwrap_or_else(|e| panic!("{e}\n{src}"));
|
||||||
|
let _graph = cord_expr::resolve_scene(scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cordial_roundtrip_union() {
|
||||||
|
let node = SdfNode::Union(vec![
|
||||||
|
SdfNode::Sphere { radius: 1.0 },
|
||||||
|
SdfNode::Translate {
|
||||||
|
offset: [5.0, 0.0, 0.0],
|
||||||
|
child: Box::new(SdfNode::Sphere { radius: 1.0 }),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
let src = cord_sdf::sdf_to_cordial(&node);
|
||||||
|
let scene = parse_expr_scene(&src).unwrap_or_else(|e| panic!("{e}\n{src}"));
|
||||||
|
let graph = cord_expr::resolve_scene(scene);
|
||||||
|
assert!(evaluate(&graph, 0.0, 0.0, 0.0) < 0.0);
|
||||||
|
assert!(evaluate(&graph, 5.0, 0.0, 0.0) < 0.0);
|
||||||
|
assert!(evaluate(&graph, 2.5, 0.0, 0.0) > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mesh_bounding_source_parses() {
|
||||||
|
let src = "// bounding box approximation\n\
|
||||||
|
let result: Obj = translate(box(5.0000, 3.0000, 2.0000), 1.0000, 2.0000, 0.0000)\n\
|
||||||
|
cast(result)";
|
||||||
|
let scene = parse_expr_scene(src).unwrap_or_else(|e| panic!("{e}\n{src}"));
|
||||||
|
let graph = cord_expr::resolve_scene(scene);
|
||||||
|
assert!(evaluate(&graph, 1.0, 2.0, 0.0) < 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue