forked from 1b-agent/opencadstudio-upstream
Standardize parametric constraints and interoperability
This commit is contained in:
1 parent
753df67ee4
commit
d738ebfcc6
41 files changed
+10038
-7847
No files matched your search
+129
-127
@@ -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()
|
||||
|
||||
+1809
-533
File diff suppressed because it is too large.
Load diff
+202
-227
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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(_)
|
||||
)
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user