forked from 1b-agent/opencadstudio-upstream
Implement surface thickening workflow
This commit is contained in:
1 parent
56e09cd3f5
commit
4b45e06cf8
7 files changed
+308
-4
No files matched your search
@@ -3803,6 +3803,126 @@ impl OpenCADStudio {
|
||||
self.refresh_properties();
|
||||
}
|
||||
|
||||
CmdResult::ThickenEntities { handles, 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();
|
||||
|
||||
if handles.is_empty() {
|
||||
self.command_line
|
||||
.push_output(crate::t!("No surfaces selected.").as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
if !distance.is_finite() {
|
||||
self.command_line.push_error(
|
||||
crate::t!("Requires numeric distance or two points.").as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
if distance.abs() <= f64::EPSILON {
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
use crate::modules::insert::solid3d_cmds::empty_solid3d;
|
||||
let pending = self.begin_undo(i, "THICKEN", handles.len(), true);
|
||||
let mut created = 0usize;
|
||||
let mut failed = 0usize;
|
||||
let mut self_intersections = 0usize;
|
||||
for handle in handles {
|
||||
let Some(acadrust::EntityType::Surface(surface)) =
|
||||
self.tabs[i].scene.document.get_entity(handle)
|
||||
else {
|
||||
failed += 1;
|
||||
continue;
|
||||
};
|
||||
let kernel_distance = if surface.kind
|
||||
== acadrust::entities::SurfaceKind::Revolved
|
||||
{
|
||||
-distance
|
||||
} else {
|
||||
distance
|
||||
};
|
||||
let body = self.tabs[i]
|
||||
.scene
|
||||
.solid_models
|
||||
.get(&handle)
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
crate::scene::convert::solid3d_tess::kernel_surface_body(surface)
|
||||
});
|
||||
let Some(body) = body else {
|
||||
failed += 1;
|
||||
continue;
|
||||
};
|
||||
let solid = match cadkernel::brep::thicken(&body, kernel_distance) {
|
||||
Ok(solid) => solid,
|
||||
Err(cadkernel::brep::ThickenError::SelfIntersection) => {
|
||||
failed += 1;
|
||||
self_intersections += 1;
|
||||
continue;
|
||||
}
|
||||
Err(_) => {
|
||||
failed += 1;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let history = crate::scene::model::solid_history::brep_op(&solid);
|
||||
if self
|
||||
.add_solid_model(empty_solid3d(), solid, history)
|
||||
.is_null()
|
||||
{
|
||||
failed += 1;
|
||||
} else {
|
||||
created += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if created > 0 {
|
||||
self.tabs[i].dirty = true;
|
||||
if self_intersections > 0 {
|
||||
let message = if self_intersections == 1 {
|
||||
"1 surface cannot be thickened with the specified value. Object intersects itself."
|
||||
.to_owned()
|
||||
} else {
|
||||
format!(
|
||||
"{} surfaces cannot be thickened with the specified value. Objects intersect themselves.",
|
||||
self_intersections
|
||||
)
|
||||
};
|
||||
self.command_line.push_error(&message);
|
||||
}
|
||||
if failed > 0 {
|
||||
self.command_line.push_output(&format!(
|
||||
"THICKEN: created {created} solid(s); {failed} surface(s) could not be thickened."
|
||||
));
|
||||
}
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
} else {
|
||||
if self_intersections > 0 {
|
||||
let message = if self_intersections == 1 {
|
||||
"1 surface cannot be thickened with the specified value. Object intersects itself."
|
||||
.to_owned()
|
||||
} else {
|
||||
format!(
|
||||
"{} surfaces cannot be thickened with the specified value. Objects intersect themselves.",
|
||||
self_intersections
|
||||
)
|
||||
};
|
||||
self.command_line.push_error(&message);
|
||||
} else {
|
||||
self.command_line
|
||||
.push_error(&format!("{failed} surface(s) could not be thickened."));
|
||||
}
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
}
|
||||
self.refresh_properties();
|
||||
}
|
||||
|
||||
CmdResult::PresspullPick { handle, point, offset, multiple: _ } => {
|
||||
self.presspull_pick(handle, point, offset);
|
||||
}
|
||||
|
||||
@@ -734,7 +734,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
// ── EXTRUDE ────────────────────────────────────────────────────
|
||||
"EXTRUDE" | "THICKEN" => {
|
||||
"EXTRUDE" => {
|
||||
use crate::modules::insert::solid3d_cmds::ExtrudeCommand;
|
||||
// A preselection becomes the complete source set; otherwise
|
||||
// the interactive command gathers any number of profiles.
|
||||
@@ -769,6 +769,19 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
"THICKEN" => {
|
||||
use crate::modules::insert::solid3d_cmds::ThickenCommand;
|
||||
let selected = self.tabs[i]
|
||||
.scene
|
||||
.selected_entities()
|
||||
.into_iter()
|
||||
.map(|(handle, entity)| (handle, entity.clone()))
|
||||
.collect();
|
||||
let command = ThickenCommand::new(selected);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
|
||||
"PRESSPULL" => {
|
||||
use crate::modules::insert::solid3d_cmds::PresspullCommand;
|
||||
let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
|
||||
|
||||
@@ -1518,6 +1518,11 @@ pub enum CmdResult {
|
||||
taper_angle: f64,
|
||||
color: [f32; 4],
|
||||
},
|
||||
/// Thicken one or more persistent surface bodies without consuming them.
|
||||
ThickenEntities {
|
||||
handles: Vec<Handle>,
|
||||
distance: f64,
|
||||
},
|
||||
/// Resolve a profile, bounded area, or solid face for PRESSPULL.
|
||||
PresspullPick {
|
||||
handle: Option<Handle>,
|
||||
|
||||
@@ -513,6 +513,172 @@ impl CadCommand for ExtrudeCommand {
|
||||
}
|
||||
}
|
||||
|
||||
// ── THICKEN command ────────────────────────────────────────────────────────
|
||||
|
||||
pub struct ThickenCommand {
|
||||
step: ThickenStep,
|
||||
handles: Vec<Handle>,
|
||||
first_point: Option<DVec3>,
|
||||
last_distance: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum ThickenStep {
|
||||
Pick,
|
||||
Distance,
|
||||
SecondPoint,
|
||||
}
|
||||
|
||||
fn thicken_default() -> &'static Mutex<f64> {
|
||||
static DEFAULT: OnceLock<Mutex<f64>> = OnceLock::new();
|
||||
DEFAULT.get_or_init(|| Mutex::new(0.0))
|
||||
}
|
||||
|
||||
impl ThickenCommand {
|
||||
pub fn new(preselection: Vec<(Handle, EntityType)>) -> Self {
|
||||
let handles = preselection
|
||||
.into_iter()
|
||||
.filter_map(|(handle, entity)| {
|
||||
matches!(entity, EntityType::Surface(_)).then_some(handle)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Self {
|
||||
step: if handles.is_empty() {
|
||||
ThickenStep::Pick
|
||||
} else {
|
||||
ThickenStep::Distance
|
||||
},
|
||||
handles,
|
||||
first_point: None,
|
||||
last_distance: *thicken_default()
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner()),
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(&mut self, distance: f64) -> CmdResult {
|
||||
self.last_distance = distance;
|
||||
*thicken_default()
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner()) = distance;
|
||||
CmdResult::ThickenEntities {
|
||||
handles: self.handles.clone(),
|
||||
distance,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for ThickenCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"THICKEN"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match self.step {
|
||||
ThickenStep::Pick => t!("Select surfaces to thicken:").into_owned(),
|
||||
ThickenStep::Distance => format!(
|
||||
"{} <{}>:",
|
||||
t!("Specify thickness"),
|
||||
crate::entities::common::format_length(self.last_distance)
|
||||
),
|
||||
ThickenStep::SecondPoint => {
|
||||
t!("Specify second point:").into_owned()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
self.step == ThickenStep::Distance
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
if self.step != ThickenStep::Distance {
|
||||
return None;
|
||||
}
|
||||
let value = text.trim();
|
||||
if value.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(match crate::entities::common::parse_typed_length(value) {
|
||||
Some(distance) if distance.is_finite() => self.finish(distance),
|
||||
_ => CmdResult::ReportError(
|
||||
t!("Requires numeric distance or two points.").into_owned(),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
match self.step {
|
||||
ThickenStep::Distance => {
|
||||
self.first_point = Some(point);
|
||||
self.step = ThickenStep::SecondPoint;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
ThickenStep::SecondPoint => {
|
||||
let distance = point.distance(self.first_point.unwrap_or(point));
|
||||
if distance.is_finite() {
|
||||
self.finish(distance)
|
||||
} else {
|
||||
CmdResult::ReportError(
|
||||
t!("Requires numeric distance or two points.").into_owned(),
|
||||
)
|
||||
}
|
||||
}
|
||||
ThickenStep::Pick => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
match self.step {
|
||||
ThickenStep::Pick if self.handles.is_empty() => {
|
||||
CmdResult::Measurement(t!("No surfaces selected.").into_owned())
|
||||
}
|
||||
ThickenStep::Pick => {
|
||||
self.step = ThickenStep::Distance;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
ThickenStep::Distance => self.finish(self.last_distance),
|
||||
ThickenStep::SecondPoint => CmdResult::ReportError(
|
||||
t!("Requires numeric distance or two points.").into_owned(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_selection_gathering(&self) -> bool {
|
||||
self.step == ThickenStep::Pick
|
||||
}
|
||||
|
||||
fn selection_forces_add(&self) -> bool {
|
||||
self.step == ThickenStep::Pick
|
||||
}
|
||||
|
||||
fn inject_selection_entities(&mut self, entities: Vec<SelectionEntity>) {
|
||||
if self.step != ThickenStep::Pick {
|
||||
return;
|
||||
}
|
||||
self.handles = entities
|
||||
.into_iter()
|
||||
.filter_map(|entry| {
|
||||
matches!(entry.entity, EntityType::Surface(_)).then_some(entry.handle)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
fn on_selection_complete(&mut self, _handles: Vec<Handle>) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_undo_step(&mut self) -> Option<CmdResult> {
|
||||
if self.step == ThickenStep::SecondPoint {
|
||||
self.first_point = None;
|
||||
self.step = ThickenStep::Distance;
|
||||
Some(CmdResult::NeedPoint)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── PRESSPULL command ─────────────────────────────────────────────────────
|
||||
|
||||
pub struct PresspullCommand {
|
||||
|
||||
Reference in new issue
Block a user