forked from 1b-agent/opencadstudio-upstream
Complete associative centerline workflow
This commit is contained in:
1 parent
573ed3245e
commit
eba4fb87a8
14 files changed
+683
-223
No files matched your search
@@ -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",
|
||||
]
|
||||
});
|
||||
Reference in new issue
Block a user