From 0bfa48909f5e0b984c07c70cd0887384c3d274e4 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Tue, 1 Sep 2026 10:38:05 +0300 Subject: [PATCH 1/3] Match pyramid creation and properties behavior --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/app/model_ops.rs | 2 + src/modules/model/primitive_cmd.rs | 710 ++++++++++++++++++++++++++++- src/scene/model/solid_history.rs | 282 +++++++++++- src/scene/model/solid_model.rs | 11 + 6 files changed, 987 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7bcdc4b0d..8736aab97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 1536791ee..cf6913615 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/src/app/model_ops.rs b/src/app/model_ops.rs index 2fe32100d..a7204dd4c 100644 --- a/src/app/model_ops.rs +++ b/src/app/model_ops.rs @@ -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(); diff --git a/src/modules/model/primitive_cmd.rs b/src/modules/model/primitive_cmd.rs index cd7d95537..c0bfcdc6a 100644 --- a/src/modules/model/primitive_cmd.rs +++ b/src/modules/model/primitive_cmd.rs @@ -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 { DEFAULTS.get_or_init(|| std::sync::Mutex::new(ConeDefaults::default())) } +fn pyramid_defaults() -> &'static Mutex { + static DEFAULTS: OnceLock> = 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 { 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, + pyramid_edge_first: Option, + pyramid_height_first: Option, + 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,481 @@ 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 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 { + let frame = self.pyramid_frame?; + Some((point - frame.origin).dot(frame.z)) + } + + fn pyramid_polygon(frame: WorkingPlane, radius: f64, sides: usize, z: f64) -> Vec { + (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 { + 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 { + 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 { + 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 { + 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.pyramid_type = PyramidType::Inscribed; + return Some(CmdResult::NeedPoint); + } + PyramidStep::BaseRadius if matches!(upper.as_str(), "C" | "CIRCUMSCRIBED") => { + self.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::().ok()?; + if !(3..=32).contains(&sides) { + return None; + } + self.pyramid_sides = 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 { + 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 +1843,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 +1925,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 +2001,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 +2105,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 +2169,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 +2315,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 +2386,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 +2426,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 +2484,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 +2575,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 +2731,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 +2814,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 +2975,14 @@ fn push_loop(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()]); diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index 5043a52e2..b43e31fb7 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -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 } 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,124 @@ 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_properties( + document: &acadrust::CadDocument, + handle: acadrust::Handle, + value: &SolidHistoryPyramid, +) -> Vec { + let Some(position) = world_point(value.base.transform, [0.0; 3]) else { + return Vec::new(); + }; + let rotation = matrix(value.base.transform) + .map(|matrix| matrix.x_axis.y.atan2(matrix.x_axis.x)) + .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 +680,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 +695,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 +726,7 @@ pub fn is_primitive_property(field: &str) -> bool { | PROP_POSITION_Y | PROP_POSITION_Z | PROP_ROTATION + | PROP_PYRAMID_TYPE ) } @@ -827,6 +948,94 @@ fn canonicalize_cylinder_radii(value: &mut SolidHistoryCylinder) -> bool { true } +fn apply_pyramid_geometry_property( + value: &mut SolidHistoryPyramid, + field: &str, + text: &str, +) -> Option { + 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 + }; + value.radius = if next_inscribed { base_display } else { base_display / factor }; + value.top_radius = if next_inscribed { top_display } else { top_display / factor }; + let Some(current) = matrix(value.base.transform) else { + return Some(false); + }; + 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 delta = (target - 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) +} + pub fn apply_primitive_property( operation: &mut SolidHistoryOperation, field: &str, @@ -856,6 +1065,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::().ok() } else { @@ -1008,7 +1222,26 @@ 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(); @@ -1016,9 +1249,26 @@ pub fn apply_primitive_property( return false; } let sides = rounded as i32; - if !(3..=71).contains(&sides) { + if !(3..=32).contains(&sides) { return false; } + if sides == value.sides { + return false; + } + if !pyramid_is_inscribed(value) { + 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 Some(current) = matrix(value.base.transform) else { + return false; + }; + 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; } _ => return false, @@ -1318,6 +1568,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 +1582,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 +1737,14 @@ 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); + value.sides = value.sides.clamp(3, 32); } _ => return false, }, @@ -1608,15 +1868,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() }) } diff --git a/src/scene/model/solid_model.rs b/src/scene/model/solid_model.rs index 335c3377f..582037219 100644 --- a/src/scene/model/solid_model.rs +++ b/src/scene/model/solid_model.rs @@ -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 { + 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. From f9641bd9d51f01f5b41122d62538c078c6202d5e Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Tue, 1 Sep 2026 11:19:06 +0300 Subject: [PATCH 2/3] Correct pyramid rotation and property sections --- src/app/properties.rs | 27 +++++++++++++++++++++++++++ src/scene/model/solid_history.rs | 21 +++++++++++++++++---- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/app/properties.rs b/src/app/properties.rs index 6520a0939..d0038380e 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -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, +) { + 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], diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index b43e31fb7..409ba1b04 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -384,6 +384,16 @@ fn pyramid_display_radius(value: &SolidHistoryPyramid, radius: f64) -> f64 { } } +fn pyramid_display_rotation(value: &SolidHistoryPyramid) -> Option { + 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, @@ -392,9 +402,7 @@ fn pyramid_properties( let Some(position) = world_point(value.base.transform, [0.0; 3]) else { return Vec::new(); }; - let rotation = matrix(value.base.transform) - .map(|matrix| matrix.x_axis.y.atan2(matrix.x_axis.x)) - .unwrap_or(0.0); + 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) = @@ -998,7 +1006,12 @@ fn apply_pyramid_geometry_property( return Some(false); } let current_angle = projected.y.atan2(projected.x); - let delta = (target - current_angle + std::f64::consts::PI) + 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 { From 8f5bffd74a629d1e2e080bd90353dd193510fb80 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 1 Sep 2026 22:51:42 +0300 Subject: [PATCH 3/3] fix(pyramid): preserve displayed construction values --- src/modules/model/primitive_cmd.rs | 25 ++++++++++++-- src/scene/model/solid_history.rs | 54 ++++++++++++++++-------------- 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/src/modules/model/primitive_cmd.rs b/src/modules/model/primitive_cmd.rs index c0bfcdc6a..c862fe8e9 100644 --- a/src/modules/model/primitive_cmd.rs +++ b/src/modules/model/primitive_cmd.rs @@ -1058,6 +1058,17 @@ impl PrimitiveCommand { } } + 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; @@ -1410,11 +1421,11 @@ impl PrimitiveCommand { return Some(CmdResult::NeedPoint); } PyramidStep::BaseRadius if matches!(upper.as_str(), "I" | "INSCRIBED") => { - self.pyramid_type = PyramidType::Inscribed; + self.set_pyramid_type(PyramidType::Inscribed); return Some(CmdResult::NeedPoint); } PyramidStep::BaseRadius if matches!(upper.as_str(), "C" | "CIRCUMSCRIBED") => { - self.pyramid_type = PyramidType::Circumscribed; + self.set_pyramid_type(PyramidType::Circumscribed); return Some(CmdResult::NeedPoint); } PyramidStep::Height => { @@ -1445,7 +1456,17 @@ impl PrimitiveCommand { 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); diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index 409ba1b04..a0b5a1879 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -983,11 +983,11 @@ fn apply_pyramid_geometry_property( } else { value.top_radius * factor }; - value.radius = if next_inscribed { base_display } else { base_display / factor }; - value.top_radius = if next_inscribed { top_display } else { top_display / 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 = @@ -1049,6 +1049,27 @@ fn apply_pyramid_geometry_property( 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, @@ -1261,28 +1282,7 @@ pub fn apply_primitive_property( if (number - rounded).abs() > 1e-9 { return false; } - let sides = rounded as i32; - if !(3..=32).contains(&sides) { - return false; - } - if sides == value.sides { - return false; - } - if !pyramid_is_inscribed(value) { - 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 Some(current) = matrix(value.base.transform) else { - return false; - }; - 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; + return set_pyramid_sides(value, rounded as i32); } _ => return false, }, @@ -1756,8 +1756,10 @@ pub fn apply_primitive_grip( 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, 32); + let sides = ((angle.to_degrees() / 5.0).round() as i32).clamp(3, 32); + if !set_pyramid_sides(value, sides) { + return false; + } } _ => return false, },