forked from 1b-agent/opencadstudio-upstream
Merge remote-tracking branch 'origin/main' into codex/extrude-taper-history
This commit is contained in:
commit
5e6452f1b5
53 files changed
+817
-86
No files matched your search
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user