diff --git a/src/entities/extended.rs b/src/entities/extended.rs index 38fe46ddd..f79e5163b 100644 --- a/src/entities/extended.rs +++ b/src/entities/extended.rs @@ -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 { 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 = 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 { 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 { + let (first, last) = data.vertices.first().zip(data.vertices.last())?; + Some((*first + *last) * 0.5) +} + +fn section_plane_edge_grips(data: &SectionObjectData) -> Vec { + 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) => { diff --git a/src/scene/mod.rs b/src/scene/mod.rs index f136d16e3..ac87040ed 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -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, ()> { + 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 = 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