diff --git a/Cargo.lock b/Cargo.lock index 93225764..01919784 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 0182714c..7b2f3a35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/crates/ocs_plugin_api/Cargo.toml b/crates/ocs_plugin_api/Cargo.toml index d5ec1fb6..e367ad7f 100644 --- a/crates/ocs_plugin_api/Cargo.toml +++ b/crates/ocs_plugin_api/Cargo.toml @@ -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" diff --git a/crates/ocs_web_worker/Cargo.toml b/crates/ocs_web_worker/Cargo.toml index 3d662239..739bc690 100644 --- a/crates/ocs_web_worker/Cargo.toml +++ b/crates/ocs_web_worker/Cargo.toml @@ -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" diff --git a/crates/plugin-template-api2/Cargo.toml b/crates/plugin-template-api2/Cargo.toml index 365b8014..89cea10e 100644 --- a/crates/plugin-template-api2/Cargo.toml +++ b/crates/plugin-template-api2/Cargo.toml @@ -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"] } diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index e0bf9acc..1df31b75 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -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(); diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index 6433fc08..c3fd00ed 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -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; diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index 6dca6de4..de326abb 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -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 { diff --git a/src/app/properties.rs b/src/app/properties.rs index ae41ebb9..7331d13d 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -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); } diff --git a/src/command.rs b/src/command.rs index 96f5e4d0..34a5446a 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1206,6 +1206,12 @@ pub enum CmdResult { ReplaceMany(Vec<(Handle, Vec)>, Vec), /// Replace several entities as one undo step while keeping the command active. ReplaceManyContinue(Vec<(Handle, Vec)>), + /// 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 diff --git a/src/entities/line.rs b/src/entities/line.rs index 851082fe..1b9cf904 100644 --- a/src/entities/line.rs +++ b/src/entities/line.rs @@ -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 { + 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 { } fn properties(line: &Line) -> Vec { + 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 { } 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 { Some(to_render(self)) @@ -300,6 +522,16 @@ impl crate::entities::traits::Grippable for Line { } fn grip_menu(&self, grip_id: usize) -> Vec { 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", diff --git a/src/entities/names.rs b/src/entities/names.rs index 55deabf1..ef766de9 100644 --- a/src/entities/names.rs +++ b/src/entities/names.rs @@ -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", diff --git a/src/entities/traits.rs b/src/entities/traits.rs index ed736a43..ca716388 100644 --- a/src/entities/traits.rs +++ b/src/entities/traits.rs @@ -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, diff --git a/src/modules/draw/draw/dimcenter.rs b/src/modules/draw/draw/dimcenter.rs index c725c7e6..645b9da3 100644 --- a/src/modules/draw/draw/dimcenter.rs +++ b/src/modules/draw/draw/dimcenter.rs @@ -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, + 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 :").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, +} + +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"] diff --git a/src/modules/draw/modify/explode.rs b/src/modules/draw/modify/explode.rs index fa1789c2..302033f9 100644 --- a/src/modules/draw/modify/explode.rs +++ b/src/modules/draw/modify/explode.rs @@ -63,6 +63,27 @@ pub fn explode_polyline_segments(entity: &EntityType) -> Vec { /// Returns an empty vec if the entity cannot be exploded. pub fn explode_entity(entity: &EntityType, document: &CadDocument) -> Vec { 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), diff --git a/src/scene/centerline.rs b/src/scene/centerline.rs index 3c8d699d..78e76651 100644 --- a/src/scene/centerline.rs +++ b/src/scene/centerline.rs @@ -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::().is_ok_and(|value| value.is_finite() && value > 0.0) + }) + || trimmed.parse::().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::().ok()) { + return diameter * factor.max(0.0); + } + text.parse::().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']) +} diff --git a/src/scene/centermark.rs b/src/scene/centermark.rs new file mode 100644 index 00000000..bff9aba2 --- /dev/null +++ b/src/scene/centermark.rs @@ -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() + } +} diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 9ecb642e..8b9dbbeb 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -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);