Merge pull request #1320 from ghost/styled-dimension-preview

This commit is contained in:
Hakan Seven committed 2026-09-17 07:11:10 +03:00
commit 79bc800728
23 files changed
+724 -138

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

+119 -14
View File
@@ -2,7 +2,7 @@
//! supplies a viewport scale; sheet-only dimensions retain paper units.
use super::OpenCADStudio;
use crate::command::DimensionAssociationSource;
use crate::command::{DimensionAssociationSource, DimensionPreview};
use crate::entities::dim_override;
use crate::scene::viewport_ref::{AcceptedSnap, MeasurementScale, SnapSourceRef, ViewportFrame};
use acadrust::entities::Dimension;
@@ -27,12 +27,25 @@ impl OpenCADStudio {
}
pub(crate) fn dimension_measure_space(&self, i: usize) -> DimensionMeasureSpace {
self.dimension_measure_space_with_candidate(i, None)
}
fn dimension_measure_space_with_candidate(
&self,
i: usize,
candidate: Option<ViewportFrame>,
) -> DimensionMeasureSpace {
let scene = &self.tabs[i].scene;
if scene.current_layout == "Model" || scene.active_viewport.is_some() {
return DimensionMeasureSpace::Direct;
}
let mut frame: Option<ViewportFrame> = None;
for f in self.accepted_snaps().iter().filter_map(|snap| snap.frame) {
for f in self
.accepted_snaps()
.iter()
.filter_map(|snap| snap.frame)
.chain(candidate)
{
match frame {
None => frame = Some(f),
Some(previous) if previous.viewport == f.viewport => {}
@@ -122,13 +135,10 @@ impl OpenCADStudio {
i: usize,
entity: &mut EntityType,
) -> bool {
let EntityType::Dimension(dimension) = entity else {
if !matches!(entity, EntityType::Dimension(_)) {
return true;
};
let angular = matches!(
dimension,
Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_)
);
}
let angular = is_angular_dimension(entity);
let frame = match self.dimension_measure_space(i) {
DimensionMeasureSpace::Direct => return true,
DimensionMeasureSpace::ConflictingViewports => {
@@ -140,18 +150,106 @@ impl OpenCADStudio {
if angular {
return true;
}
let Some(scale) = self.viewport_dimension_scale(i, entity, frame) else {
return false;
};
// Owner is needed even by DIMASSOC=0's explode path, before insertion.
entity.common_mut().owner_handle = self.tabs[i].scene.current_layout_block_handle_pub();
scale.write_to_entity(entity);
true
}
fn viewport_dimension_scale(
&self,
i: usize,
entity: &EntityType,
frame: ViewportFrame,
) -> Option<MeasurementScale> {
let dimlfac = self.pending_dimension_style_dimlfac(i, entity);
let scale = MeasurementScale {
user_lfac: MeasurementScale::user_lfac_for_space(dimlfac, true),
viewport_compensation: frame.paper_to_model_length_factor(),
};
if !scale.paper_factor().is_finite() || scale.paper_factor() <= 0.0 {
return false;
(scale.paper_factor().is_finite() && scale.paper_factor() > 0.0).then_some(scale)
}
/// Styled dimensions for the placement preview at `cursor`, carrying the
/// same style and viewport measurement the commit path would apply.
/// `None` when the active command has no committable dimension at this
/// stage; an empty list when the candidate geometry or scale is invalid.
pub(crate) fn dimension_preview_entities(
&self,
i: usize,
cursor: DVec3,
) -> Option<Vec<EntityType>> {
let command = self.tabs[i].active_cmd.as_ref()?;
let previews = command.dimension_preview(cursor)?;
let scene = &self.tabs[i].scene;
let measure_space = if command.measures_through_viewports() {
// The hovered model snap supplies a provisional viewport only while
// the second definition point is still being acquired.
let candidate = if command.dimension_placement_pending() {
None
} else {
self.vp_snap_frame.filter(|frame| {
self.tabs[i]
.snap_result
.is_some_and(|hit| hit.viewport == Some(frame.viewport))
})
};
self.dimension_measure_space_with_candidate(i, candidate)
} else {
DimensionMeasureSpace::Direct
};
if matches!(measure_space, DimensionMeasureSpace::ConflictingViewports) {
return Some(Vec::new());
}
// Owner is needed even by DIMASSOC=0's explode path, before insertion.
entity.common_mut().owner_handle = self.tabs[i].scene.current_layout_block_handle_pub();
scale.write_to_entity(entity);
true
let mut entities = Vec::with_capacity(previews.len());
for DimensionPreview {
mut entity,
preserve_base_style,
} in previews
{
if !preserve_base_style {
crate::scene::creation_style::apply_current_creation_styles(
&scene.document,
&mut entity,
);
}
entity.common_mut().owner_handle = scene.current_layout_block_handle_pub();
if let DimensionMeasureSpace::Viewport(frame) = measure_space {
if !is_angular_dimension(&entity) {
let Some(scale) = self.viewport_dimension_scale(i, &entity, frame) else {
return Some(Vec::new());
};
scale.write_to_entity(&mut entity);
}
}
entities.push(entity);
}
Some(entities)
}
pub(crate) fn dimension_preview_wires(
&self,
i: usize,
cursor: DVec3,
) -> Option<Vec<crate::scene::WireModel>> {
let entities = self.dimension_preview_entities(i, cursor)?;
// Degenerate geometry, an invalid scale, or conflicting viewports
// produce no styled dimension. Returning None lets the command's
// plain rubber-band preview run, which shows no value to mistrust.
if entities.is_empty() {
return None;
}
let mut wires = self.tabs[i].scene.wires_for_entities(&entities);
for wire in &mut wires {
wire.color = crate::scene::WireModel::CYAN;
for vertex in &mut wire.text_verts {
vertex.color = crate::scene::WireModel::CYAN;
}
}
Some(wires)
}
pub(crate) fn try_dimension_viewport_entity_pick(
@@ -254,3 +352,10 @@ impl OpenCADStudio {
.bump_entities(&[(dimension, crate::scene::ChangeKind::Modified)]);
}
}
fn is_angular_dimension(entity: &EntityType) -> bool {
matches!(
entity,
EntityType::Dimension(Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_))
)
}
+225
View File
@@ -0,0 +1,225 @@
//! Styled placement previews must show the text and geometry the command
//! commits at the same stage. Viewport measurement cases live in
//! `viewport_dimension_tests`; these cover the remaining creation commands
//! in model space, style inheritance, batches, and degenerate stages.
use super::viewport_dimension_tests::preview_text as text;
use super::OpenCADStudio;
use crate::command::StepInput;
use acadrust::entities::{Arc, Circle, Line};
use acadrust::types::{Handle, Vector3};
use acadrust::EntityType;
use glam::DVec3;
fn app() -> (OpenCADStudio, usize) {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
let i = app.active_tab;
(app, i)
}
fn style_name(entity: &EntityType) -> String {
let EntityType::Dimension(dimension) = entity else {
panic!("expected dimension")
};
dimension.base().style_name.clone()
}
fn committed(app: &OpenCADStudio) -> Vec<EntityType> {
app.tabs[app.active_tab]
.scene
.document
.entities()
.filter(|entity| matches!(entity, EntityType::Dimension(_)))
.cloned()
.collect()
}
/// Styled preview entities at `cursor`, asserting they also tessellate.
fn preview(app: &OpenCADStudio, i: usize, cursor: DVec3) -> Vec<EntityType> {
let entities = app.dimension_preview_entities(i, cursor).unwrap();
assert!(!entities.is_empty(), "no styled preview at {cursor}");
assert!(!app.dimension_preview_wires(i, cursor).unwrap().is_empty());
entities
}
fn commit_and_compare(app: &mut OpenCADStudio, i: usize, cursor: DVec3) -> EntityType {
let previewed = preview(app, i, cursor);
assert_eq!(previewed.len(), 1);
let before = committed(app).len();
let _ = app.feed_command(StepInput::Point(cursor));
let after = committed(app);
assert_eq!(after.len(), before + 1, "commit did not add one dimension");
let entity = after.last().unwrap().clone();
assert_eq!(text(app, &entity), text(app, &previewed[0]));
assert_eq!(style_name(&entity), style_name(&previewed[0]));
entity
}
#[test]
fn ordinate_preview_matches_commit() {
let (mut app, i) = app();
let _ = app.dispatch_command("DIMORDINATE");
assert!(app.dimension_preview_entities(i, DVec3::ZERO).is_none());
let _ = app.feed_command(StepInput::Point(DVec3::new(12.5, 40.0, 0.0)));
let entity = commit_and_compare(&mut app, i, DVec3::new(12.5, 90.0, 0.0));
assert_eq!(text(&app, &entity), "12.5");
}
#[test]
fn arc_length_preview_matches_commit_for_full_and_partial_arcs() {
let (mut app, i) = app();
let mut arc = Arc::new();
arc.center = Vector3::ZERO;
arc.radius = 50.0;
arc.start_angle = 0.0;
arc.end_angle = std::f64::consts::FRAC_PI_2;
let handle = app.tabs[i].scene.add_entity(EntityType::Arc(arc));
let on_arc = |degrees: f64| {
let radians = degrees.to_radians();
DVec3::new(50.0 * radians.cos(), 50.0 * radians.sin(), 0.0)
};
let _ = app.dispatch_command("DIMARC");
let _ = app.feed_command(StepInput::EntityPick(handle, on_arc(45.0)));
let full = commit_and_compare(&mut app, i, DVec3::new(60.0, 60.0, 0.0));
let _ = app.dispatch_command("DIMARC");
let _ = app.feed_command(StepInput::EntityPick(handle, on_arc(45.0)));
let _ = app.feed_command(StepInput::Text("P".into()));
let _ = app.feed_command(StepInput::Point(on_arc(10.0)));
// While the second partial point is acquired the command has no
// committable dimension; the plain arc preview stays in charge.
assert!(app.dimension_preview_entities(i, on_arc(80.0)).is_none());
let _ = app.feed_command(StepInput::Point(on_arc(80.0)));
let partial = commit_and_compare(&mut app, i, DVec3::new(60.0, 60.0, 0.0));
assert_ne!(text(&app, &full), text(&app, &partial));
}
#[test]
fn jogged_radius_preview_matches_commit_at_text_and_jog_stages() {
let (mut app, i) = app();
let mut circle = Circle::new();
circle.center = Vector3::ZERO;
circle.radius = 100.0;
let handle = app.tabs[i].scene.add_entity(EntityType::Circle(circle));
let _ = app.dispatch_command("DIMJOGGED");
let _ = app.feed_command(StepInput::EntityPick(handle, DVec3::new(100.0, 0.0, 0.0)));
assert!(app
.dimension_preview_entities(i, DVec3::new(30.0, 0.0, 0.0))
.is_none());
let _ = app.feed_command(StepInput::Point(DVec3::new(30.0, 0.0, 0.0)));
let text_position = DVec3::new(150.0, 40.0, 0.0);
let staged = preview(&app, i, text_position);
assert_eq!(staged.len(), 1);
let _ = app.feed_command(StepInput::Point(text_position));
let entity = commit_and_compare(&mut app, i, DVec3::new(90.0, 20.0, 0.0));
assert_eq!(text(&app, &entity), text(&app, &staged[0]));
assert_eq!(text(&app, &entity), "R100");
}
#[test]
fn angular_three_point_preview_matches_commit() {
let (mut app, i) = app();
let _ = app.dispatch_command("DIMANGULAR");
let _ = app.feed_command(StepInput::Point(DVec3::ZERO));
let _ = app.feed_command(StepInput::Point(DVec3::new(100.0, 0.0, 0.0)));
assert!(app
.dimension_preview_entities(i, DVec3::new(0.0, 100.0, 0.0))
.is_none());
let _ = app.feed_command(StepInput::Point(DVec3::new(0.0, 100.0, 0.0)));
let entity = commit_and_compare(&mut app, i, DVec3::new(70.0, 70.0, 0.0));
assert_eq!(text(&app, &entity), "90°");
}
fn place_linear(app: &mut OpenCADStudio, first: DVec3, second: DVec3, line: DVec3) -> Handle {
let _ = app.dispatch_command("DIMLINEAR");
let _ = app.feed_command(StepInput::Point(first));
let _ = app.feed_command(StepInput::Point(second));
let _ = app.feed_command(StepInput::Point(line));
committed(app).last().unwrap().common().handle
}
#[test]
fn continue_and_baseline_previews_follow_style_inheritance() {
let (mut app, i) = app();
let default_style = app.tabs[i]
.scene
.document
.header
.current_dimstyle_name
.clone();
let mut style = acadrust::tables::DimStyle::new("Inherit");
style.dimlfac = 2.0;
app.tabs[i].scene.document.dim_styles.add(style).unwrap();
app.tabs[i].scene.document.header.current_dimstyle_name = "Inherit".into();
let base = place_linear(
&mut app,
DVec3::ZERO,
DVec3::new(100.0, 0.0, 0.0),
DVec3::new(50.0, -20.0, 0.0),
);
assert_eq!(text(&app, committed(&app).last().unwrap()), "200");
app.tabs[i].scene.document.header.current_dimstyle_name = default_style;
// DIMCONTINUEMODE=1 keeps the base dimension's style in the preview.
app.dimension_continue_mode = 1;
app.tabs[i].scene.last_created_dimension = Some(base);
let _ = app.dispatch_command("DIMCONTINUE");
let inherited = commit_and_compare(&mut app, i, DVec3::new(250.0, 0.0, 0.0));
assert_eq!(style_name(&inherited), "Inherit");
assert_eq!(text(&app, &inherited), "300");
// DIMCONTINUEMODE=0 runs the current creation defaults over the built
// dimension instead; preview and commit must still agree.
app.dimension_continue_mode = 0;
app.tabs[i].scene.last_created_dimension = Some(inherited.common().handle);
let _ = app.dispatch_command("DIMCONTINUE");
commit_and_compare(&mut app, i, DVec3::new(350.0, 0.0, 0.0));
app.dimension_continue_mode = 1;
app.tabs[i].scene.last_created_dimension = Some(base);
let _ = app.dispatch_command("DIMBASELINE");
let baseline = commit_and_compare(&mut app, i, DVec3::new(300.0, 0.0, 0.0));
assert_eq!(style_name(&baseline), "Inherit");
assert_eq!(text(&app, &baseline), "600");
}
#[test]
fn qdim_preview_matches_the_committed_batch() {
let (mut app, i) = app();
let segments = [(0.0, 40.0), (40.0, 100.0), (100.0, 130.0)];
for (start, end) in segments {
let line = Line::from_points(Vector3::new(start, 0.0, 0.0), Vector3::new(end, 0.0, 0.0));
let handle = app.tabs[i].scene.add_entity(EntityType::Line(line));
app.tabs[i].scene.select_entity(handle, false);
}
let _ = app.dispatch_command("QDIM");
let cursor = DVec3::new(60.0, 40.0, 0.0);
let previewed = preview(&app, i, cursor);
assert_eq!(previewed.len(), segments.len());
let mut preview_texts: Vec<String> = previewed.iter().map(|e| text(&app, e)).collect();
let _ = app.feed_command(StepInput::Point(cursor));
let mut committed_texts: Vec<String> = committed(&app).iter().map(|e| text(&app, e)).collect();
preview_texts.sort();
committed_texts.sort();
assert_eq!(committed_texts, preview_texts);
assert_eq!(committed_texts, ["30", "40", "60"]);
}
#[test]
fn degenerate_linear_preview_defers_to_plain_rubber_band() {
let (mut app, i) = app();
let _ = app.dispatch_command("DIMLINEAR");
assert!(app.dimension_preview_entities(i, DVec3::ZERO).is_none());
let _ = app.feed_command(StepInput::Point(DVec3::ZERO));
assert!(app
.dimension_preview_entities(i, DVec3::ZERO)
.is_some_and(|entities| entities.is_empty()));
assert!(app.dimension_preview_wires(i, DVec3::ZERO).is_none());
assert!(committed(&app).is_empty());
let cursor = DVec3::new(80.0, 0.0, 0.0);
let styled = preview(&app, i, cursor);
assert_eq!(text(&app, &styled[0]), "80");
assert!(committed(&app).is_empty(), "hover must not commit");
}
+2
View File
@@ -13,6 +13,8 @@ pub(crate) mod commands;
pub(crate) mod dim_viewport;
#[cfg(test)]
mod viewport_dimension_tests;
#[cfg(test)]
mod dimension_preview_tests;
mod document;
mod drafting_settings;
pub(crate) mod expr_eval;
+2
View File
@@ -2547,6 +2547,8 @@ impl OpenCADStudio {
}
}
p
} else if let Some(wires) = self.dimension_preview_wires(i, effective) {
wires
} else {
self.tabs[i]
.active_cmd
+49 -2
View File
@@ -87,6 +87,14 @@ fn displayed(doc: &CadDocument) -> f64 {
dim.measurement() * MeasurementScale::user_lfac_for_space(factor, true)
}
pub(super) fn preview_text(app: &OpenCADStudio, entity: &EntityType) -> String {
let EntityType::Dimension(dimension) = entity else { panic!("expected dimension") };
let doc = &app.tabs[app.active_tab].scene.document;
let source = doc.dim_styles.get(&dimension.base().style_name).unwrap();
let style = crate::entities::dimension::resolved_dimension_style(source, dimension, doc);
crate::entities::dimension::dimension_text_value(dimension, Some(&style)).unwrap()
}
fn finish_aligned(app: &mut OpenCADStudio, line: Handle, frame: ViewportFrame) {
let _ = app.dispatch_command("DIMALIGNED");
point(app, frame, line, DVec3::ZERO);
@@ -300,7 +308,13 @@ fn viewport_dimension_linear_aligned_and_radial_object_picks() {
if radial { 1 } else { 2 },
"{name}"
);
let _ = app.feed_command(StepInput::Point(DVec3::new(55.0, 80.0, 0.0)));
let place = DVec3::new(55.0, 80.0, 0.0);
let preview = app.dimension_preview_entities(i, place).unwrap();
assert_eq!(preview.len(), 1, "{name}");
let value = preview_text(&app, &preview[0]);
assert!(!app.dimension_preview_wires(i, place).unwrap().is_empty(), "{name}");
let _ = app.feed_command(StepInput::Point(place));
assert_eq!(preview_text(&app, &EntityType::Dimension(dimension(&app.tabs[i].scene.document).clone())), value, "{name}");
assert!(
(displayed(&app.tabs[i].scene.document) - expected).abs() < 1e-4,
"{name}: {}",
@@ -326,12 +340,45 @@ fn viewport_dimension_angular_preserves_angle_without_length_factor() {
point(&mut app, frame, line, DVec3::ZERO);
point(&mut app, frame, line, DVec3::X * 100.0);
point(&mut app, frame, vertical, DVec3::Y * 100.0);
let _ = app.feed_command(StepInput::Point(DVec3::new(70.0, 70.0, 0.0)));
let place = DVec3::new(70.0, 70.0, 0.0);
let preview = app.dimension_preview_entities(i, place).unwrap();
assert_eq!(preview.len(), 1);
assert!(MeasurementScale::read(&preview[0].common().extended_data).is_none());
let value = preview_text(&app, &preview[0]);
let _ = app.feed_command(StepInput::Point(place));
let doc = &app.tabs[app.active_tab].scene.document;
assert_eq!(preview_text(&app, &EntityType::Dimension(dimension(doc).clone())), value);
assert!((dimension(doc).measurement() - 90.0).abs() < 1e-5);
assert!(MeasurementScale::read(&dimension(doc).base().common.extended_data).is_none());
}
#[test]
fn viewport_dimension_preview_uses_candidate_scale_only_before_second_pick() {
for command in ["DIMLINEAR", "DIMALIGNED"] {
let (mut app, model, frame) = fixture();
let i = app.active_tab;
let mut style = acadrust::tables::DimStyle::new("PreviewUnits");
style.dimlfac = 25.4;
app.tabs[i].scene.document.dim_styles.add(style).unwrap();
app.tabs[i].scene.document.header.current_dimstyle_name = "PreviewUnits".into();
let _ = app.dispatch_command(command);
let _ = app.feed_command(StepInput::Point(DVec3::new(55.0, 50.0, 0.0)));
let target = hit(frame, model, DVec3::X * 100.0);
let sheet = app.dimension_preview_entities(i, target.world).unwrap();
assert_eq!(preview_text(&app, &sheet[0]), "127");
app.vp_snap_frame = Some(frame);
app.tabs[i].snap_result = Some(target);
let model_preview = app.dimension_preview_entities(i, target.world).unwrap();
assert_eq!(preview_text(&app, &model_preview[0]), "1270");
point(&mut app, frame, model, DVec3::X * 100.0);
let other = ViewportFrame { viewport: Handle::new(0xFFFF), scale: frame.scale * 2.0, ..frame };
app.vp_snap_frame = Some(other);
app.tabs[i].snap_result = Some(hit(other, model, DVec3::X * 100.0));
let placement = app.dimension_preview_entities(i, DVec3::new(60.0, 80.0, 0.0)).unwrap();
assert_eq!(preview_text(&app, &placement[0]), "1270");
}
}
#[test]
fn viewport_dimension_snap_query_matches_pan_scale_and_twist() {
for (height, twist, target) in [
+24
View File
@@ -1345,6 +1345,23 @@ pub enum DimensionBreakOperation {
Remove,
}
/// A transient dimension built for the placement preview.
pub struct DimensionPreview {
pub entity: EntityType,
/// Keep the base dimension's style instead of the current one
/// (`DIMCONTINUE` / `DIMBASELINE` with `DIMCONTINUEMODE=1`).
pub preserve_base_style: bool,
}
impl DimensionPreview {
pub fn current_style(entity: EntityType) -> Self {
Self {
entity,
preserve_base_style: false,
}
}
}
/// Returned by every `CadCommand` method to tell main.rs what to do.
#[allow(dead_code)]
pub enum CmdResult {
@@ -2279,6 +2296,13 @@ pub trait CadCommand: Send {
false
}
/// Dimensions the command would commit if the cursor were clicked now.
/// `None` means this stage has no committable dimension; an empty list
/// means the geometry is degenerate and nothing styled should be shown.
fn dimension_preview(&self, _cursor: DVec3) -> Option<Vec<DimensionPreview>> {
None
}
/// Needs entity hit-testing instead of point input.
fn needs_entity_pick(&self) -> bool {
false
+43 -21
View File
@@ -6,7 +6,8 @@ use acadrust::EntityType;
use glam::DVec3;
use crate::command::{
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, InputKind, WorkingPlane,
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, DimensionPreview, InputKind,
WorkingPlane,
};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
@@ -61,6 +62,29 @@ impl AlignedDimensionCommand {
mtext_override: false,
}
}
fn build_dimension(&self, first: DVec3, second: DVec3, point: DVec3) -> EntityType {
let first = self.plane.to_local(first);
let second = self.plane.to_local(second);
let point = self.plane.to_local(point);
let mut dim = DimensionAligned::new(v3(first), v3(second));
// The placement point is stored so commit matches the preview.
dim.definition_point = v3(point);
dim.base.definition_point = v3(point);
let (d1, d2) = dim_line_endpoints(first, second, point);
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();
crate::entities::dimension::set_dimension_text_override(
&mut dim.base,
self.text_override.clone(),
);
// An explicit text angle overrides the default rotation.
if let Some(angle) = self.text_angle {
dim.base.text_rotation = angle;
}
self.plane.place_entity(EntityType::Dimension(Dimension::Aligned(dim)))
}
}
impl CadCommand for AlignedDimensionCommand {
@@ -115,27 +139,8 @@ impl CadCommand for AlignedDimensionCommand {
CmdResult::NeedPoint
}
Step::DimLine { p1, p2 } => {
let p1 = self.plane.to_local(p1);
let p2 = self.plane.to_local(p2);
let pt = self.plane.to_local(pt);
let mut dim = DimensionAligned::new(v3(p1), v3(p2));
// Store the cursor position so commit matches the preview.
dim.definition_point = v3(pt);
dim.base.definition_point = v3(pt);
let (d1, d2) = dim_line_endpoints(p1, p2, pt);
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();
crate::entities::dimension::set_dimension_text_override(
&mut dim.base,
self.text_override.clone(),
);
// An explicit text angle overrides the default rotation.
if let Some(a) = self.text_angle {
dim.base.text_rotation = a;
}
CmdResult::CommitDimension {
entity: self.plane.place_entity(EntityType::Dimension(Dimension::Aligned(dim))),
entity: self.build_dimension(p1, p2, pt),
association: DimensionAssociationInput::Infer(self.source_handle),
preserve_base_style: false,
continue_command: false,
@@ -363,6 +368,23 @@ impl CadCommand for AlignedDimensionCommand {
ref_point: None,
})
}
fn dimension_preview(&self, cursor: DVec3) -> Option<Vec<DimensionPreview>> {
if self.selecting_object {
return None;
}
let (first, second) = match self.step {
Step::First => return None,
Step::Second(first) => (first, cursor),
Step::DimLine { p1, p2 } => (p1, p2),
};
if first.distance_squared(second) <= 1e-24 {
return Some(Vec::new());
}
Some(vec![DimensionPreview::current_style(
self.build_dimension(first, second, cursor),
)])
}
}
fn v3(p: DVec3) -> Vector3 {
+28 -2
View File
@@ -7,8 +7,8 @@ use cadkernel::geom2d::{
};
use crate::command::{
CadCommand, CmdOption, CmdResult, DimensionAssociationInput,
DimensionAssociationSource, InputKind, WorkingPlane,
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, DimensionAssociationSource,
DimensionPreview, InputKind, WorkingPlane,
};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
@@ -807,6 +807,32 @@ impl CadCommand for AngularDimensionCommand {
};
Some(preview_wire(points))
}
fn dimension_preview(&self, cursor: DVec3) -> Option<Vec<DimensionPreview>> {
if self.selecting_object {
return None;
}
let result = match self.step {
Step::ArcPoint3 {
vertex,
first,
second,
} => self.finish_three_point(vertex, first, second, cursor),
Step::ArcPoint2 {
first_start,
first_end,
second_start,
second_end,
} => self.finish_two_line(first_start, first_end, second_start, second_end, cursor),
_ => return None,
};
match result {
CmdResult::CommitDimension { entity, .. } => {
Some(vec![DimensionPreview::current_style(entity)])
}
_ => Some(Vec::new()),
}
}
}
fn picked_curve(entity: &EntityType, click: DVec3) -> Option<PickedCurve> {
+14 -2
View File
@@ -5,8 +5,8 @@ use cadkernel::geom2d::tessellate::{arc, DEFAULT_SEGMENTS_PER_RADIAN};
use glam::DVec3;
use crate::command::{
CadCommand, CmdOption, CmdResult, DimensionAssociationInput,
DimensionAssociationSource, InputKind, WorkingPlane,
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, DimensionAssociationSource,
DimensionPreview, InputKind, WorkingPlane,
};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::dimension_assoc::{
@@ -516,6 +516,18 @@ impl CadCommand for ArcLengthDimensionCommand {
}
}
}
fn dimension_preview(&self, cursor: DVec3) -> Option<Vec<DimensionPreview>> {
let Step::DimLine(selection) = self.step else {
return None;
};
match self.commit_dimension(selection, cursor) {
CmdResult::CommitDimension { entity, .. } => {
Some(vec![DimensionPreview::current_style(entity)])
}
_ => Some(Vec::new()),
}
}
}
fn positive_sweep(start: f64, end: f64) -> f64 {
+39 -24
View File
@@ -4,7 +4,8 @@ use acadrust::EntityType;
use glam::{DVec3, Vec3};
use crate::command::{
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, InputKind, WorkingPlane,
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, DimensionPreview, InputKind,
WorkingPlane,
};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
@@ -55,6 +56,35 @@ impl DiameterDimensionCommand {
Step::DimLine(source) => dvec(source.point_at_angle(source.start_angle)),
}
}
fn build_dimension(
&self,
source: crate::scene::dimension_assoc::RadialSourceGeometry,
world: DVec3,
) -> EntityType {
let plane = WorkingPlane::new(
DVec3::from_array(source.plane.origin),
DVec3::from_array(source.plane.x_axis),
DVec3::from_array(source.plane.y_axis),
);
let chord = plane.to_local(dvec(source.chord_at(world.to_array())));
let far_chord = plane.to_local(dvec(source.opposite_chord_at(world.to_array())));
let point = plane.to_local(world);
let mut dim = DimensionDiameter::new(v3(chord), v3(far_chord));
dim.base.text_middle_point = v3(point);
dim.base.insertion_point = v3(point);
dim.base.text_user_positioned = true;
dim.leader_length = chord.distance(point);
dim.base.actual_measurement = dim.measurement();
crate::entities::dimension::set_dimension_text_override(
&mut dim.base,
self.text_override.clone(),
);
if let Some(angle) = self.text_angle {
dim.base.text_rotation = angle;
}
plane.place_entity(EntityType::Dimension(Dimension::Diameter(dim)))
}
}
impl CadCommand for DiameterDimensionCommand {
@@ -88,30 +118,8 @@ impl CadCommand for DiameterDimensionCommand {
match self.step {
Step::SelectObject => CmdResult::NeedPoint,
Step::DimLine(source) => {
let source_plane = WorkingPlane::new(
DVec3::from_array(source.plane.origin),
DVec3::from_array(source.plane.x_axis),
DVec3::from_array(source.plane.y_axis),
);
let chord = source_plane.to_local(dvec(source.chord_at(pt.to_array())));
let far_chord = source_plane.to_local(dvec(source.opposite_chord_at(pt.to_array())));
let pt = source_plane.to_local(pt);
let mut dim = DimensionDiameter::new(v3(chord), v3(far_chord));
dim.base.text_middle_point = v3(pt);
dim.base.insertion_point = v3(pt);
dim.base.text_user_positioned = true;
dim.leader_length = chord.distance(pt);
dim.base.actual_measurement = dim.measurement();
crate::entities::dimension::set_dimension_text_override(
&mut dim.base,
self.text_override.clone(),
);
if let Some(a) = self.text_angle {
dim.base.text_rotation = a;
}
CmdResult::CommitDimension {
entity: source_plane
.place_entity(EntityType::Dimension(Dimension::Diameter(dim))),
entity: self.build_dimension(source, pt),
association: DimensionAssociationInput::Infer(self.source_handle),
preserve_base_style: false,
continue_command: false,
@@ -278,6 +286,13 @@ impl CadCommand for DiameterDimensionCommand {
}
}
}
fn dimension_preview(&self, cursor: DVec3) -> Option<Vec<DimensionPreview>> {
let Step::DimLine(source) = self.step else {
return None;
};
Some(vec![DimensionPreview::current_style(self.build_dimension(source, cursor))])
}
}
fn v3(p: DVec3) -> Vector3 {
+13 -1
View File
@@ -13,7 +13,9 @@ use cadkernel::geom2d::{
use cadkernel::space::Plane;
use glam::DVec3;
use crate::command::{CadCommand, CmdOption, CmdResult, DimensionAssociationInput};
use crate::command::{
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, DimensionPreview,
};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
@@ -482,6 +484,16 @@ impl CadCommand for DimBaselineCommand {
self.build_dimension(point)
.map(|dimension| preview_for_dimension(&dimension))
}
fn dimension_preview(&self, cursor: DVec3) -> Option<Vec<DimensionPreview>> {
self.build_dimension(cursor)
.map(|dimension| {
vec![DimensionPreview {
entity: EntityType::Dimension(dimension),
preserve_base_style: self.preserve_base_style,
}]
})
}
}
fn linear_base(
+13 -1
View File
@@ -11,7 +11,9 @@ use cadkernel::geom2d::{
use cadkernel::space::Plane;
use glam::DVec3;
use crate::command::{CadCommand, CmdOption, CmdResult, DimensionAssociationInput};
use crate::command::{
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, DimensionPreview,
};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
@@ -423,6 +425,16 @@ impl CadCommand for DimContinueCommand {
self.build_dimension(point)
.map(|dimension| preview_for_dimension(&dimension))
}
fn dimension_preview(&self, cursor: DVec3) -> Option<Vec<DimensionPreview>> {
self.build_dimension(cursor)
.map(|dimension| {
vec![DimensionPreview {
entity: EntityType::Dimension(dimension),
preserve_base_style: self.preserve_base_style,
}]
})
}
}
fn build_linear(
+30 -1
View File
@@ -5,7 +5,7 @@ use glam::{DVec3, Vec3};
use crate::command::{
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, DimensionAssociationSource,
InputKind, WorkingPlane,
DimensionPreview, InputKind, WorkingPlane,
};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::creation_style::DimensionCreationDefaults;
@@ -390,6 +390,35 @@ impl CadCommand for JoggedRadiusDimensionCommand {
}
}
}
fn dimension_preview(&self, cursor: DVec3) -> Option<Vec<DimensionPreview>> {
let result = match self.step {
Step::DimLine {
source,
override_center,
} => {
// The jog is not chosen yet; preview it midway along the
// dimension line so the styled value is visible while the
// text position is placed.
let chord = dvec(source.chord_at(cursor.to_array()));
let jog = (override_center + chord) * 0.5;
self.commit_dimension(source, override_center, chord, cursor, jog)
}
Step::Jog {
source,
override_center,
chord,
text_position,
} => self.commit_dimension(source, override_center, chord, text_position, cursor),
_ => return None,
};
match result {
CmdResult::CommitDimension { entity, .. } => {
Some(vec![DimensionPreview::current_style(entity)])
}
_ => Some(Vec::new()),
}
}
}
fn source_plane(source: RadialSourceGeometry) -> WorkingPlane {
+43 -23
View File
@@ -6,7 +6,8 @@ use cadkernel::geom2d::{
};
use crate::command::{
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, InputKind, WorkingPlane,
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, DimensionPreview, InputKind,
WorkingPlane,
};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
@@ -107,6 +108,29 @@ impl LinearDimensionCommand {
mtext_override: false,
}
}
fn build_dimension(&self, first: DVec3, second: DVec3, point: DVec3) -> EntityType {
let first = self.plane.to_local(first);
let second = self.plane.to_local(second);
let point = self.plane.to_local(point);
let mut dim = DimensionLinear::new(v3(first), v3(second));
let axis = self.axis_mode.axis(first, second, point);
dim.rotation = axis.y.atan2(axis.x);
dim.set_offset(dimension_line_offset(second, point, axis));
dim.base.definition_point = dim.definition_point;
dim.base.text_middle_point = v3(linear_text_pos(first, second, point, axis));
dim.base.insertion_point = dim.base.text_middle_point;
dim.base.actual_measurement = dim.measurement();
crate::entities::dimension::set_dimension_text_override(
&mut dim.base,
self.text_override.clone(),
);
// An explicit text angle overrides the UCS-derived rotation.
if let Some(angle) = self.text_angle {
dim.base.text_rotation = angle;
}
self.plane.place_entity(EntityType::Dimension(Dimension::Linear(dim)))
}
}
impl CadCommand for LinearDimensionCommand {
@@ -164,28 +188,7 @@ impl CadCommand for LinearDimensionCommand {
CmdResult::NeedPoint
}
Step::DimensionLine { first, second } => {
let first = self.plane.to_local(first);
let second = self.plane.to_local(second);
let pt = self.plane.to_local(pt);
let mut dim = DimensionLinear::new(v3(first), v3(second));
let axis = self.axis_mode.axis(first, second, pt);
dim.rotation = axis.y.atan2(axis.x);
dim.set_offset(dimension_line_offset(second, pt, axis));
dim.base.definition_point = dim.definition_point;
dim.base.text_middle_point = v3(linear_text_pos(first, second, pt, axis));
dim.base.insertion_point = dim.base.text_middle_point;
dim.base.actual_measurement = dim.measurement();
crate::entities::dimension::set_dimension_text_override(
&mut dim.base,
self.text_override.clone(),
);
// An explicit text angle overrides the UCS-derived rotation.
if let Some(a) = self.text_angle {
dim.base.text_rotation = a;
}
let entity = self.plane.place_entity(EntityType::Dimension(
Dimension::Linear(dim),
));
let entity = self.build_dimension(first, second, pt);
CmdResult::CommitDimension {
entity,
association: DimensionAssociationInput::Infer(self.source_handle),
@@ -402,6 +405,23 @@ impl CadCommand for LinearDimensionCommand {
ref_point: None,
})
}
fn dimension_preview(&self, cursor: DVec3) -> Option<Vec<DimensionPreview>> {
if self.selecting_object {
return None;
}
let (first, second) = match self.step {
Step::FirstPoint => return None,
Step::SecondPoint(first) => (first, cursor),
Step::DimensionLine { first, second } => (first, second),
};
if first.distance_squared(second) <= 1e-24 {
return Some(Vec::new());
}
Some(vec![DimensionPreview::current_style(
self.build_dimension(first, second, cursor),
)])
}
}
fn v3(pt: DVec3) -> Vector3 {
+30 -20
View File
@@ -4,7 +4,8 @@ use acadrust::EntityType;
use glam::{DVec3, Vec3};
use crate::command::{
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, InputKind, WorkingPlane,
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, DimensionPreview, InputKind,
WorkingPlane,
};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
@@ -60,6 +61,26 @@ impl OrdinateDimCommand {
Step::LeaderEndpoint { feature } => feature,
}
}
fn build_dimension(&self, feature: DVec3, point: DVec3) -> EntityType {
let feature = self.plane.to_local(feature);
let point = self.plane.to_local(point);
let is_x = match self.datum_mode {
DatumMode::Automatic => is_x_type(feature, point),
DatumMode::X => true,
DatumMode::Y => false,
};
let mut dim = DimensionOrdinate::new(v3(feature), v3(point), is_x);
crate::entities::dimension::set_dimension_text_override(
&mut dim.base,
self.text_override.clone(),
);
if let Some(angle) = self.text_angle {
dim.base.text_rotation = angle;
}
dim.refresh_measurement();
self.plane.place_entity(EntityType::Dimension(Dimension::Ordinate(dim)))
}
}
impl CadCommand for OrdinateDimCommand {
@@ -95,26 +116,8 @@ impl CadCommand for OrdinateDimCommand {
CmdResult::NeedPoint
}
Step::LeaderEndpoint { feature } => {
let feature = self.plane.to_local(feature);
let pt = self.plane.to_local(pt);
let is_x = match self.datum_mode {
DatumMode::Automatic => is_x_type(feature, pt),
DatumMode::X => true,
DatumMode::Y => false,
};
let mut dim = DimensionOrdinate::new(v3(feature), v3(pt), is_x);
crate::entities::dimension::set_dimension_text_override(
&mut dim.base,
self.text_override.clone(),
);
if let Some(angle) = self.text_angle {
dim.base.text_rotation = angle;
}
dim.refresh_measurement();
CmdResult::CommitDimension {
entity: self.plane.place_entity(EntityType::Dimension(
Dimension::Ordinate(dim),
)),
entity: self.build_dimension(feature, pt),
association: DimensionAssociationInput::Infer(None),
preserve_base_style: false,
continue_command: false,
@@ -258,6 +261,13 @@ impl CadCommand for OrdinateDimCommand {
.collect(),
))
}
fn dimension_preview(&self, cursor: DVec3) -> Option<Vec<DimensionPreview>> {
let Step::LeaderEndpoint { feature } = self.step else {
return None;
};
Some(vec![DimensionPreview::current_style(self.build_dimension(feature, cursor))])
}
}
fn v3(p: DVec3) -> Vector3 {
+10 -1
View File
@@ -8,7 +8,7 @@ use glam::DVec3;
use crate::command::{
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, DimensionAssociationSource,
EntityTransform, SelectionEntity, WorkingPlane,
DimensionPreview, EntityTransform, SelectionEntity, WorkingPlane,
};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
@@ -364,6 +364,15 @@ impl CadCommand for QdimCommand {
}
Vec::new()
}
fn dimension_preview(&self, cursor: DVec3) -> Option<Vec<DimensionPreview>> {
(self.step == Step::Place).then(|| {
self.build_dimensions(cursor)
.into_iter()
.map(|(entity, _)| DimensionPreview::current_style(entity))
.collect()
})
}
}
fn v3(point: DVec3) -> Vector3 { Vector3::new(point.x, point.y, point.z) }
+40 -26
View File
@@ -3,7 +3,8 @@ use acadrust::types::{Handle, Vector3};
use acadrust::EntityType;
use crate::command::{
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, InputKind, WorkingPlane,
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, DimensionPreview, InputKind,
WorkingPlane,
};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
@@ -55,6 +56,36 @@ impl RadiusDimensionCommand {
Step::DimLine(source) => dvec(source.point_at_angle(source.start_angle)),
}
}
fn build_dimension(
&self,
source: crate::scene::dimension_assoc::RadialSourceGeometry,
world: DVec3,
) -> EntityType {
let plane = WorkingPlane::new(
DVec3::from_array(source.plane.origin),
DVec3::from_array(source.plane.x_axis),
DVec3::from_array(source.plane.y_axis),
);
let center = plane.to_local(dvec(source.center_world()));
let chord = plane.to_local(dvec(source.chord_at(world.to_array())));
let point = plane.to_local(world);
let mut dim = DimensionRadius::new(v3(center), v3(chord));
dim.base.definition_point = v3(chord);
dim.base.text_middle_point = v3(point);
dim.base.insertion_point = v3(point);
dim.base.text_user_positioned = true;
dim.leader_length = chord.distance(point);
dim.base.actual_measurement = dim.measurement();
crate::entities::dimension::set_dimension_text_override(
&mut dim.base,
self.text_override.clone(),
);
if let Some(angle) = self.text_angle {
dim.base.text_rotation = angle;
}
plane.place_entity(EntityType::Dimension(Dimension::Radius(dim)))
}
}
impl CadCommand for RadiusDimensionCommand {
@@ -84,32 +115,8 @@ impl CadCommand for RadiusDimensionCommand {
match self.step {
Step::SelectObject => CmdResult::NeedPoint,
Step::DimLine(source) => {
let source_plane = WorkingPlane::new(
DVec3::from_array(source.plane.origin),
DVec3::from_array(source.plane.x_axis),
DVec3::from_array(source.plane.y_axis),
);
let center_world = dvec(source.center_world());
let point_world = dvec(source.chord_at(pt.to_array()));
let center = source_plane.to_local(center_world);
let point = source_plane.to_local(point_world);
let pt = source_plane.to_local(pt);
let mut dim = DimensionRadius::new(v3(center), v3(point));
dim.base.definition_point = v3(point);
dim.base.text_middle_point = v3(pt);
dim.base.insertion_point = v3(pt);
dim.base.text_user_positioned = true;
dim.leader_length = point.distance(pt);
dim.base.actual_measurement = dim.measurement();
crate::entities::dimension::set_dimension_text_override(
&mut dim.base,
self.text_override.clone(),
);
if let Some(a) = self.text_angle {
dim.base.text_rotation = a;
}
CmdResult::CommitDimension {
entity: source_plane.place_entity(EntityType::Dimension(Dimension::Radius(dim))),
entity: self.build_dimension(source, pt),
association: DimensionAssociationInput::Infer(self.source_handle),
preserve_base_style: false,
continue_command: false,
@@ -284,6 +291,13 @@ impl CadCommand for RadiusDimensionCommand {
}
}
}
fn dimension_preview(&self, cursor: DVec3) -> Option<Vec<DimensionPreview>> {
let Step::DimLine(source) = self.step else {
return None;
};
Some(vec![DimensionPreview::current_style(self.build_dimension(source, cursor))])
}
}
fn v3(pt: DVec3) -> Vector3 {