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 {