Merge pull request #850 from ramox81/codex/associative-center-mark

This commit is contained in:
Hakan Seven committed 2026-08-21 20:19:54 +03:00
commit b648000e78
21 files changed
+1672 -241

No files matched your search

+61 -211
View File
@@ -1,218 +1,50 @@
// Centre line tool — interactive command.
//
// Command: CENTERLINE — pick two straight Line entities and draw a single
// centre line between them, committed as one Line entity.
//
// * If the two picked lines are (near) parallel, the result is the midline:
// a line running between the two, centred on the average of their
// midpoints, oriented along the averaged direction, with a length equal to
// the longer of the two source lines.
// * If the two picked lines cross, the result is the bisector of the acute
// angle at their intersection point, drawn symmetrically either side of
// that point.
//
// The command picks the first line on the first click, stores it, prompts for
// the second line, then computes the centre line and ends.
//! Associative centre-line construction from lines and linear polyline segments.
use acadrust::entities::{CenterLineAssociation, CenterLineSource};
use acadrust::types::Vector3;
use acadrust::{EntityType, Handle, Line};
use acadrust::{EntityType, Handle};
use glam::DVec3;
use crate::t;
use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::centerline::{construct_line, picked_source, CenterLineSettings};
use crate::t;
// ── Ribbon definition ─────────────────────────────────────────────────────
#[allow(dead_code)] // ribbon definition ready for wiring; command works via the command line
#[allow(dead_code)]
pub fn tool() -> ToolDef {
ToolDef {
id: "CENTERLINE",
label: "Center Line",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/line.svg")),
event: ModuleEvent::Command("CENTERLINE".to_string()),
event: ModuleEvent::Command("CENTERLINE".to_owned()),
}
}
// ── Geometry helpers ──────────────────────────────────────────────────────
/// A line reduced to the quantities the centre-line maths needs.
#[derive(Clone, Copy)]
struct LineGeom {
#[derive(Clone)]
struct PickedSource {
source: CenterLineSource,
start: DVec3,
end: DVec3,
}
impl LineGeom {
fn from_line(line: &Line) -> Self {
Self {
start: DVec3::new(line.start.x, line.start.y, line.start.z),
end: DVec3::new(line.end.x, line.end.y, line.end.z),
}
}
fn midpoint(&self) -> DVec3 {
(self.start + self.end) * 0.5
}
fn length(&self) -> f64 {
(self.end - self.start).length()
}
/// Normalized direction in 3D, or `None` if the line is degenerate.
fn dir(&self) -> Option<DVec3> {
let d = self.end - self.start;
let len = d.length();
if len <= 1e-9 {
None
} else {
Some(d / len)
}
}
fn vector(point: DVec3) -> Vector3 {
Vector3::new(point.x, point.y, point.z)
}
/// 2D cross product of the XY components of two vectors.
fn cross_xy(a: DVec3, b: DVec3) -> f64 {
a.x * b.y - a.y * b.x
}
fn to_v3(p: DVec3) -> Vector3 {
Vector3::new(p.x, p.y, p.z)
}
/// Compute the centre line between two source lines. `None` when the result
/// would be degenerate (zero-length sources, or coincident parallel lines that
/// give no usable direction).
fn compute_center_line(a: LineGeom, b: LineGeom) -> Option<Line> {
let da = a.dir()?;
let db = b.dir()?;
// Average Z of the four endpoints keeps the result on a sensible plane even
// when the picked lines sit at slightly different elevations.
let avg_z = (a.start.z + a.end.z + b.start.z + b.end.z) * 0.25;
let parallel = cross_xy(da, db).abs() <= 1e-9;
if parallel {
// Midline: average direction (flip db so the two add constructively),
// centred on the average of the two midpoints, length = longer source.
let db_aligned = if da.dot(db) < 0.0 { -db } else { db };
let avg = da + db_aligned;
let dir = if avg.length() <= 1e-9 {
da
} else {
avg / avg.length()
};
let dir = DVec3::new(dir.x, dir.y, 0.0);
let dir = if dir.length() <= 1e-9 {
return None;
} else {
dir / dir.length()
};
let mid = (a.midpoint() + b.midpoint()) * 0.5;
let half = a.length().max(b.length()) * 0.5;
if half <= 1e-9 {
return None;
}
let center = DVec3::new(mid.x, mid.y, avg_z);
let p0 = center - dir * half;
let p1 = center + dir * half;
Some(Line::from_points(to_v3(p0), to_v3(p1)))
} else {
// Intersecting: bisector of the acute angle at the intersection point.
// Solve the 2D line-line intersection in XY.
// a.start + t*da = b.start + s*db
let r = da; // direction of line a
let s = db; // direction of line b
let denom = cross_xy(r, s);
if denom.abs() <= 1e-12 {
return None;
}
let qp = b.start - a.start;
let t = cross_xy(qp, s) / denom;
let ix = a.start.x + r.x * t;
let iy = a.start.y + r.y * t;
let p = DVec3::new(ix, iy, avg_z);
// Orient both directions away from the intersection point, choosing the
// half of each line whose midpoint lies on the side we keep, so the two
// oriented directions bisect the acute angle between the lines.
let a_away = orient_away(p, a);
let b_away = orient_away(p, b);
// For an acute-angle bisector, flip b_away if the two oriented dirs open
// obtuse (their dot is negative).
let b_oriented = if a_away.dot(b_away) < 0.0 {
-b_away
} else {
b_away
};
let bis = a_away + b_oriented;
let bis = DVec3::new(bis.x, bis.y, 0.0);
if bis.length() <= 1e-9 {
return None;
}
let dir = bis / bis.length();
let half = ((a.length() + b.length()) * 0.5) * 0.5;
if half <= 1e-9 {
return None;
}
let p0 = p - dir * half;
let p1 = p + dir * half;
Some(Line::from_points(to_v3(p0), to_v3(p1)))
}
}
/// Unit direction from `p` toward the farther endpoint of `line` (the side that
/// extends away from the intersection point).
fn orient_away(p: DVec3, line: LineGeom) -> DVec3 {
let to_start = line.start - p;
let to_end = line.end - p;
let pick = if to_end.length() >= to_start.length() {
to_end
} else {
to_start
};
let len = pick.length();
if len <= 1e-9 {
// Fall back to the raw line direction if the pick degenerates.
line.dir().unwrap_or(DVec3::X)
} else {
pick / len
}
}
// ── Command implementation ────────────────────────────────────────────────
pub struct CenterLineCommand {
/// First picked Line's geometry, captured once the first pick lands.
first: Option<LineGeom>,
/// The entity injected by the host before `on_entity_pick` runs; `None`
/// until the host injects it.
first: Option<PickedSource>,
picked: Option<EntityType>,
plane: WorkingPlane,
settings: CenterLineSettings,
}
impl CenterLineCommand {
pub fn new() -> Self {
pub(crate) fn new(settings: CenterLineSettings) -> Self {
Self {
first: None,
picked: None,
plane: WorkingPlane::default(),
}
}
/// Extract a `LineGeom` from a picked entity; `None` for anything else.
fn as_line(&self, entity: &EntityType) -> Option<LineGeom> {
match entity {
EntityType::Line(line) => {
let mut geom = LineGeom::from_line(line);
geom.start = self.plane.to_local(geom.start);
geom.end = self.plane.to_local(geom.end);
Some(geom)
}
_ => None,
settings,
}
}
}
@@ -246,39 +78,53 @@ impl CadCommand for CenterLineCommand {
self.picked = Some(entity);
}
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult {
if handle.is_null() {
return CmdResult::NeedPoint;
}
// Resolve the just-picked entity; ignore the pick if it isn't a Line.
let picked = self.picked.take();
let geom = match picked.as_ref().and_then(|entity| self.as_line(entity)) {
Some(g) => g,
None => return CmdResult::NeedPoint,
let Some(entity) = self.picked.take() else {
return CmdResult::NeedPoint;
};
// Reject zero-length picks rather than corrupting the result.
if geom.length() <= 1e-9 {
let Some((source, start, end)) = picked_source(&entity, handle, point) else {
return CmdResult::NeedPoint;
};
let picked = PickedSource { source, start, end };
let Some(first) = self.first.take() else {
self.first = Some(picked);
return CmdResult::NeedPoint;
};
if first.source.handle == picked.source.handle
&& first.source.segment_index == picked.source.segment_index
{
self.first = Some(first);
return CmdResult::NeedPoint;
}
match self.first {
None => {
// First line captured; keep prompting for the second.
self.first = Some(geom);
CmdResult::NeedPoint
}
Some(first) => match compute_center_line(first, geom) {
Some(line) => CmdResult::CommitAndExit(
self.plane.place_entity(EntityType::Line(line)),
),
// Degenerate combination (coincident / no usable result):
// keep prompting for a usable second line.
None => CmdResult::NeedPoint,
},
}
let association = CenterLineAssociation {
first: first.source.clone(),
second: picked.source.clone(),
plane_origin: vector(self.plane.origin),
plane_x: vector(self.plane.x),
plane_y: vector(self.plane.y),
start_extension: self.settings.extension,
end_extension: self.settings.extension,
start_length_adjustment: 0.0,
end_length_adjustment: 0.0,
associated: true,
};
let Some(mut line) = construct_line(
(first.start, first.end),
(picked.start, picked.end),
&association,
) else {
self.first = Some(first);
return CmdResult::NeedPoint;
};
association.write(&mut line.common.extended_data);
CmdResult::CommitAndExit(EntityType::Line(line))
}
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
fn on_point(&mut self, _point: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
@@ -291,7 +137,11 @@ impl CadCommand for CenterLineCommand {
}
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration {
names: &["CENTERLINE"]
}); // CenterLineCommand
names: &[
"CENTERLINE",
"CENTERRESET",
"CENTERREASSOCIATE",
"CENTERDISASSOCIATE",
]
});
+120 -10
View File
@@ -1,29 +1,26 @@
// Center mark tool — interactive command.
//
// Command: DIMCENTER (aliases DCE, CENTERMARK) — pick a Circle or Arc and draw
// a small cross of two Line entities through its center. Each arm of the cross
// extends a distance of `radius * 0.2` from the center in the four cardinal
// directions, so the result is a horizontal line from (cx - m, cy) to
// (cx + m, cy) and a vertical line from (cx, cy - m) to (cx, cy + m), with
// `m = radius * 0.2`. Both lines are committed at once and the command ends.
// Legacy centre cross and associative centre mark commands.
use acadrust::types::Vector3;
use acadrust::entities::{CenterMarkAssociation, CenterMarkSource};
use acadrust::{EntityType, Handle, Line};
use glam::DVec3;
use crate::t;
use crate::command::{CadCommand, CmdResult};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::centerline::{
center_measure_is_relative, resolve_center_measure, CenterLineSettings,
};
// ── Ribbon definition ─────────────────────────────────────────────────────
#[allow(dead_code)] // ribbon definition ready for wiring; command works via the command line
pub fn tool() -> ToolDef {
ToolDef {
id: "DIMCENTER",
id: "CENTERMARK",
label: "Center Mark",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/line.svg")),
event: ModuleEvent::Command("DIMCENTER".to_string()),
event: ModuleEvent::Command("CENTERMARK".to_string()),
}
}
@@ -133,6 +130,119 @@ impl CadCommand for DimCenterCommand {
}
}
/// Associative centre-mark command. Each accepted pick commits one smart mark
/// and keeps the command active so further circular objects can be selected.
pub struct CenterMarkCommand {
picked: Option<EntityType>,
settings: CenterLineSettings,
}
impl CenterMarkCommand {
pub(crate) fn new(settings: CenterLineSettings) -> Self {
Self { picked: None, settings }
}
fn build_mark(
&self,
source: CenterMarkSource,
center: DVec3,
radius: f64,
x: DVec3,
y: DVec3,
) -> EntityType {
let diameter = radius * 2.0;
let association = CenterMarkAssociation {
source,
plane_origin: Vector3::new(center.x, center.y, center.z),
plane_x: Vector3::new(x.x, x.y, x.z),
plane_y: Vector3::new(y.x, y.y, y.z),
center: Vector3::new(center.x, center.y, center.z),
radius,
cross_size: resolve_center_measure(&self.settings.cross_size, diameter, 0.1),
cross_gap: resolve_center_measure(&self.settings.cross_gap, diameter, 0.05),
cross_size_relative: center_measure_is_relative(&self.settings.cross_size),
cross_gap_relative: center_measure_is_relative(&self.settings.cross_gap),
extension_length: self.settings.extension,
length_adjustments: [0.0; 4],
overshoots: [0.0; 4],
show_extensions: self.settings.mark_extensions,
associated: true,
};
let mut line = Line::from_points(
Vector3::new(center.x, center.y, center.z),
Vector3::new(center.x, center.y, center.z),
);
crate::scene::centermark::update_carrier(&mut line, &association);
EntityType::Line(line)
}
}
impl CadCommand for CenterMarkCommand {
fn name(&self) -> &'static str { "CENTERMARK" }
fn prompt(&self) -> String {
t!("CENTERMARK Select arc or circle <finish>:").into_owned()
}
fn needs_entity_pick(&self) -> bool { true }
fn inject_before_entity_pick(&self) -> bool { true }
fn inject_picked_entity(&mut self, entity: EntityType) {
self.picked = Some(entity);
}
fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult {
if handle.is_null() {
return CmdResult::NeedPoint;
}
let Some((source, center, radius, x, y)) = self
.picked
.as_ref()
.and_then(|entity| crate::scene::centermark::picked_mark_source(entity, handle, point))
else {
return CmdResult::NeedPoint;
};
CmdResult::CommitEntity(self.build_mark(source, center, radius, x, y))
}
fn on_point(&mut self, _point: DVec3) -> CmdResult { CmdResult::NeedPoint }
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel }
}
pub struct CenterMarkReassociateCommand {
target: Handle,
picked: Option<EntityType>,
}
impl CenterMarkReassociateCommand {
pub fn new(target: Handle) -> Self {
Self { target, picked: None }
}
}
impl CadCommand for CenterMarkReassociateCommand {
fn name(&self) -> &'static str { "CENTERREASSOCIATE" }
fn prompt(&self) -> String {
t!("CENTERREASSOCIATE Select new arc or circle:").into_owned()
}
fn needs_entity_pick(&self) -> bool { true }
fn inject_before_entity_pick(&self) -> bool { true }
fn inject_picked_entity(&mut self, entity: EntityType) { self.picked = Some(entity); }
fn on_entity_pick(&mut self, source: Handle, point: DVec3) -> CmdResult {
let valid = self.picked.as_ref().and_then(|entity| {
crate::scene::centermark::picked_mark_source(entity, source, point)
}).is_some();
if !valid {
return CmdResult::NeedPoint;
}
CmdResult::ReassociateCenterMark { target: self.target, source, point }
}
fn on_point(&mut self, _point: DVec3) -> CmdResult { CmdResult::NeedPoint }
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel }
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration {
names: &["DIMCENTER", "DCE", "CENTERMARK"]
+21
View File
@@ -63,6 +63,27 @@ pub fn explode_polyline_segments(entity: &EntityType) -> Vec<EntityType> {
/// Returns an empty vec if the entity cannot be exploded.
pub fn explode_entity(entity: &EntityType, document: &CadDocument) -> Vec<EntityType> {
match entity {
EntityType::Line(line) => {
let Some(association) = acadrust::entities::CenterMarkAssociation::read(
&line.common.extended_data,
) else {
return vec![];
};
crate::scene::centermark::mark_segments(&association)
.into_iter()
.map(|segment| {
let mut common = line.common.clone();
common.handle = Handle::NULL;
acadrust::entities::CenterMarkAssociation::remove(&mut common.extended_data);
EntityType::Line(LineEnt {
common,
start: Vector3::new(segment[0].x, segment[0].y, segment[0].z),
end: Vector3::new(segment[1].x, segment[1].y, segment[1].z),
..LineEnt::new()
})
})
.collect()
}
EntityType::LwPolyline(p) => explode_lwpolyline(p),
EntityType::Polyline2D(p) => explode_polyline2d(p),
EntityType::Polyline(p) => explode_polyline(p),