cargo fmt

This commit is contained in:
Hakan 2026-05-12 10:36:15 +03:00
commit ca07268c7c
204 changed files with 13988 additions and 5516 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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 ─────────────────────────────────────────────────

View file

@ -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", …).

View file

@ -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()
}

View file

@ -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,
);
}
}

View file

@ -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();

View file

@ -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(),

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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 {

View file

@ -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()),
],
}

View file

@ -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,

View file

@ -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();

View file

@ -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))

View file

@ -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 {

View file

@ -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;

View file

@ -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);
});
}

View file

@ -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))
}
}

View file

@ -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),

View file

@ -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,

View file

@ -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()

View file

@ -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;
}

View file

@ -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;

View file

@ -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,
);
});
}

View file

@ -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);
}
}

View file

@ -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();
}
}
}
}

View file

@ -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,

View file

@ -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;
}
_ => {}
}
}

View file

@ -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(),
_ => {}
}

View file

@ -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);
}
});
}

View file

@ -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),
],
}

View file

@ -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() {

View file

@ -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),
),
],
}
}

View file

@ -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

View file

@ -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()
}

View file

@ -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,

View file

@ -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),

View file

@ -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 {

View file

@ -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,

View file

@ -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;

View file

@ -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);

View file

@ -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,
})
}

View file

@ -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.

View file

@ -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}")
}

View file

@ -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],
});
}
}

View file

@ -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;
}

View file

@ -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>) {

View file

@ -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![],
}
}

View file

@ -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![],
}
}

View file

@ -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,
}
}

View file

@ -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![],
}
}

View file

@ -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],

View file

@ -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],

View file

@ -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

View file

@ -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
}
}

View file

@ -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 { .. }) {

View file

@ -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 {

View file

@ -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 { .. }) {

View file

@ -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,
),
]
}

View file

@ -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![],
}
}

View file

@ -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,
),
]
}

View file

@ -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![],
}
}

View file

@ -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()]],
},
],
},

View file

@ -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 {

View file

@ -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;

View file

@ -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![],
}
}

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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);

View file

@ -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![],
}
}

View file

@ -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 }
}

View file

@ -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 {

View file

@ -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;

View file

@ -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,
))
}
}

View file

@ -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()
}

View file

@ -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);

View file

@ -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))

View file

@ -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
}
}

View file

@ -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()),
}
}

View file

@ -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()),
}
}

View file

@ -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",

View file

@ -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))
}

View file

@ -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![]
}
}

View file

@ -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);
}
}
}

View file

@ -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,
}
}

View file

@ -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()),
}
};

View file

@ -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

View file

@ -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)]))
}

View file

@ -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;

View file

@ -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;

View file

@ -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![],

View file

@ -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,

View file

@ -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,
},
);
}

View file

@ -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()

View file

@ -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 {

View file

@ -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

View file

@ -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