fix(section): align live section wireframe and grips

This commit is contained in:
ramox81 committed 2026-09-10 10:42:19 +03:00
1 parent b19992cbcb
commit 78bbd1065a
2 files changed
+270 -15

No files matched your search

+180 -10
View File
@@ -20,6 +20,11 @@ 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;
pub(crate) fn section_is_slice(entity: &ExtendedEntity) -> bool {
entity
@@ -1563,15 +1568,43 @@ 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(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 +1676,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 +1771,109 @@ 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,
tangent,
),
(
SECTION_GRIP_RIGHT,
*last + vertical * edge_center_offset,
vertical,
tangent,
),
(
SECTION_GRIP_TOP,
center + vertical * data.top_height,
tangent,
vertical,
),
(
SECTION_GRIP_BOTTOM,
center - vertical * data.bottom_height,
tangent,
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: Some(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;
match grip_id {
SECTION_GRIP_LEFT => {
let change = delta
.dot(&tangent)
.min((span - minimum_span).max(0.0));
data.vertices[0] = data.vertices[0] + tangent * change;
}
SECTION_GRIP_RIGHT => {
let last = data.vertices.len() - 1;
let change = delta
.dot(&tangent)
.max((-span + minimum_span).min(0.0));
data.vertices[last] = data.vertices[last] + tangent * 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) => {
+90 -5
View File
@@ -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, &section.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, &section) {
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