Merge pull request #1131 from ramox81/codex/thicken-surface-grips

This commit is contained in:
Hakan Seven committed 2026-09-10 17:05:43 +03:00
commit 4201b0a031
18 files changed
+536 -67

No files matched your search

Generated
+1 -1
View File
@@ -885,7 +885,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cadkernel"
version = "0.1.0"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=dfd0de176a878c20a1aeb8d23b0b8265197b5f5c#dfd0de176a878c20a1aeb8d23b0b8265197b5f5c"
source = "git+https://github.com/ramox81/cadkernel.git?rev=d7431a6ab8600454ab3ad78ad7674576cbc2d5db#d7431a6ab8600454ab3ad78ad7674576cbc2d5db"
dependencies = [
"acadrust",
"cavalier_contours",
+1 -1
View File
@@ -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/HakanSeven12/cadkernel.git", rev = "dfd0de176a878c20a1aeb8d23b0b8265197b5f5c", 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"] }
+1 -1
View File
@@ -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/HakanSeven12/cadkernel.git", rev = "dfd0de176a878c20a1aeb8d23b0b8265197b5f5c", 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]
+30 -4
View File
@@ -3756,10 +3756,36 @@ impl OpenCADStudio {
let direction = direction
.or(path_direction)
.unwrap_or(glam::DVec3::ZERO);
self.add_surface_model(
empty_extruded_surface(direction, taper_angle),
body,
)
let history = path
.is_none()
.then(|| {
sweep_model::extrusion_history(
&entity,
None,
direction.to_array(),
taper_angle,
profile.plane.origin,
)
})
.flatten();
let mut surface = empty_extruded_surface(direction, taper_angle);
if let (
Some(acadrust::objects::SolidHistoryOperation::Extrusion(value)),
acadrust::EntityType::Surface(entity),
) = (&history, &mut surface)
{
if let Some(data) =
crate::scene::model::solid_history::extrusion_surface_data(value)
{
entity.surface_data = data;
}
}
match history {
Some(history) => {
self.add_surface_model_with_history(surface, body, history)
}
None => self.add_surface_model(surface, body),
}
} else {
let direction = direction.unwrap_or(glam::DVec3::ZERO);
let history = path
+18
View File
@@ -402,6 +402,24 @@ impl super::OpenCADStudio {
self.add_surface_model_inner(entity, surface, true)
}
pub(super) fn add_surface_model_with_history(
&mut self,
entity: EntityType,
surface: Body,
history: SolidHistoryOperation,
) -> Handle {
let i = self.active_tab;
let handle = self.add_surface_model_inner(entity, surface, true);
if handle.is_null() {
return handle;
}
if !self.tabs[i].scene.create_solid_history(handle, history) {
self.tabs[i].scene.rollback_new_entities(&[handle]);
return Handle::NULL;
}
handle
}
fn add_surface_model_preserving_style(
&mut self,
entity: EntityType,
+24 -9
View File
@@ -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::<Vec<_>>(),
));
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<crate::scene::model::object::PropSection>,
) {
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());
+42 -1
View File
@@ -2003,7 +2003,48 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
}
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 {
+44 -9
View File
@@ -1535,15 +1535,30 @@ 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 acquired_point = otrack_hit.is_some()
|| snap_hit.is_some_and(|hit| {
hit.snap_type != crate::snap::SnapType::Grid
});
if acquired_point {
if let Some(axis) = axis.try_normalize() {
// An axis grip still moves on its construction axis, but
// an acquired object point supplies the exact coordinate
// along that axis. This lets height and draft arrows seat
// back onto existing surface/solid geometry.
snapped = grip.origin_world
+ axis * (snapped - grip.origin_world).dot(axis);
}
} else {
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;
@@ -1751,6 +1766,26 @@ impl OpenCADStudio {
let preview_ms = preview_started.elapsed().as_secs_f64() * 1000.0;
let geometry_ms = grip_started.elapsed().as_secs_f64() * 1000.0;
self.refresh_selected_grips();
// A history rebuild can clamp a requested value or move a derived
// construction handle non-linearly. Rebase each active target onto
// the freshly rebuilt grip so the hot arrow remains attached to the
// live surface and reverses immediately from a geometric limit.
let refreshed_targets: Vec<_> = self.tabs[i]
.selected_grip_handles
.iter()
.copied()
.zip(self.tabs[i].selected_grips.iter())
.map(|(handle, grip)| ((handle, grip.id), grip.world))
.collect();
if let Some(active) = self.tabs[i].active_grip.as_mut() {
for target in &mut active.targets {
if let Some((_, world)) = refreshed_targets.iter().find(|((handle, id), _)| {
*handle == target.handle && *id == target.grip_id
}) {
target.last_world = *world;
}
}
}
let grips_ms = grip_started.elapsed().as_secs_f64() * 1000.0 - geometry_ms;
// Properties are refreshed when the grip is committed or cancelled.
// Rebuilding the inspector on every pointer event adds no drawing
+83 -1
View File
@@ -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<XDataValue> {
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<acadrust::Handle>) -> 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)
+2
View File
@@ -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,
+6 -1
View File
@@ -210,7 +210,12 @@ impl SolidEdgeCommand {
)
.ok()?,
};
let mut wires = crate::scene::model::solid_model::grip_preview_wires(&result, handle);
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());
+4 -2
View File
@@ -24,7 +24,8 @@ pub fn tessellate_sat(
color: [f32; 4],
facet_res: f64,
chordal_deflection: Option<f64>,
isolines: usize,
isolines: [usize; 2],
planar_isolines: bool,
) -> Option<MeshLodSet> {
let (bodies, loss) = lift(document);
if bodies.is_empty() {
@@ -114,7 +115,8 @@ 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);
}
+15 -6
View File
@@ -10,7 +10,8 @@ fn tessellate_acis(
color: [f32; 4],
facet_res: f64,
chordal_deflection: Option<f64>,
isolines: usize,
isolines: [usize; 2],
planar_isolines: bool,
) -> Option<MeshLodSet> {
crate::scene::convert::acis_kernel::tessellate_sat(
sat,
@@ -19,6 +20,7 @@ fn tessellate_acis(
facet_res,
chordal_deflection,
isolines,
planar_isolines,
)
}
@@ -218,7 +220,8 @@ fn finish(
color: [f32; 4],
facet_res: f64,
chordal_deflection: Option<f64>,
isolines: usize,
isolines: [usize; 2],
planar_isolines: bool,
acis: &acadrust::entities::AcisData,
) -> Option<MeshLodSet> {
let mut set = tessellate_acis(
@@ -228,6 +231,7 @@ fn finish(
facet_res,
chordal_deflection,
isolines,
planar_isolines,
)?;
remap_acis_material_bindings(&mut set, acis);
Some(set)
@@ -251,7 +255,8 @@ pub fn tessellate_region(
color,
facet_res,
chordal_deflection,
isolines,
[isolines; 2],
false,
&region.acis_data,
)
}
@@ -274,7 +279,8 @@ pub fn tessellate_body(
color,
facet_res,
chordal_deflection,
isolines,
[isolines; 2],
false,
&body.acis_data,
)
}
@@ -284,7 +290,8 @@ pub fn tessellate_surface(
color: [f32; 4],
facet_res: f64,
chordal_deflection: Option<f64>,
isolines: usize,
isolines: [usize; 2],
planar_isolines: bool,
) -> Option<MeshLodSet> {
let sat = parse_acis(
|| surface.parse_sat(),
@@ -298,6 +305,7 @@ pub fn tessellate_surface(
facet_res,
chordal_deflection,
isolines,
planar_isolines,
&surface.acis_data,
)
}
@@ -320,7 +328,8 @@ pub fn tessellate_solid3d(
color,
facet_res,
chordal_deflection,
isolines,
[isolines; 2],
false,
&solid.acis_data,
)
}
+8 -1
View File
@@ -3082,7 +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 (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,
@@ -3090,6 +3096,7 @@ impl Scene {
facet_resolution,
chordal_deflection,
isolines,
planar_isolines,
)?;
let name = handle.value().to_string();
for mesh in &mut set.lods {
+195 -19
View File
@@ -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<glam::DVec3> {
@@ -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;
}
@@ -3307,6 +3469,7 @@ pub fn primitive_grips(
if let SolidHistoryOperation::Chamfer(value) = operation {
return chamfer_distance_grips(document, handle, value);
}
let is_surface = matches!(document.get_entity(handle), Some(EntityType::Surface(_)));
let mut grips = Vec::new();
let mut add = |id, transform, point, shape, axis: Option<[f64; 3]>| {
if let Some(world) = world_point(transform, point) {
@@ -3528,27 +3691,40 @@ pub fn primitive_grips(
}
SolidHistoryOperation::Extrusion(value) => {
let (profile_grips, profile_center) = extrusion_profile_grips(value);
grips.extend(profile_grips);
if !is_surface {
grips.extend(profile_grips);
}
let direction = [value.direction.x, value.direction.y, value.direction.z];
if value.path_entity.is_none() {
if let (Some(center), Some(base)) = (profile_center, matrix(value.base.transform)) {
let direction = base.transform_vector3(glam::DVec3::from_array(direction));
if direction.length_squared() > 1e-12 {
grips.push(grip(
let mut height_grip = grip(
GRIP_HEIGHT,
center + direction,
GripShape::Triangle,
Some(direction.normalize()),
));
);
if is_surface {
// Surface construction arrows keep one stable screen
// orientation while their world-space axis continues to
// constrain the edit.
height_grip.dir = None;
}
grips.push(height_grip);
}
}
if let Some((world, _, radial, _, _)) = extrusion_draft_grip(value) {
grips.push(grip(
let mut draft_grip = grip(
GRIP_DRAFT,
world,
GripShape::Triangle,
Some(radial),
));
);
if is_surface {
draft_grip.dir = None;
}
grips.push(draft_grip);
}
}
}
+36 -10
View File
@@ -22,7 +22,8 @@ fn display_tessellation(
body: &Body,
facet_resolution: f64,
chordal_deflection: Option<f64>,
isolines: usize,
isolines: [usize; 2],
planar_isolines: bool,
) -> brep::mesh::BodyMesh {
let resolution = if facet_resolution.is_finite() && facet_resolution > 0.0 {
facet_resolution.clamp(0.01, 10.0)
@@ -34,7 +35,8 @@ 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);
}
@@ -267,20 +269,42 @@ pub fn edge_wires(body: &Body) -> Vec<acadrust::entities::Wire> {
.collect()
}
/// White wireframe used while a solid-history grip is hot.
/// White wireframe used while a solid-history grip is hot. Surface previews
/// include the requested construction isolines so their cage follows the edit.
///
/// This deliberately does not touch the resident solid mesh or its entity
/// wires: the selected source stays visible in blue while the candidate body
/// is presented as a separate, non-pickable outline until placement.
pub fn grip_preview_wires(body: &Body, handle: acadrust::Handle) -> Vec<WireModel> {
tessellation(body)
pub fn grip_preview_wires(
body: &Body,
handle: acadrust::Handle,
isolines: [usize; 2],
planar_isolines: bool,
) -> Vec<WireModel> {
let tessellation = brep::mesh::tessellate(
body,
brep::mesh::TessellationTolerance::new(
cadkernel::tessellation::DEFAULT_ANGLE,
TOL,
)
.with_uv_isolines(isolines[0], isolines[1])
.with_planar_isolines(planar_isolines),
);
tessellation
.edges
.into_iter()
.filter(|edge| edge.positions.len() >= 2)
.map(|edge| {
.map(|edge| edge.positions)
.chain(
tessellation
.isolines
.into_iter()
.map(|isoline| isoline.positions),
)
.filter(|positions| positions.len() >= 2)
.map(|positions| {
WireModel::solid_f64(
format!("{}-GRIP-PREVIEW", handle.value()),
edge.positions,
positions,
WireModel::WHITE,
false,
)
@@ -453,7 +477,8 @@ pub fn display_from_solid(
color: [f32; 4],
facet_resolution: f64,
chordal_deflection: Option<f64>,
isolines: usize,
isolines: [usize; 2],
planar_isolines: bool,
) -> Option<(MeshLodSet, Vec<acadrust::entities::Wire>, [f64; 3])> {
use acadrust::types::Vector3;
let tessellation = display_tessellation(
@@ -461,6 +486,7 @@ pub fn display_from_solid(
facet_resolution,
chordal_deflection,
isolines,
planar_isolines,
);
let center = mesh_center(&tessellation.mesh)?;
let wires = tessellation
@@ -541,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)
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)
}
+13 -1
View File
@@ -1041,9 +1041,21 @@ 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),
crate::scene::model::solid_model::grip_preview_wires(
&body,
handle,
isoline_counts,
planar_isolines,
),
);
true
}
+13
View File
@@ -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