feat(find): add find and replace

Closes #583
This commit is contained in:
Hakan Seven committed 2026-07-30 17:56:54 +03:00
1 parent 4af141ebec
commit c9aab122f0
10 files changed
+814 -7

No files matched your search

+216
View File
@@ -3,6 +3,97 @@
// hit-testing lives in `scene::pick::hit_test`.)
use super::*;
fn rendered_wire_center(wires: &[WireModel], fallback_z: f64) -> Option<glam::DVec3> {
let mut min = glam::DVec3::splat(f64::INFINITY);
let mut max = glam::DVec3::splat(f64::NEG_INFINITY);
let mut include = |point: glam::DVec3| {
if point.is_finite() {
min = min.min(point);
max = max.max(point);
}
};
for wire in wires {
let mut has_precise_position = false;
for (index, &[x, y, z]) in wire.points.iter().enumerate() {
let low = wire.points_low.get(index).copied().unwrap_or([0.0; 3]);
include(glam::DVec3::new(
x as f64 + low[0] as f64,
y as f64 + low[1] as f64,
z as f64 + low[2] as f64,
));
has_precise_position = true;
}
for vertex in &wire.text_verts {
include(glam::DVec3::new(
vertex.pos[0] as f64 + vertex.pos_low[0] as f64,
vertex.pos[1] as f64 + vertex.pos_low[1] as f64,
vertex.pos[2] as f64 + vertex.pos_low[2] as f64,
));
has_precise_position = true;
}
for &[x, y, z] in &wire.key_vertices {
include(glam::DVec3::new(x, y, z));
has_precise_position = true;
}
for &(point, _) in &wire.snap_pts {
include(point);
has_precise_position = true;
}
if !has_precise_position {
let [x0, y0, x1, y1] = wire.aabb;
if wire.aabb != WireModel::UNBOUNDED_AABB
&& [x0, y0, x1, y1].iter().all(|value| value.is_finite())
&& x0 <= x1
&& y0 <= y1
{
include(glam::DVec3::new(x0 as f64, y0 as f64, fallback_z));
include(glam::DVec3::new(x1 as f64, y1 as f64, fallback_z));
}
}
}
if min.is_finite() && max.is_finite() {
Some((min + max) * 0.5)
} else {
None
}
}
fn block_entity_transform(
document: &CadDocument,
block_name: &str,
target: Handle,
visited: &mut Vec<String>,
) -> Option<acadrust::types::Transform> {
if visited
.iter()
.any(|name| name.eq_ignore_ascii_case(block_name))
{
return None;
}
let record = document
.block_records
.iter()
.find(|record| record.name.eq_ignore_ascii_case(block_name))?;
if record.entity_handles.contains(&target) {
return Some(acadrust::types::Transform::identity());
}
visited.push(record.name.clone());
let transform = record.entity_handles.iter().find_map(|handle| {
let EntityType::Insert(insert) = document.get_entity(*handle)? else {
return None;
};
let inner =
block_entity_transform(document, &insert.block_name, target, visited)?;
Some(inner.then(&insert.get_transform()))
});
visited.pop();
transform
}
/// The model-space active viewport is reserved as `*Active`, but that name is
/// case-insensitive in DXF/DWG — a file may store it as `*ACTIVE`. Match it
/// accordingly, otherwise an uppercased record reads as a *distinct* viewport:
@@ -131,6 +222,131 @@ impl Scene {
self.camera_generation += 1;
}
/// Centre the active camera on one rendered entity without changing zoom
/// distance, orientation, or viewport scale.
pub fn center_camera_on_entity(&mut self, handle: Handle) -> bool {
let fallback_z = self.active_camera_target().z;
let wires = self.wire_models_for(&[handle]);
let Some(center) = rendered_wire_center(&wires, fallback_z) else {
return false;
};
self.center_active_camera_on(center)
}
/// Centre on one concrete attribute value attached to an INSERT rather
/// than on the complete block extents.
pub fn center_camera_on_insert_attribute(&mut self, insert: Handle, index: usize) -> bool {
let (attribute, fallback) = {
let Some(EntityType::Insert(entity)) = self.document.get_entity(insert) else {
return false;
};
let Some(attribute) = entity.attributes.get(index) else {
return false;
};
(
EntityType::AttributeEntity(attribute.clone()),
glam::DVec3::new(
attribute.insertion_point.x,
attribute.insertion_point.y,
attribute.insertion_point.z,
),
)
};
let wires = self.tessellate_one(&attribute);
let center = rendered_wire_center(&wires, fallback.z).unwrap_or(fallback);
self.center_active_camera_on(center)
}
/// Centre on a text entity stored in a block definition, transformed
/// through the concrete visible INSERT occurrence that FIND is visiting.
pub fn center_camera_on_block_entity(&mut self, insert: Handle, entity: Handle) -> bool {
let (source, mut transform) = {
let Some(EntityType::Insert(insert_entity)) = self.document.get_entity(insert) else {
return false;
};
let Some(source) = self.document.get_entity(entity) else {
return false;
};
let mut visited = Vec::new();
let Some(inner) = block_entity_transform(
&self.document,
&insert_entity.block_name,
entity,
&mut visited,
) else {
return false;
};
(
source.clone(),
inner.then(&insert_entity.get_transform()),
)
};
if (self.annotation_scale - 1.0).abs() > 1e-6 {
let Some(EntityType::Insert(insert_entity)) = self.document.get_entity(insert) else {
return false;
};
if insert_entity
.common
.extended_data
.get_record("AcAnnotativeData")
.is_some()
{
let point = insert_entity.insert_point;
let scale_about =
acadrust::types::Transform::from_translation(acadrust::types::Vector3::new(
-point.x, -point.y, -point.z,
))
.then(&acadrust::types::Transform::from_scale(
self.annotation_scale as f64,
))
.then(&acadrust::types::Transform::from_translation(
acadrust::types::Vector3::new(point.x, point.y, point.z),
));
transform = transform.then(&scale_about);
}
}
let fallback_z = self.active_camera_target().z;
let wires = self.tessellate_one(&source);
let Some(local_center) = rendered_wire_center(&wires, fallback_z) else {
return false;
};
let world = transform.apply(acadrust::types::Vector3::new(
local_center.x,
local_center.y,
local_center.z,
));
self.center_active_camera_on(glam::DVec3::new(world.x, world.y, world.z))
}
fn active_camera_target(&self) -> glam::DVec3 {
self.active_viewport
.and_then(|handle| self.camera_for_viewport(handle))
.map_or_else(|| self.camera.borrow().target, |camera| camera.target)
}
fn center_active_camera_on(&mut self, center: glam::DVec3) -> bool {
if !center.is_finite() {
return false;
}
if let Some(handle) = self.active_viewport {
let Some(EntityType::Viewport(viewport)) = self.document.get_entity_mut(handle) else {
return false;
};
if viewport.status.locked {
return false;
}
viewport.view_target.x = center.x;
viewport.view_target.y = center.y;
viewport.view_target.z = center.z;
} else {
self.camera.borrow_mut().target = center;
}
self.camera_generation += 1;
true
}
/// Apply camera state from an acadrust View table entry, through the shared
/// `camera_from_view` decoder so the twist round-trips like every other
/// saved view. `model_space`: if true, subtracts world_offset from target