forked from 1b-agent/opencadstudio-upstream
Merge pull request #1131 from ramox81/codex/thicken-surface-grips
This commit is contained in:
commit
4201b0a031
18 files changed
+536
-67
No files matched your search
Generated
+1
-1
@@ -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
@@ -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"] }
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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());
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
®ion.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
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+13
-1
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user