forked from 1b-agent/opencadstudio-upstream
fix(plot): align paper space output
Use the same area pipeline as model space while keeping Layout bound to the physical sheet and printable margins. Share scale, rotation, clipping, and viewport fills across preview, PDF, and printer output.
This commit is contained in:
1 parent
7285e3de51
commit
eb0861cf6e
8 files changed
+1032
-370
No files matched your search
+15
-37
@@ -1948,7 +1948,6 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
CmdResult::SetPlotWindow { p1, p2 } => {
|
||||
use acadrust::objects::{ObjectType, PlotSettings};
|
||||
let layout_name = self.tabs[i].scene.current_layout.clone();
|
||||
if layout_name == "Model" {
|
||||
// Model space: remember the window (world X/Y) for the plot dialog.
|
||||
@@ -1965,42 +1964,21 @@ impl OpenCADStudio {
|
||||
self.plot_dialog.area = "Window".to_string();
|
||||
self.active_modal = Some(super::ModalKind::Plot);
|
||||
} else {
|
||||
let block_handle = self.tabs[i].scene.current_layout_block_handle_pub();
|
||||
let doc = &mut self.tabs[i].scene.document;
|
||||
let ps_handle = doc.objects.iter().find_map(|(h, obj)| {
|
||||
if let ObjectType::PlotSettings(ps) = obj {
|
||||
if ps.page_name == layout_name {
|
||||
Some(*h)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let ps_entry = match ps_handle {
|
||||
Some(h) => doc.objects.get_mut(&h),
|
||||
None => {
|
||||
let nh = acadrust::Handle::new(doc.next_handle());
|
||||
let ps = PlotSettings::new(layout_name.clone());
|
||||
doc.objects.insert(nh, ObjectType::PlotSettings(ps));
|
||||
doc.objects.get_mut(&nh)
|
||||
}
|
||||
};
|
||||
let _ = block_handle;
|
||||
if let Some(ObjectType::PlotSettings(ps)) = ps_entry {
|
||||
// Convert world-space points to DXF coordinates (X, Z plane → DXF X, Y).
|
||||
let x1 = p1.x.min(p2.x) as f64;
|
||||
let y1 = p1.z.min(p2.z) as f64;
|
||||
let x2 = p1.x.max(p2.x) as f64;
|
||||
let y2 = p1.z.max(p2.z) as f64;
|
||||
ps.set_plot_window(x1, y1, x2, y2);
|
||||
self.push_undo_snapshot(i, "PLOTWINDOW");
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(&format!(
|
||||
"PLOTWINDOW: ({x1:.3},{y1:.3}) → ({x2:.3},{y2:.3})"
|
||||
));
|
||||
}
|
||||
// PLOTWINDOW always describes the plotted layout. In MSPACE
|
||||
// the command points are model coordinates, so map them back
|
||||
// through the active floating viewport first.
|
||||
let p1 = self.tabs[i].scene.model_to_paper(p1);
|
||||
let p2 = self.tabs[i].scene.model_to_paper(p2);
|
||||
let x1 = p1.x.min(p2.x);
|
||||
let y1 = p1.y.min(p2.y);
|
||||
let x2 = p1.x.max(p2.x);
|
||||
let y2 = p1.y.max(p2.y);
|
||||
self.plot_window = Some((x1, y1, x2, y2));
|
||||
self.command_line.push_output(&format!(
|
||||
"Plot window: {x1:.2},{y1:.2} to {x2:.2},{y2:.2}"
|
||||
));
|
||||
self.plot_dialog.area = "Window".to_string();
|
||||
self.active_modal = Some(super::ModalKind::Plot);
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
|
||||
+582
-321
File diff suppressed because it is too large.
Load diff
+15
-5
@@ -170,9 +170,12 @@ fn build_pdf(
|
||||
let needs_state = rotation_deg != 0 || (scale - 1.0).abs() > 1e-6 || clip.is_some();
|
||||
if needs_state {
|
||||
let (cos_a, sin_a, tx, ty) = match rotation_deg {
|
||||
90 => (0.0_f64, 1.0_f64, 0.0, paper_h as f64),
|
||||
// `paper_w`/`paper_h` are already the effective, rotation-swapped
|
||||
// page dimensions. A 90° turn maps x' = page_w - y, y' = x;
|
||||
// 270° maps x' = y, y' = page_h - x.
|
||||
90 => (0.0_f64, 1.0_f64, paper_w as f64, 0.0),
|
||||
180 => (-1.0_f64, 0.0_f64, paper_w as f64, paper_h as f64),
|
||||
270 => (0.0_f64, -1.0_f64, paper_w as f64, 0.0),
|
||||
270 => (0.0_f64, -1.0_f64, 0.0, paper_h as f64),
|
||||
_ => (1.0_f64, 0.0_f64, 0.0, 0.0),
|
||||
};
|
||||
let s = scale as f64;
|
||||
@@ -245,8 +248,9 @@ fn build_pdf(
|
||||
if a < 0.01 {
|
||||
continue;
|
||||
}
|
||||
// Skip the paper-boundary wire — the white PDF background already provides it.
|
||||
if wire.name == "__paper_boundary__" {
|
||||
// Skip screen-only paper helpers. The PDF page supplies its own white
|
||||
// boundary, and the printable-area rectangle is a UI guide, not ink.
|
||||
if matches!(wire.name.as_str(), "__paper_boundary__" | "paper_printable_area") {
|
||||
continue;
|
||||
}
|
||||
// Apply CTB plot style table overrides (color + lineweight).
|
||||
@@ -433,7 +437,13 @@ fn emit_hatch(ops: &mut Vec<Op>, hatch: &HatchModel, ox: f64, oy: f64) {
|
||||
// would vanish white-on-white on paper. Force near-white/near-yellow → black
|
||||
// and near-cyan → dark blue, matching AutoCAD's colour-7-on-white plotting.
|
||||
// Genuine colours are untouched; WIPEOUTS keep their paper-white mask.
|
||||
if hatch.name != "WIPEOUT_FILL" {
|
||||
if hatch.name == "WIPEOUT_FILL" {
|
||||
// Screen wipeouts match the configured canvas colour; printed
|
||||
// wipeouts must mask with the white paper colour.
|
||||
r = 1.0;
|
||||
g = 1.0;
|
||||
b = 1.0;
|
||||
} else {
|
||||
let is_light = r > 0.80 && g > 0.80 && b > 0.80;
|
||||
let is_yellow = r > 0.80 && g > 0.70 && b < 0.30;
|
||||
let is_cyan = r < 0.30 && g > 0.70 && b > 0.70;
|
||||
|
||||
@@ -44,6 +44,8 @@ pub async fn print_wires(
|
||||
_offset_x: f64,
|
||||
_offset_y: f64,
|
||||
_rotation_deg: i32,
|
||||
_scale: f32,
|
||||
_clip: Option<(f32, f32, f32, f32)>,
|
||||
_plot_style: Option<PlotStyleTable>,
|
||||
) -> Result<String, String> {
|
||||
Err("Printing is not available in the web version.".into())
|
||||
@@ -65,6 +67,8 @@ pub async fn print_wires_with(
|
||||
_offset_x: f64,
|
||||
_offset_y: f64,
|
||||
_rotation_deg: i32,
|
||||
_scale: f32,
|
||||
_clip: Option<(f32, f32, f32, f32)>,
|
||||
_plot_style: Option<PlotStyleTable>,
|
||||
_opts: PrintOptions,
|
||||
) -> Result<String, String> {
|
||||
@@ -95,6 +99,8 @@ pub async fn print_wires(
|
||||
offset_x: f64,
|
||||
offset_y: f64,
|
||||
rotation_deg: i32,
|
||||
scale: f32,
|
||||
clip: Option<(f32, f32, f32, f32)>,
|
||||
plot_style: Option<PlotStyleTable>,
|
||||
) -> Result<String, String> {
|
||||
// ── 1. Write to a named temp file ─────────────────────────────────────
|
||||
@@ -108,8 +114,8 @@ pub async fn print_wires(
|
||||
offset_x,
|
||||
offset_y,
|
||||
rotation_deg,
|
||||
1.0,
|
||||
None,
|
||||
scale,
|
||||
clip,
|
||||
&tmp_path,
|
||||
plot_style.as_ref(),
|
||||
)?;
|
||||
@@ -158,6 +164,8 @@ pub async fn print_wires_with(
|
||||
offset_x: f64,
|
||||
offset_y: f64,
|
||||
rotation_deg: i32,
|
||||
scale: f32,
|
||||
clip: Option<(f32, f32, f32, f32)>,
|
||||
plot_style: Option<PlotStyleTable>,
|
||||
opts: PrintOptions,
|
||||
) -> Result<String, String> {
|
||||
@@ -171,8 +179,8 @@ pub async fn print_wires_with(
|
||||
offset_x,
|
||||
offset_y,
|
||||
rotation_deg,
|
||||
1.0,
|
||||
None,
|
||||
scale,
|
||||
clip,
|
||||
&tmp_path,
|
||||
plot_style.as_ref(),
|
||||
)?;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// PLOTWINDOW command — pick two corners to define the plot window area.
|
||||
//
|
||||
// In paper space it writes the layout's PlotSettings (PlotType::Window); in
|
||||
// model space the host stores the window for the plot dialog.
|
||||
// In both model and paper space the host stores the picked rectangle for the
|
||||
// plot dialog; committing the dialog persists it to layout PlotSettings.
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
|
||||
+1
-1
@@ -1420,7 +1420,7 @@ impl Scene {
|
||||
/// Recursively collect wipeout masks defined inside a block, transformed to
|
||||
/// world space by the accumulated insert transform. See `wipeout_models`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn collect_block_wipeouts(
|
||||
pub(super) fn collect_block_wipeouts(
|
||||
&self,
|
||||
xform: &acadrust::types::Transform,
|
||||
block_name: &str,
|
||||
|
||||
@@ -409,6 +409,149 @@ impl Scene {
|
||||
Arc::new(models)
|
||||
}
|
||||
|
||||
/// Plot-only hatch set for a specific block. Paper PDF generation uses this
|
||||
/// for the model block behind each floating viewport; unlike
|
||||
/// `paper_canvas_hatches`, it must not include the active paper block.
|
||||
pub(super) fn plot_hatches_for_block(
|
||||
&self,
|
||||
block: Handle,
|
||||
frozen: Option<&rustc_hash::FxHashSet<Handle>>,
|
||||
) -> Vec<HatchModel> {
|
||||
let layer_hidden = |layer: &str| {
|
||||
self.document
|
||||
.layers
|
||||
.get(layer)
|
||||
.map(|l| l.flags.off || l.flags.frozen)
|
||||
.unwrap_or(false)
|
||||
};
|
||||
let mut models = Vec::new();
|
||||
for (&handle, model) in self.hatches.iter() {
|
||||
let Some(source) = self.document.get_entity(handle) else {
|
||||
continue;
|
||||
};
|
||||
let contextual =
|
||||
crate::scene::annotative::entity_for_active_context(&self.document, source);
|
||||
let entity = contextual.as_ref();
|
||||
let common = entity.common();
|
||||
if common.invisible
|
||||
|| self.entity_temporarily_hidden(handle)
|
||||
|| layer_hidden(&common.layer)
|
||||
|| self.layer_frozen_in(&common.layer, frozen)
|
||||
|| !self.belongs_to_visible_block(handle, common.owner_handle, block)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let mut hatch = match entity {
|
||||
EntityType::Hatch(dxf)
|
||||
if crate::scene::annotative::active_object_context(
|
||||
&self.document,
|
||||
handle,
|
||||
)
|
||||
.is_some() =>
|
||||
{
|
||||
Self::hatch_model_from_dxf(dxf, model.color)
|
||||
.unwrap_or_else(|| model.clone())
|
||||
}
|
||||
_ => model.clone(),
|
||||
};
|
||||
hatch.color = self.render_style(entity).0;
|
||||
if let EntityType::Hatch(dxf) = entity {
|
||||
if let model::hatch_model::HatchPattern::Pattern(_) = &hatch.pattern {
|
||||
if dxf.pattern.lines.is_empty() {
|
||||
hatch.angle_offset = dxf.pattern_angle as f32;
|
||||
hatch.scale = dxf.pattern_scale as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
models.push(hatch);
|
||||
}
|
||||
models.extend(self.exploded_insert_hatch_models(
|
||||
block,
|
||||
self.paper_bg_color,
|
||||
false,
|
||||
frozen,
|
||||
));
|
||||
models
|
||||
}
|
||||
|
||||
/// Plot-only wipeout masks for a specific block. Nested inserts follow the
|
||||
/// same recursive collector as the on-screen model path.
|
||||
pub(super) fn plot_wipeouts_for_block(
|
||||
&self,
|
||||
block: Handle,
|
||||
frozen: Option<&rustc_hash::FxHashSet<Handle>>,
|
||||
) -> Vec<HatchModel> {
|
||||
let depth_map = self.draw_depth_map();
|
||||
let mut models = Vec::new();
|
||||
for entity in self.document.entities() {
|
||||
let EntityType::Wipeout(wipeout) = entity else {
|
||||
continue;
|
||||
};
|
||||
let common = &wipeout.common;
|
||||
if common.invisible
|
||||
|| self.entity_temporarily_hidden(common.handle)
|
||||
|| self
|
||||
.document
|
||||
.layers
|
||||
.get(&common.layer)
|
||||
.map(|layer| layer.flags.off || layer.flags.frozen)
|
||||
.unwrap_or(false)
|
||||
|| self.layer_frozen_in(&common.layer, frozen)
|
||||
|| !self.belongs_to_visible_block(common.handle, common.owner_handle, block)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let (world_origin, boundary) = Self::wipeout_boundary_2d(wipeout);
|
||||
if boundary.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
models.push(HatchModel {
|
||||
boundary: Arc::new(boundary),
|
||||
boundary_wcs: None,
|
||||
pattern: model::hatch_model::HatchPattern::Solid,
|
||||
name: "WIPEOUT_FILL".into(),
|
||||
color: self.paper_bg_color,
|
||||
angle_offset: 0.0,
|
||||
scale: 1.0,
|
||||
world_origin,
|
||||
draw_depth: depth_map
|
||||
.get(&common.handle.value())
|
||||
.map_or(0.0, |depth| depth[0]),
|
||||
});
|
||||
}
|
||||
for entity in self.document.entities() {
|
||||
let contextual =
|
||||
crate::scene::annotative::entity_for_active_context(&self.document, entity);
|
||||
let EntityType::Insert(insert) = contextual.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
let common = &insert.common;
|
||||
if common.invisible
|
||||
|| self.entity_temporarily_hidden(common.handle)
|
||||
|| self
|
||||
.document
|
||||
.layers
|
||||
.get(&common.layer)
|
||||
.map(|layer| layer.flags.off || layer.flags.frozen)
|
||||
.unwrap_or(false)
|
||||
|| self.layer_frozen_in(&common.layer, frozen)
|
||||
|| !self.belongs_to_visible_block(common.handle, common.owner_handle, block)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
self.collect_block_wipeouts(
|
||||
&insert.get_transform(),
|
||||
&insert.block_name,
|
||||
0,
|
||||
frozen,
|
||||
self.paper_bg_color,
|
||||
&depth_map,
|
||||
&mut models,
|
||||
);
|
||||
}
|
||||
models
|
||||
}
|
||||
|
||||
/// Paper-layout wipeout fills (paper hit-testing / export). Same rationale as
|
||||
/// `paper_canvas_hatches` — only include wipeouts owned by the
|
||||
/// active paper layout block, so model wipeouts (drawn through their
|
||||
|
||||
@@ -349,6 +349,149 @@ impl Scene {
|
||||
result
|
||||
}
|
||||
|
||||
/// Model-space fills projected into every active paper viewport for vector
|
||||
/// plotting. The screen renderer projects these on the GPU; PDF generation
|
||||
/// needs the equivalent paper-space geometry explicitly.
|
||||
pub fn viewport_plot_fills(
|
||||
&self,
|
||||
) -> (Vec<WireModel>, Vec<HatchModel>, Vec<HatchModel>) {
|
||||
use acadrust::entities::Viewport;
|
||||
use model::hatch_model::HatchPattern;
|
||||
|
||||
if self.current_layout == "Model" {
|
||||
return (Vec::new(), Vec::new(), Vec::new());
|
||||
}
|
||||
let paper_block = self.current_layout_block_handle();
|
||||
let model_block = self.model_space_block_handle();
|
||||
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(viewport))
|
||||
if viewport.common.owner_handle == paper_block && viewport.status.is_on =>
|
||||
{
|
||||
Some(viewport)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut pattern_wires = Vec::new();
|
||||
let mut projected_hatches = Vec::new();
|
||||
let mut projected_wipeouts = Vec::new();
|
||||
|
||||
for viewport in viewports {
|
||||
let Some(camera) = self.camera_for_viewport(viewport.common.handle) else {
|
||||
continue;
|
||||
};
|
||||
let view_right = camera.rotation * glam::Vec3::X;
|
||||
let view_up = camera.rotation * glam::Vec3::Y;
|
||||
let view_forward = camera.rotation * glam::Vec3::Z;
|
||||
let view_height = (camera.ortho_size() * 2.0) as f64;
|
||||
let viewport_scale = if view_height > 1e-9 {
|
||||
viewport.height / view_height
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let center_x = viewport.center.x;
|
||||
let center_y = viewport.center.y;
|
||||
let half_w = viewport.width * 0.5;
|
||||
let half_h = viewport.height * 0.5;
|
||||
let (xmin, ymin, xmax, ymax) = (
|
||||
(center_x - half_w) as f32,
|
||||
(center_y - half_h) as f32,
|
||||
(center_x + half_w) as f32,
|
||||
(center_y + half_h) as f32,
|
||||
);
|
||||
let perspective = viewport.status.perspective && viewport.lens_length > 1.0;
|
||||
let camera_distance = if perspective {
|
||||
(viewport.view_height * viewport.lens_length / 24.0).max(0.001)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let project = |x: f64, y: f64| -> Option<[f32; 2]> {
|
||||
let delta = glam::DVec3::new(x, y, 0.0) - camera.target;
|
||||
let u = delta.dot(view_right.as_dvec3());
|
||||
let v = delta.dot(view_up.as_dvec3());
|
||||
let factor = if perspective {
|
||||
let depth = camera_distance - delta.dot(view_forward.as_dvec3());
|
||||
if depth <= 0.001 {
|
||||
return None;
|
||||
}
|
||||
camera_distance / depth
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
Some([
|
||||
(center_x + u * factor * viewport_scale) as f32,
|
||||
(center_y + v * factor * viewport_scale) as f32,
|
||||
])
|
||||
};
|
||||
|
||||
let frozen: rustc_hash::FxHashSet<Handle> =
|
||||
viewport.frozen_layers.iter().copied().collect();
|
||||
let hatches = self.plot_hatches_for_block(model_block, Some(&frozen));
|
||||
for hatch in hatches {
|
||||
if matches!(&hatch.pattern, HatchPattern::Pattern(_)) {
|
||||
let mut points = Vec::new();
|
||||
let mut aabb = [
|
||||
f32::INFINITY,
|
||||
f32::INFINITY,
|
||||
f32::NEG_INFINITY,
|
||||
f32::NEG_INFINITY,
|
||||
];
|
||||
for [a, b] in hatch.pattern_segments() {
|
||||
let (Some(a), Some(b)) =
|
||||
(project(a[0], a[1]), project(b[0], b[1]))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some((ax, ay, bx, by)) =
|
||||
cs_clip(a[0], a[1], b[0], b[1], xmin, ymin, xmax, ymax)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if !points.is_empty() {
|
||||
points.push([f32::NAN, f32::NAN, f32::NAN]);
|
||||
}
|
||||
points.push([ax, ay, viewport.center.z as f32]);
|
||||
points.push([bx, by, viewport.center.z as f32]);
|
||||
aabb[0] = aabb[0].min(ax).min(bx);
|
||||
aabb[1] = aabb[1].min(ay).min(by);
|
||||
aabb[2] = aabb[2].max(ax).max(bx);
|
||||
aabb[3] = aabb[3].max(ay).max(by);
|
||||
}
|
||||
if !points.is_empty() {
|
||||
let mut wire = WireModel::solid(
|
||||
"viewport_hatch_pattern".into(),
|
||||
points,
|
||||
hatch.color,
|
||||
false,
|
||||
);
|
||||
wire.aabb = aabb;
|
||||
pattern_wires.push(wire);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if let Some(hatch) =
|
||||
project_plot_fill(hatch, &project, xmin, ymin, xmax, ymax)
|
||||
{
|
||||
projected_hatches.push(hatch);
|
||||
}
|
||||
}
|
||||
|
||||
for wipeout in self.plot_wipeouts_for_block(model_block, Some(&frozen)) {
|
||||
if let Some(wipeout) =
|
||||
project_plot_fill(wipeout, &project, xmin, ymin, xmax, ymax)
|
||||
{
|
||||
projected_wipeouts.push(wipeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(pattern_wires, projected_hatches, projected_wipeouts)
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -592,6 +735,125 @@ fn sample_polyline_clip_boundary(
|
||||
output
|
||||
}
|
||||
|
||||
fn project_plot_fill<F>(
|
||||
mut fill: HatchModel,
|
||||
project: &F,
|
||||
xmin: f32,
|
||||
ymin: f32,
|
||||
xmax: f32,
|
||||
ymax: f32,
|
||||
) -> Option<HatchModel>
|
||||
where
|
||||
F: Fn(f64, f64) -> Option<[f32; 2]>,
|
||||
{
|
||||
let mut output = Vec::new();
|
||||
let mut ring = Vec::new();
|
||||
let flush_ring = |ring: &mut Vec<[f32; 2]>, output: &mut Vec<[f32; 2]>| {
|
||||
if ring.len() < 3 {
|
||||
ring.clear();
|
||||
return;
|
||||
}
|
||||
if ring.first() == ring.last() {
|
||||
ring.pop();
|
||||
}
|
||||
let mut clipped = clip_polygon_to_rect(ring, xmin, ymin, xmax, ymax);
|
||||
ring.clear();
|
||||
if clipped.len() < 3 {
|
||||
return;
|
||||
}
|
||||
if clipped.first() != clipped.last() {
|
||||
clipped.push(clipped[0]);
|
||||
}
|
||||
if !output.is_empty() {
|
||||
output.push([f32::NAN, f32::NAN]);
|
||||
}
|
||||
output.extend(clipped);
|
||||
};
|
||||
for &[x, y] in fill.boundary.iter() {
|
||||
if x.is_nan() || y.is_nan() {
|
||||
flush_ring(&mut ring, &mut output);
|
||||
continue;
|
||||
}
|
||||
let absolute_x = fill.world_origin[0] + x as f64;
|
||||
let absolute_y = fill.world_origin[1] + y as f64;
|
||||
if let Some(point) = project(absolute_x, absolute_y) {
|
||||
ring.push(point);
|
||||
}
|
||||
}
|
||||
flush_ring(&mut ring, &mut output);
|
||||
if output.is_empty() {
|
||||
return None;
|
||||
}
|
||||
fill.world_origin = [0.0, 0.0];
|
||||
fill.boundary = std::sync::Arc::new(output);
|
||||
fill.boundary_wcs = None;
|
||||
Some(fill)
|
||||
}
|
||||
|
||||
fn clip_polygon_to_rect(
|
||||
polygon: &[[f32; 2]],
|
||||
xmin: f32,
|
||||
ymin: f32,
|
||||
xmax: f32,
|
||||
ymax: f32,
|
||||
) -> Vec<[f32; 2]> {
|
||||
let mut output = polygon.to_vec();
|
||||
for (edge, value) in [(0u8, xmin), (1, xmax), (2, ymin), (3, ymax)] {
|
||||
if output.is_empty() {
|
||||
break;
|
||||
}
|
||||
let input = std::mem::take(&mut output);
|
||||
let mut previous = *input.last().unwrap();
|
||||
let mut previous_inside = polygon_edge_inside(previous, edge, value);
|
||||
for current in input {
|
||||
let current_inside = polygon_edge_inside(current, edge, value);
|
||||
if current_inside != previous_inside {
|
||||
output.push(polygon_edge_intersection(previous, current, edge, value));
|
||||
}
|
||||
if current_inside {
|
||||
output.push(current);
|
||||
}
|
||||
previous = current;
|
||||
previous_inside = current_inside;
|
||||
}
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
fn polygon_edge_inside(point: [f32; 2], edge: u8, value: f32) -> bool {
|
||||
match edge {
|
||||
0 => point[0] >= value,
|
||||
1 => point[0] <= value,
|
||||
2 => point[1] >= value,
|
||||
_ => point[1] <= value,
|
||||
}
|
||||
}
|
||||
|
||||
fn polygon_edge_intersection(
|
||||
start: [f32; 2],
|
||||
end: [f32; 2],
|
||||
edge: u8,
|
||||
value: f32,
|
||||
) -> [f32; 2] {
|
||||
if edge <= 1 {
|
||||
let dx = end[0] - start[0];
|
||||
let t = if dx.abs() > 1e-12 {
|
||||
(value - start[0]) / dx
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
[value, start[1] + (end[1] - start[1]) * t]
|
||||
} else {
|
||||
let dy = end[1] - start[1];
|
||||
let t = if dy.abs() > 1e-12 {
|
||||
(value - start[1]) / dy
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
[start[0] + (end[0] - start[0]) * t, value]
|
||||
}
|
||||
}
|
||||
|
||||
// ── Paper boundary wire ────────────────────────────────────────────────────
|
||||
|
||||
// ── Cohen-Sutherland line clipping ───────────────────────────────────────
|
||||
|
||||
Reference in new issue
Block a user