Implement associative center mark workflow

This commit is contained in:
ramox81 2026-08-21 12:05:18 +03:00
commit ff09fc7800
18 changed files with 936 additions and 40 deletions

4
Cargo.lock generated
View file

@ -72,7 +72,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
[[package]]
name = "acadrust"
version = "0.4.1"
source = "git+https://github.com/ramox81/cadcodec.git?rev=1eeab52#1eeab52a4e743816e5974e1e6441422b5042001d"
source = "git+https://github.com/ramox81/cadcodec.git?rev=6dcda1a#6dcda1adc5a99edb1e15fcdeb206c6d65d073828"
dependencies = [
"ahash 0.8.12",
"anyhow",
@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cadkernel"
version = "0.1.0"
source = "git+https://github.com/ramox81/cadkernel.git?rev=50c2551#50c2551d6bd46d19cfa664384bdc312b424269c4"
source = "git+https://github.com/ramox81/cadkernel.git?rev=90922c8#90922c8cf0b6c77f1db01edac56b37ad59f8fc76"
dependencies = [
"acadrust",
"cavalier_contours",

View file

@ -27,8 +27,8 @@ glam = { version = "0.33", features = ["bytemuck"] }
rfd = "0.17"
clap = { version = "4", features = ["derive"] }
env_logger = "0.11"
acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "1eeab52", features = ["serde"] }
cadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "50c2551", features = ["acis", "offset"] }
acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "6dcda1a", features = ["serde"] }
cadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "90922c8", features = ["acis", "offset"] }
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
flate2 = "1"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }

View file

@ -14,7 +14,7 @@ serde = { version = "1", features = ["derive"] }
# Pulled in only by the `host` feature, which adds the `acadrust`-typed
# `HostApi` runtime surface. The default crate stays dependency-free so engine
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "1eeab52", optional = true, features = ["serde"] }
acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "6dcda1a", optional = true, features = ["serde"] }
# Runtime IPC and serialization (host feature only).
interprocess = { version = "2", optional = true }
@ -37,7 +37,7 @@ serde_json = "1"
serde = { version = "1", features = ["derive"] }
cargo-lock = "11"
# acadrust is scanned at build time to generate the embedded type registry.
acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "1eeab52", features = ["serde"] }
acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "6dcda1a", features = ["serde"] }
[dev-dependencies]
serde_json = "1"

View file

@ -8,7 +8,7 @@ publish = false
crate-type = ["cdylib"]
[dependencies]
acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "1eeab52", features = ["serde"] }
acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "6dcda1a", features = ["serde"] }
bincode = "1.3"
serde = { version = "1", features = ["derive"] }
console_error_panic_hook = "0.1"

View file

@ -10,4 +10,4 @@ crate-type = ["cdylib"]
[dependencies]
ocs_plugin_api = { path = "../ocs_plugin_api", features = ["host"] }
acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "1eeab52", features = ["serde"] }
acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "6dcda1a", features = ["serde"] }

View file

