cargo fmt
This commit is contained in:
parent
468cbde5e1
commit
ca07268c7c
204 changed files with 13988 additions and 5516 deletions
File diff suppressed because it is too large
Load diff
2516
src/app/commands.rs
2516
src/app/commands.rs
File diff suppressed because it is too large
Load diff
|
|
@ -1,15 +1,15 @@
|
|||
use crate::scene::Scene;
|
||||
use crate::ui::{LayerPanel, PropertiesPanel};
|
||||
use crate::command::CadCommand;
|
||||
use crate::snap::SnapResult;
|
||||
use crate::linetypes;
|
||||
use crate::modules::home::modify::refedit::RefEditSession;
|
||||
use crate::scene::grip::GripEdit;
|
||||
use crate::scene::GripDef;
|
||||
use crate::modules::home::modify::refedit::RefEditSession;
|
||||
use acadrust::{CadDocument, Handle};
|
||||
use crate::scene::Scene;
|
||||
use crate::snap::SnapResult;
|
||||
use crate::ui::{LayerPanel, PropertiesPanel};
|
||||
use acadrust::tables::Ucs;
|
||||
use crate::linetypes;
|
||||
use std::path::PathBuf;
|
||||
use acadrust::{CadDocument, Handle};
|
||||
use iced;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// ── Per-document tab state ─────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::ui::overlay::GridPlane;
|
||||
use crate::scene::WireModel;
|
||||
use crate::ui::overlay::GridPlane;
|
||||
use acadrust::tables::Ucs;
|
||||
|
||||
// ── Coordinate parsing ─────────────────────────────────────────────────────
|
||||
|
|
@ -27,9 +27,21 @@ pub(super) fn parse_coord(text: &str) -> Option<glam::Vec3> {
|
|||
///
|
||||
/// WCS = origin + x_axis*u + y_axis*v + z_axis*w
|
||||
pub(super) fn ucs_to_wcs(pt: glam::Vec3, ucs: &Ucs) -> glam::Vec3 {
|
||||
let o = glam::Vec3::new(ucs.origin.x as f32, ucs.origin.y as f32, ucs.origin.z as f32);
|
||||
let x = glam::Vec3::new(ucs.x_axis.x as f32, ucs.x_axis.y as f32, ucs.x_axis.z as f32);
|
||||
let y = glam::Vec3::new(ucs.y_axis.x as f32, ucs.y_axis.y as f32, ucs.y_axis.z as f32);
|
||||
let o = glam::Vec3::new(
|
||||
ucs.origin.x as f32,
|
||||
ucs.origin.y as f32,
|
||||
ucs.origin.z as f32,
|
||||
);
|
||||
let x = glam::Vec3::new(
|
||||
ucs.x_axis.x as f32,
|
||||
ucs.x_axis.y as f32,
|
||||
ucs.x_axis.z as f32,
|
||||
);
|
||||
let y = glam::Vec3::new(
|
||||
ucs.y_axis.x as f32,
|
||||
ucs.y_axis.y as f32,
|
||||
ucs.y_axis.z as f32,
|
||||
);
|
||||
let z_ax = ucs_z_axis(ucs);
|
||||
o + x * pt.x + y * pt.y + z_ax * pt.z
|
||||
}
|
||||
|
|
@ -37,9 +49,21 @@ pub(super) fn ucs_to_wcs(pt: glam::Vec3, ucs: &Ucs) -> glam::Vec3 {
|
|||
/// Convert a WCS point back to UCS local coordinates.
|
||||
#[allow(dead_code)]
|
||||
pub(super) fn wcs_to_ucs(pt: glam::Vec3, ucs: &Ucs) -> glam::Vec3 {
|
||||
let o = glam::Vec3::new(ucs.origin.x as f32, ucs.origin.y as f32, ucs.origin.z as f32);
|
||||
let x = glam::Vec3::new(ucs.x_axis.x as f32, ucs.x_axis.y as f32, ucs.x_axis.z as f32);
|
||||
let y = glam::Vec3::new(ucs.y_axis.x as f32, ucs.y_axis.y as f32, ucs.y_axis.z as f32);
|
||||
let o = glam::Vec3::new(
|
||||
ucs.origin.x as f32,
|
||||
ucs.origin.y as f32,
|
||||
ucs.origin.z as f32,
|
||||
);
|
||||
let x = glam::Vec3::new(
|
||||
ucs.x_axis.x as f32,
|
||||
ucs.x_axis.y as f32,
|
||||
ucs.x_axis.z as f32,
|
||||
);
|
||||
let y = glam::Vec3::new(
|
||||
ucs.y_axis.x as f32,
|
||||
ucs.y_axis.y as f32,
|
||||
ucs.y_axis.z as f32,
|
||||
);
|
||||
let z_ax = ucs_z_axis(ucs);
|
||||
let d = pt - o;
|
||||
glam::Vec3::new(d.dot(x), d.dot(y), d.dot(z_ax))
|
||||
|
|
@ -47,8 +71,16 @@ pub(super) fn wcs_to_ucs(pt: glam::Vec3, ucs: &Ucs) -> glam::Vec3 {
|
|||
|
||||
/// Return the normalised Z axis of a UCS (cross product of X and Y axes).
|
||||
pub(super) fn ucs_z_axis(ucs: &Ucs) -> glam::Vec3 {
|
||||
let x = glam::Vec3::new(ucs.x_axis.x as f32, ucs.x_axis.y as f32, ucs.x_axis.z as f32);
|
||||
let y = glam::Vec3::new(ucs.y_axis.x as f32, ucs.y_axis.y as f32, ucs.y_axis.z as f32);
|
||||
let x = glam::Vec3::new(
|
||||
ucs.x_axis.x as f32,
|
||||
ucs.x_axis.y as f32,
|
||||
ucs.x_axis.z as f32,
|
||||
);
|
||||
let y = glam::Vec3::new(
|
||||
ucs.y_axis.x as f32,
|
||||
ucs.y_axis.y as f32,
|
||||
ucs.y_axis.z as f32,
|
||||
);
|
||||
x.cross(y).normalize_or_zero()
|
||||
}
|
||||
|
||||
|
|
@ -57,9 +89,7 @@ pub(super) fn ucs_rotated_z(origin: glam::Vec3, angle_z: f32) -> Ucs {
|
|||
let cos = angle_z.cos() as f64;
|
||||
let sin = angle_z.sin() as f64;
|
||||
let mut ucs = Ucs::new("*ACTIVE*");
|
||||
ucs.origin = acadrust::types::Vector3::new(
|
||||
origin.x as f64, origin.y as f64, origin.z as f64,
|
||||
);
|
||||
ucs.origin = acadrust::types::Vector3::new(origin.x as f64, origin.y as f64, origin.z as f64);
|
||||
ucs.x_axis = acadrust::types::Vector3::new(cos, sin, 0.0);
|
||||
ucs.y_axis = acadrust::types::Vector3::new(-sin, cos, 0.0);
|
||||
ucs
|
||||
|
|
@ -134,7 +164,11 @@ pub(super) fn entities_centroid(wires: &[WireModel]) -> glam::Vec3 {
|
|||
count += 1;
|
||||
}
|
||||
}
|
||||
if count > 0 { sum / count as f32 } else { glam::Vec3::ZERO }
|
||||
if count > 0 {
|
||||
sum / count as f32
|
||||
} else {
|
||||
glam::Vec3::ZERO
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the next available auto group name ("*A1", "*A2", …).
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use super::{H7CAD, document::HistorySnapshot};
|
||||
use super::{document::HistorySnapshot, H7CAD};
|
||||
use std::collections::HashSet;
|
||||
|
||||
impl H7CAD {
|
||||
|
|
@ -10,7 +10,11 @@ impl H7CAD {
|
|||
.unwrap_or_else(|| fallback.to_string())
|
||||
}
|
||||
|
||||
pub(super) fn capture_history_snapshot(&self, i: usize, label: impl Into<String>) -> HistorySnapshot {
|
||||
pub(super) fn capture_history_snapshot(
|
||||
&self,
|
||||
i: usize,
|
||||
label: impl Into<String>,
|
||||
) -> HistorySnapshot {
|
||||
HistorySnapshot {
|
||||
document: self.tabs[i].scene.document.clone(),
|
||||
current_layout: self.tabs[i].scene.current_layout.clone(),
|
||||
|
|
@ -45,7 +49,9 @@ impl H7CAD {
|
|||
self.tabs[i].dirty = snapshot.dirty;
|
||||
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
||||
let vp_info = self.tabs[i].scene.viewport_list();
|
||||
self.tabs[i].layers.sync_with_viewports(&doc_layers, vp_info);
|
||||
self.tabs[i]
|
||||
.layers
|
||||
.sync_with_viewports(&doc_layers, vp_info);
|
||||
self.sync_ribbon_layers();
|
||||
self.refresh_properties();
|
||||
}
|
||||
|
|
@ -78,7 +84,8 @@ impl H7CAD {
|
|||
self.restore_history_snapshot(i, snapshot);
|
||||
last_label = label;
|
||||
}
|
||||
self.command_line.push_output(&format!("Undo: {last_label}"));
|
||||
self.command_line
|
||||
.push_output(&format!("Undo: {last_label}"));
|
||||
}
|
||||
|
||||
pub(super) fn redo_steps(&mut self, steps: usize) {
|
||||
|
|
@ -101,10 +108,15 @@ impl H7CAD {
|
|||
self.restore_history_snapshot(i, snapshot);
|
||||
last_label = label;
|
||||
}
|
||||
self.command_line.push_output(&format!("Redo: {last_label}"));
|
||||
self.command_line
|
||||
.push_output(&format!("Redo: {last_label}"));
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn history_dropdown_labels(stack: &[HistorySnapshot]) -> Vec<String> {
|
||||
stack.iter().rev().map(|snapshot| snapshot.label.clone()).collect()
|
||||
stack
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|snapshot| snapshot.label.clone())
|
||||
.collect()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,11 @@ impl H7CAD {
|
|||
})
|
||||
.collect();
|
||||
let names: Vec<String> = infos.iter().map(|l| l.name.clone()).collect();
|
||||
let active = if names.contains(&active) { active } else { "0".to_string() };
|
||||
let active = if names.contains(&active) {
|
||||
active
|
||||
} else {
|
||||
"0".to_string()
|
||||
};
|
||||
self.tabs[i].active_layer = active.clone();
|
||||
self.tabs[i].layers.current_layer = active.clone();
|
||||
self.ribbon.set_layers(infos, &active);
|
||||
|
|
@ -48,44 +52,74 @@ impl H7CAD {
|
|||
|
||||
let text_names: Vec<String> = doc.text_styles.iter().map(|s| s.name.clone()).collect();
|
||||
let active_text = doc.header.current_text_style_name.clone();
|
||||
let active_text = if text_names.contains(&active_text) { active_text } else {
|
||||
text_names.first().cloned().unwrap_or_else(|| "Standard".to_string())
|
||||
let active_text = if text_names.contains(&active_text) {
|
||||
active_text
|
||||
} else {
|
||||
text_names
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "Standard".to_string())
|
||||
};
|
||||
|
||||
let dim_names: Vec<String> = doc.dim_styles.iter().map(|s| s.name.clone()).collect();
|
||||
let active_dim = doc.header.current_dimstyle_name.clone();
|
||||
let active_dim = if dim_names.contains(&active_dim) { active_dim } else {
|
||||
dim_names.first().cloned().unwrap_or_else(|| "Standard".to_string())
|
||||
let active_dim = if dim_names.contains(&active_dim) {
|
||||
active_dim
|
||||
} else {
|
||||
dim_names
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "Standard".to_string())
|
||||
};
|
||||
|
||||
let mleader_names: Vec<String> = doc.objects.values().filter_map(|o| {
|
||||
if let acadrust::objects::ObjectType::MultiLeaderStyle(mls) = o {
|
||||
Some(mls.name.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}).collect();
|
||||
let mleader_names: Vec<String> = doc
|
||||
.objects
|
||||
.values()
|
||||
.filter_map(|o| {
|
||||
if let acadrust::objects::ObjectType::MultiLeaderStyle(mls) = o {
|
||||
Some(mls.name.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let active_mleader = self.tabs[i].active_mleader_style.clone();
|
||||
let active_mleader = if mleader_names.contains(&active_mleader) { active_mleader } else {
|
||||
mleader_names.first().cloned().unwrap_or_else(|| "Standard".to_string())
|
||||
let active_mleader = if mleader_names.contains(&active_mleader) {
|
||||
active_mleader
|
||||
} else {
|
||||
mleader_names
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "Standard".to_string())
|
||||
};
|
||||
|
||||
let table_names: Vec<String> = doc.objects.values().filter_map(|o| {
|
||||
if let acadrust::objects::ObjectType::TableStyle(ts) = o {
|
||||
Some(ts.name.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}).collect();
|
||||
let active_table = table_names.first().cloned().unwrap_or_else(|| "Standard".to_string());
|
||||
let table_names: Vec<String> = doc
|
||||
.objects
|
||||
.values()
|
||||
.filter_map(|o| {
|
||||
if let acadrust::objects::ObjectType::TableStyle(ts) = o {
|
||||
Some(ts.name.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let active_table = table_names
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "Standard".to_string());
|
||||
|
||||
let active_mleader2 = active_mleader.clone();
|
||||
let active_table2 = active_table.clone();
|
||||
self.ribbon.set_styles(
|
||||
text_names, &active_text,
|
||||
dim_names, &active_dim,
|
||||
mleader_names, &active_mleader2,
|
||||
table_names, &active_table2,
|
||||
text_names,
|
||||
&active_text,
|
||||
dim_names,
|
||||
&active_dim,
|
||||
mleader_names,
|
||||
&active_mleader2,
|
||||
table_names,
|
||||
&active_table2,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
214
src/app/mod.rs
214
src/app/mod.rs
|
|
@ -1,21 +1,21 @@
|
|||
mod cmd_result;
|
||||
mod commands;
|
||||
mod document;
|
||||
mod helpers;
|
||||
mod history;
|
||||
mod properties;
|
||||
mod layers;
|
||||
mod commands;
|
||||
mod cmd_result;
|
||||
mod view;
|
||||
mod properties;
|
||||
mod update;
|
||||
mod view;
|
||||
|
||||
use document::DocumentTab;
|
||||
|
||||
use acadrust::types::{Color as AcadColor, LineWeight};
|
||||
use acadrust::CadDocument;
|
||||
use crate::modules::ModuleEvent;
|
||||
use crate::scene::CubeRegion;
|
||||
use crate::snap::Snapper;
|
||||
use crate::ui::{AppMenu, CommandLine, Ribbon, StatusBar};
|
||||
use acadrust::types::{Color as AcadColor, LineWeight};
|
||||
use acadrust::CadDocument;
|
||||
|
||||
use iced::time::Instant;
|
||||
use iced::window;
|
||||
|
|
@ -71,15 +71,15 @@ pub(super) struct H7CAD {
|
|||
/// OS window Id of the primary application window.
|
||||
main_window: Option<window::Id>,
|
||||
// ── Floating panel windows ────────────────────────────────────────────
|
||||
page_setup_window: Option<window::Id>,
|
||||
textstyle_window: Option<window::Id>,
|
||||
tablestyle_window: Option<window::Id>,
|
||||
mlstyle_window: Option<window::Id>,
|
||||
layout_manager_window: Option<window::Id>,
|
||||
plotstyle_window: Option<window::Id>,
|
||||
dimstyle_window: Option<window::Id>,
|
||||
shortcuts_window: Option<window::Id>,
|
||||
about_window: Option<window::Id>,
|
||||
page_setup_window: Option<window::Id>,
|
||||
textstyle_window: Option<window::Id>,
|
||||
tablestyle_window: Option<window::Id>,
|
||||
mlstyle_window: Option<window::Id>,
|
||||
layout_manager_window: Option<window::Id>,
|
||||
plotstyle_window: Option<window::Id>,
|
||||
dimstyle_window: Option<window::Id>,
|
||||
shortcuts_window: Option<window::Id>,
|
||||
about_window: Option<window::Id>,
|
||||
/// In-memory clipboard: cloned entities waiting to be pasted.
|
||||
clipboard: Vec<acadrust::EntityType>,
|
||||
/// Centroid of the clipboard entities (XZ plane, Y-up).
|
||||
|
|
@ -174,18 +174,34 @@ pub(super) struct H7CAD {
|
|||
/// Active tab: 0=Lines, 1=Arrows, 2=Text, 3=Scale/Units, 4=Tolerances.
|
||||
dimstyle_tab: u8,
|
||||
// Edit buffers (strings while typing):
|
||||
ds_dimdle: String, ds_dimdli: String, ds_dimgap: String,
|
||||
ds_dimexe: String, ds_dimexo: String,
|
||||
ds_dimsd1: bool, ds_dimsd2: bool,
|
||||
ds_dimse1: bool, ds_dimse2: bool,
|
||||
ds_dimasz: String, ds_dimcen: String, ds_dimtsz: String,
|
||||
ds_dimtxt: String, ds_dimtxsty: String, ds_dimtad: String,
|
||||
ds_dimtih: bool, ds_dimtoh: bool,
|
||||
ds_dimscale: String, ds_dimlfac: String,
|
||||
ds_dimlunit: String, ds_dimdec: String, ds_dimpost: String,
|
||||
ds_dimtol: bool, ds_dimlim: bool,
|
||||
ds_dimtp: String, ds_dimtm: String,
|
||||
ds_dimtdec: String, ds_dimtfac: String,
|
||||
ds_dimdle: String,
|
||||
ds_dimdli: String,
|
||||
ds_dimgap: String,
|
||||
ds_dimexe: String,
|
||||
ds_dimexo: String,
|
||||
ds_dimsd1: bool,
|
||||
ds_dimsd2: bool,
|
||||
ds_dimse1: bool,
|
||||
ds_dimse2: bool,
|
||||
ds_dimasz: String,
|
||||
ds_dimcen: String,
|
||||
ds_dimtsz: String,
|
||||
ds_dimtxt: String,
|
||||
ds_dimtxsty: String,
|
||||
ds_dimtad: String,
|
||||
ds_dimtih: bool,
|
||||
ds_dimtoh: bool,
|
||||
ds_dimscale: String,
|
||||
ds_dimlfac: String,
|
||||
ds_dimlunit: String,
|
||||
ds_dimdec: String,
|
||||
ds_dimpost: String,
|
||||
ds_dimtol: bool,
|
||||
ds_dimlim: bool,
|
||||
ds_dimtp: String,
|
||||
ds_dimtm: String,
|
||||
ds_dimtdec: String,
|
||||
ds_dimtfac: String,
|
||||
}
|
||||
|
||||
/// What triggered the "unsaved changes" dialog.
|
||||
|
|
@ -200,12 +216,34 @@ pub(super) enum PendingClose {
|
|||
/// Identifies a DimStyle field that can be edited in the dialog.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum DsField {
|
||||
Dimdle, Dimdli, Dimgap, Dimexe, Dimexo,
|
||||
Dimsd1, Dimsd2, Dimse1, Dimse2,
|
||||
Dimasz, Dimcen, Dimtsz,
|
||||
Dimtxt, Dimtxsty, Dimtad, Dimtih, Dimtoh,
|
||||
Dimscale, Dimlfac, Dimlunit, Dimdec, Dimpost,
|
||||
Dimtol, Dimlim, Dimtp, Dimtm, Dimtdec, Dimtfac,
|
||||
Dimdle,
|
||||
Dimdli,
|
||||
Dimgap,
|
||||
Dimexe,
|
||||
Dimexo,
|
||||
Dimsd1,
|
||||
Dimsd2,
|
||||
Dimse1,
|
||||
Dimse2,
|
||||
Dimasz,
|
||||
Dimcen,
|
||||
Dimtsz,
|
||||
Dimtxt,
|
||||
Dimtxsty,
|
||||
Dimtad,
|
||||
Dimtih,
|
||||
Dimtoh,
|
||||
Dimscale,
|
||||
Dimlfac,
|
||||
Dimlunit,
|
||||
Dimdec,
|
||||
Dimpost,
|
||||
Dimtol,
|
||||
Dimlim,
|
||||
Dimtp,
|
||||
Dimtm,
|
||||
Dimtdec,
|
||||
Dimtfac,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -372,7 +410,10 @@ pub enum Message {
|
|||
/// User changed the active lineweight in the Properties toolbar.
|
||||
RibbonLineweightChanged(LineWeight),
|
||||
/// User selected a style from a style combobox in the ribbon.
|
||||
RibbonStyleChanged { key: crate::modules::StyleKey, name: String },
|
||||
RibbonStyleChanged {
|
||||
key: crate::modules::StyleKey,
|
||||
name: String,
|
||||
},
|
||||
|
||||
// ── Properties panel ──────────────────────────────────────────────────
|
||||
/// User selected a layer from the layer pick_list in the Properties panel.
|
||||
|
|
@ -524,7 +565,10 @@ pub enum Message {
|
|||
TextStyleDialogNew,
|
||||
TextStyleDialogDelete,
|
||||
/// Edit a string field (FontFile / Width / Oblique).
|
||||
TextStyleEdit { field: &'static str, value: String },
|
||||
TextStyleEdit {
|
||||
field: &'static str,
|
||||
value: String,
|
||||
},
|
||||
/// Commit edits to the selected text style.
|
||||
TextStyleApply,
|
||||
/// Select a font from the built-in font list.
|
||||
|
|
@ -631,15 +675,15 @@ impl H7CAD {
|
|||
last_point: None,
|
||||
layer_window: None,
|
||||
main_window: None,
|
||||
page_setup_window: None,
|
||||
textstyle_window: None,
|
||||
tablestyle_window: None,
|
||||
mlstyle_window: None,
|
||||
page_setup_window: None,
|
||||
textstyle_window: None,
|
||||
tablestyle_window: None,
|
||||
mlstyle_window: None,
|
||||
layout_manager_window: None,
|
||||
plotstyle_window: None,
|
||||
dimstyle_window: None,
|
||||
shortcuts_window: None,
|
||||
about_window: None,
|
||||
plotstyle_window: None,
|
||||
dimstyle_window: None,
|
||||
shortcuts_window: None,
|
||||
about_window: None,
|
||||
clipboard: Vec::new(),
|
||||
clipboard_centroid: glam::Vec3::ZERO,
|
||||
layout_context_menu: None,
|
||||
|
|
@ -690,22 +734,34 @@ impl H7CAD {
|
|||
// DimStyle dialog
|
||||
dimstyle_selected: "Standard".to_string(),
|
||||
dimstyle_tab: 0,
|
||||
ds_dimdle: "0".to_string(), ds_dimdli: "3.75".to_string(),
|
||||
ds_dimgap: "0.625".to_string(), ds_dimexe: "1.25".to_string(),
|
||||
ds_dimdle: "0".to_string(),
|
||||
ds_dimdli: "3.75".to_string(),
|
||||
ds_dimgap: "0.625".to_string(),
|
||||
ds_dimexe: "1.25".to_string(),
|
||||
ds_dimexo: "0.625".to_string(),
|
||||
ds_dimsd1: false, ds_dimsd2: false,
|
||||
ds_dimse1: false, ds_dimse2: false,
|
||||
ds_dimasz: "0.18".to_string(), ds_dimcen: "0.09".to_string(),
|
||||
ds_dimsd1: false,
|
||||
ds_dimsd2: false,
|
||||
ds_dimse1: false,
|
||||
ds_dimse2: false,
|
||||
ds_dimasz: "0.18".to_string(),
|
||||
ds_dimcen: "0.09".to_string(),
|
||||
ds_dimtsz: "0".to_string(),
|
||||
ds_dimtxt: "0.18".to_string(), ds_dimtxsty: "Standard".to_string(),
|
||||
ds_dimtxt: "0.18".to_string(),
|
||||
ds_dimtxsty: "Standard".to_string(),
|
||||
ds_dimtad: "1".to_string(),
|
||||
ds_dimtih: false, ds_dimtoh: false,
|
||||
ds_dimscale: "1".to_string(), ds_dimlfac: "1".to_string(),
|
||||
ds_dimlunit: "2".to_string(), ds_dimdec: "2".to_string(),
|
||||
ds_dimtih: false,
|
||||
ds_dimtoh: false,
|
||||
ds_dimscale: "1".to_string(),
|
||||
ds_dimlfac: "1".to_string(),
|
||||
ds_dimlunit: "2".to_string(),
|
||||
ds_dimdec: "2".to_string(),
|
||||
ds_dimpost: "<>".to_string(),
|
||||
ds_dimtol: false, ds_dimlim: false,
|
||||
ds_dimtp: "0".to_string(), ds_dimtm: "0".to_string(),
|
||||
ds_dimtdec: "2".to_string(), ds_dimtfac: "1".to_string(),
|
||||
ds_dimtol: false,
|
||||
ds_dimlim: false,
|
||||
ds_dimtp: "0".to_string(),
|
||||
ds_dimtm: "0".to_string(),
|
||||
ds_dimtdec: "2".to_string(),
|
||||
ds_dimtfac: "1".to_string(),
|
||||
};
|
||||
app.sync_ribbon_layers();
|
||||
app
|
||||
|
|
@ -735,18 +791,42 @@ pub fn run() -> iced::Result {
|
|||
iced::daemon(H7CAD::boot, H7CAD::update, H7CAD::view)
|
||||
.subscription(H7CAD::subscription)
|
||||
.title(|state: &H7CAD, window_id: window::Id| {
|
||||
if Some(window_id) == state.layer_window { return "Layer Properties Manager".into(); }
|
||||
if Some(window_id) == state.page_setup_window { return "Page Setup".into(); }
|
||||
if Some(window_id) == state.textstyle_window { return "Text Style".into(); }
|
||||
if Some(window_id) == state.tablestyle_window { return "Table Style".into(); }
|
||||
if Some(window_id) == state.mlstyle_window { return "Multiline Style".into(); }
|
||||
if Some(window_id) == state.layout_manager_window { return "Layout Manager".into(); }
|
||||
if Some(window_id) == state.plotstyle_window { return "Plot Style Table Editor".into(); }
|
||||
if Some(window_id) == state.dimstyle_window { return "Dimension Style Manager".into(); }
|
||||
if Some(window_id) == state.shortcuts_window { return "Keyboard Shortcuts".into(); }
|
||||
if Some(window_id) == state.about_window { return "About H7CAD".into(); }
|
||||
if Some(window_id) == state.unsaved_dialog_window { return "Unsaved Changes".into(); }
|
||||
if Some(window_id) == state.save_dialog_window { return "Save As".into(); }
|
||||
if Some(window_id) == state.layer_window {
|
||||
return "Layer Properties Manager".into();
|
||||
}
|
||||
if Some(window_id) == state.page_setup_window {
|
||||
return "Page Setup".into();
|
||||
}
|
||||
if Some(window_id) == state.textstyle_window {
|
||||
return "Text Style".into();
|
||||
}
|
||||
if Some(window_id) == state.tablestyle_window {
|
||||
return "Table Style".into();
|
||||
}
|
||||
if Some(window_id) == state.mlstyle_window {
|
||||
return "Multiline Style".into();
|
||||
}
|
||||
if Some(window_id) == state.layout_manager_window {
|
||||
return "Layout Manager".into();
|
||||
}
|
||||
if Some(window_id) == state.plotstyle_window {
|
||||
return "Plot Style Table Editor".into();
|
||||
}
|
||||
if Some(window_id) == state.dimstyle_window {
|
||||
return "Dimension Style Manager".into();
|
||||
}
|
||||
if Some(window_id) == state.shortcuts_window {
|
||||
return "Keyboard Shortcuts".into();
|
||||
}
|
||||
if Some(window_id) == state.about_window {
|
||||
return "About H7CAD".into();
|
||||
}
|
||||
if Some(window_id) == state.unsaved_dialog_window {
|
||||
return "Unsaved Changes".into();
|
||||
}
|
||||
if Some(window_id) == state.save_dialog_window {
|
||||
return "Save As".into();
|
||||
}
|
||||
if let Some(tab) = state.tabs.get(state.active_tab) {
|
||||
let dot = if tab.dirty { "● " } else { "" };
|
||||
let name = tab.tab_display_name();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use super::helpers::{entity_type_key, entity_type_label, title_case_word};
|
||||
use super::{H7CAD, VARIES_LABEL};
|
||||
use super::helpers::{entity_type_label, entity_type_key, title_case_word};
|
||||
use crate::linetypes;
|
||||
use crate::scene::dispatch;
|
||||
use crate::ui;
|
||||
use crate::linetypes;
|
||||
use acadrust::{EntityType, Handle};
|
||||
|
||||
impl H7CAD {
|
||||
|
|
@ -62,28 +62,41 @@ impl H7CAD {
|
|||
.frozen_layers
|
||||
.iter()
|
||||
.filter_map(|&h| {
|
||||
self.tabs[i].scene.document.layers.iter()
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layers
|
||||
.iter()
|
||||
.find(|l| l.handle == h)
|
||||
.map(|l| l.name.clone())
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Collect available UCS names for the name picker.
|
||||
let ucs_names: Vec<String> = self.tabs[i].scene.document.ucss
|
||||
let ucs_names: Vec<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.ucss
|
||||
.iter()
|
||||
.map(|u| u.name.clone())
|
||||
.filter(|n| !n.is_empty())
|
||||
.collect();
|
||||
|
||||
// Current UCS name (resolved from vp.ucs_handle).
|
||||
let current_ucs = self.tabs[i].scene.document.ucss
|
||||
let current_ucs = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.ucss
|
||||
.iter()
|
||||
.find(|u| u.handle == vp.ucs_handle)
|
||||
.map(|u| u.name.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
// Collect available named view names.
|
||||
let view_names: Vec<String> = self.tabs[i].scene.document.views
|
||||
let view_names: Vec<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.views
|
||||
.iter()
|
||||
.map(|v| v.name.clone())
|
||||
.filter(|n| !n.is_empty())
|
||||
|
|
@ -122,7 +135,10 @@ impl H7CAD {
|
|||
|
||||
// Inject DimStyle picker for Dimension entities.
|
||||
if let acadrust::EntityType::Dimension(_) = entity {
|
||||
let dim_style_names: Vec<String> = self.tabs[i].scene.document.dim_styles
|
||||
let dim_style_names: Vec<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.dim_styles
|
||||
.iter()
|
||||
.map(|s| s.name.clone())
|
||||
.filter(|n| !n.is_empty())
|
||||
|
|
@ -132,7 +148,9 @@ impl H7CAD {
|
|||
// replace/upgrade it to a Choice if we have a list.
|
||||
if let Some(geom) = sections.last_mut() {
|
||||
// Find and replace the style_name EditText with a Choice.
|
||||
if let Some(prop) = geom.props.iter_mut().find(|p| p.field == "style_name") {
|
||||
if let Some(prop) =
|
||||
geom.props.iter_mut().find(|p| p.field == "style_name")
|
||||
{
|
||||
let current = match &prop.value {
|
||||
crate::scene::object::PropValue::EditText(s) => s.clone(),
|
||||
_ => String::new(),
|
||||
|
|
@ -247,7 +265,11 @@ impl H7CAD {
|
|||
let is_paper = self.tabs[i].scene.current_layout != "Model";
|
||||
// Paper-space entity coordinates are NOT offset by world_offset (same rule
|
||||
// as wire tessellation in wires_for_block). Only subtract in model space.
|
||||
let wo = if is_paper { [0.0f64; 3] } else { self.tabs[i].scene.world_offset };
|
||||
let wo = if is_paper {
|
||||
[0.0f64; 3]
|
||||
} else {
|
||||
self.tabs[i].scene.world_offset
|
||||
};
|
||||
let (new_handle, new_grips) = {
|
||||
let selected = self.tabs[i].scene.selected_entities();
|
||||
if selected.len() == 1 {
|
||||
|
|
@ -320,7 +342,11 @@ impl H7CAD {
|
|||
.entities()
|
||||
.filter_map(|e| {
|
||||
if let acadrust::EntityType::Viewport(v) = e {
|
||||
if v.common.owner_handle == layout_block { Some(v.id) } else { None }
|
||||
if v.common.owner_handle == layout_block {
|
||||
Some(v.id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
@ -404,13 +430,17 @@ fn merge_sections(
|
|||
) -> Vec<crate::scene::object::PropSection> {
|
||||
left.iter()
|
||||
.filter_map(|section| {
|
||||
let rhs = right.iter().find(|candidate| candidate.title == section.title)?;
|
||||
let rhs = right
|
||||
.iter()
|
||||
.find(|candidate| candidate.title == section.title)?;
|
||||
let props: Vec<crate::scene::object::Property> = section
|
||||
.props
|
||||
.iter()
|
||||
.filter_map(|prop| {
|
||||
let other =
|
||||
rhs.props.iter().find(|candidate| candidate.field == prop.field)?;
|
||||
let other = rhs
|
||||
.props
|
||||
.iter()
|
||||
.find(|candidate| candidate.field == prop.field)?;
|
||||
Some(crate::scene::object::Property {
|
||||
label: prop.label.clone(),
|
||||
field: prop.field,
|
||||
|
|
@ -475,8 +505,7 @@ fn merge_prop_value(
|
|||
(
|
||||
PropValue::BoolToggle { field, .. },
|
||||
PropValue::BoolToggle {
|
||||
field: other_field,
|
||||
..
|
||||
field: other_field, ..
|
||||
},
|
||||
) if field == other_field => PropValue::ReadOnly(VARIES_LABEL.into()),
|
||||
_ => left.clone(),
|
||||
|
|
|
|||
1545
src/app/update.rs
1545
src/app/update.rs
File diff suppressed because it is too large
Load diff
912
src/app/view.rs
912
src/app/view.rs
File diff suppressed because it is too large
Load diff
|
|
@ -127,7 +127,12 @@ pub enum CmdResult {
|
|||
/// Replace the text content of a Text/MText entity in-place.
|
||||
DdeditEntity { handle: Handle, new_text: String },
|
||||
/// Apply new pattern/scale/angle to an existing hatch entity.
|
||||
HatcheditApply { handle: Handle, name: String, scale: f32, angle: f32 },
|
||||
HatcheditApply {
|
||||
handle: Handle,
|
||||
name: String,
|
||||
scale: f32,
|
||||
angle: f32,
|
||||
},
|
||||
/// Stretch entities: move only vertices/endpoints inside the crossing window.
|
||||
StretchEntities {
|
||||
handles: Vec<Handle>,
|
||||
|
|
@ -144,7 +149,11 @@ pub enum CmdResult {
|
|||
mesh_fn: Box<dyn FnOnce(String) -> Option<crate::scene::mesh_model::MeshModel> + Send>,
|
||||
},
|
||||
/// Extrude the profile entity `handle` by `height` along Z.
|
||||
ExtrudeEntity { handle: Handle, height: f32, color: [f32; 4] },
|
||||
ExtrudeEntity {
|
||||
handle: Handle,
|
||||
height: f32,
|
||||
color: [f32; 4],
|
||||
},
|
||||
/// Revolve the profile entity `handle` around the given axis by `angle_deg`.
|
||||
RevolveEntity {
|
||||
handle: Handle,
|
||||
|
|
@ -283,7 +292,6 @@ pub trait CadCommand: Send {
|
|||
None
|
||||
}
|
||||
|
||||
|
||||
/// Called instead of `on_point` when the command needs a tangent pick
|
||||
/// and the snap system found a tangent object.
|
||||
fn on_tangent_point(&mut self, obj: TangentObject, hit: Vec3) -> CmdResult {
|
||||
|
|
|
|||
|
|
@ -61,16 +61,28 @@ fn to_truck(arc: &Arc) -> TruckEntity {
|
|||
for i in 0..=n {
|
||||
let a = start_a + (end_a - start_a) * (i as f64 / n as f64);
|
||||
let p = arc_pt(a);
|
||||
pts.push([(p.x + t * nx) as f32, (p.y + t * ny) as f32, (p.z + t * nz) as f32]);
|
||||
pts.push([
|
||||
(p.x + t * nx) as f32,
|
||||
(p.y + t * ny) as f32,
|
||||
(p.z + t * nz) as f32,
|
||||
]);
|
||||
}
|
||||
pts.push([f32::NAN; 3]);
|
||||
let ps = arc_pt(sa);
|
||||
pts.push([ps.x as f32, ps.y as f32, ps.z as f32]);
|
||||
pts.push([(ps.x + t * nx) as f32, (ps.y + t * ny) as f32, (ps.z + t * nz) as f32]);
|
||||
pts.push([
|
||||
(ps.x + t * nx) as f32,
|
||||
(ps.y + t * ny) as f32,
|
||||
(ps.z + t * nz) as f32,
|
||||
]);
|
||||
pts.push([f32::NAN; 3]);
|
||||
let pe = arc_pt(ea);
|
||||
pts.push([pe.x as f32, pe.y as f32, pe.z as f32]);
|
||||
pts.push([(pe.x + t * nx) as f32, (pe.y + t * ny) as f32, (pe.z + t * nz) as f32]);
|
||||
pts.push([
|
||||
(pe.x + t * nx) as f32,
|
||||
(pe.y + t * ny) as f32,
|
||||
(pe.z + t * nz) as f32,
|
||||
]);
|
||||
return TruckEntity {
|
||||
object: TruckObject::Lines(pts),
|
||||
snap_pts: vec![(cv, SnapHint::Center)],
|
||||
|
|
@ -129,7 +141,11 @@ fn properties(arc: &Arc) -> PropSection {
|
|||
edit("Center Y", "center_y", arc.center.y),
|
||||
edit("Center Z", "center_z", arc.center.z),
|
||||
edit("Radius", "radius", arc.radius),
|
||||
edit("Start Angle (deg)", "start_angle", arc.start_angle.to_degrees()),
|
||||
edit(
|
||||
"Start Angle (deg)",
|
||||
"start_angle",
|
||||
arc.start_angle.to_degrees(),
|
||||
),
|
||||
edit("End Angle (deg)", "end_angle", arc.end_angle.to_degrees()),
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,11 @@ impl TruckConvertible for AttributeDefinition {
|
|||
fn to_truck(&self, document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
||||
let normal = (self.normal.x, self.normal.y, self.normal.z);
|
||||
let (wsx, wsy, wsz) = transform::ocs_point_to_wcs(
|
||||
(self.insertion_point.x, self.insertion_point.y, self.insertion_point.z),
|
||||
(
|
||||
self.insertion_point.x,
|
||||
self.insertion_point.y,
|
||||
self.insertion_point.z,
|
||||
),
|
||||
normal,
|
||||
);
|
||||
let snap_pt = Vec3::new(wsx as f32, wsy as f32, wsz as f32);
|
||||
|
|
@ -95,7 +99,9 @@ impl PropertyEditable for AttributeDefinition {
|
|||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
let Ok(v) = value.trim().parse::<f64>() else { return };
|
||||
let Ok(v) = value.trim().parse::<f64>() else {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"att_ix" => self.insertion_point.x = v,
|
||||
"att_iy" => self.insertion_point.y = v,
|
||||
|
|
@ -126,7 +132,11 @@ impl TruckConvertible for AttributeEntity {
|
|||
fn to_truck(&self, document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
||||
let normal = (self.normal.x, self.normal.y, self.normal.z);
|
||||
let (wsx, wsy, wsz) = transform::ocs_point_to_wcs(
|
||||
(self.insertion_point.x, self.insertion_point.y, self.insertion_point.z),
|
||||
(
|
||||
self.insertion_point.x,
|
||||
self.insertion_point.y,
|
||||
self.insertion_point.z,
|
||||
),
|
||||
normal,
|
||||
);
|
||||
let snap_pt = Vec3::new(wsx as f32, wsy as f32, wsz as f32);
|
||||
|
|
@ -199,7 +209,9 @@ impl PropertyEditable for AttributeEntity {
|
|||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
let Ok(v) = value.trim().parse::<f64>() else { return };
|
||||
let Ok(v) = value.trim().parse::<f64>() else {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"atte_ix" => self.insertion_point.x = v,
|
||||
"atte_iy" => self.insertion_point.y = v,
|
||||
|
|
|
|||
|
|
@ -31,21 +31,23 @@ fn to_truck(circle: &Circle) -> TruckEntity {
|
|||
};
|
||||
// right: centre + r*Ax, left: centre - r*Ax, top: centre + r*Ay, bot: centre - r*Ay
|
||||
let p_right = pt(ax, (0.0, 0.0, 0.0), r);
|
||||
let p_left = pt((ax.0 * -1.0, ax.1 * -1.0, ax.2 * -1.0), (0.0, 0.0, 0.0), r);
|
||||
let p_top = pt(ay, (0.0, 0.0, 0.0), r);
|
||||
let p_bot = pt((ay.0 * -1.0, ay.1 * -1.0, ay.2 * -1.0), (0.0, 0.0, 0.0), r);
|
||||
let p_left = pt((ax.0 * -1.0, ax.1 * -1.0, ax.2 * -1.0), (0.0, 0.0, 0.0), r);
|
||||
let p_top = pt(ay, (0.0, 0.0, 0.0), r);
|
||||
let p_bot = pt((ay.0 * -1.0, ay.1 * -1.0, ay.2 * -1.0), (0.0, 0.0, 0.0), r);
|
||||
|
||||
let cv = Vec3::new(cwx as f32, cwy as f32, cwz as f32);
|
||||
let rf = r as f32;
|
||||
let q = |d: (f64, f64, f64)| Vec3::new(
|
||||
(cwx + r * d.0) as f32,
|
||||
(cwy + r * d.1) as f32,
|
||||
(cwz + r * d.2) as f32,
|
||||
);
|
||||
let q = |d: (f64, f64, f64)| {
|
||||
Vec3::new(
|
||||
(cwx + r * d.0) as f32,
|
||||
(cwy + r * d.1) as f32,
|
||||
(cwz + r * d.2) as f32,
|
||||
)
|
||||
};
|
||||
let snap_pts = vec![
|
||||
(cv, SnapHint::Center),
|
||||
(q(ax), SnapHint::Quadrant),
|
||||
(q(ay), SnapHint::Quadrant),
|
||||
(q(ax), SnapHint::Quadrant),
|
||||
(q(ay), SnapHint::Quadrant),
|
||||
(q((-ax.0, -ax.1, -ax.2)), SnapHint::Quadrant),
|
||||
(q((-ay.0, -ay.1, -ay.2)), SnapHint::Quadrant),
|
||||
];
|
||||
|
|
@ -75,13 +77,21 @@ fn to_truck(circle: &Circle) -> TruckEntity {
|
|||
pts.push([f32::NAN; 3]);
|
||||
for i in 0..=n {
|
||||
let (x, y, z) = circ_pt(i as f64 * tau / n as f64);
|
||||
pts.push([(x + t * nx) as f32, (y + t * ny) as f32, (z + t * nz) as f32]);
|
||||
pts.push([
|
||||
(x + t * nx) as f32,
|
||||
(y + t * ny) as f32,
|
||||
(z + t * nz) as f32,
|
||||
]);
|
||||
}
|
||||
pts.push([f32::NAN; 3]);
|
||||
for i in 0..4usize {
|
||||
let (x, y, z) = circ_pt(i as f64 * std::f64::consts::FRAC_PI_2);
|
||||
pts.push([x as f32, y as f32, z as f32]);
|
||||
pts.push([(x + t * nx) as f32, (y + t * ny) as f32, (z + t * nz) as f32]);
|
||||
pts.push([
|
||||
(x + t * nx) as f32,
|
||||
(y + t * ny) as f32,
|
||||
(z + t * nz) as f32,
|
||||
]);
|
||||
if i < 3 {
|
||||
pts.push([f32::NAN; 3]);
|
||||
}
|
||||
|
|
@ -96,7 +106,7 @@ fn to_truck(circle: &Circle) -> TruckEntity {
|
|||
}
|
||||
|
||||
let right = builder::vertex(p_right);
|
||||
let left = builder::vertex(p_left);
|
||||
let left = builder::vertex(p_left);
|
||||
let upper = builder::circle_arc(&right, &left, p_top);
|
||||
let lower = builder::circle_arc(&left, &right, p_bot);
|
||||
let wire: Wire = [upper, lower].into_iter().collect();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ use acadrust::Entity;
|
|||
use glam::Vec3;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{diamond_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip};
|
||||
use crate::entities::common::{
|
||||
diamond_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip,
|
||||
};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable};
|
||||
use crate::scene::object::{GripApply, GripDef, PropSection};
|
||||
|
||||
|
|
@ -33,9 +35,21 @@ fn base_props(base: &DimensionBase) -> Vec<crate::scene::object::Property> {
|
|||
edit("Text Y", "text_y", base.text_middle_point.y),
|
||||
edit("Text Z", "text_z", base.text_middle_point.z),
|
||||
edit("Text Rotation", "text_rotation", base.text_rotation),
|
||||
edit("Horizontal Dir", "horizontal_direction", base.horizontal_direction),
|
||||
edit("Line Spacing", "line_spacing_factor", base.line_spacing_factor),
|
||||
ro("Measurement", "measurement", format!("{:.4}", base.actual_measurement)),
|
||||
edit(
|
||||
"Horizontal Dir",
|
||||
"horizontal_direction",
|
||||
base.horizontal_direction,
|
||||
),
|
||||
edit(
|
||||
"Line Spacing",
|
||||
"line_spacing_factor",
|
||||
base.line_spacing_factor,
|
||||
),
|
||||
ro(
|
||||
"Measurement",
|
||||
"measurement",
|
||||
format!("{:.4}", base.actual_measurement),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -43,13 +57,29 @@ fn properties(dim: &Dimension) -> PropSection {
|
|||
let mut props = base_props(dim.base());
|
||||
match dim {
|
||||
Dimension::Aligned(d) => {
|
||||
props.extend(linear_like_props(d.first_point, d.second_point, d.definition_point));
|
||||
props.push(edit("Ext Rotation", "ext_line_rotation", d.ext_line_rotation));
|
||||
props.extend(linear_like_props(
|
||||
d.first_point,
|
||||
d.second_point,
|
||||
d.definition_point,
|
||||
));
|
||||
props.push(edit(
|
||||
"Ext Rotation",
|
||||
"ext_line_rotation",
|
||||
d.ext_line_rotation,
|
||||
));
|
||||
}
|
||||
Dimension::Linear(d) => {
|
||||
props.extend(linear_like_props(d.first_point, d.second_point, d.definition_point));
|
||||
props.extend(linear_like_props(
|
||||
d.first_point,
|
||||
d.second_point,
|
||||
d.definition_point,
|
||||
));
|
||||
props.push(edit("Rotation", "rotation", d.rotation));
|
||||
props.push(edit("Ext Rotation", "ext_line_rotation", d.ext_line_rotation));
|
||||
props.push(edit(
|
||||
"Ext Rotation",
|
||||
"ext_line_rotation",
|
||||
d.ext_line_rotation,
|
||||
));
|
||||
}
|
||||
Dimension::Radius(d) => {
|
||||
props.extend(radius_like_props(d.angle_vertex, d.definition_point));
|
||||
|
|
@ -438,9 +468,7 @@ fn apply_ordinate_fields(d: &mut DimensionOrdinate, field: &str, value: &str) {
|
|||
fn apply_transform(dim: &mut Dimension, t: &EntityTransform) {
|
||||
match t {
|
||||
EntityTransform::Translate(d) => dim.translate(acadrust::types::Vector3::new(
|
||||
d.x as f64,
|
||||
d.y as f64,
|
||||
d.z as f64,
|
||||
d.x as f64, d.y as f64, d.z as f64,
|
||||
)),
|
||||
EntityTransform::Rotate { center, angle_rad } => {
|
||||
transform_dimension_points(dim, |pt| rotate_point(pt, *center, *angle_rad))
|
||||
|
|
|
|||
|
|
@ -92,9 +92,9 @@ fn to_truck(ell: &Ellipse) -> TruckEntity {
|
|||
};
|
||||
let snap_pts = vec![
|
||||
(center_v3, SnapHint::Center),
|
||||
(q(r_major, 0.0), SnapHint::Quadrant),
|
||||
(q(r_major, 0.0), SnapHint::Quadrant),
|
||||
(q(-r_major, 0.0), SnapHint::Quadrant),
|
||||
(q(0.0, r_minor), SnapHint::Quadrant),
|
||||
(q(0.0, r_minor), SnapHint::Quadrant),
|
||||
(q(0.0, -r_minor), SnapHint::Quadrant),
|
||||
];
|
||||
TruckEntity {
|
||||
|
|
|
|||
|
|
@ -193,17 +193,29 @@ impl Grippable for Hatch {
|
|||
}
|
||||
}
|
||||
BoundaryEdge::Line(l) => {
|
||||
out.push(diamond_grip(id, Vec3::new(l.start.x as f32, l.start.y as f32, elev)));
|
||||
out.push(diamond_grip(
|
||||
id,
|
||||
Vec3::new(l.start.x as f32, l.start.y as f32, elev),
|
||||
));
|
||||
id += 1;
|
||||
out.push(diamond_grip(id, Vec3::new(l.end.x as f32, l.end.y as f32, elev)));
|
||||
out.push(diamond_grip(
|
||||
id,
|
||||
Vec3::new(l.end.x as f32, l.end.y as f32, elev),
|
||||
));
|
||||
id += 1;
|
||||
}
|
||||
BoundaryEdge::CircularArc(a) => {
|
||||
out.push(diamond_grip(id, Vec3::new(a.center.x as f32, a.center.y as f32, elev)));
|
||||
out.push(diamond_grip(
|
||||
id,
|
||||
Vec3::new(a.center.x as f32, a.center.y as f32, elev),
|
||||
));
|
||||
id += 1;
|
||||
}
|
||||
BoundaryEdge::EllipticArc(e) => {
|
||||
out.push(diamond_grip(id, Vec3::new(e.center.x as f32, e.center.y as f32, elev)));
|
||||
out.push(diamond_grip(
|
||||
id,
|
||||
Vec3::new(e.center.x as f32, e.center.y as f32, elev),
|
||||
));
|
||||
id += 1;
|
||||
}
|
||||
BoundaryEdge::Spline(s) => {
|
||||
|
|
@ -241,7 +253,8 @@ impl Grippable for Hatch {
|
|||
BoundaryEdge::Polyline(p) => {
|
||||
for v in &mut p.vertices {
|
||||
if id == grip_id {
|
||||
let (nx, ny) = resolve(&apply, Vec3::new(v.x as f32, v.y as f32, elev));
|
||||
let (nx, ny) =
|
||||
resolve(&apply, Vec3::new(v.x as f32, v.y as f32, elev));
|
||||
v.x = nx;
|
||||
v.y = ny;
|
||||
break 'outer;
|
||||
|
|
@ -251,14 +264,18 @@ impl Grippable for Hatch {
|
|||
}
|
||||
BoundaryEdge::Line(l) => {
|
||||
if id == grip_id {
|
||||
let (nx, ny) = resolve(&apply, Vec3::new(l.start.x as f32, l.start.y as f32, elev));
|
||||
let (nx, ny) = resolve(
|
||||
&apply,
|
||||
Vec3::new(l.start.x as f32, l.start.y as f32, elev),
|
||||
);
|
||||
l.start.x = nx;
|
||||
l.start.y = ny;
|
||||
break 'outer;
|
||||
}
|
||||
id += 1;
|
||||
if id == grip_id {
|
||||
let (nx, ny) = resolve(&apply, Vec3::new(l.end.x as f32, l.end.y as f32, elev));
|
||||
let (nx, ny) =
|
||||
resolve(&apply, Vec3::new(l.end.x as f32, l.end.y as f32, elev));
|
||||
l.end.x = nx;
|
||||
l.end.y = ny;
|
||||
break 'outer;
|
||||
|
|
@ -267,7 +284,10 @@ impl Grippable for Hatch {
|
|||
}
|
||||
BoundaryEdge::CircularArc(a) => {
|
||||
if id == grip_id {
|
||||
let (nx, ny) = resolve(&apply, Vec3::new(a.center.x as f32, a.center.y as f32, elev));
|
||||
let (nx, ny) = resolve(
|
||||
&apply,
|
||||
Vec3::new(a.center.x as f32, a.center.y as f32, elev),
|
||||
);
|
||||
a.center.x = nx;
|
||||
a.center.y = ny;
|
||||
break 'outer;
|
||||
|
|
@ -276,7 +296,10 @@ impl Grippable for Hatch {
|
|||
}
|
||||
BoundaryEdge::EllipticArc(e) => {
|
||||
if id == grip_id {
|
||||
let (nx, ny) = resolve(&apply, Vec3::new(e.center.x as f32, e.center.y as f32, elev));
|
||||
let (nx, ny) = resolve(
|
||||
&apply,
|
||||
Vec3::new(e.center.x as f32, e.center.y as f32, elev),
|
||||
);
|
||||
e.center.x = nx;
|
||||
e.center.y = ny;
|
||||
break 'outer;
|
||||
|
|
@ -287,7 +310,8 @@ impl Grippable for Hatch {
|
|||
if !s.fit_points.is_empty() {
|
||||
for fp in &mut s.fit_points {
|
||||
if id == grip_id {
|
||||
let (nx, ny) = resolve(&apply, Vec3::new(fp.x as f32, fp.y as f32, elev));
|
||||
let (nx, ny) =
|
||||
resolve(&apply, Vec3::new(fp.x as f32, fp.y as f32, elev));
|
||||
fp.x = nx;
|
||||
fp.y = ny;
|
||||
break 'outer;
|
||||
|
|
@ -297,7 +321,8 @@ impl Grippable for Hatch {
|
|||
} else {
|
||||
for cp in &mut s.control_points {
|
||||
if id == grip_id {
|
||||
let (nx, ny) = resolve(&apply, Vec3::new(cp.x as f32, cp.y as f32, elev));
|
||||
let (nx, ny) =
|
||||
resolve(&apply, Vec3::new(cp.x as f32, cp.y as f32, elev));
|
||||
cp.x = nx;
|
||||
cp.y = ny;
|
||||
break 'outer;
|
||||
|
|
|
|||
|
|
@ -84,7 +84,11 @@ fn apply_transform(ins: &mut Insert, t: &EntityTransform) {
|
|||
};
|
||||
let t = Transform::from_translation(Vector3::new(-(p1.x as f64), -(p1.y as f64), 0.0))
|
||||
.then(&Transform::from_matrix(mirror))
|
||||
.then(&Transform::from_translation(Vector3::new(p1.x as f64, p1.y as f64, 0.0)));
|
||||
.then(&Transform::from_translation(Vector3::new(
|
||||
p1.x as f64,
|
||||
p1.y as f64,
|
||||
0.0,
|
||||
)));
|
||||
acadrust::Entity::apply_transform(entity, &t);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,10 @@ fn to_truck(leader: &Leader) -> TruckEntity {
|
|||
key_verts.push(p3(v));
|
||||
}
|
||||
for i in 0..verts.len().saturating_sub(1) {
|
||||
tangents.push(TangentGeom::Line { p1: p3(&verts[i]), p2: p3(&verts[i + 1]) });
|
||||
tangents.push(TangentGeom::Line {
|
||||
p1: p3(&verts[i]),
|
||||
p2: p3(&verts[i + 1]),
|
||||
});
|
||||
}
|
||||
|
||||
// Arrowhead at vertex[0]
|
||||
|
|
@ -42,16 +45,28 @@ fn to_truck(leader: &Leader) -> TruckEntity {
|
|||
let (s, c) = a.sin_cos();
|
||||
let tip_f = p3(tip);
|
||||
points.push(nan);
|
||||
points.push([tip_f[0] + (dx*c - dy*s)*sz, tip_f[1] + (dx*s + dy*c)*sz, tip_f[2]]);
|
||||
points.push([
|
||||
tip_f[0] + (dx * c - dy * s) * sz,
|
||||
tip_f[1] + (dx * s + dy * c) * sz,
|
||||
tip_f[2],
|
||||
]);
|
||||
points.push(tip_f);
|
||||
points.push([tip_f[0] + (dx*c + dy*s)*sz, tip_f[1] + (-dx*s + dy*c)*sz, tip_f[2]]);
|
||||
points.push([
|
||||
tip_f[0] + (dx * c + dy * s) * sz,
|
||||
tip_f[1] + (-dx * s + dy * c) * sz,
|
||||
tip_f[2],
|
||||
]);
|
||||
}
|
||||
|
||||
// Landing line at last vertex
|
||||
if leader.hookline_enabled && verts.len() >= 2 {
|
||||
let last = verts.last().unwrap();
|
||||
let prev = &verts[verts.len() - 2];
|
||||
let sign = if (last.x - prev.x) >= 0.0 { 1.0_f32 } else { -1.0_f32 };
|
||||
let sign = if (last.x - prev.x) >= 0.0 {
|
||||
1.0_f32
|
||||
} else {
|
||||
-1.0_f32
|
||||
};
|
||||
let len = leader.text_height as f32 * 1.5;
|
||||
let last_f = p3(last);
|
||||
points.push(nan);
|
||||
|
|
@ -108,9 +123,7 @@ fn apply_grip(leader: &mut Leader, grip_id: usize, apply: GripApply) {
|
|||
}
|
||||
} else if let GripApply::Translate(d) = apply {
|
||||
leader.translate(acadrust::types::Vector3::new(
|
||||
d.x as f64,
|
||||
d.y as f64,
|
||||
d.z as f64,
|
||||
d.x as f64, d.y as f64, d.z as f64,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -257,8 +270,20 @@ fn apply_geom_prop(leader: &mut Leader, field: &str, value: &str) {
|
|||
_ => LeaderCreationType::WithText,
|
||||
};
|
||||
}
|
||||
"arrow_enabled" => leader.arrow_enabled = if value == "toggle" { !leader.arrow_enabled } else { value == "true" },
|
||||
"hookline_enabled" => leader.hookline_enabled = if value == "toggle" { !leader.hookline_enabled } else { value == "true" },
|
||||
"arrow_enabled" => {
|
||||
leader.arrow_enabled = if value == "toggle" {
|
||||
!leader.arrow_enabled
|
||||
} else {
|
||||
value == "true"
|
||||
}
|
||||
}
|
||||
"hookline_enabled" => {
|
||||
leader.hookline_enabled = if value == "toggle" {
|
||||
!leader.hookline_enabled
|
||||
} else {
|
||||
value == "true"
|
||||
}
|
||||
}
|
||||
"hookline_direction" => {
|
||||
leader.hookline_direction = match value {
|
||||
"Same" => HooklineDirection::Same,
|
||||
|
|
@ -404,7 +429,9 @@ fn apply_transform(leader: &mut Leader, t: &EntityTransform) {
|
|||
|
||||
impl TruckConvertible for Leader {
|
||||
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
||||
if self.vertices.is_empty() { return None; }
|
||||
if self.vertices.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(to_truck(self))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,36 +14,47 @@ use crate::scene::wire_model::TangentGeom;
|
|||
fn to_truck(line: &Line) -> TruckEntity {
|
||||
let normal = (line.normal.x, line.normal.y, line.normal.z);
|
||||
let (sx, sy, sz) = crate::scene::transform::ocs_point_to_wcs(
|
||||
(line.start.x, line.start.y, line.start.z), normal,
|
||||
);
|
||||
let (ex, ey, ez) = crate::scene::transform::ocs_point_to_wcs(
|
||||
(line.end.x, line.end.y, line.end.z), normal,
|
||||
(line.start.x, line.start.y, line.start.z),
|
||||
normal,
|
||||
);
|
||||
let (ex, ey, ez) =
|
||||
crate::scene::transform::ocs_point_to_wcs((line.end.x, line.end.y, line.end.z), normal);
|
||||
let p0 = Point3::new(sx, sy, sz);
|
||||
let p1 = Point3::new(ex, ey, ez);
|
||||
let kv = vec![
|
||||
[p0.x as f32, p0.y as f32, p0.z as f32],
|
||||
[p1.x as f32, p1.y as f32, p1.z as f32],
|
||||
];
|
||||
let tangent = TangentGeom::Line { p1: kv[0], p2: kv[1] };
|
||||
let tangent = TangentGeom::Line {
|
||||
p1: kv[0],
|
||||
p2: kv[1],
|
||||
};
|
||||
|
||||
if line.thickness.abs() > 1e-10 {
|
||||
let t = line.thickness;
|
||||
let (nx, ny, nz) = normal;
|
||||
let p0t = [
|
||||
(sx + t * nx) as f32, (sy + t * ny) as f32, (sz + t * nz) as f32,
|
||||
(sx + t * nx) as f32,
|
||||
(sy + t * ny) as f32,
|
||||
(sz + t * nz) as f32,
|
||||
];
|
||||
let p1t = [
|
||||
(ex + t * nx) as f32, (ey + t * ny) as f32, (ez + t * nz) as f32,
|
||||
(ex + t * nx) as f32,
|
||||
(ey + t * ny) as f32,
|
||||
(ez + t * nz) as f32,
|
||||
];
|
||||
let pts = vec![
|
||||
kv[0], kv[1],
|
||||
kv[0],
|
||||
kv[1],
|
||||
[f32::NAN; 3],
|
||||
p0t, p1t,
|
||||
p0t,
|
||||
p1t,
|
||||
[f32::NAN; 3],
|
||||
kv[0], p0t,
|
||||
kv[0],
|
||||
p0t,
|
||||
[f32::NAN; 3],
|
||||
kv[1], p1t,
|
||||
kv[1],
|
||||
p1t,
|
||||
];
|
||||
return TruckEntity {
|
||||
object: TruckObject::Lines(pts),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ use glam::Vec3;
|
|||
use truck_modeling::{builder, Edge, Point3, Wire};
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{diamond_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip};
|
||||
use crate::entities::common::{
|
||||
diamond_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip,
|
||||
};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
|
||||
use crate::scene::acad_to_truck::{TruckEntity, TruckObject};
|
||||
use crate::scene::object::{GripApply, GripDef, PropSection};
|
||||
|
|
@ -32,7 +34,9 @@ fn arc_midpoint(p0: [f64; 2], p1: [f64; 2], bulge: f64) -> [f64; 2] {
|
|||
let a0 = (p0[1] - cy).atan2(p0[0] - cx);
|
||||
let a1 = (p1[1] - cy).atan2(p1[0] - cx);
|
||||
let (sa, mut ea) = if bulge > 0.0 { (a0, a1) } else { (a1, a0) };
|
||||
if ea < sa { ea += TAU; }
|
||||
if ea < sa {
|
||||
ea += TAU;
|
||||
}
|
||||
let mid_a = sa + (ea - sa) * 0.5;
|
||||
[cx + r * mid_a.cos(), cy + r * mid_a.sin()]
|
||||
}
|
||||
|
|
@ -58,7 +62,9 @@ fn bulge_from_midpoint(p0: [f64; 2], p1: [f64; 2], mid: [f64; 2]) -> Option<f64>
|
|||
// Determine arc direction: cross product (p1-p0) × (mid-p0)
|
||||
let cross = (p1[0] - p0[0]) * (mid[1] - p0[1]) - (p1[1] - p0[1]) * (mid[0] - p0[0]);
|
||||
let (sa, mut ea) = if cross > 0.0 { (a0, a1) } else { (a1, a0) };
|
||||
if ea < sa { ea += TAU; }
|
||||
if ea < sa {
|
||||
ea += TAU;
|
||||
}
|
||||
let span = ea - sa; // central angle in (0, TAU]
|
||||
let bulge = (span / 4.0).tan();
|
||||
Some(if cross >= 0.0 { bulge } else { -bulge })
|
||||
|
|
@ -77,7 +83,11 @@ fn thick_segments(
|
|||
let (nx, ny, nz) = normal;
|
||||
let t = thickness;
|
||||
let off = |p: [f32; 3]| -> [f32; 3] {
|
||||
[(p[0] as f64 + t * nx) as f32, (p[1] as f64 + t * ny) as f32, (p[2] as f64 + t * nz) as f32]
|
||||
[
|
||||
(p[0] as f64 + t * nx) as f32,
|
||||
(p[1] as f64 + t * ny) as f32,
|
||||
(p[2] as f64 + t * nz) as f32,
|
||||
]
|
||||
};
|
||||
let mut pts: Vec<[f32; 3]> = Vec::with_capacity(path_pts.len() * 2 + seg_data.len() * 3 + 4);
|
||||
// Bottom path
|
||||
|
|
@ -106,7 +116,13 @@ fn thick_segments(
|
|||
pts.push(off(last));
|
||||
}
|
||||
}
|
||||
TruckEntity { object: TruckObject::Lines(pts), snap_pts: vec![], tangent_geoms: tangents, key_vertices: key_verts, fill_tris: vec![] }
|
||||
TruckEntity {
|
||||
object: TruckObject::Lines(pts),
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: tangents,
|
||||
key_vertices: key_verts,
|
||||
fill_tris: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn to_truck(pline: &LwPolyline) -> TruckEntity {
|
||||
|
|
@ -156,22 +172,40 @@ fn to_truck(pline: &LwPolyline) -> TruckEntity {
|
|||
if bulge.abs() < 1e-9 {
|
||||
let (wx, wy, wz) = to_wcs(ox1, oy1);
|
||||
path.push([wx as f32, wy as f32, wz as f32]);
|
||||
tgs.push(TangentGeom::Line { p1: path[path.len()-2], p2: *path.last().unwrap() });
|
||||
tgs.push(TangentGeom::Line {
|
||||
p1: path[path.len() - 2],
|
||||
p2: *path.last().unwrap(),
|
||||
});
|
||||
} else {
|
||||
let angle = 4.0 * bulge.atan();
|
||||
let dx = ox1 - ox0; let dy = oy1 - oy0;
|
||||
let dx = ox1 - ox0;
|
||||
let dy = oy1 - oy0;
|
||||
let d = (dx * dx + dy * dy).sqrt().max(1e-12);
|
||||
let r = (d / 2.0) / (angle / 2.0).sin().abs();
|
||||
let mx = (ox0 + ox1) * 0.5; let my = (oy0 + oy1) * 0.5;
|
||||
let px = -dy / d; let py = dx / d;
|
||||
let mx = (ox0 + ox1) * 0.5;
|
||||
let my = (oy0 + oy1) * 0.5;
|
||||
let px = -dy / d;
|
||||
let py = dx / d;
|
||||
let ss = if bulge > 0.0 { 1.0_f64 } else { -1.0_f64 };
|
||||
let h = r - (r * r - d * d / 4.0).max(0.0).sqrt();
|
||||
let ocx = mx + ss * px * (r - h); let ocy = my + ss * py * (r - h);
|
||||
let ocx = mx + ss * px * (r - h);
|
||||
let ocy = my + ss * py * (r - h);
|
||||
let a0 = (oy0 - ocy).atan2(ox0 - ocx);
|
||||
let mut a1 = (oy1 - ocy).atan2(ox1 - ocx);
|
||||
if bulge > 0.0 { if a1 < a0 { a1 += TAU; } } else { if a1 > a0 { a1 -= TAU; } }
|
||||
if bulge > 0.0 {
|
||||
if a1 < a0 {
|
||||
a1 += TAU;
|
||||
}
|
||||
} else {
|
||||
if a1 > a0 {
|
||||
a1 -= TAU;
|
||||
}
|
||||
}
|
||||
let (wcx, wcy, wcz) = to_wcs(ocx, ocy);
|
||||
tgs.push(TangentGeom::Circle { center: [wcx as f32, wcy as f32, wcz as f32], radius: r as f32 });
|
||||
tgs.push(TangentGeom::Circle {
|
||||
center: [wcx as f32, wcy as f32, wcz as f32],
|
||||
radius: r as f32,
|
||||
});
|
||||
for j in 1..=16usize {
|
||||
let a = a0 + (a1 - a0) * (j as f64 / 16.0);
|
||||
let (wx, wy, wz) = to_wcs(ocx + r * a.cos(), ocy + r * a.sin());
|
||||
|
|
@ -199,26 +233,43 @@ fn to_truck(pline: &LwPolyline) -> TruckEntity {
|
|||
let (wx0, wy0, wz0) = to_wcs(ox0, oy0);
|
||||
let p_start = [wx0 as f32, wy0 as f32, wz0 as f32];
|
||||
pts.push(p_start);
|
||||
if i == 0 { kv.push(p_start); }
|
||||
if i == 0 {
|
||||
kv.push(p_start);
|
||||
}
|
||||
if bulge.abs() < 1e-9 {
|
||||
let (wx1, wy1, wz1) = to_wcs(ox1, oy1);
|
||||
let p_end = [wx1 as f32, wy1 as f32, wz1 as f32];
|
||||
pts.push(p_end);
|
||||
kv.push(p_end);
|
||||
tgs.push(TangentGeom::Line { p1: p_start, p2: p_end });
|
||||
tgs.push(TangentGeom::Line {
|
||||
p1: p_start,
|
||||
p2: p_end,
|
||||
});
|
||||
} else {
|
||||
let angle = 4.0 * bulge.atan();
|
||||
let dx = ox1 - ox0; let dy = oy1 - oy0;
|
||||
let dx = ox1 - ox0;
|
||||
let dy = oy1 - oy0;
|
||||
let d = (dx * dx + dy * dy).sqrt().max(1e-12);
|
||||
let r = (d / 2.0) / (angle / 2.0).sin().abs();
|
||||
let mx = (ox0 + ox1) * 0.5; let my = (oy0 + oy1) * 0.5;
|
||||
let px = -dy / d; let py = dx / d;
|
||||
let mx = (ox0 + ox1) * 0.5;
|
||||
let my = (oy0 + oy1) * 0.5;
|
||||
let px = -dy / d;
|
||||
let py = dx / d;
|
||||
let ss = if bulge > 0.0 { 1.0_f64 } else { -1.0_f64 };
|
||||
let h = r - (r * r - d * d / 4.0).max(0.0).sqrt();
|
||||
let ocx = mx + ss * px * (r - h); let ocy = my + ss * py * (r - h);
|
||||
let ocx = mx + ss * px * (r - h);
|
||||
let ocy = my + ss * py * (r - h);
|
||||
let a0 = (oy0 - ocy).atan2(ox0 - ocx);
|
||||
let mut a1 = (oy1 - ocy).atan2(ox1 - ocx);
|
||||
if bulge > 0.0 { if a1 < a0 { a1 += TAU; } } else { if a1 > a0 { a1 -= TAU; } }
|
||||
if bulge > 0.0 {
|
||||
if a1 < a0 {
|
||||
a1 += TAU;
|
||||
}
|
||||
} else {
|
||||
if a1 > a0 {
|
||||
a1 -= TAU;
|
||||
}
|
||||
}
|
||||
for j in 1..=16usize {
|
||||
let a = a0 + (a1 - a0) * (j as f64 / 16.0);
|
||||
let (wx, wy, wz) = to_wcs(ocx + r * a.cos(), ocy + r * a.sin());
|
||||
|
|
@ -227,7 +278,10 @@ fn to_truck(pline: &LwPolyline) -> TruckEntity {
|
|||
let (wx1, wy1, wz1) = to_wcs(ox1, oy1);
|
||||
kv.push([wx1 as f32, wy1 as f32, wz1 as f32]);
|
||||
let (wcx, wcy, wcz) = to_wcs(ocx, ocy);
|
||||
tgs.push(TangentGeom::Circle { center: [wcx as f32, wcy as f32, wcz as f32], radius: r as f32 });
|
||||
tgs.push(TangentGeom::Circle {
|
||||
center: [wcx as f32, wcy as f32, wcz as f32],
|
||||
radius: r as f32,
|
||||
});
|
||||
}
|
||||
if i + 1 < seg_count {
|
||||
pts.push([f32::NAN; 3]);
|
||||
|
|
@ -314,7 +368,11 @@ fn to_truck(pline: &LwPolyline) -> TruckEntity {
|
|||
fn grips(pline: &LwPolyline) -> Vec<GripDef> {
|
||||
let elev = pline.elevation as f32;
|
||||
let n = pline.vertices.len();
|
||||
let seg_count = if pline.is_closed { n } else { n.saturating_sub(1) };
|
||||
let seg_count = if pline.is_closed {
|
||||
n
|
||||
} else {
|
||||
n.saturating_sub(1)
|
||||
};
|
||||
|
||||
let mut out: Vec<GripDef> = pline
|
||||
.vertices
|
||||
|
|
@ -335,7 +393,10 @@ fn grips(pline: &LwPolyline) -> Vec<GripDef> {
|
|||
[v1.location.x, v1.location.y],
|
||||
v0.bulge,
|
||||
);
|
||||
out.push(diamond_grip(n + i, Vec3::new(mid[0] as f32, mid[1] as f32, elev)));
|
||||
out.push(diamond_grip(
|
||||
n + i,
|
||||
Vec3::new(mid[0] as f32, mid[1] as f32, elev),
|
||||
));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
|
@ -382,7 +443,11 @@ fn apply_grip(pline: &mut LwPolyline, grip_id: usize, apply: GripApply) {
|
|||
} else {
|
||||
// Arc midpoint grip for segment (grip_id - n)
|
||||
let seg = grip_id - n;
|
||||
let count = if pline.is_closed { n } else { n.saturating_sub(1) };
|
||||
let count = if pline.is_closed {
|
||||
n
|
||||
} else {
|
||||
n.saturating_sub(1)
|
||||
};
|
||||
if seg >= count {
|
||||
return;
|
||||
}
|
||||
|
|
@ -399,7 +464,10 @@ fn apply_grip(pline: &mut LwPolyline, grip_id: usize, apply: GripApply) {
|
|||
[old[0] + d.x as f64, old[1] + d.y as f64]
|
||||
}
|
||||
};
|
||||
let p0 = [pline.vertices[seg].location.x, pline.vertices[seg].location.y];
|
||||
let p0 = [
|
||||
pline.vertices[seg].location.x,
|
||||
pline.vertices[seg].location.y,
|
||||
];
|
||||
let p1 = [
|
||||
pline.vertices[(seg + 1) % n].location.x,
|
||||
pline.vertices[(seg + 1) % n].location.y,
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ use glam::Vec3;
|
|||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{ro_prop as ro, square_grip};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
|
||||
use crate::scene::wire_model::SnapHint;
|
||||
use crate::scene::acad_to_truck::{TruckEntity, TruckObject};
|
||||
use crate::scene::object::{GripApply, GripDef, PropSection};
|
||||
use crate::scene::wire_model::SnapHint;
|
||||
|
||||
// ── Face3D ────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -125,7 +125,9 @@ impl PropertyEditable for Face3D {
|
|||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
let Ok(v) = value.trim().parse::<f64>() else { return };
|
||||
let Ok(v) = value.trim().parse::<f64>() else {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"f3_p1x" => self.first_corner.x = v,
|
||||
"f3_p1y" => self.first_corner.y = v,
|
||||
|
|
@ -153,12 +155,7 @@ impl Transformable for Face3D {
|
|||
&mut entity.third_corner,
|
||||
&mut entity.fourth_corner,
|
||||
] {
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut corner.x,
|
||||
&mut corner.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
crate::scene::transform::reflect_xy_point(&mut corner.x, &mut corner.y, p1, p2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -174,12 +171,20 @@ impl TruckConvertible for PolygonMesh {
|
|||
return None;
|
||||
}
|
||||
|
||||
let closed_m = self.flags.contains(acadrust::entities::PolygonMeshFlags::CLOSED_M);
|
||||
let closed_n = self.flags.contains(acadrust::entities::PolygonMeshFlags::CLOSED_N);
|
||||
let closed_m = self
|
||||
.flags
|
||||
.contains(acadrust::entities::PolygonMeshFlags::CLOSED_M);
|
||||
let closed_n = self
|
||||
.flags
|
||||
.contains(acadrust::entities::PolygonMeshFlags::CLOSED_N);
|
||||
|
||||
let pt = |i: usize, j: usize| -> [f32; 3] {
|
||||
let v = &self.vertices[i * n + j];
|
||||
[v.location.x as f32, v.location.y as f32, v.location.z as f32]
|
||||
[
|
||||
v.location.x as f32,
|
||||
v.location.y as f32,
|
||||
v.location.z as f32,
|
||||
]
|
||||
};
|
||||
|
||||
let mut pts: Vec<[f32; 3]> = Vec::new();
|
||||
|
|
@ -238,7 +243,11 @@ impl Grippable for PolygonMesh {
|
|||
.map(|(i, v)| {
|
||||
square_grip(
|
||||
i,
|
||||
Vec3::new(v.location.x as f32, v.location.y as f32, v.location.z as f32),
|
||||
Vec3::new(
|
||||
v.location.x as f32,
|
||||
v.location.y as f32,
|
||||
v.location.z as f32,
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
|
|
@ -303,7 +312,11 @@ impl TruckConvertible for PolyfaceMesh {
|
|||
let get_v = |idx: i16| -> Option<[f32; 3]> {
|
||||
let i = (idx.abs() as usize).checked_sub(1)?;
|
||||
let v = self.vertices.get(i)?;
|
||||
Some([v.location.x as f32, v.location.y as f32, v.location.z as f32])
|
||||
Some([
|
||||
v.location.x as f32,
|
||||
v.location.y as f32,
|
||||
v.location.z as f32,
|
||||
])
|
||||
};
|
||||
|
||||
let mut pts: Vec<[f32; 3]> = Vec::new();
|
||||
|
|
@ -356,7 +369,11 @@ impl Grippable for PolyfaceMesh {
|
|||
.map(|(i, v)| {
|
||||
square_grip(
|
||||
i,
|
||||
Vec3::new(v.location.x as f32, v.location.y as f32, v.location.z as f32),
|
||||
Vec3::new(
|
||||
v.location.x as f32,
|
||||
v.location.y as f32,
|
||||
v.location.z as f32,
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
|
|
|
|||
|
|
@ -26,7 +26,11 @@ impl TruckConvertible for MLine {
|
|||
|
||||
// Center spine.
|
||||
for v in &self.vertices {
|
||||
pts.push([v.position.x as f32, v.position.y as f32, v.position.z as f32]);
|
||||
pts.push([
|
||||
v.position.x as f32,
|
||||
v.position.y as f32,
|
||||
v.position.z as f32,
|
||||
]);
|
||||
}
|
||||
if closed && n >= 2 {
|
||||
pts.push([
|
||||
|
|
@ -79,7 +83,7 @@ impl TruckConvertible for MLine {
|
|||
[
|
||||
[f32::NAN; 3],
|
||||
[px + mx * (-half), py + my * (-half), pz + mz * (-half)],
|
||||
[px + mx * half, py + my * half, pz + mz * half ],
|
||||
[px + mx * half, py + my * half, pz + mz * half],
|
||||
]
|
||||
};
|
||||
pts.extend_from_slice(&cap_v(&self.vertices[0]));
|
||||
|
|
@ -90,7 +94,13 @@ impl TruckConvertible for MLine {
|
|||
let key_verts: Vec<[f32; 3]> = self
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|v| [v.position.x as f32, v.position.y as f32, v.position.z as f32])
|
||||
.map(|v| {
|
||||
[
|
||||
v.position.x as f32,
|
||||
v.position.y as f32,
|
||||
v.position.z as f32,
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
|
||||
let snap_pts = self
|
||||
|
|
@ -98,7 +108,11 @@ impl TruckConvertible for MLine {
|
|||
.iter()
|
||||
.map(|v| {
|
||||
(
|
||||
Vec3::new(v.position.x as f32, v.position.y as f32, v.position.z as f32),
|
||||
Vec3::new(
|
||||
v.position.x as f32,
|
||||
v.position.y as f32,
|
||||
v.position.z as f32,
|
||||
),
|
||||
SnapHint::Node,
|
||||
)
|
||||
})
|
||||
|
|
@ -122,7 +136,11 @@ impl Grippable for MLine {
|
|||
.map(|(i, v)| {
|
||||
square_grip(
|
||||
i,
|
||||
Vec3::new(v.position.x as f32, v.position.y as f32, v.position.z as f32),
|
||||
Vec3::new(
|
||||
v.position.x as f32,
|
||||
v.position.y as f32,
|
||||
v.position.z as f32,
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
|
|
@ -174,12 +192,15 @@ impl PropertyEditable for MLine {
|
|||
} else {
|
||||
value == "true"
|
||||
};
|
||||
self.flags.set(acadrust::entities::MLineFlags::CLOSED, closed);
|
||||
self.flags
|
||||
.set(acadrust::entities::MLineFlags::CLOSED, closed);
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let Ok(v) = value.trim().parse::<f64>() else { return };
|
||||
let Ok(v) = value.trim().parse::<f64>() else {
|
||||
return;
|
||||
};
|
||||
if field == "ml_scale" && v != 0.0 {
|
||||
self.scale_factor = v;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,24 +8,24 @@ pub mod hatch;
|
|||
pub mod insert;
|
||||
pub mod leader;
|
||||
pub mod line;
|
||||
pub mod multileader;
|
||||
pub mod lwpolyline;
|
||||
pub mod mesh;
|
||||
pub mod mline;
|
||||
pub mod mtext;
|
||||
pub mod multileader;
|
||||
pub mod ole2frame;
|
||||
pub mod point;
|
||||
pub mod polyline;
|
||||
pub mod raster_image;
|
||||
pub mod underlay;
|
||||
pub mod ray;
|
||||
pub mod shape;
|
||||
pub mod solid;
|
||||
pub mod solid3d;
|
||||
pub mod spline;
|
||||
pub mod table;
|
||||
pub mod text;
|
||||
pub(crate) mod text_support;
|
||||
pub mod table;
|
||||
pub mod tolerance;
|
||||
pub mod traits;
|
||||
pub mod underlay;
|
||||
pub mod viewport;
|
||||
|
|
|
|||
|
|
@ -25,12 +25,11 @@ pub(crate) fn catmull_rom_pts(ctrl: &[[f32; 3]], segs_per_span: u32) -> Vec<[f32
|
|||
let t3 = t2 * t;
|
||||
let mut pt = [0.0_f32; 3];
|
||||
for k in 0..3 {
|
||||
pt[k] = 0.5 * (
|
||||
(2.0 * p1[k])
|
||||
+ (-p0[k] + p2[k]) * t
|
||||
+ (2.0 * p0[k] - 5.0 * p1[k] + 4.0 * p2[k] - p3[k]) * t2
|
||||
+ (-p0[k] + 3.0 * p1[k] - 3.0 * p2[k] + p3[k]) * t3
|
||||
);
|
||||
pt[k] = 0.5
|
||||
* ((2.0 * p1[k])
|
||||
+ (-p0[k] + p2[k]) * t
|
||||
+ (2.0 * p0[k] - 5.0 * p1[k] + 4.0 * p2[k] - p3[k]) * t2
|
||||
+ (-p0[k] + 3.0 * p1[k] - 3.0 * p2[k] + p3[k]) * t3);
|
||||
}
|
||||
out.push(pt);
|
||||
}
|
||||
|
|
@ -60,16 +59,20 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<TruckE
|
|||
snap_pts.push(node(cp_f));
|
||||
|
||||
for line in &root.lines {
|
||||
if line.points.is_empty() { continue; }
|
||||
if line.points.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !invisible {
|
||||
if !first { points.push(nan); }
|
||||
if !first {
|
||||
points.push(nan);
|
||||
}
|
||||
first = false;
|
||||
|
||||
// Build the full control-point list: line.points + connection_point
|
||||
let mut ctrl: Vec<[f32; 3]> = line.points.iter().map(|p| p3(p)).collect();
|
||||
let last_f = *ctrl.last().unwrap_or(&cp_f);
|
||||
let dist = ((last_f[0]-cp_f[0]).powi(2) + (last_f[1]-cp_f[1]).powi(2)).sqrt();
|
||||
let dist = ((last_f[0] - cp_f[0]).powi(2) + (last_f[1] - cp_f[1]).powi(2)).sqrt();
|
||||
if dist > 1e-9 {
|
||||
ctrl.push(cp_f);
|
||||
}
|
||||
|
|
@ -91,7 +94,10 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<TruckE
|
|||
}
|
||||
|
||||
for i in 0..ctrl.len().saturating_sub(1) {
|
||||
tangents.push(TangentGeom::Line { p1: ctrl[i], p2: ctrl[i + 1] });
|
||||
tangents.push(TangentGeom::Line {
|
||||
p1: ctrl[i],
|
||||
p2: ctrl[i + 1],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -99,34 +105,54 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<TruckE
|
|||
if draw_arrow {
|
||||
let tip = &line.points[0];
|
||||
let tip_f = p3(tip);
|
||||
let next = if line.points.len() >= 2 { line.points[1] } else { *cp };
|
||||
let next = if line.points.len() >= 2 {
|
||||
line.points[1]
|
||||
} else {
|
||||
*cp
|
||||
};
|
||||
let dx = (next.x - tip.x) as f32;
|
||||
let dy = (next.y - tip.y) as f32;
|
||||
let dl = (dx*dx + dy*dy).sqrt().max(1e-9);
|
||||
let dl = (dx * dx + dy * dy).sqrt().max(1e-9);
|
||||
let (dx, dy) = (dx / dl, dy / dl);
|
||||
let a = std::f32::consts::PI / 6.0;
|
||||
let (s, c) = a.sin_cos();
|
||||
points.push(nan);
|
||||
points.push([tip_f[0]+(dx*c-dy*s)*arrow_size, tip_f[1]+(dx*s+dy*c)*arrow_size, tip_f[2]]);
|
||||
points.push([
|
||||
tip_f[0] + (dx * c - dy * s) * arrow_size,
|
||||
tip_f[1] + (dx * s + dy * c) * arrow_size,
|
||||
tip_f[2],
|
||||
]);
|
||||
points.push(tip_f);
|
||||
points.push([tip_f[0]+(dx*c+dy*s)*arrow_size, tip_f[1]+(-dx*s+dy*c)*arrow_size, tip_f[2]]);
|
||||
points.push([
|
||||
tip_f[0] + (dx * c + dy * s) * arrow_size,
|
||||
tip_f[1] + (-dx * s + dy * c) * arrow_size,
|
||||
tip_f[2],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Landing shelf at connection_point
|
||||
if ml.enable_landing && ml.enable_dogleg && ml.dogleg_length > 0.0 {
|
||||
let dir = &root.direction;
|
||||
let dl = (dir.x*dir.x + dir.y*dir.y).sqrt().max(1e-9);
|
||||
let dl = (dir.x * dir.x + dir.y * dir.y).sqrt().max(1e-9);
|
||||
let d = ml.dogleg_length;
|
||||
points.push(nan);
|
||||
points.push(cp_f);
|
||||
points.push([(cp.x + dir.x/dl*d) as f32, (cp.y + dir.y/dl*d) as f32, cp.z as f32]);
|
||||
points.push([
|
||||
(cp.x + dir.x / dl * d) as f32,
|
||||
(cp.y + dir.y / dl * d) as f32,
|
||||
cp.z as f32,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Text strokes (MText content rendered inline)
|
||||
if ml.content_type == LeaderContentType::MText && !ml.context.text_string.is_empty() {
|
||||
let height = if ml.context.text_height > 0.0 { ml.context.text_height } else { ml.text_height };
|
||||
let height = if ml.context.text_height > 0.0 {
|
||||
ml.context.text_height
|
||||
} else {
|
||||
ml.text_height
|
||||
};
|
||||
let ins = &ml.context.text_location;
|
||||
let z = ins.z as f32;
|
||||
snap_pts.push(node([ins.x as f32, ins.y as f32, z]));
|
||||
|
|
@ -141,7 +167,9 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<TruckE
|
|||
&ml.context.text_string,
|
||||
);
|
||||
for stroke in &strokes {
|
||||
if stroke.len() < 2 { continue; }
|
||||
if stroke.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
points.push(nan);
|
||||
for &[x, y] in stroke {
|
||||
points.push([x, y, z]);
|
||||
|
|
@ -149,7 +177,9 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<TruckE
|
|||
}
|
||||
}
|
||||
|
||||
if points.is_empty() { return None; }
|
||||
if points.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(TruckEntity {
|
||||
object: TruckObject::Lines(points),
|
||||
|
|
@ -173,7 +203,10 @@ fn grips(ml: &MultiLeader) -> Vec<GripDef> {
|
|||
for root in &ml.context.leader_roots {
|
||||
for line in &root.lines {
|
||||
for p in &line.points {
|
||||
result.push(square_grip(id, Vec3::new(p.x as f32, p.y as f32, p.z as f32)));
|
||||
result.push(square_grip(
|
||||
id,
|
||||
Vec3::new(p.x as f32, p.y as f32, p.z as f32),
|
||||
));
|
||||
id += 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -181,7 +214,10 @@ fn grips(ml: &MultiLeader) -> Vec<GripDef> {
|
|||
|
||||
if ml.content_type == LeaderContentType::MText {
|
||||
let tl = &ml.context.text_location;
|
||||
result.push(diamond_grip(id, Vec3::new(tl.x as f32, tl.y as f32, tl.z as f32)));
|
||||
result.push(diamond_grip(
|
||||
id,
|
||||
Vec3::new(tl.x as f32, tl.y as f32, tl.z as f32),
|
||||
));
|
||||
}
|
||||
|
||||
result
|
||||
|
|
@ -292,8 +328,12 @@ fn properties(ml: &MultiLeader) -> PropSection {
|
|||
|
||||
let mut props = vec![
|
||||
// Content
|
||||
choice("Content Type", "content_type", content_type_str(&ml.content_type),
|
||||
&["None", "MText", "Block", "Tolerance"]),
|
||||
choice(
|
||||
"Content Type",
|
||||
"content_type",
|
||||
content_type_str(&ml.content_type),
|
||||
&["None", "MText", "Block", "Tolerance"],
|
||||
),
|
||||
Property {
|
||||
label: "Text".into(),
|
||||
field: "text_string",
|
||||
|
|
@ -305,21 +345,49 @@ fn properties(ml: &MultiLeader) -> PropSection {
|
|||
edit("Text Z", "text_z", ctx.text_location.z),
|
||||
bool_toggle("Text Frame", "text_frame", ml.text_frame),
|
||||
// Leader line
|
||||
choice("Path Type", "path_type", path_type_str(&ml.path_type),
|
||||
&["Straight", "Spline", "Invisible"]),
|
||||
choice(
|
||||
"Path Type",
|
||||
"path_type",
|
||||
path_type_str(&ml.path_type),
|
||||
&["Straight", "Spline", "Invisible"],
|
||||
),
|
||||
bool_toggle("Landing", "enable_landing", ml.enable_landing),
|
||||
bool_toggle("Dogleg", "enable_dogleg", ml.enable_dogleg),
|
||||
edit("Dogleg Length", "dogleg_length", ml.dogleg_length),
|
||||
edit("Arrow Size", "arrowhead_size", ml.arrowhead_size),
|
||||
edit("Scale", "scale_factor", ml.scale_factor),
|
||||
bool_toggle("Annotation Scale", "enable_annotation_scale", ml.enable_annotation_scale),
|
||||
bool_toggle(
|
||||
"Annotation Scale",
|
||||
"enable_annotation_scale",
|
||||
ml.enable_annotation_scale,
|
||||
),
|
||||
// Text attachment
|
||||
choice("Left Attach", "text_left_attachment",
|
||||
attachment_str(&ml.text_left_attachment),
|
||||
&["Top of Top","Mid of Top","Mid of Text","Mid of Bot","Bot of Bot","Bottom Line"]),
|
||||
choice("Right Attach", "text_right_attachment",
|
||||
attachment_str(&ml.text_right_attachment),
|
||||
&["Top of Top","Mid of Top","Mid of Text","Mid of Bot","Bot of Bot","Bottom Line"]),
|
||||
choice(
|
||||
"Left Attach",
|
||||
"text_left_attachment",
|
||||
attachment_str(&ml.text_left_attachment),
|
||||
&[
|
||||
"Top of Top",
|
||||
"Mid of Top",
|
||||
"Mid of Text",
|
||||
"Mid of Bot",
|
||||
"Bot of Bot",
|
||||
"Bottom Line",
|
||||
],
|
||||
),
|
||||
choice(
|
||||
"Right Attach",
|
||||
"text_right_attachment",
|
||||
attachment_str(&ml.text_right_attachment),
|
||||
&[
|
||||
"Top of Top",
|
||||
"Mid of Top",
|
||||
"Mid of Text",
|
||||
"Mid of Bot",
|
||||
"Bot of Bot",
|
||||
"Bottom Line",
|
||||
],
|
||||
),
|
||||
// Stats
|
||||
ro("Leader Pts", "total_pts", total_pts.to_string()),
|
||||
ro("Roots", "root_count", ctx.leader_roots.len().to_string()),
|
||||
|
|
@ -332,7 +400,10 @@ fn properties(ml: &MultiLeader) -> PropSection {
|
|||
props.push(edit("Root Conn Z", "conn_z", root.connection_point.z));
|
||||
}
|
||||
|
||||
PropSection { title: "Geometry".into(), props }
|
||||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
||||
|
|
@ -349,12 +420,33 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
|||
}
|
||||
"text_string" => ml.context.text_string = value.to_string(),
|
||||
"text_height" => {
|
||||
if let Some(v) = f64(value) { ml.text_height = v; ml.context.text_height = v; }
|
||||
if let Some(v) = f64(value) {
|
||||
ml.text_height = v;
|
||||
ml.context.text_height = v;
|
||||
}
|
||||
}
|
||||
"text_x" => {
|
||||
if let Some(v) = f64(value) {
|
||||
ml.context.text_location.x = v;
|
||||
}
|
||||
}
|
||||
"text_y" => {
|
||||
if let Some(v) = f64(value) {
|
||||
ml.context.text_location.y = v;
|
||||
}
|
||||
}
|
||||
"text_z" => {
|
||||
if let Some(v) = f64(value) {
|
||||
ml.context.text_location.z = v;
|
||||
}
|
||||
}
|
||||
"text_frame" => {
|
||||
ml.text_frame = if value == "toggle" {
|
||||
!ml.text_frame
|
||||
} else {
|
||||
value == "true"
|
||||
}
|
||||
}
|
||||
"text_x" => { if let Some(v) = f64(value) { ml.context.text_location.x = v; } }
|
||||
"text_y" => { if let Some(v) = f64(value) { ml.context.text_location.y = v; } }
|
||||
"text_z" => { if let Some(v) = f64(value) { ml.context.text_location.z = v; } }
|
||||
"text_frame" => ml.text_frame = if value == "toggle" { !ml.text_frame } else { value == "true" },
|
||||
"path_type" => {
|
||||
ml.path_type = match value {
|
||||
"Spline" => MultiLeaderPathType::Spline,
|
||||
|
|
@ -362,12 +454,42 @@ fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
|||
_ => MultiLeaderPathType::StraightLineSegments,
|
||||
};
|
||||
}
|
||||
"enable_landing" => ml.enable_landing = if value == "toggle" { !ml.enable_landing } else { value == "true" },
|
||||
"enable_dogleg" => ml.enable_dogleg = if value == "toggle" { !ml.enable_dogleg } else { value == "true" },
|
||||
"enable_annotation_scale" => ml.enable_annotation_scale = if value == "toggle" { !ml.enable_annotation_scale } else { value == "true" },
|
||||
"dogleg_length" => { if let Some(v) = f64(value) { ml.dogleg_length = v; } }
|
||||
"arrowhead_size" => { if let Some(v) = f64(value) { ml.arrowhead_size = v; } }
|
||||
"scale_factor" => { if let Some(v) = f64(value) { ml.scale_factor = v; } }
|
||||
"enable_landing" => {
|
||||
ml.enable_landing = if value == "toggle" {
|
||||
!ml.enable_landing
|
||||
} else {
|
||||
value == "true"
|
||||
}
|
||||
}
|
||||
"enable_dogleg" => {
|
||||
ml.enable_dogleg = if value == "toggle" {
|
||||
!ml.enable_dogleg
|
||||
} else {
|
||||
value == "true"
|
||||
}
|
||||
}
|
||||
"enable_annotation_scale" => {
|
||||
ml.enable_annotation_scale = if value == "toggle" {
|
||||
!ml.enable_annotation_scale
|
||||
} else {
|
||||
value == "true"
|
||||
}
|
||||
}
|
||||
"dogleg_length" => {
|
||||
if let Some(v) = f64(value) {
|
||||
ml.dogleg_length = v;
|
||||
}
|
||||
}
|
||||
"arrowhead_size" => {
|
||||
if let Some(v) = f64(value) {
|
||||
ml.arrowhead_size = v;
|
||||
}
|
||||
}
|
||||
"scale_factor" => {
|
||||
if let Some(v) = f64(value) {
|
||||
ml.scale_factor = v;
|
||||
}
|
||||
}
|
||||
"conn_x" => {
|
||||
if let (Some(v), Some(root)) = (f64(value), ml.context.leader_roots.first_mut()) {
|
||||
root.connection_point.x = v;
|
||||
|
|
@ -417,10 +539,18 @@ fn apply_transform(ml: &mut MultiLeader, t: &EntityTransform) {
|
|||
}
|
||||
}
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut root.connection_point.x, &mut root.connection_point.y, p1, p2);
|
||||
&mut root.connection_point.x,
|
||||
&mut root.connection_point.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
}
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut entity.context.text_location.x, &mut entity.context.text_location.y, p1, p2);
|
||||
&mut entity.context.text_location.x,
|
||||
&mut entity.context.text_location.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use acadrust::entities::{Ole2Frame, OleObjectType};
|
|||
use glam::Vec3;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{diamond_grip, ro_prop as ro, edit_prop as edit, square_grip};
|
||||
use crate::entities::common::{diamond_grip, edit_prop as edit, ro_prop as ro, square_grip};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
|
||||
use crate::scene::acad_to_truck::{TruckEntity, TruckObject};
|
||||
use crate::scene::object::{GripApply, GripDef, PropSection};
|
||||
|
|
@ -13,7 +13,7 @@ fn to_truck(ole: &Ole2Frame) -> TruckEntity {
|
|||
let y0 = ole.lower_right_corner.y as f32;
|
||||
let x1 = ole.lower_right_corner.x as f32;
|
||||
let y1 = ole.upper_left_corner.y as f32;
|
||||
let z = ole.upper_left_corner.z as f32;
|
||||
let z = ole.upper_left_corner.z as f32;
|
||||
|
||||
if (x1 - x0).abs() < 1e-6 && (y1 - y0).abs() < 1e-6 {
|
||||
let s = 0.5_f32;
|
||||
|
|
@ -30,12 +30,20 @@ fn to_truck(ole: &Ole2Frame) -> TruckEntity {
|
|||
let cy = (y0 + y1) * 0.5;
|
||||
let pts = vec![
|
||||
// Rectangle
|
||||
[x0, y0, z], [x1, y0, z], [x1, y0, z], [x1, y1, z],
|
||||
[x1, y1, z], [x0, y1, z], [x0, y1, z], [x0, y0, z],
|
||||
[x0, y0, z],
|
||||
[x1, y0, z],
|
||||
[x1, y0, z],
|
||||
[x1, y1, z],
|
||||
[x1, y1, z],
|
||||
[x0, y1, z],
|
||||
[x0, y1, z],
|
||||
[x0, y0, z],
|
||||
// Diagonal X
|
||||
[x0, y0, z], [x1, y1, z],
|
||||
[x0, y0, z],
|
||||
[x1, y1, z],
|
||||
[f32::NAN; 3],
|
||||
[x1, y0, z], [x0, y1, z],
|
||||
[x1, y0, z],
|
||||
[x0, y1, z],
|
||||
];
|
||||
let center = Vec3::new(cx, cy, z);
|
||||
TruckEntity {
|
||||
|
|
@ -59,7 +67,11 @@ fn grips(ole: &Ole2Frame) -> Vec<GripDef> {
|
|||
ole.lower_right_corner.z as f32,
|
||||
);
|
||||
let center = (ul + lr) * 0.5;
|
||||
vec![square_grip(0, ul), square_grip(1, lr), diamond_grip(2, center)]
|
||||
vec![
|
||||
square_grip(0, ul),
|
||||
square_grip(1, lr),
|
||||
diamond_grip(2, center),
|
||||
]
|
||||
}
|
||||
|
||||
fn properties(ole: &Ole2Frame) -> PropSection {
|
||||
|
|
@ -81,7 +93,9 @@ fn properties(ole: &Ole2Frame) -> PropSection {
|
|||
}
|
||||
|
||||
fn apply_geom_prop(ole: &mut Ole2Frame, field: &str, value: &str) {
|
||||
let Ok(v) = value.trim().parse::<f64>() else { return };
|
||||
let Ok(v) = value.trim().parse::<f64>() else {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"ole_ulx" => ole.upper_left_corner.x = v,
|
||||
"ole_uly" => ole.upper_left_corner.y = v,
|
||||
|
|
@ -139,15 +153,25 @@ impl TruckConvertible for Ole2Frame {
|
|||
}
|
||||
|
||||
impl Grippable for Ole2Frame {
|
||||
fn grips(&self) -> Vec<GripDef> { grips(self) }
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { apply_grip(self, grip_id, apply); }
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
grips(self)
|
||||
}
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
apply_grip(self, grip_id, apply);
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyEditable for Ole2Frame {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection { properties(self) }
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) { apply_geom_prop(self, field, value); }
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection {
|
||||
properties(self)
|
||||
}
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
apply_geom_prop(self, field, value);
|
||||
}
|
||||
}
|
||||
|
||||
impl Transformable for Ole2Frame {
|
||||
fn apply_transform(&mut self, t: &EntityTransform) { apply_transform(self, t); }
|
||||
fn apply_transform(&mut self, t: &EntityTransform) {
|
||||
apply_transform(self, t);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,13 @@ fn tessellate_polyline(pl: &Polyline) -> TruckEntity {
|
|||
let pts: Vec<[f32; 3]> = pl
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|v| [v.location.x as f32, v.location.y as f32, v.location.z as f32])
|
||||
.map(|v| {
|
||||
[
|
||||
v.location.x as f32,
|
||||
v.location.y as f32,
|
||||
v.location.z as f32,
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut points = pts.clone();
|
||||
|
|
@ -49,7 +55,11 @@ impl Grippable for Polyline {
|
|||
.map(|(i, v)| {
|
||||
square_grip(
|
||||
i,
|
||||
Vec3::new(v.location.x as f32, v.location.y as f32, v.location.z as f32),
|
||||
Vec3::new(
|
||||
v.location.x as f32,
|
||||
v.location.y as f32,
|
||||
v.location.z as f32,
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
|
|
@ -152,7 +162,11 @@ fn tessellate_polyline2d(pl: &Polyline2D) -> TruckEntity {
|
|||
let (nx, ny, nz) = normal;
|
||||
let t = pl.thickness;
|
||||
let off = |p: [f32; 3]| -> [f32; 3] {
|
||||
[(p[0] as f64 + t * nx) as f32, (p[1] as f64 + t * ny) as f32, (p[2] as f64 + t * nz) as f32]
|
||||
[
|
||||
(p[0] as f64 + t * nx) as f32,
|
||||
(p[1] as f64 + t * ny) as f32,
|
||||
(p[2] as f64 + t * nz) as f32,
|
||||
]
|
||||
};
|
||||
let mut path: Vec<[f32; 3]> = Vec::new();
|
||||
let mut kv: Vec<[f32; 3]> = Vec::new();
|
||||
|
|
@ -169,13 +183,18 @@ fn tessellate_polyline2d(pl: &Polyline2D) -> TruckEntity {
|
|||
if bulge.abs() < 1e-9 {
|
||||
let (wx, wy, wz) = to_wcs(ox1, oy1);
|
||||
path.push([wx as f32, wy as f32, wz as f32]);
|
||||
tgs.push(TangentGeom::Line { p1: path[path.len()-2], p2: *path.last().unwrap() });
|
||||
tgs.push(TangentGeom::Line {
|
||||
p1: path[path.len() - 2],
|
||||
p2: *path.last().unwrap(),
|
||||
});
|
||||
} else {
|
||||
let angle = 4.0 * bulge.atan();
|
||||
let dx = ox1 - ox0; let dy = oy1 - oy0;
|
||||
let dx = ox1 - ox0;
|
||||
let dy = oy1 - oy0;
|
||||
let d = (dx * dx + dy * dy).sqrt().max(1e-12);
|
||||
let r = (d / 2.0) / (angle / 2.0).sin().abs();
|
||||
let mx = (ox0 + ox1) * 0.5; let my = (oy0 + oy1) * 0.5;
|
||||
let mx = (ox0 + ox1) * 0.5;
|
||||
let my = (oy0 + oy1) * 0.5;
|
||||
let px = -dy / d;
|
||||
let ss = if bulge > 0.0 { 1.0_f64 } else { -1.0_f64 };
|
||||
let h = r - (r * r - d * d / 4.0).max(0.0).sqrt();
|
||||
|
|
@ -183,9 +202,20 @@ fn tessellate_polyline2d(pl: &Polyline2D) -> TruckEntity {
|
|||
let ocy = my + ss * (dx / d) * (r - h);
|
||||
let a0 = (oy0 - ocy).atan2(ox0 - ocx);
|
||||
let mut a1 = (oy1 - ocy).atan2(ox1 - ocx);
|
||||
if bulge > 0.0 { if a1 < a0 { a1 += TAU; } } else { if a1 > a0 { a1 -= TAU; } }
|
||||
if bulge > 0.0 {
|
||||
if a1 < a0 {
|
||||
a1 += TAU;
|
||||
}
|
||||
} else {
|
||||
if a1 > a0 {
|
||||
a1 -= TAU;
|
||||
}
|
||||
}
|
||||
let (wcx, wcy, wcz) = to_wcs(ocx, ocy);
|
||||
tgs.push(TangentGeom::Circle { center: [wcx as f32, wcy as f32, wcz as f32], radius: r as f32 });
|
||||
tgs.push(TangentGeom::Circle {
|
||||
center: [wcx as f32, wcy as f32, wcz as f32],
|
||||
radius: r as f32,
|
||||
});
|
||||
for j in 1..=16usize {
|
||||
let a = a0 + (a1 - a0) * (j as f64 / 16.0);
|
||||
let (wx, wy, wz) = to_wcs(ocx + r * a.cos(), ocy + r * a.sin());
|
||||
|
|
@ -198,15 +228,26 @@ fn tessellate_polyline2d(pl: &Polyline2D) -> TruckEntity {
|
|||
let mut pts: Vec<[f32; 3]> = Vec::with_capacity(path.len() * 2 + kv.len() * 3 + 4);
|
||||
pts.extend_from_slice(&path);
|
||||
pts.push([f32::NAN; 3]);
|
||||
for &p in &path { pts.push(off(p)); }
|
||||
for &p in &path {
|
||||
pts.push(off(p));
|
||||
}
|
||||
if !kv.is_empty() {
|
||||
pts.push([f32::NAN; 3]);
|
||||
for (i, &pb) in kv.iter().enumerate() {
|
||||
pts.push(pb); pts.push(off(pb));
|
||||
if i + 1 < kv.len() { pts.push([f32::NAN; 3]); }
|
||||
pts.push(pb);
|
||||
pts.push(off(pb));
|
||||
if i + 1 < kv.len() {
|
||||
pts.push([f32::NAN; 3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return TruckEntity { object: TruckObject::Lines(pts), snap_pts: vec![], tangent_geoms: tgs, key_vertices: kv, fill_tris: vec![] };
|
||||
return TruckEntity {
|
||||
object: TruckObject::Lines(pts),
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: tgs,
|
||||
key_vertices: kv,
|
||||
fill_tris: vec![],
|
||||
};
|
||||
}
|
||||
|
||||
for i in 0..seg_count {
|
||||
|
|
@ -289,12 +330,7 @@ impl Grippable for Polyline2D {
|
|||
self.vertices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, v)| {
|
||||
square_grip(
|
||||
i,
|
||||
Vec3::new(v.location.x as f32, v.location.y as f32, elev),
|
||||
)
|
||||
})
|
||||
.map(|(i, v)| square_grip(i, Vec3::new(v.location.x as f32, v.location.y as f32, elev)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
|
@ -341,7 +377,11 @@ impl PropertyEditable for Polyline2D {
|
|||
} else {
|
||||
value == "true"
|
||||
};
|
||||
if closed { self.close(); } else { self.flags.set_closed(false); }
|
||||
if closed {
|
||||
self.close();
|
||||
} else {
|
||||
self.flags.set_closed(false);
|
||||
}
|
||||
}
|
||||
"pl2_elevation" => {
|
||||
if let Ok(v) = value.trim().parse::<f64>() {
|
||||
|
|
@ -372,14 +412,18 @@ impl Transformable for Polyline2D {
|
|||
|
||||
fn tessellate_polyline3d(pl: &Polyline3D) -> TruckEntity {
|
||||
let to_pt = |v: &acadrust::entities::Vertex3DPolyline| -> [f32; 3] {
|
||||
[v.position.x as f32, v.position.y as f32, v.position.z as f32]
|
||||
[
|
||||
v.position.x as f32,
|
||||
v.position.y as f32,
|
||||
v.position.z as f32,
|
||||
]
|
||||
};
|
||||
|
||||
// DXF vertex flags: 8 = spline-fit curve point, 16 = spline frame control point.
|
||||
// When spline-fit vertices are present use them for the wire and control points for snap;
|
||||
// otherwise treat all vertices uniformly.
|
||||
let spline_curve: Vec<_> = pl.vertices.iter().filter(|v| v.flags & 8 != 0).collect();
|
||||
let ctrl_pts: Vec<_> = pl.vertices.iter().filter(|v| v.flags & 16 != 0).collect();
|
||||
let ctrl_pts: Vec<_> = pl.vertices.iter().filter(|v| v.flags & 16 != 0).collect();
|
||||
|
||||
let (wire_pts, key_verts) = if !spline_curve.is_empty() {
|
||||
let wire: Vec<[f32; 3]> = spline_curve.iter().map(|v| to_pt(v)).collect();
|
||||
|
|
@ -418,7 +462,11 @@ impl Grippable for Polyline3D {
|
|||
.map(|(i, v)| {
|
||||
square_grip(
|
||||
i,
|
||||
Vec3::new(v.position.x as f32, v.position.y as f32, v.position.z as f32),
|
||||
Vec3::new(
|
||||
v.position.x as f32,
|
||||
v.position.y as f32,
|
||||
v.position.z as f32,
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
|
|
@ -462,8 +510,16 @@ impl PropertyEditable for Polyline3D {
|
|||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
if field == "pl3_closed" {
|
||||
let closed = if value == "toggle" { !self.is_closed() } else { value == "true" };
|
||||
if closed { self.close(); } else { self.open(); }
|
||||
let closed = if value == "toggle" {
|
||||
!self.is_closed()
|
||||
} else {
|
||||
value == "true"
|
||||
};
|
||||
if closed {
|
||||
self.close();
|
||||
} else {
|
||||
self.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,13 +57,7 @@ fn image_wire(corners: [[f32; 3]; 4], with_x: bool) -> Vec<[f32; 3]> {
|
|||
pts
|
||||
}
|
||||
|
||||
fn reflect_vec3(
|
||||
vx: &mut f64,
|
||||
vy: &mut f64,
|
||||
ax: f64,
|
||||
ay: f64,
|
||||
len2: f64,
|
||||
) {
|
||||
fn reflect_vec3(vx: &mut f64, vy: &mut f64, ax: f64, ay: f64, len2: f64) {
|
||||
let dot = *vx * ax + *vy * ay;
|
||||
*vx = 2.0 * dot * ax / len2 - *vx;
|
||||
*vy = 2.0 * dot * ay / len2 - *vy;
|
||||
|
|
@ -97,11 +91,8 @@ impl TruckConvertible for RasterImage {
|
|||
let cb = &self.clip_boundary;
|
||||
match cb.clip_type {
|
||||
acadrust::entities::ClipType::Polygonal if cb.vertices.len() >= 3 => {
|
||||
let mut poly: Vec<[f32; 3]> = cb
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|v| px_to_world(v.x, v.y))
|
||||
.collect();
|
||||
let mut poly: Vec<[f32; 3]> =
|
||||
cb.vertices.iter().map(|v| px_to_world(v.x, v.y)).collect();
|
||||
if let Some(&first) = poly.first() {
|
||||
poly.push(first);
|
||||
}
|
||||
|
|
@ -198,13 +189,18 @@ impl PropertyEditable for RasterImage {
|
|||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
match field {
|
||||
"ri_clip" => {
|
||||
self.clipping_enabled =
|
||||
if value == "toggle" { !self.clipping_enabled } else { value == "true" };
|
||||
self.clipping_enabled = if value == "toggle" {
|
||||
!self.clipping_enabled
|
||||
} else {
|
||||
value == "true"
|
||||
};
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let Ok(v) = value.trim().parse::<f64>() else { return };
|
||||
let Ok(v) = value.trim().parse::<f64>() else {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"ri_ox" => self.insertion_point.x = v,
|
||||
"ri_oy" => self.insertion_point.y = v,
|
||||
|
|
@ -255,8 +251,7 @@ impl TruckConvertible for Wipeout {
|
|||
&& matches!(
|
||||
self.clip_type,
|
||||
acadrust::entities::WipeoutClipType::Polygonal
|
||||
)
|
||||
{
|
||||
) {
|
||||
// Convert pixel-space boundary vertices to world space:
|
||||
// world = insertion_point + u_vector * v.x * size.x + v_vector * v.y * size.y
|
||||
let ox = self.insertion_point.x as f32;
|
||||
|
|
@ -358,13 +353,18 @@ impl Grippable for Wipeout {
|
|||
let ox = self.insertion_point.x;
|
||||
let oy = self.insertion_point.y;
|
||||
let oz = self.insertion_point.z;
|
||||
let cur_wx = ox + self.u_vector.x * v.x * self.size.x + self.v_vector.x * v.y * self.size.y;
|
||||
let cur_wy = oy + self.u_vector.y * v.x * self.size.x + self.v_vector.y * v.y * self.size.y;
|
||||
let cur_wz = oz + self.u_vector.z * v.x * self.size.x + self.v_vector.z * v.y * self.size.y;
|
||||
let cur_wx =
|
||||
ox + self.u_vector.x * v.x * self.size.x + self.v_vector.x * v.y * self.size.y;
|
||||
let cur_wy =
|
||||
oy + self.u_vector.y * v.x * self.size.x + self.v_vector.y * v.y * self.size.y;
|
||||
let cur_wz =
|
||||
oz + self.u_vector.z * v.x * self.size.x + self.v_vector.z * v.y * self.size.y;
|
||||
let new_w = match apply {
|
||||
GripApply::Translate(d) => {
|
||||
[cur_wx + d.x as f64, cur_wy + d.y as f64, cur_wz + d.z as f64]
|
||||
}
|
||||
GripApply::Translate(d) => [
|
||||
cur_wx + d.x as f64,
|
||||
cur_wy + d.y as f64,
|
||||
cur_wz + d.z as f64,
|
||||
],
|
||||
GripApply::Absolute(p) => [p.x as f64, p.y as f64, p.z as f64],
|
||||
};
|
||||
// Back-project: solve for pixel coords using u_vector and v_vector.
|
||||
|
|
@ -424,13 +424,18 @@ impl PropertyEditable for Wipeout {
|
|||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
match field {
|
||||
"wo_clip" => {
|
||||
self.clipping_enabled =
|
||||
if value == "toggle" { !self.clipping_enabled } else { value == "true" };
|
||||
self.clipping_enabled = if value == "toggle" {
|
||||
!self.clipping_enabled
|
||||
} else {
|
||||
value == "true"
|
||||
};
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let Ok(v) = value.trim().parse::<f64>() else { return };
|
||||
let Ok(v) = value.trim().parse::<f64>() else {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"wo_ox" => self.insertion_point.x = v,
|
||||
"wo_oy" => self.insertion_point.y = v,
|
||||
|
|
|
|||
|
|
@ -104,14 +104,22 @@ impl PropertyEditable for Ray {
|
|||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
let Ok(v) = value.trim().parse::<f64>() else { return };
|
||||
let Ok(v) = value.trim().parse::<f64>() else {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"ray_bx" => self.base_point.x = v,
|
||||
"ray_by" => self.base_point.y = v,
|
||||
"ray_bz" => self.base_point.z = v,
|
||||
"ray_dx" => { self.direction.x = v; }
|
||||
"ray_dy" => { self.direction.y = v; }
|
||||
"ray_dz" => { self.direction.z = v; }
|
||||
"ray_dx" => {
|
||||
self.direction.x = v;
|
||||
}
|
||||
"ray_dy" => {
|
||||
self.direction.y = v;
|
||||
}
|
||||
"ray_dz" => {
|
||||
self.direction.z = v;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -233,14 +241,22 @@ impl PropertyEditable for XLine {
|
|||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
let Ok(v) = value.trim().parse::<f64>() else { return };
|
||||
let Ok(v) = value.trim().parse::<f64>() else {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"xl_bx" => self.base_point.x = v,
|
||||
"xl_by" => self.base_point.y = v,
|
||||
"xl_bz" => self.base_point.z = v,
|
||||
"xl_dx" => { self.direction.x = v; }
|
||||
"xl_dy" => { self.direction.y = v; }
|
||||
"xl_dz" => { self.direction.z = v; }
|
||||
"xl_dx" => {
|
||||
self.direction.x = v;
|
||||
}
|
||||
"xl_dy" => {
|
||||
self.direction.y = v;
|
||||
}
|
||||
"xl_dz" => {
|
||||
self.direction.z = v;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@ use acadrust::entities::Shape;
|
|||
use glam::Vec3;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{ro_prop as ro, edit_prop as edit, square_grip};
|
||||
use crate::entities::common::{edit_prop as edit, ro_prop as ro, square_grip};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
|
||||
use crate::scene::acad_to_truck::{TruckEntity, TruckObject};
|
||||
use crate::scene::object::{GripApply, GripDef, PropSection};
|
||||
use crate::scene::wire_model::SnapHint;
|
||||
use crate::scene::transform;
|
||||
use crate::scene::wire_model::SnapHint;
|
||||
|
||||
// ── Marker geometry ───────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -21,11 +21,11 @@ use crate::scene::transform;
|
|||
fn shape_marker(ox: f32, oy: f32, oz: f32, size: f32) -> Vec<[f32; 3]> {
|
||||
let s = size * 0.5;
|
||||
vec![
|
||||
[ox, oy + s, oz], // top
|
||||
[ox + s, oy, oz], // right
|
||||
[ox, oy - s, oz], // bottom
|
||||
[ox - s, oy, oz], // left
|
||||
[ox, oy + s, oz], // close
|
||||
[ox, oy + s, oz], // top
|
||||
[ox + s, oy, oz], // right
|
||||
[ox, oy - s, oz], // bottom
|
||||
[ox - s, oy, oz], // left
|
||||
[ox, oy + s, oz], // close
|
||||
[f32::NAN; 3],
|
||||
]
|
||||
}
|
||||
|
|
@ -91,24 +91,26 @@ impl PropertyEditable for Shape {
|
|||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props: vec![
|
||||
ro("Name", "shp_name", self.shape_name.clone()),
|
||||
ro("Style", "shp_style", self.style_name.clone()),
|
||||
edit("Insert X", "shp_ix", self.insertion_point.x),
|
||||
edit("Insert Y", "shp_iy", self.insertion_point.y),
|
||||
edit("Insert Z", "shp_iz", self.insertion_point.z),
|
||||
edit("Size", "shp_sz", self.size),
|
||||
edit("Rotation","shp_rot", self.rotation.to_degrees()),
|
||||
ro("Name", "shp_name", self.shape_name.clone()),
|
||||
ro("Style", "shp_style", self.style_name.clone()),
|
||||
edit("Insert X", "shp_ix", self.insertion_point.x),
|
||||
edit("Insert Y", "shp_iy", self.insertion_point.y),
|
||||
edit("Insert Z", "shp_iz", self.insertion_point.z),
|
||||
edit("Size", "shp_sz", self.size),
|
||||
edit("Rotation", "shp_rot", self.rotation.to_degrees()),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
let Ok(v) = value.trim().parse::<f64>() else { return };
|
||||
let Ok(v) = value.trim().parse::<f64>() else {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"shp_ix" => self.insertion_point.x = v,
|
||||
"shp_iy" => self.insertion_point.y = v,
|
||||
"shp_iz" => self.insertion_point.z = v,
|
||||
"shp_sz" => self.size = v.max(0.001),
|
||||
"shp_ix" => self.insertion_point.x = v,
|
||||
"shp_iy" => self.insertion_point.y = v,
|
||||
"shp_iz" => self.insertion_point.z = v,
|
||||
"shp_sz" => self.size = v.max(0.001),
|
||||
"shp_rot" => self.rotation = v.to_radians(),
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,10 +29,17 @@ impl TruckConvertible for Solid {
|
|||
// AutoCAD stores corners in "Z" order: p0-p1 are top edge, p2-p3 are bottom edge,
|
||||
// so the visual quad is p0→p1→p3→p2→p0.
|
||||
let pts = vec![
|
||||
p0, p1, [f32::NAN; 3],
|
||||
p1, p3, [f32::NAN; 3],
|
||||
p3, p2, [f32::NAN; 3],
|
||||
p2, p0,
|
||||
p0,
|
||||
p1,
|
||||
[f32::NAN; 3],
|
||||
p1,
|
||||
p3,
|
||||
[f32::NAN; 3],
|
||||
p3,
|
||||
p2,
|
||||
[f32::NAN; 3],
|
||||
p2,
|
||||
p0,
|
||||
];
|
||||
|
||||
let snap = vec![
|
||||
|
|
@ -108,7 +115,9 @@ impl PropertyEditable for Solid {
|
|||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
let Ok(v) = value.trim().parse::<f64>() else { return };
|
||||
let Ok(v) = value.trim().parse::<f64>() else {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"sl_p1x" => self.first_corner.x = v,
|
||||
"sl_p1y" => self.first_corner.y = v,
|
||||
|
|
@ -137,12 +146,7 @@ impl Transformable for Solid {
|
|||
&mut entity.third_corner,
|
||||
&mut entity.fourth_corner,
|
||||
] {
|
||||
crate::scene::transform::reflect_xy_point(
|
||||
&mut corner.x,
|
||||
&mut corner.y,
|
||||
p1,
|
||||
p2,
|
||||
);
|
||||
crate::scene::transform::reflect_xy_point(&mut corner.x, &mut corner.y, p1, p2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,9 +71,21 @@ impl PropertyEditable for Solid3D {
|
|||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props: vec![
|
||||
ro("Ref Pt X", "s3d_px", format!("{:.4}", self.point_of_reference.x)),
|
||||
ro("Ref Pt Y", "s3d_py", format!("{:.4}", self.point_of_reference.y)),
|
||||
ro("Ref Pt Z", "s3d_pz", format!("{:.4}", self.point_of_reference.z)),
|
||||
ro(
|
||||
"Ref Pt X",
|
||||
"s3d_px",
|
||||
format!("{:.4}", self.point_of_reference.x),
|
||||
),
|
||||
ro(
|
||||
"Ref Pt Y",
|
||||
"s3d_py",
|
||||
format!("{:.4}", self.point_of_reference.y),
|
||||
),
|
||||
ro(
|
||||
"Ref Pt Z",
|
||||
"s3d_pz",
|
||||
format!("{:.4}", self.point_of_reference.z),
|
||||
),
|
||||
ro("ACIS Data", "s3d_acis", size),
|
||||
],
|
||||
}
|
||||
|
|
@ -113,9 +125,21 @@ impl PropertyEditable for Region {
|
|||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props: vec![
|
||||
ro("Ref Pt X", "rgn_px", format!("{:.4}", self.point_of_reference.x)),
|
||||
ro("Ref Pt Y", "rgn_py", format!("{:.4}", self.point_of_reference.y)),
|
||||
ro("Ref Pt Z", "rgn_pz", format!("{:.4}", self.point_of_reference.z)),
|
||||
ro(
|
||||
"Ref Pt X",
|
||||
"rgn_px",
|
||||
format!("{:.4}", self.point_of_reference.x),
|
||||
),
|
||||
ro(
|
||||
"Ref Pt Y",
|
||||
"rgn_py",
|
||||
format!("{:.4}", self.point_of_reference.y),
|
||||
),
|
||||
ro(
|
||||
"Ref Pt Z",
|
||||
"rgn_pz",
|
||||
format!("{:.4}", self.point_of_reference.z),
|
||||
),
|
||||
ro("ACIS Data", "rgn_acis", size),
|
||||
],
|
||||
}
|
||||
|
|
@ -155,9 +179,21 @@ impl PropertyEditable for Body {
|
|||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props: vec![
|
||||
ro("Ref Pt X", "bdy_px", format!("{:.4}", self.point_of_reference.x)),
|
||||
ro("Ref Pt Y", "bdy_py", format!("{:.4}", self.point_of_reference.y)),
|
||||
ro("Ref Pt Z", "bdy_pz", format!("{:.4}", self.point_of_reference.z)),
|
||||
ro(
|
||||
"Ref Pt X",
|
||||
"bdy_px",
|
||||
format!("{:.4}", self.point_of_reference.x),
|
||||
),
|
||||
ro(
|
||||
"Ref Pt Y",
|
||||
"bdy_py",
|
||||
format!("{:.4}", self.point_of_reference.y),
|
||||
),
|
||||
ro(
|
||||
"Ref Pt Z",
|
||||
"bdy_pz",
|
||||
format!("{:.4}", self.point_of_reference.z),
|
||||
),
|
||||
ro("ACIS Data", "bdy_acis", size),
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,11 @@ fn to_truck(spl: &Spline) -> TruckEntity {
|
|||
.collect();
|
||||
let bspline = BSplineCurve::new(knot_vec, ctrl_pts);
|
||||
let (t0, t1) = bspline.range_tuple();
|
||||
(bspline.subs(t0), bspline.subs(t1), Curve::BSplineCurve(bspline))
|
||||
(
|
||||
bspline.subs(t0),
|
||||
bspline.subs(t1),
|
||||
Curve::BSplineCurve(bspline),
|
||||
)
|
||||
};
|
||||
|
||||
let snap_source = if !spl.fit_points.is_empty() {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,11 @@ impl TruckConvertible for Table {
|
|||
|
||||
let origin = v3(&self.insertion_point);
|
||||
let h_raw = v3(&self.horizontal_direction);
|
||||
let h = if h_raw.length_squared() > 1e-10 { h_raw.normalize() } else { Vec3::X };
|
||||
let h = if h_raw.length_squared() > 1e-10 {
|
||||
h_raw.normalize()
|
||||
} else {
|
||||
Vec3::X
|
||||
};
|
||||
// Perpendicular "down" direction in the drawing plane (tables grow downward)
|
||||
let v_down = Vec3::new(h.y, -h.x, 0.0);
|
||||
|
||||
|
|
@ -58,13 +62,13 @@ impl TruckConvertible for Table {
|
|||
};
|
||||
|
||||
for &ry in &row_offsets {
|
||||
let left = origin + h * 0.0 + v_down * ry;
|
||||
let left = origin + h * 0.0 + v_down * ry;
|
||||
let right = origin + h * total_w + v_down * ry;
|
||||
add_seg(left, right);
|
||||
}
|
||||
|
||||
for &cx in &col_offsets {
|
||||
let top = origin + h * cx + v_down * 0.0;
|
||||
let top = origin + h * cx + v_down * 0.0;
|
||||
let bottom = origin + h * cx + v_down * total_h;
|
||||
add_seg(top, bottom);
|
||||
}
|
||||
|
|
@ -75,7 +79,10 @@ impl TruckConvertible for Table {
|
|||
|
||||
for (ri, row) in self.rows.iter().enumerate() {
|
||||
let row_top = row_offsets[ri];
|
||||
let row_bot = row_offsets.get(ri + 1).copied().unwrap_or(row_top + row.height as f32);
|
||||
let row_bot = row_offsets
|
||||
.get(ri + 1)
|
||||
.copied()
|
||||
.unwrap_or(row_top + row.height as f32);
|
||||
let row_mid = (row_top + row_bot) * 0.5;
|
||||
|
||||
for (ci, cell) in row.cells.iter().enumerate() {
|
||||
|
|
@ -84,7 +91,7 @@ impl TruckConvertible for Table {
|
|||
continue;
|
||||
}
|
||||
|
||||
let col_left = col_offsets[ci];
|
||||
let col_left = col_offsets[ci];
|
||||
let col_width = self.columns.get(ci).map(|c| c.width as f32).unwrap_or(1.0);
|
||||
let col_right = col_left + col_width;
|
||||
|
||||
|
|
@ -95,19 +102,19 @@ impl TruckConvertible for Table {
|
|||
// 0/default = MiddleCenter (5)
|
||||
let align = cell.style.as_ref().map_or(5, |s| s.alignment);
|
||||
let horiz = ((align - 1).rem_euclid(3)) + 1; // 1=left, 2=center, 3=right
|
||||
let vert = ((align - 1) / 3) + 1; // 1=top, 2=middle, 3=bottom
|
||||
let vert = ((align - 1) / 3) + 1; // 1=top, 2=middle, 3=bottom
|
||||
|
||||
let text_w = cxf::measure_text(text, text_height, 1.0, "txt");
|
||||
|
||||
let x_offset = match horiz {
|
||||
1 => col_left + margin, // left
|
||||
3 => col_right - margin - text_w, // right
|
||||
_ => col_left + (col_width - text_w) * 0.5, // center (default)
|
||||
1 => col_left + margin, // left
|
||||
3 => col_right - margin - text_w, // right
|
||||
_ => col_left + (col_width - text_w) * 0.5, // center (default)
|
||||
};
|
||||
let y_offset = match vert {
|
||||
1 => row_top + margin, // top
|
||||
3 => row_bot - margin - text_height, // bottom
|
||||
_ => row_mid - text_height * 0.5, // middle (default)
|
||||
1 => row_top + margin, // top
|
||||
3 => row_bot - margin - text_height, // bottom
|
||||
_ => row_mid - text_height * 0.5, // middle (default)
|
||||
};
|
||||
|
||||
let text_origin = origin + h * x_offset + v_down * y_offset;
|
||||
|
|
@ -170,11 +177,23 @@ impl PropertyEditable for Table {
|
|||
PropSection {
|
||||
title: "Table".into(),
|
||||
props: vec![
|
||||
ro("Rows", "tbl_rows", self.rows.len().to_string()),
|
||||
ro("Columns", "tbl_cols", self.columns.len().to_string()),
|
||||
ro("Insert X","tbl_ix", format!("{:.4}", self.insertion_point.x)),
|
||||
ro("Insert Y","tbl_iy", format!("{:.4}", self.insertion_point.y)),
|
||||
ro("Insert Z","tbl_iz", format!("{:.4}", self.insertion_point.z)),
|
||||
ro("Rows", "tbl_rows", self.rows.len().to_string()),
|
||||
ro("Columns", "tbl_cols", self.columns.len().to_string()),
|
||||
ro(
|
||||
"Insert X",
|
||||
"tbl_ix",
|
||||
format!("{:.4}", self.insertion_point.x),
|
||||
),
|
||||
ro(
|
||||
"Insert Y",
|
||||
"tbl_iy",
|
||||
format!("{:.4}", self.insertion_point.y),
|
||||
),
|
||||
ro(
|
||||
"Insert Z",
|
||||
"tbl_iz",
|
||||
format!("{:.4}", self.insertion_point.z),
|
||||
),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ use glam::Vec3;
|
|||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{edit_prop as edit, parse_f64, square_grip};
|
||||
use crate::entities::text_support::{resolve_dxf_special_chars, resolve_text_style, text_local_bounds};
|
||||
use crate::entities::text_support::{
|
||||
resolve_dxf_special_chars, resolve_text_style, text_local_bounds,
|
||||
};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
|
||||
use crate::scene::acad_to_truck::{TextStroke, TruckEntity, TruckObject};
|
||||
use crate::scene::cxf;
|
||||
|
|
@ -49,17 +51,28 @@ fn sync_text_alignment_point(t: &mut Text) {
|
|||
fn to_truck(t: &Text, document: &acadrust::CadDocument) -> TruckEntity {
|
||||
let normal = (t.normal.x, t.normal.y, t.normal.z);
|
||||
let (wsx, wsy, wsz) = crate::scene::transform::ocs_point_to_wcs(
|
||||
(t.insertion_point.x, t.insertion_point.y, t.insertion_point.z),
|
||||
(
|
||||
t.insertion_point.x,
|
||||
t.insertion_point.y,
|
||||
t.insertion_point.z,
|
||||
),
|
||||
normal,
|
||||
);
|
||||
let snap_pt = Vec3::new(wsx as f32, wsy as f32, wsz as f32);
|
||||
let resolved_style = resolve_text_style(&t.style, document);
|
||||
let font_name = resolved_style.font_name;
|
||||
let base_wf = (if t.width_factor > 0.0 { t.width_factor as f32 } else { 1.0 }
|
||||
* resolved_style.width_factor.max(0.01))
|
||||
let base_wf = (if t.width_factor > 0.0 {
|
||||
t.width_factor as f32
|
||||
} else {
|
||||
1.0
|
||||
} * resolved_style.width_factor.max(0.01))
|
||||
.clamp(0.01, 100.0);
|
||||
// is_backward mirrors text left-right via negative width factor.
|
||||
let width_factor = if resolved_style.is_backward { -base_wf } else { base_wf };
|
||||
let width_factor = if resolved_style.is_backward {
|
||||
-base_wf
|
||||
} else {
|
||||
base_wf
|
||||
};
|
||||
// is_upside_down rotates 180° around the insertion point.
|
||||
let rotation = if resolved_style.is_upside_down {
|
||||
t.rotation as f32 + std::f32::consts::PI
|
||||
|
|
|
|||
|
|
@ -125,16 +125,32 @@ pub fn resolve_dxf_special_chars(s: &str) -> String {
|
|||
}
|
||||
chars.next(); // consume second '%'
|
||||
match chars.peek().map(|c| c.to_ascii_lowercase()) {
|
||||
Some('d') => { chars.next(); out.push('°'); }
|
||||
Some('p') => { chars.next(); out.push('±'); }
|
||||
Some('c') => { chars.next(); out.push('⌀'); }
|
||||
Some('u') | Some('o') => { chars.next(); } // toggle codes — strip silently
|
||||
Some('%') => { chars.next(); out.push('%'); }
|
||||
Some('d') => {
|
||||
chars.next();
|
||||
out.push('°');
|
||||
}
|
||||
Some('p') => {
|
||||
chars.next();
|
||||
out.push('±');
|
||||
}
|
||||
Some('c') => {
|
||||
chars.next();
|
||||
out.push('⌀');
|
||||
}
|
||||
Some('u') | Some('o') => {
|
||||
chars.next();
|
||||
} // toggle codes — strip silently
|
||||
Some('%') => {
|
||||
chars.next();
|
||||
out.push('%');
|
||||
}
|
||||
Some(d) if d.is_ascii_digit() => {
|
||||
let mut digits = String::with_capacity(3);
|
||||
for _ in 0..3 {
|
||||
match chars.peek() {
|
||||
Some(&ch) if ch.is_ascii_digit() => { digits.push(chars.next().unwrap()); }
|
||||
Some(&ch) if ch.is_ascii_digit() => {
|
||||
digits.push(chars.next().unwrap());
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
|
@ -150,7 +166,10 @@ pub fn resolve_dxf_special_chars(s: &str) -> String {
|
|||
out.push('%');
|
||||
out.push_str(&digits);
|
||||
}
|
||||
_ => { out.push('%'); out.push('%'); }
|
||||
_ => {
|
||||
out.push('%');
|
||||
out.push('%');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -208,7 +227,9 @@ pub fn strip_mtext_codes(s: &str) -> String {
|
|||
} else {
|
||||
// \U or \u without '+' — strip until semicolon
|
||||
for c in chars.by_ref() {
|
||||
if c == ';' { break; }
|
||||
if c == ';' {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -222,7 +243,9 @@ pub fn strip_mtext_codes(s: &str) -> String {
|
|||
let mut sep = '/';
|
||||
let mut in_lower = false;
|
||||
for c in chars.by_ref() {
|
||||
if c == ';' { break; }
|
||||
if c == ';' {
|
||||
break;
|
||||
}
|
||||
if !in_lower && (c == '/' || c == '^' || c == '#') {
|
||||
sep = c;
|
||||
in_lower = true;
|
||||
|
|
@ -261,12 +284,16 @@ pub fn strip_mtext_codes(s: &str) -> String {
|
|||
Some(c) if "pHWQTACcfFMX".contains(c) => {
|
||||
chars.next();
|
||||
for c in chars.by_ref() {
|
||||
if c == ';' { break; }
|
||||
if c == ';' {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Unknown escape — consume and silently discard the code character so
|
||||
// it does not appear as a literal in the output.
|
||||
Some(_) => { chars.next(); }
|
||||
Some(_) => {
|
||||
chars.next();
|
||||
}
|
||||
None => {}
|
||||
},
|
||||
// Strip brace grouping markers (scope delimiters for in-line formatting)
|
||||
|
|
@ -280,13 +307,15 @@ pub fn strip_mtext_codes(s: &str) -> String {
|
|||
}
|
||||
|
||||
pub fn split_mtext_lines(s: &str) -> Vec<String> {
|
||||
let lines: Vec<String> = s.split('\n')
|
||||
.map(|l| l.trim().to_string())
|
||||
.collect();
|
||||
let lines: Vec<String> = s.split('\n').map(|l| l.trim().to_string()).collect();
|
||||
// Drop leading and trailing blank lines, but preserve blank lines in the
|
||||
// middle — they are intentional paragraph separators (\\P\\P in MTEXT).
|
||||
let start = lines.iter().position(|l| !l.is_empty()).unwrap_or(0);
|
||||
let end = lines.iter().rposition(|l| !l.is_empty()).map(|i| i + 1).unwrap_or(0);
|
||||
let end = lines
|
||||
.iter()
|
||||
.rposition(|l| !l.is_empty())
|
||||
.map(|i| i + 1)
|
||||
.unwrap_or(0);
|
||||
lines[start..end].to_vec()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,13 +40,20 @@ fn substitute_gdt_codes(s: &str) -> String {
|
|||
let mut depth = 1usize;
|
||||
for c in chars.by_ref() {
|
||||
match c {
|
||||
'{' => { depth += 1; inner.push(c); }
|
||||
'}' => {
|
||||
depth -= 1;
|
||||
if depth == 0 { break; }
|
||||
'{' => {
|
||||
depth += 1;
|
||||
inner.push(c);
|
||||
}
|
||||
'}' => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
break;
|
||||
}
|
||||
inner.push(c);
|
||||
}
|
||||
_ => {
|
||||
inner.push(c);
|
||||
}
|
||||
_ => { inner.push(c); }
|
||||
}
|
||||
}
|
||||
// Is it a GDT font switch?
|
||||
|
|
@ -67,27 +74,27 @@ fn substitute_gdt_codes(s: &str) -> String {
|
|||
/// Map a GDT font character to a short ASCII approximation.
|
||||
fn gdt_char_to_ascii(c: char) -> &'static str {
|
||||
match c {
|
||||
'a' => "SRT", // Straightness
|
||||
'b' => "FLT", // Flatness
|
||||
'c' => "FLT", // Flatness
|
||||
'd' => "PSF", // Profile of Surface
|
||||
'e' => "CYL", // Cylindricity
|
||||
'f' => "PRL", // Profile of Line
|
||||
'g' => "CIR", // Circularity
|
||||
'h' => "PAR", // Parallelism
|
||||
'i' => "SYM", // Symmetry
|
||||
'j' => "PRP", // Perpendicularity
|
||||
'k' => "PLN", // Profile of Line
|
||||
'l' => "(L)", // LMC
|
||||
'm' => "(M)", // MMC / Diameter
|
||||
'n' => "ANG", // Angularity
|
||||
'o' => "(o)", // at maximum material boundary
|
||||
'p' => "POS", // Position
|
||||
'a' => "SRT", // Straightness
|
||||
'b' => "FLT", // Flatness
|
||||
'c' => "FLT", // Flatness
|
||||
'd' => "PSF", // Profile of Surface
|
||||
'e' => "CYL", // Cylindricity
|
||||
'f' => "PRL", // Profile of Line
|
||||
'g' => "CIR", // Circularity
|
||||
'h' => "PAR", // Parallelism
|
||||
'i' => "SYM", // Symmetry
|
||||
'j' => "PRP", // Perpendicularity
|
||||
'k' => "PLN", // Profile of Line
|
||||
'l' => "(L)", // LMC
|
||||
'm' => "(M)", // MMC / Diameter
|
||||
'n' => "ANG", // Angularity
|
||||
'o' => "(o)", // at maximum material boundary
|
||||
'p' => "POS", // Position
|
||||
'q' => "(q)",
|
||||
'r' => "RUN", // Circular Runout
|
||||
's' => "(S)", // RFS / Regardless of Feature Size
|
||||
't' => "TRN", // Total Runout
|
||||
'u' => "CON", // Concentricity
|
||||
'r' => "RUN", // Circular Runout
|
||||
's' => "(S)", // RFS / Regardless of Feature Size
|
||||
't' => "TRN", // Total Runout
|
||||
'u' => "CON", // Concentricity
|
||||
'v' => "(v)",
|
||||
'w' => "(w)",
|
||||
_ => "?",
|
||||
|
|
@ -118,7 +125,11 @@ fn tessellate_tolerance(tol: &Tolerance) -> Vec<Vec<[f32; 2]>> {
|
|||
}
|
||||
|
||||
// ── Metrics ──────────────────────────────────────────────────────────
|
||||
let h = if tol.text_height > 1e-6 { tol.text_height as f32 } else { 2.5_f32 };
|
||||
let h = if tol.text_height > 1e-6 {
|
||||
tol.text_height as f32
|
||||
} else {
|
||||
2.5_f32
|
||||
};
|
||||
let gap = (h * 0.35).max(0.1);
|
||||
let cell_h = h + 2.0 * gap;
|
||||
let char_w = h * 0.65;
|
||||
|
|
@ -143,9 +154,7 @@ fn tessellate_tolerance(tol: &Tolerance) -> Vec<Vec<[f32; 2]>> {
|
|||
let (sa, ca) = angle.sin_cos();
|
||||
|
||||
// Rotate only; origin is kept as f64 and applied later with full precision.
|
||||
let rot = |x: f32, y: f32| -> [f32; 2] {
|
||||
[x * ca - y * sa, x * sa + y * ca]
|
||||
};
|
||||
let rot = |x: f32, y: f32| -> [f32; 2] { [x * ca - y * sa, x * sa + y * ca] };
|
||||
|
||||
let mut out: Vec<Vec<[f32; 2]>> = Vec::new();
|
||||
|
||||
|
|
@ -182,20 +191,17 @@ fn tessellate_tolerance(tol: &Tolerance) -> Vec<Vec<[f32; 2]>> {
|
|||
let row_y = cell_h * ri as f32 + gap;
|
||||
let mut cell_x = 0.0_f32;
|
||||
for (ci, cell) in row.iter().enumerate() {
|
||||
let cw = if ci < col_widths.len() { col_widths[ci] } else { 0.0 };
|
||||
let cw = if ci < col_widths.len() {
|
||||
col_widths[ci]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
if !cell.is_empty() {
|
||||
let text_w = cell.len() as f32 * char_w;
|
||||
let tx = cell_x + (cw - text_w) * 0.5;
|
||||
// Tessellate text in local frame then transform
|
||||
let local_strokes = cxf::tessellate_text_ex(
|
||||
[0.0, 0.0],
|
||||
h,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
"txt",
|
||||
cell,
|
||||
);
|
||||
let local_strokes =
|
||||
cxf::tessellate_text_ex([0.0, 0.0], h, 0.0, 1.0, 0.0, "txt", cell);
|
||||
for polyline in local_strokes {
|
||||
let transformed: Vec<[f32; 2]> = polyline
|
||||
.into_iter()
|
||||
|
|
@ -289,7 +295,9 @@ impl PropertyEditable for Tolerance {
|
|||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
let Ok(v) = value.trim().parse::<f64>() else { return };
|
||||
let Ok(v) = value.trim().parse::<f64>() else {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"tol_ix" => self.insertion_point.x = v,
|
||||
"tol_iy" => self.insertion_point.y = v,
|
||||
|
|
|
|||
|
|
@ -132,82 +132,62 @@ impl EntityTypeOps for EntityType {
|
|||
pline,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::Polyline(pl) => Some(PropertyEditable::geometry_properties(
|
||||
pl,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::Polyline2D(pl) => Some(PropertyEditable::geometry_properties(
|
||||
pl,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::Polyline3D(pl) => Some(PropertyEditable::geometry_properties(
|
||||
pl,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::Ray(ray) => Some(PropertyEditable::geometry_properties(
|
||||
ray,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::XLine(xl) => Some(PropertyEditable::geometry_properties(
|
||||
xl,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::RasterImage(img) => Some(PropertyEditable::geometry_properties(
|
||||
img,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::Wipeout(wo) => Some(PropertyEditable::geometry_properties(
|
||||
wo,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::AttributeDefinition(a) => Some(PropertyEditable::geometry_properties(
|
||||
a,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::AttributeEntity(a) => Some(PropertyEditable::geometry_properties(
|
||||
a,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::MLine(ml) => Some(PropertyEditable::geometry_properties(
|
||||
ml,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::Tolerance(tol) => Some(PropertyEditable::geometry_properties(
|
||||
tol,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::Polyline(pl) => {
|
||||
Some(PropertyEditable::geometry_properties(pl, text_style_names))
|
||||
}
|
||||
EntityType::Polyline2D(pl) => {
|
||||
Some(PropertyEditable::geometry_properties(pl, text_style_names))
|
||||
}
|
||||
EntityType::Polyline3D(pl) => {
|
||||
Some(PropertyEditable::geometry_properties(pl, text_style_names))
|
||||
}
|
||||
EntityType::Ray(ray) => {
|
||||
Some(PropertyEditable::geometry_properties(ray, text_style_names))
|
||||
}
|
||||
EntityType::XLine(xl) => {
|
||||
Some(PropertyEditable::geometry_properties(xl, text_style_names))
|
||||
}
|
||||
EntityType::RasterImage(img) => {
|
||||
Some(PropertyEditable::geometry_properties(img, text_style_names))
|
||||
}
|
||||
EntityType::Wipeout(wo) => {
|
||||
Some(PropertyEditable::geometry_properties(wo, text_style_names))
|
||||
}
|
||||
EntityType::AttributeDefinition(a) => {
|
||||
Some(PropertyEditable::geometry_properties(a, text_style_names))
|
||||
}
|
||||
EntityType::AttributeEntity(a) => {
|
||||
Some(PropertyEditable::geometry_properties(a, text_style_names))
|
||||
}
|
||||
EntityType::MLine(ml) => {
|
||||
Some(PropertyEditable::geometry_properties(ml, text_style_names))
|
||||
}
|
||||
EntityType::Tolerance(tol) => {
|
||||
Some(PropertyEditable::geometry_properties(tol, text_style_names))
|
||||
}
|
||||
EntityType::Solid(solid) => Some(PropertyEditable::geometry_properties(
|
||||
solid,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::Solid3D(s) => Some(PropertyEditable::geometry_properties(
|
||||
s,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::Region(r) => Some(PropertyEditable::geometry_properties(
|
||||
r,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::Body(b) => Some(PropertyEditable::geometry_properties(
|
||||
b,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::Face3D(f) => Some(PropertyEditable::geometry_properties(
|
||||
f,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::PolygonMesh(pm) => Some(PropertyEditable::geometry_properties(
|
||||
pm,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::PolyfaceMesh(pfm) => Some(PropertyEditable::geometry_properties(
|
||||
pfm,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::Table(tbl) => Some(PropertyEditable::geometry_properties(
|
||||
tbl,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::Solid3D(s) => {
|
||||
Some(PropertyEditable::geometry_properties(s, text_style_names))
|
||||
}
|
||||
EntityType::Region(r) => {
|
||||
Some(PropertyEditable::geometry_properties(r, text_style_names))
|
||||
}
|
||||
EntityType::Body(b) => Some(PropertyEditable::geometry_properties(b, text_style_names)),
|
||||
EntityType::Face3D(f) => {
|
||||
Some(PropertyEditable::geometry_properties(f, text_style_names))
|
||||
}
|
||||
EntityType::PolygonMesh(pm) => {
|
||||
Some(PropertyEditable::geometry_properties(pm, text_style_names))
|
||||
}
|
||||
EntityType::PolyfaceMesh(pfm) => {
|
||||
Some(PropertyEditable::geometry_properties(pfm, text_style_names))
|
||||
}
|
||||
EntityType::Table(tbl) => {
|
||||
Some(PropertyEditable::geometry_properties(tbl, text_style_names))
|
||||
}
|
||||
EntityType::Hatch(hatch) => Some(PropertyEditable::geometry_properties(
|
||||
hatch,
|
||||
text_style_names,
|
||||
|
|
@ -233,30 +213,25 @@ impl EntityTypeOps for EntityType {
|
|||
EntityType::Insert(ins) => {
|
||||
Some(PropertyEditable::geometry_properties(ins, text_style_names))
|
||||
}
|
||||
EntityType::Dimension(dim) => Some(PropertyEditable::geometry_properties(
|
||||
dim,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::Dimension(dim) => {
|
||||
Some(PropertyEditable::geometry_properties(dim, text_style_names))
|
||||
}
|
||||
EntityType::Leader(leader) => Some(PropertyEditable::geometry_properties(
|
||||
leader,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::MultiLeader(ml) => Some(PropertyEditable::geometry_properties(
|
||||
ml,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::Underlay(ul) => Some(PropertyEditable::geometry_properties(
|
||||
ul,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::Shape(shp) => Some(PropertyEditable::geometry_properties(
|
||||
shp,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::Ole2Frame(ole) => Some(PropertyEditable::geometry_properties(
|
||||
ole,
|
||||
text_style_names,
|
||||
)),
|
||||
EntityType::MultiLeader(ml) => {
|
||||
Some(PropertyEditable::geometry_properties(ml, text_style_names))
|
||||
}
|
||||
EntityType::Underlay(ul) => {
|
||||
Some(PropertyEditable::geometry_properties(ul, text_style_names))
|
||||
}
|
||||
EntityType::Shape(shp) => {
|
||||
Some(PropertyEditable::geometry_properties(shp, text_style_names))
|
||||
}
|
||||
EntityType::Ole2Frame(ole) => {
|
||||
Some(PropertyEditable::geometry_properties(ole, text_style_names))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -277,7 +252,9 @@ impl EntityTypeOps for EntityType {
|
|||
EntityType::XLine(xl) => PropertyEditable::apply_geom_prop(xl, field, value),
|
||||
EntityType::RasterImage(img) => PropertyEditable::apply_geom_prop(img, field, value),
|
||||
EntityType::Wipeout(wo) => PropertyEditable::apply_geom_prop(wo, field, value),
|
||||
EntityType::AttributeDefinition(a) => PropertyEditable::apply_geom_prop(a, field, value),
|
||||
EntityType::AttributeDefinition(a) => {
|
||||
PropertyEditable::apply_geom_prop(a, field, value)
|
||||
}
|
||||
EntityType::AttributeEntity(a) => PropertyEditable::apply_geom_prop(a, field, value),
|
||||
EntityType::MLine(ml) => PropertyEditable::apply_geom_prop(ml, field, value),
|
||||
EntityType::Tolerance(tol) => PropertyEditable::apply_geom_prop(tol, field, value),
|
||||
|
|
|
|||
|
|
@ -12,15 +12,15 @@ const STANDARD_SCALES: &[(&str, f64)] = &[
|
|||
("1:500", 0.002),
|
||||
("1:200", 0.005),
|
||||
("1:100", 0.01),
|
||||
("1:50", 0.02),
|
||||
("1:20", 0.05),
|
||||
("1:10", 0.1),
|
||||
("1:5", 0.2),
|
||||
("1:2", 0.5),
|
||||
("1:1", 1.0),
|
||||
("2:1", 2.0),
|
||||
("5:1", 5.0),
|
||||
("10:1", 10.0),
|
||||
("1:50", 0.02),
|
||||
("1:20", 0.05),
|
||||
("1:10", 0.1),
|
||||
("1:5", 0.2),
|
||||
("1:2", 0.5),
|
||||
("1:1", 1.0),
|
||||
("2:1", 2.0),
|
||||
("5:1", 5.0),
|
||||
("10:1", 10.0),
|
||||
];
|
||||
|
||||
fn scale_label(scale: f64) -> String {
|
||||
|
|
@ -29,24 +29,35 @@ fn scale_label(scale: f64) -> String {
|
|||
return label.to_string();
|
||||
}
|
||||
}
|
||||
format!("{:.6}", scale).trim_end_matches('0').trim_end_matches('.').to_string()
|
||||
format!("{:.6}", scale)
|
||||
.trim_end_matches('0')
|
||||
.trim_end_matches('.')
|
||||
.to_string()
|
||||
}
|
||||
|
||||
// ── Render mode options ───────────────────────────────────────────────────
|
||||
|
||||
const RENDER_MODES: &[(&str, ViewportRenderMode)] = &[
|
||||
("2D Wireframe", ViewportRenderMode::Wireframe2D),
|
||||
("3D Wireframe", ViewportRenderMode::Wireframe3D),
|
||||
("Hidden Line", ViewportRenderMode::HiddenLine),
|
||||
("Flat Shaded", ViewportRenderMode::FlatShaded),
|
||||
("Gouraud Shaded", ViewportRenderMode::GouraudShaded),
|
||||
("Flat Shaded + Edges", ViewportRenderMode::FlatShadedWithEdges),
|
||||
("Gouraud Shaded + Edges", ViewportRenderMode::GouraudShadedWithEdges),
|
||||
("2D Wireframe", ViewportRenderMode::Wireframe2D),
|
||||
("3D Wireframe", ViewportRenderMode::Wireframe3D),
|
||||
("Hidden Line", ViewportRenderMode::HiddenLine),
|
||||
("Flat Shaded", ViewportRenderMode::FlatShaded),
|
||||
("Gouraud Shaded", ViewportRenderMode::GouraudShaded),
|
||||
(
|
||||
"Flat Shaded + Edges",
|
||||
ViewportRenderMode::FlatShadedWithEdges,
|
||||
),
|
||||
(
|
||||
"Gouraud Shaded + Edges",
|
||||
ViewportRenderMode::GouraudShadedWithEdges,
|
||||
),
|
||||
];
|
||||
|
||||
fn render_mode_label(mode: &ViewportRenderMode) -> &'static str {
|
||||
for (label, m) in RENDER_MODES {
|
||||
if m == mode { return label; }
|
||||
if m == mode {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
"2D Wireframe"
|
||||
}
|
||||
|
|
@ -56,14 +67,25 @@ fn render_mode_label(mode: &ViewportRenderMode) -> &'static str {
|
|||
const SHADE_PLOT_LABELS: &[&str] = &["As Displayed", "Wireframe", "Hidden", "Rendered"];
|
||||
|
||||
fn shade_plot_label(mode: i16) -> &'static str {
|
||||
SHADE_PLOT_LABELS.get(mode as usize).copied().unwrap_or("As Displayed")
|
||||
SHADE_PLOT_LABELS
|
||||
.get(mode as usize)
|
||||
.copied()
|
||||
.unwrap_or("As Displayed")
|
||||
}
|
||||
|
||||
// ── Standard view options ─────────────────────────────────────────────────
|
||||
|
||||
const STD_VIEWS: &[&str] = &[
|
||||
"Top", "Bottom", "Front", "Back", "Left", "Right",
|
||||
"SW Isometric", "SE Isometric", "NE Isometric", "NW Isometric",
|
||||
"Top",
|
||||
"Bottom",
|
||||
"Front",
|
||||
"Back",
|
||||
"Left",
|
||||
"Right",
|
||||
"SW Isometric",
|
||||
"SE Isometric",
|
||||
"NE Isometric",
|
||||
"NW Isometric",
|
||||
];
|
||||
|
||||
fn grips(vp: &Viewport) -> Vec<GripDef> {
|
||||
|
|
@ -144,27 +166,42 @@ fn properties(vp: &Viewport) -> PropSection {
|
|||
Property {
|
||||
label: "Locked".into(),
|
||||
field: "vp_locked",
|
||||
value: PropValue::BoolToggle { field: "vp_locked", value: vp.status.locked },
|
||||
value: PropValue::BoolToggle {
|
||||
field: "vp_locked",
|
||||
value: vp.status.locked,
|
||||
},
|
||||
},
|
||||
Property {
|
||||
label: "On".into(),
|
||||
field: "vp_on",
|
||||
value: PropValue::BoolToggle { field: "vp_on", value: vp.status.is_on },
|
||||
value: PropValue::BoolToggle {
|
||||
field: "vp_on",
|
||||
value: vp.status.is_on,
|
||||
},
|
||||
},
|
||||
Property {
|
||||
label: "Perspective".into(),
|
||||
field: "vp_perspective",
|
||||
value: PropValue::BoolToggle { field: "vp_perspective", value: vp.status.perspective },
|
||||
value: PropValue::BoolToggle {
|
||||
field: "vp_perspective",
|
||||
value: vp.status.perspective,
|
||||
},
|
||||
},
|
||||
Property {
|
||||
label: "Hide Plot".into(),
|
||||
field: "vp_hide_plot",
|
||||
value: PropValue::BoolToggle { field: "vp_hide_plot", value: vp.status.hide_plot },
|
||||
value: PropValue::BoolToggle {
|
||||
field: "vp_hide_plot",
|
||||
value: vp.status.hide_plot,
|
||||
},
|
||||
},
|
||||
Property {
|
||||
label: "UCS Icon".into(),
|
||||
field: "vp_ucs_icon",
|
||||
value: PropValue::BoolToggle { field: "vp_ucs_icon", value: vp.ucs_icon_visible },
|
||||
value: PropValue::BoolToggle {
|
||||
field: "vp_ucs_icon",
|
||||
value: vp.ucs_icon_visible,
|
||||
},
|
||||
},
|
||||
edit("Target X", "vtgt_x", vp.view_target.x),
|
||||
edit("Target Z", "vtgt_z", vp.view_target.z),
|
||||
|
|
@ -174,7 +211,10 @@ fn properties(vp: &Viewport) -> PropSection {
|
|||
Property {
|
||||
label: "UCS Per Viewport".into(),
|
||||
field: "vp_ucs_per_vp",
|
||||
value: PropValue::BoolToggle { field: "vp_ucs_per_vp", value: vp.ucs_per_viewport },
|
||||
value: PropValue::BoolToggle {
|
||||
field: "vp_ucs_per_vp",
|
||||
value: vp.ucs_per_viewport,
|
||||
},
|
||||
},
|
||||
edit("UCS Origin X", "vp_ucs_ox", vp.ucs_origin.x),
|
||||
edit("UCS Origin Y", "vp_ucs_oy", vp.ucs_origin.y),
|
||||
|
|
@ -197,16 +237,36 @@ fn viewport_view_label(vp: &Viewport) -> String {
|
|||
let dz = d.z;
|
||||
|
||||
// Use a simple threshold comparison to classify the view direction.
|
||||
if dx.abs() < 0.1 && dy.abs() < 0.1 && dz > 0.5 { return "Top".into(); }
|
||||
if dx.abs() < 0.1 && dy.abs() < 0.1 && dz < -0.5 { return "Bottom".into(); }
|
||||
if dx.abs() < 0.1 && dy < -0.5 && dz.abs() < 0.1 { return "Front".into(); }
|
||||
if dx.abs() < 0.1 && dy > 0.5 && dz.abs() < 0.1 { return "Back".into(); }
|
||||
if dx < -0.5 && dy.abs() < 0.1 && dz.abs() < 0.1 { return "Left".into(); }
|
||||
if dx > 0.5 && dy.abs() < 0.1 && dz.abs() < 0.1 { return "Right".into(); }
|
||||
if dx < -0.4 && dy < -0.4 && dz > 0.4 { return "SW Isometric".into(); }
|
||||
if dx > 0.4 && dy < -0.4 && dz > 0.4 { return "SE Isometric".into(); }
|
||||
if dx > 0.4 && dy > 0.4 && dz > 0.4 { return "NE Isometric".into(); }
|
||||
if dx < -0.4 && dy > 0.4 && dz > 0.4 { return "NW Isometric".into(); }
|
||||
if dx.abs() < 0.1 && dy.abs() < 0.1 && dz > 0.5 {
|
||||
return "Top".into();
|
||||
}
|
||||
if dx.abs() < 0.1 && dy.abs() < 0.1 && dz < -0.5 {
|
||||
return "Bottom".into();
|
||||
}
|
||||
if dx.abs() < 0.1 && dy < -0.5 && dz.abs() < 0.1 {
|
||||
return "Front".into();
|
||||
}
|
||||
if dx.abs() < 0.1 && dy > 0.5 && dz.abs() < 0.1 {
|
||||
return "Back".into();
|
||||
}
|
||||
if dx < -0.5 && dy.abs() < 0.1 && dz.abs() < 0.1 {
|
||||
return "Left".into();
|
||||
}
|
||||
if dx > 0.5 && dy.abs() < 0.1 && dz.abs() < 0.1 {
|
||||
return "Right".into();
|
||||
}
|
||||
if dx < -0.4 && dy < -0.4 && dz > 0.4 {
|
||||
return "SW Isometric".into();
|
||||
}
|
||||
if dx > 0.4 && dy < -0.4 && dz > 0.4 {
|
||||
return "SE Isometric".into();
|
||||
}
|
||||
if dx > 0.4 && dy > 0.4 && dz > 0.4 {
|
||||
return "NE Isometric".into();
|
||||
}
|
||||
if dx < -0.4 && dy > 0.4 && dz > 0.4 {
|
||||
return "NW Isometric".into();
|
||||
}
|
||||
"Custom".into()
|
||||
}
|
||||
|
||||
|
|
@ -216,27 +276,51 @@ fn apply_geom_prop(vp: &mut Viewport, field: &str, value: &str) {
|
|||
// Boolean / toggle fields handled first (value = "toggle" or "true"/"false").
|
||||
match field {
|
||||
"vp_locked" => {
|
||||
vp.status.locked = if value == "toggle" { !vp.status.locked } else { value == "true" };
|
||||
vp.status.locked = if value == "toggle" {
|
||||
!vp.status.locked
|
||||
} else {
|
||||
value == "true"
|
||||
};
|
||||
return;
|
||||
}
|
||||
"vp_on" => {
|
||||
vp.status.is_on = if value == "toggle" { !vp.status.is_on } else { value == "true" };
|
||||
vp.status.is_on = if value == "toggle" {
|
||||
!vp.status.is_on
|
||||
} else {
|
||||
value == "true"
|
||||
};
|
||||
return;
|
||||
}
|
||||
"vp_perspective" => {
|
||||
vp.status.perspective = if value == "toggle" { !vp.status.perspective } else { value == "true" };
|
||||
vp.status.perspective = if value == "toggle" {
|
||||
!vp.status.perspective
|
||||
} else {
|
||||
value == "true"
|
||||
};
|
||||
return;
|
||||
}
|
||||
"vp_hide_plot" => {
|
||||
vp.status.hide_plot = if value == "toggle" { !vp.status.hide_plot } else { value == "true" };
|
||||
vp.status.hide_plot = if value == "toggle" {
|
||||
!vp.status.hide_plot
|
||||
} else {
|
||||
value == "true"
|
||||
};
|
||||
return;
|
||||
}
|
||||
"vp_ucs_icon" => {
|
||||
vp.ucs_icon_visible = if value == "toggle" { !vp.ucs_icon_visible } else { value == "true" };
|
||||
vp.ucs_icon_visible = if value == "toggle" {
|
||||
!vp.ucs_icon_visible
|
||||
} else {
|
||||
value == "true"
|
||||
};
|
||||
return;
|
||||
}
|
||||
"vp_ucs_per_vp" => {
|
||||
vp.ucs_per_viewport = if value == "toggle" { !vp.ucs_per_viewport } else { value == "true" };
|
||||
vp.ucs_per_viewport = if value == "toggle" {
|
||||
!vp.ucs_per_viewport
|
||||
} else {
|
||||
value == "true"
|
||||
};
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -272,16 +356,16 @@ fn apply_geom_prop(vp: &mut Viewport, field: &str, value: &str) {
|
|||
// Standard view direction picker.
|
||||
if field == "vp_view" {
|
||||
let dir: Option<(f64, f64, f64)> = match value {
|
||||
"Top" => Some(( 0.0, 0.0, 1.0)),
|
||||
"Bottom" => Some(( 0.0, 0.0, -1.0)),
|
||||
"Front" => Some(( 0.0, -1.0, 0.0)),
|
||||
"Back" => Some(( 0.0, 1.0, 0.0)),
|
||||
"Left" => Some((-1.0, 0.0, 0.0)),
|
||||
"Right" => Some(( 1.0, 0.0, 0.0)),
|
||||
"SW Isometric" => Some((-1.0, -1.0, 1.0)),
|
||||
"SE Isometric" => Some(( 1.0, -1.0, 1.0)),
|
||||
"NE Isometric" => Some(( 1.0, 1.0, 1.0)),
|
||||
"NW Isometric" => Some((-1.0, 1.0, 1.0)),
|
||||
"Top" => Some((0.0, 0.0, 1.0)),
|
||||
"Bottom" => Some((0.0, 0.0, -1.0)),
|
||||
"Front" => Some((0.0, -1.0, 0.0)),
|
||||
"Back" => Some((0.0, 1.0, 0.0)),
|
||||
"Left" => Some((-1.0, 0.0, 0.0)),
|
||||
"Right" => Some((1.0, 0.0, 0.0)),
|
||||
"SW Isometric" => Some((-1.0, -1.0, 1.0)),
|
||||
"SE Isometric" => Some((1.0, -1.0, 1.0)),
|
||||
"NE Isometric" => Some((1.0, 1.0, 1.0)),
|
||||
"NW Isometric" => Some((-1.0, 1.0, 1.0)),
|
||||
_ => None,
|
||||
};
|
||||
if let Some((dx, dy, dz)) = dir {
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ pub mod step;
|
|||
pub mod stl;
|
||||
pub mod xref;
|
||||
|
||||
use crate::scene::DerivedCaches;
|
||||
use acadrust::entities::{Dimension, EntityType};
|
||||
use acadrust::io::dwg::DwgReader;
|
||||
use acadrust::{CadDocument, DwgWriter, DxfReader, DxfWriter};
|
||||
use crate::scene::DerivedCaches;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
// ── Open ──────────────────────────────────────────────────────────────────
|
||||
|
|
@ -43,7 +43,9 @@ pub async fn pick_and_open() -> Result<(String, PathBuf, CadDocument, DerivedCac
|
|||
/// Load a CAD file from a known path (used by recent files).
|
||||
/// Parsing and cache building run on a dedicated OS thread so the async
|
||||
/// executor stays free for rendering during the load.
|
||||
pub async fn open_path(path: PathBuf) -> Result<(String, PathBuf, CadDocument, DerivedCaches), String> {
|
||||
pub async fn open_path(
|
||||
path: PathBuf,
|
||||
) -> Result<(String, PathBuf, CadDocument, DerivedCaches), String> {
|
||||
let name = path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
|
|
@ -102,7 +104,9 @@ pub fn read_dir_entries(dir: &std::path::Path) -> Vec<(String, bool, PathBuf)> {
|
|||
if let Ok(rd) = std::fs::read_dir(dir) {
|
||||
for entry in rd.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().into_owned();
|
||||
if name.starts_with('.') { continue; }
|
||||
if name.starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
let is_dir = entry.file_type().map(|t| t.is_dir()).unwrap_or(false);
|
||||
if is_dir {
|
||||
dirs.push((name, true, entry.path()));
|
||||
|
|
@ -126,13 +130,21 @@ pub fn parse_save_format(format: &str) -> (&'static str, acadrust::DxfVersion) {
|
|||
let f = format.to_ascii_uppercase();
|
||||
let is_dxf = f.starts_with("DXF");
|
||||
let ext = if is_dxf { "dxf" } else { "dwg" };
|
||||
let version = if f.contains("2013") { DxfVersion::AC1027 }
|
||||
else if f.contains("2010") { DxfVersion::AC1024 }
|
||||
else if f.contains("2007") { DxfVersion::AC1021 }
|
||||
else if f.contains("2004") { DxfVersion::AC1018 }
|
||||
else if f.contains("2000") { DxfVersion::AC1015 }
|
||||
else if f.contains("R14") { DxfVersion::AC1014 }
|
||||
else { DxfVersion::AC1032 }; // 2018
|
||||
let version = if f.contains("2013") {
|
||||
DxfVersion::AC1027
|
||||
} else if f.contains("2010") {
|
||||
DxfVersion::AC1024
|
||||
} else if f.contains("2007") {
|
||||
DxfVersion::AC1021
|
||||
} else if f.contains("2004") {
|
||||
DxfVersion::AC1018
|
||||
} else if f.contains("2000") {
|
||||
DxfVersion::AC1015
|
||||
} else if f.contains("R14") {
|
||||
DxfVersion::AC1014
|
||||
} else {
|
||||
DxfVersion::AC1032
|
||||
}; // 2018
|
||||
(ext, version)
|
||||
}
|
||||
|
||||
|
|
@ -185,8 +197,10 @@ pub fn save_as_version(
|
|||
.map(|e| e.to_string_lossy().to_lowercase())
|
||||
.unwrap_or_default();
|
||||
match ext.as_str() {
|
||||
"dxf" => DxfWriter::new(&doc).write_to_file(path).map_err(|e| e.to_string()),
|
||||
_ => DwgWriter::write_to_file(path, &doc).map_err(|e| e.to_string()),
|
||||
"dxf" => DxfWriter::new(&doc)
|
||||
.write_to_file(path)
|
||||
.map_err(|e| e.to_string()),
|
||||
_ => DwgWriter::write_to_file(path, &doc).map_err(|e| e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -195,7 +209,6 @@ pub fn save(doc: &CadDocument, path: &Path) -> Result<(), String> {
|
|||
save_as_version(doc, path, doc.version)
|
||||
}
|
||||
|
||||
|
||||
// ── Post-load fixups ──────────────────────────────────────────────────────
|
||||
|
||||
/// acadrust's ViewportStatusFlags::from_bits() maps bit 0 → is_on and bit 15 → locked,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ pub fn parse_obj(src: &str, color: [f32; 4]) -> Option<MeshModel> {
|
|||
|
||||
for line in src.lines() {
|
||||
let line = line.trim();
|
||||
if line.starts_with('#') || line.is_empty() { continue; }
|
||||
if line.starts_with('#') || line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut parts = line.split_whitespace();
|
||||
let keyword = parts.next().unwrap_or("");
|
||||
|
|
@ -64,9 +66,9 @@ pub fn parse_obj(src: &str, color: [f32; 4]) -> Option<MeshModel> {
|
|||
}
|
||||
|
||||
// Build flat (un-indexed) vertex + normal arrays.
|
||||
let mut verts: Vec<[f32; 3]> = Vec::with_capacity(face_verts.len());
|
||||
let mut norms: Vec<[f32; 3]> = Vec::with_capacity(face_verts.len());
|
||||
let mut indices: Vec<u32> = Vec::with_capacity(face_verts.len());
|
||||
let mut verts: Vec<[f32; 3]> = Vec::with_capacity(face_verts.len());
|
||||
let mut norms: Vec<[f32; 3]> = Vec::with_capacity(face_verts.len());
|
||||
let mut indices: Vec<u32> = Vec::with_capacity(face_verts.len());
|
||||
|
||||
for (vi, (pos_i, norm_i)) in face_verts.iter().enumerate() {
|
||||
let pos = *positions.get(*pos_i).unwrap_or(&[0.0; 3]);
|
||||
|
|
@ -84,13 +86,13 @@ pub fn parse_obj(src: &str, color: [f32; 4]) -> Option<MeshModel> {
|
|||
let a = verts[tri[0] as usize];
|
||||
let b = verts[tri[1] as usize];
|
||||
let c = verts[tri[2] as usize];
|
||||
let ab = [b[0]-a[0], b[1]-a[1], b[2]-a[2]];
|
||||
let ac = [c[0]-a[0], c[1]-a[1], c[2]-a[2]];
|
||||
let nx = ab[1]*ac[2] - ab[2]*ac[1];
|
||||
let ny = ab[2]*ac[0] - ab[0]*ac[2];
|
||||
let nz = ab[0]*ac[1] - ab[1]*ac[0];
|
||||
let len = (nx*nx + ny*ny + nz*nz).sqrt().max(1e-12);
|
||||
let n = [nx/len, ny/len, nz/len];
|
||||
let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
||||
let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
|
||||
let nx = ab[1] * ac[2] - ab[2] * ac[1];
|
||||
let ny = ab[2] * ac[0] - ab[0] * ac[2];
|
||||
let nz = ab[0] * ac[1] - ab[1] * ac[0];
|
||||
let len = (nx * nx + ny * ny + nz * nz).sqrt().max(1e-12);
|
||||
let n = [nx / len, ny / len, nz / len];
|
||||
norms[tri[0] as usize] = n;
|
||||
norms[tri[1] as usize] = n;
|
||||
norms[tri[2] as usize] = n;
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@
|
|||
// is needed — we shift the coordinates by (offset_x, offset_y) to place the
|
||||
// drawing origin at the paper origin.
|
||||
|
||||
use crate::scene::WireModel;
|
||||
use crate::io::plot_style::PlotStyleTable;
|
||||
use printpdf::{Color, Line, LineCapStyle, LineJoinStyle, LinePoint, Mm, Op, PdfDocument,
|
||||
PdfPage, PdfSaveOptions, Point, Pt, Rgb};
|
||||
use crate::scene::WireModel;
|
||||
use printpdf::{
|
||||
Color, Line, LineCapStyle, LineJoinStyle, LinePoint, Mm, Op, PdfDocument, PdfPage,
|
||||
PdfSaveOptions, Point, Pt, Rgb,
|
||||
};
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
|
|
@ -33,7 +35,15 @@ pub fn export_pdf(
|
|||
path: &Path,
|
||||
plot_style: Option<&PlotStyleTable>,
|
||||
) -> Result<(), String> {
|
||||
let bytes = build_pdf(wires, paper_w as f32, paper_h as f32, offset_x, offset_y, rotation_deg, plot_style);
|
||||
let bytes = build_pdf(
|
||||
wires,
|
||||
paper_w as f32,
|
||||
paper_h as f32,
|
||||
offset_x,
|
||||
offset_y,
|
||||
rotation_deg,
|
||||
plot_style,
|
||||
);
|
||||
let mut file = std::fs::File::create(path).map_err(|e| e.to_string())?;
|
||||
file.write_all(&bytes).map_err(|e| e.to_string())
|
||||
}
|
||||
|
|
@ -52,21 +62,38 @@ pub async fn pick_pdf_path_owned(stem: String) -> Option<std::path::PathBuf> {
|
|||
|
||||
// ── PDF builder ───────────────────────────────────────────────────────────
|
||||
|
||||
fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32, ox: f32, oy: f32, rotation_deg: i32, plot_style: Option<&PlotStyleTable>) -> Vec<u8> {
|
||||
fn build_pdf(
|
||||
wires: &[WireModel],
|
||||
paper_w: f32,
|
||||
paper_h: f32,
|
||||
ox: f32,
|
||||
oy: f32,
|
||||
rotation_deg: i32,
|
||||
plot_style: Option<&PlotStyleTable>,
|
||||
) -> Vec<u8> {
|
||||
let mut doc = PdfDocument::new("H7CAD Export");
|
||||
let mut ops: Vec<Op> = Vec::new();
|
||||
|
||||
// White page background.
|
||||
ops.push(Op::SetFillColor {
|
||||
col: Color::Rgb(Rgb { r: 1.0, g: 1.0, b: 1.0, icc_profile: None }),
|
||||
col: Color::Rgb(Rgb {
|
||||
r: 1.0,
|
||||
g: 1.0,
|
||||
b: 1.0,
|
||||
icc_profile: None,
|
||||
}),
|
||||
});
|
||||
ops.push(Op::DrawRectangle {
|
||||
rectangle: printpdf::Rect::from_wh(Mm(paper_w).into(), Mm(paper_h).into()),
|
||||
});
|
||||
|
||||
// Round line caps/joins for CAD aesthetics.
|
||||
ops.push(Op::SetLineCapStyle { cap: LineCapStyle::Round });
|
||||
ops.push(Op::SetLineJoinStyle { join: LineJoinStyle::Round });
|
||||
ops.push(Op::SetLineCapStyle {
|
||||
cap: LineCapStyle::Round,
|
||||
});
|
||||
ops.push(Op::SetLineJoinStyle {
|
||||
join: LineJoinStyle::Round,
|
||||
});
|
||||
|
||||
// Apply rotation transform if needed.
|
||||
// PDF uses mm-based coordinate system with origin at bottom-left.
|
||||
|
|
@ -74,10 +101,10 @@ fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32, ox: f32, oy: f32,
|
|||
let needs_rotation = rotation_deg != 0;
|
||||
if needs_rotation {
|
||||
let (cos_a, sin_a, tx, ty) = match rotation_deg {
|
||||
90 => ( 0.0_f64, 1.0_f64, 0.0, paper_h as f64),
|
||||
180 => (-1.0_f64, 0.0_f64, paper_w as f64, paper_h as f64),
|
||||
270 => ( 0.0_f64, -1.0_f64, paper_w as f64, 0.0),
|
||||
_ => ( 1.0_f64, 0.0_f64, 0.0, 0.0),
|
||||
90 => (0.0_f64, 1.0_f64, 0.0, paper_h as f64),
|
||||
180 => (-1.0_f64, 0.0_f64, paper_w as f64, paper_h as f64),
|
||||
270 => (0.0_f64, -1.0_f64, paper_w as f64, 0.0),
|
||||
_ => (1.0_f64, 0.0_f64, 0.0, 0.0),
|
||||
};
|
||||
// PDF CTM: [a b c d e f] = [cos sin -sin cos tx ty]
|
||||
ops.push(Op::SaveGraphicsState);
|
||||
|
|
@ -86,9 +113,12 @@ fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32, ox: f32, oy: f32,
|
|||
let ty_pt = (ty * 2.834645) as f32;
|
||||
ops.push(Op::SetTransformationMatrix {
|
||||
matrix: printpdf::CurTransMat::Raw([
|
||||
cos_a as f32, sin_a as f32,
|
||||
-(sin_a as f32), cos_a as f32,
|
||||
tx_pt, ty_pt,
|
||||
cos_a as f32,
|
||||
sin_a as f32,
|
||||
-(sin_a as f32),
|
||||
cos_a as f32,
|
||||
tx_pt,
|
||||
ty_pt,
|
||||
]),
|
||||
});
|
||||
}
|
||||
|
|
@ -114,9 +144,12 @@ fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32, ox: f32, oy: f32,
|
|||
if let Some(ctb) = plot_style {
|
||||
if wire.aci > 0 {
|
||||
if let Some([cr, cg, cb]) = ctb.resolve_color(wire.aci) {
|
||||
r = cr; g = cg; b = cb;
|
||||
r = cr;
|
||||
g = cg;
|
||||
b = cb;
|
||||
}
|
||||
lw_override = ctb.resolve_lineweight(wire.aci)
|
||||
lw_override = ctb
|
||||
.resolve_lineweight(wire.aci)
|
||||
.map(|mm| (mm * MM_TO_PT).max(0.1));
|
||||
}
|
||||
}
|
||||
|
|
@ -125,27 +158,36 @@ fn build_pdf(wires: &[WireModel], paper_w: f32, paper_h: f32, ox: f32, oy: f32,
|
|||
if lw_override.is_none() {
|
||||
let is_light = r > 0.80 && g > 0.80 && b > 0.80;
|
||||
let is_yellow = r > 0.80 && g > 0.70 && b < 0.30;
|
||||
let is_cyan = r < 0.30 && g > 0.70 && b > 0.70;
|
||||
let is_cyan = r < 0.30 && g > 0.70 && b > 0.70;
|
||||
if is_light || is_yellow {
|
||||
r = 0.0; g = 0.0; b = 0.0;
|
||||
r = 0.0;
|
||||
g = 0.0;
|
||||
b = 0.0;
|
||||
} else if is_cyan {
|
||||
// Viewport border: print as dark blue.
|
||||
r = 0.0; g = 0.15; b = 0.50;
|
||||
r = 0.0;
|
||||
g = 0.15;
|
||||
b = 0.50;
|
||||
}
|
||||
}
|
||||
|
||||
if last_color.map(|c| {
|
||||
(c[0]-r).abs() > 0.01 || (c[1]-g).abs() > 0.01 || (c[2]-b).abs() > 0.01
|
||||
}).unwrap_or(true) {
|
||||
if last_color
|
||||
.map(|c| (c[0] - r).abs() > 0.01 || (c[1] - g).abs() > 0.01 || (c[2] - b).abs() > 0.01)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
ops.push(Op::SetOutlineColor {
|
||||
col: Color::Rgb(Rgb { r, g, b, icc_profile: None }),
|
||||
col: Color::Rgb(Rgb {
|
||||
r,
|
||||
g,
|
||||
b,
|
||||
icc_profile: None,
|
||||
}),
|
||||
});
|
||||
last_color = Some([r, g, b]);
|
||||
}
|
||||
|
||||
// Line weight: CTB override (in pt) or screen px → points.
|
||||
let lw_pt = lw_override
|
||||
.unwrap_or_else(|| (wire.line_weight_px * PX_TO_PT).max(0.1));
|
||||
let lw_pt = lw_override.unwrap_or_else(|| (wire.line_weight_px * PX_TO_PT).max(0.1));
|
||||
if last_lw.map(|l| (l - lw_pt).abs() > 0.01).unwrap_or(true) {
|
||||
ops.push(Op::SetOutlineThickness { pt: Pt(lw_pt) });
|
||||
last_lw = Some(lw_pt);
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@ use std::path::Path;
|
|||
/// Lineweight table: index value → mm, matching AutoCAD's LWEIGHT codes.
|
||||
/// Index 0 = 0.00 mm (hairline), others follow the DXF lineweight enum.
|
||||
pub const LW_TABLE: &[f32] = &[
|
||||
0.00, 0.05, 0.09, 0.10, 0.13, 0.15, 0.18, 0.20,
|
||||
0.25, 0.30, 0.35, 0.40, 0.50, 0.53, 0.60, 0.70,
|
||||
0.00, 0.05, 0.09, 0.10, 0.13, 0.15, 0.18, 0.20, 0.25, 0.30, 0.35, 0.40, 0.50, 0.53, 0.60, 0.70,
|
||||
0.80, 0.90, 1.00, 1.06, 1.20, 1.40, 1.58, 2.00, 2.11,
|
||||
];
|
||||
|
||||
|
|
@ -81,8 +80,11 @@ impl PlotStyleTable {
|
|||
/// Load a CTB or STB file from disk.
|
||||
pub fn load(path: &Path) -> Result<Self, String> {
|
||||
let raw = std::fs::read(path).map_err(|e| e.to_string())?;
|
||||
let name = path.file_name()
|
||||
.unwrap_or_default().to_string_lossy().into_owned();
|
||||
let name = path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let is_stb = name.to_lowercase().ends_with(".stb");
|
||||
let text = decompress_ctb(&raw)?;
|
||||
parse_plot_style_text(&text, name, is_stb)
|
||||
|
|
@ -100,9 +102,9 @@ impl PlotStyleTable {
|
|||
/// Returns None if no override (use object color).
|
||||
pub fn resolve_color(&self, aci: u8) -> Option<[f32; 3]> {
|
||||
let entry = self.aci_entries.get(aci as usize)?;
|
||||
entry.color.map(|[r, g, b]| {
|
||||
[r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0]
|
||||
})
|
||||
entry
|
||||
.color
|
||||
.map(|[r, g, b]| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0])
|
||||
}
|
||||
|
||||
/// Resolve the effective lineweight in mm for the given ACI index.
|
||||
|
|
@ -158,7 +160,9 @@ impl PlotStyleTable {
|
|||
/// (with the two-byte zlib header 0x78 0x9C) instead — we handle both.
|
||||
fn decompress_ctb(data: &[u8]) -> Result<String, String> {
|
||||
// Find the first newline — everything after it is the compressed payload.
|
||||
let split_at = data.iter().position(|&b| b == b'\n')
|
||||
let split_at = data
|
||||
.iter()
|
||||
.position(|&b| b == b'\n')
|
||||
.map(|p| p + 1)
|
||||
.unwrap_or(0);
|
||||
let payload = &data[split_at..];
|
||||
|
|
@ -196,12 +200,9 @@ fn compress_ctb(text: &[u8]) -> Result<Vec<u8>, String> {
|
|||
|
||||
// ── Text parser ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn parse_plot_style_text(
|
||||
text: &str,
|
||||
name: String,
|
||||
is_stb: bool,
|
||||
) -> Result<PlotStyleTable, String> {
|
||||
let mut aci_entries: Vec<PlotStyleEntry> = (0..=255).map(|_| PlotStyleEntry::default()).collect();
|
||||
fn parse_plot_style_text(text: &str, name: String, is_stb: bool) -> Result<PlotStyleTable, String> {
|
||||
let mut aci_entries: Vec<PlotStyleEntry> =
|
||||
(0..=255).map(|_| PlotStyleEntry::default()).collect();
|
||||
let mut named_entries: HashMap<String, PlotStyleEntry> = HashMap::new();
|
||||
let mut style_index: usize = 1; // CTB: 1-based ACI index
|
||||
let mut current: Option<PlotStyleEntry> = None;
|
||||
|
|
@ -225,7 +226,9 @@ fn parse_plot_style_text(
|
|||
}
|
||||
continue;
|
||||
}
|
||||
let Some(entry) = current.as_mut() else { continue };
|
||||
let Some(entry) = current.as_mut() else {
|
||||
continue;
|
||||
};
|
||||
if let Some((key, val)) = line.split_once('=') {
|
||||
let key = key.trim();
|
||||
let val = val.trim();
|
||||
|
|
@ -237,10 +240,14 @@ fn parse_plot_style_text(
|
|||
}
|
||||
}
|
||||
"screen" => {
|
||||
if let Ok(v) = val.parse::<u8>() { entry.screening = v; }
|
||||
if let Ok(v) = val.parse::<u8>() {
|
||||
entry.screening = v;
|
||||
}
|
||||
}
|
||||
"lineweight" => {
|
||||
if let Ok(v) = val.parse::<u8>() { entry.lineweight = v; }
|
||||
if let Ok(v) = val.parse::<u8>() {
|
||||
entry.lineweight = v;
|
||||
}
|
||||
}
|
||||
"color1" => {
|
||||
if val.starts_with('#') && val.len() == 7 {
|
||||
|
|
@ -255,8 +262,8 @@ fn parse_plot_style_text(
|
|||
if packed != -1056964608i32 {
|
||||
let u = packed as u32;
|
||||
let r = ((u >> 16) & 0xFF) as u8;
|
||||
let g = ((u >> 8) & 0xFF) as u8;
|
||||
let b = ( u & 0xFF) as u8;
|
||||
let g = ((u >> 8) & 0xFF) as u8;
|
||||
let b = (u & 0xFF) as u8;
|
||||
entry.color = Some([r, g, b]);
|
||||
}
|
||||
}
|
||||
|
|
@ -266,5 +273,10 @@ fn parse_plot_style_text(
|
|||
}
|
||||
}
|
||||
|
||||
Ok(PlotStyleTable { name, is_stb, aci_entries, named_entries })
|
||||
Ok(PlotStyleTable {
|
||||
name,
|
||||
is_stb,
|
||||
aci_entries,
|
||||
named_entries,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
//
|
||||
// The function is async so the UI remains responsive while the job is queued.
|
||||
|
||||
use crate::io::plot_style::PlotStyleTable;
|
||||
use crate::io::pdf_export;
|
||||
use crate::io::plot_style::PlotStyleTable;
|
||||
use crate::scene::WireModel;
|
||||
|
||||
/// Render `wires` to a temp PDF and dispatch it to the default system printer.
|
||||
|
|
|
|||
137
src/io/step.rs
137
src/io/step.rs
|
|
@ -27,10 +27,10 @@ pub fn build_step(meshes: &[&MeshModel]) -> Option<String> {
|
|||
|
||||
let mut tris: Vec<Tri> = Vec::new();
|
||||
for mesh in meshes {
|
||||
let verts = &mesh.verts;
|
||||
let verts = &mesh.verts;
|
||||
let normals = &mesh.normals;
|
||||
let idx = &mesh.indices;
|
||||
let n_tri = idx.len() / 3;
|
||||
let idx = &mesh.indices;
|
||||
let n_tri = idx.len() / 3;
|
||||
for t in 0..n_tri {
|
||||
let i0 = idx[t * 3] as usize;
|
||||
let i1 = idx[t * 3 + 1] as usize;
|
||||
|
|
@ -44,13 +44,13 @@ pub fn build_step(meshes: &[&MeshModel]) -> Option<String> {
|
|||
let n = if !normals.is_empty() && i0 < normals.len() {
|
||||
normals[i0]
|
||||
} else {
|
||||
let ab = [b[0]-a[0], b[1]-a[1], b[2]-a[2]];
|
||||
let ac = [c[0]-a[0], c[1]-a[1], c[2]-a[2]];
|
||||
let nx = ab[1]*ac[2] - ab[2]*ac[1];
|
||||
let ny = ab[2]*ac[0] - ab[0]*ac[2];
|
||||
let nz = ab[0]*ac[1] - ab[1]*ac[0];
|
||||
let len = (nx*nx + ny*ny + nz*nz).sqrt().max(f32::EPSILON);
|
||||
[nx/len, ny/len, nz/len]
|
||||
let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
||||
let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
|
||||
let nx = ab[1] * ac[2] - ab[2] * ac[1];
|
||||
let ny = ab[2] * ac[0] - ab[0] * ac[2];
|
||||
let nz = ab[0] * ac[1] - ab[1] * ac[0];
|
||||
let len = (nx * nx + ny * ny + nz * nz).sqrt().max(f32::EPSILON);
|
||||
[nx / len, ny / len, nz / len]
|
||||
};
|
||||
tris.push(Tri { v: [a, b, c], n });
|
||||
}
|
||||
|
|
@ -95,18 +95,22 @@ pub fn build_step(meshes: &[&MeshModel]) -> Option<String> {
|
|||
// Edge loop.
|
||||
let el = alloc();
|
||||
// Plane normal direction and axis placement.
|
||||
let norm_dir = alloc();
|
||||
let plane_ax = alloc();
|
||||
let plane = alloc();
|
||||
let norm_dir = alloc();
|
||||
let plane_ax = alloc();
|
||||
let plane = alloc();
|
||||
// Advanced face.
|
||||
let face_id = alloc();
|
||||
let face_id = alloc();
|
||||
face_ids.push(face_id);
|
||||
|
||||
// Emit cartesian points.
|
||||
for k in 0..3 {
|
||||
let [x, y, z] = tri.v[k];
|
||||
writeln!(data, "#{} = CARTESIAN_POINT('',({:.6},{:.6},{:.6}));",
|
||||
cp[k], x, y, z).ok();
|
||||
writeln!(
|
||||
data,
|
||||
"#{} = CARTESIAN_POINT('',({:.6},{:.6},{:.6}));",
|
||||
cp[k], x, y, z
|
||||
)
|
||||
.ok();
|
||||
writeln!(data, "#{} = VERTEX_POINT('',#{});", vpref[k], cp[k]).ok();
|
||||
}
|
||||
// Emit vertex points (binding).
|
||||
|
|
@ -124,52 +128,99 @@ pub fn build_step(meshes: &[&MeshModel]) -> Option<String> {
|
|||
tri.v[k1][1] - tri.v[k][1],
|
||||
tri.v[k1][2] - tri.v[k][2],
|
||||
];
|
||||
let len = (dx*dx + dy*dy + dz*dz).sqrt().max(f32::EPSILON);
|
||||
writeln!(data, "#{} = DIRECTION('',({:.6},{:.6},{:.6}));",
|
||||
dir[k], dx/len, dy/len, dz/len).ok();
|
||||
writeln!(data, "#{} = LINE('',#{},VECTOR('',#{},1.));",
|
||||
lc[k], cp[k], dir[k]).ok();
|
||||
writeln!(data, "#{} = EDGE_CURVE('',#{},#{},#{},.T.);",
|
||||
ec[k], vpref[k], vpref[k1], lc[k]).ok();
|
||||
writeln!(data, "#{} = ORIENTED_EDGE('',*,*,#{},.T.);",
|
||||
oe[k], ec[k]).ok();
|
||||
let len = (dx * dx + dy * dy + dz * dz).sqrt().max(f32::EPSILON);
|
||||
writeln!(
|
||||
data,
|
||||
"#{} = DIRECTION('',({:.6},{:.6},{:.6}));",
|
||||
dir[k],
|
||||
dx / len,
|
||||
dy / len,
|
||||
dz / len
|
||||
)
|
||||
.ok();
|
||||
writeln!(
|
||||
data,
|
||||
"#{} = LINE('',#{},VECTOR('',#{},1.));",
|
||||
lc[k], cp[k], dir[k]
|
||||
)
|
||||
.ok();
|
||||
writeln!(
|
||||
data,
|
||||
"#{} = EDGE_CURVE('',#{},#{},#{},.T.);",
|
||||
ec[k], vpref[k], vpref[k1], lc[k]
|
||||
)
|
||||
.ok();
|
||||
writeln!(data, "#{} = ORIENTED_EDGE('',*,*,#{},.T.);", oe[k], ec[k]).ok();
|
||||
}
|
||||
|
||||
// Edge loop and face normal.
|
||||
writeln!(data, "#{} = EDGE_LOOP('',({},{},{}));",
|
||||
writeln!(
|
||||
data,
|
||||
"#{} = EDGE_LOOP('',({},{},{}));",
|
||||
el,
|
||||
format!("#{}", oe[0]),
|
||||
format!("#{}", oe[1]),
|
||||
format!("#{}", oe[2])).ok();
|
||||
format!("#{}", oe[2])
|
||||
)
|
||||
.ok();
|
||||
|
||||
let [nx, ny, nz] = tri.n;
|
||||
writeln!(data, "#{} = DIRECTION('',({:.6},{:.6},{:.6}));",
|
||||
norm_dir, nx, ny, nz).ok();
|
||||
writeln!(data, "#{} = AXIS2_PLACEMENT_3D('',#{},#{},#{});",
|
||||
plane_ax, cp[0], norm_dir, dir[0]).ok();
|
||||
writeln!(
|
||||
data,
|
||||
"#{} = DIRECTION('',({:.6},{:.6},{:.6}));",
|
||||
norm_dir, nx, ny, nz
|
||||
)
|
||||
.ok();
|
||||
writeln!(
|
||||
data,
|
||||
"#{} = AXIS2_PLACEMENT_3D('',#{},#{},#{});",
|
||||
plane_ax, cp[0], norm_dir, dir[0]
|
||||
)
|
||||
.ok();
|
||||
writeln!(data, "#{} = PLANE('',#{});", plane, plane_ax).ok();
|
||||
writeln!(data, "#{} = ADVANCED_FACE('',(FACE_BOUND('',#{},.T.)),#{},.T.);",
|
||||
face_id, el, plane).ok();
|
||||
writeln!(
|
||||
data,
|
||||
"#{} = ADVANCED_FACE('',(FACE_BOUND('',#{},.T.)),#{},.T.);",
|
||||
face_id, el, plane
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Closed shell wrapping all faces.
|
||||
let shell_id = alloc();
|
||||
let face_list: String = face_ids.iter().map(|id| format!("#{id}")).collect::<Vec<_>>().join(",");
|
||||
let face_list: String = face_ids
|
||||
.iter()
|
||||
.map(|id| format!("#{id}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
writeln!(data, "#{} = CLOSED_SHELL('',({face_list}));", shell_id).ok();
|
||||
|
||||
// Manifold solid B-rep.
|
||||
let msb_id = alloc();
|
||||
writeln!(data, "#{} = MANIFOLD_SOLID_BREP('H7CAD_Solid',#{});", msb_id, shell_id).ok();
|
||||
writeln!(
|
||||
data,
|
||||
"#{} = MANIFOLD_SOLID_BREP('H7CAD_Solid',#{});",
|
||||
msb_id, shell_id
|
||||
)
|
||||
.ok();
|
||||
|
||||
// Shape representation.
|
||||
let sr_id = alloc();
|
||||
let pu_id = alloc();
|
||||
let gc_id = alloc();
|
||||
writeln!(data, "#{} = (LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.));", pu_id).ok();
|
||||
writeln!(
|
||||
data,
|
||||
"#{} = (LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.));",
|
||||
pu_id
|
||||
)
|
||||
.ok();
|
||||
writeln!(data, "#{} = GEOMETRIC_REPRESENTATION_CONTEXT(3);", gc_id).ok();
|
||||
writeln!(data,
|
||||
writeln!(
|
||||
data,
|
||||
"#{sr_id} = SHAPE_REPRESENTATION('H7CAD_Shape',(#{}),#{gc_id});",
|
||||
msb_id).ok();
|
||||
msb_id
|
||||
)
|
||||
.ok();
|
||||
|
||||
// ── Assemble file ─────────────────────────────────────────────────────
|
||||
let ts = chrono_timestamp();
|
||||
|
|
@ -199,16 +250,16 @@ fn chrono_timestamp() -> String {
|
|||
.unwrap_or(0);
|
||||
// Format: YYYY-MM-DDTHH:MM:SS (approximate UTC from epoch seconds).
|
||||
let s = secs;
|
||||
let mins = s / 60;
|
||||
let mins = s / 60;
|
||||
let hours = mins / 60;
|
||||
let days = hours / 24;
|
||||
let days = hours / 24;
|
||||
let hh = hours % 24;
|
||||
let mm = mins % 60;
|
||||
let ss = s % 60;
|
||||
// Days since epoch → year/month/day (approximate, ignoring leap years).
|
||||
let year = 1970 + days / 365;
|
||||
let doy = days % 365;
|
||||
let year = 1970 + days / 365;
|
||||
let doy = days % 365;
|
||||
let month = doy / 30 + 1;
|
||||
let day = doy % 30 + 1;
|
||||
let day = doy % 30 + 1;
|
||||
format!("{year:04}-{month:02}-{day:02}T{hh:02}:{mm:02}:{ss:02}")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,10 @@ pub fn build_stl(meshes: &[&MeshModel]) -> Option<Vec<u8>> {
|
|||
[nx / len, ny / len, nz / len]
|
||||
};
|
||||
|
||||
tris.push(Tri { normal, v: [a, b, c] });
|
||||
tris.push(Tri {
|
||||
normal,
|
||||
v: [a, b, c],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -108,9 +108,9 @@ fn resolve_path(raw: &str, base_dir: &Path) -> Option<PathBuf> {
|
|||
/// Make sure `doc` has BLOCK + ENDBLK entities for `block_name`.
|
||||
/// These are required so renderers can find the block content.
|
||||
fn ensure_block_entities(doc: &mut CadDocument, block_name: &str) {
|
||||
let has_block = doc.entities().any(|e| {
|
||||
matches!(e, EntityType::Block(b) if b.name == block_name)
|
||||
});
|
||||
let has_block = doc
|
||||
.entities()
|
||||
.any(|e| matches!(e, EntityType::Block(b) if b.name == block_name));
|
||||
if has_block {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -377,8 +377,15 @@ fn parse_text_element(inner: &str) -> Option<LtSegment> {
|
|||
let after = rest[end + 1..].trim().trim_start_matches(',');
|
||||
|
||||
let mut parts = after.split(',');
|
||||
let style = parts.next().map(|s| s.trim().to_string()).unwrap_or_default();
|
||||
let style = if style.is_empty() { "Standard".to_string() } else { style };
|
||||
let style = parts
|
||||
.next()
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_default();
|
||||
let style = if style.is_empty() {
|
||||
"Standard".to_string()
|
||||
} else {
|
||||
style
|
||||
};
|
||||
|
||||
let mut x = 0.0f32;
|
||||
let mut y = 0.0f32;
|
||||
|
|
@ -403,7 +410,14 @@ fn parse_text_element(inner: &str) -> Option<LtSegment> {
|
|||
}
|
||||
}
|
||||
|
||||
Some(LtSegment::Text { text, style, x, y, scale, rot_deg })
|
||||
Some(LtSegment::Text {
|
||||
text,
|
||||
style,
|
||||
x,
|
||||
y,
|
||||
scale,
|
||||
rot_deg,
|
||||
})
|
||||
}
|
||||
|
||||
fn push_lt_segment(token: &str, out: &mut Vec<LtSegment>) {
|
||||
|
|
|
|||
|
|
@ -37,13 +37,15 @@ impl AlignedDimensionCommand {
|
|||
}
|
||||
|
||||
impl CadCommand for AlignedDimensionCommand {
|
||||
fn name(&self) -> &'static str { "DIMALIGNED" }
|
||||
fn name(&self) -> &'static str {
|
||||
"DIMALIGNED"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match self.step {
|
||||
Step::First => "DIMALIGNED Specify first extension line origin:".into(),
|
||||
Step::Second(_) => "DIMALIGNED Specify second extension line origin:".into(),
|
||||
Step::DimLine { .. } => "DIMALIGNED Specify dimension line location:".into(),
|
||||
Step::First => "DIMALIGNED Specify first extension line origin:".into(),
|
||||
Step::Second(_) => "DIMALIGNED Specify second extension line origin:".into(),
|
||||
Step::DimLine { .. } => "DIMALIGNED Specify dimension line location:".into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -65,13 +67,11 @@ impl CadCommand for AlignedDimensionCommand {
|
|||
let offset = (dx * dx + dy * dy).sqrt();
|
||||
dim.set_offset(offset);
|
||||
dim.base.definition_point = v3(pt);
|
||||
dim.base.text_middle_point = v3(
|
||||
Vec3::new(
|
||||
(p1.x + p2.x) * 0.5,
|
||||
(p1.y + p2.y) * 0.5,
|
||||
(p1.z + p2.z) * 0.5,
|
||||
)
|
||||
);
|
||||
dim.base.text_middle_point = v3(Vec3::new(
|
||||
(p1.x + p2.x) * 0.5,
|
||||
(p1.y + p2.y) * 0.5,
|
||||
(p1.z + p2.z) * 0.5,
|
||||
));
|
||||
dim.base.insertion_point = dim.base.text_middle_point;
|
||||
dim.base.actual_measurement = dim.measurement();
|
||||
CmdResult::CommitAndExit(EntityType::Dimension(Dimension::Aligned(dim)))
|
||||
|
|
@ -79,7 +79,9 @@ impl CadCommand for AlignedDimensionCommand {
|
|||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
|
||||
let (p1, p2) = match self.step {
|
||||
|
|
@ -123,11 +125,14 @@ fn preview_aligned(p1: Vec3, p2: Vec3, dim_pt: Vec3) -> WireModel {
|
|||
WireModel {
|
||||
name: "dimaligned_preview".into(),
|
||||
points: vec![
|
||||
[p1.x, p1.y, p1.z], [d1.x, d1.y, d1.z],
|
||||
[p1.x, p1.y, p1.z],
|
||||
[d1.x, d1.y, d1.z],
|
||||
[f32::NAN, 0.0, 0.0],
|
||||
[p2.x, p2.y, p2.z], [d2.x, d2.y, d2.z],
|
||||
[p2.x, p2.y, p2.z],
|
||||
[d2.x, d2.y, d2.z],
|
||||
[f32::NAN, 0.0, 0.0],
|
||||
[d1.x, d1.y, d1.z], [d2.x, d2.y, d2.z],
|
||||
[d1.x, d1.y, d1.z],
|
||||
[d2.x, d2.y, d2.z],
|
||||
],
|
||||
color: WireModel::CYAN,
|
||||
selected: false,
|
||||
|
|
@ -137,10 +142,10 @@ fn preview_aligned(p1: Vec3, p2: Vec3, dim_pt: Vec3) -> WireModel {
|
|||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
aci: 0,
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,8 +21,15 @@ pub fn tool() -> ToolDef {
|
|||
enum Step {
|
||||
Vertex,
|
||||
FirstRay(Vec3),
|
||||
SecondRay { vertex: Vec3, first: Vec3 },
|
||||
ArcPoint { vertex: Vec3, first: Vec3, second: Vec3 },
|
||||
SecondRay {
|
||||
vertex: Vec3,
|
||||
first: Vec3,
|
||||
},
|
||||
ArcPoint {
|
||||
vertex: Vec3,
|
||||
first: Vec3,
|
||||
second: Vec3,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct AngularDimensionCommand {
|
||||
|
|
@ -95,9 +102,13 @@ impl CadCommand for AngularDimensionCommand {
|
|||
match self.step {
|
||||
Step::Vertex => None,
|
||||
Step::FirstRay(vertex) => Some(preview_wire(vec![vertex, pt])),
|
||||
Step::SecondRay { vertex, first } => {
|
||||
Some(preview_wire(vec![vertex, first, Vec3::new(f32::NAN, f32::NAN, f32::NAN), vertex, pt]))
|
||||
}
|
||||
Step::SecondRay { vertex, first } => Some(preview_wire(vec![
|
||||
vertex,
|
||||
first,
|
||||
Vec3::new(f32::NAN, f32::NAN, f32::NAN),
|
||||
vertex,
|
||||
pt,
|
||||
])),
|
||||
Step::ArcPoint {
|
||||
vertex,
|
||||
first,
|
||||
|
|
@ -123,11 +134,11 @@ fn preview_wire(points: Vec<Vec3>) -> WireModel {
|
|||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
aci: 0,
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,17 +32,23 @@ pub struct DdeditCommand {
|
|||
|
||||
impl DdeditCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { step: DdeditStep::PickEntity }
|
||||
Self {
|
||||
step: DdeditStep::PickEntity,
|
||||
}
|
||||
}
|
||||
|
||||
/// Start with a pre-picked entity (for double-click use).
|
||||
pub fn with_handle(handle: Handle, current: String) -> Self {
|
||||
Self { step: DdeditStep::EnterText { handle, current } }
|
||||
Self {
|
||||
step: DdeditStep::EnterText { handle, current },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for DdeditCommand {
|
||||
fn name(&self) -> &'static str { "DDEDIT" }
|
||||
fn name(&self) -> &'static str {
|
||||
"DDEDIT"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
|
|
@ -58,10 +64,15 @@ impl CadCommand for DdeditCommand {
|
|||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult {
|
||||
if handle.is_null() { return CmdResult::NeedPoint; }
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
// The current value will be filled in by the caller (commands.rs dispatch)
|
||||
// via on_text_input once the entity is known. Store handle here.
|
||||
self.step = DdeditStep::EnterText { handle, current: String::new() };
|
||||
self.step = DdeditStep::EnterText {
|
||||
handle,
|
||||
current: String::new(),
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
|
|
@ -75,23 +86,32 @@ impl CadCommand for DdeditCommand {
|
|||
_ => return None,
|
||||
};
|
||||
// Empty input → keep existing text
|
||||
let new_text = if text.trim().is_empty() { current } else { text.to_string() };
|
||||
let new_text = if text.trim().is_empty() {
|
||||
current
|
||||
} else {
|
||||
text.to_string()
|
||||
};
|
||||
Some(CmdResult::DdeditEntity { handle, new_text })
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult { CmdResult::NeedPoint }
|
||||
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the text content from a Text or MText entity.
|
||||
pub fn entity_text(entity: &EntityType) -> Option<String> {
|
||||
match entity {
|
||||
EntityType::Text(t) => Some(t.value.clone()),
|
||||
EntityType::Text(t) => Some(t.value.clone()),
|
||||
EntityType::MText(t) => Some(t.value.clone()),
|
||||
EntityType::AttributeDefinition(a) => Some(a.default_value.clone()),
|
||||
EntityType::AttributeEntity(a) => Some(a.get_value().to_string()),
|
||||
EntityType::AttributeEntity(a) => Some(a.get_value().to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,18 +32,22 @@ pub struct DiameterDimensionCommand {
|
|||
|
||||
impl DiameterDimensionCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { step: Step::CenterPoint }
|
||||
Self {
|
||||
step: Step::CenterPoint,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for DiameterDimensionCommand {
|
||||
fn name(&self) -> &'static str { "DIMDIAMETER" }
|
||||
fn name(&self) -> &'static str {
|
||||
"DIMDIAMETER"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match self.step {
|
||||
Step::CenterPoint => "DIMDIAMETER Specify center point:".into(),
|
||||
Step::ArcPoint(_) => "DIMDIAMETER Specify point on circle:".into(),
|
||||
Step::TextPoint {..} => "DIMDIAMETER Specify dimension line location:".into(),
|
||||
Step::CenterPoint => "DIMDIAMETER Specify center point:".into(),
|
||||
Step::ArcPoint(_) => "DIMDIAMETER Specify point on circle:".into(),
|
||||
Step::TextPoint { .. } => "DIMDIAMETER Specify dimension line location:".into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -59,9 +63,9 @@ impl CadCommand for DiameterDimensionCommand {
|
|||
}
|
||||
Step::TextPoint { center, arc_pt } => {
|
||||
let mut dim = DimensionDiameter::new(v3(center), v3(arc_pt));
|
||||
dim.base.definition_point = v3(arc_pt);
|
||||
dim.base.definition_point = v3(arc_pt);
|
||||
dim.base.text_middle_point = v3(pt);
|
||||
dim.base.insertion_point = v3(pt);
|
||||
dim.base.insertion_point = v3(pt);
|
||||
dim.leader_length = arc_pt.distance(pt) as f64;
|
||||
dim.base.actual_measurement = dim.measurement();
|
||||
CmdResult::CommitAndExit(EntityType::Dimension(Dimension::Diameter(dim)))
|
||||
|
|
@ -69,7 +73,9 @@ impl CadCommand for DiameterDimensionCommand {
|
|||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
|
||||
match self.step {
|
||||
|
|
@ -99,10 +105,10 @@ fn preview_line(a: Vec3, b: Vec3) -> WireModel {
|
|||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
aci: 0,
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,11 @@ impl DimBaselineCommand {
|
|||
/// `definition_point` — dim-line position of the base dim (defines perpendicular side).
|
||||
/// `rotation` — 0.0 = horizontal, PI/2 = vertical.
|
||||
pub fn from_base(p1: Vec3, _p2: Vec3, definition_point: Vec3, rotation: f64) -> Self {
|
||||
let axis = if rotation.abs() < 0.1 { Vec3::X } else { Vec3::Y };
|
||||
let axis = if rotation.abs() < 0.1 {
|
||||
Vec3::X
|
||||
} else {
|
||||
Vec3::Y
|
||||
};
|
||||
let perp = Vec3::new(-axis.y, axis.x, 0.0);
|
||||
let base_offset = (definition_point - p1).dot(perp);
|
||||
// Next baseline dim goes one DIMDLI further from the baseline.
|
||||
|
|
@ -77,7 +81,9 @@ impl DimBaselineCommand {
|
|||
}
|
||||
|
||||
impl CadCommand for DimBaselineCommand {
|
||||
fn name(&self) -> &'static str { "DIMBASELINE" }
|
||||
fn name(&self) -> &'static str {
|
||||
"DIMBASELINE"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if !self.ready {
|
||||
|
|
@ -126,9 +132,11 @@ impl CadCommand for DimBaselineCommand {
|
|||
Some(WireModel {
|
||||
name: "dimbase_preview".into(),
|
||||
points: vec![
|
||||
[p1.x, p1.y, p1.z], [dim_line_pt.x, dim_line_pt.y, dim_line_pt.z],
|
||||
[p1.x, p1.y, p1.z],
|
||||
[dim_line_pt.x, dim_line_pt.y, dim_line_pt.z],
|
||||
[f32::NAN, 0.0, 0.0],
|
||||
[pt.x, pt.y, pt.z], [dim_line_pt2.x, dim_line_pt2.y, dim_line_pt2.z],
|
||||
[pt.x, pt.y, pt.z],
|
||||
[dim_line_pt2.x, dim_line_pt2.y, dim_line_pt2.z],
|
||||
[f32::NAN, 0.0, 0.0],
|
||||
[dim_line_pt.x, dim_line_pt.y, dim_line_pt.z],
|
||||
[dim_line_pt2.x, dim_line_pt2.y, dim_line_pt2.z],
|
||||
|
|
|
|||
|
|
@ -59,7 +59,11 @@ impl DimContinueCommand {
|
|||
/// `rotation` — 0.0 = horizontal dim, PI/2 = vertical dim.
|
||||
pub fn from_base(p1: Vec3, p2: Vec3, definition_point: Vec3, rotation: f64) -> Self {
|
||||
// Axis unit vector along the measurement direction.
|
||||
let axis = if rotation.abs() < 0.1 { Vec3::X } else { Vec3::Y };
|
||||
let axis = if rotation.abs() < 0.1 {
|
||||
Vec3::X
|
||||
} else {
|
||||
Vec3::Y
|
||||
};
|
||||
// Perpendicular unit vector toward the dim line.
|
||||
let perp = Vec3::new(-axis.y, axis.x, 0.0);
|
||||
let dim_offset = (definition_point - p1).dot(perp);
|
||||
|
|
@ -74,7 +78,9 @@ impl DimContinueCommand {
|
|||
}
|
||||
|
||||
impl CadCommand for DimContinueCommand {
|
||||
fn name(&self) -> &'static str { "DIMCONTINUE" }
|
||||
fn name(&self) -> &'static str {
|
||||
"DIMCONTINUE"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if !self.ready {
|
||||
|
|
@ -123,9 +129,11 @@ impl CadCommand for DimContinueCommand {
|
|||
Some(WireModel {
|
||||
name: "dimcont_preview".into(),
|
||||
points: vec![
|
||||
[p1.x, p1.y, p1.z], [dim_line_pt.x, dim_line_pt.y, dim_line_pt.z],
|
||||
[p1.x, p1.y, p1.z],
|
||||
[dim_line_pt.x, dim_line_pt.y, dim_line_pt.z],
|
||||
[f32::NAN, 0.0, 0.0],
|
||||
[pt.x, pt.y, pt.z], [dim_line_pt2.x, dim_line_pt2.y, dim_line_pt2.z],
|
||||
[pt.x, pt.y, pt.z],
|
||||
[dim_line_pt2.x, dim_line_pt2.y, dim_line_pt2.z],
|
||||
[f32::NAN, 0.0, 0.0],
|
||||
[dim_line_pt.x, dim_line_pt.y, dim_line_pt.z],
|
||||
[dim_line_pt2.x, dim_line_pt2.y, dim_line_pt2.z],
|
||||
|
|
|
|||
|
|
@ -37,24 +37,34 @@ pub struct DimBreakCommand {
|
|||
|
||||
impl DimBreakCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { step: Step::PickDim }
|
||||
Self {
|
||||
step: Step::PickDim,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for DimBreakCommand {
|
||||
fn name(&self) -> &'static str { "DIMBREAK" }
|
||||
fn name(&self) -> &'static str {
|
||||
"DIMBREAK"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::PickDim => "DIMBREAK Select dimension to break:".into(),
|
||||
Step::PickCrossing { .. } => "DIMBREAK Select object to break at, or Enter for Auto:".into(),
|
||||
Step::PickCrossing { .. } => {
|
||||
"DIMBREAK Select object to break at, or Enter for Auto:".into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool { true }
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult {
|
||||
if handle.is_null() { return CmdResult::NeedPoint; }
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
match &self.step {
|
||||
Step::PickDim => {
|
||||
self.step = Step::PickCrossing { dim_handle: handle };
|
||||
|
|
@ -71,7 +81,9 @@ impl CadCommand for DimBreakCommand {
|
|||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult { CmdResult::NeedPoint }
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
// Auto mode: apply to all crossings
|
||||
|
|
|
|||
|
|
@ -32,17 +32,23 @@ pub struct DimEditCommand {
|
|||
|
||||
impl DimEditCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { step: Step::PickDim }
|
||||
Self {
|
||||
step: Step::PickDim,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for DimEditCommand {
|
||||
fn name(&self) -> &'static str { "DIMEDIT" }
|
||||
fn name(&self) -> &'static str {
|
||||
"DIMEDIT"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::PickDim => "DIMEDIT Select dimension:".into(),
|
||||
Step::EnterText { .. } => "DIMEDIT Enter text override (blank = reset to measured):".into(),
|
||||
Step::EnterText { .. } => {
|
||||
"DIMEDIT Enter text override (blank = reset to measured):".into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,7 +57,9 @@ impl CadCommand for DimEditCommand {
|
|||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult {
|
||||
if handle.is_null() { return CmdResult::NeedPoint; }
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.step = Step::EnterText { handle };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
|
@ -70,6 +78,10 @@ impl CadCommand for DimEditCommand {
|
|||
Some(CmdResult::DdeditEntity { handle, new_text })
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult { CmdResult::NeedPoint }
|
||||
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,12 +33,16 @@ pub struct DimJogLineCommand {
|
|||
|
||||
impl DimJogLineCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { step: Step::PickDim }
|
||||
Self {
|
||||
step: Step::PickDim,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for DimJogLineCommand {
|
||||
fn name(&self) -> &'static str { "DIMJOGLINE" }
|
||||
fn name(&self) -> &'static str {
|
||||
"DIMJOGLINE"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
|
|
@ -52,7 +56,9 @@ impl CadCommand for DimJogLineCommand {
|
|||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult {
|
||||
if handle.is_null() { return CmdResult::NeedPoint; }
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.step = Step::PickJogPos { handle };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
|
@ -69,7 +75,9 @@ impl CadCommand for DimJogLineCommand {
|
|||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
|
||||
if !matches!(self.step, Step::PickJogPos { .. }) {
|
||||
|
|
|
|||
|
|
@ -34,18 +34,24 @@ pub struct DimSpaceCommand {
|
|||
|
||||
impl DimSpaceCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { step: Step::PickBase }
|
||||
Self {
|
||||
step: Step::PickBase,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for DimSpaceCommand {
|
||||
fn name(&self) -> &'static str { "DIMSPACE" }
|
||||
fn name(&self) -> &'static str {
|
||||
"DIMSPACE"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::PickBase => "DIMSPACE Select base dimension:".into(),
|
||||
Step::PickOthers { others, .. } =>
|
||||
format!("DIMSPACE Select dimension to space ({} selected, Enter when done):", others.len()),
|
||||
Step::PickOthers { others, .. } => format!(
|
||||
"DIMSPACE Select dimension to space ({} selected, Enter when done):",
|
||||
others.len()
|
||||
),
|
||||
Step::EnterSpacing { .. } => "DIMSPACE Enter value (0 = auto):".into(),
|
||||
}
|
||||
}
|
||||
|
|
@ -55,10 +61,15 @@ impl CadCommand for DimSpaceCommand {
|
|||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult {
|
||||
if handle.is_null() { return CmdResult::NeedPoint; }
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
match &mut self.step {
|
||||
Step::PickBase => {
|
||||
self.step = Step::PickOthers { base: handle, others: vec![] };
|
||||
self.step = Step::PickOthers {
|
||||
base: handle,
|
||||
others: vec![],
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::PickOthers { others, .. } => {
|
||||
|
|
@ -84,13 +95,23 @@ impl CadCommand for DimSpaceCommand {
|
|||
use acadrust::entities::XLine;
|
||||
let mut xl = XLine::default();
|
||||
let handles_str: Vec<String> = o.iter().map(|h| h.value().to_string()).collect();
|
||||
xl.common.layer = format!("__DIMSPACE__{},{},{}", b.value(), handles_str.join(";"), spacing);
|
||||
return Some(CmdResult::ReplaceEntity(b, vec![acadrust::EntityType::XLine(xl)]));
|
||||
xl.common.layer = format!(
|
||||
"__DIMSPACE__{},{},{}",
|
||||
b.value(),
|
||||
handles_str.join(";"),
|
||||
spacing
|
||||
);
|
||||
return Some(CmdResult::ReplaceEntity(
|
||||
b,
|
||||
vec![acadrust::EntityType::XLine(xl)],
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult { CmdResult::NeedPoint }
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
match &self.step {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,10 @@ pub fn tool() -> ToolDef {
|
|||
|
||||
enum Step {
|
||||
PickDim,
|
||||
PickTextPos { handle: Handle, entity: Option<EntityType> },
|
||||
PickTextPos {
|
||||
handle: Handle,
|
||||
entity: Option<EntityType>,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct DimTeditCommand {
|
||||
|
|
@ -33,13 +36,16 @@ pub struct DimTeditCommand {
|
|||
|
||||
impl DimTeditCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { step: Step::PickDim }
|
||||
Self {
|
||||
step: Step::PickDim,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl CadCommand for DimTeditCommand {
|
||||
fn name(&self) -> &'static str { "DIMTEDIT" }
|
||||
fn name(&self) -> &'static str {
|
||||
"DIMTEDIT"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
|
|
@ -53,8 +59,13 @@ impl CadCommand for DimTeditCommand {
|
|||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult {
|
||||
if handle.is_null() { return CmdResult::NeedPoint; }
|
||||
self.step = Step::PickTextPos { handle, entity: None };
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.step = Step::PickTextPos {
|
||||
handle,
|
||||
entity: None,
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +74,8 @@ impl CadCommand for DimTeditCommand {
|
|||
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 as f64, pt.y as f64, pt.z as f64);
|
||||
let new_pt =
|
||||
acadrust::types::Vector3::new(pt.x as f64, pt.y as f64, pt.z as f64);
|
||||
d.base_mut().text_middle_point = new_pt;
|
||||
d.base_mut().insertion_point = new_pt;
|
||||
}
|
||||
|
|
@ -76,7 +88,9 @@ impl CadCommand for DimTeditCommand {
|
|||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
|
||||
if !matches!(self.step, Step::PickTextPos { .. }) {
|
||||
|
|
|
|||
|
|
@ -39,25 +39,36 @@ pub struct LeaderCommand {
|
|||
|
||||
impl LeaderCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { step: Step::CollectPoints { verts: Vec::new() } }
|
||||
Self {
|
||||
step: Step::CollectPoints { verts: Vec::new() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for LeaderCommand {
|
||||
fn name(&self) -> &'static str { "LEADER" }
|
||||
fn name(&self) -> &'static str {
|
||||
"LEADER"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::CollectPoints { verts } if verts.is_empty() =>
|
||||
"LEADER Specify arrowhead point:".into(),
|
||||
Step::CollectPoints { verts } =>
|
||||
format!("LEADER Specify next point [{} pts — Enter to finish]:", verts.len()),
|
||||
Step::AskCreationType { .. } =>
|
||||
"LEADER Annotation type [None/Text/Block/Tolerance] <Text>:".into(),
|
||||
Step::AskText { verts } =>
|
||||
format!("LEADER Annotation text [{} pts — blank = skip]:", verts.len()),
|
||||
Step::AskBlock { verts } =>
|
||||
format!("LEADER Block name [{} pts — blank = skip]:", verts.len()),
|
||||
Step::CollectPoints { verts } if verts.is_empty() => {
|
||||
"LEADER Specify arrowhead point:".into()
|
||||
}
|
||||
Step::CollectPoints { verts } => format!(
|
||||
"LEADER Specify next point [{} pts — Enter to finish]:",
|
||||
verts.len()
|
||||
),
|
||||
Step::AskCreationType { .. } => {
|
||||
"LEADER Annotation type [None/Text/Block/Tolerance] <Text>:".into()
|
||||
}
|
||||
Step::AskText { verts } => format!(
|
||||
"LEADER Annotation text [{} pts — blank = skip]:",
|
||||
verts.len()
|
||||
),
|
||||
Step::AskBlock { verts } => {
|
||||
format!("LEADER Block name [{} pts — blank = skip]:", verts.len())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +85,9 @@ impl CadCommand for LeaderCommand {
|
|||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
if let Step::CollectPoints { verts } = &self.step {
|
||||
if verts.len() < 2 { return CmdResult::Cancel; }
|
||||
if verts.len() < 2 {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
let verts = verts.clone();
|
||||
self.step = Step::AskCreationType { verts };
|
||||
CmdResult::NeedPoint
|
||||
|
|
@ -91,7 +104,9 @@ impl CadCommand for LeaderCommand {
|
|||
match parse_ct(text) {
|
||||
LeaderCreationType::NoAnnotation | LeaderCreationType::WithTolerance => {
|
||||
let ct = parse_ct(text);
|
||||
Some(CmdResult::CommitAndExit(EntityType::Leader(build_leader(&verts, ct))))
|
||||
Some(CmdResult::CommitAndExit(EntityType::Leader(build_leader(
|
||||
&verts, ct,
|
||||
))))
|
||||
}
|
||||
LeaderCreationType::WithBlock => {
|
||||
self.step = Step::AskBlock { verts };
|
||||
|
|
@ -109,7 +124,11 @@ impl CadCommand for LeaderCommand {
|
|||
if text.is_empty() {
|
||||
return Some(CmdResult::CommitAndExit(EntityType::Leader(leader)));
|
||||
}
|
||||
let mtext = build_mtext(text, landing_pt(&verts, leader.text_height), leader.text_height);
|
||||
let mtext = build_mtext(
|
||||
text,
|
||||
landing_pt(&verts, leader.text_height),
|
||||
leader.text_height,
|
||||
);
|
||||
Some(CmdResult::ReplaceMany(
|
||||
vec![],
|
||||
vec![EntityType::Leader(leader), EntityType::MText(mtext)],
|
||||
|
|
@ -131,11 +150,15 @@ impl CadCommand for LeaderCommand {
|
|||
}
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
|
||||
if let Step::CollectPoints { verts } = &self.step {
|
||||
if verts.is_empty() { return None; }
|
||||
if verts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut pts = verts.clone();
|
||||
pts.push(pt);
|
||||
Some(preview_wire(&pts))
|
||||
|
|
@ -149,14 +172,16 @@ impl CadCommand for LeaderCommand {
|
|||
|
||||
fn parse_ct(s: &str) -> LeaderCreationType {
|
||||
match s.to_ascii_uppercase().as_str() {
|
||||
"N" | "NONE" => LeaderCreationType::NoAnnotation,
|
||||
"B" | "BLOCK" => LeaderCreationType::WithBlock,
|
||||
"TL"| "TOLERANCE" => LeaderCreationType::WithTolerance,
|
||||
_ => LeaderCreationType::WithText,
|
||||
"N" | "NONE" => LeaderCreationType::NoAnnotation,
|
||||
"B" | "BLOCK" => LeaderCreationType::WithBlock,
|
||||
"TL" | "TOLERANCE" => LeaderCreationType::WithTolerance,
|
||||
_ => LeaderCreationType::WithText,
|
||||
}
|
||||
}
|
||||
|
||||
fn v3(p: Vec3) -> Vector3 { Vector3::new(p.x as f64, p.y as f64, p.z as f64) }
|
||||
fn v3(p: Vec3) -> Vector3 {
|
||||
Vector3::new(p.x as f64, p.y as f64, p.z as f64)
|
||||
}
|
||||
|
||||
fn build_leader(verts: &[Vec3], ct: LeaderCreationType) -> Leader {
|
||||
let mut l = Leader::from_vertices(verts.iter().map(|p| v3(*p)).collect());
|
||||
|
|
@ -200,11 +225,11 @@ fn preview_wire(pts: &[Vec3]) -> WireModel {
|
|||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
aci: 0,
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -215,7 +240,15 @@ pub fn arrowhead_wings(tip: Vec3, next: Vec3, size: f32) -> [Vec3; 2] {
|
|||
let angle = std::f32::consts::PI / 6.0;
|
||||
let (s, c) = angle.sin_cos();
|
||||
[
|
||||
Vec3::new(tip.x + (dx*c - dy*s)*size, tip.y + (dx*s + dy*c)*size, tip.z),
|
||||
Vec3::new(tip.x + (dx*c + dy*s)*size, tip.y + (-dx*s + dy*c)*size, tip.z),
|
||||
Vec3::new(
|
||||
tip.x + (dx * c - dy * s) * size,
|
||||
tip.y + (dx * s + dy * c) * size,
|
||||
tip.z,
|
||||
),
|
||||
Vec3::new(
|
||||
tip.x + (dx * c + dy * s) * size,
|
||||
tip.y + (-dx * s + dy * c) * size,
|
||||
tip.z,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,11 +111,11 @@ fn preview_wire(points: Vec<Vec3>) -> WireModel {
|
|||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
aci: 0,
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,21 +36,30 @@ pub struct MLeaderCommand {
|
|||
|
||||
impl MLeaderCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { step: Step::CollectPoints { verts: Vec::new() } }
|
||||
Self {
|
||||
step: Step::CollectPoints { verts: Vec::new() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for MLeaderCommand {
|
||||
fn name(&self) -> &'static str { "MLEADER" }
|
||||
fn name(&self) -> &'static str {
|
||||
"MLEADER"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::CollectPoints { verts } if verts.is_empty() =>
|
||||
"MLEADER Specify arrowhead point:".into(),
|
||||
Step::CollectPoints { verts } =>
|
||||
format!("MLEADER Specify next point [{} pts — Enter to finish]:", verts.len()),
|
||||
Step::AskText { verts } =>
|
||||
format!("MLEADER Enter annotation text [{} pts — blank = no text]:", verts.len()),
|
||||
Step::CollectPoints { verts } if verts.is_empty() => {
|
||||
"MLEADER Specify arrowhead point:".into()
|
||||
}
|
||||
Step::CollectPoints { verts } => format!(
|
||||
"MLEADER Specify next point [{} pts — Enter to finish]:",
|
||||
verts.len()
|
||||
),
|
||||
Step::AskText { verts } => format!(
|
||||
"MLEADER Enter annotation text [{} pts — blank = no text]:",
|
||||
verts.len()
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -67,7 +76,9 @@ impl CadCommand for MLeaderCommand {
|
|||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
if let Step::CollectPoints { verts } = &self.step {
|
||||
if verts.len() < 2 { return CmdResult::Cancel; }
|
||||
if verts.len() < 2 {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
let verts = verts.clone();
|
||||
self.step = Step::AskText { verts };
|
||||
CmdResult::NeedPoint
|
||||
|
|
@ -86,11 +97,15 @@ impl CadCommand for MLeaderCommand {
|
|||
}
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
|
||||
if let Step::CollectPoints { verts } = &self.step {
|
||||
if verts.is_empty() { return None; }
|
||||
if verts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut pts = verts.clone();
|
||||
pts.push(pt);
|
||||
Some(preview_wire(&pts))
|
||||
|
|
@ -102,7 +117,9 @@ impl CadCommand for MLeaderCommand {
|
|||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn v3(p: Vec3) -> Vector3 { Vector3::new(p.x as f64, p.y as f64, p.z as f64) }
|
||||
fn v3(p: Vec3) -> Vector3 {
|
||||
Vector3::new(p.x as f64, p.y as f64, p.z as f64)
|
||||
}
|
||||
|
||||
fn build_mleader(text: &str, verts: &[Vec3]) -> MultiLeader {
|
||||
// Last vertex = content/text location; remaining = leader line points
|
||||
|
|
@ -115,13 +132,15 @@ fn build_mleader(text: &str, verts: &[Vec3]) -> MultiLeader {
|
|||
let mut ml = MultiLeader::with_text(text, content_v3, leader_v3);
|
||||
|
||||
// Match Leader entity defaults
|
||||
ml.text_height = 2.5;
|
||||
ml.context.text_height = 2.5;
|
||||
ml.arrowhead_size = 2.5;
|
||||
ml.dogleg_length = 2.5;
|
||||
ml.text_height = 2.5;
|
||||
ml.context.text_height = 2.5;
|
||||
ml.arrowhead_size = 2.5;
|
||||
ml.dogleg_length = 2.5;
|
||||
|
||||
// Direction: from last leader pt toward content
|
||||
if let (Some(last_leader), Some(root)) = (leader_pts.last(), ml.context.leader_roots.first_mut()) {
|
||||
if let (Some(last_leader), Some(root)) =
|
||||
(leader_pts.last(), ml.context.leader_roots.first_mut())
|
||||
{
|
||||
let dx = (content_pt.x - last_leader.x) as f64;
|
||||
let dy = (content_pt.y - last_leader.y) as f64;
|
||||
let len = (dx * dx + dy * dy).sqrt().max(1e-9);
|
||||
|
|
@ -153,11 +172,11 @@ fn preview_wire(pts: &[Vec3]) -> WireModel {
|
|||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
aci: 0,
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -168,7 +187,15 @@ fn arrowhead_wings(tip: Vec3, next: Vec3, size: f32) -> [Vec3; 2] {
|
|||
let angle = std::f32::consts::PI / 6.0;
|
||||
let (s, c) = angle.sin_cos();
|
||||
[
|
||||
Vec3::new(tip.x + (dx*c - dy*s)*size, tip.y + (dx*s + dy*c)*size, tip.z),
|
||||
Vec3::new(tip.x + (dx*c + dy*s)*size, tip.y + (-dx*s + dy*c)*size, tip.z),
|
||||
Vec3::new(
|
||||
tip.x + (dx * c - dy * s) * size,
|
||||
tip.y + (dx * s + dy * c) * size,
|
||||
tip.z,
|
||||
),
|
||||
Vec3::new(
|
||||
tip.x + (dx * c + dy * s) * size,
|
||||
tip.y + (-dx * s + dy * c) * size,
|
||||
tip.z,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ use crate::scene::wire_model::WireModel;
|
|||
|
||||
// ── MLEADERADD ────────────────────────────────────────────────────────────────
|
||||
|
||||
pub const ICON_ADD: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/mleader_add.svg"));
|
||||
pub const ICON_ADD: IconKind =
|
||||
IconKind::Svg(include_bytes!("../../../assets/icons/mleader_add.svg"));
|
||||
|
||||
pub fn tool_add() -> ToolDef {
|
||||
ToolDef {
|
||||
|
|
@ -29,8 +30,15 @@ pub fn tool_add() -> ToolDef {
|
|||
|
||||
enum AddStep {
|
||||
PickMLeader,
|
||||
PickArrowhead { handle: Handle, entity: Option<EntityType> },
|
||||
CollectPoints { handle: Handle, entity: EntityType, pts: Vec<Vec3> },
|
||||
PickArrowhead {
|
||||
handle: Handle,
|
||||
entity: Option<EntityType>,
|
||||
},
|
||||
CollectPoints {
|
||||
handle: Handle,
|
||||
entity: EntityType,
|
||||
pts: Vec<Vec3>,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct MLeaderAddCommand {
|
||||
|
|
@ -38,20 +46,26 @@ pub struct MLeaderAddCommand {
|
|||
}
|
||||
|
||||
impl MLeaderAddCommand {
|
||||
pub fn new() -> Self { Self { step: AddStep::PickMLeader } }
|
||||
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: AddStep::PickMLeader,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for MLeaderAddCommand {
|
||||
fn name(&self) -> &'static str { "MLEADERADD" }
|
||||
fn name(&self) -> &'static str {
|
||||
"MLEADERADD"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
AddStep::PickMLeader => "MLEADERADD Select a multileader:".into(),
|
||||
AddStep::PickArrowhead { .. } => "MLEADERADD Specify arrowhead location:".into(),
|
||||
AddStep::CollectPoints { pts, .. } =>
|
||||
format!("MLEADERADD Specify next leader point ({} pts, Enter to finish):", pts.len()),
|
||||
AddStep::CollectPoints { pts, .. } => format!(
|
||||
"MLEADERADD Specify next leader point ({} pts, Enter to finish):",
|
||||
pts.len()
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -60,8 +74,13 @@ impl CadCommand for MLeaderAddCommand {
|
|||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult {
|
||||
if handle.is_null() { return CmdResult::NeedPoint; }
|
||||
self.step = AddStep::PickArrowhead { handle, entity: None };
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.step = AddStep::PickArrowhead {
|
||||
handle,
|
||||
entity: None,
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +89,11 @@ impl CadCommand for MLeaderAddCommand {
|
|||
AddStep::PickArrowhead { handle, entity } => {
|
||||
if let Some(ent) = entity.take() {
|
||||
let h = *handle;
|
||||
self.step = AddStep::CollectPoints { handle: h, entity: ent, pts: vec![pt] };
|
||||
self.step = AddStep::CollectPoints {
|
||||
handle: h,
|
||||
entity: ent,
|
||||
pts: vec![pt],
|
||||
};
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
|
|
@ -84,12 +107,20 @@ impl CadCommand for MLeaderAddCommand {
|
|||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
if let AddStep::CollectPoints { handle, entity, pts } = &mut self.step {
|
||||
if pts.len() < 1 { return CmdResult::Cancel; }
|
||||
if let AddStep::CollectPoints {
|
||||
handle,
|
||||
entity,
|
||||
pts,
|
||||
} = &mut self.step
|
||||
{
|
||||
if pts.len() < 1 {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
let h = *handle;
|
||||
if let EntityType::MultiLeader(ref mut ml) = entity {
|
||||
// Add a new leader root with the collected points
|
||||
let points: Vec<Vector3> = pts.iter()
|
||||
let points: Vec<Vector3> = pts
|
||||
.iter()
|
||||
.map(|p| Vector3::new(p.x as f64, p.y as f64, p.z as f64))
|
||||
.collect();
|
||||
let root = ml.context.add_leader_root();
|
||||
|
|
@ -120,7 +151,8 @@ impl CadCommand for MLeaderAddCommand {
|
|||
|
||||
// ── MLEADERREMOVE ─────────────────────────────────────────────────────────────
|
||||
|
||||
pub const ICON_REMOVE: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/mleader_remove.svg"));
|
||||
pub const ICON_REMOVE: IconKind =
|
||||
IconKind::Svg(include_bytes!("../../../assets/icons/mleader_remove.svg"));
|
||||
|
||||
pub fn tool_remove() -> ToolDef {
|
||||
ToolDef {
|
||||
|
|
@ -133,7 +165,10 @@ pub fn tool_remove() -> ToolDef {
|
|||
|
||||
enum RemoveStep {
|
||||
PickMLeader,
|
||||
PickLeaderToRemove { handle: Handle, entity: Option<EntityType> },
|
||||
PickLeaderToRemove {
|
||||
handle: Handle,
|
||||
entity: Option<EntityType>,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct MLeaderRemoveCommand {
|
||||
|
|
@ -141,18 +176,24 @@ pub struct MLeaderRemoveCommand {
|
|||
}
|
||||
|
||||
impl MLeaderRemoveCommand {
|
||||
pub fn new() -> Self { Self { step: RemoveStep::PickMLeader } }
|
||||
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: RemoveStep::PickMLeader,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for MLeaderRemoveCommand {
|
||||
fn name(&self) -> &'static str { "MLEADERREMOVE" }
|
||||
fn name(&self) -> &'static str {
|
||||
"MLEADERREMOVE"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
RemoveStep::PickMLeader => "MLEADERREMOVE Select a multileader:".into(),
|
||||
RemoveStep::PickLeaderToRemove { .. } => "MLEADERREMOVE Click near the leader line to remove:".into(),
|
||||
RemoveStep::PickLeaderToRemove { .. } => {
|
||||
"MLEADERREMOVE Click near the leader line to remove:".into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -161,8 +202,13 @@ impl CadCommand for MLeaderRemoveCommand {
|
|||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult {
|
||||
if handle.is_null() { return CmdResult::NeedPoint; }
|
||||
self.step = RemoveStep::PickLeaderToRemove { handle, entity: None };
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.step = RemoveStep::PickLeaderToRemove {
|
||||
handle,
|
||||
entity: None,
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
|
|
@ -173,15 +219,17 @@ impl CadCommand for MLeaderRemoveCommand {
|
|||
if let EntityType::MultiLeader(ref mut ml) = ent {
|
||||
// Remove the leader root whose first point is closest to `pt`
|
||||
let pick = Vector3::new(pt.x as f64, pt.y as f64, pt.z as f64);
|
||||
let best = ml.context.leader_roots.iter().enumerate()
|
||||
let best = ml
|
||||
.context
|
||||
.leader_roots
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, root)| {
|
||||
root.lines.first()
|
||||
.and_then(|l| l.points.first())
|
||||
.map(|p| {
|
||||
let dx = p.x - pick.x;
|
||||
let dz = p.z - pick.z;
|
||||
(i, dx * dx + dz * dz)
|
||||
})
|
||||
root.lines.first().and_then(|l| l.points.first()).map(|p| {
|
||||
let dx = p.x - pick.x;
|
||||
let dz = p.z - pick.z;
|
||||
(i, dx * dx + dz * dz)
|
||||
})
|
||||
})
|
||||
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
if let Some((idx, _)) = best {
|
||||
|
|
@ -196,8 +244,12 @@ impl CadCommand for MLeaderRemoveCommand {
|
|||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
fn on_mouse_move(&mut self, _pt: Vec3) -> Option<WireModel> { None }
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
fn on_mouse_move(&mut self, _pt: Vec3) -> Option<WireModel> {
|
||||
None
|
||||
}
|
||||
|
||||
fn inject_picked_entity(&mut self, entity: EntityType) {
|
||||
if let RemoveStep::PickLeaderToRemove { entity: slot, .. } = &mut self.step {
|
||||
|
|
@ -208,7 +260,8 @@ impl CadCommand for MLeaderRemoveCommand {
|
|||
|
||||
// ── MLEADERALIGN ─────────────────────────────────────────────────────────────
|
||||
|
||||
pub const ICON_ALIGN: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/mleader_align.svg"));
|
||||
pub const ICON_ALIGN: IconKind =
|
||||
IconKind::Svg(include_bytes!("../../../assets/icons/mleader_align.svg"));
|
||||
|
||||
pub fn tool_align() -> ToolDef {
|
||||
ToolDef {
|
||||
|
|
@ -230,17 +283,29 @@ pub struct MLeaderAlignCommand {
|
|||
}
|
||||
|
||||
impl MLeaderAlignCommand {
|
||||
pub fn new() -> Self { Self { step: AlignStep::Gathering } }
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: AlignStep::Gathering,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for MLeaderAlignCommand {
|
||||
fn name(&self) -> &'static str { "MLEADERALIGN" }
|
||||
fn name(&self) -> &'static str {
|
||||
"MLEADERALIGN"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
AlignStep::Gathering => "MLEADERALIGN Select multileaders to align (Enter when done):".into(),
|
||||
AlignStep::PickDirection { .. } => "MLEADERALIGN Specify direction — pick start point:".into(),
|
||||
AlignStep::PickEndDir { .. } => "MLEADERALIGN Specify end point of alignment direction:".into(),
|
||||
AlignStep::Gathering => {
|
||||
"MLEADERALIGN Select multileaders to align (Enter when done):".into()
|
||||
}
|
||||
AlignStep::PickDirection { .. } => {
|
||||
"MLEADERALIGN Specify direction — pick start point:".into()
|
||||
}
|
||||
AlignStep::PickEndDir { .. } => {
|
||||
"MLEADERALIGN Specify end point of alignment direction:".into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -249,7 +314,9 @@ impl CadCommand for MLeaderAlignCommand {
|
|||
}
|
||||
|
||||
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
|
||||
if handles.is_empty() { return CmdResult::Cancel; }
|
||||
if handles.is_empty() {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
self.step = AlignStep::PickDirection { handles };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
|
@ -258,7 +325,10 @@ impl CadCommand for MLeaderAlignCommand {
|
|||
match &mut self.step {
|
||||
AlignStep::PickDirection { handles } => {
|
||||
let h = handles.clone();
|
||||
self.step = AlignStep::PickEndDir { handles: h, from: pt };
|
||||
self.step = AlignStep::PickEndDir {
|
||||
handles: h,
|
||||
from: pt,
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
AlignStep::PickEndDir { handles, from } => {
|
||||
|
|
@ -268,8 +338,14 @@ impl CadCommand for MLeaderAlignCommand {
|
|||
use acadrust::entities::XLine;
|
||||
let mut xl = XLine::default();
|
||||
let hstr: Vec<String> = h.iter().map(|hh| hh.value().to_string()).collect();
|
||||
xl.common.layer = format!("__MLEADERALIGN__{};{:.4},{:.4};{:.4},{:.4}",
|
||||
hstr.join(","), f.x, f.z, pt.x, pt.z);
|
||||
xl.common.layer = format!(
|
||||
"__MLEADERALIGN__{};{:.4},{:.4};{:.4},{:.4}",
|
||||
hstr.join(","),
|
||||
f.x,
|
||||
f.z,
|
||||
pt.x,
|
||||
pt.z
|
||||
);
|
||||
// Use first handle as the "replaced" entity (sentinel)
|
||||
if let Some(&first) = h.first() {
|
||||
return CmdResult::ReplaceEntity(first, vec![EntityType::XLine(xl)]);
|
||||
|
|
@ -280,13 +356,18 @@ impl CadCommand for MLeaderAlignCommand {
|
|||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
fn on_mouse_move(&mut self, _pt: Vec3) -> Option<WireModel> { None }
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
fn on_mouse_move(&mut self, _pt: Vec3) -> Option<WireModel> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// ── MLEADERCOLLECT ────────────────────────────────────────────────────────────
|
||||
|
||||
pub const ICON_COLLECT: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/mleader_collect.svg"));
|
||||
pub const ICON_COLLECT: IconKind =
|
||||
IconKind::Svg(include_bytes!("../../../assets/icons/mleader_collect.svg"));
|
||||
|
||||
pub fn tool_collect() -> ToolDef {
|
||||
ToolDef {
|
||||
|
|
@ -307,16 +388,26 @@ pub struct MLeaderCollectCommand {
|
|||
}
|
||||
|
||||
impl MLeaderCollectCommand {
|
||||
pub fn new() -> Self { Self { step: CollectStep::Gathering } }
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: CollectStep::Gathering,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for MLeaderCollectCommand {
|
||||
fn name(&self) -> &'static str { "MLEADERCOLLECT" }
|
||||
fn name(&self) -> &'static str {
|
||||
"MLEADERCOLLECT"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
CollectStep::Gathering => "MLEADERCOLLECT Select multileaders to collect (Enter when done):".into(),
|
||||
CollectStep::PickLocation { .. } => "MLEADERCOLLECT Specify collected multileader location:".into(),
|
||||
CollectStep::Gathering => {
|
||||
"MLEADERCOLLECT Select multileaders to collect (Enter when done):".into()
|
||||
}
|
||||
CollectStep::PickLocation { .. } => {
|
||||
"MLEADERCOLLECT Specify collected multileader location:".into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -325,7 +416,9 @@ impl CadCommand for MLeaderCollectCommand {
|
|||
}
|
||||
|
||||
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
|
||||
if handles.is_empty() { return CmdResult::Cancel; }
|
||||
if handles.is_empty() {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
self.step = CollectStep::PickLocation { handles };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
|
@ -338,7 +431,12 @@ impl CadCommand for MLeaderCollectCommand {
|
|||
use acadrust::entities::XLine;
|
||||
let mut xl = XLine::default();
|
||||
let hstr: Vec<String> = h.iter().map(|hh| hh.value().to_string()).collect();
|
||||
xl.common.layer = format!("__MLEADERCOLLECT__{};{:.4},{:.4}", hstr.join(","), pt.x, pt.z);
|
||||
xl.common.layer = format!(
|
||||
"__MLEADERCOLLECT__{};{:.4},{:.4}",
|
||||
hstr.join(","),
|
||||
pt.x,
|
||||
pt.z
|
||||
);
|
||||
if let Some(&first) = h.first() {
|
||||
return CmdResult::ReplaceEntity(first, vec![EntityType::XLine(xl)]);
|
||||
}
|
||||
|
|
@ -346,8 +444,12 @@ impl CadCommand for MLeaderCollectCommand {
|
|||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
fn on_mouse_move(&mut self, _pt: Vec3) -> Option<WireModel> { None }
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
fn on_mouse_move(&mut self, _pt: Vec3) -> Option<WireModel> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// ── Shared helpers ────────────────────────────────────────────────────────────
|
||||
|
|
@ -366,9 +468,9 @@ fn preview_wire(pts: &[Vec3]) -> WireModel {
|
|||
aci: 0,
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// Annotate module — dimension, text, leader, table, and markup tools.
|
||||
|
||||
pub mod aligned_dim;
|
||||
pub mod angular_dim;
|
||||
pub mod data_extract;
|
||||
pub mod data_link;
|
||||
pub mod angular_dim;
|
||||
pub mod ddedit;
|
||||
pub mod diameter_dim;
|
||||
pub mod dim_baseline;
|
||||
|
|
@ -38,7 +38,7 @@ impl CadModule for AnnotateModule {
|
|||
}
|
||||
|
||||
fn ribbon_groups(&self) -> Vec<RibbonGroup> {
|
||||
use crate::modules::home::draw::{wipeout, revcloud};
|
||||
use crate::modules::home::draw::{revcloud, wipeout};
|
||||
|
||||
vec![
|
||||
// ── Text ─────────────────────────────────────────────────────
|
||||
|
|
@ -51,7 +51,7 @@ impl CadModule for AnnotateModule {
|
|||
icon: mtext::ICON,
|
||||
items: vec![
|
||||
(mtext::tool().id, mtext::tool().label, mtext::tool().icon),
|
||||
(text::tool().id, text::tool().label, text::tool().icon),
|
||||
(text::tool().id, text::tool().label, text::tool().icon),
|
||||
(ddedit::tool().id, ddedit::tool().label, ddedit::tool().icon),
|
||||
],
|
||||
default: "MTEXT",
|
||||
|
|
@ -60,16 +60,14 @@ impl CadModule for AnnotateModule {
|
|||
style_key: StyleKey::TextStyle,
|
||||
combo_id: "TEXT_STYLE_COMBO",
|
||||
manager_cmd: Some("STYLE"),
|
||||
rows: vec![
|
||||
vec![
|
||||
crate::modules::ToolDef {
|
||||
id: "FIND",
|
||||
label: "Find",
|
||||
icon: crate::modules::IconKind::Svg(include_bytes!("../../../assets/icons/find.svg")),
|
||||
event: crate::modules::ModuleEvent::Command("FIND".to_string()),
|
||||
},
|
||||
],
|
||||
],
|
||||
rows: vec![vec![crate::modules::ToolDef {
|
||||
id: "FIND",
|
||||
label: "Find",
|
||||
icon: crate::modules::IconKind::Svg(include_bytes!(
|
||||
"../../../assets/icons/find.svg"
|
||||
)),
|
||||
event: crate::modules::ModuleEvent::Command("FIND".to_string()),
|
||||
}]],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -82,12 +80,36 @@ impl CadModule for AnnotateModule {
|
|||
label: "Dimension",
|
||||
icon: linear_dim::ICON,
|
||||
items: vec![
|
||||
(linear_dim::tool().id, linear_dim::tool().label, linear_dim::tool().icon),
|
||||
(aligned_dim::tool().id, aligned_dim::tool().label, aligned_dim::tool().icon),
|
||||
(angular_dim::tool().id, angular_dim::tool().label, angular_dim::tool().icon),
|
||||
(radius_dim::tool().id, radius_dim::tool().label, radius_dim::tool().icon),
|
||||
(diameter_dim::tool().id, diameter_dim::tool().label, diameter_dim::tool().icon),
|
||||
(ordinate_dim::tool().id, ordinate_dim::tool().label, ordinate_dim::tool().icon),
|
||||
(
|
||||
linear_dim::tool().id,
|
||||
linear_dim::tool().label,
|
||||
linear_dim::tool().icon,
|
||||
),
|
||||
(
|
||||
aligned_dim::tool().id,
|
||||
aligned_dim::tool().label,
|
||||
aligned_dim::tool().icon,
|
||||
),
|
||||
(
|
||||
angular_dim::tool().id,
|
||||
angular_dim::tool().label,
|
||||
angular_dim::tool().icon,
|
||||
),
|
||||
(
|
||||
radius_dim::tool().id,
|
||||
radius_dim::tool().label,
|
||||
radius_dim::tool().icon,
|
||||
),
|
||||
(
|
||||
diameter_dim::tool().id,
|
||||
diameter_dim::tool().label,
|
||||
diameter_dim::tool().icon,
|
||||
),
|
||||
(
|
||||
ordinate_dim::tool().id,
|
||||
ordinate_dim::tool().label,
|
||||
ordinate_dim::tool().icon,
|
||||
),
|
||||
(qdim::tool().id, qdim::tool().label, qdim::tool().icon),
|
||||
],
|
||||
default: "DIMLINEAR",
|
||||
|
|
@ -97,11 +119,7 @@ impl CadModule for AnnotateModule {
|
|||
combo_id: "DIM_STYLE_COMBO",
|
||||
manager_cmd: Some("DIMSTYLE"),
|
||||
rows: vec![
|
||||
vec![
|
||||
qdim::tool(),
|
||||
dim_continue::tool(),
|
||||
dim_baseline::tool(),
|
||||
],
|
||||
vec![qdim::tool(), dim_continue::tool(), dim_baseline::tool()],
|
||||
vec![
|
||||
tolerance_cmd::tool(),
|
||||
dimedit::tool(),
|
||||
|
|
@ -123,8 +141,16 @@ impl CadModule for AnnotateModule {
|
|||
label: "Multileader",
|
||||
icon: mleader_cmd::ICON,
|
||||
items: vec![
|
||||
(mleader_cmd::tool().id, mleader_cmd::tool().label, mleader_cmd::tool().icon),
|
||||
(leader_cmd::tool().id, leader_cmd::tool().label, leader_cmd::tool().icon),
|
||||
(
|
||||
mleader_cmd::tool().id,
|
||||
mleader_cmd::tool().label,
|
||||
mleader_cmd::tool().icon,
|
||||
),
|
||||
(
|
||||
leader_cmd::tool().id,
|
||||
leader_cmd::tool().label,
|
||||
leader_cmd::tool().icon,
|
||||
),
|
||||
],
|
||||
default: "MLEADER",
|
||||
},
|
||||
|
|
@ -133,14 +159,8 @@ impl CadModule for AnnotateModule {
|
|||
combo_id: "MLEADER_STYLE_COMBO",
|
||||
manager_cmd: Some("MLEADERSTYLE"),
|
||||
rows: vec![
|
||||
vec![
|
||||
mleader_edit::tool_add(),
|
||||
mleader_edit::tool_remove(),
|
||||
],
|
||||
vec![
|
||||
mleader_edit::tool_align(),
|
||||
mleader_edit::tool_collect(),
|
||||
],
|
||||
vec![mleader_edit::tool_add(), mleader_edit::tool_remove()],
|
||||
vec![mleader_edit::tool_align(), mleader_edit::tool_collect()],
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
@ -154,12 +174,7 @@ impl CadModule for AnnotateModule {
|
|||
style_key: StyleKey::TableStyle,
|
||||
combo_id: "TABLE_STYLE_COMBO",
|
||||
manager_cmd: Some("TABLESTYLE"),
|
||||
rows: vec![
|
||||
vec![
|
||||
data_extract::tool(),
|
||||
data_link::tool(),
|
||||
],
|
||||
],
|
||||
rows: vec![vec![data_extract::tool(), data_link::tool()]],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -37,12 +37,16 @@ pub struct OrdinateDimCommand {
|
|||
|
||||
impl OrdinateDimCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { step: Step::FeaturePoint }
|
||||
Self {
|
||||
step: Step::FeaturePoint,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for OrdinateDimCommand {
|
||||
fn name(&self) -> &'static str { "DIMORDINATE" }
|
||||
fn name(&self) -> &'static str {
|
||||
"DIMORDINATE"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match self.step {
|
||||
|
|
@ -71,8 +75,12 @@ impl CadCommand for OrdinateDimCommand {
|
|||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
fn on_preview_wires(&mut self, _pt: Vec3) -> Vec<WireModel> { vec![] }
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
fn on_preview_wires(&mut self, _pt: Vec3) -> Vec<WireModel> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
fn v3(p: Vec3) -> Vector3 {
|
||||
|
|
|
|||
|
|
@ -37,14 +37,16 @@ pub struct QdimCommand {
|
|||
|
||||
impl QdimCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { step: Step::Gathering }
|
||||
Self {
|
||||
step: Step::Gathering,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
impl CadCommand for QdimCommand {
|
||||
fn name(&self) -> &'static str { "QDIM" }
|
||||
fn name(&self) -> &'static str {
|
||||
"QDIM"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
|
|
@ -76,7 +78,9 @@ impl CadCommand for QdimCommand {
|
|||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
|
||||
if !matches!(self.step, Step::PlaceLine { .. }) {
|
||||
|
|
@ -85,10 +89,7 @@ impl CadCommand for QdimCommand {
|
|||
let d = 0.5_f32;
|
||||
Some(WireModel {
|
||||
name: "qdim_preview".into(),
|
||||
points: vec![
|
||||
[pt.x - d * 3.0, pt.y, pt.z],
|
||||
[pt.x + d * 3.0, pt.y, pt.z],
|
||||
],
|
||||
points: vec![[pt.x - d * 3.0, pt.y, pt.z], [pt.x + d * 3.0, pt.y, pt.z]],
|
||||
color: WireModel::CYAN,
|
||||
selected: false,
|
||||
pattern_length: 0.0,
|
||||
|
|
@ -114,7 +115,11 @@ pub fn make_linear_dim(p1: Vec3, p2: Vec3, dim_pt: Vec3) -> EntityType {
|
|||
// Determine axis: horizontal if Δx > Δz, else vertical
|
||||
let dx = (p2.x - p1.x).abs();
|
||||
let dz = (p2.z - p1.z).abs();
|
||||
dim.rotation = if dz > dx { std::f64::consts::FRAC_PI_2 } else { 0.0 };
|
||||
dim.rotation = if dz > dx {
|
||||
std::f64::consts::FRAC_PI_2
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
dim.definition_point = v(dim_pt);
|
||||
dim.base.definition_point = v(dim_pt);
|
||||
let text_pos = (p1 + p2) * 0.5;
|
||||
|
|
|
|||
|
|
@ -110,10 +110,10 @@ fn preview_wire(points: Vec<Vec3>) -> WireModel {
|
|||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
aci: 0,
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,18 +44,26 @@ pub struct TableCommand {
|
|||
|
||||
impl TableCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { step: Step::Columns }
|
||||
Self {
|
||||
step: Step::Columns,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for TableCommand {
|
||||
fn name(&self) -> &'static str { "TABLE" }
|
||||
fn name(&self) -> &'static str {
|
||||
"TABLE"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::Columns => format!("TABLE Enter number of columns [{DEFAULT_COLS}]:"),
|
||||
Step::Rows { cols } => format!("TABLE Enter number of rows (incl. header) [{DEFAULT_ROWS}] ({cols} cols):"),
|
||||
Step::Insertion { cols, rows } => format!("TABLE Specify insertion point [{cols}×{rows}]:"),
|
||||
Step::Rows { cols } => format!(
|
||||
"TABLE Enter number of rows (incl. header) [{DEFAULT_ROWS}] ({cols} cols):"
|
||||
),
|
||||
Step::Insertion { cols, rows } => {
|
||||
format!("TABLE Specify insertion point [{cols}×{rows}]:")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -98,7 +106,10 @@ impl CadCommand for TableCommand {
|
|||
}
|
||||
Step::Rows { cols } => {
|
||||
let cols = *cols;
|
||||
self.step = Step::Insertion { cols, rows: DEFAULT_ROWS };
|
||||
self.step = Step::Insertion {
|
||||
cols,
|
||||
rows: DEFAULT_ROWS,
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Insertion { .. } => CmdResult::Cancel,
|
||||
|
|
@ -130,10 +141,14 @@ impl CadCommand for TableCommand {
|
|||
Some(WireModel {
|
||||
name: "table_preview".into(),
|
||||
points: vec![
|
||||
[x, y, z], [x + w, y, z],
|
||||
[x + w, y, z], [x + w, y, z - h],
|
||||
[x + w, y, z - h], [x, y, z - h],
|
||||
[x, y, z - h], [x, y, z],
|
||||
[x, y, z],
|
||||
[x + w, y, z],
|
||||
[x + w, y, z],
|
||||
[x + w, y, z - h],
|
||||
[x + w, y, z - h],
|
||||
[x, y, z - h],
|
||||
[x, y, z - h],
|
||||
[x, y, z],
|
||||
],
|
||||
color: WireModel::CYAN,
|
||||
selected: false,
|
||||
|
|
@ -143,11 +158,11 @@ impl CadCommand for TableCommand {
|
|||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
aci: 0,
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
})
|
||||
} else {
|
||||
None
|
||||
|
|
|
|||
|
|
@ -40,7 +40,9 @@ impl ToleranceCommand {
|
|||
}
|
||||
|
||||
impl CadCommand for ToleranceCommand {
|
||||
fn name(&self) -> &'static str { "TOLERANCE" }
|
||||
fn name(&self) -> &'static str {
|
||||
"TOLERANCE"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
|
|
@ -84,9 +86,11 @@ impl CadCommand for ToleranceCommand {
|
|||
Some(WireModel {
|
||||
name: "tolerance_preview".into(),
|
||||
points: vec![
|
||||
[pt.x - d, pt.y, pt.z], [pt.x + d, pt.y, pt.z],
|
||||
[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],
|
||||
[pt.x, pt.y, pt.z - d],
|
||||
[pt.x, pt.y, pt.z + d],
|
||||
],
|
||||
color: WireModel::CYAN,
|
||||
selected: false,
|
||||
|
|
|
|||
|
|
@ -16,9 +16,18 @@ use crate::scene::wire_model::WireModel;
|
|||
|
||||
enum Step {
|
||||
Tag,
|
||||
Prompt { tag: String },
|
||||
Default { tag: String, prompt: String },
|
||||
Insertion { tag: String, prompt: String, default: String },
|
||||
Prompt {
|
||||
tag: String,
|
||||
},
|
||||
Default {
|
||||
tag: String,
|
||||
prompt: String,
|
||||
},
|
||||
Insertion {
|
||||
tag: String,
|
||||
prompt: String,
|
||||
default: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct AttdefCommand {
|
||||
|
|
@ -29,18 +38,25 @@ pub struct AttdefCommand {
|
|||
|
||||
impl AttdefCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { step: Step::Tag, height: 0.2 }
|
||||
Self {
|
||||
step: Step::Tag,
|
||||
height: 0.2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for AttdefCommand {
|
||||
fn name(&self) -> &'static str { "ATTDEF" }
|
||||
fn name(&self) -> &'static str {
|
||||
"ATTDEF"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::Tag => "ATTDEF Enter attribute tag (no spaces):".into(),
|
||||
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::Default { tag, .. } => {
|
||||
format!("ATTDEF Enter default value for '{tag}' (Enter=blank):")
|
||||
}
|
||||
Step::Insertion { tag, .. } => format!("ATTDEF Specify insertion point for '{tag}':"),
|
||||
}
|
||||
}
|
||||
|
|
@ -61,7 +77,11 @@ impl CadCommand for AttdefCommand {
|
|||
}
|
||||
Step::Prompt { tag } => {
|
||||
let tag = tag.clone();
|
||||
let prompt = if text.trim().is_empty() { tag.clone() } else { text.trim().to_string() };
|
||||
let prompt = if text.trim().is_empty() {
|
||||
tag.clone()
|
||||
} else {
|
||||
text.trim().to_string()
|
||||
};
|
||||
self.step = Step::Default { tag, prompt };
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
|
|
@ -69,7 +89,11 @@ impl CadCommand for AttdefCommand {
|
|||
let tag = tag.clone();
|
||||
let prompt = prompt.clone();
|
||||
let default = text.trim().to_string();
|
||||
self.step = Step::Insertion { tag, prompt, default };
|
||||
self.step = Step::Insertion {
|
||||
tag,
|
||||
prompt,
|
||||
default,
|
||||
};
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::Insertion { .. } => None,
|
||||
|
|
@ -77,7 +101,12 @@ impl CadCommand for AttdefCommand {
|
|||
}
|
||||
|
||||
fn on_point(&mut self, pt: Vec3) -> CmdResult {
|
||||
if let Step::Insertion { tag, prompt, default } = &self.step {
|
||||
if let Step::Insertion {
|
||||
tag,
|
||||
prompt,
|
||||
default,
|
||||
} = &self.step
|
||||
{
|
||||
let mut attdef = AttributeDefinition {
|
||||
tag: tag.clone(),
|
||||
prompt: prompt.clone(),
|
||||
|
|
@ -99,12 +128,19 @@ impl CadCommand for AttdefCommand {
|
|||
// 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 };
|
||||
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() };
|
||||
self.step = Step::Insertion {
|
||||
tag,
|
||||
prompt,
|
||||
default: String::new(),
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Insertion { .. } => CmdResult::Cancel,
|
||||
|
|
@ -120,9 +156,11 @@ impl CadCommand for AttdefCommand {
|
|||
Some(WireModel {
|
||||
name: "attdef_preview".into(),
|
||||
points: vec![
|
||||
[pt.x - d, pt.y, pt.z], [pt.x + d, pt.y, pt.z],
|
||||
[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],
|
||||
[pt.x, pt.y, pt.z - d],
|
||||
[pt.x, pt.y, pt.z + d],
|
||||
],
|
||||
color: WireModel::CYAN,
|
||||
selected: false,
|
||||
|
|
|
|||
|
|
@ -39,12 +39,14 @@ impl DonutCommand {
|
|||
}
|
||||
|
||||
impl CadCommand for DonutCommand {
|
||||
fn name(&self) -> &'static str { "DONUT" }
|
||||
fn name(&self) -> &'static str {
|
||||
"DONUT"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.state {
|
||||
DonutState::AskInner => "DONUT Specify inside diameter <0>:".into(),
|
||||
DonutState::AskOuter => "DONUT Specify outside diameter:".into(),
|
||||
DonutState::AskInner => "DONUT Specify inside diameter <0>:".into(),
|
||||
DonutState::AskOuter => "DONUT Specify outside diameter:".into(),
|
||||
DonutState::PlaceCenter => "DONUT Specify center of donut (Enter to exit):".into(),
|
||||
}
|
||||
}
|
||||
|
|
@ -54,7 +56,12 @@ impl CadCommand for DonutCommand {
|
|||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let val: f64 = text.trim().replace(',', ".").parse().ok().filter(|&v: &f64| v >= 0.0)?;
|
||||
let val: f64 = text
|
||||
.trim()
|
||||
.replace(',', ".")
|
||||
.parse()
|
||||
.ok()
|
||||
.filter(|&v: &f64| v >= 0.0)?;
|
||||
match &self.state {
|
||||
DonutState::AskInner => {
|
||||
self.inner_r = val / 2.0;
|
||||
|
|
@ -62,7 +69,9 @@ impl CadCommand for DonutCommand {
|
|||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
DonutState::AskOuter => {
|
||||
if val <= 0.0 { return None; }
|
||||
if val <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
self.outer_r = val / 2.0;
|
||||
if self.inner_r > self.outer_r {
|
||||
std::mem::swap(&mut self.inner_r, &mut self.outer_r);
|
||||
|
|
|
|||
|
|
@ -525,10 +525,10 @@ fn line_wire(from: Vec3, to: Vec3) -> WireModel {
|
|||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
aci: 0,
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,23 +29,34 @@ pub struct HatcheditCommand {
|
|||
|
||||
impl HatcheditCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { step: HatcheditStep::PickHatch }
|
||||
Self {
|
||||
step: HatcheditStep::PickHatch,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_handle(handle: Handle, name: String, scale: f32, angle: f32) -> Self {
|
||||
Self {
|
||||
step: HatcheditStep::EditOptions { handle, name, scale, angle },
|
||||
step: HatcheditStep::EditOptions {
|
||||
handle,
|
||||
name,
|
||||
scale,
|
||||
angle,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for HatcheditCommand {
|
||||
fn name(&self) -> &'static str { "HATCHEDIT" }
|
||||
fn name(&self) -> &'static str {
|
||||
"HATCHEDIT"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
HatcheditStep::PickHatch => "HATCHEDIT Select hatch:".into(),
|
||||
HatcheditStep::EditOptions { name, scale, angle, .. } => format!(
|
||||
HatcheditStep::EditOptions {
|
||||
name, scale, angle, ..
|
||||
} => format!(
|
||||
"HATCHEDIT Pattern:{name} Scale:{scale:.4} Angle:{angle:.1} \
|
||||
[P <pat> / S <scale> / A <angle> | Enter to apply]:"
|
||||
),
|
||||
|
|
@ -57,7 +68,9 @@ impl CadCommand for HatcheditCommand {
|
|||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult {
|
||||
if handle.is_null() { return CmdResult::NeedPoint; }
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
// Actual hatch model retrieval happens in commands.rs dispatch.
|
||||
// Store handle; name/scale/angle filled in by dispatch.
|
||||
self.step = HatcheditStep::EditOptions {
|
||||
|
|
@ -75,9 +88,12 @@ impl CadCommand for HatcheditCommand {
|
|||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let (handle, name, scale, angle) = match &mut self.step {
|
||||
HatcheditStep::EditOptions { handle, name, scale, angle } => {
|
||||
(*handle, name, scale, angle)
|
||||
}
|
||||
HatcheditStep::EditOptions {
|
||||
handle,
|
||||
name,
|
||||
scale,
|
||||
angle,
|
||||
} => (*handle, name, scale, angle),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
|
|
@ -96,12 +112,16 @@ impl CadCommand for HatcheditCommand {
|
|||
// Parse option: P/S/A followed by value
|
||||
if let Some(rest) = text.strip_prefix('P') {
|
||||
let n = rest.trim().to_string();
|
||||
if !n.is_empty() { *name = n; }
|
||||
if !n.is_empty() {
|
||||
*name = n;
|
||||
}
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
if let Some(rest) = text.strip_prefix('S') {
|
||||
if let Ok(v) = rest.trim().replace(',', ".").parse::<f32>() {
|
||||
if v > 0.0 { *scale = v; }
|
||||
if v > 0.0 {
|
||||
*scale = v;
|
||||
}
|
||||
}
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
|
|
@ -116,16 +136,28 @@ impl CadCommand for HatcheditCommand {
|
|||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult { CmdResult::NeedPoint }
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
// Enter without text → apply current settings
|
||||
let (handle, name, scale, angle) = match &self.step {
|
||||
HatcheditStep::EditOptions { handle, name, scale, angle } => {
|
||||
(*handle, name.clone(), *scale, *angle)
|
||||
}
|
||||
HatcheditStep::EditOptions {
|
||||
handle,
|
||||
name,
|
||||
scale,
|
||||
angle,
|
||||
} => (*handle, name.clone(), *scale, *angle),
|
||||
_ => return CmdResult::Cancel,
|
||||
};
|
||||
CmdResult::HatcheditApply { handle, name, scale, angle }
|
||||
CmdResult::HatcheditApply {
|
||||
handle,
|
||||
name,
|
||||
scale,
|
||||
angle,
|
||||
}
|
||||
}
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,16 +23,28 @@ pub struct MlineCommand {
|
|||
impl MlineCommand {
|
||||
#[allow(dead_code)]
|
||||
pub fn new() -> Self {
|
||||
Self { points: vec![], scale: 1.0, waiting_scale: false, style_name: "Standard".into() }
|
||||
Self {
|
||||
points: vec![],
|
||||
scale: 1.0,
|
||||
waiting_scale: false,
|
||||
style_name: "Standard".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_style(style_name: impl Into<String>) -> Self {
|
||||
Self { points: vec![], scale: 1.0, waiting_scale: false, style_name: style_name.into() }
|
||||
Self {
|
||||
points: vec![],
|
||||
scale: 1.0,
|
||||
waiting_scale: false,
|
||||
style_name: style_name.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for MlineCommand {
|
||||
fn name(&self) -> &'static str { "MLINE" }
|
||||
fn name(&self) -> &'static str {
|
||||
"MLINE"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if self.waiting_scale {
|
||||
|
|
@ -40,7 +52,10 @@ impl CadCommand for MlineCommand {
|
|||
} else if self.points.is_empty() {
|
||||
format!("MLINE Specify start point (scale={:.2}):", self.scale)
|
||||
} else {
|
||||
format!("MLINE Specify next point ({} pts, Enter to finish, C to close, S to set scale):", self.points.len())
|
||||
format!(
|
||||
"MLINE Specify next point ({} pts, Enter to finish, C to close, S to set scale):",
|
||||
self.points.len()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,7 +66,12 @@ impl CadCommand for MlineCommand {
|
|||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
// Waiting for scale value
|
||||
if self.waiting_scale {
|
||||
let v: f64 = text.trim().replace(',', ".").parse().ok().filter(|&v: &f64| v > 0.0)?;
|
||||
let v: f64 = text
|
||||
.trim()
|
||||
.replace(',', ".")
|
||||
.parse()
|
||||
.ok()
|
||||
.filter(|&v: &f64| v > 0.0)?;
|
||||
self.scale = v;
|
||||
self.waiting_scale = false;
|
||||
return Some(CmdResult::NeedPoint);
|
||||
|
|
@ -74,7 +94,9 @@ impl CadCommand for MlineCommand {
|
|||
// Scale: "S <value>" inline
|
||||
if let Some(rest) = up.strip_prefix("S ") {
|
||||
if let Ok(v) = rest.trim().replace(',', ".").parse::<f64>() {
|
||||
if v > 0.0 { self.scale = v; }
|
||||
if v > 0.0 {
|
||||
self.scale = v;
|
||||
}
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
}
|
||||
|
|
@ -96,9 +118,10 @@ impl CadCommand for MlineCommand {
|
|||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
|
||||
if self.points.is_empty() { return None; }
|
||||
let mut pts: Vec<[f32; 3]> =
|
||||
self.points.iter().map(|p| [p.x, p.y, p.z]).collect();
|
||||
if self.points.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut pts: Vec<[f32; 3]> = self.points.iter().map(|p| [p.x, p.y, p.z]).collect();
|
||||
pts.push([pt.x, pt.y, pt.z]);
|
||||
Some(WireModel {
|
||||
name: "mline_preview".into(),
|
||||
|
|
@ -121,7 +144,8 @@ impl CadCommand for MlineCommand {
|
|||
}
|
||||
|
||||
fn build_mline(pts: &[Vec3], scale: f64, closed: bool, style_name: &str) -> EntityType {
|
||||
let verts: Vec<Vector3> = pts.iter()
|
||||
let verts: Vec<Vector3> = pts
|
||||
.iter()
|
||||
.map(|p| Vector3::new(p.x as f64, p.z as f64, p.y as f64))
|
||||
.collect();
|
||||
let mut mline = if closed {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ pub mod attdef;
|
|||
pub mod circle;
|
||||
pub mod donut;
|
||||
pub mod ellipse;
|
||||
pub mod hatchedit;
|
||||
pub mod hatch;
|
||||
pub mod hatchedit;
|
||||
pub mod line;
|
||||
pub mod mline;
|
||||
pub mod point;
|
||||
|
|
|
|||
|
|
@ -130,7 +130,8 @@ fn update_tangent_after_arc(tangent: &mut Option<Vec2>, bulge: f64) {
|
|||
// 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());
|
||||
*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.
|
||||
|
|
@ -169,7 +170,11 @@ fn arc_sample_points(a: Vec3, bulge: f64, b: Vec3, n: usize) -> Vec<[f32; 3]> {
|
|||
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.push([
|
||||
(cx + r * angle.cos()) as f32,
|
||||
(cy + r * angle.sin()) as f32,
|
||||
a.z,
|
||||
]);
|
||||
}
|
||||
pts
|
||||
}
|
||||
|
|
@ -308,6 +313,11 @@ impl CadCommand for PlineCommand {
|
|||
}
|
||||
}
|
||||
|
||||
Some(WireModel::solid("rubber_band".into(), pts, WireModel::CYAN, false))
|
||||
Some(WireModel::solid(
|
||||
"rubber_band".into(),
|
||||
pts,
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,12 @@ pub struct ImageCommand {
|
|||
|
||||
impl ImageCommand {
|
||||
pub fn new(file_path: String, pixel_width: u32, pixel_height: u32) -> Self {
|
||||
Self { file_path, pixel_width, pixel_height, origin: None }
|
||||
Self {
|
||||
file_path,
|
||||
pixel_width,
|
||||
pixel_height,
|
||||
origin: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn aspect(&self) -> f64 {
|
||||
|
|
@ -61,7 +66,10 @@ impl CadCommand for ImageCommand {
|
|||
|
||||
fn prompt(&self) -> String {
|
||||
if self.origin.is_none() {
|
||||
format!("IMAGE Specify insertion point ({}): ", short_name(&self.file_path))
|
||||
format!(
|
||||
"IMAGE Specify insertion point ({}): ",
|
||||
short_name(&self.file_path)
|
||||
)
|
||||
} else {
|
||||
"IMAGE Specify width (drag right):".into()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
use acadrust::entities::LwPolyline;
|
||||
use acadrust::types::Vector2;
|
||||
use acadrust::{EntityType, entities::LwVertex};
|
||||
use acadrust::{entities::LwVertex, EntityType};
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
|
|
@ -41,13 +41,21 @@ impl RevCloudCommand {
|
|||
}
|
||||
|
||||
impl CadCommand for RevCloudCommand {
|
||||
fn name(&self) -> &'static str { "REVCLOUD" }
|
||||
fn name(&self) -> &'static str {
|
||||
"REVCLOUD"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if self.points.is_empty() {
|
||||
format!("REVCLOUD Specify start point (arc length = {:.2}):", self.arc_length)
|
||||
format!(
|
||||
"REVCLOUD Specify start point (arc length = {:.2}):",
|
||||
self.arc_length
|
||||
)
|
||||
} else {
|
||||
format!("REVCLOUD Specify next point ({} pts, Enter to close):", self.points.len())
|
||||
format!(
|
||||
"REVCLOUD Specify next point ({} pts, Enter to close):",
|
||||
self.points.len()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -65,9 +73,10 @@ impl CadCommand for RevCloudCommand {
|
|||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
|
||||
if self.points.is_empty() { return None; }
|
||||
let mut preview_pts: Vec<[f32; 3]> =
|
||||
self.points.iter().map(|p| [p.x, p.y, p.z]).collect();
|
||||
if self.points.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut preview_pts: Vec<[f32; 3]> = self.points.iter().map(|p| [p.x, p.y, p.z]).collect();
|
||||
preview_pts.push([pt.x, pt.y, pt.z]);
|
||||
preview_pts.push([self.points[0].x, self.points[0].y, self.points[0].z]);
|
||||
Some(WireModel {
|
||||
|
|
@ -101,7 +110,9 @@ fn make_revcloud(pts: &[Vec3], arc_len: f64) -> EntityType {
|
|||
let p0 = pts[i];
|
||||
let p1 = pts[(i + 1) % n];
|
||||
let seg_len = ((p1.x - p0.x).powi(2) + (p1.z - p0.z).powi(2)).sqrt() as f64;
|
||||
if seg_len < 1e-6 { continue; }
|
||||
if seg_len < 1e-6 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// How many full arcs fit?
|
||||
let num_arcs = ((seg_len / arc_len).round() as usize).max(1);
|
||||
|
|
|
|||
|
|
@ -37,15 +37,25 @@ enum WipeoutMode {
|
|||
|
||||
impl WipeoutCommand {
|
||||
pub fn new_rectangular() -> Self {
|
||||
Self { mode: WipeoutMode::Rectangular, first: None, poly_pts: vec![] }
|
||||
Self {
|
||||
mode: WipeoutMode::Rectangular,
|
||||
first: None,
|
||||
poly_pts: vec![],
|
||||
}
|
||||
}
|
||||
pub fn new_polygonal() -> Self {
|
||||
Self { mode: WipeoutMode::Polygonal, first: None, poly_pts: vec![] }
|
||||
Self {
|
||||
mode: WipeoutMode::Polygonal,
|
||||
first: None,
|
||||
poly_pts: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for WipeoutCommand {
|
||||
fn name(&self) -> &'static str { "WIPEOUT" }
|
||||
fn name(&self) -> &'static str {
|
||||
"WIPEOUT"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.mode {
|
||||
|
|
@ -60,7 +70,10 @@ impl CadCommand for WipeoutCommand {
|
|||
if self.poly_pts.is_empty() {
|
||||
"WIPEOUT (Polygon) Specify first point:".into()
|
||||
} else {
|
||||
format!("WIPEOUT (Polygon) Specify next point ({} pts, Enter to close):", self.poly_pts.len())
|
||||
format!(
|
||||
"WIPEOUT (Polygon) Specify next point ({} pts, Enter to close):",
|
||||
self.poly_pts.len()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -117,15 +130,17 @@ impl CadCommand for WipeoutCommand {
|
|||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
aci: 0,
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
})
|
||||
}
|
||||
WipeoutMode::Polygonal => {
|
||||
if self.poly_pts.is_empty() { return None; }
|
||||
if self.poly_pts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut pts: Vec<[f32; 3]> =
|
||||
self.poly_pts.iter().map(|p| [p.x, p.y, p.z]).collect();
|
||||
pts.push([pt.x, pt.y, pt.z]);
|
||||
|
|
@ -141,11 +156,11 @@ impl CadCommand for WipeoutCommand {
|
|||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
aci: 0,
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -154,23 +169,16 @@ impl CadCommand for WipeoutCommand {
|
|||
|
||||
fn make_rect_wipeout(p1: Vec3, p2: Vec3) -> EntityType {
|
||||
// Y-up world: X→DXF X, Z→DXF Y, Y→DXF Z
|
||||
let c1 = Vector3::new(
|
||||
p1.x.min(p2.x) as f64,
|
||||
p1.z.min(p2.z) as f64,
|
||||
p1.y as f64,
|
||||
);
|
||||
let c2 = Vector3::new(
|
||||
p1.x.max(p2.x) as f64,
|
||||
p1.z.max(p2.z) as f64,
|
||||
p1.y as f64,
|
||||
);
|
||||
let c1 = Vector3::new(p1.x.min(p2.x) as f64, p1.z.min(p2.z) as f64, p1.y as f64);
|
||||
let c2 = Vector3::new(p1.x.max(p2.x) as f64, p1.z.max(p2.z) as f64, p1.y as f64);
|
||||
EntityType::Wipeout(Wipeout::from_corners(c1, c2))
|
||||
}
|
||||
|
||||
fn make_poly_wipeout(pts: &[Vec3]) -> EntityType {
|
||||
use acadrust::types::Vector2;
|
||||
let z = pts[0].y as f64;
|
||||
let verts: Vec<Vector2> = pts.iter()
|
||||
let verts: Vec<Vector2> = pts
|
||||
.iter()
|
||||
.map(|p| Vector2::new(p.x as f64, p.z as f64))
|
||||
.collect();
|
||||
EntityType::Wipeout(Wipeout::polygonal(&verts, z))
|
||||
|
|
|
|||
|
|
@ -19,12 +19,17 @@ pub struct DivideCommand {
|
|||
|
||||
impl DivideCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { target: None, waiting_for_n: false }
|
||||
Self {
|
||||
target: None,
|
||||
waiting_for_n: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for DivideCommand {
|
||||
fn name(&self) -> &'static str { "DIVIDE" }
|
||||
fn name(&self) -> &'static str {
|
||||
"DIVIDE"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if self.target.is_none() {
|
||||
|
|
@ -34,16 +39,22 @@ impl CadCommand for DivideCommand {
|
|||
}
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool { self.target.is_none() }
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
self.target.is_none()
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult {
|
||||
if handle.is_null() { return CmdResult::NeedPoint; }
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.target = Some(handle);
|
||||
self.waiting_for_n = true;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool { self.waiting_for_n }
|
||||
fn wants_text_input(&self) -> bool {
|
||||
self.waiting_for_n
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let n: usize = text.trim().parse().ok().filter(|&n| n >= 2)?;
|
||||
|
|
@ -52,8 +63,12 @@ impl CadCommand for DivideCommand {
|
|||
Some(CmdResult::DivideEntity { handle, n })
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult { CmdResult::NeedPoint }
|
||||
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
// ── MEASURE ────────────────────────────────────────────────────────────────
|
||||
|
|
@ -65,12 +80,17 @@ pub struct MeasureCommand {
|
|||
|
||||
impl MeasureCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { target: None, waiting_for_dist: false }
|
||||
Self {
|
||||
target: None,
|
||||
waiting_for_dist: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for MeasureCommand {
|
||||
fn name(&self) -> &'static str { "MEASURE" }
|
||||
fn name(&self) -> &'static str {
|
||||
"MEASURE"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if self.target.is_none() {
|
||||
|
|
@ -80,47 +100,75 @@ impl CadCommand for MeasureCommand {
|
|||
}
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool { self.target.is_none() }
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
self.target.is_none()
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult {
|
||||
if handle.is_null() { return CmdResult::NeedPoint; }
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.target = Some(handle);
|
||||
self.waiting_for_dist = true;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool { self.waiting_for_dist }
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let dist: f64 = text.trim().replace(',', ".").parse().ok().filter(|&d: &f64| d > 0.0)?;
|
||||
let handle = self.target?;
|
||||
self.waiting_for_dist = false;
|
||||
Some(CmdResult::MeasureEntity { handle, segment_length: dist })
|
||||
fn wants_text_input(&self) -> bool {
|
||||
self.waiting_for_dist
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult { CmdResult::NeedPoint }
|
||||
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let dist: f64 = text
|
||||
.trim()
|
||||
.replace(',', ".")
|
||||
.parse()
|
||||
.ok()
|
||||
.filter(|&d: &f64| d > 0.0)?;
|
||||
let handle = self.target?;
|
||||
self.waiting_for_dist = false;
|
||||
Some(CmdResult::MeasureEntity {
|
||||
handle,
|
||||
segment_length: dist,
|
||||
})
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
// ── Geometry ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// Compute N-1 equally spaced points along the entity (DIVIDE).
|
||||
pub fn divide_entity(entity: &EntityType, n: usize) -> Vec<EntityType> {
|
||||
if n < 2 { return vec![]; }
|
||||
if n < 2 {
|
||||
return vec![];
|
||||
}
|
||||
let total = entity_length(entity);
|
||||
if total < 1e-10 { return vec![]; }
|
||||
if total < 1e-10 {
|
||||
return vec![];
|
||||
}
|
||||
let step = total / n as f64;
|
||||
(1..n).filter_map(|k| {
|
||||
let t = step * k as f64;
|
||||
point_at_distance(entity, t).map(make_point)
|
||||
}).collect()
|
||||
(1..n)
|
||||
.filter_map(|k| {
|
||||
let t = step * k as f64;
|
||||
point_at_distance(entity, t).map(make_point)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Compute points at fixed `segment_length` intervals along the entity (MEASURE).
|
||||
pub fn measure_entity(entity: &EntityType, segment_length: f64) -> Vec<EntityType> {
|
||||
if segment_length <= 0.0 { return vec![]; }
|
||||
if segment_length <= 0.0 {
|
||||
return vec![];
|
||||
}
|
||||
let total = entity_length(entity);
|
||||
if total < 1e-10 { return vec![]; }
|
||||
if total < 1e-10 {
|
||||
return vec![];
|
||||
}
|
||||
let mut pts = Vec::new();
|
||||
let mut t = segment_length;
|
||||
while t < total - 1e-6 {
|
||||
|
|
@ -144,7 +192,7 @@ fn entity_length(entity: &EntityType) -> f64 {
|
|||
let dx = l.end.x - l.start.x;
|
||||
let dy = l.end.y - l.start.y;
|
||||
let dz = l.end.z - l.start.z;
|
||||
(dx*dx + dy*dy + dz*dz).sqrt()
|
||||
(dx * dx + dy * dy + dz * dz).sqrt()
|
||||
}
|
||||
EntityType::Arc(a) => {
|
||||
let span = arc_span_rad(a.start_angle, a.end_angle);
|
||||
|
|
@ -153,15 +201,19 @@ fn entity_length(entity: &EntityType) -> f64 {
|
|||
EntityType::Circle(c) => 2.0 * PI * c.radius,
|
||||
EntityType::LwPolyline(p) => {
|
||||
let n = p.vertices.len();
|
||||
if n < 2 { return 0.0; }
|
||||
if n < 2 {
|
||||
return 0.0;
|
||||
}
|
||||
let segs = if p.is_closed { n } else { n - 1 };
|
||||
(0..segs).map(|i| {
|
||||
let v0 = &p.vertices[i];
|
||||
let v1 = &p.vertices[(i + 1) % n];
|
||||
let dx = v1.location.x - v0.location.x;
|
||||
let dy = v1.location.y - v0.location.y;
|
||||
(dx*dx + dy*dy).sqrt()
|
||||
}).sum()
|
||||
(0..segs)
|
||||
.map(|i| {
|
||||
let v0 = &p.vertices[i];
|
||||
let v1 = &p.vertices[(i + 1) % n];
|
||||
let dx = v1.location.x - v0.location.x;
|
||||
let dy = v1.location.y - v0.location.y;
|
||||
(dx * dx + dy * dy).sqrt()
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
_ => 0.0,
|
||||
}
|
||||
|
|
@ -171,7 +223,9 @@ fn point_at_distance(entity: &EntityType, d: f64) -> Option<Vector3> {
|
|||
match entity {
|
||||
EntityType::Line(l) => {
|
||||
let total = entity_length(entity);
|
||||
if total < 1e-10 { return None; }
|
||||
if total < 1e-10 {
|
||||
return None;
|
||||
}
|
||||
let t = (d / total).clamp(0.0, 1.0);
|
||||
Some(Vector3::new(
|
||||
l.start.x + t * (l.end.x - l.start.x),
|
||||
|
|
@ -182,7 +236,9 @@ fn point_at_distance(entity: &EntityType, d: f64) -> Option<Vector3> {
|
|||
EntityType::Arc(a) => {
|
||||
let span = arc_span_rad(a.start_angle, a.end_angle);
|
||||
let t = d / a.radius; // arc_length = r * theta
|
||||
if t > span { return None; }
|
||||
if t > span {
|
||||
return None;
|
||||
}
|
||||
let angle = a.start_angle + t;
|
||||
Some(Vector3::new(
|
||||
a.center.x + a.radius * angle.cos(),
|
||||
|
|
@ -192,7 +248,9 @@ fn point_at_distance(entity: &EntityType, d: f64) -> Option<Vector3> {
|
|||
}
|
||||
EntityType::Circle(c) => {
|
||||
let circumference = 2.0 * PI * c.radius;
|
||||
if circumference < 1e-10 { return None; }
|
||||
if circumference < 1e-10 {
|
||||
return None;
|
||||
}
|
||||
let angle = 2.0 * PI * (d / circumference);
|
||||
Some(Vector3::new(
|
||||
c.center.x + c.radius * angle.cos(),
|
||||
|
|
@ -202,7 +260,9 @@ fn point_at_distance(entity: &EntityType, d: f64) -> Option<Vector3> {
|
|||
}
|
||||
EntityType::LwPolyline(p) => {
|
||||
let n = p.vertices.len();
|
||||
if n < 2 { return None; }
|
||||
if n < 2 {
|
||||
return None;
|
||||
}
|
||||
let segs = if p.is_closed { n } else { n - 1 };
|
||||
let mut acc = 0.0f64;
|
||||
for i in 0..segs {
|
||||
|
|
@ -210,7 +270,7 @@ fn point_at_distance(entity: &EntityType, d: f64) -> Option<Vector3> {
|
|||
let v1 = &p.vertices[(i + 1) % n];
|
||||
let dx = v1.location.x - v0.location.x;
|
||||
let dy = v1.location.y - v0.location.y;
|
||||
let seg_len = (dx*dx + dy*dy).sqrt();
|
||||
let seg_len = (dx * dx + dy * dy).sqrt();
|
||||
if acc + seg_len >= d - 1e-10 {
|
||||
let t = (d - acc) / seg_len.max(1e-10);
|
||||
return Some(Vector3::new(
|
||||
|
|
@ -229,5 +289,9 @@ fn point_at_distance(entity: &EntityType, d: f64) -> Option<Vector3> {
|
|||
|
||||
fn arc_span_rad(start: f64, end: f64) -> f64 {
|
||||
let span = (end - start).rem_euclid(2.0 * PI);
|
||||
if span < 1e-6 { 2.0 * PI } else { span }
|
||||
if span < 1e-6 {
|
||||
2.0 * PI
|
||||
} else {
|
||||
span
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ pub fn tool() -> ToolDef {
|
|||
ToolDef {
|
||||
id: "LAYMCUR",
|
||||
label: "Make Current",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/layers/laymcur.svg")),
|
||||
icon: IconKind::Svg(include_bytes!(
|
||||
"../../../../assets/icons/layers/laymcur.svg"
|
||||
)),
|
||||
event: ModuleEvent::Command("LAYMCUR".to_string()),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ pub fn tool() -> ToolDef {
|
|||
ToolDef {
|
||||
id: "LAYMATCH",
|
||||
label: "Match Layer",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/layers/laymatch.svg")),
|
||||
icon: IconKind::Svg(include_bytes!(
|
||||
"../../../../assets/icons/layers/laymatch.svg"
|
||||
)),
|
||||
event: ModuleEvent::Command("LAYMATCH".to_string()),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
// Home module — Draw, Modify, Annotation and Layer tools.
|
||||
|
||||
pub mod clipboard;
|
||||
pub mod defaults;
|
||||
mod about;
|
||||
mod changelog;
|
||||
pub mod clipboard;
|
||||
pub mod defaults;
|
||||
mod donate;
|
||||
pub mod draw;
|
||||
mod report;
|
||||
pub mod groups;
|
||||
pub mod inquiry;
|
||||
pub mod layers;
|
||||
pub mod modify;
|
||||
pub mod properties;
|
||||
mod report;
|
||||
pub mod select;
|
||||
|
||||
use crate::modules::{CadModule, RibbonGroup, RibbonItem};
|
||||
|
|
@ -27,21 +27,25 @@ impl CadModule for HomeModule {
|
|||
}
|
||||
|
||||
fn ribbon_groups(&self) -> Vec<RibbonGroup> {
|
||||
use crate::modules::annotate::{angular_dim, leader_cmd, linear_dim, mleader_cmd, mtext, radius_dim, text};
|
||||
use crate::modules::annotate::{
|
||||
angular_dim, leader_cmd, linear_dim, mleader_cmd, mtext, radius_dim, text,
|
||||
};
|
||||
use crate::modules::insert::{create_block, insert_block};
|
||||
use clipboard::{copy_clip, cut, paste};
|
||||
use about;
|
||||
use changelog;
|
||||
use clipboard::{copy_clip, cut, paste};
|
||||
use donate;
|
||||
use report;
|
||||
use draw::{arc, circle, ellipse, hatch, line, polyline, shapes};
|
||||
use layers::{layfrz, laylck, layoff, layon, laythw, layulk, make_current, match_layer, panel};
|
||||
use groups::{group, ungroup};
|
||||
use layers::{
|
||||
layfrz, laylck, layoff, layon, laythw, layulk, make_current, match_layer, panel,
|
||||
};
|
||||
use modify::{
|
||||
array, copy, delete, explode, fillet, mirror, offset, rotate, scale, stretch,
|
||||
translate, trim,
|
||||
};
|
||||
use properties::match_prop;
|
||||
use report;
|
||||
|
||||
vec![
|
||||
RibbonGroup {
|
||||
|
|
@ -133,9 +137,21 @@ impl CadModule for HomeModule {
|
|||
label: "Dimensions",
|
||||
icon: linear_dim::ICON,
|
||||
items: vec![
|
||||
(linear_dim::tool().id, linear_dim::tool().label, linear_dim::tool().icon),
|
||||
(radius_dim::tool().id, radius_dim::tool().label, radius_dim::tool().icon),
|
||||
(angular_dim::tool().id, angular_dim::tool().label, angular_dim::tool().icon),
|
||||
(
|
||||
linear_dim::tool().id,
|
||||
linear_dim::tool().label,
|
||||
linear_dim::tool().icon,
|
||||
),
|
||||
(
|
||||
radius_dim::tool().id,
|
||||
radius_dim::tool().label,
|
||||
radius_dim::tool().icon,
|
||||
),
|
||||
(
|
||||
angular_dim::tool().id,
|
||||
angular_dim::tool().label,
|
||||
angular_dim::tool().icon,
|
||||
),
|
||||
],
|
||||
default: "DIMLINEAR",
|
||||
},
|
||||
|
|
@ -144,8 +160,16 @@ impl CadModule for HomeModule {
|
|||
label: "Leader",
|
||||
icon: leader_cmd::ICON,
|
||||
items: vec![
|
||||
(mleader_cmd::tool().id, mleader_cmd::tool().label, mleader_cmd::tool().icon),
|
||||
(leader_cmd::tool().id, leader_cmd::tool().label, leader_cmd::tool().icon),
|
||||
(
|
||||
mleader_cmd::tool().id,
|
||||
mleader_cmd::tool().label,
|
||||
mleader_cmd::tool().icon,
|
||||
),
|
||||
(
|
||||
leader_cmd::tool().id,
|
||||
leader_cmd::tool().label,
|
||||
leader_cmd::tool().icon,
|
||||
),
|
||||
],
|
||||
default: "MLEADER",
|
||||
},
|
||||
|
|
@ -156,8 +180,18 @@ impl CadModule for HomeModule {
|
|||
tools: vec![
|
||||
RibbonItem::LargeTool(panel::tool()),
|
||||
RibbonItem::LayerComboGroup {
|
||||
row2: vec![layoff::tool(), layfrz::tool(), laylck::tool(), make_current::tool()],
|
||||
row3: vec![layon::tool(), laythw::tool(), layulk::tool(), match_layer::tool()],
|
||||
row2: vec![
|
||||
layoff::tool(),
|
||||
layfrz::tool(),
|
||||
laylck::tool(),
|
||||
make_current::tool(),
|
||||
],
|
||||
row3: vec![
|
||||
layon::tool(),
|
||||
laythw::tool(),
|
||||
layulk::tool(),
|
||||
match_layer::tool(),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -170,9 +204,9 @@ impl CadModule for HomeModule {
|
|||
},
|
||||
RibbonGroup {
|
||||
title: "Properties",
|
||||
tools: vec![
|
||||
RibbonItem::PropertiesGroup { match_prop: match_prop::tool() },
|
||||
],
|
||||
tools: vec![RibbonItem::PropertiesGroup {
|
||||
match_prop: match_prop::tool(),
|
||||
}],
|
||||
},
|
||||
RibbonGroup {
|
||||
title: "Groups",
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@
|
|||
// With 1 pair: pure translation (src1 → dst1)
|
||||
// With 2 pairs: translate + rotate (+ optional uniform scale to fit)
|
||||
|
||||
use glam::Vec3;
|
||||
use acadrust::Handle;
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, EntityTransform};
|
||||
|
||||
|
|
@ -38,23 +38,30 @@ impl AlignCommand {
|
|||
Self {
|
||||
state: AlignState::Gathering,
|
||||
handles: vec![],
|
||||
src1: None, dst1: None,
|
||||
src2: None, dst2: None,
|
||||
src1: None,
|
||||
dst1: None,
|
||||
src2: None,
|
||||
dst2: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for AlignCommand {
|
||||
fn name(&self) -> &'static str { "ALIGN" }
|
||||
fn name(&self) -> &'static str {
|
||||
"ALIGN"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match self.state {
|
||||
AlignState::Gathering => format!("ALIGN Select objects ({} selected, Enter when done):", self.handles.len()),
|
||||
AlignState::Src1 => "ALIGN Specify 1st source point:".into(),
|
||||
AlignState::Dst1 => "ALIGN Specify 1st destination point:".into(),
|
||||
AlignState::Src2 => "ALIGN Specify 2nd source point (Enter = translate only):".into(),
|
||||
AlignState::Dst2 => "ALIGN Specify 2nd destination point:".into(),
|
||||
AlignState::AskScale => "ALIGN Scale objects based on alignment points? [Y/N]:".into(),
|
||||
AlignState::Gathering => format!(
|
||||
"ALIGN Select objects ({} selected, Enter when done):",
|
||||
self.handles.len()
|
||||
),
|
||||
AlignState::Src1 => "ALIGN Specify 1st source point:".into(),
|
||||
AlignState::Dst1 => "ALIGN Specify 1st destination point:".into(),
|
||||
AlignState::Src2 => "ALIGN Specify 2nd source point (Enter = translate only):".into(),
|
||||
AlignState::Dst2 => "ALIGN Specify 2nd destination point:".into(),
|
||||
AlignState::AskScale => "ALIGN Scale objects based on alignment points? [Y/N]:".into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +104,9 @@ impl CadCommand for AlignCommand {
|
|||
fn on_enter(&mut self) -> CmdResult {
|
||||
match self.state {
|
||||
AlignState::Gathering => {
|
||||
if self.handles.is_empty() { return CmdResult::Cancel; }
|
||||
if self.handles.is_empty() {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
self.state = AlignState::Src1;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
|
@ -127,7 +136,9 @@ impl CadCommand for AlignCommand {
|
|||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
if self.state != AlignState::AskScale { return None; }
|
||||
if self.state != AlignState::AskScale {
|
||||
return None;
|
||||
}
|
||||
let scale = text.trim().to_uppercase().starts_with('Y');
|
||||
Some(self.compute_align(scale))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -705,11 +705,31 @@ impl CadCommand for ArrayPathCommand {
|
|||
#[derive(Debug, Clone, Copy)]
|
||||
enum Array3DStep {
|
||||
Rows,
|
||||
Cols { rows: u32 },
|
||||
Levels { rows: u32, cols: u32 },
|
||||
RowSp { rows: u32, cols: u32, levels: u32 },
|
||||
ColSp { rows: u32, cols: u32, levels: u32, row_sp: f32 },
|
||||
LvlSp { rows: u32, cols: u32, levels: u32, row_sp: f32, col_sp: f32 },
|
||||
Cols {
|
||||
rows: u32,
|
||||
},
|
||||
Levels {
|
||||
rows: u32,
|
||||
cols: u32,
|
||||
},
|
||||
RowSp {
|
||||
rows: u32,
|
||||
cols: u32,
|
||||
levels: u32,
|
||||
},
|
||||
ColSp {
|
||||
rows: u32,
|
||||
cols: u32,
|
||||
levels: u32,
|
||||
row_sp: f32,
|
||||
},
|
||||
LvlSp {
|
||||
rows: u32,
|
||||
cols: u32,
|
||||
levels: u32,
|
||||
row_sp: f32,
|
||||
col_sp: f32,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct Array3DCommand {
|
||||
|
|
@ -719,18 +739,27 @@ pub struct Array3DCommand {
|
|||
|
||||
impl Array3DCommand {
|
||||
pub fn new(handles: Vec<Handle>) -> Self {
|
||||
Self { handles, step: Array3DStep::Rows }
|
||||
Self {
|
||||
handles,
|
||||
step: Array3DStep::Rows,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_transforms(
|
||||
rows: u32, cols: u32, levels: u32,
|
||||
row_sp: f32, col_sp: f32, lvl_sp: f32,
|
||||
rows: u32,
|
||||
cols: u32,
|
||||
levels: u32,
|
||||
row_sp: f32,
|
||||
col_sp: f32,
|
||||
lvl_sp: f32,
|
||||
) -> Vec<EntityTransform> {
|
||||
let mut t = Vec::new();
|
||||
for l in 0..levels {
|
||||
for r in 0..rows {
|
||||
for c in 0..cols {
|
||||
if l == 0 && r == 0 && c == 0 { continue; }
|
||||
if l == 0 && r == 0 && c == 0 {
|
||||
continue;
|
||||
}
|
||||
// World: X=col dir, Z=row dir (DXF Y), Y=level (up)
|
||||
t.push(EntityTransform::Translate(Vec3::new(
|
||||
col_sp * c as f32,
|
||||
|
|
@ -745,65 +774,142 @@ impl Array3DCommand {
|
|||
}
|
||||
|
||||
impl CadCommand for Array3DCommand {
|
||||
fn name(&self) -> &'static str { "ARRAY3D" }
|
||||
fn name(&self) -> &'static str {
|
||||
"ARRAY3D"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match self.step {
|
||||
Array3DStep::Rows => "ARRAY3D Enter row count:".into(),
|
||||
Array3DStep::Cols { rows } => format!("ARRAY3D Enter column count [{rows} rows]:"),
|
||||
Array3DStep::Levels { rows, cols } => format!("ARRAY3D Enter level count [{rows}×{cols}]:"),
|
||||
Array3DStep::RowSp { rows, cols, levels } => format!("ARRAY3D Row spacing [{rows}×{cols}×{levels}]:"),
|
||||
Array3DStep::ColSp { rows, cols, levels, row_sp } => format!("ARRAY3D Column spacing [{rows}×{cols}×{levels}, row={row_sp:.0}]:"),
|
||||
Array3DStep::LvlSp { rows, cols, levels, row_sp, col_sp } => format!("ARRAY3D Level spacing [{rows}×{cols}×{levels}, r={row_sp:.0} c={col_sp:.0}]:"),
|
||||
Array3DStep::Levels { rows, cols } => {
|
||||
format!("ARRAY3D Enter level count [{rows}×{cols}]:")
|
||||
}
|
||||
Array3DStep::RowSp { rows, cols, levels } => {
|
||||
format!("ARRAY3D Row spacing [{rows}×{cols}×{levels}]:")
|
||||
}
|
||||
Array3DStep::ColSp {
|
||||
rows,
|
||||
cols,
|
||||
levels,
|
||||
row_sp,
|
||||
} => format!("ARRAY3D Column spacing [{rows}×{cols}×{levels}, row={row_sp:.0}]:"),
|
||||
Array3DStep::LvlSp {
|
||||
rows,
|
||||
cols,
|
||||
levels,
|
||||
row_sp,
|
||||
col_sp,
|
||||
} => format!(
|
||||
"ARRAY3D Level spacing [{rows}×{cols}×{levels}, r={row_sp:.0} c={col_sp:.0}]:"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool { true }
|
||||
fn wants_text_input(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let t = text.trim().replace(',', ".");
|
||||
let t = t.as_str();
|
||||
match self.step {
|
||||
Array3DStep::Rows => {
|
||||
let v = if t.is_empty() { 2 } else { t.parse::<u32>().unwrap_or(2).max(1) };
|
||||
let v = if t.is_empty() {
|
||||
2
|
||||
} else {
|
||||
t.parse::<u32>().unwrap_or(2).max(1)
|
||||
};
|
||||
self.step = Array3DStep::Cols { rows: v };
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Array3DStep::Cols { rows } => {
|
||||
let v = if t.is_empty() { 2 } else { t.parse::<u32>().unwrap_or(2).max(1) };
|
||||
let v = if t.is_empty() {
|
||||
2
|
||||
} else {
|
||||
t.parse::<u32>().unwrap_or(2).max(1)
|
||||
};
|
||||
self.step = Array3DStep::Levels { rows, cols: v };
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Array3DStep::Levels { rows, cols } => {
|
||||
let v = if t.is_empty() { 2 } else { t.parse::<u32>().unwrap_or(2).max(1) };
|
||||
self.step = Array3DStep::RowSp { rows, cols, levels: v };
|
||||
let v = if t.is_empty() {
|
||||
2
|
||||
} else {
|
||||
t.parse::<u32>().unwrap_or(2).max(1)
|
||||
};
|
||||
self.step = Array3DStep::RowSp {
|
||||
rows,
|
||||
cols,
|
||||
levels: v,
|
||||
};
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Array3DStep::RowSp { rows, cols, levels } => {
|
||||
let v: f32 = if t.is_empty() { 1.0 } else { t.parse().unwrap_or(1.0) };
|
||||
self.step = Array3DStep::ColSp { rows, cols, levels, row_sp: v };
|
||||
let v: f32 = if t.is_empty() {
|
||||
1.0
|
||||
} else {
|
||||
t.parse().unwrap_or(1.0)
|
||||
};
|
||||
self.step = Array3DStep::ColSp {
|
||||
rows,
|
||||
cols,
|
||||
levels,
|
||||
row_sp: v,
|
||||
};
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Array3DStep::ColSp { rows, cols, levels, row_sp } => {
|
||||
let v: f32 = if t.is_empty() { 1.0 } else { t.parse().unwrap_or(1.0) };
|
||||
self.step = Array3DStep::LvlSp { rows, cols, levels, row_sp, col_sp: v };
|
||||
Array3DStep::ColSp {
|
||||
rows,
|
||||
cols,
|
||||
levels,
|
||||
row_sp,
|
||||
} => {
|
||||
let v: f32 = if t.is_empty() {
|
||||
1.0
|
||||
} else {
|
||||
t.parse().unwrap_or(1.0)
|
||||
};
|
||||
self.step = Array3DStep::LvlSp {
|
||||
rows,
|
||||
cols,
|
||||
levels,
|
||||
row_sp,
|
||||
col_sp: v,
|
||||
};
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Array3DStep::LvlSp { rows, cols, levels, row_sp, col_sp } => {
|
||||
let v: f32 = if t.is_empty() { 1.0 } else { t.parse().unwrap_or(1.0) };
|
||||
Array3DStep::LvlSp {
|
||||
rows,
|
||||
cols,
|
||||
levels,
|
||||
row_sp,
|
||||
col_sp,
|
||||
} => {
|
||||
let v: f32 = if t.is_empty() {
|
||||
1.0
|
||||
} else {
|
||||
t.parse().unwrap_or(1.0)
|
||||
};
|
||||
let transforms = Self::build_transforms(rows, cols, levels, row_sp, col_sp, v);
|
||||
Some(CmdResult::BatchCopy(self.handles.clone(), transforms))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult { CmdResult::NeedPoint }
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
self.on_text_input("").map_or(CmdResult::NeedPoint, |r| r)
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_preview_wires(&mut self, _pt: Vec3) -> Vec<WireModel> { vec![] }
|
||||
fn on_preview_wires(&mut self, _pt: Vec3) -> Vec<WireModel> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,19 +31,26 @@ enum Step {
|
|||
|
||||
impl AtteditCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { step: Step::SelectInsert }
|
||||
Self {
|
||||
step: Step::SelectInsert,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for AtteditCommand {
|
||||
fn name(&self) -> &'static str { "ATTEDIT" }
|
||||
fn name(&self) -> &'static str {
|
||||
"ATTEDIT"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::SelectInsert => "ATTEDIT Select block with attributes:".to_string(),
|
||||
Step::EditAttr { attrs, idx, .. } => {
|
||||
let (tag, val) = &attrs[*idx];
|
||||
format!("ATTEDIT {} = <{}> (Enter to keep, type new value):", tag, val)
|
||||
format!(
|
||||
"ATTEDIT {} = <{}> (Enter to keep, type new value):",
|
||||
tag, val
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -61,7 +68,7 @@ impl CadCommand for AtteditCommand {
|
|||
// Instead, signal the host to call prepare_attedit().
|
||||
self.step = Step::EditAttr {
|
||||
handle,
|
||||
attrs: vec![], // will be filled by init_with_attrs() in cmd_result.rs
|
||||
attrs: vec![], // will be filled by init_with_attrs() in cmd_result.rs
|
||||
idx: 0,
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
|
|
@ -76,7 +83,9 @@ impl CadCommand for AtteditCommand {
|
|||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let Step::EditAttr { handle, attrs, idx } = &mut self.step else { return None; };
|
||||
let Step::EditAttr { handle, attrs, idx } = &mut self.step else {
|
||||
return None;
|
||||
};
|
||||
let handle = *handle;
|
||||
|
||||
// Update the current attribute value if the user typed something.
|
||||
|
|
@ -99,9 +108,15 @@ impl CadCommand for AtteditCommand {
|
|||
None
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult { CmdResult::NeedPoint }
|
||||
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
fn on_preview_wires(&mut self, _pt: Vec3) -> Vec<WireModel> { vec![] }
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
fn on_preview_wires(&mut self, _pt: Vec3) -> Vec<WireModel> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn attedit_pending_handle(&self) -> Option<acadrust::Handle> {
|
||||
if let Step::EditAttr { handle, attrs, .. } = &self.step {
|
||||
|
|
@ -122,10 +137,7 @@ impl CadCommand for AtteditCommand {
|
|||
|
||||
/// Make a sentinel entity carrying the edited attribute values.
|
||||
/// Encodes all (tag=value) pairs in the layer field as "tag1\x01val1\x02tag2\x01val2...".
|
||||
fn make_attedit_sentinel(
|
||||
_handle: acadrust::Handle,
|
||||
pairs: &[(String, String)],
|
||||
) -> EntityType {
|
||||
fn make_attedit_sentinel(_handle: acadrust::Handle, pairs: &[(String, String)]) -> EntityType {
|
||||
let encoded: String = pairs
|
||||
.iter()
|
||||
.map(|(t, v)| format!("{}\x01{}", t, v))
|
||||
|
|
@ -141,19 +153,20 @@ fn make_attedit_sentinel(
|
|||
|
||||
/// Apply edited attribute values to an INSERT entity in the document.
|
||||
/// Called from `cmd_result.rs` when the sentinel is detected.
|
||||
pub fn apply_attedit(
|
||||
doc: &mut acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
encoded: &str,
|
||||
) {
|
||||
let Some(EntityType::Insert(ins)) = doc.get_entity_mut(handle) else { return; };
|
||||
pub fn apply_attedit(doc: &mut acadrust::CadDocument, handle: acadrust::Handle, encoded: &str) {
|
||||
let Some(EntityType::Insert(ins)) = doc.get_entity_mut(handle) else {
|
||||
return;
|
||||
};
|
||||
for pair in encoded.split('\x02') {
|
||||
let mut parts = pair.splitn(2, '\x01');
|
||||
let Some(tag) = parts.next() else { continue; };
|
||||
let Some(val) = parts.next() else { continue; };
|
||||
let Some(tag) = parts.next() else {
|
||||
continue;
|
||||
};
|
||||
let Some(val) = parts.next() else {
|
||||
continue;
|
||||
};
|
||||
if let Some(attrib) = ins.attributes.iter_mut().find(|a| a.tag == tag) {
|
||||
attrib.set_value(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,12 +7,16 @@
|
|||
//
|
||||
// BREAK @ (at-sign as second point) → Break at a single point (splits without gap).
|
||||
|
||||
use acadrust::entities::{Arc as ArcEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline, Spline as SplineEnt};
|
||||
use truck_modeling::base::{BoundedCurve, Cut};
|
||||
use crate::modules::home::modify::spline_ops::{bspline_to_spline, spline_nearest_t, spline_to_bspline};
|
||||
use crate::modules::home::modify::spline_ops::{
|
||||
bspline_to_spline, spline_nearest_t, spline_to_bspline,
|
||||
};
|
||||
use acadrust::entities::{
|
||||
Arc as ArcEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline, Spline as SplineEnt,
|
||||
};
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::{EntityType, Handle};
|
||||
use glam::Vec3;
|
||||
use truck_modeling::base::{BoundedCurve, Cut};
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
|
@ -37,17 +41,21 @@ pub fn tool() -> ToolDef {
|
|||
pub fn break_entity(entity: &EntityType, p1: Vec3, p2: Vec3) -> Option<Vec<EntityType>> {
|
||||
match entity {
|
||||
EntityType::Line(line) => Some(break_line(line, p1, p2)),
|
||||
EntityType::Arc(arc) => Some(break_arc(arc, p1, p2)),
|
||||
EntityType::Circle(c) => Some(break_circle(c, p1, p2)),
|
||||
EntityType::Arc(arc) => Some(break_arc(arc, p1, p2)),
|
||||
EntityType::Circle(c) => Some(break_circle(c, p1, p2)),
|
||||
EntityType::LwPolyline(p) => Some(break_lwpolyline(p, p1, p2)),
|
||||
EntityType::Ellipse(e) => Some(break_ellipse(e, p1, p2)),
|
||||
EntityType::Spline(s) => Some(break_spline(s, p1, p2)),
|
||||
EntityType::Ellipse(e) => Some(break_ellipse(e, p1, p2)),
|
||||
EntityType::Spline(s) => Some(break_spline(s, p1, p2)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn break_line(line: &LineEnt, p1: Vec3, p2: Vec3) -> Vec<EntityType> {
|
||||
let s = Vec3::new(line.start.x as f32, line.start.z as f32, line.start.y as f32);
|
||||
let s = Vec3::new(
|
||||
line.start.x as f32,
|
||||
line.start.z as f32,
|
||||
line.start.y as f32,
|
||||
);
|
||||
let e = Vec3::new(line.end.x as f32, line.end.z as f32, line.end.y as f32);
|
||||
let dir = e - s;
|
||||
let len2 = dir.length_squared();
|
||||
|
|
@ -189,7 +197,9 @@ fn break_ellipse(ell: &EllipseEnt, p1: Vec3, p2: Vec3) -> Vec<EntityType> {
|
|||
let cx = ell.center.x;
|
||||
let cy = ell.center.y;
|
||||
let a = (ell.major_axis.x.powi(2) + ell.major_axis.y.powi(2)).sqrt();
|
||||
if a < 1e-9 { return vec![EntityType::Ellipse(ell.clone())]; }
|
||||
if a < 1e-9 {
|
||||
return vec![EntityType::Ellipse(ell.clone())];
|
||||
}
|
||||
let _b = a * ell.minor_axis_ratio;
|
||||
let nx = ell.major_axis.x / a;
|
||||
let ny = ell.major_axis.y / a;
|
||||
|
|
@ -198,7 +208,7 @@ fn break_ellipse(ell: &EllipseEnt, p1: Vec3, p2: Vec3) -> Vec<EntityType> {
|
|||
let param_of = |pt: Vec3| -> f64 {
|
||||
let rx = pt.x as f64 - cx;
|
||||
let ry = pt.z as f64 - cy; // Y-up: DXF Y → world Z
|
||||
let xl = rx * nx + ry * ny;
|
||||
let xl = rx * nx + ry * ny;
|
||||
let yl = -rx * ny + ry * nx;
|
||||
yl.atan2(xl) // atan2(yl/b*b, xl/a*a) simplifies to atan2(yl,xl) for ordering
|
||||
};
|
||||
|
|
@ -212,11 +222,22 @@ fn break_ellipse(ell: &EllipseEnt, p1: Vec3, p2: Vec3) -> Vec<EntityType> {
|
|||
// Clamp both params to arc range (same logic as clamp_to_arc for arcs)
|
||||
let span_deg = {
|
||||
let s = t1 - t0;
|
||||
if s <= 0.0 { s + std::f64::consts::TAU } else { s }
|
||||
if s <= 0.0 {
|
||||
s + std::f64::consts::TAU
|
||||
} else {
|
||||
s
|
||||
}
|
||||
};
|
||||
let clamp = |a: f64| -> f64 {
|
||||
let rel = ((a - t0) % std::f64::consts::TAU + std::f64::consts::TAU) % std::f64::consts::TAU;
|
||||
if rel <= span_deg { a } else if rel < span_deg + (std::f64::consts::TAU - span_deg) / 2.0 { t1 } else { t0 }
|
||||
let rel =
|
||||
((a - t0) % std::f64::consts::TAU + std::f64::consts::TAU) % std::f64::consts::TAU;
|
||||
if rel <= span_deg {
|
||||
a
|
||||
} else if rel < span_deg + (std::f64::consts::TAU - span_deg) / 2.0 {
|
||||
t1
|
||||
} else {
|
||||
t0
|
||||
}
|
||||
};
|
||||
let a1_on = clamp(pa1);
|
||||
let a2_on = clamp(pa2);
|
||||
|
|
@ -229,7 +250,7 @@ fn break_ellipse(ell: &EllipseEnt, p1: Vec3, p2: Vec3) -> Vec<EntityType> {
|
|||
let mut result = ell.clone();
|
||||
result.common.handle = Handle::NULL;
|
||||
result.start_parameter = a2_on;
|
||||
result.end_parameter = a1_on;
|
||||
result.end_parameter = a1_on;
|
||||
vec![EntityType::Ellipse(result)]
|
||||
}
|
||||
|
||||
|
|
@ -257,11 +278,16 @@ fn clamp_to_arc(a: f32, start: f32, end: f32) -> f32 {
|
|||
|
||||
/// Find the index of the polyline vertex closest to `pt`.
|
||||
fn nearest_pline_param(p: &LwPolyline, pt: Vec3) -> usize {
|
||||
p.vertices.iter().enumerate().min_by_key(|(_, v)| {
|
||||
let dx = v.location.x as f32 - pt.x;
|
||||
let dy = v.location.y as f32 - pt.z;
|
||||
((dx * dx + dy * dy) * 1e6) as i64
|
||||
}).map(|(i, _)| i).unwrap_or(0)
|
||||
p.vertices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.min_by_key(|(_, v)| {
|
||||
let dx = v.location.x as f32 - pt.x;
|
||||
let dy = v.location.y as f32 - pt.z;
|
||||
((dx * dx + dy * dy) * 1e6) as i64
|
||||
})
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn world_to_dxf(v: Vec3) -> Vec3 {
|
||||
|
|
@ -307,13 +333,13 @@ fn break_spline(spl: &SplineEnt, p1: Vec3, p2: Vec3) -> Vec<EntityType> {
|
|||
// Left piece [t0, ta]
|
||||
if ta - t0 > 1e-9 {
|
||||
let mut left = bs.clone();
|
||||
let _right = left.cut(ta); // left = [t0, ta]
|
||||
let _right = left.cut(ta); // left = [t0, ta]
|
||||
result.push(EntityType::Spline(bspline_to_spline(&left, spl)));
|
||||
}
|
||||
// Right piece [tb, t_end]
|
||||
if t_end - tb > 1e-9 {
|
||||
let mut full = bs.clone();
|
||||
let right = full.cut(tb); // right = [tb, t_end]
|
||||
let right = full.cut(tb); // right = [tb, t_end]
|
||||
result.push(EntityType::Spline(bspline_to_spline(&right, spl)));
|
||||
}
|
||||
result
|
||||
|
|
@ -330,12 +356,17 @@ pub struct BreakInteractiveCommand {
|
|||
|
||||
impl BreakInteractiveCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { target: None, p1: None }
|
||||
Self {
|
||||
target: None,
|
||||
p1: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for BreakInteractiveCommand {
|
||||
fn name(&self) -> &'static str { "BREAK" }
|
||||
fn name(&self) -> &'static str {
|
||||
"BREAK"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if self.target.is_none() {
|
||||
|
|
@ -397,7 +428,9 @@ impl BreakAtPointCommand {
|
|||
}
|
||||
|
||||
impl CadCommand for BreakAtPointCommand {
|
||||
fn name(&self) -> &'static str { "BREAKATPOINT" }
|
||||
fn name(&self) -> &'static str {
|
||||
"BREAKATPOINT"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if self.target.is_none() {
|
||||
|
|
@ -421,7 +454,11 @@ impl CadCommand for BreakAtPointCommand {
|
|||
|
||||
fn on_point(&mut self, pt: Vec3) -> CmdResult {
|
||||
match self.target {
|
||||
Some(handle) => CmdResult::BreakEntity { handle, p1: pt, p2: pt },
|
||||
Some(handle) => CmdResult::BreakEntity {
|
||||
handle,
|
||||
p1: pt,
|
||||
p2: pt,
|
||||
},
|
||||
None => CmdResult::Cancel,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -310,10 +310,16 @@ fn explode_mline(ml: &MLine) -> Vec<EntityType> {
|
|||
// Helper: build a Line from two Vector3 positions.
|
||||
let make_line = |common: &acadrust::entities::EntityCommon,
|
||||
s: &acadrust::types::Vector3,
|
||||
e: &acadrust::types::Vector3| -> EntityType {
|
||||
e: &acadrust::types::Vector3|
|
||||
-> EntityType {
|
||||
let mut c = common.clone();
|
||||
c.handle = Handle::NULL;
|
||||
EntityType::Line(LineEnt { common: c, start: s.clone(), end: e.clone(), ..LineEnt::new() })
|
||||
EntityType::Line(LineEnt {
|
||||
common: c,
|
||||
start: s.clone(),
|
||||
end: e.clone(),
|
||||
..LineEnt::new()
|
||||
})
|
||||
};
|
||||
|
||||
// For each segment, emit the center-spine line and the two ±scale/2 offset lines.
|
||||
|
|
@ -361,20 +367,30 @@ fn explode_dimension(dim: &Dimension) -> Vec<EntityType> {
|
|||
let make_seg = |a: &Vector3, b: &Vector3, common: &EntityCommon| -> EntityType {
|
||||
let mut c = common.clone();
|
||||
c.handle = Handle::NULL;
|
||||
EntityType::Line(LineEnt { common: c, start: a.clone(), end: b.clone(), ..LineEnt::new() })
|
||||
EntityType::Line(LineEnt {
|
||||
common: c,
|
||||
start: a.clone(),
|
||||
end: b.clone(),
|
||||
..LineEnt::new()
|
||||
})
|
||||
};
|
||||
|
||||
let v3 = |x: f64, y: f64, z: f64| Vector3::new(x, y, z);
|
||||
|
||||
match dim {
|
||||
Dimension::Aligned(d) => {
|
||||
let fx = d.first_point.x; let fy = d.first_point.y;
|
||||
let sx = d.second_point.x; let sy = d.second_point.y;
|
||||
let dx_s = sx - fx; let dy_s = sy - fy;
|
||||
let len = (dx_s*dx_s + dy_s*dy_s).sqrt().max(1e-12);
|
||||
let fx = d.first_point.x;
|
||||
let fy = d.first_point.y;
|
||||
let sx = d.second_point.x;
|
||||
let sy = d.second_point.y;
|
||||
let dx_s = sx - fx;
|
||||
let dy_s = sy - fy;
|
||||
let len = (dx_s * dx_s + dy_s * dy_s).sqrt().max(1e-12);
|
||||
let axis_angle = dy_s.atan2(dx_s);
|
||||
let perp_x = -(axis_angle.sin()); let perp_y = axis_angle.cos();
|
||||
let offset = (d.definition_point.x - fx) * perp_x + (d.definition_point.y - fy) * perp_y;
|
||||
let perp_x = -(axis_angle.sin());
|
||||
let perp_y = axis_angle.cos();
|
||||
let offset =
|
||||
(d.definition_point.x - fx) * perp_x + (d.definition_point.y - fy) * perp_y;
|
||||
let d1 = v3(fx + perp_x * offset, fy + perp_y * offset, d.first_point.z);
|
||||
let d2 = v3(sx + perp_x * offset, sy + perp_y * offset, d.second_point.z);
|
||||
result.push(make_seg(&d.first_point, &d1, &common));
|
||||
|
|
@ -384,10 +400,14 @@ fn explode_dimension(dim: &Dimension) -> Vec<EntityType> {
|
|||
}
|
||||
Dimension::Linear(d) => {
|
||||
let angle = d.rotation.to_radians();
|
||||
let perp_x = -(angle.sin()); let perp_y = angle.cos();
|
||||
let fx = d.first_point.x; let fy = d.first_point.y;
|
||||
let sx = d.second_point.x; let sy = d.second_point.y;
|
||||
let offset = (d.definition_point.x - fx) * perp_x + (d.definition_point.y - fy) * perp_y;
|
||||
let perp_x = -(angle.sin());
|
||||
let perp_y = angle.cos();
|
||||
let fx = d.first_point.x;
|
||||
let fy = d.first_point.y;
|
||||
let sx = d.second_point.x;
|
||||
let sy = d.second_point.y;
|
||||
let offset =
|
||||
(d.definition_point.x - fx) * perp_x + (d.definition_point.y - fy) * perp_y;
|
||||
let d1 = v3(fx + perp_x * offset, fy + perp_y * offset, d.first_point.z);
|
||||
let d2 = v3(sx + perp_x * offset, sy + perp_y * offset, d.second_point.z);
|
||||
result.push(make_seg(&d.first_point, &d1, &common));
|
||||
|
|
@ -416,15 +436,20 @@ fn explode_dimension(dim: &Dimension) -> Vec<EntityType> {
|
|||
|
||||
// Text entity for the dimension label
|
||||
let text_val = if let Some(u) = &base.user_text {
|
||||
if !u.trim().is_empty() { u.clone() } else { format!("{:.4}", dim.measurement()) }
|
||||
if !u.trim().is_empty() {
|
||||
u.clone()
|
||||
} else {
|
||||
format!("{:.4}", dim.measurement())
|
||||
}
|
||||
} else if !base.text.trim().is_empty() {
|
||||
base.text.clone()
|
||||
} else {
|
||||
match dim {
|
||||
Dimension::Radius(_) => format!("R{:.4}", dim.measurement()),
|
||||
Dimension::Radius(_) => format!("R{:.4}", dim.measurement()),
|
||||
Dimension::Diameter(_) => format!("Ø{:.4}", dim.measurement()),
|
||||
Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_) =>
|
||||
format!("{:.2}°", dim.measurement()),
|
||||
Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_) => {
|
||||
format!("{:.2}°", dim.measurement())
|
||||
}
|
||||
_ => format!("{:.4}", dim.measurement()),
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -306,16 +306,28 @@ fn entity_pts(e: &EntityType) -> Vec<[f32; 3]> {
|
|||
|
||||
/// Extract center, radius, start/end angle (radians), elevation from an arc.
|
||||
fn arc_geom(a: &ArcEnt) -> ([f64; 2], f64, f64, f64, f64) {
|
||||
([a.center.x, a.center.y], a.radius, a.start_angle, a.end_angle, a.center.z)
|
||||
(
|
||||
[a.center.x, a.center.y],
|
||||
a.radius,
|
||||
a.start_angle,
|
||||
a.end_angle,
|
||||
a.center.z,
|
||||
)
|
||||
}
|
||||
|
||||
/// Normalize angle to [0, 2π).
|
||||
fn norm_angle(a: f64) -> f64 { ((a % TAU) + TAU) % TAU }
|
||||
fn norm_angle(a: f64) -> f64 {
|
||||
((a % TAU) + TAU) % TAU
|
||||
}
|
||||
|
||||
/// Return the CCW angular span from `start` to `end`.
|
||||
fn arc_span(start: f64, end: f64) -> f64 {
|
||||
let s = (end - start).rem_euclid(TAU);
|
||||
if s < 1e-6 { TAU } else { s }
|
||||
if s < 1e-6 {
|
||||
TAU
|
||||
} else {
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
/// Project a pick point onto an arc: return the angle in radians.
|
||||
|
|
@ -328,9 +340,13 @@ fn arc_angle_at(center: [f64; 2], pt: [f64; 2]) -> f64 {
|
|||
fn clamp_angle_to_arc(a: f64, start: f64, end: f64) -> f64 {
|
||||
let span = arc_span(start, end);
|
||||
let rel = (a - start).rem_euclid(TAU);
|
||||
if rel <= span { a }
|
||||
else if rel < span + (TAU - span) / 2.0 { end }
|
||||
else { start }
|
||||
if rel <= span {
|
||||
a
|
||||
} else if rel < span + (TAU - span) / 2.0 {
|
||||
end
|
||||
} else {
|
||||
start
|
||||
}
|
||||
}
|
||||
|
||||
/// Trim an arc so it goes from `new_start` to `new_end` (both in radians).
|
||||
|
|
@ -338,7 +354,7 @@ fn trim_arc(orig: &ArcEnt, new_start: f64, new_end: f64) -> ArcEnt {
|
|||
let mut a = orig.clone();
|
||||
a.common.handle = Handle::NULL;
|
||||
a.start_angle = norm_angle(new_start);
|
||||
a.end_angle = norm_angle(new_end);
|
||||
a.end_angle = norm_angle(new_end);
|
||||
a
|
||||
}
|
||||
|
||||
|
|
@ -351,7 +367,9 @@ fn line_circle_ts(px: f64, py: f64, dx: f64, dy: f64, cx: f64, cy: f64, r: f64)
|
|||
let b = 2.0 * (fx * dx + fy * dy);
|
||||
let c = fx * fx + fy * fy - r * r;
|
||||
let disc = b * b - 4.0 * a * c;
|
||||
if disc < 0.0 { return vec![]; }
|
||||
if disc < 0.0 {
|
||||
return vec![];
|
||||
}
|
||||
let sq = disc.sqrt();
|
||||
if disc < 1e-14 {
|
||||
vec![(-b) / (2.0 * a)]
|
||||
|
|
@ -365,10 +383,14 @@ fn circle_circle_pts(c1: [f64; 2], r1: f64, c2: [f64; 2], r2: f64) -> Vec<[f64;
|
|||
let dx = c2[0] - c1[0];
|
||||
let dy = c2[1] - c1[1];
|
||||
let d = (dx * dx + dy * dy).sqrt();
|
||||
if d < 1e-12 || d > r1 + r2 + 1e-9 || d < (r1 - r2).abs() - 1e-9 { return vec![]; }
|
||||
if d < 1e-12 || d > r1 + r2 + 1e-9 || d < (r1 - r2).abs() - 1e-9 {
|
||||
return vec![];
|
||||
}
|
||||
let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
|
||||
let h2 = r1 * r1 - a * a;
|
||||
if h2 < 0.0 { return vec![]; }
|
||||
if h2 < 0.0 {
|
||||
return vec![];
|
||||
}
|
||||
let h = h2.sqrt();
|
||||
let mx = c1[0] + a * dx / d;
|
||||
let my = c1[1] + a * dy / d;
|
||||
|
|
@ -387,7 +409,11 @@ fn circle_circle_pts(c1: [f64; 2], r1: f64, c2: [f64; 2], r2: f64) -> Vec<[f64;
|
|||
/// Find the index of the LwPolyline segment nearest to `click` (DXF XY).
|
||||
fn lwpoly_nearest_seg(poly: &LwPolyline, click: [f64; 2]) -> usize {
|
||||
let n = poly.vertices.len();
|
||||
let seg_count = if poly.is_closed { n } else { n.saturating_sub(1) };
|
||||
let seg_count = if poly.is_closed {
|
||||
n
|
||||
} else {
|
||||
n.saturating_sub(1)
|
||||
};
|
||||
let mut best_idx = 0;
|
||||
let mut best_dist = f64::MAX;
|
||||
for i in 0..seg_count {
|
||||
|
|
@ -424,7 +450,7 @@ fn lwpoly_seg_as_line(poly: &LwPolyline, seg_idx: usize) -> LineEnt {
|
|||
l.common = poly.common.clone();
|
||||
l.common.handle = Handle::NULL;
|
||||
l.start = Vector3::new(v0.location.x, v0.location.y, poly.elevation);
|
||||
l.end = Vector3::new(v1.location.x, v1.location.y, poly.elevation);
|
||||
l.end = Vector3::new(v1.location.x, v1.location.y, poly.elevation);
|
||||
l
|
||||
}
|
||||
|
||||
|
|
@ -441,10 +467,10 @@ fn compute_bulge(t1: [f64; 2], t2: [f64; 2], arc_center: [f64; 2]) -> f64 {
|
|||
let to_c = [arc_center[0] - mid[0], arc_center[1] - mid[1]];
|
||||
let cross = chord[0] * to_c[1] - chord[1] * to_c[0];
|
||||
if cross >= 0.0 {
|
||||
(span_ccw / 4.0).tan() // CCW arc
|
||||
(span_ccw / 4.0).tan() // CCW arc
|
||||
} else {
|
||||
let span_cw = TAU - span_ccw;
|
||||
-(span_cw / 4.0).tan() // CW arc
|
||||
-(span_cw / 4.0).tan() // CW arc
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -503,7 +529,11 @@ fn lwpoly_shorten_seg(
|
|||
fn lwpoly_pts(poly: &LwPolyline) -> Vec<[f32; 3]> {
|
||||
let elev = poly.elevation as f32;
|
||||
let n = poly.vertices.len();
|
||||
let seg_count = if poly.is_closed { n } else { n.saturating_sub(1) };
|
||||
let seg_count = if poly.is_closed {
|
||||
n
|
||||
} else {
|
||||
n.saturating_sub(1)
|
||||
};
|
||||
let mut pts = Vec::with_capacity(seg_count * 2);
|
||||
for i in 0..seg_count {
|
||||
let v0 = &poly.vertices[i];
|
||||
|
|
@ -522,27 +552,35 @@ enum FilletEntity {
|
|||
Line(LineEnt),
|
||||
Arc(ArcEnt),
|
||||
/// A segment of an LwPolyline identified by its entity handle and segment index.
|
||||
LwPoly { poly: LwPolyline, handle: Handle, seg_idx: usize },
|
||||
LwPoly {
|
||||
poly: LwPolyline,
|
||||
handle: Handle,
|
||||
seg_idx: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl FilletEntity {
|
||||
fn from_entity(e: &EntityType) -> Option<Self> {
|
||||
match e {
|
||||
EntityType::Line(l) => Some(Self::Line(l.clone())),
|
||||
EntityType::Arc(a) => Some(Self::Arc(a.clone())),
|
||||
EntityType::Arc(a) => Some(Self::Arc(a.clone())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_lwpoly(poly: &LwPolyline, handle: Handle, click: [f64; 2]) -> Self {
|
||||
let seg_idx = lwpoly_nearest_seg(poly, click);
|
||||
Self::LwPoly { poly: poly.clone(), handle, seg_idx }
|
||||
Self::LwPoly {
|
||||
poly: poly.clone(),
|
||||
handle,
|
||||
seg_idx,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_entity_type(&self) -> EntityType {
|
||||
match self {
|
||||
Self::Line(l) => EntityType::Line(l.clone()),
|
||||
Self::Arc(a) => EntityType::Arc(a.clone()),
|
||||
Self::Arc(a) => EntityType::Arc(a.clone()),
|
||||
Self::LwPoly { poly, .. } => EntityType::LwPolyline(poly.clone()),
|
||||
}
|
||||
}
|
||||
|
|
@ -550,7 +588,7 @@ impl FilletEntity {
|
|||
fn elevation(&self) -> f64 {
|
||||
match self {
|
||||
Self::Line(l) => l.start.z,
|
||||
Self::Arc(a) => a.center.z,
|
||||
Self::Arc(a) => a.center.z,
|
||||
Self::LwPoly { poly, .. } => poly.elevation,
|
||||
}
|
||||
}
|
||||
|
|
@ -587,23 +625,35 @@ fn compute_fillet_entities(
|
|||
}
|
||||
// ── LwPoly × LwPoly (same entity — corner fillet) ─────────────────
|
||||
(
|
||||
FilletEntity::LwPoly { poly: p1, handle: h1, seg_idx: s1 },
|
||||
FilletEntity::LwPoly { handle: h2, seg_idx: s2, .. },
|
||||
FilletEntity::LwPoly {
|
||||
poly: p1,
|
||||
handle: h1,
|
||||
seg_idx: s1,
|
||||
},
|
||||
FilletEntity::LwPoly {
|
||||
handle: h2,
|
||||
seg_idx: s2,
|
||||
..
|
||||
},
|
||||
) if h1 == h2 => {
|
||||
// Adjacent segments share a corner vertex — fillet that corner.
|
||||
let (low, high) = if *s1 < *s2 { (*s1, *s2) } else { (*s2, *s1) };
|
||||
let n = p1.vertices.len();
|
||||
// Segments must be consecutive (high == low+1, or wrap-around on closed poly).
|
||||
let consecutive = high == low + 1
|
||||
|| (p1.is_closed && low == 0 && high == n.saturating_sub(1));
|
||||
let consecutive =
|
||||
high == low + 1 || (p1.is_closed && low == 0 && high == n.saturating_sub(1));
|
||||
if !consecutive {
|
||||
return None;
|
||||
}
|
||||
let corner_idx = high % n; // vertex shared by both segments
|
||||
let l1 = lwpoly_seg_as_line(p1, low);
|
||||
let l2 = lwpoly_seg_as_line(p1, high % n.max(1)); // seg after corner
|
||||
// Re-map click to whichever segment each was picked on.
|
||||
let (c1, c2) = if *s1 == low { (click1, click2) } else { (click2, click1) };
|
||||
// Re-map click to whichever segment each was picked on.
|
||||
let (c1, c2) = if *s1 == low {
|
||||
(click1, click2)
|
||||
} else {
|
||||
(click2, click1)
|
||||
};
|
||||
match compute_fillet(&l1, c1, &l2, c2, radius)? {
|
||||
(EntityType::Line(tl1), EntityType::Line(tl2), maybe_arc) => {
|
||||
let t1 = [tl1.end.x, tl1.end.y]; // trimmed end of seg before corner
|
||||
|
|
@ -624,8 +674,16 @@ fn compute_fillet_entities(
|
|||
}
|
||||
// ── LwPoly × LwPoly (different entities) ──────────────────────────
|
||||
(
|
||||
FilletEntity::LwPoly { poly: p1, seg_idx: s1, .. },
|
||||
FilletEntity::LwPoly { poly: p2, seg_idx: s2, .. },
|
||||
FilletEntity::LwPoly {
|
||||
poly: p1,
|
||||
seg_idx: s1,
|
||||
..
|
||||
},
|
||||
FilletEntity::LwPoly {
|
||||
poly: p2,
|
||||
seg_idx: s2,
|
||||
..
|
||||
},
|
||||
) => {
|
||||
let l1 = lwpoly_seg_as_line(p1, *s1);
|
||||
let l2 = lwpoly_seg_as_line(p2, *s2);
|
||||
|
|
@ -633,7 +691,11 @@ fn compute_fillet_entities(
|
|||
if let (EntityType::Line(tl1), EntityType::Line(tl2)) = (&tl1_e, &tl2_e) {
|
||||
let np1 = rebuild_poly_from_trimmed_line(p1, *s1, &l1, tl1);
|
||||
let np2 = rebuild_poly_from_trimmed_line(p2, *s2, &l2, tl2);
|
||||
Some((EntityType::LwPolyline(np1), EntityType::LwPolyline(np2), maybe_arc))
|
||||
Some((
|
||||
EntityType::LwPolyline(np1),
|
||||
EntityType::LwPolyline(np2),
|
||||
maybe_arc,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
@ -692,9 +754,17 @@ fn rebuild_poly_from_trimmed_line(
|
|||
trimmed: &LineEnt,
|
||||
) -> LwPolyline {
|
||||
let start_moved = (trimmed.start.x - orig.start.x).hypot(trimmed.start.y - orig.start.y) > 1e-9;
|
||||
let end_moved = (trimmed.end.x - orig.end.x ).hypot(trimmed.end.y - orig.end.y ) > 1e-9;
|
||||
let new_start = if start_moved { Some([trimmed.start.x, trimmed.start.y]) } else { None };
|
||||
let new_end = if end_moved { Some([trimmed.end.x, trimmed.end.y ]) } else { None };
|
||||
let end_moved = (trimmed.end.x - orig.end.x).hypot(trimmed.end.y - orig.end.y) > 1e-9;
|
||||
let new_start = if start_moved {
|
||||
Some([trimmed.start.x, trimmed.start.y])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let new_end = if end_moved {
|
||||
Some([trimmed.end.x, trimmed.end.y])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
lwpoly_shorten_seg(poly, seg_idx, new_start, new_end)
|
||||
}
|
||||
|
||||
|
|
@ -715,12 +785,13 @@ fn fillet_line_arc(
|
|||
|
||||
if radius < 1e-9 {
|
||||
// r=0: trim to intersection (nearest to each click)
|
||||
let t_best = ts.iter().copied()
|
||||
.min_by(|a, b| {
|
||||
let da = (p1[0]+a*u[0]-click_line[0]).powi(2)+(p1[1]+a*u[1]-click_line[1]).powi(2);
|
||||
let db = (p1[0]+b*u[0]-click_line[0]).powi(2)+(p1[1]+b*u[1]-click_line[1]).powi(2);
|
||||
da.partial_cmp(&db).unwrap()
|
||||
})?;
|
||||
let t_best = ts.iter().copied().min_by(|a, b| {
|
||||
let da = (p1[0] + a * u[0] - click_line[0]).powi(2)
|
||||
+ (p1[1] + a * u[1] - click_line[1]).powi(2);
|
||||
let db = (p1[0] + b * u[0] - click_line[0]).powi(2)
|
||||
+ (p1[1] + b * u[1] - click_line[1]).powi(2);
|
||||
da.partial_cmp(&db).unwrap()
|
||||
})?;
|
||||
let ix = p1[0] + t_best * u[0];
|
||||
let iy = p1[1] + t_best * u[1];
|
||||
|
||||
|
|
@ -749,7 +820,7 @@ fn fillet_line_arc(
|
|||
// and at distance |ar ± radius| from the arc center.
|
||||
// Sign: outside=ar+radius (external), inside=ar-radius (internal).
|
||||
let perp_x = -u[1];
|
||||
let perp_y = u[0];
|
||||
let perp_y = u[0];
|
||||
|
||||
let mut best: Option<(EntityType, EntityType, EntityType)> = None;
|
||||
let mut best_dist = f64::MAX;
|
||||
|
|
@ -759,7 +830,9 @@ fn fillet_line_arc(
|
|||
// Candidate fillet center offset from line by ±radius in perp direction.
|
||||
// Find point on offset line closest to arc center at distance |ar + sign*radius|.
|
||||
let off_dist = ar + sign_circle * radius;
|
||||
if off_dist < 1e-9 { continue; }
|
||||
if off_dist < 1e-9 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The fillet center is at distance `off_dist` from the arc center
|
||||
// and also at distance `radius` from the line (perpendicular).
|
||||
|
|
@ -769,38 +842,49 @@ fn fillet_line_arc(
|
|||
let qx = p1[0] + sign_perp * radius * perp_x - ac[0];
|
||||
let qy = p1[1] + sign_perp * radius * perp_y - ac[1];
|
||||
// (qx + t*u[0])^2 + (qy + t*u[1])^2 = off_dist^2
|
||||
let qa = u[0]*u[0] + u[1]*u[1]; // = 1.0
|
||||
let qb = 2.0*(qx*u[0] + qy*u[1]);
|
||||
let qc = qx*qx + qy*qy - off_dist*off_dist;
|
||||
let disc = qb*qb - 4.0*qa*qc;
|
||||
if disc < 0.0 { continue; }
|
||||
let qa = u[0] * u[0] + u[1] * u[1]; // = 1.0
|
||||
let qb = 2.0 * (qx * u[0] + qy * u[1]);
|
||||
let qc = qx * qx + qy * qy - off_dist * off_dist;
|
||||
let disc = qb * qb - 4.0 * qa * qc;
|
||||
if disc < 0.0 {
|
||||
continue;
|
||||
}
|
||||
let sq = disc.sqrt();
|
||||
for &sign_t in &[-1.0_f64, 1.0_f64] {
|
||||
let t_fc = (-qb + sign_t * sq) / (2.0 * qa);
|
||||
let fc = [p1[0] + t_fc*u[0] + sign_perp*radius*perp_x,
|
||||
p1[1] + t_fc*u[1] + sign_perp*radius*perp_y];
|
||||
let fc = [
|
||||
p1[0] + t_fc * u[0] + sign_perp * radius * perp_x,
|
||||
p1[1] + t_fc * u[1] + sign_perp * radius * perp_y,
|
||||
];
|
||||
|
||||
// Tangent point on the line
|
||||
let tp_line = [p1[0] + t_fc*u[0], p1[1] + t_fc*u[1]];
|
||||
let tp_line = [p1[0] + t_fc * u[0], p1[1] + t_fc * u[1]];
|
||||
// Tangent point on the arc circle
|
||||
let fd = [(ac[0] - fc[0]), (ac[1] - fc[1])];
|
||||
let fdl = (fd[0]*fd[0]+fd[1]*fd[1]).sqrt().max(1e-12);
|
||||
let tp_arc = [ac[0] + fd[0]/fdl * ar, ac[1] + fd[1]/fdl * ar];
|
||||
let fdl = (fd[0] * fd[0] + fd[1] * fd[1]).sqrt().max(1e-12);
|
||||
let tp_arc = [ac[0] + fd[0] / fdl * ar, ac[1] + fd[1] / fdl * ar];
|
||||
|
||||
// The tangent point on the arc must be within the arc's angular range
|
||||
let tp_arc_angle = arc_angle_at(ac, tp_arc);
|
||||
let tp_arc_clamped = clamp_angle_to_arc(tp_arc_angle, a_start, a_end);
|
||||
if (norm_angle(tp_arc_angle) - norm_angle(tp_arc_clamped)).abs() > 0.01 { continue; }
|
||||
if (norm_angle(tp_arc_angle) - norm_angle(tp_arc_clamped)).abs() > 0.01 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The tangent point on the line must be on the correct side of the click
|
||||
// (prefer the intersection closest to the click)
|
||||
let dist_to_click_line = (tp_line[0]-click_line[0]).hypot(tp_line[1]-click_line[1]);
|
||||
let dist_to_click_arc = (tp_arc[0]-click_arc[0]).hypot(tp_arc[1]-click_arc[1]);
|
||||
let dist_to_click_line =
|
||||
(tp_line[0] - click_line[0]).hypot(tp_line[1] - click_line[1]);
|
||||
let dist_to_click_arc = (tp_arc[0] - click_arc[0]).hypot(tp_arc[1] - click_arc[1]);
|
||||
let dist_total = dist_to_click_line + dist_to_click_arc;
|
||||
if dist_total >= best_dist { continue; }
|
||||
if dist_total >= best_dist {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build trimmed line
|
||||
let Some(new_line) = trim_line_to_point(line, tp_line, click_line) else { continue };
|
||||
let Some(new_line) = trim_line_to_point(line, tp_line, click_line) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Build trimmed arc
|
||||
let arc_click_angle = arc_angle_at(ac, click_arc);
|
||||
|
|
@ -814,19 +898,28 @@ fn fillet_line_arc(
|
|||
|
||||
// Build fillet arc angles
|
||||
let fa_line = arc_angle_at(fc, tp_line);
|
||||
let fa_arc = arc_angle_at(fc, tp_arc);
|
||||
let cross = (tp_line[0]-fc[0])*(tp_arc[1]-fc[1]) - (tp_line[1]-fc[1])*(tp_arc[0]-fc[0]);
|
||||
let (fstart, fend) = if cross >= 0.0 { (fa_line, fa_arc) } else { (fa_arc, fa_line) };
|
||||
let fa_arc = arc_angle_at(fc, tp_arc);
|
||||
let cross = (tp_line[0] - fc[0]) * (tp_arc[1] - fc[1])
|
||||
- (tp_line[1] - fc[1]) * (tp_arc[0] - fc[0]);
|
||||
let (fstart, fend) = if cross >= 0.0 {
|
||||
(fa_line, fa_arc)
|
||||
} else {
|
||||
(fa_arc, fa_line)
|
||||
};
|
||||
let mut fillet_arc = ArcEnt::new();
|
||||
fillet_arc.common = line.common.clone();
|
||||
fillet_arc.common.handle = Handle::NULL;
|
||||
fillet_arc.center = Vector3::new(fc[0], fc[1], z);
|
||||
fillet_arc.radius = radius;
|
||||
fillet_arc.start_angle = norm_angle(fstart);
|
||||
fillet_arc.end_angle = norm_angle(fend);
|
||||
fillet_arc.end_angle = norm_angle(fend);
|
||||
|
||||
best_dist = dist_total;
|
||||
best = Some((EntityType::Line(new_line), EntityType::Arc(new_arc), EntityType::Arc(fillet_arc)));
|
||||
best = Some((
|
||||
EntityType::Line(new_line),
|
||||
EntityType::Arc(new_arc),
|
||||
EntityType::Arc(fillet_arc),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -849,12 +942,17 @@ fn fillet_arc_arc(
|
|||
if radius < 1e-9 {
|
||||
// r=0: trim both arcs to their intersection point
|
||||
let pts = circle_circle_pts(c1, r1, c2, r2);
|
||||
if pts.is_empty() { return None; }
|
||||
if pts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
// Pick the intersection point nearest to the average of the two clicks
|
||||
let cx = (click1[0] + click2[0]) / 2.0;
|
||||
let cy = (click1[1] + click2[1]) / 2.0;
|
||||
let ip = *pts.iter().min_by(|a, b| {
|
||||
(a[0]-cx).hypot(a[1]-cy).partial_cmp(&(b[0]-cx).hypot(b[1]-cy)).unwrap()
|
||||
(a[0] - cx)
|
||||
.hypot(a[1] - cy)
|
||||
.partial_cmp(&(b[0] - cx).hypot(b[1] - cy))
|
||||
.unwrap()
|
||||
})?;
|
||||
|
||||
let ia1 = arc_angle_at(c1, ip);
|
||||
|
|
@ -885,31 +983,41 @@ fn fillet_arc_arc(
|
|||
|
||||
for sign1 in &[-1.0_f64, 1.0_f64] {
|
||||
let d1 = r1 + sign1 * radius;
|
||||
if d1 < 1e-9 { continue; }
|
||||
if d1 < 1e-9 {
|
||||
continue;
|
||||
}
|
||||
for sign2 in &[-1.0_f64, 1.0_f64] {
|
||||
let d2 = r2 + sign2 * radius;
|
||||
if d2 < 1e-9 { continue; }
|
||||
if d2 < 1e-9 {
|
||||
continue;
|
||||
}
|
||||
for fc in circle_circle_pts(c1, d1, c2, d2) {
|
||||
// Tangent points on each arc
|
||||
let fd1 = [(c1[0]-fc[0]), (c1[1]-fc[1])];
|
||||
let fdl1 = (fd1[0]*fd1[0]+fd1[1]*fd1[1]).sqrt().max(1e-12);
|
||||
let tp1 = [c1[0]+fd1[0]/fdl1*r1, c1[1]+fd1[1]/fdl1*r1];
|
||||
let fd1 = [(c1[0] - fc[0]), (c1[1] - fc[1])];
|
||||
let fdl1 = (fd1[0] * fd1[0] + fd1[1] * fd1[1]).sqrt().max(1e-12);
|
||||
let tp1 = [c1[0] + fd1[0] / fdl1 * r1, c1[1] + fd1[1] / fdl1 * r1];
|
||||
|
||||
let fd2 = [(c2[0]-fc[0]), (c2[1]-fc[1])];
|
||||
let fdl2 = (fd2[0]*fd2[0]+fd2[1]*fd2[1]).sqrt().max(1e-12);
|
||||
let tp2 = [c2[0]+fd2[0]/fdl2*r2, c2[1]+fd2[1]/fdl2*r2];
|
||||
let fd2 = [(c2[0] - fc[0]), (c2[1] - fc[1])];
|
||||
let fdl2 = (fd2[0] * fd2[0] + fd2[1] * fd2[1]).sqrt().max(1e-12);
|
||||
let tp2 = [c2[0] + fd2[0] / fdl2 * r2, c2[1] + fd2[1] / fdl2 * r2];
|
||||
|
||||
// Tangent points must lie within respective arc ranges
|
||||
let tp1a = arc_angle_at(c1, tp1);
|
||||
let tp2a = arc_angle_at(c2, tp2);
|
||||
let tc1 = clamp_angle_to_arc(tp1a, s1, e1);
|
||||
let tc2 = clamp_angle_to_arc(tp2a, s2, e2);
|
||||
if (norm_angle(tp1a) - norm_angle(tc1)).abs() > 0.01 { continue; }
|
||||
if (norm_angle(tp2a) - norm_angle(tc2)).abs() > 0.01 { continue; }
|
||||
if (norm_angle(tp1a) - norm_angle(tc1)).abs() > 0.01 {
|
||||
continue;
|
||||
}
|
||||
if (norm_angle(tp2a) - norm_angle(tc2)).abs() > 0.01 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let dist_total = (tp1[0]-click1[0]).hypot(tp1[1]-click1[1])
|
||||
+ (tp2[0]-click2[0]).hypot(tp2[1]-click2[1]);
|
||||
if dist_total >= best_dist { continue; }
|
||||
let dist_total = (tp1[0] - click1[0]).hypot(tp1[1] - click1[1])
|
||||
+ (tp2[0] - click2[0]).hypot(tp2[1] - click2[1]);
|
||||
if dist_total >= best_dist {
|
||||
continue;
|
||||
}
|
||||
|
||||
let ca1 = clamp_angle_to_arc(arc_angle_at(c1, click1), s1, e1);
|
||||
let ca2 = clamp_angle_to_arc(arc_angle_at(c2, click2), s2, e2);
|
||||
|
|
@ -927,7 +1035,8 @@ fn fillet_arc_arc(
|
|||
|
||||
let fa1 = arc_angle_at(fc, tp1);
|
||||
let fa2 = arc_angle_at(fc, tp2);
|
||||
let cross = (tp1[0]-fc[0])*(tp2[1]-fc[1]) - (tp1[1]-fc[1])*(tp2[0]-fc[0]);
|
||||
let cross =
|
||||
(tp1[0] - fc[0]) * (tp2[1] - fc[1]) - (tp1[1] - fc[1]) * (tp2[0] - fc[0]);
|
||||
let (fstart, fend) = if cross >= 0.0 { (fa1, fa2) } else { (fa2, fa1) };
|
||||
let mut fillet_arc = ArcEnt::new();
|
||||
fillet_arc.common = a1.common.clone();
|
||||
|
|
@ -935,10 +1044,14 @@ fn fillet_arc_arc(
|
|||
fillet_arc.center = Vector3::new(fc[0], fc[1], z);
|
||||
fillet_arc.radius = radius;
|
||||
fillet_arc.start_angle = norm_angle(fstart);
|
||||
fillet_arc.end_angle = norm_angle(fend);
|
||||
fillet_arc.end_angle = norm_angle(fend);
|
||||
|
||||
best_dist = dist_total;
|
||||
best = Some((EntityType::Arc(new_a1), EntityType::Arc(new_a2), EntityType::Arc(fillet_arc)));
|
||||
best = Some((
|
||||
EntityType::Arc(new_a1),
|
||||
EntityType::Arc(new_a2),
|
||||
EntityType::Arc(fillet_arc),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -951,16 +1064,18 @@ fn trim_line_to_point(line: &LineEnt, isect: [f64; 2], click: [f64; 2]) -> Optio
|
|||
let mut l = line.clone();
|
||||
l.common.handle = Handle::NULL;
|
||||
let (p1, _, u, len) = line_geom(line);
|
||||
if len < 1e-12 { return None; }
|
||||
if len < 1e-12 {
|
||||
return None;
|
||||
}
|
||||
// Parameter of intersection
|
||||
let t_i = (isect[0]-p1[0])*u[0] + (isect[1]-p1[1])*u[1];
|
||||
let t_i = (isect[0] - p1[0]) * u[0] + (isect[1] - p1[1]) * u[1];
|
||||
// Parameter of click
|
||||
let t_c = (click[0]-p1[0])*u[0] + (click[1]-p1[1])*u[1];
|
||||
let t_c = (click[0] - p1[0]) * u[0] + (click[1] - p1[1]) * u[1];
|
||||
// If click is past intersection on the + side, keep intersection..end
|
||||
if t_c >= t_i {
|
||||
l.start = Vector3::new(isect[0], isect[1], line.start.z);
|
||||
} else {
|
||||
l.end = Vector3::new(isect[0], isect[1], line.end.z);
|
||||
l.end = Vector3::new(isect[0], isect[1], line.end.z);
|
||||
}
|
||||
Some(l)
|
||||
}
|
||||
|
|
@ -1060,12 +1175,14 @@ impl CadCommand for FilletCommand {
|
|||
"FILLET Select first object (Line/Arc/LwPolyline) [R={:.4} | type R to change]:",
|
||||
self.radius
|
||||
),
|
||||
FilletStep::WaitingForRadius => format!(
|
||||
"FILLET Enter fillet radius <{:.4}>:",
|
||||
self.radius
|
||||
),
|
||||
FilletStep::WaitingForRadius => {
|
||||
format!("FILLET Enter fillet radius <{:.4}>:", self.radius)
|
||||
}
|
||||
FilletStep::Second { .. } => {
|
||||
format!("FILLET Select second object (Line/Arc/LwPolyline) [R={:.4}]:", self.radius)
|
||||
format!(
|
||||
"FILLET Select second object (Line/Arc/LwPolyline) [R={:.4}]:",
|
||||
self.radius
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1145,7 +1262,11 @@ impl CadCommand for FilletCommand {
|
|||
other => FilletEntity::from_entity(other),
|
||||
});
|
||||
if let Some(e) = e1 {
|
||||
self.step = FilletStep::Second { h1: handle, e1: e, click1: click };
|
||||
self.step = FilletStep::Second {
|
||||
h1: handle,
|
||||
e1: e,
|
||||
click1: click,
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
} else {
|
||||
CmdResult::NeedPoint
|
||||
|
|
@ -1177,7 +1298,9 @@ impl CadCommand for FilletCommand {
|
|||
match compute_fillet_entities(&e1, click1, &e2, click, self.radius) {
|
||||
Some((new_e1, new_e2, maybe_arc)) => {
|
||||
let mut additions = vec![];
|
||||
if let Some(arc) = maybe_arc { additions.push(arc); }
|
||||
if let Some(arc) = maybe_arc {
|
||||
additions.push(arc);
|
||||
}
|
||||
if same_entity {
|
||||
// Corner fillet: both results are the same rebuilt poly.
|
||||
CmdResult::ReplaceMany(vec![(h1, vec![new_e1])], additions)
|
||||
|
|
@ -1214,7 +1337,12 @@ impl CadCommand for FilletCommand {
|
|||
FilletEntity::from_entity(e).map(|fe| entity_pts(&fe.to_entity_type()))
|
||||
});
|
||||
if let Some(pts) = pts {
|
||||
vec![WireModel::solid("fillet_hover".into(), pts, WireModel::CYAN, false)]
|
||||
vec![WireModel::solid(
|
||||
"fillet_hover".into(),
|
||||
pts,
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
)]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
|
|
@ -1240,11 +1368,26 @@ impl CadCommand for FilletCommand {
|
|||
compute_fillet_entities(&e1, click1, &e2, click, self.radius)
|
||||
{
|
||||
let mut out = vec![
|
||||
WireModel::solid("fillet_e1".into(), entity_pts(&new_e1), WireModel::CYAN, false),
|
||||
WireModel::solid("fillet_e2".into(), entity_pts(&new_e2), WireModel::CYAN, false),
|
||||
WireModel::solid(
|
||||
"fillet_e1".into(),
|
||||
entity_pts(&new_e1),
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
),
|
||||
WireModel::solid(
|
||||
"fillet_e2".into(),
|
||||
entity_pts(&new_e2),
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
),
|
||||
];
|
||||
if let Some(arc) = maybe_arc {
|
||||
out.push(WireModel::solid("fillet_arc".into(), entity_pts(&arc), WireModel::CYAN, false));
|
||||
out.push(WireModel::solid(
|
||||
"fillet_arc".into(),
|
||||
entity_pts(&arc),
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
|
@ -1309,10 +1452,9 @@ impl CadCommand for ChamferCommand {
|
|||
"CHAMFER Select first line [D1={:.4} D2={:.4} | type D to change]:",
|
||||
self.dist1, self.dist2
|
||||
),
|
||||
ChamferStep::WaitingForDist1 => format!(
|
||||
"CHAMFER Enter first chamfer distance <{:.4}>:",
|
||||
self.dist1
|
||||
),
|
||||
ChamferStep::WaitingForDist1 => {
|
||||
format!("CHAMFER Enter first chamfer distance <{:.4}>:", self.dist1)
|
||||
}
|
||||
ChamferStep::WaitingForDist2 => format!(
|
||||
"CHAMFER Enter second chamfer distance <{:.4}>:",
|
||||
self.dist2
|
||||
|
|
|
|||
|
|
@ -22,18 +22,28 @@ pub struct JoinCommand {
|
|||
|
||||
impl JoinCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { handles: vec![], gathering: true }
|
||||
Self {
|
||||
handles: vec![],
|
||||
gathering: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for JoinCommand {
|
||||
fn name(&self) -> &'static str { "JOIN" }
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
format!("JOIN Select objects to join ({} selected, Enter to apply):", self.handles.len())
|
||||
fn name(&self) -> &'static str {
|
||||
"JOIN"
|
||||
}
|
||||
|
||||
fn is_selection_gathering(&self) -> bool { self.gathering }
|
||||
fn prompt(&self) -> String {
|
||||
format!(
|
||||
"JOIN Select objects to join ({} selected, Enter to apply):",
|
||||
self.handles.len()
|
||||
)
|
||||
}
|
||||
|
||||
fn is_selection_gathering(&self) -> bool {
|
||||
self.gathering
|
||||
}
|
||||
|
||||
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
|
||||
self.handles = handles;
|
||||
|
|
@ -63,10 +73,12 @@ pub fn join_entities(entities: &[(Handle, &EntityType)]) -> Option<(Vec<Handle>,
|
|||
}
|
||||
|
||||
// Split into lines and arcs
|
||||
let lines: Vec<_> = entities.iter()
|
||||
let lines: Vec<_> = entities
|
||||
.iter()
|
||||
.filter(|(_, e)| matches!(e, EntityType::Line(_)))
|
||||
.collect();
|
||||
let arcs: Vec<_> = entities.iter()
|
||||
let arcs: Vec<_> = entities
|
||||
.iter()
|
||||
.filter(|(_, e)| matches!(e, EntityType::Arc(_)))
|
||||
.collect();
|
||||
|
||||
|
|
@ -81,93 +93,123 @@ pub fn join_entities(entities: &[(Handle, &EntityType)]) -> Option<(Vec<Handle>,
|
|||
|
||||
fn try_join_lines(lines: &[&(Handle, &EntityType)]) -> Option<(Vec<Handle>, Vec<EntityType>)> {
|
||||
// Collect endpoints in XZ plane
|
||||
let segs: Vec<_> = lines.iter().map(|(h, e)| {
|
||||
if let EntityType::Line(l) = e {
|
||||
let s = Vec3::new(l.start.x as f32, l.start.z as f32, 0.0);
|
||||
let e2 = Vec3::new(l.end.x as f32, l.end.z as f32, 0.0);
|
||||
(*h, l, s, e2)
|
||||
} else { unreachable!() }
|
||||
}).collect();
|
||||
let segs: Vec<_> = lines
|
||||
.iter()
|
||||
.map(|(h, e)| {
|
||||
if let EntityType::Line(l) = e {
|
||||
let s = Vec3::new(l.start.x as f32, l.start.z as f32, 0.0);
|
||||
let e2 = Vec3::new(l.end.x as f32, l.end.z as f32, 0.0);
|
||||
(*h, l, s, e2)
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Check collinearity: all lines must be parallel and on the same infinite line.
|
||||
let (_, first_line, s0, e0) = segs[0];
|
||||
let dir0 = (e0 - s0).normalize_or_zero();
|
||||
if dir0.length_squared() < 1e-12 { return None; }
|
||||
if dir0.length_squared() < 1e-12 {
|
||||
return None;
|
||||
}
|
||||
|
||||
for (_, _, si, ei) in &segs[1..] {
|
||||
// Check parallel
|
||||
let diri = (*ei - *si).normalize_or_zero();
|
||||
let cross = (dir0.x * diri.y - dir0.y * diri.x).abs();
|
||||
if cross > 1e-4 { return None; }
|
||||
if cross > 1e-4 {
|
||||
return None;
|
||||
}
|
||||
// Check co-linear (point on same line)
|
||||
let off = *si - s0;
|
||||
let perp = (off - dir0 * off.dot(dir0)).length();
|
||||
if perp > 1e-3 { return None; }
|
||||
if perp > 1e-3 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
// Project all endpoints onto the direction axis
|
||||
let params: Vec<f32> = segs.iter().flat_map(|(_, _, si, ei)| {
|
||||
[si.dot(dir0), ei.dot(dir0)]
|
||||
}).collect();
|
||||
let params: Vec<f32> = segs
|
||||
.iter()
|
||||
.flat_map(|(_, _, si, ei)| [si.dot(dir0), ei.dot(dir0)])
|
||||
.collect();
|
||||
let t_min = params.iter().cloned().fold(f32::INFINITY, f32::min);
|
||||
let t_max = params.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
|
||||
|
||||
// Check connectivity: no gaps larger than tolerance
|
||||
let mut sorted: Vec<[f32; 2]> = segs.iter().map(|(_, _, si, ei)| {
|
||||
let ta = si.dot(dir0);
|
||||
let tb = ei.dot(dir0);
|
||||
[ta.min(tb), ta.max(tb)]
|
||||
}).collect();
|
||||
let mut sorted: Vec<[f32; 2]> = segs
|
||||
.iter()
|
||||
.map(|(_, _, si, ei)| {
|
||||
let ta = si.dot(dir0);
|
||||
let tb = ei.dot(dir0);
|
||||
[ta.min(tb), ta.max(tb)]
|
||||
})
|
||||
.collect();
|
||||
sorted.sort_by(|a, b| a[0].partial_cmp(&b[0]).unwrap());
|
||||
for w in sorted.windows(2) {
|
||||
if w[1][0] > w[0][1] + 1e-3 { return None; } // gap
|
||||
if w[1][0] > w[0][1] + 1e-3 {
|
||||
return None;
|
||||
} // gap
|
||||
}
|
||||
|
||||
let new_start = s0 + dir0 * t_min;
|
||||
let new_end = s0 + dir0 * t_max;
|
||||
let new_end = s0 + dir0 * t_max;
|
||||
|
||||
let mut merged = first_line.clone();
|
||||
merged.common.handle = Handle::NULL;
|
||||
merged.start = vec3_xz_to_v3(new_start, first_line.start.z);
|
||||
merged.end = vec3_xz_to_v3(new_end, first_line.start.z);
|
||||
merged.end = vec3_xz_to_v3(new_end, first_line.start.z);
|
||||
|
||||
let handles: Vec<Handle> = lines.iter().map(|(h, _)| *h).collect();
|
||||
Some((handles, vec![EntityType::Line(merged)]))
|
||||
}
|
||||
|
||||
fn try_join_arcs(arcs: &[&(Handle, &EntityType)]) -> Option<(Vec<Handle>, Vec<EntityType>)> {
|
||||
let segs: Vec<_> = arcs.iter().map(|(h, e)| {
|
||||
if let EntityType::Arc(a) = e { (*h, a) }
|
||||
else { unreachable!() }
|
||||
}).collect();
|
||||
let segs: Vec<_> = arcs
|
||||
.iter()
|
||||
.map(|(h, e)| {
|
||||
if let EntityType::Arc(a) = e {
|
||||
(*h, a)
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// All arcs must share the same center and radius
|
||||
let (_, first) = segs[0];
|
||||
let cx = first.center.x;
|
||||
let cy = first.center.y;
|
||||
let r = first.radius;
|
||||
let r = first.radius;
|
||||
|
||||
for (_, a) in &segs[1..] {
|
||||
if (a.center.x - cx).abs() > 1e-3 || (a.center.y - cy).abs() > 1e-3 { return None; }
|
||||
if (a.radius - r).abs() > 1e-3 { return None; }
|
||||
if (a.center.x - cx).abs() > 1e-3 || (a.center.y - cy).abs() > 1e-3 {
|
||||
return None;
|
||||
}
|
||||
if (a.radius - r).abs() > 1e-3 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect (start_angle, end_angle) in radians.
|
||||
let mut intervals: Vec<[f32; 2]> = segs.iter().map(|(_, a)| {
|
||||
[a.start_angle as f32, a.end_angle as f32]
|
||||
}).collect();
|
||||
let mut intervals: Vec<[f32; 2]> = segs
|
||||
.iter()
|
||||
.map(|(_, a)| [a.start_angle as f32, a.end_angle as f32])
|
||||
.collect();
|
||||
|
||||
// Sort by start angle
|
||||
intervals.sort_by(|a, b| a[0].partial_cmp(&b[0]).unwrap());
|
||||
|
||||
// Try to merge into one contiguous arc
|
||||
let merged_start = intervals[0][0];
|
||||
let mut merged_end = intervals[0][1];
|
||||
let mut merged_end = intervals[0][1];
|
||||
for &[s, e] in &intervals[1..] {
|
||||
let span = (e - merged_end).rem_euclid(std::f32::consts::TAU);
|
||||
let gap = (s - merged_end).rem_euclid(std::f32::consts::TAU);
|
||||
let gap = (s - merged_end).rem_euclid(std::f32::consts::TAU);
|
||||
let _ = span;
|
||||
if gap > 1e-3 { return None; } // discontinuous
|
||||
if gap > 1e-3 {
|
||||
return None;
|
||||
} // discontinuous
|
||||
merged_end = e;
|
||||
}
|
||||
|
||||
|
|
@ -188,7 +230,7 @@ fn try_join_arcs(arcs: &[&(Handle, &EntityType)]) -> Option<(Vec<Handle>, Vec<En
|
|||
let mut merged_arc = first.clone();
|
||||
merged_arc.common.handle = Handle::NULL;
|
||||
merged_arc.start_angle = merged_start as f64;
|
||||
merged_arc.end_angle = merged_end as f64;
|
||||
merged_arc.end_angle = merged_end as f64;
|
||||
Some((handles, vec![EntityType::Arc(merged_arc)]))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,12 +7,14 @@
|
|||
//
|
||||
// The entity is modified at whichever end is closest to the pick point.
|
||||
|
||||
use acadrust::entities::{Arc as ArcEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline, Spline as SplineEnt};
|
||||
use truck_modeling::base::{BoundedCurve, Cut};
|
||||
use crate::modules::home::modify::spline_ops::{bspline_to_spline, spline_to_bspline};
|
||||
use acadrust::entities::{
|
||||
Arc as ArcEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline, Spline as SplineEnt,
|
||||
};
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::{EntityType, Handle};
|
||||
use glam::Vec3;
|
||||
use truck_modeling::base::{BoundedCurve, Cut};
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
|
||||
|
|
@ -27,18 +29,23 @@ enum LenState {
|
|||
|
||||
impl LengthenCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { state: LenState::PickEntity }
|
||||
Self {
|
||||
state: LenState::PickEntity,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for LengthenCommand {
|
||||
fn name(&self) -> &'static str { "LENGTHEN" }
|
||||
fn name(&self) -> &'static str {
|
||||
"LENGTHEN"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.state {
|
||||
LenState::PickEntity => "LENGTHEN Select object:".into(),
|
||||
LenState::PickOption { .. } =>
|
||||
"LENGTHEN Enter option [DE <delta> / TO <total> / P <pct>]:".into(),
|
||||
LenState::PickOption { .. } => {
|
||||
"LENGTHEN Enter option [DE <delta> / TO <total> / P <pct>]:".into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -47,8 +54,13 @@ impl CadCommand for LengthenCommand {
|
|||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, pt: Vec3) -> CmdResult {
|
||||
if handle.is_null() { return CmdResult::NeedPoint; }
|
||||
self.state = LenState::PickOption { handle, pick_pt: pt };
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.state = LenState::PickOption {
|
||||
handle,
|
||||
pick_pt: pt,
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
|
|
@ -65,22 +77,52 @@ impl CadCommand for LengthenCommand {
|
|||
let text = text.trim().to_uppercase();
|
||||
if let Some(rest) = text.strip_prefix("DE ").or_else(|| text.strip_prefix("DE")) {
|
||||
let delta: f64 = rest.trim().replace(',', ".").parse().ok()?;
|
||||
Some(CmdResult::LengthenEntity { handle, pick_pt, mode: LenMode::Delta(delta) })
|
||||
Some(CmdResult::LengthenEntity {
|
||||
handle,
|
||||
pick_pt,
|
||||
mode: LenMode::Delta(delta),
|
||||
})
|
||||
} else if let Some(rest) = text.strip_prefix("TO ").or_else(|| text.strip_prefix("TO")) {
|
||||
let total: f64 = rest.trim().replace(',', ".").parse().ok().filter(|&v: &f64| v > 0.0)?;
|
||||
Some(CmdResult::LengthenEntity { handle, pick_pt, mode: LenMode::Total(total) })
|
||||
let total: f64 = rest
|
||||
.trim()
|
||||
.replace(',', ".")
|
||||
.parse()
|
||||
.ok()
|
||||
.filter(|&v: &f64| v > 0.0)?;
|
||||
Some(CmdResult::LengthenEntity {
|
||||
handle,
|
||||
pick_pt,
|
||||
mode: LenMode::Total(total),
|
||||
})
|
||||
} else if let Some(rest) = text.strip_prefix("P ").or_else(|| text.strip_prefix("P")) {
|
||||
let pct: f64 = rest.trim().replace(',', ".").parse().ok().filter(|&v: &f64| v > 0.0)?;
|
||||
Some(CmdResult::LengthenEntity { handle, pick_pt, mode: LenMode::Percent(pct) })
|
||||
let pct: f64 = rest
|
||||
.trim()
|
||||
.replace(',', ".")
|
||||
.parse()
|
||||
.ok()
|
||||
.filter(|&v: &f64| v > 0.0)?;
|
||||
Some(CmdResult::LengthenEntity {
|
||||
handle,
|
||||
pick_pt,
|
||||
mode: LenMode::Percent(pct),
|
||||
})
|
||||
} else {
|
||||
// Try plain number as delta
|
||||
let delta: f64 = text.replace(',', ".").parse().ok()?;
|
||||
Some(CmdResult::LengthenEntity { handle, pick_pt, mode: LenMode::Delta(delta) })
|
||||
Some(CmdResult::LengthenEntity {
|
||||
handle,
|
||||
pick_pt,
|
||||
mode: LenMode::Delta(delta),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult { CmdResult::NeedPoint }
|
||||
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mode enum (also used in CmdResult) ────────────────────────────────────
|
||||
|
|
@ -98,10 +140,10 @@ pub enum LenMode {
|
|||
/// `pick_pt` determines which end to extend/trim (closest end is modified).
|
||||
pub fn lengthen_entity(entity: &EntityType, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
|
||||
match entity {
|
||||
EntityType::Line(l) => lengthen_line(l, pick_pt, mode),
|
||||
EntityType::Arc(a) => lengthen_arc(a, pick_pt, mode),
|
||||
EntityType::Ellipse(e) => lengthen_ellipse(e, pick_pt, mode),
|
||||
EntityType::Spline(s) => lengthen_spline(s, pick_pt, mode),
|
||||
EntityType::Line(l) => lengthen_line(l, pick_pt, mode),
|
||||
EntityType::Arc(a) => lengthen_arc(a, pick_pt, mode),
|
||||
EntityType::Ellipse(e) => lengthen_ellipse(e, pick_pt, mode),
|
||||
EntityType::Spline(s) => lengthen_spline(s, pick_pt, mode),
|
||||
EntityType::LwPolyline(p) => lengthen_lwpoly(p, pick_pt, mode),
|
||||
_ => None,
|
||||
}
|
||||
|
|
@ -109,20 +151,24 @@ pub fn lengthen_entity(entity: &EntityType, pick_pt: Vec3, mode: &LenMode) -> Op
|
|||
|
||||
fn lengthen_line(line: &LineEnt, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
|
||||
let s = Vec3::new(line.start.x as f32, line.start.z as f32, 0.0);
|
||||
let e = Vec3::new(line.end.x as f32, line.end.z as f32, 0.0);
|
||||
let e = Vec3::new(line.end.x as f32, line.end.z as f32, 0.0);
|
||||
let p = Vec3::new(pick_pt.x, pick_pt.z, 0.0);
|
||||
|
||||
let current_len = (e - s).length() as f64;
|
||||
if current_len < 1e-10 { return None; }
|
||||
if current_len < 1e-10 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let new_len = apply_mode(current_len, mode)?;
|
||||
if new_len < 1e-10 { return None; }
|
||||
if new_len < 1e-10 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let dir = (e - s) / current_len as f32;
|
||||
|
||||
// Which end is closer to pick?
|
||||
let dist_to_start = (p - s).length();
|
||||
let dist_to_end = (p - e).length();
|
||||
let dist_to_end = (p - e).length();
|
||||
|
||||
let mut result = line.clone();
|
||||
result.common.handle = Handle::NULL;
|
||||
|
|
@ -148,12 +194,14 @@ fn lengthen_arc(arc: &ArcEnt, pick_pt: Vec3, mode: &LenMode) -> Option<EntityTyp
|
|||
let current_arc_len = arc.radius * span;
|
||||
|
||||
let new_arc_len = apply_mode(current_arc_len, mode)?;
|
||||
if new_arc_len < 1e-10 { return None; }
|
||||
if new_arc_len < 1e-10 {
|
||||
return None;
|
||||
}
|
||||
let new_span = new_arc_len / arc.radius;
|
||||
|
||||
// Which end (start or end angle) is closer to pick?
|
||||
let start_rad = arc.start_angle;
|
||||
let end_rad = arc.end_angle;
|
||||
let end_rad = arc.end_angle;
|
||||
|
||||
let start_pt = Vec3::new(
|
||||
cx + arc.radius as f32 * start_rad.cos() as f32,
|
||||
|
|
@ -166,7 +214,7 @@ fn lengthen_arc(arc: &ArcEnt, pick_pt: Vec3, mode: &LenMode) -> Option<EntityTyp
|
|||
cy + arc.radius as f32 * end_rad.sin() as f32,
|
||||
);
|
||||
let dist_start = (pick_pt - start_pt).length();
|
||||
let dist_end = (pick_pt - end_pt).length();
|
||||
let dist_end = (pick_pt - end_pt).length();
|
||||
|
||||
let delta_span = new_span - span;
|
||||
|
||||
|
|
@ -186,14 +234,18 @@ fn lengthen_arc(arc: &ArcEnt, pick_pt: Vec3, mode: &LenMode) -> Option<EntityTyp
|
|||
|
||||
fn lengthen_ellipse(ell: &EllipseEnt, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
|
||||
let a = (ell.major_axis.x.powi(2) + ell.major_axis.y.powi(2)).sqrt();
|
||||
if a < 1e-9 { return None; }
|
||||
if a < 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let b = a * ell.minor_axis_ratio;
|
||||
let nx = ell.major_axis.x / a;
|
||||
let ny = ell.major_axis.y / a;
|
||||
|
||||
let t0 = ell.start_parameter;
|
||||
let mut t1 = ell.end_parameter;
|
||||
if t1 <= t0 { t1 += std::f64::consts::TAU; }
|
||||
if t1 <= t0 {
|
||||
t1 += std::f64::consts::TAU;
|
||||
}
|
||||
let span = t1 - t0;
|
||||
|
||||
// Approximate arc length via 128-point Gaussian quadrature estimate.
|
||||
|
|
@ -213,10 +265,14 @@ fn lengthen_ellipse(ell: &EllipseEnt, pick_pt: Vec3, mode: &LenMode) -> Option<E
|
|||
};
|
||||
|
||||
let current_len = arc_len_approx(span);
|
||||
if current_len < 1e-10 { return None; }
|
||||
if current_len < 1e-10 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let new_len = apply_mode(current_len, mode)?;
|
||||
if new_len < 1e-10 { return None; }
|
||||
if new_len < 1e-10 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Find the new span via bisection so that arc_len_approx(new_span) ≈ new_len.
|
||||
let max_span = std::f64::consts::TAU;
|
||||
|
|
@ -224,7 +280,11 @@ fn lengthen_ellipse(ell: &EllipseEnt, pick_pt: Vec3, mode: &LenMode) -> Option<E
|
|||
let mut hi = max_span;
|
||||
for _ in 0..40 {
|
||||
let mid = (lo + hi) * 0.5;
|
||||
if arc_len_approx(mid) < new_len { lo = mid; } else { hi = mid; }
|
||||
if arc_len_approx(mid) < new_len {
|
||||
lo = mid;
|
||||
} else {
|
||||
hi = mid;
|
||||
}
|
||||
}
|
||||
let new_span = (lo + hi) * 0.5;
|
||||
|
||||
|
|
@ -233,10 +293,10 @@ fn lengthen_ellipse(ell: &EllipseEnt, pick_pt: Vec3, mode: &LenMode) -> Option<E
|
|||
let p_y = pick_pt.z as f64; // Y-up: world Z → DXF Y
|
||||
let pt_start_x = ell.center.x + a * t0.cos() * nx - b * t0.sin() * ny;
|
||||
let pt_start_y = ell.center.y + a * t0.cos() * ny + b * t0.sin() * nx;
|
||||
let pt_end_x = ell.center.x + a * t1.cos() * nx - b * t1.sin() * ny;
|
||||
let pt_end_y = ell.center.y + a * t1.cos() * ny + b * t1.sin() * nx;
|
||||
let pt_end_x = ell.center.x + a * t1.cos() * nx - b * t1.sin() * ny;
|
||||
let pt_end_y = ell.center.y + a * t1.cos() * ny + b * t1.sin() * nx;
|
||||
let dist_start = (p_x - pt_start_x).hypot(p_y - pt_start_y);
|
||||
let dist_end = (p_x - pt_end_x).hypot(p_y - pt_end_y);
|
||||
let dist_end = (p_x - pt_end_x).hypot(p_y - pt_end_y);
|
||||
|
||||
let mut result = ell.clone();
|
||||
result.common.handle = Handle::NULL;
|
||||
|
|
@ -251,15 +311,19 @@ fn lengthen_ellipse(ell: &EllipseEnt, pick_pt: Vec3, mode: &LenMode) -> Option<E
|
|||
|
||||
fn apply_mode(current: f64, mode: &LenMode) -> Option<f64> {
|
||||
match mode {
|
||||
LenMode::Delta(d) => Some(current + d),
|
||||
LenMode::Total(t) => Some(*t),
|
||||
LenMode::Delta(d) => Some(current + d),
|
||||
LenMode::Total(t) => Some(*t),
|
||||
LenMode::Percent(p) => Some(current * p / 100.0),
|
||||
}
|
||||
}
|
||||
|
||||
fn arc_span_rad(start: f64, end: f64) -> f64 {
|
||||
let span = (end - start).rem_euclid(std::f64::consts::TAU);
|
||||
if span < 1e-6 { std::f64::consts::TAU } else { span }
|
||||
if span < 1e-6 {
|
||||
std::f64::consts::TAU
|
||||
} else {
|
||||
span
|
||||
}
|
||||
}
|
||||
|
||||
fn xz_to_v3(v: Vec3, z: f64) -> Vector3 {
|
||||
|
|
@ -269,34 +333,48 @@ fn xz_to_v3(v: Vec3, z: f64) -> Vector3 {
|
|||
|
||||
fn lengthen_lwpoly(poly: &LwPolyline, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
|
||||
let n = poly.vertices.len();
|
||||
if n < 2 { return None; }
|
||||
if n < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Determine which end is closer to the pick point (DXF XY: pick_pt.x, pick_pt.z).
|
||||
let px = pick_pt.x as f64;
|
||||
let py = pick_pt.z as f64; // Y-up: world Z = DXF Y
|
||||
|
||||
let first = &poly.vertices[0];
|
||||
let last = &poly.vertices[n - 1];
|
||||
let last = &poly.vertices[n - 1];
|
||||
let d_first = (first.location.x - px).hypot(first.location.y - py);
|
||||
let d_last = (last.location.x - px).hypot(last.location.y - py);
|
||||
let d_last = (last.location.x - px).hypot(last.location.y - py);
|
||||
let at_end = d_last <= d_first;
|
||||
|
||||
// Terminal segment direction and current length.
|
||||
let (sx, sy, ex, ey) = if at_end {
|
||||
(poly.vertices[n - 2].location.x, poly.vertices[n - 2].location.y,
|
||||
last.location.x, last.location.y)
|
||||
(
|
||||
poly.vertices[n - 2].location.x,
|
||||
poly.vertices[n - 2].location.y,
|
||||
last.location.x,
|
||||
last.location.y,
|
||||
)
|
||||
} else {
|
||||
(poly.vertices[1].location.x, poly.vertices[1].location.y,
|
||||
first.location.x, first.location.y)
|
||||
(
|
||||
poly.vertices[1].location.x,
|
||||
poly.vertices[1].location.y,
|
||||
first.location.x,
|
||||
first.location.y,
|
||||
)
|
||||
};
|
||||
|
||||
let dx = ex - sx;
|
||||
let dy = ey - sy;
|
||||
let current_len = (dx * dx + dy * dy).sqrt();
|
||||
if current_len < 1e-10 { return None; }
|
||||
if current_len < 1e-10 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let new_len = apply_mode(current_len, mode)?;
|
||||
if new_len < 1e-10 { return None; }
|
||||
if new_len < 1e-10 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let ux = dx / current_len;
|
||||
let uy = dy / current_len;
|
||||
|
|
@ -320,7 +398,9 @@ fn lengthen_lwpoly(poly: &LwPolyline, pick_pt: Vec3, mode: &LenMode) -> Option<E
|
|||
fn lengthen_spline(spl: &SplineEnt, pick_pt: Vec3, mode: &LenMode) -> Option<EntityType> {
|
||||
let bs = spline_to_bspline(spl)?;
|
||||
let (t0, t1) = bs.range_tuple();
|
||||
if (t1 - t0).abs() < 1e-12 { return None; }
|
||||
if (t1 - t0).abs() < 1e-12 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Approximate arc length via 64-point numerical integration.
|
||||
use truck_modeling::base::ParametricCurve;
|
||||
|
|
@ -336,16 +416,20 @@ fn lengthen_spline(spl: &SplineEnt, pick_pt: Vec3, mode: &LenMode) -> Option<Ent
|
|||
}
|
||||
len
|
||||
};
|
||||
if arc_len < 1e-10 { return None; }
|
||||
if arc_len < 1e-10 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let new_len = apply_mode(arc_len, mode)?;
|
||||
if new_len < 1e-10 { return None; }
|
||||
if new_len < 1e-10 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Determine which end (start or end) is closer to pick_pt.
|
||||
let p_start = bs.subs(t0);
|
||||
let p_end = bs.subs(t1);
|
||||
let p_end = bs.subs(t1);
|
||||
let dist_start = (p_start.x - pick_pt.x as f64).hypot(p_start.y - pick_pt.z as f64);
|
||||
let dist_end = (p_end.x - pick_pt.x as f64).hypot(p_end.y - pick_pt.z as f64);
|
||||
let dist_end = (p_end.x - pick_pt.x as f64).hypot(p_end.y - pick_pt.z as f64);
|
||||
let extend_end = dist_end <= dist_start;
|
||||
|
||||
// Find the parameter `t_new` such that the arc length from the fixed end to t_new = new_len.
|
||||
|
|
@ -355,8 +439,14 @@ fn lengthen_spline(spl: &SplineEnt, pick_pt: Vec3, mode: &LenMode) -> Option<Ent
|
|||
|
||||
// Find t_new via bisection: cumulative_len(fixed_t..t_new) = new_len.
|
||||
let cum_len = |t_end_param: f64| -> f64 {
|
||||
let (lo, hi) = if extend_end { (t0, t_end_param) } else { (t_end_param, t1) };
|
||||
if hi <= lo { return 0.0; }
|
||||
let (lo, hi) = if extend_end {
|
||||
(t0, t_end_param)
|
||||
} else {
|
||||
(t_end_param, t1)
|
||||
};
|
||||
if hi <= lo {
|
||||
return 0.0;
|
||||
}
|
||||
let n = 32usize;
|
||||
let mut len = 0.0f64;
|
||||
for i in 0..n {
|
||||
|
|
@ -382,9 +472,17 @@ fn lengthen_spline(spl: &SplineEnt, pick_pt: Vec3, mode: &LenMode) -> Option<Ent
|
|||
for _ in 0..40 {
|
||||
let mid = (lo_t + hi_t) * 0.5;
|
||||
if cum_len(mid) < new_len {
|
||||
if extend_end { hi_t = mid; } else { lo_t = mid; }
|
||||
if extend_end {
|
||||
hi_t = mid;
|
||||
} else {
|
||||
lo_t = mid;
|
||||
}
|
||||
} else {
|
||||
if extend_end { lo_t = mid; } else { hi_t = mid; }
|
||||
if extend_end {
|
||||
lo_t = mid;
|
||||
} else {
|
||||
hi_t = mid;
|
||||
}
|
||||
}
|
||||
}
|
||||
let t_new = (lo_t + hi_t) * 0.5;
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
pub mod align;
|
||||
pub mod attedit;
|
||||
pub mod array;
|
||||
pub mod attedit;
|
||||
pub mod break_cmd;
|
||||
pub mod copy;
|
||||
pub mod delete;
|
||||
pub mod explode;
|
||||
pub mod join;
|
||||
pub mod lengthen;
|
||||
pub mod pedit;
|
||||
mod extend;
|
||||
pub mod fillet;
|
||||
pub mod join;
|
||||
pub mod lengthen;
|
||||
pub mod mirror;
|
||||
pub mod offset;
|
||||
pub mod pedit;
|
||||
pub mod refedit;
|
||||
pub mod rotate;
|
||||
pub mod scale;
|
||||
pub mod stretch;
|
||||
pub mod translate;
|
||||
pub mod spline_ops;
|
||||
pub mod splinedit;
|
||||
pub mod refedit;
|
||||
pub mod stretch;
|
||||
pub mod translate;
|
||||
pub mod trim;
|
||||
|
|
|
|||
|
|
@ -11,9 +11,12 @@
|
|||
|
||||
use std::f64::consts::TAU;
|
||||
|
||||
use crate::modules::home::modify::spline_ops::{spline_pts_wire, spline_sample_xy};
|
||||
use acadrust::entities::LwVertex;
|
||||
use acadrust::entities::{Arc as ArcEnt, Circle as CircleEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline, Spline as SplineEnt};
|
||||
use crate::modules::home::modify::spline_ops::{spline_sample_xy, spline_pts_wire};
|
||||
use acadrust::entities::{
|
||||
Arc as ArcEnt, Circle as CircleEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline,
|
||||
Spline as SplineEnt,
|
||||
};
|
||||
use acadrust::{EntityType, Handle};
|
||||
use glam::Vec3;
|
||||
|
||||
|
|
@ -265,21 +268,25 @@ fn offset_lwpolyline(p: &LwPolyline, dist: f64, side_pt: Vec3) -> Option<EntityT
|
|||
|
||||
fn offset_ellipse(e: &EllipseEnt, dist: f64, side_pt: Vec3) -> Option<EntityType> {
|
||||
let a = (e.major_axis.x.powi(2) + e.major_axis.y.powi(2)).sqrt();
|
||||
if a < 1e-9 { return None; }
|
||||
if a < 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let b = a * e.minor_axis_ratio;
|
||||
let nx = e.major_axis.x / a;
|
||||
let ny = e.major_axis.y / a;
|
||||
// Project side_pt onto ellipse local frame and test inside/outside.
|
||||
let rx = side_pt.x as f64 - e.center.x;
|
||||
let ry = side_pt.y as f64 - e.center.y;
|
||||
let xl = rx * nx + ry * ny;
|
||||
let xl = rx * nx + ry * ny;
|
||||
let yl = -rx * ny + ry * nx;
|
||||
let inside = (xl / a).powi(2) + (yl / b).powi(2) < 1.0;
|
||||
let sign = if inside { -1.0 } else { 1.0 };
|
||||
|
||||
let new_a = a + sign * dist;
|
||||
let new_b = b + sign * dist;
|
||||
if new_a <= 1e-9 || new_b <= 1e-9 { return None; }
|
||||
if new_a <= 1e-9 || new_b <= 1e-9 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut new_e = e.clone();
|
||||
new_e.common.handle = Handle::NULL;
|
||||
|
|
@ -301,14 +308,18 @@ fn offset_ellipse(e: &EllipseEnt, dist: f64, side_pt: Vec3) -> Option<EntityType
|
|||
fn offset_spline(spl: &SplineEnt, dist: f64, side_pt: Vec3) -> Option<EntityType> {
|
||||
let (ts_knot, pts) = spline_sample_xy(spl, 64);
|
||||
let n = pts.len();
|
||||
if n < 2 { return None; }
|
||||
if n < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Determine offset sign from the first non-degenerate tangent.
|
||||
let sign: f64 = (0..n - 1).find_map(|i| {
|
||||
let dx = pts[i + 1][0] - pts[i][0];
|
||||
let dy = pts[i + 1][1] - pts[i][1];
|
||||
let len = (dx * dx + dy * dy).sqrt();
|
||||
if len < 1e-12 { return None; }
|
||||
if len < 1e-12 {
|
||||
return None;
|
||||
}
|
||||
let vx = side_pt.x as f64 - pts[i][0];
|
||||
let vy = side_pt.y as f64 - pts[i][1];
|
||||
let cross = dx * vy - dy * vx;
|
||||
|
|
@ -328,18 +339,16 @@ fn offset_spline(spl: &SplineEnt, dist: f64, side_pt: Vec3) -> Option<EntityType
|
|||
let d = [pts[n - 1][0] - pts[n - 2][0], pts[n - 1][1] - pts[n - 2][1]];
|
||||
(d[0], d[1])
|
||||
} else {
|
||||
((pts[i + 1][0] - pts[i - 1][0]) * 0.5,
|
||||
(pts[i + 1][1] - pts[i - 1][1]) * 0.5)
|
||||
(
|
||||
(pts[i + 1][0] - pts[i - 1][0]) * 0.5,
|
||||
(pts[i + 1][1] - pts[i - 1][1]) * 0.5,
|
||||
)
|
||||
};
|
||||
let len = (dx * dx + dy * dy).sqrt().max(1e-12);
|
||||
let nx = -dy / len; // left perpendicular
|
||||
let ny = dx / len;
|
||||
let nx = -dy / len; // left perpendicular
|
||||
let ny = dx / len;
|
||||
let z = spl.control_points.first().map(|v| v.z).unwrap_or(0.0);
|
||||
acadrust::types::Vector3::new(
|
||||
p[0] + sign * nx * dist,
|
||||
p[1] + sign * ny * dist,
|
||||
z,
|
||||
)
|
||||
acadrust::types::Vector3::new(p[0] + sign * nx * dist, p[1] + sign * ny * dist, z)
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
|
@ -419,23 +428,31 @@ fn entity_wire_pts(e: &EntityType) -> Vec<[f32; 3]> {
|
|||
EntityType::LwPolyline(p) => lwpolyline_pts(p),
|
||||
EntityType::Ellipse(e) => {
|
||||
let a = (e.major_axis.x.powi(2) + e.major_axis.y.powi(2)).sqrt();
|
||||
if a < 1e-9 { return vec![]; }
|
||||
if a < 1e-9 {
|
||||
return vec![];
|
||||
}
|
||||
let b = a * e.minor_axis_ratio;
|
||||
let nx = e.major_axis.x / a;
|
||||
let ny = e.major_axis.y / a;
|
||||
let t0 = e.start_parameter;
|
||||
let mut t1 = e.end_parameter;
|
||||
if t1 <= t0 { t1 += TAU; }
|
||||
if t1 <= t0 {
|
||||
t1 += TAU;
|
||||
}
|
||||
let span = t1 - t0;
|
||||
let steps = ((span.abs() * 20.0).ceil() as usize).max(4);
|
||||
(0..=steps).map(|i| {
|
||||
let t = t0 + span * (i as f64 / steps as f64);
|
||||
let lx = a * t.cos();
|
||||
let ly = b * t.sin();
|
||||
[(e.center.x + lx * nx - ly * ny) as f32,
|
||||
(e.center.y + lx * ny + ly * nx) as f32,
|
||||
e.center.z as f32]
|
||||
}).collect()
|
||||
(0..=steps)
|
||||
.map(|i| {
|
||||
let t = t0 + span * (i as f64 / steps as f64);
|
||||
let lx = a * t.cos();
|
||||
let ly = b * t.sin();
|
||||
[
|
||||
(e.center.x + lx * nx - ly * ny) as f32,
|
||||
(e.center.y + lx * ny + ly * nx) as f32,
|
||||
e.center.z as f32,
|
||||
]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
EntityType::Spline(s) => spline_pts_wire(s),
|
||||
_ => vec![],
|
||||
|
|
|
|||
|
|
@ -25,7 +25,9 @@ impl PeditCommand {
|
|||
}
|
||||
|
||||
impl CadCommand for PeditCommand {
|
||||
fn name(&self) -> &'static str { "PEDIT" }
|
||||
fn name(&self) -> &'static str {
|
||||
"PEDIT"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if self.target.is_none() {
|
||||
|
|
@ -40,7 +42,9 @@ impl CadCommand for PeditCommand {
|
|||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult {
|
||||
if handle.is_null() { return CmdResult::NeedPoint; }
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.target = Some(handle);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
|
@ -55,21 +59,43 @@ impl CadCommand for PeditCommand {
|
|||
|
||||
match up.as_str() {
|
||||
"X" | "EXIT" => return Some(CmdResult::Cancel),
|
||||
"C" | "CLOSE" => return Some(CmdResult::PeditOp { handle, op: PeditOp::SetClosed(true) }),
|
||||
"O" | "OPEN" => return Some(CmdResult::PeditOp { handle, op: PeditOp::SetClosed(false) }),
|
||||
"C" | "CLOSE" => {
|
||||
return Some(CmdResult::PeditOp {
|
||||
handle,
|
||||
op: PeditOp::SetClosed(true),
|
||||
})
|
||||
}
|
||||
"O" | "OPEN" => {
|
||||
return Some(CmdResult::PeditOp {
|
||||
handle,
|
||||
op: PeditOp::SetClosed(false),
|
||||
})
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Some(rest) = up.strip_prefix("W ").or_else(|| up.strip_prefix("W")) {
|
||||
let w: f64 = rest.trim().replace(',', ".").parse().ok().filter(|&v: &f64| v >= 0.0)?;
|
||||
return Some(CmdResult::PeditOp { handle, op: PeditOp::SetWidth(w) });
|
||||
let w: f64 = rest
|
||||
.trim()
|
||||
.replace(',', ".")
|
||||
.parse()
|
||||
.ok()
|
||||
.filter(|&v: &f64| v >= 0.0)?;
|
||||
return Some(CmdResult::PeditOp {
|
||||
handle,
|
||||
op: PeditOp::SetWidth(w),
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult { CmdResult::NeedPoint }
|
||||
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
// ── Op enum (used in CmdResult) ────────────────────────────────────────────
|
||||
|
|
@ -85,9 +111,16 @@ pub enum PeditOp {
|
|||
pub fn apply_pedit(entity: &mut EntityType, op: &PeditOp) -> bool {
|
||||
match op {
|
||||
PeditOp::SetClosed(closed) => match entity {
|
||||
EntityType::LwPolyline(p) => { p.is_closed = *closed; true }
|
||||
EntityType::LwPolyline(p) => {
|
||||
p.is_closed = *closed;
|
||||
true
|
||||
}
|
||||
EntityType::Polyline2D(p) => {
|
||||
if *closed { p.close(); } else { p.flags.set_closed(false); }
|
||||
if *closed {
|
||||
p.close();
|
||||
} else {
|
||||
p.flags.set_closed(false);
|
||||
}
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
|
|
|
|||
|
|
@ -71,10 +71,7 @@ impl CadCommand for RefEditPickCommand {
|
|||
}
|
||||
// Signal the host to enter the editing session for this handle.
|
||||
// We reuse Relaunch("REFEDIT_BEGIN:<handle>") as a convention.
|
||||
CmdResult::Relaunch(
|
||||
format!("REFEDIT_BEGIN:{}", handle.value()),
|
||||
vec![handle],
|
||||
)
|
||||
CmdResult::Relaunch(format!("REFEDIT_BEGIN:{}", handle.value()), vec![handle])
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
|
||||
|
|
@ -142,7 +139,10 @@ pub fn apply_insert_transform(entity: &mut EntityType, session: &RefEditSession)
|
|||
if (session.scale - 1.0).abs() > 1e-10 {
|
||||
dispatch::apply_transform(
|
||||
entity,
|
||||
&EntityTransform::Scale { center: origin, factor: session.scale as f32 },
|
||||
&EntityTransform::Scale {
|
||||
center: origin,
|
||||
factor: session.scale as f32,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@
|
|||
use acadrust::entities::Spline;
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::Handle;
|
||||
use truck_modeling::{BSplineCurve, KnotVec, Point3};
|
||||
use truck_modeling::base::{BoundedCurve, ParametricCurve};
|
||||
use truck_modeling::{BSplineCurve, KnotVec, Point3};
|
||||
|
||||
// ── Conversion ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -39,11 +39,7 @@ pub fn spline_to_bspline(spl: &Spline) -> Option<BSplineCurve<Point3>> {
|
|||
/// Rebuild an acadrust `Spline` from a trimmed `BSplineCurve<Point3>`.
|
||||
/// The `template` spline provides the entity common data, degree, and Z-elevation.
|
||||
pub fn bspline_to_spline(bs: &BSplineCurve<Point3>, template: &Spline) -> Spline {
|
||||
let z = template
|
||||
.control_points
|
||||
.first()
|
||||
.map(|v| v.z)
|
||||
.unwrap_or(0.0);
|
||||
let z = template.control_points.first().map(|v| v.z).unwrap_or(0.0);
|
||||
let mut spl = template.clone();
|
||||
spl.common.handle = Handle::NULL;
|
||||
spl.degree = bs.degree() as i32;
|
||||
|
|
@ -53,7 +49,7 @@ pub fn bspline_to_spline(bs: &BSplineCurve<Point3>, template: &Spline) -> Spline
|
|||
.iter()
|
||||
.map(|p| Vector3::new(p.x, p.y, z))
|
||||
.collect();
|
||||
spl.weights.clear(); // not rational after split
|
||||
spl.weights.clear(); // not rational after split
|
||||
spl.fit_points.clear(); // fit points are no longer valid
|
||||
spl
|
||||
}
|
||||
|
|
@ -71,10 +67,13 @@ pub fn spline_sample_xy(spl: &Spline, n: usize) -> (Vec<f64>, Vec<[f64; 2]>) {
|
|||
let ts: Vec<f64> = (0..=n)
|
||||
.map(|i| t0 + (t1 - t0) * (i as f64 / n as f64))
|
||||
.collect();
|
||||
let pts: Vec<[f64; 2]> = ts.iter().map(|&t| {
|
||||
let p = bs.subs(t);
|
||||
[p.x, p.y]
|
||||
}).collect();
|
||||
let pts: Vec<[f64; 2]> = ts
|
||||
.iter()
|
||||
.map(|&t| {
|
||||
let p = bs.subs(t);
|
||||
[p.x, p.y]
|
||||
})
|
||||
.collect();
|
||||
(ts, pts)
|
||||
}
|
||||
|
||||
|
|
@ -112,7 +111,11 @@ pub fn spline_nearest_t(spl: &Spline, x: f64, y: f64) -> Option<f64> {
|
|||
let p2 = bs.subs(m2);
|
||||
let d1 = (p1.x - x).powi(2) + (p1.y - y).powi(2);
|
||||
let d2 = (p2.x - x).powi(2) + (p2.y - y).powi(2);
|
||||
if d1 < d2 { b = m2; } else { a = m1; }
|
||||
if d1 < d2 {
|
||||
b = m2;
|
||||
} else {
|
||||
a = m1;
|
||||
}
|
||||
}
|
||||
Some((a + b) * 0.5)
|
||||
}
|
||||
|
|
@ -120,7 +123,9 @@ pub fn spline_nearest_t(spl: &Spline, x: f64, y: f64) -> Option<f64> {
|
|||
/// Returns the normalised parameter in [0, 1] given an actual B-spline
|
||||
/// parameter `t_actual` and the range `[t0, t1]`.
|
||||
pub fn t_to_rel(t_actual: f64, t0: f64, t1: f64) -> f64 {
|
||||
if (t1 - t0).abs() < 1e-12 { return 0.0; }
|
||||
if (t1 - t0).abs() < 1e-12 {
|
||||
return 0.0;
|
||||
}
|
||||
((t_actual - t0) / (t1 - t0)).clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
|
|
@ -130,8 +135,14 @@ pub fn t_to_rel(t_actual: f64, t0: f64, t1: f64) -> f64 {
|
|||
/// Y-up convention: world (x, 0, y) for DXF (x, y).
|
||||
pub fn spline_pts_wire(spl: &Spline) -> Vec<[f32; 3]> {
|
||||
let (_, pts) = spline_sample_xy(spl, 64);
|
||||
if pts.is_empty() { return vec![]; }
|
||||
let elev = spl.control_points.first().map(|v| v.z as f32).unwrap_or(0.0);
|
||||
if pts.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
let elev = spl
|
||||
.control_points
|
||||
.first()
|
||||
.map(|v| v.z as f32)
|
||||
.unwrap_or(0.0);
|
||||
pts.iter()
|
||||
.map(|p| [p[0] as f32, elev, p[1] as f32])
|
||||
.collect()
|
||||
|
|
|
|||
|
|
@ -39,19 +39,21 @@ pub struct SplineditCommand {
|
|||
|
||||
impl SplineditCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { step: Step::SelectSpline }
|
||||
Self {
|
||||
step: Step::SelectSpline,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for SplineditCommand {
|
||||
fn name(&self) -> &'static str { "SPLINEDIT" }
|
||||
fn name(&self) -> &'static str {
|
||||
"SPLINEDIT"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::SelectSpline => "SPLINEDIT Select spline:".to_string(),
|
||||
Step::SubCommand { .. } => {
|
||||
"SPLINEDIT [CLOSE/OPEN/REVERSE/EXIT]:".to_string()
|
||||
}
|
||||
Step::SubCommand { .. } => "SPLINEDIT [CLOSE/OPEN/REVERSE/EXIT]:".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,7 +74,9 @@ impl CadCommand for SplineditCommand {
|
|||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let Step::SubCommand { handle } = &self.step else { return None; };
|
||||
let Step::SubCommand { handle } = &self.step else {
|
||||
return None;
|
||||
};
|
||||
let handle = *handle;
|
||||
match text.trim().to_uppercase().as_str() {
|
||||
"CLOSE" | "C" => Some(CmdResult::ReplaceEntity(
|
||||
|
|
@ -92,9 +96,15 @@ impl CadCommand for SplineditCommand {
|
|||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult { CmdResult::NeedPoint }
|
||||
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
fn on_preview_wires(&mut self, _pt: Vec3) -> Vec<WireModel> { vec![] }
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
fn on_preview_wires(&mut self, _pt: Vec3) -> Vec<WireModel> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
// ── Spline operation helpers ───────────────────────────────────────────────
|
||||
|
|
@ -108,7 +118,11 @@ impl CadCommand for SplineditCommand {
|
|||
// we only have the handle. The real transformation (close/open/reverse) is
|
||||
// applied in the Scene via `apply_spline_op`.
|
||||
|
||||
enum SplineOp { Close, Open, Reverse }
|
||||
enum SplineOp {
|
||||
Close,
|
||||
Open,
|
||||
Reverse,
|
||||
}
|
||||
|
||||
impl SplineOp {
|
||||
/// Return a placeholder that encodes the op in a comment field.
|
||||
|
|
@ -130,8 +144,8 @@ impl SplineOp {
|
|||
acadrust::types::Vector3::new(1.0, 0.0, 0.0),
|
||||
);
|
||||
sentinel.common.layer = match self {
|
||||
SplineOp::Close => "__SPLINEDIT_CLOSE__".to_string(),
|
||||
SplineOp::Open => "__SPLINEDIT_OPEN__".to_string(),
|
||||
SplineOp::Close => "__SPLINEDIT_CLOSE__".to_string(),
|
||||
SplineOp::Open => "__SPLINEDIT_OPEN__".to_string(),
|
||||
SplineOp::Reverse => "__SPLINEDIT_REVERSE__".to_string(),
|
||||
};
|
||||
EntityType::XLine(sentinel)
|
||||
|
|
@ -141,8 +155,9 @@ impl SplineOp {
|
|||
/// Apply a spline operation (CLOSE/OPEN/REVERSE) to a spline entity.
|
||||
/// Called from `cmd_result.rs` when the ReplaceEntity sentinel is detected.
|
||||
pub fn apply_spline_op(doc: &mut acadrust::CadDocument, handle: acadrust::Handle, op: &str) {
|
||||
|
||||
let Some(EntityType::Spline(spline)) = doc.get_entity_mut(handle) else { return; };
|
||||
let Some(EntityType::Spline(spline)) = doc.get_entity_mut(handle) else {
|
||||
return;
|
||||
};
|
||||
match op {
|
||||
"__SPLINEDIT_CLOSE__" => {
|
||||
if spline.control_points.len() >= 2 {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,11 @@ enum Step {
|
|||
/// Crossing window defined; waiting for base point.
|
||||
Base { win_min: Vec3, win_max: Vec3 },
|
||||
/// Waiting for target point.
|
||||
Target { win_min: Vec3, win_max: Vec3, base: Vec3 },
|
||||
Target {
|
||||
win_min: Vec3,
|
||||
win_max: Vec3,
|
||||
base: Vec3,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct StretchCommand {
|
||||
|
|
@ -53,12 +57,17 @@ pub struct StretchCommand {
|
|||
|
||||
impl StretchCommand {
|
||||
pub fn new(handles: Vec<Handle>) -> Self {
|
||||
Self { handles, step: Step::WindowCorner1 }
|
||||
Self {
|
||||
handles,
|
||||
step: Step::WindowCorner1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for StretchCommand {
|
||||
fn name(&self) -> &'static str { "STRETCH" }
|
||||
fn name(&self) -> &'static str {
|
||||
"STRETCH"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
|
|
@ -69,7 +78,8 @@ impl CadCommand for StretchCommand {
|
|||
Step::WindowCorner2(_) => "STRETCH Specify opposite corner:".into(),
|
||||
Step::Base { .. } => "STRETCH Specify base point:".into(),
|
||||
Step::Target { base, .. } => format!(
|
||||
"STRETCH Specify new point [base {:.3},{:.3}]:", base.x, base.z
|
||||
"STRETCH Specify new point [base {:.3},{:.3}]:",
|
||||
base.x, base.z
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
@ -88,10 +98,18 @@ impl CadCommand for StretchCommand {
|
|||
}
|
||||
Step::Base { win_min, win_max } => {
|
||||
let (wmin, wmax) = (*win_min, *win_max);
|
||||
self.step = Step::Target { win_min: wmin, win_max: wmax, base: pt };
|
||||
self.step = Step::Target {
|
||||
win_min: wmin,
|
||||
win_max: wmax,
|
||||
base: pt,
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Target { win_min, win_max, base } => {
|
||||
Step::Target {
|
||||
win_min,
|
||||
win_max,
|
||||
base,
|
||||
} => {
|
||||
let delta = pt - *base;
|
||||
CmdResult::StretchEntities {
|
||||
handles: self.handles.clone(),
|
||||
|
|
@ -103,8 +121,12 @@ impl CadCommand for StretchCommand {
|
|||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel }
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
|
||||
match &self.step {
|
||||
|
|
@ -113,32 +135,35 @@ impl CadCommand for StretchCommand {
|
|||
let c1 = *c1;
|
||||
let pts = vec![
|
||||
[c1.x, c1.y, c1.z],
|
||||
[pt.x, c1.y, c1.z],
|
||||
[pt.x, pt.y, pt.z],
|
||||
[c1.x, pt.y, pt.z],
|
||||
[pt.x, c1.y, c1.z],
|
||||
[pt.x, pt.y, pt.z],
|
||||
[c1.x, pt.y, pt.z],
|
||||
[c1.x, c1.y, c1.z],
|
||||
];
|
||||
Some(WireModel::solid("stretch_window".into(), pts, [0.3, 1.0, 0.3, 0.7], false))
|
||||
}
|
||||
Step::Target { base, .. } => {
|
||||
Some(WireModel {
|
||||
name: "rubber_band".into(),
|
||||
points: vec![[base.x, base.y, base.z], [pt.x, pt.y, pt.z]],
|
||||
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,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
})
|
||||
Some(WireModel::solid(
|
||||
"stretch_window".into(),
|
||||
pts,
|
||||
[0.3, 1.0, 0.3, 0.7],
|
||||
false,
|
||||
))
|
||||
}
|
||||
Step::Target { base, .. } => Some(WireModel {
|
||||
name: "rubber_band".into(),
|
||||
points: vec![[base.x, base.y, base.z], [pt.x, pt.y, pt.z]],
|
||||
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,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,10 @@
|
|||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/attdef.svg"));
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef { id: "ATTDEF", label: "Define\nAttributes", icon: ICON, event: ModuleEvent::Command("ATTDEF".to_string()) }
|
||||
ToolDef {
|
||||
id: "ATTDEF",
|
||||
label: "Define\nAttributes",
|
||||
icon: ICON,
|
||||
event: ModuleEvent::Command("ATTDEF".to_string()),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue