forked from 1b-agent/opencadstudio-upstream
Merge remote-tracking branch 'origin/main' into merge-test/main-into-constraints
# Conflicts: # src/app/command_driver.rs # src/command.rs # src/ui/ribbon/mod.rs # src/ui/window/options.rs
This commit is contained in:
commit
ad718def3f
124 files changed
+9866
-692
No files matched your search
+79
-9
@@ -364,19 +364,44 @@ impl crate::entities::traits::Grippable for Arc {
|
||||
action: crate::scene::model::object::GripMenuAction,
|
||||
point: glam::DVec3,
|
||||
) -> Option<f64> {
|
||||
use cadkernel::geom2d::{Circle as KernelCircle, Curve as KernelCurve, Vec2};
|
||||
use crate::scene::model::object::GripMenuAction as A;
|
||||
if !matches!(action, A::Lengthen) || self.radius <= 1.0e-9 {
|
||||
if self.radius <= 1.0e-9 {
|
||||
return None;
|
||||
}
|
||||
let (x, y, _) = crate::scene::view::transform::wcs_point_to_ocs(
|
||||
(point.x, point.y, point.z),
|
||||
(self.normal.x, self.normal.y, self.normal.z),
|
||||
);
|
||||
let cursor_angle = (y - self.center.y).atan2(x - self.center.x);
|
||||
let current_sweep = (self.end_angle - self.start_angle).rem_euclid(TAU);
|
||||
let curve = crate::entities::curve::arc_curve(self);
|
||||
let point = curve.plane.project(point.to_array())?;
|
||||
let KernelCurve::Arc(arc) = &curve.curve else {
|
||||
unreachable!("arc entity must produce an arc curve")
|
||||
};
|
||||
if matches!(action, A::Radius) && grip_id == 3 {
|
||||
let radius = Vec2::from(point).distance(Vec2::from(arc.centre));
|
||||
return (radius > 1.0e-9).then_some(radius);
|
||||
}
|
||||
if matches!(action, A::ArcLength) && grip_id == 3 {
|
||||
let middle = Vec2::from(curve.curve.point_at(0.5));
|
||||
let tangent = Vec2::from(curve.curve.tangent_at(0.5)).normalize()?;
|
||||
let length = curve.length() + (Vec2::from(point) - middle).dot(tangent);
|
||||
let circumference = KernelCircle {
|
||||
centre: arc.centre,
|
||||
radius: arc.radius,
|
||||
}
|
||||
.length();
|
||||
return (length > 1.0e-9 && length < circumference - 1.0e-9).then_some(length);
|
||||
}
|
||||
if !matches!(action, A::Lengthen) {
|
||||
return None;
|
||||
}
|
||||
let cursor_angle = KernelCurve::Circle(KernelCircle {
|
||||
centre: arc.centre,
|
||||
radius: arc.radius,
|
||||
})
|
||||
.parameter_at(point)
|
||||
* TAU;
|
||||
let current_sweep = arc.sweep();
|
||||
let desired_sweep = match grip_id {
|
||||
1 => (self.end_angle - cursor_angle).rem_euclid(TAU),
|
||||
2 => (cursor_angle - self.start_angle).rem_euclid(TAU),
|
||||
1 => (arc.end_angle - cursor_angle).rem_euclid(TAU),
|
||||
2 => (cursor_angle - arc.start_angle).rem_euclid(TAU),
|
||||
_ => return None,
|
||||
};
|
||||
if desired_sweep <= 1.0e-9 {
|
||||
@@ -458,3 +483,48 @@ impl crate::entities::traits::MassPropsCalc for acadrust::entities::Arc {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::entities::traits::Grippable;
|
||||
use crate::scene::model::object::GripMenuAction;
|
||||
|
||||
#[test]
|
||||
fn midpoint_grip_drives_radius_and_arc_length_in_entity_plane() {
|
||||
let mut arc = Arc::default();
|
||||
arc.center = acadrust::types::Vector3::new(1.0, 2.0, 3.0);
|
||||
arc.normal = acadrust::types::Vector3::new(0.0, 1.0, 0.0);
|
||||
arc.radius = 2.0;
|
||||
arc.start_angle = 0.0;
|
||||
arc.end_angle = std::f64::consts::FRAC_PI_2;
|
||||
|
||||
let curve = crate::entities::curve::arc_curve(&arc);
|
||||
let middle_angle = std::f64::consts::FRAC_PI_4;
|
||||
let radius_point = curve.plane.point_at([
|
||||
arc.center.x + 5.0 * middle_angle.cos(),
|
||||
arc.center.y + 5.0 * middle_angle.sin(),
|
||||
]);
|
||||
let radius = arc
|
||||
.grip_menu_point_value(3, GripMenuAction::Radius, radius_point.into())
|
||||
.expect("radius value");
|
||||
assert!((radius - 5.0).abs() < 1.0e-9);
|
||||
|
||||
let middle = glam::DVec3::from_array(curve.point_at(0.5));
|
||||
let tangent = glam::DVec3::from_array(curve.tangent_at(0.5)).normalize();
|
||||
let wanted_length = curve.length() + 1.25;
|
||||
let arc_length = arc
|
||||
.grip_menu_point_value(
|
||||
3,
|
||||
GripMenuAction::ArcLength,
|
||||
middle + tangent * 1.25,
|
||||
)
|
||||
.expect("arc length value");
|
||||
assert!((arc_length - wanted_length).abs() < 1.0e-9);
|
||||
|
||||
arc.apply_grip_menu_value(3, GripMenuAction::ArcLength, arc_length);
|
||||
assert!(
|
||||
(crate::entities::curve::arc_curve(&arc).length() - wanted_length).abs() < 1.0e-9
|
||||
);
|
||||
}
|
||||
}
|
||||
+105
-2
@@ -256,7 +256,89 @@ impl RenderConvertible for Circle {
|
||||
}
|
||||
}
|
||||
|
||||
crate::impl_entity_basics!(Circle);
|
||||
impl crate::entities::traits::Grippable for Circle {
|
||||
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<crate::scene::model::object::GripMenuItem> {
|
||||
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
|
||||
let mut items = vec![GripMenuItem {
|
||||
label: "Stretch",
|
||||
action: GripMenuAction::Stretch,
|
||||
}];
|
||||
if (1..=4).contains(&grip_id) {
|
||||
items.push(GripMenuItem {
|
||||
label: "Radius",
|
||||
action: GripMenuAction::Radius,
|
||||
});
|
||||
}
|
||||
items
|
||||
}
|
||||
|
||||
fn grip_menu_value_prompt(
|
||||
&self,
|
||||
_grip_id: usize,
|
||||
action: crate::scene::model::object::GripMenuAction,
|
||||
) -> Option<&'static str> {
|
||||
matches!(action, crate::scene::model::object::GripMenuAction::Radius)
|
||||
.then_some("New radius")
|
||||
}
|
||||
|
||||
fn grip_menu_point_value(
|
||||
&self,
|
||||
grip_id: usize,
|
||||
action: crate::scene::model::object::GripMenuAction,
|
||||
point: glam::DVec3,
|
||||
) -> Option<f64> {
|
||||
if !matches!(action, crate::scene::model::object::GripMenuAction::Radius)
|
||||
|| !(1..=4).contains(&grip_id)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let plane = crate::entities::curve::circle_curve(self).plane;
|
||||
let point = plane.project(point.to_array())?;
|
||||
let radius = (point[0] - self.center.x).hypot(point[1] - self.center.y);
|
||||
(radius > 1.0e-9).then_some(radius)
|
||||
}
|
||||
|
||||
fn apply_grip_menu_value(
|
||||
&mut self,
|
||||
grip_id: usize,
|
||||
action: crate::scene::model::object::GripMenuAction,
|
||||
value: f64,
|
||||
) {
|
||||
if matches!(action, crate::scene::model::object::GripMenuAction::Radius)
|
||||
&& (1..=4).contains(&grip_id)
|
||||
&& value > 1.0e-9
|
||||
{
|
||||
self.radius = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::entities::traits::PropertyEditable for Circle {
|
||||
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 crate::entities::traits::Transformable for Circle {
|
||||
fn apply_transform(&mut self, transform: &EntityTransform) {
|
||||
apply_transform(self, transform);
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::entities::traits::MassPropsCalc for Circle {
|
||||
fn mass_props(&self) -> crate::entities::traits::MassProps {
|
||||
@@ -276,6 +358,28 @@ impl crate::entities::traits::MassPropsCalc for Circle {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn quadrant_grips_offer_interactive_radius() {
|
||||
use crate::entities::traits::Grippable;
|
||||
use crate::scene::model::object::GripMenuAction;
|
||||
|
||||
let mut circle = Circle::default();
|
||||
circle.radius = 2.0;
|
||||
|
||||
assert_eq!(circle.grip_menu(0).len(), 1);
|
||||
assert!(circle
|
||||
.grip_menu(1)
|
||||
.iter()
|
||||
.any(|item| item.action == GripMenuAction::Radius));
|
||||
assert_eq!(
|
||||
circle.grip_menu_value_prompt(1, GripMenuAction::Radius),
|
||||
Some("New radius")
|
||||
);
|
||||
|
||||
circle.apply_grip_menu_value(1, GripMenuAction::Radius, 5.0);
|
||||
assert_eq!(circle.radius, 5.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_circle_segments_scales_with_zoom() {
|
||||
let r = 10.0;
|
||||
@@ -361,4 +465,3 @@ mod tests {
|
||||
assert!(close_count > far_count, "{close_count} <= {far_count}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ use std::collections::HashSet;
|
||||
use crate::scene::model::object::{GripDef, GripShape, PropValue, Property};
|
||||
use crate::scene::model::wire_model::PatternStationPiece;
|
||||
|
||||
pub const VARIES_LABEL: &str = "*VARIES*";
|
||||
|
||||
/// Linear / angular unit format pulled from the document header so the
|
||||
/// per-thread properties pipeline can format values consistently without
|
||||
/// passing the document through every callsite.
|
||||
|
||||
+287
-11
@@ -13,13 +13,19 @@ use crate::entities::common::{
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
|
||||
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
|
||||
use crate::scene::model::object::{
|
||||
GripApply, GripDef, PropSection, PropValue, Property,
|
||||
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
|
||||
@@ -1563,15 +1569,56 @@ fn move_extents(
|
||||
|
||||
fn grips(entity: &ExtendedEntity) -> Vec<GripDef> {
|
||||
match &entity.data {
|
||||
ExtendedEntityData::SectionObject(data) => 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(),
|
||||
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),
|
||||
@@ -1643,7 +1690,41 @@ fn grips(entity: &ExtendedEntity) -> Vec<GripDef> {
|
||||
fn apply_grip(entity: &mut ExtendedEntity, grip_id: usize, apply: GripApply) {
|
||||
match &mut entity.data {
|
||||
ExtendedEntityData::SectionObject(data) => {
|
||||
if grip_id < data.vertices.len() {
|
||||
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
|
||||
@@ -1704,6 +1785,115 @@ fn apply_grip(entity: &mut ExtendedEntity, grip_id: usize, apply: GripApply) {
|
||||
}
|
||||
}
|
||||
|
||||
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) => {
|
||||
@@ -1943,6 +2133,60 @@ impl Grippable for ExtendedEntity {
|
||||
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 {
|
||||
@@ -1960,3 +2204,35 @@ impl Transformable for ExtendedEntity {
|
||||
apply_transform(self, transform);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use acadrust::entities::EntityCommon;
|
||||
use acadrust::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));
|
||||
}
|
||||
}
|
||||
+158
-18
@@ -6,7 +6,7 @@ use crate::t;
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{
|
||||
edit_prop as edit, edit_scalar_prop as edit_scalar, format_area, format_length, parse_f64,
|
||||
rectangle_grip, ro_prop as ro, square_grip, stepper_prop as stepper,
|
||||
rectangle_grip, ro_prop as ro, square_grip, stepper_prop as stepper, VARIES_LABEL,
|
||||
};
|
||||
use crate::entities::traits::RenderConvertible;
|
||||
use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject};
|
||||
@@ -18,14 +18,38 @@ const REVCLOUD_BULGE: f64 = 0.5;
|
||||
const MAX_REVCLOUD_VERTICES: usize = 100_000;
|
||||
const WIDTH_EPSILON: f64 = 1.0e-9;
|
||||
|
||||
fn effective_width(width: f64, constant_width: f64) -> f64 {
|
||||
if width > WIDTH_EPSILON {
|
||||
width
|
||||
pub(crate) fn vertex_segment_widths(vertex: &LwVertex, constant_width: f64) -> (f64, f64) {
|
||||
if vertex.start_width > WIDTH_EPSILON || vertex.end_width > WIDTH_EPSILON {
|
||||
(vertex.start_width, vertex.end_width)
|
||||
} else {
|
||||
constant_width
|
||||
(constant_width, constant_width)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn lwpolyline_global_width(pline: &LwPolyline) -> Option<f64> {
|
||||
let count = pline.vertices.len();
|
||||
let seg_count = if pline.is_closed {
|
||||
count
|
||||
} else {
|
||||
count.saturating_sub(1)
|
||||
};
|
||||
if seg_count == 0 {
|
||||
return Some(pline.constant_width);
|
||||
}
|
||||
let (w0_start, w0_end) = vertex_segment_widths(&pline.vertices[0], pline.constant_width);
|
||||
if (w0_start - w0_end).abs() > 1e-6 {
|
||||
return None;
|
||||
}
|
||||
let w0 = w0_start;
|
||||
for i in 1..seg_count {
|
||||
let (sw, ew) = vertex_segment_widths(&pline.vertices[i], pline.constant_width);
|
||||
if (sw - ew).abs() > 1e-6 || (sw - w0).abs() > 1e-6 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(w0)
|
||||
}
|
||||
|
||||
/// Midpoint position on an arc segment defined by its bulge.
|
||||
fn arc_midpoint(p0: [f64; 2], p1: [f64; 2], bulge: f64) -> [f64; 2] {
|
||||
match crate::entities::common::BulgeArc::from_bulge(p0, p1, bulge) {
|
||||
@@ -504,8 +528,7 @@ fn band_verts(pline: &LwPolyline) -> Vec<([f64; 2], f64, f64, f64)> {
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|v| {
|
||||
let sw = effective_width(v.start_width, c);
|
||||
let ew = effective_width(v.end_width, c);
|
||||
let (sw, ew) = vertex_segment_widths(v, c);
|
||||
([v.location.x, v.location.y], v.bulge, sw, ew)
|
||||
})
|
||||
.collect()
|
||||
@@ -899,11 +922,8 @@ fn properties(pline: &LwPolyline) -> Vec<PropSection> {
|
||||
let v = pline.vertices.get(vi);
|
||||
let vx = v.map_or(0.0, |v| v.location.x);
|
||||
let vy = v.map_or(0.0, |v| v.location.y);
|
||||
let start_w = v.map_or(0.0, |v| {
|
||||
effective_width(v.start_width, pline.constant_width)
|
||||
});
|
||||
let end_w = v.map_or(0.0, |v| {
|
||||
effective_width(v.end_width, pline.constant_width)
|
||||
let (start_w, end_w) = v.map_or((0.0, 0.0), |v| {
|
||||
vertex_segment_widths(v, pline.constant_width)
|
||||
});
|
||||
let mp = <LwPolyline as crate::entities::traits::MassPropsCalc>::mass_props(pline);
|
||||
let cloud_arc_length = revision_cloud_arc_length(pline);
|
||||
@@ -937,6 +957,14 @@ fn properties(pline: &LwPolyline) -> Vec<PropSection> {
|
||||
arc_length,
|
||||
));
|
||||
}
|
||||
let global_width_prop = match lwpolyline_global_width(pline) {
|
||||
Some(gw) => edit(t!("Global width").as_ref(), "global_width", gw),
|
||||
None => Property {
|
||||
label: t!("Global width").into_owned(),
|
||||
field: "global_width",
|
||||
value: PropValue::EditText(VARIES_LABEL.to_string()),
|
||||
},
|
||||
};
|
||||
let mut geometry_props = vec![
|
||||
stepper(t!("Current Vertex").as_ref(), "current_vertex", vertex_label),
|
||||
edit(t!("Vertex X").as_ref(), "vertex_x", vx),
|
||||
@@ -948,7 +976,7 @@ fn properties(pline: &LwPolyline) -> Vec<PropSection> {
|
||||
geometry_props.push(edit(t!("End segment width").as_ref(), "end_width", end_w));
|
||||
}
|
||||
geometry_props.extend([
|
||||
edit(t!("Global width").as_ref(), "global_width", pline.constant_width),
|
||||
global_width_prop,
|
||||
edit(t!("Elevation").as_ref(), "elevation", pline.elevation),
|
||||
ro(t!("Area").as_ref(), "area", format_area(mp.area)),
|
||||
ro(t!("Length").as_ref(), "length", format_length(mp.perimeter)),
|
||||
@@ -1015,7 +1043,13 @@ fn apply_geom_prop(pline: &mut LwPolyline, field: &str, value: &str) {
|
||||
};
|
||||
match field {
|
||||
"elevation" => pline.elevation = v,
|
||||
"global_width" if v.is_finite() && v >= 0.0 => pline.constant_width = v,
|
||||
"global_width" if v.is_finite() && v >= 0.0 => {
|
||||
pline.constant_width = v;
|
||||
for vtx in &mut pline.vertices {
|
||||
vtx.start_width = v;
|
||||
vtx.end_width = v;
|
||||
}
|
||||
}
|
||||
"vertex_x" => {
|
||||
if let Some(vtx) = pline.vertices.get_mut(vi) {
|
||||
vtx.location.x = v;
|
||||
@@ -1027,11 +1061,31 @@ fn apply_geom_prop(pline: &mut LwPolyline, field: &str, value: &str) {
|
||||
}
|
||||
}
|
||||
"start_width" if v.is_finite() && v >= 0.0 => {
|
||||
if pline.constant_width > WIDTH_EPSILON {
|
||||
let c = pline.constant_width;
|
||||
for vtx in &mut pline.vertices {
|
||||
if vtx.start_width <= WIDTH_EPSILON && vtx.end_width <= WIDTH_EPSILON {
|
||||
vtx.start_width = c;
|
||||
vtx.end_width = c;
|
||||
}
|
||||
}
|
||||
pline.constant_width = 0.0;
|
||||
}
|
||||
if let Some(vtx) = pline.vertices.get_mut(vi) {
|
||||
vtx.start_width = v;
|
||||
}
|
||||
}
|
||||
"end_width" if v.is_finite() && v >= 0.0 => {
|
||||
if pline.constant_width > WIDTH_EPSILON {
|
||||
let c = pline.constant_width;
|
||||
for vtx in &mut pline.vertices {
|
||||
if vtx.start_width <= WIDTH_EPSILON && vtx.end_width <= WIDTH_EPSILON {
|
||||
vtx.start_width = c;
|
||||
vtx.end_width = c;
|
||||
}
|
||||
}
|
||||
pline.constant_width = 0.0;
|
||||
}
|
||||
if let Some(vtx) = pline.vertices.get_mut(vi) {
|
||||
vtx.end_width = v;
|
||||
}
|
||||
@@ -1252,8 +1306,8 @@ impl crate::entities::traits::Grippable for LwPolyline {
|
||||
new_v.location.x = midpoint[0];
|
||||
new_v.location.y = midpoint[1];
|
||||
new_v.vertex_id = 0;
|
||||
let effective_start = effective_width(v0.start_width, self.constant_width);
|
||||
let effective_end = effective_width(v0.end_width, self.constant_width);
|
||||
let (effective_start, effective_end) =
|
||||
vertex_segment_widths(&v0, self.constant_width);
|
||||
let middle_width = (effective_start + effective_end) * 0.5;
|
||||
self.vertices[i0].end_width = middle_width;
|
||||
new_v.start_width = middle_width;
|
||||
@@ -1325,8 +1379,9 @@ pub(crate) fn wide_fills(pl: &acadrust::entities::LwPolyline) -> ([f64; 2], Vec<
|
||||
for i in 0..seg_count {
|
||||
let v0 = &verts[i];
|
||||
let v1 = &verts[(i + 1) % n];
|
||||
let hw0 = effective_width(v0.start_width, pl.constant_width) as f32 * 0.5;
|
||||
let hw1 = effective_width(v0.end_width, pl.constant_width) as f32 * 0.5;
|
||||
let (sw, ew) = vertex_segment_widths(v0, pl.constant_width);
|
||||
let hw0 = sw as f32 * 0.5;
|
||||
let hw1 = ew as f32 * 0.5;
|
||||
if hw0 < 1e-6 && hw1 < 1e-6 {
|
||||
continue;
|
||||
}
|
||||
@@ -1381,3 +1436,88 @@ impl crate::entities::traits::MassPropsCalc for acadrust::entities::LwPolyline {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::entities::traits::PropertyEditable;
|
||||
use acadrust::entities::{LwPolyline, LwVertex};
|
||||
use acadrust::Vector2;
|
||||
|
||||
fn make_test_lwpolyline(count: usize, constant_width: f64) -> LwPolyline {
|
||||
let mut pl = LwPolyline::default();
|
||||
pl.constant_width = constant_width;
|
||||
for i in 0..count {
|
||||
pl.vertices.push(LwVertex::new(Vector2::new(i as f64 * 10.0, 0.0)));
|
||||
}
|
||||
pl
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lwpolyline_uniform_width_shows_global_width() {
|
||||
let pl = make_test_lwpolyline(3, 5.0);
|
||||
assert_eq!(lwpolyline_global_width(&pl), Some(5.0));
|
||||
|
||||
let props = pl.geometry_properties(&[]);
|
||||
let geom_props = &props[0].props;
|
||||
let gw = geom_props.iter().find(|p| p.field == "global_width").unwrap();
|
||||
match &gw.value {
|
||||
PropValue::EditText(val) => assert_eq!(parse_f64(val), Some(5.0)),
|
||||
_ => panic!("expected EditText"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lwpolyline_varying_vertex_width_shows_varies() {
|
||||
let mut pl = make_test_lwpolyline(3, 5.0);
|
||||
pl.vertices[0].start_width = 2.0;
|
||||
pl.vertices[0].end_width = 3.0;
|
||||
|
||||
assert_eq!(lwpolyline_global_width(&pl), None);
|
||||
|
||||
let props = pl.geometry_properties(&[]);
|
||||
let geom_props = &props[0].props;
|
||||
let gw = geom_props.iter().find(|p| p.field == "global_width").unwrap();
|
||||
match &gw.value {
|
||||
PropValue::EditText(val) => assert_eq!(val, VARIES_LABEL),
|
||||
_ => panic!("expected EditText with VARIES_LABEL"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lwpolyline_modifying_global_width_sets_all_vertex_widths() {
|
||||
let mut pl = make_test_lwpolyline(3, 5.0);
|
||||
pl.vertices[0].start_width = 2.0;
|
||||
pl.vertices[0].end_width = 3.0;
|
||||
|
||||
pl.apply_geom_prop("global_width", "10.0");
|
||||
|
||||
assert_eq!(pl.constant_width, 10.0);
|
||||
for v in &pl.vertices {
|
||||
assert_eq!(v.start_width, 10.0);
|
||||
assert_eq!(v.end_width, 10.0);
|
||||
}
|
||||
assert_eq!(lwpolyline_global_width(&pl), Some(10.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lwpolyline_modifying_segment_width_materializes_constant_width() {
|
||||
let mut pl = make_test_lwpolyline(3, 5.0);
|
||||
// Initially constant_width is 5.0, vertices start/end are 0.0
|
||||
assert_eq!(pl.vertices[0].start_width, 0.0);
|
||||
|
||||
// Edit start width of current vertex (vertex 0) to 2.0
|
||||
pl.apply_geom_prop("start_width", "2.0");
|
||||
|
||||
// Vertex 0 start width should be 2.0, end width materialized to 5.0
|
||||
assert_eq!(pl.vertices[0].start_width, 2.0);
|
||||
assert_eq!(pl.vertices[0].end_width, 5.0);
|
||||
|
||||
// Vertex 1 and 2 should have materialized 5.0 instead of collapsing to 0.0
|
||||
assert_eq!(pl.vertices[1].start_width, 5.0);
|
||||
assert_eq!(pl.vertices[1].end_width, 5.0);
|
||||
|
||||
// Global width must now be VARIES
|
||||
assert_eq!(lwpolyline_global_width(&pl), None);
|
||||
}
|
||||
}
|
||||
+179
-11
@@ -4,7 +4,7 @@ use crate::t;
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{
|
||||
edit_prop as edit, format_length, parse_f64, ro_prop as ro, square_grip,
|
||||
stepper_prop as stepper,
|
||||
stepper_prop as stepper, VARIES_LABEL,
|
||||
};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
|
||||
use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject};
|
||||
@@ -609,6 +609,51 @@ impl Grippable for Polyline2D {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn polyline2d_vertex_segment_widths(
|
||||
vertex: &acadrust::entities::Vertex2D,
|
||||
default_start: f64,
|
||||
default_end: f64,
|
||||
) -> (f64, f64) {
|
||||
if vertex.start_width > 1e-9 || vertex.end_width > 1e-9 {
|
||||
(vertex.start_width, vertex.end_width)
|
||||
} else {
|
||||
(default_start, default_end)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn polyline2d_global_width(pline: &Polyline2D) -> Option<f64> {
|
||||
let filtered = drawn_vertices2d(pline);
|
||||
let verts: &[acadrust::entities::Vertex2D] =
|
||||
filtered.as_deref().unwrap_or(&pline.vertices);
|
||||
let count = verts.len();
|
||||
let seg_count = if pline.is_closed() {
|
||||
count
|
||||
} else {
|
||||
count.saturating_sub(1)
|
||||
};
|
||||
if seg_count == 0 {
|
||||
if (pline.start_width - pline.end_width).abs() < 1e-6 {
|
||||
return Some(pline.start_width);
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let (w0_start, w0_end) =
|
||||
polyline2d_vertex_segment_widths(&verts[0], pline.start_width, pline.end_width);
|
||||
if (w0_start - w0_end).abs() > 1e-6 {
|
||||
return None;
|
||||
}
|
||||
let w0 = w0_start;
|
||||
for i in 1..seg_count {
|
||||
let (sw, ew) =
|
||||
polyline2d_vertex_segment_widths(&verts[i], pline.start_width, pline.end_width);
|
||||
if (sw - ew).abs() > 1e-6 || (sw - w0).abs() > 1e-6 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(w0)
|
||||
}
|
||||
|
||||
impl PropertyEditable for Polyline2D {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
||||
let n = self.vertices.len();
|
||||
@@ -631,14 +676,24 @@ impl PropertyEditable for Polyline2D {
|
||||
let v = self.vertices.get(vi);
|
||||
let vertex_x = v.map(|v| v.location.x).unwrap_or_default();
|
||||
let vertex_y = v.map(|v| v.location.y).unwrap_or_default();
|
||||
let seg_start_w = v.map(|v| v.start_width).unwrap_or_default();
|
||||
let seg_end_w = v.map(|v| v.end_width).unwrap_or_default();
|
||||
let (seg_start_w, seg_end_w) = v.map_or((0.0, 0.0), |v| {
|
||||
polyline2d_vertex_segment_widths(v, self.start_width, self.end_width)
|
||||
});
|
||||
let vertex_label = if n == 0 {
|
||||
"—".to_string()
|
||||
} else {
|
||||
format!("{} / {}", vi + 1, n)
|
||||
};
|
||||
|
||||
let global_width_prop = match polyline2d_global_width(self) {
|
||||
Some(gw) => edit(t!("Global width").as_ref(), "pl2_start_w", gw),
|
||||
None => Property {
|
||||
label: t!("Global width").into_owned(),
|
||||
field: "pl2_start_w",
|
||||
value: PropValue::EditText(VARIES_LABEL.to_string()),
|
||||
},
|
||||
};
|
||||
|
||||
vec![
|
||||
PropSection {
|
||||
title: t!("Geometry").into_owned(),
|
||||
@@ -648,7 +703,7 @@ impl PropertyEditable for Polyline2D {
|
||||
edit(t!("Vertex Y").as_ref(), "pl2_vertex_y", vertex_y),
|
||||
edit(t!("Start segment width").as_ref(), "pl2_seg_start_w", seg_start_w),
|
||||
edit(t!("End segment width").as_ref(), "pl2_seg_end_w", seg_end_w),
|
||||
edit(t!("Global width").as_ref(), "pl2_start_w", self.start_width),
|
||||
global_width_prop,
|
||||
edit(t!("Elevation").as_ref(), "pl2_elevation", self.elevation),
|
||||
ro(t!("Area").as_ref(), "pl2_area", format!("{area:.4}")),
|
||||
ro(t!("Length").as_ref(), "pl2_length", format!("{length:.4}")),
|
||||
@@ -706,9 +761,13 @@ impl PropertyEditable for Polyline2D {
|
||||
}
|
||||
"pl2_start_w" => {
|
||||
if let Some(v) = parse_f64(value) {
|
||||
if v >= 0.0 {
|
||||
if v.is_finite() && v >= 0.0 {
|
||||
self.start_width = v;
|
||||
self.end_width = v;
|
||||
for vtx in &mut self.vertices {
|
||||
vtx.start_width = v;
|
||||
vtx.end_width = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -723,16 +782,44 @@ impl PropertyEditable for Polyline2D {
|
||||
}
|
||||
}
|
||||
"pl2_seg_start_w" => {
|
||||
if let (Some(v), Some(vert)) = (parse_f64(value), self.vertices.get_mut(vi)) {
|
||||
if v >= 0.0 {
|
||||
vert.start_width = v;
|
||||
if let Some(v) = parse_f64(value) {
|
||||
if v.is_finite() && v >= 0.0 {
|
||||
if self.start_width > 1e-9 || self.end_width > 1e-9 {
|
||||
let def_s = self.start_width;
|
||||
let def_e = self.end_width;
|
||||
for vtx in &mut self.vertices {
|
||||
if vtx.start_width <= 1e-9 && vtx.end_width <= 1e-9 {
|
||||
vtx.start_width = def_s;
|
||||
vtx.end_width = def_e;
|
||||
}
|
||||
}
|
||||
self.start_width = 0.0;
|
||||
self.end_width = 0.0;
|
||||
}
|
||||
if let Some(vert) = self.vertices.get_mut(vi) {
|
||||
vert.start_width = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"pl2_seg_end_w" => {
|
||||
if let (Some(v), Some(vert)) = (parse_f64(value), self.vertices.get_mut(vi)) {
|
||||
if v >= 0.0 {
|
||||
vert.end_width = v;
|
||||
if let Some(v) = parse_f64(value) {
|
||||
if v.is_finite() && v >= 0.0 {
|
||||
if self.start_width > 1e-9 || self.end_width > 1e-9 {
|
||||
let def_s = self.start_width;
|
||||
let def_e = self.end_width;
|
||||
for vtx in &mut self.vertices {
|
||||
if vtx.start_width <= 1e-9 && vtx.end_width <= 1e-9 {
|
||||
vtx.start_width = def_s;
|
||||
vtx.end_width = def_e;
|
||||
}
|
||||
}
|
||||
self.start_width = 0.0;
|
||||
self.end_width = 0.0;
|
||||
}
|
||||
if let Some(vert) = self.vertices.get_mut(vi) {
|
||||
vert.end_width = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1331,3 +1418,84 @@ pub(crate) fn wide_fills(pl: &acadrust::entities::Polyline2D) -> ([f64; 2], Vec<
|
||||
}
|
||||
(origin, out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::entities::traits::PropertyEditable;
|
||||
use acadrust::entities::{Polyline2D, Vertex2D};
|
||||
use acadrust::Vector3;
|
||||
|
||||
fn make_test_polyline2d(count: usize, start_w: f64, end_w: f64) -> Polyline2D {
|
||||
let mut pl = Polyline2D::default();
|
||||
pl.start_width = start_w;
|
||||
pl.end_width = end_w;
|
||||
for i in 0..count {
|
||||
pl.vertices.push(Vertex2D::new(Vector3::new(i as f64 * 10.0, 0.0, 0.0)));
|
||||
}
|
||||
pl
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_polyline2d_uniform_width_shows_global_width() {
|
||||
let pl = make_test_polyline2d(3, 4.0, 4.0);
|
||||
assert_eq!(polyline2d_global_width(&pl), Some(4.0));
|
||||
|
||||
let props = pl.geometry_properties(&[]);
|
||||
let geom_props = &props[0].props;
|
||||
let gw = geom_props.iter().find(|p| p.field == "pl2_start_w").unwrap();
|
||||
match &gw.value {
|
||||
PropValue::EditText(val) => assert_eq!(parse_f64(val), Some(4.0)),
|
||||
_ => panic!("expected EditText"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_polyline2d_varying_vertex_width_shows_varies() {
|
||||
let mut pl = make_test_polyline2d(3, 4.0, 4.0);
|
||||
pl.vertices[0].start_width = 1.0;
|
||||
pl.vertices[0].end_width = 2.0;
|
||||
|
||||
assert_eq!(polyline2d_global_width(&pl), None);
|
||||
|
||||
let props = pl.geometry_properties(&[]);
|
||||
let geom_props = &props[0].props;
|
||||
let gw = geom_props.iter().find(|p| p.field == "pl2_start_w").unwrap();
|
||||
match &gw.value {
|
||||
PropValue::EditText(val) => assert_eq!(val, VARIES_LABEL),
|
||||
_ => panic!("expected EditText with VARIES_LABEL"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_polyline2d_modifying_global_width_sets_all_vertex_widths() {
|
||||
let mut pl = make_test_polyline2d(3, 4.0, 4.0);
|
||||
pl.vertices[0].start_width = 1.0;
|
||||
pl.vertices[0].end_width = 2.0;
|
||||
|
||||
pl.apply_geom_prop("pl2_start_w", "8.0");
|
||||
|
||||
assert_eq!(pl.start_width, 8.0);
|
||||
assert_eq!(pl.end_width, 8.0);
|
||||
for v in &pl.vertices {
|
||||
assert_eq!(v.start_width, 8.0);
|
||||
assert_eq!(v.end_width, 8.0);
|
||||
}
|
||||
assert_eq!(polyline2d_global_width(&pl), Some(8.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_polyline2d_modifying_segment_width_materializes_default_width() {
|
||||
let mut pl = make_test_polyline2d(3, 4.0, 4.0);
|
||||
assert_eq!(pl.vertices[0].start_width, 0.0);
|
||||
|
||||
pl.apply_geom_prop("pl2_seg_start_w", "1.5");
|
||||
|
||||
assert_eq!(pl.vertices[0].start_width, 1.5);
|
||||
assert_eq!(pl.vertices[0].end_width, 4.0);
|
||||
assert_eq!(pl.vertices[1].start_width, 4.0);
|
||||
assert_eq!(pl.vertices[1].end_width, 4.0);
|
||||
|
||||
assert_eq!(polyline2d_global_width(&pl), None);
|
||||
}
|
||||
}
|
||||
+83
-1
@@ -3,6 +3,7 @@
|
||||
// Shared grips and properties for modeler entities.
|
||||
|
||||
use acadrust::entities::{Body, Region, Solid3D, Surface};
|
||||
use acadrust::xdata::{ExtendedDataRecord, XDataValue};
|
||||
use cadkernel::space::polygon;
|
||||
use crate::t;
|
||||
use crate::command::EntityTransform;
|
||||
@@ -49,6 +50,71 @@ fn yes_no(value: bool) -> &'static str {
|
||||
if value { "Yes" } else { "No" }
|
||||
}
|
||||
|
||||
pub(crate) const SURFACE_PROPERTIES_APP: &str = "OCS_SURFACE_PROPERTIES";
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct SurfacePropertyState {
|
||||
pub isolines: bool,
|
||||
pub maintain_associativity: bool,
|
||||
pub show_associativity: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn surface_property_state(surface: &Surface) -> SurfacePropertyState {
|
||||
let defaults = SurfacePropertyState {
|
||||
isolines: true,
|
||||
maintain_associativity: surface.history_handle.is_some(),
|
||||
show_associativity: false,
|
||||
};
|
||||
let Some(record) = surface
|
||||
.common
|
||||
.extended_data
|
||||
.get_record(SURFACE_PROPERTIES_APP)
|
||||
else {
|
||||
return defaults;
|
||||
};
|
||||
let mut values = record.values.iter().filter_map(|value| match value {
|
||||
XDataValue::Integer16(value) => Some(*value),
|
||||
_ => None,
|
||||
});
|
||||
let Some(version) = values.next() else {
|
||||
return defaults;
|
||||
};
|
||||
if version != 1 {
|
||||
return defaults;
|
||||
}
|
||||
SurfacePropertyState {
|
||||
isolines: values.next().map_or(defaults.isolines, |value| value == 0),
|
||||
maintain_associativity: values
|
||||
.next()
|
||||
.map_or(defaults.maintain_associativity, |value| value != 0),
|
||||
show_associativity: values
|
||||
.next()
|
||||
.map_or(defaults.show_associativity, |value| value != 0),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn surface_property_xdata_values(state: SurfacePropertyState) -> Vec<XDataValue> {
|
||||
vec![
|
||||
XDataValue::Integer16(1),
|
||||
XDataValue::Integer16(if state.isolines { 0 } else { 1 }),
|
||||
XDataValue::Integer16(state.maintain_associativity as i16),
|
||||
XDataValue::Integer16(state.show_associativity as i16),
|
||||
]
|
||||
}
|
||||
|
||||
fn write_surface_property_state(surface: &mut Surface, state: SurfacePropertyState) {
|
||||
let mut record = ExtendedDataRecord::new(SURFACE_PROPERTIES_APP);
|
||||
record.values = surface_property_xdata_values(state);
|
||||
surface.common.extended_data.upsert_record(record);
|
||||
}
|
||||
|
||||
pub(crate) fn surface_isoline_counts(surface: &Surface) -> [usize; 2] {
|
||||
[
|
||||
surface.u_isolines.max(0) as usize,
|
||||
surface.v_isolines.max(0) as usize,
|
||||
]
|
||||
}
|
||||
|
||||
fn handle_text(handle: Option<acadrust::Handle>) -> String {
|
||||
handle
|
||||
.filter(|handle| handle.is_valid())
|
||||
@@ -700,6 +766,21 @@ impl PropertyEditable for Surface {
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
match field {
|
||||
"srf_wireframe_type" => {
|
||||
let mut state = surface_property_state(self);
|
||||
state.isolines = !value.eq_ignore_ascii_case("Isoparms");
|
||||
write_surface_property_state(self, state);
|
||||
}
|
||||
"srf_maintain_associativity" => {
|
||||
let mut state = surface_property_state(self);
|
||||
state.maintain_associativity = value.eq_ignore_ascii_case("Yes");
|
||||
write_surface_property_state(self, state);
|
||||
}
|
||||
"srf_show_associativity" => {
|
||||
let mut state = surface_property_state(self);
|
||||
state.show_associativity = value.eq_ignore_ascii_case("Yes");
|
||||
write_surface_property_state(self, state);
|
||||
}
|
||||
"srf_u_isolines" => {
|
||||
if let Some(value) = parse_f64(value) {
|
||||
self.u_isolines = (value.round() as i16).max(0);
|
||||
@@ -822,7 +903,8 @@ pub fn tessellate_volume(
|
||||
color,
|
||||
facet_res,
|
||||
chordal_deflection,
|
||||
isolines,
|
||||
surface_isoline_counts(s),
|
||||
surface_property_state(s).isolines,
|
||||
),
|
||||
EntityType::Mesh(_) | EntityType::PolygonMesh(_) | EntityType::PolyfaceMesh(_) => {
|
||||
crate::entities::mesh::tessellate_shaded_mesh(e, color)
|
||||
|
||||
Reference in new issue
Block a user