Files
OneCAD-Draft/src/modules/annotate/mleader_edit.rs
T
Hakan SevenandClaude Opus 5.5 f537aff75c feat: node graph overlay; move to opencadcodec/opencadkernel/opencadgraph
The codec and kernel repositories and crates are renamed opencadcodec and
opencadkernel. Inside OCS they are depended on under short keys, so code
reads `codec::`, `kernel::` and the new `graph::` (opencadgraph, the node
graph engine). File-format and protocol spellings stay as they were: the
ACIS product id `acadrust`, the ACADRUST_* switches and the plugin
version keys `acadrust_version` / `acadrust_source`.

A button left of MCP on the command line opens a translucent node graph
over the viewport, with a palette of object, input, math, logic, list,
text and geometry nodes added by click or drag. An object node owns
drawing entities and its rows are the Properties panel's own rows for
them: the single-object page is extracted from refresh_properties as
entity_property_sections, values use the control protocol's
property_json, and writes go through set_property_value with a property
target override, so the panel, MCP and the graph share one read/write
path. A list reaching an object row makes one entity per element. Each
graph action is one undo step; the canvas pans and zooms.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-25 07:34:23 +03:00

548 lines
18 KiB
Rust

// MLEADERADD / MLEADERREMOVE / MLEADERALIGN / MLEADERCOLLECT commands.
//
// MLEADERADD: pick a multileader → pick new arrowhead point → adds a leader line
// MLEADERREMOVE: pick a multileader → pick a leader line to remove
// MLEADERALIGN: select multileaders → pick base alignment direction
// MLEADERCOLLECT: select block-content multileaders → pick collection point
use codec::entities::{LeaderLine, MultiLeader};
use codec::types::Vector3;
use codec::{EntityType, Handle};
use kernel::geom2d::{closest_point, Curve, Line};
use glam::{DVec3, Vec3};
use crate::command::{CadCommand, CmdResult};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
use crate::t;
// ── MLEADERADD ────────────────────────────────────────────────────────────────
pub const ICON_ADD: IconKind =
IconKind::Svg(include_bytes!("../../../assets/icons/mleader_add.svg"));
pub fn tool_add() -> ToolDef {
ToolDef {
id: "MLEADERADD",
label: "Add Leader",
icon: ICON_ADD,
event: ModuleEvent::Command("MLEADERADD".to_string()),
}
}
enum AddStep {
PickMLeader,
PickArrowhead {
handle: Handle,
entity: Option<EntityType>,
},
CollectPoints {
handle: Handle,
entity: EntityType,
pts: Vec<DVec3>,
},
}
pub struct MLeaderAddCommand {
step: AddStep,
}
impl MLeaderAddCommand {
pub fn new() -> Self {
Self {
step: AddStep::PickMLeader,
}
}
}
impl CadCommand for MLeaderAddCommand {
fn name(&self) -> &'static str {
"MLEADERADD"
}
fn prompt(&self) -> String {
match &self.step {
AddStep::PickMLeader => t!("MLEADERADD Select a multileader:").into_owned(),
AddStep::PickArrowhead { .. } => {
t!("MLEADERADD Specify arrowhead location:").into_owned()
}
AddStep::CollectPoints { pts, .. } => t!(
"MLEADERADD Specify next leader point (%{count} pts, Enter to finish):",
count = pts.len()
)
.into_owned(),
}
}
fn needs_entity_pick(&self) -> bool {
matches!(self.step, AddStep::PickMLeader)
}
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
if handle.is_null() {
return CmdResult::NeedPoint;
}
self.step = AddStep::PickArrowhead {
handle,
entity: None,
};
CmdResult::NeedPoint
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
match &mut self.step {
AddStep::PickArrowhead { handle, entity } => {
if let Some(ent) = entity.take() {
if !matches!(ent, EntityType::MultiLeader(_)) {
return CmdResult::Cancel;
}
let h = *handle;
self.step = AddStep::CollectPoints {
handle: h,
entity: ent,
pts: vec![pt],
};
return CmdResult::NeedPoint;
}
CmdResult::NeedPoint
}
AddStep::CollectPoints { pts, .. } => {
pts.push(pt);
CmdResult::NeedPoint
}
_ => CmdResult::NeedPoint,
}
}
fn on_enter(&mut self) -> CmdResult {
if let AddStep::CollectPoints {
handle,
entity,
pts,
} = &mut self.step
{
if pts.len() < 1 {
return CmdResult::Cancel;
}
let h = *handle;
if let EntityType::MultiLeader(ref mut ml) = entity {
let points: Vec<Vector3> = pts
.iter()
.map(|p| Vector3::new(p.x, p.y, p.z))
.collect();
let template_root = ml.context.leader_roots.first().cloned();
let template_line = template_root
.as_ref()
.and_then(|root| root.lines.first())
.cloned();
let path_type = ml.path_type;
let line_color = ml.line_color;
let line_type_handle = ml.line_type_handle;
let line_weight = ml.line_weight;
let arrowhead_handle = ml.arrowhead_handle;
let arrowhead_size = ml.arrowhead_size;
let root = ml.context.add_leader_root();
if let Some(template) = template_root {
root.connection_point = template.connection_point;
root.direction = template.direction;
root.landing_distance = template.landing_distance;
root.text_attachment_direction = template.text_attachment_direction;
}
let line = root.create_line(points);
if let Some(template) = template_line {
line.path_type = template.path_type;
line.line_color = template.line_color;
line.line_type_handle = template.line_type_handle;
line.line_weight = template.line_weight;
line.arrowhead_handle = template.arrowhead_handle;
line.arrowhead_size = template.arrowhead_size;
line.override_flags = template.override_flags;
} else {
line.path_type = path_type;
line.line_color = line_color;
line.line_type_handle = line_type_handle;
line.line_weight = line_weight;
line.arrowhead_handle = arrowhead_handle;
line.arrowhead_size = arrowhead_size;
}
}
let updated = std::mem::replace(entity, EntityType::XLine(Default::default()));
return CmdResult::ReplaceEntity(h, vec![updated]);
}
CmdResult::Cancel
}
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
let existing_pts = match &self.step {
AddStep::CollectPoints { pts, .. } => pts.clone(),
_ => return None,
};
let mut all_pts = existing_pts;
all_pts.push(pt);
Some(preview_wire(
&all_pts.iter().map(|p| p.as_vec3()).collect::<Vec<_>>(),
))
}
fn inject_picked_entity(&mut self, entity: EntityType) {
if let AddStep::PickArrowhead { entity: slot, .. } = &mut self.step {
*slot = Some(entity);
}
}
}
// ── MLEADERREMOVE ─────────────────────────────────────────────────────────────
pub const ICON_REMOVE: IconKind =
IconKind::Svg(include_bytes!("../../../assets/icons/mleader_remove.svg"));
pub fn tool_remove() -> ToolDef {
ToolDef {
id: "MLEADERREMOVE",
label: "Remove Leader",
icon: ICON_REMOVE,
event: ModuleEvent::Command("MLEADERREMOVE".to_string()),
}
}
enum RemoveStep {
PickMLeader,
PickLeaderToRemove {
handle: Handle,
entity: Option<EntityType>,
},
}
pub struct MLeaderRemoveCommand {
step: RemoveStep,
}
impl MLeaderRemoveCommand {
pub fn new() -> Self {
Self {
step: RemoveStep::PickMLeader,
}
}
}
impl CadCommand for MLeaderRemoveCommand {
fn name(&self) -> &'static str {
"MLEADERREMOVE"
}
fn prompt(&self) -> String {
match &self.step {
RemoveStep::PickMLeader => t!("MLEADERREMOVE Select a multileader:").into_owned(),
RemoveStep::PickLeaderToRemove { .. } => {
t!("MLEADERREMOVE Click near the leader line to remove:").into_owned()
}
}
}
fn needs_entity_pick(&self) -> bool {
matches!(self.step, RemoveStep::PickMLeader)
}
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
if handle.is_null() {
return CmdResult::NeedPoint;
}
self.step = RemoveStep::PickLeaderToRemove {
handle,
entity: None,
};
CmdResult::NeedPoint
}
fn on_point(&mut self, pt: DVec3) -> CmdResult { let pt = pt.as_vec3();
if let RemoveStep::PickLeaderToRemove { handle, entity } = &mut self.step {
if let Some(mut ent) = entity.take() {
let h = *handle;
if let EntityType::MultiLeader(ref mut ml) = ent {
let pick = Vector3::new(pt.x as f64, pt.y as f64, pt.z as f64);
let best = ml
.context
.leader_roots
.iter()
.enumerate()
.flat_map(|(root_index, root)| {
root.lines.iter().enumerate().map(move |(line_index, line)| {
let mut distance = line
.points
.windows(2)
.map(|segment| point_segment_distance_xy(pick, segment[0], segment[1]))
.fold(f64::INFINITY, f64::min);
if let Some(last) = line.points.last().copied() {
distance = distance.min(point_segment_distance_xy(
pick,
last,
root.connection_point,
));
}
(root_index, line_index, distance)
})
})
.min_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal));
let Some((root_index, line_index, _)) = best else {
return CmdResult::Cancel;
};
if ml.total_leader_line_count() <= 1 {
return CmdResult::Cancel;
}
let root = &mut ml.context.leader_roots[root_index];
root.lines.remove(line_index);
if root.lines.is_empty() {
ml.context.leader_roots.remove(root_index);
}
} else {
return CmdResult::Cancel;
}
return CmdResult::ReplaceEntity(h, vec![ent]);
}
}
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_mouse_move(&mut self, _pt: DVec3) -> Option<WireModel> {
None
}
fn inject_picked_entity(&mut self, entity: EntityType) {
if let RemoveStep::PickLeaderToRemove { entity: slot, .. } = &mut self.step {
*slot = Some(entity);
}
}
}
// ── MLEADERALIGN ─────────────────────────────────────────────────────────────
pub const ICON_ALIGN: IconKind =
IconKind::Svg(include_bytes!("../../../assets/icons/mleader_align.svg"));
pub fn tool_align() -> ToolDef {
ToolDef {
id: "MLEADERALIGN",
label: "Align Leaders",
icon: ICON_ALIGN,
event: ModuleEvent::Command("MLEADERALIGN".to_string()),
}
}
enum AlignStep {
Gathering,
PickDirection { handles: Vec<Handle> },
PickEndDir { handles: Vec<Handle>, from: DVec3 },
}
pub struct MLeaderAlignCommand {
step: AlignStep,
}
impl MLeaderAlignCommand {
pub fn new() -> Self {
Self {
step: AlignStep::Gathering,
}
}
}
impl CadCommand for MLeaderAlignCommand {
fn name(&self) -> &'static str {
"MLEADERALIGN"
}
fn prompt(&self) -> String {
match &self.step {
AlignStep::Gathering => {
t!("MLEADERALIGN Select multileaders to align (Enter when done):").into_owned()
}
AlignStep::PickDirection { .. } => {
t!("MLEADERALIGN Specify direction — pick start point:").into_owned()
}
AlignStep::PickEndDir { .. } => {
t!("MLEADERALIGN Specify end point of alignment direction:").into_owned()
}
}
}
fn is_selection_gathering(&self) -> bool {
matches!(self.step, AlignStep::Gathering)
}
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
if handles.is_empty() {
return CmdResult::Cancel;
}
self.step = AlignStep::PickDirection { handles };
CmdResult::NeedPoint
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
match &mut self.step {
AlignStep::PickDirection { handles } => {
let h = handles.clone();
self.step = AlignStep::PickEndDir {
handles: h,
from: pt,
};
CmdResult::NeedPoint
}
AlignStep::PickEndDir { handles, from } => {
CmdResult::AlignMLeaders {
handles: handles.clone(),
from: *from,
to: pt,
}
}
_ => CmdResult::NeedPoint,
}
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_mouse_move(&mut self, _pt: DVec3) -> Option<WireModel> {
None
}
}
// ── MLEADERCOLLECT ────────────────────────────────────────────────────────────
pub const ICON_COLLECT: IconKind =
IconKind::Svg(include_bytes!("../../../assets/icons/mleader_collect.svg"));
pub fn tool_collect() -> ToolDef {
ToolDef {
id: "MLEADERCOLLECT",
label: "Collect Leaders",
icon: ICON_COLLECT,
event: ModuleEvent::Command("MLEADERCOLLECT".to_string()),
}
}
enum CollectStep {
Gathering,
PickLocation { handles: Vec<Handle> },
}
pub struct MLeaderCollectCommand {
step: CollectStep,
}
impl MLeaderCollectCommand {
pub fn new() -> Self {
Self {
step: CollectStep::Gathering,
}
}
}
impl CadCommand for MLeaderCollectCommand {
fn name(&self) -> &'static str {
"MLEADERCOLLECT"
}
fn prompt(&self) -> String {
match &self.step {
CollectStep::Gathering => {
t!("MLEADERCOLLECT Select multileaders to collect (Enter when done):").into_owned()
}
CollectStep::PickLocation { .. } => {
t!("MLEADERCOLLECT Specify collected multileader location:").into_owned()
}
}
}
fn is_selection_gathering(&self) -> bool {
matches!(self.step, CollectStep::Gathering)
}
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
if handles.is_empty() {
return CmdResult::Cancel;
}
self.step = CollectStep::PickLocation { handles };
CmdResult::NeedPoint
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
if let CollectStep::PickLocation { handles } = &self.step {
return CmdResult::CollectMLeaders {
handles: handles.clone(),
point: pt,
};
}
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_mouse_move(&mut self, _pt: DVec3) -> Option<WireModel> {
None
}
}
// ── Shared helpers ────────────────────────────────────────────────────────────
fn preview_wire(pts: &[Vec3]) -> WireModel {
WireModel {
bg_adapt: None,
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
dash_align_end: None,
text_verts: Vec::new(),
name: "mleader_edit_preview".into(),
points: pts.iter().map(|p| [p.x, p.y, p.z]).collect(),
points_low: Vec::new(),
color: WireModel::CYAN,
selected: false,
pattern_length: 0.0,
pattern: [0.0; 8],
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
aci: 0,
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
fill_tris: vec![],
fill_tris_low: Vec::new(),
}
}
fn point_segment_distance_xy(point: Vector3, start: Vector3, end: Vector3) -> f64 {
closest_point(
&Curve::Line(Line {
start: [start.x, start.y],
end: [end.x, end.y],
}),
[point.x, point.y],
)
.distance
}
// Silence unused-import warning for MultiLeader and LeaderLine if not used in all paths
fn _uses_ml_types(_ml: &MultiLeader, _ll: &LeaderLine) {}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["MLEADERADD"] }); // MLeaderAddCommand
inventory::submit!(crate::command::CommandRegistration { names: &["MLEADERALIGN"] }); // MLeaderAlignCommand
inventory::submit!(crate::command::CommandRegistration { names: &["MLEADERCOLLECT"] }); // MLeaderCollectCommand
inventory::submit!(crate::command::CommandRegistration { names: &["MLEADERREMOVE"] }); // MLeaderRemoveCommand