Standardize parametric constraints and interoperability

This commit is contained in:
Hakan Seven committed 2026-09-15 03:19:52 +03:00
1 parent 753df67ee4
commit d738ebfcc6
41 files changed
+10038 -7847

No files matched your search

+129 -127
View File
@@ -1,13 +1,13 @@
use acadrust::entities::Dimension;
use acadrust::objects::{
AssocDimensionAssociation, AssocDimensionReference, AssociativeData,
AssociativeObject, ObjectType,
AssocDimensionAssociation, AssocDimensionReference, AssociativeData, AssociativeObject,
ObjectType,
};
use acadrust::types::{Handle, Vector3};
use acadrust::EntityType;
use cadkernel::geom2d::{
angle_within_arc, arc as tessellate_arc, arc_span, closest_point, Arc as KernelArc,
BulgeArc, Circle as KernelCircle, Curve as KernelCurve, DEFAULT_SEGMENTS_PER_RADIAN,
angle_within_arc, arc as tessellate_arc, arc_span, closest_point, Arc as KernelArc, BulgeArc,
Circle as KernelCircle, Curve as KernelCurve, DEFAULT_SEGMENTS_PER_RADIAN,
};
use cadkernel::space::Plane;
use std::f64::consts::TAU;
@@ -25,8 +25,7 @@ pub(crate) fn polyline_arc_point_marker(segment: i32) -> i32 {
}
fn polyline_arc_segment_from_point_marker(marker: i32) -> Option<i32> {
(marker <= POLYLINE_ARC_POINT_MARKER_BASE)
.then_some(POLYLINE_ARC_POINT_MARKER_BASE - marker)
(marker <= POLYLINE_ARC_POINT_MARKER_BASE).then_some(POLYLINE_ARC_POINT_MARKER_BASE - marker)
}
#[derive(Clone, Copy, Debug)]
@@ -158,11 +157,13 @@ pub(crate) fn radial_source_at(
entity: &EntityType,
point: Vector3,
) -> Option<RadialSourceGeometry> {
radial_candidates(entity).into_iter().min_by(|first, second| {
first
.distance_squared_to(dpoint(point))
.total_cmp(&second.distance_squared_to(dpoint(point)))
})
radial_candidates(entity)
.into_iter()
.min_by(|first, second| {
first
.distance_squared_to(dpoint(point))
.total_cmp(&second.distance_squared_to(dpoint(point)))
})
}
fn radial_source_for_marker(entity: &EntityType, marker: i32) -> Option<RadialSourceGeometry> {
@@ -177,14 +178,16 @@ fn radial_source_matching(
radius: f64,
chord: Vector3,
) -> Option<RadialSourceGeometry> {
radial_candidates(entity).into_iter().min_by(|first, second| {
let score = |candidate: &RadialSourceGeometry| {
point_distance_squared(candidate.center_world(), center)
+ (candidate.radius - radius).powi(2)
+ candidate.distance_squared_to(dpoint(chord)) * 1e-6
};
score(first).total_cmp(&score(second))
})
radial_candidates(entity)
.into_iter()
.min_by(|first, second| {
let score = |candidate: &RadialSourceGeometry| {
point_distance_squared(candidate.center_world(), center)
+ (candidate.radius - radius).powi(2)
+ candidate.distance_squared_to(dpoint(chord)) * 1e-6
};
score(first).total_cmp(&score(second))
})
}
fn point_distance_squared(first: Vector3, second: Vector3) -> f64 {
@@ -235,9 +238,10 @@ fn polyline_arc_center(entity: &EntityType, segment: usize) -> Option<Vector3> {
EntityType::LwPolyline(polyline) => {
let count = polyline.vertices.len();
let first = *polyline.vertices.get(segment)?;
let second = *polyline
.vertices
.get(next_segment_index(count, polyline.is_closed, segment)?)?;
let second =
*polyline
.vertices
.get(next_segment_index(count, polyline.is_closed, segment)?)?;
bulge_center_world(
[first.location.x, first.location.y],
[second.location.x, second.location.y],
@@ -249,9 +253,10 @@ fn polyline_arc_center(entity: &EntityType, segment: usize) -> Option<Vector3> {
EntityType::Polyline2D(polyline) => {
let count = polyline.vertices.len();
let first = polyline.vertices.get(segment)?;
let second = polyline
.vertices
.get(next_segment_index(count, polyline.is_closed(), segment)?)?;
let second =
polyline
.vertices
.get(next_segment_index(count, polyline.is_closed(), segment)?)?;
bulge_center_world(
[first.location.x, first.location.y],
[second.location.x, second.location.y],
@@ -267,20 +272,24 @@ fn polyline_arc_center(entity: &EntityType, segment: usize) -> Option<Vector3> {
/// Ordered, named points for an entity (line start/end, polyline vertices,
/// ...), indexed by the same non-negative GsMarker convention
/// `AssocDimensionReference::main_gs_marker` uses. Promoted to
/// `pub(crate)` so `sketch::constraint_set` can address constraint
/// `pub(crate)` so the parametric solver can address constraint
/// endpoints the same way associative dimensions already address theirs,
/// rather than inventing a second sub-element scheme.
pub(crate) fn source_points(entity: &EntityType) -> Vec<Vector3> {
match entity {
EntityType::Line(line) => vec![line.start, line.end],
EntityType::Ray(ray) => vec![ray.base_point],
EntityType::Arc(arc) => vec![arc.start_point_wcs(), arc.end_point_wcs()],
EntityType::Circle(_) => Vec::new(),
EntityType::Spline(spline) => crate::entities::spline::nurbs3(spline)
.map(|curve| {
let (start, end) = curve.domain();
[curve.point_at_knot(start), curve.point_at_knot(end)].into_iter()
.map(|point| Vector3::new(point[0], point[1], point[2])).collect()
}).unwrap_or_default(),
[curve.point_at_knot(start), curve.point_at_knot(end)]
.into_iter()
.map(|point| Vector3::new(point[0], point[1], point[2]))
.collect()
})
.unwrap_or_default(),
EntityType::LwPolyline(polyline) => polyline
.vertices
.iter()
@@ -345,8 +354,7 @@ fn source_marker(entity: &EntityType, point: Vector3) -> Option<i32> {
.into_iter()
.enumerate()
.min_by(|(_, first), (_, second)| {
point_distance_squared(*first, point)
.total_cmp(&point_distance_squared(*second, point))
point_distance_squared(*first, point).total_cmp(&point_distance_squared(*second, point))
})
.map(|(index, _)| index as i32)
}
@@ -360,9 +368,7 @@ fn resolve_reference(scene: &Scene, reference: &AssocDimensionReference) -> Opti
let angle = radial.start_angle + sweep * reference.osnap_distance.clamp(0.0, 1.0);
return Some(radial.point_at_angle(angle));
}
if let Some(segment) =
polyline_arc_segment_from_point_marker(reference.main_gs_marker)
{
if let Some(segment) = polyline_arc_segment_from_point_marker(reference.main_gs_marker) {
let radial = radial_source_for_marker(entity, segment)?;
let sweep = positive_sweep(radial.start_angle, radial.end_angle);
let angle = radial.start_angle + sweep * reference.osnap_distance.clamp(0.0, 1.0);
@@ -381,9 +387,7 @@ fn resolve_reference(scene: &Scene, reference: &AssocDimensionReference) -> Opti
}
if reference.main_gs_marker == -2 {
return match entity {
EntityType::Circle(circle) => {
Some(circle.point_at_angle_wcs(reference.osnap_distance))
}
EntityType::Circle(circle) => Some(circle.point_at_angle_wcs(reference.osnap_distance)),
EntityType::Arc(arc) => Some(arc.point_at_angle_wcs(reference.osnap_distance)),
_ => None,
};
@@ -399,14 +403,17 @@ fn resolve_reference(scene: &Scene, reference: &AssocDimensionReference) -> Opti
);
let curve = circle_curve(circle);
let stored_parameter = curve.parameter_at([stored.0, stored.1]) * TAU;
let parameter = if reference.osnap_distance.abs() > 1e-12
|| stored_parameter.abs() <= 1e-12
let parameter = if reference.osnap_distance.abs() > 1e-12 || stored_parameter.abs() <= 1e-12
{
reference.osnap_distance
} else {
stored_parameter
};
let parameter = if parameter.is_finite() { parameter } else { 0.0 };
let parameter = if parameter.is_finite() {
parameter
} else {
0.0
};
let point = curve.point_at(parameter / TAU);
return Some(ocs_point(
point[0],
@@ -494,27 +501,13 @@ fn signed_angle_delta(value: f64) -> f64 {
(value + std::f64::consts::PI).rem_euclid(TAU) - std::f64::consts::PI
}
fn angle_about_plane(
plane: Plane,
center: Vector3,
point: Vector3,
) -> f64 {
let delta = [
point.x - center.x,
point.y - center.y,
point.z - center.z,
];
let dot = |axis: [f64; 3]| {
delta[0] * axis[0] + delta[1] * axis[1] + delta[2] * axis[2]
};
fn angle_about_plane(plane: Plane, center: Vector3, point: Vector3) -> f64 {
let delta = [point.x - center.x, point.y - center.y, point.z - center.z];
let dot = |axis: [f64; 3]| delta[0] * axis[0] + delta[1] * axis[1] + delta[2] * axis[2];
dot(plane.y_axis).atan2(dot(plane.x_axis))
}
fn point_on_radial_circle(
radial: RadialSourceGeometry,
radius: f64,
angle: f64,
) -> Vector3 {
fn point_on_radial_circle(radial: RadialSourceGeometry, radius: f64, angle: f64) -> Vector3 {
vector3(radial.plane.point_at([
radial.center[0] + radius * angle.cos(),
radial.center[1] + radius * angle.sin(),
@@ -522,9 +515,7 @@ fn point_on_radial_circle(
}
fn plane_from_normal(origin: Vector3, normal: Vector3) -> Plane {
let (x_axis, y_axis) = crate::scene::view::transform::ocs_axes((
normal.x, normal.y, normal.z,
));
let (x_axis, y_axis) = crate::scene::view::transform::ocs_axes((normal.x, normal.y, normal.z));
Plane::from_axes(
[origin.x, origin.y, origin.z],
[x_axis.0, x_axis.1, x_axis.2],
@@ -574,35 +565,52 @@ pub(crate) fn constraint_from_associative_dimension(
document: &acadrust::CadDocument,
handle: Handle,
) -> Option<(
super::sketch_constraints::ConstraintKind,
Vec<super::sketch_constraints::SketchRef>,
super::parametric_constraints::ConstraintKind,
Vec<super::parametric_constraints::ParametricRef>,
super::named_parameters::DrivingValue,
)> {
use super::named_parameters::DrivingValue;
use super::sketch_constraints::{ConstraintKind, SketchRef};
use super::parametric_constraints::{ConstraintKind, ParametricRef};
let EntityType::Dimension(dimension) = document.get_entity(handle)? else { return None };
let EntityType::Dimension(dimension) = document.get_entity(handle)? else {
return None;
};
let association = document.objects.values().find_map(|object| {
let ObjectType::Associative(object) = object else { return None };
let AssociativeData::DimensionAssociation(association) = &object.data else { return None };
let ObjectType::Associative(object) = object else {
return None;
};
let AssociativeData::DimensionAssociation(association) = &object.data else {
return None;
};
(association.dimension == handle && association.associativity != 0).then_some(association)
})?;
let point = |index: usize| {
let reference = association.references.get(index)?.first()?;
Some(SketchRef::point(*reference.xrefs.first()?, reference.main_gs_marker))
Some(ParametricRef::point(
*reference.xrefs.first()?,
reference.main_gs_marker,
))
};
let whole = |index: usize| {
let reference = association.references.get(index)?.first()?;
Some(SketchRef::whole(*reference.xrefs.first()?))
Some(ParametricRef::whole(*reference.xrefs.first()?))
};
let measurement = DrivingValue::Literal(dimension.measurement());
match dimension {
Dimension::Aligned(_) => Some((ConstraintKind::Distance, vec![point(0)?, point(1)?], measurement)),
Dimension::Aligned(_) => Some((
ConstraintKind::Distance,
vec![point(0)?, point(1)?],
measurement,
)),
Dimension::Linear(linear) => {
let direction = [linear.rotation.cos().abs(), linear.rotation.sin().abs()];
let kind = if direction[1] <= 1e-9 { ConstraintKind::DistanceX }
else if direction[0] <= 1e-9 { ConstraintKind::DistanceY }
else { return None };
let kind = if direction[1] <= 1e-9 {
ConstraintKind::DistanceX
} else if direction[0] <= 1e-9 {
ConstraintKind::DistanceY
} else {
return None;
};
Some((kind, vec![point(0)?, point(1)?], measurement))
}
Dimension::Radius(_) | Dimension::LargeRadial(_) => {
@@ -610,16 +618,24 @@ pub(crate) fn constraint_from_associative_dimension(
}
Dimension::Diameter(_) => Some((ConstraintKind::Diameter, vec![whole(0)?], measurement)),
Dimension::Angular2Ln(_) => {
let mut sources: Vec<Handle> = association.references.iter().flatten()
.filter_map(|reference| reference.xrefs.first().copied()).collect();
let mut sources: Vec<Handle> = association
.references
.iter()
.flatten()
.filter_map(|reference| reference.xrefs.first().copied())
.collect();
sources.sort_unstable();
sources.dedup();
let [first, second] = sources.as_slice() else { return None };
Some((ConstraintKind::Angle,
vec![SketchRef::whole(*first), SketchRef::whole(*second)], measurement))
let [first, second] = sources.as_slice() else {
return None;
};
Some((
ConstraintKind::Angle,
vec![ParametricRef::whole(*first), ParametricRef::whole(*second)],
measurement,
))
}
Dimension::Arc(_) => Some((ConstraintKind::ArcLength, vec![whole(0)?], measurement)),
Dimension::Angular3Pt(_) | Dimension::Ordinate(_) => None,
Dimension::Arc(_) | Dimension::Angular3Pt(_) | Dimension::Ordinate(_) => None,
}
}
@@ -716,8 +732,7 @@ impl Scene {
dimension: Handle,
sources: Vec<Option<DimensionAssociationSource>>,
) {
let Some(EntityType::Dimension(dimension_entity)) =
self.document.get_entity(dimension)
let Some(EntityType::Dimension(dimension_entity)) = self.document.get_entity(dimension)
else {
return;
};
@@ -803,22 +818,20 @@ impl Scene {
return;
}
let reference = |source: Handle,
marker: i32,
parameter: f64,
osnap_type: u8,
point: Vector3| AssocDimensionReference {
class_name: "AcDbOsnapPointRef".to_string(),
osnap_type,
xrefs: vec![source],
main_subent_type: 1,
main_gs_marker: marker,
osnap_distance: parameter,
osnap_point: point,
..AssocDimensionReference::default()
};
let mut references: [Vec<AssocDimensionReference>; 4] =
std::array::from_fn(|_| Vec::new());
let reference =
|source: Handle, marker: i32, parameter: f64, osnap_type: u8, point: Vector3| {
AssocDimensionReference {
class_name: "AcDbOsnapPointRef".to_string(),
osnap_type,
xrefs: vec![source],
main_subent_type: 1,
main_gs_marker: marker,
osnap_distance: parameter,
osnap_point: point,
..AssocDimensionReference::default()
}
};
let mut references: [Vec<AssocDimensionReference>; 4] = std::array::from_fn(|_| Vec::new());
let mut associativity = 0;
for (index, resolved) in resolved.into_iter().enumerate() {
if index >= references.len() {
@@ -873,12 +886,8 @@ impl Scene {
}
}
pub(crate) fn infer_dimension_sources(
&self,
dimension: Handle,
) -> Vec<Option<Handle>> {
let Some(EntityType::Dimension(entity)) = self.document.get_entity(dimension)
else {
pub(crate) fn infer_dimension_sources(&self, dimension: Handle) -> Vec<Option<Handle>> {
let Some(EntityType::Dimension(entity)) = self.document.get_entity(dimension) else {
return Vec::new();
};
let radial_data = match entity {
@@ -906,18 +915,11 @@ impl Scene {
.entities()
.filter(|candidate| candidate.common().handle != dimension)
.filter_map(|candidate| {
let radial = radial_source_matching(
candidate,
center,
radius,
chord,
)?;
let radial = radial_source_matching(candidate, center, radius, chord)?;
let center_error = point_distance_squared(radial.center_world(), center);
let radius_error = (radial.radius - radius).powi(2);
(center_error + radius_error <= tolerance * tolerance).then_some((
center_error + radius_error,
candidate.common().handle,
))
(center_error + radius_error <= tolerance * tolerance)
.then_some((center_error + radius_error, candidate.common().handle))
})
.min_by(|first, second| first.0.total_cmp(&second.0))
.map(|(_, handle)| handle);
@@ -946,8 +948,7 @@ impl Scene {
&mut self,
changes: &[(Handle, ChangeKind)],
) -> Vec<(Handle, ChangeKind)> {
let changed: rustc_hash::FxHashSet<_> =
changes.iter().map(|(handle, _)| *handle).collect();
let changed: rustc_hash::FxHashSet<_> = changes.iter().map(|(handle, _)| *handle).collect();
if changed.is_empty() {
return Vec::new();
}
@@ -966,7 +967,12 @@ impl Scene {
.references
.iter()
.flatten()
.any(|reference| reference.xrefs.iter().any(|handle| changed.contains(handle)))
.any(|reference| {
reference
.xrefs
.iter()
.any(|handle| changed.contains(handle))
})
.then_some(association.clone())
})
.collect();
@@ -989,9 +995,7 @@ impl Scene {
let entity = self.document.get_entity(source)?;
let segment = match reference.main_gs_marker {
-3 => 0,
POLYLINE_ARC_CENTER_MARKER => {
reference.osnap_distance.round().max(0.0) as i32
}
POLYLINE_ARC_CENTER_MARKER => reference.osnap_distance.round().max(0.0) as i32,
_ => return None,
};
radial_source_for_marker(entity, segment)
@@ -1144,10 +1148,7 @@ impl Scene {
let old_dim_radius = old_center.distance(&old_definition);
let radial_offset = old_dim_radius - old_source_radius;
let old_mid = arc.arc_start_parameter
+ positive_sweep(
arc.arc_start_parameter,
arc.arc_end_parameter,
) * 0.5;
+ positive_sweep(arc.arc_start_parameter, arc.arc_end_parameter) * 0.5;
let old_definition_angle =
angle_about_plane(old_plane, old_center, old_definition);
let definition_angle_offset =
@@ -1208,8 +1209,7 @@ impl Scene {
arc.base.insertion_point = definition;
}
if arc.has_leader {
arc.first_leader_point =
point_on_radial_circle(radial, dim_radius, middle);
arc.first_leader_point = point_on_radial_circle(radial, dim_radius, middle);
arc.second_leader_point = second_leader;
}
arc.base.actual_measurement = arc.measurement();
@@ -1228,7 +1228,9 @@ impl Scene {
fn linear_measurement(linear: &acadrust::entities::DimensionLinear) -> f64 {
let plane = plane_from_normal(linear.first_point, linear.base.normal);
let first = plane.project(dpoint(linear.first_point)).unwrap_or([0.0; 2]);
let first = plane
.project(dpoint(linear.first_point))
.unwrap_or([0.0; 2]);
let second = plane.project(dpoint(linear.second_point)).unwrap_or(first);
let axis = [linear.rotation.cos(), linear.rotation.sin()];
((second[0] - first[0]) * axis[0] + (second[1] - first[1]) * axis[1]).abs()
File diff suppressed because it is too large. Load diff
+202 -227
View File
@@ -146,8 +146,7 @@ impl Scene {
fn prepare_section_objects(&mut self, entity: &mut EntityType) -> bool {
use acadrust::entities::ExtendedEntityData;
use acadrust::objects::{
ClassObject, ClassObjectData, SectionManager, SectionSettings,
SectionTypeSettings,
ClassObject, ClassObjectData, SectionManager, SectionSettings, SectionTypeSettings,
};
let EntityType::Extended(extended) = entity else {
@@ -169,16 +168,15 @@ impl Scene {
);
if !valid_settings {
let settings_handle = self.document.allocate_handle();
let mut settings = ClassObject::new(ClassObjectData::SectionSettings(
SectionSettings {
let mut settings =
ClassObject::new(ClassObjectData::SectionSettings(SectionSettings {
current_type: 4,
types: vec![SectionTypeSettings {
section_type: 4,
generation: 17,
..SectionTypeSettings::default()
}],
},
));
}));
settings.handle = settings_handle;
settings.owner = entity_handle;
self.document
@@ -223,9 +221,9 @@ impl Scene {
if let Some(ObjectType::Dictionary(dictionary)) =
self.document.objects.get_mut(&root)
{
dictionary.entries.retain(|(name, _)| {
!name.eq_ignore_ascii_case("ACAD_SECTION_MANAGER")
});
dictionary
.entries
.retain(|(name, _)| !name.eq_ignore_ascii_case("ACAD_SECTION_MANAGER"));
dictionary.add_entry("ACAD_SECTION_MANAGER", handle);
}
handle
@@ -256,10 +254,7 @@ impl Scene {
let mut needs_geometry_bump = false;
for entity in entities {
let affects_blocks = matches!(
&entity,
EntityType::Block(_) | EntityType::BlockEnd(_)
);
let affects_blocks = matches!(&entity, EntityType::Block(_) | EntityType::BlockEnd(_));
let handle = self.add_entity_internal(entity, false);
handles.push(handle);
if handle.is_null() {
@@ -285,10 +280,7 @@ impl Scene {
// Only block sentinels mutate a block definition and require rebuilding
// the block cache. A top-level INSERT merely references an existing
// definition, so adding it can patch just that new render handle.
let affects_blocks = matches!(
&entity,
EntityType::Block(_) | EntityType::BlockEnd(_)
);
let affects_blocks = matches!(&entity, EntityType::Block(_) | EntityType::BlockEnd(_));
// INSERT invalidates rendered block instances, but it does not mutate
// the referenced block definition. Only block sentinels require a
// structure image for undo; ordinary owner membership is intrinsic add
@@ -363,8 +355,9 @@ impl Scene {
// Delta-undo poison inputs (captured before the mutations below): an
// add that also creates a new layer, adds a block, or inserts an image
// definition mutates non-entity state a pure-entity delta can't undo.
let creates_layer =
self.is_recording_undo() && !layer.trim().is_empty() && !self.document.layers.contains(&layer);
let creates_layer = self.is_recording_undo()
&& !layer.trim().is_empty()
&& !self.document.layers.contains(&layer);
self.ensure_layer(&layer);
let app_ids: Vec<String> = entity
.common()
@@ -374,9 +367,9 @@ impl Scene {
.map(|record| record.application_name.clone())
.collect();
let creates_app_id = self.is_recording_undo()
&& app_ids.iter().any(|name| {
!name.trim().is_empty() && !self.document.app_ids.contains(name)
});
&& app_ids
.iter()
.any(|name| !name.trim().is_empty() && !self.document.app_ids.contains(name));
for name in &app_ids {
self.ensure_app_id(name);
}
@@ -412,14 +405,13 @@ impl Scene {
if let Some(mut model) = mesh_seed {
if let Some(entity) = self.document.get_entity(handle) {
let color = self.render_style(entity).0;
let material =
crate::scene::model::material_model::resolve_material_with_base(
&self.document,
entity,
color,
None,
self.material_base_dir.as_deref(),
);
let material = crate::scene::model::material_model::resolve_material_with_base(
&self.document,
entity,
color,
None,
self.material_base_dir.as_deref(),
);
material.apply_to_with_face_overrides(
&mut model,
&self.document,
@@ -539,13 +531,8 @@ impl Scene {
// Replacing (or becoming) a block sentinel forces a full block-cache
// rebuild. INSERT edits (including retargeting to another existing
// definition) only change that top-level render handle.
let affects_blocks = matches!(
existing,
EntityType::Block(_) | EntityType::BlockEnd(_)
) || matches!(
&entity,
EntityType::Block(_) | EntityType::BlockEnd(_)
);
let affects_blocks = matches!(existing, EntityType::Block(_) | EntityType::BlockEnd(_))
|| matches!(&entity, EntityType::Block(_) | EntityType::BlockEnd(_));
// A plugin edit may retarget the entity to a novel layer; register it
// so the edited entity keeps that layer on save instead of collapsing
@@ -627,14 +614,13 @@ impl Scene {
if let Some(mut model) = mesh_seed {
if let Some(entity) = self.document.get_entity(handle) {
let color = self.render_style(entity).0;
let material =
crate::scene::model::material_model::resolve_material_with_base(
&self.document,
entity,
color,
None,
self.material_base_dir.as_deref(),
);
let material = crate::scene::model::material_model::resolve_material_with_base(
&self.document,
entity,
color,
None,
self.material_base_dir.as_deref(),
);
material.apply_to_with_face_overrides(
&mut model,
&self.document,
@@ -770,15 +756,14 @@ impl Scene {
_ => None,
})
.collect();
let entries: Vec<(Handle, std::sync::Arc<EntityType>, [f32; 4], bool)> =
mesh_entities
.into_iter()
.map(|(handle, entity)| {
let color = self.render_style(entity.as_ref()).0;
let top_level = layout_blocks.contains(&entity.common().owner_handle);
(handle, entity, color, top_level)
})
.collect();
let entries: Vec<(Handle, std::sync::Arc<EntityType>, [f32; 4], bool)> = mesh_entities
.into_iter()
.map(|(handle, entity)| {
let color = self.render_style(entity.as_ref()).0;
let top_level = layout_blocks.contains(&entity.common().owner_handle);
(handle, entity, color, top_level)
})
.collect();
let facet_res = self.document.header.facet_resolution;
let chordal_deflection =
crate::entities::solid3d::display_deflection(&self.document.header, facet_res);
@@ -1119,16 +1104,13 @@ impl Scene {
self.belongs_to_visible_block(handle, c.owner_handle, target_block)
})
.flat_map(|(&handle, model)| {
let contextual = self
.document
.get_entity(handle)
.map(|entity| {
crate::scene::annotative::entity_for_annotation_context(
&self.document,
entity,
annotation_scale_handle,
)
});
let contextual = self.document.get_entity(handle).map(|entity| {
crate::scene::annotative::entity_for_annotation_context(
&self.document,
entity,
annotation_scale_handle,
)
});
let entity = contextual.as_deref();
let mut m = match entity {
Some(EntityType::Hatch(dxf))
@@ -1196,10 +1178,7 @@ impl Scene {
),
index,
),
other => (
crate::scene::convert::tess_util::aci_to_rgba(&other),
0,
),
other => (crate::scene::convert::tess_util::aci_to_rgba(&other), 0),
};
b.color = bg_color;
b.aci = bg_aci;
@@ -1218,9 +1197,7 @@ impl Scene {
m.scale = dxf.pattern_scale as f32;
}
model::hatch_model::HatchPattern::Gradient {
angle_deg,
shift,
..
angle_deg, shift, ..
} => {
*angle_deg = dxf.gradient_color.angle.to_degrees() as f32;
*shift = dxf.gradient_color.shift as f32;
@@ -1351,7 +1328,10 @@ impl Scene {
.iter()
.copied()
.filter(|&handle| {
matches!(self.document.get_entity(handle), Some(EntityType::Insert(_)))
matches!(
self.document.get_entity(handle),
Some(EntityType::Insert(_))
)
})
.collect();
if targets.is_empty() {
@@ -1421,8 +1401,7 @@ impl Scene {
return true;
}
if self.object_isolation.hides(common.handle)
|| (!include_preview_hidden
&& self.preview_hidden.contains(&common.handle))
|| (!include_preview_hidden && self.preview_hidden.contains(&common.handle))
{
return false;
}
@@ -1462,10 +1441,7 @@ impl Scene {
}
let style = context.style_for(&self.document, entity);
let preserve_white_mask = source_hatch.is_solid
&& matches!(
source_hatch.common.color,
acadrust::types::Color::Index(7)
);
&& matches!(source_hatch.common.color, acadrust::types::Color::Index(7));
let color = if preserve_white_mask {
style.0
} else {
@@ -1504,9 +1480,15 @@ impl Scene {
}
let matrix = &context.transform.matrix.m;
let linear = [
matrix[0][0].to_bits(), matrix[0][1].to_bits(), matrix[0][2].to_bits(),
matrix[1][0].to_bits(), matrix[1][1].to_bits(), matrix[1][2].to_bits(),
matrix[2][0].to_bits(), matrix[2][1].to_bits(), matrix[2][2].to_bits(),
matrix[0][0].to_bits(),
matrix[0][1].to_bits(),
matrix[0][2].to_bits(),
matrix[1][0].to_bits(),
matrix[1][1].to_bits(),
matrix[1][2].to_bits(),
matrix[2][0].to_bits(),
matrix[2][1].to_bits(),
matrix[2][2].to_bits(),
];
let key = (
source_hatch.common.handle.value(),
@@ -1525,12 +1507,10 @@ impl Scene {
let source_id = *hatch_sources
.entry(key)
.or_insert_with(crate::scene::model::instance_model::next_source_id);
model.render_instance = Some(
crate::scene::model::instance_model::RenderInstance {
source_id,
translation: [matrix[0][3], matrix[1][3], matrix[2][3]],
},
);
model.render_instance = Some(crate::scene::model::instance_model::RenderInstance {
source_id,
translation: [matrix[0][3], matrix[1][3], matrix[2][3]],
});
models.push(model);
},
);
@@ -1598,48 +1578,37 @@ impl Scene {
};
let mut wipeout = source.clone();
if context.is_instanced() {
wipeout.insertion_point =
context.transform.apply(source.insertion_point);
wipeout.u_vector =
context.transform.apply_rotation(source.u_vector);
wipeout.v_vector =
context.transform.apply_rotation(source.v_vector);
wipeout.insertion_point = context.transform.apply(source.insertion_point);
wipeout.u_vector = context.transform.apply_rotation(source.u_vector);
wipeout.v_vector = context.transform.apply_rotation(source.v_vector);
}
let Some(mut fill_plane) = Self::wipeout_fill_plane(&wipeout) else {
return;
};
let (world_origin, mut boundary) =
Self::wipeout_boundary_2d(&wipeout);
let (world_origin, mut boundary) = Self::wipeout_boundary_2d(&wipeout);
for clip in &context.clips {
let clip: Vec<[f32; 2]> = clip
.iter()
.map(|point| [point[0] as f32, point[1] as f32])
.collect();
boundary = pick::xclip::clip_hatch_boundary(
&boundary,
world_origin,
&clip,
);
boundary = pick::xclip::clip_hatch_boundary(&boundary, world_origin, &clip);
}
if boundary.len() < 3 {
return;
}
if !context.clips.is_empty() {
let Some(local) = Self::wipeout_boundary_at_xy(
fill_plane.0,
world_origin,
&boundary,
) else {
let Some(local) =
Self::wipeout_boundary_at_xy(fill_plane.0, world_origin, &boundary)
else {
return;
};
fill_plane.1 = local;
}
let selection_handle =
if tint_insert_selection && context.is_instanced() {
context.root_handle
} else {
source.common.handle
};
let selection_handle = if tint_insert_selection && context.is_instanced() {
context.root_handle
} else {
source.common.handle
};
let color = if self.selected.contains(&selection_handle) {
[0.15, 0.55, 1.00, 0.35]
} else {
@@ -1648,9 +1617,15 @@ impl Scene {
let render_instance = if context.is_instanced() {
let matrix = &context.transform.matrix.m;
let linear = [
matrix[0][0].to_bits(), matrix[0][1].to_bits(), matrix[0][2].to_bits(),
matrix[1][0].to_bits(), matrix[1][1].to_bits(), matrix[1][2].to_bits(),
matrix[2][0].to_bits(), matrix[2][1].to_bits(), matrix[2][2].to_bits(),
matrix[0][0].to_bits(),
matrix[0][1].to_bits(),
matrix[0][2].to_bits(),
matrix[1][0].to_bits(),
matrix[1][1].to_bits(),
matrix[1][2].to_bits(),
matrix[2][0].to_bits(),
matrix[2][1].to_bits(),
matrix[2][2].to_bits(),
];
let key = (
source.common.handle.value(),
@@ -1661,9 +1636,9 @@ impl Scene {
.collect::<Vec<_>>(),
color.map(f32::to_bits),
);
let source_id = *wipeout_sources.entry(key).or_insert_with(
crate::scene::model::instance_model::next_source_id,
);
let source_id = *wipeout_sources
.entry(key)
.or_insert_with(crate::scene::model::instance_model::next_source_id);
Some(crate::scene::model::instance_model::RenderInstance {
source_id,
translation: [matrix[0][3], matrix[1][3], matrix[2][3]],
@@ -1692,8 +1667,7 @@ impl Scene {
angle_offset: 0.0,
scale: 1.0,
world_origin,
draw_depth: context
.draw_depth(source.common.handle, depth_map.as_ref()),
draw_depth: context.draw_depth(source.common.handle, depth_map.as_ref()),
});
},
);
@@ -1708,7 +1682,11 @@ impl Scene {
) -> ([f64; 2], Vec<[f32; 2]>) {
let origin = [wo.insertion_point.x, wo.insertion_point.y];
let plane = cadkernel::space::Plane::from_axes(
[wo.insertion_point.x, wo.insertion_point.y, wo.insertion_point.z],
[
wo.insertion_point.x,
wo.insertion_point.y,
wo.insertion_point.z,
],
[wo.u_vector.x, wo.u_vector.y, wo.u_vector.z],
[wo.v_vector.x, wo.v_vector.y, wo.v_vector.z],
);
@@ -1744,12 +1722,7 @@ impl Scene {
let mut clip: Vec<[f64; 2]> = wo
.clip_boundary_vertices
.iter()
.map(|point| {
[
point.x + wo.size.x * 0.5,
wo.size.y * 0.5 - point.y,
]
})
.map(|point| [point.x + wo.size.x * 0.5, wo.size.y * 0.5 - point.y])
.collect();
if clip.last() != clip.first() {
clip.push(clip[0]);
@@ -1793,11 +1766,7 @@ impl Scene {
world_origin: [f64; 2],
boundary: &[[f32; 2]],
) -> Option<Vec<[f32; 2]>> {
let plane = cadkernel::space::Plane::from_axes(
plane.origin,
plane.x_axis,
plane.y_axis,
);
let plane = cadkernel::space::Plane::from_axes(plane.origin, plane.x_axis, plane.y_axis);
boundary
.iter()
.map(|point| {
@@ -1815,10 +1784,7 @@ impl Scene {
.collect()
}
pub(crate) fn hatch_model_from_dxf(
dxf: &DxfHatch,
color: [f32; 4],
) -> Option<HatchModel> {
pub(crate) fn hatch_model_from_dxf(dxf: &DxfHatch, color: [f32; 4]) -> Option<HatchModel> {
let normal = (dxf.normal.x, dxf.normal.y, dxf.normal.z);
// Build the boundary in f64 first so the precision-preserving
// origin computation below sees full WCS precision. We only cast
@@ -1849,9 +1815,7 @@ impl Scene {
let mut edge_polys: Vec<Vec<[f64; 2]>> = Vec::new();
for edge in &path.edges {
if let Some(curve) = crate::entities::hatch::edge_curve(edge) {
edge_polys.push(
curve.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE),
);
edge_polys.push(curve.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE));
}
}
let mut local_ring = chain_path_edges(edge_polys);
@@ -1902,11 +1866,7 @@ impl Scene {
local_boundary.push([f32::NAN, f32::NAN]);
}
boundary.extend(ring);
local_boundary.extend(
local_ring
.into_iter()
.map(|[x, y]| [x as f32, y as f32]),
);
local_boundary.extend(local_ring.into_iter().map(|[x, y]| [x as f32, y as f32]));
boundary_exterior.push(depth == 0);
boundary_sources.push(sources);
}
@@ -1941,18 +1901,19 @@ impl Scene {
// lines. The stored offset is the authoritative world-unit spacing, so
// the resulting families are already final: no pattern_scale / angle
// is re-applied (see `prebaked` below and the HatchModel fields).
let prebaked = !dxf.is_solid
&& !dxf.gradient_color.is_enabled()
&& !dxf.pattern.lines.is_empty();
let prebaked =
!dxf.is_solid && !dxf.gradient_color.is_enabled() && !dxf.pattern.lines.is_empty();
// The gradient's first stop is the fill's start colour (not the
// entity colour); capture it so the HatchModel draws stop-0 → stop-1.
let mut gradient_color1: Option<[f32; 4]> = None;
let mut pattern = if dxf.gradient_color.is_enabled() {
let stop = |i: usize| {
dxf.gradient_color.colors.get(i).and_then(|e| e.color.rgb()).map(
|(r, g, b)| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0],
)
dxf.gradient_color
.colors
.get(i)
.and_then(|e| e.color.rgb())
.map(|(r, g, b)| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0])
};
gradient_color1 = stop(0);
let color1 = gradient_color1.unwrap_or(color);
@@ -1975,7 +1936,11 @@ impl Scene {
model::hatch_model::HatchPattern::Solid
} else if prebaked {
model::hatch_model::HatchPattern::Pattern(
dxf.pattern.lines.iter().map(family_from_stored_line).collect(),
dxf.pattern
.lines
.iter()
.map(family_from_stored_line)
.collect(),
)
} else {
let pat_name = &dxf.pattern.name;
@@ -2038,10 +2003,18 @@ impl Scene {
let mut max_y = f64::NEG_INFINITY;
for &[x, y] in &boundary {
if x.is_finite() && y.is_finite() {
if x < min_x { min_x = x; }
if y < min_y { min_y = y; }
if x > max_x { max_x = x; }
if y > max_y { max_y = y; }
if x < min_x {
min_x = x;
}
if y < min_y {
min_y = y;
}
if x > max_x {
max_x = x;
}
if y > max_y {
max_y = y;
}
}
}
let world_origin = if min_x.is_finite() && min_y.is_finite() {
@@ -2129,8 +2102,16 @@ impl Scene {
color: gradient_color1.unwrap_or(color),
aci: 0,
line_weight_px: 1.0,
angle_offset: if prebaked { 0.0 } else { dxf.pattern_angle as f32 },
scale: if prebaked { 1.0 } else { dxf.pattern_scale as f32 },
angle_offset: if prebaked {
0.0
} else {
dxf.pattern_angle as f32
},
scale: if prebaked {
1.0
} else {
dxf.pattern_scale as f32
},
world_origin,
draw_depth: 0.0,
})
@@ -2168,16 +2149,13 @@ impl Scene {
/// at load, so a pattern-scale / background / boundary edit stays
/// invisible until the cached model is refreshed (#415).
pub fn refresh_fill_model(&mut self, handle: Handle) {
let contextual = self
.document
.get_entity(handle)
.map(|entity| {
crate::scene::annotative::entity_for_annotation_context(
&self.document,
entity,
self.displayed_annotation_scale_handle(),
)
});
let contextual = self.document.get_entity(handle).map(|entity| {
crate::scene::annotative::entity_for_annotation_context(
&self.document,
entity,
self.displayed_annotation_scale_handle(),
)
});
let new_model = match contextual.as_deref() {
Some(EntityType::Hatch(dxf)) => {
let color = convert::tess_util::aci_to_rgba(&dxf.common.color);
@@ -2213,7 +2191,7 @@ impl Scene {
e,
self.displayed_annotation_scale_handle(),
)
.into_owned(),
.into_owned(),
)),
EntityType::Solid(s) => Some((s.common.handle, e.clone())),
_ => None,
@@ -2315,7 +2293,8 @@ impl Scene {
// Incremental (post-xref) pass: leave already-tessellated
// host solids untouched, only build the newly merged ones.
if incremental
&& (self.meshes.contains_key(&handle) || self.block_meshes.contains_key(&handle))
&& (self.meshes.contains_key(&handle)
|| self.block_meshes.contains_key(&handle))
{
return None;
}
@@ -2363,7 +2342,11 @@ impl Scene {
&self.document,
&entity,
);
let mesh = if top_level { offset_mesh_lod_set(mesh) } else { mesh };
let mesh = if top_level {
offset_mesh_lod_set(mesh)
} else {
mesh
};
(handle, mesh, top_level)
})
})
@@ -2478,18 +2461,18 @@ impl Scene {
} else {
// Otherwise reconstruct every ring from render offsets.
let reconstructed_wcs: Vec<[f64; 2]> = if model.boundary_wcs.is_none() {
let [wx, wy] = model.world_origin;
model
.boundary
.iter()
.map(|&[x, y]| {
if x.is_finite() && y.is_finite() {
[x as f64 + wx, y as f64 + wy]
} else {
[f64::NAN, f64::NAN]
}
})
.collect()
let [wx, wy] = model.world_origin;
model
.boundary
.iter()
.map(|&[x, y]| {
if x.is_finite() && y.is_finite() {
[x as f64 + wx, y as f64 + wy]
} else {
[f64::NAN, f64::NAN]
}
})
.collect()
} else {
Vec::new()
};
@@ -2502,31 +2485,31 @@ impl Scene {
let mut first = true;
let mut ring_index = 0usize;
let mut push_ring = |r: &mut Vec<Vector2>, is_outer: bool, index: usize| {
if !r.is_empty() {
let edge = PolylineEdge::new(std::mem::take(r), true);
let handles: Vec<_> = model
.boundary_sources
.as_deref()
.and_then(|sources| sources.get(index))
.into_iter()
.flatten()
.copied()
.filter(|handle| handle.is_valid())
.collect();
let mut bits = 0;
if is_outer {
bits |= acadrust::entities::hatch::BoundaryPathFlags::OUTERMOST.bits();
bits |= acadrust::entities::hatch::BoundaryPathFlags::EXTERNAL.bits();
if !r.is_empty() {
let edge = PolylineEdge::new(std::mem::take(r), true);
let handles: Vec<_> = model
.boundary_sources
.as_deref()
.and_then(|sources| sources.get(index))
.into_iter()
.flatten()
.copied()
.filter(|handle| handle.is_valid())
.collect();
let mut bits = 0;
if is_outer {
bits |= acadrust::entities::hatch::BoundaryPathFlags::OUTERMOST.bits();
bits |= acadrust::entities::hatch::BoundaryPathFlags::EXTERNAL.bits();
}
let mut path = BoundaryPath::with_flags(
acadrust::entities::hatch::BoundaryPathFlags::from_bits(bits),
);
path.add_edge(BoundaryEdge::Polyline(edge));
for handle in handles {
path.add_boundary_handle(handle);
}
dxf.paths.push(path);
}
let mut path = BoundaryPath::with_flags(
acadrust::entities::hatch::BoundaryPathFlags::from_bits(bits),
);
path.add_edge(BoundaryEdge::Polyline(edge));
for handle in handles {
path.add_boundary_handle(handle);
}
dxf.paths.push(path);
}
};
for &[x, y] in wcs {
if x.is_finite() && y.is_finite() {
@@ -2580,28 +2563,20 @@ impl Scene {
let family_angle = (family.angle_deg as f64).to_radians();
let angle = family_angle + rotation;
let (family_sin, family_cos) = family_angle.sin_cos();
let local_offset_x = family.dx as f64 * family_cos
- family.dy as f64 * family_sin;
let local_offset_y = family.dx as f64 * family_sin
+ family.dy as f64 * family_cos;
let local_offset_x = family.dx as f64 * family_cos - family.dy as f64 * family_sin;
let local_offset_y = family.dx as f64 * family_sin + family.dy as f64 * family_cos;
let base_x = family.x0 as f64 * pattern_scale;
let base_y = family.y0 as f64 * pattern_scale;
pattern.lines.push(acadrust::entities::HatchPatternLine {
angle,
base_point: Vector2::new(
pattern_origin[0]
+ base_x * rotation_cos
- base_y * rotation_sin,
pattern_origin[1]
+ base_x * rotation_sin
+ base_y * rotation_cos,
pattern_origin[0] + base_x * rotation_cos - base_y * rotation_sin,
pattern_origin[1] + base_x * rotation_sin + base_y * rotation_cos,
),
offset: Vector2::new(
(local_offset_x * rotation_cos
- local_offset_y * rotation_sin)
(local_offset_x * rotation_cos - local_offset_y * rotation_sin)
* pattern_scale,
(local_offset_x * rotation_sin
+ local_offset_y * rotation_cos)
(local_offset_x * rotation_sin + local_offset_y * rotation_cos)
* pattern_scale,
),
dash_lengths: family
@@ -2642,13 +2617,13 @@ impl Scene {
dxf.gradient_color.is_single_color = false;
// Linear has no INV name in the standard set: persist an inverted
// linear by swapping the colour stops instead.
let (c0, c1) =
if *invert && matches!(kind, crate::scene::model::hatch_model::GradientKind::Linear)
{
(*color2, model.color)
} else {
(model.color, *color2)
};
let (c0, c1) = if *invert
&& matches!(kind, crate::scene::model::hatch_model::GradientKind::Linear)
{
(*color2, model.color)
} else {
(model.color, *color2)
};
dxf.gradient_color.colors = vec![
acadrust::entities::hatch::GradientColorEntry {
value: 0.0,
@@ -2703,7 +2678,7 @@ impl Scene {
// resetting them, a "new"/"open" (e.g. via automation, which reuses
// this same reset) would leak the previous document's constraints
// and named parameters into the fresh one.
self.sketch_constraints.clear();
self.parametric_constraints.clear();
self.named_parameters = crate::scene::named_parameters::ParameterTable::new();
self.bump_geometry();
}
+627 -621
View File
File diff suppressed because it is too large. Load diff
+11 -8
View File
@@ -24,10 +24,7 @@ pub enum PropValue {
/// Editable text plus a dropdown of existing options (block reference
/// Name row): picking an option re-points the reference, submitting a
/// new name renames the definition.
EditChoice {
value: String,
options: Vec<String>,
},
EditChoice { value: String, options: Vec<String> },
/// ACI/RGB/ByLayer/ByBlock color — rendered as a color picker.
ColorChoice(AcadColor),
/// Color-book color with its file-provided display name.
@@ -51,7 +48,10 @@ pub enum PropValue {
BoolToggle { field: &'static str, value: bool },
/// A 0-based index navigated with ◀ / ▶ buttons (e.g. a polyline's Current
/// Vertex). `display` is the label shown between the arrows (e.g. "2 / 7").
Stepper { field: &'static str, display: String },
Stepper {
field: &'static str,
display: String,
},
/// Hatch pattern name — rendered as a combo_box from the catalog.
HatchPatternChoice(String),
/// Block attribute value keyed by its (dynamic, runtime) tag — rendered as
@@ -59,11 +59,14 @@ pub enum PropValue {
/// tag carried here, not the row's `&'static str` field.
AttrText { tag: String, value: String },
/// A clickable link to one or more entities — the Constraints section's
/// row for one persistent sketch constraint. Clicking it selects every
/// row for one parametric constraint. Clicking it selects every
/// entity in `handles` in the viewport. `conflicting` mirrors
/// `SketchConstraintSet::conflicts`, tinting the row the same danger
/// `ParametricConstraintSet::conflicts`, tinting the row the same danger
/// color the constraint's glyph pill already uses.
EntityLink { handles: Vec<Handle>, conflicting: bool },
EntityLink {
handles: Vec<Handle>,
conflicting: bool,
},
/// One row of the document-wide named-parameter table (Parameters
/// section, shown when nothing is selected). `index` is the row's
/// position in `ParameterTable::iter()` order — stable across edits to
+65 -93
View File
@@ -9,9 +9,7 @@ struct TextOrient {
x_scale: f64,
}
fn inverse_affine(
transform: &acadrust::types::Transform,
) -> Option<acadrust::types::Transform> {
fn inverse_affine(transform: &acadrust::types::Transform) -> Option<acadrust::types::Transform> {
use acadrust::types::{Matrix3, Matrix4, Transform, Vector3};
let matrix = &transform.matrix.m;
let linear = Matrix3::from_rows(
@@ -248,9 +246,7 @@ impl Scene {
let image_bearing = self.document.get_entity(h).is_some_and(|entity| {
matches!(
entity,
EntityType::RasterImage(_)
| EntityType::Ole2Frame(_)
| EntityType::Underlay(_)
EntityType::RasterImage(_) | EntityType::Ole2Frame(_) | EntityType::Underlay(_)
)
});
if image_bearing {
@@ -301,10 +297,7 @@ impl Scene {
/// Entities in anonymous dimension blocks that visually belong to
/// dimensions inside `block_record`. They must follow a block-coordinate
/// reframe and be included in BEDIT's discard snapshot.
pub(crate) fn block_definition_dependent_handles(
&self,
block_record: Handle,
) -> Vec<Handle> {
pub(crate) fn block_definition_dependent_handles(&self, block_record: Handle) -> Vec<Handle> {
let Some(record) = self
.document
.block_records
@@ -367,17 +360,16 @@ impl Scene {
let owned: HashSet<Handle> = handles.iter().copied().collect();
let transform = EntityTransform::Affine(*local_from_old);
let matrix = &local_from_old.matrix.m;
let dependent_transform =
EntityTransform::Affine(acadrust::types::Transform::from_matrix(
acadrust::types::Matrix4 {
m: [
[matrix[0][0], matrix[0][1], matrix[0][2], 0.0],
[matrix[1][0], matrix[1][1], matrix[1][2], 0.0],
[matrix[2][0], matrix[2][1], matrix[2][2], 0.0],
[0.0, 0.0, 0.0, 1.0],
],
},
));
let dependent_transform = EntityTransform::Affine(acadrust::types::Transform::from_matrix(
acadrust::types::Matrix4 {
m: [
[matrix[0][0], matrix[0][1], matrix[0][2], 0.0],
[matrix[1][0], matrix[1][1], matrix[1][2], 0.0],
[matrix[2][0], matrix[2][1], matrix[2][2], 0.0],
[0.0, 0.0, 0.0, 1.0],
],
},
));
let mut changed = Vec::new();
for handle in handles {
@@ -596,10 +588,7 @@ impl Scene {
let entity = self.document.get_entity(h)?.clone();
let annotation_scales = if leader_pair_handles.contains(&h) {
crate::scene::annotative::annotation_scale_handles_for_entity(
&self.document,
h,
)
crate::scene::annotative::annotation_scale_handles_for_entity(&self.document, h)
} else {
Vec::new()
};
@@ -691,8 +680,8 @@ impl Scene {
if let Some(model) = new_model {
self.hatches.insert(h, model);
}
let rebuilt_history = self.copy_solid_history(src_handle, h)
&& self.transform_solid_history(h, t);
let rebuilt_history =
self.copy_solid_history(src_handle, h) && self.transform_solid_history(h, t);
if !rebuilt_history
&& self.document.get_entity(h).is_some_and(|entity| {
matches!(
@@ -717,8 +706,7 @@ impl Scene {
let leader_links: Vec<(Handle, Handle)> = handle_map
.iter()
.filter_map(|(&source_handle, &copied_handle)| {
let EntityType::Leader(source_leader) =
self.document.get_entity(source_handle)?
let EntityType::Leader(source_leader) = self.document.get_entity(source_handle)?
else {
return None;
};
@@ -733,9 +721,7 @@ impl Scene {
.collect();
for (leader_handle, annotation_handle) in leader_links {
if let Some(EntityType::Leader(leader)) =
self.document.get_entity_mut(leader_handle)
{
if let Some(EntityType::Leader(leader)) = self.document.get_entity_mut(leader_handle) {
leader.annotation_handle = annotation_handle;
}
@@ -753,7 +739,7 @@ impl Scene {
.collect();
self.bump_entities(&changes);
// Constraints wholly within the duplicated selection follow it.
self.duplicate_sketch_constraints_for(&handle_map);
self.duplicate_parametric_constraints_for(&handle_map);
self.refresh_meshes_for_handles(&refresh_solid_handles);
new_handles
}
@@ -840,11 +826,7 @@ impl Scene {
let Some(reference) = reference else {
return;
};
let point = acadrust::types::Vector3::new(
reference.x,
reference.y,
reference.z,
);
let point = acadrust::types::Vector3::new(reference.x, reference.y, reference.z);
match self.document.get_entity_mut(handle) {
Some(EntityType::Solid3D(entity)) => entity.point_of_reference = point,
Some(EntityType::Surface(entity)) => entity.point_of_reference = point,
@@ -928,7 +910,9 @@ impl Scene {
(EntityType::Surface(_), SolidHistoryOperation::Loft(value)) => {
let EntityType::Surface(surface) =
crate::scene::model::loft_command_model::surface_entity(value)
else { return None; };
else {
return None;
};
Some(Some(surface.surface_data))
}
(EntityType::Solid3D(_), _) => Some(None),
@@ -953,9 +937,11 @@ impl Scene {
let Some(display) = self.prepare_solid_model_display(handle, &body) else {
return false;
};
if matches!(&operation, acadrust::objects::SolidHistoryOperation::Loft(_)
| acadrust::objects::SolidHistoryOperation::Extrusion(_))
&& !display.0.complete
if matches!(
&operation,
acadrust::objects::SolidHistoryOperation::Loft(_)
| acadrust::objects::SolidHistoryOperation::Extrusion(_)
) && !display.0.complete
{
return false;
}
@@ -970,7 +956,8 @@ impl Scene {
match self.document.get_entity_mut(handle) {
Some(EntityType::Solid3D(entity)) => entity.set_sat_document(&document),
Some(EntityType::Surface(entity)) => {
entity.acis_data = acadrust::entities::AcisData::from_sat(&document.to_sat_string());
entity.acis_data =
acadrust::entities::AcisData::from_sat(&document.to_sat_string());
if let Some(data) = surface_data {
if matches!(&data, acadrust::entities::SurfaceData::Extruded { .. }) {
entity.kind = acadrust::entities::SurfaceKind::Extruded;
@@ -1001,16 +988,19 @@ impl Scene {
let Some(display) = self.prepare_solid_model_display(handle, &body) else {
return false;
};
if matches!(&operation, acadrust::objects::SolidHistoryOperation::Loft(_)
| acadrust::objects::SolidHistoryOperation::Extrusion(_))
&& !display.0.complete
if matches!(
&operation,
acadrust::objects::SolidHistoryOperation::Loft(_)
| acadrust::objects::SolidHistoryOperation::Extrusion(_)
) && !display.0.complete
{
return false;
}
match self.document.get_entity_mut(handle) {
Some(EntityType::Solid3D(entity)) => entity.set_sat_document(&document),
Some(EntityType::Surface(entity)) => {
entity.acis_data = acadrust::entities::AcisData::from_sat(&document.to_sat_string());
entity.acis_data =
acadrust::entities::AcisData::from_sat(&document.to_sat_string());
if let Some(data) = surface_data {
if matches!(&data, acadrust::entities::SurfaceData::Extruded { .. }) {
entity.kind = acadrust::entities::SurfaceKind::Extruded;
@@ -1062,24 +1052,22 @@ impl Scene {
true
}
fn transform_solid_history(
&mut self,
handle: Handle,
transform: &EntityTransform,
) -> bool {
fn transform_solid_history(&mut self, handle: Handle, transform: &EntityTransform) -> bool {
let Some(mut operation) = self.document.solid_history_operation(handle).cloned() else {
return false;
};
if !crate::scene::model::solid_history::transform_operation(
&mut operation,
transform,
) {
if !crate::scene::model::solid_history::transform_operation(&mut operation, transform) {
return false;
}
if let EntityTransform::Translate(delta) = transform {
let surface_data = match (&operation, self.document.get_entity(handle)) {
(acadrust::objects::SolidHistoryOperation::Extrusion(value), Some(EntityType::Surface(_))) => {
let Some(data) = crate::scene::model::solid_history::extrusion_surface_data(value) else {
(
acadrust::objects::SolidHistoryOperation::Extrusion(value),
Some(EntityType::Surface(_)),
) => {
let Some(data) =
crate::scene::model::solid_history::extrusion_surface_data(value)
else {
return false;
};
Some(data)
@@ -1104,12 +1092,7 @@ impl Scene {
self.rebuild_solid_history(handle, operation)
}
pub fn apply_solid_history_choice(
&mut self,
handle: Handle,
field: &str,
value: &str,
) -> bool {
pub fn apply_solid_history_choice(&mut self, handle: Handle, field: &str, value: &str) -> bool {
self.record_solid_history_before(handle);
let mut created = false;
if self.document.solid_history_graph(handle).is_none() {
@@ -1137,7 +1120,10 @@ impl Scene {
_ => return false,
};
if create {
if !matches!(self.document.get_entity(handle), Some(EntityType::Solid3D(_))) {
if !matches!(
self.document.get_entity(handle),
Some(EntityType::Solid3D(_))
) {
return false;
}
self.restore_solid_models(&[handle]);
@@ -1195,13 +1181,9 @@ impl Scene {
return false;
};
self.restore_solid_models(&[handle]);
let Some(definition) = self
.solid_models
.get(&handle)
.and_then(|body| {
crate::scene::model::solid_history::fillet_radius_grip(body, radius)
})
else {
let Some(definition) = self.solid_models.get(&handle).and_then(|body| {
crate::scene::model::solid_history::fillet_radius_grip(body, radius)
}) else {
return false;
};
let Some(axis) = definition.axis else {
@@ -1247,8 +1229,7 @@ impl Scene {
let acadrust::objects::SolidHistoryOperation::Chamfer(value) = &mut operation else {
return false;
};
let distance = if grip_id
== crate::scene::model::solid_history::GRIP_CHAMFER_DISTANCE1
let distance = if grip_id == crate::scene::model::solid_history::GRIP_CHAMFER_DISTANCE1
{
&mut value.base_distance
} else {
@@ -1260,11 +1241,8 @@ impl Scene {
}
return self.preview_solid_history(handle, operation);
}
if !crate::scene::model::solid_history::apply_primitive_grip(
&mut operation,
grip_id,
apply,
) {
if !crate::scene::model::solid_history::apply_primitive_grip(&mut operation, grip_id, apply)
{
return false;
}
self.preview_solid_history(handle, operation)
@@ -1276,12 +1254,10 @@ impl Scene {
field: &str,
value: &str,
) -> bool {
let Some(mut operation) =
crate::scene::model::solid_history::primitive_property_operation(
&self.document,
handle,
)
else {
let Some(mut operation) = crate::scene::model::solid_history::primitive_property_operation(
&self.document,
handle,
) else {
return false;
};
if !crate::scene::model::solid_history::apply_primitive_property(
@@ -1407,7 +1383,8 @@ impl Scene {
};
let n = leader.vertices.len();
if n < 3 || (grip_id != n - 1 && grip_id != n - 2) || leader.annotation_handle.is_null() {
if n < 3 || (grip_id != n - 1 && grip_id != n - 2) || leader.annotation_handle.is_null()
{
return None;
}
@@ -1451,10 +1428,7 @@ impl Scene {
if self.sync_displayed_annotation_context(annotation_handle) {
self.poison_undo_recording();
}
self.bump_entities(&[(
annotation_handle,
crate::scene::ChangeKind::Modified,
)]);
self.bump_entities(&[(annotation_handle, crate::scene::ChangeKind::Modified)]);
}
}
}
@@ -1510,9 +1484,7 @@ impl Scene {
let image_bearing = self.document.get_entity(handle).is_some_and(|entity| {
matches!(
entity,
EntityType::RasterImage(_)
| EntityType::Ole2Frame(_)
| EntityType::Underlay(_)
EntityType::RasterImage(_) | EntityType::Ole2Frame(_) | EntityType::Underlay(_)
)
});
+6 -21
View File
@@ -2,14 +2,13 @@
//! Formulas are parsed locally so dependency cycles can be rejected before evaluation.
use super::Scene;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
/// A parsed formula. Ordinary arithmetic plus [`Expr::Ref`] (a reference to
/// another parameter by name) and [`Expr::Call`] (a function call). Only the
/// small builtin set in `call_builtin` is accepted.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
Number(f64),
Ref(String),
@@ -57,7 +56,7 @@ impl Expr {
/// stages, the UI) rather than panicking — matching this codebase's
/// established pattern of isolating a bad derived value instead of letting
/// it take down an unrelated computation (e.g. dangling-constraint handling
/// in `sketch_solve.rs`).
/// in `parametric_solve.rs`).
#[derive(Debug, Clone, PartialEq)]
pub enum ParamError {
/// A formula's text doesn't parse. The `String` is a short, human-
@@ -131,7 +130,7 @@ pub fn is_valid_name(name: &str) -> bool {
}
/// A dimensional constraint's literal or named driving value.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq)]
pub enum DrivingValue {
Literal(f64),
Named(String),
@@ -458,7 +457,7 @@ pub fn parse(source: &str) -> Result<Expr, ParamError> {
/// One named parameter: a name plus its formula, both the original typed
/// text (`source`, for redisplay/re-editing) and the parsed [`Expr`]
/// (`expr`, for evaluation).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq)]
pub struct Parameter {
pub name: String,
pub source: String,
@@ -468,11 +467,11 @@ pub struct Parameter {
/// Every named parameter for one document.
///
/// `parameters` is intentionally not `pub` (unlike the sibling
/// `SketchConstraintSet::constraints`): this table's entire value is that
/// `ParametricConstraintSet::constraints`): this table's entire value is that
/// every stored formula is already known to be parse-valid and cycle-free,
/// an invariant only `set`/`remove` maintain. A direct external push could
/// silently violate it.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ParameterTable {
parameters: Vec<Parameter>,
}
@@ -932,20 +931,6 @@ mod tests {
assert_eq!(table.resolve("a"), Ok(2.0));
}
#[test]
fn table_round_trips_through_bincode() {
let mut table = ParameterTable::new();
table.set("hole_dia", "5").unwrap();
table.set("hole_spacing", "2 * hole_dia + 1.5").unwrap();
let bytes = bincode::serialize(&table).expect("serialize");
let restored: ParameterTable = bincode::deserialize(&bytes).expect("deserialize");
assert_eq!(restored.len(), 2);
assert_eq!(restored.resolve("hole_spacing"), Ok(11.5));
assert_eq!(restored.get("hole_dia").unwrap().source, "5");
}
#[test]
fn iter_preserves_insertion_order_across_redefinitions() {
let mut table = ParameterTable::new();
-210
View File
@@ -1,210 +0,0 @@
//! Persists the drawing-wide named-parameter table in a versioned XRecord.
use super::named_parameters::ParameterTable;
use super::Scene;
use bincode::Options;
const XRECORD_KEY: &str = "OCS_NAMED_PARAMETERS";
/// See `sketch_persist::MAX_CHUNK_BYTES`'s doc comment: the vendored DWG
/// writer truncates any single `XRecordValue::Chunk` entry to `u8::MAX`
/// bytes, so a table past a handful of parameters must be split across
/// multiple same-code `310` entries on write and concatenated back on read.
const MAX_CHUNK_BYTES: usize = u8::MAX as usize;
const MAX_RECORD_BYTES: usize = 8 * 1024 * 1024;
/// Prefixed onto the serialized blob so a future schema change can be
/// detected and gracefully skipped rather than silently misreading bytes —
/// same call-out as `sketch_persist::FORMAT_VERSION`, same caveat that the
/// migration path itself is still undesigned.
const FORMAT_VERSION: u8 = 1;
fn encode(table: &ParameterTable) -> Option<Vec<u8>> {
let mut bytes = vec![FORMAT_VERSION];
let body = bincode::serialize(table).ok()?;
if body.len() >= MAX_RECORD_BYTES {
return None;
}
bytes.extend(body);
Some(bytes)
}
fn decode(bytes: &[u8]) -> Option<ParameterTable> {
let (&version, body) = bytes.split_first()?;
if version != FORMAT_VERSION {
return None;
}
bincode::DefaultOptions::new()
.with_fixint_encoding()
.allow_trailing_bytes()
.with_limit(MAX_RECORD_BYTES as u64)
.deserialize(body)
.ok()
}
impl Scene {
/// Writes `named_parameters` into the document's `OCS_NAMED_PARAMETERS`
/// XRecord, ready for whatever save call happens next to serialize
/// `self.document` as-is. Called from the same save entry points as
/// `materialize_sketch_constraints_for_save`
/// (`OpenCADStudio::prepare_native_save`, the wasm save path).
///
/// An empty table is skipped rather than writing an empty XRecord — an
/// unconstrained-by-parameters drawing (the common case, at least until
/// this feature is used) should not gain persisted-but-empty
/// bookkeeping on every save.
pub(crate) fn materialize_named_parameters_for_save(&mut self) {
if self.named_parameters.is_empty() {
return;
}
let Some(bytes) = encode(&self.named_parameters) else {
return;
};
let owner = self.document.header.model_space_block_handle;
if owner.is_null() {
return;
}
self.document.ensure_xrecord(owner, XRECORD_KEY);
if let Some(record) = self.document.xrecord_mut(owner, XRECORD_KEY) {
// Overwrite, not append: a resave must replace the prior blob,
// not accumulate more Chunk entries every time.
record.entries.clear();
for chunk in bytes.chunks(MAX_CHUNK_BYTES) {
record.entries.push(acadrust::objects::XRecordEntry::new(
310,
acadrust::objects::XRecordValue::Chunk(chunk.to_vec()),
));
}
}
}
/// Populates `named_parameters` from the just-loaded `self.document`'s
/// `OCS_NAMED_PARAMETERS` XRecord, if any — called right after a
/// document open installs its `CadDocument` into this `Scene`, the same
/// call sites `load_sketch_constraints_from_document` already has
/// (`OpenCADStudio::on_file_opened`, the automation `"open"`/`"new"`
/// ops). Replaces whatever was already in `named_parameters` (a fresh
/// table for a real open; `"new"`'s blank document has nothing to find
/// anyway).
pub(crate) fn load_named_parameters_from_document(&mut self) {
self.named_parameters = ParameterTable::new();
let owner = self.document.header.model_space_block_handle;
if owner.is_null() {
return;
}
let Some(record) = self.document.xrecord(owner, XRECORD_KEY) else {
return;
};
// Concatenate every Chunk entry in order, not just the first — see
// `MAX_CHUNK_BYTES`'s doc comment.
let mut bytes = Vec::new();
for entry in &record.entries {
if let acadrust::objects::XRecordValue::Chunk(chunk) = &entry.value {
if bytes.len().saturating_add(chunk.len()) > MAX_RECORD_BYTES {
bytes.clear();
break;
}
bytes.extend_from_slice(chunk);
}
}
if bytes.is_empty() {
return;
}
if let Some(table) = decode(&bytes) {
self.named_parameters = table;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_parameter_table_survives_materialize_and_reload() {
let mut scene = Scene::new();
scene.named_parameters_mut().set("hole_dia", "5").unwrap();
scene
.named_parameters_mut()
.set("hole_spacing", "2 * hole_dia + 1.5")
.unwrap();
scene.materialize_named_parameters_for_save();
// Simulate the load-time side of a save/reload round trip: drop the
// in-memory table (a fresh open starts empty) and repopulate purely
// from what materialize just wrote into `document.objects`.
scene.named_parameters = ParameterTable::new();
scene.load_named_parameters_from_document();
assert_eq!(scene.named_parameters().len(), 2);
assert_eq!(scene.named_parameters().resolve("hole_spacing"), Ok(11.5));
assert_eq!(
scene.named_parameters().get("hole_dia").unwrap().source,
"5"
);
}
/// The above test only proves `materialize`/`load` agree with each
/// other on the same in-memory `CadDocument` — this drives a real
/// `save_to_bytes`/`load_bytes` round trip (the same primitives
/// `OpenCADStudio::on_file_opened`'s native save/open path uses),
/// through both supported formats, mirroring
/// `sketch_persist`'s own real-bytes tests.
fn full_bytes_roundtrip(ext: &str) -> ParameterTable {
let mut scene = Scene::new();
scene
.named_parameters_mut()
.set("plate_len", "42.5")
.unwrap();
scene
.named_parameters_mut()
.set("plate_width", "plate_len / 2")
.unwrap();
scene.materialize_named_parameters_for_save();
let bytes = crate::io::save_to_bytes(&scene.document, ext, scene.document.version)
.unwrap_or_else(|e| panic!("save to {ext} bytes: {e}"));
let mut reloaded_scene = Scene::new();
reloaded_scene.document =
crate::io::load_bytes(&format!("named_params_roundtrip.{ext}"), bytes)
.unwrap_or_else(|e| panic!("reload {ext} bytes: {e}"));
reloaded_scene.load_named_parameters_from_document();
reloaded_scene.named_parameters
}
#[test]
fn a_parameter_table_survives_a_real_dxf_save_and_load_round_trip() {
let restored = full_bytes_roundtrip("dxf");
assert_eq!(restored.len(), 2);
assert_eq!(restored.resolve("plate_width"), Ok(21.25));
}
#[test]
fn a_parameter_table_survives_a_real_dwg_save_and_load_round_trip() {
let restored = full_bytes_roundtrip("dwg");
assert_eq!(restored.len(), 2);
assert_eq!(restored.resolve("plate_width"), Ok(21.25));
}
#[test]
fn an_empty_table_is_not_materialized() {
let mut scene = Scene::new();
scene.materialize_named_parameters_for_save();
let owner = scene.document.header.model_space_block_handle;
assert!(
scene.document.xrecord(owner, XRECORD_KEY).is_none(),
"an empty table should not create an XRecord"
);
}
#[test]
fn decode_rejects_a_mismatched_format_version() {
let mut bytes = vec![FORMAT_VERSION.wrapping_add(1)];
bytes.extend(bincode::serialize(&ParameterTable::new()).unwrap());
assert!(
decode(&bytes).is_none(),
"a future/unknown format version must be rejected, not misread"
);
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
-393
View File
@@ -1,393 +0,0 @@
//! Persists each sketch constraint set in a versioned, chunked XRecord.
use super::sketch_constraints::SketchConstraintSet;
use super::Scene;
use acadrust::{CadDocument, Handle};
use bincode::Options;
const XRECORD_KEY: &str = "OCS_SKETCH_CONSTRAINTS";
/// The vendored DWG writer's `XRecordValue::Chunk` encoder packs a single
/// `u8` length prefix per entry and silently truncates anything longer
/// (`cadcodec`'s `io/dwg/dwg_stream_writers/object_writer/objects.rs`,
/// `encode_xrecord_entries`) — unlike the DXF writer, which has no such
/// limit. A constraint set past a handful of entries serializes past 255
/// bytes easily, so a single `XRecordEntry` silently loses the tail and
/// `bincode::deserialize` then fails on the truncated bytes, dropping the
/// *entire* scope's constraints on the next DWG load — reproduced and
/// root-caused live (not from a change log): a 5-entry set round-tripped
/// clean, a 6-entry set of the same shapes lost everything. Splitting the
/// blob across as many same-code `310` entries as needed (below) works
/// around it without touching the vendored crate — `CadDocument::xrecord`'s
/// `entries: Vec<XRecordEntry>` already supports repeating a code.
const MAX_CHUNK_BYTES: usize = u8::MAX as usize;
const MAX_RECORD_BYTES: usize = 8 * 1024 * 1024;
/// Removes a named XRecord from `owner`'s extension dictionary, if present.
/// The dictionary itself is left in place even if now empty — other code may
/// already reference it via `xdictionary_handle`, and an empty extension
/// dictionary is harmless bookkeeping, unlike a stale XRecord that would
/// resurrect deleted data on the next load.
fn remove_xrecord(document: &mut CadDocument, owner: Handle, key: &str) {
let Some(dictionary_handle) = document.extension_dictionary_handle(owner) else {
return;
};
let Some(acadrust::objects::ObjectType::Dictionary(dictionary)) =
document.objects.get_mut(&dictionary_handle)
else {
return;
};
let Some(index) = dictionary
.entries
.iter()
.position(|(name, _)| name.eq_ignore_ascii_case(key))
else {
return;
};
let (_, record_handle) = dictionary.entries.remove(index);
document.objects.remove(&record_handle);
}
/// Serialized schema version. Unknown versions are skipped without affecting
/// the rest of the drawing.
const FORMAT_VERSION: u8 = 2;
fn encode(set: &SketchConstraintSet) -> Option<Vec<u8>> {
let mut bytes = vec![FORMAT_VERSION];
let body = bincode::serialize(set).ok()?;
if body.len() >= MAX_RECORD_BYTES {
return None;
}
bytes.extend(body);
Some(bytes)
}
fn decode(bytes: &[u8]) -> Option<SketchConstraintSet> {
let (&version, body) = bytes.split_first()?;
if version != FORMAT_VERSION {
return None;
}
bincode::DefaultOptions::new()
.with_fixint_encoding()
.allow_trailing_bytes()
.with_limit(MAX_RECORD_BYTES as u64)
.deserialize(body)
.ok()
}
impl Scene {
/// Writes every current `sketch_constraints` entry into its scope's
/// owner-handle XRecord, ready for whatever save call happens next to
/// serialize `self.document` as-is. Called right before a native/web
/// save (`OpenCADStudio::prepare_native_save` and the wasm save path)
/// — see those call sites' doc comments for why "right before save"
/// rather than keeping the XRecord resident and live-updated (design
/// doc §5.2's "live" model, deferred: it would also let constraint-set
/// edits ride the ordinary object-delta undo path per §5.1(b), but this
/// lazy model was the doc's own recommended first step).
///
/// A scope with an empty constraint set writes no XRecord — an
/// unconstrained drawing (the common case) should not gain
/// persisted-but-empty bookkeeping on every save. If a *previous* save
/// already materialized a non-empty XRecord for that scope, the now-empty
/// set actively removes it instead: leaving the stale blob in place would
/// resurrect the deleted constraints the next time the document loads.
pub(crate) fn materialize_sketch_constraints_for_save(&mut self) {
for index in 0..self.sketch_constraints.len() {
let set = &self.sketch_constraints[index];
if set.constraints.is_empty() {
let owner = set.scope.owner_handle(&self.document);
if !owner.is_null() {
remove_xrecord(&mut self.document, owner, XRECORD_KEY);
}
continue;
}
let owner = set.scope.owner_handle(&self.document);
if owner.is_null() {
continue;
}
let Some(bytes) = encode(set) else { continue };
self.document.ensure_xrecord(owner, XRECORD_KEY);
if let Some(record) = self.document.xrecord_mut(owner, XRECORD_KEY) {
// Overwrite, not append: a resave must replace the prior
// blob, not accumulate more Chunk entries every time.
record.entries.clear();
for chunk in bytes.chunks(MAX_CHUNK_BYTES) {
record.entries.push(acadrust::objects::XRecordEntry::new(
310,
acadrust::objects::XRecordValue::Chunk(chunk.to_vec()),
));
}
}
}
}
/// Populates `sketch_constraints` from every `OCS_SKETCH_CONSTRAINTS`
/// XRecord found in the just-loaded `self.document` — called right
/// after a document open installs its `CadDocument` into this `Scene`
/// (`OpenCADStudio::on_file_opened` and the automation `"open"`/`"new"`
/// ops). Every `BlockRecord` (model space, paper space, and named
/// block definitions) is a candidate owner; most will have no such
/// XRecord and are skipped cheaply. Replaces whatever was already in
/// `sketch_constraints` (a fresh/empty `Vec` for a real open; `"new"`'s
/// blank document has nothing to find anyway).
pub(crate) fn load_sketch_constraints_from_document(&mut self) {
self.sketch_constraints.clear();
let owners: Vec<acadrust::Handle> = self
.document
.block_records
.iter()
.map(|record| record.handle)
.collect();
for owner in owners {
if let Some(record) = self.document.xrecord(owner, XRECORD_KEY) {
let mut bytes = Vec::new();
for entry in &record.entries {
if let acadrust::objects::XRecordValue::Chunk(chunk) = &entry.value {
if bytes.len().saturating_add(chunk.len()) > MAX_RECORD_BYTES {
bytes.clear();
break;
}
bytes.extend_from_slice(chunk);
}
}
if let Some(set) = decode(&bytes) {
self.sketch_constraints.push(set);
}
continue;
}
if let Some(set) = super::dwg_native_constraints::native_constraint_set(
&self.document,
owner,
&mut self.named_parameters,
) {
self.sketch_constraints.push(set);
}
}
}
}
#[cfg(test)]
mod tests {
use super::super::sketch_constraints::{ConstraintKind, SketchRef, SketchScope};
use super::*;
use acadrust::entities::EntityType;
use acadrust::types::Vector3;
#[test]
fn a_model_space_constraint_set_survives_materialize_and_reload() {
let mut scene = Scene::new();
let a = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points(
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(10.0, 0.0, 0.0),
)));
scene
.sketch_constraint_set_mut(SketchScope::ModelSpace)
.add(ConstraintKind::Horizontal, vec![SketchRef::whole(a)], None);
scene.materialize_sketch_constraints_for_save();
// Simulate the load-time side of a save/reload round trip: drop the
// in-memory sets (a fresh open starts with none) and repopulate
// purely from what materialize just wrote into `document.objects`.
scene.sketch_constraints.clear();
scene.load_sketch_constraints_from_document();
let set = scene
.sketch_constraint_set(SketchScope::ModelSpace)
.expect("constraint set should have been reloaded");
assert_eq!(set.constraints.len(), 1);
assert_eq!(set.constraints[0].kind, ConstraintKind::Horizontal);
assert_eq!(set.constraints[0].refs, vec![SketchRef::whole(a)]);
}
/// The above test only proves `materialize`/`load` agree with each
/// other on the same in-memory `CadDocument` — it never actually
/// serializes anything. This drives a real `save_to_bytes`/`load_bytes`
/// round trip (the same primitives `OpenCADStudio::on_file_opened`'s
/// native save/open path uses), through both supported formats, closing
/// the gap stage 2's spike proved for the raw XRecord mechanism up to
/// this stage's actual app-level wiring.
fn full_bytes_roundtrip(ext: &str) -> SketchConstraintSet {
let mut scene = Scene::new();
let a = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points(
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(10.0, 5.0, 0.0),
)));
let b = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points(
Vector3::new(10.0, 5.0, 0.0),
Vector3::new(20.0, 5.0, 0.0),
)));
scene
.sketch_constraint_set_mut(SketchScope::ModelSpace)
.add(
ConstraintKind::Coincident,
vec![SketchRef::point(a, 1), SketchRef::point(b, 0)],
None,
);
scene
.sketch_constraint_set_mut(SketchScope::ModelSpace)
.add(
ConstraintKind::Distance,
vec![SketchRef::whole(b)],
Some(crate::scene::named_parameters::DrivingValue::Literal(12.5)),
);
scene.materialize_sketch_constraints_for_save();
let bytes = crate::io::save_to_bytes(&scene.document, ext, scene.document.version)
.unwrap_or_else(|e| panic!("save to {ext} bytes: {e}"));
let mut reloaded_scene = Scene::new();
reloaded_scene.document = crate::io::load_bytes(&format!("stage4_roundtrip.{ext}"), bytes)
.unwrap_or_else(|e| panic!("reload {ext} bytes: {e}"));
reloaded_scene.load_sketch_constraints_from_document();
reloaded_scene
.sketch_constraint_set(SketchScope::ModelSpace)
.cloned()
.unwrap_or_else(|| {
panic!("no ModelSpace constraint set survived the {ext} save/load round trip")
})
}
#[test]
fn a_constraint_set_survives_a_real_dxf_save_and_load_round_trip() {
let restored = full_bytes_roundtrip("dxf");
assert_eq!(restored.constraints.len(), 2);
assert_eq!(restored.constraints[0].kind, ConstraintKind::Coincident);
assert_eq!(restored.constraints[1].kind, ConstraintKind::Distance);
assert_eq!(
restored.constraints[1].driving_param,
Some(crate::scene::named_parameters::DrivingValue::Literal(12.5))
);
}
#[test]
fn a_constraint_set_survives_a_real_dwg_save_and_load_round_trip() {
let restored = full_bytes_roundtrip("dwg");
assert_eq!(restored.constraints.len(), 2);
assert_eq!(restored.constraints[0].kind, ConstraintKind::Coincident);
assert_eq!(restored.constraints[1].kind, ConstraintKind::Distance);
assert_eq!(
restored.constraints[1].driving_param,
Some(crate::scene::named_parameters::DrivingValue::Literal(12.5))
);
}
#[test]
fn a_block_scoped_constraint_set_survives_materialize_and_reload() {
let mut scene = Scene::new();
let a = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points(
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(1.0, 0.0, 0.0),
)));
// `create_block_from_entities` returns the placed INSERT's handle,
// not the BlockRecord's own handle — `SketchScope::Block` needs the
// latter (it's what `BlockEditSession::br_handle` and
// `SketchScope::owner_handle` both mean by "a block definition's
// handle"), so look it up from the table by name instead of using
// the return value directly.
let _insert_handle = scene
.create_block_from_entities(
&[a],
"OCS_STAGE4_TEST_BLOCK",
&acadrust::types::Transform::identity(),
&acadrust::types::Transform::identity(),
)
.expect("create a block definition to own the constraint set");
let br_handle = scene
.document
.block_records
.get("OCS_STAGE4_TEST_BLOCK")
.expect("block record should exist")
.handle;
scene
.sketch_constraint_set_mut(SketchScope::Block(br_handle))
.add(ConstraintKind::Vertical, vec![SketchRef::whole(a)], None);
scene.materialize_sketch_constraints_for_save();
scene.sketch_constraints.clear();
scene.load_sketch_constraints_from_document();
let set = scene
.sketch_constraint_set(SketchScope::Block(br_handle))
.expect("block-scoped constraint set should have been reloaded");
assert_eq!(set.constraints.len(), 1);
assert_eq!(set.constraints[0].kind, ConstraintKind::Vertical);
}
#[test]
fn an_empty_constraint_set_is_not_materialized() {
// A scope touched (e.g. `sketch_constraint_set_mut` called once,
// then every constraint removed) but left with zero constraints
// should not persist an empty XRecord.
let mut scene = Scene::new();
let _ = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
scene.materialize_sketch_constraints_for_save();
let owner = scene.document.header.model_space_block_handle;
assert!(
scene.document.xrecord(owner, XRECORD_KEY).is_none(),
"an empty constraint set should not create an XRecord"
);
}
#[test]
fn clearing_a_previously_materialized_set_removes_its_stale_xrecord() {
// Regression for the audit-flagged case `an_empty_constraint_set_is_not_materialized`
// doesn't cover: a scope that already had a *non-empty* XRecord from a
// prior save must have that XRecord actively removed once its
// in-memory set goes back to empty — otherwise the stale blob
// survives the save and resurrects the deleted constraints on the
// next load.
let mut scene = Scene::new();
let a = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points(
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(10.0, 0.0, 0.0),
)));
scene
.sketch_constraint_set_mut(SketchScope::ModelSpace)
.add(ConstraintKind::Horizontal, vec![SketchRef::whole(a)], None);
scene.materialize_sketch_constraints_for_save();
let owner = scene.document.header.model_space_block_handle;
assert!(
scene.document.xrecord(owner, XRECORD_KEY).is_some(),
"sanity: the first save should have materialized an XRecord"
);
// Clear every constraint (mirrors deleting the constrained entity, or
// removing the last constraint by hand) and save again.
scene
.sketch_constraint_set_mut(SketchScope::ModelSpace)
.constraints
.clear();
scene.materialize_sketch_constraints_for_save();
assert!(
scene.document.xrecord(owner, XRECORD_KEY).is_none(),
"the stale XRecord from the earlier non-empty save must be removed, not left behind"
);
// And the load path must agree: nothing resurrects on reload.
scene.sketch_constraints.clear();
scene.load_sketch_constraints_from_document();
assert!(
scene
.sketch_constraint_set(SketchScope::ModelSpace)
.is_none(),
"no constraints should come back after the set was cleared and re-saved"
);
}
#[test]
fn decode_rejects_a_mismatched_format_version() {
let mut bytes = vec![FORMAT_VERSION.wrapping_add(1)];
bytes.extend(
bincode::serialize(&SketchConstraintSet::new(SketchScope::ModelSpace)).unwrap(),
);
assert!(
decode(&bytes).is_none(),
"a future/unknown format version must be rejected, not misread"
);
}
}