forked from 1b-agent/opencadstudio-upstream
Merge viewport creation fixes and current upstream into associations
This commit is contained in:
commit
7f018ab0f1
102 files changed
+28293
-11663
No files matched your search
+7522
File diff suppressed because it is too large.
Load diff
+8
@@ -0,0 +1,8 @@
|
||||
Synthetic regression fixture for #1266. Generated with ezdxf/Pillow from basic
|
||||
geometry; it contains no external references or customer drawing content.
|
||||
|
||||
Layout1 has an INSERT of SyntheticImage at (2, 4). That block contains one
|
||||
OLE2FRAME whose compound-file CONTENTS stream embeds a 64 x 32 BMP: red on
|
||||
the left, blue on the right, with a green diagonal from top-left to bottom-right.
|
||||
The frame occupies (0, 0) to (2, 1) in the block. Text and a triangle provide
|
||||
vector controls for manually opening the fixture and checking Plot Preview.
|
||||
@@ -1,7 +1,9 @@
|
||||
use acadrust::entities::EntityType;
|
||||
use acadrust::types::{Handle, Vector3};
|
||||
use OpenCADStudio::scene::named_parameters::DrivingValue;
|
||||
use OpenCADStudio::scene::sketch_constraints::{ConstraintKind, SketchRef, SketchScope};
|
||||
use OpenCADStudio::scene::parametric_constraints::{
|
||||
ConstraintKind, ParametricRef, ParametricScope,
|
||||
};
|
||||
use OpenCADStudio::scene::{ChangeKind, Scene};
|
||||
|
||||
fn add_line(scene: &mut Scene, x1: f64, y1: f64, x2: f64, y2: f64) -> Handle {
|
||||
@@ -62,13 +64,6 @@ fn arc_geom(scene: &Scene, handle: Handle) -> (Vector3, f64) {
|
||||
}
|
||||
}
|
||||
|
||||
fn arc_angles(scene: &Scene, handle: Handle) -> (f64, f64, f64) {
|
||||
match scene.document.get_entity(handle).expect("entity exists") {
|
||||
EntityType::Arc(a) => (a.radius, a.start_angle, a.end_angle),
|
||||
other => panic!("expected an Arc, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_ellipse(scene: &mut Scene, cx: f64, cy: f64, major_axis: (f64, f64), ratio: f64) -> Handle {
|
||||
scene.add_entity(EntityType::Ellipse(
|
||||
acadrust::entities::Ellipse::from_center_axes(
|
||||
@@ -92,10 +87,10 @@ fn horizontal_constraint_levels_the_line_when_an_endpoint_moves() {
|
||||
let line = add_line(&mut scene, 0.0, 0.0, 10.0, 0.0);
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Horizontal,
|
||||
vec![SketchRef::whole(line)],
|
||||
vec![ParametricRef::whole(line)],
|
||||
None,
|
||||
);
|
||||
|
||||
@@ -118,7 +113,7 @@ fn horizontal_constraint_levels_the_line_when_an_endpoint_moves() {
|
||||
// constraint-referenced params, so an untouched one was invisible to
|
||||
// `diagnose` entirely before this was fixed).
|
||||
let dof = scene
|
||||
.sketch_constraint_set(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set(ParametricScope::ModelSpace)
|
||||
.and_then(|s| s.dof);
|
||||
assert_eq!(
|
||||
dof,
|
||||
@@ -127,6 +122,77 @@ fn horizontal_constraint_levels_the_line_when_an_endpoint_moves() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn horizontal_constraint_levels_a_polyline_segment() {
|
||||
let mut scene = Scene::new();
|
||||
let polyline = scene.add_entity(EntityType::LwPolyline(
|
||||
acadrust::entities::LwPolyline::from_points(vec![
|
||||
acadrust::types::Vector2::new(0.0, 0.0),
|
||||
acadrust::types::Vector2::new(5.0, 2.0),
|
||||
acadrust::types::Vector2::new(10.0, 7.0),
|
||||
]),
|
||||
));
|
||||
scene
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Horizontal,
|
||||
vec![ParametricRef::segment(polyline, 1)],
|
||||
None,
|
||||
);
|
||||
|
||||
scene.bump_entities(&[(polyline, ChangeKind::Modified)]);
|
||||
let EntityType::LwPolyline(polyline) = scene.document.get_entity(polyline).unwrap() else {
|
||||
panic!("expected a lightweight polyline");
|
||||
};
|
||||
assert!(
|
||||
(polyline.vertices[1].location.y - polyline.vertices[2].location.y).abs() < 1e-6,
|
||||
"the selected segment should be horizontal"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn horizontal_constraint_does_not_flatten_a_polyline_arc_segment() {
|
||||
let mut source = acadrust::entities::LwPolyline::new();
|
||||
source.add_point_with_bulge(acadrust::types::Vector2::new(0.0, 0.0), 0.5);
|
||||
source.add_point(acadrust::types::Vector2::new(5.0, 2.0));
|
||||
let mut scene = Scene::new();
|
||||
let polyline = scene.add_entity(EntityType::LwPolyline(source));
|
||||
scene
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Horizontal,
|
||||
vec![ParametricRef::segment(polyline, 0)],
|
||||
None,
|
||||
);
|
||||
|
||||
scene.bump_entities(&[(polyline, ChangeKind::Modified)]);
|
||||
let EntityType::LwPolyline(polyline) = scene.document.get_entity(polyline).unwrap() else {
|
||||
panic!("expected a lightweight polyline");
|
||||
};
|
||||
assert_eq!(polyline.vertices[0].location.y, 0.0);
|
||||
assert_eq!(polyline.vertices[1].location.y, 2.0);
|
||||
assert_eq!(polyline.vertices[0].bulge, 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn horizontal_constraint_aligns_two_selected_points() {
|
||||
let mut scene = Scene::new();
|
||||
let a = add_line(&mut scene, 0.0, 1.0, 4.0, 3.0);
|
||||
let b = add_line(&mut scene, 8.0, 7.0, 12.0, 9.0);
|
||||
scene
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Horizontal,
|
||||
vec![ParametricRef::point(a, 0), ParametricRef::point(b, 0)],
|
||||
None,
|
||||
);
|
||||
|
||||
scene.bump_entities(&[(b, ChangeKind::Modified)]);
|
||||
let (a_start, _) = line_endpoints(&scene, a);
|
||||
let (b_start, _) = line_endpoints(&scene, b);
|
||||
assert!((a_start.y - b_start.y).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parallel_constraint_rotates_the_other_line_when_one_moves() {
|
||||
let mut scene = Scene::new();
|
||||
@@ -134,10 +200,10 @@ fn parallel_constraint_rotates_the_other_line_when_one_moves() {
|
||||
let b = add_line(&mut scene, 0.0, 5.0, 10.0, 5.0);
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Parallel,
|
||||
vec![SketchRef::whole(a), SketchRef::whole(b)],
|
||||
vec![ParametricRef::whole(a), ParametricRef::whole(b)],
|
||||
None,
|
||||
);
|
||||
|
||||
@@ -163,10 +229,10 @@ fn distance_constraint_holds_the_target_length_after_an_unrelated_endpoint_edit(
|
||||
let line = add_line(&mut scene, 0.0, 0.0, 10.0, 0.0);
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Distance,
|
||||
vec![SketchRef::point(line, 0), SketchRef::point(line, 1)],
|
||||
vec![ParametricRef::point(line, 0), ParametricRef::point(line, 1)],
|
||||
Some(DrivingValue::Literal(20.0)),
|
||||
);
|
||||
|
||||
@@ -194,10 +260,10 @@ fn distance_constraint_resolves_a_named_parameter_reference() {
|
||||
.unwrap();
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Distance,
|
||||
vec![SketchRef::point(line, 0), SketchRef::point(line, 1)],
|
||||
vec![ParametricRef::point(line, 0), ParametricRef::point(line, 1)],
|
||||
Some(DrivingValue::Named("target_len".to_string())),
|
||||
);
|
||||
|
||||
@@ -236,10 +302,10 @@ fn distance_constraint_with_an_undefined_named_reference_is_skipped_not_panicked
|
||||
let line = add_line(&mut scene, 0.0, 0.0, 10.0, 0.0);
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Distance,
|
||||
vec![SketchRef::point(line, 0), SketchRef::point(line, 1)],
|
||||
vec![ParametricRef::point(line, 0), ParametricRef::point(line, 1)],
|
||||
Some(DrivingValue::Named("does_not_exist".to_string())),
|
||||
);
|
||||
|
||||
@@ -260,10 +326,10 @@ fn tangent_constraint_between_a_line_and_a_circle_solves_to_touching() {
|
||||
let line = add_line(&mut scene, -10.0, 10.0, 10.0, 10.0);
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Tangent,
|
||||
vec![SketchRef::whole(circle), SketchRef::whole(line)],
|
||||
vec![ParametricRef::whole(circle), ParametricRef::whole(line)],
|
||||
None,
|
||||
);
|
||||
scene.bump_entities(&[(circle, ChangeKind::Modified), (line, ChangeKind::Modified)]);
|
||||
@@ -284,10 +350,10 @@ fn tangent_constraint_between_two_circles_solves_to_external_tangency() {
|
||||
let b = add_circle(&mut scene, 20.0, 0.0, 2.0); // far apart: distance 20, r1+r2 = 5
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Tangent,
|
||||
vec![SketchRef::whole(a), SketchRef::whole(b)],
|
||||
vec![ParametricRef::whole(a), ParametricRef::whole(b)],
|
||||
None,
|
||||
);
|
||||
scene.bump_entities(&[(a, ChangeKind::Modified), (b, ChangeKind::Modified)]);
|
||||
@@ -309,10 +375,10 @@ fn coincident_constraint_pulls_the_second_point_onto_the_first_when_it_moves() {
|
||||
let b = add_line(&mut scene, 5.0, 0.0, 5.0, 5.0); // b.start coincident with a.end
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Coincident,
|
||||
vec![SketchRef::point(a, 1), SketchRef::point(b, 0)],
|
||||
vec![ParametricRef::point(a, 1), ParametricRef::point(b, 0)],
|
||||
None,
|
||||
);
|
||||
|
||||
@@ -335,10 +401,10 @@ fn unrelated_entity_edit_does_not_touch_constrained_geometry() {
|
||||
let unrelated = add_line(&mut scene, 100.0, 100.0, 200.0, 100.0);
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Horizontal,
|
||||
vec![SketchRef::whole(line)],
|
||||
vec![ParametricRef::whole(line)],
|
||||
None,
|
||||
);
|
||||
|
||||
@@ -363,20 +429,24 @@ fn erasing_a_constrained_entity_drops_its_constraints_instead_of_dangling() {
|
||||
let c = add_line(&mut scene, 20.0, 20.0, 30.0, 20.0); // unrelated, own constraint
|
||||
|
||||
let parallel_id = scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Parallel,
|
||||
vec![SketchRef::whole(a), SketchRef::whole(b)],
|
||||
vec![ParametricRef::whole(a), ParametricRef::whole(b)],
|
||||
None,
|
||||
);
|
||||
let horizontal_id = scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.add(ConstraintKind::Horizontal, vec![SketchRef::whole(c)], None);
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Horizontal,
|
||||
vec![ParametricRef::whole(c)],
|
||||
None,
|
||||
);
|
||||
|
||||
scene.erase_entities(&[a]);
|
||||
|
||||
let set = scene
|
||||
.sketch_constraint_set(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set(ParametricScope::ModelSpace)
|
||||
.expect("scope still exists");
|
||||
assert!(
|
||||
set.get(parallel_id).is_none(),
|
||||
@@ -404,16 +474,16 @@ fn copying_two_constrained_entities_carries_their_constraint_along() {
|
||||
let a = add_line(&mut scene, 0.0, 0.0, 10.0, 0.0);
|
||||
let b = add_line(&mut scene, 0.0, 5.0, 10.0, 3.0); // deliberately not parallel yet
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Parallel,
|
||||
vec![SketchRef::whole(a), SketchRef::whole(b)],
|
||||
vec![ParametricRef::whole(a), ParametricRef::whole(b)],
|
||||
None,
|
||||
);
|
||||
// Solve once so the source pair is actually parallel before copying.
|
||||
scene.bump_entities(&[(a, ChangeKind::Modified), (b, ChangeKind::Modified)]);
|
||||
let constraints_before = scene
|
||||
.sketch_constraint_set(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set(ParametricScope::ModelSpace)
|
||||
.unwrap()
|
||||
.constraints
|
||||
.len();
|
||||
@@ -425,7 +495,7 @@ fn copying_two_constrained_entities_carries_their_constraint_along() {
|
||||
let (new_a, new_b) = (new_handles[0], new_handles[1]);
|
||||
|
||||
let constraints_after = scene
|
||||
.sketch_constraint_set(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set(ParametricScope::ModelSpace)
|
||||
.unwrap()
|
||||
.constraints
|
||||
.len();
|
||||
@@ -463,25 +533,33 @@ fn copying_two_constrained_entities_carries_their_constraint_along() {
|
||||
// The "one edit that ripples through a constraint still records as one undo
|
||||
// step" case needs `Scene::record_undo_before`, which is `pub(crate)` (an
|
||||
// integration test crate can't reach it) — covered instead as an internal
|
||||
// unit test in `src/scene/sketch_solve.rs`.
|
||||
// unit test in `src/scene/parametric_solve.rs`.
|
||||
|
||||
#[test]
|
||||
fn a_duplicated_horizontal_constraint_is_reported_as_redundant() {
|
||||
// `solve_scope` resolves the redundant row back to the
|
||||
// `ConstraintId` a `ConflictResolverPanel` would name.
|
||||
use cadkernel_constraints::diagnosis::RedundancyKind;
|
||||
use OpenCADStudio::scene::sketch_constraints::ConstraintKind;
|
||||
use OpenCADStudio::scene::parametric_constraints::ConstraintKind;
|
||||
|
||||
let mut scene = Scene::new();
|
||||
let a = add_line(&mut scene, 0.0, 0.0, 10.0, 3.0);
|
||||
let set = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
|
||||
let first = set.add(ConstraintKind::Horizontal, vec![SketchRef::whole(a)], None);
|
||||
let second = set.add(ConstraintKind::Horizontal, vec![SketchRef::whole(a)], None); // exact duplicate
|
||||
let set = scene.parametric_constraint_set_mut(ParametricScope::ModelSpace);
|
||||
let first = set.add(
|
||||
ConstraintKind::Horizontal,
|
||||
vec![ParametricRef::whole(a)],
|
||||
None,
|
||||
);
|
||||
let second = set.add(
|
||||
ConstraintKind::Horizontal,
|
||||
vec![ParametricRef::whole(a)],
|
||||
None,
|
||||
); // exact duplicate
|
||||
|
||||
scene.bump_entities(&[(a, ChangeKind::Modified)]);
|
||||
|
||||
let set = scene
|
||||
.sketch_constraint_set(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set(ParametricScope::ModelSpace)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
set.conflicts.len(),
|
||||
@@ -499,26 +577,26 @@ fn a_duplicated_horizontal_constraint_is_reported_as_redundant() {
|
||||
#[test]
|
||||
fn two_conflicting_distance_targets_are_reported_as_conflicting() {
|
||||
use cadkernel_constraints::diagnosis::RedundancyKind;
|
||||
use OpenCADStudio::scene::sketch_constraints::ConstraintKind;
|
||||
use OpenCADStudio::scene::parametric_constraints::ConstraintKind;
|
||||
|
||||
let mut scene = Scene::new();
|
||||
let a = add_line(&mut scene, 0.0, 0.0, 10.0, 0.0);
|
||||
let set = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
|
||||
let set = scene.parametric_constraint_set_mut(ParametricScope::ModelSpace);
|
||||
let first = set.add(
|
||||
ConstraintKind::Distance,
|
||||
vec![SketchRef::point(a, 0), SketchRef::point(a, 1)],
|
||||
vec![ParametricRef::point(a, 0), ParametricRef::point(a, 1)],
|
||||
Some(DrivingValue::Literal(20.0)),
|
||||
);
|
||||
let second = set.add(
|
||||
ConstraintKind::Distance,
|
||||
vec![SketchRef::point(a, 0), SketchRef::point(a, 1)],
|
||||
vec![ParametricRef::point(a, 0), ParametricRef::point(a, 1)],
|
||||
Some(DrivingValue::Literal(50.0)),
|
||||
);
|
||||
|
||||
scene.bump_entities(&[(a, ChangeKind::Modified)]);
|
||||
|
||||
let set = scene
|
||||
.sketch_constraint_set(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set(ParametricScope::ModelSpace)
|
||||
.unwrap();
|
||||
assert_eq!(set.conflicts.len(), 1);
|
||||
let (flagged_id, kind) = set.conflicts[0];
|
||||
@@ -548,10 +626,10 @@ fn concentric_constraint_pulls_the_second_circles_center_onto_the_firsts() {
|
||||
let b = add_circle(&mut scene, 5.0, 5.0, 1.0);
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Concentric,
|
||||
vec![SketchRef::center(a), SketchRef::center(b)],
|
||||
vec![ParametricRef::center(a), ParametricRef::center(b)],
|
||||
None,
|
||||
);
|
||||
|
||||
@@ -578,10 +656,10 @@ fn center_point_constraint_pulls_a_lines_endpoint_onto_a_circles_center() {
|
||||
let line = add_line(&mut scene, 10.0, 10.0, 20.0, 20.0);
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::CenterPoint,
|
||||
vec![SketchRef::point(line, 0), SketchRef::center(circle)],
|
||||
vec![ParametricRef::point(line, 0), ParametricRef::center(circle)],
|
||||
None,
|
||||
);
|
||||
|
||||
@@ -603,10 +681,10 @@ fn colinear_constraint_pulls_the_second_line_onto_the_firsts_infinite_line() {
|
||||
let b = add_line(&mut scene, 3.0, 4.0, 7.0, 6.0); // off-axis, not on a's line
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Colinear,
|
||||
vec![SketchRef::whole(a), SketchRef::whole(b)],
|
||||
vec![ParametricRef::whole(a), ParametricRef::whole(b)],
|
||||
None,
|
||||
);
|
||||
|
||||
@@ -636,10 +714,10 @@ fn midpoint_constraint_pulls_a_point_onto_a_lines_midpoint() {
|
||||
let marker = add_line(&mut scene, 20.0, 20.0, 21.0, 21.0); // marker.start is the tracked point
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Midpoint,
|
||||
vec![SketchRef::point(marker, 0), SketchRef::whole(base)],
|
||||
vec![ParametricRef::point(marker, 0), ParametricRef::whole(base)],
|
||||
None,
|
||||
);
|
||||
|
||||
@@ -655,23 +733,75 @@ fn midpoint_constraint_pulls_a_point_onto_a_lines_midpoint() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn midpoint_constraint_supports_a_point_entity() {
|
||||
let mut scene = Scene::new();
|
||||
let base = add_line(&mut scene, 0.0, 0.0, 10.0, 6.0);
|
||||
let point = scene.add_entity(EntityType::Point(acadrust::entities::Point::at(
|
||||
Vector3::new(20.0, 20.0, 0.0),
|
||||
)));
|
||||
scene
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Midpoint,
|
||||
vec![ParametricRef::point(point, 0), ParametricRef::whole(base)],
|
||||
None,
|
||||
);
|
||||
|
||||
scene.bump_entities(&[(base, ChangeKind::Modified)]);
|
||||
let (start, end) = line_endpoints(&scene, base);
|
||||
let EntityType::Point(point) = scene.document.get_entity(point).unwrap() else {
|
||||
panic!("expected a point");
|
||||
};
|
||||
assert!((point.location.x - (start.x + end.x) / 2.0).abs() < 1e-6);
|
||||
assert!((point.location.y - (start.y + end.y) / 2.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coincident_constraint_supports_a_block_insertion_point() {
|
||||
let mut scene = Scene::new();
|
||||
let line = add_line(&mut scene, 0.0, 0.0, 10.0, 4.0);
|
||||
let insert = scene.add_entity(EntityType::Insert(acadrust::entities::Insert::new(
|
||||
"fixture",
|
||||
Vector3::new(30.0, 20.0, 0.0),
|
||||
)));
|
||||
scene
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Coincident,
|
||||
vec![
|
||||
ParametricRef::point(line, 1),
|
||||
ParametricRef::point(insert, 0),
|
||||
],
|
||||
None,
|
||||
);
|
||||
|
||||
scene.bump_entities(&[(line, ChangeKind::Modified)]);
|
||||
let (_, end) = line_endpoints(&scene, line);
|
||||
let EntityType::Insert(insert) = scene.document.get_entity(insert).unwrap() else {
|
||||
panic!("expected a block reference");
|
||||
};
|
||||
assert!((insert.insert_point.x - end.x).abs() < 1e-6);
|
||||
assert!((insert.insert_point.y - end.y).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_constraint_holds_an_entity_in_place_despite_a_connected_edit() {
|
||||
let mut scene = Scene::new();
|
||||
let fixed_line = add_line(&mut scene, 0.0, 0.0, 10.0, 0.0);
|
||||
let moving_line = add_line(&mut scene, 10.0, 0.0, 10.0, 10.0);
|
||||
|
||||
let set = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
|
||||
let set = scene.parametric_constraint_set_mut(ParametricScope::ModelSpace);
|
||||
set.add(
|
||||
ConstraintKind::Fixed,
|
||||
vec![SketchRef::whole(fixed_line)],
|
||||
vec![ParametricRef::whole(fixed_line)],
|
||||
None,
|
||||
);
|
||||
set.add(
|
||||
ConstraintKind::Coincident,
|
||||
vec![
|
||||
SketchRef::point(fixed_line, 1),
|
||||
SketchRef::point(moving_line, 0),
|
||||
ParametricRef::point(fixed_line, 1),
|
||||
ParametricRef::point(moving_line, 0),
|
||||
],
|
||||
None,
|
||||
);
|
||||
@@ -705,10 +835,13 @@ fn point_on_curve_constraint_pulls_a_point_onto_a_circles_circumference() {
|
||||
let marker = add_line(&mut scene, 100.0, 100.0, 101.0, 101.0); // marker.start is the tracked point
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::PointOnCurve,
|
||||
vec![SketchRef::point(marker, 0), SketchRef::whole(circle)],
|
||||
vec![
|
||||
ParametricRef::point(marker, 0),
|
||||
ParametricRef::whole(circle),
|
||||
],
|
||||
None,
|
||||
);
|
||||
|
||||
@@ -723,6 +856,95 @@ fn point_on_curve_constraint_pulls_a_point_onto_a_circles_circumference() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_on_curve_constraint_pulls_a_point_onto_an_ellipse() {
|
||||
let mut scene = Scene::new();
|
||||
let ellipse = add_ellipse(&mut scene, 0.0, 0.0, (5.0, 0.0), 0.6);
|
||||
let marker = add_line(&mut scene, 12.0, 8.0, 13.0, 8.0);
|
||||
|
||||
scene
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::PointOnCurve,
|
||||
vec![
|
||||
ParametricRef::point(marker, 0),
|
||||
ParametricRef::whole(ellipse),
|
||||
],
|
||||
None,
|
||||
);
|
||||
scene.bump_entities(&[(marker, ChangeKind::Modified)]);
|
||||
|
||||
let (center, major, ratio) = ellipse_geom(&scene, ellipse);
|
||||
let (point, _) = line_endpoints(&scene, marker);
|
||||
let a = major.length();
|
||||
let b = a * ratio;
|
||||
let u = major / a;
|
||||
let v = Vector3::new(-u.y, u.x, 0.0);
|
||||
let offset = point - center;
|
||||
let equation = (offset.dot(&u) / a).powi(2) + (offset.dot(&v) / b).powi(2);
|
||||
assert!(
|
||||
(equation - 1.0).abs() < 1e-5,
|
||||
"point should satisfy the solved ellipse equation: {equation}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tangent_constraint_solves_an_ellipse_and_line() {
|
||||
let mut scene = Scene::new();
|
||||
let ellipse = add_ellipse(&mut scene, 0.0, 0.0, (5.0, 0.0), 0.6);
|
||||
let line = add_line(&mut scene, -8.0, 8.0, 8.0, 8.0);
|
||||
|
||||
scene
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Tangent,
|
||||
vec![ParametricRef::whole(ellipse), ParametricRef::whole(line)],
|
||||
None,
|
||||
);
|
||||
scene.bump_entities(&[(line, ChangeKind::Modified)]);
|
||||
|
||||
let (center, major, ratio) = ellipse_geom(&scene, ellipse);
|
||||
let (start, end) = line_endpoints(&scene, line);
|
||||
let direction = (end - start).normalize();
|
||||
let normal = Vector3::new(-direction.y, direction.x, 0.0);
|
||||
let unit_major = major.normalize();
|
||||
let unit_minor = Vector3::new(-unit_major.y, unit_major.x, 0.0);
|
||||
let a = major.length();
|
||||
let b = a * ratio;
|
||||
let support =
|
||||
((a * normal.dot(&unit_major)).powi(2) + (b * normal.dot(&unit_minor)).powi(2)).sqrt();
|
||||
let distance = (center - start).dot(&normal).abs();
|
||||
assert!(
|
||||
(distance - support).abs() < 1e-5,
|
||||
"line should be tangent to the solved ellipse: distance={distance} support={support}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equal_constraint_matches_ellipse_major_axes() {
|
||||
let mut scene = Scene::new();
|
||||
let a = add_ellipse(&mut scene, 0.0, 0.0, (5.0, 0.0), 0.6);
|
||||
let b = add_ellipse(&mut scene, 20.0, 0.0, (2.0, 0.0), 0.5);
|
||||
|
||||
scene
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Equal,
|
||||
vec![ParametricRef::whole(a), ParametricRef::whole(b)],
|
||||
None,
|
||||
);
|
||||
scene.bump_entities(&[(b, ChangeKind::Modified)]);
|
||||
|
||||
let (_, major_a, _) = ellipse_geom(&scene, a);
|
||||
let (_, major_b, _) = ellipse_geom(&scene, b);
|
||||
assert!(
|
||||
(major_a.length() - major_b.length()).abs() < 1e-5,
|
||||
"ellipse major radii should match: {} vs {}",
|
||||
major_a.length(),
|
||||
major_b.length()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equal_distance_constraint_matches_a_second_point_pairs_separation() {
|
||||
let mut scene = Scene::new();
|
||||
@@ -732,14 +954,14 @@ fn equal_distance_constraint_matches_a_second_point_pairs_separation() {
|
||||
let b = add_line(&mut scene, 20.0, 20.0, 25.0, 20.0);
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::EqualDistance,
|
||||
vec![
|
||||
SketchRef::point(b, 0),
|
||||
SketchRef::point(b, 1),
|
||||
SketchRef::point(a, 0),
|
||||
SketchRef::point(a, 1),
|
||||
ParametricRef::point(b, 0),
|
||||
ParametricRef::point(b, 1),
|
||||
ParametricRef::point(a, 0),
|
||||
ParametricRef::point(a, 1),
|
||||
],
|
||||
None,
|
||||
);
|
||||
@@ -766,21 +988,25 @@ fn symmetric_constraint_mirrors_one_circles_center_across_the_axis_line() {
|
||||
// scale dividing by zero at the very first solve iteration).
|
||||
let b = add_circle(&mut scene, 8.0, 9.0, 1.0);
|
||||
|
||||
let set = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
|
||||
let set = scene.parametric_constraint_set_mut(ParametricScope::ModelSpace);
|
||||
// Pin the axis and `a` in place — otherwise they're just as free to move
|
||||
// as `b`'s center, and with only 2 equations (MidpointOnLine +
|
||||
// Perpendicular) against that many more unknowns the solver is free to
|
||||
// converge on any of infinitely many valid configurations, not
|
||||
// necessarily "only b moves, to a's exact mirror" (which is the one
|
||||
// deterministic outcome this test actually wants to check).
|
||||
set.add(ConstraintKind::Fixed, vec![SketchRef::whole(axis)], None);
|
||||
set.add(ConstraintKind::Fixed, vec![SketchRef::whole(a)], None);
|
||||
set.add(
|
||||
ConstraintKind::Fixed,
|
||||
vec![ParametricRef::whole(axis)],
|
||||
None,
|
||||
);
|
||||
set.add(ConstraintKind::Fixed, vec![ParametricRef::whole(a)], None);
|
||||
set.add(
|
||||
ConstraintKind::Symmetric,
|
||||
vec![
|
||||
SketchRef::center(a),
|
||||
SketchRef::center(b),
|
||||
SketchRef::whole(axis),
|
||||
ParametricRef::center(a),
|
||||
ParametricRef::center(b),
|
||||
ParametricRef::whole(axis),
|
||||
],
|
||||
None,
|
||||
);
|
||||
@@ -813,10 +1039,10 @@ fn radius_constraint_on_an_arc_solves_to_the_target() {
|
||||
let arc = add_arc(&mut scene, 0.0, 0.0, 3.0, 0.0, std::f64::consts::FRAC_PI_2);
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Radius,
|
||||
vec![SketchRef::whole(arc)],
|
||||
vec![ParametricRef::whole(arc)],
|
||||
Some(DrivingValue::Literal(7.5)),
|
||||
);
|
||||
scene.bump_entities(&[(arc, ChangeKind::Modified)]);
|
||||
@@ -835,10 +1061,10 @@ fn tangent_constraint_between_a_line_and_an_arc_solves_to_touching() {
|
||||
let line = add_line(&mut scene, -10.0, 10.0, 10.0, 10.0); // clear of the arc's circle
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Tangent,
|
||||
vec![SketchRef::whole(arc), SketchRef::whole(line)],
|
||||
vec![ParametricRef::whole(arc), ParametricRef::whole(line)],
|
||||
None,
|
||||
);
|
||||
scene.bump_entities(&[(arc, ChangeKind::Modified), (line, ChangeKind::Modified)]);
|
||||
@@ -858,10 +1084,10 @@ fn concentric_constraint_between_an_arc_and_a_circle_pulls_centers_together() {
|
||||
let circle = add_circle(&mut scene, 10.0, -6.0, 2.0);
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Concentric,
|
||||
vec![SketchRef::center(arc), SketchRef::center(circle)],
|
||||
vec![ParametricRef::center(arc), ParametricRef::center(circle)],
|
||||
None,
|
||||
);
|
||||
scene.bump_entities(&[(arc, ChangeKind::Modified), (circle, ChangeKind::Modified)]);
|
||||
@@ -896,10 +1122,10 @@ fn coincident_constraint_pulls_a_lines_endpoint_onto_an_arcs_start_point() {
|
||||
let line = add_line(&mut scene, 20.0, 20.0, 30.0, 30.0);
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Coincident,
|
||||
vec![SketchRef::point(arc, 0), SketchRef::point(line, 0)],
|
||||
vec![ParametricRef::point(arc, 0), ParametricRef::point(line, 0)],
|
||||
None,
|
||||
);
|
||||
|
||||
@@ -932,11 +1158,18 @@ fn an_arcs_endpoint_stays_consistent_with_its_center_radius_and_angle_after_solv
|
||||
let arc = add_arc(&mut scene, 0.0, 0.0, 5.0, 0.0, std::f64::consts::PI);
|
||||
let anchor = add_line(&mut scene, 12.0, 7.0, 12.0, 7.0);
|
||||
|
||||
let set = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
|
||||
set.add(ConstraintKind::Fixed, vec![SketchRef::whole(anchor)], None);
|
||||
let set = scene.parametric_constraint_set_mut(ParametricScope::ModelSpace);
|
||||
set.add(
|
||||
ConstraintKind::Fixed,
|
||||
vec![ParametricRef::whole(anchor)],
|
||||
None,
|
||||
);
|
||||
set.add(
|
||||
ConstraintKind::Coincident,
|
||||
vec![SketchRef::point(arc, 0), SketchRef::point(anchor, 0)],
|
||||
vec![
|
||||
ParametricRef::point(arc, 0),
|
||||
ParametricRef::point(anchor, 0),
|
||||
],
|
||||
None,
|
||||
);
|
||||
scene.bump_entities(&[(arc, ChangeKind::Modified)]);
|
||||
@@ -964,26 +1197,87 @@ fn an_arcs_endpoint_stays_consistent_with_its_center_radius_and_angle_after_solv
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_on_curve_constraint_pulls_a_point_onto_an_arcs_underlying_circle() {
|
||||
fn point_on_curve_constraint_keeps_a_point_within_an_arcs_sweep() {
|
||||
let mut scene = Scene::new();
|
||||
let arc = add_arc(&mut scene, 0.0, 0.0, 5.0, 0.0, std::f64::consts::PI);
|
||||
let marker = add_line(&mut scene, 20.0, 20.0, 21.0, 20.0);
|
||||
let marker = add_line(&mut scene, 0.0, -20.0, 1.0, -20.0);
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::PointOnCurve,
|
||||
vec![SketchRef::point(marker, 0), SketchRef::whole(arc)],
|
||||
vec![ParametricRef::point(marker, 0), ParametricRef::whole(arc)],
|
||||
None,
|
||||
);
|
||||
scene.bump_entities(&[(marker, ChangeKind::Modified)]);
|
||||
|
||||
let (center, radius) = arc_geom(&scene, arc);
|
||||
let (center, radius, start_angle, end_angle) =
|
||||
match scene.document.get_entity(arc).expect("entity exists") {
|
||||
EntityType::Arc(arc) => (arc.center, arc.radius, arc.start_angle, arc.end_angle),
|
||||
other => panic!("expected an Arc, got {other:?}"),
|
||||
};
|
||||
let (p, _) = line_endpoints(&scene, marker);
|
||||
let dist = ((p.x - center.x).powi(2) + (p.y - center.y).powi(2)).sqrt();
|
||||
assert!(
|
||||
(dist - radius).abs() < 1e-6,
|
||||
"point should land exactly on the arc's circle: dist={dist} radius={radius}"
|
||||
"point should land exactly on the arc: dist={dist} radius={radius}"
|
||||
);
|
||||
assert!(
|
||||
cadkernel::geom2d::angle_within_arc(
|
||||
(p.y - center.y).atan2(p.x - center.x),
|
||||
start_angle,
|
||||
end_angle,
|
||||
),
|
||||
"point must stay within the solved arc sweep: {p:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_on_curve_constraint_keeps_a_point_within_a_polyline_arc_segment() {
|
||||
let mut source = acadrust::entities::LwPolyline::new();
|
||||
source.add_point_with_bulge(acadrust::types::Vector2::new(0.0, 0.0), 1.0);
|
||||
source.add_point(acadrust::types::Vector2::new(10.0, 0.0));
|
||||
let mut scene = Scene::new();
|
||||
let polyline = scene.add_entity(EntityType::LwPolyline(source));
|
||||
let marker = add_line(&mut scene, 5.0, 20.0, 6.0, 20.0);
|
||||
|
||||
scene
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::PointOnCurve,
|
||||
vec![
|
||||
ParametricRef::point(marker, 0),
|
||||
ParametricRef::segment(polyline, 0),
|
||||
],
|
||||
None,
|
||||
);
|
||||
scene.bump_entities(&[(marker, ChangeKind::Modified)]);
|
||||
|
||||
let (point, _) = line_endpoints(&scene, marker);
|
||||
let EntityType::LwPolyline(polyline) = scene.document.get_entity(polyline).unwrap() else {
|
||||
panic!("expected a lightweight polyline");
|
||||
};
|
||||
let arc = cadkernel::geom2d::BulgeArc::from_bulge(
|
||||
[
|
||||
polyline.vertices[0].location.x,
|
||||
polyline.vertices[0].location.y,
|
||||
],
|
||||
[
|
||||
polyline.vertices[1].location.x,
|
||||
polyline.vertices[1].location.y,
|
||||
],
|
||||
polyline.vertices[0].bulge,
|
||||
)
|
||||
.unwrap();
|
||||
let distance = (point.x - arc.center[0]).hypot(point.y - arc.center[1]);
|
||||
assert!((distance - arc.radius).abs() < 1e-6);
|
||||
assert!(
|
||||
cadkernel::geom2d::angle_within_arc(
|
||||
(point.y - arc.center[1]).atan2(point.x - arc.center[0]),
|
||||
arc.start_angle,
|
||||
arc.start_angle + arc.sweep,
|
||||
),
|
||||
"point must stay within the solved polyline arc segment: {point:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -996,16 +1290,23 @@ fn radius_and_endpoint_coincident_constraints_coexist_on_the_same_arc() {
|
||||
let arc = add_arc(&mut scene, 0.0, 0.0, 3.0, 0.0, std::f64::consts::FRAC_PI_2);
|
||||
let anchor = add_line(&mut scene, 9.0, -4.0, 9.0, -4.0);
|
||||
|
||||
let set = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
|
||||
set.add(ConstraintKind::Fixed, vec![SketchRef::whole(anchor)], None);
|
||||
let set = scene.parametric_constraint_set_mut(ParametricScope::ModelSpace);
|
||||
set.add(
|
||||
ConstraintKind::Fixed,
|
||||
vec![ParametricRef::whole(anchor)],
|
||||
None,
|
||||
);
|
||||
set.add(
|
||||
ConstraintKind::Coincident,
|
||||
vec![SketchRef::point(arc, 0), SketchRef::point(anchor, 0)],
|
||||
vec![
|
||||
ParametricRef::point(arc, 0),
|
||||
ParametricRef::point(anchor, 0),
|
||||
],
|
||||
None,
|
||||
);
|
||||
set.add(
|
||||
ConstraintKind::Radius,
|
||||
vec![SketchRef::whole(arc)],
|
||||
vec![ParametricRef::whole(arc)],
|
||||
Some(DrivingValue::Literal(6.0)),
|
||||
);
|
||||
scene.bump_entities(&[(arc, ChangeKind::Modified)]);
|
||||
@@ -1032,10 +1333,10 @@ fn diameter_constraint_drives_the_radius_to_half_the_target() {
|
||||
let circle = add_circle(&mut scene, 0.0, 0.0, 3.0);
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Diameter,
|
||||
vec![SketchRef::whole(circle)],
|
||||
vec![ParametricRef::whole(circle)],
|
||||
Some(DrivingValue::Literal(16.0)),
|
||||
);
|
||||
scene.bump_entities(&[(circle, ChangeKind::Modified)]);
|
||||
@@ -1055,10 +1356,10 @@ fn distance_x_constraint_pins_only_the_x_component() {
|
||||
let line = add_line(&mut scene, 0.0, 0.0, 6.0, 8.0);
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::DistanceX,
|
||||
vec![SketchRef::point(line, 0), SketchRef::point(line, 1)],
|
||||
vec![ParametricRef::point(line, 0), ParametricRef::point(line, 1)],
|
||||
Some(DrivingValue::Literal(10.0)),
|
||||
);
|
||||
scene.bump_entities(&[(line, ChangeKind::Modified)]);
|
||||
@@ -1083,10 +1384,10 @@ fn normal_constraint_pulls_the_line_through_the_circles_center() {
|
||||
let line = add_line(&mut scene, -10.0, 0.0, 10.0, 0.0);
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Normal,
|
||||
vec![SketchRef::whole(circle), SketchRef::whole(line)],
|
||||
vec![ParametricRef::whole(circle), ParametricRef::whole(line)],
|
||||
None,
|
||||
);
|
||||
scene.bump_entities(&[(circle, ChangeKind::Modified), (line, ChangeKind::Modified)]);
|
||||
@@ -1102,42 +1403,6 @@ fn normal_constraint_pulls_the_line_through_the_circles_center() {
|
||||
);
|
||||
}
|
||||
|
||||
// Phase 4a of the same plan: `ArcLength` registers an arc's
|
||||
// `start_angle`/`end_angle` separately from its center/radius (which still
|
||||
// flow through the existing `EntityGeom::Circle` aliasing), so it can
|
||||
// combine with a `Radius` constraint on the same arc in one solve.
|
||||
|
||||
#[test]
|
||||
fn arc_length_constraint_drives_the_swept_length_to_the_target() {
|
||||
let mut scene = Scene::new();
|
||||
// radius 5, a 90° span (length ≈ 7.85) — deliberately not the target.
|
||||
let arc = add_arc(&mut scene, 0.0, 0.0, 5.0, 0.0, std::f64::consts::FRAC_PI_2);
|
||||
|
||||
let set = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
|
||||
set.add(
|
||||
ConstraintKind::Radius,
|
||||
vec![SketchRef::whole(arc)],
|
||||
Some(DrivingValue::Literal(5.0)),
|
||||
);
|
||||
set.add(
|
||||
ConstraintKind::ArcLength,
|
||||
vec![SketchRef::whole(arc)],
|
||||
Some(DrivingValue::Literal(10.0)),
|
||||
);
|
||||
scene.bump_entities(&[(arc, ChangeKind::Modified)]);
|
||||
|
||||
let (radius, start_angle, end_angle) = arc_angles(&scene, arc);
|
||||
assert!(
|
||||
(radius - 5.0).abs() < 1e-6,
|
||||
"Radius constraint should still hold: got {radius}"
|
||||
);
|
||||
let length = radius * (end_angle - start_angle);
|
||||
assert!(
|
||||
(length - 10.0).abs() < 1e-5,
|
||||
"swept length should equal the ArcLength target: got {length}"
|
||||
);
|
||||
}
|
||||
|
||||
// Phase 5 of the same plan: an `Ellipse` registers center/focus1/minor-
|
||||
// radius (converted from acadrust's center + major-axis-vector + ratio
|
||||
// parametrization). Only center-based whole-entity constraints are wired
|
||||
@@ -1152,10 +1417,13 @@ fn concentric_constraint_between_an_ellipse_and_a_circle_pulls_centers_together(
|
||||
let circle = add_circle(&mut scene, 10.0, -6.0, 2.0);
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Concentric,
|
||||
vec![SketchRef::center(ellipse), SketchRef::center(circle)],
|
||||
vec![
|
||||
ParametricRef::center(ellipse),
|
||||
ParametricRef::center(circle),
|
||||
],
|
||||
None,
|
||||
);
|
||||
scene.bump_entities(&[
|
||||
@@ -1178,10 +1446,13 @@ fn center_point_constraint_pulls_a_lines_endpoint_onto_an_ellipses_center() {
|
||||
let line = add_line(&mut scene, 10.0, 10.0, 20.0, 20.0);
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::CenterPoint,
|
||||
vec![SketchRef::point(line, 0), SketchRef::center(ellipse)],
|
||||
vec![
|
||||
ParametricRef::point(line, 0),
|
||||
ParametricRef::center(ellipse),
|
||||
],
|
||||
None,
|
||||
);
|
||||
|
||||
@@ -1215,10 +1486,13 @@ fn a_tilted_ellipse_round_trips_through_the_solver_without_drifting() {
|
||||
let circle = add_circle(&mut scene, 2.0, -3.0, 1.0); // already concentric — no solving needed
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Concentric,
|
||||
vec![SketchRef::center(ellipse), SketchRef::center(circle)],
|
||||
vec![
|
||||
ParametricRef::center(ellipse),
|
||||
ParametricRef::center(circle),
|
||||
],
|
||||
None,
|
||||
);
|
||||
scene.bump_entities(&[(ellipse, ChangeKind::Modified)]);
|
||||
@@ -1257,10 +1531,13 @@ fn concentric_constraint_does_not_reshape_the_ellipse_when_its_center_actually_m
|
||||
let circle = add_circle(&mut scene, 10.0, 7.0, 1.5);
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Concentric,
|
||||
vec![SketchRef::center(ellipse), SketchRef::center(circle)],
|
||||
vec![
|
||||
ParametricRef::center(ellipse),
|
||||
ParametricRef::center(circle),
|
||||
],
|
||||
None,
|
||||
);
|
||||
scene.bump_entities(&[
|
||||
@@ -1294,11 +1571,18 @@ fn fixed_constraint_holds_an_ellipse_in_place_despite_a_connected_edit() {
|
||||
let ellipse = add_ellipse(&mut scene, 0.0, 0.0, (6.0, 0.0), 0.5);
|
||||
let line = add_line(&mut scene, 0.0, 0.0, 5.0, 5.0);
|
||||
|
||||
let set = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
|
||||
set.add(ConstraintKind::Fixed, vec![SketchRef::whole(ellipse)], None);
|
||||
let set = scene.parametric_constraint_set_mut(ParametricScope::ModelSpace);
|
||||
set.add(
|
||||
ConstraintKind::Fixed,
|
||||
vec![ParametricRef::whole(ellipse)],
|
||||
None,
|
||||
);
|
||||
set.add(
|
||||
ConstraintKind::CenterPoint,
|
||||
vec![SketchRef::point(line, 0), SketchRef::center(ellipse)],
|
||||
vec![
|
||||
ParametricRef::point(line, 0),
|
||||
ParametricRef::center(ellipse),
|
||||
],
|
||||
None,
|
||||
);
|
||||
|
||||
@@ -1328,10 +1612,10 @@ fn distance_y_constraint_pins_only_the_y_component() {
|
||||
let line = add_line(&mut scene, 0.0, 0.0, 6.0, 8.0);
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::DistanceY,
|
||||
vec![SketchRef::point(line, 0), SketchRef::point(line, 1)],
|
||||
vec![ParametricRef::point(line, 0), ParametricRef::point(line, 1)],
|
||||
Some(DrivingValue::Literal(-3.0)),
|
||||
);
|
||||
scene.bump_entities(&[(line, ChangeKind::Modified)]);
|
||||
@@ -0,0 +1,512 @@
|
||||
//! #1266: decoded IMAGE/OLE content must survive the shared PDF/preview/print path.
|
||||
#![cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
use acadrust::entities::{Insert, Viewport, Wipeout};
|
||||
use acadrust::types::{Vector2, Vector3};
|
||||
use acadrust::EntityType;
|
||||
use std::sync::Arc;
|
||||
use OpenCADStudio::io::pdf_export::{
|
||||
export_pdf, export_pdf_pages, PdfPageInput, PdfPlotOptions, PlotContent, PlotGroupSplits,
|
||||
PlotImage, PlotWire,
|
||||
};
|
||||
use OpenCADStudio::scene::model::image_model::{ImageModel, ImageQuadVertex};
|
||||
use OpenCADStudio::scene::{Scene, WireModel};
|
||||
|
||||
fn embedded_scene() -> Scene {
|
||||
let mut scene = Scene::new();
|
||||
scene.document = OpenCADStudio::io::load_bytes(
|
||||
"embedded-image-print.dxf",
|
||||
include_bytes!("fixtures/embedded-image-print.dxf").to_vec(),
|
||||
)
|
||||
.expect("synthetic embedded bitmap DXF");
|
||||
scene.images =
|
||||
OpenCADStudio::scene::build_derived_caches_with_progress(&scene.document, &|_| {}, None)
|
||||
.images;
|
||||
scene.set_current_layout("Layout1".into());
|
||||
scene
|
||||
}
|
||||
|
||||
fn page(images: Vec<PlotImage>) -> PdfPageInput {
|
||||
PdfPageInput {
|
||||
content: PlotContent {
|
||||
images,
|
||||
..Default::default()
|
||||
},
|
||||
paper_w: 100.0,
|
||||
paper_h: 100.0,
|
||||
offset_x: 0.0,
|
||||
offset_y: 0.0,
|
||||
rotation_deg: 0,
|
||||
scale: 1.0,
|
||||
clip: None,
|
||||
options: PdfPlotOptions::default(),
|
||||
plot_style: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn export(pages: &[PdfPageInput]) -> Vec<u8> {
|
||||
static NEXT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"ocs1266-{}-{}.pdf",
|
||||
std::process::id(),
|
||||
NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
|
||||
));
|
||||
if let [page] = pages {
|
||||
export_pdf(page, &path).expect("export bitmap PDF");
|
||||
} else {
|
||||
export_pdf_pages(pages, &path, None).expect("export bitmap PDF pages");
|
||||
}
|
||||
let bytes = std::fs::read(&path).unwrap();
|
||||
std::fs::remove_file(path).unwrap();
|
||||
bytes
|
||||
}
|
||||
|
||||
struct Raster {
|
||||
pixels: Vec<u8>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
scale: f32,
|
||||
}
|
||||
impl Raster {
|
||||
fn from_pdf(bytes: &[u8], page: usize) -> Self {
|
||||
Self::at_scale(bytes, page, 2.0)
|
||||
}
|
||||
|
||||
fn at_scale(bytes: &[u8], page: usize, scale: f32) -> Self {
|
||||
let pdf = hayro::hayro_syntax::Pdf::new(Arc::new(bytes.to_vec())).unwrap();
|
||||
let pixmap = hayro::render(
|
||||
&pdf.pages()[page],
|
||||
&hayro::RenderCache::new(),
|
||||
&hayro::hayro_interpret::InterpreterSettings::default(),
|
||||
&hayro::RenderSettings {
|
||||
x_scale: scale,
|
||||
y_scale: scale,
|
||||
bg_color: hayro::vello_cpu::color::palette::css::WHITE,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
Self {
|
||||
pixels: pixmap.data_as_u8_slice().to_vec(),
|
||||
width: u32::from(pixmap.width()) as usize,
|
||||
height: u32::from(pixmap.height()) as usize,
|
||||
scale,
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_color(&self, x_mm: f32, y_mm: f32, expected: [u8; 3]) {
|
||||
let x = (x_mm * 72.0 / 25.4 * self.scale) as usize;
|
||||
let y = self.height - 1 - (y_mm * 72.0 / 25.4 * self.scale) as usize;
|
||||
let actual = &self.pixels[(y * self.width + x) * 4..][..3];
|
||||
assert!(
|
||||
actual
|
||||
.iter()
|
||||
.zip(expected)
|
||||
.all(|(&a, b)| a.abs_diff(b) < 25),
|
||||
"at ({x_mm}, {y_mm}): expected {expected:?}, got {actual:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn image() -> PlotImage {
|
||||
let corners = [
|
||||
[10.0, 10.0, 0.0],
|
||||
[50.0, 10.0, 0.0],
|
||||
[50.0, 50.0, 0.0],
|
||||
[10.0, 50.0, 0.0],
|
||||
];
|
||||
let uv = [[0.0, 1.0], [1.0, 1.0], [1.0, 0.0], [0.0, 0.0]];
|
||||
// Four distinct quadrants detect flips, reflections, and diagonal seams.
|
||||
let pixels = (0..32)
|
||||
.flat_map(|y| {
|
||||
(0..32).flat_map(move |x| match (x < 16, y < 16) {
|
||||
(true, true) => [255, 0, 0, 255],
|
||||
(false, true) => [0, 0, 255, 255],
|
||||
(true, false) => [0, 255, 0, 255],
|
||||
(false, false) => [255, 255, 0, 255],
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
PlotImage {
|
||||
image: ImageModel {
|
||||
render_instance: None,
|
||||
file_path: "synthetic".into(),
|
||||
pixels: Arc::new(pixels),
|
||||
width: 32,
|
||||
height: 32,
|
||||
opacity: 1.0,
|
||||
corners,
|
||||
corners_low: [[0.0; 3]; 4],
|
||||
draw_depth: 0.5,
|
||||
verts: [0, 1, 2, 0, 2, 3]
|
||||
.into_iter()
|
||||
.map(|i| ImageQuadVertex {
|
||||
pos: corners[i],
|
||||
pos_low: [0.0; 3],
|
||||
uv: uv[i],
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
clips: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_ole_inside_insert_reaches_rendered_pdf() {
|
||||
let scene = embedded_scene();
|
||||
let images = scene.paper_plot_images();
|
||||
assert_eq!(images.len(), 1);
|
||||
assert_eq!((images[0].image.width, images[0].image.height), (64, 32));
|
||||
assert!(scene
|
||||
.images
|
||||
.values()
|
||||
.any(|source| Arc::ptr_eq(&source.pixels, &images[0].image.pixels)));
|
||||
let mut input = page(images);
|
||||
input.scale = 10.0;
|
||||
let raster = Raster::from_pdf(&export(&[input]), 0);
|
||||
raster.assert_color(23.0, 42.0, [255, 0, 0]);
|
||||
raster.assert_color(37.0, 48.0, [0, 0, 255]);
|
||||
raster.assert_color(25.0, 47.5, [0, 255, 0]);
|
||||
raster.assert_color(45.0, 45.0, [255, 255, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotated_nonuniform_insert_preserves_pixel_orientation() {
|
||||
let mut scene = embedded_scene();
|
||||
let handle = scene
|
||||
.document
|
||||
.entities()
|
||||
.find_map(|e| match e {
|
||||
EntityType::Insert(i) if i.block_name == "SyntheticImage" => Some(i.common.handle),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
let Some(EntityType::Insert(insert)) = scene.document.get_entity_mut(handle) else {
|
||||
panic!()
|
||||
};
|
||||
insert.insert_point = Vector3::new(60.0, 20.0, 0.0);
|
||||
insert.rotation = std::f64::consts::FRAC_PI_2;
|
||||
insert.set_x_scale(20.0);
|
||||
insert.set_y_scale(10.0);
|
||||
let raster = Raster::from_pdf(&export(&[page(scene.paper_plot_images())]), 0);
|
||||
raster.assert_color(58.0, 26.0, [255, 0, 0]);
|
||||
raster.assert_color(52.0, 54.0, [0, 0, 255]);
|
||||
raster.assert_color(52.5, 30.0, [0, 255, 0]);
|
||||
raster.assert_color(65.0, 40.0, [255, 255, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clipping_page_transform_and_shared_resources_survive_serialization() {
|
||||
let mut first = image();
|
||||
// A visible-region triangle (IMAGE clip), plus inherited and viewport clips.
|
||||
first.image.verts.truncate(3);
|
||||
first
|
||||
.clips
|
||||
.push(vec![[15.0, 5.0], [55.0, 5.0], [55.0, 45.0], [15.0, 45.0]]);
|
||||
first
|
||||
.clips
|
||||
.push(vec![[5.0, 15.0], [45.0, 15.0], [45.0, 55.0], [5.0, 55.0]]);
|
||||
let mut second = page(vec![first.clone()]);
|
||||
second.rotation_deg = 90;
|
||||
second.scale = 0.5;
|
||||
second.offset_x = 10.0;
|
||||
second.offset_y = 10.0;
|
||||
let bytes = export(&[page(vec![first]), second]);
|
||||
let parsed = printpdf::PdfDocument::parse(
|
||||
&bytes,
|
||||
&printpdf::PdfParseOptions::default(),
|
||||
&mut Vec::new(),
|
||||
)
|
||||
.unwrap();
|
||||
let uses: Vec<_> = parsed
|
||||
.pages
|
||||
.iter()
|
||||
.flat_map(|p| p.ops.iter())
|
||||
.filter_map(|op| {
|
||||
if let printpdf::Op::UseXobject { id, .. } = op {
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
uses.len(),
|
||||
2,
|
||||
"one image draw per affine quad, without a diagonal seam"
|
||||
);
|
||||
assert_eq!(uses[0], uses[1], "pages should share one image resource");
|
||||
let raster = Raster::from_pdf(&bytes, 0);
|
||||
raster.assert_color(40.0, 20.0, [255, 255, 0]);
|
||||
for (x, y) in [(12.0, 20.0), (48.0, 20.0), (20.0, 40.0), (40.0, 12.0)] {
|
||||
raster.assert_color(x, y, [255, 255, 255]);
|
||||
}
|
||||
Raster::from_pdf(&bytes, 1).assert_color(85.0, 25.0, [255, 255, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_alpha_and_plot_transparency_are_preserved() {
|
||||
let mut plot = image();
|
||||
plot.image.opacity = 0.5;
|
||||
// Intrinsic transparent texels remain transparent with either plot setting.
|
||||
let pixels = Arc::make_mut(&mut plot.image.pixels);
|
||||
for y in 16..32 {
|
||||
for x in 0..16 {
|
||||
pixels[(y * 32 + x) * 4 + 3] = 0;
|
||||
}
|
||||
}
|
||||
let opaque = page(vec![plot.clone()]);
|
||||
let mut transparent = page(vec![plot]);
|
||||
transparent.options.transparency = true;
|
||||
let bytes = export(&[opaque, transparent]);
|
||||
let off = Raster::from_pdf(&bytes, 0);
|
||||
off.assert_color(20.0, 40.0, [255, 0, 0]);
|
||||
off.assert_color(20.0, 20.0, [255, 255, 255]);
|
||||
let on = Raster::from_pdf(&bytes, 1);
|
||||
on.assert_color(20.0, 40.0, [255, 128, 128]);
|
||||
on.assert_color(20.0, 20.0, [255, 255, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn depth_and_paper_model_groups_order_images_with_wipeouts_and_wires() {
|
||||
let mut scene = Scene::new();
|
||||
let wipeout = Wipeout::polygonal(
|
||||
&[
|
||||
Vector2::new(10.0, 30.0),
|
||||
Vector2::new(30.0, 30.0),
|
||||
Vector2::new(30.0, 50.0),
|
||||
Vector2::new(10.0, 50.0),
|
||||
],
|
||||
0.0,
|
||||
);
|
||||
scene.add_entity(EntityType::Wipeout(wipeout));
|
||||
let mut input = page(vec![image()]);
|
||||
input.content.wipeouts = scene.paper_plot_wipeouts().as_ref().clone();
|
||||
assert_eq!(input.content.wipeouts.len(), 1);
|
||||
input.content.wipeouts[0].draw_depth = 0.8;
|
||||
let mut wire = WireModel::solid(
|
||||
"over image".into(),
|
||||
vec![[35.0, 15.0, 0.0], [45.0, 15.0, 0.0]],
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
false,
|
||||
);
|
||||
wire.line_weight_px = 8.0;
|
||||
input.content.wires = Arc::new(vec![PlotWire {
|
||||
wire,
|
||||
draw_depth: 0.9,
|
||||
}]);
|
||||
let raster = Raster::from_pdf(&export(&[input]), 0);
|
||||
raster.assert_color(20.0, 40.0, [255, 255, 255]);
|
||||
raster.assert_color(40.0, 40.0, [0, 0, 255]);
|
||||
raster.assert_color(40.0, 15.0, [0, 0, 0]);
|
||||
|
||||
// First group wins by depth internally, second group still paints last.
|
||||
let mut input = page(vec![image()]);
|
||||
input.content.wipeouts = scene.paper_plot_wipeouts().as_ref().clone();
|
||||
input.content.wipeouts[0].draw_depth = 0.9;
|
||||
input.content.group_splits = PlotGroupSplits {
|
||||
wipeouts: 1,
|
||||
..Default::default()
|
||||
};
|
||||
Raster::from_pdf(&export(&[input]), 0).assert_color(20.0, 40.0, [255, 0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_projection_clips_images_and_respects_frozen_and_nonplot_layers() {
|
||||
let mut scene = embedded_scene();
|
||||
scene.set_current_layout("Model".into());
|
||||
scene.ensure_layer("Bitmap");
|
||||
let mut insert = Insert::new("SyntheticImage", Vector3::new(0.0, 0.0, 0.0));
|
||||
insert.common.layer = "Bitmap".into();
|
||||
insert.set_x_scale(20.0);
|
||||
insert.set_y_scale(20.0);
|
||||
scene.add_entity(EntityType::Insert(insert));
|
||||
scene.set_current_layout("Layout1".into());
|
||||
let mut viewport = Viewport::new();
|
||||
viewport.center = Vector3::new(50.0, 50.0, 0.0);
|
||||
viewport.width = 20.0;
|
||||
viewport.height = 20.0;
|
||||
viewport.view_height = 20.0;
|
||||
viewport.view_center = Vector3::new(20.0, 10.0, 0.0);
|
||||
viewport.id = 2;
|
||||
viewport.status.is_on = true;
|
||||
let handle = scene.add_entity(EntityType::Viewport(viewport));
|
||||
let (_, _, _, images) = scene.viewport_plot_fills();
|
||||
assert_eq!(images.len(), 1);
|
||||
let raster = Raster::from_pdf(&export(&[page(images)]), 0);
|
||||
raster.assert_color(42.0, 43.0, [255, 0, 0]);
|
||||
raster.assert_color(58.0, 57.0, [0, 0, 255]);
|
||||
raster.assert_color(35.0, 45.0, [255, 255, 255]);
|
||||
let layer = scene.document.layers.get("Bitmap").unwrap().handle;
|
||||
let Some(EntityType::Viewport(vp)) = scene.document.get_entity_mut(handle) else {
|
||||
panic!()
|
||||
};
|
||||
vp.frozen_layers.push(layer);
|
||||
assert!(scene.viewport_plot_fills().3.is_empty());
|
||||
let Some(EntityType::Viewport(vp)) = scene.document.get_entity_mut(handle) else {
|
||||
panic!()
|
||||
};
|
||||
vp.frozen_layers.clear();
|
||||
scene
|
||||
.document
|
||||
.layers
|
||||
.get_mut("Bitmap")
|
||||
.unwrap()
|
||||
.is_plottable = false;
|
||||
assert!(scene.viewport_plot_fills().3.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linked_image_uses_decoded_pixels_and_its_image_clip_without_reopening() {
|
||||
use acadrust::entities::{ClipBoundary, RasterImage};
|
||||
let path = std::env::temp_dir().join(format!("ocs1266-linked-{}.png", std::process::id()));
|
||||
let source = image();
|
||||
image::save_buffer(&path, &source.image.pixels, 32, 32, image::ColorType::Rgba8).unwrap();
|
||||
let mut raster = RasterImage::new(
|
||||
path.to_str().unwrap(),
|
||||
Vector3::new(10.0, 10.0, 0.0),
|
||||
32.0,
|
||||
32.0,
|
||||
);
|
||||
raster.u_vector = Vector3::new(1.25, 0.0, 0.0);
|
||||
raster.v_vector = Vector3::new(0.0, 1.25, 0.0);
|
||||
raster.clipping_enabled = true;
|
||||
raster.clip_boundary =
|
||||
ClipBoundary::rectangular(Vector2::new(0.0, 0.0), Vector2::new(16.0, 32.0));
|
||||
let mut scene = Scene::new();
|
||||
let handle = scene.add_entity(EntityType::RasterImage(raster));
|
||||
scene.populate_images_from_document();
|
||||
std::fs::remove_file(path).unwrap();
|
||||
let images = scene.paper_plot_images();
|
||||
assert_eq!(images.len(), 1);
|
||||
assert!(Arc::ptr_eq(
|
||||
&images[0].image.pixels,
|
||||
&scene.images[&handle].pixels
|
||||
));
|
||||
let rendered = Raster::from_pdf(&export(&[page(images)]), 0);
|
||||
rendered.assert_color(20.0, 40.0, [255, 0, 0]);
|
||||
rendered.assert_color(20.0, 20.0, [0, 255, 0]);
|
||||
rendered.assert_color(40.0, 40.0, [255, 255, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equal_sized_sources_remain_distinct_across_pages() {
|
||||
let pages = {
|
||||
let red = image();
|
||||
let mut cyan = image();
|
||||
Arc::make_mut(&mut cyan.image.pixels)
|
||||
.chunks_exact_mut(4)
|
||||
.for_each(|pixel| pixel.copy_from_slice(&[0, 255, 255, 255]));
|
||||
assert!(!Arc::ptr_eq(&red.image.pixels, &cyan.image.pixels));
|
||||
vec![
|
||||
page(vec![red.clone()]),
|
||||
page(vec![cyan.clone()]),
|
||||
page(vec![red]),
|
||||
page(vec![cyan]),
|
||||
]
|
||||
};
|
||||
let bytes = export(&pages);
|
||||
let parsed = printpdf::PdfDocument::parse(
|
||||
&bytes,
|
||||
&printpdf::PdfParseOptions::default(),
|
||||
&mut Vec::new(),
|
||||
)
|
||||
.unwrap();
|
||||
let uses: Vec<_> = parsed
|
||||
.pages
|
||||
.iter()
|
||||
.flat_map(|page| &page.ops)
|
||||
.filter_map(|op| {
|
||||
if let printpdf::Op::UseXobject { id, .. } = op {
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(uses.len(), 4);
|
||||
assert_ne!(uses[0], uses[1]);
|
||||
assert_eq!(uses[0], uses[2]);
|
||||
assert_eq!(uses[1], uses[3]);
|
||||
for (index, expected) in [[255, 0, 0], [0, 255, 255], [255, 0, 0], [0, 255, 255]]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
Raster::from_pdf(&bytes, index).assert_color(20.0, 40.0, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn affine_triangle_clip_has_no_shared_edge_seam() {
|
||||
let mut plot = image();
|
||||
Arc::make_mut(&mut plot.image.pixels)
|
||||
.chunks_exact_mut(4)
|
||||
.for_each(|pixel| pixel.copy_from_slice(&[0, 0, 0, 128]));
|
||||
let mut sheared = plot.clone();
|
||||
let transform = |[x, y, z]: [f32; 3]| [x + 0.27 * y + 5.0, 0.19 * x + y + 3.0, z];
|
||||
sheared
|
||||
.image
|
||||
.corners
|
||||
.iter_mut()
|
||||
.for_each(|p| *p = transform(*p));
|
||||
sheared
|
||||
.image
|
||||
.verts
|
||||
.iter_mut()
|
||||
.for_each(|v| v.pos = transform(v.pos));
|
||||
let pages = [page(vec![plot]), page(vec![sheared])];
|
||||
let bytes = export(&pages);
|
||||
for (index, page) in pages.iter().enumerate() {
|
||||
let raster = Raster::at_scale(&bytes, index, 8.0);
|
||||
let corners = page.content.images[0].image.corners;
|
||||
let u = [corners[1][0] - corners[0][0], corners[1][1] - corners[0][1]];
|
||||
let v = [corners[3][0] - corners[0][0], corners[3][1] - corners[0][1]];
|
||||
let determinant = u[0] * v[1] - u[1] * v[0];
|
||||
let mut checked = 0;
|
||||
for y in 0..raster.height {
|
||||
for x in 0..raster.width {
|
||||
let dx = (x as f32 + 0.5) / raster.scale * 25.4 / 72.0 - corners[0][0];
|
||||
let dy = (raster.height as f32 - y as f32 - 0.5) / raster.scale * 25.4 / 72.0
|
||||
- corners[0][1];
|
||||
let a = (dx * v[1] - dy * v[0]) / determinant;
|
||||
let b = (u[0] * dy - u[1] * dx) / determinant;
|
||||
if (0.15..0.85).contains(&a) && (0.15..0.85).contains(&b) {
|
||||
let rgb = &raster.pixels[(y * raster.width + x) * 4..][..3];
|
||||
assert!(rgb.iter().all(|&c| c.abs_diff(127) <= 2),
|
||||
"page {index}, pixel ({x}, {y}): shared-edge gap or double coverage: {rgb:?}");
|
||||
checked += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(checked > 100_000);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_bitmap_reports_an_error_without_replacing_the_pdf() {
|
||||
let path = std::env::temp_dir().join(format!("ocs1266-invalid-{}.pdf", std::process::id()));
|
||||
std::fs::write(&path, b"existing PDF").unwrap();
|
||||
let mut pixels = image();
|
||||
pixels.image.width += 1;
|
||||
let mut coordinates = image();
|
||||
coordinates.image.corners[0][0] = f32::NAN;
|
||||
let mut mapping = image();
|
||||
mapping.image.corners[2][0] += 10.0;
|
||||
mapping
|
||||
.image
|
||||
.verts
|
||||
.iter_mut()
|
||||
.for_each(|vertex| vertex.uv = [0.0, 0.0]);
|
||||
for (plot, reason) in [
|
||||
(pixels, "dimensions"),
|
||||
(coordinates, "coordinates"),
|
||||
(mapping, "texture mapping"),
|
||||
] {
|
||||
let error = export_pdf(&page(vec![plot]), &path).unwrap_err();
|
||||
assert!(
|
||||
error.contains("Page 1") && error.contains(reason),
|
||||
"{error}"
|
||||
);
|
||||
assert_eq!(std::fs::read(&path).unwrap(), b"existing PDF");
|
||||
}
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
@@ -11,7 +11,9 @@
|
||||
use acadrust::entities::{Dimension, DimensionLinear, Text};
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::EntityType;
|
||||
use OpenCADStudio::io::pdf_export::{export_pdf, PdfPlotOptions, PlotWire};
|
||||
use OpenCADStudio::io::pdf_export::{
|
||||
export_pdf, PdfPageInput, PdfPlotOptions, PlotContent, PlotWire,
|
||||
};
|
||||
use OpenCADStudio::scene::Scene;
|
||||
|
||||
#[test]
|
||||
@@ -60,49 +62,38 @@ fn text_and_dim_reach_pdf_export() {
|
||||
.collect();
|
||||
|
||||
let p_text = dir.join("with_text.pdf");
|
||||
export_pdf(
|
||||
&plot_wires,
|
||||
&[],
|
||||
&[],
|
||||
210.0,
|
||||
297.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0,
|
||||
1.0,
|
||||
None,
|
||||
&p_text,
|
||||
None,
|
||||
PdfPlotOptions::default(),
|
||||
)
|
||||
.expect("export with text");
|
||||
let mut page = PdfPageInput {
|
||||
content: PlotContent {
|
||||
wires: std::sync::Arc::new(plot_wires),
|
||||
..Default::default()
|
||||
},
|
||||
paper_w: 210.0,
|
||||
paper_h: 297.0,
|
||||
offset_x: 0.0,
|
||||
offset_y: 0.0,
|
||||
rotation_deg: 0,
|
||||
scale: 1.0,
|
||||
clip: None,
|
||||
options: PdfPlotOptions::default(),
|
||||
plot_style: None,
|
||||
};
|
||||
export_pdf(&page, &p_text).expect("export with text");
|
||||
let with_text = std::fs::read(&p_text).expect("read pdf");
|
||||
assert!(with_text.starts_with(b"%PDF"), "not a PDF");
|
||||
|
||||
let stripped: Vec<_> = plot_wires
|
||||
.into_iter()
|
||||
let stripped: Vec<_> = page
|
||||
.content
|
||||
.wires
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|mut w| {
|
||||
w.wire.text_verts.clear();
|
||||
w
|
||||
})
|
||||
.collect();
|
||||
let p_bare = dir.join("no_text.pdf");
|
||||
export_pdf(
|
||||
&stripped,
|
||||
&[],
|
||||
&[],
|
||||
210.0,
|
||||
297.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0,
|
||||
1.0,
|
||||
None,
|
||||
&p_bare,
|
||||
None,
|
||||
PdfPlotOptions::default(),
|
||||
)
|
||||
.expect("export without text");
|
||||
page.content.wires = std::sync::Arc::new(stripped);
|
||||
export_pdf(&page, &p_bare).expect("export without text");
|
||||
let no_text = std::fs::read(&p_bare).expect("read pdf");
|
||||
|
||||
assert!(
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
// Proves a SketchConstraintSet serialized into an XRecord entry
|
||||
// block record's extension dictionary, survives a real save/reload through
|
||||
// both supported file formats byte-for-byte.
|
||||
|
||||
use acadrust::entities::EntityType;
|
||||
use acadrust::objects::{XRecordEntry, XRecordValue};
|
||||
use acadrust::types::{Handle, Vector3};
|
||||
use OpenCADStudio::scene::named_parameters::DrivingValue;
|
||||
use OpenCADStudio::scene::sketch_constraints::{
|
||||
ConstraintKind, SketchConstraintSet, SketchRef, SketchScope,
|
||||
};
|
||||
use OpenCADStudio::scene::Scene;
|
||||
|
||||
const RECORD_KEY: &str = "OCS_SKETCH_CONSTRAINTS_TEST";
|
||||
|
||||
fn sample_set(scope: SketchScope) -> SketchConstraintSet {
|
||||
let mut set = SketchConstraintSet::new(scope);
|
||||
set.add(
|
||||
ConstraintKind::Coincident,
|
||||
vec![
|
||||
SketchRef::point(Handle::new(101), 0),
|
||||
SketchRef::point(Handle::new(102), 1),
|
||||
],
|
||||
None,
|
||||
);
|
||||
set.add(
|
||||
ConstraintKind::Distance,
|
||||
vec![SketchRef::whole(Handle::new(103))],
|
||||
Some(DrivingValue::Literal(25.0)),
|
||||
);
|
||||
set.add(
|
||||
ConstraintKind::Angle,
|
||||
vec![
|
||||
SketchRef::whole(Handle::new(104)),
|
||||
SketchRef::whole(Handle::new(105)),
|
||||
],
|
||||
Some(DrivingValue::Literal(45.0)),
|
||||
);
|
||||
set.add(
|
||||
ConstraintKind::Radius,
|
||||
vec![SketchRef::whole(Handle::new(106))],
|
||||
Some(DrivingValue::Literal(3.5)),
|
||||
);
|
||||
set
|
||||
}
|
||||
|
||||
/// Writes `set` into a fresh XRecord Chunk under `owner`'s extension
|
||||
/// dictionary, round-trips the whole document through `ext` (`"dxf"` or
|
||||
/// anything else for DWG), and returns the `SketchConstraintSet` read back
|
||||
/// out of the reloaded document.
|
||||
fn roundtrip_through(
|
||||
scene: &Scene,
|
||||
owner: Handle,
|
||||
set: &SketchConstraintSet,
|
||||
ext: &str,
|
||||
) -> SketchConstraintSet {
|
||||
let mut doc = scene.document.clone();
|
||||
let bytes = bincode::serialize(set).expect("serialize SketchConstraintSet");
|
||||
|
||||
doc.ensure_xrecord(owner, RECORD_KEY);
|
||||
let record = doc
|
||||
.xrecord_mut(owner, RECORD_KEY)
|
||||
.expect("xrecord_mut after ensure_xrecord");
|
||||
record
|
||||
.entries
|
||||
.push(XRecordEntry::new(310, XRecordValue::Chunk(bytes)));
|
||||
|
||||
let saved = OpenCADStudio::io::save_to_bytes(&doc, ext, doc.version)
|
||||
.unwrap_or_else(|e| panic!("save to {ext} bytes: {e}"));
|
||||
let reloaded = OpenCADStudio::io::load_bytes(&format!("roundtrip.{ext}"), saved)
|
||||
.unwrap_or_else(|e| panic!("reload {ext} bytes: {e}"));
|
||||
|
||||
let record = reloaded
|
||||
.xrecord(owner, RECORD_KEY)
|
||||
.unwrap_or_else(|| panic!("XRecord {RECORD_KEY:?} missing after {ext} round-trip"));
|
||||
let chunk = record
|
||||
.entries
|
||||
.iter()
|
||||
.find_map(|entry| match &entry.value {
|
||||
XRecordValue::Chunk(bytes) => Some(bytes.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_else(|| panic!("no Chunk entry survived the {ext} round-trip"));
|
||||
|
||||
bincode::deserialize::<SketchConstraintSet>(&chunk)
|
||||
.unwrap_or_else(|e| panic!("deserialize round-tripped {ext} blob: {e}"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sketch_constraint_set_survives_a_dxf_roundtrip() {
|
||||
let scene = Scene::new();
|
||||
let owner = scene.document.header.model_space_block_handle;
|
||||
assert!(
|
||||
!owner.is_null(),
|
||||
"a fresh document must already have a model-space block handle"
|
||||
);
|
||||
let original = sample_set(SketchScope::ModelSpace);
|
||||
|
||||
let restored = roundtrip_through(&scene, owner, &original, "dxf");
|
||||
|
||||
assert_eq!(restored.scope, original.scope);
|
||||
assert_eq!(restored.constraints.len(), original.constraints.len());
|
||||
for (r, o) in restored.constraints.iter().zip(original.constraints.iter()) {
|
||||
assert_eq!(r.id, o.id);
|
||||
assert_eq!(r.kind, o.kind);
|
||||
assert_eq!(r.refs, o.refs);
|
||||
assert_eq!(r.driving_param, o.driving_param);
|
||||
assert_eq!(r.enabled, o.enabled);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sketch_constraint_set_survives_a_dwg_roundtrip() {
|
||||
let scene = Scene::new();
|
||||
let owner = scene.document.header.model_space_block_handle;
|
||||
let original = sample_set(SketchScope::ModelSpace);
|
||||
|
||||
let restored = roundtrip_through(&scene, owner, &original, "dwg");
|
||||
|
||||
assert_eq!(restored.scope, original.scope);
|
||||
assert_eq!(restored.constraints.len(), original.constraints.len());
|
||||
for (r, o) in restored.constraints.iter().zip(original.constraints.iter()) {
|
||||
assert_eq!(r.id, o.id);
|
||||
assert_eq!(r.kind, o.kind);
|
||||
assert_eq!(r.refs, o.refs);
|
||||
assert_eq!(r.driving_param, o.driving_param);
|
||||
}
|
||||
}
|
||||
|
||||
/// A block-record-scoped set (not model space) — confirms the XRecord
|
||||
/// extension-dictionary mechanism works for an arbitrary owner handle, not
|
||||
/// just the one every document already has.
|
||||
#[test]
|
||||
fn sketch_constraint_set_survives_roundtrip_when_owned_by_a_block_record() {
|
||||
let mut scene = Scene::new();
|
||||
// A real block definition via the app's own `create_block_from_entities`
|
||||
// (needs at least one source entity) — a bare, entity-less BlockRecord
|
||||
// table entry turned out not to survive a DXF round-trip at all (the
|
||||
// writer prunes it as unreferenced), so this exercises the actual shape
|
||||
// `SketchScope::Block` will see in practice.
|
||||
let line_handle = 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),
|
||||
)));
|
||||
let br_handle = scene
|
||||
.create_block_from_entities(
|
||||
&[line_handle],
|
||||
"OCS_TEST_BLOCK",
|
||||
&acadrust::types::Transform::identity(),
|
||||
&acadrust::types::Transform::identity(),
|
||||
)
|
||||
.expect("create a block definition to own the constraint set");
|
||||
|
||||
let original = sample_set(SketchScope::Block(br_handle));
|
||||
let restored = roundtrip_through(&scene, br_handle, &original, "dxf");
|
||||
|
||||
assert_eq!(restored.scope, SketchScope::Block(br_handle));
|
||||
assert_eq!(restored.constraints.len(), original.constraints.len());
|
||||
}
|
||||
Reference in new issue
Block a user