forked from 1b-agent/opencadstudio-upstream
- Gradient stops keep their DWG color book name and book; RGB wins over ACI in a DXF gradient block in either tag order. - MTEXT decimal tab stops keep their alignment character. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
274 lines
9.3 KiB
Rust
274 lines
9.3 KiB
Rust
use crate::scene::Scene;
|
|
use codec::entities::hatch::{BoundaryEdge, BoundaryPath, Hatch, PolylineEdge, SplineEdge};
|
|
use codec::types::{Vector2, Vector3};
|
|
use kernel::geom2d::{refine_spline_boundary, triangulate_rings, Curve, NurbsCurve};
|
|
use kernel::tessellation::DEFAULT_ANGLE;
|
|
|
|
#[test]
|
|
fn gradient_stops_keep_entity_transparency() {
|
|
let mut hatch = Hatch::new();
|
|
hatch.is_solid = true;
|
|
hatch
|
|
.paths
|
|
.push(rectangle([0.0, 0.0], [10.0, 10.0], [0.0; 2]));
|
|
hatch.gradient_color.enabled = true;
|
|
hatch.gradient_color.name = "LINEAR".into();
|
|
hatch.gradient_color.colors = vec![
|
|
codec::entities::hatch::GradientColorEntry {
|
|
value: 0.0,
|
|
color: codec::types::Color::Rgb { r: 255, g: 0, b: 0 },
|
|
..Default::default()
|
|
},
|
|
codec::entities::hatch::GradientColorEntry {
|
|
value: 1.0,
|
|
color: codec::types::Color::Rgb { r: 0, g: 0, b: 255 },
|
|
..Default::default()
|
|
},
|
|
];
|
|
|
|
let mut model = Scene::hatch_model_from_dxf(&hatch, [0.2, 0.3, 0.4, 0.35]).unwrap();
|
|
assert!((model.color[3] - 0.35).abs() < f32::EPSILON);
|
|
let crate::scene::model::hatch_model::HatchPattern::Gradient { color2, .. } = &model.pattern
|
|
else {
|
|
panic!("gradient hatch must produce a gradient render pattern");
|
|
};
|
|
assert!((color2[3] - 0.35).abs() < f32::EPSILON);
|
|
|
|
model.set_alpha(0.6);
|
|
assert!((model.color[3] - 0.6).abs() < f32::EPSILON);
|
|
let crate::scene::model::hatch_model::HatchPattern::Gradient { color2, .. } = &model.pattern
|
|
else {
|
|
unreachable!();
|
|
};
|
|
assert!((color2[3] - 0.6).abs() < f32::EPSILON);
|
|
}
|
|
|
|
fn narrow(clearance: f64, offset: [f64; 2]) -> NurbsCurve {
|
|
let line = |a: [f64; 2], b: [f64; 2]| {
|
|
[
|
|
a,
|
|
std::array::from_fn(|i| a[i] + (b[i] - a[i]) / 3.0),
|
|
std::array::from_fn(|i| a[i] + 2.0 * (b[i] - a[i]) / 3.0),
|
|
b,
|
|
]
|
|
};
|
|
let [a, b, c, d, e, f] = [
|
|
[-1.0, -1.0],
|
|
[0.0, -1.0],
|
|
[0.0, 1.0],
|
|
[clearance, 0.999],
|
|
[-0.5, 1.5],
|
|
[-1.0, 1.5],
|
|
];
|
|
// The returning cubic passes close to the vertical side b--c.
|
|
let spans = [
|
|
line(a, b),
|
|
line(b, c),
|
|
line(c, d),
|
|
[d, [clearance, 1.1], [-0.3, 1.5], e],
|
|
line(e, f),
|
|
line(f, a),
|
|
];
|
|
let mut controls = vec![a];
|
|
for span in spans {
|
|
controls.extend_from_slice(&span[1..]);
|
|
}
|
|
for p in &mut controls {
|
|
for i in 0..2 {
|
|
p[i] += offset[i];
|
|
}
|
|
}
|
|
let mut knots = vec![0.0; 4];
|
|
for i in 1..6 {
|
|
knots.extend([i as f64; 3]);
|
|
}
|
|
knots.extend([6.0; 4]);
|
|
NurbsCurve::new(3, controls, knots, None).unwrap()
|
|
}
|
|
|
|
fn edge(spline: NurbsCurve) -> BoundaryEdge {
|
|
BoundaryEdge::Spline(SplineEdge {
|
|
degree: spline.degree() as i32,
|
|
rational: false,
|
|
periodic: false,
|
|
knots: spline.knots().to_vec(),
|
|
control_points: spline
|
|
.control_points()
|
|
.iter()
|
|
.map(|p| Vector3::new(p[0], p[1], 0.0))
|
|
.collect(),
|
|
fit_points: Vec::new(),
|
|
start_tangent: Vector2::new(0.0, 0.0),
|
|
end_tangent: Vector2::new(0.0, 0.0),
|
|
})
|
|
}
|
|
|
|
fn rectangle(a: [f64; 2], b: [f64; 2], offset: [f64; 2]) -> BoundaryPath {
|
|
let points = [a, [b[0], a[1]], b, [a[0], b[1]]]
|
|
.map(|p| Vector2::new(p[0] + offset[0], p[1] + offset[1]));
|
|
let mut path = BoundaryPath::new();
|
|
path.add_edge(BoundaryEdge::Polyline(PolylineEdge::new(
|
|
points.to_vec(),
|
|
true,
|
|
)));
|
|
path
|
|
}
|
|
|
|
#[test]
|
|
fn narrow_fill_preserves_corners_holes_and_shared_origin() {
|
|
let curve = narrow(0.00001, [0.0; 2]);
|
|
let area: f64 = (0..6)
|
|
.map(|i| {
|
|
Curve::Nurbs(curve.trimmed(i as f64 / 6.0, (i + 1) as f64 / 6.0).unwrap())
|
|
.enclosed_area()
|
|
})
|
|
.sum();
|
|
for (offset, shuffled, multiple) in [
|
|
([0.0; 2], false, false),
|
|
([1e6, -1e6], false, false),
|
|
([0.0; 2], true, true),
|
|
([1e6, -1e6], true, true),
|
|
] {
|
|
let spline = narrow(0.00001, offset);
|
|
let mut path = BoundaryPath::new();
|
|
path.edges = if shuffled {
|
|
let (a, b) = spline.split_at(0.5).unwrap();
|
|
vec![edge(b), edge(a.reversed())]
|
|
} else {
|
|
vec![edge(spline.clone())]
|
|
};
|
|
let mut hatch = Hatch::new();
|
|
hatch.is_solid = true;
|
|
hatch.paths.push(path);
|
|
if multiple {
|
|
hatch.paths.extend([
|
|
rectangle([-0.8, -0.8], [-0.2, -0.2], offset),
|
|
rectangle([-0.6, -0.6], [-0.4, -0.4], offset),
|
|
rectangle([1.0, -1.0], [2.0, 2.5], offset),
|
|
]);
|
|
}
|
|
let model = Scene::hatch_model_from_dxf(&hatch, [1.0; 4]).unwrap();
|
|
if multiple {
|
|
assert_eq!(model.world_origin, [offset[0] + 0.5, offset[1] + 0.75]);
|
|
}
|
|
let project = |p: [f64; 2]| {
|
|
[
|
|
(p[0] - model.world_origin[0]) as f32,
|
|
(p[1] - model.world_origin[1]) as f32,
|
|
]
|
|
};
|
|
let original = Curve::Nurbs(spline.clone()).tessellate_angle(DEFAULT_ANGLE);
|
|
let original: Vec<_> = original
|
|
.into_iter()
|
|
.map(|p| project(p).map(f64::from))
|
|
.collect();
|
|
assert!(triangulate_rings(&[original]).1.is_empty());
|
|
for i in 0..=6 {
|
|
assert!(model
|
|
.boundary
|
|
.contains(&project(spline.point_at(i as f64 / 6.0))));
|
|
}
|
|
assert_eq!(model.boundary_paths.as_deref().unwrap(), &hatch.paths);
|
|
let (vertices, indices) = model.fill_mesh();
|
|
assert!(!indices.is_empty());
|
|
let mut filled_area = 0.0;
|
|
for triangle in indices.chunks_exact(3) {
|
|
let [a, b, c] = [triangle[0], triangle[1], triangle[2]]
|
|
.map(|i| vertices[i as usize].map(f64::from));
|
|
let twice_area = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
|
|
assert!(twice_area > 0.0);
|
|
filled_area += twice_area * 0.5;
|
|
if multiple {
|
|
let p =
|
|
[0, 1].map(|i| (a[i] + b[i] + c[i]) / 3.0 + model.world_origin[i] - offset[i]);
|
|
let inside = |lo, hi| p.iter().all(|&v| v > lo && v < hi);
|
|
assert!(!inside(-0.8, -0.2) || inside(-0.6, -0.4));
|
|
}
|
|
}
|
|
let expected = area + if multiple { 3.5 - 0.36 + 0.04 } else { 0.0 };
|
|
assert!(
|
|
(filled_area - expected).abs() < 0.001,
|
|
"{filled_area} vs {expected}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn valid_samples_are_unchanged_and_invalid_samples_stay_rejected() {
|
|
for clearance in [0.01, 0.000001, 0.000002, 0.000005] {
|
|
let spline = narrow(clearance, [0.0; 2]);
|
|
let original = Curve::Nurbs(spline.clone()).tessellate_angle(DEFAULT_ANGLE);
|
|
let mut path = BoundaryPath::new();
|
|
path.edges.push(edge(spline));
|
|
let mut hatch = Hatch::new();
|
|
hatch.is_solid = true;
|
|
hatch.paths.push(path);
|
|
let model = Scene::hatch_model_from_dxf(&hatch, [1.0; 4]).unwrap();
|
|
let expected: Vec<_> = original
|
|
.iter()
|
|
.map(|p| {
|
|
[
|
|
(p[0] - model.world_origin[0]) as f32,
|
|
(p[1] - model.world_origin[1]) as f32,
|
|
]
|
|
})
|
|
.collect();
|
|
assert_eq!(model.boundary.as_ref(), &expected);
|
|
assert_eq!(model.fill_mesh().1.is_empty(), clearance < 0.01);
|
|
}
|
|
let curve = Curve::Nurbs(narrow(0.01, [0.0; 2]));
|
|
assert!(refine_spline_boundary(
|
|
&curve.tessellate_angle(DEFAULT_ANGLE),
|
|
|| panic!("valid path resampled"),
|
|
|p| p
|
|
)
|
|
.is_none());
|
|
assert!(
|
|
refine_spline_boundary(&vec![[0.0; 2]; 513], || panic!("size cap ignored"), |p| p)
|
|
.is_none()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn recovery_respects_the_total_hatch_vertex_cap() {
|
|
let curve = Curve::Nurbs(narrow(0.00001, [0.0; 2]));
|
|
let original = curve.tessellate_angle(DEFAULT_ANGLE);
|
|
let Curve::Nurbs(spline) = curve.clone() else {
|
|
unreachable!()
|
|
};
|
|
let mut path = BoundaryPath::new();
|
|
path.edges.push(edge(spline));
|
|
let mut hatch = Hatch::new();
|
|
hatch.is_solid = true;
|
|
hatch.paths.push(path);
|
|
let count = 16_384 - original.len() - 2;
|
|
let points = (0..count)
|
|
.map(|i| {
|
|
let angle = std::f64::consts::TAU * i as f64 / count as f64;
|
|
Vector2::new(1.5 + 0.5 * angle.cos(), 0.75 + 1.75 * angle.sin())
|
|
})
|
|
.collect();
|
|
let mut other = BoundaryPath::new();
|
|
other.add_edge(BoundaryEdge::Polyline(PolylineEdge::new(points, true)));
|
|
hatch.paths.push(other);
|
|
let model = Scene::hatch_model_from_dxf(&hatch, [1.0; 4]).unwrap();
|
|
let project = |p: [f64; 2]| {
|
|
[
|
|
(p[0] - model.world_origin[0]) as f32 as f64,
|
|
(p[1] - model.world_origin[1]) as f32 as f64,
|
|
]
|
|
};
|
|
assert!(
|
|
refine_spline_boundary(&original, || vec![curve], project)
|
|
.unwrap()
|
|
.len()
|
|
> original.len()
|
|
);
|
|
assert_eq!(model.boundary.len(), 16_384);
|
|
let expected: Vec<_> = original
|
|
.into_iter()
|
|
.map(|p| project(p).map(|v| v as f32))
|
|
.collect();
|
|
assert_eq!(&model.boundary[..expected.len()], expected.as_slice());
|
|
}
|