Implement associative center mark workflow

This commit is contained in:
ramox81 committed 2026-08-21 12:05:18 +03:00
1 parent 0ae6355c2c
commit ff09fc7800
18 files changed
+936 -40

No files matched your search

+46
View File
@@ -17,6 +17,9 @@ pub(crate) struct CenterLineSettings {
pub linetype: String,
pub linetype_scale: f64,
pub linetype_file: String,
pub cross_size: String,
pub cross_gap: String,
pub mark_extensions: bool,
}
impl Default for CenterLineSettings {
@@ -27,6 +30,9 @@ impl Default for CenterLineSettings {
linetype: "CENTER2".to_owned(),
linetype_scale: 1.0,
linetype_file: String::new(),
cross_size: "0.1x".to_owned(),
cross_gap: "0.05x".to_owned(),
mark_extensions: true,
}
}
}
@@ -219,6 +225,9 @@ impl Scene {
linetype: value(2).and_then(XRecordValue::as_string).unwrap_or(&defaults.linetype).to_owned(),
linetype_scale: value(41).and_then(XRecordValue::as_double).unwrap_or(defaults.linetype_scale),
linetype_file: value(3).and_then(XRecordValue::as_string).unwrap_or(&defaults.linetype_file).to_owned(),
cross_size: value(4).and_then(XRecordValue::as_string).unwrap_or(&defaults.cross_size).to_owned(),
cross_gap: value(5).and_then(XRecordValue::as_string).unwrap_or(&defaults.cross_gap).to_owned(),
mark_extensions: value(290).and_then(XRecordValue::as_bool).unwrap_or(defaults.mark_extensions),
}
}
@@ -259,6 +268,27 @@ impl Scene {
(41, XRecordValue::Double(number), number.to_string())
}
"CENTERLTYPEFILE" => (3, XRecordValue::String(value.to_owned()), value.to_owned()),
"CENTERCROSSSIZE" | "CENTERCROSSGAP" => {
let trimmed = value.trim();
let valid = trimmed.eq_ignore_ascii_case("ByLineType")
|| trimmed.strip_suffix(['x', 'X']).is_some_and(|number| {
number.parse::<f64>().is_ok_and(|value| value.is_finite() && value > 0.0)
})
|| trimmed.parse::<f64>().is_ok_and(|value| value.is_finite() && value > 0.0);
if !valid {
return Err(format!("{name} requires a positive length, a positive x factor, or ByLineType."));
}
let code = if name == "CENTERCROSSSIZE" { 4 } else { 5 };
(code, XRecordValue::String(trimmed.to_owned()), trimmed.to_owned())
}
"CENTERMARKEXE" => {
let enabled = match value.trim().to_ascii_lowercase().as_str() {
"1" | "on" | "yes" | "true" => true,
"0" | "off" | "no" | "false" => false,
_ => return Err("CENTERMARKEXE requires On/Off or 1/0.".to_owned()),
};
(290, XRecordValue::Bool(enabled), if enabled { "1" } else { "0" }.to_owned())
}
_ => return Err(format!("Unknown centerline setting: {name}")),
};
if let Some(entry) = record.entries.iter_mut().find(|entry| entry.code == code) {
@@ -362,3 +392,19 @@ impl Scene {
candidates.len()
}
}
pub(crate) fn resolve_center_measure(specification: &str, diameter: f64, fallback_factor: f64) -> f64 {
let text = specification.trim();
if text.eq_ignore_ascii_case("ByLineType") {
return diameter * fallback_factor;
}
if let Some(factor) = text.strip_suffix(['x', 'X']).and_then(|value| value.parse::<f64>().ok()) {
return diameter * factor.max(0.0);
}
text.parse::<f64>().unwrap_or(diameter * fallback_factor).max(0.0)
}
pub(crate) fn center_measure_is_relative(specification: &str) -> bool {
let text = specification.trim();
text.eq_ignore_ascii_case("ByLineType") || text.ends_with(['x', 'X'])
}
+402
View File
@@ -0,0 +1,402 @@
use super::*;
use acadrust::entities::{
CenterMarkAssociation, CenterMarkSource, CenterMarkSourceKind,
};
use acadrust::types::Vector3;
use cadkernel::geom2d::BulgeArc;
use glam::DVec3;
fn vector(point: DVec3) -> Vector3 {
Vector3::new(point.x, point.y, point.z)
}
pub(crate) fn dvec(point: Vector3) -> DVec3 {
DVec3::new(point.x, point.y, point.z)
}
fn ocs_point(point: (f64, f64, f64), normal: Vector3) -> DVec3 {
let point = crate::scene::view::transform::ocs_point_to_wcs(
point,
(normal.x, normal.y, normal.z),
);
DVec3::new(point.0, point.1, point.2)
}
fn axes(normal: Vector3) -> (DVec3, DVec3) {
let (x, y) = crate::scene::view::transform::ocs_axes((normal.x, normal.y, normal.z));
(
DVec3::new(x.0, x.1, x.2),
DVec3::new(y.0, y.1, y.2),
)
}
fn arc_distance(arc: &BulgeArc, pick: [f64; 2]) -> f64 {
(0..=24)
.map(|index| {
let point = arc.sample(index as f64 / 24.0);
(point[0] - pick[0]).hypot(point[1] - pick[1])
})
.fold(f64::INFINITY, f64::min)
}
fn segment_count(vertices: usize, closed: bool) -> usize {
if closed && vertices > 1 {
vertices
} else {
vertices.saturating_sub(1)
}
}
/// Resolve a pick to circular source geometry, including bulged polyline arcs.
pub(crate) fn picked_mark_source(
entity: &EntityType,
handle: Handle,
pick: DVec3,
) -> Option<(CenterMarkSource, DVec3, f64, DVec3, DVec3)> {
match entity {
EntityType::Circle(circle) if circle.radius > 1.0e-10 => {
let center = ocs_point(
(circle.center.x, circle.center.y, circle.center.z),
circle.normal,
);
let (x, y) = axes(circle.normal);
Some((
CenterMarkSource {
handle,
kind: CenterMarkSourceKind::Circle,
segment_index: -1,
pick_point: vector(pick),
},
center,
circle.radius,
x,
y,
))
}
EntityType::Arc(arc) if arc.radius > 1.0e-10 => {
let center = ocs_point((arc.center.x, arc.center.y, arc.center.z), arc.normal);
let (x, y) = axes(arc.normal);
Some((
CenterMarkSource {
handle,
kind: CenterMarkSourceKind::Arc,
segment_index: -1,
pick_point: vector(pick),
},
center,
arc.radius,
x,
y,
))
}
EntityType::LwPolyline(polyline) => {
let local_pick = {
let origin = ocs_point((0.0, 0.0, polyline.elevation), polyline.normal);
let (x, y) = axes(polyline.normal);
let delta = pick - origin;
[delta.dot(x), delta.dot(y)]
};
let count = segment_count(polyline.vertices.len(), polyline.is_closed);
let (index, arc) = (0..count)
.filter_map(|index| {
let next = (index + 1) % polyline.vertices.len();
let a = polyline.vertices[index].location;
let b = polyline.vertices[next].location;
BulgeArc::from_bulge([a.x, a.y], [b.x, b.y], polyline.vertices[index].bulge)
.map(|arc| (index, arc))
})
.min_by(|(_, a), (_, b)| arc_distance(a, local_pick).total_cmp(&arc_distance(b, local_pick)))?;
let center = ocs_point((arc.center[0], arc.center[1], polyline.elevation), polyline.normal);
let (x, y) = axes(polyline.normal);
Some((
CenterMarkSource {
handle,
kind: CenterMarkSourceKind::LwPolylineArcSegment,
segment_index: index as i32,
pick_point: vector(pick),
},
center,
arc.radius,
x,
y,
))
}
EntityType::Polyline2D(polyline) => {
let local_pick = {
let origin = ocs_point((0.0, 0.0, polyline.elevation), polyline.normal);
let (x, y) = axes(polyline.normal);
let delta = pick - origin;
[delta.dot(x), delta.dot(y)]
};
let count = segment_count(polyline.vertices.len(), polyline.is_closed());
let (index, arc) = (0..count)
.filter_map(|index| {
let next = (index + 1) % polyline.vertices.len();
let a = polyline.vertices[index].location;
let b = polyline.vertices[next].location;
BulgeArc::from_bulge([a.x, a.y], [b.x, b.y], polyline.vertices[index].bulge)
.map(|arc| (index, arc))
})
.min_by(|(_, a), (_, b)| arc_distance(a, local_pick).total_cmp(&arc_distance(b, local_pick)))?;
let center = ocs_point((arc.center[0], arc.center[1], polyline.elevation), polyline.normal);
let (x, y) = axes(polyline.normal);
Some((
CenterMarkSource {
handle,
kind: CenterMarkSourceKind::Polyline2DArcSegment,
segment_index: index as i32,
pick_point: vector(pick),
},
center,
arc.radius,
x,
y,
))
}
_ => None,
}
}
fn resolve_source(
document: &acadrust::CadDocument,
source: &CenterMarkSource,
) -> Option<(DVec3, f64, DVec3, DVec3)> {
let entity = document.get_entity(source.handle)?;
match (source.kind, entity) {
(CenterMarkSourceKind::Circle, EntityType::Circle(circle)) if circle.radius > 1.0e-10 => {
let center = ocs_point((circle.center.x, circle.center.y, circle.center.z), circle.normal);
let (x, y) = axes(circle.normal);
Some((center, circle.radius, x, y))
}
(CenterMarkSourceKind::Arc, EntityType::Arc(arc)) if arc.radius > 1.0e-10 => {
let center = ocs_point((arc.center.x, arc.center.y, arc.center.z), arc.normal);
let (x, y) = axes(arc.normal);
Some((center, arc.radius, x, y))
}
(CenterMarkSourceKind::LwPolylineArcSegment, EntityType::LwPolyline(polyline)) => {
let index = usize::try_from(source.segment_index).ok()?;
let count = segment_count(polyline.vertices.len(), polyline.is_closed);
if index >= count { return None; }
let next = (index + 1) % polyline.vertices.len();
let a = polyline.vertices[index].location;
let b = polyline.vertices[next].location;
let arc = BulgeArc::from_bulge([a.x, a.y], [b.x, b.y], polyline.vertices[index].bulge)?;
let center = ocs_point((arc.center[0], arc.center[1], polyline.elevation), polyline.normal);
let (x, y) = axes(polyline.normal);
Some((center, arc.radius, x, y))
}
(CenterMarkSourceKind::Polyline2DArcSegment, EntityType::Polyline2D(polyline)) => {
let index = usize::try_from(source.segment_index).ok()?;
let count = segment_count(polyline.vertices.len(), polyline.is_closed());
if index >= count { return None; }
let next = (index + 1) % polyline.vertices.len();
let a = polyline.vertices[index].location;
let b = polyline.vertices[next].location;
let arc = BulgeArc::from_bulge([a.x, a.y], [b.x, b.y], polyline.vertices[index].bulge)?;
let center = ocs_point((arc.center[0], arc.center[1], polyline.elevation), polyline.normal);
let (x, y) = axes(polyline.normal);
Some((center, arc.radius, x, y))
}
_ => None,
}
}
pub(crate) fn mark_directions(association: &CenterMarkAssociation) -> [DVec3; 4] {
let x = dvec(association.plane_x).normalize_or(DVec3::X);
let y = dvec(association.plane_y).normalize_or(DVec3::Y);
[x, -x, y, -y]
}
pub(crate) fn mark_segments(association: &CenterMarkAssociation) -> Vec<[DVec3; 2]> {
let center = dvec(association.center);
let directions = mark_directions(association);
let half = (association.cross_size * 0.5).max(0.0);
let mut segments = vec![
[center - directions[0] * half, center + directions[0] * half],
[center - directions[2] * half, center + directions[2] * half],
];
if association.show_extensions && association.radius > half + association.cross_gap {
for (index, direction) in directions.into_iter().enumerate() {
let start = center + direction * (half + association.cross_gap);
let end_distance = (association.radius
+ association.extension_length
+ association.length_adjustments[index]
+ association.overshoots[index])
.max(half + association.cross_gap);
segments.push([start, center + direction * end_distance]);
}
}
segments
}
pub(crate) fn update_carrier(line: &mut acadrust::Line, association: &CenterMarkAssociation) {
let segments = mark_segments(association);
let horizontal = segments.first().copied().unwrap_or([
dvec(association.center),
dvec(association.center),
]);
line.start = vector(horizontal[0]);
line.end = vector(horizontal[1]);
association.write(&mut line.common.extended_data);
}
impl Scene {
pub(crate) fn reassociate_center_mark(
&mut self,
target: Handle,
source_handle: Handle,
pick: DVec3,
) -> bool {
let Some(source_entity) = self.document.get_entity(source_handle) else { return false; };
let Some((source, center, radius, x, y)) =
picked_mark_source(source_entity, source_handle, pick)
else { return false; };
let Some(EntityType::Line(target_line)) = self.document.get_entity(target) else { return false; };
let Some(mut association) = CenterMarkAssociation::read(&target_line.common.extended_data) else { return false; };
let old_diameter = association.radius * 2.0;
let size_factor = (old_diameter > 1.0e-12)
.then_some(association.cross_size / old_diameter)
.unwrap_or(0.1);
let gap_factor = (old_diameter > 1.0e-12)
.then_some(association.cross_gap / old_diameter)
.unwrap_or(0.05);
association.source = source;
association.center = vector(center);
association.radius = radius;
association.plane_origin = vector(center);
association.plane_x = vector(x);
association.plane_y = vector(y);
association.associated = true;
if association.cross_size_relative {
association.cross_size = radius * 2.0 * size_factor;
}
if association.cross_gap_relative {
association.cross_gap = radius * 2.0 * gap_factor;
}
if self.is_recording_undo() {
let before = self.document.get_entity_arc(target);
self.record_undo_before(target, before);
}
let Some(EntityType::Line(line)) = self.document.get_entity_mut(target) else { return false; };
update_carrier(line, &association);
self.bump_entities(&[(target, ChangeKind::Modified)]);
true
}
pub(crate) fn refresh_associative_center_marks(
&mut self,
changes: &[(Handle, ChangeKind)],
) -> Vec<(Handle, ChangeKind)> {
let changed: rustc_hash::FxHashSet<_> =
changes.iter().map(|(handle, _)| *handle).collect();
if changed.is_empty() {
return Vec::new();
}
let candidates: Vec<_> = self
.document
.entities()
.filter_map(|entity| {
let EntityType::Line(line) = entity else { return None; };
let association = CenterMarkAssociation::read(&line.common.extended_data)?;
(association.associated && changed.contains(&association.source.handle))
.then_some((line.common.handle, association))
})
.collect();
let mut result = Vec::new();
for (handle, mut association) in candidates {
if self.is_recording_undo() {
let before = self.document.get_entity_arc(handle);
self.record_undo_before(handle, before);
}
if let Some((center, radius, x, y)) = resolve_source(&self.document, &association.source) {
let old_diameter = association.radius * 2.0;
let size_factor = (old_diameter > 1.0e-12)
.then_some(association.cross_size / old_diameter)
.unwrap_or(0.1);
let gap_factor = (old_diameter > 1.0e-12)
.then_some(association.cross_gap / old_diameter)
.unwrap_or(0.05);
association.center = vector(center);
association.radius = radius;
association.plane_x = vector(x);
association.plane_y = vector(y);
if association.cross_size_relative {
association.cross_size = radius * 2.0 * size_factor;
}
if association.cross_gap_relative {
association.cross_gap = radius * 2.0 * gap_factor;
}
} else {
association.associated = false;
}
if let Some(EntityType::Line(line)) = self.document.get_entity_mut(handle) {
update_carrier(line, &association);
result.push((handle, ChangeKind::Modified));
}
}
result
}
pub(crate) fn reset_center_marks(&mut self, handles: &[Handle]) -> usize {
let extension = self.centerline_settings().extension;
let candidates: Vec<_> = handles
.iter()
.filter_map(|handle| {
let EntityType::Line(line) = self.document.get_entity(*handle)? else { return None; };
let mut association = CenterMarkAssociation::read(&line.common.extended_data)?;
association.extension_length = extension;
association.overshoots = [0.0; 4];
Some((*handle, association))
})
.collect();
for (handle, association) in &candidates {
if let Some(EntityType::Line(line)) = self.document.get_entity_mut(*handle) {
update_carrier(line, association);
}
}
if !candidates.is_empty() {
let changes: Vec<_> = candidates
.iter()
.map(|(handle, _)| (*handle, ChangeKind::Modified))
.collect();
self.bump_entities(&changes);
}
candidates.len()
}
pub(crate) fn set_center_mark_association(
&mut self,
handles: &[Handle],
associated: bool,
) -> usize {
let candidates: Vec<_> = handles
.iter()
.filter_map(|handle| {
let EntityType::Line(line) = self.document.get_entity(*handle)? else { return None; };
let mut association = CenterMarkAssociation::read(&line.common.extended_data)?;
if associated {
let (center, radius, x, y) = resolve_source(&self.document, &association.source)?;
association.center = vector(center);
association.radius = radius;
association.plane_x = vector(x);
association.plane_y = vector(y);
}
association.associated = associated;
Some((*handle, association))
})
.collect();
for (handle, association) in &candidates {
if let Some(EntityType::Line(line)) = self.document.get_entity_mut(*handle) {
update_carrier(line, association);
}
}
if !candidates.is_empty() {
let changes: Vec<_> = candidates
.iter()
.map(|(handle, _)| (*handle, ChangeKind::Modified))
.collect();
self.bump_entities(&changes);
}
candidates.len()
}
}
+6
View File
@@ -22,6 +22,7 @@ pub mod view;
mod boundary;
mod camera_ops;
pub(crate) mod centerline;
pub(crate) mod centermark;
mod entity;
mod group_layer;
mod layout;
@@ -2399,6 +2400,11 @@ impl Scene {
changes.push(change);
}
}
for change in self.refresh_associative_center_marks(&changes) {
if !changes.iter().any(|(handle, _)| *handle == change.0) {
changes.push(change);
}
}
for change in self.refresh_associative_hatches(&changes) {
if !changes.iter().any(|(handle, _)| *handle == change.0) {
changes.push(change);