Complete EXTRUDE behavior and editable properties (#965)

Complete EXTRUDE behavior and editable properties
This commit is contained in:
Hakan Seven authored and GitHub committed 2026-09-02 17:42:15 +03:00
commit 11e25b8e73
13 files changed
+1615 -114

No files matched your search

Generated
+1 -1
View File
@@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cadkernel"
version = "0.1.0"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=a809cb32cc6d90fd0e9c4199c3b4977cd56b7fa8#a809cb32cc6d90fd0e9c4199c3b4977cd56b7fa8"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=5a4c287236e03a23be4d5099a45b16346e9f9442#5a4c287236e03a23be4d5099a45b16346e9f9442"
dependencies = [
"acadrust",
"cavalier_contours",
+1 -1
View File
@@ -28,7 +28,7 @@ rfd = "0.17"
clap = { version = "4", features = ["derive"] }
env_logger = "0.11"
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "a0f7d444f1607bc4b2c881060cbe7ea1014253cb", features = ["serde"] }
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "a809cb32cc6d90fd0e9c4199c3b4977cd56b7fa8", features = ["acis", "offset"] }
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "5a4c287236e03a23be4d5099a45b16346e9f9442", features = ["acis", "offset"] }
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
flate2 = "1"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
+157 -21
View File
@@ -3589,42 +3589,178 @@ impl OpenCADStudio {
}
}
// ── EXTRUDE ────────────────────────────────────────────────────
CmdResult::ExtrudeEntity {
handle,
height,
CmdResult::ExtrudeEntities {
handles,
extent,
mode,
taper_angle,
color: _,
} => {
if self.reject_locked_edit(i, handle) {
let delete_sources = self.tabs[i].scene.document.header.delete_objects;
if handles.is_empty()
|| handles
.iter()
.any(|handle| self.reject_locked_edit(i, *handle))
{
self.tabs[i].active_cmd = None;
return Task::none();
}
use crate::modules::insert::solid3d_cmds::empty_solid3d;
use crate::command::{ExtrudeExtent, ExtrudeMode};
use crate::modules::insert::solid3d_cmds::{
empty_extruded_surface, empty_solid3d,
};
use crate::scene::model::sweep_model;
let entity_opt = self.tabs[i].scene.document.get_entity(handle).cloned();
if let Some(entity) = entity_opt {
if let Some(solid) = sweep_model::extruded(&entity, height) {
let history = crate::scene::model::solid_history::brep_op(&solid);
let pending = self.begin_undo(i, "EXTRUDE", 1, true);
let created = self.add_solid_model(empty_solid3d(), solid, history);
if !created.is_null() {
self.tabs[i].dirty = true;
self.command_line
.push_output(crate::t!("EXTRUDE: solid created.").as_ref());
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
let path = match extent {
ExtrudeExtent::Path(handle) => self.tabs[i]
.scene
.document
.get_entity(handle)
.cloned()
.map(|entity| (handle, entity)),
_ => None,
};
if matches!(extent, ExtrudeExtent::Path(_)) && path.is_none() {
self.command_line
.push_error(crate::t!("EXTRUDE: path entity not found.").as_ref());
self.tabs[i].active_cmd = None;
return Task::none();
}
let pending = self.begin_undo(i, "EXTRUDE", handles.len(), true);
let mut created_handles = Vec::new();
let mut consumed = Vec::new();
let mut failed = 0usize;
for handle in &handles {
let Some(entity) = self.tabs[i].scene.document.get_entity(*handle).cloned() else {
failed += 1;
continue;
};
let Some((profile, closed)) = sweep_model::extrusion_profile_of(&entity) else {
failed += 1;
continue;
};
let direction = match extent {
ExtrudeExtent::Height(height) => profile.plane.normal().map(|normal| {
glam::DVec3::new(
normal[0] * height,
normal[1] * height,
normal[2] * height,
)
}),
ExtrudeExtent::Direction(direction) => Some(direction),
ExtrudeExtent::Path(_) => None,
};
let path_direction = path
.as_ref()
.and_then(|(_, path)| sweep_model::straight_path_direction(path))
.map(glam::DVec3::from_array);
// The creation mode controls closed profiles. Open curves
// always create sheet surfaces.
let creates_surface = matches!(mode, ExtrudeMode::Surface) || !closed;
let body = match (creates_surface, &path, direction) {
(false, Some((_, path)), _) => {
sweep_model::extruded_along_path(&entity, path, taper_angle)
}
(false, None, Some(direction)) => {
sweep_model::extruded_direction(
&entity,
direction.to_array(),
taper_angle,
)
}
(true, None, Some(direction)) => {
sweep_model::extruded_surface(
&entity,
direction.to_array(),
taper_angle,
)
}
(true, Some(_), _) => path_direction.and_then(|direction| {
sweep_model::extruded_surface(
&entity,
direction.to_array(),
taper_angle,
)
}),
_ => None,
};
let Some(body) = body else {
failed += 1;
continue;
};
let created = if creates_surface {
let direction = direction
.or(path_direction)
.unwrap_or(glam::DVec3::ZERO);
self.add_surface_model(
empty_extruded_surface(direction, taper_angle),
body,
)
} else {
self.command_line.push_error(crate::t!("EXTRUDE: could not build profile. Select a closed 2D entity (Circle, LwPolyline, etc.).").as_ref());
let direction = direction.unwrap_or(glam::DVec3::ZERO);
let history = path
.as_ref()
.and_then(|(_, path)| {
sweep_model::sweep_history(
&entity,
path,
taper_angle,
profile.plane.origin,
)
})
.or_else(|| {
sweep_model::extrusion_history(
&entity,
None,
direction.to_array(),
taper_angle,
profile.plane.origin,
)
})
.unwrap_or_else(|| crate::scene::model::solid_history::brep_op(&body));
self.add_solid_model(empty_solid3d(), body, history)
};
if created.is_null() {
failed += 1;
} else {
created_handles.push(created);
if delete_sources {
consumed.push(*handle);
}
}
}
if delete_sources && !created_handles.is_empty() {
consumed.sort_unstable_by_key(|handle| handle.value());
consumed.dedup();
self.tabs[i].scene.erase_entities(&consumed);
}
if !created_handles.is_empty() {
self.tabs[i].scene.deselect_all();
for handle in &created_handles {
self.tabs[i].scene.select_entity(*handle, true);
}
self.tabs[i].dirty = true;
self.command_line.push_output(
crate::tf!(
"EXTRUDE: created %{created} object(s); %{failed} source(s) could not be extruded.",
created = created_handles.len(),
failed = failed
)
.as_ref(),
);
if let Some(pending) = pending {
self.commit_undo_delta(i, pending);
}
} else {
self.command_line.push_error(crate::t!("EXTRUDE: entity not found.").as_ref());
self.command_line.push_error(crate::t!("EXTRUDE: no selected object could be extruded with the requested options.").as_ref());
if let Some(pending) = pending {
self.commit_undo_delta(i, pending);
}
}
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
self.refresh_properties();
}
CmdResult::PresspullEntity {
+21 -12
View File
@@ -736,21 +736,30 @@ impl OpenCADStudio {
// ── EXTRUDE ────────────────────────────────────────────────────
"EXTRUDE" | "THICKEN" => {
use crate::modules::insert::solid3d_cmds::ExtrudeCommand;
// If a single entity is already selected, skip the pick step.
// A preselection becomes the complete source set; otherwise
// the interactive command gathers any number of profiles.
let selected: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect();
let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
if selected.len() == 1 {
let handle = selected[0].0;
if !selected.is_empty() {
let mut cmd = ExtrudeCommand::new_named(cmd, color);
if let Some(curve) = crate::entities::curve::entity_curve(selected[0].1) {
cmd.set_entity_pick_direction(
curve.plane.normal().map(glam::DVec3::from_array),
);
cmd.on_entity_pick(
handle,
glam::DVec3::from_array(curve.plane.origin),
);
}
let first_curve = selected
.iter()
.find_map(|(_, entity)| crate::entities::curve::entity_curve(entity));
let anchor = first_curve
.as_ref()
.map(|curve| glam::DVec3::from_array(curve.plane.origin))
.unwrap_or(glam::DVec3::ZERO);
let direction = first_curve
.and_then(|curve| curve.plane.normal())
.map(glam::DVec3::from_array);
cmd.set_preselection(
selected
.iter()
.map(|(handle, entity)| (*handle, (*entity).clone()))
.collect(),
anchor,
direction,
);
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
} else {
+1 -1
View File
@@ -7,7 +7,7 @@ pub use automation::{export_headless, serve};
mod command_driver;
pub(crate) mod commands;
mod document;
mod expr_eval;
pub(crate) mod expr_eval;
mod find_replace;
pub(crate) mod helpers;
mod history;
+27
View File
@@ -40,6 +40,33 @@ impl super::OpenCADStudio {
handle
}
/// Add an open sheet body as a persistent Surface entity and register its
/// exact B-rep for shaded and wireframe display.
pub(super) fn add_surface_model(
&mut self,
mut entity: EntityType,
surface: Body,
) -> Handle {
let i = self.active_tab;
let EntityType::Surface(inner) = &mut entity else {
return Handle::NULL;
};
inner.common.plotstyle_flags = 2;
inner.wires = solid_model::edge_wires(&surface);
let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&surface)
else {
self.command_line
.push_error(crate::t!("The surface could not be encoded as ACIS.").as_ref());
return Handle::NULL;
};
inner.acis_data = acadrust::entities::AcisData::from_sat(&document.to_sat_string());
let Some(handle) = self.commit_entity_handle(entity) else {
return Handle::NULL;
};
self.tabs[i].scene.register_solid_model(handle, surface);
handle
}
fn selected_solid_handles(&mut self) -> Vec<Handle> {
let i = self.active_tab;
let mut handles: Vec<Handle> = self.tabs[i]
+19 -4
View File
@@ -1129,6 +1129,19 @@ impl CadCommand for TCountCommand {
}
// ── Result token ──────────────────────────────────────────────────────────
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExtrudeMode {
Solid,
Surface,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ExtrudeExtent {
Height(f64),
Direction(DVec3),
Path(Handle),
}
/// Returned by every `CadCommand` method to tell main.rs what to do.
#[allow(dead_code)]
pub enum CmdResult {
@@ -1414,10 +1427,12 @@ pub enum CmdResult {
/// Translation vector applied once to every selected point.
delta: DVec3,
},
/// Extrude the profile entity `handle` along its plane normal.
ExtrudeEntity {
handle: Handle,
height: f64,
/// Extrude one or more profiles with the requested construction mode.
ExtrudeEntities {
handles: Vec<Handle>,
extent: ExtrudeExtent,
mode: ExtrudeMode,
taper_angle: f64,
color: [f32; 4],
},
/// Pull a closed profile or a planar solid face by a signed distance.
+430 -41
View File
@@ -1,9 +1,14 @@
// Profile-based kernel solid commands stored as exact ACIS data.
use acadrust::{entities::Solid3D, EntityType};
use std::sync::{Mutex, OnceLock};
use acadrust::{entities::Solid3D, EntityType, Handle};
use glam::DVec3;
use crate::command::{CadCommand, CmdResult};
use crate::command::{
CadCommand, CmdOption, CmdResult, ExtrudeExtent, ExtrudeMode, SelectionEntity,
};
use crate::scene::WireModel;
use crate::t;
// ── EXTRUDE command ────────────────────────────────────────────────────────
@@ -11,32 +16,173 @@ use crate::t;
pub struct ExtrudeCommand {
command_name: &'static str,
step: ExtrudeStep,
pub target_handle: acadrust::Handle,
handles: Vec<Handle>,
preview_profiles: Vec<(Handle, EntityType)>,
injected_profile: Option<EntityType>,
anchor: DVec3,
direction: Option<DVec3>,
profile_direction: Option<DVec3>,
direction_start: Option<DVec3>,
mode: ExtrudeMode,
taper_angle: f64,
last_height: Option<f64>,
taper_return: ExtrudeStep,
color: [f32; 4],
}
#[derive(PartialEq)]
#[derive(Clone, Copy)]
struct ExtrudeDefaults {
mode: ExtrudeMode,
height: Option<f64>,
taper_angle: f64,
}
fn extrude_defaults() -> &'static Mutex<ExtrudeDefaults> {
static DEFAULTS: OnceLock<Mutex<ExtrudeDefaults>> = OnceLock::new();
DEFAULTS.get_or_init(|| {
Mutex::new(ExtrudeDefaults {
mode: ExtrudeMode::Solid,
height: None,
taper_angle: 0.0,
})
})
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum ExtrudeStep {
Pick,
Mode,
Height,
DirectionStart,
DirectionEnd,
Path,
Taper,
Expression,
TaperExpression,
}
impl ExtrudeCommand {
pub fn new_named(name: &str, color: [f32; 4]) -> Self {
let defaults = *extrude_defaults().lock().unwrap_or_else(|error| error.into_inner());
Self {
command_name: match name {
"THICKEN" => "THICKEN",
_ => "EXTRUDE",
},
step: ExtrudeStep::Pick,
target_handle: acadrust::Handle::NULL,
handles: Vec::new(),
preview_profiles: Vec::new(),
injected_profile: None,
anchor: DVec3::ZERO,
direction: None,
profile_direction: None,
direction_start: None,
mode: defaults.mode,
taper_angle: defaults.taper_angle,
last_height: defaults.height,
taper_return: ExtrudeStep::Height,
color,
}
}
pub fn set_preselection(
&mut self,
profiles: Vec<(Handle, EntityType)>,
anchor: DVec3,
direction: Option<DVec3>,
) {
self.handles = profiles.iter().map(|(handle, _)| *handle).collect();
self.preview_profiles = profiles;
self.anchor = anchor;
self.profile_direction = direction.and_then(DVec3::try_normalize);
if !self.handles.is_empty() {
self.step = ExtrudeStep::Height;
}
}
fn finish(&self, extent: ExtrudeExtent) -> CmdResult {
if let Some(height) = match extent {
ExtrudeExtent::Height(height) => Some(height),
ExtrudeExtent::Direction(direction) => Some(direction.length()),
ExtrudeExtent::Path(_) => None,
} {
extrude_defaults()
.lock()
.unwrap_or_else(|error| error.into_inner())
.height = Some(height);
}
CmdResult::ExtrudeEntities {
handles: self.handles.clone(),
extent,
mode: self.mode,
taper_angle: self.taper_angle,
color: self.color,
}
}
fn preview_wires(&self, cursor: DVec3) -> Vec<WireModel> {
if self.step != ExtrudeStep::Height {
return Vec::new();
}
let Some(axis) = self.profile_direction else {
return Vec::new();
};
let height = (cursor - self.anchor).dot(axis);
if !height.is_finite() || height.abs() <= 1e-6 {
return Vec::new();
}
self.preview_profiles
.iter()
.flat_map(|(_, entity)| {
let Some((profile, closed)) =
crate::scene::model::sweep_model::extrusion_profile_of(entity)
else {
return Vec::new();
};
let Some(normal) = profile.plane.normal() else {
return Vec::new();
};
let direction = [normal[0] * height, normal[1] * height, normal[2] * height];
let creates_surface = self.mode == ExtrudeMode::Surface || !closed;
let body = if creates_surface {
crate::scene::model::sweep_model::extruded_surface(
entity,
direction,
self.taper_angle,
)
} else {
crate::scene::model::sweep_model::extruded_direction(
entity,
direction,
self.taper_angle,
)
};
body.map(|body| preview_body_wires(&body, self.color))
.unwrap_or_default()
})
.collect()
}
}
fn preview_body_wires(body: &cadkernel::brep::Body, color: [f32; 4]) -> Vec<WireModel> {
let mesh = cadkernel::brep::mesh::tessellate(
body,
cadkernel::brep::mesh::TessellationTolerance::new(
cadkernel::tessellation::DEFAULT_ANGLE,
1e-9,
),
);
mesh.edges
.into_iter()
.filter(|edge| edge.positions.len() >= 2)
.map(|edge| {
WireModel::solid_f64(
"EXTRUDE-PREVIEW".to_owned(),
edge.positions,
color,
false,
)
})
.collect()
}
impl CadCommand for ExtrudeCommand {
@@ -45,63 +191,249 @@ impl CadCommand for ExtrudeCommand {
}
fn prompt(&self) -> String {
match self.step {
ExtrudeStep::Pick => t!("EXTRUDE Select closed profile (Circle, LwPolyline…):")
.into_owned(),
ExtrudeStep::Height => t!("EXTRUDE Height:").into_owned(),
ExtrudeStep::Pick => t!("EXTRUDE Select objects to extrude or [Mode] (Enter to finish):").into_owned(),
ExtrudeStep::Mode => format!(
"{} <{}>:",
t!("EXTRUDE Creation mode [Solid/Surface]"),
if self.mode == ExtrudeMode::Solid { "Solid" } else { "Surface" }
),
ExtrudeStep::Height => {
let base = t!("EXTRUDE Specify height or [Direction/Path/Taper angle/Expression]");
self.last_height.map_or_else(
|| format!("{base}:"),
|height| format!("{base} <{}>:", crate::entities::common::format_length(height)),
)
}
ExtrudeStep::DirectionStart => t!("EXTRUDE Start point of direction:").into_owned(),
ExtrudeStep::DirectionEnd => t!("EXTRUDE End point of direction:").into_owned(),
ExtrudeStep::Path => t!("EXTRUDE Select extrusion path or [Taper angle]:").into_owned(),
ExtrudeStep::Taper => format!(
"{} <{}>:",
t!("EXTRUDE Specify taper angle or [Expression]"),
crate::entities::common::format_angle(self.taper_angle)
),
ExtrudeStep::Expression => t!("EXTRUDE Enter height expression:").into_owned(),
ExtrudeStep::TaperExpression => t!("EXTRUDE Enter taper angle expression:").into_owned(),
}
}
fn options(&self) -> Vec<CmdOption> {
match self.step {
ExtrudeStep::Pick => vec![CmdOption::new("Mode", "MODE"), CmdOption::enter("Done")],
ExtrudeStep::Mode => vec![
CmdOption::new("Solid", "SOLID"),
CmdOption::new("Surface", "SURFACE"),
],
ExtrudeStep::Height => vec![
CmdOption::new("Direction", "DIRECTION"),
CmdOption::new("Path", "PATH"),
CmdOption::new("Taper angle", "TAPER"),
CmdOption::new("Expression", "EXPRESSION"),
],
ExtrudeStep::Path => vec![CmdOption::new("Taper angle", "TAPER")],
ExtrudeStep::Taper => vec![CmdOption::new("Expression", "EXPRESSION")],
_ => Vec::new(),
}
}
fn needs_entity_pick(&self) -> bool {
self.step == ExtrudeStep::Pick
self.step == ExtrudeStep::Path
}
fn entity_pick_uses_surface_point(&self) -> bool {
true
}
fn set_entity_pick_direction(&mut self, direction: Option<DVec3>) {
self.direction = direction.and_then(DVec3::try_normalize);
if self.step == ExtrudeStep::Pick {
self.profile_direction = direction.and_then(DVec3::try_normalize);
}
}
fn on_entity_pick(&mut self, handle: acadrust::Handle, point: DVec3) -> CmdResult {
fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult {
if handle.is_null() {
return CmdResult::NeedPoint;
}
self.target_handle = handle;
self.anchor = point;
self.step = ExtrudeStep::Height;
CmdResult::NeedPoint
match self.step {
ExtrudeStep::Pick => {
if !self.handles.contains(&handle) {
self.handles.push(handle);
self.anchor = point;
if let Some(entity) = self.injected_profile.take() {
self.preview_profiles.push((handle, entity));
}
} else {
self.injected_profile = None;
}
CmdResult::NeedPoint
}
ExtrudeStep::Path if !self.handles.contains(&handle) => {
self.finish(ExtrudeExtent::Path(handle))
}
_ => CmdResult::NeedPoint,
}
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
if self.step == ExtrudeStep::Height {
let height = self
.direction
.map(|direction| (pt - self.anchor).dot(direction))
.unwrap_or_else(|| pt.distance(self.anchor));
if !height.is_finite() || height.abs() <= 1e-6 {
return CmdResult::NeedPoint;
match self.step {
ExtrudeStep::Height => {
let height = self
.profile_direction
.map(|direction| (pt - self.anchor).dot(direction))
.unwrap_or_else(|| pt.distance(self.anchor));
if height.is_finite() && height.abs() > 1e-6 {
self.finish(ExtrudeExtent::Height(height))
} else {
CmdResult::NeedPoint
}
}
return CmdResult::ExtrudeEntity {
handle: self.target_handle,
height,
color: self.color,
};
ExtrudeStep::DirectionStart => {
self.direction_start = Some(pt);
self.step = ExtrudeStep::DirectionEnd;
CmdResult::NeedPoint
}
ExtrudeStep::DirectionEnd => {
let direction = pt - self.direction_start.unwrap_or(pt);
if direction.is_finite() && direction.length_squared() > 1e-12 {
self.finish(ExtrudeExtent::Direction(direction))
} else {
CmdResult::NeedPoint
}
}
_ => CmdResult::NeedPoint,
}
CmdResult::NeedPoint
}
fn wants_text_input(&self) -> bool {
self.step == ExtrudeStep::Height
matches!(
self.step,
ExtrudeStep::Pick
| ExtrudeStep::Mode
| ExtrudeStep::Height
| ExtrudeStep::Path
| ExtrudeStep::Taper
| ExtrudeStep::Expression
| ExtrudeStep::TaperExpression
)
}
fn point_step_accepts_keywords(&self) -> bool {
matches!(self.step, ExtrudeStep::Height | ExtrudeStep::Path)
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
crate::entities::common::parse_typed_length(text)
.filter(|&h| h.abs() > 1e-6)
.map(|h| CmdResult::ExtrudeEntity {
handle: self.target_handle,
height: h,
color: self.color,
})
let value = text.trim();
// A bare Enter finalizes the current source selection through
// `on_enter`. Treating the empty string as an option prefix makes it
// match every keyword and silently advances to the wrong branch.
if value.is_empty() {
return None;
}
match self.step {
ExtrudeStep::Pick if "MODE".starts_with(&value.to_ascii_uppercase()) => {
self.step = ExtrudeStep::Mode;
Some(CmdResult::NeedPoint)
}
ExtrudeStep::Mode => {
let upper = value.to_ascii_uppercase();
if "SOLID".starts_with(&upper) {
self.mode = ExtrudeMode::Solid;
} else if "SURFACE".starts_with(&upper) {
self.mode = ExtrudeMode::Surface;
} else {
return Some(CmdResult::NeedPoint);
}
extrude_defaults()
.lock()
.unwrap_or_else(|error| error.into_inner())
.mode = self.mode;
self.step = ExtrudeStep::Pick;
Some(CmdResult::NeedPoint)
}
ExtrudeStep::Height => {
let upper = value.to_ascii_uppercase();
if "DIRECTION".starts_with(&upper) {
self.step = ExtrudeStep::DirectionStart;
return Some(CmdResult::NeedPoint);
}
if "PATH".starts_with(&upper) {
self.step = ExtrudeStep::Path;
return Some(CmdResult::NeedPoint);
}
if "TAPER".starts_with(&upper) || "TAPER ANGLE".starts_with(&upper) {
self.taper_return = ExtrudeStep::Height;
self.step = ExtrudeStep::Taper;
return Some(CmdResult::NeedPoint);
}
if "EXPRESSION".starts_with(&upper) {
self.step = ExtrudeStep::Expression;
return Some(CmdResult::NeedPoint);
}
crate::entities::common::parse_typed_length(value)
.filter(|height| height.is_finite() && height.abs() > 1e-6)
.map(|height| self.finish(ExtrudeExtent::Height(height)))
}
ExtrudeStep::Path => {
let upper = value.to_ascii_uppercase();
if "TAPER".starts_with(&upper) || "TAPER ANGLE".starts_with(&upper) {
self.taper_return = ExtrudeStep::Path;
self.step = ExtrudeStep::Taper;
}
Some(CmdResult::NeedPoint)
}
ExtrudeStep::Taper => {
if "EXPRESSION".starts_with(&value.to_ascii_uppercase()) {
self.step = ExtrudeStep::TaperExpression;
return Some(CmdResult::NeedPoint);
}
let angle = crate::entities::common::parse_angle(value)?;
if !angle.is_finite() || angle.abs() >= std::f64::consts::FRAC_PI_2 {
return Some(CmdResult::NeedPoint);
}
self.taper_angle = angle;
extrude_defaults()
.lock()
.unwrap_or_else(|error| error.into_inner())
.taper_angle = angle;
self.step = self.taper_return;
Some(CmdResult::NeedPoint)
}
ExtrudeStep::Expression => crate::app::expr_eval::eval_number(value)
.filter(|height| height.is_finite() && height.abs() > 1e-6)
.map(|height| self.finish(ExtrudeExtent::Height(height))),
ExtrudeStep::TaperExpression => {
let angle = crate::app::expr_eval::eval_number(value)
.and_then(|value| crate::entities::common::parse_angle(&value.to_string()))?;
if !angle.is_finite() || angle.abs() >= std::f64::consts::FRAC_PI_2 {
return Some(CmdResult::NeedPoint);
}
self.taper_angle = angle;
extrude_defaults()
.lock()
.unwrap_or_else(|error| error.into_inner())
.taper_angle = angle;
self.step = self.taper_return;
Some(CmdResult::NeedPoint)
}
_ => None,
}
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
match self.step {
ExtrudeStep::Pick if !self.handles.is_empty() => {
self.step = ExtrudeStep::Height;
CmdResult::NeedPoint
}
ExtrudeStep::Mode => {
self.step = ExtrudeStep::Pick;
CmdResult::NeedPoint
}
ExtrudeStep::Height => self
.last_height
.filter(|height| height.is_finite() && height.abs() > 1e-6)
.map(|height| self.finish(ExtrudeExtent::Height(height)))
.unwrap_or(CmdResult::Cancel),
ExtrudeStep::Taper => {
self.step = self.taper_return;
CmdResult::NeedPoint
}
_ => CmdResult::Cancel,
}
}
fn cursor_axis(&self) -> Option<(DVec3, DVec3)> {
(self.step == ExtrudeStep::Height).then_some((self.anchor, self.direction?))
(self.step == ExtrudeStep::Height)
.then_some((self.anchor, self.profile_direction?))
}
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
(self.step == ExtrudeStep::Height).then_some(crate::command::DynSpec {
@@ -114,7 +446,46 @@ impl CadCommand for ExtrudeCommand {
})
}
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
Some((cursor - self.anchor).dot(self.direction?))
Some((cursor - self.anchor).dot(self.profile_direction?))
}
fn is_selection_gathering(&self) -> bool {
self.step == ExtrudeStep::Pick
}
fn selection_forces_add(&self) -> bool {
self.step == ExtrudeStep::Pick
}
fn inject_selection_entities(&mut self, entities: Vec<SelectionEntity>) {
if self.step != ExtrudeStep::Pick {
return;
}
self.handles = entities.iter().map(|entry| entry.handle).collect();
self.preview_profiles = entities
.iter()
.map(|entry| (entry.handle, entry.entity.clone()))
.collect();
if let Some(curve) = entities
.iter()
.find_map(|entry| crate::entities::curve::entity_curve(&entry.entity))
{
self.anchor = DVec3::from_array(curve.plane.origin);
self.profile_direction = curve.plane.normal().map(DVec3::from_array);
} else {
self.profile_direction = None;
}
}
fn on_selection_complete(&mut self, _handles: Vec<Handle>) -> CmdResult {
CmdResult::NeedPoint
}
fn inject_before_entity_pick(&self) -> bool {
self.step == ExtrudeStep::Pick
}
fn inject_picked_entity(&mut self, entity: EntityType) {
if self.step == ExtrudeStep::Pick {
self.injected_profile = Some(entity);
}
}
fn on_preview_wires(&mut self, cursor: DVec3) -> Vec<WireModel> {
self.preview_wires(cursor)
}
}
@@ -479,6 +850,24 @@ pub fn empty_solid3d() -> EntityType {
EntityType::Solid3D(Solid3D::new())
}
pub fn empty_extruded_surface(direction: DVec3, taper_angle: f64) -> EntityType {
use acadrust::entities::{Surface, SurfaceData, SurfaceKind};
use acadrust::types::Vector3;
let mut surface = Surface::new(SurfaceKind::Extruded);
if let SurfaceData::Extruded {
options,
sweep_vector,
..
} = &mut surface.surface_data
{
options.draft_angle = taper_angle;
options.is_solid = false;
*sweep_vector = Vector3::new(direction.x, direction.y, direction.z);
}
EntityType::Surface(surface)
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration {
names: &["EXTRUDE", "THICKEN", "PRESSPULL"]
+12 -4
View File
@@ -1,5 +1,5 @@
use acadrust::entities::acis::{SabReader, SatBody, SatDocument};
use acadrust::entities::{Body, Region, Solid3D};
use acadrust::entities::{Body, Region, Solid3D, Surface};
use cadkernel::brep::{self, Body as KernelBody};
use crate::scene::model::mesh_model::{MeshLodSet, MeshModel};
@@ -156,10 +156,18 @@ fn parse_acis(
}
pub fn kernel_body(solid: &Solid3D) -> Option<KernelBody> {
kernel_acis_body(&solid.acis_data)
}
pub fn kernel_surface_body(surface: &Surface) -> Option<KernelBody> {
kernel_acis_body(&surface.acis_data)
}
fn kernel_acis_body(acis: &acadrust::entities::AcisData) -> Option<KernelBody> {
let sat = parse_acis(
|| solid.parse_sat(),
solid.acis_data.is_binary,
&solid.acis_data.sab_data,
|| acis.parse(),
acis.is_binary,
&acis.sab_data,
)?;
let (mut bodies, loss) = cadkernel::acis::lift(&sat);
if !loss.is_empty() || bodies.len() != 1 {
+3
View File
@@ -602,6 +602,9 @@ impl Scene {
Some(EntityType::Solid3D(solid)) => {
crate::scene::convert::solid3d_tess::kernel_body(solid)
}
Some(EntityType::Surface(surface)) => {
crate::scene::convert::solid3d_tess::kernel_surface_body(surface)
}
_ => None,
})?;
Some((handle, body))
+15 -6
View File
@@ -2725,12 +2725,21 @@ impl Scene {
if let Some((mut set, wires, center)) =
crate::scene::model::solid_model::display_from_solid(&solid, color)
{
if let Some(EntityType::Solid3D(entity)) = self.document.get_entity_mut(handle) {
entity.point_of_reference = acadrust::types::Vector3::new(
center[0], center[1], center[2],
);
entity.wires = wires;
entity.silhouettes.clear();
if let Some(entity) = self.document.get_entity_mut(handle) {
let reference = acadrust::types::Vector3::new(center[0], center[1], center[2]);
match entity {
EntityType::Solid3D(entity) => {
entity.point_of_reference = reference;
entity.wires = wires;
entity.silhouettes.clear();
}
EntityType::Surface(entity) => {
entity.point_of_reference = reference;
entity.wires = wires;
entity.silhouettes.clear();
}
_ => {}
}
}
let name = handle.value().to_string();
for mesh in &mut set.lods {
+491 -3
View File
@@ -1,12 +1,14 @@
use acadrust::entities::Solid3D;
use acadrust::entities::{EmbeddedEntity, Solid3D};
use acadrust::objects::{
DynamicBlockData, ObjectType, SolidHistoryBox, SolidHistoryBrep, SolidHistoryCone,
SolidHistoryCylinder, SolidHistoryNodeBase, SolidHistoryOperation,
SolidHistoryPyramid, SolidHistorySphere, SolidHistorySweep, SolidHistoryTorus,
};
use acadrust::EntityType;
use cadkernel::brep::Body;
use crate::command::EntityTransform;
use crate::entities::traits::EntityTypeOps;
use crate::scene::model::object::{
GripApply, GripDef, GripShape, PropSection, PropValue, Property,
};
@@ -23,6 +25,8 @@ pub const GRIP_MAJOR_RADIUS: usize = 10_008;
pub const GRIP_MINOR_RADIUS: usize = 10_009;
pub const GRIP_TOP_RADIUS: usize = 10_010;
pub const GRIP_POSITION: usize = 10_011;
pub const GRIP_DRAFT: usize = 10_012;
pub const GRIP_PROFILE_FIRST: usize = 10_200;
pub const GRIP_BOX_CORNER_FIRST: usize = 10_100;
pub const GRIP_BOX_FACE_X_MIN: usize = 10_110;
pub const GRIP_BOX_FACE_X_MAX: usize = 10_111;
@@ -57,6 +61,8 @@ pub const PROP_BANK: &str = "solid_history_bank";
pub const PROP_TWIST_ALONG_PATH: &str = "solid_history_twist_along_path";
pub const PROP_SCALE_ALONG_PATH: &str = "solid_history_scale_along_path";
pub const PROP_SWEEP_LENGTH: &str = "solid_history_sweep_length";
pub const PROP_EXTRUSION_HEIGHT: &str = "solid_history_extrusion_height";
pub const PROP_TAPER_ANGLE: &str = "solid_history_taper_angle";
pub const PROP_PYRAMID_TYPE: &str = "solid_history_pyramid_type";
pub const PROP_HISTORY: &str = "solid_history_record";
pub const PROP_SHOW_HISTORY: &str = "solid_history_show";
@@ -110,6 +116,7 @@ pub fn has_specialized_primitive_properties(
| SolidHistoryOperation::Torus(_)
| SolidHistoryOperation::Pyramid(_)
| SolidHistoryOperation::Sweep(_)
| SolidHistoryOperation::Extrusion(_)
)
)
}
@@ -125,7 +132,7 @@ pub fn reference_point(operation: &SolidHistoryOperation) -> Option<glam::DVec3>
}
SolidHistoryOperation::Cone(value) => world_point(value.base.transform, [0.0; 3]),
SolidHistoryOperation::Cylinder(value) => world_point(value.base.transform, [0.0; 3]),
SolidHistoryOperation::Sweep(value) => world_point(
SolidHistoryOperation::Sweep(value) | SolidHistoryOperation::Extrusion(value) => world_point(
value.base.transform,
[
value.reference_point.x,
@@ -139,6 +146,100 @@ pub fn reference_point(operation: &SolidHistoryOperation) -> Option<glam::DVec3>
}
}
fn extrusion_properties(
document: &acadrust::CadDocument,
handle: acadrust::Handle,
value: &SolidHistorySweep,
) -> Vec<PropSection> {
let position = world_point(
value.base.transform,
[
value.reference_point.x,
value.reference_point.y,
value.reference_point.z,
],
)
.unwrap_or(glam::DVec3::ZERO);
let (record_history, object_show_history, show_history_mode) =
history_flags(document, handle).unwrap_or((false, false, 1));
let (show_history, show_history_editable) =
displayed_history_state(object_show_history, show_history_mode);
let path_length = sweep_path_length(value);
let height_value = path_length.unwrap_or(value.end_draft_distance);
let height = if value.path_entity.is_some() {
PropValue::ReadOnly(crate::entities::common::format_length(height_value))
} else {
PropValue::EditText(crate::entities::common::format_length(height_value))
};
let taper = if value.path_entity.is_some() {
PropValue::ReadOnly(crate::entities::common::format_angle(value.draft_angle))
} else {
PropValue::EditText(crate::entities::common::format_angle(value.draft_angle))
};
vec![
PropSection {
title: t!("Geometry").into_owned(),
props: vec![
Property {
label: t!("Solid type").into_owned(),
field: "solid_history_type",
value: PropValue::ReadOnly(t!("Extrusion").into_owned()),
},
crate::entities::common::edit_prop(
t!("Position X").as_ref(),
PROP_POSITION_X,
position.x,
),
crate::entities::common::edit_prop(
t!("Position Y").as_ref(),
PROP_POSITION_Y,
position.y,
),
crate::entities::common::edit_prop(
t!("Position Z").as_ref(),
PROP_POSITION_Z,
position.z,
),
Property {
label: t!("Height").into_owned(),
field: PROP_EXTRUSION_HEIGHT,
value: height,
},
Property {
label: t!("Taper angle").into_owned(),
field: PROP_TAPER_ANGLE,
value: taper,
},
],
},
PropSection {
title: t!("Solid History").into_owned(),
props: vec![
Property {
label: t!("History").into_owned(),
field: PROP_HISTORY,
value: PropValue::Choice {
selected: if record_history { "Record" } else { "None" }.to_string(),
options: vec!["None".to_string(), "Record".to_string()],
},
},
Property {
label: t!("Show History").into_owned(),
field: PROP_SHOW_HISTORY,
value: if show_history_editable {
PropValue::Choice {
selected: if show_history { "Yes" } else { "No" }.to_string(),
options: vec!["No".to_string(), "Yes".to_string()],
}
} else {
PropValue::ReadOnly(if show_history { "Yes" } else { "No" }.to_string())
},
},
],
},
]
}
fn sweep_properties(
document: &acadrust::CadDocument,
handle: acadrust::Handle,
@@ -226,7 +327,30 @@ fn sweep_path_length(value: &SolidHistorySweep) -> Option<f64> {
use acadrust::entities::EmbeddedEntity;
use acadrust::EntityType;
let entity = match value.path_entity.as_ref()? {
let path_entity = value.path_entity.as_ref()?;
if let EmbeddedEntity::Spline(path) = path_entity {
if path.degree == 1 && path.control_points.len() >= 2 && path.weights.is_empty() {
let transform = glam::DMat4::from_cols_array(&value.path_entity_transform);
if !transform.is_finite() || transform.determinant().abs() <= 1e-12 {
return None;
}
return Some(
path.control_points
.windows(2)
.map(|pair| {
let start = transform.transform_point3(glam::DVec3::new(
pair[0].x, pair[0].y, pair[0].z,
));
let end = transform.transform_point3(glam::DVec3::new(
pair[1].x, pair[1].y, pair[1].z,
));
end.distance(start)
})
.sum(),
);
}
}
let entity = match path_entity {
EmbeddedEntity::Line(value) => EntityType::Line(value.clone()),
EmbeddedEntity::Arc(value) => EntityType::Arc(value.clone()),
EmbeddedEntity::Circle(value) => EntityType::Circle(value.clone()),
@@ -889,6 +1013,9 @@ pub fn primitive_properties(
SolidHistoryOperation::Pyramid(value) => pyramid_properties(document, handle, value),
SolidHistoryOperation::Torus(value) => torus_properties(document, handle, value),
SolidHistoryOperation::Sweep(value) => sweep_properties(document, handle, value),
SolidHistoryOperation::Extrusion(value) => {
extrusion_properties(document, handle, value)
}
_ => Vec::new(),
}
}
@@ -919,6 +1046,8 @@ pub fn is_primitive_property(field: &str) -> bool {
| PROP_PROFILE_ROTATION
| PROP_TWIST_ALONG_PATH
| PROP_SCALE_ALONG_PATH
| PROP_EXTRUSION_HEIGHT
| PROP_TAPER_ANGLE
| PROP_PYRAMID_TYPE
)
}
@@ -933,6 +1062,81 @@ pub fn is_specialized_property(field: &str) -> bool {
|| is_history_choice(field)
}
fn apply_extrusion_geometry_property(
value: &mut SolidHistorySweep,
field: &str,
text: &str,
) -> Option<bool> {
if value.path_entity.is_some()
&& matches!(field, PROP_EXTRUSION_HEIGHT | PROP_TAPER_ANGLE)
{
return Some(false);
}
match field {
PROP_POSITION_X | PROP_POSITION_Y | PROP_POSITION_Z => {
let axis = match field {
PROP_POSITION_X => 0,
PROP_POSITION_Y => 1,
_ => 2,
};
let target = crate::entities::common::parse_length(text)?;
if !target.is_finite() {
return Some(false);
}
let current = matrix(value.base.transform)?;
let reference = current.transform_point3(glam::DVec3::new(
value.reference_point.x,
value.reference_point.y,
value.reference_point.z,
));
if !reference.is_finite() || (target - reference[axis]).abs() <= 1e-12 {
return Some(false);
}
let mut delta = glam::DVec3::ZERO;
delta[axis] = target - reference[axis];
let updated = glam::DMat4::from_translation(delta) * current;
if !updated.is_finite() || updated.determinant().abs() <= 1e-12 {
return Some(false);
}
value.base.transform = updated.to_cols_array();
Some(true)
}
PROP_EXTRUSION_HEIGHT => {
let height = crate::entities::common::parse_length(text)?;
if !height.is_finite() || height <= 1e-9 {
return Some(false);
}
let direction = glam::DVec3::new(
value.direction.x,
value.direction.y,
value.direction.z,
);
let Some(unit) = direction.try_normalize() else {
return Some(false);
};
let updated = unit * height;
if (updated - direction).length_squared() <= 1e-24 {
return Some(false);
}
value.direction = acadrust::types::Vector3::new(updated.x, updated.y, updated.z);
value.end_draft_distance = height;
Some(true)
}
PROP_TAPER_ANGLE => {
let angle = crate::entities::common::parse_angle(text)?;
if !angle.is_finite()
|| angle.abs() >= std::f64::consts::FRAC_PI_2
|| (angle - value.draft_angle).abs() <= 1e-12
{
return Some(false);
}
value.draft_angle = angle;
Some(true)
}
_ => None,
}
}
pub fn apply_history_choice(
document: &mut acadrust::CadDocument,
handle: acadrust::Handle,
@@ -1362,6 +1566,13 @@ pub fn apply_primitive_property(
return applied;
}
}
if let SolidHistoryOperation::Extrusion(extrusion_value) = operation {
if let Some(applied) =
apply_extrusion_geometry_property(extrusion_value, field, value)
{
return applied;
}
}
if let SolidHistoryOperation::Torus(torus_value) = operation {
if let Some(applied) = apply_torus_position_property(torus_value, field, value) {
return applied;
@@ -1641,6 +1852,214 @@ fn local_point(transform: [f64; 16], point: glam::DVec3) -> Option<glam::DVec3>
Some(matrix(transform)?.inverse().transform_point3(point))
}
fn embedded_entity(value: &EmbeddedEntity) -> Option<EntityType> {
Some(match value {
EmbeddedEntity::Point(value) => EntityType::Point(value.clone()),
EmbeddedEntity::Line(value) => EntityType::Line(value.clone()),
EmbeddedEntity::Arc(value) => EntityType::Arc(value.clone()),
EmbeddedEntity::Circle(value) => EntityType::Circle(value.clone()),
EmbeddedEntity::Ellipse(value) => EntityType::Ellipse(value.clone()),
EmbeddedEntity::Spline(value) => EntityType::Spline(value.clone()),
EmbeddedEntity::LwPolyline(value) => EntityType::LwPolyline(value.clone()),
EmbeddedEntity::Ray(value) => EntityType::Ray(value.clone()),
EmbeddedEntity::XLine(value) => EntityType::XLine(value.clone()),
EmbeddedEntity::Unknown { .. } => return None,
})
}
fn into_embedded_entity(value: EntityType) -> Option<EmbeddedEntity> {
Some(match value {
EntityType::Point(value) => EmbeddedEntity::Point(value),
EntityType::Line(value) => EmbeddedEntity::Line(value),
EntityType::Arc(value) => EmbeddedEntity::Arc(value),
EntityType::Circle(value) => EmbeddedEntity::Circle(value),
EntityType::Ellipse(value) => EmbeddedEntity::Ellipse(value),
EntityType::Spline(value) => EmbeddedEntity::Spline(value),
EntityType::LwPolyline(value) => EmbeddedEntity::LwPolyline(value),
EntityType::Ray(value) => EmbeddedEntity::Ray(value),
EntityType::XLine(value) => EmbeddedEntity::XLine(value),
_ => return None,
})
}
fn profile_matrix(value: &SolidHistorySweep) -> Option<glam::DMat4> {
Some(matrix(value.base.transform)? * matrix(value.sweep_entity_transform)?)
}
fn extrusion_profile_grips(value: &SolidHistorySweep) -> (Vec<GripDef>, Option<glam::DVec3>) {
let Some(profile) = value.sweep_entity.as_ref().and_then(embedded_entity) else {
return (Vec::new(), None);
};
let Some(transform) = profile_matrix(value) else {
return (Vec::new(), None);
};
let source = profile.grips();
if source.is_empty() {
return (Vec::new(), None);
}
let mut center = glam::DVec3::ZERO;
let mut count = 0.0;
let grips = source
.into_iter()
.enumerate()
.filter_map(|(index, grip)| {
let world = transform.transform_point3(grip.world);
if !world.is_finite() {
return None;
}
center += world;
count += 1.0;
let transform_vector = |vector: glam::DVec3| {
let vector = transform.transform_vector3(vector);
(vector.length_squared() > 1e-12).then(|| vector.normalize())
};
Some(GripDef {
id: GRIP_PROFILE_FIRST + index,
world,
is_midpoint: grip.is_midpoint,
shape: grip.shape,
dir: grip.dir.and_then(transform_vector),
axis: grip.axis.and_then(transform_vector),
})
})
.collect::<Vec<_>>();
let center = (count > 0.0).then_some(center / count);
(grips, center)
}
fn apply_extrusion_profile_grip(
value: &mut SolidHistorySweep,
index: usize,
apply: GripApply,
) -> bool {
let Some(mut profile) = value.sweep_entity.as_ref().and_then(embedded_entity) else {
return false;
};
let Some(source_grip) = profile.grips().get(index).cloned() else {
return false;
};
let Some(transform) = profile_matrix(value) else {
return false;
};
let inverse = transform.inverse();
if !inverse.is_finite() {
return false;
}
let local_apply = match apply {
GripApply::Absolute(world) => {
let local = inverse.transform_point3(world);
if !local.is_finite() {
return false;
}
GripApply::Absolute(local)
}
GripApply::Translate(world_delta) => {
let local_delta = inverse.transform_vector3(world_delta);
if !local_delta.is_finite() || local_delta.length_squared() <= 1e-24 {
return false;
}
GripApply::Translate(local_delta)
}
};
profile.apply_grip(source_grip.id, local_apply);
let Some(profile) = into_embedded_entity(profile) else {
return false;
};
value.sweep_entity = Some(profile);
true
}
fn extrusion_draft_grip(
value: &SolidHistorySweep,
) -> Option<(glam::DVec3, glam::DVec3, glam::DVec3, f64, f64)> {
if value.path_entity.is_some() {
return None;
}
let (profile_grips, center) = extrusion_profile_grips(value);
let center = center?;
let transform = profile_matrix(value)?;
let inverse = transform.inverse();
if !inverse.is_finite() {
return None;
}
let profile = value.sweep_entity.as_ref().and_then(embedded_entity)?;
let normal = crate::entities::curve::entity_curve(&profile)?.plane.normal()?;
let normal = transform
.transform_vector3(glam::DVec3::from_array(normal))
.try_normalize()?;
let center_local = inverse.transform_point3(center);
let anchor = profile_grips
.into_iter()
.filter_map(|grip| {
let local = inverse.transform_point3(grip.world);
let radius = (grip.world - center).length_squared();
(local.is_finite() && radius > 1e-12).then_some((grip.world, local))
})
.min_by(|(_, a), (_, b)| {
a.x.total_cmp(&b.x)
.then_with(|| {
(a.y - center_local.y)
.abs()
.total_cmp(&(b.y - center_local.y).abs())
})
.then_with(|| {
(a.z - center_local.z)
.abs()
.total_cmp(&(b.z - center_local.z).abs())
})
})?
.0;
let radial = anchor - center;
let radial_length = radial.length();
if !radial_length.is_finite() || radial_length <= 1e-6 {
return None;
}
let radial = radial / radial_length;
let base = matrix(value.base.transform)?;
let direction = base.transform_vector3(glam::DVec3::new(
value.direction.x,
value.direction.y,
value.direction.z,
));
let height = direction.dot(normal).abs();
if !direction.is_finite() || !height.is_finite() || height <= 1e-6 {
return None;
}
let draft_offset = -height * value.draft_angle.tan();
if !draft_offset.is_finite() {
return None;
}
let base_top = anchor + direction;
let world = base_top + radial * draft_offset;
world
.is_finite()
.then_some((world, base_top, radial, radial_length, height))
}
fn apply_extrusion_draft_grip(value: &mut SolidHistorySweep, apply: GripApply) -> bool {
let GripApply::Absolute(world) = apply else {
return false;
};
let Some((_, base_top, radial, profile_radius, height)) = extrusion_draft_grip(value) else {
return false;
};
let top_offset = (world - base_top).dot(radial);
if !top_offset.is_finite() {
return false;
}
let limit = 89.0_f64.to_radians();
let inward_limit = ((profile_radius - 1e-6).max(1e-6) / height)
.atan()
.min(limit);
let angle = (-top_offset).atan2(height).clamp(-limit, inward_limit);
if !angle.is_finite() || (angle - value.draft_angle).abs() <= 1e-12 {
return false;
}
value.draft_angle = angle;
true
}
fn grip(
id: usize,
world: glam::DVec3,
@@ -1873,6 +2292,32 @@ pub fn primitive_grips(
GripShape::Square,
None,
),
SolidHistoryOperation::Extrusion(value) => {
let (profile_grips, profile_center) = extrusion_profile_grips(value);
grips.extend(profile_grips);
let direction = [value.direction.x, value.direction.y, value.direction.z];
if value.path_entity.is_none() {
if let (Some(center), Some(base)) = (profile_center, matrix(value.base.transform)) {
let direction = base.transform_vector3(glam::DVec3::from_array(direction));
if direction.length_squared() > 1e-12 {
grips.push(grip(
GRIP_HEIGHT,
center + direction,
GripShape::Triangle,
Some(direction.normalize()),
));
}
}
if let Some((world, _, radial, _, _)) = extrusion_draft_grip(value) {
grips.push(grip(
GRIP_DRAFT,
world,
GripShape::Triangle,
Some(radial),
));
}
}
}
_ => {}
}
grips
@@ -1883,6 +2328,14 @@ pub fn apply_primitive_grip(
grip_id: usize,
apply: GripApply,
) -> bool {
if let SolidHistoryOperation::Extrusion(value) = operation {
if let Some(index) = grip_id.checked_sub(GRIP_PROFILE_FIRST) {
return apply_extrusion_profile_grip(value, index, apply);
}
if grip_id == GRIP_DRAFT {
return apply_extrusion_draft_grip(value, apply);
}
}
let GripApply::Absolute(world) = apply else {
return false;
};
@@ -2032,6 +2485,41 @@ pub fn apply_primitive_grip(
value.base.transform =
(glam::DMat4::from_translation(delta) * current).to_cols_array();
}
SolidHistoryOperation::Extrusion(value) => match grip_id {
GRIP_HEIGHT => {
if value.path_entity.is_some() {
return false;
}
let (_, Some(profile_center)) = extrusion_profile_grips(value) else {
return false;
};
let Some(current) = matrix(value.base.transform) else {
return false;
};
let direction = current
.inverse()
.transform_vector3(world - profile_center);
let height = direction.length();
if !direction.is_finite() || !height.is_finite() || height <= 1e-6 {
return false;
}
let current = glam::DVec3::new(
value.direction.x,
value.direction.y,
value.direction.z,
);
if (direction - current).length_squared() <= 1e-24 {
return false;
}
value.direction = acadrust::types::Vector3::new(
direction.x,
direction.y,
direction.z,
);
value.end_draft_distance = height;
}
_ => return false,
},
_ => return false,
}
true
+437 -20
View File
@@ -3,7 +3,7 @@
use cadkernel::brep::{self, Body};
use cadkernel::geom2d::{Arc, Curve, EllipseArc, Line};
use cadkernel::space::{PlanarCurve, Plane, Vec3};
use acadrust::entities::{EmbeddedEntity, LwPolyline, LwVertex};
use acadrust::entities::{EmbeddedEntity, LwPolyline, LwVertex, Spline};
use acadrust::objects::{
SolidHistoryNodeBase, SolidHistoryOperation, SolidHistorySweep,
};
@@ -28,30 +28,185 @@ pub struct Profile {
/// is handed over as the arcs it is made of rather than as one curve with
/// nowhere for the sweep to start.
pub fn profile_of(entity: &EntityType) -> Option<Profile> {
let planar: PlanarCurve = entity_curve(entity)?;
let planar = entity_curve(entity).or_else(|| planar_polygon_entity(entity))?;
if !planar.curve.is_closed() {
return None;
}
let pieces = match &planar.curve {
Curve::Polyline(_) => planar.curve.segments(),
Curve::Circle(circle) => quarters(circle.centre, circle.radius),
Curve::Arc(arc) => split_arc(*arc),
Curve::Ellipse(arc) => split_ellipse(*arc),
Curve::Nurbs(curve) => (0..4)
.map(|part| {
curve
.trimmed(part as f64 / 4.0, (part + 1) as f64 / 4.0)
.map(Curve::Nurbs)
})
.collect::<Option<Vec<_>>>()?,
_ => return None,
};
let pieces = profile_pieces(&planar.curve, true)?;
(pieces.len() >= 3).then_some(Profile {
plane: planar.plane,
pieces,
})
}
/// An entity-level planar curve suitable for either a solid profile or an
/// open surface profile.
pub fn extrusion_profile_of(entity: &EntityType) -> Option<(Profile, bool)> {
let planar = entity_curve(entity).or_else(|| planar_polygon_entity(entity))?;
let closed = planar.curve.is_closed();
let pieces = profile_pieces(&planar.curve, closed)?;
(!pieces.is_empty()).then_some((
Profile {
plane: planar.plane,
pieces,
},
closed,
))
}
fn profile_pieces(curve: &Curve, closed: bool) -> Option<Vec<Curve>> {
let pieces = match curve {
Curve::Polyline(_) => curve.segments(),
Curve::Circle(circle) => quarters(circle.centre, circle.radius),
Curve::Arc(arc) if closed => split_arc(*arc),
Curve::Arc(arc) => vec![Curve::Arc(*arc)],
Curve::Ellipse(arc) if closed => split_ellipse(*arc),
Curve::Ellipse(arc) => vec![Curve::Ellipse(*arc)],
Curve::Line(line) => vec![Curve::Line(*line)],
Curve::Nurbs(nurbs) if closed => (0..4)
.map(|part| {
nurbs
.trimmed(part as f64 / 4.0, (part + 1) as f64 / 4.0)
.map(Curve::Nurbs)
})
.collect::<Option<Vec<_>>>()?,
Curve::Nurbs(nurbs) => vec![Curve::Nurbs(nurbs.clone())],
_ => return None,
};
Some(pieces)
}
fn planar_polygon_entity(entity: &EntityType) -> Option<PlanarCurve> {
let (points, closed) = match entity {
EntityType::Polyline3D(value) => (
value
.vertices
.iter()
.map(|vertex| [vertex.position.x, vertex.position.y, vertex.position.z])
.collect::<Vec<_>>(),
value.is_closed(),
),
EntityType::Face3D(value) => (
value
.corners()
.into_iter()
.map(|point| [point.x, point.y, point.z])
.collect::<Vec<_>>(),
true,
),
EntityType::Solid(value) => (
value
.boundary_corners()
.into_iter()
.map(|point| [point.x, point.y, point.z])
.collect::<Vec<_>>(),
true,
),
EntityType::Region(value) => (
value
.wires
.first()?
.points
.iter()
.map(|point| [point.x, point.y, point.z])
.collect::<Vec<_>>(),
true,
),
_ => return None,
};
let mut points = points;
if closed && points.len() > 2 && points.first() == points.last() {
points.pop();
}
if points.len() < if closed { 3 } else { 2 } {
return None;
}
let origin = Vec3::from(points[0]);
let first = Vec3::from(points[1]) - origin;
let normal = points[2..]
.iter()
.find_map(|point| first.cross(Vec3::from(*point) - origin).normalize())
.or_else(|| {
(!closed).then(|| first.cross(Vec3::Z).normalize().unwrap_or(Vec3::Y))
})?;
let plane = Plane::orthonormal(points[0], first.to_array(), normal.to_array())?;
let scale = points
.iter()
.flatten()
.map(|value| value.abs())
.fold(1.0_f64, f64::max);
if points
.iter()
.any(|point| !plane.contains(*point, scale * 1e-9))
{
return None;
}
let vertices = points
.iter()
.map(|point| {
plane.project(*point).map(|position| cadkernel::geom2d::PolylineVertex {
position,
bulge: 0.0,
})
})
.collect::<Option<Vec<_>>>()?;
Some(PlanarCurve::new(
plane,
Curve::Polyline(cadkernel::geom2d::Polyline { vertices, closed }),
))
}
fn region_profiles(entity: &EntityType) -> Option<(Plane, Vec<Vec<Curve>>)> {
let EntityType::Region(region) = entity else {
return None;
};
let plane = planar_polygon_entity(entity)?.plane;
let mut profiles = Vec::with_capacity(region.wires.len());
for wire in &region.wires {
let mut points = wire
.points
.iter()
.map(|point| [point.x, point.y, point.z])
.collect::<Vec<_>>();
if points.len() > 2 && points.first() == points.last() {
points.pop();
}
if points.len() < 3 {
return None;
}
let scale = points
.iter()
.flatten()
.map(|value| value.abs())
.fold(1.0_f64, f64::max);
if points
.iter()
.any(|point| !plane.contains(*point, scale * 1e-9))
{
return None;
}
let vertices = points
.iter()
.map(|point| {
plane
.project(*point)
.map(|position| cadkernel::geom2d::PolylineVertex {
position,
bulge: 0.0,
})
})
.collect::<Option<Vec<_>>>()?;
profiles.push(
Curve::Polyline(cadkernel::geom2d::Polyline {
vertices,
closed: true,
})
.segments(),
);
}
(!profiles.is_empty()).then_some((plane, profiles))
}
/// A circle as four arcs.
fn quarters(centre: [f64; 2], radius: f64) -> Vec<Curve> {
use std::f64::consts::FRAC_PI_2;
@@ -103,10 +258,112 @@ fn split_ellipse(arc: EllipseArc) -> Vec<Curve> {
pub fn extruded(entity: &EntityType, height: f64) -> Option<Body> {
let profile = profile_of(entity)?;
let normal = profile.plane.normal()?;
brep::extrude(
extruded_direction(
entity,
[normal[0] * height, normal[1] * height, normal[2] * height],
0.0,
)
}
pub fn extruded_direction(
entity: &EntityType,
direction: [f64; 3],
taper_angle: f64,
) -> Option<Body> {
if let Some((plane, profiles)) =
region_profiles(entity).filter(|(_, profiles)| profiles.len() > 1)
{
if taper_angle.abs() > 1e-12 {
return None;
}
return brep::extrude_region(plane, &profiles, direction);
}
let profile = profile_of(entity)?;
brep::extrude_tapered(profile.plane, &profile.pieces, direction, taper_angle)
}
pub fn extruded_surface(
entity: &EntityType,
direction: [f64; 3],
taper_angle: f64,
) -> Option<Body> {
if matches!(entity, EntityType::Region(region) if region.wires.len() > 1) {
return None;
}
let (profile, _) = extrusion_profile_of(entity)?;
brep::extrude_surface_tapered(
profile.plane,
&profile.pieces,
[normal[0] * height, normal[1] * height, normal[2] * height],
direction,
taper_angle,
)
}
pub fn extruded_along_path(
entity: &EntityType,
path: &EntityType,
taper_angle: f64,
) -> Option<Body> {
if matches!(entity, EntityType::Region(region) if region.wires.len() > 1) {
if taper_angle.abs() > 1e-12 {
return None;
}
return extruded_direction(entity, straight_path_direction(path)?, 0.0);
}
let profile = profile_of(entity)?;
if let EntityType::Polyline3D(path) = path {
let points = path
.vertices
.iter()
.map(|vertex| [vertex.position.x, vertex.position.y, vertex.position.z])
.collect::<Vec<_>>();
if taper_angle.abs() > 1e-12 {
if points.len() != 2 {
return None;
}
return brep::extrude_tapered(
profile.plane,
&profile.pieces,
(Vec3::from(points[1]) - Vec3::from(points[0])).to_array(),
taper_angle,
);
}
return brep::sweep_along_polyline3d(profile.plane, &profile.pieces, &points);
}
if taper_angle.abs() <= 1e-12 {
return swept(entity, path);
}
let curve = entity_curve(path)?;
let Curve::Line(line) = curve.curve else {
return None;
};
let direction = Vec3::from(curve.plane.point_at(line.end))
- Vec3::from(curve.plane.point_at(line.start));
brep::extrude_tapered(
profile.plane,
&profile.pieces,
direction.to_array(),
taper_angle,
)
}
pub fn straight_path_direction(path: &EntityType) -> Option<[f64; 3]> {
if let EntityType::Polyline3D(path) = path {
if path.vertices.len() != 2 {
return None;
}
let start = &path.vertices[0].position;
let end = &path.vertices[1].position;
return Some([end.x - start.x, end.y - start.y, end.z - start.z]);
}
let curve = entity_curve(path)?;
let Curve::Line(line) = curve.curve else {
return None;
};
Some(
(Vec3::from(curve.plane.point_at(line.end))
- Vec3::from(curve.plane.point_at(line.start)))
.to_array(),
)
}
@@ -142,13 +399,40 @@ pub fn revolved(
/// SWEEP as an exact B-rep along straight and circular path pieces.
pub fn swept(profile: &EntityType, path: &EntityType) -> Option<Body> {
let profile = profile_of(profile)?;
let path = entity_curve(path)?;
let mut path = entity_curve(path)?;
let pieces = match &path.curve {
Curve::Line(line) => vec![Curve::Line(*line)],
Curve::Arc(arc) => vec![Curve::Arc(*arc)],
Curve::Circle(circle) => quarters(circle.centre, circle.radius),
Curve::Polyline(_) => path.curve.segments(),
Curve::Ellipse(_) | Curve::Nurbs(_) => {
let scale = path
.curve
.point_at(0.0)
.into_iter()
.chain(path.curve.point_at(1.0))
.map(f64::abs)
.fold(1.0_f64, f64::max);
path.curve
.tessellate_within(scale * 1e-4)
.windows(2)
.filter(|pair| pair[0] != pair[1])
.map(|pair| Curve::Line(Line {
start: pair[0],
end: pair[1],
}))
.collect()
}
_ => return None,
};
let profile_center = profile
.pieces
.iter()
.map(|piece| Vec3::from(profile.plane.point_at(piece.point_at(0.0))))
.fold(Vec3::ZERO, |sum, point| sum + point)
/ profile.pieces.len() as f64;
let path_start = Vec3::from(path.plane.point_at(path.curve.point_at(0.0)));
path.plane.origin = (Vec3::from(path.plane.origin) + profile_center - path_start).to_array();
brep::sweep_along(profile.plane, &profile.pieces, path.plane, &pieces)
}
@@ -174,7 +458,7 @@ pub enum PolysolidJustification {
Right,
}
fn embedded_path(entity: &EntityType) -> Option<EmbeddedEntity> {
pub fn embedded_path(entity: &EntityType) -> Option<EmbeddedEntity> {
match entity {
EntityType::Line(value) => Some(EmbeddedEntity::Line(value.clone())),
EntityType::Arc(value) => Some(EmbeddedEntity::Arc(value.clone())),
@@ -182,10 +466,143 @@ fn embedded_path(entity: &EntityType) -> Option<EmbeddedEntity> {
EntityType::Ellipse(value) => Some(EmbeddedEntity::Ellipse(value.clone())),
EntityType::LwPolyline(value) => Some(EmbeddedEntity::LwPolyline(value.clone())),
EntityType::Spline(value) => Some(EmbeddedEntity::Spline(value.clone())),
EntityType::Polyline3D(value) if !value.is_closed() && value.vertices.len() >= 2 => {
let mut spline = Spline::from_control_points(
1,
value
.vertices
.iter()
.map(|vertex| vertex.position)
.collect(),
);
spline.flags.linear = true;
spline.flags.planar = false;
Some(EmbeddedEntity::Spline(spline))
}
_ => None,
}
}
fn embedded_planar_entity(entity: &EntityType) -> Option<(EmbeddedEntity, [f64; 16])> {
if matches!(entity, EntityType::Region(region) if region.wires.len() > 1) {
return None;
}
if !matches!(entity, EntityType::Polyline3D(_)) {
if let Some(embedded) = embedded_path(entity) {
return Some((embedded, glam::DMat4::IDENTITY.to_cols_array()));
}
}
let (profile, closed) = extrusion_profile_of(entity)?;
let mut polyline = LwPolyline::new();
for piece in &profile.pieces {
let Curve::Line(line) = piece else {
return None;
};
polyline
.vertices
.push(LwVertex::new(Vector2::new(line.start[0], line.start[1])));
}
if !closed {
let Curve::Line(last) = profile.pieces.last()? else {
return None;
};
polyline
.vertices
.push(LwVertex::new(Vector2::new(last.end[0], last.end[1])));
}
polyline.is_closed = closed;
let normal = profile.plane.normal()?;
let transform = glam::DMat4::from_cols(
glam::DVec4::new(
profile.plane.x_axis[0],
profile.plane.x_axis[1],
profile.plane.x_axis[2],
0.0,
),
glam::DVec4::new(
profile.plane.y_axis[0],
profile.plane.y_axis[1],
profile.plane.y_axis[2],
0.0,
),
glam::DVec4::new(normal[0], normal[1], normal[2], 0.0),
glam::DVec4::new(
profile.plane.origin[0],
profile.plane.origin[1],
profile.plane.origin[2],
1.0,
),
)
.to_cols_array();
Some((EmbeddedEntity::LwPolyline(polyline), transform))
}
pub fn extrusion_history(
profile: &EntityType,
path: Option<&EntityType>,
direction: [f64; 3],
taper_angle: f64,
reference_point: [f64; 3],
) -> Option<SolidHistoryOperation> {
let signed_height = Vec3::from(direction).length();
let mut base = SolidHistoryNodeBase::new(1);
base.transform = glam::DMat4::IDENTITY.to_cols_array();
let (sweep_entity, sweep_entity_transform) = embedded_planar_entity(profile)?;
let (path_entity, path_entity_transform) = match path {
Some(path) => {
let (entity, transform) = embedded_planar_entity(path)?;
(Some(entity), transform)
}
None => (None, glam::DMat4::IDENTITY.to_cols_array()),
};
Some(SolidHistoryOperation::Extrusion(SolidHistorySweep {
base,
operation_major: 1,
direction: Vector3::new(direction[0], direction[1], direction[2]),
sweep_entity: Some(sweep_entity),
path_entity,
draft_angle: taper_angle,
start_draft_distance: 0.0,
end_draft_distance: signed_height,
scale_factor: 1.0,
sweep_entity_transform,
path_entity_transform,
reference_point: Vector3::new(
reference_point[0],
reference_point[1],
reference_point[2],
),
..SolidHistorySweep::default()
}))
}
pub fn sweep_history(
profile: &EntityType,
path: &EntityType,
taper_angle: f64,
reference_point: [f64; 3],
) -> Option<SolidHistoryOperation> {
let mut base = SolidHistoryNodeBase::new(1);
base.transform = glam::DMat4::IDENTITY.to_cols_array();
let (sweep_entity, sweep_entity_transform) = embedded_planar_entity(profile)?;
Some(SolidHistoryOperation::Sweep(SolidHistorySweep {
base,
operation_major: 1,
sweep_entity: Some(sweep_entity),
path_entity: Some(embedded_path(path)?),
draft_angle: taper_angle,
scale_factor: 1.0,
sweep_entity_transform,
path_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
reference_point: Vector3::new(
reference_point[0],
reference_point[1],
reference_point[2],
),
..SolidHistorySweep::default()
}))
}
/// Builds a history-backed wall solid along a supported planar curve.
pub fn polysolid(
entity: &EntityType,