From f6455b22eb6ff555dff1eebc7fa55def21b70e2d Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 16 Oct 2026 02:11:16 +0300 Subject: [PATCH] fix(view): Zoom Extents and All frame a drawing placed at survey coordinates (#1829) A block whose content is only 3D solids leaves a marker wire carrying its insertion snap, and that point counted as drawing extent: a georeferenced block inserted at the origin made ZOOM E frame the origin and the model together, 860 km wide. Extents now skip those markers; the solids count through their meshes. The marker's snap also took the insertion point straight from the OCS, so a mirrored insert's snap sat on the far side of the origin. ZOOM All fitted the limits alone. In a 3D view it is now Zoom Extents, and in plan it frames the limits together with the extents. Instanced solids summed their rotated point and translation in f32 before meeting the eye, which cost up to half a metre at survey coordinates; the large terms now cancel first. Co-Authored-By: Claude Opus 5.5 --- src/entities/insert.rs | 8 +++----- src/scene/camera_ops.rs | 17 ++++++++++++++--- src/scene/convert/tess.rs | 10 ++++------ src/scene/limits.rs | 27 +++++++++++++++++++++++---- src/shaders/mesh.wgsl | 24 +++++++++++++++++------- 5 files changed, 61 insertions(+), 25 deletions(-) diff --git a/src/entities/insert.rs b/src/entities/insert.rs index 9b9b06da6..2cb3de224 100644 --- a/src/entities/insert.rs +++ b/src/entities/insert.rs @@ -329,11 +329,9 @@ crate::impl_entity_basics!(Insert); impl crate::entities::traits::FallbackTess for Insert { fn fallback_geometry(&self) -> crate::scene::convert::tess_util::FallbackGeometry { - let (ipx, ipy, ipz) = ( - self.insert_point.x, - self.insert_point.y, - self.insert_point.z, - ); + // `insert_point` is in the OCS of `normal`. + let w = Matrix3::arbitrary_axis(self.normal) * self.insert_point; + let (ipx, ipy, ipz) = (w.x, w.y, w.z); let ip = Vec3::new(ipx as f32, ipy as f32, ipz as f32); let s = 0.1_f64; let pts = vec![ diff --git a/src/scene/camera_ops.rs b/src/scene/camera_ops.rs index 204ea758e..62d728ded 100644 --- a/src/scene/camera_ops.rs +++ b/src/scene/camera_ops.rs @@ -1722,9 +1722,20 @@ impl Scene { // Keep their base points as a fallback for otherwise empty drawings. let mut infinite_base: Vec = Vec::new(); for wire in wires.iter() { - let is_infinite = Self::handle_from_wire_name(&wire.name) - .and_then(|handle| self.document.get_entity(handle)) - .is_some_and(|entity| matches!(entity, EntityType::XLine(_) | EntityType::Ray(_))); + let entity = Self::handle_from_wire_name(&wire.name) + .and_then(|handle| self.document.get_entity(handle)); + // A block whose content draws no wire (3D solids only) leaves a + // marker carrying just its insertion snap. The content counts + // through its meshes; the insertion point is no geometry, and a + // georeferenced block's sits far from what it draws (#1829). + if matches!(entity, Some(EntityType::Insert(_))) + && wire.points.is_empty() + && wire.text_verts.is_empty() + { + continue; + } + let is_infinite = + entity.is_some_and(|entity| matches!(entity, EntityType::XLine(_) | EntityType::Ray(_))); if is_infinite { infinite_base.extend( wire.key_vertices diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index 87f2630b3..688058b74 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -1185,11 +1185,9 @@ fn tessellate_entity_inner( } if let EntityType::Insert(ins) = e { - let ip = glam::Vec3::new( - (ins.insert_point.x) as f32, - (ins.insert_point.y) as f32, - (ins.insert_point.z) as f32, - ); + // `insert_point` is in the OCS of `normal`. + let w = codec::types::Matrix3::arbitrary_axis(ins.normal) * ins.insert_point; + let ip = glam::DVec3::new(w.x, w.y, w.z); let marker = WireModel { bg_adapt: None, point_marker: None, @@ -1217,7 +1215,7 @@ fn tessellate_entity_inner( pattern_length: 0.0, pattern: [0.0; 8], line_weight_px: 1.0, - snap_pts: vec![(ip.as_dvec3(), model::wire_model::SnapHint::Insertion)], + snap_pts: vec![(ip, model::wire_model::SnapHint::Insertion)], tangent_geoms: vec![], key_vertices: vec![], aabb: WireModel::UNBOUNDED_AABB, diff --git a/src/scene/limits.rs b/src/scene/limits.rs index 52ddaa62e..b15e95c6e 100644 --- a/src/scene/limits.rs +++ b/src/scene/limits.rs @@ -120,16 +120,35 @@ impl Scene { .remove(&self.current_layout); } - /// ZOOM All frames the configured drawing limits. Object-only framing - /// remains the responsibility of ZOOM Extents. + /// ZOOM All frames the drawing limits together with what the drawing + /// draws. pub fn fit_all_with_limits(&mut self) { let Some((limit_min, limit_max)) = self.current_drawing_limits() else { self.fit_all(); return; }; + // In a 3D view ZOOM All is Zoom Extents. In plan it shows the larger + // of the limits and the extents, so geometry outside the limits (a + // drawing placed at survey coordinates) stays on screen (#1829). + // ponytail: plan means looking down world Z; a rotated UCS plan view + // counts as 3D and gets Zoom Extents. + let eye = self.camera.borrow().rotation * glam::Vec3::Z; + if self.active_viewport.is_none() && eye.z.abs() < 0.999 { + self.fit_all(); + return; + } - let min = glam::Vec3::new(limit_min.x as f32, limit_min.y as f32, 0.0); - let max = glam::Vec3::new(limit_max.x as f32, limit_max.y as f32, 0.0); + let mut min = glam::Vec3::new(limit_min.x as f32, limit_min.y as f32, 0.0); + let mut max = glam::Vec3::new(limit_max.x as f32, limit_max.y as f32, 0.0); + let block = if self.current_layout == "Model" || self.active_viewport.is_some() { + self.model_space_block_handle() + } else { + self.current_layout_block_handle() + }; + if let Some((lo, hi)) = self.visible_block_bounds(block) { + min = min.min(lo.as_vec3()); + max = max.max(hi.as_vec3()); + } // MSPACE owns a camera encoded on the active viewport entity. if self.active_viewport.is_some() { diff --git a/src/shaders/mesh.wgsl b/src/shaders/mesh.wgsl index dbf2e6299..830cb7d2b 100644 --- a/src/shaders/mesh.wgsl +++ b/src/shaders/mesh.wgsl @@ -153,17 +153,27 @@ fn relative_position( position_low: vec3, instance: MeshInstance, ) -> vec3 { - let world_high = vec3( - dot(instance.model_row_0.xyz, position) + instance.model_row_0.w, - dot(instance.model_row_1.xyz, position) + instance.model_row_1.w, - dot(instance.model_row_2.xyz, position) + instance.model_row_2.w, + // Large terms cancel before small ones join: the translation against the + // eye first, then the rotated point, so neither a survey-placed instance + // nor a block drawn at survey coordinates loses its low bits to an f32 + // sum (it cost up to half a metre). + let translation_high = vec3( + instance.model_row_0.w, + instance.model_row_1.w, + instance.model_row_2.w, ); - let world_low = vec3( + let rotated_high = vec3( + dot(instance.model_row_0.xyz, position), + dot(instance.model_row_1.xyz, position), + dot(instance.model_row_2.xyz, position), + ); + let rotated_low = vec3( dot(instance.model_row_0.xyz, position_low), dot(instance.model_row_1.xyz, position_low), dot(instance.model_row_2.xyz, position_low), - ) + instance.translation_low.xyz; - return (world_high - u.eye_high) + (world_low - u.eye_low); + ); + return ((translation_high - u.eye_high) + rotated_high) + + (rotated_low + instance.translation_low.xyz - u.eye_low); } fn mesh_instance(input: InstanceIn) -> MeshInstance {