forked from 1b-agent/opencadstudio-upstream
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:
596 files changed
+69069
-34120
No files matched your search
+387
-123
@@ -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)]
|
||||
|
||||
Reference in new issue
Block a user