forked from 1b-agent/opencadstudio-upstream
Merge pull request #1751 from ramox81/codex/adjust-parity
ADJUST: images and underlays, -IMAGEADJUST and PDF/DWF/DGNADJUST
This commit is contained in:
commit
f6fc244430
33 files changed
+903
-312
No files matched your search
@@ -41,6 +41,9 @@ pub struct SelectObjectsCommand {
|
||||
/// The command works on associative dimensions only; the host drops
|
||||
/// everything else from each completed selection.
|
||||
associative_dimensions_only: bool,
|
||||
/// The kinds of object the command takes; the host drops the rest of
|
||||
/// each completed selection silently, and Enter on nothing ends quietly.
|
||||
keep: Option<fn(&codec::EntityType) -> bool>,
|
||||
}
|
||||
|
||||
impl SelectObjectsCommand {
|
||||
@@ -57,6 +60,7 @@ impl SelectObjectsCommand {
|
||||
auto_constrain_settings: false,
|
||||
prompt_text: None,
|
||||
associative_dimensions_only: false,
|
||||
keep: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +78,7 @@ impl SelectObjectsCommand {
|
||||
auto_constrain_settings: false,
|
||||
prompt_text: None,
|
||||
associative_dimensions_only: false,
|
||||
keep: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +96,7 @@ impl SelectObjectsCommand {
|
||||
auto_constrain_settings: false,
|
||||
prompt_text: None,
|
||||
associative_dimensions_only: false,
|
||||
keep: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +114,7 @@ impl SelectObjectsCommand {
|
||||
auto_constrain_settings: false,
|
||||
prompt_text: None,
|
||||
associative_dimensions_only: false,
|
||||
keep: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +138,18 @@ impl SelectObjectsCommand {
|
||||
command
|
||||
}
|
||||
|
||||
/// The command's own wording, keeping only what `keep` accepts.
|
||||
pub fn filtered(
|
||||
prompt_cmd: &str,
|
||||
pending_cmd: &str,
|
||||
prompt_text: &'static str,
|
||||
keep: fn(&codec::EntityType) -> bool,
|
||||
) -> Self {
|
||||
let mut command = Self::with_prompt(prompt_cmd, pending_cmd, prompt_text);
|
||||
command.keep = Some(keep);
|
||||
command
|
||||
}
|
||||
|
||||
pub fn auto_constrain(pending_cmd: &str) -> Self {
|
||||
let mut command = Self::new(pending_cmd);
|
||||
command.auto_constrain_settings = true;
|
||||
@@ -203,6 +222,10 @@ impl CadCommand for SelectObjectsCommand {
|
||||
self.associative_dimensions_only
|
||||
}
|
||||
|
||||
fn selection_entity_filter(&self) -> Option<fn(&codec::EntityType) -> bool> {
|
||||
self.keep
|
||||
}
|
||||
|
||||
fn selection_drops_locked_point_clouds(&self) -> bool {
|
||||
matches!(
|
||||
self.pending_cmd.as_str(),
|
||||
@@ -320,6 +343,9 @@ impl CadCommand for SelectObjectsCommand {
|
||||
}
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
if self.keep.is_some() && self.handles.is_empty() {
|
||||
return CmdResult::Relaunch(std::mem::take(&mut self.pending_cmd), Vec::new());
|
||||
}
|
||||
if !self.commit_on_enter || self.handles.is_empty() {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ mod snap_underlays;
|
||||
pub(crate) mod solid3d_cmds;
|
||||
mod underlay_layers;
|
||||
pub(crate) mod wblock;
|
||||
mod xadjust;
|
||||
pub(crate) mod xadjust;
|
||||
pub(crate) mod xattach;
|
||||
pub(crate) mod xclip;
|
||||
pub(crate) mod xref_cmd;
|
||||
|
||||
@@ -1,4 +1,30 @@
|
||||
// ADJUST / -IMAGEADJUST / PDFADJUST / DWFADJUST / DGNADJUST — brightness,
|
||||
// contrast and fade of raster images; fade, contrast and monochrome of
|
||||
// PDF, DWF and DGN underlays.
|
||||
//
|
||||
// Select image or underlay:
|
||||
// Enter image option [Contrast/Fade/Brightness] <Brightness>:
|
||||
// Enter brightness value (0-100) <50>:
|
||||
//
|
||||
// Enter PDF underlay option [Fade/Contrast/Monochrome] <Fade>:
|
||||
// Enter fade value (0-100) <25>:
|
||||
// Monochrome? [Yes/No] <No>:
|
||||
//
|
||||
// An image value prompt offers the first image's own value. The underlay
|
||||
// option and the fade / contrast values offered are the last ones used in
|
||||
// the session, whatever the underlay kind. The host applies the value to
|
||||
// every chosen object as one undo step.
|
||||
|
||||
use std::sync::Mutex;
|
||||
|
||||
use codec::entities::{UnderlayDisplayFlags, UnderlayType};
|
||||
use codec::types::Handle;
|
||||
use codec::EntityType;
|
||||
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::t;
|
||||
|
||||
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/xadjust.svg"));
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
@@ -8,3 +34,330 @@ pub fn tool() -> ToolDef {
|
||||
event: ModuleEvent::Command("ADJUST".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// What a command takes: `A` images or underlays, `I` images, `P` / `W` /
|
||||
/// `G` PDF / DWF / DGN underlays.
|
||||
pub fn keeps(mode: &str) -> fn(&EntityType) -> bool {
|
||||
fn any(e: &EntityType) -> bool {
|
||||
matches!(e, EntityType::RasterImage(_) | EntityType::Underlay(_))
|
||||
}
|
||||
fn image(e: &EntityType) -> bool {
|
||||
matches!(e, EntityType::RasterImage(_))
|
||||
}
|
||||
fn pdf(e: &EntityType) -> bool {
|
||||
matches!(e, EntityType::Underlay(u) if u.underlay_type == UnderlayType::Pdf)
|
||||
}
|
||||
fn dwf(e: &EntityType) -> bool {
|
||||
matches!(e, EntityType::Underlay(u) if u.underlay_type == UnderlayType::Dwf)
|
||||
}
|
||||
fn dgn(e: &EntityType) -> bool {
|
||||
matches!(e, EntityType::Underlay(u) if u.underlay_type == UnderlayType::Dgn)
|
||||
}
|
||||
match mode {
|
||||
"I" => image,
|
||||
"P" => pdf,
|
||||
"W" => dwf,
|
||||
"G" => dgn,
|
||||
_ => any,
|
||||
}
|
||||
}
|
||||
|
||||
/// The command each mode belongs to.
|
||||
pub fn command_name(mode: &str) -> &'static str {
|
||||
match mode {
|
||||
"I" => "-IMAGEADJUST",
|
||||
"P" => "PDFADJUST",
|
||||
"W" => "DWFADJUST",
|
||||
"G" => "DGNADJUST",
|
||||
_ => "ADJUST",
|
||||
}
|
||||
}
|
||||
|
||||
/// The selection prompt of each command.
|
||||
pub fn select_prompt(mode: &str) -> &'static str {
|
||||
match mode {
|
||||
"I" => "Select image(s):",
|
||||
"P" => "Select PDF underlay:",
|
||||
"W" => "Select DWF underlay:",
|
||||
"G" => "Select DGN underlay:",
|
||||
_ => "Select image or underlay:",
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Prop {
|
||||
Brightness,
|
||||
Contrast,
|
||||
Fade,
|
||||
Monochrome,
|
||||
}
|
||||
|
||||
impl Prop {
|
||||
fn label(self) -> &'static str {
|
||||
match self {
|
||||
Prop::Brightness => "Brightness",
|
||||
Prop::Contrast => "Contrast",
|
||||
Prop::Fade => "Fade",
|
||||
Prop::Monochrome => "Monochrome",
|
||||
}
|
||||
}
|
||||
fn key(self) -> &'static str {
|
||||
match self {
|
||||
Prop::Brightness => "BRIGHTNESS",
|
||||
Prop::Contrast => "CONTRAST",
|
||||
Prop::Fade => "FADE",
|
||||
Prop::Monochrome => "MONOCHROME",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const IMAGE_OPTIONS: [Prop; 3] = [Prop::Contrast, Prop::Fade, Prop::Brightness];
|
||||
const UNDERLAY_OPTIONS: [Prop; 3] = [Prop::Fade, Prop::Contrast, Prop::Monochrome];
|
||||
|
||||
/// The underlay option, fade and contrast last used in the session.
|
||||
static LAST_UNDERLAY: Mutex<(usize, u8, u8)> = Mutex::new((0, 25, 75));
|
||||
|
||||
fn last_underlay() -> (usize, u8, u8) {
|
||||
LAST_UNDERLAY.lock().map(|v| *v).unwrap_or((0, 25, 75))
|
||||
}
|
||||
|
||||
enum Subject {
|
||||
Image {
|
||||
brightness: u8,
|
||||
contrast: u8,
|
||||
fade: u8,
|
||||
},
|
||||
Underlay {
|
||||
kind: &'static str,
|
||||
monochrome: bool,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct AdjustCommand {
|
||||
handles: Vec<Handle>,
|
||||
subject: Subject,
|
||||
prop: Option<Prop>,
|
||||
}
|
||||
|
||||
/// Why a chosen set cannot be adjusted together.
|
||||
pub const MIXED: &str =
|
||||
"*Invalid selection set. Select underlay objects of the same file type only.*";
|
||||
|
||||
impl AdjustCommand {
|
||||
/// The command for the chosen objects (the first one supplies the image
|
||||
/// values and the monochrome state), or the reason they do not go
|
||||
/// together. `None` when nothing adjustable was chosen.
|
||||
pub fn for_objects(objects: &[(Handle, EntityType)]) -> Option<Result<Self, &'static str>> {
|
||||
let (_, first) = objects.first()?;
|
||||
let kind = |e: &EntityType| match e {
|
||||
EntityType::Underlay(u) => Some(u.underlay_type),
|
||||
_ => None,
|
||||
};
|
||||
if objects.iter().any(|(_, e)| kind(e) != kind(first)) {
|
||||
return Some(Err(MIXED));
|
||||
}
|
||||
let subject = match first {
|
||||
EntityType::RasterImage(img) => Subject::Image {
|
||||
brightness: img.brightness,
|
||||
contrast: img.contrast,
|
||||
fade: img.fade,
|
||||
},
|
||||
EntityType::Underlay(u) => Subject::Underlay {
|
||||
kind: match u.underlay_type {
|
||||
UnderlayType::Pdf => "PDF",
|
||||
UnderlayType::Dwf => "DWF",
|
||||
UnderlayType::Dgn => "DGN",
|
||||
},
|
||||
monochrome: u.flags.contains(UnderlayDisplayFlags::MONOCHROME),
|
||||
},
|
||||
_ => return None,
|
||||
};
|
||||
Some(Ok(Self {
|
||||
handles: objects.iter().map(|(h, _)| *h).collect(),
|
||||
subject,
|
||||
prop: None,
|
||||
}))
|
||||
}
|
||||
|
||||
fn options_list(&self) -> &'static [Prop; 3] {
|
||||
match self.subject {
|
||||
Subject::Image { .. } => &IMAGE_OPTIONS,
|
||||
Subject::Underlay { .. } => &UNDERLAY_OPTIONS,
|
||||
}
|
||||
}
|
||||
|
||||
fn default_option(&self) -> Prop {
|
||||
match self.subject {
|
||||
Subject::Image { .. } => Prop::Brightness,
|
||||
Subject::Underlay { .. } => UNDERLAY_OPTIONS[last_underlay().0.min(2)],
|
||||
}
|
||||
}
|
||||
|
||||
fn default_value(&self, prop: Prop) -> u8 {
|
||||
match (&self.subject, prop) {
|
||||
(Subject::Image { brightness, .. }, Prop::Brightness) => *brightness,
|
||||
(Subject::Image { contrast, .. }, Prop::Contrast) => *contrast,
|
||||
(Subject::Image { fade, .. }, _) => *fade,
|
||||
(Subject::Underlay { .. }, Prop::Contrast) => last_underlay().2,
|
||||
(Subject::Underlay { .. }, _) => last_underlay().1,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply(&self, prop: Prop, value: u8) -> CmdResult {
|
||||
if let Subject::Underlay { .. } = self.subject {
|
||||
if let Ok(mut last) = LAST_UNDERLAY.lock() {
|
||||
last.0 = UNDERLAY_OPTIONS.iter().position(|p| *p == prop).unwrap_or(0);
|
||||
match prop {
|
||||
Prop::Fade => last.1 = value,
|
||||
Prop::Contrast => last.2 = value,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
let handles: Vec<String> = self
|
||||
.handles
|
||||
.iter()
|
||||
.map(|h| format!("{:X}", h.value()))
|
||||
.collect();
|
||||
CmdResult::Dispatch(format!(
|
||||
"_ADJUSTAPPLY {} {} {}",
|
||||
prop.key(),
|
||||
value,
|
||||
handles.join(" ")
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `text` names `label` (its key letter or a longer prefix, in
|
||||
/// English or in the interface language).
|
||||
fn names(text: &str, label: &str) -> bool {
|
||||
let english = label.to_uppercase();
|
||||
let local = t!(label).to_uppercase();
|
||||
!text.is_empty() && (english.starts_with(text) || local.starts_with(text))
|
||||
}
|
||||
|
||||
impl CadCommand for AdjustCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"ADJUST"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match (self.prop, &self.subject) {
|
||||
(None, Subject::Image { .. }) => {
|
||||
t!("Enter image option [Contrast/Fade/Brightness] <Brightness>:").into_owned()
|
||||
}
|
||||
(None, Subject::Underlay { kind, .. }) => t!(
|
||||
"Enter %{kind} underlay option [Fade/Contrast/Monochrome] <%{option}>:",
|
||||
kind = kind,
|
||||
option = t!(self.default_option().label())
|
||||
)
|
||||
.into_owned(),
|
||||
(Some(Prop::Monochrome), Subject::Underlay { monochrome, .. }) => t!(
|
||||
"Monochrome? [Yes/No] <%{value}>:",
|
||||
value = if *monochrome { t!("Yes") } else { t!("No") }
|
||||
)
|
||||
.into_owned(),
|
||||
(Some(Prop::Brightness), _) => t!(
|
||||
"Enter brightness value (0-100) <%{value}>:",
|
||||
value = self.default_value(Prop::Brightness)
|
||||
)
|
||||
.into_owned(),
|
||||
(Some(Prop::Contrast), _) => t!(
|
||||
"Enter contrast value (0-100) <%{value}>:",
|
||||
value = self.default_value(Prop::Contrast)
|
||||
)
|
||||
.into_owned(),
|
||||
(Some(_), _) => t!(
|
||||
"Enter fade value (0-100) <%{value}>:",
|
||||
value = self.default_value(Prop::Fade)
|
||||
)
|
||||
.into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
match self.prop {
|
||||
None => self
|
||||
.options_list()
|
||||
.iter()
|
||||
.map(|p| CmdOption::new(p.label(), &p.label()[..1]))
|
||||
.collect(),
|
||||
Some(Prop::Monochrome) => vec![CmdOption::new("Yes", "Y"), CmdOption::new("No", "N")],
|
||||
Some(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn input_kind(&self) -> InputKind {
|
||||
InputKind::SingleToken
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let text = text.trim().to_uppercase();
|
||||
let invalid = || {
|
||||
Some(CmdResult::ReportError(
|
||||
t!("Invalid option keyword.").into_owned(),
|
||||
))
|
||||
};
|
||||
match self.prop {
|
||||
None => {
|
||||
let chosen = if text.is_empty() {
|
||||
Some(self.default_option())
|
||||
} else {
|
||||
self.options_list()
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|p| names(&text, p.label()))
|
||||
};
|
||||
let Some(prop) = chosen else {
|
||||
return invalid();
|
||||
};
|
||||
self.prop = Some(prop);
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Some(Prop::Monochrome) => {
|
||||
let Subject::Underlay { monochrome, .. } = self.subject else {
|
||||
return Some(CmdResult::Cancel);
|
||||
};
|
||||
let on = if text.is_empty() {
|
||||
monochrome
|
||||
} else if names(&text, "Yes") {
|
||||
true
|
||||
} else if names(&text, "No") {
|
||||
false
|
||||
} else {
|
||||
return invalid();
|
||||
};
|
||||
Some(self.apply(Prop::Monochrome, on as u8))
|
||||
}
|
||||
Some(prop) => {
|
||||
let value = if text.is_empty() {
|
||||
Some(self.default_value(prop))
|
||||
} else {
|
||||
text.parse::<u8>().ok().filter(|v| *v <= 100)
|
||||
};
|
||||
match (value, &self.subject) {
|
||||
(Some(v), _) => Some(self.apply(prop, v)),
|
||||
(None, Subject::Image { .. }) => Some(CmdResult::ReportError(
|
||||
t!("Value must be an integer between 0 and 100.").into_owned(),
|
||||
)),
|
||||
(None, Subject::Underlay { .. }) => Some(CmdResult::ReportError(
|
||||
crate::tf!("Requires an integer between {min} and {max}.", min = 0, max = 100)
|
||||
.into_owned(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: glam::DVec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
self.on_text_input("").unwrap_or(CmdResult::Cancel)
|
||||
}
|
||||
}
|
||||
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &["-IMAGEADJUST", "PDFADJUST", "DWFADJUST", "DGNADJUST"]
|
||||
});
|
||||
Reference in new issue
Block a user