fix(layout): complete paperspace integration
Apply each layout's embedded page setup and plot style consistently across display, PDF, printing, and multi-layout output.\n\nRefs #844
This commit is contained in:
parent
9c8f841f71
commit
8c61d89b1e
18 changed files with 1389 additions and 369 deletions
|
|
@ -30,6 +30,7 @@ impl OpenCADStudio {
|
|||
.document
|
||||
.header
|
||||
.paper_space_insertion_base = pt;
|
||||
self.tabs[i].scene.persist_current_layout_state();
|
||||
} else {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
|
|
|
|||
|
|
@ -412,14 +412,7 @@ impl OpenCADStudio {
|
|||
vec![]
|
||||
};
|
||||
|
||||
// Blocks: everything the layout-reachability walk above did
|
||||
// NOT mark live. That covers unreferenced named blocks,
|
||||
// leftover anonymous blocks (*U hatch/unnamed, orphaned *D
|
||||
// dimension, *T table, dynamic-block variants — AutoCAD drops
|
||||
// these too), AND whole dead subgraphs a detached xref leaves
|
||||
// behind. Live xrefs stay (an attached xref's blocks are
|
||||
// reached from the layout that inserts them); the *Model_Space
|
||||
// / *Paper_Space containers are never removed.
|
||||
// Remove unreachable blocks but retain live xrefs and space containers.
|
||||
let block_remove: Vec<String> = if do_blocks {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
|
|
@ -494,15 +487,7 @@ impl OpenCADStudio {
|
|||
n_blocks += block_remove.len();
|
||||
}
|
||||
|
||||
// Draw-order cleanup. A SortEntitiesTable lists a block's
|
||||
// entities in draw order; purging the block drops the entities
|
||||
// and the block record, but this object lingers, still holding
|
||||
// the (now dangling) handle of every deleted entity. On a
|
||||
// detached-xref purge that is hundreds of thousands of stale
|
||||
// handles — megabytes of dead weight the save re-emits, which is
|
||||
// why an OCS-purged file stayed far larger than AutoCAD's.
|
||||
// Drop every SortEntitiesTable whose owning block is gone
|
||||
// (AutoCAD's "orphaned data").
|
||||
// Remove draw-order tables whose owning blocks are gone.
|
||||
let mut n_sortents = 0usize;
|
||||
if do_blocks {
|
||||
let live_blocks: rustc_hash::FxHashSet<acadrust::Handle> = self.tabs[i]
|
||||
|
|
@ -1913,6 +1898,9 @@ impl OpenCADStudio {
|
|||
if name == "FILLMODE" {
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
}
|
||||
if matches!(name.as_str(), "PSLTSCALE" | "PLIMCHECK") {
|
||||
self.tabs[i].scene.persist_current_layout_state();
|
||||
}
|
||||
self.command_line.push_output(&msg);
|
||||
} else {
|
||||
// Queried with no value (`changed == false`):
|
||||
|
|
|
|||
|
|
@ -568,63 +568,49 @@ impl OpenCADStudio {
|
|||
}
|
||||
} else {
|
||||
// ── Preset viewport layout ───────────────────────────
|
||||
// Determine paper dimensions from PlotSettings (fallback A4 landscape).
|
||||
let layout_name = scene.current_layout.clone();
|
||||
let (paper_w, paper_h) = {
|
||||
use acadrust::objects::ObjectType;
|
||||
let mut pw = 297.0_f64;
|
||||
let mut ph = 210.0_f64;
|
||||
for (_, obj) in &scene.document.objects {
|
||||
if let ObjectType::PlotSettings(ps) = obj {
|
||||
if ps.page_name == layout_name && ps.paper_width > 0.0 {
|
||||
pw = ps.paper_width;
|
||||
ph = ps.paper_height;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
(pw, ph)
|
||||
};
|
||||
let margin = 5.0_f64; // mm margin around the usable area
|
||||
let uw = paper_w - 2.0 * margin; // usable width
|
||||
let uh = paper_h - 2.0 * margin; // usable height
|
||||
// Collect rectangle specs: (cx, cz, w, h) in mm
|
||||
let ((x0, y0), (x1, y1)) = scene
|
||||
.printable_area_limits()
|
||||
.unwrap_or(((0.0, 0.0), (297.0, 210.0)));
|
||||
let uw = (x1 - x0).max(1.0);
|
||||
let uh = (y1 - y0).max(1.0);
|
||||
let gap = 2.0 * scene.paper_space_unit_factor();
|
||||
let rects: Vec<(f64, f64, f64, f64)> = match sub.as_str() {
|
||||
"2H" => {
|
||||
// Two viewports side by side (horizontal split)
|
||||
let vw = (uw - 2.0) / 2.0;
|
||||
let vw = (uw - gap) / 2.0;
|
||||
vec![
|
||||
(margin + vw / 2.0, margin + uh / 2.0, vw, uh),
|
||||
(margin + vw + 2.0 + vw / 2.0, margin + uh / 2.0, vw, uh),
|
||||
(x0 + vw / 2.0, y0 + uh / 2.0, vw, uh),
|
||||
(x0 + vw + gap + vw / 2.0, y0 + uh / 2.0, vw, uh),
|
||||
]
|
||||
}
|
||||
"2V" => {
|
||||
// Two viewports stacked (vertical split)
|
||||
let vh = (uh - 2.0) / 2.0;
|
||||
let vh = (uh - gap) / 2.0;
|
||||
vec![
|
||||
(margin + uw / 2.0, margin + vh + 2.0 + vh / 2.0, uw, vh),
|
||||
(margin + uw / 2.0, margin + vh / 2.0, uw, vh),
|
||||
(x0 + uw / 2.0, y0 + vh + gap + vh / 2.0, uw, vh),
|
||||
(x0 + uw / 2.0, y0 + vh / 2.0, uw, vh),
|
||||
]
|
||||
}
|
||||
"4" => {
|
||||
// Four equal viewports (2×2 grid)
|
||||
let vw = (uw - 2.0) / 2.0;
|
||||
let vh = (uh - 2.0) / 2.0;
|
||||
let vw = (uw - gap) / 2.0;
|
||||
let vh = (uh - gap) / 2.0;
|
||||
vec![
|
||||
(margin + vw / 2.0, margin + vh + 2.0 + vh / 2.0, vw, vh),
|
||||
(x0 + vw / 2.0, y0 + vh + gap + vh / 2.0, vw, vh),
|
||||
(
|
||||
margin + vw + 2.0 + vw / 2.0,
|
||||
margin + vh + 2.0 + vh / 2.0,
|
||||
x0 + vw + gap + vw / 2.0,
|
||||
y0 + vh + gap + vh / 2.0,
|
||||
vw,
|
||||
vh,
|
||||
),
|
||||
(margin + vw / 2.0, margin + vh / 2.0, vw, vh),
|
||||
(margin + vw + 2.0 + vw / 2.0, margin + vh / 2.0, vw, vh),
|
||||
(x0 + vw / 2.0, y0 + vh / 2.0, vw, vh),
|
||||
(x0 + vw + gap + vw / 2.0, y0 + vh / 2.0, vw, vh),
|
||||
]
|
||||
}
|
||||
"SINGLE" | "1" => {
|
||||
// Single full-page viewport
|
||||
vec![(margin + uw / 2.0, margin + uh / 2.0, uw, uh)]
|
||||
vec![(x0 + uw / 2.0, y0 + uh / 2.0, uw, uh)]
|
||||
}
|
||||
_ => {
|
||||
self.command_line.push_error(
|
||||
|
|
|
|||
|
|
@ -256,12 +256,11 @@ impl DocumentTab {
|
|||
.unwrap_or(glam::DVec3::ZERO)
|
||||
}
|
||||
|
||||
/// True when the active pane edits **model-space** geometry: the Model tab,
|
||||
/// or inside a floating viewport (MSPACE). The UCS applies in both; plain
|
||||
/// paper space (no active viewport) is excluded. Single predicate so every
|
||||
/// UCS-aware system shares one rule. [[feedback_shared_infra]]
|
||||
/// True when the active pane has a drawing coordinate plane.
|
||||
pub(super) fn editing_model_space(&self) -> bool {
|
||||
self.scene.current_layout == "Model" || self.scene.active_viewport.is_some()
|
||||
self.scene.current_layout == "Model"
|
||||
|| self.scene.active_viewport.is_some()
|
||||
|| self.active_ucs.is_some()
|
||||
}
|
||||
|
||||
/// The document's saved model-space UCS (header), as a `Ucs`. `None` when it
|
||||
|
|
@ -321,9 +320,51 @@ impl DocumentTab {
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) fn ucs_from_layout(&self) -> Option<Ucs> {
|
||||
let layout = self.scene.document.objects.values().find_map(|object| {
|
||||
let acadrust::objects::ObjectType::Layout(layout) = object else {
|
||||
return None;
|
||||
};
|
||||
(layout.name == self.scene.current_layout).then_some(layout)
|
||||
})?;
|
||||
let mut ucs = self
|
||||
.scene
|
||||
.document
|
||||
.ucss
|
||||
.iter()
|
||||
.find(|ucs| ucs.handle == layout.named_ucs)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Ucs::new("*PAPERUCS*"));
|
||||
ucs.origin = acadrust::types::Vector3::new(
|
||||
layout.ucs_origin.0,
|
||||
layout.ucs_origin.1,
|
||||
layout.ucs_origin.2,
|
||||
);
|
||||
ucs.x_axis = acadrust::types::Vector3::new(
|
||||
layout.ucs_x_axis.0,
|
||||
layout.ucs_x_axis.1,
|
||||
layout.ucs_x_axis.2,
|
||||
);
|
||||
ucs.y_axis = acadrust::types::Vector3::new(
|
||||
layout.ucs_y_axis.0,
|
||||
layout.ucs_y_axis.1,
|
||||
layout.ucs_y_axis.2,
|
||||
);
|
||||
ucs.elevation = layout.elevation;
|
||||
ucs.ortho_type = layout.ucs_ortho_type;
|
||||
ucs.named_ucs_handle = layout.named_ucs;
|
||||
ucs.base_ucs_handle = layout.base_ucs;
|
||||
let is_world = layout.named_ucs.is_null()
|
||||
&& layout.base_ucs.is_null()
|
||||
&& layout.ucs_ortho_type == 0
|
||||
&& layout.elevation.abs() <= f64::EPSILON
|
||||
&& super::helpers::UcsXform::from_ucs(&ucs).is_identity();
|
||||
(!is_world).then_some(ucs)
|
||||
}
|
||||
|
||||
/// Set `active_ucs` to the UCS of the *current pane*: the entered viewport's
|
||||
/// own per-viewport UCS, the model header UCS in the Model tab, or none in
|
||||
/// plain paper space. Keeps the ViewCube in lock-step. Call on every pane
|
||||
/// own per-viewport UCS, the model header UCS, or the layout UCS. Keeps the
|
||||
/// ViewCube in lock-step. Call on every pane
|
||||
/// change (enter/exit viewport, layout / tab switch, load) so one field
|
||||
/// drives all UCS-aware systems regardless of where editing happens.
|
||||
pub(super) fn refresh_active_ucs(&mut self) {
|
||||
|
|
@ -334,15 +375,52 @@ impl DocumentTab {
|
|||
} else if self.scene.current_layout == "Model" {
|
||||
self.model_ucs_from_header()
|
||||
} else {
|
||||
None
|
||||
self.ucs_from_layout()
|
||||
};
|
||||
if self.active_block_edit.is_none()
|
||||
&& self.scene.active_viewport.is_none()
|
||||
&& self.scene.current_layout != "Model"
|
||||
{
|
||||
self.sync_paper_ucs_header();
|
||||
}
|
||||
self.sync_ucs_to_scene();
|
||||
}
|
||||
|
||||
fn sync_paper_ucs_header(&mut self) {
|
||||
use acadrust::types::Vector3;
|
||||
let header = &mut self.scene.document.header;
|
||||
match &self.active_ucs {
|
||||
Some(ucs) => {
|
||||
header.paper_space_ucs_origin = ucs.origin;
|
||||
header.paper_space_ucs_x_axis = ucs.x_axis;
|
||||
header.paper_space_ucs_y_axis = ucs.y_axis;
|
||||
header.paper_elevation = ucs.elevation;
|
||||
header.paper_space_ucs_name = if (ucs.named_ucs_handle.is_valid()
|
||||
|| ucs.handle.is_valid())
|
||||
&& !ucs.name.starts_with('*')
|
||||
{
|
||||
ucs.name.clone()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
header.paper_ucs_ortho_ref = ucs.base_ucs_handle;
|
||||
header.paper_ucs_ortho_view = ucs.ortho_type;
|
||||
}
|
||||
None => {
|
||||
header.paper_space_ucs_origin = Vector3::ZERO;
|
||||
header.paper_space_ucs_x_axis = Vector3::UNIT_X;
|
||||
header.paper_space_ucs_y_axis = Vector3::UNIT_Y;
|
||||
header.paper_elevation = 0.0;
|
||||
header.paper_space_ucs_name.clear();
|
||||
header.paper_ucs_ortho_ref = Handle::NULL;
|
||||
header.paper_ucs_ortho_view = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Persist `active_ucs` back to its pane's storage so it round-trips: the
|
||||
/// entered viewport's per-viewport UCS fields, or the document header's
|
||||
/// model-space UCS in the Model tab. No-op in plain paper space. Call after
|
||||
/// any UCS change.
|
||||
/// model-space UCS in the Model tab, or the layout UCS. Call after a change.
|
||||
pub(super) fn persist_active_ucs(&mut self) {
|
||||
use acadrust::types::Vector3;
|
||||
if let Some(index) = self.active_block_edit {
|
||||
|
|
@ -385,6 +463,48 @@ impl DocumentTab {
|
|||
h.model_space_ucs_name.clear();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let (origin, x_axis, y_axis, elevation, ortho, named, base) =
|
||||
match &self.active_ucs {
|
||||
Some(ucs) => (
|
||||
ucs.origin,
|
||||
ucs.x_axis,
|
||||
ucs.y_axis,
|
||||
ucs.elevation,
|
||||
ucs.ortho_type,
|
||||
if ucs.named_ucs_handle.is_valid() {
|
||||
ucs.named_ucs_handle
|
||||
} else {
|
||||
ucs.handle
|
||||
},
|
||||
ucs.base_ucs_handle,
|
||||
),
|
||||
None => (
|
||||
Vector3::ZERO,
|
||||
Vector3::UNIT_X,
|
||||
Vector3::UNIT_Y,
|
||||
0.0,
|
||||
0,
|
||||
Handle::NULL,
|
||||
Handle::NULL,
|
||||
),
|
||||
};
|
||||
for object in self.scene.document.objects.values_mut() {
|
||||
let acadrust::objects::ObjectType::Layout(layout) = object else {
|
||||
continue;
|
||||
};
|
||||
if layout.name == self.scene.current_layout {
|
||||
layout.ucs_origin = (origin.x, origin.y, origin.z);
|
||||
layout.ucs_x_axis = (x_axis.x, x_axis.y, x_axis.z);
|
||||
layout.ucs_y_axis = (y_axis.x, y_axis.y, y_axis.z);
|
||||
layout.elevation = elevation;
|
||||
layout.ucs_ortho_type = ortho;
|
||||
layout.named_ucs = named;
|
||||
layout.base_ucs = base;
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.sync_paper_ucs_header();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -810,16 +810,21 @@ pub(super) struct OpenCADStudio {
|
|||
/// Snapshot of the dialog's settings taken when it opened, restored by the
|
||||
/// `<previous>` list entry.
|
||||
plot_prev: Option<crate::ui::window::plot::PlotDialogState>,
|
||||
/// Full source settings behind the fields currently shown in Plot.
|
||||
plot_setup_template: Option<acadrust::objects::PlotSettings>,
|
||||
/// Paper layouts shown by Print All, in tab order with their selection.
|
||||
print_all_layouts: Vec<(String, bool)>,
|
||||
/// True while the Plot dialog is editing settings for Print All.
|
||||
print_all_options: bool,
|
||||
/// True when Print All should override each layout's page setup.
|
||||
print_all_settings_override: bool,
|
||||
/// Settings restored when the Print All options dialog is cancelled.
|
||||
print_all_options_prev: Option<crate::ui::window::plot::PlotDialogState>,
|
||||
/// Plot style restored together with cancelled Print All options.
|
||||
print_all_plot_style_prev: Option<Option<crate::io::plot_style::PlotStyleTable>>,
|
||||
/// Plot window restored together with cancelled Print All options.
|
||||
print_all_plot_window_prev: Option<Option<(f64, f64, f64, f64)>>,
|
||||
print_all_plot_setup_prev: Option<Option<acadrust::objects::PlotSettings>>,
|
||||
|
||||
// ── Plot Style Table ──────────────────────────────────────────────────
|
||||
/// Currently loaded CTB/STB table (None = no override).
|
||||
|
|
@ -3287,11 +3292,14 @@ impl OpenCADStudio {
|
|||
plot_orientation: crate::io::paper_sizes::Orientation::Landscape,
|
||||
plot_dialog: crate::ui::window::plot::PlotDialogState::default(),
|
||||
plot_prev: None,
|
||||
plot_setup_template: None,
|
||||
print_all_layouts: Vec::new(),
|
||||
print_all_options: false,
|
||||
print_all_settings_override: false,
|
||||
print_all_options_prev: None,
|
||||
print_all_plot_style_prev: None,
|
||||
print_all_plot_window_prev: None,
|
||||
print_all_plot_setup_prev: None,
|
||||
opening: None,
|
||||
open_job_serial: 0,
|
||||
recovery_report: None,
|
||||
|
|
|
|||
|
|
@ -120,6 +120,32 @@ type ClippedPlotParams = (
|
|||
Option<(f32, f32, f32, f32)>,
|
||||
);
|
||||
|
||||
fn plot_dialog_sheet_mm(d: &crate::ui::window::plot::PlotDialogState) -> (f64, f64) {
|
||||
use crate::io::paper_sizes::{sheet_mm, Orientation, PaperSize};
|
||||
let orientation = if d.orientation == "Portrait" {
|
||||
Orientation::Portrait
|
||||
} else {
|
||||
Orientation::Landscape
|
||||
};
|
||||
let standard = match d.paper.as_str() {
|
||||
"A3" => Some(PaperSize::A3),
|
||||
"A2" => Some(PaperSize::A2),
|
||||
"A1" => Some(PaperSize::A1),
|
||||
"A0" => Some(PaperSize::A0),
|
||||
"A4" => Some(PaperSize::A4),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(paper) = standard {
|
||||
return sheet_mm(paper, orientation);
|
||||
}
|
||||
let short = d.paper_width_mm.min(d.paper_height_mm).max(1.0);
|
||||
let long = d.paper_width_mm.max(d.paper_height_mm).max(1.0);
|
||||
match orientation {
|
||||
Orientation::Portrait => (short, long),
|
||||
Orientation::Landscape => (long, short),
|
||||
}
|
||||
}
|
||||
|
||||
fn plot_content_extents(
|
||||
wires: &[crate::io::pdf_export::PlotWire],
|
||||
hatches: &[crate::scene::model::hatch_model::HatchModel],
|
||||
|
|
@ -169,7 +195,18 @@ fn plot_scene_content(
|
|||
crate::io::pdf_export::PlotGroupSplits,
|
||||
) {
|
||||
let (mut paper_wires, mut model_wires) = scene.plot_wire_groups(render_mode_override);
|
||||
paper_wires.retain(|wire| wire.plot_visible);
|
||||
let plot_viewport_borders = scene
|
||||
.effective_plot_settings()
|
||||
.is_none_or(|settings| settings.flags.plot_viewport_borders);
|
||||
paper_wires.retain(|wire| {
|
||||
wire.plot_visible
|
||||
&& (plot_viewport_borders
|
||||
|| !crate::scene::Scene::handle_from_wire_name(&wire.name)
|
||||
.and_then(|handle| scene.document.get_entity(handle))
|
||||
.is_some_and(|entity| {
|
||||
matches!(entity, acadrust::EntityType::Viewport(viewport) if crate::scene::Scene::is_content_viewport(viewport))
|
||||
}))
|
||||
});
|
||||
model_wires.retain(|wire| wire.plot_visible);
|
||||
let with_depth = |wires: Vec<crate::scene::WireModel>| {
|
||||
let depths = scene.plot_wire_depths(&wires);
|
||||
|
|
@ -1364,6 +1401,8 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
.nth(1)
|
||||
.unwrap_or_else(|| "Model".to_string()),
|
||||
};
|
||||
self.tabs[i].scene.load_current_layout_state();
|
||||
self.tabs[i].refresh_active_ucs();
|
||||
}
|
||||
// Object isolation is session-only. A newly opened drawing must
|
||||
// not inherit the previous tab's filter, and persisted entity
|
||||
|
|
@ -2662,8 +2701,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
iced::exit()
|
||||
}
|
||||
|
||||
/// Write the given plot page settings into the active layout's Layout +
|
||||
/// PlotSettings objects (paper size, plot area, offset, rotation, scale).
|
||||
/// Write the given plot page settings into the active layout.
|
||||
/// No-op on the Model tab (which has no paper layout). Marks the tab dirty
|
||||
/// and re-tessellates the sheet. Called by the Plot dialog's Set current action.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
|
@ -2684,118 +2722,97 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
if layout_name != "Model" {
|
||||
let w: f64 = w.max(1.0);
|
||||
let h: f64 = h.max(1.0);
|
||||
use acadrust::objects::{
|
||||
PlotRotation, PlotSettings, PlotType, ScaledType, ShadePlotMode,
|
||||
ShadePlotResolutionLevel,
|
||||
};
|
||||
let mut ps = self
|
||||
.plot_setup_template
|
||||
.clone()
|
||||
.or_else(|| self.tabs[i].scene.plot_settings_for(&layout_name))
|
||||
.unwrap_or_else(|| PlotSettings::new(""));
|
||||
ps.paper_width = w;
|
||||
ps.paper_height = h;
|
||||
ps.paper_size = dialog.paper.clone();
|
||||
ps.plot_type = match plot_area {
|
||||
"Window" => PlotType::Window,
|
||||
"Display" => PlotType::LastScreenDisplay,
|
||||
"Extents" => PlotType::Extents,
|
||||
"Limits" => PlotType::Limits,
|
||||
area if area.starts_with("View: ") => PlotType::View,
|
||||
_ => PlotType::Layout,
|
||||
};
|
||||
ps.plot_view_name = plot_area
|
||||
.strip_prefix("View: ")
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
if plot_area == "Window" {
|
||||
if let Some((x0, y0, x1, y1)) = plot_window {
|
||||
ps.set_plot_window(x0, y0, x1, y1);
|
||||
}
|
||||
}
|
||||
ps.flags.plot_centered = center && plot_area != "Layout";
|
||||
ps.origin_x = if plot_area == "Layout" { 0.0 } else { offset_x };
|
||||
ps.origin_y = if plot_area == "Layout" { 0.0 } else { offset_y };
|
||||
ps.rotation = match rotation {
|
||||
90 => PlotRotation::Degrees90,
|
||||
180 => PlotRotation::Degrees180,
|
||||
270 => PlotRotation::Degrees270,
|
||||
_ => PlotRotation::None,
|
||||
};
|
||||
if dialog.fit_to_paper && plot_area != "Layout" {
|
||||
ps.set_scale_to_fit();
|
||||
} else if plot_area == "Layout" {
|
||||
ps.set_standard_scale(ScaledType::OneToOne);
|
||||
ps.standard_scale_factor = 1.0;
|
||||
} else {
|
||||
let factor = plot_dialog_scale_factor(&dialog);
|
||||
ps.scale_type = ScaledType::CustomScale;
|
||||
ps.scale_numerator = factor;
|
||||
ps.scale_denominator = 1.0;
|
||||
ps.standard_scale_factor = factor;
|
||||
ps.flags.use_standard_scale = false;
|
||||
}
|
||||
ps.printer_name = if dialog.to_file {
|
||||
crate::ui::window::plot::OUT_PDF.into()
|
||||
} else {
|
||||
dialog.printer.clone().unwrap_or_default()
|
||||
};
|
||||
ps.current_style_sheet = dialog.style_name.clone();
|
||||
ps.flags.scale_lineweights = dialog.scale_lw;
|
||||
ps.flags.print_lineweights = dialog.lineweights;
|
||||
ps.flags.plot_plot_styles = dialog.apply_plot_styles && !dialog.style_name.is_empty();
|
||||
ps.flags.show_plot_styles = dialog.show_plot_styles && !dialog.style_name.is_empty();
|
||||
ps.flags.draw_viewports_first = dialog.paperspace_last;
|
||||
ps.flags.plot_hidden = dialog.shade == "Hidden Line";
|
||||
ps.shade_plot_mode = match dialog.shade.as_str() {
|
||||
"2D Wireframe" | "3D Wireframe" => ShadePlotMode::Wireframe,
|
||||
"Hidden Line" => ShadePlotMode::Hidden,
|
||||
"As displayed" => ShadePlotMode::AsDisplayed,
|
||||
_ => ShadePlotMode::Rendered,
|
||||
};
|
||||
ps.shade_plot_resolution = match dialog.quality.as_str() {
|
||||
"Low" => ShadePlotResolutionLevel::Draft,
|
||||
"High" => ShadePlotResolutionLevel::Presentation,
|
||||
_ => ShadePlotResolutionLevel::Normal,
|
||||
};
|
||||
ps.shade_plot_dpi = 300;
|
||||
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.set_layout_plot_settings(&layout_name, &ps);
|
||||
for obj in self.tabs[i].scene.document.objects.values_mut() {
|
||||
if let acadrust::objects::ObjectType::Layout(l) = obj {
|
||||
if l.name == layout_name {
|
||||
l.min_limits = (0.0, 0.0);
|
||||
l.max_limits = (w, h);
|
||||
l.min_extents = (0.0, 0.0, 0.0);
|
||||
l.max_extents = (w, h, 0.0);
|
||||
l.paper_width = w;
|
||||
l.paper_height = h;
|
||||
l.plot_rotation = 0;
|
||||
l.plot_paper_units = 1;
|
||||
l.plot_origin_x = offset_x;
|
||||
l.plot_origin_y = offset_y;
|
||||
l.paper_size = String::new();
|
||||
if let acadrust::objects::ObjectType::Layout(layout) = obj {
|
||||
if layout.name == layout_name {
|
||||
layout.min_limits = (0.0, 0.0);
|
||||
layout.max_limits = (w, h);
|
||||
layout.min_extents = (0.0, 0.0, 0.0);
|
||||
layout.max_extents = (w, h, 0.0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use acadrust::objects::{
|
||||
ObjectType, PlotPaperUnits, PlotRotation, PlotSettings, PlotType, ScaledType,
|
||||
ShadePlotMode, ShadePlotResolutionLevel,
|
||||
};
|
||||
let plot_handle = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.iter()
|
||||
.find_map(|(h, obj)| {
|
||||
if let ObjectType::PlotSettings(ps) = obj {
|
||||
(ps.page_name == layout_name).then_some(*h)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let ps_entry = if let Some(h) = plot_handle {
|
||||
self.tabs[i].scene.document.objects.get_mut(&h)
|
||||
} else {
|
||||
let mut ps = PlotSettings::new(layout_name.clone());
|
||||
ps.handle = self.tabs[i].scene.document.allocate_handle();
|
||||
let h = ps.handle;
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.insert(h, ObjectType::PlotSettings(ps));
|
||||
self.tabs[i].scene.document.objects.get_mut(&h)
|
||||
};
|
||||
|
||||
if let Some(ObjectType::PlotSettings(ps)) = ps_entry {
|
||||
ps.paper_width = w;
|
||||
ps.paper_height = h;
|
||||
ps.paper_size = dialog.paper.clone();
|
||||
ps.paper_units = PlotPaperUnits::Millimeters;
|
||||
ps.plot_type = match plot_area {
|
||||
"Window" => PlotType::Window,
|
||||
"Display" => PlotType::LastScreenDisplay,
|
||||
"Extents" => PlotType::Extents,
|
||||
_ => PlotType::Layout,
|
||||
};
|
||||
if plot_area == "Window" {
|
||||
if let Some((x0, y0, x1, y1)) = plot_window {
|
||||
ps.set_plot_window(x0, y0, x1, y1);
|
||||
}
|
||||
}
|
||||
ps.flags.plot_centered = center && plot_area != "Layout";
|
||||
ps.origin_x = if plot_area == "Layout" { 0.0 } else { offset_x };
|
||||
ps.origin_y = if plot_area == "Layout" { 0.0 } else { offset_y };
|
||||
ps.rotation = match rotation {
|
||||
90 => PlotRotation::Degrees90,
|
||||
180 => PlotRotation::Degrees180,
|
||||
270 => PlotRotation::Degrees270,
|
||||
_ => PlotRotation::None,
|
||||
};
|
||||
if dialog.fit_to_paper && plot_area != "Layout" {
|
||||
ps.set_scale_to_fit();
|
||||
} else {
|
||||
let factor = if plot_area == "Layout" {
|
||||
1.0
|
||||
} else {
|
||||
plot_dialog_scale_factor(&dialog)
|
||||
};
|
||||
ps.scale_type = ScaledType::CustomScale;
|
||||
ps.scale_numerator = factor;
|
||||
ps.scale_denominator = 1.0;
|
||||
}
|
||||
ps.printer_name = if dialog.to_file {
|
||||
crate::ui::window::plot::OUT_PDF.into()
|
||||
} else {
|
||||
dialog.printer.clone().unwrap_or_default()
|
||||
};
|
||||
ps.current_style_sheet = dialog.style_name.clone();
|
||||
ps.flags.scale_lineweights = dialog.scale_lw;
|
||||
ps.flags.print_lineweights = dialog.lineweights;
|
||||
ps.flags.plot_plot_styles = !dialog.style_name.is_empty();
|
||||
ps.flags.draw_viewports_first = dialog.paperspace_last;
|
||||
ps.flags.plot_hidden = false;
|
||||
ps.shade_plot_mode = match dialog.shade.as_str() {
|
||||
"2D Wireframe" | "3D Wireframe" => ShadePlotMode::Wireframe,
|
||||
"Hidden Line" => ShadePlotMode::Hidden,
|
||||
"As displayed" => ShadePlotMode::AsDisplayed,
|
||||
_ => ShadePlotMode::Rendered,
|
||||
};
|
||||
ps.shade_plot_resolution = match dialog.quality.as_str() {
|
||||
"Low" => ShadePlotResolutionLevel::Draft,
|
||||
"High" => ShadePlotResolutionLevel::Presentation,
|
||||
_ => ShadePlotResolutionLevel::Normal,
|
||||
};
|
||||
ps.shade_plot_dpi = 300;
|
||||
}
|
||||
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.bump_geometry_no_blocks();
|
||||
self.command_line.push_info(crate::tf!(
|
||||
|
|
@ -2809,6 +2826,19 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
&mut self,
|
||||
path: std::path::PathBuf,
|
||||
) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
if self.tabs[i].scene.current_layout != "Model" {
|
||||
self.plot_dialog.paper_space = true;
|
||||
self.plot_dialog.scales = self.tabs[i]
|
||||
.scene
|
||||
.scale_list()
|
||||
.into_iter()
|
||||
.map(|(name, _, factor)| (name, factor))
|
||||
.collect();
|
||||
if let Some(settings) = self.tabs[i].scene.effective_plot_settings() {
|
||||
self.load_plotsettings_into_dialog(&settings);
|
||||
}
|
||||
}
|
||||
let Some((wires, hatches, wipeouts, group_splits, page_w, page_h, ox, oy, rotation, scale, clip)) =
|
||||
self.direct_plot_params()
|
||||
else {
|
||||
|
|
@ -2911,6 +2941,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
.filter(|name| name != "Model")
|
||||
.map(|name| (name, true))
|
||||
.collect();
|
||||
self.print_all_settings_override = false;
|
||||
self.active_modal = Some(crate::app::ModalKind::PrintAll);
|
||||
self.reset_modal_geometry();
|
||||
Task::none()
|
||||
|
|
@ -2920,10 +2951,12 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
let previous = self.plot_dialog.clone();
|
||||
let previous_style = self.active_plot_style.clone();
|
||||
let previous_window = self.plot_window;
|
||||
let previous_setup = self.plot_setup_template.clone();
|
||||
let task = self.on_plot_dialog_open();
|
||||
self.print_all_options_prev = Some(previous);
|
||||
self.print_all_plot_style_prev = Some(previous_style);
|
||||
self.print_all_plot_window_prev = Some(previous_window);
|
||||
self.print_all_plot_setup_prev = Some(previous_setup);
|
||||
self.print_all_options = true;
|
||||
self.plot_dialog.paper_space = true;
|
||||
self.plot_dialog.area = "Layout".into();
|
||||
|
|
@ -2945,47 +2978,145 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
let i = self.active_tab;
|
||||
let original_layout = self.tabs[i].scene.current_layout.clone();
|
||||
let original_viewport = self.tabs[i].scene.active_viewport;
|
||||
let dialog = self.plot_dialog.clone();
|
||||
let mut pages = Vec::with_capacity(selected.len());
|
||||
for name in selected {
|
||||
// Plot helpers read the active layout. Swap only this transient
|
||||
// selector so the drawing's saved active-space metadata is not
|
||||
// touched while the owned page snapshot is collected.
|
||||
{
|
||||
let scene = &mut self.tabs[i].scene;
|
||||
scene.current_layout = name;
|
||||
scene.active_viewport = None;
|
||||
let original_psltscale = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.header
|
||||
.paper_space_linetype_scaling;
|
||||
let original_plimcheck = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.header
|
||||
.paper_space_limit_check;
|
||||
let original_dialog = self.plot_dialog.clone();
|
||||
let original_style = self.active_plot_style.clone();
|
||||
let original_window = self.plot_window;
|
||||
let original_setup = self.plot_setup_template.clone();
|
||||
let original_camera = self.tabs[i].scene.camera.borrow().clone();
|
||||
let original_camera_generation = self.tabs[i].scene.camera_generation;
|
||||
let override_dialog = self.print_all_settings_override.then(|| original_dialog.clone());
|
||||
let override_style = self.print_all_settings_override.then(|| original_style.clone());
|
||||
let result = (|| {
|
||||
let mut pages = Vec::with_capacity(selected.len());
|
||||
for name in selected {
|
||||
let page_setup = self.tabs[i]
|
||||
.scene
|
||||
.plot_settings_for(&name)
|
||||
.ok_or_else(|| format!("Layout '{name}' has no page setup."))?;
|
||||
{
|
||||
let scene = &mut self.tabs[i].scene;
|
||||
scene.current_layout = name.clone();
|
||||
scene.active_viewport = None;
|
||||
scene.load_current_layout_state();
|
||||
}
|
||||
if let Some(dialog) = &override_dialog {
|
||||
self.plot_dialog = dialog.clone();
|
||||
self.active_plot_style = override_style.clone().flatten();
|
||||
self.plot_window = original_window;
|
||||
} else {
|
||||
self.plot_dialog.paper_space = true;
|
||||
self.plot_window = None;
|
||||
self.load_plotsettings_into_dialog(&page_setup);
|
||||
}
|
||||
let dialog = self.plot_dialog.clone();
|
||||
if dialog.area == "Display" {
|
||||
self.tabs[i].scene.restore_saved_camera();
|
||||
}
|
||||
if dialog.style_missing && dialog.apply_plot_styles {
|
||||
return Err(format!(
|
||||
"Layout '{name}' plot style table '{}' is not loaded.",
|
||||
dialog.style_name
|
||||
));
|
||||
}
|
||||
let plot_style = self.dialog_plot_style(&dialog);
|
||||
let params = match dialog.area.as_str() {
|
||||
"Display" => self.display_plot_job(),
|
||||
"Extents" => self.extents_plot_job(),
|
||||
"Limits" => self.limits_plot_job(),
|
||||
"Window" => self.window_plot_job(),
|
||||
area if area.starts_with("View: ") => {
|
||||
self.named_view_plot_job(area.trim_start_matches("View: "))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let (
|
||||
wires,
|
||||
hatches,
|
||||
wipeouts,
|
||||
group_splits,
|
||||
paper_w,
|
||||
paper_h,
|
||||
offset_x,
|
||||
offset_y,
|
||||
rotation_deg,
|
||||
scale,
|
||||
clip,
|
||||
) = if dialog.area == "Layout" {
|
||||
self.layout_plot_params_for("Layout")
|
||||
} else {
|
||||
let (
|
||||
wires,
|
||||
hatches,
|
||||
wipeouts,
|
||||
group_splits,
|
||||
paper_w,
|
||||
paper_h,
|
||||
offset_x,
|
||||
offset_y,
|
||||
rotation_deg,
|
||||
scale,
|
||||
clip,
|
||||
) = params.ok_or_else(|| format!("Layout '{name}' plot area is empty."))?;
|
||||
(
|
||||
std::sync::Arc::new(wires),
|
||||
hatches,
|
||||
wipeouts,
|
||||
group_splits,
|
||||
paper_w,
|
||||
paper_h,
|
||||
offset_x,
|
||||
offset_y,
|
||||
rotation_deg,
|
||||
scale,
|
||||
clip,
|
||||
)
|
||||
};
|
||||
pages.push(crate::io::pdf_export::PdfPageInput {
|
||||
wires,
|
||||
hatches,
|
||||
wipeouts,
|
||||
paper_w,
|
||||
paper_h,
|
||||
offset_x,
|
||||
offset_y,
|
||||
rotation_deg,
|
||||
scale,
|
||||
clip,
|
||||
options: Self::pdf_plot_options(&dialog, group_splits),
|
||||
plot_style,
|
||||
});
|
||||
}
|
||||
let (
|
||||
wires,
|
||||
hatches,
|
||||
wipeouts,
|
||||
group_splits,
|
||||
paper_w,
|
||||
paper_h,
|
||||
offset_x,
|
||||
offset_y,
|
||||
rotation_deg,
|
||||
scale,
|
||||
clip,
|
||||
) = self.layout_plot_params_for("Layout");
|
||||
pages.push(crate::io::pdf_export::PdfPageInput {
|
||||
wires,
|
||||
hatches,
|
||||
wipeouts,
|
||||
paper_w,
|
||||
paper_h,
|
||||
offset_x,
|
||||
offset_y,
|
||||
rotation_deg,
|
||||
scale,
|
||||
clip,
|
||||
options: Self::pdf_plot_options(&dialog, group_splits),
|
||||
});
|
||||
}
|
||||
Ok(pages)
|
||||
})();
|
||||
self.tabs[i].scene.current_layout = original_layout;
|
||||
self.tabs[i].scene.active_viewport = original_viewport;
|
||||
Ok(pages)
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.header
|
||||
.paper_space_linetype_scaling = original_psltscale;
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.header
|
||||
.paper_space_limit_check = original_plimcheck;
|
||||
self.plot_dialog = original_dialog;
|
||||
self.active_plot_style = original_style;
|
||||
self.plot_window = original_window;
|
||||
self.plot_setup_template = original_setup;
|
||||
*self.tabs[i].scene.camera.borrow_mut() = original_camera;
|
||||
self.tabs[i].scene.camera_generation = original_camera_generation;
|
||||
result
|
||||
}
|
||||
|
||||
pub(super) fn on_print_all_pdf_path_some(
|
||||
|
|
@ -2993,7 +3124,10 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
path: std::path::PathBuf,
|
||||
) -> Task<Message> {
|
||||
let dialog = self.plot_dialog.clone();
|
||||
if dialog.style_missing {
|
||||
if self.print_all_settings_override
|
||||
&& dialog.style_missing
|
||||
&& dialog.apply_plot_styles
|
||||
{
|
||||
self.command_line.push_error(crate::tf!(
|
||||
"Plot style table '{}' is not loaded.",
|
||||
dialog.style_name
|
||||
|
|
@ -3007,7 +3141,6 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
return Task::none();
|
||||
}
|
||||
};
|
||||
let plot_style = self.dialog_plot_style(&dialog);
|
||||
let worker_path = path.clone();
|
||||
self.save_config();
|
||||
self.close_active_modal();
|
||||
|
|
@ -3015,7 +3148,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
crate::io::pdf_export::export_pdf_pages(
|
||||
&pages,
|
||||
&worker_path,
|
||||
plot_style.as_ref(),
|
||||
None,
|
||||
)
|
||||
.map(|_| format!("Exported {} layouts to {}", pages.len(), worker_path.display()))
|
||||
.map_err(|error| format!("Export failed: {error}"))
|
||||
|
|
@ -3034,7 +3167,10 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let dialog = self.plot_dialog.clone();
|
||||
if dialog.style_missing {
|
||||
if self.print_all_settings_override
|
||||
&& dialog.style_missing
|
||||
&& dialog.apply_plot_styles
|
||||
{
|
||||
self.command_line.push_error(crate::tf!(
|
||||
"Plot style table '{}' is not loaded.",
|
||||
dialog.style_name
|
||||
|
|
@ -3048,7 +3184,6 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
return Task::none();
|
||||
}
|
||||
};
|
||||
let plot_style = self.dialog_plot_style(&dialog);
|
||||
let options = self.plot_print_options(&dialog, Default::default());
|
||||
let temp_path = crate::io::print_to_printer::temp_pdf_path("print_all");
|
||||
self.save_config();
|
||||
|
|
@ -3060,7 +3195,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
crate::io::pdf_export::export_pdf_pages(
|
||||
&pages,
|
||||
&temp_path,
|
||||
plot_style.as_ref(),
|
||||
None,
|
||||
)
|
||||
.and_then(|_| {
|
||||
crate::io::print_to_printer::print_existing_pdf(&temp_path, &options)
|
||||
|
|
@ -3112,23 +3247,10 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
}
|
||||
|
||||
fn layout_plot_params_for(&self, plot_area: &str) -> LayoutPlotParams {
|
||||
use crate::io::paper_sizes::{sheet_mm, Orientation, PaperSize};
|
||||
let i = self.active_tab;
|
||||
let scene = &self.tabs[i].scene;
|
||||
let paper_space = scene.current_layout != "Model";
|
||||
let selected_paper = match self.plot_dialog.paper.as_str() {
|
||||
"A3" => PaperSize::A3,
|
||||
"A2" => PaperSize::A2,
|
||||
"A1" => PaperSize::A1,
|
||||
"A0" => PaperSize::A0,
|
||||
_ => PaperSize::A4,
|
||||
};
|
||||
let selected_orientation = if self.plot_dialog.orientation == "Portrait" {
|
||||
Orientation::Portrait
|
||||
} else {
|
||||
Orientation::Landscape
|
||||
};
|
||||
let selected_sheet = sheet_mm(selected_paper, selected_orientation);
|
||||
let selected_sheet = plot_dialog_sheet_mm(&self.plot_dialog);
|
||||
let (source_wires, hatches, wipeouts, mut group_splits) =
|
||||
plot_scene_content(
|
||||
scene,
|
||||
|
|
@ -3463,14 +3585,21 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
let one_to_one = scale_name_for_factor(&scales, 1.0)
|
||||
.or_else(|| scales.first().map(|(name, _)| name.clone()))
|
||||
.unwrap_or_else(|| "1:1".into());
|
||||
// Keep the user's last print preferences (printer, copies, quality,
|
||||
// output options — persisted in `self.plot_dialog`); only refresh the
|
||||
// runtime printer list and reseed the drawing-specific fields from the
|
||||
// active layout.
|
||||
let paper_space = self.tabs[self.active_tab].scene.current_layout != "Model";
|
||||
let plot_views = self.tabs[self.active_tab]
|
||||
.scene
|
||||
.document
|
||||
.views
|
||||
.iter()
|
||||
.filter(|view| view.paper_space == paper_space)
|
||||
.map(|view| view.name.clone())
|
||||
.collect();
|
||||
// Keep session-only choices while loading drawing fields from the layout.
|
||||
let d = &mut self.plot_dialog;
|
||||
d.printers = crate::io::print_to_printer::list_printers();
|
||||
d.plot_styles = crate::io::plot_style::available_ctb_names();
|
||||
d.scales = scales;
|
||||
d.plot_views = plot_views;
|
||||
if d.scale.eq_ignore_ascii_case("fit") {
|
||||
d.fit_to_paper = true;
|
||||
d.scale_lw = false;
|
||||
|
|
@ -3478,7 +3607,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
} else if !d.scales.iter().any(|(name, _)| name == &d.scale) {
|
||||
d.scale = one_to_one.clone();
|
||||
}
|
||||
d.paper_space = self.tabs[self.active_tab].scene.current_layout != "Model";
|
||||
d.paper_space = paper_space;
|
||||
d.paper = self.plot_format.label().to_string();
|
||||
d.orientation = match self.plot_orientation {
|
||||
Orientation::Portrait => "Portrait",
|
||||
|
|
@ -3510,32 +3639,24 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
.map(|t| t.name.clone())
|
||||
.unwrap_or_default();
|
||||
d.style_missing = false;
|
||||
// `area`, scale and fit are remembered user choices,
|
||||
// not reseeded from the layout. Offset / center / rotation ARE layout
|
||||
// properties, so reflect them.
|
||||
if let Some(ps) = self.tabs[self.active_tab].scene.effective_plot_settings() {
|
||||
let d = &mut self.plot_dialog;
|
||||
d.center = ps.flags.plot_centered;
|
||||
d.offset_x = format!("{:.2}", ps.origin_x);
|
||||
d.offset_y = format!("{:.2}", ps.origin_y);
|
||||
let deg = ps.rotation.to_degrees() as i32;
|
||||
d.upside_down = matches!(deg, 180 | 270);
|
||||
}
|
||||
self.plot_dialog.name_input = None;
|
||||
self.plot_dialog.name_rename = false;
|
||||
if self.plot_dialog.fit_to_paper {
|
||||
self.plot_dialog.scale_lw = false;
|
||||
}
|
||||
// Refresh document/runtime lists, then restore the live choices from
|
||||
// the preceding dialog session. Previously the snapshot happened
|
||||
// after the layout values above were reloaded, so opening Plot itself
|
||||
// destroyed the user's last paper, area, scale and output choices.
|
||||
self.refresh_page_setups();
|
||||
self.plot_prev = Some(previous);
|
||||
self.select_page_setup(crate::ui::window::plot::SETUP_PREV);
|
||||
// `<previous>` may come from another tab/drawing whose custom scale is
|
||||
// not present in this document. Validate again after restoring it; the
|
||||
// pre-restore validation above only saw the temporarily reseeded state.
|
||||
let cur = self.tabs[self.active_tab].scene.current_layout.clone();
|
||||
let layout_entry = format!("*{cur}*");
|
||||
if self.tabs[self.active_tab]
|
||||
.scene
|
||||
.plot_settings_for(&cur)
|
||||
.is_some()
|
||||
{
|
||||
self.select_page_setup(&layout_entry);
|
||||
} else {
|
||||
self.select_page_setup(crate::ui::window::plot::SETUP_PREV);
|
||||
}
|
||||
if self.plot_dialog.scale.eq_ignore_ascii_case("fit") {
|
||||
self.plot_dialog.fit_to_paper = true;
|
||||
self.plot_dialog.scale = one_to_one.clone();
|
||||
|
|
@ -3550,10 +3671,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
if self.plot_dialog.fit_to_paper {
|
||||
self.plot_dialog.scale_lw = false;
|
||||
}
|
||||
// A model-space plot cannot use the paper-only Layout area. Other
|
||||
// values remain exactly as the user left them; selecting a layout or
|
||||
// named setup explicitly still reloads that setup below.
|
||||
let cur = self.tabs[self.active_tab].scene.current_layout.clone();
|
||||
// A model-space plot cannot use the paper-only Layout area.
|
||||
if cur == "Model" {
|
||||
if self.plot_dialog.area == "Layout" {
|
||||
self.plot_dialog.area = "Window".into();
|
||||
|
|
@ -3608,10 +3726,16 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
}
|
||||
M::Paper(s) => {
|
||||
self.plot_dialog.paper = s;
|
||||
let (w, h) = plot_dialog_sheet_mm(&self.plot_dialog);
|
||||
self.plot_dialog.paper_width_mm = w;
|
||||
self.plot_dialog.paper_height_mm = h;
|
||||
Task::none()
|
||||
}
|
||||
M::Orientation(s) => {
|
||||
self.plot_dialog.orientation = s;
|
||||
let (w, h) = plot_dialog_sheet_mm(&self.plot_dialog);
|
||||
self.plot_dialog.paper_width_mm = w;
|
||||
self.plot_dialog.paper_height_mm = h;
|
||||
Task::none()
|
||||
}
|
||||
M::Area(s) => {
|
||||
|
|
@ -3681,6 +3805,12 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
PlotFlag::ScaleLw if !d.fit_to_paper => {
|
||||
d.scale_lw = !d.scale_lw
|
||||
}
|
||||
PlotFlag::PlotStyles if !d.style_name.is_empty() => {
|
||||
d.apply_plot_styles = !d.apply_plot_styles
|
||||
}
|
||||
PlotFlag::DisplayStyles if d.paper_space && !d.style_name.is_empty() => {
|
||||
d.show_plot_styles = !d.show_plot_styles
|
||||
}
|
||||
PlotFlag::UpsideDown => {
|
||||
d.upside_down = !d.upside_down;
|
||||
}
|
||||
|
|
@ -3700,11 +3830,14 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
if name == STYLE_NONE {
|
||||
self.active_plot_style = None;
|
||||
self.plot_dialog.style_name.clear();
|
||||
self.plot_dialog.apply_plot_styles = false;
|
||||
self.plot_dialog.show_plot_styles = false;
|
||||
self.plot_dialog.style_missing = false;
|
||||
} else {
|
||||
match crate::io::plot_style::PlotStyleTable::load_named(&name) {
|
||||
Ok(table) => {
|
||||
self.plot_dialog.style_name = table.name.clone();
|
||||
self.plot_dialog.apply_plot_styles = true;
|
||||
self.plot_dialog.style_missing = false;
|
||||
self.active_plot_style = Some(table);
|
||||
}
|
||||
|
|
@ -3852,7 +3985,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
}
|
||||
M::Preview => self.on_plot_dlg_commit(true),
|
||||
M::Commit if self.print_all_options => {
|
||||
if self.plot_dialog.style_missing {
|
||||
if self.plot_dialog.style_missing && self.plot_dialog.apply_plot_styles {
|
||||
self.command_line.push_error(crate::tf!(
|
||||
"Plot style table '{}' is not loaded.",
|
||||
self.plot_dialog.style_name
|
||||
|
|
@ -3863,9 +3996,11 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
self.plot_dialog.area = "Layout".into();
|
||||
self.sync_dialog_plot_runtime();
|
||||
self.save_config();
|
||||
self.print_all_settings_override = true;
|
||||
self.print_all_options = false;
|
||||
self.print_all_options_prev = None;
|
||||
self.print_all_plot_style_prev = None;
|
||||
self.print_all_plot_setup_prev = None;
|
||||
if let Some(previous) = self.print_all_plot_window_prev.take() {
|
||||
self.plot_window = previous;
|
||||
}
|
||||
|
|
@ -3894,12 +4029,15 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
use crate::ui::window::plot::{SETUP_NONE, SETUP_PREV};
|
||||
self.plot_dialog.selected_setup = name.to_string();
|
||||
if name == SETUP_NONE {
|
||||
self.plot_setup_template = None;
|
||||
// No page setup: default geometry + PDF output.
|
||||
let is_model = self.tabs[self.active_tab].scene.current_layout == "Model";
|
||||
let d = &mut self.plot_dialog;
|
||||
d.to_file = true;
|
||||
d.paper = "A4".into();
|
||||
d.orientation = "Landscape".into();
|
||||
d.paper_width_mm = 297.0;
|
||||
d.paper_height_mm = 210.0;
|
||||
d.area = if is_model { "Window".into() } else { "Layout".into() };
|
||||
d.center = true;
|
||||
d.offset_x = "0.0".into();
|
||||
|
|
@ -3947,32 +4085,32 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
PlotPaperUnits, PlotRotation, PlotSettings, PlotType, ScaledType, ShadePlotMode,
|
||||
ShadePlotResolutionLevel,
|
||||
};
|
||||
use crate::io::paper_sizes::{sheet_mm, Orientation, PaperSize};
|
||||
let d = &self.plot_dialog;
|
||||
let paper = match d.paper.as_str() {
|
||||
"A3" => PaperSize::A3,
|
||||
"A2" => PaperSize::A2,
|
||||
"A1" => PaperSize::A1,
|
||||
"A0" => PaperSize::A0,
|
||||
_ => PaperSize::A4,
|
||||
let (w, h) = plot_dialog_sheet_mm(d);
|
||||
let mut ps = match self.plot_setup_template.clone() {
|
||||
Some(settings) => settings,
|
||||
None => {
|
||||
let mut settings = PlotSettings::new("");
|
||||
settings.paper_units = PlotPaperUnits::Millimeters;
|
||||
settings
|
||||
}
|
||||
};
|
||||
let orient = if d.orientation == "Portrait" {
|
||||
Orientation::Portrait
|
||||
} else {
|
||||
Orientation::Landscape
|
||||
};
|
||||
let (w, h) = sheet_mm(paper, orient);
|
||||
let mut ps = PlotSettings::new("");
|
||||
ps.paper_width = w;
|
||||
ps.paper_height = h;
|
||||
ps.paper_size = d.paper.clone();
|
||||
ps.paper_units = PlotPaperUnits::Millimeters;
|
||||
ps.plot_type = match d.area.as_str() {
|
||||
"Window" => PlotType::Window,
|
||||
"Layout" => PlotType::Layout,
|
||||
"Display" => PlotType::LastScreenDisplay,
|
||||
"Limits" => PlotType::Limits,
|
||||
area if area.starts_with("View: ") => PlotType::View,
|
||||
_ => PlotType::Extents,
|
||||
};
|
||||
ps.plot_view_name = d
|
||||
.area
|
||||
.strip_prefix("View: ")
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
if d.area == "Window" {
|
||||
if let Some((x0, y0, x1, y1)) = self.plot_window {
|
||||
ps.set_plot_window(x0, y0, x1, y1);
|
||||
|
|
@ -3998,6 +4136,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
};
|
||||
if d.area == "Layout" {
|
||||
ps.set_standard_scale(ScaledType::OneToOne);
|
||||
ps.standard_scale_factor = 1.0;
|
||||
} else if d.fit_to_paper {
|
||||
ps.set_scale_to_fit();
|
||||
} else {
|
||||
|
|
@ -4005,6 +4144,8 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
ps.scale_type = ScaledType::CustomScale;
|
||||
ps.scale_numerator = factor;
|
||||
ps.scale_denominator = 1.0;
|
||||
ps.standard_scale_factor = factor;
|
||||
ps.flags.use_standard_scale = false;
|
||||
}
|
||||
ps.printer_name = if d.to_file {
|
||||
crate::ui::window::plot::OUT_PDF.into()
|
||||
|
|
@ -4014,9 +4155,10 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
ps.current_style_sheet = d.style_name.clone();
|
||||
ps.flags.scale_lineweights = d.scale_lw;
|
||||
ps.flags.print_lineweights = d.lineweights;
|
||||
ps.flags.plot_plot_styles = !d.style_name.is_empty();
|
||||
ps.flags.plot_plot_styles = d.apply_plot_styles && !d.style_name.is_empty();
|
||||
ps.flags.show_plot_styles = d.show_plot_styles && !d.style_name.is_empty();
|
||||
ps.flags.draw_viewports_first = d.paperspace_last;
|
||||
ps.flags.plot_hidden = false;
|
||||
ps.flags.plot_hidden = d.shade == "Hidden Line";
|
||||
ps.shade_plot_mode = match d.shade.as_str() {
|
||||
"2D Wireframe" | "3D Wireframe" => ShadePlotMode::Wireframe,
|
||||
"Hidden Line" => ShadePlotMode::Hidden,
|
||||
|
|
@ -4037,6 +4179,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
use acadrust::objects::{
|
||||
PlotType, ShadePlotMode, ShadePlotResolutionLevel,
|
||||
};
|
||||
self.plot_setup_template = Some(ps.clone());
|
||||
if matches!(ps.plot_type, PlotType::Window) && !ps.plot_window.is_empty() {
|
||||
self.plot_window = Some((
|
||||
ps.plot_window.lower_left_x,
|
||||
|
|
@ -4059,7 +4202,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
}
|
||||
let active_style_name = self.active_plot_style.as_ref().map(|table| table.name.clone());
|
||||
let style_name = if ps.current_style_sheet.is_empty() {
|
||||
active_style_name.clone().unwrap_or_default()
|
||||
String::new()
|
||||
} else if active_style_name
|
||||
.as_deref()
|
||||
.is_some_and(|name| name.eq_ignore_ascii_case(&ps.current_style_sheet))
|
||||
|
|
@ -4072,17 +4215,30 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
&& active_style_name
|
||||
.as_deref()
|
||||
.is_some_and(|name| name.eq_ignore_ascii_case(&style_name));
|
||||
let (paper, orient) = paper_label_from_dims(ps.paper_width, ps.paper_height);
|
||||
let (mut paper, orient) = paper_label_from_dims(ps.paper_width, ps.paper_height);
|
||||
if !matches!(paper.as_str(), "A4" | "A3" | "A2" | "A1" | "A0")
|
||||
&& !ps.paper_size.is_empty()
|
||||
{
|
||||
paper = ps.paper_size.clone();
|
||||
}
|
||||
let d = &mut self.plot_dialog;
|
||||
d.paper = paper;
|
||||
d.paper_width_mm = ps.paper_width.max(1.0);
|
||||
d.paper_height_mm = ps.paper_height.max(1.0);
|
||||
d.orientation = orient;
|
||||
d.area = match ps.plot_type {
|
||||
PlotType::Window => "Window",
|
||||
PlotType::Layout => "Layout",
|
||||
PlotType::LastScreenDisplay => "Display",
|
||||
_ => "Extents",
|
||||
}
|
||||
.to_string();
|
||||
PlotType::Window => "Window".to_string(),
|
||||
PlotType::Layout => "Layout".to_string(),
|
||||
PlotType::LastScreenDisplay => "Display".to_string(),
|
||||
PlotType::Limits => "Limits".to_string(),
|
||||
PlotType::View if !ps.plot_view_name.is_empty() => {
|
||||
d.plot_views.push(ps.plot_view_name.clone());
|
||||
d.plot_views.sort();
|
||||
d.plot_views.dedup();
|
||||
format!("View: {}", ps.plot_view_name)
|
||||
}
|
||||
_ => "Extents".to_string(),
|
||||
};
|
||||
if !d.paper_space && d.area == "Layout" {
|
||||
d.area = "Extents".into();
|
||||
}
|
||||
|
|
@ -4102,6 +4258,12 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
d.fit_to_paper = d.area != "Layout" && ps.is_scale_to_fit();
|
||||
let target_factor = if d.area == "Layout" {
|
||||
1.0
|
||||
} else if ps.flags.use_standard_scale {
|
||||
if ps.standard_scale_factor.is_finite() && ps.standard_scale_factor > 0.0 {
|
||||
ps.standard_scale_factor
|
||||
} else {
|
||||
ps.scale_type.scale_factor()
|
||||
}
|
||||
} else if ps.scale_denominator.abs() > 1e-9 {
|
||||
ps.scale_numerator / ps.scale_denominator
|
||||
} else {
|
||||
|
|
@ -4116,11 +4278,15 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
d.scale_lw = ps.flags.scale_lineweights && !d.fit_to_paper;
|
||||
d.lineweights = ps.flags.print_lineweights;
|
||||
d.paperspace_last = ps.flags.draw_viewports_first;
|
||||
d.shade = match ps.shade_plot_mode {
|
||||
ShadePlotMode::Wireframe => "2D Wireframe",
|
||||
ShadePlotMode::Hidden => "Hidden Line",
|
||||
ShadePlotMode::Rendered => "Gouraud Shaded",
|
||||
ShadePlotMode::AsDisplayed => "As displayed",
|
||||
d.shade = if ps.flags.plot_hidden {
|
||||
"Hidden Line"
|
||||
} else {
|
||||
match ps.shade_plot_mode {
|
||||
ShadePlotMode::Wireframe => "2D Wireframe",
|
||||
ShadePlotMode::Hidden => "Hidden Line",
|
||||
ShadePlotMode::Rendered => "Gouraud Shaded",
|
||||
ShadePlotMode::AsDisplayed => "As displayed",
|
||||
}
|
||||
}
|
||||
.into();
|
||||
d.quality = match ps.shade_plot_resolution {
|
||||
|
|
@ -4139,6 +4305,8 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
d.printer = (!ps.printer_name.is_empty()).then(|| ps.printer_name.clone());
|
||||
}
|
||||
d.style_name = style_name;
|
||||
d.apply_plot_styles = ps.flags.plot_plot_styles;
|
||||
d.show_plot_styles = ps.flags.show_plot_styles;
|
||||
d.style_missing = !d.style_name.is_empty() && !style_loaded;
|
||||
}
|
||||
|
||||
|
|
@ -4164,10 +4332,9 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
}
|
||||
|
||||
fn apply_dialog_to_layout(&mut self) {
|
||||
use crate::io::paper_sizes::sheet_mm;
|
||||
self.sync_dialog_plot_runtime();
|
||||
let d = self.plot_dialog.clone();
|
||||
let (sheet_w, sheet_h) = sheet_mm(self.plot_format, self.plot_orientation);
|
||||
let (sheet_w, sheet_h) = plot_dialog_sheet_mm(&d);
|
||||
let rotation: i16 = if d.upside_down { 180 } else { 0 };
|
||||
let off_x = d.offset_x.parse::<f64>().unwrap_or(0.0);
|
||||
let off_y = d.offset_y.parse::<f64>().unwrap_or(0.0);
|
||||
|
|
@ -4185,7 +4352,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
/// Open a preview PDF, export a PDF, or send the job to the chosen printer.
|
||||
fn on_plot_dlg_commit(&mut self, preview: bool) -> Task<Message> {
|
||||
let d = self.plot_dialog.clone();
|
||||
if d.style_missing {
|
||||
if d.style_missing && d.apply_plot_styles {
|
||||
self.command_line.push_error(crate::tf!(
|
||||
"Plot style table '{}' is not loaded.",
|
||||
d.style_name
|
||||
|
|
@ -4203,11 +4370,16 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
let plot_style = self.dialog_plot_style(&d);
|
||||
// Extents, Window and Display use one plot path in both spaces. Only
|
||||
// Paper-space Layout is special: it uses the physical sheet bounds.
|
||||
if matches!(d.area.as_str(), "Extents" | "Window" | "Display") {
|
||||
if d.area != "Layout" {
|
||||
let job = match d.area.as_str() {
|
||||
"Display" => self.display_plot_job(),
|
||||
"Extents" => self.extents_plot_job(),
|
||||
_ => self.window_plot_job(),
|
||||
"Limits" => self.limits_plot_job(),
|
||||
"Window" => self.window_plot_job(),
|
||||
area if area.starts_with("View: ") => {
|
||||
self.named_view_plot_job(area.trim_start_matches("View: "))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let Some((
|
||||
w_wires,
|
||||
|
|
@ -4341,7 +4513,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
&self,
|
||||
d: &crate::ui::window::plot::PlotDialogState,
|
||||
) -> Option<crate::io::plot_style::PlotStyleTable> {
|
||||
if d.style_name.is_empty() || d.style_missing {
|
||||
if d.style_name.is_empty() || d.style_missing || !d.apply_plot_styles {
|
||||
return None;
|
||||
}
|
||||
self.active_plot_style
|
||||
|
|
@ -4358,6 +4530,34 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
self.area_plot_job(self.display_plot_window()?)
|
||||
}
|
||||
|
||||
fn limits_plot_job(&self) -> Option<ClippedPlotParams> {
|
||||
let (min, max) = self.tabs[self.active_tab]
|
||||
.scene
|
||||
.current_drawing_limits()?;
|
||||
self.area_plot_job((min.x, min.y, max.x, max.y))
|
||||
}
|
||||
|
||||
fn named_view_plot_job(&self, name: &str) -> Option<ClippedPlotParams> {
|
||||
let view = self.tabs[self.active_tab]
|
||||
.scene
|
||||
.document
|
||||
.views
|
||||
.iter()
|
||||
.find(|view| {
|
||||
view.name.eq_ignore_ascii_case(name)
|
||||
&& view.paper_space == (self.tabs[self.active_tab].scene.current_layout != "Model")
|
||||
})?;
|
||||
let half_w = view.width.abs() * 0.5;
|
||||
let half_h = view.height.abs() * 0.5;
|
||||
(half_w > 1e-9 && half_h > 1e-9).then_some(())?;
|
||||
self.area_plot_job((
|
||||
view.center.x - half_w,
|
||||
view.center.y - half_h,
|
||||
view.center.x + half_w,
|
||||
view.center.y + half_h,
|
||||
))
|
||||
}
|
||||
|
||||
fn extents_plot_job(&self) -> Option<ClippedPlotParams> {
|
||||
let scene = &self.tabs[self.active_tab].scene;
|
||||
if scene.current_layout == "Model" {
|
||||
|
|
@ -4446,13 +4646,13 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
/// Render a selected rectangle through one shared Model/Paper path. The
|
||||
/// window may lie partly or wholly outside a paper sheet.
|
||||
fn area_plot_job(&self, window: (f64, f64, f64, f64)) -> Option<ClippedPlotParams> {
|
||||
use crate::io::paper_sizes::{sheet_mm, window_to_sheet, PlotScale};
|
||||
use crate::io::paper_sizes::{window_to_sheet, PlotScale};
|
||||
let i = self.active_tab;
|
||||
let (x0, y0, x1, y1) = window;
|
||||
if (x1 - x0) < 1e-6 || (y1 - y0) < 1e-6 {
|
||||
return None;
|
||||
}
|
||||
let (sheet_w, sheet_h) = sheet_mm(self.plot_format, self.plot_orientation);
|
||||
let (sheet_w, sheet_h) = plot_dialog_sheet_mm(&self.plot_dialog);
|
||||
let win_w = (x1 - x0).max(1e-9);
|
||||
let win_h = (y1 - y0).max(1e-9);
|
||||
let scale_sel = if self.plot_dialog.fit_to_paper {
|
||||
|
|
|
|||
|
|
@ -151,6 +151,9 @@ impl OpenCADStudio {
|
|||
if let Some(previous) = self.print_all_plot_window_prev.take() {
|
||||
self.plot_window = previous;
|
||||
}
|
||||
if let Some(previous) = self.print_all_plot_setup_prev.take() {
|
||||
self.plot_setup_template = previous;
|
||||
}
|
||||
self.print_all_options = false;
|
||||
self.active_modal = Some(PrintAll);
|
||||
self.reset_modal_geometry();
|
||||
|
|
@ -6917,6 +6920,9 @@ impl OpenCADStudio {
|
|||
self.plot_dialog.style_missing = false;
|
||||
self.plot_dialog.plot_styles =
|
||||
crate::io::plot_style::available_ctb_names();
|
||||
self.tabs[self.active_tab]
|
||||
.scene
|
||||
.invalidate_display_plot_style();
|
||||
|
||||
self.command_line.push_output(
|
||||
crate::tf!(
|
||||
|
|
@ -6968,6 +6974,9 @@ impl OpenCADStudio {
|
|||
self.plot_dialog.style_missing = false;
|
||||
self.plot_dialog.plot_styles =
|
||||
crate::io::plot_style::available_ctb_names();
|
||||
self.tabs[self.active_tab]
|
||||
.scene
|
||||
.invalidate_display_plot_style();
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"Plot style table saved to \"{}\".",
|
||||
path.display()
|
||||
|
|
|
|||
|
|
@ -62,8 +62,7 @@ pub(super) fn layout_entry_name(s: &str) -> &str {
|
|||
s.trim_start_matches('*').trim_end_matches('*')
|
||||
}
|
||||
|
||||
/// Infer the closest A-series paper label and orientation from sheet
|
||||
/// dimensions (mm). Falls back to A4 when nothing is close.
|
||||
/// Infer an A-series label when dimensions match; otherwise retain the size.
|
||||
pub(super) fn paper_label_from_dims(w: f64, h: f64) -> (String, String) {
|
||||
use crate::io::paper_sizes::PaperSize;
|
||||
let orient = if w >= h { "Landscape" } else { "Portrait" };
|
||||
|
|
@ -76,7 +75,12 @@ pub(super) fn paper_label_from_dims(w: f64, h: f64) -> (String, String) {
|
|||
best = (p.label().to_string(), err);
|
||||
}
|
||||
}
|
||||
(best.0, orient.to_string())
|
||||
let label = if best.1 <= 2.0 {
|
||||
best.0
|
||||
} else {
|
||||
format!("{w:.2} × {h:.2} mm")
|
||||
};
|
||||
(label, orient.to_string())
|
||||
}
|
||||
|
||||
pub(super) fn parse_plot_scale(s: &str) -> (f64, f64) {
|
||||
|
|
|
|||
|
|
@ -5293,8 +5293,7 @@ impl OpenCADStudio {
|
|||
self.tabs[i].scene.set_current_layout(name.clone());
|
||||
self.tabs[i].scene.deselect_all();
|
||||
let switch_ms = perf_phase.elapsed().as_secs_f64() * 1000.0;
|
||||
// UCS follows the pane: model header UCS in the Model tab, none
|
||||
// in plain paper space (a viewport's UCS is adopted on entry).
|
||||
// UCS follows the active model, layout, or floating viewport pane.
|
||||
let perf_phase = Instant::now();
|
||||
self.tabs[i].refresh_active_ucs();
|
||||
self.tabs[i].scene.restore_saved_camera();
|
||||
|
|
@ -5358,10 +5357,28 @@ impl OpenCADStudio {
|
|||
self.push_undo_snapshot(i, "LAYOUT");
|
||||
match self.tabs[i].scene.document.add_layout(&new_name) {
|
||||
Ok(_) => {
|
||||
// Override the acadrust default limits (12×9 imperial) with A4 landscape.
|
||||
let layout_flags = i16::from(
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.header
|
||||
.paper_space_linetype_scaling,
|
||||
) | (i16::from(
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.header
|
||||
.paper_space_limit_check,
|
||||
) << 1);
|
||||
let plot_style = self
|
||||
.active_plot_style
|
||||
.as_ref()
|
||||
.map(|style| style.name.clone())
|
||||
.unwrap_or_default();
|
||||
for obj in self.tabs[i].scene.document.objects.values_mut() {
|
||||
if let acadrust::objects::ObjectType::Layout(l) = obj {
|
||||
if l.name == new_name {
|
||||
l.flags = layout_flags;
|
||||
l.min_limits = (0.0, 0.0);
|
||||
l.max_limits = (297.0, 210.0);
|
||||
l.min_extents = (0.0, 0.0, 0.0);
|
||||
|
|
@ -5371,6 +5388,14 @@ impl OpenCADStudio {
|
|||
l.plot_paper_units = 1;
|
||||
l.plot_scale_numerator = 1.0;
|
||||
l.plot_scale_denominator = 1.0;
|
||||
l.plot_scale_type = 16;
|
||||
l.plot_scale_factor = 1.0;
|
||||
l.plot_type = 5;
|
||||
l.plot_flags.use_standard_scale = true;
|
||||
l.plot_flags.print_lineweights = true;
|
||||
l.plot_flags.plot_plot_styles = !plot_style.is_empty();
|
||||
l.plot_flags.show_plot_styles = !plot_style.is_empty();
|
||||
l.plot_style_sheet = plot_style;
|
||||
l.paper_size = "ISO_A4_(297.00_x_210.00_MM)".into();
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ use std::io::Write;
|
|||
use std::path::Path;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
||||
pub struct PlotWire {
|
||||
pub wire: WireModel,
|
||||
pub draw_depth: f32,
|
||||
|
|
@ -120,6 +121,7 @@ pub struct PdfPageInput {
|
|||
pub scale: f32,
|
||||
pub clip: Option<(f32, f32, f32, f32)>,
|
||||
pub options: PdfPlotOptions,
|
||||
pub plot_style: Option<PlotStyleTable>,
|
||||
}
|
||||
|
||||
impl Default for PdfPlotOptions {
|
||||
|
|
@ -270,7 +272,7 @@ fn build_pdf_pages(pages: &[PdfPageInput], plot_style: Option<&PlotStyleTable>)
|
|||
page.rotation_deg,
|
||||
page.scale,
|
||||
page.clip,
|
||||
plot_style,
|
||||
page.plot_style.as_ref().or(plot_style),
|
||||
page.options,
|
||||
);
|
||||
}
|
||||
|
|
@ -440,6 +442,8 @@ fn append_pdf_page(
|
|||
|
||||
let mut last_color: Option<[f32; 3]> = None;
|
||||
let mut last_lw: Option<f32> = None;
|
||||
let mut last_cap = Some(LineCapStyle::Round);
|
||||
let mut last_join = Some(LineJoinStyle::Round);
|
||||
// Current PDF dash array (empty = solid). Tracked so the dash op is only
|
||||
// re-emitted when it actually changes between wires.
|
||||
let mut last_dash: Option<Vec<i64>> = None;
|
||||
|
|
@ -453,7 +457,9 @@ fn append_pdf_page(
|
|||
ox,
|
||||
oy,
|
||||
plot_style,
|
||||
scale,
|
||||
options,
|
||||
normal_blend.as_ref(),
|
||||
);
|
||||
last_color = None;
|
||||
last_lw = None;
|
||||
|
|
@ -506,6 +512,8 @@ fn append_pdf_page(
|
|||
let mut lw_override: Option<f32> = None;
|
||||
let mut screening = 1.0;
|
||||
let mut color_overridden = false;
|
||||
let mut cap = None;
|
||||
let mut join = None;
|
||||
if let Some(ctb) = plot_style {
|
||||
if wire.aci > 0 {
|
||||
if let Some([cr, cg, cb]) = ctb.resolve_color(wire.aci) {
|
||||
|
|
@ -518,6 +526,20 @@ fn append_pdf_page(
|
|||
.resolve_lineweight(wire.aci)
|
||||
.map(|mm| (mm * MM_TO_PT).max(0.1));
|
||||
screening = ctb.resolve_screening(wire.aci);
|
||||
if let Some(entry) = ctb.aci_entries.get(wire.aci as usize) {
|
||||
cap = match entry.end_style {
|
||||
0 => Some(LineCapStyle::Butt),
|
||||
1 | 3 => Some(LineCapStyle::ProjectingSquare),
|
||||
2 => Some(LineCapStyle::Round),
|
||||
_ => None,
|
||||
};
|
||||
join = match entry.join_style {
|
||||
0 => Some(LineJoinStyle::Miter),
|
||||
1 | 3 => Some(LineJoinStyle::Bevel),
|
||||
2 => Some(LineJoinStyle::Round),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
// Near-white and near-yellow (viewport active border) → dark grey for print
|
||||
|
|
@ -539,6 +561,17 @@ fn append_pdf_page(
|
|||
}
|
||||
[r, g, b] = plotted_color([r, g, b], a, screening, options);
|
||||
|
||||
let cap = cap.unwrap_or(LineCapStyle::Round);
|
||||
if last_cap != Some(cap) {
|
||||
ops.push(Op::SetLineCapStyle { cap });
|
||||
last_cap = Some(cap);
|
||||
}
|
||||
let join = join.unwrap_or(LineJoinStyle::Round);
|
||||
if last_join != Some(join) {
|
||||
ops.push(Op::SetLineJoinStyle { join });
|
||||
last_join = Some(join);
|
||||
}
|
||||
|
||||
if last_color
|
||||
.map(|c| (c[0] - r).abs() > 0.01 || (c[1] - g).abs() > 0.01 || (c[2] - b).abs() > 0.01)
|
||||
.unwrap_or(true)
|
||||
|
|
@ -576,13 +609,11 @@ fn append_pdf_page(
|
|||
let lw_pt = if wire.world_width > 0.0 {
|
||||
wire.world_width * MM_TO_PT
|
||||
} else {
|
||||
let physical = lw_override.unwrap_or_else(|| {
|
||||
if options.object_lineweights {
|
||||
(wire.line_weight_px * LW_PX_TO_PT).max(0.1)
|
||||
} else {
|
||||
0.1
|
||||
}
|
||||
});
|
||||
let physical = if options.object_lineweights {
|
||||
lw_override.unwrap_or_else(|| (wire.line_weight_px * LW_PX_TO_PT).max(0.1))
|
||||
} else {
|
||||
0.1
|
||||
};
|
||||
physical / pen_divisor
|
||||
};
|
||||
if last_lw.map(|l| (l - lw_pt).abs() > 0.01).unwrap_or(true) {
|
||||
|
|
@ -852,12 +883,71 @@ fn emit_wire_fills(
|
|||
ox: f64,
|
||||
oy: f64,
|
||||
plot_style: Option<&PlotStyleTable>,
|
||||
scale: f32,
|
||||
options: PdfPlotOptions,
|
||||
normal_blend: Option<&ExtendedGraphicsStateId>,
|
||||
) {
|
||||
for wire in wires {
|
||||
if wire.fill_tris.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let styled_pattern = plot_style.and_then(|table| {
|
||||
(wire.aci > 0)
|
||||
.then(|| table.aci_entries.get(wire.aci as usize))
|
||||
.flatten()
|
||||
.and_then(|entry| {
|
||||
(65..=72)
|
||||
.contains(&entry.fill_style)
|
||||
.then(|| {
|
||||
crate::scene::model::hatch_model::plot_style_fill_pattern(
|
||||
entry.fill_style,
|
||||
)
|
||||
})
|
||||
.flatten()
|
||||
})
|
||||
});
|
||||
if let Some(pattern) = styled_pattern {
|
||||
for (triangle_index, triangle) in wire.fill_tris.chunks_exact(3).enumerate() {
|
||||
let mut boundary = Vec::with_capacity(4);
|
||||
for (point_index, point) in triangle.iter().enumerate() {
|
||||
let index = triangle_index * 3 + point_index;
|
||||
let low = wire.fill_tris_low.get(index).copied().unwrap_or([0.0; 3]);
|
||||
boundary.push([point[0] + low[0], point[1] + low[1]]);
|
||||
}
|
||||
boundary.push(boundary[0]);
|
||||
let hatch = HatchModel {
|
||||
render_instance: wire.render_instance.clone(),
|
||||
world_origin: [0.0, 0.0],
|
||||
boundary: std::sync::Arc::new(boundary),
|
||||
boundary_wcs: None,
|
||||
fill_plane: None,
|
||||
fill_plane_boundary: None,
|
||||
boundary_exterior: None,
|
||||
boundary_sources: None,
|
||||
boundary_paths: None,
|
||||
style: acadrust::entities::HatchStyleType::Normal,
|
||||
pattern: pattern.clone(),
|
||||
name: "PLOTSTYLE".to_string(),
|
||||
color: wire.color,
|
||||
aci: wire.aci,
|
||||
line_weight_px: wire.line_weight_px,
|
||||
angle_offset: 0.0,
|
||||
scale: 1.0 / scale.max(1.0e-6),
|
||||
draw_depth: wire.depth_override.unwrap_or(0.0),
|
||||
};
|
||||
emit_hatch(
|
||||
ops,
|
||||
&hatch,
|
||||
ox,
|
||||
oy,
|
||||
plot_style,
|
||||
scale,
|
||||
options,
|
||||
normal_blend,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let [mut r, mut g, mut b, a] = wire.color;
|
||||
if a < 0.01 {
|
||||
continue;
|
||||
|
|
@ -964,6 +1054,26 @@ fn emit_hatch(
|
|||
if hatch.boundary.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut styled_hatch = None;
|
||||
if let Some(table) = plot_style {
|
||||
if hatch.aci > 0 && matches!(hatch.pattern, HatchPattern::Solid) {
|
||||
if let Some(pattern) = table
|
||||
.aci_entries
|
||||
.get(hatch.aci as usize)
|
||||
.and_then(|entry| {
|
||||
crate::scene::model::hatch_model::plot_style_fill_pattern(
|
||||
entry.fill_style,
|
||||
)
|
||||
})
|
||||
{
|
||||
let mut model = hatch.clone();
|
||||
model.pattern = pattern;
|
||||
model.scale = 1.0 / scale.max(1.0e-6);
|
||||
styled_hatch = Some(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
let hatch = styled_hatch.as_ref().unwrap_or(hatch);
|
||||
let [mut r, mut g, mut b, a] = hatch.color;
|
||||
if a < 0.01 {
|
||||
return;
|
||||
|
|
@ -1082,13 +1192,11 @@ fn emit_hatch(
|
|||
// Pattern hatches: rasterise the family lines clipped to the boundary
|
||||
// and emit each as a stroked line. Skips the polygon outline entirely.
|
||||
if matches!(hatch.pattern, HatchPattern::Pattern(_)) {
|
||||
let physical = lw_override.unwrap_or_else(|| {
|
||||
if options.object_lineweights {
|
||||
(hatch.line_weight_px * LW_PX_TO_PT).max(0.1)
|
||||
} else {
|
||||
0.1
|
||||
}
|
||||
});
|
||||
let physical = if options.object_lineweights {
|
||||
lw_override.unwrap_or_else(|| (hatch.line_weight_px * LW_PX_TO_PT).max(0.1))
|
||||
} else {
|
||||
0.1
|
||||
};
|
||||
let divisor = if options.scale_lineweights {
|
||||
1.0
|
||||
} else {
|
||||
|
|
@ -1248,16 +1356,16 @@ fn emit_text(
|
|||
if let Some(ctb) = plot_style {
|
||||
if wire.aci > 0 {
|
||||
ctb_color = ctb.resolve_color(wire.aci);
|
||||
lw_override = ctb
|
||||
.resolve_lineweight(wire.aci)
|
||||
.map(|mm| {
|
||||
lw_override = options.object_lineweights.then(|| {
|
||||
ctb.resolve_lineweight(wire.aci).map(|mm| {
|
||||
let divisor = if options.scale_lineweights {
|
||||
1.0
|
||||
} else {
|
||||
scale.max(1e-6)
|
||||
};
|
||||
(mm * MM_TO_PT).max(0.1) / divisor
|
||||
});
|
||||
})
|
||||
}).flatten();
|
||||
screening = ctb.resolve_screening(wire.aci);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! Plot Style Table — CTB (color-based) and STB (named) file support.
|
||||
//!
|
||||
//! CTB files map indexed drawing colors (ACI, 1-255) to pen properties:
|
||||
//! RGB color override, lineweight, and screeing percentage.
|
||||
//! RGB color override, lineweight, and screening percentage.
|
||||
//!
|
||||
//! File format: a fixed 60-byte header followed by zlib-compressed text.
|
||||
//!
|
||||
|
|
@ -17,6 +17,21 @@ use std::path::PathBuf;
|
|||
|
||||
pub const DEFAULT_PLOT_STYLE: &str = "ocad.ctb";
|
||||
pub const MONOCHROME_PLOT_STYLE: &str = "monochrome.ctb";
|
||||
pub const GRAYSCALE_PLOT_STYLE: &str = "Grayscale.ctb";
|
||||
pub const FILL_PATTERNS_PLOT_STYLE: &str = "Fill Patterns.ctb";
|
||||
pub const SCREENING_100_PLOT_STYLE: &str = "Screening 100%.ctb";
|
||||
pub const SCREENING_75_PLOT_STYLE: &str = "Screening 75%.ctb";
|
||||
pub const SCREENING_50_PLOT_STYLE: &str = "Screening 50%.ctb";
|
||||
pub const SCREENING_25_PLOT_STYLE: &str = "Screening 25%.ctb";
|
||||
|
||||
const STANDARD_PLOT_STYLES: &[&str] = &[
|
||||
GRAYSCALE_PLOT_STYLE,
|
||||
FILL_PATTERNS_PLOT_STYLE,
|
||||
SCREENING_100_PLOT_STYLE,
|
||||
SCREENING_75_PLOT_STYLE,
|
||||
SCREENING_50_PLOT_STYLE,
|
||||
SCREENING_25_PLOT_STYLE,
|
||||
];
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn plot_styles_dir() -> Result<PathBuf, String> {
|
||||
|
|
@ -52,10 +67,14 @@ pub fn available_ctb_names() -> Vec<String> {
|
|||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let Ok(dir) = ensure_plot_styles_dir() else {
|
||||
return vec![DEFAULT_PLOT_STYLE.into(), MONOCHROME_PLOT_STYLE.into()];
|
||||
let mut names = vec![DEFAULT_PLOT_STYLE.into(), MONOCHROME_PLOT_STYLE.into()];
|
||||
names.extend(STANDARD_PLOT_STYLES.iter().map(|name| (*name).to_string()));
|
||||
return names;
|
||||
};
|
||||
let Ok(entries) = std::fs::read_dir(dir) else {
|
||||
return vec![DEFAULT_PLOT_STYLE.into(), MONOCHROME_PLOT_STYLE.into()];
|
||||
let mut names = vec![DEFAULT_PLOT_STYLE.into(), MONOCHROME_PLOT_STYLE.into()];
|
||||
names.extend(STANDARD_PLOT_STYLES.iter().map(|name| (*name).to_string()));
|
||||
return names;
|
||||
};
|
||||
let mut names: Vec<String> = entries
|
||||
.filter_map(Result::ok)
|
||||
|
|
@ -67,13 +86,16 @@ pub fn available_ctb_names() -> Vec<String> {
|
|||
.then(|| entry.file_name().to_string_lossy().into_owned())
|
||||
})
|
||||
.collect();
|
||||
names.extend(STANDARD_PLOT_STYLES.iter().map(|name| (*name).to_string()));
|
||||
names.sort_by_key(|name| name.to_ascii_lowercase());
|
||||
names.dedup_by(|left, right| left.eq_ignore_ascii_case(right));
|
||||
names
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
vec![DEFAULT_PLOT_STYLE.into(), MONOCHROME_PLOT_STYLE.into()]
|
||||
let mut names = vec![DEFAULT_PLOT_STYLE.into(), MONOCHROME_PLOT_STYLE.into()];
|
||||
names.extend(STANDARD_PLOT_STYLES.iter().map(|name| (*name).to_string()));
|
||||
names
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -199,10 +221,44 @@ impl PlotStyleTable {
|
|||
MONOCHROME_PLOT_STYLE,
|
||||
include_bytes!("../../assets/plotstyles/monochrome.ctb"),
|
||||
),
|
||||
"grayscale.ctb" => {
|
||||
let mut table = Self::identity(GRAYSCALE_PLOT_STYLE);
|
||||
for aci in 1..=255u8 {
|
||||
if let Some((r, g, b)) = acadrust::types::aci_to_rgb(aci) {
|
||||
let gray = if aci == 7 {
|
||||
0
|
||||
} else {
|
||||
(0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32)
|
||||
.round() as u8
|
||||
};
|
||||
table.aci_entries[aci as usize].color = Some([gray, gray, gray]);
|
||||
}
|
||||
}
|
||||
Ok(table)
|
||||
}
|
||||
"fill patterns.ctb" => {
|
||||
let mut table = Self::identity(FILL_PATTERNS_PLOT_STYLE);
|
||||
for aci in 1..=9usize {
|
||||
table.aci_entries[aci].fill_style = 63 + aci as u8;
|
||||
}
|
||||
Ok(table)
|
||||
}
|
||||
"screening 100%.ctb" => Ok(Self::screening(SCREENING_100_PLOT_STYLE, 100)),
|
||||
"screening 75%.ctb" => Ok(Self::screening(SCREENING_75_PLOT_STYLE, 75)),
|
||||
"screening 50%.ctb" => Ok(Self::screening(SCREENING_50_PLOT_STYLE, 50)),
|
||||
"screening 25%.ctb" => Ok(Self::screening(SCREENING_25_PLOT_STYLE, 25)),
|
||||
_ => Err(format!("Unknown built-in plot style: {name}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn screening(name: &str, percent: u8) -> Self {
|
||||
let mut table = Self::identity(name);
|
||||
for entry in table.aci_entries.iter_mut().skip(1) {
|
||||
entry.screening = percent;
|
||||
}
|
||||
table
|
||||
}
|
||||
|
||||
/// Load one CTB by file name from the per-user plot styles folder.
|
||||
pub fn load_named(name: &str) -> Result<Self, String> {
|
||||
let path = Path::new(name);
|
||||
|
|
@ -227,9 +283,11 @@ impl PlotStyleTable {
|
|||
.to_string_lossy()
|
||||
.eq_ignore_ascii_case(name)
|
||||
})
|
||||
.map(|entry| entry.path())
|
||||
.ok_or_else(|| format!("Plot style not found: {name}"))?;
|
||||
return Self::load(&matched);
|
||||
.map(|entry| entry.path());
|
||||
return match matched {
|
||||
Some(path) => Self::load(&path),
|
||||
None => Self::builtin(name),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
|
@ -248,9 +306,23 @@ impl PlotStyleTable {
|
|||
/// Returns None if no override (use object color).
|
||||
pub fn resolve_color(&self, aci: u8) -> Option<[f32; 3]> {
|
||||
let entry = self.aci_entries.get(aci as usize)?;
|
||||
entry
|
||||
let mut color = entry
|
||||
.color
|
||||
.map(|[r, g, b]| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0])
|
||||
.map(|[r, g, b]| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0]);
|
||||
if entry.color_policy & 2 != 0 {
|
||||
let rgb = color.or_else(|| {
|
||||
if aci == 7 {
|
||||
Some([0.0; 3])
|
||||
} else {
|
||||
acadrust::types::aci_to_rgb(aci).map(|(r, g, b)| {
|
||||
[r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0]
|
||||
})
|
||||
}
|
||||
})?;
|
||||
let gray = 0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2];
|
||||
color = Some([gray; 3]);
|
||||
}
|
||||
color
|
||||
}
|
||||
|
||||
/// Resolve the effective lineweight in mm for the given ACI index.
|
||||
|
|
@ -260,7 +332,13 @@ impl PlotStyleTable {
|
|||
if matches!(entry.lineweight, 0 | 255) {
|
||||
None
|
||||
} else {
|
||||
self.lineweights.get(entry.lineweight as usize).copied()
|
||||
self.lineweights.get(entry.lineweight as usize).copied().map(|weight| {
|
||||
if self.apply_factor {
|
||||
weight * self.scale_factor.max(0.0)
|
||||
} else {
|
||||
weight
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,11 @@ pub(crate) fn temp_pdf_path(kind: &str) -> std::path::PathBuf {
|
|||
))
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) fn temp_pdf_path(kind: &str) -> std::path::PathBuf {
|
||||
std::path::PathBuf::from(format!("{kind}.pdf"))
|
||||
}
|
||||
|
||||
/// Extra options for a print job. On CUPS (Linux/macOS) these map to `lp`
|
||||
/// flags / `-o` options. On Windows the generated PDF already carries render
|
||||
/// options. Windows queues repeated jobs when more than one copy is requested;
|
||||
|
|
|
|||
|
|
@ -72,8 +72,7 @@ impl Scene {
|
|||
// Remove the Layout object itself.
|
||||
self.document.objects.remove(&layout_handle);
|
||||
|
||||
// Drop the layout's entry from the ACAD_LAYOUT dictionary so it does not
|
||||
// dangle (and so AutoCAD doesn't try to recover a now-missing layout).
|
||||
// Drop the layout's dictionary entry so it does not dangle.
|
||||
let dict_handle = self.document.header.acad_layout_dict_handle;
|
||||
if let Some(ObjectType::Dictionary(d)) = self.document.objects.get_mut(&dict_handle) {
|
||||
d.entries.retain(|(k, _)| k != name);
|
||||
|
|
@ -90,20 +89,6 @@ impl Scene {
|
|||
self.document.block_records.remove(&bn);
|
||||
}
|
||||
|
||||
// Drop any standalone PlotSettings page setup tied to this layout.
|
||||
let ps_handles: Vec<Handle> = self
|
||||
.document
|
||||
.objects
|
||||
.iter()
|
||||
.filter_map(|(h, o)| match o {
|
||||
ObjectType::PlotSettings(ps) if ps.page_name == name => Some(*h),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
for h in ps_handles {
|
||||
self.document.objects.remove(&h);
|
||||
}
|
||||
|
||||
// If the deleted layout was active, fall back to Model space.
|
||||
if self.current_layout == name {
|
||||
self.current_layout = "Model".to_string();
|
||||
|
|
@ -498,9 +483,7 @@ impl Scene {
|
|||
|
||||
/// Reconstruct a `pane_grid` configuration from a set of (tile-index, rect)
|
||||
/// items covering `region`, by recursively finding a full vertical or
|
||||
/// horizontal guillotine cut. AutoCAD tiled configs (and anything pane_grid
|
||||
/// produces) are guillotine layouts, so this round-trips them. A non-guillotine
|
||||
/// set falls back to chaining panes so nothing is lost.
|
||||
/// horizontal guillotine cut. Non-guillotine input falls back to chained panes.
|
||||
fn config_from_rects(
|
||||
items: &[(usize, iced::Rectangle)],
|
||||
region: iced::Rectangle,
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ impl Scene {
|
|||
self.document.header.limit_check = enabled;
|
||||
} else {
|
||||
self.document.header.paper_space_limit_check = enabled;
|
||||
self.persist_current_layout_state();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
200
src/scene/mod.rs
200
src/scene/mod.rs
|
|
@ -1614,6 +1614,15 @@ pub struct Scene {
|
|||
/// movement scanned the whole document for wipeouts and recreated the same
|
||||
/// Arcs, making Paper frame construction CPU-bound on large drawings.
|
||||
paper_sheet_render_cache: RefCell<HashMap<String, PaperSheetRenderCache>>,
|
||||
display_plot_style_cache:
|
||||
RefCell<HashMap<(String, String), Option<Arc<crate::io::plot_style::PlotStyleTable>>>>,
|
||||
styled_wire_cache: RefCell<
|
||||
HashMap<(u64, String), (u64, Arc<Vec<WireModel>>)>,
|
||||
>,
|
||||
styled_hatch_cache:
|
||||
RefCell<HashMap<(u64, usize, usize, String, u32), Arc<Vec<HatchModel>>>>,
|
||||
styled_wire_fill_cache:
|
||||
RefCell<HashMap<(u64, String, u32), Arc<Vec<HatchModel>>>>,
|
||||
/// Per-viewport projected wire cache for paper-space content viewports.
|
||||
/// Stores projected + clipped wires in paper-space coordinates.
|
||||
/// Maps vp_handle → (geometry_epoch, Vec<WireModel>).
|
||||
|
|
@ -1880,6 +1889,10 @@ impl Scene {
|
|||
paper_sheet_cache: RefCell::new(HashMap::default()),
|
||||
paper_viewport_cache: RefCell::new(HashMap::default()),
|
||||
paper_sheet_render_cache: RefCell::new(HashMap::default()),
|
||||
display_plot_style_cache: RefCell::new(HashMap::default()),
|
||||
styled_wire_cache: RefCell::new(HashMap::default()),
|
||||
styled_hatch_cache: RefCell::new(HashMap::default()),
|
||||
styled_wire_fill_cache: RefCell::new(HashMap::default()),
|
||||
paper_projected_cache: RefCell::new(HashMap::default()),
|
||||
current_layout: "Model".to_string(),
|
||||
block_edit_block: None,
|
||||
|
|
@ -2878,7 +2891,9 @@ impl Scene {
|
|||
/// active layout or its background are dropped.
|
||||
pub fn set_current_layout(&mut self, name: String) {
|
||||
if self.current_layout != name {
|
||||
self.persist_current_layout_state();
|
||||
self.current_layout = name;
|
||||
self.load_current_layout_state();
|
||||
self.sync_active_space_to_document();
|
||||
self.recolor_meshes();
|
||||
*self.wire_cache.borrow_mut() = None;
|
||||
|
|
@ -2888,6 +2903,91 @@ impl Scene {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn load_current_layout_state(&mut self) {
|
||||
self.display_plot_style_cache
|
||||
.borrow_mut()
|
||||
.retain(|(layout, _), _| layout != &self.current_layout);
|
||||
self.styled_wire_cache.borrow_mut().clear();
|
||||
self.styled_hatch_cache.borrow_mut().clear();
|
||||
self.styled_wire_fill_cache.borrow_mut().clear();
|
||||
if self.current_layout == "Model" {
|
||||
return;
|
||||
}
|
||||
if let Some((flags, insertion_base, min_extents, max_extents, min_limits, max_limits)) =
|
||||
self.document.objects.values().find_map(|object| {
|
||||
let ObjectType::Layout(layout) = object else {
|
||||
return None;
|
||||
};
|
||||
(layout.name == self.current_layout).then_some((
|
||||
layout.flags,
|
||||
layout.insertion_base,
|
||||
layout.min_extents,
|
||||
layout.max_extents,
|
||||
layout.min_limits,
|
||||
layout.max_limits,
|
||||
))
|
||||
}) {
|
||||
self.document.header.paper_space_linetype_scaling = flags & 1 != 0;
|
||||
self.document.header.paper_space_limit_check = flags & 2 != 0;
|
||||
self.document.header.paper_space_insertion_base = acadrust::types::Vector3::new(
|
||||
insertion_base.0,
|
||||
insertion_base.1,
|
||||
insertion_base.2,
|
||||
);
|
||||
self.document.header.paper_space_extents_min = acadrust::types::Vector3::new(
|
||||
min_extents.0,
|
||||
min_extents.1,
|
||||
min_extents.2,
|
||||
);
|
||||
self.document.header.paper_space_extents_max = acadrust::types::Vector3::new(
|
||||
max_extents.0,
|
||||
max_extents.1,
|
||||
max_extents.2,
|
||||
);
|
||||
self.document.header.paper_space_limits_min =
|
||||
acadrust::types::Vector2::new(min_limits.0, min_limits.1);
|
||||
self.document.header.paper_space_limits_max =
|
||||
acadrust::types::Vector2::new(max_limits.0, max_limits.1);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn persist_current_layout_state(&mut self) {
|
||||
if self.current_layout == "Model" {
|
||||
return;
|
||||
}
|
||||
let header = &self.document.header;
|
||||
let psltscale = header.paper_space_linetype_scaling;
|
||||
let plimcheck = header.paper_space_limit_check;
|
||||
let insertion_base = header.paper_space_insertion_base;
|
||||
let min_extents = header.paper_space_extents_min;
|
||||
let max_extents = header.paper_space_extents_max;
|
||||
let min_limits = header.paper_space_limits_min;
|
||||
let max_limits = header.paper_space_limits_max;
|
||||
for object in self.document.objects.values_mut() {
|
||||
let ObjectType::Layout(layout) = object else {
|
||||
continue;
|
||||
};
|
||||
if layout.name == self.current_layout {
|
||||
layout.flags = if psltscale {
|
||||
layout.flags | 1
|
||||
} else {
|
||||
layout.flags & !1
|
||||
};
|
||||
layout.flags = if plimcheck {
|
||||
layout.flags | 2
|
||||
} else {
|
||||
layout.flags & !2
|
||||
};
|
||||
layout.insertion_base = (insertion_base.x, insertion_base.y, insertion_base.z);
|
||||
layout.min_extents = (min_extents.x, min_extents.y, min_extents.z);
|
||||
layout.max_extents = (max_extents.x, max_extents.y, max_extents.z);
|
||||
layout.min_limits = (min_limits.x, min_limits.y);
|
||||
layout.max_limits = (max_limits.x, max_limits.y);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirror the active space (Model tile-mode vs a paper layout) into the
|
||||
/// document's persisted settings so it round-trips on save: the `$TILEMODE`
|
||||
/// header (`show_model_space`) and the `CTAB` current-tab variable. The
|
||||
|
|
@ -3305,32 +3405,17 @@ impl Scene {
|
|||
Some(wire)
|
||||
}
|
||||
|
||||
/// The effective plot settings for the current layout: a standalone
|
||||
/// PlotSettings page setup if one exists, otherwise the settings embedded in
|
||||
/// the LAYOUT object (paper size, margins, origin, rotation, scale). Loaded
|
||||
/// The fallback preserves rotation, origin and scale from loaded files.
|
||||
/// The effective plot settings embedded in the current layout.
|
||||
pub fn effective_plot_settings(&self) -> Option<acadrust::objects::PlotSettings> {
|
||||
self.plot_settings_for(&self.current_layout)
|
||||
}
|
||||
|
||||
/// Plot settings for a specific layout by name: its standalone
|
||||
/// `PlotSettings` object if one exists, else synthesized from the `Layout`
|
||||
/// object's embedded fields.
|
||||
/// Plot settings embedded in a specific layout.
|
||||
pub fn plot_settings_for(&self, name: &str) -> Option<acadrust::objects::PlotSettings> {
|
||||
use acadrust::objects::{
|
||||
ObjectType, PaperMargin, PlotPaperUnits, PlotRotation, PlotSettings, PlotType,
|
||||
PlotWindow, ScaledType,
|
||||
PlotWindow, ScaledType, ShadePlotMode, ShadePlotResolutionLevel,
|
||||
};
|
||||
if let Some(ps) = self.document.objects.values().find_map(|o| {
|
||||
if let ObjectType::PlotSettings(ps) = o {
|
||||
if ps.page_name.as_str() == name {
|
||||
return Some(ps.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}) {
|
||||
return Some(ps);
|
||||
}
|
||||
self.document.objects.values().find_map(|o| {
|
||||
let ObjectType::Layout(l) = o else {
|
||||
return None;
|
||||
|
|
@ -3338,10 +3423,13 @@ impl Scene {
|
|||
if l.name.as_str() != name {
|
||||
return None;
|
||||
}
|
||||
let mut ps = PlotSettings::new(l.name.clone());
|
||||
let mut ps = PlotSettings::new(l.plot_page_name.clone());
|
||||
ps.printer_name = l.plot_printer_name.clone();
|
||||
ps.paper_width = l.paper_width;
|
||||
ps.paper_height = l.paper_height;
|
||||
ps.paper_size = l.paper_size.clone();
|
||||
ps.plot_view_name = l.plot_view_name.clone();
|
||||
ps.current_style_sheet = l.plot_style_sheet.clone();
|
||||
ps.margins = PaperMargin::new(
|
||||
l.plot_margin_left,
|
||||
l.plot_margin_bottom,
|
||||
|
|
@ -3362,10 +3450,84 @@ impl Scene {
|
|||
ps.scale_type = ScaledType::from_code(l.plot_scale_type);
|
||||
ps.scale_numerator = l.plot_scale_numerator;
|
||||
ps.scale_denominator = l.plot_scale_denominator;
|
||||
ps.flags = l.plot_flags;
|
||||
ps.standard_scale_factor = l.plot_scale_factor;
|
||||
ps.paper_image_origin_x = l.paper_image_origin_x;
|
||||
ps.paper_image_origin_y = l.paper_image_origin_y;
|
||||
ps.shade_plot_mode = ShadePlotMode::from_code(l.shade_plot_mode);
|
||||
ps.shade_plot_resolution =
|
||||
ShadePlotResolutionLevel::from_code(l.shade_plot_resolution);
|
||||
ps.shade_plot_dpi = l.shade_plot_dpi;
|
||||
ps.plot_view_handle = l.plot_view_handle;
|
||||
ps.visual_style_handle = l.visual_style_handle;
|
||||
Some(ps)
|
||||
})
|
||||
}
|
||||
|
||||
/// Replace the plot-settings portion embedded in one layout.
|
||||
pub fn set_layout_plot_settings(
|
||||
&mut self,
|
||||
name: &str,
|
||||
ps: &acadrust::objects::PlotSettings,
|
||||
) -> bool {
|
||||
let Some(layout) = self.document.objects.values_mut().find_map(|object| {
|
||||
let ObjectType::Layout(layout) = object else {
|
||||
return None;
|
||||
};
|
||||
(layout.name == name).then_some(layout)
|
||||
}) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
layout.plot_page_name = ps.page_name.clone();
|
||||
layout.plot_printer_name = ps.printer_name.clone();
|
||||
layout.paper_size = ps.paper_size.clone();
|
||||
layout.plot_view_name = ps.plot_view_name.clone();
|
||||
layout.plot_style_sheet = ps.current_style_sheet.clone();
|
||||
layout.plot_margin_left = ps.margins.left;
|
||||
layout.plot_margin_bottom = ps.margins.bottom;
|
||||
layout.plot_margin_right = ps.margins.right;
|
||||
layout.plot_margin_top = ps.margins.top;
|
||||
layout.paper_width = ps.paper_width;
|
||||
layout.paper_height = ps.paper_height;
|
||||
layout.plot_origin_x = ps.origin_x;
|
||||
layout.plot_origin_y = ps.origin_y;
|
||||
layout.plot_window_min_x = ps.plot_window.lower_left_x;
|
||||
layout.plot_window_min_y = ps.plot_window.lower_left_y;
|
||||
layout.plot_window_max_x = ps.plot_window.upper_right_x;
|
||||
layout.plot_window_max_y = ps.plot_window.upper_right_y;
|
||||
layout.plot_scale_numerator = ps.scale_numerator;
|
||||
layout.plot_scale_denominator = ps.scale_denominator;
|
||||
layout.plot_paper_units = ps.paper_units.to_code();
|
||||
layout.plot_rotation = ps.rotation.to_code();
|
||||
layout.plot_type = ps.plot_type.to_code();
|
||||
layout.plot_scale_type = ps.scale_type.to_code();
|
||||
layout.shade_plot_mode = ps.shade_plot_mode.to_code();
|
||||
layout.shade_plot_resolution = ps.shade_plot_resolution.to_code();
|
||||
layout.shade_plot_dpi = ps.shade_plot_dpi;
|
||||
layout.plot_flags = ps.flags;
|
||||
layout.plot_scale_factor = ps.standard_scale_factor;
|
||||
layout.paper_image_origin_x = ps.paper_image_origin_x;
|
||||
layout.paper_image_origin_y = ps.paper_image_origin_y;
|
||||
layout.plot_view_handle = ps.plot_view_handle;
|
||||
layout.visual_style_handle = ps.visual_style_handle;
|
||||
layout.raw_plot_settings_codes = None;
|
||||
self.display_plot_style_cache
|
||||
.borrow_mut()
|
||||
.retain(|(layout_name, _), _| layout_name != name);
|
||||
self.styled_wire_cache.borrow_mut().clear();
|
||||
self.styled_hatch_cache.borrow_mut().clear();
|
||||
self.styled_wire_fill_cache.borrow_mut().clear();
|
||||
true
|
||||
}
|
||||
|
||||
pub fn invalidate_display_plot_style(&self) {
|
||||
self.display_plot_style_cache.borrow_mut().clear();
|
||||
self.styled_wire_cache.borrow_mut().clear();
|
||||
self.styled_hatch_cache.borrow_mut().clear();
|
||||
self.styled_wire_fill_cache.borrow_mut().clear();
|
||||
}
|
||||
|
||||
/// PlotSettings store the paper size and plot margins in millimetres, but a
|
||||
/// layout's paper space is laid out in its own units — its viewports,
|
||||
/// drawing limits and the sheet the user sees can be in inches, millimetres,
|
||||
|
|
|
|||
|
|
@ -139,6 +139,39 @@ impl GradientKind {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn plot_style_fill_pattern(style: u8) -> Option<HatchPattern> {
|
||||
let family = |angle_deg, dx, dy, dashes: &[f32]| PatFamily {
|
||||
angle_deg,
|
||||
x0: 0.0,
|
||||
y0: 0.0,
|
||||
dx,
|
||||
dy,
|
||||
dashes: dashes.to_vec(),
|
||||
};
|
||||
let pattern = match style {
|
||||
64 => HatchPattern::Solid,
|
||||
65 => HatchPattern::Pattern(vec![
|
||||
family(0.0, 0.0, 2.0, &[2.0, -2.0]),
|
||||
family(90.0, 2.0, 0.0, &[2.0, -2.0]),
|
||||
]),
|
||||
66 => HatchPattern::Pattern(vec![
|
||||
family(0.0, 0.0, 2.0, &[]),
|
||||
family(90.0, 2.0, 0.0, &[]),
|
||||
]),
|
||||
67 => HatchPattern::Pattern(vec![
|
||||
family(45.0, 0.0, 2.0, &[]),
|
||||
family(135.0, 0.0, 2.0, &[]),
|
||||
]),
|
||||
68 => HatchPattern::Pattern(vec![family(0.0, 0.0, 2.0, &[])]),
|
||||
69 => HatchPattern::Pattern(vec![family(135.0, 0.0, 2.0, &[])]),
|
||||
70 => HatchPattern::Pattern(vec![family(45.0, 0.0, 2.0, &[])]),
|
||||
71 => HatchPattern::Pattern(vec![family(0.0, 0.0, 2.0, &[0.0, -2.0])]),
|
||||
72 => HatchPattern::Pattern(vec![family(90.0, 2.0, 0.0, &[])]),
|
||||
_ => return None,
|
||||
};
|
||||
Some(pattern)
|
||||
}
|
||||
|
||||
/// Hatch fill pattern.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum HatchPattern {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ use crate::scene::{
|
|||
SceneLight, Uniforms, ViewportInstance, WireModel,
|
||||
};
|
||||
|
||||
const DISPLAY_STYLE_CACHE_LIMIT: usize = if cfg!(target_arch = "wasm32") { 4 } else { 24 };
|
||||
|
||||
// ── Camera hover state (shader::Program::State) ───────────────────────────
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
|
|
@ -2496,6 +2498,238 @@ impl Scene {
|
|||
};
|
||||
(final_color, pl, pat, lw, aci)
|
||||
}
|
||||
|
||||
fn display_plot_style(&self) -> Option<Arc<crate::io::plot_style::PlotStyleTable>> {
|
||||
if self.current_layout == "Model" {
|
||||
return None;
|
||||
}
|
||||
let settings = self.effective_plot_settings()?;
|
||||
if !settings.flags.show_plot_styles || settings.current_style_sheet.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let key = (
|
||||
self.current_layout.clone(),
|
||||
settings.current_style_sheet.to_ascii_lowercase(),
|
||||
);
|
||||
if let Some(style) = self.display_plot_style_cache.borrow().get(&key) {
|
||||
return style.clone();
|
||||
}
|
||||
let style = crate::io::plot_style::PlotStyleTable::load_named(
|
||||
&settings.current_style_sheet,
|
||||
)
|
||||
.ok()
|
||||
.map(Arc::new);
|
||||
self.display_plot_style_cache
|
||||
.borrow_mut()
|
||||
.insert(key, style.clone());
|
||||
style
|
||||
}
|
||||
|
||||
fn apply_display_plot_style(
|
||||
&self,
|
||||
color: &mut [f32; 4],
|
||||
aci: u8,
|
||||
style: &crate::io::plot_style::PlotStyleTable,
|
||||
) {
|
||||
if aci == 0 {
|
||||
return;
|
||||
}
|
||||
if let Some(rgb) = style.resolve_color(aci) {
|
||||
color[..3].copy_from_slice(&rgb);
|
||||
}
|
||||
let screening = style.resolve_screening(aci);
|
||||
for (channel, paper) in color[..3]
|
||||
.iter_mut()
|
||||
.zip(self.paper_bg_color[..3].iter())
|
||||
{
|
||||
*channel = *channel * screening + *paper * (1.0 - screening);
|
||||
}
|
||||
}
|
||||
|
||||
fn display_styled_wires(
|
||||
&self,
|
||||
source: Arc<Vec<WireModel>>,
|
||||
source_gen: u64,
|
||||
) -> (Arc<Vec<WireModel>>, u64) {
|
||||
let Some(style) = self.display_plot_style() else {
|
||||
return (source, source_gen);
|
||||
};
|
||||
let key = (source_gen, style.name.to_ascii_lowercase());
|
||||
if let Some((gen, wires)) = self.styled_wire_cache.borrow().get(&key) {
|
||||
return (Arc::clone(wires), *gen);
|
||||
}
|
||||
let mut wires = source.as_ref().clone();
|
||||
for wire in &mut wires {
|
||||
self.apply_display_plot_style(&mut wire.color, wire.aci, &style);
|
||||
if wire.aci > 0 {
|
||||
if wire.fill_is_2d_solid
|
||||
&& style
|
||||
.aci_entries
|
||||
.get(wire.aci as usize)
|
||||
.is_some_and(|entry| (65..=72).contains(&entry.fill_style))
|
||||
{
|
||||
wire.fill_tris.clear();
|
||||
wire.fill_tris_low.clear();
|
||||
}
|
||||
if let Some(mm) = style.resolve_lineweight(wire.aci) {
|
||||
wire.line_weight_px = (mm * (96.0 / 25.4) * 2.0).max(1.0);
|
||||
}
|
||||
for vertex in &mut wire.text_verts {
|
||||
self.apply_display_plot_style(&mut vertex.color, wire.aci, &style);
|
||||
}
|
||||
}
|
||||
}
|
||||
let wires = Arc::new(wires);
|
||||
let gen = crate::scene::WIRE_CONTENT_GEN
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let mut cache = self.styled_wire_cache.borrow_mut();
|
||||
if cache.len() >= DISPLAY_STYLE_CACHE_LIMIT {
|
||||
cache.clear();
|
||||
}
|
||||
cache.insert(key, (gen, Arc::clone(&wires)));
|
||||
(wires, gen)
|
||||
}
|
||||
|
||||
fn display_styled_hatches(
|
||||
&self,
|
||||
source: Arc<Vec<HatchModel>>,
|
||||
wire_fills: Option<&Arc<Vec<HatchModel>>>,
|
||||
pattern_scale: f32,
|
||||
) -> Arc<Vec<HatchModel>> {
|
||||
let Some(style) = self.display_plot_style() else {
|
||||
return source;
|
||||
};
|
||||
let key = (
|
||||
self.geometry_epoch,
|
||||
Arc::as_ptr(&source) as usize,
|
||||
wire_fills.map_or(0, |fills| Arc::as_ptr(fills) as usize),
|
||||
style.name.to_ascii_lowercase(),
|
||||
pattern_scale.to_bits(),
|
||||
);
|
||||
if let Some(hatches) = self.styled_hatch_cache.borrow().get(&key) {
|
||||
return Arc::clone(hatches);
|
||||
}
|
||||
let mut hatches = source.as_ref().clone();
|
||||
for hatch in &mut hatches {
|
||||
self.apply_display_plot_style(&mut hatch.color, hatch.aci, &style);
|
||||
if hatch.aci > 0 {
|
||||
if matches!(hatch.pattern, crate::scene::model::hatch_model::HatchPattern::Solid) {
|
||||
if let Some(fill_style) = style
|
||||
.aci_entries
|
||||
.get(hatch.aci as usize)
|
||||
.and_then(|entry| {
|
||||
crate::scene::model::hatch_model::plot_style_fill_pattern(
|
||||
entry.fill_style,
|
||||
)
|
||||
})
|
||||
{
|
||||
hatch.pattern = fill_style;
|
||||
hatch.scale = pattern_scale;
|
||||
}
|
||||
}
|
||||
if let Some(mm) = style.resolve_lineweight(hatch.aci) {
|
||||
hatch.line_weight_px = (mm * (96.0 / 25.4) * 2.0).max(1.0);
|
||||
}
|
||||
if let crate::scene::model::hatch_model::HatchPattern::Gradient {
|
||||
color2, ..
|
||||
} = &mut hatch.pattern
|
||||
{
|
||||
self.apply_display_plot_style(color2, hatch.aci, &style);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(wire_fills) = wire_fills {
|
||||
hatches.extend(wire_fills.iter().cloned());
|
||||
}
|
||||
let hatches = Arc::new(hatches);
|
||||
let mut cache = self.styled_hatch_cache.borrow_mut();
|
||||
cache.retain(|(epoch, _, _, _, _), _| *epoch == self.geometry_epoch);
|
||||
if cache.len() >= DISPLAY_STYLE_CACHE_LIMIT * 2 {
|
||||
cache.clear();
|
||||
}
|
||||
cache.insert(key, Arc::clone(&hatches));
|
||||
hatches
|
||||
}
|
||||
|
||||
fn display_styled_wire_fills(
|
||||
&self,
|
||||
wires: &Arc<Vec<WireModel>>,
|
||||
source_gen: u64,
|
||||
pattern_scale: f32,
|
||||
) -> Option<Arc<Vec<HatchModel>>> {
|
||||
let Some(style) = self.display_plot_style() else {
|
||||
return None;
|
||||
};
|
||||
let key = (
|
||||
source_gen,
|
||||
style.name.to_ascii_lowercase(),
|
||||
pattern_scale.to_bits(),
|
||||
);
|
||||
if let Some(hatches) = self.styled_wire_fill_cache.borrow().get(&key) {
|
||||
return Some(Arc::clone(hatches));
|
||||
}
|
||||
let mut hatches = Vec::new();
|
||||
for wire in wires.iter().filter(|wire| wire.fill_is_2d_solid && wire.aci > 0) {
|
||||
let Some(pattern) = style
|
||||
.aci_entries
|
||||
.get(wire.aci as usize)
|
||||
.and_then(|entry| {
|
||||
(65..=72)
|
||||
.contains(&entry.fill_style)
|
||||
.then(|| {
|
||||
crate::scene::model::hatch_model::plot_style_fill_pattern(
|
||||
entry.fill_style,
|
||||
)
|
||||
})
|
||||
.flatten()
|
||||
})
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let mut color = wire.color;
|
||||
self.apply_display_plot_style(&mut color, wire.aci, &style);
|
||||
let line_weight_px = style
|
||||
.resolve_lineweight(wire.aci)
|
||||
.map(|mm| (mm * (96.0 / 25.4) * 2.0).max(1.0))
|
||||
.unwrap_or(wire.line_weight_px);
|
||||
for (triangle_index, triangle) in wire.fill_tris.chunks_exact(3).enumerate() {
|
||||
let mut boundary = Vec::with_capacity(4);
|
||||
for (point_index, point) in triangle.iter().enumerate() {
|
||||
let index = triangle_index * 3 + point_index;
|
||||
let low = wire.fill_tris_low.get(index).copied().unwrap_or([0.0; 3]);
|
||||
boundary.push([point[0] + low[0], point[1] + low[1]]);
|
||||
}
|
||||
boundary.push(boundary[0]);
|
||||
hatches.push(HatchModel {
|
||||
render_instance: wire.render_instance.clone(),
|
||||
world_origin: [0.0, 0.0],
|
||||
boundary: Arc::new(boundary),
|
||||
boundary_wcs: None,
|
||||
fill_plane: None,
|
||||
fill_plane_boundary: None,
|
||||
boundary_exterior: None,
|
||||
boundary_sources: None,
|
||||
boundary_paths: None,
|
||||
style: acadrust::entities::HatchStyleType::Normal,
|
||||
pattern: pattern.clone(),
|
||||
name: "PLOTSTYLE".to_string(),
|
||||
color,
|
||||
aci: 0,
|
||||
line_weight_px,
|
||||
angle_offset: 0.0,
|
||||
scale: pattern_scale,
|
||||
draw_depth: wire.depth_override.unwrap_or(0.0),
|
||||
});
|
||||
}
|
||||
}
|
||||
let hatches = Arc::new(hatches);
|
||||
let mut cache = self.styled_wire_fill_cache.borrow_mut();
|
||||
if cache.len() >= DISPLAY_STYLE_CACHE_LIMIT {
|
||||
cache.clear();
|
||||
}
|
||||
cache.insert(key, Arc::clone(&hatches));
|
||||
Some(hatches)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether an entity sits on a locked layer (via the document's layer table).
|
||||
|
|
@ -3403,11 +3637,15 @@ impl Scene {
|
|||
} else {
|
||||
self.model_wires_for_viewport_arc(inst.handle, full.height)
|
||||
};
|
||||
let source_gen = self.last_model_wire_gen.get();
|
||||
let styled_fill_source = Arc::clone(&base_arc);
|
||||
let (base_arc, styled_gen) = self.display_styled_wires(base_arc, source_gen);
|
||||
self.last_model_wire_gen.set(styled_gen);
|
||||
// Wire-buffer content id for the upload gate. Preview / interim wires
|
||||
// are NOT part of this buffer anymore (they go in a separate per-frame
|
||||
// overlay buffer below), so the base id is the source's stable content
|
||||
// gen — a drag or camera move never re-uploads the base wire set.
|
||||
let base_wire_content_id = self.last_model_wire_gen.get();
|
||||
let base_wire_content_id = styled_gen;
|
||||
let base_wire_patch = self.model_wire_patch_for(base_wire_content_id);
|
||||
// Split Face3D wires from the rest. The split is content-only (keyed
|
||||
// by the wire-set content id), so while the geometry is unchanged it's
|
||||
|
|
@ -3548,8 +3786,15 @@ impl Scene {
|
|||
width: full.width.max(1.0),
|
||||
height: full.height.max(1.0),
|
||||
};
|
||||
let mut uniforms =
|
||||
Uniforms::new(&inst.camera, full_bounds, self.document.header.lineweight_display);
|
||||
let display_plot_lineweights = self.current_layout != "Model"
|
||||
&& self.effective_plot_settings().is_some_and(|settings| {
|
||||
settings.flags.show_plot_styles && settings.flags.print_lineweights
|
||||
});
|
||||
let mut uniforms = Uniforms::new(
|
||||
&inst.camera,
|
||||
full_bounds,
|
||||
self.document.header.lineweight_display || display_plot_lineweights,
|
||||
);
|
||||
if self.document.header.paper_space_linetype_scaling
|
||||
&& !inst.paper_sheet
|
||||
&& inst.tile_idx.is_none()
|
||||
|
|
@ -3642,6 +3887,29 @@ impl Scene {
|
|||
None,
|
||||
)
|
||||
};
|
||||
let viewport_scale = if inst.paper_sheet {
|
||||
1.0
|
||||
} else {
|
||||
self.document
|
||||
.get_entity(inst.handle)
|
||||
.and_then(|entity| match entity {
|
||||
EntityType::Viewport(viewport) => Some(vp_effective_scale(
|
||||
viewport.custom_scale,
|
||||
viewport.view_height,
|
||||
viewport.height,
|
||||
)),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(1.0)
|
||||
};
|
||||
let pattern_scale = (self.paper_space_unit_factor() / viewport_scale.max(1.0e-9)) as f32;
|
||||
let styled_wire_fills =
|
||||
self.display_styled_wire_fills(&styled_fill_source, source_gen, pattern_scale);
|
||||
let hatches = self.display_styled_hatches(
|
||||
hatches,
|
||||
styled_wire_fills.as_ref().filter(|fills| !fills.is_empty()),
|
||||
pattern_scale,
|
||||
);
|
||||
let images = if let Some(images) = paper_images {
|
||||
images
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ pub enum PlotFlag {
|
|||
FitToPaper,
|
||||
Center,
|
||||
ScaleLw,
|
||||
PlotStyles,
|
||||
DisplayStyles,
|
||||
UpsideDown,
|
||||
Lineweights,
|
||||
Transparency,
|
||||
|
|
@ -132,6 +134,10 @@ pub struct PlotDialogState {
|
|||
/// Output goes to a PDF file instead of a printer.
|
||||
pub to_file: bool,
|
||||
pub paper: String,
|
||||
#[serde(skip)]
|
||||
pub paper_width_mm: f64,
|
||||
#[serde(skip)]
|
||||
pub paper_height_mm: f64,
|
||||
pub orientation: String,
|
||||
pub upside_down: bool,
|
||||
pub copies: String,
|
||||
|
|
@ -144,6 +150,8 @@ pub struct PlotDialogState {
|
|||
pub fit_to_paper: bool,
|
||||
#[serde(skip)]
|
||||
pub scales: Vec<(String, f64)>,
|
||||
#[serde(skip)]
|
||||
pub plot_views: Vec<String>,
|
||||
pub scale_lw: bool,
|
||||
pub quality: String,
|
||||
pub shade: String,
|
||||
|
|
@ -155,6 +163,8 @@ pub struct PlotDialogState {
|
|||
pub stamp: bool,
|
||||
/// Display name of the active plot style table ("" = none).
|
||||
pub style_name: String,
|
||||
pub apply_plot_styles: bool,
|
||||
pub show_plot_styles: bool,
|
||||
/// CTB file names discovered in the per-user plot styles folder.
|
||||
#[serde(skip)]
|
||||
pub plot_styles: Vec<String>,
|
||||
|
|
@ -185,6 +195,8 @@ impl Default for PlotDialogState {
|
|||
printer: None,
|
||||
to_file: false,
|
||||
paper: "A4".into(),
|
||||
paper_width_mm: 297.0,
|
||||
paper_height_mm: 210.0,
|
||||
orientation: "Landscape".into(),
|
||||
upside_down: false,
|
||||
copies: "1".into(),
|
||||
|
|
@ -195,6 +207,7 @@ impl Default for PlotDialogState {
|
|||
scale: "1:1".into(),
|
||||
fit_to_paper: true,
|
||||
scales: Vec::new(),
|
||||
plot_views: Vec::new(),
|
||||
scale_lw: false,
|
||||
quality: "Normal".into(),
|
||||
shade: "As displayed".into(),
|
||||
|
|
@ -205,6 +218,8 @@ impl Default for PlotDialogState {
|
|||
paperspace_last: false,
|
||||
stamp: false,
|
||||
style_name: String::new(),
|
||||
apply_plot_styles: true,
|
||||
show_plot_styles: false,
|
||||
plot_styles: Vec::new(),
|
||||
style_missing: false,
|
||||
page_setups: Vec::new(),
|
||||
|
|
@ -224,6 +239,8 @@ impl PlotDialogState {
|
|||
self.printer = o.printer.clone();
|
||||
self.to_file = o.to_file;
|
||||
self.paper = o.paper.clone();
|
||||
self.paper_width_mm = o.paper_width_mm;
|
||||
self.paper_height_mm = o.paper_height_mm;
|
||||
self.orientation = o.orientation.clone();
|
||||
self.upside_down = o.upside_down;
|
||||
self.copies = o.copies.clone();
|
||||
|
|
@ -243,6 +260,8 @@ impl PlotDialogState {
|
|||
self.paperspace_last = o.paperspace_last;
|
||||
self.stamp = o.stamp;
|
||||
self.style_name = o.style_name.clone();
|
||||
self.apply_plot_styles = o.apply_plot_styles;
|
||||
self.show_plot_styles = o.show_plot_styles;
|
||||
self.style_missing = o.style_missing;
|
||||
}
|
||||
|
||||
|
|
@ -622,7 +641,10 @@ pub fn view_window(
|
|||
None => PlotChoice::localized(OUT_DEFAULT),
|
||||
})
|
||||
};
|
||||
let paper_opts: Vec<String> = PaperSize::ALL.iter().map(|p| p.label().to_string()).collect();
|
||||
let mut paper_opts: Vec<String> = PaperSize::ALL.iter().map(|p| p.label().to_string()).collect();
|
||||
if !paper_opts.iter().any(|name| name == &s.paper) {
|
||||
paper_opts.push(s.paper.clone());
|
||||
}
|
||||
let paper_note: Element<'_, Message> = if s.area == "Layout" {
|
||||
text(t!("Layout plots the current sheet using the selected paper size."))
|
||||
.size(10)
|
||||
|
|
@ -685,11 +707,18 @@ pub fn view_window(
|
|||
let mut area_options = if print_all_options {
|
||||
choices(&["Layout"])
|
||||
} else {
|
||||
choices(&["Extents", "Display", "Window"])
|
||||
choices(&["Extents", "Limits", "Display", "Window"])
|
||||
};
|
||||
if s.paper_space && !print_all_options {
|
||||
area_options.insert(0, PlotChoice::localized("Layout"));
|
||||
}
|
||||
if !print_all_options {
|
||||
area_options.extend(
|
||||
s.plot_views
|
||||
.iter()
|
||||
.map(|name| PlotChoice::raw(format!("View: {name}"))),
|
||||
);
|
||||
}
|
||||
let mut area_row = row![
|
||||
text(t!("What to plot")).size(11).style(muted_style).width(92),
|
||||
iced::widget::pick_list(
|
||||
|
|
@ -779,6 +808,18 @@ pub fn view_window(
|
|||
PlotDlgMsg::Style,
|
||||
width,
|
||||
),
|
||||
check_enabled(
|
||||
t!("Plot with plot styles"),
|
||||
s.apply_plot_styles,
|
||||
PlotFlag::PlotStyles,
|
||||
!s.style_name.is_empty(),
|
||||
),
|
||||
check_enabled(
|
||||
t!("Display plot styles"),
|
||||
s.show_plot_styles,
|
||||
PlotFlag::DisplayStyles,
|
||||
s.paper_space && !s.style_name.is_empty(),
|
||||
),
|
||||
row![
|
||||
button(text(t!("Load…")).size(11))
|
||||
.on_press(Message::PlotDlg(PlotDlgMsg::LoadStyle))
|
||||
|
|
|
|||
Loading…
Reference in a new issue