Merge pull request #1751 from ramox81/codex/adjust-parity

ADJUST: images and underlays, -IMAGEADJUST and PDF/DWF/DGNADJUST
This commit is contained in:
Hakan Seven committed 2026-10-14 08:42:35 +03:00
commit f6fc244430
33 files changed
+903 -312

No files matched your search

+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"]
});