3MF: export, and 3MF/STL import as mesh bodies
Export: ExportCommand/CLI --out/MCP export with .3mf write a 3MF Core package ([Content_Types].xml, _rels/.rels, 3D/3dmodel.model; millimetres), one mesh object and build item per body, named after it, vertices shared and welded watertight (T-junctions between faces split), base-material colours when bodies carry one (STEP colours now ride on bodies). Tessellation as for STL. File > Export 3MF. Import: doc.open/drag-drop/CLI/MCP open accept .3mf and .stl; Insert Mesh (ParaMeshInsertAlignCommand) adds them to a design as a MeshImport feature. Kernel mesh bodies render, measure exactly, move and export; solid features refuse them with a clear error. 3MF reading flattens components and build transforms, converts units, reads colours; sizes, entries, nesting and element counts are capped. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
043a542e29
commit
ea3dd0ed6e
30 files changed
+1361
-47
No files matched your search
@@ -291,6 +291,25 @@ pub enum FeatureKind {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
components: Vec<solvecraft_kernel::StepNode>,
|
||||
},
|
||||
/// A non-parametric base feature: mesh bodies imported from a 3MF or STL file (kept in the
|
||||
/// design). Mesh bodies render, measure, move and export; solid features cannot use them.
|
||||
MeshImport {
|
||||
/// Source file name (display only).
|
||||
file: String,
|
||||
meshes: Vec<MeshData>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Triangles of one imported mesh body (millimetres).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct MeshData {
|
||||
pub name: String,
|
||||
/// x, y, z per vertex.
|
||||
pub positions: Vec<f64>,
|
||||
/// Three vertex indices per triangle.
|
||||
pub triangles: Vec<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub color: Option<[f32; 3]>,
|
||||
}
|
||||
|
||||
/// One loft section: profiles of a sketch.
|
||||
@@ -391,6 +410,7 @@ impl FeatureKind {
|
||||
FeatureKind::Split { .. } => "SplitBodyFeature",
|
||||
FeatureKind::Move { .. } => "MoveFeature",
|
||||
FeatureKind::Import { .. } => "BaseFeature",
|
||||
FeatureKind::MeshImport { .. } => "MeshFeature",
|
||||
}
|
||||
}
|
||||
/// Default name prefix (`Extrude` → `Extrude1`).
|
||||
@@ -418,6 +438,7 @@ impl FeatureKind {
|
||||
FeatureKind::Split { .. } => "Split",
|
||||
FeatureKind::Move { .. } => "Move",
|
||||
FeatureKind::Import { .. } => "Import",
|
||||
FeatureKind::MeshImport { .. } => "Mesh",
|
||||
}
|
||||
}
|
||||
/// Every expression the feature uses.
|
||||
@@ -478,7 +499,7 @@ impl FeatureKind {
|
||||
v.push(a);
|
||||
}
|
||||
}
|
||||
FeatureKind::Import { .. } => {}
|
||||
FeatureKind::Import { .. } | FeatureKind::MeshImport { .. } => {}
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
+26
-1
@@ -569,7 +569,13 @@ fn blend_target(state: &ModelState, body: &Option<String>, edges: &[Vec3]) -> Re
|
||||
best = Some((i, d));
|
||||
}
|
||||
}
|
||||
best.map(|(i, _)| i).ok_or_else(|| DocError::Invalid("there is no body to modify".into()))
|
||||
best.map(|(i, _)| i).ok_or_else(|| {
|
||||
if state.bodies.iter().any(|b| b.body.is_mesh()) {
|
||||
DocError::Invalid("there is no solid body to modify (mesh bodies have no edges)".into())
|
||||
} else {
|
||||
DocError::Invalid("there is no body to modify".into())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn revolve_axis(ss: &SolvedSketch, axis: &AxisRef) -> Result<(Vec2, Vec2)> {
|
||||
@@ -1100,6 +1106,13 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::MeshImport { meshes, .. } => {
|
||||
for (name, b) in mesh_bodies(meshes)? {
|
||||
let name = unique_body_name(st, &name);
|
||||
st.bodies.push(ModelBody::new(name, b, f.id));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::Import { step, .. } => {
|
||||
let imp = kernel::step_import_shared(step)?;
|
||||
for (k, b) in imp.bodies.iter().enumerate() {
|
||||
@@ -1118,6 +1131,18 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
}
|
||||
}
|
||||
|
||||
/// Mesh bodies of a MeshImport feature.
|
||||
fn mesh_bodies(meshes: &[crate::MeshData]) -> Result<Vec<(String, Body)>> {
|
||||
let mut out = Vec::with_capacity(meshes.len());
|
||||
for m in meshes {
|
||||
let positions: Vec<Vec3> = m.positions.as_chunks::<3>().0.iter().map(|c| Vec3::new(c[0], c[1], c[2])).collect();
|
||||
let triangles: Vec<[u32; 3]> = m.triangles.as_chunks::<3>().0.to_vec();
|
||||
let b = kernel::mesh_body(&positions, &triangles).map_err(|e| DocError::Invalid(format!("mesh `{}`: {e}", m.name)))?;
|
||||
out.push((m.name.clone(), b.with_color(m.color)));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// `base`, or `base (2)`, `base (3)`… if a body already has that name.
|
||||
fn unique_body_name(st: &ModelState, base: &str) -> String {
|
||||
let base = if base.trim().is_empty() { "Body" } else { base.trim() };
|
||||
|
||||
Reference in new issue
Block a user