diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index a5cadff8e..b467e8b1d 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -353,7 +353,32 @@ impl OpenCADStudio { self.reset_tracking_after_point(); self.push_ucs_to_cmd(i); } - if let StepInput::EntityPick(handle, _) = &input { + if let StepInput::EntityPick(handle, point) = &input { + let solid_pick = matches!( + self.tabs[i].scene.document.get_entity(*handle), + Some( + acadrust::EntityType::Solid3D(_) + | acadrust::EntityType::Surface(_) + | acadrust::EntityType::Region(_) + | acadrust::EntityType::Body(_) + ) + ); + let direction = if solid_pick { + self.tabs[i] + .scene + .solid_planar_face_normal_at(*handle, *point) + } else { + self.tabs[i] + .scene + .document + .get_entity(*handle) + .and_then(crate::entities::curve::entity_curve) + .and_then(|curve| curve.plane.normal()) + .map(glam::DVec3::from_array) + }; + if let Some(command) = self.tabs[i].active_cmd.as_mut() { + command.set_entity_pick_direction(direction); + } if self.tabs[i].active_cmd.as_ref() .is_some_and(|command| command.inject_before_entity_pick()) { diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index 69dc797b2..a17d1a9cb 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -1212,6 +1212,64 @@ impl OpenCADStudio { } } + "SECTIONPLANE" => { + use crate::modules::model::sectionplane_cmd::SectionPlaneCommand; + let (bounds, next_name) = { + let scene = &mut self.tabs[i].scene; + let solid_handles = scene + .document + .entities() + .filter_map(|entity| { + matches!( + entity, + acadrust::EntityType::Solid3D(_) + | acadrust::EntityType::Surface(_) + | acadrust::EntityType::Region(_) + | acadrust::EntityType::Body(_) + ) + .then_some(entity.common().handle) + }) + .collect::>(); + scene.restore_solid_models(&solid_handles); + let bounds = solid_handles + .iter() + .filter_map(|handle| scene.solid_models.get(handle)) + .filter_map(crate::scene::model::solid_model::extent) + .fold(None::<(glam::DVec3, glam::DVec3)>, |bounds, (low, high)| { + let low = glam::DVec3::from_array(low); + let high = glam::DVec3::from_array(high); + Some(match bounds { + None => (low, high), + Some((min, max)) => (min.min(low), max.max(high)), + }) + }); + let next_name = scene + .document + .entities() + .filter_map(|entity| { + let acadrust::EntityType::Extended(extended) = entity else { + return None; + }; + let acadrust::entities::ExtendedEntityData::SectionObject(data) = + &extended.data + else { + return None; + }; + data.name + .strip_prefix("Section Plane (") + .and_then(|name| name.strip_suffix(')')) + .and_then(|name| name.parse::().ok()) + }) + .max() + .unwrap_or(0) + + 1; + (bounds, next_name) + }; + let command = SectionPlaneCommand::new(bounds, next_name); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + } + // 3DALIGN <18 numbers> — align the selected solid by 3 source→3 dest points. cmd if cmd == "3DALIGN" || cmd == "ALIGN3D" diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index b69daec39..8ea644fc0 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -462,6 +462,7 @@ inventory::submit!(crate::command::CommandRegistration { "3DALIGN", "ALIGN3D", "SECTION", + "SECTIONPLANE", "PYRAMID", "PYR", "SPLINEFIT", diff --git a/src/app/control/actions.rs b/src/app/control/actions.rs index f6e9df8a9..13ae83069 100644 --- a/src/app/control/actions.rs +++ b/src/app/control/actions.rs @@ -171,6 +171,7 @@ impl OpenCADStudio { | "text_color" | "block_content_color" | "background_fill_color" + | "indicator_fill_color" ) { self.update(Message::PropColorFieldChanged { field: p.field.into(), diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index a76158d93..a64709371 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -5226,6 +5226,21 @@ impl OpenCADStudio { Message::PropColorFieldChanged { field, color } => { let i = self.active_tab; let handles = self.property_target_handles(i); + if field == "indicator_fill_color" { + self.apply_property_op(i, "CHPROP", &handles, |app, handle| { + if let Some(acadrust::EntityType::Extended(extended)) = + app.tabs[i].scene.document.get_entity_mut(handle) + { + if let acadrust::entities::ExtendedEntityData::SectionObject(data) = + &mut extended.data + { + data.indicator_color = color; + } + } + }); + self.tabs[i].properties.open_color_field = None; + return Task::none(); + } // Dim-line colour override (Leader / Dimension): write it as an // ACAD_DSTYLE code-176 override (an ACI index) so it round-trips // through DWG and DXF. RGB picks collapse to the nearest ACI, in diff --git a/src/entities/extended.rs b/src/entities/extended.rs index d5740ec2a..8042cb8a4 100644 --- a/src/entities/extended.rs +++ b/src/entities/extended.rs @@ -46,6 +46,22 @@ fn bool_prop(label: &str, field: &'static str, value: bool) -> Property { } } +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); @@ -211,14 +227,73 @@ fn append_arc_aligned_text(points: &mut Vec<[f64; 3]>, data: &ArcAlignedTextData 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 } @@ -487,52 +562,220 @@ fn section_properties(data: &SectionObjectData) -> Vec { let vertices = data .vertices .iter() - .enumerate() - .map(|(index, point)| format!("{}: {}", index + 1, vector_text(*point))) + .map(|point| vector_text(*point)) .collect::>() - .join("\n"); - let back_vertices = data - .back_line_vertices + .join("; "); + let kind = section_kind(data); + let viewing = section_viewing_direction(data); + let offset = section_plane_offset(data); + let depth = 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(data: &SectionObjectData) -> &'static str { + match data.state { + 1 => "Plane", + 4 => "Volume", + 2 => { + let span = data + .vertices + .first() + .zip(data.vertices.last()) + .map_or(1.0, |(first, last)| (*last - *first).length().max(1e-9)); + if section_depth(data) < span * 0.25 { "Slice" } else { "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() - .enumerate() - .map(|(index, point)| format!("{}: {}", index + 1, vector_text(*point))) - .collect::>() - .join("\n"); - vec![ - PropSection { - title: t!("Section Object").into_owned(), - props: vec![ - text_prop(t!("Name").as_ref(), "ext_section_name", &data.name), - ro_prop(t!("State").as_ref(), "ext_section_state", data.state.to_string()), - ro_prop(t!("Flags").as_ref(), "ext_section_flags", data.flags.to_string()), - ro_prop(t!("Vertical Direction").as_ref(), - "ext_section_vertical", - vector_text(data.vertical_direction), - ), - 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!("Indicator Alpha").as_ref(), - "ext_section_alpha", - data.indicator_alpha.to_string(), - ), - ro_prop(t!("Indicator Color").as_ref(), - "ext_section_color", - format!("{:?}", data.indicator_color), - ), - ro_prop(t!("Settings").as_ref(), - "ext_section_settings", - handle_text(data.settings_handle), - ), - ], - }, - PropSection { - title: t!("Section Vertices").into_owned(), - props: vec![ - ro_prop(t!("Cutting Line").as_ref(), "ext_section_vertices", vertices), - ro_prop(t!("Back Line").as_ref(), "ext_section_back_vertices", back_vertices), - ], - }, - ] + .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 = 2; + set_section_depth(data, span / 60.0); + } + "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 { @@ -1043,8 +1286,59 @@ 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), + "ext_section_state" | "ext_section_state2" => set_section_kind(data, value), + "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) { + set_section_depth(data, depth); + } + } _ => {} }, ExtendedEntityData::ArcAlignedText(data) => match field { diff --git a/src/entities/names.rs b/src/entities/names.rs index 9a00c9ffe..c26b252f2 100644 --- a/src/entities/names.rs +++ b/src/entities/names.rs @@ -66,6 +66,11 @@ pub fn ui_name(e: &EntityType) -> &'static str { EntityType::Light(_) => "Light", EntityType::SectionSymbol(_) => "Section Symbol", EntityType::ViewBorder(_) => "View Border", + EntityType::Extended(extended) + if matches!( + extended.data, + acadrust::entities::ExtendedEntityData::SectionObject(_) + ) => "Section Plane", EntityType::Extended(_) => "Extended Entity", EntityType::Seqend(_) => "Seqend", EntityType::Unknown(_) => "Unknown", @@ -79,6 +84,12 @@ pub fn ui_name(e: &EntityType) -> &'static str { /// (the numeric `DWG_TYPE_` placeholder). pub fn ui_name_or_class(e: &EntityType) -> String { if let EntityType::Extended(entity) = e { + if matches!( + entity.data, + acadrust::entities::ExtendedEntityData::SectionObject(_) + ) { + return "Section Plane".to_string(); + } return entity.class_name().to_string(); } if let EntityType::Unknown(u) = e { @@ -148,6 +159,11 @@ pub fn dxf_name(e: &EntityType) -> &'static str { EntityType::Light(_) => "LIGHT", EntityType::SectionSymbol(_) => "SECTIONLINE", EntityType::ViewBorder(_) => "DRAWINGVIEW", + EntityType::Extended(extended) + if matches!( + extended.data, + acadrust::entities::ExtendedEntityData::SectionObject(_) + ) => "SECTIONOBJECT", _ => "ENTITY", } } diff --git a/src/modules/model/mod.rs b/src/modules/model/mod.rs index 546342d00..8c18f7486 100644 --- a/src/modules/model/mod.rs +++ b/src/modules/model/mod.rs @@ -5,6 +5,7 @@ pub mod cylinder_cmd; pub mod edge_cmd; pub mod polysolid_cmd; pub mod primitive_cmd; +pub mod sectionplane_cmd; pub mod shell_cmd; pub mod slice_cmd; diff --git a/src/modules/model/sectionplane_cmd.rs b/src/modules/model/sectionplane_cmd.rs new file mode 100644 index 000000000..61b68e85f --- /dev/null +++ b/src/modules/model/sectionplane_cmd.rs @@ -0,0 +1,484 @@ +use acadrust::entities::{EntityCommon, ExtendedEntity, ExtendedEntityData, SectionObjectData}; +use acadrust::types::{Color, Handle, Vector3}; +use acadrust::EntityType; +use glam::DVec3; + +use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane}; +use crate::scene::model::wire_model::WireModel; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum SectionKind { + Plane, + Slice, + Boundary, + Volume, +} + +impl SectionKind { + fn label(self) -> &'static str { + match self { + Self::Plane => "Plane", + Self::Slice => "Slice", + Self::Boundary => "Boundary", + Self::Volume => "Volume", + } + } + + fn state(self) -> i32 { + match self { + Self::Plane => 1, + Self::Slice | Self::Boundary => 2, + Self::Volume => 4, + } + } +} + +#[derive(Clone, Copy)] +enum Orthographic { + Front, + Back, + Top, + Bottom, + Left, + Right, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Step { + Locate, + Through, + DrawStart, + DrawNext, + DrawDirection, + Orthographic, + Type, +} + +/// Creates persistent SECTIONOBJECT entities without modifying source solids. +pub struct SectionPlaneCommand { + step: Step, + kind: SectionKind, + first: Option, + draw_points: Vec, + picked_direction: Option, + working: WorkingPlane, + bounds: (DVec3, DVec3), + empty_extents: bool, + next_name: usize, + notice: Option<&'static str>, +} + +impl SectionPlaneCommand { + pub fn new(bounds: Option<(DVec3, DVec3)>, next_name: usize) -> Self { + let empty_extents = bounds.is_none(); + let bounds = bounds.unwrap_or((DVec3::ZERO, DVec3::splat(4000.0))); + Self { + step: Step::Locate, + kind: SectionKind::Plane, + first: None, + draw_points: Vec::new(), + picked_direction: None, + working: WorkingPlane::default(), + bounds, + empty_extents, + next_name: next_name.max(1), + notice: None, + } + } + + fn corners(&self) -> [DVec3; 8] { + let (lo, hi) = self.bounds; + [ + DVec3::new(lo.x, lo.y, lo.z), + DVec3::new(hi.x, lo.y, lo.z), + DVec3::new(lo.x, hi.y, lo.z), + DVec3::new(hi.x, hi.y, lo.z), + DVec3::new(lo.x, lo.y, hi.z), + DVec3::new(hi.x, lo.y, hi.z), + DVec3::new(lo.x, hi.y, hi.z), + DVec3::new(hi.x, hi.y, hi.z), + ] + } + + fn frame_extents(&self, centre: DVec3, tangent: DVec3, vertical: DVec3) -> (f64, f64, f64, f64) { + let mut t_min = f64::INFINITY; + let mut t_max = f64::NEG_INFINITY; + let mut v_min = f64::INFINITY; + let mut v_max = f64::NEG_INFINITY; + for corner in self.corners() { + let delta = corner - centre; + let t = delta.dot(tangent); + let v = delta.dot(vertical); + t_min = t_min.min(t); + t_max = t_max.max(t); + v_min = v_min.min(v); + v_max = v_max.max(v); + } + let t_span = t_max - t_min; + let v_span = v_max - v_min; + let t_margin = if t_span > 1e-12 { t_span * 0.1 } else { 1.0 }; + let v_margin = if v_span > 1e-12 { + v_span * if self.empty_extents { 0.1 } else { 0.15 } + } else { + 1.0 + }; + (t_min - t_margin, t_max + t_margin, v_min - v_margin, v_max + v_margin) + } + + fn entity(&self, vertices: Vec, vertical: DVec3, positive_view: bool) -> EntityType { + let first = vertices.first().copied().unwrap_or(DVec3::ZERO); + let last = vertices.last().copied().unwrap_or(DVec3::X); + let tangent = (last - first).normalize_or(DVec3::X); + let vertical = (vertical - tangent * vertical.dot(tangent)).normalize_or(DVec3::Z); + let viewing = vertical.cross(tangent).normalize_or(-DVec3::Z) + * if positive_view { 1.0 } else { -1.0 }; + let centre = (first + last) * 0.5; + let (_, _, v_min, v_max) = self.frame_extents(centre, tangent, vertical); + let span = (last - first).length().max(1.0); + let depth = match self.kind { + SectionKind::Plane => 0.0, + SectionKind::Slice => (span / 60.0).max(1e-4), + SectionKind::Boundary | SectionKind::Volume => span, + }; + let back_line_vertices = if depth > 0.0 { + vertices.iter().map(|point| *point + viewing * depth).collect() + } else { + Vec::new() + }; + EntityType::Extended(ExtendedEntity { + common: EntityCommon::new(), + data: ExtendedEntityData::SectionObject(SectionObjectData { + state: self.kind.state(), + flags: 1 | if positive_view { 4 } else { 0 }, + name: format!("Section Plane ({})", self.next_name), + vertical_direction: Vector3::new(vertical.x, vertical.y, vertical.z), + top_height: v_max.max(0.0), + bottom_height: (-v_min).max(0.0), + indicator_alpha: 70, + indicator_color: Color::from_index(9), + vertices: vertices + .into_iter() + .map(|point| Vector3::new(point.x, point.y, point.z)) + .collect(), + back_line_vertices: back_line_vertices + .into_iter() + .map(|point| Vector3::new(point.x, point.y, point.z)) + .collect(), + settings_handle: Handle::NULL, + }), + }) + } + + fn line_at(&self, point: DVec3, viewing: DVec3, vertical_hint: DVec3) -> (Vec, DVec3) { + let viewing = viewing.normalize_or(-DVec3::Z); + let vertical_candidate = vertical_hint - viewing * vertical_hint.dot(viewing); + let vertical = if vertical_candidate.length_squared() > 1e-18 { + vertical_candidate.normalize() + } else { + viewing.any_orthonormal_vector() + }; + let tangent = viewing.cross(vertical).normalize_or(DVec3::X); + let (t_min, t_max, _, _) = self.frame_extents(point, tangent, vertical); + (vec![point + tangent * t_min, point + tangent * t_max], vertical) + } + + fn orthographic(&self, kind: Orthographic) -> EntityType { + let (lo, hi) = self.bounds; + let centre = (lo + hi) * 0.5; + let (viewing, vertical, positive) = match kind { + Orthographic::Front => (DVec3::Y, DVec3::Z, true), + Orthographic::Back => (DVec3::Y, DVec3::Z, false), + Orthographic::Top => (-DVec3::Z, DVec3::Y, true), + Orthographic::Bottom => (-DVec3::Z, DVec3::Y, false), + Orthographic::Left => (-DVec3::X, DVec3::Z, false), + Orthographic::Right => (-DVec3::X, DVec3::Z, true), + }; + let (vertices, vertical) = self.line_at(centre, viewing, vertical); + self.entity(vertices, vertical, positive) + } + + fn finish_through(&mut self, through: DVec3) -> CmdResult { + let Some(first) = self.first else { + return CmdResult::NeedPoint; + }; + let tangent = through - first; + if tangent.length_squared() <= 1e-18 { + self.notice = Some("SECTIONPLANE The through point must differ from the first point:"); + return CmdResult::NeedPoint; + } + let tangent = tangent.normalize(); + let vertical_hint = if tangent.dot(self.working.z).abs() < 0.98 { + self.working.z + } else { + self.working.y + }; + let vertical = (vertical_hint - tangent * vertical_hint.dot(tangent)).normalize(); + CmdResult::CommitAndExit(self.entity(vec![first, through], vertical, true)) + } + + fn finish_draw(&mut self, direction: DVec3) -> CmdResult { + if self.draw_points.len() < 2 { + self.notice = Some("SECTIONPLANE Specify at least two different section points:"); + self.step = Step::DrawNext; + return CmdResult::NeedPoint; + } + let first = self.draw_points[0]; + let last = *self.draw_points.last().unwrap_or(&first); + let tangent = (last - first).normalize_or(DVec3::X); + let vertical_hint = if tangent.dot(self.working.z).abs() < 0.98 { + self.working.z + } else { + self.working.y + }; + let vertical = (vertical_hint - tangent * vertical_hint.dot(tangent)).normalize(); + let base_view = vertical.cross(tangent).normalize_or(-DVec3::Z); + let centre = (first + last) * 0.5; + let positive = (direction - centre).dot(base_view) >= 0.0; + let vertices = std::mem::take(&mut self.draw_points); + CmdResult::CommitAndExit(self.entity(vertices, vertical, positive)) + } + + fn choose_kind(&mut self, kind: SectionKind) -> CmdResult { + self.kind = kind; + self.step = Step::Locate; + self.notice = None; + CmdResult::NeedPoint + } +} + +impl CadCommand for SectionPlaneCommand { + fn name(&self) -> &'static str { + "SECTIONPLANE" + } + + fn prompt(&self) -> String { + if let Some(notice) = self.notice { + return crate::t!(notice).into_owned(); + } + match self.step { + Step::Locate => format!( + "SECTIONPLANE Type = {}\n{}", + self.kind.label(), + crate::t!("Select face or any point to locate section line or [Draw section/Orthographic/Type]:") + ), + Step::Through => crate::t!("SECTIONPLANE Specify through point:").into_owned(), + Step::DrawStart => crate::t!("SECTIONPLANE Specify start point:").into_owned(), + Step::DrawNext if self.draw_points.len() == 1 => { + crate::t!("SECTIONPLANE Specify next point:").into_owned() + } + Step::DrawNext => crate::t!("SECTIONPLANE Specify next point or ENTER to complete:").into_owned(), + Step::DrawDirection => crate::t!("SECTIONPLANE Specify point in direction of section view:").into_owned(), + Step::Orthographic => crate::t!("SECTIONPLANE Align section to [Front/bAck/Top/Bottom/Left/Right] :").into_owned(), + Step::Type => crate::t!("SECTIONPLANE Enter section plane type [Plane/Slice/Boundary/Volume] :").into_owned(), + } + } + + fn options(&self) -> Vec { + match self.step { + Step::Locate => vec![ + CmdOption::new(crate::t!("Draw section").as_ref(), "D"), + CmdOption::new(crate::t!("Orthographic").as_ref(), "O"), + CmdOption::new(crate::t!("Type").as_ref(), "T"), + ], + Step::Orthographic => vec![ + CmdOption::new(crate::t!("Front").as_ref(), "F"), + CmdOption::new(crate::t!("Back").as_ref(), "A"), + CmdOption::new(crate::t!("Top").as_ref(), "T"), + CmdOption::new(crate::t!("Bottom").as_ref(), "B"), + CmdOption::new(crate::t!("Left").as_ref(), "L"), + CmdOption::new(crate::t!("Right").as_ref(), "R"), + ], + Step::Type => vec![ + CmdOption::new(crate::t!("Plane").as_ref(), "P"), + CmdOption::new(crate::t!("Slice").as_ref(), "S"), + CmdOption::new(crate::t!("Boundary").as_ref(), "B"), + CmdOption::new(crate::t!("Volume").as_ref(), "V"), + ], + _ => Vec::new(), + } + } + + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.working = plane; + } + + fn on_point(&mut self, point: DVec3) -> CmdResult { + self.notice = None; + match self.step { + Step::Locate => { + self.first = Some(point); + self.step = Step::Through; + CmdResult::NeedPoint + } + Step::Through => self.finish_through(point), + Step::DrawStart => { + self.draw_points.clear(); + self.draw_points.push(point); + self.step = Step::DrawNext; + CmdResult::NeedPoint + } + Step::DrawNext => { + if self.draw_points.last().is_some_and(|last| last.distance(point) <= 1e-9) { + self.notice = Some("SECTIONPLANE Consecutive section points must differ:"); + } else { + self.draw_points.push(point); + } + CmdResult::NeedPoint + } + Step::DrawDirection => self.finish_draw(point), + _ => CmdResult::NeedPoint, + } + } + + fn on_enter(&mut self) -> CmdResult { + self.notice = None; + match self.step { + Step::DrawNext if self.draw_points.len() >= 2 => { + self.step = Step::DrawDirection; + CmdResult::NeedPoint + } + Step::Orthographic => CmdResult::CommitAndExit(self.orthographic(Orthographic::Top)), + Step::Type => self.choose_kind(SectionKind::Plane), + _ => CmdResult::NeedPoint, + } + } + + fn on_text_input(&mut self, text: &str) -> Option { + let keyword = text.trim().to_ascii_uppercase(); + Some(match self.step { + Step::Locate => match keyword.as_str() { + "D" | "DRAW" | "DRAWSECTION" => { + self.step = Step::DrawStart; + CmdResult::NeedPoint + } + "O" | "ORTHO" | "ORTHOGRAPHIC" => { + self.step = Step::Orthographic; + CmdResult::NeedPoint + } + "T" | "TYPE" => { + self.step = Step::Type; + CmdResult::NeedPoint + } + _ => { + self.notice = Some("SECTIONPLANE Unknown option. Choose Draw section, Orthographic, or Type:"); + CmdResult::NeedPoint + } + }, + Step::Orthographic => { + let kind = match keyword.as_str() { + "F" | "FRONT" => Some(Orthographic::Front), + "A" | "BACK" => Some(Orthographic::Back), + "T" | "TOP" => Some(Orthographic::Top), + "B" | "BOTTOM" => Some(Orthographic::Bottom), + "L" | "LEFT" => Some(Orthographic::Left), + "R" | "RIGHT" => Some(Orthographic::Right), + _ => None, + }; + match kind { + Some(kind) => CmdResult::CommitAndExit(self.orthographic(kind)), + None => { + self.notice = Some("SECTIONPLANE Unknown alignment. Choose Front, Back, Top, Bottom, Left, or Right:"); + CmdResult::NeedPoint + } + } + } + Step::Type => match keyword.as_str() { + "P" | "PLANE" => self.choose_kind(SectionKind::Plane), + "S" | "SLICE" => self.choose_kind(SectionKind::Slice), + "B" | "BOUNDARY" => self.choose_kind(SectionKind::Boundary), + "V" | "VOLUME" => self.choose_kind(SectionKind::Volume), + _ => { + self.notice = Some("SECTIONPLANE Unknown type. Choose Plane, Slice, Boundary, or Volume:"); + CmdResult::NeedPoint + } + }, + _ => CmdResult::NeedPoint, + }) + } + + fn point_step_accepts_keywords(&self) -> bool { + matches!(self.step, Step::Locate | Step::Orthographic | Step::Type) + } + + fn needs_entity_pick(&self) -> bool { + self.step == Step::Locate + } + + fn entity_pick_accepts_points(&self) -> bool { + true + } + + fn entity_pick_uses_surface_point(&self) -> bool { + true + } + + fn entity_pick_highlights_hover(&self) -> bool { + true + } + + fn set_entity_pick_direction(&mut self, direction: Option) { + self.picked_direction = direction; + } + + fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult { + if handle.is_null() { + self.first = Some(point); + self.step = Step::Through; + return CmdResult::NeedPoint; + } + let Some(face_normal) = self.picked_direction.take().filter(|normal| normal.length_squared() > 1e-18) else { + self.notice = Some("SECTIONPLANE Select a planar face or specify a point:"); + return CmdResult::NeedPoint; + }; + let viewing = -face_normal.normalize(); + let vertical = if viewing.dot(self.working.z).abs() < 0.98 { + self.working.z + } else { + self.working.y + }; + let (vertices, vertical) = self.line_at(point, viewing, vertical); + CmdResult::CommitAndExit(self.entity(vertices, vertical, true)) + } + + fn on_undo_step(&mut self) -> Option { + if self.step != Step::DrawNext { + return None; + } + self.draw_points.pop(); + self.step = if self.draw_points.is_empty() { Step::DrawStart } else { Step::DrawNext }; + Some(CmdResult::NeedPoint) + } + + fn on_preview_wires(&mut self, point: DVec3) -> Vec { + let mut points = self.draw_points.iter().map(|p| p.to_array()).collect::>(); + match self.step { + Step::Through => { + if let Some(first) = self.first { + points = vec![first.to_array(), point.to_array()]; + } + } + Step::DrawNext => points.push(point.to_array()), + Step::DrawDirection if self.draw_points.len() >= 2 => { + let centre = (self.draw_points[0] + *self.draw_points.last().unwrap()) * 0.5; + points.push([f64::NAN; 3]); + points.push(centre.to_array()); + points.push(point.to_array()); + } + _ => {} + } + if points.len() < 2 { + Vec::new() + } else { + vec![WireModel::solid_f64( + "sectionplane_preview".into(), + points, + WireModel::CYAN, + false, + )] + } + } +} + +inventory::submit!(crate::command::CommandRegistration { + names: &["SECTIONPLANE"] +}); diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 6aba6b27b..67426ed6b 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -2021,6 +2021,43 @@ pub struct Scene { undo_recording: Option, } +fn section_frame( + data: &acadrust::entities::SectionObjectData, +) -> Option<(glam::DVec3, glam::DVec3, glam::DVec3, glam::DVec3)> { + let first = data.vertices.first()?; + let last = data.vertices.last()?; + let origin = glam::DVec3::new(first.x, first.y, first.z); + let tangent = (glam::DVec3::new(last.x, last.y, last.z) - origin).try_normalize()?; + let raw_vertical = glam::DVec3::new( + data.vertical_direction.x, + data.vertical_direction.y, + data.vertical_direction.z, + ); + let vertical = (raw_vertical - tangent * raw_vertical.dot(tangent)).try_normalize()?; + let base_view = vertical.cross(tangent).try_normalize()?; + let viewing = if data.flags & 4 != 0 { base_view } else { -base_view }; + Some((origin, tangent, vertical, viewing)) +} + +fn keep_section_positive( + body: &cadkernel::brep::Body, + plane: cadkernel::space::Plane, +) -> Result, ()> { + match cadkernel::brep::slice_by_plane(body, plane) { + Ok(Some(result)) => Ok(Some(result.positive)), + Ok(None) => { + let bounds = cadkernel::brep::body_bounds(body).ok_or(())?; + let centre = [ + (bounds.min[0] + bounds.max[0]) * 0.5, + (bounds.min[1] + bounds.max[1]) * 0.5, + (bounds.min[2] + bounds.max[2]) * 0.5, + ]; + Ok((plane.distance_to(centre).ok_or(())? >= 0.0).then(|| body.clone())) + } + Err(_) => Err(()), + } +} + impl Scene { pub fn new() -> Self { Self { @@ -2431,7 +2468,17 @@ impl Scene { bits |= CACHE_CATEGORY_IMAGE; } let is_insert = matches!(entity, EntityType::Insert(_)); - if self.meshes.contains_key(&handle) || self.block_meshes.contains_key(&handle) || is_insert + let is_section = matches!( + entity, + EntityType::Extended(acadrust::entities::ExtendedEntity { + data: acadrust::entities::ExtendedEntityData::SectionObject(_), + .. + }) + ); + if self.meshes.contains_key(&handle) + || self.block_meshes.contains_key(&handle) + || is_insert + || is_section { bits |= CACHE_CATEGORY_MESH | CACHE_CATEGORY_INTERACTION; } @@ -6594,6 +6641,12 @@ impl Scene { || matches!( self.document.get_entity(h), Some(EntityType::Insert(_)) + | Some(EntityType::Extended( + acadrust::entities::ExtendedEntity { + data: acadrust::entities::ExtendedEntityData::SectionObject(_), + .. + } + )) ) }, ) => @@ -6780,6 +6833,7 @@ impl Scene { all_visible: bool, viewport: Option, ) -> Vec { + let live_section = self.active_live_section(target_block); // Top-level solids: drop those whose layer is off/frozen or that are // flagged invisible / isolated-hidden, mirroring the 2D wire path, plus // any whose layer is frozen in the requesting viewport. @@ -6801,8 +6855,19 @@ impl Scene { }) .unwrap_or(false) }) - .map(|(&handle, set)| { - let mut set = set.clone(); + .filter_map(|(&handle, set)| { + let mut set = if let Some(section) = live_section.as_ref() { + match self.sectioned_body(handle, section) { + Ok(Some(body)) => self + .prepare_solid_model_display(handle, &body) + .map(|display| display.0) + .unwrap_or_else(|| set.clone()), + Ok(None) => return None, + Err(()) => set.clone(), + } + } else { + set.clone() + }; if let Some(entity) = self.document.get_entity(handle) { let style = crate::scene::view::render::render_style_for_viewport( &self.document, @@ -6826,7 +6891,7 @@ impl Scene { material.diffuse[3] = style.0[3]; } } - set + Some(set) }) .collect(); // Block-definition solids are instanced per INSERT of the ACTIVE space's @@ -6843,6 +6908,116 @@ impl Scene { all } + fn active_live_section( + &self, + target_block: Handle, + ) -> Option { + self.document + .entities() + .filter_map(|entity| { + let EntityType::Extended(extended) = entity else { + return None; + }; + let acadrust::entities::ExtendedEntityData::SectionObject(data) = &extended.data else { + return None; + }; + let owner = extended.common.owner_handle; + (data.flags & 1 != 0 + && !extended.common.invisible + && (owner.is_null() || owner == target_block)) + .then(|| data.clone()) + }) + .last() + } + + fn sectioned_body( + &self, + handle: Handle, + data: &acadrust::entities::SectionObjectData, + ) -> Result, ()> { + let body = self.solid_models.get(&handle).ok_or(())?; + let Some((origin, tangent, vertical, viewing)) = section_frame(data) else { + return Err(()); + }; + let front = cadkernel::space::Plane::orthonormal( + origin.to_array(), + tangent.to_array(), + viewing.to_array(), + ) + .ok_or(())?; + let mut clipped = keep_section_positive(body, front)?; + let Some(mut current) = clipped.take() else { + return Ok(None); + }; + if data.state == 1 { + return Ok(Some(current)); + } + + let depth = data + .vertices + .first() + .zip(data.back_line_vertices.first()) + .map_or(0.0, |(front, back)| { + (glam::DVec3::new(back.x, back.y, back.z) + - glam::DVec3::new(front.x, front.y, front.z)) + .dot(viewing) + .abs() + }); + if depth <= 1e-12 { + return Ok(Some(current)); + } + let back = cadkernel::space::Plane::orthonormal( + (origin + viewing * depth).to_array(), + tangent.to_array(), + (-viewing).to_array(), + ) + .ok_or(())?; + let Some(next) = keep_section_positive(¤t, back)? else { + return Ok(None); + }; + current = next; + + let span = data + .vertices + .first() + .zip(data.vertices.last()) + .map_or(0.0, |(first, last)| { + (glam::DVec3::new(last.x, last.y, last.z) + - glam::DVec3::new(first.x, first.y, first.z)) + .length() + }); + let bounded = data.state == 4 || (data.state == 2 && depth >= span * 0.25); + if !bounded || span <= 1e-12 { + return Ok(Some(current)); + } + + let first = data.vertices.first().map(|point| glam::DVec3::new(point.x, point.y, point.z)).ok_or(())?; + let last = data.vertices.last().map(|point| glam::DVec3::new(point.x, point.y, point.z)).ok_or(())?; + for plane in [ + cadkernel::space::Plane::orthonormal(first.to_array(), vertical.to_array(), tangent.to_array()), + cadkernel::space::Plane::orthonormal(last.to_array(), vertical.to_array(), (-tangent).to_array()), + cadkernel::space::Plane::orthonormal( + (origin - vertical * data.bottom_height).to_array(), + tangent.to_array(), + vertical.to_array(), + ), + cadkernel::space::Plane::orthonormal( + (origin + vertical * data.top_height).to_array(), + tangent.to_array(), + (-vertical).to_array(), + ), + ] { + let Some(plane) = plane else { + return Err(()); + }; + let Some(next) = keep_section_positive(¤t, plane)? else { + return Ok(None); + }; + current = next; + } + Ok(Some(current)) + } + /// True when `layer` is turned off or frozen — entities on it never render. fn layer_hidden(&self, layer: &str) -> bool { self.document diff --git a/src/ui/properties.rs b/src/ui/properties.rs index 7bbf8329b..9033adb99 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -921,6 +921,7 @@ impl PropertiesPanel { | "text_color" | "block_content_color" | "background_fill_color" + | "indicator_fill_color" ) { let open = self.open_color_field.as_deref() == Some(field); let fsel = field.to_string();