From a2a64dcfb74e3d6322d0657a9d16aadaaa548c9d Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:58:18 +0300 Subject: [PATCH 1/7] Complete CONE creation workflow --- src/modules/draw/draw/circle.rs | 6 +- src/modules/model/primitive_cmd.rs | 772 ++++++++++++++++++++++++++++- src/scene/model/solid_model.rs | 17 + 3 files changed, 771 insertions(+), 24 deletions(-) diff --git a/src/modules/draw/draw/circle.rs b/src/modules/draw/draw/circle.rs index a5bb9b943..12c684bf5 100644 --- a/src/modules/draw/draw/circle.rs +++ b/src/modules/draw/draw/circle.rs @@ -98,7 +98,7 @@ fn plane_distance(a: DVec3, b: DVec3, plane: WorkingPlane) -> f64 { d.x.hypot(d.y) } -fn tangent_object_local(object: TangentObject, plane: WorkingPlane) -> TangentObject { +pub(crate) fn tangent_object_local(object: TangentObject, plane: WorkingPlane) -> TangentObject { match object { TangentObject::Line { p1, p2 } => TangentObject::Line { p1: plane.to_local(p1), @@ -594,7 +594,7 @@ fn solve_quadratic(a: f64, b: f64, c: f64) -> Vec { vec![(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)] } -fn best_of(candidates: &[DVec3], hint: DVec3) -> Option { +pub(crate) fn best_of(candidates: &[DVec3], hint: DVec3) -> Option { candidates.iter().copied().min_by(|a, b| { a.distance(hint) .partial_cmp(&b.distance(hint)) @@ -627,7 +627,7 @@ fn tangent_curve(object: TangentObject) -> KernelCurve { } } -fn ttr_candidates(first: TangentObject, second: TangentObject, radius: f64) -> Vec { +pub(crate) fn ttr_candidates(first: TangentObject, second: TangentObject, radius: f64) -> Vec { let first = tangent_curve(first); let second = tangent_curve(second); fillets_between(&first, &second, radius, Tolerance::default()) diff --git a/src/modules/model/primitive_cmd.rs b/src/modules/model/primitive_cmd.rs index cfaa65ca2..a3bf6f544 100644 --- a/src/modules/model/primitive_cmd.rs +++ b/src/modules/model/primitive_cmd.rs @@ -6,7 +6,7 @@ use acadrust::EntityType; use cadkernel::brep::Body; use glam::DVec3; -use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane}; +use crate::command::{CadCommand, CmdOption, CmdResult, TangentObject, WorkingPlane}; use crate::scene::model::solid_model; use crate::scene::model::wire_model::WireModel; use crate::t; @@ -36,6 +36,59 @@ enum BoxStep { HeightSecondPoint, } +#[derive(Clone, Copy)] +enum ConeStep { + BaseCenter, + BaseRadius, + BaseDiameter, + ThreePointFirst, + ThreePointSecond(DVec3), + ThreePointThird(DVec3, DVec3), + TwoPointFirst, + TwoPointSecond(DVec3), + EllipseFirst, + EllipseSecond(DVec3), + EllipseThird(DVec3, DVec3), + TtrFirst, + TtrSecond { object: TangentObject, hit: DVec3 }, + TtrRadius { + first: TangentObject, + second: TangentObject, + first_hit: DVec3, + second_hit: DVec3, + }, + Height, + HeightFirstPoint, + HeightSecondPoint(DVec3), + AxisEndpoint, + TopRadius, +} + +#[derive(Clone, Copy)] +struct ConeDefaults { + base_x_radius: f64, + base_y_radius: f64, + top_radius: f64, + height: f64, +} + +impl Default for ConeDefaults { + fn default() -> Self { + Self { + base_x_radius: 1.0, + base_y_radius: 1.0, + top_radius: 0.0, + height: 1.0, + } + } +} + +fn cone_defaults() -> &'static std::sync::Mutex { + static DEFAULTS: std::sync::OnceLock> = + std::sync::OnceLock::new(); + DEFAULTS.get_or_init(|| std::sync::Mutex::new(ConeDefaults::default())) +} + impl Shape { fn from_id(id: &str) -> Option { Some(match id { @@ -85,11 +138,21 @@ pub struct PrimitiveCommand { box_angle: f64, box_width_sign: f64, box_height_anchor: Option, + cone_step: ConeStep, + cone_frame: Option, + cone_base_x_radius: f64, + cone_base_y_radius: f64, + cone_top_radius: f64, + cone_defaults: ConeDefaults, plane: WorkingPlane, } impl PrimitiveCommand { pub fn new(id: &str) -> Self { + let remembered = cone_defaults() + .lock() + .map(|value| *value) + .unwrap_or_default(); Self { shape: Shape::from_id(id).unwrap_or(Shape::Box), pts: Vec::new(), @@ -102,10 +165,545 @@ impl PrimitiveCommand { box_angle: 0.0, box_width_sign: 1.0, box_height_anchor: None, + cone_step: ConeStep::BaseCenter, + cone_frame: None, + cone_base_x_radius: remembered.base_x_radius, + cone_base_y_radius: remembered.base_y_radius, + cone_top_radius: remembered.top_radius, + cone_defaults: remembered, plane: WorkingPlane::default(), } } + fn cone_frame_at(&self, center: DVec3) -> WorkingPlane { + WorkingPlane { + origin: center, + x: self.plane.x, + y: self.plane.y, + z: self.plane.z, + } + } + + fn set_cone_base( + &mut self, + frame: WorkingPlane, + base_x_radius: f64, + base_y_radius: f64, + ) -> bool { + if !frame.origin.is_finite() + || !frame.x.is_finite() + || !frame.y.is_finite() + || !frame.z.is_finite() + || !base_x_radius.is_finite() + || !base_y_radius.is_finite() + || base_x_radius < 1e-6 + || base_y_radius < 1e-6 + { + return false; + } + self.cone_frame = Some(frame); + self.cone_base_x_radius = base_x_radius; + self.cone_base_y_radius = base_y_radius; + self.cone_step = ConeStep::Height; + true + } + + fn cone_three_point_frame(a: DVec3, b: DVec3, c: DVec3) -> Option<(WorkingPlane, f64)> { + let normal = (b - a).cross(c - a).try_normalize()?; + let first_axis = (b - a).try_normalize()?; + let second_axis = normal.cross(first_axis).try_normalize()?; + let plane = WorkingPlane::new(a, first_axis, second_axis); + let local_b = plane.to_local(b); + let local_c = plane.to_local(c); + let circle = cadkernel::geom2d::arc_through_points( + [0.0, 0.0], + [local_b.x, local_b.y], + [local_c.x, local_c.y], + )?; + let center = plane.to_world(DVec3::new(circle.centre[0], circle.centre[1], 0.0)); + let x = (a - center).try_normalize()?; + let y = normal.cross(x).try_normalize()?; + Some((WorkingPlane::new(center, x, y), circle.radius)) + } + + fn cone_axis_frame(&self, axis: DVec3) -> Option<(WorkingPlane, f64)> { + let current = self.cone_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 cone_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_cone(&self, axis: DVec3) -> Option<(Body, SolidHistoryOperation, f64)> { + let (frame, height) = self.cone_axis_frame(axis)?; + let local = solid_model::cone_frustum_solid( + [0.0; 3], + self.cone_base_x_radius, + self.cone_base_y_radius, + self.cone_top_radius, + height, + )?; + 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::cone_op( + Self::cone_history_transform(frame), + self.cone_base_x_radius, + self.cone_base_y_radius, + self.cone_top_radius, + height, + ), + height, + )) + } + + fn commit_cone_axis(&self, axis: DVec3) -> CmdResult { + let Some((solid, history, height)) = self.build_cone(axis) else { + return CmdResult::NeedPoint; + }; + if let Ok(mut defaults) = cone_defaults().lock() { + *defaults = ConeDefaults { + base_x_radius: self.cone_base_x_radius, + base_y_radius: self.cone_base_y_radius, + top_radius: self.cone_top_radius, + height, + }; + } + 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_cone_height(&self, height: f64) -> CmdResult { + let Some(frame) = self.cone_frame else { + return CmdResult::NeedPoint; + }; + if !height.is_finite() || height.abs() < 1e-6 { + return CmdResult::NeedPoint; + } + self.commit_cone_axis(frame.z * height) + } + + fn cone_height_at(&self, point: DVec3) -> Option { + let frame = self.cone_frame?; + Some((point - frame.origin).dot(frame.z)) + } + + fn cone_ttr_center(&self, radius: f64) -> Option { + let ConeStep::TtrRadius { + first, + second, + first_hit, + second_hit, + } = self.cone_step + else { + return None; + }; + let candidates = crate::modules::draw::draw::circle::ttr_candidates(first, second, radius); + let local = crate::modules::draw::draw::circle::best_of( + &candidates, + (first_hit + second_hit) * 0.5, + )?; + Some(self.plane.to_world(local)) + } + + fn cone_preview(&self, axis: DVec3) -> Option { + let (frame, height) = self.cone_axis_frame(axis)?; + let top = frame.origin + frame.z * height; + let top_y = if self.cone_top_radius > 0.0 { + self.cone_top_radius * self.cone_base_y_radius / self.cone_base_x_radius + } else { + 0.0 + }; + let mut points = Vec::new(); + push_ellipse_world( + &mut points, + frame.origin, + frame.x, + frame.y, + self.cone_base_x_radius, + self.cone_base_y_radius, + ); + if self.cone_top_radius > 1e-9 { + push_ellipse_world( + &mut points, + top, + frame.x, + frame.y, + self.cone_top_radius, + top_y, + ); + } + for index in 0..4 { + let angle = index as f64 * std::f64::consts::FRAC_PI_2; + let base = frame.origin + + frame.x * (self.cone_base_x_radius * angle.cos()) + + frame.y * (self.cone_base_y_radius * angle.sin()); + let upper = if self.cone_top_radius > 1e-9 { + top + frame.x * (self.cone_top_radius * angle.cos()) + + frame.y * (top_y * angle.sin()) + } else { + top + }; + push_segment(&mut points, base, upper); + } + Some(wire("primitive_height_preview", points)) + } + + fn cone_base_preview(&self, frame: WorkingPlane, x_radius: f64, y_radius: f64) -> WireModel { + let mut points = Vec::new(); + push_ellipse_world( + &mut points, + frame.origin, + frame.x, + frame.y, + x_radius.max(1e-9), + y_radius.max(1e-9), + ); + wire("primitive_preview", points) + } + + fn cone_prompt(&self) -> String { + match self.cone_step { + ConeStep::BaseCenter => + t!("CONE Specify center point of base or [3P/2P/Ttr/Elliptical]:").into_owned(), + ConeStep::BaseRadius => crate::tf!( + "CONE Specify base radius or [Diameter] <{:.4}>:", + self.cone_defaults.base_x_radius + ).into_owned(), + ConeStep::BaseDiameter => crate::tf!( + "CONE Specify base diameter <{:.4}>:", + self.cone_defaults.base_x_radius * 2.0 + ).into_owned(), + ConeStep::ThreePointFirst => t!("CONE Specify first point on base:").into_owned(), + ConeStep::ThreePointSecond(_) => t!("CONE Specify second point on base:").into_owned(), + ConeStep::ThreePointThird(_, _) => t!("CONE Specify third point on base:").into_owned(), + ConeStep::TwoPointFirst => t!("CONE Specify first endpoint of base diameter:").into_owned(), + ConeStep::TwoPointSecond(_) => t!("CONE Specify second endpoint of base diameter:").into_owned(), + ConeStep::EllipseFirst => t!("CONE Specify first endpoint of ellipse axis:").into_owned(), + ConeStep::EllipseSecond(_) => t!("CONE Specify second endpoint of ellipse axis:").into_owned(), + ConeStep::EllipseThird(_, _) => t!("CONE Specify distance to other ellipse axis:").into_owned(), + ConeStep::TtrFirst => t!("CONE Select first tangent object:").into_owned(), + ConeStep::TtrSecond { .. } => t!("CONE Select second tangent object:").into_owned(), + ConeStep::TtrRadius { .. } => crate::tf!( + "CONE Specify base radius <{:.4}>:", + self.cone_defaults.base_x_radius + ).into_owned(), + ConeStep::Height => crate::tf!( + "CONE Specify height or [2Point/Axis endpoint/Top radius] <{:.4}>:", + self.cone_defaults.height + ).into_owned(), + ConeStep::HeightFirstPoint => t!("CONE Specify first point for height:").into_owned(), + ConeStep::HeightSecondPoint(_) => t!("CONE Specify second point for height:").into_owned(), + ConeStep::AxisEndpoint => t!("CONE Specify axis endpoint:").into_owned(), + ConeStep::TopRadius => crate::tf!( + "CONE Specify top radius <{:.4}>:", + self.cone_defaults.top_radius + ).into_owned(), + } + } + + fn cone_options(&self) -> Vec { + match self.cone_step { + ConeStep::BaseCenter => vec![ + CmdOption::new("3P", "3P"), + CmdOption::new("2P", "2P"), + CmdOption::new("Ttr", "TTR"), + CmdOption::new(t!("Elliptical").as_ref(), "E"), + ], + ConeStep::BaseRadius => vec![CmdOption::new(t!("Diameter").as_ref(), "D")], + ConeStep::Height => vec![ + CmdOption::new("2Point", "2P"), + CmdOption::new(t!("Axis endpoint").as_ref(), "A"), + CmdOption::new(t!("Top radius").as_ref(), "T"), + ], + _ => Vec::new(), + } + } + + fn on_cone_point(&mut self, point: DVec3) -> CmdResult { + match self.cone_step { + ConeStep::BaseCenter => { + self.cone_frame = Some(self.cone_frame_at(point)); + self.cone_step = ConeStep::BaseRadius; + CmdResult::NeedPoint + } + ConeStep::BaseRadius | ConeStep::BaseDiameter => { + let Some(frame) = self.cone_frame else { + return CmdResult::NeedPoint; + }; + let local = frame.to_local(point); + let radius = local.x.hypot(local.y); + if self.set_cone_base(frame, radius, radius) { + CmdResult::NeedPoint + } else { + CmdResult::NeedPoint + } + } + ConeStep::ThreePointFirst => { + self.cone_step = ConeStep::ThreePointSecond(point); + CmdResult::NeedPoint + } + ConeStep::ThreePointSecond(first) => { + if point.distance_squared(first) > 1e-12 { + self.cone_step = ConeStep::ThreePointThird(first, point); + } + CmdResult::NeedPoint + } + ConeStep::ThreePointThird(first, second) => { + if let Some((frame, radius)) = Self::cone_three_point_frame(first, second, point) { + self.set_cone_base(frame, radius, radius); + } + CmdResult::NeedPoint + } + ConeStep::TwoPointFirst => { + self.cone_step = ConeStep::TwoPointSecond(point); + CmdResult::NeedPoint + } + ConeStep::TwoPointSecond(first) => { + let a = self.plane.to_local(first); + let b = self.plane.to_local(point); + let radius = (b - a).truncate().length() * 0.5; + let center = self.plane.to_world((a + b) * 0.5); + self.set_cone_base(self.cone_frame_at(center), radius, radius); + CmdResult::NeedPoint + } + ConeStep::EllipseFirst => { + self.cone_step = ConeStep::EllipseSecond(point); + CmdResult::NeedPoint + } + ConeStep::EllipseSecond(first) => { + if point.distance_squared(first) > 1e-12 { + self.cone_step = ConeStep::EllipseThird(first, point); + } + CmdResult::NeedPoint + } + ConeStep::EllipseThird(first, second) => { + let a = self.plane.to_local(first); + let b = self.plane.to_local(second); + let center_local = (a + b) * 0.5; + let axis = (b - a).truncate(); + let x_radius = axis.length() * 0.5; + if let Some(x2) = axis.try_normalize() { + let x = self.plane.vector_to_world(DVec3::new(x2.x, x2.y, 0.0)); + let y = self.plane.z.cross(x).normalize_or_zero(); + let frame = WorkingPlane::new(self.plane.to_world(center_local), x, y); + let local = frame.to_local(point); + self.set_cone_base(frame, x_radius, local.y.abs()); + } + CmdResult::NeedPoint + } + ConeStep::TtrRadius { second_hit, .. } => { + let local = self.plane.to_local(point); + let radius = (local - second_hit).truncate().length(); + if let Some(center) = self.cone_ttr_center(radius) { + self.set_cone_base(self.cone_frame_at(center), radius, radius); + } + CmdResult::NeedPoint + } + ConeStep::Height => self + .cone_height_at(point) + .map(|height| self.commit_cone_height(height)) + .unwrap_or(CmdResult::NeedPoint), + ConeStep::HeightFirstPoint => { + self.cone_step = ConeStep::HeightSecondPoint(point); + CmdResult::NeedPoint + } + ConeStep::HeightSecondPoint(first) => self.commit_cone_height(first.distance(point)), + ConeStep::AxisEndpoint => { + let Some(frame) = self.cone_frame else { + return CmdResult::NeedPoint; + }; + self.commit_cone_axis(point - frame.origin) + } + ConeStep::TopRadius => { + let Some(frame) = self.cone_frame else { + return CmdResult::NeedPoint; + }; + let local = frame.to_local(point); + let radius = local.x.hypot(local.y); + if radius.is_finite() { + self.cone_top_radius = radius; + self.cone_step = ConeStep::Height; + } + CmdResult::NeedPoint + } + ConeStep::TtrFirst | ConeStep::TtrSecond { .. } => CmdResult::NeedPoint, + } + } + + fn on_cone_tangent(&mut self, object: TangentObject, hit: DVec3) -> CmdResult { + let object = crate::modules::draw::draw::circle::tangent_object_local(object, self.plane); + let hit = self.plane.to_local(hit); + match self.cone_step { + ConeStep::TtrFirst => { + self.cone_step = ConeStep::TtrSecond { object, hit }; + } + ConeStep::TtrSecond { + object: first, + hit: first_hit, + } => { + self.cone_step = ConeStep::TtrRadius { + first, + second: object, + first_hit, + second_hit: hit, + }; + } + _ => {} + } + CmdResult::NeedPoint + } + + fn on_cone_text(&mut self, raw: &str) -> Option { + let token = raw.trim(); + let upper = token.to_uppercase(); + match self.cone_step { + ConeStep::BaseCenter => { + self.cone_step = match upper.as_str() { + "3P" => ConeStep::ThreePointFirst, + "2P" => ConeStep::TwoPointFirst, + "T" | "TTR" => ConeStep::TtrFirst, + "E" | "ELLIPTICAL" => ConeStep::EllipseFirst, + _ => return None, + }; + return Some(CmdResult::NeedPoint); + } + ConeStep::BaseRadius if matches!(upper.as_str(), "D" | "DIAMETER") => { + self.cone_step = ConeStep::BaseDiameter; + return Some(CmdResult::NeedPoint); + } + ConeStep::Height => { + self.cone_step = match upper.as_str() { + "2P" | "2POINT" => ConeStep::HeightFirstPoint, + "A" | "AXIS" | "AXIS ENDPOINT" => ConeStep::AxisEndpoint, + "T" | "TOP" | "TOP RADIUS" => ConeStep::TopRadius, + _ => { + let height = crate::entities::common::parse_typed_length(token)?; + return Some(self.commit_cone_height(height)); + } + }; + return Some(CmdResult::NeedPoint); + } + _ => {} + } + let number = crate::entities::common::parse_typed_length(token)?; + match self.cone_step { + ConeStep::BaseRadius => { + let frame = self.cone_frame?; + (number > 0.0).then(|| { + self.set_cone_base(frame, number, number); + CmdResult::NeedPoint + }) + } + ConeStep::BaseDiameter => { + let frame = self.cone_frame?; + (number > 0.0).then(|| { + self.set_cone_base(frame, number * 0.5, number * 0.5); + CmdResult::NeedPoint + }) + } + ConeStep::TtrRadius { .. } => { + let center = self.cone_ttr_center(number)?; + (number > 0.0).then(|| { + self.set_cone_base(self.cone_frame_at(center), number, number); + CmdResult::NeedPoint + }) + } + ConeStep::TopRadius if number >= 0.0 => { + self.cone_top_radius = number; + self.cone_step = ConeStep::Height; + Some(CmdResult::NeedPoint) + } + _ => None, + } + } + + fn cone_mouse_move(&self, point: DVec3) -> Option { + match self.cone_step { + ConeStep::BaseRadius | ConeStep::BaseDiameter => { + let frame = self.cone_frame?; + let local = frame.to_local(point); + let radius = local.x.hypot(local.y); + Some(self.cone_base_preview(frame, radius, radius)) + } + ConeStep::ThreePointSecond(first) | ConeStep::TwoPointSecond(first) => { + let a = self.plane.to_local(first); + let b = self.plane.to_local(point); + let center = self.plane.to_world((a + b) * 0.5); + let radius = (b - a).truncate().length() * 0.5; + Some(self.cone_base_preview(self.cone_frame_at(center), radius, radius)) + } + ConeStep::ThreePointThird(first, second) => { + let (frame, radius) = Self::cone_three_point_frame(first, second, point)?; + Some(self.cone_base_preview(frame, radius, radius)) + } + ConeStep::EllipseThird(first, second) => { + let a = self.plane.to_local(first); + let b = self.plane.to_local(second); + let center_local = (a + b) * 0.5; + let axis = (b - a).truncate(); + let x2 = axis.try_normalize()?; + let x = self.plane.vector_to_world(DVec3::new(x2.x, x2.y, 0.0)); + let y = self.plane.z.cross(x).normalize_or_zero(); + let frame = WorkingPlane::new(self.plane.to_world(center_local), x, y); + let local = frame.to_local(point); + Some(self.cone_base_preview(frame, axis.length() * 0.5, local.y.abs())) + } + ConeStep::TtrRadius { second_hit, .. } => { + let local = self.plane.to_local(point); + let radius = (local - second_hit).truncate().length(); + let center = self.cone_ttr_center(radius)?; + Some(self.cone_base_preview(self.cone_frame_at(center), radius, radius)) + } + ConeStep::Height => { + let frame = self.cone_frame?; + self.cone_preview(frame.z * self.cone_height_at(point)?) + } + ConeStep::HeightSecondPoint(first) => { + let frame = self.cone_frame?; + self.cone_preview(frame.z * first.distance(point)) + } + ConeStep::AxisEndpoint => { + let frame = self.cone_frame?; + self.cone_preview(point - frame.origin) + } + _ => None, + } + } + /// Number of footprint points the shape needs before the height step. fn footprint_pts(&self) -> usize { match self.shape { @@ -361,33 +959,19 @@ impl PrimitiveCommand { ), ) } - Shape::Cylinder | Shape::Cone => { + Shape::Cylinder => { let c = self.pts[0]; let r = (self.pts[1] - c).length(); if r < 1e-6 || height < 1e-6 { return None; } let center = [c.x, c.y, c.z]; - if self.shape == Shape::Cylinder { - ( - solid_model::cylinder_solid(center, r, height), - solid_history::cylinder_op( - self.history_transform(c), - r, - height, - ), - ) - } else { - ( - solid_model::cone_solid(center, r, height), - solid_history::cone_op( - self.history_transform(c), - r, - height, - ), - ) - } + ( + solid_model::cylinder_solid(center, r, height), + solid_history::cylinder_op(self.history_transform(c), r, height), + ) } + Shape::Cone => return None, Shape::Sphere => { let c = self.pts[0]; let r = (self.pts[1] - c).length(); @@ -482,6 +1066,12 @@ impl CadCommand for PrimitiveCommand { } fn cursor_axis(&self) -> Option<(DVec3, DVec3)> { + if self.shape == Shape::Cone { + return matches!(self.cone_step, ConeStep::Height).then(|| { + let frame = self.cone_frame.unwrap_or(self.plane); + (frame.origin, frame.z.normalize_or_zero()) + }); + } let constrained = if self.shape == Shape::Box { self.box_step == BoxStep::Height } else { @@ -501,6 +1091,9 @@ impl CadCommand for PrimitiveCommand { fn prompt(&self) -> String { let n = self.shape.name(); + if self.shape == Shape::Cone { + return self.cone_prompt(); + } if self.shape == Shape::Box { return match self.box_step { BoxStep::FirstCorner => t!("%{n} Specify first corner or [Center]:", n = n), @@ -540,6 +1133,9 @@ impl CadCommand for PrimitiveCommand { } fn options(&self) -> Vec { + if self.shape == Shape::Cone { + return self.cone_options(); + } if self.shape != Shape::Box { return Vec::new(); } @@ -555,6 +1151,9 @@ impl CadCommand for PrimitiveCommand { } fn on_point(&mut self, pt: DVec3) -> CmdResult { + if self.shape == Shape::Cone { + return self.on_cone_point(pt); + } if self.shape == Shape::Box { let local = self.plane.to_local(pt); if !local.is_finite() { @@ -632,6 +1231,36 @@ impl CadCommand for PrimitiveCommand { } fn on_enter(&mut self) -> CmdResult { + if self.shape == Shape::Cone { + return match self.cone_step { + ConeStep::BaseRadius | ConeStep::BaseDiameter => { + let Some(frame) = self.cone_frame else { + return CmdResult::Cancel; + }; + self.set_cone_base( + frame, + self.cone_defaults.base_x_radius, + self.cone_defaults.base_y_radius, + ); + CmdResult::NeedPoint + } + ConeStep::TtrRadius { .. } => { + let radius = self.cone_defaults.base_x_radius; + let Some(center) = self.cone_ttr_center(radius) else { + return CmdResult::NeedPoint; + }; + self.set_cone_base(self.cone_frame_at(center), radius, radius); + CmdResult::NeedPoint + } + ConeStep::Height => self.commit_cone_height(self.cone_defaults.height), + ConeStep::TopRadius => { + self.cone_top_radius = self.cone_defaults.top_radius; + self.cone_step = ConeStep::Height; + CmdResult::NeedPoint + } + _ => CmdResult::Cancel, + }; + } if self.shape == Shape::Box { return CmdResult::Cancel; } @@ -646,7 +1275,31 @@ impl CadCommand for PrimitiveCommand { CmdResult::Cancel } + fn needs_tangent_pick(&self) -> bool { + self.shape == Shape::Cone + && matches!(self.cone_step, ConeStep::TtrFirst | ConeStep::TtrSecond { .. }) + } + + fn on_tangent_point(&mut self, object: TangentObject, hit: DVec3) -> CmdResult { + if self.shape == Shape::Cone { + self.on_cone_tangent(object, hit) + } else { + self.on_point(hit) + } + } + fn wants_text_input(&self) -> bool { + if self.shape == Shape::Cone { + return matches!( + self.cone_step, + ConeStep::BaseCenter + | ConeStep::BaseRadius + | ConeStep::BaseDiameter + | ConeStep::TtrRadius { .. } + | ConeStep::Height + | ConeStep::TopRadius + ); + } if self.shape == Shape::Box { matches!( self.box_step, @@ -663,6 +1316,12 @@ impl CadCommand for PrimitiveCommand { } fn point_step_accepts_keywords(&self) -> bool { + if self.shape == Shape::Cone { + return matches!( + self.cone_step, + ConeStep::BaseCenter | ConeStep::BaseRadius | ConeStep::Height + ); + } self.shape == Shape::Box && matches!( self.box_step, @@ -671,6 +1330,9 @@ impl CadCommand for PrimitiveCommand { } fn on_text_input(&mut self, raw: &str) -> Option { + if self.shape == Shape::Cone { + return self.on_cone_text(raw); + } if self.shape == Shape::Box { let token = raw.trim(); let upper = token.to_uppercase(); @@ -719,6 +1381,9 @@ impl CadCommand for PrimitiveCommand { } fn on_mouse_move(&mut self, pt: DVec3) -> Option { + if self.shape == Shape::Cone { + return self.cone_mouse_move(pt); + } if self.pts.is_empty() { return None; } @@ -806,6 +1471,30 @@ impl CadCommand for PrimitiveCommand { fn dyn_spec(&self) -> Option { use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec}; + if self.shape == Shape::Cone { + let frame = self.cone_frame.unwrap_or(self.plane); + let role = match self.cone_step { + ConeStep::BaseRadius | ConeStep::TtrRadius { .. } | ConeStep::TopRadius => { + DynRole::Radius + } + ConeStep::BaseDiameter => DynRole::Diameter, + ConeStep::Height | ConeStep::HeightSecondPoint(_) | ConeStep::AxisEndpoint => { + DynRole::Height + } + _ => return None, + }; + return Some(DynSpec { + anchor: DynAnchor::Point(frame.origin), + fields: vec![DynFieldSpec::new(role)], + guide: if matches!(role, DynRole::Radius | DynRole::Diameter) { + DynGuide::Radius + } else { + DynGuide::None + }, + ref_point: None, + }); + } + let role = if self.shape == Shape::Box { match self.box_step { BoxStep::CubeSize | BoxStep::Length => DynRole::Distance, @@ -827,6 +1516,28 @@ impl CadCommand for PrimitiveCommand { } fn dyn_live_value(&self, cursor: DVec3) -> Option { + if self.shape == Shape::Cone { + return match self.cone_step { + ConeStep::BaseRadius | ConeStep::TopRadius => { + let frame = self.cone_frame?; + let local = frame.to_local(cursor); + Some(local.x.hypot(local.y)) + } + ConeStep::BaseDiameter => { + let frame = self.cone_frame?; + let local = frame.to_local(cursor); + Some(local.x.hypot(local.y) * 2.0) + } + ConeStep::TtrRadius { second_hit, .. } => { + let local = self.plane.to_local(cursor); + Some((local - second_hit).truncate().length()) + } + ConeStep::Height => self.cone_height_at(cursor).map(f64::abs), + ConeStep::HeightSecondPoint(first) => Some(first.distance(cursor)), + ConeStep::AxisEndpoint => self.cone_frame.map(|frame| frame.origin.distance(cursor)), + _ => None, + }; + } if self.shape == Shape::Box { let local = self.plane.to_local(cursor); if !local.is_finite() { @@ -972,6 +1683,25 @@ fn push_circle(points: &mut Vec<[f32; 3]>, center: DVec3, radius: f64) { circle_points(points, center, radius); } +fn push_ellipse_world( + points: &mut Vec<[f32; 3]>, + center: DVec3, + x_axis: DVec3, + y_axis: DVec3, + x_radius: f64, + y_radius: f64, +) { + push_break(points); + const SEGMENTS: usize = 64; + for index in 0..=SEGMENTS { + let angle = index as f64 / SEGMENTS as f64 * std::f64::consts::TAU; + let point = center + + x_axis * (x_radius * angle.cos()) + + y_axis * (y_radius * angle.sin()); + points.push(point.as_vec3().to_array()); + } +} + fn circle_points(out: &mut Vec<[f32; 3]>, c: DVec3, r: f64) { const SEG: usize = 48; for i in 0..=SEG { diff --git a/src/scene/model/solid_model.rs b/src/scene/model/solid_model.rs index b9d7f5dbf..403983bc9 100644 --- a/src/scene/model/solid_model.rs +++ b/src/scene/model/solid_model.rs @@ -45,6 +45,23 @@ pub fn cone_solid(center: [f64; 3], radius: f64, height: f64) -> Option { brep::make::cone(center, radius, height) } +/// Circular or elliptical cone/frustum standing on the z = base plane. +pub fn cone_frustum_solid( + center: [f64; 3], + base_x_radius: f64, + base_y_radius: f64, + top_radius: f64, + height: f64, +) -> Option { + brep::make::frustum( + center, + base_x_radius, + base_y_radius, + top_radius, + height, + ) +} + /// Solid sphere about `center`. pub fn sphere_solid(center: [f64; 3], radius: f64) -> Option { brep::make::sphere(center, radius) From 0f4825f06c4dee64c6e7522cd39913ba75bfae52 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:58:29 +0300 Subject: [PATCH 2/7] Align CONE properties and editing --- src/app/properties.rs | 10 +- src/app/update/command.rs | 11 +- src/entities/object_data.rs | 13 +- src/scene/mod.rs | 1 + src/scene/model/solid_history.rs | 236 +++++++++++++++++++++++++++++-- src/scene/modify.rs | 10 +- 6 files changed, 258 insertions(+), 23 deletions(-) diff --git a/src/app/properties.rs b/src/app/properties.rs index cf4a5d093..62bb3f19c 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -441,14 +441,14 @@ impl OpenCADStudio { let display_entity = dispatch::entity_in_working_plane(contextual.as_ref(), plane); let entity = &display_entity; let group_names = self.tabs[i].scene.group_names_for_entity(handle); - let box_primitive = - crate::scene::model::solid_history::is_box_primitive( + let specialized_primitive = + crate::scene::model::solid_history::has_specialized_primitive_properties( &self.tabs[i].scene.document, handle, ); let mut sections = dispatch::properties_sectioned(handle, entity, &text_style_names); - if box_primitive { + if specialized_primitive { sections.retain(|section| { !section.props.iter().any(|property| { property.field.starts_with("acis_") @@ -620,7 +620,7 @@ impl OpenCADStudio { } } - if !box_primitive && matches!( + if !specialized_primitive && matches!( entity, acadrust::EntityType::Solid3D(_) | acadrust::EntityType::Region(_) @@ -694,7 +694,7 @@ impl OpenCADStudio { } } - if !box_primitive { + if !specialized_primitive { sections.extend(crate::entities::object_data::sections( &self.tabs[i].scene.document, &self.tabs[i].scene.object_data_cache, diff --git a/src/app/update/command.rs b/src/app/update/command.rs index ce1b6256d..a73fac21f 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -1940,7 +1940,16 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } self.push_undo_snapshot(i, "CHPROP"); - if crate::scene::model::solid_history::is_history_choice(field) { + if crate::scene::model::solid_history::is_primitive_choice(field) { + for &handle in &handles { + if self.tabs[i].scene.is_layer_locked(handle) { + continue; + } + self.tabs[i] + .scene + .apply_solid_history_property(handle, field, &value); + } + } else if crate::scene::model::solid_history::is_history_choice(field) { for &handle in &handles { if self.tabs[i].scene.is_layer_locked(handle) { continue; diff --git a/src/entities/object_data.rs b/src/entities/object_data.rs index 1807ee628..bed43563d 100644 --- a/src/entities/object_data.rs +++ b/src/entities/object_data.rs @@ -151,7 +151,8 @@ fn history_step_id(operation: &SolidHistoryOperation) -> Option { SolidHistoryOperation::Unknown => return None, SolidHistoryOperation::Box(value) | SolidHistoryOperation::Wedge(value) => &value.base, SolidHistoryOperation::Sphere(value) => &value.base, - SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => &value.base, + SolidHistoryOperation::Cylinder(value) => &value.base, + SolidHistoryOperation::Cone(value) => &value.base, SolidHistoryOperation::Pyramid(value) => &value.base, SolidHistoryOperation::Torus(value) => &value.base, SolidHistoryOperation::Boolean(value) => &value.base, @@ -225,7 +226,7 @@ fn history_operation_text(operation: &SolidHistoryOperation) -> String { history_base_text(&value.base), value.radius ), - SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => format!( + SolidHistoryOperation::Cylinder(value) => format!( "{}; height {:.6}; radii {:.6}/{:.6}; x-radius {:.6}", history_base_text(&value.base), value.height, @@ -233,6 +234,14 @@ fn history_operation_text(operation: &SolidHistoryOperation) -> String { value.minor_radius, value.x_radius ), + SolidHistoryOperation::Cone(value) => format!( + "{}; height {:.6}; base radii {:.6}/{:.6}; top radius {:.6}", + history_base_text(&value.base), + value.height, + value.base_x_radius, + value.base_y_radius, + value.top_radius + ), SolidHistoryOperation::Pyramid(value) => format!( "{}; height {:.6}; sides {}; radii {:.6}/{:.6}", history_base_text(&value.base), diff --git a/src/scene/mod.rs b/src/scene/mod.rs index afa0e3be8..e54209b74 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -2746,6 +2746,7 @@ impl Scene { self.meshes.remove(&handle); } self.solid_models.insert(handle, solid); + self.sync_solid_reference_point(handle); // Only this solid's mesh changed — report just its handle so the mesh / // wire caches patch it in rather than rebuilding the whole drawing (and // so a bulk register loop stays O(n), not O(n²)). diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index a7c05f7be..dc00c16ea 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -1,6 +1,6 @@ use acadrust::entities::Solid3D; use acadrust::objects::{ - DynamicBlockData, ObjectType, SolidHistoryBox, SolidHistoryBrep, + DynamicBlockData, ObjectType, SolidHistoryBox, SolidHistoryBrep, SolidHistoryCone, SolidHistoryCylinder, SolidHistoryNodeBase, SolidHistoryOperation, SolidHistoryPyramid, SolidHistorySphere, SolidHistoryTorus, }; @@ -31,6 +31,9 @@ pub const PROP_LENGTH: &str = "solid_history_length"; pub const PROP_WIDTH: &str = "solid_history_width"; pub const PROP_HEIGHT: &str = "solid_history_height"; pub const PROP_RADIUS: &str = "solid_history_radius"; +pub const PROP_BASE_RADIUS: &str = "solid_history_base_radius"; +pub const PROP_TOP_RADIUS: &str = "solid_history_top_radius"; +pub const PROP_ELLIPTICAL: &str = "solid_history_elliptical"; pub const PROP_OUTER_RADIUS: &str = "solid_history_outer_radius"; pub const PROP_INNER_RADIUS: &str = "solid_history_inner_radius"; pub const PROP_SIDES: &str = "solid_history_sides"; @@ -73,16 +76,108 @@ pub fn is_box_primitive( ) } +pub fn has_specialized_primitive_properties( + document: &acadrust::CadDocument, + handle: acadrust::Handle, +) -> bool { + matches!( + document.solid_history_operation(handle), + Some(SolidHistoryOperation::Box(_) | SolidHistoryOperation::Cone(_)) + ) +} + pub fn reference_point(operation: &SolidHistoryOperation) -> Option { match operation { SolidHistoryOperation::Box(value) => world_point( value.base.transform, [value.length * 0.5, value.width * 0.5, 0.0], ), + SolidHistoryOperation::Cone(value) => world_point(value.base.transform, [0.0; 3]), _ => None, } } +fn cone_properties( + document: &acadrust::CadDocument, + handle: acadrust::Handle, + value: &SolidHistoryCone, +) -> Vec { + let Some(position) = world_point(value.base.transform, [0.0; 3]) else { + return Vec::new(); + }; + let scale = value + .base_x_radius + .abs() + .max(value.base_y_radius.abs()) + .max(1.0); + let elliptical = (value.base_x_radius - value.base_y_radius).abs() > 1e-9 * scale; + let (record_history, show_history) = history_flags(document, handle).unwrap_or((false, false)); + 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!("Cone").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!("Elliptical").into_owned(), + field: PROP_ELLIPTICAL, + value: PropValue::Choice { + selected: if elliptical { "Yes" } else { "No" }.to_string(), + options: vec!["No".to_string(), "Yes".to_string()], + }, + }, + history_prop(t!("Base radius").as_ref(), PROP_BASE_RADIUS, value.base_x_radius), + history_prop(t!("Top radius").as_ref(), PROP_TOP_RADIUS, value.top_radius), + 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 record_history { + PropValue::Choice { + selected: show_value.to_string(), + options: vec!["No".to_string(), "Yes".to_string()], + } + } else { + PropValue::ReadOnly(show_value.to_string()) + }, + }, + ], + }, + ] +} + fn box_properties( document: &acadrust::CadDocument, handle: acadrust::Handle, @@ -184,12 +279,13 @@ pub fn primitive_properties( }; let props = match operation { SolidHistoryOperation::Box(value) => return box_properties(document, handle, value), + SolidHistoryOperation::Cone(value) => return cone_properties(document, handle, value), SolidHistoryOperation::Wedge(value) => vec![ history_prop(t!("Length").as_ref(), PROP_LENGTH, value.length), history_prop(t!("Width").as_ref(), PROP_WIDTH, value.width), history_prop(t!("Height").as_ref(), PROP_HEIGHT, value.height), ], - SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => vec![ + SolidHistoryOperation::Cylinder(value) => vec![ history_prop(t!("Radius").as_ref(), PROP_RADIUS, value.major_radius), history_prop(t!("Height").as_ref(), PROP_HEIGHT, value.height), ], @@ -230,6 +326,8 @@ pub fn is_primitive_property(field: &str) -> bool { | PROP_WIDTH | PROP_HEIGHT | PROP_RADIUS + | PROP_BASE_RADIUS + | PROP_TOP_RADIUS | PROP_OUTER_RADIUS | PROP_INNER_RADIUS | PROP_SIDES @@ -240,6 +338,10 @@ pub fn is_primitive_property(field: &str) -> bool { ) } +pub fn is_primitive_choice(field: &str) -> bool { + field == PROP_ELLIPTICAL +} + pub fn is_history_choice(field: &str) -> bool { matches!(field, PROP_HISTORY | PROP_SHOW_HISTORY) } @@ -355,6 +457,32 @@ fn apply_box_geometry_property( Some(true) } +fn apply_cone_position_property( + value: &mut SolidHistoryCone, + field: &str, + text: &str, +) -> Option { + 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] { + return Some(false); + } + let mut delta = glam::DVec3::ZERO; + delta[axis] = target - origin[axis]; + value.base.transform = (glam::DMat4::from_translation(delta) * current).to_cols_array(); + Some(true) +} + pub fn apply_primitive_property( operation: &mut SolidHistoryOperation, field: &str, @@ -365,6 +493,36 @@ pub fn apply_primitive_property( return applied; } } + if let SolidHistoryOperation::Cone(cone_value) = operation { + if let Some(applied) = apply_cone_position_property(cone_value, field, value) { + return applied; + } + if field == PROP_ELLIPTICAL { + let elliptical = if value.eq_ignore_ascii_case("Yes") { + true + } else if value.eq_ignore_ascii_case("No") { + false + } else { + return false; + }; + let scale = cone_value + .base_x_radius + .abs() + .max(cone_value.base_y_radius.abs()) + .max(1.0); + let currently_elliptical = + (cone_value.base_x_radius - cone_value.base_y_radius).abs() > 1e-9 * scale; + if elliptical == currently_elliptical { + return false; + } + cone_value.base_y_radius = if elliptical { + cone_value.base_x_radius * 0.5 + } else { + cone_value.base_x_radius + }; + return true; + } + } let Some(number) = (if field == PROP_SIDES { value.trim().parse::().ok() } else { @@ -417,7 +575,7 @@ pub fn apply_primitive_property( PROP_HEIGHT => value.height = positive().unwrap_or(value.height), _ => return false, }, - SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => match field { + SolidHistoryOperation::Cylinder(value) => match field { PROP_RADIUS => { let Some(radius) = positive() else { return false; @@ -429,6 +587,28 @@ pub fn apply_primitive_property( PROP_HEIGHT => value.height = positive().unwrap_or(value.height), _ => return false, }, + SolidHistoryOperation::Cone(value) => match field { + PROP_BASE_RADIUS => { + let Some(radius) = positive() else { + return false; + }; + let ratio = if value.base_x_radius > 1e-9 { + value.base_y_radius / value.base_x_radius + } else { + 1.0 + }; + value.base_x_radius = radius; + value.base_y_radius = radius * ratio; + } + PROP_TOP_RADIUS => { + if number < 0.0 { + return false; + } + value.top_radius = number; + } + PROP_HEIGHT => value.height = positive().unwrap_or(value.height), + _ => return false, + }, SolidHistoryOperation::Sphere(value) if field == PROP_RADIUS => { value.radius = positive().unwrap_or(value.radius); } @@ -477,7 +657,7 @@ pub fn apply_primitive_property( }, _ => return false, } - positive().is_some() || field == PROP_SIDES + positive().is_some() || field == PROP_SIDES || field == PROP_TOP_RADIUS } fn matrix(transform: [f64; 16]) -> Option { @@ -676,7 +856,7 @@ pub fn primitive_grips( Some([0.0, 0.0, 1.0]), ); } - SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => { + SolidHistoryOperation::Cylinder(value) => { add( GRIP_RADIUS, value.base.transform, @@ -692,6 +872,22 @@ pub fn primitive_grips( Some([0.0, 0.0, 1.0]), ); } + SolidHistoryOperation::Cone(value) => { + add( + GRIP_RADIUS, + value.base.transform, + [value.base_x_radius, 0.0, 0.0], + GripShape::Square, + None, + ); + add( + GRIP_HEIGHT, + value.base.transform, + [0.0, 0.0, value.height], + GripShape::Square, + Some([0.0, 0.0, 1.0]), + ); + } SolidHistoryOperation::Sphere(value) => add( GRIP_RADIUS, value.base.transform, @@ -825,7 +1021,7 @@ pub fn apply_primitive_grip( _ => return false, } } - SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => { + SolidHistoryOperation::Cylinder(value) => { match grip_id { GRIP_RADIUS => { let radius = local.x.hypot(local.y).max(1e-6); @@ -837,6 +1033,20 @@ pub fn apply_primitive_grip( _ => return false, } } + SolidHistoryOperation::Cone(value) => match grip_id { + GRIP_RADIUS => { + let radius = local.x.hypot(local.y).max(1e-6); + let ratio = if value.base_x_radius > 1e-9 { + value.base_y_radius / value.base_x_radius + } else { + 1.0 + }; + value.base_x_radius = radius; + value.base_y_radius = radius * ratio; + } + GRIP_HEIGHT => value.height = local.z.max(1e-6), + _ => return false, + }, SolidHistoryOperation::Sphere(value) if grip_id == GRIP_RADIUS => { value.radius = local.length().max(1e-6); } @@ -932,17 +1142,19 @@ pub fn cylinder_op( pub fn cone_op( transform: [f64; 16], - radius: f64, + base_x_radius: f64, + base_y_radius: f64, + top_radius: f64, height: f64, ) -> SolidHistoryOperation { - SolidHistoryOperation::Cone(SolidHistoryCylinder { + SolidHistoryOperation::Cone(SolidHistoryCone { base: base(transform), operation_major: 1, height, - major_radius: radius, - minor_radius: radius, - x_radius: radius, - ..SolidHistoryCylinder::default() + base_x_radius, + base_y_radius, + top_radius, + ..SolidHistoryCone::default() }) } diff --git a/src/scene/modify.rs b/src/scene/modify.rs index f376f9307..917d695b2 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -791,7 +791,11 @@ impl Scene { handle: Handle, operation: acadrust::objects::SolidHistoryOperation, ) -> bool { - let box_primitive = matches!(operation, acadrust::objects::SolidHistoryOperation::Box(_)); + let user_facing_primitive = matches!( + operation, + acadrust::objects::SolidHistoryOperation::Box(_) + | acadrust::objects::SolidHistoryOperation::Cone(_) + ); let Some(graph) = self.document.create_solid_history(handle, operation) else { return false; }; @@ -799,7 +803,7 @@ impl Scene { for node in graph.nodes { self.record_undo_object_before(node, None); } - if box_primitive { + if user_facing_primitive { let _ = crate::scene::model::solid_history::apply_history_choice( &mut self.document, handle, @@ -811,7 +815,7 @@ impl Scene { true } - fn sync_solid_reference_point(&mut self, handle: Handle) { + pub(crate) fn sync_solid_reference_point(&mut self, handle: Handle) { let reference = self .document .solid_history_operation(handle) From 00fafb034fb744621f82aa7e3fa82bce938c1eb8 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 31 Aug 2026 12:00:05 +0300 Subject: [PATCH 3/7] Update cone geometry dependencies --- Cargo.lock | 4 ++-- Cargo.toml | 6 ++---- crates/ocs_plugin_api/Cargo.toml | 4 ++-- crates/ocs_web_worker/Cargo.toml | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a1983e88b..bed02b08e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,7 +72,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.4.1" -source = "git+https://git@github.com/HakanSeven12/cadcodec.git?rev=45789a7700bd5c006feb4c7ff16c2ff80a7047db#45789a7700bd5c006feb4c7ff16c2ff80a7047db" +source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=c11f34334a6bd210674b56b1d9e91991b6943553#c11f34334a6bd210674b56b1d9e91991b6943553" dependencies = [ "ahash 0.8.12", "anyhow", @@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=0d48aa453f8689e4d737201b0a79604974427c6f#0d48aa453f8689e4d737201b0a79604974427c6f" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=6c4e5ee25f160a1e9232e2f3906ec13497ba20ac#6c4e5ee25f160a1e9232e2f3906ec13497ba20ac" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index 9c54144aa..72d2b6632 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,8 +27,8 @@ glam = { version = "0.33", features = ["bytemuck"] } rfd = "0.17" clap = { version = "4", features = ["derive"] } env_logger = "0.11" -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "45789a7700bd5c006feb4c7ff16c2ff80a7047db", features = ["serde"] } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "0d48aa453f8689e4d737201b0a79604974427c6f", features = ["acis", "offset"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "c11f34334a6bd210674b56b1d9e91991b6943553", features = ["serde"] } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "6c4e5ee25f160a1e9232e2f3906ec13497ba20ac", features = ["acis", "offset"] } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } @@ -60,8 +60,6 @@ ashpd = { version = "0.13.13", default-features = false, features = ["async-io", iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } iced_widget = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } -[patch."https://github.com/HakanSeven12/cadcodec.git"] -acadrust = { git = "https://git@github.com/HakanSeven12/cadcodec.git", rev = "45789a7700bd5c006feb4c7ff16c2ff80a7047db" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] ocs_plugin_api = { path = "crates/ocs_plugin_api", features = ["host"] } diff --git a/crates/ocs_plugin_api/Cargo.toml b/crates/ocs_plugin_api/Cargo.toml index 3d0f7b422..32736612b 100644 --- a/crates/ocs_plugin_api/Cargo.toml +++ b/crates/ocs_plugin_api/Cargo.toml @@ -14,7 +14,7 @@ serde = { version = "1", features = ["derive"] } # Pulled in only by the `host` feature, which adds the `acadrust`-typed # `HostApi` runtime surface. The default crate stays dependency-free so engine # crates and external tooling can depend on the manifest/ribbon contract cheaply. -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "5b2ae66", optional = true, features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "c11f34334a6bd210674b56b1d9e91991b6943553", optional = true, features = ["serde"] } # Runtime IPC and serialization (host feature only). interprocess = { version = "2", optional = true } @@ -37,7 +37,7 @@ serde_json = "1" serde = { version = "1", features = ["derive"] } cargo-lock = "11" # acadrust is scanned at build time to generate the embedded type registry. -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "5b2ae66", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "c11f34334a6bd210674b56b1d9e91991b6943553", features = ["serde"] } [dev-dependencies] serde_json = "1" diff --git a/crates/ocs_web_worker/Cargo.toml b/crates/ocs_web_worker/Cargo.toml index 202b51643..0346e09bd 100644 --- a/crates/ocs_web_worker/Cargo.toml +++ b/crates/ocs_web_worker/Cargo.toml @@ -8,7 +8,7 @@ publish = false crate-type = ["cdylib"] [dependencies] -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "5b2ae66", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "c11f34334a6bd210674b56b1d9e91991b6943553", features = ["serde"] } bincode = "1.3" serde = { version = "1", features = ["derive"] } console_error_panic_hook = "0.1" From 8917300973f3b5d1eb90739e39265ce45756a41a Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 31 Aug 2026 12:21:14 +0300 Subject: [PATCH 4/7] Complete CONE elliptical command flow --- src/modules/model/primitive_cmd.rs | 119 ++++++++++++++++++++++++++--- 1 file changed, 108 insertions(+), 11 deletions(-) diff --git a/src/modules/model/primitive_cmd.rs b/src/modules/model/primitive_cmd.rs index a3bf6f544..60ee81fce 100644 --- a/src/modules/model/primitive_cmd.rs +++ b/src/modules/model/primitive_cmd.rs @@ -47,6 +47,9 @@ enum ConeStep { TwoPointFirst, TwoPointSecond(DVec3), EllipseFirst, + EllipseCenter, + EllipseCenterFirstAxis(DVec3), + EllipseCenterSecondAxis(DVec3, DVec3), EllipseSecond(DVec3), EllipseThird(DVec3, DVec3), TtrFirst, @@ -58,6 +61,7 @@ enum ConeStep { second_hit: DVec3, }, Height, + HeightAfterTopRadius, HeightFirstPoint, HeightSecondPoint(DVec3), AxisEndpoint, @@ -412,7 +416,16 @@ impl PrimitiveCommand { ConeStep::ThreePointThird(_, _) => t!("CONE Specify third point on base:").into_owned(), ConeStep::TwoPointFirst => t!("CONE Specify first endpoint of base diameter:").into_owned(), ConeStep::TwoPointSecond(_) => t!("CONE Specify second endpoint of base diameter:").into_owned(), - ConeStep::EllipseFirst => t!("CONE Specify first endpoint of ellipse axis:").into_owned(), + ConeStep::EllipseFirst => + t!("CONE Specify endpoint of first axis or [Center]:").into_owned(), + ConeStep::EllipseCenter => t!("CONE Specify center point:").into_owned(), + ConeStep::EllipseCenterFirstAxis(_) => crate::tf!( + "CONE Specify distance to first axis <{:.4}>:", + self.cone_defaults.base_x_radius + ) + .into_owned(), + ConeStep::EllipseCenterSecondAxis(_, _) => + t!("CONE Specify endpoint of second axis:").into_owned(), ConeStep::EllipseSecond(_) => t!("CONE Specify second endpoint of ellipse axis:").into_owned(), ConeStep::EllipseThird(_, _) => t!("CONE Specify distance to other ellipse axis:").into_owned(), ConeStep::TtrFirst => t!("CONE Select first tangent object:").into_owned(), @@ -425,6 +438,11 @@ impl PrimitiveCommand { "CONE Specify height or [2Point/Axis endpoint/Top radius] <{:.4}>:", self.cone_defaults.height ).into_owned(), + ConeStep::HeightAfterTopRadius => crate::tf!( + "CONE Specify height or [2Point/Axis endpoint] <{:.4}>:", + self.cone_defaults.height + ) + .into_owned(), ConeStep::HeightFirstPoint => t!("CONE Specify first point for height:").into_owned(), ConeStep::HeightSecondPoint(_) => t!("CONE Specify second point for height:").into_owned(), ConeStep::AxisEndpoint => t!("CONE Specify axis endpoint:").into_owned(), @@ -444,11 +462,16 @@ impl PrimitiveCommand { CmdOption::new(t!("Elliptical").as_ref(), "E"), ], ConeStep::BaseRadius => vec![CmdOption::new(t!("Diameter").as_ref(), "D")], + ConeStep::EllipseFirst => vec![CmdOption::new(t!("Center").as_ref(), "C")], ConeStep::Height => vec![ CmdOption::new("2Point", "2P"), CmdOption::new(t!("Axis endpoint").as_ref(), "A"), CmdOption::new(t!("Top radius").as_ref(), "T"), ], + ConeStep::HeightAfterTopRadius => vec![ + CmdOption::new("2Point", "2P"), + CmdOption::new(t!("Axis endpoint").as_ref(), "A"), + ], _ => Vec::new(), } } @@ -504,6 +527,26 @@ impl PrimitiveCommand { self.cone_step = ConeStep::EllipseSecond(point); CmdResult::NeedPoint } + ConeStep::EllipseCenter => { + self.cone_step = ConeStep::EllipseCenterFirstAxis(point); + CmdResult::NeedPoint + } + ConeStep::EllipseCenterFirstAxis(center) => { + if point.distance_squared(center) > 1e-12 { + self.cone_step = ConeStep::EllipseCenterSecondAxis(center, point); + } + CmdResult::NeedPoint + } + ConeStep::EllipseCenterSecondAxis(center, first_axis_end) => { + let axis = first_axis_end - center; + if let Some(x) = axis.try_normalize() { + let y = self.plane.z.cross(x).normalize_or_zero(); + let frame = WorkingPlane::new(center, x, y); + let local = frame.to_local(point); + self.set_cone_base(frame, axis.length(), local.y.abs()); + } + CmdResult::NeedPoint + } ConeStep::EllipseSecond(first) => { if point.distance_squared(first) > 1e-12 { self.cone_step = ConeStep::EllipseThird(first, point); @@ -533,7 +576,7 @@ impl PrimitiveCommand { } CmdResult::NeedPoint } - ConeStep::Height => self + ConeStep::Height | ConeStep::HeightAfterTopRadius => self .cone_height_at(point) .map(|height| self.commit_cone_height(height)) .unwrap_or(CmdResult::NeedPoint), @@ -556,7 +599,7 @@ impl PrimitiveCommand { let radius = local.x.hypot(local.y); if radius.is_finite() { self.cone_top_radius = radius; - self.cone_step = ConeStep::Height; + self.cone_step = ConeStep::HeightAfterTopRadius; } CmdResult::NeedPoint } @@ -605,6 +648,10 @@ impl PrimitiveCommand { self.cone_step = ConeStep::BaseDiameter; return Some(CmdResult::NeedPoint); } + ConeStep::EllipseFirst if matches!(upper.as_str(), "C" | "CENTER") => { + self.cone_step = ConeStep::EllipseCenter; + return Some(CmdResult::NeedPoint); + } ConeStep::Height => { self.cone_step = match upper.as_str() { "2P" | "2POINT" => ConeStep::HeightFirstPoint, @@ -617,6 +664,17 @@ impl PrimitiveCommand { }; return Some(CmdResult::NeedPoint); } + ConeStep::HeightAfterTopRadius => { + self.cone_step = match upper.as_str() { + "2P" | "2POINT" => ConeStep::HeightFirstPoint, + "A" | "AXIS" | "AXIS ENDPOINT" => ConeStep::AxisEndpoint, + _ => { + let height = crate::entities::common::parse_typed_length(token)?; + return Some(self.commit_cone_height(height)); + } + }; + return Some(CmdResult::NeedPoint); + } _ => {} } let number = crate::entities::common::parse_typed_length(token)?; @@ -635,6 +693,13 @@ impl PrimitiveCommand { CmdResult::NeedPoint }) } + ConeStep::EllipseCenterFirstAxis(center) => (number > 0.0).then(|| { + self.cone_step = ConeStep::EllipseCenterSecondAxis( + center, + center + self.plane.x * number, + ); + CmdResult::NeedPoint + }), ConeStep::TtrRadius { .. } => { let center = self.cone_ttr_center(number)?; (number > 0.0).then(|| { @@ -644,7 +709,7 @@ impl PrimitiveCommand { } ConeStep::TopRadius if number >= 0.0 => { self.cone_top_radius = number; - self.cone_step = ConeStep::Height; + self.cone_step = ConeStep::HeightAfterTopRadius; Some(CmdResult::NeedPoint) } _ => None, @@ -682,13 +747,21 @@ impl PrimitiveCommand { let local = frame.to_local(point); Some(self.cone_base_preview(frame, axis.length() * 0.5, local.y.abs())) } + ConeStep::EllipseCenterSecondAxis(center, first_axis_end) => { + let axis = first_axis_end - center; + let x = axis.try_normalize()?; + let y = self.plane.z.cross(x).normalize_or_zero(); + let frame = WorkingPlane::new(center, x, y); + let local = frame.to_local(point); + Some(self.cone_base_preview(frame, axis.length(), local.y.abs())) + } ConeStep::TtrRadius { second_hit, .. } => { let local = self.plane.to_local(point); let radius = (local - second_hit).truncate().length(); let center = self.cone_ttr_center(radius)?; Some(self.cone_base_preview(self.cone_frame_at(center), radius, radius)) } - ConeStep::Height => { + ConeStep::Height | ConeStep::HeightAfterTopRadius => { let frame = self.cone_frame?; self.cone_preview(frame.z * self.cone_height_at(point)?) } @@ -1067,7 +1140,8 @@ impl CadCommand for PrimitiveCommand { fn cursor_axis(&self) -> Option<(DVec3, DVec3)> { if self.shape == Shape::Cone { - return matches!(self.cone_step, ConeStep::Height).then(|| { + return matches!(self.cone_step, ConeStep::Height | ConeStep::HeightAfterTopRadius) + .then(|| { let frame = self.cone_frame.unwrap_or(self.plane); (frame.origin, frame.z.normalize_or_zero()) }); @@ -1252,10 +1326,19 @@ impl CadCommand for PrimitiveCommand { self.set_cone_base(self.cone_frame_at(center), radius, radius); CmdResult::NeedPoint } - ConeStep::Height => self.commit_cone_height(self.cone_defaults.height), + ConeStep::EllipseCenterFirstAxis(center) => { + self.cone_step = ConeStep::EllipseCenterSecondAxis( + center, + center + self.plane.x * self.cone_defaults.base_x_radius, + ); + CmdResult::NeedPoint + } + ConeStep::Height | ConeStep::HeightAfterTopRadius => { + self.commit_cone_height(self.cone_defaults.height) + } ConeStep::TopRadius => { self.cone_top_radius = self.cone_defaults.top_radius; - self.cone_step = ConeStep::Height; + self.cone_step = ConeStep::HeightAfterTopRadius; CmdResult::NeedPoint } _ => CmdResult::Cancel, @@ -1295,8 +1378,11 @@ impl CadCommand for PrimitiveCommand { ConeStep::BaseCenter | ConeStep::BaseRadius | ConeStep::BaseDiameter + | ConeStep::EllipseFirst + | ConeStep::EllipseCenterFirstAxis(_) | ConeStep::TtrRadius { .. } | ConeStep::Height + | ConeStep::HeightAfterTopRadius | ConeStep::TopRadius ); } @@ -1319,7 +1405,11 @@ impl CadCommand for PrimitiveCommand { if self.shape == Shape::Cone { return matches!( self.cone_step, - ConeStep::BaseCenter | ConeStep::BaseRadius | ConeStep::Height + ConeStep::BaseCenter + | ConeStep::BaseRadius + | ConeStep::EllipseFirst + | ConeStep::Height + | ConeStep::HeightAfterTopRadius ); } self.shape == Shape::Box @@ -1477,8 +1567,12 @@ impl CadCommand for PrimitiveCommand { ConeStep::BaseRadius | ConeStep::TtrRadius { .. } | ConeStep::TopRadius => { DynRole::Radius } + ConeStep::EllipseCenterFirstAxis(_) => DynRole::Distance, ConeStep::BaseDiameter => DynRole::Diameter, - ConeStep::Height | ConeStep::HeightSecondPoint(_) | ConeStep::AxisEndpoint => { + ConeStep::Height + | ConeStep::HeightAfterTopRadius + | ConeStep::HeightSecondPoint(_) + | ConeStep::AxisEndpoint => { DynRole::Height } _ => return None, @@ -1532,7 +1626,10 @@ impl CadCommand for PrimitiveCommand { let local = self.plane.to_local(cursor); Some((local - second_hit).truncate().length()) } - ConeStep::Height => self.cone_height_at(cursor).map(f64::abs), + ConeStep::EllipseCenterFirstAxis(center) => Some(center.distance(cursor)), + ConeStep::Height | ConeStep::HeightAfterTopRadius => { + self.cone_height_at(cursor).map(f64::abs) + } ConeStep::HeightSecondPoint(first) => Some(first.distance(cursor)), ConeStep::AxisEndpoint => self.cone_frame.map(|frame| frame.origin.distance(cursor)), _ => None, From d450376771d6c01231e066044bca8c3816c9f498 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 31 Aug 2026 12:21:15 +0300 Subject: [PATCH 5/7] Align elliptical CONE properties --- src/app/update/command.rs | 11 +- src/scene/model/solid_history.rs | 178 ++++++++++++++++++++----------- 2 files changed, 116 insertions(+), 73 deletions(-) diff --git a/src/app/update/command.rs b/src/app/update/command.rs index a73fac21f..ce1b6256d 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -1940,16 +1940,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } self.push_undo_snapshot(i, "CHPROP"); - if crate::scene::model::solid_history::is_primitive_choice(field) { - for &handle in &handles { - if self.tabs[i].scene.is_layer_locked(handle) { - continue; - } - self.tabs[i] - .scene - .apply_solid_history_property(handle, field, &value); - } - } else if crate::scene::model::solid_history::is_history_choice(field) { + if crate::scene::model::solid_history::is_history_choice(field) { for &handle in &handles { if self.tabs[i].scene.is_layer_locked(handle) { continue; diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index dc00c16ea..1f5ab634d 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -33,6 +33,10 @@ pub const PROP_HEIGHT: &str = "solid_history_height"; pub const PROP_RADIUS: &str = "solid_history_radius"; pub const PROP_BASE_RADIUS: &str = "solid_history_base_radius"; pub const PROP_TOP_RADIUS: &str = "solid_history_top_radius"; +pub const PROP_BASE_MAJOR_RADIUS: &str = "solid_history_base_major_radius"; +pub const PROP_BASE_MINOR_RADIUS: &str = "solid_history_base_minor_radius"; +pub const PROP_TOP_MAJOR_RADIUS: &str = "solid_history_top_major_radius"; +pub const PROP_TOP_MINOR_RADIUS: &str = "solid_history_top_minor_radius"; pub const PROP_ELLIPTICAL: &str = "solid_history_elliptical"; pub const PROP_OUTER_RADIUS: &str = "solid_history_outer_radius"; pub const PROP_INNER_RADIUS: &str = "solid_history_inner_radius"; @@ -111,44 +115,94 @@ fn cone_properties( .max(value.base_y_radius.abs()) .max(1.0); let elliptical = (value.base_x_radius - value.base_y_radius).abs() > 1e-9 * scale; + let rotation = matrix(value.base.transform) + .map(|matrix| matrix.x_axis.y.atan2(matrix.x_axis.x)) + .unwrap_or(0.0); + let top_minor_radius = if value.base_x_radius.abs() > 1e-9 { + value.top_radius * value.base_y_radius / value.base_x_radius + } else { + 0.0 + }; let (record_history, show_history) = history_flags(document, handle).unwrap_or((false, false)); let show_value = if show_history { "Yes" } else { "No" }; + let mut geometry = vec![ + Property { + label: t!("Solid type").into_owned(), + field: "solid_history_type", + value: PropValue::ReadOnly(t!("Cone").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!("Elliptical").into_owned(), + field: PROP_ELLIPTICAL, + value: PropValue::ReadOnly(if elliptical { "Yes" } else { "No" }.to_string()), + }, + ]; + if elliptical { + geometry.extend([ + history_prop( + t!("Base major radius").as_ref(), + PROP_BASE_MAJOR_RADIUS, + value.base_x_radius, + ), + history_prop( + t!("Base minor radius").as_ref(), + PROP_BASE_MINOR_RADIUS, + value.base_y_radius, + ), + history_prop( + t!("Top major radius").as_ref(), + PROP_TOP_MAJOR_RADIUS, + value.top_radius, + ), + history_prop( + t!("Top minor radius").as_ref(), + PROP_TOP_MINOR_RADIUS, + top_minor_radius, + ), + Property { + label: t!("Rotation").into_owned(), + field: PROP_ROTATION, + value: PropValue::ReadOnly(crate::entities::common::format_direction(rotation)), + }, + ]); + } else { + geometry.extend([ + history_prop( + t!("Base radius").as_ref(), + PROP_BASE_RADIUS, + value.base_x_radius, + ), + history_prop( + t!("Top radius").as_ref(), + PROP_TOP_RADIUS, + value.top_radius, + ), + ]); + } + geometry.push(history_prop( + t!("Height").as_ref(), + PROP_HEIGHT, + value.height, + )); vec![ PropSection { title: t!("Geometry").into_owned(), - props: vec![ - Property { - label: t!("Solid type").into_owned(), - field: "solid_history_type", - value: PropValue::ReadOnly(t!("Cone").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!("Elliptical").into_owned(), - field: PROP_ELLIPTICAL, - value: PropValue::Choice { - selected: if elliptical { "Yes" } else { "No" }.to_string(), - options: vec!["No".to_string(), "Yes".to_string()], - }, - }, - history_prop(t!("Base radius").as_ref(), PROP_BASE_RADIUS, value.base_x_radius), - history_prop(t!("Top radius").as_ref(), PROP_TOP_RADIUS, value.top_radius), - history_prop(t!("Height").as_ref(), PROP_HEIGHT, value.height), - ], + props: geometry, }, PropSection { title: t!("Solid History").into_owned(), @@ -328,6 +382,10 @@ pub fn is_primitive_property(field: &str) -> bool { | PROP_RADIUS | PROP_BASE_RADIUS | PROP_TOP_RADIUS + | PROP_BASE_MAJOR_RADIUS + | PROP_BASE_MINOR_RADIUS + | PROP_TOP_MAJOR_RADIUS + | PROP_TOP_MINOR_RADIUS | PROP_OUTER_RADIUS | PROP_INNER_RADIUS | PROP_SIDES @@ -338,10 +396,6 @@ pub fn is_primitive_property(field: &str) -> bool { ) } -pub fn is_primitive_choice(field: &str) -> bool { - field == PROP_ELLIPTICAL -} - pub fn is_history_choice(field: &str) -> bool { matches!(field, PROP_HISTORY | PROP_SHOW_HISTORY) } @@ -497,31 +551,6 @@ pub fn apply_primitive_property( if let Some(applied) = apply_cone_position_property(cone_value, field, value) { return applied; } - if field == PROP_ELLIPTICAL { - let elliptical = if value.eq_ignore_ascii_case("Yes") { - true - } else if value.eq_ignore_ascii_case("No") { - false - } else { - return false; - }; - let scale = cone_value - .base_x_radius - .abs() - .max(cone_value.base_y_radius.abs()) - .max(1.0); - let currently_elliptical = - (cone_value.base_x_radius - cone_value.base_y_radius).abs() > 1e-9 * scale; - if elliptical == currently_elliptical { - return false; - } - cone_value.base_y_radius = if elliptical { - cone_value.base_x_radius * 0.5 - } else { - cone_value.base_x_radius - }; - return true; - } } let Some(number) = (if field == PROP_SIDES { value.trim().parse::().ok() @@ -600,12 +629,30 @@ pub fn apply_primitive_property( value.base_x_radius = radius; value.base_y_radius = radius * ratio; } + PROP_BASE_MAJOR_RADIUS => { + value.base_x_radius = positive().unwrap_or(value.base_x_radius); + } + PROP_BASE_MINOR_RADIUS => { + value.base_y_radius = positive().unwrap_or(value.base_y_radius); + } PROP_TOP_RADIUS => { if number < 0.0 { return false; } value.top_radius = number; } + PROP_TOP_MAJOR_RADIUS => { + if number < 0.0 { + return false; + } + value.top_radius = number; + } + PROP_TOP_MINOR_RADIUS => { + if number < 0.0 || value.base_y_radius.abs() <= 1e-9 { + return false; + } + value.top_radius = number * value.base_x_radius / value.base_y_radius; + } PROP_HEIGHT => value.height = positive().unwrap_or(value.height), _ => return false, }, @@ -657,7 +704,12 @@ pub fn apply_primitive_property( }, _ => return false, } - positive().is_some() || field == PROP_SIDES || field == PROP_TOP_RADIUS + positive().is_some() + || field == PROP_SIDES + || matches!( + field, + PROP_TOP_RADIUS | PROP_TOP_MAJOR_RADIUS | PROP_TOP_MINOR_RADIUS + ) } fn matrix(transform: [f64; 16]) -> Option { From 4ba0ea88081f5b6309623a617f41ef0ce8ce74a3 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:33:10 +0300 Subject: [PATCH 6/7] Fix cone silhouettes and history properties --- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- crates/ocs_plugin_api/Cargo.toml | 4 ++-- crates/ocs_web_worker/Cargo.toml | 2 +- src/scene/model/solid_history.rs | 40 ++++++++++++++++++++++++++------ src/scene/modify.rs | 13 ----------- 6 files changed, 40 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bed02b08e..5e24b314c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,7 +72,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.4.1" -source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=c11f34334a6bd210674b56b1d9e91991b6943553#c11f34334a6bd210674b56b1d9e91991b6943553" +source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=c4f8fb49477aa76445e429cf61b29b30ed44471c#c4f8fb49477aa76445e429cf61b29b30ed44471c" dependencies = [ "ahash 0.8.12", "anyhow", @@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=6c4e5ee25f160a1e9232e2f3906ec13497ba20ac#6c4e5ee25f160a1e9232e2f3906ec13497ba20ac" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=d44c869406b2c12944c0507201347cc2d7fd0004#d44c869406b2c12944c0507201347cc2d7fd0004" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index 72d2b6632..c09680e2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,8 +27,8 @@ glam = { version = "0.33", features = ["bytemuck"] } rfd = "0.17" clap = { version = "4", features = ["derive"] } env_logger = "0.11" -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "c11f34334a6bd210674b56b1d9e91991b6943553", features = ["serde"] } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "6c4e5ee25f160a1e9232e2f3906ec13497ba20ac", features = ["acis", "offset"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "c4f8fb49477aa76445e429cf61b29b30ed44471c", features = ["serde"] } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "d44c869406b2c12944c0507201347cc2d7fd0004", 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/crates/ocs_plugin_api/Cargo.toml b/crates/ocs_plugin_api/Cargo.toml index 32736612b..a1ba9c62e 100644 --- a/crates/ocs_plugin_api/Cargo.toml +++ b/crates/ocs_plugin_api/Cargo.toml @@ -14,7 +14,7 @@ serde = { version = "1", features = ["derive"] } # Pulled in only by the `host` feature, which adds the `acadrust`-typed # `HostApi` runtime surface. The default crate stays dependency-free so engine # crates and external tooling can depend on the manifest/ribbon contract cheaply. -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "c11f34334a6bd210674b56b1d9e91991b6943553", optional = true, features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "c4f8fb49477aa76445e429cf61b29b30ed44471c", optional = true, features = ["serde"] } # Runtime IPC and serialization (host feature only). interprocess = { version = "2", optional = true } @@ -37,7 +37,7 @@ serde_json = "1" serde = { version = "1", features = ["derive"] } cargo-lock = "11" # acadrust is scanned at build time to generate the embedded type registry. -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "c11f34334a6bd210674b56b1d9e91991b6943553", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "c4f8fb49477aa76445e429cf61b29b30ed44471c", features = ["serde"] } [dev-dependencies] serde_json = "1" diff --git a/crates/ocs_web_worker/Cargo.toml b/crates/ocs_web_worker/Cargo.toml index 0346e09bd..675e1dd35 100644 --- a/crates/ocs_web_worker/Cargo.toml +++ b/crates/ocs_web_worker/Cargo.toml @@ -8,7 +8,7 @@ publish = false crate-type = ["cdylib"] [dependencies] -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "c11f34334a6bd210674b56b1d9e91991b6943553", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "c4f8fb49477aa76445e429cf61b29b30ed44471c", features = ["serde"] } bincode = "1.3" serde = { version = "1", features = ["derive"] } console_error_panic_hook = "0.1" diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index 1f5ab634d..4a8321a7e 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -59,7 +59,7 @@ fn history_prop(label: &str, field: &'static str, value: impl ToString) -> Prope fn history_flags( document: &acadrust::CadDocument, handle: acadrust::Handle, -) -> Option<(bool, bool)> { +) -> Option<(bool, bool, i16)> { let graph = document.solid_history_graph(handle)?; let ObjectType::DynamicBlock(object) = document.objects.get(&graph.root)? else { return None; @@ -67,7 +67,26 @@ fn history_flags( let DynamicBlockData::SolidHistory(history) = &object.data else { return None; }; - Some((history.record_history, history.show_history)) + Some(( + history.record_history, + history.show_history, + document.header.show_solid_history.clamp(0, 2), + )) +} + +fn displayed_history_state( + record_history: bool, + object_show_history: bool, + show_history_mode: i16, +) -> (bool, bool) { + if !record_history { + return (false, false); + } + match show_history_mode { + 0 => (false, false), + 2 => (true, false), + _ => (object_show_history, true), + } } pub fn is_box_primitive( @@ -123,7 +142,10 @@ fn cone_properties( } else { 0.0 }; - let (record_history, show_history) = history_flags(document, handle).unwrap_or((false, false)); + 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(record_history, object_show_history, show_history_mode); let show_value = if show_history { "Yes" } else { "No" }; let mut geometry = vec![ Property { @@ -218,7 +240,7 @@ fn cone_properties( Property { label: t!("Show History").into_owned(), field: PROP_SHOW_HISTORY, - value: if record_history { + value: if show_history_editable { PropValue::Choice { selected: show_value.to_string(), options: vec!["No".to_string(), "Yes".to_string()], @@ -246,7 +268,10 @@ fn box_properties( let rotation = matrix(value.base.transform) .map(|matrix| matrix.x_axis.y.atan2(matrix.x_axis.x)) .unwrap_or(0.0); - let (record_history, show_history) = history_flags(document, handle).unwrap_or((false, false)); + 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(record_history, object_show_history, show_history_mode); let show_value = if show_history { "Yes" } else { "No" }; vec![ PropSection { @@ -310,7 +335,7 @@ fn box_properties( Property { label: t!("Show History").into_owned(), field: PROP_SHOW_HISTORY, - value: if record_history { + value: if show_history_editable { PropValue::Choice { selected: show_value.to_string(), options: vec!["No".to_string(), "Yes".to_string()], @@ -406,6 +431,7 @@ pub fn apply_history_choice( field: &str, value: &str, ) -> bool { + let show_history_mode = document.header.show_solid_history.clamp(0, 2); let Some(graph) = document.solid_history_graph(handle) else { return false; }; @@ -429,7 +455,7 @@ pub fn apply_history_choice( history.show_history = false; } } - PROP_SHOW_HISTORY if history.record_history => { + PROP_SHOW_HISTORY if history.record_history && show_history_mode == 1 => { history.show_history = if value.eq_ignore_ascii_case("Yes") { true } else if value.eq_ignore_ascii_case("No") { diff --git a/src/scene/modify.rs b/src/scene/modify.rs index 917d695b2..e3cc388d4 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -791,11 +791,6 @@ impl Scene { handle: Handle, operation: acadrust::objects::SolidHistoryOperation, ) -> bool { - let user_facing_primitive = matches!( - operation, - acadrust::objects::SolidHistoryOperation::Box(_) - | acadrust::objects::SolidHistoryOperation::Cone(_) - ); let Some(graph) = self.document.create_solid_history(handle, operation) else { return false; }; @@ -803,14 +798,6 @@ impl Scene { for node in graph.nodes { self.record_undo_object_before(node, None); } - if user_facing_primitive { - let _ = crate::scene::model::solid_history::apply_history_choice( - &mut self.document, - handle, - crate::scene::model::solid_history::PROP_HISTORY, - "None", - ); - } self.sync_solid_reference_point(handle); true } From 013703b5bad071b1a7fb0c351f21d4c66a68e4e3 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:49:46 +0300 Subject: [PATCH 7/7] Keep show history independently editable --- src/scene/model/solid_history.rs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index 4a8321a7e..c2c3812e6 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -75,13 +75,9 @@ fn history_flags( } fn displayed_history_state( - record_history: bool, object_show_history: bool, show_history_mode: i16, ) -> (bool, bool) { - if !record_history { - return (false, false); - } match show_history_mode { 0 => (false, false), 2 => (true, false), @@ -145,7 +141,7 @@ fn cone_properties( 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(record_history, object_show_history, show_history_mode); + displayed_history_state(object_show_history, show_history_mode); let show_value = if show_history { "Yes" } else { "No" }; let mut geometry = vec![ Property { @@ -271,7 +267,7 @@ fn box_properties( 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(record_history, object_show_history, show_history_mode); + displayed_history_state(object_show_history, show_history_mode); let show_value = if show_history { "Yes" } else { "No" }; vec![ PropSection { @@ -451,11 +447,8 @@ pub fn apply_history_choice( } else { return false; }; - if !history.record_history { - history.show_history = false; - } } - PROP_SHOW_HISTORY if history.record_history && show_history_mode == 1 => { + PROP_SHOW_HISTORY if show_history_mode == 1 => { history.show_history = if value.eq_ignore_ascii_case("Yes") { true } else if value.eq_ignore_ascii_case("No") {