Merge pull request #850 from ramox81/codex/associative-center-mark

This commit is contained in:
Hakan Seven 2026-08-21 20:19:54 +03:00
commit b648000e78
21 changed files with 1672 additions and 241 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/HakanSeven12/cadcodec.git?rev=8a92803#8a9280381d75a52ac5a35663449bdd44827e1463"
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/HakanSeven12/cadkernel.git?rev=b1901dd#b1901dde3351fc4bbcdffbdcf7328a5dec4d28d3"
source = "git+https://github.com/HakanSeven12/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/HakanSeven12/cadcodec.git", rev = "8a92803", features = ["serde"] }
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "b1901dd", features = ["acis", "offset"] }
acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "6dcda1a", features = ["serde"] }
cadkernel = { git = "https://github.com/HakanSeven12/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/HakanSeven12/cadcodec.git", rev = "8a92803", 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/HakanSeven12/cadcodec.git", rev = "8a92803", 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/HakanSeven12/cadcodec.git", rev = "8a92803", 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 = "0.3.4"
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

@ -473,18 +473,74 @@ impl OpenCADStudio {
"CENTERLINE" => {
use crate::modules::draw::draw::centerline::CenterLineCommand;
let new_cmd = CenterLineCommand::new();
let settings = self.tabs[i].scene.centerline_settings();
let new_cmd = CenterLineCommand::new(settings);
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
"DIMCENTER" | "CENTERMARK" => {
"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)
+ self.tabs[i].scene.reset_center_marks(&handles);
if count > 0 {
self.tabs[i].dirty = true;
}
self.command_line
.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)
+ 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} 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)
+ 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} center object(s) detached."));
}
"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

