Merge pull request #954 from ramox81/codex/pyramid-command-properties

Match pyramid creation and properties behavior
This commit is contained in:
Hakan Seven authored and GitHub committed 2026-09-01 22:52:12 +03:00
commit 40b9124bd0
7 files changed
+1055 -27

No files matched your search

Generated
+1 -1
View File
@@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cadkernel"
version = "0.1.0"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=48c634995fa60d3928c37bae49b12b421c56c886#48c634995fa60d3928c37bae49b12b421c56c886"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=67ff1f59f380afcfd939fa98883012a0c7b109f7#67ff1f59f380afcfd939fa98883012a0c7b109f7"
dependencies = [
"acadrust",
"cavalier_contours",
+1 -1
View File
@@ -28,7 +28,7 @@ rfd = "0.17"
clap = { version = "4", features = ["derive"] }
env_logger = "0.11"
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "a0f7d444f1607bc4b2c881060cbe7ea1014253cb", features = ["serde"] }
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "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"] }
+2
View File
@@ -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();
+27
View File
@@ -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],
+720 -11
View File
@@ -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()]);
+293 -14
View File
@@ -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()
})
}
+11
View File
@@ -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.