Implement solid shell workflow and properties

This commit is contained in:
ramox81 committed 2026-09-08 19:37:35 +03:00
1 parent febedad916
commit be06a756da
16 files changed
+780 -52

No files matched your search

+21
View File
@@ -2926,6 +2926,14 @@ impl OpenCADStudio {
self.command_line.push_info(&prompt);
}
}
CmdResult::ReportError(msg) => {
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.command_line.push_error(&msg);
if let Some(prompt) = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()) {
self.command_line.push_info(&prompt);
}
}
CmdResult::ReportMeasurementAndDeselect(msg) => {
self.tabs[i].snap_result = None;
self.tabs[i].scene.deselect_all();
@@ -4115,6 +4123,19 @@ impl OpenCADStudio {
return task;
}
CmdResult::SolidShell {
handle,
actions,
distance,
} => {
let task = self.solid_shell(handle, &actions, distance);
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
return task;
}
CmdResult::SolidSubtract {
bases,
cutters,
+24
View File
@@ -971,6 +971,30 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
"SHELL" | "SOLIDEDIT" => {
use crate::modules::model::shell_cmd::ShellCommand;
let selected = self.tabs[i]
.scene
.selected_handles_in_order()
.into_iter()
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.filter(|handle| {
matches!(
self.tabs[i].scene.document.get_entity(*handle),
Some(acadrust::EntityType::Solid3D(_))
)
})
.collect::<Vec<_>>();
let target = (selected.len() == 1).then_some(selected[0]);
let new_cmd = if cmd == "SHELL" {
ShellCommand::direct(target)
} else {
ShellCommand::solid_edit(target)
};
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
// ── Solid booleans ─────────────────────────────────────────────
"UNION" => {
use crate::modules::model::boolean_cmd::BoolOp;
+1 -1
View File
@@ -982,7 +982,7 @@ impl OpenCADStudio {
"area":planar.curve.is_closed().then(|| planar.curve.enclosed_area().abs())
})
});
out.push(json!({"handle":format!("{:X}",handle.value()),"type":crate::entities::names::ui_name(entity),"bounds":{"min":[bb.min.x,bb.min.y,bb.min.z],"max":[bb.max.x,bb.max.y,bb.max.z]},"curve":curve,"mesh":metrics.map(|m|json!({"vertices":m.metrics.vertices,"triangles":m.metrics.triangles,"surface_area":m.metrics.surface_area,"volume":m.metrics.volume,"centroid":m.metrics.centroid}))}));
out.push(json!({"handle":format!("{:X}",handle.value()),"type":crate::entities::names::ui_name(entity),"bounds":{"min":[bb.min.x,bb.min.y,bb.min.z],"max":[bb.max.x,bb.max.y,bb.max.z]},"curve":curve,"mesh":metrics.map(|m|json!({"vertices":m.metrics.vertices,"triangles":m.metrics.triangles,"surface_area":m.metrics.surface_area,"volume":m.metrics.volume,"centroid":m.metrics.centroid,"moment_of_inertia":m.metrics.moment_of_inertia,"principal_directions":m.metrics.principal_directions,"principal_moments":m.metrics.principal_moments,"product_of_inertia":m.metrics.product_of_inertia,"radii_of_gyration":m.metrics.radii_of_gyration}))}));
}
json!({"ok":true,"document_id":tab.id,"geometry_revision":tab.scene.geometry_epoch,"measurements":out})
}
+102 -1
View File
@@ -9,7 +9,7 @@ use acadrust::{
};
use cadkernel::brep::Body;
use iced::Task;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use super::Message;
use crate::modules::model::boolean_cmd::BoolOp;
@@ -646,6 +646,107 @@ impl super::OpenCADStudio {
Task::none()
}
pub(super) fn solid_shell(
&mut self,
handle: Handle,
actions: &[crate::command::ShellFaceAction],
distance: f64,
) -> Task<Message> {
use crate::command::ShellFaceAction;
let i = self.active_tab;
if self.reject_locked_edit(i, handle) {
return Task::none();
}
if !matches!(
self.tabs[i].scene.document.get_entity(handle),
Some(EntityType::Solid3D(_))
) {
self.command_line
.push_error(crate::t!("Select a 3D solid.").as_ref());
return Task::none();
}
if !distance.is_finite() || distance.abs() <= f64::EPSILON {
self.command_line
.push_error(crate::t!("The shell offset distance must be nonzero.").as_ref());
return Task::none();
}
self.tabs[i].scene.restore_solid_models(&[handle]);
let Some(body) = self.tabs[i].scene.solid_models.get(&handle).cloned() else {
self.command_line
.push_error(crate::t!("The solid geometry could not be restored.").as_ref());
return Task::none();
};
let all_faces = body.face_keys().collect::<Vec<_>>();
let mut removed = HashSet::new();
for action in actions {
match *action {
ShellFaceAction::Remove(point) => {
let Some(face) = solid_model::nearest_face(&body, point.to_array()) else {
self.command_line
.push_error(crate::t!("A selected face could not be resolved.").as_ref());
return Task::none();
};
removed.insert(face);
}
ShellFaceAction::Add(point) => {
let Some(face) = solid_model::nearest_face(&body, point.to_array()) else {
self.command_line
.push_error(crate::t!("A selected face could not be resolved.").as_ref());
return Task::none();
};
removed.remove(&face);
}
ShellFaceAction::RemoveAll => removed.extend(all_faces.iter().copied()),
ShellFaceAction::AddAll => removed.clear(),
}
}
let removed = removed.into_iter().collect::<Vec<_>>();
let result = match cadkernel::brep::shell(&body, &removed, distance) {
Ok(result) => result,
Err(cadkernel::brep::ShellError::InvalidDistance) => {
self.command_line
.push_error(crate::t!("The shell offset distance must be nonzero.").as_ref());
return Task::none();
}
Err(cadkernel::brep::ShellError::UnsupportedSolid) => {
self.command_line.push_error(
crate::t!("SHELL supports rectangular boxes, circular cylinders, and spheres.")
.as_ref(),
);
return Task::none();
}
Err(cadkernel::brep::ShellError::UnknownFace) => {
self.command_line
.push_error(crate::t!("A selected face does not belong to the solid.").as_ref());
return Task::none();
}
Err(cadkernel::brep::ShellError::NoMaterial) => {
self.command_line.push_error(
crate::t!("The offset removes all material. Use a smaller distance or add a face back.")
.as_ref(),
);
return Task::none();
}
Err(cadkernel::brep::ShellError::Kernel(_)) => {
self.command_line.push_error(
crate::t!("The shell could not be constructed from the selected solid and distance.")
.as_ref(),
);
return Task::none();
}
};
if self.replace_solid_body(handle, result, "SHELL") {
self.command_line
.push_output(crate::t!("SHELL: solid updated.").as_ref());
}
Task::none()
}
/// Intersect compatible selected solids, Regions, and coplanar Surfaces.
pub(super) fn solid_intersect(&mut self) -> Task<Message> {
let i = self.active_tab;
+77 -34
View File
@@ -636,7 +636,7 @@ impl OpenCADStudio {
}
}
if !compact_solid && matches!(
if matches!(
entity,
acadrust::EntityType::Solid3D(_)
| acadrust::EntityType::Region(_)
@@ -654,38 +654,44 @@ impl OpenCADStudio {
{
use crate::entities::common::ro_prop;
let metrics = mesh.metrics;
let mut props = vec![
ro_prop(
t!("Vertices").as_ref(),
"mesh_vertices",
metrics.vertices.to_string(),
),
ro_prop(
t!("Triangles").as_ref(),
"mesh_triangles",
metrics.triangles.to_string(),
),
ro_prop(
t!("Surface Area").as_ref(),
"mesh_surface_area",
format!("{:.6}", metrics.surface_area),
),
ro_prop(
let triple = |values: [f64; 3]| {
format!("{:.6}, {:.6}, {:.6}", values[0], values[1], values[2])
};
let mut props = if compact_solid {
vec![ro_prop(
t!("Centroid").as_ref(),
"mesh_centroid",
format!(
"{:.6}, {:.6}, {:.6}",
metrics.centroid[0],
metrics.centroid[1],
metrics.centroid[2]
triple(metrics.centroid),
)]
} else {
vec![
ro_prop(
t!("Vertices").as_ref(),
"mesh_vertices",
metrics.vertices.to_string(),
),
),
ro_prop(
t!("Tessellation").as_ref(),
"mesh_complete",
if mesh.complete { "Complete" } else { "Partial" },
),
];
ro_prop(
t!("Triangles").as_ref(),
"mesh_triangles",
metrics.triangles.to_string(),
),
ro_prop(
t!("Surface Area").as_ref(),
"mesh_surface_area",
format!("{:.6}", metrics.surface_area),
),
ro_prop(
t!("Centroid").as_ref(),
"mesh_centroid",
triple(metrics.centroid),
),
ro_prop(
t!("Tessellation").as_ref(),
"mesh_complete",
if mesh.complete { "Complete" } else { "Partial" },
),
]
};
if mesh.complete
&& matches!(
entity,
@@ -694,14 +700,50 @@ impl OpenCADStudio {
| acadrust::EntityType::Body(_)
)
{
props.insert(
3,
let closed_props = vec![
ro_prop(
t!("Moment of inertia").as_ref(),
"mesh_moment_of_inertia",
triple(metrics.moment_of_inertia),
),
ro_prop(
t!("Principal directions").as_ref(),
"mesh_principal_directions",
metrics
.principal_directions
.chunks_exact(3)
.map(|axis| format!("({:.6}, {:.6}, {:.6})", axis[0], axis[1], axis[2]))
.collect::<Vec<_>>()
.join(", "),
),
ro_prop(
t!("Principal moments").as_ref(),
"mesh_principal_moments",
triple(metrics.principal_moments),
),
ro_prop(
t!("Product of inertia").as_ref(),
"mesh_product_of_inertia",
triple(metrics.product_of_inertia),
),
ro_prop(
t!("Radii of gyration").as_ref(),
"mesh_radii_of_gyration",
triple(metrics.radii_of_gyration),
),
ro_prop(
t!("Volume").as_ref(),
"mesh_volume",
format!("{:.6}", metrics.volume),
),
);
];
if compact_solid {
props.extend(closed_props);
} else {
let volume = closed_props.last().cloned().unwrap();
props.insert(3, volume);
props.extend(closed_props.into_iter().take(5));
}
}
sections.push(crate::scene::model::object::PropSection {
title: t!("Mass Properties").into_owned(),
@@ -3093,7 +3135,8 @@ fn retain_compact_solid_sections(
| "transparency"
| "hyperlink"
| "material"
) || crate::scene::model::solid_history::is_specialized_property(property.field)
) || property.field.starts_with("mesh_")
|| crate::scene::model::solid_history::is_specialized_property(property.field)
});
});
sections.retain(|section| !section.props.is_empty());