forked from 1b-agent/opencadstudio-upstream
feat(mview): add viewport creation options
Support polygonal and object viewports through native clip-boundary handles, including clipped rendering and selection. Closes #525
This commit is contained in:
1 parent
3d90450b3f
commit
b8889ccd20
10 files changed
+1038
-135
No files matched your search
@@ -1386,8 +1386,36 @@ pub fn tessellate(
|
||||
}
|
||||
|
||||
// ── Fallback for Viewport / Insert / Hatch / Ole2Frame ────────────────
|
||||
let (points_f64, snap_pts, tangent_geoms, key_vertices) =
|
||||
let (mut points_f64, snap_pts, tangent_geoms, mut key_vertices) =
|
||||
fallback_geometry(entity);
|
||||
let clipped_viewport_polygon = match entity {
|
||||
EntityType::Viewport(viewport) if !viewport.clip_boundary_handle.is_null() => {
|
||||
let polygon = crate::scene::project::clip_boundary_polygon_for_document(
|
||||
document,
|
||||
viewport.clip_boundary_handle,
|
||||
viewport.center.z as f32,
|
||||
);
|
||||
if polygon.len() >= 3 {
|
||||
let polygon: Vec<[f64; 3]> = polygon
|
||||
.into_iter()
|
||||
.map(|point| {
|
||||
[
|
||||
point[0] as f64,
|
||||
point[1] as f64,
|
||||
point[2] as f64,
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
points_f64 = polygon.clone();
|
||||
points_f64.push(polygon[0]);
|
||||
key_vertices = polygon.clone();
|
||||
Some(polygon)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
// `points_f64` are absolute world coords; split into the double-single
|
||||
// high/low pair so the outline reconstructs to f64 precision at UTM scale
|
||||
// (a NaN separator stays NaN in both buffers).
|
||||
@@ -1427,14 +1455,18 @@ pub fn tessellate(
|
||||
};
|
||||
let (pick_tris, pick_tris_low) = match entity {
|
||||
EntityType::Viewport(vp) if !is_sheet_vp(vp) => {
|
||||
let (cx, cy, cz) = (vp.center.x, vp.center.y, vp.center.z);
|
||||
let (hw, hh) = (vp.width / 2.0, vp.height / 2.0);
|
||||
points_to_ds(crate::entities::common::quad_pick_tris(&[
|
||||
[cx - hw, cy - hh, cz],
|
||||
[cx + hw, cy - hh, cz],
|
||||
[cx + hw, cy + hh, cz],
|
||||
[cx - hw, cy + hh, cz],
|
||||
]))
|
||||
if let Some(polygon) = clipped_viewport_polygon.as_ref() {
|
||||
points_to_ds(crate::entities::mesh::triangulate_planar(polygon))
|
||||
} else {
|
||||
let (cx, cy, cz) = (vp.center.x, vp.center.y, vp.center.z);
|
||||
let (hw, hh) = (vp.width / 2.0, vp.height / 2.0);
|
||||
points_to_ds(crate::entities::common::quad_pick_tris(&[
|
||||
[cx - hw, cy - hh, cz],
|
||||
[cx + hw, cy - hh, cz],
|
||||
[cx + hw, cy + hh, cz],
|
||||
[cx - hw, cy + hh, cz],
|
||||
]))
|
||||
}
|
||||
}
|
||||
_ => (Vec::new(), Vec::new()),
|
||||
};
|
||||
|
||||
@@ -2710,6 +2710,21 @@ impl Scene {
|
||||
self.current_layout_block_handle()
|
||||
}
|
||||
|
||||
/// True when an entity belongs to the active layout, including imported
|
||||
/// DXF entities whose owner handle is NULL but whose BlockRecord still
|
||||
/// lists the entity. Keep command validation aligned with the same
|
||||
/// ownership fallback used by rendering and hit-testing.
|
||||
pub(crate) fn entity_belongs_to_current_layout(&self, handle: Handle) -> bool {
|
||||
let Some(entity) = self.document.get_entity(handle) else {
|
||||
return false;
|
||||
};
|
||||
self.belongs_to_visible_block(
|
||||
handle,
|
||||
entity.common().owner_handle,
|
||||
self.current_layout_block_handle(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the block-record handle for `current_layout`.
|
||||
///
|
||||
/// Primary path: the Layout object's `block_record` field (set correctly
|
||||
@@ -3066,6 +3081,46 @@ impl Scene {
|
||||
.or(Some(((0.0, 0.0), (297.0, 210.0))))
|
||||
}
|
||||
|
||||
/// Printable rectangle of the current paper layout, in paper-space units.
|
||||
/// Falls back to the whole sheet when the layout has no usable margins.
|
||||
pub fn printable_area_limits(&self) -> Option<((f64, f64), (f64, f64))> {
|
||||
let ((x0, y0), (x1, y1)) = self.paper_limits()?;
|
||||
let margins = self.document.objects.values().find_map(|object| {
|
||||
if let ObjectType::Layout(layout) = object {
|
||||
if layout.name == self.current_layout {
|
||||
return Some((
|
||||
layout.plot_margin_left,
|
||||
layout.plot_margin_bottom,
|
||||
layout.plot_margin_right,
|
||||
layout.plot_margin_top,
|
||||
layout.plot_rotation,
|
||||
));
|
||||
}
|
||||
}
|
||||
None
|
||||
});
|
||||
let Some((left, bottom, right, top, rotation)) = margins else {
|
||||
return Some(((x0, y0), (x1, y1)));
|
||||
};
|
||||
let (left, bottom, right, top) = match rotation {
|
||||
1 | 3 => (bottom, left, top, right),
|
||||
2 => (right, top, left, bottom),
|
||||
_ => (left, bottom, right, top),
|
||||
};
|
||||
let factor = self.paper_space_unit_factor();
|
||||
let printable = (
|
||||
(x0 + left * factor, y0 + bottom * factor),
|
||||
(x1 - right * factor, y1 - top * factor),
|
||||
);
|
||||
if printable.1.0 - printable.0.0 < 1e-6
|
||||
|| printable.1.1 - printable.0.1 < 1e-6
|
||||
{
|
||||
Some(((x0, y0), (x1, y1)))
|
||||
} else {
|
||||
Some(printable)
|
||||
}
|
||||
}
|
||||
|
||||
/// Scale of the first user viewport (id > 1) in the current paper layout,
|
||||
/// used for the status-bar display. Returns `None` in Model space or if
|
||||
/// no user viewport exists.
|
||||
|
||||
+166
-51
@@ -418,65 +418,180 @@ impl Scene {
|
||||
}
|
||||
|
||||
/// Tessellated outline of a non-rectangular viewport / XCLIP clip-boundary
|
||||
/// entity, in paper coordinates, by reusing the entity's own `to_truck`
|
||||
/// tessellation (Lines). NaN segment breaks are dropped so the result is a
|
||||
/// single ordered ring suitable for a stencil triangle-fan.
|
||||
pub(super) fn clip_boundary_polygon(&self, handle: Handle, z: f32) -> Vec<[f32; 3]> {
|
||||
use std::f64::consts::TAU;
|
||||
let Some(entity) = self
|
||||
.document
|
||||
.entities()
|
||||
.find(|e| e.common().handle == handle)
|
||||
else {
|
||||
return vec![];
|
||||
};
|
||||
// Circles and ellipses tessellate directly — their `to_truck` returns a
|
||||
// parametric TruckObject (not `Lines`), so extracting a polygon there
|
||||
// would come back empty. Everything else (splines, polylines, …) reuses
|
||||
// the entity's own `Lines` tessellation.
|
||||
const N: usize = 64;
|
||||
match entity {
|
||||
EntityType::Circle(c) => (0..N)
|
||||
/// entity, in paper coordinates. Closed polylines are sampled directly
|
||||
/// because their normal tessellation is a `Contour`, not `Lines`.
|
||||
pub(crate) fn clip_boundary_polygon(&self, handle: Handle, z: f32) -> Vec<[f32; 3]> {
|
||||
clip_boundary_polygon_for_document(&self.document, handle, z)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clip_boundary_polygon_for_document(
|
||||
document: &CadDocument,
|
||||
handle: Handle,
|
||||
z: f32,
|
||||
) -> Vec<[f32; 3]> {
|
||||
use std::f64::consts::TAU;
|
||||
let Some(entity) = document
|
||||
.entities()
|
||||
.find(|e| e.common().handle == handle)
|
||||
else {
|
||||
return vec![];
|
||||
};
|
||||
// Circles and ellipses tessellate directly — their `to_truck` returns a
|
||||
// parametric TruckObject (not `Lines`), so extracting a polygon there
|
||||
// would come back empty. Everything else (splines, polylines, …) reuses
|
||||
// the entity's own `Lines` tessellation.
|
||||
const N: usize = 64;
|
||||
match entity {
|
||||
EntityType::Circle(c) => (0..N)
|
||||
.map(|i| {
|
||||
let a = i as f64 * TAU / N as f64;
|
||||
[
|
||||
(c.center.x + a.cos() * c.radius) as f32,
|
||||
(c.center.y + a.sin() * c.radius) as f32,
|
||||
z,
|
||||
]
|
||||
})
|
||||
.collect(),
|
||||
EntityType::Ellipse(el) => {
|
||||
// major_axis = center → major endpoint; minor = perp(major) × ratio.
|
||||
let (mx, my) = (el.major_axis.x, el.major_axis.y);
|
||||
let r = el.minor_axis_ratio;
|
||||
(0..N)
|
||||
.map(|i| {
|
||||
let a = i as f64 * TAU / N as f64;
|
||||
[
|
||||
(c.center.x + a.cos() * c.radius) as f32,
|
||||
(c.center.y + a.sin() * c.radius) as f32,
|
||||
z,
|
||||
]
|
||||
let t = i as f64 * TAU / N as f64;
|
||||
let px = mx * t.cos() - my * r * t.sin();
|
||||
let py = my * t.cos() + mx * r * t.sin();
|
||||
[(el.center.x + px) as f32, (el.center.y + py) as f32, z]
|
||||
})
|
||||
.collect(),
|
||||
EntityType::Ellipse(el) => {
|
||||
// major_axis = center → major endpoint; minor = perp(major) × ratio.
|
||||
let (mx, my) = (el.major_axis.x, el.major_axis.y);
|
||||
let r = el.minor_axis_ratio;
|
||||
(0..N)
|
||||
.map(|i| {
|
||||
let t = i as f64 * TAU / N as f64;
|
||||
let px = mx * t.cos() - my * r * t.sin();
|
||||
let py = my * t.cos() + mx * r * t.sin();
|
||||
[(el.center.x + px) as f32, (el.center.y + py) as f32, z]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
EntityType::LwPolyline(polyline) if polyline.is_closed => {
|
||||
let vertices: Vec<([f64; 2], f64)> = polyline
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|vertex| {
|
||||
(
|
||||
[vertex.location.x, vertex.location.y],
|
||||
vertex.bulge,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
sample_polyline_clip_boundary(
|
||||
&vertices,
|
||||
polyline.elevation,
|
||||
(
|
||||
polyline.normal.x,
|
||||
polyline.normal.y,
|
||||
polyline.normal.z,
|
||||
),
|
||||
z,
|
||||
)
|
||||
}
|
||||
EntityType::Polyline2D(polyline) if polyline.is_closed() => {
|
||||
let vertices: Vec<([f64; 2], f64)> = polyline
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|vertex| {
|
||||
(
|
||||
[vertex.location.x, vertex.location.y],
|
||||
vertex.bulge,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
sample_polyline_clip_boundary(
|
||||
&vertices,
|
||||
polyline.elevation,
|
||||
(
|
||||
polyline.normal.x,
|
||||
polyline.normal.y,
|
||||
polyline.normal.z,
|
||||
),
|
||||
z,
|
||||
)
|
||||
}
|
||||
EntityType::Polyline(polyline) if polyline.is_closed() => polyline
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|vertex| {
|
||||
[
|
||||
vertex.location.x as f32,
|
||||
vertex.location.y as f32,
|
||||
z,
|
||||
]
|
||||
})
|
||||
.collect(),
|
||||
EntityType::Polyline3D(polyline) if polyline.flags.closed => polyline
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|vertex| {
|
||||
[
|
||||
vertex.position.x as f32,
|
||||
vertex.position.y as f32,
|
||||
z,
|
||||
]
|
||||
})
|
||||
.collect(),
|
||||
_ => {
|
||||
use crate::entities::traits::EntityTypeOps;
|
||||
let Some(te) = entity.to_truck_entity(document) else {
|
||||
return vec![];
|
||||
};
|
||||
if let crate::scene::convert::acad_to_truck::TruckObject::Lines(pts) = te.object {
|
||||
pts.into_iter()
|
||||
.filter(|p| p[0].is_finite() && p[1].is_finite())
|
||||
.map(|p| [p[0] as f32, p[1] as f32, z])
|
||||
.collect()
|
||||
}
|
||||
_ => {
|
||||
use crate::entities::traits::EntityTypeOps;
|
||||
let Some(te) = entity.to_truck_entity(&self.document) else {
|
||||
return vec![];
|
||||
};
|
||||
if let crate::scene::convert::acad_to_truck::TruckObject::Lines(pts) = te.object {
|
||||
pts.into_iter()
|
||||
.filter(|p| p[0].is_finite() && p[1].is_finite())
|
||||
.map(|p| [p[0] as f32, p[1] as f32, z])
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_polyline_clip_boundary(
|
||||
vertices: &[([f64; 2], f64)],
|
||||
elevation: f64,
|
||||
normal: (f64, f64, f64),
|
||||
z: f32,
|
||||
) -> Vec<[f32; 3]> {
|
||||
if vertices.len() < 3 {
|
||||
return Vec::new();
|
||||
}
|
||||
let to_wcs = |point: [f64; 2]| {
|
||||
crate::scene::view::transform::ocs_point_to_wcs(
|
||||
(point[0], point[1], elevation),
|
||||
normal,
|
||||
)
|
||||
};
|
||||
let mut output = Vec::new();
|
||||
let first = to_wcs(vertices[0].0);
|
||||
output.push([first.0 as f32, first.1 as f32, z]);
|
||||
for index in 0..vertices.len() {
|
||||
let (start, bulge) = vertices[index];
|
||||
let end_index = (index + 1) % vertices.len();
|
||||
let end = vertices[end_index].0;
|
||||
if let Some(arc) =
|
||||
crate::entities::common::BulgeArc::from_bulge(start, end, bulge)
|
||||
{
|
||||
let steps = ((arc.sweep.abs() / std::f64::consts::TAU * 64.0).ceil()
|
||||
as usize)
|
||||
.clamp(4, 64);
|
||||
for step in 1..=steps {
|
||||
if end_index == 0 && step == steps {
|
||||
break;
|
||||
}
|
||||
let point = to_wcs(arc.sample(step as f64 / steps as f64));
|
||||
output.push([point.0 as f32, point.1 as f32, z]);
|
||||
}
|
||||
} else if end_index != 0 {
|
||||
let point = to_wcs(end);
|
||||
output.push([point.0 as f32, point.1 as f32, z]);
|
||||
}
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
// ── Paper boundary wire ────────────────────────────────────────────────────
|
||||
|
||||
// ── Cohen-Sutherland line clipping ───────────────────────────────────────
|
||||
|
||||
Reference in new issue
Block a user