forked from 1b-agent/opencadstudio-upstream
Merge upstream main into Coincident parity
This commit is contained in:
commit
f6d8209139
44 files changed
+11089
-884
No files matched your search
+30
-3
@@ -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]| {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user