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

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

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/attedit.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "ATTEDIT", label: "Edit\nAttribute", icon: ICON, event: ModuleEvent::Command("ATTEDIT".to_string()) }
ToolDef {
id: "ATTEDIT",
label: "Edit\nAttribute",
icon: ICON,
event: ModuleEvent::Command("ATTEDIT".to_string()),
}
}

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/attman.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "ATTMAN", label: "Manage", icon: ICON, event: ModuleEvent::Command("ATTMAN".to_string()) }
ToolDef {
id: "ATTMAN",
label: "Manage",
icon: ICON,
event: ModuleEvent::Command("ATTMAN".to_string()),
}
}

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/attsync.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "ATTSYNC", label: "Synchronize", icon: ICON, event: ModuleEvent::Command("ATTSYNC".to_string()) }
ToolDef {
id: "ATTSYNC",
label: "Synchronize",
icon: ICON,
event: ModuleEvent::Command("ATTSYNC".to_string()),
}
}

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/base_point.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "BASE", label: "Set Base\nPoint", icon: ICON, event: ModuleEvent::Command("BASE".to_string()) }
ToolDef {
id: "BASE",
label: "Set Base\nPoint",
icon: ICON,
event: ModuleEvent::Command("BASE".to_string()),
}
}

View file

@ -1,5 +1,11 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/content_browser.svg"));
pub const ICON: IconKind =
IconKind::Svg(include_bytes!("../../../assets/icons/content_browser.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "CONTENTBROWSER", label: "Content\nBrowser", icon: ICON, event: ModuleEvent::Command("CONTENTBROWSER".to_string()) }
ToolDef {
id: "CONTENTBROWSER",
label: "Content\nBrowser",
icon: ICON,
event: ModuleEvent::Command("CONTENTBROWSER".to_string()),
}
}

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/design_center.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "ADCENTER", label: "Design\nCenter", icon: ICON, event: ModuleEvent::Command("ADCENTER".to_string()) }
ToolDef {
id: "ADCENTER",
label: "Design\nCenter",
icon: ICON,
event: ModuleEvent::Command("ADCENTER".to_string()),
}
}

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/edit_block.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "BEDIT", label: "Edit Block", icon: ICON, event: ModuleEvent::Command("BEDIT".to_string()) }
ToolDef {
id: "BEDIT",
label: "Edit Block",
icon: ICON,
event: ModuleEvent::Command("BEDIT".to_string()),
}
}

View file

@ -18,7 +18,9 @@ pub fn tool() -> ToolDef {
enum Step {
Name,
Point { name: String },
Point {
name: String,
},
/// Attribute filling: prompt the user tag by tag.
FillAttr {
/// Attribute definitions: (tag, prompt_text, default_value).
@ -70,7 +72,11 @@ impl CadCommand for InsertBlockCommand {
} else {
format!(" <{default}>")
};
let prompt_text = if prompt.is_empty() { tag.as_str() } else { prompt.as_str() };
let prompt_text = if prompt.is_empty() {
tag.as_str()
} else {
prompt.as_str()
};
format!("INSERT {prompt_text}{default_hint}:")
} else {
"INSERT Filling attributes...".into()
@ -132,11 +138,17 @@ impl CadCommand for InsertBlockCommand {
}
fn attreq_set_attdefs(&mut self, attdefs: Vec<(String, String, String)>) {
self.step = Step::FillAttr { attdefs, idx: 0, values: vec![] };
self.step = Step::FillAttr {
attdefs,
idx: 0,
values: vec![],
};
}
fn attreq_take_insert(&mut self) -> Option<acadrust::EntityType> {
self.pending_insert.take().map(|ins| EntityType::Insert(ins))
self.pending_insert
.take()
.map(|ins| EntityType::Insert(ins))
}
}
@ -152,9 +164,18 @@ impl InsertBlockCommand {
_ => return CmdResult::Cancel,
};
let value = if text.trim().is_empty() { default } else { text.trim().to_string() };
let value = if text.trim().is_empty() {
default
} else {
text.trim().to_string()
};
if let Step::FillAttr { ref mut values, ref mut idx, .. } = self.step {
if let Step::FillAttr {
ref mut values,
ref mut idx,
..
} = self.step
{
values.push((tag, value));
*idx = next_idx;
}

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/landxml.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "LANDXMLIMPORT", label: "Land\nXML", icon: ICON, event: ModuleEvent::Command("LANDXMLIMPORT".to_string()) }
ToolDef {
id: "LANDXMLIMPORT",
label: "Land\nXML",
icon: ICON,
event: ModuleEvent::Command("LANDXMLIMPORT".to_string()),
}
}

View file

@ -5,8 +5,8 @@ mod attedit;
mod attman;
mod attsync;
mod base_point;
pub(crate) mod create_block;
mod content_browser;
pub(crate) mod create_block;
mod design_center;
mod edit_block;
pub(crate) mod insert_block;
@ -19,8 +19,8 @@ pub(crate) mod solid3d_cmds;
mod underlay_frames;
mod underlay_layers;
pub(crate) mod wblock;
pub(crate) mod xattach;
mod xadjust;
pub(crate) mod xattach;
mod xclip;
use crate::modules::{CadModule, IconKind, RibbonGroup, RibbonItem};
@ -47,11 +47,31 @@ impl CadModule for InsertModule {
RibbonItem::Tool(underlay_layers::tool()),
RibbonItem::Dropdown {
id: "FRAMES_DROPDOWN",
icon: IconKind::Svg(include_bytes!("../../../assets/icons/underlay_frames.svg")),
icon: IconKind::Svg(include_bytes!(
"../../../assets/icons/underlay_frames.svg"
)),
items: vec![
("FRAMES0", "Frames Off", IconKind::Svg(include_bytes!("../../../assets/icons/underlay_frames.svg"))),
("FRAMES1", "Frames On", IconKind::Svg(include_bytes!("../../../assets/icons/underlay_frames.svg"))),
("FRAMES2", "Frames & Print",IconKind::Svg(include_bytes!("../../../assets/icons/underlay_frames.svg"))),
(
"FRAMES0",
"Frames Off",
IconKind::Svg(include_bytes!(
"../../../assets/icons/underlay_frames.svg"
)),
),
(
"FRAMES1",
"Frames On",
IconKind::Svg(include_bytes!(
"../../../assets/icons/underlay_frames.svg"
)),
),
(
"FRAMES2",
"Frames & Print",
IconKind::Svg(include_bytes!(
"../../../assets/icons/underlay_frames.svg"
)),
),
],
default: "FRAMES1",
},
@ -61,9 +81,7 @@ impl CadModule for InsertModule {
// ── Point Cloud ───────────────────────────────────────────────────
RibbonGroup {
title: "Point Cloud",
tools: vec![
RibbonItem::LargeTool(pc_attach::tool()),
],
tools: vec![RibbonItem::LargeTool(pc_attach::tool())],
},
// ── Block ─────────────────────────────────────────────────────────
RibbonGroup {

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/mview_block.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "BLOCKPALETTE", label: "Multi-View\nBlock", icon: ICON, event: ModuleEvent::Command("BLOCKPALETTE".to_string()) }
ToolDef {
id: "BLOCKPALETTE",
label: "Multi-View\nBlock",
icon: ICON,
event: ModuleEvent::Command("BLOCKPALETTE".to_string()),
}
}

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/pc_attach.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "POINTCLOUDATTACH", label: "Attach", icon: ICON, event: ModuleEvent::Command("POINTCLOUDATTACH".to_string()) }
ToolDef {
id: "POINTCLOUDATTACH",
label: "Attach",
icon: ICON,
event: ModuleEvent::Command("POINTCLOUDATTACH".to_string()),
}
}

