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:
parent
3d90450b3f
commit
b8889ccd20
10 changed files with 1039 additions and 136 deletions
|
|
@ -384,6 +384,225 @@ impl OpenCADStudio {
|
|||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
CmdResult::MviewCreate {
|
||||
viewport,
|
||||
preserve_view,
|
||||
} => {
|
||||
let saved_view = preserve_view.then(|| {
|
||||
(
|
||||
viewport.view_target.clone(),
|
||||
viewport.view_direction.clone(),
|
||||
viewport.view_center.clone(),
|
||||
viewport.view_height,
|
||||
viewport.custom_scale,
|
||||
viewport.lens_length,
|
||||
viewport.twist_angle,
|
||||
viewport.status.perspective,
|
||||
)
|
||||
});
|
||||
let label = self.history_label_from_active_cmd(i, "MVIEW");
|
||||
let pending = self.begin_undo(i, label, 1, false);
|
||||
let handle = self.commit_entity_handle(
|
||||
acadrust::EntityType::Viewport(viewport),
|
||||
);
|
||||
if let (Some(handle), Some(saved)) = (handle, saved_view) {
|
||||
if let Some(acadrust::EntityType::Viewport(viewport)) =
|
||||
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||
{
|
||||
viewport.view_target = saved.0;
|
||||
viewport.view_direction = saved.1;
|
||||
viewport.view_center = saved.2;
|
||||
viewport.view_height = saved.3;
|
||||
viewport.custom_scale = saved.4;
|
||||
viewport.lens_length = saved.5;
|
||||
viewport.twist_angle = saved.6;
|
||||
viewport.status.perspective = saved.7;
|
||||
}
|
||||
self.tabs[i].scene.camera_generation += 1;
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
}
|
||||
CmdResult::MviewCreateClipped {
|
||||
boundary,
|
||||
boundary_handle,
|
||||
} => {
|
||||
if boundary.is_none() {
|
||||
let scene = &self.tabs[i].scene;
|
||||
let valid = scene
|
||||
.entity_belongs_to_current_layout(boundary_handle)
|
||||
&& scene
|
||||
.document
|
||||
.get_entity(boundary_handle)
|
||||
.is_some_and(|entity| {
|
||||
match entity {
|
||||
acadrust::EntityType::Circle(_) => true,
|
||||
acadrust::EntityType::Ellipse(ellipse) => ellipse.is_full(),
|
||||
acadrust::EntityType::LwPolyline(polyline) => {
|
||||
polyline.is_closed
|
||||
}
|
||||
acadrust::EntityType::Polyline(polyline) => {
|
||||
polyline.is_closed()
|
||||
}
|
||||
acadrust::EntityType::Polyline2D(polyline) => {
|
||||
polyline.is_closed()
|
||||
}
|
||||
acadrust::EntityType::Polyline3D(polyline) => {
|
||||
polyline.flags.closed
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
if !valid {
|
||||
self.command_line.push_error(
|
||||
"MVIEW Object: select a closed paper-space circle, ellipse, or polyline.",
|
||||
);
|
||||
if let Some(prompt) =
|
||||
self.tabs[i].active_cmd.as_ref().map(|command| command.prompt())
|
||||
{
|
||||
self.command_line.push_info(&prompt);
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
|
||||
let created_boundary = boundary.is_some();
|
||||
let touched = 2;
|
||||
let label = self.history_label_from_active_cmd(i, "MVIEW");
|
||||
let pending = self.begin_undo(i, label, touched, false);
|
||||
let clip_handle = match boundary {
|
||||
Some(mut boundary) => {
|
||||
// A non-rectangular viewport owns a helper boundary
|
||||
// entity through `clip_boundary_handle`. Keep that
|
||||
// helper in the document for DWG compatibility and
|
||||
// stencil clipping, but do not expose it as a separate
|
||||
// selectable polyline.
|
||||
boundary.common_mut().invisible = true;
|
||||
match self.commit_entity_handle(boundary) {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
self.tabs[i].active_cmd = None;
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
}
|
||||
None => boundary_handle,
|
||||
};
|
||||
let polygon = self.tabs[i]
|
||||
.scene
|
||||
.clip_boundary_polygon(clip_handle, 0.0);
|
||||
let bounds: Option<(f64, f64, f64, f64)> =
|
||||
polygon.iter().fold(None, |bounds, point| {
|
||||
if !point[0].is_finite() || !point[1].is_finite() {
|
||||
return bounds;
|
||||
}
|
||||
Some(match bounds {
|
||||
Some((min_x, min_y, max_x, max_y)) => (
|
||||
min_x.min(point[0] as f64),
|
||||
min_y.min(point[1] as f64),
|
||||
max_x.max(point[0] as f64),
|
||||
max_y.max(point[1] as f64),
|
||||
),
|
||||
None => (
|
||||
point[0] as f64,
|
||||
point[1] as f64,
|
||||
point[0] as f64,
|
||||
point[1] as f64,
|
||||
),
|
||||
})
|
||||
});
|
||||
let Some((min_x, min_y, max_x, max_y)) = bounds else {
|
||||
self.command_line
|
||||
.push_error("MVIEW: the clipping boundary has no usable area.");
|
||||
self.tabs[i].active_cmd = None;
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
return Task::none();
|
||||
};
|
||||
if max_x - min_x < 1e-6 || max_y - min_y < 1e-6 {
|
||||
self.command_line
|
||||
.push_error("MVIEW: the clipping boundary has no usable area.");
|
||||
self.tabs[i].active_cmd = None;
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
let mut viewport = acadrust::entities::Viewport::new();
|
||||
viewport.center = acadrust::types::Vector3::new(
|
||||
(min_x + max_x) / 2.0,
|
||||
(min_y + max_y) / 2.0,
|
||||
0.0,
|
||||
);
|
||||
viewport.width = max_x - min_x;
|
||||
viewport.height = max_y - min_y;
|
||||
viewport.id = 2;
|
||||
viewport.clip_boundary_handle = clip_handle;
|
||||
let viewport_handle = self.commit_entity_handle(
|
||||
acadrust::EntityType::Viewport(viewport),
|
||||
);
|
||||
if let Some(viewport_handle) = viewport_handle {
|
||||
if !created_boundary {
|
||||
let before = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(clip_handle)
|
||||
.cloned()
|
||||
.map(std::sync::Arc::new);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_undo_before(clip_handle, before);
|
||||
}
|
||||
if let Some(boundary) =
|
||||
self.tabs[i].scene.document.get_entity_mut(clip_handle)
|
||||
{
|
||||
let common = boundary.common_mut();
|
||||
common.invisible = true;
|
||||
if !common.reactors.contains(&viewport_handle) {
|
||||
common.reactors.push(viewport_handle);
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.bump_entities(&[(
|
||||
clip_handle,
|
||||
crate::scene::ChangeKind::Modified,
|
||||
)]);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
}
|
||||
CmdResult::MviewSwitchLayout(layout) => {
|
||||
let task = self.on_layout_switch(layout);
|
||||
if let Some(prompt) =
|
||||
self.tabs[i].active_cmd.as_ref().map(|command| command.prompt())
|
||||
{
|
||||
self.command_line.push_info(&prompt);
|
||||
}
|
||||
return task;
|
||||
}
|
||||
CmdResult::MviewCancelToLayout(layout) => {
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
return self.on_layout_switch(layout);
|
||||
}
|
||||
CmdResult::TransformSelected(handles, transform) => {
|
||||
let label = self.history_label_from_active_cmd(i, "MOVE");
|
||||
// A move/rotate/scale/mirror mutates only the selected entities
|
||||
|
|
|
|||
|
|
@ -412,7 +412,14 @@ impl OpenCADStudio {
|
|||
.push_error("MVIEW: switch to a paper space layout first.");
|
||||
} else {
|
||||
use crate::modules::layout::mview::MviewCommand;
|
||||
let new_cmd = MviewCommand::new();
|
||||
let scene = &self.tabs[i].scene;
|
||||
let layout = scene.current_layout.clone();
|
||||
let paper_bounds = scene
|
||||
.printable_area_limits()
|
||||
.or_else(|| scene.paper_limits())
|
||||
.unwrap_or(((0.0, 0.0), (297.0, 210.0)));
|
||||
let views = scene.document.views.iter().cloned().collect();
|
||||
let new_cmd = MviewCommand::new(layout, paper_bounds, views);
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3675,7 +3675,7 @@ impl OpenCADStudio {
|
|||
))
|
||||
}
|
||||
|
||||
pub(super) fn on_layout_switch(&mut self, name: String) -> Task<Message> {
|
||||
pub(crate) fn on_layout_switch(&mut self, name: String) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
if self.tabs[i].is_start {
|
||||
self.command_line
|
||||
|
|
|
|||
|
|
@ -906,6 +906,23 @@ pub enum CmdResult {
|
|||
},
|
||||
/// Set the plot window on the active layout's PlotSettings.
|
||||
SetPlotWindow { p1: DVec3, p2: DVec3 },
|
||||
/// Create a paper-space viewport. `preserve_view` keeps an explicitly
|
||||
/// selected/defined view instead of applying the normal model-extents fit.
|
||||
MviewCreate {
|
||||
viewport: acadrust::entities::Viewport,
|
||||
preserve_view: bool,
|
||||
},
|
||||
/// Create a viewport clipped by either a new polygon boundary or an
|
||||
/// existing closed paper-space entity.
|
||||
MviewCreateClipped {
|
||||
boundary: Option<EntityType>,
|
||||
boundary_handle: Handle,
|
||||
},
|
||||
/// Temporarily switch between paper and Model while MVIEW defines a new
|
||||
/// model-space window, keeping the command active.
|
||||
MviewSwitchLayout(String),
|
||||
/// Cancel MVIEW's temporary Model-space step and return to its layout.
|
||||
MviewCancelToLayout(String),
|
||||
/// Quick-print the bounding box of the given selected entities to a PDF.
|
||||
QuickPrint(Vec<Handle>),
|
||||
/// Replace the text content of a Text/MText entity in-place.
|
||||
|
|
|
|||
|
|
@ -458,6 +458,12 @@ impl crate::entities::traits::FallbackTess for Viewport {
|
|||
fn fallback_geometry(
|
||||
&self,
|
||||
) -> crate::scene::convert::tess_util::FallbackGeometry {
|
||||
// A clipped viewport uses its linked boundary entity as its visible
|
||||
// frame. Drawing the viewport's rectangular extents as well leaves an
|
||||
// incorrect box around polygonal/Object MVIEW results.
|
||||
if !self.clip_boundary_handle.is_null() {
|
||||
return (vec![], vec![], vec![], vec![]);
|
||||
}
|
||||
let cx = self.center.x;
|
||||
let cy = self.center.y;
|
||||
let cz = self.center.z;
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ impl PlineCommand {
|
|||
|
||||
/// Compute the bulge for the arc from `a` to `b` that is tangent to `tangent` at `a`.
|
||||
/// Returns 0.0 if the points are coincident or the tangent is parallel to the chord.
|
||||
fn compute_bulge(a: DVec2, tangent: DVec2, b: DVec2) -> f64 {
|
||||
pub(crate) fn compute_bulge(a: DVec2, tangent: DVec2, b: DVec2) -> f64 {
|
||||
let d = b - a;
|
||||
let len_sq = d.length_squared();
|
||||
if len_sq < 1e-10 {
|
||||
|
|
@ -178,7 +178,7 @@ fn compute_bulge(a: DVec2, tangent: DVec2, b: DVec2) -> f64 {
|
|||
/// the chord direction rotated by half the arc sweep (the chord bisects the
|
||||
/// entry/exit tangents of a bulge arc). Used to restore tangent continuity
|
||||
/// after Undo pops a segment.
|
||||
fn seg_exit_tangent(a: DVec3, b: DVec3, bulge: f64) -> Option<Vec2> {
|
||||
pub(crate) fn seg_exit_tangent(a: DVec3, b: DVec3, bulge: f64) -> Option<Vec2> {
|
||||
let d = DVec2::new(b.x - a.x, b.y - a.y);
|
||||
if d.length_squared() < 1e-10 {
|
||||
return None;
|
||||
|
|
@ -189,7 +189,7 @@ fn seg_exit_tangent(a: DVec3, b: DVec3, bulge: f64) -> Option<Vec2> {
|
|||
}
|
||||
|
||||
/// Update `tangent` after an arc segment described by `bulge` from `a` to `b`.
|
||||
fn update_tangent_after_arc(tangent: &mut Option<Vec2>, bulge: f64) {
|
||||
pub(crate) fn update_tangent_after_arc(tangent: &mut Option<Vec2>, bulge: f64) {
|
||||
let Some(t) = *tangent else {
|
||||
return;
|
||||
};
|
||||
|
|
@ -203,7 +203,7 @@ fn update_tangent_after_arc(tangent: &mut Option<Vec2>, bulge: f64) {
|
|||
|
||||
/// Sample a circular arc defined by bulge into `n` line-segment points.
|
||||
/// Returns the sampled [x, y, z] points (uses `z` from `a`).
|
||||
fn arc_sample_points(a: Vec3, bulge: f64, b: Vec3, n: usize) -> Vec<[f32; 3]> {
|
||||
pub(crate) fn arc_sample_points(a: Vec3, bulge: f64, b: Vec3, n: usize) -> Vec<[f32; 3]> {
|
||||
let ax = a.x as f64;
|
||||
let ay = a.y as f64;
|
||||
let bx = b.x as f64;
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
// MVIEW — interactive paper-space viewport creation.
|
||||
//
|
||||
// Two clicks define opposite corners of a new viewport rectangle.
|
||||
// The created Viewport entity is routed to add_entity_to_layout by apply_cmd_result
|
||||
// because we're in paper space.
|
||||
|
||||
use acadrust::entities::Viewport;
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::EntityType;
|
||||
use acadrust::entities::{LwPolyline, LwVertex, Viewport};
|
||||
use acadrust::tables::View;
|
||||
use acadrust::types::{Vector2, Vector3};
|
||||
use acadrust::{EntityType, Handle};
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult};
|
||||
use crate::modules::draw::draw::polyline::{
|
||||
arc_sample_points, compute_bulge, seg_exit_tangent, update_tangent_after_arc,
|
||||
};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use glam::DVec3;
|
||||
use glam::{DVec2, DVec3, Vec2};
|
||||
|
||||
// ── Ribbon definition ─────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -26,13 +26,237 @@ pub fn tool() -> ToolDef {
|
|||
|
||||
// ── Command ───────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Step {
|
||||
RectangleFirst,
|
||||
RectangleSecond,
|
||||
Polygon,
|
||||
Object,
|
||||
ChooseView,
|
||||
DefineNewFirst,
|
||||
DefineNewSecond,
|
||||
PlaceView,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum PolygonMode {
|
||||
Line,
|
||||
Arc,
|
||||
}
|
||||
|
||||
pub struct MviewCommand {
|
||||
corner1: Option<DVec3>,
|
||||
step: Step,
|
||||
first: Option<DVec3>,
|
||||
polygon: Vec<DVec3>,
|
||||
polygon_bulges: Vec<f64>,
|
||||
polygon_mode: PolygonMode,
|
||||
polygon_last_tangent: Option<Vec2>,
|
||||
view: Option<View>,
|
||||
views: Vec<View>,
|
||||
paper_bounds: ((f64, f64), (f64, f64)),
|
||||
original_layout: String,
|
||||
}
|
||||
|
||||
impl MviewCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { corner1: None }
|
||||
pub fn new(
|
||||
original_layout: String,
|
||||
paper_bounds: ((f64, f64), (f64, f64)),
|
||||
views: Vec<View>,
|
||||
) -> Self {
|
||||
Self {
|
||||
step: Step::RectangleFirst,
|
||||
first: None,
|
||||
polygon: Vec::new(),
|
||||
polygon_bulges: Vec::new(),
|
||||
polygon_mode: PolygonMode::Line,
|
||||
polygon_last_tangent: None,
|
||||
view: None,
|
||||
views,
|
||||
paper_bounds,
|
||||
original_layout,
|
||||
}
|
||||
}
|
||||
|
||||
fn viewport_from_corners(a: DVec3, b: DVec3) -> Option<Viewport> {
|
||||
let width = (b.x - a.x).abs();
|
||||
let height = (b.y - a.y).abs();
|
||||
if width < 1e-6 || height < 1e-6 {
|
||||
return None;
|
||||
}
|
||||
let mut viewport = Viewport::new();
|
||||
viewport.center = Vector3::new(
|
||||
(a.x + b.x) / 2.0,
|
||||
(a.y + b.y) / 2.0,
|
||||
a.z,
|
||||
);
|
||||
viewport.width = width;
|
||||
viewport.height = height;
|
||||
viewport.id = 2;
|
||||
Some(viewport)
|
||||
}
|
||||
|
||||
fn fit_viewport(&self) -> Option<Viewport> {
|
||||
let ((x0, y0), (x1, y1)) = self.paper_bounds;
|
||||
Self::viewport_from_corners(
|
||||
DVec3::new(x0, y0, 0.0),
|
||||
DVec3::new(x1, y1, 0.0),
|
||||
)
|
||||
}
|
||||
|
||||
fn placed_viewport(&self, center: DVec3) -> Option<Viewport> {
|
||||
let view = self.view.as_ref()?;
|
||||
let source_width = view.width.abs().max(1e-6);
|
||||
let source_height = view.height.abs().max(1e-6);
|
||||
let ((x0, y0), (x1, y1)) = self.paper_bounds;
|
||||
let max_width = ((x1 - x0).abs() * 0.5).max(1e-6);
|
||||
let max_height = ((y1 - y0).abs() * 0.5).max(1e-6);
|
||||
let aspect = source_width / source_height;
|
||||
let (width, height) = if max_width / max_height > aspect {
|
||||
(max_height * aspect, max_height)
|
||||
} else {
|
||||
(max_width, max_width / aspect)
|
||||
};
|
||||
|
||||
let mut viewport = Viewport::new();
|
||||
viewport.center = Vector3::new(center.x, center.y, center.z);
|
||||
viewport.width = width;
|
||||
viewport.height = height;
|
||||
viewport.id = 2;
|
||||
viewport.view_target = view.target.clone();
|
||||
viewport.view_direction = view.direction.clone();
|
||||
viewport.view_height = source_height;
|
||||
viewport.custom_scale = height / source_height;
|
||||
viewport.lens_length = view.lens_length;
|
||||
viewport.twist_angle = view.twist_angle;
|
||||
viewport.status.perspective = view.perspective;
|
||||
Some(viewport)
|
||||
}
|
||||
|
||||
fn polygon_boundary(&self) -> Option<EntityType> {
|
||||
if self.polygon.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
let mut polyline = LwPolyline::new();
|
||||
polyline.is_closed = true;
|
||||
polyline.elevation = self.polygon[0].z;
|
||||
polyline.vertices = self
|
||||
.polygon
|
||||
.iter()
|
||||
.zip(self.polygon_bulges.iter())
|
||||
.map(|(point, bulge)| {
|
||||
let mut vertex = LwVertex::new(Vector2::new(point.x, point.y));
|
||||
vertex.bulge = *bulge;
|
||||
vertex
|
||||
})
|
||||
.collect();
|
||||
Some(EntityType::LwPolyline(polyline))
|
||||
}
|
||||
|
||||
fn finish_polygon(&self) -> CmdResult {
|
||||
match self.polygon_boundary() {
|
||||
Some(boundary) => CmdResult::MviewCreateClipped {
|
||||
boundary: Some(boundary),
|
||||
boundary_handle: Handle::NULL,
|
||||
},
|
||||
None => CmdResult::Cancel,
|
||||
}
|
||||
}
|
||||
|
||||
fn undo_polygon(&mut self) -> CmdResult {
|
||||
self.polygon.pop();
|
||||
self.polygon_bulges.pop();
|
||||
let count = self.polygon.len();
|
||||
self.polygon_last_tangent = if count >= 2 {
|
||||
seg_exit_tangent(
|
||||
self.polygon[count - 2],
|
||||
self.polygon[count - 1],
|
||||
self.polygon_bulges[count - 2],
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn polygon_preview(&self, cursor: DVec3) -> Option<WireModel> {
|
||||
let last = self.polygon.last()?.as_vec3();
|
||||
let mut points: Vec<[f32; 3]> = Vec::new();
|
||||
|
||||
for index in 0..self.polygon.len().saturating_sub(1) {
|
||||
let start = self.polygon[index].as_vec3();
|
||||
let end = self.polygon[index + 1].as_vec3();
|
||||
let bulge = self.polygon_bulges[index];
|
||||
if bulge.abs() < 1e-10 {
|
||||
if points.is_empty() {
|
||||
points.push([start.x, start.y, start.z]);
|
||||
}
|
||||
points.push([end.x, end.y, end.z]);
|
||||
} else {
|
||||
let sampled = arc_sample_points(start, bulge, end, 16);
|
||||
if points.is_empty() {
|
||||
points.extend_from_slice(&sampled);
|
||||
} else {
|
||||
points.extend_from_slice(&sampled[1..]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cursor = cursor.as_vec3();
|
||||
match self.polygon_mode {
|
||||
PolygonMode::Line => {
|
||||
if points.is_empty() {
|
||||
points.push([last.x, last.y, last.z]);
|
||||
}
|
||||
points.push([cursor.x, cursor.y, cursor.z]);
|
||||
}
|
||||
PolygonMode::Arc => {
|
||||
let tangent = self
|
||||
.polygon_last_tangent
|
||||
.map(|value| value.as_dvec2())
|
||||
.unwrap_or(DVec2::new(1.0, 0.0));
|
||||
let bulge = compute_bulge(
|
||||
DVec2::new(last.x as f64, last.y as f64),
|
||||
tangent,
|
||||
DVec2::new(cursor.x as f64, cursor.y as f64),
|
||||
);
|
||||
let sampled = arc_sample_points(last, bulge, cursor, 16);
|
||||
if points.is_empty() {
|
||||
points.extend_from_slice(&sampled);
|
||||
} else {
|
||||
points.extend_from_slice(&sampled[1..]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(WireModel::solid(
|
||||
"mview_preview".to_string(),
|
||||
points,
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
))
|
||||
}
|
||||
|
||||
fn select_view(&mut self, name: &str) -> Option<CmdResult> {
|
||||
let view = self
|
||||
.views
|
||||
.iter()
|
||||
.find(|view| view.name.eq_ignore_ascii_case(name.trim()))?
|
||||
.clone();
|
||||
self.view = Some(view);
|
||||
self.step = Step::PlaceView;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
|
||||
fn preview(points: Vec<DVec3>) -> Option<WireModel> {
|
||||
if points.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
Some(WireModel::solid_f64(
|
||||
"mview_preview".to_string(),
|
||||
points.iter().map(|point| [point.x, point.y, point.z]).collect(),
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -42,77 +266,305 @@ impl CadCommand for MviewCommand {
|
|||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if self.corner1.is_none() {
|
||||
"MVIEW Specify first corner:".to_string()
|
||||
} else {
|
||||
"MVIEW Specify opposite corner:".to_string()
|
||||
match self.step {
|
||||
Step::RectangleFirst => {
|
||||
"MVIEW Specify corner of viewport or [Polygonal/Object/Fit/Insert view]:".into()
|
||||
}
|
||||
Step::RectangleSecond => "MVIEW Specify opposite corner:".into(),
|
||||
Step::Polygon if self.polygon.is_empty() => {
|
||||
"MVIEW Polygonal Specify start point:".into()
|
||||
}
|
||||
Step::Polygon => {
|
||||
let mode = match self.polygon_mode {
|
||||
PolygonMode::Line => "Line",
|
||||
PolygonMode::Arc => "Arc",
|
||||
};
|
||||
format!(
|
||||
"MVIEW Polygonal [{mode}] Specify next point or [Arc/Line/Close/Undo] ({} points):",
|
||||
self.polygon.len()
|
||||
)
|
||||
}
|
||||
Step::Object => {
|
||||
"MVIEW Object Select a circle, full ellipse, or closed polyline:".into()
|
||||
}
|
||||
Step::ChooseView if self.views.is_empty() => {
|
||||
"MVIEW Insert view No named views; choose [New]:".into()
|
||||
}
|
||||
Step::ChooseView => "MVIEW Insert view Choose a named view or [New]:".into(),
|
||||
Step::DefineNewFirst => "MVIEW New view Specify first model-space corner:".into(),
|
||||
Step::DefineNewSecond => "MVIEW New view Specify opposite model-space corner:".into(),
|
||||
Step::PlaceView => "MVIEW Insert view Specify placement point:".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
match self.step {
|
||||
Step::RectangleFirst => vec![
|
||||
CmdOption::new("Polygonal", "POLYGONAL"),
|
||||
CmdOption::new("Object", "OBJECT"),
|
||||
CmdOption::new("Fit", "FIT"),
|
||||
CmdOption::new("Insert view", "INSERT"),
|
||||
],
|
||||
Step::Polygon if !self.polygon.is_empty() => vec![
|
||||
CmdOption::new("Arc", "ARC"),
|
||||
CmdOption::new("Line", "LINE"),
|
||||
CmdOption::new("Close", "CLOSE"),
|
||||
CmdOption::new("Undo", "UNDO"),
|
||||
CmdOption::enter("Done"),
|
||||
],
|
||||
Step::ChooseView => {
|
||||
let mut options = vec![CmdOption::new("New", "NEW")];
|
||||
options.extend(
|
||||
self.views
|
||||
.iter()
|
||||
.map(|view| CmdOption::new(&view.name, &view.name)),
|
||||
);
|
||||
options
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
if let Some(c1) = self.corner1 {
|
||||
let w = (pt.x - c1.x).abs();
|
||||
let h = (pt.y - c1.y).abs();
|
||||
if w < 1.0 || h < 1.0 {
|
||||
return CmdResult::Cancel;
|
||||
match self.step {
|
||||
Step::RectangleFirst => {
|
||||
self.first = Some(pt);
|
||||
self.step = Step::RectangleSecond;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
let cx = (c1.x + pt.x) / 2.0;
|
||||
let cy = (c1.y + pt.y) / 2.0;
|
||||
let cz = c1.z;
|
||||
Step::RectangleSecond => match self
|
||||
.first
|
||||
.and_then(|first| Self::viewport_from_corners(first, pt))
|
||||
{
|
||||
Some(viewport) => CmdResult::MviewCreate {
|
||||
viewport,
|
||||
preserve_view: false,
|
||||
},
|
||||
None => CmdResult::NeedPoint,
|
||||
},
|
||||
Step::Polygon => {
|
||||
if let Some(last) = self.polygon.last().copied() {
|
||||
let last_index = self.polygon.len() - 1;
|
||||
let bulge = match self.polygon_mode {
|
||||
PolygonMode::Line => {
|
||||
let direction = DVec2::new(pt.x - last.x, pt.y - last.y);
|
||||
if direction.length_squared() > 1e-10 {
|
||||
self.polygon_last_tangent =
|
||||
Some(direction.normalize().as_vec2());
|
||||
}
|
||||
0.0
|
||||
}
|
||||
PolygonMode::Arc => {
|
||||
let tangent = self
|
||||
.polygon_last_tangent
|
||||
.map(|value| value.as_dvec2())
|
||||
.unwrap_or(DVec2::new(1.0, 0.0));
|
||||
let bulge = compute_bulge(
|
||||
DVec2::new(last.x, last.y),
|
||||
tangent,
|
||||
DVec2::new(pt.x, pt.y),
|
||||
);
|
||||
update_tangent_after_arc(
|
||||
&mut self.polygon_last_tangent,
|
||||
bulge,
|
||||
);
|
||||
bulge
|
||||
}
|
||||
};
|
||||
self.polygon_bulges[last_index] = bulge;
|
||||
}
|
||||
|
||||
let mut vp = Viewport::new();
|
||||
vp.center = Vector3::new(cx, cy, cz);
|
||||
vp.width = w;
|
||||
vp.height = h;
|
||||
vp.id = 2; // user viewport (id > 1)
|
||||
if let Some(first) = self.polygon.first() {
|
||||
let distance_squared =
|
||||
(pt.x - first.x).powi(2) + (pt.y - first.y).powi(2);
|
||||
if self.polygon.len() >= 3 && distance_squared < 1e-12 {
|
||||
return self.finish_polygon();
|
||||
}
|
||||
}
|
||||
|
||||
CmdResult::CommitAndExit(EntityType::Viewport(vp))
|
||||
} else {
|
||||
self.corner1 = Some(pt);
|
||||
CmdResult::NeedPoint
|
||||
self.polygon.push(pt);
|
||||
self.polygon_bulges.push(0.0);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::DefineNewFirst => {
|
||||
self.first = Some(pt);
|
||||
self.step = Step::DefineNewSecond;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::DefineNewSecond => {
|
||||
let Some(first) = self.first else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
let width = (pt.x - first.x).abs();
|
||||
let height = (pt.y - first.y).abs();
|
||||
if width < 1e-6 || height < 1e-6 {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
let mut view = View::new("");
|
||||
view.width = width;
|
||||
view.height = height;
|
||||
view.target = Vector3::new(
|
||||
(first.x + pt.x) / 2.0,
|
||||
(first.y + pt.y) / 2.0,
|
||||
(first.z + pt.z) / 2.0,
|
||||
);
|
||||
self.view = Some(view);
|
||||
self.step = Step::PlaceView;
|
||||
CmdResult::MviewSwitchLayout(self.original_layout.clone())
|
||||
}
|
||||
Step::PlaceView => match self.placed_viewport(pt) {
|
||||
Some(viewport) => CmdResult::MviewCreate {
|
||||
viewport,
|
||||
preserve_view: true,
|
||||
},
|
||||
None => CmdResult::Cancel,
|
||||
},
|
||||
Step::Object | Step::ChooseView => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
match self.step {
|
||||
Step::Polygon if self.polygon.len() >= 3 => self.finish_polygon(),
|
||||
Step::DefineNewFirst | Step::DefineNewSecond => {
|
||||
CmdResult::MviewCancelToLayout(self.original_layout.clone())
|
||||
}
|
||||
_ => CmdResult::Cancel,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
match self.step {
|
||||
Step::DefineNewFirst | Step::DefineNewSecond => {
|
||||
CmdResult::MviewCancelToLayout(self.original_layout.clone())
|
||||
}
|
||||
_ => CmdResult::Cancel,
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
self.step == Step::Object
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
CmdResult::MviewCreateClipped {
|
||||
boundary: None,
|
||||
boundary_handle: handle,
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
self.step == Step::RectangleFirst
|
||||
|| self.step == Step::ChooseView
|
||||
|| (self.step == Step::Polygon && !self.polygon.is_empty())
|
||||
}
|
||||
|
||||
fn wants_text_with_spaces(&self) -> bool {
|
||||
self.step == Step::ChooseView
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
self.step == Step::RectangleFirst
|
||||
|| (self.step == Step::Polygon && !self.polygon.is_empty())
|
||||
}
|
||||
|
||||
fn window_corner_pick(&self) -> bool {
|
||||
matches!(self.step, Step::RectangleSecond | Step::DefineNewSecond)
|
||||
}
|
||||
|
||||
fn window_first_corner(&self) -> Option<DVec3> {
|
||||
self.window_corner_pick().then_some(self.first).flatten()
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let keyword = text.trim();
|
||||
let upper = keyword.to_ascii_uppercase();
|
||||
match self.step {
|
||||
Step::RectangleFirst => match upper.as_str() {
|
||||
"P" | "POLYGONAL" => {
|
||||
self.step = Step::Polygon;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"O" | "OBJECT" => {
|
||||
self.step = Step::Object;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"F" | "FIT" => self.fit_viewport().map(|viewport| CmdResult::MviewCreate {
|
||||
viewport,
|
||||
preserve_view: false,
|
||||
}),
|
||||
"I" | "INSERT" | "INSERTVIEW" | "INSERT VIEW" => {
|
||||
self.step = Step::ChooseView;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
Step::Polygon => match upper.as_str() {
|
||||
"A" | "ARC" if !self.polygon.is_empty() => {
|
||||
self.polygon_mode = PolygonMode::Arc;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"L" | "LINE" if !self.polygon.is_empty() => {
|
||||
self.polygon_mode = PolygonMode::Line;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"C" | "CLOSE" if self.polygon.len() >= 3 => Some(self.finish_polygon()),
|
||||
"U" | "UNDO" if !self.polygon.is_empty() => {
|
||||
Some(self.undo_polygon())
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
Step::ChooseView => {
|
||||
if matches!(upper.as_str(), "N" | "NEW") {
|
||||
self.first = None;
|
||||
self.step = Step::DefineNewFirst;
|
||||
Some(CmdResult::MviewSwitchLayout("Model".to_string()))
|
||||
} else {
|
||||
self.select_view(keyword)
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_undo_step(&mut self) -> Option<CmdResult> {
|
||||
if self.step == Step::Polygon && !self.polygon.is_empty() {
|
||||
Some(self.undo_polygon())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
let pt = pt.as_vec3();
|
||||
let c1 = self.corner1?;
|
||||
Some(WireModel {
|
||||
taper_widths: Vec::new(),
|
||||
world_width: 0.0,
|
||||
depth_override: None,
|
||||
fill_is_3d: false,
|
||||
pick_tris: Vec::new(),
|
||||
pick_tris_low: Vec::new(),
|
||||
dash_from_start: false,
|
||||
dash_align_end: None,
|
||||
text_verts: Vec::new(),
|
||||
name: "mview_preview".to_string(),
|
||||
points: vec![
|
||||
[c1.x as f32, c1.y as f32, c1.z as f32],
|
||||
[pt.x, c1.y as f32, c1.z as f32],
|
||||
[pt.x, pt.y, c1.z as f32],
|
||||
[c1.x as f32, pt.y, c1.z as f32],
|
||||
[c1.x as f32, c1.y as f32, c1.z as f32],
|
||||
],
|
||||
points_low: Vec::new(),
|
||||
color: WireModel::CYAN,
|
||||
selected: false,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px: 1.0,
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
aci: 0,
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
fill_tris: vec![],
|
||||
fill_tris_low: Vec::new(),
|
||||
})
|
||||
match self.step {
|
||||
Step::RectangleSecond => {
|
||||
let first = self.first?;
|
||||
Self::preview(vec![
|
||||
first,
|
||||
DVec3::new(pt.x, first.y, first.z),
|
||||
DVec3::new(pt.x, pt.y, first.z),
|
||||
DVec3::new(first.x, pt.y, first.z),
|
||||
first,
|
||||
])
|
||||
}
|
||||
Step::Polygon => self.polygon_preview(pt),
|
||||
Step::PlaceView => {
|
||||
let viewport = self.placed_viewport(pt)?;
|
||||
let half_width = viewport.width / 2.0;
|
||||
let half_height = viewport.height / 2.0;
|
||||
Self::preview(vec![
|
||||
DVec3::new(pt.x - half_width, pt.y - half_height, pt.z),
|
||||
DVec3::new(pt.x + half_width, pt.y - half_height, pt.z),
|
||||
DVec3::new(pt.x + half_width, pt.y + half_height, pt.z),
|
||||
DVec3::new(pt.x - half_width, pt.y + half_height, pt.z),
|
||||
DVec3::new(pt.x - half_width, pt.y - half_height, pt.z),
|
||||
])
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 ───────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Reference in a new issue