forked from 1b-agent/opencadstudio-upstream
Merge pull request #1246
This commit is contained in:
commit
293d0796fb
38 files changed
+4399
-487
No files matched your search
@@ -6,7 +6,7 @@ use acadrust::EntityType;
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{
|
||||
CadCommand, CmdResult, DimensionAssociationInput, InputKind, WorkingPlane,
|
||||
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, InputKind, WorkingPlane,
|
||||
};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
@@ -164,11 +164,24 @@ impl CadCommand for AlignedDimensionCommand {
|
||||
fn input_kind(&self) -> InputKind {
|
||||
if self.awaiting_text {
|
||||
InputKind::FreeText
|
||||
} else {
|
||||
} else if self.awaiting_angle {
|
||||
InputKind::SingleToken
|
||||
} else {
|
||||
InputKind::Point
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
if !matches!(self.step, Step::DimLine { .. }) || self.awaiting_text || self.awaiting_angle {
|
||||
return Vec::new();
|
||||
}
|
||||
vec![
|
||||
CmdOption::new("MText", "MTEXT"),
|
||||
CmdOption::new("Text", "TEXT"),
|
||||
CmdOption::new("Angle", "ANGLE"),
|
||||
]
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
// While entering the override text or angle it is a value, not a point
|
||||
// step.
|
||||
|
||||
@@ -489,8 +489,10 @@ impl CadCommand for AngularDimensionCommand {
|
||||
fn input_kind(&self) -> InputKind {
|
||||
if self.awaiting_text {
|
||||
InputKind::FreeText
|
||||
} else {
|
||||
} else if self.awaiting_angle || self.awaiting_quadrant {
|
||||
InputKind::SingleToken
|
||||
} else {
|
||||
InputKind::Point
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
use acadrust::Handle;
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
|
||||
/// Selection front-end used by the annotation-scale ribbon actions. The
|
||||
/// action itself runs only after the normal selection engine completes, so
|
||||
/// window/crossing selection, removal, Escape and Previous all keep their
|
||||
/// usual behavior.
|
||||
pub struct AnnotationScaleSelectionCommand {
|
||||
name: &'static str,
|
||||
action: &'static str,
|
||||
}
|
||||
|
||||
impl AnnotationScaleSelectionCommand {
|
||||
pub fn new(name: &'static str, action: &'static str) -> Self {
|
||||
Self { name, action }
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for AnnotationScaleSelectionCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
self.name
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
crate::tf!("{} Select annotative objects:", self.name).into_owned()
|
||||
}
|
||||
|
||||
fn is_selection_gathering(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
|
||||
if handles.is_empty() {
|
||||
CmdResult::Cancel
|
||||
} else {
|
||||
CmdResult::Relaunch(self.action.to_string(), handles)
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _point: DVec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,8 +353,10 @@ impl CadCommand for ArcLengthDimensionCommand {
|
||||
fn input_kind(&self) -> InputKind {
|
||||
if self.awaiting_text {
|
||||
InputKind::FreeText
|
||||
} else {
|
||||
} else if self.awaiting_angle {
|
||||
InputKind::SingleToken
|
||||
} else {
|
||||
InputKind::Point
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ pub fn tool() -> ToolDef {
|
||||
pub struct DataLinkPlaceCommand {
|
||||
table: Table,
|
||||
plane: WorkingPlane,
|
||||
command_name: &'static str,
|
||||
}
|
||||
|
||||
impl DataLinkPlaceCommand {
|
||||
@@ -30,6 +31,43 @@ impl DataLinkPlaceCommand {
|
||||
Self {
|
||||
table,
|
||||
plane: WorkingPlane::default(),
|
||||
command_name: "DATALINK",
|
||||
}
|
||||
}
|
||||
|
||||
/// Place a table that references a data-link object already stored in the
|
||||
/// drawing. Every populated cell is locked and points at the same link so
|
||||
/// update and save/reopen keep one stable relationship.
|
||||
pub fn existing(mut table: Table, link: acadrust::Handle) -> Self {
|
||||
use acadrust::entities::table::CellStateFlags;
|
||||
let rows = table.row_count() as i32;
|
||||
let columns = table.column_count() as i32;
|
||||
for row in &mut table.rows {
|
||||
for cell in &mut row.cells {
|
||||
cell.has_linked_data = true;
|
||||
cell.data_link_handle = Some(link);
|
||||
cell.data_link_rows = rows;
|
||||
cell.data_link_columns = columns;
|
||||
cell.state.insert(
|
||||
CellStateFlags::LINKED
|
||||
| CellStateFlags::CONTENT_LOCKED
|
||||
| CellStateFlags::FORMAT_LOCKED,
|
||||
);
|
||||
}
|
||||
}
|
||||
Self {
|
||||
table,
|
||||
plane: WorkingPlane::default(),
|
||||
command_name: "DATALINK",
|
||||
}
|
||||
}
|
||||
|
||||
/// Place a prepared, unlinked table (used by the extraction wizard).
|
||||
pub fn unlinked(table: Table) -> Self {
|
||||
Self {
|
||||
table,
|
||||
plane: WorkingPlane::default(),
|
||||
command_name: "DATAEXTRACTION",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,11 +143,15 @@ impl CadCommand for DataLinkPlaceCommand {
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"DATALINK"
|
||||
self.command_name
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
t!("DATALINK Specify insertion point:").into_owned()
|
||||
if self.command_name == "DATALINK" {
|
||||
t!("DATALINK Specify insertion point:").into_owned()
|
||||
} else {
|
||||
t!("DATAEXTRACTION Specify insertion point:").into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
|
||||
@@ -139,8 +139,10 @@ impl CadCommand for DiameterDimensionCommand {
|
||||
fn input_kind(&self) -> InputKind {
|
||||
if self.awaiting_text {
|
||||
InputKind::FreeText
|
||||
} else {
|
||||
} else if self.awaiting_angle {
|
||||
InputKind::SingleToken
|
||||
} else {
|
||||
InputKind::Point
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
// DIMBREAK command — insert a break in a dimension/extension line where
|
||||
// another object crosses it.
|
||||
//
|
||||
// Simplified workflow:
|
||||
// 1. Select the dimension to break
|
||||
// 2. Select the object that crosses it (or Enter for AUTO mode on all crossings)
|
||||
//
|
||||
// Because breaking a dimension line requires deep geometric intersection logic
|
||||
// that depends on the renderer, this implementation records the target handles
|
||||
// and emits a ReplaceEntity sentinel that commands.rs intercepts to apply the break.
|
||||
// DIMBREAK — create automatic, object-driven or manual gaps in dimensions.
|
||||
|
||||
use acadrust::Handle;
|
||||
use acadrust::{EntityType, Handle};
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::t;
|
||||
|
||||
@@ -28,20 +19,54 @@ pub fn tool() -> ToolDef {
|
||||
}
|
||||
|
||||
enum Step {
|
||||
PickDim,
|
||||
PickCrossing { dim_handle: Handle },
|
||||
PickDimension,
|
||||
PickDimensions(Vec<Handle>),
|
||||
PickCrossing(Vec<Handle>),
|
||||
ManualFirst(Vec<Handle>),
|
||||
ManualSecond(Vec<Handle>, DVec3),
|
||||
}
|
||||
|
||||
pub struct DimBreakCommand {
|
||||
step: Step,
|
||||
picked_entity: Option<EntityType>,
|
||||
}
|
||||
|
||||
impl DimBreakCommand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: Step::PickDim,
|
||||
step: Step::PickDimension,
|
||||
picked_entity: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn marker(layer: String, primary: Handle) -> CmdResult {
|
||||
use acadrust::entities::XLine;
|
||||
let mut marker = XLine::default();
|
||||
marker.common.layer = layer;
|
||||
CmdResult::ReplaceEntity(primary, vec![EntityType::XLine(marker)])
|
||||
}
|
||||
|
||||
fn handles(handles: &[Handle]) -> String {
|
||||
handles
|
||||
.iter()
|
||||
.map(|handle| handle.value().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(";")
|
||||
}
|
||||
|
||||
fn automatic(handles: &[Handle]) -> CmdResult {
|
||||
Self::marker(
|
||||
format!("__DIMBREAK_AUTO__{}", Self::handles(handles)),
|
||||
handles[0],
|
||||
)
|
||||
}
|
||||
|
||||
fn remove(handles: &[Handle]) -> CmdResult {
|
||||
Self::marker(
|
||||
format!("__DIMBREAK_REMOVE__{}", Self::handles(handles)),
|
||||
handles[0],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for DimBreakCommand {
|
||||
@@ -51,54 +76,151 @@ impl CadCommand for DimBreakCommand {
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::PickDim => t!("DIMBREAK Select dimension to break:").into_owned(),
|
||||
Step::PickCrossing { .. } => {
|
||||
t!("DIMBREAK Select object to break at, or Enter for Auto:").into_owned()
|
||||
Step::PickDimension => {
|
||||
t!("DIMBREAK Select dimension to add/remove break or [Multiple]:").into_owned()
|
||||
}
|
||||
Step::PickDimensions(handles) => crate::tf!(
|
||||
"DIMBREAK Select dimensions ({} selected, press Enter when done):",
|
||||
handles.len()
|
||||
)
|
||||
.into_owned(),
|
||||
Step::PickCrossing(_) => t!(
|
||||
"DIMBREAK Select object to break dimension or [Auto/Manual/Remove] <Auto>:"
|
||||
)
|
||||
.into_owned(),
|
||||
Step::ManualFirst(_) => t!("DIMBREAK Specify first break point:").into_owned(),
|
||||
Step::ManualSecond(_, _) => {
|
||||
t!("DIMBREAK Specify second break point:").into_owned()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
match self.step {
|
||||
Step::PickDimension => vec![CmdOption::new("Multiple", "MULTIPLE")],
|
||||
Step::PickCrossing(_) => vec![
|
||||
CmdOption::new("Auto", "AUTO"),
|
||||
CmdOption::new("Manual", "MANUAL"),
|
||||
CmdOption::new("Remove", "REMOVE"),
|
||||
],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn input_kind(&self) -> InputKind {
|
||||
InputKind::Point
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
matches!(self.step, Step::PickDimension | Step::PickCrossing(_))
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
true
|
||||
matches!(
|
||||
self.step,
|
||||
Step::PickDimension | Step::PickDimensions(_) | Step::PickCrossing(_)
|
||||
)
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
|
||||
fn inject_before_entity_pick(&self) -> bool {
|
||||
matches!(self.step, Step::PickDimension | Step::PickDimensions(_))
|
||||
}
|
||||
|
||||
fn inject_picked_entity(&mut self, entity: EntityType) {
|
||||
self.picked_entity = Some(entity);
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _point: DVec3) -> CmdResult {
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
match &self.step {
|
||||
Step::PickDim => {
|
||||
self.step = Step::PickCrossing { dim_handle: handle };
|
||||
match &mut self.step {
|
||||
Step::PickDimension => {
|
||||
if !matches!(self.picked_entity.take(), Some(EntityType::Dimension(_))) {
|
||||
return CmdResult::ReportError(
|
||||
t!("DIMBREAK: select a dimension.").into_owned(),
|
||||
);
|
||||
}
|
||||
self.step = Step::PickCrossing(vec![handle]);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::PickCrossing { dim_handle } => {
|
||||
let dim_h = *dim_handle;
|
||||
// Emit sentinel: the actual break logic is in commands.rs
|
||||
use acadrust::entities::XLine;
|
||||
let mut xl = XLine::default();
|
||||
xl.common.layer = format!("__DIMBREAK__{},{}", dim_h.value(), handle.value());
|
||||
CmdResult::ReplaceEntity(dim_h, vec![acadrust::EntityType::XLine(xl)])
|
||||
Step::PickDimensions(handles) => {
|
||||
if !matches!(self.picked_entity.take(), Some(EntityType::Dimension(_))) {
|
||||
return CmdResult::ReportError(
|
||||
t!("DIMBREAK: select a dimension.").into_owned(),
|
||||
);
|
||||
}
|
||||
if !handles.contains(&handle) {
|
||||
handles.push(handle);
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::PickCrossing(handles) => Self::marker(
|
||||
format!(
|
||||
"__DIMBREAK_OBJECT__{}|{}",
|
||||
Self::handles(handles),
|
||||
handle.value()
|
||||
),
|
||||
handles[0],
|
||||
),
|
||||
Step::ManualFirst(_) | Step::ManualSecond(_, _) => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let keyword = text.trim().to_ascii_uppercase();
|
||||
match &mut self.step {
|
||||
Step::PickDimension if matches!(keyword.as_str(), "M" | "MULTIPLE") => {
|
||||
self.step = Step::PickDimensions(Vec::new());
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::PickCrossing(handles) if matches!(keyword.as_str(), "A" | "AUTO") => {
|
||||
Some(Self::automatic(handles))
|
||||
}
|
||||
Step::PickCrossing(handles) if matches!(keyword.as_str(), "M" | "MANUAL") => {
|
||||
self.step = Step::ManualFirst(handles.clone());
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::PickCrossing(handles) if matches!(keyword.as_str(), "R" | "REMOVE") => {
|
||||
Some(Self::remove(handles))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
match &self.step {
|
||||
Step::ManualFirst(handles) => {
|
||||
self.step = Step::ManualSecond(handles.clone(), point);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::ManualSecond(handles, first) => Self::marker(
|
||||
format!(
|
||||
"__DIMBREAK_MANUAL__{}|{:.15},{:.15},{:.15}|{:.15},{:.15},{:.15}",
|
||||
Self::handles(handles),
|
||||
first.x,
|
||||
first.y,
|
||||
first.z,
|
||||
point.x,
|
||||
point.y,
|
||||
point.z
|
||||
),
|
||||
handles[0],
|
||||
),
|
||||
_ => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
// Auto mode: apply to all crossings
|
||||
if let Step::PickCrossing { dim_handle } = &self.step {
|
||||
let dim_h = *dim_handle;
|
||||
use acadrust::entities::XLine;
|
||||
let mut xl = XLine::default();
|
||||
xl.common.layer = format!("__DIMBREAK_AUTO__{}", dim_h.value());
|
||||
return CmdResult::ReplaceEntity(dim_h, vec![acadrust::EntityType::XLine(xl)]);
|
||||
match &self.step {
|
||||
Step::PickDimensions(handles) if !handles.is_empty() => {
|
||||
self.step = Step::PickCrossing(handles.clone());
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::PickCrossing(handles) => Self::automatic(handles),
|
||||
_ => CmdResult::Cancel,
|
||||
}
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["DIMBREAK"] }); // DimBreakCommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["DIMBREAK"] });
|
||||
+105
-43
@@ -1,13 +1,11 @@
|
||||
// DIMEDIT command — edit dimension text or reset it to the measured value.
|
||||
//
|
||||
// Workflow:
|
||||
// 1. Pick a dimension entity
|
||||
// 2. Enter new text (blank = reset to auto-measured value, "<>" = measured value placeholder)
|
||||
// DIMEDIT — edit dimension text, rotation, extension-line obliquing or home position.
|
||||
|
||||
use acadrust::Handle;
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{
|
||||
CadCommand, CmdOption, CmdResult, DimensionEditOperation, InputKind,
|
||||
};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::t;
|
||||
|
||||
@@ -23,8 +21,11 @@ pub fn tool() -> ToolDef {
|
||||
}
|
||||
|
||||
enum Step {
|
||||
PickDim,
|
||||
EnterText { handle: Handle },
|
||||
Choose,
|
||||
NewText,
|
||||
Rotate,
|
||||
Oblique,
|
||||
Select(DimensionEditOperation),
|
||||
}
|
||||
|
||||
pub struct DimEditCommand {
|
||||
@@ -33,9 +34,12 @@ pub struct DimEditCommand {
|
||||
|
||||
impl DimEditCommand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: Step::PickDim,
|
||||
}
|
||||
Self { step: Step::Choose }
|
||||
}
|
||||
|
||||
fn select(&mut self, operation: DimensionEditOperation) -> CmdResult {
|
||||
self.step = Step::Select(operation);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,48 +49,106 @@ impl CadCommand for DimEditCommand {
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::PickDim => t!("DIMEDIT Select dimension:").into_owned(),
|
||||
Step::EnterText { .. } => {
|
||||
t!("DIMEDIT Enter text override (blank = reset to measured):").into_owned()
|
||||
match self.step {
|
||||
Step::Choose => {
|
||||
t!("DIMEDIT Enter type of dimension editing [Home/New/Rotate/Oblique] <Home>:")
|
||||
.into_owned()
|
||||
}
|
||||
Step::NewText => t!("DIMEDIT Enter new dimension text <measured value>:").into_owned(),
|
||||
Step::Rotate => t!("DIMEDIT Specify angle for dimension text:").into_owned(),
|
||||
Step::Oblique => {
|
||||
t!("DIMEDIT Enter obliquing angle (press Enter for none):").into_owned()
|
||||
}
|
||||
Step::Select(_) => t!("DIMEDIT Select objects:").into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
if !matches!(self.step, Step::Choose) {
|
||||
return Vec::new();
|
||||
}
|
||||
vec![
|
||||
CmdOption::new("Home", "HOME"),
|
||||
CmdOption::new("New", "NEW"),
|
||||
CmdOption::new("Rotate", "ROTATE"),
|
||||
CmdOption::new("Oblique", "OBLIQUE"),
|
||||
]
|
||||
}
|
||||
|
||||
fn input_kind(&self) -> InputKind {
|
||||
match self.step {
|
||||
Step::NewText => InputKind::FreeText,
|
||||
Step::Select(_) => InputKind::Point,
|
||||
Step::Choose | Step::Rotate | Step::Oblique => InputKind::SingleToken,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_selection_gathering(&self) -> bool {
|
||||
matches!(self.step, Step::Select(_))
|
||||
}
|
||||
|
||||
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
|
||||
let Step::Select(operation) = &self.step else {
|
||||
return CmdResult::Cancel;
|
||||
};
|
||||
if handles.is_empty() {
|
||||
CmdResult::Cancel
|
||||
} else {
|
||||
CmdResult::EditDimensions {
|
||||
handles,
|
||||
operation: operation.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
matches!(self.step, Step::PickDim)
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.step = Step::EnterText { handle };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
matches!(self.step, Step::EnterText { .. })
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let handle = match &self.step {
|
||||
Step::EnterText { handle } => *handle,
|
||||
_ => return None,
|
||||
};
|
||||
// Empty = reset to auto-measured, "<>" = keep measured, else override
|
||||
let new_text = text.trim().to_string();
|
||||
Some(CmdResult::DdeditEntity { handle, new_text })
|
||||
let value = text.trim();
|
||||
let keyword = value.to_ascii_uppercase();
|
||||
match self.step {
|
||||
Step::Choose => match keyword.as_str() {
|
||||
"H" | "HOME" => Some(self.select(DimensionEditOperation::Home)),
|
||||
"N" | "NEW" => {
|
||||
self.step = Step::NewText;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"R" | "ROTATE" => {
|
||||
self.step = Step::Rotate;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"O" | "OBLIQUE" => {
|
||||
self.step = Step::Oblique;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
Step::NewText => Some(self.select(DimensionEditOperation::NewText(
|
||||
if value == "<>" { String::new() } else { text.to_string() },
|
||||
))),
|
||||
Step::Rotate => value
|
||||
.parse::<f64>()
|
||||
.ok()
|
||||
.map(|degrees| self.select(DimensionEditOperation::Rotate(degrees))),
|
||||
Step::Oblique => value
|
||||
.parse::<f64>()
|
||||
.ok()
|
||||
.filter(|degrees| (-85.0..=85.0).contains(degrees))
|
||||
.map(|degrees| self.select(DimensionEditOperation::Oblique(degrees))),
|
||||
Step::Select(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
|
||||
fn on_point(&mut self, _point: DVec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
match self.step {
|
||||
Step::Choose => self.select(DimensionEditOperation::Home),
|
||||
Step::NewText => self.select(DimensionEditOperation::NewText(String::new())),
|
||||
Step::Oblique => self.select(DimensionEditOperation::Oblique(0.0)),
|
||||
Step::Rotate | Step::Select(_) => CmdResult::Cancel,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["DIMEDIT"] }); // DimEditCommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["DIMEDIT"] });
|
||||
@@ -1,13 +1,10 @@
|
||||
// DIMJOGLINE command — add a jog (zigzag) symbol to a linear or aligned dimension.
|
||||
//
|
||||
// Workflow:
|
||||
// 1. Pick the dimension
|
||||
// 2. Click the position on the dimension line where the jog should appear
|
||||
// DIMJOGLINE — add or remove a jog on a linear/aligned dimension.
|
||||
|
||||
use acadrust::Handle;
|
||||
use glam::DVec3;
|
||||
use acadrust::entities::Dimension;
|
||||
use acadrust::{EntityType, Handle};
|
||||
use glam::{DVec3, Vec3};
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use crate::t;
|
||||
@@ -30,12 +27,76 @@ enum Step {
|
||||
|
||||
pub struct DimJogLineCommand {
|
||||
step: Step,
|
||||
remove: bool,
|
||||
picked_entity: Option<EntityType>,
|
||||
dimension: Option<Dimension>,
|
||||
}
|
||||
|
||||
impl DimJogLineCommand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: Step::PickDim,
|
||||
remove: false,
|
||||
picked_entity: None,
|
||||
dimension: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn result(handle: Handle, point: Option<DVec3>) -> CmdResult {
|
||||
use acadrust::entities::XLine;
|
||||
let mut marker = XLine::default();
|
||||
marker.common.layer = match point {
|
||||
Some(point) => format!(
|
||||
"__DIMJOG__{}|{:.15},{:.15},{:.15}",
|
||||
handle.value(), point.x, point.y, point.z
|
||||
),
|
||||
None => format!("__DIMJOG_REMOVE__{}", handle.value()),
|
||||
};
|
||||
CmdResult::ReplaceEntity(handle, vec![EntityType::XLine(marker)])
|
||||
}
|
||||
|
||||
fn default_position(dimension: &Dimension) -> Option<DVec3> {
|
||||
let (first, second, definition, axis) = match dimension {
|
||||
Dimension::Linear(value) => (
|
||||
value.first_point,
|
||||
value.second_point,
|
||||
value.definition_point,
|
||||
DVec3::new(value.rotation.cos(), value.rotation.sin(), 0.0),
|
||||
),
|
||||
Dimension::Aligned(value) => {
|
||||
let first = DVec3::new(value.first_point.x, value.first_point.y, value.first_point.z);
|
||||
let second = DVec3::new(value.second_point.x, value.second_point.y, value.second_point.z);
|
||||
(
|
||||
value.first_point,
|
||||
value.second_point,
|
||||
value.definition_point,
|
||||
(second - first).try_normalize().unwrap_or(DVec3::X),
|
||||
)
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
let first = DVec3::new(first.x, first.y, first.z);
|
||||
let second = DVec3::new(second.x, second.y, second.z);
|
||||
let definition = DVec3::new(definition.x, definition.y, definition.z);
|
||||
let axis = axis.try_normalize().unwrap_or(DVec3::X);
|
||||
let perpendicular = DVec3::new(-axis.y, axis.x, 0.0);
|
||||
let offset = definition.dot(perpendicular);
|
||||
let first_on_line = first + perpendicular * (offset - first.dot(perpendicular));
|
||||
let second_on_line = second + perpendicular * (offset - second.dot(perpendicular));
|
||||
let midpoint = (first_on_line + second_on_line) * 0.5;
|
||||
|
||||
let text = dimension.base().text_middle_point;
|
||||
let text = DVec3::new(text.x, text.y, text.z);
|
||||
let line = second_on_line - first_on_line;
|
||||
let length_squared = line.length_squared();
|
||||
if length_squared <= 1.0e-18 {
|
||||
return Some(midpoint);
|
||||
}
|
||||
let parameter = (text - first_on_line).dot(line) / length_squared;
|
||||
if text.is_finite() && (0.0..=1.0).contains(¶meter) {
|
||||
Some((first_on_line + text) * 0.5)
|
||||
} else {
|
||||
Some(midpoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,90 +107,129 @@ impl CadCommand for DimJogLineCommand {
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::PickDim => t!("DIMJOGLINE Select linear or aligned dimension:").into_owned(),
|
||||
Step::PickJogPos { .. } => t!("DIMJOGLINE Specify jog location:").into_owned(),
|
||||
match self.step {
|
||||
Step::PickDim if self.remove => {
|
||||
t!("DIMJOGLINE Select dimension to remove jog:").into_owned()
|
||||
}
|
||||
Step::PickDim => {
|
||||
t!("DIMJOGLINE Select dimension to add jog or [Remove]:").into_owned()
|
||||
}
|
||||
Step::PickJogPos { .. } => {
|
||||
t!("DIMJOGLINE Specify jog location (or press Enter):").into_owned()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
if matches!(self.step, Step::PickDim) && !self.remove {
|
||||
vec![CmdOption::new("Remove", "REMOVE")]
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn input_kind(&self) -> InputKind {
|
||||
InputKind::Point
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
matches!(self.step, Step::PickDim) && !self.remove
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
matches!(self.step, Step::PickDim)
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
|
||||
fn inject_before_entity_pick(&self) -> bool {
|
||||
matches!(self.step, Step::PickDim)
|
||||
}
|
||||
|
||||
fn inject_picked_entity(&mut self, entity: EntityType) {
|
||||
self.picked_entity = Some(entity);
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _point: DVec3) -> CmdResult {
|
||||
let Some(EntityType::Dimension(dimension @ (Dimension::Linear(_) | Dimension::Aligned(_)))) =
|
||||
self.picked_entity.take()
|
||||
else {
|
||||
return CmdResult::ReportError(
|
||||
t!("DIMJOGLINE: select a linear or aligned dimension.").into_owned(),
|
||||
);
|
||||
};
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
if self.remove {
|
||||
return Self::result(handle, None);
|
||||
}
|
||||
self.dimension = Some(dimension);
|
||||
self.step = Step::PickJogPos { handle };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
if let Step::PickJogPos { handle } = &self.step {
|
||||
let h = *handle;
|
||||
// Emit sentinel for commands.rs to store the jog position
|
||||
use acadrust::entities::XLine;
|
||||
let mut xl = XLine::default();
|
||||
xl.common.layer = format!("__DIMJOG__{},{:.6},{:.6}", h.value(), pt.x, pt.z);
|
||||
return CmdResult::ReplaceEntity(h, vec![acadrust::EntityType::XLine(xl)]);
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
if matches!(self.step, Step::PickDim)
|
||||
&& matches!(text.trim().to_ascii_uppercase().as_str(), "R" | "REMOVE")
|
||||
{
|
||||
self.remove = true;
|
||||
Some(CmdResult::NeedPoint)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
match self.step {
|
||||
Step::PickJogPos { handle } => Self::result(handle, Some(point)),
|
||||
Step::PickDim => CmdResult::NeedPoint,
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
match (&self.step, &self.dimension) {
|
||||
(Step::PickJogPos { handle }, Some(dimension)) => Self::default_position(dimension)
|
||||
.map(|point| Self::result(*handle, Some(point)))
|
||||
.unwrap_or(CmdResult::Cancel),
|
||||
_ => CmdResult::Cancel,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { let pt = pt.as_vec3();
|
||||
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
|
||||
if !matches!(self.step, Step::PickJogPos { .. }) {
|
||||
return None;
|
||||
}
|
||||
let d = 0.3_f32;
|
||||
Some(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: "dimjog_preview".into(),
|
||||
// Jog zigzag in the XY drawing plane (Z is elevation, ~0). The old
|
||||
// marker varied Z, so in the top-down view it collapsed to a flat
|
||||
// line instead of a zigzag. (#150)
|
||||
points: vec![
|
||||
[pt.x - d, pt.y, pt.z],
|
||||
[pt.x - d * 0.3, pt.y + d, pt.z],
|
||||
[pt.x + d * 0.3, pt.y - d, pt.z],
|
||||
[pt.x + d, pt.y, pt.z],
|
||||
],
|
||||
points_low: Vec::new(),
|
||||
color: WireModel::CYAN,
|
||||
selected: false,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px: 1.2,
|
||||
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(),
|
||||
})
|
||||
let axis = self
|
||||
.dimension
|
||||
.as_ref()
|
||||
.and_then(|dimension| match dimension {
|
||||
Dimension::Linear(value) => {
|
||||
Some(Vec3::new(value.rotation.cos() as f32, value.rotation.sin() as f32, 0.0))
|
||||
}
|
||||
Dimension::Aligned(value) => Some(Vec3::new(
|
||||
(value.second_point.x - value.first_point.x) as f32,
|
||||
(value.second_point.y - value.first_point.y) as f32,
|
||||
0.0,
|
||||
)),
|
||||
_ => None,
|
||||
})
|
||||
.and_then(Vec3::try_normalize)
|
||||
.unwrap_or(Vec3::X);
|
||||
let perpendicular = Vec3::new(-axis.y, axis.x, 0.0);
|
||||
let center = point.as_vec3();
|
||||
let size = 0.3;
|
||||
let points = vec![
|
||||
center - axis * size,
|
||||
center - axis * size * 0.25 + perpendicular * size,
|
||||
center + axis * size * 0.25 - perpendicular * size,
|
||||
center + axis * size,
|
||||
];
|
||||
let mut preview = WireModel::default();
|
||||
preview.name = "dimjog_preview".into();
|
||||
preview.points = points.into_iter().map(|point| point.to_array()).collect();
|
||||
preview.color = WireModel::CYAN;
|
||||
preview.line_weight_px = 1.2;
|
||||
Some(preview)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["DIMJOGLINE"] }); // DimJogLineCommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["DIMJOGLINE"] });
|
||||
@@ -3,12 +3,12 @@
|
||||
// Workflow:
|
||||
// 1. Select the base dimension
|
||||
// 2. Select the other dimensions to space (click each, Enter to finish)
|
||||
// 3. Enter spacing value (or 0 for automatic equal spacing)
|
||||
// 3. Enter a non-negative spacing value, or choose Auto
|
||||
|
||||
use acadrust::Handle;
|
||||
use acadrust::{EntityType, Handle};
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind, SelectionEntity};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::t;
|
||||
|
||||
@@ -31,12 +31,14 @@ enum Step {
|
||||
|
||||
pub struct DimSpaceCommand {
|
||||
step: Step,
|
||||
picked_entity: Option<EntityType>,
|
||||
}
|
||||
|
||||
impl DimSpaceCommand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: Step::PickBase,
|
||||
picked_entity: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,12 +56,66 @@ impl CadCommand for DimSpaceCommand {
|
||||
count = others.len()
|
||||
)
|
||||
.into_owned(),
|
||||
Step::EnterSpacing { .. } => t!("DIMSPACE Enter value (0 = auto):").into_owned(),
|
||||
Step::EnterSpacing { .. } => {
|
||||
t!("DIMSPACE Specify distance between dimensions or [Auto] <Auto>:")
|
||||
.into_owned()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
matches!(self.step, Step::EnterSpacing { .. })
|
||||
.then(|| vec![CmdOption::new("Auto", "AUTO")])
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn input_kind(&self) -> InputKind {
|
||||
if matches!(self.step, Step::EnterSpacing { .. }) {
|
||||
InputKind::SingleToken
|
||||
} else {
|
||||
InputKind::Point
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
matches!(self.step, Step::PickBase | Step::PickOthers { .. })
|
||||
matches!(self.step, Step::PickBase)
|
||||
}
|
||||
|
||||
fn inject_before_entity_pick(&self) -> bool {
|
||||
matches!(self.step, Step::PickBase)
|
||||
}
|
||||
|
||||
fn inject_picked_entity(&mut self, entity: EntityType) {
|
||||
self.picked_entity = Some(entity);
|
||||
}
|
||||
|
||||
fn is_selection_gathering(&self) -> bool {
|
||||
matches!(self.step, Step::PickOthers { .. })
|
||||
}
|
||||
|
||||
fn selection_entities_exclude_locked(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn inject_selection_entities(&mut self, entities: Vec<SelectionEntity>) {
|
||||
let Step::PickOthers { base, others } = &mut self.step else {
|
||||
return;
|
||||
};
|
||||
others.clear();
|
||||
for selected in entities {
|
||||
if selected.handle != *base
|
||||
&& matches!(
|
||||
selected.entity,
|
||||
EntityType::Dimension(
|
||||
acadrust::entities::Dimension::Linear(_)
|
||||
| acadrust::entities::Dimension::Aligned(_)
|
||||
)
|
||||
)
|
||||
&& !others.contains(&selected.handle)
|
||||
{
|
||||
others.push(selected.handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
|
||||
@@ -68,29 +124,60 @@ impl CadCommand for DimSpaceCommand {
|
||||
}
|
||||
match &mut self.step {
|
||||
Step::PickBase => {
|
||||
if !matches!(
|
||||
self.picked_entity.take(),
|
||||
Some(EntityType::Dimension(
|
||||
acadrust::entities::Dimension::Linear(_)
|
||||
| acadrust::entities::Dimension::Aligned(_)
|
||||
))
|
||||
) {
|
||||
return CmdResult::ReportError(
|
||||
t!("DIMSPACE: select a linear or aligned dimension.").into_owned(),
|
||||
);
|
||||
}
|
||||
self.step = Step::PickOthers {
|
||||
base: handle,
|
||||
others: vec![],
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::PickOthers { others, .. } => {
|
||||
if !others.contains(&handle) {
|
||||
others.push(handle);
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
matches!(self.step, Step::EnterSpacing { .. })
|
||||
fn on_selection_complete(&mut self, _handles: Vec<Handle>) -> CmdResult {
|
||||
let Step::PickOthers { base, others } = &self.step else {
|
||||
return CmdResult::Cancel;
|
||||
};
|
||||
if others.is_empty() {
|
||||
return CmdResult::ReportError(
|
||||
t!("DIMSPACE: select one or more linear or aligned dimensions.").into_owned(),
|
||||
);
|
||||
}
|
||||
let base = *base;
|
||||
let others = others.clone();
|
||||
self.step = Step::EnterSpacing { base, others };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
if let Step::EnterSpacing { base, others } = &self.step {
|
||||
let spacing: f64 = text.trim().parse().unwrap_or(0.0);
|
||||
let token = text.trim();
|
||||
let spacing = if matches!(token.to_ascii_uppercase().as_str(), "A" | "AUTO") {
|
||||
"AUTO".to_string()
|
||||
} else {
|
||||
let Ok(value) = token.parse::<f64>() else {
|
||||
return Some(CmdResult::ReportError(
|
||||
t!("DIMSPACE: enter a non-negative spacing value or Auto.").into_owned(),
|
||||
));
|
||||
};
|
||||
if value < 0.0 {
|
||||
return Some(CmdResult::ReportError(
|
||||
t!("DIMSPACE: spacing value cannot be negative.").into_owned(),
|
||||
));
|
||||
}
|
||||
value.to_string()
|
||||
};
|
||||
let b = *base;
|
||||
let o = others.clone();
|
||||
// Emit sentinel for commands.rs to handle
|
||||
@@ -116,15 +203,12 @@ impl CadCommand for DimSpaceCommand {
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
match &self.step {
|
||||
Step::PickOthers { base, others } if !others.is_empty() => {
|
||||
let b = *base;
|
||||
let o = others.clone();
|
||||
self.step = Step::EnterSpacing { base: b, others: o };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => CmdResult::Cancel,
|
||||
if matches!(self.step, Step::EnterSpacing { .. }) {
|
||||
return self
|
||||
.on_text_input("AUTO")
|
||||
.unwrap_or(CmdResult::Cancel);
|
||||
}
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
// DIMTEDIT command — reposition the text of an existing dimension.
|
||||
//
|
||||
// Workflow:
|
||||
// 1. Pick a dimension entity
|
||||
// 2. Click the new text position (entity data is injected by update.rs after pick)
|
||||
// DIMTEDIT — reposition, justify, home or rotate existing dimension text.
|
||||
|
||||
use acadrust::entities::{AttachmentPointType, Dimension};
|
||||
use acadrust::{EntityType, Handle};
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use crate::t;
|
||||
@@ -29,18 +26,47 @@ enum Step {
|
||||
handle: Handle,
|
||||
entity: Option<EntityType>,
|
||||
},
|
||||
EnterAngle {
|
||||
handle: Handle,
|
||||
entity: Option<EntityType>,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct DimTeditCommand {
|
||||
step: Step,
|
||||
picked_entity: Option<EntityType>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Placement {
|
||||
Left,
|
||||
Right,
|
||||
Center,
|
||||
Home,
|
||||
}
|
||||
|
||||
impl DimTeditCommand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: Step::PickDim,
|
||||
picked_entity: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_placement(
|
||||
handle: Handle,
|
||||
entity: &mut Option<EntityType>,
|
||||
placement: Placement,
|
||||
) -> CmdResult {
|
||||
let Some(mut entity) = entity.take() else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
let EntityType::Dimension(dimension) = &mut entity else {
|
||||
return CmdResult::Cancel;
|
||||
};
|
||||
apply_placement(dimension, placement);
|
||||
CmdResult::UpdateEntityAndFinish { handle, entity }
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for DimTeditCommand {
|
||||
@@ -49,47 +75,123 @@ impl CadCommand for DimTeditCommand {
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
match self.step {
|
||||
Step::PickDim => t!("DIMTEDIT Select dimension:").into_owned(),
|
||||
Step::PickTextPos { .. } => {
|
||||
t!("DIMTEDIT Specify new location for dimension text:").into_owned()
|
||||
Step::PickTextPos { .. } => t!(
|
||||
"DIMTEDIT Specify new location for dimension text or [Left/Right/Center/Home/Angle]:"
|
||||
)
|
||||
.into_owned(),
|
||||
Step::EnterAngle { .. } => {
|
||||
t!("DIMTEDIT Specify angle for dimension text:").into_owned()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
if !matches!(self.step, Step::PickTextPos { .. }) {
|
||||
return Vec::new();
|
||||
}
|
||||
vec![
|
||||
CmdOption::new("Left", "LEFT"),
|
||||
CmdOption::new("Right", "RIGHT"),
|
||||
CmdOption::new("Center", "CENTER"),
|
||||
CmdOption::new("Home", "HOME"),
|
||||
CmdOption::new("Angle", "ANGLE"),
|
||||
]
|
||||
}
|
||||
|
||||
fn input_kind(&self) -> InputKind {
|
||||
match self.step {
|
||||
Step::EnterAngle { .. } => InputKind::SingleToken,
|
||||
Step::PickDim | Step::PickTextPos { .. } => InputKind::Point,
|
||||
}
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
matches!(self.step, Step::PickTextPos { .. })
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
matches!(self.step, Step::PickDim)
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
|
||||
fn inject_before_entity_pick(&self) -> bool {
|
||||
matches!(self.step, Step::PickDim)
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _point: DVec3) -> CmdResult {
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
if !matches!(self.picked_entity.as_ref(), Some(EntityType::Dimension(_))) {
|
||||
self.picked_entity = None;
|
||||
return CmdResult::ReportError(
|
||||
t!("DIMTEDIT: select a dimension.").into_owned(),
|
||||
);
|
||||
}
|
||||
self.step = Step::PickTextPos {
|
||||
handle,
|
||||
entity: None,
|
||||
entity: self.picked_entity.take(),
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
if let Step::PickTextPos { handle, entity } = &mut self.step {
|
||||
let h = *handle;
|
||||
if let Some(mut ent) = entity.take() {
|
||||
if let EntityType::Dimension(ref mut d) = ent {
|
||||
let new_pt = acadrust::types::Vector3::new(pt.x, pt.y, pt.z);
|
||||
d.base_mut().text_middle_point = new_pt;
|
||||
d.base_mut().insertion_point = new_pt;
|
||||
// Pin the text to this location, else the renderer recomputes
|
||||
// the style-default placement and the move is discarded.
|
||||
d.base_mut().text_user_positioned = true;
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let keyword = text.trim().to_ascii_uppercase();
|
||||
match &mut self.step {
|
||||
Step::PickTextPos { handle, entity } => match keyword.as_str() {
|
||||
"L" | "LEFT" => Some(Self::finish_placement(*handle, entity, Placement::Left)),
|
||||
"R" | "RIGHT" => {
|
||||
Some(Self::finish_placement(*handle, entity, Placement::Right))
|
||||
}
|
||||
return CmdResult::ReplaceEntity(h, vec![ent]);
|
||||
"C" | "CENTER" => {
|
||||
Some(Self::finish_placement(*handle, entity, Placement::Center))
|
||||
}
|
||||
"H" | "HOME" => Some(Self::finish_placement(*handle, entity, Placement::Home)),
|
||||
"A" | "ANGLE" => {
|
||||
let handle = *handle;
|
||||
let entity = entity.take();
|
||||
self.step = Step::EnterAngle { handle, entity };
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
Step::EnterAngle { handle, entity } => {
|
||||
let degrees = text.trim().parse::<f64>().ok()?;
|
||||
let Some(mut entity) = entity.take() else {
|
||||
return Some(CmdResult::NeedPoint);
|
||||
};
|
||||
let EntityType::Dimension(dimension) = &mut entity else {
|
||||
return Some(CmdResult::Cancel);
|
||||
};
|
||||
dimension.base_mut().text_rotation = degrees.to_radians();
|
||||
Some(CmdResult::UpdateEntityAndFinish {
|
||||
handle: *handle,
|
||||
entity,
|
||||
})
|
||||
}
|
||||
// No entity yet — wait for inject
|
||||
CmdResult::NeedPoint
|
||||
} else {
|
||||
CmdResult::NeedPoint
|
||||
Step::PickDim => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
let Step::PickTextPos { handle, entity } = &mut self.step else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
let Some(mut entity) = entity.take() else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
let EntityType::Dimension(dimension) = &mut entity else {
|
||||
return CmdResult::Cancel;
|
||||
};
|
||||
let point = acadrust::types::Vector3::new(point.x, point.y, point.z);
|
||||
let base = dimension.base_mut();
|
||||
base.text_middle_point = point;
|
||||
base.insertion_point = point;
|
||||
base.text_user_positioned = true;
|
||||
CmdResult::UpdateEntityAndFinish {
|
||||
handle: *handle,
|
||||
entity,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,63 +199,102 @@ impl CadCommand for DimTeditCommand {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { let pt = pt.as_vec3();
|
||||
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
|
||||
if !matches!(self.step, Step::PickTextPos { .. }) {
|
||||
return None;
|
||||
}
|
||||
let point = point.as_vec3();
|
||||
let d = 0.2_f32;
|
||||
Some(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: "dimtedit_preview".into(),
|
||||
// Marker box in the XY drawing plane (Z is elevation, ~0). The old
|
||||
// box varied Z, so in the top-down view it collapsed to a flat line
|
||||
// instead of a square. (#150)
|
||||
points: vec![
|
||||
[pt.x - d, pt.y - d, pt.z],
|
||||
[pt.x + d, pt.y - d, pt.z],
|
||||
[pt.x + d, pt.y + d, pt.z],
|
||||
[pt.x - d, pt.y + d, pt.z],
|
||||
[pt.x - d, pt.y - d, pt.z],
|
||||
[point.x - d, point.y - d, point.z],
|
||||
[point.x + d, point.y - d, point.z],
|
||||
[point.x + d, point.y + d, point.z],
|
||||
[point.x - d, point.y + d, point.z],
|
||||
[point.x - d, point.y - d, point.z],
|
||||
],
|
||||
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(),
|
||||
..WireModel::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn inject_picked_entity(&mut self, entity: EntityType) {
|
||||
if let Step::PickTextPos { entity: slot, .. } = &mut self.step {
|
||||
*slot = Some(entity);
|
||||
match &mut self.step {
|
||||
Step::PickTextPos { entity: slot, .. } | Step::EnterAngle { entity: slot, .. } => {
|
||||
*slot = Some(entity);
|
||||
}
|
||||
Step::PickDim => self.picked_entity = Some(entity),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dimension_line_endpoints(dimension: &Dimension) -> Option<(DVec3, DVec3)> {
|
||||
let (first, second, definition, axis) = match dimension {
|
||||
Dimension::Linear(value) => (
|
||||
value.first_point,
|
||||
value.second_point,
|
||||
value.definition_point,
|
||||
DVec3::new(value.rotation.cos(), value.rotation.sin(), 0.0),
|
||||
),
|
||||
Dimension::Aligned(value) => {
|
||||
let delta = DVec3::new(
|
||||
value.second_point.x - value.first_point.x,
|
||||
value.second_point.y - value.first_point.y,
|
||||
0.0,
|
||||
);
|
||||
if delta.length_squared() <= 1.0e-18 {
|
||||
return None;
|
||||
}
|
||||
(
|
||||
value.first_point,
|
||||
value.second_point,
|
||||
value.definition_point,
|
||||
delta.normalize(),
|
||||
)
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
let first = DVec3::new(first.x, first.y, first.z);
|
||||
let second = DVec3::new(second.x, second.y, second.z);
|
||||
let definition = DVec3::new(definition.x, definition.y, definition.z);
|
||||
let perpendicular = DVec3::new(-axis.y, axis.x, 0.0);
|
||||
let project = |point: DVec3| {
|
||||
point + perpendicular * (definition - point).dot(perpendicular)
|
||||
};
|
||||
let first = project(first);
|
||||
let second = project(second);
|
||||
if first.dot(axis) <= second.dot(axis) {
|
||||
Some((first, second))
|
||||
} else {
|
||||
Some((second, first))
|
||||
}
|
||||
}
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["DIMTED", "DIMTEDIT"] }); // DimTeditCommand
|
||||
fn apply_placement(dimension: &mut Dimension, placement: Placement) {
|
||||
if matches!(placement, Placement::Home) {
|
||||
let base = dimension.base_mut();
|
||||
base.text_middle_point = acadrust::types::Vector3::ZERO;
|
||||
base.insertion_point = acadrust::types::Vector3::ZERO;
|
||||
base.text_user_positioned = false;
|
||||
base.attachment_point = AttachmentPointType::MiddleCenter;
|
||||
return;
|
||||
}
|
||||
let Some((left, right)) = dimension_line_endpoints(dimension) else {
|
||||
return;
|
||||
};
|
||||
let (point, attachment) = match placement {
|
||||
Placement::Left => (left, AttachmentPointType::MiddleLeft),
|
||||
Placement::Right => (right, AttachmentPointType::MiddleRight),
|
||||
Placement::Center => ((left + right) * 0.5, AttachmentPointType::MiddleCenter),
|
||||
Placement::Home => unreachable!(),
|
||||
};
|
||||
let point = acadrust::types::Vector3::new(point.x, point.y, point.z);
|
||||
let base = dimension.base_mut();
|
||||
base.text_middle_point = point;
|
||||
base.insertion_point = point;
|
||||
base.text_user_positioned = true;
|
||||
base.attachment_point = attachment;
|
||||
}
|
||||
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["DIMTED", "DIMTEDIT"] });
|
||||
@@ -230,8 +230,10 @@ impl CadCommand for JoggedRadiusDimensionCommand {
|
||||
fn input_kind(&self) -> InputKind {
|
||||
if self.awaiting_text {
|
||||
InputKind::FreeText
|
||||
} else {
|
||||
} else if self.awaiting_angle {
|
||||
InputKind::SingleToken
|
||||
} else {
|
||||
InputKind::Point
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
// (DXF group 340); editing/erasing them stays in sync via that link.
|
||||
|
||||
use acadrust::entities::mtext::AttachmentPoint;
|
||||
use acadrust::entities::{Leader, LeaderCreationType, MText};
|
||||
use acadrust::entities::{Leader, LeaderCreationType, LeaderPathType, MText};
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::EntityType;
|
||||
use glam::{DVec3, Mat4, Vec3};
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind, WorkingPlane};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use crate::t;
|
||||
@@ -31,8 +31,19 @@ pub fn tool() -> ToolDef {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Step {
|
||||
Points,
|
||||
Format,
|
||||
Annotation,
|
||||
AnnotationOptions,
|
||||
}
|
||||
|
||||
pub struct LeaderCommand {
|
||||
verts: Vec<DVec3>,
|
||||
step: Step,
|
||||
path_type: LeaderPathType,
|
||||
arrow_enabled: bool,
|
||||
plane: WorkingPlane,
|
||||
dimension_style: String,
|
||||
text_style: String,
|
||||
@@ -41,6 +52,7 @@ pub struct LeaderCommand {
|
||||
gap: f64,
|
||||
arrow_size: f64,
|
||||
annotative: bool,
|
||||
annotation_lines: Vec<String>,
|
||||
}
|
||||
|
||||
impl LeaderCommand {
|
||||
@@ -56,6 +68,9 @@ impl LeaderCommand {
|
||||
|
||||
Self {
|
||||
verts: Vec::new(),
|
||||
step: Step::Points,
|
||||
path_type: LeaderPathType::StraightLine,
|
||||
arrow_enabled: true,
|
||||
plane: WorkingPlane::default(),
|
||||
dimension_style: defaults.style_name,
|
||||
text_style: defaults.text_style_name,
|
||||
@@ -64,10 +79,11 @@ impl LeaderCommand {
|
||||
gap: defaults.gap,
|
||||
arrow_size: defaults.arrow_size,
|
||||
annotative: defaults.annotative,
|
||||
annotation_lines: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(&self) -> CmdResult {
|
||||
fn finish(&self, annotation: Option<&str>, open_editor: bool) -> CmdResult {
|
||||
if self.verts.len() < 2 {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
@@ -91,7 +107,7 @@ impl LeaderCommand {
|
||||
let mut leader_points = local.clone();
|
||||
|
||||
let first = local[0];
|
||||
let elbow = local[1];
|
||||
let elbow = *local.last().unwrap_or(&first);
|
||||
let sign = if elbow.x >= first.x { 1.0 } else { -1.0 };
|
||||
|
||||
let landing_end = DVec3::new(
|
||||
@@ -102,7 +118,7 @@ impl LeaderCommand {
|
||||
|
||||
leader_points.push(landing_end);
|
||||
|
||||
let leader = build_leader(
|
||||
let mut leader = build_leader(
|
||||
&leader_points,
|
||||
Mat4::IDENTITY,
|
||||
&self.dimension_style,
|
||||
@@ -110,6 +126,8 @@ impl LeaderCommand {
|
||||
self.gap,
|
||||
self.arrow_size,
|
||||
);
|
||||
leader.path_type = self.path_type;
|
||||
leader.arrow_enabled = self.arrow_enabled;
|
||||
|
||||
// The MTEXT starts at the real end of the landing.
|
||||
let (anchor, attach) =
|
||||
@@ -120,8 +138,13 @@ impl LeaderCommand {
|
||||
// when another representation becomes active.
|
||||
let mtext_height = displayed_height;
|
||||
|
||||
if annotation.is_none() {
|
||||
leader.creation_type = LeaderCreationType::NoAnnotation;
|
||||
return CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Leader(leader)));
|
||||
}
|
||||
|
||||
let mtext = build_mtext(
|
||||
"",
|
||||
annotation.unwrap_or_default(),
|
||||
anchor,
|
||||
mtext_height,
|
||||
attach,
|
||||
@@ -136,6 +159,7 @@ impl LeaderCommand {
|
||||
self.plane.place_entity(EntityType::MText(mtext)),
|
||||
],
|
||||
edit_index: 1,
|
||||
open_editor,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,25 +174,138 @@ impl CadCommand for LeaderCommand {
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if self.verts.is_empty() {
|
||||
t!("LEADER Specify arrowhead point:").into_owned()
|
||||
if self.step == Step::Format {
|
||||
return t!("LEADER Enter leader formatting option [Spline/Straight/Arrow/None] <exit>:")
|
||||
.into_owned();
|
||||
}
|
||||
if self.step == Step::AnnotationOptions {
|
||||
return t!("LEADER Enter an annotation option [None/Mtext]:").into_owned();
|
||||
}
|
||||
if self.step == Step::Annotation {
|
||||
return if self.annotation_lines.is_empty() {
|
||||
t!("LEADER Enter first line of annotation text or <options>:").into_owned()
|
||||
} else {
|
||||
t!("LEADER Enter next line of annotation text:").into_owned()
|
||||
};
|
||||
}
|
||||
match self.verts.len() {
|
||||
0 => t!("LEADER Specify leader start point:").into_owned(),
|
||||
1 => t!("LEADER Specify next point:").into_owned(),
|
||||
_ => t!("LEADER Specify next point or [Annotation/Format/Undo] <Annotation>:")
|
||||
.into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
if self.step == Step::Format {
|
||||
return vec![
|
||||
CmdOption::new("Spline", "SPLINE"),
|
||||
CmdOption::new("Straight", "STRAIGHT"),
|
||||
CmdOption::new("Arrow", "ARROW"),
|
||||
CmdOption::new("None", "NONE"),
|
||||
];
|
||||
}
|
||||
if self.step == Step::AnnotationOptions {
|
||||
return vec![
|
||||
CmdOption::new("None", "NONE"),
|
||||
CmdOption::new("Mtext", "MTEXT"),
|
||||
];
|
||||
}
|
||||
if self.verts.len() >= 2 {
|
||||
vec![
|
||||
CmdOption::new("Annotation", "ANNOTATION"),
|
||||
CmdOption::new("Format", "FORMAT"),
|
||||
CmdOption::new("Undo", "UNDO"),
|
||||
]
|
||||
} else {
|
||||
t!("LEADER Specify landing point:").into_owned()
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn input_kind(&self) -> InputKind {
|
||||
if matches!(self.step, Step::Format | Step::AnnotationOptions) {
|
||||
InputKind::SingleToken
|
||||
} else if self.step == Step::Annotation {
|
||||
InputKind::FreeText
|
||||
} else {
|
||||
InputKind::Point
|
||||
}
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
self.step == Step::Points && self.verts.len() >= 2
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let keyword = text.trim().to_ascii_uppercase();
|
||||
if self.step == Step::Annotation {
|
||||
self.annotation_lines.push(text.to_string());
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
if self.step == Step::AnnotationOptions {
|
||||
return match keyword.as_str() {
|
||||
"N" | "NONE" => Some(self.finish(None, false)),
|
||||
"M" | "MTEXT" => Some(self.finish(Some(""), true)),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
if self.step == Step::Format {
|
||||
match keyword.as_str() {
|
||||
"S" | "SPLINE" => self.path_type = LeaderPathType::Spline,
|
||||
"ST" | "STRAIGHT" => self.path_type = LeaderPathType::StraightLine,
|
||||
"A" | "ARROW" => self.arrow_enabled = true,
|
||||
"N" | "NONE" => self.arrow_enabled = false,
|
||||
_ => return None,
|
||||
}
|
||||
self.step = Step::Points;
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
if self.verts.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
match keyword.as_str() {
|
||||
"A" | "ANNOTATION" => {
|
||||
self.step = Step::Annotation;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"F" | "FORMAT" => {
|
||||
self.step = Step::Format;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"U" | "UNDO" => {
|
||||
self.verts.pop();
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
self.verts.push(pt);
|
||||
|
||||
if self.verts.len() >= 2 {
|
||||
self.finish()
|
||||
} else {
|
||||
CmdResult::NeedPoint
|
||||
if self.step != Step::Points {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.verts.push(pt);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
self.finish()
|
||||
if self.step == Step::Format {
|
||||
self.step = Step::Points;
|
||||
CmdResult::NeedPoint
|
||||
} else if self.step == Step::Annotation {
|
||||
if self.annotation_lines.is_empty() {
|
||||
self.step = Step::AnnotationOptions;
|
||||
CmdResult::NeedPoint
|
||||
} else {
|
||||
let text = self.annotation_lines.join("\n");
|
||||
self.finish(Some(&text), false)
|
||||
}
|
||||
} else if self.step == Step::AnnotationOptions {
|
||||
CmdResult::NeedPoint
|
||||
} else {
|
||||
self.step = Step::Annotation;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
@@ -176,7 +313,7 @@ impl CadCommand for LeaderCommand {
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
if self.verts.is_empty() {
|
||||
if self.step != Step::Points || self.verts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut pts: Vec<Vec3> = self
|
||||
|
||||
@@ -6,7 +6,7 @@ use cadkernel::geom2d::{
|
||||
};
|
||||
|
||||
use crate::command::{
|
||||
CadCommand, CmdResult, DimensionAssociationInput, InputKind, WorkingPlane,
|
||||
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, InputKind, WorkingPlane,
|
||||
};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
@@ -224,11 +224,31 @@ impl CadCommand for LinearDimensionCommand {
|
||||
fn input_kind(&self) -> InputKind {
|
||||
if self.awaiting_text {
|
||||
InputKind::FreeText
|
||||
} else {
|
||||
} else if self.awaiting_angle || self.awaiting_rotation {
|
||||
InputKind::SingleToken
|
||||
} else {
|
||||
InputKind::Point
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
if !matches!(self.step, Step::DimensionLine { .. })
|
||||
|| self.awaiting_text
|
||||
|| self.awaiting_angle
|
||||
|| self.awaiting_rotation
|
||||
{
|
||||
return Vec::new();
|
||||
}
|
||||
vec![
|
||||
CmdOption::new("MText", "MTEXT"),
|
||||
CmdOption::new("Text", "TEXT"),
|
||||
CmdOption::new("Angle", "ANGLE"),
|
||||
CmdOption::new("Horizontal", "HORIZONTAL"),
|
||||
CmdOption::new("Vertical", "VERTICAL"),
|
||||
CmdOption::new("Rotated", "ROTATED"),
|
||||
]
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
// While typing the override text or angle, route input as a value, not
|
||||
// a point pick / keyword.
|
||||
|
||||
@@ -472,6 +472,8 @@ impl CadCommand for MLeaderCommand {
|
||||
fn input_kind(&self) -> InputKind {
|
||||
if matches!(self.step, Step::Layer | Step::PreEnterText) {
|
||||
InputKind::FreeText
|
||||
} else if matches!(self.step, Step::PickPoints | Step::SelectMText) {
|
||||
InputKind::Point
|
||||
} else {
|
||||
InputKind::SingleToken
|
||||
}
|
||||
|
||||
+36
-21
@@ -1,6 +1,7 @@
|
||||
// Annotate module — dimension, text, leader, table, and markup tools.
|
||||
|
||||
pub mod aligned_dim;
|
||||
pub mod annotation_scale;
|
||||
pub mod arc_length_dim;
|
||||
pub mod angular_dim;
|
||||
pub mod data_extract;
|
||||
@@ -205,29 +206,33 @@ impl CadModule for AnnotateModule {
|
||||
RibbonGroup {
|
||||
title: "Annotation Scaling",
|
||||
tools: vec![
|
||||
RibbonItem::Tool(crate::modules::ToolDef {
|
||||
id: "ANNOSCALE",
|
||||
label: "Scale List",
|
||||
icon: crate::modules::IconKind::Svg(include_bytes!(
|
||||
"../../../assets/icons/scale_list.svg"
|
||||
)),
|
||||
event: crate::modules::ModuleEvent::Command("ANNOSCALE".to_string()),
|
||||
}),
|
||||
RibbonItem::Tool(crate::modules::ToolDef {
|
||||
id: "OBJECTSCALE",
|
||||
label: "Add Scale",
|
||||
RibbonItem::LabeledDropdown {
|
||||
id: "ANNOTATION_CURRENT_SCALE",
|
||||
label: "Add Current Scale",
|
||||
icon: crate::modules::IconKind::Svg(include_bytes!(
|
||||
"../../../assets/icons/add_scale.svg"
|
||||
)),
|
||||
// The quick "mark annotative at the current scale"
|
||||
// action; bare OBJECTSCALE opens the manage dialog.
|
||||
event: crate::modules::ModuleEvent::Command(
|
||||
"OBJECTSCALE ADD".to_string(),
|
||||
),
|
||||
}),
|
||||
items: vec![
|
||||
(
|
||||
"OBJECTSCALE ADD",
|
||||
"Add Current Scale",
|
||||
crate::modules::IconKind::Svg(include_bytes!(
|
||||
"../../../assets/icons/add_scale.svg"
|
||||
)),
|
||||
),
|
||||
(
|
||||
"OBJECTSCALE DELETE",
|
||||
"Delete Current Scale",
|
||||
crate::modules::IconKind::Svg(include_bytes!(
|
||||
"../../../assets/icons/add_scale.svg"
|
||||
)),
|
||||
),
|
||||
],
|
||||
default: "OBJECTSCALE ADD",
|
||||
},
|
||||
RibbonItem::Tool(crate::modules::ToolDef {
|
||||
id: "SCALELISTEDIT",
|
||||
label: "Scale Edit",
|
||||
label: "Scale List",
|
||||
icon: crate::modules::IconKind::Svg(include_bytes!(
|
||||
"../../../assets/icons/scale_list.svg"
|
||||
)),
|
||||
@@ -236,13 +241,23 @@ impl CadModule for AnnotateModule {
|
||||
),
|
||||
}),
|
||||
RibbonItem::Tool(crate::modules::ToolDef {
|
||||
id: "SYNCPVIEWPORTS",
|
||||
label: "Sync Scales",
|
||||
id: "OBJECTSCALE",
|
||||
label: "Add/Delete Scales",
|
||||
icon: crate::modules::IconKind::Svg(include_bytes!(
|
||||
"../../../assets/icons/add_scale.svg"
|
||||
)),
|
||||
event: crate::modules::ModuleEvent::Command(
|
||||
"OBJECTSCALE".to_string(),
|
||||
),
|
||||
}),
|
||||
RibbonItem::Tool(crate::modules::ToolDef {
|
||||
id: "ANNORESET",
|
||||
label: "Sync Scale Positions",
|
||||
icon: crate::modules::IconKind::Svg(include_bytes!(
|
||||
"../../../assets/icons/sync.svg"
|
||||
)),
|
||||
event: crate::modules::ModuleEvent::Command(
|
||||
"SYNCPVIEWPORTS".to_string(),
|
||||
"ANNORESET".to_string(),
|
||||
),
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -246,6 +246,8 @@ impl CadCommand for MTextCommand {
|
||||
fn input_kind(&self) -> InputKind {
|
||||
if matches!(self.step, Step::Style) {
|
||||
InputKind::FreeText
|
||||
} else if matches!(self.step, Step::FirstCorner | Step::OppositeCorner) {
|
||||
InputKind::Point
|
||||
} else {
|
||||
InputKind::SingleToken
|
||||
}
|
||||
|
||||
@@ -142,8 +142,10 @@ impl CadCommand for OrdinateDimCommand {
|
||||
fn input_kind(&self) -> InputKind {
|
||||
if self.awaiting_text {
|
||||
InputKind::FreeText
|
||||
} else {
|
||||
} else if self.awaiting_angle {
|
||||
InputKind::SingleToken
|
||||
} else {
|
||||
InputKind::Point
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -137,8 +137,10 @@ impl CadCommand for RadiusDimensionCommand {
|
||||
fn input_kind(&self) -> InputKind {
|
||||
if self.awaiting_text {
|
||||
InputKind::FreeText
|
||||
} else {
|
||||
} else if self.awaiting_angle {
|
||||
InputKind::SingleToken
|
||||
} else {
|
||||
InputKind::Point
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// data rows. The command remembers its previous settings and supports either a
|
||||
// fixed insertion point or a two-corner sizing window.
|
||||
|
||||
use acadrust::entities::table::{CellStyle, CellStylePropertyFlags};
|
||||
use acadrust::entities::TableBuilder;
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::EntityType;
|
||||
@@ -101,9 +102,47 @@ pub struct TableCommand {
|
||||
preview_scale: f64,
|
||||
title_row: bool,
|
||||
header_row: bool,
|
||||
row_style_overrides: Option<[CellStyle; 3]>,
|
||||
preview_enabled: bool,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
fn cell_style_from_row(style: &acadrust::objects::RowCellStyle) -> CellStyle {
|
||||
let mut result = CellStyle::new();
|
||||
result.property_flags = CellStylePropertyFlags::DATA_TYPE
|
||||
| CellStylePropertyFlags::DATA_FORMAT
|
||||
| CellStylePropertyFlags::ALIGNMENT
|
||||
| CellStylePropertyFlags::CONTENT_COLOR
|
||||
| CellStylePropertyFlags::TEXT_STYLE
|
||||
| CellStylePropertyFlags::TEXT_HEIGHT
|
||||
| CellStylePropertyFlags::BACKGROUND_COLOR;
|
||||
result.content_color = style.text_color;
|
||||
result.text_style_handle = style.text_style_handle;
|
||||
result.text_style_name = style.text_style_name.clone();
|
||||
result.text_height = style.text_height;
|
||||
result.alignment = style.alignment as i32;
|
||||
result.background_color = style.fill_color;
|
||||
result.fill_enabled = style.fill_enabled;
|
||||
result.value_data_type = style.data_type;
|
||||
result.value_unit_type = style.unit_type;
|
||||
result.value_format = style.format_string.clone();
|
||||
result
|
||||
}
|
||||
|
||||
fn selected_row_style(
|
||||
table_style: &acadrust::objects::TableStyle,
|
||||
name: &str,
|
||||
) -> CellStyle {
|
||||
let style = if name.eq_ignore_ascii_case("Title") {
|
||||
&table_style.title_row_style
|
||||
} else if name.eq_ignore_ascii_case("Header") {
|
||||
&table_style.header_row_style
|
||||
} else {
|
||||
&table_style.data_row_style
|
||||
};
|
||||
cell_style_from_row(style)
|
||||
}
|
||||
|
||||
impl TableCommand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@@ -114,6 +153,8 @@ impl TableCommand {
|
||||
preview_scale: 1.0,
|
||||
title_row: true,
|
||||
header_row: true,
|
||||
row_style_overrides: None,
|
||||
preview_enabled: true,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
@@ -141,10 +182,60 @@ impl TableCommand {
|
||||
},
|
||||
title_row: !style.title_suppressed,
|
||||
header_row: !style.header_suppressed,
|
||||
row_style_overrides: None,
|
||||
preview_enabled: true,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the insertion command from the values accepted by the Insert
|
||||
/// Table dialog. The dialog owns validation, so the command starts at the
|
||||
/// point/window placement step instead of asking for the same values again.
|
||||
pub fn configured(
|
||||
style: Option<(acadrust::Handle, &acadrust::objects::TableStyle)>,
|
||||
annotation_multiplier: f64,
|
||||
columns: usize,
|
||||
data_rows: usize,
|
||||
column_width: f64,
|
||||
row_height: f64,
|
||||
use_window: bool,
|
||||
preview_enabled: bool,
|
||||
row_style_names: [&str; 3],
|
||||
) -> Self {
|
||||
let mut command = style.map_or_else(Self::new, |(handle, style)| {
|
||||
Self::with_style(handle, style, annotation_multiplier)
|
||||
});
|
||||
let insertion_mode = if use_window {
|
||||
InsertionMode::Window
|
||||
} else {
|
||||
InsertionMode::Point
|
||||
};
|
||||
if let Ok(mut saved) = saved_defaults().lock() {
|
||||
*saved = TableDefaults {
|
||||
columns,
|
||||
data_rows,
|
||||
column_width,
|
||||
row_height,
|
||||
insertion_mode,
|
||||
};
|
||||
}
|
||||
command.step = if use_window {
|
||||
Step::WindowFirst { columns, data_rows }
|
||||
} else {
|
||||
Step::Insertion {
|
||||
columns,
|
||||
data_rows,
|
||||
column_width,
|
||||
row_height,
|
||||
}
|
||||
};
|
||||
command.preview_enabled = preview_enabled;
|
||||
command.row_style_overrides = style.map(|(_, table_style)| {
|
||||
row_style_names.map(|name| selected_row_style(table_style, name))
|
||||
});
|
||||
command
|
||||
}
|
||||
|
||||
fn defaults(&self) -> TableDefaults {
|
||||
let mut defaults = saved_defaults().lock().map(|value| *value).unwrap_or_default();
|
||||
if (defaults.column_width - DEFAULT_COL_WIDTH).abs() < 1.0e-9 {
|
||||
@@ -175,6 +266,11 @@ impl TableCommand {
|
||||
.column_width(column_width)
|
||||
.build();
|
||||
table.table_style_handle = self.style_handle;
|
||||
if let Some(styles) = &self.row_style_overrides {
|
||||
for (row_index, row) in table.rows.iter_mut().enumerate() {
|
||||
row.style = Some(styles[row_index.min(2)].clone());
|
||||
}
|
||||
}
|
||||
self.plane.place_entity(EntityType::Table(table))
|
||||
}
|
||||
|
||||
@@ -447,6 +543,9 @@ impl CadCommand for TableCommand {
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
|
||||
if !self.preview_enabled {
|
||||
return None;
|
||||
}
|
||||
match self.step {
|
||||
Step::Insertion {
|
||||
columns,
|
||||
|
||||
@@ -9,7 +9,7 @@ use cadkernel::space::{PlanarCurve, Plane, Vec3};
|
||||
use glam::DVec3;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane};
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind, WorkingPlane};
|
||||
use crate::entities::curve::{curve_points, entity_curve, ocs_plane};
|
||||
use crate::entities::lwpolyline::{is_revision_cloud, revision_cloud_from_curve};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
@@ -666,6 +666,17 @@ impl CadCommand for RevCloudCommand {
|
||||
true
|
||||
}
|
||||
|
||||
fn input_kind(&self) -> InputKind {
|
||||
match self.stage {
|
||||
Stage::ArcLength | Stage::Style | Stage::Reverse(_) => InputKind::SingleToken,
|
||||
Stage::Create
|
||||
| Stage::Object
|
||||
| Stage::ModifySelect
|
||||
| Stage::ModifyDraw(_)
|
||||
| Stage::ModifyErase(_) => InputKind::Point,
|
||||
}
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
matches!(self.stage, Stage::Create | Stage::ModifyDraw(_))
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use cadkernel::space::Plane;
|
||||
use glam::DVec3;
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane};
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind, WorkingPlane};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
|
||||
@@ -270,6 +270,15 @@ impl CadCommand for WipeoutCommand {
|
||||
)
|
||||
}
|
||||
|
||||
fn input_kind(&self) -> InputKind {
|
||||
match self.mode {
|
||||
WipeoutMode::Frames | WipeoutMode::ErasePolyline => InputKind::SingleToken,
|
||||
WipeoutMode::Draw | WipeoutMode::Polyline | WipeoutMode::Rectangular => {
|
||||
InputKind::Point
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
matches!(
|
||||
self.mode,
|
||||
|
||||
Reference in new issue
Block a user