Merge main into codex/graphic-attributes-cleanup

Resolve conflicts from main's rustfmt pass by taking main's versions of
command.rs, style.rs, view/mod.rs, scene/mod.rs, ui/window/mod.rs and
ui/properties.rs and re-applying this branch's Graphic Attributes changes.
Add the Graphic Attributes strings to the new lo-LA catalog.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01E1QnME6WJbD5iGFae1eeCb
This commit is contained in:
Claude committed 2026-10-11 19:09:37 +00:00
commit 44f8f76ab3
596 files changed
+69069 -34120

No files matched your search

+387 -123
View File
@@ -36,18 +36,17 @@ pub use dimension_assoc::{ReferenceStatus, ResolvedReference};
pub use page_setup::{apply_default_page_setup, document_page_setups, rotated_margins};
mod dwg_native_constraints;
mod entity;
pub use entity::CreateBlockOptions;
pub use camera_ops::named_view_center;
pub(crate) use entity::gradient_tint_color;
pub use entity::CreateBlockOptions;
mod group_layer;
#[cfg(test)]
mod hatch_boundary;
mod group_layer;
mod layout;
mod limits;
mod modify;
pub(crate) use modify::remap_copied_reactors;
mod mspace;
pub mod viewport_ref;
pub mod viewport_dimension_pick;
pub mod named_parameters;
mod page_setup;
mod paper;
@@ -55,6 +54,8 @@ mod preview;
mod project;
mod scene_markers;
mod selection;
pub mod viewport_dimension_pick;
pub mod viewport_ref;
pub(crate) use selection::{pe_url_description_of, pe_url_of};
pub mod parametric_constraints;
mod parametric_solve;
@@ -112,9 +113,9 @@ pub(crate) fn tessellate_entity(
// including holes, then sample only for this cached display.
let mut boundaries = Vec::new();
if let Ok(kernel::brep::LoftSection::Profile { plane, wires, .. }) =
kernel::acis::loft_section_geometry(&[
codec::entities::EmbeddedEntity::Region(region.clone()),
])
kernel::acis::loft_section_geometry(&[codec::entities::EmbeddedEntity::Region(
region.clone(),
)])
{
let (color, _, _, line_weight_px, _) =
view::render::render_style_for_viewport(document, &source, active_viewport);
@@ -166,7 +167,12 @@ pub(crate) fn tessellate_entity(
for points in model::solid_history::composite_history_edges(document, handle) {
let selected = selected.contains(&handle);
let color = [1.0, 0.25, 0.25, 1.0];
wires.push(WireModel::solid_f64(handle.value().to_string(), points, color, selected));
wires.push(WireModel::solid_f64(
handle.value().to_string(),
points,
color,
selected,
));
}
}
wires
@@ -209,9 +215,7 @@ impl Scene {
{
let (edges, lows) = set.geometry_edges();
let mut seen = HashSet::default();
for (a, b) in
crate::scene::model::mesh_model::solid_edge_segments(edges, lows)
{
for (a, b) in crate::scene::model::mesh_model::solid_edge_segments(edges, lows) {
for (point, hint) in [(a, SnapHint::Vertex), (b, SnapHint::Vertex)] {
let key = [point.x.to_bits(), point.y.to_bits(), point.z.to_bits()];
if seen.insert(key) {
@@ -310,8 +314,8 @@ impl Scene {
want_nearest: bool,
) -> Option<crate::snap::SnapResult> {
use crate::snap::{
closest_point_on_tri_2d, foot_on_triangle, snap_better, snap_priority,
snap_tier, SnapType,
closest_point_on_tri_2d, foot_on_triangle, snap_better, snap_priority, snap_tier,
SnapType,
};
if !want_perp && !want_nearest {
return None;
@@ -355,8 +359,7 @@ impl Scene {
|| (tier == *bt
&& (d2 - *bd2).abs() <= 1e-4
&& sub == *bs
&& (depth2 < *bdepth2
|| (depth2 == *bdepth2 && handle.value() < *bh)))
&& (depth2 < *bdepth2 || (depth2 == *bdepth2 && handle.value() < *bh)))
}
};
if better {
@@ -376,9 +379,7 @@ impl Scene {
extension_origin: None,
extension_dir: None,
viewport: None,
source: Some(
crate::command::DimensionAssociationSource::inferred(handle),
),
source: Some(crate::command::DimensionAssociationSource::inferred(handle)),
secondary_source: None,
model_point: None,
},
@@ -431,7 +432,10 @@ impl Scene {
for (slot, index) in v.iter_mut().zip(tri.iter()) {
let (Some(high), low) = (
lod.verts.get(*index as usize),
lod.verts_low.get(*index as usize).copied().unwrap_or([0.0; 3]),
lod.verts_low
.get(*index as usize)
.copied()
.unwrap_or([0.0; 3]),
) else {
ok = false;
break;
@@ -455,14 +459,8 @@ impl Scene {
continue;
}
// Per-triangle screen reject before any 3D work.
let (tx0, ty0) = (
sa[0].min(sb[0]).min(sc[0]),
sa[1].min(sb[1]).min(sc[1]),
);
let (tx1, ty1) = (
sa[0].max(sb[0]).max(sc[0]),
sa[1].max(sb[1]).max(sc[1]),
);
let (tx0, ty0) = (sa[0].min(sb[0]).min(sc[0]), sa[1].min(sb[1]).min(sc[1]));
let (tx1, ty1) = (sa[0].max(sb[0]).max(sc[0]), sa[1].max(sb[1]).max(sc[1]));
if cursor[0] < tx0 - aperture_px
|| cursor[0] > tx1 + aperture_px
|| cursor[1] < ty0 - aperture_px
@@ -1846,10 +1844,7 @@ fn offset_mesh_lod_set(mut set: MeshLodSet) -> MeshLodSet {
}
/// Build a lightweight placement over immutable block-local geometry.
fn transform_block_mesh_lod_set(
set: &MeshLodSet,
xform: &codec::types::Transform,
) -> MeshLodSet {
fn transform_block_mesh_lod_set(set: &MeshLodSet, xform: &codec::types::Transform) -> MeshLodSet {
use codec::types::Vector3;
let source = set.instance_source.clone().unwrap_or_else(|| {
std::sync::Arc::new(model::mesh_model::MeshInstanceSource {
@@ -2286,8 +2281,7 @@ pub struct Scene {
RefCell<HashMap<(String, String), Option<Arc<crate::io::plot_style::PlotStyleTable>>>>,
styled_wire_cache: RefCell<HashMap<(u64, String), (u64, Arc<Vec<WireModel>>)>>,
styled_hatch_cache: RefCell<HashMap<(u64, usize, usize, String, u32), Arc<Vec<HatchModel>>>>,
styled_wire_fill_cache:
RefCell<HashMap<(u64, String, u32, u64), Arc<Vec<HatchModel>>>>,
styled_wire_fill_cache: RefCell<HashMap<(u64, String, u32, u64), Arc<Vec<HatchModel>>>>,
/// Per-viewport projected wire cache for paper-space content viewports.
/// Stores projected + clipped wires in paper-space coordinates.
/// Maps vp_handle → (geometry_epoch, Vec<WireModel>).
@@ -2369,8 +2363,15 @@ pub struct Scene {
layout_names_cache: RefCell<Option<(u64, std::sync::Arc<Vec<String>>)>>,
/// Bump on any annotation-scale list mutation.
scale_epoch: u64,
scale_picker_cache:
RefCell<Option<(u64, f64, Option<bool>, String, std::sync::Arc<Vec<(String, f32, f64)>>)>>,
scale_picker_cache: RefCell<
Option<(
u64,
f64,
Option<bool>,
String,
std::sync::Arc<Vec<(String, f32, f64)>>,
)>,
>,
/// Bump on any named-UCS table mutation (UCS SAVE / DELETE / RENAME,
/// record-API patch, file-open, undo Full-swap).
ucs_epoch: u64,
@@ -2386,12 +2387,21 @@ pub struct Scene {
/// Conservative association hint: unknown until scanned, then updated from
/// changed entities. Retaining `true` after deletion only costs an extra scan.
has_associative_centers: std::cell::Cell<Option<bool>>,
/// Conservative hint for whether any Underlay entities exist in the document.
pub(crate) has_underlays: std::cell::Cell<Option<bool>>,
/// Conservative hint for whether any Face3D entities exist in the document.
pub(crate) has_face3d: std::cell::Cell<Option<bool>>,
/// Cached resolved document render environment (fog and background image), keyed by (geometry_epoch, document.objects.len()).
document_render_env_cache: RefCell<Option<(u64, usize, crate::scene::view::render::CachedDocumentRenderEnvironment)>>,
document_render_env_cache: RefCell<
Option<(
u64,
usize,
crate::scene::view::render::CachedDocumentRenderEnvironment,
)>,
>,
/// Memoized background image resolution to avoid repeated filesystem probes on each render frame.
background_image_cache: RefCell<HashMap<String, Option<crate::scene::model::image_model::DecodedImage>>>,
background_image_cache:
RefCell<HashMap<String, Option<crate::scene::model::image_model::DecodedImage>>>,
/// Runtime parametric constraint sets decoded from standard graph scopes.
pub(crate) parametric_constraints: Vec<parametric_constraints::ParametricConstraintSet>,
/// CONSTRAINTNAMEFORMAT: what a dynamic dimension's text shows —
@@ -2617,9 +2627,8 @@ fn section_plane(
let translation = codec::types::Vector3::new(matrix[0][3], matrix[1][3], matrix[2][3]);
let origin = inverse
.transform_point(codec::types::Vector3::new(origin.x, origin.y, origin.z) - translation);
let tangent = inverse.transform_point(codec::types::Vector3::new(
tangent.x, tangent.y, tangent.z,
));
let tangent =
inverse.transform_point(codec::types::Vector3::new(tangent.x, tangent.y, tangent.z));
let normal = linear
.transpose()
.transform_point(codec::types::Vector3::new(normal.x, normal.y, normal.z));
@@ -2783,6 +2792,7 @@ impl Scene {
glyph_cache: RefCell::new(None),
named_parameters: named_parameters::ParameterTable::new(),
has_associative_centers: std::cell::Cell::new(None),
has_underlays: std::cell::Cell::new(None),
has_face3d: std::cell::Cell::new(None),
document_render_env_cache: RefCell::new(None),
background_image_cache: RefCell::new(HashMap::default()),
@@ -2837,7 +2847,9 @@ impl Scene {
return;
};
let step = (f64::from(wpp).log2() * 4.0).ceil() as i32;
let current = self.underlay_world_per_pixel.map(|w| (w.log2() * 4.0).round() as i32);
let current = self
.underlay_world_per_pixel
.map(|w| (w.log2() * 4.0).round() as i32);
if current == Some(step) {
self.underlay_scale_pending = None;
return;
@@ -2855,7 +2867,11 @@ impl Scene {
}
self.underlay_scale_pending = None;
self.underlay_world_per_pixel = Some(2f64.powf(step as f64 / 4.0));
if self.document.entities().any(|e| matches!(e, EntityType::Underlay(_))) {
if self
.document
.entities()
.any(|e| matches!(e, EntityType::Underlay(_)))
{
self.reseed_underlays();
}
}
@@ -2863,11 +2879,16 @@ impl Scene {
/// The view has moved to another scale step than the underlay rasters
/// were made for (frames are requested until they are made again).
pub fn underlay_resolution_stale(&self) -> bool {
if !self.has_underlays() {
return false;
}
let Some(wpp) = self.world_per_pixel() else {
return false;
};
let step = (f64::from(wpp).log2() * 4.0).ceil() as i32;
self.underlay_world_per_pixel.map(|w| (w.log2() * 4.0).round() as i32) != Some(step)
self.underlay_world_per_pixel
.map(|w| (w.log2() * 4.0).round() as i32)
!= Some(step)
}
pub(super) fn world_per_pixel(&self) -> Option<f32> {
@@ -3114,9 +3135,7 @@ impl Scene {
// rebuilding every glyph in the drawing on each mouse move.
if let EntityType::RasterImage(image) = entity {
let path = image.file_path.trim();
if !path.is_empty()
&& crate::scene::model::image_model::resolve_image(path).is_none()
{
if !path.is_empty() && crate::scene::model::image_model::resolve_image(path).is_none() {
return true;
}
}
@@ -3406,6 +3425,19 @@ impl Scene {
any
}
/// Fast check for whether any Underlay entities exist in the document.
pub(crate) fn has_underlays(&self) -> bool {
if let Some(known) = self.has_underlays.get() {
return known;
}
let any = self
.document
.entities()
.any(|e| matches!(e, EntityType::Underlay(_)));
self.has_underlays.set(Some(any));
any
}
/// Fast check for whether any Face3D entities exist in the document.
pub(crate) fn has_face3d(&self) -> bool {
if let Some(known) = self.has_face3d.get() {
@@ -3428,7 +3460,8 @@ impl Scene {
/// text changed. Returns the number of hosts whose text changed and the
/// number of fields the hosts hold.
pub fn update_fields(&mut self, event: i32, hosts: Option<&[Handle]>) -> (usize, usize) {
let (changed, found) = crate::entities::field::update_fields(&mut self.document, event, hosts);
let (changed, found) =
crate::entities::field::update_fields(&mut self.document, event, hosts);
if !changed.is_empty() {
self.bump_text_hosts(&changed);
}
@@ -3454,7 +3487,9 @@ impl Scene {
.objects
.values()
.filter_map(|object| match object {
ObjectType::Layout(layout) if !layout.block_record.is_null() => Some(layout.block_record),
ObjectType::Layout(layout) if !layout.block_record.is_null() => {
Some(layout.block_record)
}
_ => None,
})
.collect();
@@ -3484,7 +3519,10 @@ impl Scene {
}
EntityType::Table(table) => {
if let Some(record) = table.block_record_handle {
tables_of.entry(record).or_default().push(table.common.handle);
tables_of
.entry(record)
.or_default()
.push(table.common.handle);
}
}
_ => {}
@@ -3500,10 +3538,15 @@ impl Scene {
None => continue,
},
};
let owner = self.document.get_entity(handle).map(|e| e.common().owner_handle);
let record = owner
.and_then(|owner| definitions.get(&owner))
.or_else(|| members.get(&handle).and_then(|record| definitions.get(record)));
let owner = self
.document
.get_entity(handle)
.map(|e| e.common().owner_handle);
let record = owner.and_then(|owner| definitions.get(&owner)).or_else(|| {
members
.get(&handle)
.and_then(|record| definitions.get(record))
});
let Some((record_handle, name)) = record else {
targets.render_handles.insert(handle);
continue;
@@ -3515,7 +3558,9 @@ impl Scene {
targets.render_handles.extend(users.iter().copied());
}
}
targets.render_handles.extend(tables_of.get(record_handle).into_iter().flatten().copied());
targets
.render_handles
.extend(tables_of.get(record_handle).into_iter().flatten().copied());
targets.touches_block_definition = true;
}
self.invalidate_dependency_targets(targets);
@@ -3557,7 +3602,10 @@ impl Scene {
}
/// Refresh display and associations after a solved grip or exact history restore.
pub(crate) fn bump_entities_after_parametric_solve(&mut self, changes: &[(Handle, ChangeKind)]) {
pub(crate) fn bump_entities_after_parametric_solve(
&mut self,
changes: &[(Handle, ChangeKind)],
) {
self.bump_entities_with_solve_policy(changes, &[], false, &[], &[], false, false);
}
@@ -3641,8 +3689,11 @@ impl Scene {
)
{
rebuilt |= self.document.refresh_table_block(*handle);
rebuilt |= !crate::entities::field::refresh_table_formulas(&mut self.document, *handle)
.is_empty();
rebuilt |= !crate::entities::field::refresh_table_formulas(
&mut self.document,
*handle,
)
.is_empty();
}
}
// The block's cached drawing is stale once its texts change.
@@ -3677,6 +3728,11 @@ impl Scene {
changes.push(change);
}
}
for change in self.refresh_associative_sweeps(&changes) {
if !changes.iter().any(|(handle, _)| *handle == change.0) {
changes.push(change);
}
}
if solve_parametric && !self.parametric_constraints.is_empty() {
for change in self.refresh_parametric_constraints_with_initial_policy(
&changes,
@@ -3744,7 +3800,10 @@ impl Scene {
self.lighting_cache.borrow_mut().clear();
}
if let Some(true) = self.has_face3d.get() {
if changes.iter().any(|(_, k)| matches!(k, ChangeKind::Removed)) {
if changes
.iter()
.any(|(_, k)| matches!(k, ChangeKind::Removed))
{
self.has_face3d.set(None);
}
} else if changes.iter().any(|(h, _)| {
@@ -3823,6 +3882,7 @@ impl Scene {
self.lighting_cache.borrow_mut().clear();
*self.document_render_env_cache.borrow_mut() = None;
self.background_image_cache.borrow_mut().clear();
self.has_underlays.set(None);
self.has_face3d.set(None);
// Default: also invalidate block definitions. Safe for every caller;
// operations that know blocks are untouched use `bump_geometry_no_blocks`.
@@ -4046,7 +4106,10 @@ impl Scene {
crate::entities::solid3d::surface_isoline_counts(surface),
crate::entities::solid3d::surface_property_state(surface).isolines,
),
_ => ([crate::entities::solid3d::clamp_header_isolines(self.document.header.isolines); 2], false),
_ => (
[crate::entities::solid3d::clamp_header_isolines(self.document.header.isolines); 2],
false,
),
};
let (mut set, wires, center) = crate::scene::model::solid_model::display_from_solid(
solid,
@@ -4321,7 +4384,10 @@ impl Scene {
.filter(|h| recoloured || class(&old, h) != class(new, h))
.collect();
self.recolor_meshes_for_handles(&changed);
let changes: Vec<(Handle, ChangeKind)> = changed.into_iter().map(|h| (h, ChangeKind::Modified)).collect();
let changes: Vec<(Handle, ChangeKind)> = changed
.into_iter()
.map(|h| (h, ChangeKind::Modified))
.collect();
self.bump_entities_restyled(&changes);
}
@@ -4352,7 +4418,10 @@ impl Scene {
let (color, class) = match Self::handle_from_wire_name(&w.name) {
Some(h) if count.errors.contains(&h) => (count.error_color, 2u64),
Some(h) if count.counted.contains(&h) => (count.color, 1),
_ => (crate::scene::cache::block_cache::fade_toward_bg(w.color, bg), 0),
_ => (
crate::scene::cache::block_cache::fade_toward_bg(w.color, bg),
0,
),
};
w.color = color;
// Instances of one definition draw in one colour: counted and
@@ -4691,7 +4760,9 @@ impl Scene {
.document
.entities()
.filter_map(|e| match e {
codec::EntityType::Viewport(v) if v.common.owner_handle == layout_block => Some(v.id),
codec::EntityType::Viewport(v) if v.common.owner_handle == layout_block => {
Some(v.id)
}
_ => None,
})
.max()
@@ -6361,7 +6432,13 @@ impl Scene {
if self.current_layout == "Model" {
return self.document.header.current_annotation_scale.clone();
}
self.explicit_viewport_handle()
// Same viewport the pill number comes from (active → selected →
// first-content fallback), so label, number and badge always describe
// one viewport (#48). Degenerate case: with zero Scale objects the
// name falls back to paper/"1:1" while the number stays factual.
// The render paths use displayed_annotation_scale_HANDLE, which stays
// explicit-only — untouched by this fix.
self.target_viewport_handle()
.and_then(|viewport| self.viewport_scale_handle(viewport))
.and_then(|handle| match self.document.objects.get(&handle) {
Some(ObjectType::Scale(scale)) => Some(scale.name.clone()),
@@ -6429,15 +6506,20 @@ impl Scene {
return false;
};
let unit_factor = self.annotation_scale_unit_factor();
let named = self.document.objects.values().find_map(|object| match object {
ObjectType::Scale(scale)
if !scale.is_temporary
&& (scale.factor() * unit_factor - factor).abs() <= 1.0e-9 * factor.max(1.0) =>
{
Some(scale.name.clone())
}
_ => None,
});
let named = self
.document
.objects
.values()
.find_map(|object| match object {
ObjectType::Scale(scale)
if !scale.is_temporary
&& (scale.factor() * unit_factor - factor).abs()
<= 1.0e-9 * factor.max(1.0) =>
{
Some(scale.name.clone())
}
_ => None,
});
if let Some(name) = named {
return self.set_viewport_scale_named_for(viewport, &name).is_some();
}
@@ -6455,7 +6537,8 @@ impl Scene {
}
pub fn viewport_annotation_scale_synced(&self) -> Option<bool> {
let viewport = self.explicit_viewport_handle()?;
// Same viewport as the pill number and label (#48).
let viewport = self.target_viewport_handle()?;
let EntityType::Viewport(vp) = self.document.get_entity(viewport)? else {
return None;
};
@@ -6543,7 +6626,8 @@ impl Scene {
}
}
let names = std::sync::Arc::new(self.layout_names());
*self.layout_names_cache.borrow_mut() = Some((self.layout_epoch, std::sync::Arc::clone(&names)));
*self.layout_names_cache.borrow_mut() =
Some((self.layout_epoch, std::sync::Arc::clone(&names)));
names
}
@@ -6612,8 +6696,7 @@ impl Scene {
.filter(|n| !n.is_empty())
.collect::<Vec<_>>()
.into();
*self.ucs_names_cache.borrow_mut() =
Some((self.ucs_epoch, std::sync::Arc::clone(&names)));
*self.ucs_names_cache.borrow_mut() = Some((self.ucs_epoch, std::sync::Arc::clone(&names)));
names
}
@@ -7479,7 +7562,7 @@ impl Scene {
let Some(EntityType::Viewport(viewport)) = self.document.get_entity(handle) else {
continue;
};
if viewport.common.owner_handle != paper_block || !viewport.status.is_on {
if viewport.common.owner_handle != paper_block || !viewport.is_on() {
continue;
}
let mode = render_mode_override.unwrap_or(viewport.render_mode);
@@ -7919,8 +8002,15 @@ impl Scene {
all_visible: bool,
viewport: Option<Handle>,
) -> Vec<ImageModel> {
self.collect_images(target_block, frozen, annotation_scale_handle, all_visible,
viewport, false, |image, _| image)
self.collect_images(
target_block,
frozen,
annotation_scale_handle,
all_visible,
viewport,
false,
|image, _| image,
)
}
pub fn paper_plot_images(&self) -> Vec<crate::io::pdf_export::PlotImage> {
@@ -7929,8 +8019,14 @@ impl Scene {
} else {
self.paper_annotation_scale_handle()
};
self.placed_images(self.current_layout_block_handle(), None,
scale, self.annotation_all_visible(), None, true)
self.placed_images(
self.current_layout_block_handle(),
None,
scale,
self.annotation_all_visible(),
None,
true,
)
}
fn placed_images(
@@ -7942,11 +8038,18 @@ impl Scene {
viewport: Option<Handle>,
plotting: bool,
) -> Vec<crate::io::pdf_export::PlotImage> {
self.collect_images(target_block, frozen, annotation_scale_handle, all_visible,
viewport, plotting, |image, context| crate::io::pdf_export::PlotImage {
self.collect_images(
target_block,
frozen,
annotation_scale_handle,
all_visible,
viewport,
plotting,
|image, context| crate::io::pdf_export::PlotImage {
image,
clips: context.clips.clone(),
})
},
)
}
fn collect_images<T>(
@@ -7987,8 +8090,12 @@ impl Scene {
graph.walk_root(
root,
|entity, context| {
(!plotting || self.document.layers.get(&entity.common().layer)
.is_none_or(|layer| layer.is_plottable))
(!plotting
|| self
.document
.layers
.get(&entity.common().layer)
.is_none_or(|layer| layer.is_plottable))
&& (context.is_instanced()
|| !self.entity_temporarily_hidden(entity.common().handle))
},
@@ -8081,21 +8188,17 @@ impl Scene {
// also invalidate. Block-definition edits route through
// bump_geometry (a full delta) and invalidate regardless.
Some((cached_epoch, arc))
if self.category_cache_valid(
*cached_epoch,
CACHE_CATEGORY_MESH,
|h| {
self.meshes.contains_key(&h)
|| match self.document.get_entity(h) {
Some(EntityType::Insert(_)) => true,
Some(EntityType::Extended(extended)) => matches!(
extended.data,
codec::entities::ExtendedEntityData::SectionObject(_)
),
_ => false,
}
},
) =>
if self.category_cache_valid(*cached_epoch, CACHE_CATEGORY_MESH, |h| {
self.meshes.contains_key(&h)
|| match self.document.get_entity(h) {
Some(EntityType::Insert(_)) => true,
Some(EntityType::Extended(extended)) => matches!(
extended.data,
codec::entities::ExtendedEntityData::SectionObject(_)
),
_ => false,
}
}) =>
{
Some(Arc::clone(arc))
}
@@ -8583,7 +8686,12 @@ impl Scene {
};
// ponytail: full entity scan per call, gated to drawings declaring the
// class; memoize per geometry epoch if large Inventor sheets show up.
if self.document.classes.get_by_name("ACIDBLOCKREFERENCE").is_some() {
if self
.document
.classes
.get_by_name("ACIDBLOCKREFERENCE")
.is_some()
{
let model = self.model_space_block_handle();
hidden.extend(self.document.entities().filter_map(|e| match e {
EntityType::Insert(i)
@@ -8773,7 +8881,8 @@ impl Scene {
graph.walk_root(
self.render_scene_root(target_block),
|entity, context| {
context.is_instanced() || !self.entity_temporarily_hidden(entity.common().handle)
context.is_instanced()
|| !self.entity_temporarily_hidden(entity.common().handle)
},
|entity, context| {
let EntityType::Extended(extended) = entity else {
@@ -8830,7 +8939,8 @@ impl Scene {
pub(super) fn point_cloud_2d_wires(&self) -> Vec<WireModel> {
use crate::scene::model::point_cloud::{resolve_source, setting, setting_value};
if !crate::scene::model::point_cloud::any_definition(&self.document)
|| setting("POINTCLOUD2DVSDISPLAY").is_some_and(|s| setting_value(&self.document, s) == 1)
|| setting("POINTCLOUD2DVSDISPLAY")
.is_some_and(|s| setting_value(&self.document, s) == 1)
{
return Vec::new();
}
@@ -8845,13 +8955,21 @@ impl Scene {
let common = entity.common();
if common.invisible
|| self.entity_temporarily_hidden(common.handle)
|| self.document.layers.get(&common.layer).is_some_and(|layer| layer.is_off() || layer.is_frozen())
|| self
.document
.layers
.get(&common.layer)
.is_some_and(|layer| layer.is_off() || layer.is_frozen())
|| resolve_source(&self.document, data).is_none()
{
continue;
}
let color = if matches!(common.color, codec::types::Color::ByLayer) {
self.document.layers.get(&common.layer).map(|layer| layer.color.clone()).unwrap_or(common.color.clone())
self.document
.layers
.get(&common.layer)
.map(|layer| layer.color.clone())
.unwrap_or(common.color.clone())
} else {
common.color.clone()
};
@@ -8859,7 +8977,12 @@ impl Scene {
wires.push(WireModel::solid_f64(
common.handle.value().to_string(),
crate::entities::extended::point_cloud_2d_style_lines(data),
[f32::from(r) / 255.0, f32::from(g) / 255.0, f32::from(b) / 255.0, 1.0],
[
f32::from(r) / 255.0,
f32::from(g) / 255.0,
f32::from(b) / 255.0,
1.0,
],
false,
));
}
@@ -9079,7 +9202,10 @@ impl Scene {
);
}
if self.count_display.is_some() {
let own = transformed.instance_color.or(set.display_color()).unwrap_or([1.0; 4]);
let own = transformed
.instance_color
.or(set.display_color())
.unwrap_or([1.0; 4]);
let color = self.count_color(context.root_handle, own, self.current_bg());
transformed.instance_color = Some(color);
if let Some(material) = transformed.material.as_mut() {
@@ -9653,12 +9779,28 @@ impl Scene {
}
}
}
let block = self.interaction_block_handle();
let frozen: Option<HashSet<Handle>> = self
.interaction_viewport_frozen_layers()
.map(|layers| layers.iter().copied().collect());
let annotation_scale_handle = self.displayed_annotation_scale_handle();
let all_visible = self.annotation_all_visible();
let changed_live: Vec<Handle> = changes
.iter()
.filter_map(|(handle, kind)| {
(!matches!(kind, ChangeKind::Removed)
&& !self.entity_temporarily_hidden(*handle)
&& self.document.get_entity(*handle).is_some())
if matches!(kind, ChangeKind::Removed) {
return None;
}
let entity = self.document.get_entity(*handle)?;
self.resident_entity_visible(
entity,
block,
frozen.as_ref(),
annotation_scale_handle,
all_visible,
)
.then_some(*handle)
})
.collect();
@@ -9907,8 +10049,25 @@ impl Scene {
/// Prepare hover indexes after derived geometry is installed during open.
pub(crate) fn warm_interaction_caches(&self) {
let perf = crate::perf::enabled();
let t_start = perf.then(iced::time::Instant::now);
let t_ei = perf.then(iced::time::Instant::now);
let _ = self.entity_index();
let ei_ms = crate::perf::elapsed_ms(t_ei);
let t_ihi = perf.then(iced::time::Instant::now);
let _ = self.interaction_handle_index();
let ihi_ms = crate::perf::elapsed_ms(t_ihi);
let total_ms = crate::perf::elapsed_ms(t_start);
if perf {
crate::perf_record!(
"[perf] warm_interaction_caches total={:.1}ms entity_index={:.1}ms interaction_handle_index={:.1}ms",
total_ms,
ei_ms,
ihi_ms,
);
}
}
fn interaction_handle_index(
@@ -11152,8 +11311,14 @@ impl Scene {
let t_build = perf.then(iced::time::Instant::now);
let mut wires: Vec<WireModel> = if memo_active {
// Invalidate when any baked display input changes.
let guard =
tess_memo_guard(view_aabb, anno, annotation_scale_handle, all_visible, bg, avp);
let guard = tess_memo_guard(
view_aabb,
anno,
annotation_scale_handle,
all_visible,
bg,
avp,
);
let (memo_cell, guard_cell) = if resident {
(&self.resident_tess_memo, &self.resident_tess_guard)
} else {
@@ -11219,8 +11384,7 @@ impl Scene {
for (h, a) in miss_pairs {
// Uniquely owned (just built above): enrich solid shells
// with B-rep vertex snaps before the memo/assembly split.
let mut wires =
Arc::try_unwrap(a).unwrap_or_else(|a| a.as_ref().clone());
let mut wires = Arc::try_unwrap(a).unwrap_or_else(|a| a.as_ref().clone());
self.attach_solid_snaps(h, &mut wires);
let a = Arc::new(wires);
out.extend(a.iter().cloned());
@@ -11958,6 +12122,34 @@ vis_index={:.1} visible_probe={:.1}",
/// block users re-expand, since their definitions bake the children's
/// visibility. Nothing is recoloured.
pub fn invalidate_layer_visibility(&mut self, names: &[String]) {
// Turning a layer off or freezing it removes its entities from the
// interactive scene as well as from rendering. Drop any entities that
// were already selected before the visibility change, otherwise a
// subsequent MOVE/ERASE/etc. could still modify invisible geometry.
let block = self.interaction_block_handle();
let frozen: Option<HashSet<Handle>> = self
.interaction_viewport_frozen_layers()
.map(|layers| layers.iter().copied().collect());
let annotation_scale_handle = self.displayed_annotation_scale_handle();
let all_visible = self.annotation_all_visible();
let visible_selection: Vec<Handle> = self
.selected_handles_in_order()
.into_iter()
.filter(|handle| {
self.document.get_entity(*handle).is_some_and(|entity| {
self.resident_entity_visible(
entity,
block,
frozen.as_ref(),
annotation_scale_handle,
all_visible,
)
})
})
.collect();
self.replace_selection_exact(&visible_selection);
let targets = self.dependency_targets(DependencyKind::Layer, names);
if targets.render_handles.is_empty() {
return;
@@ -12152,6 +12344,8 @@ vis_index={:.1} visible_probe={:.1}",
}
}
let perf = crate::perf::enabled();
let t_aabbs_start = perf.then(iced::time::Instant::now);
let mut items: Vec<(Handle, [f64; 4])> = Vec::new();
let mut unbounded: Vec<Handle> = Vec::new();
let mut union: Option<[f64; 4]> = None;
@@ -12175,6 +12369,7 @@ vis_index={:.1} visible_probe={:.1}",
None => unbounded.push(e.common().handle),
}
}
let aabbs_ms = crate::perf::elapsed_ms(t_aabbs_start);
let root = match union {
Some(u) => {
let w = (u[2] - u[0]).max(1.0);
@@ -12185,10 +12380,22 @@ vis_index={:.1} visible_probe={:.1}",
}
None => [-1.0, -1.0, 1.0, 1.0],
};
let t_tree_start = perf.then(iced::time::Instant::now);
let mut tree = pick::quadtree::QuadTree::new(root);
for (h, ab) in items {
tree.insert(h, ab);
}
let tree_ms = crate::perf::elapsed_ms(t_tree_start);
if perf {
crate::perf_record!(
"[perf] entity_index-rebuild aabbs={:.1}ms tree={:.1}ms entities={} root={:?}",
aabbs_ms,
tree_ms,
tree.len(),
root,
);
}
*self.entity_index_cache.borrow_mut() = Some((
self.geometry_epoch,
@@ -12526,11 +12733,12 @@ mod section_tests {
#[test]
fn instance_transform_pulls_the_section_plane_into_body_space() {
let body = kernel::brep::make::cuboid([0.0; 3], [10.0; 3]).unwrap();
let transform =
codec::types::Transform::from_scaling(codec::types::Vector3::new(2.0, 1.0, 1.0))
.then(&codec::types::Transform::from_translation(
codec::types::Vector3::new(10.0, 0.0, 0.0),
));
let transform = codec::types::Transform::from_scaling(codec::types::Vector3::new(
2.0, 1.0, 1.0,
))
.then(&codec::types::Transform::from_translation(
codec::types::Vector3::new(10.0, 0.0, 0.0),
));
let mut data = section(1, 0.0);
for point in &mut data.vertices {
point.x = 20.0;
@@ -13043,8 +13251,19 @@ mod journal_tests {
hatch.common.layer = "Shadows".to_string();
let handle = scene.add_entity(EntityType::Hatch(hatch));
let model = scene.model_space_block_handle();
assert_eq!(scene.document.get_entity(handle).unwrap().common().owner_handle, model);
scene.document.objects.retain(|_, object| !matches!(object, ObjectType::Layout(_)));
assert_eq!(
scene
.document
.get_entity(handle)
.unwrap()
.common()
.owner_handle,
model
);
scene
.document
.objects
.retain(|_, object| !matches!(object, ObjectType::Layout(_)));
scene.invalidate_dependency_index();
let targets = scene.dependency_targets(DependencyKind::Layer, &["Shadows".to_string()]);
@@ -13055,7 +13274,10 @@ mod journal_tests {
scene.document.layers.get_mut("Shadows").unwrap().flags.off = true;
let epoch = scene.geometry_epoch;
scene.invalidate_layer_dependencies(&["Shadows".to_string()]);
assert_ne!(scene.geometry_epoch, epoch, "the cached hatch needs an entity delta");
assert_ne!(
scene.geometry_epoch, epoch,
"the cached hatch needs an entity delta"
);
}
/// The loader's handed-over draw depths must equal a local rebuild.
@@ -14346,6 +14568,48 @@ mod layout_cache_tests {
);
assert!(after.iter().any(|n| n == "TEST_CACHED_UCS"));
}
// #48: with no active/selected viewport, the pill number, label and sync
// badge must all describe the SAME viewport (the first-content fallback
// the number already used) — not a mix of viewport number, paper label
// and hidden badge.
#[test]
fn scale_pill_sources_agree_without_targeted_viewport() {
let mut scene = Scene::new();
scene.add_layout("P1").unwrap();
scene.current_layout = "P1".to_string();
assert!(scene.add_scale("1:50", 1.0, 50.0));
let mut vp = codec::entities::Viewport::new();
vp.width = 100.0;
vp.height = 100.0;
let handle = scene
.document
.add_entity_to_layout(codec::EntityType::Viewport(vp), "P1")
.unwrap();
assert!(scene.set_viewport_scale_named_for(handle, "1:50").is_some());
assert!(scene.active_viewport.is_none());
let number = scene.first_viewport_scale();
let name = scene.displayed_annotation_scale_name();
let synced = scene.viewport_annotation_scale_synced();
eprintln!("number={number:?} name={name:?} synced={synced:?}");
assert!(
number.is_some() && name == "1:50" && synced == Some(true),
"pill trio must agree"
);
}
#[test]
fn scale_pill_model_layout_unchanged() {
// Model layout has no viewports: number None, header label, no badge.
let scene = Scene::new();
assert!(scene.current_layout == "Model");
assert!(scene.first_viewport_scale().is_none());
assert_eq!(
scene.displayed_annotation_scale_name(),
scene.document.header.current_annotation_scale
);
assert!(scene.viewport_annotation_scale_synced().is_none());
}
}
#[cfg(test)]