ADJUST: images and underlays, -IMAGEADJUST and PDF/DWF/DGNADJUST

ADJUST gathers raster images or PDF/DWF/DGN underlays (other objects are
dropped silently, pickfirst is used, a mixed set is refused), then asks the
image option (Brightness default) or the underlay option (last used), with
value prompts that offer the image's own value or the session's last
underlay fade/contrast, and Monochrome Yes/No. Invalid values re-prompt.
One undo step. -IMAGEADJUST, PDFADJUST, DWFADJUST and DGNADJUST take one
kind each. Raster image Brightness/Contrast/Fade move to an Image Adjust
section as whole numbers with 0-100 validation.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-10-13 15:53:20 +03:00
1 parent b1d4c9bd9b
commit a0a83ad681
32 files changed
+841 -311

No files matched your search

+27
View File
@@ -507,6 +507,32 @@ impl OpenCADStudio {
StepInput::SelectionComplete(kept)
}
/// Keeps only the kinds of object the active command takes, dropping
/// the rest of a completed selection silently.
fn filter_command_entity_selection(&mut self, input: StepInput) -> StepInput {
let StepInput::SelectionComplete(handles) = input else {
return input;
};
let i = self.active_tab;
let Some(keep) = self.tabs[i]
.active_cmd
.as_ref()
.and_then(|command| command.selection_entity_filter())
else {
return StepInput::SelectionComplete(handles);
};
let (kept, dropped): (Vec<Handle>, Vec<Handle>) = handles
.into_iter()
.partition(|h| self.tabs[i].scene.document.get_entity(*h).is_some_and(keep));
for handle in &dropped {
self.tabs[i].scene.deselect_entity(*handle);
}
if !dropped.is_empty() {
self.refresh_properties();
}
StepInput::SelectionComplete(kept)
}
/// Drops locked point clouds from a completed selection when the active
/// command would move them, saying which were locked and how many went.
fn filter_locked_point_cloud_selection(&mut self, input: StepInput) -> StepInput {
@@ -562,6 +588,7 @@ impl OpenCADStudio {
let input = self.filter_associative_dimension_selection(input);
let input = self.filter_block_reference_selection(input);
let input = self.filter_locked_point_cloud_selection(input);
let input = self.filter_command_entity_selection(input);
// XCLIP / CLIP learn which chosen references carry a clip already.
{
let i = self.active_tab;
+130 -91
View File
@@ -1381,101 +1381,120 @@ impl OpenCADStudio {
return Some(Task::none());
}
// ADJUST — set brightness / contrast / fade on selected raster images
// ADJUST BRIGHTNESS|CONTRAST|FADE <0-100>
"ADJUST" => {
use crate::command::SelectThenKeywordCommand;
let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
let c = SelectThenKeywordCommand::new(
"ADJUST",
"ADJUST [Brightness / Contrast / Fade]:",
vec![
(
"Brightness",
"BRIGHTNESS",
Some("ADJUST brightness 0-100:"),
),
("Contrast", "CONTRAST", Some("ADJUST contrast 0-100:")),
("Fade", "FADE", Some("ADJUST fade 0-100:")),
],
has_sel,
);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("ADJUST ") => {
let rest = cmd.trim_start_matches("ADJUST").trim();
let parts: Vec<&str> = rest.splitn(2, char::is_whitespace).collect();
let action = parts.first().map(|s| s.to_uppercase()).unwrap_or_default();
let arg = parts.get(1).copied().unwrap_or("").trim();
let handles: Vec<codec::Handle> = self.tabs[i]
// ADJUST / -IMAGEADJUST / PDFADJUST / DWFADJUST / DGNADJUST:
// chosen images or underlays (pickfirst first), then the option
// and the value.
"ADJUST" | "-IMAGEADJUST" | "PDFADJUST" | "DWFADJUST" | "DGNADJUST" => {
let mode = match cmd {
"-IMAGEADJUST" => "I",
"PDFADJUST" => "P",
"DWFADJUST" => "W",
"DGNADJUST" => "G",
_ => "A",
};
let keep = crate::modules::insert::xadjust::keeps(mode);
let chosen: Vec<codec::Handle> = self.tabs[i]
.scene
.selected_entities()
.iter()
.map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.into_iter()
.filter(|(_, e)| keep(e))
.map(|(h, _)| h)
.collect();
if handles.is_empty() {
self.command_line
.push_error(crate::t!("ADJUST: select raster image(s) first.").as_ref());
} else if action.is_empty() {
self.command_line.push_info(
crate::t!("Usage: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100>").as_ref(),
);
} else if let Ok(v) = arg.parse::<u8>() {
let v = v.min(100);
self.push_undo_snapshot(i, "ADJUST");
let mut changed = 0usize;
let mut changed_handles = Vec::new();
for h in &handles {
if let Some(codec::EntityType::RasterImage(img)) = self.tabs[i]
.scene
.document
.entities_mut()
.find(|e| e.common().handle == *h)
{
match action.as_str() {
"BRIGHTNESS" => {
img.brightness = v;
changed += 1;
changed_handles.push(*h);
}
"CONTRAST" => {
img.contrast = v;
changed += 1;
changed_handles.push(*h);
}
"FADE" => {
img.fade = v;
changed += 1;
changed_handles.push(*h);
}
_ => {}
}
}
}
if changed > 0 {
self.tabs[i].dirty = true;
for &handle in &changed_handles {
self.tabs[i].scene.reseed_derived_caches(handle);
}
let changes: Vec<_> = changed_handles
.into_iter()
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
.collect();
self.tabs[i].scene.bump_entities(&changes);
self.command_line.push_output(
crate::tf!("ADJUST: {action} = {v} on {changed} image(s).").as_ref(),
);
} else {
self.command_line.push_error(
crate::t!("ADJUST: no raster images selected, or unknown property (use BRIGHTNESS|CONTRAST|FADE).").as_ref(),
);
}
} else {
self.command_line
.push_error(crate::t!("ADJUST: value must be 0-100.").as_ref());
if !chosen.is_empty() {
self.tabs[i]
.scene
.replace_selection(chosen.into_iter().collect());
return Some(self.dispatch_command(&format!("_ADJUSTSEL {mode}")));
}
self.start_adjust_selection(i, cmd, mode);
}
// The gathered objects: one kind at a time, then the option.
c if c.starts_with("_ADJUSTSEL ") => {
use crate::command::CadCommand;
use crate::modules::insert::xadjust::{self, AdjustCommand};
let mode = c["_ADJUSTSEL ".len()..].trim().to_string();
let keep = xadjust::keeps(&mode);
let name = xadjust::command_name(&mode);
let objects: Vec<(codec::Handle, codec::EntityType)> = self.tabs[i]
.scene
.selected_entities()
.into_iter()
.filter(|(h, e)| keep(e) && !self.tabs[i].scene.is_layer_locked(*h))
.map(|(h, e)| (h, e.clone()))
.collect();
match AdjustCommand::for_objects(&objects) {
None => {}
Some(Err(message)) => {
self.command_line.push_error(crate::t!(message).as_ref());
self.start_adjust_selection(i, name, &mode);
}
Some(Ok(command)) => {
self.command_line.push_info(&command.prompt());
self.command_line.set_step_options(command.options());
self.tabs[i].active_cmd = Some(Box::new(command));
}
}
// Repeating runs the command, not this step.
return Some(self.finish_dispatch(name));
}
// `_ADJUSTAPPLY <BRIGHTNESS|CONTRAST|FADE|MONOCHROME> <value> <handles…>`
// writes the value to every listed image or underlay, one undo step.
c if c.starts_with("_ADJUSTAPPLY ") => {
let mut parts = c.split_whitespace().skip(1);
let prop = parts.next().unwrap_or("").to_string();
let value: u8 = parts.next().and_then(|v| v.parse().ok()).unwrap_or(0);
let handles: Vec<codec::Handle> = parts
.filter_map(|h| u64::from_str_radix(h, 16).ok())
.map(codec::Handle::new)
.collect();
self.push_undo_snapshot(i, "ADJUST");
let mut changed = Vec::new();
for h in &handles {
let done = match self.tabs[i].scene.document.get_entity_mut(*h) {
Some(codec::EntityType::RasterImage(img)) => match prop.as_str() {
"BRIGHTNESS" => {
img.brightness = value;
true
}
"CONTRAST" => {
img.contrast = value;
true
}
"FADE" => {
img.fade = value;
true
}
_ => false,
},
Some(codec::EntityType::Underlay(u)) => match prop.as_str() {
"CONTRAST" => {
u.set_contrast(value);
true
}
"FADE" => {
u.set_fade(value);
true
}
"MONOCHROME" => {
u.set_monochrome(value != 0);
true
}
_ => false,
},
_ => false,
};
if done {
self.tabs[i].scene.reseed_derived_caches(*h);
changed.push((*h, crate::scene::ChangeKind::Modified));
}
}
if !changed.is_empty() {
self.tabs[i].scene.bump_entities(&changed);
self.tabs[i].dirty = true;
}
// The command used the set up; nothing stays chosen.
self.tabs[i].scene.deselect_all();
self.refresh_properties();
}
// ANNOSCALE / CANNOSCALE <ratio> — set the current annotation scale
@@ -2206,6 +2225,26 @@ pub(crate) fn table_to_csv(table: &codec::entities::Table) -> String {
.join("\r\n")
}
impl OpenCADStudio {
/// The ADJUST family's object prompt, keeping only the kinds `mode`
/// takes (see `xadjust::keeps`).
fn start_adjust_selection(&mut self, i: usize, name: &str, mode: &str) {
use crate::command::CadCommand;
use crate::modules::insert::xadjust;
self.tabs[i].scene.deselect_all();
self.refresh_properties();
let command = crate::modules::draw::select::SelectObjectsCommand::filtered(
name,
&format!("_ADJUSTSEL {mode}"),
xadjust::select_prompt(mode),
xadjust::keeps(mode),
);
self.command_line.push_info(&command.prompt());
self.command_line.set_step_options(command.options());
self.tabs[i].active_cmd = Some(Box::new(command));
}
}
#[cfg(test)]
mod tests {
use crate::app::OpenCADStudio;
+3 -1
View File
@@ -3809,7 +3809,9 @@ impl OpenCADStudio {
return Task::none();
}
}
if field.starts_with("ul_") {
if field.starts_with("ul_")
|| matches!(field, "ri_bright" | "ri_contrast" | "ri_fade")
{
if let Err(message) = crate::entities::underlay::validate_property(field, &val)
{
self.command_line.push_error(message);
+6
View File
@@ -2960,6 +2960,12 @@ pub trait CadCommand: Send {
false
}
/// The kinds of object the command takes (ADJUST): the host drops the
/// rest of each completed selection without a word.
fn selection_entity_filter(&self) -> Option<fn(&EntityType) -> bool> {
None
}
/// The command takes block references only (XCLIP): the host drops the
/// rest and says how many were ineligible.
fn selection_keeps_block_references(&self) -> bool {
+17 -10
View File
@@ -333,7 +333,20 @@ impl PropertyEditable for RasterImage {
.contains(codec::entities::ImageDisplayFlags::USE_CLIPPING_BOUNDARY);
let clip_inverted = self.clip_boundary.clip_mode == codec::entities::ClipMode::Inside;
let transparency = self.common.transparency.to_string();
let whole = |label: &str, field: &'static str, value: u8| Property {
label: t!(label).into_owned(),
field,
value: PropValue::EditText(value.to_string()),
};
vec![
PropSection {
title: t!("Image Adjust").into_owned(),
props: vec![
whole("Brightness", "ri_bright", self.brightness),
whole("Contrast", "ri_contrast", self.contrast),
whole("Fade", "ri_fade", self.fade),
],
},
PropSection {
title: t!("Geometry").into_owned(),
props: vec![
@@ -362,13 +375,6 @@ impl PropertyEditable for RasterImage {
title: t!("Misc").into_owned(),
props: vec![
ro(t!("Name").as_ref(), "ri_name", self.file_name().to_string()),
edit(
t!("Brightness").as_ref(),
"ri_bright",
self.brightness as f64,
),
edit(t!("Contrast").as_ref(), "ri_contrast", self.contrast as f64),
edit(t!("Fade").as_ref(), "ri_fade", self.fade as f64),
ro(t!("Transparency").as_ref(), "ri_transparency", transparency),
Property {
label: t!("Show image").into_owned(),
@@ -449,9 +455,10 @@ impl PropertyEditable for RasterImage {
"ri_ox" => self.insertion_point.x = v,
"ri_oy" => self.insertion_point.y = v,
"ri_oz" => self.insertion_point.z = v,
"ri_bright" => self.brightness = v.clamp(0.0, 100.0) as u8,
"ri_contrast" => self.contrast = v.clamp(0.0, 100.0) as u8,
"ri_fade" => self.fade = v.clamp(0.0, 100.0) as u8,
// Out-of-range values are refused before they get here.
"ri_bright" if (0.0..=100.0).contains(&v) => self.brightness = v as u8,
"ri_contrast" if (0.0..=100.0).contains(&v) => self.contrast = v as u8,
"ri_fade" if (0.0..=100.0).contains(&v) => self.fade = v as u8,
_ => {}
}
}
+2 -1
View File
@@ -227,7 +227,8 @@ fn parse_flag(value: &str, current: bool) -> bool {
pub(crate) fn validate_property(field: &str, value: &str) -> Result<(), &'static str> {
let number = || value.trim().parse::<f64>().ok();
match field {
"ul_contrast" | "ul_fade" => match number() {
// A raster image's Image Adjust rows take the same range.
"ul_contrast" | "ul_fade" | "ri_bright" | "ri_contrast" | "ri_fade" => match number() {
Some(v) if (0.0..=100.0).contains(&v) && v.fract() == 0.0 => Ok(()),
_ => Err("Value must be an integer between 0 and 100"),
},
+12 -9
View File
@@ -557,13 +557,6 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"ACI:" => Some(("styles", "aci")),
"ADDSELECTED: creating a new {kind} is not supported." => Some(("inquiry", "addselected-creating-a-new-kind-is-not-support")),
"ADDSELECTED: selected object not found." => Some(("inquiry", "addselected-selected-object-not-found")),
"ADJUST [Brightness / Contrast / Fade]:" => Some(("view", "adjust-brightness-contrast-fade")),
"ADJUST brightness 0-100:" => Some(("view", "adjust-brightness-0-100")),
"ADJUST contrast 0-100:" => Some(("view", "adjust-contrast-0-100")),
"ADJUST fade 0-100:" => Some(("view", "adjust-fade-0-100")),
"ADJUST: select raster image(s) first." => Some(("view", "adjust-select-raster-image-s-first")),
"ADJUST: value must be 0-100." => Some(("view", "adjust-value-must-be-0-100")),
"ADJUST: {action} = {v} on {changed} image(s)." => Some(("view", "adjust-action-v-on-changed-image-s")),
"ALIGN Scale objects based on alignment points? [Y/N]:" => Some(("modify", "align-scale-objects-based-on-alignment-points")),
"ALIGN Select objects (%{count} selected, Enter when done):" => Some(("modify", "align-select-objects-count-selected-enter-when")),
"ALIGN Specify 1st destination point:" => Some(("modify", "align-specify-1st-destination-point")),
@@ -3487,7 +3480,6 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"Updated {changed} underlay(s)." => Some(("view", "updated-changed-underlay-s")),
"Upper" => Some(("annotate", "upper")),
"Upside down" => Some(("common", "upside-down")),
"Usage: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100>" => Some(("view", "usage-adjust-brightness-contrast-fade-0-100")),
"Usage: ANNOSCALE <ratio> e.g. 1:50, 2:1, or a factor" => Some(("view", "usage-annoscale-ratio-e-g-1-50-2-1-or-a-factor")),
"Usage: ATTDISP ON | OFF | NORMAL" => Some(("draw", "usage-attdisp-on-off-normal")),
"Usage: ATTSYNC <block name>. Blocks: {}" => Some(("insert", "usage-attsync-block-name-blocks")),
@@ -3748,6 +3740,18 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"Attach DWG" => Some(("insert", "xref-attach-dwg")),
"Attach Image" => Some(("insert", "xref-attach-image")),
"Select image(s):" => Some(("insert", "select-images")),
"Select image or underlay:" => Some(("insert", "adjust-select-image-or-underlay")),
"Select PDF underlay:" => Some(("insert", "adjust-select-pdf-underlay")),
"Select DWF underlay:" => Some(("insert", "adjust-select-dwf-underlay")),
"Select DGN underlay:" => Some(("insert", "adjust-select-dgn-underlay")),
"*Invalid selection set. Select underlay objects of the same file type only.*" => Some(("insert", "adjust-mixed-underlays")),
"Enter image option [Contrast/Fade/Brightness] <Brightness>:" => Some(("insert", "adjust-image-option")),
"Enter %{kind} underlay option [Fade/Contrast/Monochrome] <%{option}>:" => Some(("insert", "adjust-underlay-option")),
"Monochrome? [Yes/No] <%{value}>:" => Some(("insert", "adjust-monochrome")),
"Enter brightness value (0-100) <%{value}>:" => Some(("insert", "adjust-brightness-value")),
"Enter contrast value (0-100) <%{value}>:" => Some(("insert", "adjust-contrast-value")),
"Enter fade value (0-100) <%{value}>:" => Some(("insert", "adjust-fade-value")),
"Value must be an integer between 0 and 100." => Some(("insert", "adjust-value-integer-0-100")),
"Enter transparency mode [ON/OFF] <ON>:" => Some(("insert", "transparency-mode-on")),
"Enter transparency mode [ON/OFF] <OFF>:" => Some(("insert", "transparency-mode-off")),
"Attach..." => Some(("insert", "xref-attach-menu")),
@@ -4127,7 +4131,6 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"ISOPLANE: expected Left, Top, or Right." => Some(("common", "isoplane-expected-left-top-or-right")),
"ISODRAFT: expected On or Off." => Some(("common", "isodraft-expected-on-or-off")),
"XDATA: selected entities are on locked layers." => Some(("common", "xdata-selected-entities-are-on-locked-layers")),
"ADJUST: no raster images selected, or unknown property (use BRIGHTNESS|CONTRAST|FADE)." => Some(("common", "adjust-no-raster-images-selected-or-unknown-property-use-brightness-contrast-fade")),
"ANNOAUTOSCALE: enter an integer from -4 through 4." => Some(("common", "annoautoscale-enter-an-integer-from-4-through-4")),
"Usage: DATALINK <path-to-.csv>" => Some(("common", "usage-datalink-path-to-csv")),
"DATALINKUPDATE: no linked tables found." => Some(("common", "datalinkupdate-no-linked-tables-found")),
+26
View File
@@ -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;
}
+1 -1
View File
@@ -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;
+353
View File
@@ -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"]
});