Merge remote-tracking branch 'origin/main' into codex/extrude-taper-history

This commit is contained in:
ramox81 committed 2026-10-08 15:01:55 +03:00
commit 5e6452f1b5
53 files changed
+817 -86

No files matched your search

+218 -20
View File
@@ -167,7 +167,11 @@ pub fn sweep_selection_options(profiles: &[EntityType], mut options: SweepOption
}
// Spatial profiles keep their own anchor.
let profiles = profiles.iter().filter(|profile| spatial_profile(profile).is_none()).collect::<Vec<_>>();
if profiles.is_empty() {
// A single profile takes its base from the path it is swept along
// (`sweep_record`).
// ponytail: several profiles share the group anchor even when the path
// starts inside one of them; the reference rule for that case is unmeasured.
if profiles.len() < 2 {
return Some(options);
}
let geometry = profiles.iter().map(|profile| {
@@ -184,23 +188,7 @@ pub fn sweep_selection_options(profiles: &[EntityType], mut options: SweepOption
/// The path traversed the other way when it was picked nearer its end
/// (measured in plan, as the reference measures the pick), else `None`.
fn reversed_toward_pick(path: &EntityType, pick: glam::DVec3) -> Option<EntityType> {
let (start, end) = match path {
EntityType::Polyline3D(value) if !value.is_closed() => {
let p = |v: &codec::entities::Vertex3DPolyline| glam::DVec3::new(v.position.x, v.position.y, v.position.z);
(p(value.vertices.first()?), p(value.vertices.last()?))
}
EntityType::Spline(value) if !value.flags.closed => {
let points = if value.control_points.is_empty() { &value.fit_points } else { &value.control_points };
let p = |v: &codec::types::Vector3| glam::DVec3::new(v.x, v.y, v.z);
(p(points.first()?), p(points.last()?))
}
_ => {
let planar = crate::entities::curve::entity_curve(path)?;
if planar.curve.is_closed() { return None; }
let at = |t: f64| glam::DVec3::from_array(planar.plane.point_at(planar.curve.point_at(t)));
(at(0.0), at(1.0))
}
};
let (start, end) = path_ends(path)?;
let flat = |p: glam::DVec3| p.truncate().distance(pick.truncate());
if flat(end) >= flat(start) { return None; }
match path {
@@ -220,6 +208,27 @@ fn reversed_toward_pick(path: &EntityType, pick: glam::DVec3) -> Option<EntityTy
}
}
/// The start and end of an open path.
fn path_ends(path: &EntityType) -> Option<(glam::DVec3, glam::DVec3)> {
Some(match path {
EntityType::Polyline3D(value) if !value.is_closed() => {
let p = |v: &codec::entities::Vertex3DPolyline| glam::DVec3::new(v.position.x, v.position.y, v.position.z);
(p(value.vertices.first()?), p(value.vertices.last()?))
}
EntityType::Spline(value) if !value.flags.closed => {
let points = if value.control_points.is_empty() { &value.fit_points } else { &value.control_points };
let p = |v: &codec::types::Vector3| glam::DVec3::new(v.x, v.y, v.z);
(p(points.first()?), p(points.last()?))
}
_ => {
let planar = crate::entities::curve::entity_curve(path)?;
if planar.curve.is_closed() { return None; }
let at = |t: f64| glam::DVec3::from_array(planar.plane.point_at(planar.curve.point_at(t)));
(at(0.0), at(1.0))
}
})
}
pub fn sweep_record(profile: &EntityType, path: &EntityType, options: SweepOptions) -> Option<SolidHistorySweep> {
// Placed from the picked end; the record keeps the original path, and
// the placed frame tells which end the sweep starts from.
@@ -229,10 +238,10 @@ pub fn sweep_record(profile: &EntityType, path: &EntityType, options: SweepOptio
return Some(record);
}
let (sweep_entity, sweep_entity_transform) = embedded_sweep_profile(profile)?;
let (plane, wires, _) = kernel::acis::sweep_profile_geometry(&sweep_entity, sweep_entity_transform).ok()?;
kernel::acis::sweep_profile_geometry(&sweep_entity, sweep_entity_transform).ok()?;
let base_point = match options.base_point {
Some(point) => point.to_array(),
None => kernel::brep::sweep_profile_base(plane, &wires)?,
None => kernel::acis::sweep_default_base(&sweep_entity, sweep_entity_transform, &embedded_sweep_path(path)?).ok()?,
};
let mut base = SolidHistoryNodeBase::new(1);
base.transform = glam::DMat4::IDENTITY.to_cols_array();
@@ -337,6 +346,195 @@ pub fn link_sweep_expressions(
crate::scene::view::dispatch::set_entity_xdata(document, handle, SWEEP_EXPRESSION_APP, Some(values));
}
/// SURFACEASSOCIATIVITY: whether new surfaces stay associative to the
/// objects they were made from. A profile setting, so one value for every
/// drawing.
static SURFACE_ASSOCIATIVITY: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true);
pub fn surface_associativity() -> bool {
SURFACE_ASSOCIATIVITY.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn set_surface_associativity(on: bool) {
SURFACE_ASSOCIATIVITY.store(on, std::sync::atomic::Ordering::Relaxed);
}
/// The application name under which an associative swept surface keeps
/// the profile and path it was swept from.
pub const SWEEP_SOURCES_APP: &str = "OCS_SWEEP_SOURCES";
/// Marks a swept surface associative to the profile and path it was swept
/// from, with the options it is swept again with when they change; the
/// drawing's associative network for it is written on save.
pub fn link_sweep_sources(
document: &mut codec::CadDocument,
surface: codec::Handle,
profile: codec::Handle,
path: codec::Handle,
options: &SweepOptions,
) {
use codec::xdata::XDataValue;
if surface.is_null() { return; }
let point = |p: glam::DVec3| XDataValue::Point3D(Vector3::new(p.x, p.y, p.z));
// The path keeps the direction it was swept in when it is edited.
let reversed = options.path_pick
.zip(document.get_entity(path))
.is_some_and(|(pick, path)| reversed_toward_pick(path, pick).is_some());
let mut values = vec![
XDataValue::Handle(profile),
XDataValue::Handle(path),
XDataValue::Integer16(i16::from(options.align) | (i16::from(options.bank) << 1) | (i16::from(reversed) << 2)),
];
if let Some(base) = options.base_point {
values.push(XDataValue::String("BASE".to_string()));
values.push(point(base));
}
crate::scene::view::dispatch::set_entity_xdata(document, surface, SWEEP_SOURCES_APP, Some(values));
}
/// The profile and path an associative swept surface was swept from.
pub fn sweep_sources(entity: &EntityType) -> Option<(codec::Handle, codec::Handle)> {
let record = entity.common().extended_data.records().iter()
.find(|record| record.application_name == SWEEP_SOURCES_APP)?;
let mut handles = record.values.iter().filter_map(|value| match value {
codec::xdata::XDataValue::Handle(handle) => Some(*handle),
_ => None,
});
Some((handles.next()?, handles.next()?))
}
/// The surface swept again from its current profile and path, with the
/// options it was made with: (surface record, body).
pub fn resweep_from_sources(
surface: &EntityType,
profile: &EntityType,
path: &EntityType,
) -> Option<(SolidHistorySweep, Body)> {
use codec::xdata::XDataValue;
let record = surface.common().extended_data.records().iter()
.find(|record| record.application_name == SWEEP_SOURCES_APP)?;
let EntityType::Surface(value) = surface else { return None };
let SurfaceData::Swept { options: stored, .. } = &value.surface_data else { return None };
let mut options = SweepOptions {
scale: stored.scale_factor,
twist_angle: stored.twist_angle,
..SweepOptions::default()
};
let mut values = record.values.iter();
while let Some(value) = values.next() {
match value {
XDataValue::Integer16(flags) => {
options.align = flags & 1 != 0;
options.bank = flags & 2 != 0;
// Swept from the end: picked there.
if flags & 4 != 0 {
options.path_pick = path_ends(path).map(|(_, end)| end);
}
}
XDataValue::String(key) if key == "BASE" => {
if let Some(XDataValue::Point3D(p)) = values.next() {
options.base_point = Some(glam::DVec3::new(p.x, p.y, p.z));
}
}
_ => {}
}
}
let record = if spatial_profile(profile).is_some() {
spatial_sweep_record(profile, path, options).ok()?
} else {
sweep_record(profile, path, options)?
};
let body = kernel::acis::rebuild_sweep_with_mode(&record, true).ok()?;
Some((record, body))
}
/// An entity's curve as the reference stores it in an edge action
/// parameter: line segment (23: start, vector to the end), arc (11:
/// centre, normal, reference axis, radius, start and end angle, 0) or a
/// composite of those (47: count, then each part's type and values).
pub fn assoc_edge_curve(entity: &EntityType) -> Option<(i32, codec::objects::AssocSubcurveKind, Vec<codec::objects::AssocCurveValue>)> {
use codec::objects::{AssocCurveValue as V, AssocSubcurveKind as K};
use codec::types::Vector3 as P;
let point = |p: glam::DVec3| V::Point(P::new(p.x, p.y, p.z));
let segment = |a: glam::DVec3, b: glam::DVec3| vec![point(a), point(b - a)];
// An arc about `normal` from `start` through `sweep` (signed, about the
// normal) on a circle of `radius`; measured from the reference axis.
let arc = |centre: glam::DVec3, normal: glam::DVec3, axis: glam::DVec3, radius: f64, start: f64, end: f64| vec![
point(centre), point(normal), point(axis), V::Real(radius), V::Real(start), V::Real(end), V::Real(0.0),
];
match entity {
EntityType::Line(line) => {
let (a, b) = (glam::DVec3::new(line.start.x, line.start.y, line.start.z), glam::DVec3::new(line.end.x, line.end.y, line.end.z));
Some((23, K::LineSegment3d, segment(a, b)))
}
EntityType::Circle(circle) => {
let normal = glam::DVec3::new(circle.normal.x, circle.normal.y, circle.normal.z).try_normalize()?;
let axis = arbitrary_x(normal);
Some((11, K::Arc, arc(glam::DVec3::new(circle.center.x, circle.center.y, circle.center.z), normal, axis, circle.radius, 0.0, std::f64::consts::TAU)))
}
EntityType::Arc(value) => {
let normal = glam::DVec3::new(value.normal.x, value.normal.y, value.normal.z).try_normalize()?;
let axis = arbitrary_x(normal);
// The end past the start by the arc's sweep; an equal pair is a
// full turn. (A loop adding TAU never ends on a huge angle.)
let sweep = (value.end_angle - value.start_angle).rem_euclid(std::f64::consts::TAU);
let end = value.start_angle + if sweep > 0.0 { sweep } else { std::f64::consts::TAU };
Some((11, K::Arc, arc(glam::DVec3::new(value.center.x, value.center.y, value.center.z), normal, axis, value.radius, value.start_angle, end)))
}
EntityType::LwPolyline(polyline) => {
let normal = glam::DVec3::new(polyline.normal.x, polyline.normal.y, polyline.normal.z).try_normalize()?;
let axis_x = arbitrary_x(normal);
let axis_y = normal.cross(axis_x);
let at = |v: &codec::entities::LwVertex| axis_x * v.location.x + axis_y * v.location.y + normal * polyline.elevation;
let count = polyline.vertices.len();
let spans = if polyline.is_closed { count } else { count.saturating_sub(1) };
if spans == 0 { return None; }
let mut values = vec![V::Int(spans as i32)];
for index in 0..spans {
let (from, to) = (&polyline.vertices[index], &polyline.vertices[(index + 1) % count]);
let (a, b) = (at(from), at(to));
if from.bulge.abs() <= 1e-12 {
values.push(V::Int(23));
values.extend(segment(a, b));
} else {
// The arc in the polyline's plane; a clockwise one turns about
// the opposite normal, from the same reference axis.
let bulge = from.bulge;
let chord = b - a;
let centre = a + chord * 0.5 + normal.cross(chord) * ((1.0 - bulge * bulge) / (4.0 * bulge));
let radius = chord.length() * (1.0 + bulge * bulge) / (4.0 * bulge.abs());
let turn = normal * bulge.signum();
let across = turn.cross(axis_x);
let angle = |p: glam::DVec3| (p - centre).dot(across).atan2((p - centre).dot(axis_x));
let start = angle(a);
let end = start + 4.0 * bulge.atan().abs();
values.push(V::Int(11));
values.extend(arc(centre, turn, axis_x, radius, start, end));
}
}
Some((47, K::None, values))
}
EntityType::Polyline3D(polyline) => {
let points = polyline.vertices.iter().map(|v| glam::DVec3::new(v.position.x, v.position.y, v.position.z)).collect::<Vec<_>>();
let spans = if polyline.is_closed() { points.len() } else { points.len().saturating_sub(1) };
if spans == 0 { return None; }
let mut values = vec![V::Int(spans as i32)];
for index in 0..spans {
values.push(V::Int(23));
values.extend(segment(points[index], points[(index + 1) % points.len()]));
}
Some((47, K::None, values))
}
_ => None,
}
}
/// The arbitrary-axis X direction of an entity normal.
fn arbitrary_x(normal: glam::DVec3) -> glam::DVec3 {
let world = if normal.x.abs() < 1.0 / 64.0 && normal.y.abs() < 1.0 / 64.0 { glam::DVec3::Y } else { glam::DVec3::Z };
world.cross(normal).normalize()
}
/// The scale and twist expressions a swept surface is linked to.
pub fn sweep_expressions(entity: &EntityType) -> Option<[Option<String>; 2]> {
let record = entity.common().extended_data.records().iter()
+1 -1
View File
@@ -13,7 +13,7 @@ use codec::EntityType;
use crate::entities::curve::entity_curve;
pub use super::sweep_command_model::{
is_sweep_path, is_sweep_profile, scaled_sweep_path_refusal, spatial_profile, spatial_sweep_record, surface_sweep_record, sweep_expressions, link_sweep_expressions, sweep_record, sweep_refusal_code, sweep_selection_options, swept_surface_entity, swept_with_options,
is_sweep_path, is_sweep_profile, scaled_sweep_path_refusal, spatial_profile, spatial_sweep_record, surface_sweep_record, sweep_expressions, link_sweep_expressions, link_sweep_sources, sweep_sources, SWEEP_SOURCES_APP, resweep_from_sources, surface_associativity, set_surface_associativity, assoc_edge_curve, sweep_record, sweep_refusal_code, sweep_selection_options, swept_surface_entity, swept_with_options,
};
/// A drawn profile, as the kernel wants it: the plane it lies in and the