forked from 1b-agent/opencadstudio-upstream
Merge remote-tracking branch 'origin/main' into merge-test/main-into-constraints
# Conflicts: # src/app/command_driver.rs # src/command.rs # src/ui/ribbon/mod.rs # src/ui/window/options.rs
This commit is contained in:
commit
ad718def3f
124 files changed
+9866
-692
No files matched your search
@@ -120,6 +120,8 @@ pub enum GripShape {
|
||||
Circle,
|
||||
/// Screen-offset menu selector.
|
||||
Dropdown,
|
||||
/// Menu selector placed immediately beside its anchor grip.
|
||||
DropdownAdjacent,
|
||||
}
|
||||
|
||||
/// Describes one grip point for an entity.
|
||||
@@ -202,6 +204,10 @@ pub enum GripMenuAction {
|
||||
RefineVertices,
|
||||
ShowFit,
|
||||
ShowControlVertices,
|
||||
SectionPlane,
|
||||
SectionSlice,
|
||||
SectionBoundary,
|
||||
SectionVolume,
|
||||
MoveWithText,
|
||||
StackText,
|
||||
UnstackText,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -558,6 +584,23 @@ mod tests {
|
||||
assert!(tri_count(&pyramid_solid(c, 5.0, 9.0, 6).unwrap()) >= 8, "pyramid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn planar_display_uses_independent_isoline_counts() {
|
||||
let body = box_solid([0.0; 3], 10.0, 10.0, 10.0).unwrap();
|
||||
let display = |counts, planar| {
|
||||
display_from_solid(&body, [0.7, 0.7, 0.7, 1.0], 1.0, None, counts, planar)
|
||||
.unwrap()
|
||||
.0
|
||||
.edge_verts
|
||||
.len()
|
||||
};
|
||||
let boundaries = display([0, 0], false);
|
||||
|
||||
assert_eq!(display([1, 0], true) - boundaries, 12);
|
||||
assert_eq!(display([0, 2], true) - boundaries, 24);
|
||||
assert_eq!(display([2, 2], false), boundaries);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_primitive_is_the_size_it_was_asked_for() {
|
||||
// Triangle counts say a mesh exists; the volume says it is the right
|
||||
|
||||
Reference in new issue
Block a user