feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices, TangentDirection, MoveWithText, StackText, UnstackText. Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same. Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion, weights kept in sync, knots cleared so to_truck rebuilds uniform vec). Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value). Leader: vertex Add/Remove (arrow + centroid stay Stretch-only). MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader / Move-Independent (menu live, structural edits stubbed). Text/MText: Rotate prompts for degrees and sets rotation. Spline, Leader, MultiLeader switched from impl_entity_basics! to manual trait impls so grip_menu can be overridden. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
559cd76369
commit
4b4fe5ca6d
9 changed files with 580 additions and 3 deletions
|
|
@ -329,6 +329,61 @@ impl Grippable for Hatch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn grip_menu(
|
||||||
|
&self,
|
||||||
|
_grip_id: usize,
|
||||||
|
) -> Vec<crate::scene::object::GripMenuItem> {
|
||||||
|
use crate::scene::object::{GripMenuAction, GripMenuItem};
|
||||||
|
vec![
|
||||||
|
GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch },
|
||||||
|
GripMenuItem { label: "Origin Point", action: GripMenuAction::OriginPoint },
|
||||||
|
GripMenuItem { label: "Hatch Angle", action: GripMenuAction::HatchAngle },
|
||||||
|
GripMenuItem { label: "Hatch Scale", action: GripMenuAction::HatchScale },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_grip_menu(
|
||||||
|
&mut self,
|
||||||
|
_grip_id: usize,
|
||||||
|
_action: crate::scene::object::GripMenuAction,
|
||||||
|
) {
|
||||||
|
// Origin / Angle / Scale need a follow-up value — handled by
|
||||||
|
// `apply_grip_menu_value`.
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grip_menu_value_prompt(
|
||||||
|
&self,
|
||||||
|
_grip_id: usize,
|
||||||
|
action: crate::scene::object::GripMenuAction,
|
||||||
|
) -> Option<&'static str> {
|
||||||
|
use crate::scene::object::GripMenuAction as A;
|
||||||
|
match action {
|
||||||
|
A::HatchAngle => Some("Angle (deg)"),
|
||||||
|
A::HatchScale => Some("Scale"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_grip_menu_value(
|
||||||
|
&mut self,
|
||||||
|
_grip_id: usize,
|
||||||
|
action: crate::scene::object::GripMenuAction,
|
||||||
|
value: f64,
|
||||||
|
) {
|
||||||
|
use crate::scene::object::GripMenuAction as A;
|
||||||
|
match action {
|
||||||
|
A::HatchAngle => {
|
||||||
|
self.pattern_angle = value.to_radians();
|
||||||
|
}
|
||||||
|
A::HatchScale => {
|
||||||
|
if value > 0.0 {
|
||||||
|
self.pattern_scale = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FallbackTess for Hatch {
|
impl FallbackTess for Hatch {
|
||||||
|
|
|
||||||
|
|
@ -455,7 +455,80 @@ impl TruckConvertible for Leader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
crate::impl_entity_basics!(Leader);
|
impl crate::entities::traits::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);
|
||||||
|
}
|
||||||
|
fn grip_menu(
|
||||||
|
&self,
|
||||||
|
grip_id: usize,
|
||||||
|
) -> Vec<crate::scene::object::GripMenuItem> {
|
||||||
|
use crate::scene::object::{GripMenuAction, GripMenuItem};
|
||||||
|
let n = self.vertices.len();
|
||||||
|
if grip_id == 0 {
|
||||||
|
// Arrow head — stretch only.
|
||||||
|
vec![GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch }]
|
||||||
|
} else if grip_id < n {
|
||||||
|
vec![
|
||||||
|
GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch },
|
||||||
|
GripMenuItem { label: "Add Vertex", action: GripMenuAction::AddVertex },
|
||||||
|
GripMenuItem { label: "Remove Vertex", action: GripMenuAction::RemoveVertex },
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
// Centroid grip — move whole leader.
|
||||||
|
vec![GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn apply_grip_menu(
|
||||||
|
&mut self,
|
||||||
|
grip_id: usize,
|
||||||
|
action: crate::scene::object::GripMenuAction,
|
||||||
|
) {
|
||||||
|
use crate::scene::object::GripMenuAction as A;
|
||||||
|
let n = self.vertices.len();
|
||||||
|
match action {
|
||||||
|
A::AddVertex if grip_id < n => {
|
||||||
|
let i1 = (grip_id + 1).min(n - 1);
|
||||||
|
if i1 == grip_id {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let v0 = &self.vertices[grip_id];
|
||||||
|
let v1 = &self.vertices[i1];
|
||||||
|
let mid = acadrust::types::Vector3::new(
|
||||||
|
(v0.x + v1.x) * 0.5,
|
||||||
|
(v0.y + v1.y) * 0.5,
|
||||||
|
(v0.z + v1.z) * 0.5,
|
||||||
|
);
|
||||||
|
self.vertices.insert(i1, mid);
|
||||||
|
}
|
||||||
|
A::RemoveVertex if grip_id < n && n > 2 => {
|
||||||
|
self.vertices.remove(grip_id);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::entities::traits::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 crate::entities::traits::Transformable for Leader {
|
||||||
|
fn apply_transform(&mut self, t: &EntityTransform) {
|
||||||
|
apply_transform(self, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Per-entity tessellation entry for `Leader`. Lives here so all leader
|
/// Per-entity tessellation entry for `Leader`. Lives here so all leader
|
||||||
/// tess code stays alongside the entity definition. Cross-entity dim
|
/// tess code stays alongside the entity definition. Cross-entity dim
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,46 @@ impl Grippable for MLine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn grip_menu(
|
||||||
|
&self,
|
||||||
|
_grip_id: usize,
|
||||||
|
) -> Vec<crate::scene::object::GripMenuItem> {
|
||||||
|
use crate::scene::object::{GripMenuAction, GripMenuItem};
|
||||||
|
vec![
|
||||||
|
GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch },
|
||||||
|
GripMenuItem { label: "Add Vertex", action: GripMenuAction::AddVertex },
|
||||||
|
GripMenuItem { label: "Remove Vertex", action: GripMenuAction::RemoveVertex },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_grip_menu(
|
||||||
|
&mut self,
|
||||||
|
grip_id: usize,
|
||||||
|
action: crate::scene::object::GripMenuAction,
|
||||||
|
) {
|
||||||
|
use crate::scene::object::GripMenuAction as A;
|
||||||
|
let n = self.vertices.len();
|
||||||
|
match action {
|
||||||
|
A::AddVertex if grip_id < n => {
|
||||||
|
let i1 = (grip_id + 1).min(n - 1);
|
||||||
|
if i1 == grip_id {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let v0 = &self.vertices[grip_id];
|
||||||
|
let v1 = &self.vertices[i1];
|
||||||
|
let mut new_v = v0.clone();
|
||||||
|
new_v.position.x = (v0.position.x + v1.position.x) * 0.5;
|
||||||
|
new_v.position.y = (v0.position.y + v1.position.y) * 0.5;
|
||||||
|
new_v.position.z = (v0.position.z + v1.position.z) * 0.5;
|
||||||
|
self.vertices.insert(i1, new_v);
|
||||||
|
}
|
||||||
|
A::RemoveVertex if grip_id < n && n > 2 => {
|
||||||
|
self.vertices.remove(grip_id);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropertyEditable for MLine {
|
impl PropertyEditable for MLine {
|
||||||
|
|
|
||||||
|
|
@ -300,6 +300,57 @@ impl Grippable for MText {
|
||||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||||
apply_grip(self, grip_id, apply);
|
apply_grip(self, grip_id, apply);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn grip_menu(
|
||||||
|
&self,
|
||||||
|
grip_id: usize,
|
||||||
|
) -> Vec<crate::scene::object::GripMenuItem> {
|
||||||
|
use crate::scene::object::{GripMenuAction, GripMenuItem};
|
||||||
|
if grip_id == 0 {
|
||||||
|
// Insertion point
|
||||||
|
vec![
|
||||||
|
GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch },
|
||||||
|
GripMenuItem { label: "Move with Text", action: GripMenuAction::MoveWithText },
|
||||||
|
GripMenuItem { label: "Rotate", action: GripMenuAction::RotateText },
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
// Width grip
|
||||||
|
vec![GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_grip_menu(
|
||||||
|
&mut self,
|
||||||
|
_grip_id: usize,
|
||||||
|
_action: crate::scene::object::GripMenuAction,
|
||||||
|
) {
|
||||||
|
// Rotate needs a follow-up angle handled by
|
||||||
|
// `apply_grip_menu_value`; Move-with-Text is the default drag.
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grip_menu_value_prompt(
|
||||||
|
&self,
|
||||||
|
_grip_id: usize,
|
||||||
|
action: crate::scene::object::GripMenuAction,
|
||||||
|
) -> Option<&'static str> {
|
||||||
|
use crate::scene::object::GripMenuAction as A;
|
||||||
|
match action {
|
||||||
|
A::RotateText => Some("Rotation (deg)"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_grip_menu_value(
|
||||||
|
&mut self,
|
||||||
|
_grip_id: usize,
|
||||||
|
action: crate::scene::object::GripMenuAction,
|
||||||
|
value: f64,
|
||||||
|
) {
|
||||||
|
use crate::scene::object::GripMenuAction as A;
|
||||||
|
if matches!(action, A::RotateText) {
|
||||||
|
self.rotation = value.to_radians();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropertyEditable for MText {
|
impl PropertyEditable for MText {
|
||||||
|
|
|
||||||
|
|
@ -827,7 +827,71 @@ impl TruckConvertible for MultiLeader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
crate::impl_entity_basics!(MultiLeader);
|
impl crate::entities::traits::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);
|
||||||
|
}
|
||||||
|
fn grip_menu(
|
||||||
|
&self,
|
||||||
|
grip_id: usize,
|
||||||
|
) -> Vec<crate::scene::object::GripMenuItem> {
|
||||||
|
use crate::scene::object::{GripMenuAction, GripMenuItem};
|
||||||
|
// Compute vertex count to identify text-location grip (last id).
|
||||||
|
let n_vertices: usize = self
|
||||||
|
.context
|
||||||
|
.leader_roots
|
||||||
|
.iter()
|
||||||
|
.flat_map(|r| r.lines.iter())
|
||||||
|
.map(|l| l.points.len())
|
||||||
|
.sum();
|
||||||
|
let is_text_loc = self.content_type == LeaderContentType::MText
|
||||||
|
&& grip_id == n_vertices;
|
||||||
|
|
||||||
|
if is_text_loc {
|
||||||
|
vec![
|
||||||
|
GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch },
|
||||||
|
GripMenuItem { label: "Move with Leader", action: GripMenuAction::MoveWithLeader },
|
||||||
|
GripMenuItem { label: "Move Independent", action: GripMenuAction::MoveIndependent },
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
vec![
|
||||||
|
GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch },
|
||||||
|
GripMenuItem { label: "Add Leader", action: GripMenuAction::AddLeader },
|
||||||
|
GripMenuItem { label: "Remove Leader", action: GripMenuAction::RemoveLeader },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn apply_grip_menu(
|
||||||
|
&mut self,
|
||||||
|
_grip_id: usize,
|
||||||
|
_action: crate::scene::object::GripMenuAction,
|
||||||
|
) {
|
||||||
|
// Add/Remove Leader, Move with Leader, Move Independent — these
|
||||||
|
// require deeper structural edits across leader_roots/lines and
|
||||||
|
// are stubbed here; Stretch falls through to the regular drag.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::entities::traits::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 crate::entities::traits::Transformable for MultiLeader {
|
||||||
|
fn apply_transform(&mut self, t: &EntityTransform) {
|
||||||
|
apply_transform(self, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Per-entity tessellation entry for `MultiLeader`. Returns multiple
|
/// Per-entity tessellation entry for `MultiLeader`. Returns multiple
|
||||||
/// `WireModel`s (leader/dogleg + arrow fill, optional block content,
|
/// `WireModel`s (leader/dogleg + arrow fill, optional block content,
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,50 @@ impl Grippable for Polyline {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn grip_menu(
|
||||||
|
&self,
|
||||||
|
_grip_id: usize,
|
||||||
|
) -> Vec<crate::scene::object::GripMenuItem> {
|
||||||
|
use crate::scene::object::{GripMenuAction, GripMenuItem};
|
||||||
|
vec![
|
||||||
|
GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch },
|
||||||
|
GripMenuItem { label: "Add Vertex", action: GripMenuAction::AddVertex },
|
||||||
|
GripMenuItem { label: "Remove Vertex", action: GripMenuAction::RemoveVertex },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_grip_menu(
|
||||||
|
&mut self,
|
||||||
|
grip_id: usize,
|
||||||
|
action: crate::scene::object::GripMenuAction,
|
||||||
|
) {
|
||||||
|
use crate::scene::object::GripMenuAction as A;
|
||||||
|
let n = self.vertices.len();
|
||||||
|
match action {
|
||||||
|
A::AddVertex if grip_id < n => {
|
||||||
|
let i1 = (grip_id + 1) % n;
|
||||||
|
if i1 == 0 && grip_id + 1 == n {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let v0 = &self.vertices[grip_id];
|
||||||
|
let v1 = &self.vertices[i1];
|
||||||
|
let mx = (v0.location.x + v1.location.x) * 0.5;
|
||||||
|
let my = (v0.location.y + v1.location.y) * 0.5;
|
||||||
|
let mz = (v0.location.z + v1.location.z) * 0.5;
|
||||||
|
let mut new_v = v0.clone();
|
||||||
|
new_v.location.x = mx;
|
||||||
|
new_v.location.y = my;
|
||||||
|
new_v.location.z = mz;
|
||||||
|
let insert_at = (grip_id + 1).min(self.vertices.len());
|
||||||
|
self.vertices.insert(insert_at, new_v);
|
||||||
|
}
|
||||||
|
A::RemoveVertex if grip_id < n && n > 2 => {
|
||||||
|
self.vertices.remove(grip_id);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropertyEditable for Polyline {
|
impl PropertyEditable for Polyline {
|
||||||
|
|
@ -300,6 +344,50 @@ impl Grippable for Polyline2D {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn grip_menu(
|
||||||
|
&self,
|
||||||
|
_grip_id: usize,
|
||||||
|
) -> Vec<crate::scene::object::GripMenuItem> {
|
||||||
|
use crate::scene::object::{GripMenuAction, GripMenuItem};
|
||||||
|
vec![
|
||||||
|
GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch },
|
||||||
|
GripMenuItem { label: "Add Vertex", action: GripMenuAction::AddVertex },
|
||||||
|
GripMenuItem { label: "Remove Vertex", action: GripMenuAction::RemoveVertex },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_grip_menu(
|
||||||
|
&mut self,
|
||||||
|
grip_id: usize,
|
||||||
|
action: crate::scene::object::GripMenuAction,
|
||||||
|
) {
|
||||||
|
use crate::scene::object::GripMenuAction as A;
|
||||||
|
let n = self.vertices.len();
|
||||||
|
let elev = self.elevation;
|
||||||
|
match action {
|
||||||
|
A::AddVertex if grip_id < n => {
|
||||||
|
let i1 = (grip_id + 1) % n;
|
||||||
|
if i1 == 0 && grip_id + 1 == n {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let v0 = &self.vertices[grip_id];
|
||||||
|
let v1 = &self.vertices[i1];
|
||||||
|
let mx = (v0.location.x + v1.location.x) * 0.5;
|
||||||
|
let my = (v0.location.y + v1.location.y) * 0.5;
|
||||||
|
let mut new_v = v0.clone();
|
||||||
|
new_v.location.x = mx;
|
||||||
|
new_v.location.y = my;
|
||||||
|
new_v.location.z = elev;
|
||||||
|
let insert_at = (grip_id + 1).min(self.vertices.len());
|
||||||
|
self.vertices.insert(insert_at, new_v);
|
||||||
|
}
|
||||||
|
A::RemoveVertex if grip_id < n && n > 2 => {
|
||||||
|
self.vertices.remove(grip_id);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropertyEditable for Polyline2D {
|
impl PropertyEditable for Polyline2D {
|
||||||
|
|
@ -473,6 +561,50 @@ impl Grippable for Polyline3D {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn grip_menu(
|
||||||
|
&self,
|
||||||
|
_grip_id: usize,
|
||||||
|
) -> Vec<crate::scene::object::GripMenuItem> {
|
||||||
|
use crate::scene::object::{GripMenuAction, GripMenuItem};
|
||||||
|
vec![
|
||||||
|
GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch },
|
||||||
|
GripMenuItem { label: "Add Vertex", action: GripMenuAction::AddVertex },
|
||||||
|
GripMenuItem { label: "Remove Vertex", action: GripMenuAction::RemoveVertex },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_grip_menu(
|
||||||
|
&mut self,
|
||||||
|
grip_id: usize,
|
||||||
|
action: crate::scene::object::GripMenuAction,
|
||||||
|
) {
|
||||||
|
use crate::scene::object::GripMenuAction as A;
|
||||||
|
let n = self.vertices.len();
|
||||||
|
match action {
|
||||||
|
A::AddVertex if grip_id < n => {
|
||||||
|
let i1 = (grip_id + 1) % n;
|
||||||
|
if i1 == 0 && grip_id + 1 == n {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let v0 = &self.vertices[grip_id];
|
||||||
|
let v1 = &self.vertices[i1];
|
||||||
|
let mx = (v0.position.x + v1.position.x) * 0.5;
|
||||||
|
let my = (v0.position.y + v1.position.y) * 0.5;
|
||||||
|
let mz = (v0.position.z + v1.position.z) * 0.5;
|
||||||
|
let mut new_v = v0.clone();
|
||||||
|
new_v.position.x = mx;
|
||||||
|
new_v.position.y = my;
|
||||||
|
new_v.position.z = mz;
|
||||||
|
let insert_at = (grip_id + 1).min(self.vertices.len());
|
||||||
|
self.vertices.insert(insert_at, new_v);
|
||||||
|
}
|
||||||
|
A::RemoveVertex if grip_id < n && n > 2 => {
|
||||||
|
self.vertices.remove(grip_id);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropertyEditable for Polyline3D {
|
impl PropertyEditable for Polyline3D {
|
||||||
|
|
|
||||||
|
|
@ -161,5 +161,114 @@ impl TruckConvertible for Spline {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
crate::impl_entity_basics!(Spline);
|
impl crate::entities::traits::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);
|
||||||
|
}
|
||||||
|
fn grip_menu(
|
||||||
|
&self,
|
||||||
|
_grip_id: usize,
|
||||||
|
) -> Vec<crate::scene::object::GripMenuItem> {
|
||||||
|
use crate::scene::object::{GripMenuAction, GripMenuItem};
|
||||||
|
vec![
|
||||||
|
GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch },
|
||||||
|
GripMenuItem { label: "Add Vertex", action: GripMenuAction::AddVertex },
|
||||||
|
GripMenuItem { label: "Remove Vertex", action: GripMenuAction::RemoveVertex },
|
||||||
|
GripMenuItem { label: "Refine Vertices", action: GripMenuAction::RefineVertices },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
fn apply_grip_menu(
|
||||||
|
&mut self,
|
||||||
|
grip_id: usize,
|
||||||
|
action: crate::scene::object::GripMenuAction,
|
||||||
|
) {
|
||||||
|
use crate::scene::object::GripMenuAction as A;
|
||||||
|
let n = self.control_points.len();
|
||||||
|
let min_cv = (self.degree as usize).saturating_add(1).max(2);
|
||||||
|
match action {
|
||||||
|
A::AddVertex if grip_id < n => {
|
||||||
|
let i1 = (grip_id + 1).min(n - 1);
|
||||||
|
if i1 == grip_id {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let p0 = &self.control_points[grip_id];
|
||||||
|
let p1 = &self.control_points[i1];
|
||||||
|
let mid = acadrust::types::Vector3::new(
|
||||||
|
(p0.x + p1.x) * 0.5,
|
||||||
|
(p0.y + p1.y) * 0.5,
|
||||||
|
(p0.z + p1.z) * 0.5,
|
||||||
|
);
|
||||||
|
self.control_points.insert(i1, mid);
|
||||||
|
if !self.weights.is_empty() && self.weights.len() == n {
|
||||||
|
let w = (self.weights[grip_id] + self.weights[i1.min(self.weights.len() - 1)]) * 0.5;
|
||||||
|
self.weights.insert(i1, w);
|
||||||
|
}
|
||||||
|
// Clear knots so to_truck rebuilds a uniform knot vector
|
||||||
|
// for the new CV count.
|
||||||
|
self.knots.clear();
|
||||||
|
}
|
||||||
|
A::RemoveVertex if grip_id < n && n > min_cv => {
|
||||||
|
self.control_points.remove(grip_id);
|
||||||
|
if grip_id < self.weights.len() {
|
||||||
|
self.weights.remove(grip_id);
|
||||||
|
}
|
||||||
|
self.knots.clear();
|
||||||
|
}
|
||||||
|
A::RefineVertices => {
|
||||||
|
// Insert a CV between every adjacent pair (chord midpoints)
|
||||||
|
// and rebuild a uniform knot vector.
|
||||||
|
if n >= 2 {
|
||||||
|
let mut refined = Vec::with_capacity(n * 2 - 1);
|
||||||
|
let mut refined_w = Vec::with_capacity(n * 2 - 1);
|
||||||
|
let has_w = !self.weights.is_empty() && self.weights.len() == n;
|
||||||
|
for i in 0..n {
|
||||||
|
refined.push(self.control_points[i].clone());
|
||||||
|
if has_w {
|
||||||
|
refined_w.push(self.weights[i]);
|
||||||
|
}
|
||||||
|
if i + 1 < n {
|
||||||
|
let a = &self.control_points[i];
|
||||||
|
let b = &self.control_points[i + 1];
|
||||||
|
refined.push(acadrust::types::Vector3::new(
|
||||||
|
(a.x + b.x) * 0.5,
|
||||||
|
(a.y + b.y) * 0.5,
|
||||||
|
(a.z + b.z) * 0.5,
|
||||||
|
));
|
||||||
|
if has_w {
|
||||||
|
refined_w.push((self.weights[i] + self.weights[i + 1]) * 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.control_points = refined;
|
||||||
|
if has_w {
|
||||||
|
self.weights = refined_w;
|
||||||
|
}
|
||||||
|
self.knots.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::entities::traits::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 crate::entities::traits::Transformable for Spline {
|
||||||
|
fn apply_transform(&mut self, t: &EntityTransform) {
|
||||||
|
apply_transform(self, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -315,6 +315,52 @@ impl Grippable for Text {
|
||||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||||
apply_grip(self, grip_id, apply);
|
apply_grip(self, grip_id, apply);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn grip_menu(
|
||||||
|
&self,
|
||||||
|
_grip_id: usize,
|
||||||
|
) -> Vec<crate::scene::object::GripMenuItem> {
|
||||||
|
use crate::scene::object::{GripMenuAction, GripMenuItem};
|
||||||
|
vec![
|
||||||
|
GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch },
|
||||||
|
GripMenuItem { label: "Move with Text", action: GripMenuAction::MoveWithText },
|
||||||
|
GripMenuItem { label: "Rotate", action: GripMenuAction::RotateText },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_grip_menu(
|
||||||
|
&mut self,
|
||||||
|
_grip_id: usize,
|
||||||
|
_action: crate::scene::object::GripMenuAction,
|
||||||
|
) {
|
||||||
|
// Move-with-Text falls through to Stretch (single grip moves
|
||||||
|
// the whole text); Rotate needs a follow-up angle handled by
|
||||||
|
// `apply_grip_menu_value`.
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grip_menu_value_prompt(
|
||||||
|
&self,
|
||||||
|
_grip_id: usize,
|
||||||
|
action: crate::scene::object::GripMenuAction,
|
||||||
|
) -> Option<&'static str> {
|
||||||
|
use crate::scene::object::GripMenuAction as A;
|
||||||
|
match action {
|
||||||
|
A::RotateText => Some("Rotation (deg)"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_grip_menu_value(
|
||||||
|
&mut self,
|
||||||
|
_grip_id: usize,
|
||||||
|
action: crate::scene::object::GripMenuAction,
|
||||||
|
value: f64,
|
||||||
|
) {
|
||||||
|
use crate::scene::object::GripMenuAction as A;
|
||||||
|
if matches!(action, A::RotateText) {
|
||||||
|
self.rotation = value.to_radians();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropertyEditable for Text {
|
impl PropertyEditable for Text {
|
||||||
|
|
|
||||||
|
|
@ -137,4 +137,11 @@ pub enum GripMenuAction {
|
||||||
HatchScale,
|
HatchScale,
|
||||||
HatchPattern,
|
HatchPattern,
|
||||||
TangentDirection,
|
TangentDirection,
|
||||||
|
AddFitPoint,
|
||||||
|
RemoveFitPoint,
|
||||||
|
Refit,
|
||||||
|
RefineVertices,
|
||||||
|
MoveWithText,
|
||||||
|
StackText,
|
||||||
|
UnstackText,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue