Files
OneCAD-Draft/src/scene/centermark.rs
T
Hakan SevenandClaude Opus 5.5 f537aff75c feat: node graph overlay; move to opencadcodec/opencadkernel/opencadgraph
The codec and kernel repositories and crates are renamed opencadcodec and
opencadkernel. Inside OCS they are depended on under short keys, so code
reads `codec::`, `kernel::` and the new `graph::` (opencadgraph, the node
graph engine). File-format and protocol spellings stay as they were: the
ACIS product id `acadrust`, the ACADRUST_* switches and the plugin
version keys `acadrust_version` / `acadrust_source`.

A button left of MCP on the command line opens a translucent node graph
over the viewport, with a palette of object, input, math, logic, list,
text and geometry nodes added by click or drag. An object node owns
drawing entities and its rows are the Properties panel's own rows for
them: the single-object page is extracted from refresh_properties as
entity_property_sections, values use the control protocol's
property_json, and writes go through set_property_value with a property
target override, so the panel, MCP and the graph share one read/write
path. A list reaching an object row makes one entity per element. Each
graph action is one undo step; the canvas pans and zooms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 07:34:23 +03:00

488 lines
19 KiB
Rust

use super::*;
use codec::entities::{
CenterMarkAssociation, CenterMarkSource, CenterMarkSourceKind,
};
use codec::types::Vector3;
use kernel::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: &codec::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 codec::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()
}
}
/// Whether `entity` carries a center mark association.
///
/// Used to keep the drawing-wide scan off the edit path: only an entity that
/// actually carries one can make the scan necessary.
pub(crate) fn entity_has_center_association(entity: &EntityType) -> bool {
let EntityType::Line(line) = entity else {
return false;
};
CenterMarkAssociation::read(&line.common.extended_data)
.is_some_and(|association| association.associated)
}