forked from 1b-agent/opencadstudio-upstream
first commit
This commit is contained in:
1 parent
d52c6440ff
commit
d2fdac7f1e
274 files changed
+89330
No files matched your search
@@ -0,0 +1,188 @@
|
||||
use acadrust::entities::Arc;
|
||||
use glam::Vec3;
|
||||
use truck_modeling::{builder, Point3};
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{diamond_grip, edit_prop as edit, parse_f64, square_grip};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
|
||||
use crate::scene::acad_to_truck::{TruckEntity, TruckObject};
|
||||
use crate::scene::object::{GripApply, GripDef, PropSection};
|
||||
use crate::scene::wire_model::{SnapHint, TangentGeom};
|
||||
|
||||
const TAU: f64 = std::f64::consts::TAU;
|
||||
|
||||
fn to_truck(arc: &Arc) -> TruckEntity {
|
||||
let cx = arc.center.x;
|
||||
let cy = arc.center.y;
|
||||
let cz = arc.center.z;
|
||||
let r = arc.radius;
|
||||
let sa = arc.start_angle.to_radians();
|
||||
let ea = arc.end_angle.to_radians();
|
||||
let mut end = ea;
|
||||
if end < sa {
|
||||
end += TAU;
|
||||
}
|
||||
let mid_a = sa + (end - sa) * 0.5;
|
||||
let p_start = Point3::new(cx + r * sa.cos(), cy + r * sa.sin(), cz);
|
||||
let p_end = Point3::new(cx + r * ea.cos(), cy + r * ea.sin(), cz);
|
||||
let p_mid = Point3::new(cx + r * mid_a.cos(), cy + r * mid_a.sin(), cz);
|
||||
let v_start = builder::vertex(p_start);
|
||||
let v_end = builder::vertex(p_end);
|
||||
let edge = builder::circle_arc(&v_start, &v_end, p_mid);
|
||||
let cv = Vec3::new(cx as f32, cy as f32, cz as f32);
|
||||
TruckEntity {
|
||||
object: TruckObject::Curve(edge),
|
||||
snap_pts: vec![(cv, SnapHint::Center)],
|
||||
tangent_geoms: vec![TangentGeom::Circle {
|
||||
center: [cx as f32, cy as f32, cz as f32],
|
||||
radius: r as f32,
|
||||
}],
|
||||
key_vertices: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn angle_span(start: f32, end: f32) -> f32 {
|
||||
let mut span = end - start;
|
||||
if span < 0.0 {
|
||||
span += std::f32::consts::TAU;
|
||||
}
|
||||
span
|
||||
}
|
||||
|
||||
fn grips(arc: &Arc) -> Vec<GripDef> {
|
||||
let ctr = Vec3::new(
|
||||
arc.center.x as f32,
|
||||
arc.center.y as f32,
|
||||
arc.center.z as f32,
|
||||
);
|
||||
let r = arc.radius as f32;
|
||||
let sa = (arc.start_angle as f32).to_radians();
|
||||
let ea = (arc.end_angle as f32).to_radians();
|
||||
let ma = sa + angle_span(sa, ea) * 0.5;
|
||||
vec![
|
||||
diamond_grip(0, ctr),
|
||||
square_grip(1, ctr + Vec3::new(r * sa.cos(), r * sa.sin(), 0.0)),
|
||||
square_grip(2, ctr + Vec3::new(r * ea.cos(), r * ea.sin(), 0.0)),
|
||||
diamond_grip(3, ctr + Vec3::new(r * ma.cos(), r * ma.sin(), 0.0)),
|
||||
]
|
||||
}
|
||||
|
||||
fn properties(arc: &Arc) -> PropSection {
|
||||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props: vec![
|
||||
edit("Center X", "center_x", arc.center.x),
|
||||
edit("Center Y", "center_y", arc.center.y),
|
||||
edit("Center Z", "center_z", arc.center.z),
|
||||
edit("Radius", "radius", arc.radius),
|
||||
edit("Start Angle", "start_angle", arc.start_angle),
|
||||
edit("End Angle", "end_angle", arc.end_angle),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_geom_prop(arc: &mut Arc, field: &str, value: &str) {
|
||||
let Some(v) = parse_f64(value) else {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"center_x" => arc.center.x = v,
|
||||
"center_y" => arc.center.y = v,
|
||||
"center_z" => arc.center.z = v,
|
||||
"radius" if v > 0.0 => arc.radius = v,
|
||||
"start_angle" => arc.start_angle = v,
|
||||
"end_angle" => arc.end_angle = v,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_grip(arc: &mut Arc, grip_id: usize, apply: GripApply) {
|
||||
match (grip_id, apply) {
|
||||
(0, GripApply::Translate(d)) => {
|
||||
arc.center.x += d.x as f64;
|
||||
arc.center.y += d.y as f64;
|
||||
arc.center.z += d.z as f64;
|
||||
}
|
||||
(0, GripApply::Absolute(p)) => {
|
||||
arc.center.x = p.x as f64;
|
||||
arc.center.y = p.y as f64;
|
||||
arc.center.z = p.z as f64;
|
||||
}
|
||||
(1, GripApply::Absolute(p)) => {
|
||||
let dx = p.x - arc.center.x as f32;
|
||||
let dy = p.y - arc.center.y as f32;
|
||||
arc.start_angle = (dy as f64).atan2(dx as f64).to_degrees();
|
||||
}
|
||||
(2, GripApply::Absolute(p)) => {
|
||||
let dx = p.x - arc.center.x as f32;
|
||||
let dy = p.y - arc.center.y as f32;
|
||||
arc.end_angle = (dy as f64).atan2(dx as f64).to_degrees();
|
||||
}
|
||||
(3, GripApply::Translate(d)) => {
|
||||
let sa = (arc.start_angle as f32).to_radians();
|
||||
let ea = (arc.end_angle as f32).to_radians();
|
||||
let span = angle_span(sa, ea);
|
||||
let mid_a = sa + span * 0.5;
|
||||
let current_mid_x = arc.center.x as f32 + arc.radius as f32 * mid_a.cos();
|
||||
let current_mid_y = arc.center.y as f32 + arc.radius as f32 * mid_a.sin();
|
||||
let new_mid_x = current_mid_x + d.x;
|
||||
let new_mid_y = current_mid_y + d.y;
|
||||
let dx = new_mid_x - arc.center.x as f32;
|
||||
let dy = new_mid_y - arc.center.y as f32;
|
||||
let new_r = (dx * dx + dy * dy).sqrt() as f64;
|
||||
if new_r > 1e-6 {
|
||||
arc.radius = new_r;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_transform(arc: &mut Arc, t: &EntityTransform) {
|
||||
crate::scene::transform::apply_standard_entity_transform(arc, t, |entity, p1, p2| {
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut entity.center.x,
|
||||
&mut entity.center.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
let dx = (p2.x - p1.x) as f64;
|
||||
let dy = (p2.y - p1.y) as f64;
|
||||
let line_angle_deg = dy.atan2(dx).to_degrees();
|
||||
let tmp = entity.start_angle;
|
||||
entity.start_angle = 2.0 * line_angle_deg - entity.end_angle;
|
||||
entity.end_angle = 2.0 * line_angle_deg - tmp;
|
||||
});
|
||||
}
|
||||
|
||||
impl TruckConvertible for Arc {
|
||||
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
||||
Some(to_truck(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl Grippable for Arc {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
grips(self)
|
||||
}
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
apply_grip(self, grip_id, apply);
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyEditable for Arc {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection {
|
||||
properties(self)
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
apply_geom_prop(self, field, value);
|
||||
}
|
||||
}
|
||||
|
||||
impl Transformable for Arc {
|
||||
fn apply_transform(&mut self, t: &EntityTransform) {
|
||||
apply_transform(self, t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
use acadrust::entities::Circle;
|
||||
use glam::Vec3;
|
||||
use truck_modeling::{builder, Point3, Wire};
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{
|
||||
diamond_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip,
|
||||
};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
|
||||
use crate::scene::acad_to_truck::{TruckEntity, TruckObject};
|
||||
use crate::scene::object::{GripApply, GripDef, PropSection};
|
||||
use crate::scene::wire_model::{SnapHint, TangentGeom};
|
||||
|
||||
fn to_truck(circle: &Circle) -> TruckEntity {
|
||||
let cx = circle.center.x;
|
||||
let cy = circle.center.y;
|
||||
let cz = circle.center.z;
|
||||
let r = circle.radius;
|
||||
let right = builder::vertex(Point3::new(cx + r, cy, cz));
|
||||
let left = builder::vertex(Point3::new(cx - r, cy, cz));
|
||||
let top = Point3::new(cx, cy + r, cz);
|
||||
let bot = Point3::new(cx, cy - r, cz);
|
||||
let upper = builder::circle_arc(&right, &left, top);
|
||||
let lower = builder::circle_arc(&left, &right, bot);
|
||||
let wire: Wire = [upper, lower].into_iter().collect();
|
||||
let cv = Vec3::new(cx as f32, cy as f32, cz as f32);
|
||||
let rf = r as f32;
|
||||
TruckEntity {
|
||||
object: TruckObject::Contour(wire),
|
||||
snap_pts: vec![
|
||||
(cv, SnapHint::Center),
|
||||
(
|
||||
Vec3::new(cx as f32 + rf, cy as f32, cz as f32),
|
||||
SnapHint::Quadrant,
|
||||
),
|
||||
(
|
||||
Vec3::new(cx as f32, cy as f32 + rf, cz as f32),
|
||||
SnapHint::Quadrant,
|
||||
),
|
||||
(
|
||||
Vec3::new(cx as f32 - rf, cy as f32, cz as f32),
|
||||
SnapHint::Quadrant,
|
||||
),
|
||||
(
|
||||
Vec3::new(cx as f32, cy as f32 - rf, cz as f32),
|
||||
SnapHint::Quadrant,
|
||||
),
|
||||
],
|
||||
tangent_geoms: vec![TangentGeom::Circle {
|
||||
center: [cx as f32, cy as f32, cz as f32],
|
||||
radius: rf,
|
||||
}],
|
||||
key_vertices: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn grips(circle: &Circle) -> Vec<GripDef> {
|
||||
let ctr = Vec3::new(
|
||||
circle.center.x as f32,
|
||||
circle.center.y as f32,
|
||||
circle.center.z as f32,
|
||||
);
|
||||
let r = circle.radius as f32;
|
||||
vec![
|
||||
diamond_grip(0, ctr),
|
||||
square_grip(1, ctr + Vec3::new(r, 0.0, 0.0)),
|
||||
square_grip(2, ctr + Vec3::new(0.0, r, 0.0)),
|
||||
square_grip(3, ctr - Vec3::new(r, 0.0, 0.0)),
|
||||
square_grip(4, ctr - Vec3::new(0.0, r, 0.0)),
|
||||
]
|
||||
}
|
||||
|
||||
fn properties(circle: &Circle) -> PropSection {
|
||||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props: vec![
|
||||
edit("Center X", "center_x", circle.center.x),
|
||||
edit("Center Y", "center_y", circle.center.y),
|
||||
edit("Center Z", "center_z", circle.center.z),
|
||||
edit("Radius", "radius", circle.radius),
|
||||
ro(
|
||||
"Diameter",
|
||||
"diameter",
|
||||
format!("{:.4}", circle.radius * 2.0),
|
||||
),
|
||||
ro(
|
||||
"Circumference",
|
||||
"circumference",
|
||||
format!("{:.4}", circle.radius * 2.0 * std::f64::consts::PI),
|
||||
),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_geom_prop(circle: &mut Circle, field: &str, value: &str) {
|
||||
let Some(v) = parse_f64(value) else {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"center_x" => circle.center.x = v,
|
||||
"center_y" => circle.center.y = v,
|
||||
"center_z" => circle.center.z = v,
|
||||
"radius" if v > 0.0 => circle.radius = v,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_grip(circle: &mut Circle, grip_id: usize, apply: GripApply) {
|
||||
match (grip_id, apply) {
|
||||
(0, GripApply::Absolute(p)) => {
|
||||
circle.center.x = p.x as f64;
|
||||
circle.center.y = p.y as f64;
|
||||
circle.center.z = p.z as f64;
|
||||
}
|
||||
(0, GripApply::Translate(d)) => {
|
||||
circle.center.x += d.x as f64;
|
||||
circle.center.y += d.y as f64;
|
||||
circle.center.z += d.z as f64;
|
||||
}
|
||||
(1..=4, GripApply::Absolute(p)) => {
|
||||
let cx = circle.center.x as f32;
|
||||
let cy = circle.center.y as f32;
|
||||
let dx = p.x - cx;
|
||||
let dy = p.y - cy;
|
||||
circle.radius = ((dx * dx + dy * dy) as f64).sqrt();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_transform(circle: &mut Circle, t: &EntityTransform) {
|
||||
crate::scene::transform::apply_standard_entity_transform(circle, t, |entity, p1, p2| {
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut entity.center.x,
|
||||
&mut entity.center.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
impl TruckConvertible for Circle {
|
||||
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
||||
Some(to_truck(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl Grippable for Circle {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
grips(self)
|
||||
}
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
apply_grip(self, grip_id, apply);
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyEditable for Circle {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection {
|
||||
properties(self)
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
apply_geom_prop(self, field, value);
|
||||
}
|
||||
}
|
||||
|
||||
impl Transformable for Circle {
|
||||
fn apply_transform(&mut self, t: &EntityTransform) {
|
||||
apply_transform(self, t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::scene::object::{GripDef, GripShape, PropValue, Property};
|
||||
|
||||
pub fn square_grip(id: usize, world: Vec3) -> GripDef {
|
||||
GripDef {
|
||||
id,
|
||||
world,
|
||||
is_midpoint: false,
|
||||
shape: GripShape::Square,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn diamond_grip(id: usize, world: Vec3) -> GripDef {
|
||||
GripDef {
|
||||
id,
|
||||
world,
|
||||
is_midpoint: true,
|
||||
shape: GripShape::Diamond,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn triangle_grip(id: usize, world: Vec3) -> GripDef {
|
||||
GripDef {
|
||||
id,
|
||||
world,
|
||||
is_midpoint: false,
|
||||
shape: GripShape::Triangle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn edit_prop(label: &'static str, field: &'static str, value: f64) -> Property {
|
||||
Property {
|
||||
label: label.into(),
|
||||
field,
|
||||
value: PropValue::EditText(format!("{value:.4}")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ro_prop(label: &'static str, field: &'static str, value: impl Into<String>) -> Property {
|
||||
Property {
|
||||
label: label.into(),
|
||||
field,
|
||||
value: PropValue::ReadOnly(value.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_f64(value: &str) -> Option<f64> {
|
||||
value.trim().parse::<f64>().ok()
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
use acadrust::entities::{
|
||||
Dimension, DimensionAligned, DimensionAngular2Ln, DimensionAngular3Pt, DimensionBase,
|
||||
DimensionDiameter, DimensionLinear, DimensionOrdinate, DimensionRadius,
|
||||
};
|
||||
use acadrust::Entity;
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{edit_prop as edit, parse_f64, ro_prop as ro};
|
||||
use crate::entities::traits::{PropertyEditable, Transformable};
|
||||
use crate::scene::object::PropSection;
|
||||
|
||||
fn base_props(base: &DimensionBase) -> Vec<crate::scene::object::Property> {
|
||||
vec![
|
||||
crate::scene::object::Property {
|
||||
label: "Text".into(),
|
||||
field: "text",
|
||||
value: crate::scene::object::PropValue::EditText(base.text.clone()),
|
||||
},
|
||||
crate::scene::object::Property {
|
||||
label: "User Text".into(),
|
||||
field: "user_text",
|
||||
value: crate::scene::object::PropValue::EditText(
|
||||
base.user_text.clone().unwrap_or_default(),
|
||||
),
|
||||
},
|
||||
crate::scene::object::Property {
|
||||
label: "Style".into(),
|
||||
field: "style_name",
|
||||
value: crate::scene::object::PropValue::EditText(base.style_name.clone()),
|
||||
},
|
||||
edit("Text X", "text_x", base.text_middle_point.x),
|
||||
edit("Text Y", "text_y", base.text_middle_point.y),
|
||||
edit("Text Z", "text_z", base.text_middle_point.z),
|
||||
edit("Text Rotation", "text_rotation", base.text_rotation),
|
||||
edit("Horizontal Dir", "horizontal_direction", base.horizontal_direction),
|
||||
edit("Line Spacing", "line_spacing_factor", base.line_spacing_factor),
|
||||
ro("Measurement", "measurement", format!("{:.4}", base.actual_measurement)),
|
||||
]
|
||||
}
|
||||
|
||||
fn properties(dim: &Dimension) -> PropSection {
|
||||
let mut props = base_props(dim.base());
|
||||
match dim {
|
||||
Dimension::Aligned(d) => {
|
||||
props.extend(linear_like_props(d.first_point, d.second_point, d.definition_point));
|
||||
props.push(edit("Ext Rotation", "ext_line_rotation", d.ext_line_rotation));
|
||||
}
|
||||
Dimension::Linear(d) => {
|
||||
props.extend(linear_like_props(d.first_point, d.second_point, d.definition_point));
|
||||
props.push(edit("Rotation", "rotation", d.rotation));
|
||||
props.push(edit("Ext Rotation", "ext_line_rotation", d.ext_line_rotation));
|
||||
}
|
||||
Dimension::Radius(d) => {
|
||||
props.extend(radius_like_props(d.angle_vertex, d.definition_point));
|
||||
props.push(edit("Leader Length", "leader_length", d.leader_length));
|
||||
}
|
||||
Dimension::Diameter(d) => {
|
||||
props.extend(radius_like_props(d.angle_vertex, d.definition_point));
|
||||
props.push(edit("Leader Length", "leader_length", d.leader_length));
|
||||
}
|
||||
Dimension::Angular2Ln(d) => {
|
||||
props.extend(angular_props(
|
||||
d.angle_vertex,
|
||||
d.first_point,
|
||||
d.second_point,
|
||||
d.definition_point,
|
||||
));
|
||||
props.push(edit("Arc X", "dimension_arc_x", d.dimension_arc.x));
|
||||
props.push(edit("Arc Y", "dimension_arc_y", d.dimension_arc.y));
|
||||
props.push(edit("Arc Z", "dimension_arc_z", d.dimension_arc.z));
|
||||
}
|
||||
Dimension::Angular3Pt(d) => {
|
||||
props.extend(angular_props(
|
||||
d.angle_vertex,
|
||||
d.first_point,
|
||||
d.second_point,
|
||||
d.definition_point,
|
||||
));
|
||||
}
|
||||
Dimension::Ordinate(d) => {
|
||||
props.push(edit("Origin X", "definition_x", d.definition_point.x));
|
||||
props.push(edit("Origin Y", "definition_y", d.definition_point.y));
|
||||
props.push(edit("Origin Z", "definition_z", d.definition_point.z));
|
||||
props.push(edit("Feature X", "feature_x", d.feature_location.x));
|
||||
props.push(edit("Feature Y", "feature_y", d.feature_location.y));
|
||||
props.push(edit("Feature Z", "feature_z", d.feature_location.z));
|
||||
props.push(edit("Leader X", "leader_x", d.leader_endpoint.x));
|
||||
props.push(edit("Leader Y", "leader_y", d.leader_endpoint.y));
|
||||
props.push(edit("Leader Z", "leader_z", d.leader_endpoint.z));
|
||||
props.push(ro(
|
||||
"Ordinate Type",
|
||||
"ordinate_type",
|
||||
if d.is_ordinate_type_x { "X" } else { "Y" },
|
||||
));
|
||||
}
|
||||
}
|
||||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props,
|
||||
}
|
||||
}
|
||||
|
||||
fn linear_like_props(
|
||||
first: acadrust::types::Vector3,
|
||||
second: acadrust::types::Vector3,
|
||||
definition: acadrust::types::Vector3,
|
||||
) -> Vec<crate::scene::object::Property> {
|
||||
vec![
|
||||
edit("First X", "first_x", first.x),
|
||||
edit("First Y", "first_y", first.y),
|
||||
edit("First Z", "first_z", first.z),
|
||||
edit("Second X", "second_x", second.x),
|
||||
edit("Second Y", "second_y", second.y),
|
||||
edit("Second Z", "second_z", second.z),
|
||||
edit("Definition X", "definition_x", definition.x),
|
||||
edit("Definition Y", "definition_y", definition.y),
|
||||
edit("Definition Z", "definition_z", definition.z),
|
||||
]
|
||||
}
|
||||
|
||||
fn radius_like_props(
|
||||
center: acadrust::types::Vector3,
|
||||
point: acadrust::types::Vector3,
|
||||
) -> Vec<crate::scene::object::Property> {
|
||||
vec![
|
||||
edit("Center X", "center_x", center.x),
|
||||
edit("Center Y", "center_y", center.y),
|
||||
edit("Center Z", "center_z", center.z),
|
||||
edit("Point X", "point_x", point.x),
|
||||
edit("Point Y", "point_y", point.y),
|
||||
edit("Point Z", "point_z", point.z),
|
||||
]
|
||||
}
|
||||
|
||||
fn angular_props(
|
||||
vertex: acadrust::types::Vector3,
|
||||
first: acadrust::types::Vector3,
|
||||
second: acadrust::types::Vector3,
|
||||
definition: acadrust::types::Vector3,
|
||||
) -> Vec<crate::scene::object::Property> {
|
||||
vec![
|
||||
edit("Vertex X", "vertex_x", vertex.x),
|
||||
edit("Vertex Y", "vertex_y", vertex.y),
|
||||
edit("Vertex Z", "vertex_z", vertex.z),
|
||||
edit("First X", "first_x", first.x),
|
||||
edit("First Y", "first_y", first.y),
|
||||
edit("First Z", "first_z", first.z),
|
||||
edit("Second X", "second_x", second.x),
|
||||
edit("Second Y", "second_y", second.y),
|
||||
edit("Second Z", "second_z", second.z),
|
||||
edit("Definition X", "definition_x", definition.x),
|
||||
edit("Definition Y", "definition_y", definition.y),
|
||||
edit("Definition Z", "definition_z", definition.z),
|
||||
]
|
||||
}
|
||||
|
||||
fn apply_base_prop(base: &mut DimensionBase, field: &str, value: &str) -> bool {
|
||||
match field {
|
||||
"text" => {
|
||||
base.text = value.to_string();
|
||||
true
|
||||
}
|
||||
"user_text" => {
|
||||
base.user_text = if value.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(value.to_string())
|
||||
};
|
||||
true
|
||||
}
|
||||
"style_name" => {
|
||||
base.style_name = value.to_string();
|
||||
true
|
||||
}
|
||||
"text_x" => assign_f64(value, &mut base.text_middle_point.x),
|
||||
"text_y" => assign_f64(value, &mut base.text_middle_point.y),
|
||||
"text_z" => assign_f64(value, &mut base.text_middle_point.z),
|
||||
"text_rotation" => assign_f64(value, &mut base.text_rotation),
|
||||
"horizontal_direction" => assign_f64(value, &mut base.horizontal_direction),
|
||||
"line_spacing_factor" => assign_f64(value, &mut base.line_spacing_factor),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn assign_f64(value: &str, target: &mut f64) -> bool {
|
||||
let Some(v) = parse_f64(value) else {
|
||||
return false;
|
||||
};
|
||||
*target = v;
|
||||
true
|
||||
}
|
||||
|
||||
fn apply_geom_prop(dim: &mut Dimension, field: &str, value: &str) {
|
||||
if apply_base_prop(dim.base_mut(), field, value) {
|
||||
return;
|
||||
}
|
||||
match dim {
|
||||
Dimension::Aligned(d) => apply_linear_fields_aligned(d, field, value),
|
||||
Dimension::Linear(d) => apply_linear_fields_linear(d, field, value),
|
||||
Dimension::Radius(d) => apply_radius_fields(d, field, value),
|
||||
Dimension::Diameter(d) => apply_diameter_fields(d, field, value),
|
||||
Dimension::Angular2Ln(d) => apply_angular2_fields(d, field, value),
|
||||
Dimension::Angular3Pt(d) => apply_angular3_fields(d, field, value),
|
||||
Dimension::Ordinate(d) => apply_ordinate_fields(d, field, value),
|
||||
}
|
||||
dim.base_mut().actual_measurement = dim.measurement();
|
||||
}
|
||||
|
||||
fn apply_linear_fields_aligned(d: &mut DimensionAligned, field: &str, value: &str) {
|
||||
apply_linear_common(
|
||||
&mut d.first_point,
|
||||
&mut d.second_point,
|
||||
&mut d.definition_point,
|
||||
field,
|
||||
value,
|
||||
);
|
||||
let _ = assign_f64(value, &mut d.ext_line_rotation);
|
||||
}
|
||||
|
||||
fn apply_linear_fields_linear(d: &mut DimensionLinear, field: &str, value: &str) {
|
||||
apply_linear_common(
|
||||
&mut d.first_point,
|
||||
&mut d.second_point,
|
||||
&mut d.definition_point,
|
||||
field,
|
||||
value,
|
||||
);
|
||||
match field {
|
||||
"rotation" => {
|
||||
let _ = assign_f64(value, &mut d.rotation);
|
||||
}
|
||||
"ext_line_rotation" => {
|
||||
let _ = assign_f64(value, &mut d.ext_line_rotation);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_linear_common(
|
||||
first: &mut acadrust::types::Vector3,
|
||||
second: &mut acadrust::types::Vector3,
|
||||
definition: &mut acadrust::types::Vector3,
|
||||
field: &str,
|
||||
value: &str,
|
||||
) {
|
||||
match field {
|
||||
"first_x" => {
|
||||
let _ = assign_f64(value, &mut first.x);
|
||||
}
|
||||
"first_y" => {
|
||||
let _ = assign_f64(value, &mut first.y);
|
||||
}
|
||||
"first_z" => {
|
||||
let _ = assign_f64(value, &mut first.z);
|
||||
}
|
||||
"second_x" => {
|
||||
let _ = assign_f64(value, &mut second.x);
|
||||
}
|
||||
"second_y" => {
|
||||
let _ = assign_f64(value, &mut second.y);
|
||||
}
|
||||
"second_z" => {
|
||||
let _ = assign_f64(value, &mut second.z);
|
||||
}
|
||||
"definition_x" => {
|
||||
let _ = assign_f64(value, &mut definition.x);
|
||||
}
|
||||
"definition_y" => {
|
||||
let _ = assign_f64(value, &mut definition.y);
|
||||
}
|
||||
"definition_z" => {
|
||||
let _ = assign_f64(value, &mut definition.z);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_radius_fields(d: &mut DimensionRadius, field: &str, value: &str) {
|
||||
apply_radius_common(&mut d.angle_vertex, &mut d.definition_point, field, value);
|
||||
if field == "leader_length" {
|
||||
let _ = assign_f64(value, &mut d.leader_length);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_diameter_fields(d: &mut DimensionDiameter, field: &str, value: &str) {
|
||||
apply_radius_common(&mut d.angle_vertex, &mut d.definition_point, field, value);
|
||||
if field == "leader_length" {
|
||||
let _ = assign_f64(value, &mut d.leader_length);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_radius_common(
|
||||
center: &mut acadrust::types::Vector3,
|
||||
point: &mut acadrust::types::Vector3,
|
||||
field: &str,
|
||||
value: &str,
|
||||
) {
|
||||
match field {
|
||||
"center_x" => {
|
||||
let _ = assign_f64(value, &mut center.x);
|
||||
}
|
||||
"center_y" => {
|
||||
let _ = assign_f64(value, &mut center.y);
|
||||
}
|
||||
"center_z" => {
|
||||
let _ = assign_f64(value, &mut center.z);
|
||||
}
|
||||
"point_x" => {
|
||||
let _ = assign_f64(value, &mut point.x);
|
||||
}
|
||||
"point_y" => {
|
||||
let _ = assign_f64(value, &mut point.y);
|
||||
}
|
||||
"point_z" => {
|
||||
let _ = assign_f64(value, &mut point.z);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_angular2_fields(d: &mut DimensionAngular2Ln, field: &str, value: &str) {
|
||||
apply_angular_common(
|
||||
&mut d.angle_vertex,
|
||||
&mut d.first_point,
|
||||
&mut d.second_point,
|
||||
&mut d.definition_point,
|
||||
field,
|
||||
value,
|
||||
);
|
||||
match field {
|
||||
"dimension_arc_x" => {
|
||||
let _ = assign_f64(value, &mut d.dimension_arc.x);
|
||||
}
|
||||
"dimension_arc_y" => {
|
||||
let _ = assign_f64(value, &mut d.dimension_arc.y);
|
||||
}
|
||||
"dimension_arc_z" => {
|
||||
let _ = assign_f64(value, &mut d.dimension_arc.z);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_angular3_fields(d: &mut DimensionAngular3Pt, field: &str, value: &str) {
|
||||
apply_angular_common(
|
||||
&mut d.angle_vertex,
|
||||
&mut d.first_point,
|
||||
&mut d.second_point,
|
||||
&mut d.definition_point,
|
||||
field,
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
fn apply_angular_common(
|
||||
vertex: &mut acadrust::types::Vector3,
|
||||
first: &mut acadrust::types::Vector3,
|
||||
second: &mut acadrust::types::Vector3,
|
||||
definition: &mut acadrust::types::Vector3,
|
||||
field: &str,
|
||||
value: &str,
|
||||
) {
|
||||
match field {
|
||||
"vertex_x" => {
|
||||
let _ = assign_f64(value, &mut vertex.x);
|
||||
}
|
||||
"vertex_y" => {
|
||||
let _ = assign_f64(value, &mut vertex.y);
|
||||
}
|
||||
"vertex_z" => {
|
||||
let _ = assign_f64(value, &mut vertex.z);
|
||||
}
|
||||
"first_x" => {
|
||||
let _ = assign_f64(value, &mut first.x);
|
||||
}
|
||||
"first_y" => {
|
||||
let _ = assign_f64(value, &mut first.y);
|
||||
}
|
||||
"first_z" => {
|
||||
let _ = assign_f64(value, &mut first.z);
|
||||
}
|
||||
"second_x" => {
|
||||
let _ = assign_f64(value, &mut second.x);
|
||||
}
|
||||
"second_y" => {
|
||||
let _ = assign_f64(value, &mut second.y);
|
||||
}
|
||||
"second_z" => {
|
||||
let _ = assign_f64(value, &mut second.z);
|
||||
}
|
||||
"definition_x" => {
|
||||
let _ = assign_f64(value, &mut definition.x);
|
||||
}
|
||||
"definition_y" => {
|
||||
let _ = assign_f64(value, &mut definition.y);
|
||||
}
|
||||
"definition_z" => {
|
||||
let _ = assign_f64(value, &mut definition.z);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_ordinate_fields(d: &mut DimensionOrdinate, field: &str, value: &str) {
|
||||
match field {
|
||||
"definition_x" => {
|
||||
let _ = assign_f64(value, &mut d.definition_point.x);
|
||||
}
|
||||
"definition_y" => {
|
||||
let _ = assign_f64(value, &mut d.definition_point.y);
|
||||
}
|
||||
"definition_z" => {
|
||||
let _ = assign_f64(value, &mut d.definition_point.z);
|
||||
}
|
||||
"feature_x" => {
|
||||
let _ = assign_f64(value, &mut d.feature_location.x);
|
||||
}
|
||||
"feature_y" => {
|
||||
let _ = assign_f64(value, &mut d.feature_location.y);
|
||||
}
|
||||
"feature_z" => {
|
||||
let _ = assign_f64(value, &mut d.feature_location.z);
|
||||
}
|
||||
"leader_x" => {
|
||||
let _ = assign_f64(value, &mut d.leader_endpoint.x);
|
||||
}
|
||||
"leader_y" => {
|
||||
let _ = assign_f64(value, &mut d.leader_endpoint.y);
|
||||
}
|
||||
"leader_z" => {
|
||||
let _ = assign_f64(value, &mut d.leader_endpoint.z);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_transform(dim: &mut Dimension, t: &EntityTransform) {
|
||||
match t {
|
||||
EntityTransform::Translate(d) => dim.translate(acadrust::types::Vector3::new(
|
||||
d.x as f64,
|
||||
d.y as f64,
|
||||
d.z as f64,
|
||||
)),
|
||||
EntityTransform::Rotate { center, angle_rad } => {
|
||||
transform_dimension_points(dim, |pt| rotate_point(pt, *center, *angle_rad))
|
||||
}
|
||||
EntityTransform::Scale { center, factor } => {
|
||||
transform_dimension_points(dim, |pt| scale_point(pt, *center, *factor))
|
||||
}
|
||||
EntityTransform::Mirror { p1, p2 } => {
|
||||
transform_dimension_points(dim, |pt| mirror_point(pt, *p1, *p2))
|
||||
}
|
||||
}
|
||||
dim.base_mut().actual_measurement = dim.measurement();
|
||||
}
|
||||
|
||||
fn transform_dimension_points<F>(dim: &mut Dimension, mut f: F)
|
||||
where
|
||||
F: FnMut(&mut acadrust::types::Vector3),
|
||||
{
|
||||
f(&mut dim.base_mut().text_middle_point);
|
||||
f(&mut dim.base_mut().insertion_point);
|
||||
match dim {
|
||||
Dimension::Aligned(d) => {
|
||||
f(&mut d.first_point);
|
||||
f(&mut d.second_point);
|
||||
f(&mut d.definition_point);
|
||||
}
|
||||
Dimension::Linear(d) => {
|
||||
f(&mut d.first_point);
|
||||
f(&mut d.second_point);
|
||||
f(&mut d.definition_point);
|
||||
}
|
||||
Dimension::Radius(d) => {
|
||||
f(&mut d.angle_vertex);
|
||||
f(&mut d.definition_point);
|
||||
}
|
||||
Dimension::Diameter(d) => {
|
||||
f(&mut d.angle_vertex);
|
||||
f(&mut d.definition_point);
|
||||
}
|
||||
Dimension::Angular2Ln(d) => {
|
||||
f(&mut d.dimension_arc);
|
||||
f(&mut d.first_point);
|
||||
f(&mut d.second_point);
|
||||
f(&mut d.angle_vertex);
|
||||
f(&mut d.definition_point);
|
||||
}
|
||||
Dimension::Angular3Pt(d) => {
|
||||
f(&mut d.first_point);
|
||||
f(&mut d.second_point);
|
||||
f(&mut d.angle_vertex);
|
||||
f(&mut d.definition_point);
|
||||
}
|
||||
Dimension::Ordinate(d) => {
|
||||
f(&mut d.definition_point);
|
||||
f(&mut d.feature_location);
|
||||
f(&mut d.leader_endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rotate_point(p: &mut acadrust::types::Vector3, center: Vec3, angle_rad: f32) {
|
||||
let dx = p.x as f32 - center.x;
|
||||
let dy = p.y as f32 - center.y;
|
||||
let (s, c) = angle_rad.sin_cos();
|
||||
p.x = (center.x + dx * c - dy * s) as f64;
|
||||
p.y = (center.y + dx * s + dy * c) as f64;
|
||||
}
|
||||
|
||||
fn scale_point(p: &mut acadrust::types::Vector3, center: Vec3, factor: f32) {
|
||||
p.x = (center.x + (p.x as f32 - center.x) * factor) as f64;
|
||||
p.y = (center.y + (p.y as f32 - center.y) * factor) as f64;
|
||||
p.z = (center.z + (p.z as f32 - center.z) * factor) as f64;
|
||||
}
|
||||
|
||||
fn mirror_point(p: &mut acadrust::types::Vector3, p1: Vec3, p2: Vec3) {
|
||||
crate::scene::transform::reflect_xy_point(&mut p.x, &mut p.y, p1, p2);
|
||||
}
|
||||
|
||||
impl PropertyEditable for Dimension {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection {
|
||||
properties(self)
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
apply_geom_prop(self, field, value);
|
||||
}
|
||||
}
|
||||
|
||||
impl Transformable for Dimension {
|
||||
fn apply_transform(&mut self, t: &EntityTransform) {
|
||||
apply_transform(self, t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
use acadrust::entities::Ellipse;
|
||||
use glam::Vec3;
|
||||
use truck_modeling::{builder, BSplineCurve, Curve, Edge, KnotVec, Point3, Wire};
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{diamond_grip, edit_prop as edit, ro_prop as ro, square_grip};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
|
||||
use crate::scene::acad_to_truck::{TruckEntity, TruckObject};
|
||||
use crate::scene::object::{GripApply, GripDef, PropSection};
|
||||
use crate::scene::wire_model::SnapHint;
|
||||
|
||||
const TAU: f64 = std::f64::consts::TAU;
|
||||
|
||||
fn to_truck(ell: &Ellipse) -> TruckEntity {
|
||||
let cx = ell.center.x;
|
||||
let cy = ell.center.y;
|
||||
let cz = ell.center.z;
|
||||
let maj = glam::Vec3::new(
|
||||
ell.major_axis.x as f32,
|
||||
ell.major_axis.y as f32,
|
||||
ell.major_axis.z as f32,
|
||||
);
|
||||
let r_major = maj.length() as f64;
|
||||
let r_minor = r_major * ell.minor_axis_ratio;
|
||||
let t0 = ell.start_parameter;
|
||||
let mut t1 = ell.end_parameter;
|
||||
if t1 <= t0 {
|
||||
t1 += TAU;
|
||||
}
|
||||
let u = if r_major > 1e-9 {
|
||||
glam::Vec3::from(maj) / maj.length()
|
||||
} else {
|
||||
glam::Vec3::X
|
||||
};
|
||||
let v_axis = glam::Vec3::Z.cross(u);
|
||||
let center_v3 = Vec3::new(cx as f32, cy as f32, cz as f32);
|
||||
let is_closed = (t1 - t0 - TAU).abs() < 1e-6;
|
||||
|
||||
if is_closed {
|
||||
let n = 16usize;
|
||||
let pts_upper: Vec<Point3> = (0..=n)
|
||||
.map(|i| {
|
||||
let t = (i as f64 / n as f64) * std::f64::consts::PI;
|
||||
let lx = (r_major * t.cos()) as f32;
|
||||
let lz = (r_minor * t.sin()) as f32;
|
||||
Point3::new(
|
||||
cx + (lx * u.x + lz * v_axis.x) as f64,
|
||||
cy + (lx * u.y + lz * v_axis.y) as f64,
|
||||
cz + (lx * u.z + lz * v_axis.z) as f64,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let pts_lower: Vec<Point3> = (0..=n)
|
||||
.map(|i| {
|
||||
let t = std::f64::consts::PI + (i as f64 / n as f64) * std::f64::consts::PI;
|
||||
let lx = (r_major * t.cos()) as f32;
|
||||
let lz = (r_minor * t.sin()) as f32;
|
||||
Point3::new(
|
||||
cx + (lx * u.x + lz * v_axis.x) as f64,
|
||||
cy + (lx * u.y + lz * v_axis.y) as f64,
|
||||
cz + (lx * u.z + lz * v_axis.z) as f64,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let v_pos = builder::vertex(*pts_upper.first().unwrap());
|
||||
let v_neg = builder::vertex(*pts_upper.last().unwrap());
|
||||
let kv_u = KnotVec::uniform_knot(1, n);
|
||||
let kv_l = KnotVec::uniform_knot(1, n);
|
||||
let spl_u = BSplineCurve::new(kv_u, pts_upper);
|
||||
let spl_l = BSplineCurve::new(kv_l, pts_lower);
|
||||
let edge_upper = Edge::new(&v_pos, &v_neg, Curve::BSplineCurve(spl_u));
|
||||
let edge_lower = Edge::new(&v_neg, &v_pos, Curve::BSplineCurve(spl_l));
|
||||
let wire: Wire = [edge_upper, edge_lower].into_iter().collect();
|
||||
TruckEntity {
|
||||
object: TruckObject::Contour(wire),
|
||||
snap_pts: vec![(center_v3, SnapHint::Center)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
}
|
||||
} else {
|
||||
let n = 32usize;
|
||||
let ctrl_pts: Vec<Point3> = (0..=n)
|
||||
.map(|i| {
|
||||
let t = t0 + (t1 - t0) * (i as f64 / n as f64);
|
||||
let lx = (r_major * t.cos()) as f32;
|
||||
let lz = (r_minor * t.sin()) as f32;
|
||||
Point3::new(
|
||||
cx + (lx * u.x + lz * v_axis.x) as f64,
|
||||
cy + (lx * u.y + lz * v_axis.y) as f64,
|
||||
cz + (lx * u.z + lz * v_axis.z) as f64,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let kv = KnotVec::uniform_knot(1, n);
|
||||
let bspline = BSplineCurve::new(kv, ctrl_pts.clone());
|
||||
let v_start = builder::vertex(*ctrl_pts.first().unwrap());
|
||||
let v_end = builder::vertex(*ctrl_pts.last().unwrap());
|
||||
let edge = Edge::new(&v_start, &v_end, Curve::BSplineCurve(bspline));
|
||||
TruckEntity {
|
||||
object: TruckObject::Curve(edge),
|
||||
snap_pts: vec![(center_v3, SnapHint::Center)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn grips(ell: &Ellipse) -> Vec<GripDef> {
|
||||
let ctr = Vec3::new(
|
||||
ell.center.x as f32,
|
||||
ell.center.y as f32,
|
||||
ell.center.z as f32,
|
||||
);
|
||||
let maj = Vec3::new(
|
||||
(ell.center.x + ell.major_axis.x) as f32,
|
||||
(ell.center.y + ell.major_axis.y) as f32,
|
||||
(ell.center.z + ell.major_axis.z) as f32,
|
||||
);
|
||||
let major_xy =
|
||||
((ell.major_axis.x * ell.major_axis.x + ell.major_axis.y * ell.major_axis.y) as f64).sqrt();
|
||||
let (px, py) = if major_xy > 1e-10 {
|
||||
let s = ell.major_axis_length() * ell.minor_axis_ratio / major_xy;
|
||||
(-ell.major_axis.y * s, ell.major_axis.x * s)
|
||||
} else {
|
||||
(0.0, ell.major_axis_length() * ell.minor_axis_ratio)
|
||||
};
|
||||
let min = Vec3::new(
|
||||
(ell.center.x + px) as f32,
|
||||
(ell.center.y + py) as f32,
|
||||
ell.center.z as f32,
|
||||
);
|
||||
vec![
|
||||
diamond_grip(0, ctr),
|
||||
square_grip(1, maj),
|
||||
square_grip(2, min),
|
||||
]
|
||||
}
|
||||
|
||||
fn properties(ell: &Ellipse) -> PropSection {
|
||||
let r_major = (ell.major_axis.x * ell.major_axis.x
|
||||
+ ell.major_axis.y * ell.major_axis.y
|
||||
+ ell.major_axis.z * ell.major_axis.z)
|
||||
.sqrt();
|
||||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props: vec![
|
||||
edit("Center X", "center_x", ell.center.x),
|
||||
edit("Center Y", "center_y", ell.center.y),
|
||||
edit("Center Z", "center_z", ell.center.z),
|
||||
ro("Major Radius", "major_r", format!("{r_major:.4}")),
|
||||
ro(
|
||||
"Minor Radius",
|
||||
"minor_r",
|
||||
format!("{:.4}", r_major * ell.minor_axis_ratio),
|
||||
),
|
||||
ro(
|
||||
"Minor/Major",
|
||||
"ratio",
|
||||
format!("{:.4}", ell.minor_axis_ratio),
|
||||
),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_geom_prop(_ell: &mut Ellipse, _field: &str, _value: &str) {}
|
||||
|
||||
fn apply_grip(ell: &mut Ellipse, grip_id: usize, apply: GripApply) {
|
||||
match (grip_id, apply) {
|
||||
(0, GripApply::Translate(d)) => {
|
||||
ell.center.x += d.x as f64;
|
||||
ell.center.y += d.y as f64;
|
||||
ell.center.z += d.z as f64;
|
||||
}
|
||||
(0, GripApply::Absolute(p)) => {
|
||||
ell.center.x = p.x as f64;
|
||||
ell.center.y = p.y as f64;
|
||||
ell.center.z = p.z as f64;
|
||||
}
|
||||
(1, GripApply::Absolute(p)) => {
|
||||
ell.major_axis.x = p.x as f64 - ell.center.x;
|
||||
ell.major_axis.y = p.y as f64 - ell.center.y;
|
||||
ell.major_axis.z = p.z as f64 - ell.center.z;
|
||||
}
|
||||
(2, GripApply::Absolute(p)) => {
|
||||
let major_len = ell.major_axis_length();
|
||||
if major_len > 1e-10 {
|
||||
let dx = p.x as f64 - ell.center.x;
|
||||
let dy = p.y as f64 - ell.center.y;
|
||||
let dist = (dx * dx + dy * dy).sqrt();
|
||||
ell.minor_axis_ratio = (dist / major_len).clamp(0.001, 1.0);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_transform(ell: &mut Ellipse, t: &EntityTransform) {
|
||||
crate::scene::transform::apply_standard_entity_transform(ell, t, |entity, p1, p2| {
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut entity.center.x,
|
||||
&mut entity.center.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut entity.major_axis.x,
|
||||
&mut entity.major_axis.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
impl TruckConvertible for Ellipse {
|
||||
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
||||
Some(to_truck(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl Grippable for Ellipse {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
grips(self)
|
||||
}
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
apply_grip(self, grip_id, apply);
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyEditable for Ellipse {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection {
|
||||
properties(self)
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
apply_geom_prop(self, field, value);
|
||||
}
|
||||
}
|
||||
|
||||
impl Transformable for Ellipse {
|
||||
fn apply_transform(&mut self, t: &EntityTransform) {
|
||||
apply_transform(self, t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
use acadrust::entities::{BoundaryEdge, Hatch};
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{edit_prop as edit, parse_f64, ro_prop as ro};
|
||||
use crate::entities::traits::{PropertyEditable, Transformable};
|
||||
use crate::scene::object::{PropSection, PropValue, Property};
|
||||
|
||||
fn properties(h: &Hatch) -> PropSection {
|
||||
let pattern_type = match h.pattern_type {
|
||||
acadrust::entities::HatchPatternType::Predefined => "Predefined",
|
||||
acadrust::entities::HatchPatternType::UserDefined => "User Defined",
|
||||
acadrust::entities::HatchPatternType::Custom => "Custom",
|
||||
};
|
||||
let style = match h.style {
|
||||
acadrust::entities::HatchStyleType::Normal => "Normal",
|
||||
acadrust::entities::HatchStyleType::Outer => "Outer",
|
||||
acadrust::entities::HatchStyleType::Ignore => "Ignore",
|
||||
};
|
||||
let fill_type = if h.gradient_color.enabled {
|
||||
format!("Gradient ({})", h.gradient_color.name)
|
||||
} else if h.is_solid {
|
||||
"Solid".into()
|
||||
} else {
|
||||
format!("Pattern ({})", h.pattern.name)
|
||||
};
|
||||
let boundary_count: usize = h
|
||||
.paths
|
||||
.iter()
|
||||
.map(|p| {
|
||||
p.edges
|
||||
.iter()
|
||||
.map(|e| match e {
|
||||
BoundaryEdge::Polyline(poly) => poly.vertices.len(),
|
||||
_ => 1,
|
||||
})
|
||||
.sum::<usize>()
|
||||
})
|
||||
.sum();
|
||||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props: vec![
|
||||
ro("Fill Type", "fill_type", fill_type),
|
||||
Property {
|
||||
label: "Pattern Name".into(),
|
||||
field: "pattern_name",
|
||||
value: PropValue::HatchPatternChoice(h.pattern.name.clone()),
|
||||
},
|
||||
ro("Pattern Type", "pattern_type", pattern_type),
|
||||
edit(
|
||||
"Pattern Angle",
|
||||
"pattern_angle",
|
||||
h.pattern_angle.to_degrees(),
|
||||
),
|
||||
edit("Pattern Scale", "pattern_scale", h.pattern_scale),
|
||||
ro("Style", "style", style),
|
||||
ro("Boundary Paths", "path_count", h.paths.len().to_string()),
|
||||
ro("Boundary Verts", "vert_count", boundary_count.to_string()),
|
||||
ro("Double", "double", if h.is_double { "Yes" } else { "No" }),
|
||||
ro(
|
||||
"Associative",
|
||||
"associative",
|
||||
if h.is_associative { "Yes" } else { "No" },
|
||||
),
|
||||
edit("Elevation", "elevation", h.elevation),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
|
||||
let Some(v) = parse_f64(value) else {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"pattern_angle" => h.pattern_angle = v.to_radians(),
|
||||
"pattern_scale" if v > 0.0 => h.pattern_scale = v,
|
||||
"elevation" => h.elevation = v,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_transform(h: &mut Hatch, t: &EntityTransform) {
|
||||
crate::scene::transform::apply_standard_entity_transform(h, t, |entity, p1, p2| {
|
||||
let dx = (p2.x - p1.x) as f64;
|
||||
let dy = (p2.y - p1.y) as f64;
|
||||
let len2 = dx * dx + dy * dy;
|
||||
if len2 < 1e-12 {
|
||||
return;
|
||||
}
|
||||
let line_angle_deg = dy.atan2(dx).to_degrees();
|
||||
for path in &mut entity.paths {
|
||||
for edge in &mut path.edges {
|
||||
match edge {
|
||||
BoundaryEdge::Line(l) => {
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut l.start.x,
|
||||
&mut l.start.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut l.end.x,
|
||||
&mut l.end.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
}
|
||||
BoundaryEdge::CircularArc(a) => {
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut a.center.x,
|
||||
&mut a.center.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
let tmp = a.start_angle;
|
||||
a.start_angle = 2.0 * line_angle_deg - a.end_angle;
|
||||
a.end_angle = 2.0 * line_angle_deg - tmp;
|
||||
}
|
||||
BoundaryEdge::EllipticArc(e) => {
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut e.center.x,
|
||||
&mut e.center.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
let ax = dx;
|
||||
let ay = dy;
|
||||
let rx = e.major_axis_endpoint.x;
|
||||
let ry = e.major_axis_endpoint.y;
|
||||
let dot = rx * ax + ry * ay;
|
||||
e.major_axis_endpoint.x = 2.0 * dot * ax / len2 - rx;
|
||||
e.major_axis_endpoint.y = 2.0 * dot * ay / len2 - ry;
|
||||
let tmp = e.start_angle;
|
||||
e.start_angle = 2.0 * line_angle_deg - e.end_angle;
|
||||
e.end_angle = 2.0 * line_angle_deg - tmp;
|
||||
}
|
||||
BoundaryEdge::Spline(s) => {
|
||||
for cp in &mut s.control_points {
|
||||
crate::scene::transform::reflect_xy_point(&mut cp.x, &mut cp.y, p1, p2);
|
||||
}
|
||||
for fp in &mut s.fit_points {
|
||||
crate::scene::transform::reflect_xy_point(&mut fp.x, &mut fp.y, p1, p2);
|
||||
}
|
||||
}
|
||||
BoundaryEdge::Polyline(p) => {
|
||||
for v in &mut p.vertices {
|
||||
crate::scene::transform::reflect_xy_point(&mut v.x, &mut v.y, p1, p2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
impl PropertyEditable for Hatch {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection {
|
||||
properties(self)
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
apply_geom_prop(self, field, value);
|
||||
}
|
||||
}
|
||||
|
||||
impl Transformable for Hatch {
|
||||
fn apply_transform(&mut self, t: &EntityTransform) {
|
||||
apply_transform(self, t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
use acadrust::entities::Insert;
|
||||
use acadrust::types::{Transform, Vector3};
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{edit_prop as edit, parse_f64, ro_prop as ro, square_grip};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable};
|
||||
use crate::scene::object::{GripApply, GripDef, PropSection};
|
||||
|
||||
fn grips(ins: &Insert) -> Vec<GripDef> {
|
||||
let p = Vec3::new(
|
||||
ins.insert_point.x as f32,
|
||||
ins.insert_point.y as f32,
|
||||
ins.insert_point.z as f32,
|
||||
);
|
||||
vec![square_grip(0, p)]
|
||||
}
|
||||
|
||||
fn properties(ins: &Insert) -> PropSection {
|
||||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props: vec![
|
||||
edit("Insert X", "ins_x", ins.insert_point.x),
|
||||
edit("Insert Y", "ins_y", ins.insert_point.y),
|
||||
edit("Insert Z", "ins_z", ins.insert_point.z),
|
||||
edit("Scale X", "x_scale", ins.x_scale()),
|
||||
edit("Scale Y", "y_scale", ins.y_scale()),
|
||||
edit("Scale Z", "z_scale", ins.z_scale()),
|
||||
edit("Rotation", "rotation", ins.rotation.to_degrees()),
|
||||
ro("Block", "block", ins.block_name.clone()),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_geom_prop(ins: &mut Insert, field: &str, value: &str) {
|
||||
let Some(v) = parse_f64(value) else {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"ins_x" => ins.insert_point.x = v,
|
||||
"ins_y" => ins.insert_point.y = v,
|
||||
"ins_z" => ins.insert_point.z = v,
|
||||
"x_scale" => ins.set_x_scale(v),
|
||||
"y_scale" => ins.set_y_scale(v),
|
||||
"z_scale" => ins.set_z_scale(v),
|
||||
"rotation" => ins.rotation = v.to_radians(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_grip(ins: &mut Insert, _grip_id: usize, apply: GripApply) {
|
||||
match apply {
|
||||
GripApply::Absolute(p) => {
|
||||
ins.insert_point.x = p.x as f64;
|
||||
ins.insert_point.y = p.y as f64;
|
||||
ins.insert_point.z = p.z as f64;
|
||||
}
|
||||
GripApply::Translate(d) => {
|
||||
ins.insert_point.x += d.x as f64;
|
||||
ins.insert_point.y += d.y as f64;
|
||||
ins.insert_point.z += d.z as f64;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_transform(ins: &mut Insert, t: &EntityTransform) {
|
||||
crate::scene::transform::apply_standard_entity_transform(ins, t, |entity, p1, p2| {
|
||||
let dx = (p2.x - p1.x) as f64;
|
||||
let dy = (p2.y - p1.y) as f64;
|
||||
let len = (dx * dx + dy * dy).sqrt();
|
||||
if len < 1e-12 {
|
||||
return;
|
||||
}
|
||||
|
||||
let ux = dx / len;
|
||||
let uy = dy / len;
|
||||
let mirror = acadrust::types::Matrix4 {
|
||||
m: [
|
||||
[2.0 * ux * ux - 1.0, 2.0 * ux * uy, 0.0, 0.0],
|
||||
[2.0 * ux * uy, 2.0 * uy * uy - 1.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 1.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
],
|
||||
};
|
||||
let t = Transform::from_translation(Vector3::new(-(p1.x as f64), -(p1.y as f64), 0.0))
|
||||
.then(&Transform::from_matrix(mirror))
|
||||
.then(&Transform::from_translation(Vector3::new(p1.x as f64, p1.y as f64, 0.0)));
|
||||
acadrust::Entity::apply_transform(entity, &t);
|
||||
});
|
||||
}
|
||||
|
||||
impl Grippable for Insert {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
grips(self)
|
||||
}
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
apply_grip(self, grip_id, apply);
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyEditable for Insert {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection {
|
||||
properties(self)
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
apply_geom_prop(self, field, value);
|
||||
}
|
||||
}
|
||||
|
||||
impl Transformable for Insert {
|
||||
fn apply_transform(&mut self, t: &EntityTransform) {
|
||||
apply_transform(self, t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
use acadrust::entities::{HooklineDirection, Leader, LeaderCreationType, LeaderPathType};
|
||||
use acadrust::Entity;
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{diamond_grip, edit_prop as edit, ro_prop as ro, square_grip};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
|
||||
use crate::scene::acad_to_truck::{TruckEntity, TruckObject};
|
||||
use crate::scene::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||||
use crate::scene::wire_model::TangentGeom;
|
||||
|
||||
// ── TruckConvertible (used for snap/grip key-vertices) ─────────────────────
|
||||
|
||||
fn to_truck(leader: &Leader) -> TruckEntity {
|
||||
let verts = &leader.vertices;
|
||||
let nan = [f32::NAN; 3];
|
||||
let p3 = |v: &acadrust::types::Vector3| -> [f32; 3] { [v.x as f32, v.y as f32, v.z as f32] };
|
||||
|
||||
let mut points: Vec<[f32; 3]> = Vec::new();
|
||||
let mut tangents: Vec<TangentGeom> = Vec::new();
|
||||
let mut key_verts: Vec<[f32; 3]> = Vec::new();
|
||||
|
||||
// Main leader path
|
||||
for v in verts {
|
||||
points.push(p3(v));
|
||||
key_verts.push(p3(v));
|
||||
}
|
||||
for i in 0..verts.len().saturating_sub(1) {
|
||||
tangents.push(TangentGeom::Line { p1: p3(&verts[i]), p2: p3(&verts[i + 1]) });
|
||||
}
|
||||
|
||||
// Arrowhead at vertex[0]
|
||||
if leader.arrow_enabled && verts.len() >= 2 {
|
||||
let tip = &verts[0];
|
||||
let next = &verts[1];
|
||||
let dx = (next.x - tip.x) as f32;
|
||||
let dy = (next.y - tip.y) as f32;
|
||||
let len = (dx * dx + dy * dy).sqrt().max(1e-9);
|
||||
let (dx, dy) = (dx / len, dy / len);
|
||||
let sz = (leader.text_height as f32).max(1.0) * 0.8;
|
||||
let a = std::f32::consts::PI / 6.0;
|
||||
let (s, c) = a.sin_cos();
|
||||
let tip_f = p3(tip);
|
||||
points.push(nan);
|
||||
points.push([tip_f[0] + (dx*c - dy*s)*sz, tip_f[1] + (dx*s + dy*c)*sz, tip_f[2]]);
|
||||
points.push(tip_f);
|
||||
points.push([tip_f[0] + (dx*c + dy*s)*sz, tip_f[1] + (-dx*s + dy*c)*sz, tip_f[2]]);
|
||||
}
|
||||
|
||||
// Landing line at last vertex
|
||||
if leader.hookline_enabled && verts.len() >= 2 {
|
||||
let last = verts.last().unwrap();
|
||||
let prev = &verts[verts.len() - 2];
|
||||
let sign = if (last.x - prev.x) >= 0.0 { 1.0_f32 } else { -1.0_f32 };
|
||||
let len = leader.text_height as f32 * 1.5;
|
||||
let last_f = p3(last);
|
||||
points.push(nan);
|
||||
points.push(last_f);
|
||||
points.push([last_f[0] + sign * len, last_f[1], last_f[2]]);
|
||||
}
|
||||
|
||||
TruckEntity {
|
||||
object: TruckObject::Lines(points),
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: tangents,
|
||||
key_vertices: key_verts,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Grips ──────────────────────────────────────────────────────────────────
|
||||
|
||||
fn grips(leader: &Leader) -> Vec<GripDef> {
|
||||
let n = leader.vertices.len();
|
||||
let mut grips: Vec<GripDef> = leader
|
||||
.vertices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, v)| square_grip(i, Vec3::new(v.x as f32, v.y as f32, v.z as f32)))
|
||||
.collect();
|
||||
|
||||
if n >= 2 {
|
||||
let sum = leader.vertices.iter().fold(Vec3::ZERO, |acc, v| {
|
||||
acc + Vec3::new(v.x as f32, v.y as f32, v.z as f32)
|
||||
});
|
||||
grips.push(diamond_grip(n, sum / n as f32));
|
||||
}
|
||||
|
||||
grips
|
||||
}
|
||||
|
||||
fn apply_grip(leader: &mut Leader, grip_id: usize, apply: GripApply) {
|
||||
let n = leader.vertices.len();
|
||||
if grip_id < n {
|
||||
if let Some(v) = leader.vertices.get_mut(grip_id) {
|
||||
match apply {
|
||||
GripApply::Absolute(p) => {
|
||||
v.x = p.x as f64;
|
||||
v.y = p.y as f64;
|
||||
v.z = p.z as f64;
|
||||
}
|
||||
GripApply::Translate(d) => {
|
||||
v.x += d.x as f64;
|
||||
v.y += d.y as f64;
|
||||
v.z += d.z as f64;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let GripApply::Translate(d) = apply {
|
||||
leader.translate(acadrust::types::Vector3::new(
|
||||
d.x as f64,
|
||||
d.y as f64,
|
||||
d.z as f64,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Properties ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn bool_toggle(label: &str, field: &'static str, value: bool) -> Property {
|
||||
Property {
|
||||
label: label.into(),
|
||||
field,
|
||||
value: PropValue::BoolToggle { field, value },
|
||||
}
|
||||
}
|
||||
|
||||
fn choice_prop(label: &str, field: &'static str, selected: &str, options: &[&str]) -> Property {
|
||||
Property {
|
||||
label: label.into(),
|
||||
field,
|
||||
value: PropValue::Choice {
|
||||
selected: selected.to_string(),
|
||||
options: options.iter().map(|s| s.to_string()).collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn path_type_str(pt: &LeaderPathType) -> &'static str {
|
||||
match pt {
|
||||
LeaderPathType::StraightLine => "Straight",
|
||||
LeaderPathType::Spline => "Spline",
|
||||
}
|
||||
}
|
||||
|
||||
fn creation_type_str(ct: &LeaderCreationType) -> &'static str {
|
||||
match ct {
|
||||
LeaderCreationType::WithText => "With Text",
|
||||
LeaderCreationType::WithTolerance => "With Tolerance",
|
||||
LeaderCreationType::WithBlock => "With Block",
|
||||
LeaderCreationType::NoAnnotation => "No Annotation",
|
||||
}
|
||||
}
|
||||
|
||||
fn hookline_dir_str(hd: &HooklineDirection) -> &'static str {
|
||||
match hd {
|
||||
HooklineDirection::Opposite => "Opposite",
|
||||
HooklineDirection::Same => "Same",
|
||||
}
|
||||
}
|
||||
|
||||
fn properties(leader: &Leader) -> PropSection {
|
||||
let n = leader.vertices.len();
|
||||
let mut props = vec![
|
||||
// Style
|
||||
Property {
|
||||
label: "Dim Style".into(),
|
||||
field: "dimension_style",
|
||||
value: PropValue::EditText(leader.dimension_style.clone()),
|
||||
},
|
||||
// Path type
|
||||
choice_prop(
|
||||
"Path Type",
|
||||
"path_type",
|
||||
path_type_str(&leader.path_type),
|
||||
&["Straight", "Spline"],
|
||||
),
|
||||
// Creation type
|
||||
choice_prop(
|
||||
"Creation Type",
|
||||
"creation_type",
|
||||
creation_type_str(&leader.creation_type),
|
||||
&["With Text", "With Tolerance", "With Block", "No Annotation"],
|
||||
),
|
||||
// Arrow
|
||||
bool_toggle("Arrow", "arrow_enabled", leader.arrow_enabled),
|
||||
// Hookline
|
||||
bool_toggle("Hookline", "hookline_enabled", leader.hookline_enabled),
|
||||
choice_prop(
|
||||
"Hookline Dir",
|
||||
"hookline_direction",
|
||||
hookline_dir_str(&leader.hookline_direction),
|
||||
&["Opposite", "Same"],
|
||||
),
|
||||
// Text dims
|
||||
edit("Text Height", "text_height", leader.text_height),
|
||||
edit("Text Width", "text_width", leader.text_width),
|
||||
// Normal
|
||||
edit("Normal X", "normal_x", leader.normal.x),
|
||||
edit("Normal Y", "normal_y", leader.normal.y),
|
||||
edit("Normal Z", "normal_z", leader.normal.z),
|
||||
// Horizontal direction
|
||||
edit("H Dir X", "h_dir_x", leader.horizontal_direction.x),
|
||||
edit("H Dir Y", "h_dir_y", leader.horizontal_direction.y),
|
||||
edit("H Dir Z", "h_dir_z", leader.horizontal_direction.z),
|
||||
// Block offset
|
||||
edit("Block Offset X", "block_offset_x", leader.block_offset.x),
|
||||
edit("Block Offset Y", "block_offset_y", leader.block_offset.y),
|
||||
edit("Block Offset Z", "block_offset_z", leader.block_offset.z),
|
||||
// Annotation offset
|
||||
edit("Ann Offset X", "ann_offset_x", leader.annotation_offset.x),
|
||||
edit("Ann Offset Y", "ann_offset_y", leader.annotation_offset.y),
|
||||
edit("Ann Offset Z", "ann_offset_z", leader.annotation_offset.z),
|
||||
// Stats
|
||||
ro("Vertices", "vertex_count", n.to_string()),
|
||||
ro("Length", "length", format!("{:.4}", leader.length())),
|
||||
];
|
||||
|
||||
// Arrow point (vertex[0])
|
||||
if let Some(a) = leader.arrow_point() {
|
||||
props.push(edit("Arrow X", "arrow_x", a.x));
|
||||
props.push(edit("Arrow Y", "arrow_y", a.y));
|
||||
props.push(edit("Arrow Z", "arrow_z", a.z));
|
||||
}
|
||||
|
||||
// End point (last vertex)
|
||||
if n >= 2 {
|
||||
if let Some(e) = leader.end_point() {
|
||||
props.push(edit("End X", "end_x", e.x));
|
||||
props.push(edit("End Y", "end_y", e.y));
|
||||
props.push(edit("End Z", "end_z", e.z));
|
||||
}
|
||||
}
|
||||
|
||||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_geom_prop(leader: &mut Leader, field: &str, value: &str) {
|
||||
let f64 = |s: &str| -> Option<f64> { s.trim().parse().ok() };
|
||||
|
||||
match field {
|
||||
"dimension_style" => leader.dimension_style = value.to_string(),
|
||||
"path_type" => {
|
||||
leader.path_type = match value {
|
||||
"Spline" => LeaderPathType::Spline,
|
||||
_ => LeaderPathType::StraightLine,
|
||||
};
|
||||
}
|
||||
"creation_type" => {
|
||||
leader.creation_type = match value {
|
||||
"With Tolerance" => LeaderCreationType::WithTolerance,
|
||||
"With Block" => LeaderCreationType::WithBlock,
|
||||
"No Annotation" => LeaderCreationType::NoAnnotation,
|
||||
_ => LeaderCreationType::WithText,
|
||||
};
|
||||
}
|
||||
"arrow_enabled" => leader.arrow_enabled = if value == "toggle" { !leader.arrow_enabled } else { value == "true" },
|
||||
"hookline_enabled" => leader.hookline_enabled = if value == "toggle" { !leader.hookline_enabled } else { value == "true" },
|
||||
"hookline_direction" => {
|
||||
leader.hookline_direction = match value {
|
||||
"Same" => HooklineDirection::Same,
|
||||
_ => HooklineDirection::Opposite,
|
||||
};
|
||||
}
|
||||
"text_height" => {
|
||||
if let Some(v) = f64(value) {
|
||||
leader.text_height = v;
|
||||
}
|
||||
}
|
||||
"text_width" => {
|
||||
if let Some(v) = f64(value) {
|
||||
leader.text_width = v;
|
||||
}
|
||||
}
|
||||
"normal_x" => {
|
||||
if let Some(v) = f64(value) {
|
||||
leader.normal.x = v;
|
||||
}
|
||||
}
|
||||
"normal_y" => {
|
||||
if let Some(v) = f64(value) {
|
||||
leader.normal.y = v;
|
||||
}
|
||||
}
|
||||
"normal_z" => {
|
||||
if let Some(v) = f64(value) {
|
||||
leader.normal.z = v;
|
||||
}
|
||||
}
|
||||
"h_dir_x" => {
|
||||
if let Some(v) = f64(value) {
|
||||
leader.horizontal_direction.x = v;
|
||||
}
|
||||
}
|
||||
"h_dir_y" => {
|
||||
if let Some(v) = f64(value) {
|
||||
leader.horizontal_direction.y = v;
|
||||
}
|
||||
}
|
||||
"h_dir_z" => {
|
||||
if let Some(v) = f64(value) {
|
||||
leader.horizontal_direction.z = v;
|
||||
}
|
||||
}
|
||||
"block_offset_x" => {
|
||||
if let Some(v) = f64(value) {
|
||||
leader.block_offset.x = v;
|
||||
}
|
||||
}
|
||||
"block_offset_y" => {
|
||||
if let Some(v) = f64(value) {
|
||||
leader.block_offset.y = v;
|
||||
}
|
||||
}
|
||||
"block_offset_z" => {
|
||||
if let Some(v) = f64(value) {
|
||||
leader.block_offset.z = v;
|
||||
}
|
||||
}
|
||||
"ann_offset_x" => {
|
||||
if let Some(v) = f64(value) {
|
||||
leader.annotation_offset.x = v;
|
||||
}
|
||||
}
|
||||
"ann_offset_y" => {
|
||||
if let Some(v) = f64(value) {
|
||||
leader.annotation_offset.y = v;
|
||||
}
|
||||
}
|
||||
"ann_offset_z" => {
|
||||
if let Some(v) = f64(value) {
|
||||
leader.annotation_offset.z = v;
|
||||
}
|
||||
}
|
||||
"arrow_x" => {
|
||||
if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(0)) {
|
||||
vert.x = v;
|
||||
}
|
||||
}
|
||||
"arrow_y" => {
|
||||
if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(0)) {
|
||||
vert.y = v;
|
||||
}
|
||||
}
|
||||
"arrow_z" => {
|
||||
if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(0)) {
|
||||
vert.z = v;
|
||||
}
|
||||
}
|
||||
"end_x" => {
|
||||
let last = leader.vertices.len().saturating_sub(1);
|
||||
if last > 0 {
|
||||
if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(last)) {
|
||||
vert.x = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
"end_y" => {
|
||||
let last = leader.vertices.len().saturating_sub(1);
|
||||
if last > 0 {
|
||||
if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(last)) {
|
||||
vert.y = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
"end_z" => {
|
||||
let last = leader.vertices.len().saturating_sub(1);
|
||||
if last > 0 {
|
||||
if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(last)) {
|
||||
vert.z = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Transform ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn apply_transform(leader: &mut Leader, t: &EntityTransform) {
|
||||
crate::scene::transform::apply_standard_entity_transform(leader, t, |entity, p1, p2| {
|
||||
for v in &mut entity.vertices {
|
||||
crate::scene::transform::reflect_xy_point(&mut v.x, &mut v.y, p1, p2);
|
||||
}
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut entity.block_offset.x,
|
||||
&mut entity.block_offset.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut entity.annotation_offset.x,
|
||||
&mut entity.annotation_offset.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Trait impls ────────────────────────────────────────────────────────────
|
||||
|
||||
impl TruckConvertible for Leader {
|
||||
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
||||
if self.vertices.is_empty() { return None; }
|
||||
Some(to_truck(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl Grippable for Leader {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
grips(self)
|
||||
}
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
apply_grip(self, grip_id, apply);
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyEditable for Leader {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection {
|
||||
properties(self)
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
apply_geom_prop(self, field, value);
|
||||
}
|
||||
}
|
||||
|
||||
impl Transformable for Leader {
|
||||
fn apply_transform(&mut self, t: &EntityTransform) {
|
||||
apply_transform(self, t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
use acadrust::{entities::Line, Entity};
|
||||
use glam::Vec3;
|
||||
use truck_modeling::{builder, Point3};
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{
|
||||
diamond_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip,
|
||||
};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
|
||||
use crate::scene::acad_to_truck::{TruckEntity, TruckObject};
|
||||
use crate::scene::object::{GripApply, GripDef, PropSection};
|
||||
use crate::scene::wire_model::TangentGeom;
|
||||
|
||||
fn to_truck(line: &Line) -> TruckEntity {
|
||||
let p0 = Point3::new(line.start.x, line.start.y, line.start.z);
|
||||
let p1 = Point3::new(line.end.x, line.end.y, line.end.z);
|
||||
let v0 = builder::vertex(p0);
|
||||
let v1 = builder::vertex(p1);
|
||||
let edge = builder::line(&v0, &v1);
|
||||
let kv = vec![
|
||||
[p0.x as f32, p0.y as f32, p0.z as f32],
|
||||
[p1.x as f32, p1.y as f32, p1.z as f32],
|
||||
];
|
||||
TruckEntity {
|
||||
object: TruckObject::Curve(edge),
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![TangentGeom::Line {
|
||||
p1: kv[0],
|
||||
p2: kv[1],
|
||||
}],
|
||||
key_vertices: kv,
|
||||
}
|
||||
}
|
||||
|
||||
fn grips(line: &Line) -> Vec<GripDef> {
|
||||
let s = Vec3::new(
|
||||
line.start.x as f32,
|
||||
line.start.y as f32,
|
||||
line.start.z as f32,
|
||||
);
|
||||
let e = Vec3::new(line.end.x as f32, line.end.y as f32, line.end.z as f32);
|
||||
let m = (s + e) * 0.5;
|
||||
vec![square_grip(0, s), square_grip(1, e), diamond_grip(2, m)]
|
||||
}
|
||||
|
||||
fn properties(line: &Line) -> PropSection {
|
||||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props: vec![
|
||||
edit("Start X", "start_x", line.start.x),
|
||||
edit("Start Y", "start_y", line.start.y),
|
||||
edit("Start Z", "start_z", line.start.z),
|
||||
edit("End X", "end_x", line.end.x),
|
||||
edit("End Y", "end_y", line.end.y),
|
||||
edit("End Z", "end_z", line.end.z),
|
||||
ro("Length", "length", format!("{:.4}", line.length())),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_geom_prop(line: &mut Line, field: &str, value: &str) {
|
||||
let Some(v) = parse_f64(value) else {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"start_x" => line.start.x = v,
|
||||
"start_y" => line.start.y = v,
|
||||
"start_z" => line.start.z = v,
|
||||
"end_x" => line.end.x = v,
|
||||
"end_y" => line.end.y = v,
|
||||
"end_z" => line.end.z = v,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_grip(line: &mut Line, grip_id: usize, apply: GripApply) {
|
||||
match (grip_id, apply) {
|
||||
(0, GripApply::Absolute(p)) => {
|
||||
line.start.x = p.x as f64;
|
||||
line.start.y = p.y as f64;
|
||||
line.start.z = p.z as f64;
|
||||
}
|
||||
(1, GripApply::Absolute(p)) => {
|
||||
line.end.x = p.x as f64;
|
||||
line.end.y = p.y as f64;
|
||||
line.end.z = p.z as f64;
|
||||
}
|
||||
(2, GripApply::Translate(d)) => {
|
||||
line.start.x += d.x as f64;
|
||||
line.start.y += d.y as f64;
|
||||
line.start.z += d.z as f64;
|
||||
line.end.x += d.x as f64;
|
||||
line.end.y += d.y as f64;
|
||||
line.end.z += d.z as f64;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_transform(line: &mut Line, t: &EntityTransform) {
|
||||
match t {
|
||||
EntityTransform::Translate(d) => {
|
||||
line.translate(acadrust::types::Vector3::new(
|
||||
d.x as f64, d.y as f64, d.z as f64,
|
||||
));
|
||||
}
|
||||
EntityTransform::Rotate { center, angle_rad } => {
|
||||
crate::scene::transform::apply_standard_transform(line, *center, *angle_rad);
|
||||
}
|
||||
EntityTransform::Scale { center, factor } => {
|
||||
crate::scene::transform::apply_standard_scale(line, *center, *factor);
|
||||
}
|
||||
EntityTransform::Mirror { p1, p2 } => {
|
||||
crate::scene::transform::mirror_xy_line(line, *p1, *p2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TruckConvertible for Line {
|
||||
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
||||
Some(to_truck(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl Grippable for Line {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
grips(self)
|
||||
}
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
apply_grip(self, grip_id, apply);
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyEditable for Line {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection {
|
||||
properties(self)
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
apply_geom_prop(self, field, value);
|
||||
}
|
||||
}
|
||||
|
||||
impl Transformable for Line {
|
||||
fn apply_transform(&mut self, t: &EntityTransform) {
|
||||
apply_transform(self, t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
use acadrust::entities::{LwPolyline, LwVertex};
|
||||
use glam::Vec3;
|
||||
use truck_modeling::{builder, Edge, Point3, Wire};
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{edit_prop as edit, parse_f64, ro_prop as ro, square_grip};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
|
||||
use crate::scene::acad_to_truck::{TruckEntity, TruckObject};
|
||||
use crate::scene::object::{GripApply, GripDef, PropSection};
|
||||
use crate::scene::wire_model::TangentGeom;
|
||||
|
||||
const TAU: f64 = std::f64::consts::TAU;
|
||||
|
||||
fn to_truck(pline: &LwPolyline) -> TruckEntity {
|
||||
let verts = &pline.vertices;
|
||||
if verts.is_empty() {
|
||||
let v = builder::vertex(Point3::new(0.0, 0.0, 0.0));
|
||||
let edge = builder::line(&v, &v);
|
||||
return TruckEntity {
|
||||
object: TruckObject::Contour(std::iter::once(edge).collect()),
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
};
|
||||
}
|
||||
|
||||
let elev = pline.elevation;
|
||||
let count = verts.len();
|
||||
let seg_count = if pline.is_closed { count } else { count - 1 };
|
||||
let mut edges: Vec<Edge> = Vec::new();
|
||||
let mut tangents: Vec<TangentGeom> = Vec::new();
|
||||
let mut key_verts: Vec<[f32; 3]> = Vec::new();
|
||||
|
||||
let to_pt = |v: &LwVertex| -> Point3 { Point3::new(v.location.x, v.location.y, elev) };
|
||||
|
||||
for i in 0..seg_count {
|
||||
let v0 = &verts[i];
|
||||
let v1 = &verts[(i + 1) % count];
|
||||
let p0 = to_pt(v0);
|
||||
let p1 = to_pt(v1);
|
||||
let bulge = v0.bulge;
|
||||
|
||||
if bulge.abs() < 1e-9 {
|
||||
let tv0 = builder::vertex(p0);
|
||||
let tv1 = builder::vertex(p1);
|
||||
edges.push(builder::line(&tv0, &tv1));
|
||||
tangents.push(TangentGeom::Line {
|
||||
p1: [p0.x as f32, p0.y as f32, p0.z as f32],
|
||||
p2: [p1.x as f32, p1.y as f32, p1.z as f32],
|
||||
});
|
||||
} else {
|
||||
let angle = 4.0 * (bulge as f64).atan();
|
||||
let dx = p1.x - p0.x;
|
||||
let dy = p1.y - p0.y;
|
||||
let d = (dx * dx + dy * dy).sqrt();
|
||||
let r = (d / 2.0) / (angle / 2.0).sin().abs();
|
||||
let mx = (p0.x + p1.x) * 0.5;
|
||||
let my = (p0.y + p1.y) * 0.5;
|
||||
let len = d.max(1e-12);
|
||||
let px = -dy / len;
|
||||
let py = dx / len;
|
||||
let sagitta_sign = if bulge > 0.0 { 1.0_f64 } else { -1.0_f64 };
|
||||
let h = r - (r * r - d * d / 4.0).max(0.0).sqrt();
|
||||
let cx = mx - sagitta_sign * px * (r - h);
|
||||
let cy = my - sagitta_sign * py * (r - h);
|
||||
let mid_a = {
|
||||
let a0 = (p0.y - cy).atan2(p0.x - cx);
|
||||
let a1 = (p1.y - cy).atan2(p1.x - cx);
|
||||
let (sa, mut ea) = if bulge > 0.0 { (a0, a1) } else { (a1, a0) };
|
||||
if ea < sa {
|
||||
ea += TAU;
|
||||
}
|
||||
sa + (ea - sa) * 0.5
|
||||
};
|
||||
let p_mid = Point3::new(cx + r * mid_a.cos(), cy + r * mid_a.sin(), p0.z);
|
||||
let tv0 = builder::vertex(p0);
|
||||
let tv1 = builder::vertex(p1);
|
||||
edges.push(builder::circle_arc(&tv0, &tv1, p_mid));
|
||||
tangents.push(TangentGeom::Circle {
|
||||
center: [cx as f32, cy as f32, p0.z as f32],
|
||||
radius: r as f32,
|
||||
});
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
key_verts.push([p0.x as f32, p0.y as f32, p0.z as f32]);
|
||||
}
|
||||
key_verts.push([p1.x as f32, p1.y as f32, p1.z as f32]);
|
||||
}
|
||||
|
||||
TruckEntity {
|
||||
object: TruckObject::Contour(edges.into_iter().collect::<Wire>()),
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: tangents,
|
||||
key_vertices: key_verts,
|
||||
}
|
||||
}
|
||||
|
||||
fn grips(pline: &LwPolyline) -> Vec<GripDef> {
|
||||
let elev = pline.elevation as f32;
|
||||
pline
|
||||
.vertices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, v)| square_grip(i, Vec3::new(v.location.x as f32, v.location.y as f32, elev)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn properties(pline: &LwPolyline) -> PropSection {
|
||||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props: vec![
|
||||
ro("Vertices", "vertices", pline.vertices.len().to_string()),
|
||||
ro(
|
||||
"Closed",
|
||||
"closed",
|
||||
if pline.is_closed { "Yes" } else { "No" },
|
||||
),
|
||||
edit("Elevation", "elevation", pline.elevation),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_geom_prop(pline: &mut LwPolyline, field: &str, value: &str) {
|
||||
let Some(v) = parse_f64(value) else {
|
||||
return;
|
||||
};
|
||||
if field == "elevation" {
|
||||
pline.elevation = v;
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_grip(pline: &mut LwPolyline, grip_id: usize, apply: GripApply) {
|
||||
if let Some(v) = pline.vertices.get_mut(grip_id) {
|
||||
match apply {
|
||||
GripApply::Absolute(p) => {
|
||||
v.location.x = p.x as f64;
|
||||
v.location.y = p.y as f64;
|
||||
}
|
||||
GripApply::Translate(d) => {
|
||||
v.location.x += d.x as f64;
|
||||
v.location.y += d.y as f64;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_transform(pline: &mut LwPolyline, t: &EntityTransform) {
|
||||
crate::scene::transform::apply_standard_entity_transform(pline, t, |entity, p1, p2| {
|
||||
for v in &mut entity.vertices {
|
||||
crate::scene::transform::reflect_xy_point(&mut v.location.x, &mut v.location.y, p1, p2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
impl TruckConvertible for LwPolyline {
|
||||
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
||||
Some(to_truck(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl Grippable for LwPolyline {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
grips(self)
|
||||
}
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
apply_grip(self, grip_id, apply);
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyEditable for LwPolyline {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection {
|
||||
properties(self)
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
apply_geom_prop(self, field, value);
|
||||
}
|
||||
}
|
||||
|
||||
impl Transformable for LwPolyline {
|
||||
fn apply_transform(&mut self, t: &EntityTransform) {
|
||||
apply_transform(self, t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
pub mod arc;
|
||||
pub mod circle;
|
||||
mod common;
|
||||
pub mod dimension;
|
||||
pub mod ellipse;
|
||||
pub mod hatch;
|
||||
pub mod insert;
|
||||
pub mod leader;
|
||||
pub mod line;
|
||||
pub mod multileader;
|
||||
pub mod lwpolyline;
|
||||
pub mod mtext;
|
||||
pub mod point;
|
||||
pub mod spline;
|
||||
pub mod text;
|
||||
mod text_support;
|
||||
pub mod traits;
|
||||
pub mod viewport;
|
||||
@@ -0,0 +1,372 @@
|
||||
use acadrust::entities::{AttachmentPoint, DrawingDirection, MText};
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{edit_prop as edit, ro_prop as ro, square_grip, triangle_grip};
|
||||
use crate::entities::text_support::{
|
||||
resolve_text_style, split_mtext_lines, strip_mtext_codes, word_wrap,
|
||||
};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
|
||||
use crate::scene::acad_to_truck::{TruckEntity, TruckObject};
|
||||
use crate::scene::cxf;
|
||||
use crate::scene::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||||
use crate::scene::wire_model::SnapHint;
|
||||
|
||||
fn attachment_str(a: &AttachmentPoint) -> &'static str {
|
||||
match a {
|
||||
AttachmentPoint::TopLeft => "Top Left",
|
||||
AttachmentPoint::TopCenter => "Top Center",
|
||||
AttachmentPoint::TopRight => "Top Right",
|
||||
AttachmentPoint::MiddleLeft => "Middle Left",
|
||||
AttachmentPoint::MiddleCenter => "Middle Center",
|
||||
AttachmentPoint::MiddleRight => "Middle Right",
|
||||
AttachmentPoint::BottomLeft => "Bottom Left",
|
||||
AttachmentPoint::BottomCenter => "Bottom Center",
|
||||
AttachmentPoint::BottomRight => "Bottom Right",
|
||||
}
|
||||
}
|
||||
|
||||
fn mtext_halign_str(a: &AttachmentPoint) -> &'static str {
|
||||
match a {
|
||||
AttachmentPoint::TopLeft | AttachmentPoint::MiddleLeft | AttachmentPoint::BottomLeft => {
|
||||
"Left"
|
||||
}
|
||||
AttachmentPoint::TopCenter
|
||||
| AttachmentPoint::MiddleCenter
|
||||
| AttachmentPoint::BottomCenter => "Center",
|
||||
AttachmentPoint::TopRight | AttachmentPoint::MiddleRight | AttachmentPoint::BottomRight => {
|
||||
"Right"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mtext_valign_str(a: &AttachmentPoint) -> &'static str {
|
||||
match a {
|
||||
AttachmentPoint::TopLeft | AttachmentPoint::TopCenter | AttachmentPoint::TopRight => "Top",
|
||||
AttachmentPoint::MiddleLeft
|
||||
| AttachmentPoint::MiddleCenter
|
||||
| AttachmentPoint::MiddleRight => "Middle",
|
||||
AttachmentPoint::BottomLeft
|
||||
| AttachmentPoint::BottomCenter
|
||||
| AttachmentPoint::BottomRight => "Bottom",
|
||||
}
|
||||
}
|
||||
|
||||
fn mtext_attachment_from_align(h: &str, v: &str) -> Option<AttachmentPoint> {
|
||||
Some(match (h, v) {
|
||||
("Left", "Top") => AttachmentPoint::TopLeft,
|
||||
("Center", "Top") => AttachmentPoint::TopCenter,
|
||||
("Right", "Top") => AttachmentPoint::TopRight,
|
||||
("Left", "Middle") => AttachmentPoint::MiddleLeft,
|
||||
("Center", "Middle") => AttachmentPoint::MiddleCenter,
|
||||
("Right", "Middle") => AttachmentPoint::MiddleRight,
|
||||
("Left", "Bottom") => AttachmentPoint::BottomLeft,
|
||||
("Center", "Bottom") => AttachmentPoint::BottomCenter,
|
||||
("Right", "Bottom") => AttachmentPoint::BottomRight,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn drawing_dir_str(d: &DrawingDirection) -> &'static str {
|
||||
match d {
|
||||
DrawingDirection::LeftToRight => "Left to Right",
|
||||
DrawingDirection::TopToBottom => "Top to Bottom",
|
||||
DrawingDirection::ByStyle => "By Style",
|
||||
}
|
||||
}
|
||||
|
||||
fn to_truck(t: &MText, document: &acadrust::CadDocument) -> TruckEntity {
|
||||
let resolved_style = resolve_text_style(&t.style, document);
|
||||
let font_name = resolved_style.font_name;
|
||||
let font = cxf::get_font(&font_name);
|
||||
let style_width_factor = resolved_style.width_factor.max(0.01);
|
||||
let style_oblique = resolved_style.oblique_angle;
|
||||
let plain = strip_mtext_codes(&t.value);
|
||||
let explicit_lines = split_mtext_lines(&plain);
|
||||
let lines: Vec<String> = if t.rectangle_width > 0.0 {
|
||||
let scale = t.height as f32 / 9.0 * style_width_factor;
|
||||
let max_w = t.rectangle_width as f32;
|
||||
explicit_lines
|
||||
.iter()
|
||||
.flat_map(|line| word_wrap(line, max_w, scale, font))
|
||||
.collect()
|
||||
} else {
|
||||
explicit_lines
|
||||
};
|
||||
let n_lines = lines.len().max(1) as f32;
|
||||
let ls_factor = if t.line_spacing_factor > 0.0 {
|
||||
t.line_spacing_factor as f32
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let line_h = t.height as f32 * ls_factor * font.line_spacing;
|
||||
let total_h = line_h * n_lines;
|
||||
let v_offset = match t.attachment_point {
|
||||
AttachmentPoint::TopLeft | AttachmentPoint::TopCenter | AttachmentPoint::TopRight => 0.0,
|
||||
AttachmentPoint::MiddleLeft
|
||||
| AttachmentPoint::MiddleCenter
|
||||
| AttachmentPoint::MiddleRight => -total_h * 0.5,
|
||||
AttachmentPoint::BottomLeft
|
||||
| AttachmentPoint::BottomCenter
|
||||
| AttachmentPoint::BottomRight => -total_h,
|
||||
};
|
||||
let h_anchor = match t.attachment_point {
|
||||
AttachmentPoint::TopCenter
|
||||
| AttachmentPoint::MiddleCenter
|
||||
| AttachmentPoint::BottomCenter => 0.5,
|
||||
AttachmentPoint::TopRight | AttachmentPoint::MiddleRight | AttachmentPoint::BottomRight => {
|
||||
1.0
|
||||
}
|
||||
_ => 0.0,
|
||||
};
|
||||
let vertical_text = matches!(t.drawing_direction, DrawingDirection::TopToBottom);
|
||||
let rot = t.rotation as f32;
|
||||
let (cos_r, sin_r) = (rot.cos(), rot.sin());
|
||||
let insertion = Vec3::new(
|
||||
t.insertion_point.x as f32,
|
||||
t.insertion_point.y as f32,
|
||||
t.insertion_point.z as f32,
|
||||
);
|
||||
let mut all_strokes = Vec::new();
|
||||
for (i, line) in lines.iter().enumerate() {
|
||||
let li = i as f32;
|
||||
let (ox, oy) = if vertical_text {
|
||||
let col_offset = li * t.height as f32 * 1.2;
|
||||
(
|
||||
t.insertion_point.x as f32 + col_offset * cos_r + v_offset * (-sin_r),
|
||||
t.insertion_point.y as f32 + col_offset * sin_r + v_offset * cos_r,
|
||||
)
|
||||
} else {
|
||||
let line_y = -(li * line_h) + v_offset;
|
||||
(
|
||||
t.insertion_point.x as f32 + line_y * (-sin_r),
|
||||
t.insertion_point.y as f32 + line_y * cos_r,
|
||||
)
|
||||
};
|
||||
let line_w = if h_anchor > 0.0 {
|
||||
let scale = t.height as f32 / 9.0 * style_width_factor;
|
||||
line.chars()
|
||||
.map(|c| {
|
||||
if c == ' ' {
|
||||
return font.word_spacing * scale;
|
||||
}
|
||||
font.glyph(c)
|
||||
.map(|g| (g.advance + font.letter_spacing) * scale)
|
||||
.unwrap_or(t.height as f32 * 0.6)
|
||||
})
|
||||
.sum()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let h_shift = -line_w * h_anchor;
|
||||
let origin_x = ox + h_shift * cos_r;
|
||||
let origin_y = oy + h_shift * sin_r;
|
||||
let strokes = cxf::tessellate_text_ex(
|
||||
[origin_x, origin_y],
|
||||
t.height as f32,
|
||||
rot,
|
||||
style_width_factor,
|
||||
style_oblique,
|
||||
&font_name,
|
||||
line,
|
||||
);
|
||||
all_strokes.extend(strokes);
|
||||
}
|
||||
TruckEntity {
|
||||
object: TruckObject::Text(all_strokes),
|
||||
snap_pts: vec![(insertion, SnapHint::Insertion)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn grips(t: &MText) -> Vec<GripDef> {
|
||||
let p = Vec3::new(
|
||||
t.insertion_point.x as f32,
|
||||
t.insertion_point.y as f32,
|
||||
t.insertion_point.z as f32,
|
||||
);
|
||||
let dir = Vec3::new((t.rotation as f32).cos(), (t.rotation as f32).sin(), 0.0);
|
||||
let width_grip = p + dir * t.rectangle_width.max(0.0) as f32;
|
||||
vec![square_grip(0, p), triangle_grip(1, width_grip)]
|
||||
}
|
||||
|
||||
fn properties(t: &MText, text_style_names: &[String]) -> PropSection {
|
||||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props: vec![
|
||||
edit("Insert X", "ins_x", t.insertion_point.x),
|
||||
edit("Insert Y", "ins_y", t.insertion_point.y),
|
||||
edit("Insert Z", "ins_z", t.insertion_point.z),
|
||||
edit("Height", "height", t.height),
|
||||
edit("Width", "rect_w", t.rectangle_width),
|
||||
edit("Rect Height", "rect_h", t.rectangle_height.unwrap_or(0.0)),
|
||||
edit("Rotation", "rotation", t.rotation.to_degrees()),
|
||||
edit("Line Spacing", "line_spacing", t.line_spacing_factor),
|
||||
Property {
|
||||
label: "H-Align".into(),
|
||||
field: "h_align",
|
||||
value: PropValue::Choice {
|
||||
selected: mtext_halign_str(&t.attachment_point).to_string(),
|
||||
options: ["Left", "Center", "Right"]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect(),
|
||||
},
|
||||
},
|
||||
Property {
|
||||
label: "V-Align".into(),
|
||||
field: "v_align",
|
||||
value: PropValue::Choice {
|
||||
selected: mtext_valign_str(&t.attachment_point).to_string(),
|
||||
options: ["Top", "Middle", "Bottom"]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect(),
|
||||
},
|
||||
},
|
||||
ro(
|
||||
"Attachment",
|
||||
"attachment",
|
||||
attachment_str(&t.attachment_point).to_string(),
|
||||
),
|
||||
ro(
|
||||
"Direction",
|
||||
"direction",
|
||||
drawing_dir_str(&t.drawing_direction).to_string(),
|
||||
),
|
||||
Property {
|
||||
label: "Content".into(),
|
||||
field: "content",
|
||||
value: PropValue::EditText(t.value.clone()),
|
||||
},
|
||||
Property {
|
||||
label: "Style".into(),
|
||||
field: "style",
|
||||
value: PropValue::Choice {
|
||||
selected: if t.style.trim().is_empty() {
|
||||
"Standard".into()
|
||||
} else {
|
||||
t.style.clone()
|
||||
},
|
||||
options: text_style_names.to_vec(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_geom_prop(t: &mut MText, field: &str, value: &str) {
|
||||
match field {
|
||||
"content" => {
|
||||
t.value = value.to_string();
|
||||
return;
|
||||
}
|
||||
"style" => {
|
||||
t.style = value.to_string();
|
||||
return;
|
||||
}
|
||||
"h_align" => {
|
||||
if let Some(next) =
|
||||
mtext_attachment_from_align(value, mtext_valign_str(&t.attachment_point))
|
||||
{
|
||||
t.attachment_point = next;
|
||||
}
|
||||
return;
|
||||
}
|
||||
"v_align" => {
|
||||
if let Some(next) =
|
||||
mtext_attachment_from_align(mtext_halign_str(&t.attachment_point), value)
|
||||
{
|
||||
t.attachment_point = next;
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let Some(v) = crate::entities::common::parse_f64(value) else {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"ins_x" => t.insertion_point.x = v,
|
||||
"ins_y" => t.insertion_point.y = v,
|
||||
"ins_z" => t.insertion_point.z = v,
|
||||
"height" if v > 0.0 => t.height = v,
|
||||
"rect_w" if v > 0.0 => t.rectangle_width = v,
|
||||
"rect_h" if v > 0.0 => t.rectangle_height = Some(v),
|
||||
"rotation" => t.rotation = v.to_radians(),
|
||||
"line_spacing" if v > 0.0 => t.line_spacing_factor = v,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_grip(t: &mut MText, grip_id: usize, apply: GripApply) {
|
||||
match (grip_id, apply) {
|
||||
(0, GripApply::Absolute(p)) => {
|
||||
t.insertion_point.x = p.x as f64;
|
||||
t.insertion_point.y = p.y as f64;
|
||||
t.insertion_point.z = p.z as f64;
|
||||
}
|
||||
(0, GripApply::Translate(d)) => {
|
||||
t.insertion_point.x += d.x as f64;
|
||||
t.insertion_point.y += d.y as f64;
|
||||
t.insertion_point.z += d.z as f64;
|
||||
}
|
||||
(1, GripApply::Absolute(p)) => {
|
||||
let dir_x = t.rotation.cos();
|
||||
let dir_y = t.rotation.sin();
|
||||
let dx = p.x as f64 - t.insertion_point.x;
|
||||
let dy = p.y as f64 - t.insertion_point.y;
|
||||
let projected = dx * dir_x + dy * dir_y;
|
||||
t.rectangle_width = projected.max(0.01);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_transform(t: &mut MText, tr: &EntityTransform) {
|
||||
crate::scene::transform::apply_standard_entity_transform(t, tr, |entity, p1, p2| {
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut entity.insertion_point.x,
|
||||
&mut entity.insertion_point.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
let dx = (p2.x - p1.x) as f64;
|
||||
let dy = (p2.y - p1.y) as f64;
|
||||
let line_angle = dy.atan2(dx);
|
||||
entity.rotation = 2.0 * line_angle - entity.rotation;
|
||||
});
|
||||
}
|
||||
|
||||
impl TruckConvertible for MText {
|
||||
fn to_truck(&self, document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
||||
Some(to_truck(self, document))
|
||||
}
|
||||
}
|
||||
|
||||
impl Grippable for MText {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
grips(self)
|
||||
}
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
apply_grip(self, grip_id, apply);
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyEditable for MText {
|
||||
fn geometry_properties(&self, text_style_names: &[String]) -> PropSection {
|
||||
properties(self, text_style_names)
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
apply_geom_prop(self, field, value);
|
||||
}
|
||||
}
|
||||
|
||||
impl Transformable for MText {
|
||||
fn apply_transform(&mut self, t: &EntityTransform) {
|
||||
apply_transform(self, t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
use acadrust::entities::{LeaderContentType, MultiLeader, MultiLeaderPathType, TextAttachmentType};
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{diamond_grip, edit_prop as edit, ro_prop as ro, square_grip};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
|
||||
use crate::scene::acad_to_truck::{TruckEntity, TruckObject};
|
||||
use crate::scene::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||||
use crate::scene::wire_model::TangentGeom;
|
||||
|
||||
// ── TruckConvertible ────────────────────────────────────────────────────────
|
||||
|
||||
fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
||||
let nan = [f32::NAN; 3];
|
||||
let p3 = |v: &acadrust::types::Vector3| -> [f32; 3] { [v.x as f32, v.y as f32, v.z as f32] };
|
||||
|
||||
let arrow_size = ml.arrowhead_size as f32;
|
||||
let draw_arrow = arrow_size > 0.0;
|
||||
let invisible = ml.path_type == MultiLeaderPathType::Invisible;
|
||||
|
||||
let mut points: Vec<[f32; 3]> = Vec::new();
|
||||
let mut tangents: Vec<TangentGeom> = Vec::new();
|
||||
let mut key_verts: Vec<[f32; 3]> = Vec::new();
|
||||
let mut first = true;
|
||||
|
||||
for root in &ml.context.leader_roots {
|
||||
let cp = &root.connection_point;
|
||||
let cp_f = p3(cp);
|
||||
|
||||
for line in &root.lines {
|
||||
if line.points.is_empty() { continue; }
|
||||
|
||||
if !invisible {
|
||||
if !first { points.push(nan); }
|
||||
first = false;
|
||||
|
||||
for p in &line.points {
|
||||
points.push(p3(p));
|
||||
key_verts.push(p3(p));
|
||||
}
|
||||
|
||||
// Closing segment: last bend → connection_point
|
||||
let last = line.points.last().unwrap();
|
||||
let last_f = p3(last);
|
||||
let dist = ((last_f[0]-cp_f[0]).powi(2) + (last_f[1]-cp_f[1]).powi(2)).sqrt();
|
||||
if dist > 1e-9 {
|
||||
points.push(cp_f);
|
||||
key_verts.push(cp_f);
|
||||
}
|
||||
|
||||
for i in 0..line.points.len().saturating_sub(1) {
|
||||
tangents.push(TangentGeom::Line {
|
||||
p1: p3(&line.points[i]),
|
||||
p2: p3(&line.points[i + 1]),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Arrowhead
|
||||
if draw_arrow {
|
||||
let tip = &line.points[0];
|
||||
let tip_f = p3(tip);
|
||||
let next = if line.points.len() >= 2 { line.points[1] } else { *cp };
|
||||
let dx = (next.x - tip.x) as f32;
|
||||
let dy = (next.y - tip.y) as f32;
|
||||
let dl = (dx*dx + dy*dy).sqrt().max(1e-9);
|
||||
let (dx, dy) = (dx / dl, dy / dl);
|
||||
let a = std::f32::consts::PI / 6.0;
|
||||
let (s, c) = a.sin_cos();
|
||||
points.push(nan);
|
||||
points.push([tip_f[0]+(dx*c-dy*s)*arrow_size, tip_f[1]+(dx*s+dy*c)*arrow_size, tip_f[2]]);
|
||||
points.push(tip_f);
|
||||
points.push([tip_f[0]+(dx*c+dy*s)*arrow_size, tip_f[1]+(-dx*s+dy*c)*arrow_size, tip_f[2]]);
|
||||
}
|
||||
}
|
||||
|
||||
// Landing shelf at connection_point
|
||||
if ml.enable_landing && ml.enable_dogleg && ml.dogleg_length > 0.0 {
|
||||
let dir = &root.direction;
|
||||
let dl = (dir.x*dir.x + dir.y*dir.y).sqrt().max(1e-9);
|
||||
let d = ml.dogleg_length;
|
||||
points.push(nan);
|
||||
points.push(cp_f);
|
||||
points.push([(cp.x + dir.x/dl*d) as f32, (cp.y + dir.y/dl*d) as f32, cp.z as f32]);
|
||||
}
|
||||
}
|
||||
|
||||
// Text strokes (MText content rendered inline)
|
||||
if ml.content_type == LeaderContentType::MText && !ml.context.text_string.is_empty() {
|
||||
let height = if ml.context.text_height > 0.0 { ml.context.text_height } else { ml.text_height };
|
||||
let ins = &ml.context.text_location;
|
||||
let z = ins.z as f32;
|
||||
let style = crate::entities::text_support::resolve_text_style("STANDARD", document);
|
||||
let strokes = crate::scene::cxf::tessellate_text_ex(
|
||||
[ins.x as f32, ins.y as f32],
|
||||
height as f32,
|
||||
0.0,
|
||||
style.width_factor.max(0.01),
|
||||
style.oblique_angle,
|
||||
&style.font_name,
|
||||
&ml.context.text_string,
|
||||
);
|
||||
for stroke in &strokes {
|
||||
if stroke.len() < 2 { continue; }
|
||||
points.push(nan);
|
||||
for &[x, y] in stroke {
|
||||
points.push([x, y, z]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if points.is_empty() { return None; }
|
||||
|
||||
Some(TruckEntity {
|
||||
object: TruckObject::Lines(points),
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: tangents,
|
||||
key_vertices: key_verts,
|
||||
})
|
||||
}
|
||||
|
||||
// ── Grips ──────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// IDs are assigned in two passes:
|
||||
// 0 .. total_line_pts - 1 : square grips on every LeaderLine vertex
|
||||
// total_line_pts : diamond grip on context.text_location (if MText)
|
||||
|
||||
fn grips(ml: &MultiLeader) -> Vec<GripDef> {
|
||||
let mut result: Vec<GripDef> = Vec::new();
|
||||
let mut id = 0usize;
|
||||
|
||||
for root in &ml.context.leader_roots {
|
||||
for line in &root.lines {
|
||||
for p in &line.points {
|
||||
result.push(square_grip(id, Vec3::new(p.x as f32, p.y as f32, p.z as f32)));
|
||||
id += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ml.content_type == LeaderContentType::MText {
|
||||
let tl = &ml.context.text_location;
|
||||
result.push(diamond_grip(id, Vec3::new(tl.x as f32, tl.y as f32, tl.z as f32)));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn apply_grip(ml: &mut MultiLeader, grip_id: usize, apply: GripApply) {
|
||||
let mut idx = 0usize;
|
||||
|
||||
for root in &mut ml.context.leader_roots {
|
||||
for line in &mut root.lines {
|
||||
for p in &mut line.points {
|
||||
if idx == grip_id {
|
||||
match apply {
|
||||
GripApply::Absolute(a) => {
|
||||
p.x = a.x as f64;
|
||||
p.y = a.y as f64;
|
||||
p.z = a.z as f64;
|
||||
}
|
||||
GripApply::Translate(d) => {
|
||||
p.x += d.x as f64;
|
||||
p.y += d.y as f64;
|
||||
p.z += d.z as f64;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Text location grip
|
||||
if ml.content_type == LeaderContentType::MText && idx == grip_id {
|
||||
let tl = &mut ml.context.text_location;
|
||||
match apply {
|
||||
GripApply::Absolute(a) => {
|
||||
tl.x = a.x as f64;
|
||||
tl.y = a.y as f64;
|
||||
tl.z = a.z as f64;
|
||||
}
|
||||
GripApply::Translate(d) => {
|
||||
tl.x += d.x as f64;
|
||||
tl.y += d.y as f64;
|
||||
tl.z += d.z as f64;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Properties ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn content_type_str(ct: &LeaderContentType) -> &'static str {
|
||||
match ct {
|
||||
LeaderContentType::None => "None",
|
||||
LeaderContentType::Block => "Block",
|
||||
LeaderContentType::MText => "MText",
|
||||
LeaderContentType::Tolerance => "Tolerance",
|
||||
}
|
||||
}
|
||||
|
||||
fn path_type_str(pt: &MultiLeaderPathType) -> &'static str {
|
||||
match pt {
|
||||
MultiLeaderPathType::Invisible => "Invisible",
|
||||
MultiLeaderPathType::StraightLineSegments => "Straight",
|
||||
MultiLeaderPathType::Spline => "Spline",
|
||||
}
|
||||
}
|
||||
|
||||
fn attachment_str(a: &TextAttachmentType) -> &'static str {
|
||||
match a {
|
||||
TextAttachmentType::TopOfTopLine => "Top of Top",
|
||||
TextAttachmentType::MiddleOfTopLine => "Mid of Top",
|
||||
TextAttachmentType::MiddleOfText => "Mid of Text",
|
||||
TextAttachmentType::MiddleOfBottomLine => "Mid of Bot",
|
||||
TextAttachmentType::BottomOfBottomLine => "Bot of Bot",
|
||||
TextAttachmentType::BottomLine => "Bottom Line",
|
||||
_ => "Other",
|
||||
}
|
||||
}
|
||||
|
||||
fn bool_toggle(label: &str, field: &'static str, value: bool) -> Property {
|
||||
Property {
|
||||
label: label.into(),
|
||||
field,
|
||||
value: PropValue::BoolToggle { field, value },
|
||||
}
|
||||
}
|
||||
|
||||
fn choice(label: &str, field: &'static str, selected: &str, opts: &[&str]) -> Property {
|
||||
Property {
|
||||
label: label.into(),
|
||||
field,
|
||||
value: PropValue::Choice {
|
||||
selected: selected.to_string(),
|
||||
options: opts.iter().map(|s| s.to_string()).collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn properties(ml: &MultiLeader) -> PropSection {
|
||||
let ctx = &ml.context;
|
||||
let total_pts: usize = ctx
|
||||
.leader_roots
|
||||
.iter()
|
||||
.flat_map(|r| r.lines.iter())
|
||||
.map(|l| l.points.len())
|
||||
.sum();
|
||||
|
||||
let mut props = vec![
|
||||
// Content
|
||||
choice("Content Type", "content_type", content_type_str(&ml.content_type),
|
||||
&["None", "MText", "Block", "Tolerance"]),
|
||||
Property {
|
||||
label: "Text".into(),
|
||||
field: "text_string",
|
||||
value: PropValue::EditText(ctx.text_string.clone()),
|
||||
},
|
||||
edit("Text Height", "text_height", ml.text_height),
|
||||
edit("Text X", "text_x", ctx.text_location.x),
|
||||
edit("Text Y", "text_y", ctx.text_location.y),
|
||||
edit("Text Z", "text_z", ctx.text_location.z),
|
||||
bool_toggle("Text Frame", "text_frame", ml.text_frame),
|
||||
// Leader line
|
||||
choice("Path Type", "path_type", path_type_str(&ml.path_type),
|
||||
&["Straight", "Spline", "Invisible"]),
|
||||
bool_toggle("Landing", "enable_landing", ml.enable_landing),
|
||||
bool_toggle("Dogleg", "enable_dogleg", ml.enable_dogleg),
|
||||
edit("Dogleg Length", "dogleg_length", ml.dogleg_length),
|
||||
edit("Arrow Size", "arrowhead_size", ml.arrowhead_size),
|
||||
edit("Scale", "scale_factor", ml.scale_factor),
|
||||
bool_toggle("Annotation Scale", "enable_annotation_scale", ml.enable_annotation_scale),
|
||||
// Text attachment
|
||||
choice("Left Attach", "text_left_attachment",
|
||||
attachment_str(&ml.text_left_attachment),
|
||||
&["Top of Top","Mid of Top","Mid of Text","Mid of Bot","Bot of Bot","Bottom Line"]),
|
||||
choice("Right Attach", "text_right_attachment",
|
||||
attachment_str(&ml.text_right_attachment),
|
||||
&["Top of Top","Mid of Top","Mid of Text","Mid of Bot","Bot of Bot","Bottom Line"]),
|
||||
// Stats
|
||||
ro("Leader Pts", "total_pts", total_pts.to_string()),
|
||||
ro("Roots", "root_count", ctx.leader_roots.len().to_string()),
|
||||
];
|
||||
|
||||
// Connection point for first root (most common case)
|
||||
if let Some(root) = ctx.leader_roots.first() {
|
||||
props.push(edit("Root Conn X", "conn_x", root.connection_point.x));
|
||||
props.push(edit("Root Conn Y", "conn_y", root.connection_point.y));
|
||||
props.push(edit("Root Conn Z", "conn_z", root.connection_point.z));
|
||||
}
|
||||
|
||||
PropSection { title: "Geometry".into(), props }
|
||||
}
|
||||
|
||||
fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
||||
let f64 = |s: &str| -> Option<f64> { s.trim().parse().ok() };
|
||||
|
||||
match field {
|
||||
"content_type" => {
|
||||
ml.content_type = match value {
|
||||
"Block" => LeaderContentType::Block,
|
||||
"MText" => LeaderContentType::MText,
|
||||
"Tolerance" => LeaderContentType::Tolerance,
|
||||
_ => LeaderContentType::None,
|
||||
};
|
||||
}
|
||||
"text_string" => ml.context.text_string = value.to_string(),
|
||||
"text_height" => {
|
||||
if let Some(v) = f64(value) { ml.text_height = v; ml.context.text_height = v; }
|
||||
}
|
||||
"text_x" => { if let Some(v) = f64(value) { ml.context.text_location.x = v; } }
|
||||
"text_y" => { if let Some(v) = f64(value) { ml.context.text_location.y = v; } }
|
||||
"text_z" => { if let Some(v) = f64(value) { ml.context.text_location.z = v; } }
|
||||
"text_frame" => ml.text_frame = if value == "toggle" { !ml.text_frame } else { value == "true" },
|
||||
"path_type" => {
|
||||
ml.path_type = match value {
|
||||
"Spline" => MultiLeaderPathType::Spline,
|
||||
"Invisible" => MultiLeaderPathType::Invisible,
|
||||
_ => MultiLeaderPathType::StraightLineSegments,
|
||||
};
|
||||
}
|
||||
"enable_landing" => ml.enable_landing = if value == "toggle" { !ml.enable_landing } else { value == "true" },
|
||||
"enable_dogleg" => ml.enable_dogleg = if value == "toggle" { !ml.enable_dogleg } else { value == "true" },
|
||||
"enable_annotation_scale" => ml.enable_annotation_scale = if value == "toggle" { !ml.enable_annotation_scale } else { value == "true" },
|
||||
"dogleg_length" => { if let Some(v) = f64(value) { ml.dogleg_length = v; } }
|
||||
"arrowhead_size" => { if let Some(v) = f64(value) { ml.arrowhead_size = v; } }
|
||||
"scale_factor" => { if let Some(v) = f64(value) { ml.scale_factor = v; } }
|
||||
"conn_x" => {
|
||||
if let (Some(v), Some(root)) = (f64(value), ml.context.leader_roots.first_mut()) {
|
||||
root.connection_point.x = v;
|
||||
}
|
||||
}
|
||||
"conn_y" => {
|
||||
if let (Some(v), Some(root)) = (f64(value), ml.context.leader_roots.first_mut()) {
|
||||
root.connection_point.y = v;
|
||||
}
|
||||
}
|
||||
"conn_z" => {
|
||||
if let (Some(v), Some(root)) = (f64(value), ml.context.leader_roots.first_mut()) {
|
||||
root.connection_point.z = v;
|
||||
}
|
||||
}
|
||||
"text_left_attachment" => {
|
||||
ml.text_left_attachment = parse_attachment(value);
|
||||
ml.context.text_left_attachment = parse_attachment(value);
|
||||
}
|
||||
"text_right_attachment" => {
|
||||
ml.text_right_attachment = parse_attachment(value);
|
||||
ml.context.text_right_attachment = parse_attachment(value);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_attachment(s: &str) -> TextAttachmentType {
|
||||
match s {
|
||||
"Top of Top" => TextAttachmentType::TopOfTopLine,
|
||||
"Mid of Top" => TextAttachmentType::MiddleOfTopLine,
|
||||
"Mid of Bot" => TextAttachmentType::MiddleOfBottomLine,
|
||||
"Bot of Bot" => TextAttachmentType::BottomOfBottomLine,
|
||||
"Bottom Line" => TextAttachmentType::BottomLine,
|
||||
_ => TextAttachmentType::MiddleOfText,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Transform ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn apply_transform(ml: &mut MultiLeader, t: &EntityTransform) {
|
||||
crate::scene::transform::apply_standard_entity_transform(ml, t, |entity, p1, p2| {
|
||||
for root in &mut entity.context.leader_roots {
|
||||
for line in &mut root.lines {
|
||||
for p in &mut line.points {
|
||||
crate::scene::transform::reflect_xy_point(&mut p.x, &mut p.y, p1, p2);
|
||||
}
|
||||
}
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut root.connection_point.x, &mut root.connection_point.y, p1, p2);
|
||||
}
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut entity.context.text_location.x, &mut entity.context.text_location.y, p1, p2);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Trait impls ────────────────────────────────────────────────────────────
|
||||
|
||||
impl TruckConvertible for MultiLeader {
|
||||
fn to_truck(&self, document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
||||
to_truck(self, document)
|
||||
}
|
||||
}
|
||||
|
||||
impl Grippable for MultiLeader {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
grips(self)
|
||||
}
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
apply_grip(self, grip_id, apply);
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyEditable for MultiLeader {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection {
|
||||
properties(self)
|
||||
}
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
apply_geom_prop(self, field, value);
|
||||
}
|
||||
}
|
||||
|
||||
impl Transformable for MultiLeader {
|
||||
fn apply_transform(&mut self, t: &EntityTransform) {
|
||||
apply_transform(self, t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
use acadrust::entities::Point;
|
||||
use glam::Vec3;
|
||||
use truck_modeling::{builder, Point3};
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{edit_prop as edit, parse_f64, square_grip};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
|
||||
use crate::scene::acad_to_truck::{TruckEntity, TruckObject};
|
||||
use crate::scene::object::{GripApply, GripDef, PropSection};
|
||||
use crate::scene::wire_model::SnapHint;
|
||||
|
||||
fn to_truck(pt: &Point) -> TruckEntity {
|
||||
let p = Point3::new(pt.location.x, pt.location.y, pt.location.z);
|
||||
let snap = Vec3::new(p.x as f32, p.y as f32, p.z as f32);
|
||||
TruckEntity {
|
||||
object: TruckObject::Point(builder::vertex(p)),
|
||||
snap_pts: vec![(snap, SnapHint::Node)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn grips(pt: &Point) -> Vec<GripDef> {
|
||||
let p = Vec3::new(
|
||||
pt.location.x as f32,
|
||||
pt.location.y as f32,
|
||||
pt.location.z as f32,
|
||||
);
|
||||
vec![square_grip(0, p)]
|
||||
}
|
||||
|
||||
fn properties(pt: &Point) -> PropSection {
|
||||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props: vec![
|
||||
edit("X", "loc_x", pt.location.x),
|
||||
edit("Y", "loc_y", pt.location.y),
|
||||
edit("Z", "loc_z", pt.location.z),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_geom_prop(pt: &mut Point, field: &str, value: &str) {
|
||||
let Some(v) = parse_f64(value) else {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"loc_x" => pt.location.x = v,
|
||||
"loc_y" => pt.location.y = v,
|
||||
"loc_z" => pt.location.z = v,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_grip(pt: &mut Point, _grip_id: usize, apply: GripApply) {
|
||||
match apply {
|
||||
GripApply::Absolute(p) => {
|
||||
pt.location.x = p.x as f64;
|
||||
pt.location.y = p.y as f64;
|
||||
pt.location.z = p.z as f64;
|
||||
}
|
||||
GripApply::Translate(d) => {
|
||||
pt.location.x += d.x as f64;
|
||||
pt.location.y += d.y as f64;
|
||||
pt.location.z += d.z as f64;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_transform(pt: &mut Point, t: &EntityTransform) {
|
||||
crate::scene::transform::apply_standard_entity_transform(pt, t, |entity, p1, p2| {
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut entity.location.x,
|
||||
&mut entity.location.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
impl TruckConvertible for Point {
|
||||
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
||||
Some(to_truck(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl Grippable for Point {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
grips(self)
|
||||
}
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
apply_grip(self, grip_id, apply);
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyEditable for Point {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection {
|
||||
properties(self)
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
apply_geom_prop(self, field, value);
|
||||
}
|
||||
}
|
||||
|
||||
impl Transformable for Point {
|
||||
fn apply_transform(&mut self, t: &EntityTransform) {
|
||||
apply_transform(self, t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
use acadrust::entities::Spline;
|
||||
use glam::Vec3;
|
||||
use truck_modeling::{
|
||||
base::{BoundedCurve, ParametricCurve},
|
||||
builder, BSplineCurve, Curve, Edge, KnotVec, Point3,
|
||||
};
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{ro_prop as ro, square_grip};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
|
||||
use crate::scene::acad_to_truck::{TruckEntity, TruckObject};
|
||||
use crate::scene::object::{GripApply, GripDef, PropSection};
|
||||
|
||||
fn to_truck(spl: &Spline) -> TruckEntity {
|
||||
let ctrl_pts: Vec<Point3> = spl
|
||||
.control_points
|
||||
.iter()
|
||||
.map(|p| Point3::new(p.x, p.y, p.z))
|
||||
.collect();
|
||||
if ctrl_pts.len() < 2 {
|
||||
let v = builder::vertex(Point3::new(0.0, 0.0, 0.0));
|
||||
let edge = builder::line(&v, &v);
|
||||
return TruckEntity {
|
||||
object: TruckObject::Curve(edge),
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
};
|
||||
}
|
||||
let knot_vec = if !spl.knots.is_empty() {
|
||||
KnotVec::from(spl.knots.clone())
|
||||
} else {
|
||||
KnotVec::uniform_knot(spl.degree as usize, ctrl_pts.len() - 1)
|
||||
};
|
||||
let bspline = BSplineCurve::new(knot_vec, ctrl_pts);
|
||||
let (t0, t1) = bspline.range_tuple();
|
||||
let p_start = bspline.subs(t0);
|
||||
let p_end = bspline.subs(t1);
|
||||
let v_start = builder::vertex(p_start);
|
||||
let v_end = builder::vertex(p_end);
|
||||
let edge = Edge::new(&v_start, &v_end, Curve::BSplineCurve(bspline));
|
||||
TruckEntity {
|
||||
object: TruckObject::Curve(edge),
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn grips(spline: &Spline) -> Vec<GripDef> {
|
||||
spline
|
||||
.control_points
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, p)| square_grip(i, Vec3::new(p.x as f32, p.y as f32, p.z as f32)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn properties(spline: &Spline) -> PropSection {
|
||||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props: vec![
|
||||
ro("Degree", "degree", spline.degree.to_string()),
|
||||
ro(
|
||||
"Control Pts",
|
||||
"ctrl_pts",
|
||||
spline.control_points.len().to_string(),
|
||||
),
|
||||
ro("Fit Pts", "fit_pts", spline.fit_points.len().to_string()),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_geom_prop(_spline: &mut Spline, _field: &str, _value: &str) {}
|
||||
|
||||
fn apply_grip(spline: &mut Spline, grip_id: usize, apply: GripApply) {
|
||||
if let Some(cp) = spline.control_points.get_mut(grip_id) {
|
||||
match apply {
|
||||
GripApply::Absolute(p) => {
|
||||
cp.x = p.x as f64;
|
||||
cp.y = p.y as f64;
|
||||
cp.z = p.z as f64;
|
||||
}
|
||||
GripApply::Translate(d) => {
|
||||
cp.x += d.x as f64;
|
||||
cp.y += d.y as f64;
|
||||
cp.z += d.z as f64;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_transform(spline: &mut Spline, t: &EntityTransform) {
|
||||
crate::scene::transform::apply_standard_entity_transform(spline, t, |entity, p1, p2| {
|
||||
for cp in &mut entity.control_points {
|
||||
crate::scene::transform::reflect_xy_point(&mut cp.x, &mut cp.y, p1, p2);
|
||||
}
|
||||
for fp in &mut entity.fit_points {
|
||||
crate::scene::transform::reflect_xy_point(&mut fp.x, &mut fp.y, p1, p2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
impl TruckConvertible for Spline {
|
||||
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
||||
Some(to_truck(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl Grippable for Spline {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
grips(self)
|
||||
}
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
apply_grip(self, grip_id, apply);
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyEditable for Spline {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection {
|
||||
properties(self)
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
apply_geom_prop(self, field, value);
|
||||
}
|
||||
}
|
||||
|
||||
impl Transformable for Spline {
|
||||
fn apply_transform(&mut self, t: &EntityTransform) {
|
||||
apply_transform(self, t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
use acadrust::entities::{Text, TextHorizontalAlignment as HA, TextVerticalAlignment as VA};
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{edit_prop as edit, parse_f64, square_grip};
|
||||
use crate::entities::text_support::{resolve_text_style, text_local_bounds};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
|
||||
use crate::scene::acad_to_truck::{TruckEntity, TruckObject};
|
||||
use crate::scene::cxf;
|
||||
use crate::scene::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||||
use crate::scene::wire_model::SnapHint;
|
||||
|
||||
fn text_halign_str(a: &acadrust::entities::TextHorizontalAlignment) -> &'static str {
|
||||
use acadrust::entities::TextHorizontalAlignment::*;
|
||||
match a {
|
||||
Left => "Left",
|
||||
Center => "Center",
|
||||
Right => "Right",
|
||||
Aligned => "Aligned",
|
||||
Middle => "Middle",
|
||||
Fit => "Fit",
|
||||
}
|
||||
}
|
||||
|
||||
fn text_valign_str(a: &acadrust::entities::TextVerticalAlignment) -> &'static str {
|
||||
use acadrust::entities::TextVerticalAlignment::*;
|
||||
match a {
|
||||
Baseline => "Baseline",
|
||||
Bottom => "Bottom",
|
||||
Middle => "Middle",
|
||||
Top => "Top",
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_text_alignment_point(t: &mut Text) {
|
||||
let needs_alignment_point = !matches!(
|
||||
(t.horizontal_alignment, t.vertical_alignment),
|
||||
(HA::Left, VA::Baseline)
|
||||
);
|
||||
if needs_alignment_point {
|
||||
if t.alignment_point.is_none() {
|
||||
t.alignment_point = Some(t.insertion_point);
|
||||
}
|
||||
} else {
|
||||
t.alignment_point = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn to_truck(t: &Text, document: &acadrust::CadDocument) -> TruckEntity {
|
||||
let snap_pt = Vec3::new(
|
||||
t.insertion_point.x as f32,
|
||||
t.insertion_point.y as f32,
|
||||
t.insertion_point.z as f32,
|
||||
);
|
||||
let resolved_style = resolve_text_style(&t.style, document);
|
||||
let font_name = resolved_style.font_name;
|
||||
let width_factor = (if t.width_factor > 0.0 {
|
||||
t.width_factor as f32
|
||||
} else {
|
||||
1.0
|
||||
} * resolved_style.width_factor.max(0.01))
|
||||
.clamp(0.01, 100.0);
|
||||
let oblique_angle = t.oblique_angle as f32 + resolved_style.oblique_angle;
|
||||
let anchor = match (
|
||||
&t.horizontal_alignment,
|
||||
&t.vertical_alignment,
|
||||
&t.alignment_point,
|
||||
) {
|
||||
(HA::Aligned | HA::Middle | HA::Fit, _, Some(a)) => [a.x as f32, a.y as f32],
|
||||
(HA::Center | HA::Right, _, Some(a)) => [a.x as f32, a.y as f32],
|
||||
(_, VA::Bottom | VA::Middle | VA::Top, Some(a)) => [a.x as f32, a.y as f32],
|
||||
_ => [t.insertion_point.x as f32, t.insertion_point.y as f32],
|
||||
};
|
||||
let bounds = text_local_bounds(
|
||||
&font_name,
|
||||
&t.value,
|
||||
t.height as f32,
|
||||
width_factor,
|
||||
oblique_angle,
|
||||
);
|
||||
let (anchor_local_x, anchor_local_y) = if let Some(([min_x, min_y], [max_x, max_y])) = bounds {
|
||||
let ax = match t.horizontal_alignment {
|
||||
HA::Left => min_x,
|
||||
HA::Center | HA::Middle => (min_x + max_x) * 0.5,
|
||||
HA::Right | HA::Aligned | HA::Fit => max_x,
|
||||
};
|
||||
let ay = match t.vertical_alignment {
|
||||
VA::Baseline => 0.0,
|
||||
VA::Bottom => min_y,
|
||||
VA::Middle => (min_y + max_y) * 0.5,
|
||||
VA::Top => max_y,
|
||||
};
|
||||
(ax, ay)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
let (cos_r, sin_r) = ((t.rotation as f32).cos(), (t.rotation as f32).sin());
|
||||
let origin = [
|
||||
anchor[0] - (anchor_local_x * cos_r - anchor_local_y * sin_r),
|
||||
anchor[1] - (anchor_local_x * sin_r + anchor_local_y * cos_r),
|
||||
];
|
||||
let strokes_2d = cxf::tessellate_text_ex(
|
||||
origin,
|
||||
t.height as f32,
|
||||
t.rotation as f32,
|
||||
width_factor,
|
||||
oblique_angle,
|
||||
&font_name,
|
||||
&t.value,
|
||||
);
|
||||
TruckEntity {
|
||||
object: TruckObject::Text(strokes_2d),
|
||||
snap_pts: vec![(snap_pt, SnapHint::Insertion)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn grips(t: &Text) -> Vec<GripDef> {
|
||||
let p = Vec3::new(
|
||||
t.insertion_point.x as f32,
|
||||
t.insertion_point.y as f32,
|
||||
t.insertion_point.z as f32,
|
||||
);
|
||||
vec![square_grip(0, p)]
|
||||
}
|
||||
|
||||
fn properties(t: &Text, text_style_names: &[String]) -> PropSection {
|
||||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props: vec![
|
||||
edit("Insert X", "ins_x", t.insertion_point.x),
|
||||
edit("Insert Y", "ins_y", t.insertion_point.y),
|
||||
edit("Insert Z", "ins_z", t.insertion_point.z),
|
||||
edit("Height", "height", t.height),
|
||||
edit("Rotation", "rotation", t.rotation.to_degrees()),
|
||||
edit("Width Factor", "width_factor", t.width_factor),
|
||||
edit(
|
||||
"Oblique Angle",
|
||||
"oblique_angle",
|
||||
t.oblique_angle.to_degrees(),
|
||||
),
|
||||
Property {
|
||||
label: "H-Align".into(),
|
||||
field: "h_align",
|
||||
value: PropValue::Choice {
|
||||
selected: text_halign_str(&t.horizontal_alignment).to_string(),
|
||||
options: ["Left", "Center", "Right", "Aligned", "Middle", "Fit"]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect(),
|
||||
},
|
||||
},
|
||||
Property {
|
||||
label: "V-Align".into(),
|
||||
field: "v_align",
|
||||
value: PropValue::Choice {
|
||||
selected: text_valign_str(&t.vertical_alignment).to_string(),
|
||||
options: ["Baseline", "Bottom", "Middle", "Top"]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect(),
|
||||
},
|
||||
},
|
||||
Property {
|
||||
label: "Content".into(),
|
||||
field: "content",
|
||||
value: PropValue::EditText(t.value.clone()),
|
||||
},
|
||||
Property {
|
||||
label: "Style".into(),
|
||||
field: "style",
|
||||
value: PropValue::Choice {
|
||||
selected: if t.style.trim().is_empty() {
|
||||
"Standard".into()
|
||||
} else {
|
||||
t.style.clone()
|
||||
},
|
||||
options: text_style_names.to_vec(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_geom_prop(t: &mut Text, field: &str, value: &str) {
|
||||
match field {
|
||||
"content" => {
|
||||
t.value = value.to_string();
|
||||
return;
|
||||
}
|
||||
"style" => {
|
||||
t.style = value.to_string();
|
||||
return;
|
||||
}
|
||||
"h_align" => {
|
||||
t.horizontal_alignment = match value {
|
||||
"Left" => HA::Left,
|
||||
"Center" => HA::Center,
|
||||
"Right" => HA::Right,
|
||||
"Aligned" => HA::Aligned,
|
||||
"Middle" => HA::Middle,
|
||||
"Fit" => HA::Fit,
|
||||
_ => return,
|
||||
};
|
||||
sync_text_alignment_point(t);
|
||||
return;
|
||||
}
|
||||
"v_align" => {
|
||||
t.vertical_alignment = match value {
|
||||
"Baseline" => VA::Baseline,
|
||||
"Bottom" => VA::Bottom,
|
||||
"Middle" => VA::Middle,
|
||||
"Top" => VA::Top,
|
||||
_ => return,
|
||||
};
|
||||
sync_text_alignment_point(t);
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let Some(v) = parse_f64(value) else {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"ins_x" => t.insertion_point.x = v,
|
||||
"ins_y" => t.insertion_point.y = v,
|
||||
"ins_z" => t.insertion_point.z = v,
|
||||
"height" if v > 0.0 => t.height = v,
|
||||
"rotation" => t.rotation = v.to_radians(),
|
||||
"width_factor" if v > 0.0 => t.width_factor = v,
|
||||
"oblique_angle" => t.oblique_angle = v.to_radians(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_grip(t: &mut Text, _grip_id: usize, apply: GripApply) {
|
||||
match apply {
|
||||
GripApply::Absolute(p) => {
|
||||
t.insertion_point.x = p.x as f64;
|
||||
t.insertion_point.y = p.y as f64;
|
||||
t.insertion_point.z = p.z as f64;
|
||||
}
|
||||
GripApply::Translate(d) => {
|
||||
t.insertion_point.x += d.x as f64;
|
||||
t.insertion_point.y += d.y as f64;
|
||||
t.insertion_point.z += d.z as f64;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_transform(t: &mut Text, tr: &EntityTransform) {
|
||||
crate::scene::transform::apply_standard_entity_transform(t, tr, |entity, p1, p2| {
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut entity.insertion_point.x,
|
||||
&mut entity.insertion_point.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
if let Some(ref mut a) = entity.alignment_point {
|
||||
crate::scene::transform::reflect_xy_point(&mut a.x, &mut a.y, p1, p2);
|
||||
}
|
||||
let dx = (p2.x - p1.x) as f64;
|
||||
let dy = (p2.y - p1.y) as f64;
|
||||
let line_angle = dy.atan2(dx);
|
||||
entity.rotation = 2.0 * line_angle - entity.rotation;
|
||||
entity.oblique_angle = -entity.oblique_angle;
|
||||
});
|
||||
}
|
||||
|
||||
impl TruckConvertible for Text {
|
||||
fn to_truck(&self, document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
||||
Some(to_truck(self, document))
|
||||
}
|
||||
}
|
||||
|
||||
impl Grippable for Text {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
grips(self)
|
||||
}
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
apply_grip(self, grip_id, apply);
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyEditable for Text {
|
||||
fn geometry_properties(&self, text_style_names: &[String]) -> PropSection {
|
||||
properties(self, text_style_names)
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
apply_geom_prop(self, field, value);
|
||||
}
|
||||
}
|
||||
|
||||
impl Transformable for Text {
|
||||
fn apply_transform(&mut self, t: &EntityTransform) {
|
||||
apply_transform(self, t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
use acadrust::CadDocument;
|
||||
|
||||
use crate::scene::cxf;
|
||||
|
||||
pub struct ResolvedTextStyle {
|
||||
pub font_name: String,
|
||||
pub width_factor: f32,
|
||||
pub oblique_angle: f32,
|
||||
}
|
||||
|
||||
pub fn resolve_text_style(style_name: &str, document: &CadDocument) -> ResolvedTextStyle {
|
||||
let style = document.text_styles.iter().find(|entry| {
|
||||
entry.name.eq_ignore_ascii_case(style_name)
|
||||
|| (style_name.trim().is_empty() && entry.name.eq_ignore_ascii_case("Standard"))
|
||||
});
|
||||
|
||||
let font_name = if let Some(style) = style {
|
||||
if !style.font_file.trim().is_empty() {
|
||||
let file = style.font_file.trim();
|
||||
let basename = file.rsplit(['/', '\\']).next().unwrap_or(file);
|
||||
let stem = basename.split('.').next().unwrap_or(basename).trim();
|
||||
if !stem.is_empty() {
|
||||
stem.to_string()
|
||||
} else if !style.true_type_font.trim().is_empty() {
|
||||
style.true_type_font.trim().to_string()
|
||||
} else if !style.name.trim().is_empty() {
|
||||
style.name.trim().to_string()
|
||||
} else {
|
||||
"Standard".to_string()
|
||||
}
|
||||
} else if !style.true_type_font.trim().is_empty() {
|
||||
style.true_type_font.trim().to_string()
|
||||
} else if !style.name.trim().is_empty() {
|
||||
style.name.trim().to_string()
|
||||
} else {
|
||||
"Standard".to_string()
|
||||
}
|
||||
} else if style_name.trim().is_empty() {
|
||||
"Standard".to_string()
|
||||
} else {
|
||||
style_name.trim().to_string()
|
||||
};
|
||||
|
||||
ResolvedTextStyle {
|
||||
font_name,
|
||||
width_factor: style.map(|s| s.width_factor as f32).unwrap_or(1.0),
|
||||
oblique_angle: style.map(|s| s.oblique_angle as f32).unwrap_or(0.0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text_local_bounds(
|
||||
font_name: &str,
|
||||
text: &str,
|
||||
height: f32,
|
||||
width_factor: f32,
|
||||
oblique_angle: f32,
|
||||
) -> Option<([f32; 2], [f32; 2])> {
|
||||
if text.is_empty() || height <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let font = cxf::get_font(font_name);
|
||||
let scale = height / 9.0;
|
||||
let wf = width_factor.clamp(0.01, 100.0);
|
||||
let ob = oblique_angle.tan();
|
||||
let mut cursor_x = 0.0_f32;
|
||||
let mut min_x = f32::INFINITY;
|
||||
let mut max_x = f32::NEG_INFINITY;
|
||||
let mut min_y = f32::INFINITY;
|
||||
let mut max_y = f32::NEG_INFINITY;
|
||||
|
||||
for ch in text.chars() {
|
||||
if ch == ' ' {
|
||||
cursor_x += font.word_spacing;
|
||||
continue;
|
||||
}
|
||||
match font.glyph(ch) {
|
||||
Some(glyph) => {
|
||||
for stroke in &glyph.strokes {
|
||||
for &[gx, gy] in stroke {
|
||||
let sx = (cursor_x + gx) * scale * wf + gy * scale * ob;
|
||||
let sy = gy * scale;
|
||||
min_x = min_x.min(sx);
|
||||
max_x = max_x.max(sx);
|
||||
min_y = min_y.min(sy);
|
||||
max_y = max_y.max(sy);
|
||||
}
|
||||
}
|
||||
cursor_x += glyph.advance + font.letter_spacing;
|
||||
}
|
||||
None => {
|
||||
cursor_x += 6.0 + font.letter_spacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if min_x.is_finite() && min_y.is_finite() && max_x.is_finite() && max_y.is_finite() {
|
||||
Some(([min_x, min_y], [max_x, max_y]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn strip_mtext_codes(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len());
|
||||
let mut chars = s.chars().peekable();
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
'\\' => match chars.peek().copied() {
|
||||
Some('P') | Some('n') | Some('N') => {
|
||||
chars.next();
|
||||
out.push('\n');
|
||||
}
|
||||
Some('~') => {
|
||||
chars.next();
|
||||
out.push(' ');
|
||||
}
|
||||
Some(c) if "pHWQTACcLOKlfFUu".contains(c) => {
|
||||
chars.next();
|
||||
for c in chars.by_ref() {
|
||||
if c == ';' {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some('\\') => {
|
||||
chars.next();
|
||||
out.push('\\');
|
||||
}
|
||||
Some('{') | Some('}') => {
|
||||
out.push(chars.next().unwrap());
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
'{' | '}' => {}
|
||||
'\r' => {}
|
||||
other => out.push(other),
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
pub fn split_mtext_lines(s: &str) -> Vec<String> {
|
||||
s.split('\n')
|
||||
.map(|l| l.trim().to_string())
|
||||
.filter(|l| !l.is_empty())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn word_wrap(text: &str, max_w: f32, scale: f32, font: &'static cxf::CxfFile) -> Vec<String> {
|
||||
if max_w <= 0.0 || text.is_empty() {
|
||||
return vec![text.to_string()];
|
||||
}
|
||||
|
||||
let glyph_w = |c: char| -> f32 {
|
||||
if c == ' ' {
|
||||
return font.word_spacing * scale;
|
||||
}
|
||||
font.glyph(c)
|
||||
.map(|g| (g.advance + font.letter_spacing) * scale)
|
||||
.unwrap_or(scale * 6.0)
|
||||
};
|
||||
|
||||
let mut lines = Vec::new();
|
||||
let mut current = String::new();
|
||||
let mut current_w = 0.0_f32;
|
||||
|
||||
for word in text.split(' ') {
|
||||
let word_w: f32 = word.chars().map(glyph_w).sum();
|
||||
let space_w = if current.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
glyph_w(' ')
|
||||
};
|
||||
if !current.is_empty() && current_w + space_w + word_w > max_w {
|
||||
lines.push(std::mem::take(&mut current));
|
||||
current_w = 0.0;
|
||||
}
|
||||
if !current.is_empty() {
|
||||
current.push(' ');
|
||||
current_w += space_w;
|
||||
}
|
||||
current.push_str(word);
|
||||
current_w += word_w;
|
||||
}
|
||||
if !current.is_empty() {
|
||||
lines.push(current);
|
||||
}
|
||||
lines
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
use acadrust::{CadDocument, EntityType};
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::scene::acad_to_truck::TruckEntity;
|
||||
use crate::scene::object::{GripApply, GripDef, PropSection};
|
||||
|
||||
pub trait TruckConvertible {
|
||||
fn to_truck(&self, document: &CadDocument) -> Option<TruckEntity>;
|
||||
}
|
||||
|
||||
pub trait Grippable {
|
||||
fn grips(&self) -> Vec<GripDef>;
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply);
|
||||
}
|
||||
|
||||
pub trait PropertyEditable {
|
||||
fn geometry_properties(&self, text_style_names: &[String]) -> PropSection;
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str);
|
||||
}
|
||||
|
||||
pub trait Transformable {
|
||||
fn apply_transform(&mut self, t: &EntityTransform);
|
||||
}
|
||||
|
||||
pub trait EntityTypeOps {
|
||||
fn to_truck_entity(&self, document: &CadDocument) -> Option<TruckEntity>;
|
||||
fn grips(&self) -> Vec<GripDef>;
|
||||
fn geometry_properties(&self, text_style_names: &[String]) -> Option<PropSection>;
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str);
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply);
|
||||
fn apply_transform(&mut self, t: &EntityTransform);
|
||||
}
|
||||
|
||||
impl EntityTypeOps for EntityType {
|
||||
fn to_truck_entity(&self, document: &CadDocument) -> Option<TruckEntity> {
|
||||
match self {
|
||||
EntityType::Point(pt) => TruckConvertible::to_truck(pt, document),
|
||||
EntityType::Line(line) => TruckConvertible::to_truck(line, document),
|
||||
EntityType::Circle(circle) => TruckConvertible::to_truck(circle, document),
|
||||
EntityType::Arc(arc) => TruckConvertible::to_truck(arc, document),
|
||||
EntityType::Ellipse(ellipse) => TruckConvertible::to_truck(ellipse, document),
|
||||
EntityType::Spline(spline) => TruckConvertible::to_truck(spline, document),
|
||||
EntityType::LwPolyline(pline) => TruckConvertible::to_truck(pline, document),
|
||||
EntityType::Text(text) => TruckConvertible::to_truck(text, document),
|
||||
EntityType::MText(text) => TruckConvertible::to_truck(text, document),
|
||||
EntityType::Leader(leader) => TruckConvertible::to_truck(leader, document),
|
||||
EntityType::MultiLeader(ml) => TruckConvertible::to_truck(ml, document),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
match self {
|
||||
EntityType::Line(line) => Grippable::grips(line),
|
||||
EntityType::Circle(circle) => Grippable::grips(circle),
|
||||
EntityType::Arc(arc) => Grippable::grips(arc),
|
||||
EntityType::Ellipse(ellipse) => Grippable::grips(ellipse),
|
||||
EntityType::LwPolyline(pline) => Grippable::grips(pline),
|
||||
EntityType::Point(pt) => Grippable::grips(pt),
|
||||
EntityType::Spline(spline) => Grippable::grips(spline),
|
||||
EntityType::Text(text) => Grippable::grips(text),
|
||||
EntityType::MText(text) => Grippable::grips(text),
|
||||
EntityType::Viewport(vp) => Grippable::grips(vp),
|
||||
EntityType::Insert(ins) => Grippable::grips(ins),
|
||||
EntityType::Leader(leader) => Grippable::grips(leader),
|
||||
EntityType::MultiLeader(ml) => Grippable::grips(ml),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn geometry_properties(&self, text_style_names: &[String]) -> Option<PropSection> {
|
||||
match self {
|
||||
EntityType::Line(line) => Some(PropertyEditable::geometry_properties(
|
||||
line,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::Circle(circle) => Some(PropertyEditable::geometry_properties(
|
||||
circle,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::Arc(arc) => {
|
||||
Some(PropertyEditable::geometry_properties(arc, text_style_names))
|
||||
}
|
||||
EntityType::Ellipse(ellipse) => Some(PropertyEditable::geometry_properties(
|
||||
ellipse,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::LwPolyline(pline) => Some(PropertyEditable::geometry_properties(
|
||||
pline,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::Hatch(hatch) => Some(PropertyEditable::geometry_properties(
|
||||
hatch,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::Point(pt) => {
|
||||
Some(PropertyEditable::geometry_properties(pt, text_style_names))
|
||||
}
|
||||
EntityType::Spline(spline) => Some(PropertyEditable::geometry_properties(
|
||||
spline,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::Text(text) => Some(PropertyEditable::geometry_properties(
|
||||
text,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::MText(text) => Some(PropertyEditable::geometry_properties(
|
||||
text,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::Viewport(vp) => {
|
||||
Some(PropertyEditable::geometry_properties(vp, text_style_names))
|
||||
}
|
||||
EntityType::Insert(ins) => {
|
||||
Some(PropertyEditable::geometry_properties(ins, text_style_names))
|
||||
}
|
||||
EntityType::Dimension(dim) => Some(PropertyEditable::geometry_properties(
|
||||
dim,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::Leader(leader) => Some(PropertyEditable::geometry_properties(
|
||||
leader,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::MultiLeader(ml) => Some(PropertyEditable::geometry_properties(
|
||||
ml,
|
||||
text_style_names,
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
match self {
|
||||
EntityType::Line(line) => PropertyEditable::apply_geom_prop(line, field, value),
|
||||
EntityType::Circle(circle) => PropertyEditable::apply_geom_prop(circle, field, value),
|
||||
EntityType::Arc(arc) => PropertyEditable::apply_geom_prop(arc, field, value),
|
||||
EntityType::Ellipse(ellipse) => {
|
||||
PropertyEditable::apply_geom_prop(ellipse, field, value)
|
||||
}
|
||||
EntityType::LwPolyline(pline) => PropertyEditable::apply_geom_prop(pline, field, value),
|
||||
EntityType::Hatch(hatch) => PropertyEditable::apply_geom_prop(hatch, field, value),
|
||||
EntityType::Point(pt) => PropertyEditable::apply_geom_prop(pt, field, value),
|
||||
EntityType::Spline(spline) => PropertyEditable::apply_geom_prop(spline, field, value),
|
||||
EntityType::Text(text) => PropertyEditable::apply_geom_prop(text, field, value),
|
||||
EntityType::MText(text) => PropertyEditable::apply_geom_prop(text, field, value),
|
||||
EntityType::Viewport(vp) => PropertyEditable::apply_geom_prop(vp, field, value),
|
||||
EntityType::Insert(ins) => PropertyEditable::apply_geom_prop(ins, field, value),
|
||||
EntityType::Dimension(dim) => PropertyEditable::apply_geom_prop(dim, field, value),
|
||||
EntityType::Leader(leader) => PropertyEditable::apply_geom_prop(leader, field, value),
|
||||
EntityType::MultiLeader(ml) => PropertyEditable::apply_geom_prop(ml, field, value),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
match self {
|
||||
EntityType::Line(line) => Grippable::apply_grip(line, grip_id, apply),
|
||||
EntityType::Circle(circle) => Grippable::apply_grip(circle, grip_id, apply),
|
||||
EntityType::Arc(arc) => Grippable::apply_grip(arc, grip_id, apply),
|
||||
EntityType::Ellipse(ellipse) => Grippable::apply_grip(ellipse, grip_id, apply),
|
||||
EntityType::LwPolyline(pline) => Grippable::apply_grip(pline, grip_id, apply),
|
||||
EntityType::Point(pt) => Grippable::apply_grip(pt, grip_id, apply),
|
||||
EntityType::Spline(spline) => Grippable::apply_grip(spline, grip_id, apply),
|
||||
EntityType::Text(text) => Grippable::apply_grip(text, grip_id, apply),
|
||||
EntityType::MText(text) => Grippable::apply_grip(text, grip_id, apply),
|
||||
EntityType::Viewport(vp) => Grippable::apply_grip(vp, grip_id, apply),
|
||||
EntityType::Insert(ins) => Grippable::apply_grip(ins, grip_id, apply),
|
||||
EntityType::Leader(leader) => Grippable::apply_grip(leader, grip_id, apply),
|
||||
EntityType::MultiLeader(ml) => Grippable::apply_grip(ml, grip_id, apply),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_transform(&mut self, t: &EntityTransform) {
|
||||
match self {
|
||||
EntityType::Arc(arc) => Transformable::apply_transform(arc, t),
|
||||
EntityType::Circle(circle) => Transformable::apply_transform(circle, t),
|
||||
EntityType::Ellipse(ellipse) => Transformable::apply_transform(ellipse, t),
|
||||
EntityType::Hatch(hatch) => Transformable::apply_transform(hatch, t),
|
||||
EntityType::Insert(ins) => Transformable::apply_transform(ins, t),
|
||||
EntityType::Line(line) => Transformable::apply_transform(line, t),
|
||||
EntityType::LwPolyline(pline) => Transformable::apply_transform(pline, t),
|
||||
EntityType::MText(text) => Transformable::apply_transform(text, t),
|
||||
EntityType::Point(pt) => Transformable::apply_transform(pt, t),
|
||||
EntityType::Spline(spline) => Transformable::apply_transform(spline, t),
|
||||
EntityType::Text(text) => Transformable::apply_transform(text, t),
|
||||
EntityType::Viewport(vp) => Transformable::apply_transform(vp, t),
|
||||
EntityType::Dimension(dim) => Transformable::apply_transform(dim, t),
|
||||
EntityType::Leader(leader) => Transformable::apply_transform(leader, t),
|
||||
EntityType::MultiLeader(ml) => Transformable::apply_transform(ml, t),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
use acadrust::entities::Viewport;
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{diamond_grip, edit_prop as edit, parse_f64, square_grip};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable};
|
||||
use crate::scene::object::{GripApply, GripDef, PropSection};
|
||||
|
||||
fn grips(vp: &Viewport) -> Vec<GripDef> {
|
||||
let cx = vp.center.x as f32;
|
||||
let cy = vp.center.y as f32;
|
||||
let cz = vp.center.z as f32;
|
||||
let hw = (vp.width / 2.0) as f32;
|
||||
let hh = (vp.height / 2.0) as f32;
|
||||
vec![
|
||||
diamond_grip(0, Vec3::new(cx, cy, cz)),
|
||||
square_grip(1, Vec3::new(cx + hw, cy + hh, cz)),
|
||||
square_grip(2, Vec3::new(cx - hw, cy + hh, cz)),
|
||||
square_grip(3, Vec3::new(cx - hw, cy - hh, cz)),
|
||||
square_grip(4, Vec3::new(cx + hw, cy - hh, cz)),
|
||||
]
|
||||
}
|
||||
|
||||
fn properties(vp: &Viewport) -> PropSection {
|
||||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props: vec![
|
||||
edit("Center X", "center_x", vp.center.x),
|
||||
edit("Center Y", "center_y", vp.center.y),
|
||||
edit("Center Z", "center_z", vp.center.z),
|
||||
edit("Width", "vp_w", vp.width),
|
||||
edit("Height", "vp_h", vp.height),
|
||||
edit("Scale", "vscale", vp.custom_scale),
|
||||
edit("Target X", "vtgt_x", vp.view_target.x),
|
||||
edit("Target Z", "vtgt_z", vp.view_target.z),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_geom_prop(vp: &mut Viewport, field: &str, value: &str) {
|
||||
let Some(v) = parse_f64(value) else {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"center_x" => vp.center.x = v,
|
||||
"center_y" => vp.center.y = v,
|
||||
"center_z" => vp.center.z = v,
|
||||
"vp_w" if v > 0.0 => vp.width = v,
|
||||
"vp_h" if v > 0.0 => vp.height = v,
|
||||
"vscale" if v > 0.0 => vp.custom_scale = v,
|
||||
"vtgt_x" => vp.view_target.x = v,
|
||||
"vtgt_z" => vp.view_target.z = v,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_grip(vp: &mut Viewport, grip_id: usize, apply: GripApply) {
|
||||
match (grip_id, apply) {
|
||||
(0, GripApply::Translate(d)) => {
|
||||
vp.center.x += d.x as f64;
|
||||
vp.center.y += d.y as f64;
|
||||
vp.center.z += d.z as f64;
|
||||
}
|
||||
(0, GripApply::Absolute(p)) => {
|
||||
vp.center.x = p.x as f64;
|
||||
vp.center.y = p.y as f64;
|
||||
vp.center.z = p.z as f64;
|
||||
}
|
||||
(1..=4, GripApply::Absolute(p)) => {
|
||||
let new_hw = (p.x as f64 - vp.center.x).abs();
|
||||
let new_hh = (p.y as f64 - vp.center.y).abs();
|
||||
if new_hw > 0.01 {
|
||||
vp.width = new_hw * 2.0;
|
||||
}
|
||||
if new_hh > 0.01 {
|
||||
vp.height = new_hh * 2.0;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_transform(vp: &mut Viewport, t: &EntityTransform) {
|
||||
crate::scene::transform::apply_standard_entity_transform(vp, t, |entity, p1, p2| {
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut entity.center.x,
|
||||
&mut entity.center.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
impl Grippable for Viewport {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
grips(self)
|
||||
}
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
apply_grip(self, grip_id, apply);
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyEditable for Viewport {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection {
|
||||
properties(self)
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
apply_geom_prop(self, field, value);
|
||||
}
|
||||
}
|
||||
|
||||
impl Transformable for Viewport {
|
||||
fn apply_transform(&mut self, t: &EntityTransform) {
|
||||
apply_transform(self, t);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user