fix(helix): repair parametric editing

This commit is contained in:
Hakan Seven committed 2026-08-28 20:05:36 +03:00
commit 7f6e5c0405
42 files changed
+4567 -1209

No files matched your search

+127 -39
View File
@@ -1,6 +1,6 @@
use acadrust::entities::{Helix, HelixConstraint, Spline};
use acadrust::types::Vector3;
use cadkernel::space::{HelixCurve, HelixDirection, NurbsCurve3};
use cadkernel::space::{HelixCurve, HelixDirection, NurbsCurve3, Vec3};
use glam::DVec3;
use crate::command::EntityTransform;
@@ -15,40 +15,50 @@ use crate::t;
const EPSILON: f64 = 1.0e-9;
fn point(vector: Vector3) -> DVec3 {
DVec3::new(vector.x, vector.y, vector.z)
fn point(vector: Vector3) -> Vec3 {
Vec3::new(vector.x, vector.y, vector.z)
}
fn vector(value: DVec3) -> Vector3 {
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 = spline
let controls: Vec<[f64; 3]> = spline
.control_points
.iter()
.map(|value| [value.x, value.y, value.z])
.collect();
let weights = (spline.weights.len() == spline.control_points.len())
.then(|| spline.weights.clone());
NurbsCurve3::new(spline.degree.max(1) as usize, controls, spline.knots.clone(), weights)
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<(DVec3, DVec3, DVec3)> {
fn axis_frame(helix: &Helix) -> Option<(Vec3, Vec3, Vec3)> {
let base = point(helix.axis_base_point);
let axis = point(helix.axis_vector).normalize_or_zero();
if axis.length_squared() <= EPSILON * EPSILON {
return None;
}
let axis = point(helix.axis_vector).normalize()?;
let radial = point(helix.start_point) - base;
let start_direction = (radial - axis * radial.dot(axis)).normalize_or_zero();
if start_direction.length_squared() <= EPSILON * EPSILON {
return None;
}
let start_direction = (radial - axis * radial.dot(axis)).normalize()?;
Some((base, axis, start_direction))
}
fn radius_from_axis(helix: &Helix, value: DVec3) -> Option<f64> {
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())
@@ -56,7 +66,7 @@ fn radius_from_axis(helix: &Helix, value: DVec3) -> Option<f64> {
fn top_radius(helix: &Helix) -> f64 {
spline_curve(&helix.spline)
.map(|curve| DVec3::from_array(curve.point_at(1.0)))
.map(|curve| Vec3::from(curve.point_at(1.0)))
.and_then(|endpoint| radius_from_axis(helix, endpoint))
.unwrap_or(helix.radius)
}
@@ -79,6 +89,21 @@ fn kernel_curve(helix: &Helix, top_radius: f64) -> Option<HelixCurve> {
})
}
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;
@@ -125,13 +150,13 @@ fn grips(helix: &Helix) -> Vec<GripDef> {
let height = helix.turns * helix.turn_height;
let top_center = base + axis * height;
let top_endpoint = spline_curve(&helix.spline)
.map(|curve| DVec3::from_array(curve.point_at(1.0)))
.map(|curve| Vec3::from(curve.point_at(1.0)))
.unwrap_or(top_center);
vec![
center_grip(0, base),
square_grip(1, point(helix.start_point)),
square_grip(2, top_center),
square_grip(3, top_endpoint),
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)),
]
}
@@ -180,22 +205,34 @@ fn properties(helix: &Helix) -> Vec<PropSection> {
fn apply_geom_prop(helix: &mut Helix, field: &str, value: &str) {
if field == "constrain" {
helix.constraint = if value == t!("Height").as_ref() {
let constraint = if value == t!("Height").as_ref() {
HelixConstraint::Height
} else if value == t!("Turns").as_ref() {
HelixConstraint::Turns
} else {
} 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" {
helix.handedness = value == t!("CCW").as_ref();
let _ = rebuild(helix, top);
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) else {
let Some(number) = parse_f64(value).filter(|number| number.is_finite()) else {
return;
};
let old_base = point(helix.axis_base_point);
@@ -214,7 +251,11 @@ fn apply_geom_prop(helix: &mut Helix, field: &str, value: &str) {
}
"height" if number.abs() > EPSILON => {
if helix.constraint == HelixConstraint::TurnHeight && helix.turn_height.abs() > EPSILON {
helix.turns = (number / helix.turn_height).abs().max(EPSILON);
let turns = number / helix.turn_height;
if turns <= EPSILON {
return;
}
helix.turns = turns;
} else {
helix.turn_height = number / helix.turns.max(EPSILON);
}
@@ -231,8 +272,12 @@ fn apply_geom_prop(helix: &mut Helix, field: &str, value: &str) {
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 = (old_height / number).abs().max(EPSILON);
helix.turns = turns;
}
}
"base_radius" if number > EPSILON => {
@@ -243,30 +288,47 @@ fn apply_geom_prop(helix: &mut Helix, field: &str, value: &str) {
helix.start_point = vector(base + start_direction * number);
}
"top_radius" if number >= 0.0 => {
let _ = rebuild(helix, number);
if !rebuild(helix, number) {
*helix = original;
}
return;
}
_ => return,
}
let _ = rebuild(helix, top);
if !rebuild(helix, top) {
*helix = original;
}
}
fn apply_grip(helix: &mut Helix, grip_id: usize, apply: GripApply) {
let original = helix.clone();
let top = top_radius(helix);
let Some((base, axis, _)) = axis_frame(helix) else {
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 {
@@ -276,19 +338,37 @@ fn apply_grip(helix: &mut Helix, grip_id: usize, apply: GripApply) {
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;
}
helix.axis_vector = vector(axis_vector / height);
let target_axis = axis_vector / height;
if helix.constraint == HelixConstraint::TurnHeight && helix.turn_height.abs() > EPSILON {
helix.turns = (height / helix.turn_height).abs().max(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();
@@ -296,16 +376,24 @@ fn apply_grip(helix: &mut Helix, grip_id: usize, apply: GripApply) {
return;
}
if helix.constraint == HelixConstraint::TurnHeight && helix.turn_height.abs() > EPSILON {
helix.turns = (height / helix.turn_height).abs().max(EPSILON);
let turns = height / helix.turn_height;
if turns <= EPSILON {
return;
}
helix.turns = turns;
} else {
helix.turn_height = height / helix.turns.max(EPSILON);
}
let _ = rebuild(helix, radius);
if !rebuild(helix, radius) {
*helix = original;
}
return;
}
_ => return,
}
let _ = rebuild(helix, top);
if !rebuild(helix, top) {
*helix = original;
}
}
fn apply_transform(helix: &mut Helix, transform: &EntityTransform) {
+3 -5
View File
@@ -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(),
+300 -77
View File
@@ -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
View File
@@ -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);
}
}
_ => {}