forked from 1b-agent/opencadstudio-upstream
fix(dimension): repair baseline workflows
This commit is contained in:
1 parent
45e84edae4
commit
1d58082e0a
14 files changed
+640
-450
No files matched your search
@@ -856,7 +856,7 @@ impl OpenCADStudio {
|
||||
.infer_dimension_sources(handle);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.attach_dimension_association(handle, sources.to_vec());
|
||||
.attach_dimension_association(handle, sources);
|
||||
}
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
@@ -1269,7 +1269,7 @@ impl OpenCADStudio {
|
||||
.infer_dimension_sources(handle);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.attach_dimension_association(handle, sources.to_vec());
|
||||
.attach_dimension_association(handle, sources);
|
||||
}
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
@@ -1288,6 +1288,7 @@ impl OpenCADStudio {
|
||||
mut entity,
|
||||
association,
|
||||
preserve_base_style,
|
||||
continue_command,
|
||||
} => {
|
||||
let label = self.history_label_from_active_cmd(i, "DIMENSION");
|
||||
let association_mode = self.tabs[i]
|
||||
@@ -1378,8 +1379,6 @@ impl OpenCADStudio {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.infer_dimension_sources(handle)
|
||||
.into_iter()
|
||||
.collect()
|
||||
},
|
||||
|source| {
|
||||
if single_source_dimension {
|
||||
@@ -1414,12 +1413,22 @@ impl OpenCADStudio {
|
||||
};
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
if continue_command {
|
||||
if let Some(prompt) = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.map(|cmd| cmd.prompt())
|
||||
{
|
||||
self.command_line.push_info(&prompt);
|
||||
}
|
||||
} else {
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
}
|
||||
}
|
||||
CmdResult::CommitSolid {
|
||||
entity,
|
||||
|
||||
+14
-73
@@ -646,13 +646,19 @@ impl OpenCADStudio {
|
||||
|
||||
"DIMCONTINUE" => {
|
||||
use crate::modules::annotate::dim_continue::DimContinueCommand;
|
||||
let cmd = if let Some((p1, p2, dp, rot, trot)) =
|
||||
find_last_linear_dim(&self.tabs[i].scene)
|
||||
{
|
||||
DimContinueCommand::from_base(p1, p2, dp, rot, trot)
|
||||
} else {
|
||||
DimContinueCommand::new()
|
||||
};
|
||||
let scene = &self.tabs[i].scene;
|
||||
let preserve_base_style = self.dimension_continue_mode == 1;
|
||||
let cmd = scene
|
||||
.last_created_dimension
|
||||
.filter(|handle| scene.entity_belongs_to_active_space(*handle))
|
||||
.and_then(|handle| scene.document.get_entity(handle))
|
||||
.and_then(|entity| match entity {
|
||||
acadrust::EntityType::Dimension(dimension) => Some(
|
||||
DimContinueCommand::from_dimension(dimension, preserve_base_style),
|
||||
),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_else(|| DimContinueCommand::new(preserve_base_style));
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
}
|
||||
@@ -662,6 +668,7 @@ impl OpenCADStudio {
|
||||
let scene = &self.tabs[i].scene;
|
||||
let recent = scene
|
||||
.last_created_dimension
|
||||
.filter(|handle| scene.entity_belongs_to_active_space(*handle))
|
||||
.and_then(|handle| scene.document.get_entity(handle))
|
||||
.cloned();
|
||||
let dimdli_by_style = scene
|
||||
@@ -1383,72 +1390,6 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the last placed linear or aligned dimension in the document.
|
||||
/// Returns `(first_point, second_point, definition_point, rotation_rad)` in world-space.
|
||||
fn find_last_linear_dim(
|
||||
scene: &crate::scene::Scene,
|
||||
) -> Option<(glam::Vec3, glam::Vec3, glam::Vec3, f64, f64)> {
|
||||
use acadrust::entities::Dimension;
|
||||
let mut best_handle: u64 = 0;
|
||||
let mut result: Option<(glam::Vec3, glam::Vec3, glam::Vec3, f64, f64)> = None;
|
||||
|
||||
for entity in scene.document.entities() {
|
||||
if let acadrust::EntityType::Dimension(dim) = entity {
|
||||
let h = entity.common().handle.value();
|
||||
if h <= best_handle {
|
||||
continue;
|
||||
}
|
||||
let item = match dim {
|
||||
Dimension::Linear(d) => {
|
||||
let p1 = glam::Vec3::new(
|
||||
d.first_point.x as f32,
|
||||
d.first_point.y as f32,
|
||||
d.first_point.z as f32,
|
||||
);
|
||||
let p2 = glam::Vec3::new(
|
||||
d.second_point.x as f32,
|
||||
d.second_point.y as f32,
|
||||
d.second_point.z as f32,
|
||||
);
|
||||
let dp = glam::Vec3::new(
|
||||
d.base.definition_point.x as f32,
|
||||
d.base.definition_point.y as f32,
|
||||
d.base.definition_point.z as f32,
|
||||
);
|
||||
Some((p1, p2, dp, d.rotation, d.base.text_rotation))
|
||||
}
|
||||
Dimension::Aligned(d) => {
|
||||
let p1 = glam::Vec3::new(
|
||||
d.first_point.x as f32,
|
||||
d.first_point.y as f32,
|
||||
d.first_point.z as f32,
|
||||
);
|
||||
let p2 = glam::Vec3::new(
|
||||
d.second_point.x as f32,
|
||||
d.second_point.y as f32,
|
||||
d.second_point.z as f32,
|
||||
);
|
||||
let dp = glam::Vec3::new(
|
||||
d.base.definition_point.x as f32,
|
||||
d.base.definition_point.y as f32,
|
||||
d.base.definition_point.z as f32,
|
||||
);
|
||||
let dx = (d.second_point.x - d.first_point.x) as f32;
|
||||
let dy = (d.second_point.y - d.first_point.y) as f32;
|
||||
let rot = dy.atan2(dx) as f64;
|
||||
Some((p1, p2, dp, rot, d.base.text_rotation))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(data) = item {
|
||||
best_handle = h;
|
||||
result = Some(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Collect candidate dimension endpoints from an entity for QDIM — line ends,
|
||||
/// polyline vertices, arc endpoints — in world space.
|
||||
fn qdim_collect_points(e: &acadrust::EntityType, out: &mut Vec<glam::DVec3>) {
|
||||
|
||||
@@ -1251,6 +1251,8 @@ pub enum CmdResult {
|
||||
/// Retain the source dimension's layer and style instead of replacing
|
||||
/// them with the current creation defaults (DIMCONTINUEMODE=1).
|
||||
preserve_base_style: bool,
|
||||
/// Keep collecting points after the dimension is committed.
|
||||
continue_command: bool,
|
||||
},
|
||||
/// Commit a Model-tab 3D solid: the acadrust entity (for selection /
|
||||
/// persistence) plus its B-rep (cached for boolean ops + shaded
|
||||
|
||||
@@ -136,6 +136,7 @@ impl CadCommand for AlignedDimensionCommand {
|
||||
entity: self.plane.place_entity(EntityType::Dimension(Dimension::Aligned(dim))),
|
||||
association: DimensionAssociationInput::Infer(self.source_handle),
|
||||
preserve_base_style: false,
|
||||
continue_command: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,6 +235,7 @@ impl AngularDimensionCommand {
|
||||
entity,
|
||||
association: DimensionAssociationInput::Explicit(self.source_refs.clone()),
|
||||
preserve_base_style: false,
|
||||
continue_command: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -266,6 +266,7 @@ impl ArcLengthDimensionCommand {
|
||||
entity: plane.place_entity(EntityType::Dimension(Dimension::Arc(dimension))),
|
||||
association,
|
||||
preserve_base_style: false,
|
||||
continue_command: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,7 @@ impl CadCommand for DiameterDimensionCommand {
|
||||
.place_entity(EntityType::Dimension(Dimension::Diameter(dim))),
|
||||
association: DimensionAssociationInput::Infer(self.source_handle),
|
||||
preserve_base_style: false,
|
||||
continue_command: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Repeating baseline dimensions from a session base or an explicitly picked base.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::f64::consts::PI;
|
||||
|
||||
use acadrust::entities::{
|
||||
Dimension, DimensionAligned, DimensionAngular2Ln, DimensionAngular3Pt, DimensionBase,
|
||||
@@ -8,6 +7,10 @@ use acadrust::entities::{
|
||||
};
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::{EntityType, Handle};
|
||||
use cadkernel::geom2d::{
|
||||
arc_span, intersect, nearest_of, Arc, Curve, Line, Tolerance, Transform, Vec2, XLine,
|
||||
};
|
||||
use cadkernel::space::Plane;
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, DimensionAssociationInput};
|
||||
@@ -56,35 +59,39 @@ impl SourceStyle {
|
||||
|
||||
enum BaselineKind {
|
||||
Linear {
|
||||
fixed: DVec3,
|
||||
fixed: Vec2,
|
||||
rotation: f64,
|
||||
aligned: bool,
|
||||
perpendicular: DVec3,
|
||||
perpendicular: Vec2,
|
||||
next_offset: f64,
|
||||
increment: f64,
|
||||
},
|
||||
Angular2Ln {
|
||||
source: DimensionAngular2Ln,
|
||||
vertex: DVec3,
|
||||
fixed_ray: DVec3,
|
||||
fixed_line: [Vec2; 2],
|
||||
fixed_is_first: bool,
|
||||
vertex: Vec2,
|
||||
fixed_ray: Vec2,
|
||||
next_radius: f64,
|
||||
increment: f64,
|
||||
},
|
||||
Angular3Pt {
|
||||
source: DimensionAngular3Pt,
|
||||
vertex: DVec3,
|
||||
fixed_ray: DVec3,
|
||||
vertex: Vec2,
|
||||
fixed_point: Vec2,
|
||||
fixed_ray: Vec2,
|
||||
next_radius: f64,
|
||||
increment: f64,
|
||||
},
|
||||
Ordinate {
|
||||
source: DimensionOrdinate,
|
||||
definition: Vec2,
|
||||
leader: Vec2,
|
||||
is_x: bool,
|
||||
},
|
||||
}
|
||||
|
||||
struct BaselineState {
|
||||
kind: BaselineKind,
|
||||
style: SourceStyle,
|
||||
plane: Plane,
|
||||
}
|
||||
|
||||
pub struct DimBaselineCommand {
|
||||
@@ -140,35 +147,59 @@ impl DimBaselineCommand {
|
||||
};
|
||||
let style = SourceStyle::from_base(dimension.base());
|
||||
let increment = self.effective_increment(&style.style_name);
|
||||
let plane = plane_from_normal(dimension.base().definition_point, style.normal);
|
||||
let pick = pick.and_then(|point| plane.project(point.to_array()).map(Vec2::from));
|
||||
let kind = match dimension {
|
||||
Dimension::Linear(source) => {
|
||||
let (fixed, _) = nearest_end(source.first_point, source.second_point, pick);
|
||||
let first = project(&plane, source.first_point);
|
||||
let second = project(&plane, source.second_point);
|
||||
let (fixed, _) = nearest_end(first, second, pick);
|
||||
linear_base(
|
||||
fixed,
|
||||
source.rotation,
|
||||
false,
|
||||
dv(source.base.definition_point),
|
||||
project(&plane, source.base.definition_point),
|
||||
increment,
|
||||
)
|
||||
}
|
||||
Dimension::Aligned(source) => {
|
||||
let (fixed, other) = nearest_end(source.first_point, source.second_point, pick);
|
||||
let first = project(&plane, source.first_point);
|
||||
let second = project(&plane, source.second_point);
|
||||
let (fixed, other) = nearest_end(first, second, pick);
|
||||
linear_base(
|
||||
fixed,
|
||||
(other - fixed).y.atan2((other - fixed).x),
|
||||
(other - fixed).angle(),
|
||||
true,
|
||||
dv(source.base.definition_point),
|
||||
project(&plane, source.base.definition_point),
|
||||
increment,
|
||||
)
|
||||
}
|
||||
Dimension::Angular2Ln(source) => {
|
||||
let vertex = angular2_vertex(source);
|
||||
let first = dv(source.second_point) - vertex;
|
||||
let second = dv(source.definition_point) - vertex;
|
||||
let fixed_ray = nearest_direction(first, second, vertex, pick);
|
||||
let radius = (dv(source.dimension_arc) - vertex).length().max(1.0e-9);
|
||||
let lines = [
|
||||
[
|
||||
project(&plane, source.first_point),
|
||||
project(&plane, source.second_point),
|
||||
],
|
||||
[
|
||||
project(&plane, source.angle_vertex),
|
||||
project(&plane, source.definition_point),
|
||||
],
|
||||
];
|
||||
let vertex = angular2_vertex(lines).unwrap_or(lines[1][0]);
|
||||
let fixed_index = nearest_line(lines, pick);
|
||||
let fixed_line = lines[fixed_index];
|
||||
let fixed_end = if fixed_index == 0 {
|
||||
lines[0][1]
|
||||
} else {
|
||||
lines[1][1]
|
||||
};
|
||||
let fixed_ray = fixed_end - vertex;
|
||||
let radius = project(&plane, source.dimension_arc)
|
||||
.distance(vertex)
|
||||
.max(1.0e-9);
|
||||
BaselineKind::Angular2Ln {
|
||||
source: source.clone(),
|
||||
fixed_line,
|
||||
fixed_is_first: fixed_index == 0,
|
||||
vertex,
|
||||
fixed_ray,
|
||||
next_radius: radius + increment,
|
||||
@@ -176,31 +207,42 @@ impl DimBaselineCommand {
|
||||
}
|
||||
}
|
||||
Dimension::Angular3Pt(source) => {
|
||||
let vertex = dv(source.angle_vertex);
|
||||
let first = dv(source.first_point) - vertex;
|
||||
let second = dv(source.second_point) - vertex;
|
||||
let fixed_ray = nearest_direction(first, second, vertex, pick);
|
||||
let radius = (dv(source.definition_point) - vertex).length().max(1.0e-9);
|
||||
let vertex = project(&plane, source.angle_vertex);
|
||||
let points = [
|
||||
project(&plane, source.first_point),
|
||||
project(&plane, source.second_point),
|
||||
];
|
||||
let fixed_index = nearest_line(
|
||||
[[vertex, points[0]], [vertex, points[1]]],
|
||||
pick,
|
||||
);
|
||||
let fixed_point = points[fixed_index];
|
||||
let radius = project(&plane, source.definition_point)
|
||||
.distance(vertex)
|
||||
.max(1.0e-9);
|
||||
BaselineKind::Angular3Pt {
|
||||
source: source.clone(),
|
||||
vertex,
|
||||
fixed_ray,
|
||||
fixed_point,
|
||||
fixed_ray: fixed_point - vertex,
|
||||
next_radius: radius + increment,
|
||||
increment,
|
||||
}
|
||||
}
|
||||
Dimension::Ordinate(source) => BaselineKind::Ordinate {
|
||||
source: source.clone(),
|
||||
definition: project(&plane, source.definition_point),
|
||||
leader: project(&plane, source.leader_endpoint),
|
||||
is_x: source.is_ordinate_type_x,
|
||||
},
|
||||
_ => return false,
|
||||
};
|
||||
self.base = Some(BaselineState { kind, style });
|
||||
self.base = Some(BaselineState { kind, style, plane });
|
||||
self.committed = 0;
|
||||
true
|
||||
}
|
||||
|
||||
fn build_dimension(&self, point: DVec3) -> Option<Dimension> {
|
||||
fn build_dimension(&self, point_world: DVec3) -> Option<Dimension> {
|
||||
let state = self.base.as_ref()?;
|
||||
let point = state.plane.project(point_world.to_array()).map(Vec2::from)?;
|
||||
let mut dimension = match &state.kind {
|
||||
BaselineKind::Linear {
|
||||
fixed,
|
||||
@@ -210,82 +252,93 @@ impl DimBaselineCommand {
|
||||
next_offset,
|
||||
..
|
||||
} => build_linear(
|
||||
state.plane,
|
||||
*fixed,
|
||||
point,
|
||||
*rotation,
|
||||
*aligned,
|
||||
*perpendicular,
|
||||
*next_offset,
|
||||
),
|
||||
)?,
|
||||
BaselineKind::Angular2Ln {
|
||||
source,
|
||||
fixed_line,
|
||||
fixed_is_first,
|
||||
vertex,
|
||||
fixed_ray,
|
||||
next_radius,
|
||||
..
|
||||
} => {
|
||||
let moving = point - *vertex;
|
||||
if moving.length_squared() <= 1.0e-18 {
|
||||
return None;
|
||||
}
|
||||
let mut result = source.clone();
|
||||
result.first_point = v3(*vertex);
|
||||
result.second_point = v3(*vertex + normalized_or(*fixed_ray, DVec3::X));
|
||||
result.angle_vertex = v3(*vertex);
|
||||
result.definition_point = v3(point);
|
||||
let (definition, text) =
|
||||
angular_definition_and_text(*vertex, *fixed_ray, moving, *next_radius);
|
||||
result.dimension_arc = v3(definition);
|
||||
angular_definition_and_text(*vertex, *fixed_ray, moving, *next_radius)?;
|
||||
let mut result = DimensionAngular2Ln::default();
|
||||
if *fixed_is_first {
|
||||
result.first_point = world(state.plane, fixed_line[0]);
|
||||
result.second_point = world(state.plane, fixed_line[1]);
|
||||
result.angle_vertex = world(state.plane, *vertex);
|
||||
result.definition_point = world(state.plane, point);
|
||||
} else {
|
||||
result.first_point = world(state.plane, *vertex);
|
||||
result.second_point = world(state.plane, point);
|
||||
result.angle_vertex = world(state.plane, fixed_line[0]);
|
||||
result.definition_point = world(state.plane, fixed_line[1]);
|
||||
}
|
||||
result.dimension_arc = world(state.plane, definition);
|
||||
result.base.definition_point = result.dimension_arc;
|
||||
result.base.text_middle_point = v3(text);
|
||||
result.base.text_middle_point = world(state.plane, text);
|
||||
result.base.insertion_point = result.base.text_middle_point;
|
||||
result.base.actual_measurement = result.measurement_degrees();
|
||||
Dimension::Angular2Ln(result)
|
||||
}
|
||||
BaselineKind::Angular3Pt {
|
||||
source,
|
||||
vertex,
|
||||
fixed_point,
|
||||
fixed_ray,
|
||||
next_radius,
|
||||
..
|
||||
} => {
|
||||
let moving = point - *vertex;
|
||||
if moving.length_squared() <= 1.0e-18 {
|
||||
return None;
|
||||
}
|
||||
let mut result = source.clone();
|
||||
result.angle_vertex = v3(*vertex);
|
||||
result.first_point = v3(*vertex + normalized_or(*fixed_ray, DVec3::X));
|
||||
result.second_point = v3(point);
|
||||
let (definition, text) =
|
||||
angular_definition_and_text(*vertex, *fixed_ray, moving, *next_radius);
|
||||
result.definition_point = v3(definition);
|
||||
angular_definition_and_text(*vertex, *fixed_ray, moving, *next_radius)?;
|
||||
let mut result = DimensionAngular3Pt::new(
|
||||
world(state.plane, *vertex),
|
||||
world(state.plane, *fixed_point),
|
||||
world(state.plane, point),
|
||||
);
|
||||
result.definition_point = world(state.plane, definition);
|
||||
result.base.definition_point = result.definition_point;
|
||||
result.base.text_middle_point = v3(text);
|
||||
result.base.text_middle_point = world(state.plane, text);
|
||||
result.base.insertion_point = result.base.text_middle_point;
|
||||
result.base.actual_measurement = result.measurement_degrees();
|
||||
Dimension::Angular3Pt(result)
|
||||
}
|
||||
BaselineKind::Ordinate { source } => {
|
||||
let source_leader = dv(source.leader_endpoint);
|
||||
let leader = if source.is_ordinate_type_x {
|
||||
DVec3::new(point.x, source_leader.y, source_leader.z)
|
||||
BaselineKind::Ordinate {
|
||||
definition,
|
||||
leader,
|
||||
is_x,
|
||||
} => {
|
||||
let to_dimension = Transform::rotation(-state.style.horizontal_direction);
|
||||
let from_dimension = Transform::rotation(state.style.horizontal_direction);
|
||||
let feature = Vec2::from(to_dimension.apply_point(point.into()));
|
||||
let source_leader = Vec2::from(to_dimension.apply_point((*leader).into()));
|
||||
let leader = if *is_x {
|
||||
Vec2::new(feature.x, source_leader.y)
|
||||
} else {
|
||||
DVec3::new(source_leader.x, point.y, source_leader.z)
|
||||
Vec2::new(source_leader.x, feature.y)
|
||||
};
|
||||
let leader = Vec2::from(from_dimension.apply_point(leader.into()));
|
||||
let mut result = DimensionOrdinate::new(
|
||||
v3(point),
|
||||
v3(leader),
|
||||
source.is_ordinate_type_x,
|
||||
world(state.plane, point),
|
||||
world(state.plane, leader),
|
||||
*is_x,
|
||||
);
|
||||
result.definition_point = source.definition_point;
|
||||
result.base.definition_point = source.base.definition_point;
|
||||
result.base.text_middle_point = v3(leader);
|
||||
result.definition_point = world(state.plane, *definition);
|
||||
result.base.definition_point = result.definition_point;
|
||||
result.base.text_middle_point = result.leader_endpoint;
|
||||
result.base.insertion_point = result.base.text_middle_point;
|
||||
Dimension::Ordinate(result)
|
||||
}
|
||||
};
|
||||
state.style.apply(dimension.base_mut());
|
||||
refresh_measurement(&mut dimension, state.plane);
|
||||
dimension.base_mut().common.handle = Handle::NULL;
|
||||
dimension.base_mut().block_name.clear();
|
||||
Some(dimension)
|
||||
@@ -389,6 +442,7 @@ impl CadCommand for DimBaselineCommand {
|
||||
entity: EntityType::Dimension(dimension),
|
||||
association: DimensionAssociationInput::Infer(None),
|
||||
preserve_base_style: self.preserve_base_style,
|
||||
continue_command: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,18 +480,19 @@ impl CadCommand for DimBaselineCommand {
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
|
||||
self.build_dimension(point).map(|dimension| preview_for_dimension(&dimension))
|
||||
self.build_dimension(point)
|
||||
.map(|dimension| preview_for_dimension(&dimension))
|
||||
}
|
||||
}
|
||||
|
||||
fn linear_base(
|
||||
fixed: DVec3,
|
||||
fixed: Vec2,
|
||||
rotation: f64,
|
||||
aligned: bool,
|
||||
definition: DVec3,
|
||||
definition: Vec2,
|
||||
increment: f64,
|
||||
) -> BaselineKind {
|
||||
let perpendicular = DVec3::new(-rotation.sin(), rotation.cos(), 0.0);
|
||||
let perpendicular = Vec2::new(-rotation.sin(), rotation.cos());
|
||||
let offset = (definition - fixed).dot(perpendicular);
|
||||
let increment = increment * if offset >= 0.0 { 1.0 } else { -1.0 };
|
||||
BaselineKind::Linear {
|
||||
@@ -451,39 +506,48 @@ fn linear_base(
|
||||
}
|
||||
|
||||
fn build_linear(
|
||||
fixed: DVec3,
|
||||
point: DVec3,
|
||||
plane: Plane,
|
||||
fixed: Vec2,
|
||||
point: Vec2,
|
||||
rotation: f64,
|
||||
aligned: bool,
|
||||
perpendicular: DVec3,
|
||||
source_perpendicular: Vec2,
|
||||
offset: f64,
|
||||
) -> Dimension {
|
||||
let target = fixed.dot(perpendicular) + offset;
|
||||
let first_line = fixed + perpendicular * (target - fixed.dot(perpendicular));
|
||||
let second_line = point + perpendicular * (target - point.dot(perpendicular));
|
||||
if aligned {
|
||||
let mut result = DimensionAligned::new(v3(fixed), v3(point));
|
||||
result.definition_point = v3(second_line);
|
||||
result.base.definition_point = v3(second_line);
|
||||
result.base.text_middle_point = v3((first_line + second_line) * 0.5);
|
||||
result.base.insertion_point = result.base.text_middle_point;
|
||||
result.base.actual_measurement = result.measurement();
|
||||
Dimension::Aligned(result)
|
||||
) -> Option<Dimension> {
|
||||
let (first_line, second_line) = if aligned {
|
||||
let mut perpendicular = (point - fixed).normalize()?.perpendicular();
|
||||
let source_side = source_perpendicular * offset.signum();
|
||||
if perpendicular.dot(source_side) < 0.0 {
|
||||
perpendicular = -perpendicular;
|
||||
}
|
||||
let distance = offset.abs();
|
||||
(fixed + perpendicular * distance, point + perpendicular * distance)
|
||||
} else {
|
||||
let mut result = DimensionLinear::new(v3(fixed), v3(point));
|
||||
result.rotation = rotation;
|
||||
result.definition_point = v3(second_line);
|
||||
result.base.definition_point = v3(second_line);
|
||||
result.base.text_middle_point = v3((first_line + second_line) * 0.5);
|
||||
let target = fixed.dot(source_perpendicular) + offset;
|
||||
(
|
||||
project_to_line(fixed, source_perpendicular, target),
|
||||
project_to_line(point, source_perpendicular, target),
|
||||
)
|
||||
};
|
||||
if aligned {
|
||||
let mut result = DimensionAligned::new(world(plane, fixed), world(plane, point));
|
||||
result.definition_point = world(plane, second_line);
|
||||
result.base.definition_point = result.definition_point;
|
||||
result.base.text_middle_point = world(plane, first_line.lerp(second_line, 0.5));
|
||||
result.base.insertion_point = result.base.text_middle_point;
|
||||
result.base.actual_measurement = result.measurement();
|
||||
Dimension::Linear(result)
|
||||
Some(Dimension::Aligned(result))
|
||||
} else {
|
||||
let mut result = DimensionLinear::new(world(plane, fixed), world(plane, point));
|
||||
result.rotation = rotation;
|
||||
result.definition_point = world(plane, second_line);
|
||||
result.base.definition_point = result.definition_point;
|
||||
result.base.text_middle_point = world(plane, first_line.lerp(second_line, 0.5));
|
||||
result.base.insertion_point = result.base.text_middle_point;
|
||||
Some(Dimension::Linear(result))
|
||||
}
|
||||
}
|
||||
|
||||
fn nearest_end(first: Vector3, second: Vector3, pick: Option<DVec3>) -> (DVec3, DVec3) {
|
||||
let first = dv(first);
|
||||
let second = dv(second);
|
||||
fn nearest_end(first: Vec2, second: Vec2, pick: Option<Vec2>) -> (Vec2, Vec2) {
|
||||
if pick.is_some_and(|point| point.distance_squared(second) < point.distance_squared(first)) {
|
||||
(second, first)
|
||||
} else {
|
||||
@@ -491,104 +555,156 @@ fn nearest_end(first: Vector3, second: Vector3, pick: Option<DVec3>) -> (DVec3,
|
||||
}
|
||||
}
|
||||
|
||||
fn nearest_direction(first: DVec3, second: DVec3, vertex: DVec3, pick: Option<DVec3>) -> DVec3 {
|
||||
if let Some(point) = pick {
|
||||
let direction = normalized_or(point - vertex, DVec3::X);
|
||||
if normalized_or(second, DVec3::Y).dot(direction)
|
||||
> normalized_or(first, DVec3::X).dot(direction)
|
||||
{
|
||||
return second;
|
||||
}
|
||||
}
|
||||
first
|
||||
fn nearest_line(lines: [[Vec2; 2]; 2], pick: Option<Vec2>) -> usize {
|
||||
let Some(pick) = pick else {
|
||||
return 0;
|
||||
};
|
||||
let curves = lines.map(|line| {
|
||||
Curve::Line(Line {
|
||||
start: line[0].into(),
|
||||
end: line[1].into(),
|
||||
})
|
||||
});
|
||||
nearest_of(curves.iter(), pick.into())
|
||||
.map(|(index, _)| index)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn angular2_vertex(source: &DimensionAngular2Ln) -> DVec3 {
|
||||
let first = dv(source.first_point);
|
||||
let first_direction = dv(source.second_point) - first;
|
||||
let second = dv(source.angle_vertex);
|
||||
let second_direction = dv(source.definition_point) - second;
|
||||
line_intersection(first, first_direction, second, second_direction).unwrap_or(second)
|
||||
}
|
||||
|
||||
fn line_intersection(a: DVec3, b: DVec3, c: DVec3, d: DVec3) -> Option<DVec3> {
|
||||
let cross = b.x * d.y - b.y * d.x;
|
||||
if cross.abs() <= 1.0e-12 {
|
||||
return None;
|
||||
}
|
||||
let delta = c - a;
|
||||
Some(a + b * ((delta.x * d.y - delta.y * d.x) / cross))
|
||||
fn angular2_vertex(lines: [[Vec2; 2]; 2]) -> Option<Vec2> {
|
||||
let first = Curve::XLine(XLine {
|
||||
base: lines[0][0].into(),
|
||||
direction: (lines[0][1] - lines[0][0]).into(),
|
||||
});
|
||||
let second = Curve::XLine(XLine {
|
||||
base: lines[1][0].into(),
|
||||
direction: (lines[1][1] - lines[1][0]).into(),
|
||||
});
|
||||
let scale = lines
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(Vec2::length)
|
||||
.fold(1.0, f64::max);
|
||||
intersect(&first, &second, Tolerance::new(scale * 1.0e-9))
|
||||
.first()
|
||||
.map(|crossing| Vec2::from(crossing.point))
|
||||
}
|
||||
|
||||
fn angular_definition_and_text(
|
||||
vertex: DVec3,
|
||||
first: DVec3,
|
||||
second: DVec3,
|
||||
vertex: Vec2,
|
||||
first: Vec2,
|
||||
second: Vec2,
|
||||
radius: f64,
|
||||
) -> (DVec3, DVec3) {
|
||||
let start = first.y.atan2(first.x);
|
||||
let mut sweep = second.y.atan2(second.x) - start;
|
||||
while sweep <= -std::f64::consts::PI {
|
||||
sweep += std::f64::consts::TAU;
|
||||
}
|
||||
while sweep > std::f64::consts::PI {
|
||||
sweep -= std::f64::consts::TAU;
|
||||
}
|
||||
let point_at = |fraction: f64| {
|
||||
let angle = start + sweep * fraction;
|
||||
vertex + DVec3::new(angle.cos(), angle.sin(), 0.0) * radius
|
||||
};
|
||||
(point_at(2.0 / 3.0), point_at(0.5))
|
||||
) -> Option<(Vec2, Vec2)> {
|
||||
let curve = angular_arc(vertex, first, second, radius)?;
|
||||
Some((
|
||||
Vec2::from(curve.point_at(2.0 / 3.0)),
|
||||
Vec2::from(curve.point_at(0.5)),
|
||||
))
|
||||
}
|
||||
|
||||
fn normalized_or(value: DVec3, fallback: DVec3) -> DVec3 {
|
||||
if value.length_squared() > 1.0e-18 {
|
||||
value.normalize()
|
||||
fn angular_arc(vertex: Vec2, first: Vec2, second: Vec2, radius: f64) -> Option<Curve> {
|
||||
let first = first.normalize()?;
|
||||
let second = second.normalize()?;
|
||||
let first_angle = first.angle();
|
||||
let second_angle = second.angle();
|
||||
let (start_angle, end_angle) = if arc_span(first_angle, second_angle) <= PI {
|
||||
(first_angle, second_angle)
|
||||
} else {
|
||||
fallback
|
||||
(second_angle, first_angle)
|
||||
};
|
||||
Some(Curve::Arc(Arc {
|
||||
centre: vertex.into(),
|
||||
radius,
|
||||
start_angle,
|
||||
end_angle,
|
||||
}))
|
||||
}
|
||||
|
||||
fn refresh_measurement(dimension: &mut Dimension, plane: Plane) {
|
||||
match dimension {
|
||||
Dimension::Linear(value) => {
|
||||
let first = project(&plane, value.first_point);
|
||||
let second = project(&plane, value.second_point);
|
||||
let axis = Vec2::new(value.rotation.cos(), value.rotation.sin());
|
||||
value.base.actual_measurement = (second - first).dot(axis).abs();
|
||||
}
|
||||
Dimension::Aligned(value) => value.base.actual_measurement = value.measurement(),
|
||||
Dimension::Angular2Ln(value) => {
|
||||
value.base.actual_measurement = value.measurement_degrees()
|
||||
}
|
||||
Dimension::Angular3Pt(value) => {
|
||||
value.base.actual_measurement = value.measurement_degrees()
|
||||
}
|
||||
Dimension::Ordinate(value) => value.refresh_measurement(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn preview_for_dimension(dimension: &Dimension) -> WireModel {
|
||||
let mut points = Vec::<[f32; 3]>::new();
|
||||
match dimension {
|
||||
Dimension::Linear(dimension) => linear_preview(
|
||||
&mut points,
|
||||
dv(dimension.first_point),
|
||||
dv(dimension.second_point),
|
||||
dimension.rotation,
|
||||
dv(dimension.definition_point),
|
||||
),
|
||||
Dimension::Aligned(dimension) => {
|
||||
let first = dv(dimension.first_point);
|
||||
let second = dv(dimension.second_point);
|
||||
let delta = second - first;
|
||||
Dimension::Linear(value) => {
|
||||
let plane = plane_from_normal(value.first_point, value.base.normal);
|
||||
linear_preview(
|
||||
&mut points,
|
||||
first,
|
||||
second,
|
||||
delta.y.atan2(delta.x),
|
||||
dv(dimension.definition_point),
|
||||
plane,
|
||||
project(&plane, value.first_point),
|
||||
project(&plane, value.second_point),
|
||||
value.rotation,
|
||||
project(&plane, value.definition_point),
|
||||
);
|
||||
}
|
||||
Dimension::Angular2Ln(dimension) => angular_preview(
|
||||
Dimension::Aligned(value) => {
|
||||
let plane = plane_from_normal(value.first_point, value.base.normal);
|
||||
let first = project(&plane, value.first_point);
|
||||
let second = project(&plane, value.second_point);
|
||||
linear_preview(
|
||||
&mut points,
|
||||
plane,
|
||||
first,
|
||||
second,
|
||||
(second - first).angle(),
|
||||
project(&plane, value.definition_point),
|
||||
);
|
||||
}
|
||||
Dimension::Angular2Ln(value) => {
|
||||
let plane = plane_from_normal(value.first_point, value.base.normal);
|
||||
let lines = [
|
||||
[
|
||||
project(&plane, value.first_point),
|
||||
project(&plane, value.second_point),
|
||||
],
|
||||
[
|
||||
project(&plane, value.angle_vertex),
|
||||
project(&plane, value.definition_point),
|
||||
],
|
||||
];
|
||||
let vertex = angular2_vertex(lines).unwrap_or(lines[1][0]);
|
||||
angular_preview(
|
||||
&mut points,
|
||||
plane,
|
||||
vertex,
|
||||
lines[0][1] - vertex,
|
||||
lines[1][1] - vertex,
|
||||
project(&plane, value.dimension_arc),
|
||||
);
|
||||
}
|
||||
Dimension::Angular3Pt(value) => {
|
||||
let plane = plane_from_normal(value.angle_vertex, value.base.normal);
|
||||
let vertex = project(&plane, value.angle_vertex);
|
||||
angular_preview(
|
||||
&mut points,
|
||||
plane,
|
||||
vertex,
|
||||
project(&plane, value.first_point) - vertex,
|
||||
project(&plane, value.second_point) - vertex,
|
||||
project(&plane, value.definition_point),
|
||||
);
|
||||
}
|
||||
Dimension::Ordinate(value) => segment_world(
|
||||
&mut points,
|
||||
angular2_vertex(dimension),
|
||||
dv(dimension.second_point) - angular2_vertex(dimension),
|
||||
dv(dimension.definition_point) - angular2_vertex(dimension),
|
||||
dv(dimension.dimension_arc),
|
||||
),
|
||||
Dimension::Angular3Pt(dimension) => angular_preview(
|
||||
&mut points,
|
||||
dv(dimension.angle_vertex),
|
||||
dv(dimension.first_point) - dv(dimension.angle_vertex),
|
||||
dv(dimension.second_point) - dv(dimension.angle_vertex),
|
||||
dv(dimension.definition_point),
|
||||
),
|
||||
Dimension::Ordinate(dimension) => segment(
|
||||
&mut points,
|
||||
dv(dimension.feature_location),
|
||||
dv(dimension.leader_endpoint),
|
||||
point(value.feature_location),
|
||||
point(value.leader_endpoint),
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
@@ -629,72 +745,103 @@ fn preview_for_dimension(dimension: &Dimension) -> WireModel {
|
||||
|
||||
fn linear_preview(
|
||||
points: &mut Vec<[f32; 3]>,
|
||||
first: DVec3,
|
||||
second: DVec3,
|
||||
plane: Plane,
|
||||
first: Vec2,
|
||||
second: Vec2,
|
||||
rotation: f64,
|
||||
definition: DVec3,
|
||||
definition: Vec2,
|
||||
) {
|
||||
let perpendicular = DVec3::new(-rotation.sin(), rotation.cos(), 0.0);
|
||||
let perpendicular = Vec2::new(-rotation.sin(), rotation.cos());
|
||||
let target = definition.dot(perpendicular);
|
||||
let first_line = first + perpendicular * (target - first.dot(perpendicular));
|
||||
let second_line = second + perpendicular * (target - second.dot(perpendicular));
|
||||
segment(points, first, first_line);
|
||||
segment(points, second, second_line);
|
||||
segment(points, first_line, second_line);
|
||||
append_arrows(points, first_line, second_line);
|
||||
let first_line = project_to_line(first, perpendicular, target);
|
||||
let second_line = project_to_line(second, perpendicular, target);
|
||||
segment_local(points, plane, first, first_line);
|
||||
segment_local(points, plane, second, second_line);
|
||||
segment_local(points, plane, first_line, second_line);
|
||||
append_arrows(points, plane, first_line, second_line);
|
||||
}
|
||||
|
||||
fn segment(points: &mut Vec<[f32; 3]>, first: DVec3, second: DVec3) {
|
||||
points.push(first.as_vec3().to_array());
|
||||
points.push(second.as_vec3().to_array());
|
||||
points.push([f32::NAN, 0.0, 0.0]);
|
||||
}
|
||||
|
||||
fn append_arrows(points: &mut Vec<[f32; 3]>, first: DVec3, second: DVec3) {
|
||||
let axis = normalized_or(second - first, DVec3::X);
|
||||
let normal = DVec3::new(-axis.y, axis.x, 0.0);
|
||||
fn append_arrows(points: &mut Vec<[f32; 3]>, plane: Plane, first: Vec2, second: Vec2) {
|
||||
let Some(axis) = (second - first).normalize() else {
|
||||
return;
|
||||
};
|
||||
let normal = axis.perpendicular();
|
||||
let size = first.distance(second).clamp(1.0, 100.0) * 0.035;
|
||||
for (tip, inward) in [(first, axis), (second, -axis)] {
|
||||
let back = tip + inward * size;
|
||||
segment(points, tip, back + normal * size * 0.4);
|
||||
segment(points, tip, back - normal * size * 0.4);
|
||||
segment_local(points, plane, tip, back + normal * size * 0.4);
|
||||
segment_local(points, plane, tip, back - normal * size * 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
fn angular_preview(
|
||||
points: &mut Vec<[f32; 3]>,
|
||||
vertex: DVec3,
|
||||
first: DVec3,
|
||||
second: DVec3,
|
||||
arc_point: DVec3,
|
||||
plane: Plane,
|
||||
vertex: Vec2,
|
||||
first: Vec2,
|
||||
second: Vec2,
|
||||
arc_point: Vec2,
|
||||
) {
|
||||
let radius = (arc_point - vertex).length().max(1.0e-9);
|
||||
let start = first.y.atan2(first.x);
|
||||
let mut sweep = second.y.atan2(second.x) - start;
|
||||
while sweep <= -std::f64::consts::PI {
|
||||
sweep += std::f64::consts::TAU;
|
||||
}
|
||||
while sweep > std::f64::consts::PI {
|
||||
sweep -= std::f64::consts::TAU;
|
||||
}
|
||||
segment(points, vertex, vertex + normalized_or(first, DVec3::X) * radius);
|
||||
segment(points, vertex, vertex + normalized_or(second, DVec3::Y) * radius);
|
||||
let radius = arc_point.distance(vertex).max(1.0e-9);
|
||||
let Some(first_direction) = first.normalize() else {
|
||||
return;
|
||||
};
|
||||
let Some(second_direction) = second.normalize() else {
|
||||
return;
|
||||
};
|
||||
let Some(arc) = angular_arc(vertex, first, second, radius) else {
|
||||
return;
|
||||
};
|
||||
segment_local(points, plane, vertex, vertex + first_direction * radius);
|
||||
segment_local(points, plane, vertex, vertex + second_direction * radius);
|
||||
for index in 0..=24 {
|
||||
let angle = start + sweep * index as f64 / 24.0;
|
||||
points.push(
|
||||
(vertex + DVec3::new(angle.cos(), angle.sin(), 0.0) * radius)
|
||||
.as_vec3()
|
||||
.to_array(),
|
||||
);
|
||||
push_local(points, plane, Vec2::from(arc.point_at(index as f64 / 24.0)));
|
||||
}
|
||||
}
|
||||
|
||||
fn dv(point: Vector3) -> DVec3 {
|
||||
DVec3::new(point.x, point.y, point.z)
|
||||
fn project_to_line(point: Vec2, perpendicular: Vec2, offset: f64) -> Vec2 {
|
||||
point + perpendicular * (offset - point.dot(perpendicular))
|
||||
}
|
||||
|
||||
fn v3(point: DVec3) -> Vector3 {
|
||||
Vector3::new(point.x, point.y, point.z)
|
||||
fn plane_from_normal(origin: Vector3, normal: Vector3) -> Plane {
|
||||
let (x_axis, y_axis) =
|
||||
crate::scene::view::transform::ocs_axes((normal.x, normal.y, normal.z));
|
||||
Plane::from_axes(
|
||||
point(origin),
|
||||
[x_axis.0, x_axis.1, x_axis.2],
|
||||
[y_axis.0, y_axis.1, y_axis.2],
|
||||
)
|
||||
}
|
||||
|
||||
fn project(plane: &Plane, value: Vector3) -> Vec2 {
|
||||
Vec2::from(plane.project(point(value)).unwrap_or([0.0; 2]))
|
||||
}
|
||||
|
||||
fn world(plane: Plane, value: Vec2) -> Vector3 {
|
||||
let value = plane.point_at(value.into());
|
||||
Vector3::new(value[0], value[1], value[2])
|
||||
}
|
||||
|
||||
fn point(value: Vector3) -> [f64; 3] {
|
||||
[value.x, value.y, value.z]
|
||||
}
|
||||
|
||||
fn segment_local(points: &mut Vec<[f32; 3]>, plane: Plane, first: Vec2, second: Vec2) {
|
||||
segment_world(points, plane.point_at(first.into()), plane.point_at(second.into()));
|
||||
}
|
||||
|
||||
fn segment_world(points: &mut Vec<[f32; 3]>, first: [f64; 3], second: [f64; 3]) {
|
||||
points.push(float3(first));
|
||||
points.push(float3(second));
|
||||
points.push([f32::NAN, 0.0, 0.0]);
|
||||
}
|
||||
|
||||
fn push_local(points: &mut Vec<[f32; 3]>, plane: Plane, point: Vec2) {
|
||||
points.push(float3(plane.point_at(point.into())));
|
||||
}
|
||||
|
||||
fn float3(value: [f64; 3]) -> [f32; 3] {
|
||||
[value[0] as f32, value[1] as f32, value[2] as f32]
|
||||
}
|
||||
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["DIMBASELINE"] });
|
||||
@@ -1,17 +1,10 @@
|
||||
// DIMCONTINUE command — chain linear/aligned dimensions end-to-end.
|
||||
//
|
||||
// Each new point becomes the second extension line origin of a new dimension,
|
||||
// whose first extension line origin is the second extension line of the previous dim.
|
||||
// The dimension line stays at the same perpendicular offset as the base dimension.
|
||||
//
|
||||
// Constructed from commands.rs after finding the last placed linear/aligned dimension.
|
||||
|
||||
use acadrust::entities::{Dimension, DimensionLinear};
|
||||
use acadrust::entities::{Dimension, DimensionBase, DimensionLinear};
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::EntityType;
|
||||
use glam::{DVec3, Vec3};
|
||||
use cadkernel::space::Plane;
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdResult, DimensionAssociationInput};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use crate::t;
|
||||
@@ -27,75 +20,112 @@ pub fn tool() -> ToolDef {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DimContinueCommand {
|
||||
/// Fixed first-extension-line origin for the current step (moves each iteration).
|
||||
chain_p1: DVec3,
|
||||
/// Direction along the dimension axis (0.0 = horizontal, PI/2 = vertical).
|
||||
rotation: f64,
|
||||
/// Text reading rotation inherited from the base dim so a UCS-aligned chain
|
||||
/// keeps its text consistent with the originating DIMLINEAR. 0 = natural.
|
||||
#[derive(Clone)]
|
||||
struct SourceStyle {
|
||||
layer: String,
|
||||
style_name: String,
|
||||
normal: Vector3,
|
||||
text_rotation: f64,
|
||||
/// Absolute perpendicular coordinate (dot with `perp`) of the base
|
||||
/// dimension line. Each continued dim line is projected onto this exact
|
||||
/// coordinate so the whole chain stays collinear — even when the extension
|
||||
/// origins sit at different perpendicular positions (extension lines of
|
||||
/// different lengths). (#151)
|
||||
dim_line_perp: f64,
|
||||
/// Direction of "up" perpendicular to the dim axis (points toward the dim line).
|
||||
perp: DVec3,
|
||||
/// True once we have a base dimension loaded.
|
||||
ready: bool,
|
||||
horizontal_direction: f64,
|
||||
}
|
||||
|
||||
impl DimContinueCommand {
|
||||
/// No base dim found — will show an error prompt and cancel immediately.
|
||||
pub fn new() -> Self {
|
||||
impl SourceStyle {
|
||||
fn from_base(base: &DimensionBase) -> Self {
|
||||
Self {
|
||||
chain_p1: DVec3::ZERO,
|
||||
rotation: 0.0,
|
||||
text_rotation: 0.0,
|
||||
dim_line_perp: 0.0,
|
||||
perp: DVec3::Y,
|
||||
ready: false,
|
||||
layer: base.common.layer.clone(),
|
||||
style_name: base.style_name.clone(),
|
||||
normal: base.normal,
|
||||
text_rotation: base.text_rotation,
|
||||
horizontal_direction: base.horizontal_direction,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build from the last placed dimension.
|
||||
///
|
||||
/// `p2` — second extension line origin of the base dim (the chain starts
|
||||
/// here). `p1` is unused — the dim line position comes from
|
||||
/// `definition_point` alone — but kept for signature parity with the
|
||||
/// shared `find_last_linear_dim` tuple (DIMBASELINE uses p1).
|
||||
/// `definition_point` — where the dim line was placed; its perpendicular
|
||||
/// coordinate is the line the whole chain stays collinear with.
|
||||
/// `rotation` — 0.0 = horizontal dim, PI/2 = vertical dim.
|
||||
pub fn from_base(
|
||||
_p1: Vec3,
|
||||
p2: Vec3,
|
||||
definition_point: Vec3,
|
||||
rotation: f64,
|
||||
text_rotation: f64,
|
||||
) -> Self {
|
||||
// Widen the base-dim points to f64 so the committed chain math runs in
|
||||
// full precision. (The base points arrive as f32 from the entity, but
|
||||
// every derived committed coordinate must stay f64 from here on.)
|
||||
let p2 = p2.as_dvec3();
|
||||
let definition_point = definition_point.as_dvec3();
|
||||
// Axis unit vector along the measurement direction — the base dim's
|
||||
// rotation angle (any angle, incl. a UCS-aligned one), not a world H/V.
|
||||
let axis = DVec3::new(rotation.cos(), rotation.sin(), 0.0);
|
||||
// Perpendicular unit vector toward the dim line.
|
||||
let perp = DVec3::new(-axis.y, axis.x, 0.0);
|
||||
let dim_line_perp = definition_point.dot(perp);
|
||||
fn apply(&self, base: &mut DimensionBase) {
|
||||
base.common.layer = self.layer.clone();
|
||||
base.style_name = self.style_name.clone();
|
||||
base.normal = self.normal;
|
||||
base.text_rotation = self.text_rotation;
|
||||
base.horizontal_direction = self.horizontal_direction;
|
||||
}
|
||||
}
|
||||
|
||||
struct ContinueState {
|
||||
chain_p1: [f64; 2],
|
||||
rotation: f64,
|
||||
dim_line_perp: f64,
|
||||
perpendicular: [f64; 2],
|
||||
plane: Plane,
|
||||
style: SourceStyle,
|
||||
}
|
||||
|
||||
pub struct DimContinueCommand {
|
||||
state: Option<ContinueState>,
|
||||
preserve_base_style: bool,
|
||||
}
|
||||
|
||||
impl DimContinueCommand {
|
||||
pub fn new(preserve_base_style: bool) -> Self {
|
||||
Self {
|
||||
chain_p1: p2,
|
||||
rotation,
|
||||
text_rotation,
|
||||
dim_line_perp,
|
||||
perp,
|
||||
ready: true,
|
||||
state: None,
|
||||
preserve_base_style,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_dimension(dimension: &Dimension, preserve_base_style: bool) -> Self {
|
||||
let (first, second, definition, rotation) = match dimension {
|
||||
Dimension::Linear(source) => (
|
||||
source.first_point,
|
||||
source.second_point,
|
||||
source.base.definition_point,
|
||||
source.rotation,
|
||||
),
|
||||
Dimension::Aligned(source) => {
|
||||
let plane = plane_from_normal(source.first_point, source.base.normal);
|
||||
let first = plane.project(point(source.first_point)).unwrap_or([0.0; 2]);
|
||||
let second = plane.project(point(source.second_point)).unwrap_or(first);
|
||||
let delta = [second[0] - first[0], second[1] - first[1]];
|
||||
(
|
||||
source.first_point,
|
||||
source.second_point,
|
||||
source.base.definition_point,
|
||||
delta[1].atan2(delta[0]),
|
||||
)
|
||||
}
|
||||
_ => return Self::new(preserve_base_style),
|
||||
};
|
||||
let plane = plane_from_normal(first, dimension.base().normal);
|
||||
let second = plane.project(point(second)).unwrap_or([0.0; 2]);
|
||||
let definition = plane.project(point(definition)).unwrap_or(second);
|
||||
let perpendicular = [-rotation.sin(), rotation.cos()];
|
||||
let dim_line_perp = dot(definition, perpendicular);
|
||||
Self {
|
||||
state: Some(ContinueState {
|
||||
chain_p1: second,
|
||||
rotation,
|
||||
dim_line_perp,
|
||||
perpendicular,
|
||||
plane,
|
||||
style: SourceStyle::from_base(dimension.base()),
|
||||
}),
|
||||
preserve_base_style,
|
||||
}
|
||||
}
|
||||
|
||||
fn placement(&self, point_world: DVec3) -> Option<([f64; 2], [f64; 2], [f64; 2])> {
|
||||
let state = self.state.as_ref()?;
|
||||
let second = state.plane.project(point_world.to_array())?;
|
||||
let first_line = project_to_line(
|
||||
state.chain_p1,
|
||||
state.perpendicular,
|
||||
state.dim_line_perp,
|
||||
);
|
||||
let second_line = project_to_line(
|
||||
second,
|
||||
state.perpendicular,
|
||||
state.dim_line_perp,
|
||||
);
|
||||
Some((second, first_line, second_line))
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for DimContinueCommand {
|
||||
@@ -104,61 +134,61 @@ impl CadCommand for DimContinueCommand {
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if !self.ready {
|
||||
if self.state.is_none() {
|
||||
t!("DIMCONTINUE No base dimension found. Place a dimension first.").into_owned()
|
||||
} else {
|
||||
t!("DIMCONTINUE Specify a second extension line origin (Enter to exit):").into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
if !self.ready {
|
||||
fn on_point(&mut self, point_world: DVec3) -> CmdResult {
|
||||
let Some((second, first_line, second_line)) = self.placement(point_world) else {
|
||||
return CmdResult::Cancel;
|
||||
};
|
||||
let state = self.state.as_mut().expect("continue state");
|
||||
let mut dimension = DimensionLinear::new(
|
||||
vector3(state.plane.point_at(state.chain_p1)),
|
||||
vector3(state.plane.point_at(second)),
|
||||
);
|
||||
dimension.rotation = state.rotation;
|
||||
state.style.apply(&mut dimension.base);
|
||||
dimension.definition_point = vector3(state.plane.point_at(first_line));
|
||||
dimension.base.definition_point = dimension.definition_point;
|
||||
dimension.base.text_middle_point = vector3(state.plane.point_at([
|
||||
(first_line[0] + second_line[0]) * 0.5,
|
||||
(first_line[1] + second_line[1]) * 0.5,
|
||||
]));
|
||||
dimension.base.insertion_point = dimension.base.text_middle_point;
|
||||
let axis = [state.rotation.cos(), state.rotation.sin()];
|
||||
dimension.base.actual_measurement = dot(
|
||||
[
|
||||
second[0] - state.chain_p1[0],
|
||||
second[1] - state.chain_p1[1],
|
||||
],
|
||||
axis,
|
||||
)
|
||||
.abs();
|
||||
state.chain_p1 = second;
|
||||
|
||||
CmdResult::CommitDimension {
|
||||
entity: EntityType::Dimension(Dimension::Linear(dimension)),
|
||||
association: DimensionAssociationInput::Infer(None),
|
||||
preserve_base_style: self.preserve_base_style,
|
||||
continue_command: true,
|
||||
}
|
||||
let p1 = self.chain_p1;
|
||||
let p2 = pt;
|
||||
|
||||
// Build a new linear dimension.
|
||||
let mut dim = DimensionLinear::new(v3(p1), v3(p2));
|
||||
dim.rotation = self.rotation;
|
||||
if self.text_rotation.abs() > 1e-9 {
|
||||
dim.base.text_rotation = self.text_rotation;
|
||||
}
|
||||
|
||||
// Dim line stays collinear with the base: project both extension
|
||||
// origins onto the base dim line's absolute perpendicular coordinate.
|
||||
// (A fixed offset from p1 drifts off the base line whenever p1 and p2
|
||||
// sit at different perpendicular positions.) (#151)
|
||||
let d1 = p1 + self.perp * (self.dim_line_perp - p1.dot(self.perp));
|
||||
let d2 = p2 + self.perp * (self.dim_line_perp - p2.dot(self.perp));
|
||||
dim.definition_point = v3(d1);
|
||||
dim.base.definition_point = v3(d1);
|
||||
dim.base.text_middle_point = v3((d1 + d2) * 0.5);
|
||||
dim.base.insertion_point = dim.base.text_middle_point;
|
||||
dim.base.actual_measurement = dim.measurement();
|
||||
|
||||
// Advance chain: next dim's P1 = this dim's P2.
|
||||
self.chain_p1 = p2;
|
||||
|
||||
CmdResult::CommitEntity(EntityType::Dimension(Dimension::Linear(dim)))
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
if !self.ready {
|
||||
return None;
|
||||
}
|
||||
// Preview / rubber-band geometry feeds an f32 GPU buffer, so drop to f32
|
||||
// at this screen-only boundary.
|
||||
let pt = pt.as_vec3();
|
||||
let p1 = self.chain_p1.as_vec3();
|
||||
let perp = self.perp.as_vec3();
|
||||
let dim_line_perp = self.dim_line_perp as f32;
|
||||
let dim_line_pt = p1 + perp * (dim_line_perp - p1.dot(perp));
|
||||
let dim_line_pt2 = pt + perp * (dim_line_perp - pt.dot(perp));
|
||||
fn on_mouse_move(&mut self, point_world: DVec3) -> Option<WireModel> {
|
||||
let (second, first_line, second_line) = self.placement(point_world)?;
|
||||
let state = self.state.as_ref()?;
|
||||
let first = state.plane.point_at(state.chain_p1);
|
||||
let second = state.plane.point_at(second);
|
||||
let first_line = state.plane.point_at(first_line);
|
||||
let second_line = state.plane.point_at(second_line);
|
||||
Some(WireModel {
|
||||
point_marker: None,
|
||||
taper_widths: Vec::new(),
|
||||
@@ -177,14 +207,14 @@ impl CadCommand for DimContinueCommand {
|
||||
text_verts: Vec::new(),
|
||||
name: "dimcont_preview".into(),
|
||||
points: vec![
|
||||
[p1.x, p1.y, p1.z],
|
||||
[dim_line_pt.x, dim_line_pt.y, dim_line_pt.z],
|
||||
float3(first),
|
||||
float3(first_line),
|
||||
[f32::NAN, 0.0, 0.0],
|
||||
[pt.x, pt.y, pt.z],
|
||||
[dim_line_pt2.x, dim_line_pt2.y, dim_line_pt2.z],
|
||||
float3(second),
|
||||
float3(second_line),
|
||||
[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],
|
||||
float3(first_line),
|
||||
float3(second_line),
|
||||
],
|
||||
points_low: Vec::new(),
|
||||
color: WireModel::CYAN,
|
||||
@@ -192,22 +222,50 @@ impl CadCommand for DimContinueCommand {
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px: 1.0,
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
snap_pts: Vec::new(),
|
||||
tangent_geoms: Vec::new(),
|
||||
aci: 0,
|
||||
key_vertices: vec![],
|
||||
key_vertices: Vec::new(),
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
fill_tris: vec![],
|
||||
fill_tris: Vec::new(),
|
||||
fill_tris_low: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn v3(p: DVec3) -> Vector3 {
|
||||
Vector3::new(p.x, p.y, p.z)
|
||||
fn plane_from_normal(origin: Vector3, normal: Vector3) -> Plane {
|
||||
let (x_axis, y_axis) =
|
||||
crate::scene::view::transform::ocs_axes((normal.x, normal.y, normal.z));
|
||||
Plane::from_axes(
|
||||
point(origin),
|
||||
[x_axis.0, x_axis.1, x_axis.2],
|
||||
[y_axis.0, y_axis.1, y_axis.2],
|
||||
)
|
||||
}
|
||||
|
||||
fn project_to_line(point: [f64; 2], perpendicular: [f64; 2], offset: f64) -> [f64; 2] {
|
||||
let distance = offset - dot(point, perpendicular);
|
||||
[
|
||||
point[0] + perpendicular[0] * distance,
|
||||
point[1] + perpendicular[1] * distance,
|
||||
]
|
||||
}
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["DIMCONTINUE"] }); // DimContinueCommand
|
||||
fn dot(first: [f64; 2], second: [f64; 2]) -> f64 {
|
||||
first[0] * second[0] + first[1] * second[1]
|
||||
}
|
||||
|
||||
fn point(value: Vector3) -> [f64; 3] {
|
||||
[value.x, value.y, value.z]
|
||||
}
|
||||
|
||||
fn vector3(value: [f64; 3]) -> Vector3 {
|
||||
Vector3::new(value[0], value[1], value[2])
|
||||
}
|
||||
|
||||
fn float3(value: [f64; 3]) -> [f32; 3] {
|
||||
[value[0] as f32, value[1] as f32, value[2] as f32]
|
||||
}
|
||||
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["DIMCONTINUE"] });
|
||||
@@ -140,6 +140,7 @@ impl JoggedRadiusDimensionCommand {
|
||||
))),
|
||||
association,
|
||||
preserve_base_style: false,
|
||||
continue_command: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,6 +188,7 @@ impl CadCommand for LinearDimensionCommand {
|
||||
entity,
|
||||
association: DimensionAssociationInput::Infer(self.source_handle),
|
||||
preserve_base_style: false,
|
||||
continue_command: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ impl CadCommand for OrdinateDimCommand {
|
||||
)),
|
||||
association: DimensionAssociationInput::Infer(None),
|
||||
preserve_base_style: false,
|
||||
continue_command: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,7 @@ impl CadCommand for RadiusDimensionCommand {
|
||||
entity: source_plane.place_entity(EntityType::Dimension(Dimension::Radius(dim))),
|
||||
association: DimensionAssociationInput::Infer(self.source_handle),
|
||||
preserve_base_style: false,
|
||||
continue_command: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,12 +408,23 @@ fn resolve_reference(scene: &Scene, reference: &AssocDimensionReference) -> Opti
|
||||
.copied()
|
||||
}
|
||||
|
||||
fn dimension_inference_points(dimension: &Dimension) -> Option<[Option<Vector3>; 2]> {
|
||||
fn dimension_inference_points(dimension: &Dimension) -> Vec<Option<Vector3>> {
|
||||
match dimension {
|
||||
Dimension::Linear(linear) => Some([Some(linear.first_point), Some(linear.second_point)]),
|
||||
Dimension::Aligned(aligned) => Some([Some(aligned.first_point), Some(aligned.second_point)]),
|
||||
Dimension::Ordinate(ordinate) => Some([Some(ordinate.feature_location), None]),
|
||||
_ => None,
|
||||
Dimension::Linear(linear) => vec![Some(linear.first_point), Some(linear.second_point)],
|
||||
Dimension::Aligned(aligned) => vec![Some(aligned.first_point), Some(aligned.second_point)],
|
||||
Dimension::Angular3Pt(angular) => vec![
|
||||
Some(angular.angle_vertex),
|
||||
Some(angular.first_point),
|
||||
Some(angular.second_point),
|
||||
],
|
||||
Dimension::Angular2Ln(angular) => vec![
|
||||
Some(angular.first_point),
|
||||
Some(angular.second_point),
|
||||
Some(angular.angle_vertex),
|
||||
Some(angular.definition_point),
|
||||
],
|
||||
Dimension::Ordinate(ordinate) => vec![Some(ordinate.feature_location)],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -800,10 +811,10 @@ impl Scene {
|
||||
pub(crate) fn infer_dimension_sources(
|
||||
&self,
|
||||
dimension: Handle,
|
||||
) -> [Option<Handle>; 2] {
|
||||
) -> Vec<Option<Handle>> {
|
||||
let Some(EntityType::Dimension(entity)) = self.document.get_entity(dimension)
|
||||
else {
|
||||
return [None, None];
|
||||
return Vec::new();
|
||||
};
|
||||
let radial_data = match entity {
|
||||
Dimension::Radius(radius) => Some((
|
||||
@@ -845,23 +856,25 @@ impl Scene {
|
||||
})
|
||||
.min_by(|first, second| first.0.total_cmp(&second.0))
|
||||
.map(|(_, handle)| handle);
|
||||
return [source, None];
|
||||
return vec![source];
|
||||
}
|
||||
let Some(points) = dimension_inference_points(entity) else {
|
||||
return [None, None];
|
||||
};
|
||||
points.map(|point| {
|
||||
point.and_then(|point| self.document
|
||||
.entities()
|
||||
.filter(|entity| entity.common().handle != dimension)
|
||||
.filter_map(|entity| {
|
||||
source_distance_squared(entity, point)
|
||||
.map(|distance| (distance, entity.common().handle))
|
||||
dimension_inference_points(entity)
|
||||
.into_iter()
|
||||
.map(|point| {
|
||||
point.and_then(|point| {
|
||||
self.document
|
||||
.entities()
|
||||
.filter(|entity| entity.common().handle != dimension)
|
||||
.filter_map(|entity| {
|
||||
source_distance_squared(entity, point)
|
||||
.map(|distance| (distance, entity.common().handle))
|
||||
})
|
||||
.filter(|(distance, _)| *distance <= 1e-16)
|
||||
.min_by(|first, second| first.0.total_cmp(&second.0))
|
||||
.map(|(_, handle)| handle)
|
||||
})
|
||||
.filter(|(distance, _)| *distance <= 1e-16)
|
||||
.min_by(|first, second| first.0.total_cmp(&second.0))
|
||||
.map(|(_, handle)| handle))
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_associative_dimensions(
|
||||
@@ -970,7 +983,7 @@ impl Scene {
|
||||
if let Some(point) = resolved[3] {
|
||||
angular.definition_point = point;
|
||||
}
|
||||
angular.base.definition_point = angular.definition_point;
|
||||
angular.base.definition_point = angular.dimension_arc;
|
||||
}
|
||||
Dimension::Radius(radius) => {
|
||||
let Some((radial, angle)) = radial_source else {
|
||||
@@ -1137,9 +1150,21 @@ impl Scene {
|
||||
arc.base.actual_measurement = arc.measurement();
|
||||
}
|
||||
}
|
||||
dimension.base_mut().actual_measurement = dimension.measurement();
|
||||
let measurement = match &*dimension {
|
||||
Dimension::Linear(linear) => linear_measurement(linear),
|
||||
_ => dimension.measurement(),
|
||||
};
|
||||
dimension.base_mut().actual_measurement = measurement;
|
||||
refreshed.push((association.dimension, ChangeKind::Modified));
|
||||
}
|
||||
refreshed
|
||||
}
|
||||
}
|
||||
|
||||
fn linear_measurement(linear: &acadrust::entities::DimensionLinear) -> f64 {
|
||||
let plane = plane_from_normal(linear.first_point, linear.base.normal);
|
||||
let first = plane.project(dpoint(linear.first_point)).unwrap_or([0.0; 2]);
|
||||
let second = plane.project(dpoint(linear.second_point)).unwrap_or(first);
|
||||
let axis = [linear.rotation.cos(), linear.rotation.sin()];
|
||||
((second[0] - first[0]) * axis[0] + (second[1] - first[1]) * axis[1]).abs()
|
||||
}
|
||||
Reference in new issue
Block a user