forked from 1b-agent/opencadstudio-upstream
Complete EXTRUDE behavior and properties
This commit is contained in:
1 parent
bde9cc2715
commit
847d218c35
13 files changed
+912
-108
No files matched your search
Generated
+1
-1
@@ -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=1ad942cc34b592917ca5fff9936afb902109a29d#1ad942cc34b592917ca5fff9936afb902109a29d"
|
||||
dependencies = [
|
||||
"acadrust",
|
||||
"cavalier_contours",
|
||||
|
||||
+1
-1
@@ -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 = "1ad942cc34b592917ca5fff9936afb902109a29d", features = ["acis", "offset"] }
|
||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||
flate2 = "1"
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||
|
||||
+151
-21
@@ -3454,42 +3454,172 @@ 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) => {
|
||||
if delete_sources && self.reject_locked_edit(i, handle) {
|
||||
self.tabs[i].active_cmd = None;
|
||||
return Task::none();
|
||||
}
|
||||
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)| {
|
||||
crate::entities::curve::entity_curve(path).and_then(|curve| {
|
||||
let cadkernel::geom2d::Curve::Line(line) = curve.curve else {
|
||||
return None;
|
||||
};
|
||||
let start = glam::DVec3::from_array(curve.plane.point_at(line.start));
|
||||
let end = glam::DVec3::from_array(curve.plane.point_at(line.end));
|
||||
Some(end - start)
|
||||
})
|
||||
});
|
||||
// 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)), _) if taper_angle.abs() <= 1e-12 => {
|
||||
sweep_model::extruded_along_path(&entity, path)
|
||||
}
|
||||
(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(), 0.0)
|
||||
}),
|
||||
_ => 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 = sweep_model::extrusion_history(
|
||||
&entity,
|
||||
path.as_ref().map(|(_, entity)| entity),
|
||||
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() {
|
||||
if let Some((path_handle, _)) = path {
|
||||
consumed.push(path_handle);
|
||||
}
|
||||
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 {
|
||||
|
||||
+18
-12
@@ -736,21 +736,27 @@ 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, _)| *handle).collect(),
|
||||
anchor,
|
||||
direction,
|
||||
);
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
} else {
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
@@ -1408,10 +1421,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.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use acadrust::{entities::Solid3D, EntityType};
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, ExtrudeExtent, ExtrudeMode};
|
||||
use crate::t;
|
||||
|
||||
// ── EXTRUDE command ────────────────────────────────────────────────────────
|
||||
@@ -11,16 +11,25 @@ use crate::t;
|
||||
pub struct ExtrudeCommand {
|
||||
command_name: &'static str,
|
||||
step: ExtrudeStep,
|
||||
pub target_handle: acadrust::Handle,
|
||||
handles: Vec<acadrust::Handle>,
|
||||
anchor: DVec3,
|
||||
direction: Option<DVec3>,
|
||||
profile_direction: Option<DVec3>,
|
||||
direction_start: Option<DVec3>,
|
||||
mode: ExtrudeMode,
|
||||
taper_angle: f64,
|
||||
color: [f32; 4],
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum ExtrudeStep {
|
||||
Pick,
|
||||
Mode,
|
||||
Height,
|
||||
DirectionStart,
|
||||
DirectionEnd,
|
||||
Path,
|
||||
Taper,
|
||||
Expression,
|
||||
}
|
||||
|
||||
impl ExtrudeCommand {
|
||||
@@ -31,12 +40,39 @@ impl ExtrudeCommand {
|
||||
_ => "EXTRUDE",
|
||||
},
|
||||
step: ExtrudeStep::Pick,
|
||||
target_handle: acadrust::Handle::NULL,
|
||||
handles: Vec::new(),
|
||||
anchor: DVec3::ZERO,
|
||||
direction: None,
|
||||
profile_direction: None,
|
||||
direction_start: None,
|
||||
mode: ExtrudeMode::Solid,
|
||||
taper_angle: 0.0,
|
||||
color,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_preselection(
|
||||
&mut self,
|
||||
handles: Vec<acadrust::Handle>,
|
||||
anchor: DVec3,
|
||||
direction: Option<DVec3>,
|
||||
) {
|
||||
self.handles = handles;
|
||||
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 {
|
||||
CmdResult::ExtrudeEntities {
|
||||
handles: self.handles.clone(),
|
||||
extent,
|
||||
mode: self.mode,
|
||||
taper_angle: self.taper_angle,
|
||||
color: self.color,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for ExtrudeCommand {
|
||||
@@ -45,63 +81,170 @@ 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 => t!("EXTRUDE Creation mode [Solid/Surface]:").into_owned(),
|
||||
ExtrudeStep::Height => t!("EXTRUDE Specify height or [Direction/Path/Taper angle/Expression]:").into_owned(),
|
||||
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:").into_owned(),
|
||||
ExtrudeStep::Taper => t!("EXTRUDE Specify taper angle:").into_owned(),
|
||||
ExtrudeStep::Expression => t!("EXTRUDE Enter height 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"),
|
||||
],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
self.step == ExtrudeStep::Pick
|
||||
matches!(self.step, ExtrudeStep::Pick | 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 {
|
||||
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;
|
||||
}
|
||||
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 {
|
||||
matches!(
|
||||
self.step,
|
||||
ExtrudeStep::Pick
|
||||
| ExtrudeStep::Mode
|
||||
| ExtrudeStep::Height
|
||||
| ExtrudeStep::Taper
|
||||
| ExtrudeStep::Expression
|
||||
)
|
||||
}
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
self.step == ExtrudeStep::Height
|
||||
}
|
||||
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();
|
||||
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);
|
||||
}
|
||||
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.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::Taper => {
|
||||
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;
|
||||
self.step = ExtrudeStep::Height;
|
||||
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))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
if self.step == ExtrudeStep::Pick && !self.handles.is_empty() {
|
||||
self.step = ExtrudeStep::Height;
|
||||
CmdResult::NeedPoint
|
||||
} else {
|
||||
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 +257,7 @@ 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?))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,6 +622,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"]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -57,6 +57,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 +112,7 @@ pub fn has_specialized_primitive_properties(
|
||||
| SolidHistoryOperation::Torus(_)
|
||||
| SolidHistoryOperation::Pyramid(_)
|
||||
| SolidHistoryOperation::Sweep(_)
|
||||
| SolidHistoryOperation::Extrusion(_)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -125,7 +128,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 +142,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,
|
||||
@@ -889,6 +986,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 +1019,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 +1035,86 @@ 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.abs() <= 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 sign = if height.signum() == value.end_draft_distance.signum() {
|
||||
1.0
|
||||
} else {
|
||||
-1.0
|
||||
};
|
||||
let updated = unit * height.abs() * sign;
|
||||
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 +1544,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;
|
||||
|
||||
+275
-19
@@ -32,26 +32,130 @@ pub fn profile_of(entity: &EntityType) -> Option<Profile> {
|
||||
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 }),
|
||||
))
|
||||
}
|
||||
|
||||
/// A circle as four arcs.
|
||||
fn quarters(centre: [f64; 2], radius: f64) -> Vec<Curve> {
|
||||
use std::f64::consts::FRAC_PI_2;
|
||||
@@ -103,13 +207,46 @@ 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(
|
||||
profile.plane,
|
||||
&profile.pieces,
|
||||
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> {
|
||||
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> {
|
||||
let (profile, closed) = extrusion_profile_of(entity)?;
|
||||
if closed {
|
||||
brep::extrude_surface_tapered(
|
||||
profile.plane,
|
||||
&profile.pieces,
|
||||
direction,
|
||||
taper_angle,
|
||||
)
|
||||
} else if taper_angle.abs() <= 1e-12 {
|
||||
brep::extrude_surface(profile.plane, &profile.pieces, direction)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extruded_along_path(entity: &EntityType, path: &EntityType) -> Option<Body> {
|
||||
swept(entity, path)
|
||||
}
|
||||
|
||||
/// Signed distance of a drag along a profile's normal.
|
||||
pub fn projected_drag(entity: &EntityType, from: glam::DVec3, to: glam::DVec3) -> Option<f64> {
|
||||
let normal = entity_curve(entity)?.plane.normal()?;
|
||||
@@ -142,13 +279,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 +338,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())),
|
||||
@@ -186,6 +350,98 @@ fn embedded_path(entity: &EntityType) -> Option<EmbeddedEntity> {
|
||||
}
|
||||
}
|
||||
|
||||
fn embedded_planar_entity(entity: &EntityType) -> Option<(EmbeddedEntity, [f64; 16])> {
|
||||
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 = extrusion_profile_of(profile)
|
||||
.and_then(|(profile, _)| profile.plane.normal())
|
||||
.map(|normal| Vec3::from(direction).dot(Vec3::from(normal)))
|
||||
.filter(|value| value.is_finite())
|
||||
.unwrap_or_else(|| 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()
|
||||
}))
|
||||
}
|
||||
|
||||
/// Builds a history-backed wall solid along a supported planar curve.
|
||||
pub fn polysolid(
|
||||
entity: &EntityType,
|
||||
|
||||
Reference in new issue
Block a user