Associative extruded, revolved and lofted surfaces; spline and ellipse sources

SURFACEASSOCIATIVITY now keeps EXTRUDE, REVOLVE and LOFT surfaces linked to
their sources: the profile (and axis object) or cross sections are kept, the
surface is made again when they change, and the drawing's associative
network carries an action with the extruded, revolved or lofted surface
action body, its path parameters and value parameters as the reference
writes them. Swept, extruded, revolved and lofted surfaces can read
splines (NURBS edge curve, fit-point splines through their fit points)
and ellipses (elliptical edge curve). Associative surfaces of a file
opened here follow their sources too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-10-09 10:31:58 +03:00
1 parent aa0b87108c
commit 27bf35fc96
10 files changed
+661 -25

No files matched your search

+1 -1
View File
@@ -7,7 +7,7 @@ use kernel::brep::Body;
use crate::command::{ExtrudeMode, LoftOptions, LoftSectionSelection};
use super::sweep_model::embedded_path;
fn embedded_section(entity: &EntityType) -> Option<EmbeddedEntity> {
pub(crate) fn embedded_section(entity: &EntityType) -> Option<EmbeddedEntity> {
match entity {
EntityType::Region(region) => Some(EmbeddedEntity::Region(region.clone())),
EntityType::Point(point) => Some(EmbeddedEntity::Point(point.clone())),
+1
View File
@@ -17,6 +17,7 @@ pub mod solid_history;
pub mod sweep_model;
pub mod presspull_model;
pub mod loft_command_model;
pub mod surface_sources;
mod sweep_command_model;
pub mod visual_style_model;
pub mod object;
+121
View File
@@ -0,0 +1,121 @@
//! Associative extruded, revolved and lofted surfaces (SURFACEASSOCIATIVITY):
//! the sources and values each surface was made from, kept with the surface
//! so it follows edits of its sources and the drawing's associative network
//! describes it on save.
use codec::xdata::XDataValue;
use codec::{EntityType, Handle};
use glam::DVec3;
/// The application name under which a surface keeps its sources.
pub const SURFACE_SOURCES_APP: &str = "OCS_SURFACE_SOURCES";
/// What an associative surface was made from.
#[derive(Clone, Debug, PartialEq)]
pub enum SurfaceSources {
/// EXTRUDE: the profile, the height along its normal (or the length of a
/// given direction) and the taper angle.
Extrude { profile: Handle, height: f64, direction: Option<DVec3>, taper: f64 },
/// REVOLVE: the profile, the axis (an axis object or two points), the
/// angle and the start angle.
Revolve { profile: Handle, axis: Option<Handle>, start: DVec3, end: DVec3, angle: f64, start_angle: f64 },
/// LOFT: the cross sections in order.
Loft { sections: Vec<Handle> },
}
impl SurfaceSources {
/// Every entity the surface reads.
pub fn handles(&self) -> Vec<Handle> {
match self {
SurfaceSources::Extrude { profile, .. } => vec![*profile],
SurfaceSources::Revolve { profile, axis, .. } => std::iter::once(*profile).chain(*axis).collect(),
SurfaceSources::Loft { sections } => sections.clone(),
}
}
}
/// Keep the sources with the surface.
pub fn link_surface_sources(document: &mut codec::CadDocument, surface: Handle, sources: &SurfaceSources) {
if surface.is_null() {
return;
}
let point = |p: DVec3| XDataValue::Point3D(codec::types::Vector3::new(p.x, p.y, p.z));
let values = match sources {
SurfaceSources::Extrude { profile, height, direction, taper } => {
let mut values = vec![
XDataValue::String("EXTRUDE".into()),
XDataValue::Handle(*profile),
XDataValue::Real(*height),
XDataValue::Real(*taper),
];
values.extend(direction.map(point));
values
}
SurfaceSources::Revolve { profile, axis, start, end, angle, start_angle } => {
let mut values = vec![
XDataValue::String("REVOLVE".into()),
XDataValue::Handle(*profile),
point(*start),
point(*end),
XDataValue::Real(*angle),
XDataValue::Real(*start_angle),
];
values.extend(axis.map(XDataValue::Handle));
values
}
SurfaceSources::Loft { sections } => std::iter::once(XDataValue::String("LOFT".into()))
.chain(sections.iter().map(|handle| XDataValue::Handle(*handle)))
.collect(),
};
crate::scene::view::dispatch::set_entity_xdata(document, surface, SURFACE_SOURCES_APP, Some(values));
}
/// The sources an associative surface keeps.
pub fn surface_sources(entity: &EntityType) -> Option<SurfaceSources> {
let record = entity
.common()
.extended_data
.records()
.iter()
.find(|record| record.application_name == SURFACE_SOURCES_APP)?;
let mut kind = None;
let (mut handles, mut reals, mut points) = (Vec::new(), Vec::new(), Vec::new());
for value in &record.values {
match value {
XDataValue::String(text) if kind.is_none() => kind = Some(text.as_str()),
XDataValue::Handle(handle) => handles.push(*handle),
XDataValue::Real(value) => reals.push(*value),
XDataValue::Point3D(p) => points.push(DVec3::new(p.x, p.y, p.z)),
_ => {}
}
}
match kind? {
"EXTRUDE" => Some(SurfaceSources::Extrude {
profile: *handles.first()?,
height: *reals.first()?,
taper: *reals.get(1)?,
direction: points.first().copied(),
}),
"REVOLVE" => Some(SurfaceSources::Revolve {
profile: *handles.first()?,
axis: handles.get(1).copied(),
start: *points.first()?,
end: *points.get(1)?,
angle: *reals.first()?,
start_angle: *reals.get(1)?,
}),
"LOFT" => (handles.len() >= 2).then_some(SurfaceSources::Loft { sections: handles }),
_ => None,
}
}
/// The two ends of an axis object (a line).
pub fn axis_points(entity: &EntityType) -> Option<(DVec3, DVec3)> {
match entity {
EntityType::Line(line) => Some((
DVec3::new(line.start.x, line.start.y, line.start.z),
DVec3::new(line.end.x, line.end.y, line.end.z),
)),
_ => None,
}
}
+54
View File
@@ -525,6 +525,60 @@ pub fn assoc_edge_curve(entity: &EntityType) -> Option<(i32, codec::objects::Ass
}
Some((47, K::None, values))
}
// Ellipse (17): centre, unit major axis, unit minor axis (the normal
// across the major one), major and minor radius, start and end
// parameter, 0.
EntityType::Ellipse(ellipse) => {
let major = glam::DVec3::new(ellipse.major_axis.x, ellipse.major_axis.y, ellipse.major_axis.z);
let normal = glam::DVec3::new(ellipse.normal.x, ellipse.normal.y, ellipse.normal.z).try_normalize()?;
let radius = major.length();
let axis = major.try_normalize()?;
Some((17, K::Ellipse, vec![
point(glam::DVec3::new(ellipse.center.x, ellipse.center.y, ellipse.center.z)),
point(axis),
point(normal.cross(axis)),
V::Real(radius),
V::Real(radius * ellipse.minor_axis_ratio),
V::Real(ellipse.start_parameter),
V::Real(ellipse.end_parameter),
V::Real(0.0),
]))
}
// NURBS (42): rational flag, a second flag (0), degree, knot
// tolerance, then knots, weights (empty unless rational) and control
// points, each as its length, the length reserved (at least 8) and the
// growth step 8, followed by the items.
EntityType::Spline(spline) => {
// A fit-point spline as the curve through its points, its knots
// in the fit parameter (chord length) as the reference keeps them.
let curve = crate::entities::spline::nurbs3(spline)?;
let mut knots = curve.knots().to_vec();
if crate::entities::spline::uses_fit_method(spline) && spline.knot_parameterization == 0 {
let chord: f64 = spline.fit_points.windows(2)
.map(|pair| ((pair[1].x - pair[0].x).powi(2) + (pair[1].y - pair[0].y).powi(2) + (pair[1].z - pair[0].z).powi(2)).sqrt())
.sum();
let (low, high) = (knots[0], knots[knots.len() - 1]);
if high - low > 0.0 && chord > 0.0 && (high - low - chord).abs() > 1e-9 * chord {
knots.iter_mut().for_each(|knot| *knot = (*knot - low) / (high - low) * chord);
}
}
let points = curve.control_points();
if points.len() < 2 || knots.is_empty() { return None; }
let rational = curve.weights().iter().any(|weight| (weight - 1.0).abs() > 1e-12);
let header = |values: &mut Vec<V>, count: usize| {
values.extend([V::Int(count as i32), V::Int(if count == 0 { 0 } else { count.max(8) } as i32), V::Int(8)]);
};
let tolerance = if spline.knot_tolerance > 0.0 { spline.knot_tolerance } else { 1e-9 };
let mut values = vec![V::Bool(rational), V::Bool(false), V::Int(curve.degree() as i32), V::Real(tolerance)];
header(&mut values, knots.len());
values.extend(knots.iter().map(|knot| V::Real(*knot)));
let weights = if rational { curve.weights() } else { &[] };
header(&mut values, weights.len());
values.extend(weights.iter().map(|weight| V::Real(*weight)));
header(&mut values, points.len());
values.extend(points.iter().map(|p| V::Point(P::new(p[0], p[1], p[2]))));
Some((42, K::Nurb3d, values))
}
_ => None,
}
}