forked from 1b-agent/opencadstudio-upstream
Merge main and preserve revolve history grips
This commit is contained in:
commit
b1deec753c
38 files changed
+2989
-453
No files matched your search
@@ -274,6 +274,16 @@ impl MeshLodSet {
|
||||
.map_or(self.lods.as_slice(), |source| source.lods.as_slice())
|
||||
}
|
||||
|
||||
pub fn geometry_edges(&self) -> (&[[f32; 3]], &[[f32; 3]]) {
|
||||
self.instance_source.as_ref().map_or(
|
||||
(self.edge_verts.as_slice(), self.edge_verts_low.as_slice()),
|
||||
|source| (
|
||||
source.edge_verts.as_slice(),
|
||||
source.edge_verts_low.as_slice(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn entity_handle(&self) -> Option<acadrust::Handle> {
|
||||
self.instance_handle.or_else(|| {
|
||||
self.lods
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use acadrust::entities::{EmbeddedEntity, Solid3D};
|
||||
use acadrust::objects::{
|
||||
DynamicBlockData, ObjectType, SolidHistoryBox, SolidHistoryBrep, SolidHistoryCone,
|
||||
SolidHistoryCylinder, SolidHistoryNodeBase, SolidHistoryOperation,
|
||||
SolidHistoryCylinder, SolidHistoryLoft, SolidHistoryNodeBase, SolidHistoryOperation,
|
||||
SolidHistoryPyramid, SolidHistoryRevolve, SolidHistorySphere, SolidHistorySweep,
|
||||
SolidHistoryTorus,
|
||||
};
|
||||
@@ -25,9 +25,13 @@ pub const GRIP_SIDES: usize = 10_007;
|
||||
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_REVOLVE_AXIS: usize = 10_012;
|
||||
pub const GRIP_DRAFT: usize = 10_012;
|
||||
pub const GRIP_PROFILE_FIRST: usize = 10_200;
|
||||
pub const GRIP_SWEEP_PROFILE_FIRST: usize = 20_000;
|
||||
pub const GRIP_SWEEP_PATH_FIRST: usize = 30_000;
|
||||
pub const GRIP_LOFT_SECTION_FIRST: usize = 40_000;
|
||||
pub const GRIP_REVOLVE_PROFILE_FIRST: usize = 50_000;
|
||||
pub const GRIP_REVOLVE_AXIS: usize = 10_013;
|
||||
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;
|
||||
@@ -62,6 +66,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";
|
||||
@@ -115,6 +121,7 @@ pub fn has_specialized_primitive_properties(
|
||||
| SolidHistoryOperation::Torus(_)
|
||||
| SolidHistoryOperation::Pyramid(_)
|
||||
| SolidHistoryOperation::Sweep(_)
|
||||
| SolidHistoryOperation::Extrusion(_)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -130,7 +137,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,
|
||||
@@ -144,6 +151,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,
|
||||
@@ -231,7 +332,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()),
|
||||
@@ -894,6 +1018,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(),
|
||||
}
|
||||
}
|
||||
@@ -924,6 +1051,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
|
||||
)
|
||||
}
|
||||
@@ -938,6 +1067,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,
|
||||
@@ -1367,6 +1571,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;
|
||||
@@ -1676,6 +1887,268 @@ fn into_embedded_entity(value: EntityType) -> Option<EmbeddedEntity> {
|
||||
})
|
||||
}
|
||||
|
||||
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 sweep_embedded_grips(
|
||||
value: Option<&EmbeddedEntity>,
|
||||
base_transform: [f64; 16],
|
||||
entity_transform: [f64; 16],
|
||||
first_id: usize,
|
||||
) -> Vec<GripDef> {
|
||||
let Some(entity) = value.and_then(embedded_entity) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let (Some(base), Some(entity_placement)) =
|
||||
(matrix(base_transform), matrix(entity_transform))
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
let transform = base * entity_placement;
|
||||
entity
|
||||
.grips()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, grip)| {
|
||||
let world = transform.transform_point3(grip.world);
|
||||
if !world.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let transform_vector = |vector: glam::DVec3| {
|
||||
let vector = transform.transform_vector3(vector);
|
||||
(vector.length_squared() > 1e-12).then(|| vector.normalize())
|
||||
};
|
||||
Some(GripDef {
|
||||
id: first_id + 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()
|
||||
}
|
||||
|
||||
fn apply_sweep_embedded_grip(
|
||||
value: &mut Option<EmbeddedEntity>,
|
||||
base_transform: [f64; 16],
|
||||
entity_transform: [f64; 16],
|
||||
index: usize,
|
||||
apply: GripApply,
|
||||
) -> bool {
|
||||
let Some(mut entity) = value.as_ref().and_then(embedded_entity) else {
|
||||
return false;
|
||||
};
|
||||
let Some(source_grip) = entity.grips().get(index).cloned() else {
|
||||
return false;
|
||||
};
|
||||
let (Some(base), Some(entity_placement)) =
|
||||
(matrix(base_transform), matrix(entity_transform))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let inverse = (base * entity_placement).inverse();
|
||||
if !inverse.is_finite() {
|
||||
return false;
|
||||
}
|
||||
let 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)
|
||||
}
|
||||
};
|
||||
entity.apply_grip(source_grip.id, apply);
|
||||
let Some(entity) = into_embedded_entity(entity) else {
|
||||
return false;
|
||||
};
|
||||
*value = Some(entity);
|
||||
true
|
||||
}
|
||||
|
||||
fn loft_section_grips(value: &SolidHistoryLoft) -> Vec<GripDef> {
|
||||
if !value.guides.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let Some(transform) = matrix(value.base.transform) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut result = Vec::new();
|
||||
let mut index = 0;
|
||||
for section in &value.cross_sections {
|
||||
let Some(entity) = embedded_entity(section) else {
|
||||
continue;
|
||||
};
|
||||
for source in entity.grips() {
|
||||
let id = GRIP_LOFT_SECTION_FIRST + index;
|
||||
index += 1;
|
||||
let world = transform.transform_point3(source.world);
|
||||
if !world.is_finite() {
|
||||
continue;
|
||||
}
|
||||
let transform_vector = |vector: glam::DVec3| {
|
||||
let vector = transform.transform_vector3(vector);
|
||||
(vector.length_squared() > 1e-12).then(|| vector.normalize())
|
||||
};
|
||||
result.push(GripDef {
|
||||
id,
|
||||
world,
|
||||
is_midpoint: source.is_midpoint,
|
||||
shape: source.shape,
|
||||
dir: source.dir.and_then(transform_vector),
|
||||
axis: source.axis.and_then(transform_vector),
|
||||
});
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn apply_loft_section_grip(
|
||||
value: &mut SolidHistoryLoft,
|
||||
mut index: usize,
|
||||
apply: GripApply,
|
||||
) -> bool {
|
||||
if !value.guides.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let Some(transform) = matrix(value.base.transform) 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)
|
||||
}
|
||||
};
|
||||
for section in &mut value.cross_sections {
|
||||
let Some(mut entity) = embedded_entity(section) else {
|
||||
continue;
|
||||
};
|
||||
let grips = entity.grips();
|
||||
if index >= grips.len() {
|
||||
index -= grips.len();
|
||||
continue;
|
||||
}
|
||||
let grip_id = grips[index].id;
|
||||
entity.apply_grip(grip_id, local_apply);
|
||||
let Some(entity) = into_embedded_entity(entity) else {
|
||||
return false;
|
||||
};
|
||||
*section = entity;
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn revolve_profile_matrix(value: &SolidHistoryRevolve) -> Option<glam::DMat4> {
|
||||
let base = matrix(value.base.transform)?;
|
||||
let axis_point = glam::DVec3::new(
|
||||
@@ -1684,8 +2157,8 @@ fn revolve_profile_matrix(value: &SolidHistoryRevolve) -> Option<glam::DMat4> {
|
||||
value.axis_point.z,
|
||||
);
|
||||
let axis = glam::DVec3::new(value.direction.x, value.direction.y, value.direction.z)
|
||||
.normalize_or_zero();
|
||||
if !axis_point.is_finite() || axis.length_squared() <= 1e-12 {
|
||||
.try_normalize()?;
|
||||
if !axis_point.is_finite() || !value.start_angle.is_finite() {
|
||||
return None;
|
||||
}
|
||||
Some(
|
||||
@@ -1770,6 +2243,96 @@ fn apply_revolve_profile_grip(
|
||||
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,
|
||||
@@ -1991,17 +2554,21 @@ pub fn primitive_grips(
|
||||
None,
|
||||
);
|
||||
}
|
||||
SolidHistoryOperation::Sweep(value) => add(
|
||||
GRIP_POSITION,
|
||||
value.base.transform,
|
||||
[
|
||||
value.reference_point.x,
|
||||
value.reference_point.y,
|
||||
value.reference_point.z,
|
||||
],
|
||||
GripShape::Square,
|
||||
None,
|
||||
),
|
||||
SolidHistoryOperation::Sweep(value) => {
|
||||
grips.extend(sweep_embedded_grips(
|
||||
value.sweep_entity.as_ref(),
|
||||
value.base.transform,
|
||||
value.sweep_entity_transform,
|
||||
GRIP_SWEEP_PROFILE_FIRST,
|
||||
));
|
||||
grips.extend(sweep_embedded_grips(
|
||||
value.path_entity.as_ref(),
|
||||
value.base.transform,
|
||||
value.path_entity_transform,
|
||||
GRIP_SWEEP_PATH_FIRST,
|
||||
));
|
||||
}
|
||||
SolidHistoryOperation::Loft(value) => grips.extend(loft_section_grips(value)),
|
||||
SolidHistoryOperation::Revolve(value) => {
|
||||
add(
|
||||
GRIP_REVOLVE_AXIS,
|
||||
@@ -2012,6 +2579,32 @@ pub fn primitive_grips(
|
||||
);
|
||||
grips.extend(revolve_profile_grips(value));
|
||||
}
|
||||
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
|
||||
@@ -2027,6 +2620,42 @@ pub fn apply_primitive_grip(
|
||||
return apply_revolve_profile_grip(value, index, apply);
|
||||
}
|
||||
}
|
||||
if let SolidHistoryOperation::Loft(value) = operation {
|
||||
if let Some(index) = grip_id.checked_sub(GRIP_LOFT_SECTION_FIRST) {
|
||||
return apply_loft_section_grip(value, index, apply);
|
||||
}
|
||||
}
|
||||
if let SolidHistoryOperation::Sweep(value) = operation {
|
||||
if let Some(index) = grip_id.checked_sub(GRIP_SWEEP_PATH_FIRST) {
|
||||
return apply_sweep_embedded_grip(
|
||||
&mut value.path_entity,
|
||||
value.base.transform,
|
||||
value.path_entity_transform,
|
||||
index,
|
||||
apply,
|
||||
);
|
||||
}
|
||||
if let Some(index) = grip_id
|
||||
.checked_sub(GRIP_SWEEP_PROFILE_FIRST)
|
||||
.filter(|_| grip_id < GRIP_SWEEP_PATH_FIRST)
|
||||
{
|
||||
return apply_sweep_embedded_grip(
|
||||
&mut value.sweep_entity,
|
||||
value.base.transform,
|
||||
value.sweep_entity_transform,
|
||||
index,
|
||||
apply,
|
||||
);
|
||||
}
|
||||
}
|
||||
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;
|
||||
};
|
||||
@@ -2160,25 +2789,44 @@ pub fn apply_primitive_grip(
|
||||
}
|
||||
_ => return false,
|
||||
},
|
||||
SolidHistoryOperation::Sweep(value) if grip_id == GRIP_POSITION => {
|
||||
let Some(current) = matrix(value.base.transform) else {
|
||||
return false;
|
||||
};
|
||||
let reference = current.transform_point3(glam::DVec3::new(
|
||||
value.reference_point.x,
|
||||
value.reference_point.y,
|
||||
value.reference_point.z,
|
||||
));
|
||||
let delta = world - reference;
|
||||
if !delta.is_finite() || delta.length_squared() <= 1e-24 {
|
||||
return false;
|
||||
}
|
||||
value.base.transform =
|
||||
(glam::DMat4::from_translation(delta) * current).to_cols_array();
|
||||
}
|
||||
SolidHistoryOperation::Revolve(value) if grip_id == GRIP_REVOLVE_AXIS => {
|
||||
value.axis_point = acadrust::types::Vector3::new(local.x, local.y, local.z);
|
||||
}
|
||||
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
|
||||
|
||||
+466
-42
@@ -3,9 +3,9 @@
|
||||
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, SolidHistoryRevolve, SolidHistorySweep,
|
||||
SolidHistoryLoft, SolidHistoryNodeBase, SolidHistoryOperation, SolidHistorySweep,
|
||||
};
|
||||
use acadrust::types::{Vector2, Vector3};
|
||||
use acadrust::EntityType;
|
||||
@@ -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 ®ion.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(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -139,26 +396,13 @@ pub fn revolved(
|
||||
)
|
||||
}
|
||||
|
||||
fn profile_bounds_center(profile: &Profile) -> Option<Vec3> {
|
||||
let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
|
||||
let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
|
||||
let mut found = false;
|
||||
for piece in &profile.pieces {
|
||||
for point in piece.tessellate_within(1e-4) {
|
||||
let point = Vec3::from(profile.plane.point_at(point));
|
||||
if !point.is_finite() {
|
||||
continue;
|
||||
}
|
||||
low.x = low.x.min(point.x);
|
||||
low.y = low.y.min(point.y);
|
||||
low.z = low.z.min(point.z);
|
||||
high.x = high.x.max(point.x);
|
||||
high.y = high.y.max(point.y);
|
||||
high.z = high.z.max(point.z);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
found.then(|| (low + high) * 0.5)
|
||||
fn profile_center(profile: &Profile) -> Option<Vec3> {
|
||||
let count = profile.pieces.len();
|
||||
let sum = profile.pieces.iter().try_fold(Vec3::ZERO, |sum, piece| {
|
||||
let point = Vec3::from(profile.plane.point_at(piece.point_at(0.0)));
|
||||
point.is_finite().then_some(sum + point)
|
||||
})?;
|
||||
(count > 0).then_some(sum / count as f64)
|
||||
}
|
||||
|
||||
/// Stores the source profile and canonical axis required to rebuild and edit
|
||||
@@ -175,7 +419,7 @@ pub fn revolve_history(
|
||||
let profile = profile_of(entity)?;
|
||||
let from = Vec3::from(from);
|
||||
let direction = (Vec3::from(to) - from).normalize()?;
|
||||
let center = profile_bounds_center(&profile)?;
|
||||
let center = profile_center(&profile)?;
|
||||
let axis_point = from + direction * (center - from).dot(direction);
|
||||
let mut base = SolidHistoryNodeBase::new(1);
|
||||
base.transform = glam::DMat4::IDENTITY.to_cols_array();
|
||||
@@ -194,13 +438,40 @@ pub fn revolve_history(
|
||||
/// 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)
|
||||
}
|
||||
|
||||
@@ -219,6 +490,26 @@ pub fn lofted(profiles: &[EntityType]) -> Option<Body> {
|
||||
brep::loft(§ions)
|
||||
}
|
||||
|
||||
/// Stores the source sections required to rebuild and edit a loft.
|
||||
pub fn loft_history(profiles: &[EntityType]) -> Option<SolidHistoryOperation> {
|
||||
let cross_sections = profiles
|
||||
.iter()
|
||||
.map(embedded_path)
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
if cross_sections.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let mut base = SolidHistoryNodeBase::new(1);
|
||||
base.transform = glam::DMat4::IDENTITY.to_cols_array();
|
||||
Some(SolidHistoryOperation::Loft(SolidHistoryLoft {
|
||||
base,
|
||||
operation_major: 1,
|
||||
cross_sections,
|
||||
guides: Vec::new(),
|
||||
..SolidHistoryLoft::default()
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum PolysolidJustification {
|
||||
Left,
|
||||
@@ -226,7 +517,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())),
|
||||
@@ -234,10 +525,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,
|
||||
|
||||
Reference in new issue
Block a user