forked from 1b-agent/opencadstudio-upstream
Merge pull request #954 from ramox81/codex/pyramid-command-properties
Match pyramid creation and properties behavior
This commit is contained in:
7 files changed
+1055
-27
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=48c634995fa60d3928c37bae49b12b421c56c886#48c634995fa60d3928c37bae49b12b421c56c886"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=67ff1f59f380afcfd939fa98883012a0c7b109f7#67ff1f59f380afcfd939fa98883012a0c7b109f7"
|
||||
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 = "48c634995fa60d3928c37bae49b12b421c56c886", features = ["acis", "offset"] }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "67ff1f59f380afcfd939fa98883012a0c7b109f7", features = ["acis", "offset"] }
|
||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||
flate2 = "1"
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||
|
||||
@@ -699,8 +699,10 @@ impl super::OpenCADStudio {
|
||||
let history = solid_history::pyramid_op(
|
||||
glam::DMat4::IDENTITY.to_cols_array(),
|
||||
radius,
|
||||
0.0,
|
||||
height,
|
||||
n,
|
||||
true,
|
||||
);
|
||||
let handle = self.add_solid_model(entity, solid, history);
|
||||
self.tabs[i].scene.deselect_all();
|
||||
|
||||
@@ -2074,6 +2074,9 @@ impl OpenCADStudio {
|
||||
});
|
||||
}
|
||||
}
|
||||
if specialized_primitive {
|
||||
retain_specialized_primitive_sections(&mut sections);
|
||||
}
|
||||
let title = match entity {
|
||||
acadrust::EntityType::Insert(ins) => {
|
||||
let is_xref = self.tabs[i]
|
||||
@@ -3047,6 +3050,30 @@ fn aggregate_solid_history_sections(
|
||||
merged
|
||||
}
|
||||
|
||||
fn retain_specialized_primitive_sections(
|
||||
sections: &mut Vec<crate::scene::model::object::PropSection>,
|
||||
) {
|
||||
sections.iter_mut().for_each(|section| {
|
||||
section.props.retain(|property| {
|
||||
matches!(
|
||||
property.field,
|
||||
"color"
|
||||
| "layer"
|
||||
| "linetype"
|
||||
| "linetype_scale"
|
||||
| "plot_style"
|
||||
| "lineweight"
|
||||
| "transparency"
|
||||
| "hyperlink"
|
||||
| "material"
|
||||
| "solid_history_type"
|
||||
) || crate::scene::model::solid_history::is_primitive_property(property.field)
|
||||
|| crate::scene::model::solid_history::is_history_choice(property.field)
|
||||
});
|
||||
});
|
||||
sections.retain(|section| !section.props.is_empty());
|
||||
}
|
||||
|
||||
fn merge_sections(
|
||||
left: &[crate::scene::model::object::PropSection],
|
||||
right: &[crate::scene::model::object::PropSection],
|
||||
|
||||
@@ -99,6 +99,48 @@ enum ConeStep {
|
||||
TopRadius,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum PyramidType {
|
||||
Circumscribed,
|
||||
Inscribed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum PyramidStep {
|
||||
BaseCenter,
|
||||
Sides,
|
||||
EdgeFirst,
|
||||
EdgeSecond,
|
||||
BaseRadius,
|
||||
Height,
|
||||
HeightAfterTopRadius,
|
||||
HeightFirstPoint,
|
||||
HeightSecondPoint,
|
||||
AxisEndpoint,
|
||||
TopRadius,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct PyramidDefaults {
|
||||
displayed_base_radius: f64,
|
||||
displayed_top_radius: f64,
|
||||
height: f64,
|
||||
sides: usize,
|
||||
kind: PyramidType,
|
||||
}
|
||||
|
||||
impl Default for PyramidDefaults {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
displayed_base_radius: 1.0,
|
||||
displayed_top_radius: 0.0,
|
||||
height: 1.0,
|
||||
sides: 4,
|
||||
kind: PyramidType::Circumscribed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct ConeDefaults {
|
||||
base_x_radius: f64,
|
||||
@@ -211,6 +253,29 @@ fn cone_defaults() -> &'static std::sync::Mutex<ConeDefaults> {
|
||||
DEFAULTS.get_or_init(|| std::sync::Mutex::new(ConeDefaults::default()))
|
||||
}
|
||||
|
||||
fn pyramid_defaults() -> &'static Mutex<PyramidDefaults> {
|
||||
static DEFAULTS: OnceLock<Mutex<PyramidDefaults>> = OnceLock::new();
|
||||
DEFAULTS.get_or_init(|| Mutex::new(PyramidDefaults::default()))
|
||||
}
|
||||
|
||||
fn pyramid_apothem_factor(sides: usize) -> f64 {
|
||||
(std::f64::consts::PI / sides.max(3) as f64).cos()
|
||||
}
|
||||
|
||||
fn pyramid_display_to_circumradius(value: f64, kind: PyramidType, sides: usize) -> f64 {
|
||||
match kind {
|
||||
PyramidType::Circumscribed => value / pyramid_apothem_factor(sides),
|
||||
PyramidType::Inscribed => value,
|
||||
}
|
||||
}
|
||||
|
||||
fn pyramid_circumradius_to_display(value: f64, kind: PyramidType, sides: usize) -> f64 {
|
||||
match kind {
|
||||
PyramidType::Circumscribed => value * pyramid_apothem_factor(sides),
|
||||
PyramidType::Inscribed => value,
|
||||
}
|
||||
}
|
||||
|
||||
impl Shape {
|
||||
fn from_id(id: &str) -> Option<Shape> {
|
||||
Some(match id {
|
||||
@@ -271,6 +336,15 @@ pub struct PrimitiveCommand {
|
||||
cone_base_y_radius: f64,
|
||||
cone_top_radius: f64,
|
||||
cone_defaults: ConeDefaults,
|
||||
pyramid_step: PyramidStep,
|
||||
pyramid_frame: Option<WorkingPlane>,
|
||||
pyramid_edge_first: Option<DVec3>,
|
||||
pyramid_height_first: Option<DVec3>,
|
||||
pyramid_base_radius: f64,
|
||||
pyramid_top_radius: f64,
|
||||
pyramid_sides: usize,
|
||||
pyramid_type: PyramidType,
|
||||
pyramid_defaults: PyramidDefaults,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
@@ -280,6 +354,10 @@ impl PrimitiveCommand {
|
||||
.lock()
|
||||
.map(|value| *value)
|
||||
.unwrap_or_default();
|
||||
let pyramid_remembered = pyramid_defaults()
|
||||
.lock()
|
||||
.map(|value| *value)
|
||||
.unwrap_or_default();
|
||||
Self {
|
||||
shape: Shape::from_id(id).unwrap_or(Shape::Box),
|
||||
pts: Vec::new(),
|
||||
@@ -300,6 +378,23 @@ impl PrimitiveCommand {
|
||||
cone_base_y_radius: remembered.base_y_radius,
|
||||
cone_top_radius: remembered.top_radius,
|
||||
cone_defaults: remembered,
|
||||
pyramid_step: PyramidStep::BaseCenter,
|
||||
pyramid_frame: None,
|
||||
pyramid_edge_first: None,
|
||||
pyramid_height_first: None,
|
||||
pyramid_base_radius: pyramid_display_to_circumradius(
|
||||
pyramid_remembered.displayed_base_radius,
|
||||
pyramid_remembered.kind,
|
||||
pyramid_remembered.sides,
|
||||
),
|
||||
pyramid_top_radius: pyramid_display_to_circumradius(
|
||||
pyramid_remembered.displayed_top_radius,
|
||||
pyramid_remembered.kind,
|
||||
pyramid_remembered.sides,
|
||||
),
|
||||
pyramid_sides: pyramid_remembered.sides,
|
||||
pyramid_type: pyramid_remembered.kind,
|
||||
pyramid_defaults: pyramid_remembered,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
@@ -954,6 +1049,502 @@ impl PrimitiveCommand {
|
||||
}
|
||||
}
|
||||
|
||||
fn pyramid_frame_at(&self, center: DVec3) -> WorkingPlane {
|
||||
WorkingPlane {
|
||||
origin: center,
|
||||
x: self.plane.x,
|
||||
y: self.plane.y,
|
||||
z: self.plane.z,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_pyramid_type(&mut self, kind: PyramidType) {
|
||||
let displayed_top = pyramid_circumradius_to_display(
|
||||
self.pyramid_top_radius,
|
||||
self.pyramid_type,
|
||||
self.pyramid_sides,
|
||||
);
|
||||
self.pyramid_type = kind;
|
||||
self.pyramid_top_radius =
|
||||
pyramid_display_to_circumradius(displayed_top, kind, self.pyramid_sides);
|
||||
}
|
||||
|
||||
fn pyramid_set_base_from_point(&mut self, point: DVec3) -> bool {
|
||||
let Some(frame) = self.pyramid_frame else {
|
||||
return false;
|
||||
};
|
||||
let delta = point - frame.origin;
|
||||
let planar = delta - frame.z * delta.dot(frame.z);
|
||||
let displayed_radius = planar.length();
|
||||
let Some(direction) = planar.try_normalize() else {
|
||||
return false;
|
||||
};
|
||||
if !displayed_radius.is_finite() || displayed_radius < 1e-6 {
|
||||
return false;
|
||||
}
|
||||
let x = match self.pyramid_type {
|
||||
PyramidType::Inscribed => direction,
|
||||
PyramidType::Circumscribed => {
|
||||
let half = std::f64::consts::PI / self.pyramid_sides as f64;
|
||||
direction * half.cos() - frame.z.cross(direction) * half.sin()
|
||||
}
|
||||
};
|
||||
let Some(x) = x.try_normalize() else {
|
||||
return false;
|
||||
};
|
||||
let Some(y) = frame.z.cross(x).try_normalize() else {
|
||||
return false;
|
||||
};
|
||||
self.pyramid_frame = Some(WorkingPlane::new(frame.origin, x, y));
|
||||
self.pyramid_base_radius = pyramid_display_to_circumradius(
|
||||
displayed_radius,
|
||||
self.pyramid_type,
|
||||
self.pyramid_sides,
|
||||
);
|
||||
self.pyramid_step = PyramidStep::Height;
|
||||
true
|
||||
}
|
||||
|
||||
fn pyramid_set_base_from_edge(&mut self, first: DVec3, second: DVec3) -> bool {
|
||||
let delta = second - first;
|
||||
let planar = delta - self.plane.z * delta.dot(self.plane.z);
|
||||
let length = planar.length();
|
||||
let Some(edge) = planar.try_normalize() else {
|
||||
return false;
|
||||
};
|
||||
if !length.is_finite() || length < 1e-6 {
|
||||
return false;
|
||||
}
|
||||
let radius = length
|
||||
/ (2.0 * (std::f64::consts::PI / self.pyramid_sides as f64).sin());
|
||||
let apothem = radius * pyramid_apothem_factor(self.pyramid_sides);
|
||||
let center = (first + second) * 0.5 + self.plane.z.cross(edge) * apothem;
|
||||
let Some(x) = (first - center).try_normalize() else {
|
||||
return false;
|
||||
};
|
||||
let Some(y) = self.plane.z.cross(x).try_normalize() else {
|
||||
return false;
|
||||
};
|
||||
self.pyramid_frame = Some(WorkingPlane::new(center, x, y));
|
||||
self.pyramid_base_radius = radius;
|
||||
self.pyramid_step = PyramidStep::Height;
|
||||
true
|
||||
}
|
||||
|
||||
fn pyramid_axis_frame(&self, axis: DVec3) -> Option<(WorkingPlane, f64)> {
|
||||
let current = self.pyramid_frame?;
|
||||
let height = axis.length();
|
||||
if !height.is_finite() || height < 1e-6 {
|
||||
return None;
|
||||
}
|
||||
let z = axis / height;
|
||||
let mut x = current.x - z * current.x.dot(z);
|
||||
if x.length_squared() < 1e-12 {
|
||||
x = current.y - z * current.y.dot(z);
|
||||
}
|
||||
let x = x.try_normalize()?;
|
||||
let y = z.cross(x).try_normalize()?;
|
||||
Some((WorkingPlane::new(current.origin, x, y), height))
|
||||
}
|
||||
|
||||
fn pyramid_history_transform(frame: WorkingPlane) -> [f64; 16] {
|
||||
glam::DMat4::from_cols(
|
||||
frame.x.extend(0.0),
|
||||
frame.y.extend(0.0),
|
||||
frame.z.extend(0.0),
|
||||
frame.origin.extend(1.0),
|
||||
)
|
||||
.to_cols_array()
|
||||
}
|
||||
|
||||
fn build_pyramid(&self, axis: DVec3) -> Option<(Body, SolidHistoryOperation, f64)> {
|
||||
let (frame, height) = self.pyramid_axis_frame(axis)?;
|
||||
let local = solid_model::pyramid_frustum_solid(
|
||||
[0.0; 3],
|
||||
self.pyramid_base_radius,
|
||||
self.pyramid_top_radius,
|
||||
height,
|
||||
self.pyramid_sides,
|
||||
)?;
|
||||
let placed = solid_model::placed(
|
||||
&local,
|
||||
frame.x.to_array(),
|
||||
frame.y.to_array(),
|
||||
frame.z.to_array(),
|
||||
frame.origin.to_array(),
|
||||
)?;
|
||||
Some((
|
||||
placed,
|
||||
crate::scene::model::solid_history::pyramid_op(
|
||||
Self::pyramid_history_transform(frame),
|
||||
self.pyramid_base_radius,
|
||||
self.pyramid_top_radius,
|
||||
height,
|
||||
self.pyramid_sides,
|
||||
self.pyramid_type == PyramidType::Inscribed,
|
||||
),
|
||||
height,
|
||||
))
|
||||
}
|
||||
|
||||
fn commit_pyramid_axis(&self, axis: DVec3) -> CmdResult {
|
||||
let Some((solid, history, height)) = self.build_pyramid(axis) else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
if let Ok(mut defaults) = pyramid_defaults().lock() {
|
||||
*defaults = PyramidDefaults {
|
||||
displayed_base_radius: pyramid_circumradius_to_display(
|
||||
self.pyramid_base_radius,
|
||||
self.pyramid_type,
|
||||
self.pyramid_sides,
|
||||
),
|
||||
displayed_top_radius: pyramid_circumradius_to_display(
|
||||
self.pyramid_top_radius,
|
||||
self.pyramid_type,
|
||||
self.pyramid_sides,
|
||||
),
|
||||
height,
|
||||
sides: self.pyramid_sides,
|
||||
kind: self.pyramid_type,
|
||||
};
|
||||
}
|
||||
let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&solid) else {
|
||||
return CmdResult::Cancel;
|
||||
};
|
||||
let mut entity = Solid3D::new();
|
||||
entity.set_sat_document(&document);
|
||||
entity.wires = solid_model::edge_wires(&solid);
|
||||
CmdResult::CommitSolid {
|
||||
entity: EntityType::Solid3D(entity),
|
||||
solid: Box::new(solid),
|
||||
history,
|
||||
}
|
||||
}
|
||||
|
||||
fn commit_pyramid_height(&self, height: f64) -> CmdResult {
|
||||
let Some(frame) = self.pyramid_frame else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
if !height.is_finite() || height.abs() < 1e-6 {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.commit_pyramid_axis(frame.z * height)
|
||||
}
|
||||
|
||||
fn pyramid_height_at(&self, point: DVec3) -> Option<f64> {
|
||||
let frame = self.pyramid_frame?;
|
||||
Some((point - frame.origin).dot(frame.z))
|
||||
}
|
||||
|
||||
fn pyramid_polygon(frame: WorkingPlane, radius: f64, sides: usize, z: f64) -> Vec<DVec3> {
|
||||
(0..sides)
|
||||
.map(|index| {
|
||||
let angle = std::f64::consts::TAU * index as f64 / sides as f64;
|
||||
frame.origin
|
||||
+ frame.x * (radius * angle.cos())
|
||||
+ frame.y * (radius * angle.sin())
|
||||
+ frame.z * z
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn pyramid_preview(&self, axis: DVec3) -> Option<WireModel> {
|
||||
let (frame, height) = self.pyramid_axis_frame(axis)?;
|
||||
let base = Self::pyramid_polygon(frame, self.pyramid_base_radius, self.pyramid_sides, 0.0);
|
||||
let top = Self::pyramid_polygon(frame, self.pyramid_top_radius, self.pyramid_sides, height);
|
||||
let apex = frame.origin + frame.z * height;
|
||||
let mut points = Vec::new();
|
||||
push_path_loop(&mut points, &base);
|
||||
if self.pyramid_top_radius > 1e-9 {
|
||||
push_path_loop(&mut points, &top);
|
||||
}
|
||||
for index in 0..self.pyramid_sides {
|
||||
push_segment(
|
||||
&mut points,
|
||||
base[index],
|
||||
if self.pyramid_top_radius > 1e-9 { top[index] } else { apex },
|
||||
);
|
||||
}
|
||||
Some(wire("primitive_height_preview", points))
|
||||
}
|
||||
|
||||
fn pyramid_base_preview(&self, frame: WorkingPlane, displayed_radius: f64) -> Option<WireModel> {
|
||||
if !displayed_radius.is_finite() || displayed_radius < 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let radius = pyramid_display_to_circumradius(
|
||||
displayed_radius,
|
||||
self.pyramid_type,
|
||||
self.pyramid_sides,
|
||||
);
|
||||
let points = Self::pyramid_polygon(frame, radius, self.pyramid_sides, 0.0);
|
||||
let mut wire_points = Vec::new();
|
||||
push_path_loop(&mut wire_points, &points);
|
||||
Some(wire("primitive_preview", wire_points))
|
||||
}
|
||||
|
||||
fn pyramid_prompt(&self) -> String {
|
||||
let base_default = self.pyramid_defaults.displayed_base_radius;
|
||||
let top_default = self.pyramid_defaults.displayed_top_radius;
|
||||
match self.pyramid_step {
|
||||
PyramidStep::BaseCenter => crate::tf!(
|
||||
"PYRAMID Specify center point of base or [Edge/Sides]:"
|
||||
).into_owned(),
|
||||
PyramidStep::Sides => crate::tf!(
|
||||
"PYRAMID Enter number of sides <{}>:",
|
||||
self.pyramid_sides
|
||||
).into_owned(),
|
||||
PyramidStep::EdgeFirst => t!("PYRAMID Specify first endpoint of edge:").into_owned(),
|
||||
PyramidStep::EdgeSecond => t!("PYRAMID Specify second endpoint of edge:").into_owned(),
|
||||
PyramidStep::BaseRadius => match self.pyramid_type {
|
||||
PyramidType::Circumscribed => crate::tf!(
|
||||
"PYRAMID Specify base radius or [Inscribed] <{:.4}>:",
|
||||
base_default
|
||||
).into_owned(),
|
||||
PyramidType::Inscribed => crate::tf!(
|
||||
"PYRAMID Specify base radius or [Circumscribed] <{:.4}>:",
|
||||
base_default
|
||||
).into_owned(),
|
||||
},
|
||||
PyramidStep::Height => crate::tf!(
|
||||
"PYRAMID Specify height or [2Point/Axis endpoint/Top radius] <{:.4}>:",
|
||||
self.pyramid_defaults.height
|
||||
).into_owned(),
|
||||
PyramidStep::HeightAfterTopRadius => crate::tf!(
|
||||
"PYRAMID Specify height or [2Point/Axis endpoint] <{:.4}>:",
|
||||
self.pyramid_defaults.height
|
||||
).into_owned(),
|
||||
PyramidStep::HeightFirstPoint => t!("PYRAMID Specify first point for height:").into_owned(),
|
||||
PyramidStep::HeightSecondPoint => t!("PYRAMID Specify second point for height:").into_owned(),
|
||||
PyramidStep::AxisEndpoint => t!("PYRAMID Specify axis endpoint:").into_owned(),
|
||||
PyramidStep::TopRadius => crate::tf!(
|
||||
"PYRAMID Specify top radius <{:.4}>:",
|
||||
top_default
|
||||
).into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn pyramid_options(&self) -> Vec<CmdOption> {
|
||||
match self.pyramid_step {
|
||||
PyramidStep::BaseCenter => vec![
|
||||
CmdOption::new(t!("Edge").as_ref(), "E"),
|
||||
CmdOption::new(t!("Sides").as_ref(), "S"),
|
||||
],
|
||||
PyramidStep::BaseRadius => vec![match self.pyramid_type {
|
||||
PyramidType::Circumscribed => CmdOption::new(t!("Inscribed").as_ref(), "I"),
|
||||
PyramidType::Inscribed => CmdOption::new(t!("Circumscribed").as_ref(), "C"),
|
||||
}],
|
||||
PyramidStep::Height => vec![
|
||||
CmdOption::new("2Point", "2P"),
|
||||
CmdOption::new(t!("Axis endpoint").as_ref(), "A"),
|
||||
CmdOption::new(t!("Top radius").as_ref(), "T"),
|
||||
],
|
||||
PyramidStep::HeightAfterTopRadius => vec![
|
||||
CmdOption::new("2Point", "2P"),
|
||||
CmdOption::new(t!("Axis endpoint").as_ref(), "A"),
|
||||
],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_pyramid_point(&mut self, point: DVec3) -> CmdResult {
|
||||
match self.pyramid_step {
|
||||
PyramidStep::BaseCenter => {
|
||||
self.pyramid_frame = Some(self.pyramid_frame_at(point));
|
||||
self.pyramid_step = PyramidStep::BaseRadius;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
PyramidStep::EdgeFirst => {
|
||||
self.pyramid_edge_first = Some(point);
|
||||
self.pyramid_step = PyramidStep::EdgeSecond;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
PyramidStep::EdgeSecond => {
|
||||
if let Some(first) = self.pyramid_edge_first {
|
||||
self.pyramid_set_base_from_edge(first, point);
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
PyramidStep::BaseRadius => {
|
||||
self.pyramid_set_base_from_point(point);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
PyramidStep::Height | PyramidStep::HeightAfterTopRadius => self
|
||||
.pyramid_height_at(point)
|
||||
.map(|height| self.commit_pyramid_height(height))
|
||||
.unwrap_or(CmdResult::NeedPoint),
|
||||
PyramidStep::HeightFirstPoint => {
|
||||
self.pyramid_height_first = Some(point);
|
||||
self.pyramid_step = PyramidStep::HeightSecondPoint;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
PyramidStep::HeightSecondPoint => self
|
||||
.pyramid_height_first
|
||||
.map(|first| self.commit_pyramid_height(first.distance(point)))
|
||||
.unwrap_or(CmdResult::NeedPoint),
|
||||
PyramidStep::AxisEndpoint => {
|
||||
let Some(frame) = self.pyramid_frame else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
self.commit_pyramid_axis(point - frame.origin)
|
||||
}
|
||||
PyramidStep::TopRadius => {
|
||||
let Some(frame) = self.pyramid_frame else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
let delta = point - frame.origin;
|
||||
let displayed = (delta - frame.z * delta.dot(frame.z)).length();
|
||||
if displayed.is_finite() {
|
||||
self.pyramid_top_radius = pyramid_display_to_circumradius(
|
||||
displayed,
|
||||
self.pyramid_type,
|
||||
self.pyramid_sides,
|
||||
);
|
||||
self.pyramid_step = PyramidStep::HeightAfterTopRadius;
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
PyramidStep::Sides => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_pyramid_text(&mut self, raw: &str) -> Option<CmdResult> {
|
||||
let token = raw.trim();
|
||||
let upper = token.to_uppercase();
|
||||
match self.pyramid_step {
|
||||
PyramidStep::BaseCenter => {
|
||||
self.pyramid_step = match upper.as_str() {
|
||||
"E" | "EDGE" => PyramidStep::EdgeFirst,
|
||||
"S" | "SIDES" => PyramidStep::Sides,
|
||||
_ => return None,
|
||||
};
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
PyramidStep::BaseRadius if matches!(upper.as_str(), "I" | "INSCRIBED") => {
|
||||
self.set_pyramid_type(PyramidType::Inscribed);
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
PyramidStep::BaseRadius if matches!(upper.as_str(), "C" | "CIRCUMSCRIBED") => {
|
||||
self.set_pyramid_type(PyramidType::Circumscribed);
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
PyramidStep::Height => {
|
||||
self.pyramid_step = match upper.as_str() {
|
||||
"2P" | "2POINT" => PyramidStep::HeightFirstPoint,
|
||||
"A" | "AXIS" | "AXIS ENDPOINT" => PyramidStep::AxisEndpoint,
|
||||
"T" | "TOP" | "TOP RADIUS" => PyramidStep::TopRadius,
|
||||
_ => {
|
||||
let height = crate::entities::common::parse_typed_length(token)?;
|
||||
return Some(self.commit_pyramid_height(height));
|
||||
}
|
||||
};
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
PyramidStep::HeightAfterTopRadius => {
|
||||
self.pyramid_step = match upper.as_str() {
|
||||
"2P" | "2POINT" => PyramidStep::HeightFirstPoint,
|
||||
"A" | "AXIS" | "AXIS ENDPOINT" => PyramidStep::AxisEndpoint,
|
||||
_ => {
|
||||
let height = crate::entities::common::parse_typed_length(token)?;
|
||||
return Some(self.commit_pyramid_height(height));
|
||||
}
|
||||
};
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
PyramidStep::Sides => {
|
||||
let sides = token.parse::<usize>().ok()?;
|
||||
if !(3..=32).contains(&sides) {
|
||||
return None;
|
||||
}
|
||||
let displayed_top = pyramid_circumradius_to_display(
|
||||
self.pyramid_top_radius,
|
||||
self.pyramid_type,
|
||||
self.pyramid_sides,
|
||||
);
|
||||
self.pyramid_sides = sides;
|
||||
self.pyramid_top_radius = pyramid_display_to_circumradius(
|
||||
displayed_top,
|
||||
self.pyramid_type,
|
||||
self.pyramid_sides,
|
||||
);
|
||||
self.pyramid_defaults.sides = sides;
|
||||
self.pyramid_step = PyramidStep::BaseCenter;
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let number = crate::entities::common::parse_typed_length(token)?;
|
||||
match self.pyramid_step {
|
||||
PyramidStep::BaseRadius if number > 0.0 => {
|
||||
let frame = self.pyramid_frame?;
|
||||
self.pyramid_set_base_from_point(frame.origin + frame.x * number)
|
||||
.then_some(CmdResult::NeedPoint)
|
||||
}
|
||||
PyramidStep::TopRadius if number >= 0.0 => {
|
||||
self.pyramid_top_radius = pyramid_display_to_circumradius(
|
||||
number,
|
||||
self.pyramid_type,
|
||||
self.pyramid_sides,
|
||||
);
|
||||
self.pyramid_step = PyramidStep::HeightAfterTopRadius;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn pyramid_mouse_move(&self, point: DVec3) -> Option<WireModel> {
|
||||
match self.pyramid_step {
|
||||
PyramidStep::EdgeSecond => {
|
||||
let first = self.pyramid_edge_first?;
|
||||
let mut preview = PrimitiveCommand::new("PYRAMID");
|
||||
preview.plane = self.plane;
|
||||
preview.pyramid_sides = self.pyramid_sides;
|
||||
preview.pyramid_type = self.pyramid_type;
|
||||
preview.pyramid_set_base_from_edge(first, point);
|
||||
let frame = preview.pyramid_frame?;
|
||||
preview.pyramid_base_preview(
|
||||
frame,
|
||||
pyramid_circumradius_to_display(
|
||||
preview.pyramid_base_radius,
|
||||
preview.pyramid_type,
|
||||
preview.pyramid_sides,
|
||||
),
|
||||
)
|
||||
}
|
||||
PyramidStep::BaseRadius => {
|
||||
let frame = self.pyramid_frame?;
|
||||
let delta = point - frame.origin;
|
||||
let planar = delta - frame.z * delta.dot(frame.z);
|
||||
let displayed = planar.length();
|
||||
let direction = planar.try_normalize()?;
|
||||
let x = match self.pyramid_type {
|
||||
PyramidType::Inscribed => direction,
|
||||
PyramidType::Circumscribed => {
|
||||
let half = std::f64::consts::PI / self.pyramid_sides as f64;
|
||||
direction * half.cos() - frame.z.cross(direction) * half.sin()
|
||||
}
|
||||
}
|
||||
.try_normalize()?;
|
||||
let y = frame.z.cross(x).try_normalize()?;
|
||||
self.pyramid_base_preview(WorkingPlane::new(frame.origin, x, y), displayed)
|
||||
}
|
||||
PyramidStep::Height | PyramidStep::HeightAfterTopRadius => {
|
||||
let frame = self.pyramid_frame?;
|
||||
self.pyramid_preview(frame.z * self.pyramid_height_at(point)?)
|
||||
}
|
||||
PyramidStep::HeightSecondPoint => {
|
||||
let frame = self.pyramid_frame?;
|
||||
self.pyramid_preview(frame.z * self.pyramid_height_first?.distance(point))
|
||||
}
|
||||
PyramidStep::AxisEndpoint => {
|
||||
let frame = self.pyramid_frame?;
|
||||
self.pyramid_preview(point - frame.origin)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of footprint points the shape needs before the height step.
|
||||
fn footprint_pts(&self) -> usize {
|
||||
match self.shape {
|
||||
@@ -1273,17 +1864,7 @@ impl PrimitiveCommand {
|
||||
solid_history::sphere_op(self.history_transform(c), r),
|
||||
)
|
||||
}
|
||||
Shape::Pyramid => {
|
||||
let c = self.pts[0];
|
||||
let r = (self.pts[1] - c).length();
|
||||
if r < 1e-6 || height < 1e-6 {
|
||||
return None;
|
||||
}
|
||||
(
|
||||
solid_model::pyramid_solid([c.x, c.y, c.z], r, height, 4),
|
||||
solid_history::pyramid_op(self.history_transform(c), r, height, 4),
|
||||
)
|
||||
}
|
||||
Shape::Pyramid => return None,
|
||||
Shape::Torus => {
|
||||
let c = self.pts[0];
|
||||
let first = (self.pts[1] - c).length();
|
||||
@@ -1365,6 +1946,17 @@ impl CadCommand for PrimitiveCommand {
|
||||
(frame.origin, frame.z.normalize_or_zero())
|
||||
});
|
||||
}
|
||||
|
||||
if self.shape == Shape::Pyramid {
|
||||
return matches!(
|
||||
self.pyramid_step,
|
||||
PyramidStep::Height | PyramidStep::HeightAfterTopRadius
|
||||
)
|
||||
.then(|| {
|
||||
let frame = self.pyramid_frame.unwrap_or(self.plane);
|
||||
(frame.origin, frame.z.normalize_or_zero())
|
||||
});
|
||||
}
|
||||
let constrained = if self.shape.rectangular() {
|
||||
self.box_step == BoxStep::Height
|
||||
} else {
|
||||
@@ -1430,6 +2022,9 @@ impl CadCommand for PrimitiveCommand {
|
||||
if self.shape == Shape::Cone {
|
||||
return self.cone_prompt();
|
||||
}
|
||||
if self.shape == Shape::Pyramid {
|
||||
return self.pyramid_prompt();
|
||||
}
|
||||
if self.shape.rectangular() {
|
||||
return match self.box_step {
|
||||
BoxStep::FirstCorner => t!("%{n} Specify first corner or [Center]:", n = n),
|
||||
@@ -1531,6 +2126,9 @@ impl CadCommand for PrimitiveCommand {
|
||||
if self.shape == Shape::Cone {
|
||||
return self.cone_options();
|
||||
}
|
||||
if self.shape == Shape::Pyramid {
|
||||
return self.pyramid_options();
|
||||
}
|
||||
if !self.shape.rectangular() {
|
||||
return Vec::new();
|
||||
}
|
||||
@@ -1592,6 +2190,9 @@ impl CadCommand for PrimitiveCommand {
|
||||
if self.shape == Shape::Cone {
|
||||
return self.on_cone_point(pt);
|
||||
}
|
||||
if self.shape == Shape::Pyramid {
|
||||
return self.on_pyramid_point(pt);
|
||||
}
|
||||
if self.shape.rectangular() {
|
||||
let local = self.plane.to_local(pt);
|
||||
if !local.is_finite() {
|
||||
@@ -1735,6 +2336,35 @@ impl CadCommand for PrimitiveCommand {
|
||||
_ => CmdResult::Cancel,
|
||||
};
|
||||
}
|
||||
if self.shape == Shape::Pyramid {
|
||||
return match self.pyramid_step {
|
||||
PyramidStep::Sides => {
|
||||
self.pyramid_step = PyramidStep::BaseCenter;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
PyramidStep::BaseRadius => {
|
||||
let displayed = self.pyramid_defaults.displayed_base_radius;
|
||||
let Some(frame) = self.pyramid_frame else {
|
||||
return CmdResult::Cancel;
|
||||
};
|
||||
self.pyramid_set_base_from_point(frame.origin + frame.x * displayed);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
PyramidStep::Height | PyramidStep::HeightAfterTopRadius => {
|
||||
self.commit_pyramid_height(self.pyramid_defaults.height)
|
||||
}
|
||||
PyramidStep::TopRadius => {
|
||||
self.pyramid_top_radius = pyramid_display_to_circumradius(
|
||||
self.pyramid_defaults.displayed_top_radius,
|
||||
self.pyramid_type,
|
||||
self.pyramid_sides,
|
||||
);
|
||||
self.pyramid_step = PyramidStep::HeightAfterTopRadius;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => CmdResult::Cancel,
|
||||
};
|
||||
}
|
||||
if self.shape.rectangular() {
|
||||
return if self.shape == Shape::Wedge && self.box_step == BoxStep::Height {
|
||||
self.commit(self.default_height())
|
||||
@@ -1777,6 +2407,17 @@ impl CadCommand for PrimitiveCommand {
|
||||
| ConeStep::TopRadius
|
||||
);
|
||||
}
|
||||
if self.shape == Shape::Pyramid {
|
||||
return matches!(
|
||||
self.pyramid_step,
|
||||
PyramidStep::BaseCenter
|
||||
| PyramidStep::Sides
|
||||
| PyramidStep::BaseRadius
|
||||
| PyramidStep::Height
|
||||
| PyramidStep::HeightAfterTopRadius
|
||||
| PyramidStep::TopRadius
|
||||
);
|
||||
}
|
||||
if self.shape.rectangular() {
|
||||
matches!(
|
||||
self.box_step,
|
||||
@@ -1806,6 +2447,15 @@ impl CadCommand for PrimitiveCommand {
|
||||
| ConeStep::HeightAfterTopRadius
|
||||
);
|
||||
}
|
||||
if self.shape == Shape::Pyramid {
|
||||
return matches!(
|
||||
self.pyramid_step,
|
||||
PyramidStep::BaseCenter
|
||||
| PyramidStep::BaseRadius
|
||||
| PyramidStep::Height
|
||||
| PyramidStep::HeightAfterTopRadius
|
||||
);
|
||||
}
|
||||
self.shape.rectangular()
|
||||
&& matches!(
|
||||
self.box_step,
|
||||
@@ -1855,6 +2505,9 @@ impl CadCommand for PrimitiveCommand {
|
||||
if self.shape == Shape::Cone {
|
||||
return self.on_cone_text(raw);
|
||||
}
|
||||
if self.shape == Shape::Pyramid {
|
||||
return self.on_pyramid_text(raw);
|
||||
}
|
||||
if self.shape.rectangular() {
|
||||
let token = raw.trim();
|
||||
let upper = token.to_uppercase();
|
||||
@@ -1943,6 +2596,9 @@ impl CadCommand for PrimitiveCommand {
|
||||
if self.shape == Shape::Cone {
|
||||
return self.cone_mouse_move(pt);
|
||||
}
|
||||
if self.shape == Shape::Pyramid {
|
||||
return self.pyramid_mouse_move(pt);
|
||||
}
|
||||
if self.pts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
@@ -2096,6 +2752,32 @@ impl CadCommand for PrimitiveCommand {
|
||||
});
|
||||
}
|
||||
|
||||
if self.shape == Shape::Pyramid {
|
||||
let frame = self.pyramid_frame.unwrap_or(self.plane);
|
||||
let (anchor, role) = match self.pyramid_step {
|
||||
PyramidStep::BaseRadius | PyramidStep::TopRadius => {
|
||||
(frame.origin, DynRole::Radius)
|
||||
}
|
||||
PyramidStep::Height
|
||||
| PyramidStep::HeightAfterTopRadius
|
||||
| PyramidStep::AxisEndpoint => (frame.origin, DynRole::Height),
|
||||
PyramidStep::HeightSecondPoint => {
|
||||
(self.pyramid_height_first?, DynRole::Height)
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
return Some(DynSpec {
|
||||
anchor: DynAnchor::Point(anchor),
|
||||
fields: vec![DynFieldSpec::new(role)],
|
||||
guide: if role == DynRole::Radius {
|
||||
DynGuide::Radius
|
||||
} else {
|
||||
DynGuide::None
|
||||
},
|
||||
ref_point: None,
|
||||
});
|
||||
}
|
||||
|
||||
let role = if self.shape.rectangular() {
|
||||
match self.box_step {
|
||||
BoxStep::CubeSize | BoxStep::Length => DynRole::Distance,
|
||||
@@ -2153,6 +2835,25 @@ impl CadCommand for PrimitiveCommand {
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
if self.shape == Shape::Pyramid {
|
||||
return match self.pyramid_step {
|
||||
PyramidStep::BaseRadius | PyramidStep::TopRadius => {
|
||||
let frame = self.pyramid_frame?;
|
||||
let delta = cursor - frame.origin;
|
||||
Some((delta - frame.z * delta.dot(frame.z)).length())
|
||||
}
|
||||
PyramidStep::Height | PyramidStep::HeightAfterTopRadius => {
|
||||
self.pyramid_height_at(cursor).map(f64::abs)
|
||||
}
|
||||
PyramidStep::HeightSecondPoint => {
|
||||
Some(self.pyramid_height_first?.distance(cursor))
|
||||
}
|
||||
PyramidStep::AxisEndpoint => {
|
||||
self.pyramid_frame.map(|frame| frame.origin.distance(cursor))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
if self.shape.rectangular() {
|
||||
let local = self.plane.to_local(cursor);
|
||||
if !local.is_finite() {
|
||||
@@ -2295,6 +2996,14 @@ fn push_loop<const N: usize>(points: &mut Vec<[f32; 3]>, path: &[DVec3; N]) {
|
||||
points.extend(path.iter().chain(path.first()).map(|point| point.as_vec3().to_array()));
|
||||
}
|
||||
|
||||
fn push_path_loop(points: &mut Vec<[f32; 3]>, path: &[DVec3]) {
|
||||
if path.is_empty() {
|
||||
return;
|
||||
}
|
||||
push_break(points);
|
||||
points.extend(path.iter().chain(path.first()).map(|point| point.as_vec3().to_array()));
|
||||
}
|
||||
|
||||
fn push_segment(points: &mut Vec<[f32; 3]>, a: DVec3, b: DVec3) {
|
||||
push_break(points);
|
||||
points.extend([a.as_vec3().to_array(), b.as_vec3().to_array()]);
|
||||
|
||||
@@ -21,6 +21,7 @@ pub const GRIP_INNER_RADIUS: usize = 10_006;
|
||||
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_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;
|
||||
@@ -50,6 +51,7 @@ pub const PROP_POSITION_X: &str = "solid_history_position_x";
|
||||
pub const PROP_POSITION_Y: &str = "solid_history_position_y";
|
||||
pub const PROP_POSITION_Z: &str = "solid_history_position_z";
|
||||
pub const PROP_ROTATION: &str = "solid_history_rotation";
|
||||
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";
|
||||
|
||||
@@ -99,6 +101,7 @@ pub fn has_specialized_primitive_properties(
|
||||
| SolidHistoryOperation::Sphere(_)
|
||||
| SolidHistoryOperation::Cone(_)
|
||||
| SolidHistoryOperation::Cylinder(_)
|
||||
| SolidHistoryOperation::Pyramid(_)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -114,6 +117,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::Pyramid(value) => world_point(value.base.transform, [0.0; 3]),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -364,6 +368,132 @@ fn cylinder_properties(
|
||||
]
|
||||
}
|
||||
|
||||
fn pyramid_is_inscribed(value: &SolidHistoryPyramid) -> bool {
|
||||
value.operation_minor == -1
|
||||
}
|
||||
|
||||
fn pyramid_apothem_factor(sides: i32) -> f64 {
|
||||
(std::f64::consts::PI / sides.clamp(3, 32) as f64).cos()
|
||||
}
|
||||
|
||||
fn pyramid_display_radius(value: &SolidHistoryPyramid, radius: f64) -> f64 {
|
||||
if pyramid_is_inscribed(value) {
|
||||
radius
|
||||
} else {
|
||||
radius * pyramid_apothem_factor(value.sides)
|
||||
}
|
||||
}
|
||||
|
||||
fn pyramid_display_rotation(value: &SolidHistoryPyramid) -> Option<f64> {
|
||||
let vertex_rotation = matrix(value.base.transform)
|
||||
.map(|matrix| matrix.x_axis.y.atan2(matrix.x_axis.x))?;
|
||||
if pyramid_is_inscribed(value) {
|
||||
Some(vertex_rotation)
|
||||
} else {
|
||||
Some(vertex_rotation + std::f64::consts::PI / value.sides.clamp(3, 32) as f64)
|
||||
}
|
||||
}
|
||||
|
||||
fn pyramid_properties(
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
value: &SolidHistoryPyramid,
|
||||
) -> Vec<PropSection> {
|
||||
let Some(position) = world_point(value.base.transform, [0.0; 3]) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let rotation = pyramid_display_rotation(value).unwrap_or(0.0);
|
||||
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 show_value = if show_history { "Yes" } else { "No" };
|
||||
vec![
|
||||
PropSection {
|
||||
title: t!("Geometry").into_owned(),
|
||||
props: vec![
|
||||
Property {
|
||||
label: t!("Solid type").into_owned(),
|
||||
field: "solid_history_type",
|
||||
value: PropValue::ReadOnly(t!("Pyramid").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!("Type").into_owned(),
|
||||
field: PROP_PYRAMID_TYPE,
|
||||
value: PropValue::Choice {
|
||||
selected: if pyramid_is_inscribed(value) {
|
||||
"Inscribed"
|
||||
} else {
|
||||
"Circumscribed"
|
||||
}
|
||||
.to_string(),
|
||||
options: vec!["Circumscribed".to_string(), "Inscribed".to_string()],
|
||||
},
|
||||
},
|
||||
history_prop(
|
||||
t!("Base radius").as_ref(),
|
||||
PROP_BASE_RADIUS,
|
||||
pyramid_display_radius(value, value.radius),
|
||||
),
|
||||
history_prop(
|
||||
t!("Top radius").as_ref(),
|
||||
PROP_TOP_RADIUS,
|
||||
pyramid_display_radius(value, value.top_radius),
|
||||
),
|
||||
history_prop(t!("Sides").as_ref(), PROP_SIDES, value.sides),
|
||||
Property {
|
||||
label: t!("Rotation").into_owned(),
|
||||
field: PROP_ROTATION,
|
||||
value: PropValue::EditText(
|
||||
crate::entities::common::format_direction(rotation),
|
||||
),
|
||||
},
|
||||
history_prop(t!("Height").as_ref(), PROP_HEIGHT, value.height),
|
||||
],
|
||||
},
|
||||
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: show_value.to_string(),
|
||||
options: vec!["No".to_string(), "Yes".to_string()],
|
||||
}
|
||||
} else {
|
||||
PropValue::ReadOnly(show_value.to_string())
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn sphere_properties(
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
@@ -558,6 +688,9 @@ pub fn primitive_properties(
|
||||
SolidHistoryOperation::Cylinder(value) => {
|
||||
return cylinder_properties(document, handle, value)
|
||||
}
|
||||
SolidHistoryOperation::Pyramid(value) => {
|
||||
return pyramid_properties(document, handle, value)
|
||||
}
|
||||
SolidHistoryOperation::Torus(value) => vec![
|
||||
history_prop(
|
||||
t!("Outer Radius").as_ref(),
|
||||
@@ -570,11 +703,6 @@ pub fn primitive_properties(
|
||||
(value.major_radius - value.minor_radius).max(0.0),
|
||||
),
|
||||
],
|
||||
SolidHistoryOperation::Pyramid(value) => vec![
|
||||
history_prop(t!("Radius").as_ref(), PROP_RADIUS, value.radius),
|
||||
history_prop(t!("Height").as_ref(), PROP_HEIGHT, value.height),
|
||||
history_prop(t!("Sides").as_ref(), PROP_SIDES, value.sides),
|
||||
],
|
||||
_ => return Vec::new(),
|
||||
};
|
||||
vec![PropSection {
|
||||
@@ -606,6 +734,7 @@ pub fn is_primitive_property(field: &str) -> bool {
|
||||
| PROP_POSITION_Y
|
||||
| PROP_POSITION_Z
|
||||
| PROP_ROTATION
|
||||
| PROP_PYRAMID_TYPE
|
||||
)
|
||||
}
|
||||
|
||||
@@ -827,6 +956,120 @@ fn canonicalize_cylinder_radii(value: &mut SolidHistoryCylinder) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn apply_pyramid_geometry_property(
|
||||
value: &mut SolidHistoryPyramid,
|
||||
field: &str,
|
||||
text: &str,
|
||||
) -> Option<bool> {
|
||||
if field == PROP_PYRAMID_TYPE {
|
||||
let next_inscribed = if text.eq_ignore_ascii_case("Inscribed") {
|
||||
true
|
||||
} else if text.eq_ignore_ascii_case("Circumscribed") {
|
||||
false
|
||||
} else {
|
||||
return Some(false);
|
||||
};
|
||||
let old_inscribed = pyramid_is_inscribed(value);
|
||||
if next_inscribed == old_inscribed {
|
||||
return Some(false);
|
||||
}
|
||||
let factor = pyramid_apothem_factor(value.sides);
|
||||
if factor <= 1e-9 {
|
||||
return Some(false);
|
||||
}
|
||||
let base_display = if old_inscribed { value.radius } else { value.radius * factor };
|
||||
let top_display = if old_inscribed {
|
||||
value.top_radius
|
||||
} else {
|
||||
value.top_radius * factor
|
||||
};
|
||||
let Some(current) = matrix(value.base.transform) else {
|
||||
return Some(false);
|
||||
};
|
||||
value.radius = if next_inscribed { base_display } else { base_display / factor };
|
||||
value.top_radius = if next_inscribed { top_display } else { top_display / factor };
|
||||
let half = std::f64::consts::PI / value.sides.clamp(3, 32) as f64;
|
||||
let delta = if next_inscribed { half } else { -half };
|
||||
value.base.transform =
|
||||
(current * glam::DMat4::from_rotation_z(delta)).to_cols_array();
|
||||
value.operation_minor = if next_inscribed { -1 } else { 0 };
|
||||
return Some(true);
|
||||
}
|
||||
if field == PROP_ROTATION {
|
||||
let target = crate::entities::common::parse_direction(text)?;
|
||||
if !target.is_finite() {
|
||||
return Some(false);
|
||||
}
|
||||
let current = matrix(value.base.transform)?;
|
||||
let projected = current.x_axis.truncate();
|
||||
if projected.length_squared() <= 1e-12 {
|
||||
return Some(false);
|
||||
}
|
||||
let current_angle = projected.y.atan2(projected.x);
|
||||
let target_vertex_angle = if pyramid_is_inscribed(value) {
|
||||
target
|
||||
} else {
|
||||
target - std::f64::consts::PI / value.sides.clamp(3, 32) as f64
|
||||
};
|
||||
let delta = (target_vertex_angle - current_angle + std::f64::consts::PI)
|
||||
.rem_euclid(std::f64::consts::TAU)
|
||||
- std::f64::consts::PI;
|
||||
if delta.abs() <= 1e-12 {
|
||||
return Some(false);
|
||||
}
|
||||
let updated = current * glam::DMat4::from_rotation_z(delta);
|
||||
if !updated.is_finite() || updated.determinant().abs() <= 1e-12 {
|
||||
return Some(false);
|
||||
}
|
||||
value.base.transform = updated.to_cols_array();
|
||||
return Some(true);
|
||||
}
|
||||
let axis = match field {
|
||||
PROP_POSITION_X => 0,
|
||||
PROP_POSITION_Y => 1,
|
||||
PROP_POSITION_Z => 2,
|
||||
_ => return None,
|
||||
};
|
||||
let target = crate::entities::common::parse_length(text)?;
|
||||
if !target.is_finite() {
|
||||
return Some(false);
|
||||
}
|
||||
let current = matrix(value.base.transform)?;
|
||||
let origin = current.transform_point3(glam::DVec3::ZERO);
|
||||
if !origin.is_finite() || (target - origin[axis]).abs() <= 1e-12 {
|
||||
return Some(false);
|
||||
}
|
||||
let mut delta = glam::DVec3::ZERO;
|
||||
delta[axis] = target - origin[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)
|
||||
}
|
||||
|
||||
fn set_pyramid_sides(value: &mut SolidHistoryPyramid, sides: i32) -> bool {
|
||||
if !(3..=32).contains(&sides) || sides == value.sides {
|
||||
return false;
|
||||
}
|
||||
if !pyramid_is_inscribed(value) {
|
||||
let Some(current) = matrix(value.base.transform) else {
|
||||
return false;
|
||||
};
|
||||
let old_factor = pyramid_apothem_factor(value.sides);
|
||||
let new_factor = pyramid_apothem_factor(sides);
|
||||
value.radius = value.radius * old_factor / new_factor;
|
||||
value.top_radius = value.top_radius * old_factor / new_factor;
|
||||
let old_half = std::f64::consts::PI / value.sides.clamp(3, 32) as f64;
|
||||
let new_half = std::f64::consts::PI / sides as f64;
|
||||
value.base.transform =
|
||||
(current * glam::DMat4::from_rotation_z(old_half - new_half)).to_cols_array();
|
||||
}
|
||||
value.sides = sides;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn apply_primitive_property(
|
||||
operation: &mut SolidHistoryOperation,
|
||||
field: &str,
|
||||
@@ -856,6 +1099,11 @@ pub fn apply_primitive_property(
|
||||
return applied;
|
||||
}
|
||||
}
|
||||
if let SolidHistoryOperation::Pyramid(pyramid_value) = operation {
|
||||
if let Some(applied) = apply_pyramid_geometry_property(pyramid_value, field, value) {
|
||||
return applied;
|
||||
}
|
||||
}
|
||||
let Some(number) = (if field == PROP_SIDES {
|
||||
value.trim().parse::<f64>().ok()
|
||||
} else {
|
||||
@@ -1008,18 +1256,33 @@ pub fn apply_primitive_property(
|
||||
}
|
||||
}
|
||||
SolidHistoryOperation::Pyramid(value) => match field {
|
||||
PROP_RADIUS => value.radius = positive().unwrap_or(value.radius),
|
||||
PROP_BASE_RADIUS => {
|
||||
let Some(displayed) = positive() else {
|
||||
return false;
|
||||
};
|
||||
value.radius = if pyramid_is_inscribed(value) {
|
||||
displayed
|
||||
} else {
|
||||
displayed / pyramid_apothem_factor(value.sides)
|
||||
};
|
||||
}
|
||||
PROP_TOP_RADIUS => {
|
||||
if number < 0.0 {
|
||||
return false;
|
||||
}
|
||||
value.top_radius = if pyramid_is_inscribed(value) {
|
||||
number
|
||||
} else {
|
||||
number / pyramid_apothem_factor(value.sides)
|
||||
};
|
||||
}
|
||||
PROP_HEIGHT => value.height = positive().unwrap_or(value.height),
|
||||
PROP_SIDES => {
|
||||
let rounded = number.round();
|
||||
if (number - rounded).abs() > 1e-9 {
|
||||
return false;
|
||||
}
|
||||
let sides = rounded as i32;
|
||||
if !(3..=71).contains(&sides) {
|
||||
return false;
|
||||
}
|
||||
value.sides = sides;
|
||||
return set_pyramid_sides(value, rounded as i32);
|
||||
}
|
||||
_ => return false,
|
||||
},
|
||||
@@ -1318,6 +1581,13 @@ pub fn primitive_grips(
|
||||
GripShape::Square,
|
||||
None,
|
||||
);
|
||||
add(
|
||||
GRIP_TOP_RADIUS,
|
||||
value.base.transform,
|
||||
[value.top_radius, 0.0, value.height],
|
||||
GripShape::Square,
|
||||
None,
|
||||
);
|
||||
add(
|
||||
GRIP_HEIGHT,
|
||||
value.base.transform,
|
||||
@@ -1325,7 +1595,7 @@ pub fn primitive_grips(
|
||||
GripShape::Square,
|
||||
Some([0.0, 0.0, 1.0]),
|
||||
);
|
||||
let angle = (value.sides.clamp(3, 71) as f64 * 5.0).to_radians();
|
||||
let angle = (value.sides.clamp(3, 32) as f64 * 5.0).to_radians();
|
||||
add(
|
||||
GRIP_SIDES,
|
||||
value.base.transform,
|
||||
@@ -1480,11 +1750,16 @@ pub fn apply_primitive_grip(
|
||||
},
|
||||
SolidHistoryOperation::Pyramid(value) => match grip_id {
|
||||
GRIP_RADIUS => value.radius = local.x.hypot(local.y).max(1e-6),
|
||||
GRIP_TOP_RADIUS => {
|
||||
value.top_radius = local.x.hypot(local.y).max(0.0);
|
||||
}
|
||||
GRIP_HEIGHT => value.height = local.z.max(1e-6),
|
||||
GRIP_SIDES => {
|
||||
let angle = local.y.atan2(local.x).rem_euclid(std::f64::consts::TAU);
|
||||
value.sides = (angle.to_degrees() / 5.0).round() as i32;
|
||||
value.sides = value.sides.clamp(3, 71);
|
||||
let sides = ((angle.to_degrees() / 5.0).round() as i32).clamp(3, 32);
|
||||
if !set_pyramid_sides(value, sides) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
_ => return false,
|
||||
},
|
||||
@@ -1608,15 +1883,19 @@ pub fn torus_op(
|
||||
pub fn pyramid_op(
|
||||
transform: [f64; 16],
|
||||
radius: f64,
|
||||
top_radius: f64,
|
||||
height: f64,
|
||||
sides: usize,
|
||||
inscribed: bool,
|
||||
) -> SolidHistoryOperation {
|
||||
SolidHistoryOperation::Pyramid(SolidHistoryPyramid {
|
||||
base: base(transform),
|
||||
operation_major: 1,
|
||||
operation_minor: if inscribed { -1 } else { 0 },
|
||||
height,
|
||||
sides: sides as i32,
|
||||
radius,
|
||||
top_radius,
|
||||
..SolidHistoryPyramid::default()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -82,6 +82,17 @@ pub fn pyramid_solid(center: [f64; 3], radius: f64, height: f64, sides: usize) -
|
||||
brep::make::pyramid(center, radius, height, sides)
|
||||
}
|
||||
|
||||
/// Solid pyramid or polygonal frustum on a regular polygon of `sides` corners.
|
||||
pub fn pyramid_frustum_solid(
|
||||
center: [f64; 3],
|
||||
base_radius: f64,
|
||||
top_radius: f64,
|
||||
height: f64,
|
||||
sides: usize,
|
||||
) -> Option<Body> {
|
||||
brep::make::pyramid_frustum(center, base_radius, top_radius, height, sides)
|
||||
}
|
||||
|
||||
// ── Placement ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// Moves a body by a rigid transform, given as three axes and an origin.
|
||||
|
||||
Reference in new issue
Block a user