diff --git a/Cargo.lock b/Cargo.lock index d77046e7b..21c353fd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -885,7 +885,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/ramox81/cadkernel.git?rev=c584d19dc45b3bef5146d6cf2e3dae79b72e9468#c584d19dc45b3bef5146d6cf2e3dae79b72e9468" +source = "git+https://github.com/ramox81/cadkernel.git?rev=d7431a6ab8600454ab3ad78ad7674576cbc2d5db#d7431a6ab8600454ab3ad78ad7674576cbc2d5db" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index f7a8295a2..325321206 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ rfd = "0.17" clap = { version = "4", features = ["derive", "string"] } env_logger = "0.11" acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "f23ffc873228e5f41b876485c741e4a8ac20a67d", features = ["serde"] } -cadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "c584d19dc45b3bef5146d6cf2e3dae79b72e9468", features = ["acis", "offset"] } +cadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "d7431a6ab8600454ab3ad78ad7674576cbc2d5db", features = ["acis", "offset"] } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } diff --git a/crates/ocs_doc_api/Cargo.toml b/crates/ocs_doc_api/Cargo.toml index 4c902ed91..e673dc1fb 100644 --- a/crates/ocs_doc_api/Cargo.toml +++ b/crates/ocs_doc_api/Cargo.toml @@ -15,7 +15,7 @@ thiserror = "1" # Host adapters convert ObjectId to acadrust handles; kernel handles stay local. acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "f23ffc873228e5f41b876485c741e4a8ac20a67d", features = ["serde"], optional = true } -cadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "c584d19dc45b3bef5146d6cf2e3dae79b72e9468", features = ["acis", "offset"], optional = true } +cadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "d7431a6ab8600454ab3ad78ad7674576cbc2d5db", features = ["acis", "offset"], optional = true } ocs_plugin_api = { path = "../ocs_plugin_api", optional = true, features = ["host"] } [build-dependencies] diff --git a/src/app/properties.rs b/src/app/properties.rs index 8fd1faec1..61742e50f 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -2142,6 +2142,17 @@ impl OpenCADStudio { entity_type_label(entity) } } + acadrust::EntityType::Surface(_) + if matches!( + crate::scene::model::solid_history::primitive_property_operation( + &self.tabs[i].scene.document, + handle, + ), + Some(acadrust::objects::SolidHistoryOperation::Extrusion(_)) + ) => + { + format!("{} ({})", t!("Surface"), t!("Extrusion")) + } _ => entity_type_label(entity), }; ui::PropertiesPanel { @@ -2249,23 +2260,22 @@ filter={:.1} local={:.1} aggregate={:.1} entities={}", ); } } - if local_refs.iter().all(|(handle, _)| { + let compact_solids = local_refs.iter().all(|(handle, _)| { crate::scene::model::solid_history::has_compact_solid_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_") - }) - }); + }); + if compact_solids { + retain_compact_solid_sections(&mut sections); } sections.extend(aggregate_solid_history_sections( &self.tabs[i].scene.document, &local_refs.iter().map(|(handle, _)| *handle).collect::>(), )); + if compact_solids { + retain_compact_solid_sections(&mut sections); + } ui::PropertiesPanel { choice_combos: sections .iter() @@ -3205,6 +3215,10 @@ fn retain_compact_solid_sections( sections: &mut Vec, ) { sections.iter_mut().for_each(|section| { + let internal_surface_section = section + .props + .iter() + .any(|property| property.field == "srf_kind"); section.props.retain(|property| { matches!( property.field, @@ -3217,7 +3231,8 @@ fn retain_compact_solid_sections( | "transparency" | "hyperlink" | "material" - ) || crate::scene::model::solid_history::is_specialized_property(property.field) + ) || (!internal_surface_section + && crate::scene::model::solid_history::is_specialized_property(property.field)) }); }); sections.retain(|section| !section.props.is_empty()); diff --git a/src/app/update/command.rs b/src/app/update/command.rs index c91e0f3e6..4b37b2e42 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -2002,7 +2002,48 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } self.push_undo_snapshot(i, "CHPROP"); - if crate::scene::model::solid_history::is_loft_geometry_choice(field) { + if crate::scene::model::solid_history::is_surface_property_choice(field) { + use crate::scene::model::solid_history::{ + PROP_SURFACE_MAINTAIN_ASSOCIATIVITY, PROP_SURFACE_SHOW_ASSOCIATIVITY, + PROP_SURFACE_WIREFRAME_TYPE, + }; + for &handle in &handles { + if self.tabs[i].scene.is_layer_locked(handle) { + continue; + } + let Some(mut state) = self.tabs[i] + .scene + .document + .get_entity(handle) + .and_then(|entity| match entity { + acadrust::EntityType::Surface(surface) => Some( + crate::entities::solid3d::surface_property_state(surface), + ), + _ => None, + }) + else { + continue; + }; + match field { + PROP_SURFACE_WIREFRAME_TYPE => { + state.isolines = !value.eq_ignore_ascii_case("Isoparms") + } + PROP_SURFACE_MAINTAIN_ASSOCIATIVITY => { + state.maintain_associativity = value.eq_ignore_ascii_case("Yes") + } + PROP_SURFACE_SHOW_ASSOCIATIVITY => { + state.show_associativity = value.eq_ignore_ascii_case("Yes") + } + _ => continue, + } + crate::scene::view::dispatch::set_entity_xdata( + &mut self.tabs[i].scene.document, + handle, + crate::entities::solid3d::SURFACE_PROPERTIES_APP, + Some(crate::entities::solid3d::surface_property_xdata_values(state)), + ); + } + } else if crate::scene::model::solid_history::is_loft_geometry_choice(field) { // These choices change the generated body, not just history // flags. Use the same transactional rebuild as numeric edits. for &handle in &handles { diff --git a/src/entities/solid3d.rs b/src/entities/solid3d.rs index ce06f0b5a..8cf4f1924 100644 --- a/src/entities/solid3d.rs +++ b/src/entities/solid3d.rs @@ -3,6 +3,7 @@ // Shared grips and properties for modeler entities. use acadrust::entities::{Body, Region, Solid3D, Surface}; +use acadrust::xdata::{ExtendedDataRecord, XDataValue}; use cadkernel::space::polygon; use crate::t; use crate::command::EntityTransform; @@ -49,6 +50,71 @@ fn yes_no(value: bool) -> &'static str { if value { "Yes" } else { "No" } } +pub(crate) const SURFACE_PROPERTIES_APP: &str = "OCS_SURFACE_PROPERTIES"; + +#[derive(Clone, Copy)] +pub(crate) struct SurfacePropertyState { + pub isolines: bool, + pub maintain_associativity: bool, + pub show_associativity: bool, +} + +pub(crate) fn surface_property_state(surface: &Surface) -> SurfacePropertyState { + let defaults = SurfacePropertyState { + isolines: true, + maintain_associativity: surface.history_handle.is_some(), + show_associativity: false, + }; + let Some(record) = surface + .common + .extended_data + .get_record(SURFACE_PROPERTIES_APP) + else { + return defaults; + }; + let mut values = record.values.iter().filter_map(|value| match value { + XDataValue::Integer16(value) => Some(*value), + _ => None, + }); + let Some(version) = values.next() else { + return defaults; + }; + if version != 1 { + return defaults; + } + SurfacePropertyState { + isolines: values.next().map_or(defaults.isolines, |value| value == 0), + maintain_associativity: values + .next() + .map_or(defaults.maintain_associativity, |value| value != 0), + show_associativity: values + .next() + .map_or(defaults.show_associativity, |value| value != 0), + } +} + +pub(crate) fn surface_property_xdata_values(state: SurfacePropertyState) -> Vec { + vec![ + XDataValue::Integer16(1), + XDataValue::Integer16(if state.isolines { 0 } else { 1 }), + XDataValue::Integer16(state.maintain_associativity as i16), + XDataValue::Integer16(state.show_associativity as i16), + ] +} + +fn write_surface_property_state(surface: &mut Surface, state: SurfacePropertyState) { + let mut record = ExtendedDataRecord::new(SURFACE_PROPERTIES_APP); + record.values = surface_property_xdata_values(state); + surface.common.extended_data.upsert_record(record); +} + +pub(crate) fn surface_isoline_counts(surface: &Surface) -> [usize; 2] { + [ + surface.u_isolines.max(0) as usize, + surface.v_isolines.max(0) as usize, + ] +} + fn handle_text(handle: Option) -> String { handle .filter(|handle| handle.is_valid()) @@ -700,6 +766,21 @@ impl PropertyEditable for Surface { fn apply_geom_prop(&mut self, field: &str, value: &str) { match field { + "srf_wireframe_type" => { + let mut state = surface_property_state(self); + state.isolines = !value.eq_ignore_ascii_case("Isoparms"); + write_surface_property_state(self, state); + } + "srf_maintain_associativity" => { + let mut state = surface_property_state(self); + state.maintain_associativity = value.eq_ignore_ascii_case("Yes"); + write_surface_property_state(self, state); + } + "srf_show_associativity" => { + let mut state = surface_property_state(self); + state.show_associativity = value.eq_ignore_ascii_case("Yes"); + write_surface_property_state(self, state); + } "srf_u_isolines" => { if let Some(value) = parse_f64(value) { self.u_isolines = (value.round() as i16).max(0); @@ -822,7 +903,8 @@ pub fn tessellate_volume( color, facet_res, chordal_deflection, - isolines, + surface_isoline_counts(s), + surface_property_state(s).isolines, ), EntityType::Mesh(_) | EntityType::PolygonMesh(_) | EntityType::PolyfaceMesh(_) => { crate::entities::mesh::tessellate_shaded_mesh(e, color) diff --git a/src/modules/insert/solid3d_cmds.rs b/src/modules/insert/solid3d_cmds.rs index 3f5788237..5e4d5d129 100644 --- a/src/modules/insert/solid3d_cmds.rs +++ b/src/modules/insert/solid3d_cmds.rs @@ -2778,6 +2778,8 @@ pub fn empty_extruded_surface(direction: DVec3, taper_angle: f64) -> EntityType use acadrust::types::Vector3; let mut surface = Surface::new(SurfaceKind::Extruded); + surface.u_isolines = 6; + surface.v_isolines = 6; if let SurfaceData::Extruded { options, sweep_vector, diff --git a/src/modules/model/edge_cmd.rs b/src/modules/model/edge_cmd.rs index 59a8a9c4b..08dfda16e 100644 --- a/src/modules/model/edge_cmd.rs +++ b/src/modules/model/edge_cmd.rs @@ -210,7 +210,12 @@ impl SolidEdgeCommand { ) .ok()?, }; - let mut wires = crate::scene::model::solid_model::grip_preview_wires(&result, handle, 0); + let mut wires = crate::scene::model::solid_model::grip_preview_wires( + &result, + handle, + [0; 2], + false, + ); for wire in &mut wires { wire.color = self.preview_color; wire.name = format!("{}-{}-PREVIEW", handle.value(), self.name()); diff --git a/src/scene/convert/acis_kernel.rs b/src/scene/convert/acis_kernel.rs index 4edc63268..29fb8b613 100644 --- a/src/scene/convert/acis_kernel.rs +++ b/src/scene/convert/acis_kernel.rs @@ -24,7 +24,7 @@ pub fn tessellate_sat( color: [f32; 4], facet_res: f64, chordal_deflection: Option, - isolines: usize, + isolines: [usize; 2], planar_isolines: bool, ) -> Option { let (bodies, loss) = lift(document); @@ -115,7 +115,7 @@ pub fn tessellate_sat( max_angle, source_fit * placement_scale, ) - .with_isolines(isolines) + .with_uv_isolines(isolines[0], isolines[1]) .with_planar_isolines(planar_isolines); if let Some(deflection) = chordal_deflection { tolerance = tolerance.with_chordal_deflection(deflection); diff --git a/src/scene/convert/solid3d_tess.rs b/src/scene/convert/solid3d_tess.rs index 3015b4a7f..682bfd3c0 100644 --- a/src/scene/convert/solid3d_tess.rs +++ b/src/scene/convert/solid3d_tess.rs @@ -10,7 +10,7 @@ fn tessellate_acis( color: [f32; 4], facet_res: f64, chordal_deflection: Option, - isolines: usize, + isolines: [usize; 2], planar_isolines: bool, ) -> Option { crate::scene::convert::acis_kernel::tessellate_sat( @@ -220,7 +220,7 @@ fn finish( color: [f32; 4], facet_res: f64, chordal_deflection: Option, - isolines: usize, + isolines: [usize; 2], planar_isolines: bool, acis: &acadrust::entities::AcisData, ) -> Option { @@ -255,7 +255,7 @@ pub fn tessellate_region( color, facet_res, chordal_deflection, - isolines, + [isolines; 2], false, ®ion.acis_data, ) @@ -279,7 +279,7 @@ pub fn tessellate_body( color, facet_res, chordal_deflection, - isolines, + [isolines; 2], false, &body.acis_data, ) @@ -290,7 +290,8 @@ pub fn tessellate_surface( color: [f32; 4], facet_res: f64, chordal_deflection: Option, - isolines: usize, + isolines: [usize; 2], + planar_isolines: bool, ) -> Option { let sat = parse_acis( || surface.parse_sat(), @@ -304,7 +305,7 @@ pub fn tessellate_surface( facet_res, chordal_deflection, isolines, - true, + planar_isolines, &surface.acis_data, ) } @@ -327,7 +328,7 @@ pub fn tessellate_solid3d( color, facet_res, chordal_deflection, - isolines, + [isolines; 2], false, &solid.acis_data, ) diff --git a/src/scene/mod.rs b/src/scene/mod.rs index bd2cf4fb0..715e6bb5d 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -3082,11 +3082,13 @@ impl Scene { let facet_resolution = self.document.header.facet_resolution; let chordal_deflection = crate::entities::solid3d::display_deflection(&self.document.header, facet_resolution); - let isolines = self.document.header.isolines.max(0) as usize; - let planar_isolines = matches!( - self.document.get_entity(handle), - Some(EntityType::Surface(_)) - ); + let (isolines, planar_isolines) = match self.document.get_entity(handle) { + Some(EntityType::Surface(surface)) => ( + crate::entities::solid3d::surface_isoline_counts(surface), + crate::entities::solid3d::surface_property_state(surface).isolines, + ), + _ => ([self.document.header.isolines.max(0) as usize; 2], false), + }; let (mut set, wires, center) = crate::scene::model::solid_model::display_from_solid( solid, diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index ba6027255..45eafe9e7 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -94,6 +94,14 @@ pub const PROP_LOFT_CLOSED: &str = "solid_history_loft_closed"; pub const PROP_LOFT_PERIODIC: &str = "solid_history_loft_periodic"; pub const PROP_HISTORY: &str = "solid_history_record"; pub const PROP_SHOW_HISTORY: &str = "solid_history_show"; +pub const PROP_SURFACE_TYPE: &str = "surface_type"; +pub const PROP_SURFACE_WIREFRAME_TYPE: &str = "srf_wireframe_type"; +pub const PROP_SURFACE_U_ISOLINES: &str = "srf_u_isolines"; +pub const PROP_SURFACE_V_ISOLINES: &str = "srf_v_isolines"; +pub const PROP_SURFACE_MAINTAIN_ASSOCIATIVITY: &str = "srf_maintain_associativity"; +pub const PROP_SURFACE_SHOW_ASSOCIATIVITY: &str = "srf_show_associativity"; +pub const PROP_SURFACE_TRIMMED: &str = "srf_trimmed"; +pub const PROP_SURFACE_TRIMMING_EDGES: &str = "srf_trimming_edges"; fn history_prop(label: &str, field: &'static str, value: impl ToString) -> Property { Property { @@ -103,6 +111,23 @@ fn history_prop(label: &str, field: &'static str, value: impl ToString) -> Prope } } +fn compact_surface_number(mut value: String) -> String { + if value.ends_with('°') { + value.pop(); + } + if let Some(decimal) = value.rfind('.') { + if value[decimal + 1..].chars().all(|character| character.is_ascii_digit()) { + while value.ends_with('0') { + value.pop(); + } + if value.ends_with('.') { + value.pop(); + } + } + } + value +} + fn history_flags( document: &acadrust::CadDocument, handle: acadrust::Handle, @@ -239,9 +264,14 @@ pub fn has_compact_solid_properties( document: &acadrust::CadDocument, handle: acadrust::Handle, ) -> bool { - document - .get_entity(handle) - .is_some_and(|entity| matches!(entity, acadrust::EntityType::Solid3D(_))) + match document.get_entity(handle) { + Some(acadrust::EntityType::Solid3D(_)) => true, + Some(acadrust::EntityType::Surface(_)) => matches!( + primitive_property_operation(document, handle), + Some(SolidHistoryOperation::Extrusion(_)) + ), + _ => false, + } } pub fn reference_point(operation: &SolidHistoryOperation) -> Option { @@ -403,10 +433,6 @@ fn extrusion_properties( |direction| crate::entities::common::format_length(direction[axis]), )), }; - let (record_history, object_show_history, show_history_mode) = - history_flags(document, handle).unwrap_or((false, false, 1)); - let (show_history, show_history_editable) = - displayed_history_state(object_show_history, show_history_mode); let path_length = sweep_path_length(value); let height_value = path_length.unwrap_or(value.end_draft_distance); let height = if value.path_entity.is_some() { @@ -419,6 +445,134 @@ fn extrusion_properties( } else { PropValue::EditText(crate::entities::common::format_angle(value.draft_angle)) }; + if let Some(EntityType::Surface(surface)) = document.get_entity(handle) { + let state = crate::entities::solid3d::surface_property_state(surface); + let surface_height = if value.path_entity.is_some() { + PropValue::ReadOnly(compact_surface_number( + crate::entities::common::format_length(height_value), + )) + } else { + PropValue::EditText(compact_surface_number( + crate::entities::common::format_length(height_value), + )) + }; + let surface_taper = if value.path_entity.is_some() { + PropValue::ReadOnly(compact_surface_number( + crate::entities::common::format_angle(value.draft_angle), + )) + } else { + PropValue::EditText(compact_surface_number( + crate::entities::common::format_angle(value.draft_angle), + )) + }; + let surface_direction_property = + |label: &str, field: &'static str, axis: usize| Property { + label: label.to_string(), + field, + value: PropValue::ReadOnly(direction.map_or_else( + || t!("Unavailable").into_owned(), + |direction| { + compact_surface_number(crate::entities::common::format_length( + direction[axis], + )) + }, + )), + }; + let yes_no_choice = |selected: bool| PropValue::Choice { + selected: if selected { "Yes" } else { "No" }.to_string(), + options: vec!["Yes".to_string(), "No".to_string()], + }; + return vec![ + PropSection { + title: t!("Geometry").into_owned(), + props: vec![ + Property { + label: t!("Surface Type").into_owned(), + field: PROP_SURFACE_TYPE, + value: PropValue::ReadOnly(t!("Extrusion").into_owned()), + }, + Property { + label: t!("Height").into_owned(), + field: PROP_EXTRUSION_HEIGHT, + value: surface_height, + }, + Property { + label: t!("Taper angle").into_owned(), + field: PROP_TAPER_ANGLE, + value: surface_taper, + }, + surface_direction_property( + t!("Direction X").as_ref(), + PROP_EXTRUSION_DIRECTION_X, + 0, + ), + surface_direction_property( + t!("Direction Y").as_ref(), + PROP_EXTRUSION_DIRECTION_Y, + 1, + ), + surface_direction_property( + t!("Direction Z").as_ref(), + PROP_EXTRUSION_DIRECTION_Z, + 2, + ), + Property { + label: t!("Wireframe type").into_owned(), + field: PROP_SURFACE_WIREFRAME_TYPE, + value: PropValue::Choice { + selected: if state.isolines { "Isolines" } else { "Isoparms" } + .to_string(), + options: vec!["Isolines".to_string(), "Isoparms".to_string()], + }, + }, + Property { + label: t!("U isolines").into_owned(), + field: PROP_SURFACE_U_ISOLINES, + value: PropValue::EditText(surface.u_isolines.max(0).to_string()), + }, + Property { + label: t!("V isolines").into_owned(), + field: PROP_SURFACE_V_ISOLINES, + value: PropValue::EditText(surface.v_isolines.max(0).to_string()), + }, + ], + }, + PropSection { + title: t!("Surface Associativity").into_owned(), + props: vec![ + Property { + label: t!("Maintain associativity").into_owned(), + field: PROP_SURFACE_MAINTAIN_ASSOCIATIVITY, + value: yes_no_choice(state.maintain_associativity), + }, + Property { + label: t!("Show associativity").into_owned(), + field: PROP_SURFACE_SHOW_ASSOCIATIVITY, + value: yes_no_choice(state.show_associativity), + }, + ], + }, + PropSection { + title: t!("Trims").into_owned(), + props: vec![ + Property { + label: t!("Trimmed surface").into_owned(), + field: PROP_SURFACE_TRIMMED, + value: PropValue::ReadOnly("No".to_string()), + }, + Property { + label: t!("Trimming edges").into_owned(), + field: PROP_SURFACE_TRIMMING_EDGES, + value: PropValue::ReadOnly("0".to_string()), + }, + ], + }, + ]; + } + let (record_history, object_show_history, show_history_mode) = + history_flags(document, handle).unwrap_or((false, false, 1)); + let (show_history, show_history_editable) = + displayed_history_state(object_show_history, show_history_mode); vec![ PropSection { title: t!("Geometry").into_owned(), @@ -1531,6 +1685,15 @@ pub fn is_history_choice(field: &str) -> bool { matches!(field, PROP_HISTORY | PROP_SHOW_HISTORY) } +pub fn is_surface_property_choice(field: &str) -> bool { + matches!( + field, + PROP_SURFACE_WIREFRAME_TYPE + | PROP_SURFACE_MAINTAIN_ASSOCIATIVITY + | PROP_SURFACE_SHOW_ASSOCIATIVITY + ) +} + pub fn is_loft_geometry_choice(field: &str) -> bool { matches!(field, PROP_LOFT_NORMALS | PROP_LOFT_CLOSED | PROP_LOFT_PERIODIC) } @@ -1538,7 +1701,11 @@ pub fn is_loft_geometry_choice(field: &str) -> bool { pub fn is_specialized_property(field: &str) -> bool { matches!(field, "solid_history_type" | PROP_BANK | PROP_SWEEP_LENGTH | PROP_LOFT_TYPE | PROP_LOFT_SECTION_COUNT - | PROP_EXTRUSION_DIRECTION_X | PROP_EXTRUSION_DIRECTION_Y | PROP_EXTRUSION_DIRECTION_Z) + | PROP_EXTRUSION_DIRECTION_X | PROP_EXTRUSION_DIRECTION_Y | PROP_EXTRUSION_DIRECTION_Z + | PROP_SURFACE_TYPE | PROP_SURFACE_WIREFRAME_TYPE + | PROP_SURFACE_U_ISOLINES | PROP_SURFACE_V_ISOLINES + | PROP_SURFACE_MAINTAIN_ASSOCIATIVITY | PROP_SURFACE_SHOW_ASSOCIATIVITY + | PROP_SURFACE_TRIMMED | PROP_SURFACE_TRIMMING_EDGES) || is_primitive_property(field) || is_history_choice(field) } @@ -3018,11 +3185,6 @@ fn extrusion_draft_grip( if !inverse.is_finite() { return None; } - let profile = value.sweep_entity.as_ref().and_then(embedded_entity)?; - let normal = crate::entities::curve::entity_curve(&profile)?.plane.normal()?; - let normal = transform - .transform_vector3(glam::DVec3::from_array(normal)) - .try_normalize()?; let center_local = inverse.transform_point3(center); let anchor = profile_grips .into_iter() @@ -3057,7 +3219,7 @@ fn extrusion_draft_grip( value.direction.y, value.direction.z, )); - let height = direction.dot(normal).abs(); + let height = direction.length(); if !direction.is_finite() || !height.is_finite() || height <= 1e-6 { return None; } diff --git a/src/scene/model/solid_model.rs b/src/scene/model/solid_model.rs index fadc09389..948f1e652 100644 --- a/src/scene/model/solid_model.rs +++ b/src/scene/model/solid_model.rs @@ -22,7 +22,7 @@ fn display_tessellation( body: &Body, facet_resolution: f64, chordal_deflection: Option, - isolines: usize, + isolines: [usize; 2], planar_isolines: bool, ) -> brep::mesh::BodyMesh { let resolution = if facet_resolution.is_finite() && facet_resolution > 0.0 { @@ -35,7 +35,7 @@ fn display_tessellation( |_| cadkernel::tessellation::display_angle_for_resolution(resolution), ); let mut tolerance = brep::mesh::TessellationTolerance::new(max_angle, TOL) - .with_isolines(isolines) + .with_uv_isolines(isolines[0], isolines[1]) .with_planar_isolines(planar_isolines); if let Some(deflection) = chordal_deflection { tolerance = tolerance.with_chordal_deflection(deflection); @@ -278,7 +278,8 @@ pub fn edge_wires(body: &Body) -> Vec { pub fn grip_preview_wires( body: &Body, handle: acadrust::Handle, - isolines: usize, + isolines: [usize; 2], + planar_isolines: bool, ) -> Vec { let tessellation = brep::mesh::tessellate( body, @@ -286,8 +287,8 @@ pub fn grip_preview_wires( cadkernel::tessellation::DEFAULT_ANGLE, TOL, ) - .with_isolines(isolines) - .with_planar_isolines(isolines > 0), + .with_uv_isolines(isolines[0], isolines[1]) + .with_planar_isolines(planar_isolines), ); tessellation .edges @@ -476,7 +477,7 @@ pub fn display_from_solid( color: [f32; 4], facet_resolution: f64, chordal_deflection: Option, - isolines: usize, + isolines: [usize; 2], planar_isolines: bool, ) -> Option<(MeshLodSet, Vec, [f64; 3])> { use acadrust::types::Vector3; @@ -566,7 +567,7 @@ mod tests { use super::*; fn tri_count(body: &Body) -> usize { - display_from_solid(body, [0.7, 0.7, 0.7, 1.0], 1.0, None, 0, false) + display_from_solid(body, [0.7, 0.7, 0.7, 1.0], 1.0, None, [0; 2], false) .map(|(m, _, _)| m.lods[0].indices.len() / 3) .unwrap_or(0) } diff --git a/src/scene/modify.rs b/src/scene/modify.rs index edd7195d5..a3c9434bd 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -1041,16 +1041,20 @@ impl Scene { { return false; } + let (isoline_counts, planar_isolines) = match self.document.get_entity(handle) { + Some(EntityType::Surface(surface)) => ( + crate::entities::solid3d::surface_isoline_counts(surface), + crate::entities::solid3d::surface_property_state(surface).isolines, + ), + _ => ([0; 2], false), + }; self.solid_history_preview_wires.insert( handle, crate::scene::model::solid_model::grip_preview_wires( &body, handle, - if matches!(self.document.get_entity(handle), Some(EntityType::Surface(_))) { - self.document.header.isolines.max(0) as usize - } else { - 0 - }, + isoline_counts, + planar_isolines, ), ); true diff --git a/src/snap.rs b/src/snap.rs index b3021cc22..8969eff91 100644 --- a/src/snap.rs +++ b/src/snap.rs @@ -1023,12 +1023,25 @@ impl Snapper { f32::MAX } }; + let flat_ortho = view_rot.z_axis.x.abs() < 1e-9 + && view_rot.z_axis.y.abs() < 1e-9 + && (view_rot.w_axis.w - 1.0).abs() < 1e-6; // Returns false when the wire's AABB does not overlap the snap circle — // safe to skip all vertex work for this wire. // UNBOUNDED_AABB (±infinity) passes through automatically without a // special-case branch because the arithmetic is exact for infinities. let wire_in_range = |wire: &WireModel| -> bool { + // In a tilted or perspective view `cursor_world` lies on the active + // construction plane, while the visible vertex may be anywhere on + // the same view ray. A world-XY AABB comparison can therefore reject + // a 3-D solid corner that is directly under the cursor. The scene's + // screen-space interaction index already performs the broad phase + // for those views; small unindexed drawings are cheap enough to let + // the exact screen-aperture checks below decide. + if !flat_ortho { + return true; + } // The AABB is stored in f32, so at UTM-scale coordinates each bound // is quantized by up to ~1 ulp (≈ coord × 2⁻²³ ≈ 0.7 m at 5.7e6). // When zoomed in hard the snap radius shrinks below that, so the