forked from 1b-agent/opencadstudio-upstream
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets; trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added - Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup panel; PageSetupCommit applies scale to PlotSettings - Viewport UX: locked viewport gets orange border; selected viewport gets white border - Named view → viewport assignment: Choice dropdown in properties panel copies view_target/direction/height to the viewport entity - MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines - Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal; world→pixel back-projection on drag - Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom text positioning; cxf::measure_text() helper added - New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE, ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML); DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended - PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview - Warning cleanup: removed unused imports and unreachable patterns Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
1 parent
27b5e124c7
commit
0aef1fc12f
27 files changed
+2217
-100
No files matched your search
@@ -0,0 +1,137 @@
|
||||
// ATTDEF command — define a block attribute (AttributeDefinition entity).
|
||||
//
|
||||
// Workflow (command-line only):
|
||||
// 1. Text: Enter attribute tag (required, no spaces)
|
||||
// 2. Text: Enter attribute prompt (optional — press Enter to use tag)
|
||||
// 3. Text: Enter default value (optional — press Enter for blank)
|
||||
// 4. Point: Click insertion point
|
||||
|
||||
use acadrust::entities::AttributeDefinition;
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::EntityType;
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::scene::wire_model::WireModel;
|
||||
|
||||
enum Step {
|
||||
Tag,
|
||||
Prompt { tag: String },
|
||||
Default { tag: String, prompt: String },
|
||||
Insertion { tag: String, prompt: String, default: String },
|
||||
}
|
||||
|
||||
pub struct AttdefCommand {
|
||||
step: Step,
|
||||
/// Text height in world units.
|
||||
height: f64,
|
||||
}
|
||||
|
||||
impl AttdefCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { step: Step::Tag, height: 0.2 }
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for AttdefCommand {
|
||||
fn name(&self) -> &'static str { "ATTDEF" }
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::Tag => "ATTDEF Enter attribute tag (no spaces):".into(),
|
||||
Step::Prompt { tag } => format!("ATTDEF Enter prompt for '{tag}' (Enter=use tag):"),
|
||||
Step::Default { tag, .. } => format!("ATTDEF Enter default value for '{tag}' (Enter=blank):"),
|
||||
Step::Insertion { tag, .. } => format!("ATTDEF Specify insertion point for '{tag}':"),
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
!matches!(self.step, Step::Insertion { .. })
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
match &self.step {
|
||||
Step::Tag => {
|
||||
let tag = text.trim().replace(' ', "_");
|
||||
if tag.is_empty() {
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
self.step = Step::Prompt { tag };
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::Prompt { tag } => {
|
||||
let tag = tag.clone();
|
||||
let prompt = if text.trim().is_empty() { tag.clone() } else { text.trim().to_string() };
|
||||
self.step = Step::Default { tag, prompt };
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::Default { tag, prompt } => {
|
||||
let tag = tag.clone();
|
||||
let prompt = prompt.clone();
|
||||
let default = text.trim().to_string();
|
||||
self.step = Step::Insertion { tag, prompt, default };
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::Insertion { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: Vec3) -> CmdResult {
|
||||
if let Step::Insertion { tag, prompt, default } = &self.step {
|
||||
let mut attdef = AttributeDefinition {
|
||||
tag: tag.clone(),
|
||||
prompt: prompt.clone(),
|
||||
default_value: default.clone(),
|
||||
insertion_point: Vector3::new(pt.x as f64, pt.z as f64, pt.y as f64),
|
||||
height: self.height,
|
||||
..Default::default()
|
||||
};
|
||||
attdef.common.layer = "0".into();
|
||||
CmdResult::CommitAndExit(EntityType::AttributeDefinition(attdef))
|
||||
} else {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
match &self.step {
|
||||
Step::Tag => CmdResult::Cancel,
|
||||
// Treat Enter as empty text input for prompt/default steps.
|
||||
Step::Prompt { tag } => {
|
||||
let tag = tag.clone();
|
||||
self.step = Step::Default { tag: tag.clone(), prompt: tag };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Default { tag, prompt } => {
|
||||
let (tag, prompt) = (tag.clone(), prompt.clone());
|
||||
self.step = Step::Insertion { tag, prompt, default: String::new() };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Insertion { .. } => CmdResult::Cancel,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
|
||||
if !matches!(self.step, Step::Insertion { .. }) {
|
||||
return None;
|
||||
}
|
||||
// Show a small cross at the insertion point.
|
||||
let d = 0.15_f32;
|
||||
Some(WireModel {
|
||||
name: "attdef_preview".into(),
|
||||
points: vec![
|
||||
[pt.x - d, pt.y, pt.z], [pt.x + d, pt.y, pt.z],
|
||||
[f32::NAN, 0.0, 0.0],
|
||||
[pt.x, pt.y, pt.z - d], [pt.x, pt.y, pt.z + d],
|
||||
],
|
||||
color: WireModel::CYAN,
|
||||
selected: false,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px: 1.0,
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod arc;
|
||||
pub mod attdef;
|
||||
pub mod circle;
|
||||
pub mod donut;
|
||||
pub mod ellipse;
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
// Polyline tool — ribbon definition + interactive command.
|
||||
//
|
||||
// Command: PLINE (PL)
|
||||
// Each click adds a vertex. The rubber-band shows the segment being drawn.
|
||||
// Each click adds a vertex.
|
||||
// Type A = switch to Arc segment mode.
|
||||
// Type L = switch back to Line segment mode.
|
||||
// Enter / C = close and commit. Escape = commit as-is (if ≥2 vertices).
|
||||
//
|
||||
// Arc mode: arcs are tangent-continuous with the preceding segment.
|
||||
// Bulge is stored per vertex (segment i→i+1); positive = CCW, negative = CW.
|
||||
|
||||
use acadrust::entities::LwVertex;
|
||||
use acadrust::types::Vector2;
|
||||
use acadrust::{EntityType, LwPolyline};
|
||||
use glam::{Vec2, Vec3};
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::wire_model::WireModel;
|
||||
use glam::Vec3;
|
||||
|
||||
// ── Ribbon definition ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -23,16 +29,32 @@ pub fn tool() -> ToolDef {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Segment mode ───────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum SegMode {
|
||||
Line,
|
||||
Arc,
|
||||
}
|
||||
|
||||
// ── Command implementation ─────────────────────────────────────────────────
|
||||
|
||||
pub struct PlineCommand {
|
||||
vertices: Vec<Vec3>,
|
||||
/// Bulge for segment i → i+1 (one entry per vertex; last entry unused on commit).
|
||||
bulges: Vec<f64>,
|
||||
mode: SegMode,
|
||||
/// Unit direction of the last committed segment (for arc tangent continuity).
|
||||
last_tangent: Option<Vec2>,
|
||||
}
|
||||
|
||||
impl PlineCommand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
vertices: Vec::new(),
|
||||
bulges: Vec::new(),
|
||||
mode: SegMode::Line,
|
||||
last_tangent: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +65,12 @@ impl PlineCommand {
|
||||
let lw_verts: Vec<LwVertex> = self
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|v| LwVertex::new(acadrust::types::Vector2::new(v.x as f64, v.y as f64)))
|
||||
.enumerate()
|
||||
.map(|(i, v)| {
|
||||
let mut lv = LwVertex::new(Vector2::new(v.x as f64, v.y as f64));
|
||||
lv.bulge = self.bulges.get(i).copied().unwrap_or(0.0);
|
||||
lv
|
||||
})
|
||||
.collect();
|
||||
let pline = LwPolyline {
|
||||
vertices: lw_verts,
|
||||
@@ -54,24 +81,151 @@ impl PlineCommand {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Arc geometry helpers ───────────────────────────────────────────────────
|
||||
|
||||
/// Compute the bulge for the arc from `a` to `b` that is tangent to `tangent` at `a`.
|
||||
/// Returns 0.0 if the points are coincident or the tangent is parallel to the chord.
|
||||
fn compute_bulge(a: Vec2, tangent: Vec2, b: Vec2) -> f64 {
|
||||
let d = b - a;
|
||||
let len_sq = d.length_squared();
|
||||
if len_sq < 1e-10 {
|
||||
return 0.0;
|
||||
}
|
||||
// Perpendicular to tangent (CCW) — this is the direction to the arc center.
|
||||
let perp = Vec2::new(-tangent.y, tangent.x);
|
||||
let dot = d.dot(perp);
|
||||
if dot.abs() < 1e-10 {
|
||||
// Tangent is perpendicular to chord → straight line (bulge = 0).
|
||||
return 0.0;
|
||||
}
|
||||
// t = distance from a to center along perp.
|
||||
let t = len_sq / (2.0 * dot);
|
||||
let center = a + perp * t;
|
||||
|
||||
// Arc angle from start to end (signed).
|
||||
let start_angle = (a - center).y.atan2((a - center).x);
|
||||
let end_angle = (b - center).y.atan2((b - center).x);
|
||||
let mut arc_angle = end_angle - start_angle;
|
||||
|
||||
if t > 0.0 {
|
||||
// CCW arc: ensure arc_angle is in (0, 2π].
|
||||
if arc_angle <= 0.0 {
|
||||
arc_angle += std::f32::consts::TAU;
|
||||
}
|
||||
} else {
|
||||
// CW arc: ensure arc_angle is in [-2π, 0).
|
||||
if arc_angle >= 0.0 {
|
||||
arc_angle -= std::f32::consts::TAU;
|
||||
}
|
||||
}
|
||||
(arc_angle as f64 / 4.0).tan()
|
||||
}
|
||||
|
||||
/// Update `tangent` after an arc segment described by `bulge` from `a` to `b`.
|
||||
fn update_tangent_after_arc(tangent: &mut Option<Vec2>, bulge: f64) {
|
||||
let Some(t) = *tangent else {
|
||||
return;
|
||||
};
|
||||
// The arc sweeps theta = 4*atan(bulge) radians, so the exit tangent is
|
||||
// the entry tangent rotated by that angle.
|
||||
let theta = 4.0 * (bulge as f32).atan();
|
||||
let (sin_t, cos_t) = theta.sin_cos();
|
||||
*tangent = Some(Vec2::new(t.x * cos_t - t.y * sin_t, t.x * sin_t + t.y * cos_t).normalize_or_zero());
|
||||
}
|
||||
|
||||
/// Sample a circular arc defined by bulge into `n` line-segment points.
|
||||
/// Returns the sampled [x, y, z] points (uses `z` from `a`).
|
||||
fn arc_sample_points(a: Vec3, bulge: f64, b: Vec3, n: usize) -> Vec<[f32; 3]> {
|
||||
let ax = a.x as f64;
|
||||
let ay = a.y as f64;
|
||||
let bx = b.x as f64;
|
||||
let by = b.y as f64;
|
||||
|
||||
let dx = bx - ax;
|
||||
let dy = by - ay;
|
||||
let chord_len = (dx * dx + dy * dy).sqrt();
|
||||
if chord_len < 1e-10 || bulge.abs() < 1e-10 {
|
||||
return vec![[a.x, a.y, a.z], [b.x, b.y, b.z]];
|
||||
}
|
||||
|
||||
// Center of the arc.
|
||||
// Formula: center = midpoint + offset * perp_unit
|
||||
// where offset = chord_len * (1 - bulge²) / (4 * bulge).
|
||||
let b2 = bulge * bulge;
|
||||
let offset = chord_len * (1.0 - b2) / (4.0 * bulge);
|
||||
let perp_x = -dy / chord_len;
|
||||
let perp_y = dx / chord_len;
|
||||
let mx = (ax + bx) / 2.0;
|
||||
let my = (ay + by) / 2.0;
|
||||
let cx = mx + offset * perp_x;
|
||||
let cy = my + offset * perp_y;
|
||||
|
||||
let r = ((ax - cx) * (ax - cx) + (ay - cy) * (ay - cy)).sqrt();
|
||||
let start_angle = (ay - cy).atan2(ax - cx);
|
||||
// Total arc angle (signed).
|
||||
let theta = 4.0 * bulge.atan();
|
||||
|
||||
let mut pts = Vec::with_capacity(n + 1);
|
||||
for i in 0..=n {
|
||||
let t = i as f64 / n as f64;
|
||||
let angle = start_angle + t * theta;
|
||||
pts.push([(cx + r * angle.cos()) as f32, (cy + r * angle.sin()) as f32, a.z]);
|
||||
}
|
||||
pts
|
||||
}
|
||||
|
||||
// ── CadCommand impl ────────────────────────────────────────────────────────
|
||||
|
||||
impl CadCommand for PlineCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"PLINE"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
let mode_tag = match self.mode {
|
||||
SegMode::Line => "Line",
|
||||
SegMode::Arc => "Arc",
|
||||
};
|
||||
if self.vertices.is_empty() {
|
||||
"PLINE Specify start point:".into()
|
||||
} else {
|
||||
format!(
|
||||
"PLINE Specify next point [{}pts | Enter=done C=close Esc=cancel]:",
|
||||
"PLINE [{mode_tag}] Next pt [{}pts | A=arc L=line C=close Enter=done]:",
|
||||
self.vertices.len()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: Vec3) -> CmdResult {
|
||||
if !self.vertices.is_empty() {
|
||||
let last = *self.vertices.last().unwrap();
|
||||
let last_idx = self.vertices.len() - 1;
|
||||
|
||||
let bulge = match self.mode {
|
||||
SegMode::Line => {
|
||||
let d = Vec2::new(pt.x - last.x, pt.y - last.y);
|
||||
if d.length_squared() > 1e-10 {
|
||||
self.last_tangent = Some(d.normalize());
|
||||
}
|
||||
0.0
|
||||
}
|
||||
SegMode::Arc => {
|
||||
let a = Vec2::new(last.x, last.y);
|
||||
let b = Vec2::new(pt.x, pt.y);
|
||||
let tangent = self.last_tangent.unwrap_or_else(|| {
|
||||
// No previous tangent: default to pointing right (arbitrary).
|
||||
Vec2::new(1.0, 0.0)
|
||||
});
|
||||
let bulge = compute_bulge(a, tangent, b);
|
||||
update_tangent_after_arc(&mut self.last_tangent, bulge);
|
||||
bulge
|
||||
}
|
||||
};
|
||||
self.bulges[last_idx] = bulge;
|
||||
}
|
||||
|
||||
self.vertices.push(pt);
|
||||
self.bulges.push(0.0);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
@@ -89,18 +243,71 @@ impl CadCommand for PlineCommand {
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
// Accept A / L / C once we have at least the first point.
|
||||
!self.vertices.is_empty()
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
match text.trim().to_uppercase().as_str() {
|
||||
"A" | "ARC" => {
|
||||
self.mode = SegMode::Arc;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"L" | "LINE" => {
|
||||
self.mode = SegMode::Line;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"C" | "CLOSE" => match self.build_entity(true) {
|
||||
Some(e) => Some(CmdResult::CommitAndExit(e)),
|
||||
None => Some(CmdResult::Cancel),
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
|
||||
if self.vertices.is_empty() {
|
||||
return None;
|
||||
}
|
||||
// Show all committed vertices + cursor as a continuous preview.
|
||||
let mut pts: Vec<[f32; 3]> = self.vertices.iter().map(|v| [v.x, v.y, v.z]).collect();
|
||||
pts.push([pt.x, pt.y, pt.z]);
|
||||
Some(WireModel::solid(
|
||||
"rubber_band".into(),
|
||||
pts,
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
))
|
||||
let last = *self.vertices.last().unwrap();
|
||||
|
||||
// Build committed segments first.
|
||||
let mut pts: Vec<[f32; 3]> = Vec::new();
|
||||
|
||||
// Re-emit all committed vertices + arc samples between them.
|
||||
for i in 0..self.vertices.len() {
|
||||
let v = self.vertices[i];
|
||||
if i == 0 {
|
||||
pts.push([v.x, v.y, v.z]);
|
||||
} else {
|
||||
let prev = self.vertices[i - 1];
|
||||
let b = self.bulges.get(i - 1).copied().unwrap_or(0.0);
|
||||
if b.abs() > 1e-6 {
|
||||
// Arc segment: sample it.
|
||||
let arc_pts = arc_sample_points(prev, b, v, 16);
|
||||
pts.extend_from_slice(&arc_pts[1..]); // skip first (already added)
|
||||
} else {
|
||||
pts.push([v.x, v.y, v.z]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rubber-band to cursor.
|
||||
match self.mode {
|
||||
SegMode::Line => {
|
||||
pts.push([pt.x, pt.y, pt.z]);
|
||||
}
|
||||
SegMode::Arc => {
|
||||
let a = Vec2::new(last.x, last.y);
|
||||
let b = Vec2::new(pt.x, pt.y);
|
||||
let tangent = self.last_tangent.unwrap_or(Vec2::new(1.0, 0.0));
|
||||
let bulge = compute_bulge(a, tangent, b);
|
||||
let arc_pts = arc_sample_points(last, bulge, pt, 16);
|
||||
pts.extend_from_slice(&arc_pts[1..]);
|
||||
}
|
||||
}
|
||||
|
||||
Some(WireModel::solid("rubber_band".into(), pts, WireModel::CYAN, false))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user