@ -933,6 +933,14 @@ impl OpenCADStudio {
| "SKETCHINC"
| "SKPOLY"
| "SKTOLERANCE"
| "CENTEREXE"
| "CENTERLAYER"
| "CENTERLTYPE"
| "CENTERLTSCALE"
| "CENTERLTYPEFILE"
| "CENTERCROSSSIZE"
| "CENTERCROSSGAP"
| "CENTERMARKEXE"
) =>
{
return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i);
@ -955,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 | 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);
@ -1032,6 +1040,45 @@ impl OpenCADStudio {
}
return Some(self.finish_dispatch(cmd));
}
if matches!(
name.as_str(),
"CENTEREXE"
| "CENTERLAYER"
| "CENTERLTYPE"
| "CENTERLTSCALE"
| "CENTERLTYPEFILE"
| "CENTERCROSSSIZE"
| "CENTERCROSSGAP"
| "CENTERMARKEXE"
) {
let settings = self.tabs[i].scene.centerline_settings();
let current = match name.as_str() {
"CENTEREXE" => settings.extension.to_string(),
"CENTERLAYER" => settings.layer,
"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 {
match self.tabs[i].scene.set_centerline_setting(&name, value) {
Ok(message) => {
self.tabs[i].dirty = true;
self.command_line.push_output(&message);
}
Err(error) => self.command_line.push_error(&error),
}
} else {
self.command_line.push_output(crate::tf!(
"Enter new value for {name} <{current}>:"
).as_ref());
self.pending_setvar = Some(name.clone());
}
return Some(self.finish_dispatch(cmd));
}
// Parse a boolean given as 0/1 or ON/OFF.
let parse_bool = |s: &str| match s.to_uppercase().as_str() {
"1" | "ON" | "TRUE" => Some(true),

View file

@ -1844,6 +1844,41 @@ impl OpenCADStudio {
&mut entity,
);
// 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.
let center_line = acadrust::entities::CenterLineAssociation::read(
&entity.common().extended_data,
).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;
}
if !settings.linetype.eq_ignore_ascii_case("Current") {
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(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);
}
}
let text_style_annotative = match &entity {
acadrust::EntityType::Text(text) => {
crate::scene::annotative::text_style_is_annotative(

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

@ -3,7 +3,8 @@ use crate::t;
use crate::command::EntityTransform;
use crate::entities::common::{
center_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip,
center_grip, edit_prop as edit, oriented_triangle_grip, parse_f64, ro_prop as ro,
square_grip,
};
use crate::entities::traits::RenderConvertible;
use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject};
@ -11,6 +12,37 @@ 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 segments = crate::scene::centermark::render_segments(&association);
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
@ -64,13 +96,106 @@ 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)];
let extension_start = association.cross_size * 0.5 + association.cross_gap;
if !association.show_extensions || association.radius <= extension_start {
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(extension_start);
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;
if let Some(association) =
acadrust::entities::CenterLineAssociation::read(&line.common.extended_data)
{
let direction = (e - s).normalize_or(glam::DVec3::X);
let base_start = s + direction * association.start_extension;
let base_end = e - direction * association.end_extension;
return vec![
square_grip(0, base_start),
square_grip(1, base_end),
center_grip(2, m),
oriented_triangle_grip(3, s, -direction),
oriented_triangle_grip(4, e, direction),
];
}
vec![square_grip(0, s), square_grip(1, e), center_grip(2, m)]
}
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)
{
return vec![PropSection {
title: t!("Geometry").into_owned(),
props: vec![
edit(
"Start extension",
"centerline_start_extension",
association.start_extension,
),
edit(
"End extension",
"centerline_end_extension",
association.end_extension,
),
ro(t!("Length").as_ref(), "length", format!("{:.4}", line.length())),
ro(
"Associative",
"centerline_associative",
if association.associated { "Yes" } else { "No" },
),
],
}];
}
let dx = line.end.x - line.start.x;
let dy = line.end.y - line.start.y;
let dz = line.end.z - line.start.z;
@ -94,9 +219,66 @@ 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;
};
if let Some(mut association) =
acadrust::entities::CenterLineAssociation::read(&line.common.extended_data)
{
if !v.is_finite() || v < 0.0 {
return;
}
let start = glam::DVec3::new(line.start.x, line.start.y, line.start.z);
let end = glam::DVec3::new(line.end.x, line.end.y, line.end.z);
let direction = (end - start).normalize_or(glam::DVec3::X);
match field {
"centerline_start_extension" => {
let delta = v - association.start_extension;
let moved = start - direction * delta;
line.start = acadrust::types::Vector3::new(moved.x, moved.y, moved.z);
association.start_extension = v;
}
"centerline_end_extension" => {
let delta = v - association.end_extension;
let moved = end + direction * delta;
line.end = acadrust::types::Vector3::new(moved.x, moved.y, moved.z);
association.end_extension = v;
}
_ => return,
}
association.write(&mut line.common.extended_data);
return;
}
match field {
"start_x" => line.start.x = v,
"start_y" => line.start.y = v,
@ -109,6 +291,84 @@ 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)
{
let start = glam::DVec3::new(line.start.x, line.start.y, line.start.z);
let end = glam::DVec3::new(line.end.x, line.end.y, line.end.z);
let direction = (end - start).normalize_or(glam::DVec3::X);
let base_start = start + direction * association.start_extension;
let base_end = end - direction * association.end_extension;
match (grip_id, apply) {
(0, GripApply::Absolute(point)) => {
let delta = (point - base_start).dot(direction);
association.start_length_adjustment -= delta;
let moved = start + direction * delta;
line.start = acadrust::types::Vector3::new(moved.x, moved.y, moved.z);
}
(1, GripApply::Absolute(point)) => {
let delta = (point - base_end).dot(direction);
association.end_length_adjustment += delta;
let moved = end + direction * delta;
line.end = acadrust::types::Vector3::new(moved.x, moved.y, moved.z);
}
(2, GripApply::Translate(delta)) => {
line.start.x += delta.x;
line.start.y += delta.y;
line.start.z += delta.z;
line.end.x += delta.x;
line.end.y += delta.y;
line.end.z += delta.z;
association.associated = false;
}
(3, GripApply::Absolute(point)) => {
let total = (base_start - point).dot(direction).max(0.0);
association.start_extension = total;
let moved = base_start - direction * association.start_extension;
line.start = acadrust::types::Vector3::new(moved.x, moved.y, moved.z);
}
(4, GripApply::Absolute(point)) => {
let total = (point - base_end).dot(direction).max(0.0);
association.end_extension = total;
let moved = base_end + direction * association.end_extension;
line.end = acadrust::types::Vector3::new(moved.x, moved.y, moved.z);
}
_ => return,
}
association.write(&mut line.common.extended_data);
return;
}
match (grip_id, apply) {
(0, GripApply::Absolute(p)) => {
line.start.x = p.x as f64;
@ -132,12 +392,10 @@ fn apply_grip(line: &mut Line, grip_id: usize, apply: GripApply) {
}
}
fn apply_transform(line: &mut Line, t: &EntityTransform) {
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);
@ -149,9 +407,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,
),
);
}
@ -161,6 +417,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))
@ -176,6 +494,14 @@ 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()
|| acadrust::entities::CenterLineAssociation::read(&self.common.extended_data).is_some()
{
return vec![GripMenuItem {
label: "Stretch",
action: GripMenuAction::Stretch,
}];
}
if grip_id == 2 {
vec![GripMenuItem {
label: "Stretch",

View file

@ -13,6 +13,10 @@ 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)
if acadrust::entities::CenterLineAssociation::read(&line.common.extended_data).is_some() => "Center Line",
EntityType::Line(_) => "Line",
EntityType::Circle(_) => "Circle",
EntityType::Arc(_) => "Arc",

View file

@ -111,6 +111,22 @@ 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,
)
.is_some() =>
{
"CenterLine"
}
EntityType::Line(_) => "Line",
EntityType::Circle(_) => "Circle",
EntityType::Arc(_) => "Arc",

View file

@ -265,12 +265,21 @@ pub fn extract_pattern(desc: &str) -> String {
/// Add all standard OpenCADStudio linetypes to `doc`, skipping existing ones.
pub fn populate_document(doc: &mut CadDocument) {
for mut lt in parse(LIN_SOURCE) {
populate_document_from_source(doc, LIN_SOURCE);
}
/// Add all simple linetypes parsed from a caller-supplied `.lin` source.
/// Returns the number of definitions newly registered in the drawing.
pub fn populate_document_from_source(doc: &mut CadDocument, source: &str) -> usize {
let mut added = 0;
for mut lt in parse(source) {
if !doc.line_types.contains(&lt.name) {
lt.set_handle(doc.allocate_handle());
doc.line_types.add(lt).ok();
added += 1;
}
}
added
}

View file

@ -1,218 +1,50 @@
// Centre line tool — interactive command.
//
// Command: CENTERLINE — pick two straight Line entities and draw a single
// centre line between them, committed as one Line entity.
//
// * If the two picked lines are (near) parallel, the result is the midline:
// a line running between the two, centred on the average of their
// midpoints, oriented along the averaged direction, with a length equal to
// the longer of the two source lines.
// * If the two picked lines cross, the result is the bisector of the acute
// angle at their intersection point, drawn symmetrically either side of
// that point.
//
// The command picks the first line on the first click, stores it, prompts for
// the second line, then computes the centre line and ends.
//! Associative centre-line construction from lines and linear polyline segments.
use acadrust::entities::{CenterLineAssociation, CenterLineSource};
use acadrust::types::Vector3;
use acadrust::{EntityType, Handle, Line};
use acadrust::{EntityType, Handle};
use glam::DVec3;
use crate::t;
use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::centerline::{construct_line, picked_source, CenterLineSettings};
use crate::t;
// ── Ribbon definition ─────────────────────────────────────────────────────
#[allow(dead_code)] // ribbon definition ready for wiring; command works via the command line
#[allow(dead_code)]
pub fn tool() -> ToolDef {
ToolDef {
id: "CENTERLINE",
label: "Center Line",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/line.svg")),
event: ModuleEvent::Command("CENTERLINE".to_string()),
event: ModuleEvent::Command("CENTERLINE".to_owned()),
}
}
// ── Geometry helpers ──────────────────────────────────────────────────────
/// A line reduced to the quantities the centre-line maths needs.
#[derive(Clone, Copy)]
struct LineGeom {
#[derive(Clone)]
struct PickedSource {
source: CenterLineSource,
start: DVec3,
end: DVec3,
}
impl LineGeom {
fn from_line(line: &Line) -> Self {
Self {
start: DVec3::new(line.start.x, line.start.y, line.start.z),
end: DVec3::new(line.end.x, line.end.y, line.end.z),
}
}
fn midpoint(&self) -> DVec3 {
(self.start + self.end) * 0.5
}
fn length(&self) -> f64 {
(self.end - self.start).length()
}
/// Normalized direction in 3D, or `None` if the line is degenerate.
fn dir(&self) -> Option<DVec3> {
let d = self.end - self.start;
let len = d.length();
if len <= 1e-9 {
None
} else {
Some(d / len)
}
}
fn vector(point: DVec3) -> Vector3 {
Vector3::new(point.x, point.y, point.z)
}
/// 2D cross product of the XY components of two vectors.
fn cross_xy(a: DVec3, b: DVec3) -> f64 {
a.x * b.y - a.y * b.x
}
fn to_v3(p: DVec3) -> Vector3 {
Vector3::new(p.x, p.y, p.z)
}
/// Compute the centre line between two source lines. `None` when the result
/// would be degenerate (zero-length sources, or coincident parallel lines that
/// give no usable direction).
fn compute_center_line(a: LineGeom, b: LineGeom) -> Option<Line> {
let da = a.dir()?;
let db = b.dir()?;
// Average Z of the four endpoints keeps the result on a sensible plane even
// when the picked lines sit at slightly different elevations.
let avg_z = (a.start.z + a.end.z + b.start.z + b.end.z) * 0.25;
let parallel = cross_xy(da, db).abs() <= 1e-9;
if parallel {
// Midline: average direction (flip db so the two add constructively),
// centred on the average of the two midpoints, length = longer source.
let db_aligned = if da.dot(db) < 0.0 { -db } else { db };
let avg = da + db_aligned;
let dir = if avg.length() <= 1e-9 {
da
} else {
avg / avg.length()
};
let dir = DVec3::new(dir.x, dir.y, 0.0);
let dir = if dir.length() <= 1e-9 {
return None;
} else {
dir / dir.length()
};
let mid = (a.midpoint() + b.midpoint()) * 0.5;
let half = a.length().max(b.length()) * 0.5;
if half <= 1e-9 {
return None;
}
let center = DVec3::new(mid.x, mid.y, avg_z);
let p0 = center - dir * half;
let p1 = center + dir * half;
Some(Line::from_points(to_v3(p0), to_v3(p1)))
} else {
// Intersecting: bisector of the acute angle at the intersection point.
// Solve the 2D line-line intersection in XY.
// a.start + t*da = b.start + s*db
let r = da; // direction of line a
let s = db; // direction of line b
let denom = cross_xy(r, s);
if denom.abs() <= 1e-12 {
return None;
}
let qp = b.start - a.start;
let t = cross_xy(qp, s) / denom;
let ix = a.start.x + r.x * t;
let iy = a.start.y + r.y * t;
let p = DVec3::new(ix, iy, avg_z);
// Orient both directions away from the intersection point, choosing the
// half of each line whose midpoint lies on the side we keep, so the two
// oriented directions bisect the acute angle between the lines.
let a_away = orient_away(p, a);
let b_away = orient_away(p, b);
// For an acute-angle bisector, flip b_away if the two oriented dirs open
// obtuse (their dot is negative).
let b_oriented = if a_away.dot(b_away) < 0.0 {
-b_away
} else {
b_away
};
let bis = a_away + b_oriented;
let bis = DVec3::new(bis.x, bis.y, 0.0);
if bis.length() <= 1e-9 {
return None;
}
let dir = bis / bis.length();
let half = ((a.length() + b.length()) * 0.5) * 0.5;
if half <= 1e-9 {
return None;
}
let p0 = p - dir * half;
let p1 = p + dir * half;
Some(Line::from_points(to_v3(p0), to_v3(p1)))
}
}
/// Unit direction from `p` toward the farther endpoint of `line` (the side that
/// extends away from the intersection point).
fn orient_away(p: DVec3, line: LineGeom) -> DVec3 {
let to_start = line.start - p;
let to_end = line.end - p;
let pick = if to_end.length() >= to_start.length() {
to_end
} else {
to_start
};
let len = pick.length();
if len <= 1e-9 {
// Fall back to the raw line direction if the pick degenerates.
line.dir().unwrap_or(DVec3::X)
} else {
pick / len
}
}
// ── Command implementation ────────────────────────────────────────────────
pub struct CenterLineCommand {
/// First picked Line's geometry, captured once the first pick lands.
first: Option<LineGeom>,
/// The entity injected by the host before `on_entity_pick` runs; `None`
/// until the host injects it.
first: Option<PickedSource>,
picked: Option<EntityType>,
plane: WorkingPlane,
settings: CenterLineSettings,
}
impl CenterLineCommand {
pub fn new() -> Self {
pub(crate) fn new(settings: CenterLineSettings) -> Self {
Self {
first: None,
picked: None,
plane: WorkingPlane::default(),
}
}
/// Extract a `LineGeom` from a picked entity; `None` for anything else.
fn as_line(&self, entity: &EntityType) -> Option<LineGeom> {
match entity {
EntityType::Line(line) => {
let mut geom = LineGeom::from_line(line);
geom.start = self.plane.to_local(geom.start);
geom.end = self.plane.to_local(geom.end);
Some(geom)
}
_ => None,
settings,
}
}
}
@ -246,39 +78,53 @@ impl CadCommand for CenterLineCommand {
self.picked = Some(entity);
}
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult {
if handle.is_null() {
return CmdResult::NeedPoint;
}
// Resolve the just-picked entity; ignore the pick if it isn't a Line.
let picked = self.picked.take();
let geom = match picked.as_ref().and_then(|entity| self.as_line(entity)) {
Some(g) => g,
None => return CmdResult::NeedPoint,
let Some(entity) = self.picked.take() else {
return CmdResult::NeedPoint;
};
// Reject zero-length picks rather than corrupting the result.
if geom.length() <= 1e-9 {
let Some((source, start, end)) = picked_source(&entity, handle, point) else {
return CmdResult::NeedPoint;
};
let picked = PickedSource { source, start, end };
let Some(first) = self.first.take() else {
self.first = Some(picked);
return CmdResult::NeedPoint;
};
if first.source.handle == picked.source.handle
&& first.source.segment_index == picked.source.segment_index
{
self.first = Some(first);
return CmdResult::NeedPoint;
}
match self.first {
None => {
// First line captured; keep prompting for the second.
self.first = Some(geom);
CmdResult::NeedPoint
}
Some(first) => match compute_center_line(first, geom) {
Some(line) => CmdResult::CommitAndExit(
self.plane.place_entity(EntityType::Line(line)),
),
// Degenerate combination (coincident / no usable result):
// keep prompting for a usable second line.
None => CmdResult::NeedPoint,
},
}
let association = CenterLineAssociation {
first: first.source.clone(),
second: picked.source.clone(),
plane_origin: vector(self.plane.origin),
plane_x: vector(self.plane.x),
plane_y: vector(self.plane.y),
start_extension: self.settings.extension,
end_extension: self.settings.extension,
start_length_adjustment: 0.0,
end_length_adjustment: 0.0,
associated: true,
};
let Some(mut line) = construct_line(
(first.start, first.end),
(picked.start, picked.end),
&association,
) else {
self.first = Some(first);
return CmdResult::NeedPoint;
};
association.write(&mut line.common.extended_data);
CmdResult::CommitAndExit(EntityType::Line(line))
}
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
fn on_point(&mut self, _point: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
@ -291,7 +137,11 @@ impl CadCommand for CenterLineCommand {
}
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration {
names: &["CENTERLINE"]
}); // CenterLineCommand
names: &[
"CENTERLINE",
"CENTERRESET",
"CENTERREASSOCIATE",
"CENTERDISASSOCIATE",
]
});

View file

@ -1,29 +1,26 @@
// Center mark tool — interactive command.
//
// Command: DIMCENTER (aliases DCE, CENTERMARK) — pick a Circle or Arc and draw
// a small cross of two Line entities through its center. Each arm of the cross
// extends a distance of `radius * 0.2` from the center in the four cardinal
// directions, so the result is a horizontal line from (cx - m, cy) to
// (cx + m, cy) and a vertical line from (cx, cy - m) to (cx, cy + m), with
// `m = radius * 0.2`. Both lines are committed at once and the command ends.
// Legacy centre cross and associative centre mark commands.
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 +130,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),

437
src/scene/centerline.rs Normal file
View file

@ -0,0 +1,437 @@
use super::*;
use acadrust::entities::{
CenterLineAssociation, CenterLineSource, CenterLineSourceKind,
};
use acadrust::objects::{XRecordEntry, XRecordValue};
use acadrust::types::Vector3;
use cadkernel::geom2d::{centerline_between, closest_point, Curve, Line as KernelLine};
use glam::DVec3;
use crate::command::WorkingPlane;
const SETTINGS_RECORD: &str = "OPEN_CAD_CENTERLINE_SETTINGS";
#[derive(Clone, Debug)]
pub(crate) struct CenterLineSettings {
pub extension: f64,
pub layer: String,
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 {
fn default() -> Self {
Self {
extension: 0.12,
layer: "Current".to_owned(),
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,
}
}
}
fn vector(point: DVec3) -> Vector3 {
Vector3::new(point.x, point.y, point.z)
}
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 segment_distance(point: [f64; 2], start: [f64; 2], end: [f64; 2]) -> f64 {
closest_point(&Curve::Line(KernelLine { start, end }), point).distance
}
fn segment_indices(vertex_count: usize, closed: bool) -> impl Iterator<Item = (usize, usize)> {
let count = if closed && vertex_count > 2 {
vertex_count
} else {
vertex_count.saturating_sub(1)
};
(0..count).map(move |start| (start, (start + 1) % vertex_count.max(1)))
}
/// Resolve a user pick to a line or the closest linear polyline segment.
pub(crate) fn picked_source(
entity: &EntityType,
handle: Handle,
pick: DVec3,
) -> Option<(CenterLineSource, DVec3, DVec3)> {
match entity {
EntityType::Line(line) if line.length() > 1.0e-10 => Some((
CenterLineSource {
handle,
kind: CenterLineSourceKind::Line,
segment_index: -1,
pick_point: vector(pick),
},
dvec(line.start),
dvec(line.end),
)),
EntityType::LwPolyline(polyline) => {
let local_pick = crate::scene::view::transform::wcs_point_to_ocs(
(pick.x, pick.y, pick.z),
(polyline.normal.x, polyline.normal.y, polyline.normal.z),
);
let pick_2d = [local_pick.0, local_pick.1];
segment_indices(polyline.vertices.len(), polyline.is_closed)
.filter(|(start, _)| polyline.vertices[*start].bulge.abs() <= 1.0e-12)
.filter_map(|(start, end)| {
let a = polyline.vertices[start].location;
let b = polyline.vertices[end].location;
((a.x - b.x).hypot(a.y - b.y) > 1.0e-10).then_some((start, a, b))
})
.min_by(|(_, a, b), (_, c, d)| {
segment_distance(pick_2d, [a.x, a.y], [b.x, b.y])
.total_cmp(&segment_distance(pick_2d, [c.x, c.y], [d.x, d.y]))
})
.map(|(index, a, b)| {
let start = ocs_point((a.x, a.y, polyline.elevation), polyline.normal);
let end = ocs_point((b.x, b.y, polyline.elevation), polyline.normal);
(
CenterLineSource {
handle,
kind: CenterLineSourceKind::LwPolylineSegment,
segment_index: index as i32,
pick_point: vector(pick),
},
start,
end,
)
})
}
EntityType::Polyline2D(polyline) => {
let local_pick = crate::scene::view::transform::wcs_point_to_ocs(
(pick.x, pick.y, pick.z),
(polyline.normal.x, polyline.normal.y, polyline.normal.z),
);
let pick_2d = [local_pick.0, local_pick.1];
segment_indices(polyline.vertices.len(), polyline.is_closed())
.filter(|(start, _)| polyline.vertices[*start].bulge.abs() <= 1.0e-12)
.filter_map(|(start, end)| {
let a = polyline.vertices[start].location;
let b = polyline.vertices[end].location;
((a.x - b.x).hypot(a.y - b.y) > 1.0e-10).then_some((start, a, b))
})
.min_by(|(_, a, b), (_, c, d)| {
segment_distance(pick_2d, [a.x, a.y], [b.x, b.y])
.total_cmp(&segment_distance(pick_2d, [c.x, c.y], [d.x, d.y]))
})
.map(|(index, a, b)| {
let start = ocs_point((a.x, a.y, polyline.elevation), polyline.normal);
let end = ocs_point((b.x, b.y, polyline.elevation), polyline.normal);
(
CenterLineSource {
handle,
kind: CenterLineSourceKind::Polyline2DSegment,
segment_index: index as i32,
pick_point: vector(pick),
},
start,
end,
)
})
}
_ => None,
}
}
fn resolve_source(document: &acadrust::CadDocument, source: &CenterLineSource) -> Option<(DVec3, DVec3)> {
let entity = document.get_entity(source.handle)?;
let (_, start, end) = picked_source(entity, source.handle, dvec(source.pick_point))?;
match source.kind {
CenterLineSourceKind::Line => matches!(entity, EntityType::Line(_)).then_some((start, end)),
CenterLineSourceKind::LwPolylineSegment => {
let EntityType::LwPolyline(polyline) = entity else { return None; };
let index = usize::try_from(source.segment_index).ok()?;
let end_index = if index + 1 < polyline.vertices.len() { index + 1 } else if polyline.is_closed { 0 } else { return None; };
if polyline.vertices.get(index)?.bulge.abs() > 1.0e-12 { return None; }
let a = polyline.vertices[index].location;
let b = polyline.vertices[end_index].location;
Some((
ocs_point((a.x, a.y, polyline.elevation), polyline.normal),
ocs_point((b.x, b.y, polyline.elevation), polyline.normal),
))
}
CenterLineSourceKind::Polyline2DSegment => {
let EntityType::Polyline2D(polyline) = entity else { return None; };
let index = usize::try_from(source.segment_index).ok()?;
let end_index = if index + 1 < polyline.vertices.len() { index + 1 } else if polyline.is_closed() { 0 } else { return None; };
if polyline.vertices.get(index)?.bulge.abs() > 1.0e-12 { return None; }
let a = polyline.vertices[index].location;
let b = polyline.vertices[end_index].location;
Some((
ocs_point((a.x, a.y, polyline.elevation), polyline.normal),
ocs_point((b.x, b.y, polyline.elevation), polyline.normal),
))
}
}
}
fn rebuild_line(
document: &acadrust::CadDocument,
association: &CenterLineAssociation,
) -> Option<acadrust::Line> {
let first = resolve_source(document, &association.first)?;
let second = resolve_source(document, &association.second)?;
construct_line(first, second, association)
}
pub(crate) fn construct_line(
first: (DVec3, DVec3),
second: (DVec3, DVec3),
association: &CenterLineAssociation,
) -> Option<acadrust::Line> {
let plane = WorkingPlane::new(
dvec(association.plane_origin),
dvec(association.plane_x),
dvec(association.plane_y),
);
let first_start = plane.to_local(first.0);
let first_end = plane.to_local(first.1);
let second_start = plane.to_local(second.0);
let second_end = plane.to_local(second.1);
let first_pick = plane.to_local(dvec(association.first.pick_point));
let second_pick = plane.to_local(dvec(association.second.pick_point));
let geometry = centerline_between(
KernelLine { start: [first_start.x, first_start.y], end: [first_end.x, first_end.y] },
KernelLine { start: [second_start.x, second_start.y], end: [second_end.x, second_end.y] },
[first_pick.x, first_pick.y],
[second_pick.x, second_pick.y],
association.start_extension + association.start_length_adjustment,
association.end_extension + association.end_length_adjustment,
)?;
let elevation = (first_start.z + first_end.z + second_start.z + second_end.z) * 0.25;
let start = plane.to_world(DVec3::new(geometry.start[0], geometry.start[1], elevation));
let end = plane.to_world(DVec3::new(geometry.end[0], geometry.end[1], elevation));
Some(acadrust::Line::from_points(vector(start), vector(end)))
}
impl Scene {
fn centerline_settings_owner(&self) -> Option<Handle> {
self.document
.objects
.iter()
.find_map(|(handle, object)| match object {
ObjectType::Layout(layout) if layout.name.eq_ignore_ascii_case("Model") => {
Some(*handle)
}
_ => None,
})
.or_else(|| self.current_layout_object_handle())
}
pub(crate) fn centerline_settings(&self) -> CenterLineSettings {
let mut defaults = CenterLineSettings::default();
if matches!(self.document.header.insertion_units, 4..=7 | 11..=17) {
defaults.extension = 3.5;
}
let Some(owner) = self.centerline_settings_owner() else { return defaults; };
let Some(record) = self.document.xrecord(owner, SETTINGS_RECORD) else { return defaults; };
let value = |code| record.entries.iter().find(|entry| entry.code == code).map(|entry| &entry.value);
CenterLineSettings {
extension: value(40).and_then(XRecordValue::as_double).unwrap_or(defaults.extension),
layer: value(1).and_then(XRecordValue::as_string).unwrap_or(&defaults.layer).to_owned(),
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),
}
}
pub(crate) fn set_centerline_setting(&mut self, name: &str, value: &str) -> Result<String, String> {
let Some(owner) = self.centerline_settings_owner() else { return Err("No drawing settings owner.".to_owned()); };
if name == "CENTERLTYPE"
&& !value.eq_ignore_ascii_case("Current")
&& !self.document.line_types.contains(value)
{
return Err(format!("CENTERLTYPE: linetype \"{value}\" is not loaded."));
}
if name == "CENTERLTYPEFILE" && !value.trim().is_empty() {
#[cfg(not(target_arch = "wasm32"))]
{
let source = std::fs::read_to_string(value)
.map_err(|error| format!("CENTERLTYPEFILE: {error}"))?;
crate::io::linetypes::populate_document_from_source(
&mut self.document,
&source,
);
}
#[cfg(target_arch = "wasm32")]
return Err("CENTERLTYPEFILE is unavailable in the web build.".to_owned());
}
self.document.ensure_xrecord(owner, SETTINGS_RECORD);
let record = self.document.xrecord_mut(owner, SETTINGS_RECORD).ok_or_else(|| "Cannot store centerline settings.".to_owned())?;
let (code, replacement, display) = match name {
"CENTEREXE" => {
let number = value.parse::<f64>().map_err(|_| "CENTEREXE requires a non-negative number.".to_owned())?;
if !number.is_finite() || number < 0.0 { return Err("CENTEREXE requires a non-negative number.".to_owned()); }
(40, XRecordValue::Double(number), number.to_string())
}
"CENTERLAYER" => (1, XRecordValue::String(value.to_owned()), value.to_owned()),
"CENTERLTYPE" => (2, XRecordValue::String(value.to_owned()), value.to_owned()),
"CENTERLTSCALE" => {
let number = value.parse::<f64>().map_err(|_| "CENTERLTSCALE requires a non-zero number.".to_owned())?;
if !number.is_finite() || number == 0.0 { return Err("CENTERLTSCALE requires a non-zero number.".to_owned()); }
(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) {
entry.value = replacement;
} else {
record.entries.push(XRecordEntry { code, value: replacement });
}
Ok(format!("{name} = {display}"))
}
pub(crate) fn refresh_associative_centerlines(&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 = CenterLineAssociation::read(&line.common.extended_data)?;
(association.associated && (changed.contains(&association.first.handle) || changed.contains(&association.second.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);
}
let refreshed = resolve_source(&self.document, &association.first)
.zip(resolve_source(&self.document, &association.second))
.and_then(|(first, second)| construct_line(first, second, &association));
let Some(mut new_line) = refreshed else {
association.associated = false;
if let Some(EntityType::Line(line)) = self.document.get_entity_mut(handle) {
association.write(&mut line.common.extended_data);
result.push((handle, ChangeKind::Modified));
}
continue;
};
if let Some(EntityType::Line(line)) = self.document.get_entity_mut(handle) {
new_line.common = line.common.clone();
*line = new_line;
result.push((handle, ChangeKind::Modified));
}
}
result
}
pub(crate) fn reset_centerlines(&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 = CenterLineAssociation::read(&line.common.extended_data)?;
association.start_extension = extension;
association.end_extension = extension;
association.start_length_adjustment = 0.0;
association.end_length_adjustment = 0.0;
rebuild_line(&self.document, &association).map(|line| (*handle, association, line))
}).collect();
for (handle, association, rebuilt) in &candidates {
if self.is_recording_undo() {
let before = self.document.get_entity_arc(*handle);
self.record_undo_before(*handle, before);
}
if let Some(EntityType::Line(line)) = self.document.get_entity_mut(*handle) {
let mut rebuilt = rebuilt.clone();
rebuilt.common = line.common.clone();
association.write(&mut rebuilt.common.extended_data);
*line = rebuilt;
}
}
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_centerline_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 = CenterLineAssociation::read(&line.common.extended_data)?;
association.associated = associated;
let rebuilt = associated.then(|| rebuild_line(&self.document, &association)).flatten();
(!associated || rebuilt.is_some()).then_some((*handle, association, rebuilt))
}).collect();
for (handle, association, rebuilt) in &candidates {
if self.is_recording_undo() {
let before = self.document.get_entity_arc(*handle);
self.record_undo_before(*handle, before);
}
if let Some(EntityType::Line(line)) = self.document.get_entity_mut(*handle) {
if let Some(mut rebuilt) = rebuilt.clone() {
rebuilt.common = line.common.clone();
association.write(&mut rebuilt.common.extended_data);
*line = rebuilt;
} else {
association.write(&mut line.common.extended_data);
}
}
}
if !candidates.is_empty() {
let changes: Vec<_> = candidates.iter().map(|(handle, _, _)| (*handle, ChangeKind::Modified)).collect();
self.bump_entities(&changes);
}
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'])
}

475
src/scene/centermark.rs Normal file
View file

@ -0,0 +1,475 @@
use super::*;
use acadrust::entities::{
CenterMarkAssociation, CenterMarkSource, CenterMarkSourceKind,
};
use acadrust::types::Vector3;
use cadkernel::geom2d::{closest_point, Arc as KernelArc, BulgeArc, Curve};
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 {
let (start_angle, end_angle) = if arc.sweep >= 0.0 {
(arc.start_angle, arc.end_angle)
} else {
(arc.end_angle, arc.start_angle)
};
closest_point(
&Curve::Arc(KernelArc {
centre: arc.center,
radius: arc.radius,
start_angle,
end_angle,
}),
pick,
)
.distance
}
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 render_segments(association: &CenterMarkAssociation) -> Vec<[DVec3; 2]> {
let mut segments = mark_segments(association);
if association.associated {
return segments;
}
let center = dvec(association.center);
let directions = 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,
]);
segments
}
pub(crate) fn mark_bounds(association: &CenterMarkAssociation) -> ([f64; 3], [f64; 3]) {
let mut minimum = [f64::INFINITY; 3];
let mut maximum = [f64::NEG_INFINITY; 3];
for point in render_segments(association).into_iter().flatten() {
let point = point.to_array();
for axis in 0..3 {
minimum[axis] = minimum[axis].min(point[axis]);
maximum[axis] = maximum[axis].max(point[axis]);
}
}
(minimum, maximum)
}
fn apply_source_geometry(
association: &mut CenterMarkAssociation,
center: DVec3,
radius: f64,
x: DVec3,
y: DVec3,
) {
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_origin = vector(center);
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;
}
}
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; };
association.source = source;
apply_source_geometry(&mut association, center, radius, x, y);
association.associated = true;
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) {
apply_source_geometry(&mut association, center, radius, x, y);
} 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 settings = self.centerline_settings();
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)?;
let diameter = association.radius * 2.0;
association.cross_size = super::centerline::resolve_center_measure(
&settings.cross_size,
diameter,
0.1,
);
association.cross_gap = super::centerline::resolve_center_measure(
&settings.cross_gap,
diameter,
0.05,
);
association.cross_size_relative =
super::centerline::center_measure_is_relative(&settings.cross_size);
association.cross_gap_relative =
super::centerline::center_measure_is_relative(&settings.cross_gap);
association.extension_length = settings.extension;
association.length_adjustments = [0.0; 4];
association.overshoots = [0.0; 4];
association.show_extensions = settings.mark_extensions;
Some((*handle, association))
})
.collect();
for (handle, 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(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)?;
apply_source_geometry(&mut association, center, radius, x, y);
}
association.associated = associated;
Some((*handle, association))
})
.collect();
for (handle, 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(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

@ -1705,6 +1705,13 @@ pub(crate) fn set_wire_aabb(w: &mut WireModel, entity_box: [f32; 4]) {
}
fn entity_bounds(e: &acadrust::EntityType) -> ([f64; 3], [f64; 3]) {
if let acadrust::EntityType::Line(line) = e {
if let Some(association) = acadrust::entities::CenterMarkAssociation::read(
&line.common.extended_data,
) {
return crate::scene::centermark::mark_bounds(&association);
}
}
if let acadrust::EntityType::Solid(solid) = e {
if let Some(bounds) = crate::entities::solid::wcs_bounds(solid) {
return (bounds.min, bounds.max);

View file

@ -21,6 +21,8 @@ pub mod view;
// blocks and/or free functions). Pure text-move from the original mod.rs.
mod boundary;
mod camera_ops;
pub(crate) mod centerline;
pub(crate) mod centermark;
mod entity;
mod group_layer;
mod layout;
@ -2393,6 +2395,16 @@ impl Scene {
self.associative_hatch_source_cache.borrow_mut().take();
}
let mut changes = changes.to_vec();
for change in self.refresh_associative_centerlines(&changes) {
if !changes.iter().any(|(handle, _)| *handle == change.0) {
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);