forked from 1b-agent/opencadstudio-upstream
Merge pull request #1125 from ramox81/codex/sectionplane-wireframe-clipping
Align live section wireframes and SECTIONPLANE grips
This commit is contained in:
6 files changed
+365
-23
No files matched your search
@@ -3049,7 +3049,11 @@ impl OpenCADStudio {
|
||||
return Task::none();
|
||||
};
|
||||
let grip_shape = self.tabs[i].selected_grips[grip_index].shape;
|
||||
if grip_shape == crate::scene::model::object::GripShape::Dropdown {
|
||||
if matches!(
|
||||
grip_shape,
|
||||
crate::scene::model::object::GripShape::Dropdown
|
||||
| crate::scene::model::object::GripShape::DropdownAdjacent
|
||||
) {
|
||||
use crate::entities::traits::EntityTypeOps;
|
||||
let items = self.tabs[i]
|
||||
.scene
|
||||
|
||||
+249
-11
@@ -13,13 +13,19 @@ use crate::entities::common::{
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
|
||||
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
|
||||
use crate::scene::model::object::{
|
||||
GripApply, GripDef, PropSection, PropValue, Property,
|
||||
GripApply, GripDef, GripMenuAction, GripMenuItem, PropSection, PropValue, Property,
|
||||
};
|
||||
use crate::scene::model::wire_model::SnapHint;
|
||||
|
||||
const NAN: [f64; 3] = [f64::NAN; 3];
|
||||
const SECTION_SLICE_APP: &str = "IsSlice";
|
||||
const SECTION_THICKNESS_APP: &str = "ThicknessDepth";
|
||||
const SECTION_GRIP_LEFT: usize = 100_000;
|
||||
const SECTION_GRIP_RIGHT: usize = 100_001;
|
||||
const SECTION_GRIP_TOP: usize = 100_002;
|
||||
const SECTION_GRIP_BOTTOM: usize = 100_003;
|
||||
const SECTION_GRIP_NORMAL: usize = 100_004;
|
||||
const SECTION_GRIP_STATE: usize = 100_005;
|
||||
|
||||
pub(crate) fn section_is_slice(entity: &ExtendedEntity) -> bool {
|
||||
entity
|
||||
@@ -1563,15 +1569,56 @@ fn move_extents(
|
||||
|
||||
fn grips(entity: &ExtendedEntity) -> Vec<GripDef> {
|
||||
match &entity.data {
|
||||
ExtendedEntityData::SectionObject(data) => data
|
||||
.vertices
|
||||
.iter()
|
||||
.chain(data.back_line_vertices.iter())
|
||||
.enumerate()
|
||||
.map(|(index, point)| {
|
||||
square_grip(index, glam::DVec3::new(point.x, point.y, point.z))
|
||||
})
|
||||
.collect(),
|
||||
ExtendedEntityData::SectionObject(data) => {
|
||||
let mut grips: Vec<GripDef> = if section_kind(entity, data) == "Plane"
|
||||
&& data.vertices.len() == 2
|
||||
&& data.back_line_vertices.is_empty()
|
||||
{
|
||||
section_plane_edge_grips(data)
|
||||
} else {
|
||||
data.vertices
|
||||
.iter()
|
||||
.chain(data.back_line_vertices.iter())
|
||||
.enumerate()
|
||||
.map(|(index, point)| {
|
||||
square_grip(index, glam::DVec3::new(point.x, point.y, point.z))
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
if let Some(right) = data.vertices.last() {
|
||||
let vertical = normalized(data.vertical_direction, Vector3::UNIT_Z);
|
||||
let edge_center_offset = (data.top_height - data.bottom_height) * 0.5;
|
||||
let anchor = *right + vertical * edge_center_offset;
|
||||
grips.push(GripDef {
|
||||
id: SECTION_GRIP_STATE,
|
||||
world: glam::DVec3::new(anchor.x, anchor.y, anchor.z),
|
||||
is_midpoint: false,
|
||||
shape: crate::scene::model::object::GripShape::DropdownAdjacent,
|
||||
dir: None,
|
||||
axis: None,
|
||||
});
|
||||
}
|
||||
if let Some(center) = section_center(data) {
|
||||
let normal = section_viewing_direction(data);
|
||||
let span = data
|
||||
.vertices
|
||||
.first()
|
||||
.zip(data.vertices.last())
|
||||
.map_or(1.0, |(first, last)| (*last - *first).length());
|
||||
let offset = (span * 0.08).max(1.0e-6);
|
||||
let normal_world = glam::DVec3::new(normal.x, normal.y, normal.z);
|
||||
grips.push(GripDef {
|
||||
id: SECTION_GRIP_NORMAL,
|
||||
world: glam::DVec3::new(center.x, center.y, center.z)
|
||||
- normal_world * offset,
|
||||
is_midpoint: false,
|
||||
shape: crate::scene::model::object::GripShape::Triangle,
|
||||
dir: Some(-normal_world),
|
||||
axis: Some(normal_world),
|
||||
});
|
||||
}
|
||||
grips
|
||||
}
|
||||
ExtendedEntityData::ArcAlignedText(data) => vec![center_grip(
|
||||
0,
|
||||
glam::DVec3::new(data.center.x, data.center.y, data.center.z),
|
||||
@@ -1643,7 +1690,41 @@ fn grips(entity: &ExtendedEntity) -> Vec<GripDef> {
|
||||
fn apply_grip(entity: &mut ExtendedEntity, grip_id: usize, apply: GripApply) {
|
||||
match &mut entity.data {
|
||||
ExtendedEntityData::SectionObject(data) => {
|
||||
if grip_id < data.vertices.len() {
|
||||
if matches!(
|
||||
grip_id,
|
||||
SECTION_GRIP_LEFT
|
||||
| SECTION_GRIP_RIGHT
|
||||
| SECTION_GRIP_TOP
|
||||
| SECTION_GRIP_BOTTOM
|
||||
) {
|
||||
apply_section_plane_edge_grip(data, grip_id, apply);
|
||||
} else if grip_id == SECTION_GRIP_NORMAL {
|
||||
let Some(center) = section_center(data) else {
|
||||
return;
|
||||
};
|
||||
let normal = section_viewing_direction(data);
|
||||
let span = data
|
||||
.vertices
|
||||
.first()
|
||||
.zip(data.vertices.last())
|
||||
.map_or(1.0, |(first, last)| (*last - *first).length());
|
||||
let offset = (span * 0.08).max(1.0e-6);
|
||||
let current = center - normal * offset;
|
||||
let delta = match apply {
|
||||
GripApply::Translate(delta) => Vector3::new(delta.x, delta.y, delta.z),
|
||||
GripApply::Absolute(position) => {
|
||||
Vector3::new(position.x, position.y, position.z) - current
|
||||
}
|
||||
};
|
||||
let constrained = normal * delta.dot(&normal);
|
||||
for point in data
|
||||
.vertices
|
||||
.iter_mut()
|
||||
.chain(data.back_line_vertices.iter_mut())
|
||||
{
|
||||
*point = *point + constrained;
|
||||
}
|
||||
} else if grip_id < data.vertices.len() {
|
||||
apply_point(&mut data.vertices[grip_id], apply);
|
||||
} else if let Some(point) = data
|
||||
.back_line_vertices
|
||||
@@ -1704,6 +1785,115 @@ fn apply_grip(entity: &mut ExtendedEntity, grip_id: usize, apply: GripApply) {
|
||||
}
|
||||
}
|
||||
|
||||
fn section_center(data: &SectionObjectData) -> Option<Vector3> {
|
||||
let (first, last) = data.vertices.first().zip(data.vertices.last())?;
|
||||
Some((*first + *last) * 0.5)
|
||||
}
|
||||
|
||||
fn section_plane_edge_grips(data: &SectionObjectData) -> Vec<GripDef> {
|
||||
let Some((first, last)) = data.vertices.first().zip(data.vertices.last()) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let center = (*first + *last) * 0.5;
|
||||
let tangent = section_tangent(data);
|
||||
let vertical = normalized(data.vertical_direction, Vector3::UNIT_Z);
|
||||
let edge_center_offset = (data.top_height - data.bottom_height) * 0.5;
|
||||
let definitions = [
|
||||
(
|
||||
SECTION_GRIP_LEFT,
|
||||
*first + vertical * edge_center_offset,
|
||||
vertical,
|
||||
None,
|
||||
),
|
||||
(
|
||||
SECTION_GRIP_RIGHT,
|
||||
*last + vertical * edge_center_offset,
|
||||
vertical,
|
||||
None,
|
||||
),
|
||||
(
|
||||
SECTION_GRIP_TOP,
|
||||
center + vertical * data.top_height,
|
||||
tangent,
|
||||
Some(vertical),
|
||||
),
|
||||
(
|
||||
SECTION_GRIP_BOTTOM,
|
||||
center - vertical * data.bottom_height,
|
||||
tangent,
|
||||
Some(vertical),
|
||||
),
|
||||
];
|
||||
definitions
|
||||
.into_iter()
|
||||
.map(|(id, world, edge, axis)| GripDef {
|
||||
id,
|
||||
world: glam::DVec3::new(world.x, world.y, world.z),
|
||||
is_midpoint: false,
|
||||
shape: crate::scene::model::object::GripShape::Rectangle,
|
||||
dir: Some(glam::DVec3::new(edge.x, edge.y, edge.z)),
|
||||
axis: axis.map(|axis| glam::DVec3::new(axis.x, axis.y, axis.z)),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn apply_section_plane_edge_grip(
|
||||
data: &mut SectionObjectData,
|
||||
grip_id: usize,
|
||||
apply: GripApply,
|
||||
) {
|
||||
let Some((first, last)) = data.vertices.first().zip(data.vertices.last()) else {
|
||||
return;
|
||||
};
|
||||
let center = (*first + *last) * 0.5;
|
||||
let tangent = section_tangent(data);
|
||||
let vertical = normalized(data.vertical_direction, Vector3::UNIT_Z);
|
||||
let edge_center_offset = (data.top_height - data.bottom_height) * 0.5;
|
||||
let current = match grip_id {
|
||||
SECTION_GRIP_LEFT => *first + vertical * edge_center_offset,
|
||||
SECTION_GRIP_RIGHT => *last + vertical * edge_center_offset,
|
||||
SECTION_GRIP_TOP => center + vertical * data.top_height,
|
||||
SECTION_GRIP_BOTTOM => center - vertical * data.bottom_height,
|
||||
_ => return,
|
||||
};
|
||||
let delta = match apply {
|
||||
GripApply::Translate(delta) => Vector3::new(delta.x, delta.y, delta.z),
|
||||
GripApply::Absolute(position) => {
|
||||
Vector3::new(position.x, position.y, position.z) - current
|
||||
}
|
||||
};
|
||||
let span = (*last - *first).length();
|
||||
let minimum_span = 1.0e-6;
|
||||
let in_plane_vertical = normalized(
|
||||
vertical - tangent * vertical.dot(&tangent),
|
||||
vertical,
|
||||
);
|
||||
let vertical_change = in_plane_vertical * delta.dot(&in_plane_vertical);
|
||||
match grip_id {
|
||||
SECTION_GRIP_LEFT => {
|
||||
let tangent_change = delta
|
||||
.dot(&tangent)
|
||||
.min((span - minimum_span).max(0.0));
|
||||
data.vertices[0] = data.vertices[0] + tangent * tangent_change + vertical_change;
|
||||
}
|
||||
SECTION_GRIP_RIGHT => {
|
||||
let last = data.vertices.len() - 1;
|
||||
let tangent_change = delta
|
||||
.dot(&tangent)
|
||||
.max((-span + minimum_span).min(0.0));
|
||||
data.vertices[last] =
|
||||
data.vertices[last] + tangent * tangent_change + vertical_change;
|
||||
}
|
||||
SECTION_GRIP_TOP => {
|
||||
data.top_height = (data.top_height + delta.dot(&vertical)).max(0.0);
|
||||
}
|
||||
SECTION_GRIP_BOTTOM => {
|
||||
data.bottom_height = (data.bottom_height - delta.dot(&vertical)).max(0.0);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn entity_transform(transform: &EntityTransform) -> Transform {
|
||||
match transform {
|
||||
EntityTransform::Translate(delta) => {
|
||||
@@ -1943,6 +2133,54 @@ impl Grippable for ExtendedEntity {
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
apply_grip(self, grip_id, apply);
|
||||
}
|
||||
|
||||
fn grip_menu(&self, grip_id: usize) -> Vec<GripMenuItem> {
|
||||
if grip_id != SECTION_GRIP_STATE {
|
||||
return Vec::new();
|
||||
}
|
||||
let ExtendedEntityData::SectionObject(data) = &self.data else {
|
||||
return Vec::new();
|
||||
};
|
||||
let current = section_kind(self, data);
|
||||
vec![
|
||||
GripMenuItem {
|
||||
label: if current == "Plane" { "✓ Plane" } else { "Plane" },
|
||||
action: GripMenuAction::SectionPlane,
|
||||
},
|
||||
GripMenuItem {
|
||||
label: if current == "Slice" { "✓ Slice" } else { "Slice" },
|
||||
action: GripMenuAction::SectionSlice,
|
||||
},
|
||||
GripMenuItem {
|
||||
label: if current == "Boundary" {
|
||||
"✓ Boundary"
|
||||
} else {
|
||||
"Boundary"
|
||||
},
|
||||
action: GripMenuAction::SectionBoundary,
|
||||
},
|
||||
GripMenuItem {
|
||||
label: if current == "Volume" { "✓ Volume" } else { "Volume" },
|
||||
action: GripMenuAction::SectionVolume,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn apply_grip_menu(&mut self, grip_id: usize, action: GripMenuAction) {
|
||||
if grip_id != SECTION_GRIP_STATE
|
||||
|| !matches!(self.data, ExtendedEntityData::SectionObject(_))
|
||||
{
|
||||
return;
|
||||
}
|
||||
let value = match action {
|
||||
GripMenuAction::SectionPlane => "Plane",
|
||||
GripMenuAction::SectionSlice => "Slice",
|
||||
GripMenuAction::SectionBoundary => "Boundary",
|
||||
GripMenuAction::SectionVolume => "Volume",
|
||||
_ => return,
|
||||
};
|
||||
apply_section_prop(self, "ext_section_state", value);
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyEditable for ExtendedEntity {
|
||||
|
||||
+90
-5
@@ -5410,8 +5410,12 @@ impl Scene {
|
||||
// changed entities into it, instead of re-cloning + re-sorting the whole
|
||||
// set. Falls back to the full build below when it can't (no cache entry,
|
||||
// journal un-replayable, or a structural assumption violated).
|
||||
if let Some(arc) =
|
||||
self.try_resident_patch(
|
||||
// A live section changes the displayed wires of every intersected
|
||||
// solid, even when the delta journal contains only the section entity.
|
||||
// Rebuild the resident set in that case so an incremental patch cannot
|
||||
// retain the source solid's uncut edge cache.
|
||||
if self.active_live_section(block).is_none() {
|
||||
if let Some(arc) = self.try_resident_patch(
|
||||
key,
|
||||
block,
|
||||
bg,
|
||||
@@ -5420,9 +5424,9 @@ impl Scene {
|
||||
all_visible,
|
||||
frozen_layers,
|
||||
style_viewport,
|
||||
)
|
||||
{
|
||||
return arc;
|
||||
) {
|
||||
return arc;
|
||||
}
|
||||
}
|
||||
// Build once: full tessellation, no cull, no zoom LOD — the resident
|
||||
// set is zoom-independent (GPU analytical circles/arcs/ellipses).
|
||||
@@ -6996,6 +7000,48 @@ impl Scene {
|
||||
Self::section_body(&body, §ion.data, section.slice_depth, None)
|
||||
}
|
||||
|
||||
/// Build a temporary entity whose wire data matches the live-section body.
|
||||
/// The document entity stays unchanged so disabling the section restores
|
||||
/// the complete source geometry and saving never persists the clipped body.
|
||||
fn sectioned_wire_entity(
|
||||
&self,
|
||||
handle: Handle,
|
||||
section: &LiveSection,
|
||||
) -> Result<Option<EntityType>, ()> {
|
||||
let Some(body) = self.sectioned_body(handle, section)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let (_, wires, center) = self
|
||||
.prepare_solid_model_display(handle, &body)
|
||||
.ok_or(())?;
|
||||
let mut entity = self.document.get_entity(handle).cloned().ok_or(())?;
|
||||
let reference = acadrust::types::Vector3::new(center[0], center[1], center[2]);
|
||||
match &mut entity {
|
||||
EntityType::Solid3D(solid) => {
|
||||
solid.point_of_reference = reference;
|
||||
solid.wires = wires;
|
||||
solid.silhouettes.clear();
|
||||
}
|
||||
EntityType::Surface(surface) => {
|
||||
surface.point_of_reference = reference;
|
||||
surface.wires = wires;
|
||||
surface.silhouettes.clear();
|
||||
}
|
||||
EntityType::Region(region) => {
|
||||
region.point_of_reference = reference;
|
||||
region.wires = wires;
|
||||
region.silhouettes.clear();
|
||||
}
|
||||
EntityType::Body(body) => {
|
||||
body.point_of_reference = reference;
|
||||
body.wires = wires;
|
||||
body.silhouettes.clear();
|
||||
}
|
||||
_ => return Err(()),
|
||||
}
|
||||
Ok(Some(entity))
|
||||
}
|
||||
|
||||
fn section_source_body(
|
||||
&self,
|
||||
handle: Handle,
|
||||
@@ -9709,6 +9755,45 @@ impl Scene {
|
||||
memo_misses = visible_count;
|
||||
out
|
||||
};
|
||||
|
||||
// Normal Wireframe renders the persisted entity wires, whereas shaded
|
||||
// and 3D Wireframe modes render the temporary sectioned mesh. Replace
|
||||
// only the displayed wire run with one tessellated from that same
|
||||
// temporary body so every visual style shows the identical live cut.
|
||||
if let Some(section) = self.active_live_section(block_handle) {
|
||||
let handles: Vec<Handle> = self.meshes.keys().copied().collect();
|
||||
for handle in handles {
|
||||
let Some(source) = self.document.get_entity(handle) else {
|
||||
continue;
|
||||
};
|
||||
if !visibility_ok(source) {
|
||||
continue;
|
||||
}
|
||||
let replacement = match self.sectioned_wire_entity(handle, §ion) {
|
||||
Ok(Some(entity)) => Some(tessellate_entity(
|
||||
doc,
|
||||
sel,
|
||||
avp,
|
||||
bg,
|
||||
anno,
|
||||
annotation_scale_handle,
|
||||
&entity,
|
||||
Some(blk_ref),
|
||||
view_aabb,
|
||||
wpp,
|
||||
paper,
|
||||
)),
|
||||
Ok(None) => Some(Vec::new()),
|
||||
Err(()) => None,
|
||||
};
|
||||
let Some(replacement) = replacement else {
|
||||
continue;
|
||||
};
|
||||
let name = handle.value().to_string();
|
||||
wires.retain(|wire| wire.name != name);
|
||||
wires.extend(replacement);
|
||||
}
|
||||
}
|
||||
let build_ms = crate::perf::elapsed_ms(t_build);
|
||||
|
||||
// Resolve display colours for the live background. Memo hits were
|
||||
|
||||
@@ -98,6 +98,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.
|
||||
@@ -180,6 +182,10 @@ pub enum GripMenuAction {
|
||||
RefineVertices,
|
||||
ShowFit,
|
||||
ShowControlVertices,
|
||||
SectionPlane,
|
||||
SectionSlice,
|
||||
SectionBoundary,
|
||||
SectionVolume,
|
||||
MoveWithText,
|
||||
StackText,
|
||||
UnstackText,
|
||||
|
||||
+10
-3
@@ -13,11 +13,18 @@ pub const GRIP_HALF_PX: f32 = 5.0;
|
||||
/// Screen offset for dropdown selectors anchored to a vertex.
|
||||
pub const GRIP_DROPDOWN_OFFSET_X_PX: f32 = 24.0;
|
||||
pub const GRIP_DROPDOWN_OFFSET_Y_PX: f32 = 27.0;
|
||||
pub const GRIP_ADJACENT_DROPDOWN_OFFSET_X_PX: f32 = 12.0;
|
||||
|
||||
fn marker_screen_position(mut point: Point, shape: GripShape) -> Point {
|
||||
if shape == GripShape::Dropdown {
|
||||
point.x += GRIP_DROPDOWN_OFFSET_X_PX;
|
||||
point.y += GRIP_DROPDOWN_OFFSET_Y_PX;
|
||||
match shape {
|
||||
GripShape::Dropdown => {
|
||||
point.x += GRIP_DROPDOWN_OFFSET_X_PX;
|
||||
point.y += GRIP_DROPDOWN_OFFSET_Y_PX;
|
||||
}
|
||||
GripShape::DropdownAdjacent => {
|
||||
point.x += GRIP_ADJACENT_DROPDOWN_OFFSET_X_PX;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
point
|
||||
}
|
||||
|
||||
+5
-3
@@ -781,7 +781,7 @@ fn draw_grip_marker(
|
||||
b.close();
|
||||
}),
|
||||
GripShape::Circle => canvas::Path::circle(Point::new(sp.x, sp.y), h),
|
||||
GripShape::Dropdown => canvas::Path::new(|b| {
|
||||
GripShape::Dropdown | GripShape::DropdownAdjacent => canvas::Path::new(|b| {
|
||||
b.move_to(Point::new(sp.x - h, sp.y - h * 0.5));
|
||||
b.line_to(Point::new(sp.x + h, sp.y - h * 0.5));
|
||||
b.line_to(Point::new(sp.x, sp.y + h));
|
||||
@@ -831,7 +831,10 @@ fn draw_grip_marker(
|
||||
} else {
|
||||
palette.primary.base.color
|
||||
};
|
||||
let fill = if grip.shape == GripShape::Dropdown {
|
||||
let fill = if matches!(
|
||||
grip.shape,
|
||||
GripShape::Dropdown | GripShape::DropdownAdjacent
|
||||
) {
|
||||
color
|
||||
} else {
|
||||
palette.background.base.color.scale_alpha(0.7)
|
||||
@@ -3797,4 +3800,3 @@ mod selection_visual_color_tests {
|
||||
assert!((selection_fill_alpha(100.0, true) - 0.45).abs() < 1e-5);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user