@ -1495,6 +1495,26 @@ impl OpenCADStudio {
}
self.refresh_properties();
}
CmdResult::ReassociateCenterMark { target, source, point } => {
if self.reject_locked_edit(i, target) {
return Task::none();
}
self.push_undo_snapshot(i, "CENTERREASSOCIATE");
if self.tabs[i].scene.reassociate_center_mark(target, source, point) {
self.tabs[i].dirty = true;
self.command_line.push_output(
"CENTERREASSOCIATE: center mark associated.",
);
} else {
self.command_line.push_error(
"CENTERREASSOCIATE: the selected source is not circular.",
);
}
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.refresh_properties();
}
CmdResult::ReplaceEntity(handle, new_entities) => {
if self.reject_locked_edit(i, handle) {
return Task::none();

View file

@ -482,43 +482,65 @@ impl OpenCADStudio {
"CENTERRESET" => {
let handles = self.tabs[i].scene.selected_handles_in_order();
self.push_undo_snapshot(i, "CENTERRESET");
let count = self.tabs[i].scene.reset_centerlines(&handles);
let count = self.tabs[i].scene.reset_centerlines(&handles)
+ self.tabs[i].scene.reset_center_marks(&handles);
if count > 0 {
self.tabs[i].dirty = true;
}
self.command_line
.push_output(&format!("CENTERRESET: {count} centerline(s) updated."));
.push_output(&format!("CENTERRESET: {count} center object(s) updated."));
}
"CENTERREASSOCIATE" => {
let handles = self.tabs[i].scene.selected_handles_in_order();
let mark_targets: Vec<_> = handles.iter().copied().filter(|handle| {
let Some(acadrust::EntityType::Line(line)) = self.tabs[i].scene.document.get_entity(*handle) else { return false; };
acadrust::entities::CenterMarkAssociation::read(&line.common.extended_data).is_some()
}).collect();
if mark_targets.len() == 1 && handles.len() == 1 {
use crate::modules::draw::draw::dimcenter::CenterMarkReassociateCommand;
let new_cmd = CenterMarkReassociateCommand::new(mark_targets[0]);
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
return Some(self.finish_dispatch(cmd));
}
self.push_undo_snapshot(i, "CENTERREASSOCIATE");
let count = self.tabs[i].scene.set_centerline_association(&handles, true);
let count = self.tabs[i].scene.set_centerline_association(&handles, true)
+ self.tabs[i].scene.set_center_mark_association(&handles, true);
if count > 0 {
self.tabs[i].dirty = true;
}
self.command_line
.push_output(&format!("CENTERREASSOCIATE: {count} centerline(s) associated."));
.push_output(&format!("CENTERREASSOCIATE: {count} center object(s) associated."));
}
"CENTERDISASSOCIATE" => {
let handles = self.tabs[i].scene.selected_handles_in_order();
self.push_undo_snapshot(i, "CENTERDISASSOCIATE");
let count = self.tabs[i].scene.set_centerline_association(&handles, false);
let count = self.tabs[i].scene.set_centerline_association(&handles, false)
+ self.tabs[i].scene.set_center_mark_association(&handles, false);
if count > 0 {
self.tabs[i].dirty = true;
}
self.command_line
.push_output(&format!("CENTERDISASSOCIATE: {count} centerline(s) detached."));
.push_output(&format!("CENTERDISASSOCIATE: {count} center object(s) detached."));
}
"DIMCENTER" | "CENTERMARK" => {
"DIMCENTER" => {
use crate::modules::draw::draw::dimcenter::DimCenterCommand;
let new_cmd = DimCenterCommand::new();
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
"CENTERMARK" => {
use crate::modules::draw::draw::dimcenter::CenterMarkCommand;
let settings = self.tabs[i].scene.centerline_settings();
let new_cmd = CenterMarkCommand::new(settings);
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
"SKETCH" => {
use crate::modules::draw::draw::sketch::SketchCommand;
let header = &self.tabs[i].scene.document.header;

View file

@ -938,6 +938,9 @@ impl OpenCADStudio {
| "CENTERLTYPE"
| "CENTERLTSCALE"
| "CENTERLTYPEFILE"
| "CENTERCROSSSIZE"
| "CENTERCROSSGAP"
| "CENTERMARKEXE"
) =>
{
return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i);
@ -960,7 +963,7 @@ impl OpenCADStudio {
let value = it.next().map(|s| s.trim().to_string());
if name.is_empty() || name == "?" {
self.command_line.push_info(
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG ATTREQ ATTDIA DIMASSOC ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE | CLAYER CELTYPE TEXTSTYLE (read-only)",
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG ATTREQ ATTDIA DIMASSOC ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE | CLAYER CELTYPE TEXTSTYLE (read-only)",
);
} else {
let frame_kind = crate::scene::frame::kind_for_name(&name);
@ -1044,6 +1047,9 @@ impl OpenCADStudio {
| "CENTERLTYPE"
| "CENTERLTSCALE"
| "CENTERLTYPEFILE"
| "CENTERCROSSSIZE"
| "CENTERCROSSGAP"
| "CENTERMARKEXE"
) {
let settings = self.tabs[i].scene.centerline_settings();
let current = match name.as_str() {
@ -1052,6 +1058,9 @@ impl OpenCADStudio {
"CENTERLTYPE" => settings.linetype,
"CENTERLTSCALE" => settings.linetype_scale.to_string(),
"CENTERLTYPEFILE" => settings.linetype_file,
"CENTERCROSSSIZE" => settings.cross_size,
"CENTERCROSSGAP" => settings.cross_gap,
"CENTERMARKEXE" => i16::from(settings.mark_extensions).to_string(),
_ => unreachable!(),
};
if let Some(value) = &value {

View file

@ -1844,14 +1844,16 @@ impl OpenCADStudio {
&mut entity,
);
// Smart centre lines carry their own drawing-level creation style.
// Smart centre objects carry their own drawing-level creation style.
// Apply it after the generic ribbon style so ordinary LINE entities
// keep the existing path while centre lines honour their settings.
if acadrust::entities::CenterLineAssociation::read(
let center_line = acadrust::entities::CenterLineAssociation::read(
&entity.common().extended_data,
)
.is_some()
{
).is_some();
let center_mark = acadrust::entities::CenterMarkAssociation::read(
&entity.common().extended_data,
).is_some();
if center_line || center_mark {
let settings = self.tabs[i].scene.centerline_settings();
if !settings.layer.eq_ignore_ascii_case("Current") {
entity.common_mut().layer = settings.layer;
@ -1860,15 +1862,18 @@ impl OpenCADStudio {
entity.common_mut().linetype = settings.linetype;
}
entity.common_mut().linetype_scale = settings.linetype_scale;
let application = if center_mark {
acadrust::entities::CENTERMARK_XDATA_APPLICATION
} else {
acadrust::entities::CENTERLINE_XDATA_APPLICATION
};
if !self.tabs[i]
.scene
.document
.app_ids
.contains(acadrust::entities::CENTERLINE_XDATA_APPLICATION)
.contains(application)
{
let mut app = acadrust::tables::AppId::new(
acadrust::entities::CENTERLINE_XDATA_APPLICATION,
);
let mut app = acadrust::tables::AppId::new(application);
app.handle = self.tabs[i].scene.document.allocate_handle();
let _ = self.tabs[i].scene.document.app_ids.add(app);
}

View file

@ -1206,6 +1206,12 @@ pub enum CmdResult {
ReplaceMany(Vec<(Handle, Vec<EntityType>)>, Vec<EntityType>),
/// Replace several entities as one undo step while keeping the command active.
ReplaceManyContinue(Vec<(Handle, Vec<EntityType>)>),
/// Attach one smart centre mark to a newly selected circular source.
ReassociateCenterMark {
target: Handle,
source: Handle,
point: DVec3,
},
/// Cancel: discard any preview and end the command.
Cancel,
/// Cancel because the active drawing space changed. Cleanup is identical

View file

@ -12,6 +12,62 @@ use crate::scene::model::object::{GripApply, GripDef, PropSection};
use crate::scene::model::wire_model::TangentGeom;
fn to_render(line: &Line) -> RenderEntity {
if let Some(association) =
acadrust::entities::CenterMarkAssociation::read(&line.common.extended_data)
{
let mut segments = crate::scene::centermark::mark_segments(&association);
if !association.associated {
let center = crate::scene::centermark::dvec(association.center);
let directions = crate::scene::centermark::mark_directions(&association);
let size = association.cross_size.max(association.radius * 0.08).max(1.0e-6) * 0.35;
let badge_center = center
+ directions[0] * (association.radius + association.extension_length + size * 2.0)
+ directions[2] * size * 2.0;
let corners = [
badge_center + directions[2] * size,
badge_center + directions[0] * size,
badge_center - directions[2] * size,
badge_center - directions[0] * size,
];
for index in 0..4 {
segments.push([corners[index], corners[(index + 1) % 4]]);
}
segments.push([
badge_center + directions[2] * size * 0.45,
badge_center - directions[2] * size * 0.2,
]);
segments.push([
badge_center - directions[2] * size * 0.55,
badge_center - directions[2] * size * 0.65,
]);
}
let mut points = Vec::with_capacity(segments.len() * 3);
let mut key_vertices = Vec::with_capacity(segments.len() * 2 + 1);
let mut tangent_geoms = Vec::with_capacity(segments.len());
for (index, segment) in segments.iter().enumerate() {
if index > 0 {
points.push([f64::NAN; 3]);
}
for point in segment {
points.push([point.x, point.y, point.z]);
key_vertices.push([point.x, point.y, point.z]);
}
tangent_geoms.push(TangentGeom::Line {
p1: [segment[0].x as f32, segment[0].y as f32, segment[0].z as f32],
p2: [segment[1].x as f32, segment[1].y as f32, segment[1].z as f32],
});
}
let center = crate::scene::centermark::dvec(association.center);
key_vertices.push([center.x, center.y, center.z]);
return RenderEntity {
pick_tris: Vec::new(),
object: RenderObject::Lines(points),
snap_pts: Vec::new(),
tangent_geoms,
key_vertices,
fill_tris: Vec::new(),
};
}
// LINE endpoints are stored in WCS — unlike the planar OCS entities
// (ARC/CIRCLE/LWPOLYLINE/TEXT), the extrusion normal on a LINE only
// orients its thickness sweep. Remapping the endpoints through the
@ -65,6 +121,29 @@ fn to_render(line: &Line) -> RenderEntity {
}
fn grips(line: &Line) -> Vec<GripDef> {
if let Some(association) =
acadrust::entities::CenterMarkAssociation::read(&line.common.extended_data)
{
let center = crate::scene::centermark::dvec(association.center);
let directions = crate::scene::centermark::mark_directions(&association);
let mut result = vec![center_grip(0, center)];
if !association.show_extensions {
return result;
}
for (index, direction) in directions.iter().enumerate() {
let distance = (association.radius + association.length_adjustments[index]).max(0.0);
result.push(square_grip(index + 1, center + *direction * distance));
}
for (index, direction) in directions.iter().enumerate() {
let distance = (association.radius
+ association.extension_length
+ association.length_adjustments[index]
+ association.overshoots[index])
.max(0.0);
result.push(oriented_triangle_grip(index + 5, center + *direction * distance, *direction));
}
return result;
}
let s = glam::DVec3::new(line.start.x, line.start.y, line.start.z);
let e = glam::DVec3::new(line.end.x, line.end.y, line.end.z);
let m = (s + e) * 0.5;
@ -88,6 +167,36 @@ fn grips(line: &Line) -> Vec<GripDef> {
}
fn properties(line: &Line) -> Vec<PropSection> {
if let Some(association) =
acadrust::entities::CenterMarkAssociation::read(&line.common.extended_data)
{
use crate::scene::model::object::{PropValue, Property};
return vec![PropSection {
title: t!("Geometry").into_owned(),
props: vec![
Property {
label: "Show extension".to_owned(),
field: "centermark_show_extension",
value: PropValue::Choice {
selected: if association.show_extensions { "Yes" } else { "No" }.to_owned(),
options: vec!["Yes".to_owned(), "No".to_owned()],
},
},
edit("Cross size", "centermark_cross_size", association.cross_size),
edit("Cross gap", "centermark_cross_gap", association.cross_gap),
edit(
"Extension length",
"centermark_extension_length",
association.extension_length,
),
ro(
"Associative",
"centermark_associative",
if association.associated { "Yes" } else { "No" },
),
],
}];
}
if let Some(association) =
acadrust::entities::CenterLineAssociation::read(&line.common.extended_data)
{
@ -136,6 +245,36 @@ fn properties(line: &Line) -> Vec<PropSection> {
}
fn apply_geom_prop(line: &mut Line, field: &str, value: &str) {
if let Some(mut association) =
acadrust::entities::CenterMarkAssociation::read(&line.common.extended_data)
{
match field {
"centermark_show_extension" => {
association.show_extensions = matches!(
value.trim().to_ascii_lowercase().as_str(),
"yes" | "on" | "true" | "1"
);
}
"centermark_cross_size" | "centermark_cross_gap" | "centermark_extension_length" => {
let Some(number) = parse_f64(value) else { return; };
if !number.is_finite() || number < 0.0 { return; }
match field {
"centermark_cross_size" => association.cross_size = number,
"centermark_cross_gap" => association.cross_gap = number,
"centermark_extension_length" => association.extension_length = number,
_ => unreachable!(),
}
if field == "centermark_cross_size" {
association.cross_size_relative = false;
} else if field == "centermark_cross_gap" {
association.cross_gap_relative = false;
}
}
_ => return,
}
crate::scene::centermark::update_carrier(line, &association);
return;
}
let Some(v) = parse_f64(value) else {
return;
};
@ -178,6 +317,37 @@ fn apply_geom_prop(line: &mut Line, field: &str, value: &str) {
}
fn apply_grip(line: &mut Line, grip_id: usize, apply: GripApply) {
if let Some(mut association) =
acadrust::entities::CenterMarkAssociation::read(&line.common.extended_data)
{
let center = crate::scene::centermark::dvec(association.center);
let directions = crate::scene::centermark::mark_directions(&association);
match (grip_id, apply) {
(0, GripApply::Translate(delta)) => {
let moved = center + delta;
association.center = acadrust::types::Vector3::new(moved.x, moved.y, moved.z);
let origin = crate::scene::centermark::dvec(association.plane_origin) + delta;
association.plane_origin = acadrust::types::Vector3::new(origin.x, origin.y, origin.z);
association.associated = false;
}
(id @ 1..=4, GripApply::Absolute(point)) => {
let index = id - 1;
let distance = (point - center).dot(directions[index]).max(0.0);
association.length_adjustments[index] = distance - association.radius;
}
(id @ 5..=8, GripApply::Absolute(point)) => {
let index = id - 5;
let distance = (point - center).dot(directions[index]).max(0.0);
association.overshoots[index] = distance
- association.radius
- association.extension_length
- association.length_adjustments[index];
}
_ => return,
}
crate::scene::centermark::update_carrier(line, &association);
return;
}
if let Some(mut association) =
acadrust::entities::CenterLineAssociation::read(&line.common.extended_data)
{
@ -250,18 +420,10 @@ fn apply_grip(line: &mut Line, grip_id: usize, apply: GripApply) {
}
}
fn apply_transform(line: &mut Line, t: &EntityTransform) {
if let Some(mut association) =
acadrust::entities::CenterLineAssociation::read(&line.common.extended_data)
{
association.associated = false;
association.write(&mut line.common.extended_data);
}
fn apply_plain_transform(line: &mut Line, t: &EntityTransform) {
match t {
EntityTransform::Translate(d) => {
line.translate(acadrust::types::Vector3::new(
d.x as f64, d.y as f64, d.z as f64,
));
line.translate(acadrust::types::Vector3::new(d.x, d.y, d.z));
}
EntityTransform::Rotate { center, axis, angle_rad } => {
crate::scene::view::transform::apply_standard_transform(line, *center, *axis, *angle_rad);
@ -273,9 +435,7 @@ fn apply_transform(line: &mut Line, t: &EntityTransform) {
acadrust::Entity::apply_transform(
line,
&crate::scene::view::transform::reflection_about_working_line(
*p1,
*p2,
*working_normal,
*p1, *p2, *working_normal,
),
);
}
@ -285,6 +445,68 @@ fn apply_transform(line: &mut Line, t: &EntityTransform) {
}
}
fn apply_transform(line: &mut Line, t: &EntityTransform) {
if let Some(mut association) =
acadrust::entities::CenterMarkAssociation::read(&line.common.extended_data)
{
if let EntityTransform::Translate(delta) = t {
let center = crate::scene::centermark::dvec(association.center) + *delta;
association.center = acadrust::types::Vector3::new(center.x, center.y, center.z);
let origin = crate::scene::centermark::dvec(association.plane_origin) + *delta;
association.plane_origin = acadrust::types::Vector3::new(origin.x, origin.y, origin.z);
association.associated = false;
crate::scene::centermark::update_carrier(line, &association);
return;
}
let center = crate::scene::centermark::dvec(association.center);
let x = crate::scene::centermark::dvec(association.plane_x).normalize_or(glam::DVec3::X);
let y = crate::scene::centermark::dvec(association.plane_y).normalize_or(glam::DVec3::Y);
let mut x_basis = Line::from_points(
acadrust::types::Vector3::new(center.x, center.y, center.z),
acadrust::types::Vector3::new(center.x + x.x, center.y + x.y, center.z + x.z),
);
let mut y_basis = Line::from_points(
acadrust::types::Vector3::new(center.x, center.y, center.z),
acadrust::types::Vector3::new(center.x + y.x, center.y + y.y, center.z + y.z),
);
apply_plain_transform(&mut x_basis, t);
apply_plain_transform(&mut y_basis, t);
let moved = glam::DVec3::new(x_basis.start.x, x_basis.start.y, x_basis.start.z);
let moved_x = glam::DVec3::new(x_basis.end.x, x_basis.end.y, x_basis.end.z) - moved;
let moved_y = glam::DVec3::new(y_basis.end.x, y_basis.end.y, y_basis.end.z) - moved;
let scale = ((moved_x.length() + moved_y.length()) * 0.5).max(1.0e-12);
association.center = acadrust::types::Vector3::new(moved.x, moved.y, moved.z);
association.plane_origin = association.center;
association.plane_x = acadrust::types::Vector3::new(
moved_x.normalize_or(x).x,
moved_x.normalize_or(x).y,
moved_x.normalize_or(x).z,
);
association.plane_y = acadrust::types::Vector3::new(
moved_y.normalize_or(y).x,
moved_y.normalize_or(y).y,
moved_y.normalize_or(y).z,
);
association.radius *= scale;
association.cross_size *= scale;
association.cross_gap *= scale;
association.extension_length *= scale;
for value in association.length_adjustments.iter_mut().chain(association.overshoots.iter_mut()) {
*value *= scale;
}
association.associated = false;
crate::scene::centermark::update_carrier(line, &association);
return;
}
if let Some(mut association) =
acadrust::entities::CenterLineAssociation::read(&line.common.extended_data)
{
association.associated = false;
association.write(&mut line.common.extended_data);
}
apply_plain_transform(line, t);
}
impl RenderConvertible for Line {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
Some(to_render(self))
@ -300,6 +522,16 @@ impl crate::entities::traits::Grippable for Line {
}
fn grip_menu(&self, grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
if acadrust::entities::CenterMarkAssociation::read(&self.common.extended_data).is_some() {
return if grip_id == 0 {
vec![GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch }]
} else {
vec![
GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch },
GripMenuItem { label: "Lengthen", action: GripMenuAction::Lengthen },
]
};
}
if grip_id == 2 {
vec![GripMenuItem {
label: "Stretch",

View file

@ -13,6 +13,8 @@ use acadrust::EntityType;
pub fn ui_name(e: &EntityType) -> &'static str {
match e {
EntityType::Point(_) => "Point",
EntityType::Line(line)
if acadrust::entities::CenterMarkAssociation::read(&line.common.extended_data).is_some() => "Center Mark",
EntityType::Line(_) => "Line",
EntityType::Circle(_) => "Circle",
EntityType::Arc(_) => "Arc",

View file

@ -111,6 +111,14 @@ pub trait TextContent {
pub fn entity_type_name(et: &EntityType) -> &str {
match et {
EntityType::Point(_) => "Point",
EntityType::Line(line)
if acadrust::entities::CenterMarkAssociation::read(
&line.common.extended_data,
)
.is_some() =>
{
"CenterMark"
}
EntityType::Line(line)
if acadrust::entities::CenterLineAssociation::read(
&line.common.extended_data,

View file

@ -8,22 +8,26 @@
// `m = radius * 0.2`. Both lines are committed at once and the command ends.
use acadrust::types::Vector3;
use acadrust::entities::{CenterMarkAssociation, CenterMarkSource};
use acadrust::{EntityType, Handle, Line};
use glam::DVec3;
use crate::t;
use crate::command::{CadCommand, CmdResult};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::centerline::{
center_measure_is_relative, resolve_center_measure, CenterLineSettings,
};
// ── Ribbon definition ─────────────────────────────────────────────────────
#[allow(dead_code)] // ribbon definition ready for wiring; command works via the command line
pub fn tool() -> ToolDef {
ToolDef {
id: "DIMCENTER",
id: "CENTERMARK",
label: "Center Mark",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/line.svg")),
event: ModuleEvent::Command("DIMCENTER".to_string()),
event: ModuleEvent::Command("CENTERMARK".to_string()),
}
}
@ -133,6 +137,119 @@ impl CadCommand for DimCenterCommand {
}
}
/// Associative centre-mark command. Each accepted pick commits one smart mark
/// and keeps the command active so further circular objects can be selected.
pub struct CenterMarkCommand {
picked: Option<EntityType>,
settings: CenterLineSettings,
}
impl CenterMarkCommand {
pub(crate) fn new(settings: CenterLineSettings) -> Self {
Self { picked: None, settings }
}
fn build_mark(
&self,
source: CenterMarkSource,
center: DVec3,
radius: f64,
x: DVec3,
y: DVec3,
) -> EntityType {
let diameter = radius * 2.0;
let association = CenterMarkAssociation {
source,
plane_origin: Vector3::new(center.x, center.y, center.z),
plane_x: Vector3::new(x.x, x.y, x.z),
plane_y: Vector3::new(y.x, y.y, y.z),
center: Vector3::new(center.x, center.y, center.z),
radius,
cross_size: resolve_center_measure(&self.settings.cross_size, diameter, 0.1),
cross_gap: resolve_center_measure(&self.settings.cross_gap, diameter, 0.05),
cross_size_relative: center_measure_is_relative(&self.settings.cross_size),
cross_gap_relative: center_measure_is_relative(&self.settings.cross_gap),
extension_length: self.settings.extension,
length_adjustments: [0.0; 4],
overshoots: [0.0; 4],
show_extensions: self.settings.mark_extensions,
associated: true,
};
let mut line = Line::from_points(
Vector3::new(center.x, center.y, center.z),
Vector3::new(center.x, center.y, center.z),
);
crate::scene::centermark::update_carrier(&mut line, &association);
EntityType::Line(line)
}
}
impl CadCommand for CenterMarkCommand {
fn name(&self) -> &'static str { "CENTERMARK" }
fn prompt(&self) -> String {
t!("CENTERMARK Select arc or circle <finish>:").into_owned()
}
fn needs_entity_pick(&self) -> bool { true }
fn inject_before_entity_pick(&self) -> bool { true }
fn inject_picked_entity(&mut self, entity: EntityType) {
self.picked = Some(entity);
}
fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult {
if handle.is_null() {
return CmdResult::NeedPoint;
}
let Some((source, center, radius, x, y)) = self
.picked
.as_ref()
.and_then(|entity| crate::scene::centermark::picked_mark_source(entity, handle, point))
else {
return CmdResult::NeedPoint;
};
CmdResult::CommitEntity(self.build_mark(source, center, radius, x, y))
}
fn on_point(&mut self, _point: DVec3) -> CmdResult { CmdResult::NeedPoint }
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel }
}
pub struct CenterMarkReassociateCommand {
target: Handle,
picked: Option<EntityType>,
}
impl CenterMarkReassociateCommand {
pub fn new(target: Handle) -> Self {
Self { target, picked: None }
}
}
impl CadCommand for CenterMarkReassociateCommand {
fn name(&self) -> &'static str { "CENTERREASSOCIATE" }
fn prompt(&self) -> String {
t!("CENTERREASSOCIATE Select new arc or circle:").into_owned()
}
fn needs_entity_pick(&self) -> bool { true }
fn inject_before_entity_pick(&self) -> bool { true }
fn inject_picked_entity(&mut self, entity: EntityType) { self.picked = Some(entity); }
fn on_entity_pick(&mut self, source: Handle, point: DVec3) -> CmdResult {
let valid = self.picked.as_ref().and_then(|entity| {
crate::scene::centermark::picked_mark_source(entity, source, point)
}).is_some();
if !valid {
return CmdResult::NeedPoint;
}
CmdResult::ReassociateCenterMark { target: self.target, source, point }
}
fn on_point(&mut self, _point: DVec3) -> CmdResult { CmdResult::NeedPoint }
fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel }
fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel }
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration {
names: &["DIMCENTER", "DCE", "CENTERMARK"]

View file

@ -63,6 +63,27 @@ pub fn explode_polyline_segments(entity: &EntityType) -> Vec<EntityType> {
/// Returns an empty vec if the entity cannot be exploded.
pub fn explode_entity(entity: &EntityType, document: &CadDocument) -> Vec<EntityType> {
match entity {
EntityType::Line(line) => {
let Some(association) = acadrust::entities::CenterMarkAssociation::read(
&line.common.extended_data,
) else {
return vec![];
};
crate::scene::centermark::mark_segments(&association)
.into_iter()
.map(|segment| {
let mut common = line.common.clone();
common.handle = Handle::NULL;
acadrust::entities::CenterMarkAssociation::remove(&mut common.extended_data);
EntityType::Line(LineEnt {
common,
start: Vector3::new(segment[0].x, segment[0].y, segment[0].z),
end: Vector3::new(segment[1].x, segment[1].y, segment[1].z),
..LineEnt::new()
})
})
.collect()
}
EntityType::LwPolyline(p) => explode_lwpolyline(p),
EntityType::Polyline2D(p) => explode_polyline2d(p),
EntityType::Polyline(p) => explode_polyline(p),

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
src/scene/centermark.rs Normal file
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()
}
}

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