merge integration

This commit is contained in:
jess 2026-03-31 01:14:13 -07:00
commit 582ecf7c42
1 changed files with 114 additions and 98 deletions

View File

@ -466,6 +466,8 @@ impl App {
Err(e) => {
self.info = None;
self.error = Some(e);
self.viewport.set_graph(&default_sphere_graph());
self.viewport.set_bounds(2.0);
}
}
return;
@ -545,6 +547,8 @@ impl App {
self.scene_objects.clear();
self.needs_cast = 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 source = match decompile(&mesh, &config) {
Ok(result) => sdf_to_source(&result.sdf, "imported"),
Ok(result) => cord_sdf::sdf_to_cordial(&result.sdf),
Err(e) => {
self.status = Some(format!("decompose error: {e}"));
return;
@ -638,6 +642,7 @@ impl App {
.to_lowercase();
let is_mesh = matches!(ext.as_str(), "obj" | "stl" | "3mf");
let mut fallback_note: Option<&str> = None;
let result = match ext.as_str() {
"zcd" => load_zcd(path),
@ -646,7 +651,15 @@ impl App {
Err("binary format (.cord) — no source to edit".into())
}
"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" => {
Err(format!("import for .{ext} not yet implemented"))
@ -666,7 +679,8 @@ impl App {
self.reparse();
self.update_markdown();
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) => {
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)
}
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> {
use cord_parse::lexer::Lexer;
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())
}
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::{decompile, DecompileConfig};
let mesh = TriangleMesh::load(path).map_err(|e| e.to_string())?;
let config = DecompileConfig::default();
match decompile(&mesh, &config) {
Ok(result) => Ok(sdf_to_source(&result.sdf, "imported")),
Err(_) => Ok(mesh_bounding_source(&mesh)),
Ok(result) => Ok(MeshImport {
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> {
for (i, line) in lines.iter().enumerate() {
let t = line.trim();
@ -2280,3 +2227,72 @@ fn setup_native_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);
}
}