forked from 1b-agent/opencadstudio-upstream
fix(mesh): complete polygon mesh support
This commit is contained in:
commit
e8cb752fab
48 files changed
+5962
-1701
No files matched your search
+405
-82
@@ -1,106 +1,427 @@
|
||||
// Helix entity (AcDbHelix): a spline curve plus its generating parameters.
|
||||
//
|
||||
// A loaded helix carries a fully evaluated `spline` (the NURBS the file
|
||||
// stored) alongside the parameters that generated it (base point, axis,
|
||||
// radius, turns, turn height, twist, constraint). We render and grip the
|
||||
// entity through its embedded spline, and expose the generating parameters
|
||||
// as the Geometry property group.
|
||||
|
||||
use acadrust::entities::{Helix, HelixConstraint};
|
||||
use crate::t;
|
||||
use acadrust::entities::{Helix, HelixConstraint, Spline};
|
||||
use acadrust::types::Vector3;
|
||||
use cadkernel::space::{HelixCurve, HelixDirection, NurbsCurve3, Vec3};
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::ro_prop as ro;
|
||||
use crate::entities::traits::{Grippable, Transformable, RenderConvertible};
|
||||
use crate::entities::common::{
|
||||
center_grip, edit_prop as edit, format_angle, format_length, parse_f64, ro_prop as ro,
|
||||
square_grip,
|
||||
};
|
||||
use crate::entities::traits::{Grippable, RenderConvertible, Transformable};
|
||||
use crate::scene::convert::acad_to_render::RenderEntity;
|
||||
use crate::scene::model::object::{GripApply, GripDef, PropSection};
|
||||
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||||
use crate::t;
|
||||
|
||||
/// Perpendicular distance from `p` to the helix axis (line through
|
||||
/// `axis_base_point` along `axis_vector`). Used to recover the top radius
|
||||
/// from the last defining point of the spline.
|
||||
fn radius_from_axis(helix: &Helix, p: &acadrust::types::Vector3) -> Option<f64> {
|
||||
let base = &helix.axis_base_point;
|
||||
let axis = helix.axis_vector.normalize();
|
||||
// A zero axis vector cannot define a line.
|
||||
if axis.length() < 1e-9 {
|
||||
return None;
|
||||
const EPSILON: f64 = 1.0e-9;
|
||||
|
||||
fn point(vector: Vector3) -> Vec3 {
|
||||
Vec3::new(vector.x, vector.y, vector.z)
|
||||
}
|
||||
|
||||
fn vector(value: Vec3) -> Vector3 {
|
||||
Vector3::new(value.x, value.y, value.z)
|
||||
}
|
||||
|
||||
fn display(value: Vec3) -> DVec3 {
|
||||
DVec3::new(value.x, value.y, value.z)
|
||||
}
|
||||
|
||||
fn kernel(value: DVec3) -> Vec3 {
|
||||
Vec3::new(value.x, value.y, value.z)
|
||||
}
|
||||
|
||||
fn finite(value: Vec3) -> bool {
|
||||
value.x.is_finite() && value.y.is_finite() && value.z.is_finite()
|
||||
}
|
||||
|
||||
fn spline_curve(spline: &Spline) -> Option<NurbsCurve3> {
|
||||
let controls: Vec<[f64; 3]> = spline
|
||||
.control_points
|
||||
.iter()
|
||||
.map(|value| [value.x, value.y, value.z])
|
||||
.collect();
|
||||
let degree = usize::try_from(spline.degree).ok()?;
|
||||
let weights = if spline.weights.is_empty() {
|
||||
vec![1.0; controls.len()]
|
||||
} else {
|
||||
spline.weights.clone()
|
||||
};
|
||||
NurbsCurve3::new_strict(degree, controls, spline.knots.clone(), weights)
|
||||
}
|
||||
|
||||
fn axis_frame(helix: &Helix) -> Option<(Vec3, Vec3, Vec3)> {
|
||||
let base = point(helix.axis_base_point);
|
||||
let axis = point(helix.axis_vector).normalize()?;
|
||||
let radial = point(helix.start_point) - base;
|
||||
let start_direction = (radial - axis * radial.dot(axis)).normalize()?;
|
||||
Some((base, axis, start_direction))
|
||||
}
|
||||
|
||||
fn radius_from_axis(helix: &Helix, value: Vec3) -> Option<f64> {
|
||||
let (base, axis, _) = axis_frame(helix)?;
|
||||
let delta = value - base;
|
||||
Some((delta - axis * delta.dot(axis)).length())
|
||||
}
|
||||
|
||||
fn top_radius(helix: &Helix) -> f64 {
|
||||
spline_curve(&helix.spline)
|
||||
.map(|curve| Vec3::from(curve.point_at(1.0)))
|
||||
.and_then(|endpoint| radius_from_axis(helix, endpoint))
|
||||
.unwrap_or(helix.radius)
|
||||
}
|
||||
|
||||
fn kernel_curve(helix: &Helix, top_radius: f64) -> Option<HelixCurve> {
|
||||
let (base, axis, start_direction) = axis_frame(helix)?;
|
||||
Some(HelixCurve {
|
||||
base_center: base.to_array(),
|
||||
axis_direction: axis.to_array(),
|
||||
start_direction: start_direction.to_array(),
|
||||
base_radius: helix.radius,
|
||||
top_radius,
|
||||
height: helix.turns * helix.turn_height,
|
||||
turns: helix.turns,
|
||||
direction: if helix.handedness {
|
||||
HelixDirection::CounterClockwise
|
||||
} else {
|
||||
HelixDirection::Clockwise
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn projected_direction(direction: Vec3, axis: Vec3) -> Option<Vec3> {
|
||||
(direction - axis * direction.dot(axis)).normalize()
|
||||
}
|
||||
|
||||
fn perpendicular(axis: Vec3) -> Option<Vec3> {
|
||||
let basis = if axis.x.abs() <= axis.y.abs() && axis.x.abs() <= axis.z.abs() {
|
||||
Vec3::X
|
||||
} else if axis.y.abs() <= axis.z.abs() {
|
||||
Vec3::Y
|
||||
} else {
|
||||
Vec3::Z
|
||||
};
|
||||
projected_direction(basis, axis)
|
||||
}
|
||||
|
||||
fn rebuild(helix: &mut Helix, top_radius: f64) -> bool {
|
||||
let Some(curve) = kernel_curve(helix, top_radius) else {
|
||||
return false;
|
||||
};
|
||||
let Some(nurbs) = curve.nurbs() else {
|
||||
return false;
|
||||
};
|
||||
helix.spline.degree = nurbs.degree() as i32;
|
||||
helix.spline.knots = nurbs.knots().to_vec();
|
||||
helix.spline.control_points = nurbs
|
||||
.control_points()
|
||||
.iter()
|
||||
.map(|value| Vector3::new(value[0], value[1], value[2]))
|
||||
.collect();
|
||||
helix.spline.weights = if nurbs.is_rational() {
|
||||
nurbs.weights().to_vec()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
helix.spline.fit_points.clear();
|
||||
helix.spline.flags.closed = false;
|
||||
helix.spline.flags.periodic = false;
|
||||
helix.spline.flags.rational = nurbs.is_rational();
|
||||
helix.spline.flags.planar = false;
|
||||
helix.spline.flags.linear = false;
|
||||
true
|
||||
}
|
||||
|
||||
fn choice(label: &str, field: &'static str, selected: &str, options: &[String]) -> Property {
|
||||
Property {
|
||||
label: label.to_string(),
|
||||
field,
|
||||
value: PropValue::Choice {
|
||||
selected: selected.to_string(),
|
||||
options: options.to_vec(),
|
||||
},
|
||||
}
|
||||
let vx = p.x - base.x;
|
||||
let vy = p.y - base.y;
|
||||
let vz = p.z - base.z;
|
||||
let proj = vx * axis.x + vy * axis.y + vz * axis.z;
|
||||
let px = vx - proj * axis.x;
|
||||
let py = vy - proj * axis.y;
|
||||
let pz = vz - proj * axis.z;
|
||||
Some((px * px + py * py + pz * pz).sqrt())
|
||||
}
|
||||
|
||||
fn grips(helix: &Helix) -> Vec<GripDef> {
|
||||
Grippable::grips(&helix.spline)
|
||||
let Some((base, axis, _)) = axis_frame(helix) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let height = helix.turns * helix.turn_height;
|
||||
let top_center = base + axis * height;
|
||||
let top_endpoint = spline_curve(&helix.spline)
|
||||
.map(|curve| Vec3::from(curve.point_at(1.0)))
|
||||
.unwrap_or(top_center);
|
||||
vec![
|
||||
center_grip(0, display(base)),
|
||||
square_grip(1, display(point(helix.start_point))),
|
||||
square_grip(2, display(top_center)),
|
||||
square_grip(3, display(top_endpoint)),
|
||||
]
|
||||
}
|
||||
|
||||
fn properties(helix: &Helix) -> Vec<PropSection> {
|
||||
let bp = &helix.axis_base_point;
|
||||
let turns = helix.turns;
|
||||
let turn_height = helix.turn_height;
|
||||
// Overall height is turns × per-turn rise.
|
||||
let height = turns * turn_height;
|
||||
let base_radius = helix.radius;
|
||||
// Top radius: the axial distance of the last defining point. For a
|
||||
// cylindrical helix this equals the base radius.
|
||||
let top_radius = helix
|
||||
.spline
|
||||
.control_points
|
||||
.last()
|
||||
.or_else(|| helix.spline.fit_points.last())
|
||||
.and_then(|p| radius_from_axis(helix, p))
|
||||
.unwrap_or(base_radius);
|
||||
// Turn slope: rise angle of a single turn against the base circumference.
|
||||
let turn_slope = if base_radius.abs() > 1e-9 {
|
||||
(turn_height / (std::f64::consts::TAU * base_radius))
|
||||
.atan()
|
||||
.to_degrees()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let twist = if helix.handedness { "CW" } else { "CCW" };
|
||||
let base = point(helix.axis_base_point);
|
||||
let height = helix.turns * helix.turn_height;
|
||||
let top_radius = top_radius(helix);
|
||||
let curve = kernel_curve(helix, top_radius);
|
||||
let turn_slope = curve.as_ref().and_then(HelixCurve::turn_slope).unwrap_or(0.0);
|
||||
let total_length = curve.as_ref().and_then(HelixCurve::length).unwrap_or(0.0);
|
||||
let constrain = match helix.constraint {
|
||||
HelixConstraint::TurnHeight => "Turn Height",
|
||||
HelixConstraint::Turns => "Turns",
|
||||
HelixConstraint::Height => "Height",
|
||||
HelixConstraint::TurnHeight => t!("Turn Height").into_owned(),
|
||||
HelixConstraint::Turns => t!("Turns").into_owned(),
|
||||
HelixConstraint::Height => t!("Height").into_owned(),
|
||||
};
|
||||
let twist = if helix.handedness {
|
||||
t!("CCW").into_owned()
|
||||
} else {
|
||||
t!("CW").into_owned()
|
||||
};
|
||||
let constraint_options = vec![
|
||||
t!("Turn Height").into_owned(),
|
||||
t!("Turns").into_owned(),
|
||||
t!("Height").into_owned(),
|
||||
];
|
||||
let twist_options = vec![t!("CW").into_owned(), t!("CCW").into_owned()];
|
||||
|
||||
vec![PropSection {
|
||||
title: t!("Geometry").into_owned(),
|
||||
props: vec![
|
||||
ro(t!("Base point X").as_ref(), "base_x", format!("{:.4}", bp.x)),
|
||||
ro(t!("Base point Y").as_ref(), "base_y", format!("{:.4}", bp.y)),
|
||||
ro(t!("Base point Z").as_ref(), "base_z", format!("{:.4}", bp.z)),
|
||||
ro(t!("Number of turns").as_ref(), "num_turns", format!("{turns:.4}")),
|
||||
ro(t!("Turn height").as_ref(), "turn_height", format!("{turn_height:.4}")),
|
||||
ro(t!("Turns").as_ref(), "turns", format!("{turns:.4}")),
|
||||
ro(t!("Height").as_ref(), "height", format!("{height:.4}")),
|
||||
ro(t!("Base radius").as_ref(), "base_radius", format!("{base_radius:.4}")),
|
||||
ro(t!("Top radius").as_ref(), "top_radius", format!("{top_radius:.4}")),
|
||||
ro(t!("Turn slope").as_ref(), "turn_slope", format!("{turn_slope:.4}")),
|
||||
ro(t!("Twist").as_ref(), "twist", twist),
|
||||
ro(t!("Constrain").as_ref(), "constrain", constrain),
|
||||
edit(t!("Position X").as_ref(), "position_x", base.x),
|
||||
edit(t!("Position Y").as_ref(), "position_y", base.y),
|
||||
edit(t!("Position Z").as_ref(), "position_z", base.z),
|
||||
choice(t!("Constrain").as_ref(), "constrain", &constrain, &constraint_options),
|
||||
edit(t!("Height").as_ref(), "height", height),
|
||||
edit(t!("Turns").as_ref(), "turns", helix.turns),
|
||||
edit(t!("Turn height").as_ref(), "turn_height", helix.turn_height),
|
||||
edit(t!("Base radius").as_ref(), "base_radius", helix.radius),
|
||||
edit(t!("Top radius").as_ref(), "top_radius", top_radius),
|
||||
choice(t!("Twist").as_ref(), "twist", &twist, &twist_options),
|
||||
ro(t!("Turn slope").as_ref(), "turn_slope", format_angle(turn_slope)),
|
||||
ro(t!("Total length").as_ref(), "total_length", format_length(total_length)),
|
||||
],
|
||||
}]
|
||||
}
|
||||
|
||||
/// Helix geometry is derived from the stored spline; the generating
|
||||
/// parameters are read-only (editing one would require regenerating the
|
||||
/// spline, which the loaded entity does not carry a recipe for).
|
||||
fn apply_geom_prop(_helix: &mut Helix, _field: &str, _value: &str) {}
|
||||
|
||||
fn apply_grip(helix: &mut Helix, grip_id: usize, apply: GripApply) {
|
||||
Grippable::apply_grip(&mut helix.spline, grip_id, apply);
|
||||
fn apply_geom_prop(helix: &mut Helix, field: &str, value: &str) {
|
||||
if field == "constrain" {
|
||||
let constraint = if value == t!("Height").as_ref() {
|
||||
HelixConstraint::Height
|
||||
} else if value == t!("Turns").as_ref() {
|
||||
HelixConstraint::Turns
|
||||
} else if value == t!("Turn Height").as_ref() {
|
||||
HelixConstraint::TurnHeight
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
helix.constraint = constraint;
|
||||
return;
|
||||
}
|
||||
let original = helix.clone();
|
||||
let top = top_radius(helix);
|
||||
if field == "twist" {
|
||||
if value == t!("CCW").as_ref() {
|
||||
helix.handedness = true;
|
||||
} else if value == t!("CW").as_ref() {
|
||||
helix.handedness = false;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if !rebuild(helix, top) {
|
||||
*helix = original;
|
||||
}
|
||||
return;
|
||||
}
|
||||
let Some(number) = parse_f64(value).filter(|number| number.is_finite()) else {
|
||||
return;
|
||||
};
|
||||
let old_base = point(helix.axis_base_point);
|
||||
let old_height = helix.turns * helix.turn_height;
|
||||
match field {
|
||||
"position_x" | "position_y" | "position_z" => {
|
||||
let mut base = old_base;
|
||||
match field {
|
||||
"position_x" => base.x = number,
|
||||
"position_y" => base.y = number,
|
||||
_ => base.z = number,
|
||||
}
|
||||
let delta = base - old_base;
|
||||
helix.axis_base_point = vector(base);
|
||||
helix.start_point = vector(point(helix.start_point) + delta);
|
||||
}
|
||||
"height" if number.abs() > EPSILON => {
|
||||
if helix.constraint == HelixConstraint::TurnHeight && helix.turn_height.abs() > EPSILON {
|
||||
let turns = number / helix.turn_height;
|
||||
if turns <= EPSILON {
|
||||
return;
|
||||
}
|
||||
helix.turns = turns;
|
||||
} else {
|
||||
helix.turn_height = number / helix.turns.max(EPSILON);
|
||||
}
|
||||
}
|
||||
"turns" if number > EPSILON => {
|
||||
if helix.constraint == HelixConstraint::TurnHeight {
|
||||
helix.turns = number;
|
||||
} else {
|
||||
helix.turns = number;
|
||||
helix.turn_height = old_height / number;
|
||||
}
|
||||
}
|
||||
"turn_height" if number.abs() > EPSILON => {
|
||||
if helix.constraint == HelixConstraint::Turns {
|
||||
helix.turn_height = number;
|
||||
} else {
|
||||
let turns = old_height / number;
|
||||
if turns <= EPSILON {
|
||||
return;
|
||||
}
|
||||
helix.turn_height = number;
|
||||
helix.turns = turns;
|
||||
}
|
||||
}
|
||||
"base_radius" if number > EPSILON => {
|
||||
let Some((base, _, start_direction)) = axis_frame(helix) else {
|
||||
return;
|
||||
};
|
||||
helix.radius = number;
|
||||
helix.start_point = vector(base + start_direction * number);
|
||||
}
|
||||
"top_radius" if number >= 0.0 => {
|
||||
if !rebuild(helix, number) {
|
||||
*helix = original;
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ => return,
|
||||
}
|
||||
if !rebuild(helix, top) {
|
||||
*helix = original;
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_transform(helix: &mut Helix, t: &EntityTransform) {
|
||||
// Move the visible curve, then keep the generating points in step.
|
||||
Transformable::apply_transform(&mut helix.spline, t);
|
||||
fn apply_grip(helix: &mut Helix, grip_id: usize, apply: GripApply) {
|
||||
let original = helix.clone();
|
||||
let top = top_radius(helix);
|
||||
let Some((base, axis, start_direction)) = axis_frame(helix) else {
|
||||
return;
|
||||
};
|
||||
match (grip_id, apply) {
|
||||
(0, GripApply::Translate(delta)) => {
|
||||
let delta = kernel(delta);
|
||||
if !finite(delta) {
|
||||
return;
|
||||
}
|
||||
helix.axis_base_point = vector(base + delta);
|
||||
helix.start_point = vector(point(helix.start_point) + delta);
|
||||
}
|
||||
(0, GripApply::Absolute(position)) => {
|
||||
let position = kernel(position);
|
||||
if !finite(position) {
|
||||
return;
|
||||
}
|
||||
let delta = position - base;
|
||||
helix.axis_base_point = vector(position);
|
||||
helix.start_point = vector(point(helix.start_point) + delta);
|
||||
}
|
||||
(1, GripApply::Absolute(position)) => {
|
||||
let position = kernel(position);
|
||||
if !finite(position) {
|
||||
return;
|
||||
}
|
||||
let radial = position - base - axis * (position - base).dot(axis);
|
||||
let radius = radial.length();
|
||||
if radius <= EPSILON {
|
||||
return;
|
||||
}
|
||||
helix.radius = radius;
|
||||
helix.start_point = vector(base + radial);
|
||||
}
|
||||
(2, GripApply::Absolute(position)) => {
|
||||
let position = kernel(position);
|
||||
if !finite(position) {
|
||||
return;
|
||||
}
|
||||
let axis_vector = position - base;
|
||||
let height = axis_vector.length();
|
||||
if height <= EPSILON {
|
||||
return;
|
||||
}
|
||||
let target_axis = axis_vector / height;
|
||||
if helix.constraint == HelixConstraint::TurnHeight && helix.turn_height.abs() > EPSILON {
|
||||
helix.axis_vector = vector(target_axis * helix.turn_height.signum());
|
||||
helix.turns = height / helix.turn_height.abs();
|
||||
} else {
|
||||
helix.axis_vector = vector(target_axis);
|
||||
helix.turn_height = height / helix.turns.max(EPSILON);
|
||||
}
|
||||
let new_axis = point(helix.axis_vector);
|
||||
let start_direction = projected_direction(start_direction, new_axis)
|
||||
.or_else(|| perpendicular(new_axis));
|
||||
let Some(start_direction) = start_direction else {
|
||||
*helix = original;
|
||||
return;
|
||||
};
|
||||
helix.start_point = vector(base + start_direction * helix.radius);
|
||||
}
|
||||
(3, GripApply::Absolute(position)) => {
|
||||
let position = kernel(position);
|
||||
if !finite(position) {
|
||||
return;
|
||||
}
|
||||
let delta = position - base;
|
||||
let height = delta.dot(axis);
|
||||
let radius = (delta - axis * height).length();
|
||||
if height.abs() <= EPSILON {
|
||||
return;
|
||||
}
|
||||
if helix.constraint == HelixConstraint::TurnHeight && helix.turn_height.abs() > EPSILON {
|
||||
let turns = height / helix.turn_height;
|
||||
if turns <= EPSILON {
|
||||
return;
|
||||
}
|
||||
helix.turns = turns;
|
||||
} else {
|
||||
helix.turn_height = height / helix.turns.max(EPSILON);
|
||||
}
|
||||
if !rebuild(helix, radius) {
|
||||
*helix = original;
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ => return,
|
||||
}
|
||||
if !rebuild(helix, top) {
|
||||
*helix = original;
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_transform(helix: &mut Helix, transform: &EntityTransform) {
|
||||
match transform {
|
||||
EntityTransform::Translate(delta) => {
|
||||
acadrust::Entity::translate(helix, Vector3::new(delta.x, delta.y, delta.z));
|
||||
}
|
||||
EntityTransform::Rotate { center, axis, angle_rad } => {
|
||||
crate::scene::view::transform::apply_standard_transform(
|
||||
helix,
|
||||
*center,
|
||||
*axis,
|
||||
*angle_rad,
|
||||
);
|
||||
}
|
||||
EntityTransform::Scale { center, factor } => {
|
||||
crate::scene::view::transform::apply_standard_scale(helix, *center, *factor);
|
||||
}
|
||||
EntityTransform::Mirror { p1, p2, working_normal } => {
|
||||
let reflection = crate::scene::view::transform::reflection_about_working_line(
|
||||
*p1,
|
||||
*p2,
|
||||
*working_normal,
|
||||
);
|
||||
acadrust::Entity::apply_transform(helix, &reflection);
|
||||
}
|
||||
EntityTransform::Affine(value) => acadrust::Entity::apply_transform(helix, value),
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderConvertible for Helix {
|
||||
@@ -113,6 +434,7 @@ impl Grippable for Helix {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
grips(self)
|
||||
}
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
apply_grip(self, grip_id, apply);
|
||||
}
|
||||
@@ -122,13 +444,14 @@ impl crate::entities::traits::PropertyEditable for Helix {
|
||||
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 Helix {
|
||||
fn apply_transform(&mut self, t: &EntityTransform) {
|
||||
apply_transform(self, t);
|
||||
fn apply_transform(&mut self, transform: &EntityTransform) {
|
||||
apply_transform(self, transform);
|
||||
}
|
||||
}
|
||||
@@ -269,11 +269,10 @@ fn properties(leader: &Leader) -> Vec<PropSection> {
|
||||
let vertex_label = if n == 0 {
|
||||
"—".to_string()
|
||||
} else {
|
||||
format!("{} / {}", vi + 1, n)
|
||||
(vi + 1).to_string()
|
||||
};
|
||||
|
||||
// Geometry sits right after General — a leader owns editable path vertices,
|
||||
// navigated one at a time by the Current Vertex spinner.
|
||||
// Geometry exposes one editable vertex at a time.
|
||||
let mut geometry = vec![stepper(t!("Current Vertex").as_ref(), "current_vertex", vertex_label)];
|
||||
if let Some(v) = leader.vertices.get(vi) {
|
||||
geometry.push(edit(t!("Vertex X").as_ref(), "vertex_x", v.x));
|
||||
@@ -285,8 +284,7 @@ fn properties(leader: &Leader) -> Vec<PropSection> {
|
||||
geometry.push(ro(t!("Vertex Z").as_ref(), "vertex_z", String::new()));
|
||||
}
|
||||
|
||||
// Misc: Dim style (upgraded to a dropdown by the panel builder), the
|
||||
// combined path/arrow Type, and annotative state (from the dim style).
|
||||
// The panel builder resolves the dimension style and annotative state.
|
||||
let misc = vec![
|
||||
Property {
|
||||
label: t!("Dim style").into_owned(),
|
||||
|
||||
+181
-245
@@ -2,144 +2,57 @@ use acadrust::entities::{mesh::Mesh, polygon_mesh::PolygonMesh, Face3D, Polyface
|
||||
use cadkernel::geom2d::{triangulate, Tolerance};
|
||||
use cadkernel::space::{polygon, NurbsSurface3, Plane, Vec3 as KernelVec3};
|
||||
use glam::Vec3;
|
||||
use crate::t;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{
|
||||
edit_prop, edit_scalar_prop, parse_f64, ro_prop as ro, square_grip,
|
||||
stepper_prop,
|
||||
edit_prop as edit, edit_scalar_prop as edit_scalar, parse_f64, ro_prop as ro,
|
||||
square_grip, stepper_prop as stepper,
|
||||
};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, RenderConvertible, Transformable};
|
||||
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
|
||||
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||||
use crate::scene::model::wire_model::SnapHint;
|
||||
use crate::t;
|
||||
|
||||
/// Triangulate a planar (possibly concave) polygon into a flat triangle-soup
|
||||
/// (3 vertices per triangle), preserving the polygon's winding. A simple fan
|
||||
/// from vertex 0 is only valid for convex faces — a concave face (e.g. an
|
||||
/// L-shaped mesh face) fans into triangles that spill outside the outline. Ear
|
||||
/// clipping handles both. Falls back to a fan when the polygon is degenerate.
|
||||
/// Triangulate a planar polygon through the kernel.
|
||||
pub(crate) fn triangulate_planar(poly: &[[f64; 3]]) -> Vec<[f64; 3]> {
|
||||
let n = poly.len();
|
||||
if n < 3 {
|
||||
if poly.len() < 3 {
|
||||
return Vec::new();
|
||||
}
|
||||
if n == 3 {
|
||||
return vec![poly[0], poly[1], poly[2]];
|
||||
}
|
||||
let cross = |a: [f64; 3], b: [f64; 3]| {
|
||||
[
|
||||
a[1] * b[2] - a[2] * b[1],
|
||||
a[2] * b[0] - a[0] * b[2],
|
||||
a[0] * b[1] - a[1] * b[0],
|
||||
]
|
||||
let origin = KernelVec3::from(poly[0]);
|
||||
let tolerance = Tolerance::default().linear();
|
||||
let Some(axis) = poly[1..]
|
||||
.iter()
|
||||
.map(|point| KernelVec3::from(*point) - origin)
|
||||
.find(|axis| axis.length() > tolerance)
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
let dot = |a: [f64; 3], b: [f64; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
||||
let fan = || {
|
||||
let mut out = Vec::new();
|
||||
for i in 1..n - 1 {
|
||||
out.push(poly[0]);
|
||||
out.push(poly[i]);
|
||||
out.push(poly[i + 1]);
|
||||
}
|
||||
out
|
||||
let Some(normal) = polygon::normal(poly) else {
|
||||
return Vec::new();
|
||||
};
|
||||
// Face normal via Newell's method (robust for near-planar polygons).
|
||||
let mut normal = [0.0f64; 3];
|
||||
for i in 0..n {
|
||||
let a = poly[i];
|
||||
let b = poly[(i + 1) % n];
|
||||
normal[0] += (a[1] - b[1]) * (a[2] + b[2]);
|
||||
normal[1] += (a[2] - b[2]) * (a[0] + b[0]);
|
||||
normal[2] += (a[0] - b[0]) * (a[1] + b[1]);
|
||||
}
|
||||
let nlen = dot(normal, normal).sqrt();
|
||||
if nlen < 1e-12 {
|
||||
return fan();
|
||||
}
|
||||
let normal = [normal[0] / nlen, normal[1] / nlen, normal[2] / nlen];
|
||||
// Orthonormal in-plane basis.
|
||||
let seed = if normal[0].abs() < 0.9 { [1.0, 0.0, 0.0] } else { [0.0, 1.0, 0.0] };
|
||||
let mut u = cross(seed, normal);
|
||||
let ul = dot(u, u).sqrt();
|
||||
if ul < 1e-12 {
|
||||
return fan();
|
||||
}
|
||||
u = [u[0] / ul, u[1] / ul, u[2] / ul];
|
||||
let v = cross(normal, u);
|
||||
let p2: Vec<[f64; 2]> = poly.iter().map(|&p| [dot(p, u), dot(p, v)]).collect();
|
||||
// Signed area → winding (CCW when positive in the (u, v) frame).
|
||||
let mut area = 0.0;
|
||||
for i in 0..n {
|
||||
let a = p2[i];
|
||||
let b = p2[(i + 1) % n];
|
||||
area += a[0] * b[1] - b[0] * a[1];
|
||||
}
|
||||
let ccw = area > 0.0;
|
||||
let tri_area2 = |a: [f64; 2], b: [f64; 2], c: [f64; 2]| {
|
||||
(b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
|
||||
let Some(plane) = Plane::orthonormal(poly[0], axis.to_array(), normal) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let in_tri = |p: [f64; 2], a: [f64; 2], b: [f64; 2], c: [f64; 2]| {
|
||||
let d1 = tri_area2(a, b, p);
|
||||
let d2 = tri_area2(b, c, p);
|
||||
let d3 = tri_area2(c, a, p);
|
||||
let neg = d1 < 0.0 || d2 < 0.0 || d3 < 0.0;
|
||||
let pos = d1 > 0.0 || d2 > 0.0 || d3 > 0.0;
|
||||
!(neg && pos)
|
||||
};
|
||||
let mut idx: Vec<usize> = (0..n).collect();
|
||||
let mut out: Vec<[f64; 3]> = Vec::with_capacity((n - 2) * 3);
|
||||
let mut guard = 0usize;
|
||||
while idx.len() > 3 && guard < n * n {
|
||||
guard += 1;
|
||||
let m = idx.len();
|
||||
let mut clipped = false;
|
||||
for k in 0..m {
|
||||
let i0 = idx[(k + m - 1) % m];
|
||||
let i1 = idx[k];
|
||||
let i2 = idx[(k + 1) % m];
|
||||
let (a, b, c) = (p2[i0], p2[i1], p2[i2]);
|
||||
let convex = if ccw { tri_area2(a, b, c) > 0.0 } else { tri_area2(a, b, c) < 0.0 };
|
||||
if !convex {
|
||||
continue;
|
||||
}
|
||||
let mut contains = false;
|
||||
for &j in &idx {
|
||||
if j == i0 || j == i1 || j == i2 {
|
||||
continue;
|
||||
}
|
||||
if in_tri(p2[j], a, b, c) {
|
||||
contains = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if contains {
|
||||
continue;
|
||||
}
|
||||
out.push(poly[i0]);
|
||||
out.push(poly[i1]);
|
||||
out.push(poly[i2]);
|
||||
idx.remove(k);
|
||||
clipped = true;
|
||||
break;
|
||||
}
|
||||
if !clipped {
|
||||
// No ear found (self-intersecting / numerically degenerate) — bail
|
||||
// to a fan of the remainder rather than loop forever.
|
||||
for i in 1..idx.len() - 1 {
|
||||
out.push(poly[idx[0]]);
|
||||
out.push(poly[idx[i]]);
|
||||
out.push(poly[idx[i + 1]]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
let projected: Vec<[f64; 2]> = poly
|
||||
.iter()
|
||||
.filter_map(|&point| plane.project(point))
|
||||
.collect();
|
||||
if projected.len() != poly.len() {
|
||||
return Vec::new();
|
||||
}
|
||||
if idx.len() == 3 {
|
||||
out.push(poly[idx[0]]);
|
||||
out.push(poly[idx[1]]);
|
||||
out.push(poly[idx[2]]);
|
||||
let (points, triangles) = triangulate(&projected, &[]);
|
||||
let source: Vec<usize> = points
|
||||
.iter()
|
||||
.filter_map(|point| projected.iter().position(|candidate| candidate == point))
|
||||
.collect();
|
||||
if source.len() != points.len() {
|
||||
return Vec::new();
|
||||
}
|
||||
out
|
||||
triangles
|
||||
.into_iter()
|
||||
.flat_map(|triangle| triangle.map(|index| poly[source[index]]))
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ── Face3D ────────────────────────────────────────────────────────────────────
|
||||
@@ -177,32 +90,7 @@ fn face3d_fill(corners: [[f64; 3]; 4]) -> Vec<[f64; 3]> {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let Some(normal) = polygon::normal(&ring) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let axis = (KernelVec3::from(ring[1]) - KernelVec3::from(ring[0])).to_array();
|
||||
let Some(plane) = Plane::orthonormal(ring[0], axis, normal) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let projected: Vec<[f64; 2]> = ring
|
||||
.iter()
|
||||
.filter_map(|&point| plane.project(point))
|
||||
.collect();
|
||||
if projected.len() != ring.len() {
|
||||
return Vec::new();
|
||||
}
|
||||
let (points, triangles) = triangulate(&projected, &[]);
|
||||
let source: Vec<usize> = points
|
||||
.iter()
|
||||
.filter_map(|point| projected.iter().position(|candidate| candidate == point))
|
||||
.collect();
|
||||
if source.len() != points.len() {
|
||||
return Vec::new();
|
||||
}
|
||||
triangles
|
||||
.into_iter()
|
||||
.flat_map(|triangle| triangle.map(|index| ring[source[index]]))
|
||||
.collect()
|
||||
triangulate_planar(&ring)
|
||||
}
|
||||
|
||||
impl RenderConvertible for Face3D {
|
||||
@@ -241,15 +129,6 @@ impl RenderConvertible for Face3D {
|
||||
add_edge(p3, p0);
|
||||
}
|
||||
|
||||
if pts.is_empty() {
|
||||
// All edges invisible — show a tiny cross at centroid.
|
||||
let cx = (p0[0] + p1[0] + p2[0] + p3[0]) / 4.0;
|
||||
let cy = (p0[1] + p1[1] + p2[1] + p3[1]) / 4.0;
|
||||
let cz = (p0[2] + p1[2] + p2[2] + p3[2]) / 4.0;
|
||||
let s = 0.1_f64;
|
||||
pts = vec![[cx - s, cy, cz], [cx + s, cy, cz]];
|
||||
}
|
||||
|
||||
Some(RenderEntity {
|
||||
pick_tris: Vec::new(),
|
||||
object: RenderObject::Lines(pts),
|
||||
@@ -268,15 +147,22 @@ impl RenderConvertible for Face3D {
|
||||
|
||||
impl Grippable for Face3D {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
vec![
|
||||
let mut grips = vec![
|
||||
square_grip(0, dvec3(&self.first_corner)),
|
||||
square_grip(1, dvec3(&self.second_corner)),
|
||||
square_grip(2, dvec3(&self.third_corner)),
|
||||
square_grip(3, dvec3(&self.fourth_corner)),
|
||||
]
|
||||
];
|
||||
if !self.is_triangle() {
|
||||
grips.push(square_grip(3, dvec3(&self.fourth_corner)));
|
||||
}
|
||||
grips
|
||||
}
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
let was_triangle = self.is_triangle();
|
||||
if was_triangle && grip_id == 3 {
|
||||
return;
|
||||
}
|
||||
let corner = match grip_id {
|
||||
0 => &mut self.first_corner,
|
||||
1 => &mut self.second_corner,
|
||||
@@ -284,69 +170,107 @@ impl Grippable for Face3D {
|
||||
3 => &mut self.fourth_corner,
|
||||
_ => return,
|
||||
};
|
||||
match apply {
|
||||
GripApply::Translate(d) => {
|
||||
corner.x += d.x as f64;
|
||||
corner.y += d.y as f64;
|
||||
corner.z += d.z as f64;
|
||||
}
|
||||
GripApply::Absolute(p) => {
|
||||
corner.x = p.x as f64;
|
||||
corner.y = p.y as f64;
|
||||
corner.z = p.z as f64;
|
||||
}
|
||||
let current = dvec3(corner);
|
||||
let updated = match apply {
|
||||
GripApply::Translate(delta) => current + delta,
|
||||
GripApply::Absolute(position) => position,
|
||||
};
|
||||
if !updated.is_finite() {
|
||||
return;
|
||||
}
|
||||
corner.x = updated.x;
|
||||
corner.y = updated.y;
|
||||
corner.z = updated.z;
|
||||
if was_triangle && grip_id == 2 {
|
||||
self.fourth_corner = self.third_corner;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyEditable for Face3D {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
||||
use crate::entities::common::edit_prop as edit;
|
||||
let was_triangle = self.is_triangle();
|
||||
let vertex_count = if was_triangle { 3 } else { 4 };
|
||||
let current = crate::scene::view::dispatch::prop_current_vertex()
|
||||
.min(vertex_count - 1);
|
||||
let corner = match current {
|
||||
0 => &self.first_corner,
|
||||
1 => &self.second_corner,
|
||||
2 => &self.third_corner,
|
||||
_ => &self.fourth_corner,
|
||||
};
|
||||
let inv = self.invisible_edges;
|
||||
let edge = |hidden: bool| if hidden { "Invisible" } else { "Visible" };
|
||||
let edge = |label: &str, field: &'static str, hidden: bool| Property {
|
||||
label: label.to_string(),
|
||||
field,
|
||||
value: PropValue::Choice {
|
||||
selected: if hidden {
|
||||
t!("Invisible").into_owned()
|
||||
} else {
|
||||
t!("Visible").into_owned()
|
||||
},
|
||||
options: vec![t!("Visible").into_owned(), t!("Invisible").into_owned()],
|
||||
},
|
||||
};
|
||||
vec![PropSection {
|
||||
title: t!("Geometry").into_owned(),
|
||||
props: vec![
|
||||
ro(t!("Current vertex").as_ref(), "f3_current", String::new()),
|
||||
edit(t!("Vertex 1 X").as_ref(), "f3_p1x", self.first_corner.x),
|
||||
edit(t!("Vertex 1 Y").as_ref(), "f3_p1y", self.first_corner.y),
|
||||
edit(t!("Vertex 1 Z").as_ref(), "f3_p1z", self.first_corner.z),
|
||||
edit(t!("Vertex 2 X").as_ref(), "f3_p2x", self.second_corner.x),
|
||||
edit(t!("Vertex 2 Y").as_ref(), "f3_p2y", self.second_corner.y),
|
||||
edit(t!("Vertex 2 Z").as_ref(), "f3_p2z", self.second_corner.z),
|
||||
edit(t!("Vertex 3 X").as_ref(), "f3_p3x", self.third_corner.x),
|
||||
edit(t!("Vertex 3 Y").as_ref(), "f3_p3y", self.third_corner.y),
|
||||
edit(t!("Vertex 3 Z").as_ref(), "f3_p3z", self.third_corner.z),
|
||||
edit(t!("Vertex 4 X").as_ref(), "f3_p4x", self.fourth_corner.x),
|
||||
edit(t!("Vertex 4 Y").as_ref(), "f3_p4y", self.fourth_corner.y),
|
||||
edit(t!("Vertex 4 Z").as_ref(), "f3_p4z", self.fourth_corner.z),
|
||||
ro(t!("Edge 1").as_ref(), "f3_edge1", edge(inv.is_first_invisible())),
|
||||
ro(t!("Edge 2").as_ref(), "f3_edge2", edge(inv.is_second_invisible())),
|
||||
ro(t!("Edge 3").as_ref(), "f3_edge3", edge(inv.is_third_invisible())),
|
||||
ro(t!("Edge 4").as_ref(), "f3_edge4", edge(inv.is_fourth_invisible())),
|
||||
stepper(
|
||||
t!("Current vertex").as_ref(),
|
||||
"current_vertex",
|
||||
format!("{}", current + 1),
|
||||
),
|
||||
edit(t!("Vertex X").as_ref(), "f3_vertex_x", corner.x),
|
||||
edit(t!("Vertex Y").as_ref(), "f3_vertex_y", corner.y),
|
||||
edit(t!("Vertex Z").as_ref(), "f3_vertex_z", corner.z),
|
||||
edge(t!("Edge 1").as_ref(), "f3_edge1", inv.is_first_invisible()),
|
||||
edge(t!("Edge 2").as_ref(), "f3_edge2", inv.is_second_invisible()),
|
||||
edge(t!("Edge 3").as_ref(), "f3_edge3", inv.is_third_invisible()),
|
||||
edge(t!("Edge 4").as_ref(), "f3_edge4", inv.is_fourth_invisible()),
|
||||
],
|
||||
}]
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
let Ok(v) = value.trim().parse::<f64>() else {
|
||||
if field.starts_with("f3_edge") {
|
||||
let invisible = if value == t!("Invisible").as_ref() {
|
||||
true
|
||||
} else if value == t!("Visible").as_ref() {
|
||||
false
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"f3_edge1" => self.invisible_edges.set_first_invisible(invisible),
|
||||
"f3_edge2" => self.invisible_edges.set_second_invisible(invisible),
|
||||
"f3_edge3" => self.invisible_edges.set_third_invisible(invisible),
|
||||
"f3_edge4" => self.invisible_edges.set_fourth_invisible(invisible),
|
||||
_ => {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
let Some(value) = parse_f64(value).filter(|value| value.is_finite()) else {
|
||||
return;
|
||||
};
|
||||
let was_triangle = self.is_triangle();
|
||||
let vertex_count = if was_triangle { 3 } else { 4 };
|
||||
let current = crate::scene::view::dispatch::prop_current_vertex()
|
||||
.min(vertex_count - 1);
|
||||
let corner = match current {
|
||||
0 => &mut self.first_corner,
|
||||
1 => &mut self.second_corner,
|
||||
2 => &mut self.third_corner,
|
||||
_ => &mut self.fourth_corner,
|
||||
};
|
||||
match field {
|
||||
"f3_p1x" => self.first_corner.x = v,
|
||||
"f3_p1y" => self.first_corner.y = v,
|
||||
"f3_p1z" => self.first_corner.z = v,
|
||||
"f3_p2x" => self.second_corner.x = v,
|
||||
"f3_p2y" => self.second_corner.y = v,
|
||||
"f3_p2z" => self.second_corner.z = v,
|
||||
"f3_p3x" => self.third_corner.x = v,
|
||||
"f3_p3y" => self.third_corner.y = v,
|
||||
"f3_p3z" => self.third_corner.z = v,
|
||||
"f3_p4x" => self.fourth_corner.x = v,
|
||||
"f3_p4y" => self.fourth_corner.y = v,
|
||||
"f3_p4z" => self.fourth_corner.z = v,
|
||||
"f3_vertex_x" => corner.x = value,
|
||||
"f3_vertex_y" => corner.y = value,
|
||||
"f3_vertex_z" => corner.z = value,
|
||||
_ => {}
|
||||
}
|
||||
if was_triangle && current == 2 {
|
||||
self.fourth_corner = self.third_corner;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,16 +317,13 @@ fn polygon_mesh_display(mesh: &PolygonMesh) -> (Vec<[f64; 3]>, usize, usize) {
|
||||
return (base, m, n);
|
||||
}
|
||||
let control_points: Vec<Vec<[f64; 3]>> = base.chunks(n).map(|row| row.to_vec()).collect();
|
||||
let Some(surface) = NurbsSurface3::new(
|
||||
let Some(surface) = NurbsSurface3::from_control_net(
|
||||
u_degree,
|
||||
v_degree,
|
||||
control_points,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
None,
|
||||
)
|
||||
.map(|surface| surface.with_periodicity(mesh.is_closed_m(), mesh.is_closed_n()))
|
||||
else {
|
||||
mesh.is_closed_m(),
|
||||
mesh.is_closed_n(),
|
||||
) else {
|
||||
return (base, m, n);
|
||||
};
|
||||
|
||||
@@ -499,18 +420,17 @@ impl Grippable for PolygonMesh {
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
if let Some(v) = self.vertices.get_mut(grip_id) {
|
||||
match apply {
|
||||
GripApply::Translate(d) => {
|
||||
v.location.x += d.x as f64;
|
||||
v.location.y += d.y as f64;
|
||||
v.location.z += d.z as f64;
|
||||
}
|
||||
GripApply::Absolute(p) => {
|
||||
v.location.x = p.x as f64;
|
||||
v.location.y = p.y as f64;
|
||||
v.location.z = p.z as f64;
|
||||
}
|
||||
let current = glam::DVec3::new(v.location.x, v.location.y, v.location.z);
|
||||
let updated = match apply {
|
||||
GripApply::Translate(delta) => current + delta,
|
||||
GripApply::Absolute(position) => position,
|
||||
};
|
||||
if !updated.is_finite() {
|
||||
return;
|
||||
}
|
||||
v.location.x = updated.x;
|
||||
v.location.y = updated.y;
|
||||
v.location.z = updated.z;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -537,7 +457,7 @@ impl PropertyEditable for PolygonMesh {
|
||||
let smoothed = self.smooth_type != SurfaceSmoothType::NoSmooth;
|
||||
let density_prop = |label: &str, field: &'static str, value: i16| {
|
||||
if smoothed {
|
||||
edit_scalar_prop(label, field, value as f64)
|
||||
edit_scalar(label, field, value as f64)
|
||||
} else {
|
||||
ro(label, field, value.to_string())
|
||||
}
|
||||
@@ -546,16 +466,16 @@ impl PropertyEditable for PolygonMesh {
|
||||
PropSection {
|
||||
title: t!("Geometry").into_owned(),
|
||||
props: vec![
|
||||
stepper_prop(t!("Current Vertex").as_ref(), "pm_current_vertex", display),
|
||||
edit_prop(t!("Vertex X").as_ref(),
|
||||
stepper(t!("Current Vertex").as_ref(), "pm_current_vertex", display),
|
||||
edit(t!("Vertex X").as_ref(),
|
||||
"pm_vx",
|
||||
vertex.map_or(0.0, |vertex| vertex.location.x),
|
||||
),
|
||||
edit_prop(t!("Vertex Y").as_ref(),
|
||||
edit(t!("Vertex Y").as_ref(),
|
||||
"pm_vy",
|
||||
vertex.map_or(0.0, |vertex| vertex.location.y),
|
||||
),
|
||||
edit_prop(t!("Vertex Z").as_ref(),
|
||||
edit(t!("Vertex Z").as_ref(),
|
||||
"pm_vz",
|
||||
vertex.map_or(0.0, |vertex| vertex.location.z),
|
||||
),
|
||||
@@ -605,48 +525,64 @@ impl PropertyEditable for PolygonMesh {
|
||||
|
||||
let bool_value = |current: bool| {
|
||||
if value == "toggle" {
|
||||
!current
|
||||
Some(!current)
|
||||
} else if value.eq_ignore_ascii_case("true")
|
||||
|| value.eq_ignore_ascii_case("yes")
|
||||
{
|
||||
Some(true)
|
||||
} else if value.eq_ignore_ascii_case("false")
|
||||
|| value.eq_ignore_ascii_case("no")
|
||||
{
|
||||
Some(false)
|
||||
} else {
|
||||
value.eq_ignore_ascii_case("true") || value.eq_ignore_ascii_case("yes")
|
||||
None
|
||||
}
|
||||
};
|
||||
match field {
|
||||
"pm_closed_m" => self.flags.set(
|
||||
PolygonMeshFlags::CLOSED_M,
|
||||
bool_value(self.is_closed_m()),
|
||||
),
|
||||
"pm_closed_n" => self.flags.set(
|
||||
PolygonMeshFlags::CLOSED_N,
|
||||
bool_value(self.is_closed_n()),
|
||||
),
|
||||
"pm_closed_m" => {
|
||||
if let Some(closed) = bool_value(self.is_closed_m()) {
|
||||
self.flags.set(PolygonMeshFlags::CLOSED_M, closed);
|
||||
}
|
||||
}
|
||||
"pm_closed_n" => {
|
||||
if let Some(closed) = bool_value(self.is_closed_n()) {
|
||||
self.flags.set(PolygonMeshFlags::CLOSED_N, closed);
|
||||
}
|
||||
}
|
||||
"pm_smooth" => {
|
||||
self.smooth_type = match value.trim().to_ascii_uppercase().as_str() {
|
||||
let smooth = match value.trim().to_ascii_uppercase().as_str() {
|
||||
"NONE" => SurfaceSmoothType::NoSmooth,
|
||||
"QUADRATIC" => SurfaceSmoothType::Quadratic,
|
||||
"CUBIC" => SurfaceSmoothType::Cubic,
|
||||
"BEZIER" => SurfaceSmoothType::Bezier,
|
||||
_ => SurfaceSmoothType::NoSmooth,
|
||||
_ => return,
|
||||
};
|
||||
self.smooth_type = smooth;
|
||||
if self.smooth_type != SurfaceSmoothType::NoSmooth {
|
||||
if self.m_smooth_density < 2 {
|
||||
if !(2..=256).contains(&self.m_smooth_density) {
|
||||
self.m_smooth_density = 6;
|
||||
}
|
||||
if self.n_smooth_density < 2 {
|
||||
if !(2..=256).contains(&self.n_smooth_density) {
|
||||
self.n_smooth_density = 6;
|
||||
}
|
||||
}
|
||||
}
|
||||
"pm_smooth_m" => {
|
||||
if let Ok(density @ 2..=256) = value.trim().parse::<i16>() {
|
||||
self.m_smooth_density = density;
|
||||
if self.smooth_type != SurfaceSmoothType::NoSmooth {
|
||||
if let Ok(density @ 2..=256) = value.trim().parse::<i16>() {
|
||||
self.m_smooth_density = density;
|
||||
}
|
||||
}
|
||||
}
|
||||
"pm_smooth_n" => {
|
||||
if let Ok(density @ 2..=256) = value.trim().parse::<i16>() {
|
||||
self.n_smooth_density = density;
|
||||
if self.smooth_type != SurfaceSmoothType::NoSmooth {
|
||||
if let Ok(density @ 2..=256) = value.trim().parse::<i16>() {
|
||||
self.n_smooth_density = density;
|
||||
}
|
||||
}
|
||||
}
|
||||
"pm_vx" | "pm_vy" | "pm_vz" => {
|
||||
let Some(number) = parse_f64(value) else {
|
||||
let Some(number) = parse_f64(value).filter(|number| number.is_finite()) else {
|
||||
return;
|
||||
};
|
||||
let index = crate::scene::view::dispatch::prop_current_vertex()
|
||||
|
||||
+300
-77
@@ -1,7 +1,9 @@
|
||||
use acadrust::entities::{
|
||||
FlowDirectionType, LeaderContentType, LineSpacingStyle, MultiLeader, MultiLeaderPathType,
|
||||
TextAlignmentType, TextAttachmentDirectionType, TextAttachmentType,
|
||||
BlockContentConnectionType, FlowDirectionType, LeaderContentType, LineSpacingStyle,
|
||||
MultiLeader, MultiLeaderPathType, MultiLeaderPropertyOverrideFlags, TextAlignmentType,
|
||||
TextAttachmentDirectionType, TextAttachmentType,
|
||||
};
|
||||
use cadkernel::geom2d::Transform;
|
||||
|
||||
use crate::entities::text_support::{
|
||||
layout_mtext, resolve_text_style, MTextRenderOpts, MTextVAnchor, ResolvedTextStyle,
|
||||
@@ -904,9 +906,7 @@ fn properties(ml: &MultiLeader) -> Vec<PropSection> {
|
||||
// ── Leaders ──────────────────────────────────────────────────────────
|
||||
// Landing rows are folded in here: the standalone "Leader Structure" group
|
||||
// is a style-dialog tab, not a palette group.
|
||||
let leaders = PropSection {
|
||||
title: t!("Leaders").into_owned(),
|
||||
props: vec![
|
||||
let mut leader_props = vec![
|
||||
choice(
|
||||
t!("Leader type").as_ref(),
|
||||
"path_type",
|
||||
@@ -923,20 +923,34 @@ fn properties(ml: &MultiLeader) -> Vec<PropSection> {
|
||||
Property {
|
||||
label: t!("Leader lineweight").into_owned(),
|
||||
field: "line_weight",
|
||||
value: PropValue::LwChoice(ml.line_weight),
|
||||
value: PropValue::FieldLwChoice {
|
||||
field: "line_weight",
|
||||
value: ml.line_weight,
|
||||
},
|
||||
},
|
||||
// Arrowhead block name resolved by the panel builder (default "Closed filled").
|
||||
ro(t!("Arrowhead").as_ref(), "arrowhead_handle", "Closed filled"),
|
||||
edit(t!("Arrowhead size").as_ref(), "arrowhead_size", ml.arrowhead_size),
|
||||
edit(t!("Arrowhead Size").as_ref(), "arrowhead_size", ml.arrowhead_size),
|
||||
bool_toggle(t!("Horizontal Landing").as_ref(), "enable_dogleg", ml.enable_dogleg),
|
||||
num_row(
|
||||
t!("Landing distance").as_ref(),
|
||||
"landing_distance",
|
||||
ml.dogleg_length,
|
||||
if ml.enable_dogleg { ml.dogleg_length } else { 0.0 },
|
||||
ml.enable_dogleg,
|
||||
),
|
||||
bool_toggle(t!("Leader extension").as_ref(), "enable_landing", ml.enable_landing),
|
||||
],
|
||||
];
|
||||
// The extension is an MText-only landing option. It is absent for block
|
||||
// content and whenever the horizontal landing itself is disabled.
|
||||
if ml.content_type == LeaderContentType::MText && ml.enable_dogleg {
|
||||
leader_props.push(bool_toggle(
|
||||
t!("Leader extension").as_ref(),
|
||||
"extend_leader_to_text",
|
||||
ml.extend_leader_to_text,
|
||||
));
|
||||
}
|
||||
let leaders = PropSection {
|
||||
title: t!("Leaders").into_owned(),
|
||||
props: leader_props,
|
||||
};
|
||||
|
||||
// ── Text (shown only for MText content) ──────────────────────────────
|
||||
@@ -988,6 +1002,18 @@ fn properties(ml: &MultiLeader) -> Vec<PropSection> {
|
||||
attach_dir_str(&ml.text_attachment_direction),
|
||||
&["Horizontal", "Vertical"],
|
||||
),
|
||||
edit(
|
||||
t!("Landing gap").as_ref(),
|
||||
"landing_gap",
|
||||
ctx.landing_gap / ctx.scale_factor.max(1.0e-12),
|
||||
),
|
||||
bool_toggle(t!("Text frame").as_ref(), "text_frame", ml.text_frame),
|
||||
],
|
||||
};
|
||||
let mut text = text;
|
||||
let attachment_insert = text.props.len().saturating_sub(2);
|
||||
let attachment_rows = match ml.text_attachment_direction {
|
||||
TextAttachmentDirectionType::Horizontal => vec![
|
||||
choice(
|
||||
t!("Left Attachment").as_ref(),
|
||||
"text_left_attachment",
|
||||
@@ -1000,10 +1026,23 @@ fn properties(ml: &MultiLeader) -> Vec<PropSection> {
|
||||
attachment_str(&ml.text_right_attachment),
|
||||
&ATTACH_LABELS,
|
||||
),
|
||||
edit(t!("Landing gap").as_ref(), "landing_gap", ctx.landing_gap),
|
||||
bool_toggle(t!("Text frame").as_ref(), "text_frame", ml.text_frame),
|
||||
],
|
||||
TextAttachmentDirectionType::Vertical => vec![
|
||||
choice(
|
||||
t!("Top Attachment").as_ref(),
|
||||
"text_top_attachment",
|
||||
attachment_str(&ml.text_top_attachment),
|
||||
&ATTACH_LABELS,
|
||||
),
|
||||
choice(
|
||||
t!("Bottom Attachment").as_ref(),
|
||||
"text_bottom_attachment",
|
||||
attachment_str(&ml.text_bottom_attachment),
|
||||
&ATTACH_LABELS,
|
||||
),
|
||||
],
|
||||
};
|
||||
text.props.splice(attachment_insert..attachment_insert, attachment_rows);
|
||||
|
||||
// ── Block ────────────────────────────────────────────────────────────
|
||||
let block = PropSection {
|
||||
@@ -1014,29 +1053,16 @@ fn properties(ml: &MultiLeader) -> Vec<PropSection> {
|
||||
"block_content_handle",
|
||||
hexh(ml.block_content_handle),
|
||||
),
|
||||
ro(
|
||||
t!("Block connection").as_ref(),
|
||||
choice(
|
||||
t!("Attachment").as_ref(),
|
||||
"block_connection_type",
|
||||
format!("{:?}", ml.block_connection_type),
|
||||
),
|
||||
ro(
|
||||
t!("Block color").as_ref(),
|
||||
"block_content_color",
|
||||
format!("{:?}", ml.block_content_color),
|
||||
),
|
||||
ro(
|
||||
t!("Block scale").as_ref(),
|
||||
"block_scale",
|
||||
format!(
|
||||
"{:.3} × {:.3} × {:.3}",
|
||||
ml.block_scale.x, ml.block_scale.y, ml.block_scale.z
|
||||
),
|
||||
),
|
||||
edit(
|
||||
t!("Block rotation").as_ref(),
|
||||
"block_rotation",
|
||||
ml.block_rotation.to_degrees(),
|
||||
match ml.block_connection_type {
|
||||
BlockContentConnectionType::BasePoint => "Insertion point",
|
||||
BlockContentConnectionType::BlockExtents => "Extents",
|
||||
},
|
||||
&["Extents", "Insertion point"],
|
||||
),
|
||||
edit(t!("Scale").as_ref(), "block_scale", ml.block_scale.x),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1067,7 +1093,9 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
||||
"text_height" => {
|
||||
if let Some(v) = f64(value) {
|
||||
ml.text_height = v;
|
||||
ml.context.text_height = v;
|
||||
ml.context.text_height = v * ml.context.scale_factor.max(1.0e-12);
|
||||
ml.property_override_flags
|
||||
.insert(MultiLeaderPropertyOverrideFlags::TEXT_HEIGHT);
|
||||
}
|
||||
}
|
||||
"text_x" => {
|
||||
@@ -1093,7 +1121,9 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
||||
!ml.text_frame
|
||||
} else {
|
||||
value == "true"
|
||||
}
|
||||
};
|
||||
ml.property_override_flags
|
||||
.insert(MultiLeaderPropertyOverrideFlags::TEXT_FRAME);
|
||||
}
|
||||
"path_type" => {
|
||||
ml.path_type = match value {
|
||||
@@ -1101,19 +1131,40 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
||||
"None" => MultiLeaderPathType::Invisible,
|
||||
_ => MultiLeaderPathType::StraightLineSegments,
|
||||
};
|
||||
for root in &mut ml.context.leader_roots {
|
||||
for line in &mut root.lines {
|
||||
line.path_type = ml.path_type;
|
||||
line.override_flags.insert(
|
||||
acadrust::entities::LeaderLinePropertyOverrideFlags::PATH_TYPE,
|
||||
);
|
||||
}
|
||||
}
|
||||
ml.property_override_flags
|
||||
.insert(MultiLeaderPropertyOverrideFlags::PATH_TYPE);
|
||||
}
|
||||
"enable_landing" => {
|
||||
ml.enable_landing = if value == "toggle" {
|
||||
!ml.enable_landing
|
||||
} else {
|
||||
value == "true"
|
||||
}
|
||||
};
|
||||
ml.property_override_flags
|
||||
.insert(MultiLeaderPropertyOverrideFlags::ENABLE_LANDING);
|
||||
}
|
||||
"enable_dogleg" => {
|
||||
ml.enable_dogleg = if value == "toggle" {
|
||||
!ml.enable_dogleg
|
||||
} else {
|
||||
value == "true"
|
||||
};
|
||||
ml.property_override_flags
|
||||
.insert(MultiLeaderPropertyOverrideFlags::ENABLE_DOGLEG);
|
||||
}
|
||||
"extend_leader_to_text" => {
|
||||
ml.extend_leader_to_text = if value == "toggle" {
|
||||
!ml.extend_leader_to_text
|
||||
} else {
|
||||
value == "true"
|
||||
}
|
||||
}
|
||||
"enable_annotation_scale" => {
|
||||
@@ -1130,39 +1181,130 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
||||
value == "true"
|
||||
}
|
||||
}
|
||||
"background_mask_fill_on" => {
|
||||
ml.context.background_mask_fill_on = if value == "toggle" {
|
||||
!ml.context.background_mask_fill_on
|
||||
} else {
|
||||
value == "true"
|
||||
}
|
||||
}
|
||||
"background_scale_factor" => {
|
||||
if let Some(v) = f64(value) {
|
||||
if v > 0.0 {
|
||||
ml.context.background_scale_factor = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
"background_transparency" => {
|
||||
if let Ok(v) = value.trim().parse::<i32>() {
|
||||
ml.context.background_transparency = v.clamp(0, 90);
|
||||
}
|
||||
}
|
||||
"dogleg_length" => {
|
||||
if let Some(v) = f64(value) {
|
||||
ml.dogleg_length = v;
|
||||
ml.property_override_flags
|
||||
.insert(MultiLeaderPropertyOverrideFlags::LANDING_DISTANCE);
|
||||
}
|
||||
}
|
||||
"arrowhead_size" => {
|
||||
if let Some(v) = f64(value) {
|
||||
ml.arrowhead_size = v;
|
||||
ml.context.arrowhead_size = v * ml.context.scale_factor.max(1.0e-12);
|
||||
for root in &mut ml.context.leader_roots {
|
||||
for line in &mut root.lines {
|
||||
line.arrowhead_size = v;
|
||||
line.override_flags.insert(
|
||||
acadrust::entities::LeaderLinePropertyOverrideFlags::ARROWHEAD_SIZE,
|
||||
);
|
||||
}
|
||||
}
|
||||
ml.property_override_flags
|
||||
.insert(MultiLeaderPropertyOverrideFlags::ARROWHEAD_SIZE);
|
||||
}
|
||||
}
|
||||
"scale_factor" => {
|
||||
if let Some(v) = f64(value) {
|
||||
ml.scale_factor = v;
|
||||
if v > 0.0 {
|
||||
let old = ml.context.scale_factor.max(1.0e-12);
|
||||
let ratio = v / old;
|
||||
ml.scale_factor = v;
|
||||
ml.context.scale_factor = v;
|
||||
ml.context.arrowhead_size *= ratio;
|
||||
ml.context.text_height *= ratio;
|
||||
ml.context.landing_gap *= ratio;
|
||||
ml.block_scale.x *= ratio;
|
||||
ml.block_scale.y *= ratio;
|
||||
ml.block_scale.z *= ratio;
|
||||
ml.context.block_content_scale = ml.block_scale;
|
||||
if let Some(root) = ml.context.leader_roots.first() {
|
||||
let anchor = root.connection_point;
|
||||
for point in [
|
||||
&mut ml.context.content_base_point,
|
||||
&mut ml.context.text_location,
|
||||
&mut ml.context.block_content_location,
|
||||
] {
|
||||
point.x = anchor.x + (point.x - anchor.x) * ratio;
|
||||
point.y = anchor.y + (point.y - anchor.y) * ratio;
|
||||
point.z = anchor.z + (point.z - anchor.z) * ratio;
|
||||
}
|
||||
}
|
||||
for root in &mut ml.context.leader_roots {
|
||||
root.landing_distance *= ratio;
|
||||
}
|
||||
ml.property_override_flags
|
||||
.insert(MultiLeaderPropertyOverrideFlags::SCALE_FACTOR);
|
||||
}
|
||||
}
|
||||
}
|
||||
"landing_distance" => {
|
||||
if let Some(v) = f64(value) {
|
||||
ml.dogleg_length = v;
|
||||
if let Some(root) = ml.context.leader_roots.first_mut() {
|
||||
root.landing_distance = v;
|
||||
let display = v * ml.context.scale_factor.max(1.0e-12);
|
||||
for root in &mut ml.context.leader_roots {
|
||||
root.landing_distance = display;
|
||||
}
|
||||
place_content_after_landing(ml);
|
||||
ml.property_override_flags
|
||||
.insert(MultiLeaderPropertyOverrideFlags::LANDING_DISTANCE);
|
||||
}
|
||||
}
|
||||
"landing_gap" => {
|
||||
if let Some(v) = f64(value) {
|
||||
ml.context.landing_gap = v;
|
||||
ml.context.landing_gap = v * ml.context.scale_factor.max(1.0e-12);
|
||||
place_content_after_landing(ml);
|
||||
ml.property_override_flags
|
||||
.insert(MultiLeaderPropertyOverrideFlags::LANDING_GAP);
|
||||
}
|
||||
}
|
||||
"block_rotation" => {
|
||||
if let Some(v) = f64(value) {
|
||||
ml.block_rotation = v.to_radians();
|
||||
ml.context.block_rotation = ml.block_rotation;
|
||||
ml.property_override_flags
|
||||
.insert(MultiLeaderPropertyOverrideFlags::BLOCK_CONTENT_ROTATION);
|
||||
}
|
||||
}
|
||||
"block_scale" => {
|
||||
if let Some(v) = f64(value) {
|
||||
if v > 0.0 {
|
||||
ml.block_scale = acadrust::types::Vector3::new(v, v, v);
|
||||
ml.context.block_content_scale = ml.block_scale;
|
||||
ml.property_override_flags
|
||||
.insert(MultiLeaderPropertyOverrideFlags::BLOCK_CONTENT_SCALE);
|
||||
}
|
||||
}
|
||||
}
|
||||
"block_connection_type" => {
|
||||
ml.block_connection_type = if value == "Insertion point" {
|
||||
BlockContentConnectionType::BasePoint
|
||||
} else {
|
||||
BlockContentConnectionType::BlockExtents
|
||||
};
|
||||
ml.context.block_connection_type = ml.block_connection_type;
|
||||
ml.property_override_flags
|
||||
.insert(MultiLeaderPropertyOverrideFlags::BLOCK_CONTENT_CONNECTION);
|
||||
}
|
||||
"conn_x" => {
|
||||
if let (Some(v), Some(root)) = (f64(value), ml.context.leader_roots.first_mut()) {
|
||||
root.connection_point.x = v;
|
||||
@@ -1181,18 +1323,26 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
||||
"text_left_attachment" => {
|
||||
ml.text_left_attachment = parse_attachment(value);
|
||||
ml.context.text_left_attachment = parse_attachment(value);
|
||||
ml.property_override_flags
|
||||
.insert(MultiLeaderPropertyOverrideFlags::TEXT_LEFT_ATTACHMENT);
|
||||
}
|
||||
"text_right_attachment" => {
|
||||
ml.text_right_attachment = parse_attachment(value);
|
||||
ml.context.text_right_attachment = parse_attachment(value);
|
||||
ml.property_override_flags
|
||||
.insert(MultiLeaderPropertyOverrideFlags::TEXT_RIGHT_ATTACHMENT);
|
||||
}
|
||||
"text_top_attachment" => {
|
||||
ml.text_top_attachment = parse_attachment(value);
|
||||
ml.context.text_top_attachment = parse_attachment(value);
|
||||
ml.property_override_flags
|
||||
.insert(MultiLeaderPropertyOverrideFlags::TEXT_TOP_ATTACHMENT);
|
||||
}
|
||||
"text_bottom_attachment" => {
|
||||
ml.text_bottom_attachment = parse_attachment(value);
|
||||
ml.context.text_bottom_attachment = parse_attachment(value);
|
||||
ml.property_override_flags
|
||||
.insert(MultiLeaderPropertyOverrideFlags::TEXT_BOTTOM_ATTACHMENT);
|
||||
}
|
||||
"text_alignment" => {
|
||||
ml.text_alignment = match value {
|
||||
@@ -1205,6 +1355,8 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
||||
"Right" => TextAlignmentType::Right,
|
||||
_ => TextAlignmentType::Left,
|
||||
};
|
||||
ml.property_override_flags
|
||||
.insert(MultiLeaderPropertyOverrideFlags::TEXT_ALIGNMENT);
|
||||
}
|
||||
"text_flow_direction" => {
|
||||
ml.context.text_flow_direction = match value {
|
||||
@@ -1218,6 +1370,16 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
||||
"Vertical" => TextAttachmentDirectionType::Vertical,
|
||||
_ => TextAttachmentDirectionType::Horizontal,
|
||||
};
|
||||
if ml.text_attachment_direction == TextAttachmentDirectionType::Vertical {
|
||||
ml.enable_dogleg = false;
|
||||
ml.property_override_flags
|
||||
.insert(MultiLeaderPropertyOverrideFlags::ENABLE_DOGLEG);
|
||||
}
|
||||
for root in &mut ml.context.leader_roots {
|
||||
root.text_attachment_direction = ml.text_attachment_direction;
|
||||
}
|
||||
ml.property_override_flags
|
||||
.insert(MultiLeaderPropertyOverrideFlags::TEXT_ATTACHMENT_DIRECTION);
|
||||
}
|
||||
"line_space_style" => {
|
||||
ml.context.line_spacing_style = match value {
|
||||
@@ -1233,6 +1395,13 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
||||
"text_rotation" => {
|
||||
if let Some(v) = f64(value) {
|
||||
ml.context.text_rotation = v.to_radians();
|
||||
ml.context.text_direction = acadrust::types::Vector3::new(
|
||||
ml.context.text_rotation.cos(),
|
||||
ml.context.text_rotation.sin(),
|
||||
0.0,
|
||||
);
|
||||
ml.property_override_flags
|
||||
.insert(MultiLeaderPropertyOverrideFlags::TEXT_ANGLE);
|
||||
}
|
||||
}
|
||||
"line_spacing" => {
|
||||
@@ -1254,6 +1423,27 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
fn place_content_after_landing(ml: &mut MultiLeader) {
|
||||
let Some(root) = ml.context.leader_roots.first() else {
|
||||
return;
|
||||
};
|
||||
let length = (root.direction.x * root.direction.x + root.direction.y * root.direction.y).sqrt();
|
||||
let (ux, uy) = if length > 1.0e-12 {
|
||||
(root.direction.x / length, root.direction.y / length)
|
||||
} else {
|
||||
(1.0, 0.0)
|
||||
};
|
||||
let distance = root.landing_distance.max(0.0) + ml.context.landing_gap.max(0.0);
|
||||
let location = acadrust::types::Vector3::new(
|
||||
root.connection_point.x + ux * distance,
|
||||
root.connection_point.y + uy * distance,
|
||||
root.connection_point.z,
|
||||
);
|
||||
ml.context.content_base_point = location;
|
||||
ml.context.text_location = location;
|
||||
ml.context.block_content_location = location;
|
||||
}
|
||||
|
||||
fn parse_attachment(s: &str) -> TextAttachmentType {
|
||||
match s {
|
||||
"Top of top line" => TextAttachmentType::TopOfTopLine,
|
||||
@@ -1758,9 +1948,7 @@ impl MultiLeaderTess for MultiLeader {
|
||||
}
|
||||
}
|
||||
|
||||
// Vertically-attached leaders (text above/below) have no dogleg:
|
||||
// the leader meets the text edge directly, so the short horizontal
|
||||
// dash would be a stray mark AutoCAD never draws.
|
||||
// Vertically attached leaders meet the text edge directly.
|
||||
let vertical_attach = matches!(
|
||||
ml.text_attachment_direction,
|
||||
acadrust::entities::multileader::TextAttachmentDirectionType::Vertical
|
||||
@@ -1785,11 +1973,7 @@ impl MultiLeaderTess for MultiLeader {
|
||||
// for a rotated leader that is the angled baseline, not world
|
||||
// X. Roots without a usable direction keep the legacy
|
||||
// horizontal toward the text side.
|
||||
let (ux, uy) = ml
|
||||
.context
|
||||
.leader_roots
|
||||
.first()
|
||||
.map(|r| (r.direction.x, r.direction.y))
|
||||
let (ux, uy) = Some((root.direction.x, root.direction.y))
|
||||
.filter(|(x, y)| (x * x + y * y).sqrt() > 1e-9)
|
||||
.map(|(x, y)| {
|
||||
let l = (x * x + y * y).sqrt();
|
||||
@@ -1804,6 +1988,15 @@ impl MultiLeaderTess for MultiLeader {
|
||||
points.push(nan);
|
||||
points.push(cp_f);
|
||||
points.push(landing_end);
|
||||
if ml.extend_leader_to_text {
|
||||
let gap = ml.context.landing_gap.max(0.0)
|
||||
* context_scale_correction as f64;
|
||||
points.push([
|
||||
(text_loc_w.x - ux * gap) as f32,
|
||||
(text_loc_w.y - uy * gap) as f32,
|
||||
text_loc_w.z as f32,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1856,31 +2049,66 @@ impl MultiLeaderTess for MultiLeader {
|
||||
// synthetic Insert supplies that context to the shared scene graph;
|
||||
// every child then uses its normal tessellator.
|
||||
if ml.content_type == LeaderContentType::Block && ml.context.has_block_contents {
|
||||
let block_name = match ml.block_content_handle {
|
||||
let block_record = match ml.block_content_handle {
|
||||
Some(h) if !h.is_null() => document
|
||||
.block_records
|
||||
.iter()
|
||||
.find(|br| br.handle == h)
|
||||
.map(|br| br.name.clone()),
|
||||
.cloned(),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(block_name) = block_name {
|
||||
if let Some(block_record) = block_record {
|
||||
let block_name = block_record.name.clone();
|
||||
let block_color = if selected {
|
||||
line_color
|
||||
} else {
|
||||
color_or_inherit(&ml.block_content_color, entity_color)
|
||||
};
|
||||
let mut synth_ins =
|
||||
acadrust::entities::Insert::new(block_name, ml.context.block_content_location);
|
||||
let mut insertion = ml.context.block_content_location;
|
||||
if ml.block_connection_type == BlockContentConnectionType::BlockExtents {
|
||||
let mut bounds = [f32::MAX, f32::MAX, f32::MIN, f32::MIN];
|
||||
for entity_handle in &block_record.entity_handles {
|
||||
if let Some(entity) = document.get_entity(*entity_handle) {
|
||||
let aabb = crate::scene::convert::tess::entity_aabb(entity);
|
||||
if aabb != WireModel::UNBOUNDED_AABB {
|
||||
bounds[0] = bounds[0].min(aabb[0]);
|
||||
bounds[1] = bounds[1].min(aabb[1]);
|
||||
bounds[2] = bounds[2].max(aabb[2]);
|
||||
bounds[3] = bounds[3].max(aabb[3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if bounds[0] <= bounds[2] && bounds[1] <= bounds[3] {
|
||||
let to_block = Transform::rotation(-ml.block_rotation);
|
||||
let from_left = ml
|
||||
.context
|
||||
.leader_roots
|
||||
.first()
|
||||
.map_or(true, |root| {
|
||||
to_block.apply_vector([
|
||||
root.direction.x,
|
||||
root.direction.y,
|
||||
])[0]
|
||||
>= 0.0
|
||||
});
|
||||
let anchor_x = if from_left { bounds[0] } else { bounds[2] } as f64;
|
||||
let anchor_y = ((bounds[1] + bounds[3]) * 0.5) as f64;
|
||||
let local_x =
|
||||
(anchor_x - block_record.base_point.x) * ml.block_scale.x;
|
||||
let local_y =
|
||||
(anchor_y - block_record.base_point.y) * ml.block_scale.y;
|
||||
let rotated = Transform::rotation(ml.block_rotation)
|
||||
.apply_vector([local_x, local_y]);
|
||||
insertion.x -= rotated[0];
|
||||
insertion.y -= rotated[1];
|
||||
}
|
||||
}
|
||||
let mut synth_ins = acadrust::entities::Insert::new(block_name, insertion);
|
||||
synth_ins.set_x_scale(ml.block_scale.x);
|
||||
synth_ins.set_y_scale(ml.block_scale.y);
|
||||
synth_ins.set_z_scale(ml.block_scale.z);
|
||||
synth_ins.rotation = ml.block_rotation;
|
||||
synth_ins.common.layer = ml.common.layer.clone();
|
||||
// block_connection_type (BlockExtents vs BasePoint) chooses the
|
||||
// anchor when *creating* the multileader; at render time the
|
||||
// file's stored leader endpoints already encode that choice.
|
||||
let _ = ml.block_connection_type;
|
||||
let depths = rustc_hash::FxHashMap::default();
|
||||
let graph = crate::scene::render_graph::RenderSceneGraph::new(
|
||||
document,
|
||||
@@ -1956,12 +2184,7 @@ impl MultiLeaderTess for MultiLeader {
|
||||
let local_ins_x = (ins.x) as f32;
|
||||
let z = (ins.z) as f32;
|
||||
|
||||
// Rotation: the annotation context is AutoCAD's BAKED display
|
||||
// state — text_direction / text_rotation already reflect whatever
|
||||
// the style's angle setting produced at edit time. Re-deriving
|
||||
// from ml.text_angle_type here (e.g. forcing Horizontal to 0)
|
||||
// un-rotated leaders whose stored context is angled; the angle
|
||||
// type only guides recomputation during edits, not display.
|
||||
// The context stores the resolved display rotation.
|
||||
let td = ctx.text_direction;
|
||||
let mut rot = if td.x.abs() > 1e-9 || td.y.abs() > 1e-9 {
|
||||
(td.y as f32).atan2(td.x as f32)
|
||||
@@ -2013,14 +2236,14 @@ impl MultiLeaderTess for MultiLeader {
|
||||
// anchor within the text block (Left/Center/Right) — honour it
|
||||
// instead of guessing from the leader side.
|
||||
use acadrust::entities::multileader::{
|
||||
TextAttachmentDirectionType, TextAttachmentPointType,
|
||||
TextAttachmentDirectionType,
|
||||
};
|
||||
// The context's attachment point exists in every DWG version;
|
||||
// the entity-level copy only exists from R2010 on.
|
||||
let h_anchor: f32 = match ctx.text_attachment_point {
|
||||
TextAttachmentPointType::Left => 0.0,
|
||||
TextAttachmentPointType::Center => 0.5,
|
||||
TextAttachmentPointType::Right => 1.0,
|
||||
let h_anchor: f32 = match ctx.text_alignment {
|
||||
TextAlignmentType::Left => 0.0,
|
||||
TextAlignmentType::Center => 0.5,
|
||||
TextAlignmentType::Right => 1.0,
|
||||
};
|
||||
// Pick the vertical-anchor attachment based on text_attachment_direction:
|
||||
// Horizontal — leader attaches left/right; use ml.text_left_attachment
|
||||
@@ -2273,14 +2496,9 @@ impl MultiLeaderTess for MultiLeader {
|
||||
|
||||
// Text frame / background-fill rectangle in local frame, then rotated to WCS.
|
||||
if ml.text_frame || ctx.background_fill_enabled {
|
||||
// Frame/fill offset from the glyphs: the context's landing gap
|
||||
// (AutoCAD spaces the box by exactly that), with the old
|
||||
// quarter-height heuristic as the no-gap fallback.
|
||||
let pad = if ctx.landing_gap > 0.0 {
|
||||
ctx.landing_gap as f32
|
||||
} else {
|
||||
height * 0.25
|
||||
};
|
||||
// Border offset is a scale factor around the laid-out text box.
|
||||
let pad = height
|
||||
* ((ctx.background_scale_factor.max(1.0) as f32 - 1.0) * 0.5);
|
||||
// Box the glyphs that were actually laid out (valid for
|
||||
// vertical flow too); the metric-derived box is only the
|
||||
// no-glyph fallback.
|
||||
@@ -2311,11 +2529,16 @@ impl MultiLeaderTess for MultiLeader {
|
||||
|
||||
// Background fill — emit two triangles; renders under the text strokes.
|
||||
if ctx.background_fill_enabled {
|
||||
let fill_color = if selected {
|
||||
let mut fill_color = if selected {
|
||||
line_color
|
||||
} else if ctx.background_mask_fill_on {
|
||||
bg_color
|
||||
} else {
|
||||
color_or_inherit(&ctx.background_fill_color, entity_color)
|
||||
};
|
||||
fill_color[3] *=
|
||||
(1.0 - ctx.background_transparency.clamp(0, 90) as f32 / 100.0)
|
||||
.clamp(0.1, 1.0);
|
||||
let fill_tris: Vec<[f32; 3]> = vec![
|
||||
wcs_corners[0],
|
||||
wcs_corners[1],
|
||||
|
||||
+344
-48
@@ -3,12 +3,14 @@ use crate::t;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{
|
||||
edit_prop as edit, parse_f64, ro_prop as ro, square_grip, stepper_prop as stepper,
|
||||
edit_prop as edit, format_length, parse_f64, ro_prop as ro, square_grip,
|
||||
stepper_prop as stepper,
|
||||
};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
|
||||
use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject};
|
||||
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||||
use crate::scene::model::wire_model::TangentGeom;
|
||||
use cadkernel::space::{NurbsCurve3, Vec3};
|
||||
|
||||
// ── Polyline (old-style 3D heavy polyline) ────────────────────────────────────
|
||||
|
||||
@@ -783,20 +785,191 @@ impl Transformable for Polyline2D {
|
||||
|
||||
// ── Polyline3D ────────────────────────────────────────────────────────────────
|
||||
|
||||
const POLY3D_SPLINE_POINT: i32 = 8;
|
||||
const POLY3D_SPLINE_CONTROL: i32 = 16;
|
||||
const POLY3D_VERTEX: i32 = 32;
|
||||
|
||||
fn polyline3d_control_indices(pl: &Polyline3D) -> Vec<usize> {
|
||||
if pl
|
||||
.vertices
|
||||
.iter()
|
||||
.any(|vertex| vertex.flags & POLY3D_SPLINE_CONTROL != 0)
|
||||
{
|
||||
return pl
|
||||
.vertices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, vertex)| {
|
||||
(vertex.flags & POLY3D_SPLINE_CONTROL != 0).then_some(index)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
let controls: Vec<usize> = pl
|
||||
.vertices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, vertex)| {
|
||||
(vertex.flags & POLY3D_SPLINE_POINT == 0).then_some(index)
|
||||
})
|
||||
.collect();
|
||||
if controls.is_empty() {
|
||||
(0..pl.vertices.len()).collect()
|
||||
} else {
|
||||
controls
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn polyline3d_control_vertex_count(pl: &Polyline3D) -> usize {
|
||||
polyline3d_control_indices(pl).len()
|
||||
}
|
||||
|
||||
fn polyline3d_controls(pl: &Polyline3D) -> Vec<acadrust::entities::Vertex3DPolyline> {
|
||||
polyline3d_control_indices(pl)
|
||||
.into_iter()
|
||||
.filter_map(|index| pl.vertices.get(index).cloned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn polyline3d_wire_points(pl: &Polyline3D) -> Vec<[f64; 3]> {
|
||||
let curve: Vec<[f64; 3]> = pl
|
||||
.vertices
|
||||
.iter()
|
||||
.filter(|vertex| vertex.flags & POLY3D_SPLINE_POINT != 0)
|
||||
.map(|vertex| [vertex.position.x, vertex.position.y, vertex.position.z])
|
||||
.collect();
|
||||
if !curve.is_empty() {
|
||||
curve
|
||||
} else {
|
||||
polyline3d_controls(pl)
|
||||
.iter()
|
||||
.map(|vertex| [vertex.position.x, vertex.position.y, vertex.position.z])
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn polyline3d_length(pl: &Polyline3D) -> f64 {
|
||||
let points = polyline3d_wire_points(pl);
|
||||
let mut length = points
|
||||
.windows(2)
|
||||
.map(|pair| Vec3::from(pair[0]).distance(Vec3::from(pair[1])))
|
||||
.sum::<f64>();
|
||||
if pl.is_closed() && points.len() >= 2 {
|
||||
length += Vec3::from(points[0])
|
||||
.distance(Vec3::from(points[points.len() - 1]));
|
||||
}
|
||||
length
|
||||
}
|
||||
|
||||
fn polyline3d_spline_samples(
|
||||
controls: &[[f64; 3]],
|
||||
degree: usize,
|
||||
closed: bool,
|
||||
) -> Option<Vec<[f64; 3]>> {
|
||||
if !controls.iter().flatten().all(|value| value.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
let curve = if closed {
|
||||
let mut periodic_controls = controls.to_vec();
|
||||
periodic_controls.extend(controls.iter().take(degree).copied());
|
||||
let knots = (0..periodic_controls.len() + degree + 1)
|
||||
.map(|index| index as f64)
|
||||
.collect();
|
||||
NurbsCurve3::new(degree, periodic_controls, knots, None)?.with_periodicity(true)
|
||||
} else {
|
||||
NurbsCurve3::new(degree, controls.to_vec(), Vec::new(), None)?
|
||||
};
|
||||
let mut samples = curve.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE);
|
||||
if closed
|
||||
&& samples.len() >= 2
|
||||
&& Vec3::from(samples[0]).distance(Vec3::from(samples[samples.len() - 1])) <= 1.0e-9
|
||||
{
|
||||
samples.pop();
|
||||
}
|
||||
Some(samples)
|
||||
}
|
||||
|
||||
fn rebuild_polyline3d_fit(pl: &mut Polyline3D) -> bool {
|
||||
use acadrust::entities::polyline3d::SmoothSurfaceType as SST;
|
||||
|
||||
let mut controls = polyline3d_controls(pl);
|
||||
if pl.smooth_type == SST::None {
|
||||
for vertex in &mut controls {
|
||||
vertex.flags =
|
||||
(vertex.flags & !(POLY3D_SPLINE_POINT | POLY3D_SPLINE_CONTROL)) | POLY3D_VERTEX;
|
||||
}
|
||||
pl.vertices = controls;
|
||||
pl.flags.spline_fit = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if pl.smooth_type == SST::Bezier {
|
||||
pl.smooth_type = SST::CubicBSpline;
|
||||
}
|
||||
let degree = match pl.smooth_type {
|
||||
SST::QuadraticBSpline => 2,
|
||||
SST::CubicBSpline => 3,
|
||||
SST::Bezier => unreachable!(),
|
||||
SST::None => unreachable!(),
|
||||
};
|
||||
if controls.len() <= degree {
|
||||
return false;
|
||||
}
|
||||
let points: Vec<[f64; 3]> = controls
|
||||
.iter()
|
||||
.map(|vertex| [vertex.position.x, vertex.position.y, vertex.position.z])
|
||||
.collect();
|
||||
let Some(samples) = polyline3d_spline_samples(&points, degree, pl.is_closed()) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
for vertex in &mut controls {
|
||||
vertex.flags = (vertex.flags & !(POLY3D_SPLINE_POINT | POLY3D_SPLINE_CONTROL))
|
||||
| POLY3D_VERTEX
|
||||
| POLY3D_SPLINE_CONTROL;
|
||||
}
|
||||
let vertex_layer = controls
|
||||
.first()
|
||||
.map(|vertex| vertex.layer.clone())
|
||||
.unwrap_or_else(|| "0".to_string());
|
||||
let curve_vertices = samples.into_iter().map(|point| {
|
||||
let mut vertex = acadrust::entities::Vertex3DPolyline::from_xyz(
|
||||
point[0], point[1], point[2],
|
||||
);
|
||||
vertex.layer = vertex_layer.clone();
|
||||
vertex.flags = POLY3D_VERTEX | POLY3D_SPLINE_POINT;
|
||||
vertex
|
||||
});
|
||||
controls.extend(curve_vertices);
|
||||
pl.vertices = controls;
|
||||
pl.flags.spline_fit = true;
|
||||
true
|
||||
}
|
||||
|
||||
fn tessellate_polyline3d(pl: &Polyline3D) -> RenderEntity {
|
||||
let to_pt = |v: &acadrust::entities::Vertex3DPolyline| -> [f64; 3] {
|
||||
[v.position.x, v.position.y, v.position.z]
|
||||
};
|
||||
|
||||
// DXF vertex flags: 8 = spline-fit curve point, 16 = spline frame control point.
|
||||
// When spline-fit vertices are present use them for the wire and control points for snap;
|
||||
// otherwise treat all vertices uniformly.
|
||||
let spline_curve: Vec<_> = pl.vertices.iter().filter(|v| v.flags & 8 != 0).collect();
|
||||
let ctrl_pts: Vec<_> = pl.vertices.iter().filter(|v| v.flags & 16 != 0).collect();
|
||||
// Spline samples draw the wire; frame vertices remain snap points.
|
||||
let spline_curve: Vec<_> = pl
|
||||
.vertices
|
||||
.iter()
|
||||
.filter(|v| v.flags & POLY3D_SPLINE_POINT != 0)
|
||||
.collect();
|
||||
let ctrl_pts: Vec<_> = pl
|
||||
.vertices
|
||||
.iter()
|
||||
.filter(|v| v.flags & POLY3D_SPLINE_CONTROL != 0)
|
||||
.collect();
|
||||
|
||||
let (wire_pts, key_verts) = if !spline_curve.is_empty() {
|
||||
let wire: Vec<[f64; 3]> = spline_curve.iter().map(|v| to_pt(v)).collect();
|
||||
let ctrl: Vec<[f64; 3]> = ctrl_pts.iter().map(|v| to_pt(v)).collect();
|
||||
let ctrl: Vec<[f64; 3]> = if ctrl_pts.is_empty() {
|
||||
polyline3d_controls(pl).iter().map(to_pt).collect()
|
||||
} else {
|
||||
ctrl_pts.iter().map(|v| to_pt(v)).collect()
|
||||
};
|
||||
(wire, ctrl)
|
||||
} else {
|
||||
let pts: Vec<[f64; 3]> = pl.vertices.iter().map(to_pt).collect();
|
||||
@@ -826,8 +999,9 @@ impl RenderConvertible for Polyline3D {
|
||||
|
||||
impl Grippable for Polyline3D {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
self.vertices
|
||||
.iter()
|
||||
polyline3d_control_indices(self)
|
||||
.into_iter()
|
||||
.filter_map(|index| self.vertices.get(index))
|
||||
.enumerate()
|
||||
.map(|(i, v)| {
|
||||
square_grip(
|
||||
@@ -839,7 +1013,8 @@ impl Grippable for Polyline3D {
|
||||
}
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
if let Some(v) = self.vertices.get_mut(grip_id) {
|
||||
let raw_index = polyline3d_control_indices(self).get(grip_id).copied();
|
||||
if let Some(v) = raw_index.and_then(|index| self.vertices.get_mut(index)) {
|
||||
match apply {
|
||||
GripApply::Translate(d) => {
|
||||
v.position.x += d.x as f64;
|
||||
@@ -852,12 +1027,19 @@ impl Grippable for Polyline3D {
|
||||
v.position.z = p.z as f64;
|
||||
}
|
||||
}
|
||||
if self.smooth_type
|
||||
!= acadrust::entities::polyline3d::SmoothSurfaceType::None
|
||||
{
|
||||
let _ = rebuild_polyline3d_fit(self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn grip_menu(&self, _grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
|
||||
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
|
||||
vec![
|
||||
use acadrust::entities::polyline3d::SmoothSurfaceType as SST;
|
||||
|
||||
let mut items = vec![
|
||||
GripMenuItem {
|
||||
label: "Stretch",
|
||||
action: GripMenuAction::Stretch,
|
||||
@@ -866,40 +1048,75 @@ impl Grippable for Polyline3D {
|
||||
label: "Add Vertex",
|
||||
action: GripMenuAction::AddVertex,
|
||||
},
|
||||
GripMenuItem {
|
||||
];
|
||||
let minimum = match self.smooth_type {
|
||||
SST::None if self.is_closed() => 3,
|
||||
SST::None => 2,
|
||||
SST::QuadraticBSpline => 3,
|
||||
SST::CubicBSpline | SST::Bezier => 4,
|
||||
};
|
||||
if polyline3d_control_vertex_count(self) > minimum {
|
||||
items.push(GripMenuItem {
|
||||
label: "Remove Vertex",
|
||||
action: GripMenuAction::RemoveVertex,
|
||||
},
|
||||
]
|
||||
});
|
||||
}
|
||||
items
|
||||
}
|
||||
|
||||
fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) {
|
||||
use crate::scene::model::object::GripMenuAction as A;
|
||||
let n = self.vertices.len();
|
||||
use acadrust::entities::polyline3d::SmoothSurfaceType as SST;
|
||||
|
||||
let mut controls = polyline3d_controls(self);
|
||||
let n = controls.len();
|
||||
match action {
|
||||
A::AddVertex if grip_id < n => {
|
||||
if grip_id == n - 1 && !self.is_closed() {
|
||||
let mut new_v = self.vertices[grip_id].clone();
|
||||
let mut new_v = controls[grip_id].clone();
|
||||
if n >= 2 {
|
||||
let previous = &controls[n - 2].position;
|
||||
let last = &controls[n - 1].position;
|
||||
let next = Vec3::new(previous.x, previous.y, previous.z).lerp(
|
||||
Vec3::new(last.x, last.y, last.z),
|
||||
2.0,
|
||||
);
|
||||
new_v.position.x = next.x;
|
||||
new_v.position.y = next.y;
|
||||
new_v.position.z = next.z;
|
||||
}
|
||||
new_v.handle = acadrust::Handle::NULL;
|
||||
self.vertices.push(new_v);
|
||||
return;
|
||||
controls.push(new_v);
|
||||
} else {
|
||||
let i1 = (grip_id + 1) % n;
|
||||
let v0 = &controls[grip_id];
|
||||
let v1 = &controls[i1];
|
||||
let midpoint = Vec3::new(v0.position.x, v0.position.y, v0.position.z).lerp(
|
||||
Vec3::new(v1.position.x, v1.position.y, v1.position.z),
|
||||
0.5,
|
||||
);
|
||||
let mut new_v = v0.clone();
|
||||
new_v.position.x = midpoint.x;
|
||||
new_v.position.y = midpoint.y;
|
||||
new_v.position.z = midpoint.z;
|
||||
new_v.handle = acadrust::Handle::NULL;
|
||||
controls.insert(grip_id + 1, new_v);
|
||||
}
|
||||
let i1 = (grip_id + 1) % n;
|
||||
let v0 = &self.vertices[grip_id];
|
||||
let v1 = &self.vertices[i1];
|
||||
let mx = (v0.position.x + v1.position.x) * 0.5;
|
||||
let my = (v0.position.y + v1.position.y) * 0.5;
|
||||
let mz = (v0.position.z + v1.position.z) * 0.5;
|
||||
let mut new_v = v0.clone();
|
||||
new_v.position.x = mx;
|
||||
new_v.position.y = my;
|
||||
new_v.position.z = mz;
|
||||
new_v.handle = acadrust::Handle::NULL;
|
||||
let insert_at = (grip_id + 1).min(self.vertices.len());
|
||||
self.vertices.insert(insert_at, new_v);
|
||||
self.vertices = controls;
|
||||
let _ = rebuild_polyline3d_fit(self);
|
||||
}
|
||||
A::RemoveVertex if grip_id < n && n > 2 => {
|
||||
self.vertices.remove(grip_id);
|
||||
A::RemoveVertex if grip_id < n => {
|
||||
let minimum = match self.smooth_type {
|
||||
SST::None if self.is_closed() => 3,
|
||||
SST::None => 2,
|
||||
SST::QuadraticBSpline => 3,
|
||||
SST::CubicBSpline | SST::Bezier => 4,
|
||||
};
|
||||
if n > minimum {
|
||||
controls.remove(grip_id);
|
||||
self.vertices = controls;
|
||||
let _ = rebuild_polyline3d_fit(self);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -909,40 +1126,79 @@ impl Grippable for Polyline3D {
|
||||
impl PropertyEditable for Polyline3D {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
||||
use acadrust::entities::polyline3d::SmoothSurfaceType as SST;
|
||||
let n = self.vertices.len();
|
||||
let v0 = self.vertices.first();
|
||||
let vertex_x = v0.map(|v| v.position.x).unwrap_or_default();
|
||||
let vertex_y = v0.map(|v| v.position.y).unwrap_or_default();
|
||||
let vertex_z = v0.map(|v| v.position.z).unwrap_or_default();
|
||||
let control_indices = polyline3d_control_indices(self);
|
||||
let n = control_indices.len();
|
||||
let vi = if n == 0 {
|
||||
0
|
||||
} else {
|
||||
crate::scene::view::dispatch::prop_current_vertex().min(n - 1)
|
||||
};
|
||||
let vertex = control_indices
|
||||
.get(vi)
|
||||
.and_then(|index| self.vertices.get(*index));
|
||||
let vertex_x = vertex.map(|v| v.position.x).unwrap_or_default();
|
||||
let vertex_y = vertex.map(|v| v.position.y).unwrap_or_default();
|
||||
let vertex_z = vertex.map(|v| v.position.z).unwrap_or_default();
|
||||
let vertex_label = if n == 0 {
|
||||
"—".to_string()
|
||||
} else {
|
||||
format!("{} / {}", vi + 1, n)
|
||||
};
|
||||
let fit_smooth = match self.smooth_type {
|
||||
SST::None => "None",
|
||||
SST::QuadraticBSpline => "Quadratic",
|
||||
SST::CubicBSpline => "Cubic",
|
||||
SST::Bezier => "Bezier",
|
||||
SST::CubicBSpline | SST::Bezier => "Cubic",
|
||||
};
|
||||
let mut smooth_options = vec!["None".to_string()];
|
||||
if n >= 3 {
|
||||
smooth_options.push("Quadratic".to_string());
|
||||
}
|
||||
if n >= 4 {
|
||||
smooth_options.push("Cubic".to_string());
|
||||
}
|
||||
|
||||
vec![
|
||||
PropSection {
|
||||
title: t!("Geometry").into_owned(),
|
||||
props: vec![
|
||||
ro(t!("Vertex").as_ref(), "pl3_vertex", if n > 0 { "1" } else { "" }),
|
||||
stepper(
|
||||
t!("Current Vertex").as_ref(),
|
||||
"pl3_current_vertex",
|
||||
vertex_label,
|
||||
),
|
||||
edit(t!("Vertex X").as_ref(), "pl3_vertex_x", vertex_x),
|
||||
edit(t!("Vertex Y").as_ref(), "pl3_vertex_y", vertex_y),
|
||||
edit(t!("Vertex Z").as_ref(), "pl3_vertex_z", vertex_z),
|
||||
ro(
|
||||
t!("Length").as_ref(),
|
||||
"pl3_length",
|
||||
format_length(polyline3d_length(self)),
|
||||
),
|
||||
],
|
||||
},
|
||||
PropSection {
|
||||
title: t!("Misc").into_owned(),
|
||||
props: vec![
|
||||
Property {
|
||||
label: t!("Fit/Smooth").into_owned(),
|
||||
field: "pl3_smooth",
|
||||
value: PropValue::Choice {
|
||||
selected: fit_smooth.to_string(),
|
||||
options: smooth_options,
|
||||
},
|
||||
},
|
||||
Property {
|
||||
label: t!("Closed").into_owned(),
|
||||
field: "pl3_closed",
|
||||
value: PropValue::BoolToggle {
|
||||
field: "pl3_closed",
|
||||
value: self.is_closed(),
|
||||
value: if n >= 3 {
|
||||
PropValue::BoolToggle {
|
||||
field: "pl3_closed",
|
||||
value: self.is_closed(),
|
||||
}
|
||||
} else {
|
||||
PropValue::ReadOnly("No".to_string())
|
||||
},
|
||||
},
|
||||
ro(t!("Fit/Smooth").as_ref(), "pl3_smooth", fit_smooth),
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -956,25 +1212,65 @@ impl PropertyEditable for Polyline3D {
|
||||
} else {
|
||||
value == "true"
|
||||
};
|
||||
if closed && polyline3d_control_vertex_count(self) < 3 {
|
||||
return;
|
||||
}
|
||||
if closed {
|
||||
self.close();
|
||||
} else {
|
||||
self.open();
|
||||
}
|
||||
if self.smooth_type
|
||||
!= acadrust::entities::polyline3d::SmoothSurfaceType::None
|
||||
{
|
||||
let _ = rebuild_polyline3d_fit(self);
|
||||
}
|
||||
}
|
||||
"pl3_smooth" => {
|
||||
use acadrust::entities::polyline3d::SmoothSurfaceType as SST;
|
||||
let next = match value {
|
||||
"None" => SST::None,
|
||||
"Quadratic" => SST::QuadraticBSpline,
|
||||
"Cubic" => SST::CubicBSpline,
|
||||
_ => return,
|
||||
};
|
||||
let previous = self.smooth_type;
|
||||
self.smooth_type = next;
|
||||
if !rebuild_polyline3d_fit(self) {
|
||||
self.smooth_type = previous;
|
||||
}
|
||||
}
|
||||
"pl3_vertex_x" => {
|
||||
if let (Some(v), Some(vert)) = (parse_f64(value), self.vertices.first_mut()) {
|
||||
let vi = crate::scene::view::dispatch::prop_current_vertex();
|
||||
let raw_index = polyline3d_control_indices(self).get(vi).copied();
|
||||
if let (Some(v), Some(vert)) = (
|
||||
parse_f64(value).filter(|value| value.is_finite()),
|
||||
raw_index.and_then(|index| self.vertices.get_mut(index)),
|
||||
) {
|
||||
vert.position.x = v;
|
||||
let _ = rebuild_polyline3d_fit(self);
|
||||
}
|
||||
}
|
||||
"pl3_vertex_y" => {
|
||||
if let (Some(v), Some(vert)) = (parse_f64(value), self.vertices.first_mut()) {
|
||||
let vi = crate::scene::view::dispatch::prop_current_vertex();
|
||||
let raw_index = polyline3d_control_indices(self).get(vi).copied();
|
||||
if let (Some(v), Some(vert)) = (
|
||||
parse_f64(value).filter(|value| value.is_finite()),
|
||||
raw_index.and_then(|index| self.vertices.get_mut(index)),
|
||||
) {
|
||||
vert.position.y = v;
|
||||
let _ = rebuild_polyline3d_fit(self);
|
||||
}
|
||||
}
|
||||
"pl3_vertex_z" => {
|
||||
if let (Some(v), Some(vert)) = (parse_f64(value), self.vertices.first_mut()) {
|
||||
let vi = crate::scene::view::dispatch::prop_current_vertex();
|
||||
let raw_index = polyline3d_control_indices(self).get(vi).copied();
|
||||
if let (Some(v), Some(vert)) = (
|
||||
parse_f64(value).filter(|value| value.is_finite()),
|
||||
raw_index.and_then(|index| self.vertices.get_mut(index)),
|
||||
) {
|
||||
vert.position.z = v;
|
||||
let _ = rebuild_polyline3d_fit(self);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
||||
Reference in new issue
Block a user