View file

@ -1,5 +1,11 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/snap_underlays.svg"));
pub const ICON: IconKind =
IconKind::Svg(include_bytes!("../../../assets/icons/snap_underlays.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "UOSNAP", label: "Snap to\nUnderlays", icon: ICON, event: ModuleEvent::Command("UOSNAP".to_string()) }
ToolDef {
id: "UOSNAP",
label: "Snap to\nUnderlays",
icon: ICON,
event: ModuleEvent::Command("UOSNAP".to_string()),
}
}

View file

@ -26,9 +26,15 @@ use crate::scene::truck_tess;
fn solid_to_mesh(solid: &truck_modeling::Solid, color: [f32; 4], name: &str) -> Option<MeshModel> {
match truck_tess::tessellate_solid(solid, [0.0; 3]) {
truck_tess::TruckTessResult::Mesh { verts, normals, indices } => Some(MeshModel {
truck_tess::TruckTessResult::Mesh {
verts,
normals,
indices,
} => Some(MeshModel {
name: name.to_string(),
verts, normals, indices,
verts,
normals,
indices,
color,
selected: false,
}),
@ -38,9 +44,15 @@ fn solid_to_mesh(solid: &truck_modeling::Solid, color: [f32; 4], name: &str) ->
fn shell_to_mesh(shell: &truck_modeling::Shell, color: [f32; 4], name: &str) -> Option<MeshModel> {
match truck_tess::tessellate_shell(shell, [0.0; 3]) {
truck_tess::TruckTessResult::Mesh { verts, normals, indices } => Some(MeshModel {
truck_tess::TruckTessResult::Mesh {
verts,
normals,
indices,
} => Some(MeshModel {
name: name.to_string(),
verts, normals, indices,
verts,
normals,
indices,
color,
selected: false,
}),
@ -58,36 +70,67 @@ pub struct BoxCommand {
}
#[derive(PartialEq)]
enum BoxStep { Corner1, Corner2, Height }
enum BoxStep {
Corner1,
Corner2,
Height,
}
impl BoxCommand {
pub fn new(color: [f32; 4]) -> Self {
Self { step: BoxStep::Corner1, p1: Vec3::ZERO, p2: Vec3::ZERO, color }
Self {
step: BoxStep::Corner1,
p1: Vec3::ZERO,
p2: Vec3::ZERO,
color,
}
}
}
impl CadCommand for BoxCommand {
fn name(&self) -> &'static str { "BOX" }
fn name(&self) -> &'static str {
"BOX"
}
fn prompt(&self) -> String {
match self.step {
BoxStep::Corner1 => "BOX First corner:".into(),
BoxStep::Corner2 => "BOX Opposite corner (XY):".into(),
BoxStep::Height => "BOX Height:".into(),
BoxStep::Height => "BOX Height:".into(),
}
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
match self.step {
BoxStep::Corner1 => { self.p1 = pt; self.step = BoxStep::Corner2; CmdResult::NeedPoint }
BoxStep::Corner2 => { self.p2 = pt; self.step = BoxStep::Height; CmdResult::NeedPoint }
BoxStep::Height => commit_box(self.p1, self.p2, (pt.y - self.p1.y).abs().max(1e-4), self.color),
BoxStep::Corner1 => {
self.p1 = pt;
self.step = BoxStep::Corner2;
CmdResult::NeedPoint
}
BoxStep::Corner2 => {
self.p2 = pt;
self.step = BoxStep::Height;
CmdResult::NeedPoint
}
BoxStep::Height => commit_box(
self.p1,
self.p2,
(pt.y - self.p1.y).abs().max(1e-4),
self.color,
),
}
}
fn wants_text_input(&self) -> bool { self.step == BoxStep::Height }
fn wants_text_input(&self) -> bool {
self.step == BoxStep::Height
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
text.trim().parse::<f32>().ok().filter(|&h| h.abs() > 1e-6)
text.trim()
.parse::<f32>()
.ok()
.filter(|&h| h.abs() > 1e-6)
.map(|h| commit_box(self.p1, self.p2, h.abs(), self.color))
}
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
fn commit_box(p1: Vec3, p2: Vec3, height: f32, color: [f32; 4]) -> CmdResult {
@ -97,7 +140,7 @@ fn commit_box(p1: Vec3, p2: Vec3, height: f32, color: [f32; 4]) -> CmdResult {
let x1 = p1.x.max(p2.x) as f64;
let y1 = p1.z.max(p2.z) as f64;
let z0 = p1.y as f64;
let h = height as f64;
let h = height as f64;
// Build face at z=z0, then sweep to z0+h.
let v00 = builder::vertex(Point3::new(x0, y0, z0));
@ -115,7 +158,9 @@ fn commit_box(p1: Vec3, p2: Vec3, height: f32, color: [f32; 4]) -> CmdResult {
};
// tsweep on Face → Solid
let solid = builder::tsweep(&face, TruckVec3::new(0.0, 0.0, h));
CmdResult::CommitSolid3D { mesh_fn: Box::new(move |name| solid_to_mesh(&solid, color, &name)) }
CmdResult::CommitSolid3D {
mesh_fn: Box::new(move |name| solid_to_mesh(&solid, color, &name)),
}
}
// ── SPHERE command ─────────────────────────────────────────────────────────
@ -127,16 +172,25 @@ pub struct SphereCommand {
}
#[derive(PartialEq)]
enum SphereStep { Center, Radius }
enum SphereStep {
Center,
Radius,
}
impl SphereCommand {
pub fn new(color: [f32; 4]) -> Self {
Self { step: SphereStep::Center, center: Vec3::ZERO, color }
Self {
step: SphereStep::Center,
center: Vec3::ZERO,
color,
}
}
}
impl CadCommand for SphereCommand {
fn name(&self) -> &'static str { "SPHERE" }
fn name(&self) -> &'static str {
"SPHERE"
}
fn prompt(&self) -> String {
match self.step {
SphereStep::Center => "SPHERE Center:".into(),
@ -145,35 +199,50 @@ impl CadCommand for SphereCommand {
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
match self.step {
SphereStep::Center => { self.center = pt; self.step = SphereStep::Radius; CmdResult::NeedPoint }
SphereStep::Radius => commit_sphere(self.center, (pt - self.center).length(), self.color),
SphereStep::Center => {
self.center = pt;
self.step = SphereStep::Radius;
CmdResult::NeedPoint
}
SphereStep::Radius => {
commit_sphere(self.center, (pt - self.center).length(), self.color)
}
}
}
fn wants_text_input(&self) -> bool { self.step == SphereStep::Radius }
fn wants_text_input(&self) -> bool {
self.step == SphereStep::Radius
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
text.trim().parse::<f32>().ok().filter(|&r| r > 1e-6)
text.trim()
.parse::<f32>()
.ok()
.filter(|&r| r > 1e-6)
.map(|r| commit_sphere(self.center, r, self.color))
}
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
fn commit_sphere(center: Vec3, radius: f32, color: [f32; 4]) -> CmdResult {
let cx = center.x as f64;
let cy = center.z as f64;
let cz = center.y as f64;
let r = radius as f64;
let r = radius as f64;
// Build a half-circle arc wire from north pole to south pole (XZ plane).
let north = builder::vertex(Point3::new(cx, cy, cz + r));
let south = builder::vertex(Point3::new(cx, cy, cz - r));
let east = Point3::new(cx + r, cy, cz);
let arc = builder::circle_arc(&north, &south, east);
let east = Point3::new(cx + r, cy, cz);
let arc = builder::circle_arc(&north, &south, east);
// rsweep the wire around the Z axis for a full revolution → Shell.
let wire: truck_modeling::Wire = std::iter::once(arc).collect();
let axis_pt = Point3::new(cx, cy, cz);
let axis = TruckVec3::new(0.0, 0.0, 1.0);
let shell = builder::rsweep(&wire, axis_pt, axis, Rad(std::f64::consts::TAU));
CmdResult::CommitSolid3D { mesh_fn: Box::new(move |name| shell_to_mesh(&shell, color, &name)) }
let axis = TruckVec3::new(0.0, 0.0, 1.0);
let shell = builder::rsweep(&wire, axis_pt, axis, Rad(std::f64::consts::TAU));
CmdResult::CommitSolid3D {
mesh_fn: Box::new(move |name| shell_to_mesh(&shell, color, &name)),
}
}
// ── CYLINDER command ───────────────────────────────────────────────────────
@ -186,16 +255,27 @@ pub struct CylinderCommand {
}
#[derive(PartialEq)]
enum CylStep { Center, Radius, Height }
enum CylStep {
Center,
Radius,
Height,
}
impl CylinderCommand {
pub fn new(color: [f32; 4]) -> Self {
Self { step: CylStep::Center, center: Vec3::ZERO, radius: 1.0, color }
Self {
step: CylStep::Center,
center: Vec3::ZERO,
radius: 1.0,
color,
}
}
}
impl CadCommand for CylinderCommand {
fn name(&self) -> &'static str { "CYLINDER" }
fn name(&self) -> &'static str {
"CYLINDER"
}
fn prompt(&self) -> String {
match self.step {
CylStep::Center => "CYLINDER Center of base:".into(),
@ -205,37 +285,54 @@ impl CadCommand for CylinderCommand {
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
match self.step {
CylStep::Center => { self.center = pt; self.step = CylStep::Radius; CmdResult::NeedPoint }
CylStep::Center => {
self.center = pt;
self.step = CylStep::Radius;
CmdResult::NeedPoint
}
CylStep::Radius => {
self.radius = (pt - self.center).length().max(1e-4);
self.step = CylStep::Height;
CmdResult::NeedPoint
}
CylStep::Height => commit_cylinder(self.center, self.radius, (pt.y - self.center.y).abs().max(1e-4), self.color),
CylStep::Height => commit_cylinder(
self.center,
self.radius,
(pt.y - self.center.y).abs().max(1e-4),
self.color,
),
}
}
fn wants_text_input(&self) -> bool { matches!(self.step, CylStep::Radius | CylStep::Height) }
fn wants_text_input(&self) -> bool {
matches!(self.step, CylStep::Radius | CylStep::Height)
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let v = text.trim().parse::<f32>().ok().filter(|&v| v > 1e-6)?;
match self.step {
CylStep::Radius => { self.radius = v; self.step = CylStep::Height; Some(CmdResult::NeedPoint) }
CylStep::Radius => {
self.radius = v;
self.step = CylStep::Height;
Some(CmdResult::NeedPoint)
}
CylStep::Height => Some(commit_cylinder(self.center, self.radius, v, self.color)),
_ => None,
}
}
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
fn commit_cylinder(center: Vec3, radius: f32, height: f32, color: [f32; 4]) -> CmdResult {
let cx = center.x as f64;
let cy = center.z as f64;
let cz = center.y as f64;
let r = radius as f64;
let h = height as f64;
let r = radius as f64;
let h = height as f64;
// Build circle face at z=cz, sweep upward.
let right = builder::vertex(Point3::new(cx + r, cy, cz));
let left = builder::vertex(Point3::new(cx - r, cy, cz));
let left = builder::vertex(Point3::new(cx - r, cy, cz));
let top_t = Point3::new(cx, cy + r, cz);
let bot_t = Point3::new(cx, cy - r, cz);
let upper = builder::circle_arc(&right, &left, top_t);
@ -246,7 +343,9 @@ fn commit_cylinder(center: Vec3, radius: f32, height: f32, color: [f32; 4]) -> C
Err(_) => return CmdResult::Cancel,
};
let solid = builder::tsweep(&face, TruckVec3::new(0.0, 0.0, h));
CmdResult::CommitSolid3D { mesh_fn: Box::new(move |name| solid_to_mesh(&solid, color, &name)) }
CmdResult::CommitSolid3D {
mesh_fn: Box::new(move |name| solid_to_mesh(&solid, color, &name)),
}
}
// ── EXTRUDE command ────────────────────────────────────────────────────────
@ -258,41 +357,69 @@ pub struct ExtrudeCommand {
}
#[derive(PartialEq)]
enum ExtrudeStep { Pick, Height }
enum ExtrudeStep {
Pick,
Height,
}
impl ExtrudeCommand {
pub fn new(color: [f32; 4]) -> Self {
Self { step: ExtrudeStep::Pick, target_handle: acadrust::Handle::NULL, color }
Self {
step: ExtrudeStep::Pick,
target_handle: acadrust::Handle::NULL,
color,
}
}
}
impl CadCommand for ExtrudeCommand {
fn name(&self) -> &'static str { "EXTRUDE" }
fn name(&self) -> &'static str {
"EXTRUDE"
}
fn prompt(&self) -> String {
match self.step {
ExtrudeStep::Pick => "EXTRUDE Select closed profile (Circle, LwPolyline…):".into(),
ExtrudeStep::Pick => "EXTRUDE Select closed profile (Circle, LwPolyline…):".into(),
ExtrudeStep::Height => "EXTRUDE Height:".into(),
}
}
fn needs_entity_pick(&self) -> bool { self.step == ExtrudeStep::Pick }
fn needs_entity_pick(&self) -> bool {
self.step == ExtrudeStep::Pick
}
fn on_entity_pick(&mut self, handle: acadrust::Handle, _pt: Vec3) -> CmdResult {
if handle.is_null() { return CmdResult::NeedPoint; }
if handle.is_null() {
return CmdResult::NeedPoint;
}
self.target_handle = handle;
self.step = ExtrudeStep::Height;
CmdResult::NeedPoint
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
if self.step == ExtrudeStep::Height {
return CmdResult::ExtrudeEntity { handle: self.target_handle, height: pt.y.abs().max(1e-4), color: self.color };
return CmdResult::ExtrudeEntity {
handle: self.target_handle,
height: pt.y.abs().max(1e-4),
color: self.color,
};
}
CmdResult::NeedPoint
}
fn wants_text_input(&self) -> bool { self.step == ExtrudeStep::Height }
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
text.trim().parse::<f32>().ok().filter(|&h| h.abs() > 1e-6)
.map(|h| CmdResult::ExtrudeEntity { handle: self.target_handle, height: h.abs(), color: self.color })
fn wants_text_input(&self) -> bool {
self.step == ExtrudeStep::Height
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
text.trim()
.parse::<f32>()
.ok()
.filter(|&h| h.abs() > 1e-6)
.map(|h| CmdResult::ExtrudeEntity {
handle: self.target_handle,
height: h.abs(),
color: self.color,
})
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
}
// ── REVOLVE command ────────────────────────────────────────────────────────
@ -306,7 +433,12 @@ pub struct RevolveCommand {
}
#[derive(PartialEq)]
enum RevolveStep { Pick, AxisStart, AxisEnd, Angle }
enum RevolveStep {
Pick,
AxisStart,
AxisEnd,
Angle,
}
impl RevolveCommand {
pub fn new(color: [f32; 4]) -> Self {
@ -321,38 +453,64 @@ impl RevolveCommand {
}
impl CadCommand for RevolveCommand {
fn name(&self) -> &'static str { "REVOLVE" }
fn name(&self) -> &'static str {
"REVOLVE"
}
fn prompt(&self) -> String {
match self.step {
RevolveStep::Pick => "REVOLVE Select profile:".into(),
RevolveStep::Pick => "REVOLVE Select profile:".into(),
RevolveStep::AxisStart => "REVOLVE Axis start point:".into(),
RevolveStep::AxisEnd => "REVOLVE Axis end point:".into(),
RevolveStep::Angle => "REVOLVE Angle of revolution <360>:".into(),
RevolveStep::AxisEnd => "REVOLVE Axis end point:".into(),
RevolveStep::Angle => "REVOLVE Angle of revolution <360>:".into(),
}
}
fn needs_entity_pick(&self) -> bool { self.step == RevolveStep::Pick }
fn needs_entity_pick(&self) -> bool {
self.step == RevolveStep::Pick
}
fn on_entity_pick(&mut self, handle: acadrust::Handle, _pt: Vec3) -> CmdResult {
if handle.is_null() { return CmdResult::NeedPoint; }
if handle.is_null() {
return CmdResult::NeedPoint;
}
self.target_handle = handle;
self.step = RevolveStep::AxisStart;
CmdResult::NeedPoint
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
match self.step {
RevolveStep::AxisStart => { self.axis_start = pt; self.step = RevolveStep::AxisEnd; CmdResult::NeedPoint }
RevolveStep::AxisEnd => { self.axis_end = pt; self.step = RevolveStep::Angle; CmdResult::NeedPoint }
RevolveStep::Angle => self.make_revolve(360.0),
RevolveStep::AxisStart => {
self.axis_start = pt;
self.step = RevolveStep::AxisEnd;
CmdResult::NeedPoint
}
RevolveStep::AxisEnd => {
self.axis_end = pt;
self.step = RevolveStep::Angle;
CmdResult::NeedPoint
}
RevolveStep::Angle => self.make_revolve(360.0),
_ => CmdResult::NeedPoint,
}
}
fn wants_text_input(&self) -> bool { self.step == RevolveStep::Angle }
fn wants_text_input(&self) -> bool {
self.step == RevolveStep::Angle
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let angle = if text.trim().is_empty() { 360.0f32 }
else { text.trim().parse::<f32>().ok().filter(|&a| a.abs() > 1e-3)? };
let angle = if text.trim().is_empty() {
360.0f32
} else {
text.trim()
.parse::<f32>()
.ok()
.filter(|&a| a.abs() > 1e-3)?
};
Some(self.make_revolve(angle.abs()))
}
fn on_enter(&mut self) -> CmdResult {
if self.step == RevolveStep::Angle { self.make_revolve(360.0) } else { CmdResult::Cancel }
if self.step == RevolveStep::Angle {
self.make_revolve(360.0)
} else {
CmdResult::Cancel
}
}
}
@ -377,42 +535,57 @@ pub struct SweepCommand {
}
#[derive(PartialEq)]
enum SweepStep { PickProfile, PickPath }
enum SweepStep {
PickProfile,
PickPath,
}
impl SweepCommand {
pub fn new(color: [f32; 4]) -> Self {
Self { step: SweepStep::PickProfile, profile_handle: acadrust::Handle::NULL, color }
Self {
step: SweepStep::PickProfile,
profile_handle: acadrust::Handle::NULL,
color,
}
}
}
impl CadCommand for SweepCommand {
fn name(&self) -> &'static str { "SWEEP" }
fn name(&self) -> &'static str {
"SWEEP"
}
fn prompt(&self) -> String {
match self.step {
SweepStep::PickProfile => "SWEEP Select profile to sweep:".into(),
SweepStep::PickPath => "SWEEP Select path (Line, Arc, LwPolyline):".into(),
SweepStep::PickPath => "SWEEP Select path (Line, Arc, LwPolyline):".into(),
}
}
fn needs_entity_pick(&self) -> bool { true }
fn needs_entity_pick(&self) -> bool {
true
}
fn on_entity_pick(&mut self, handle: acadrust::Handle, _pt: Vec3) -> CmdResult {
if handle.is_null() { return CmdResult::NeedPoint; }
if handle.is_null() {
return CmdResult::NeedPoint;
}
match self.step {
SweepStep::PickProfile => {
self.profile_handle = handle;
self.step = SweepStep::PickPath;
CmdResult::NeedPoint
}
SweepStep::PickPath => {
CmdResult::SweepEntity {
profile_handle: self.profile_handle,
path_handle: handle,
color: self.color,
}
}
SweepStep::PickPath => CmdResult::SweepEntity {
profile_handle: self.profile_handle,
path_handle: handle,
color: self.color,
},
}
}
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
}
}
// ── LOFT command ───────────────────────────────────────────────────────────
@ -424,36 +597,57 @@ pub struct LoftCommand {
impl LoftCommand {
pub fn new(color: [f32; 4]) -> Self {
Self { profiles: Vec::new(), color }
Self {
profiles: Vec::new(),
color,
}
}
}
impl CadCommand for LoftCommand {
fn name(&self) -> &'static str { "LOFT" }
fn name(&self) -> &'static str {
"LOFT"
}
fn prompt(&self) -> String {
if self.profiles.is_empty() {
"LOFT Select first cross-section:".into()
} else {
format!("LOFT Select next cross-section ({} selected, Enter to finish):", self.profiles.len())
format!(
"LOFT Select next cross-section ({} selected, Enter to finish):",
self.profiles.len()
)
}
}
fn needs_entity_pick(&self) -> bool { true }
fn needs_entity_pick(&self) -> bool {
true
}
fn on_entity_pick(&mut self, handle: acadrust::Handle, _pt: Vec3) -> CmdResult {
if handle.is_null() { return CmdResult::NeedPoint; }
if handle.is_null() {
return CmdResult::NeedPoint;
}
// Avoid duplicate picks.
if !self.profiles.contains(&handle) {
self.profiles.push(handle);
}
CmdResult::NeedPoint
}
fn on_point(&mut self, _pt: Vec3) -> CmdResult { CmdResult::NeedPoint }
fn wants_text_input(&self) -> bool { self.profiles.len() >= 2 }
fn on_text_input(&mut self, _text: &str) -> Option<CmdResult> { None }
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
CmdResult::NeedPoint
}
fn wants_text_input(&self) -> bool {
self.profiles.len() >= 2
}
fn on_text_input(&mut self, _text: &str) -> Option<CmdResult> {
None
}
fn on_enter(&mut self) -> CmdResult {
if self.profiles.len() < 2 {
CmdResult::Cancel
} else {
CmdResult::LoftEntities { handles: self.profiles.clone(), color: self.color }
CmdResult::LoftEntities {
handles: self.profiles.clone(),
color: self.color,
}
}
}
}

View file

@ -1,7 +1,13 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
#[allow(dead_code)]
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/underlay_frames.svg"));
pub const ICON: IconKind =
IconKind::Svg(include_bytes!("../../../assets/icons/underlay_frames.svg"));
#[allow(dead_code)]
pub fn tool() -> ToolDef {
ToolDef { id: "FRAMES", label: "Frames", icon: ICON, event: ModuleEvent::Command("FRAMES".to_string()) }
ToolDef {
id: "FRAMES",
label: "Frames",
icon: ICON,
event: ModuleEvent::Command("FRAMES".to_string()),
}
}

View file

@ -1,5 +1,11 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/underlay_layers.svg"));
pub const ICON: IconKind =
IconKind::Svg(include_bytes!("../../../assets/icons/underlay_layers.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "UNDERLAYLAYERS", label: "Underlay\nLayers", icon: ICON, event: ModuleEvent::Command("UNDERLAYLAYERS".to_string()) }
ToolDef {
id: "UNDERLAYLAYERS",
label: "Underlay\nLayers",
icon: ICON,
event: ModuleEvent::Command("UNDERLAYLAYERS".to_string()),
}
}

View file

@ -22,10 +22,7 @@ pub fn tool() -> ToolDef {
/// extracted into model space.
///
/// Returns `Err` if the block is not found or has no entities.
pub fn extract_block_to_doc(
src: &CadDocument,
block_name: &str,
) -> Result<CadDocument, String> {
pub fn extract_block_to_doc(src: &CadDocument, block_name: &str) -> Result<CadDocument, String> {
let br = src
.block_records
.get(block_name)
@ -62,7 +59,9 @@ pub fn extract_block_to_doc(
}
if out.entities().count() == 0 {
return Err(format!("Block \"{block_name}\" produced no exportable entities."));
return Err(format!(
"Block \"{block_name}\" produced no exportable entities."
));
}
Ok(out)

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/xadjust.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "ADJUST", label: "Adjust", icon: ICON, event: ModuleEvent::Command("ADJUST".to_string()) }
ToolDef {
id: "ADJUST",
label: "Adjust",
icon: ICON,
event: ModuleEvent::Command("ADJUST".to_string()),
}
}

View file

@ -98,7 +98,10 @@ impl CadCommand for XAttachCommand {
fn on_point(&mut self, pt: Vec3) -> CmdResult {
match &self.step {
Step::FilePath => CmdResult::NeedPoint,
Step::InsertionPoint { path: _, block_name } => {
Step::InsertionPoint {
path: _,
block_name,
} => {
// We return the INSERT entity; the command handler in
// commands.rs will call `prepare_xref_block` on the scene
// before committing.
@ -163,7 +166,9 @@ pub fn prepare_xref_block(scene: &mut Scene, path: &str) -> String {
// Create BLOCK entity.
let b = Block::new(&block_name, Vector3::zero()).with_xref_path(path);
let _ = scene.document.add_entity(EntityType::Block(b));
let _ = scene.document.add_entity(EntityType::BlockEnd(BlockEnd::new()));
let _ = scene
.document
.add_entity(EntityType::BlockEnd(BlockEnd::new()));
// Resolve the XREF content immediately.
let path_buf = std::path::PathBuf::from(path);

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/xclip.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "XCLIP", label: "Clip", icon: ICON, event: ModuleEvent::Command("XCLIP".to_string()) }
ToolDef {
id: "XCLIP",
label: "Clip",
icon: ICON,
event: ModuleEvent::Command("XCLIP".to_string()),
}
}

View file

@ -53,7 +53,11 @@ impl CadCommand for VplayerCommand {
let op = tokens[0].to_uppercase();
// Check for ALL keyword: "F ALL layer1 layer2" or "T ALL layer1"
let (all_viewports, layer_start) = if tokens.get(1).map(|s| s.to_uppercase().as_str() == "ALL").unwrap_or(false) {
let (all_viewports, layer_start) = if tokens
.get(1)
.map(|s| s.to_uppercase().as_str() == "ALL")
.unwrap_or(false)
{
(true, 2)
} else {
(false, 1)
@ -69,7 +73,11 @@ impl CadCommand for VplayerCommand {
}
// Handle::NULL signals "apply to all viewports" in cmd_result.rs
let vp_handle = if all_viewports { acadrust::Handle::NULL } else { self.vp_handle };
let vp_handle = if all_viewports {
acadrust::Handle::NULL
} else {
self.vp_handle
};
match op.as_str() {
"F" | "FREEZE" => Some(CmdResult::VpLayerUpdate {

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/audit.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "AUDIT", label: "Audit", icon: ICON, event: ModuleEvent::Command("AUDIT".to_string()) }
ToolDef {
id: "AUDIT",
label: "Audit",
icon: ICON,
event: ModuleEvent::Command("AUDIT".to_string()),
}
}

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/cui_export.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "CUIEXPORT", label: "Export", icon: ICON, event: ModuleEvent::Command("CUIEXPORT".to_string()) }
ToolDef {
id: "CUIEXPORT",
label: "Export",
icon: ICON,
event: ModuleEvent::Command("CUIEXPORT".to_string()),
}
}

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/cui_import.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "CUIIMPORT", label: "Import", icon: ICON, event: ModuleEvent::Command("CUIIMPORT".to_string()) }
ToolDef {
id: "CUIIMPORT",
label: "Import",
icon: ICON,
event: ModuleEvent::Command("CUIIMPORT".to_string()),
}
}

View file

@ -3,5 +3,10 @@ use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/edit_aliases.svg"));
#[allow(dead_code)]
pub fn tool() -> ToolDef {
ToolDef { id: "ALIASEDIT", label: "Edit Aliases", icon: ICON, event: ModuleEvent::Command("ALIASEDIT".to_string()) }
ToolDef {
id: "ALIASEDIT",
label: "Edit Aliases",
icon: ICON,
event: ModuleEvent::Command("ALIASEDIT".to_string()),
}
}

View file

@ -1,5 +1,12 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/find_nonpurgeable.svg"));
pub const ICON: IconKind = IconKind::Svg(include_bytes!(
"../../../assets/icons/find_nonpurgeable.svg"
));
pub fn tool() -> ToolDef {
ToolDef { id: "FINDNONPURGEABLE", label: "Find Non-\nPurgeable Items", icon: ICON, event: ModuleEvent::Command("FINDNONPURGEABLE".to_string()) }
ToolDef {
id: "FINDNONPURGEABLE",
label: "Find Non-\nPurgeable Items",
icon: ICON,
event: ModuleEvent::Command("FINDNONPURGEABLE".to_string()),
}
}

View file

@ -32,17 +32,33 @@ impl CadModule for ManageModule {
RibbonItem::LargeTool(crate::modules::ToolDef {
id: "TOOLPALETTES",
label: "Tool\nPalettes",
icon: IconKind::Svg(include_bytes!("../../../assets/icons/tool_palettes.svg")),
icon: IconKind::Svg(include_bytes!(
"../../../assets/icons/tool_palettes.svg"
)),
event: crate::modules::ModuleEvent::Command("TOOLPALETTES".to_string()),
}),
RibbonItem::Tool(cui_import::tool()),
RibbonItem::Tool(cui_export::tool()),
RibbonItem::Dropdown {
id: "ALIASEDIT_DROPDOWN",
icon: IconKind::Svg(include_bytes!("../../../assets/icons/edit_aliases.svg")),
icon: IconKind::Svg(include_bytes!(
"../../../assets/icons/edit_aliases.svg"
)),
items: vec![
("ALIASEDIT", "Edit Aliases", IconKind::Svg(include_bytes!("../../../assets/icons/edit_aliases.svg"))),
("CUILOAD", "Load Partial CUI", IconKind::Svg(include_bytes!("../../../assets/icons/cui_import.svg"))),
(
"ALIASEDIT",
"Edit Aliases",
IconKind::Svg(include_bytes!(
"../../../assets/icons/edit_aliases.svg"
)),
),
(
"CUILOAD",
"Load Partial CUI",
IconKind::Svg(include_bytes!(
"../../../assets/icons/cui_import.svg"
)),
),
],
default: "ALIASEDIT",
},

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/overkill.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "OVERKILL", label: "Overkill", icon: ICON, event: ModuleEvent::Command("OVERKILL".to_string()) }
ToolDef {
id: "OVERKILL",
label: "Overkill",
icon: ICON,
event: ModuleEvent::Command("OVERKILL".to_string()),
}
}

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/purge.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "PURGE", label: "Purge", icon: ICON, event: ModuleEvent::Command("PURGE".to_string()) }
ToolDef {
id: "PURGE",
label: "Purge",
icon: ICON,
event: ModuleEvent::Command("PURGE".to_string()),
}
}

View file

@ -1,5 +1,11 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/user_interface.svg"));
pub const ICON: IconKind =
IconKind::Svg(include_bytes!("../../../assets/icons/user_interface.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "CUI", label: "User\nInterface", icon: ICON, event: ModuleEvent::Command("CUI".to_string()) }
ToolDef {
id: "CUI",
label: "User\nInterface",
icon: ICON,
event: ModuleEvent::Command("CUI".to_string()),
}
}

View file

@ -77,7 +77,10 @@ pub enum RibbonItem {
/// Layer combo + two rows of small tools below.
/// row2: operates on the layer of a selected object (off/freeze/lock/make-current)
/// row3: all-layers operations + match (on/thaw/unlock/match)
LayerComboGroup { row2: Vec<ToolDef>, row3: Vec<ToolDef> },
LayerComboGroup {
row2: Vec<ToolDef>,
row3: Vec<ToolDef>,
},
/// Match Properties (large button) + Color / Linetype / Lineweight combos on the right.
PropertiesGroup { match_prop: ToolDef },
/// A style selector combobox (text / dim / mleader / table style) with

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/cascade.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "CASCADE", label: "Cascade", icon: ICON, event: ModuleEvent::Command("CASCADE".to_string()) }
ToolDef {
id: "CASCADE",
label: "Cascade",
icon: ICON,
event: ModuleEvent::Command("CASCADE".to_string()),
}
}

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/file_tabs.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "FILETAB", label: "File\nTabs", icon: ICON, event: ModuleEvent::Command("FILETAB".to_string()) }
ToolDef {
id: "FILETAB",
label: "File\nTabs",
icon: ICON,
event: ModuleEvent::Command("FILETAB".to_string()),
}
}

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/hidden.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "HIDDENLINE", label: "Hidden", icon: ICON, event: ModuleEvent::Command("HIDDENLINE".to_string()) }
ToolDef {
id: "HIDDENLINE",
label: "Hidden",
icon: ICON,
event: ModuleEvent::Command("HIDDENLINE".to_string()),
}
}

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/layout_tabs.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "LAYOUTTAB", label: "Layout\nTabs", icon: ICON, event: ModuleEvent::Command("LAYOUTTAB".to_string()) }
ToolDef {
id: "LAYOUTTAB",
label: "Layout\nTabs",
icon: ICON,
event: ModuleEvent::Command("LAYOUTTAB".to_string()),
}
}

View file

@ -9,6 +9,7 @@ mod orbit;
mod ortho;
mod pan;
mod persp;
pub mod plot_window;
mod properties_palette;
mod sheetset;
mod solid;
@ -16,11 +17,11 @@ mod tile_horiz;
mod tile_vert;
mod tool_palettes;
mod ucs_icon;
mod viewcube;
mod view_front;
mod view_iso;
mod view_right;
mod view_top;
mod viewcube;
mod vports_config;
mod vports_join;
mod vports_named;
@ -30,7 +31,6 @@ mod xray;
mod zoom_ext;
mod zoom_in;
mod zoom_out;
pub mod plot_window;
pub mod zoom_window;
use crate::modules::{CadModule, RibbonGroup, RibbonItem};
@ -82,20 +82,18 @@ impl CadModule for ViewModule {
// WIREFRAME and SOLID ids are special-cased in ribbon.rs
// for toggle-state highlighting based on Ribbon::wireframe.
title: "Visual Style",
tools: vec![
RibbonItem::LargeDropdown {
id: "VISUAL_STYLE",
label: "Visual\nStyle",
icon: wireframe::tool().icon,
items: vec![
("WIREFRAME", "Wireframe", wireframe::tool().icon),
("SOLID", "Shaded", solid::tool().icon),
("HIDDEN", "Hidden", hidden::tool().icon),
("XRAY", "X-Ray", xray::tool().icon),
],
default: "WIREFRAME",
},
],
tools: vec![RibbonItem::LargeDropdown {
id: "VISUAL_STYLE",
label: "Visual\nStyle",
icon: wireframe::tool().icon,
items: vec![
("WIREFRAME", "Wireframe", wireframe::tool().icon),
("SOLID", "Shaded", solid::tool().icon),
("HIDDEN", "Hidden", hidden::tool().icon),
("XRAY", "X-Ray", xray::tool().icon),
],
default: "WIREFRAME",
}],
},
// ── Projection ────────────────────────────────────────────────────
RibbonGroup {

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/navbar.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "NAVBAR", label: "Navigation\nBar", icon: ICON, event: ModuleEvent::Command("NAVBAR".to_string()) }
ToolDef {
id: "NAVBAR",
label: "Navigation\nBar",
icon: ICON,
event: ModuleEvent::Command("NAVBAR".to_string()),
}
}

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/orbit.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "3DORBIT", label: "3D Orbit", icon: ICON, event: ModuleEvent::Command("3DORBIT".to_string()) }
ToolDef {
id: "3DORBIT",
label: "3D Orbit",
icon: ICON,
event: ModuleEvent::Command("3DORBIT".to_string()),
}
}

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/ortho.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "ORTHO", label: "Ortho", icon: ICON, event: ModuleEvent::Command("ORTHO".into()) }
ToolDef {
id: "ORTHO",
label: "Ortho",
icon: ICON,
event: ModuleEvent::Command("ORTHO".into()),
}
}

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/pan.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "PAN", label: "Pan", icon: ICON, event: ModuleEvent::Command("PAN".to_string()) }
ToolDef {
id: "PAN",
label: "Pan",
icon: ICON,
event: ModuleEvent::Command("PAN".to_string()),
}
}

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/persp.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "PERSP", label: "Persp", icon: ICON, event: ModuleEvent::Command("PERSP".into()) }
ToolDef {
id: "PERSP",
label: "Persp",
icon: ICON,
event: ModuleEvent::Command("PERSP".into()),
}
}

