forked from 1b-agent/opencadstudio-upstream
fix(sectionplane): persist and clip section modes correctly
This commit is contained in:
1 parent
57b9cf979e
commit
f69f0b32be
14 files changed
+831
-119
No files matched your search
Generated
+2
-2
@@ -79,7 +79,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "acadrust"
|
name = "acadrust"
|
||||||
version = "0.5.4"
|
version = "0.5.4"
|
||||||
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=62628c41670a914d1598a14faafc3c6e3aa4a270#62628c41670a914d1598a14faafc3c6e3aa4a270"
|
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=f23ffc873228e5f41b876485c741e4a8ac20a67d#f23ffc873228e5f41b876485c741e4a8ac20a67d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.12",
|
"ahash 0.8.12",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -885,7 +885,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "cadkernel"
|
name = "cadkernel"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=920ff591028ab4ba13648b920d9d333ec3b7d44b#920ff591028ab4ba13648b920d9d333ec3b7d44b"
|
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=dfd0de176a878c20a1aeb8d23b0b8265197b5f5c#dfd0de176a878c20a1aeb8d23b0b8265197b5f5c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"acadrust",
|
"acadrust",
|
||||||
"cavalier_contours",
|
"cavalier_contours",
|
||||||
|
|||||||
+2
-2
@@ -31,8 +31,8 @@ glam = { version = "0.33", features = ["bytemuck"] }
|
|||||||
rfd = "0.17"
|
rfd = "0.17"
|
||||||
clap = { version = "4", features = ["derive", "string"] }
|
clap = { version = "4", features = ["derive", "string"] }
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "62628c41670a914d1598a14faafc3c6e3aa4a270", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "f23ffc873228e5f41b876485c741e4a8ac20a67d", features = ["serde"] }
|
||||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "920ff591028ab4ba13648b920d9d333ec3b7d44b", features = ["acis", "offset"] }
|
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "dfd0de176a878c20a1aeb8d23b0b8265197b5f5c", features = ["acis", "offset"] }
|
||||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ bincode = "1.3"
|
|||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
|
||||||
# Host adapters convert ObjectId to acadrust handles; kernel handles stay local.
|
# Host adapters convert ObjectId to acadrust handles; kernel handles stay local.
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "62628c41670a914d1598a14faafc3c6e3aa4a270", features = ["serde"], optional = true }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "f23ffc873228e5f41b876485c741e4a8ac20a67d", features = ["serde"], optional = true }
|
||||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "920ff591028ab4ba13648b920d9d333ec3b7d44b", features = ["acis", "offset"], optional = true }
|
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "dfd0de176a878c20a1aeb8d23b0b8265197b5f5c", features = ["acis", "offset"], optional = true }
|
||||||
ocs_plugin_api = { path = "../ocs_plugin_api", optional = true, features = ["host"] }
|
ocs_plugin_api = { path = "../ocs_plugin_api", optional = true, features = ["host"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
# Pulled in only by the `host` feature, which adds the `acadrust`-typed
|
# Pulled in only by the `host` feature, which adds the `acadrust`-typed
|
||||||
# `HostApi` runtime surface. The default crate stays dependency-free so engine
|
# `HostApi` runtime surface. The default crate stays dependency-free so engine
|
||||||
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
|
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "62628c41670a914d1598a14faafc3c6e3aa4a270", optional = true, features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "f23ffc873228e5f41b876485c741e4a8ac20a67d", optional = true, features = ["serde"] }
|
||||||
|
|
||||||
# Runtime IPC and serialization (host feature only).
|
# Runtime IPC and serialization (host feature only).
|
||||||
interprocess = { version = "2", optional = true }
|
interprocess = { version = "2", optional = true }
|
||||||
@@ -38,7 +38,7 @@ serde_json = "1"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
cargo-lock = "11"
|
cargo-lock = "11"
|
||||||
# acadrust is scanned at build time to generate the embedded type registry.
|
# acadrust is scanned at build time to generate the embedded type registry.
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "62628c41670a914d1598a14faafc3c6e3aa4a270", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "f23ffc873228e5f41b876485c741e4a8ac20a67d", features = ["serde"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
@@ -8,7 +8,7 @@ publish = false
|
|||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "62628c41670a914d1598a14faafc3c6e3aa4a270", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "f23ffc873228e5f41b876485c741e4a8ac20a67d", features = ["serde"] }
|
||||||
bincode = "1.3"
|
bincode = "1.3"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
|
|||||||
@@ -10,4 +10,4 @@ crate-type = ["cdylib"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ocs_plugin_api = { path = "../ocs_plugin_api", features = ["host"] }
|
ocs_plugin_api = { path = "../ocs_plugin_api", features = ["host"] }
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "62628c41670a914d1598a14faafc3c6e3aa4a270", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "f23ffc873228e5f41b876485c741e4a8ac20a67d", features = ["serde"] }
|
||||||
@@ -675,6 +675,10 @@ impl OpenCADStudio {
|
|||||||
entity,
|
entity,
|
||||||
EntityType::Block(_)
|
EntityType::Block(_)
|
||||||
| EntityType::BlockEnd(_)
|
| EntityType::BlockEnd(_)
|
||||||
|
| EntityType::Extended(acadrust::entities::ExtendedEntity {
|
||||||
|
data: acadrust::entities::ExtendedEntityData::SectionObject(_),
|
||||||
|
..
|
||||||
|
})
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
+162
-65
@@ -3,6 +3,7 @@ use acadrust::entities::{
|
|||||||
PointCloudData, PointCloudExData, RemoteTextData, SectionObjectData,
|
PointCloudData, PointCloudExData, RemoteTextData, SectionObjectData,
|
||||||
};
|
};
|
||||||
use acadrust::types::{Handle, Transform, Vector3};
|
use acadrust::types::{Handle, Transform, Vector3};
|
||||||
|
use acadrust::xdata::{ExtendedDataRecord, XDataValue};
|
||||||
use crate::t;
|
use crate::t;
|
||||||
|
|
||||||
use crate::command::EntityTransform;
|
use crate::command::EntityTransform;
|
||||||
@@ -17,6 +18,69 @@ use crate::scene::model::object::{
|
|||||||
use crate::scene::model::wire_model::SnapHint;
|
use crate::scene::model::wire_model::SnapHint;
|
||||||
|
|
||||||
const NAN: [f64; 3] = [f64::NAN; 3];
|
const NAN: [f64; 3] = [f64::NAN; 3];
|
||||||
|
const SECTION_SLICE_APP: &str = "IsSlice";
|
||||||
|
const SECTION_THICKNESS_APP: &str = "ThicknessDepth";
|
||||||
|
|
||||||
|
pub(crate) fn section_is_slice(entity: &ExtendedEntity) -> bool {
|
||||||
|
entity
|
||||||
|
.common
|
||||||
|
.extended_data
|
||||||
|
.get_record(SECTION_SLICE_APP)
|
||||||
|
.is_some_and(|record| {
|
||||||
|
record
|
||||||
|
.values
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.find_map(|value| match value {
|
||||||
|
XDataValue::Integer16(value) => Some(*value != 0),
|
||||||
|
XDataValue::Integer32(value) => Some(*value != 0),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn section_slice_depth(entity: &ExtendedEntity) -> Option<f64> {
|
||||||
|
entity
|
||||||
|
.common
|
||||||
|
.extended_data
|
||||||
|
.get_record(SECTION_THICKNESS_APP)?
|
||||||
|
.values
|
||||||
|
.iter()
|
||||||
|
.find_map(|value| match value {
|
||||||
|
XDataValue::Real(value) | XDataValue::Distance(value) => Some(*value),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.filter(|value| value.is_finite())
|
||||||
|
.map(f64::abs)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_section_slice_metadata(
|
||||||
|
entity: &mut ExtendedEntity,
|
||||||
|
enabled: bool,
|
||||||
|
depth: f64,
|
||||||
|
) {
|
||||||
|
if !enabled {
|
||||||
|
entity.common.extended_data.remove_record(SECTION_SLICE_APP);
|
||||||
|
entity
|
||||||
|
.common
|
||||||
|
.extended_data
|
||||||
|
.remove_record(SECTION_THICKNESS_APP);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut slice = ExtendedDataRecord::new(SECTION_SLICE_APP);
|
||||||
|
slice.values = vec![XDataValue::Integer16(0), XDataValue::Integer16(1)];
|
||||||
|
entity.common.extended_data.upsert_record(slice);
|
||||||
|
|
||||||
|
let mut thickness = ExtendedDataRecord::new(SECTION_THICKNESS_APP);
|
||||||
|
thickness.values = vec![XDataValue::Real(if depth.is_finite() {
|
||||||
|
depth.abs()
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
})];
|
||||||
|
entity.common.extended_data.upsert_record(thickness);
|
||||||
|
}
|
||||||
|
|
||||||
fn vector_text(value: Vector3) -> String {
|
fn vector_text(value: Vector3) -> String {
|
||||||
format!("{:.6}, {:.6}, {:.6}", value.x, value.y, value.z)
|
format!("{:.6}, {:.6}, {:.6}", value.x, value.y, value.z)
|
||||||
@@ -558,17 +622,17 @@ fn to_render(entity: &ExtendedEntity, document: &acadrust::CadDocument) -> Optio
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn section_properties(data: &SectionObjectData) -> Vec<PropSection> {
|
fn section_properties(entity: &ExtendedEntity, data: &SectionObjectData) -> Vec<PropSection> {
|
||||||
let vertices = data
|
let vertices = data
|
||||||
.vertices
|
.vertices
|
||||||
.iter()
|
.iter()
|
||||||
.map(|point| vector_text(*point))
|
.map(|point| vector_text(*point))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("; ");
|
.join("; ");
|
||||||
let kind = section_kind(data);
|
let kind = section_kind(entity, data);
|
||||||
let viewing = section_viewing_direction(data);
|
let viewing = section_viewing_direction(data);
|
||||||
let offset = section_plane_offset(data);
|
let offset = section_plane_offset(data);
|
||||||
let depth = section_depth(data);
|
let depth = section_slice_depth(entity).unwrap_or_else(|| section_depth(data));
|
||||||
vec![PropSection {
|
vec![PropSection {
|
||||||
title: t!("Section Plane").into_owned(),
|
title: t!("Section Plane").into_owned(),
|
||||||
props: vec![
|
props: vec![
|
||||||
@@ -656,18 +720,14 @@ fn section_depth(data: &SectionObjectData) -> f64 {
|
|||||||
.map_or(0.0, |(front, back)| (*back - *front).length())
|
.map_or(0.0, |(front, back)| (*back - *front).length())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn section_kind(data: &SectionObjectData) -> &'static str {
|
fn section_kind(entity: &ExtendedEntity, data: &SectionObjectData) -> &'static str {
|
||||||
|
if section_is_slice(entity) {
|
||||||
|
return "Slice";
|
||||||
|
}
|
||||||
match data.state {
|
match data.state {
|
||||||
1 => "Plane",
|
1 => "Plane",
|
||||||
4 => "Volume",
|
4 => "Volume",
|
||||||
2 => {
|
2 => "Boundary",
|
||||||
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",
|
_ => "Plane",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -734,8 +794,8 @@ fn set_section_kind(data: &mut SectionObjectData, value: &str) {
|
|||||||
.map_or(1.0, |(first, last)| (*last - *first).length().max(1e-4));
|
.map_or(1.0, |(first, last)| (*last - *first).length().max(1e-4));
|
||||||
match value.trim().to_ascii_uppercase().as_str() {
|
match value.trim().to_ascii_uppercase().as_str() {
|
||||||
"SLICE" => {
|
"SLICE" => {
|
||||||
data.state = 2;
|
data.state = 1;
|
||||||
set_section_depth(data, span / 60.0);
|
data.back_line_vertices.clear();
|
||||||
}
|
}
|
||||||
"BOUNDARY" => {
|
"BOUNDARY" => {
|
||||||
data.state = 2;
|
data.state = 2;
|
||||||
@@ -1135,7 +1195,7 @@ fn properties(entity: &ExtendedEntity) -> Vec<PropSection> {
|
|||||||
title: t!("Camera").into_owned(),
|
title: t!("Camera").into_owned(),
|
||||||
props: vec![ro_prop(t!("View").as_ref(), "ext_camera_view", handle_text(*view_handle))],
|
props: vec![ro_prop(t!("View").as_ref(), "ext_camera_view", handle_text(*view_handle))],
|
||||||
}],
|
}],
|
||||||
ExtendedEntityData::SectionObject(data) => section_properties(data),
|
ExtendedEntityData::SectionObject(data) => section_properties(entity, data),
|
||||||
ExtendedEntityData::ArcAlignedText(data) => arc_text_properties(data),
|
ExtendedEntityData::ArcAlignedText(data) => arc_text_properties(data),
|
||||||
ExtendedEntityData::RemoteText(data) => remote_text_properties(data),
|
ExtendedEntityData::RemoteText(data) => remote_text_properties(data),
|
||||||
ExtendedEntityData::GeoPositionMarker(data) => geo_marker_properties(data),
|
ExtendedEntityData::GeoPositionMarker(data) => geo_marker_properties(data),
|
||||||
@@ -1282,65 +1342,97 @@ fn set_f64(value: &str, target: &mut f64) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_geom_prop(entity: &mut ExtendedEntity, field: &str, value: &str) {
|
fn apply_section_prop(entity: &mut ExtendedEntity, field: &str, value: &str) {
|
||||||
match &mut entity.data {
|
let was_slice = section_is_slice(entity);
|
||||||
ExtendedEntityData::SectionObject(data) => match field {
|
let stored_slice_depth = section_slice_depth(entity).unwrap_or(0.0);
|
||||||
"ext_section_name" => data.name = value.to_string(),
|
let ExtendedEntityData::SectionObject(data) = &mut entity.data else {
|
||||||
"ext_section_state" | "ext_section_state2" => set_section_kind(data, value),
|
return;
|
||||||
"ext_section_viewing" => {
|
};
|
||||||
if let Some(direction) = parse_vector(value) {
|
let mut slice_update = None;
|
||||||
set_viewing_direction(data, direction);
|
match field {
|
||||||
}
|
"ext_section_name" => data.name = value.to_string(),
|
||||||
|
"ext_section_state" | "ext_section_state2" => {
|
||||||
|
let kind = value.trim().to_ascii_uppercase();
|
||||||
|
let span = data
|
||||||
|
.vertices
|
||||||
|
.first()
|
||||||
|
.zip(data.vertices.last())
|
||||||
|
.map_or(1.0, |(first, last)| (*last - *first).length().max(1e-4));
|
||||||
|
set_section_kind(data, &kind);
|
||||||
|
slice_update = Some((kind == "SLICE", (span / 60.0).max(1e-4)));
|
||||||
|
}
|
||||||
|
"ext_section_viewing" => {
|
||||||
|
if let Some(direction) = parse_vector(value) {
|
||||||
|
set_viewing_direction(data, direction);
|
||||||
}
|
}
|
||||||
"ext_section_vertical" => {
|
}
|
||||||
if let Some(direction) = parse_vector(value) {
|
"ext_section_vertical" => {
|
||||||
let depth = section_depth(data);
|
if let Some(direction) = parse_vector(value) {
|
||||||
let direction = normalized(direction, data.vertical_direction);
|
let depth = section_depth(data);
|
||||||
if direction.dot(§ion_tangent(data)).abs() < 1.0 - 1e-9 {
|
let direction = normalized(direction, data.vertical_direction);
|
||||||
data.vertical_direction = direction;
|
if direction.dot(§ion_tangent(data)).abs() < 1.0 - 1e-9 {
|
||||||
if depth > 0.0 {
|
data.vertical_direction = direction;
|
||||||
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 {
|
if depth > 0.0 {
|
||||||
set_section_depth(data, depth);
|
set_section_depth(data, depth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"ext_section_depth" => {
|
}
|
||||||
if let Some(depth) = parse_f64(value) {
|
"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);
|
set_section_depth(data, depth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
}
|
||||||
},
|
"ext_section_depth" => {
|
||||||
|
if let Some(depth) = parse_f64(value) {
|
||||||
|
if was_slice {
|
||||||
|
slice_update = Some((true, depth));
|
||||||
|
} else {
|
||||||
|
set_section_depth(data, depth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((enabled, depth)) = slice_update {
|
||||||
|
set_section_slice_metadata(entity, enabled, depth);
|
||||||
|
} else if was_slice && field != "ext_section_depth" {
|
||||||
|
set_section_slice_metadata(entity, true, stored_slice_depth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_geom_prop(entity: &mut ExtendedEntity, field: &str, value: &str) {
|
||||||
|
if matches!(entity.data, ExtendedEntityData::SectionObject(_)) {
|
||||||
|
apply_section_prop(entity, field, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match &mut entity.data {
|
||||||
ExtendedEntityData::ArcAlignedText(data) => match field {
|
ExtendedEntityData::ArcAlignedText(data) => match field {
|
||||||
"ext_arc_text" => data.text = value.to_string(),
|
"ext_arc_text" => data.text = value.to_string(),
|
||||||
"ext_arc_font" => data.font_name = value.to_string(),
|
"ext_arc_font" => data.font_name = value.to_string(),
|
||||||
@@ -1692,6 +1784,8 @@ fn transformed_planar_angle(
|
|||||||
fn apply_transform(entity: &mut ExtendedEntity, requested: &EntityTransform) {
|
fn apply_transform(entity: &mut ExtendedEntity, requested: &EntityTransform) {
|
||||||
let transform = entity_transform(requested);
|
let transform = entity_transform(requested);
|
||||||
let scale = scalar_scale(requested);
|
let scale = scalar_scale(requested);
|
||||||
|
let slice_depth = section_is_slice(entity)
|
||||||
|
.then(|| section_slice_depth(entity).unwrap_or(0.0) * scale);
|
||||||
match &mut entity.data {
|
match &mut entity.data {
|
||||||
ExtendedEntityData::SectionObject(data) => {
|
ExtendedEntityData::SectionObject(data) => {
|
||||||
for point in data
|
for point in data
|
||||||
@@ -1830,6 +1924,9 @@ fn apply_transform(entity: &mut ExtendedEntity, requested: &EntityTransform) {
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
if let Some(depth) = slice_depth {
|
||||||
|
set_section_slice_metadata(entity, true, depth);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderConvertible for ExtendedEntity {
|
impl RenderConvertible for ExtendedEntity {
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ impl SectionKind {
|
|||||||
|
|
||||||
fn state(self) -> i32 {
|
fn state(self) -> i32 {
|
||||||
match self {
|
match self {
|
||||||
Self::Plane => 1,
|
Self::Plane | Self::Slice => 1,
|
||||||
Self::Slice | Self::Boundary => 2,
|
Self::Boundary => 2,
|
||||||
Self::Volume => 4,
|
Self::Volume => 4,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,12 +140,12 @@ impl SectionPlaneCommand {
|
|||||||
SectionKind::Slice => (span / 60.0).max(1e-4),
|
SectionKind::Slice => (span / 60.0).max(1e-4),
|
||||||
SectionKind::Boundary | SectionKind::Volume => span,
|
SectionKind::Boundary | SectionKind::Volume => span,
|
||||||
};
|
};
|
||||||
let back_line_vertices = if depth > 0.0 {
|
let back_line_vertices = if matches!(self.kind, SectionKind::Boundary | SectionKind::Volume) {
|
||||||
vertices.iter().map(|point| *point + viewing * depth).collect()
|
vertices.iter().map(|point| *point + viewing * depth).collect()
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
EntityType::Extended(ExtendedEntity {
|
let mut entity = ExtendedEntity {
|
||||||
common: EntityCommon::new(),
|
common: EntityCommon::new(),
|
||||||
data: ExtendedEntityData::SectionObject(SectionObjectData {
|
data: ExtendedEntityData::SectionObject(SectionObjectData {
|
||||||
state: self.kind.state(),
|
state: self.kind.state(),
|
||||||
@@ -166,7 +166,13 @@ impl SectionPlaneCommand {
|
|||||||
.collect(),
|
.collect(),
|
||||||
settings_handle: Handle::NULL,
|
settings_handle: Handle::NULL,
|
||||||
}),
|
}),
|
||||||
})
|
};
|
||||||
|
crate::entities::extended::set_section_slice_metadata(
|
||||||
|
&mut entity,
|
||||||
|
self.kind == SectionKind::Slice,
|
||||||
|
depth,
|
||||||
|
);
|
||||||
|
EntityType::Extended(entity)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn line_at(&self, point: DVec3, viewing: DVec3, vertical_hint: DVec3) -> (Vec<DVec3>, DVec3) {
|
fn line_at(&self, point: DVec3, viewing: DVec3, vertical_hint: DVec3) -> (Vec<DVec3>, DVec3) {
|
||||||
@@ -482,3 +488,50 @@ impl CadCommand for SectionPlaneCommand {
|
|||||||
inventory::submit!(crate::command::CommandRegistration {
|
inventory::submit!(crate::command::CommandRegistration {
|
||||||
names: &["SECTIONPLANE"]
|
names: &["SECTIONPLANE"]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn data(entity: &EntityType) -> (&ExtendedEntity, &SectionObjectData) {
|
||||||
|
let EntityType::Extended(entity) = entity else {
|
||||||
|
panic!("expected extended entity");
|
||||||
|
};
|
||||||
|
let ExtendedEntityData::SectionObject(data) = &entity.data else {
|
||||||
|
panic!("expected section object");
|
||||||
|
};
|
||||||
|
(entity, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn slice_uses_persistent_thickness_without_boundary_geometry() {
|
||||||
|
let mut command = SectionPlaneCommand::new(
|
||||||
|
Some((DVec3::ZERO, DVec3::splat(10.0))),
|
||||||
|
3,
|
||||||
|
);
|
||||||
|
command.kind = SectionKind::Slice;
|
||||||
|
let entity = command.entity(vec![DVec3::ZERO, DVec3::Y * 6.0], DVec3::Z, true);
|
||||||
|
let (entity, section) = data(&entity);
|
||||||
|
|
||||||
|
assert_eq!(section.state, 1);
|
||||||
|
assert!(section.back_line_vertices.is_empty());
|
||||||
|
assert!(crate::entities::extended::section_is_slice(entity));
|
||||||
|
assert!((crate::entities::extended::section_slice_depth(entity).unwrap() - 0.1).abs()
|
||||||
|
< 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn boundary_keeps_a_real_back_line_and_no_slice_marker() {
|
||||||
|
let mut command = SectionPlaneCommand::new(
|
||||||
|
Some((DVec3::ZERO, DVec3::splat(10.0))),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
command.kind = SectionKind::Boundary;
|
||||||
|
let entity = command.entity(vec![DVec3::ZERO, DVec3::Y * 6.0], DVec3::Z, true);
|
||||||
|
let (entity, section) = data(&entity);
|
||||||
|
|
||||||
|
assert_eq!(section.state, 2);
|
||||||
|
assert_eq!(section.back_line_vertices.len(), section.vertices.len());
|
||||||
|
assert!(!crate::entities::extended::section_is_slice(entity));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -167,7 +167,7 @@ pub fn kernel_surface_body(surface: &Surface) -> Option<KernelBody> {
|
|||||||
kernel_acis_body(&surface.acis_data)
|
kernel_acis_body(&surface.acis_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn kernel_acis_body(acis: &acadrust::entities::AcisData) -> Option<KernelBody> {
|
pub(crate) fn kernel_acis_body(acis: &acadrust::entities::AcisData) -> Option<KernelBody> {
|
||||||
let sat = parse_acis(
|
let sat = parse_acis(
|
||||||
|| acis.parse(),
|
|| acis.parse(),
|
||||||
acis.is_binary,
|
acis.is_binary,
|
||||||
|
|||||||
+106
-1
@@ -143,6 +143,106 @@ impl Scene {
|
|||||||
let _ = self.document.app_ids.add(app_id);
|
let _ = self.document.app_ids.add(app_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prepare_section_objects(&mut self, entity: &mut EntityType) -> bool {
|
||||||
|
use acadrust::entities::ExtendedEntityData;
|
||||||
|
use acadrust::objects::{
|
||||||
|
ClassObject, ClassObjectData, SectionManager, SectionSettings,
|
||||||
|
SectionTypeSettings,
|
||||||
|
};
|
||||||
|
|
||||||
|
let EntityType::Extended(extended) = entity else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let ExtendedEntityData::SectionObject(data) = &mut extended.data else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if extended.common.handle.is_null() {
|
||||||
|
extended.common.handle = self.document.allocate_handle();
|
||||||
|
}
|
||||||
|
let entity_handle = extended.common.handle;
|
||||||
|
|
||||||
|
let valid_settings = matches!(
|
||||||
|
self.document.objects.get(&data.settings_handle),
|
||||||
|
Some(ObjectType::ClassObject(object))
|
||||||
|
if matches!(&object.data, ClassObjectData::SectionSettings(_))
|
||||||
|
);
|
||||||
|
if !valid_settings {
|
||||||
|
let settings_handle = self.document.allocate_handle();
|
||||||
|
let mut settings = ClassObject::new(ClassObjectData::SectionSettings(
|
||||||
|
SectionSettings {
|
||||||
|
current_type: 4,
|
||||||
|
types: vec![SectionTypeSettings {
|
||||||
|
section_type: 4,
|
||||||
|
generation: 17,
|
||||||
|
..SectionTypeSettings::default()
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
));
|
||||||
|
settings.handle = settings_handle;
|
||||||
|
settings.owner = entity_handle;
|
||||||
|
self.document
|
||||||
|
.objects
|
||||||
|
.insert(settings_handle, ObjectType::ClassObject(settings));
|
||||||
|
data.settings_handle = settings_handle;
|
||||||
|
} else if let Some(ObjectType::ClassObject(settings)) =
|
||||||
|
self.document.objects.get_mut(&data.settings_handle)
|
||||||
|
{
|
||||||
|
settings.owner = entity_handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
let root = crate::scene::annotative::root_named_dict_handle(&mut self.document);
|
||||||
|
let manager_handle = self
|
||||||
|
.document
|
||||||
|
.objects
|
||||||
|
.get(&root)
|
||||||
|
.and_then(|object| match object {
|
||||||
|
ObjectType::Dictionary(dictionary) => dictionary
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.find(|(name, _)| name.eq_ignore_ascii_case("ACAD_SECTION_MANAGER"))
|
||||||
|
.map(|(_, handle)| *handle),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.filter(|handle| {
|
||||||
|
matches!(
|
||||||
|
self.document.objects.get(handle),
|
||||||
|
Some(ObjectType::ClassObject(object))
|
||||||
|
if matches!(&object.data, ClassObjectData::SectionManager(_))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
let handle = self.document.allocate_handle();
|
||||||
|
let mut manager =
|
||||||
|
ClassObject::new(ClassObjectData::SectionManager(SectionManager::default()));
|
||||||
|
manager.handle = handle;
|
||||||
|
manager.owner = root;
|
||||||
|
self.document
|
||||||
|
.objects
|
||||||
|
.insert(handle, ObjectType::ClassObject(manager));
|
||||||
|
if let Some(ObjectType::Dictionary(dictionary)) =
|
||||||
|
self.document.objects.get_mut(&root)
|
||||||
|
{
|
||||||
|
dictionary.entries.retain(|(name, _)| {
|
||||||
|
!name.eq_ignore_ascii_case("ACAD_SECTION_MANAGER")
|
||||||
|
});
|
||||||
|
dictionary.add_entry("ACAD_SECTION_MANAGER", handle);
|
||||||
|
}
|
||||||
|
handle
|
||||||
|
});
|
||||||
|
if let Some(ObjectType::ClassObject(manager)) =
|
||||||
|
self.document.objects.get_mut(&manager_handle)
|
||||||
|
{
|
||||||
|
manager.owner = root;
|
||||||
|
if let ClassObjectData::SectionManager(manager) = &mut manager.data {
|
||||||
|
if !manager.sections.contains(&entity_handle) {
|
||||||
|
manager.sections.push(entity_handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_entity(&mut self, entity: EntityType) -> Handle {
|
pub fn add_entity(&mut self, entity: EntityType) -> Handle {
|
||||||
self.add_entity_internal(entity, true)
|
self.add_entity_internal(entity, true)
|
||||||
}
|
}
|
||||||
@@ -195,6 +295,7 @@ impl Scene {
|
|||||||
// bookkeeping and remains in place while an entity delta is undone.
|
// bookkeeping and remains in place while an entity delta is undone.
|
||||||
let mutates_block_structure =
|
let mutates_block_structure =
|
||||||
matches!(&entity, EntityType::Block(_) | EntityType::BlockEnd(_));
|
matches!(&entity, EntityType::Block(_) | EntityType::BlockEnd(_));
|
||||||
|
let creates_section_structure = self.prepare_section_objects(&mut entity);
|
||||||
let hatch_seed = if let EntityType::Hatch(dxf) = &entity {
|
let hatch_seed = if let EntityType::Hatch(dxf) = &entity {
|
||||||
let color = self.render_style(&entity).0;
|
let color = self.render_style(&entity).0;
|
||||||
Self::hatch_model_from_dxf(dxf, color)
|
Self::hatch_model_from_dxf(dxf, color)
|
||||||
@@ -337,7 +438,11 @@ impl Scene {
|
|||||||
// state (a new layer, application ID, or block).
|
// state (a new layer, application ID, or block).
|
||||||
if self.is_recording_undo() {
|
if self.is_recording_undo() {
|
||||||
self.record_undo_before(handle, None);
|
self.record_undo_before(handle, None);
|
||||||
if creates_layer || creates_app_id || mutates_block_structure {
|
if creates_layer
|
||||||
|
|| creates_app_id
|
||||||
|
|| mutates_block_structure
|
||||||
|
|| creates_section_structure
|
||||||
|
{
|
||||||
self.poison_undo_recording();
|
self.poison_undo_recording();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+335
-37
@@ -2040,6 +2040,53 @@ fn section_frame(
|
|||||||
Some((origin, tangent, vertical, viewing))
|
Some((origin, tangent, vertical, viewing))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct LiveSection {
|
||||||
|
data: acadrust::entities::SectionObjectData,
|
||||||
|
slice_depth: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn section_plane(
|
||||||
|
origin: glam::DVec3,
|
||||||
|
tangent: glam::DVec3,
|
||||||
|
normal: glam::DVec3,
|
||||||
|
body_to_world: Option<&acadrust::types::Transform>,
|
||||||
|
) -> Option<cadkernel::space::Plane> {
|
||||||
|
let Some(transform) = body_to_world else {
|
||||||
|
return cadkernel::space::Plane::orthonormal(
|
||||||
|
origin.to_array(),
|
||||||
|
tangent.to_array(),
|
||||||
|
normal.to_array(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
let matrix = transform.matrix.m;
|
||||||
|
let linear = acadrust::types::Matrix3::from_rows(
|
||||||
|
[matrix[0][0], matrix[0][1], matrix[0][2]],
|
||||||
|
[matrix[1][0], matrix[1][1], matrix[1][2]],
|
||||||
|
[matrix[2][0], matrix[2][1], matrix[2][2]],
|
||||||
|
);
|
||||||
|
let inverse = linear.inverse()?;
|
||||||
|
let translation = acadrust::types::Vector3::new(
|
||||||
|
matrix[0][3],
|
||||||
|
matrix[1][3],
|
||||||
|
matrix[2][3],
|
||||||
|
);
|
||||||
|
let origin = inverse.transform_point(
|
||||||
|
acadrust::types::Vector3::new(origin.x, origin.y, origin.z) - translation,
|
||||||
|
);
|
||||||
|
let tangent = inverse.transform_point(acadrust::types::Vector3::new(
|
||||||
|
tangent.x, tangent.y, tangent.z,
|
||||||
|
));
|
||||||
|
let normal = linear
|
||||||
|
.transpose()
|
||||||
|
.transform_point(acadrust::types::Vector3::new(normal.x, normal.y, normal.z));
|
||||||
|
cadkernel::space::Plane::orthonormal(
|
||||||
|
[origin.x, origin.y, origin.z],
|
||||||
|
[tangent.x, tangent.y, tangent.z],
|
||||||
|
[normal.x, normal.y, normal.z],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn keep_section_positive(
|
fn keep_section_positive(
|
||||||
body: &cadkernel::brep::Body,
|
body: &cadkernel::brep::Body,
|
||||||
plane: cadkernel::space::Plane,
|
plane: cadkernel::space::Plane,
|
||||||
@@ -6906,6 +6953,7 @@ impl Scene {
|
|||||||
annotation_scale_handle,
|
annotation_scale_handle,
|
||||||
all_visible,
|
all_visible,
|
||||||
viewport,
|
viewport,
|
||||||
|
live_section.as_ref(),
|
||||||
));
|
));
|
||||||
all
|
all
|
||||||
}
|
}
|
||||||
@@ -6913,7 +6961,7 @@ impl Scene {
|
|||||||
fn active_live_section(
|
fn active_live_section(
|
||||||
&self,
|
&self,
|
||||||
target_block: Handle,
|
target_block: Handle,
|
||||||
) -> Option<acadrust::entities::SectionObjectData> {
|
) -> Option<LiveSection> {
|
||||||
self.document
|
self.document
|
||||||
.entities()
|
.entities()
|
||||||
.filter_map(|entity| {
|
.filter_map(|entity| {
|
||||||
@@ -6927,7 +6975,14 @@ impl Scene {
|
|||||||
(data.flags & 1 != 0
|
(data.flags & 1 != 0
|
||||||
&& !extended.common.invisible
|
&& !extended.common.invisible
|
||||||
&& (owner.is_null() || owner == target_block))
|
&& (owner.is_null() || owner == target_block))
|
||||||
.then(|| data.clone())
|
.then(|| LiveSection {
|
||||||
|
data: data.clone(),
|
||||||
|
slice_depth: crate::entities::extended::section_is_slice(extended)
|
||||||
|
.then(|| {
|
||||||
|
crate::entities::extended::section_slice_depth(extended)
|
||||||
|
.unwrap_or(0.0)
|
||||||
|
}),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.last()
|
.last()
|
||||||
}
|
}
|
||||||
@@ -6935,43 +6990,74 @@ impl Scene {
|
|||||||
fn sectioned_body(
|
fn sectioned_body(
|
||||||
&self,
|
&self,
|
||||||
handle: Handle,
|
handle: Handle,
|
||||||
|
section: &LiveSection,
|
||||||
|
) -> Result<Option<cadkernel::brep::Body>, ()> {
|
||||||
|
let body = self.section_source_body(handle).ok_or(())?;
|
||||||
|
Self::section_body(&body, §ion.data, section.slice_depth, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn section_source_body(
|
||||||
|
&self,
|
||||||
|
handle: Handle,
|
||||||
|
) -> Option<std::borrow::Cow<'_, cadkernel::brep::Body>> {
|
||||||
|
if let Some(body) = self.solid_models.get(&handle) {
|
||||||
|
return Some(std::borrow::Cow::Borrowed(body));
|
||||||
|
}
|
||||||
|
let body = match self.document.get_entity(handle) {
|
||||||
|
Some(EntityType::Solid3D(solid)) => {
|
||||||
|
crate::scene::convert::solid3d_tess::kernel_body(solid)
|
||||||
|
}
|
||||||
|
Some(EntityType::Region(region)) => {
|
||||||
|
crate::scene::convert::solid3d_tess::kernel_region_body(region)
|
||||||
|
}
|
||||||
|
Some(EntityType::Body(body)) => {
|
||||||
|
crate::scene::convert::solid3d_tess::kernel_acis_body(&body.acis_data)
|
||||||
|
}
|
||||||
|
Some(EntityType::Surface(surface)) => {
|
||||||
|
crate::scene::convert::solid3d_tess::kernel_surface_body(surface)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}?;
|
||||||
|
Some(std::borrow::Cow::Owned(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn section_body(
|
||||||
|
body: &cadkernel::brep::Body,
|
||||||
data: &acadrust::entities::SectionObjectData,
|
data: &acadrust::entities::SectionObjectData,
|
||||||
|
slice_depth: Option<f64>,
|
||||||
|
body_to_world: Option<&acadrust::types::Transform>,
|
||||||
) -> Result<Option<cadkernel::brep::Body>, ()> {
|
) -> Result<Option<cadkernel::brep::Body>, ()> {
|
||||||
let body = self.solid_models.get(&handle).ok_or(())?;
|
|
||||||
let Some((origin, tangent, vertical, viewing)) = section_frame(data) else {
|
let Some((origin, tangent, vertical, viewing)) = section_frame(data) else {
|
||||||
return Err(());
|
return Err(());
|
||||||
};
|
};
|
||||||
let front = cadkernel::space::Plane::orthonormal(
|
let front = section_plane(origin, tangent, viewing, body_to_world).ok_or(())?;
|
||||||
origin.to_array(),
|
|
||||||
tangent.to_array(),
|
|
||||||
viewing.to_array(),
|
|
||||||
)
|
|
||||||
.ok_or(())?;
|
|
||||||
let mut clipped = keep_section_positive(body, front)?;
|
let mut clipped = keep_section_positive(body, front)?;
|
||||||
let Some(mut current) = clipped.take() else {
|
let Some(mut current) = clipped.take() else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
if data.state == 1 {
|
if data.state == 1 && slice_depth.is_none() {
|
||||||
return Ok(Some(current));
|
return Ok(Some(current));
|
||||||
}
|
}
|
||||||
|
|
||||||
let depth = data
|
let depth = slice_depth.unwrap_or_else(|| {
|
||||||
.vertices
|
data.vertices
|
||||||
.first()
|
.first()
|
||||||
.zip(data.back_line_vertices.first())
|
.zip(data.back_line_vertices.first())
|
||||||
.map_or(0.0, |(front, back)| {
|
.map_or(0.0, |(front, back)| {
|
||||||
(glam::DVec3::new(back.x, back.y, back.z)
|
(glam::DVec3::new(back.x, back.y, back.z)
|
||||||
- glam::DVec3::new(front.x, front.y, front.z))
|
- glam::DVec3::new(front.x, front.y, front.z))
|
||||||
.dot(viewing)
|
.dot(viewing)
|
||||||
.abs()
|
.abs()
|
||||||
});
|
})
|
||||||
|
});
|
||||||
if depth <= 1e-12 {
|
if depth <= 1e-12 {
|
||||||
return Ok(Some(current));
|
return Ok(Some(current));
|
||||||
}
|
}
|
||||||
let back = cadkernel::space::Plane::orthonormal(
|
let back = section_plane(
|
||||||
(origin + viewing * depth).to_array(),
|
origin + viewing * depth,
|
||||||
tangent.to_array(),
|
tangent,
|
||||||
(-viewing).to_array(),
|
-viewing,
|
||||||
|
body_to_world,
|
||||||
)
|
)
|
||||||
.ok_or(())?;
|
.ok_or(())?;
|
||||||
let Some(next) = keep_section_positive(¤t, back)? else {
|
let Some(next) = keep_section_positive(¤t, back)? else {
|
||||||
@@ -6979,6 +7065,10 @@ impl Scene {
|
|||||||
};
|
};
|
||||||
current = next;
|
current = next;
|
||||||
|
|
||||||
|
if data.state == 1 {
|
||||||
|
return Ok(Some(current));
|
||||||
|
}
|
||||||
|
|
||||||
let span = data
|
let span = data
|
||||||
.vertices
|
.vertices
|
||||||
.first()
|
.first()
|
||||||
@@ -6988,25 +7078,41 @@ impl Scene {
|
|||||||
- glam::DVec3::new(first.x, first.y, first.z))
|
- glam::DVec3::new(first.x, first.y, first.z))
|
||||||
.length()
|
.length()
|
||||||
});
|
});
|
||||||
let bounded = data.state == 4 || (data.state == 2 && depth >= span * 0.25);
|
if span <= 1e-12 {
|
||||||
if !bounded || span <= 1e-12 {
|
|
||||||
return Ok(Some(current));
|
return Ok(Some(current));
|
||||||
}
|
}
|
||||||
|
|
||||||
let first = data.vertices.first().map(|point| glam::DVec3::new(point.x, point.y, point.z)).ok_or(())?;
|
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(())?;
|
let last = data.vertices.last().map(|point| glam::DVec3::new(point.x, point.y, point.z)).ok_or(())?;
|
||||||
for plane in [
|
for plane in [
|
||||||
cadkernel::space::Plane::orthonormal(first.to_array(), vertical.to_array(), tangent.to_array()),
|
section_plane(first, vertical, tangent, body_to_world),
|
||||||
cadkernel::space::Plane::orthonormal(last.to_array(), vertical.to_array(), (-tangent).to_array()),
|
section_plane(last, vertical, -tangent, body_to_world),
|
||||||
cadkernel::space::Plane::orthonormal(
|
] {
|
||||||
(origin - vertical * data.bottom_height).to_array(),
|
let Some(plane) = plane else {
|
||||||
tangent.to_array(),
|
return Err(());
|
||||||
vertical.to_array(),
|
};
|
||||||
|
let Some(next) = keep_section_positive(¤t, plane)? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.state != 4 {
|
||||||
|
return Ok(Some(current));
|
||||||
|
}
|
||||||
|
|
||||||
|
for plane in [
|
||||||
|
section_plane(
|
||||||
|
origin - vertical * data.bottom_height,
|
||||||
|
tangent,
|
||||||
|
vertical,
|
||||||
|
body_to_world,
|
||||||
),
|
),
|
||||||
cadkernel::space::Plane::orthonormal(
|
section_plane(
|
||||||
(origin + vertical * data.top_height).to_array(),
|
origin + vertical * data.top_height,
|
||||||
tangent.to_array(),
|
tangent,
|
||||||
(-vertical).to_array(),
|
-vertical,
|
||||||
|
body_to_world,
|
||||||
),
|
),
|
||||||
] {
|
] {
|
||||||
let Some(plane) = plane else {
|
let Some(plane) = plane else {
|
||||||
@@ -7299,6 +7405,7 @@ impl Scene {
|
|||||||
annotation_scale_handle: Option<Handle>,
|
annotation_scale_handle: Option<Handle>,
|
||||||
all_visible: bool,
|
all_visible: bool,
|
||||||
viewport: Option<Handle>,
|
viewport: Option<Handle>,
|
||||||
|
live_section: Option<&LiveSection>,
|
||||||
) -> Vec<MeshLodSet> {
|
) -> Vec<MeshLodSet> {
|
||||||
if self.block_meshes.is_empty() {
|
if self.block_meshes.is_empty() {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
@@ -7325,9 +7432,37 @@ impl Scene {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let handle = entity.common().handle;
|
let handle = entity.common().handle;
|
||||||
let Some(set) = self.block_meshes.get(&handle) else {
|
let Some(original) = self.block_meshes.get(&handle) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let sectioned;
|
||||||
|
let set = if let Some((section, body)) = live_section.and_then(|section| {
|
||||||
|
self.section_source_body(handle)
|
||||||
|
.map(|body| (section, body))
|
||||||
|
}) {
|
||||||
|
match Self::section_body(
|
||||||
|
&body,
|
||||||
|
§ion.data,
|
||||||
|
section.slice_depth,
|
||||||
|
Some(&context.transform),
|
||||||
|
) {
|
||||||
|
Ok(Some(body)) => {
|
||||||
|
if let Some((mut mesh, _, _)) =
|
||||||
|
self.prepare_solid_model_display(handle, &body)
|
||||||
|
{
|
||||||
|
mesh.prepare_instance_source(handle);
|
||||||
|
sectioned = mesh;
|
||||||
|
§ioned
|
||||||
|
} else {
|
||||||
|
original
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => return,
|
||||||
|
Err(()) => original,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
original
|
||||||
|
};
|
||||||
let inherit = self.mesh_inherit_for_path(&context.insert_path);
|
let inherit = self.mesh_inherit_for_path(&context.insert_path);
|
||||||
let own_alpha = set.display_color().map_or(1.0, |color| color[3]);
|
let own_alpha = set.display_color().map_or(1.0, |color| color[3]);
|
||||||
let mut transformed =
|
let mut transformed =
|
||||||
@@ -10698,6 +10833,169 @@ impl Default for Scene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod section_tests {
|
||||||
|
use super::*;
|
||||||
|
use acadrust::entities::{
|
||||||
|
EntityCommon, ExtendedEntity, ExtendedEntityData, SectionObjectData, Solid3D,
|
||||||
|
};
|
||||||
|
use acadrust::objects::ClassObjectData;
|
||||||
|
use acadrust::types::{Color, Vector3};
|
||||||
|
|
||||||
|
fn section(state: i32, depth: f64) -> SectionObjectData {
|
||||||
|
let vertices = vec![Vector3::new(5.0, 2.0, 5.0), Vector3::new(5.0, 8.0, 5.0)];
|
||||||
|
SectionObjectData {
|
||||||
|
state,
|
||||||
|
flags: 5,
|
||||||
|
name: "Section Plane (1)".to_string(),
|
||||||
|
vertical_direction: Vector3::UNIT_Z,
|
||||||
|
top_height: 1.0,
|
||||||
|
bottom_height: 2.0,
|
||||||
|
indicator_alpha: 70,
|
||||||
|
indicator_color: Color::from_index(9),
|
||||||
|
back_line_vertices: (depth > 0.0)
|
||||||
|
.then(|| {
|
||||||
|
vertices
|
||||||
|
.iter()
|
||||||
|
.map(|point| *point + Vector3::new(-depth, 0.0, 0.0))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
|
vertices,
|
||||||
|
settings_handle: Handle::NULL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bounds(body: Option<cadkernel::brep::Body>) -> ([f64; 3], [f64; 3]) {
|
||||||
|
let body = body.expect("section should retain material");
|
||||||
|
assert!(body.validate().is_empty());
|
||||||
|
let bounds = cadkernel::brep::body_bounds(&body).expect("sectioned body should be bounded");
|
||||||
|
(bounds.min, bounds.max)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_near(actual: [f64; 3], expected: [f64; 3]) {
|
||||||
|
for (actual, expected) in actual.into_iter().zip(expected) {
|
||||||
|
assert!((actual - expected).abs() < 1e-8, "{actual} != {expected}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn kernel_clipping_obeys_plane_slice_boundary_and_volume_limits() {
|
||||||
|
let body = cadkernel::brep::make::cuboid([0.0; 3], [10.0; 3]).unwrap();
|
||||||
|
|
||||||
|
let (min, max) = bounds(Scene::section_body(&body, §ion(1, 0.0), None, None).unwrap());
|
||||||
|
assert_near(min, [0.0, 0.0, 0.0]);
|
||||||
|
assert_near(max, [5.0, 10.0, 10.0]);
|
||||||
|
|
||||||
|
let (min, max) = bounds(
|
||||||
|
Scene::section_body(&body, §ion(1, 0.0), Some(2.0), None).unwrap(),
|
||||||
|
);
|
||||||
|
assert_near(min, [3.0, 0.0, 0.0]);
|
||||||
|
assert_near(max, [5.0, 10.0, 10.0]);
|
||||||
|
|
||||||
|
let (min, max) = bounds(Scene::section_body(&body, §ion(2, 4.0), None, None).unwrap());
|
||||||
|
assert_near(min, [1.0, 2.0, 0.0]);
|
||||||
|
assert_near(max, [5.0, 8.0, 10.0]);
|
||||||
|
|
||||||
|
let (min, max) = bounds(Scene::section_body(&body, §ion(4, 4.0), None, None).unwrap());
|
||||||
|
assert_near(min, [1.0, 2.0, 3.0]);
|
||||||
|
assert_near(max, [5.0, 8.0, 6.0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn instance_transform_pulls_the_section_plane_into_body_space() {
|
||||||
|
let body = cadkernel::brep::make::cuboid([0.0; 3], [10.0; 3]).unwrap();
|
||||||
|
let transform = acadrust::types::Transform::from_scaling(
|
||||||
|
acadrust::types::Vector3::new(2.0, 1.0, 1.0),
|
||||||
|
)
|
||||||
|
.then(&acadrust::types::Transform::from_translation(
|
||||||
|
acadrust::types::Vector3::new(10.0, 0.0, 0.0),
|
||||||
|
));
|
||||||
|
let mut data = section(1, 0.0);
|
||||||
|
for point in &mut data.vertices {
|
||||||
|
point.x = 20.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (min, max) = bounds(
|
||||||
|
Scene::section_body(&body, &data, None, Some(&transform)).unwrap(),
|
||||||
|
);
|
||||||
|
assert_near(min, [0.0, 0.0, 0.0]);
|
||||||
|
assert_near(max, [5.0, 10.0, 10.0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn adding_and_erasing_a_section_keeps_its_object_graph_consistent() {
|
||||||
|
let mut scene = Scene::new();
|
||||||
|
let handle = scene.add_entity(EntityType::Extended(ExtendedEntity {
|
||||||
|
common: EntityCommon::new(),
|
||||||
|
data: ExtendedEntityData::SectionObject(section(1, 0.0)),
|
||||||
|
}));
|
||||||
|
let settings_handle = match scene.document.get_entity(handle).unwrap() {
|
||||||
|
EntityType::Extended(ExtendedEntity {
|
||||||
|
data: ExtendedEntityData::SectionObject(data),
|
||||||
|
..
|
||||||
|
}) => data.settings_handle,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
assert!(matches!(
|
||||||
|
scene.document.objects.get(&settings_handle),
|
||||||
|
Some(ObjectType::ClassObject(object))
|
||||||
|
if object.owner == handle
|
||||||
|
&& matches!(&object.data, ClassObjectData::SectionSettings(_))
|
||||||
|
));
|
||||||
|
|
||||||
|
let root = scene.document.header.named_objects_dict_handle;
|
||||||
|
let manager_handle = match scene.document.objects.get(&root).unwrap() {
|
||||||
|
ObjectType::Dictionary(dictionary) => dictionary
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.find(|(name, _)| name == "ACAD_SECTION_MANAGER")
|
||||||
|
.map(|(_, handle)| *handle)
|
||||||
|
.unwrap(),
|
||||||
|
_ => panic!("root must be a dictionary"),
|
||||||
|
};
|
||||||
|
assert!(matches!(
|
||||||
|
scene.document.objects.get(&manager_handle),
|
||||||
|
Some(ObjectType::ClassObject(object))
|
||||||
|
if matches!(&object.data, ClassObjectData::SectionManager(manager)
|
||||||
|
if manager.sections == vec![handle])
|
||||||
|
));
|
||||||
|
|
||||||
|
scene.erase_entities(&[handle]);
|
||||||
|
assert!(!scene.document.objects.contains_key(&settings_handle));
|
||||||
|
assert!(matches!(
|
||||||
|
scene.document.objects.get(&manager_handle),
|
||||||
|
Some(ObjectType::ClassObject(object))
|
||||||
|
if matches!(&object.data, ClassObjectData::SectionManager(manager)
|
||||||
|
if manager.sections.is_empty())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn live_section_rebuilds_an_uncached_persisted_solid() {
|
||||||
|
let body = cadkernel::brep::make::cuboid([0.0; 3], [10.0; 3]).unwrap();
|
||||||
|
let sat = crate::scene::convert::acis_export::solid_to_sat(&body).unwrap();
|
||||||
|
let mut solid = Solid3D::new();
|
||||||
|
solid.set_sat_document(&sat);
|
||||||
|
let mut scene = Scene::new();
|
||||||
|
let handle = scene.add_entity(EntityType::Solid3D(solid));
|
||||||
|
assert!(!scene.solid_models.contains_key(&handle));
|
||||||
|
|
||||||
|
let clipped = scene
|
||||||
|
.sectioned_body(
|
||||||
|
handle,
|
||||||
|
&LiveSection {
|
||||||
|
data: section(1, 0.0),
|
||||||
|
slice_depth: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let (min, max) = bounds(clipped);
|
||||||
|
assert_near(min, [0.0, 0.0, 0.0]);
|
||||||
|
assert_near(max, [5.0, 10.0, 10.0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod journal_tests {
|
mod journal_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -938,12 +938,57 @@ impl Scene {
|
|||||||
if respect_layer_locks && self.is_layer_locked(h) {
|
if respect_layer_locks && self.is_layer_locked(h) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let settings_handle = self.document.get_entity(h).and_then(|entity| match entity {
|
||||||
|
EntityType::Extended(acadrust::entities::ExtendedEntity {
|
||||||
|
data: acadrust::entities::ExtendedEntityData::SectionObject(data),
|
||||||
|
..
|
||||||
|
}) if !data.settings_handle.is_null() => Some(data.settings_handle),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
let section_managers: Vec<Handle> = self
|
||||||
|
.document
|
||||||
|
.objects
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(handle, object)| match object {
|
||||||
|
ObjectType::ClassObject(object) => match &object.data {
|
||||||
|
acadrust::objects::ClassObjectData::SectionManager(manager)
|
||||||
|
if manager.sections.contains(&h) =>
|
||||||
|
{
|
||||||
|
Some(*handle)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
// Delta-undo: capture the removed entity so an undo can re-insert it.
|
// Delta-undo: capture the removed entity so an undo can re-insert it.
|
||||||
if self.is_recording_undo() {
|
if self.is_recording_undo() {
|
||||||
let before = self.document.get_entity_arc(h);
|
let before = self.document.get_entity_arc(h);
|
||||||
self.record_undo_before(h, before);
|
self.record_undo_before(h, before);
|
||||||
|
if let Some(settings_handle) = settings_handle {
|
||||||
|
let before = self.document.objects.get(&settings_handle).cloned();
|
||||||
|
self.record_undo_object_before(settings_handle, before);
|
||||||
|
}
|
||||||
|
for &manager_handle in §ion_managers {
|
||||||
|
let before = self.document.objects.get(&manager_handle).cloned();
|
||||||
|
self.record_undo_object_before(manager_handle, before);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.delete_solid_history(h);
|
self.delete_solid_history(h);
|
||||||
|
if let Some(settings_handle) = settings_handle {
|
||||||
|
self.document.objects.remove(&settings_handle);
|
||||||
|
}
|
||||||
|
for manager_handle in section_managers {
|
||||||
|
if let Some(ObjectType::ClassObject(object)) =
|
||||||
|
self.document.objects.get_mut(&manager_handle)
|
||||||
|
{
|
||||||
|
if let acadrust::objects::ClassObjectData::SectionManager(manager) =
|
||||||
|
&mut object.data
|
||||||
|
{
|
||||||
|
manager.sections.retain(|section| *section != h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
self.remember_removed_cache_categories(h);
|
self.remember_removed_cache_categories(h);
|
||||||
self.document.remove_entity_arc(h);
|
self.document.remove_entity_arc(h);
|
||||||
selection_changed |= self.selected.remove(&h);
|
selection_changed |= self.selected.remove(&h);
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
use acadrust::entities::{EntityCommon, ExtendedEntity, ExtendedEntityData, SectionObjectData};
|
||||||
|
use acadrust::objects::{ClassObjectData, ObjectType};
|
||||||
|
use acadrust::types::{Color, Handle, Vector3};
|
||||||
|
use acadrust::xdata::{ExtendedDataRecord, XDataValue};
|
||||||
|
use acadrust::EntityType;
|
||||||
|
use OpenCADStudio::scene::Scene;
|
||||||
|
|
||||||
|
fn add_slice(scene: &mut Scene) -> Handle {
|
||||||
|
let mut common = EntityCommon::new();
|
||||||
|
let mut slice = ExtendedDataRecord::new("IsSlice");
|
||||||
|
slice.values = vec![XDataValue::Integer16(0), XDataValue::Integer16(1)];
|
||||||
|
common.extended_data.add_record(slice);
|
||||||
|
let mut thickness = ExtendedDataRecord::new("ThicknessDepth");
|
||||||
|
thickness.values = vec![XDataValue::Real(0.75)];
|
||||||
|
common.extended_data.add_record(thickness);
|
||||||
|
|
||||||
|
scene.add_entity(EntityType::Extended(ExtendedEntity {
|
||||||
|
common,
|
||||||
|
data: ExtendedEntityData::SectionObject(SectionObjectData {
|
||||||
|
state: 1,
|
||||||
|
flags: 5,
|
||||||
|
name: "Section Plane (1)".to_string(),
|
||||||
|
vertical_direction: Vector3::UNIT_Z,
|
||||||
|
top_height: 5.0,
|
||||||
|
bottom_height: 5.0,
|
||||||
|
indicator_alpha: 70,
|
||||||
|
indicator_color: Color::from_index(9),
|
||||||
|
vertices: vec![Vector3::ZERO, Vector3::new(10.0, 0.0, 0.0)],
|
||||||
|
back_line_vertices: Vec::new(),
|
||||||
|
settings_handle: Handle::NULL,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_section_graph(document: &acadrust::CadDocument, extension: &str) {
|
||||||
|
let (handle, section) = document
|
||||||
|
.entities()
|
||||||
|
.find_map(|entity| match entity {
|
||||||
|
EntityType::Extended(extended) => match &extended.data {
|
||||||
|
ExtendedEntityData::SectionObject(_) => Some((extended.common.handle, extended)),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| panic!("section entity should round-trip through {extension}"));
|
||||||
|
let ExtendedEntityData::SectionObject(data) = §ion.data else {
|
||||||
|
unreachable!();
|
||||||
|
};
|
||||||
|
assert_eq!(data.state, 1);
|
||||||
|
assert!(data.back_line_vertices.is_empty());
|
||||||
|
assert!(matches!(
|
||||||
|
section
|
||||||
|
.common
|
||||||
|
.extended_data
|
||||||
|
.get_record("IsSlice")
|
||||||
|
.map(|record| record.values.as_slice()),
|
||||||
|
Some([XDataValue::Integer16(0), XDataValue::Integer16(1)])
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
section
|
||||||
|
.common
|
||||||
|
.extended_data
|
||||||
|
.get_record("ThicknessDepth")
|
||||||
|
.and_then(|record| record.values.first()),
|
||||||
|
Some(XDataValue::Real(value)) if (*value - 0.75).abs() < 1e-12
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
document.objects.get(&data.settings_handle),
|
||||||
|
Some(ObjectType::ClassObject(object))
|
||||||
|
if object.owner == handle
|
||||||
|
&& matches!(&object.data, ClassObjectData::SectionSettings(_))
|
||||||
|
));
|
||||||
|
|
||||||
|
let root = document
|
||||||
|
.objects
|
||||||
|
.get(&document.header.named_objects_dict_handle)
|
||||||
|
.expect("root dictionary should round-trip");
|
||||||
|
let ObjectType::Dictionary(root) = root else {
|
||||||
|
panic!("root should be a dictionary");
|
||||||
|
};
|
||||||
|
let manager_handle = root
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.find(|(name, _)| name == "ACAD_SECTION_MANAGER")
|
||||||
|
.map(|(_, handle)| *handle)
|
||||||
|
.expect("section manager should be registered");
|
||||||
|
let Some(ObjectType::ClassObject(manager)) = document.objects.get(&manager_handle) else {
|
||||||
|
panic!("section manager handle should resolve");
|
||||||
|
};
|
||||||
|
let ClassObjectData::SectionManager(manager) = &manager.data else {
|
||||||
|
panic!("registered object should be a section manager");
|
||||||
|
};
|
||||||
|
assert_eq!(manager.sections, vec![handle]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn section_plane_graph_survives_dxf_and_dwg_roundtrips() {
|
||||||
|
let mut scene = Scene::new();
|
||||||
|
add_slice(&mut scene);
|
||||||
|
|
||||||
|
for extension in ["dxf", "dwg"] {
|
||||||
|
let bytes =
|
||||||
|
OpenCADStudio::io::save_to_bytes(&scene.document, extension, scene.document.version)
|
||||||
|
.expect("section document should save");
|
||||||
|
let document =
|
||||||
|
OpenCADStudio::io::load_bytes(&format!("section-roundtrip.{extension}"), bytes)
|
||||||
|
.expect("section document should reload");
|
||||||
|
assert_section_graph(&document, extension);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user