From 6fb68d1c7c68936e90e6cc2862970495437c0ae0 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sun, 26 Jul 2026 23:38:38 +0300 Subject: [PATCH] feat(scene): integrate DWG object data --- Cargo.toml | 4 +- src/app/properties.rs | 35 +- src/app/update/file.rs | 1 + src/entities/light.rs | 210 ++- src/entities/mod.rs | 1 + src/entities/object_data.rs | 2725 ++++++++++++++++++++++++++++++++ src/scene/mod.rs | 65 + src/scene/pipeline/uniforms.rs | 20 + src/scene/view/render.rs | 259 ++- src/shaders/mesh.wgsl | 80 +- 10 files changed, 3379 insertions(+), 21 deletions(-) create mode 100644 src/entities/object_data.rs diff --git a/Cargo.toml b/Cargo.toml index 4f0bf3a83..29b847412 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,8 +90,8 @@ lyon_tessellation = "1.0.20" windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_System_Com", "Win32_System_Registry", "Win32_Storage_FileSystem", "Win32_Foundation"] } [patch.crates-io] -# Track standard DWG entity data used by the scene integration. -acadrust = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "7aaea11" } +# Track standard DWG object relationships used by the scene integration. +acadrust = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "4afb27d" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # Native enables the plugin host runtime (out-of-process plugins). diff --git a/src/app/properties.rs b/src/app/properties.rs index a5d375e5b..9281f0655 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -59,6 +59,12 @@ impl OpenCADStudio { /// Preserves UI state (open pickers, edit buffer) across refreshes. pub(super) fn refresh_properties(&mut self) { let i = self.active_tab; + if !crate::entities::object_data::cache_is_prepared( + &self.tabs[i].scene.object_data_cache, + ) { + self.tabs[i].scene.object_data_cache = + crate::entities::object_data::build_cache(&self.tabs[i].scene.document); + } // Note: the color-picker dropdown is intentionally NOT carried over — a // rebuild means the selection (or a property) changed, so the dropdown // closes, matching the deselect / reselect / click-away expectation. @@ -135,7 +141,27 @@ impl OpenCADStudio { let new_panel = { let selected = self.tabs[i].scene.selected_entities(); let mut panel = match selected.len() { - 0 => ui::PropertiesPanel::empty(), + 0 => { + let sections = crate::entities::object_data::cached_document_sections( + &self.tabs[i].scene.object_data_cache, + ); + ui::PropertiesPanel { + title: "Drawing".to_string(), + sections, + layer_combo: iced::widget::combo_box::State::new(layer_names.clone()), + linetype_combo: iced::widget::combo_box::State::new( + linetype_items.clone(), + ), + lineweight_combo: iced::widget::combo_box::State::new( + ui::properties::lw_options(), + ), + hatch_pattern_combo: iced::widget::combo_box::State::new( + crate::scene::model::hatch_patterns::names(), + ), + linetype_items, + ..Default::default() + } + } 1 => { let (handle, source_entity) = selected[0]; let contextual = crate::scene::annotative::entity_for_active_context( @@ -514,6 +540,13 @@ impl OpenCADStudio { } } + sections.extend(crate::entities::object_data::sections( + &self.tabs[i].scene.document, + &self.tabs[i].scene.object_data_cache, + handle, + entity, + )); + // Uniform-scale checkbox for block references (#427): // while the three scale factors are equal (and the user // hasn't opted into per-axis editing) the panel shows one diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 8d9c326ce..a47fd4034 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -542,6 +542,7 @@ pub(super) fn on_open_file(&mut self) -> Task { self.tabs[i].scene.images = caches.images; self.tabs[i].scene.meshes = caches.meshes; self.tabs[i].scene.block_meshes = caches.block_meshes; + self.tabs[i].scene.object_data_cache = caches.object_data; let prepared_geometry = caches.prepared_geometry.take(); // Invalidate the wire cache so the new document is tessellated. self.tabs[i].scene.bump_geometry(); diff --git a/src/entities/light.rs b/src/entities/light.rs index 75df95545..446612436 100644 --- a/src/entities/light.rs +++ b/src/entities/light.rs @@ -13,10 +13,12 @@ use acadrust::entities::Light; use acadrust::EntityType; use crate::command::EntityTransform; -use crate::entities::common::{center_grip, edit_prop as edit, ro_prop, square_grip}; +use crate::entities::common::{ + center_grip, edit_angle_prop, edit_prop as edit, ro_prop, square_grip, +}; use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; -use crate::scene::model::object::{GripApply, GripDef, PropSection}; +use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::model::wire_model::SnapHint; /// On-screen glyph radius in pixels for the screen-relative path. @@ -271,13 +273,189 @@ impl PropertyEditable for Light { props.push(edit("Target Y", "li_ty", self.target.y)); props.push(edit("Target Z", "li_tz", self.target.z)); } - vec![PropSection { - title: "Geometry".into(), - props, - }] + let mut sections = vec![ + PropSection { + title: "Geometry".into(), + props, + }, + PropSection { + title: "Light".into(), + props: vec![ + Property { + label: "On".into(), + field: "li_status", + value: PropValue::BoolToggle { + field: "li_status", + value: self.status, + }, + }, + Property { + label: "Plot Glyph".into(), + field: "li_plot_glyph", + value: PropValue::BoolToggle { + field: "li_plot_glyph", + value: self.plot_glyph, + }, + }, + ro_prop("Color", "li_color", format!("{:?}", self.light_color)), + edit("Intensity", "li_intensity", self.intensity), + Property { + label: "Attenuation".into(), + field: "li_attenuation", + value: PropValue::Choice { + selected: match self.attenuation_type { + 1 => "Inverse Linear", + 2 => "Inverse Square", + _ => "None", + } + .to_string(), + options: vec![ + "None".to_string(), + "Inverse Linear".to_string(), + "Inverse Square".to_string(), + ], + }, + }, + Property { + label: "Use Limits".into(), + field: "li_use_limits", + value: PropValue::BoolToggle { + field: "li_use_limits", + value: self.use_attenuation_limits, + }, + }, + edit("Start Limit", "li_start", self.attenuation_start_limit), + edit("End Limit", "li_end", self.attenuation_end_limit), + edit_angle_prop( + "Hotspot Angle", + "li_hotspot", + self.hotspot_angle.to_degrees(), + ), + edit_angle_prop( + "Falloff Angle", + "li_falloff", + self.falloff_angle.to_degrees(), + ), + ], + }, + PropSection { + title: "Shadows".into(), + props: vec![ + Property { + label: "Cast Shadows".into(), + field: "li_shadows", + value: PropValue::BoolToggle { + field: "li_shadows", + value: self.cast_shadows, + }, + }, + ro_prop("Type", "li_shadow_type", self.shadow_type.to_string()), + ro_prop("Map Size", "li_shadow_size", self.shadow_map_size.to_string()), + ro_prop( + "Softness", + "li_shadow_softness", + self.shadow_map_softness.to_string(), + ), + ], + }, + ]; + if let Some(photo) = &self.photometric_data { + sections.push(PropSection { + title: "Photometric".into(), + props: vec![ + ro_prop("Web File", "li_web_file", photo.web_file.clone()), + ro_prop( + "Physical Method", + "li_physical_method", + photo.physical_intensity_method.to_string(), + ), + edit( + "Physical Intensity", + "li_physical_intensity", + photo.physical_intensity, + ), + edit( + "Illuminance Distance", + "li_illuminance_distance", + photo.illuminance_distance, + ), + edit( + "Color Temperature", + "li_color_temperature", + photo.lamp_color_temperature, + ), + ro_prop( + "Lamp Preset", + "li_lamp_preset", + photo.lamp_color_preset.to_string(), + ), + ro_prop( + "Shape", + "li_shape", + format!( + "{} · {:.6} × {:.6} · radius {:.6}", + photo.extended_light_shape, + photo.extended_light_length, + photo.extended_light_width, + photo.extended_light_radius + ), + ), + ro_prop( + "Web", + "li_web", + format!( + "type {}; symmetry {}; flux {:.6}; angles {:?}; rotation {:.6},{:.6},{:.6}", + photo.web_file_type, + photo.web_symmetry, + photo.web_flux, + photo.web_angles, + photo.web_rotation.x, + photo.web_rotation.y, + photo.web_rotation.z + ), + ), + ], + }); + } + sections } fn apply_geom_prop(&mut self, field: &str, value: &str) { + match field { + "li_status" => { + if let Ok(value) = value.parse::() { + self.status = value; + } + return; + } + "li_plot_glyph" => { + if let Ok(value) = value.parse::() { + self.plot_glyph = value; + } + return; + } + "li_use_limits" => { + if let Ok(value) = value.parse::() { + self.use_attenuation_limits = value; + } + return; + } + "li_shadows" => { + if let Ok(value) = value.parse::() { + self.cast_shadows = value; + } + return; + } + "li_attenuation" => { + self.attenuation_type = match value { + "Inverse Linear" => 1, + "Inverse Square" => 2, + _ => 0, + }; + return; + } + _ => {} + } let Ok(v) = value.trim().parse::() else { return; }; @@ -288,6 +466,26 @@ impl PropertyEditable for Light { "li_tx" => self.target.x = v, "li_ty" => self.target.y = v, "li_tz" => self.target.z = v, + "li_intensity" => self.intensity = v.max(0.0), + "li_start" => self.attenuation_start_limit = v, + "li_end" => self.attenuation_end_limit = v, + "li_hotspot" => self.hotspot_angle = v.to_radians(), + "li_falloff" => self.falloff_angle = v.to_radians(), + "li_physical_intensity" => { + if let Some(photo) = self.photometric_data.as_mut() { + photo.physical_intensity = v; + } + } + "li_illuminance_distance" => { + if let Some(photo) = self.photometric_data.as_mut() { + photo.illuminance_distance = v; + } + } + "li_color_temperature" => { + if let Some(photo) = self.photometric_data.as_mut() { + photo.lamp_color_temperature = v; + } + } _ => {} } } diff --git a/src/entities/mod.rs b/src/entities/mod.rs index d6e0fb789..69e81b14e 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -19,6 +19,7 @@ pub mod mline; pub mod mtext; pub mod multileader; pub mod names; +pub mod object_data; pub mod ole2frame; pub mod point; pub mod polyline; diff --git a/src/entities/object_data.rs b/src/entities/object_data.rs new file mode 100644 index 000000000..8decfb960 --- /dev/null +++ b/src/entities/object_data.rs @@ -0,0 +1,2725 @@ +//! OCS-facing integration for non-graphical DWG object graphs. +//! +//! Dynamic-block evaluation records, solid construction history and +//! associative networks do not draw standalone geometry. Their native place in +//! OCS is the selected entity's semantic/property view, while the evaluated +//! entity geometry continues through the normal render pipeline. + +use acadrust::objects::{ + AssociativeData, BlockEvalValue, DynamicBlockData, ObjectType, + SolidHistoryNodeBase, SolidHistoryOperation, +}; +use acadrust::{CadDocument, EntityType, Handle}; + +use crate::scene::model::object::{PropSection, PropValue, Property}; + +#[derive(Debug, Clone, Default)] +pub struct ObjectDataCache { + document_sections: std::sync::Arc>, + dynamic_objects: std::sync::Arc>, + associative_objects: std::sync::Arc>, + light_entities: std::sync::Arc>, + sun_objects: std::sync::Arc>, + geo_objects: std::sync::Arc>, +} + +pub fn build_cache(document: &CadDocument) -> ObjectDataCache { + let mut dynamic_objects = Vec::new(); + let mut associative_objects = Vec::new(); + let mut ordered_lights = Vec::new(); + let mut sun_objects = Vec::new(); + let mut geo_objects = Vec::new(); + for (handle, object) in &document.objects { + match object { + ObjectType::DynamicBlock(_) => dynamic_objects.push(*handle), + ObjectType::Associative(_) => associative_objects.push(*handle), + ObjectType::GeoData(_) => geo_objects.push(*handle), + ObjectType::ClassObject(value) => match &value.data { + acadrust::objects::ClassObjectData::LightList(list) => { + ordered_lights.extend(list.lights.iter().map(|entry| entry.handle)); + } + acadrust::objects::ClassObjectData::Sun(_) => sun_objects.push(*handle), + _ => {} + }, + _ => {} + } + } + let mut seen_lights = rustc_hash::FxHashSet::default(); + ordered_lights.retain(|handle| seen_lights.insert(*handle)); + for entity in document.entities() { + if let EntityType::Light(light) = entity { + if seen_lights.insert(light.common.handle) { + ordered_lights.push(light.common.handle); + } + } + } + dynamic_objects.sort_by_key(|handle| handle.value()); + associative_objects.sort_by_key(|handle| handle.value()); + sun_objects.sort_by_key(|handle| handle.value()); + geo_objects.sort_by_key(|handle| handle.value()); + ObjectDataCache { + document_sections: std::sync::Arc::new(build_document_sections(document)), + dynamic_objects: std::sync::Arc::new(dynamic_objects), + associative_objects: std::sync::Arc::new(associative_objects), + light_entities: std::sync::Arc::new(ordered_lights), + sun_objects: std::sync::Arc::new(sun_objects), + geo_objects: std::sync::Arc::new(geo_objects), + } +} + +pub fn cached_document_sections(cache: &ObjectDataCache) -> Vec { + cache.document_sections.as_ref().clone() +} + +pub fn cache_is_prepared(cache: &ObjectDataCache) -> bool { + !cache.document_sections.is_empty() +} + +pub fn light_entities(cache: &ObjectDataCache) -> &[Handle] { + &cache.light_entities +} + +pub fn sun_objects(cache: &ObjectDataCache) -> &[Handle] { + &cache.sun_objects +} + +pub fn geo_objects(cache: &ObjectDataCache) -> &[Handle] { + &cache.geo_objects +} + +pub fn update_light_entity( + cache: &mut ObjectDataCache, + handle: Handle, + exists: bool, +) { + let lights = std::sync::Arc::make_mut(&mut cache.light_entities); + if exists { + if !lights.contains(&handle) { + lights.push(handle); + } + } else { + lights.retain(|candidate| *candidate != handle); + } +} + +fn ro(label: impl Into, value: impl Into) -> Property { + Property { + label: label.into(), + field: "dwg_object_data", + value: PropValue::ReadOnly(value.into()), + } +} + +fn handle_text(handle: Handle) -> String { + if handle.is_valid() { + format!("{:X}", handle.value()) + } else { + "None".to_string() + } +} + +fn entity_history_handle(entity: &EntityType) -> Option { + match entity { + EntityType::Solid3D(value) => value.history_handle, + EntityType::Region(value) => value.history_handle, + EntityType::Body(value) => value.history_handle, + EntityType::Surface(value) => value.history_handle, + _ => None, + } + .filter(|handle| handle.is_valid()) +} + +fn history_operation_name(operation: &SolidHistoryOperation) -> &'static str { + match operation { + SolidHistoryOperation::Unknown => "Unknown", + SolidHistoryOperation::Box(_) => "Box", + SolidHistoryOperation::Wedge(_) => "Wedge", + SolidHistoryOperation::Sphere(_) => "Sphere", + SolidHistoryOperation::Cylinder(_) => "Cylinder", + SolidHistoryOperation::Cone(_) => "Cone", + SolidHistoryOperation::Pyramid(_) => "Pyramid", + SolidHistoryOperation::Torus(_) => "Torus", + SolidHistoryOperation::Boolean(_) => "Boolean", + SolidHistoryOperation::Brep(_) => "Brep", + SolidHistoryOperation::Fillet(_) => "Fillet", + SolidHistoryOperation::Chamfer(_) => "Chamfer", + SolidHistoryOperation::Sweep(_) => "Sweep", + SolidHistoryOperation::Extrusion(_) => "Extrusion", + SolidHistoryOperation::Loft(_) => "Loft", + SolidHistoryOperation::Revolve(_) => "Revolve", + } +} + +fn history_step_id(operation: &SolidHistoryOperation) -> Option { + let base = match operation { + SolidHistoryOperation::Unknown => return None, + SolidHistoryOperation::Box(value) | SolidHistoryOperation::Wedge(value) => &value.base, + SolidHistoryOperation::Sphere(value) => &value.base, + SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => &value.base, + SolidHistoryOperation::Pyramid(value) => &value.base, + SolidHistoryOperation::Torus(value) => &value.base, + SolidHistoryOperation::Boolean(value) => &value.base, + SolidHistoryOperation::Brep(value) => &value.base, + SolidHistoryOperation::Fillet(value) => &value.base, + SolidHistoryOperation::Chamfer(value) => &value.base, + SolidHistoryOperation::Sweep(value) | SolidHistoryOperation::Extrusion(value) => { + &value.base + } + SolidHistoryOperation::Loft(value) => &value.base, + SolidHistoryOperation::Revolve(value) => &value.base, + }; + Some(base.step_id) +} + +fn history_base_text(base: &SolidHistoryNodeBase) -> String { + format!( + "node {}; version {}.{}; material {}; color {:?}; transform [{:.4}, {:.4}, {:.4}, {:.4}; {:.4}, {:.4}, {:.4}, {:.4}; {:.4}, {:.4}, {:.4}, {:.4}; {:.4}, {:.4}, {:.4}, {:.4}]", + base.eval.node_id, + base.major, + base.minor, + handle_text(base.material), + base.color, + base.transform[0], + base.transform[1], + base.transform[2], + base.transform[3], + base.transform[4], + base.transform[5], + base.transform[6], + base.transform[7], + base.transform[8], + base.transform[9], + base.transform[10], + base.transform[11], + base.transform[12], + base.transform[13], + base.transform[14], + base.transform[15], + ) +} + +fn embedded_name(value: Option<&acadrust::entities::EmbeddedEntity>) -> &'static str { + match value { + Some(acadrust::entities::EmbeddedEntity::Point(_)) => "Point", + Some(acadrust::entities::EmbeddedEntity::Line(_)) => "Line", + Some(acadrust::entities::EmbeddedEntity::Arc(_)) => "Arc", + Some(acadrust::entities::EmbeddedEntity::Circle(_)) => "Circle", + Some(acadrust::entities::EmbeddedEntity::Ellipse(_)) => "Ellipse", + Some(acadrust::entities::EmbeddedEntity::Spline(_)) => "Spline", + Some(acadrust::entities::EmbeddedEntity::LwPolyline(_)) => "Polyline", + Some(acadrust::entities::EmbeddedEntity::Ray(_)) => "Ray", + Some(acadrust::entities::EmbeddedEntity::XLine(_)) => "XLine", + Some(acadrust::entities::EmbeddedEntity::Unknown { .. }) => "Unknown", + None => "None", + } +} + +fn history_operation_text(operation: &SolidHistoryOperation) -> String { + match operation { + SolidHistoryOperation::Unknown => "Unknown operation".to_string(), + SolidHistoryOperation::Box(value) | SolidHistoryOperation::Wedge(value) => format!( + "{}; length {:.6}; width {:.6}; height {:.6}", + history_base_text(&value.base), + value.length, + value.width, + value.height + ), + SolidHistoryOperation::Sphere(value) => format!( + "{}; radius {:.6}", + history_base_text(&value.base), + value.radius + ), + SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => format!( + "{}; height {:.6}; radii {:.6}/{:.6}; x-radius {:.6}", + history_base_text(&value.base), + value.height, + value.major_radius, + value.minor_radius, + value.x_radius + ), + SolidHistoryOperation::Pyramid(value) => format!( + "{}; height {:.6}; sides {}; radii {:.6}/{:.6}", + history_base_text(&value.base), + value.height, + value.sides, + value.radius, + value.top_radius + ), + SolidHistoryOperation::Torus(value) => format!( + "{}; radii {:.6}/{:.6}", + history_base_text(&value.base), + value.major_radius, + value.minor_radius + ), + SolidHistoryOperation::Boolean(value) => format!( + "{}; operation {}; operands {}/{}", + history_base_text(&value.base), + value.operation, + value.first_operand, + value.second_operand + ), + SolidHistoryOperation::Brep(value) => format!( + "{}; ACIS {} bytes; materials {}", + history_base_text(&value.base), + value.acis_data.sat_data.len() + value.acis_data.sab_data.len(), + value.acis_data.materials.len() + ), + SolidHistoryOperation::Fillet(value) => format!( + "{}; method {}; edges {}; radii {}; setbacks {}/{}", + history_base_text(&value.base), + value.method, + value.edges.len(), + value.radii.len(), + value.start_setbacks.len(), + value.end_setbacks.len() + ), + SolidHistoryOperation::Chamfer(value) => format!( + "{}; method {}; distances {:.6}/{:.6}; edges {}; base face {}", + history_base_text(&value.base), + value.method, + value.base_distance, + value.other_distance, + value.edges.len(), + value.base_face + ), + SolidHistoryOperation::Sweep(value) | SolidHistoryOperation::Extrusion(value) => format!( + "{}; sweep {}; path {}; direction {:.6},{:.6},{:.6}; draft {:.6}; scale {:.6}; twist {:.6}; align {:.6}; bank {}; intersections {}", + history_base_text(&value.base), + embedded_name(value.sweep_entity.as_ref()), + embedded_name(value.path_entity.as_ref()), + value.direction.x, + value.direction.y, + value.direction.z, + value.draft_angle, + value.scale_factor, + value.twist_angle, + value.align_angle, + value.bank, + value.check_intersections + ), + SolidHistoryOperation::Loft(value) => format!( + "{}; cross sections {}; guides {}", + history_base_text(&value.base), + value.cross_sections.len(), + value.guides.len() + ), + SolidHistoryOperation::Revolve(value) => format!( + "{}; profile {}; axis point {:.6},{:.6},{:.6}; direction {:.6},{:.6}; angle {:.6}; start {:.6}; draft {:.6}; twist {:.6}; close-to-axis {}", + history_base_text(&value.base), + embedded_name(value.sweep_entity.as_ref()), + value.axis_point.x, + value.axis_point.y, + value.axis_point.z, + value.direction.x, + value.direction.y, + value.revolve_angle, + value.start_angle, + value.draft_angle, + value.twist_angle, + value.close_to_axis + ), + } +} + +fn solid_history_sections( + document: &CadDocument, + cache: &ObjectDataCache, + entity: &EntityType, +) -> Vec { + let Some(history_handle) = entity_history_handle(entity) else { + return Vec::new(); + }; + let Some(ObjectType::DynamicBlock(history_object)) = + document.objects.get(&history_handle) + else { + return vec![PropSection { + title: "Solid History".to_string(), + props: vec![ + ro("Handle", handle_text(history_handle)), + ro("Status", "Referenced history object is unresolved"), + ], + }]; + }; + let DynamicBlockData::SolidHistory(history) = &history_object.data else { + return vec![PropSection { + title: "Solid History".to_string(), + props: vec![ + ro("Handle", handle_text(history_handle)), + ro("Status", "Referenced object is not a solid history"), + ], + }]; + }; + let mut nodes = cache + .dynamic_objects + .iter() + .filter_map(|handle| match document.objects.get(handle) { + Some(ObjectType::DynamicBlock(value)) + if document.owner_chain_reaches(value.owner, history_handle) => + { + match &value.data { + DynamicBlockData::SolidHistoryNode(node) => Some(node), + _ => None, + } + } + _ => None, + }) + .collect::>(); + nodes.sort_by_key(|node| history_step_id(node).unwrap_or(i32::MAX)); + + let mut sections = vec![PropSection { + title: "Solid History".to_string(), + props: vec![ + ro("Handle", handle_text(history_handle)), + ro("Version", format!("{}.{}", history.major, history.minor)), + ro("Root node", history.history_node_id.to_string()), + ro("Show history", history.show_history.to_string()), + ro("Record history", history.record_history.to_string()), + ro("Steps", nodes.len().to_string()), + ], + }]; + if !nodes.is_empty() { + sections.push(PropSection { + title: "Construction Steps".to_string(), + props: nodes + .into_iter() + .map(|node| { + let step = history_step_id(node) + .map(|value| value.to_string()) + .unwrap_or_else(|| "?".to_string()); + ro( + format!("Step {step} · {}", history_operation_name(node)), + history_operation_text(node), + ) + }) + .collect(), + }); + } + sections +} + +fn block_value_text(value: &BlockEvalValue) -> String { + match value { + BlockEvalValue::None => "None".to_string(), + BlockEvalValue::Real(value) => format!("{value:.6}"), + BlockEvalValue::Point(value) => format!("{:.6}, {:.6}", value[0], value[1]), + BlockEvalValue::Text(value) => value.clone(), + BlockEvalValue::Long(value) => value.to_string(), + BlockEvalValue::Handle(value) => handle_text(*value), + BlockEvalValue::Short(value) => value.to_string(), + } +} + +fn dynamic_data_text(data: &DynamicBlockData) -> String { + match data { + DynamicBlockData::Unknown => "Unknown".to_string(), + DynamicBlockData::Representation(value) => { + format!("block {}; flags {}", handle_text(value.block), value.flags) + } + DynamicBlockData::ProxyNode(value) => format!( + "node {}; parent {}; version {}.{}; value {}", + value.node_id, + value.parent_id, + value.major, + value.minor, + block_value_text(&value.value) + ), + DynamicBlockData::GripLocationComponent(value) => format!( + "grip type {}; expression {}; value {}", + value.grip_type, + value.expression, + block_value_text(&value.eval.value) + ), + DynamicBlockData::AlignmentGrip(value) | DynamicBlockData::LinearGrip(value) => format!( + "{} at {:.6},{:.6},{:.6}; orientation {:.6},{:.6},{:.6}; cycling {}", + value.grip.element.name, + value.grip.location.x, + value.grip.location.y, + value.grip.location.z, + value.orientation.x, + value.orientation.y, + value.orientation.z, + value.grip.insert_cycling + ), + DynamicBlockData::FlipGrip(value) => format!( + "{} at {:.6},{:.6},{:.6}; state {}; orientation {:.6},{:.6},{:.6}", + value.grip.element.name, + value.grip.location.x, + value.grip.location.y, + value.grip.location.z, + value.combined_state, + value.orientation.x, + value.orientation.y, + value.orientation.z + ), + DynamicBlockData::LookupGrip(value) + | DynamicBlockData::PolarGrip(value) + | DynamicBlockData::RotationGrip(value) + | DynamicBlockData::VisibilityGrip(value) + | DynamicBlockData::XYGrip(value) + | DynamicBlockData::PropertiesTableGrip(value) => format!( + "{} at {:.6},{:.6},{:.6}; flags {}/{}; cycling {} weight {}", + value.element.name, + value.location.x, + value.location.y, + value.location.z, + value.flags_91, + value.flags_92, + value.insert_cycling, + value.insert_cycling_weight + ), + DynamicBlockData::AlignmentParameter(value) => format!( + "{}; perpendicular {}; base {:.6},{:.6},{:.6}; end {:.6},{:.6},{:.6}", + value.parameter.parameter.element.name, + value.align_perpendicular, + value.parameter.definition_base_point.x, + value.parameter.definition_base_point.y, + value.parameter.definition_base_point.z, + value.parameter.definition_end_point.x, + value.parameter.definition_end_point.y, + value.parameter.definition_end_point.z + ), + DynamicBlockData::BasePointParameter(value) => format!( + "{}; point {:.6},{:.6},{:.6}; base {:.6},{:.6},{:.6}", + value.parameter.parameter.element.name, + value.point.x, + value.point.y, + value.point.z, + value.base_point.x, + value.base_point.y, + value.base_point.z + ), + DynamicBlockData::FlipParameter(value) => format!( + "{}; labels {}/{}; tooltip {}", + value.parameter.parameter.element.name, + value.base_state_label, + value.flipped_state_label, + value.tooltip + ), + DynamicBlockData::LinearParameter(value) => format!( + "{}; {} = {:.6}; range {:.6}..{:.6}; increment {:.6}; values {}", + value.parameter.parameter.element.name, + value.distance_name, + value.distance, + value.value_set.minimum, + value.value_set.maximum, + value.value_set.increment, + value.value_set.values.len() + ), + DynamicBlockData::LookupParameter(value) => format!( + "{}; {} index {}; description {}", + value.parameter.parameter.element.name, + value.lookup_name, + value.index, + value.lookup_description + ), + DynamicBlockData::PointParameter(value) => format!( + "{}; {}; label at {:.6},{:.6},{:.6}", + value.parameter.parameter.element.name, + value.position_name, + value.definition_label_point.x, + value.definition_label_point.y, + value.definition_label_point.z + ), + DynamicBlockData::PolarParameter(value) => format!( + "{}; angle {}; distance {}; offset {:.6}", + value.parameter.parameter.element.name, + value.angle_name, + value.distance_name, + value.offset + ), + DynamicBlockData::RotationParameter(value) => format!( + "{}; {} = {:.6}; range {:.6}..{:.6}; values {}", + value.parameter.parameter.element.name, + value.angle_name, + value.angle, + value.value_set.minimum, + value.value_set.maximum, + value.value_set.values.len() + ), + DynamicBlockData::UserParameter(value) => format!( + "{}; expression {}; value {}; variable {}", + value.parameter.parameter.element.name, + value.expression, + block_value_text(&value.value), + handle_text(value.associated_variable) + ), + DynamicBlockData::VisibilityParameter(value) => format!( + "{}; states {}; members {}; point {:.6},{:.6},{:.6}", + value.name, + value.states.len(), + value.all_blocks.len(), + value.def_point.x, + value.def_point.y, + value.def_point.z + ), + DynamicBlockData::XYParameter(value) => format!( + "{}; {} = {:.6}; {} = {:.6}", + value.parameter.parameter.element.name, + value.x_label, + value.x_value, + value.y_label, + value.y_value + ), + DynamicBlockData::AngularConstraintParameter(value) => format!( + "{} = {:.6}; dependency {}", + value.expression_name, + value.angle, + handle_text(value.constraint.dependency) + ), + DynamicBlockData::DiametricConstraintParameter(value) + | DynamicBlockData::RadialConstraintParameter(value) => format!( + "{} = {:.6}; dependency {}", + value.expression_name, + value.distance, + handle_text(value.constraint.dependency) + ), + DynamicBlockData::AlignedConstraintParameter(value) + | DynamicBlockData::LinearConstraintParameter(value) + | DynamicBlockData::HorizontalConstraintParameter(value) + | DynamicBlockData::VerticalConstraintParameter(value) => format!( + "{} = {:.6}; dependency {}", + value.expression_name, + value.value, + handle_text(value.constraint.dependency) + ), + DynamicBlockData::ParameterDependencyBody(value) => format!( + "{}; dependency version {}; dimension version {}; class version {}", + value.name, + value.dependency_body_version, + value.dimension_base_version, + value.class_version + ), + DynamicBlockData::MoveAction(value) => format!( + "{}; dependencies {}; actions {}; offsets {:.6},{:.6}; angle {:.6}", + value.action.element.name, + value.action.dependencies.len(), + value.action.action_ids.len(), + value.offsets.offset_x, + value.offsets.offset_y, + value.offsets.angle_offset + ), + DynamicBlockData::FlipAction(value) => format!( + "{}; dependencies {}; actions {}; connections {}", + value.action.element.name, + value.action.dependencies.len(), + value.action.action_ids.len(), + value.connections.len() + ), + DynamicBlockData::RotateAction(value) | DynamicBlockData::ScaleAction(value) => format!( + "{}; dependencies {}; actions {}; base {:.6},{:.6},{:.6}; dependent {}", + value.action.action.element.name, + value.action.action.dependencies.len(), + value.action.action.action_ids.len(), + value.action.base_point.x, + value.action.base_point.y, + value.action.base_point.z, + value.action.dependent + ), + DynamicBlockData::ArrayAction(value) => format!( + "{}; dependencies {}; columns offset {:.6}; rows offset {:.6}", + value.action.element.name, + value.action.dependencies.len(), + value.column_offset, + value.row_offset + ), + DynamicBlockData::LookupAction(value) => format!( + "{}; dependencies {}; table {}×{}; expressions {}; rows {}", + value.action.element.name, + value.action.dependencies.len(), + value.row_count, + value.column_count, + value.expressions.len(), + value.rows.len() + ), + DynamicBlockData::StretchAction(value) => format!( + "{}; dependencies {}; points {}; handles {}; codes {}", + value.action.element.name, + value.action.dependencies.len(), + value.points.len(), + value.handles.len(), + value.codes.len() + ), + DynamicBlockData::PolarStretchAction(value) => format!( + "{}; dependencies {}; points {}; handles {}; codes {}", + value.action.element.name, + value.action.dependencies.len(), + value.points.len(), + value.handles.len(), + value.codes.len() + ), + DynamicBlockData::EvaluationGraph(value) => format!( + "first node {}; nodes {}; edges {}", + value.first_node_id, + value.nodes.len(), + value.edges.len() + ), + DynamicBlockData::AngularConstraintParameterEntity(value) => format!( + "{} = {:.6}; dependency {}", + value.expression_name, + value.angle, + handle_text(value.constraint.dependency) + ), + DynamicBlockData::SolidHistory(value) => format!( + "version {}.{}; root {}; show {}; record {}", + value.major, + value.minor, + value.history_node_id, + value.show_history, + value.record_history + ), + DynamicBlockData::SolidHistoryNode(value) => history_operation_text(value), + DynamicBlockData::PropertiesTable + | DynamicBlockData::AlignmentParameterEntity + | DynamicBlockData::BasePointParameterEntity + | DynamicBlockData::FlipParameterEntity + | DynamicBlockData::LinearParameterEntity + | DynamicBlockData::PointParameterEntity + | DynamicBlockData::RotationParameterEntity + | DynamicBlockData::VisibilityParameterEntity + | DynamicBlockData::XYParameterEntity + | DynamicBlockData::FlipGripEntity + | DynamicBlockData::LinearGripEntity + | DynamicBlockData::PolarGripEntity + | DynamicBlockData::RotationGripEntity + | DynamicBlockData::VisibilityGripEntity + | DynamicBlockData::XYGripEntity => "Semantic entity marker".to_string(), + } +} + +fn dynamic_block_sections( + document: &CadDocument, + cache: &ObjectDataCache, + handle: Handle, +) -> Vec { + let Some(definition) = document.dynamic_definition_for_insert(handle) else { + return Vec::new(); + }; + let mut objects = cache + .dynamic_objects + .iter() + .filter_map(|object_handle| match document.objects.get(object_handle) { + Some(ObjectType::DynamicBlock(value)) + if document.owner_chain_reaches(value.owner, definition) => + { + Some(value) + } + _ => None, + }) + .collect::>(); + let visibility = document.block_visibility_param_for_def(definition); + if objects.is_empty() && visibility.is_none() { + return Vec::new(); + } + objects.sort_by_key(|object| object.handle.value()); + + let mut overview = vec![ + ro("Definition", handle_text(definition)), + ro("Native records", objects.len().to_string()), + ]; + if let Some(parameter) = visibility { + overview.push(ro("Visibility", parameter.name.clone())); + overview.push(ro( + "Visibility states", + parameter + .states + .iter() + .map(|state| state.name.as_str()) + .collect::>() + .join(", "), + )); + } + + let mut sections = vec![PropSection { + title: "Dynamic Block".to_string(), + props: overview, + }]; + if !objects.is_empty() { + sections.push(PropSection { + title: "Dynamic Parameters / Actions".to_string(), + props: objects + .into_iter() + .filter(|object| { + !matches!( + object.data, + DynamicBlockData::SolidHistory(_) + | DynamicBlockData::SolidHistoryNode(_) + ) + }) + .map(|object| { + let class = if object.dxf_name.is_empty() { + object.cpp_class_name.as_str() + } else { + object.dxf_name.as_str() + }; + ro( + format!("{} · {}", handle_text(object.handle), class), + dynamic_data_text(&object.data), + ) + }) + .collect(), + }); + } + sections +} + +fn associative_data_text(data: &AssociativeData) -> String { + match data { + AssociativeData::Unknown => "Unknown".to_string(), + AssociativeData::Dependency(value) => format!( + "status {}; read {}; write {}; attached {}; target {}; body {}", + value.status, + value.is_read_dependency, + value.is_write_dependency, + value.is_attached_to_object, + handle_text(value.dependent_on), + handle_text(value.dependency_body) + ), + AssociativeData::ValueDependency(value) => format!( + "{}; target {}; value {:?}", + value.name, + handle_text(value.dependency.dependent_on), + value.value.value + ), + AssociativeData::GeomDependency(value) => format!( + "target {}; enabled {}; subentity {}", + handle_text(value.dependency.dependent_on), + value.enabled, + value.persistent_subent.class_name + ), + AssociativeData::Action(value) => format!( + "status {}; network {}; body {}; dependencies {}; parameters {}; values {}", + value.geometry_status, + handle_text(value.owning_network), + handle_text(value.action_body), + value.dependencies.len(), + value.owned_parameters.len(), + value.values.len() + ), + AssociativeData::Network(value) => format!( + "actions {}; owned {}; dependencies {}; values {}", + value.actions.len(), + value.owned_actions.len(), + value.action.dependencies.len(), + value.action.values.len() + ), + AssociativeData::ConstraintGroup(value) => format!( + "status {}; dependency {}; actions {}; nodes {}", + value.action.geometry_status, + handle_text(value.dependency), + value.actions.len(), + value.nodes.len() + ), + AssociativeData::Variable(value) => format!( + "{} = {:?}; expression {}; evaluator {}; values {}", + value.name, + value.value.value, + value.expression, + value.evaluator, + value.action.values.len() + ), + AssociativeData::ArrayParameters(value) => format!( + "{}; items {}; rows {}; levels {}", + value.class_name, + value.items.len(), + value.row_count, + value.level_count + ), + AssociativeData::ArrayActionBody(value) => format!( + "parameter block {}; dependencies {}; value parameters {}", + value.parameter_block, + value.parameter_body.dependencies.len(), + value.parameter_body.values.len() + ), + AssociativeData::ArrayModifyActionBody(value) => format!( + "items {}; dependencies {}; value parameters {}", + value.item_locations.len(), + value.body.parameter_body.dependencies.len(), + value.body.parameter_body.values.len() + ), + AssociativeData::DimensionAssociation(value) => format!( + "dimension {}; associativity {}; references {}", + handle_text(value.dimension), + value.associativity, + value.references.iter().flatten().count() + ), + other => format!("{} semantic payload", associative_data_name(other)), + } +} + +fn associative_data_name(data: &AssociativeData) -> &'static str { + match data { + AssociativeData::Unknown => "Unknown", + AssociativeData::Dependency(_) => "Dependency", + AssociativeData::ValueDependency(_) => "Value Dependency", + AssociativeData::GeomDependency(_) => "Geometry Dependency", + AssociativeData::SurfaceActionBody(_) => "Surface Action", + AssociativeData::Action(_) => "Action", + AssociativeData::Network(_) => "Network", + AssociativeData::AnnotationActionBody(_) => "Annotation Action", + AssociativeData::PersSubentManager(_) => "Persistent Subentity Manager", + AssociativeData::EdgeActionParam(_) => "Edge Parameter", + AssociativeData::ConstraintGroup(_) => "Constraint Group", + AssociativeData::Variable(_) => "Variable", + AssociativeData::ActionParam(_) => "Action Parameter", + AssociativeData::CompoundActionParam(_) => "Compound Parameter", + AssociativeData::OsnapPointRefActionParam(_) => "Object Snap Parameter", + AssociativeData::PointRefActionParam(_) => "Point Reference", + AssociativeData::ObjectActionParam(_) => "Object Parameter", + AssociativeData::PathActionParam(_) => "Path Parameter", + AssociativeData::DimDependencyBody(_) => "Dimension Dependency", + AssociativeData::FaceActionParam(_) => "Face Parameter", + AssociativeData::VertexActionParam(_) => "Vertex Parameter", + AssociativeData::AsmBodyActionParam(_) => "ASM Body Parameter", + AssociativeData::ArrayParameters(_) => "Array Parameters", + AssociativeData::ArrayActionBody(_) => "Array Action", + AssociativeData::ArrayModifyActionBody(_) => "Array Modify Action", + AssociativeData::DimensionAssociation(_) => "Dimension Association", + AssociativeData::PersSubentManagerStatic(_) => "Static Subentity Manager", + AssociativeData::ViewRepActionBody(_) => "View Representation Action", + AssociativeData::ViewObjectActionParam(_) => "View Object Parameter", + AssociativeData::ViewRepHatchManager(_) => "View Hatch Manager", + AssociativeData::ViewRepHatchActionParam(_) => "View Hatch Parameter", + AssociativeData::ViewLabelActionParam(_) => "View Label Parameter", + } +} + +fn associative_sections( + document: &CadDocument, + cache: &ObjectDataCache, + handle: Handle, +) -> Vec { + let mut objects = cache + .associative_objects + .iter() + .filter_map(|object_handle| match document.objects.get(object_handle) { + Some(ObjectType::Associative(value)) + if document.owner_chain_reaches(value.owner, handle) + || value.references_handle(handle) => + { + Some(value) + } + _ => None, + }) + .collect::>(); + if objects.is_empty() { + return Vec::new(); + } + objects.sort_by_key(|object| object.handle.value()); + vec![PropSection { + title: "Associative Data".to_string(), + props: objects + .into_iter() + .map(|object| { + let class = if object.dxf_name.is_empty() { + object.cpp_class_name.as_str() + } else { + object.dxf_name.as_str() + }; + ro( + format!("{} · {}", handle_text(object.handle), class), + associative_data_text(&object.data), + ) + }) + .collect(), + }] +} + +/// Semantic DWG object sections connected to one selected entity. +pub fn sections( + document: &CadDocument, + cache: &ObjectDataCache, + handle: Handle, + entity: &EntityType, +) -> Vec { + let mut sections = solid_history_sections(document, cache, entity); + if matches!(entity, EntityType::Insert(_)) { + sections.extend(dynamic_block_sections(document, cache, handle)); + } + if let EntityType::Table(table) = entity { + let mut linked = rustc_hash::FxHashSet::default(); + for row in &table.rows { + for cell in &row.cells { + if let Some(link_handle) = cell.data_link_handle { + if linked.insert(link_handle) { + if let Some(ObjectType::ClassObject(object)) = + document.objects.get(&link_handle) + { + if let Some(section) = class_object_section(&object.data) { + sections.push(section); + } + } + } + } + } + } + } + sections.extend(associative_sections(document, cache, handle)); + + // A directly attached typed object can still be useful when its semantic + // graph contains no backlink (for example a sparse extension dictionary). + if sections.is_empty() { + if let Some(ObjectType::DynamicBlock(object)) = document.objects.get(&handle) { + sections.push(PropSection { + title: "Dynamic Object".to_string(), + props: vec![ro("Decoded data", dynamic_data_text(&object.data))], + }); + } + } + sections +} + +fn preview_text(value: &str, limit: usize) -> String { + if value.chars().count() <= limit { + return value.to_string(); + } + let mut preview: String = value.chars().take(limit).collect(); + preview.push_str(&format!("… ({} chars)", value.chars().count())); + preview +} + +fn bounded_join(values: I, limit: usize) -> String +where + I: IntoIterator, +{ + let mut values = values.into_iter(); + let mut visible = Vec::new(); + for _ in 0..limit { + let Some(value) = values.next() else { + break; + }; + visible.push(value); + } + let hidden = values.count(); + let mut result = visible.join("; "); + if hidden != 0 { + result.push_str(&format!("; … +{hidden}")); + } + result +} + +fn class_object_section( + data: &acadrust::objects::ClassObjectData, +) -> Option { + use acadrust::objects::ClassObjectData; + + let section = match data { + ClassObjectData::Empty => return None, + ClassObjectData::SpatialIndex(value) => PropSection { + title: "Spatial Index".to_string(), + props: vec![ + ro( + "Updated", + format!( + "Julian {}; {} ms", + value.last_updated_julian_day, + value.last_updated_milliseconds + ), + ), + ro( + "Extents", + format!( + "{:.6},{:.6},{:.6} → {:.6},{:.6},{:.6}", + value.min_corner.x, + value.min_corner.y, + value.min_corner.z, + value.max_corner.x, + value.max_corner.y, + value.max_corner.z + ), + ), + ro("Indexed Objects", value.indexed_objects.len().to_string()), + ], + }, + ClassObjectData::LayerFilter(value) => PropSection { + title: "Layer Filter".to_string(), + props: vec![ro("Names", bounded_join(value.names.iter().cloned(), 64))], + }, + ClassObjectData::PartialViewingIndex(value) => PropSection { + title: "Partial Viewing Index".to_string(), + props: vec![ + ro("Has Entries", value.has_entries.to_string()), + ro( + "Entries", + bounded_join( + value.entries.iter().map(|entry| { + format!( + "{}: {:.4},{:.4},{:.4} → {:.4},{:.4},{:.4}", + handle_text(entry.object), + entry.extents_min.x, + entry.extents_min.y, + entry.extents_min.z, + entry.extents_max.x, + entry.extents_max.y, + entry.extents_max.z + ) + }), + 32, + ), + ), + ], + }, + ClassObjectData::VbaProject(value) => PropSection { + title: "VBA Project".to_string(), + props: vec![ + ro( + "Detached Records", + format!( + "{} leading / {} bytes; {} trailing / {} bytes", + value.storage.leading_records.len(), + value + .storage + .leading_records + .iter() + .map(|record| record.data.len()) + .sum::(), + value.storage.trailing_records.len(), + value + .storage + .trailing_records + .iter() + .map(|record| record.data.len()) + .sum::() + ), + ), + ro( + "Streams", + value + .storage + .compound_file + .as_ref() + .map(|storage| storage.root.entries.len()) + .unwrap_or_default() + .to_string(), + ), + ], + }, + ClassObjectData::SectionManager(value) => PropSection { + title: "Section Manager".to_string(), + props: vec![ + ro("Live", value.is_live.to_string()), + ro( + "Sections", + bounded_join( + value.sections.iter().map(|handle| handle_text(*handle)), + 64, + ), + ), + ], + }, + ClassObjectData::SectionSettings(value) => PropSection { + title: "Section Settings".to_string(), + props: vec![ + ro("Current Type", value.current_type.to_string()), + ro( + "Types", + bounded_join( + value.types.iter().map(|setting| { + let geometry = bounded_join( + setting.geometry.iter().map(|geometry| { + format!( + "#{} flags {} layer '{}' linetype '{}' scale {:.4} weight {} transparency {}/{} hatch {} '{}' angle {:.4} spacing {:.4} scale {:.4} color {:?}", + geometry.index, + geometry.flags, + geometry.layer, + geometry.linetype, + geometry.linetype_scale, + geometry.lineweight, + geometry.face_transparency, + geometry.edge_transparency, + geometry.hatch_type, + geometry.hatch_pattern, + geometry.hatch_angle, + geometry.hatch_spacing, + geometry.hatch_scale, + geometry.color + ) + }), + 16, + ); + format!( + "type {} generation {} sources [{}] destination {} file '{}' geometry {} [{}]", + setting.section_type, + setting.generation, + bounded_join( + setting.sources.iter().map(|handle| handle_text(*handle)), + 32 + ), + handle_text(setting.destination_block), + setting.destination_file, + setting.geometry.len(), + geometry + ) + }), + 16, + ), + ), + ], + }, + ClassObjectData::GradientBackground(value) => PropSection { + title: "Gradient Background".to_string(), + props: vec![ + ro("Version", value.class_version.to_string()), + ro( + "Colors", + format!( + "#{:06X} / #{:06X} / #{:06X}", + value.color_top & 0x00FF_FFFF, + value.color_middle & 0x00FF_FFFF, + value.color_bottom & 0x00FF_FFFF + ), + ), + ro( + "Geometry", + format!( + "horizon {:.6}; height {:.6}; rotation {:.6}", + value.horizon, value.height, value.rotation + ), + ), + ], + }, + ClassObjectData::GroundPlaneBackground(value) => PropSection { + title: "Ground Plane Background".to_string(), + props: vec![ + ro("Version", value.class_version.to_string()), + ro( + "Colors", + format!( + "sky #{:06X}/#{:06X}; underground #{:06X}/#{:06X}; near/far #{:06X}/#{:06X}", + value.color_sky_zenith & 0x00FF_FFFF, + value.color_sky_horizon & 0x00FF_FFFF, + value.color_underground_horizon & 0x00FF_FFFF, + value.color_underground_azimuth & 0x00FF_FFFF, + value.color_near & 0x00FF_FFFF, + value.color_far & 0x00FF_FFFF + ), + ), + ], + }, + ClassObjectData::IblBackground(value) => PropSection { + title: "Image Based Lighting Background".to_string(), + props: vec![ + ro("Version", value.class_version.to_string()), + ro("Enabled", value.enabled.to_string()), + ro("Name", value.name.clone()), + ro("Rotation", format!("{:.6}", value.rotation)), + ro("Display Image", value.display_image.to_string()), + ro( + "Secondary Background", + handle_text(value.secondary_background), + ), + ], + }, + ClassObjectData::ImageBackground(value) => PropSection { + title: "Image Background".to_string(), + props: vec![ + ro("Version", value.class_version.to_string()), + ro("File", value.filename.clone()), + ro( + "Display", + format!( + "fit {}; aspect {}; tile {}", + value.fit_to_screen, + value.maintain_aspect_ratio, + value.use_tiling + ), + ), + ro( + "Offset / Scale", + format!( + "{:.6},{:.6} / {:.6},{:.6}", + value.offset.x, value.offset.y, value.scale.x, value.scale.y + ), + ), + ], + }, + ClassObjectData::SkyLightBackground(value) => PropSection { + title: "Sky Light Background".to_string(), + props: vec![ + ro("Version", value.class_version.to_string()), + ro("Sun", handle_text(value.sun)), + ], + }, + ClassObjectData::SolidBackground(value) => PropSection { + title: "Solid Background".to_string(), + props: vec![ + ro("Version", value.class_version.to_string()), + ro("Color", format!("#{:06X}", value.color & 0x00FF_FFFF)), + ], + }, + ClassObjectData::RenderEntry(value) => PropSection { + title: "Render History Entry".to_string(), + props: vec![ + ro("Image", value.image_filename.clone()), + ro( + "Preset / View", + format!("{} / {}", value.preset_name, value.view_name), + ), + ro("Size", format!("{}×{}", value.width, value.height)), + ro( + "Started", + format!( + "{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:03}", + value.start_year, + value.start_month, + value.start_day, + value.start_hour, + value.start_minute, + value.start_second, + value.start_millisecond + ), + ), + ro( + "Finished", + format!( + "{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:03}", + value.end_year, + value.end_month, + value.end_day, + value.end_hour, + value.end_minute, + value.end_second, + value.end_millisecond + ), + ), + ro( + "Statistics", + format!( + "{:.4}s; {} memory; {} materials; {} lights; {} triangles; index {}", + value.render_time, + value.memory_amount, + value.material_count, + value.light_count, + value.triangle_count, + value.display_index + ), + ), + ], + }, + ClassObjectData::MotionPath(value) => PropSection { + title: "Motion Path".to_string(), + props: vec![ + ro("Version", value.class_version.to_string()), + ro( + "Camera / Target / View", + format!( + "{} / {} / {}", + handle_text(value.camera_path), + handle_text(value.target_path), + handle_text(value.view) + ), + ), + ro( + "Playback", + format!( + "{} frames at {} fps; corner deceleration {}", + value.frames, value.frame_rate, value.corner_deceleration + ), + ), + ], + }, + ClassObjectData::CurvePath(value) => PropSection { + title: "Curve Path".to_string(), + props: vec![ + ro("Version", value.class_version.to_string()), + ro("Entity", handle_text(value.entity)), + ], + }, + ClassObjectData::PointPath(value) => PropSection { + title: "Point Path".to_string(), + props: vec![ + ro("Version", value.class_version.to_string()), + ro( + "Point", + format!("{:.8}, {:.8}, {:.8}", value.point.x, value.point.y, value.point.z), + ), + ], + }, + ClassObjectData::TvDeviceProperties(value) => PropSection { + title: "Graphics Device Properties".to_string(), + props: vec![ + ro("Flags", value.flags.to_string()), + ro("Max Regen Threads", value.max_regen_threads.to_string()), + ro( + "Palette / Highlight", + format!( + "{} / {} / {}", + value.use_lut_palette, + value.alternate_highlight, + value.alternate_highlight_color + ), + ), + ro( + "GPU", + format!( + "geometry shader {}; blending {}; AA {:.4}; reserved {:.4}", + value.geometry_shader_usage, + value.blending_mode, + value.antialiasing_level, + value.reserved_double + ), + ), + ], + }, + ClassObjectData::PointCloudDefinition(value) + | ClassObjectData::PointCloudDefinitionEx(value) => PropSection { + title: "Point Cloud Definition".to_string(), + props: vec![ + ro("Version", value.class_version.to_string()), + ro("File", value.source_filename.clone()), + ro("Loaded", value.is_loaded.to_string()), + ro("Points", value.point_count.to_string()), + ro( + "Extents", + format!( + "{:.6},{:.6},{:.6} → {:.6},{:.6},{:.6}", + value.extents_min.x, + value.extents_min.y, + value.extents_min.z, + value.extents_max.x, + value.extents_max.y, + value.extents_max.z + ), + ), + ], + }, + ClassObjectData::PointCloudDefinitionReactor(value) + | ClassObjectData::PointCloudDefinitionReactorEx(value) => PropSection { + title: "Point Cloud Reactor".to_string(), + props: vec![ro("Version", value.class_version.to_string())], + }, + ClassObjectData::PointCloudColorMap(value) => PropSection { + title: "Point Cloud Color Map".to_string(), + props: vec![ + ro("Version", value.class_version.to_string()), + ro( + "Defaults", + format!( + "intensity '{}'; elevation '{}'; classification '{}'", + value.default_intensity_scheme, + value.default_elevation_scheme, + value.default_classification_scheme + ), + ), + ro( + "Ramps", + bounded_join( + value.color_ramps.iter().map(|ramp| { + format!( + "v{} [{}]", + ramp.class_version, + ramp.color_schemes.join(", ") + ) + }), + 32, + ), + ), + ro( + "Classification Ramps", + bounded_join( + value.classification_color_ramps.iter().map(|ramp| { + format!( + "v{} [{}]", + ramp.class_version, + ramp.color_schemes.join(", ") + ) + }), + 32, + ), + ), + ], + }, + ClassObjectData::NavisworksModelDefinition(value) => PropSection { + title: "Coordination Model Definition".to_string(), + props: vec![ + ro("Path", value.path.clone()), + ro( + "State", + format!( + "flags {}; status {}; host visible {}", + value.flags, value.status, value.host_drawing_visibility + ), + ), + ro( + "Extents", + format!( + "{:.6},{:.6},{:.6} → {:.6},{:.6},{:.6}", + value.extents_min.x, + value.extents_min.y, + value.extents_min.z, + value.extents_max.x, + value.extents_max.y, + value.extents_max.z + ), + ), + ], + }, + ClassObjectData::ContextDataManager(value) => PropSection { + title: "Context Data Manager".to_string(), + props: vec![ + ro("Object Context", handle_text(value.object_context)), + ro( + "Sub-Managers", + bounded_join( + value.sub_managers.iter().map(|manager| { + format!( + "{} [{}]", + handle_text(manager.handle), + bounded_join( + manager.entries.iter().map(|entry| format!( + "{}={}", + entry.name, + handle_text(entry.item) + )), + 32 + ) + ) + }), + 32, + ), + ), + ], + }, + ClassObjectData::SunStudy(value) => PropSection { + title: "Sun Study".to_string(), + props: vec![ + ro("Name", value.setup_name.clone()), + ro("Description", value.description.clone()), + ro( + "Output", + format!( + "type {}; subset {} '{} / {}'; calendar {}; range {}", + value.output_type, + value.use_subset, + value.sheet_set_name, + value.sheet_subset_name, + value.select_dates_from_calendar, + value.select_range_of_dates + ), + ), + ro( + "Dates", + bounded_join( + value.dates.iter().map(|date| { + format!("Julian {} @ {} ms", date.julian_day, date.milliseconds) + }), + 64, + ), + ), + ro( + "Time", + format!( + "{}..{} every {}; active hours {}", + value.start_time, + value.end_time, + value.interval, + value.hours.iter().filter(|enabled| **enabled).count() + ), + ), + ro( + "Layout", + format!( + "shade {}; viewports {}; {}×{} spacing {:.6}; lock {}; labels {}", + value.shade_plot_type, + value.viewport_count, + value.rows, + value.columns, + value.spacing, + value.lock_viewports, + value.label_viewports + ), + ), + ro( + "References", + format!( + "wizard {}; view {}; visual style {}; text style {}", + handle_text(value.page_setup_wizard), + handle_text(value.view), + handle_text(value.visual_style), + handle_text(value.text_style) + ), + ), + ], + }, + ClassObjectData::DataTable(value) => PropSection { + title: format!("Data Table · {}", value.name), + props: vec![ + ro( + "Shape", + format!( + "{} rows; {} columns; flags {}", + value.row_count, + value.columns.len(), + value.flags + ), + ), + ro( + "Columns", + bounded_join( + value.columns.iter().map(|column| { + format!( + "{} type {} rows [{}]", + column.name, + column.value_type, + bounded_join( + column.rows.iter().map(|cell| format!( + "{} | {:.8} | {}", + cell.integer, + cell.real, + preview_text(&cell.text, 128) + )), + 32 + ) + ) + }), + 32, + ), + ), + ], + }, + ClassObjectData::DataLink(value) => PropSection { + title: "Data Link".to_string(), + props: vec![ + ro("Adapter", value.data_adapter.clone()), + ro("Description", value.description.clone()), + ro("Tooltip", value.tooltip.clone()), + ro( + "Connection", + preview_text(&value.connection_string, 1024), + ), + ro( + "Options", + format!( + "{} / update {}; flags {}; path {}; status {} '{}'", + value.option, + value.update_option, + value.flags, + value.path_option, + value.status_flags, + value.update_status + ), + ), + ro( + "Updated", + format!( + "{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:03}", + value.year, + value.month, + value.day, + value.hour, + value.minute, + value.second, + value.millisecond + ), + ), + ro("Owner", handle_text(value.hard_owner)), + ro( + "Custom Data", + bounded_join( + value.custom_data.iter().map(|entry| { + format!( + "{}='{}'", + handle_text(entry.target), + preview_text(&entry.value, 256) + ) + }), + 64, + ), + ), + ], + }, + ClassObjectData::PersistentSubentityManager(value) => PropSection { + title: "Persistent Subentity Manager".to_string(), + props: vec![ + ro("Version", value.class_version.to_string()), + ro( + "Reserved", + format!("{}/{}", value.reserved_zero, value.reserved_two), + ), + ro( + "Steps", + format!( + "{} declared; [{}]", + value.associated_step_count, + bounded_join(value.steps.iter().map(ToString::to_string), 128) + ), + ), + ro( + "Subentities", + format!( + "{} declared; [{}]", + value.associated_subentity_count, + bounded_join(value.subentities.iter().map(ToString::to_string), 128) + ), + ), + ], + }, + ClassObjectData::GeoMapImage(value) => PropSection { + title: "Geographic Map Image".to_string(), + props: vec![ + ro("Version", value.class_version.to_string()), + ro( + "Origin / Size", + format!( + "{:.8},{:.8},{:.8} / {:.4}×{:.4}", + value.origin.x, + value.origin.y, + value.origin.z, + value.image_size.x, + value.image_size.y + ), + ), + ro( + "Display", + format!( + "properties {}; clip {}; brightness {}; contrast {}; fade {}", + value.display_properties, + value.clipping_enabled, + value.brightness, + value.contrast, + value.fade + ), + ), + ], + }, + ClassObjectData::DetailViewStyle(value) => PropSection { + title: format!("Detail View Style · {}", value.base.display_name), + props: vec![ + ro("Description", value.base.description.clone()), + ro( + "State", + format!( + "base v{} flags {}; modified {}; v{} flags {}", + value.base.class_version, + value.base.flags, + value.base.modified_for_recompute, + value.class_version, + value.flags + ), + ), + ro( + "Identifier", + format!( + "style {}; color {:?}; height {:.6}; excluded '{}'; offset {:.6}; placement {}", + handle_text(value.identifier_style), + value.identifier_color, + value.identifier_height, + value.identifier_excluded_characters, + value.identifier_offset, + value.identifier_placement + ), + ), + ro( + "Arrow / Boundary", + format!( + "symbol {} color {:?} size {:.6}; boundary {} weight {} color {:?}", + handle_text(value.arrow_symbol), + value.arrow_symbol_color, + value.arrow_symbol_size, + handle_text(value.boundary_linetype), + value.boundary_lineweight, + value.boundary_color + ), + ), + ro( + "View Label", + format!( + "style {} color {:?} height {:.6}; attachment {}; offset {:.6}; alignment {}; pattern '{}'", + handle_text(value.view_label_text_style), + value.view_label_text_color, + value.view_label_text_height, + value.view_label_attachment, + value.view_label_offset, + value.view_label_alignment, + value.view_label_pattern + ), + ), + ro( + "Connection / Border", + format!( + "{} weight {} color {:?}; {} weight {} color {:?}; model edge {}", + handle_text(value.connection_linetype), + value.connection_lineweight, + value.connection_color, + handle_text(value.border_linetype), + value.border_lineweight, + value.border_color, + value.model_edge + ), + ), + ], + }, + ClassObjectData::SectionViewStyle(value) => PropSection { + title: format!("Section View Style · {}", value.base.display_name), + props: vec![ + ro("Description", value.base.description.clone()), + ro( + "State", + format!( + "base v{} flags {}; modified {}; v{} flags {}; reserved {:?}", + value.base.class_version, + value.base.flags, + value.base.modified_for_recompute, + value.class_version, + value.flags, + value.reserved_flags + ), + ), + ro( + "Identifier / Arrows", + format!( + "style {} color {:?} height {:.6}; symbols {}/{} color {:?} size {:.6}; excluded '{}'; extension {:.6}", + handle_text(value.identifier_style), + value.identifier_color, + value.identifier_height, + handle_text(value.arrow_start_symbol), + handle_text(value.arrow_end_symbol), + value.arrow_symbol_color, + value.arrow_symbol_size, + value.identifier_excluded_characters, + value.arrow_symbol_extension_length + ), + ), + ro( + "Plane / Bend", + format!( + "plane {} weight {} color {:?}; bend {} weight {} color {:?}; lengths {:.6}/{:.6}", + handle_text(value.plane_linetype), + value.plane_lineweight, + value.plane_color, + handle_text(value.bend_linetype), + value.bend_lineweight, + value.bend_color, + value.bend_line_length, + value.end_line_length + ), + ), + ro( + "View Label", + format!( + "style {} color {:?} height {:.6}; attachment {}; offset {:.6}; alignment {}; pattern '{}'", + handle_text(value.view_label_text_style), + value.view_label_text_color, + value.view_label_text_height, + value.view_label_attachment, + value.view_label_offset, + value.view_label_alignment, + value.view_label_pattern + ), + ), + ro( + "Hatch", + format!( + "color {:?}; background {:?}; pattern '{}'; scale {:.6}; transparency {}; angles [{}]", + value.hatch_color, + value.hatch_background_color, + value.hatch_pattern, + value.hatch_scale, + value.hatch_transparency, + bounded_join(value.hatch_angles.iter().map(|angle| format!("{angle:.6}")), 64) + ), + ), + ro( + "Placement", + format!( + "identifier {} offset {:.6}; arrow {}; overshoot {:.6}", + value.identifier_position, + value.identifier_offset, + value.arrow_position, + value.end_line_overshoot + ), + ), + ], + }, + ClassObjectData::AcMeCommandHistory(value) => PropSection { + title: "Model Documentation Command History".to_string(), + props: vec![ro("Version", value.class_version.to_string())], + }, + ClassObjectData::AcMeScope(value) => PropSection { + title: "Model Documentation Scope".to_string(), + props: vec![ro("Version", value.class_version.to_string())], + }, + ClassObjectData::AcMeStateManager(value) => PropSection { + title: "Model Documentation State Manager".to_string(), + props: vec![ro("Version", value.class_version.to_string())], + }, + ClassObjectData::CsacDocumentOptions(value) => PropSection { + title: "Model Documentation Options".to_string(), + props: vec![ro("Version", value.class_version.to_string())], + }, + ClassObjectData::ViewRepSourceManager(value) => PropSection { + title: "View Representation Source Manager".to_string(), + props: vec![ + ro("Has Source", value.has_source.to_string()), + ro("Source", handle_text(value.source)), + ro("Status", value.status.to_string()), + ], + }, + ClassObjectData::ViewRepStandard(value) => PropSection { + title: "View Representation Standard".to_string(), + props: vec![ro("Values", format!("{:?}", value.values))], + }, + ClassObjectData::ViewRepOrientationDefinition => PropSection { + title: "View Representation Orientation Definition".to_string(), + props: Vec::new(), + }, + ClassObjectData::ViewRepOrientation(value) => PropSection { + title: "View Representation Orientation".to_string(), + props: vec![ + ro( + "Camera", + format!("{:.8},{:.8},{:.8}", value.camera.x, value.camera.y, value.camera.z), + ), + ro( + "Target", + format!("{:.8},{:.8},{:.8}", value.target.x, value.target.y, value.target.z), + ), + ro( + "Normal", + format!("{:.8},{:.8},{:.8}", value.normal.x, value.normal.y, value.normal.z), + ), + ], + }, + ClassObjectData::ViewRepSectionDefinition(value) => PropSection { + title: "View Representation Section Definition".to_string(), + props: vec![ + ro("Version", value.version.to_string()), + ro("Depth", format!("{:.8}", value.section_depth)), + ro("Flags", format!("{:?}", value.flags)), + ], + }, + ClassObjectData::ViewRepModelSpaceViewSelectionSet(value) => PropSection { + title: "View Representation Selection Set".to_string(), + props: vec![ + ro("Version", value.version.to_string()), + ro( + "Entities", + bounded_join( + value.entities.iter().map(|handle| handle_text(*handle)), + 128, + ), + ), + ], + }, + ClassObjectData::ViewRep(value) => PropSection { + title: format!("View Representation · {}", value.name), + props: vec![ + ro( + "Header", + format!( + "{:?}; scale {}; status {}; description '{}'", + value.header_values, value.scale, value.header_status, value.description + ), + ), + ro( + "Source", + format!( + "id {}; enabled {}; version {}; model {}; guid {:08X}-{:04X}-{:04X}-{:?}", + value.source_id, + value.source_enabled, + value.source_version, + value.model_id, + value.guid.data1, + value.guid.data2, + value.guid.data3, + value.guid.data4 + ), + ), + ro( + "Geometry", + format!( + "marker {}; transform v{} {:?}; database {}; geometry v{} marker {}; sketches {}", + value.marker, + value.transform_version, + value.transform, + value.database_id, + value.geometry_version, + value.geometry_marker, + value.sketches.len() + ), + ), + ro( + "References", + format!( + "related {:?}; source manager {}; owned {:?}; optional {:?}; orientation {}; linked {:?}; style {}", + value.related_objects.map(handle_text), + handle_text(value.source_manager), + value.owned_objects.map(handle_text), + value.optional_objects.map(handle_text), + handle_text(value.orientation), + value.linked_views.map(handle_text), + handle_text(value.style) + ), + ), + ro( + "Placement", + format!( + "{:.8},{:.8}; rotation {:.8}; active {}; projection {}", + value.position.x, + value.position.y, + value.rotation, + value.is_active, + value.projection + ), + ), + ro( + "Actions / Parent", + format!( + "mode {}; action {}; has parent {}; parent {}; section paths {}", + value.action_mode, + value.action.map(handle_text).unwrap_or_else(|| "None".to_string()), + value.has_parent, + handle_text(value.parent), + value.section_sketches.len() + ), + ), + ro( + "Tail / Block Path", + format!( + "version {}; state {}; id {}; path count {}; version {}; id {}; enabled {}; entries {}", + value.tail_version, + value.tail_state, + value.tail_id, + value.path_count, + value.path_version, + value.path_id, + value.has_block_path, + value.block_path.as_ref().map(|path| path.entries.len()).unwrap_or_default() + ), + ), + ], + }, + ClassObjectData::ViewRepModelSpaceSource(value) => PropSection { + title: "View Representation Model Source".to_string(), + props: vec![ + ro( + "Header", + format!( + "enabled {}; values {:?}; source v{} status {}; tail {:?}", + value.enabled, + value.header_values, + value.source_version, + value.source_status, + value.tail_values + ), + ), + ro("Transform", format!("{:?}", value.transform)), + ro( + "References", + format!( + "model {}; references {:?}; orientation {}", + handle_text(value.model), + value.references.map(handle_text), + handle_text(value.orientation) + ), + ), + ro( + "GUID", + format!( + "{:08X}-{:04X}-{:04X}-{:?}", + value.guid.data1, + value.guid.data2, + value.guid.data3, + value.guid.data4 + ), + ), + ], + }, + ClassObjectData::LightList(_) + | ClassObjectData::Sun(_) + | ClassObjectData::RenderSettings(_) + | ClassObjectData::MentalRayRenderSettings(_) + | ClassObjectData::RapidRtRenderSettings(_) + | ClassObjectData::RenderEnvironment(_) + | ClassObjectData::RenderGlobal(_) => return None, + }; + Some(section) +} + +fn semantic_property_text( + value: &acadrust::objects::SemanticPropertyValue, +) -> String { + use acadrust::objects::SemanticPropertyValue; + match value { + SemanticPropertyValue::Text(value) => preview_text(value, 512), + SemanticPropertyValue::Bool(value) => value.to_string(), + SemanticPropertyValue::Byte(value) => value.to_string(), + SemanticPropertyValue::Int16(value) => value.to_string(), + SemanticPropertyValue::Int32(value) => value.to_string(), + SemanticPropertyValue::Int64(value) => value.to_string(), + SemanticPropertyValue::Double(value) => format!("{value:.12}"), + SemanticPropertyValue::Handle(value) => handle_text(*value), + SemanticPropertyValue::Binary(value) => format!("{} bytes", value.len()), + } +} + +fn auxiliary_object_property( + object: &ObjectType, +) -> Option { + use acadrust::objects::DataObjectData; + + let (label, value) = match object { + ObjectType::TableContent(table) => ( + format!("Table Content {}", handle_text(table.common.handle)), + format!( + "{} rows × {} columns; {} merged ranges; {} fields", + table.rows.len(), + table.columns.len(), + table.merged_ranges.len(), + table.field_handles.len() + ), + ), + ObjectType::ImageDefinitionReactor(value) => ( + format!("Image Reactor {}", handle_text(value.handle)), + format!( + "owner {}; image {}", + handle_text(value.owner), + handle_text(value.image_handle) + ), + ), + ObjectType::BookColor(value) => ( + format!("Book Color {}", handle_text(value.handle)), + format!("{} / {} = {:?}", value.book_name, value.color_name, value.color), + ), + ObjectType::PlaceHolder(value) => ( + format!("Placeholder {}", handle_text(value.handle)), + format!("owner {}", handle_text(value.owner)), + ), + ObjectType::FieldList(value) => ( + format!("Field List {}", handle_text(value.handle)), + format!( + "owner {}; unknown {}; fields [{}]", + handle_text(value.owner), + value.unknown, + bounded_join(value.fields.iter().map(|handle| handle_text(*handle)), 128) + ), + ), + ObjectType::DataObject(value) => { + let text = match &value.data { + DataObjectData::BreakPointRef => "break point reference".to_string(), + DataObjectData::BreakData(data) => format!( + "v{} dimension {}; reserved {}; point references {}", + data.version, + handle_text(data.dimension_reference), + handle_text(data.reserved_reference), + data.point_references.len() + ), + DataObjectData::CellStyleMap(data) => { + format!("{} named cell styles", data.cells.len()) + } + DataObjectData::AcDsRecord => "AcDs record".to_string(), + DataObjectData::AcDsSchema => "AcDs schema".to_string(), + DataObjectData::Dummy => "dummy helper".to_string(), + DataObjectData::IdBuffer(data) => format!( + "flags {}; objects [{}]", + data.flags, + bounded_join( + data.object_ids.iter().map(|handle| handle_text(*handle)), + 128 + ) + ), + DataObjectData::Index(data) => format!( + "updated Julian {} @ {} ms", + data.last_updated_julian_day, data.last_updated_milliseconds + ), + DataObjectData::LayerIndex(data) => format!( + "updated Julian {} @ {} ms; entries [{}]", + data.last_updated_julian_day, + data.last_updated_milliseconds, + bounded_join( + data.entries.iter().map(|entry| format!( + "{}: {} layers, buffer {}", + entry.name, + entry.layer_count, + handle_text(entry.id_buffer) + )), + 64 + ) + ), + DataObjectData::LongTransaction => "long transaction".to_string(), + DataObjectData::ObjectPointer => "object pointer".to_string(), + DataObjectData::PartialViewingFilter(_) => { + "partial viewing filter".to_string() + } + DataObjectData::TableGeometry(data) => format!( + "{} rows × {} columns; {} cell geometry records; {} primitives", + data.rows, + data.columns, + data.cells.len(), + data.cells.iter().map(|cell| cell.geometry.len()).sum::() + ), + }; + ( + format!("{} {}", value.dxf_name(), handle_text(value.handle)), + format!("owner {}; {text}", handle_text(value.owner)), + ) + } + ObjectType::RegisteredClass(value) => ( + format!( + "{} {}", + if value.dxf_name.is_empty() { + value.cpp_class_name.as_str() + } else { + value.dxf_name.as_str() + }, + handle_text(value.handle) + ), + format!( + "owner {}; properties [{}]; payload {} bits / {} records; references {}", + handle_text(value.owner), + bounded_join( + value.properties.iter().map(|property| format!( + "{}:{}={}", + property.subclass, + property.code, + semantic_property_text(&property.value) + )), + 64 + ), + value.payload.bit_count, + value.payload.records.len(), + value.object_ids.len() + ), + ), + ObjectType::ProxyObject(value) => ( + format!( + "Proxy Object {}", + handle_text(value.handle) + ), + format!( + "class {} / id {}; subclass '{}'; version {}/{} maintenance {}; DXF {}; payload {} + {} bits; references {}", + value.class_id, + value.proxy_id, + value.dxf_subclass, + value.version, + value.dwg_version, + value.maintenance_version, + value.from_dxf, + value.payload.bit_count, + value.text_payload.bit_count, + value.object_ids.len() + ), + ), + ObjectType::Unknown { + type_name, + handle, + owner, + raw_dxf_codes, + raw_dwg_data, + raw_dwg_handle_bits, + .. + } => ( + format!("Unknown Object {}", handle_text(*handle)), + format!( + "{}; owner {}; DXF codes {}; DWG bytes {}; handle bits {}", + type_name, + handle_text(*owner), + raw_dxf_codes.as_ref().map(Vec::len).unwrap_or_default(), + raw_dwg_data.as_ref().map(Vec::len).unwrap_or_default(), + raw_dwg_handle_bits + ), + ), + _ => return None, + }; + Some(ro(label, value)) +} + +/// Drawing-level standard object data prepared on the file-open worker. +fn build_document_sections(document: &CadDocument) -> Vec { + use acadrust::objects::ClassObjectData; + + let mut sections = vec![PropSection { + title: "Drawing".to_string(), + props: vec![ + ro("Version", format!("{:?}", document.version)), + ro("Entities", document.entity_count().to_string()), + ro("Objects", document.objects.len().to_string()), + ro("Layers", document.layers.len().to_string()), + ro("Blocks", document.block_records.len().to_string()), + ], + }]; + + let mut objects = document.objects.iter().collect::>(); + objects.sort_by_key(|(handle, _)| handle.value()); + for (_, object) in objects { + match object { + ObjectType::GeoData(value) => { + sections.push(PropSection { + title: "Geographic Location".to_string(), + props: vec![ + ro("Version", value.version.to_string()), + ro("Coordinate Type", value.coordinate_type.to_string()), + ro( + "Design Point", + format!( + "{:.8}, {:.8}, {:.8}", + value.design_point.x, + value.design_point.y, + value.design_point.z + ), + ), + ro( + "Reference Point", + format!( + "{:.8}, {:.8}, {:.8}", + value.reference_point.x, + value.reference_point.y, + value.reference_point.z + ), + ), + ro( + "North", + format!( + "{:.8}, {:.8}", + value.north_direction.x, value.north_direction.y + ), + ), + ro( + "Up", + format!( + "{:.8}, {:.8}, {:.8}", + value.up_direction.x, + value.up_direction.y, + value.up_direction.z + ), + ), + ro( + "Units / Scale", + format!( + "horizontal {} × {:.12}; vertical {} × {:.12}; user {:.12}", + value.horizontal_units, + value.horizontal_unit_scale, + value.vertical_units, + value.vertical_unit_scale, + value.user_scale_factor + ), + ), + ro( + "Sea Level", + format!( + "enabled {}; elevation {:.8}; radius {:.8}", + value.sea_level_correction, + value.sea_level_elevation, + value.coordinate_projection_radius + ), + ), + ro( + "Coordinate System", + preview_text(&value.coordinate_system_definition, 1024), + ), + ro("Datum", preview_text(&value.coordinate_system_datum, 512)), + ro("WKT", preview_text(&value.coordinate_system_wkt, 1024)), + ro( + "Transformation Mesh", + format!( + "{} points; {} faces", + value.mesh_points.len(), + value.mesh_faces.len() + ), + ), + ro( + "Observation", + format!( + "{} → {}; coverage {}; RSS {}", + value.observation_from_tag, + value.observation_to_tag, + value.observation_coverage_tag, + value.geo_rss_tag + ), + ), + ], + }); + } + ObjectType::RasterVariables(value) => { + sections.push(PropSection { + title: "Raster Settings".to_string(), + props: vec![ + ro("Version", value.class_version.to_string()), + ro("Display Frame", value.display_image_frame.to_string()), + ro("Quality", value.image_quality.to_string()), + ro("Units", value.units.to_string()), + ], + }); + } + ObjectType::WipeoutVariables(value) => { + sections.push(PropSection { + title: "Wipeout Settings".to_string(), + props: vec![ro("Display Frame", value.display_frame.to_string())], + }); + } + ObjectType::ClassObject(value) => match &value.data { + ClassObjectData::LightList(list) => { + sections.push(PropSection { + title: "Light List".to_string(), + props: vec![ + ro("Version", list.class_version.to_string()), + ro("Lights", list.lights.len().to_string()), + ro( + "Entries", + bounded_join( + list.lights.iter().map(|light| { + format!( + "{}={}", + light.name, + handle_text(light.handle) + ) + }), + 64, + ), + ), + ], + }); + } + ClassObjectData::Sun(sun) => { + sections.push(PropSection { + title: "Sun".to_string(), + props: vec![ + ro("Version", sun.class_version.to_string()), + ro("On", sun.is_on.to_string()), + ro("Color", format!("{:?}", sun.color)), + ro("Intensity", format!("{:.6}", sun.intensity)), + ro( + "Date / Time", + format!( + "Julian {}; {} ms; DST {}", + sun.julian_day, + sun.milliseconds, + sun.is_daylight_savings_on + ), + ), + ro( + "Shadows", + format!( + "enabled {}; type {}; map {}; softness {}", + sun.has_shadow, + sun.shadow_type, + sun.shadow_map_size, + sun.shadow_softness + ), + ), + ], + }); + } + ClassObjectData::RenderSettings(settings) => { + sections.push(PropSection { + title: "Render Settings".to_string(), + props: vec![ + ro("Version", settings.class_version.to_string()), + ro("Name", settings.name.clone()), + ro("Description", settings.description.clone()), + ro( + "Flags", + format!( + "fog {}; fog background {}; backfaces {}; environment {}", + settings.fog_enabled, + settings.fog_background_enabled, + settings.backfaces_enabled, + settings.environment_image_enabled + ), + ), + ro( + "Environment Image", + settings.environment_image_filename.clone(), + ), + ro( + "Display", + format!( + "index {}; predefined {}", + settings.display_index, settings.has_predefined + ), + ), + ], + }); + } + ClassObjectData::MentalRayRenderSettings(settings) => { + sections.push(PropSection { + title: "Mental Ray".to_string(), + props: vec![ + ro("Preset", settings.base.name.clone()), + ro( + "Version", + format!( + "base {}; mental ray {}", + settings.base.class_version, settings.version + ), + ), + ro( + "Preset State", + format!( + "description '{}'; display {}; predefined {}; fog {}/{}; backfaces {}; environment {} '{}'", + settings.base.description, + settings.base.display_index, + settings.base.has_predefined, + settings.base.fog_enabled, + settings.base.fog_background_enabled, + settings.base.backfaces_enabled, + settings.base.environment_image_enabled, + settings.base.environment_image_filename + ), + ), + ro( + "Sampling", + format!( + "{}..{}; filter {} {:.4}×{:.4}; contrast {:?}", + settings.sampling_min, + settings.sampling_max, + settings.sampling_filter, + settings.sampling_filter_width, + settings.sampling_filter_height, + settings.sampling_contrast + ), + ), + ro( + "Ray Tracing", + format!( + "{}; depth {:?}; shadows {} / maps {}", + settings.ray_tracing_enabled, + settings.ray_trace_depth, + settings.shadow_mode, + settings.shadow_maps_enabled + ), + ), + ro( + "Global Illumination", + format!( + "{}; samples {}; radius {} {:.6}; photons {}; trace {:?}", + settings.global_illumination_enabled, + settings.global_illumination_sample_count, + settings.global_illumination_sample_radius_enabled, + settings.global_illumination_sample_radius, + settings.photons_per_light, + settings.photon_trace_depth + ), + ), + ro( + "Final Gather", + format!( + "{}; rays {}; state {:?}; radius {:?}", + settings.final_gathering_enabled, + settings.final_gathering_ray_count, + settings.final_gathering_sample_radius_state, + settings.final_gathering_sample_radius + ), + ), + ro( + "Diagnostics", + format!( + "mode {}; grid {} size {:.6}; photon {}; BSP {}; samples {}; luminance {:.6}", + settings.diagnostics_mode, + settings.diagnostics_grid_mode, + settings.diagnostics_grid_size, + settings.diagnostics_photon_mode, + settings.diagnostics_bsp_mode, + settings.diagnostics_samples_mode, + settings.light_luminance_scale + ), + ), + ro( + "Execution", + format!( + "export {}; description '{}'; tile {} order {}; memory {}; energy {:.6}", + settings.export_mi_enabled, + settings.description, + settings.tile_size, + settings.tile_order, + settings.memory_limit, + settings.energy_multiplier + ), + ), + ], + }); + } + ClassObjectData::RapidRtRenderSettings(settings) => { + sections.push(PropSection { + title: "Rapid RT".to_string(), + props: vec![ + ro("Preset", settings.base.name.clone()), + ro( + "Version", + format!( + "base {}; Rapid RT {}", + settings.base.class_version, settings.version + ), + ), + ro( + "Preset State", + format!( + "description '{}'; display {}; predefined {}; fog {}/{}; backfaces {}; environment {} '{}'", + settings.base.description, + settings.base.display_index, + settings.base.has_predefined, + settings.base.fog_enabled, + settings.base.fog_background_enabled, + settings.base.backfaces_enabled, + settings.base.environment_image_enabled, + settings.base.environment_image_filename + ), + ), + ro( + "Target / Level / Time", + format!( + "{} / {} / {}", + settings.render_target, + settings.render_level, + settings.render_time + ), + ), + ro( + "Lighting / Filter", + format!( + "{} / {} {:.4}×{:.4}", + settings.lighting_model, + settings.filter_type, + settings.filter_width, + settings.filter_height + ), + ), + ], + }); + } + ClassObjectData::RenderEnvironment(environment) => { + sections.push(PropSection { + title: "Render Environment".to_string(), + props: vec![ + ro("Version", environment.class_version.to_string()), + ro( + "Fog", + format!( + "{}; background {}; color {:?}; density {:.4}..{:.4}; distance {:.4}..{:.4}", + environment.fog_enabled, + environment.fog_background_enabled, + environment.fog_color, + environment.fog_density_near, + environment.fog_density_far, + environment.fog_distance_near, + environment.fog_distance_far + ), + ), + ro( + "Environment Image", + format!( + "{} · {}", + environment.environment_image_enabled, + environment.environment_image_filename + ), + ), + ], + }); + } + ClassObjectData::RenderGlobal(global) => { + sections.push(PropSection { + title: "Render Output".to_string(), + props: vec![ + ro("Version", global.class_version.to_string()), + ro( + "Procedure / Destination", + format!("{} / {}", global.procedure, global.destination), + ), + ro( + "Image", + format!( + "{}×{}; save {} · {}", + global.image_width, + global.image_height, + global.save_enabled, + global.save_filename + ), + ), + ro( + "Display", + format!( + "predefined first {}; high-level info {}", + global.predefined_presets_first, + global.high_level_info + ), + ), + ], + }); + } + other => { + if let Some(section) = class_object_section(other) { + sections.push(section); + } + } + }, + _ => {} + } + } + + if !document.dgn_ls_definitions.is_empty() { + let mut definitions = document + .dgn_ls_definitions + .values() + .collect::>(); + definitions.sort_by_key(|definition| definition.handle.value()); + let omitted = definitions.len().saturating_sub(128); + let mut props = definitions + .into_iter() + .take(128) + .map(|definition| { + ro( + definition.name.clone(), + format!( + "handle {}; root {}; components {}", + handle_text(definition.handle), + handle_text(definition.root_component), + document.dgn_ls_components.len() + ), + ) + }) + .collect::>(); + if omitted != 0 { + props.push(ro("More Definitions", format!("{omitted} omitted"))); + } + sections.push(PropSection { + title: "DGN Line Styles".to_string(), + props, + }); + } + let mut auxiliary = document + .objects + .values() + .filter_map(auxiliary_object_property) + .collect::>(); + auxiliary.sort_by(|left, right| left.label.cmp(&right.label)); + let auxiliary_omitted = auxiliary.len().saturating_sub(256); + auxiliary.truncate(256); + if auxiliary_omitted != 0 { + auxiliary.push(ro( + "More Standard Objects", + format!("{auxiliary_omitted} omitted"), + )); + } + if !auxiliary.is_empty() { + sections.push(PropSection { + title: "Standard Object Data".to_string(), + props: auxiliary, + }); + } + let mut per_title = rustc_hash::FxHashMap::default(); + let mut omitted = rustc_hash::FxHashMap::default(); + sections.retain(|section| { + if section.title == "Drawing" { + return true; + } + let shown = per_title.entry(section.title.clone()).or_insert(0usize); + if *shown < 4 { + *shown += 1; + true + } else { + *omitted.entry(section.title.clone()).or_insert(0usize) += 1; + false + } + }); + if !omitted.is_empty() { + let mut entries = omitted.into_iter().collect::>(); + entries.sort_by(|left, right| left.0.cmp(&right.0)); + sections.push(PropSection { + title: "Additional Standard Objects".to_string(), + props: entries + .into_iter() + .map(|(name, count)| ro(name, format!("{count} more records"))) + .collect(), + }); + } + sections +} diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 5d1bf52f9..9cb496f2c 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -385,6 +385,10 @@ pub struct DerivedCaches { pub meshes: HashMap, /// Block-definition solid meshes, block-local frame (instanced per INSERT). (#123) pub block_meshes: HashMap, + /// Non-graphical DWG object relationships and Drawing-property rows. + /// Prepared during file open so entity selection/deselection never scans + /// the complete object store on the UI thread. + pub object_data: crate::entities::object_data::ObjectDataCache, /// Number of entities removed by the corrupt-entity guard during load. /// Reported back to the UI so the user knows when a file had parser-junk /// entities silently dropped. @@ -441,6 +445,7 @@ fn build_derived_caches_impl( // the memoised set so each reference re-reads / re-fetches once here (and // stays cached across this document's later cache rebuilds). crate::scene::model::image_model::clear_image_cache(); + let object_data = crate::entities::object_data::build_cache(doc); // model-space block handle (same logic as Scene::model_space_block_handle) let model_block = doc .objects @@ -665,6 +670,7 @@ fn build_derived_caches_impl( images, meshes, block_meshes, + object_data, corrupt_dropped: 0, xref_dropped: 0, xrefs: Vec::new(), @@ -1279,6 +1285,23 @@ struct BlockMeshInherit { layer0_material: crate::scene::model::material_model::MeshMaterial, } +#[derive(Clone)] +struct SceneLight { + handle: Handle, + /// Present when `color` must follow the current ByLayer color. + color_layer: Option, + light_type: f32, + position: [f64; 3], + direction: [f32; 3], + color: [f32; 3], + intensity: f32, + hotspot_cos: f32, + falloff_cos: f32, + attenuation_type: f32, + attenuation_start: f32, + attenuation_end: f32, +} + pub struct Scene { pub camera: Rc>, /// Model-space tiled viewport layout. One full-window tile by default; @@ -1325,6 +1348,12 @@ pub struct Scene { pub selection: Rc>, /// The CAD document — single source of truth for all entities. pub document: CadDocument, + /// File-open-prepared index for non-graphical semantic object lookups. + pub(crate) object_data_cache: crate::entities::object_data::ObjectDataCache, + /// Native AcDbLight/Sun inputs. Built once after document load, then + /// invalidated only when a light entity changes; ordinary geometry edits + /// never rescan a million-entity drawing just to rediscover its lights. + lighting_cache: RefCell>>, /// Currently selected entity handles. pub selected: HashSet, /// Entity handles hidden by Isolate / Hide. Empty = nothing hidden. @@ -1716,6 +1745,8 @@ impl Scene { model_panes: iced::widget::pane_grid::State::new(0).0, selection: Rc::new(RefCell::new(SelectionState::default())), document: CadDocument::new(), + object_data_cache: crate::entities::object_data::ObjectDataCache::default(), + lighting_cache: RefCell::new(None), selected: HashSet::default(), hidden: HashSet::default(), refedit_keep: None, @@ -2093,6 +2124,16 @@ impl Scene { entities: &[(Handle, Option>, Option>)], undo: bool, ) -> Vec<(Handle, ChangeKind)> { + if entities.iter().any(|(_, before, after)| { + before + .as_deref() + .is_some_and(|entity| matches!(entity, EntityType::Light(_))) + || after + .as_deref() + .is_some_and(|entity| matches!(entity, EntityType::Light(_))) + }) { + *self.lighting_cache.borrow_mut() = None; + } let mut changes: Vec<(Handle, ChangeKind)> = Vec::with_capacity(entities.len()); for (h, before, after) in entities { let target = if undo { before } else { after }; @@ -2127,6 +2168,30 @@ impl Scene { } pub fn bump_entities(&mut self, changes: &[(Handle, ChangeKind)]) { + let cached_light_changed = self.lighting_cache.borrow().as_ref().is_some_and(|lights| { + changes + .iter() + .any(|(handle, _)| lights.iter().any(|light| light.handle == *handle)) + }); + let live_light_changed = changes.iter().any(|(handle, _)| { + self.document + .get_entity(*handle) + .is_some_and(|entity| matches!(entity, EntityType::Light(_))) + }); + if cached_light_changed || live_light_changed { + for (handle, _) in changes { + let exists = self + .document + .get_entity(*handle) + .is_some_and(|entity| matches!(entity, EntityType::Light(_))); + crate::entities::object_data::update_light_entity( + &mut self.object_data_cache, + *handle, + exists, + ); + } + *self.lighting_cache.borrow_mut() = None; + } let epoch = GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed); self.geometry_epoch = epoch; { diff --git a/src/scene/pipeline/uniforms.rs b/src/scene/pipeline/uniforms.rs index e9197ac07..ac6084155 100644 --- a/src/scene/pipeline/uniforms.rs +++ b/src/scene/pipeline/uniforms.rs @@ -40,6 +40,21 @@ pub struct Uniforms { pub _pad_eh: f32, pub eye_low: [f32; 3], pub _pad_el: f32, + + // Up to four native AcDbLight/Sun sources. Positions are uploaded relative + // to the current eye, preserving large-coordinate precision without + // changing mesh buffers. + /// xyz = eye-relative source position, w = light type (1 distant, 2 point, + /// 3 spot). + pub light_position_type: [[f32; 4]; 4], + /// xyz = source-to-target direction, w = intensity. + pub light_direction_intensity: [[f32; 4]; 4], + /// rgb = source colour, w = cosine of hotspot angle. + pub light_color_hotspot: [[f32; 4]; 4], + /// x = attenuation mode, y/z = start/end limits, w = cosine of falloff. + pub light_attenuation: [[f32; 4]; 4], + /// x = active light count, y = fallback ambient strength. + pub lighting: [f32; 4], } impl Uniforms { @@ -64,6 +79,11 @@ impl Uniforms { _pad_eh: 0.0, eye_low, _pad_el: 0.0, + light_position_type: [[0.0; 4]; 4], + light_direction_intensity: [[0.0; 4]; 4], + light_color_hotspot: [[0.0; 4]; 4], + light_attenuation: [[0.0; 4]; 4], + lighting: [0.0, 0.18, 0.0, 0.0], } } } diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index d99736f5a..187b9cc39 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -15,8 +15,8 @@ use crate::scene::pipeline::viewcube::{hover_id, VIEWCUBE_PX}; use crate::scene::pipeline::MultiPipeline; use crate::scene::convert::tess_util; use crate::scene::{ - vp_effective_scale, HatchModel, ImageModel, MeshLodSet, NavPerfSample, Scene, Uniforms, - ViewportInstance, WireModel, + vp_effective_scale, HatchModel, ImageModel, MeshLodSet, NavPerfSample, Scene, SceneLight, + Uniforms, ViewportInstance, WireModel, }; // ── Camera hover state (shader::Program::State) ─────────────────────────── @@ -1077,6 +1077,260 @@ fn crop_view_proj(view_proj: glam::Mat4, uo: f32, vo: f32, us: f32, vs: f32) -> // ── Render-style helpers (impl Scene) ──────────────────────────────────── impl Scene { + fn build_lighting_cache(&self) -> Vec { + use acadrust::objects::{ClassObjectData, ObjectType}; + + fn normalized(value: [f64; 3], fallback: [f32; 3]) -> [f32; 3] { + let length = + (value[0] * value[0] + value[1] * value[1] + value[2] * value[2]).sqrt(); + if length <= 1e-12 { + fallback + } else { + [ + (value[0] / length) as f32, + (value[1] / length) as f32, + (value[2] / length) as f32, + ] + } + } + + fn solar_direction( + sun: &acadrust::objects::Sun, + geo: &acadrust::objects::GeoData, + ) -> Option<[f32; 3]> { + if sun.julian_day < 1_000_000 { + return None; + } + let daylight_ms = if sun.is_daylight_savings_on { + 3_600_000.0 + } else { + 0.0 + }; + let jd = sun.julian_day as f64 + + (sun.milliseconds as f64 - daylight_ms) / 86_400_000.0; + let days = jd - 2_451_545.0; + let mean_longitude = (280.460 + 0.985_647_4 * days).to_radians(); + let mean_anomaly = (357.528 + 0.985_600_3 * days).to_radians(); + let ecliptic_longitude = mean_longitude + + (1.915 * mean_anomaly.sin() + + 0.020 * (2.0 * mean_anomaly).sin()) + .to_radians(); + let obliquity = (23.439 - 0.000_000_4 * days).to_radians(); + let right_ascension = (obliquity.cos() * ecliptic_longitude.sin()) + .atan2(ecliptic_longitude.cos()); + let declination = + (obliquity.sin() * ecliptic_longitude.sin()).asin(); + let local_sidereal = (280.460_618_37 + + 360.985_647_366_29 * days + + geo.reference_point.x) + .to_radians(); + let hour_angle = (local_sidereal - right_ascension + std::f64::consts::PI) + .rem_euclid(std::f64::consts::TAU) + - std::f64::consts::PI; + let latitude = geo.reference_point.y.to_radians(); + let east_component = -declination.cos() * hour_angle.sin(); + let north_component = declination.sin() * latitude.cos() + - declination.cos() * hour_angle.cos() * latitude.sin(); + let up_component = declination.sin() * latitude.sin() + + declination.cos() * hour_angle.cos() * latitude.cos(); + if up_component <= 0.0 { + return None; + } + let north = normalized( + [geo.north_direction.x, geo.north_direction.y, 0.0], + [0.0, 1.0, 0.0], + ); + let east = [north[1], -north[0], 0.0]; + let up = normalized( + [geo.up_direction.x, geo.up_direction.y, geo.up_direction.z], + [0.0, 0.0, 1.0], + ); + Some(normalized( + [ + -(east[0] as f64 * east_component + + north[0] as f64 * north_component + + up[0] as f64 * up_component), + -(east[1] as f64 * east_component + + north[1] as f64 * north_component + + up[1] as f64 * up_component), + -(east[2] as f64 * east_component + + north[2] as f64 * north_component + + up[2] as f64 * up_component), + ], + [0.0, 0.0, -1.0], + )) + } + + fn converted(scene: &Scene, light: &acadrust::entities::Light) -> Option { + if !light.status { + return None; + } + let direction = normalized( + [ + light.target.x - light.position.x, + light.target.y - light.position.y, + light.target.z - light.position.z, + ], + [0.0, 0.0, -1.0], + ); + let color_layer = if light.light_color.rgb().is_some() { + None + } else { + Some(light.common.layer.clone()) + }; + let rgba = if color_layer.is_none() { + tess_util::aci_to_rgba(&light.light_color) + } else { + scene.layer_color(&light.common.layer) + }; + Some(SceneLight { + handle: light.common.handle, + color_layer, + light_type: light.light_type as f32, + position: [light.position.x, light.position.y, light.position.z], + direction, + color: [rgba[0], rgba[1], rgba[2]], + intensity: light.intensity.max(0.0) as f32, + hotspot_cos: if light.hotspot_angle > 0.0 { + (light.hotspot_angle * 0.5).cos() as f32 + } else { + 1.0 + }, + falloff_cos: if light.falloff_angle > 0.0 { + (light.falloff_angle * 0.5).cos() as f32 + } else { + -1.0 + }, + attenuation_type: light.attenuation_type as f32, + attenuation_start: if light.use_attenuation_limits { + light.attenuation_start_limit as f32 + } else { + 0.0 + }, + attenuation_end: if light.use_attenuation_limits { + light.attenuation_end_limit as f32 + } else { + 0.0 + }, + }) + } + + let mut lights = Vec::new(); + for &handle in crate::entities::object_data::light_entities( + &self.object_data_cache, + ) { + if lights.len() >= 4 { + break; + } + if let Some(EntityType::Light(light)) = self.document.get_entity(handle) { + if let Some(light) = converted(self, light) { + lights.push(light); + } + } + } + + if lights.len() < 4 { + let geo = crate::entities::object_data::geo_objects( + &self.object_data_cache, + ) + .iter() + .find_map(|handle| match self.document.objects.get(handle) { + Some(ObjectType::GeoData(value)) + if value.coordinate_type == 3 + && value.reference_point.x.is_finite() + && value.reference_point.y.is_finite() + && value.reference_point.x.abs() <= 180.0 + && value.reference_point.y.abs() <= 90.0 => Some(value), + _ => None, + }); + for handle in crate::entities::object_data::sun_objects( + &self.object_data_cache, + ) { + let Some(ObjectType::ClassObject(value)) = + self.document.objects.get(handle) + else { + continue; + }; + let ClassObjectData::Sun(sun) = &value.data else { + continue; + }; + if !sun.is_on { + continue; + } + let Some(geo) = geo else { + break; + }; + let Some(direction) = solar_direction(sun, geo) else { + break; + }; + let rgba = tess_util::aci_to_rgba(&sun.color); + lights.push(SceneLight { + handle: value.handle, + color_layer: None, + light_type: 1.0, + position: [0.0; 3], + direction, + color: [rgba[0], rgba[1], rgba[2]], + intensity: sun.intensity.max(0.0) as f32, + hotspot_cos: 1.0, + falloff_cos: -1.0, + attenuation_type: 0.0, + attenuation_start: 0.0, + attenuation_end: 0.0, + }); + break; + } + } + lights + } + + fn apply_document_lighting(&self, uniforms: &mut Uniforms) { + if self.lighting_cache.borrow().is_none() { + let lights = self.build_lighting_cache(); + *self.lighting_cache.borrow_mut() = Some(lights); + } + let cache = self.lighting_cache.borrow(); + let lights = cache.as_deref().unwrap_or_default(); + let eye = [ + uniforms.eye_high[0] as f64 + uniforms.eye_low[0] as f64, + uniforms.eye_high[1] as f64 + uniforms.eye_low[1] as f64, + uniforms.eye_high[2] as f64 + uniforms.eye_low[2] as f64, + ]; + uniforms.lighting[0] = lights.len().min(4) as f32; + for (index, light) in lights.iter().take(4).enumerate() { + let color = light + .color_layer + .as_deref() + .map(|layer| self.layer_color(layer)) + .map(|rgba| [rgba[0], rgba[1], rgba[2]]) + .unwrap_or(light.color); + uniforms.light_position_type[index] = [ + (light.position[0] - eye[0]) as f32, + (light.position[1] - eye[1]) as f32, + (light.position[2] - eye[2]) as f32, + light.light_type, + ]; + uniforms.light_direction_intensity[index] = [ + light.direction[0], + light.direction[1], + light.direction[2], + light.intensity, + ]; + uniforms.light_color_hotspot[index] = [ + color[0], + color[1], + color[2], + light.hotspot_cos, + ]; + uniforms.light_attenuation[index] = [ + light.attenuation_type, + light.attenuation_start, + light.attenuation_end, + light.falloff_cos, + ]; + } + } + /// Returns (entity_color, pattern_length, pattern, line_weight_px, aci). pub(in crate::scene) fn render_style(&self, e: &EntityType) -> ([f32; 4], f32, [f32; 8], f32, u8) { let (color, pl, pat, lw, aci) = render_style_for(&self.document, e); @@ -1695,6 +1949,7 @@ impl Scene { uniforms.viewport_size = [visible_w, visible_h]; uniforms.flat_shade = if flags.flat_shade { 1.0 } else { 0.0 }; uniforms.transparency_enable = if self.transparency_display { 1.0 } else { 0.0 }; + self.apply_document_lighting(&mut uniforms); // `screen_rect` carries the *visible* sub-rectangle in normalized // canvas coords — that's what `Pipeline::prepare` uses to size diff --git a/src/shaders/mesh.wgsl b/src/shaders/mesh.wgsl index 37046512f..239c6b854 100644 --- a/src/shaders/mesh.wgsl +++ b/src/shaders/mesh.wgsl @@ -22,6 +22,11 @@ struct Uniforms { _pad_eh: f32, eye_low: vec3, _pad_el: f32, + light_position_type: array, 4>, + light_direction_intensity: array, 4>, + light_color_hotspot: array, 4>, + light_attenuation: array, 4>, + lighting: vec4, }; @group(0) @binding(0) @@ -161,18 +166,68 @@ fn fs_main(in: VertexOut) -> @location(0) vec4 { n = normalize(n - slope * maps.blends1.x * max(in.advanced.y, 0.0) * 4.0); } - // Three-point-ish lighting (world space) plus ambient. Spread directions - // keep every face — and the back faces seen through an open surface — lit - // from at least one source, so the model never reads as a flat dark mass. - // `abs(dot)` makes each light two-sided (independent of normal direction). + // Native AcDbLight / Sun lighting. When a drawing has no active lights, + // retain the neutral three-point editor rig so ordinary models remain + // readable. let l0 = normalize(vec3( 0.5, 0.8, 0.6)); // key (upper front) let l1 = normalize(vec3(-0.7, 0.3, 0.4)); // fill (left) let l2 = normalize(vec3( 0.2, -0.6, -0.8)); // back/under - let diff = 0.45 * abs(dot(n, l0)) - + 0.30 * abs(dot(n, l1)) - + 0.25 * abs(dot(n, l2)); + var direct_light = vec3( + 0.45 * abs(dot(n, l0)) + + 0.30 * abs(dot(n, l1)) + + 0.25 * abs(dot(n, l2)) + ); + var key_light = l0; + if (u.lighting.x > 0.5) { + direct_light = vec3(0.0); + let count = min(u32(u.lighting.x), 4u); + for (var index = 0u; index < count; index = index + 1u) { + let position_type = u.light_position_type[index]; + let direction_intensity = u.light_direction_intensity[index]; + let color_hotspot = u.light_color_hotspot[index]; + let attenuation_data = u.light_attenuation[index]; + var light_vector = -normalize(direction_intensity.xyz); + var attenuation = 1.0; + if (position_type.w > 1.5) { + let delta = position_type.xyz - in.world_pos; + let distance = max(length(delta), 1e-5); + light_vector = delta / distance; + if (attenuation_data.x > 1.5) { + attenuation /= max(distance * distance, 1.0); + } else if (attenuation_data.x > 0.5) { + attenuation /= max(distance, 1.0); + } + if (attenuation_data.z > attenuation_data.y) { + attenuation *= 1.0 - smoothstep( + attenuation_data.y, + attenuation_data.z, + distance, + ); + } + if (position_type.w > 2.5) { + let source_to_fragment = -light_vector; + let cone = dot( + source_to_fragment, + normalize(direction_intensity.xyz), + ); + attenuation *= smoothstep( + attenuation_data.w, + color_hotspot.w, + cone, + ); + } + } + let strength = max(direction_intensity.w, 0.0) * attenuation; + direct_light += color_hotspot.rgb + * max(dot(n, light_vector), 0.0) + * strength; + if (index == 0u) { + key_light = light_vector; + } + } + } let view = normalize(in.eye_vec); - let half_vec = normalize(l0 + view); + let half_vec = normalize(key_light + view); let gloss_exp = mix(2.0, 128.0, clamp(in.material.x, 0.0, 1.0)); let fresnel0 = pow( (max(in.specular.w, 1.0) - 1.0) / (max(in.specular.w, 1.0) + 1.0), @@ -186,8 +241,13 @@ fn fs_main(in: VertexOut) -> @location(0) vec4 { let texel = textureSample(specular_map, specular_sampler, in.uv_specular).rgb; specular_color = mix(specular_color, texel, clamp(maps.blends0.y, 0.0, 1.0)); } + let half_response = select( + abs(dot(n, half_vec)), + max(dot(n, half_vec), 0.0), + u.lighting.x > 0.5, + ); let specular = specular_color - * pow(max(abs(dot(n, half_vec)), 0.0), gloss_exp) + * pow(half_response, gloss_exp) * specular_strength * max(in.advanced.z, 0.0); var albedo = in.color.rgb; @@ -196,7 +256,7 @@ fn fs_main(in: VertexOut) -> @location(0) vec4 { albedo = mix(albedo, texel, clamp(maps.blends0.x, 0.0, 1.0)); } let ambient_light = albedo * clamp(in.ambient.rgb, vec3(0.0), vec3(1.0)); - var lit = ambient_light + albedo * clamp(diff, 0.0, 1.0) + specular; + var lit = ambient_light + albedo * clamp(direct_light, vec3(0.0), vec3(2.0)) + specular; if (maps.present0.z != 0u) { let reflected = textureSample( reflection_map,