forked from 1b-agent/opencadstudio-upstream
The codec and kernel repositories and crates are renamed opencadcodec and opencadkernel. Inside OCS they are depended on under short keys, so code reads `codec::`, `kernel::` and the new `graph::` (opencadgraph, the node graph engine). File-format and protocol spellings stay as they were: the ACIS product id `acadrust`, the ACADRUST_* switches and the plugin version keys `acadrust_version` / `acadrust_source`. A button left of MCP on the command line opens a translucent node graph over the viewport, with a palette of object, input, math, logic, list, text and geometry nodes added by click or drag. An object node owns drawing entities and its rows are the Properties panel's own rows for them: the single-object page is extracted from refresh_properties as entity_property_sections, values use the control protocol's property_json, and writes go through set_property_value with a property target override, so the panel, MCP and the graph share one read/write path. A list reaching an object row makes one entity per element. Each graph action is one undo step; the canvas pans and zooms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2239 lines
81 KiB
Rust
2239 lines
81 KiB
Rust
use codec::entities::{
|
|
ArcAlignedTextData, ExtendedEntity, ExtendedEntityData, GeoPositionMarkerData,
|
|
PointCloudData, PointCloudExData, RemoteTextData, SectionObjectData,
|
|
};
|
|
use codec::types::{Handle, Transform, Vector3};
|
|
use codec::xdata::{ExtendedDataRecord, XDataValue};
|
|
use crate::t;
|
|
|
|
use crate::command::EntityTransform;
|
|
use crate::entities::common::{
|
|
center_grip, edit_angle_prop, edit_prop, parse_f64, ro_prop, square_grip,
|
|
};
|
|
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
|
|
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
|
|
use crate::scene::model::object::{
|
|
GripApply, GripDef, GripMenuAction, GripMenuItem, PropSection, PropValue, Property,
|
|
};
|
|
use crate::scene::model::wire_model::SnapHint;
|
|
|
|
const NAN: [f64; 3] = [f64::NAN; 3];
|
|
const SECTION_SLICE_APP: &str = "IsSlice";
|
|
const SECTION_THICKNESS_APP: &str = "ThicknessDepth";
|
|
const SECTION_GRIP_LEFT: usize = 100_000;
|
|
const SECTION_GRIP_RIGHT: usize = 100_001;
|
|
const SECTION_GRIP_TOP: usize = 100_002;
|
|
const SECTION_GRIP_BOTTOM: usize = 100_003;
|
|
const SECTION_GRIP_NORMAL: usize = 100_004;
|
|
const SECTION_GRIP_STATE: usize = 100_005;
|
|
|
|
pub(crate) fn section_is_slice(entity: &ExtendedEntity) -> bool {
|
|
entity
|
|
.common
|
|
.extended_data
|
|
.get_record(SECTION_SLICE_APP)
|
|
.is_some_and(|record| {
|
|
record
|
|
.values
|
|
.iter()
|
|
.rev()
|
|
.find_map(|value| match value {
|
|
XDataValue::Integer16(value) => Some(*value != 0),
|
|
XDataValue::Integer32(value) => Some(*value != 0),
|
|
_ => None,
|
|
})
|
|
.unwrap_or(false)
|
|
})
|
|
}
|
|
|
|
pub(crate) fn section_slice_depth(entity: &ExtendedEntity) -> Option<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 {
|
|
format!("{:.6}, {:.6}, {:.6}", value.x, value.y, value.z)
|
|
}
|
|
|
|
fn handle_text(handle: Handle) -> String {
|
|
if handle.is_null() {
|
|
"None".to_string()
|
|
} else {
|
|
format!("{:X}", handle.value())
|
|
}
|
|
}
|
|
|
|
fn text_prop(label: &str, field: &'static str, value: &str) -> Property {
|
|
Property {
|
|
label: label.into(),
|
|
field,
|
|
value: PropValue::PlainText(value.to_string()),
|
|
}
|
|
}
|
|
|
|
fn bool_prop(label: &str, field: &'static str, value: bool) -> Property {
|
|
Property {
|
|
label: label.into(),
|
|
field,
|
|
value: PropValue::BoolToggle { field, value },
|
|
}
|
|
}
|
|
|
|
fn choice_prop(
|
|
label: &str,
|
|
field: &'static str,
|
|
selected: &str,
|
|
options: &[&str],
|
|
) -> Property {
|
|
Property {
|
|
label: label.into(),
|
|
field,
|
|
value: PropValue::Choice {
|
|
selected: selected.to_string(),
|
|
options: options.iter().map(|option| (*option).to_string()).collect(),
|
|
},
|
|
}
|
|
}
|
|
|
|
fn push_segment(points: &mut Vec<[f64; 3]>, first: [f64; 3], second: [f64; 3]) {
|
|
if !points.is_empty() {
|
|
points.push(NAN);
|
|
}
|
|
points.push(first);
|
|
points.push(second);
|
|
}
|
|
|
|
fn push_chain(points: &mut Vec<[f64; 3]>, chain: impl IntoIterator<Item = [f64; 3]>) {
|
|
let chain: Vec<_> = chain.into_iter().collect();
|
|
if chain.len() < 2 {
|
|
return;
|
|
}
|
|
if !points.is_empty() {
|
|
points.push(NAN);
|
|
}
|
|
points.extend(chain);
|
|
}
|
|
|
|
fn push_box(points: &mut Vec<[f64; 3]>, min: Vector3, max: Vector3) {
|
|
let corners = [
|
|
[min.x, min.y, min.z],
|
|
[max.x, min.y, min.z],
|
|
[max.x, max.y, min.z],
|
|
[min.x, max.y, min.z],
|
|
[min.x, min.y, max.z],
|
|
[max.x, min.y, max.z],
|
|
[max.x, max.y, max.z],
|
|
[min.x, max.y, max.z],
|
|
];
|
|
for (a, b) in [
|
|
(0, 1),
|
|
(1, 2),
|
|
(2, 3),
|
|
(3, 0),
|
|
(4, 5),
|
|
(5, 6),
|
|
(6, 7),
|
|
(7, 4),
|
|
(0, 4),
|
|
(1, 5),
|
|
(2, 6),
|
|
(3, 7),
|
|
] {
|
|
push_segment(points, corners[a], corners[b]);
|
|
}
|
|
}
|
|
|
|
fn normalized(value: Vector3, fallback: Vector3) -> Vector3 {
|
|
let length = value.length();
|
|
if length > 1e-12 {
|
|
value / length
|
|
} else {
|
|
fallback
|
|
}
|
|
}
|
|
|
|
fn plane_axes(normal: Vector3) -> (Vector3, Vector3, Vector3) {
|
|
let normal = normalized(normal, Vector3::UNIT_Z);
|
|
let (x, y) =
|
|
crate::scene::view::transform::ocs_axes((normal.x, normal.y, normal.z));
|
|
(
|
|
Vector3::new(x.0, x.1, x.2),
|
|
Vector3::new(y.0, y.1, y.2),
|
|
normal,
|
|
)
|
|
}
|
|
|
|
fn add_scaled(origin: Vector3, x: Vector3, sx: f64, y: Vector3, sy: f64) -> [f64; 3] {
|
|
[
|
|
origin.x + x.x * sx + y.x * sy,
|
|
origin.y + x.y * sx + y.y * sy,
|
|
origin.z + x.z * sx + y.z * sy,
|
|
]
|
|
}
|
|
|
|
fn append_planar_text(
|
|
points: &mut Vec<[f64; 3]>,
|
|
text: &str,
|
|
font: &str,
|
|
origin: Vector3,
|
|
normal: Vector3,
|
|
rotation: f64,
|
|
height: f64,
|
|
width_factor: f64,
|
|
) {
|
|
if text.is_empty() || height.abs() < 1e-12 {
|
|
return;
|
|
}
|
|
let (axis_x, axis_y, _) = plane_axes(normal);
|
|
let (cos, sin) = rotation.sin_cos();
|
|
let baseline = axis_x * cos + axis_y * sin;
|
|
let upward = axis_y * cos - axis_x * sin;
|
|
let (strokes, _) = crate::scene::text::lff::tessellate_text_ex(
|
|
[0.0, 0.0],
|
|
height.abs() as f32,
|
|
0.0,
|
|
width_factor as f32,
|
|
0.0,
|
|
if font.trim().is_empty() { "standard" } else { font },
|
|
text,
|
|
);
|
|
for stroke in strokes {
|
|
push_chain(
|
|
points,
|
|
stroke
|
|
.into_iter()
|
|
.map(|[x, y]| add_scaled(origin, baseline, x as f64, upward, y as f64)),
|
|
);
|
|
}
|
|
}
|
|
|
|
fn append_arc_aligned_text(points: &mut Vec<[f64; 3]>, data: &ArcAlignedTextData) {
|
|
if data.text.is_empty() || data.radius.abs() < 1e-12 || data.text_size.abs() < 1e-12 {
|
|
return;
|
|
}
|
|
let (axis_x, axis_y, _) = plane_axes(data.normal);
|
|
let direction = if data.reverse || data.text_direction < 0 {
|
|
-1.0
|
|
} else {
|
|
1.0
|
|
};
|
|
let mut angle = if direction > 0.0 {
|
|
data.start_angle
|
|
} else {
|
|
data.end_angle
|
|
};
|
|
let advance = data.text_size.abs()
|
|
* data.x_scale.abs().max(0.01)
|
|
* data.character_spacing.abs().max(0.01)
|
|
* 0.72;
|
|
let delta = direction * advance / data.radius.abs().max(1e-9);
|
|
let radius = data.radius + data.offset_from_arc;
|
|
let font = if data.font_name.trim().is_empty() {
|
|
data.style_name.as_str()
|
|
} else {
|
|
data.font_name.as_str()
|
|
};
|
|
for character in data.text.chars() {
|
|
let radial = axis_x * angle.cos() + axis_y * angle.sin();
|
|
let tangent = (axis_y * angle.cos() - axis_x * angle.sin()) * direction;
|
|
let origin = data.center + radial * radius;
|
|
let (strokes, _) = crate::scene::text::lff::tessellate_text_ex(
|
|
[0.0, 0.0],
|
|
data.text_size.abs() as f32,
|
|
0.0,
|
|
data.x_scale.abs().max(0.01) as f32,
|
|
0.0,
|
|
if font.trim().is_empty() { "standard" } else { font },
|
|
&character.to_string(),
|
|
);
|
|
for stroke in strokes {
|
|
push_chain(
|
|
points,
|
|
stroke.into_iter().map(|[x, y]| {
|
|
add_scaled(origin, tangent, x as f64, radial, y as f64)
|
|
}),
|
|
);
|
|
}
|
|
angle += delta;
|
|
}
|
|
}
|
|
|
|
fn section_lines(data: &SectionObjectData) -> Vec<[f64; 3]> {
|
|
let mut points = Vec::new();
|
|
let vertical = normalized(data.vertical_direction, Vector3::UNIT_Z);
|
|
let offset = |point: &Vector3, distance: f64| {
|
|
[
|
|
point.x + vertical.x * distance,
|
|
point.y + vertical.y * distance,
|
|
point.z + vertical.z * distance,
|
|
]
|
|
};
|
|
push_chain(
|
|
&mut points,
|
|
data.vertices.iter().map(|p| [p.x, p.y, p.z]),
|
|
);
|
|
push_chain(
|
|
&mut points,
|
|
data.vertices.iter().map(|point| offset(point, data.top_height)),
|
|
);
|
|
push_chain(
|
|
&mut points,
|
|
data.vertices.iter().map(|point| offset(point, -data.bottom_height)),
|
|
);
|
|
for point in &data.vertices {
|
|
push_segment(
|
|
&mut points,
|
|
offset(point, -data.bottom_height),
|
|
offset(point, data.top_height),
|
|
);
|
|
}
|
|
push_chain(
|
|
&mut points,
|
|
data.back_line_vertices.iter().map(|p| [p.x, p.y, p.z]),
|
|
);
|
|
if !data.back_line_vertices.is_empty() {
|
|
push_chain(
|
|
&mut points,
|
|
data.back_line_vertices
|
|
.iter()
|
|
.map(|point| offset(point, data.top_height)),
|
|
);
|
|
push_chain(
|
|
&mut points,
|
|
data.back_line_vertices
|
|
.iter()
|
|
.map(|point| offset(point, -data.bottom_height)),
|
|
);
|
|
for point in &data.back_line_vertices {
|
|
push_segment(
|
|
&mut points,
|
|
offset(point, -data.bottom_height),
|
|
offset(point, data.top_height),
|
|
);
|
|
}
|
|
if let (Some(front), Some(back)) = (
|
|
data.vertices.first().zip(data.vertices.last()),
|
|
data.back_line_vertices
|
|
.first()
|
|
.zip(data.back_line_vertices.last()),
|
|
) {
|
|
for (a, b) in [(front.0, back.0), (front.1, back.1)] {
|
|
push_segment(&mut points, offset(a, data.top_height), offset(b, data.top_height));
|
|
push_segment(
|
|
&mut points,
|
|
offset(a, -data.bottom_height),
|
|
offset(b, -data.bottom_height),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
points
|
|
}
|
|
|
|
fn remote_text_lines(data: &RemoteTextData) -> Vec<[f64; 3]> {
|
|
let mut points = Vec::new();
|
|
append_planar_text(
|
|
&mut points,
|
|
&data.text,
|
|
&data.style_name,
|
|
data.position,
|
|
data.normal,
|
|
data.rotation,
|
|
data.height,
|
|
1.0,
|
|
);
|
|
points
|
|
}
|
|
|
|
fn geo_marker_lines(data: &GeoPositionMarkerData) -> Vec<[f64; 3]> {
|
|
let mut points = Vec::new();
|
|
let radius = data.radius.abs().max(1e-6);
|
|
let count = 32;
|
|
push_chain(
|
|
&mut points,
|
|
(0..=count).map(|index| {
|
|
let angle = std::f64::consts::TAU * index as f64 / count as f64;
|
|
[
|
|
data.position.x + radius * angle.cos(),
|
|
data.position.y + radius * angle.sin(),
|
|
data.position.z,
|
|
]
|
|
}),
|
|
);
|
|
push_segment(
|
|
&mut points,
|
|
[data.position.x - radius, data.position.y, data.position.z],
|
|
[data.position.x + radius, data.position.y, data.position.z],
|
|
);
|
|
push_segment(
|
|
&mut points,
|
|
[data.position.x, data.position.y - radius, data.position.z],
|
|
[data.position.x, data.position.y + radius, data.position.z],
|
|
);
|
|
let label = data
|
|
.embedded_mtext
|
|
.as_ref()
|
|
.filter(|_| data.mtext_visible)
|
|
.map(|text| text.value.as_str())
|
|
.filter(|value| !value.trim().is_empty())
|
|
.unwrap_or(data.notes.as_str());
|
|
if !label.trim().is_empty() {
|
|
let origin = Vector3::new(
|
|
data.position.x + radius + data.landing_gap.max(0.0),
|
|
data.position.y,
|
|
data.position.z,
|
|
);
|
|
push_segment(
|
|
&mut points,
|
|
[
|
|
data.position.x + radius,
|
|
data.position.y,
|
|
data.position.z,
|
|
],
|
|
[origin.x, origin.y, origin.z],
|
|
);
|
|
append_planar_text(
|
|
&mut points,
|
|
label,
|
|
"standard",
|
|
origin,
|
|
Vector3::UNIT_Z,
|
|
0.0,
|
|
radius * 0.8,
|
|
1.0,
|
|
);
|
|
}
|
|
points
|
|
}
|
|
|
|
fn point_cloud_lines(data: &PointCloudData) -> Vec<[f64; 3]> {
|
|
let mut points = Vec::new();
|
|
push_box(&mut points, data.extents_min, data.extents_max);
|
|
points
|
|
}
|
|
|
|
fn point_cloud_clip_lines(data: &PointCloudData) -> Vec<[f64; 3]> {
|
|
let mut points = Vec::new();
|
|
if data.show_clipping {
|
|
for clip in &data.clippings {
|
|
if clip.vertices.len() < 2 {
|
|
continue;
|
|
}
|
|
for z in [clip.z_min, clip.z_max] {
|
|
let mut chain: Vec<[f64; 3]> = clip
|
|
.vertices
|
|
.iter()
|
|
.map(|point| [point.x, point.y, z])
|
|
.collect();
|
|
chain.push(chain[0]);
|
|
push_chain(&mut points, chain);
|
|
}
|
|
}
|
|
}
|
|
points
|
|
}
|
|
|
|
fn point_cloud_ex_lines(data: &PointCloudExData) -> Vec<[f64; 3]> {
|
|
let mut points = Vec::new();
|
|
push_box(&mut points, data.extents_min, data.extents_max);
|
|
points
|
|
}
|
|
|
|
fn point_cloud_ex_clip_lines(data: &PointCloudExData) -> Vec<[f64; 3]> {
|
|
let mut points = Vec::new();
|
|
if data.show_cropping {
|
|
for crop in &data.croppings {
|
|
if crop.points.len() < 2 {
|
|
continue;
|
|
}
|
|
let mut chain: Vec<[f64; 3]> =
|
|
crop.points.iter().map(|p| [p.x, p.y, p.z]).collect();
|
|
if crop.points.len() > 2 {
|
|
chain.push(chain[0]);
|
|
}
|
|
push_chain(&mut points, chain);
|
|
}
|
|
}
|
|
points
|
|
}
|
|
|
|
pub(crate) fn point_cloud_frame_lines(entity: &ExtendedEntity) -> Option<Vec<[f64; 3]>> {
|
|
match &entity.data {
|
|
ExtendedEntityData::PointCloud(data) => Some(point_cloud_clip_lines(data)),
|
|
ExtendedEntityData::PointCloudEx(data) => Some(point_cloud_ex_clip_lines(data)),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn camera_lines(document: &codec::CadDocument, view_handle: Handle) -> Vec<[f64; 3]> {
|
|
let Some(view) = document.views.iter().find(|view| view.handle == view_handle) else {
|
|
return Vec::new();
|
|
};
|
|
let direction = normalized(view.direction, Vector3::UNIT_Z);
|
|
let distance = view.direction.length().max(view.height.abs()).max(1.0);
|
|
let eye = view.target + direction * distance;
|
|
let (right, up, _) = plane_axes(direction);
|
|
let half_width = view.width.abs().max(view.height.abs()) * 0.08;
|
|
let half_height = view.height.abs().max(view.width.abs()) * 0.06;
|
|
let corners = [
|
|
view.target + right * half_width + up * half_height,
|
|
view.target - right * half_width + up * half_height,
|
|
view.target - right * half_width - up * half_height,
|
|
view.target + right * half_width - up * half_height,
|
|
];
|
|
let mut points = Vec::new();
|
|
for corner in corners {
|
|
push_segment(
|
|
&mut points,
|
|
[eye.x, eye.y, eye.z],
|
|
[corner.x, corner.y, corner.z],
|
|
);
|
|
}
|
|
push_chain(
|
|
&mut points,
|
|
corners
|
|
.into_iter()
|
|
.chain(std::iter::once(corners[0]))
|
|
.map(|p| [p.x, p.y, p.z]),
|
|
);
|
|
points
|
|
}
|
|
|
|
fn to_render(entity: &ExtendedEntity, document: &codec::CadDocument) -> Option<RenderEntity> {
|
|
let (points, snaps, keys): (
|
|
Vec<[f64; 3]>,
|
|
Vec<(glam::DVec3, SnapHint)>,
|
|
Vec<[f64; 3]>,
|
|
) = match &entity.data {
|
|
ExtendedEntityData::Camera { view_handle } => {
|
|
let points = camera_lines(document, *view_handle);
|
|
(points, Vec::new(), Vec::new())
|
|
}
|
|
ExtendedEntityData::SectionObject(data) => {
|
|
let snaps = data
|
|
.vertices
|
|
.iter()
|
|
.chain(data.back_line_vertices.iter())
|
|
.map(|point| {
|
|
(
|
|
glam::DVec3::new(point.x, point.y, point.z),
|
|
SnapHint::Node,
|
|
)
|
|
})
|
|
.collect();
|
|
let keys = data
|
|
.vertices
|
|
.iter()
|
|
.chain(data.back_line_vertices.iter())
|
|
.map(|point| [point.x, point.y, point.z])
|
|
.collect();
|
|
(section_lines(data), snaps, keys)
|
|
}
|
|
ExtendedEntityData::ArcAlignedText(data) => {
|
|
let mut points = Vec::new();
|
|
append_arc_aligned_text(&mut points, data);
|
|
(
|
|
points,
|
|
vec![(
|
|
glam::DVec3::new(data.center.x, data.center.y, data.center.z),
|
|
SnapHint::Center,
|
|
)],
|
|
vec![[data.center.x, data.center.y, data.center.z]],
|
|
)
|
|
}
|
|
ExtendedEntityData::RemoteText(data) => (
|
|
remote_text_lines(data),
|
|
vec![(
|
|
glam::DVec3::new(data.position.x, data.position.y, data.position.z),
|
|
SnapHint::Insertion,
|
|
)],
|
|
vec![[data.position.x, data.position.y, data.position.z]],
|
|
),
|
|
ExtendedEntityData::GeoPositionMarker(data) => (
|
|
geo_marker_lines(data),
|
|
vec![(
|
|
glam::DVec3::new(data.position.x, data.position.y, data.position.z),
|
|
SnapHint::Node,
|
|
)],
|
|
vec![[data.position.x, data.position.y, data.position.z]],
|
|
),
|
|
ExtendedEntityData::PointCloud(data) => (
|
|
point_cloud_lines(data),
|
|
vec![(
|
|
glam::DVec3::new(data.origin.x, data.origin.y, data.origin.z),
|
|
SnapHint::Insertion,
|
|
)],
|
|
vec![
|
|
[data.extents_min.x, data.extents_min.y, data.extents_min.z],
|
|
[data.extents_max.x, data.extents_max.y, data.extents_max.z],
|
|
],
|
|
),
|
|
ExtendedEntityData::PointCloudEx(data) => (
|
|
point_cloud_ex_lines(data),
|
|
Vec::new(),
|
|
vec![
|
|
[data.extents_min.x, data.extents_min.y, data.extents_min.z],
|
|
[data.extents_max.x, data.extents_max.y, data.extents_max.z],
|
|
],
|
|
),
|
|
_ => return None,
|
|
};
|
|
if points.len() < 2 {
|
|
return None;
|
|
}
|
|
Some(RenderEntity {
|
|
object: RenderObject::Lines(points),
|
|
snap_pts: snaps,
|
|
tangent_geoms: Vec::new(),
|
|
key_vertices: keys,
|
|
fill_tris: Vec::new(),
|
|
pick_tris: Vec::new(),
|
|
})
|
|
}
|
|
|
|
fn section_properties(entity: &ExtendedEntity, data: &SectionObjectData) -> Vec<PropSection> {
|
|
let vertices = data
|
|
.vertices
|
|
.iter()
|
|
.map(|point| vector_text(*point))
|
|
.collect::<Vec<_>>()
|
|
.join("; ");
|
|
let kind = section_kind(entity, data);
|
|
let viewing = section_viewing_direction(data);
|
|
let offset = section_plane_offset(data);
|
|
let depth = section_slice_depth(entity).unwrap_or_else(|| section_depth(data));
|
|
vec![PropSection {
|
|
title: t!("Section Plane").into_owned(),
|
|
props: vec![
|
|
text_prop(t!("Name").as_ref(), "ext_section_name", &data.name),
|
|
choice_prop(
|
|
t!("State").as_ref(),
|
|
"ext_section_state",
|
|
kind,
|
|
&["Plane", "Slice", "Boundary", "Volume"],
|
|
),
|
|
text_prop(
|
|
t!("Viewing Direction").as_ref(),
|
|
"ext_section_viewing",
|
|
&vector_text(viewing),
|
|
),
|
|
text_prop(
|
|
t!("Vertical Direction").as_ref(),
|
|
"ext_section_vertical",
|
|
&vector_text(data.vertical_direction),
|
|
),
|
|
ro_prop(t!("Normal").as_ref(), "ext_section_normal", vector_text(viewing)),
|
|
bool_prop(
|
|
t!("Live Section Enabled").as_ref(),
|
|
"ext_section_live",
|
|
data.flags & 1 != 0,
|
|
),
|
|
edit_prop(
|
|
t!("Indicator Transparency").as_ref(),
|
|
"ext_section_alpha",
|
|
f64::from(data.indicator_alpha),
|
|
),
|
|
Property {
|
|
label: t!("Indicator Fill Color").into_owned(),
|
|
field: "indicator_fill_color",
|
|
value: PropValue::ColorChoice(data.indicator_color),
|
|
},
|
|
edit_prop(t!("Elevation").as_ref(), "ext_section_elevation", offset),
|
|
edit_prop(t!("Top Height").as_ref(), "ext_section_top", data.top_height),
|
|
edit_prop(t!("Bottom Height").as_ref(), "ext_section_bottom", data.bottom_height),
|
|
ro_prop(
|
|
t!("Number of Vertices").as_ref(),
|
|
"ext_section_vertex_count",
|
|
data.vertices.len().to_string(),
|
|
),
|
|
text_prop(t!("Vertices").as_ref(), "ext_section_vertices", &vertices),
|
|
ro_prop(
|
|
t!("Settings").as_ref(),
|
|
"ext_section_settings",
|
|
handle_text(data.settings_handle),
|
|
),
|
|
choice_prop(
|
|
t!("State2").as_ref(),
|
|
"ext_section_state2",
|
|
kind,
|
|
&["Plane", "Slice", "Boundary", "Volume"],
|
|
),
|
|
edit_prop(t!("Slice Depth").as_ref(), "ext_section_depth", depth),
|
|
edit_prop(
|
|
t!("Section Plane Offset").as_ref(),
|
|
"ext_section_offset",
|
|
offset,
|
|
),
|
|
],
|
|
}]
|
|
}
|
|
|
|
fn section_tangent(data: &SectionObjectData) -> Vector3 {
|
|
let Some((first, last)) = data.vertices.first().zip(data.vertices.last()) else {
|
|
return Vector3::UNIT_X;
|
|
};
|
|
normalized(*last - *first, Vector3::UNIT_X)
|
|
}
|
|
|
|
fn section_viewing_direction(data: &SectionObjectData) -> Vector3 {
|
|
let tangent = section_tangent(data);
|
|
let vertical = normalized(data.vertical_direction, Vector3::UNIT_Z);
|
|
let base = normalized(vertical.cross(&tangent), Vector3::new(0.0, 0.0, -1.0));
|
|
if data.flags & 4 != 0 { base } else { -base }
|
|
}
|
|
|
|
fn section_depth(data: &SectionObjectData) -> f64 {
|
|
data.vertices
|
|
.first()
|
|
.zip(data.back_line_vertices.first())
|
|
.map_or(0.0, |(front, back)| (*back - *front).length())
|
|
}
|
|
|
|
fn section_kind(entity: &ExtendedEntity, data: &SectionObjectData) -> &'static str {
|
|
if section_is_slice(entity) {
|
|
return "Slice";
|
|
}
|
|
match data.state {
|
|
1 => "Plane",
|
|
4 => "Volume",
|
|
2 => "Boundary",
|
|
_ => "Plane",
|
|
}
|
|
}
|
|
|
|
fn section_plane_offset(data: &SectionObjectData) -> f64 {
|
|
let Some(first) = data.vertices.first() else {
|
|
return 0.0;
|
|
};
|
|
-section_viewing_direction(data).dot(first)
|
|
}
|
|
|
|
fn parse_vector(value: &str) -> Option<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()
|
|
.map(|point| *point + viewing * depth)
|
|
.collect();
|
|
}
|
|
|
|
fn set_section_kind(data: &mut SectionObjectData, value: &str) {
|
|
let span = data
|
|
.vertices
|
|
.first()
|
|
.zip(data.vertices.last())
|
|
.map_or(1.0, |(first, last)| (*last - *first).length().max(1e-4));
|
|
match value.trim().to_ascii_uppercase().as_str() {
|
|
"SLICE" => {
|
|
data.state = 1;
|
|
data.back_line_vertices.clear();
|
|
}
|
|
"BOUNDARY" => {
|
|
data.state = 2;
|
|
set_section_depth(data, span);
|
|
}
|
|
"VOLUME" => {
|
|
data.state = 4;
|
|
set_section_depth(data, span);
|
|
}
|
|
_ => {
|
|
data.state = 1;
|
|
data.back_line_vertices.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn move_section_to_offset(data: &mut SectionObjectData, desired: f64) {
|
|
let current = section_plane_offset(data);
|
|
let delta = section_viewing_direction(data) * (current - desired);
|
|
for point in data.vertices.iter_mut().chain(data.back_line_vertices.iter_mut()) {
|
|
*point = *point + delta;
|
|
}
|
|
}
|
|
|
|
fn set_viewing_direction(data: &mut SectionObjectData, value: Vector3) {
|
|
if value.length() <= 1e-12 || data.vertices.len() < 2 {
|
|
return;
|
|
}
|
|
let desired = normalized(value, section_viewing_direction(data));
|
|
let tangent = section_tangent(data);
|
|
let vertical = tangent.cross(&desired);
|
|
if vertical.length() <= 1e-12 {
|
|
return;
|
|
}
|
|
data.vertical_direction = normalized(vertical, data.vertical_direction);
|
|
data.flags |= 4;
|
|
let depth = section_depth(data);
|
|
if depth > 0.0 {
|
|
set_section_depth(data, depth);
|
|
}
|
|
}
|
|
|
|
fn arc_text_properties(data: &ArcAlignedTextData) -> Vec<PropSection> {
|
|
vec![PropSection {
|
|
title: t!("Arc-Aligned Text").into_owned(),
|
|
props: vec![
|
|
text_prop(t!("Text").as_ref(), "ext_arc_text", &data.text),
|
|
text_prop(t!("Font").as_ref(), "ext_arc_font", &data.font_name),
|
|
text_prop(t!("Big Font").as_ref(), "ext_arc_big_font", &data.big_font_name),
|
|
text_prop(t!("Style").as_ref(), "ext_arc_style", &data.style_name),
|
|
ro_prop(t!("Center").as_ref(), "ext_arc_center", vector_text(data.center)),
|
|
edit_prop(t!("Radius").as_ref(), "ext_arc_radius", data.radius),
|
|
edit_prop(t!("X Scale").as_ref(), "ext_arc_xscale", data.x_scale),
|
|
edit_prop(t!("Text Size").as_ref(), "ext_arc_size", data.text_size),
|
|
edit_prop(t!("Character Spacing").as_ref(),
|
|
"ext_arc_spacing",
|
|
data.character_spacing,
|
|
),
|
|
edit_prop(t!("Offset From Arc").as_ref(),
|
|
"ext_arc_offset",
|
|
data.offset_from_arc,
|
|
),
|
|
edit_prop(t!("Right Offset").as_ref(), "ext_arc_right", data.right_offset),
|
|
edit_prop(t!("Left Offset").as_ref(), "ext_arc_left", data.left_offset),
|
|
edit_angle_prop(t!("Start Angle").as_ref(),
|
|
"ext_arc_start",
|
|
data.start_angle.to_degrees(),
|
|
),
|
|
edit_angle_prop(t!("End Angle").as_ref(), "ext_arc_end", data.end_angle.to_degrees()),
|
|
bool_prop(t!("Reverse").as_ref(), "ext_arc_reverse", data.reverse),
|
|
ro_prop(t!("Text Direction").as_ref(),
|
|
"ext_arc_direction",
|
|
data.text_direction.to_string(),
|
|
),
|
|
ro_prop(t!("Alignment").as_ref(), "ext_arc_alignment", data.alignment.to_string()),
|
|
ro_prop(t!("Text Position").as_ref(),
|
|
"ext_arc_position",
|
|
data.text_position.to_string(),
|
|
),
|
|
bool_prop(t!("Bold").as_ref(), "ext_arc_bold", data.bold),
|
|
bool_prop(t!("Italic").as_ref(), "ext_arc_italic", data.italic),
|
|
bool_prop(t!("Underlined").as_ref(), "ext_arc_underlined", data.underlined),
|
|
ro_prop(t!("Character Set").as_ref(),
|
|
"ext_arc_charset",
|
|
data.character_set.to_string(),
|
|
),
|
|
ro_prop(t!("Pitch And Family").as_ref(),
|
|
"ext_arc_pitch",
|
|
data.pitch_and_family.to_string(),
|
|
),
|
|
bool_prop(t!("SHX").as_ref(), "ext_arc_shx", data.is_shx),
|
|
ro_prop(t!("Text Color").as_ref(), "ext_arc_color", data.text_color.to_string()),
|
|
ro_prop(t!("Normal").as_ref(), "ext_arc_normal", vector_text(data.normal)),
|
|
bool_prop(t!("Wizard Flag").as_ref(), "ext_arc_wizard", data.wizard_flag),
|
|
ro_prop(t!("Arc").as_ref(), "ext_arc_handle", handle_text(data.arc_handle)),
|
|
],
|
|
}]
|
|
}
|
|
|
|
fn remote_text_properties(data: &RemoteTextData) -> Vec<PropSection> {
|
|
vec![PropSection {
|
|
title: t!("Remote Text").into_owned(),
|
|
props: vec![
|
|
text_prop(t!("Text").as_ref(), "ext_rtext_text", &data.text),
|
|
ro_prop(t!("Position").as_ref(), "ext_rtext_position", vector_text(data.position)),
|
|
ro_prop(t!("Normal").as_ref(), "ext_rtext_normal", vector_text(data.normal)),
|
|
edit_angle_prop(t!("Rotation").as_ref(),
|
|
"ext_rtext_rotation",
|
|
data.rotation.to_degrees(),
|
|
),
|
|
edit_prop(t!("Height").as_ref(), "ext_rtext_height", data.height),
|
|
text_prop(t!("Style Name").as_ref(), "ext_rtext_style", &data.style_name),
|
|
ro_prop(t!("Style Handle").as_ref(),
|
|
"ext_rtext_style_handle",
|
|
handle_text(data.style_handle),
|
|
),
|
|
ro_prop(t!("Flags").as_ref(), "ext_rtext_flags", data.flags.to_string()),
|
|
],
|
|
}]
|
|
}
|
|
|
|
fn geo_marker_properties(data: &GeoPositionMarkerData) -> Vec<PropSection> {
|
|
vec![PropSection {
|
|
title: t!("Geographic Position Marker").into_owned(),
|
|
props: vec![
|
|
ro_prop(t!("Class Version").as_ref(),
|
|
"ext_geo_version",
|
|
data.class_version.to_string(),
|
|
),
|
|
ro_prop(t!("Position").as_ref(), "ext_geo_position", vector_text(data.position)),
|
|
edit_prop(t!("Radius").as_ref(), "ext_geo_radius", data.radius),
|
|
text_prop(t!("Notes").as_ref(), "ext_geo_notes", &data.notes),
|
|
edit_prop(t!("Landing Gap").as_ref(), "ext_geo_gap", data.landing_gap),
|
|
bool_prop(t!("Text Visible").as_ref(), "ext_geo_visible", data.mtext_visible),
|
|
ro_prop(t!("Text Alignment").as_ref(),
|
|
"ext_geo_alignment",
|
|
data.text_alignment.to_string(),
|
|
),
|
|
bool_prop(t!("Frame Text").as_ref(), "ext_geo_frame", data.enable_frame_text),
|
|
ro_prop(t!("Embedded MText").as_ref(),
|
|
"ext_geo_mtext",
|
|
data.embedded_mtext
|
|
.as_ref()
|
|
.map(|text| text.value.clone())
|
|
.unwrap_or_default(),
|
|
),
|
|
],
|
|
}]
|
|
}
|
|
|
|
fn point_cloud_properties(data: &PointCloudData) -> Vec<PropSection> {
|
|
let clips = data
|
|
.clippings
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(index, clip)| {
|
|
format!(
|
|
"{}: type {}; inverted {}; vertices {}; Z {:.6}..{:.6}",
|
|
index + 1,
|
|
clip.clip_type,
|
|
clip.inverted,
|
|
clip.vertices.len(),
|
|
clip.z_min,
|
|
clip.z_max
|
|
)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
vec![
|
|
PropSection {
|
|
title: t!("Point Cloud").into_owned(),
|
|
props: vec![
|
|
ro_prop(t!("Class Version").as_ref(),
|
|
"ext_pc_version",
|
|
data.class_version.to_string(),
|
|
),
|
|
ro_prop(t!("Origin").as_ref(), "ext_pc_origin", vector_text(data.origin)),
|
|
text_prop(t!("Saved File").as_ref(), "ext_pc_file", &data.saved_filename),
|
|
ro_prop(t!("Source Files").as_ref(),
|
|
"ext_pc_sources",
|
|
data.source_files.join("\n"),
|
|
),
|
|
ro_prop(t!("Extents Min").as_ref(), "ext_pc_min", vector_text(data.extents_min)),
|
|
ro_prop(t!("Extents Max").as_ref(), "ext_pc_max", vector_text(data.extents_max)),
|
|
ro_prop(t!("Point Count").as_ref(),
|
|
"ext_pc_count",
|
|
data.point_count.to_string(),
|
|
),
|
|
text_prop(t!("UCS Name").as_ref(), "ext_pc_ucs", &data.ucs_name),
|
|
ro_prop(t!("UCS Origin").as_ref(),
|
|
"ext_pc_ucs_origin",
|
|
vector_text(data.ucs_origin),
|
|
),
|
|
ro_prop(t!("UCS X").as_ref(),
|
|
"ext_pc_ucs_x",
|
|
vector_text(data.ucs_x_direction),
|
|
),
|
|
ro_prop(t!("UCS Y").as_ref(),
|
|
"ext_pc_ucs_y",
|
|
vector_text(data.ucs_y_direction),
|
|
),
|
|
ro_prop(t!("UCS Z").as_ref(),
|
|
"ext_pc_ucs_z",
|
|
vector_text(data.ucs_z_direction),
|
|
),
|
|
ro_prop(t!("Definition").as_ref(),
|
|
"ext_pc_definition",
|
|
handle_text(data.definition_handle),
|
|
),
|
|
ro_prop(t!("Reactor").as_ref(),
|
|
"ext_pc_reactor",
|
|
handle_text(data.reactor_handle),
|
|
),
|
|
],
|
|
},
|
|
PropSection {
|
|
title: t!("Point Cloud Display").into_owned(),
|
|
props: vec![
|
|
bool_prop(t!("Show Intensity").as_ref(), "ext_pc_show_intensity", data.show_intensity),
|
|
ro_prop(t!("Intensity Scheme").as_ref(),
|
|
"ext_pc_intensity_scheme",
|
|
data.intensity_scheme.to_string(),
|
|
),
|
|
edit_prop(t!("Minimum Intensity").as_ref(),
|
|
"ext_pc_intensity_min",
|
|
data.minimum_intensity,
|
|
),
|
|
edit_prop(t!("Maximum Intensity").as_ref(),
|
|
"ext_pc_intensity_max",
|
|
data.maximum_intensity,
|
|
),
|
|
edit_prop(t!("Low Threshold").as_ref(),
|
|
"ext_pc_low_threshold",
|
|
data.low_intensity_threshold,
|
|
),
|
|
edit_prop(t!("High Threshold").as_ref(),
|
|
"ext_pc_high_threshold",
|
|
data.high_intensity_threshold,
|
|
),
|
|
bool_prop(t!("Show Clipping").as_ref(), "ext_pc_show_clipping", data.show_clipping),
|
|
ro_prop(t!("Clippings").as_ref(), "ext_pc_clippings", clips),
|
|
],
|
|
},
|
|
]
|
|
}
|
|
|
|
fn point_cloud_ex_properties(data: &PointCloudExData) -> Vec<PropSection> {
|
|
let crops = data
|
|
.croppings
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(index, crop)| {
|
|
format!(
|
|
"{}: type {}; inside {}; inverted {}; points {}; plane [{}]",
|
|
index + 1,
|
|
crop.crop_type,
|
|
crop.inside,
|
|
crop.inverted,
|
|
crop.points.len(),
|
|
vector_text(crop.plane)
|
|
)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
vec![
|
|
PropSection {
|
|
title: t!("Point Cloud Ex").into_owned(),
|
|
props: vec![
|
|
ro_prop(t!("Class Version").as_ref(),
|
|
"ext_pcx_version",
|
|
data.class_version.to_string(),
|
|
),
|
|
text_prop(t!("Name").as_ref(), "ext_pcx_name", &data.name),
|
|
ro_prop(t!("Extents Min").as_ref(), "ext_pcx_min", vector_text(data.extents_min)),
|
|
ro_prop(t!("Extents Max").as_ref(), "ext_pcx_max", vector_text(data.extents_max)),
|
|
ro_prop(t!("UCS Origin").as_ref(),
|
|
"ext_pcx_ucs_origin",
|
|
vector_text(data.ucs_origin),
|
|
),
|
|
ro_prop(t!("UCS X").as_ref(),
|
|
"ext_pcx_ucs_x",
|
|
vector_text(data.ucs_x_direction),
|
|
),
|
|
ro_prop(t!("UCS Y").as_ref(),
|
|
"ext_pcx_ucs_y",
|
|
vector_text(data.ucs_y_direction),
|
|
),
|
|
ro_prop(t!("UCS Z").as_ref(),
|
|
"ext_pcx_ucs_z",
|
|
vector_text(data.ucs_z_direction),
|
|
),
|
|
bool_prop(t!("Locked").as_ref(), "ext_pcx_locked", data.locked),
|
|
ro_prop(t!("Definition").as_ref(),
|
|
"ext_pcx_definition",
|
|
handle_text(data.definition_handle),
|
|
),
|
|
ro_prop(t!("Reactor").as_ref(),
|
|
"ext_pcx_reactor",
|
|
handle_text(data.reactor_handle),
|
|
),
|
|
],
|
|
},
|
|
PropSection {
|
|
title: t!("Point Cloud Ex Display").into_owned(),
|
|
props: vec![
|
|
bool_prop(t!("Show Intensity").as_ref(),
|
|
"ext_pcx_show_intensity",
|
|
data.show_intensity,
|
|
),
|
|
bool_prop(t!("Show Cropping").as_ref(), "ext_pcx_show_cropping", data.show_cropping),
|
|
ro_prop(t!("Unknown Flags").as_ref(),
|
|
"ext_pcx_unknown",
|
|
format!("{}, {}", data.unknown_bl0, data.unknown_bl1),
|
|
),
|
|
ro_prop(t!("Stylization Type").as_ref(),
|
|
"ext_pcx_stylization",
|
|
data.stylization_type.to_string(),
|
|
),
|
|
text_prop(t!("Intensity Color Scheme").as_ref(),
|
|
"ext_pcx_intensity_scheme",
|
|
&data.intensity_color_scheme,
|
|
),
|
|
text_prop(t!("Current Color Scheme").as_ref(),
|
|
"ext_pcx_current_scheme",
|
|
&data.current_color_scheme,
|
|
),
|
|
text_prop(t!("Classification Scheme").as_ref(),
|
|
"ext_pcx_class_scheme",
|
|
&data.classification_color_scheme,
|
|
),
|
|
edit_prop(t!("Elevation Min").as_ref(), "ext_pcx_elevation_min", data.elevation_min),
|
|
edit_prop(t!("Elevation Max").as_ref(), "ext_pcx_elevation_max", data.elevation_max),
|
|
ro_prop(t!("Intensity Range").as_ref(),
|
|
"ext_pcx_intensity_range",
|
|
format!("{}..{}", data.intensity_min, data.intensity_max),
|
|
),
|
|
ro_prop(t!("Out Of Range Behavior").as_ref(),
|
|
"ext_pcx_out_of_range",
|
|
format!(
|
|
"intensity {}; elevation {}",
|
|
data.intensity_out_of_range_behavior,
|
|
data.elevation_out_of_range_behavior
|
|
),
|
|
),
|
|
bool_prop(t!("Fixed Elevation Range").as_ref(),
|
|
"ext_pcx_fixed_range",
|
|
data.elevation_apply_to_fixed_range,
|
|
),
|
|
bool_prop(t!("Intensity Gradient").as_ref(),
|
|
"ext_pcx_intensity_gradient",
|
|
data.intensity_as_gradient,
|
|
),
|
|
bool_prop(t!("Elevation Gradient").as_ref(),
|
|
"ext_pcx_elevation_gradient",
|
|
data.elevation_as_gradient,
|
|
),
|
|
ro_prop(t!("Croppings").as_ref(), "ext_pcx_croppings", crops),
|
|
],
|
|
},
|
|
]
|
|
}
|
|
|
|
fn semantic_properties(
|
|
properties: &[codec::objects::SemanticProperty],
|
|
) -> String {
|
|
properties
|
|
.iter()
|
|
.map(|property| {
|
|
format!(
|
|
"{} [{}]: {:?}",
|
|
property.subclass, property.code, property.value
|
|
)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n")
|
|
}
|
|
|
|
fn reference_properties(
|
|
references: &[codec::objects::ProxyObjectReference],
|
|
) -> String {
|
|
references
|
|
.iter()
|
|
.map(|reference| {
|
|
format!(
|
|
"{:X}: {:?}",
|
|
reference.handle.value(),
|
|
reference.kind
|
|
)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n")
|
|
}
|
|
|
|
fn properties(entity: &ExtendedEntity) -> Vec<PropSection> {
|
|
match &entity.data {
|
|
ExtendedEntityData::Camera { view_handle } => vec![PropSection {
|
|
title: t!("Camera").into_owned(),
|
|
props: vec![ro_prop(t!("View").as_ref(), "ext_camera_view", handle_text(*view_handle))],
|
|
}],
|
|
ExtendedEntityData::SectionObject(data) => section_properties(entity, data),
|
|
ExtendedEntityData::ArcAlignedText(data) => arc_text_properties(data),
|
|
ExtendedEntityData::RemoteText(data) => remote_text_properties(data),
|
|
ExtendedEntityData::GeoPositionMarker(data) => geo_marker_properties(data),
|
|
ExtendedEntityData::CoordinationModel(data) => vec![PropSection {
|
|
title: t!("Coordination Model").into_owned(),
|
|
props: vec![
|
|
ro_prop(t!("Flags").as_ref(), "ext_coord_flags", data.flags.to_string()),
|
|
ro_prop(t!("Definition").as_ref(),
|
|
"ext_coord_definition",
|
|
handle_text(data.definition_handle),
|
|
),
|
|
edit_prop(t!("Unit Factor").as_ref(), "ext_coord_unit", data.unit_factor),
|
|
ro_prop(t!("Transform").as_ref(),
|
|
"ext_coord_transform",
|
|
data.transform
|
|
.chunks_exact(4)
|
|
.map(|row| {
|
|
format!(
|
|
"{:.6}, {:.6}, {:.6}, {:.6}",
|
|
row[0], row[1], row[2], row[3]
|
|
)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n"),
|
|
),
|
|
],
|
|
}],
|
|
ExtendedEntityData::PointCloud(data) => point_cloud_properties(data),
|
|
ExtendedEntityData::PointCloudEx(data) => point_cloud_ex_properties(data),
|
|
ExtendedEntityData::Proxy(data) => vec![PropSection {
|
|
title: t!("Proxy Entity").into_owned(),
|
|
props: vec![
|
|
ro_prop(t!("Proxy ID").as_ref(), "ext_proxy_id", data.proxy_id.to_string()),
|
|
ro_prop(t!("Class ID").as_ref(), "ext_proxy_class", data.class_id.to_string()),
|
|
ro_prop(t!("DXF Subclass").as_ref(), "ext_proxy_subclass", data.dxf_subclass.clone()),
|
|
ro_prop(t!("Version").as_ref(), "ext_proxy_version", data.version.to_string()),
|
|
ro_prop(t!("DWG Version").as_ref(),
|
|
"ext_proxy_dwg_version",
|
|
format!("{}.{}", data.dwg_version, data.maintenance_version),
|
|
),
|
|
ro_prop(t!("From DXF").as_ref(), "ext_proxy_from_dxf", data.from_dxf.to_string()),
|
|
ro_prop(t!("Graphics").as_ref(),
|
|
"ext_proxy_graphics",
|
|
format!("{} bits", data.graphics.bit_count),
|
|
),
|
|
ro_prop(t!("Payload").as_ref(),
|
|
"ext_proxy_payload",
|
|
format!("{} bits", data.payload.bit_count),
|
|
),
|
|
ro_prop(t!("Text Payload").as_ref(),
|
|
"ext_proxy_text_payload",
|
|
format!("{} bits", data.text_payload.bit_count),
|
|
),
|
|
ro_prop(t!("Object References").as_ref(),
|
|
"ext_proxy_references",
|
|
reference_properties(&data.object_ids),
|
|
),
|
|
],
|
|
}],
|
|
ExtendedEntityData::OleFrame(data) => vec![PropSection {
|
|
title: t!("OLE Frame").into_owned(),
|
|
props: vec![
|
|
ro_prop(t!("Flag").as_ref(), "ext_ole_flag", data.flag.to_string()),
|
|
ro_prop(t!("Mode").as_ref(), "ext_ole_mode", data.mode.to_string()),
|
|
ro_prop(t!("Storage Size").as_ref(),
|
|
"ext_ole_size",
|
|
format!("{} bytes", data.storage.encoded_len()),
|
|
),
|
|
],
|
|
}],
|
|
ExtendedEntityData::LayoutPrintConfig(data) => vec![PropSection {
|
|
title: t!("Layout Print Configuration").into_owned(),
|
|
props: vec![
|
|
ro_prop(t!("Class Version").as_ref(),
|
|
"ext_print_version",
|
|
data.class_version.to_string(),
|
|
),
|
|
ro_prop(t!("Flag").as_ref(), "ext_print_flag", data.flag.to_string()),
|
|
],
|
|
}],
|
|
ExtendedEntityData::Format(data) => vec![PropSection {
|
|
title: t!("Format").into_owned(),
|
|
props: vec![
|
|
ro_prop(t!("DWG Payload").as_ref(),
|
|
"ext_format_dwg",
|
|
format!(
|
|
"{} bytes / {} handle bits",
|
|
data.raw_dwg_data.as_ref().map_or(0, Vec::len),
|
|
data.raw_dwg_handle_bits
|
|
),
|
|
),
|
|
ro_prop(t!("DWG Version").as_ref(),
|
|
"ext_format_version",
|
|
format!("{:?}", data.raw_dwg_version),
|
|
),
|
|
ro_prop(t!("DXF Codes").as_ref(),
|
|
"ext_format_dxf",
|
|
data.raw_dxf_codes.as_ref().map_or(0, Vec::len).to_string(),
|
|
),
|
|
],
|
|
}],
|
|
ExtendedEntityData::Legacy(data) => vec![PropSection {
|
|
title: t!("Legacy Entity").into_owned(),
|
|
props: vec![ro_prop(t!("Data").as_ref(), "ext_legacy_data", format!("{data:#?}"))],
|
|
}],
|
|
ExtendedEntityData::DynamicBlock(data) => vec![PropSection {
|
|
title: t!("Dynamic Block Entity").into_owned(),
|
|
props: vec![
|
|
ro_prop(t!("Class").as_ref(),
|
|
"ext_dynamic_class",
|
|
data.entity_dxf_name().unwrap_or("Helper"),
|
|
),
|
|
ro_prop(t!("Decoded Data").as_ref(), "ext_dynamic_data", format!("{data:#?}")),
|
|
],
|
|
}],
|
|
ExtendedEntityData::RegisteredClass(data) => vec![PropSection {
|
|
title: t!("Registered Class Entity").into_owned(),
|
|
props: vec![
|
|
ro_prop(t!("DXF Name").as_ref(), "ext_registered_dxf", data.dxf_name.clone()),
|
|
ro_prop(t!("C++ Class").as_ref(),
|
|
"ext_registered_cpp",
|
|
data.cpp_class_name.clone(),
|
|
),
|
|
ro_prop(t!("Properties").as_ref(),
|
|
"ext_registered_properties",
|
|
semantic_properties(&data.properties),
|
|
),
|
|
ro_prop(t!("Payload").as_ref(),
|
|
"ext_registered_payload",
|
|
format!("{} bits", data.payload.bit_count),
|
|
),
|
|
ro_prop(t!("Object References").as_ref(),
|
|
"ext_registered_references",
|
|
reference_properties(&data.object_ids),
|
|
),
|
|
],
|
|
}],
|
|
}
|
|
}
|
|
|
|
fn set_f64(value: &str, target: &mut f64) {
|
|
if let Some(value) = parse_f64(value) {
|
|
*target = value;
|
|
}
|
|
}
|
|
|
|
fn apply_section_prop(entity: &mut ExtendedEntity, field: &str, value: &str) {
|
|
let was_slice = section_is_slice(entity);
|
|
let stored_slice_depth = section_slice_depth(entity).unwrap_or(0.0);
|
|
let ExtendedEntityData::SectionObject(data) = &mut entity.data else {
|
|
return;
|
|
};
|
|
let mut slice_update = None;
|
|
match field {
|
|
"ext_section_name" => data.name = value.to_string(),
|
|
"ext_section_state" | "ext_section_state2" => {
|
|
let kind = value.trim().to_ascii_uppercase();
|
|
let span = data
|
|
.vertices
|
|
.first()
|
|
.zip(data.vertices.last())
|
|
.map_or(1.0, |(first, last)| (*last - *first).length().max(1e-4));
|
|
set_section_kind(data, &kind);
|
|
slice_update = Some((kind == "SLICE", (span / 60.0).max(1e-4)));
|
|
}
|
|
"ext_section_viewing" => {
|
|
if let Some(direction) = parse_vector(value) {
|
|
set_viewing_direction(data, direction);
|
|
}
|
|
}
|
|
"ext_section_vertical" => {
|
|
if let Some(direction) = parse_vector(value) {
|
|
let depth = section_depth(data);
|
|
let direction = normalized(direction, data.vertical_direction);
|
|
if direction.dot(§ion_tangent(data)).abs() < 1.0 - 1e-9 {
|
|
data.vertical_direction = direction;
|
|
if depth > 0.0 {
|
|
set_section_depth(data, depth);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"ext_section_live" => data.flags ^= 1,
|
|
"ext_section_alpha" => {
|
|
if let Ok(alpha) = value.trim().parse::<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) {
|
|
if was_slice {
|
|
slice_update = Some((true, depth));
|
|
} else {
|
|
set_section_depth(data, depth);
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
if let Some((enabled, depth)) = slice_update {
|
|
set_section_slice_metadata(entity, enabled, depth);
|
|
} else if was_slice && field != "ext_section_depth" {
|
|
set_section_slice_metadata(entity, true, stored_slice_depth);
|
|
}
|
|
}
|
|
|
|
fn apply_geom_prop(entity: &mut ExtendedEntity, field: &str, value: &str) {
|
|
if matches!(entity.data, ExtendedEntityData::SectionObject(_)) {
|
|
apply_section_prop(entity, field, value);
|
|
return;
|
|
}
|
|
match &mut entity.data {
|
|
ExtendedEntityData::ArcAlignedText(data) => match field {
|
|
"ext_arc_text" => data.text = value.to_string(),
|
|
"ext_arc_font" => data.font_name = value.to_string(),
|
|
"ext_arc_big_font" => data.big_font_name = value.to_string(),
|
|
"ext_arc_style" => data.style_name = value.to_string(),
|
|
"ext_arc_radius" => set_f64(value, &mut data.radius),
|
|
"ext_arc_xscale" => set_f64(value, &mut data.x_scale),
|
|
"ext_arc_size" => set_f64(value, &mut data.text_size),
|
|
"ext_arc_spacing" => set_f64(value, &mut data.character_spacing),
|
|
"ext_arc_offset" => set_f64(value, &mut data.offset_from_arc),
|
|
"ext_arc_right" => set_f64(value, &mut data.right_offset),
|
|
"ext_arc_left" => set_f64(value, &mut data.left_offset),
|
|
"ext_arc_start" => {
|
|
if let Some(value) = parse_f64(value) {
|
|
data.start_angle = value.to_radians();
|
|
}
|
|
}
|
|
"ext_arc_end" => {
|
|
if let Some(value) = parse_f64(value) {
|
|
data.end_angle = value.to_radians();
|
|
}
|
|
}
|
|
"ext_arc_reverse" => data.reverse = !data.reverse,
|
|
"ext_arc_bold" => data.bold = !data.bold,
|
|
"ext_arc_italic" => data.italic = !data.italic,
|
|
"ext_arc_underlined" => data.underlined = !data.underlined,
|
|
"ext_arc_shx" => data.is_shx = !data.is_shx,
|
|
"ext_arc_wizard" => data.wizard_flag = !data.wizard_flag,
|
|
_ => {}
|
|
},
|
|
ExtendedEntityData::RemoteText(data) => match field {
|
|
"ext_rtext_text" => data.text = value.to_string(),
|
|
"ext_rtext_style" => data.style_name = value.to_string(),
|
|
"ext_rtext_height" => set_f64(value, &mut data.height),
|
|
"ext_rtext_rotation" => {
|
|
if let Some(value) = parse_f64(value) {
|
|
data.rotation = value.to_radians();
|
|
}
|
|
}
|
|
_ => {}
|
|
},
|
|
ExtendedEntityData::GeoPositionMarker(data) => match field {
|
|
"ext_geo_radius" => set_f64(value, &mut data.radius),
|
|
"ext_geo_notes" => data.notes = value.to_string(),
|
|
"ext_geo_gap" => set_f64(value, &mut data.landing_gap),
|
|
"ext_geo_visible" => data.mtext_visible = !data.mtext_visible,
|
|
"ext_geo_frame" => data.enable_frame_text = !data.enable_frame_text,
|
|
_ => {}
|
|
},
|
|
ExtendedEntityData::CoordinationModel(data) => {
|
|
if field == "ext_coord_unit" {
|
|
set_f64(value, &mut data.unit_factor);
|
|
}
|
|
}
|
|
ExtendedEntityData::PointCloud(data) => match field {
|
|
"ext_pc_file" => data.saved_filename = value.to_string(),
|
|
"ext_pc_ucs" => data.ucs_name = value.to_string(),
|
|
"ext_pc_show_intensity" => data.show_intensity = !data.show_intensity,
|
|
"ext_pc_show_clipping" => data.show_clipping = !data.show_clipping,
|
|
"ext_pc_intensity_min" => set_f64(value, &mut data.minimum_intensity),
|
|
"ext_pc_intensity_max" => set_f64(value, &mut data.maximum_intensity),
|
|
"ext_pc_low_threshold" => set_f64(value, &mut data.low_intensity_threshold),
|
|
"ext_pc_high_threshold" => set_f64(value, &mut data.high_intensity_threshold),
|
|
_ => {}
|
|
},
|
|
ExtendedEntityData::PointCloudEx(data) => match field {
|
|
"ext_pcx_name" => data.name = value.to_string(),
|
|
"ext_pcx_locked" => data.locked = !data.locked,
|
|
"ext_pcx_show_intensity" => data.show_intensity = !data.show_intensity,
|
|
"ext_pcx_show_cropping" => data.show_cropping = !data.show_cropping,
|
|
"ext_pcx_intensity_scheme" => data.intensity_color_scheme = value.to_string(),
|
|
"ext_pcx_current_scheme" => data.current_color_scheme = value.to_string(),
|
|
"ext_pcx_class_scheme" => data.classification_color_scheme = value.to_string(),
|
|
"ext_pcx_elevation_min" => set_f64(value, &mut data.elevation_min),
|
|
"ext_pcx_elevation_max" => set_f64(value, &mut data.elevation_max),
|
|
"ext_pcx_fixed_range" => {
|
|
data.elevation_apply_to_fixed_range = !data.elevation_apply_to_fixed_range
|
|
}
|
|
"ext_pcx_intensity_gradient" => {
|
|
data.intensity_as_gradient = !data.intensity_as_gradient
|
|
}
|
|
"ext_pcx_elevation_gradient" => {
|
|
data.elevation_as_gradient = !data.elevation_as_gradient
|
|
}
|
|
_ => {}
|
|
},
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn apply_point(point: &mut Vector3, apply: GripApply) {
|
|
match apply {
|
|
GripApply::Translate(delta) => {
|
|
point.x += delta.x;
|
|
point.y += delta.y;
|
|
point.z += delta.z;
|
|
}
|
|
GripApply::Absolute(position) => {
|
|
*point = Vector3::new(position.x, position.y, position.z);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn move_extents(
|
|
min: &mut Vector3,
|
|
max: &mut Vector3,
|
|
grip_id: usize,
|
|
apply: GripApply,
|
|
) {
|
|
match grip_id {
|
|
0 => apply_point(min, apply),
|
|
1 => apply_point(max, apply),
|
|
2 => {
|
|
let center = (*min + *max) * 0.5;
|
|
let delta = match apply {
|
|
GripApply::Translate(delta) => delta,
|
|
GripApply::Absolute(position) => {
|
|
position - glam::DVec3::new(center.x, center.y, center.z)
|
|
}
|
|
};
|
|
let offset = Vector3::new(delta.x, delta.y, delta.z);
|
|
*min = *min + offset;
|
|
*max = *max + offset;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn grips(entity: &ExtendedEntity) -> Vec<GripDef> {
|
|
match &entity.data {
|
|
ExtendedEntityData::SectionObject(data) => {
|
|
let mut grips: Vec<GripDef> = if section_kind(entity, data) == "Plane"
|
|
&& data.vertices.len() == 2
|
|
&& data.back_line_vertices.is_empty()
|
|
{
|
|
section_plane_edge_grips(data)
|
|
} else {
|
|
data.vertices
|
|
.iter()
|
|
.chain(data.back_line_vertices.iter())
|
|
.enumerate()
|
|
.map(|(index, point)| {
|
|
square_grip(index, glam::DVec3::new(point.x, point.y, point.z))
|
|
})
|
|
.collect()
|
|
};
|
|
if let Some(right) = data.vertices.last() {
|
|
let vertical = normalized(data.vertical_direction, Vector3::UNIT_Z);
|
|
let edge_center_offset = (data.top_height - data.bottom_height) * 0.5;
|
|
let anchor = *right + vertical * edge_center_offset;
|
|
grips.push(GripDef {
|
|
id: SECTION_GRIP_STATE,
|
|
world: glam::DVec3::new(anchor.x, anchor.y, anchor.z),
|
|
is_midpoint: false,
|
|
shape: crate::scene::model::object::GripShape::DropdownAdjacent,
|
|
dir: None,
|
|
axis: None,
|
|
});
|
|
}
|
|
if let Some(center) = section_center(data) {
|
|
let normal = section_viewing_direction(data);
|
|
let span = data
|
|
.vertices
|
|
.first()
|
|
.zip(data.vertices.last())
|
|
.map_or(1.0, |(first, last)| (*last - *first).length());
|
|
let offset = (span * 0.08).max(1.0e-6);
|
|
let normal_world = glam::DVec3::new(normal.x, normal.y, normal.z);
|
|
grips.push(GripDef {
|
|
id: SECTION_GRIP_NORMAL,
|
|
world: glam::DVec3::new(center.x, center.y, center.z)
|
|
- normal_world * offset,
|
|
is_midpoint: false,
|
|
shape: crate::scene::model::object::GripShape::Triangle,
|
|
dir: Some(-normal_world),
|
|
axis: Some(normal_world),
|
|
});
|
|
}
|
|
grips
|
|
}
|
|
ExtendedEntityData::ArcAlignedText(data) => vec![center_grip(
|
|
0,
|
|
glam::DVec3::new(data.center.x, data.center.y, data.center.z),
|
|
)],
|
|
ExtendedEntityData::RemoteText(data) => vec![square_grip(
|
|
0,
|
|
glam::DVec3::new(data.position.x, data.position.y, data.position.z),
|
|
)],
|
|
ExtendedEntityData::GeoPositionMarker(data) => vec![center_grip(
|
|
0,
|
|
glam::DVec3::new(data.position.x, data.position.y, data.position.z),
|
|
)],
|
|
ExtendedEntityData::PointCloud(data) => vec![
|
|
square_grip(
|
|
0,
|
|
glam::DVec3::new(
|
|
data.extents_min.x,
|
|
data.extents_min.y,
|
|
data.extents_min.z,
|
|
),
|
|
),
|
|
square_grip(
|
|
1,
|
|
glam::DVec3::new(
|
|
data.extents_max.x,
|
|
data.extents_max.y,
|
|
data.extents_max.z,
|
|
),
|
|
),
|
|
center_grip(
|
|
2,
|
|
glam::DVec3::new(
|
|
(data.extents_min.x + data.extents_max.x) * 0.5,
|
|
(data.extents_min.y + data.extents_max.y) * 0.5,
|
|
(data.extents_min.z + data.extents_max.z) * 0.5,
|
|
),
|
|
),
|
|
],
|
|
ExtendedEntityData::PointCloudEx(data) => vec![
|
|
square_grip(
|
|
0,
|
|
glam::DVec3::new(
|
|
data.extents_min.x,
|
|
data.extents_min.y,
|
|
data.extents_min.z,
|
|
),
|
|
),
|
|
square_grip(
|
|
1,
|
|
glam::DVec3::new(
|
|
data.extents_max.x,
|
|
data.extents_max.y,
|
|
data.extents_max.z,
|
|
),
|
|
),
|
|
center_grip(
|
|
2,
|
|
glam::DVec3::new(
|
|
(data.extents_min.x + data.extents_max.x) * 0.5,
|
|
(data.extents_min.y + data.extents_max.y) * 0.5,
|
|
(data.extents_min.z + data.extents_max.z) * 0.5,
|
|
),
|
|
),
|
|
],
|
|
_ => Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn apply_grip(entity: &mut ExtendedEntity, grip_id: usize, apply: GripApply) {
|
|
match &mut entity.data {
|
|
ExtendedEntityData::SectionObject(data) => {
|
|
if matches!(
|
|
grip_id,
|
|
SECTION_GRIP_LEFT
|
|
| SECTION_GRIP_RIGHT
|
|
| SECTION_GRIP_TOP
|
|
| SECTION_GRIP_BOTTOM
|
|
) {
|
|
apply_section_plane_edge_grip(data, grip_id, apply);
|
|
} else if grip_id == SECTION_GRIP_NORMAL {
|
|
let Some(center) = section_center(data) else {
|
|
return;
|
|
};
|
|
let normal = section_viewing_direction(data);
|
|
let span = data
|
|
.vertices
|
|
.first()
|
|
.zip(data.vertices.last())
|
|
.map_or(1.0, |(first, last)| (*last - *first).length());
|
|
let offset = (span * 0.08).max(1.0e-6);
|
|
let current = center - normal * offset;
|
|
let delta = match apply {
|
|
GripApply::Translate(delta) => Vector3::new(delta.x, delta.y, delta.z),
|
|
GripApply::Absolute(position) => {
|
|
Vector3::new(position.x, position.y, position.z) - current
|
|
}
|
|
};
|
|
let constrained = normal * delta.dot(&normal);
|
|
for point in data
|
|
.vertices
|
|
.iter_mut()
|
|
.chain(data.back_line_vertices.iter_mut())
|
|
{
|
|
*point = *point + constrained;
|
|
}
|
|
} else if grip_id < data.vertices.len() {
|
|
apply_point(&mut data.vertices[grip_id], apply);
|
|
} else if let Some(point) = data
|
|
.back_line_vertices
|
|
.get_mut(grip_id - data.vertices.len())
|
|
{
|
|
apply_point(point, apply);
|
|
}
|
|
}
|
|
ExtendedEntityData::ArcAlignedText(data) if grip_id == 0 => {
|
|
apply_point(&mut data.center, apply)
|
|
}
|
|
ExtendedEntityData::RemoteText(data) if grip_id == 0 => {
|
|
apply_point(&mut data.position, apply)
|
|
}
|
|
ExtendedEntityData::GeoPositionMarker(data) if grip_id == 0 => {
|
|
let before = data.position;
|
|
apply_point(&mut data.position, apply);
|
|
if let Some(text) = data.embedded_mtext.as_mut() {
|
|
text.insertion_point =
|
|
text.insertion_point + (data.position - before);
|
|
}
|
|
}
|
|
ExtendedEntityData::PointCloud(data) => {
|
|
let before = (data.extents_min + data.extents_max) * 0.5;
|
|
move_extents(
|
|
&mut data.extents_min,
|
|
&mut data.extents_max,
|
|
grip_id,
|
|
apply,
|
|
);
|
|
if grip_id == 2 {
|
|
let after = (data.extents_min + data.extents_max) * 0.5;
|
|
let delta = after - before;
|
|
data.origin = data.origin + delta;
|
|
data.ucs_origin = data.ucs_origin + delta;
|
|
}
|
|
}
|
|
ExtendedEntityData::PointCloudEx(data) => {
|
|
let before = (data.extents_min + data.extents_max) * 0.5;
|
|
move_extents(
|
|
&mut data.extents_min,
|
|
&mut data.extents_max,
|
|
grip_id,
|
|
apply,
|
|
);
|
|
if grip_id == 2 {
|
|
let after = (data.extents_min + data.extents_max) * 0.5;
|
|
let delta = after - before;
|
|
data.ucs_origin = data.ucs_origin + delta;
|
|
for crop in &mut data.croppings {
|
|
for point in &mut crop.points {
|
|
*point = *point + delta;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn section_center(data: &SectionObjectData) -> Option<Vector3> {
|
|
let (first, last) = data.vertices.first().zip(data.vertices.last())?;
|
|
Some((*first + *last) * 0.5)
|
|
}
|
|
|
|
fn section_plane_edge_grips(data: &SectionObjectData) -> Vec<GripDef> {
|
|
let Some((first, last)) = data.vertices.first().zip(data.vertices.last()) else {
|
|
return Vec::new();
|
|
};
|
|
let center = (*first + *last) * 0.5;
|
|
let tangent = section_tangent(data);
|
|
let vertical = normalized(data.vertical_direction, Vector3::UNIT_Z);
|
|
let edge_center_offset = (data.top_height - data.bottom_height) * 0.5;
|
|
let definitions = [
|
|
(
|
|
SECTION_GRIP_LEFT,
|
|
*first + vertical * edge_center_offset,
|
|
vertical,
|
|
None,
|
|
),
|
|
(
|
|
SECTION_GRIP_RIGHT,
|
|
*last + vertical * edge_center_offset,
|
|
vertical,
|
|
None,
|
|
),
|
|
(
|
|
SECTION_GRIP_TOP,
|
|
center + vertical * data.top_height,
|
|
tangent,
|
|
Some(vertical),
|
|
),
|
|
(
|
|
SECTION_GRIP_BOTTOM,
|
|
center - vertical * data.bottom_height,
|
|
tangent,
|
|
Some(vertical),
|
|
),
|
|
];
|
|
definitions
|
|
.into_iter()
|
|
.map(|(id, world, edge, axis)| GripDef {
|
|
id,
|
|
world: glam::DVec3::new(world.x, world.y, world.z),
|
|
is_midpoint: false,
|
|
shape: crate::scene::model::object::GripShape::Rectangle,
|
|
dir: Some(glam::DVec3::new(edge.x, edge.y, edge.z)),
|
|
axis: axis.map(|axis| glam::DVec3::new(axis.x, axis.y, axis.z)),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn apply_section_plane_edge_grip(
|
|
data: &mut SectionObjectData,
|
|
grip_id: usize,
|
|
apply: GripApply,
|
|
) {
|
|
let Some((first, last)) = data.vertices.first().zip(data.vertices.last()) else {
|
|
return;
|
|
};
|
|
let center = (*first + *last) * 0.5;
|
|
let tangent = section_tangent(data);
|
|
let vertical = normalized(data.vertical_direction, Vector3::UNIT_Z);
|
|
let edge_center_offset = (data.top_height - data.bottom_height) * 0.5;
|
|
let current = match grip_id {
|
|
SECTION_GRIP_LEFT => *first + vertical * edge_center_offset,
|
|
SECTION_GRIP_RIGHT => *last + vertical * edge_center_offset,
|
|
SECTION_GRIP_TOP => center + vertical * data.top_height,
|
|
SECTION_GRIP_BOTTOM => center - vertical * data.bottom_height,
|
|
_ => return,
|
|
};
|
|
let delta = match apply {
|
|
GripApply::Translate(delta) => Vector3::new(delta.x, delta.y, delta.z),
|
|
GripApply::Absolute(position) => {
|
|
Vector3::new(position.x, position.y, position.z) - current
|
|
}
|
|
};
|
|
let span = (*last - *first).length();
|
|
let minimum_span = 1.0e-6;
|
|
let in_plane_vertical = normalized(
|
|
vertical - tangent * vertical.dot(&tangent),
|
|
vertical,
|
|
);
|
|
let vertical_change = in_plane_vertical * delta.dot(&in_plane_vertical);
|
|
match grip_id {
|
|
SECTION_GRIP_LEFT => {
|
|
let tangent_change = delta
|
|
.dot(&tangent)
|
|
.min((span - minimum_span).max(0.0));
|
|
data.vertices[0] = data.vertices[0] + tangent * tangent_change + vertical_change;
|
|
}
|
|
SECTION_GRIP_RIGHT => {
|
|
let last = data.vertices.len() - 1;
|
|
let tangent_change = delta
|
|
.dot(&tangent)
|
|
.max((-span + minimum_span).min(0.0));
|
|
data.vertices[last] =
|
|
data.vertices[last] + tangent * tangent_change + vertical_change;
|
|
}
|
|
SECTION_GRIP_TOP => {
|
|
data.top_height = (data.top_height + delta.dot(&vertical)).max(0.0);
|
|
}
|
|
SECTION_GRIP_BOTTOM => {
|
|
data.bottom_height = (data.bottom_height - delta.dot(&vertical)).max(0.0);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn entity_transform(transform: &EntityTransform) -> Transform {
|
|
match transform {
|
|
EntityTransform::Translate(delta) => {
|
|
Transform::from_translation(Vector3::new(delta.x, delta.y, delta.z))
|
|
}
|
|
EntityTransform::Rotate { center, axis, angle_rad } => {
|
|
Transform::from_translation(Vector3::new(-center.x, -center.y, -center.z))
|
|
.then(&Transform::from_rotation(
|
|
Vector3::new(axis.x, axis.y, axis.z),
|
|
*angle_rad,
|
|
))
|
|
.then(&Transform::from_translation(Vector3::new(
|
|
center.x, center.y, center.z,
|
|
)))
|
|
}
|
|
EntityTransform::Scale { center, factor } => Transform::from_scaling_with_origin(
|
|
Vector3::new(*factor, *factor, *factor),
|
|
Vector3::new(center.x, center.y, center.z),
|
|
),
|
|
EntityTransform::Mirror { p1, p2, working_normal } => {
|
|
crate::scene::view::transform::reflection_about_working_line(
|
|
*p1,
|
|
*p2,
|
|
*working_normal,
|
|
)
|
|
}
|
|
EntityTransform::Affine(transform) => *transform,
|
|
}
|
|
}
|
|
|
|
fn scalar_scale(transform: &EntityTransform) -> f64 {
|
|
match transform {
|
|
EntityTransform::Scale { factor, .. } => factor.abs(),
|
|
_ => 1.0,
|
|
}
|
|
}
|
|
|
|
fn transform_extents(min: Vector3, max: Vector3, transform: &Transform) -> (Vector3, Vector3) {
|
|
let corners = [
|
|
Vector3::new(min.x, min.y, min.z),
|
|
Vector3::new(max.x, min.y, min.z),
|
|
Vector3::new(max.x, max.y, min.z),
|
|
Vector3::new(min.x, max.y, min.z),
|
|
Vector3::new(min.x, min.y, max.z),
|
|
Vector3::new(max.x, min.y, max.z),
|
|
Vector3::new(max.x, max.y, max.z),
|
|
Vector3::new(min.x, max.y, max.z),
|
|
];
|
|
let mut out_min = Vector3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
|
|
let mut out_max = Vector3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
|
|
for point in corners.map(|point| transform.apply(point)) {
|
|
out_min.x = out_min.x.min(point.x);
|
|
out_min.y = out_min.y.min(point.y);
|
|
out_min.z = out_min.z.min(point.z);
|
|
out_max.x = out_max.x.max(point.x);
|
|
out_max.y = out_max.y.max(point.y);
|
|
out_max.z = out_max.z.max(point.z);
|
|
}
|
|
(out_min, out_max)
|
|
}
|
|
|
|
fn transformed_planar_angle(
|
|
normal: Vector3,
|
|
angle: f64,
|
|
transform: &Transform,
|
|
) -> (Vector3, f64) {
|
|
let (axis_x, axis_y, _) = plane_axes(normal);
|
|
let direction = axis_x * angle.cos() + axis_y * angle.sin();
|
|
let new_normal = normalized(transform.apply_rotation(normal), Vector3::UNIT_Z);
|
|
let new_direction = normalized(transform.apply_rotation(direction), Vector3::UNIT_X);
|
|
let (new_x, new_y, _) = plane_axes(new_normal);
|
|
(
|
|
new_normal,
|
|
new_direction.dot(&new_y).atan2(new_direction.dot(&new_x)),
|
|
)
|
|
}
|
|
|
|
fn apply_transform(entity: &mut ExtendedEntity, requested: &EntityTransform) {
|
|
let transform = entity_transform(requested);
|
|
let scale = scalar_scale(requested);
|
|
let slice_depth = section_is_slice(entity)
|
|
.then(|| section_slice_depth(entity).unwrap_or(0.0) * scale);
|
|
match &mut entity.data {
|
|
ExtendedEntityData::SectionObject(data) => {
|
|
for point in data
|
|
.vertices
|
|
.iter_mut()
|
|
.chain(data.back_line_vertices.iter_mut())
|
|
{
|
|
*point = transform.apply(*point);
|
|
}
|
|
data.vertical_direction =
|
|
normalized(transform.apply_rotation(data.vertical_direction), Vector3::UNIT_Z);
|
|
data.top_height *= scale;
|
|
data.bottom_height *= scale;
|
|
}
|
|
ExtendedEntityData::ArcAlignedText(data) => {
|
|
let old_normal = data.normal;
|
|
let (_, start) =
|
|
transformed_planar_angle(old_normal, data.start_angle, &transform);
|
|
let (normal, end) =
|
|
transformed_planar_angle(old_normal, data.end_angle, &transform);
|
|
data.center = transform.apply(data.center);
|
|
data.normal = normal;
|
|
data.start_angle = start;
|
|
data.end_angle = end;
|
|
data.radius *= scale;
|
|
data.text_size *= scale;
|
|
data.offset_from_arc *= scale;
|
|
data.right_offset *= scale;
|
|
data.left_offset *= scale;
|
|
}
|
|
ExtendedEntityData::RemoteText(data) => {
|
|
let (normal, rotation) =
|
|
transformed_planar_angle(data.normal, data.rotation, &transform);
|
|
data.position = transform.apply(data.position);
|
|
data.normal = normal;
|
|
data.rotation = rotation;
|
|
data.height *= scale;
|
|
}
|
|
ExtendedEntityData::GeoPositionMarker(data) => {
|
|
data.position = transform.apply(data.position);
|
|
data.radius *= scale;
|
|
data.landing_gap *= scale;
|
|
if let Some(text) = data.embedded_mtext.as_mut() {
|
|
codec::Entity::apply_transform(text, &transform);
|
|
}
|
|
}
|
|
ExtendedEntityData::CoordinationModel(data) => {
|
|
let mut matrix = codec::types::Matrix4::zero();
|
|
for row in 0..4 {
|
|
for column in 0..4 {
|
|
matrix.m[row][column] = data.transform[row * 4 + column];
|
|
}
|
|
}
|
|
let composed = transform.matrix * matrix;
|
|
for row in 0..4 {
|
|
for column in 0..4 {
|
|
data.transform[row * 4 + column] = composed.m[row][column];
|
|
}
|
|
}
|
|
data.unit_factor *= scale;
|
|
}
|
|
ExtendedEntityData::PointCloud(data) => {
|
|
data.origin = transform.apply(data.origin);
|
|
data.ucs_origin = transform.apply(data.ucs_origin);
|
|
data.ucs_x_direction = normalized(
|
|
transform.apply_rotation(data.ucs_x_direction),
|
|
Vector3::UNIT_X,
|
|
);
|
|
data.ucs_y_direction = normalized(
|
|
transform.apply_rotation(data.ucs_y_direction),
|
|
Vector3::UNIT_Y,
|
|
);
|
|
data.ucs_z_direction = normalized(
|
|
transform.apply_rotation(data.ucs_z_direction),
|
|
Vector3::UNIT_Z,
|
|
);
|
|
(data.extents_min, data.extents_max) =
|
|
transform_extents(data.extents_min, data.extents_max, &transform);
|
|
for clip in &mut data.clippings {
|
|
let mut min = Vector3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
|
|
let mut max =
|
|
Vector3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
|
|
for point in &clip.vertices {
|
|
for z in [clip.z_min, clip.z_max] {
|
|
let point = transform.apply(Vector3::new(point.x, point.y, z));
|
|
min.x = min.x.min(point.x);
|
|
min.y = min.y.min(point.y);
|
|
min.z = min.z.min(point.z);
|
|
max.x = max.x.max(point.x);
|
|
max.y = max.y.max(point.y);
|
|
max.z = max.z.max(point.z);
|
|
}
|
|
}
|
|
for point in &mut clip.vertices {
|
|
let transformed = transform.apply(Vector3::new(point.x, point.y, clip.z_min));
|
|
point.x = transformed.x;
|
|
point.y = transformed.y;
|
|
}
|
|
if min.z.is_finite() {
|
|
clip.z_min = min.z;
|
|
clip.z_max = max.z;
|
|
}
|
|
}
|
|
}
|
|
ExtendedEntityData::PointCloudEx(data) => {
|
|
data.ucs_origin = transform.apply(data.ucs_origin);
|
|
data.ucs_x_direction = normalized(
|
|
transform.apply_rotation(data.ucs_x_direction),
|
|
Vector3::UNIT_X,
|
|
);
|
|
data.ucs_y_direction = normalized(
|
|
transform.apply_rotation(data.ucs_y_direction),
|
|
Vector3::UNIT_Y,
|
|
);
|
|
data.ucs_z_direction = normalized(
|
|
transform.apply_rotation(data.ucs_z_direction),
|
|
Vector3::UNIT_Z,
|
|
);
|
|
(data.extents_min, data.extents_max) =
|
|
transform_extents(data.extents_min, data.extents_max, &transform);
|
|
for crop in &mut data.croppings {
|
|
crop.plane =
|
|
normalized(transform.apply_rotation(crop.plane), Vector3::UNIT_Z);
|
|
crop.x_direction = normalized(
|
|
transform.apply_rotation(crop.x_direction),
|
|
Vector3::UNIT_X,
|
|
);
|
|
crop.y_direction = normalized(
|
|
transform.apply_rotation(crop.y_direction),
|
|
Vector3::UNIT_Y,
|
|
);
|
|
for point in &mut crop.points {
|
|
*point = transform.apply(*point);
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
if let Some(depth) = slice_depth {
|
|
set_section_slice_metadata(entity, true, depth);
|
|
}
|
|
}
|
|
|
|
impl RenderConvertible for ExtendedEntity {
|
|
fn to_render(&self, document: &codec::CadDocument) -> Option<RenderEntity> {
|
|
to_render(self, document)
|
|
}
|
|
}
|
|
|
|
impl Grippable for ExtendedEntity {
|
|
fn grips(&self) -> Vec<GripDef> {
|
|
grips(self)
|
|
}
|
|
|
|
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
|
apply_grip(self, grip_id, apply);
|
|
}
|
|
|
|
fn grip_menu(&self, grip_id: usize) -> Vec<GripMenuItem> {
|
|
if grip_id != SECTION_GRIP_STATE {
|
|
return Vec::new();
|
|
}
|
|
let ExtendedEntityData::SectionObject(data) = &self.data else {
|
|
return Vec::new();
|
|
};
|
|
let current = section_kind(self, data);
|
|
vec![
|
|
GripMenuItem {
|
|
label: if current == "Plane" { "✓ Plane" } else { "Plane" },
|
|
action: GripMenuAction::SectionPlane,
|
|
},
|
|
GripMenuItem {
|
|
label: if current == "Slice" { "✓ Slice" } else { "Slice" },
|
|
action: GripMenuAction::SectionSlice,
|
|
},
|
|
GripMenuItem {
|
|
label: if current == "Boundary" {
|
|
"✓ Boundary"
|
|
} else {
|
|
"Boundary"
|
|
},
|
|
action: GripMenuAction::SectionBoundary,
|
|
},
|
|
GripMenuItem {
|
|
label: if current == "Volume" { "✓ Volume" } else { "Volume" },
|
|
action: GripMenuAction::SectionVolume,
|
|
},
|
|
]
|
|
}
|
|
|
|
fn apply_grip_menu(&mut self, grip_id: usize, action: GripMenuAction) {
|
|
if grip_id != SECTION_GRIP_STATE
|
|
|| !matches!(self.data, ExtendedEntityData::SectionObject(_))
|
|
{
|
|
return;
|
|
}
|
|
let value = match action {
|
|
GripMenuAction::SectionPlane => "Plane",
|
|
GripMenuAction::SectionSlice => "Slice",
|
|
GripMenuAction::SectionBoundary => "Boundary",
|
|
GripMenuAction::SectionVolume => "Volume",
|
|
_ => return,
|
|
};
|
|
let ExtendedEntityData::SectionObject(data) = &self.data else {
|
|
return;
|
|
};
|
|
if section_kind(self, data) == value {
|
|
return;
|
|
}
|
|
apply_section_prop(self, "ext_section_state", value);
|
|
}
|
|
}
|
|
|
|
impl PropertyEditable for ExtendedEntity {
|
|
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
|
properties(self)
|
|
}
|
|
|
|
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
|
apply_geom_prop(self, field, value);
|
|
}
|
|
}
|
|
|
|
impl Transformable for ExtendedEntity {
|
|
fn apply_transform(&mut self, transform: &EntityTransform) {
|
|
apply_transform(self, transform);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use codec::entities::EntityCommon;
|
|
use codec::types::Color;
|
|
|
|
#[test]
|
|
fn reselecting_slice_keeps_its_depth() {
|
|
let mut entity = ExtendedEntity {
|
|
common: EntityCommon::new(),
|
|
data: ExtendedEntityData::SectionObject(SectionObjectData {
|
|
state: 1,
|
|
flags: 5,
|
|
name: String::new(),
|
|
vertical_direction: Vector3::UNIT_Z,
|
|
top_height: 1.0,
|
|
bottom_height: 1.0,
|
|
indicator_alpha: 70,
|
|
indicator_color: Color::from_index(9),
|
|
back_line_vertices: Vec::new(),
|
|
vertices: vec![Vector3::ZERO, Vector3::new(60.0, 0.0, 0.0)],
|
|
settings_handle: Handle::NULL,
|
|
}),
|
|
};
|
|
set_section_slice_metadata(&mut entity, true, 0.25);
|
|
|
|
entity.apply_grip_menu(SECTION_GRIP_STATE, GripMenuAction::SectionSlice);
|
|
|
|
assert_eq!(section_slice_depth(&entity), Some(0.25));
|
|
}
|
|
}
|