forked from 1b-agent/opencadstudio-upstream
Merge pull request #1653 from ramox81/codex/sweep-surface-expressions
This commit is contained in:
commit
2d1f45eeb3
11 files changed
+498
-23
No files matched your search
Generated
+3
-3
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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" }
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user