Merge pull request #1653 from ramox81/codex/sweep-surface-expressions

This commit is contained in:
Hakan Seven committed 2026-10-07 21:31:16 +03:00
commit 2d1f45eeb3
11 files changed
+498 -23

No files matched your search

Generated
+3 -3
View File
@@ -4205,7 +4205,7 @@ dependencies = [
[[package]]
name = "opencadcodec"
version = "0.6.0"
source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=063c106#063c10671fe7833d562f772159771318c7a0ebb9"
source = "git+https://github.com/ramox81/cadcodec.git?rev=64fe2a6#64fe2a6f8896d674d606ded65f4c9cc4b581ac66"
dependencies = [
"anyhow",
"bitflags 2.13.1",
@@ -4237,7 +4237,7 @@ dependencies = [
[[package]]
name = "opencadkernel"
version = "0.1.0"
source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=3f3781a#3f3781a227008aec6847c894dc843245b539c682"
source = "git+https://github.com/ramox81/cadkernel.git?rev=23138fc#23138fcd3ef251c5624164a2311b8b264e22341e"
dependencies = [
"cavalier_contours",
"opencadcodec",
@@ -4248,7 +4248,7 @@ dependencies = [
[[package]]
name = "opencadkernel-constraints"
version = "0.1.0"
source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=3f3781a#3f3781a227008aec6847c894dc843245b539c682"
source = "git+https://github.com/ramox81/cadkernel.git?rev=23138fc#23138fcd3ef251c5624164a2311b8b264e22341e"
dependencies = [
"nalgebra",
]
+7
View File
@@ -181,3 +181,10 @@ harness = false
[[test]]
name = "integration"
path = "tests/integration.rs"
[patch."https://github.com/HakanSeven12/opencadkernel.git"]
opencadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "23138fc" }
opencadkernel-constraints = { git = "https://github.com/ramox81/cadkernel.git", rev = "23138fc" }
[patch."https://github.com/HakanSeven12/opencadcodec.git"]
opencadcodec = { git = "https://github.com/ramox81/cadcodec.git", rev = "64fe2a6" }
+1
View File
@@ -448,6 +448,7 @@ impl OpenCADStudio {
/// table, so unlike that whole-table Apply this can cheaply scope the
/// re-solve to just the entities that actually reference it.
fn resolve_named_parameter_edit(&mut self, i: usize, name: &str) {
self.tabs[i].scene.refresh_expression_sweeps();
let readers: Vec<&crate::scene::parametric_constraints::ParametricConstraint> = self
.tabs[i]
.scene
+9 -1
View File
@@ -459,6 +459,7 @@ impl OpenCADStudio {
mode,
options,
color: _,
expressions,
} = result
else {
unreachable!("router only routes the matching variant");
@@ -501,6 +502,11 @@ impl OpenCADStudio {
// reference does.
let result = (|| -> Result<_, Option<u32>> {
let (path, options) = path.as_ref().zip(options).ok_or(None)?;
if sweep_model::spatial_profile(&profile).is_some() {
let record = sweep_model::spatial_sweep_record(&profile, path, options)?;
let body = kernel::acis::rebuild_sweep_with_mode(&record, true).map_err(|_| None)?;
return Ok((body, record, true));
}
let mut record = sweep_model::sweep_record(&profile, path, options).ok_or(None)?;
record.flags_294_296[0] |= picked_base;
let (_, _, closed) = kernel::acis::sweep_profile_geometry(
@@ -533,7 +539,9 @@ impl OpenCADStudio {
continue;
}
let created = if surface {
self.add_surface_model(sweep_model::swept_surface_entity(&record), body)
let mut entity = sweep_model::swept_surface_entity(&record);
sweep_model::link_sweep_expressions(&mut entity, &expressions);
self.add_surface_model(entity, body)
} else {
self.add_solid_model(
empty_solid3d(),
+1
View File
@@ -904,6 +904,7 @@ mod delobj_tests {
mode,
options: crate::command::SweepOptions::default(),
color: [1.0; 4],
expressions: [None, None],
});
let created = app.tabs[i]
+7
View File
@@ -1356,6 +1356,9 @@ pub struct SweepOptions {
pub scale: f64,
/// Total twist along the path, in radians.
pub twist_angle: f64,
/// Where the path was picked: an open path is swept from its end nearest
/// this point (seen from above), as the reference does.
pub path_pick: Option<DVec3>,
}
impl Default for SweepOptions {
@@ -1366,6 +1369,7 @@ impl Default for SweepOptions {
base_point: None,
scale: 1.0,
twist_angle: 0.0,
path_pick: None,
}
}
}
@@ -2100,6 +2104,9 @@ pub enum CmdResult {
mode: ExtrudeMode,
options: SweepOptions,
color: [f32; 4],
/// Scale and twist expressions entered with the Expression option;
/// a swept surface stays linked to them.
expressions: [Option<String>; 2],
},
/// Loft through ordered cross-sections, with optional guides or a path.
LoftEntities {
+10 -1
View File
@@ -1625,6 +1625,8 @@ pub struct SweepCommand {
end_length: f64,
/// The drawing's named parameters, for Expression input.
parameters: crate::scene::named_parameters::ParameterTable,
/// The scale and twist expressions entered, if any.
expressions: [Option<String>; 2],
isolines: usize,
color: [f32; 4],
}
@@ -1667,6 +1669,7 @@ impl SweepCommand {
new_length_start: None,
end_length: 1.0,
parameters: Default::default(),
expressions: [None, None],
isolines,
color,
}
@@ -1720,6 +1723,7 @@ impl SweepCommand {
mode: self.mode,
options: self.options,
color: self.color,
expressions: self.expressions.clone(),
}
}
@@ -1822,7 +1826,7 @@ impl CadCommand for SweepCommand {
self.step == SweepStep::PickPath
}
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
fn on_entity_pick(&mut self, handle: Handle, pt: DVec3) -> CmdResult {
if self.step != SweepStep::PickPath || handle.is_null() || self.contains_profile(handle) {
self.injected_path = None;
return CmdResult::NeedPoint;
@@ -1835,6 +1839,7 @@ impl CadCommand for SweepCommand {
let Some(path) = path.filter(crate::scene::model::sweep_model::is_sweep_path) else {
return CmdResult::NeedPoint;
};
self.options.path_pick = pt.is_finite().then_some(pt);
if (self.options.scale - 1.0).abs() > 1e-12 {
if let Some(message) = crate::scene::model::sweep_model::scaled_sweep_path_refusal(&path) {
return CmdResult::ReportMeasurement(format!(
@@ -1944,6 +1949,7 @@ impl CadCommand for SweepCommand {
self.new_length_start = None;
self.step = SweepStep::ReferenceLength;
} else if let Ok(scale) = value.parse::<f64>() {
self.expressions[0] = None;
self.set_scale(scale);
}
}
@@ -1967,6 +1973,8 @@ impl CadCommand for SweepCommand {
"The syntax is incorrect or a variable is not defined. Reenter the expression or value."
).into_owned()));
};
let slot = if self.step == SweepStep::ScaleExpression { 0 } else { 1 };
self.expressions[slot] = Some(text.trim().to_string());
if self.step == SweepStep::ScaleExpression {
self.set_scale(value);
} else {
@@ -1986,6 +1994,7 @@ impl CadCommand for SweepCommand {
} else if let Some(angle) = crate::entities::common::parse_angle(value) {
if angle.is_finite() {
self.options.twist_angle = angle;
self.expressions[1] = None;
self.options.bank = false;
SWEEP_BANK.store(false, std::sync::atomic::Ordering::Relaxed);
self.step = SweepStep::PickPath;
+189 -16
View File
@@ -1250,7 +1250,7 @@ impl Allocator<'_> {
has_cached_value: false,
cached_value: String::new(),
flag: false,
reserved: 0,
dependencies: Vec::new(),
}),
);
self.variables.insert(name.to_string(), handle);
@@ -2463,9 +2463,31 @@ fn sync_retained_group_constraints(document: &mut CadDocument, set: &ParametricC
fn detach_preserved_scope(
document: &mut CadDocument,
owner: Handle,
) -> (FxHashMap<String, Handle>, Vec<Handle>) {
) -> (FxHashMap<String, Handle>, Vec<Handle>, Vec<Handle>) {
let Some(network_handle) = native_scope_network_handle(document, owner) else {
return (FxHashMap::default(), Vec::new());
return (FxHashMap::default(), Vec::new(), Vec::new());
};
// Actions the constraint graph does not rebuild (a swept surface's
// action and the unnamed variables holding its expressions) stay in the
// network with everything they own.
let foreign_actions: Vec<Handle> = match document.objects.get(&network_handle) {
Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::Network(network),
..
})) => network
.actions
.iter()
.map(|action| action.dependency)
.filter(|handle| match document.objects.get(handle) {
Some(ObjectType::Associative(object)) => match &object.data {
AssociativeData::ConstraintGroup(_) | AssociativeData::Network(_) => false,
AssociativeData::Variable(variable) => variable.name.trim().is_empty(),
_ => true,
},
_ => false,
})
.collect(),
_ => Vec::new(),
};
let preserved_groups: Vec<_> = native_group_handles(document, owner)
.into_iter()
@@ -2479,11 +2501,33 @@ fn detach_preserved_scope(
)
})
.collect();
if preserved_groups.is_empty() {
return (FxHashMap::default(), Vec::new());
if preserved_groups.is_empty() && foreign_actions.is_empty() {
return (FxHashMap::default(), Vec::new(), Vec::new());
}
let mut variable_handles = Vec::new();
// The named variables the kept actions read keep their handles.
for (_, object) in &document.objects {
let ObjectType::Associative(AssociativeObject {
owner,
data: AssociativeData::ValueDependency(value),
..
}) = object
else {
continue;
};
let handle = value.dependency.dependent_on;
let named = matches!(
document.objects.get(&handle),
Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::Variable(variable),
..
})) if !variable.name.trim().is_empty()
);
if named && foreign_actions.contains(owner) && !variable_handles.contains(&handle) {
variable_handles.push(handle);
}
}
for group_handle in &preserved_groups {
let Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::ConstraintGroup(group),
@@ -2510,7 +2554,7 @@ fn detach_preserved_scope(
let dictionary = document
.extension_dictionary_handle(owner)
.unwrap_or(Handle::NULL);
for handle in &preserved_groups {
for handle in preserved_groups.iter().chain(&foreign_actions) {
if let Some(ObjectType::Associative(object)) = document.objects.get_mut(handle) {
object.owner = dictionary;
}
@@ -2527,7 +2571,100 @@ fn detach_preserved_scope(
}
debug_assert!(document.objects.contains_key(&network_handle));
(variables, preserved_groups)
(variables, preserved_groups, foreign_actions)
}
/// Re-evaluates the unnamed variables of kept actions (a swept surface's
/// scale or twist expression) and the values their actions cache, so the
/// file carries the values the named parameters give.
fn refresh_foreign_values(
document: &mut CadDocument,
foreign_actions: &[Handle],
parameters: &ParameterTable,
) {
let value_of = |document: &CadDocument, handle: Handle| match document.objects.get(&handle) {
Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::Variable(variable),
..
})) => {
let mut table = parameters.clone();
table.set("foreignExpression", &variable.expression).ok()?;
table.resolve("foreignExpression").ok().filter(|value| value.is_finite())
}
_ => None,
};
let dependencies: Vec<(Handle, Handle)> = document
.objects
.iter()
.filter_map(|(handle, object)| match object {
ObjectType::Associative(AssociativeObject {
owner,
data: AssociativeData::ValueDependency(value),
..
}) if foreign_actions.contains(owner) => Some((*handle, value.dependency.dependent_on)),
_ => None,
})
.collect();
for &handle in foreign_actions {
if let Some(value) = value_of(document, handle) {
if let Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::Variable(variable),
..
})) = document.objects.get_mut(&handle)
{
variable.value = Allocator::numeric_eval(value);
}
}
}
for (dependency, source) in dependencies {
let Some(value) = value_of(document, source) else { continue };
if let Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::ValueDependency(dependency),
..
})) = document.objects.get_mut(&dependency)
{
dependency.value = Allocator::numeric_eval(value);
}
}
for &handle in foreign_actions {
let Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::Action(action),
..
})) = document.objects.get(&handle)
else {
continue;
};
let mut updates = Vec::new();
for (index, param) in action.values.iter().enumerate() {
for (slot, variable) in param.variables.iter().enumerate() {
let Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::ValueDependency(dependency),
..
})) = document.objects.get(&variable.handle)
else {
continue;
};
let Some(value) = value_of(document, dependency.dependency.dependent_on) else {
continue;
};
// Angle parameters are cached in radians.
let value = if param.unit_type == 2 { value.to_radians() } else { value };
updates.push((index, slot, value));
}
}
if let Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::Action(action),
..
})) = document.objects.get_mut(&handle)
{
for (index, slot, value) in updates {
action.values[index].variables[slot].value = AssocEvalVariant {
code: 40,
value: AssocEvalValue::Real(value),
};
}
}
}
}
/// Materializes one scope into the drawing's associative object graph.
@@ -2540,6 +2677,7 @@ fn materialize_scope(
action_index: i32,
existing_variables: FxHashMap<String, Handle>,
preserved_groups: Vec<Handle>,
foreign_actions: Vec<Handle>,
materialize_all_parameters: bool,
) -> Option<Handle> {
let dictionary_handle = ensure_extension_dictionary(document, owner);
@@ -2575,6 +2713,7 @@ fn materialize_scope(
&& parameters.is_empty()
&& existing_variables.is_empty()
&& preserved_groups.is_empty()
&& foreign_actions.is_empty()
{
return None;
}
@@ -2741,6 +2880,25 @@ fn materialize_scope(
}
}
let first_foreign = allocator.variable_actions.len() as i32
+ i32::from(group_handle.is_some())
+ preserved_groups.len() as i32;
for (offset, handle) in foreign_actions.iter().copied().enumerate() {
if let Some(ObjectType::Associative(object)) = allocator.document.objects.get_mut(&handle) {
object.owner = network_handle;
let action = match &mut object.data {
AssociativeData::Action(action) => Some(action),
AssociativeData::Variable(variable) => Some(&mut variable.action),
_ => None,
};
if let Some(action) = action {
action.owning_network = network_handle;
action.action_index = first_foreign + offset as i32 + 1;
}
}
}
refresh_foreign_values(allocator.document, &foreign_actions, parameters);
let network = AssocNetwork {
action: AssocAction {
class_version: 2,
@@ -2749,15 +2907,14 @@ fn materialize_scope(
..Default::default()
},
network_version: 0,
network_action_index: allocator.variable_actions.len() as i32
+ i32::from(group_handle.is_some())
+ preserved_groups.len() as i32,
network_action_index: first_foreign + foreign_actions.len() as i32,
actions: allocator
.variable_actions
.iter()
.copied()
.chain(group_handle)
.chain(preserved_groups)
.chain(foreign_actions)
.map(|dependency| AssocActionDependency {
is_owned: true,
dependency,
@@ -2765,9 +2922,23 @@ fn materialize_scope(
.collect(),
owned_actions: Vec::new(),
};
// The reference holds a scope's network through a dictionary of its own
// under the owner's extension dictionary; its parameter commands do not
// find a network stored directly there.
let holder_handle = allocator.document.allocate_handle();
let mut holder = codec::objects::Dictionary::new();
holder.handle = holder_handle;
holder.owner = dictionary_handle;
holder.duplicate_cloning = 1;
holder.reactors.push(dictionary_handle);
holder.add_entry(NETWORK_DICTIONARY_KEY, network_handle);
allocator
.document
.objects
.insert(holder_handle, ObjectType::Dictionary(holder));
allocator.insert_associative_at(
network_handle,
dictionary_handle,
holder_handle,
"ASSOCNETWORK",
"AcDbAssocNetwork",
AssociativeData::Network(network),
@@ -2775,14 +2946,14 @@ fn materialize_scope(
if let Some(ObjectType::Associative(network)) =
allocator.document.objects.get_mut(&network_handle)
{
network.reactors.push(dictionary_handle);
network.reactors.push(holder_handle);
}
set_dictionary_entry(
allocator.document,
dictionary_handle,
NETWORK_DICTIONARY_KEY,
network_handle,
holder_handle,
);
Some(network_handle)
}
@@ -2845,6 +3016,7 @@ impl Scene {
// `enabled` / `axis_direction` / `distance_direction` / `angle_sector`
// writes, which happen on the local set during decode.
self.bump_constraints_epoch();
self.import_sweep_expression_links();
}
/// Makes the standard associative graph match the live command model.
@@ -2890,7 +3062,7 @@ impl Scene {
if let Some(set) = &set {
sync_retained_group_constraints(&mut self.document, set);
}
let (variables, groups) = detach_preserved_scope(&mut self.document, owner);
let (variables, groups, foreign) = detach_preserved_scope(&mut self.document, owner);
let mut set = set.unwrap_or_else(|| {
if owner == model_owner {
ParametricConstraintSet::new(ParametricScope::ModelSpace)
@@ -2906,7 +3078,7 @@ impl Scene {
for constraint in &mut set.constraints {
constraint.native_origin = None;
}
scopes.push((owner, set, variables, groups));
scopes.push((owner, set, variables, groups, foreign));
}
if scopes.is_empty() {
return;
@@ -2921,7 +3093,7 @@ impl Scene {
);
let root_network_handle = self.document.allocate_handle();
let mut child_networks = Vec::new();
for (index, (owner, set, variables, groups)) in scopes.into_iter().enumerate() {
for (index, (owner, set, variables, groups, foreign)) in scopes.into_iter().enumerate() {
let parameters = if set.local_parameters.is_empty() {
&self.named_parameters
} else {
@@ -2936,6 +3108,7 @@ impl Scene {
index as i32 + 1,
variables,
groups,
foreign,
owner == model_owner,
) {
child_networks.push(handle);
+187 -1
View File
@@ -43,7 +43,71 @@ fn polyline_wire(value: &codec::entities::Polyline3D) -> Option<codec::entities:
pub fn is_sweep_profile(entity: &EntityType) -> bool {
embedded_sweep_profile(entity).is_some_and(|(profile, transform)| {
kernel::acis::sweep_profile_geometry(&profile, transform).is_ok()
})
}) || spatial_profile(entity).is_some()
}
/// The vertices of a 3D polyline profile that does not lie in one plane.
pub fn spatial_profile(entity: &EntityType) -> Option<(Vec<[f64; 3]>, bool)> {
let EntityType::Polyline3D(_) = entity else { return None };
let (profile, transform) = embedded_sweep_profile(entity)?;
kernel::acis::sweep_spatial_profile(&profile, transform)
}
/// The sweep record of a spatial 3D polyline profile, which the reference
/// sweeps as a surface: the profile stays in its XY plane and each point's
/// height above it is carried along the path. Refused as the reference
/// refuses it: along a path with a corner (85036), and when the profile seen
/// from above crosses itself (85021); a side that is a point when seen from
/// above cannot be swept at all (`None` code).
pub fn spatial_sweep_record(profile: &EntityType, path: &EntityType, options: SweepOptions) -> Result<SolidHistorySweep, Option<u32>> {
let (points, closed) = spatial_profile(profile).ok_or(None)?;
let count = points.len();
let sides = if closed { count } else { count - 1 };
let plan = |index: usize| glam::DVec2::new(points[index % count][0], points[index % count][1]);
if (0..sides).any(|index| plan(index).distance(plan(index + 1)) <= 1e-9) {
return Err(None);
}
let cross = |a: glam::DVec2, b: glam::DVec2| a.x * b.y - a.y * b.x;
for i in 0..sides {
for j in i + 1..sides {
// Neighbouring sides share a corner.
if j == i + 1 || (closed && i == 0 && j == sides - 1) { continue; }
let (p, r) = (plan(i), plan(i + 1) - plan(i));
let (q, s) = (plan(j), plan(j + 1) - plan(j));
let denominator = cross(r, s);
if denominator.abs() <= 1e-12 { continue; }
let t = cross(q - p, s) / denominator;
let u = cross(q - p, r) / denominator;
if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) {
return Err(Some(85021));
}
}
}
let (sweep_entity, _) = embedded_sweep_profile(profile).ok_or(None)?;
let base_point = options.base_point.map(|point| point.to_array());
let mut base = SolidHistoryNodeBase::new(1);
base.transform = glam::DMat4::IDENTITY.to_cols_array();
let record = SolidHistorySweep {
base,
operation_major: 1,
sweep_entity: Some(sweep_entity),
path_entity: Some(embedded_sweep_path(path).ok_or(None)?),
scale_factor: options.scale,
twist_angle: options.twist_angle,
align_option: if options.align { 1 } else { 2 },
has_align_start: true,
flags_294_296: [base_point.is_some(), false, false],
bank: options.bank,
sweep_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
path_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
reference_point: base_point.map_or(Vector3::new(0.0, 0.0, 0.0), |p| Vector3::new(p[0], p[1], p[2])),
miter_option: 2,
..SolidHistorySweep::default()
};
if kernel::acis::sweep_history_path_has_corner(&record).ok_or(None)? {
return Err(Some(85036));
}
Ok(record)
}
/// The modeling error code the reference reports for a refused sweep.
@@ -101,6 +165,11 @@ pub fn sweep_selection_options(profiles: &[EntityType], mut options: SweepOption
if options.base_point.is_some() {
return Some(options);
}
// Spatial profiles keep their own anchor.
let profiles = profiles.iter().filter(|profile| spatial_profile(profile).is_none()).collect::<Vec<_>>();
if profiles.is_empty() {
return Some(options);
}
let geometry = profiles.iter().map(|profile| {
let (entity, transform) = embedded_sweep_profile(profile)?;
let (plane, wires, _) = kernel::acis::sweep_profile_geometry(&entity, transform).ok()?;
@@ -112,7 +181,53 @@ pub fn sweep_selection_options(profiles: &[EntityType], mut options: SweepOption
Some(options)
}
/// 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 flat = |p: glam::DVec3| p.truncate().distance(pick.truncate());
if flat(end) >= flat(start) { return None; }
match path {
// An arc has one direction; reversed it is a one-segment polyline.
EntityType::Arc(arc) => {
let mut polyline = codec::LwPolyline::new();
let sweep = (arc.end_angle - arc.start_angle).rem_euclid(std::f64::consts::TAU);
let at = |angle: f64| codec::types::Vector2::new(arc.center.x + arc.radius * angle.cos(), arc.center.y + arc.radius * angle.sin());
let mut first = codec::entities::LwVertex::new(at(arc.end_angle));
first.bulge = -(sweep / 4.0).tan();
polyline.vertices = vec![first, codec::entities::LwVertex::new(at(arc.start_angle))];
polyline.normal = arc.normal;
polyline.elevation = arc.center.z;
Some(EntityType::LwPolyline(polyline))
}
_ => crate::modules::draw::modify::reverse::ReverseCommand::reversed(path),
}
}
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.
if let Some(reversed) = options.path_pick.and_then(|pick| reversed_toward_pick(path, pick)) {
let mut record = sweep_record(profile, &reversed, SweepOptions { path_pick: None, ..options })?;
record.path_entity = Some(embedded_sweep_path(path)?);
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()?;
let base_point = match options.base_point {
@@ -193,11 +308,80 @@ fn placed_sweep_record(profile: &EntityType, mut record: SolidHistorySweep) -> O
}
pub fn swept_with_options(profile: &EntityType, path: &EntityType, mode: ExtrudeMode, options: SweepOptions) -> Option<Body> {
if spatial_profile(profile).is_some() {
return kernel::acis::rebuild_sweep_with_mode(&spatial_sweep_record(profile, path, options).ok()?, true).ok();
}
let record = sweep_record(profile, path, options)?;
kernel::acis::rebuild_sweep_with_mode(&record, mode == ExtrudeMode::Surface).ok()
}
/// Preserve native construction parameters alongside the sheet's saved B-rep.
/// The application name of the expressions a swept surface stays linked to.
pub const SWEEP_EXPRESSION_APP: &str = "OCS_SWEEP_EXPRESSION";
/// Links a swept surface to its scale and twist expressions (kept as the
/// surface's own data, so they are saved with the drawing).
pub fn link_sweep_expressions(entity: &mut EntityType, expressions: &[Option<String>; 2]) {
if expressions.iter().all(Option::is_none) { return; }
let mut record = codec::xdata::ExtendedDataRecord::new(SWEEP_EXPRESSION_APP);
for (name, expression) in ["ScaleFactor", "TwistAngle"].iter().zip(expressions) {
if let Some(expression) = expression {
record.add_value(codec::xdata::XDataValue::String(name.to_string()));
record.add_value(codec::xdata::XDataValue::String(expression.clone()));
}
}
entity.common_mut().extended_data.add_record(record);
}
/// 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()
.find(|record| record.application_name == SWEEP_EXPRESSION_APP)?;
let strings = record.values.iter().filter_map(|value| match value {
codec::xdata::XDataValue::String(text) => Some(text.clone()),
_ => None,
}).collect::<Vec<_>>();
let mut result = [None, None];
for pair in strings.chunks(2) {
if let [name, expression] = pair {
match name.as_str() {
"ScaleFactor" => result[0] = Some(expression.clone()),
"TwistAngle" => result[1] = Some(expression.clone()),
_ => {}
}
}
}
Some(result)
}
/// The sweep record a swept surface was made from.
pub fn surface_sweep_record(entity: &EntityType) -> Option<SolidHistorySweep> {
let EntityType::Surface(surface) = entity else { return None };
let SurfaceData::Swept { sweep_entity, path_entity, options, .. } = &surface.surface_data else { return None };
let mut base = SolidHistoryNodeBase::new(1);
base.transform = glam::DMat4::IDENTITY.to_cols_array();
Some(SolidHistorySweep {
base,
operation_major: 1,
sweep_entity: sweep_entity.clone(),
path_entity: path_entity.clone(),
draft_angle: options.draft_angle,
scale_factor: options.scale_factor,
twist_angle: options.twist_angle,
align_angle: options.align_angle,
align_option: options.sweep_alignment_flags as u8,
has_align_start: true,
align_start: options.align_start,
bank: options.bank,
sweep_entity_transform: options.sweep_entity_transform,
path_entity_transform: options.path_entity_transform,
reference_point: options.reference_vector,
flags_294_296: [false, options.sweep_entity_transform_computed, options.path_entity_transform_computed],
miter_option: 2,
..SolidHistorySweep::default()
})
}
pub fn swept_surface_entity(record: &SolidHistorySweep) -> EntityType {
let mut surface = Surface::new(SurfaceKind::Swept);
if let Ok(point) = kernel::acis::sweep_history_reference_point(record) {
@@ -220,6 +404,8 @@ pub fn swept_surface_entity(record: &SolidHistorySweep) -> EntityType {
align_start: record.align_start,
bank: record.bank,
base_point_set: true,
sweep_entity_transform_computed: record.flags_294_296[1],
path_entity_transform_computed: record.flags_294_296[2],
reference_vector: record.reference_point,
..SurfaceSweepOptions::default()
},
+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, 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, 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
+83
View File
@@ -1084,6 +1084,89 @@ impl Scene {
}
}
/// Swept surfaces linked to scale/twist expressions follow the named
/// parameters: re-evaluated and rebuilt when a value changes, as the
/// reference keeps them associative. Returns the surfaces rebuilt.
pub fn refresh_expression_sweeps(&mut self) -> Vec<Handle> {
use crate::scene::model::sweep_model;
let linked = self.document.entities()
.filter_map(|entity| Some((entity.common().handle, sweep_model::sweep_expressions(entity)?)))
.collect::<Vec<_>>();
let mut rebuilt = Vec::new();
for (handle, [scale, twist]) in linked {
let Some(entity) = self.document.get_entity(handle) else { continue };
let Some(mut record) = sweep_model::surface_sweep_record(entity) else { continue };
let evaluate = |expression: &str| {
let mut table = self.named_parameters().clone();
table.set("sweepExpression", expression).ok()?;
table.resolve("sweepExpression").ok().filter(|value| value.is_finite())
};
let mut changed = false;
if let Some(value) = scale.as_deref().and_then(evaluate) {
changed |= (record.scale_factor - value).abs() > 1e-12;
record.scale_factor = value;
}
if let Some(value) = twist.as_deref().and_then(evaluate).map(f64::to_radians) {
changed |= (record.twist_angle - value).abs() > 1e-12;
record.twist_angle = value;
}
if !changed { continue; }
let Ok(body) = kernel::acis::rebuild_sweep_with_mode(&record, true) else { continue };
let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&body) else { continue };
let Some(display) = self.prepare_solid_model_display(handle, &body) else { continue };
let before = self.document.get_entity(handle).cloned().map(std::sync::Arc::new);
self.record_undo_before(handle, before);
if let Some(EntityType::Surface(surface)) = self.document.get_entity_mut(handle) {
surface.acis_data = codec::entities::AcisData::from_sat(&document.to_sat_string());
if let codec::entities::SurfaceData::Swept { options, .. } = &mut surface.surface_data {
options.scale_factor = record.scale_factor;
options.twist_angle = record.twist_angle;
}
}
self.register_prepared_solid_model(handle, body, display);
rebuilt.push(handle);
}
rebuilt
}
/// Swept surfaces the reference keeps associative to a scale or twist
/// expression (an associative swept surface action whose value parameter
/// depends on an expression variable) are linked the same way here.
pub(crate) fn import_sweep_expression_links(&mut self) {
use codec::objects::{AssocSurfaceActionKind, AssociativeData, ObjectType};
let object = |handle: Handle| match self.document.objects.get(&handle) {
Some(ObjectType::Associative(object)) => Some(&object.data),
_ => None,
};
let mut links = Vec::new();
for item in self.document.objects.values() {
let ObjectType::Associative(action) = item else { continue };
let AssociativeData::Action(action) = &action.data else { continue };
let Some(AssociativeData::SurfaceActionBody(body)) = object(action.action_body) else { continue };
if body.kind != AssocSurfaceActionKind::Swept { continue; }
let Some(AssociativeData::Dependency(written)) = object(body.surface_body.dependency) else { continue };
let mut expressions: [Option<String>; 2] = [None, None];
for value in &action.values {
let slot = match value.name.as_str() { "ScaleFactor" => 0, "TwistAngle" => 1, _ => continue };
for variable in &value.variables {
let Some(AssociativeData::ValueDependency(dependency)) = object(variable.handle) else { continue };
let Some(AssociativeData::Variable(source)) = object(dependency.dependency.dependent_on) else { continue };
if !source.expression.trim().is_empty() && source.expression.trim().parse::<f64>().is_err() {
expressions[slot] = Some(source.expression.trim().to_string());
}
}
}
if expressions.iter().any(Option::is_some) {
links.push((written.dependent_on, expressions));
}
}
for (handle, expressions) in links {
let Some(entity) = self.document.get_entity_mut(handle) else { continue };
if crate::scene::model::sweep_model::sweep_expressions(entity).is_some() { continue; }
crate::scene::model::sweep_model::link_sweep_expressions(entity, &expressions);
}
}
pub fn rebuild_solid_history(
&mut self,
handle: Handle,