first commit

This commit is contained in:
Hakan Seven committed 2026-03-22 14:56:27 +03:00
1 parent d52c6440ff
commit d2fdac7f1e
274 files changed
+89330

No files matched your search

+188
View File
@@ -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);
}
}
+172
View File
@@ -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);
}
}
+50
View File
@@ -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()
}
+536
View File
@@ -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);
}
}
+244
View File
@@ -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);
}
}
+169
View File
@@ -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);
}
}
+116
View File
@@ -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);
}
}
+435
View File
@@ -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);
}
}
+149
View File
@@ -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);
}
}
+186
View File
@@ -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);
}
}
+18
View File
@@ -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;
+372
View File
@@ -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);
}
}
+416
View File
@@ -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);
}
}
+111
View File
@@ -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);
}
}
+134
View File
@@ -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);
}
}
+301
View File
@@ -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);
}
}
+192
View File
@@ -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
}
+195
View File
@@ -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),
_ => {}
}
}
}
+118
View File
@@ -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);
}
}