use codec::entities::{ ArcAlignedTextData, ExtendedEntity, ExtendedEntityData, GeoPositionMarkerData, PointCloudData, PointCloudExData, RemoteTextData, SectionObjectData, }; use codec::types::{Handle, Transform, Vector3}; use codec::xdata::{ExtendedDataRecord, XDataValue}; use crate::t; 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, RenderConvertible}; use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject}; use crate::scene::model::object::{ GripApply, GripDef, GripMenuAction, GripMenuItem, PropSection, PropValue, Property, }; use crate::scene::model::wire_model::SnapHint; const NAN: [f64; 3] = [f64::NAN; 3]; const SECTION_SLICE_APP: &str = "IsSlice"; const SECTION_THICKNESS_APP: &str = "ThicknessDepth"; const SECTION_GRIP_LEFT: usize = 100_000; const SECTION_GRIP_RIGHT: usize = 100_001; const SECTION_GRIP_TOP: usize = 100_002; const SECTION_GRIP_BOTTOM: usize = 100_003; const SECTION_GRIP_NORMAL: usize = 100_004; const SECTION_GRIP_STATE: usize = 100_005; pub(crate) fn section_is_slice(entity: &ExtendedEntity) -> bool { entity .common .extended_data .get_record(SECTION_SLICE_APP) .is_some_and(|record| { record .values .iter() .rev() .find_map(|value| match value { XDataValue::Integer16(value) => Some(*value != 0), XDataValue::Integer32(value) => Some(*value != 0), _ => None, }) .unwrap_or(false) }) } pub(crate) fn section_slice_depth(entity: &ExtendedEntity) -> Option { entity .common .extended_data .get_record(SECTION_THICKNESS_APP)? .values .iter() .find_map(|value| match value { XDataValue::Real(value) | XDataValue::Distance(value) => Some(*value), _ => None, }) .filter(|value| value.is_finite()) .map(f64::abs) } pub(crate) fn set_section_slice_metadata( entity: &mut ExtendedEntity, enabled: bool, depth: f64, ) { if !enabled { entity.common.extended_data.remove_record(SECTION_SLICE_APP); entity .common .extended_data .remove_record(SECTION_THICKNESS_APP); return; } let mut slice = ExtendedDataRecord::new(SECTION_SLICE_APP); slice.values = vec![XDataValue::Integer16(0), XDataValue::Integer16(1)]; entity.common.extended_data.upsert_record(slice); let mut thickness = ExtendedDataRecord::new(SECTION_THICKNESS_APP); thickness.values = vec![XDataValue::Real(if depth.is_finite() { depth.abs() } else { 0.0 })]; entity.common.extended_data.upsert_record(thickness); } 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: &str, field: &'static str, value: &str) -> Property { Property { label: label.into(), field, value: PropValue::PlainText(value.to_string()), } } fn bool_prop(label: &str, field: &'static str, value: bool) -> Property { Property { label: label.into(), field, value: PropValue::BoolToggle { field, value }, } } fn choice_prop( label: &str, field: &'static str, selected: &str, options: &[&str], ) -> Property { Property { label: label.into(), field, value: PropValue::Choice { selected: selected.to_string(), options: options.iter().map(|option| (*option).to_string()).collect(), }, } } 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(); let vertical = normalized(data.vertical_direction, Vector3::UNIT_Z); let offset = |point: &Vector3, distance: f64| { [ point.x + vertical.x * distance, point.y + vertical.y * distance, point.z + vertical.z * distance, ] }; push_chain( &mut points, data.vertices.iter().map(|p| [p.x, p.y, p.z]), ); push_chain( &mut points, data.vertices.iter().map(|point| offset(point, data.top_height)), ); push_chain( &mut points, data.vertices.iter().map(|point| offset(point, -data.bottom_height)), ); for point in &data.vertices { push_segment( &mut points, offset(point, -data.bottom_height), offset(point, data.top_height), ); } push_chain( &mut points, data.back_line_vertices.iter().map(|p| [p.x, p.y, p.z]), ); if !data.back_line_vertices.is_empty() { push_chain( &mut points, data.back_line_vertices .iter() .map(|point| offset(point, data.top_height)), ); push_chain( &mut points, data.back_line_vertices .iter() .map(|point| offset(point, -data.bottom_height)), ); for point in &data.back_line_vertices { push_segment( &mut points, offset(point, -data.bottom_height), offset(point, data.top_height), ); } if let (Some(front), Some(back)) = ( data.vertices.first().zip(data.vertices.last()), data.back_line_vertices .first() .zip(data.back_line_vertices.last()), ) { for (a, b) in [(front.0, back.0), (front.1, back.1)] { push_segment(&mut points, offset(a, data.top_height), offset(b, data.top_height)); push_segment( &mut points, offset(a, -data.bottom_height), offset(b, -data.bottom_height), ); } } } 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); points } fn point_cloud_clip_lines(data: &PointCloudData) -> Vec<[f64; 3]> { let mut points = Vec::new(); 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); points } fn point_cloud_ex_clip_lines(data: &PointCloudExData) -> Vec<[f64; 3]> { let mut points = Vec::new(); 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 } pub(crate) fn point_cloud_frame_lines(entity: &ExtendedEntity) -> Option> { match &entity.data { ExtendedEntityData::PointCloud(data) => Some(point_cloud_clip_lines(data)), ExtendedEntityData::PointCloudEx(data) => Some(point_cloud_ex_clip_lines(data)), _ => None, } } fn camera_lines(document: &codec::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_render(entity: &ExtendedEntity, document: &codec::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(RenderEntity { object: RenderObject::Lines(points), snap_pts: snaps, tangent_geoms: Vec::new(), key_vertices: keys, fill_tris: Vec::new(), pick_tris: Vec::new(), }) } fn section_properties(entity: &ExtendedEntity, data: &SectionObjectData) -> Vec { let vertices = data .vertices .iter() .map(|point| vector_text(*point)) .collect::>() .join("; "); let kind = section_kind(entity, data); let viewing = section_viewing_direction(data); let offset = section_plane_offset(data); let depth = section_slice_depth(entity).unwrap_or_else(|| section_depth(data)); vec![PropSection { title: t!("Section Plane").into_owned(), props: vec![ text_prop(t!("Name").as_ref(), "ext_section_name", &data.name), choice_prop( t!("State").as_ref(), "ext_section_state", kind, &["Plane", "Slice", "Boundary", "Volume"], ), text_prop( t!("Viewing Direction").as_ref(), "ext_section_viewing", &vector_text(viewing), ), text_prop( t!("Vertical Direction").as_ref(), "ext_section_vertical", &vector_text(data.vertical_direction), ), ro_prop(t!("Normal").as_ref(), "ext_section_normal", vector_text(viewing)), bool_prop( t!("Live Section Enabled").as_ref(), "ext_section_live", data.flags & 1 != 0, ), edit_prop( t!("Indicator Transparency").as_ref(), "ext_section_alpha", f64::from(data.indicator_alpha), ), Property { label: t!("Indicator Fill Color").into_owned(), field: "indicator_fill_color", value: PropValue::ColorChoice(data.indicator_color), }, edit_prop(t!("Elevation").as_ref(), "ext_section_elevation", offset), edit_prop(t!("Top Height").as_ref(), "ext_section_top", data.top_height), edit_prop(t!("Bottom Height").as_ref(), "ext_section_bottom", data.bottom_height), ro_prop( t!("Number of Vertices").as_ref(), "ext_section_vertex_count", data.vertices.len().to_string(), ), text_prop(t!("Vertices").as_ref(), "ext_section_vertices", &vertices), ro_prop( t!("Settings").as_ref(), "ext_section_settings", handle_text(data.settings_handle), ), choice_prop( t!("State2").as_ref(), "ext_section_state2", kind, &["Plane", "Slice", "Boundary", "Volume"], ), edit_prop(t!("Slice Depth").as_ref(), "ext_section_depth", depth), edit_prop( t!("Section Plane Offset").as_ref(), "ext_section_offset", offset, ), ], }] } fn section_tangent(data: &SectionObjectData) -> Vector3 { let Some((first, last)) = data.vertices.first().zip(data.vertices.last()) else { return Vector3::UNIT_X; }; normalized(*last - *first, Vector3::UNIT_X) } fn section_viewing_direction(data: &SectionObjectData) -> Vector3 { let tangent = section_tangent(data); let vertical = normalized(data.vertical_direction, Vector3::UNIT_Z); let base = normalized(vertical.cross(&tangent), Vector3::new(0.0, 0.0, -1.0)); if data.flags & 4 != 0 { base } else { -base } } fn section_depth(data: &SectionObjectData) -> f64 { data.vertices .first() .zip(data.back_line_vertices.first()) .map_or(0.0, |(front, back)| (*back - *front).length()) } fn section_kind(entity: &ExtendedEntity, data: &SectionObjectData) -> &'static str { if section_is_slice(entity) { return "Slice"; } match data.state { 1 => "Plane", 4 => "Volume", 2 => "Boundary", _ => "Plane", } } fn section_plane_offset(data: &SectionObjectData) -> f64 { let Some(first) = data.vertices.first() else { return 0.0; }; -section_viewing_direction(data).dot(first) } fn parse_vector(value: &str) -> Option { let values = value .split(|character: char| character == ',' || character == ';' || character.is_whitespace()) .filter(|part| !part.is_empty()) .map(str::parse::) .collect::, _>>() .ok()?; (values.len() == 3).then(|| Vector3::new(values[0], values[1], values[2])) } fn parse_vertices(value: &str) -> Option> { let values = value .split(|character: char| { character == ',' || character == ';' || character == ':' || character.is_whitespace() }) .filter(|part| !part.trim().is_empty()) .map(str::parse::) .collect::, _>>() .ok()?; if values.len() < 6 || values.len() % 3 != 0 { return None; } let vertices = values .chunks_exact(3) .map(|point| Vector3::new(point[0], point[1], point[2])) .collect::>(); let valid = vertices .first() .zip(vertices.last()) .is_some_and(|(first, last)| (*last - *first).length() > 1e-12); valid.then_some(vertices) } fn set_section_depth(data: &mut SectionObjectData, depth: f64) { let depth = depth.max(0.0); if depth <= 1e-12 || data.vertices.is_empty() { data.back_line_vertices.clear(); return; } let viewing = section_viewing_direction(data); data.back_line_vertices = data .vertices .iter() .map(|point| *point + viewing * depth) .collect(); } fn set_section_kind(data: &mut SectionObjectData, value: &str) { let span = data .vertices .first() .zip(data.vertices.last()) .map_or(1.0, |(first, last)| (*last - *first).length().max(1e-4)); match value.trim().to_ascii_uppercase().as_str() { "SLICE" => { data.state = 1; data.back_line_vertices.clear(); } "BOUNDARY" => { data.state = 2; set_section_depth(data, span); } "VOLUME" => { data.state = 4; set_section_depth(data, span); } _ => { data.state = 1; data.back_line_vertices.clear(); } } } fn move_section_to_offset(data: &mut SectionObjectData, desired: f64) { let current = section_plane_offset(data); let delta = section_viewing_direction(data) * (current - desired); for point in data.vertices.iter_mut().chain(data.back_line_vertices.iter_mut()) { *point = *point + delta; } } fn set_viewing_direction(data: &mut SectionObjectData, value: Vector3) { if value.length() <= 1e-12 || data.vertices.len() < 2 { return; } let desired = normalized(value, section_viewing_direction(data)); let tangent = section_tangent(data); let vertical = tangent.cross(&desired); if vertical.length() <= 1e-12 { return; } data.vertical_direction = normalized(vertical, data.vertical_direction); data.flags |= 4; let depth = section_depth(data); if depth > 0.0 { set_section_depth(data, depth); } } fn arc_text_properties(data: &ArcAlignedTextData) -> Vec { vec![PropSection { title: t!("Arc-Aligned Text").into_owned(), props: vec![ text_prop(t!("Text").as_ref(), "ext_arc_text", &data.text), text_prop(t!("Font").as_ref(), "ext_arc_font", &data.font_name), text_prop(t!("Big Font").as_ref(), "ext_arc_big_font", &data.big_font_name), text_prop(t!("Style").as_ref(), "ext_arc_style", &data.style_name), ro_prop(t!("Center").as_ref(), "ext_arc_center", vector_text(data.center)), edit_prop(t!("Radius").as_ref(), "ext_arc_radius", data.radius), edit_prop(t!("X Scale").as_ref(), "ext_arc_xscale", data.x_scale), edit_prop(t!("Text Size").as_ref(), "ext_arc_size", data.text_size), edit_prop(t!("Character Spacing").as_ref(), "ext_arc_spacing", data.character_spacing, ), edit_prop(t!("Offset From Arc").as_ref(), "ext_arc_offset", data.offset_from_arc, ), edit_prop(t!("Right Offset").as_ref(), "ext_arc_right", data.right_offset), edit_prop(t!("Left Offset").as_ref(), "ext_arc_left", data.left_offset), edit_angle_prop(t!("Start Angle").as_ref(), "ext_arc_start", data.start_angle.to_degrees(), ), edit_angle_prop(t!("End Angle").as_ref(), "ext_arc_end", data.end_angle.to_degrees()), bool_prop(t!("Reverse").as_ref(), "ext_arc_reverse", data.reverse), ro_prop(t!("Text Direction").as_ref(), "ext_arc_direction", data.text_direction.to_string(), ), ro_prop(t!("Alignment").as_ref(), "ext_arc_alignment", data.alignment.to_string()), ro_prop(t!("Text Position").as_ref(), "ext_arc_position", data.text_position.to_string(), ), bool_prop(t!("Bold").as_ref(), "ext_arc_bold", data.bold), bool_prop(t!("Italic").as_ref(), "ext_arc_italic", data.italic), bool_prop(t!("Underlined").as_ref(), "ext_arc_underlined", data.underlined), ro_prop(t!("Character Set").as_ref(), "ext_arc_charset", data.character_set.to_string(), ), ro_prop(t!("Pitch And Family").as_ref(), "ext_arc_pitch", data.pitch_and_family.to_string(), ), bool_prop(t!("SHX").as_ref(), "ext_arc_shx", data.is_shx), ro_prop(t!("Text Color").as_ref(), "ext_arc_color", data.text_color.to_string()), ro_prop(t!("Normal").as_ref(), "ext_arc_normal", vector_text(data.normal)), bool_prop(t!("Wizard Flag").as_ref(), "ext_arc_wizard", data.wizard_flag), ro_prop(t!("Arc").as_ref(), "ext_arc_handle", handle_text(data.arc_handle)), ], }] } fn remote_text_properties(data: &RemoteTextData) -> Vec { vec![PropSection { title: t!("Remote Text").into_owned(), props: vec![ text_prop(t!("Text").as_ref(), "ext_rtext_text", &data.text), ro_prop(t!("Position").as_ref(), "ext_rtext_position", vector_text(data.position)), ro_prop(t!("Normal").as_ref(), "ext_rtext_normal", vector_text(data.normal)), edit_angle_prop(t!("Rotation").as_ref(), "ext_rtext_rotation", data.rotation.to_degrees(), ), edit_prop(t!("Height").as_ref(), "ext_rtext_height", data.height), text_prop(t!("Style Name").as_ref(), "ext_rtext_style", &data.style_name), ro_prop(t!("Style Handle").as_ref(), "ext_rtext_style_handle", handle_text(data.style_handle), ), ro_prop(t!("Flags").as_ref(), "ext_rtext_flags", data.flags.to_string()), ], }] } fn geo_marker_properties(data: &GeoPositionMarkerData) -> Vec { vec![PropSection { title: t!("Geographic Position Marker").into_owned(), props: vec![ ro_prop(t!("Class Version").as_ref(), "ext_geo_version", data.class_version.to_string(), ), ro_prop(t!("Position").as_ref(), "ext_geo_position", vector_text(data.position)), edit_prop(t!("Radius").as_ref(), "ext_geo_radius", data.radius), text_prop(t!("Notes").as_ref(), "ext_geo_notes", &data.notes), edit_prop(t!("Landing Gap").as_ref(), "ext_geo_gap", data.landing_gap), bool_prop(t!("Text Visible").as_ref(), "ext_geo_visible", data.mtext_visible), ro_prop(t!("Text Alignment").as_ref(), "ext_geo_alignment", data.text_alignment.to_string(), ), bool_prop(t!("Frame Text").as_ref(), "ext_geo_frame", data.enable_frame_text), ro_prop(t!("Embedded MText").as_ref(), "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: t!("Point Cloud").into_owned(), props: vec![ ro_prop(t!("Class Version").as_ref(), "ext_pc_version", data.class_version.to_string(), ), ro_prop(t!("Origin").as_ref(), "ext_pc_origin", vector_text(data.origin)), text_prop(t!("Saved File").as_ref(), "ext_pc_file", &data.saved_filename), ro_prop(t!("Source Files").as_ref(), "ext_pc_sources", data.source_files.join("\n"), ), ro_prop(t!("Extents Min").as_ref(), "ext_pc_min", vector_text(data.extents_min)), ro_prop(t!("Extents Max").as_ref(), "ext_pc_max", vector_text(data.extents_max)), ro_prop(t!("Point Count").as_ref(), "ext_pc_count", data.point_count.to_string(), ), text_prop(t!("UCS Name").as_ref(), "ext_pc_ucs", &data.ucs_name), ro_prop(t!("UCS Origin").as_ref(), "ext_pc_ucs_origin", vector_text(data.ucs_origin), ), ro_prop(t!("UCS X").as_ref(), "ext_pc_ucs_x", vector_text(data.ucs_x_direction), ), ro_prop(t!("UCS Y").as_ref(), "ext_pc_ucs_y", vector_text(data.ucs_y_direction), ), ro_prop(t!("UCS Z").as_ref(), "ext_pc_ucs_z", vector_text(data.ucs_z_direction), ), ro_prop(t!("Definition").as_ref(), "ext_pc_definition", handle_text(data.definition_handle), ), ro_prop(t!("Reactor").as_ref(), "ext_pc_reactor", handle_text(data.reactor_handle), ), ], }, PropSection { title: t!("Point Cloud Display").into_owned(), props: vec![ bool_prop(t!("Show Intensity").as_ref(), "ext_pc_show_intensity", data.show_intensity), ro_prop(t!("Intensity Scheme").as_ref(), "ext_pc_intensity_scheme", data.intensity_scheme.to_string(), ), edit_prop(t!("Minimum Intensity").as_ref(), "ext_pc_intensity_min", data.minimum_intensity, ), edit_prop(t!("Maximum Intensity").as_ref(), "ext_pc_intensity_max", data.maximum_intensity, ), edit_prop(t!("Low Threshold").as_ref(), "ext_pc_low_threshold", data.low_intensity_threshold, ), edit_prop(t!("High Threshold").as_ref(), "ext_pc_high_threshold", data.high_intensity_threshold, ), bool_prop(t!("Show Clipping").as_ref(), "ext_pc_show_clipping", data.show_clipping), ro_prop(t!("Clippings").as_ref(), "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: t!("Point Cloud Ex").into_owned(), props: vec![ ro_prop(t!("Class Version").as_ref(), "ext_pcx_version", data.class_version.to_string(), ), text_prop(t!("Name").as_ref(), "ext_pcx_name", &data.name), ro_prop(t!("Extents Min").as_ref(), "ext_pcx_min", vector_text(data.extents_min)), ro_prop(t!("Extents Max").as_ref(), "ext_pcx_max", vector_text(data.extents_max)), ro_prop(t!("UCS Origin").as_ref(), "ext_pcx_ucs_origin", vector_text(data.ucs_origin), ), ro_prop(t!("UCS X").as_ref(), "ext_pcx_ucs_x", vector_text(data.ucs_x_direction), ), ro_prop(t!("UCS Y").as_ref(), "ext_pcx_ucs_y", vector_text(data.ucs_y_direction), ), ro_prop(t!("UCS Z").as_ref(), "ext_pcx_ucs_z", vector_text(data.ucs_z_direction), ), bool_prop(t!("Locked").as_ref(), "ext_pcx_locked", data.locked), ro_prop(t!("Definition").as_ref(), "ext_pcx_definition", handle_text(data.definition_handle), ), ro_prop(t!("Reactor").as_ref(), "ext_pcx_reactor", handle_text(data.reactor_handle), ), ], }, PropSection { title: t!("Point Cloud Ex Display").into_owned(), props: vec![ bool_prop(t!("Show Intensity").as_ref(), "ext_pcx_show_intensity", data.show_intensity, ), bool_prop(t!("Show Cropping").as_ref(), "ext_pcx_show_cropping", data.show_cropping), ro_prop(t!("Unknown Flags").as_ref(), "ext_pcx_unknown", format!("{}, {}", data.unknown_bl0, data.unknown_bl1), ), ro_prop(t!("Stylization Type").as_ref(), "ext_pcx_stylization", data.stylization_type.to_string(), ), text_prop(t!("Intensity Color Scheme").as_ref(), "ext_pcx_intensity_scheme", &data.intensity_color_scheme, ), text_prop(t!("Current Color Scheme").as_ref(), "ext_pcx_current_scheme", &data.current_color_scheme, ), text_prop(t!("Classification Scheme").as_ref(), "ext_pcx_class_scheme", &data.classification_color_scheme, ), edit_prop(t!("Elevation Min").as_ref(), "ext_pcx_elevation_min", data.elevation_min), edit_prop(t!("Elevation Max").as_ref(), "ext_pcx_elevation_max", data.elevation_max), ro_prop(t!("Intensity Range").as_ref(), "ext_pcx_intensity_range", format!("{}..{}", data.intensity_min, data.intensity_max), ), ro_prop(t!("Out Of Range Behavior").as_ref(), "ext_pcx_out_of_range", format!( "intensity {}; elevation {}", data.intensity_out_of_range_behavior, data.elevation_out_of_range_behavior ), ), bool_prop(t!("Fixed Elevation Range").as_ref(), "ext_pcx_fixed_range", data.elevation_apply_to_fixed_range, ), bool_prop(t!("Intensity Gradient").as_ref(), "ext_pcx_intensity_gradient", data.intensity_as_gradient, ), bool_prop(t!("Elevation Gradient").as_ref(), "ext_pcx_elevation_gradient", data.elevation_as_gradient, ), ro_prop(t!("Croppings").as_ref(), "ext_pcx_croppings", crops), ], }, ] } fn semantic_properties( properties: &[codec::objects::SemanticProperty], ) -> String { properties .iter() .map(|property| { format!( "{} [{}]: {:?}", property.subclass, property.code, property.value ) }) .collect::>() .join("\n") } fn reference_properties( references: &[codec::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: t!("Camera").into_owned(), props: vec![ro_prop(t!("View").as_ref(), "ext_camera_view", handle_text(*view_handle))], }], ExtendedEntityData::SectionObject(data) => section_properties(entity, 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: t!("Coordination Model").into_owned(), props: vec![ ro_prop(t!("Flags").as_ref(), "ext_coord_flags", data.flags.to_string()), ro_prop(t!("Definition").as_ref(), "ext_coord_definition", handle_text(data.definition_handle), ), edit_prop(t!("Unit Factor").as_ref(), "ext_coord_unit", data.unit_factor), ro_prop(t!("Transform").as_ref(), "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: t!("Proxy Entity").into_owned(), props: vec![ ro_prop(t!("Proxy ID").as_ref(), "ext_proxy_id", data.proxy_id.to_string()), ro_prop(t!("Class ID").as_ref(), "ext_proxy_class", data.class_id.to_string()), ro_prop(t!("DXF Subclass").as_ref(), "ext_proxy_subclass", data.dxf_subclass.clone()), ro_prop(t!("Version").as_ref(), "ext_proxy_version", data.version.to_string()), ro_prop(t!("DWG Version").as_ref(), "ext_proxy_dwg_version", format!("{}.{}", data.dwg_version, data.maintenance_version), ), ro_prop(t!("From DXF").as_ref(), "ext_proxy_from_dxf", data.from_dxf.to_string()), ro_prop(t!("Graphics").as_ref(), "ext_proxy_graphics", format!("{} bits", data.graphics.bit_count), ), ro_prop(t!("Payload").as_ref(), "ext_proxy_payload", format!("{} bits", data.payload.bit_count), ), ro_prop(t!("Text Payload").as_ref(), "ext_proxy_text_payload", format!("{} bits", data.text_payload.bit_count), ), ro_prop(t!("Object References").as_ref(), "ext_proxy_references", reference_properties(&data.object_ids), ), ], }], ExtendedEntityData::OleFrame(data) => vec![PropSection { title: t!("OLE Frame").into_owned(), props: vec![ ro_prop(t!("Flag").as_ref(), "ext_ole_flag", data.flag.to_string()), ro_prop(t!("Mode").as_ref(), "ext_ole_mode", data.mode.to_string()), ro_prop(t!("Storage Size").as_ref(), "ext_ole_size", format!("{} bytes", data.storage.encoded_len()), ), ], }], ExtendedEntityData::LayoutPrintConfig(data) => vec![PropSection { title: t!("Layout Print Configuration").into_owned(), props: vec![ ro_prop(t!("Class Version").as_ref(), "ext_print_version", data.class_version.to_string(), ), ro_prop(t!("Flag").as_ref(), "ext_print_flag", data.flag.to_string()), ], }], ExtendedEntityData::Format(data) => vec![PropSection { title: t!("Format").into_owned(), props: vec![ ro_prop(t!("DWG Payload").as_ref(), "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(t!("DWG Version").as_ref(), "ext_format_version", format!("{:?}", data.raw_dwg_version), ), ro_prop(t!("DXF Codes").as_ref(), "ext_format_dxf", data.raw_dxf_codes.as_ref().map_or(0, Vec::len).to_string(), ), ], }], ExtendedEntityData::Legacy(data) => vec![PropSection { title: t!("Legacy Entity").into_owned(), props: vec![ro_prop(t!("Data").as_ref(), "ext_legacy_data", format!("{data:#?}"))], }], ExtendedEntityData::DynamicBlock(data) => vec![PropSection { title: t!("Dynamic Block Entity").into_owned(), props: vec![ ro_prop(t!("Class").as_ref(), "ext_dynamic_class", data.entity_dxf_name().unwrap_or("Helper"), ), ro_prop(t!("Decoded Data").as_ref(), "ext_dynamic_data", format!("{data:#?}")), ], }], ExtendedEntityData::RegisteredClass(data) => vec![PropSection { title: t!("Registered Class Entity").into_owned(), props: vec![ ro_prop(t!("DXF Name").as_ref(), "ext_registered_dxf", data.dxf_name.clone()), ro_prop(t!("C++ Class").as_ref(), "ext_registered_cpp", data.cpp_class_name.clone(), ), ro_prop(t!("Properties").as_ref(), "ext_registered_properties", semantic_properties(&data.properties), ), ro_prop(t!("Payload").as_ref(), "ext_registered_payload", format!("{} bits", data.payload.bit_count), ), ro_prop(t!("Object References").as_ref(), "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_section_prop(entity: &mut ExtendedEntity, field: &str, value: &str) { let was_slice = section_is_slice(entity); let stored_slice_depth = section_slice_depth(entity).unwrap_or(0.0); let ExtendedEntityData::SectionObject(data) = &mut entity.data else { return; }; let mut slice_update = None; match field { "ext_section_name" => data.name = value.to_string(), "ext_section_state" | "ext_section_state2" => { let kind = value.trim().to_ascii_uppercase(); let span = data .vertices .first() .zip(data.vertices.last()) .map_or(1.0, |(first, last)| (*last - *first).length().max(1e-4)); set_section_kind(data, &kind); slice_update = Some((kind == "SLICE", (span / 60.0).max(1e-4))); } "ext_section_viewing" => { if let Some(direction) = parse_vector(value) { set_viewing_direction(data, direction); } } "ext_section_vertical" => { if let Some(direction) = parse_vector(value) { let depth = section_depth(data); let direction = normalized(direction, data.vertical_direction); if direction.dot(§ion_tangent(data)).abs() < 1.0 - 1e-9 { data.vertical_direction = direction; if depth > 0.0 { set_section_depth(data, depth); } } } } "ext_section_live" => data.flags ^= 1, "ext_section_alpha" => { if let Ok(alpha) = value.trim().parse::() { data.indicator_alpha = alpha.clamp(0, 100); } } "ext_section_elevation" | "ext_section_offset" => { if let Some(offset) = parse_f64(value) { move_section_to_offset(data, offset); } } "ext_section_top" => { if let Some(height) = parse_f64(value).filter(|height| *height >= 0.0) { data.top_height = height; } } "ext_section_bottom" => { if let Some(height) = parse_f64(value).filter(|height| *height >= 0.0) { data.bottom_height = height; } } "ext_section_vertices" => { if let Some(vertices) = parse_vertices(value) { let depth = section_depth(data); data.vertices = vertices; if depth > 0.0 { set_section_depth(data, depth); } } } "ext_section_depth" => { if let Some(depth) = parse_f64(value) { if was_slice { slice_update = Some((true, depth)); } else { set_section_depth(data, depth); } } } _ => {} } if let Some((enabled, depth)) = slice_update { set_section_slice_metadata(entity, enabled, depth); } else if was_slice && field != "ext_section_depth" { set_section_slice_metadata(entity, true, stored_slice_depth); } } fn apply_geom_prop(entity: &mut ExtendedEntity, field: &str, value: &str) { if matches!(entity.data, ExtendedEntityData::SectionObject(_)) { apply_section_prop(entity, field, value); return; } match &mut entity.data { 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) => { let mut grips: Vec = if section_kind(entity, data) == "Plane" && data.vertices.len() == 2 && data.back_line_vertices.is_empty() { section_plane_edge_grips(data) } else { 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() }; if let Some(right) = data.vertices.last() { let vertical = normalized(data.vertical_direction, Vector3::UNIT_Z); let edge_center_offset = (data.top_height - data.bottom_height) * 0.5; let anchor = *right + vertical * edge_center_offset; grips.push(GripDef { id: SECTION_GRIP_STATE, world: glam::DVec3::new(anchor.x, anchor.y, anchor.z), is_midpoint: false, shape: crate::scene::model::object::GripShape::DropdownAdjacent, dir: None, axis: None, }); } if let Some(center) = section_center(data) { let normal = section_viewing_direction(data); let span = data .vertices .first() .zip(data.vertices.last()) .map_or(1.0, |(first, last)| (*last - *first).length()); let offset = (span * 0.08).max(1.0e-6); let normal_world = glam::DVec3::new(normal.x, normal.y, normal.z); grips.push(GripDef { id: SECTION_GRIP_NORMAL, world: glam::DVec3::new(center.x, center.y, center.z) - normal_world * offset, is_midpoint: false, shape: crate::scene::model::object::GripShape::Triangle, dir: Some(-normal_world), axis: Some(normal_world), }); } grips } 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 matches!( grip_id, SECTION_GRIP_LEFT | SECTION_GRIP_RIGHT | SECTION_GRIP_TOP | SECTION_GRIP_BOTTOM ) { apply_section_plane_edge_grip(data, grip_id, apply); } else if grip_id == SECTION_GRIP_NORMAL { let Some(center) = section_center(data) else { return; }; let normal = section_viewing_direction(data); let span = data .vertices .first() .zip(data.vertices.last()) .map_or(1.0, |(first, last)| (*last - *first).length()); let offset = (span * 0.08).max(1.0e-6); let current = center - normal * offset; let delta = match apply { GripApply::Translate(delta) => Vector3::new(delta.x, delta.y, delta.z), GripApply::Absolute(position) => { Vector3::new(position.x, position.y, position.z) - current } }; let constrained = normal * delta.dot(&normal); for point in data .vertices .iter_mut() .chain(data.back_line_vertices.iter_mut()) { *point = *point + constrained; } } else 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 section_center(data: &SectionObjectData) -> Option { let (first, last) = data.vertices.first().zip(data.vertices.last())?; Some((*first + *last) * 0.5) } fn section_plane_edge_grips(data: &SectionObjectData) -> Vec { let Some((first, last)) = data.vertices.first().zip(data.vertices.last()) else { return Vec::new(); }; let center = (*first + *last) * 0.5; let tangent = section_tangent(data); let vertical = normalized(data.vertical_direction, Vector3::UNIT_Z); let edge_center_offset = (data.top_height - data.bottom_height) * 0.5; let definitions = [ ( SECTION_GRIP_LEFT, *first + vertical * edge_center_offset, vertical, None, ), ( SECTION_GRIP_RIGHT, *last + vertical * edge_center_offset, vertical, None, ), ( SECTION_GRIP_TOP, center + vertical * data.top_height, tangent, Some(vertical), ), ( SECTION_GRIP_BOTTOM, center - vertical * data.bottom_height, tangent, Some(vertical), ), ]; definitions .into_iter() .map(|(id, world, edge, axis)| GripDef { id, world: glam::DVec3::new(world.x, world.y, world.z), is_midpoint: false, shape: crate::scene::model::object::GripShape::Rectangle, dir: Some(glam::DVec3::new(edge.x, edge.y, edge.z)), axis: axis.map(|axis| glam::DVec3::new(axis.x, axis.y, axis.z)), }) .collect() } fn apply_section_plane_edge_grip( data: &mut SectionObjectData, grip_id: usize, apply: GripApply, ) { let Some((first, last)) = data.vertices.first().zip(data.vertices.last()) else { return; }; let center = (*first + *last) * 0.5; let tangent = section_tangent(data); let vertical = normalized(data.vertical_direction, Vector3::UNIT_Z); let edge_center_offset = (data.top_height - data.bottom_height) * 0.5; let current = match grip_id { SECTION_GRIP_LEFT => *first + vertical * edge_center_offset, SECTION_GRIP_RIGHT => *last + vertical * edge_center_offset, SECTION_GRIP_TOP => center + vertical * data.top_height, SECTION_GRIP_BOTTOM => center - vertical * data.bottom_height, _ => return, }; let delta = match apply { GripApply::Translate(delta) => Vector3::new(delta.x, delta.y, delta.z), GripApply::Absolute(position) => { Vector3::new(position.x, position.y, position.z) - current } }; let span = (*last - *first).length(); let minimum_span = 1.0e-6; let in_plane_vertical = normalized( vertical - tangent * vertical.dot(&tangent), vertical, ); let vertical_change = in_plane_vertical * delta.dot(&in_plane_vertical); match grip_id { SECTION_GRIP_LEFT => { let tangent_change = delta .dot(&tangent) .min((span - minimum_span).max(0.0)); data.vertices[0] = data.vertices[0] + tangent * tangent_change + vertical_change; } SECTION_GRIP_RIGHT => { let last = data.vertices.len() - 1; let tangent_change = delta .dot(&tangent) .max((-span + minimum_span).min(0.0)); data.vertices[last] = data.vertices[last] + tangent * tangent_change + vertical_change; } SECTION_GRIP_TOP => { data.top_height = (data.top_height + delta.dot(&vertical)).max(0.0); } SECTION_GRIP_BOTTOM => { data.bottom_height = (data.bottom_height - delta.dot(&vertical)).max(0.0); } _ => {} } } 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, axis, angle_rad } => { Transform::from_translation(Vector3::new(-center.x, -center.y, -center.z)) .then(&Transform::from_rotation( Vector3::new(axis.x, axis.y, axis.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, working_normal } => { crate::scene::view::transform::reflection_about_working_line( *p1, *p2, *working_normal, ) } EntityTransform::Affine(transform) => *transform, } } 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); let slice_depth = section_is_slice(entity) .then(|| section_slice_depth(entity).unwrap_or(0.0) * scale); 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() { codec::Entity::apply_transform(text, &transform); } } ExtendedEntityData::CoordinationModel(data) => { let mut matrix = codec::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); } } } _ => {} } if let Some(depth) = slice_depth { set_section_slice_metadata(entity, true, depth); } } impl RenderConvertible for ExtendedEntity { fn to_render(&self, document: &codec::CadDocument) -> Option { to_render(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); } fn grip_menu(&self, grip_id: usize) -> Vec { if grip_id != SECTION_GRIP_STATE { return Vec::new(); } let ExtendedEntityData::SectionObject(data) = &self.data else { return Vec::new(); }; let current = section_kind(self, data); vec![ GripMenuItem { label: if current == "Plane" { "✓ Plane" } else { "Plane" }, action: GripMenuAction::SectionPlane, }, GripMenuItem { label: if current == "Slice" { "✓ Slice" } else { "Slice" }, action: GripMenuAction::SectionSlice, }, GripMenuItem { label: if current == "Boundary" { "✓ Boundary" } else { "Boundary" }, action: GripMenuAction::SectionBoundary, }, GripMenuItem { label: if current == "Volume" { "✓ Volume" } else { "Volume" }, action: GripMenuAction::SectionVolume, }, ] } fn apply_grip_menu(&mut self, grip_id: usize, action: GripMenuAction) { if grip_id != SECTION_GRIP_STATE || !matches!(self.data, ExtendedEntityData::SectionObject(_)) { return; } let value = match action { GripMenuAction::SectionPlane => "Plane", GripMenuAction::SectionSlice => "Slice", GripMenuAction::SectionBoundary => "Boundary", GripMenuAction::SectionVolume => "Volume", _ => return, }; let ExtendedEntityData::SectionObject(data) = &self.data else { return; }; if section_kind(self, data) == value { return; } apply_section_prop(self, "ext_section_state", value); } } 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); } } #[cfg(test)] mod tests { use super::*; use codec::entities::EntityCommon; use codec::types::Color; #[test] fn reselecting_slice_keeps_its_depth() { let mut entity = ExtendedEntity { common: EntityCommon::new(), data: ExtendedEntityData::SectionObject(SectionObjectData { state: 1, flags: 5, name: String::new(), vertical_direction: Vector3::UNIT_Z, top_height: 1.0, bottom_height: 1.0, indicator_alpha: 70, indicator_color: Color::from_index(9), back_line_vertices: Vec::new(), vertices: vec![Vector3::ZERO, Vector3::new(60.0, 0.0, 0.0)], settings_handle: Handle::NULL, }), }; set_section_slice_metadata(&mut entity, true, 0.25); entity.apply_grip_menu(SECTION_GRIP_STATE, GripMenuAction::SectionSlice); assert_eq!(section_slice_depth(&entity), Some(0.25)); } }