From 2148fa93c8840977eb66708840d5875f662caf8c Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 31 Aug 2026 18:01:55 +0300 Subject: [PATCH 1/3] Enhance sphere creation workflow --- src/modules/model/primitive_cmd.rs | 422 ++++++++++++++++++++++++++++- 1 file changed, 421 insertions(+), 1 deletion(-) diff --git a/src/modules/model/primitive_cmd.rs b/src/modules/model/primitive_cmd.rs index cfaa65ca2..0abfbeec7 100644 --- a/src/modules/model/primitive_cmd.rs +++ b/src/modules/model/primitive_cmd.rs @@ -4,9 +4,14 @@ use acadrust::entities::Solid3D; use acadrust::objects::SolidHistoryOperation; use acadrust::EntityType; use cadkernel::brep::Body; +use cadkernel::geom2d::{ + fillets_between, Circle as KernelCircle, Curve as KernelCurve, Line as KernelLine, + Tolerance, +}; use glam::DVec3; +use std::sync::{Mutex, OnceLock}; -use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane}; +use crate::command::{CadCommand, CmdOption, CmdResult, TangentObject, WorkingPlane}; use crate::scene::model::solid_model; use crate::scene::model::wire_model::WireModel; use crate::t; @@ -36,6 +41,116 @@ enum BoxStep { HeightSecondPoint, } +#[derive(Clone, Copy)] +enum SphereStep { + Center, + Radius(DVec3), + Diameter(DVec3), + TwoPointFirst, + TwoPointSecond(DVec3), + ThreePointFirst, + ThreePointSecond(DVec3), + ThreePointThird(DVec3, DVec3), + TtrFirst, + TtrSecond { + object: TangentObject, + hit: DVec3, + }, + TtrRadius { + first: TangentObject, + second: TangentObject, + first_hit: DVec3, + second_hit: DVec3, + }, +} + +fn sphere_radius_store() -> &'static Mutex { + static VALUE: OnceLock> = OnceLock::new(); + VALUE.get_or_init(|| Mutex::new(1.0)) +} + +fn sphere_radius_default() -> f64 { + sphere_radius_store() + .lock() + .map(|value| *value) + .unwrap_or(1.0) +} + +fn remember_sphere_radius(radius: f64) { + if radius.is_finite() && radius > 1e-6 { + if let Ok(mut value) = sphere_radius_store().lock() { + *value = radius; + } + } +} + +fn sphere_tangent_local(object: TangentObject, plane: WorkingPlane) -> TangentObject { + match object { + TangentObject::Line { p1, p2 } => TangentObject::Line { + p1: plane.to_local(p1), + p2: plane.to_local(p2), + }, + TangentObject::Circle { center, radius } => TangentObject::Circle { + center: plane.to_local(center), + radius, + }, + } +} + +fn sphere_tangent_curve(object: TangentObject) -> KernelCurve { + match object { + TangentObject::Line { p1, p2 } => KernelCurve::Line(KernelLine { + start: [p1.x, p1.y], + end: [p2.x, p2.y], + }), + TangentObject::Circle { center, radius } => KernelCurve::Circle(KernelCircle { + centre: [center.x, center.y], + radius, + }), + } +} + +fn sphere_ttr_centers( + first: TangentObject, + second: TangentObject, + radius: f64, +) -> Vec { + fillets_between( + &sphere_tangent_curve(first), + &sphere_tangent_curve(second), + radius, + Tolerance::default(), + ) + .into_iter() + .map(|fillet| DVec3::new(fillet.centre[0], fillet.centre[1], 0.0)) + .collect() +} + +fn closest_sphere_center(candidates: &[DVec3], hint: DVec3) -> Option { + candidates.iter().copied().min_by(|first, second| { + first + .distance(hint) + .partial_cmp(&second.distance(hint)) + .unwrap_or(std::cmp::Ordering::Equal) + }) +} + +fn sphere_through_three_points( + first: DVec3, + second: DVec3, + third: DVec3, +) -> Option<(DVec3, f64)> { + let circle = cadkernel::geom2d::arc_through_points( + [first.x, first.y], + [second.x, second.y], + [third.x, third.y], + )?; + Some(( + DVec3::new(circle.centre[0], circle.centre[1], first.z), + circle.radius, + )) +} + impl Shape { fn from_id(id: &str) -> Option { Some(match id { @@ -85,6 +200,8 @@ pub struct PrimitiveCommand { box_angle: f64, box_width_sign: f64, box_height_anchor: Option, + sphere_step: SphereStep, + sphere_default_radius: f64, plane: WorkingPlane, } @@ -102,10 +219,52 @@ impl PrimitiveCommand { box_angle: 0.0, box_width_sign: 1.0, box_height_anchor: None, + sphere_step: SphereStep::Center, + sphere_default_radius: sphere_radius_default(), plane: WorkingPlane::default(), } } + fn commit_sphere(&mut self, center: DVec3, radius: f64) -> CmdResult { + if !center.is_finite() || !radius.is_finite() || radius <= 1e-6 { + return CmdResult::NeedPoint; + } + self.pts = vec![center, center + DVec3::X * radius]; + self.sphere_default_radius = radius; + remember_sphere_radius(radius); + self.commit(0.0) + } + + fn sphere_ttr_result(&mut self, radius: f64) -> CmdResult { + let SphereStep::TtrRadius { + first, + second, + first_hit, + second_hit, + } = self.sphere_step + else { + return CmdResult::NeedPoint; + }; + let hint = (first_hit + second_hit) * 0.5; + let Some(center) = closest_sphere_center( + &sphere_ttr_centers(first, second, radius), + hint, + ) else { + return CmdResult::NeedPoint; + }; + self.commit_sphere(center, radius) + } + + fn sphere_preview(&self, center: DVec3, radius: f64) -> Option { + if !center.is_finite() || !radius.is_finite() || radius <= 1e-6 { + return None; + } + Some(self.place_preview(footprint_wire( + Shape::Sphere, + &[center, center + DVec3::X * radius], + ))) + } + /// Number of footprint points the shape needs before the height step. fn footprint_pts(&self) -> usize { match self.shape { @@ -499,6 +658,41 @@ impl CadCommand for PrimitiveCommand { self.shape.name() } + fn needs_tangent_pick(&self) -> bool { + self.shape == Shape::Sphere + && matches!( + self.sphere_step, + SphereStep::TtrFirst | SphereStep::TtrSecond { .. } + ) + } + + fn on_tangent_point(&mut self, object: TangentObject, hit: DVec3) -> CmdResult { + if self.shape != Shape::Sphere { + return self.on_point(hit); + } + let object = sphere_tangent_local(object, self.plane); + let hit = self.plane.to_local(hit); + match self.sphere_step { + SphereStep::TtrFirst => { + self.sphere_step = SphereStep::TtrSecond { object, hit }; + CmdResult::NeedPoint + } + SphereStep::TtrSecond { + object: first, + hit: first_hit, + } => { + self.sphere_step = SphereStep::TtrRadius { + first, + second: object, + first_hit, + second_hit: hit, + }; + CmdResult::NeedPoint + } + _ => CmdResult::NeedPoint, + } + } + fn prompt(&self) -> String { let n = self.shape.name(); if self.shape == Shape::Box { @@ -521,6 +715,49 @@ impl CadCommand for PrimitiveCommand { } .into_owned(); } + if self.shape == Shape::Sphere { + return match self.sphere_step { + SphereStep::Center => { + t!("SPHERE Specify center point or [3P/2P/Ttr]:").into_owned() + } + SphereStep::Radius(_) => crate::tf!( + "SPHERE Specify radius or [Diameter] <{:.4}>:", + self.sphere_default_radius + ) + .into_owned(), + SphereStep::Diameter(_) => crate::tf!( + "SPHERE Specify diameter <{:.4}>:", + self.sphere_default_radius * 2.0 + ) + .into_owned(), + SphereStep::TwoPointFirst => { + t!("SPHERE Specify first end point of diameter:").into_owned() + } + SphereStep::TwoPointSecond(_) => { + t!("SPHERE Specify second end point of diameter:").into_owned() + } + SphereStep::ThreePointFirst => { + t!("SPHERE Specify first point on sphere:").into_owned() + } + SphereStep::ThreePointSecond(_) => { + t!("SPHERE Specify second point on sphere:").into_owned() + } + SphereStep::ThreePointThird(_, _) => { + t!("SPHERE Specify third point on sphere:").into_owned() + } + SphereStep::TtrFirst => { + t!("SPHERE Select first tangent object:").into_owned() + } + SphereStep::TtrSecond { .. } => { + t!("SPHERE Select second tangent object:").into_owned() + } + SphereStep::TtrRadius { .. } => crate::tf!( + "SPHERE Specify radius <{:.4}>:", + self.sphere_default_radius + ) + .into_owned(), + }; + } if self.height_step { return t!("%{n} Specify height :", n = n).into_owned(); } @@ -540,6 +777,17 @@ impl CadCommand for PrimitiveCommand { } fn options(&self) -> Vec { + if self.shape == Shape::Sphere { + return match self.sphere_step { + SphereStep::Center => vec![ + CmdOption::new("3P", "3P"), + CmdOption::new("2P", "2P"), + CmdOption::new("Ttr", "T"), + ], + SphereStep::Radius(_) => vec![CmdOption::new(t!("Diameter").as_ref(), "D")], + _ => Vec::new(), + }; + } if self.shape != Shape::Box { return Vec::new(); } @@ -555,6 +803,49 @@ impl CadCommand for PrimitiveCommand { } fn on_point(&mut self, pt: DVec3) -> CmdResult { + if self.shape == Shape::Sphere { + let local = self.plane.to_local(pt); + if !local.is_finite() { + return CmdResult::NeedPoint; + } + return match self.sphere_step { + SphereStep::Center => { + self.sphere_step = SphereStep::Radius(local); + CmdResult::NeedPoint + } + SphereStep::Radius(center) => self.commit_sphere(center, center.distance(local)), + SphereStep::Diameter(center) => { + self.commit_sphere(center, center.distance(local) * 0.5) + } + SphereStep::TwoPointFirst => { + self.sphere_step = SphereStep::TwoPointSecond(local); + CmdResult::NeedPoint + } + SphereStep::TwoPointSecond(first) => { + self.commit_sphere((first + local) * 0.5, first.distance(local) * 0.5) + } + SphereStep::ThreePointFirst => { + self.sphere_step = SphereStep::ThreePointSecond(local); + CmdResult::NeedPoint + } + SphereStep::ThreePointSecond(first) => { + self.sphere_step = SphereStep::ThreePointThird(first, local); + CmdResult::NeedPoint + } + SphereStep::ThreePointThird(first, second) => { + let Some((center, radius)) = + sphere_through_three_points(first, second, local) + else { + return CmdResult::NeedPoint; + }; + self.commit_sphere(center, radius) + } + SphereStep::TtrRadius { second_hit, .. } => { + self.sphere_ttr_result(second_hit.distance(local)) + } + SphereStep::TtrFirst | SphereStep::TtrSecond { .. } => CmdResult::NeedPoint, + }; + } if self.shape == Shape::Box { let local = self.plane.to_local(pt); if !local.is_finite() { @@ -632,6 +923,20 @@ impl CadCommand for PrimitiveCommand { } fn on_enter(&mut self) -> CmdResult { + if self.shape == Shape::Sphere { + return match self.sphere_step { + SphereStep::Radius(center) => { + self.commit_sphere(center, self.sphere_default_radius) + } + SphereStep::Diameter(center) => { + self.commit_sphere(center, self.sphere_default_radius) + } + SphereStep::TtrRadius { .. } => { + self.sphere_ttr_result(self.sphere_default_radius) + } + _ => CmdResult::Cancel, + }; + } if self.shape == Shape::Box { return CmdResult::Cancel; } @@ -647,6 +952,15 @@ impl CadCommand for PrimitiveCommand { } fn wants_text_input(&self) -> bool { + if self.shape == Shape::Sphere { + return matches!( + self.sphere_step, + SphereStep::Center + | SphereStep::Radius(_) + | SphereStep::Diameter(_) + | SphereStep::TtrRadius { .. } + ); + } if self.shape == Shape::Box { matches!( self.box_step, @@ -663,6 +977,9 @@ impl CadCommand for PrimitiveCommand { } fn point_step_accepts_keywords(&self) -> bool { + if self.shape == Shape::Sphere { + return matches!(self.sphere_step, SphereStep::Center | SphereStep::Radius(_)); + } self.shape == Shape::Box && matches!( self.box_step, @@ -671,6 +988,44 @@ impl CadCommand for PrimitiveCommand { } fn on_text_input(&mut self, raw: &str) -> Option { + if self.shape == Shape::Sphere { + let token = raw.trim(); + let upper = token.to_uppercase(); + return match self.sphere_step { + SphereStep::Center if upper == "3P" => { + self.sphere_step = SphereStep::ThreePointFirst; + Some(CmdResult::NeedPoint) + } + SphereStep::Center if upper == "2P" => { + self.sphere_step = SphereStep::TwoPointFirst; + Some(CmdResult::NeedPoint) + } + SphereStep::Center if matches!(upper.as_str(), "T" | "TTR") => { + self.sphere_step = SphereStep::TtrFirst; + Some(CmdResult::NeedPoint) + } + SphereStep::Radius(_) if matches!(upper.as_str(), "D" | "DIAMETER") => { + let SphereStep::Radius(center) = self.sphere_step else { + unreachable!(); + }; + self.sphere_step = SphereStep::Diameter(center); + Some(CmdResult::NeedPoint) + } + SphereStep::Radius(center) => { + let radius = crate::entities::common::parse_typed_length(token)?; + Some(self.commit_sphere(center, radius)) + } + SphereStep::Diameter(center) => { + let diameter = crate::entities::common::parse_typed_length(token)?; + Some(self.commit_sphere(center, diameter * 0.5)) + } + SphereStep::TtrRadius { .. } => { + let radius = crate::entities::common::parse_typed_length(token)?; + Some(self.sphere_ttr_result(radius)) + } + _ => None, + }; + } if self.shape == Shape::Box { let token = raw.trim(); let upper = token.to_uppercase(); @@ -719,6 +1074,43 @@ impl CadCommand for PrimitiveCommand { } fn on_mouse_move(&mut self, pt: DVec3) -> Option { + if self.shape == Shape::Sphere { + let local = self.plane.to_local(pt); + if !local.is_finite() { + return None; + } + return match self.sphere_step { + SphereStep::Radius(center) => self.sphere_preview(center, center.distance(local)), + SphereStep::Diameter(center) => { + self.sphere_preview(center, center.distance(local) * 0.5) + } + SphereStep::TwoPointSecond(first) => { + self.sphere_preview((first + local) * 0.5, first.distance(local) * 0.5) + } + SphereStep::ThreePointSecond(first) => { + self.sphere_preview((first + local) * 0.5, first.distance(local) * 0.5) + } + SphereStep::ThreePointThird(first, second) => { + let (center, radius) = sphere_through_three_points(first, second, local)?; + self.sphere_preview(center, radius) + } + SphereStep::TtrRadius { + first, + second, + first_hit, + second_hit, + } => { + let radius = second_hit.distance(local); + let hint = (first_hit + second_hit) * 0.5; + let center = closest_sphere_center( + &sphere_ttr_centers(first, second, radius), + hint, + )?; + self.sphere_preview(center, radius) + } + _ => None, + }; + } if self.pts.is_empty() { return None; } @@ -806,6 +1198,23 @@ impl CadCommand for PrimitiveCommand { fn dyn_spec(&self) -> Option { use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec}; + if self.shape == Shape::Sphere { + let (anchor, role) = match self.sphere_step { + SphereStep::Radius(center) => (center, DynRole::Radius), + SphereStep::Diameter(center) => (center, DynRole::Diameter), + SphereStep::TtrRadius { second_hit, .. } => { + (second_hit, DynRole::Radius) + } + _ => return None, + }; + return Some(DynSpec { + anchor: DynAnchor::Point(self.plane.to_world(anchor)), + fields: vec![DynFieldSpec::new(role)], + guide: DynGuide::Radius, + ref_point: None, + }); + } + let role = if self.shape == Shape::Box { match self.box_step { BoxStep::CubeSize | BoxStep::Length => DynRole::Distance, @@ -827,6 +1236,17 @@ impl CadCommand for PrimitiveCommand { } fn dyn_live_value(&self, cursor: DVec3) -> Option { + if self.shape == Shape::Sphere { + let local = self.plane.to_local(cursor); + return match self.sphere_step { + SphereStep::Radius(center) => Some(center.distance(local)), + SphereStep::Diameter(center) => Some(center.distance(local)), + SphereStep::TtrRadius { second_hit, .. } => { + Some(second_hit.distance(local)) + } + _ => None, + }; + } if self.shape == Shape::Box { let local = self.plane.to_local(cursor); if !local.is_finite() { From 9cc299d02487e1347b5161c946555ed22d90ae6a Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 31 Aug 2026 18:01:56 +0300 Subject: [PATCH 2/3] Align sphere properties --- src/app/properties.rs | 23 ++++-- src/scene/model/solid_history.rs | 138 +++++++++++++++++++++++++++++-- 2 files changed, 149 insertions(+), 12 deletions(-) diff --git a/src/app/properties.rs b/src/app/properties.rs index cf4a5d093..1eeaa930d 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, @@ -2146,6 +2146,19 @@ impl OpenCADStudio { .map(|(handle, entity)| (*handle, entity)) .collect(); let mut sections = aggregate_sections(&local_refs, &text_style_names); + if local_refs.iter().all(|(handle, _)| { + crate::scene::model::solid_history::has_specialized_primitive_properties( + &self.tabs[i].scene.document, + *handle, + ) + }) { + sections.retain(|section| { + !section.props.iter().any(|property| { + property.field.starts_with("acis_") + || property.field.starts_with("s3d_") + }) + }); + } sections.extend(aggregate_solid_history_sections( &self.tabs[i].scene.document, &local_refs.iter().map(|(handle, _)| *handle).collect::>(), diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index a7c05f7be..49f697ff6 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -31,6 +31,7 @@ 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_DIAMETER: &str = "solid_history_diameter"; pub const PROP_OUTER_RADIUS: &str = "solid_history_outer_radius"; pub const PROP_INNER_RADIUS: &str = "solid_history_inner_radius"; pub const PROP_SIDES: &str = "solid_history_sides"; @@ -73,16 +74,94 @@ pub fn is_box_primitive( ) } +pub fn has_specialized_primitive_properties( + document: &acadrust::CadDocument, + handle: acadrust::Handle, +) -> bool { + matches!( + document.solid_history_operation(handle), + Some(SolidHistoryOperation::Box(_) | SolidHistoryOperation::Sphere(_)) + ) +} + pub fn reference_point(operation: &SolidHistoryOperation) -> Option { match operation { SolidHistoryOperation::Box(value) => world_point( value.base.transform, [value.length * 0.5, value.width * 0.5, 0.0], ), + SolidHistoryOperation::Sphere(value) => { + world_point(value.base.transform, [0.0, 0.0, 0.0]) + } _ => None, } } +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 { + 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 show_value = if show_history { "Yes" } else { "No" }; + vec![ + PropSection { + title: t!("Geometry").into_owned(), + props: vec![ + Property { + label: t!("Solid type").into_owned(), + field: "solid_history_type", + value: PropValue::ReadOnly(t!("Sphere").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, + ), + crate::entities::common::edit_prop( + t!("Radius").as_ref(), + PROP_RADIUS, + value.radius, + ), + crate::entities::common::edit_prop( + t!("Diameter").as_ref(), + PROP_DIAMETER, + value.radius * 2.0, + ), + ], + }, + PropSection { + title: t!("Solid History").into_owned(), + props: vec![ + Property { + label: t!("History").into_owned(), + field: PROP_HISTORY, + value: PropValue::ReadOnly(history_value.to_string()), + }, + Property { + label: t!("Show History").into_owned(), + field: PROP_SHOW_HISTORY, + value: PropValue::ReadOnly(show_value.to_string()), + }, + ], + }, + ] +} + fn box_properties( document: &acadrust::CadDocument, handle: acadrust::Handle, @@ -184,6 +263,9 @@ pub fn primitive_properties( }; let props = match operation { SolidHistoryOperation::Box(value) => return box_properties(document, handle, value), + SolidHistoryOperation::Sphere(value) => { + return sphere_properties(document, handle, value) + } SolidHistoryOperation::Wedge(value) => vec![ history_prop(t!("Length").as_ref(), PROP_LENGTH, value.length), history_prop(t!("Width").as_ref(), PROP_WIDTH, value.width), @@ -193,11 +275,6 @@ pub fn primitive_properties( history_prop(t!("Radius").as_ref(), PROP_RADIUS, value.major_radius), history_prop(t!("Height").as_ref(), PROP_HEIGHT, value.height), ], - SolidHistoryOperation::Sphere(value) => vec![history_prop( - t!("Radius").as_ref(), - PROP_RADIUS, - value.radius, - )], SolidHistoryOperation::Torus(value) => vec![ history_prop( t!("Outer Radius").as_ref(), @@ -230,6 +307,7 @@ pub fn is_primitive_property(field: &str) -> bool { | PROP_WIDTH | PROP_HEIGHT | PROP_RADIUS + | PROP_DIAMETER | PROP_OUTER_RADIUS | PROP_INNER_RADIUS | PROP_SIDES @@ -355,6 +433,36 @@ fn apply_box_geometry_property( Some(true) } +fn apply_sphere_geometry_property( + value: &mut SolidHistorySphere, + 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 center = current.transform_point3(glam::DVec3::ZERO); + if !center.is_finite() || (target - center[axis]).abs() <= 1e-12 { + return Some(false); + } + let mut delta = glam::DVec3::ZERO; + delta[axis] = target - center[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, @@ -365,6 +473,11 @@ pub fn apply_primitive_property( return applied; } } + if let SolidHistoryOperation::Sphere(sphere_value) = operation { + if let Some(applied) = apply_sphere_geometry_property(sphere_value, field, value) { + return applied; + } + } let Some(number) = (if field == PROP_SIDES { value.trim().parse::().ok() } else { @@ -429,8 +542,19 @@ pub fn apply_primitive_property( PROP_HEIGHT => value.height = positive().unwrap_or(value.height), _ => return false, }, - SolidHistoryOperation::Sphere(value) if field == PROP_RADIUS => { - value.radius = positive().unwrap_or(value.radius); + SolidHistoryOperation::Sphere(value) => { + let Some(next) = positive() else { + return false; + }; + let radius = match field { + PROP_RADIUS => next, + PROP_DIAMETER => next * 0.5, + _ => return false, + }; + if (radius - value.radius).abs() <= 1e-12 { + return false; + } + value.radius = radius; } SolidHistoryOperation::Torus(value) => { let outer = value.major_radius + value.minor_radius; From 2768efeed5476afd347247c3ec7f0cac2e740665 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 31 Aug 2026 18:01:57 +0300 Subject: [PATCH 3/3] Fix mesh wireframe vertex layout --- src/scene/pipeline/mesh_gpu.rs | 21 ++++++++++++++++++--- src/scene/pipeline/mod.rs | 6 +++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/scene/pipeline/mesh_gpu.rs b/src/scene/pipeline/mesh_gpu.rs index b23d6b94f..93a696b07 100644 --- a/src/scene/pipeline/mesh_gpu.rs +++ b/src/scene/pipeline/mesh_gpu.rs @@ -343,8 +343,9 @@ pub struct MeshBatchChunk { /// erase — the geometry behind them. pub transp_index_buffer: wgpu::Buffer, pub transp_index_count: u32, - /// Triangle-edge line list (into `vertex_buffer`) for plain meshes that - /// carry no B-rep edges — the tessellation wireframe. + /// Triangle-edge line list for plain meshes that carry no B-rep edges. + /// The dedicated vertex buffer matches the edge pipeline layout. + pub wire_vertex_buffer: wgpu::Buffer, pub wire_index_buffer: wgpu::Buffer, pub wire_index_count: u32, /// B-rep feature edges of ACIS solids, as a standalone LineList vertex @@ -478,6 +479,18 @@ fn make_chunk( } else { instances }; + let wire_vertices: Vec = if wire_indices.is_empty() { + Vec::new() + } else { + verts + .iter() + .map(|vertex| MeshEdgeVertex { + position: vertex.position, + color: face_color, + position_low: vertex.position_low, + }) + .collect() + }; MeshBatchChunk { vertex_buffer: vertex_buffer_override.unwrap_or_else(|| { if compact_vertices { @@ -491,6 +504,7 @@ fn make_chunk( index_count: indices.len() as u32, transp_index_buffer: mk_index(transp_indices, "mesh.batch.transp_ibuf"), transp_index_count: transp_indices.len() as u32, + wire_vertex_buffer: mk_edge_vertex(&wire_vertices, "mesh.batch.wire_vbuf"), wire_index_buffer: mk_index(wire_indices, "mesh.batch.wire_ibuf"), wire_index_count: wire_indices.len() as u32, edge_vertex_buffer: edge_buffer_override @@ -1204,7 +1218,8 @@ fn build_instanced_chunk( uv_normal: uvs[6], } }; - let verts: Vec<_> = if shared_vertex_buffer.is_none() { + let needs_wire_vertices = first.include_edges && source.edge_verts.is_empty(); + let verts: Vec<_> = if shared_vertex_buffer.is_none() || needs_wire_vertices { (0..mesh.verts.len()).map(vertex).collect() } else { Vec::new() diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index 62cb5cedc..034d057ca 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -3810,7 +3810,7 @@ impl Pipeline { pass.set_vertex_buffer(1, c.instance_buffer.slice(..)); // Plain-mesh triangulation edges. if c.wire_index_count != 0 { - pass.set_vertex_buffer(0, c.vertex_buffer.slice(..)); + pass.set_vertex_buffer(0, c.wire_vertex_buffer.slice(..)); pass.set_index_buffer( c.wire_index_buffer.slice(..), wgpu::IndexFormat::Uint32, @@ -3847,7 +3847,7 @@ impl Pipeline { ); pass.set_vertex_buffer(1, c.instance_buffer.slice(..)); if c.wire_index_count != 0 { - pass.set_vertex_buffer(0, c.vertex_buffer.slice(..)); + pass.set_vertex_buffer(0, c.wire_vertex_buffer.slice(..)); pass.set_index_buffer( c.wire_index_buffer.slice(..), wgpu::IndexFormat::Uint32, @@ -4003,7 +4003,7 @@ impl Pipeline { ); pass.set_vertex_buffer(1, c.instance_buffer.slice(..)); if c.wire_index_count != 0 { - pass.set_vertex_buffer(0, c.vertex_buffer.slice(..)); + pass.set_vertex_buffer(0, c.wire_vertex_buffer.slice(..)); pass.set_index_buffer( c.wire_index_buffer.slice(..), wgpu::IndexFormat::Uint32,