diff --git a/Cargo.lock b/Cargo.lock index a1983e88b..f85069b6b 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://git@github.com/HakanSeven12/cadcodec.git?rev=3a87743ad7b0fc74f0137ccd48f598116e2904b0#3a87743ad7b0fc74f0137ccd48f598116e2904b0" 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=48c634995fa60d3928c37bae49b12b421c56c886#48c634995fa60d3928c37bae49b12b421c56c886" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index 9c54144aa..74f7792a8 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 = "3a87743ad7b0fc74f0137ccd48f598116e2904b0", features = ["serde"] } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "48c634995fa60d3928c37bae49b12b421c56c886", features = ["acis", "offset"] } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } @@ -61,7 +61,8 @@ iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aa 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" } +acadrust = { git = "https://git@github.com/HakanSeven12/cadcodec.git", rev = "3a87743ad7b0fc74f0137ccd48f598116e2904b0" } + [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..bd69e4060 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 = "3a87743ad7b0fc74f0137ccd48f598116e2904b0", 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 = "3a87743ad7b0fc74f0137ccd48f598116e2904b0", 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..bef08417a 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 = "3a87743ad7b0fc74f0137ccd48f598116e2904b0", features = ["serde"] } bincode = "1.3" serde = { version = "1", features = ["derive"] } console_error_panic_hook = "0.1" 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/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 df54a3400..cd7d95537 100644 --- a/src/modules/model/primitive_cmd.rs +++ b/src/modules/model/primitive_cmd.rs @@ -67,6 +67,57 @@ enum SphereStep { }, } +#[derive(Clone, Copy)] +enum ConeStep { + BaseCenter, + BaseRadius, + BaseDiameter, + ThreePointFirst, + ThreePointSecond(DVec3), + ThreePointThird(DVec3, DVec3), + TwoPointFirst, + TwoPointSecond(DVec3), + EllipseFirst, + EllipseCenter, + EllipseCenterFirstAxis(DVec3), + EllipseCenterSecondAxis(DVec3, DVec3), + EllipseSecond(DVec3), + EllipseThird(DVec3, DVec3), + TtrFirst, + TtrSecond { object: TangentObject, hit: DVec3 }, + TtrRadius { + first: TangentObject, + second: TangentObject, + first_hit: DVec3, + second_hit: DVec3, + }, + Height, + HeightAfterTopRadius, + 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 sphere_radius_store() -> &'static Mutex { static VALUE: OnceLock> = OnceLock::new(); VALUE.get_or_init(|| Mutex::new(1.0)) @@ -154,6 +205,12 @@ fn sphere_through_three_points( )) } +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 { @@ -208,11 +265,21 @@ pub struct PrimitiveCommand { box_height_anchor: Option, sphere_step: SphereStep, sphere_default_radius: f64, + 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(), @@ -227,6 +294,12 @@ impl PrimitiveCommand { box_height_anchor: None, sphere_step: SphereStep::Center, sphere_default_radius: sphere_radius_default(), + 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(), } } @@ -271,6 +344,616 @@ impl PrimitiveCommand { ))) } + 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 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(), + 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::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(), + 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::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(), + } + } + + 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 => { + let Some(frame) = self.cone_frame else { + return CmdResult::NeedPoint; + }; + let local = frame.to_local(point); + let radius = local.x.hypot(local.y); + self.set_cone_base(frame, radius, radius); + CmdResult::NeedPoint + } + 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) * 0.5; + self.set_cone_base(frame, radius, radius); + 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::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); + } + 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 | ConeStep::HeightAfterTopRadius => 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::HeightAfterTopRadius; + } + 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::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, + "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); + } + 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)?; + 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::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(|| { + 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::HeightAfterTopRadius; + Some(CmdResult::NeedPoint) + } + _ => None, + } + } + + fn cone_mouse_move(&self, point: DVec3) -> Option { + match self.cone_step { + ConeStep::BaseRadius => { + 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::BaseDiameter => { + let frame = self.cone_frame?; + let local = frame.to_local(point); + let radius = local.x.hypot(local.y) * 0.5; + 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::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::HeightAfterTopRadius => { + 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 { @@ -565,33 +1248,19 @@ impl PrimitiveCommand { let (solid, history) = match self.shape { Shape::Box => return self.build_box(height), Shape::Wedge => return self.build_wedge(height), - 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(); @@ -689,6 +1358,13 @@ impl CadCommand for PrimitiveCommand { } fn cursor_axis(&self) -> Option<(DVec3, DVec3)> { + if self.shape == Shape::Cone { + 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()) + }); + } let constrained = if self.shape.rectangular() { self.box_step == BoxStep::Height } else { @@ -707,14 +1383,22 @@ impl CadCommand for PrimitiveCommand { } fn needs_tangent_pick(&self) -> bool { - self.shape == Shape::Sphere + (self.shape == Shape::Sphere && matches!( self.sphere_step, SphereStep::TtrFirst | SphereStep::TtrSecond { .. } - ) + )) + || (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 { + return self.on_cone_tangent(object, hit); + } if self.shape != Shape::Sphere { return self.on_point(hit); } @@ -743,6 +1427,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.rectangular() { return match self.box_step { BoxStep::FirstCorner => t!("%{n} Specify first corner or [Center]:", n = n), @@ -841,6 +1528,9 @@ impl CadCommand for PrimitiveCommand { _ => Vec::new(), }; } + if self.shape == Shape::Cone { + return self.cone_options(); + } if !self.shape.rectangular() { return Vec::new(); } @@ -899,6 +1589,9 @@ impl CadCommand for PrimitiveCommand { SphereStep::TtrFirst | SphereStep::TtrSecond { .. } => CmdResult::NeedPoint, }; } + if self.shape == Shape::Cone { + return self.on_cone_point(pt); + } if self.shape.rectangular() { let local = self.plane.to_local(pt); if !local.is_finite() { @@ -1003,6 +1696,45 @@ impl CadCommand for PrimitiveCommand { _ => CmdResult::Cancel, }; } + 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::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::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()) @@ -1031,6 +1763,20 @@ impl CadCommand for PrimitiveCommand { | SphereStep::TtrRadius { .. } ); } + if self.shape == Shape::Cone { + return matches!( + self.cone_step, + ConeStep::BaseCenter + | ConeStep::BaseRadius + | ConeStep::BaseDiameter + | ConeStep::EllipseFirst + | ConeStep::EllipseCenterFirstAxis(_) + | ConeStep::TtrRadius { .. } + | ConeStep::Height + | ConeStep::HeightAfterTopRadius + | ConeStep::TopRadius + ); + } if self.shape.rectangular() { matches!( self.box_step, @@ -1050,6 +1796,16 @@ impl CadCommand for PrimitiveCommand { if self.shape == Shape::Sphere { return matches!(self.sphere_step, SphereStep::Center | SphereStep::Radius(_)); } + if self.shape == Shape::Cone { + return matches!( + self.cone_step, + ConeStep::BaseCenter + | ConeStep::BaseRadius + | ConeStep::EllipseFirst + | ConeStep::Height + | ConeStep::HeightAfterTopRadius + ); + } self.shape.rectangular() && matches!( self.box_step, @@ -1096,6 +1852,9 @@ impl CadCommand for PrimitiveCommand { _ => None, }; } + if self.shape == Shape::Cone { + return self.on_cone_text(raw); + } if self.shape.rectangular() { let token = raw.trim(); let upper = token.to_uppercase(); @@ -1181,6 +1940,9 @@ impl CadCommand for PrimitiveCommand { _ => None, }; } + if self.shape == Shape::Cone { + return self.cone_mouse_move(pt); + } if self.pts.is_empty() { return None; } @@ -1305,6 +2067,35 @@ impl CadCommand for PrimitiveCommand { }); } + if self.shape == Shape::Cone { + let frame = self.cone_frame.unwrap_or(self.plane); + let (anchor, role) = match self.cone_step { + ConeStep::BaseRadius | ConeStep::TopRadius => { + (frame.origin, DynRole::Radius) + } + ConeStep::BaseDiameter => (frame.origin, DynRole::Diameter), + ConeStep::TtrRadius { second_hit, .. } => { + (self.plane.to_world(second_hit), DynRole::Radius) + } + ConeStep::EllipseCenterFirstAxis(center) => (center, DynRole::Distance), + ConeStep::Height | ConeStep::HeightAfterTopRadius | ConeStep::AxisEndpoint => { + (frame.origin, DynRole::Height) + } + ConeStep::HeightSecondPoint(first) => (first, DynRole::Height), + _ => return None, + }; + return Some(DynSpec { + anchor: DynAnchor::Point(anchor), + 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.rectangular() { match self.box_step { BoxStep::CubeSize | BoxStep::Length => DynRole::Distance, @@ -1337,6 +2128,31 @@ impl CadCommand for PrimitiveCommand { _ => None, }; } + 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)) + } + ConeStep::TtrRadius { second_hit, .. } => { + let local = self.plane.to_local(cursor); + Some((local - second_hit).truncate().length()) + } + 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, + }; + } if self.shape.rectangular() { let local = self.plane.to_local(cursor); if !local.is_finite() { @@ -1489,6 +2305,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/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 57fe02b0b..08d74a299 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, }; @@ -32,6 +32,13 @@ 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_DIAMETER: &str = "solid_history_diameter"; +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"; pub const PROP_SIDES: &str = "solid_history_sides"; @@ -53,7 +60,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; @@ -61,7 +68,22 @@ 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( + object_show_history: bool, + show_history_mode: i16, +) -> (bool, bool) { + match show_history_mode { + 0 => (false, false), + 2 => (true, false), + _ => (object_show_history, true), + } } pub fn is_rectangular_primitive( @@ -84,6 +106,7 @@ pub fn has_specialized_primitive_properties( SolidHistoryOperation::Box(_) | SolidHistoryOperation::Wedge(_) | SolidHistoryOperation::Sphere(_) + | SolidHistoryOperation::Cone(_) ) ) } @@ -97,20 +120,157 @@ pub fn reference_point(operation: &SolidHistoryOperation) -> Option SolidHistoryOperation::Sphere(value) => { world_point(value.base.transform, [0.0, 0.0, 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 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, 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" }; + 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: geometry, + }, + 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, value: &SolidHistorySphere, ) -> Vec { - let Some(position) = world_point(value.base.transform, [0.0, 0.0, 0.0]) else { + let Some(position) = world_point(value.base.transform, [0.0; 3]) else { return Vec::new(); }; - let (record_history, show_history) = history_flags(document, handle).unwrap_or((false, false)); - let history_value = if record_history { "Record" } else { "None" }; + 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 { @@ -154,12 +314,22 @@ fn sphere_properties( Property { label: t!("History").into_owned(), field: PROP_HISTORY, - value: PropValue::ReadOnly(history_value.to_string()), + 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: PropValue::ReadOnly(show_value.to_string()), + 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()) + }, }, ], }, @@ -181,7 +351,10 @@ fn rectangular_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(object_show_history, show_history_mode); let show_value = if show_history { "Yes" } else { "No" }; vec![ PropSection { @@ -245,7 +418,7 @@ fn rectangular_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()], @@ -276,7 +449,8 @@ pub fn primitive_properties( SolidHistoryOperation::Sphere(value) => { return sphere_properties(document, handle, value) } - SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => vec![ + SolidHistoryOperation::Cone(value) => return cone_properties(document, handle, value), + 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), ], @@ -313,6 +487,12 @@ pub fn is_primitive_property(field: &str) -> bool { | PROP_HEIGHT | PROP_RADIUS | PROP_DIAMETER + | 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 @@ -333,6 +513,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; }; @@ -352,11 +533,8 @@ pub fn apply_history_choice( } else { return false; }; - if !history.record_history { - history.show_history = false; - } } - PROP_SHOW_HISTORY if history.record_history => { + 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") { @@ -468,6 +646,36 @@ fn apply_sphere_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]).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, @@ -487,6 +695,11 @@ 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; + } + } let Some(number) = (if field == PROP_SIDES { value.trim().parse::().ok() } else { @@ -533,7 +746,7 @@ pub fn apply_primitive_property( return true; } match operation { - SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => match field { + SolidHistoryOperation::Cylinder(value) => match field { PROP_RADIUS => { let Some(radius) = positive() else { return false; @@ -545,6 +758,46 @@ 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_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, + }, SolidHistoryOperation::Sphere(value) => { let Some(next) = positive() else { return false; @@ -604,7 +857,12 @@ pub fn apply_primitive_property( }, _ => return false, } - positive().is_some() || field == PROP_SIDES + 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 { @@ -803,7 +1061,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, @@ -819,6 +1077,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, @@ -952,7 +1226,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); @@ -964,6 +1238,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); } @@ -1059,17 +1347,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/model/solid_model.rs b/src/scene/model/solid_model.rs index b9d7f5dbf..8ed8faa05 100644 --- a/src/scene/model/solid_model.rs +++ b/src/scene/model/solid_model.rs @@ -40,9 +40,21 @@ pub fn cylinder_solid(center: [f64; 3], radius: f64, height: f64) -> Option 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`. @@ -452,7 +464,7 @@ mod tests { assert!(tri_count(&box_solid(c, 10.0, 10.0, 10.0).unwrap()) >= 12, "box"); assert!(tri_count(&wedge_solid(c, 10.0, 10.0, 10.0).unwrap()) >= 6, "wedge"); assert!(tri_count(&cylinder_solid(c, 5.0, 12.0).unwrap()) > 20, "cylinder"); - assert!(tri_count(&cone_solid(c, 5.0, 12.0).unwrap()) > 10, "cone"); + assert!(tri_count(&cone_frustum_solid(c, 5.0, 5.0, 0.0, 12.0).unwrap()) > 10, "cone"); assert!(tri_count(&sphere_solid(c, 5.0).unwrap()) > 50, "sphere"); assert!(tri_count(&torus_solid(c, 8.0, 2.0).unwrap()) > 50, "torus"); assert!(tri_count(&pyramid_solid(c, 5.0, 9.0, 6).unwrap()) >= 8, "pyramid"); @@ -469,7 +481,10 @@ mod tests { let cases: [(Body, f64); 5] = [ (box_solid(c, 10.0, 4.0, 6.0).unwrap(), 240.0), (cylinder_solid(c, 5.0, 12.0).unwrap(), PI * 25.0 * 12.0), - (cone_solid(c, 5.0, 12.0).unwrap(), PI * 25.0 * 12.0 / 3.0), + ( + cone_frustum_solid(c, 5.0, 5.0, 0.0, 12.0).unwrap(), + PI * 25.0 * 12.0 / 3.0, + ), (sphere_solid(c, 5.0).unwrap(), 4.0 / 3.0 * PI * 125.0), (torus_solid(c, 8.0, 2.0).unwrap(), 2.0 * PI * PI * 8.0 * 4.0), ]; diff --git a/src/scene/modify.rs b/src/scene/modify.rs index 505bba18e..e3cc388d4 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -802,7 +802,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)