forked from 1b-agent/opencadstudio-upstream
feat(model): add section plane workflow
This commit is contained in:
1 parent
56e09cd3f5
commit
c9c768cd9f
11 files changed
+1122
-51
No files matched your search
@@ -353,7 +353,32 @@ impl OpenCADStudio {
|
|||||||
self.reset_tracking_after_point();
|
self.reset_tracking_after_point();
|
||||||
self.push_ucs_to_cmd(i);
|
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()
|
if self.tabs[i].active_cmd.as_ref()
|
||||||
.is_some_and(|command| command.inject_before_entity_pick())
|
.is_some_and(|command| command.inject_before_entity_pick())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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::<Vec<_>>();
|
||||||
|
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::<usize>().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.
|
// 3DALIGN <18 numbers> — align the selected solid by 3 source→3 dest points.
|
||||||
cmd if cmd == "3DALIGN"
|
cmd if cmd == "3DALIGN"
|
||||||
|| cmd == "ALIGN3D"
|
|| cmd == "ALIGN3D"
|
||||||
|
|||||||
@@ -462,6 +462,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
|||||||
"3DALIGN",
|
"3DALIGN",
|
||||||
"ALIGN3D",
|
"ALIGN3D",
|
||||||
"SECTION",
|
"SECTION",
|
||||||
|
"SECTIONPLANE",
|
||||||
"PYRAMID",
|
"PYRAMID",
|
||||||
"PYR",
|
"PYR",
|
||||||
"SPLINEFIT",
|
"SPLINEFIT",
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ impl OpenCADStudio {
|
|||||||
| "text_color"
|
| "text_color"
|
||||||
| "block_content_color"
|
| "block_content_color"
|
||||||
| "background_fill_color"
|
| "background_fill_color"
|
||||||
|
| "indicator_fill_color"
|
||||||
) {
|
) {
|
||||||
self.update(Message::PropColorFieldChanged {
|
self.update(Message::PropColorFieldChanged {
|
||||||
field: p.field.into(),
|
field: p.field.into(),
|
||||||
|
|||||||
@@ -5226,6 +5226,21 @@ impl OpenCADStudio {
|
|||||||
Message::PropColorFieldChanged { field, color } => {
|
Message::PropColorFieldChanged { field, color } => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let handles = self.property_target_handles(i);
|
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
|
// Dim-line colour override (Leader / Dimension): write it as an
|
||||||
// ACAD_DSTYLE code-176 override (an ACI index) so it round-trips
|
// ACAD_DSTYLE code-176 override (an ACI index) so it round-trips
|
||||||
// through DWG and DXF. RGB picks collapse to the nearest ACI, in
|
// through DWG and DXF. RGB picks collapse to the nearest ACI, in
|
||||||
|
|||||||
+340
-46
@@ -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]) {
|
fn push_segment(points: &mut Vec<[f64; 3]>, first: [f64; 3], second: [f64; 3]) {
|
||||||
if !points.is_empty() {
|
if !points.is_empty() {
|
||||||
points.push(NAN);
|
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]> {
|
fn section_lines(data: &SectionObjectData) -> Vec<[f64; 3]> {
|
||||||
let mut points = Vec::new();
|
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(
|
push_chain(
|
||||||
&mut points,
|
&mut points,
|
||||||
data.vertices.iter().map(|p| [p.x, p.y, p.z]),
|
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(
|
push_chain(
|
||||||
&mut points,
|
&mut points,
|
||||||
data.back_line_vertices.iter().map(|p| [p.x, p.y, p.z]),
|
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
|
points
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,52 +562,220 @@ fn section_properties(data: &SectionObjectData) -> Vec<PropSection> {
|
|||||||
let vertices = data
|
let vertices = data
|
||||||
.vertices
|
.vertices
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.map(|point| vector_text(*point))
|
||||||
.map(|(index, point)| format!("{}: {}", index + 1, vector_text(*point)))
|
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n");
|
.join("; ");
|
||||||
let back_vertices = data
|
let kind = section_kind(data);
|
||||||
.back_line_vertices
|
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<Vector3> {
|
||||||
|
let values = value
|
||||||
|
.split(|character: char| character == ',' || character == ';' || character.is_whitespace())
|
||||||
|
.filter(|part| !part.is_empty())
|
||||||
|
.map(str::parse::<f64>)
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.ok()?;
|
||||||
|
(values.len() == 3).then(|| Vector3::new(values[0], values[1], values[2]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_vertices(value: &str) -> Option<Vec<Vector3>> {
|
||||||
|
let values = value
|
||||||
|
.split(|character: char| {
|
||||||
|
character == ',' || character == ';' || character == ':' || character.is_whitespace()
|
||||||
|
})
|
||||||
|
.filter(|part| !part.trim().is_empty())
|
||||||
|
.map(str::parse::<f64>)
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.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::<Vec<_>>();
|
||||||
|
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()
|
.iter()
|
||||||
.enumerate()
|
.map(|point| *point + viewing * depth)
|
||||||
.map(|(index, point)| format!("{}: {}", index + 1, vector_text(*point)))
|
.collect();
|
||||||
.collect::<Vec<_>>()
|
}
|
||||||
.join("\n");
|
|
||||||
vec![
|
fn set_section_kind(data: &mut SectionObjectData, value: &str) {
|
||||||
PropSection {
|
let span = data
|
||||||
title: t!("Section Object").into_owned(),
|
.vertices
|
||||||
props: vec![
|
.first()
|
||||||
text_prop(t!("Name").as_ref(), "ext_section_name", &data.name),
|
.zip(data.vertices.last())
|
||||||
ro_prop(t!("State").as_ref(), "ext_section_state", data.state.to_string()),
|
.map_or(1.0, |(first, last)| (*last - *first).length().max(1e-4));
|
||||||
ro_prop(t!("Flags").as_ref(), "ext_section_flags", data.flags.to_string()),
|
match value.trim().to_ascii_uppercase().as_str() {
|
||||||
ro_prop(t!("Vertical Direction").as_ref(),
|
"SLICE" => {
|
||||||
"ext_section_vertical",
|
data.state = 2;
|
||||||
vector_text(data.vertical_direction),
|
set_section_depth(data, span / 60.0);
|
||||||
),
|
}
|
||||||
edit_prop(t!("Top Height").as_ref(), "ext_section_top", data.top_height),
|
"BOUNDARY" => {
|
||||||
edit_prop(t!("Bottom Height").as_ref(), "ext_section_bottom", data.bottom_height),
|
data.state = 2;
|
||||||
ro_prop(t!("Indicator Alpha").as_ref(),
|
set_section_depth(data, span);
|
||||||
"ext_section_alpha",
|
}
|
||||||
data.indicator_alpha.to_string(),
|
"VOLUME" => {
|
||||||
),
|
data.state = 4;
|
||||||
ro_prop(t!("Indicator Color").as_ref(),
|
set_section_depth(data, span);
|
||||||
"ext_section_color",
|
}
|
||||||
format!("{:?}", data.indicator_color),
|
_ => {
|
||||||
),
|
data.state = 1;
|
||||||
ro_prop(t!("Settings").as_ref(),
|
data.back_line_vertices.clear();
|
||||||
"ext_section_settings",
|
}
|
||||||
handle_text(data.settings_handle),
|
}
|
||||||
),
|
}
|
||||||
],
|
|
||||||
},
|
fn move_section_to_offset(data: &mut SectionObjectData, desired: f64) {
|
||||||
PropSection {
|
let current = section_plane_offset(data);
|
||||||
title: t!("Section Vertices").into_owned(),
|
let delta = section_viewing_direction(data) * (current - desired);
|
||||||
props: vec![
|
for point in data.vertices.iter_mut().chain(data.back_line_vertices.iter_mut()) {
|
||||||
ro_prop(t!("Cutting Line").as_ref(), "ext_section_vertices", vertices),
|
*point = *point + delta;
|
||||||
ro_prop(t!("Back Line").as_ref(), "ext_section_back_vertices", back_vertices),
|
}
|
||||||
],
|
}
|
||||||
},
|
|
||||||
]
|
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<PropSection> {
|
fn arc_text_properties(data: &ArcAlignedTextData) -> Vec<PropSection> {
|
||||||
@@ -1043,8 +1286,59 @@ fn apply_geom_prop(entity: &mut ExtendedEntity, field: &str, value: &str) {
|
|||||||
match &mut entity.data {
|
match &mut entity.data {
|
||||||
ExtendedEntityData::SectionObject(data) => match field {
|
ExtendedEntityData::SectionObject(data) => match field {
|
||||||
"ext_section_name" => data.name = value.to_string(),
|
"ext_section_name" => data.name = value.to_string(),
|
||||||
"ext_section_top" => set_f64(value, &mut data.top_height),
|
"ext_section_state" | "ext_section_state2" => set_section_kind(data, value),
|
||||||
"ext_section_bottom" => set_f64(value, &mut data.bottom_height),
|
"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::<i16>() {
|
||||||
|
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 {
|
ExtendedEntityData::ArcAlignedText(data) => match field {
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ pub fn ui_name(e: &EntityType) -> &'static str {
|
|||||||
EntityType::Light(_) => "Light",
|
EntityType::Light(_) => "Light",
|
||||||
EntityType::SectionSymbol(_) => "Section Symbol",
|
EntityType::SectionSymbol(_) => "Section Symbol",
|
||||||
EntityType::ViewBorder(_) => "View Border",
|
EntityType::ViewBorder(_) => "View Border",
|
||||||
|
EntityType::Extended(extended)
|
||||||
|
if matches!(
|
||||||
|
extended.data,
|
||||||
|
acadrust::entities::ExtendedEntityData::SectionObject(_)
|
||||||
|
) => "Section Plane",
|
||||||
EntityType::Extended(_) => "Extended Entity",
|
EntityType::Extended(_) => "Extended Entity",
|
||||||
EntityType::Seqend(_) => "Seqend",
|
EntityType::Seqend(_) => "Seqend",
|
||||||
EntityType::Unknown(_) => "Unknown",
|
EntityType::Unknown(_) => "Unknown",
|
||||||
@@ -79,6 +84,12 @@ pub fn ui_name(e: &EntityType) -> &'static str {
|
|||||||
/// (the numeric `DWG_TYPE_<n>` placeholder).
|
/// (the numeric `DWG_TYPE_<n>` placeholder).
|
||||||
pub fn ui_name_or_class(e: &EntityType) -> String {
|
pub fn ui_name_or_class(e: &EntityType) -> String {
|
||||||
if let EntityType::Extended(entity) = e {
|
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();
|
return entity.class_name().to_string();
|
||||||
}
|
}
|
||||||
if let EntityType::Unknown(u) = e {
|
if let EntityType::Unknown(u) = e {
|
||||||
@@ -148,6 +159,11 @@ pub fn dxf_name(e: &EntityType) -> &'static str {
|
|||||||
EntityType::Light(_) => "LIGHT",
|
EntityType::Light(_) => "LIGHT",
|
||||||
EntityType::SectionSymbol(_) => "SECTIONLINE",
|
EntityType::SectionSymbol(_) => "SECTIONLINE",
|
||||||
EntityType::ViewBorder(_) => "DRAWINGVIEW",
|
EntityType::ViewBorder(_) => "DRAWINGVIEW",
|
||||||
|
EntityType::Extended(extended)
|
||||||
|
if matches!(
|
||||||
|
extended.data,
|
||||||
|
acadrust::entities::ExtendedEntityData::SectionObject(_)
|
||||||
|
) => "SECTIONOBJECT",
|
||||||
_ => "ENTITY",
|
_ => "ENTITY",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ pub mod cylinder_cmd;
|
|||||||
pub mod edge_cmd;
|
pub mod edge_cmd;
|
||||||
pub mod polysolid_cmd;
|
pub mod polysolid_cmd;
|
||||||
pub mod primitive_cmd;
|
pub mod primitive_cmd;
|
||||||
|
pub mod sectionplane_cmd;
|
||||||
pub mod shell_cmd;
|
pub mod shell_cmd;
|
||||||
pub mod slice_cmd;
|
pub mod slice_cmd;
|
||||||
|
|
||||||
|
|||||||
@@ -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<DVec3>,
|
||||||
|
draw_points: Vec<DVec3>,
|
||||||
|
picked_direction: Option<DVec3>,
|
||||||
|
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<DVec3>, 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>, 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] <Top>:").into_owned(),
|
||||||
|
Step::Type => crate::t!("SECTIONPLANE Enter section plane type [Plane/Slice/Boundary/Volume] <Plane>:").into_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
|
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<CmdResult> {
|
||||||
|
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<DVec3>) {
|
||||||
|
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<CmdResult> {
|
||||||
|
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<WireModel> {
|
||||||
|
let mut points = self.draw_points.iter().map(|p| p.to_array()).collect::<Vec<_>>();
|
||||||
|
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"]
|
||||||
|
});
|
||||||
+179
-4
@@ -2021,6 +2021,43 @@ pub struct Scene {
|
|||||||
undo_recording: Option<UndoRecording>,
|
undo_recording: Option<UndoRecording>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<Option<cadkernel::brep::Body>, ()> {
|
||||||
|
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 {
|
impl Scene {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -2431,7 +2468,17 @@ impl Scene {
|
|||||||
bits |= CACHE_CATEGORY_IMAGE;
|
bits |= CACHE_CATEGORY_IMAGE;
|
||||||
}
|
}
|
||||||
let is_insert = matches!(entity, EntityType::Insert(_));
|
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;
|
bits |= CACHE_CATEGORY_MESH | CACHE_CATEGORY_INTERACTION;
|
||||||
}
|
}
|
||||||
@@ -6594,6 +6641,12 @@ impl Scene {
|
|||||||
|| matches!(
|
|| matches!(
|
||||||
self.document.get_entity(h),
|
self.document.get_entity(h),
|
||||||
Some(EntityType::Insert(_))
|
Some(EntityType::Insert(_))
|
||||||
|
| Some(EntityType::Extended(
|
||||||
|
acadrust::entities::ExtendedEntity {
|
||||||
|
data: acadrust::entities::ExtendedEntityData::SectionObject(_),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
))
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) =>
|
) =>
|
||||||
@@ -6780,6 +6833,7 @@ impl Scene {
|
|||||||
all_visible: bool,
|
all_visible: bool,
|
||||||
viewport: Option<Handle>,
|
viewport: Option<Handle>,
|
||||||
) -> Vec<MeshLodSet> {
|
) -> Vec<MeshLodSet> {
|
||||||
|
let live_section = self.active_live_section(target_block);
|
||||||
// Top-level solids: drop those whose layer is off/frozen or that are
|
// Top-level solids: drop those whose layer is off/frozen or that are
|
||||||
// flagged invisible / isolated-hidden, mirroring the 2D wire path, plus
|
// flagged invisible / isolated-hidden, mirroring the 2D wire path, plus
|
||||||
// any whose layer is frozen in the requesting viewport.
|
// any whose layer is frozen in the requesting viewport.
|
||||||
@@ -6801,8 +6855,19 @@ impl Scene {
|
|||||||
})
|
})
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
})
|
})
|
||||||
.map(|(&handle, set)| {
|
.filter_map(|(&handle, set)| {
|
||||||
let mut set = set.clone();
|
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) {
|
if let Some(entity) = self.document.get_entity(handle) {
|
||||||
let style = crate::scene::view::render::render_style_for_viewport(
|
let style = crate::scene::view::render::render_style_for_viewport(
|
||||||
&self.document,
|
&self.document,
|
||||||
@@ -6826,7 +6891,7 @@ impl Scene {
|
|||||||
material.diffuse[3] = style.0[3];
|
material.diffuse[3] = style.0[3];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
set
|
Some(set)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
// Block-definition solids are instanced per INSERT of the ACTIVE space's
|
// Block-definition solids are instanced per INSERT of the ACTIVE space's
|
||||||
@@ -6843,6 +6908,116 @@ impl Scene {
|
|||||||
all
|
all
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn active_live_section(
|
||||||
|
&self,
|
||||||
|
target_block: Handle,
|
||||||
|
) -> Option<acadrust::entities::SectionObjectData> {
|
||||||
|
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<Option<cadkernel::brep::Body>, ()> {
|
||||||
|
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.
|
/// True when `layer` is turned off or frozen — entities on it never render.
|
||||||
fn layer_hidden(&self, layer: &str) -> bool {
|
fn layer_hidden(&self, layer: &str) -> bool {
|
||||||
self.document
|
self.document
|
||||||
|
|||||||
@@ -921,6 +921,7 @@ impl PropertiesPanel {
|
|||||||
| "text_color"
|
| "text_color"
|
||||||
| "block_content_color"
|
| "block_content_color"
|
||||||
| "background_fill_color"
|
| "background_fill_color"
|
||||||
|
| "indicator_fill_color"
|
||||||
) {
|
) {
|
||||||
let open = self.open_color_field.as_deref() == Some(field);
|
let open = self.open_color_field.as_deref() == Some(field);
|
||||||
let fsel = field.to_string();
|
let fsel = field.to_string();
|
||||||
|
|||||||
Reference in new issue
Block a user