Merge upstream main into Coincident parity

This commit is contained in:
ramox81 committed 2026-09-15 14:39:17 +03:00
commit f6d8209139
44 files changed
+11089 -884

No files matched your search

+30 -3
View File
@@ -1834,7 +1834,7 @@ impl Scene {
.map(|point| to_xy(point[0], point[1]))
.collect();
rings.push(ring);
local_rings.push(local_ring);
local_rings.push((local_ring, path));
ring_sources.push(path.boundary_handles.clone());
}
@@ -1847,7 +1847,8 @@ impl Scene {
let mut local_boundary = Vec::new();
let mut boundary_exterior = Vec::new();
let mut boundary_sources = Vec::new();
for (((ring, local_ring), sources), depth) in rings
let mut spline_paths = Vec::new();
for (((ring, (local_ring, path)), sources), depth) in rings
.into_iter()
.zip(local_rings)
.zip(ring_sources)
@@ -1865,8 +1866,14 @@ impl Scene {
boundary.push([f64::NAN, f64::NAN]);
local_boundary.push([f32::NAN, f32::NAN]);
}
let range = boundary.len()..boundary.len() + ring.len();
boundary.extend(ring);
local_boundary.extend(local_ring.into_iter().map(|[x, y]| [x as f32, y as f32]));
local_boundary.extend(local_ring.iter().map(|&[x, y]| [x as f32, y as f32]));
if path.edges.iter().any(|edge| {
matches!(edge, acadrust::entities::BoundaryEdge::Spline(_))
}) {
spline_paths.push((path, local_ring, range));
}
boundary_exterior.push(depth == 0);
boundary_sources.push(sources);
}
@@ -2068,6 +2075,26 @@ impl Scene {
}
}
}
let project = |point: [f64; 2]| {
let [x, y] = to_xy(point[0], point[1]);
[(x - world_origin[0]) as f32 as f64, (y - world_origin[1]) as f32 as f64]
};
for (path, ring, range) in spline_paths.into_iter().rev() {
if range.end > boundary.len() {
continue;
}
let curves = || path.edges.iter().filter_map(crate::entities::hatch::edge_curve).collect();
if let Some(refined) =
cadkernel::geom2d::refine_spline_boundary(&ring, curves, &project)
{
if boundary.len() - range.len() + refined.len() > MAX_HATCH_MODEL_VERTS {
continue;
}
boundary.splice(range.clone(), refined.iter().map(|p| to_xy(p[0], p[1])));
local_boundary.splice(range, refined.into_iter().map(|[x, y]| [x as f32, y as f32]));
}
}
let boundary_f32: Vec<[f32; 2]> = boundary
.iter()
.map(|&[x, y]| {
+2
View File
@@ -0,0 +1,2 @@
#[cfg(test)]
mod tests;
+236
View File
@@ -0,0 +1,236 @@
use crate::scene::Scene;
use acadrust::entities::hatch::{BoundaryEdge, BoundaryPath, Hatch, PolylineEdge, SplineEdge};
use acadrust::types::{Vector2, Vector3};
use cadkernel::geom2d::{refine_spline_boundary, triangulate_rings, Curve, NurbsCurve};
use cadkernel::tessellation::DEFAULT_ANGLE;
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());
}
+99 -7
View File
@@ -33,6 +33,8 @@ pub(crate) mod centermark;
pub(crate) mod dimension_assoc;
mod dwg_native_constraints;
mod entity;
#[cfg(test)]
mod hatch_boundary;
mod group_layer;
mod layout;
mod limits;
@@ -1679,6 +1681,8 @@ pub struct Scene {
/// Entity drawn with the selection-highlight colour without being part
/// of the real selection — used to preview a row in the cycling list box.
pub hover_highlight: Option<Handle>,
/// All entities related to the constraint indicator currently under the cursor.
constraint_hover_highlights: HashSet<Handle>,
/// Color used to draw the selection highlight overlay wires (SELECTIONEFFECTCOLOR).
pub selection_color: [f32; 4],
/// Whether selection visual effect (glow/highlight) is enabled (SELECTIONEFFECT).
@@ -1959,6 +1963,11 @@ pub struct Scene {
parametric_constraints::ParametricScope,
parametric_constraints::ConstraintId,
)>,
/// Constraint glyphs explicitly shown through the constraint-bar commands.
shown_parametric_constraints: HashSet<(
parametric_constraints::ParametricScope,
parametric_constraints::ConstraintId,
)>,
/// Document-wide named-parameter and expression table decoded from
/// standard associative variables.
pub(crate) named_parameters: named_parameters::ParameterTable,
@@ -2203,6 +2212,7 @@ impl Scene {
command_preview_hidden: HashSet::default(),
refedit_keep: None,
hover_highlight: None,
constraint_hover_highlights: HashSet::default(),
selection_color: crate::scene::model::wire_model::WireModel::SELECTED,
selection_effect: true,
transparency_display: true,
@@ -2277,6 +2287,7 @@ impl Scene {
associative_hatch_source_cache: RefCell::new(None),
parametric_constraints: Vec::new(),
hidden_parametric_constraints: HashSet::default(),
shown_parametric_constraints: HashSet::default(),
named_parameters: named_parameters::ParameterTable::new(),
has_associative_centers: std::cell::Cell::new(None),
block_defn_cache: RefCell::new(HashMap::default()),
@@ -2836,13 +2847,32 @@ impl Scene {
}
pub fn bump_entities(&mut self, changes: &[(Handle, ChangeKind)]) {
self.bump_entities_with_parametric_driven(changes, &[]);
self.bump_entities_with_parametric_policy(changes, &[], false);
}
pub fn bump_entities_with_parametric_driven(
&mut self,
changes: &[(Handle, ChangeKind)],
driven_refs: &[parametric_constraints::ParametricRef],
) {
self.bump_entities_with_parametric_policy(changes, driven_refs, false);
}
pub fn bump_entities_with_parametric_policy(
&mut self,
changes: &[(Handle, ChangeKind)],
driven_refs: &[parametric_constraints::ParametricRef],
retain_size: bool,
) {
self.bump_entities_with_parametric_originals(changes, driven_refs, retain_size, &[]);
}
pub fn bump_entities_with_parametric_originals(
&mut self,
changes: &[(Handle, ChangeKind)],
driven_refs: &[parametric_constraints::ParametricRef],
retain_size: bool,
retained_originals: &[(Handle, EntityType)],
) {
if changes.iter().any(|(handle, kind)| {
matches!(kind, ChangeKind::Removed)
@@ -2877,7 +2907,12 @@ impl Scene {
}
}
if !self.parametric_constraints.is_empty() {
for change in self.refresh_parametric_constraints_with_driven(&changes, driven_refs) {
for change in self.refresh_parametric_constraints_with_originals(
&changes,
driven_refs,
retain_size,
retained_originals,
) {
if !changes.iter().any(|(handle, _)| *handle == change.0) {
changes.push(change);
}
@@ -5081,9 +5116,20 @@ impl Scene {
/// remains the picked member for hit-test/UI bookkeeping; rendering expands
/// it to the same selectable group that a click would select.
pub fn hover_highlight_handles(&self) -> HashSet<Handle> {
self.hover_highlight
let mut handles = self
.hover_highlight
.map(|handle| self.handles_expanded_for_selectable_groups(&[handle]))
.unwrap_or_default()
.unwrap_or_default();
handles.extend(self.constraint_hover_highlights.iter().copied());
handles
}
pub fn set_constraint_hover_highlights(&mut self, handles: &[Handle]) {
let desired: HashSet<_> = handles.iter().copied().collect();
if desired != self.constraint_hover_highlights {
self.constraint_hover_highlights = desired;
self.bump_selection();
}
}
/// Keep the current selection visible and temporarily filter every other
@@ -6661,6 +6707,49 @@ impl Scene {
all_visible: bool,
viewport: Option<Handle>,
) -> Vec<ImageModel> {
self.collect_images(target_block, frozen, annotation_scale_handle, all_visible,
viewport, false, |image, _| image)
}
pub fn paper_plot_images(&self) -> Vec<crate::io::pdf_export::PlotImage> {
let scale = if self.current_layout == "Model" {
self.displayed_annotation_scale_handle()
} else {
self.paper_annotation_scale_handle()
};
self.placed_images(self.current_layout_block_handle(), None,
scale, self.annotation_all_visible(), None, true)
}
fn placed_images(
&self,
target_block: Handle,
frozen: Option<&HashSet<Handle>>,
annotation_scale_handle: Option<Handle>,
all_visible: bool,
viewport: Option<Handle>,
plotting: bool,
) -> Vec<crate::io::pdf_export::PlotImage> {
self.collect_images(target_block, frozen, annotation_scale_handle, all_visible,
viewport, plotting, |image, context| crate::io::pdf_export::PlotImage {
image,
clips: context.clips.clone(),
})
}
fn collect_images<T>(
&self,
target_block: Handle,
frozen: Option<&HashSet<Handle>>,
annotation_scale_handle: Option<Handle>,
all_visible: bool,
viewport: Option<Handle>,
plotting: bool,
mut collect: impl FnMut(ImageModel, &render_graph::InstanceContext) -> T,
) -> Vec<T> {
if self.images.is_empty() {
return Vec::new();
}
let depth_map = self.draw_depth_map();
let graph = render_graph::RenderSceneGraph::new(
&self.document,
@@ -6686,7 +6775,10 @@ impl Scene {
graph.walk_root(
root,
|entity, context| {
context.is_instanced() || !self.entity_temporarily_hidden(entity.common().handle)
(!plotting || self.document.layers.get(&entity.common().layer)
.is_none_or(|layer| layer.is_plottable))
&& (context.is_instanced()
|| !self.entity_temporarily_hidden(entity.common().handle))
},
|entity, context| {
let handle = entity.common().handle;
@@ -6745,7 +6837,7 @@ impl Scene {
});
}
placed.draw_depth = context.draw_depth(handle, depth_map.as_ref());
models.push(placed);
models.push(collect(placed, context));
},
);
models
@@ -12499,7 +12591,7 @@ mod layout_cache_tests {
let mut arc = acadrust::entities::Arc::default();
arc.radius = 25.0;
arc.start_angle = 0.0;
arc.end_angle = 3.14159;
arc.end_angle = std::f64::consts::PI;
let a_h = s.add_entity(EntityType::Arc(arc));
let line = acadrust::entities::Line::from_points(
+269 -38
View File
@@ -83,6 +83,45 @@ impl ParametricRef {
}
}
/// Point reference driven by a native entity grip. Segment midpoint and
/// scalar radius grips deliberately return None because they do not identify
/// one stored point parameter.
pub(crate) fn driven_ref_for_grip(
entity: &acadrust::EntityType,
handle: Handle,
grip_id: usize,
) -> Option<ParametricRef> {
match entity {
acadrust::EntityType::Line(_) if grip_id <= 1 => {
Some(ParametricRef::point(handle, grip_id as i32))
}
acadrust::EntityType::LwPolyline(polyline) if grip_id < polyline.vertices.len() => {
Some(ParametricRef::point(handle, grip_id as i32))
}
acadrust::EntityType::Polyline2D(polyline) if grip_id < polyline.vertices.len() => {
Some(ParametricRef::point(handle, grip_id as i32))
}
acadrust::EntityType::Arc(_) => match grip_id {
0 => Some(ParametricRef::center(handle)),
1 => Some(ParametricRef::point(handle, 0)),
2 => Some(ParametricRef::point(handle, 1)),
_ => None,
},
acadrust::EntityType::Circle(_) if grip_id == 0 => {
Some(ParametricRef::center(handle))
}
acadrust::EntityType::Point(_)
| acadrust::EntityType::Insert(_)
| acadrust::EntityType::Text(_)
| acadrust::EntityType::MText(_)
if grip_id == 0 =>
{
Some(ParametricRef::point(handle, 0))
}
_ => None,
}
}
/// The friendly, user-facing constraint types — the "what button did they
/// click" vocabulary, one layer above the `cadkernel_constraints` primitives each maps
/// onto (that mapping is `constraint_map`, a later stage; see the design
@@ -803,6 +842,19 @@ impl super::Scene {
!self.hidden_parametric_constraints.contains(&(scope, id))
}
pub fn should_display_parametric_constraint(
&self,
scope: ParametricScope,
id: ConstraintId,
related_entity_selected: bool,
display_mode: i16,
) -> bool {
self.is_parametric_constraint_visible(scope, id)
&& (self.shown_parametric_constraints.contains(&(scope, id))
|| display_mode & 1 != 0
|| (display_mode & 2 != 0 && related_entity_selected))
}
pub fn set_parametric_constraint_visibility(
&mut self,
scope: ParametricScope,
@@ -828,8 +880,10 @@ impl super::Scene {
for id in &ids {
if visible {
self.hidden_parametric_constraints.remove(&(scope, *id));
self.shown_parametric_constraints.insert((scope, *id));
} else {
self.hidden_parametric_constraints.insert((scope, *id));
self.shown_parametric_constraints.remove(&(scope, *id));
}
}
ids.len()
@@ -840,40 +894,155 @@ impl super::Scene {
&self,
scope: ParametricScope,
handles: &[Handle],
settings: &crate::app::settings::AutoConstrainSettings,
) -> Vec<(ConstraintKind, Vec<ParametricRef>)> {
use cadkernel::geom2d::{
infer_constraints, Arc, Circle, ConstraintEndpoint, InferredConstraint, Line,
ParametricPrimitive, Tolerance,
infer_constraints_with_settings, Arc, Circle, ConstraintEndpoint, InferenceKind,
InferenceSettings, InferredConstraint, Line, ParametricPrimitive,
};
struct Source {
handle: Handle,
primitive: ParametricPrimitive,
whole: ParametricRef,
endpoints: [ParametricRef; 2],
}
let mut sources = Vec::new();
for handle in handles {
let primitive = match self.document.get_entity(*handle) {
Some(acadrust::EntityType::Line(line)) => ParametricPrimitive::Line(Line {
start: [line.start.x, line.start.y],
end: [line.end.x, line.end.y],
match self.document.get_entity(*handle) {
Some(acadrust::EntityType::Line(line)) => sources.push(Source {
handle: *handle,
primitive: ParametricPrimitive::Line(Line {
start: [line.start.x, line.start.y],
end: [line.end.x, line.end.y],
}),
whole: ParametricRef::whole(*handle),
endpoints: [
ParametricRef::point(*handle, 0),
ParametricRef::point(*handle, 1),
],
}),
Some(acadrust::EntityType::Circle(circle)) => ParametricPrimitive::Circle(Circle {
centre: [circle.center.x, circle.center.y],
radius: circle.radius,
Some(acadrust::EntityType::Circle(circle)) => sources.push(Source {
handle: *handle,
primitive: ParametricPrimitive::Circle(Circle {
centre: [circle.center.x, circle.center.y],
radius: circle.radius,
}),
whole: ParametricRef::whole(*handle),
endpoints: [
ParametricRef::center(*handle),
ParametricRef::center(*handle),
],
}),
Some(acadrust::EntityType::Arc(arc)) => ParametricPrimitive::Arc(Arc {
centre: [arc.center.x, arc.center.y],
radius: arc.radius,
start_angle: arc.start_angle,
end_angle: arc.end_angle,
Some(acadrust::EntityType::Arc(arc)) => sources.push(Source {
handle: *handle,
primitive: ParametricPrimitive::Arc(Arc {
centre: [arc.center.x, arc.center.y],
radius: arc.radius,
start_angle: arc.start_angle,
end_angle: arc.end_angle,
}),
whole: ParametricRef::whole(*handle),
endpoints: [
ParametricRef::point(*handle, 0),
ParametricRef::point(*handle, 1),
],
}),
Some(acadrust::EntityType::LwPolyline(polyline)) => {
let Some(world) = crate::entities::curve::lwpolyline_world_xy(polyline) else {
continue;
};
let count = world.vertices.len();
for index in 0..count {
let next = index + 1;
if next >= count && !world.is_closed {
break;
}
if world.vertices[index].bulge.abs() > 1e-9 {
continue;
}
let a = world.vertices[index].location;
let b = world.vertices[next % count].location;
sources.push(Source {
handle: *handle,
primitive: ParametricPrimitive::Line(Line {
start: [a.x, a.y],
end: [b.x, b.y],
}),
whole: ParametricRef::segment(*handle, index),
endpoints: [
ParametricRef::point(*handle, index as i32),
ParametricRef::point(*handle, (next % count) as i32),
],
});
}
}
Some(acadrust::EntityType::Polyline2D(polyline)) => {
let count = polyline.vertices.len();
for index in 0..count {
let next = index + 1;
if next >= count && !polyline.is_closed() {
break;
}
if polyline.vertices[index].bulge.abs() > 1e-9 {
continue;
}
let a = polyline.vertices[index].location;
let b = polyline.vertices[next % count].location;
sources.push(Source {
handle: *handle,
primitive: ParametricPrimitive::Line(Line {
start: [a.x, a.y],
end: [b.x, b.y],
}),
whole: ParametricRef::segment(*handle, index),
endpoints: [
ParametricRef::point(*handle, index as i32),
ParametricRef::point(*handle, (next % count) as i32),
],
});
}
}
_ => continue,
};
sources.push((*handle, primitive));
}
}
let primitives: Vec<_> = sources.iter().map(|(_, primitive)| *primitive).collect();
let primitives: Vec<_> = sources.iter().map(|source| source.primitive).collect();
let marker = |endpoint| match endpoint {
ConstraintEndpoint::Start => 0,
ConstraintEndpoint::End => 1,
ConstraintEndpoint::Start => 0usize,
ConstraintEndpoint::End => 1usize,
};
let kind = |kind| match kind {
crate::app::settings::AutoConstraintKind::Coincident => InferenceKind::Coincident,
crate::app::settings::AutoConstraintKind::Collinear => InferenceKind::Collinear,
crate::app::settings::AutoConstraintKind::Parallel => InferenceKind::Parallel,
crate::app::settings::AutoConstraintKind::Perpendicular => {
InferenceKind::Perpendicular
}
crate::app::settings::AutoConstraintKind::Tangent => InferenceKind::Tangent,
crate::app::settings::AutoConstraintKind::Concentric => InferenceKind::Concentric,
crate::app::settings::AutoConstraintKind::Horizontal => InferenceKind::Horizontal,
crate::app::settings::AutoConstraintKind::Vertical => InferenceKind::Vertical,
crate::app::settings::AutoConstraintKind::Equal => InferenceKind::Equal,
};
let inference_settings = InferenceSettings {
priority: settings
.priority
.iter()
.copied()
.filter(|candidate| settings.enabled.contains(candidate))
.map(kind)
.collect(),
distance_tolerance: settings.distance_tolerance,
angle_tolerance_radians: settings.angle_tolerance_deg.to_radians(),
tangent_must_share_point: settings.tangent_must_share_point,
perpendicular_must_intersect: settings.perpendicular_must_intersect,
};
let mut mapped: Vec<_> =
infer_constraints(&primitives, Tolerance::new(1e-6), 0.5_f64.to_radians())
infer_constraints_with_settings(&primitives, &inference_settings)
.into_iter()
.filter(|relation| {
!matches!(relation, InferredConstraint::Coincident { first, second, .. }
if sources[*first].handle == sources[*second].handle)
})
.map(|relation| match relation {
InferredConstraint::Coincident {
first,
@@ -883,53 +1052,57 @@ impl super::Scene {
} => (
ConstraintKind::Coincident,
vec![
ParametricRef::point(sources[first].0, marker(first_endpoint)),
ParametricRef::point(sources[second].0, marker(second_endpoint)),
sources[first].endpoints[marker(first_endpoint)],
sources[second].endpoints[marker(second_endpoint)],
],
),
InferredConstraint::Collinear { first, second } => (
ConstraintKind::Colinear,
vec![
ParametricRef::whole(sources[first].0),
ParametricRef::whole(sources[second].0),
sources[first].whole,
sources[second].whole,
],
),
InferredConstraint::Concentric { first, second } => (
ConstraintKind::Concentric,
vec![
ParametricRef::center(sources[first].0),
ParametricRef::center(sources[second].0),
ParametricRef::center(sources[first].handle),
ParametricRef::center(sources[second].handle),
],
),
InferredConstraint::Parallel { first, second } => (
ConstraintKind::Parallel,
vec![
ParametricRef::whole(sources[first].0),
ParametricRef::whole(sources[second].0),
sources[first].whole,
sources[second].whole,
],
),
InferredConstraint::Perpendicular { first, second } => (
ConstraintKind::Perpendicular,
vec![
ParametricRef::whole(sources[first].0),
ParametricRef::whole(sources[second].0),
sources[first].whole,
sources[second].whole,
],
),
InferredConstraint::Horizontal { entity } => (
ConstraintKind::Horizontal,
vec![ParametricRef::whole(sources[entity].0)],
vec![sources[entity].whole],
),
InferredConstraint::Vertical { entity } => (
ConstraintKind::Vertical,
vec![ParametricRef::whole(sources[entity].0)],
vec![sources[entity].whole],
),
InferredConstraint::Tangent { first, second } => (
ConstraintKind::Tangent,
vec![
ParametricRef::whole(sources[first].0),
ParametricRef::whole(sources[second].0),
sources[first].whole,
sources[second].whole,
],
),
InferredConstraint::Equal { first, second } => (
ConstraintKind::Equal,
vec![sources[first].whole, sources[second].whole],
),
})
.collect();
if let Some(existing) = self.parametric_constraint_set(scope) {
@@ -1095,6 +1268,7 @@ impl super::Scene {
scope: ParametricScope,
vp_size: (f32, f32),
show_values: bool,
display_mode: i16,
) -> Vec<(ConstraintId, iced::Point, [f32; 2], String, bool)> {
let Some(set) = self.parametric_constraint_set(scope) else {
return Vec::new();
@@ -1116,7 +1290,13 @@ impl super::Scene {
set.constraints
.iter()
.filter(|c| c.enabled)
.filter(|c| self.is_parametric_constraint_visible(scope, c.id))
.filter(|c| {
let selected = c
.refs
.iter()
.any(|reference| self.selected.contains(&reference.entity));
self.should_display_parametric_constraint(scope, c.id, selected, display_mode)
})
.filter_map(|c| {
let (anchor, outward) = glyph_placement(&self.document, c)?;
let screen = crate::scene::pick::grip::project_rte(
@@ -1158,9 +1338,15 @@ impl super::Scene {
scope: ParametricScope,
vp_size: (f32, f32),
show_values: bool,
display_mode: i16,
p: iced::Point,
) -> Option<ConstraintId> {
let placements = self.constraint_glyph_placements_screen(scope, vp_size, show_values);
let placements = self.constraint_glyph_placements_screen(
scope,
vp_size,
show_values,
display_mode,
);
let glyphs: Vec<(iced::Point, [f32; 2], String, bool)> = placements
.iter()
.map(|(_, point, direction, label, is_conflicting)| {
@@ -1447,6 +1633,33 @@ mod tests {
);
}
#[test]
fn arc_grips_drive_center_start_and_end_but_not_midpoint() {
let handle = h(8);
let arc = acadrust::EntityType::Arc(acadrust::entities::Arc::from_coords(
0.0,
0.0,
0.0,
5.0,
0.0,
std::f64::consts::PI,
));
assert_eq!(
driven_ref_for_grip(&arc, handle, 0),
Some(ParametricRef::center(handle))
);
assert_eq!(
driven_ref_for_grip(&arc, handle, 1),
Some(ParametricRef::point(handle, 0))
);
assert_eq!(
driven_ref_for_grip(&arc, handle, 2),
Some(ParametricRef::point(handle, 1))
);
assert_eq!(driven_ref_for_grip(&arc, handle, 3), None);
}
#[test]
fn parameter_usage_finds_every_constraint_driven_by_the_named_parameter() {
let mut scene = super::super::Scene::new();
@@ -1555,7 +1768,11 @@ mod tests {
);
let inferred =
scene.inferred_parametric_constraints(ParametricScope::ModelSpace, &[first, second]);
scene.inferred_parametric_constraints(
ParametricScope::ModelSpace,
&[first, second],
&crate::app::settings::AutoConstrainSettings::default(),
);
assert!(!inferred.iter().any(|(kind, refs)| {
*kind == ConstraintKind::Horizontal && *refs == [ParametricRef::whole(first)]
@@ -1566,7 +1783,7 @@ mod tests {
assert!(inferred
.iter()
.any(|(kind, _)| *kind == ConstraintKind::Coincident));
assert!(inferred
assert!(!inferred
.iter()
.any(|(kind, _)| *kind == ConstraintKind::Colinear));
}
@@ -1604,4 +1821,18 @@ mod tests {
assert!(!scene.is_parametric_constraint_visible(ParametricScope::ModelSpace, geometric));
assert!(scene.is_parametric_constraint_visible(ParametricScope::ModelSpace, dimensional));
}
#[test]
fn hidden_constraint_stays_hidden_when_related_entity_is_selected() {
let mut scene = super::super::Scene::new();
let scope = ParametricScope::ModelSpace;
let id = scene.parametric_constraint_set_mut(scope).add(
ConstraintKind::Horizontal,
vec![ParametricRef::whole(h(1))],
None,
);
scene.hidden_parametric_constraints.insert((scope, id));
assert!(!scene.should_display_parametric_constraint(scope, id, true, 2));
}
}
+180 -5
View File
@@ -1692,6 +1692,44 @@ type SolveResult = (
)>,
);
fn retained_line_length(entity: &EntityType, segment: Option<usize>) -> Option<f64> {
match (entity, segment) {
(EntityType::Line(line), None) => Some((line.end - line.start).length()),
(EntityType::LwPolyline(polyline), Some(index)) => {
let world = crate::entities::curve::lwpolyline_world_xy(polyline)?;
let a = world.vertices.get(index)?.location;
let b = if let Some(vertex) = world.vertices.get(index + 1) {
vertex.location
} else if world.is_closed {
world.vertices.first()?.location
} else {
return None;
};
Some((b - a).length())
}
(EntityType::Polyline2D(polyline), Some(index)) => {
let a = polyline.vertices.get(index)?.location;
let b = if let Some(vertex) = polyline.vertices.get(index + 1) {
vertex.location
} else if polyline.is_closed() {
polyline.vertices.first()?.location
} else {
return None;
};
Some((b - a).length())
}
_ => None,
}
}
fn retained_radius(entity: &EntityType) -> Option<f64> {
match entity {
EntityType::Circle(circle) => Some(circle.radius),
EntityType::Arc(arc) => Some(arc.radius),
_ => None,
}
}
/// Rebuilds `set`'s entire `cadkernel_constraints::System` from current document
/// geometry, solves it, and returns the resulting entity states for every
/// handle whose registered coordinates actually moved beyond floating-point
@@ -1708,6 +1746,8 @@ fn solve_scope(
drawing_params: &ParameterTable,
set: &ParametricConstraintSet,
driven_refs: &[ParametricRef],
retain_size: bool,
retained_before: &HashMap<Handle, std::sync::Arc<EntityType>>,
) -> Option<SolveResult> {
let params = if set.local_parameters.is_empty() {
drawing_params
@@ -1741,6 +1781,82 @@ fn solve_scope(
return None;
}
// Preserve current line lengths and radii for this solve when
// CONSTRAINTSOLVEMODE requests it. These temporary equations guide the
// solution without becoming persistent dimensional constraints.
if retain_size {
for (handle, geom) in &cache {
match geom {
EntityGeom::Line(line) => {
let current_length = {
let store = sys.store();
let dx = store.get(line.p2.x) - store.get(line.p1.x);
let dy = store.get(line.p2.y) - store.get(line.p1.y);
(dx * dx + dy * dy).sqrt()
};
let length = retained_before
.get(handle)
.and_then(|entity| retained_line_length(entity, None))
.unwrap_or(current_length);
let target = sys.add_param(length, true);
sys.add_constraint(Rc::new(P2PDistance::new(line.p1, line.p2, target)));
}
EntityGeom::Polyline {
points,
straight,
closed,
..
} => {
for index in 0..straight.len() {
if !straight[index] {
continue;
}
let Some(a) = points.get(index).copied() else {
continue;
};
let b = if let Some(point) = points.get(index + 1).copied() {
Some(point)
} else if *closed {
points.first().copied()
} else {
None
};
let Some(b) = b else { continue };
let current_length = {
let store = sys.store();
let dx = store.get(b.x) - store.get(a.x);
let dy = store.get(b.y) - store.get(a.y);
(dx * dx + dy * dy).sqrt()
};
let length = retained_before
.get(handle)
.and_then(|entity| retained_line_length(entity, Some(index)))
.unwrap_or(current_length);
let target = sys.add_param(length, true);
sys.add_constraint(Rc::new(P2PDistance::new(a, b, target)));
}
}
EntityGeom::Circle(circle) => {
let radius = retained_before
.get(handle)
.and_then(|entity| retained_radius(entity))
.unwrap_or_else(|| sys.store().get(circle.rad));
let target = sys.add_param(radius, true);
sys.add_constraint(Rc::new(Equal::new(circle.rad, target, 1.0)));
}
EntityGeom::Arc(arc) => {
let radius = retained_before
.get(handle)
.and_then(|entity| retained_radius(entity))
.unwrap_or_else(|| sys.store().get(arc.circle.rad));
let target = sys.add_param(radius, true);
sys.add_constraint(Rc::new(Equal::new(arc.circle.rad, target, 1.0)));
}
_ => {}
}
}
}
// Arc rules (this module's doc comment): keep every registered arc's
// `start`/`end` points consistent with its center/radius/angle,
// unconditionally — the kernel adds these the moment an arc
@@ -2375,18 +2491,43 @@ impl Scene {
/// the resulting `(Handle, ChangeKind::Modified)` entries the same way
/// `refresh_associative_dimensions`/`_hatches` do, for `bump_entities`
/// to fold into its own `changes` vec.
#[cfg(test)]
#[allow(dead_code)]
pub(crate) fn refresh_parametric_constraints(
&mut self,
changes: &[(Handle, ChangeKind)],
) -> Vec<(Handle, ChangeKind)> {
self.refresh_parametric_constraints_with_driven(changes, &[])
self.refresh_parametric_constraints_with_policy(changes, &[], false)
}
#[allow(dead_code)]
pub(crate) fn refresh_parametric_constraints_with_driven(
&mut self,
changes: &[(Handle, ChangeKind)],
driven_refs: &[ParametricRef],
) -> Vec<(Handle, ChangeKind)> {
self.refresh_parametric_constraints_with_policy(changes, driven_refs, false)
}
pub(crate) fn refresh_parametric_constraints_with_policy(
&mut self,
changes: &[(Handle, ChangeKind)],
driven_refs: &[ParametricRef],
retain_size: bool,
) -> Vec<(Handle, ChangeKind)> {
self.refresh_parametric_constraints_with_originals(
changes,
driven_refs,
retain_size,
&[],
)
}
pub(crate) fn refresh_parametric_constraints_with_originals(
&mut self,
changes: &[(Handle, ChangeKind)],
driven_refs: &[ParametricRef],
retain_size: bool,
retained_originals: &[(Handle, EntityType)],
) -> Vec<(Handle, ChangeKind)> {
// An erased entity takes its constraints with it instead of leaving
// dangling references. Done first,
@@ -2418,6 +2559,25 @@ impl Scene {
}
}
let retained_before: HashMap<Handle, std::sync::Arc<EntityType>> = if retain_size {
let mut retained: HashMap<Handle, std::sync::Arc<EntityType>> = self
.undo_recording
.as_ref()
.into_iter()
.flat_map(|recording| recording.before.iter())
.filter_map(|(handle, entity)| {
entity
.as_ref()
.map(|entity| (*handle, std::sync::Arc::clone(entity)))
})
.collect();
for (handle, entity) in retained_originals {
retained.insert(*handle, std::sync::Arc::new(entity.clone()));
}
retained
} else {
HashMap::new()
};
let mut result = Vec::new();
for i in 0..self.parametric_constraints.len() {
let touched = changes.iter().any(|(handle, _)| {
@@ -2434,6 +2594,8 @@ impl Scene {
&self.named_parameters,
&self.parametric_constraints[i],
driven_refs,
retain_size,
&retained_before,
) else {
continue;
};
@@ -2458,10 +2620,17 @@ impl Scene {
pub(crate) fn solve_parametric_constraints_preview(
&self,
touched: &[Handle],
driven_refs: &[ParametricRef],
retain_size: bool,
retained_originals: &[(Handle, EntityType)],
) -> Vec<(Handle, EntityType)> {
if self.parametric_constraints.is_empty() || touched.is_empty() {
return Vec::new();
}
let retained_before: HashMap<Handle, std::sync::Arc<EntityType>> = retained_originals
.iter()
.map(|(handle, entity)| (*handle, std::sync::Arc::new(entity.clone())))
.collect();
let mut result = Vec::new();
for set in &self.parametric_constraints {
let is_touched = touched
@@ -2470,8 +2639,14 @@ impl Scene {
if !is_touched {
continue;
}
let Some((solved, _dof, _conflicts)) =
solve_scope(&self.document, &self.named_parameters, set, &[])
let Some((solved, _dof, _conflicts)) = solve_scope(
&self.document,
&self.named_parameters,
set,
driven_refs,
retain_size,
&retained_before,
)
else {
continue;
};
@@ -2749,7 +2924,7 @@ mod tests {
}
let b_before = scene.document.get_entity(b).cloned();
let solved = scene.solve_parametric_constraints_preview(&[a]);
let solved = scene.solve_parametric_constraints_preview(&[a], &[], false, &[]);
assert_eq!(
scene.document.get_entity(b),
+10 -5
View File
@@ -547,7 +547,8 @@ fn test_selected_circle_arc_ellipse_highlight_overlay() {
pipeline.render(
&mut encoder,
&target,
iced::Size::new(512, 512),
iced::Rectangle::with_size(iced::Size::new(512.0, 512.0)),
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
[0.0, 0.0, 0.0, 1.0],
false,
@@ -635,7 +636,8 @@ fn test_thick_and_tapered_arc_gpu_rendering() {
pipeline.render(
&mut encoder,
&target,
iced::Size::new(512, 512),
iced::Rectangle::with_size(iced::Size::new(512.0, 512.0)),
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
[0.0, 0.0, 0.0, 1.0],
false,
@@ -740,7 +742,8 @@ fn test_tilted_3d_donut_and_thick_arc_gpu_rendering() {
pipeline.render(
&mut encoder,
&target,
iced::Size::new(512, 512),
iced::Rectangle::with_size(iced::Size::new(512.0, 512.0)),
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
[0.0, 0.0, 0.0, 1.0],
false,
@@ -855,7 +858,8 @@ fn test_selected_ellipse_overlay() {
pipeline.render(
&mut encoder,
&target,
iced::Size::new(512, 512),
iced::Rectangle::with_size(iced::Size::new(512.0, 512.0)),
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
[0.0, 0.0, 0.0, 1.0],
false,
@@ -926,7 +930,8 @@ fn test_pline_arc_switch_preview_and_render() {
pipeline.render(
&mut encoder,
&target,
iced::Size::new(512, 512),
iced::Rectangle::with_size(iced::Size::new(512.0, 512.0)),
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
iced::Rectangle { x: 0, y: 0, width: 512, height: 512 },
[0.0, 0.0, 0.0, 1.0],
false,
+15 -19
View File
@@ -201,6 +201,7 @@ pub struct Pipeline {
/// geometry passes render at this size; the blit UV is scaled by
/// `depth_texture_size / alloc_size` so it samples only the filled region.
depth_texture_size: Size<u32>,
pub(in crate::scene) viewport: Option<crate::scene::view::render::PhysicalViewport>,
/// Actual allocated size of the depth / MSAA / resolve textures. Rounded
/// up from the requested size to a coarse grid so a divider drag (which
/// changes the pane size a few pixels every frame) doesn't recreate these
@@ -2321,6 +2322,7 @@ impl Pipeline {
wipeout_bgl1,
image_bgl1,
depth_texture_size: Size::new(1, 1),
viewport: None,
// (0, 0) forces the first `ensure_depth_texture` to allocate at the
// real rounded size — the constructor textures above are placeholders.
alloc_size: Size::new(0, 0),
@@ -3967,8 +3969,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
);
}
/// Render the geometry passes at `vp_size` (the full viewport size — the
/// MSAA / resolve textures are this size) and blit the resulting resolve
/// Render geometry at its fractional pixel rectangle and blit the resolve
/// to `surface_dest` on the swap-chain. The UV crop is read from the
/// blit uniform buffer (written by `upload_blit_uv` during `prepare`)
/// so a viewport that hangs off the canvas still composites the correct
@@ -3977,19 +3978,14 @@ analytic={:.1} regular={:.1} blocks={:.1}",
&self,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
vp_size: Size<u32>,
raster: Rectangle,
surface_dest: Rectangle<u32>,
surface_clip: Rectangle<u32>,
bg_color: [f32; 4],
mesh_wireframe: bool,
hidden_line: bool,
show_3d_edges: bool,
) {
let vp = Rectangle::<u32> {
x: 0,
y: 0,
width: vp_size.width,
height: vp_size.height,
};
let msaa = &self.msaa_view;
let [r, g, b, a] = bg_color;
let clear_color = if self.clip_boundary.is_some() || self.skip_background {
@@ -4101,8 +4097,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
occlusion_query_set: None,
multiview_mask: None,
});
// MSAA texture is clip-bounds-sized, so viewport starts at (0, 0).
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
// Stamp the viewport clip boundary into the just-cleared stencil
// (interior → 1) before any content draws, so every pass below can
// clip to the shape with reference 1.
@@ -4155,7 +4150,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
pass.set_pipeline(&self.image_pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_stencil_reference(stencil_ref);
@@ -4192,7 +4187,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_stencil_reference(stencil_ref);
// Four draw paths share this pass:
@@ -4491,7 +4486,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_stencil_reference(stencil_ref);
if !fill.chunks_3d.is_empty() {
@@ -4562,7 +4557,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
pass.set_pipeline(&self.wire_pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_stencil_reference(stencil_ref);
@@ -4605,7 +4600,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
pass.set_pipeline(&self.wire_pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
// In a filled-with-edges mode the mesh outline edges frame the shaded
@@ -4756,7 +4751,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
pass.set_pipeline(&self.text_pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_stencil_reference(stencil_ref);
@@ -4809,7 +4804,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
pass.set_pipeline(&self.wipeout_pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_stencil_reference(stencil_ref);
@@ -4859,7 +4854,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_stencil_reference(stencil_ref);
if !self.gpu_selected_wires.is_empty() {
@@ -5002,6 +4997,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
1.0,
);
pass.set_pipeline(&self.blit_pipeline);
pass.set_scissor_rect(surface_clip.x, surface_clip.y, surface_clip.width, surface_clip.height);
pass.set_bind_group(0, &self.blit_bind_group, &[]);
pass.draw(0..6, 0..1);
}
+56 -3
View File
@@ -386,12 +386,12 @@ impl Scene {
/// needs the equivalent paper-space geometry explicitly.
pub fn viewport_plot_fills(
&self,
) -> (Vec<(WireModel, f32)>, Vec<HatchModel>, Vec<HatchModel>) {
) -> (Vec<(WireModel, f32)>, Vec<HatchModel>, Vec<HatchModel>, Vec<crate::io::pdf_export::PlotImage>) {
use acadrust::entities::Viewport;
use model::hatch_model::HatchPattern;
if self.current_layout == "Model" {
return (Vec::new(), Vec::new(), Vec::new());
return (Vec::new(), Vec::new(), Vec::new(), Vec::new());
}
let paper_block = self.current_layout_block_handle();
let model_block = self.model_space_block_handle();
@@ -411,6 +411,7 @@ impl Scene {
let mut pattern_wires = Vec::new();
let mut projected_hatches = Vec::new();
let mut projected_wipeouts = Vec::new();
let mut projected_images = Vec::new();
for viewport in viewports {
let Some(camera) = self.camera_for_viewport(viewport.common.handle) else {
@@ -463,6 +464,58 @@ impl Scene {
let frozen: rustc_hash::FxHashSet<Handle> =
viewport.frozen_layers.iter().copied().collect();
let mut viewport_clips = vec![vec![
[xmin as f64, ymin as f64], [xmax as f64, ymin as f64],
[xmax as f64, ymax as f64], [xmin as f64, ymax as f64],
]];
if !self.images.is_empty() && !viewport.clip_boundary_handle.is_null() {
let boundary = self.clip_boundary_polygon(
viewport.clip_boundary_handle, viewport.center.z as f32);
if boundary.len() >= 3 {
viewport_clips.push(boundary.iter().map(|p| [p[0] as f64, p[1] as f64]).collect());
}
}
for mut plot in self.placed_images(model_block, Some(&frozen),
self.viewport_scale_handle(viewport.common.handle),
self.annotation_all_visible(), Some(viewport.common.handle), true)
{
let mut valid = true;
let mut project_point = |high: &mut [f32; 3], low: &mut [f32; 3]| {
let point = std::array::from_fn(|i| high[i] as f64 + low[i] as f64);
if let Some([x, y]) = project_3d(point) {
*high = [x, y, 0.0];
*low = [0.0; 3];
} else {
valid = false;
}
};
for (high, low) in plot.image.corners.iter_mut().zip(&mut plot.image.corners_low) {
project_point(high, low);
}
for vertex in &mut plot.image.verts {
project_point(&mut vertex.pos, &mut vertex.pos_low);
}
for clip in &mut plot.clips {
for point in clip {
if let Some([x, y]) = project(point[0], point[1]) {
*point = [x as f64, y as f64];
} else {
valid = false;
}
}
}
if !valid {
continue;
}
if [(0, xmin, xmax), (1, ymin, ymax)].iter().any(|&(axis, min, max)| {
plot.image.verts.iter().all(|v| v.pos[axis] < min)
|| plot.image.verts.iter().all(|v| v.pos[axis] > max)
}) {
continue;
}
plot.clips.extend(viewport_clips.iter().cloned());
projected_images.push(plot);
}
let hatches = self.plot_hatches_for_block(
model_block,
Some(&frozen),
@@ -536,7 +589,7 @@ impl Scene {
}
}
(pattern_wires, projected_hatches, projected_wipeouts)
(pattern_wires, projected_hatches, projected_wipeouts, projected_images)
}
/// A content viewport's clip boundary, projected into that viewport's
+1
View File
@@ -1007,6 +1007,7 @@ impl Scene {
self.hover_highlight = None;
hover_changed = true;
}
hover_changed |= self.constraint_hover_highlights.remove(&h);
self.hatches.remove(&h);
self.images.remove(&h);
self.meshes.remove(&h);
+320 -60
View File
@@ -395,32 +395,23 @@ impl shader::Primitive for Primitive {
inner.slot_id = vp.instance_id;
inner.forget_cached_keys();
}
// The MSAA / depth / resolve textures are always sized to the
// FULL viewport rectangle (not the on-canvas-visible portion)
// so the camera matrices render at consistent aspect / scale.
// The blit step picks the visible sub-rectangle out via the
// shader's UV crop uniform, which lets partially off-canvas
// viewports composite to their visible surface area without
// drift.
let clip_size = Size::new(
(vp.screen_rect.width * bounds.width * scale).ceil().max(1.0) as u32,
(vp.screen_rect.height * bounds.height * scale).ceil().max(1.0) as u32,
// Round texture coverage, retaining fractional raster placement.
let placement = physical_viewport(
Rectangle {
x: (bounds.x + vp.screen_rect.x * bounds.width) * scale,
y: (bounds.y + vp.screen_rect.y * bounds.height) * scale,
width: vp.screen_rect.width * bounds.width * scale,
height: vp.screen_rect.height * bounds.height * scale,
},
viewport.physical_size(),
);
let clip_size = placement.size;
inner.viewport = Some(placement);
inner.ensure_depth_texture(device, clip_size);
let viewcube_side =
(crate::scene::VIEWCUBE_RENDER_PX.ceil() * scale).ceil().max(1.0) as u32;
inner
.viewcube
.ensure_depth_texture(device, Size::new(viewcube_side, viewcube_side));
// Compute the UV crop for this viewport. `screen_rect` is in
// normalized canvas units (0..1) but may extend negative or
// beyond 1 when the viewport hangs off the canvas. The on-
// canvas portion in viewport-local UV is straightforward to
// derive from how much sticks out on each side.
let sr = vp.screen_rect;
let (uo_x, us_x) = uv_crop_axis(sr.x, sr.width);
let (uo_y, us_y) = uv_crop_axis(sr.y, sr.height);
inner.upload_blit_uv(queue, [uo_x, uo_y], [us_x, us_y]);
inner.viewcube.ensure_depth_texture(device, Size::new(viewcube_side, viewcube_side));
inner.upload_blit_uv(queue, placement.uv_offset, placement.uv_scale);
inner.upload_background_images(
device,
queue,
@@ -440,7 +431,7 @@ impl shader::Primitive for Primitive {
// drawing size. The ViewCube is excluded from the signature and
// keeps updating in its own always-on pass, so cube hover still
// tracks while the scene is cached.
let sig = render_signature(vp, clip_size.width, clip_size.height);
let sig = render_signature(vp, &placement);
// REDRAW bypass: pin render_sig to force a full pass this frame
// even if the signature is otherwise unchanged. The request is
// one-shot per viewport (consumed by the builder that produced
@@ -1311,8 +1302,6 @@ retained_contributors={}",
) {
let nav_render_started = iced::time::Instant::now();
pipeline.frame_rendered.store(true, std::sync::atomic::Ordering::Relaxed);
let cw = clip.width as f32;
let ch = clip.height as f32;
let clip_right = clip.x + clip.width;
let clip_bottom = clip.y + clip.height;
for vp in &self.viewports {
@@ -1322,27 +1311,25 @@ retained_contributors={}",
let Some(inner) = pipeline.inners.get(*slot) else {
continue;
};
// Where the viewport would land on the surface in absolute
// pixels (i32 because either edge may stick off the canvas).
let vp_full_x = clip.x as i32 + (vp.screen_rect.x * cw) as i32;
let vp_full_y = clip.y as i32 + (vp.screen_rect.y * ch) as i32;
let vp_full_w = (vp.screen_rect.width * cw).max(1.0) as i32;
let vp_full_h = (vp.screen_rect.height * ch).max(1.0) as i32;
// Intersect with the surface clip — that's the slice we blit.
let dest_x = vp_full_x.max(clip.x as i32);
let dest_y = vp_full_y.max(clip.y as i32);
let dest_right = (vp_full_x + vp_full_w).min(clip_right as i32);
let dest_bottom = (vp_full_y + vp_full_h).min(clip_bottom as i32);
if dest_right <= dest_x || dest_bottom <= dest_y {
let Some(placement) = inner.viewport else {
continue;
};
let surface_dest = placement.surface;
let left = surface_dest.x.max(clip.x);
let top = surface_dest.y.max(clip.y);
let surface_clip = Rectangle {
x: left,
y: top,
width: (surface_dest.x + surface_dest.width).min(clip_right).saturating_sub(left),
height: (surface_dest.y + surface_dest.height).min(clip_bottom).saturating_sub(top),
};
if surface_clip.width == 0 || surface_clip.height == 0 {
continue;
}
let surface_dest = Rectangle {
x: dest_x as u32,
y: dest_y as u32,
width: (dest_right - dest_x) as u32,
height: (dest_bottom - dest_y) as u32,
};
let vp_size = Size::new(vp_full_w.max(1) as u32, vp_full_h.max(1) as u32);
let vp_full_x = surface_dest.x as i32;
let vp_full_y = surface_dest.y as i32;
let vp_full_w = surface_dest.width as i32;
let vp_full_h = surface_dest.height as i32;
// `mesh_fill` is false for Wireframe 2D / Wireframe 3D — flip
// the draw path so meshes use the wireframe pipeline + the
// pre-built triangle-edge index buffer.
@@ -1350,8 +1337,9 @@ retained_contributors={}",
inner.render(
encoder,
target,
vp_size,
placement.raster,
surface_dest,
surface_clip,
self.bg_color,
mesh_wireframe,
vp.hidden_line,
@@ -1363,6 +1351,8 @@ retained_contributors={}",
// rejects negative origins, and a clamped cube would scale
// distortedly. The active viewport is normally fully visible.
if vp.show_viewcube
&& surface_dest.width == placement.size.width
&& surface_dest.height == placement.size.height
&& vp_full_x >= clip.x as i32
&& vp_full_y >= clip.y as i32
&& vp_full_x + vp_full_w <= clip_right as i32
@@ -1408,7 +1398,7 @@ retained_contributors={}",
/// live preview IS included (its coordinates), so a rubber-band tracking the
/// cursor still renders, and the frame where the preview clears erases it
/// instead of freezing the last overlay on screen.
fn render_signature(vp: &ViewportData, clip_w: u32, clip_h: u32) -> u64 {
fn render_signature(vp: &ViewportData, placement: &PhysicalViewport) -> u64 {
use std::hash::{Hash, Hasher};
let mut h = rustc_hash::FxHasher::default();
// Camera + per-view shading flags all live in the uniforms (view_rot, eye
@@ -1453,8 +1443,11 @@ fn render_signature(vp: &ViewportData, clip_w: u32, clip_h: u32) -> u64 {
// frame (skip_hatch flips false) re-renders with hatches and re-caches.
vp.skip_hatch.hash(&mut h);
vp.skip_background.hash(&mut h);
clip_w.hash(&mut h);
clip_h.hash(&mut h);
placement.size.width.hash(&mut h);
placement.size.height.hash(&mut h);
for value in [placement.raster.x, placement.raster.y, placement.raster.width, placement.raster.height] {
value.to_bits().hash(&mut h);
}
// Live overlay (command preview / interim / grip drag). Small — a handful
// of wires — so hashing its coordinates is cheap and catches the endpoint
// moving with the cursor as well as the preview appearing / clearing.
@@ -1535,18 +1528,282 @@ fn render_signature(vp: &ViewportData, clip_w: u32, clip_h: u32) -> u64 {
h.finish()
}
/// On-canvas-visible UV crop on one axis. `pos` and `size` are in the
/// shader widget's normalized 0..1 coords. Returns `(uv_offset, uv_scale)`
/// applied as `actual_uv = quad_uv * uv_scale + uv_offset` in the blit
/// shader — identity `(0.0, 1.0)` for fully on-canvas viewports.
fn uv_crop_axis(pos: f32, size: f32) -> (f32, f32) {
if size <= 0.0 {
return (0.0, 1.0);
#[derive(Clone, Copy)]
pub(in crate::scene) struct PhysicalViewport {
size: Size<u32>,
raster: Rectangle,
surface: Rectangle<u32>,
uv_offset: [f32; 2],
uv_scale: [f32; 2],
}
fn physical_viewport(rect: Rectangle, window: Size<u32>) -> PhysicalViewport {
let x = rect.x.floor();
let y = rect.y.floor();
let right = (rect.x + rect.width).ceil();
let bottom = (rect.y + rect.height).ceil();
let width = (right - x).max(1.0);
let height = (bottom - y).max(1.0);
let left = x.clamp(0.0, window.width as f32);
let top = y.clamp(0.0, window.height as f32);
let visible_w = (right.min(window.width as f32) - left).max(0.0);
let visible_h = (bottom.min(window.height as f32) - top).max(0.0);
PhysicalViewport {
size: Size::new(width as u32, height as u32),
raster: Rectangle {
x: rect.x - x,
y: rect.y - y,
width: rect.width.max(0.0),
height: rect.height.max(0.0),
},
surface: Rectangle {
x: left as u32,
y: top as u32,
width: visible_w as u32,
height: visible_h as u32,
},
uv_offset: [(left - x) / width, (top - y) / height],
uv_scale: [visible_w / width, visible_h / height],
}
}
#[cfg(test)]
mod pixel_placement_tests {
use super::*;
#[test]
fn raster_and_blit_preserve_fractional_position_at_different_scales() {
for scale in [1.0, 1.25, 1.5, 2.0] {
for x in [-17.37, 0.25, 13.61, 199.7] {
for width in [0.4, 18.73, 181.83] {
let rect = Rectangle {
x: x * scale,
y: 12.13 * scale,
width: width * scale,
height: 91.17 * scale,
};
let placement = physical_viewport(rect, Size::new(256, 256));
assert!(placement.raster.x >= 0.0 && placement.raster.x < 1.0);
assert!(
placement.raster.x + placement.raster.width
<= placement.size.width as f32 + 1e-5
);
if placement.surface.width == 0 {
continue;
}
// Follow a model point through rasterization, texture UV
// crop, then the blit; paper geometry lands directly here.
for u in [0.0, 0.1, 0.5, 0.9, 1.0] {
let texture_x = placement.raster.x + u * placement.raster.width;
let output_x = placement.surface.x as f32
+ (texture_x / placement.size.width as f32 - placement.uv_offset[0])
/ placement.uv_scale[0]
* placement.surface.width as f32;
assert!((output_x - (rect.x + u * rect.width)).abs() < 0.0001);
}
assert!(
(placement.uv_scale[0] * placement.size.width as f32
- placement.surface.width as f32)
.abs()
< 1e-5
);
}
}
}
}
#[test]
fn integer_translation_reuses_the_same_raster_placement() {
let rect = Rectangle {
x: 13.25,
y: 27.5,
width: 100.75,
height: 51.25,
};
let first = physical_viewport(rect, Size::new(512, 512));
let moved = physical_viewport(
Rectangle {
x: rect.x + 20.0,
..rect
},
Size::new(512, 512),
);
assert_eq!(first.raster, moved.raster);
assert_eq!(first.size, moved.size);
assert_eq!(first.uv_scale, moved.uv_scale);
assert_eq!(moved.surface.x, first.surface.x + 20);
}
#[test]
#[ignore = "requires a GPU adapter"]
fn fractional_viewports_keep_wire_pixels_aligned_after_blitting() {
use iced::futures::executor::block_on;
use iced::wgpu;
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default()))
.expect("GPU adapter");
let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor {
required_limits: adapter.limits(),
..Default::default()
}))
.expect("GPU device");
let validation = device.push_error_scope(wgpu::ErrorFilter::Validation);
let mut pipeline =
crate::scene::pipeline::Pipeline::new(&device, &queue, wgpu::TextureFormat::Bgra8Unorm);
let target = device.create_texture(&wgpu::TextureDescriptor {
label: Some("pixel placement regression"),
size: wgpu::Extent3d {
width: 256,
height: 256,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Bgra8Unorm,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = target.create_view(&Default::default());
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("pixel readback"),
size: 256 * 256 * 4,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
for (x, width, parent_clip) in [
(13.37, 181.83, false),
(-12.61, 187.29, false),
(19.75, 173.75, false),
(13.37, 181.83, true),
(-12.61, 187.29, true),
(19.75, 173.75, true),
] {
let rect = Rectangle {
x,
y: 18.37,
width,
height: 181.83,
};
let placement = physical_viewport(rect, Size::new(256, 256));
pipeline.ensure_depth_texture(&device, placement.size);
pipeline.upload_blit_uv(&queue, placement.uv_offset, placement.uv_scale);
let mut uniforms = Uniforms::new(
&Camera::default(),
Rectangle::with_size(Size::new(width, rect.height)),
false,
);
uniforms.eye_high = [0.0; 3];
uniforms.eye_low = [0.0; 3];
uniforms.view_rot = Mat4::IDENTITY;
pipeline.upload_uniforms(&device, &queue, &uniforms);
let u = 0.73;
let ndc_x = 2.0 * u - 1.0;
let wire = WireModel::solid(
"alignment".into(),
vec![[ndc_x, -0.8, 0.5], [ndc_x, 0.8, 0.5]],
[1.0; 4],
false,
);
pipeline.upload_preview_wires(&device, &queue, &[wire], &Default::default());
let mut encoder = device.create_command_encoder(&Default::default());
{
let _clear = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
..Default::default()
});
}
let clip = if parent_clip {
Rectangle {
x: placement.surface.x + 11,
y: 64,
width: placement.surface.width - 22,
height: 96,
}
} else {
placement.surface
};
pipeline.render(
&mut encoder,
&view,
placement.raster,
placement.surface,
clip,
[0.0, 0.0, 0.0, 1.0],
false,
false,
false,
);
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &target,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(256 * 4),
rows_per_image: Some(256),
},
},
wgpu::Extent3d {
width: 256,
height: 256,
depth_or_array_layers: 1,
},
);
queue.submit([encoder.finish()]);
let (tx, rx) = std::sync::mpsc::channel();
readback
.slice(..)
.map_async(wgpu::MapMode::Read, move |result| {
tx.send(result).unwrap();
});
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
rx.recv().unwrap().unwrap();
{
let bytes = readback.slice(..).get_mapped_range();
let row = (rect.y + rect.height * 0.5) as usize;
let expected = rect.x + u * rect.width;
let row_has_wire = bytes[48 * 256 * 4..49 * 256 * 4]
.chunks_exact(4)
.any(|pixel| pixel[..3] != [0, 0, 0]);
assert_eq!(
row_has_wire, !parent_clip,
"parent clip must exclude the wire at row 48"
);
let mut weight = 0.0;
let mut center = 0.0;
for column in (expected as usize - 4)..=(expected as usize + 4) {
let value = bytes[(row * 256 + column) * 4] as f32;
weight += value;
center += (column as f32 + 0.5) * value;
}
assert!(weight > 0.0, "wire must produce pixels");
assert!(
(center / weight - expected).abs() < 0.25,
"x={x}, width={width}, expected={expected}, actual={}",
center / weight
);
}
readback.unmap();
}
assert!(
block_on(validation.pop()).is_none(),
"GPU validation failed"
);
}
let left_off = (-pos).max(0.0);
let right_off = (pos + size - 1.0).max(0.0);
let visible = (size - left_off - right_off).max(0.0);
(left_off / size, visible / size)
}
/// Apply a clip-space crop to `view_proj` so the sub-rect of the original
@@ -3435,7 +3692,10 @@ impl Scene {
&self,
inst: &ViewportInstance,
) -> Arc<Vec<WireModel>> {
if self.selected.is_empty() && self.hover_highlight.is_none() {
if self.selected.is_empty()
&& self.hover_highlight.is_none()
&& self.constraint_hover_highlights.is_empty()
{
return Arc::new(Vec::new());
}