Files
OneCAD-Draft/src/scene/project.rs
T
Hakan Seven b8889ccd20 feat(mview): add viewport creation options
Support polygonal and object viewports through native clip-boundary handles, including clipped rendering and selection.

Closes #525
2026-07-27 19:28:51 +03:00

741 lines
30 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Auto-split from scene/mod.rs. Pure text-move; behaviour unchanged.
use super::*;
impl Scene {
/// Collect model-space wires projected into paper space for all (or one specific)
/// user viewports. `only_vp = Some(h)` restricts output to that viewport.
pub(super) fn viewport_content_wires(
&self,
paper_block: Handle,
only_vp: Option<Handle>,
exclude_vp: Option<Handle>,
) -> Vec<WireModel> {
use acadrust::entities::Viewport;
let (_, _, viewport_handles) = self.paper_viewport_handles();
let viewports: Vec<&Viewport> = viewport_handles
.iter()
.filter_map(|handle| match self.document.get_entity(*handle) {
Some(EntityType::Viewport(vp)) => Some(vp),
_ => None,
})
.filter(|vp| {
vp.common.owner_handle == paper_block
&& vp.status.is_on
&& only_vp.map_or(true, |h| vp.common.handle == h)
&& exclude_vp.map_or(true, |h| vp.common.handle != h)
})
.collect();
if viewports.is_empty() {
return vec![];
}
let mut result = Vec::new();
for vp in viewports {
let vp_handle = vp.common.handle;
// ── Fast path: return cached projected wires ──────────────────
{
let cache = self.paper_projected_cache.borrow();
if let Some((cached_epoch, ref wires)) = cache.get(&vp_handle) {
if *cached_epoch == self.geometry_epoch {
result.extend_from_slice(wires);
continue;
}
}
}
// ── Cache miss: compute projection ────────────────────────────
// Use camera_for_viewport so the axes match the GPU renderer exactly.
let cam_frame = match self.camera_for_viewport(vp_handle) {
Some(c) => c,
None => continue,
};
let view_right = cam_frame.rotation * glam::Vec3::X;
let view_up = cam_frame.rotation * glam::Vec3::Y;
// Scale (paper units per model unit) comes straight from the camera
// the GPU uses: the model height shown is `2 * ortho_size`, mapped
// onto `vp.height` of paper. `camera_for_viewport` already made the
// saved-view-vs-auto-fit decision (with the twist-correct overlap
// test), so deriving scale from it keeps the CPU projection (used
// for hit-test / snap / fit) locked to the GPU render — no second,
// independently-computed scale that could disagree under a twist.
let view_height_eff = (cam_frame.ortho_size() * 2.0) as f64;
let scale = if view_height_eff > 1e-9 {
(vp.height / view_height_eff) as f32
} else {
1.0
};
let pcx = vp.center.x as f32;
let pcy = vp.center.y as f32;
let pcz = vp.center.z as f32;
let hw = (vp.width / 2.0) as f32;
let hh = (vp.height / 2.0) as f32;
// ── Use cached tessellation (model_wires_for_viewport_arc) ────
// This eliminates the per-frame tessellate_one() loop that was here
// previously; tessellation is now O(1) on navigation frames.
// Pass 0.0 for screen height — the CPU-projection / hit-test
// path wants the full-fidelity (no-LOD-stub) wire list,
// regardless of paper zoom.
let model_wires = self.model_wires_for_viewport_arc(vp_handle, 0.0);
// ── Project and clip wires into viewport ──────────────────────
let vp_x0 = pcx - hw;
let vp_x1 = pcx + hw;
let vp_y0 = pcy - hh;
let vp_y1 = pcy + hh;
// camera_dist: how far the camera is from the target plane.
let use_perspective = vp.status.perspective && vp.lens_length > 1.0;
let camera_dist = if use_perspective {
(vp.view_height as f32 * vp.lens_length as f32 / 24.0).max(0.001)
} else {
0.0
};
let mut projected: Vec<WireModel> = Vec::new();
// Precompute precision-stable WCS-space projection inputs in
// f64. The previous f32 inner loop suffered catastrophic
// cancellation on UTM-scale drawings: `(wire_offset_rel -
// target_offset_rel).dot(view_right) - view_center` is a
// small paper offset computed by subtracting two values at
// ~5e6 magnitude — f32 ULP there is ~0.5 m, so paper output
// jittered by cm even when the actual model was clean.
//
// Do everything WCS-relative in f64; cast to f32 only at the
// final paper position.
// Display centre = the camera's target, in WCS. `camera_for_viewport`
// already folded view_center through the (twisted) view basis and
// applied the empty-WCS auto-fit, so taking its target keeps the CPU
// projection identical to the GPU renderer under any twist.
let display_center_x = cam_frame.target.x as f64 + [0.0_f64; 3][0];
let display_center_y = cam_frame.target.y as f64 + [0.0_f64; 3][1];
let display_center_z = cam_frame.target.z as f64 + [0.0_f64; 3][2];
let view_right_d = (
view_right.x as f64,
view_right.y as f64,
view_right.z as f64,
);
let view_up_d = (view_up.x as f64, view_up.y as f64, view_up.z as f64);
let view_fwd = cam_frame.rotation * glam::Vec3::Z;
let view_fwd_d = (view_fwd.x as f64, view_fwd.y as f64, view_fwd.z as f64);
let camera_dist_d = camera_dist as f64;
let scale_d = scale as f64;
let pcx_d = pcx as f64;
let pcy_d = pcy as f64;
// Project one ABSOLUTE-WCS model point (f64) onto the paper sheet.
// Shared by the polyline points, snap points and key vertices so the
// hit-test / snap geometry lands in the same paper frame the wire is
// drawn in — otherwise snaps and the click-AABB stay in model (UTM)
// space and the cursor never reaches them.
let proj_abs = |ax: f64, ay: f64, az: f64| -> [f32; 3] {
let mp_x = ax - display_center_x;
let mp_y = ay - display_center_y;
let mp_z = az - display_center_z;
let u = mp_x * view_right_d.0 + mp_y * view_right_d.1 + mp_z * view_right_d.2;
let v = mp_x * view_up_d.0 + mp_y * view_up_d.1 + mp_z * view_up_d.2;
if use_perspective {
let d_vd = mp_x * view_fwd_d.0 + mp_y * view_fwd_d.1 + mp_z * view_fwd_d.2;
let fwd = camera_dist_d - d_vd;
if fwd <= 0.001 {
return [f32::NAN; 3];
}
let factor = camera_dist_d / fwd;
[
(pcx_d + u * factor * scale_d) as f32,
(pcy_d + v * factor * scale_d) as f32,
pcz,
]
} else {
[(pcx_d + u * scale_d) as f32, (pcy_d + v * scale_d) as f32, pcz]
}
};
let in_vp = |x: f32, y: f32| x >= vp_x0 && x <= vp_x1 && y >= vp_y0 && y <= vp_y1;
for wire in model_wires.iter() {
let projected_pts: Vec<[f32; 3]> = wire
.points
.iter()
.enumerate()
.map(|(pi, &[mx, my, mz])| {
if mx.is_nan() || my.is_nan() || mz.is_nan() {
return [f32::NAN; 3];
}
// Reconstruct absolute WCS from the double-single high
// (`points`) + low (`points_low`) pair — the high f32
// alone is ~0.5 m off at UTM scale.
let lo = wire.points_low.get(pi).copied().unwrap_or([0.0; 3]);
proj_abs(mx as f64 + lo[0] as f64, my as f64 + lo[1] as f64, mz as f64 + lo[2] as f64)
})
.collect();
// SDF glyph quads ride `text_verts` in absolute WCS, so they need
// the same projection the points get — a bare clone leaves the
// text at model (UTM) coordinates while its own dimension lines
// move to the sheet, putting the glyphs kilometres off the page
// (issue #385 for layout plots). Cull per glyph on the quad's
// centroid: the text analogue of `clip_polyline_to_rect`, since a
// glyph can't be split at the viewport border.
let mut projected_text = if wire.text_verts.is_empty() {
Vec::new()
} else {
model::wire_model::map_text_verts(&wire.text_verts, |x, y, z| {
let p = proj_abs(x, y, z);
(p[0] as f64, p[1] as f64, p[2] as f64)
})
.chunks_exact(6)
.filter(|quad| {
let (sx, sy) = quad.iter().fold((0.0f32, 0.0f32), |(ax, ay), v| {
(ax + v.pos[0] + v.pos_low[0], ay + v.pos[1] + v.pos_low[1])
});
let (cx, cy) = (sx / 6.0, sy / 6.0);
cx.is_finite() && cy.is_finite() && in_vp(cx, cy)
})
.flatten()
.copied()
.collect::<Vec<_>>()
};
// Fast AABB pre-reject.
let any_near = projected_pts.iter().any(|&[x, y, _]| {
x.is_finite()
&& y.is_finite()
&& x >= vp_x0 - 1.0
&& x <= vp_x1 + 1.0
&& y >= vp_y0 - 1.0
&& y <= vp_y1 + 1.0
});
let (min_x, max_x, min_y, max_y) =
projected_pts.iter().filter(|p| p[0].is_finite()).fold(
(
f32::INFINITY,
f32::NEG_INFINITY,
f32::INFINITY,
f32::NEG_INFINITY,
),
|(mnx, mxx, mny, mxy), &[x, y, _]| {
(mnx.min(x), mxx.max(x), mny.min(y), mxy.max(y))
},
);
let aabb_hits =
max_x >= vp_x0 && min_x <= vp_x1 && max_y >= vp_y0 && min_y <= vp_y1;
// A text-only wire (TEXT / MTEXT carry no stroke `points` since
// text went SDF-only) has nothing to pre-reject on, so gate both
// rejections on the glyphs too or it never reaches the sheet.
if !any_near && !aabb_hits && projected_text.is_empty() {
continue;
}
let clipped =
clip_polyline_to_rect(&projected_pts, vp_x0, vp_y0, vp_x1, vp_y1, pcz);
if clipped.is_empty() && projected_text.is_empty() {
continue;
}
// Paper-space AABB of the clipped polyline — the cloned model
// (UTM) AABB would make click_hit's screen-projected pre-reject
// discard the wire (box selection has no pre-reject, which is why
// it kept working while picking didn't).
let mut pmnx = f32::INFINITY;
let mut pmny = f32::INFINITY;
let mut pmxx = f32::NEG_INFINITY;
let mut pmxy = f32::NEG_INFINITY;
for &[x, y, _] in clipped.iter().filter(|p| p[0].is_finite()) {
pmnx = pmnx.min(x);
pmny = pmny.min(y);
pmxx = pmxx.max(x);
pmxy = pmxy.max(y);
}
// Glyphs count towards the paper AABB as well — a text-only wire
// would otherwise stay UNBOUNDED and never pick.
for v in &projected_text {
let (x, y) = (v.pos[0] + v.pos_low[0], v.pos[1] + v.pos_low[1]);
if x.is_finite() && y.is_finite() {
pmnx = pmnx.min(x);
pmny = pmny.min(y);
pmxx = pmxx.max(x);
pmxy = pmxy.max(y);
}
}
// Project snap points + key vertices into the same paper frame,
// keeping only those inside the viewport rect, so endpoint /
// midpoint / centre snaps land on the visible sheet geometry
// instead of the model's UTM coordinates.
let snap_pts: Vec<(glam::DVec3, model::wire_model::SnapHint)> = wire
.snap_pts
.iter()
.filter_map(|(w, h)| {
let p = proj_abs(w.x, w.y, w.z);
(p[0].is_finite() && in_vp(p[0], p[1]))
.then(|| (glam::DVec3::new(p[0] as f64, p[1] as f64, p[2] as f64), *h))
})
.collect();
let key_vertices: Vec<[f64; 3]> = wire
.key_vertices
.iter()
.filter_map(|&[kx, ky, kz]| {
let p = proj_abs(kx, ky, kz);
(p[0].is_finite() && in_vp(p[0], p[1]))
.then(|| [p[0] as f64, p[1] as f64, p[2] as f64])
})
.collect();
let adapted = view::render::adapt_to_bg(wire.color, self.paper_bg_color);
let [r, g, b, a] = adapted;
// Glyphs carry their own per-vertex colour, so dim them through
// the same adapt + 0.80/0.85 the wire colour below gets, or the
// text reads brighter than its own dimension lines.
for v in &mut projected_text {
let [tr, tg, tb, ta] = view::render::adapt_to_bg(v.color, self.paper_bg_color);
v.color = [tr * 0.80, tg * 0.80, tb * 0.80, ta * 0.85];
}
let mut out = wire.clone();
out.points = clipped;
out.text_verts = projected_text;
// Paper coordinates are small sheet units — no relative-to-eye
// residual is needed, and keeping the model wire's points_low
// here would add a model-scale offset to the paper points.
out.points_low = Vec::new();
out.snap_pts = snap_pts;
out.key_vertices = key_vertices;
// Tangent geometry is in model space and can't be trivially
// re-expressed in paper coords — drop it (no tangent snap on
// projected viewport content) rather than snap to UTM.
out.tangent_geoms = Vec::new();
// Same for thickness walls: `points` above were projected and
// clipped into paper coords, and the clone's wall triangles are
// still model-space. Left in place they would hit-test at a
// model-scale offset — a stray catch somewhere out on the sheet.
// Drop them: an extruded entity inside a floating viewport stays
// selectable by its edges, which do get projected.
out.pick_tris = Vec::new();
out.pick_tris_low = Vec::new();
out.aabb = if pmnx.is_finite() {
[pmnx, pmny, pmxx, pmxy]
} else {
WireModel::UNBOUNDED_AABB
};
out.color = [r * 0.80, g * 0.80, b * 0.80, a * 0.85];
out.line_weight_px = wire.line_weight_px;
// Wire patterns stay in their base model units. Projection
// normally scales them with the geometry; PSLTSCALE instead
// keeps dash sizes constant in paper units (the on-screen path
// applies the same factor through its viewport uniform).
let dash_scale = if self.document.header.paper_space_linetype_scaling {
1.0
} else {
scale
};
out.pattern_length = wire.pattern_length * dash_scale;
out.pattern = wire.pattern.map(|v| v * dash_scale);
projected.push(out);
}
// Store in cache, then extend result.
self.paper_projected_cache
.borrow_mut()
.insert(vp_handle, (self.geometry_epoch, projected.clone()));
result.extend(projected);
}
result
}
/// A content viewport's clip boundary, projected into that viewport's
/// render-target normalized device coords. Every content viewport goes
/// through the same stencil path: a rectangular viewport contributes its
/// own four corners (a full-target mask, since the render rectangle already
/// clips the edges), a viewport with a `clip_boundary_handle` contributes
/// that entity's tessellated outline. Returns empty only when the viewport
/// is degenerate.
///
/// `uo/vo/us/vs` are the visible-sub-rect crop the content camera applies
/// (see `crop_view_proj`); the same crop is applied here so the boundary
/// lines up with the content even when the viewport hangs off-canvas.
pub(super) fn viewport_clip_boundary_ndc(
&self,
vp_handle: Handle,
uo: f32,
vo: f32,
us: f32,
vs: f32,
) -> Vec<[f32; 2]> {
use acadrust::entities::Viewport;
let Some(EntityType::Viewport(vp)) = self.document.get_entity(vp_handle) else {
return vec![];
};
let vp: &Viewport = vp;
let pcx = vp.center.x as f32;
let pcy = vp.center.y as f32;
let z = vp.center.z as f32;
let hw = (vp.width / 2.0) as f32;
let hh = (vp.height / 2.0) as f32;
if hw.abs() < 1e-6 || hh.abs() < 1e-6 {
return vec![];
}
let poly = if vp.clip_boundary_handle.is_null() {
// Rectangular viewport → its own four corners (paper coords). These
// map to full-rect NDC [-1, 1], a no-op mask over a render target
// that already clips to the rectangle, but it keeps rect and
// non-rect viewports on one uniform stencil path.
vec![
[pcx - hw, pcy - hh, z],
[pcx + hw, pcy - hh, z],
[pcx + hw, pcy + hh, z],
[pcx - hw, pcy + hh, z],
]
} else {
self.clip_boundary_polygon(vp.clip_boundary_handle, z)
};
if poly.len() < 3 {
return vec![];
}
// Paper point → full-rectangle NDC ([-1, 1] across the viewport rect,
// Y up, matching the content camera + `viewport_screen_rect`), then the
// same crop the content camera uses to map its visible sub-rect back to
// NDC [-1, 1].
let us = us.max(1e-6);
let vs = vs.max(1e-6);
let sx = 1.0 / us;
let sy = 1.0 / vs;
let tx = (1.0 - 2.0 * uo - us) / us;
let ty = -(1.0 - 2.0 * vo - vs) / vs;
poly.iter()
.map(|p| {
let nx = (p[0] - pcx) / hw;
let ny = (p[1] - pcy) / hh;
[sx * nx + tx, sy * ny + ty]
})
.collect()
}
/// Tessellated outline of a non-rectangular viewport / XCLIP clip-boundary
/// 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 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()
} 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 ───────────────────────────────────────
/// Clip a single segment (x0,y0)→(x1,y1) against the axis-aligned rectangle
/// [xmin,xmax]×[ymin,ymax]. Returns the clipped endpoints or `None` if the
/// segment is entirely outside.
fn cs_clip(
mut x0: f32,
mut y0: f32,
mut x1: f32,
mut y1: f32,
xmin: f32,
ymin: f32,
xmax: f32,
ymax: f32,
) -> Option<(f32, f32, f32, f32)> {
const LEFT: u8 = 1;
const RIGHT: u8 = 2;
const BOTTOM: u8 = 4;
const TOP: u8 = 8;
let code = |x: f32, y: f32| -> u8 {
let mut c = 0u8;
if x < xmin {
c |= LEFT;
} else if x > xmax {
c |= RIGHT;
}
if y < ymin {
c |= BOTTOM;
} else if y > ymax {
c |= TOP;
}
c
};
let mut c0 = code(x0, y0);
let mut c1 = code(x1, y1);
loop {
if c0 | c1 == 0 {
return Some((x0, y0, x1, y1));
}
if c0 & c1 != 0 {
return None;
}
let cout = if c0 != 0 { c0 } else { c1 };
let (x, y);
if cout & TOP != 0 {
x = x0 + (x1 - x0) * (ymax - y0) / (y1 - y0);
y = ymax;
} else if cout & BOTTOM != 0 {
x = x0 + (x1 - x0) * (ymin - y0) / (y1 - y0);
y = ymin;
} else if cout & RIGHT != 0 {
y = y0 + (y1 - y0) * (xmax - x0) / (x1 - x0);
x = xmax;
} else {
y = y0 + (y1 - y0) * (xmin - x0) / (x1 - x0);
x = xmin;
}
if cout == c0 {
x0 = x;
y0 = y;
c0 = code(x0, y0);
} else {
x1 = x;
y1 = y;
c1 = code(x1, y1);
}
}
}
/// Clip a projected polyline (NaN-separated segments) to the viewport rectangle.
/// Returns a new points vec with proper NaN separators at clip boundaries.
fn clip_polyline_to_rect(
pts: &[[f32; 3]],
xmin: f32,
ymin: f32,
xmax: f32,
ymax: f32,
z: f32,
) -> Vec<[f32; 3]> {
const NAN3: [f32; 3] = [f32::NAN, f32::NAN, f32::NAN];
let mut result: Vec<[f32; 3]> = Vec::new();
let mut i = 0;
while i < pts.len() {
// Skip NaN separators.
if pts[i][0].is_nan() || pts[i][1].is_nan() {
i += 1;
continue;
}
// Gather contiguous run of finite points.
let start = i;
while i < pts.len() && pts[i][0].is_finite() && pts[i][1].is_finite() {
i += 1;
}
let seg = &pts[start..i];
if seg.len() < 2 {
continue;
}
// Clip each edge and track pen state to insert NaN on lift.
let mut pen_down = false;
for j in 0..seg.len() - 1 {
let [x0, y0, _] = seg[j];
let [x1, y1, _] = seg[j + 1];
match cs_clip(x0, y0, x1, y1, xmin, ymin, xmax, ymax) {
None => {
pen_down = false;
}
Some((cx0, cy0, cx1, cy1)) => {
if !pen_down {
if !result.is_empty() {
result.push(NAN3);
}
result.push([cx0, cy0, z]);
pen_down = true;
} else if let Some(&[lx, ly, _]) = result.last() {
if (lx - cx0).abs() > 1e-4 || (ly - cy0).abs() > 1e-4 {
result.push(NAN3);
result.push([cx0, cy0, z]);
}
}
result.push([cx1, cy1, z]);
// If the exit point was clipped, lift pen.
if (cx1 - x1).abs() > 1e-4 || (cy1 - y1).abs() > 1e-4 {
pen_down = false;
}
}
}
}
}
// Remove trailing NaN.
while result
.last()
.map(|p: &[f32; 3]| p[0].is_nan())
.unwrap_or(false)
{
result.pop();
}
result
}