Merge main into codex/graphic-attributes-cleanup

Conflicts were additions on both sides, kept together: the Count palette
and Sheet Set toggles next to the Graphic Attributes toggle in the ribbon,
main's new locale groups and catalog entries next to graphic-attributes,
and module declarations. The palette code follows main's crate renames
(acadrust -> codec, cadkernel -> kernel).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VgkJN8DEojuPTumAXLHotu
This commit is contained in:
Claude committed 2026-10-07 20:11:05 +00:00
commit 7a03e1c43c
625 files changed
+104397 -28053

No files matched your search

+104
View File
@@ -44,6 +44,39 @@ fn info_card<'a>(
.into()
}
/// A full-width label/value line. CPU and GPU names are too long for the
/// fixed-width cards above.
fn info_row<'a>(
label: std::borrow::Cow<'static, str>,
value: impl iced::widget::text::IntoFragment<'a>,
width: Length,
) -> Element<'a, Message> {
container(
row![
text(label).size(11).style(muted_style).width(Length::Fixed(110.0)),
text(value).size(13).width(Fill),
]
.spacing(8)
.align_y(iced::Center),
)
.padding([8, 12])
.width(width)
.style(surface_style)
.into()
}
/// `Adapter (backend)` for the GPU row.
fn gpu_summary() -> String {
#[cfg(target_arch = "wasm32")]
{
"WebGPU / WebGL".to_string()
}
#[cfg(not(target_arch = "wasm32"))]
{
crate::gpu_backend::active_gpu_summary()
}
}
pub(crate) fn platform_name() -> &'static str {
#[cfg(target_arch = "wasm32")]
{
@@ -69,6 +102,63 @@ pub(crate) fn architecture_name() -> &'static str {
}
}
/// The text `Copy Info` puts on the clipboard: everything a bug report needs
/// about the build and the machine, including which graphics backend the
/// resolver picked and why.
pub(crate) fn report() -> String {
use crate::sysinfo::{format_bytes, system_info};
let sys = system_info();
#[allow(unused_mut)]
let mut out = format!(
"Open CAD Studio v{}\nRevision: {}\nCommit date: {}\nProfile: {}\nFeatures: {}\n\n\
[System]\nOS: {} ({} {})\nCPU: {}\nLogical processors: {}\nRAM: {} total, {} available\n",
env!("OCS_FULL_VERSION"),
env!("OCS_GIT_REV"),
env!("OCS_COMMIT_DATE"),
env!("OCS_BUILD_PROFILE"),
env!("OCS_BUILD_FEATURES"),
sys.os,
platform_name(),
architecture_name(),
sys.cpu,
sys.logical_cores,
format_bytes(sys.ram_total),
format_bytes(sys.ram_available),
);
#[cfg(not(target_arch = "wasm32"))]
{
use crate::gpu_backend::{active_gpu, load_prefs};
let gpu = active_gpu();
let prefs = load_prefs();
let or_unknown = |s: &str| {
if s.is_empty() {
"Unknown".to_string()
} else {
s.to_string()
}
};
out.push_str(&format!(
"\n[Graphics]\nGPU: {}\nDriver: {}\nBackend: {}\nSelected by: {}\n\
Below WebGPU baseline: {}\nWGPU_BACKEND: {}\n\
Saved options: backend={}, compat renderer={}, OpenGL on older GPUs={}\n",
or_unknown(gpu.adapter.as_deref().unwrap_or("")),
or_unknown(&gpu.driver),
or_unknown(gpu.backend.as_deref().unwrap_or("")),
or_unknown(&gpu.origin),
if gpu.legacy { "yes" } else { "no" },
std::env::var("WGPU_BACKEND").unwrap_or_else(|_| "(unset)".into()),
prefs.backend.as_str().unwrap_or("auto"),
prefs.compat_renderer,
prefs.legacy_gl,
));
}
#[cfg(target_arch = "wasm32")]
{
out.push_str("\n[Graphics]\nBackend: WebGPU / WebGL\n");
}
out
}
/// Build card text: the metadata suffix without its leading `+`
/// (`194.gef189d77`, `gef189d77.dirty`), or "Release" for a clean build on
/// the release tag.
@@ -137,6 +227,19 @@ pub fn view_window(
.spacing(8)
.width(content_width);
let row_width = if matches!(sizing.width, Length::Fill) {
Fill
} else {
Length::Fixed(148.0 * 3.0 + 16.0)
};
let system = column![
info_row(t!("CPU"), crate::sysinfo::cpu_summary(), row_width),
info_row(t!("RAM"), crate::sysinfo::ram_summary(), row_width),
info_row(t!("GPU (backend)"), gpu_summary(), row_width),
]
.spacing(6)
.width(content_width);
let copy = dialog_button(t!("Copy Info"), Message::AboutCopyInfo, true);
container(
@@ -144,6 +247,7 @@ pub fn view_window(
hero,
metadata,
build_info,
system,
row![Space::new().width(content_width), copy]
.width(sizing.width)
.align_y(iced::Center),
+2 -2
View File
@@ -218,7 +218,7 @@ pub fn view_window<'a>(
.on_press(Message::AliasEditorApply)
.padding([4, 16])
.style(button::primary);
let apply_exit = button(text(t!("Apply && Exit")).size(12))
let apply_exit = button(text(t!("Apply & Exit")).size(12))
.on_press(Message::AliasEditorApplyExit)
.padding([4, 16])
.style(button::primary);
@@ -307,7 +307,7 @@ pub fn view_window<'a>(
text(t!("Unsaved changes will be discarded.")).size(14),
Space::new().height(10),
row![
button(text(t!("Discard && close")).size(12))
button(text(t!("Discard & close")).size(12))
.on_press(Message::AliasEditorCloseDiscard)
.padding([4, 12])
.style(button::danger),
+1 -1
View File
@@ -7,7 +7,7 @@
use std::collections::BTreeSet;
use std::fmt;
use acadrust::types::Handle;
use codec::types::Handle;
use iced::widget::{button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Space};
use iced::{Border, Element, Fill, Length, Theme};
+420
View File
@@ -0,0 +1,420 @@
//! Attribute Definition (ATTDEF) and Edit Attribute Definition dialogs:
//! cards with accent headings, a help button on the left and the named
//! action on the right, like the other drawing dialogs.
use iced::widget::{button, checkbox, column, container, pick_list, row, text, text_input, Space};
use iced::{Element, Fill, Length};
use crate::app::Message;
use crate::modules::draw::draw::attdef::{
Justify, AFLAG_CONSTANT, AFLAG_INVISIBLE, AFLAG_LOCK, AFLAG_MULTILINE, AFLAG_PRESET,
AFLAG_VERIFY,
};
use crate::t;
use crate::ui::style::common::muted_style;
use crate::ui::style::form::{button_style, dialog_button, field_style};
use crate::ui::window::pdf_dialogs::{accent_text, card_style};
/// One edit in either attribute definition dialog.
#[derive(Debug, Clone)]
pub enum AttdefDialogMsg {
Mode(u8, bool),
Annotative(bool),
Tag(String),
Prompt(String),
Default(String),
Justify(JustifyChoice),
Style(String),
Height(String),
Rotation(String),
Width(String),
OnScreen(bool),
Coord(usize, String),
AlignBelow(bool),
PickHeight,
PickRotation,
PickWidth,
/// The ƒ button: pick a field for the Default value.
InsertField,
EditInsertField,
/// Edit the default value in the multi-line text editor.
EditValue,
DismissError,
Ok,
Help,
EditTag(String),
EditPrompt(String),
EditDefault(String),
EditOk,
}
/// A justification as the dialog list shows it (translated label).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct JustifyChoice(pub Justify);
impl std::fmt::Display for JustifyChoice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&t!(self.0.label()))
}
}
/// The ATTDEF dialog's fields.
#[derive(Debug, Clone)]
pub struct AttdefDialogState {
pub aflags: u8,
pub annotative: bool,
pub tag: String,
pub prompt: String,
pub default: String,
pub justify: Justify,
pub style: String,
pub styles: Vec<String>,
pub height: String,
pub rotation: String,
pub width: String,
pub on_screen: bool,
pub coords: [String; 3],
pub align_below: bool,
/// A definition was placed before in this session.
pub can_align_below: bool,
/// Validation message shown above the buttons.
pub error: Option<String>,
/// The field the Default value shows, when one was inserted.
pub field: Option<(String, Vec<codec::Handle>)>,
}
/// The Edit Attribute Definition dialog's fields.
#[derive(Debug, Clone)]
pub struct AttdefEditState {
pub handle: codec::Handle,
pub tag: String,
pub prompt: String,
pub default: String,
pub constant: bool,
pub error: Option<String>,
pub field: Option<(String, Vec<codec::Handle>)>,
/// The Default was retyped or a field inserted: the stored field is
/// replaced on Apply.
pub field_changed: bool,
}
fn msg(m: AttdefDialogMsg) -> Message {
Message::AttdefDialog(m)
}
fn card<'a>(title: String, content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
container(column![text(title.to_uppercase()).size(10).style(accent_text), content.into()].spacing(8))
.padding([10, 12])
.width(Fill)
.style(card_style)
.into()
}
fn labelled<'a>(label: String, field: Element<'a, Message>, enabled: bool) -> Element<'a, Message> {
let label = text(label).size(12).width(Length::Fixed(118.0));
let label = if enabled { label } else { label.style(muted_style) };
row![label, field].spacing(8).align_y(iced::Center).into()
}
fn input<'a>(value: &str, enabled: bool, on: fn(String) -> AttdefDialogMsg) -> Element<'a, Message> {
let mut field = text_input("", value).size(12).padding([5, 8]).width(Fill).style(field_style);
if enabled {
field = field.on_input(move |v| msg(on(v)));
}
field.into()
}
/// The small button beside a field that takes the value from the drawing.
fn pick<'a>(
face: Element<'a, Message>,
tip: String,
message: Option<AttdefDialogMsg>,
) -> Element<'a, Message> {
let b = button(container(face).center_x(Fill))
.style(button_style(false))
.padding([4, 0])
.width(Length::Fixed(28.0));
let b = match message {
Some(m) => b.on_press(msg(m)),
None => b,
};
iced::widget::tooltip(b, text(tip).size(11), iced::widget::tooltip::Position::Bottom).into()
}
fn mode<'a>(label: String, on: bool, bit: u8) -> Element<'a, Message> {
checkbox(on)
.label(label)
.text_size(12)
.size(14)
.on_toggle(move |v| msg(AttdefDialogMsg::Mode(bit, v)))
.into()
}
/// The validation message band, as in the Write Block dialog.
fn error_band<'a>(error: &Option<String>) -> Option<Element<'a, Message>> {
let error = error.clone()?;
Some(
container(
row![
crate::ui::icons::semantic(ICON_WARN, 14.0),
text(error).size(11),
Space::new().width(Fill),
button(crate::ui::icons::themed(crate::ui::icons::CLOSE, 10.0))
.on_press(msg(AttdefDialogMsg::DismissError))
.style(button_style(false))
.padding([1, 6]),
]
.spacing(8)
.align_y(iced::Center),
)
.padding([6, 10])
.width(Fill)
.style(|theme: &iced::Theme| container::Style {
background: Some(iced::Background::Color(theme.palette().danger.base.color.scale_alpha(0.18))),
border: iced::Border { radius: 4.0.into(), ..Default::default() },
..Default::default()
})
.into(),
)
}
/// The face of the buttons that take a value from the drawing.
fn screen_pick<'a>() -> Element<'a, Message> {
crate::ui::icons::themed(crate::ui::icons::SNAP, 14.0)
}
static ICON_WARN: &[u8] = include_bytes!("../../../assets/icons/ui/warning_triangle.svg");
fn footer<'a>(action: std::borrow::Cow<'static, str>, ok: AttdefDialogMsg) -> Element<'a, Message> {
row![
button(text("?").size(12))
.on_press(msg(AttdefDialogMsg::Help))
.style(button_style(false))
.padding([5, 11]),
Space::new().width(Fill),
dialog_button(t!("Cancel"), Message::CloseModal, false),
dialog_button(action, msg(ok), true),
]
.spacing(6)
.align_y(iced::Center)
.into()
}
pub fn view<'a>(state: &'a AttdefDialogState, sizing: crate::ui::modal::ModalSizing) -> Element<'a, Message> {
let f = state.aflags;
let constant = f & AFLAG_CONSTANT != 0;
let multiline = f & AFLAG_MULTILINE != 0;
let modes = card(
t!("Mode").into_owned(),
column![
mode(t!("Invisible").into_owned(), f & AFLAG_INVISIBLE != 0, AFLAG_INVISIBLE),
mode(t!("Constant").into_owned(), constant, AFLAG_CONSTANT),
mode(t!("Verify").into_owned(), f & AFLAG_VERIFY != 0, AFLAG_VERIFY),
mode(t!("Preset").into_owned(), f & AFLAG_PRESET != 0, AFLAG_PRESET),
mode(t!("Lock position").into_owned(), f & AFLAG_LOCK != 0, AFLAG_LOCK),
mode(t!("Multiple lines").into_owned(), multiline, AFLAG_MULTILINE),
]
.spacing(7),
);
let coords_on = !state.on_screen && !state.align_below;
let coord = |i: usize, axis: &'static str| -> Element<'a, Message> {
let mut field = text_input("", &state.coords[i]).size(12).padding([5, 8]).width(Fill).style(field_style);
if coords_on {
field = field.on_input(move |v| msg(AttdefDialogMsg::Coord(i, v)));
}
let label = text(axis).size(12).width(Length::Fixed(14.0));
let label = if coords_on { label } else { label.style(muted_style) };
row![label, field].spacing(8).align_y(iced::Center).into()
};
let mut on_screen = checkbox(state.on_screen)
.label(t!("Specify on-screen").into_owned())
.text_size(12)
.size(14);
if !state.align_below {
on_screen = on_screen.on_toggle(|v| msg(AttdefDialogMsg::OnScreen(v)));
}
let insertion = card(
t!("Insertion Point").into_owned(),
column![on_screen, coord(0, "X"), coord(1, "Y"), coord(2, "Z")].spacing(7),
);
let attribute = card(
t!("Attribute").into_owned(),
column![
labelled(t!("Tag").into_owned(), input(&state.tag, true, AttdefDialogMsg::Tag), true),
labelled(
t!("Prompt").into_owned(),
input(&state.prompt, !constant, AttdefDialogMsg::Prompt),
!constant
),
labelled(
if constant { t!("Value") } else { t!("Default") }.into_owned(),
{
let mut value = row![input(&state.default, true, AttdefDialogMsg::Default)].spacing(6);
if multiline {
value = value.push(pick(
text("...").size(13).into(),
t!("Multiline editor").into_owned(),
Some(AttdefDialogMsg::EditValue),
));
}
value.push(pick(text("fx").size(12).into(), t!("Insert field").into_owned(), Some(AttdefDialogMsg::InsertField)))
}
.spacing(6)
.into(),
true
),
]
.spacing(8),
);
let text_on = !state.align_below;
let justify: Element<'a, Message> = if text_on && !multiline {
pick_list(
Some(JustifyChoice(state.justify)),
Justify::ALL.map(JustifyChoice).to_vec(),
|c: &JustifyChoice| c.to_string(),
)
.on_select(|c| msg(AttdefDialogMsg::Justify(c)))
.text_size(12)
.padding([5, 8])
.width(Fill)
.into()
} else {
container(text(JustifyChoice(state.justify).to_string()).size(12).style(muted_style))
.padding([5, 8])
.width(Fill)
.into()
};
let style: Element<'a, Message> = if text_on {
pick_list(Some(state.style.clone()), state.styles.clone(), |s: &String| s.clone())
.on_select(|s| msg(AttdefDialogMsg::Style(s)))
.text_size(12)
.padding([5, 8])
.width(Fill)
.into()
} else {
container(text(state.style.clone()).size(12).style(muted_style)).padding([5, 8]).width(Fill).into()
};
let mut annotative = checkbox(state.annotative).label(t!("Annotative").into_owned()).text_size(12).size(14);
if text_on {
annotative = annotative.on_toggle(|v| msg(AttdefDialogMsg::Annotative(v)));
}
let size_on = text_on && state.justify != Justify::Align;
let rot_on = text_on && !state.justify.two_point();
let settings = card(
t!("Text Settings").into_owned(),
column![
labelled(t!("Justification").into_owned(), justify, text_on && !multiline),
labelled(t!("Text style").into_owned(), style, text_on),
row![Space::new().width(Length::Fixed(126.0)), annotative],
labelled(
t!("Text height").into_owned(),
row![
input(&state.height, size_on, AttdefDialogMsg::Height),
pick(screen_pick(), t!("Specify on-screen").into_owned(), size_on.then_some(AttdefDialogMsg::PickHeight)),
]
.spacing(6)
.into(),
size_on
),
labelled(
t!("Rotation").into_owned(),
row![
input(&state.rotation, rot_on, AttdefDialogMsg::Rotation),
pick(screen_pick(), t!("Specify on-screen").into_owned(), rot_on.then_some(AttdefDialogMsg::PickRotation)),
]
.spacing(6)
.into(),
rot_on
),
labelled(
t!("Boundary width").into_owned(),
row![
input(&state.width, text_on && multiline, AttdefDialogMsg::Width),
pick(
screen_pick(),
t!("Specify on-screen").into_owned(),
(text_on && multiline).then_some(AttdefDialogMsg::PickWidth)
),
]
.spacing(6)
.into(),
text_on && multiline
),
]
.spacing(8),
);
let mut align = checkbox(state.align_below)
.label(t!("Align below previous attribute definition").into_owned())
.text_size(12)
.size(14);
if state.can_align_below {
align = align.on_toggle(|v| msg(AttdefDialogMsg::AlignBelow(v)));
}
let body = row![
column![modes, insertion].spacing(10).width(Length::Fixed(200.0)),
column![attribute, settings].spacing(10).width(Fill),
]
.spacing(10);
let mut content = column![body, align];
if let Some(band) = error_band(&state.error) {
content = content.push(band);
}
content
.push(footer(t!("Define"), AttdefDialogMsg::Ok))
.spacing(10)
.padding([10, 12])
.width(sizing.width)
.into()
}
pub fn view_edit<'a>(state: &'a AttdefEditState, sizing: crate::ui::modal::ModalSizing) -> Element<'a, Message> {
let field = |label: String, value: &'a str, enabled: bool, on: fn(String) -> AttdefDialogMsg| {
let mut input = text_input("", value).size(12).padding([5, 8]).width(Fill).style(field_style);
if enabled {
input = input.on_input(move |v| msg(on(v)));
}
let label = text(label).size(12).width(Length::Fixed(80.0));
let label = if enabled { label } else { label.style(muted_style) };
row![label, input].spacing(8).align_y(iced::Center)
};
let content = card(
t!("Attribute Definition").into_owned(),
column![
field(t!("Tag").into_owned(), &state.tag, true, AttdefDialogMsg::EditTag),
field(t!("Prompt").into_owned(), &state.prompt, !state.constant, AttdefDialogMsg::EditPrompt),
row![
field(
if state.constant { t!("Value") } else { t!("Default") }.into_owned(),
&state.default,
true,
AttdefDialogMsg::EditDefault
),
pick(text("fx").size(12).into(), t!("Insert field").into_owned(), Some(AttdefDialogMsg::EditInsertField)),
]
.spacing(6)
.align_y(iced::Center),
]
.spacing(8),
);
let mut content = column![content];
if let Some(band) = error_band(&state.error) {
content = content.push(band);
}
content
.push(footer(t!("Apply"), AttdefDialogMsg::EditOk))
.spacing(10)
.padding([10, 12])
.width(sizing.width)
.into()
}
/// The tag messages the reference shows for the dialogs.
pub fn tag_error(tag: &str) -> Option<&'static str> {
if tag.trim().is_empty() {
Some("The tag cannot be empty.")
} else if tag.trim().contains(char::is_whitespace) {
Some("The tag may not contain spaces.")
} else {
None
}
}
+2 -2
View File
@@ -14,8 +14,8 @@
//! edits, undo and repaint live in the update handler (`Message::AttrEditorApply`).
use crate::app::Message;
use acadrust::entities::{HorizontalAlignment, VerticalAlignment};
use acadrust::types::{Color as AcadColor, LineWeight};
use codec::entities::{HorizontalAlignment, VerticalAlignment};
use codec::types::{Color as AcadColor, LineWeight};
use iced::widget::{
button, checkbox, column, container, row, scrollable, text, text_input, Space,
};
+1 -1
View File
@@ -3,7 +3,7 @@
use std::fmt;
use acadrust::Handle;
use codec::Handle;
use iced::widget::{
button, checkbox, column, combo_box, container, mouse_area, pick_list, row, stack, text,
text_editor, text_input, Space,
File diff suppressed because it is too large. Load diff
+8 -66
View File
@@ -16,11 +16,11 @@
//! table of contents, and it is labelled as one.
use crate::app::Message;
use crate::ui::dock::{DockMsg, PanelId};
use acadrust::objects::SolidHistoryOperation;
use acadrust::{CadDocument, EntityType, Handle};
use iced::widget::{button, column, container, mouse_area, row, scrollable, text, tooltip};
use iced::{Background, Border, Element, Fill, Length, Theme};
use crate::ui::dock::PanelId;
use codec::objects::SolidHistoryOperation;
use codec::{CadDocument, EntityType, Handle};
use iced::widget::{button, column, container, scrollable, text};
use iced::{Background, Element, Fill, Theme};
/// The three world planes, with the argument `CREATESKETCH` takes for each.
const ORIGIN_PLANES: [(&str, &str); 3] = [("XY", "XY"), ("XZ", "XZ"), ("YZ", "YZ")];
@@ -99,54 +99,8 @@ pub fn view<'a>(
width: f32,
auto_collapse: bool,
) -> Element<'a, Message> {
// ── Dock chrome (title, pin, close) — matches the other panels ────────
let pin_icon = if auto_collapse {
crate::ui::icons::themed_primary_weak_text(crate::ui::icons::PIN, 12.0)
} else {
crate::ui::icons::themed_secondary(crate::ui::icons::PIN, 12.0)
};
let pin = button(pin_icon)
.on_press(Message::Dock(DockMsg::AutoCollapseToggle(PanelId::Browser)))
.style(move |theme: &Theme, status| {
let mut style = button::subtle(theme, status);
if auto_collapse {
let palette = theme.palette();
style.background = Some(Background::Color(palette.primary.weak.color));
style.text_color = palette.primary.weak.text;
style.border.color = palette.primary.base.color;
style.border.width = 1.0;
}
style
})
.padding([3, 5]);
let pin = tooltip(pin, text(crate::t!("Auto")).size(10), tooltip::Position::Bottom).gap(4);
let close = button(crate::ui::icons::themed_secondary(crate::ui::icons::CLOSE, 12.0))
.on_press(Message::Dock(DockMsg::Close(PanelId::Browser)))
.style(button::subtle)
.padding([3, 5]);
let close = tooltip(close, text(crate::t!("Close")).size(10), tooltip::Position::Bottom).gap(4);
let title_bar = mouse_area(
container(
row![
text(crate::t!("Browser")).size(12),
iced::widget::Space::new().width(Fill),
pin,
close,
]
.spacing(3)
.align_y(iced::Center),
)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
..Default::default()
})
.width(Fill)
.padding([3, 6]),
)
.on_press(Message::Dock(DockMsg::DockGrab(PanelId::Browser)))
.interaction(iced::mouse::Interaction::Grab);
let title_bar =
crate::ui::dock::title_bar(PanelId::Browser, crate::t!("Browser").into_owned(), auto_collapse);
let mut tree = column![].spacing(1);
@@ -189,17 +143,5 @@ pub fn view<'a>(
.width(Fill)
.height(Fill);
container(column![title_bar, body].spacing(6).padding(6))
.width(Length::Fixed(width))
.height(Fill)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.base.color)),
border: Border {
color: theme.palette().background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
..Default::default()
})
.into()
crate::ui::dock::frame(column![title_bar, body].spacing(6), width)
}
+675
View File
@@ -0,0 +1,675 @@
//! The Count palette (COUNTLIST) and count mode (COUNT): the drawing's
//! blocks with how many references of each are counted, and — once a block
//! is chosen — its counted references coloured in the drawing with a
//! floating toolbar above it. Counting itself is `codec::count`.
use crate::app::Message;
use crate::ui::dock::PanelId;
use crate::ui::style::common::muted_style;
use crate::ui::style::form::{button_style, field_style};
use codec::count::{BlockInstance, CountKey};
use codec::{CadDocument, Handle};
use iced::widget::{button, checkbox, column, container, mouse_area, row, scrollable, text, text_input, tooltip, Space};
use iced::{Background, Border, Color, Element, Fill, Length, Theme};
const BLOCK_ICON: &[u8] = include_bytes!("../../../assets/icons/blocks/block.svg");
const AREA_ICON: &[u8] = include_bytes!("../../../assets/icons/shapes/rect.svg");
const SELECT_ICON: &[u8] = include_bytes!("../../../assets/icons/blocks/select_objects.svg");
const TABLE_ICON: &[u8] = include_bytes!("../../../assets/icons/table.svg");
const WARNING_ICON: &[u8] = include_bytes!("../../../assets/icons/ui/warning_triangle.svg");
const CHECK_ICON: &[u8] = include_bytes!("../../../assets/icons/ui/check.svg");
/// The name of the layer count areas are drawn on.
pub const AREA_LAYER: &str = "0-CountArea";
/// What a count counts.
#[derive(Debug, Clone, PartialEq)]
pub enum CountTarget {
/// The references of a block. `reference` is the instance the match
/// options compare with; `picked` when it came from a COUNT selection.
Block {
name: String,
reference: Option<Handle>,
matching: [bool; 3],
picked: bool,
},
/// Picked objects (several, or one that is not a reference): the copies
/// of the group are counted.
Group(Vec<Handle>),
}
/// Count mode of a drawing.
#[derive(Debug, Clone, Default)]
pub struct CountMode {
/// The count area (a closed WCS polygon); `None` = all of model space.
pub area: Option<Vec<[f64; 2]>>,
/// The closed polyline marking the area, and whether count mode drew it
/// (then COUNTCLOSE erases it unless a field refers to it).
pub boundary: Option<(Handle, bool)>,
pub target: Option<CountTarget>,
/// The counted reference ← / → last showed.
pub cursor: Option<usize>,
/// The scene geometry epoch the colouring was computed at.
pub epoch: u64,
/// The result and count at `epoch` (recomputed when the drawing changes).
pub cache: Option<(CountResult, usize)>,
}
/// A count mode's result: the counted references and the overlapping
/// duplicates (errors).
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CountResult {
pub counted: Vec<Handle>,
pub errors: Vec<Handle>,
/// The counted references a duplicate overlaps (drawn in the error colour too).
pub overlapped: Vec<Handle>,
}
impl CountMode {
pub fn instances(&self, doc: &CadDocument) -> Vec<BlockInstance> {
codec::count::block_instances(doc, self.area.as_deref())
}
/// The key the match options build from the reference instance.
pub fn key(&self, instances: &[BlockInstance]) -> CountKey {
match &self.target {
Some(CountTarget::Block { reference: Some(r), matching, .. }) => instances
.iter()
.find(|i| i.handle == *r)
.map(|i| CountKey::of(i, matching[0], matching[1], matching[2]))
.unwrap_or_default(),
_ => CountKey::default(),
}
}
/// The cached result, or one worked out from the drawing when there is none.
pub fn result(&self, doc: &CadDocument) -> std::borrow::Cow<'_, CountResult> {
match &self.cache {
Some((result, _)) => std::borrow::Cow::Borrowed(result),
None => std::borrow::Cow::Owned(self.compute(doc).0),
}
}
/// What the count shows: the counted references, or the copies of a group.
pub fn count(&self, doc: &CadDocument) -> usize {
match &self.cache {
Some((_, n)) => *n,
None => self.compute(doc).1,
}
}
/// The result and the count, worked out from the drawing.
pub fn compute(&self, doc: &CadDocument) -> (CountResult, usize) {
if let Some(CountTarget::Group(handles)) = &self.target {
let groups = codec::count::group_matches(doc, handles, self.area.as_deref());
let result = CountResult { counted: groups.concat(), errors: Vec::new(), overlapped: Vec::new() };
return (result, groups.len());
}
let result = self.block_result(doc);
let n = result.counted.len();
(result, n)
}
fn block_result(&self, doc: &CadDocument) -> CountResult {
match &self.target {
None | Some(CountTarget::Group(_)) => CountResult::default(),
Some(CountTarget::Block { name, .. }) => {
let instances = self.instances(doc);
let key = self.key(&instances);
let mine = |i: &&BlockInstance| i.name.eq_ignore_ascii_case(name) && key.matches(i);
CountResult {
counted: instances.iter().filter(mine).filter(|i| i.duplicate_of.is_none()).map(|i| i.handle).collect(),
errors: instances.iter().filter(mine).filter(|i| i.duplicate_of.is_some()).map(|i| i.handle).collect(),
overlapped: instances.iter().filter(mine).filter_map(|i| i.duplicate_of).collect(),
}
}
}
}
/// The name the toolbar and the command line show for the target.
pub fn target_name(&self) -> Option<String> {
match &self.target {
Some(CountTarget::Block { name, .. }) => Some(name.clone()),
Some(CountTarget::Group(_)) => Some("group".into()),
None => None,
}
}
}
/// Palette state that is not in the drawing, and the profile variables
/// COUNTCOLOR, COUNTERRORCOLOR and COUNTSERVICE.
#[derive(Debug)]
pub struct CountPalette {
pub show: bool,
pub search: String,
/// Sorted by the Count column (else by Name), and descending.
pub by_count: bool,
pub descending: bool,
/// Expanded rows: block name (upper case) → expand by layer, scale, mirror state.
pub expanded: Vec<(String, [bool; 3])>,
/// Create Table selection mode: the checked block names (upper case).
pub table: Option<Vec<String>>,
pub details_closed: bool,
pub errors_closed: bool,
pub color: i16,
pub error_color: i16,
pub service: bool,
/// The Invalid Area dialog's "always" box while it is open.
pub invalid_always: bool,
/// What a lost count boundary does without asking: 0 ask, 1 undo, 2 go
/// on with all of model space (kept with the user settings).
pub invalid_choice: u8,
/// The list's references for (document tab id, geometry epoch, area).
pub list_cache: std::cell::RefCell<Option<(u64, u64, Option<Vec<[f64; 2]>>, std::rc::Rc<Vec<BlockInstance>>)>>,
}
impl Default for CountPalette {
fn default() -> Self {
Self {
show: false,
search: String::new(),
by_count: false,
descending: false,
expanded: Vec::new(),
table: None,
details_closed: false,
errors_closed: false,
color: 3,
error_color: 1,
service: true,
invalid_always: false,
invalid_choice: 0,
list_cache: Default::default(),
}
}
}
impl CountPalette {
pub fn expansion(&self, name: &str) -> Option<[bool; 3]> {
let key = name.to_ascii_uppercase();
self.expanded.iter().find(|(n, _)| *n == key).map(|(_, e)| *e)
}
}
/// What a row's context menu does.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RowAction {
Review,
Field,
/// Expand by layer (0), scale (1) or mirror state (2).
Expand(usize),
}
#[derive(Debug, Clone)]
pub enum CountMsg {
Search(String),
/// Sort by the Count column (`true`) or the Name column.
Sort(bool),
/// A row clicked: count mode on its block (and key, for an expanded row).
Open(String, CountKey),
Menu(String, CountKey, RowAction),
CreateTable,
TableCheck(String, bool),
TableAll(bool),
TableCancel,
TableInsert,
Back,
Match(usize, bool),
ToggleDetails,
ToggleErrors,
/// Show an overlapping duplicate.
ShowError(Handle),
Area,
Prev,
Next,
Select,
Field,
Close,
/// The Invalid Area dialog: undo the boundary change (also its ✕ and
/// Cancel), or go on counting all of model space; `InvalidAlways` is its
/// "always perform my current choice" box.
InvalidUndo,
InvalidContinue,
InvalidAlways(bool),
}
fn msg(m: CountMsg) -> Message {
Message::Count(m)
}
/// One block's row: counted references and duplicates.
pub struct Row {
pub name: String,
pub count: usize,
pub errors: usize,
}
/// The rows of the list: blocks whose name contains the search text
/// (case-insensitively), sorted by the chosen column.
pub fn rows(instances: &[BlockInstance], palette: &CountPalette) -> Vec<Row> {
let query = palette.search.trim().to_lowercase();
let mut rows: Vec<Row> = codec::count::block_counts(instances)
.into_iter()
.filter(|c| query.is_empty() || c.name.to_lowercase().contains(&query))
.map(|c| Row { count: c.counted.len(), errors: c.duplicates.len(), name: c.name })
.collect();
if palette.by_count {
rows.sort_by(|a, b| a.count.cmp(&b.count).then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase())));
}
if palette.descending {
rows.reverse();
}
rows
}
fn small_icon<'a>(bytes: &'static [u8], size: f32) -> Element<'a, Message> {
crate::ui::icons::semantic(bytes, size)
}
fn row_menu(name: String, key: CountKey, expandable: bool) -> Element<'static, Message> {
let entry = |label: String, action: RowAction| -> Element<'static, Message> {
button(text(label).size(12))
.width(Fill)
.padding([5, 12])
.on_press(msg(CountMsg::Menu(name.clone(), key.clone(), action)))
.style(|theme: &Theme, status| {
let mut s = button::text(theme, status);
if matches!(status, button::Status::Hovered) {
s.background = Some(Background::Color(theme.palette().primary.base.color));
s.text_color = theme.palette().primary.base.text;
}
s
})
.into()
};
let mut items = column![
entry(crate::t!("Review Count Details").into_owned(), RowAction::Review),
entry(crate::t!("Insert Count Field").into_owned(), RowAction::Field),
]
.spacing(1);
if expandable {
items = items
.push(container(Space::new()).height(1).width(Fill).style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.neutral.color)),
..Default::default()
}))
.push(container(text(crate::t!("EXPAND BY")).size(10).style(muted_style)).padding([3, 12]))
.push(entry(crate::t!("Layer").into_owned(), RowAction::Expand(0)))
.push(entry(crate::t!("Scale").into_owned(), RowAction::Expand(1)))
.push(entry(crate::t!("Mirror State").into_owned(), RowAction::Expand(2)));
}
container(items)
.padding(4)
.width(Length::Fixed(200.0))
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
border: Border { color: theme.palette().background.neutral.color, width: 1.0, radius: 4.0.into() },
..Default::default()
})
.into()
}
fn list_row<'a>(
palette: &CountPalette,
name: &str,
key: CountKey,
label: String,
count: Option<usize>,
errors: bool,
depth: u16,
expanded: Option<bool>,
) -> Element<'a, Message> {
let lead: Element<'a, Message> = if let Some(checked) = palette.table.as_ref().filter(|_| depth == 0) {
let on = checked.contains(&name.to_ascii_uppercase());
let n = name.to_string();
checkbox(on).size(14).on_toggle(move |v| msg(CountMsg::TableCheck(n.clone(), v))).into()
} else if expanded == Some(true) {
container(crate::ui::icons::themed(crate::ui::icons::MINUS, 12.0)).width(Length::Fixed(14.0)).into()
} else {
Space::new().width(Length::Fixed(14.0)).into()
};
let mut cells = row![Space::new().width(Length::Fixed(f32::from(depth) * 18.0)), lead];
if depth == 0 {
cells = cells.push(small_icon(BLOCK_ICON, 14.0));
}
cells = cells.push(text(label).size(12).width(Fill));
if errors {
cells = cells.push(crate::ui::icons::themed_warning(WARNING_ICON, 12.0));
}
// An expanded row shows its counts on the rows below it.
cells = cells.push(match count {
Some(n) => Element::from(text(n.to_string()).size(12)),
None => crate::ui::icons::themed(crate::ui::icons::MORE, 12.0),
});
let body = container(cells.spacing(6).align_y(iced::Center)).width(Fill).padding([4, 6]);
let area = mouse_area(body)
.on_press(msg(CountMsg::Open(name.to_string(), key.clone())))
.interaction(iced::mouse::Interaction::Pointer);
let (n, expandable) = (name.to_string(), depth == 0);
iced_aw::ContextMenu::new(area, move || row_menu(n.clone(), key.clone(), expandable)).into()
}
fn header_button<'a>(label: String, active: bool, descending: bool, m: CountMsg) -> Element<'a, Message> {
let mut content = row![text(label).size(11)].spacing(3).align_y(iced::Center);
if active {
content = content.push(crate::ui::icons::themed_arrow_toggle(!descending, 8.0));
}
button(content)
.on_press(msg(m))
.style(button::text)
.padding([2, 4])
.into()
}
/// The list: search, area button, Name / Count columns, rows, Create Table.
fn list_view<'a>(palette: &'a CountPalette, instances: &[BlockInstance]) -> Element<'a, Message> {
let search = text_input(&crate::t!("Search"), &palette.search)
.on_input(|v| msg(CountMsg::Search(v)))
.padding([4, 8])
.size(12)
.style(field_style);
let top = row![
search.width(Fill),
crate::ui::dock::tool_button(
small_icon(AREA_ICON, crate::ui::dock::TOOL_H),
crate::t!("Count in a specified area").into_owned(),
msg(CountMsg::Area),
),
]
.spacing(4)
.align_y(iced::Center);
let header = row![
header_button(crate::t!("Name").into_owned(), !palette.by_count, palette.descending, CountMsg::Sort(false)),
Space::new().width(Fill),
header_button(crate::t!("Count").into_owned(), palette.by_count, palette.descending, CountMsg::Sort(true)),
]
.align_y(iced::Center);
let mut list = column![].spacing(1);
for r in rows(instances, palette) {
let expansion = palette.expansion(&r.name).filter(|e| e.iter().any(|v| *v));
let count = expansion.is_none().then_some(r.count);
list = list.push(list_row(
palette,
&r.name,
CountKey::default(),
r.name.clone(),
count,
r.errors > 0,
0,
Some(expansion.is_some()),
));
if let Some([l, s, m]) = expansion {
for (key, n) in codec::count::expanded_counts(instances, &r.name, l, s, m) {
let label = key.label();
list = list.push(list_row(palette, &r.name, key, label, Some(n), false, 1, None));
}
}
}
let footer: Element<'a, Message> = match &palette.table {
None => row![
Space::new().width(Fill),
button(text(crate::t!("Create Table")).size(12))
.on_press(msg(CountMsg::CreateTable))
.style(button_style(false))
.padding([5, 12]),
]
.into(),
Some(checked) => {
let all = rows(instances, palette);
let every = !all.is_empty() && all.iter().all(|r| checked.contains(&r.name.to_ascii_uppercase()));
let mut insert = button(text(crate::t!("Insert")).size(12)).style(button_style(true)).padding([5, 12]);
if !checked.is_empty() {
insert = insert.on_press(msg(CountMsg::TableInsert));
}
row![
checkbox(every).size(14).on_toggle(|v| msg(CountMsg::TableAll(v))),
Space::new().width(Fill),
button(text(crate::t!("Cancel")).size(12))
.on_press(msg(CountMsg::TableCancel))
.style(button_style(false))
.padding([5, 12]),
insert,
]
.spacing(6)
.align_y(iced::Center)
.into()
}
};
column![
top,
container(column![header, scrollable(list).height(Fill)].spacing(2))
.padding(4)
.height(Fill)
.style(card),
footer,
]
.spacing(6)
.into()
}
fn card(theme: &Theme) -> container::Style {
container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
border: Border { radius: 4.0.into(), ..Default::default() },
..Default::default()
}
}
/// Count mode in the palette: back to the list, the counted block with the
/// match options, and the error report.
fn mode_view<'a>(palette: &'a CountPalette, mode: &'a CountMode, doc: &CadDocument) -> Element<'a, Message> {
let back = button(row![crate::ui::icons::themed_arrow_left(10.0), text(crate::t!("Back to List")).size(12)].spacing(4).align_y(iced::Center))
.on_press(msg(CountMsg::Back))
.style(button::text)
.padding([2, 2]);
let result = mode.result(doc);
let mut body = column![back].spacing(8);
if let Some(name) = mode.target_name() {
let mut details = column![button(
row![
crate::ui::icons::themed_success(CHECK_ICON, 12.0),
text(match mode.target {
Some(CountTarget::Group(_)) => crate::tf!("Geometries: {}", mode.count(doc)).into_owned(),
_ => format!("{name}: {}", mode.count(doc)),
})
.size(12)
.width(Fill),
]
.spacing(6)
.align_y(iced::Center),
)
.on_press(msg(CountMsg::ToggleDetails))
.style(button::text)
.padding(0)]
.spacing(6);
if let (false, Some(CountTarget::Block { matching, .. })) = (palette.details_closed, &mode.target) {
for (k, label) in ["Match Layer", "Match Scale", "Match Mirror State"].iter().enumerate() {
details = details.push(
checkbox(matching[k])
.label(crate::t!(*label).into_owned())
.text_size(12)
.size(14)
.on_toggle(move |v| msg(CountMsg::Match(k, v))),
);
}
}
body = body.push(container(details).padding(8).width(Fill).style(card));
}
if !result.errors.is_empty() {
let mut report = column![button(
row![
crate::ui::icons::themed_warning(WARNING_ICON, 12.0),
text(crate::tf!("Count Error Report: {}", result.errors.len())).size(12).width(Fill),
]
.spacing(6)
.align_y(iced::Center),
)
.on_press(msg(CountMsg::ToggleErrors))
.style(button::text)
.padding(0)]
.spacing(4);
if !palette.errors_closed {
for h in &result.errors {
report = report.push(
button(text(crate::t!("Overlapping object")).size(12))
.on_press(msg(CountMsg::ShowError(*h)))
.style(button::text)
.padding([2, 18]),
);
}
}
body = body.push(container(report).padding(8).width(Fill).style(card));
}
body.into()
}
pub fn view<'a>(
palette: &'a CountPalette,
mode: Option<&'a CountMode>,
doc: &'a CadDocument,
doc_id: u64,
epoch: u64,
width: f32,
auto_collapse: bool,
) -> Element<'a, Message> {
let title_bar = crate::ui::dock::title_bar(PanelId::Count, crate::t!("Count").into_owned(), auto_collapse);
let body = match mode {
Some(mode) if mode.target.is_some() => mode_view(palette, mode, doc),
_ => {
let area = mode.and_then(|m| m.area.clone());
let key = (doc_id, epoch);
let mut cache = palette.list_cache.borrow_mut();
let instances = match cache.as_ref() {
Some((d, e, a, list)) if (*d, *e) == key && *a == area => list.clone(),
_ => {
let list = std::rc::Rc::new(codec::count::block_instances(doc, area.as_deref()));
*cache = Some((key.0, key.1, area, list.clone()));
list
}
};
list_view(palette, &instances)
}
};
crate::ui::dock::frame(column![title_bar, body].spacing(6), width)
}
fn bar_button<'a>(icon: Element<'a, Message>, tip: String, m: Option<CountMsg>) -> Element<'a, Message> {
let mut b = button(icon).style(button::text).padding([3, 5]);
if let Some(m) = m {
b = b.on_press(msg(m));
}
tooltip(b, text(tip).size(10), tooltip::Position::Bottom).gap(4).into()
}
/// The floating toolbar over the drawing in count mode: the count, the
/// status, previous / next reference, area, select, field and close.
pub fn toolbar<'a>(mode: &CountMode, doc: &CadDocument) -> Element<'a, Message> {
let has_target = mode.target.is_some();
let result = mode.result(doc);
let count = if has_target { mode.count(doc).to_string() } else { "-".to_string() };
let status: Element<'a, Message> = if !result.errors.is_empty() {
crate::ui::icons::themed_warning(WARNING_ICON, 14.0)
} else if has_target {
crate::ui::icons::themed_primary(crate::ui::icons::INFO, 14.0)
} else {
crate::ui::icons::themed_disabled(WARNING_ICON, 14.0)
};
let on = |m: CountMsg| has_target.then_some(m);
let icon = |bytes: &'static [u8]| -> Element<'a, Message> {
if has_target {
crate::ui::icons::themed_success(bytes, 16.0)
} else {
crate::ui::icons::themed_disabled(bytes, 16.0)
}
};
let bar = row![
text(crate::tf!("Count: {}", count)).size(13),
status,
bar_button(crate::ui::icons::themed_arrow_left(12.0), crate::t!("Previous").into_owned(), on(CountMsg::Prev)),
bar_button(crate::ui::icons::themed_arrow_right(12.0), crate::t!("Next").into_owned(), on(CountMsg::Next)),
bar_button(crate::ui::icons::themed_success(AREA_ICON, 16.0), crate::t!("Count in a specified area").into_owned(), Some(CountMsg::Area)),
bar_button(crate::ui::icons::themed_success(SELECT_ICON, 16.0), crate::t!("Select objects").into_owned(), Some(CountMsg::Select)),
bar_button(icon(TABLE_ICON), crate::t!("Insert Count Field").into_owned(), on(CountMsg::Field)),
bar_button(crate::ui::icons::themed_secondary(crate::ui::icons::CLOSE, 12.0), crate::t!("Close").into_owned(), Some(CountMsg::Close)),
]
.spacing(6)
.align_y(iced::Center);
let bar = container(bar).padding([4, 10]).style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
border: Border { color: theme.palette().background.neutral.color, width: 1.0, radius: 4.0.into() },
..Default::default()
});
container(iced::widget::opaque(bar)).width(Fill).align_x(iced::Center).padding([10, 0]).into()
}
/// The blue frame around the drawing area in count mode.
pub fn border<'a>() -> Element<'a, Message> {
container(Space::new().width(Fill).height(Fill))
.width(Fill)
.height(Fill)
.style(|_: &Theme| container::Style {
border: Border { color: Color::from_rgb(0.18, 0.52, 0.95), width: 2.0, radius: 0.0.into() },
..Default::default()
})
.into()
}
/// A colour index as the display colour (`COUNTCOLOR` / `COUNTERRORCOLOR`).
pub fn aci_rgba(index: i16) -> [f32; 4] {
crate::scene::convert::tess_util::aci_to_rgba(&codec::types::Color::from_index(index))
}
/// The Invalid Area dialog, shown when the count area's boundary is gone.
pub fn invalid_area_view<'a>(palette: &CountPalette, sizing: crate::ui::modal::ModalSizing) -> Element<'a, Message> {
let action = |title: String, detail: String, m: CountMsg| -> Element<'a, Message> {
button(
row![
crate::ui::icons::themed_primary(crate::ui::icons::ARROW_LONG_RIGHT, 16.0),
column![text(title).size(13), text(detail).size(11).style(muted_style)].spacing(2),
]
.spacing(10)
.align_y(iced::Center),
)
.on_press(msg(m))
.width(Fill)
.padding([10, 12])
.style(button_style(false))
.into()
};
column![
row![
crate::ui::icons::themed_warning(WARNING_ICON, 20.0),
text(crate::t!("The changes to count boundary have resulted to an invalid count area. What do you want to do?"))
.size(14)
.width(Fill),
]
.spacing(12)
.align_y(iced::Center),
action(
crate::t!("Undo the changes to the count boundary").into_owned(),
crate::t!("The last change is undone; the boundary and the count area come back.").into_owned(),
CountMsg::InvalidUndo,
),
action(
crate::t!("Continue and count the entire model space").into_owned(),
crate::t!("The count area is dropped; counting goes on in all of model space.").into_owned(),
CountMsg::InvalidContinue,
),
row![
checkbox(palette.invalid_always)
.label(crate::t!("Always perform my current choice").into_owned())
.text_size(12)
.size(14)
.on_toggle(|v| msg(CountMsg::InvalidAlways(v))),
Space::new().width(Fill),
crate::ui::style::form::dialog_button(crate::t!("Cancel"), msg(CountMsg::InvalidUndo), false),
]
.spacing(8)
.align_y(iced::Center),
]
.spacing(10)
.padding([12, 14])
.width(sizing.width)
.into()
}
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -7,7 +7,7 @@
use std::fmt;
use acadrust::Handle;
use codec::Handle;
use iced::widget::{button, checkbox, column, container, row, text, text_input, Space};
use iced::{Border, Element, Fill, Length, Theme};
+8 -8
View File
@@ -3,7 +3,7 @@
use crate::app::Message;
use crate::scene::model::hatch_model::GradientKind;
use crate::ui::window::graphic_attributes::GraphicAttributesMsg;
use acadrust::types::Color as AcadColor;
use codec::types::Color as AcadColor;
use iced::widget::{
button, checkbox, column, container, image, mouse_area, row, slider, text, text_input, tooltip,
Space,
@@ -18,7 +18,7 @@ pub enum GradientColorMode {
#[derive(Debug, Clone)]
pub struct GradientEditorState {
pub handles: Vec<acadrust::Handle>,
pub handles: Vec<codec::Handle>,
pub color_mode: GradientColorMode,
pub color_1: AcadColor,
pub color_2: AcadColor,
@@ -55,8 +55,8 @@ fn msg(message: GradientMsg) -> Message {
impl GradientEditorState {
pub fn from_hatch(
handles: Vec<acadrust::Handle>,
hatch: &acadrust::entities::Hatch,
handles: Vec<codec::Handle>,
hatch: &codec::entities::Hatch,
) -> Self {
let color = |index: usize| {
hatch
@@ -125,7 +125,7 @@ fn rgb(color: &AcadColor) -> Color {
let (r, g, b) = match color {
AcadColor::Rgb { r, g, b } => (*r, *g, *b),
AcadColor::Index(i) => {
acadrust::types::aci_table::aci_to_rgb(*i).unwrap_or((128, 128, 128))
codec::types::aci_table::aci_to_rgb(*i).unwrap_or((128, 128, 128))
}
_ => (255, 255, 255),
};
@@ -155,11 +155,11 @@ fn preview_sized(
rgb(&state.color_2)
};
let angle = state.angle_radians();
let frame = cadkernel::geom2d::gradient_frame(
let frame = kernel::geom2d::gradient_frame(
&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]],
angle,
if state.centered { 0.0 } else { 1.0 },
cadkernel::geom2d::Tolerance::default(),
kernel::geom2d::Tolerance::default(),
)?;
let (sin, cos) = angle.sin_cos();
let radial = state.kind.radial();
@@ -214,7 +214,7 @@ fn preview(state: &GradientEditorState) -> Element<'static, Message> {
}
pub(crate) fn compact_preview(
hatch: &acadrust::entities::Hatch,
hatch: &codec::entities::Hatch,
) -> Option<iced::widget::image::Handle> {
preview_sized(&GradientEditorState::from_hatch(Vec::new(), hatch), 256, 26)
}
+3 -3
View File
@@ -6,9 +6,9 @@ use crate::app::{ColorPickTarget, Message};
use crate::scene::Scene;
use crate::ui::dock::{DockMsg, PanelId};
use crate::ui::window::gradient_editor::{GradientEditorState, GradientMsg};
use acadrust::entities::{EntityCommon, Hatch, HatchGradientPattern};
use acadrust::types::{Color as AcadColor, LineWeight, Transparency};
use acadrust::{CadDocument, EntityType, Handle};
use codec::entities::{EntityCommon, Hatch, HatchGradientPattern};
use codec::types::{Color as AcadColor, LineWeight, Transparency};
use codec::{CadDocument, EntityType, Handle};
use iced::widget::{
button, image, column, combo_box, container, mouse_area, row, slider, text, text_input, tooltip,
Space,
+5 -5
View File
@@ -2,7 +2,7 @@
use crate::app::{LayerStateLayerFlag, LayerStateProperty, Message};
use crate::ui::properties::{lw_options, LwItem};
use acadrust::{LayerState, LayerStateMask};
use codec::{LayerState, LayerStateMask};
use iced::widget::{
button, checkbox, column, container, row, scrollable, text, text_input, Space,
};
@@ -12,7 +12,7 @@ use std::borrow::Cow;
use std::fmt;
#[derive(Clone, PartialEq, Debug)]
struct TransparencyItem(Option<acadrust::types::Transparency>);
struct TransparencyItem(Option<codec::types::Transparency>);
impl fmt::Display for TransparencyItem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -23,11 +23,11 @@ impl fmt::Display for TransparencyItem {
}
}
fn transparency_options(current: Option<acadrust::types::Transparency>) -> Vec<TransparencyItem> {
fn transparency_options(current: Option<codec::types::Transparency>) -> Vec<TransparencyItem> {
let mut options = vec![TransparencyItem(None)];
for percent in (0..=90).step_by(10) {
options.push(TransparencyItem(Some(
acadrust::types::Transparency::from_percent(percent as f64 / 100.0),
codec::types::Transparency::from_percent(percent as f64 / 100.0),
)));
}
let current = TransparencyItem(current);
@@ -400,7 +400,7 @@ fn editor_header<'a>() -> Element<'a, Message> {
fn editor_layer_row<'a>(
index: usize,
layer: &'a acadrust::LayerStateLayer,
layer: &'a codec::LayerStateLayer,
color_open: bool,
linetypes: Vec<String>,
) -> Element<'a, Message> {
+24 -67
View File
@@ -3,12 +3,13 @@
use crate::app::Message;
use crate::ui::properties::{lw_options, LinetypeItem, LwItem};
use crate::ui::style::common::muted_style;
use crate::ui::style::row_button::{cell_button_style, header_button_style};
use crate::ui::ROW_H;
use acadrust::tables::layer::Layer as DocLayer;
use acadrust::tables::Table;
use acadrust::types::aci_table::aci_to_rgb;
use acadrust::types::{Color as AcadColor, LineWeight};
use acadrust::Handle;
use codec::tables::layer::Layer as DocLayer;
use codec::tables::Table;
use codec::types::aci_table::aci_to_rgb;
use codec::types::{Color as AcadColor, LineWeight};
use codec::Handle;
use iced::widget::{
button, column, combo_box, container, mouse_area, row, scrollable, text, text_input, tooltip,
};
@@ -503,48 +504,6 @@ impl LayerPanel {
// ── Sorting helpers ─────────────────────────────────────────────────────────
fn layer_cell_button_style(
theme: &Theme,
status: button::Status,
is_selected: bool,
index: usize,
) -> button::Style {
let palette = theme.palette();
let highlighted = matches!(status, button::Status::Hovered);
let pair = if highlighted {
palette.background.strong
} else if is_selected {
palette.primary.weak
} else if index % 2 == 0 {
palette.background.base
} else {
palette.background.weak
};
button::Style {
background: highlighted.then_some(Background::Color(pair.color)),
text_color: pair.text,
..Default::default()
}
}
fn layer_header_button_style(theme: &Theme, status: button::Status) -> button::Style {
let palette = theme.palette();
let highlighted = matches!(
status,
button::Status::Hovered | button::Status::Pressed
);
let pair = if highlighted {
palette.background.strong
} else {
palette.background.weak
};
button::Style {
background: highlighted.then_some(Background::Color(pair.color)),
text_color: pair.text,
..Default::default()
}
}
/// Packed RGB key for ordering colours deterministically by hue-ish bytes.
fn color_sort_key(c: AcadColor) -> u32 {
let c = iced_color_from_acad(&c);
@@ -573,7 +532,7 @@ fn sortable_header<'a>(
}
button(content)
.on_press(Message::LayerSort(col))
.style(layer_header_button_style)
.style(header_button_style)
.padding(Padding {
top: 0.0,
bottom: 0.0,
@@ -714,12 +673,13 @@ fn layer_row<'a>(
vp_cols: &'a [VpCol],
name_col_w: f32,
) -> Element<'a, Message> {
// #22: messages carry the layer name (resolved at dispatch time) so a
// resort between view() and the handler cannot mistarget the row.
let lname = layer.name.clone();
let svg_btn = |bytes: &'static [u8], on_press: Message| -> Element<'a, Message> {
button(crate::ui::icons::semantic(bytes, ICON_SZ))
.on_press(on_press)
.style(move |theme: &Theme, status| {
layer_cell_button_style(theme, status, is_selected, index)
})
.style(cell_button_style(is_selected, index))
.padding(Padding {
top: COMBO_PAD_V,
bottom: COMBO_PAD_V,
@@ -746,10 +706,8 @@ fn layer_row<'a>(
};
let plot_btn: Element<'_, Message> = button(plot_icon)
.on_press(Message::LayerTogglePlot(index))
.style(move |theme: &Theme, status| {
layer_cell_button_style(theme, status, is_selected, index)
})
.on_press(Message::LayerTogglePlot(lname.clone()))
.style(cell_button_style(is_selected, index))
.padding(Padding {
top: COMBO_PAD_V,
bottom: COMBO_PAD_V,
@@ -787,10 +745,8 @@ fn layer_row<'a>(
text(crate::ui::text_util::elide(&layer.name, name_budget))
.size(FONT_SZ),
)
.on_press(Message::LayerRenameStart(index))
.style(move |theme: &Theme, status| {
layer_cell_button_style(theme, status, is_selected, index)
})
.on_press(Message::LayerRenameStart(lname.clone()))
.style(cell_button_style(is_selected, index))
.padding(Padding {
top: COMBO_PAD_V,
bottom: COMBO_PAD_V,
@@ -821,9 +777,9 @@ fn layer_row<'a>(
..Default::default()
},
Message::LayerColorSet,
Message::LayerColorPickerToggle(index),
Message::LayerColorPickerToggle(lname.clone()),
Message::OpenColorWindow(
crate::app::ColorPickTarget::Layer(index),
crate::app::ColorPickTarget::Layer(lname.clone()),
layer.color,
),
))
@@ -888,8 +844,9 @@ fn layer_row<'a>(
// Transparency cell
let trans_str = layer.transparency.to_string();
let lname_trans = lname.clone();
let trans_cell = text_input("0", &trans_str)
.on_input(move |s| Message::LayerTransparencyEdit(index, s))
.on_input(move |s| Message::LayerTransparencyEdit(lname_trans.clone(), s))
.size(FONT_SZ)
.padding(Padding {
top: COMBO_PAD_V,
@@ -911,13 +868,13 @@ fn layer_row<'a>(
.align_y(iced::Center),
name_cell,
iced::widget::Space::new().width(2),
container(svg_btn(vis_svg, Message::LayerToggleVisible(index)))
container(svg_btn(vis_svg, Message::LayerToggleVisible(lname.clone())))
.width(Length::Fixed(COL_ICON))
.align_x(iced::Center),
container(svg_btn(frz_svg, Message::LayerToggleFreeze(index)))
container(svg_btn(frz_svg, Message::LayerToggleFreeze(lname.clone())))
.width(Length::Fixed(COL_ICON))
.align_x(iced::Center),
container(svg_btn(lck_svg, Message::LayerToggleLock(index)))
container(svg_btn(lck_svg, Message::LayerToggleLock(lname.clone())))
.width(Length::Fixed(COL_ICON))
.align_x(iced::Center),
container(plot_btn)
@@ -939,7 +896,7 @@ fn layer_row<'a>(
row_content = row_content.push(
container(svg_btn(
vp_frz_svg,
Message::LayerToggleVpFreeze(index, vp_idx),
Message::LayerToggleVpFreeze(lname.clone(), vp_idx),
))
.width(Length::Fixed(COL_ICON))
.align_x(iced::Center),
@@ -972,7 +929,7 @@ fn layer_row<'a>(
.height(Length::Fixed(ROW_H))
.width(Fill),
)
.on_press(Message::LayerSelect(index))
.on_press(Message::LayerSelect(lname))
.into()
}
+6
View File
@@ -1,4 +1,6 @@
pub mod about;
pub mod attdef_dialog;
pub mod field_dialog;
pub mod block_definition;
pub mod block_palette;
pub mod browser;
@@ -26,6 +28,10 @@ pub mod annotation_data;
pub mod alias_editor;
pub mod find_replace;
pub mod named_parameters;
pub mod pc_manager;
pub mod count_palette;
pub mod sheet_set;
pub mod pdf_dialogs;
pub mod xref_attach;
pub mod xref_help;
pub mod xref_manager;
+7 -7
View File
@@ -18,8 +18,8 @@ use crate::scene::named_parameters::ParameterTable;
use crate::scene::Scene;
use crate::t;
use crate::ui::style::common::muted_style;
use acadrust::types::Handle;
use acadrust::EntityType;
use codec::types::Handle;
use codec::EntityType;
use iced::widget::tooltip::Position as TipPos;
use iced::widget::{button, column, container, row, scrollable, text, text_input, tooltip, Space};
use iced::{Background, Element, Length, Theme};
@@ -392,10 +392,10 @@ mod tests {
ConstraintKind, ParametricRef, ParametricScope,
};
let mut scene = Scene::new();
let line = scene.add_entity(acadrust::EntityType::Line(
acadrust::entities::Line::from_points(
acadrust::types::Vector3::new(0.0, 0.0, 0.0),
acadrust::types::Vector3::new(10.0, 0.0, 0.0),
let line = scene.add_entity(codec::EntityType::Line(
codec::entities::Line::from_points(
codec::types::Vector3::new(0.0, 0.0, 0.0),
codec::types::Vector3::new(10.0, 0.0, 0.0),
),
));
scene
@@ -426,7 +426,7 @@ mod tests {
#[test]
fn entity_label_reports_erased_for_a_dangling_handle() {
let scene = Scene::new();
let label = entity_label(&scene, acadrust::types::Handle::new(999));
let label = entity_label(&scene, codec::types::Handle::new(999));
assert!(label.starts_with("(erased "), "got {label}");
}
}
+130 -9
View File
@@ -1,5 +1,8 @@
#[cfg(not(target_arch = "wasm32"))]
pub(crate) mod graphics;
pub(crate) mod spacemouse;
use crate::app::config::UiThemeConfig;
use crate::app::settings;
use crate::app::settings::{CursorType, RightClickMode};
use crate::app::Message;
use crate::ui::style::form::{dialog_button, dialog_button_styled_opt};
@@ -34,6 +37,7 @@ pub enum OptionsTab {
Modeling,
Selection,
UserPreferences,
Graphics,
}
/// Application preferences the dialog reads that are plain scalars on the app.
@@ -55,7 +59,8 @@ pub struct AppPrefs {
pub commandline_fade_ms: i32,
/// ZOOMWHEEL: reverse the mouse-wheel zoom direction.
pub zoom_wheel_reversed: bool,
/// ZOOMFACTOR, 3..=100.
/// ZOOMFACTOR. The slider sets the range the system variable has; the
/// field beside it reaches `settings::ZOOM_FACTOR_MAX`.
pub zoom_factor: i32,
/// TEXTEDITMODE: TEXTEDIT keeps prompting for the next object.
pub texteditmode: bool,
@@ -92,6 +97,9 @@ pub struct Folders {
pub plot_styles: Option<String>,
pub plugins: Option<String>,
pub autosave: Option<String>,
/// Where `.shx` / `.ttf` fonts dropped in by the user (or downloaded for
/// a drawing) are found.
pub fonts: Option<String>,
}
/// Values read from the current drawing's header rather than from preferences.
@@ -199,14 +207,21 @@ pub fn view_window<'a>(
selection: SelectionPrefs,
prefs: AppPrefs,
spacemouse: Element<'a, Message>,
graphics: Element<'a, Message>,
snap_angle_input: &'a str,
zoom_factor_input: &'a str,
drawing_prefs: DrawingPrefs,
folders: Folders,
qnew_template: &'a str,
double_click_block_refedit: bool,
double_click_block_attedit: bool,
cursor_type: CursorType,
crosshair_color: Option<[u8; 3]>,
crosshair_color_input: &'a str,
snap_marker_color: Option<[u8; 3]>,
snap_marker_color_input: &'a str,
command_text_color: Option<[u8; 3]>,
command_text_color_input: &'a str,
lineweight_display_scale: i32,
model_space: &'a crate::app::config::ModelSpaceThemeConfig,
model_bg_input: &'a str,
@@ -415,7 +430,8 @@ pub fn view_window<'a>(
text(crate::tr!("options", "default-save-format-label")).size(12).width(150),
iced::widget::pick_list(
selected_format,
crate::io::SAVE_FORMAT_OPTIONS,
// Drawings, not the template entry (the list's last).
&crate::io::SAVE_FORMAT_OPTIONS[..crate::io::SAVE_FORMAT_OPTIONS.len() - 1],
|value| value.to_string(),
)
.on_select(|format: &str| Message::DefaultSaveFormatChanged(format.to_string()))
@@ -515,6 +531,42 @@ pub fn view_window<'a>(
..Default::default()
});
let command_rgb = command_text_color.unwrap_or([220, 220, 220]);
let command_swatch = container(Space::new())
.width(28)
.height(22)
.style(move |theme: &Theme| container::Style {
background: Some(Background::Color(iced::Color::from_rgb8(
command_rgb[0],
command_rgb[1],
command_rgb[2],
))),
border: Border {
color: theme.palette().background.strong.color,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
});
let snap_rgb = snap_marker_color.unwrap_or([255, 230, 26]);
let snap_swatch = container(Space::new())
.width(28)
.height(22)
.style(move |theme: &Theme| container::Style {
background: Some(Background::Color(iced::Color::from_rgb8(
snap_rgb[0],
snap_rgb[1],
snap_rgb[2],
))),
border: Border {
color: theme.palette().background.strong.color,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
});
let model_bg_rgb = model_space.custom_bg.unwrap_or(crate::app::config::CLASSIC_CAD_DARK_BG);
let paper_bg_rgb = model_space.custom_paper_bg.unwrap_or(crate::app::config::DEFAULT_PAPER_BG);
@@ -762,6 +814,30 @@ pub fn view_window<'a>(
.spacing(10)
.align_y(iced::Center),
)
.push(Space::new().height(10))
.push(
row![
text(crate::t!("Snap marker color")).size(12).width(140),
snap_swatch,
text_input(crate::t!("#RRGGBB or blank").as_ref(), snap_marker_color_input)
.on_input(Message::SnapMarkerColorChanged)
.width(150),
]
.spacing(10)
.align_y(iced::Center),
)
.push(Space::new().height(10))
.push(
row![
text(crate::t!("Command line text color")).size(12).width(140),
command_swatch,
text_input(crate::t!("#RRGGBB or blank").as_ref(), command_text_color_input)
.on_input(Message::CommandTextColorChanged)
.width(150),
]
.spacing(10)
.align_y(iced::Center),
)
.push(Space::new().height(6))
.push(
text(crate::t!("Leave the color blank to keep automatic viewport contrast."))
@@ -845,7 +921,7 @@ pub fn view_window<'a>(
.step(250)
.width(Fill),
text(if prefs.commandline_fade_ms <= 0 {
crate::t!("Off").into_owned()
crate::t!("Never").into_owned()
} else {
format!("{:.1} s", prefs.commandline_fade_ms as f32 / 1000.0)
})
@@ -858,7 +934,7 @@ pub fn view_window<'a>(
.push(Space::new().height(6))
.push(
text(crate::t!(
"How long overlay history lines stay visible; 0 skips them (COMMANDLINEFADETIME)."
"How long overlay history lines stay visible; 0 means never fade (COMMANDLINEFADETIME)."
))
.size(11)
.width(sizing.width),
@@ -1181,10 +1257,24 @@ pub fn view_window<'a>(
Space::new().height(12),
row![
text(crate::t!("Zoom factor")).size(12).width(150),
slider(3..=100, prefs.zoom_factor.clamp(3, 100), Message::ZoomFactorChanged)
.step(1)
.width(Fill),
text(prefs.zoom_factor.clamp(3, 100).to_string()).size(11).width(44),
slider(
settings::ZOOM_FACTOR_MIN..=settings::ZOOM_FACTOR_SYSVAR_MAX,
prefs
.zoom_factor
.clamp(settings::ZOOM_FACTOR_MIN, settings::ZOOM_FACTOR_SYSVAR_MAX),
Message::ZoomFactorChanged,
)
.step(1)
.width(Fill),
text_input("60", zoom_factor_input)
.on_input(Message::ZoomFactorInputChanged)
.width(52),
text(crate::tf!(
"{:.1}% a notch",
settings::zoom_notch_percent(prefs.zoom_factor)
))
.size(11)
.width(76),
]
.spacing(10)
.align_y(iced::Center),
@@ -1192,6 +1282,15 @@ pub fn view_window<'a>(
text(crate::t!("How far one wheel notch zooms (ZOOMFACTOR)."))
.size(11)
.width(sizing.width),
Space::new().height(4),
text(crate::tf!(
"The slider covers the system variable's {} to {}; the field takes up to {} for a faster wheel.",
settings::ZOOM_FACTOR_MIN,
settings::ZOOM_FACTOR_SYSVAR_MAX,
settings::ZOOM_FACTOR_MAX
))
.size(11)
.width(sizing.width),
Space::new().height(24),
text(crate::t!("Text and Dimensions")).size(15),
Space::new().height(10),
@@ -1636,6 +1735,23 @@ pub fn view_window<'a>(
folder_row(crate::t!("Plugins"), folders.plugins.clone()),
Space::new().height(12),
folder_row(crate::t!("Autosave files"), folders.autosave.clone()),
Space::new().height(12),
folder_row(crate::t!("Fonts"), folders.fonts.clone()),
Space::new().height(16),
text(crate::t!("Default template for new drawings")).size(12),
Space::new().height(4),
row![
text_input(crate::t!("Blank drawing").as_ref(), qnew_template)
.on_input(Message::QnewTemplateChanged)
.size(11)
.width(Fill),
button(text(crate::t!("Browse...")).size(11))
.padding([4, 10])
.style(button::secondary)
.on_press(Message::QnewTemplateBrowse),
]
.spacing(10)
.align_y(iced::Center),
]
.spacing(0)
.width(sizing.width);
@@ -1649,6 +1765,7 @@ pub fn view_window<'a>(
OptionsTab::Drafting => drafting.into(),
OptionsTab::Modeling => modeling.into(),
OptionsTab::UserPreferences => user_prefs.into(),
OptionsTab::Graphics => graphics,
};
// A vertical rail rather than a horizontal strip: the tab names are
@@ -1671,7 +1788,11 @@ pub fn view_window<'a>(
tab_button(crate::t!("3D Modeling"), OptionsTab::Modeling),
tab_button(crate::t!("Selection"), OptionsTab::Selection),
tab_button(crate::t!("User Preferences"), OptionsTab::UserPreferences),
]
];
// The web build has no backend to choose, so it has no Graphics page.
#[cfg(not(target_arch = "wasm32"))]
let tabs = tabs.push(tab_button(crate::t!("Graphics"), OptionsTab::Graphics));
let tabs = tabs
.spacing(2)
.width(iced::Length::Fixed(TAB_RAIL_WIDTH));
+122
View File
@@ -0,0 +1,122 @@
//! Options > Graphics: which graphics backend the next launch uses.
//!
//! Everything here is read before the application boots (the backend is
//! chosen first), so changes are saved at once and apply on restart rather
//! than going through OK / Apply.
use crate::app::Message;
use crate::gpu_backend::{ActiveGpu, BackendChoice, GraphicsPrefs};
use iced::widget::{checkbox, column, pick_list, row, text, Space};
use iced::{Element, Fill};
fn current_row(label: std::borrow::Cow<'static, str>, value: String) -> Element<'static, Message> {
row![
text(label).size(12).width(150),
text(value).size(12).width(Fill),
]
.spacing(10)
.into()
}
fn unknown(s: &str) -> String {
if s.is_empty() {
crate::t!("Unknown").into_owned()
} else {
s.to_string()
}
}
pub(crate) fn view(prefs: GraphicsPrefs, active: ActiveGpu) -> Element<'static, Message> {
let backend = pick_list(
Some(prefs.backend),
BackendChoice::available(),
|choice: &BackendChoice| crate::t!(choice.label()).into_owned(),
)
.on_select(Message::GraphicsBackendChanged)
.width(260);
let mut col = column![
text(crate::t!("Graphics")).size(15),
Space::new().height(6),
text(crate::t!(
"Saved immediately. Changes take effect the next time Open CAD Studio starts."
))
.size(11)
.width(Fill),
Space::new().height(16),
text(crate::t!("This session")).size(13),
Space::new().height(8),
current_row(
crate::t!("GPU"),
unknown(active.adapter.as_deref().unwrap_or(""))
),
Space::new().height(4),
current_row(crate::t!("Driver"), unknown(&active.driver)),
Space::new().height(4),
current_row(
crate::t!("Backend"),
match active.backend.as_deref() {
Some("dx12") => "DirectX 12".to_string(),
Some("vulkan") => "Vulkan".to_string(),
Some("gl") => "OpenGL".to_string(),
Some("metal") => "Metal".to_string(),
Some(other) => other.to_string(),
None => {
if cfg!(target_os = "macos") {
"Metal".to_string()
} else {
unknown("")
}
}
},
),
Space::new().height(4),
current_row(crate::t!("Selected by"), unknown(&active.origin)),
Space::new().height(20),
text(crate::t!("Next launch")).size(13),
Space::new().height(8),
row![text(crate::t!("Graphics backend")).size(12).width(150), backend]
.spacing(10)
.align_y(iced::Center),
Space::new().height(12),
row![
checkbox(prefs.compat_renderer)
.on_toggle(Message::GraphicsCompatToggled)
.size(15),
text(crate::t!(
"Use the compatibility renderer (for GPUs without shader storage buffers)"
))
.size(12),
]
.spacing(8)
.align_y(iced::Center),
];
if cfg!(target_os = "windows") {
col = col.push(Space::new().height(10)).push(
row![
checkbox(prefs.legacy_gl)
.on_toggle(Message::GraphicsLegacyGlToggled)
.size(15),
text(crate::t!(
"Prefer OpenGL on older GPUs (automatic backend only)"
))
.size(12),
]
.spacing(8)
.align_y(iced::Center),
);
}
col.push(Space::new().height(16))
.push(
text(crate::t!(
"If a pinned backend fails to start, the next launch falls back to automatic selection. \
The --backend option and the WGPU_BACKEND variable override this page."
))
.size(11)
.width(Fill),
)
.spacing(0)
.into()
}
+358
View File
@@ -0,0 +1,358 @@
//! Point Cloud Manager (POINTCLOUDMANAGER): the regions and scans of the
//! drawing's point clouds as a tree, each with a switch that shows or hides
//! its points. The switches are the cloud's own hidden scans and regions, so
//! the tree is read straight from the document on every view.
use crate::app::Message;
use crate::scene::model::point_cloud::{self, UNASSIGNED_OFF};
use crate::ui::dock::PanelId;
use codec::entities::ExtendedEntityData;
use codec::{CadDocument, EntityType, Handle};
use iced::widget::{button, checkbox, column, container, mouse_area, row, scrollable, text, text_input, tooltip, Space};
use iced::{Background, Border, Element, Fill, Length, Theme};
use std::collections::HashSet;
const CLOUD_ICON: &[u8] = include_bytes!("../../../assets/icons/pc_attach.svg");
const REGION_ICON: &[u8] = include_bytes!("../../../assets/icons/region.svg");
const UNASSIGNED_ICON: &[u8] = include_bytes!("../../../assets/icons/revcloud.svg");
/// Palette state that is not in the drawing: shown, folded nodes, the
/// highlighted row and the search text.
#[derive(Debug, Default)]
pub struct PcManager {
pub show: bool,
pub collapsed: HashSet<String>,
pub selected: Option<String>,
pub search: String,
}
/// A row with a switch.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Row {
Cloud,
Unassigned,
Scans,
/// A scan, by identifier.
Scan(String),
}
/// A switch's state: a parent is mixed while some children are on.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Switch {
On,
Off,
Mixed,
}
impl Row {
/// `cloud`, `unassigned`, `scans` or `scan:<name or identifier>`
/// (automation).
pub fn parse(text: &str) -> Option<Self> {
match text {
"cloud" => Some(Self::Cloud),
"unassigned" => Some(Self::Unassigned),
"scans" => Some(Self::Scans),
_ => text.strip_prefix("scan:").map(|name| Self::Scan(name.to_string())),
}
}
}
#[derive(Debug, Clone)]
pub enum PcManagerMsg {
Toggle(Handle, Row),
Expand(String),
Select(String),
Search(String),
CollapseAll,
ExpandAll,
}
/// One cloud of the tree.
pub struct Cloud {
pub handle: Handle,
pub name: String,
pub hidden: Vec<String>,
/// Each scan's name and identifier.
pub scans: Vec<(String, String)>,
}
/// The drawing's point clouds; only the selected ones when any is selected.
pub fn clouds(document: &CadDocument, selected: &[Handle]) -> Vec<Cloud> {
let all: Vec<Cloud> = document
.entities()
.filter_map(|entity| {
let EntityType::Extended(extended) = entity else {
return None;
};
let ExtendedEntityData::PointCloudEx(data) = &extended.data else {
return None;
};
let common = entity.common();
let scans = point_cloud::resolve_source(document, data)
.and_then(|path| point_cloud::load(&path))
.map(|cloud| cloud.scan_ranges.iter().map(|s| (s.name.clone(), s.id.clone())).collect())
.unwrap_or_default();
Some(Cloud {
handle: common.handle,
name: if data.name.is_empty() { format!("{:X}", common.handle.value()) } else { data.name.clone() },
hidden: point_cloud::hidden(data),
scans,
})
})
.collect();
if all.iter().any(|cloud| selected.contains(&cloud.handle)) {
all.into_iter().filter(|cloud| selected.contains(&cloud.handle)).collect()
} else {
all
}
}
/// A row's switch. A parent is on with all its children on, off with all
/// off, mixed otherwise (the reference shows the cloud mixed with its only
/// scan off and its unassigned points on).
pub fn switch_state(hidden: &[String], scans: &[(String, String)], row: &Row) -> Switch {
let on = |id: &str| !hidden.iter().any(|h| h == id);
let of = |states: &[bool]| match (states.iter().all(|s| *s), states.iter().any(|s| *s)) {
(true, _) => Switch::On,
(false, false) => Switch::Off,
_ => Switch::Mixed,
};
let scan_states: Vec<bool> = scans.iter().map(|(_, id)| on(id)).collect();
match row {
Row::Scan(id) => of(&[on(id)]),
Row::Scans => of(&scan_states),
Row::Unassigned => of(&[on(UNASSIGNED_OFF)]),
Row::Cloud => {
let mut all = scan_states;
all.push(on(UNASSIGNED_OFF));
of(&all)
}
}
}
/// The hidden list after clicking `row`: an on switch turns its rows off,
/// an off or mixed one turns them all on.
pub fn toggled(hidden: &[String], scans: &[(String, String)], row: &Row) -> Vec<String> {
fn set(out: &mut Vec<String>, id: &str, off: bool) {
out.retain(|h| h != id);
if off {
out.push(id.to_string());
}
}
let off = switch_state(hidden, scans, row) == Switch::On;
let mut out = hidden.to_vec();
match row {
Row::Cloud => {
scans.iter().for_each(|(_, id)| set(&mut out, id, off));
set(&mut out, UNASSIGNED_OFF, off);
}
Row::Unassigned => set(&mut out, UNASSIGNED_OFF, off),
Row::Scans => scans.iter().for_each(|(_, id)| set(&mut out, id, off)),
Row::Scan(id) => set(&mut out, id, off),
}
out
}
/// Keys of the rows that fold: each cloud and its Scans.
pub fn folding_keys(clouds: &[Cloud]) -> Vec<String> {
clouds.iter().flat_map(|c| [key(c.handle), format!("{}/scans", key(c.handle))]).collect()
}
fn key(handle: Handle) -> String {
format!("{:X}", handle.value())
}
fn msg(m: PcManagerMsg) -> Message {
Message::PcManager(m)
}
/// One tree row: indent, fold arrow, icon, label and switch (`None` for a
/// row that has none to give, drawn off and disabled).
fn tree_row<'a>(
state: &PcManager,
key: String,
depth: u16,
fold: Option<bool>,
icon: Element<'a, Message>,
label: String,
switch: Option<(Switch, Message)>,
) -> Element<'a, Message> {
let arrow: Element<'a, Message> = match fold {
Some(open) => button(if open {
crate::ui::icons::themed_arrow_down(10.0)
} else {
crate::ui::icons::themed_arrow_right(10.0)
})
.on_press(msg(PcManagerMsg::Expand(key.clone())))
.style(button::text)
.padding(2)
.into(),
None => Space::new().width(14).into(),
};
let enabled = switch.is_some();
let label = text(label).size(12).width(Fill);
let label = if enabled { label } else { label.style(crate::ui::style::common::muted_style) };
let switch: Element<'a, Message> = match switch {
// Mixed: a dash in the box, as the reference's indeterminate state.
Some((Switch::Mixed, message)) => button(crate::ui::icons::themed_primary(crate::ui::icons::MINUS, 10.0))
.on_press(message)
.style(button::secondary)
.padding(1)
.width(Length::Fixed(16.0))
.height(Length::Fixed(16.0))
.into(),
Some((on, message)) => checkbox(on == Switch::On).on_toggle(move |_| message.clone()).size(14).into(),
None => checkbox(false).size(14).into(),
};
let selected = state.selected.as_deref() == Some(key.as_str());
let cells = row![Space::new().width(Length::Fixed(f32::from(depth) * 16.0)), arrow, icon, label, switch]
.spacing(5)
.align_y(iced::Center);
mouse_area(
container(cells)
.width(Fill)
.padding([3, 6])
.style(move |theme: &Theme| container::Style {
background: selected.then(|| Background::Color(theme.palette().primary.weak.color)),
text_color: selected.then(|| theme.palette().primary.weak.text),
..Default::default()
}),
)
.on_press(msg(PcManagerMsg::Select(key)))
.into()
}
fn fold_button<'a>(icon: &'static [u8], tip: String, message: PcManagerMsg) -> Element<'a, Message> {
let b = button(crate::ui::icons::themed_secondary(icon, 12.0))
.on_press(msg(message))
.style(button::subtle)
.padding([2, 4]);
tooltip(b, text(tip).size(10), tooltip::Position::Bottom).gap(4).into()
}
pub fn view<'a>(
state: &'a PcManager,
document: &'a CadDocument,
selected: &[Handle],
width: f32,
auto_collapse: bool,
) -> Element<'a, Message> {
let title_bar =
crate::ui::dock::title_bar(PanelId::PointCloudManager, crate::t!("Point Cloud Manager").into_owned(), auto_collapse);
let query = state.search.trim().to_lowercase();
let matches = |label: &str| query.is_empty() || label.to_lowercase().contains(&query);
// Searching opens every node so the matches show.
let open = |key: &str| !query.is_empty() || !state.collapsed.contains(key);
let regions = crate::t!("Regions").into_owned();
let unassigned = crate::t!("Unassigned Points").into_owned();
let scans_label = crate::t!("Scans").into_owned();
let mut tree = column![].spacing(1);
for cloud in clouds(document, selected) {
let root = key(cloud.handle);
let scans: Vec<&(String, String)> = cloud.scans.iter().filter(|(name, _)| matches(name)).collect();
let show_scans = matches(&scans_label) || !scans.is_empty();
let children = matches(&regions) || matches(&unassigned) || show_scans;
if !matches(&cloud.name) && !children {
continue;
}
let switch = |row: Row| {
let on = switch_state(&cloud.hidden, &cloud.scans, &row);
Some((on, msg(PcManagerMsg::Toggle(cloud.handle, row))))
};
let cloud_icon = || crate::ui::icons::semantic(CLOUD_ICON, 14.0);
tree = tree.push(tree_row(
state,
root.clone(),
0,
Some(open(&root)),
cloud_icon(),
cloud.name.clone(),
switch(Row::Cloud),
));
if !open(&root) {
continue;
}
if matches(&regions) {
// Region data is not read: the row is there, off and disabled.
tree = tree.push(tree_row(
state,
format!("{root}/regions"),
1,
None,
crate::ui::icons::semantic_disabled(REGION_ICON, 14.0),
regions.clone(),
None,
));
}
if matches(&unassigned) {
tree = tree.push(tree_row(
state,
format!("{root}/unassigned"),
1,
None,
crate::ui::icons::semantic(UNASSIGNED_ICON, 14.0),
unassigned.clone(),
switch(Row::Unassigned),
));
}
if show_scans {
let scans_key = format!("{root}/scans");
let scans_open = open(&scans_key);
tree = tree.push(tree_row(
state,
scans_key.clone(),
1,
Some(scans_open),
cloud_icon(),
scans_label.clone(),
switch(Row::Scans),
));
if scans_open {
for (name, id) in scans {
tree = tree.push(tree_row(
state,
format!("{scans_key}/{id}"),
2,
None,
cloud_icon(),
name.clone(),
switch(Row::Scan(id.clone())),
));
}
}
}
}
let heading = text(crate::t!("REGIONS AND SCANS").into_owned())
.size(11)
.font(iced::Font {
weight: iced::font::Weight::Bold,
style: iced::font::Style::Italic,
..iced::Font::DEFAULT
})
.style(|theme: &Theme| text::Style { color: Some(theme.palette().primary.base.color) })
.width(Fill);
let header = row![
heading,
fold_button(crate::ui::icons::MINUS, crate::t!("Collapse all").into_owned(), PcManagerMsg::CollapseAll),
fold_button(crate::ui::icons::PLUS, crate::t!("Expand all").into_owned(), PcManagerMsg::ExpandAll),
]
.spacing(2)
.align_y(iced::Center);
let card = container(column![header, scrollable(tree).height(Fill)].spacing(6))
.padding(6)
.width(Fill)
.height(Fill)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
border: Border { radius: 4.0.into(), ..Default::default() },
..Default::default()
});
let search = text_input(&crate::t!("Search"), &state.search)
.on_input(|v| msg(PcManagerMsg::Search(v)))
.padding([4, 8])
.size(12);
crate::ui::dock::frame(column![title_bar, card, search].spacing(6), width)
}
File diff suppressed because it is too large. Load diff
+4 -4
View File
@@ -158,13 +158,13 @@ pub struct PageSetupImportDraft {
pub file: String,
/// Each setup with whether it is ticked for import. The settings are
/// carried along so the source file is read once.
pub setups: Vec<(String, acadrust::objects::PlotSettings, bool)>,
pub setups: Vec<(String, codec::objects::PlotSettings, bool)>,
/// Why the file gave nothing, when it did not.
pub error: Option<String>,
}
impl PageSetupImportDraft {
pub fn selected(&self) -> impl Iterator<Item = (&str, &acadrust::objects::PlotSettings)> {
pub fn selected(&self) -> impl Iterator<Item = (&str, &codec::objects::PlotSettings)> {
self.setups
.iter()
.filter(|(_, _, on)| *on)
@@ -178,7 +178,7 @@ pub enum PageSetupImportMsg {
/// Ask for the drawing to import from.
Pick,
/// The drawing was read (or could not be): its name and page setups.
Loaded(Result<(String, Vec<(String, acadrust::objects::PlotSettings)>), String>),
Loaded(Result<(String, Vec<(String, codec::objects::PlotSettings)>), String>),
/// Tick or untick one setup.
Toggle(String),
/// Tick or untick every setup.
@@ -1553,7 +1553,7 @@ pub fn view_window(
paper_space_option(s.paperspace_last, t!("Paper space last"), PlotFlag::PaperspaceLast),
check(t!("Merge overlapping lines"), s.merge_lines, PlotFlag::MergeLines),
check(t!("Plot stamp"), s.stamp, PlotFlag::Stamp),
paper_space_option(s.save_to_layout, t!("Save changes to layout"), PlotFlag::SaveToLayout),
check(t!("Save changes to layout"), s.save_to_layout, PlotFlag::SaveToLayout),
]
.spacing(6)
.width(width),
+2 -2
View File
@@ -1033,7 +1033,7 @@ mod tests {
tag: "v1.4.0".to_string(),
api_version: 4,
acadrust_source: Some(
"git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd".to_string(),
"git+https://github.com/HakanSeven12/opencadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd".to_string(),
),
acadrust_declared: true,
acadrust_compatible: true,
@@ -1045,7 +1045,7 @@ mod tests {
tag: "v2.0.0".to_string(),
api_version: 4,
acadrust_source: Some(
"git+https://github.com/HakanSeven12/cadcodec.git?rev=0908da7#0908da7b6e4f702a6c78359a57f53e2b79cf39eb".to_string(),
"git+https://github.com/HakanSeven12/opencadcodec.git?rev=0908da7#0908da7b6e4f702a6c78359a57f53e2b79cf39eb".to_string(),
),
acadrust_declared: true,
acadrust_compatible: false,
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -241,7 +241,7 @@ pub fn view_window<'a>(
.on_press(Message::ShortcutEditorApply)
.padding([4, 16])
.style(button::primary);
let apply_exit = button(text(t!("Apply && Exit")).size(12))
let apply_exit = button(text(t!("Apply & Exit")).size(12))
.on_press(Message::ShortcutEditorApplyExit)
.padding([4, 16])
.style(button::primary);
@@ -330,7 +330,7 @@ pub fn view_window<'a>(
text(t!("Unsaved changes will be discarded.")).size(14),
Space::new().height(10),
row![
button(text(t!("Discard && close")).size(12))
button(text(t!("Discard & close")).size(12))
.on_press(Message::ShortcutEditorCloseDiscard)
.padding([4, 12])
.style(button::danger),
+1 -1
View File
@@ -2,7 +2,7 @@
use std::fmt;
use acadrust::Handle;
use codec::Handle;
use iced::widget::{
button, column, combo_box, container, pick_list, row, text, text_input, Space,
};
+56 -104
View File
@@ -10,7 +10,8 @@ use crate::io::xref::collect_entries_with_prev;
use crate::io::xref_model::{normalize_lexical, Pathtype, RefKind, RefStatus, RefType, ReferenceEntry};
use crate::ui::ROW_H;
use crate::ui::style::common::muted_style;
use acadrust::CadDocument;
use crate::ui::style::row_button::cell_button_style;
use codec::CadDocument;
use iced::widget::{button, column, container, mouse_area, row, scrollable, text, tooltip};
use iced::Padding;
use iced::{Background, Border, Element, Fill, Length, Theme};
@@ -545,60 +546,12 @@ impl XrefManagerPanel {
missing: usize,
doc: &'a CadDocument,
) -> Element<'a, Message> {
use crate::ui::dock::{DockMsg, PanelId};
// ── Dock chrome (title, pin, close) — matches the block palette ──
let pin_icon = if auto_collapse {
crate::ui::icons::themed_primary_weak_text(crate::ui::icons::PIN, 12.0)
} else {
crate::ui::icons::themed_secondary(crate::ui::icons::PIN, 12.0)
};
let pin = button(pin_icon)
.on_press(Message::Dock(DockMsg::AutoCollapseToggle(
PanelId::ExternalReferences,
)))
.style(move |theme: &Theme, status| {
let mut style = button::subtle(theme, status);
if auto_collapse {
let palette = theme.palette();
style.background = Some(Background::Color(palette.primary.weak.color));
style.text_color = palette.primary.weak.text;
style.border.color = palette.primary.base.color;
style.border.width = 1.0;
}
style
})
.padding([3, 5]);
let pin = tooltip(pin, text("Auto").size(10), tooltip::Position::Bottom).gap(4);
let close = button(crate::ui::icons::themed_secondary(crate::ui::icons::CLOSE, 12.0))
.on_press(Message::Dock(DockMsg::Close(PanelId::ExternalReferences)))
.style(button::subtle)
.padding([3, 5]);
let close = tooltip(close, text("Close").size(10), tooltip::Position::Bottom).gap(4);
let title_bar = mouse_area(
container(
row![
text(format!(
"{} ({})",
crate::t!("External References").as_ref(),
self.entries.len()
))
.size(12),
iced::widget::Space::new().width(Fill),
pin,
close,
]
.spacing(3)
.align_y(iced::Center),
)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
..Default::default()
})
.width(Fill)
.padding([3, 6]),
)
.on_press(Message::Dock(DockMsg::DockGrab(PanelId::ExternalReferences)))
.interaction(iced::mouse::Interaction::Grab);
use crate::ui::dock::PanelId;
let title_bar = crate::ui::dock::title_bar(
PanelId::ExternalReferences,
format!("{} ({})", crate::t!("External References").as_ref(), self.entries.len()),
auto_collapse,
);
// Table content width: column widths plus gutters, stretched to the
// dock when wider so rows fill the panel; narrower docks sidescroll.
let table_w = (width - 16.0).max(self.table_content_width());
@@ -1135,8 +1088,7 @@ impl XrefManagerPanel {
/// directly — mirrors the three [`collect_entries`] scans so nested entries
/// (foreign handles) classify correctly even on handle collisions.
fn direct_identities(doc: &CadDocument) -> HashSet<(u64, String)> {
use acadrust::entities::UnderlayType;
use acadrust::objects::ObjectType;
use codec::objects::ObjectType;
let mut ids: HashSet<(u64, String)> = HashSet::new();
for br in doc.block_records.iter() {
@@ -1150,8 +1102,11 @@ fn direct_identities(doc: &CadDocument) -> HashSet<(u64, String)> {
ids.insert((handle.value(), def.file_name.clone()));
}
ObjectType::UnderlayDefinition(def) => {
if def.underlay_type == UnderlayType::Pdf {
ids.insert((handle.value(), def.file_path.clone()));
ids.insert((handle.value(), def.file_path.clone()));
}
ObjectType::ClassObject(object) => {
if let codec::objects::ClassObjectData::PointCloudDefinitionEx(def) = &object.data {
ids.insert((handle.value(), def.source_filename.clone()));
}
}
_ => {}
@@ -1233,7 +1188,25 @@ fn type_text(entry: &ReferenceEntry) -> std::borrow::Cow<'static, str> {
// Raster images display their file format (spec: type column shows
// the image format); unknown extensions fall back to Image.
RefKind::Image => image_format(&entry.saved_path),
RefKind::Pdf => crate::t!("PDF"),
RefKind::Underlay => {
let ext = std::path::Path::new(&entry.saved_path.replace('\\', "/"))
.extension()
.map(|e| e.to_string_lossy().to_ascii_uppercase())
.unwrap_or_default();
match ext.as_str() {
"DWF" | "DWFX" => crate::t!("DWF"),
"DGN" => crate::t!("DGN"),
_ => crate::t!("PDF"),
}
}
// The file's own kind: RCP (project) or RCS (scan).
RefKind::PointCloud => {
let ext = std::path::Path::new(&entry.saved_path.replace('\\', "/"))
.extension()
.map(|e| e.to_string_lossy().to_ascii_uppercase())
.unwrap_or_default();
if ext == "RCS" { "RCS".into() } else { "RCP".into() }
}
}
}
@@ -1287,27 +1260,6 @@ fn format_date(modified: Option<SystemTime>) -> String {
// ── Widget helpers (layers.rs conventions) ────────────────────────────────
fn row_button_style(selected: bool, index: usize) -> impl Fn(&Theme, button::Status) -> button::Style {
move |theme: &Theme, status: button::Status| {
let palette = theme.palette();
let highlighted = matches!(status, button::Status::Hovered);
let pair = if highlighted {
palette.background.strong
} else if selected {
palette.primary.weak
} else if index % 2 == 0 {
palette.background.base
} else {
palette.background.weak
};
button::Style {
background: highlighted.then_some(Background::Color(pair.color)),
text_color: pair.text,
..Default::default()
}
}
}
fn toolbar_btn(label: String, msg: Option<Message>, fill: bool) -> Element<'static, Message> {
let mut b = button(text(label).size(11))
.style(|theme: &Theme, status| {
@@ -1836,7 +1788,7 @@ fn xref_row<'a>(
let key = entry.key;
let arrow = button(crate::ui::icons::themed_arrow_toggle(is_expanded, 10.0))
.on_press(Message::XrefManagerToggleExpand(key))
.style(row_button_style(is_selected, display.index))
.style(cell_button_style(is_selected, display.index))
.padding(Padding {
top: 6.0,
bottom: 6.0,
@@ -1921,7 +1873,7 @@ fn tree_row(
let key = entry.key;
let arrow = button(crate::ui::icons::themed_arrow_toggle(is_expanded, 10.0))
.on_press(Message::XrefManagerToggleExpand(key))
.style(row_button_style(is_selected, display.index))
.style(cell_button_style(is_selected, display.index))
.padding(Padding {
top: 6.0,
bottom: 6.0,
@@ -2166,7 +2118,7 @@ fn reference_preview(entry: &ReferenceEntry) -> Option<image::RgbaImage> {
let img = image::open(found).ok()?;
Some(img.thumbnail(PREVIEW_MAX, PREVIEW_MAX).to_rgba8())
}
RefKind::Pdf => None,
RefKind::Underlay | RefKind::PointCloud => None,
}
}
@@ -2175,17 +2127,17 @@ fn reference_preview(entry: &ReferenceEntry) -> Option<image::RgbaImage> {
fn find_image_def(
doc: &CadDocument,
key: u64,
) -> Option<&acadrust::objects::ImageDefinition> {
doc.objects.get(&acadrust::types::Handle::from(key)).and_then(|o| match o {
acadrust::objects::ObjectType::ImageDefinition(def) => Some(def),
) -> Option<&codec::objects::ImageDefinition> {
doc.objects.get(&codec::types::Handle::from(key)).and_then(|o| match o {
codec::objects::ObjectType::ImageDefinition(def) => Some(def),
_ => None,
})
}
fn find_image_display_size(doc: &CadDocument, key: u64) -> Option<(f64, f64)> {
let handle = acadrust::types::Handle::from(key);
let handle = codec::types::Handle::from(key);
for entity in doc.entities() {
if let acadrust::EntityType::RasterImage(img) = entity {
if let codec::EntityType::RasterImage(img) = entity {
if img.definition_handle == Some(handle) {
let w = (img.u_vector.x.powi(2) + img.u_vector.y.powi(2) + img.u_vector.z.powi(2)).sqrt();
let h = (img.v_vector.x.powi(2) + img.v_vector.y.powi(2) + img.v_vector.z.powi(2)).sqrt();
@@ -2442,10 +2394,10 @@ mod tests {
assert!(!panel.display_rows().iter().any(|r| r.is_nested));
}
fn preview_doc2(dir: &std::path::Path) -> acadrust::CadDocument {
fn preview_doc2(dir: &std::path::Path) -> codec::CadDocument {
// Two referenced images → genuine multi-select.
use acadrust::objects::{ImageDefinition, ObjectType};
let mut doc = acadrust::CadDocument::new();
use codec::objects::{ImageDefinition, ObjectType};
let mut doc = codec::CadDocument::new();
for (i, name) in ["a.png", "b.png"].iter().enumerate() {
let img = image::RgbaImage::from_pixel(8, 8, image::Rgba([9, 9, 9, 255]));
img.save(dir.join(name)).unwrap();
@@ -2454,37 +2406,37 @@ mod tests {
let mut def = ImageDefinition::with_dimensions(path.clone(), 8, 8);
def.handle = h;
doc.objects.insert(h, ObjectType::ImageDefinition(def));
let mut ent = acadrust::entities::RasterImage::new(
let mut ent = codec::entities::RasterImage::new(
&path,
acadrust::types::Vector3::ZERO,
codec::types::Vector3::ZERO,
8.0 + i as f64,
8.0,
);
ent.definition_handle = Some(h);
doc.add_entity(acadrust::EntityType::RasterImage(ent)).unwrap();
doc.add_entity(codec::EntityType::RasterImage(ent)).unwrap();
}
doc
}
fn preview_doc(dir: &std::path::Path, name: &str) -> acadrust::CadDocument {
fn preview_doc(dir: &std::path::Path, name: &str) -> codec::CadDocument {
// Referenced 8x8 PNG under `dir`; the entry resolves Loaded.
use acadrust::objects::{ImageDefinition, ObjectType};
use codec::objects::{ImageDefinition, ObjectType};
let img = image::RgbaImage::from_pixel(8, 8, image::Rgba([9, 9, 9, 255]));
img.save(dir.join(name)).unwrap();
let img_path = dir.join(name).to_string_lossy().into_owned();
let mut doc = acadrust::CadDocument::new();
let mut doc = codec::CadDocument::new();
let h = doc.allocate_handle();
let mut def = ImageDefinition::with_dimensions(img_path.clone(), 8, 8);
def.handle = h;
doc.objects.insert(h, ObjectType::ImageDefinition(def));
let mut ent = acadrust::entities::RasterImage::new(
let mut ent = codec::entities::RasterImage::new(
&img_path,
acadrust::types::Vector3::ZERO,
codec::types::Vector3::ZERO,
8.0,
8.0,
);
ent.definition_handle = Some(h);
doc.add_entity(acadrust::EntityType::RasterImage(ent)).unwrap();
doc.add_entity(codec::EntityType::RasterImage(ent)).unwrap();
doc
}
@@ -2558,8 +2510,8 @@ mod tests {
));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("plan.dwg"), b"not-a-real-dwg").unwrap();
let mut doc = acadrust::CadDocument::new();
let mut br = acadrust::tables::BlockRecord::new("PLAN");
let mut doc = codec::CadDocument::new();
let mut br = codec::tables::BlockRecord::new("PLAN");
br.flags.is_xref = true;
br.xref_path = dir.join("plan.dwg").to_string_lossy().into_owned();
br.handle = doc.allocate_handle();
@@ -2596,7 +2548,7 @@ mod tests {
.objects
.iter()
.find_map(|(h, o)| match o {
acadrust::objects::ObjectType::ImageDefinition(_) => Some(h.value()),
codec::objects::ObjectType::ImageDefinition(_) => Some(h.value()),
_ => None,
})
.unwrap();