forked from 1b-agent/opencadstudio-upstream
Merge pull request #1679 from ramox81/codex/surface-associativity-sources
Associative extruded, revolved and lofted surfaces; spline and ellipse sources # Conflicts: # src/app/command_driver/solid3d.rs # src/scene/dwg_native_constraints.rs # src/scene/model/mod.rs # src/scene/model/sweep_command_model.rs # src/scene/model/sweep_model.rs # src/scene/modify.rs
This commit is contained in:
commit
5bb0efd93e
12 files changed
+1106
-228
No files matched your search
@@ -42,6 +42,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
let pending = self.begin_undo(i, "EXTRUDE", handles.len(), true);
|
||||
let mut created_handles = Vec::new();
|
||||
let mut associative_sources = Vec::new();
|
||||
let mut consumed = Vec::new();
|
||||
let mut failed = 0usize;
|
||||
for handle in &handles {
|
||||
@@ -112,10 +113,27 @@ impl OpenCADStudio {
|
||||
entity.surface_data = data;
|
||||
}
|
||||
}
|
||||
match history {
|
||||
Some(history) => self.add_surface_model_with_history(surface, body, history),
|
||||
let created = match history {
|
||||
Some(history) => {
|
||||
self.add_surface_model_with_history(surface, body, history)
|
||||
}
|
||||
None => self.add_surface_model(surface, body),
|
||||
};
|
||||
// An associative surface keeps reading its profile.
|
||||
if sweep_model::surface_associativity() && path.is_none() && !created.is_null() {
|
||||
use crate::scene::model::surface_sources::{link_surface_sources, SurfaceSources};
|
||||
let (height, given) = match extent {
|
||||
ExtrudeExtent::Height(height) => (height, None),
|
||||
_ => (direction.length(), Some(direction)),
|
||||
};
|
||||
link_surface_sources(
|
||||
&mut self.tabs[i].scene.document,
|
||||
created,
|
||||
&SurfaceSources::Extrude { profile: *handle, height, direction: given, taper: taper_angle },
|
||||
);
|
||||
associative_sources.push(*handle);
|
||||
}
|
||||
created
|
||||
} else {
|
||||
let direction = direction.unwrap_or(glam::DVec3::ZERO);
|
||||
let history = path
|
||||
@@ -139,7 +157,7 @@ impl OpenCADStudio {
|
||||
failed += 1;
|
||||
} else {
|
||||
created_handles.push(created);
|
||||
if delete_sources {
|
||||
if delete_sources && !associative_sources.contains(handle) {
|
||||
consumed.push(*handle);
|
||||
}
|
||||
}
|
||||
@@ -310,6 +328,7 @@ impl OpenCADStudio {
|
||||
handles,
|
||||
axis_start,
|
||||
axis_end,
|
||||
axis_object,
|
||||
angle,
|
||||
start_angle,
|
||||
mode,
|
||||
@@ -346,6 +365,7 @@ impl OpenCADStudio {
|
||||
self.begin_undo(i, "REVOLVE", editable_handles.len(), true)
|
||||
};
|
||||
let mut created_handles = Vec::new();
|
||||
let mut associative_sources = Vec::new();
|
||||
let mut consumed = Vec::new();
|
||||
for handle in &editable_handles {
|
||||
let Some(entity) = self.tabs[i].scene.document.get_entity(*handle).cloned() else {
|
||||
@@ -364,10 +384,34 @@ impl OpenCADStudio {
|
||||
failed += 1;
|
||||
continue;
|
||||
};
|
||||
self.add_surface_model(
|
||||
empty_revolved_surface(&entity, axis_start, axis_end, angle, start_angle),
|
||||
let created = self.add_surface_model(
|
||||
empty_revolved_surface(
|
||||
&entity,
|
||||
axis_start,
|
||||
axis_end,
|
||||
angle,
|
||||
start_angle,
|
||||
),
|
||||
body,
|
||||
)
|
||||
);
|
||||
// An associative surface keeps reading its profile and axis.
|
||||
if sweep_model::surface_associativity() && !created.is_null() {
|
||||
use crate::scene::model::surface_sources::{link_surface_sources, SurfaceSources};
|
||||
link_surface_sources(
|
||||
&mut self.tabs[i].scene.document,
|
||||
created,
|
||||
&SurfaceSources::Revolve {
|
||||
profile: *handle,
|
||||
axis: axis_object,
|
||||
start: axis_start,
|
||||
end: axis_end,
|
||||
angle,
|
||||
start_angle,
|
||||
},
|
||||
);
|
||||
associative_sources.push(*handle);
|
||||
}
|
||||
created
|
||||
} else {
|
||||
let result = sweep_model::revolve_history(&entity, from, to, angle, start_angle)
|
||||
.and_then(|history| {
|
||||
@@ -391,7 +435,7 @@ impl OpenCADStudio {
|
||||
failed += 1;
|
||||
} else {
|
||||
created_handles.push(created);
|
||||
if delete_sources {
|
||||
if delete_sources && !associative_sources.contains(handle) {
|
||||
consumed.push(*handle);
|
||||
}
|
||||
}
|
||||
@@ -688,6 +732,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
let dirty_before = self.tabs[i].dirty;
|
||||
let pending = self.begin_undo(i, "LOFT", 1, true);
|
||||
let mut associative = false;
|
||||
let created = if surface {
|
||||
let handle =
|
||||
self.add_surface_model(loft_command_model::surface_entity(&record), body);
|
||||
@@ -696,6 +741,27 @@ impl OpenCADStudio {
|
||||
handle,
|
||||
codec::objects::SolidHistoryOperation::Loft(record),
|
||||
);
|
||||
// An associative surface keeps reading its cross
|
||||
// sections (each section a single object).
|
||||
let single: Option<Vec<Handle>> = sections
|
||||
.iter()
|
||||
.map(|section| match section {
|
||||
LoftSectionSelection::Entity(handle) => Some(*handle),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
if let Some(single) = single.filter(|_| {
|
||||
crate::scene::model::sweep_model::surface_associativity()
|
||||
&& guides.is_empty()
|
||||
&& path.is_none()
|
||||
}) {
|
||||
crate::scene::model::surface_sources::link_surface_sources(
|
||||
&mut self.tabs[i].scene.document,
|
||||
handle,
|
||||
&crate::scene::model::surface_sources::SurfaceSources::Loft { sections: single },
|
||||
);
|
||||
associative = true;
|
||||
}
|
||||
}
|
||||
handle
|
||||
} else {
|
||||
@@ -713,7 +779,7 @@ impl OpenCADStudio {
|
||||
self.command_line.push_error(crate::t!("LOFT could not create a complete display. The source sections were preserved.").as_ref());
|
||||
return Some(Task::none());
|
||||
} else {
|
||||
let mut consumed = if delete_sections {
|
||||
let mut consumed = if delete_sections && !associative {
|
||||
sources.clone()
|
||||
} else {
|
||||
Vec::new()
|
||||
|
||||
@@ -2094,6 +2094,9 @@ pub enum CmdResult {
|
||||
handles: Vec<Handle>,
|
||||
axis_start: glam::DVec3,
|
||||
axis_end: glam::DVec3,
|
||||
/// The line picked as the axis (Object), which an associative
|
||||
/// surface keeps reading.
|
||||
axis_object: Option<Handle>,
|
||||
angle: f64,
|
||||
start_angle: f64,
|
||||
mode: ExtrudeMode,
|
||||
|
||||
@@ -1131,6 +1131,7 @@ pub struct RevolveCommand {
|
||||
handles: Vec<Handle>,
|
||||
preview_profiles: Vec<(Handle, EntityType)>,
|
||||
injected_axis: Option<EntityType>,
|
||||
axis_object: Option<Handle>,
|
||||
axis_start: DVec3,
|
||||
axis_end: DVec3,
|
||||
working_plane: WorkingPlane,
|
||||
@@ -1183,6 +1184,7 @@ impl RevolveCommand {
|
||||
handles: Vec::new(),
|
||||
preview_profiles: Vec::new(),
|
||||
injected_axis: None,
|
||||
axis_object: None,
|
||||
axis_start: DVec3::ZERO,
|
||||
axis_end: DVec3::new(0.0, 0.0, 1.0),
|
||||
working_plane: WorkingPlane::default(),
|
||||
@@ -1319,6 +1321,7 @@ impl RevolveCommand {
|
||||
};
|
||||
self.axis_start = self.working_plane.origin;
|
||||
self.axis_end = self.axis_start + direction;
|
||||
self.axis_object = None;
|
||||
self.step = RevolveStep::Angle;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
@@ -1337,6 +1340,7 @@ impl RevolveCommand {
|
||||
handles: self.handles.clone(),
|
||||
axis_start: self.axis_start,
|
||||
axis_end: self.axis_end,
|
||||
axis_object: self.axis_object,
|
||||
angle,
|
||||
start_angle: self.start_angle,
|
||||
mode: self.mode,
|
||||
@@ -1430,12 +1434,13 @@ impl CadCommand for RevolveCommand {
|
||||
}
|
||||
match self.step {
|
||||
RevolveStep::AxisObject => {
|
||||
let Some((start, end)) = self.injected_axis.take().as_ref().and_then(revolve_axis)
|
||||
else {
|
||||
let axis = self.injected_axis.take();
|
||||
let Some((start, end)) = axis.as_ref().and_then(revolve_axis) else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
self.axis_start = start;
|
||||
self.axis_end = end;
|
||||
self.axis_object = matches!(axis, Some(EntityType::Line(_))).then_some(handle);
|
||||
self.step = RevolveStep::Angle;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
@@ -1446,6 +1451,7 @@ impl CadCommand for RevolveCommand {
|
||||
match self.step {
|
||||
RevolveStep::AxisStart => {
|
||||
self.axis_start = pt;
|
||||
self.axis_object = None;
|
||||
self.step = RevolveStep::AxisEnd;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
+420
-148
@@ -2652,19 +2652,396 @@ fn pending_swept_surfaces(document: &CadDocument) -> Vec<(Handle, Handle, Handle
|
||||
let (profile, path) = sweep_model::sweep_sources(entity)?;
|
||||
let surface = entity.common().handle;
|
||||
(!carried.contains(&surface)
|
||||
&& document
|
||||
.get_entity(profile)
|
||||
.is_some_and(|e| sweep_model::assoc_edge_curve(e).is_some())
|
||||
&& document
|
||||
.get_entity(path)
|
||||
.is_some_and(|e| sweep_model::assoc_edge_curve(e).is_some()))
|
||||
.then_some((surface, profile, path))
|
||||
&& document.get_entity(profile).is_some_and(|e| sweep_model::assoc_source_supported(e))
|
||||
&& document.get_entity(path).is_some_and(|e| sweep_model::assoc_source_supported(e)))
|
||||
.then_some((surface, profile, path))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
pending.sort_by_key(|(surface, _, _)| surface.value());
|
||||
pending
|
||||
}
|
||||
|
||||
/// Associative extruded, revolved and lofted surfaces made here that the
|
||||
/// drawing's network does not carry yet, with their sources.
|
||||
fn pending_source_surfaces(
|
||||
document: &CadDocument,
|
||||
) -> Vec<(Handle, crate::scene::model::surface_sources::SurfaceSources)> {
|
||||
use crate::scene::model::{surface_sources, sweep_model};
|
||||
let carried: Vec<Handle> = document
|
||||
.objects
|
||||
.keys()
|
||||
.filter_map(|handle| action_surface(document, *handle))
|
||||
.collect();
|
||||
let mut pending = document
|
||||
.entities()
|
||||
.filter(|entity| matches!(entity, codec::EntityType::Surface(_)))
|
||||
.filter_map(|entity| {
|
||||
let sources = surface_sources::surface_sources(entity)?;
|
||||
let surface = entity.common().handle;
|
||||
(!carried.contains(&surface)
|
||||
&& sources.handles().iter().all(|handle| {
|
||||
document.get_entity(*handle).is_some_and(|e| sweep_model::assoc_source_supported(e))
|
||||
}))
|
||||
.then_some((surface, sources))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
pending.sort_by_key(|(surface, _)| surface.value());
|
||||
pending
|
||||
}
|
||||
|
||||
/// Whether the drawing has associative surfaces its network has to carry.
|
||||
fn has_pending_surfaces(document: &CadDocument) -> bool {
|
||||
!pending_swept_surfaces(document).is_empty() || !pending_source_surfaces(document).is_empty()
|
||||
}
|
||||
|
||||
/// Named-objects dictionary key of the drawing's persistent subentity manager.
|
||||
const SUBENT_MANAGER_KEY: &str = "ACAD_ASSOCPERSSUBENTMANAGER";
|
||||
|
||||
/// A read dependency of an action on `dependent_on`.
|
||||
fn read_dependency(dependent_on: Handle, id: i32) -> AssocDependency {
|
||||
AssocDependency {
|
||||
class_version: 2,
|
||||
status: 0,
|
||||
is_read_dependency: true,
|
||||
is_write_dependency: false,
|
||||
is_attached_to_object: true,
|
||||
is_delegating_to_owning_action: true,
|
||||
order: 0,
|
||||
dependent_on,
|
||||
name: None,
|
||||
read_dependency: Handle::NULL,
|
||||
node: Handle::NULL,
|
||||
dependency_body: Handle::NULL,
|
||||
dependency_body_id: id,
|
||||
}
|
||||
}
|
||||
|
||||
/// An edge action parameter reading `curve` through dependency `read`.
|
||||
fn edge_action_param(read: Handle, (action_type, subcurve_kind, curve): crate::scene::model::sweep_model::AssocCurve) -> AssociativeData {
|
||||
use codec::objects::{AssocActionParam, AssocEdgeActionParam, AssocSingleDependencyActionParam};
|
||||
AssociativeData::EdgeActionParam(AssocEdgeActionParam {
|
||||
single_dependency: AssocSingleDependencyActionParam {
|
||||
action_param: AssocActionParam { is_r2013: 1, version: 0, name: String::new() },
|
||||
dependency_class_version: 0,
|
||||
dependency: read,
|
||||
class_version: 0,
|
||||
},
|
||||
parameter: Handle::NULL,
|
||||
has_action: true,
|
||||
action_type,
|
||||
subcurve_kind,
|
||||
curve,
|
||||
})
|
||||
}
|
||||
|
||||
/// The subentity manager's record of a region's edges: one entry per edge
|
||||
/// id from `first_id`, in the order the region's dependencies read them,
|
||||
/// each naming the edge (and, between edges, its two vertices) by the
|
||||
/// region's face and edge numbers. `loops` holds each loop's edge count.
|
||||
fn region_subent_entries(loops: &[usize], step: i32, first_id: i32) -> Vec<i32> {
|
||||
const BODY: i32 = 1_000_000_000;
|
||||
const FACE: i32 = 1001;
|
||||
let mut values = Vec::new();
|
||||
let (mut id, mut offset) = (first_id, 0usize);
|
||||
for &count in loops {
|
||||
let edge = |index: usize| 51001 + 100_000 * (offset + index % count) as i32;
|
||||
let vertex = |a: i32, b: i32| [1, 3, 1, BODY, FACE, 1, BODY, a, 1, BODY, b, 0, 0, 0, 900, 0, 900, 0, 0];
|
||||
for read in 0..count {
|
||||
if count == 1 {
|
||||
values.extend([4, 0, 5, id, step, step, 0, 0, 24, 0, 0, 1, BODY, edge(0), 0, 0, 1, 1, BODY, FACE]);
|
||||
values.extend([0; 14]);
|
||||
} else {
|
||||
// The dependencies start at the loop's second edge.
|
||||
let index = read + 1;
|
||||
values.extend([4, 0, 11, id, step, step, 0, 2, 2, 2, 0, 2, 2, 2, 54, 0, 0, 1, BODY, edge(index)]);
|
||||
values.extend([0, 0, 1, 1, BODY, FACE, 0, 0, 0, 0]);
|
||||
values.extend(vertex(edge(index), edge(index + count - 1)));
|
||||
values.extend([0, 0]);
|
||||
values.extend(vertex(edge(index + 1), edge(index)));
|
||||
}
|
||||
id += 1;
|
||||
}
|
||||
offset += count;
|
||||
}
|
||||
values
|
||||
}
|
||||
|
||||
/// Records a region read by an action in the drawing's persistent
|
||||
/// subentity manager (made when the drawing has none): a step for the
|
||||
/// action and one for the region, then one id per edge. The manager keeps
|
||||
/// the last id given, its edge entries and the list of every id given.
|
||||
/// Returns the edges' ids, or `None` when the drawing's manager has a form
|
||||
/// this cannot extend.
|
||||
fn register_region_subents(document: &mut CadDocument, loops: &[usize]) -> Option<Vec<i32>> {
|
||||
use codec::objects::AssocPersSubentManager;
|
||||
let root = document.header.named_objects_dict_handle;
|
||||
let existing = match document.objects.get(&root) {
|
||||
Some(ObjectType::Dictionary(dictionary)) => dictionary.get(SUBENT_MANAGER_KEY),
|
||||
_ => return None,
|
||||
};
|
||||
let current = existing.and_then(|handle| match document.objects.get(&handle) {
|
||||
Some(ObjectType::Associative(AssociativeObject { data: AssociativeData::PersSubentManager(manager), .. })) => {
|
||||
Some((handle, manager.values.clone()))
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
let (handle, last, count, mut entries) = match current {
|
||||
Some((handle, values)) => {
|
||||
let last = *values.first()?;
|
||||
if values.len() == 4 && values[1..].iter().all(|value| *value == 0) {
|
||||
(handle, last, 0, Vec::new())
|
||||
} else {
|
||||
let tail = usize::try_from(last).ok()? + 3;
|
||||
if values.len() < tail + 2 || values[values.len() - tail] != last {
|
||||
return None;
|
||||
}
|
||||
(handle, last, values[1], values[2..values.len() - tail].to_vec())
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let handle = document.allocate_handle();
|
||||
set_dictionary_entry(document, root, SUBENT_MANAGER_KEY, handle);
|
||||
(handle, 0, 0, Vec::new())
|
||||
}
|
||||
};
|
||||
let edges = loops.iter().sum::<usize>() as i32;
|
||||
let first = last + 3;
|
||||
entries.extend(region_subent_entries(loops, last + 2, first));
|
||||
let last = last + 2 + edges;
|
||||
let mut values = vec![last, count + edges];
|
||||
values.extend(entries);
|
||||
values.push(last);
|
||||
values.extend(1..=last);
|
||||
values.extend([0, 0]);
|
||||
document.objects.insert(handle, ObjectType::Associative(AssociativeObject {
|
||||
handle,
|
||||
owner: root,
|
||||
reactors: vec![root],
|
||||
dxf_name: "ACDBASSOCPERSSUBENTMANAGER".to_string(),
|
||||
cpp_class_name: "AcDbAssocPersSubentManager".to_string(),
|
||||
data: AssociativeData::PersSubentManager(AssocPersSubentManager {
|
||||
class_version: 2,
|
||||
markers: [3, 0, 2],
|
||||
values,
|
||||
final_flag: false,
|
||||
}),
|
||||
..Default::default()
|
||||
}));
|
||||
Some((first..first + edges).collect())
|
||||
}
|
||||
|
||||
/// How an action reads one source through `parameter`: one read dependency
|
||||
/// on a curve; on a region one geometry dependency per edge, naming the
|
||||
/// edge by its persistent subentity id and chained on the region. Adds the
|
||||
/// dependencies (owned by `action`) and edge parameters to `objects` with
|
||||
/// ids from `first_id`; returns (edge parameter, dependency) pairs.
|
||||
fn source_reads(
|
||||
document: &mut CadDocument,
|
||||
action: Handle,
|
||||
parameter: Handle,
|
||||
source: Handle,
|
||||
first_id: i32,
|
||||
objects: &mut Vec<(Handle, Handle, &'static str, &'static str, AssociativeData)>,
|
||||
) -> Option<Vec<(Handle, Handle)>> {
|
||||
use crate::scene::model::sweep_model;
|
||||
let entity = document.get_entity(source)?;
|
||||
let Some(loops) = sweep_model::region_edge_curves(entity) else {
|
||||
let curve = sweep_model::assoc_edge_curve(entity)?;
|
||||
let (edge, read) = (document.allocate_handle(), document.allocate_handle());
|
||||
objects.push((edge, parameter, "ACDBASSOCEDGEACTIONPARAM", "AcDbAssocEdgeActionParam", edge_action_param(read, curve)));
|
||||
objects.push((read, action, "ACDBASSOCDEPENDENCY", "AcDbAssocDependency",
|
||||
AssociativeData::Dependency(read_dependency(source, first_id))));
|
||||
return Some(vec![(edge, read)]);
|
||||
};
|
||||
let sizes = loops.iter().map(Vec::len).collect::<Vec<_>>();
|
||||
let ids = register_region_subents(document, &sizes)?;
|
||||
// The region carries the step its edge names belong to; without it (and
|
||||
// without named edges in its modeler data) the reference cannot find
|
||||
// the edges again.
|
||||
crate::scene::view::dispatch::set_entity_xdata(document, source, "ACAD_STEPID",
|
||||
Some(vec![codec::xdata::XDataValue::Integer32(ids.first()? - 1)]));
|
||||
let curves = loops.into_iter().flatten().collect::<Vec<_>>();
|
||||
let pairs = curves.iter().map(|_| (document.allocate_handle(), document.allocate_handle())).collect::<Vec<_>>();
|
||||
for (index, (curve, (edge, read))) in curves.into_iter().zip(pairs.iter().copied()).enumerate() {
|
||||
let mut dependency = read_dependency(source, first_id + index as i32);
|
||||
dependency.read_dependency = index.checked_sub(1).map_or(Handle::NULL, |previous| pairs[previous].1);
|
||||
dependency.node = pairs.get(index + 1).map_or(Handle::NULL, |next| next.1);
|
||||
objects.push((edge, parameter, "ACDBASSOCEDGEACTIONPARAM", "AcDbAssocEdgeActionParam", edge_action_param(read, curve)));
|
||||
objects.push((read, action, "ACDBASSOCGEOMDEPENDENCY", "AcDbAssocGeomDependency",
|
||||
AssociativeData::GeomDependency(AssocGeomDependency {
|
||||
dependency,
|
||||
class_version: 0,
|
||||
enabled: true,
|
||||
persistent_subent: AssocPersistentSubentId {
|
||||
class_name: "AcDbAssocAsmBasedEntityPersSubentId".to_string(),
|
||||
dependent_on_compound_object: false,
|
||||
class_code: 5,
|
||||
values: vec![0, 2, ids[index]],
|
||||
leading_flag: false,
|
||||
},
|
||||
})));
|
||||
}
|
||||
Some(pairs)
|
||||
}
|
||||
|
||||
/// The reference's associative network for each pending extruded, revolved
|
||||
/// or lofted surface: an action with the matching surface action body that
|
||||
/// writes the surface and reads its sources through path parameters (an
|
||||
/// empty one for a revolve axis given by points and for a loft without a
|
||||
/// path), with the surface's values as value parameters. Returns the
|
||||
/// network's new actions in order.
|
||||
fn materialize_source_surface_actions(
|
||||
allocator: &mut Allocator,
|
||||
network: Handle,
|
||||
pending: &[(Handle, crate::scene::model::surface_sources::SurfaceSources)],
|
||||
first_index: i32,
|
||||
) -> Vec<Handle> {
|
||||
use crate::scene::model::surface_sources::SurfaceSources;
|
||||
use codec::objects::*;
|
||||
let mut added = Vec::new();
|
||||
for (surface, sources) in pending.iter() {
|
||||
let surface = *surface;
|
||||
let document = &*allocator.document;
|
||||
// The value parameters: name, unit type, values, and whether they
|
||||
// control the surface.
|
||||
let real = |value: f64| AssocEvalVariant { code: 40, value: AssocEvalValue::Real(value) };
|
||||
let short = |value: i32| AssocEvalVariant { code: 70, value: AssocEvalValue::Short(value as i16) };
|
||||
let (kind, class, dxf, values, params): (
|
||||
AssocSurfaceActionKind,
|
||||
&str,
|
||||
&str,
|
||||
Vec<(&str, i32, Vec<AssocEvalVariant>, bool)>,
|
||||
Vec<(&str, Option<Handle>)>,
|
||||
) = match sources {
|
||||
SurfaceSources::Extrude { profile, height, taper, .. } => (
|
||||
AssocSurfaceActionKind::Extruded,
|
||||
"AcDbAssocExtrudedSurfaceActionBody",
|
||||
"ACDBASSOCEXTRUDEDSURFACEACTIONBODY",
|
||||
vec![("ExtrusionHeight", 1, vec![real(*height)], true), ("ExtrusionTaperAngle", 2, vec![real(*taper)], true)],
|
||||
vec![("ExtrudeProfile", Some(*profile))],
|
||||
),
|
||||
SurfaceSources::Revolve { profile, axis, angle, .. } => (
|
||||
AssocSurfaceActionKind::Revolved,
|
||||
"AcDbAssocRevolvedSurfaceActionBody",
|
||||
"ACDBASSOCREVOLVEDSURFACEACTIONBODY",
|
||||
vec![("RevolveAngle", 2, vec![real(*angle)], true)],
|
||||
vec![("RevolveProfile", Some(*profile)), ("RevolvePath", *axis)],
|
||||
),
|
||||
SurfaceSources::Loft { sections } => {
|
||||
let loft = document.solid_history_operations(surface).and_then(|operations| {
|
||||
operations.into_iter().find_map(|operation| match operation {
|
||||
SolidHistoryOperation::Loft(record) => record.parameters,
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
let (continuity, bulge) = loft.map_or(((1, 1), (0.5, 0.5)), |p| {
|
||||
((p.start_continuity, p.end_continuity), (p.start_bulge, p.end_bulge))
|
||||
});
|
||||
(
|
||||
AssocSurfaceActionKind::Lofted,
|
||||
"AcDbAssocLoftedSurfaceActionBody",
|
||||
"ACDBASSOCLOFTEDSURFACEACTIONBODY",
|
||||
// One value per cross section: the start and end
|
||||
// settings at the ends, the defaults between them.
|
||||
vec![
|
||||
("Continuity", 0, (0..sections.len()).map(|index| short(if index == 0 { continuity.0 } else if index + 1 == sections.len() { continuity.1 } else { 1 })).collect(), false),
|
||||
("Bulge", 0, (0..sections.len()).map(|index| real(if index == 0 { bulge.0 } else if index + 1 == sections.len() { bulge.1 } else { 0.5 })).collect(), false),
|
||||
],
|
||||
sections.iter().map(|section| ("CrossSection", Some(*section)))
|
||||
.chain(std::iter::once(("PathCurve", None)))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
};
|
||||
let document = &mut *allocator.document;
|
||||
let (action, body, written) = (document.allocate_handle(), document.allocate_handle(), document.allocate_handle());
|
||||
let mut objects: Vec<(Handle, Handle, &'static str, &'static str, AssociativeData)> = Vec::new();
|
||||
let mut reactors: Vec<(Handle, Handle)> = vec![(surface, written)];
|
||||
let mut dependencies = vec![written];
|
||||
let mut owned = Vec::new();
|
||||
let mut writes = read_dependency(surface, 1);
|
||||
(writes.is_read_dependency, writes.is_write_dependency, writes.order) = (false, true, i32::MIN);
|
||||
objects.push((written, action, "ACDBASSOCDEPENDENCY", "AcDbAssocDependency", AssociativeData::Dependency(writes)));
|
||||
let mut complete = true;
|
||||
for (name, source) in params.iter() {
|
||||
let parameter = document.allocate_handle();
|
||||
let mut children = Vec::new();
|
||||
if let Some(source) = source {
|
||||
let Some(reads) = source_reads(document, action, parameter, *source, dependencies.len() as i32 + 1, &mut objects) else {
|
||||
complete = false;
|
||||
break;
|
||||
};
|
||||
for (edge, read) in reads {
|
||||
reactors.push((*source, read));
|
||||
dependencies.push(read);
|
||||
children.push(edge);
|
||||
}
|
||||
}
|
||||
objects.push((parameter, action, "ACDBASSOCPATHACTIONPARAM", "AcDbAssocPathActionParam",
|
||||
AssociativeData::PathActionParam(AssocPathActionParam {
|
||||
compound: AssocCompoundActionParam {
|
||||
action_param: AssocActionParam { is_r2013: 1, version: 0, name: name.to_string() },
|
||||
class_version: 0,
|
||||
status: 0,
|
||||
parameters: children,
|
||||
child_parameter: None,
|
||||
},
|
||||
version: 0,
|
||||
})));
|
||||
owned.push(parameter);
|
||||
}
|
||||
if !complete {
|
||||
continue;
|
||||
}
|
||||
objects.push((action, network, "ACDBASSOCACTION", "AcDbAssocAction", AssociativeData::Action(AssocAction {
|
||||
class_version: 2,
|
||||
geometry_status: 0,
|
||||
owning_network: network,
|
||||
action_body: body,
|
||||
action_index: 0,
|
||||
max_dependency_index: dependencies.len() as i32,
|
||||
dependencies: dependencies.iter().map(|handle| AssocActionDependency { is_owned: true, dependency: *handle }).collect(),
|
||||
owned_parameters: owned,
|
||||
values: values
|
||||
.into_iter()
|
||||
.map(|(name, unit_type, amounts, controls)| AssocValueParam {
|
||||
class_version: 0,
|
||||
name: name.to_string(),
|
||||
unit_type,
|
||||
variables: amounts.into_iter().map(|value| AssocValueParamVariable { value, handle: Handle::NULL }).collect(),
|
||||
controlled_object_dependency: if controls { written } else { Handle::NULL },
|
||||
})
|
||||
.collect(),
|
||||
})));
|
||||
objects.push((body, action, dxf, class, AssociativeData::SurfaceActionBody(AssocSurfaceActionBody {
|
||||
kind,
|
||||
action_body: AssocActionBody { version: 2 },
|
||||
surface_body: AssocSurfaceBody { version: 0, dependency: written, is_semi_associative: false, marker: 1, is_semi_override: false, grip_status: 0 },
|
||||
..Default::default()
|
||||
})));
|
||||
for (handle, owner, dxf, class, data) in objects {
|
||||
allocator.insert_associative_at(handle, owner, dxf, class, data);
|
||||
}
|
||||
for (target, reactor) in reactors {
|
||||
let document = &mut *allocator.document;
|
||||
if let Some(entity) = document.get_entity_mut(target) {
|
||||
entity.common_mut().reactors.push(reactor);
|
||||
} else if let Some(ObjectType::Associative(object)) = document.objects.get_mut(&target) {
|
||||
object.reactors.push(reactor);
|
||||
}
|
||||
}
|
||||
added.push(action);
|
||||
}
|
||||
for (offset, handle) in added.iter().copied().enumerate() {
|
||||
if let Some(ObjectType::Associative(AssociativeObject { data: AssociativeData::Action(action), .. })) =
|
||||
allocator.document.objects.get_mut(&handle)
|
||||
{
|
||||
action.action_index = first_index + offset as i32 + 1;
|
||||
}
|
||||
}
|
||||
added
|
||||
}
|
||||
|
||||
/// The names an expression reads.
|
||||
fn expression_names(expression: &str) -> Vec<String> {
|
||||
let mut names = Vec::new();
|
||||
@@ -2717,16 +3094,6 @@ fn materialize_swept_surface_actions(
|
||||
options.twist_angle,
|
||||
);
|
||||
let expressions = sweep_model::sweep_expressions(surface_entity).unwrap_or([None, None]);
|
||||
let (Some(profile_curve), Some(path_curve)) = (
|
||||
document
|
||||
.get_entity(profile)
|
||||
.and_then(sweep_model::assoc_edge_curve),
|
||||
document
|
||||
.get_entity(path)
|
||||
.and_then(sweep_model::assoc_edge_curve),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let evaluate = |expression: &str| {
|
||||
let mut table = parameters.clone();
|
||||
table.set("sweepExpression", expression).ok()?;
|
||||
@@ -2737,27 +3104,7 @@ fn materialize_swept_surface_actions(
|
||||
};
|
||||
let document = &mut *allocator.document;
|
||||
let mut next = || document.allocate_handle();
|
||||
let (
|
||||
action,
|
||||
body,
|
||||
written,
|
||||
profile_param,
|
||||
profile_edge,
|
||||
profile_read,
|
||||
path_param,
|
||||
path_edge,
|
||||
path_read,
|
||||
) = (
|
||||
next(),
|
||||
next(),
|
||||
next(),
|
||||
next(),
|
||||
next(),
|
||||
next(),
|
||||
next(),
|
||||
next(),
|
||||
next(),
|
||||
);
|
||||
let (action, body, written, profile_param, path_param) = (next(), next(), next(), next(), next());
|
||||
let dependency = |dependent_on: Handle, read: bool, id: i32, order: i32| AssocDependency {
|
||||
class_version: 2,
|
||||
status: 0,
|
||||
@@ -2773,13 +3120,15 @@ fn materialize_swept_surface_actions(
|
||||
dependency_body: Handle::NULL,
|
||||
dependency_body_id: id,
|
||||
};
|
||||
let mut objects: Vec<(Handle, Handle, &str, &str, AssociativeData)> = Vec::new();
|
||||
let mut reactors: Vec<(Handle, Handle)> = vec![
|
||||
(surface, written),
|
||||
(profile, profile_read),
|
||||
(path, path_read),
|
||||
];
|
||||
let mut dependencies = vec![written, profile_read, path_read];
|
||||
let mut objects: Vec<(Handle, Handle, &'static str, &'static str, AssociativeData)> = Vec::new();
|
||||
// The profile (a region by its edges) and the path, read in that order.
|
||||
let Some(profile_reads) = source_reads(document, action, profile_param, profile, 2, &mut objects) else { continue };
|
||||
let Some(path_reads) = source_reads(document, action, path_param, path, profile_reads.len() as i32 + 2, &mut objects) else { continue };
|
||||
let mut reactors: Vec<(Handle, Handle)> = vec![(surface, written)];
|
||||
reactors.extend(profile_reads.iter().map(|(_, read)| (profile, *read)));
|
||||
reactors.extend(path_reads.iter().map(|(_, read)| (path, *read)));
|
||||
let mut dependencies = vec![written];
|
||||
dependencies.extend(profile_reads.iter().chain(&path_reads).map(|(_, read)| *read));
|
||||
let mut value_handles = [Handle::NULL, Handle::NULL];
|
||||
let mut network_actions = vec![action];
|
||||
// Scale (slot 0) and twist (slot 1) expressions.
|
||||
@@ -2916,94 +3265,22 @@ fn materialize_swept_surface_actions(
|
||||
grip_status: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}),
|
||||
));
|
||||
objects.push((
|
||||
written,
|
||||
action,
|
||||
"ACDBASSOCDEPENDENCY",
|
||||
"AcDbAssocDependency",
|
||||
AssociativeData::Dependency(dependency(surface, false, 1, i32::MIN)),
|
||||
));
|
||||
let edge_param =
|
||||
|read: Handle,
|
||||
(kind, subcurve, curve): (i32, AssocSubcurveKind, Vec<AssocCurveValue>)| {
|
||||
AssociativeData::EdgeActionParam(AssocEdgeActionParam {
|
||||
single_dependency: AssocSingleDependencyActionParam {
|
||||
action_param: AssocActionParam {
|
||||
is_r2013: 1,
|
||||
version: 0,
|
||||
name: String::new(),
|
||||
},
|
||||
dependency_class_version: 0,
|
||||
dependency: read,
|
||||
class_version: 0,
|
||||
},
|
||||
parameter: Handle::NULL,
|
||||
has_action: true,
|
||||
action_type: kind,
|
||||
subcurve_kind: subcurve,
|
||||
curve,
|
||||
})
|
||||
};
|
||||
let path_parameter = |name: &str, child: Handle| {
|
||||
AssociativeData::PathActionParam(AssocPathActionParam {
|
||||
compound: AssocCompoundActionParam {
|
||||
action_param: AssocActionParam {
|
||||
is_r2013: 1,
|
||||
version: 0,
|
||||
name: name.to_string(),
|
||||
},
|
||||
class_version: 0,
|
||||
status: 0,
|
||||
parameters: vec![child],
|
||||
child_parameter: None,
|
||||
},
|
||||
version: 0,
|
||||
})
|
||||
};
|
||||
objects.push((
|
||||
profile_param,
|
||||
action,
|
||||
"ACDBASSOCPATHACTIONPARAM",
|
||||
"AcDbAssocPathActionParam",
|
||||
path_parameter("SweepProfile", profile_edge),
|
||||
));
|
||||
objects.push((
|
||||
profile_edge,
|
||||
profile_param,
|
||||
"ACDBASSOCEDGEACTIONPARAM",
|
||||
"AcDbAssocEdgeActionParam",
|
||||
edge_param(profile_read, profile_curve),
|
||||
));
|
||||
objects.push((
|
||||
profile_read,
|
||||
action,
|
||||
"ACDBASSOCDEPENDENCY",
|
||||
"AcDbAssocDependency",
|
||||
AssociativeData::Dependency(dependency(profile, true, 2, 0)),
|
||||
));
|
||||
objects.push((
|
||||
path_param,
|
||||
action,
|
||||
"ACDBASSOCPATHACTIONPARAM",
|
||||
"AcDbAssocPathActionParam",
|
||||
path_parameter("SweepPath", path_edge),
|
||||
));
|
||||
objects.push((
|
||||
path_edge,
|
||||
path_param,
|
||||
"ACDBASSOCEDGEACTIONPARAM",
|
||||
"AcDbAssocEdgeActionParam",
|
||||
edge_param(path_read, path_curve),
|
||||
));
|
||||
objects.push((
|
||||
path_read,
|
||||
action,
|
||||
"ACDBASSOCDEPENDENCY",
|
||||
"AcDbAssocDependency",
|
||||
AssociativeData::Dependency(dependency(path, true, 3, 0)),
|
||||
));
|
||||
})));
|
||||
objects.push((written, action, "ACDBASSOCDEPENDENCY", "AcDbAssocDependency", AssociativeData::Dependency(dependency(surface, false, 1, i32::MIN))));
|
||||
let path_parameter = |name: &str, children: Vec<Handle>| AssociativeData::PathActionParam(AssocPathActionParam {
|
||||
compound: AssocCompoundActionParam {
|
||||
action_param: AssocActionParam { is_r2013: 1, version: 0, name: name.to_string() },
|
||||
class_version: 0,
|
||||
status: 0,
|
||||
parameters: children,
|
||||
child_parameter: None,
|
||||
},
|
||||
version: 0,
|
||||
});
|
||||
objects.push((profile_param, action, "ACDBASSOCPATHACTIONPARAM", "AcDbAssocPathActionParam",
|
||||
path_parameter("SweepProfile", profile_reads.iter().map(|(edge, _)| *edge).collect())));
|
||||
objects.push((path_param, action, "ACDBASSOCPATHACTIONPARAM", "AcDbAssocPathActionParam",
|
||||
path_parameter("SweepPath", path_reads.iter().map(|(edge, _)| *edge).collect())));
|
||||
for (handle, owner, dxf, class, data) in objects {
|
||||
allocator.insert_associative_at(handle, owner, dxf, class, data);
|
||||
}
|
||||
@@ -3180,7 +3457,7 @@ fn materialize_scope(
|
||||
&& existing_variables.is_empty()
|
||||
&& preserved_groups.is_empty()
|
||||
&& foreign_actions.is_empty()
|
||||
&& !(materialize_all_parameters && !pending_swept_surfaces(document).is_empty())
|
||||
&& !(materialize_all_parameters && has_pending_surfaces(document))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -3365,18 +3642,14 @@ fn materialize_scope(
|
||||
}
|
||||
}
|
||||
refresh_foreign_values(allocator.document, &foreign_actions, parameters);
|
||||
let pending = if materialize_all_parameters {
|
||||
pending_swept_surfaces(allocator.document)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let surface_actions = materialize_swept_surface_actions(
|
||||
&mut allocator,
|
||||
network_handle,
|
||||
parameters,
|
||||
&pending,
|
||||
first_foreign + foreign_actions.len() as i32,
|
||||
);
|
||||
let pending = if materialize_all_parameters { pending_swept_surfaces(allocator.document) } else { Vec::new() };
|
||||
let mut surface_actions = materialize_swept_surface_actions(
|
||||
&mut allocator, network_handle, parameters, &pending, first_foreign + foreign_actions.len() as i32);
|
||||
let pending_sources = if materialize_all_parameters { pending_source_surfaces(allocator.document) } else { Vec::new() };
|
||||
let source_actions = materialize_source_surface_actions(
|
||||
&mut allocator, network_handle, &pending_sources,
|
||||
first_foreign + (foreign_actions.len() + surface_actions.len()) as i32);
|
||||
surface_actions.extend(source_actions);
|
||||
|
||||
let network = AssocNetwork {
|
||||
action: AssocAction {
|
||||
@@ -3537,8 +3810,7 @@ impl Scene {
|
||||
if set.is_none()
|
||||
&& !existing
|
||||
&& (owner != model_owner
|
||||
|| (self.named_parameters.is_empty()
|
||||
&& pending_swept_surfaces(&self.document).is_empty()))
|
||||
|| (self.named_parameters.is_empty() && !has_pending_surfaces(&self.document)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -3732,6 +3732,11 @@ impl Scene {
|
||||
changes.push(change);
|
||||
}
|
||||
}
|
||||
for change in self.refresh_associative_surfaces(&changes) {
|
||||
if !changes.iter().any(|(handle, _)| *handle == change.0) {
|
||||
changes.push(change);
|
||||
}
|
||||
}
|
||||
if solve_parametric && !self.parametric_constraints.is_empty() {
|
||||
for change in self.refresh_parametric_constraints_with_initial_policy(
|
||||
&changes,
|
||||
|
||||
@@ -7,9 +7,11 @@ use codec::objects::{SolidHistoryLoft, SolidHistoryLoftParameters, SolidHistoryN
|
||||
use codec::{EntityType, Handle};
|
||||
use kernel::brep::Body;
|
||||
|
||||
fn embedded_section(entity: &EntityType) -> Option<EmbeddedEntity> {
|
||||
pub(crate) fn embedded_section(entity: &EntityType) -> Option<EmbeddedEntity> {
|
||||
match entity {
|
||||
EntityType::Region(region) => Some(EmbeddedEntity::Region(region.clone())),
|
||||
// A region as the curve round it, as the reference records it.
|
||||
EntityType::Region(region) => Some(super::sweep_model::region_boundary_entity(entity)
|
||||
.map_or_else(|| EmbeddedEntity::Region(region.clone()), |(curve, _)| curve)),
|
||||
EntityType::Point(point) => Some(EmbeddedEntity::Point(point.clone())),
|
||||
_ => embedded_path(entity),
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ pub mod point_cloud;
|
||||
pub mod presspull_model;
|
||||
pub mod solid_history;
|
||||
pub mod solid_model;
|
||||
pub mod surface_sources;
|
||||
mod sweep_command_model;
|
||||
pub mod sweep_model;
|
||||
pub mod underlay_vector;
|
||||
|
||||
@@ -850,8 +850,11 @@ pub fn extrusion_surface_data(value: &SolidHistorySweep) -> Option<codec::entiti
|
||||
sweep_entity: Some(profile),
|
||||
options: SurfaceSweepOptions {
|
||||
draft_angle: value.draft_angle,
|
||||
draft_start_distance: value.start_draft_distance,
|
||||
draft_end_distance: value.end_draft_distance,
|
||||
// The reference keeps no draft distances on an extruded
|
||||
// surface; a distance there reads as a taper when it
|
||||
// re-extrudes the surface from a changed profile.
|
||||
draft_start_distance: 0.0,
|
||||
draft_end_distance: 0.0,
|
||||
twist_angle: value.twist_angle,
|
||||
scale_factor: value.scale_factor,
|
||||
align_angle: value.align_angle,
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
//! Associative extruded, revolved and lofted surfaces (SURFACEASSOCIATIVITY):
|
||||
//! the sources and values each surface was made from, kept with the surface
|
||||
//! so it follows edits of its sources and the drawing's associative network
|
||||
//! describes it on save.
|
||||
|
||||
use codec::xdata::XDataValue;
|
||||
use codec::{EntityType, Handle};
|
||||
use glam::DVec3;
|
||||
|
||||
/// The application name under which a surface keeps its sources.
|
||||
pub const SURFACE_SOURCES_APP: &str = "OCS_SURFACE_SOURCES";
|
||||
|
||||
/// What an associative surface was made from.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum SurfaceSources {
|
||||
/// EXTRUDE: the profile, the height along its normal (or the length of a
|
||||
/// given direction) and the taper angle.
|
||||
Extrude { profile: Handle, height: f64, direction: Option<DVec3>, taper: f64 },
|
||||
/// REVOLVE: the profile, the axis (an axis object or two points), the
|
||||
/// angle and the start angle.
|
||||
Revolve { profile: Handle, axis: Option<Handle>, start: DVec3, end: DVec3, angle: f64, start_angle: f64 },
|
||||
/// LOFT: the cross sections in order.
|
||||
Loft { sections: Vec<Handle> },
|
||||
}
|
||||
|
||||
impl SurfaceSources {
|
||||
/// Every entity the surface reads.
|
||||
pub fn handles(&self) -> Vec<Handle> {
|
||||
match self {
|
||||
SurfaceSources::Extrude { profile, .. } => vec![*profile],
|
||||
SurfaceSources::Revolve { profile, axis, .. } => std::iter::once(*profile).chain(*axis).collect(),
|
||||
SurfaceSources::Loft { sections } => sections.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep the sources with the surface.
|
||||
pub fn link_surface_sources(document: &mut codec::CadDocument, surface: Handle, sources: &SurfaceSources) {
|
||||
if surface.is_null() {
|
||||
return;
|
||||
}
|
||||
let point = |p: DVec3| XDataValue::Point3D(codec::types::Vector3::new(p.x, p.y, p.z));
|
||||
let values = match sources {
|
||||
SurfaceSources::Extrude { profile, height, direction, taper } => {
|
||||
let mut values = vec![
|
||||
XDataValue::String("EXTRUDE".into()),
|
||||
XDataValue::Handle(*profile),
|
||||
XDataValue::Real(*height),
|
||||
XDataValue::Real(*taper),
|
||||
];
|
||||
values.extend(direction.map(point));
|
||||
values
|
||||
}
|
||||
SurfaceSources::Revolve { profile, axis, start, end, angle, start_angle } => {
|
||||
let mut values = vec![
|
||||
XDataValue::String("REVOLVE".into()),
|
||||
XDataValue::Handle(*profile),
|
||||
point(*start),
|
||||
point(*end),
|
||||
XDataValue::Real(*angle),
|
||||
XDataValue::Real(*start_angle),
|
||||
];
|
||||
values.extend(axis.map(XDataValue::Handle));
|
||||
values
|
||||
}
|
||||
SurfaceSources::Loft { sections } => std::iter::once(XDataValue::String("LOFT".into()))
|
||||
.chain(sections.iter().map(|handle| XDataValue::Handle(*handle)))
|
||||
.collect(),
|
||||
};
|
||||
crate::scene::view::dispatch::set_entity_xdata(document, surface, SURFACE_SOURCES_APP, Some(values));
|
||||
}
|
||||
|
||||
/// The sources an associative surface keeps.
|
||||
pub fn surface_sources(entity: &EntityType) -> Option<SurfaceSources> {
|
||||
let record = entity
|
||||
.common()
|
||||
.extended_data
|
||||
.records()
|
||||
.iter()
|
||||
.find(|record| record.application_name == SURFACE_SOURCES_APP)?;
|
||||
let mut kind = None;
|
||||
let (mut handles, mut reals, mut points) = (Vec::new(), Vec::new(), Vec::new());
|
||||
for value in &record.values {
|
||||
match value {
|
||||
XDataValue::String(text) if kind.is_none() => kind = Some(text.as_str()),
|
||||
XDataValue::Handle(handle) => handles.push(*handle),
|
||||
XDataValue::Real(value) => reals.push(*value),
|
||||
XDataValue::Point3D(p) => points.push(DVec3::new(p.x, p.y, p.z)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
match kind? {
|
||||
"EXTRUDE" => Some(SurfaceSources::Extrude {
|
||||
profile: *handles.first()?,
|
||||
height: *reals.first()?,
|
||||
taper: *reals.get(1)?,
|
||||
direction: points.first().copied(),
|
||||
}),
|
||||
"REVOLVE" => Some(SurfaceSources::Revolve {
|
||||
profile: *handles.first()?,
|
||||
axis: handles.get(1).copied(),
|
||||
start: *points.first()?,
|
||||
end: *points.get(1)?,
|
||||
angle: *reals.first()?,
|
||||
start_angle: *reals.get(1)?,
|
||||
}),
|
||||
"LOFT" => (handles.len() >= 2).then_some(SurfaceSources::Loft { sections: handles }),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The two ends of an axis object (a line).
|
||||
pub fn axis_points(entity: &EntityType) -> Option<(DVec3, DVec3)> {
|
||||
match entity {
|
||||
EntityType::Line(line) => Some((
|
||||
DVec3::new(line.start.x, line.start.y, line.start.z),
|
||||
DVec3::new(line.end.x, line.end.y, line.end.z),
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -14,13 +14,11 @@ fn embedded_sweep_profile(
|
||||
entity: &EntityType,
|
||||
) -> Option<(codec::entities::EmbeddedEntity, [f64; 16])> {
|
||||
match entity {
|
||||
EntityType::Region(region) => Some((
|
||||
codec::entities::EmbeddedEntity::Region(region.clone()),
|
||||
glam::DMat4::IDENTITY.to_cols_array(),
|
||||
)),
|
||||
EntityType::Polyline3D(value) => {
|
||||
Some((polyline_wire(value)?, glam::DMat4::IDENTITY.to_cols_array()))
|
||||
}
|
||||
// A region as the curve round it, as the reference records it.
|
||||
EntityType::Region(region) => super::sweep_model::region_boundary_entity(entity).or_else(|| {
|
||||
Some((codec::entities::EmbeddedEntity::Region(region.clone()), glam::DMat4::IDENTITY.to_cols_array()))
|
||||
}),
|
||||
EntityType::Polyline3D(value) => Some((polyline_wire(value)?, glam::DMat4::IDENTITY.to_cols_array())),
|
||||
_ => embedded_revolve_profile(entity),
|
||||
}
|
||||
}
|
||||
@@ -580,13 +578,7 @@ pub fn resweep_from_sources(
|
||||
/// 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>,
|
||||
)> {
|
||||
pub fn assoc_edge_curve(entity: &EntityType) -> Option<AssocCurve> {
|
||||
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));
|
||||
@@ -729,17 +721,154 @@ pub fn assoc_edge_curve(
|
||||
}
|
||||
Some((47, K::None, values))
|
||||
}
|
||||
// Ellipse (17): centre, unit major axis, unit minor axis (the normal
|
||||
// across the major one), major and minor radius, start and end
|
||||
// parameter, 0.
|
||||
EntityType::Ellipse(ellipse) => {
|
||||
let major = glam::DVec3::new(ellipse.major_axis.x, ellipse.major_axis.y, ellipse.major_axis.z);
|
||||
let normal = glam::DVec3::new(ellipse.normal.x, ellipse.normal.y, ellipse.normal.z).try_normalize()?;
|
||||
let radius = major.length();
|
||||
let axis = major.try_normalize()?;
|
||||
Some((17, K::Ellipse, vec![
|
||||
point(glam::DVec3::new(ellipse.center.x, ellipse.center.y, ellipse.center.z)),
|
||||
point(axis),
|
||||
point(normal.cross(axis)),
|
||||
V::Real(radius),
|
||||
V::Real(radius * ellipse.minor_axis_ratio),
|
||||
V::Real(ellipse.start_parameter),
|
||||
V::Real(ellipse.end_parameter),
|
||||
V::Real(0.0),
|
||||
]))
|
||||
}
|
||||
// NURBS (42): rational flag, a second flag (0), degree, knot
|
||||
// tolerance, then knots, weights (empty unless rational) and control
|
||||
// points, each as its length, the length reserved (at least 8) and the
|
||||
// growth step 8, followed by the items.
|
||||
EntityType::Spline(spline) => {
|
||||
// A fit-point spline as the curve through its points, its knots
|
||||
// in the fit parameter (chord length) as the reference keeps them.
|
||||
let curve = crate::entities::spline::nurbs3(spline)?;
|
||||
let mut knots = curve.knots().to_vec();
|
||||
if crate::entities::spline::uses_fit_method(spline) && spline.knot_parameterization == 0 {
|
||||
let chord: f64 = spline.fit_points.windows(2)
|
||||
.map(|pair| ((pair[1].x - pair[0].x).powi(2) + (pair[1].y - pair[0].y).powi(2) + (pair[1].z - pair[0].z).powi(2)).sqrt())
|
||||
.sum();
|
||||
let (low, high) = (knots[0], knots[knots.len() - 1]);
|
||||
if high - low > 0.0 && chord > 0.0 && (high - low - chord).abs() > 1e-9 * chord {
|
||||
knots.iter_mut().for_each(|knot| *knot = (*knot - low) / (high - low) * chord);
|
||||
}
|
||||
}
|
||||
let points = curve.control_points();
|
||||
if points.len() < 2 || knots.is_empty() { return None; }
|
||||
let tolerance = if spline.knot_tolerance > 0.0 { spline.knot_tolerance } else { 1e-9 };
|
||||
let points = points.iter().map(|p| glam::DVec3::from_array(*p)).collect::<Vec<_>>();
|
||||
Some(nurbs_curve(curve.degree(), tolerance, &knots, curve.weights(), &points))
|
||||
}
|
||||
_ => 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
|
||||
/// A NURBS curve's edge parameter values (42): rational flag, a second flag
|
||||
/// (0), degree, knot tolerance, then knots, weights (empty unless rational)
|
||||
/// and control points, each as its length, the length reserved (at least
|
||||
/// 8) and the growth step 8, followed by the items.
|
||||
fn nurbs_curve(degree: usize, tolerance: f64, knots: &[f64], weights: &[f64], points: &[glam::DVec3]) -> AssocCurve {
|
||||
use codec::objects::{AssocCurveValue as V, AssocSubcurveKind as K};
|
||||
let rational = weights.iter().any(|weight| (weight - 1.0).abs() > 1e-12);
|
||||
let header = |values: &mut Vec<V>, count: usize| {
|
||||
values.extend([V::Int(count as i32), V::Int(if count == 0 { 0 } else { count.max(8) } as i32), V::Int(8)]);
|
||||
};
|
||||
let mut values = vec![V::Bool(rational), V::Bool(false), V::Int(degree as i32), V::Real(tolerance)];
|
||||
header(&mut values, knots.len());
|
||||
values.extend(knots.iter().map(|knot| V::Real(*knot)));
|
||||
let weights = if rational { weights } else { &[] };
|
||||
header(&mut values, weights.len());
|
||||
values.extend(weights.iter().map(|weight| V::Real(*weight)));
|
||||
header(&mut values, points.len());
|
||||
values.extend(points.iter().map(|p| V::Point(codec::types::Vector3::new(p.x, p.y, p.z))));
|
||||
(42, K::Nurb3d, values)
|
||||
}
|
||||
|
||||
/// An edge parameter's curve: kind, subcurve kind and values.
|
||||
pub type AssocCurve = (i32, codec::objects::AssocSubcurveKind, Vec<codec::objects::AssocCurveValue>);
|
||||
|
||||
/// A region's edges as an associative action reads them, loop by loop, each
|
||||
/// loop from its second edge on as the reference lists them: lines (23),
|
||||
/// circles and arcs (11) measured from the arbitrary axis of the region's
|
||||
/// normal, elliptical arcs (17) and NURBS curves (42).
|
||||
pub fn region_edge_curves(entity: &EntityType) -> Option<Vec<Vec<AssocCurve>>> {
|
||||
use codec::objects::{AssocCurveValue as V, AssocSubcurveKind as K};
|
||||
use kernel::geom2d::Curve;
|
||||
let EntityType::Region(_) = entity else { return None };
|
||||
let Some((plane, loops, true)) = super::presspull_model::profile_geometry(entity) else { return None };
|
||||
let at = |uv: [f64; 2]| glam::DVec3::from_array(plane.point_at(uv));
|
||||
let normal = glam::DVec3::from_array(plane.x_axis).cross(glam::DVec3::from_array(plane.y_axis)).try_normalize()?;
|
||||
let axis = arbitrary_x(normal);
|
||||
let across = normal.cross(axis);
|
||||
let point = |p: glam::DVec3| V::Point(codec::types::Vector3::new(p.x, p.y, p.z));
|
||||
let arc = |centre: glam::DVec3, radius: f64, start: f64, end: f64| (11, K::Arc, vec![
|
||||
point(centre), point(normal), point(axis), V::Real(radius), V::Real(start), V::Real(end), V::Real(0.0),
|
||||
]);
|
||||
let curve = |piece: &Curve| -> Option<AssocCurve> {
|
||||
match piece {
|
||||
Curve::Line(line) => {
|
||||
let (a, b) = (at(line.start), at(line.end));
|
||||
Some((23, K::LineSegment3d, vec![point(a), point(b - a)]))
|
||||
}
|
||||
Curve::Circle(circle) => Some(arc(at(circle.centre), circle.radius, 0.0, std::f64::consts::TAU)),
|
||||
Curve::Arc(value) => {
|
||||
let centre = at(value.centre);
|
||||
let offset = at([
|
||||
value.centre[0] + value.start_angle.cos(),
|
||||
value.centre[1] + value.start_angle.sin(),
|
||||
]) - centre;
|
||||
let start = offset.dot(across).atan2(offset.dot(axis));
|
||||
Some(arc(centre, value.radius, start, start + value.sweep()))
|
||||
}
|
||||
Curve::Ellipse(value) => {
|
||||
let ellipse = value.ellipse;
|
||||
let centre = at(ellipse.centre);
|
||||
let tip = [ellipse.centre[0] + ellipse.major_axis[0], ellipse.centre[1] + ellipse.major_axis[1]];
|
||||
let major = (at(tip) - centre).try_normalize()?;
|
||||
let end = if value.end_parameter > value.start_parameter {
|
||||
value.end_parameter
|
||||
} else {
|
||||
value.end_parameter + std::f64::consts::TAU
|
||||
};
|
||||
Some((17, K::Ellipse, vec![
|
||||
point(centre), point(major), point(normal.cross(major)),
|
||||
V::Real(ellipse.major_radius), V::Real(ellipse.minor_radius),
|
||||
V::Real(value.start_parameter), V::Real(end), V::Real(0.0),
|
||||
]))
|
||||
}
|
||||
Curve::Nurbs(value) => {
|
||||
let points = value.control_points().iter().map(|p| at(*p)).collect::<Vec<_>>();
|
||||
Some(nurbs_curve(value.degree(), 1e-9, value.knots(), value.weights(), &points))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
loops
|
||||
.iter()
|
||||
.map(|pieces| {
|
||||
let mut curves = pieces.iter().map(curve).collect::<Option<Vec<_>>>()?;
|
||||
if curves.len() > 1 {
|
||||
curves.rotate_left(1);
|
||||
}
|
||||
Some(curves)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Whether an associative action can read this source: a region's edges
|
||||
/// or the entity's own curve.
|
||||
pub fn assoc_source_supported(entity: &EntityType) -> bool {
|
||||
region_edge_curves(entity).is_some() || assoc_edge_curve(entity).is_some()
|
||||
}
|
||||
|
||||
/// The arbitrary-axis X direction of an entity normal.
|
||||
pub(crate) 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()
|
||||
}
|
||||
|
||||
|
||||
+137
-10
@@ -12,11 +12,7 @@ use kernel::geom2d::{Arc, Curve, EllipseArc, Line};
|
||||
use kernel::space::{PlanarCurve, Plane, Vec3};
|
||||
|
||||
pub use super::sweep_command_model::{
|
||||
assoc_edge_curve, is_sweep_path, is_sweep_profile, link_sweep_expressions, link_sweep_sources,
|
||||
resweep_from_sources, scaled_sweep_path_refusal, set_surface_associativity, spatial_profile,
|
||||
spatial_sweep_record, surface_associativity, surface_sweep_record, sweep_expressions,
|
||||
sweep_record, sweep_refusal_code, sweep_selection_options, sweep_sources, swept_surface_entity,
|
||||
swept_with_options, SWEEP_SOURCES_APP,
|
||||
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, assoc_source_supported, region_edge_curves, AssocCurve, sweep_record, sweep_refusal_code, sweep_selection_options, swept_surface_entity, swept_with_options,
|
||||
};
|
||||
use crate::entities::curve::entity_curve;
|
||||
|
||||
@@ -36,6 +32,9 @@ pub struct Profile {
|
||||
/// is handed over as the arcs it is made of rather than as one curve with
|
||||
/// nowhere for the sweep to start.
|
||||
pub fn profile_of(entity: &EntityType) -> Option<Profile> {
|
||||
if let Some(profile) = region_loop(entity) {
|
||||
return (profile.pieces.len() >= 3).then_some(profile);
|
||||
}
|
||||
let planar = entity_curve(entity).or_else(|| planar_polygon_entity(entity))?;
|
||||
if !planar.curve.is_closed() {
|
||||
return None;
|
||||
@@ -50,6 +49,9 @@ pub fn profile_of(entity: &EntityType) -> Option<Profile> {
|
||||
/// An entity-level planar curve suitable for either a solid profile or an
|
||||
/// open surface profile.
|
||||
pub fn extrusion_profile_of(entity: &EntityType) -> Option<(Profile, bool)> {
|
||||
if let Some(profile) = region_loop(entity) {
|
||||
return Some((profile, true));
|
||||
}
|
||||
let planar = entity_curve(entity).or_else(|| planar_polygon_entity(entity))?;
|
||||
let closed = planar.curve.is_closed();
|
||||
let pieces = profile_pieces(&planar.curve, closed)?;
|
||||
@@ -62,6 +64,30 @@ pub fn extrusion_profile_of(entity: &EntityType) -> Option<(Profile, bool)> {
|
||||
))
|
||||
}
|
||||
|
||||
/// A region of one boundary loop as that loop, read from the region's exact
|
||||
/// edges (its display wires hold one wire per edge), a lone closed edge split
|
||||
/// as a closed curve is.
|
||||
fn region_loop(entity: &EntityType) -> Option<Profile> {
|
||||
let EntityType::Region(region) = entity else { return None };
|
||||
if !region.acis_data.has_data() {
|
||||
return None;
|
||||
}
|
||||
let (plane, loops, _) = super::presspull_model::profile_geometry(entity)?;
|
||||
let [boundary] = loops.as_slice() else { return None };
|
||||
let single = boundary.len() == 1;
|
||||
let pieces = boundary
|
||||
.iter()
|
||||
.map(|piece| profile_pieces(piece, single))
|
||||
.collect::<Option<Vec<_>>>()?
|
||||
.concat();
|
||||
Some(Profile { plane, pieces })
|
||||
}
|
||||
|
||||
/// Whether a region has more than one boundary loop.
|
||||
fn region_with_holes(entity: &EntityType) -> bool {
|
||||
matches!(entity, EntityType::Region(_)) && region_profiles(entity).is_some_and(|(_, loops)| loops.len() > 1)
|
||||
}
|
||||
|
||||
fn profile_pieces(curve: &Curve, closed: bool) -> Option<Vec<Curve>> {
|
||||
let pieces = match curve {
|
||||
Curve::Polyline(_) => curve.segments(),
|
||||
@@ -164,10 +190,16 @@ fn planar_polygon_entity(entity: &EntityType) -> Option<PlanarCurve> {
|
||||
))
|
||||
}
|
||||
|
||||
/// A region's boundary loops: from its modeler edges when it has them (its
|
||||
/// display wires then hold one wire per edge), else from its wires.
|
||||
fn region_profiles(entity: &EntityType) -> Option<(Plane, Vec<Vec<Curve>>)> {
|
||||
let EntityType::Region(region) = entity else {
|
||||
return None;
|
||||
};
|
||||
if region.acis_data.has_data() {
|
||||
let (plane, loops, _) = super::presspull_model::profile_geometry(entity)?;
|
||||
return (!loops.is_empty()).then_some((plane, loops));
|
||||
}
|
||||
let plane = planar_polygon_entity(entity)?.plane;
|
||||
let mut profiles = Vec::with_capacity(region.wires.len());
|
||||
for wire in ®ion.wires {
|
||||
@@ -295,7 +327,7 @@ pub fn extruded_surface(
|
||||
direction: [f64; 3],
|
||||
taper_angle: f64,
|
||||
) -> Option<Body> {
|
||||
if matches!(entity, EntityType::Region(region) if region.wires.len() > 1) {
|
||||
if region_with_holes(entity) {
|
||||
return None;
|
||||
}
|
||||
let (profile, _) = extrusion_profile_of(entity)?;
|
||||
@@ -307,7 +339,7 @@ pub fn extruded_along_path(
|
||||
path: &EntityType,
|
||||
taper_angle: f64,
|
||||
) -> Option<Body> {
|
||||
if matches!(entity, EntityType::Region(region) if region.wires.len() > 1) {
|
||||
if region_with_holes(entity) {
|
||||
if taper_angle.abs() > 1e-12 {
|
||||
return None;
|
||||
}
|
||||
@@ -464,7 +496,7 @@ pub fn revolve_history(
|
||||
if !angle.is_finite()
|
||||
|| angle.abs() <= 1e-12
|
||||
|| !start_angle.is_finite()
|
||||
|| matches!(entity, EntityType::Region(region) if region.wires.len() > 1)
|
||||
|| region_with_holes(entity)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -627,10 +659,105 @@ pub fn embedded_path(entity: &EntityType) -> Option<EmbeddedEntity> {
|
||||
}
|
||||
}
|
||||
|
||||
fn embedded_planar_entity(entity: &EntityType) -> Option<(EmbeddedEntity, [f64; 16])> {
|
||||
if matches!(entity, EntityType::Region(region) if region.wires.len() > 1) {
|
||||
/// A region of one boundary loop as the curve the reference records for it
|
||||
/// as a profile: a circle (one edge, or arcs of one circle making a whole
|
||||
/// turn) or whole ellipse as itself, a loop of lines and arcs as a closed
|
||||
/// polyline in its own coordinate system about the region's normal, read
|
||||
/// from the region's modeler edges.
|
||||
pub fn region_boundary_entity(entity: &EntityType) -> Option<(EmbeddedEntity, [f64; 16])> {
|
||||
let EntityType::Region(region) = entity else { return None };
|
||||
if !region.acis_data.has_data() {
|
||||
return None;
|
||||
}
|
||||
let (plane, loops, _) = super::presspull_model::profile_geometry(entity)?;
|
||||
let [boundary] = loops.as_slice() else { return None };
|
||||
let world = |v: [f64; 3]| Vector3::new(v[0], v[1], v[2]);
|
||||
let normal = world(plane.normal()?);
|
||||
let identity = glam::DMat4::IDENTITY.to_cols_array();
|
||||
match boundary.as_slice() {
|
||||
[Curve::Circle(circle)] => {
|
||||
let mut value = codec::entities::Circle::new();
|
||||
value.center = world(plane.point_at(circle.centre));
|
||||
value.radius = circle.radius;
|
||||
value.normal = normal;
|
||||
return Some((EmbeddedEntity::Circle(value), identity));
|
||||
}
|
||||
[Curve::Arc(first), rest @ ..]
|
||||
if rest.iter().all(|piece| matches!(piece, Curve::Arc(arc)
|
||||
if (arc.centre[0] - first.centre[0]).hypot(arc.centre[1] - first.centre[1]) <= 1e-9 * (1.0 + first.radius)
|
||||
&& (arc.radius - first.radius).abs() <= 1e-9 * (1.0 + first.radius)))
|
||||
&& (boundary.iter().map(|piece| match piece { Curve::Arc(arc) => arc.sweep(), _ => 0.0 }).sum::<f64>()
|
||||
- std::f64::consts::TAU).abs() <= 1e-9 =>
|
||||
{
|
||||
let mut value = codec::entities::Circle::new();
|
||||
value.center = world(plane.point_at(first.centre));
|
||||
value.radius = first.radius;
|
||||
value.normal = normal;
|
||||
return Some((EmbeddedEntity::Circle(value), identity));
|
||||
}
|
||||
[Curve::Ellipse(arc)] if (arc.sweep() - std::f64::consts::TAU).abs() <= 1e-9 => {
|
||||
let ellipse = arc.ellipse;
|
||||
let centre = Vec3::from(plane.point_at(ellipse.centre));
|
||||
let tip = Vec3::from(plane.point_at([
|
||||
ellipse.centre[0] + ellipse.major_axis[0] * ellipse.major_radius,
|
||||
ellipse.centre[1] + ellipse.major_axis[1] * ellipse.major_radius,
|
||||
]));
|
||||
let mut value = codec::entities::Ellipse::new();
|
||||
value.center = world(centre.to_array());
|
||||
value.major_axis = world((tip - centre).to_array());
|
||||
value.minor_axis_ratio = ellipse.minor_radius / ellipse.major_radius;
|
||||
value.start_parameter = arc.start_parameter;
|
||||
value.end_parameter = arc.start_parameter + std::f64::consts::TAU;
|
||||
value.normal = normal;
|
||||
return Some((EmbeddedEntity::Ellipse(value), identity));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
// Walk the loop head to tail; an arc run against its direction turns
|
||||
// clockwise (negative bulge).
|
||||
let close = |a: [f64; 2], b: [f64; 2]| (a[0] - b[0]).hypot(a[1] - b[1]) <= 1e-9 * (1.0 + a[0].abs().max(a[1].abs()));
|
||||
let ends = |piece: &Curve| match piece {
|
||||
Curve::Line(line) => Some((line.start, line.end, 0.0)),
|
||||
Curve::Arc(arc) => {
|
||||
let at = |angle: f64| [arc.centre[0] + arc.radius * angle.cos(), arc.centre[1] + arc.radius * angle.sin()];
|
||||
Some((at(arc.start_angle), at(arc.end_angle), (arc.sweep() / 4.0).tan()))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let (first_start, first_end, _) = ends(boundary.first()?)?;
|
||||
let second = boundary.get(1).and_then(ends);
|
||||
let mut at = if second.is_some_and(|(start, end, _)| close(first_start, start) || close(first_start, end)) {
|
||||
first_end
|
||||
} else {
|
||||
first_start
|
||||
};
|
||||
// The polyline in its object coordinates: the arbitrary axes of the normal.
|
||||
let axis_normal = glam::DVec3::new(normal.x, normal.y, normal.z);
|
||||
let axis_x = super::sweep_command_model::arbitrary_x(axis_normal);
|
||||
let axis_y = axis_normal.cross(axis_x);
|
||||
let mut polyline = LwPolyline::new();
|
||||
polyline.normal = normal;
|
||||
polyline.elevation = glam::DVec3::from_array(plane.origin).dot(axis_normal);
|
||||
for piece in boundary {
|
||||
let (start, end, bulge) = ends(piece)?;
|
||||
let (from, to, bulge) = if close(start, at) { (start, end, bulge) } else if close(end, at) { (end, start, -bulge) } else { return None };
|
||||
let point = glam::DVec3::from_array(plane.point_at(from));
|
||||
let mut vertex = LwVertex::new(Vector2::new(point.dot(axis_x), point.dot(axis_y)));
|
||||
vertex.bulge = bulge;
|
||||
polyline.vertices.push(vertex);
|
||||
at = to;
|
||||
}
|
||||
polyline.is_closed = true;
|
||||
Some((EmbeddedEntity::LwPolyline(polyline), identity))
|
||||
}
|
||||
|
||||
fn embedded_planar_entity(entity: &EntityType) -> Option<(EmbeddedEntity, [f64; 16])> {
|
||||
if region_with_holes(entity) {
|
||||
return None;
|
||||
}
|
||||
if let Some(found) = region_boundary_entity(entity) {
|
||||
return Some(found);
|
||||
}
|
||||
if !matches!(entity, EntityType::Polyline3D(_)) {
|
||||
if let Some(embedded) = embedded_path(entity) {
|
||||
return Some((embedded, glam::DMat4::IDENTITY.to_cols_array()));
|
||||
|
||||
+179
-36
@@ -1268,6 +1268,98 @@ impl Scene {
|
||||
rebuilt
|
||||
}
|
||||
|
||||
/// Associative extruded, revolved and lofted surfaces follow edits of
|
||||
/// their sources: made again from them with the values they were made
|
||||
/// with. A surface edited along with its sources keeps its own edit.
|
||||
/// Returns the surfaces rebuilt.
|
||||
pub(crate) fn refresh_associative_surfaces(&mut self, changes: &[(Handle, crate::scene::ChangeKind)]) -> Vec<(Handle, crate::scene::ChangeKind)> {
|
||||
use crate::scene::model::surface_sources::{axis_points, surface_sources, SurfaceSources, SURFACE_SOURCES_APP};
|
||||
use crate::scene::model::sweep_model;
|
||||
use codec::objects::SolidHistoryOperation;
|
||||
let changed = changes.iter()
|
||||
.filter(|(_, kind)| !matches!(kind, crate::scene::ChangeKind::Removed))
|
||||
.map(|(handle, _)| *handle)
|
||||
.collect::<rustc_hash::FxHashSet<_>>();
|
||||
if changed.is_empty() || !self.document.app_ids.contains(SURFACE_SOURCES_APP) {
|
||||
return Vec::new();
|
||||
}
|
||||
let linked = self.document.entities()
|
||||
.filter(|entity| matches!(entity, EntityType::Surface(_)))
|
||||
.filter_map(|entity| {
|
||||
let handle = entity.common().handle;
|
||||
let sources = surface_sources(entity)?;
|
||||
(!changed.contains(&handle) && sources.handles().iter().any(|source| changed.contains(source)))
|
||||
.then_some((handle, sources))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut rebuilt = Vec::new();
|
||||
for (handle, sources) in linked {
|
||||
let source = |handle: Handle| self.document.get_entity(handle).cloned();
|
||||
// The surface made again: a history step, or a body with its data.
|
||||
let made: Option<Result<SolidHistoryOperation, (kernel::brep::Body, codec::entities::SurfaceData)>> = match &sources {
|
||||
SurfaceSources::Extrude { profile, height, direction, taper } => (|| {
|
||||
let entity = source(*profile)?;
|
||||
let (shape, _) = sweep_model::extrusion_profile_of(&entity)?;
|
||||
let direction = match direction {
|
||||
Some(direction) => *direction,
|
||||
None => glam::DVec3::from_array(shape.plane.normal()?) * *height,
|
||||
};
|
||||
if self.document.solid_history_operations(handle).is_some() {
|
||||
return sweep_model::extrusion_history(&entity, None, direction.to_array(), *taper, shape.plane.origin).map(Ok);
|
||||
}
|
||||
let body = sweep_model::extruded_surface(&entity, direction.to_array(), *taper)?;
|
||||
let EntityType::Surface(fresh) = crate::modules::insert::solid3d_cmds::empty_extruded_surface(direction, *taper) else { return None };
|
||||
Some(Err((body, fresh.surface_data)))
|
||||
})(),
|
||||
SurfaceSources::Revolve { profile, axis, start, end, angle, start_angle } => (|| {
|
||||
let entity = source(*profile)?;
|
||||
let (start, end) = axis
|
||||
.and_then(|axis| self.document.get_entity(axis).and_then(axis_points))
|
||||
.unwrap_or((*start, *end));
|
||||
let body = sweep_model::revolved_surface(&entity, start.to_array(), end.to_array(), *angle, *start_angle)?;
|
||||
let EntityType::Surface(fresh) = crate::modules::insert::solid3d_cmds::empty_revolved_surface(&entity, start, end, *angle, *start_angle) else { return None };
|
||||
Some(Err((body, fresh.surface_data)))
|
||||
})(),
|
||||
SurfaceSources::Loft { sections } => (|| {
|
||||
let operations = self.document.solid_history_operations(handle)?;
|
||||
let mut record = operations.into_iter().find_map(|operation| match operation {
|
||||
SolidHistoryOperation::Loft(record) => Some(record),
|
||||
_ => None,
|
||||
})?;
|
||||
if record.cross_sections.len() != sections.len() {
|
||||
return None;
|
||||
}
|
||||
record.cross_sections = sections.iter()
|
||||
.map(|section| source(*section).as_ref().and_then(crate::scene::model::loft_command_model::embedded_section))
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
Some(Ok(SolidHistoryOperation::Loft(record)))
|
||||
})(),
|
||||
};
|
||||
let before = self.document.get_entity(handle).cloned().map(std::sync::Arc::new);
|
||||
match made {
|
||||
Some(Ok(operation)) => {
|
||||
self.record_undo_before(handle, before);
|
||||
if self.rebuild_solid_history(handle, operation) {
|
||||
rebuilt.push((handle, crate::scene::ChangeKind::Modified));
|
||||
}
|
||||
}
|
||||
Some(Err((body, data))) => {
|
||||
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 };
|
||||
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());
|
||||
surface.surface_data = data;
|
||||
}
|
||||
self.register_prepared_solid_model(handle, body, display);
|
||||
rebuilt.push((handle, crate::scene::ChangeKind::Modified));
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
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.
|
||||
@@ -1279,23 +1371,57 @@ impl Scene {
|
||||
};
|
||||
let mut links = Vec::new();
|
||||
let mut sources = Vec::new();
|
||||
let mut others = Vec::new();
|
||||
for item in self.document.objects.values() {
|
||||
let ObjectType::Associative(action) = item else {
|
||||
continue;
|
||||
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 };
|
||||
let Some(AssociativeData::Dependency(written)) = object(body.surface_body.dependency) else { continue };
|
||||
// The entities a named path parameter reads (an edge of a region
|
||||
// reads through a geometry dependency).
|
||||
let read = |name: &str| -> Vec<Handle> {
|
||||
action.owned_parameters.iter().filter_map(|parameter| {
|
||||
let Some(AssociativeData::PathActionParam(path)) = object(*parameter) else { return None };
|
||||
if path.compound.action_param.name != name { return None; }
|
||||
let Some(AssociativeData::EdgeActionParam(edge)) = object(*path.compound.parameters.first()?) else { return None };
|
||||
match object(edge.single_dependency.dependency) {
|
||||
Some(AssociativeData::Dependency(dependency)) => Some(dependency.dependent_on),
|
||||
Some(AssociativeData::GeomDependency(dependency)) => Some(dependency.dependency.dependent_on),
|
||||
_ => None,
|
||||
}
|
||||
}).collect()
|
||||
};
|
||||
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 real = |name: &str| action.values.iter().find(|value| value.name == name)
|
||||
.and_then(|value| value.variables.first())
|
||||
.and_then(|variable| match variable.value.value {
|
||||
codec::objects::AssocEvalValue::Real(value) => Some(value),
|
||||
_ => None,
|
||||
});
|
||||
match body.kind {
|
||||
AssocSurfaceActionKind::Extruded => {
|
||||
if let (Some(profile), Some(height), Some(taper)) =
|
||||
(read("ExtrudeProfile").first().copied(), real("ExtrusionHeight"), real("ExtrusionTaperAngle"))
|
||||
{
|
||||
others.push((written.dependent_on, (body.kind, vec![profile], None, height, taper)));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
AssocSurfaceActionKind::Revolved => {
|
||||
if let (Some(profile), Some(angle)) = (read("RevolveProfile").first().copied(), real("RevolveAngle")) {
|
||||
others.push((written.dependent_on, (body.kind, vec![profile], read("RevolvePath").first().copied(), angle, 0.0)));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
AssocSurfaceActionKind::Lofted => {
|
||||
let sections = read("CrossSection");
|
||||
if sections.len() >= 2 {
|
||||
others.push((written.dependent_on, (body.kind, sections, None, 0.0, 0.0)));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
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() {
|
||||
@@ -1325,28 +1451,7 @@ impl Scene {
|
||||
links.push((written.dependent_on, expressions));
|
||||
}
|
||||
// The profile and path the action reads.
|
||||
let source = |name: &str| {
|
||||
action.owned_parameters.iter().find_map(|parameter| {
|
||||
let Some(AssociativeData::PathActionParam(path)) = object(*parameter) else {
|
||||
return None;
|
||||
};
|
||||
if path.compound.action_param.name != name {
|
||||
return None;
|
||||
}
|
||||
let Some(AssociativeData::EdgeActionParam(edge)) =
|
||||
object(*path.compound.parameters.first()?)
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
match object(edge.single_dependency.dependency) {
|
||||
Some(AssociativeData::Dependency(dependency)) => {
|
||||
Some(dependency.dependent_on)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
};
|
||||
if let (Some(profile), Some(path)) = (source("SweepProfile"), source("SweepPath")) {
|
||||
if let (Some(profile), Some(path)) = (read("SweepProfile").first().copied(), read("SweepPath").first().copied()) {
|
||||
sources.push((written.dependent_on, profile, path));
|
||||
}
|
||||
}
|
||||
@@ -1387,6 +1492,44 @@ impl Scene {
|
||||
&expressions,
|
||||
);
|
||||
}
|
||||
// Associative extruded, revolved and lofted surfaces follow their
|
||||
// sources here too.
|
||||
use crate::scene::model::surface_sources::{link_surface_sources, surface_sources, SurfaceSources};
|
||||
for (surface, (kind, handles, axis, amount, taper)) in others {
|
||||
let Some(entity @ EntityType::Surface(value)) = self.document.get_entity(surface) else { continue };
|
||||
if surface_sources(entity).is_some() { continue; }
|
||||
let linked = match (kind, &value.surface_data) {
|
||||
(AssocSurfaceActionKind::Extruded, codec::entities::SurfaceData::Extruded { sweep_vector, .. }) => {
|
||||
let profile = handles[0];
|
||||
let along = glam::DVec3::new(sweep_vector.x, sweep_vector.y, sweep_vector.z);
|
||||
// A direction other than the profile's normal is kept.
|
||||
let normal = self.document.get_entity(profile)
|
||||
.and_then(|entity| crate::scene::model::sweep_model::extrusion_profile_of(entity))
|
||||
.and_then(|(shape, _)| shape.plane.normal())
|
||||
.map(glam::DVec3::from_array);
|
||||
let direction = match normal {
|
||||
Some(normal) if (normal * amount - along).length() <= 1e-9 * amount.abs().max(1.0) => None,
|
||||
_ if along.length() > 1e-12 => Some(along),
|
||||
_ => None,
|
||||
};
|
||||
SurfaceSources::Extrude { profile, height: amount, direction, taper }
|
||||
}
|
||||
(AssocSurfaceActionKind::Revolved, codec::entities::SurfaceData::Revolved { axis_point, axis_vector, start_angle, .. }) => {
|
||||
let start = glam::DVec3::new(axis_point.x, axis_point.y, axis_point.z);
|
||||
SurfaceSources::Revolve {
|
||||
profile: handles[0],
|
||||
axis,
|
||||
start,
|
||||
end: start + glam::DVec3::new(axis_vector.x, axis_vector.y, axis_vector.z),
|
||||
angle: amount,
|
||||
start_angle: *start_angle,
|
||||
}
|
||||
}
|
||||
(AssocSurfaceActionKind::Lofted, _) => SurfaceSources::Loft { sections: handles },
|
||||
_ => continue,
|
||||
};
|
||||
link_surface_sources(&mut self.document, surface, &linked);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rebuild_solid_history(
|
||||
|
||||
Reference in new issue
Block a user