From e4cfd4c1420abdf22c58a66fabc902331cfad33d Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 31 Aug 2026 14:51:02 +0300 Subject: [PATCH 1/2] Complete cylinder workflow and history properties --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/app/commands/draw.rs | 8 +- src/app/properties.rs | 12 +- src/modules/draw/draw/circle.rs | 6 +- src/modules/model/cylinder_cmd.rs | 796 ++++++++++++++++++++++++++++++ src/modules/model/mod.rs | 1 + src/scene/model/solid_history.rs | 281 ++++++++++- src/scene/model/solid_model.rs | 10 + 9 files changed, 1100 insertions(+), 18 deletions(-) create mode 100644 src/modules/model/cylinder_cmd.rs diff --git a/Cargo.lock b/Cargo.lock index a1983e88b..8eea0cf28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=0d48aa453f8689e4d737201b0a79604974427c6f#0d48aa453f8689e4d737201b0a79604974427c6f" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=d37fb1bee9f0fb7432dd9f08ed1a1aeeb56ba40b#d37fb1bee9f0fb7432dd9f08ed1a1aeeb56ba40b" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index 9c54144aa..3015c83fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ rfd = "0.17" clap = { version = "4", features = ["derive"] } env_logger = "0.11" acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "45789a7700bd5c006feb4c7ff16c2ff80a7047db", features = ["serde"] } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "0d48aa453f8689e4d737201b0a79604974427c6f", features = ["acis", "offset"] } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "d37fb1bee9f0fb7432dd9f08ed1a1aeeb56ba40b", features = ["acis", "offset"] } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index 5bd714fe4..5b0c84965 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -957,7 +957,13 @@ impl OpenCADStudio { } // ── Model commands (3D primitives) ───────────────────────────── - "BOX" | "WEDGE" | "CYLINDER" | "CONE" | "SPHERE" | "PYRAMID" | "PYR" + "CYLINDER" => { + use crate::modules::model::cylinder_cmd::CylinderCommand; + let new_cmd = CylinderCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + "BOX" | "WEDGE" | "CONE" | "SPHERE" | "PYRAMID" | "PYR" | "TORUS" => { use crate::modules::model::primitive_cmd::PrimitiveCommand; let new_cmd = PrimitiveCommand::new(cmd); diff --git a/src/app/properties.rs b/src/app/properties.rs index cf4a5d093..19e2dbe7f 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, @@ -2386,7 +2386,7 @@ impl OpenCADStudio { annotation_scale_handle, ); let mut entity_grips = dispatch::grips(contextual.as_ref()); - if crate::scene::model::solid_history::is_box_primitive( + if crate::scene::model::solid_history::has_specialized_primitive_properties( &self.tabs[i].scene.document, handle, ) { 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/cylinder_cmd.rs b/src/modules/model/cylinder_cmd.rs new file mode 100644 index 000000000..e97abf837 --- /dev/null +++ b/src/modules/model/cylinder_cmd.rs @@ -0,0 +1,796 @@ +use acadrust::entities::Solid3D; +use acadrust::objects::SolidHistoryOperation; +use acadrust::EntityType; +use cadkernel::brep::Body; +use glam::DVec3; + +use crate::command::{ + CadCommand, CmdOption, CmdResult, DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec, + TangentObject, WorkingPlane, +}; +use crate::scene::model::solid_model; +use crate::scene::model::wire_model::WireModel; +use crate::t; + +#[derive(Clone, Copy)] +enum Step { + 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, + HeightFirstPoint, + HeightSecondPoint(DVec3), + AxisEndpoint, +} + +#[derive(Clone, Copy)] +struct Defaults { + major_radius: f64, + minor_radius: f64, + height: f64, +} + +impl Default for Defaults { + fn default() -> Self { + Self { + major_radius: 1.0, + minor_radius: 1.0, + height: 1.0, + } + } +} + +fn defaults() -> &'static std::sync::Mutex { + static DEFAULTS: std::sync::OnceLock> = + std::sync::OnceLock::new(); + DEFAULTS.get_or_init(|| std::sync::Mutex::new(Defaults::default())) +} + +pub struct CylinderCommand { + step: Step, + frame: Option, + major_radius: f64, + minor_radius: f64, + remembered: Defaults, + plane: WorkingPlane, +} + +impl CylinderCommand { + pub fn new() -> Self { + let remembered = defaults() + .lock() + .map(|value| *value) + .unwrap_or_default(); + Self { + step: Step::BaseCenter, + frame: None, + major_radius: remembered.major_radius, + minor_radius: remembered.minor_radius, + remembered, + plane: WorkingPlane::default(), + } + } + + fn frame_at(&self, center: DVec3) -> WorkingPlane { + WorkingPlane { + origin: center, + x: self.plane.x, + y: self.plane.y, + z: self.plane.z, + } + } + + fn set_base(&mut self, mut frame: WorkingPlane, mut x_radius: f64, mut y_radius: f64) -> bool { + if !frame.origin.is_finite() + || !frame.x.is_finite() + || !frame.y.is_finite() + || !frame.z.is_finite() + || !x_radius.is_finite() + || !y_radius.is_finite() + || x_radius < 1e-6 + || y_radius < 1e-6 + { + return false; + } + if y_radius > x_radius { + std::mem::swap(&mut x_radius, &mut y_radius); + frame = WorkingPlane { + origin: frame.origin, + x: frame.y, + y: -frame.x, + z: frame.z, + }; + } + self.frame = Some(frame); + self.major_radius = x_radius; + self.minor_radius = y_radius; + self.step = Step::Height; + true + } + + fn 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 axis_frame(&self, axis: DVec3) -> Option<(WorkingPlane, f64)> { + let current = self.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 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(&self, axis: DVec3) -> Option<(Body, SolidHistoryOperation, f64)> { + let (frame, height) = self.axis_frame(axis)?; + let local = solid_model::elliptical_cylinder_solid( + [0.0; 3], + self.major_radius, + self.minor_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::elliptical_cylinder_op( + Self::history_transform(frame), + self.major_radius, + self.minor_radius, + height, + ), + height, + )) + } + + fn commit_axis(&self, axis: DVec3) -> CmdResult { + let Some((solid, history, height)) = self.build(axis) else { + return CmdResult::NeedPoint; + }; + if let Ok(mut remembered) = defaults().lock() { + *remembered = Defaults { + major_radius: self.major_radius, + minor_radius: self.minor_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_height(&self, height: f64) -> CmdResult { + let Some(frame) = self.frame else { + return CmdResult::NeedPoint; + }; + if !height.is_finite() || height.abs() < 1e-6 { + return CmdResult::NeedPoint; + } + self.commit_axis(frame.z * height) + } + + fn height_at(&self, point: DVec3) -> Option { + let frame = self.frame?; + Some((point - frame.origin).dot(frame.z)) + } + + fn ttr_center(&self, radius: f64) -> Option { + let Step::TtrRadius { + first, + second, + first_hit, + second_hit, + } = self.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 base_preview(&self, frame: WorkingPlane, x_radius: f64, y_radius: f64) -> WireModel { + let mut points = Vec::new(); + push_ellipse( + &mut points, + frame.origin, + frame.x, + frame.y, + x_radius.max(1e-9), + y_radius.max(1e-9), + ); + wire("cylinder_base_preview", points) + } + + fn solid_preview(&self, axis: DVec3) -> Option { + let (frame, height) = self.axis_frame(axis)?; + let top = frame.origin + frame.z * height; + let mut points = Vec::new(); + push_ellipse( + &mut points, + frame.origin, + frame.x, + frame.y, + self.major_radius, + self.minor_radius, + ); + push_ellipse( + &mut points, + top, + frame.x, + frame.y, + self.major_radius, + self.minor_radius, + ); + for index in 0..4 { + let angle = index as f64 * std::f64::consts::FRAC_PI_2; + let base = frame.origin + + frame.x * (self.major_radius * angle.cos()) + + frame.y * (self.minor_radius * angle.sin()); + push_segment(&mut points, base, base + frame.z * height); + } + Some(wire("cylinder_height_preview", points)) + } + + fn on_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.step { + Step::TtrFirst => self.step = Step::TtrSecond { object, hit }, + Step::TtrSecond { + object: first, + hit: first_hit, + } => { + self.step = Step::TtrRadius { + first, + second: object, + first_hit, + second_hit: hit, + }; + } + _ => {} + } + CmdResult::NeedPoint + } + + fn on_text(&mut self, raw: &str) -> Option { + let token = raw.trim(); + let upper = token.to_uppercase(); + match self.step { + Step::BaseCenter => { + self.step = match upper.as_str() { + "3P" => Step::ThreePointFirst, + "2P" => Step::TwoPointFirst, + "T" | "TTR" => Step::TtrFirst, + "E" | "ELLIPTICAL" => Step::EllipseFirst, + _ => return None, + }; + return Some(CmdResult::NeedPoint); + } + Step::BaseRadius if matches!(upper.as_str(), "D" | "DIAMETER") => { + self.step = Step::BaseDiameter; + return Some(CmdResult::NeedPoint); + } + Step::EllipseFirst if matches!(upper.as_str(), "C" | "CENTER") => { + self.step = Step::EllipseCenter; + return Some(CmdResult::NeedPoint); + } + Step::Height => { + self.step = match upper.as_str() { + "2P" | "2POINT" => Step::HeightFirstPoint, + "A" | "AXIS" | "AXIS ENDPOINT" => Step::AxisEndpoint, + _ => { + let height = crate::entities::common::parse_typed_length(token)?; + return Some(self.commit_height(height)); + } + }; + return Some(CmdResult::NeedPoint); + } + _ => {} + } + let number = crate::entities::common::parse_typed_length(token)?; + match self.step { + Step::BaseRadius => { + let frame = self.frame?; + (number > 0.0).then(|| { + self.set_base(frame, number, number); + CmdResult::NeedPoint + }) + } + Step::BaseDiameter => { + let frame = self.frame?; + (number > 0.0).then(|| { + self.set_base(frame, number * 0.5, number * 0.5); + CmdResult::NeedPoint + }) + } + Step::EllipseCenterFirstAxis(center) => (number > 0.0).then(|| { + self.step = Step::EllipseCenterSecondAxis( + center, + center + self.plane.x * number, + ); + CmdResult::NeedPoint + }), + Step::TtrRadius { .. } => { + let center = self.ttr_center(number)?; + (number > 0.0).then(|| { + self.set_base(self.frame_at(center), number, number); + CmdResult::NeedPoint + }) + } + _ => None, + } + } + + fn mouse_preview(&self, point: DVec3) -> Option { + match self.step { + Step::BaseRadius | Step::BaseDiameter => { + let frame = self.frame?; + let local = frame.to_local(point); + let radius = local.x.hypot(local.y); + Some(self.base_preview(frame, radius, radius)) + } + Step::ThreePointSecond(first) | Step::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.base_preview(self.frame_at(center), radius, radius)) + } + Step::ThreePointThird(first, second) => { + let (frame, radius) = Self::three_point_frame(first, second, point)?; + Some(self.base_preview(frame, radius, radius)) + } + Step::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.base_preview(frame, axis.length() * 0.5, local.y.abs())) + } + Step::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.base_preview(frame, axis.length(), local.y.abs())) + } + Step::TtrRadius { second_hit, .. } => { + let local = self.plane.to_local(point); + let radius = (local - second_hit).truncate().length(); + let center = self.ttr_center(radius)?; + Some(self.base_preview(self.frame_at(center), radius, radius)) + } + Step::Height => { + let frame = self.frame?; + self.solid_preview(frame.z * self.height_at(point)?) + } + Step::HeightSecondPoint(first) => { + let frame = self.frame?; + self.solid_preview(frame.z * first.distance(point)) + } + Step::AxisEndpoint => { + let frame = self.frame?; + self.solid_preview(point - frame.origin) + } + _ => None, + } + } +} + +impl CadCommand for CylinderCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + + fn cursor_axis(&self) -> Option<(DVec3, DVec3)> { + matches!(self.step, Step::Height).then(|| { + let frame = self.frame.unwrap_or(self.plane); + (frame.origin, frame.z.normalize_or_zero()) + }) + } + + fn name(&self) -> &'static str { + "CYLINDER" + } + + fn prompt(&self) -> String { + match self.step { + Step::BaseCenter => + t!("CYLINDER Specify center point of base or [3P/2P/Ttr/Elliptical]:").into_owned(), + Step::BaseRadius => crate::tf!( + "CYLINDER Specify base radius or [Diameter] <{:.4}>:", + self.remembered.major_radius + ).into_owned(), + Step::BaseDiameter => crate::tf!( + "CYLINDER Specify base diameter <{:.4}>:", + self.remembered.major_radius * 2.0 + ).into_owned(), + Step::ThreePointFirst => t!("CYLINDER Specify first point on base:").into_owned(), + Step::ThreePointSecond(_) => t!("CYLINDER Specify second point on base:").into_owned(), + Step::ThreePointThird(_, _) => t!("CYLINDER Specify third point on base:").into_owned(), + Step::TwoPointFirst => t!("CYLINDER Specify first endpoint of base diameter:").into_owned(), + Step::TwoPointSecond(_) => t!("CYLINDER Specify second endpoint of base diameter:").into_owned(), + Step::EllipseFirst => + t!("CYLINDER Specify endpoint of first axis or [Center]:").into_owned(), + Step::EllipseCenter => t!("CYLINDER Specify center point:").into_owned(), + Step::EllipseCenterFirstAxis(_) => crate::tf!( + "CYLINDER Specify distance to first axis <{:.4}>:", + self.remembered.major_radius + ).into_owned(), + Step::EllipseCenterSecondAxis(_, _) => + t!("CYLINDER Specify endpoint of second axis:").into_owned(), + Step::EllipseSecond(_) => + t!("CYLINDER Specify other endpoint of first axis:").into_owned(), + Step::EllipseThird(_, _) => + t!("CYLINDER Specify endpoint of second axis:").into_owned(), + Step::TtrFirst => t!("CYLINDER Select first tangent object:").into_owned(), + Step::TtrSecond { .. } => t!("CYLINDER Select second tangent object:").into_owned(), + Step::TtrRadius { .. } => crate::tf!( + "CYLINDER Specify base radius <{:.4}>:", + self.remembered.major_radius + ).into_owned(), + Step::Height => crate::tf!( + "CYLINDER Specify height or [2Point/Axis endpoint] <{:.4}>:", + self.remembered.height + ).into_owned(), + Step::HeightFirstPoint => + t!("CYLINDER Specify first point for height:").into_owned(), + Step::HeightSecondPoint(_) => + t!("CYLINDER Specify second point for height:").into_owned(), + Step::AxisEndpoint => t!("CYLINDER Specify axis endpoint:").into_owned(), + } + } + + fn options(&self) -> Vec { + match self.step { + Step::BaseCenter => vec![ + CmdOption::new("3P", "3P"), + CmdOption::new("2P", "2P"), + CmdOption::new("Ttr", "TTR"), + CmdOption::new(t!("Elliptical").as_ref(), "E"), + ], + Step::BaseRadius => vec![CmdOption::new(t!("Diameter").as_ref(), "D")], + Step::EllipseFirst => vec![CmdOption::new(t!("Center").as_ref(), "C")], + Step::Height => vec![ + CmdOption::new("2Point", "2P"), + CmdOption::new(t!("Axis endpoint").as_ref(), "A"), + ], + _ => Vec::new(), + } + } + + fn on_point(&mut self, point: DVec3) -> CmdResult { + match self.step { + Step::BaseCenter => { + self.frame = Some(self.frame_at(point)); + self.step = Step::BaseRadius; + CmdResult::NeedPoint + } + Step::BaseRadius | Step::BaseDiameter => { + let Some(frame) = self.frame else { + return CmdResult::NeedPoint; + }; + let local = frame.to_local(point); + let radius = local.x.hypot(local.y); + self.set_base(frame, radius, radius); + CmdResult::NeedPoint + } + Step::ThreePointFirst => { + self.step = Step::ThreePointSecond(point); + CmdResult::NeedPoint + } + Step::ThreePointSecond(first) => { + if point.distance_squared(first) > 1e-12 { + self.step = Step::ThreePointThird(first, point); + } + CmdResult::NeedPoint + } + Step::ThreePointThird(first, second) => { + if let Some((frame, radius)) = Self::three_point_frame(first, second, point) { + self.set_base(frame, radius, radius); + } + CmdResult::NeedPoint + } + Step::TwoPointFirst => { + self.step = Step::TwoPointSecond(point); + CmdResult::NeedPoint + } + Step::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_base(self.frame_at(center), radius, radius); + CmdResult::NeedPoint + } + Step::EllipseFirst => { + self.step = Step::EllipseSecond(point); + CmdResult::NeedPoint + } + Step::EllipseCenter => { + self.step = Step::EllipseCenterFirstAxis(point); + CmdResult::NeedPoint + } + Step::EllipseCenterFirstAxis(center) => { + if point.distance_squared(center) > 1e-12 { + self.step = Step::EllipseCenterSecondAxis(center, point); + } + CmdResult::NeedPoint + } + Step::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_base(frame, axis.length(), local.y.abs()); + } + CmdResult::NeedPoint + } + Step::EllipseSecond(first) => { + if point.distance_squared(first) > 1e-12 { + self.step = Step::EllipseThird(first, point); + } + CmdResult::NeedPoint + } + Step::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_base(frame, x_radius, local.y.abs()); + } + CmdResult::NeedPoint + } + Step::TtrRadius { second_hit, .. } => { + let local = self.plane.to_local(point); + let radius = (local - second_hit).truncate().length(); + if let Some(center) = self.ttr_center(radius) { + self.set_base(self.frame_at(center), radius, radius); + } + CmdResult::NeedPoint + } + Step::Height => self + .height_at(point) + .map(|height| self.commit_height(height)) + .unwrap_or(CmdResult::NeedPoint), + Step::HeightFirstPoint => { + self.step = Step::HeightSecondPoint(point); + CmdResult::NeedPoint + } + Step::HeightSecondPoint(first) => self.commit_height(first.distance(point)), + Step::AxisEndpoint => { + let Some(frame) = self.frame else { + return CmdResult::NeedPoint; + }; + self.commit_axis(point - frame.origin) + } + Step::TtrFirst | Step::TtrSecond { .. } => CmdResult::NeedPoint, + } + } + + fn on_enter(&mut self) -> CmdResult { + match self.step { + Step::BaseRadius | Step::BaseDiameter => { + let Some(frame) = self.frame else { + return CmdResult::Cancel; + }; + self.set_base( + frame, + self.remembered.major_radius, + self.remembered.major_radius, + ); + CmdResult::NeedPoint + } + Step::TtrRadius { .. } => { + let radius = self.remembered.major_radius; + let Some(center) = self.ttr_center(radius) else { + return CmdResult::NeedPoint; + }; + self.set_base(self.frame_at(center), radius, radius); + CmdResult::NeedPoint + } + Step::EllipseCenterFirstAxis(center) => { + self.step = Step::EllipseCenterSecondAxis( + center, + center + self.plane.x * self.remembered.major_radius, + ); + CmdResult::NeedPoint + } + Step::Height => self.commit_height(self.remembered.height), + _ => CmdResult::Cancel, + } + } + + fn needs_tangent_pick(&self) -> bool { + matches!(self.step, Step::TtrFirst | Step::TtrSecond { .. }) + } + + fn on_tangent_point(&mut self, object: TangentObject, hit: DVec3) -> CmdResult { + self.on_tangent(object, hit) + } + + fn wants_text_input(&self) -> bool { + matches!( + self.step, + Step::BaseCenter + | Step::BaseRadius + | Step::BaseDiameter + | Step::EllipseFirst + | Step::EllipseCenterFirstAxis(_) + | Step::TtrRadius { .. } + | Step::Height + ) + } + + fn point_step_accepts_keywords(&self) -> bool { + matches!( + self.step, + Step::BaseCenter | Step::BaseRadius | Step::EllipseFirst | Step::Height + ) + } + + fn on_text_input(&mut self, raw: &str) -> Option { + self.on_text(raw) + } + + fn on_mouse_move(&mut self, point: DVec3) -> Option { + self.mouse_preview(point) + } + + fn dyn_spec(&self) -> Option { + let frame = self.frame.unwrap_or(self.plane); + let role = match self.step { + Step::BaseRadius | Step::TtrRadius { .. } => DynRole::Radius, + Step::BaseDiameter => DynRole::Diameter, + Step::EllipseCenterFirstAxis(_) => DynRole::Distance, + Step::Height | Step::HeightSecondPoint(_) | Step::AxisEndpoint => DynRole::Height, + _ => return None, + }; + 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, + }) + } + + fn dyn_live_value(&self, cursor: DVec3) -> Option { + match self.step { + Step::BaseRadius => { + let local = self.frame?.to_local(cursor); + Some(local.x.hypot(local.y)) + } + Step::BaseDiameter => { + let local = self.frame?.to_local(cursor); + Some(local.x.hypot(local.y) * 2.0) + } + Step::TtrRadius { second_hit, .. } => { + let local = self.plane.to_local(cursor); + Some((local - second_hit).truncate().length()) + } + Step::EllipseCenterFirstAxis(center) => Some(center.distance(cursor)), + Step::Height => self.height_at(cursor).map(f64::abs), + Step::HeightSecondPoint(first) => Some(first.distance(cursor)), + Step::AxisEndpoint => self.frame.map(|frame| frame.origin.distance(cursor)), + _ => None, + } + } +} + +fn push_break(points: &mut Vec<[f32; 3]>) { + if !points.is_empty() { + points.push([f32::NAN; 3]); + } +} + +fn push_segment(points: &mut Vec<[f32; 3]>, first: DVec3, second: DVec3) { + push_break(points); + points.extend([first.as_vec3().to_array(), second.as_vec3().to_array()]); +} + +fn push_ellipse( + 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 wire(name: &str, points: Vec<[f32; 3]>) -> WireModel { + WireModel::solid(name.to_string(), points, WireModel::CYAN, false) +} diff --git a/src/modules/model/mod.rs b/src/modules/model/mod.rs index 5272161aa..b3dfb8c26 100644 --- a/src/modules/model/mod.rs +++ b/src/modules/model/mod.rs @@ -1,6 +1,7 @@ // Solid creation and kernel modelling tools. pub mod boolean_cmd; +pub mod cylinder_cmd; pub mod edge_cmd; pub mod primitive_cmd; diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index a7c05f7be..65cc5000a 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -19,6 +19,8 @@ pub const GRIP_RADIUS: usize = 10_004; pub const GRIP_OUTER_RADIUS: usize = 10_005; pub const GRIP_INNER_RADIUS: usize = 10_006; pub const GRIP_SIDES: usize = 10_007; +pub const GRIP_MAJOR_RADIUS: usize = 10_008; +pub const GRIP_MINOR_RADIUS: usize = 10_009; pub const GRIP_BOX_CORNER_FIRST: usize = 10_100; pub const GRIP_BOX_FACE_X_MIN: usize = 10_110; pub const GRIP_BOX_FACE_X_MAX: usize = 10_111; @@ -31,6 +33,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_MAJOR_RADIUS: &str = "solid_history_major_radius"; +pub const PROP_MINOR_RADIUS: &str = "solid_history_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"; @@ -63,13 +68,13 @@ fn history_flags( Some((history.record_history, history.show_history)) } -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(_)) + Some(SolidHistoryOperation::Box(_) | SolidHistoryOperation::Cylinder(_)) ) } @@ -79,10 +84,120 @@ pub fn reference_point(operation: &SolidHistoryOperation) -> Option value.base.transform, [value.length * 0.5, value.width * 0.5, 0.0], ), + SolidHistoryOperation::Cylinder(value) => world_point(value.base.transform, [0.0; 3]), _ => None, } } +fn cylinder_properties( + document: &acadrust::CadDocument, + handle: acadrust::Handle, + value: &SolidHistoryCylinder, +) -> Vec { + let Some(position) = world_point(value.base.transform, [0.0; 3]) else { + return Vec::new(); + }; + let scale = value + .major_radius + .abs() + .max(value.minor_radius.abs()) + .max(1.0); + let elliptical = (value.major_radius - value.minor_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 (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!("Cylinder").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!("Major radius").as_ref(), + PROP_MAJOR_RADIUS, + value.major_radius, + ), + history_prop( + t!("Minor radius").as_ref(), + PROP_MINOR_RADIUS, + value.minor_radius, + ), + Property { + label: t!("Rotation").into_owned(), + field: PROP_ROTATION, + value: PropValue::ReadOnly(crate::entities::common::format_direction(rotation)), + }, + ]); + } else { + geometry.push(history_prop( + t!("Radius").as_ref(), + PROP_RADIUS, + value.major_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 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, @@ -189,7 +304,10 @@ pub fn primitive_properties( 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) => { + return cylinder_properties(document, handle, value) + } + SolidHistoryOperation::Cone(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 +348,8 @@ pub fn is_primitive_property(field: &str) -> bool { | PROP_WIDTH | PROP_HEIGHT | PROP_RADIUS + | PROP_MAJOR_RADIUS + | PROP_MINOR_RADIUS | PROP_OUTER_RADIUS | PROP_INNER_RADIUS | PROP_SIDES @@ -355,6 +475,47 @@ fn apply_box_geometry_property( Some(true) } +fn apply_cylinder_position_property( + value: &mut SolidHistoryCylinder, + 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) +} + +fn canonicalize_cylinder_radii(value: &mut SolidHistoryCylinder) -> bool { + if value.minor_radius <= value.major_radius { + value.x_radius = value.major_radius; + return true; + } + let Some(current) = matrix(value.base.transform) else { + return false; + }; + std::mem::swap(&mut value.major_radius, &mut value.minor_radius); + value.x_radius = value.major_radius; + value.base.transform = + (current * glam::DMat4::from_rotation_z(std::f64::consts::FRAC_PI_2)).to_cols_array(); + true +} + pub fn apply_primitive_property( operation: &mut SolidHistoryOperation, field: &str, @@ -365,6 +526,11 @@ pub fn apply_primitive_property( return applied; } } + if let SolidHistoryOperation::Cylinder(cylinder_value) = operation { + if let Some(applied) = apply_cylinder_position_property(cylinder_value, field, value) { + return applied; + } + } let Some(number) = (if field == PROP_SIDES { value.trim().parse::().ok() } else { @@ -417,7 +583,31 @@ 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; + }; + value.major_radius = radius; + value.minor_radius = radius; + value.x_radius = radius; + } + PROP_MAJOR_RADIUS => { + value.major_radius = positive().unwrap_or(value.major_radius); + if !canonicalize_cylinder_radii(value) { + return false; + } + } + PROP_MINOR_RADIUS => { + value.minor_radius = positive().unwrap_or(value.minor_radius); + if !canonicalize_cylinder_radii(value) { + return false; + } + } + PROP_HEIGHT => value.height = positive().unwrap_or(value.height), + _ => return false, + }, + SolidHistoryOperation::Cone(value) => match field { PROP_RADIUS => { let Some(radius) = positive() else { return false; @@ -676,7 +866,45 @@ pub fn primitive_grips( Some([0.0, 0.0, 1.0]), ); } - SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => { + SolidHistoryOperation::Cylinder(value) => { + let scale = value + .major_radius + .abs() + .max(value.minor_radius.abs()) + .max(1.0); + if (value.major_radius - value.minor_radius).abs() > 1e-9 * scale { + add( + GRIP_MAJOR_RADIUS, + value.base.transform, + [value.major_radius, 0.0, value.height * 0.5], + GripShape::Square, + None, + ); + add( + GRIP_MINOR_RADIUS, + value.base.transform, + [0.0, value.minor_radius, value.height * 0.5], + GripShape::Square, + None, + ); + } else { + add( + GRIP_RADIUS, + value.base.transform, + [value.major_radius, 0.0, value.height * 0.5], + 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::Cone(value) => { add( GRIP_RADIUS, value.base.transform, @@ -825,7 +1053,31 @@ 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); + value.major_radius = radius; + value.minor_radius = radius; + value.x_radius = radius; + } + GRIP_MAJOR_RADIUS => { + value.major_radius = local.x.abs().max(1e-6); + if !canonicalize_cylinder_radii(value) { + return false; + } + } + GRIP_MINOR_RADIUS => { + value.minor_radius = local.y.abs().max(1e-6); + if !canonicalize_cylinder_radii(value) { + return false; + } + } + GRIP_HEIGHT => value.height = local.z.max(1e-6), + _ => return false, + } + } + SolidHistoryOperation::Cone(value) => { match grip_id { GRIP_RADIUS => { let radius = local.x.hypot(local.y).max(1e-6); @@ -930,6 +1182,23 @@ pub fn cylinder_op( }) } +pub fn elliptical_cylinder_op( + transform: [f64; 16], + major_radius: f64, + minor_radius: f64, + height: f64, +) -> SolidHistoryOperation { + SolidHistoryOperation::Cylinder(SolidHistoryCylinder { + base: base(transform), + operation_major: 1, + height, + major_radius, + minor_radius, + x_radius: major_radius, + ..SolidHistoryCylinder::default() + }) +} + pub fn cone_op( transform: [f64; 16], radius: f64, diff --git a/src/scene/model/solid_model.rs b/src/scene/model/solid_model.rs index b9d7f5dbf..d577b8e5a 100644 --- a/src/scene/model/solid_model.rs +++ b/src/scene/model/solid_model.rs @@ -40,6 +40,16 @@ pub fn cylinder_solid(center: [f64; 3], radius: f64, height: f64) -> Option Option { + brep::make::elliptical_cylinder(center, major_radius, minor_radius, height) +} + /// Solid cone standing on the z = base plane, apex `height` above it. pub fn cone_solid(center: [f64; 3], radius: f64, height: f64) -> Option { brep::make::cone(center, radius, height) From 70e6138f5470244641cc9ae733bd26a7827beeb8 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:59:16 +0300 Subject: [PATCH 2/2] Honor drawing solid history controls --- Cargo.lock | 2 +- Cargo.toml | 4 ++-- src/scene/model/solid_history.rs | 40 ++++++++++++++++++++++++++------ src/scene/modify.rs | 9 ------- 4 files changed, 36 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8eea0cf28..4d9637441 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=9cda1a94299acdae0ee5f26c17cbc6c2f4115b6f#9cda1a94299acdae0ee5f26c17cbc6c2f4115b6f" dependencies = [ "ahash 0.8.12", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 3015c83fc..2f3afe9d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ 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"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "9cda1a94299acdae0ee5f26c17cbc6c2f4115b6f", features = ["serde"] } cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "d37fb1bee9f0fb7432dd9f08ed1a1aeeb56ba40b", features = ["acis", "offset"] } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" @@ -61,7 +61,7 @@ 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 = "9cda1a94299acdae0ee5f26c17cbc6c2f4115b6f" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] ocs_plugin_api = { path = "crates/ocs_plugin_api", features = ["host"] } diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index 65cc5000a..f68155299 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -57,7 +57,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; @@ -65,7 +65,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 has_specialized_primitive_properties( @@ -106,7 +125,10 @@ fn cylinder_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" }; let mut geometry = vec![ Property { @@ -184,7 +206,7 @@ fn cylinder_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()], @@ -212,7 +234,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 { @@ -276,7 +301,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()], @@ -370,6 +395,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; }; @@ -393,7 +419,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 f376f9307..505bba18e 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -791,7 +791,6 @@ impl Scene { handle: Handle, operation: acadrust::objects::SolidHistoryOperation, ) -> bool { - let box_primitive = matches!(operation, acadrust::objects::SolidHistoryOperation::Box(_)); let Some(graph) = self.document.create_solid_history(handle, operation) else { return false; }; @@ -799,14 +798,6 @@ impl Scene { for node in graph.nodes { self.record_undo_object_before(node, None); } - if box_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 }