forked from 1b-agent/opencadstudio-upstream
Implement solid shell workflow and properties
This commit is contained in:
1 parent
febedad916
commit
be06a756da
16 files changed
+780
-52
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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());
|
||||
|
||||
Reference in new issue
Block a user