View file

@ -18,7 +18,9 @@ impl PlotWindowCommand {
}
impl CadCommand for PlotWindowCommand {
fn name(&self) -> &'static str { "PLOTWINDOW" }
fn name(&self) -> &'static str {
"PLOTWINDOW"
}
fn prompt(&self) -> String {
if self.p1.is_none() {
@ -48,10 +50,14 @@ impl CadCommand for PlotWindowCommand {
Some(WireModel {
name: "plotwindow_preview".into(),
points: vec![
[p1.x, p1.y, p1.z], [pt.x, p1.y, p1.z],
[pt.x, p1.y, p1.z], [pt.x, pt.y, pt.z],
[pt.x, pt.y, pt.z], [p1.x, pt.y, pt.z],
[p1.x, pt.y, pt.z], [p1.x, p1.y, p1.z],
[p1.x, p1.y, p1.z],
[pt.x, p1.y, p1.z],
[pt.x, p1.y, p1.z],
[pt.x, pt.y, pt.z],
[pt.x, pt.y, pt.z],
[p1.x, pt.y, pt.z],
[p1.x, pt.y, pt.z],
[p1.x, p1.y, p1.z],
],
color: WireModel::CYAN,
selected: false,

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/properties.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "PROPERTIES", label: "Properties", icon: ICON, event: ModuleEvent::Command("PROPERTIES".to_string()) }
ToolDef {
id: "PROPERTIES",
label: "Properties",
icon: ICON,
event: ModuleEvent::Command("PROPERTIES".to_string()),
}
}

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/sheetset.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "SHEETSET", label: "Sheet Set\nManager", icon: ICON, event: ModuleEvent::Command("SHEETSET".to_string()) }
ToolDef {
id: "SHEETSET",
label: "Sheet Set\nManager",
icon: ICON,
event: ModuleEvent::Command("SHEETSET".to_string()),
}
}

View file

@ -4,5 +4,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/solid.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "SOLID", label: "Solid", icon: ICON, event: ModuleEvent::SetWireframe(false) }
ToolDef {
id: "SOLID",
label: "Solid",
icon: ICON,
event: ModuleEvent::SetWireframe(false),
}
}

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/tile_horiz.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "HORIZONTAL", label: "Tile\nHoriz.", icon: ICON, event: ModuleEvent::Command("HORIZONTAL".to_string()) }
ToolDef {
id: "HORIZONTAL",
label: "Tile\nHoriz.",
icon: ICON,
event: ModuleEvent::Command("HORIZONTAL".to_string()),
}
}

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/tile_vert.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "VERTICAL", label: "Tile\nVert.", icon: ICON, event: ModuleEvent::Command("VERTICAL".to_string()) }
ToolDef {
id: "VERTICAL",
label: "Tile\nVert.",
icon: ICON,
event: ModuleEvent::Command("VERTICAL".to_string()),
}
}

View file

@ -1,5 +1,10 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/tool_palettes.svg"));
pub fn tool() -> ToolDef {
ToolDef { id: "TOOLPALETTES", label: "Tool\nPalettes", icon: ICON, event: ModuleEvent::Command("TOOLPALETTES".to_string()) }
ToolDef {
id: "TOOLPALETTES",
label: "Tool\nPalettes",
icon: ICON,
event: ModuleEvent::Command("TOOLPALETTES".to_string()),
}
}

Some files were not shown because too many files have changed in this diff Show more