diff --git a/Cargo.lock b/Cargo.lock index ee4656dcc..b56f5fddd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -890,7 +890,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=f85efb6#f85efb659741f8d19d6bf750f3234aa333826926" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=574cf13#574cf13345c266cce01f4413c2ba54dff936f8ab" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index 3af5aad15..d0dc099a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ rfd = "0.17" clap = { version = "4", features = ["derive"] } env_logger = "0.11" acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "74c5fe9", features = ["serde"] } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "f85efb6", features = ["acis", "offset"] } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "574cf13", features = ["acis", "offset"] } acadifc = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "47b5603", optional = true } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index bffa44b5a..246e3b6ac 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -35,6 +35,64 @@ fn pt_pt_d2(a: Point, b: Point) -> f32 { (a.x - b.x).powi(2) + (a.y - b.y).powi(2) } +fn cursor_on_projected_axis( + cursor: Point, + bounds: iced::Rectangle, + view: glam::Mat4, + eye: glam::DVec3, + origin: glam::DVec3, + direction: glam::DVec3, +) -> Option { + let direction = direction.normalize_or_zero(); + if direction.length_squared() <= 1e-12 { + return None; + } + let relative = (origin - eye).as_vec3(); + let start = view * relative.extend(1.0); + let next = view * (relative + direction.as_vec3()).extend(1.0); + if start.w.abs() <= 1e-9 { + return None; + } + let delta = next - start; + let ndc = start.truncate() / start.w; + let derivative = glam::Vec2::new( + (delta.x * start.w - start.x * delta.w) / start.w.powi(2), + (delta.y * start.w - start.y * delta.w) / start.w.powi(2), + ); + let screen_origin = glam::Vec2::new( + (ndc.x + 1.0) * 0.5 * bounds.width, + (1.0 - ndc.y) * 0.5 * bounds.height, + ); + let screen_direction = glam::Vec2::new( + derivative.x * 0.5 * bounds.width, + -derivative.y * 0.5 * bounds.height, + ); + let length_squared = screen_direction.length_squared(); + if length_squared <= 1e-8 { + return None; + } + let cursor = glam::Vec2::new(cursor.x, cursor.y); + let screen = screen_origin + + screen_direction * (cursor - screen_origin).dot(screen_direction) / length_squared; + let target = glam::Vec2::new( + screen.x / bounds.width * 2.0 - 1.0, + 1.0 - screen.y / bounds.height * 2.0, + ); + let x_denominator = target.x * delta.w - delta.x; + let y_denominator = target.y * delta.w - delta.y; + if x_denominator.abs().max(y_denominator.abs()) <= 1e-9 { + return None; + } + let distance = if x_denominator.abs() >= y_denominator.abs() { + (start.x - target.x * start.w) / x_denominator + } else { + (start.y - target.y * start.w) / y_denominator + }; + distance + .is_finite() + .then_some(origin + direction * distance as f64) +} + fn is_added_polyline_vertex( original: &AcadEntityType, current: &AcadEntityType, @@ -422,9 +480,18 @@ impl OpenCADStudio { is_translate: bool, world: glam::DVec3, ) -> GripEdit { + let axis = self.tabs[i] + .selected_grip_handles + .iter() + .copied() + .zip(self.tabs[i].selected_grips.iter()) + .find(|(owner, grip)| *owner == handle && grip.id == grip_id) + .and_then(|(_, grip)| grip.axis); if !self.tabs[i].hot_grips.contains(&(handle, grip_id)) { self.tabs[i].hot_grips.clear(); - return GripEdit::single(handle, grip_id, is_translate, world); + let mut edit = GripEdit::single(handle, grip_id, is_translate, world); + edit.axis = axis; + return edit; } let mut targets: Vec = self.tabs[i] @@ -459,14 +526,18 @@ impl OpenCADStudio { } }); if targets.is_empty() { - return GripEdit::single(handle, grip_id, is_translate, world); + let mut edit = GripEdit::single(handle, grip_id, is_translate, world); + edit.axis = axis; + return edit; } + let axis = (targets.len() == 1).then_some(axis).flatten(); GripEdit { handle, grip_id, origin_world: world, last_world: world, mode: GripEditMode::Stretch, + axis, targets, } } @@ -1244,6 +1315,18 @@ impl OpenCADStudio { } } + if let Some(axis) = grip.axis { + snapped = cursor_on_projected_axis( + p, + bounds, + view_rot, + eye, + grip.origin_world, + axis, + ) + .unwrap_or(snapped); + } + let snap_ms = snap_started.elapsed().as_secs_f64() * 1000.0; // The overlay builds the active tracking guide from `otrack_active.base` // to `last_cursor_world`. Keep it synchronized with the actual point used @@ -1734,6 +1817,14 @@ impl OpenCADStudio { } pt }; + let effective = self.tabs[i] + .active_cmd + .as_ref() + .and_then(|command| command.cursor_axis()) + .and_then(|(origin, direction)| { + cursor_on_projected_axis(p, bounds, view_rot, eye, origin, direction) + }) + .unwrap_or(effective); // Dynamic-input locked fields constrain the preview point // (#356): a typed angle pins the direction, a typed // distance pins the radius — the same resolution the Enter @@ -2737,6 +2828,9 @@ impl OpenCADStudio { let history_originals = std::mem::take(&mut self.grip_history_originals); let dirty_before = self.grip_dirty_before.take().unwrap_or(self.tabs[i].dirty); if !handles.is_empty() { + for &handle in &handles { + let _ = self.tabs[i].scene.finalize_solid_history(handle); + } if !originals.is_empty() { self.push_entity_group_history( i, @@ -2998,6 +3092,14 @@ impl OpenCADStudio { } } } + pt = self.tabs[i] + .active_cmd + .as_ref() + .and_then(|command| command.cursor_axis()) + .and_then(|(origin, direction)| { + cursor_on_projected_axis(p, bounds, view_rot, eye, origin, direction) + }) + .unwrap_or(pt); // A click while dynamic-input fields hold typed values // commits the CONSTRAINED point — the same resolution // the preview shows and Enter would commit (#356). diff --git a/src/app/visibility.rs b/src/app/visibility.rs index 8c8a12511..2fbf83da4 100644 --- a/src/app/visibility.rs +++ b/src/app/visibility.rs @@ -151,6 +151,7 @@ impl OpenCADStudio { is_midpoint: false, shape: GripShape::Triangle, dir: None, + axis: None, }); self.tabs[i].selected_grip_handles.push(handle); self.tabs[i].visibility_grip = Some(VisibilityGrip { diff --git a/src/command.rs b/src/command.rs index 04ff648cc..4ebe6d8b2 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1621,6 +1621,11 @@ pub trait CadCommand: Send { /// during TRIM and trims during EXTEND, #336). Default no-op. fn set_shift(&mut self, _shift: bool) {} + /// Constrain the cursor to a construction axis for this step. + fn cursor_axis(&self) -> Option<(DVec3, DVec3)> { + None + } + /// Mid-command Ctrl+Z: a multi-point drawing command can take the undo /// itself (PLINE pops its last vertex) instead of the document undo /// swallowing the whole in-progress object. `None` (default) lets the diff --git a/src/entities/common.rs b/src/entities/common.rs index 3a250f51e..09a416c46 100644 --- a/src/entities/common.rs +++ b/src/entities/common.rs @@ -423,6 +423,7 @@ pub fn square_grip(id: usize, world: glam::DVec3) -> GripDef { is_midpoint: false, shape: GripShape::Square, dir: None, + axis: None, } } @@ -435,6 +436,7 @@ pub fn center_grip(id: usize, world: glam::DVec3) -> GripDef { is_midpoint: true, shape: GripShape::Square, dir: None, + axis: None, } } @@ -447,6 +449,7 @@ pub fn circle_grip(id: usize, world: glam::DVec3) -> GripDef { is_midpoint: true, shape: GripShape::Circle, dir: None, + axis: None, } } @@ -460,6 +463,7 @@ pub fn rectangle_grip(id: usize, world: glam::DVec3, dir: [f32; 2]) -> GripDef { is_midpoint: true, shape: GripShape::Rectangle, dir: Some(dir), + axis: None, } } @@ -471,6 +475,7 @@ pub fn triangle_grip(id: usize, world: glam::DVec3) -> GripDef { is_midpoint: false, shape: GripShape::Triangle, dir: None, + axis: None, } } diff --git a/src/modules/model/primitive_cmd.rs b/src/modules/model/primitive_cmd.rs index 1ad5b1ea5..87f1c88e4 100644 --- a/src/modules/model/primitive_cmd.rs +++ b/src/modules/model/primitive_cmd.rs @@ -102,8 +102,7 @@ impl PrimitiveCommand { } fn cursor_height(&self, point: DVec3) -> f64 { - (self.plane.to_local(point) - self.pts[0]) - .length() + (self.plane.to_local(point).z - self.pts[0].z) .max(1e-6) } @@ -218,11 +217,15 @@ impl PrimitiveCommand { } Shape::Torus => { let c = self.pts[0]; - let major = (self.pts[1] - c).length(); - let minor = (self.pts[2] - self.pts[1]).length(); - if major < 1e-6 || minor < 1e-6 { + let first = (self.pts[1] - c).length(); + let second = (self.pts[2] - c).length(); + let outer = first.max(second); + let inner = first.min(second); + if inner < 1e-6 || outer - inner < 1e-6 { return None; } + let major = (outer + inner) * 0.5; + let minor = (outer - inner) * 0.5; let center = [c.x, c.y, c.z]; ( primitives::build_torus(center, major, minor), @@ -238,9 +241,6 @@ impl PrimitiveCommand { let solid = solid?; let mut s3d = Solid3D::new(); s3d.set_sat_document(&doc); - // Edge wires make the solid click-pickable and draw a wireframe over - // the shaded mesh. - s3d.wires = solid_model::edge_wires(&solid); Some((EntityType::Solid3D(s3d), solid, history)) } @@ -280,6 +280,15 @@ impl CadCommand for PrimitiveCommand { self.plane = plane; } + fn cursor_axis(&self) -> Option<(DVec3, DVec3)> { + self.height_step.then(|| { + ( + self.plane.to_world(self.pts[0]), + self.plane.z.normalize_or_zero(), + ) + }) + } + fn name(&self) -> &'static str { self.shape.name() } @@ -289,12 +298,18 @@ impl CadCommand for PrimitiveCommand { if self.height_step { return t!("%{n} Specify height :", n = n).into_owned(); } - match (self.shape.radial(), self.pts.len()) { - (false, 0) => t!("%{n} Specify first corner:", n = n).into_owned(), - (false, _) => t!("%{n} Specify opposite corner:", n = n).into_owned(), - (true, 0) => t!("%{n} Specify center point:", n = n).into_owned(), - (true, 1) => t!("%{n} Specify radius:", n = n).into_owned(), - (true, _) => t!("%{n} Specify tube radius:", n = n).into_owned(), + match (self.shape, self.pts.len()) { + (Shape::Torus, 0) => t!("%{n} Specify center point:", n = n).into_owned(), + (Shape::Torus, 1) => t!("%{n} Specify outer radius:", n = n).into_owned(), + (Shape::Torus, _) => t!("%{n} Specify inner radius:", n = n).into_owned(), + (shape, 0) if shape.radial() => { + t!("%{n} Specify center point:", n = n).into_owned() + } + (shape, _) if shape.radial() => { + t!("%{n} Specify radius:", n = n).into_owned() + } + (_, 0) => t!("%{n} Specify first corner:", n = n).into_owned(), + (_, _) => t!("%{n} Specify opposite corner:", n = n).into_owned(), } } @@ -380,10 +395,9 @@ fn footprint_wire(shape: Shape, pts: &[DVec3]) -> WireModel { let r = (pts[1] - c).length(); circle_points(&mut points, c, r); if shape == Shape::Torus && pts.len() >= 3 { - // outer ring at major + minor for a quick torus hint - let minor = (pts[2] - pts[1]).length(); + let inner = (pts[2] - c).length(); points.push([f32::NAN; 3]); - circle_points(&mut points, c, r + minor); + circle_points(&mut points, c, inner); } } else { let (a, b) = (pts[0], pts[1]); @@ -423,7 +437,7 @@ fn height_wire(shape: Shape, pts: &[DVec3], height: f64) -> WireModel { let low = [ DVec3::new(x0, y0, a.z), DVec3::new(x1, y0, a.z), - DVec3::new(x1, y0, a.z + height), + DVec3::new(x0, y0, a.z + height), ]; let high = low.map(|point| DVec3::new(point.x, y1, point.z)); push_loop(&mut points, &low); diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 7fbdc0c9d..ad9c53115 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -2673,30 +2673,28 @@ impl Scene { }) } - /// Re-evaluate every cached mesh's color through `render_style` so a - /// Register a Model-tab solid: cache its B-rep (for boolean ops and the - /// exact-geometry save path) and tessellate it into the shaded mesh - /// pipeline under `handle`. The body is in the same offset-relative frame - /// the mesh pipeline uses, so the mesh is stored as-is (Model-tab geometry - /// is authored at world_offset 0). + /// Cache and display a Model-tab kernel solid. pub fn register_solid_model(&mut self, handle: Handle, solid: cadkernel::brep::Body) { - if let Some(center) = crate::scene::model::solid_model::centre(&solid) { + let color = self + .document + .get_entity(handle) + .map(|e| self.render_style(e).0) + .unwrap_or([0.8, 0.8, 0.85, 1.0]); + if let Some((mut set, wires, center)) = + crate::scene::model::solid_model::display_from_solid(&solid, color) + { if let Some(EntityType::Solid3D(entity)) = self.document.get_entity_mut(handle) { entity.point_of_reference = acadrust::types::Vector3::new( center[0], center[1], center[2], ); + entity.wires = wires; + entity.silhouettes.clear(); } - } - let entity = self.document.get_entity(handle); - let color = entity - .map(|e| self.render_style(e).0) - .unwrap_or([0.8, 0.8, 0.85, 1.0]); - if let Some(mut set) = crate::scene::model::solid_model::mesh_from_solid(&solid, color) { let name = handle.value().to_string(); for mesh in &mut set.lods { mesh.name = name.clone(); } - if let Some(entity) = entity { + if let Some(entity) = self.document.get_entity(handle) { crate::scene::model::material_model::resolve_material_with_base( &self.document, entity, @@ -2716,6 +2714,8 @@ impl Scene { ); } self.meshes.insert(handle, set); + } else { + self.meshes.remove(&handle); } self.solid_models.insert(handle, solid); // Only this solid's mesh changed — report just its handle so the mesh / diff --git a/src/scene/model/object.rs b/src/scene/model/object.rs index 64499702c..39c04dc27 100644 --- a/src/scene/model/object.rs +++ b/src/scene/model/object.rs @@ -105,6 +105,8 @@ pub struct GripDef { /// along its segment. `None` for shapes that don't need rotation /// (Square, Triangle in non-directional contexts). pub dir: Option<[f32; 2]>, + /// World-space axis that constrains this grip's drag. + pub axis: Option, } /// How to apply a grip drag result. diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index cdf789548..bd1435873 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -18,16 +18,16 @@ pub const GRIP_LENGTH: usize = 10_001; pub const GRIP_WIDTH: usize = 10_002; pub const GRIP_HEIGHT: usize = 10_003; pub const GRIP_RADIUS: usize = 10_004; -pub const GRIP_MAJOR_RADIUS: usize = 10_005; -pub const GRIP_MINOR_RADIUS: usize = 10_006; +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 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_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"; fn history_prop(label: &str, field: &'static str, value: impl ToString) -> Property { @@ -62,14 +62,14 @@ pub fn primitive_properties( )], SolidHistoryOperation::Torus(value) => vec![ history_prop( - t!("Major Radius").as_ref(), - PROP_MAJOR_RADIUS, - value.major_radius, + t!("Outer Radius").as_ref(), + PROP_OUTER_RADIUS, + value.major_radius + value.minor_radius, ), history_prop( - t!("Minor Radius").as_ref(), - PROP_MINOR_RADIUS, - value.minor_radius, + t!("Inner Radius").as_ref(), + PROP_INNER_RADIUS, + (value.major_radius - value.minor_radius).max(0.0), ), ], SolidHistoryOperation::Pyramid(value) => vec![ @@ -92,8 +92,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 ) } @@ -132,11 +132,33 @@ pub fn apply_primitive_property( SolidHistoryOperation::Sphere(value) if field == PROP_RADIUS => { value.radius = positive().unwrap_or(value.radius); } - SolidHistoryOperation::Torus(value) => match field { - PROP_MAJOR_RADIUS => value.major_radius = positive().unwrap_or(value.major_radius), - PROP_MINOR_RADIUS => value.minor_radius = positive().unwrap_or(value.minor_radius), - _ => return false, - }, + SolidHistoryOperation::Torus(value) => { + let outer = value.major_radius + value.minor_radius; + let inner = (value.major_radius - value.minor_radius).max(0.0); + match field { + PROP_OUTER_RADIUS => { + let Some(next_outer) = positive() else { + return false; + }; + if next_outer <= inner { + return false; + } + value.major_radius = (next_outer + inner) * 0.5; + value.minor_radius = (next_outer - inner) * 0.5; + } + PROP_INNER_RADIUS => { + let Some(next_inner) = positive() else { + return false; + }; + if next_inner >= outer { + return false; + } + value.major_radius = (outer + next_inner) * 0.5; + value.minor_radius = (outer - next_inner) * 0.5; + } + _ => return false, + } + } SolidHistoryOperation::Pyramid(value) => match field { PROP_RADIUS => value.radius = positive().unwrap_or(value.radius), PROP_HEIGHT => value.height = positive().unwrap_or(value.height), @@ -232,17 +254,28 @@ fn world_point(transform: [f64; 16], point: [f64; 3]) -> Option { Some(matrix(transform)?.transform_point3(glam::DVec3::from_array(point))) } +fn world_vector(transform: [f64; 16], vector: [f64; 3]) -> Option { + let vector = matrix(transform)?.transform_vector3(glam::DVec3::from_array(vector)); + (vector.length_squared() > 1e-12).then(|| vector.normalize()) +} + fn local_point(transform: [f64; 16], point: glam::DVec3) -> Option { Some(matrix(transform)?.inverse().transform_point3(point)) } -fn grip(id: usize, world: glam::DVec3, shape: GripShape) -> GripDef { +fn grip( + id: usize, + world: glam::DVec3, + shape: GripShape, + axis: Option, +) -> GripDef { GripDef { id, world, is_midpoint: false, shape, dir: None, + axis, } } @@ -254,9 +287,14 @@ pub fn primitive_grips( return Vec::new(); }; let mut grips = Vec::new(); - let mut add = |id, transform, point, shape| { + let mut add = |id, transform, point, shape, axis: Option<[f64; 3]>| { if let Some(world) = world_point(transform, point) { - grips.push(grip(id, world, shape)); + grips.push(grip( + id, + world, + shape, + axis.and_then(|vector| world_vector(transform, vector)), + )); } }; match operation { @@ -266,18 +304,21 @@ pub fn primitive_grips( value.base.transform, [value.length, value.width * 0.5, 0.0], GripShape::Square, + None, ); add( GRIP_WIDTH, value.base.transform, [value.length * 0.5, value.width, 0.0], GripShape::Square, + None, ); add( GRIP_HEIGHT, value.base.transform, [value.length * 0.5, value.width * 0.5, value.height], GripShape::Square, + Some([0.0, 0.0, 1.0]), ); } SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => { @@ -286,12 +327,14 @@ pub fn primitive_grips( 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::Sphere(value) => add( @@ -299,19 +342,26 @@ pub fn primitive_grips( value.base.transform, [value.radius, 0.0, 0.0], GripShape::Square, + None, ), SolidHistoryOperation::Torus(value) => { add( - GRIP_MAJOR_RADIUS, - value.base.transform, - [value.major_radius, 0.0, 0.0], - GripShape::Square, - ); - add( - GRIP_MINOR_RADIUS, + GRIP_OUTER_RADIUS, value.base.transform, [value.major_radius + value.minor_radius, 0.0, 0.0], GripShape::Square, + None, + ); + add( + GRIP_INNER_RADIUS, + value.base.transform, + [ + (value.major_radius - value.minor_radius).max(0.0), + 0.0, + 0.0, + ], + GripShape::Square, + None, ); } SolidHistoryOperation::Pyramid(value) => { @@ -320,12 +370,14 @@ pub fn primitive_grips( value.base.transform, [value.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]), ); let angle = (value.sides.clamp(3, 71) as f64 * 5.0).to_radians(); add( @@ -333,6 +385,7 @@ pub fn primitive_grips( value.base.transform, [value.radius * angle.cos(), value.radius * angle.sin(), 0.0], GripShape::Triangle, + None, ); } _ => {} @@ -360,7 +413,7 @@ pub fn apply_primitive_grip( match grip_id { GRIP_LENGTH => value.length = positive(local.x), GRIP_WIDTH => value.width = positive(local.y), - GRIP_HEIGHT => value.height = positive(local.z), + GRIP_HEIGHT => value.height = local.z.max(1e-6), _ => return false, } } @@ -372,7 +425,7 @@ pub fn apply_primitive_grip( value.minor_radius = radius; value.x_radius = radius; } - GRIP_HEIGHT => value.height = positive(local.z), + GRIP_HEIGHT => value.height = local.z.max(1e-6), _ => return false, } } @@ -380,19 +433,29 @@ pub fn apply_primitive_grip( value.radius = local.length().max(1e-6); } SolidHistoryOperation::Torus(value) => match grip_id { - GRIP_MAJOR_RADIUS => { - value.major_radius = local.x.hypot(local.y).max(1e-6) + GRIP_OUTER_RADIUS => { + let outer = local.x.hypot(local.y).max(1e-6); + let inner = (value.major_radius - value.minor_radius).max(1e-6); + if outer <= inner { + return false; + } + value.major_radius = (outer + inner) * 0.5; + value.minor_radius = (outer - inner) * 0.5; } - GRIP_MINOR_RADIUS => { - value.minor_radius = (local.x.hypot(local.y) - value.major_radius) - .abs() - .max(1e-6) + GRIP_INNER_RADIUS => { + let inner = local.x.hypot(local.y).max(1e-6); + let outer = value.major_radius + value.minor_radius; + if inner >= outer { + return false; + } + value.major_radius = (outer + inner) * 0.5; + value.minor_radius = (outer - inner) * 0.5; } _ => return false, }, SolidHistoryOperation::Pyramid(value) => match grip_id { GRIP_RADIUS => value.radius = local.x.hypot(local.y).max(1e-6), - GRIP_HEIGHT => value.height = positive(local.z), + GRIP_HEIGHT => value.height = local.z.max(1e-6), GRIP_SIDES => { let angle = local.y.atan2(local.x).rem_euclid(std::f64::consts::TAU); value.sides = (angle.to_degrees() / 5.0).round() as i32; diff --git a/src/scene/model/solid_model.rs b/src/scene/model/solid_model.rs index 60cdd0cff..15d2fa11a 100644 --- a/src/scene/model/solid_model.rs +++ b/src/scene/model/solid_model.rs @@ -211,11 +211,11 @@ pub fn edge_wires(body: &Body) -> Vec { use acadrust::types::Vector3; tessellation(body) .edges - .into_iter() + .iter() .map(|edge| { acadrust::entities::Wire::from_points( edge.positions - .into_iter() + .iter() .map(|p| Vector3::new(p[0], p[1], p[2])) .collect(), ) @@ -254,7 +254,13 @@ pub fn boolean(op: Bool, a: &Body, b: &Body) -> Option { /// Tessellate a `Body` into a single-LOD `MeshLodSet` (world-space, before /// world_offset is applied by the caller). pub fn mesh_from_solid(body: &Body, color: [f32; 4]) -> Option { - let tessellation = tessellation(body); + mesh_from_tessellation(tessellation(body), color) +} + +fn mesh_from_tessellation( + tessellation: brep::mesh::BodyMesh, + color: [f32; 4], +) -> Option { let silhouette = tessellation.silhouette_source(); let mesh = tessellation.mesh; if mesh.is_empty() { @@ -313,12 +319,37 @@ pub fn mesh_from_solid(body: &Body, color: [f32; 4]) -> Option { Some(set) } +pub fn display_from_solid( + body: &Body, + color: [f32; 4], +) -> Option<(MeshLodSet, Vec, [f64; 3])> { + use acadrust::types::Vector3; + let tessellation = tessellation(body); + let center = mesh_center(&tessellation.mesh)?; + let wires = tessellation + .edges + .iter() + .map(|edge| { + acadrust::entities::Wire::from_points( + edge.positions + .iter() + .map(|point| Vector3::new(point[0], point[1], point[2])) + .collect(), + ) + }) + .collect(); + Some((mesh_from_tessellation(tessellation, color)?, wires, center)) +} + /// The middle of a body, for a caller needing a point to turn or scale about. /// /// Read off the mesh rather than `body_bounds`, which refuses a face that /// wraps a closed surface — a sphere is one such face and has no box at all. pub fn centre(body: &Body) -> Option<[f64; 3]> { - let mesh = tessellation(body).mesh; + mesh_center(&tessellation(body).mesh) +} + +fn mesh_center(mesh: &brep::mesh::Mesh) -> Option<[f64; 3]> { if mesh.positions.is_empty() { return None; } diff --git a/src/scene/modify.rs b/src/scene/modify.rs index 4001d93ec..2f004a18e 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -728,7 +728,6 @@ impl Scene { let Some(document) = crate::scene::convert::acis_export::planar_solid_to_sat(&body) else { return false; }; - let wires = crate::scene::model::solid_model::edge_wires(&body); self.record_solid_history_before(handle); if self .document @@ -741,8 +740,46 @@ impl Scene { return false; }; entity.set_sat_document(&document); - entity.wires = wires; - entity.silhouettes.clear(); + self.register_solid_model(handle, body); + true + } + + pub fn finalize_solid_history(&mut self, handle: Handle) -> bool { + let Some(operation) = self.document.solid_history_operation(handle).cloned() else { + return false; + }; + let Ok(body) = cadkernel::acis::rebuild_body(&operation) else { + return false; + }; + let Some(document) = crate::scene::convert::acis_export::planar_solid_to_sat(&body) else { + return false; + }; + let Some(EntityType::Solid3D(entity)) = self.document.get_entity_mut(handle) else { + return false; + }; + entity.set_sat_document(&document); + self.register_solid_model(handle, body); + true + } + + fn preview_solid_history( + &mut self, + handle: Handle, + operation: acadrust::objects::SolidHistoryOperation, + ) -> bool { + let Ok(body) = cadkernel::acis::rebuild_body(&operation) else { + return false; + }; + if self + .document + .update_solid_history(handle, operation) + .is_none() + { + return false; + } + let Some(EntityType::Solid3D(_)) = self.document.get_entity(handle) else { + return false; + }; self.register_solid_model(handle, body); true } @@ -761,12 +798,18 @@ impl Scene { ) { return false; } - if self.rebuild_solid_history(handle, operation.clone()) { + if let EntityTransform::Translate(delta) = transform { + if self + .document + .update_solid_history(handle, operation) + .is_none() + { + return false; + } + self.translate_solid_geometry(handle, delta.to_array()); return true; } - self.record_solid_history_before(handle); - let _ = self.document.update_solid_history(handle, operation); - false + self.rebuild_solid_history(handle, operation) } fn apply_solid_history_grip( @@ -785,7 +828,7 @@ impl Scene { ) { return false; } - self.rebuild_solid_history(handle, operation) + self.preview_solid_history(handle, operation) } pub fn apply_solid_history_property( diff --git a/src/scene/pick/grip.rs b/src/scene/pick/grip.rs index 263ccc291..da19a4730 100644 --- a/src/scene/pick/grip.rs +++ b/src/scene/pick/grip.rs @@ -26,6 +26,8 @@ pub struct GripEdit { pub last_world: DVec3, /// How cursor movement modifies the selected grip. pub mode: GripEditMode, + /// Optional world-space drag axis. + pub axis: Option, /// Every hot grip moved by this edit. A normal grip edit contains one target. pub targets: Vec, } @@ -57,6 +59,7 @@ impl GripEdit { origin_world: world, last_world: world, mode: GripEditMode::Stretch, + axis: None, targets: vec![GripTarget { handle, grip_id,