#![cfg_attr(test, allow(clippy::default_trait_access, clippy::unwrap_used))]
mod commands;
mod inspect;
mod loader;
mod registry;
use std::path::PathBuf;
use std::process::ExitCode;
use clap::{Args, Parser, Subcommand, ValueEnum};
use crate::registry::Registry;
#[derive(Debug, Clone, Copy, Default, ValueEnum)]
enum StepTarget {
Ap203e1,
Ap203e2,
#[default]
Ap214,
Ap242e1,
Ap242e2,
Ap242e3,
}
impl StepTarget {
fn schema(self) -> cadmpeg_step::StepSchema {
match self {
Self::Ap203e1 => cadmpeg_step::StepSchema::Ap203Edition1,
Self::Ap203e2 => cadmpeg_step::StepSchema::Ap203Edition2,
Self::Ap214 => cadmpeg_step::StepSchema::Ap214,
Self::Ap242e1 => cadmpeg_step::StepSchema::Ap242Edition1,
Self::Ap242e2 => cadmpeg_step::StepSchema::Ap242Edition2,
Self::Ap242e3 => cadmpeg_step::StepSchema::Ap242Edition3,
}
}
}
#[derive(Debug, Clone, Args)]
struct StepOutputArgs {
#[arg(long, value_enum, default_value_t)]
step_target: StepTarget,
#[arg(long)]
reject_step_losses: bool,
}
impl StepOutputArgs {
fn options(&self) -> cadmpeg_step::StepWriteOptions {
cadmpeg_step::StepWriteOptions {
schema: self.step_target.schema(),
unsupported: if self.reject_step_losses {
cadmpeg_step::StepUnsupportedPolicy::Reject
} else {
cadmpeg_step::StepUnsupportedPolicy::Report
},
..Default::default()
}
}
}
#[derive(Debug, Parser)]
#[command(
name = "cadmpeg",
version,
about = "Inspect, decode, validate, compare, and convert CAD models",
after_help = "Exit codes: 0 success, 1 semantic failure, 2 operational error."
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum Format {
#[value(alias = "json")]
Cadir,
Step,
Fcstd,
F3d,
Sldprt,
#[value(alias = "3dm")]
Rhino,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum RhinoVersion {
#[value(name = "50", alias = "5")]
V5,
#[value(name = "60", alias = "6")]
V6,
#[value(name = "70", alias = "7")]
V7,
#[value(name = "80", alias = "8")]
V8,
}
impl RhinoVersion {
const fn codec(self) -> cadmpeg_codec_rhino::RhinoArchiveVersion {
match self {
Self::V5 => cadmpeg_codec_rhino::RhinoArchiveVersion::V5,
Self::V6 => cadmpeg_codec_rhino::RhinoArchiveVersion::V6,
Self::V7 => cadmpeg_codec_rhino::RhinoArchiveVersion::V7,
Self::V8 => cadmpeg_codec_rhino::RhinoArchiveVersion::V8,
}
}
}
impl Format {
fn from_extension(extension: &str) -> Option<Self> {
match extension.to_ascii_lowercase().as_str() {
"cadir" | "json" => Some(Self::Cadir),
"step" | "stp" => Some(Self::Step),
"fcstd" => Some(Self::Fcstd),
"f3d" => Some(Self::F3d),
"sldprt" => Some(Self::Sldprt),
"3dm" => Some(Self::Rhino),
_ => None,
}
}
fn is_geometry_export(self) -> bool {
matches!(
self,
Self::Step | Self::Fcstd | Self::F3d | Self::Sldprt | Self::Rhino
)
}
fn from_path(path: Option<&std::path::Path>) -> Option<Self> {
path.and_then(std::path::Path::extension)
.and_then(|extension| extension.to_str())
.and_then(Self::from_extension)
}
fn name(self) -> &'static str {
match self {
Self::Cadir => "cadir",
Self::Step => "step",
Self::Fcstd => "fcstd",
Self::F3d => "f3d",
Self::Sldprt => "sldprt",
Self::Rhino => "rhino",
}
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum InputFormat {
Fcstd,
F3d,
Sldprt,
#[value(alias = "catia")]
Catpart,
Nx,
Creo,
#[value(alias = "3dm")]
Rhino,
#[value(alias = "igs")]
Iges,
Step,
Cadir,
}
#[derive(Debug, Clone, Copy)]
enum ForcedInput {
Codec(&'static str),
Cadir,
}
impl InputFormat {
fn resolution(self) -> ForcedInput {
match self {
Self::Fcstd => ForcedInput::Codec("fcstd"),
Self::F3d => ForcedInput::Codec("f3d"),
Self::Sldprt => ForcedInput::Codec("sldprt"),
Self::Catpart => ForcedInput::Codec("catia"),
Self::Nx => ForcedInput::Codec("nx"),
Self::Creo => ForcedInput::Codec("creo"),
Self::Rhino => ForcedInput::Codec("rhino"),
Self::Iges => ForcedInput::Codec("iges"),
Self::Step => ForcedInput::Codec("step"),
Self::Cadir => ForcedInput::Cadir,
}
}
}
#[derive(Debug, Clone, Args)]
struct InputArgs {
#[arg(long, value_enum)]
input_format: Option<InputFormat>,
}
impl InputArgs {
fn forced(&self) -> Option<ForcedInput> {
self.input_format.map(InputFormat::resolution)
}
}
#[derive(Debug, Clone, Args)]
struct DecodeArgs {
#[arg(long)]
container_only: bool,
#[arg(long)]
strict: bool,
#[arg(long, value_enum, default_value_t = LimitProfile::Desktop)]
limits: LimitProfile,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum LimitProfile {
Desktop,
Service,
}
impl DecodeArgs {
fn options(&self) -> cadmpeg_ir::DecodeOptions {
let limits = self.limits.limits();
let mode = if self.strict {
cadmpeg_codec_core::decode::DecodeMode::Strict
} else {
cadmpeg_codec_core::decode::DecodeMode::Salvage
};
cadmpeg_ir::DecodeOptions {
container_only: self.container_only,
policy: cadmpeg_codec_core::decode::DecodePolicy { mode, limits },
}
}
}
impl LimitProfile {
const fn limits(self) -> cadmpeg_codec_core::decode::ResourceLimits {
match self {
LimitProfile::Desktop => cadmpeg_codec_core::decode::ResourceLimits::desktop(),
LimitProfile::Service => cadmpeg_codec_core::decode::ResourceLimits::service(),
}
}
}
#[derive(Debug, Subcommand)]
enum Command {
#[command(args_conflicts_with_subcommands = true, subcommand_negates_reqs = true)]
Inspect {
#[arg(required = true)]
input: Option<PathBuf>,
#[arg(long)]
json: bool,
#[arg(short = 'o', long, visible_alias = "output")]
report: Option<PathBuf>,
#[arg(long)]
force: bool,
#[arg(long, value_enum, default_value_t = LimitProfile::Desktop)]
limits: LimitProfile,
#[command(flatten)]
input_args: InputArgs,
#[command(subcommand)]
bytes: Option<inspect::ByteCommand>,
},
Decode {
input: PathBuf,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
force: bool,
#[arg(long)]
report: Option<PathBuf>,
#[command(flatten)]
input_args: InputArgs,
#[command(flatten)]
decode: DecodeArgs,
},
Validate {
input: PathBuf,
#[arg(long)]
json: bool,
#[arg(short = 'o', long, visible_alias = "output")]
report: Option<PathBuf>,
#[arg(long)]
force: bool,
#[command(flatten)]
input_args: InputArgs,
#[command(flatten)]
decode: DecodeArgs,
},
Export {
input: PathBuf,
#[arg(short, long, value_enum)]
format: Option<Format>,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
force: bool,
#[arg(long)]
report: Option<PathBuf>,
#[arg(long)]
allow_empty: bool,
#[arg(long)]
reject_lossy: bool,
#[arg(long, value_enum)]
rhino_version: Option<RhinoVersion>,
#[command(flatten)]
input_args: InputArgs,
#[command(flatten)]
decode: DecodeArgs,
#[command(flatten)]
step: StepOutputArgs,
},
Diff {
a: PathBuf,
b: PathBuf,
#[arg(long, value_enum)]
input_format_a: Option<InputFormat>,
#[arg(long, value_enum)]
input_format_b: Option<InputFormat>,
#[arg(long)]
json: bool,
#[arg(short = 'o', long, visible_alias = "output")]
report: Option<PathBuf>,
#[arg(long)]
force: bool,
#[command(flatten)]
decode: DecodeArgs,
},
Convert {
input: PathBuf,
#[arg(short, long, value_enum)]
format: Option<Format>,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
force: bool,
#[arg(long)]
report: Option<PathBuf>,
#[arg(long)]
allow_invalid: bool,
#[arg(long)]
allow_empty: bool,
#[arg(long)]
reject_lossy: bool,
#[arg(long, value_enum)]
rhino_version: Option<RhinoVersion>,
#[command(flatten)]
input_args: InputArgs,
#[command(flatten)]
decode: DecodeArgs,
#[command(flatten)]
step: StepOutputArgs,
},
}
fn main() -> ExitCode {
let command = Cli::parse().command;
let registry = Registry::with_builtins();
let result = match command {
Command::Inspect {
input,
json,
report,
force,
limits,
input_args,
bytes,
} => match bytes {
Some(byte_command) => inspect::run(byte_command).map(|()| ExitCode::SUCCESS),
None => {
let input = input.expect("clap requires an input without a subcommand");
commands::inspect(
®istry,
&input,
input_args.forced(),
json,
report.as_deref(),
force,
limits.limits(),
)
.map(|()| ExitCode::SUCCESS)
}
},
Command::Decode {
input,
output,
force,
report,
input_args,
decode,
} => commands::decode(
®istry,
&input,
output.as_deref(),
force,
report.as_deref(),
input_args.forced(),
&decode,
)
.map(|()| ExitCode::SUCCESS),
Command::Validate {
input,
json,
report,
force,
input_args,
decode,
} => commands::validate_cmd(
®istry,
&input,
input_args.forced(),
&decode,
json,
report.as_deref(),
force,
)
.map(|()| ExitCode::SUCCESS),
Command::Export {
input,
format,
output,
force,
report,
allow_empty,
reject_lossy,
rhino_version,
input_args,
decode,
step,
} => commands::export(
®istry,
&input,
format,
output.as_deref(),
commands::ConversionPlan {
force,
report,
validation: commands::ValidationMode::Skipped,
allow_empty,
reject_lossy,
rhino_version: rhino_version.map(RhinoVersion::codec),
step_options: step.options(),
forced_input: input_args.forced(),
},
&decode,
)
.map(|()| ExitCode::SUCCESS),
Command::Diff {
a,
b,
input_format_a,
input_format_b,
json,
report,
force,
decode,
} => commands::diff(
®istry,
commands::DiffInput {
path: &a,
forced: input_format_a.map(InputFormat::resolution),
},
commands::DiffInput {
path: &b,
forced: input_format_b.map(InputFormat::resolution),
},
&decode,
json,
report.as_deref(),
force,
),
Command::Convert {
input,
format,
output,
force,
report,
allow_invalid,
allow_empty,
reject_lossy,
rhino_version,
input_args,
decode,
step,
} => commands::convert(
®istry,
&input,
format,
output.as_deref(),
commands::ConversionPlan {
force,
report,
validation: commands::ValidationMode::Required { allow_invalid },
allow_empty,
reject_lossy,
rhino_version: rhino_version.map(RhinoVersion::codec),
step_options: step.options(),
forced_input: input_args.forced(),
},
&decode,
)
.map(|()| ExitCode::SUCCESS),
};
result.unwrap_or_else(|err| {
eprintln!("error: {err:#}");
if err.downcast_ref::<commands::SemanticFailure>().is_some() {
ExitCode::from(1)
} else {
ExitCode::from(2)
}
})
}