diff --git a/Cargo.toml b/Cargo.toml index 53edbe2c5..4f0bf3a83 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 the verified DWG round-trip, I/O, and unified PERF fixes. -acadrust = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "de8d6b5" } +# Track standard DWG entity data used by the scene integration. +acadrust = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "7aaea11" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # Native enables the plugin host runtime (out-of-process plugins). diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index fbd9f1d02..e6228fa7b 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -1896,6 +1896,8 @@ impl OpenCADStudio { verts_low, normals, indices, + triangle_material_handles: Vec::new(), + triangle_colors: Vec::new(), color, selected: false, }, @@ -1986,6 +1988,8 @@ impl OpenCADStudio { verts_low, normals, indices, + triangle_material_handles: Vec::new(), + triangle_colors: Vec::new(), color, selected: false, }), @@ -2078,6 +2082,8 @@ impl OpenCADStudio { verts_low, normals, indices, + triangle_material_handles: Vec::new(), + triangle_colors: Vec::new(), color, selected: false, }), @@ -2097,6 +2103,8 @@ impl OpenCADStudio { verts_low, normals, indices, + triangle_material_handles: Vec::new(), + triangle_colors: Vec::new(), color, selected: false, }), @@ -2131,6 +2139,8 @@ impl OpenCADStudio { verts_low, normals, indices, + triangle_material_handles: Vec::new(), + triangle_colors: Vec::new(), color, selected: false, }), @@ -2150,6 +2160,8 @@ impl OpenCADStudio { verts_low, normals, indices, + triangle_material_handles: Vec::new(), + triangle_colors: Vec::new(), color, selected: false, }), @@ -2247,6 +2259,8 @@ impl OpenCADStudio { verts_low, normals, indices, + triangle_material_handles: Vec::new(), + triangle_colors: Vec::new(), color, selected: false, }), diff --git a/src/app/model_ops.rs b/src/app/model_ops.rs index ee3eed0c2..0d464da2b 100644 --- a/src/app/model_ops.rs +++ b/src/app/model_ops.rs @@ -671,6 +671,8 @@ impl super::OpenCADStudio { verts_low, normals, indices, + triangle_material_handles: Vec::new(), + triangle_colors: Vec::new(), color, selected: false, }; diff --git a/src/app/properties.rs b/src/app/properties.rs index 1f7fe3ec2..a5d375e5b 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -10,6 +10,50 @@ use acadrust::{EntityType, Handle}; /// shows a count-only summary instead. Bulk edits still go through the ribbon. const MAX_PROP_AGGREGATE: usize = 2_000; +fn material_color_text(color: &acadrust::objects::MaterialColor) -> String { + let rgb = color + .rgb + .map(|value| format!("#{:06X}", value as u32 & 0x00ff_ffff)) + .unwrap_or_else(|| "Object".to_string()); + format!("{rgb}, factor {:.3}", color.factor) +} + +fn material_map_text(map: &acadrust::objects::MaterialMap) -> String { + let source = match map.source { + 1 => { + if map.file_name.trim().is_empty() { + "File".to_string() + } else { + map.file_name.clone() + } + } + 2 => format!( + "Procedural {}", + map.texture.as_ref().map_or(0, |texture| texture.mode) + ), + _ => "None".to_string(), + }; + format!( + "{source}; blend {:.3}; projection {}; tiling {}; auto {}", + map.blend_factor, map.projection, map.tiling, map.auto_transform + ) +} + +fn visual_style_properties_text(style: &acadrust::objects::VisualStyle) -> String { + style + .properties + .iter() + .enumerate() + .map(|(index, property)| { + format!( + "{}: {:?} (enabled {})", + index, property.value, property.enabled + ) + }) + .collect::>() + .join("\n") +} + impl OpenCADStudio { /// Rebuild the PropertiesPanel from the current entity selection. /// Preserves UI state (open pickers, edit buffer) across refreshes. @@ -93,7 +137,12 @@ impl OpenCADStudio { let mut panel = match selected.len() { 0 => ui::PropertiesPanel::empty(), 1 => { - let (handle, entity) = selected[0]; + let (handle, source_entity) = selected[0]; + let contextual = crate::scene::annotative::entity_for_active_context( + &self.tabs[i].scene.document, + source_entity, + ); + let entity = contextual.as_ref(); let group_names = self.tabs[i].scene.group_names_for_entity(handle); let mut sections = dispatch::properties_sectioned(handle, entity, &text_style_names); @@ -140,6 +189,329 @@ impl OpenCADStudio { }; } } + let effective_handle = match common.material_flags { + 3 => common.material_handle, + 0 => doc + .layers + .get(&common.layer) + .map(|layer| layer.material) + .filter(|handle| handle.is_valid()), + _ => None, + }; + if let Some(acadrust::objects::ObjectType::Material(material)) = + effective_handle.and_then(|handle| doc.objects.get(&handle)) + { + use crate::entities::common::ro_prop; + sections.push(crate::scene::model::object::PropSection { + title: "Material Details".to_string(), + props: vec![ + ro_prop("Name", "mat_name", material.name.clone()), + ro_prop( + "Description", + "mat_description", + material.description.clone(), + ), + ro_prop( + "Ambient", + "mat_ambient", + material_color_text(&material.ambient_color), + ), + ro_prop( + "Diffuse", + "mat_diffuse", + material_color_text(&material.diffuse_color), + ), + ro_prop( + "Specular", + "mat_specular", + material_color_text(&material.specular_color), + ), + ro_prop( + "Gloss", + "mat_gloss", + format!("{:.3}", material.specular_gloss_factor), + ), + ro_prop( + "Opacity", + "mat_opacity", + format!("{:.3}", material.opacity_percent), + ), + ro_prop( + "Reflectivity", + "mat_reflectivity", + format!("{:.3}", material.reflectivity), + ), + ro_prop( + "Translucence", + "mat_translucence", + format!("{:.3}", material.translucence), + ), + ro_prop( + "Refraction", + "mat_refraction", + format!("{:.3}", material.refraction_index), + ), + ro_prop( + "Self Illumination", + "mat_self_illumination", + format!("{:.3}", material.self_illumination), + ), + ro_prop( + "Luminance", + "mat_luminance", + format!( + "{:.3} (mode {})", + material.luminance, material.luminance_mode + ), + ), + ro_prop( + "Diffuse Map", + "mat_diffuse_map", + material_map_text(&material.diffuse_map), + ), + ro_prop( + "Specular Map", + "mat_specular_map", + material_map_text(&material.specular_map), + ), + ro_prop( + "Reflection Map", + "mat_reflection_map", + material_map_text(&material.reflection_map), + ), + ro_prop( + "Opacity Map", + "mat_opacity_map", + material_map_text(&material.opacity_map), + ), + ro_prop( + "Bump Map", + "mat_bump_map", + material_map_text(&material.bump_map), + ), + ro_prop( + "Refraction Map", + "mat_refraction_map", + material_map_text(&material.refraction_map), + ), + ro_prop( + "Normal Map", + "mat_normal_map", + material_map_text(&material.normal_map), + ), + ro_prop( + "Render Flags", + "mat_render_flags", + format!( + "illumination {}; channels {}; mode {}; two-sided {}", + material.illumination_model, + material.channel_flags, + material.mode, + material.two_sided_material + ), + ), + ro_prop( + "Advanced", + "mat_advanced", + format!( + "normal {:.3}; bump {:.3}; reflect {:.3}; transmit {:.3}", + material.normal_map_strength, + material.indirect_bump_scale, + material.reflectance_scale, + material.transmittance_scale + ), + ), + ], + }); + } + } + + { + let doc = &self.tabs[i].scene.document; + let common = entity.common(); + if let Some(acadrust::objects::ObjectType::BookColor(book)) = common + .color_book_handle + .filter(|handle| handle.is_valid()) + .and_then(|handle| doc.objects.get(&handle)) + { + for section in sections.iter_mut() { + if let Some(row) = + section.props.iter_mut().find(|row| row.field == "color") + { + row.value = + crate::scene::model::object::PropValue::ColorChoice( + book.color, + ); + } + } + use crate::entities::common::ro_prop; + sections.push(crate::scene::model::object::PropSection { + title: "Color Book".to_string(), + props: vec![ + ro_prop("Book", "book_color_book", book.book_name.clone()), + ro_prop( + "Color Name", + "book_color_name", + book.color_name.clone(), + ), + ro_prop( + "Color", + "book_color_value", + format!("{:?}", book.color), + ), + ], + }); + } + + let style_handles = [ + ("Full", common.full_visual_style_handle), + ("Face", common.face_visual_style_handle), + ("Edge", common.edge_visual_style_handle), + ]; + for (scope, handle) in style_handles { + let Some((handle, style)) = handle + .filter(|handle| handle.is_valid()) + .and_then(|handle| { + doc.objects.get(&handle).and_then(|object| match object { + acadrust::objects::ObjectType::VisualStyle(style) => { + Some((handle, style)) + } + _ => None, + }) + }) + else { + continue; + }; + use crate::entities::common::ro_prop; + sections.push(crate::scene::model::object::PropSection { + title: format!("{scope} Visual Style"), + props: vec![ + ro_prop( + "Handle", + "vs_handle", + format!("{:X}", handle.value()), + ), + ro_prop( + "Description", + "vs_description", + style.description.clone(), + ), + ro_prop( + "Type", + "vs_type", + style.style_type.to_string(), + ), + ro_prop( + "Face", + "vs_face", + format!( + "lighting {}; quality {}; color {}; modifiers {}", + style.face_lighting_model, + style.face_lighting_quality, + style.face_color_mode, + style.face_modifier + ), + ), + ro_prop( + "Edges", + "vs_edges", + format!( + "model {}; style {}", + style.edge_model, style.edge_style + ), + ), + ro_prop( + "Extended Lighting", + "vs_extended_lighting", + style.extended_lighting_model.to_string(), + ), + ro_prop( + "Internal", + "vs_internal", + style.internal_use_only.to_string(), + ), + ro_prop( + "Property Bag", + "vs_properties", + visual_style_properties_text(style), + ), + ], + }); + } + } + + if matches!( + entity, + acadrust::EntityType::Solid3D(_) + | acadrust::EntityType::Region(_) + | acadrust::EntityType::Body(_) + | acadrust::EntityType::Surface(_) + | acadrust::EntityType::Mesh(_) + | acadrust::EntityType::PolygonMesh(_) + | acadrust::EntityType::PolyfaceMesh(_) + ) { + if let Some(mesh) = self.tabs[i] + .scene + .meshes + .get(&handle) + .or_else(|| self.tabs[i].scene.block_meshes.get(&handle)) + { + use crate::entities::common::ro_prop; + let metrics = mesh.metrics; + let mut props = vec![ + ro_prop( + "Vertices", + "mesh_vertices", + metrics.vertices.to_string(), + ), + ro_prop( + "Triangles", + "mesh_triangles", + metrics.triangles.to_string(), + ), + ro_prop( + "Surface Area", + "mesh_surface_area", + format!("{:.6}", metrics.surface_area), + ), + ro_prop( + "Centroid", + "mesh_centroid", + format!( + "{:.6}, {:.6}, {:.6}", + metrics.centroid[0], + metrics.centroid[1], + metrics.centroid[2] + ), + ), + ro_prop( + "Tessellation", + "mesh_complete", + if mesh.complete { "Complete" } else { "Partial" }, + ), + ]; + if mesh.complete + && matches!( + entity, + acadrust::EntityType::Solid3D(_) + | acadrust::EntityType::Region(_) + | acadrust::EntityType::Body(_) + ) + { + props.insert( + 3, + ro_prop( + "Volume", + "mesh_volume", + format!("{:.6}", metrics.volume), + ), + ); + } + sections.push(crate::scene::model::object::PropSection { + title: "Mass Properties".to_string(), + props, + }); + } } // Uniform-scale checkbox for block references (#427): @@ -1090,7 +1462,11 @@ impl OpenCADStudio { let selected = self.tabs[i].scene.selected_entities(); if selected.len() == 1 { let (handle, entity) = selected[0]; - let grips = dispatch::grips(entity) + let contextual = crate::scene::annotative::entity_for_active_context( + &self.tabs[i].scene.document, + entity, + ); + let grips = dispatch::grips(contextual.as_ref()) .into_iter() .map(|mut g| { // Subtract in f64: at UTM magnitudes an f32 cast before @@ -1124,12 +1500,21 @@ impl OpenCADStudio { } pub(super) fn invalidate_property_targets(&mut self, i: usize, handles: &[Handle]) { + let mut context_object_changed = false; for &handle in handles { + context_object_changed |= + crate::scene::annotative::sync_active_context_from_entity( + &mut self.tabs[i].scene.document, + handle, + ); // Hatch / SOLID fills render from prebuilt cached models; rebuild // them or pattern edits (scale, background, …) stay invisible // (#415). self.tabs[i].scene.refresh_fill_model(handle); } + if context_object_changed { + self.tabs[i].scene.poison_undo_recording(); + } // Solid (ACIS) meshes bake their colour into the mesh, so a colour / // layer change needs an explicit recolour — re-tessellating wires // alone wouldn't update them. diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 72a33af2e..8d9c326ce 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -491,6 +491,8 @@ pub(super) fn on_open_file(&mut self) -> Task { }; self.tabs[i].current_path = Some(path.clone()); + self.tabs[i].scene.material_base_dir = + path.parent().map(std::path::Path::to_path_buf); self.tabs[i].scene.document = doc; // A file saved without the built-in Standard styles (foreign // or damaged) gets them re-seeded so nothing dangles (#366). diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index a04d6059a..dd94a0b77 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -1967,6 +1967,7 @@ impl OpenCADStudio { self.scale_popup_open = false; if let Some(tab) = self.tabs.get_mut(self.active_tab) { tab.scene.annotation_scale = scale; + util::sync_annotation_scale_header(&mut tab.scene); tab.scene.bump_geometry(); } Task::none() diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index 50d758323..2bb42aaca 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -1224,6 +1224,102 @@ use crate::scene::convert::tessellate::{ }; use crate::scene::model::wire_model::{SnapHint, WireModel}; +fn apply_dimension_breaks( + document: &CadDocument, + dimension: Handle, + lines: &mut Vec<[f32; 3]>, +) { + let references: Vec<_> = document + .objects + .values() + .filter_map(|object| { + let acadrust::objects::ObjectType::DataObject(object) = object else { + return None; + }; + let acadrust::objects::DataObjectData::BreakData(data) = &object.data + else { + return None; + }; + (data.dimension_reference == dimension).then_some(&data.point_references) + }) + .flatten() + .filter(|reference| { + let points = [reference.first_point, reference.second_point]; + points.iter().all(|point| { + point.x.is_finite() && point.y.is_finite() && point.z.is_finite() + }) + }) + .collect(); + if references.is_empty() || lines.len() < 2 { + return; + } + + let mut output = Vec::with_capacity(lines.len() + references.len() * 3); + for run in lines.split(|point| point[0].is_nan()) { + for segment in run.windows(2) { + let a = Vec3::from_array(segment[0]); + let b = Vec3::from_array(segment[1]); + let direction = b - a; + let length_squared = direction.length_squared(); + if length_squared <= 1e-12 { + continue; + } + let mut intervals = vec![(0.0f32, 1.0f32)]; + for reference in &references { + let first = vec3_local(reference.first_point); + let second = vec3_local(reference.second_point); + let t1 = (first - a).dot(direction) / length_squared; + let t2 = (second - a).dot(direction) / length_squared; + let closest1 = a + direction * t1.clamp(0.0, 1.0); + let closest2 = a + direction * t2.clamp(0.0, 1.0); + let requested_gap = (second - first).length(); + let tolerance = (requested_gap * 0.25) + .max(direction.length() * 1e-5) + .max(1e-4); + if (first - closest1).length() > tolerance + || (second - closest2).length() > tolerance + { + continue; + } + let half_point_gap = if requested_gap <= 1e-6 { + (tolerance / direction.length()).min(0.1) + } else { + 0.0 + }; + let cut_start = (t1.min(t2) - half_point_gap).clamp(0.0, 1.0); + let cut_end = (t1.max(t2) + half_point_gap).clamp(0.0, 1.0); + if cut_end <= cut_start { + continue; + } + let mut remaining = Vec::new(); + for (start, end) in intervals { + if cut_end <= start || cut_start >= end { + remaining.push((start, end)); + continue; + } + if cut_start > start { + remaining.push((start, cut_start)); + } + if cut_end < end { + remaining.push((cut_end, end)); + } + } + intervals = remaining; + } + for (start, end) in intervals { + if end - start > 1e-6 { + add_segment( + &mut output, + a + direction * start, + a + direction * end, + ); + } + } + } + } + *lines = output; +} + pub trait DimensionTess { fn tessellate( &self, @@ -1464,6 +1560,7 @@ fn tessellate_dimension_inner( // supersedes it. Read but not honoured. let _ = s.dimunit; } + apply_dimension_breaks(document, handle, &mut geom.dim_lines); // Dimension entity fields that the render path doesn't yet use but are // preserved on save: // - base.insertion_point: legacy anchor reference; render uses diff --git a/src/entities/extended.rs b/src/entities/extended.rs new file mode 100644 index 000000000..dee7dfa4c --- /dev/null +++ b/src/entities/extended.rs @@ -0,0 +1,1609 @@ +use acadrust::entities::{ + ArcAlignedTextData, ExtendedEntity, ExtendedEntityData, GeoPositionMarkerData, + PointCloudData, PointCloudExData, RemoteTextData, SectionObjectData, +}; +use acadrust::types::{Handle, Transform, Vector3}; + +use crate::command::EntityTransform; +use crate::entities::common::{ + center_grip, edit_angle_prop, edit_prop, parse_f64, 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, PropValue, Property, +}; +use crate::scene::model::wire_model::SnapHint; + +const NAN: [f64; 3] = [f64::NAN; 3]; + +fn vector_text(value: Vector3) -> String { + format!("{:.6}, {:.6}, {:.6}", value.x, value.y, value.z) +} + +fn handle_text(handle: Handle) -> String { + if handle.is_null() { + "None".to_string() + } else { + format!("{:X}", handle.value()) + } +} + +fn text_prop(label: &'static str, field: &'static str, value: &str) -> Property { + Property { + label: label.into(), + field, + value: PropValue::EditText(value.to_string()), + } +} + +fn bool_prop(label: &'static str, field: &'static str, value: bool) -> Property { + Property { + label: label.into(), + field, + value: PropValue::BoolToggle { field, value }, + } +} + +fn push_segment(points: &mut Vec<[f64; 3]>, first: [f64; 3], second: [f64; 3]) { + if !points.is_empty() { + points.push(NAN); + } + points.push(first); + points.push(second); +} + +fn push_chain(points: &mut Vec<[f64; 3]>, chain: impl IntoIterator) { + let chain: Vec<_> = chain.into_iter().collect(); + if chain.len() < 2 { + return; + } + if !points.is_empty() { + points.push(NAN); + } + points.extend(chain); +} + +fn push_box(points: &mut Vec<[f64; 3]>, min: Vector3, max: Vector3) { + let corners = [ + [min.x, min.y, min.z], + [max.x, min.y, min.z], + [max.x, max.y, min.z], + [min.x, max.y, min.z], + [min.x, min.y, max.z], + [max.x, min.y, max.z], + [max.x, max.y, max.z], + [min.x, max.y, max.z], + ]; + for (a, b) in [ + (0, 1), + (1, 2), + (2, 3), + (3, 0), + (4, 5), + (5, 6), + (6, 7), + (7, 4), + (0, 4), + (1, 5), + (2, 6), + (3, 7), + ] { + push_segment(points, corners[a], corners[b]); + } +} + +fn normalized(value: Vector3, fallback: Vector3) -> Vector3 { + let length = value.length(); + if length > 1e-12 { + value / length + } else { + fallback + } +} + +fn plane_axes(normal: Vector3) -> (Vector3, Vector3, Vector3) { + let normal = normalized(normal, Vector3::UNIT_Z); + let (x, y) = + crate::scene::view::transform::ocs_axes((normal.x, normal.y, normal.z)); + ( + Vector3::new(x.0, x.1, x.2), + Vector3::new(y.0, y.1, y.2), + normal, + ) +} + +fn add_scaled(origin: Vector3, x: Vector3, sx: f64, y: Vector3, sy: f64) -> [f64; 3] { + [ + origin.x + x.x * sx + y.x * sy, + origin.y + x.y * sx + y.y * sy, + origin.z + x.z * sx + y.z * sy, + ] +} + +fn append_planar_text( + points: &mut Vec<[f64; 3]>, + text: &str, + font: &str, + origin: Vector3, + normal: Vector3, + rotation: f64, + height: f64, + width_factor: f64, +) { + if text.is_empty() || height.abs() < 1e-12 { + return; + } + let (axis_x, axis_y, _) = plane_axes(normal); + let (cos, sin) = rotation.sin_cos(); + let baseline = axis_x * cos + axis_y * sin; + let upward = axis_y * cos - axis_x * sin; + let (strokes, _) = crate::scene::text::lff::tessellate_text_ex( + [0.0, 0.0], + height.abs() as f32, + 0.0, + width_factor as f32, + 0.0, + if font.trim().is_empty() { "standard" } else { font }, + text, + ); + for stroke in strokes { + push_chain( + points, + stroke + .into_iter() + .map(|[x, y]| add_scaled(origin, baseline, x as f64, upward, y as f64)), + ); + } +} + +fn append_arc_aligned_text(points: &mut Vec<[f64; 3]>, data: &ArcAlignedTextData) { + if data.text.is_empty() || data.radius.abs() < 1e-12 || data.text_size.abs() < 1e-12 { + return; + } + let (axis_x, axis_y, _) = plane_axes(data.normal); + let direction = if data.reverse || data.text_direction < 0 { + -1.0 + } else { + 1.0 + }; + let mut angle = if direction > 0.0 { + data.start_angle + } else { + data.end_angle + }; + let advance = data.text_size.abs() + * data.x_scale.abs().max(0.01) + * data.character_spacing.abs().max(0.01) + * 0.72; + let delta = direction * advance / data.radius.abs().max(1e-9); + let radius = data.radius + data.offset_from_arc; + let font = if data.font_name.trim().is_empty() { + data.style_name.as_str() + } else { + data.font_name.as_str() + }; + for character in data.text.chars() { + let radial = axis_x * angle.cos() + axis_y * angle.sin(); + let tangent = (axis_y * angle.cos() - axis_x * angle.sin()) * direction; + let origin = data.center + radial * radius; + let (strokes, _) = crate::scene::text::lff::tessellate_text_ex( + [0.0, 0.0], + data.text_size.abs() as f32, + 0.0, + data.x_scale.abs().max(0.01) as f32, + 0.0, + if font.trim().is_empty() { "standard" } else { font }, + &character.to_string(), + ); + for stroke in strokes { + push_chain( + points, + stroke.into_iter().map(|[x, y]| { + add_scaled(origin, tangent, x as f64, radial, y as f64) + }), + ); + } + angle += delta; + } +} + +fn section_lines(data: &SectionObjectData) -> Vec<[f64; 3]> { + let mut points = Vec::new(); + push_chain( + &mut points, + data.vertices.iter().map(|p| [p.x, p.y, p.z]), + ); + push_chain( + &mut points, + data.back_line_vertices.iter().map(|p| [p.x, p.y, p.z]), + ); + points +} + +fn remote_text_lines(data: &RemoteTextData) -> Vec<[f64; 3]> { + let mut points = Vec::new(); + append_planar_text( + &mut points, + &data.text, + &data.style_name, + data.position, + data.normal, + data.rotation, + data.height, + 1.0, + ); + points +} + +fn geo_marker_lines(data: &GeoPositionMarkerData) -> Vec<[f64; 3]> { + let mut points = Vec::new(); + let radius = data.radius.abs().max(1e-6); + let count = 32; + push_chain( + &mut points, + (0..=count).map(|index| { + let angle = std::f64::consts::TAU * index as f64 / count as f64; + [ + data.position.x + radius * angle.cos(), + data.position.y + radius * angle.sin(), + data.position.z, + ] + }), + ); + push_segment( + &mut points, + [data.position.x - radius, data.position.y, data.position.z], + [data.position.x + radius, data.position.y, data.position.z], + ); + push_segment( + &mut points, + [data.position.x, data.position.y - radius, data.position.z], + [data.position.x, data.position.y + radius, data.position.z], + ); + let label = data + .embedded_mtext + .as_ref() + .filter(|_| data.mtext_visible) + .map(|text| text.value.as_str()) + .filter(|value| !value.trim().is_empty()) + .unwrap_or(data.notes.as_str()); + if !label.trim().is_empty() { + let origin = Vector3::new( + data.position.x + radius + data.landing_gap.max(0.0), + data.position.y, + data.position.z, + ); + push_segment( + &mut points, + [ + data.position.x + radius, + data.position.y, + data.position.z, + ], + [origin.x, origin.y, origin.z], + ); + append_planar_text( + &mut points, + label, + "standard", + origin, + Vector3::UNIT_Z, + 0.0, + radius * 0.8, + 1.0, + ); + } + points +} + +fn point_cloud_lines(data: &PointCloudData) -> Vec<[f64; 3]> { + let mut points = Vec::new(); + push_box(&mut points, data.extents_min, data.extents_max); + if data.show_clipping { + for clip in &data.clippings { + if clip.vertices.len() < 2 { + continue; + } + for z in [clip.z_min, clip.z_max] { + let mut chain: Vec<[f64; 3]> = clip + .vertices + .iter() + .map(|point| [point.x, point.y, z]) + .collect(); + chain.push(chain[0]); + push_chain(&mut points, chain); + } + } + } + points +} + +fn point_cloud_ex_lines(data: &PointCloudExData) -> Vec<[f64; 3]> { + let mut points = Vec::new(); + push_box(&mut points, data.extents_min, data.extents_max); + if data.show_cropping { + for crop in &data.croppings { + if crop.points.len() < 2 { + continue; + } + let mut chain: Vec<[f64; 3]> = + crop.points.iter().map(|p| [p.x, p.y, p.z]).collect(); + if crop.points.len() > 2 { + chain.push(chain[0]); + } + push_chain(&mut points, chain); + } + } + points +} + +fn camera_lines(document: &acadrust::CadDocument, view_handle: Handle) -> Vec<[f64; 3]> { + let Some(view) = document.views.iter().find(|view| view.handle == view_handle) else { + return Vec::new(); + }; + let direction = normalized(view.direction, Vector3::UNIT_Z); + let distance = view.direction.length().max(view.height.abs()).max(1.0); + let eye = view.target + direction * distance; + let (right, up, _) = plane_axes(direction); + let half_width = view.width.abs().max(view.height.abs()) * 0.08; + let half_height = view.height.abs().max(view.width.abs()) * 0.06; + let corners = [ + view.target + right * half_width + up * half_height, + view.target - right * half_width + up * half_height, + view.target - right * half_width - up * half_height, + view.target + right * half_width - up * half_height, + ]; + let mut points = Vec::new(); + for corner in corners { + push_segment( + &mut points, + [eye.x, eye.y, eye.z], + [corner.x, corner.y, corner.z], + ); + } + push_chain( + &mut points, + corners + .into_iter() + .chain(std::iter::once(corners[0])) + .map(|p| [p.x, p.y, p.z]), + ); + points +} + +fn to_truck(entity: &ExtendedEntity, document: &acadrust::CadDocument) -> Option { + let (points, snaps, keys): ( + Vec<[f64; 3]>, + Vec<(glam::DVec3, SnapHint)>, + Vec<[f64; 3]>, + ) = match &entity.data { + ExtendedEntityData::Camera { view_handle } => { + let points = camera_lines(document, *view_handle); + (points, Vec::new(), Vec::new()) + } + ExtendedEntityData::SectionObject(data) => { + let snaps = data + .vertices + .iter() + .chain(data.back_line_vertices.iter()) + .map(|point| { + ( + glam::DVec3::new(point.x, point.y, point.z), + SnapHint::Node, + ) + }) + .collect(); + let keys = data + .vertices + .iter() + .chain(data.back_line_vertices.iter()) + .map(|point| [point.x, point.y, point.z]) + .collect(); + (section_lines(data), snaps, keys) + } + ExtendedEntityData::ArcAlignedText(data) => { + let mut points = Vec::new(); + append_arc_aligned_text(&mut points, data); + ( + points, + vec![( + glam::DVec3::new(data.center.x, data.center.y, data.center.z), + SnapHint::Center, + )], + vec![[data.center.x, data.center.y, data.center.z]], + ) + } + ExtendedEntityData::RemoteText(data) => ( + remote_text_lines(data), + vec![( + glam::DVec3::new(data.position.x, data.position.y, data.position.z), + SnapHint::Insertion, + )], + vec![[data.position.x, data.position.y, data.position.z]], + ), + ExtendedEntityData::GeoPositionMarker(data) => ( + geo_marker_lines(data), + vec![( + glam::DVec3::new(data.position.x, data.position.y, data.position.z), + SnapHint::Node, + )], + vec![[data.position.x, data.position.y, data.position.z]], + ), + ExtendedEntityData::PointCloud(data) => ( + point_cloud_lines(data), + vec![( + glam::DVec3::new(data.origin.x, data.origin.y, data.origin.z), + SnapHint::Insertion, + )], + vec![ + [data.extents_min.x, data.extents_min.y, data.extents_min.z], + [data.extents_max.x, data.extents_max.y, data.extents_max.z], + ], + ), + ExtendedEntityData::PointCloudEx(data) => ( + point_cloud_ex_lines(data), + Vec::new(), + vec![ + [data.extents_min.x, data.extents_min.y, data.extents_min.z], + [data.extents_max.x, data.extents_max.y, data.extents_max.z], + ], + ), + _ => return None, + }; + if points.len() < 2 { + return None; + } + Some(TruckEntity { + object: TruckObject::Lines(points), + snap_pts: snaps, + tangent_geoms: Vec::new(), + key_vertices: keys, + fill_tris: Vec::new(), + pick_tris: Vec::new(), + }) +} + +fn section_properties(data: &SectionObjectData) -> Vec { + let vertices = data + .vertices + .iter() + .enumerate() + .map(|(index, point)| format!("{}: {}", index + 1, vector_text(*point))) + .collect::>() + .join("\n"); + let back_vertices = data + .back_line_vertices + .iter() + .enumerate() + .map(|(index, point)| format!("{}: {}", index + 1, vector_text(*point))) + .collect::>() + .join("\n"); + vec![ + PropSection { + title: "Section Object".into(), + props: vec![ + text_prop("Name", "ext_section_name", &data.name), + ro_prop("State", "ext_section_state", data.state.to_string()), + ro_prop("Flags", "ext_section_flags", data.flags.to_string()), + ro_prop( + "Vertical Direction", + "ext_section_vertical", + vector_text(data.vertical_direction), + ), + edit_prop("Top Height", "ext_section_top", data.top_height), + edit_prop("Bottom Height", "ext_section_bottom", data.bottom_height), + ro_prop( + "Indicator Alpha", + "ext_section_alpha", + data.indicator_alpha.to_string(), + ), + ro_prop( + "Indicator Color", + "ext_section_color", + format!("{:?}", data.indicator_color), + ), + ro_prop( + "Settings", + "ext_section_settings", + handle_text(data.settings_handle), + ), + ], + }, + PropSection { + title: "Section Vertices".into(), + props: vec![ + ro_prop("Cutting Line", "ext_section_vertices", vertices), + ro_prop("Back Line", "ext_section_back_vertices", back_vertices), + ], + }, + ] +} + +fn arc_text_properties(data: &ArcAlignedTextData) -> Vec { + vec![PropSection { + title: "Arc-Aligned Text".into(), + props: vec![ + text_prop("Text", "ext_arc_text", &data.text), + text_prop("Font", "ext_arc_font", &data.font_name), + text_prop("Big Font", "ext_arc_big_font", &data.big_font_name), + text_prop("Style", "ext_arc_style", &data.style_name), + ro_prop("Center", "ext_arc_center", vector_text(data.center)), + edit_prop("Radius", "ext_arc_radius", data.radius), + edit_prop("X Scale", "ext_arc_xscale", data.x_scale), + edit_prop("Text Size", "ext_arc_size", data.text_size), + edit_prop( + "Character Spacing", + "ext_arc_spacing", + data.character_spacing, + ), + edit_prop( + "Offset From Arc", + "ext_arc_offset", + data.offset_from_arc, + ), + edit_prop("Right Offset", "ext_arc_right", data.right_offset), + edit_prop("Left Offset", "ext_arc_left", data.left_offset), + edit_angle_prop( + "Start Angle", + "ext_arc_start", + data.start_angle.to_degrees(), + ), + edit_angle_prop("End Angle", "ext_arc_end", data.end_angle.to_degrees()), + bool_prop("Reverse", "ext_arc_reverse", data.reverse), + ro_prop( + "Text Direction", + "ext_arc_direction", + data.text_direction.to_string(), + ), + ro_prop("Alignment", "ext_arc_alignment", data.alignment.to_string()), + ro_prop( + "Text Position", + "ext_arc_position", + data.text_position.to_string(), + ), + bool_prop("Bold", "ext_arc_bold", data.bold), + bool_prop("Italic", "ext_arc_italic", data.italic), + bool_prop("Underlined", "ext_arc_underlined", data.underlined), + ro_prop( + "Character Set", + "ext_arc_charset", + data.character_set.to_string(), + ), + ro_prop( + "Pitch And Family", + "ext_arc_pitch", + data.pitch_and_family.to_string(), + ), + bool_prop("SHX", "ext_arc_shx", data.is_shx), + ro_prop("Text Color", "ext_arc_color", data.text_color.to_string()), + ro_prop("Normal", "ext_arc_normal", vector_text(data.normal)), + bool_prop("Wizard Flag", "ext_arc_wizard", data.wizard_flag), + ro_prop("Arc", "ext_arc_handle", handle_text(data.arc_handle)), + ], + }] +} + +fn remote_text_properties(data: &RemoteTextData) -> Vec { + vec![PropSection { + title: "Remote Text".into(), + props: vec![ + text_prop("Text", "ext_rtext_text", &data.text), + ro_prop("Position", "ext_rtext_position", vector_text(data.position)), + ro_prop("Normal", "ext_rtext_normal", vector_text(data.normal)), + edit_angle_prop( + "Rotation", + "ext_rtext_rotation", + data.rotation.to_degrees(), + ), + edit_prop("Height", "ext_rtext_height", data.height), + text_prop("Style Name", "ext_rtext_style", &data.style_name), + ro_prop( + "Style Handle", + "ext_rtext_style_handle", + handle_text(data.style_handle), + ), + ro_prop("Flags", "ext_rtext_flags", data.flags.to_string()), + ], + }] +} + +fn geo_marker_properties(data: &GeoPositionMarkerData) -> Vec { + vec![PropSection { + title: "Geographic Position Marker".into(), + props: vec![ + ro_prop( + "Class Version", + "ext_geo_version", + data.class_version.to_string(), + ), + ro_prop("Position", "ext_geo_position", vector_text(data.position)), + edit_prop("Radius", "ext_geo_radius", data.radius), + text_prop("Notes", "ext_geo_notes", &data.notes), + edit_prop("Landing Gap", "ext_geo_gap", data.landing_gap), + bool_prop("Text Visible", "ext_geo_visible", data.mtext_visible), + ro_prop( + "Text Alignment", + "ext_geo_alignment", + data.text_alignment.to_string(), + ), + bool_prop("Frame Text", "ext_geo_frame", data.enable_frame_text), + ro_prop( + "Embedded MText", + "ext_geo_mtext", + data.embedded_mtext + .as_ref() + .map(|text| text.value.clone()) + .unwrap_or_default(), + ), + ], + }] +} + +fn point_cloud_properties(data: &PointCloudData) -> Vec { + let clips = data + .clippings + .iter() + .enumerate() + .map(|(index, clip)| { + format!( + "{}: type {}; inverted {}; vertices {}; Z {:.6}..{:.6}", + index + 1, + clip.clip_type, + clip.inverted, + clip.vertices.len(), + clip.z_min, + clip.z_max + ) + }) + .collect::>() + .join("\n"); + vec![ + PropSection { + title: "Point Cloud".into(), + props: vec![ + ro_prop( + "Class Version", + "ext_pc_version", + data.class_version.to_string(), + ), + ro_prop("Origin", "ext_pc_origin", vector_text(data.origin)), + text_prop("Saved File", "ext_pc_file", &data.saved_filename), + ro_prop( + "Source Files", + "ext_pc_sources", + data.source_files.join("\n"), + ), + ro_prop("Extents Min", "ext_pc_min", vector_text(data.extents_min)), + ro_prop("Extents Max", "ext_pc_max", vector_text(data.extents_max)), + ro_prop( + "Point Count", + "ext_pc_count", + data.point_count.to_string(), + ), + text_prop("UCS Name", "ext_pc_ucs", &data.ucs_name), + ro_prop( + "UCS Origin", + "ext_pc_ucs_origin", + vector_text(data.ucs_origin), + ), + ro_prop( + "UCS X", + "ext_pc_ucs_x", + vector_text(data.ucs_x_direction), + ), + ro_prop( + "UCS Y", + "ext_pc_ucs_y", + vector_text(data.ucs_y_direction), + ), + ro_prop( + "UCS Z", + "ext_pc_ucs_z", + vector_text(data.ucs_z_direction), + ), + ro_prop( + "Definition", + "ext_pc_definition", + handle_text(data.definition_handle), + ), + ro_prop( + "Reactor", + "ext_pc_reactor", + handle_text(data.reactor_handle), + ), + ], + }, + PropSection { + title: "Point Cloud Display".into(), + props: vec![ + bool_prop("Show Intensity", "ext_pc_show_intensity", data.show_intensity), + ro_prop( + "Intensity Scheme", + "ext_pc_intensity_scheme", + data.intensity_scheme.to_string(), + ), + edit_prop( + "Minimum Intensity", + "ext_pc_intensity_min", + data.minimum_intensity, + ), + edit_prop( + "Maximum Intensity", + "ext_pc_intensity_max", + data.maximum_intensity, + ), + edit_prop( + "Low Threshold", + "ext_pc_low_threshold", + data.low_intensity_threshold, + ), + edit_prop( + "High Threshold", + "ext_pc_high_threshold", + data.high_intensity_threshold, + ), + bool_prop("Show Clipping", "ext_pc_show_clipping", data.show_clipping), + ro_prop("Clippings", "ext_pc_clippings", clips), + ], + }, + ] +} + +fn point_cloud_ex_properties(data: &PointCloudExData) -> Vec { + let crops = data + .croppings + .iter() + .enumerate() + .map(|(index, crop)| { + format!( + "{}: type {}; inside {}; inverted {}; points {}; plane [{}]", + index + 1, + crop.crop_type, + crop.inside, + crop.inverted, + crop.points.len(), + vector_text(crop.plane) + ) + }) + .collect::>() + .join("\n"); + vec![ + PropSection { + title: "Point Cloud Ex".into(), + props: vec![ + ro_prop( + "Class Version", + "ext_pcx_version", + data.class_version.to_string(), + ), + text_prop("Name", "ext_pcx_name", &data.name), + ro_prop("Extents Min", "ext_pcx_min", vector_text(data.extents_min)), + ro_prop("Extents Max", "ext_pcx_max", vector_text(data.extents_max)), + ro_prop( + "UCS Origin", + "ext_pcx_ucs_origin", + vector_text(data.ucs_origin), + ), + ro_prop( + "UCS X", + "ext_pcx_ucs_x", + vector_text(data.ucs_x_direction), + ), + ro_prop( + "UCS Y", + "ext_pcx_ucs_y", + vector_text(data.ucs_y_direction), + ), + ro_prop( + "UCS Z", + "ext_pcx_ucs_z", + vector_text(data.ucs_z_direction), + ), + bool_prop("Locked", "ext_pcx_locked", data.locked), + ro_prop( + "Definition", + "ext_pcx_definition", + handle_text(data.definition_handle), + ), + ro_prop( + "Reactor", + "ext_pcx_reactor", + handle_text(data.reactor_handle), + ), + ], + }, + PropSection { + title: "Point Cloud Ex Display".into(), + props: vec![ + bool_prop( + "Show Intensity", + "ext_pcx_show_intensity", + data.show_intensity, + ), + bool_prop("Show Cropping", "ext_pcx_show_cropping", data.show_cropping), + ro_prop( + "Unknown Flags", + "ext_pcx_unknown", + format!("{}, {}", data.unknown_bl0, data.unknown_bl1), + ), + ro_prop( + "Stylization Type", + "ext_pcx_stylization", + data.stylization_type.to_string(), + ), + text_prop( + "Intensity Color Scheme", + "ext_pcx_intensity_scheme", + &data.intensity_color_scheme, + ), + text_prop( + "Current Color Scheme", + "ext_pcx_current_scheme", + &data.current_color_scheme, + ), + text_prop( + "Classification Scheme", + "ext_pcx_class_scheme", + &data.classification_color_scheme, + ), + edit_prop("Elevation Min", "ext_pcx_elevation_min", data.elevation_min), + edit_prop("Elevation Max", "ext_pcx_elevation_max", data.elevation_max), + ro_prop( + "Intensity Range", + "ext_pcx_intensity_range", + format!("{}..{}", data.intensity_min, data.intensity_max), + ), + ro_prop( + "Out Of Range Behavior", + "ext_pcx_out_of_range", + format!( + "intensity {}; elevation {}", + data.intensity_out_of_range_behavior, + data.elevation_out_of_range_behavior + ), + ), + bool_prop( + "Fixed Elevation Range", + "ext_pcx_fixed_range", + data.elevation_apply_to_fixed_range, + ), + bool_prop( + "Intensity Gradient", + "ext_pcx_intensity_gradient", + data.intensity_as_gradient, + ), + bool_prop( + "Elevation Gradient", + "ext_pcx_elevation_gradient", + data.elevation_as_gradient, + ), + ro_prop("Croppings", "ext_pcx_croppings", crops), + ], + }, + ] +} + +fn semantic_properties( + properties: &[acadrust::objects::SemanticProperty], +) -> String { + properties + .iter() + .map(|property| { + format!( + "{} [{}]: {:?}", + property.subclass, property.code, property.value + ) + }) + .collect::>() + .join("\n") +} + +fn reference_properties( + references: &[acadrust::objects::ProxyObjectReference], +) -> String { + references + .iter() + .map(|reference| { + format!( + "{:X}: {:?}", + reference.handle.value(), + reference.kind + ) + }) + .collect::>() + .join("\n") +} + +fn properties(entity: &ExtendedEntity) -> Vec { + match &entity.data { + ExtendedEntityData::Camera { view_handle } => vec![PropSection { + title: "Camera".into(), + props: vec![ro_prop("View", "ext_camera_view", handle_text(*view_handle))], + }], + ExtendedEntityData::SectionObject(data) => section_properties(data), + ExtendedEntityData::ArcAlignedText(data) => arc_text_properties(data), + ExtendedEntityData::RemoteText(data) => remote_text_properties(data), + ExtendedEntityData::GeoPositionMarker(data) => geo_marker_properties(data), + ExtendedEntityData::CoordinationModel(data) => vec![PropSection { + title: "Coordination Model".into(), + props: vec![ + ro_prop("Flags", "ext_coord_flags", data.flags.to_string()), + ro_prop( + "Definition", + "ext_coord_definition", + handle_text(data.definition_handle), + ), + edit_prop("Unit Factor", "ext_coord_unit", data.unit_factor), + ro_prop( + "Transform", + "ext_coord_transform", + data.transform + .chunks_exact(4) + .map(|row| { + format!( + "{:.6}, {:.6}, {:.6}, {:.6}", + row[0], row[1], row[2], row[3] + ) + }) + .collect::>() + .join("\n"), + ), + ], + }], + ExtendedEntityData::PointCloud(data) => point_cloud_properties(data), + ExtendedEntityData::PointCloudEx(data) => point_cloud_ex_properties(data), + ExtendedEntityData::Proxy(data) => vec![PropSection { + title: "Proxy Entity".into(), + props: vec![ + ro_prop("Proxy ID", "ext_proxy_id", data.proxy_id.to_string()), + ro_prop("Class ID", "ext_proxy_class", data.class_id.to_string()), + ro_prop("DXF Subclass", "ext_proxy_subclass", data.dxf_subclass.clone()), + ro_prop("Version", "ext_proxy_version", data.version.to_string()), + ro_prop( + "DWG Version", + "ext_proxy_dwg_version", + format!("{}.{}", data.dwg_version, data.maintenance_version), + ), + ro_prop("From DXF", "ext_proxy_from_dxf", data.from_dxf.to_string()), + ro_prop( + "Graphics", + "ext_proxy_graphics", + format!("{} bits", data.graphics.bit_count), + ), + ro_prop( + "Payload", + "ext_proxy_payload", + format!("{} bits", data.payload.bit_count), + ), + ro_prop( + "Text Payload", + "ext_proxy_text_payload", + format!("{} bits", data.text_payload.bit_count), + ), + ro_prop( + "Object References", + "ext_proxy_references", + reference_properties(&data.object_ids), + ), + ], + }], + ExtendedEntityData::OleFrame(data) => vec![PropSection { + title: "OLE Frame".into(), + props: vec![ + ro_prop("Flag", "ext_ole_flag", data.flag.to_string()), + ro_prop("Mode", "ext_ole_mode", data.mode.to_string()), + ro_prop( + "Storage Size", + "ext_ole_size", + format!("{} bytes", data.storage.encoded_len()), + ), + ], + }], + ExtendedEntityData::LayoutPrintConfig(data) => vec![PropSection { + title: "Layout Print Configuration".into(), + props: vec![ + ro_prop( + "Class Version", + "ext_print_version", + data.class_version.to_string(), + ), + ro_prop("Flag", "ext_print_flag", data.flag.to_string()), + ], + }], + ExtendedEntityData::Format(data) => vec![PropSection { + title: "Format".into(), + props: vec![ + ro_prop( + "DWG Payload", + "ext_format_dwg", + format!( + "{} bytes / {} handle bits", + data.raw_dwg_data.as_ref().map_or(0, Vec::len), + data.raw_dwg_handle_bits + ), + ), + ro_prop( + "DWG Version", + "ext_format_version", + format!("{:?}", data.raw_dwg_version), + ), + ro_prop( + "DXF Codes", + "ext_format_dxf", + data.raw_dxf_codes.as_ref().map_or(0, Vec::len).to_string(), + ), + ], + }], + ExtendedEntityData::Legacy(data) => vec![PropSection { + title: "Legacy Entity".into(), + props: vec![ro_prop("Data", "ext_legacy_data", format!("{data:#?}"))], + }], + ExtendedEntityData::DynamicBlock(data) => vec![PropSection { + title: "Dynamic Block Entity".into(), + props: vec![ + ro_prop( + "Class", + "ext_dynamic_class", + data.entity_dxf_name().unwrap_or("Helper"), + ), + ro_prop("Decoded Data", "ext_dynamic_data", format!("{data:#?}")), + ], + }], + ExtendedEntityData::RegisteredClass(data) => vec![PropSection { + title: "Registered Class Entity".into(), + props: vec![ + ro_prop("DXF Name", "ext_registered_dxf", data.dxf_name.clone()), + ro_prop( + "C++ Class", + "ext_registered_cpp", + data.cpp_class_name.clone(), + ), + ro_prop( + "Properties", + "ext_registered_properties", + semantic_properties(&data.properties), + ), + ro_prop( + "Payload", + "ext_registered_payload", + format!("{} bits", data.payload.bit_count), + ), + ro_prop( + "Object References", + "ext_registered_references", + reference_properties(&data.object_ids), + ), + ], + }], + } +} + +fn set_f64(value: &str, target: &mut f64) { + if let Some(value) = parse_f64(value) { + *target = value; + } +} + +fn apply_geom_prop(entity: &mut ExtendedEntity, field: &str, value: &str) { + match &mut entity.data { + ExtendedEntityData::SectionObject(data) => match field { + "ext_section_name" => data.name = value.to_string(), + "ext_section_top" => set_f64(value, &mut data.top_height), + "ext_section_bottom" => set_f64(value, &mut data.bottom_height), + _ => {} + }, + ExtendedEntityData::ArcAlignedText(data) => match field { + "ext_arc_text" => data.text = value.to_string(), + "ext_arc_font" => data.font_name = value.to_string(), + "ext_arc_big_font" => data.big_font_name = value.to_string(), + "ext_arc_style" => data.style_name = value.to_string(), + "ext_arc_radius" => set_f64(value, &mut data.radius), + "ext_arc_xscale" => set_f64(value, &mut data.x_scale), + "ext_arc_size" => set_f64(value, &mut data.text_size), + "ext_arc_spacing" => set_f64(value, &mut data.character_spacing), + "ext_arc_offset" => set_f64(value, &mut data.offset_from_arc), + "ext_arc_right" => set_f64(value, &mut data.right_offset), + "ext_arc_left" => set_f64(value, &mut data.left_offset), + "ext_arc_start" => { + if let Some(value) = parse_f64(value) { + data.start_angle = value.to_radians(); + } + } + "ext_arc_end" => { + if let Some(value) = parse_f64(value) { + data.end_angle = value.to_radians(); + } + } + "ext_arc_reverse" => data.reverse = !data.reverse, + "ext_arc_bold" => data.bold = !data.bold, + "ext_arc_italic" => data.italic = !data.italic, + "ext_arc_underlined" => data.underlined = !data.underlined, + "ext_arc_shx" => data.is_shx = !data.is_shx, + "ext_arc_wizard" => data.wizard_flag = !data.wizard_flag, + _ => {} + }, + ExtendedEntityData::RemoteText(data) => match field { + "ext_rtext_text" => data.text = value.to_string(), + "ext_rtext_style" => data.style_name = value.to_string(), + "ext_rtext_height" => set_f64(value, &mut data.height), + "ext_rtext_rotation" => { + if let Some(value) = parse_f64(value) { + data.rotation = value.to_radians(); + } + } + _ => {} + }, + ExtendedEntityData::GeoPositionMarker(data) => match field { + "ext_geo_radius" => set_f64(value, &mut data.radius), + "ext_geo_notes" => data.notes = value.to_string(), + "ext_geo_gap" => set_f64(value, &mut data.landing_gap), + "ext_geo_visible" => data.mtext_visible = !data.mtext_visible, + "ext_geo_frame" => data.enable_frame_text = !data.enable_frame_text, + _ => {} + }, + ExtendedEntityData::CoordinationModel(data) => { + if field == "ext_coord_unit" { + set_f64(value, &mut data.unit_factor); + } + } + ExtendedEntityData::PointCloud(data) => match field { + "ext_pc_file" => data.saved_filename = value.to_string(), + "ext_pc_ucs" => data.ucs_name = value.to_string(), + "ext_pc_show_intensity" => data.show_intensity = !data.show_intensity, + "ext_pc_show_clipping" => data.show_clipping = !data.show_clipping, + "ext_pc_intensity_min" => set_f64(value, &mut data.minimum_intensity), + "ext_pc_intensity_max" => set_f64(value, &mut data.maximum_intensity), + "ext_pc_low_threshold" => set_f64(value, &mut data.low_intensity_threshold), + "ext_pc_high_threshold" => set_f64(value, &mut data.high_intensity_threshold), + _ => {} + }, + ExtendedEntityData::PointCloudEx(data) => match field { + "ext_pcx_name" => data.name = value.to_string(), + "ext_pcx_locked" => data.locked = !data.locked, + "ext_pcx_show_intensity" => data.show_intensity = !data.show_intensity, + "ext_pcx_show_cropping" => data.show_cropping = !data.show_cropping, + "ext_pcx_intensity_scheme" => data.intensity_color_scheme = value.to_string(), + "ext_pcx_current_scheme" => data.current_color_scheme = value.to_string(), + "ext_pcx_class_scheme" => data.classification_color_scheme = value.to_string(), + "ext_pcx_elevation_min" => set_f64(value, &mut data.elevation_min), + "ext_pcx_elevation_max" => set_f64(value, &mut data.elevation_max), + "ext_pcx_fixed_range" => { + data.elevation_apply_to_fixed_range = !data.elevation_apply_to_fixed_range + } + "ext_pcx_intensity_gradient" => { + data.intensity_as_gradient = !data.intensity_as_gradient + } + "ext_pcx_elevation_gradient" => { + data.elevation_as_gradient = !data.elevation_as_gradient + } + _ => {} + }, + _ => {} + } +} + +fn apply_point(point: &mut Vector3, apply: GripApply) { + match apply { + GripApply::Translate(delta) => { + point.x += delta.x; + point.y += delta.y; + point.z += delta.z; + } + GripApply::Absolute(position) => { + *point = Vector3::new(position.x, position.y, position.z); + } + } +} + +fn move_extents( + min: &mut Vector3, + max: &mut Vector3, + grip_id: usize, + apply: GripApply, +) { + match grip_id { + 0 => apply_point(min, apply), + 1 => apply_point(max, apply), + 2 => { + let center = (*min + *max) * 0.5; + let delta = match apply { + GripApply::Translate(delta) => delta, + GripApply::Absolute(position) => { + position - glam::DVec3::new(center.x, center.y, center.z) + } + }; + let offset = Vector3::new(delta.x, delta.y, delta.z); + *min = *min + offset; + *max = *max + offset; + } + _ => {} + } +} + +fn grips(entity: &ExtendedEntity) -> Vec { + match &entity.data { + ExtendedEntityData::SectionObject(data) => data + .vertices + .iter() + .chain(data.back_line_vertices.iter()) + .enumerate() + .map(|(index, point)| { + square_grip(index, glam::DVec3::new(point.x, point.y, point.z)) + }) + .collect(), + ExtendedEntityData::ArcAlignedText(data) => vec![center_grip( + 0, + glam::DVec3::new(data.center.x, data.center.y, data.center.z), + )], + ExtendedEntityData::RemoteText(data) => vec![square_grip( + 0, + glam::DVec3::new(data.position.x, data.position.y, data.position.z), + )], + ExtendedEntityData::GeoPositionMarker(data) => vec![center_grip( + 0, + glam::DVec3::new(data.position.x, data.position.y, data.position.z), + )], + ExtendedEntityData::PointCloud(data) => vec![ + square_grip( + 0, + glam::DVec3::new( + data.extents_min.x, + data.extents_min.y, + data.extents_min.z, + ), + ), + square_grip( + 1, + glam::DVec3::new( + data.extents_max.x, + data.extents_max.y, + data.extents_max.z, + ), + ), + center_grip( + 2, + glam::DVec3::new( + (data.extents_min.x + data.extents_max.x) * 0.5, + (data.extents_min.y + data.extents_max.y) * 0.5, + (data.extents_min.z + data.extents_max.z) * 0.5, + ), + ), + ], + ExtendedEntityData::PointCloudEx(data) => vec![ + square_grip( + 0, + glam::DVec3::new( + data.extents_min.x, + data.extents_min.y, + data.extents_min.z, + ), + ), + square_grip( + 1, + glam::DVec3::new( + data.extents_max.x, + data.extents_max.y, + data.extents_max.z, + ), + ), + center_grip( + 2, + glam::DVec3::new( + (data.extents_min.x + data.extents_max.x) * 0.5, + (data.extents_min.y + data.extents_max.y) * 0.5, + (data.extents_min.z + data.extents_max.z) * 0.5, + ), + ), + ], + _ => Vec::new(), + } +} + +fn apply_grip(entity: &mut ExtendedEntity, grip_id: usize, apply: GripApply) { + match &mut entity.data { + ExtendedEntityData::SectionObject(data) => { + if grip_id < data.vertices.len() { + apply_point(&mut data.vertices[grip_id], apply); + } else if let Some(point) = data + .back_line_vertices + .get_mut(grip_id - data.vertices.len()) + { + apply_point(point, apply); + } + } + ExtendedEntityData::ArcAlignedText(data) if grip_id == 0 => { + apply_point(&mut data.center, apply) + } + ExtendedEntityData::RemoteText(data) if grip_id == 0 => { + apply_point(&mut data.position, apply) + } + ExtendedEntityData::GeoPositionMarker(data) if grip_id == 0 => { + let before = data.position; + apply_point(&mut data.position, apply); + if let Some(text) = data.embedded_mtext.as_mut() { + text.insertion_point = + text.insertion_point + (data.position - before); + } + } + ExtendedEntityData::PointCloud(data) => { + let before = (data.extents_min + data.extents_max) * 0.5; + move_extents( + &mut data.extents_min, + &mut data.extents_max, + grip_id, + apply, + ); + if grip_id == 2 { + let after = (data.extents_min + data.extents_max) * 0.5; + let delta = after - before; + data.origin = data.origin + delta; + data.ucs_origin = data.ucs_origin + delta; + } + } + ExtendedEntityData::PointCloudEx(data) => { + let before = (data.extents_min + data.extents_max) * 0.5; + move_extents( + &mut data.extents_min, + &mut data.extents_max, + grip_id, + apply, + ); + if grip_id == 2 { + let after = (data.extents_min + data.extents_max) * 0.5; + let delta = after - before; + data.ucs_origin = data.ucs_origin + delta; + for crop in &mut data.croppings { + for point in &mut crop.points { + *point = *point + delta; + } + } + } + } + _ => {} + } +} + +fn entity_transform(transform: &EntityTransform) -> Transform { + match transform { + EntityTransform::Translate(delta) => { + Transform::from_translation(Vector3::new(delta.x, delta.y, delta.z)) + } + EntityTransform::Rotate { center, angle_rad } => { + Transform::from_translation(Vector3::new(-center.x, -center.y, -center.z)) + .then(&Transform::from_rotation(Vector3::UNIT_Z, *angle_rad)) + .then(&Transform::from_translation(Vector3::new( + center.x, center.y, center.z, + ))) + } + EntityTransform::Scale { center, factor } => Transform::from_scaling_with_origin( + Vector3::new(*factor, *factor, *factor), + Vector3::new(center.x, center.y, center.z), + ), + EntityTransform::Mirror { p1, p2 } => { + crate::scene::view::transform::reflection_about_xy_line(*p1, *p2) + } + } +} + +fn scalar_scale(transform: &EntityTransform) -> f64 { + match transform { + EntityTransform::Scale { factor, .. } => factor.abs(), + _ => 1.0, + } +} + +fn transform_extents(min: Vector3, max: Vector3, transform: &Transform) -> (Vector3, Vector3) { + let corners = [ + Vector3::new(min.x, min.y, min.z), + Vector3::new(max.x, min.y, min.z), + Vector3::new(max.x, max.y, min.z), + Vector3::new(min.x, max.y, min.z), + Vector3::new(min.x, min.y, max.z), + Vector3::new(max.x, min.y, max.z), + Vector3::new(max.x, max.y, max.z), + Vector3::new(min.x, max.y, max.z), + ]; + let mut out_min = Vector3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY); + let mut out_max = Vector3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY); + for point in corners.map(|point| transform.apply(point)) { + out_min.x = out_min.x.min(point.x); + out_min.y = out_min.y.min(point.y); + out_min.z = out_min.z.min(point.z); + out_max.x = out_max.x.max(point.x); + out_max.y = out_max.y.max(point.y); + out_max.z = out_max.z.max(point.z); + } + (out_min, out_max) +} + +fn transformed_planar_angle( + normal: Vector3, + angle: f64, + transform: &Transform, +) -> (Vector3, f64) { + let (axis_x, axis_y, _) = plane_axes(normal); + let direction = axis_x * angle.cos() + axis_y * angle.sin(); + let new_normal = normalized(transform.apply_rotation(normal), Vector3::UNIT_Z); + let new_direction = normalized(transform.apply_rotation(direction), Vector3::UNIT_X); + let (new_x, new_y, _) = plane_axes(new_normal); + ( + new_normal, + new_direction.dot(&new_y).atan2(new_direction.dot(&new_x)), + ) +} + +fn apply_transform(entity: &mut ExtendedEntity, requested: &EntityTransform) { + let transform = entity_transform(requested); + let scale = scalar_scale(requested); + match &mut entity.data { + ExtendedEntityData::SectionObject(data) => { + for point in data + .vertices + .iter_mut() + .chain(data.back_line_vertices.iter_mut()) + { + *point = transform.apply(*point); + } + data.vertical_direction = + normalized(transform.apply_rotation(data.vertical_direction), Vector3::UNIT_Z); + data.top_height *= scale; + data.bottom_height *= scale; + } + ExtendedEntityData::ArcAlignedText(data) => { + let old_normal = data.normal; + let (_, start) = + transformed_planar_angle(old_normal, data.start_angle, &transform); + let (normal, end) = + transformed_planar_angle(old_normal, data.end_angle, &transform); + data.center = transform.apply(data.center); + data.normal = normal; + data.start_angle = start; + data.end_angle = end; + data.radius *= scale; + data.text_size *= scale; + data.offset_from_arc *= scale; + data.right_offset *= scale; + data.left_offset *= scale; + } + ExtendedEntityData::RemoteText(data) => { + let (normal, rotation) = + transformed_planar_angle(data.normal, data.rotation, &transform); + data.position = transform.apply(data.position); + data.normal = normal; + data.rotation = rotation; + data.height *= scale; + } + ExtendedEntityData::GeoPositionMarker(data) => { + data.position = transform.apply(data.position); + data.radius *= scale; + data.landing_gap *= scale; + if let Some(text) = data.embedded_mtext.as_mut() { + acadrust::Entity::apply_transform(text, &transform); + } + } + ExtendedEntityData::CoordinationModel(data) => { + let mut matrix = acadrust::types::Matrix4::zero(); + for row in 0..4 { + for column in 0..4 { + matrix.m[row][column] = data.transform[row * 4 + column]; + } + } + let composed = transform.matrix * matrix; + for row in 0..4 { + for column in 0..4 { + data.transform[row * 4 + column] = composed.m[row][column]; + } + } + data.unit_factor *= scale; + } + ExtendedEntityData::PointCloud(data) => { + data.origin = transform.apply(data.origin); + data.ucs_origin = transform.apply(data.ucs_origin); + data.ucs_x_direction = normalized( + transform.apply_rotation(data.ucs_x_direction), + Vector3::UNIT_X, + ); + data.ucs_y_direction = normalized( + transform.apply_rotation(data.ucs_y_direction), + Vector3::UNIT_Y, + ); + data.ucs_z_direction = normalized( + transform.apply_rotation(data.ucs_z_direction), + Vector3::UNIT_Z, + ); + (data.extents_min, data.extents_max) = + transform_extents(data.extents_min, data.extents_max, &transform); + for clip in &mut data.clippings { + let mut min = Vector3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY); + let mut max = + Vector3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY); + for point in &clip.vertices { + for z in [clip.z_min, clip.z_max] { + let point = transform.apply(Vector3::new(point.x, point.y, z)); + min.x = min.x.min(point.x); + min.y = min.y.min(point.y); + min.z = min.z.min(point.z); + max.x = max.x.max(point.x); + max.y = max.y.max(point.y); + max.z = max.z.max(point.z); + } + } + for point in &mut clip.vertices { + let transformed = transform.apply(Vector3::new(point.x, point.y, clip.z_min)); + point.x = transformed.x; + point.y = transformed.y; + } + if min.z.is_finite() { + clip.z_min = min.z; + clip.z_max = max.z; + } + } + } + ExtendedEntityData::PointCloudEx(data) => { + data.ucs_origin = transform.apply(data.ucs_origin); + data.ucs_x_direction = normalized( + transform.apply_rotation(data.ucs_x_direction), + Vector3::UNIT_X, + ); + data.ucs_y_direction = normalized( + transform.apply_rotation(data.ucs_y_direction), + Vector3::UNIT_Y, + ); + data.ucs_z_direction = normalized( + transform.apply_rotation(data.ucs_z_direction), + Vector3::UNIT_Z, + ); + (data.extents_min, data.extents_max) = + transform_extents(data.extents_min, data.extents_max, &transform); + for crop in &mut data.croppings { + crop.plane = + normalized(transform.apply_rotation(crop.plane), Vector3::UNIT_Z); + crop.x_direction = normalized( + transform.apply_rotation(crop.x_direction), + Vector3::UNIT_X, + ); + crop.y_direction = normalized( + transform.apply_rotation(crop.y_direction), + Vector3::UNIT_Y, + ); + for point in &mut crop.points { + *point = transform.apply(*point); + } + } + } + _ => {} + } +} + +impl TruckConvertible for ExtendedEntity { + fn to_truck(&self, document: &acadrust::CadDocument) -> Option { + to_truck(self, document) + } +} + +impl Grippable for ExtendedEntity { + fn grips(&self) -> Vec { + grips(self) + } + + fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { + apply_grip(self, grip_id, apply); + } +} + +impl PropertyEditable for ExtendedEntity { + fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { + properties(self) + } + + fn apply_geom_prop(&mut self, field: &str, value: &str) { + apply_geom_prop(self, field, value); + } +} + +impl Transformable for ExtendedEntity { + fn apply_transform(&mut self, transform: &EntityTransform) { + apply_transform(self, transform); + } +} diff --git a/src/entities/field.rs b/src/entities/field.rs index 8ddee9256..b3373e2d1 100644 --- a/src/entities/field.rs +++ b/src/entities/field.rs @@ -53,6 +53,14 @@ pub fn resolve(document: &CadDocument, host: Handle) -> Option { acadrust::fields::resolve(document, host, &OcsFieldContext) } +pub fn resolve_handle( + document: &CadDocument, + field: Handle, + host: Handle, +) -> Option { + acadrust::fields::resolve_handle(document, field, host, &OcsFieldContext) +} + /// Seconds since the Unix epoch. Native uses the system clock; wasm uses the JS /// `Date` clock. This platform-specific read stays in the app, not the library. fn epoch_secs() -> i64 { diff --git a/src/entities/mesh.rs b/src/entities/mesh.rs index 28310501e..1f4922e2f 100644 --- a/src/entities/mesh.rs +++ b/src/entities/mesh.rs @@ -2,10 +2,10 @@ use acadrust::entities::{mesh::Mesh, polygon_mesh::PolygonMesh, Face3D, Polyface use glam::Vec3; use crate::command::EntityTransform; -use crate::entities::common::{ro_prop as ro, square_grip}; +use crate::entities::common::{parse_f64, ro_prop as ro, 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; /// Triangulate a planar (possibly concave) polygon into a flat triangle-soup @@ -319,63 +319,13 @@ impl TruckConvertible for PolygonMesh { return None; } - let closed_m = self - .flags - .contains(acadrust::entities::PolygonMeshFlags::CLOSED_M); - let closed_n = self - .flags - .contains(acadrust::entities::PolygonMeshFlags::CLOSED_N); - - let pt = |i: usize, j: usize| -> [f64; 3] { - let v = &self.vertices[i * n + j]; - [v.location.x, v.location.y, v.location.z] - }; - - let mut pts: Vec<[f64; 3]> = Vec::new(); - let mut fill_tris: Vec<[f64; 3]> = Vec::new(); - - // Rows (M direction). - for i in 0..m { - pts.push([f64::NAN; 3]); - for j in 0..n { - pts.push(pt(i, j)); - } - if closed_n { - pts.push(pt(i, 0)); - } - } - - // Columns (N direction). - for j in 0..n { - pts.push([f64::NAN; 3]); - for i in 0..m { - pts.push(pt(i, j)); - } - if closed_m { - pts.push(pt(0, j)); - } - } - - // Fill: triangulate each grid quad (two triangles per cell). - let mi = if closed_m { m } else { m - 1 }; - let ni = if closed_n { n } else { n - 1 }; - for i in 0..mi { - for j in 0..ni { - let p00 = pt(i, j); - let p10 = pt((i + 1) % m, j); - let p01 = pt(i, (j + 1) % n); - let p11 = pt((i + 1) % m, (j + 1) % n); - fill_tris.extend_from_slice(&[p00, p10, p11, p00, p11, p01]); - } - } - Some(TruckEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(pts), + object: TruckObject::Lines(Vec::new()), snap_pts: vec![], tangent_geoms: vec![], key_vertices: vec![], - fill_tris, + fill_tris: vec![], }) } } @@ -491,47 +441,13 @@ impl TruckConvertible for PolyfaceMesh { return None; } - let get_v = |idx: i16| -> Option<[f64; 3]> { - let i = (idx.abs() as usize).checked_sub(1)?; - let v = self.vertices.get(i)?; - Some([v.location.x, v.location.y, v.location.z]) - }; - - let mut pts: Vec<[f64; 3]> = Vec::new(); - let mut fill_tris: Vec<[f64; 3]> = Vec::new(); - - for face in &self.faces { - // Indices: 0 means unused. Negative = invisible edge (still render for wireframe). - let indices = [face.index1, face.index2, face.index3, face.index4]; - let verts: Vec<[f64; 3]> = indices - .iter() - .filter(|&&i| i != 0) - .filter_map(|&i| get_v(i)) - .collect(); - - if verts.len() < 2 { - continue; - } - pts.push([f64::NAN; 3]); - for &p in &verts { - pts.push(p); - } - // Close the face polygon. - pts.push(verts[0]); - - // Ear-clip the face for solid fill (handles concave faces). - if verts.len() >= 3 { - fill_tris.extend(triangulate_planar(&verts)); - } - } - Some(TruckEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(pts), + object: TruckObject::Lines(Vec::new()), snap_pts: vec![], tangent_geoms: vec![], key_vertices: vec![], - fill_tris, + fill_tris: vec![], }) } } @@ -637,53 +553,527 @@ impl Transformable for PolyfaceMesh { // ── Mesh (SubD mesh) ────────────────────────────────────────────────────────── // // Modern subdivision mesh — distinct from PolygonMesh. The render path emits -// the file's per-edge wireframe and triangulates each face into fill_tris so -// solid views still draw a shaded surface. Subdivision-level smoothing is -// honoured only as metadata; we don't run a Catmull-Clark refinement pass -// here yet. +// the refined per-edge wireframe and triangulates each face into fill_tris so +// solid views draw the same Catmull-Clark surface described by the DWG. + +#[derive(Clone)] +struct RefinedMesh { + vertices: Vec<[f64; 3]>, + faces: Vec>, + creases: std::collections::HashMap<(usize, usize), f64>, +} + +fn edge_key(a: usize, b: usize) -> (usize, usize) { + if a < b { (a, b) } else { (b, a) } +} + +fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] { + [a[0] + b[0], a[1] + b[1], a[2] + b[2]] +} + +fn mul3(a: [f64; 3], scale: f64) -> [f64; 3] { + [a[0] * scale, a[1] * scale, a[2] * scale] +} + +fn mix3(a: [f64; 3], b: [f64; 3], amount: f64) -> [f64; 3] { + add3(mul3(a, 1.0 - amount), mul3(b, amount)) +} + +fn mean_points<'a>(points: impl Iterator) -> [f64; 3] { + let mut sum = [0.0; 3]; + let mut count = 0usize; + for point in points { + sum = add3(sum, *point); + count += 1; + } + if count == 0 { + sum + } else { + mul3(sum, 1.0 / count as f64) + } +} + +fn base_refined_mesh(mesh: &Mesh) -> RefinedMesh { + let vertices: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| [v.x, v.y, v.z]).collect(); + let faces = mesh + .faces + .iter() + .filter_map(|face| { + let valid: Vec = face + .vertices + .iter() + .copied() + .filter(|&index| index < vertices.len()) + .collect(); + (valid.len() >= 3).then_some(valid) + }) + .collect(); + let creases = mesh + .edges + .iter() + .filter_map(|edge| { + let sharpness = edge.crease?; + (edge.start < vertices.len() && edge.end < vertices.len() && sharpness > 0.0) + .then_some((edge_key(edge.start, edge.end), sharpness)) + }) + .collect(); + RefinedMesh { + vertices, + faces, + creases, + } +} + +fn subdivide_catmull_clark(mesh: &RefinedMesh, blend_crease: bool) -> RefinedMesh { + use std::collections::{HashMap, HashSet}; + + if mesh.faces.is_empty() { + return mesh.clone(); + } + + let face_points: Vec<[f64; 3]> = mesh + .faces + .iter() + .map(|face| mean_points(face.iter().filter_map(|&index| mesh.vertices.get(index)))) + .collect(); + + let mut edge_faces: HashMap<(usize, usize), Vec> = HashMap::new(); + let mut vertex_faces: Vec> = vec![Vec::new(); mesh.vertices.len()]; + let mut vertex_edges: Vec> = vec![HashSet::new(); mesh.vertices.len()]; + for (face_index, face) in mesh.faces.iter().enumerate() { + for &vertex in face { + if let Some(faces) = vertex_faces.get_mut(vertex) { + faces.push(face_index); + } + } + for corner in 0..face.len() { + let a = face[corner]; + let b = face[(corner + 1) % face.len()]; + if a == b { + continue; + } + let key = edge_key(a, b); + edge_faces.entry(key).or_default().push(face_index); + if let Some(edges) = vertex_edges.get_mut(a) { + edges.insert(key); + } + if let Some(edges) = vertex_edges.get_mut(b) { + edges.insert(key); + } + } + } + + let mut vertex_points = Vec::with_capacity(mesh.vertices.len()); + for (vertex_index, &point) in mesh.vertices.iter().enumerate() { + let incident_edges = &vertex_edges[vertex_index]; + let incident_faces = &vertex_faces[vertex_index]; + if incident_edges.is_empty() || incident_faces.is_empty() { + vertex_points.push(point); + continue; + } + + let face_average = + mean_points(incident_faces.iter().filter_map(|&index| face_points.get(index))); + let edge_midpoints: Vec<[f64; 3]> = incident_edges + .iter() + .filter_map(|&(a, b)| { + let other = if a == vertex_index { b } else { a }; + mesh.vertices + .get(other) + .map(|&p| mul3(add3(point, p), 0.5)) + }) + .collect(); + let edge_average = mean_points(edge_midpoints.iter()); + let n = incident_faces.len() as f64; + let smooth = mul3( + add3(add3(face_average, mul3(edge_average, 2.0)), mul3(point, n - 3.0)), + 1.0 / n, + ); + + let mut sharp_neighbours: Vec<(f64, [f64; 3])> = incident_edges + .iter() + .filter_map(|&key| { + let boundary = edge_faces.get(&key).is_some_and(|faces| faces.len() == 1); + let sharpness = if boundary { + f64::INFINITY + } else { + mesh.creases.get(&key).copied().unwrap_or(0.0) + }; + if sharpness <= 0.0 { + return None; + } + let other = if key.0 == vertex_index { key.1 } else { key.0 }; + mesh.vertices.get(other).copied().map(|p| (sharpness, p)) + }) + .collect(); + sharp_neighbours.sort_by(|a, b| b.0.total_cmp(&a.0)); + + let crease_point = match sharp_neighbours.as_slice() { + [(_, first), (_, second), ..] => mul3( + add3(add3(mul3(point, 6.0), *first), *second), + 1.0 / 8.0, + ), + _ => smooth, + }; + let corner_point = if sharp_neighbours.len() >= 3 { + point + } else { + crease_point + }; + let sharpness = sharp_neighbours + .get(1) + .map(|item| item.0) + .unwrap_or(0.0); + let amount = if blend_crease { + sharpness.clamp(0.0, 1.0) + } else if sharpness > 0.0 { + 1.0 + } else { + 0.0 + }; + vertex_points.push(mix3(smooth, corner_point, amount)); + } + + let mut edge_point_indices = HashMap::with_capacity(edge_faces.len()); + let mut vertices = vertex_points; + for (&key, adjacent_faces) in &edge_faces { + let Some((&a, &b)) = mesh.vertices.get(key.0).zip(mesh.vertices.get(key.1)) else { + continue; + }; + let midpoint = mul3(add3(a, b), 0.5); + let smooth = if adjacent_faces.len() == 2 { + let f0 = face_points[adjacent_faces[0]]; + let f1 = face_points[adjacent_faces[1]]; + mul3(add3(add3(a, b), add3(f0, f1)), 0.25) + } else { + midpoint + }; + let boundary = adjacent_faces.len() != 2; + let sharpness = mesh.creases.get(&key).copied().unwrap_or(0.0); + let amount = if boundary { + 1.0 + } else if blend_crease { + sharpness.clamp(0.0, 1.0) + } else if sharpness > 0.0 { + 1.0 + } else { + 0.0 + }; + edge_point_indices.insert(key, vertices.len()); + vertices.push(mix3(smooth, midpoint, amount)); + } + + let face_point_start = vertices.len(); + vertices.extend(face_points.iter().copied()); + let mut faces = Vec::new(); + let mut creases = HashMap::new(); + for (face_index, face) in mesh.faces.iter().enumerate() { + for corner in 0..face.len() { + let vertex = face[corner]; + let next_key = edge_key(vertex, face[(corner + 1) % face.len()]); + let prev_key = edge_key(face[(corner + face.len() - 1) % face.len()], vertex); + let (Some(&next_edge), Some(&prev_edge)) = ( + edge_point_indices.get(&next_key), + edge_point_indices.get(&prev_key), + ) else { + continue; + }; + faces.push(vec![ + vertex, + next_edge, + face_point_start + face_index, + prev_edge, + ]); + } + } + + for (&key, &sharpness) in &mesh.creases { + let Some(&edge_point) = edge_point_indices.get(&key) else { + continue; + }; + let child_sharpness = (sharpness - 1.0).max(0.0); + if child_sharpness > 0.0 { + creases.insert(edge_key(key.0, edge_point), child_sharpness); + creases.insert(edge_key(edge_point, key.1), child_sharpness); + } + } + + RefinedMesh { + vertices, + faces, + creases, + } +} + +fn display_mesh(mesh: &Mesh) -> RefinedMesh { + let mut refined = base_refined_mesh(mesh); + for _ in 0..mesh.subdivision_level.clamp(0, 4) { + refined = subdivide_catmull_clark(&refined, mesh.blend_crease); + } + refined +} + +fn face_triangle_indices( + vertices: &[[f64; 3]], + faces: &[Vec], +) -> (Vec, Vec) { + let mut indices = Vec::new(); + let mut triangle_faces = Vec::new(); + for (face_index, face) in faces.iter().enumerate() { + let polygon: Vec<[f64; 3]> = face + .iter() + .filter_map(|&index| vertices.get(index).copied()) + .collect(); + if polygon.len() < 3 { + continue; + } + for triangle in triangulate_planar(&polygon).chunks_exact(3) { + let mut mapped = [0u32; 3]; + let mut valid = true; + for corner in 0..3 { + let Some(local) = polygon.iter().position(|point| *point == triangle[corner]) else { + valid = false; + break; + }; + mapped[corner] = face[local] as u32; + } + if valid && mapped[0] != mapped[1] && mapped[1] != mapped[2] && mapped[2] != mapped[0] { + indices.extend(mapped); + triangle_faces.push(face_index); + } + } + } + (indices, triangle_faces) +} + +fn vertex_normals(vertices: &[[f64; 3]], indices: &[u32]) -> Vec<[f32; 3]> { + let mut normals = vec![[0.0f64; 3]; vertices.len()]; + for triangle in indices.chunks_exact(3) { + let (Some(&a), Some(&b), Some(&c)) = ( + vertices.get(triangle[0] as usize), + vertices.get(triangle[1] as usize), + vertices.get(triangle[2] as usize), + ) else { + continue; + }; + let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]]; + let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]]; + let normal = [ + ab[1] * ac[2] - ab[2] * ac[1], + ab[2] * ac[0] - ab[0] * ac[2], + ab[0] * ac[1] - ab[1] * ac[0], + ]; + for &index in triangle { + if let Some(sum) = normals.get_mut(index as usize) { + *sum = add3(*sum, normal); + } + } + } + normals + .into_iter() + .map(|normal| { + let length = + (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt(); + if length > 1e-15 { + [ + (normal[0] / length) as f32, + (normal[1] / length) as f32, + (normal[2] / length) as f32, + ] + } else { + [0.0, 0.0, 1.0] + } + }) + .collect() +} + +fn append_feature_edge( + edge_verts: &mut Vec<[f32; 3]>, + edge_verts_low: &mut Vec<[f32; 3]>, + vertices: &[[f64; 3]], + a: usize, + b: usize, +) { + for index in [a, b] { + let Some(&[x, y, z]) = vertices.get(index) else { + continue; + }; + let high = [x as f32, y as f32, z as f32]; + edge_verts.push(high); + edge_verts_low.push([ + (x - high[0] as f64) as f32, + (y - high[1] as f64) as f32, + (z - high[2] as f64) as f32, + ]); + } +} + +fn make_mesh_lod_set( + name: String, + color: [f32; 4], + vertices: Vec<[f64; 3]>, + faces: Vec>, + visible_edges: std::collections::HashSet<(usize, usize)>, + face_colors: &[Option<[f32; 4]>], +) -> Option { + let (indices, triangle_faces) = face_triangle_indices(&vertices, &faces); + if indices.is_empty() { + return None; + } + let normals = vertex_normals(&vertices, &indices); + let triangle_colors = triangle_faces + .iter() + .map(|&face| face_colors.get(face).copied().flatten()) + .collect(); + let model = crate::scene::convert::solid3d_tess::finalize_mesh( + name, + vertices.clone(), + normals, + indices, + Vec::new(), + triangle_colors, + color, + None, + ); + let mut set = crate::scene::model::mesh_model::MeshLodSet::from_lods(vec![model]); + for (a, b) in visible_edges { + append_feature_edge( + &mut set.edge_verts, + &mut set.edge_verts_low, + &vertices, + a, + b, + ); + } + Some(set) +} + +/// Convert every standard DWG mesh family to the material-aware shaded mesh +/// pipeline. The retained wire entity carries snaps/grips only; fill, normals, +/// depth, materials and face colours live in `MeshLodSet`. +pub(crate) fn tessellate_shaded_mesh( + entity: &acadrust::EntityType, + color: [f32; 4], +) -> Option { + use std::collections::HashSet; + + match entity { + acadrust::EntityType::Mesh(mesh) => { + let display = display_mesh(mesh); + let mut edges = HashSet::new(); + for face in &display.faces { + for corner in 0..face.len() { + edges.insert(edge_key(face[corner], face[(corner + 1) % face.len()])); + } + } + make_mesh_lod_set( + mesh.common.handle.value().to_string(), + color, + display.vertices, + display.faces, + edges, + &[], + ) + } + acadrust::EntityType::PolygonMesh(mesh) => { + let m = mesh.m_vertex_count.max(0) as usize; + let n = mesh.n_vertex_count.max(0) as usize; + if m == 0 || n == 0 || mesh.vertices.len() < m.saturating_mul(n) { + return None; + } + let vertices: Vec<[f64; 3]> = mesh + .vertices + .iter() + .take(m * n) + .map(|vertex| { + [ + vertex.location.x, + vertex.location.y, + vertex.location.z, + ] + }) + .collect(); + let closed_m = mesh.is_closed_m(); + let closed_n = mesh.is_closed_n(); + let m_cells = if closed_m { m } else { m.saturating_sub(1) }; + let n_cells = if closed_n { n } else { n.saturating_sub(1) }; + let mut faces = Vec::with_capacity(m_cells.saturating_mul(n_cells)); + let mut edges = HashSet::new(); + for i in 0..m_cells { + for j in 0..n_cells { + let face = vec![ + i * n + j, + ((i + 1) % m) * n + j, + ((i + 1) % m) * n + (j + 1) % n, + i * n + (j + 1) % n, + ]; + for corner in 0..4 { + edges.insert(edge_key(face[corner], face[(corner + 1) % 4])); + } + faces.push(face); + } + } + make_mesh_lod_set( + mesh.common.handle.value().to_string(), + color, + vertices, + faces, + edges, + &[], + ) + } + acadrust::EntityType::PolyfaceMesh(mesh) => { + let vertices: Vec<[f64; 3]> = mesh + .vertices + .iter() + .map(|vertex| { + [ + vertex.location.x, + vertex.location.y, + vertex.location.z, + ] + }) + .collect(); + let mut faces = Vec::new(); + let mut face_colors = Vec::new(); + let mut edges = HashSet::new(); + for face in &mesh.faces { + let raw = [face.index1, face.index2, face.index3, face.index4]; + let indices: Vec = raw + .iter() + .filter(|&&index| index != 0) + .filter_map(|&index| (index.unsigned_abs() as usize).checked_sub(1)) + .filter(|&index| index < vertices.len()) + .collect(); + if indices.len() < 3 { + continue; + } + for corner in 0..indices.len() { + if raw[corner] > 0 { + edges.insert(edge_key(indices[corner], indices[(corner + 1) % indices.len()])); + } + } + faces.push(indices); + face_colors.push(face.color.as_ref().map(crate::scene::convert::tess_util::aci_to_rgba)); + } + make_mesh_lod_set( + mesh.common.handle.value().to_string(), + color, + vertices, + faces, + edges, + &face_colors, + ) + } + _ => None, + } +} impl TruckConvertible for Mesh { fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { if self.vertices.is_empty() { return None; } - let get = |i: usize| -> Option<[f64; 3]> { self.vertices.get(i).map(|v| [v.x, v.y, v.z]) }; - - let mut pts: Vec<[f64; 3]> = Vec::new(); - if !self.edges.is_empty() { - for edge in &self.edges { - if let (Some(a), Some(b)) = (get(edge.start), get(edge.end)) { - pts.push([f64::NAN; 3]); - pts.push(a); - pts.push(b); - } - } - } else { - for face in &self.faces { - if face.vertices.len() < 2 { - continue; - } - pts.push([f64::NAN; 3]); - for &vi in &face.vertices { - if let Some(p) = get(vi) { - pts.push(p); - } - } - if let Some(first) = face.vertices.first().and_then(|&i| get(i)) { - pts.push(first); - } - } - } - - // Ear-clip each face into fill_tris so shaded views render the mesh as - // a solid surface (fan triangulation spills outside concave faces). - let mut fill_tris: Vec<[f64; 3]> = Vec::new(); - for face in &self.faces { - let verts: Vec<[f64; 3]> = face.vertices.iter().filter_map(|&vi| get(vi)).collect(); - if verts.len() >= 3 { - fill_tris.extend(triangulate_planar(&verts)); - } - } // Per-vertex snap tables cost ~56 B/vertex and are retained in every // copy of the wire set. On NWD-scale imports (#358: tens of millions @@ -705,11 +1095,11 @@ impl TruckConvertible for Mesh { Some(TruckEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(pts), + object: TruckObject::Lines(Vec::new()), snap_pts, tangent_geoms: vec![], key_vertices, - fill_tris, + fill_tris: vec![], }) } } @@ -761,12 +1151,36 @@ impl PropertyEditable for Mesh { vec![PropSection { title: "Geometry".into(), props: vec![ - ro( - "Level of Smoothness", - "msh_subdiv", - self.subdivision_level.to_string(), - ), + Property { + label: "Level of Smoothness".into(), + field: "msh_subdiv_edit", + value: PropValue::EditText(self.subdivision_level.to_string()), + }, + Property { + label: "Blend Creases".into(), + field: "msh_blend_crease", + value: PropValue::BoolToggle { + field: "msh_blend_crease", + value: self.blend_crease, + }, + }, ro("Number of Faces", "msh_f", self.faces.len().to_string()), + ro("Number of Vertices", "msh_v", self.vertices.len().to_string()), + ro("Number of Edges", "msh_e", self.edges.len().to_string()), + ro( + "Creased Edges", + "msh_creased", + self.edges + .iter() + .filter(|edge| edge.crease.is_some_and(|value| value > 0.0)) + .count() + .to_string(), + ), + ro( + "Override Option", + "msh_override", + self.override_option.to_string(), + ), ro("Number of Grips", "msh_grips", self.vertices.len().to_string()), ro( "Watertight", @@ -777,7 +1191,24 @@ impl PropertyEditable for Mesh { }] } - fn apply_geom_prop(&mut self, _field: &str, _value: &str) {} + fn apply_geom_prop(&mut self, field: &str, value: &str) { + match field { + "msh_subdiv_edit" => { + if let Some(value) = parse_f64(value) { + self.subdivision_level = (value.round() as i32).clamp(0, 4); + } + } + "msh_blend_crease" => { + self.blend_crease = match value { + "toggle" => !self.blend_crease, + "On" | "Yes" | "true" | "1" => true, + "Off" | "No" | "false" | "0" => false, + _ => self.blend_crease, + }; + } + _ => {} + } + } } impl Transformable for Mesh { @@ -789,40 +1220,3 @@ impl Transformable for Mesh { }); } } - -#[cfg(test)] -mod triangulate_tests { - use super::triangulate_planar; - - fn poly_area2d(p: &[[f64; 2]]) -> f64 { - let n = p.len(); - let mut a = 0.0; - for i in 0..n { - let b = p[(i + 1) % n]; - a += p[i][0] * b[1] - b[0] * p[i][1]; - } - (a * 0.5).abs() - } - fn tri_area(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 { - // area in XY plane (test polygons are in Z=0) - (((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])) * 0.5).abs() - } - - #[test] - fn concave_l_shape_no_spillover() { - // L-shaped hexagon (concave at the inner corner). - let poly: Vec<[f64; 3]> = vec![ - [0.0, 0.0, 0.0], [4.0, 0.0, 0.0], [4.0, 2.0, 0.0], - [2.0, 2.0, 0.0], [2.0, 4.0, 0.0], [0.0, 4.0, 0.0], - ]; - let p2: Vec<[f64; 2]> = poly.iter().map(|p| [p[0], p[1]]).collect(); - let want = poly_area2d(&p2); // = 12.0 - let tris = triangulate_planar(&poly); - assert_eq!(tris.len() % 3, 0); - assert_eq!(tris.len() / 3, poly.len() - 2, "expected n-2 triangles"); - let got: f64 = tris.chunks(3).map(|t| tri_area(t[0], t[1], t[2])).sum(); - eprintln!("concave L: want_area={want} got_area={got} tris={}", tris.len() / 3); - // Fan would overshoot (triangles outside the L). Ear clip = exact. - assert!((got - want).abs() < 1e-6, "triangle area {got} != polygon area {want} (spillover)"); - } -} diff --git a/src/entities/mod.rs b/src/entities/mod.rs index 8542d26fb..d6e0fb789 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -5,6 +5,7 @@ pub mod common; pub mod dim_override; pub mod dimension; pub mod ellipse; +pub mod extended; pub mod field; pub mod hatch; pub mod helix; @@ -34,3 +35,4 @@ pub mod tolerance; pub mod traits; pub mod underlay; pub mod viewport; +pub mod view_symbol; diff --git a/src/entities/solid3d.rs b/src/entities/solid3d.rs index ef5d9cde8..f8ad6bee8 100644 --- a/src/entities/solid3d.rs +++ b/src/entities/solid3d.rs @@ -7,8 +7,6 @@ // the MeshModel vertices to match. use acadrust::entities::{Body, Region, Solid3D, Surface}; -use glam::Vec3; - use crate::command::EntityTransform; use crate::entities::common::{center_grip, edit_prop as edit, parse_f64, ro_prop as ro}; use crate::entities::traits::{Grippable, PropertyEditable, Transformable}; @@ -42,13 +40,166 @@ fn dvec3(v: &acadrust::types::Vector3) -> glam::DVec3 { glam::DVec3::new(v.x, v.y, v.z) } -fn translate_wires(wires: &mut Vec, d: Vec3) { - for wire in wires.iter_mut() { - for pt in wire.points.iter_mut() { - pt.x += d.x as f64; - pt.y += d.y as f64; - pt.z += d.z as f64; - } +fn translate_acis_entity(entity: &mut T, d: glam::DVec3) { + acadrust::Entity::translate( + entity, + acadrust::types::Vector3::new(d.x, d.y, d.z), + ); +} + +fn yes_no(value: bool) -> &'static str { + if value { "Yes" } else { "No" } +} + +fn handle_text(handle: Option) -> String { + handle + .filter(|handle| handle.is_valid()) + .map(|handle| format!("{:X}", handle.value())) + .unwrap_or_else(|| "None".to_string()) +} + +fn acis_sections( + acis: &acadrust::entities::AcisData, + wires: &[acadrust::entities::Wire], + silhouettes: &[acadrust::entities::Silhouette], + history: Option, +) -> Vec { + let mut wire_types = [0usize; 5]; + let mut transformed = 0usize; + let mut points = 0usize; + for wire in wires { + let index = match wire.wire_type { + acadrust::entities::WireType::Silhouette => 1, + acadrust::entities::WireType::VisibleEdge => 2, + acadrust::entities::WireType::HiddenEdge => 3, + acadrust::entities::WireType::Isoline => 4, + _ => 0, + }; + wire_types[index] += 1; + transformed += wire.has_transform as usize; + points += wire.points.len(); + } + let revision = if acis.revision.has_guid { + format!( + "{}-{}-{}-{:02X?}; end {}", + acis.revision.major, + acis.revision.minor1, + acis.revision.minor2, + acis.revision.bytes, + acis.revision.end_marker + ) + } else { + "None".to_string() + }; + let bindings = if acis.materials.is_empty() { + "None".to_string() + } else { + acis.materials + .iter() + .map(|binding| { + format!( + "{}:{}→{}", + binding.array_index, + binding.absolute_reference, + handle_text(binding.material_handle) + ) + }) + .collect::>() + .join(", ") + }; + let extra = acis.extra_acis_data.as_ref().map_or_else( + || "None".to_string(), + |data| { + format!( + "{}; {} bytes", + if data.is_binary { "SAB" } else { "SAT" }, + data.size() + ) + }, + ); + vec![ + PropSection { + title: "Modeler Geometry".into(), + props: vec![ + ro( + "Format", + "acis_format", + if acis.is_binary { "SAB" } else { "SAT" }, + ), + ro("Version", "acis_version", format!("{:?}", acis.version)), + ro("Data Size", "acis_size", format!("{} bytes", acis.size())), + ro("Revision", "acis_revision", revision), + ro("Material Bindings", "acis_materials", bindings), + ro("Extra Modeler Data", "acis_extra", extra), + ro("History", "acis_history", handle_text(history)), + ], + }, + PropSection { + title: "Modeler Display Data".into(), + props: vec![ + ro( + "Wireframe Cache", + "acis_wireframe", + yes_no(acis.wireframe_data_present), + ), + ro( + "Reference Point", + "acis_wireframe_point", + yes_no(acis.wireframe_point_present), + ), + ro( + "Isoline List", + "acis_wireframe_isolines", + yes_no(acis.wireframe_isoline_present), + ), + ro( + "Isolines", + "acis_isolines", + acis.wireframe_isolines.to_string(), + ), + ro( + "Wires", + "acis_wires", + format!( + "{} (unknown {}, silhouette {}, visible {}, hidden {}, isoline {}; {} points; {} transformed)", + wires.len(), + wire_types[0], + wire_types[1], + wire_types[2], + wire_types[3], + wire_types[4], + points, + transformed + ), + ), + ro( + "View Silhouettes", + "acis_silhouettes", + format!( + "{} views; {} wires", + silhouettes.len(), + silhouettes.iter().map(|silhouette| silhouette.wires.len()).sum::() + ), + ), + ], + }, + ] +} + +fn position_section(prefix: &str, p: &acadrust::types::Vector3) -> PropSection { + let fields = match prefix { + "rgn" => ["rgn_px", "rgn_py", "rgn_pz"], + "bdy" => ["bdy_px", "bdy_py", "bdy_pz"], + "srf" => ["srf_px", "srf_py", "srf_pz"], + _ => ["s3d_px", "s3d_py", "s3d_pz"], + }; + PropSection { + title: "Geometry".into(), + props: vec![ + edit("Position X", fields[0], p.x), + edit("Position Y", fields[1], p.y), + edit("Position Z", fields[2], p.z), + ], } } @@ -97,54 +248,34 @@ impl Grippable for Solid3D { return; } if let GripApply::Translate(d) = apply { - self.point_of_reference.x += d.x as f64; - self.point_of_reference.y += d.y as f64; - self.point_of_reference.z += d.z as f64; - translate_wires(&mut self.wires, d.as_vec3()); + translate_acis_entity(self, d); } } } impl PropertyEditable for Solid3D { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { - let has_history = matches!(self.history_handle, Some(h) if !h.is_null()); - let history = if has_history { - format!("{:X}", self.history_handle.unwrap().value()) - } else { - "None".to_string() - }; - vec![ - PropSection { - title: "Solid History".into(), - props: vec![ - ro("History", "s3d_history", history), - ro( - "Show History", - "s3d_show_history", - if has_history { "Yes" } else { "No" }, - ), - ], - }, - PropSection { - title: "Geometry".into(), - props: vec![ - edit("Position X", "s3d_px", self.point_of_reference.x), - edit("Position Y", "s3d_py", self.point_of_reference.y), - edit("Position Z", "s3d_pz", self.point_of_reference.z), - ], - }, - ] + let mut sections = + acis_sections(&self.acis_data, &self.wires, &self.silhouettes, self.history_handle); + sections[0] + .props + .insert(0, ro("UID", "s3d_uid", self.uid.clone())); + sections.push(position_section("s3d", &self.point_of_reference)); + sections } fn apply_geom_prop(&mut self, field: &str, value: &str) { let Some(v) = parse_f64(value) else { return; }; - match field { - "s3d_px" => self.point_of_reference.x = v, - "s3d_py" => self.point_of_reference.y = v, - "s3d_pz" => self.point_of_reference.z = v, - _ => {} + let delta = match field { + "s3d_px" => acadrust::types::Vector3::new(v - self.point_of_reference.x, 0.0, 0.0), + "s3d_py" => acadrust::types::Vector3::new(0.0, v - self.point_of_reference.y, 0.0), + "s3d_pz" => acadrust::types::Vector3::new(0.0, 0.0, v - self.point_of_reference.z), + _ => return, + }; + if delta != acadrust::types::Vector3::ZERO { + acadrust::Entity::translate(self, delta); } } } @@ -161,10 +292,7 @@ impl Grippable for Region { return; } if let GripApply::Translate(d) = apply { - self.point_of_reference.x += d.x as f64; - self.point_of_reference.y += d.y as f64; - self.point_of_reference.z += d.z as f64; - translate_wires(&mut self.wires, d.as_vec3()); + translate_acis_entity(self, d); } } } @@ -172,16 +300,36 @@ impl Grippable for Region { impl PropertyEditable for Region { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { let (area, perimeter) = region_area_perimeter(&self.wires); - vec![PropSection { - title: "Geometry".into(), - props: vec![ - ro("Area", "rgn_area", format!("{area:.4}")), - ro("Perimeter", "rgn_perimeter", format!("{perimeter:.4}")), - ], - }] + let mut sections = + acis_sections(&self.acis_data, &self.wires, &self.silhouettes, self.history_handle); + sections[0] + .props + .insert(0, ro("UID", "rgn_uid", self.uid.clone())); + let mut geometry = position_section("rgn", &self.point_of_reference); + geometry + .props + .push(ro("Area", "rgn_area", format!("{area:.4}"))); + geometry + .props + .push(ro("Perimeter", "rgn_perimeter", format!("{perimeter:.4}"))); + sections.push(geometry); + sections } - fn apply_geom_prop(&mut self, _field: &str, _value: &str) {} + fn apply_geom_prop(&mut self, field: &str, value: &str) { + let Some(v) = parse_f64(value) else { + return; + }; + let delta = match field { + "rgn_px" => acadrust::types::Vector3::new(v - self.point_of_reference.x, 0.0, 0.0), + "rgn_py" => acadrust::types::Vector3::new(0.0, v - self.point_of_reference.y, 0.0), + "rgn_pz" => acadrust::types::Vector3::new(0.0, 0.0, v - self.point_of_reference.z), + _ => return, + }; + if delta != acadrust::types::Vector3::ZERO { + acadrust::Entity::translate(self, delta); + } + } } // ── Body ────────────────────────────────────────────────────────────────────── @@ -196,36 +344,458 @@ impl Grippable for Body { return; } if let GripApply::Translate(d) = apply { - self.point_of_reference.x += d.x as f64; - self.point_of_reference.y += d.y as f64; - self.point_of_reference.z += d.z as f64; - translate_wires(&mut self.wires, d.as_vec3()); + translate_acis_entity(self, d); } } } impl PropertyEditable for Body { fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { - let has_history = matches!(self.history_handle, Some(h) if !h.is_null()); - let history = if has_history { - format!("{:X}", self.history_handle.unwrap().value()) - } else { - "None".to_string() - }; - vec![PropSection { - title: "Solid History".into(), - props: vec![ - ro("History", "bdy_history", history), - ro( - "Show History", - "bdy_show_history", - if has_history { "Yes" } else { "No" }, - ), - ], - }] + let mut sections = + acis_sections(&self.acis_data, &self.wires, &self.silhouettes, self.history_handle); + sections[0] + .props + .insert(0, ro("UID", "bdy_uid", self.uid.clone())); + sections.push(position_section("bdy", &self.point_of_reference)); + sections } - fn apply_geom_prop(&mut self, _field: &str, _value: &str) {} + fn apply_geom_prop(&mut self, field: &str, value: &str) { + let Some(v) = parse_f64(value) else { + return; + }; + let delta = match field { + "bdy_px" => acadrust::types::Vector3::new(v - self.point_of_reference.x, 0.0, 0.0), + "bdy_py" => acadrust::types::Vector3::new(0.0, v - self.point_of_reference.y, 0.0), + "bdy_pz" => acadrust::types::Vector3::new(0.0, 0.0, v - self.point_of_reference.z), + _ => return, + }; + if delta != acadrust::types::Vector3::ZERO { + acadrust::Entity::translate(self, delta); + } + } +} + +fn embedded_name(entity: Option<&acadrust::entities::EmbeddedEntity>) -> &'static str { + match entity { + 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 vector_text(vector: &acadrust::types::Vector3) -> String { + format!("{:.6}, {:.6}, {:.6}", vector.x, vector.y, vector.z) +} + +fn matrix_text(matrix: &[f64; 16]) -> String { + matrix + .chunks_exact(4) + .map(|row| { + format!( + "[{:.4}, {:.4}, {:.4}, {:.4}]", + row[0], row[1], row[2], row[3] + ) + }) + .collect::>() + .join(" ") +} + +fn sweep_options_text(options: &acadrust::entities::SurfaceSweepOptions) -> String { + format!( + "draft {:.6} ({:.6}→{:.6}); twist {:.6}; scale {:.6}; align {:.6}; solid {}; flags {}/{}; align-start {}; bank {}; base {}; sweep-xform {}; path-xform {}; ref {}", + options.draft_angle, + options.draft_start_distance, + options.draft_end_distance, + options.twist_angle, + options.scale_factor, + options.align_angle, + yes_no(options.is_solid), + options.sweep_alignment_flags, + options.path_flags, + yes_no(options.align_start), + yes_no(options.bank), + yes_no(options.base_point_set), + yes_no(options.sweep_entity_transform_computed), + yes_no(options.path_entity_transform_computed), + vector_text(&options.reference_vector) + ) +} + +fn surface_construction_section(surface: &Surface) -> PropSection { + use acadrust::entities::SurfaceData; + let mut props = vec![ + ro("Kind", "srf_kind", format!("{:?}", surface.kind)), + ro( + "Modeler Format", + "srf_modeler_version", + surface.modeler_format_version.to_string(), + ), + edit( + "U Isolines", + "srf_u_isolines", + surface.u_isolines as f64, + ), + edit( + "V Isolines", + "srf_v_isolines", + surface.v_isolines as f64, + ), + ]; + match &surface.surface_data { + SurfaceData::Generic => {} + SurfaceData::Plane { class_version } => { + props.push(ro( + "Class Version", + "srf_class_version", + class_version.to_string(), + )); + } + SurfaceData::Extruded { + sweep_entity, + options, + sweep_vector, + sweep_transform, + } => { + props.extend([ + ro( + "Sweep Entity", + "srf_sweep_entity", + embedded_name(sweep_entity.as_ref()), + ), + ro("Sweep Vector", "srf_sweep_vector", vector_text(sweep_vector)), + ro("Sweep Options", "srf_sweep_options", sweep_options_text(options)), + ro( + "Sweep Transform", + "srf_sweep_transform", + matrix_text(sweep_transform), + ), + ro( + "Sweep Entity Transform", + "srf_sweep_entity_transform", + matrix_text(&options.sweep_entity_transform), + ), + ro( + "Path Entity Transform", + "srf_path_entity_transform", + matrix_text(&options.path_entity_transform), + ), + ]); + } + SurfaceData::Lofted { + loft_transform, + cross_section_entities, + guide_entities, + path_entity, + plane_normal_lofting_type, + start_draft_angle, + end_draft_angle, + start_draft_magnitude, + end_draft_magnitude, + arc_length_parameterization, + no_twist, + align_direction, + simple_surfaces, + closed_surfaces, + solid, + ruled_surface, + virtual_guide, + cross_sections, + guide_curves, + path_curve, + } => { + props.extend([ + ro( + "Embedded Curves", + "srf_loft_embedded", + format!( + "{} cross sections; {} guides; path {}", + cross_section_entities.len(), + guide_entities.len(), + embedded_name(path_entity.as_ref()) + ), + ), + ro( + "Database Curves", + "srf_loft_handles", + format!( + "{} cross sections; {} guides; path {}", + cross_sections.len(), + guide_curves.len(), + handle_text(*path_curve) + ), + ), + ro( + "Draft", + "srf_loft_draft", + format!( + "type {}; angle {:.6}→{:.6}; magnitude {:.6}→{:.6}", + plane_normal_lofting_type, + start_draft_angle, + end_draft_angle, + start_draft_magnitude, + end_draft_magnitude + ), + ), + ro( + "Loft Flags", + "srf_loft_flags", + format!( + "arc-length {}; no-twist {}; align {}; simple {}; closed {}; solid {}; ruled {}; virtual-guide {}", + yes_no(*arc_length_parameterization), + yes_no(*no_twist), + yes_no(*align_direction), + yes_no(*simple_surfaces), + yes_no(*closed_surfaces), + yes_no(*solid), + yes_no(*ruled_surface), + yes_no(*virtual_guide) + ), + ), + ro( + "Loft Transform", + "srf_loft_transform", + matrix_text(loft_transform), + ), + ]); + } + SurfaceData::Revolved { + revolve_entity, + class_version, + entity_id, + axis_point, + axis_vector, + revolve_angle, + start_angle, + entity_transform, + draft_angle, + draft_start_distance, + draft_end_distance, + twist_angle, + solid, + close_to_axis, + } => { + props.extend([ + ro( + "Revolve Entity", + "srf_revolve_entity", + embedded_name(revolve_entity.as_ref()), + ), + ro( + "Class / Entity", + "srf_revolve_ids", + format!("{class_version} / {entity_id}"), + ), + ro("Axis Point", "srf_axis_point", vector_text(axis_point)), + ro("Axis Vector", "srf_axis_vector", vector_text(axis_vector)), + ro( + "Angles", + "srf_revolve_angles", + format!( + "start {:.6}; revolve {:.6}; draft {:.6}; twist {:.6}", + start_angle, revolve_angle, draft_angle, twist_angle + ), + ), + ro( + "Draft Distances", + "srf_revolve_draft_distances", + format!("{draft_start_distance:.6}→{draft_end_distance:.6}"), + ), + ro( + "Revolve Flags", + "srf_revolve_flags", + format!( + "solid {}; close-to-axis {}", + yes_no(*solid), + yes_no(*close_to_axis) + ), + ), + ro( + "Entity Transform", + "srf_revolve_transform", + matrix_text(entity_transform), + ), + ]); + } + SurfaceData::Swept { + class_version, + sweep_entity, + path_entity, + sweep_transform, + path_transform, + options, + } => { + props.extend([ + ro( + "Class Version", + "srf_class_version", + class_version.to_string(), + ), + ro( + "Sweep / Path", + "srf_swept_entities", + format!( + "{} / {}", + embedded_name(sweep_entity.as_ref()), + embedded_name(path_entity.as_ref()) + ), + ), + ro("Sweep Options", "srf_sweep_options", sweep_options_text(options)), + ro( + "Sweep Transform", + "srf_sweep_transform", + matrix_text(sweep_transform), + ), + ro( + "Path Transform", + "srf_path_transform", + matrix_text(path_transform), + ), + ro( + "Sweep Entity Transform", + "srf_sweep_entity_transform", + matrix_text(&options.sweep_entity_transform), + ), + ro( + "Path Entity Transform", + "srf_path_entity_transform", + matrix_text(&options.path_entity_transform), + ), + ]); + } + SurfaceData::Nurb { + short_170, + cv_hull_display, + u_vector1, + v_vector1, + u_vector2, + v_vector2, + } => { + props.extend([ + ro( + "NURB Version", + "srf_nurb_version", + short_170.to_string(), + ), + ro( + "CV Hull", + "srf_nurb_cv_hull", + yes_no(*cv_hull_display), + ), + ro("U Vector 1", "srf_nurb_u1", vector_text(u_vector1)), + ro("V Vector 1", "srf_nurb_v1", vector_text(v_vector1)), + ro("U Vector 2", "srf_nurb_u2", vector_text(u_vector2)), + ro("V Vector 2", "srf_nurb_v2", vector_text(v_vector2)), + ]); + } + } + props.extend([ + ro( + "Raw DWG Body", + "srf_raw_dwg", + surface.raw_dwg_data.as_ref().map_or_else( + || "None".to_string(), + |bytes| format!("{} bytes; {} handle bits", bytes.len(), surface.dwg_handle_bits), + ), + ), + ro( + "Raw Source Version", + "srf_raw_version", + surface + .dwg_source_version + .map(|version| format!("{version:?}")) + .unwrap_or_else(|| "None".to_string()), + ), + ]); + PropSection { + title: "Surface Construction".into(), + props, + } +} + +fn translate_surface(surface: &mut Surface, delta: acadrust::types::Vector3) { + let before = surface.point_of_reference; + acadrust::Entity::translate(surface, delta); + if surface.point_of_reference == before { + surface.point_of_reference = before + delta; + } +} + +impl Grippable for Surface { + fn grips(&self) -> Vec { + vec![center_grip(0, dvec3(&self.point_of_reference))] + } + + fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { + if grip_id != 0 { + return; + } + if let GripApply::Translate(delta) = apply { + translate_surface( + self, + acadrust::types::Vector3::new(delta.x, delta.y, delta.z), + ); + } + } +} + +impl PropertyEditable for Surface { + fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { + let mut sections = + acis_sections(&self.acis_data, &self.wires, &self.silhouettes, self.history_handle); + sections.push(surface_construction_section(self)); + sections.push(position_section("srf", &self.point_of_reference)); + sections + } + + fn apply_geom_prop(&mut self, field: &str, value: &str) { + match field { + "srf_u_isolines" => { + if let Some(value) = parse_f64(value) { + self.u_isolines = (value.round() as i16).max(0); + } + } + "srf_v_isolines" => { + if let Some(value) = parse_f64(value) { + self.v_isolines = (value.round() as i16).max(0); + } + } + _ => { + let Some(value) = parse_f64(value) else { + return; + }; + let delta = match field { + "srf_px" => acadrust::types::Vector3::new( + value - self.point_of_reference.x, + 0.0, + 0.0, + ), + "srf_py" => acadrust::types::Vector3::new( + 0.0, + value - self.point_of_reference.y, + 0.0, + ), + "srf_pz" => acadrust::types::Vector3::new( + 0.0, + 0.0, + value - self.point_of_reference.z, + ), + _ => return, + }; + if delta != acadrust::types::Vector3::ZERO { + translate_surface(self, delta); + } + } + } + } } // ── Accessors for the Solid3D / Region / Body trio ───────────────────────── @@ -246,6 +816,7 @@ pub fn point_of_reference(e: &EntityType) -> Option<&Vector3> { EntityType::Solid3D(s) => Some(&s.point_of_reference), EntityType::Region(r) => Some(&r.point_of_reference), EntityType::Body(b) => Some(&b.point_of_reference), + EntityType::Surface(s) => Some(&s.point_of_reference), _ => None, } } @@ -263,6 +834,32 @@ pub fn fallback_wires(e: &EntityType) -> Option<&[acadrust::entities::Wire]> { } } +pub fn wire_point( + wire: &acadrust::entities::Wire, + point: &acadrust::types::Vector3, +) -> acadrust::types::Vector3 { + if !wire.has_transform { + return *point; + } + let x = point.x * wire.scale.x; + let y = point.y * wire.scale.y; + let z = point.z * wire.scale.z; + acadrust::types::Vector3::new( + wire.translation.x + + wire.x_axis.x * x + + wire.y_axis.x * y + + wire.z_axis.x * z, + wire.translation.y + + wire.x_axis.y * x + + wire.y_axis.y * y + + wire.z_axis.y * z, + wire.translation.z + + wire.x_axis.z * x + + wire.y_axis.z * y + + wire.z_axis.z * z, + ) +} + /// Whether every ACIS face uses a surface family the mesh pipeline can decode. /// Unsupported or unresolved faces must keep their display-cache wires visible; /// otherwise a parseable but incomplete shell looks like a valid solid. @@ -288,13 +885,15 @@ pub fn acis_has_complete_surface_support(e: &EntityType) -> bool { | "sphere-surface" | "torus-surface" | "spline-surface" + | "meshsurf-surface" + | "bs3-surface" ) }) }) } -/// Run the appropriate `solid3d_tess::tessellate_*` for the entity, -/// returning `None` for non-volume entities or when the kernel fails. +/// Build material-aware shaded geometry for every standard 3-D solid/surface +/// and mesh family, returning `None` when decoded geometry is unusable. pub fn tessellate_volume( e: &EntityType, color: [f32; 4], @@ -306,6 +905,9 @@ pub fn tessellate_volume( EntityType::Region(r) => solid3d_tess::tessellate_region(r, color, facet_res, isolines), EntityType::Body(b) => solid3d_tess::tessellate_body(b, color, facet_res, isolines), EntityType::Surface(s) => solid3d_tess::tessellate_surface(s, color, facet_res, isolines), + EntityType::Mesh(_) | EntityType::PolygonMesh(_) | EntityType::PolyfaceMesh(_) => { + crate::entities::mesh::tessellate_shaded_mesh(e, color) + } _ => None, } } diff --git a/src/entities/table.rs b/src/entities/table.rs index 60cbe0283..1ca4b0049 100644 --- a/src/entities/table.rs +++ b/src/entities/table.rs @@ -16,6 +16,692 @@ fn v3(v: &acadrust::types::Vector3) -> Vec3 { Vec3::new(v.x as f32, v.y as f32, v.z as f32) } +fn table_axes(table: &Table) -> (Vec3, Vec3) { + let h = v3(&table.horizontal_direction).normalize_or(Vec3::X); + let normal = v3(&table.normal).normalize_or(Vec3::Z); + let down = h.cross(normal).normalize_or(Vec3::NEG_Y); + (h, down) +} + +fn merged_owner_and_span( + table: &Table, + row: usize, + column: usize, +) -> Option<(usize, usize, usize, usize)> { + for range in &table.merged_ranges { + if range.contains(row, column) { + return (row == range.top_row && column == range.left_col).then_some(( + range.top_row, + range.left_col, + range.bottom_row.min(table.rows.len().saturating_sub(1)), + range.right_col.min(table.columns.len().saturating_sub(1)), + )); + } + } + + for (owner_row, table_row) in table.rows.iter().enumerate() { + for (owner_column, cell) in table_row.cells.iter().enumerate() { + let row_end = owner_row + .saturating_add(cell.merge_height.max(1) as usize - 1) + .min(table.rows.len().saturating_sub(1)); + let column_end = owner_column + .saturating_add(cell.merge_width.max(1) as usize - 1) + .min(table.columns.len().saturating_sub(1)); + if row >= owner_row + && row <= row_end + && column >= owner_column + && column <= column_end + { + return (row == owner_row && column == owner_column).then_some(( + owner_row, + owner_column, + row_end, + column_end, + )); + } + } + } + + Some((row, column, row, column)) +} + +fn style_for_property<'a>( + table: &'a Table, + row: &'a acadrust::entities::table::TableRow, + column: usize, + cell: &'a acadrust::entities::table::TableCell, + property: acadrust::entities::table::CellStylePropertyFlags, +) -> Option<&'a acadrust::entities::table::CellStyle> { + let column_style = table + .columns + .get(column) + .and_then(|column| column.style.as_ref()); + for style in [cell.style.as_ref(), row.style.as_ref(), column_style] + .into_iter() + .flatten() + { + if style.property_flags.contains(property) { + return Some(style); + } + } + table.base_style.as_ref().or_else(|| { + [cell.style.as_ref(), row.style.as_ref(), column_style] + .into_iter() + .flatten() + .next() + }) +} + +fn table_offsets(table: &Table, scale: f32) -> (Vec, Vec) { + let mut columns = Vec::with_capacity(table.columns.len() + 1); + columns.push(0.0); + for column in &table.columns { + columns.push(columns.last().copied().unwrap_or(0.0) + column.width as f32 * scale); + } + + let mut rows = Vec::with_capacity(table.rows.len() + 1); + rows.push(0.0); + for row in &table.rows { + rows.push(rows.last().copied().unwrap_or(0.0) + row.height as f32 * scale); + } + (columns, rows) +} + +fn break_frame_for_row( + table: &Table, + row: usize, + h: Vec3, + down: Vec3, + row_offsets: &[f32], + scale: f32, +) -> (Vec3, f32) { + use acadrust::entities::table::BreakOptionFlags; + + let insertion = v3(&table.insertion_point); + let offset_to_world = + |offset: &acadrust::types::Vector3| insertion + v3(offset); + if !table.break_options.contains(BreakOptionFlags::ENABLE_BREAKS) { + return (insertion, row_offsets.get(row).copied().unwrap_or(0.0)); + } + + if let Some(range) = table + .break_ranges + .iter() + .find(|range| row as i32 >= range.start_row && row as i32 <= range.end_row) + { + let start = range.start_row.max(0) as usize; + let position = offset_to_world(&range.position); + let origin = if position.is_finite() { + position + } else { + insertion + }; + let top = row_offsets.get(row).copied().unwrap_or(0.0) + - row_offsets.get(start).copied().unwrap_or(0.0); + return (origin, top); + } + + let mut start_row = 0usize; + let mut segment = 0usize; + while start_row < table.rows.len() { + let manual_heights = table + .break_options + .contains(BreakOptionFlags::ALLOW_MANUAL_HEIGHTS); + let max_height = table + .break_data + .get(if manual_heights { segment } else { 0 }) + .map(|data| data.height as f32 * scale) + .filter(|height| *height > 1e-6) + .unwrap_or(f32::INFINITY); + let start_offset = row_offsets.get(start_row).copied().unwrap_or(0.0); + let mut end_row = start_row; + while end_row + 1 < table.rows.len() + && row_offsets[end_row + 2] - start_offset <= max_height + { + end_row += 1; + } + if row <= end_row { + let manual_positions = table + .break_options + .contains(BreakOptionFlags::ALLOW_MANUAL_POSITIONS); + let manual_origin = manual_positions + .then(|| table.break_data.get(segment)) + .flatten() + .map(|data| offset_to_world(&data.position)) + .filter(|position| position.is_finite()); + let origin = manual_origin.unwrap_or_else(|| { + let spacing = table.break_spacing as f32 * scale; + let horizontal_step = + table.total_width() as f32 * scale + spacing; + let vertical_step = if max_height.is_finite() { + max_height + spacing + } else { + table.total_height() as f32 * scale + spacing + }; + match table.break_flow_direction { + acadrust::entities::table::BreakFlowDirection::Left => { + insertion - h * segment as f32 * horizontal_step + } + acadrust::entities::table::BreakFlowDirection::Vertical => { + insertion + down * segment as f32 * vertical_step + } + acadrust::entities::table::BreakFlowDirection::Right => { + insertion + h * segment as f32 * horizontal_step + } + } + }); + return ( + origin, + row_offsets.get(row).copied().unwrap_or(0.0) - start_offset, + ); + } + start_row = end_row.saturating_add(1); + segment = segment.saturating_add(1); + } + + (insertion, row_offsets.get(row).copied().unwrap_or(0.0)) +} + +fn format_cell_value(value: &acadrust::entities::table::CellValue) -> String { + let display = value.display(); + if !display.is_empty() { + return display.to_string(); + } + use acadrust::entities::table::CellValueType; + match value.value_type { + CellValueType::Long => format!("{}", value.numeric_value as i64), + CellValueType::Double | CellValueType::Date => format!("{}", value.numeric_value), + CellValueType::Point2D => { + format!("{}, {}", value.point_value.x, value.point_value.y) + } + CellValueType::Point3D => format!( + "{}, {}, {}", + value.point_value.x, value.point_value.y, value.point_value.z + ), + CellValueType::Handle => value + .handle_value + .map(|handle| format!("{:X}", handle.value())) + .unwrap_or_default(), + _ => String::new(), + } +} + +fn fallback_content_centers( + bounds: [f32; 4], + sizes: &[(f32, f32)], + layout: acadrust::entities::table::ContentLayoutFlags, + alignment: i32, + horizontal_spacing: f32, + vertical_spacing: f32, +) -> Vec<(f32, f32)> { + if sizes.is_empty() { + return Vec::new(); + } + let [left, top, right, bottom] = bounds; + let inner_width = (right - left).max(0.0); + let horiz = ((alignment - 1).rem_euclid(3)) + 1; + let vert = ((alignment - 1) / 3) + 1; + let mut rows: Vec> = Vec::new(); + if layout.contains( + acadrust::entities::table::ContentLayoutFlags::STACKED_VERTICAL, + ) { + for index in 0..sizes.len() { + rows.push(vec![index]); + } + } else if layout.contains( + acadrust::entities::table::ContentLayoutFlags::STACKED_HORIZONTAL, + ) { + rows.push((0..sizes.len()).collect()); + } else { + let mut row = Vec::new(); + let mut width = 0.0f32; + for (index, (item_width, _)) in sizes.iter().copied().enumerate() { + let next = if row.is_empty() { + item_width + } else { + width + horizontal_spacing + item_width + }; + if !row.is_empty() && next > inner_width { + rows.push(std::mem::take(&mut row)); + width = item_width; + } else { + width = next; + } + row.push(index); + } + if !row.is_empty() { + rows.push(row); + } + } + + let row_heights: Vec = rows + .iter() + .map(|row| { + row.iter() + .map(|&index| sizes[index].1) + .fold(0.0f32, f32::max) + }) + .collect(); + let total_height = row_heights.iter().sum::() + + vertical_spacing * rows.len().saturating_sub(1) as f32; + let mut y = match vert { + 1 => top, + 3 => bottom - total_height, + _ => (top + bottom - total_height) * 0.5, + }; + let mut result = vec![(0.0f32, 0.0f32); sizes.len()]; + for (row_index, row) in rows.iter().enumerate() { + let row_width = row.iter().map(|&index| sizes[index].0).sum::() + + horizontal_spacing * row.len().saturating_sub(1) as f32; + let mut x = match horiz { + 1 => left, + 3 => right - row_width, + _ => (left + right - row_width) * 0.5, + }; + for &index in row { + let (width, height) = sizes[index]; + result[index] = + (x + width * 0.5, y + (row_heights[row_index] - height) * 0.5 + height * 0.5); + x += width + horizontal_spacing; + } + y += row_heights[row_index] + vertical_spacing; + } + result +} + +fn content_display_value( + document: &acadrust::CadDocument, + table_handle: acadrust::Handle, + content: &acadrust::entities::table::CellContent, +) -> String { + if let Some(handle) = content.field_handle { + if let Some(value) = + crate::entities::field::resolve_handle(document, handle, table_handle) + { + return value; + } + if let Some(acadrust::objects::ObjectType::Field(field)) = document.objects.get(&handle) { + if !field.value_string.is_empty() { + return field.value_string.clone(); + } + let value = format_cell_value(&field.value); + if !value.is_empty() { + return value; + } + } + } + format_cell_value(&content.value) +} + +fn resolved_content_geometry( + document: &acadrust::CadDocument, + table: &Table, + row: usize, + column: usize, + cell: &acadrust::entities::table::TableCell, + content_index: usize, +) -> Option { + if let Some(geometry) = cell + .contents + .get(content_index) + .and_then(|content| content.geometry.clone()) + .or_else(|| cell.geometries.get(content_index).cloned()) + .or_else(|| (content_index == 0).then(|| cell.geometry.clone()).flatten()) + { + return Some(geometry); + } + + let handle = cell.geometry_handle?; + let flat_index = row + .saturating_mul(table.columns.len()) + .saturating_add(column); + if let Some(acadrust::objects::ObjectType::DataObject(object)) = + document.objects.get(&handle) + { + if let acadrust::objects::DataObjectData::TableGeometry(geometry) = + &object.data + { + return geometry + .cells + .get(flat_index) + .and_then(|cell| cell.geometry.get(content_index)) + .cloned(); + } + } + document.objects.values().find_map(|object| { + let acadrust::objects::ObjectType::DataObject(object) = object else { + return None; + }; + let acadrust::objects::DataObjectData::TableGeometry(geometry) = + &object.data + else { + return None; + }; + geometry + .cells + .iter() + .find(|geometry_cell| geometry_cell.table_geometry == handle) + .and_then(|geometry_cell| geometry_cell.geometry.get(content_index)) + .cloned() + }) +} + +pub(crate) fn block_cell_inserts( + table: &Table, + document: &acadrust::CadDocument, + anno_scale: f32, +) -> Vec { + use acadrust::entities::table::ContentLayoutFlags; + use acadrust::entities::{AttributeEntity, EntityType, Insert}; + use acadrust::types::Vector3; + + if table.rows.is_empty() || table.columns.is_empty() { + return Vec::new(); + } + let (h, down) = table_axes(table); + let table_style = table.table_style_handle.and_then(|handle| { + document.objects.get(&handle).and_then(|object| match object { + acadrust::objects::ObjectType::TableStyle(style) => Some(style), + _ => None, + }) + }); + let flow = if matches!( + table_style.map(|style| style.flow_direction), + Some(acadrust::objects::TableFlowDirection::Up) + ) { + -down + } else { + down + }; + let (column_offsets, row_offsets) = table_offsets(table, anno_scale); + let table_rotation = table + .horizontal_direction + .y + .atan2(table.horizontal_direction.x); + let mut inserts = Vec::new(); + + for (row_index, row) in table.rows.iter().enumerate() { + for (column_index, cell) in row.cells.iter().enumerate() { + let Some((_, _, row_end, column_end)) = + merged_owner_and_span(table, row_index, column_index) + else { + continue; + }; + let block_contents: Vec<_> = cell + .contents + .iter() + .enumerate() + .filter(|(_, content)| content.block_handle.is_some()) + .collect(); + if block_contents.is_empty() { + continue; + } + let (origin, row_top) = break_frame_for_row( + table, + row_index, + h, + flow, + &row_offsets, + anno_scale, + ); + let row_bottom = row_top + + row_offsets + .get(row_end + 1) + .copied() + .unwrap_or(row_offsets[row_index]) + - row_offsets[row_index]; + let column_left = column_offsets[column_index]; + let column_right = column_offsets + .get(column_end + 1) + .copied() + .unwrap_or(column_left); + let layout_style = style_for_property( + table, + row, + column_index, + cell, + acadrust::entities::table::CellStylePropertyFlags::CONTENT_LAYOUT, + ); + let layout = layout_style + .map(|style| style.layout_flags) + .unwrap_or(ContentLayoutFlags::FLOW); + let count = block_contents.len() as f32; + + for (slot_index, (content_index, content)) in + block_contents.into_iter().enumerate() + { + let Some(block_handle) = content.block_handle else { + continue; + }; + let Some(record) = document + .block_records + .iter() + .find(|record| record.handle == block_handle) + else { + continue; + }; + let mut x = (column_left + column_right) * 0.5; + let mut y = (row_top + row_bottom) * 0.5; + let mut z = 0.0f32; + if let Some(geometry) = resolved_content_geometry( + document, + table, + row_index, + column_index, + cell, + content_index, + ) { + x = column_left + + geometry.distance_to_center.x as f32 * anno_scale; + y = row_top + - geometry.distance_to_center.y as f32 * anno_scale; + z = geometry.distance_to_center.z as f32 * anno_scale; + } else if count > 1.0 { + let index = slot_index as f32; + if layout.contains(ContentLayoutFlags::STACKED_VERTICAL) { + y = row_top + (index + 0.5) * (row_bottom - row_top) / count; + } else { + x = column_left + (index + 0.5) * (column_right - column_left) / count; + } + } + let position = + origin + h * x + flow * y + v3(&table.normal).normalize_or(Vec3::Z) * z; + let scale_style = style_for_property( + table, + row, + column_index, + cell, + acadrust::entities::table::CellStylePropertyFlags::BLOCK_SCALE, + ); + let style_scale = scale_style + .map(|style| style.scale) + .filter(|scale| scale.abs() > 1e-9) + .unwrap_or(1.0); + let content_scale = if content.format_property_flags + & acadrust::entities::table::CellStylePropertyFlags::BLOCK_SCALE.bits() as i32 + != 0 + && content.scale.abs() > 1e-9 + { + content.scale + } else if cell.block_scale.abs() > 1e-9 { + cell.block_scale + } else { + 1.0 + }; + let mut scale = if content.format_property_flags + & acadrust::entities::table::CellStylePropertyFlags::BLOCK_SCALE.bits() as i32 + != 0 + { + content_scale + } else { + style_scale.max(content_scale) + } * anno_scale as f64; + let auto_scale = cell.auto_fit + || style_for_property( + table, + row, + column_index, + cell, + acadrust::entities::table::CellStylePropertyFlags::AUTO_SCALE, + ) + .is_some_and(|style| { + style + .property_flags + .contains(acadrust::entities::table::CellStylePropertyFlags::AUTO_SCALE) + }); + let mut block_min = + Vector3::new(f64::MAX, f64::MAX, f64::MAX); + let mut block_max = + Vector3::new(f64::MIN, f64::MIN, f64::MIN); + let mut has_block_bounds = false; + for &handle in &record.entity_handles { + let Some(entity) = document.get_entity(handle) else { + continue; + }; + let bounds = entity.as_entity().bounding_box(); + if bounds.min.x.is_finite() + && bounds.min.y.is_finite() + && bounds.max.x.is_finite() + && bounds.max.y.is_finite() + { + block_min.x = block_min.x.min(bounds.min.x); + block_min.y = block_min.y.min(bounds.min.y); + block_min.z = block_min.z.min(bounds.min.z); + block_max.x = block_max.x.max(bounds.max.x); + block_max.y = block_max.y.max(bounds.max.y); + block_max.z = block_max.z.max(bounds.max.z); + has_block_bounds = true; + } + } + if auto_scale && has_block_bounds { + let min = block_min; + let max = block_max; + let width = (max.x - min.x).abs(); + let height = (max.y - min.y).abs(); + let margin_left = style_for_property( + table, + row, + column_index, + cell, + acadrust::entities::table::CellStylePropertyFlags::MARGIN_LEFT, + ) + .map(|style| style.margin_left) + .unwrap_or(0.0); + let margin_right = style_for_property( + table, + row, + column_index, + cell, + acadrust::entities::table::CellStylePropertyFlags::MARGIN_RIGHT, + ) + .map(|style| style.margin_right) + .unwrap_or(0.0); + let margin_top = style_for_property( + table, + row, + column_index, + cell, + acadrust::entities::table::CellStylePropertyFlags::MARGIN_TOP, + ) + .map(|style| style.margin_top) + .unwrap_or(0.0); + let margin_bottom = style_for_property( + table, + row, + column_index, + cell, + acadrust::entities::table::CellStylePropertyFlags::MARGIN_BOTTOM, + ) + .map(|style| style.margin_bottom) + .unwrap_or(0.0); + let fit_x = ((column_right - column_left) as f64 + - (margin_left + margin_right) * anno_scale as f64) + .max(0.0) + / width.max(1e-9); + let fit_y = ((row_bottom - row_top) as f64 + - (margin_top + margin_bottom) * anno_scale as f64) + .max(0.0) + / height.max(1e-9); + scale *= fit_x.min(fit_y); + } + let rotation_style = style_for_property( + table, + row, + column_index, + cell, + acadrust::entities::table::CellStylePropertyFlags::ROTATION, + ); + let content_rotation_explicit = content.format_property_flags + & acadrust::entities::table::CellStylePropertyFlags::ROTATION.bits() as i32 + != 0; + let content_rotation = if content_rotation_explicit { + content.rotation + } else { + rotation_style + .map(|style| style.rotation) + .unwrap_or(cell.rotation) + }; + let rotation = table_rotation + content_rotation; + let mut insert = Insert::new( + record.name.clone(), + Vector3::new(position.x as f64, position.y as f64, position.z as f64), + ); + insert.common = table.common.clone(); + insert.normal = table.normal; + insert.rotation = rotation; + insert.set_x_scale(scale); + insert.set_y_scale(scale); + insert.set_z_scale(scale); + let transform = insert.get_transform(); + let local_anchor = if has_block_bounds { + (block_min + block_max) * 0.5 + } else { + record.base_point + }; + let transformed_base = transform.apply(local_anchor); + let transformed_zero = transform.apply(Vector3::ZERO); + insert.insert_point = insert.insert_point + - (transformed_base - transformed_zero); + let attribute_definitions: Vec<_> = record + .entity_handles + .iter() + .filter_map(|handle| match document.get_entity(*handle) { + Some(EntityType::AttributeDefinition(definition)) => { + Some(definition) + } + _ => None, + }) + .collect(); + for attribute in &content.attributes { + let definition = match document + .get_entity(attribute.definition_handle) + { + Some(EntityType::AttributeDefinition(definition)) => { + Some(definition) + } + _ => attribute_definitions + .get(attribute.index.max(0) as usize) + .copied(), + }; + let Some(definition) = definition else { + continue; + }; + let mut entity = + AttributeEntity::from_definition(definition, Some(attribute.value.clone())); + acadrust::Entity::apply_transform( + &mut entity, + &insert.get_transform(), + ); + entity.common.handle = table.common.handle; + insert.attributes.push(entity); + } + inserts.push(insert); + } + } + } + inserts +} + impl TruckConvertible for Table { fn to_truck(&self, document: &acadrust::CadDocument) -> Option { if self.rows.is_empty() || self.columns.is_empty() { @@ -33,14 +719,7 @@ impl TruckConvertible for Table { self.insertion_point.z, ]; let origin = Vec3::ZERO; - let h_raw = v3(&self.horizontal_direction); - let h = if h_raw.length_squared() > 1e-10 { - h_raw.normalize() - } else { - Vec3::X - }; - // Perpendicular "down" direction in the drawing plane (tables grow downward) - let v_down = Vec3::new(h.y, -h.x, 0.0); + let (h, v_down) = table_axes(self); let col_offsets: Vec = { let mut off = 0.0f32; @@ -412,14 +1091,7 @@ pub fn tessellate_table( } }; - let origin = v3(&tab.insertion_point); - let h_raw = v3(&tab.horizontal_direction); - let h = if h_raw.length_squared() > 1e-10 { - h_raw.normalize() - } else { - Vec3::X - }; - let v_down = Vec3::new(h.y, -h.x, 0.0); + let (h, v_down) = table_axes(tab); // Flow direction: `Up` stacks rows upward instead of downward. let table_style: Option<&acadrust::objects::TableStyle> = tab.table_style_handle.and_then(|h| { @@ -434,24 +1106,7 @@ pub fn tessellate_table( ); let v_flow = if flow_up { -v_down } else { v_down }; - let col_offsets: Vec = { - let mut off = 0.0f32; - let mut v = vec![0.0f32]; - for col in &tab.columns { - off += col.width as f32 * anno_scale; - v.push(off); - } - v - }; - let row_offsets: Vec = { - let mut off = 0.0f32; - let mut v = vec![0.0f32]; - for row in &tab.rows { - off += row.height as f32 * anno_scale; - v.push(off); - } - v - }; + let (col_offsets, row_offsets) = table_offsets(tab, anno_scale); let title_suppressed = table_style.map(|t| t.title_suppressed).unwrap_or(false); let header_suppressed = table_style.map(|t| t.header_suppressed).unwrap_or(false); @@ -503,17 +1158,20 @@ pub fn tessellate_table( // cells; emitted as one text-carrying wire at the end. let mut text_verts: Vec = Vec::new(); let mut borders: HashMap<([u8; 4], u32), ([f32; 4], f32, Vec<[f32; 3]>)> = HashMap::default(); - let mut emitted: rustc_hash::FxHashSet<(i32, i32, i32, i32)> = rustc_hash::FxHashSet::default(); + let mut emitted: rustc_hash::FxHashSet<(i32, i32, i32, i32, i32, i32)> = + rustc_hash::FxHashSet::default(); let sel_col = WireModel::SELECTED; let mut add_edge = |a: Vec3, b: Vec3, col: [f32; 4], lw: f32| { let k = ( (a.x * 1000.0) as i32, (a.y * 1000.0) as i32, + (a.z * 1000.0) as i32, (b.x * 1000.0) as i32, (b.y * 1000.0) as i32, + (b.z * 1000.0) as i32, ); - let kr = (k.2, k.3, k.0, k.1); + let kr = (k.3, k.4, k.5, k.0, k.1, k.2); if emitted.contains(&k) || emitted.contains(&kr) { return; } @@ -528,13 +1186,8 @@ pub fn tessellate_table( entry.2.push(rel(b)); }; + let normal = v3(&tab.normal).normalize_or(Vec3::Z); for (ri, row) in tab.rows.iter().enumerate() { - let row_top = row_offsets[ri]; - let row_bot = row_offsets - .get(ri + 1) - .copied() - .unwrap_or(row_top + row.height as f32 * anno_scale); - let row_mid = (row_top + row_bot) * 0.5; let row_style: Option<&acadrust::objects::RowCellStyle> = table_style.map(|ts| { let kind = match (title_suppressed, header_suppressed, ri) { (false, _, 0) => 0, @@ -550,16 +1203,46 @@ pub fn tessellate_table( }); for (ci, cell) in row.cells.iter().enumerate() { + let Some((_, _, row_end, column_end)) = + merged_owner_and_span(tab, ri, ci) + else { + continue; + }; + let (origin, row_top) = + break_frame_for_row(tab, ri, h, v_flow, &row_offsets, anno_scale); + let merged_height = row_offsets + .get(row_end + 1) + .copied() + .unwrap_or(row_offsets[ri]) + - row_offsets[ri]; + let row_bot = row_top + merged_height; + let row_mid = (row_top + row_bot) * 0.5; let col_left = col_offsets[ci]; - let col_width = tab.columns.get(ci).map(|c| c.width as f32).unwrap_or(1.0) * anno_scale; - let col_right = col_left + col_width; + let col_right = col_offsets + .get(column_end + 1) + .copied() + .unwrap_or(col_left); + let col_width = col_right - col_left; let tl = origin + h * col_left + v_flow * row_top; let tr = origin + h * col_right + v_flow * row_top; let br_ = origin + h * col_right + v_flow * row_bot; let bl = origin + h * col_left + v_flow * row_bot; + let cell_style = cell + .style + .as_ref() + .or(row.style.as_ref()) + .or_else(|| tab.columns.get(ci).and_then(|column| column.style.as_ref())) + .or(tab.base_style.as_ref()); // ── Fill ────────────────────────────────────────────────────── - let (fill_on, fill_color) = if let Some(cs) = &cell.style { + let fill_style = style_for_property( + tab, + row, + ci, + cell, + acadrust::entities::table::CellStylePropertyFlags::BACKGROUND_COLOR, + ); + let (fill_on, fill_color) = if let Some(cs) = fill_style { (cs.fill_enabled, cs.background_color) } else if let Some(rs) = row_style { (rs.fill_enabled, rs.fill_color) @@ -579,9 +1262,8 @@ pub fn tessellate_table( // ── Borders (per edge: cell override → row style → default) ─── // (top, right, bottom, left) - let cell_b = cell.style.as_ref(); let edge = |which: u8| -> (bool, [f32; 4], f32) { - if let Some(cs) = cell_b { + if let Some(cs) = cell_style { let b = match which { 0 => &cs.top_border, 1 => &cs.right_border, @@ -638,112 +1320,343 @@ pub fn tessellate_table( add_edge(tl, bl, lc, lw); } - // ── Text ────────────────────────────────────────────────────── - let text = cell.text_value(); - if text.is_empty() { - continue; - } - let content = cell.contents.first(); - let cell_h = content - .map(|c| c.text_height) - .filter(|h| *h > 1e-6) - .or_else(|| { - cell.style - .as_ref() - .map(|s| s.text_height) - .filter(|h| *h > 1e-6) + let value_contents: Vec<_> = cell + .contents + .iter() + .enumerate() + .filter_map(|(index, content)| { + let text = + content_display_value(document, tab.common.handle, content); + (!text.is_empty()).then_some((index, content, text)) }) - .or_else(|| row_style.map(|s| s.text_height).filter(|h| *h > 1e-6)) - .map(|h| h as f32) - .unwrap_or(0.18) * anno_scale; - let m_h = if h_margin > 1e-6 { - h_margin - } else { - cell_h * 0.5 - }; - let m_v = if v_margin > 1e-6 { - v_margin - } else { - cell_h * 0.5 - }; - - let style_handle = content - .and_then(|c| c.text_style_handle) - .or_else(|| cell.style.as_ref().and_then(|s| s.text_style_handle)) - .or_else(|| row_style.and_then(|s| s.text_style_handle)); - let font_owned = font_for_handle(style_handle).unwrap_or_else(|| "txt".to_string()); - let resolved = resolved_style_for_handle(style_handle, font_owned); - - let align = cell - .style - .as_ref() - .map(|s| s.alignment) - .filter(|a| *a != 0) - .or_else(|| row_style.map(|s| s.alignment as i32)) - .unwrap_or(5); - let horiz = ((align - 1).rem_euclid(3)) + 1; - let vert = ((align - 1) / 3) + 1; - let (x_offset, attach_h_anchor) = match horiz { - 1 => (col_left + m_h, 0.0_f32), - 3 => (col_right - m_h, 1.0_f32), - _ => (col_left + col_width * 0.5, 0.5_f32), - }; - let (y_offset, v_anchor) = match vert { - 1 => (row_top + m_v, MTextVAnchor::Top), - 3 => (row_bot - m_v, MTextVAnchor::Bottom), - _ => (row_mid, MTextVAnchor::Middle), - }; - let to = origin + h * x_offset + v_flow * y_offset; - let rot = content.map(|c| c.rotation as f32).unwrap_or(0.0) + cell.rotation as f32; - let layout = layout_mtext(&MTextRenderOpts { - // Not an MTEXT: text in a fixed box, never columnar. - columns: Default::default(), - value: text, - insertion: [(to.x as f64), (to.y as f64), (to.z as f64)], - height: cell_h, - rect_w: 0.0, - rotation: rot, - style: &resolved, - attach_h_anchor, - v_anchor, - line_spacing_factor: 1.0, - vertical_text: false, - want_glyph_boxes: false, - }); - - let tcol = if selected { - sel_col - } else if let Some(cs) = &cell.style { - resolve_col(&cs.content_color, entity_color) - } else if let Some(rs) = row_style { - resolve_col(&rs.text_color, entity_color) - } else { - entity_color - }; - // SDF glyph quads per run at its placed origin (rotation baked in by - // layout_glyph_quads), coloured by the cell's text colour. - if let Ok(mut atlas) = crate::scene::text::sdf_atlas::text_atlas().lock() { - for ts in &layout.strokes { - let Some(run) = &ts.run else { continue }; - let quads = crate::scene::text::glyph_quads::layout_glyph_quads( - &mut atlas, - run.height, - run.rotation, - run.width_factor, - run.oblique, - run.tracking, - &run.font, - run.bold, - &run.text, - ); - crate::scene::pipeline::text_gpu::push_glyph_vertices( - &mut text_verts, - &quads, - [ts.origin[0], ts.origin[1], to.z as f64], - 1.0, - tcol, - 0.0, - ); + .collect(); + let fallback_text_height = style_for_property( + tab, + row, + ci, + cell, + acadrust::entities::table::CellStylePropertyFlags::TEXT_HEIGHT, + ) + .map(|style| style.text_height as f32) + .or_else(|| row_style.map(|style| style.text_height as f32)) + .filter(|height| *height > 1e-6) + .unwrap_or(0.18) + * anno_scale; + let fallback_margin_left = style_for_property( + tab, + row, + ci, + cell, + acadrust::entities::table::CellStylePropertyFlags::MARGIN_LEFT, + ) + .map(|style| style.margin_left as f32 * anno_scale) + .unwrap_or(h_margin.max(fallback_text_height * 0.5)); + let fallback_margin_right = style_for_property( + tab, + row, + ci, + cell, + acadrust::entities::table::CellStylePropertyFlags::MARGIN_RIGHT, + ) + .map(|style| style.margin_right as f32 * anno_scale) + .unwrap_or(h_margin.max(fallback_text_height * 0.5)); + let fallback_margin_top = style_for_property( + tab, + row, + ci, + cell, + acadrust::entities::table::CellStylePropertyFlags::MARGIN_TOP, + ) + .map(|style| style.margin_top as f32 * anno_scale) + .unwrap_or(v_margin.max(fallback_text_height * 0.5)); + let fallback_margin_bottom = style_for_property( + tab, + row, + ci, + cell, + acadrust::entities::table::CellStylePropertyFlags::MARGIN_BOTTOM, + ) + .map(|style| style.margin_bottom as f32 * anno_scale) + .unwrap_or(v_margin.max(fallback_text_height * 0.5)); + let fallback_layout = style_for_property( + tab, + row, + ci, + cell, + acadrust::entities::table::CellStylePropertyFlags::CONTENT_LAYOUT, + ) + .map(|style| style.layout_flags) + .unwrap_or(acadrust::entities::table::ContentLayoutFlags::FLOW); + let fallback_alignment = style_for_property( + tab, + row, + ci, + cell, + acadrust::entities::table::CellStylePropertyFlags::ALIGNMENT, + ) + .map(|style| style.alignment) + .or_else(|| row_style.map(|style| style.alignment as i32)) + .unwrap_or(5); + let fallback_h_spacing = style_for_property( + tab, + row, + ci, + cell, + acadrust::entities::table::CellStylePropertyFlags::MARGIN_HORIZONTAL_SPACING, + ) + .map(|style| style.horizontal_spacing as f32 * anno_scale) + .unwrap_or(0.0); + let fallback_v_spacing = style_for_property( + tab, + row, + ci, + cell, + acadrust::entities::table::CellStylePropertyFlags::MARGIN_VERTICAL_SPACING, + ) + .map(|style| style.vertical_spacing as f32 * anno_scale) + .unwrap_or(0.0); + let fallback_sizes: Vec<_> = value_contents + .iter() + .map(|(_, content, text)| { + let height = if content.text_height > 1e-6 { + content.text_height as f32 * anno_scale + } else { + fallback_text_height + }; + let mut max_chars = 0usize; + let mut line_count = 0usize; + for line in text.split("\\P") { + max_chars = max_chars.max(line.chars().count()); + line_count += 1; + } + ( + (max_chars as f32 * height * 0.6).max(height * 0.5), + line_count.max(1) as f32 * height * 1.2, + ) + }) + .collect(); + let fallback_centers = fallback_content_centers( + [ + col_left + fallback_margin_left, + row_top + fallback_margin_top, + col_right - fallback_margin_right, + row_bot - fallback_margin_bottom, + ], + &fallback_sizes, + fallback_layout, + fallback_alignment, + fallback_h_spacing, + fallback_v_spacing, + ); + for (slot_index, (content_index, content, text)) in + value_contents.iter().enumerate() + { + let text_height_style = style_for_property( + tab, + row, + ci, + cell, + acadrust::entities::table::CellStylePropertyFlags::TEXT_HEIGHT, + ); + let cell_h = (content.text_height > 1e-6) + .then_some(content.text_height) + .or_else(|| { + text_height_style + .map(|style| style.text_height) + .filter(|height| *height > 1e-6) + }) + .or_else(|| { + row_style + .map(|style| style.text_height) + .filter(|height| *height > 1e-6) + }) + .map(|height| height as f32) + .unwrap_or(0.18) + * anno_scale; + let margin_left = style_for_property( + tab, + row, + ci, + cell, + acadrust::entities::table::CellStylePropertyFlags::MARGIN_LEFT, + ) + .map(|style| style.margin_left as f32 * anno_scale) + .filter(|margin| *margin > 1e-6) + .unwrap_or_else(|| h_margin.max(cell_h * 0.5)); + let margin_right = style_for_property( + tab, + row, + ci, + cell, + acadrust::entities::table::CellStylePropertyFlags::MARGIN_RIGHT, + ) + .map(|style| style.margin_right as f32 * anno_scale) + .filter(|margin| *margin > 1e-6) + .unwrap_or_else(|| h_margin.max(cell_h * 0.5)); + let margin_top = style_for_property( + tab, + row, + ci, + cell, + acadrust::entities::table::CellStylePropertyFlags::MARGIN_TOP, + ) + .map(|style| style.margin_top as f32 * anno_scale) + .filter(|margin| *margin > 1e-6) + .unwrap_or_else(|| v_margin.max(cell_h * 0.5)); + let margin_bottom = style_for_property( + tab, + row, + ci, + cell, + acadrust::entities::table::CellStylePropertyFlags::MARGIN_BOTTOM, + ) + .map(|style| style.margin_bottom as f32 * anno_scale) + .filter(|margin| *margin > 1e-6) + .unwrap_or_else(|| v_margin.max(cell_h * 0.5)); + let style_handle = content.text_style_handle.or_else(|| { + style_for_property( + tab, + row, + ci, + cell, + acadrust::entities::table::CellStylePropertyFlags::TEXT_STYLE, + ) + .and_then(|style| style.text_style_handle) + }) + .or_else(|| row_style.and_then(|style| style.text_style_handle)); + let font_owned = + font_for_handle(style_handle).unwrap_or_else(|| "txt".to_string()); + let resolved = resolved_style_for_handle(style_handle, font_owned); + let align = (content.alignment != 0) + .then_some(content.alignment) + .or_else(|| { + style_for_property( + tab, + row, + ci, + cell, + acadrust::entities::table::CellStylePropertyFlags::ALIGNMENT, + ) + .map(|style| style.alignment) + .filter(|alignment| *alignment != 0) + }) + .or_else(|| row_style.map(|style| style.alignment as i32)) + .unwrap_or(5); + let horiz = ((align - 1).rem_euclid(3)) + 1; + let vert = ((align - 1) / 3) + 1; + let (mut x_offset, mut attach_h_anchor) = match horiz { + 1 => (col_left + margin_left, 0.0_f32), + 3 => (col_right - margin_right, 1.0_f32), + _ => (col_left + col_width * 0.5, 0.5_f32), + }; + let (mut y_offset, mut v_anchor) = match vert { + 1 => (row_top + margin_top, MTextVAnchor::Top), + 3 => (row_bot - margin_bottom, MTextVAnchor::Bottom), + _ => (row_mid, MTextVAnchor::Middle), + }; + let mut z_offset = 0.0; + if let Some(geometry) = resolved_content_geometry( + document, + tab, + ri, + ci, + cell, + *content_index, + ) { + x_offset = + col_left + geometry.distance_to_center.x as f32 * anno_scale; + y_offset = + row_top - geometry.distance_to_center.y as f32 * anno_scale; + z_offset = geometry.distance_to_center.z as f32 * anno_scale; + attach_h_anchor = 0.5; + v_anchor = MTextVAnchor::Middle; + } else if value_contents.len() > 1 { + if let Some((x, y)) = fallback_centers.get(slot_index) { + x_offset = *x; + y_offset = *y; + attach_h_anchor = 0.5; + v_anchor = MTextVAnchor::Middle; + } + } + let to = origin + + h * x_offset + + v_flow * y_offset + + normal * z_offset; + let content_rotation_explicit = content.format_property_flags + & acadrust::entities::table::CellStylePropertyFlags::ROTATION.bits() as i32 + != 0; + let rot = if content_rotation_explicit { + content.rotation as f32 + } else { + style_for_property( + tab, + row, + ci, + cell, + acadrust::entities::table::CellStylePropertyFlags::ROTATION, + ) + .map(|style| style.rotation as f32) + .unwrap_or(cell.rotation as f32) + }; + let layout = layout_mtext(&MTextRenderOpts { + columns: Default::default(), + value: text, + insertion: [to.x as f64, to.y as f64, to.z as f64], + height: cell_h, + rect_w: (col_width - margin_left - margin_right).max(0.0), + rotation: rot, + style: &resolved, + attach_h_anchor, + v_anchor, + line_spacing_factor: 1.0, + vertical_text: false, + want_glyph_boxes: false, + }); + let tcol = if selected { + sel_col + } else if !matches!( + content.color, + Color::ByLayer | Color::ByBlock + ) { + resolve_col(&content.color, entity_color) + } else if let Some(style) = style_for_property( + tab, + row, + ci, + cell, + acadrust::entities::table::CellStylePropertyFlags::CONTENT_COLOR, + ) { + resolve_col(&style.content_color, entity_color) + } else if let Some(style) = row_style { + resolve_col(&style.text_color, entity_color) + } else { + entity_color + }; + if let Ok(mut atlas) = crate::scene::text::sdf_atlas::text_atlas().lock() { + for stroke in &layout.strokes { + let Some(run) = &stroke.run else { + continue; + }; + let quads = crate::scene::text::glyph_quads::layout_glyph_quads( + &mut atlas, + run.height, + run.rotation, + run.width_factor, + run.oblique, + run.tracking, + &run.font, + run.bold, + &run.text, + ); + crate::scene::pipeline::text_gpu::push_glyph_vertices( + &mut text_verts, + &quads, + [stroke.origin[0], stroke.origin[1], to.z as f64], + 1.0, + tcol, + 0.0, + ); + } } } } @@ -870,6 +1783,26 @@ impl PropertyEditable for Table { // Direction = angle of the horizontal direction vector in the XY plane. let direction_deg = (self.horizontal_direction.y.atan2(self.horizontal_direction.x)).to_degrees(); + let mut content_count = 0usize; + let mut block_content_count = 0usize; + let mut field_content_count = 0usize; + let mut linked_cell_count = 0usize; + for row in &self.rows { + for cell in &row.cells { + content_count += cell.contents.len(); + linked_cell_count += usize::from(cell.has_linked_data); + for content in &cell.contents { + block_content_count += usize::from(content.block_handle.is_some()); + field_content_count += usize::from(content.field_handle.is_some()); + } + } + } + let break_heights = self + .break_data + .iter() + .map(|data| format!("{:.4}", data.height)) + .collect::>() + .join(", "); vec![ PropSection { @@ -882,6 +1815,27 @@ impl PropertyEditable for Table { ), ro("Rows", "tbl_rows", self.rows.len().to_string()), ro("Columns", "tbl_cols", self.columns.len().to_string()), + ro("Contents", "tbl_contents", content_count.to_string()), + ro( + "Block contents", + "tbl_block_contents", + block_content_count.to_string(), + ), + ro( + "Field contents", + "tbl_field_contents", + field_content_count.to_string(), + ), + ro( + "Merged ranges", + "tbl_merged_ranges", + self.merged_ranges.len().to_string(), + ), + ro( + "Linked cells", + "tbl_linked_cells", + linked_cell_count.to_string(), + ), ro("Direction", "tbl_direction", format!("{:.4}", direction_deg)), ro( "Table width", @@ -932,7 +1886,12 @@ impl PropertyEditable for Table { self.break_options .contains(BreakOptionFlags::ALLOW_MANUAL_HEIGHTS), ), - ro("Break height", "tbl_break_height", String::new()), + ro( + "Segments", + "tbl_break_segments", + self.break_ranges.len().max(self.break_data.len()).to_string(), + ), + ro("Break heights", "tbl_break_height", break_heights), edit("Spacing", "tbl_break_spacing", self.break_spacing), ], }, diff --git a/src/entities/traits.rs b/src/entities/traits.rs index 6dd37a556..980ec0f7b 100644 --- a/src/entities/traits.rs +++ b/src/entities/traits.rs @@ -276,7 +276,7 @@ impl EntityTypeOps for EntityType { Wipeout, AttributeDefinition, AttributeEntity, MLine, Tolerance, Solid, Face3D, PolygonMesh, PolyfaceMesh, Mesh, Table, Text, MText, Leader, MultiLeader, Underlay, Shape, - Ole2Frame, Helix, Light, + Ole2Frame, Helix, Light, Extended, ], _ => None, ) @@ -289,10 +289,11 @@ impl EntityTypeOps for EntityType { Line, Circle, Arc, Ellipse, LwPolyline, Polyline, Polyline2D, Polyline3D, Ray, XLine, RasterImage, Wipeout, AttributeDefinition, AttributeEntity, MLine, Tolerance, - Solid, Solid3D, Region, Body, Face3D, PolygonMesh, + Solid, Solid3D, Region, Body, Surface, Face3D, PolygonMesh, PolyfaceMesh, Mesh, Table, Point, Spline, Text, MText, Viewport, Insert, Leader, MultiLeader, Dimension, Hatch, - Underlay, Shape, Ole2Frame, Helix, Light, + Underlay, Shape, Ole2Frame, Helix, Light, SectionSymbol, + ViewBorder, Extended, ], _ => vec![], ) @@ -305,10 +306,11 @@ impl EntityTypeOps for EntityType { Line, Circle, Arc, Ellipse, LwPolyline, Polyline, Polyline2D, Polyline3D, Ray, XLine, RasterImage, Wipeout, AttributeDefinition, AttributeEntity, MLine, Tolerance, - Solid, Solid3D, Region, Body, Face3D, PolygonMesh, + Solid, Solid3D, Region, Body, Surface, Face3D, PolygonMesh, PolyfaceMesh, Mesh, Table, Hatch, Point, Spline, Text, MText, Viewport, Insert, Dimension, Leader, MultiLeader, Underlay, - Shape, Ole2Frame, Helix, Light, + Shape, Ole2Frame, Helix, Light, SectionSymbol, ViewBorder, + Extended, ], _ => vec![], ) @@ -321,10 +323,11 @@ impl EntityTypeOps for EntityType { Line, Circle, Arc, Ellipse, LwPolyline, Polyline, Polyline2D, Polyline3D, Ray, XLine, RasterImage, Wipeout, AttributeDefinition, AttributeEntity, MLine, Tolerance, - Solid, Solid3D, Region, Body, Face3D, PolygonMesh, + Solid, Solid3D, Region, Body, Surface, Face3D, PolygonMesh, PolyfaceMesh, Mesh, Table, Hatch, Point, Spline, Text, MText, Viewport, Insert, Dimension, Leader, MultiLeader, Underlay, - Shape, Ole2Frame, Helix, Light, + Shape, Ole2Frame, Helix, Light, SectionSymbol, ViewBorder, + Extended, ], _ => {}, ) @@ -337,10 +340,11 @@ impl EntityTypeOps for EntityType { Line, Circle, Arc, Ellipse, LwPolyline, Polyline, Polyline2D, Polyline3D, Ray, XLine, RasterImage, Wipeout, AttributeDefinition, AttributeEntity, MLine, Tolerance, - Solid, Solid3D, Region, Body, Face3D, PolygonMesh, + Solid, Solid3D, Region, Body, Surface, Face3D, PolygonMesh, PolyfaceMesh, Mesh, Table, Point, Spline, Text, MText, Viewport, Insert, Leader, MultiLeader, Dimension, Hatch, - Underlay, Shape, Ole2Frame, Helix, Light, + Underlay, Shape, Ole2Frame, Helix, Light, SectionSymbol, + ViewBorder, Extended, ], _ => {}, ) @@ -353,10 +357,11 @@ impl EntityTypeOps for EntityType { Line, Circle, Arc, Ellipse, LwPolyline, Polyline, Polyline2D, Polyline3D, Ray, XLine, RasterImage, Wipeout, AttributeDefinition, AttributeEntity, MLine, Tolerance, - Solid, Solid3D, Region, Body, Face3D, PolygonMesh, + Solid, Solid3D, Region, Body, Surface, Face3D, PolygonMesh, PolyfaceMesh, Mesh, Table, Point, Spline, Text, MText, Viewport, Insert, Leader, MultiLeader, Dimension, Hatch, - Underlay, Shape, Ole2Frame, + Underlay, Shape, Ole2Frame, SectionSymbol, ViewBorder, + Extended, ], _ => vec![], ) @@ -369,10 +374,11 @@ impl EntityTypeOps for EntityType { Line, Circle, Arc, Ellipse, LwPolyline, Polyline, Polyline2D, Polyline3D, Ray, XLine, RasterImage, Wipeout, AttributeDefinition, AttributeEntity, MLine, Tolerance, - Solid, Solid3D, Region, Body, Face3D, PolygonMesh, + Solid, Solid3D, Region, Body, Surface, Face3D, PolygonMesh, PolyfaceMesh, Mesh, Table, Point, Spline, Text, MText, Viewport, Insert, Leader, MultiLeader, Dimension, Hatch, - Underlay, Shape, Ole2Frame, + Underlay, Shape, Ole2Frame, SectionSymbol, ViewBorder, + Extended, ], _ => {}, ) @@ -389,10 +395,11 @@ impl EntityTypeOps for EntityType { Line, Circle, Arc, Ellipse, LwPolyline, Polyline, Polyline2D, Polyline3D, Ray, XLine, RasterImage, Wipeout, AttributeDefinition, AttributeEntity, MLine, Tolerance, - Solid, Solid3D, Region, Body, Face3D, PolygonMesh, + Solid, Solid3D, Region, Body, Surface, Face3D, PolygonMesh, PolyfaceMesh, Mesh, Table, Point, Spline, Text, MText, Viewport, Insert, Leader, MultiLeader, Dimension, Hatch, - Underlay, Shape, Ole2Frame, + Underlay, Shape, Ole2Frame, SectionSymbol, ViewBorder, + Extended, ], _ => None, ) @@ -410,10 +417,10 @@ impl EntityTypeOps for EntityType { Line, Circle, Arc, Ellipse, LwPolyline, Polyline, Polyline2D, Polyline3D, Ray, XLine, RasterImage, Wipeout, AttributeDefinition, AttributeEntity, MLine, Tolerance, - Solid, Solid3D, Region, Body, Face3D, PolygonMesh, + Solid, Solid3D, Region, Body, Surface, Face3D, PolygonMesh, PolyfaceMesh, Mesh, Table, Point, Spline, Text, MText, Viewport, Insert, Leader, MultiLeader, Dimension, Hatch, - Underlay, Shape, Ole2Frame, + Underlay, Shape, Ole2Frame, SectionSymbol, ViewBorder, ], _ => {}, ) @@ -429,7 +436,8 @@ impl EntityTypeOps for EntityType { Tolerance, Solid, Face3D, PolygonMesh, PolyfaceMesh, Mesh, Table, MText, Point, Spline, Text, Viewport, Dimension, Leader, MultiLeader, Underlay, Shape, Ole2Frame, - Solid3D, Region, Body, Surface, Helix, Light, + Solid3D, Region, Body, Surface, Helix, Light, SectionSymbol, + ViewBorder, Extended, ], _ => {}, ) diff --git a/src/entities/view_symbol.rs b/src/entities/view_symbol.rs new file mode 100644 index 000000000..8abac42e0 --- /dev/null +++ b/src/entities/view_symbol.rs @@ -0,0 +1,263 @@ +use acadrust::entities::{SectionSymbol, ViewBorder}; + +use crate::command::EntityTransform; +use crate::entities::common::{edit_angle_prop, edit_prop, parse_f64, ro_prop, square_grip}; +use crate::entities::traits::{Grippable, PropertyEditable, Transformable}; +use crate::scene::model::object::{GripApply, GripDef, PropSection}; + +fn handle_text(handle: acadrust::Handle) -> String { + if handle.is_null() { + "None".to_string() + } else { + format!("{:X}", handle.value()) + } +} + +fn vector_text(vector: &acadrust::types::Vector3) -> String { + format!("{:.6}, {:.6}, {:.6}", vector.x, vector.y, vector.z) +} + +fn apply_entity_transform(entity: &mut T, transform: &EntityTransform) { + crate::scene::view::transform::apply_standard_entity_transform( + entity, + transform, + |entity, first, second| { + entity.apply_transform( + &crate::scene::view::transform::reflection_about_xy_line(first, second), + ); + }, + ); +} + +impl Grippable for SectionSymbol { + fn grips(&self) -> Vec { + self.points + .iter() + .enumerate() + .map(|(index, point)| { + square_grip( + index, + glam::DVec3::new(point.point.x, point.point.y, point.point.z), + ) + }) + .collect() + } + + fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { + let Some(point) = self.points.get_mut(grip_id) else { + return; + }; + match apply { + GripApply::Translate(delta) => { + point.point.x += delta.x; + point.point.y += delta.y; + point.point.z += delta.z; + } + GripApply::Absolute(position) => { + point.point = + acadrust::types::Vector3::new(position.x, position.y, position.z); + } + } + self.sync_display_fields(); + } +} + +impl PropertyEditable for SectionSymbol { + fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { + let point_data = self + .points + .iter() + .enumerate() + .map(|(index, point)| { + format!( + "{}: point [{}]; bulge {:.6}; label {:?}; offset [{}]; flag {}", + index + 1, + vector_text(&point.point), + point.bulge, + point.label, + vector_text(&point.label_offset), + point.raw_flag_280 + ) + }) + .collect::>() + .join("\n"); + vec![ + PropSection { + title: "Section Symbol".into(), + props: vec![ + edit_prop("Symbol Scale", "section_scale", self.symbol_scale), + ro_prop( + "View Symbol Version", + "section_view_version", + self.view_symbol_version.to_string(), + ), + ro_prop("Version", "section_version", self.version.to_string()), + ro_prop( + "Style", + "section_style", + handle_text(self.style_handle), + ), + ro_prop( + "View Representation", + "section_view_rep", + handle_text(self.view_rep_handle), + ), + ro_prop( + "View Symbol Flags", + "section_view_flags", + self.raw_view_symbol_70.to_string(), + ), + ro_prop( + "Point Count", + "section_point_count", + self.points.len().to_string(), + ), + ro_prop( + "Stored Point Counts", + "section_raw_counts", + format!( + "{}, {}, flags {}", + self.raw_point_count_90, + self.raw_point_record_count, + self.raw_flags_90 + ), + ), + ro_prop("Identifier", "section_label", self.label.clone()), + ], + }, + PropSection { + title: "Section Points".into(), + props: vec![ro_prop("Records", "section_points", point_data)], + }, + ] + } + + fn apply_geom_prop(&mut self, field: &str, value: &str) { + if field == "section_scale" { + if let Some(value) = parse_f64(value) { + self.symbol_scale = value.max(0.0); + } + } + } +} + +impl Transformable for SectionSymbol { + fn apply_transform(&mut self, transform: &EntityTransform) { + apply_entity_transform(self, transform); + self.sync_display_fields(); + } +} + +impl Grippable for ViewBorder { + fn grips(&self) -> Vec { + vec![ + square_grip(0, glam::DVec3::new(self.min[0], self.min[1], 0.0)), + square_grip(1, glam::DVec3::new(self.max[0], self.max[1], 0.0)), + square_grip(2, glam::DVec3::new(self.center[0], self.center[1], 0.0)), + ] + } + + fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { + let delta = match apply { + GripApply::Translate(delta) => delta, + GripApply::Absolute(position) => { + let current = match grip_id { + 0 => glam::DVec3::new(self.min[0], self.min[1], 0.0), + 1 => glam::DVec3::new(self.max[0], self.max[1], 0.0), + 2 => glam::DVec3::new(self.center[0], self.center[1], 0.0), + _ => return, + }; + position - current + } + }; + match grip_id { + 0 => { + self.min[0] += delta.x; + self.min[1] += delta.y; + } + 1 => { + self.max[0] += delta.x; + self.max[1] += delta.y; + } + 2 => { + self.min[0] += delta.x; + self.min[1] += delta.y; + self.max[0] += delta.x; + self.max[1] += delta.y; + } + _ => return, + } + self.center = [ + (self.min[0] + self.max[0]) * 0.5, + (self.min[1] + self.max[1]) * 0.5, + ]; + } +} + +impl PropertyEditable for ViewBorder { + fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { + vec![PropSection { + title: "Drawing View".into(), + props: vec![ + edit_prop("Center X", "view_border_x", self.center[0]), + edit_prop("Center Y", "view_border_y", self.center[1]), + ro_prop( + "Width", + "view_border_width", + format!("{:.6}", self.max[0] - self.min[0]), + ), + ro_prop( + "Height", + "view_border_height", + format!("{:.6}", self.max[1] - self.min[1]), + ), + edit_prop("Scale", "view_border_scale", self.scale), + edit_angle_prop( + "Rotation", + "view_border_rotation", + self.rotation_angle.to_degrees(), + ), + ro_prop("Version", "view_border_version", self.version.to_string()), + ro_prop( + "Active Viewport", + "view_border_viewport", + handle_text(self.active_viewport), + ), + ro_prop( + "Scale Object", + "view_border_scale_handle", + handle_text(self.scale_handle), + ), + ], + }] + } + + fn apply_geom_prop(&mut self, field: &str, value: &str) { + let Some(value) = parse_f64(value) else { + return; + }; + match field { + "view_border_x" => { + let delta = value - self.center[0]; + self.min[0] += delta; + self.max[0] += delta; + self.center[0] = value; + } + "view_border_y" => { + let delta = value - self.center[1]; + self.min[1] += delta; + self.max[1] += delta; + self.center[1] = value; + } + "view_border_scale" => self.scale = value.max(0.0), + "view_border_rotation" => self.rotation_angle = value.to_radians(), + _ => {} + } + } +} + +impl Transformable for ViewBorder { + fn apply_transform(&mut self, transform: &EntityTransform) { + apply_entity_transform(self, transform); + } +} diff --git a/src/io/mod.rs b/src/io/mod.rs index ac6896e43..3c9d3da12 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -172,7 +172,11 @@ pub async fn open_path_with_phase( 10000, ); }; - let mut caches = crate::scene::build_derived_caches_with_progress(&doc, &cache_progress); + let mut caches = crate::scene::build_derived_caches_with_progress( + &doc, + &cache_progress, + path2.parent(), + ); caches.timings = crate::scene::OpenTimings { parse_ms, purge_ms, diff --git a/src/io/obj.rs b/src/io/obj.rs index 5bd61b29e..08507b463 100644 --- a/src/io/obj.rs +++ b/src/io/obj.rs @@ -106,6 +106,8 @@ pub fn parse_obj(src: &str, color: [f32; 4]) -> Option { verts_low: Vec::new(), normals: norms, indices, + triangle_material_handles: Vec::new(), + triangle_colors: Vec::new(), color, selected: false, }) diff --git a/src/scene/annotative.rs b/src/scene/annotative.rs index b97377dc0..1e1266fb0 100644 --- a/src/scene/annotative.rs +++ b/src/scene/annotative.rs @@ -8,10 +8,11 @@ use acadrust::entities::{EntityCommon, EntityType}; use acadrust::objects::{ - Dictionary, MTextContext, ObjectContextData, ObjectContextKind, ObjectType, + Dictionary, HatchScaleContext, MTextContext, ObjectContextData, ObjectContextKind, ObjectType, }; use acadrust::types::{Vector2, Vector3}; use acadrust::{CadDocument, Handle}; +use std::borrow::Cow; /// Resolve a handle to a `Dictionary` object, if it is one. pub fn as_dict(doc: &CadDocument, handle: Handle) -> Option<&Dictionary> { @@ -344,6 +345,566 @@ fn annotation_scales_dict(doc: &CadDocument, entity: Handle) -> Option { as_dict(doc, mgr).and_then(|d| d.get("ACDB_ANNOTATIONSCALES")) } +/// Resolve the representation leaf used by the drawing's current annotation +/// scale. Broken scale handles are ignored. When the current named scale is +/// absent, the leaf explicitly marked as the native/default representation is +/// preferred, followed by the first valid leaf. +pub fn active_object_context( + doc: &CadDocument, + entity: Handle, +) -> Option<&ObjectContextData> { + let coll_h = annotation_scales_dict(doc, entity)?; + let coll = as_dict(doc, coll_h)?; + let mut default = None; + let mut first = None; + for (_, leaf_h) in &coll.entries { + let Some(ObjectType::ObjectContextData(leaf)) = doc.objects.get(leaf_h) else { + continue; + }; + first.get_or_insert(leaf); + if leaf.is_default { + default = Some(leaf); + } + let Some(ObjectType::Scale(scale)) = doc.objects.get(&leaf.scale) else { + continue; + }; + if scale + .name + .eq_ignore_ascii_case(&doc.header.current_annotation_scale) + { + return Some(leaf); + } + } + default.or(first) +} + +fn text_horizontal(value: i16) -> acadrust::entities::TextHorizontalAlignment { + use acadrust::entities::TextHorizontalAlignment; + match value { + 1 => TextHorizontalAlignment::Center, + 2 => TextHorizontalAlignment::Right, + 3 => TextHorizontalAlignment::Aligned, + 4 => TextHorizontalAlignment::Middle, + 5 => TextHorizontalAlignment::Fit, + _ => TextHorizontalAlignment::Left, + } +} + +fn mtext_attachment(value: i32) -> acadrust::entities::AttachmentPoint { + use acadrust::entities::AttachmentPoint; + match value { + 2 => AttachmentPoint::TopCenter, + 3 => AttachmentPoint::TopRight, + 4 => AttachmentPoint::MiddleLeft, + 5 => AttachmentPoint::MiddleCenter, + 6 => AttachmentPoint::MiddleRight, + 7 => AttachmentPoint::BottomLeft, + 8 => AttachmentPoint::BottomCenter, + 9 => AttachmentPoint::BottomRight, + _ => AttachmentPoint::TopLeft, + } +} + +fn apply_mtext_context(entity: &mut acadrust::entities::MText, context: &MTextContext) { + entity.attachment_point = mtext_attachment(context.attachment); + entity.insertion_point = context.insertion; + entity.rectangle_width = context.rect_width; + entity.rectangle_height = (context.rect_height > 0.0).then_some(context.rect_height); + entity.extents_width = context.extents_width; + entity.extents_height = context.extents_height; + entity.rotation = context.x_axis_dir.y.atan2(context.x_axis_dir.x); + entity.dwg_x_direction = Some(context.x_axis_dir); + if let Some(columns) = &context.columns { + entity.column_data.column_type = context.column_type as i16; + entity.column_data.column_count = columns.num_heights; + entity.column_data.width = columns.width; + entity.column_data.gutter = columns.gutter; + entity.column_data.auto_height = columns.auto_height; + entity.column_data.flow_reversed = columns.flow_reversed; + entity.column_data.heights.clone_from(&columns.heights); + } else { + entity.column_data.column_type = context.column_type as i16; + entity.column_data.column_count = 0; + entity.column_data.heights.clear(); + } +} + +fn apply_dimension_context( + dimension: &mut acadrust::entities::Dimension, + context: &acadrust::objects::DimContext, + doc: &CadDocument, +) { + use acadrust::entities::Dimension; + use acadrust::objects::DimSubtype; + + { + let base = dimension.base_mut(); + base.text_middle_point.x = context.def_pt.x; + base.text_middle_point.y = context.def_pt.y; + base.text_rotation = context.text_rotation; + base.text_user_positioned = context.is_def_textloc; + base.flip_arrow1 = context.flip_arrow1; + base.flip_arrow2 = context.flip_arrow2; + if let Some(record) = doc.block_records.iter().find(|r| r.handle == context.block) { + base.block_name.clone_from(&record.name); + } + } + + match (&context.subtype, dimension) { + (DimSubtype::Aligned { dimline_pt }, Dimension::Aligned(dim)) => { + dim.definition_point = *dimline_pt; + dim.base.definition_point = *dimline_pt; + } + (DimSubtype::Aligned { dimline_pt }, Dimension::Linear(dim)) => { + dim.definition_point = *dimline_pt; + dim.base.definition_point = *dimline_pt; + } + (DimSubtype::Angular { arc_pt }, Dimension::Angular2Ln(dim)) => { + dim.dimension_arc = *arc_pt; + dim.base.definition_point = *arc_pt; + } + (DimSubtype::Angular { arc_pt }, Dimension::Angular3Pt(dim)) => { + dim.definition_point = *arc_pt; + dim.base.definition_point = *arc_pt; + } + ( + DimSubtype::Diametric { + first_arc_pt, + def_pt, + }, + Dimension::Diameter(dim), + ) => { + dim.angle_vertex = *first_arc_pt; + dim.definition_point = *def_pt; + dim.base.definition_point = *def_pt; + } + (DimSubtype::Radial { first_arc_pt }, Dimension::Radius(dim)) => { + dim.definition_point = *first_arc_pt; + dim.base.definition_point = *first_arc_pt; + } + ( + DimSubtype::RadialLarge { + ovr_center, + jog_point, + }, + Dimension::LargeRadial(dim), + ) => { + dim.override_center = *ovr_center; + dim.jog_point = *jog_point; + } + ( + DimSubtype::Ordinate { + feature_location_pt, + leader_endpt, + }, + Dimension::Ordinate(dim), + ) => { + dim.feature_location = *feature_location_pt; + dim.leader_endpoint = *leader_endpt; + } + _ => {} + } +} + +fn apply_attribute_context( + insertion_point: &mut Vector3, + alignment_point: &mut Vector3, + rotation: &mut f64, + horizontal_alignment: &mut acadrust::entities::HorizontalAlignment, + embedded_mtext: &mut Option>, + context: &acadrust::objects::MTextAttributeContext, +) { + insertion_point.x = context.insertion.x; + insertion_point.y = context.insertion.y; + alignment_point.x = context.alignment.x; + alignment_point.y = context.alignment.y; + *rotation = context.rotation; + *horizontal_alignment = + acadrust::entities::HorizontalAlignment::from_value(context.horizontal_mode); + if context.enable_context { + if let (Some(embedded), Some(mtext)) = (&context.context, embedded_mtext.as_mut()) { + apply_mtext_context(mtext, &embedded.mtext); + } + } +} + +fn apply_hatch_context(hatch: &mut acadrust::entities::Hatch, context: &HatchScaleContext) { + hatch.pattern.lines.clone_from(&context.pattern_lines); + hatch.pattern_scale = context.pattern_scale; + for line in &mut hatch.pattern.lines { + line.base_point.x += context.pattern_base.x; + line.base_point.y += context.pattern_base.y; + } + for (path, bits) in hatch.paths.iter_mut().zip(&context.loop_types) { + path.flags = acadrust::entities::BoundaryPathFlags::from_bits(*bits as u32); + } +} + +/// Return an ephemeral entity representation with the active scale leaf +/// overlaid on its base geometry. The source document remains unchanged, which +/// keeps save/round-trip data intact while render, picking and block expansion +/// all see the scale-specific placement. +pub fn entity_for_active_context<'a>( + doc: &'a CadDocument, + entity: &'a EntityType, +) -> Cow<'a, EntityType> { + let Some(context) = active_object_context(doc, entity.common().handle) else { + return Cow::Borrowed(entity); + }; + let mut placed = entity.clone(); + match (&context.kind, &mut placed) { + ( + ObjectContextKind::BlkRef { + rotation, + insertion, + scale_factor, + }, + EntityType::Insert(insert), + ) => { + insert.rotation = *rotation; + insert.insert_point = *insertion; + insert.set_x_scale(scale_factor.x); + insert.set_y_scale(scale_factor.y); + insert.set_z_scale(scale_factor.z); + } + ( + ObjectContextKind::Text { + horizontal_mode, + rotation, + insertion, + alignment, + }, + EntityType::Text(text), + ) => { + text.horizontal_alignment = text_horizontal(*horizontal_mode); + text.rotation = *rotation; + text.insertion_point.x = insertion.x; + text.insertion_point.y = insertion.y; + let point = text.alignment_point.get_or_insert(Vector3::ZERO); + point.x = alignment.x; + point.y = alignment.y; + } + (ObjectContextKind::MText(value), EntityType::MText(mtext)) => { + apply_mtext_context(mtext, value); + } + (ObjectContextKind::Dim(value), EntityType::Dimension(dimension)) => { + apply_dimension_context(dimension, value, doc); + } + (ObjectContextKind::MLeader(value), EntityType::MultiLeader(mleader)) => { + mleader.context.clone_from(value); + } + ( + ObjectContextKind::MTextAttribute(value), + EntityType::AttributeEntity(attribute), + ) => { + apply_attribute_context( + &mut attribute.insertion_point, + &mut attribute.alignment_point, + &mut attribute.rotation, + &mut attribute.horizontal_alignment, + &mut attribute.embedded_mtext, + value, + ); + } + ( + ObjectContextKind::MTextAttribute(value), + EntityType::AttributeDefinition(attribute), + ) => { + apply_attribute_context( + &mut attribute.insertion_point, + &mut attribute.alignment_point, + &mut attribute.rotation, + &mut attribute.horizontal_alignment, + &mut attribute.embedded_mtext, + value, + ); + } + (ObjectContextKind::Leader(value), EntityType::Leader(leader)) => { + leader.vertices = value + .points + .iter() + .map(|point| *point + value.insertion_offset) + .collect(); + leader.horizontal_direction = value.x_direction; + leader.annotation_offset = value.endpoint_projection; + } + ( + ObjectContextKind::Fcf { + location, + horizontal_direction, + }, + EntityType::Tolerance(tolerance), + ) => { + tolerance.insertion_point = *location; + tolerance.direction = *horizontal_direction; + } + (ObjectContextKind::HatchScale(value), EntityType::Hatch(hatch)) => { + apply_hatch_context(hatch, value); + } + (ObjectContextKind::HatchView(value), EntityType::Hatch(hatch)) => { + apply_hatch_context(hatch, &value.hatch); + hatch.normal = value.view_normal; + hatch.pattern_angle += value.view_rotation; + } + _ => {} + } + Cow::Owned(placed) +} + +fn sync_mtext_context(context: &mut MTextContext, entity: &acadrust::entities::MText) { + context.attachment = entity.attachment_point as i32; + context.x_axis_dir = entity.dwg_x_direction.unwrap_or_else(|| { + Vector3::new(entity.rotation.cos(), entity.rotation.sin(), 0.0) + }); + context.insertion = entity.insertion_point; + context.rect_width = entity.rectangle_width; + context.rect_height = entity.rectangle_height.unwrap_or(0.0); + context.extents_width = entity.extents_width; + context.extents_height = entity.extents_height; + context.column_type = entity.column_data.column_type as i32; + if context.column_type == 0 { + context.columns = None; + } else { + let columns = context + .columns + .get_or_insert_with(|| acadrust::objects::MTextColumns { + num_heights: 0, + width: 0.0, + gutter: 0.0, + auto_height: false, + flow_reversed: false, + heights: Vec::new(), + }); + columns.num_heights = entity.column_data.column_count; + columns.width = entity.column_data.width; + columns.gutter = entity.column_data.gutter; + columns.auto_height = entity.column_data.auto_height; + columns.flow_reversed = entity.column_data.flow_reversed; + columns.heights.clone_from(&entity.column_data.heights); + } +} + +fn sync_dimension_context( + context: &mut acadrust::objects::DimContext, + dimension: &acadrust::entities::Dimension, + block_handle: Option, +) { + use acadrust::entities::Dimension; + use acadrust::objects::DimSubtype; + + let base = dimension.base(); + context.def_pt = Vector2::new(base.text_middle_point.x, base.text_middle_point.y); + context.is_def_textloc = base.text_user_positioned; + context.text_rotation = base.text_rotation; + context.flip_arrow1 = base.flip_arrow1; + context.flip_arrow2 = base.flip_arrow2; + if let Some(handle) = block_handle { + context.block = handle; + } + match (&mut context.subtype, dimension) { + (DimSubtype::Aligned { dimline_pt }, Dimension::Aligned(dim)) => { + *dimline_pt = dim.definition_point; + } + (DimSubtype::Aligned { dimline_pt }, Dimension::Linear(dim)) => { + *dimline_pt = dim.definition_point; + } + (DimSubtype::Angular { arc_pt }, Dimension::Angular2Ln(dim)) => { + *arc_pt = dim.dimension_arc; + } + (DimSubtype::Angular { arc_pt }, Dimension::Angular3Pt(dim)) => { + *arc_pt = dim.definition_point; + } + ( + DimSubtype::Diametric { + first_arc_pt, + def_pt, + }, + Dimension::Diameter(dim), + ) => { + *first_arc_pt = dim.angle_vertex; + *def_pt = dim.definition_point; + } + (DimSubtype::Radial { first_arc_pt }, Dimension::Radius(dim)) => { + *first_arc_pt = dim.definition_point; + } + ( + DimSubtype::RadialLarge { + ovr_center, + jog_point, + }, + Dimension::LargeRadial(dim), + ) => { + *ovr_center = dim.override_center; + *jog_point = dim.jog_point; + } + ( + DimSubtype::Ordinate { + feature_location_pt, + leader_endpt, + }, + Dimension::Ordinate(dim), + ) => { + *feature_location_pt = dim.feature_location; + *leader_endpt = dim.leader_endpoint; + } + _ => {} + } +} + +/// Copy an edited entity's placement back into its active per-scale leaf. +/// Geometry edits therefore remain visible at the current annotation scale and +/// round-trip as genuine `AcDb*ObjectContextData`, while the base entity stays +/// usable as the default representation. +pub fn sync_active_context_from_entity( + doc: &mut CadDocument, + entity_handle: Handle, +) -> bool { + let Some(leaf_handle) = active_object_context(doc, entity_handle).map(|leaf| leaf.handle) + else { + return false; + }; + let Some(entity) = doc.get_entity(entity_handle).cloned() else { + return false; + }; + let dim_block_handle = match &entity { + EntityType::Dimension(dimension) => doc + .block_records + .iter() + .find(|record| record.name.eq_ignore_ascii_case(&dimension.base().block_name)) + .map(|record| record.handle), + _ => None, + }; + let Some(ObjectType::ObjectContextData(leaf)) = doc.objects.get_mut(&leaf_handle) else { + return false; + }; + match (&entity, &mut leaf.kind) { + ( + EntityType::Insert(insert), + ObjectContextKind::BlkRef { + rotation, + insertion, + scale_factor, + }, + ) => { + *rotation = insert.rotation; + *insertion = insert.insert_point; + *scale_factor = + Vector3::new(insert.x_scale(), insert.y_scale(), insert.z_scale()); + } + ( + EntityType::Text(text), + ObjectContextKind::Text { + horizontal_mode, + rotation, + insertion, + alignment, + }, + ) => { + *horizontal_mode = match text.horizontal_alignment { + acadrust::entities::TextHorizontalAlignment::Left => 0, + acadrust::entities::TextHorizontalAlignment::Center => 1, + acadrust::entities::TextHorizontalAlignment::Right => 2, + acadrust::entities::TextHorizontalAlignment::Aligned => 3, + acadrust::entities::TextHorizontalAlignment::Middle => 4, + acadrust::entities::TextHorizontalAlignment::Fit => 5, + }; + *rotation = text.rotation; + *insertion = Vector2::new(text.insertion_point.x, text.insertion_point.y); + let point = text.alignment_point.unwrap_or(Vector3::ZERO); + *alignment = Vector2::new(point.x, point.y); + } + (EntityType::MText(mtext), ObjectContextKind::MText(context)) => { + sync_mtext_context(context, mtext); + } + (EntityType::Dimension(dimension), ObjectContextKind::Dim(context)) => { + sync_dimension_context(context, dimension, dim_block_handle); + } + (EntityType::MultiLeader(mleader), ObjectContextKind::MLeader(context)) => { + context.clone_from(&mleader.context); + } + ( + EntityType::AttributeEntity(attribute), + ObjectContextKind::MTextAttribute(context), + ) => { + context.horizontal_mode = attribute.horizontal_alignment.to_value(); + context.rotation = attribute.rotation; + context.insertion = + Vector2::new(attribute.insertion_point.x, attribute.insertion_point.y); + context.alignment = + Vector2::new(attribute.alignment_point.x, attribute.alignment_point.y); + if let (Some(embedded), Some(mtext)) = + (&mut context.context, &attribute.embedded_mtext) + { + sync_mtext_context(&mut embedded.mtext, mtext); + } + } + ( + EntityType::AttributeDefinition(attribute), + ObjectContextKind::MTextAttribute(context), + ) => { + context.horizontal_mode = attribute.horizontal_alignment.to_value(); + context.rotation = attribute.rotation; + context.insertion = + Vector2::new(attribute.insertion_point.x, attribute.insertion_point.y); + context.alignment = + Vector2::new(attribute.alignment_point.x, attribute.alignment_point.y); + if let (Some(embedded), Some(mtext)) = + (&mut context.context, &attribute.embedded_mtext) + { + sync_mtext_context(&mut embedded.mtext, mtext); + } + } + (EntityType::Leader(leader), ObjectContextKind::Leader(context)) => { + context.points = leader + .vertices + .iter() + .map(|point| *point - context.insertion_offset) + .collect(); + context.x_direction = leader.horizontal_direction; + context.endpoint_projection = leader.annotation_offset; + } + ( + EntityType::Tolerance(tolerance), + ObjectContextKind::Fcf { + location, + horizontal_direction, + }, + ) => { + *location = tolerance.insertion_point; + *horizontal_direction = tolerance.direction; + } + (EntityType::Hatch(hatch), ObjectContextKind::HatchScale(context)) => { + context.pattern_lines.clone_from(&hatch.pattern.lines); + for line in &mut context.pattern_lines { + line.base_point.x -= context.pattern_base.x; + line.base_point.y -= context.pattern_base.y; + } + context.pattern_scale = hatch.pattern_scale; + context.loop_types = hatch + .paths + .iter() + .map(|path| path.flags.bits() as i32) + .collect(); + } + (EntityType::Hatch(hatch), ObjectContextKind::HatchView(context)) => { + context.hatch.pattern_lines.clone_from(&hatch.pattern.lines); + for line in &mut context.hatch.pattern_lines { + line.base_point.x -= context.hatch.pattern_base.x; + line.base_point.y -= context.hatch.pattern_base.y; + } + context.hatch.pattern_scale = hatch.pattern_scale; + context.hatch.loop_types = hatch + .paths + .iter() + .map(|path| path.flags.bits() as i32) + .collect(); + context.view_normal = hatch.normal; + } + _ => {} + } + true +} + /// Get the child dictionary stored under `key` in `parent_h`, creating an empty /// one (owned by `parent_h`) and registering the entry when absent. fn get_or_create_child_dict(doc: &mut CadDocument, parent_h: Handle, key: &str) -> Handle { @@ -489,4 +1050,3 @@ pub fn is_annotative(doc: &CadDocument, entity: &EntityType) -> bool { _ => false, } } - diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index 04eb97485..627827a06 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -358,9 +358,12 @@ fn build_defn( let cap = br.entity_handles.len(); let mut subs: Vec = Vec::with_capacity(cap); for &eh in &br.entity_handles { - let Some(entity) = doc.get_entity(eh) else { + let Some(source_entity) = doc.get_entity(eh) else { continue; }; + let contextual = + crate::scene::annotative::entity_for_active_context(doc, source_entity); + let entity = contextual.as_ref(); // Skip entities flagged invisible. Dynamic blocks (e.g. a visibility- // state parametric block) keep the geometry for every state in one // anonymous block and mark all but the active state's entities @@ -468,7 +471,9 @@ fn build_defn( let tblk = tab .block_record_handle .and_then(|h| doc.block_records.iter().find(|br| br.handle == h)); + let mut used_baked = false; if let Some(tblk) = tblk { + used_baked = !tblk.entity_handles.is_empty(); let ins = tab.insertion_point; let angle = tab.horizontal_direction.y.atan2(tab.horizontal_direction.x); for &teh in &tblk.entity_handles { @@ -505,6 +510,25 @@ fn build_defn( } } } + if !used_baked { + for lw in + tessellate_sub_local(doc, entity, anno_scale, bg_color, depth_map) + { + subs.push(LocalSub::Wire(lw)); + } + for insert in crate::entities::table::block_cell_inserts( + tab, + doc, + anno_scale, + ) { + subs.push(LocalSub::Nested(build_nested_ref( + &insert, + doc, + bg_color, + depth_map, + ))); + } + } } _ => { // A wide polyline inside a block carries its `world_width` on @@ -539,6 +563,9 @@ fn build_nested_ref( // children (raw colour; adapted at emit like `ins_color`). let l0 = crate::scene::view::render::layer_render_style(doc, &nested_ins.common.layer); let _ = bg_color; + let nested_entity = EntityType::Insert(nested_ins.clone()); + let has_book_color = + crate::scene::view::render::has_resolved_book_color(doc, &nested_entity); // Bake the XCLIP boundary (parent-defn-local) so the nested insert keeps // its clip when the parent block is expanded — the spatial filter object @@ -553,10 +580,10 @@ fn build_nested_ref( ins_pat_len, ins_pat, ins_lw_px, - color_is_byblock: nested_ins.common.color == AcadColor::ByBlock, + color_is_byblock: !has_book_color && nested_ins.common.color == AcadColor::ByBlock, lt_is_byblock: nested_ins.common.linetype.eq_ignore_ascii_case("byblock"), lw_is_byblock: matches!(nested_ins.common.line_weight, LineWeight::ByBlock), - color_is_bylayer: nested_ins.common.color == AcadColor::ByLayer, + color_is_bylayer: !has_book_color && nested_ins.common.color == AcadColor::ByLayer, lt_is_bylayer: { let lt = &nested_ins.common.linetype; lt.is_empty() || lt.eq_ignore_ascii_case("bylayer") @@ -601,7 +628,10 @@ fn tessellate_sub_local( let (sub_color, pat_len, pat, lw_px, aci) = crate::scene::view::render::render_style_for(doc, sub); let _ = bg_color; - let color_is_byblock = sub.common().color == AcadColor::ByBlock; + let has_book_color = + crate::scene::view::render::has_resolved_book_color(doc, sub); + let color_is_byblock = + !has_book_color && sub.common().color == AcadColor::ByBlock; let lt_is_byblock = sub.common().linetype.eq_ignore_ascii_case("byblock"); let lw_is_byblock = matches!(sub.common().line_weight, LineWeight::ByBlock); @@ -609,7 +639,8 @@ fn tessellate_sub_local( // INSERT's layer at expand time. Flag each ByLayer property so emit_wire // can override the cached (layer-0-resolved) value with the insert layer's. let on_l0 = crate::scene::view::render::is_effective_layer_zero(&sub.common().layer); - let color_l0 = on_l0 && sub.common().color == AcadColor::ByLayer; + let color_l0 = + !has_book_color && on_l0 && sub.common().color == AcadColor::ByLayer; let lt_l0 = on_l0 && { let lt = &sub.common().linetype; lt.is_empty() || lt.eq_ignore_ascii_case("bylayer") diff --git a/src/scene/cache/properties.rs b/src/scene/cache/properties.rs index 86391fba0..cd69bd682 100644 --- a/src/scene/cache/properties.rs +++ b/src/scene/cache/properties.rs @@ -124,13 +124,41 @@ pub fn visualization_section(entity: &EntityType) -> Option { 1 => "ByBlock", _ => "Custom", }; + let mut props = vec![Property { + label: "Material".into(), + field: "material", + value: PropValue::ReadOnly(material.into()), + }]; + let handle_text = |handle: Option| { + handle + .filter(|handle| handle.is_valid()) + .map_or_else(|| "None".to_string(), |handle| format!("{:X}", handle.value())) + }; + props.extend([ + Property { + label: "Visual style".into(), + field: "visual_style", + value: PropValue::ReadOnly(handle_text(common.full_visual_style_handle)), + }, + Property { + label: "Face style".into(), + field: "face_visual_style", + value: PropValue::ReadOnly(handle_text(common.face_visual_style_handle)), + }, + Property { + label: "Edge style".into(), + field: "edge_visual_style", + value: PropValue::ReadOnly(handle_text(common.edge_visual_style_handle)), + }, + Property { + label: "Shadow flags".into(), + field: "shadow_flags", + value: PropValue::ReadOnly(common.shadow_flags.to_string()), + }, + ]); Some(PropSection { title: "3D Visualization".into(), - props: vec![Property { - label: "Material".into(), - field: "material", - value: PropValue::ReadOnly(material.into()), - }], + props, }) } @@ -144,4 +172,3 @@ pub fn fallback_properties(_handle: Handle, entity: &EntityType) -> PropSection }], } } - diff --git a/src/scene/convert/acis_to_truck.rs b/src/scene/convert/acis_to_truck.rs index 89dbbe35b..1ea70e228 100644 --- a/src/scene/convert/acis_to_truck.rs +++ b/src/scene/convert/acis_to_truck.rs @@ -34,6 +34,7 @@ use crate::scene::convert::solid3d_tess::{ TRUCK_CHORD_FRAC, }; use crate::scene::model::mesh_model::MeshLodSet; +use rustc_hash::FxHashMap; /// Slightly over 2π so revolution builders close the loop. const FULL: f64 = std::f64::consts::TAU + 0.2; @@ -91,9 +92,53 @@ pub fn tessellate_sat_truck( let mut verts: Vec<[f64; 3]> = Vec::new(); let mut normals: Vec<[f32; 3]> = Vec::new(); let mut indices: Vec = Vec::new(); + let face_materials: FxHashMap = sat + .records + .iter() + .filter(|record| record.entity_type == "material-adesk-attrib") + .filter_map(|record| { + let owner = record.token_pointer(2)?.0; + let handle = record.token(3)?.as_integer()?; + (owner >= 0 && handle > 0) + .then(|| (owner, acadrust::Handle::new(handle as u64))) + }) + .collect(); + let face_colors: FxHashMap = sat + .records + .iter() + .filter(|record| record.entity_type == "color-adesk-attrib") + .filter_map(|record| { + let owner = record.token_pointer(2)?.0; + let value = record.token(3)?.as_integer()?; + let face_color = if (1..=255).contains(&value) { + acadrust::Color::from_index(value as i16) + } else if value > 257 { + acadrust::Color::from_true_color_value(value as i32) + } else { + return None; + }; + Some(( + owner, + { + let mut rgba = + crate::scene::convert::tess_util::aci_to_rgba(&face_color); + rgba[3] = color[3]; + rgba + }, + )) + }) + .collect(); + let mut triangle_material_handles: Vec> = Vec::new(); + let mut triangle_colors: Vec> = Vec::new(); + let face_record_indices: Vec = sat + .records + .iter() + .filter(|record| record.is_a("face")) + .map(|record| record.index) + .collect(); let mut complete = true; - for face in sat.faces().into_iter() { + for (face, face_record_index) in sat.faces().into_iter().zip(face_record_indices) { let Some(surf_rec) = sat.resolve(face.surface()) else { complete = false; continue; @@ -121,6 +166,15 @@ pub fn tessellate_sat_truck( } if !appended { complete = false; + } else { + let material = face_materials.get(&face_record_index).copied(); + triangle_material_handles.extend( + std::iter::repeat(material).take((indices.len() - before) / 3), + ); + let face_color = face_colors.get(&face_record_index).copied(); + triangle_colors.extend( + std::iter::repeat(face_color).take((indices.len() - before) / 3), + ); } } @@ -128,7 +182,16 @@ pub fn tessellate_sat_truck( return None; } - let mesh = finalize_mesh(name, verts, normals, indices, color, body_transform(sat)); + let mesh = finalize_mesh( + name, + verts, + normals, + indices, + triangle_material_handles, + triangle_colors, + color, + body_transform(sat), + ); let mut set = MeshLodSet::from_lods(vec![mesh]); set.complete = complete; Some(set) @@ -166,7 +229,7 @@ fn bespoke_face( tess_torus_face(sat, face, &t, LodConfig::HIGH, v, n, i); } } - "spline-surface" => { + "spline-surface" | "meshsurf-surface" | "bs3-surface" => { crate::scene::convert::spline_tess::tess_spline_face( sat, face, diff --git a/src/scene/convert/solid3d_tess.rs b/src/scene/convert/solid3d_tess.rs index bb7a99b2a..8f4fbd401 100644 --- a/src/scene/convert/solid3d_tess.rs +++ b/src/scene/convert/solid3d_tess.rs @@ -12,12 +12,13 @@ // editing never mistakes them for closed topology. use rustc_hash::FxHashSet as HashSet; +use rustc_hash::FxHashMap; use std::f64::consts::TAU; use acadrust::entities::acis::types::Sense; use acadrust::entities::acis::{ SabReader, SatCoedge, SatConeSurface, SatDocument, SatEdge, SatEllipseCurve, SatFace, - SatIntCurve, SatLoop, SatPlaneSurface, SatPoint, SatPointer, SatSphereSurface, SatToken, + SatIntCurve, SatLoop, SatPlaneSurface, SatPoint, SatPointer, SatSphereSurface, SatTorusSurface, SatVertex, }; use acadrust::entities::{Body, Region, Solid3D}; @@ -127,9 +128,48 @@ fn tessellate_sat( let mut verts: Vec<[f64; 3]> = Vec::new(); let mut normals: Vec<[f32; 3]> = Vec::new(); let mut indices: Vec = Vec::new(); + let face_materials: FxHashMap = sat + .records + .iter() + .filter(|record| record.entity_type == "material-adesk-attrib") + .filter_map(|record| { + let owner = record.token_pointer(2)?.0; + let handle = record.token(3)?.as_integer()?; + (owner >= 0 && handle > 0) + .then(|| (owner, acadrust::Handle::new(handle as u64))) + }) + .collect(); + let face_colors: FxHashMap = sat + .records + .iter() + .filter(|record| record.entity_type == "color-adesk-attrib") + .filter_map(|record| { + let owner = record.token_pointer(2)?.0; + let value = record.token(3)?.as_integer()?; + let color_value = if (1..=255).contains(&value) { + Some(acadrust::Color::from_index(value as i16)) + } else if value > 257 { + Some(acadrust::Color::from_true_color_value(value as i32)) + } else { + None + }?; + let mut rgba = + crate::scene::convert::tess_util::aci_to_rgba(&color_value); + rgba[3] = color[3]; + Some((owner, rgba)) + }) + .collect(); + let mut triangle_material_handles: Vec> = Vec::new(); + let mut triangle_colors: Vec> = Vec::new(); + let face_record_indices: Vec = sat + .records + .iter() + .filter(|record| record.is_a("face")) + .map(|record| record.index) + .collect(); let mut complete = true; - for face in sat.faces() { + for (face, face_record_index) in sat.faces().into_iter().zip(face_record_indices) { let surf_ptr = face.surface(); let Some(surf_rec) = sat.resolve(surf_ptr) else { complete = false; @@ -189,7 +229,7 @@ fn tessellate_sat( ); } } - "spline-surface" => { + "spline-surface" | "meshsurf-surface" | "bs3-surface" => { crate::scene::convert::spline_tess::tess_spline_face( sat, &face, @@ -203,13 +243,31 @@ fn tessellate_sat( } if indices.len() == before { complete = false; + } else { + let material = face_materials.get(&face_record_index).copied(); + triangle_material_handles.extend( + std::iter::repeat(material).take((indices.len() - before) / 3), + ); + let face_color = face_colors.get(&face_record_index).copied(); + triangle_colors.extend( + std::iter::repeat(face_color).take((indices.len() - before) / 3), + ); } } if indices.is_empty() { return None; } Some(( - finalize_mesh(name, verts, normals, indices, color, xform), + finalize_mesh( + name, + verts, + normals, + indices, + triangle_material_handles, + triangle_colors, + color, + xform, + ), complete, )) } @@ -937,34 +995,43 @@ fn tessellate_sat_lods( /// record (`<3×3> rotate reflect shear`). `None` when the /// document has no transform (treated as identity). pub(crate) fn body_transform(sat: &SatDocument) -> Option<([f64; 9], [f64; 3], f64)> { - let t = sat.records.iter().find(|r| r.entity_type == "transform")?; - // The transform record's numeric payload is its first 13 numeric values: - // 3×3 matrix, translation, scale. A leading book-keeping pointer (`$-1`) - // and the trailing rotate/reflect/shear flags aren't numeric, so - // collecting numeric tokens skips them — reading by raw token index would - // be thrown off by the leading pointer. SAT text tokenizes the payload as - // 13 individual floats, but the SAB reader groups the matrix rows and the - // translation into `Position` triplets, so those must be flattened too. - let mut v: Vec = Vec::with_capacity(13); - for tok in &t.tokens { - if v.len() >= 13 { + let transform = sat + .records + .iter() + .find(|record| record.entity_type == "transform")?; + let mut values = Vec::with_capacity(13); + for token in &transform.tokens { + if values.len() >= 13 { break; } - match tok { - SatToken::Position(x, y, z) => v.extend([*x, *y, *z]), - _ => { - if let Some(f) = tok.as_float() { - v.push(f); + if let Some((components, len)) = token.coordinate_components() { + values.extend_from_slice(&components[..len]); + } else if let Some(value) = token.as_float() { + values.push(value); + } else if let Some(text) = token.as_string() { + // Some ASM SAB bodies pack the complete SAT transform payload in + // one long-string token instead of individual numeric tokens. + for word in text.split_ascii_whitespace() { + let Ok(value) = word.parse::() else { + break; + }; + values.push(value); + if values.len() >= 13 { + break; } } } } - if v.len() < 13 { - return None; - } - let m = [v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], v[8]]; - let tr = [v[9], v[10], v[11]]; - Some((m, tr, v[12])) + (values.len() >= 13).then(|| { + ( + [ + values[0], values[1], values[2], values[3], values[4], values[5], values[6], + values[7], values[8], + ], + [values[9], values[10], values[11]], + values[12], + ) + }) } /// Apply a body placement transform to a mesh. ACIS treats points as row @@ -985,6 +1052,8 @@ pub(crate) fn finalize_mesh( verts: Vec<[f64; 3]>, normals: Vec<[f32; 3]>, indices: Vec, + triangle_material_handles: Vec>, + triangle_colors: Vec>, color: [f32; 4], xform: Option<([f64; 9], [f64; 3], f64)>, ) -> MeshModel { @@ -1031,6 +1100,8 @@ pub(crate) fn finalize_mesh( verts_low: lo, normals, indices, + triangle_material_handles, + triangle_colors, color, selected: false, } @@ -1088,6 +1159,76 @@ fn parse_acis( None } +fn remap_acis_material_bindings( + set: &mut MeshLodSet, + acis: &acadrust::entities::AcisData, +) { + if acis.materials.is_empty() { + return; + } + for lod in &mut set.lods { + for handle in lod.triangle_material_handles.iter_mut().flatten() { + let reference = handle.value() as i32; + if let Some(material_handle) = acis.materials.iter().find_map(|binding| { + (binding.absolute_reference == reference || binding.array_index == reference) + .then_some(binding.material_handle) + .flatten() + }) { + *handle = material_handle; + } + } + } +} + +fn attach_stored_silhouettes( + set: &mut MeshLodSet, + silhouettes: &[acadrust::entities::Silhouette], +) { + use crate::scene::model::mesh_model::StoredSilhouette; + set.stored_silhouettes = silhouettes + .iter() + .filter_map(|silhouette| { + let mut edge_verts = Vec::new(); + let mut edge_verts_low = Vec::new(); + for wire in &silhouette.wires { + for segment in wire.points.windows(2) { + for point in segment { + let point = crate::entities::solid3d::wire_point(wire, point); + let high = [point.x as f32, point.y as f32, point.z as f32]; + edge_verts.push(high); + edge_verts_low.push([ + (point.x - high[0] as f64) as f32, + (point.y - high[1] as f64) as f32, + (point.z - high[2] as f64) as f32, + ]); + } + } + } + (!edge_verts.is_empty()).then_some(StoredSilhouette { + viewport_id: silhouette.viewport_id, + view_direction: [ + silhouette.view_direction.x as f32, + silhouette.view_direction.y as f32, + silhouette.view_direction.z as f32, + ], + up_vector: [ + silhouette.up_vector.x as f32, + silhouette.up_vector.y as f32, + silhouette.up_vector.z as f32, + ], + target: [ + silhouette.target.x as f32, + silhouette.target.y as f32, + silhouette.target.z as f32, + ], + is_perspective: silhouette.is_perspective, + edge_verts, + edge_verts_low, + }) + }) + .collect(); +} + /// Tessellate a `Region` entity (2D planar ACIS body) at all three LOD levels. pub fn tessellate_region( region: &Region, @@ -1101,7 +1242,10 @@ pub fn tessellate_region( ®ion.acis_data.sab_data, )?; let name = region.common.handle.value().to_string(); - tessellate_acis(&sat, name, color, facet_res, isolines) + let mut set = tessellate_acis(&sat, name, color, facet_res, isolines)?; + remap_acis_material_bindings(&mut set, ®ion.acis_data); + attach_stored_silhouettes(&mut set, ®ion.silhouettes); + Some(set) } /// Tessellate a `Body` entity (3D ACIS body) at all three LOD levels. @@ -1117,7 +1261,10 @@ pub fn tessellate_body( &body.acis_data.sab_data, )?; let name = body.common.handle.value().to_string(); - tessellate_acis(&sat, name, color, facet_res, isolines) + let mut set = tessellate_acis(&sat, name, color, facet_res, isolines)?; + remap_acis_material_bindings(&mut set, &body.acis_data); + attach_stored_silhouettes(&mut set, &body.silhouettes); + Some(set) } /// Tessellate a `Surface` entity (ACAD_SURFACE family) at all three LOD @@ -1136,7 +1283,12 @@ pub fn tessellate_surface( &surface.acis_data.sab_data, )?; let name = surface.common.handle.value().to_string(); - tessellate_acis(&sat, name, color, facet_res, isolines) + let surface_isolines = usize::from(surface.u_isolines.max(surface.v_isolines).max(0) as u16); + let mut set = + tessellate_acis(&sat, name, color, facet_res, surface_isolines.max(isolines))?; + remap_acis_material_bindings(&mut set, &surface.acis_data); + attach_stored_silhouettes(&mut set, &surface.silhouettes); + Some(set) } /// Tessellate a `Solid3D` entity at all three LOD levels. @@ -1156,7 +1308,10 @@ pub fn tessellate_solid3d( &solid.acis_data.sab_data, )?; let name = solid.common.handle.value().to_string(); - tessellate_acis(&sat, name, color, facet_res, isolines) + let mut set = tessellate_acis(&sat, name, color, facet_res, isolines)?; + remap_acis_material_bindings(&mut set, &solid.acis_data); + attach_stored_silhouettes(&mut set, &solid.silhouettes); + Some(set) } // ── Topology helpers ────────────────────────────────────────────────────────── diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index 25fa64adb..b186f9e5e 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -357,6 +357,8 @@ pub(crate) fn tessellate_entity( // by the viewport's GPU uniform so it never changes resident wire content. paper_space: bool, ) -> Vec { + let contextual = crate::scene::annotative::entity_for_active_context(document, e); + let e = contextual.as_ref(); let h = e.common().handle; let sel = selected.contains(&h); @@ -518,9 +520,22 @@ pub(crate) fn tessellate_entity( // blob, the vector preview its author cached for exactly this case. AutoCAD // draws that when the object enabler is missing; draw it too, so the entity // occupies its real place instead of silently disappearing. - if let EntityType::Unknown(_) = e { - if let Some(blob) = e.common().graphic_data.as_ref() { - let dec = convert::proxy_graphics::decode(blob); + let proxy_blob: Option> = match e { + EntityType::Unknown(_) => e + .common() + .graphic_data + .as_deref() + .map(std::borrow::Cow::Borrowed), + EntityType::Extended(entity) => match &entity.data { + acadrust::entities::ExtendedEntityData::Proxy(proxy) => { + Some(std::borrow::Cow::Owned(proxy.graphics.data())) + } + _ => None, + }, + _ => None, + }; + if let Some(blob) = proxy_blob { + let dec = convert::proxy_graphics::decode(blob.as_ref()); if !dec.polylines.is_empty() || !dec.texts.is_empty() { use crate::scene::convert::proxy_graphics::ProxyColor; use std::collections::BTreeMap; @@ -618,7 +633,6 @@ pub(crate) fn tessellate_entity( return wires; } } - } } // ── Section symbol (AcDbSectionSymbol): draw the "A-A" cut mark ────────── @@ -744,10 +758,13 @@ pub(crate) fn tessellate_entity( // Sub-entities inside *D### / DIMBLOCK## blocks // typically use ByBlock color/linetype/lineweight — // they should inherit from the Dimension entity. - let sub_color_is_byblock = - sub.common().color == acadrust::types::Color::ByBlock; + let has_book_color = + view::render::has_resolved_book_color(document, sub); + let sub_color_is_byblock = !has_book_color + && sub.common().color == acadrust::types::Color::ByBlock; let sub_is_l0_bylayer = - view::render::is_effective_layer_zero(&sub.common().layer) + !has_book_color + && view::render::is_effective_layer_zero(&sub.common().layer) && sub.common().color == acadrust::types::Color::ByLayer; let sub_wires = tessellate_entity( document, @@ -898,10 +915,13 @@ pub(crate) fn tessellate_entity( let Some(sub) = document.get_entity(eh) else { continue; }; - let sub_color_is_byblock = - sub.common().color == acadrust::types::Color::ByBlock; + let has_book_color = + view::render::has_resolved_book_color(document, sub); + let sub_color_is_byblock = !has_book_color + && sub.common().color == acadrust::types::Color::ByBlock; let sub_is_l0_bylayer = - view::render::is_effective_layer_zero(&sub.common().layer) + !has_book_color + && view::render::is_effective_layer_zero(&sub.common().layer) && sub.common().color == acadrust::types::Color::ByLayer; let mut placed = sub.clone(); { @@ -976,6 +996,22 @@ pub(crate) fn tessellate_entity( line_weight_px, table_anno, ); + for insert in + crate::entities::table::block_cell_inserts(tab, document, table_anno) + { + wires.extend(tessellate_entity( + document, + selected, + active_viewport, + bg_color, + 1.0, + &EntityType::Insert(insert), + block_cache, + view_aabb, + world_per_pixel, + paper_space, + )); + } if !wires.is_empty() { let aabb = entity_aabb(e); for w in &mut wires { diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index da19cb431..513c44477 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -1723,7 +1723,8 @@ fn solid_wire_fallback(entity: &EntityType) -> Vec<[f64; 3]> { continue; } for v in &wire.points { - pts.push([v.x, v.y, v.z]); + let transformed = crate::entities::solid3d::wire_point(wire, v); + pts.push([transformed.x, transformed.y, transformed.z]); } // NaN sentinel separates distinct wire segments. pts.push([f64::NAN, f64::NAN, f64::NAN]); diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 2a39f7cfa..898508a12 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -140,7 +140,13 @@ impl Scene { let isolines = self.document.header.isolines.max(0) as usize; let mesh_seed = if matches!( &entity, - EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) | EntityType::Surface(_) + EntityType::Solid3D(_) + | EntityType::Region(_) + | EntityType::Body(_) + | EntityType::Surface(_) + | EntityType::Mesh(_) + | EntityType::PolygonMesh(_) + | EntityType::PolyfaceMesh(_) ) { let color = self.render_style(&entity).0; crate::entities::solid3d::tessellate_volume(&entity, color, facet_res, isolines) @@ -206,7 +212,28 @@ impl Scene { if let Some(model) = image_seed { self.images.insert(handle, model); } - if let Some(model) = mesh_seed { + if let Some(mut model) = mesh_seed { + if let Some(entity) = self.document.get_entity(handle) { + let color = self.render_style(entity).0; + let material = + crate::scene::model::material_model::resolve_material_with_base( + &self.document, + entity, + color, + None, + self.material_base_dir.as_deref(), + ); + material.apply_to_with_face_overrides( + &mut model, + &self.document, + self.material_base_dir.as_deref(), + ); + crate::scene::model::visual_style_model::apply_mesh_visual_style( + &mut model, + &self.document, + entity, + ); + } self.meshes.insert(handle, model); } // Delta-undo: the new handle's before-image is "nothing" (it did not @@ -330,7 +357,13 @@ impl Scene { let isolines = self.document.header.isolines.max(0) as usize; let mesh_seed = if matches!( &entity, - EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) | EntityType::Surface(_) + EntityType::Solid3D(_) + | EntityType::Region(_) + | EntityType::Body(_) + | EntityType::Surface(_) + | EntityType::Mesh(_) + | EntityType::PolygonMesh(_) + | EntityType::PolyfaceMesh(_) ) { let color = self.render_style(&entity).0; crate::entities::solid3d::tessellate_volume(&entity, color, facet_res, isolines) @@ -369,7 +402,28 @@ impl Scene { if let Some(model) = image_seed { self.images.insert(handle, model); } - if let Some(model) = mesh_seed { + if let Some(mut model) = mesh_seed { + if let Some(entity) = self.document.get_entity(handle) { + let color = self.render_style(entity).0; + let material = + crate::scene::model::material_model::resolve_material_with_base( + &self.document, + entity, + color, + None, + self.material_base_dir.as_deref(), + ); + material.apply_to_with_face_overrides( + &mut model, + &self.document, + self.material_base_dir.as_deref(), + ); + crate::scene::model::visual_style_model::apply_mesh_visual_style( + &mut model, + &self.document, + entity, + ); + } self.meshes.insert(handle, model); } @@ -448,6 +502,9 @@ impl Scene { | EntityType::Region(_) | EntityType::Body(_) | EntityType::Surface(_) + | EntityType::Mesh(_) + | EntityType::PolygonMesh(_) + | EntityType::PolyfaceMesh(_) ) { return None; } @@ -473,7 +530,24 @@ impl Scene { facet_res, isolines, ) - .map(|mesh| { + .map(|mut mesh| { + crate::scene::model::material_model::resolve_material_with_base( + &self.document, + entity.as_ref(), + color, + None, + self.material_base_dir.as_deref(), + ) + .apply_to_with_face_overrides( + &mut mesh, + &self.document, + self.material_base_dir.as_deref(), + ); + crate::scene::model::visual_style_model::apply_mesh_visual_style( + &mut mesh, + &self.document, + entity.as_ref(), + ); let mesh = if top_level { offset_mesh_lod_set(mesh) } else { @@ -758,8 +832,29 @@ impl Scene { )) }) .flat_map(|(&handle, model)| { - let entity = self.document.get_entity(handle); - let mut m = model.clone(); + let contextual = self + .document + .get_entity(handle) + .map(|entity| { + crate::scene::annotative::entity_for_active_context( + &self.document, + entity, + ) + }); + let entity = contextual.as_deref(); + let mut m = match entity { + Some(EntityType::Hatch(dxf)) + if crate::scene::annotative::active_object_context( + &self.document, + handle, + ) + .is_some() => + { + Self::hatch_model_from_dxf(dxf, model.color) + .unwrap_or_else(|| model.clone()) + } + _ => model.clone(), + }; // Optional solid backdrop drawn behind the pattern/gradient when // the hatch carries a HATCHBACKGROUNDCOLOR. Same draw_depth + // emitted first so LessEqual layering keeps it underneath. @@ -958,7 +1053,9 @@ impl Scene { out } for entity in self.document.entities() { - let EntityType::Insert(ins) = entity else { + let contextual = + crate::scene::annotative::entity_for_active_context(&self.document, entity); + let EntityType::Insert(ins) = contextual.as_ref() else { continue; }; if ins.common.invisible || layer_hidden(&ins.common.layer) { @@ -1034,6 +1131,10 @@ impl Scene { )> = explode_including_dims(ins, &self.document) .into_iter() .filter(|e| !offscale(e)) + .map(|e| { + crate::scene::annotative::entity_for_active_context(&self.document, &e) + .into_owned() + }) .map(|e| (normalize(e), 0usize, ins_color, l0, (base_depth, half_gap))) .collect(); while let Some((sub, depth, sub_ins_color, sub_l0, (d_base, d_half))) = stack.pop() { @@ -1050,14 +1151,25 @@ impl Scene { let on_l0 = crate::scene::view::render::is_effective_layer_zero( &nins.common.layer, ); - let child_ins_color = if nins.common.color == Color::ByBlock { + let nested_entity = EntityType::Insert(nins.clone()); + let has_book_color = + crate::scene::view::render::has_resolved_book_color( + &self.document, + &nested_entity, + ); + let child_ins_color = if !has_book_color + && nins.common.color == Color::ByBlock + { sub_ins_color - } else if on_l0 && nins.common.color == Color::ByLayer { + } else if !has_book_color + && on_l0 + && nins.common.color == Color::ByLayer + { sub_l0.color } else { crate::scene::view::render::render_style_for( &self.document, - &EntityType::Insert(nins.clone()), + &nested_entity, ) .0 }; @@ -1081,6 +1193,12 @@ impl Scene { if offscale(&e) { continue; } + let e = + crate::scene::annotative::entity_for_active_context( + &self.document, + &e, + ) + .into_owned(); stack.push(( normalize(e), depth + 1, @@ -1235,7 +1353,9 @@ impl Scene { // apply_rotation) rather than through Insert::explode — the latter // double-scales the u/v basis. for entity in self.document.entities() { - let EntityType::Insert(ins) = entity else { + let contextual = + crate::scene::annotative::entity_for_active_context(&self.document, entity); + let EntityType::Insert(ins) = contextual.as_ref() else { continue; }; let c = &ins.common; @@ -1927,7 +2047,13 @@ impl Scene { /// at load, so a pattern-scale / background / boundary edit stays /// invisible until the cached model is refreshed (#415). pub fn refresh_fill_model(&mut self, handle: Handle) { - let new_model = match self.document.get_entity(handle) { + let contextual = self + .document + .get_entity(handle) + .map(|entity| { + crate::scene::annotative::entity_for_active_context(&self.document, entity) + }); + let new_model = match contextual.as_deref() { Some(EntityType::Hatch(dxf)) => { let color = convert::tess_util::aci_to_rgba(&dxf.common.color); Self::hatch_model_from_dxf(dxf, color) @@ -1955,7 +2081,11 @@ impl Scene { .document .entities() .filter_map(|e| match e { - EntityType::Hatch(h) => Some((h.common.handle, e.clone())), + EntityType::Hatch(h) => Some(( + h.common.handle, + crate::scene::annotative::entity_for_active_context(&self.document, e) + .into_owned(), + )), EntityType::Solid(s) => Some((s.common.handle, e.clone())), _ => None, }) @@ -2047,7 +2177,13 @@ impl Scene { .document .entities() .filter_map(|e| match e { - EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) | EntityType::Surface(_) => { + EntityType::Solid3D(_) + | EntityType::Region(_) + | EntityType::Body(_) + | EntityType::Surface(_) + | EntityType::Mesh(_) + | EntityType::PolygonMesh(_) + | EntityType::PolyfaceMesh(_) => { let handle = e.common().handle; // Incremental (post-xref) pass: leave already-tessellated // host solids untouched, only build the newly merged ones. @@ -2073,9 +2209,26 @@ impl Scene { let built: Vec<(Handle, MeshLodSet, bool)> = entries .into_par_iter() .filter_map(|(handle, entity, color, top_level)| { - crate::entities::solid3d::tessellate_volume(&entity, color, facet_res, isolines).map(|m| { - let m = if top_level { offset_mesh_lod_set(m) } else { m }; - (handle, m, top_level) + crate::entities::solid3d::tessellate_volume(&entity, color, facet_res, isolines).map(|mut mesh| { + let material = crate::scene::model::material_model::resolve_material_with_base( + &self.document, + &entity, + color, + None, + self.material_base_dir.as_deref(), + ); + material.apply_to_with_face_overrides( + &mut mesh, + &self.document, + self.material_base_dir.as_deref(), + ); + crate::scene::model::visual_style_model::apply_mesh_visual_style( + &mut mesh, + &self.document, + &entity, + ); + let mesh = if top_level { offset_mesh_lod_set(mesh) } else { mesh }; + (handle, mesh, top_level) }) }) .collect(); diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 29de53fb7..5d1bf52f9 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -417,7 +417,7 @@ pub struct OpenTimings { /// Intended to run on a background thread during file load. #[cfg(target_arch = "wasm32")] pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches { - build_derived_caches_impl(doc, None) + build_derived_caches_impl(doc, None, None) } /// Build open-time caches while reporting monotonic progress in 0..=10000. @@ -427,13 +427,15 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches { pub fn build_derived_caches_with_progress( doc: &CadDocument, progress: &(dyn Fn(u16) + Sync), + material_base_dir: Option<&std::path::Path>, ) -> DerivedCaches { - build_derived_caches_impl(doc, Some(progress)) + build_derived_caches_impl(doc, Some(progress), material_base_dir) } fn build_derived_caches_impl( doc: &CadDocument, progress: Option<&(dyn Fn(u16) + Sync)>, + material_base_dir: Option<&std::path::Path>, ) -> DerivedCaches { // A new drawing must not inherit the previous one's resolved images — drop // the memoised set so each reference re-reads / re-fetches once here (and @@ -500,7 +502,10 @@ fn build_derived_caches_impl( EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) - | EntityType::Surface(_) => mesh_handles.push(h), + | EntityType::Surface(_) + | EntityType::Mesh(_) + | EntityType::PolygonMesh(_) + | EntityType::PolyfaceMesh(_) => mesh_handles.push(h), _ => {} } if let Some(c) = offset_centroid(e, model_block, &prep) { @@ -540,7 +545,9 @@ fn build_derived_caches_impl( let hatches: HashMap = hatch_handles .par_iter() .filter_map(|&handle| { - let e = doc.get_entity(handle)?; + let source = doc.get_entity(handle)?; + let contextual = annotative::entity_for_active_context(doc, source); + let e = contextual.as_ref(); let (raw, ..) = view::render::render_style_for(doc, e); let color = view::render::adapt_to_bg(raw, LOAD_BG); let model = match e { @@ -607,11 +614,28 @@ fn build_derived_caches_impl( let e = doc.get_entity(handle)?; let (raw, ..) = view::render::render_style_for(doc, e); let color = view::render::adapt_to_bg(raw, LOAD_BG); + let material = crate::scene::model::material_model::resolve_material_with_base( + doc, + e, + color, + None, + material_base_dir, + ); let top_level = layout_blocks.contains(&e.common().owner_handle); let result = crate::entities::solid3d::tessellate_volume(e, color, facet_res, isolines) - .map(|m| { - let m = if top_level { offset_mesh_lod_set(m) } else { m }; - (handle, m, top_level) + .map(|mut mesh| { + material.apply_to_with_face_overrides( + &mut mesh, + doc, + material_base_dir, + ); + crate::scene::model::visual_style_model::apply_mesh_visual_style( + &mut mesh, + doc, + e, + ); + let mesh = if top_level { offset_mesh_lod_set(mesh) } else { mesh }; + (handle, mesh, top_level) }); let done = detail_done.fetch_add(1, Ordering::Relaxed) + 1; if done & 0xff == 0 || done == detail_total { @@ -1007,12 +1031,104 @@ fn transform_block_mesh_lod_set( ) -> MeshLodSet { use acadrust::types::Vector3; let mut out = set.clone(); - // Silhouette generators are world-space and untransformed here, so a block - // instance would silhouette against the block-local pose. Drop them: a - // block-internal solid keeps its baked isolines and feature edges, just not - // the per-frame silhouette (deferred until the generators track the INSTANCE - // transform). - out.curved_gens.clear(); + let transform_direction = |direction: [f32; 3]| { + let transformed = xform.apply_rotation(Vector3::new( + direction[0] as f64, + direction[1] as f64, + direction[2] as f64, + )); + let length = transformed.length(); + if length > 1e-12 { + [ + (transformed.x / length) as f32, + (transformed.y / length) as f32, + (transformed.z / length) as f32, + ] + } else { + direction + } + }; + let scale_x = xform.apply_rotation(Vector3::UNIT_X).length(); + let scale_y = xform.apply_rotation(Vector3::UNIT_Y).length(); + let scale_z = xform.apply_rotation(Vector3::UNIT_Z).length(); + let uniform_scale = (scale_x + scale_y + scale_z) / 3.0; + let is_uniform = (scale_x - uniform_scale).abs() <= uniform_scale.abs().max(1.0) * 1e-8 + && (scale_y - uniform_scale).abs() <= uniform_scale.abs().max(1.0) * 1e-8 + && (scale_z - uniform_scale).abs() <= uniform_scale.abs().max(1.0) * 1e-8; + if is_uniform { + let transform_split = |high: &mut [f32; 3], low: &mut [f32; 3]| { + let transformed = xform.apply(Vector3::new( + high[0] as f64 + low[0] as f64, + high[1] as f64 + low[1] as f64, + high[2] as f64 + low[2] as f64, + )); + *high = [ + transformed.x as f32, + transformed.y as f32, + transformed.z as f32, + ]; + *low = [ + (transformed.x - high[0] as f64) as f32, + (transformed.y - high[1] as f64) as f32, + (transformed.z - high[2] as f64) as f32, + ]; + }; + for generator in &mut out.curved_gens { + match generator { + crate::scene::model::mesh_model::CurvedGen::Cone { + base, + base_low, + axis, + u_dir, + v_dir, + radius, + h_max, + .. + } => { + transform_split(base, base_low); + *axis = transform_direction(*axis); + *u_dir = transform_direction(*u_dir); + *v_dir = transform_direction(*v_dir); + *radius *= uniform_scale as f32; + *h_max *= uniform_scale as f32; + } + crate::scene::model::mesh_model::CurvedGen::Sphere { + center, + center_low, + pole, + u_dir, + v_dir, + radius, + .. + } => { + transform_split(center, center_low); + *pole = transform_direction(*pole); + *u_dir = transform_direction(*u_dir); + *v_dir = transform_direction(*v_dir); + *radius *= uniform_scale as f32; + } + crate::scene::model::mesh_model::CurvedGen::Torus { + center, + center_low, + axis, + u_dir, + v_dir, + major, + minor, + .. + } => { + transform_split(center, center_low); + *axis = transform_direction(*axis); + *u_dir = transform_direction(*u_dir); + *v_dir = transform_direction(*v_dir); + *major *= uniform_scale as f32; + *minor *= uniform_scale as f32; + } + } + } + } else { + out.curved_gens.clear(); + } let mut min_x = f32::INFINITY; let mut min_y = f32::INFINITY; let mut max_x = f32::NEG_INFINITY; @@ -1086,6 +1202,41 @@ fn transform_block_mesh_lod_set( ]; } } + for silhouette in &mut out.stored_silhouettes { + silhouette.view_direction = transform_direction(silhouette.view_direction); + silhouette.up_vector = transform_direction(silhouette.up_vector); + let target = xform.apply(Vector3::new( + silhouette.target[0] as f64, + silhouette.target[1] as f64, + silhouette.target[2] as f64, + )); + silhouette.target = [target.x as f32, target.y as f32, target.z as f32]; + let count = silhouette.edge_verts.len(); + if silhouette.edge_verts_low.len() != count { + silhouette.edge_verts_low = vec![[0.0; 3]; count]; + } + for (high, low) in silhouette + .edge_verts + .iter_mut() + .zip(silhouette.edge_verts_low.iter_mut()) + { + let transformed = xform.apply(Vector3::new( + high[0] as f64 + low[0] as f64, + high[1] as f64 + low[1] as f64, + high[2] as f64 + low[2] as f64, + )); + *high = [ + transformed.x as f32, + transformed.y as f32, + transformed.z as f32, + ]; + *low = [ + (transformed.x - high[0] as f64) as f32, + (transformed.y - high[1] as f64) as f32, + (transformed.z - high[2] as f64) as f32, + ]; + } + } if min_x.is_finite() { out.world_aabb = [min_x, min_y, max_x, max_y]; } @@ -1120,6 +1271,14 @@ pub(in crate::scene) struct NavPerfSample { pub(in crate::scene) build_ms: f64, } +#[derive(Clone)] +struct BlockMeshInherit { + insert_color: [f32; 4], + layer0_color: [f32; 4], + insert_material: crate::scene::model::material_model::MeshMaterial, + layer0_material: crate::scene::model::material_model::MeshMaterial, +} + pub struct Scene { pub camera: Rc>, /// Model-space tiled viewport layout. One full-window tile by default; @@ -1362,6 +1521,9 @@ pub struct Scene { /// Top-level (layout-owned) solids only, stored in the offset-relative /// render frame and drawn flat. pub meshes: HashMap, + /// Directory of the opened drawing, used to resolve relative AcDbMaterial + /// bitmap maps. `None` for unsaved drawings and browser uploads. + pub material_base_dir: Option, /// Meshes of block-definition solids, kept in *block-local* coordinates /// (no world_offset). They are not drawn directly; each INSERT of the /// owning block emits a transformed instance so a block placed at an @@ -1595,6 +1757,7 @@ impl Scene { viewcube_ucs: glam::Mat4::IDENTITY, hatches: HashMap::default(), meshes: HashMap::default(), + material_base_dir: None, block_meshes: HashMap::default(), solid_models: HashMap::default(), images: HashMap::default(), @@ -2100,12 +2263,30 @@ impl Scene { /// in the same offset-relative frame the mesh pipeline uses, so the mesh is /// stored as-is (Model-tab geometry is authored at world_offset 0). pub fn register_solid_model(&mut self, handle: Handle, solid: truck_modeling::Solid) { - let color = self - .document - .get_entity(handle) + let entity = self.document.get_entity(handle); + let color = entity .map(|e| self.render_style(e).0) .unwrap_or([0.8, 0.8, 0.85, 1.0]); - if let Some(set) = crate::scene::model::solid_model::mesh_from_solid(&solid, color) { + if let Some(mut set) = crate::scene::model::solid_model::mesh_from_solid(&solid, color) { + if let Some(entity) = entity { + crate::scene::model::material_model::resolve_material_with_base( + &self.document, + entity, + color, + None, + self.material_base_dir.as_deref(), + ) + .apply_to_with_face_overrides( + &mut set, + &self.document, + self.material_base_dir.as_deref(), + ); + crate::scene::model::visual_style_model::apply_mesh_visual_style( + &mut set, + &self.document, + entity, + ); + } self.meshes.insert(handle, set); } self.solid_models.insert(handle, solid); @@ -2125,27 +2306,47 @@ impl Scene { // (instanced per INSERT), so a solid recolours wherever it lives. // During REFEDIT, solids outside the edited set render faded. let bg = self.bg_color; - let colors: HashMap = self + let materials: HashMap = self .meshes .keys() .chain(self.block_meshes.keys()) .filter_map(|&h| { self.document.get_entity(h).map(|e| { - let mut c = self.render_style(e).0; + let color = self.render_style(e).0; + let mut material = + crate::scene::model::material_model::resolve_material_with_base( + &self.document, + e, + color, + None, + self.material_base_dir.as_deref(), + ); if let Some(keep) = &self.refedit_keep { if !keep.contains(&h) { - c = crate::scene::cache::block_cache::fade_toward_bg(c, bg); + material.diffuse = crate::scene::cache::block_cache::fade_toward_bg( + material.diffuse, + bg, + ); } } - (h, c) + (h, material) }) }) .collect(); for (h, set) in self.meshes.iter_mut().chain(self.block_meshes.iter_mut()) { - if let Some(&c) = colors.get(h) { - for lod in &mut set.lods { - lod.color = c; - } + if let Some(material) = materials.get(h) { + material.apply_to_with_face_overrides( + set, + &self.document, + self.material_base_dir.as_deref(), + ); + } + if let Some(entity) = self.document.get_entity(*h) { + crate::scene::model::visual_style_model::apply_mesh_visual_style( + set, + &self.document, + entity, + ); } } } @@ -2153,33 +2354,62 @@ impl Scene { /// Recolour only the named cached solids after a property edit. pub fn recolor_meshes_for_handles(&mut self, handles: &[Handle]) { let bg = self.bg_color; - let colors: HashMap = handles + let materials: HashMap = handles .iter() .filter_map(|&handle| { self.document.get_entity(handle).map(|entity| { - let mut color = self.render_style(entity).0; + let color = self.render_style(entity).0; + let mut material = + crate::scene::model::material_model::resolve_material_with_base( + &self.document, + entity, + color, + None, + self.material_base_dir.as_deref(), + ); if self .refedit_keep .as_ref() .is_some_and(|keep| !keep.contains(&handle)) { - color = crate::scene::cache::block_cache::fade_toward_bg(color, bg); + material.diffuse = crate::scene::cache::block_cache::fade_toward_bg( + material.diffuse, + bg, + ); } - (handle, color) + (handle, material) }) }) .collect(); for handle in handles { - let Some(color) = colors.get(handle) else { + let Some(material) = materials.get(handle) else { continue; }; if let Some(set) = self.meshes.get_mut(handle) { - for lod in &mut set.lods { - lod.color = *color; + material.apply_to_with_face_overrides( + set, + &self.document, + self.material_base_dir.as_deref(), + ); + if let Some(entity) = self.document.get_entity(*handle) { + crate::scene::model::visual_style_model::apply_mesh_visual_style( + set, + &self.document, + entity, + ); } } else if let Some(set) = self.block_meshes.get_mut(handle) { - for lod in &mut set.lods { - lod.color = *color; + material.apply_to_with_face_overrides( + set, + &self.document, + self.material_base_dir.as_deref(), + ); + if let Some(entity) = self.document.get_entity(*handle) { + crate::scene::model::visual_style_model::apply_mesh_visual_style( + set, + &self.document, + entity, + ); } } } @@ -3996,6 +4226,9 @@ impl Scene { | EntityType::Region(_) | EntityType::Body(_) | EntityType::Surface(_) + | EntityType::Mesh(_) + | EntityType::PolygonMesh(_) + | EntityType::PolyfaceMesh(_) ) { continue; } @@ -4073,6 +4306,9 @@ impl Scene { | EntityType::Region(_) | EntityType::Body(_) | EntityType::Surface(_) + | EntityType::Mesh(_) + | EntityType::PolygonMesh(_) + | EntityType::PolyfaceMesh(_) ) { continue; } @@ -4677,7 +4913,10 @@ impl Scene { return Vec::new(); } let mut out = Vec::new(); - for e in self.document.entities() { + for source in self.document.entities() { + let contextual = + crate::scene::annotative::entity_for_active_context(&self.document, source); + let e = contextual.as_ref(); if e.common().owner_handle != layout_block { continue; } @@ -4704,12 +4943,31 @@ impl Scene { .color, bg, ); + let inherit = BlockMeshInherit { + insert_color: ins_color, + layer0_color: l0, + insert_material: + crate::scene::model::material_model::resolve_material_with_base( + &self.document, + e, + ins_color, + None, + self.material_base_dir.as_deref(), + ), + layer0_material: + crate::scene::model::material_model::resolve_layer_material_with_base( + &self.document, + &ins.common.layer, + l0, + self.material_base_dir.as_deref(), + ), + }; let start = out.len(); self.expand_block_meshes( &ins.block_name, &ins.get_transform(), 0, - Some((ins_color, l0)), + Some(inherit), &mut out, ); // Tag the instanced meshes with the parent INSERT handle so the @@ -4734,10 +4992,9 @@ impl Scene { accum: &acadrust::types::Transform, depth: usize, // Block-child colour inheritance sources, bg-adapted: - // `(insert_color, insert_layer_color)`. `Some` only on the render path; - // pick paths pass `None` (colour irrelevant). Drives the ByBlock / - // layer-0 overrides for block-internal solids (#221). - inherit: Option<([f32; 4], [f32; 4])>, + // `Some` only on the render path; pick paths pass `None`. Carries both + // colour and AcDbMaterial inheritance for ByBlock/layer-0 children. + inherit: Option, out: &mut Vec, ) { if depth > 16 { @@ -4748,9 +5005,12 @@ impl Scene { }; let handles: Vec = br.entity_handles.clone(); for h in handles { - let Some(e) = self.document.get_entity(h) else { + let Some(source) = self.document.get_entity(h) else { continue; }; + let contextual = + crate::scene::annotative::entity_for_active_context(&self.document, source); + let e = contextual.as_ref(); // A block-internal solid / nested INSERT on an off/frozen layer // (or flagged invisible) must not render, same as a top-level one. if !self.mesh_entity_visible(h) { @@ -4758,7 +5018,9 @@ impl Scene { } if let EntityType::Insert(ins) = e { let composed = ins.get_transform().then(accum); - let child = inherit.map(|(pc, pl0)| self.chain_mesh_inherit(ins, pc, pl0)); + let child = inherit + .as_ref() + .map(|parent| self.chain_mesh_inherit(ins, parent)); self.expand_block_meshes(&ins.block_name, &composed, depth + 1, child, out); } else if let Some(set) = self.block_meshes.get(&h) { // The solid's own transparency (baked into the cached colour). @@ -4767,10 +5029,26 @@ impl Scene { // Re-resolve colour against the INSERT context: a block-internal // solid that is ByBlock or on layer "0" + ByLayer can't be // coloured at cache-build time (no insert context there). (#221) - if let Some(c) = self.block_mesh_override_color(e, h, inherit, own_alpha) { + if let Some(c) = + self.block_mesh_override_color(e, h, inherit.as_ref(), own_alpha) + { for lod in &mut ts.lods { lod.color = c; } + if let Some(material) = ts.material.as_mut() { + if material.handle.is_none() { + material.diffuse = c; + } + } + } + if let Some(material) = + self.block_mesh_override_material(e, inherit.as_ref()) + { + material.apply_to_with_face_overrides( + &mut ts, + &self.document, + self.material_base_dir.as_deref(), + ); } out.push(ts); } @@ -4793,16 +5071,21 @@ impl Scene { fn chain_mesh_inherit( &self, ins: &acadrust::entities::Insert, - parent_ins_color: [f32; 4], - parent_l0: [f32; 4], - ) -> ([f32; 4], [f32; 4]) { + parent: &BlockMeshInherit, + ) -> BlockMeshInherit { use acadrust::types::Color; let bg = self.current_bg(); let on_l0 = crate::scene::view::render::is_effective_layer_zero(&ins.common.layer); - let child_ins_color = if ins.common.color == Color::ByBlock { - parent_ins_color - } else if on_l0 && ins.common.color == Color::ByLayer { - parent_l0 + let insert_entity = EntityType::Insert(ins.clone()); + let has_book_color = + crate::scene::view::render::has_resolved_book_color( + &self.document, + &insert_entity, + ); + let child_ins_color = if !has_book_color && ins.common.color == Color::ByBlock { + parent.insert_color + } else if !has_book_color && on_l0 && ins.common.color == Color::ByLayer { + parent.layer0_color } else { crate::scene::view::render::adapt_to_bg( crate::scene::view::render::render_style_for( @@ -4814,7 +5097,7 @@ impl Scene { ) }; let child_l0 = if on_l0 { - parent_l0 + parent.layer0_color } else { crate::scene::view::render::adapt_to_bg( crate::scene::view::render::layer_render_style(&self.document, &ins.common.layer) @@ -4822,7 +5105,35 @@ impl Scene { bg, ) }; - (child_ins_color, child_l0) + let child_insert_material = if ins.common.material_flags == 1 { + parent.insert_material.clone() + } else if on_l0 && ins.common.material_flags == 0 { + parent.layer0_material.clone() + } else { + crate::scene::model::material_model::resolve_material_with_base( + &self.document, + &insert_entity, + child_ins_color, + Some(&parent.insert_material), + self.material_base_dir.as_deref(), + ) + }; + let child_l0_material = if on_l0 { + parent.layer0_material.clone() + } else { + crate::scene::model::material_model::resolve_layer_material_with_base( + &self.document, + &ins.common.layer, + child_l0, + self.material_base_dir.as_deref(), + ) + }; + BlockMeshInherit { + insert_color: child_ins_color, + layer0_color: child_l0, + insert_material: child_insert_material, + layer0_material: child_l0_material, + } } /// Per-instance colour override for a block-internal solid mesh: ByBlock → @@ -4833,19 +5144,26 @@ impl Scene { &self, e: &EntityType, h: Handle, - inherit: Option<([f32; 4], [f32; 4])>, + inherit: Option<&BlockMeshInherit>, own_alpha: f32, ) -> Option<[f32; 4]> { - let (ins_color, l0_color) = inherit?; + let inherit = inherit?; use acadrust::types::Color; let common = e.common(); let on_l0 = crate::scene::view::render::is_effective_layer_zero(&common.layer); - let mut c = if common.color == Color::ByBlock { - ins_color - } else if on_l0 && common.color == Color::ByLayer { + let has_book_color = + crate::scene::view::render::has_resolved_book_color(&self.document, e); + let mut c = if !has_book_color && common.color == Color::ByBlock { + inherit.insert_color + } else if !has_book_color && on_l0 && common.color == Color::ByLayer { // Inherit the insert layer's RGB but keep the solid's own alpha, // matching the wire/hatch path (render_style_for_block_sub). - [l0_color[0], l0_color[1], l0_color[2], own_alpha] + [ + inherit.layer0_color[0], + inherit.layer0_color[1], + inherit.layer0_color[2], + own_alpha, + ] } else { return None; }; @@ -4857,6 +5175,24 @@ impl Scene { Some(c) } + fn block_mesh_override_material( + &self, + entity: &EntityType, + inherit: Option<&BlockMeshInherit>, + ) -> Option { + let inherit = inherit?; + let common = entity.common(); + if common.material_flags == 1 { + Some(inherit.insert_material.clone()) + } else if common.material_flags == 0 + && crate::scene::view::render::is_effective_layer_zero(&common.layer) + { + Some(inherit.layer0_material.clone()) + } else { + None + } + } + /// Hatches eligible for click / box / lasso hit-testing in the current /// layout. Filters out block-internal source hatches (stored in /// `self.hatches` at block-local coords for the block-defn position, @@ -5015,7 +5351,9 @@ impl Scene { let mut hatch_memo: std::collections::HashMap = std::collections::HashMap::new(); for entity in self.document.entities() { - let EntityType::Insert(ins) = entity else { + let contextual = + crate::scene::annotative::entity_for_active_context(&self.document, entity); + let EntityType::Insert(ins) = contextual.as_ref() else { continue; }; if ins.common.invisible @@ -5037,6 +5375,13 @@ impl Scene { for sub in ins .explode_from_document(&self.document) .into_iter() + .map(|sub| { + crate::scene::annotative::entity_for_active_context( + &self.document, + &sub, + ) + .into_owned() + }) .map(crate::modules::draw::modify::explode::normalize_insert_entity) { let EntityType::Hatch(dxf) = sub else { @@ -5803,6 +6148,9 @@ impl Scene { | Some(EntityType::Region(_)) | Some(EntityType::Body(_)) | Some(EntityType::Surface(_)) + | Some(EntityType::Mesh(_)) + | Some(EntityType::PolygonMesh(_)) + | Some(EntityType::PolyfaceMesh(_)) ) }) } diff --git a/src/scene/model/material_model.rs b/src/scene/model/material_model.rs new file mode 100644 index 000000000..0179ef805 --- /dev/null +++ b/src/scene/model/material_model.rs @@ -0,0 +1,447 @@ +use acadrust::objects::{Material, MaterialColor, MaterialMap, ObjectType}; +use acadrust::{CadDocument, EntityType, Handle}; +use std::path::Path; +use std::sync::Arc; + +/// Render-ready subset of an AcDbMaterial. +/// +/// The complete decoded object stays authoritative in `CadDocument`; this +/// compact copy travels with a tessellated mesh so the GPU path does not need +/// to look back into the document. Map metadata is retained even when the +/// referenced image cannot be resolved, allowing a later texture upload to +/// replace the colour fallback without re-tessellating the solid. +#[derive(Clone, Debug)] +pub struct MeshMaterial { + pub handle: Option, + pub name: String, + pub description: String, + pub diffuse: [f32; 4], + pub ambient: [f32; 3], + pub specular: [f32; 3], + pub gloss: f32, + pub reflectivity: f32, + pub self_illumination: f32, + pub translucence: f32, + pub refraction_index: f32, + pub luminance: f32, + pub two_sided: bool, + pub illumination_model: i32, + pub channel_flags: i32, + pub mode: i32, + pub indirect_bump_scale: f32, + pub reflectance_scale: f32, + pub transmittance_scale: f32, + pub luminance_mode: i16, + pub normal_map_method: i16, + pub normal_map_strength: f32, + pub is_anonymous: bool, + pub global_illumination: i16, + pub final_gather: i16, + pub color_bleed_scale: f32, + pub advanced_data_present: bool, + pub diffuse_map: MeshTextureMap, + pub specular_map: MeshTextureMap, + pub reflection_map: MeshTextureMap, + pub opacity_map: MeshTextureMap, + pub bump_map: MeshTextureMap, + pub refraction_map: MeshTextureMap, + pub normal_map: MeshTextureMap, +} + +#[derive(Clone, Debug)] +pub struct MeshTextureMap { + pub blend_factor: f32, + pub projection: u8, + pub tiling: u8, + pub auto_transform: u8, + pub transform: [f32; 16], + pub source: u8, + pub file_name: String, + pub procedural: bool, + pub image: Option>, +} + +#[derive(Clone, Debug)] +pub struct MaterialImage { + pub width: u32, + pub height: u32, + pub rgba: Arc>, + pub path: String, +} + +impl Default for MeshTextureMap { + fn default() -> Self { + let mut transform = [0.0; 16]; + transform[0] = 1.0; + transform[5] = 1.0; + transform[10] = 1.0; + transform[15] = 1.0; + Self { + blend_factor: 1.0, + projection: 2, + tiling: 1, + auto_transform: 1, + transform, + source: 0, + file_name: String::new(), + procedural: false, + image: None, + } + } +} + +impl MeshTextureMap { + fn from_dwg(map: &MaterialMap, base_dir: Option<&Path>) -> Self { + let mut transform = [0.0; 16]; + for (dst, src) in transform.iter_mut().zip(map.transform) { + *dst = src as f32; + } + Self { + blend_factor: map.blend_factor.clamp(0.0, 1.0) as f32, + projection: map.projection, + tiling: map.tiling, + auto_transform: map.auto_transform, + transform, + source: map.source, + file_name: map.file_name.clone(), + procedural: map.texture.is_some(), + image: load_map_image(map, base_dir) + .or_else(|| procedural_map_image(map)), + } + } + + pub fn has_content(&self) -> bool { + (self.source == 1 && !self.file_name.trim().is_empty()) + || (self.source == 2 && self.procedural) + } +} + +impl MeshMaterial { + pub fn entity_color(color: [f32; 4]) -> Self { + Self { + handle: None, + name: String::new(), + description: String::new(), + diffuse: color, + ambient: [color[0], color[1], color[2]], + specular: [1.0; 3], + gloss: 0.5, + reflectivity: 0.0, + self_illumination: 0.0, + translucence: 0.0, + refraction_index: 1.0, + luminance: 0.0, + two_sided: true, + illumination_model: 0, + channel_flags: 127, + mode: 0, + indirect_bump_scale: 1.0, + reflectance_scale: 1.0, + transmittance_scale: 1.0, + luminance_mode: 0, + normal_map_method: 0, + normal_map_strength: 1.0, + is_anonymous: false, + global_illumination: 0, + final_gather: 0, + color_bleed_scale: 1.0, + advanced_data_present: false, + diffuse_map: MeshTextureMap::default(), + specular_map: MeshTextureMap::default(), + reflection_map: MeshTextureMap::default(), + opacity_map: MeshTextureMap::default(), + bump_map: MeshTextureMap::default(), + refraction_map: MeshTextureMap::default(), + normal_map: MeshTextureMap::default(), + } + } + + fn from_dwg( + handle: Handle, + material: &Material, + entity_color: [f32; 4], + base_dir: Option<&Path>, + ) -> Self { + let diffuse_map = MeshTextureMap::from_dwg(&material.diffuse_map, base_dir); + let specular_map = MeshTextureMap::from_dwg(&material.specular_map, base_dir); + let reflection_map = MeshTextureMap::from_dwg(&material.reflection_map, base_dir); + let opacity_map = MeshTextureMap::from_dwg(&material.opacity_map, base_dir); + let bump_map = MeshTextureMap::from_dwg(&material.bump_map, base_dir); + let refraction_map = MeshTextureMap::from_dwg(&material.refraction_map, base_dir); + let normal_map = MeshTextureMap::from_dwg(&material.normal_map, base_dir); + // A material whose diffuse channel explicitly references an external + // texture is incomplete when that file cannot be resolved. Its stored + // diffuse RGB is commonly only a red/magenta placeholder (for example + // VILLA_COMPLETE's Plaster material), not the intended appearance. + // Keep the material's other physical properties, but use the entity's + // fully resolved Color/ByLayer/ByBlock value for the missing diffuse + // channel. + let diffuse = if diffuse_map.has_content() && diffuse_map.image.is_none() { + entity_color + } else { + material_color(&material.diffuse_color, entity_color) + }; + let ambient = material_color(&material.ambient_color, entity_color); + let specular = material_color(&material.specular_color, [1.0; 4]); + let opacity = material.opacity_percent.clamp(0.0, 1.0) as f32; + Self { + handle: Some(handle), + name: material.name.clone(), + description: material.description.clone(), + diffuse: [ + diffuse[0], + diffuse[1], + diffuse[2], + entity_color[3] * opacity, + ], + ambient: [ambient[0], ambient[1], ambient[2]], + specular: [specular[0], specular[1], specular[2]], + gloss: material.specular_gloss_factor.clamp(0.0, 1.0) as f32, + reflectivity: material.reflectivity.clamp(0.0, 1.0) as f32, + self_illumination: material.self_illumination.clamp(0.0, 1.0) as f32, + translucence: material.translucence.clamp(0.0, 1.0) as f32, + refraction_index: material.refraction_index.max(0.0) as f32, + luminance: material.luminance.max(0.0) as f32, + two_sided: material.two_sided_material, + illumination_model: material.illumination_model, + channel_flags: material.channel_flags, + mode: material.mode, + indirect_bump_scale: material.indirect_bump_scale as f32, + reflectance_scale: material.reflectance_scale as f32, + transmittance_scale: material.transmittance_scale as f32, + luminance_mode: material.luminance_mode, + normal_map_method: material.normal_map_method, + normal_map_strength: material.normal_map_strength.max(0.0) as f32, + is_anonymous: material.is_anonymous, + global_illumination: material.global_illumination, + final_gather: material.final_gather, + color_bleed_scale: material.color_bleed_scale as f32, + advanced_data_present: material.advanced_data_present, + diffuse_map, + specular_map, + reflection_map, + opacity_map, + bump_map, + refraction_map, + normal_map, + } + } + + pub fn apply_to(&self, set: &mut super::mesh_model::MeshLodSet) { + for lod in &mut set.lods { + lod.color = self.diffuse; + } + set.material = Some(self.clone()); + } + + pub fn apply_to_with_face_overrides( + &self, + set: &mut super::mesh_model::MeshLodSet, + document: &CadDocument, + base_dir: Option<&Path>, + ) { + self.apply_to(set); + set.face_materials.clear(); + let handles: rustc_hash::FxHashSet = set + .lods + .iter() + .flat_map(|lod| lod.triangle_material_handles.iter().flatten().copied()) + .collect(); + for handle in handles { + let material = + resolve_material_handle_with_base(document, handle, self, base_dir); + set.face_materials.insert(handle, material); + } + } +} + +fn material_color(value: &MaterialColor, current: [f32; 4]) -> [f32; 4] { + let base = if value.flag == 1 { + value.rgb.map_or(current, |packed| { + let packed = packed as u32; + [ + ((packed >> 16) & 0xff) as f32 / 255.0, + ((packed >> 8) & 0xff) as f32 / 255.0, + (packed & 0xff) as f32 / 255.0, + current[3], + ] + }) + } else { + current + }; + let factor = value.factor.clamp(0.0, 1.0) as f32; + [ + base[0] * factor, + base[1] * factor, + base[2] * factor, + base[3], + ] +} + +fn procedural_map_image(map: &MaterialMap) -> Option> { + let texture = map.texture.as_ref()?; + let color1 = material_color(&texture.color1, [0.2, 0.2, 0.2, 1.0]); + let color2 = material_color(&texture.color2, [0.8, 0.8, 0.8, 1.0]); + let size = 64u32; + let mut rgba = Vec::with_capacity((size * size * 4) as usize); + for y in 0..size { + for x in 0..size { + let color = if ((x / 8) + (y / 8)) & 1 == 0 { + color1 + } else { + color2 + }; + rgba.extend(color.map(|channel| { + (channel.clamp(0.0, 1.0) * 255.0).round() as u8 + })); + } + } + Some(Arc::new(MaterialImage { + width: size, + height: size, + rgba: Arc::new(rgba), + path: format!("procedural:{}", texture.mode), + })) +} + +/// Resolve an entity's effective material using AcDbEntity's material flags. +/// +/// `by_block` supplies the enclosing INSERT's material for a block child. +/// Outside a block a ByBlock material has no owner source and falls back to +/// the entity colour, matching the existing ByBlock colour behaviour. +pub fn resolve_material_with_base( + document: &CadDocument, + entity: &EntityType, + entity_color: [f32; 4], + by_block: Option<&MeshMaterial>, + base_dir: Option<&Path>, +) -> MeshMaterial { + let common = entity.common(); + if common.material_flags == 1 { + return by_block + .cloned() + .unwrap_or_else(|| MeshMaterial::entity_color(entity_color)); + } + let handle = if common.material_flags == 3 { + common.material_handle + } else { + document + .layers + .get(&common.layer) + .map(|layer| layer.material) + .filter(|handle| handle.is_valid()) + }; + let Some(handle) = handle else { + return MeshMaterial::entity_color(entity_color); + }; + match document.objects.get(&handle) { + Some(ObjectType::Material(material)) + if material.name.eq_ignore_ascii_case("ByLayer") + || material.name.eq_ignore_ascii_case("ByBlock") => + { + MeshMaterial::entity_color(entity_color) + } + Some(ObjectType::Material(material)) => { + MeshMaterial::from_dwg(handle, material, entity_color, base_dir) + } + _ => MeshMaterial::entity_color(entity_color), + } +} + +pub fn resolve_layer_material_with_base( + document: &CadDocument, + layer_name: &str, + layer_color: [f32; 4], + base_dir: Option<&Path>, +) -> MeshMaterial { + let handle = document + .layers + .get(layer_name) + .map(|layer| layer.material) + .filter(|handle| handle.is_valid()); + match handle.and_then(|handle| document.objects.get(&handle).map(|object| (handle, object))) { + Some((_, ObjectType::Material(material))) + if material.name.eq_ignore_ascii_case("ByLayer") + || material.name.eq_ignore_ascii_case("ByBlock") => + { + MeshMaterial::entity_color(layer_color) + } + Some((handle, ObjectType::Material(material))) => { + MeshMaterial::from_dwg(handle, material, layer_color, base_dir) + } + _ => MeshMaterial::entity_color(layer_color), + } +} + +pub fn resolve_material_handle_with_base( + document: &CadDocument, + handle: Handle, + fallback: &MeshMaterial, + base_dir: Option<&Path>, +) -> MeshMaterial { + match document.objects.get(&handle) { + Some(ObjectType::Material(material)) if material.name.eq_ignore_ascii_case("ByLayer") => { + fallback.clone() + } + Some(ObjectType::Material(material)) if material.name.eq_ignore_ascii_case("ByBlock") => { + fallback.clone() + } + Some(ObjectType::Material(material)) => { + MeshMaterial::from_dwg(handle, material, fallback.diffuse, base_dir) + } + _ => fallback.clone(), + } +} + +#[cfg(not(target_arch = "wasm32"))] +fn load_map_image(map: &MaterialMap, base_dir: Option<&Path>) -> Option> { + use std::collections::HashMap; + use std::path::PathBuf; + use std::sync::{Mutex, OnceLock}; + + static CACHE: OnceLock>>>> = OnceLock::new(); + + if map.source != 1 || map.file_name.trim().is_empty() { + return None; + } + let normalized = map.file_name.trim().replace('\\', "/"); + let source = PathBuf::from(&normalized); + let mut candidates = Vec::with_capacity(4); + if source.is_absolute() { + candidates.push(source.clone()); + } + if let Some(base) = base_dir { + candidates.push(base.join(&source)); + if let Some(name) = source.file_name() { + candidates.push(base.join(name)); + for folder in ["Textures", "textures", "Materials", "materials"] { + candidates.push(base.join(folder).join(name)); + } + } + } + let path = candidates.into_iter().find(|path| path.is_file())?; + let path = path.canonicalize().unwrap_or(path); + let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new())); + if let Some(hit) = cache.lock().ok()?.get(&path).cloned() { + return hit; + } + let loaded = std::fs::read(&path) + .ok() + .and_then(|bytes| image::load_from_memory(&bytes).ok()) + .map(|image| { + let rgba = image.to_rgba8(); + Arc::new(MaterialImage { + width: rgba.width(), + height: rgba.height(), + rgba: Arc::new(rgba.into_raw()), + path: path.to_string_lossy().into_owned(), + }) + }); + cache.lock().ok()?.insert(path, loaded.clone()); + loaded +} + +#[cfg(target_arch = "wasm32")] +fn load_map_image(_map: &MaterialMap, _base_dir: Option<&Path>) -> Option> { + None +} diff --git a/src/scene/model/mesh_model.rs b/src/scene/model/mesh_model.rs index da5a4ac99..14768f837 100644 --- a/src/scene/model/mesh_model.rs +++ b/src/scene/model/mesh_model.rs @@ -18,6 +18,12 @@ pub struct MeshModel { pub normals: Vec<[f32; 3]>, /// Triangle indices into `verts` (every 3 values = one triangle). pub indices: Vec, + /// Optional AcDbMaterial handle per triangle. Empty means the whole mesh + /// uses `MeshLodSet::material`; otherwise each entry aligns with one + /// `indices` triplet and overrides the entity material for that face. + pub triangle_material_handles: Vec>, + /// Optional ACIS face colour per triangle, aligned with `indices` triplets. + pub triangle_colors: Vec>, /// RGBA colour in [0, 1]. pub color: [f32; 4], /// Whether this mesh is currently selected. @@ -94,9 +100,41 @@ pub enum CurvedGen { }, } +#[derive(Clone, Debug)] +pub struct StoredSilhouette { + pub viewport_id: i64, + pub view_direction: [f32; 3], + pub up_vector: [f32; 3], + pub target: [f32; 3], + pub is_perspective: bool, + pub edge_verts: Vec<[f32; 3]>, + pub edge_verts_low: Vec<[f32; 3]>, +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct MeshMetrics { + pub vertices: usize, + pub triangles: usize, + pub surface_area: f64, + pub volume: f64, + pub centroid: [f64; 3], +} + #[derive(Clone, Debug)] pub struct MeshLodSet { pub lods: Vec, + /// Effective AcDbMaterial resolved from entity/layer/INSERT inheritance. + /// Geometry remains usable when it is absent; the renderer then keeps the + /// per-mesh entity colour. + pub material: Option, + /// Face-level ACIS material overrides, resolved from each LOD's + /// `triangle_material_handles`. Only handles actually referenced by the + /// tessellation are retained. + pub face_materials: + rustc_hash::FxHashMap, + /// Effective AcDbVisualStyle override resolved from the entity's full, + /// face and edge style handles. + pub visual_style: Option, /// True only when every source face produced triangles. False keeps /// downstream solid-edit code from treating a display-only partial shell /// as a closed, valid solid. @@ -111,6 +149,14 @@ pub struct MeshLodSet { /// Curved-face generators for per-frame silhouette (DISPSILH). Empty for a /// solid with no curved faces, or when silhouettes aren't wanted. pub curved_gens: Vec, + /// View-specific silhouette caches stored in COMMON_3DSOLID. They are used + /// when the decoded surface family cannot provide a live analytic + /// silhouette for the current view. + pub stored_silhouettes: Vec, + /// Geometry measurements calculated once from the highest available LOD. + /// Properties can read these without re-parsing or re-tessellating ACIS on + /// the UI thread. + pub metrics: MeshMetrics, /// World XY AABB `[min_x, min_y, max_x, max_y]` of the mesh — used /// by the per-frame LOD selector to compute the projected pixel /// diagonal. @@ -144,16 +190,93 @@ pub fn compute_mesh_aabb(lods: &[MeshModel]) -> ([f32; 4], [f32; 2]) { ([min_x, min_y, max_x, max_y], [min_z, max_z]) } +fn compute_mesh_metrics(lods: &[MeshModel]) -> MeshMetrics { + let Some(mesh) = lods.iter().find(|mesh| !mesh.indices.is_empty()) else { + return MeshMetrics::default(); + }; + let point = |index: u32| { + let index = index as usize; + let high = mesh.verts.get(index).copied().unwrap_or([0.0; 3]); + let low = mesh.verts_low.get(index).copied().unwrap_or([0.0; 3]); + [ + high[0] as f64 + low[0] as f64, + high[1] as f64 + low[1] as f64, + high[2] as f64 + low[2] as f64, + ] + }; + let mut area = 0.0; + let mut area_centroid_numerator = [0.0; 3]; + let mut signed_volume = 0.0; + let mut centroid_numerator = [0.0; 3]; + for triangle in mesh.indices.chunks_exact(3) { + let a = point(triangle[0]); + let b = point(triangle[1]); + let c = point(triangle[2]); + let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]]; + let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]]; + let cross = [ + ab[1] * ac[2] - ab[2] * ac[1], + ab[2] * ac[0] - ab[0] * ac[2], + ab[0] * ac[1] - ab[1] * ac[0], + ]; + let triangle_area = 0.5 + * (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]) + .sqrt(); + area += triangle_area; + for axis in 0..3 { + area_centroid_numerator[axis] += + triangle_area * (a[axis] + b[axis] + c[axis]) / 3.0; + } + let tetra = ( + a[0] * (b[1] * c[2] - b[2] * c[1]) + - a[1] * (b[0] * c[2] - b[2] * c[0]) + + a[2] * (b[0] * c[1] - b[1] * c[0]) + ) / 6.0; + signed_volume += tetra; + for axis in 0..3 { + centroid_numerator[axis] += tetra * (a[axis] + b[axis] + c[axis]) / 4.0; + } + } + let centroid = if signed_volume.abs() > 1e-18 { + [ + centroid_numerator[0] / signed_volume, + centroid_numerator[1] / signed_volume, + centroid_numerator[2] / signed_volume, + ] + } else if area > 1e-18 { + [ + area_centroid_numerator[0] / area, + area_centroid_numerator[1] / area, + area_centroid_numerator[2] / area, + ] + } else { + [0.0; 3] + }; + MeshMetrics { + vertices: mesh.verts.len(), + triangles: mesh.indices.len() / 3, + surface_area: area, + volume: signed_volume.abs(), + centroid, + } +} + impl MeshLodSet { /// Build a set from its LODs, computing the 3D AABB. pub fn from_lods(lods: Vec) -> Self { let (world_aabb, z_aabb) = compute_mesh_aabb(&lods); + let metrics = compute_mesh_metrics(&lods); Self { lods, + material: None, + face_materials: rustc_hash::FxHashMap::default(), + visual_style: None, complete: true, edge_verts: Vec::new(), edge_verts_low: Vec::new(), curved_gens: Vec::new(), + stored_silhouettes: Vec::new(), + metrics, world_aabb, z_aabb, } diff --git a/src/scene/model/mod.rs b/src/scene/model/mod.rs index d4014f70a..42eda2d13 100644 --- a/src/scene/model/mod.rs +++ b/src/scene/model/mod.rs @@ -3,8 +3,10 @@ pub mod gdi; pub mod hatch_model; pub mod hatch_patterns; pub mod image_model; +pub mod material_model; pub mod ole_pres; pub mod pdf_raster; pub mod mesh_model; pub mod solid_model; +pub mod visual_style_model; pub mod object; diff --git a/src/scene/model/solid_model.rs b/src/scene/model/solid_model.rs index e4e666ffc..d2b35e901 100644 --- a/src/scene/model/solid_model.rs +++ b/src/scene/model/solid_model.rs @@ -180,6 +180,8 @@ pub fn mesh_from_solid(solid: &Solid, color: [f32; 4]) -> Option { verts_low, normals, indices, + triangle_material_handles: Vec::new(), + triangle_colors: Vec::new(), color, selected: false, }; diff --git a/src/scene/model/visual_style_model.rs b/src/scene/model/visual_style_model.rs new file mode 100644 index 000000000..60f52155b --- /dev/null +++ b/src/scene/model/visual_style_model.rs @@ -0,0 +1,278 @@ +use acadrust::objects::{ + ObjectType, VisualStyle, VisualStyleProperty, VisualStylePropertyValue, +}; +use acadrust::{CadDocument, EntityType, Handle}; + +#[derive(Clone, Debug)] +pub struct MeshVisualStyle { + pub full_handle: Option, + pub face_handle: Option, + pub edge_handle: Option, + pub description: String, + pub style_type: i16, + pub face_lighting_model: i16, + pub face_lighting_quality: i16, + pub face_color_mode: i16, + pub face_modifier: i32, + pub face_opacity: f32, + pub face_specular: f32, + pub mono_color: Option<[f32; 4]>, + pub edge_model: i32, + pub edge_style: i32, + pub edge_color: Option<[f32; 4]>, + pub edge_opacity: f32, + pub edge_width: f32, + pub edge_overhang: i32, + pub edge_jitter: i32, + pub silhouette_color: Option<[f32; 4]>, + pub silhouette_width: i32, + pub halo_gap: i32, + pub isoline_count: i32, + pub hide_precision: bool, + pub display_settings: i32, + pub brightness: f32, + pub shadow_type: i32, + pub extended_lighting_model: i16, + pub internal_use_only: bool, +} + +fn core_property(style: &VisualStyle, index: usize) -> Option<&VisualStyleProperty> { + if style.properties.len() >= 28 { + return style.properties.get(index); + } + if style.properties.len() == 24 { + let legacy = match index { + 4 => 0, + 5 => 1, + 6 => 2, + 9 => 3, + 10 => 4, + 11 => 5, + 13 => 6, + 14 => 7, + 15 => 8, + 16 => 9, + 17 => 10, + 18 => 11, + 19 => 12, + 20 => 13, + 21 => 14, + 22 => 15, + 23 => 16, + 24 => 17, + 12 => 19, + 25 => 20, + 26 => 21, + 27 => 22, + _ => return None, + }; + return style.properties.get(legacy); + } + style.properties.get(index) +} + +fn property_double(style: &VisualStyle, index: usize, fallback: f64) -> f64 { + let Some(property) = core_property(style, index) else { + return fallback; + }; + match property.value { + VisualStylePropertyValue::Short(value) => value as f64, + VisualStylePropertyValue::Long(value) => value as f64, + VisualStylePropertyValue::Double(value) => value, + VisualStylePropertyValue::Bool(value) => value as u8 as f64, + _ => fallback, + } +} + +fn property_long(style: &VisualStyle, index: usize, fallback: i32) -> i32 { + property_double(style, index, fallback as f64) as i32 +} + +fn property_bool(style: &VisualStyle, index: usize, fallback: bool) -> bool { + property_double(style, index, fallback as u8 as f64) != 0.0 +} + +fn property_color(style: &VisualStyle, index: usize) -> Option<[f32; 4]> { + let property = core_property(style, index)?; + if property.enabled == 0 { + return None; + } + let VisualStylePropertyValue::Color(color) = &property.value else { + return None; + }; + if matches!( + color, + acadrust::types::Color::ByLayer | acadrust::types::Color::ByBlock + ) { + return None; + } + Some(crate::scene::convert::tess_util::aci_to_rgba(color)) +} + +impl MeshVisualStyle { + fn from_dwg(handle: Handle, style: &VisualStyle) -> Self { + Self { + full_handle: Some(handle), + face_handle: None, + edge_handle: None, + description: style.description.clone(), + style_type: style.style_type, + face_lighting_model: style.face_lighting_model, + face_lighting_quality: style.face_lighting_quality, + face_color_mode: style.face_color_mode, + face_modifier: style.face_modifier, + face_opacity: property_double(style, 4, 1.0).clamp(0.0, 1.0) as f32, + face_specular: property_double(style, 5, 0.0).max(0.0) as f32, + mono_color: property_color(style, 6), + edge_model: style.edge_model, + edge_style: style.edge_style, + edge_color: property_color(style, 15), + edge_opacity: property_double(style, 16, 1.0).clamp(0.0, 1.0) as f32, + edge_width: property_double(style, 17, 1.0).max(0.0) as f32, + edge_overhang: property_long(style, 18, 0), + edge_jitter: property_long(style, 19, 0), + silhouette_color: property_color(style, 20), + silhouette_width: property_long(style, 21, 0), + halo_gap: property_long(style, 22, 0), + isoline_count: property_long(style, 23, 0), + hide_precision: property_bool(style, 24, false), + display_settings: property_long(style, 25, 0), + brightness: property_double(style, 26, 0.0) as f32, + shadow_type: if style.properties.len() == 24 { + style + .properties + .get(23) + .map_or(0, |property| match property.value { + VisualStylePropertyValue::Short(value) => value as i32, + VisualStylePropertyValue::Long(value) => value, + VisualStylePropertyValue::Double(value) => value as i32, + VisualStylePropertyValue::Bool(value) => value as i32, + _ => 0, + }) + } else { + property_long(style, 27, 0) + }, + extended_lighting_model: style.extended_lighting_model, + internal_use_only: style.internal_use_only, + } + } + + fn override_face(&mut self, handle: Handle, style: &VisualStyle) { + let source = Self::from_dwg(handle, style); + self.face_handle = Some(handle); + self.face_lighting_model = source.face_lighting_model; + self.face_lighting_quality = source.face_lighting_quality; + self.face_color_mode = source.face_color_mode; + self.face_modifier = source.face_modifier; + self.face_opacity = source.face_opacity; + self.face_specular = source.face_specular; + self.mono_color = source.mono_color; + self.brightness = source.brightness; + self.extended_lighting_model = source.extended_lighting_model; + } + + fn override_edge(&mut self, handle: Handle, style: &VisualStyle) { + let source = Self::from_dwg(handle, style); + self.edge_handle = Some(handle); + self.edge_model = source.edge_model; + self.edge_style = source.edge_style; + self.edge_color = source.edge_color; + self.edge_opacity = source.edge_opacity; + self.edge_width = source.edge_width; + self.edge_overhang = source.edge_overhang; + self.edge_jitter = source.edge_jitter; + self.silhouette_color = source.silhouette_color; + self.silhouette_width = source.silhouette_width; + self.halo_gap = source.halo_gap; + self.isoline_count = source.isoline_count; + self.hide_precision = source.hide_precision; + } + + pub fn face_visible(&self) -> bool { + self.face_lighting_model != 0 && self.face_color_mode != 0 + } + + pub fn edges_visible(&self) -> bool { + self.edge_model != 0 + } + + pub fn face_color(&self, base: [f32; 4]) -> [f32; 4] { + let mut color = match self.face_color_mode { + 3 | 4 => self.mono_color.unwrap_or(base), + 5 => { + let tint = self.mono_color.unwrap_or(base); + [ + (base[0] + tint[0]) * 0.5, + (base[1] + tint[1]) * 0.5, + (base[2] + tint[2]) * 0.5, + base[3], + ] + } + 6 => { + let luminance = base[0] * 0.2126 + base[1] * 0.7152 + base[2] * 0.0722; + [ + base[0] * 0.7 + luminance * 0.3, + base[1] * 0.7 + luminance * 0.3, + base[2] * 0.7 + luminance * 0.3, + base[3], + ] + } + _ => base, + }; + if self.face_modifier & 1 != 0 { + color[3] *= self.face_opacity; + } + color + } + + pub fn edge_color(&self, base: [f32; 4]) -> [f32; 4] { + let mut color = self.edge_color.unwrap_or(base); + color[3] *= self.edge_opacity; + color + } +} + +fn visual_style( + document: &CadDocument, + handle: Option, +) -> Option<(Handle, &VisualStyle)> { + let handle = handle.filter(|handle| handle.is_valid())?; + match document.objects.get(&handle) { + Some(ObjectType::VisualStyle(style)) => Some((handle, style)), + _ => None, + } +} + +pub fn resolve_mesh_visual_style( + document: &CadDocument, + entity: &EntityType, +) -> Option { + let common = entity.common(); + let full = visual_style(document, common.full_visual_style_handle); + let face = visual_style(document, common.face_visual_style_handle); + let edge = visual_style(document, common.edge_visual_style_handle); + let mut result = full + .map(|(handle, style)| MeshVisualStyle::from_dwg(handle, style)) + .or_else(|| { + face.or(edge) + .map(|(handle, style)| MeshVisualStyle::from_dwg(handle, style)) + })?; + if full.is_none() { + result.full_handle = None; + } + if let Some((handle, style)) = face { + result.override_face(handle, style); + } + if let Some((handle, style)) = edge { + result.override_edge(handle, style); + } + Some(result) +} + +pub fn apply_mesh_visual_style( + set: &mut super::mesh_model::MeshLodSet, + document: &CadDocument, + entity: &EntityType, +) { + set.visual_style = resolve_mesh_visual_style(document, entity); +} diff --git a/src/scene/modify.rs b/src/scene/modify.rs index b53b4ead5..17b20299c 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -200,6 +200,14 @@ impl Scene { } } } + for &h in handles { + if crate::scene::annotative::sync_active_context_from_entity( + &mut self.document, + h, + ) { + self.poison_undo_recording(); + } + } // Move the baked dimension-block sub-entities too (collected above). // These are entities not named in the reported change set, so a delta // must capture their before-images here or a dimension move won't undo. @@ -438,6 +446,12 @@ impl Scene { if let Some(entity) = self.document.get_entity_mut(handle) { view::dispatch::apply_grip(entity, grip_id, apply); } + if crate::scene::annotative::sync_active_context_from_entity( + &mut self.document, + handle, + ) { + self.poison_undo_recording(); + } // A dimension loaded from a file renders through its baked *D block; // the grip only moved a definition point, so the baked graphics are // stale — drop them so tessellation falls back to the live geometry @@ -452,21 +466,83 @@ impl Scene { .and_then(crate::entities::solid3d::point_of_reference) .map(|p| [p.x, p.y, p.z]); if let Some(new) = new_por { - let dx = (new[0] - old[0]) as f32; - let dy = (new[1] - old[1]) as f32; - let dz = (new[2] - old[2]) as f32; + let delta = [new[0] - old[0], new[1] - old[1], new[2] - old[2]]; if let Some(set) = self.meshes.get_mut(&handle) { + let translate_split = + |high: &mut [f32; 3], low: &mut [f32; 3]| { + let absolute = [ + high[0] as f64 + low[0] as f64 + delta[0], + high[1] as f64 + low[1] as f64 + delta[1], + high[2] as f64 + low[2] as f64 + delta[2], + ]; + *high = [ + absolute[0] as f32, + absolute[1] as f32, + absolute[2] as f32, + ]; + *low = [ + (absolute[0] - high[0] as f64) as f32, + (absolute[1] - high[1] as f64) as f32, + (absolute[2] - high[2] as f64) as f32, + ]; + }; for lod in &mut set.lods { - for v in &mut lod.verts { - v[0] += dx; - v[1] += dy; - v[2] += dz; + if lod.verts_low.len() != lod.verts.len() { + lod.verts_low = vec![[0.0; 3]; lod.verts.len()]; + } + for (high, low) in lod.verts.iter_mut().zip(lod.verts_low.iter_mut()) { + translate_split(high, low); } } - set.world_aabb[0] += dx; - set.world_aabb[1] += dy; - set.world_aabb[2] += dx; - set.world_aabb[3] += dy; + if set.edge_verts_low.len() != set.edge_verts.len() { + set.edge_verts_low = vec![[0.0; 3]; set.edge_verts.len()]; + } + for (high, low) in set + .edge_verts + .iter_mut() + .zip(set.edge_verts_low.iter_mut()) + { + translate_split(high, low); + } + for generator in &mut set.curved_gens { + match generator { + crate::scene::model::mesh_model::CurvedGen::Cone { + base, + base_low, + .. + } => translate_split(base, base_low), + crate::scene::model::mesh_model::CurvedGen::Sphere { + center, + center_low, + .. + } + | crate::scene::model::mesh_model::CurvedGen::Torus { + center, + center_low, + .. + } => translate_split(center, center_low), + } + } + for silhouette in &mut set.stored_silhouettes { + silhouette.target[0] += delta[0] as f32; + silhouette.target[1] += delta[1] as f32; + silhouette.target[2] += delta[2] as f32; + if silhouette.edge_verts_low.len() != silhouette.edge_verts.len() { + silhouette.edge_verts_low = + vec![[0.0; 3]; silhouette.edge_verts.len()]; + } + for (high, low) in silhouette + .edge_verts + .iter_mut() + .zip(silhouette.edge_verts_low.iter_mut()) + { + translate_split(high, low); + } + } + set.metrics.centroid[0] += delta[0]; + set.metrics.centroid[1] += delta[1]; + set.metrics.centroid[2] += delta[2]; + set.recompute_aabb(); } } } diff --git a/src/scene/paper.rs b/src/scene/paper.rs index 8e9cf83c5..c3accb6c7 100644 --- a/src/scene/paper.rs +++ b/src/scene/paper.rs @@ -310,9 +310,12 @@ impl Scene { }; let mut models: Vec = Vec::new(); for (&handle, model) in self.hatches.iter() { - let Some(entity) = self.document.get_entity(handle) else { + let Some(source) = self.document.get_entity(handle) else { continue; }; + let contextual = + crate::scene::annotative::entity_for_active_context(&self.document, source); + let entity = contextual.as_ref(); let c = entity.common(); if c.invisible || layer_hidden(&c.layer) { continue; @@ -320,7 +323,19 @@ impl Scene { if !self.belongs_to_visible_block(handle, c.owner_handle, layout_block) { continue; } - let mut m = model.clone(); + let mut m = match entity { + EntityType::Hatch(dxf) + if crate::scene::annotative::active_object_context( + &self.document, + handle, + ) + .is_some() => + { + Self::hatch_model_from_dxf(dxf, model.color) + .unwrap_or_else(|| model.clone()) + } + _ => model.clone(), + }; m.color = self.render_style(entity).0; if let EntityType::Hatch(dxf) = entity { // Only re-apply pattern_scale/angle for catalog-derived patterns diff --git a/src/scene/pipeline/mesh_gpu.rs b/src/scene/pipeline/mesh_gpu.rs index 49f441227..37ede4e92 100644 --- a/src/scene/pipeline/mesh_gpu.rs +++ b/src/scene/pipeline/mesh_gpu.rs @@ -20,6 +20,23 @@ pub struct MeshVertex { pub normal: [f32; 3], pub color: [f32; 4], pub position_low: [f32; 3], + /// gloss, reflectivity, self-illumination, luminance + pub material: [f32; 4], + /// specular RGB and refraction index + pub specular: [f32; 4], + pub uv_diffuse: [f32; 2], + /// ambient RGB and translucence + pub ambient: [f32; 4], + /// normal strength, bump scale, reflectance scale, transmittance scale + pub advanced: [f32; 4], + /// illumination model, channel flags, material mode, luminance mode + pub flags: [u32; 4], + pub uv_specular: [f32; 2], + pub uv_reflection: [f32; 2], + pub uv_opacity: [f32; 2], + pub uv_bump: [f32; 2], + pub uv_refraction: [f32; 2], + pub uv_normal: [f32; 2], } impl MeshVertex { @@ -45,6 +62,66 @@ impl MeshVertex { shader_location: 3, format: wgpu::VertexFormat::Float32x3, }, + wgpu::VertexAttribute { + offset: std::mem::offset_of!(MeshVertex, material) as u64, + shader_location: 4, + format: wgpu::VertexFormat::Float32x4, + }, + wgpu::VertexAttribute { + offset: std::mem::offset_of!(MeshVertex, specular) as u64, + shader_location: 5, + format: wgpu::VertexFormat::Float32x4, + }, + wgpu::VertexAttribute { + offset: std::mem::offset_of!(MeshVertex, uv_diffuse) as u64, + shader_location: 6, + format: wgpu::VertexFormat::Float32x2, + }, + wgpu::VertexAttribute { + offset: std::mem::offset_of!(MeshVertex, ambient) as u64, + shader_location: 7, + format: wgpu::VertexFormat::Float32x4, + }, + wgpu::VertexAttribute { + offset: std::mem::offset_of!(MeshVertex, advanced) as u64, + shader_location: 8, + format: wgpu::VertexFormat::Float32x4, + }, + wgpu::VertexAttribute { + offset: std::mem::offset_of!(MeshVertex, flags) as u64, + shader_location: 9, + format: wgpu::VertexFormat::Uint32x4, + }, + wgpu::VertexAttribute { + offset: std::mem::offset_of!(MeshVertex, uv_specular) as u64, + shader_location: 10, + format: wgpu::VertexFormat::Float32x2, + }, + wgpu::VertexAttribute { + offset: std::mem::offset_of!(MeshVertex, uv_reflection) as u64, + shader_location: 11, + format: wgpu::VertexFormat::Float32x2, + }, + wgpu::VertexAttribute { + offset: std::mem::offset_of!(MeshVertex, uv_opacity) as u64, + shader_location: 12, + format: wgpu::VertexFormat::Float32x2, + }, + wgpu::VertexAttribute { + offset: std::mem::offset_of!(MeshVertex, uv_bump) as u64, + shader_location: 13, + format: wgpu::VertexFormat::Float32x2, + }, + wgpu::VertexAttribute { + offset: std::mem::offset_of!(MeshVertex, uv_refraction) as u64, + shader_location: 14, + format: wgpu::VertexFormat::Float32x2, + }, + wgpu::VertexAttribute { + offset: std::mem::offset_of!(MeshVertex, uv_normal) as u64, + shader_location: 15, + format: wgpu::VertexFormat::Float32x2, + }, ]; wgpu::VertexBufferLayout { array_stride: std::mem::size_of::() as u64, @@ -128,6 +205,8 @@ pub struct MeshBatchChunk { /// buffer (pairs of endpoints), drawn non-indexed. Empty for plain meshes. pub edge_vertex_buffer: wgpu::Buffer, pub edge_vertex_count: u32, + pub material: Option, + pub material_bind_group: Option, } fn make_chunk( @@ -137,6 +216,7 @@ fn make_chunk( transp_indices: &[u32], wire_indices: &[u32], edge_verts: &[MeshVertex], + material: Option<&crate::scene::model::material_model::MeshMaterial>, ) -> MeshBatchChunk { // `create_buffer_init` with an empty slice yields a zero-sized buffer that // some backends reject for INDEX usage; a chunk can legitimately hold only @@ -157,6 +237,18 @@ fn make_chunk( normal: [0.0, 1.0, 0.0], color: [0.0; 4], position_low: [0.0; 3], + material: [0.0; 4], + specular: [1.0, 1.0, 1.0, 1.0], + uv_diffuse: [0.0; 2], + ambient: [0.3, 0.3, 0.3, 0.0], + advanced: [1.0; 4], + flags: [0, 127, 0, 0], + uv_specular: [0.0; 2], + uv_reflection: [0.0; 2], + uv_opacity: [0.0; 2], + uv_bump: [0.0; 2], + uv_refraction: [0.0; 2], + uv_normal: [0.0; 2], }]; let src = if data.is_empty() { &stub[..] } else { data }; device.create_buffer_init(&wgpu::util::BufferInitDescriptor { @@ -175,9 +267,411 @@ fn make_chunk( wire_index_count: wire_indices.len() as u32, edge_vertex_buffer: mk_vertex(edge_verts, "mesh.batch.edge_vbuf"), edge_vertex_count: edge_verts.len() as u32, + material: material.cloned(), + material_bind_group: None, } } +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct MaterialMapParams { + /// diffuse, specular, reflection and opacity blend factors. + blends0: [f32; 4], + /// Presence bits for the same four maps. + present0: [u32; 4], + /// bump, refraction, normal and reserved blend factors. + blends1: [f32; 4], + /// Presence bits for the same four maps. + present1: [u32; 4], +} + +fn upload_rgba_texture( + device: &wgpu::Device, + queue: &wgpu::Queue, + label: &'static str, + image: Option<&crate::scene::model::material_model::MaterialImage>, + fallback: [u8; 4], + srgb: bool, +) -> wgpu::TextureView { + let (width, height, pixels) = image.map_or((1, 1, fallback.as_slice()), |image| { + (image.width, image.height, image.rgba.as_slice()) + }); + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some(label), + size: wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: if srgb { + wgpu::TextureFormat::Rgba8UnormSrgb + } else { + wgpu::TextureFormat::Rgba8Unorm + }, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + }); + queue.write_texture( + texture.as_image_copy(), + pixels, + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(width * 4), + rows_per_image: Some(height), + }, + wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + ); + texture.create_view(&wgpu::TextureViewDescriptor::default()) +} + +pub fn create_material_bind_group( + device: &wgpu::Device, + queue: &wgpu::Queue, + layout: &wgpu::BindGroupLayout, + material: Option<&crate::scene::model::material_model::MeshMaterial>, +) -> wgpu::BindGroup { + let diffuse = material.and_then(|material| material.diffuse_map.image.as_deref()); + let specular = material.and_then(|material| material.specular_map.image.as_deref()); + let reflection = material.and_then(|material| material.reflection_map.image.as_deref()); + let opacity = material.and_then(|material| material.opacity_map.image.as_deref()); + let bump = material.and_then(|material| material.bump_map.image.as_deref()); + let refraction = material.and_then(|material| material.refraction_map.image.as_deref()); + let normal = material.and_then(|material| material.normal_map.image.as_deref()); + let diffuse_view = + upload_rgba_texture(device, queue, "mesh.material.diffuse", diffuse, [255; 4], true); + let specular_view = + upload_rgba_texture(device, queue, "mesh.material.specular", specular, [255; 4], true); + let reflection_view = + upload_rgba_texture(device, queue, "mesh.material.reflection", reflection, [0, 0, 0, 255], true); + let opacity_view = + upload_rgba_texture(device, queue, "mesh.material.opacity", opacity, [255; 4], false); + let bump_view = + upload_rgba_texture(device, queue, "mesh.material.bump", bump, [128, 128, 128, 255], false); + let refraction_view = + upload_rgba_texture(device, queue, "mesh.material.refraction", refraction, [255; 4], true); + let normal_view = upload_rgba_texture( + device, + queue, + "mesh.material.normal", + normal, + [128, 128, 255, 255], + false, + ); + let sampler = |label: &'static str, tiling: u8| { + let address = match tiling { + 1 => wgpu::AddressMode::Repeat, + 4 => wgpu::AddressMode::MirrorRepeat, + _ => wgpu::AddressMode::ClampToEdge, + }; + device.create_sampler(&wgpu::SamplerDescriptor { + label: Some(label), + address_mode_u: address, + address_mode_v: address, + address_mode_w: address, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Linear, + ..Default::default() + }) + }; + let tiling = |channel: usize| { + material.map_or(1, |material| { + [ + material.diffuse_map.tiling, + material.specular_map.tiling, + material.reflection_map.tiling, + material.opacity_map.tiling, + material.bump_map.tiling, + material.refraction_map.tiling, + material.normal_map.tiling, + ][channel] + }) + }; + let diffuse_sampler = sampler("mesh.material.diffuse_sampler", tiling(0)); + let specular_sampler = sampler("mesh.material.specular_sampler", tiling(1)); + let reflection_sampler = sampler("mesh.material.reflection_sampler", tiling(2)); + let opacity_sampler = sampler("mesh.material.opacity_sampler", tiling(3)); + let bump_sampler = sampler("mesh.material.bump_sampler", tiling(4)); + let refraction_sampler = sampler("mesh.material.refraction_sampler", tiling(5)); + let normal_sampler = sampler("mesh.material.normal_sampler", tiling(6)); + let params = MaterialMapParams { + blends0: material.map_or([0.0; 4], |material| { + [ + material.diffuse_map.blend_factor, + material.specular_map.blend_factor, + material.reflection_map.blend_factor, + material.opacity_map.blend_factor, + ] + }), + present0: material.map_or([0; 4], |material| { + [ + material.diffuse_map.image.is_some() as u32, + material.specular_map.image.is_some() as u32, + material.reflection_map.image.is_some() as u32, + material.opacity_map.image.is_some() as u32, + ] + }), + blends1: material.map_or([0.0; 4], |material| { + [ + material.bump_map.blend_factor, + material.refraction_map.blend_factor, + material.normal_map.blend_factor, + 0.0, + ] + }), + present1: material.map_or([0; 4], |material| { + [ + material.bump_map.image.is_some() as u32, + material.refraction_map.image.is_some() as u32, + material.normal_map.image.is_some() as u32, + 0, + ] + }), + }; + let params_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("mesh.material.params"), + contents: bytemuck::bytes_of(¶ms), + usage: wgpu::BufferUsages::UNIFORM, + }); + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("mesh.material.bind_group"), + layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&diffuse_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&specular_view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::TextureView(&reflection_view), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: wgpu::BindingResource::TextureView(&opacity_view), + }, + wgpu::BindGroupEntry { + binding: 4, + resource: wgpu::BindingResource::TextureView(&bump_view), + }, + wgpu::BindGroupEntry { + binding: 5, + resource: wgpu::BindingResource::TextureView(&refraction_view), + }, + wgpu::BindGroupEntry { + binding: 6, + resource: wgpu::BindingResource::TextureView(&normal_view), + }, + wgpu::BindGroupEntry { + binding: 7, + resource: wgpu::BindingResource::Sampler(&diffuse_sampler), + }, + wgpu::BindGroupEntry { + binding: 8, + resource: wgpu::BindingResource::Sampler(&specular_sampler), + }, + wgpu::BindGroupEntry { + binding: 9, + resource: wgpu::BindingResource::Sampler(&reflection_sampler), + }, + wgpu::BindGroupEntry { + binding: 10, + resource: wgpu::BindingResource::Sampler(&opacity_sampler), + }, + wgpu::BindGroupEntry { + binding: 11, + resource: wgpu::BindingResource::Sampler(&bump_sampler), + }, + wgpu::BindGroupEntry { + binding: 12, + resource: wgpu::BindingResource::Sampler(&refraction_sampler), + }, + wgpu::BindGroupEntry { + binding: 13, + resource: wgpu::BindingResource::Sampler(&normal_sampler), + }, + wgpu::BindGroupEntry { + binding: 14, + resource: params_buffer.as_entire_binding(), + }, + ], + }) +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct MaterialBatchKey([u32; 16]); + +fn material_key( + material: Option<&crate::scene::model::material_model::MeshMaterial>, + color: [f32; 4], +) -> MaterialBatchKey { + let Some(material) = material else { + return MaterialBatchKey([ + 0, + 0, + color[0].to_bits(), + color[1].to_bits(), + color[2].to_bits(), + color[3].to_bits(), + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ]); + }; + let handle = material.handle.map_or(0, |handle| handle.value()); + MaterialBatchKey([ + handle as u32, + (handle >> 32) as u32, + material.diffuse[0].to_bits(), + material.diffuse[1].to_bits(), + material.diffuse[2].to_bits(), + material.diffuse[3].to_bits(), + material.specular[0].to_bits(), + material.specular[1].to_bits(), + material.specular[2].to_bits(), + material.gloss.to_bits(), + material.reflectivity.to_bits(), + material.self_illumination.to_bits(), + material.luminance.to_bits(), + material.refraction_index.to_bits(), + material.diffuse_map.projection as u32, + material.diffuse_map.tiling as u32, + ]) +} + +struct MeshBatchPart<'a> { + set: &'a MeshLodSet, + mesh: &'a MeshModel, + material: Option<&'a crate::scene::model::material_model::MeshMaterial>, + color: [f32; 4], + indices: Vec, + include_faces: bool, + include_edges: bool, +} + +fn material_map_uv( + map: &crate::scene::model::material_model::MeshTextureMap, + position: [f32; 3], + normal: [f32; 3], +) -> [f32; 2] { + let m = &map.transform; + let p = [ + position[0] * m[0] + position[1] * m[1] + position[2] * m[2] + m[3], + position[0] * m[4] + position[1] * m[5] + position[2] * m[6] + m[7], + position[0] * m[8] + position[1] * m[9] + position[2] * m[10] + m[11], + ]; + match map.projection { + 3 => [ + p[1].atan2(p[0]) / std::f32::consts::TAU + 0.5, + p[2], + ], + 4 => { + let radius = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt(); + if radius <= f32::EPSILON { + [0.0; 2] + } else { + [ + p[1].atan2(p[0]) / std::f32::consts::TAU + 0.5, + (p[2] / radius).clamp(-1.0, 1.0).acos() / std::f32::consts::PI, + ] + } + } + 2 => { + let n = [normal[0].abs(), normal[1].abs(), normal[2].abs()]; + if n[0] >= n[1] && n[0] >= n[2] { + [p[1], p[2]] + } else if n[1] >= n[2] { + [p[0], p[2]] + } else { + [p[0], p[1]] + } + } + _ => [p[0], p[1]], + } +} + +fn material_uvs( + material: Option<&crate::scene::model::material_model::MeshMaterial>, + position: [f32; 3], + normal: [f32; 3], +) -> [[f32; 2]; 7] { + let Some(material) = material else { + return [[0.0; 2]; 7]; + }; + [ + material_map_uv(&material.diffuse_map, position, normal), + material_map_uv(&material.specular_map, position, normal), + material_map_uv(&material.reflection_map, position, normal), + material_map_uv(&material.opacity_map, position, normal), + material_map_uv(&material.bump_map, position, normal), + material_map_uv(&material.refraction_map, position, normal), + material_map_uv(&material.normal_map, position, normal), + ] +} + +fn material_vertex_params( + material: Option<&crate::scene::model::material_model::MeshMaterial>, +) -> ([f32; 4], [f32; 4], [f32; 4], [f32; 4], [u32; 4]) { + let Some(material) = material else { + return ( + [0.5, 0.0, 0.0, 0.0], + [1.0, 1.0, 1.0, 1.0], + [0.3, 0.3, 0.3, 0.0], + [1.0; 4], + [0, 127, 0, 0], + ); + }; + ( + [ + material.gloss, + material.reflectivity, + material.self_illumination, + material.luminance, + ], + [ + material.specular[0], + material.specular[1], + material.specular[2], + material.refraction_index, + ], + [ + material.ambient[0], + material.ambient[1], + material.ambient[2], + material.translucence, + ], + [ + material.normal_map_strength, + material.indirect_bump_scale, + material.reflectance_scale, + material.transmittance_scale, + ], + [ + material.illumination_model as u32, + material.channel_flags as u32, + material.mode as u32, + material.luminance_mode as u32, + ], + ) +} + /// Concatenate every set's first non-empty LOD into a few large GPU buffers. /// Returns the chunks plus the total triangle count drawn (for diagnostics). /// @@ -202,34 +696,181 @@ pub fn build_mesh_batch(device: &wgpu::Device, sets: &[MeshLodSet]) -> (Vec = Vec::new(); let mut edge_verts: Vec = Vec::new(); let mut total_tris = 0u64; + let mut ordered: Vec> = Vec::new(); for set in sets { - let Some(mesh) = set.lods.iter().find(|m| !m.indices.is_empty()) else { + let Some(mesh) = set.lods.iter().find(|mesh| !mesh.indices.is_empty()) else { continue; }; + let triangle_count = mesh.indices.len() / 3; + let has_face_materials = mesh.triangle_material_handles.len() == triangle_count + && !set.face_materials.is_empty(); + let has_face_colors = mesh.triangle_colors.len() == triangle_count + && mesh.triangle_colors.iter().any(Option::is_some); + let include_faces = set + .visual_style + .as_ref() + .map_or(true, |style| style.face_visible()); + let include_edges = set + .visual_style + .as_ref() + .map_or(true, |style| style.edges_visible()); + if !has_face_materials && !has_face_colors { + let base_color = + set.material.as_ref().map_or(mesh.color, |material| material.diffuse); + let color = set + .visual_style + .as_ref() + .map_or(base_color, |style| style.face_color(base_color)); + ordered.push(MeshBatchPart { + set, + mesh, + material: set.material.as_ref(), + color, + indices: mesh.indices.clone(), + include_faces, + include_edges, + }); + continue; + } + let mut groups: std::collections::BTreeMap< + MaterialBatchKey, + ( + Option<&crate::scene::model::material_model::MeshMaterial>, + [f32; 4], + Vec, + ), + > = std::collections::BTreeMap::new(); + for (triangle, indices) in mesh.indices.chunks_exact(3).enumerate() { + let material = if has_face_materials { + mesh.triangle_material_handles[triangle] + .and_then(|handle| set.face_materials.get(&handle)) + .or(set.material.as_ref()) + } else { + set.material.as_ref() + }; + let base_color = material.map_or(mesh.color, |material| material.diffuse); + let base_color = if has_face_colors { + mesh.triangle_colors[triangle].unwrap_or(base_color) + } else { + base_color + }; + let color = set + .visual_style + .as_ref() + .map_or(base_color, |style| style.face_color(base_color)); + groups + .entry(material_key(material, color)) + .or_insert_with(|| (material, color, Vec::new())) + .2 + .extend_from_slice(indices); + } + for (part_index, (_, (material, color, indices))) in groups.into_iter().enumerate() { + ordered.push(MeshBatchPart { + set, + mesh, + material, + color, + indices, + include_faces, + include_edges: include_edges && part_index == 0, + }); + } + } + ordered.sort_by_key(|part| { + material_key(part.material, part.color) + }); + let mut active_key: Option = None; + let mut active_material: Option<&crate::scene::model::material_model::MeshMaterial> = None; + for part in ordered { + let set = part.set; + let mesh = part.mesh; + let material = part.material; + let part_color = part.color; + let key = material_key(material, part_color); + if active_key.is_some_and(|active| active != key) + && (!verts.is_empty() || !edge_verts.is_empty()) + { + chunks.push(make_chunk( + device, + &verts, + &indices, + &transp_indices, + &wire_indices, + &edge_verts, + active_material, + )); + verts.clear(); + indices.clear(); + transp_indices.clear(); + wire_indices.clear(); + edge_verts.clear(); + } + active_key = Some(key); + active_material = material; let has_normals = mesh.normals.len() == mesh.verts.len(); - let vtx = |vi: usize| MeshVertex { - position: mesh.verts[vi], - normal: if has_normals { mesh.normals[vi] } else { [0.0, 1.0, 0.0] }, - color: mesh.color, - position_low: mesh.verts_low.get(vi).copied().unwrap_or([0.0; 3]), + let (material_params, specular, ambient, advanced, flags) = + material_vertex_params(material); + let edge_color = set + .visual_style + .as_ref() + .map_or(mesh.color, |style| style.edge_color(mesh.color)); + let vtx = |vi: usize| { + let normal = if has_normals { + mesh.normals[vi] + } else { + [0.0, 1.0, 0.0] + }; + let uv = material_uvs(material, mesh.verts[vi], normal); + MeshVertex { + position: mesh.verts[vi], + normal, + color: part_color, + position_low: mesh.verts_low.get(vi).copied().unwrap_or([0.0; 3]), + material: material_params, + specular, + uv_diffuse: uv[0], + ambient, + advanced, + flags, + uv_specular: uv[1], + uv_reflection: uv[2], + uv_opacity: uv[3], + uv_bump: uv[4], + uv_refraction: uv[5], + uv_normal: uv[6], + } }; // A solid whose baked colour is not fully opaque routes into the // transparent index stream so it is drawn last, without depth writes. - let is_transp = mesh.color[3] < 0.999; - let mesh_tris = mesh.indices.len() / 3; - total_tris += mesh_tris as u64; + let is_transp = part_color[3] < 0.999; + let mesh_tris = part.indices.len() / 3; + if part.include_faces { + total_tris += mesh_tris as u64; + } // Feature edges present (ACIS solid) → emit the B-rep edges as a line // list and skip the triangulation wireframe. Absent (plain mesh) → keep // the triangle edges so the mesh still shows a wireframe. let has_feat = !set.edge_verts.is_empty(); - if has_feat { + if has_feat && part.include_edges { for i in 0..set.edge_verts.len() { edge_verts.push(MeshVertex { position: set.edge_verts[i], normal: [0.0, 1.0, 0.0], - color: mesh.color, + color: edge_color, position_low: set.edge_verts_low.get(i).copied().unwrap_or([0.0; 3]), + material: material_params, + specular, + uv_diffuse: [0.0; 2], + ambient, + advanced, + flags, + uv_specular: [0.0; 2], + uv_reflection: [0.0; 2], + uv_opacity: [0.0; 2], + uv_bump: [0.0; 2], + uv_refraction: [0.0; 2], + uv_normal: [0.0; 2], }); } } @@ -245,6 +886,7 @@ pub fn build_mesh_batch(device: &wgpu::Device, sets: &[MeshLodSet]) -> (Vec (Vec (Vec (Vec (Vec Self { + pub fn new( + device: &wgpu::Device, + mesh: &MeshModel, + material: Option<&crate::scene::model::material_model::MeshMaterial>, + highlight: Highlight, + ) -> Self { let has_normals = mesh.normals.len() == mesh.verts.len(); // Blend the base colour toward the highlight so a selected / hovered // solid reads clearly while keeping some shape shading. @@ -358,6 +1031,8 @@ impl MeshGpu { } None => mesh.color, }; + let (material_params, specular, ambient, advanced, flags) = + material_vertex_params(material); let vertices: Vec = mesh .verts .iter() @@ -368,11 +1043,24 @@ impl MeshGpu { } else { [0.0, 1.0, 0.0] }; + let uv = material_uvs(material, pos, normal); MeshVertex { position: pos, normal, color, position_low: mesh.verts_low.get(i).copied().unwrap_or([0.0; 3]), + material: material_params, + specular, + uv_diffuse: uv[0], + ambient, + advanced, + flags, + uv_specular: uv[1], + uv_reflection: uv[2], + uv_opacity: uv[3], + uv_bump: uv[4], + uv_refraction: uv[5], + uv_normal: uv[6], } }) .collect(); diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index 9e407ade1..f18907653 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -78,6 +78,8 @@ pub struct Pipeline { /// writes, writes depth). Used in HiddenLine mode so 3D solids /// occlude wires behind them without painting visible pixels. mesh_depth_pipeline: wgpu::RenderPipeline, + mesh_material_bgl: wgpu::BindGroupLayout, + mesh_default_material_bind_group: wgpu::BindGroup, face3d_pipeline: wgpu::RenderPipeline, /// Depth-only variant of the face3d pipeline (no color writes, /// writes depth). Paired with `mesh_depth_pipeline` for HiddenLine. @@ -839,9 +841,144 @@ impl Pipeline { ))), }); + let mesh_material_bgl = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("mesh.material.bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 4, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 5, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 6, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 7, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 8, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 9, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 10, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 11, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 12, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 13, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 14, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + ], + }); + let mesh_default_material_bind_group = mesh_gpu::create_material_bind_group( + device, + queue, + &mesh_material_bgl, + None, + ); + let mesh_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("mesh.pipeline_layout"), - bind_group_layouts: &[&frame_bgl], + bind_group_layouts: &[&frame_bgl, &mesh_material_bgl], push_constant_ranges: &[], }); @@ -1437,6 +1574,8 @@ impl Pipeline { mesh_wireframe_pipeline, mesh_edge_black_pipeline, mesh_depth_pipeline, + mesh_material_bgl, + mesh_default_material_bind_group, face3d_pipeline, face3d_depth_pipeline, uniform_buffer, @@ -1757,6 +1896,18 @@ impl Pipeline { (w.y - hy as f64) as f32, (w.z - hz as f64) as f32, ], + material: [0.5, 0.0, 0.0, 0.0], + specular: [1.0, 1.0, 1.0, 1.0], + uv_diffuse: [0.0; 2], + ambient: [0.3, 0.3, 0.3, 0.0], + advanced: [1.0; 4], + flags: [0, 127, 0, 0], + uv_specular: [0.0; 2], + uv_reflection: [0.0; 2], + uv_opacity: [0.0; 2], + uv_bump: [0.0; 2], + uv_refraction: [0.0; 2], + uv_normal: [0.0; 2], } }; for g in &set.curved_gens { @@ -1863,6 +2014,28 @@ impl Pipeline { } } } + if set.curved_gens.is_empty() || !set.complete { + let best = set.stored_silhouettes.iter().max_by(|left, right| { + let score = |silhouette: &crate::scene::model::mesh_model::StoredSilhouette| { + let direction = d3(silhouette.view_direction) + .normalize_or(glam::DVec3::NEG_Z); + direction.dot(view).abs() + }; + score(left) + .partial_cmp(&score(right)) + .unwrap_or(std::cmp::Ordering::Equal) + }); + if let Some(silhouette) = best { + for (index, high) in silhouette.edge_verts.iter().copied().enumerate() { + let low = silhouette + .edge_verts_low + .get(index) + .copied() + .unwrap_or([0.0; 3]); + verts.push(mk(lo(high, low))); + } + } + } } if verts.is_empty() { self.silhouette_vbuf = None; @@ -2020,8 +2193,21 @@ impl Pipeline { /// set, drawn in a handful of calls). Selection/hover tint is intentionally /// not applied here — the batch is geometry-only and stays resident across /// camera moves and pick changes. - pub fn upload_mesh_batch(&mut self, device: &wgpu::Device, meshes: &[MeshLodSet]) { - let (chunks, _tris) = mesh_gpu::build_mesh_batch(device, meshes); + pub fn upload_mesh_batch( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + meshes: &[MeshLodSet], + ) { + let (mut chunks, _tris) = mesh_gpu::build_mesh_batch(device, meshes); + for chunk in &mut chunks { + chunk.material_bind_group = Some(mesh_gpu::create_material_bind_group( + device, + queue, + &self.mesh_material_bgl, + chunk.material.as_ref(), + )); + } self.gpu_mesh_batch = chunks; } @@ -2051,7 +2237,7 @@ impl Pipeline { Some(h) if Some(h) == hover => Highlight::Hover, _ => continue, }; - out.push(MeshGpu::new(device, m, mode)); + out.push(MeshGpu::new(device, m, set.material.as_ref(), mode)); } self.gpu_mesh_highlight = out; } @@ -2377,6 +2563,13 @@ impl Pipeline { // so both the opaque and the transparent tris write depth here. pass.set_pipeline(&self.mesh_depth_pipeline); for c in &self.gpu_mesh_batch { + pass.set_bind_group( + 1, + c.material_bind_group + .as_ref() + .unwrap_or(&self.mesh_default_material_bind_group), + &[], + ); pass.set_vertex_buffer(0, c.vertex_buffer.slice(..)); if c.index_count != 0 { pass.set_index_buffer( @@ -2395,6 +2588,13 @@ impl Pipeline { } pass.set_pipeline(&self.mesh_wireframe_pipeline); for c in &self.gpu_mesh_batch { + pass.set_bind_group( + 1, + c.material_bind_group + .as_ref() + .unwrap_or(&self.mesh_default_material_bind_group), + &[], + ); // Plain-mesh triangulation edges. if c.wire_index_count != 0 { pass.set_vertex_buffer(0, c.vertex_buffer.slice(..)); @@ -2419,6 +2619,13 @@ impl Pipeline { if mesh_wireframe { pass.set_pipeline(&self.mesh_wireframe_pipeline); for c in &self.gpu_mesh_batch { + pass.set_bind_group( + 1, + c.material_bind_group + .as_ref() + .unwrap_or(&self.mesh_default_material_bind_group), + &[], + ); if c.wire_index_count != 0 { pass.set_vertex_buffer(0, c.vertex_buffer.slice(..)); pass.set_index_buffer( @@ -2441,6 +2648,13 @@ impl Pipeline { // Opaque fills first (they write depth). pass.set_pipeline(&self.mesh_pipeline); for c in &self.gpu_mesh_batch { + pass.set_bind_group( + 1, + c.material_bind_group + .as_ref() + .unwrap_or(&self.mesh_default_material_bind_group), + &[], + ); if c.index_count == 0 { continue; } @@ -2453,6 +2667,13 @@ impl Pipeline { // culling it via the depth buffer. pass.set_pipeline(&self.mesh_transparent_pipeline); for c in &self.gpu_mesh_batch { + pass.set_bind_group( + 1, + c.material_bind_group + .as_ref() + .unwrap_or(&self.mesh_default_material_bind_group), + &[], + ); if c.transp_index_count == 0 { continue; } @@ -2469,6 +2690,7 @@ impl Pipeline { // the tint shows on top even when the solid is behind others. if !self.gpu_mesh_highlight.is_empty() { pass.set_pipeline(&self.mesh_highlight_pipeline); + pass.set_bind_group(1, &self.mesh_default_material_bind_group, &[]); for g in &self.gpu_mesh_highlight { if g.index_count == 0 { continue; @@ -2484,6 +2706,13 @@ impl Pipeline { if want_solid_with_edges { pass.set_pipeline(&self.mesh_edge_black_pipeline); for c in &self.gpu_mesh_batch { + pass.set_bind_group( + 1, + c.material_bind_group + .as_ref() + .unwrap_or(&self.mesh_default_material_bind_group), + &[], + ); if c.wire_index_count != 0 { pass.set_vertex_buffer(0, c.vertex_buffer.slice(..)); pass.set_index_buffer( diff --git a/src/scene/view/dispatch.rs b/src/scene/view/dispatch.rs index c1bc6c197..893e0c08a 100644 --- a/src/scene/view/dispatch.rs +++ b/src/scene/view/dispatch.rs @@ -215,6 +215,7 @@ pub fn toggle_invisible(entity: &mut EntityType) { pub fn apply_color(entity: &mut EntityType, color: AcadColor) { entity.as_entity_mut().set_color(color); + entity.common_mut().color_book_handle = None; } pub fn apply_line_weight(entity: &mut EntityType, lw: LineWeight) { diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 372b87ced..d99736f5a 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -768,7 +768,7 @@ impl shader::Primitive for Primitive { .as_ref() .map_or(true, |source| !Arc::ptr_eq(source, &vp.meshes)) { - inner.upload_mesh_batch(device, &vp.meshes[..]); + inner.upload_mesh_batch(device, queue, &vp.meshes[..]); inner.cached_mesh_source = Some(Arc::clone(&vp.meshes)); inner.cached_mesh_batch_epoch = vp.geometry_epoch; } @@ -1132,7 +1132,16 @@ pub(in crate::scene) fn render_style_for( ) -> ([f32; 4], f32, [f32; 8], f32, u8) { let layer_name = &e.common().layer; let (entity_color, aci) = { - let ec = &e.common().color; + let common = e.common(); + let book_color = common + .color_book_handle + .filter(|handle| handle.is_valid()) + .and_then(|handle| document.objects.get(&handle)) + .and_then(|object| match object { + acadrust::objects::ObjectType::BookColor(book) => Some(&book.color), + _ => None, + }); + let ec = book_color.unwrap_or(&common.color); let resolved = if *ec == AcadColor::ByLayer { document .layers @@ -1176,6 +1185,14 @@ pub(in crate::scene) fn render_style_for( (entity_color, pattern_length, pattern, line_weight_px, aci) } +pub(crate) fn has_resolved_book_color(document: &CadDocument, e: &EntityType) -> bool { + e.common() + .color_book_handle + .filter(|handle| handle.is_valid()) + .and_then(|handle| document.objects.get(&handle)) + .is_some_and(|object| matches!(object, acadrust::objects::ObjectType::BookColor(_))) +} + /// Resolved render style used as the inheritance source for a block child's /// ByBlock properties (the INSERT's own style) or its layer-0 properties (the /// INSERT's *layer* style). Bundled so it threads through the block-expansion @@ -1254,9 +1271,10 @@ pub(crate) fn render_style_for_block_sub( let common = e.common(); let on_l0 = is_effective_layer_zero(&common.layer); - let final_color = if common.color == AcadColor::ByBlock { + let has_book_color = has_resolved_book_color(document, e); + let final_color = if !has_book_color && common.color == AcadColor::ByBlock { insert_color - } else if on_l0 && common.color == AcadColor::ByLayer { + } else if !has_book_color && on_l0 && common.color == AcadColor::ByLayer { // Inherit the insert layer's RGB but keep the child's own transparency. [l0.color[0], l0.color[1], l0.color[2], color[3]] } else { diff --git a/src/shaders/mesh.wgsl b/src/shaders/mesh.wgsl index 19992716d..37046512f 100644 --- a/src/shaders/mesh.wgsl +++ b/src/shaders/mesh.wgsl @@ -27,11 +27,51 @@ struct Uniforms { @group(0) @binding(0) var u: Uniforms; +struct MaterialMaps { + blends0: vec4, + present0: vec4, + blends1: vec4, + present1: vec4, +}; + +@group(1) @binding(0) var diffuse_map: texture_2d; +@group(1) @binding(1) var specular_map: texture_2d; +@group(1) @binding(2) var reflection_map: texture_2d; +@group(1) @binding(3) var opacity_map: texture_2d; +@group(1) @binding(4) var bump_map: texture_2d; +@group(1) @binding(5) var refraction_map: texture_2d; +@group(1) @binding(6) var normal_map: texture_2d; +@group(1) @binding(7) var diffuse_sampler: sampler; +@group(1) @binding(8) var specular_sampler: sampler; +@group(1) @binding(9) var reflection_sampler: sampler; +@group(1) @binding(10) var opacity_sampler: sampler; +@group(1) @binding(11) var bump_sampler: sampler; +@group(1) @binding(12) var refraction_sampler: sampler; +@group(1) @binding(13) var normal_sampler: sampler; +@group(1) @binding(14) var maps: MaterialMaps; + struct VertexIn { @location(0) position: vec3, @location(1) normal: vec3, @location(2) color: vec4, @location(3) position_low: vec3, + // gloss, reflectivity, self illumination, luminance + @location(4) material: vec4, + // specular RGB, refraction index + @location(5) specular: vec4, + @location(6) uv_diffuse: vec2, + // ambient RGB, translucence + @location(7) ambient: vec4, + // normal strength, bump scale, reflectance scale, transmittance scale + @location(8) advanced: vec4, + // illumination model, channel flags, material mode, luminance mode + @location(9) flags: vec4, + @location(10) uv_specular: vec2, + @location(11) uv_reflection: vec2, + @location(12) uv_opacity: vec2, + @location(13) uv_bump: vec2, + @location(14) uv_refraction: vec2, + @location(15) uv_normal: vec2, }; struct VertexOut { @@ -39,6 +79,19 @@ struct VertexOut { @location(0) color: vec4, @location(1) normal: vec3, @location(2) world_pos: vec3, + @location(3) material: vec4, + @location(4) specular: vec4, + @location(5) eye_vec: vec3, + @location(6) uv_diffuse: vec2, + @location(7) ambient: vec4, + @location(8) advanced: vec4, + @location(9) @interpolate(flat) flags: vec4, + @location(10) uv_specular: vec2, + @location(11) uv_reflection: vec2, + @location(12) uv_opacity: vec2, + @location(13) uv_bump: vec2, + @location(14) uv_refraction: vec2, + @location(15) uv_normal: vec2, }; @vertex @@ -48,7 +101,20 @@ fn vs_main(v: VertexIn) -> VertexOut { out.clip_pos = u.view_rot * vec4(rel, 1.0); out.color = v.color; out.normal = v.normal; - out.world_pos = v.position; + out.world_pos = rel; + out.material = v.material; + out.specular = v.specular; + out.eye_vec = -rel; + out.uv_diffuse = v.uv_diffuse; + out.ambient = v.ambient; + out.advanced = v.advanced; + out.flags = v.flags; + out.uv_specular = v.uv_specular; + out.uv_reflection = v.uv_reflection; + out.uv_opacity = v.uv_opacity; + out.uv_bump = v.uv_bump; + out.uv_refraction = v.uv_refraction; + out.uv_normal = v.uv_normal; return out; } @@ -64,6 +130,37 @@ fn fs_main(in: VertexOut) -> @location(0) vec4 { n = normalize(in.normal); } + // Build a derivative tangent frame from the generated material UVs. This + // works for planar/box/cylindrical/spherical AcDbMaterial projections and + // does not require ACIS to carry explicit texture-coordinate vertices. + let dp1 = dpdx(in.world_pos); + let dp2 = dpdy(in.world_pos); + let tangent_uv = select(in.uv_bump, in.uv_normal, maps.present1.z != 0u); + let duv1 = dpdx(tangent_uv); + let duv2 = dpdy(tangent_uv); + let det = duv1.x * duv2.y - duv1.y * duv2.x; + var tangent = normalize(dp1); + var bitangent = normalize(cross(n, tangent)); + if (abs(det) > 1e-8) { + tangent = normalize((dp1 * duv2.y - dp2 * duv1.y) / det); + bitangent = normalize((-dp1 * duv2.x + dp2 * duv1.x) / det); + } + if (maps.present1.z != 0u) { + let sampled = textureSample(normal_map, normal_sampler, in.uv_normal).xyz * 2.0 - 1.0; + let mapped = normalize( + tangent * sampled.x + bitangent * sampled.y + n * sampled.z + ); + let strength = maps.blends1.z * max(in.advanced.x, 0.0); + n = normalize(mix(n, mapped, clamp(strength, 0.0, 1.0))); + } else if (maps.present1.x != 0u) { + let height = dot( + textureSample(bump_map, bump_sampler, in.uv_bump).rgb, + vec3(0.2126, 0.7152, 0.0722), + ); + let slope = tangent * dpdx(height) + bitangent * dpdy(height); + 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. @@ -71,13 +168,69 @@ fn fs_main(in: VertexOut) -> @location(0) vec4 { 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 ambient = 0.35; - let diff = ambient - + 0.45 * abs(dot(n, l0)) + let diff = 0.45 * abs(dot(n, l0)) + 0.30 * abs(dot(n, l1)) + 0.25 * abs(dot(n, l2)); - let rgb = in.color.rgb * clamp(diff, 0.0, 1.0); - return vec4(rgb, in.color.a); + let view = normalize(in.eye_vec); + let half_vec = normalize(l0 + 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), + 2.0, + ); + let fresnel = fresnel0 + + (1.0 - fresnel0) * pow(1.0 - abs(dot(n, view)), 5.0); + let specular_strength = clamp(0.08 + in.material.y + fresnel, 0.0, 1.5); + var specular_color = in.specular.rgb; + if (maps.present0.y != 0u) { + 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 specular = specular_color + * pow(max(abs(dot(n, half_vec)), 0.0), gloss_exp) + * specular_strength + * max(in.advanced.z, 0.0); + var albedo = in.color.rgb; + if (maps.present0.x != 0u) { + let texel = textureSample(diffuse_map, diffuse_sampler, in.uv_diffuse).rgb; + 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; + if (maps.present0.z != 0u) { + let reflected = textureSample( + reflection_map, + reflection_sampler, + in.uv_reflection, + ).rgb; + lit = mix( + lit, + reflected, + clamp(maps.blends0.z * in.material.y * max(in.advanced.z, 0.0), 0.0, 1.0), + ); + } + if (maps.present1.y != 0u) { + let transmitted = textureSample( + refraction_map, + refraction_sampler, + in.uv_refraction, + ).rgb; + lit = mix( + lit, + transmitted, + clamp(maps.blends1.y * in.ambient.a * max(in.advanced.w, 0.0), 0.0, 1.0), + ); + } + let emission = clamp(in.material.z + in.material.w, 0.0, 1.0); + let rgb = mix(lit, albedo, emission); + var material_alpha = in.color.a; + if (maps.present0.w != 0u) { + let opacity = textureSample(opacity_map, opacity_sampler, in.uv_opacity).r; + material_alpha *= mix(1.0, opacity, clamp(maps.blends0.w, 0.0, 1.0)); + } + material_alpha *= 1.0 - clamp(in.ambient.a * max(in.advanced.w, 0.0), 0.0, 0.95); + let alpha = select(1.0, material_alpha, u.transparency_enable > 0.5); + return vec4(rgb, alpha); } // Edge fragment (LineList): flat entity colour, no lighting. Used for the