forked from 1b-agent/opencadstudio-upstream
The codec now holds wide EntityType variants boxed (MText, Helix, Dimension, Hatch, Viewport, MultiLeader, Surface, Table, Ole2Frame, Light, Extended). Constructors box their entity; nested patterns reach a dimension through entities::dimension::as_dimension(_mut). Also follows BlockAction::action_ids, LineTypeComplexData::source_flags and the decimal tab stop with an explicit character. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
676 lines
23 KiB
Rust
676 lines
23 KiB
Rust
// DGNIMPORT / -DGNIMPORT — a DGN model (V7 or V8) as drawing objects.
|
|
//
|
|
// Enter filename for DGN import:
|
|
// Enter name of model or [?] <Default>:
|
|
// Specify conversion units [Master/Sub] <Master>:
|
|
// Specify mapping setup or [?] <Standard>:
|
|
//
|
|
// The Standard mapping setup: each level becomes the layer of its name (its
|
|
// number when unnamed), colours become the colour index of the same RGB
|
|
// (else a true colour), weights the lineweights below, V7 line styles
|
|
// Continuous. Lines, line strings and shapes, arcs, circles and ellipses,
|
|
// complex chains and shapes (polylines with arcs), text, text nodes and
|
|
// shared cells (blocks) are converted.
|
|
|
|
use codec::entities::{
|
|
Arc, Circle, Ellipse, Insert, Line, LwPolyline, LwVertex, MText, Text,
|
|
};
|
|
use codec::types::{Color, LineWeight, Vector2, Vector3};
|
|
use codec::EntityType;
|
|
|
|
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind};
|
|
use crate::scene::model::underlay_vector::{DgnElement, DgnGeom, DgnImport};
|
|
|
|
/// What one import reads and where it goes.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct DgnImportRequest {
|
|
pub path: String,
|
|
pub model: String,
|
|
/// Sub units (else master units) become drawing units.
|
|
pub sub_units: bool,
|
|
/// Text nodes become one TEXT per line (else one MTEXT).
|
|
pub explode_text_nodes: bool,
|
|
/// Into the current drawing (else a new one).
|
|
pub current_drawing: bool,
|
|
/// In the current drawing: layer, block and style names that already
|
|
/// exist get the file name as prefix (else the existing ones are used).
|
|
pub prefix_duplicates: bool,
|
|
}
|
|
|
|
/// The objects of an import: layers (name, colour), text styles (name, font
|
|
/// file), blocks (name, objects) and the model's objects, in file order.
|
|
#[derive(Default)]
|
|
pub struct Converted {
|
|
pub layers: Vec<(String, Color)>,
|
|
pub styles: Vec<(String, String)>,
|
|
pub blocks: Vec<(String, Vec<EntityType>)>,
|
|
pub entities: Vec<EntityType>,
|
|
}
|
|
|
|
/// A DGN weight (0-31) as a lineweight (1/100 mm), as the Standard mapping
|
|
/// converts it.
|
|
pub fn lineweight_of(weight: u32) -> i16 {
|
|
const TABLE: [i16; 15] = [0, 13, 30, 40, 53, 70, 80, 100, 106, 120, 140, 158, 158, 158, 200];
|
|
TABLE.get(weight as usize).copied().unwrap_or(211)
|
|
}
|
|
|
|
/// An RGB colour as the colour index of the same RGB, else a true colour.
|
|
pub fn color_of(rgb: [u8; 3]) -> Color {
|
|
let index = codec::types::aci_table::nearest_aci(rgb[0], rgb[1], rgb[2]);
|
|
if codec::types::aci_table::aci_to_rgb(index) == Some((rgb[0], rgb[1], rgb[2])) {
|
|
Color::Index(index)
|
|
} else {
|
|
Color::from_rgb(rgb[0], rgb[1], rgb[2])
|
|
}
|
|
}
|
|
|
|
/// A unit's short name as words ("m" → Meters).
|
|
pub fn unit_display(short: &str) -> String {
|
|
match short.trim().to_ascii_lowercase().as_str() {
|
|
"m" => "Meters",
|
|
"mm" => "Millimeters",
|
|
"cm" => "Centimeters",
|
|
"km" => "Kilometers",
|
|
"in" | "\"" => "Inches",
|
|
"ft" | "'" => "Feet",
|
|
"yd" => "Yards",
|
|
"mi" => "Miles",
|
|
other => return other.to_string(),
|
|
}
|
|
.to_string()
|
|
}
|
|
|
|
fn layer_name(import: &DgnImport, level: u32) -> String {
|
|
import
|
|
.levels
|
|
.get(&level)
|
|
.map(|l| l.0.clone())
|
|
.filter(|n| !n.is_empty())
|
|
.unwrap_or_else(|| level.to_string())
|
|
}
|
|
|
|
/// True when the axes `u` and `v` are of one length and at right angles.
|
|
fn circular(u: [f64; 2], v: [f64; 2]) -> bool {
|
|
let (lu, lv) = (u[0].hypot(u[1]), v[0].hypot(v[1]));
|
|
(lu - lv).abs() <= 1e-9 * lu.max(1.0) && (u[0] * v[0] + u[1] * v[1]).abs() <= 1e-9 * lu * lv
|
|
}
|
|
|
|
/// The arc's points at `n` steps.
|
|
fn arc_points(center: [f64; 2], u: [f64; 2], v: [f64; 2], start: f64, sweep: f64) -> Vec<[f64; 2]> {
|
|
let n = ((sweep.abs() / (std::f64::consts::PI / 32.0)).ceil() as usize).clamp(2, 4096);
|
|
(0..=n)
|
|
.map(|k| {
|
|
let t = start + sweep * k as f64 / n as f64;
|
|
[
|
|
center[0] + t.cos() * u[0] + t.sin() * v[0],
|
|
center[1] + t.cos() * u[1] + t.sin() * v[1],
|
|
]
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// A circular arc as centre, radius, start angle and signed sweep in the
|
|
/// drawing's angles.
|
|
fn circle_angles(u: [f64; 2], v: [f64; 2], start: f64, sweep: f64) -> (f64, f64, f64) {
|
|
let side = (u[0] * v[1] - u[1] * v[0]).signum();
|
|
let a0 = u[1].atan2(u[0]);
|
|
(u[0].hypot(u[1]), a0 + side * start, side * sweep)
|
|
}
|
|
|
|
fn polyline(points: &[[f64; 2]], bulges: &[f64]) -> EntityType {
|
|
let mut pl = LwPolyline::new();
|
|
pl.plinegen = true;
|
|
pl.vertices = points
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(k, p)| {
|
|
let mut v = LwVertex::new(Vector2::new(p[0], p[1]));
|
|
v.bulge = bulges.get(k).copied().unwrap_or(0.0);
|
|
v
|
|
})
|
|
.collect();
|
|
EntityType::LwPolyline(pl)
|
|
}
|
|
|
|
/// A complex chain's vertices and bulges: lines and line strings add their
|
|
/// points, circular arcs a bulge, other arcs their points.
|
|
fn chain(parts: &[DgnGeom]) -> (Vec<[f64; 2]>, Vec<f64>) {
|
|
let mut points: Vec<[f64; 2]> = Vec::new();
|
|
let mut bulges: Vec<f64> = Vec::new();
|
|
let mut add = |p: [f64; 2], bulge_before: Option<f64>| {
|
|
let same = points
|
|
.last()
|
|
.is_some_and(|q| (q[0] - p[0]).abs() < 1e-9 && (q[1] - p[1]).abs() < 1e-9);
|
|
if let (Some(b), Some(last)) = (bulge_before, bulges.last_mut()) {
|
|
*last = b;
|
|
}
|
|
if !same {
|
|
points.push(p);
|
|
bulges.push(0.0);
|
|
}
|
|
};
|
|
for part in parts {
|
|
match part {
|
|
DgnGeom::Line(a, b) => {
|
|
add(*a, None);
|
|
add(*b, None);
|
|
}
|
|
DgnGeom::Points(pts) => pts.iter().for_each(|p| add(*p, None)),
|
|
DgnGeom::Arc {
|
|
center,
|
|
u,
|
|
v,
|
|
start,
|
|
sweep,
|
|
} if circular(*u, *v) => {
|
|
let at = |t: f64| {
|
|
[
|
|
center[0] + t.cos() * u[0] + t.sin() * v[0],
|
|
center[1] + t.cos() * u[1] + t.sin() * v[1],
|
|
]
|
|
};
|
|
let (_, _, signed) = circle_angles(*u, *v, *start, *sweep);
|
|
add(at(*start), None);
|
|
add(at(start + sweep), Some((signed / 4.0).tan()));
|
|
}
|
|
DgnGeom::Arc {
|
|
center,
|
|
u,
|
|
v,
|
|
start,
|
|
sweep,
|
|
} => arc_points(*center, *u, *v, *start, *sweep)
|
|
.into_iter()
|
|
.for_each(|p| add(p, None)),
|
|
_ => {}
|
|
}
|
|
}
|
|
(points, bulges)
|
|
}
|
|
|
|
fn text_entity(geom: &DgnGeom, style: &str) -> Option<EntityType> {
|
|
let DgnGeom::Text {
|
|
text,
|
|
origin,
|
|
height,
|
|
width_factor,
|
|
rotation,
|
|
..
|
|
} = geom
|
|
else {
|
|
return None;
|
|
};
|
|
let mut t = Text::with_value(text.clone(), Vector3::new(origin[0], origin[1], 0.0))
|
|
.with_height(*height)
|
|
.with_rotation(*rotation);
|
|
t.width_factor = if *width_factor > 0.0 { *width_factor } else { 1.0 };
|
|
t.style = style.to_string();
|
|
Some(EntityType::Text(t))
|
|
}
|
|
|
|
/// The text style of a line of text (V7: one per font number, "dgnNNN"),
|
|
/// added to `styles` with its font file the first time.
|
|
fn text_style(geom: &DgnGeom, styles: &mut Vec<(String, String)>) -> String {
|
|
match geom {
|
|
DgnGeom::Text {
|
|
font: Some(font), ..
|
|
} => {
|
|
let name = format!("dgn{font:03}");
|
|
if !styles.iter().any(|s| s.0 == name) {
|
|
styles.push((name.clone(), format!("{name}.shx")));
|
|
}
|
|
name
|
|
}
|
|
_ => "Standard".to_string(),
|
|
}
|
|
}
|
|
|
|
/// Lines of text as one MTEXT, a paragraph a line, as the reference
|
|
/// converts them: a text node attached at its top left (a height above the
|
|
/// first baseline), a V8 text at its bottom left a third of the height
|
|
/// below its baseline.
|
|
fn mtext(lines: &[DgnGeom], style: String, node: bool) -> Option<EntityType> {
|
|
let Some(DgnGeom::Text {
|
|
origin,
|
|
height,
|
|
rotation,
|
|
..
|
|
}) = lines.first()
|
|
else {
|
|
return None;
|
|
};
|
|
let mut m = MText::new();
|
|
m.value = lines
|
|
.iter()
|
|
.filter_map(|g| match g {
|
|
DgnGeom::Text { text, .. } => Some(text.clone()),
|
|
_ => None,
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\\P");
|
|
let up = if node { *height } else { -0.33 * height };
|
|
m.insertion_point = Vector3::new(
|
|
origin[0] - rotation.sin() * up,
|
|
origin[1] + rotation.cos() * up,
|
|
0.0,
|
|
);
|
|
if !node {
|
|
m.attachment_point = codec::entities::AttachmentPoint::BottomLeft;
|
|
m.line_spacing_factor = 0.8;
|
|
}
|
|
m.height = *height;
|
|
m.rotation = *rotation;
|
|
m.rectangle_width = 0.0;
|
|
m.style = style;
|
|
Some(EntityType::MText(Box::new(m)))
|
|
}
|
|
|
|
/// The object(s) one element converts to.
|
|
fn convert_geom(
|
|
geom: &DgnGeom,
|
|
explode_text_nodes: bool,
|
|
styles: &mut Vec<(String, String)>,
|
|
) -> Vec<EntityType> {
|
|
match geom {
|
|
DgnGeom::Line(a, b) => vec![EntityType::Line(Line::from_points(
|
|
Vector3::new(a[0], a[1], 0.0),
|
|
Vector3::new(b[0], b[1], 0.0),
|
|
))],
|
|
DgnGeom::Points(pts) if pts.len() >= 2 => vec![polyline(pts, &[])],
|
|
DgnGeom::Points(_) => Vec::new(),
|
|
DgnGeom::Arc {
|
|
center,
|
|
u,
|
|
v,
|
|
start,
|
|
sweep,
|
|
} => {
|
|
let c = Vector3::new(center[0], center[1], 0.0);
|
|
let full = sweep.abs() >= std::f64::consts::TAU - 1e-9;
|
|
if circular(*u, *v) {
|
|
let (radius, a0, signed) = circle_angles(*u, *v, *start, *sweep);
|
|
if full {
|
|
return vec![EntityType::Circle(Circle::from_center_radius(c, radius))];
|
|
}
|
|
let (from, to) = if signed >= 0.0 {
|
|
(a0, a0 + signed)
|
|
} else {
|
|
(a0 + signed, a0)
|
|
};
|
|
return vec![EntityType::Arc(Arc::from_center_radius_angles(
|
|
c,
|
|
radius,
|
|
from.rem_euclid(std::f64::consts::TAU),
|
|
to.rem_euclid(std::f64::consts::TAU),
|
|
))];
|
|
}
|
|
let (lu, lv) = (u[0].hypot(u[1]), v[0].hypot(v[1]));
|
|
if (u[0] * v[0] + u[1] * v[1]).abs() > 1e-9 * lu * lv || lu < 1e-12 || lv < 1e-12 {
|
|
// ponytail: a skewed plan projection (a 3D ellipse seen
|
|
// obliquely) becomes points; find its principal axes if
|
|
// such files matter.
|
|
return vec![polyline(&arc_points(*center, *u, *v, *start, *sweep), &[])];
|
|
}
|
|
// P(t) = c + cos t·u + sin t·v as an ellipse on its major axis
|
|
// with the minor axis turned counter-clockwise from it.
|
|
let ccw = u[0] * v[1] - u[1] * v[0] > 0.0;
|
|
let (major, ratio, map): (_, f64, Box<dyn Fn(f64) -> f64>) = if lu >= lv {
|
|
if ccw {
|
|
(*u, lv / lu, Box::new(|t| t))
|
|
} else {
|
|
(*u, lv / lu, Box::new(|t: f64| -t))
|
|
}
|
|
} else if ccw {
|
|
(*v, lu / lv, Box::new(|t: f64| t - std::f64::consts::FRAC_PI_2))
|
|
} else {
|
|
(*v, lu / lv, Box::new(|t: f64| std::f64::consts::FRAC_PI_2 - t))
|
|
};
|
|
let mut e = Ellipse::from_center_axes(c, Vector3::new(major[0], major[1], 0.0), ratio);
|
|
if !full {
|
|
let (p0, p1) = (map(*start), map(start + sweep));
|
|
let (from, to) = if p1 >= p0 { (p0, p1) } else { (p1, p0) };
|
|
e.start_parameter = from.rem_euclid(std::f64::consts::TAU);
|
|
e.end_parameter = e.start_parameter + (to - from);
|
|
}
|
|
vec![EntityType::Ellipse(e)]
|
|
}
|
|
DgnGeom::Chain(parts) => {
|
|
let (points, bulges) = chain(parts);
|
|
if points.len() >= 2 {
|
|
vec![polyline(&points, &bulges)]
|
|
} else {
|
|
Vec::new()
|
|
}
|
|
}
|
|
// V7 text is TEXT, V8 text an MTEXT.
|
|
DgnGeom::Text { font: Some(_), .. } => {
|
|
let style = text_style(geom, styles);
|
|
text_entity(geom, &style).into_iter().collect()
|
|
}
|
|
DgnGeom::Text { .. } => {
|
|
let style = text_style(geom, styles);
|
|
mtext(std::slice::from_ref(geom), style, false).into_iter().collect()
|
|
}
|
|
// Exploded, each line converts as a text element of its own.
|
|
DgnGeom::TextNode(lines) if explode_text_nodes => lines
|
|
.iter()
|
|
.flat_map(|g| convert_geom(g, false, styles))
|
|
.collect(),
|
|
DgnGeom::TextNode(lines) => {
|
|
let Some(first) = lines.first() else {
|
|
return Vec::new();
|
|
};
|
|
let style = text_style(first, styles);
|
|
mtext(lines, style, true).into_iter().collect()
|
|
}
|
|
DgnGeom::Cell {
|
|
name,
|
|
m,
|
|
origin,
|
|
z_scale,
|
|
} => {
|
|
let sx = m[0].hypot(m[2]);
|
|
let det = m[0] * m[3] - m[1] * m[2];
|
|
let sy = if sx > 0.0 { det / sx } else { 1.0 };
|
|
let insert = Insert::new(name.clone(), Vector3::new(origin[0], origin[1], 0.0))
|
|
.with_scale(sx, sy, *z_scale)
|
|
.with_rotation(m[2].atan2(m[0]));
|
|
vec![EntityType::Insert(insert)]
|
|
}
|
|
}
|
|
}
|
|
|
|
fn convert_elements(
|
|
import: &DgnImport,
|
|
elements: &[DgnElement],
|
|
explode_text_nodes: bool,
|
|
in_cell: bool,
|
|
out: &mut Converted,
|
|
) -> Vec<EntityType> {
|
|
let mut entities = Vec::new();
|
|
for element in elements {
|
|
// A cell's objects are on layer 0 (V7: by block), as the reference
|
|
// defines the block.
|
|
let layer = if in_cell {
|
|
"0".to_string()
|
|
} else {
|
|
layer_name(import, element.level)
|
|
};
|
|
if !out.layers.iter().any(|l| l.0 == layer) {
|
|
let color = import
|
|
.levels
|
|
.get(&element.level)
|
|
.and_then(|l| l.1)
|
|
.map(color_of)
|
|
.unwrap_or(Color::Index(7));
|
|
out.layers.push((layer.clone(), color));
|
|
}
|
|
for mut entity in convert_geom(&element.geom, explode_text_nodes, &mut out.styles) {
|
|
let common = entity.common_mut();
|
|
common.layer = layer.clone();
|
|
common.color = element.color.map(color_of).unwrap_or(Color::ByLayer);
|
|
common.line_weight = element
|
|
.weight
|
|
.map(|w| LineWeight::Value(lineweight_of(w)))
|
|
.unwrap_or(LineWeight::ByLayer);
|
|
// The Standard mapping draws every line style Continuous.
|
|
if element.style.is_some() {
|
|
common.linetype = "Continuous".to_string();
|
|
}
|
|
if import.v7 && in_cell {
|
|
common.color = Color::ByBlock;
|
|
common.line_weight = LineWeight::ByBlock;
|
|
}
|
|
entities.push(entity);
|
|
}
|
|
}
|
|
entities
|
|
}
|
|
|
|
/// The import's objects, in sub units when `sub_units`.
|
|
pub fn convert(import: &DgnImport, sub_units: bool, explode_text_nodes: bool) -> Converted {
|
|
let k = if sub_units { import.sub_per_master } else { 1.0 };
|
|
let scale = |elements: &[DgnElement]| {
|
|
crate::scene::model::underlay_vector::scaled_elements(elements, k, [0.0, 0.0])
|
|
};
|
|
let mut out = Converted::default();
|
|
for (name, elements) in &import.cells {
|
|
let entities = convert_elements(import, &scale(elements), explode_text_nodes, true, &mut out);
|
|
if !name.is_empty() && !entities.is_empty() {
|
|
out.blocks.push((name.clone(), entities));
|
|
}
|
|
}
|
|
out.entities = convert_elements(import, &scale(&import.elements), explode_text_nodes, false, &mut out);
|
|
out
|
|
}
|
|
|
|
/// The Standard mapping of what a file uses: level → layer, line style →
|
|
/// linetype, weight → lineweight, colour → colour, one row each.
|
|
pub fn mapping_rows(import: &DgnImport, tab: usize) -> Vec<(String, String)> {
|
|
let mut rows: Vec<(String, String)> = Vec::new();
|
|
let mut add = |dgn: String, dwg: String| {
|
|
if !rows.iter().any(|r| r.0 == dgn) {
|
|
rows.push((dgn, dwg));
|
|
}
|
|
};
|
|
let all = import
|
|
.elements
|
|
.iter()
|
|
.chain(import.cells.iter().flat_map(|c| c.1.iter()));
|
|
for e in all {
|
|
match tab {
|
|
0 => {
|
|
let name = layer_name(import, e.level);
|
|
add(name.clone(), name);
|
|
}
|
|
1 => match e.style {
|
|
Some(s) => add(s.to_string(), "Continuous".to_string()),
|
|
None => add("ByLevel".to_string(), "ByLayer".to_string()),
|
|
},
|
|
2 => match e.weight {
|
|
Some(w) => add(
|
|
w.to_string(),
|
|
format!("{:.2} mm", lineweight_of(w) as f64 / 100.0),
|
|
),
|
|
None => add("ByLevel".to_string(), "ByLayer".to_string()),
|
|
},
|
|
_ => match e.color {
|
|
Some(rgb) => add(
|
|
format!("RGB {},{},{}", rgb[0], rgb[1], rgb[2]),
|
|
match color_of(rgb) {
|
|
Color::Index(i) => i.to_string(),
|
|
_ => format!("{},{},{}", rgb[0], rgb[1], rgb[2]),
|
|
},
|
|
),
|
|
None => add("ByLevel".to_string(), "ByLayer".to_string()),
|
|
},
|
|
}
|
|
}
|
|
rows
|
|
}
|
|
|
|
// ── Command line ───────────────────────────────────────────────────────────
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
enum Step {
|
|
Path,
|
|
Model,
|
|
ListModels,
|
|
Conversion,
|
|
Mapping,
|
|
ListMappings,
|
|
}
|
|
|
|
pub struct DgnImportCommand {
|
|
step: Step,
|
|
path: String,
|
|
models: Vec<String>,
|
|
model: String,
|
|
sub_units: bool,
|
|
}
|
|
|
|
impl Default for DgnImportCommand {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl DgnImportCommand {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
step: Step::Path,
|
|
path: String::new(),
|
|
models: Vec::new(),
|
|
model: String::new(),
|
|
sub_units: false,
|
|
}
|
|
}
|
|
|
|
fn accept_path(&mut self, text: &str) -> CmdResult {
|
|
let path = text.trim().trim_matches('"').to_string();
|
|
if path.is_empty() {
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
let models = crate::scene::model::underlay_vector::item_names(
|
|
codec::entities::UnderlayType::Dgn,
|
|
&path,
|
|
)
|
|
.filter(|m| !m.is_empty() && crate::scene::model::underlay_vector::dgn_import(&path, &m[0]).is_some());
|
|
let Some(models) = models else {
|
|
return CmdResult::ReportError("Invalid or unsupported DGN file.".to_string());
|
|
};
|
|
self.path = path;
|
|
self.model = models[0].clone();
|
|
self.models = models;
|
|
self.step = Step::Model;
|
|
CmdResult::NeedPoint
|
|
}
|
|
|
|
fn accept_model(&mut self, text: &str) -> CmdResult {
|
|
let text = text.trim();
|
|
if text == "?" {
|
|
self.step = Step::ListModels;
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
let name = if text.is_empty() { self.models[0].as_str() } else { text };
|
|
match self.models.iter().find(|m| m.eq_ignore_ascii_case(name)) {
|
|
Some(found) => {
|
|
self.model = found.clone();
|
|
self.step = Step::Conversion;
|
|
CmdResult::NeedPoint
|
|
}
|
|
None => CmdResult::ReportError(format!("There is no {name} in {}.", self.path)),
|
|
}
|
|
}
|
|
|
|
fn list(&mut self, pattern: &str, names: &[&str], back: Step) -> CmdResult {
|
|
let pattern = match pattern.trim() {
|
|
"" => "*",
|
|
p => p,
|
|
};
|
|
let lines: Vec<&str> = names
|
|
.iter()
|
|
.copied()
|
|
.filter(|n| crate::io::xref_model::wildcard_match(n, pattern))
|
|
.collect();
|
|
self.step = back;
|
|
CmdResult::ReportMeasurement(lines.join("\n"))
|
|
}
|
|
|
|
fn accept_conversion(&mut self, text: &str) -> CmdResult {
|
|
let t = text.trim().to_ascii_uppercase();
|
|
self.sub_units = match t.as_str() {
|
|
"" => false,
|
|
_ if "MASTER".starts_with(&t) => false,
|
|
_ if "SUB".starts_with(&t) => true,
|
|
_ => return CmdResult::ReportError("Invalid option keyword.".to_string()),
|
|
};
|
|
self.step = Step::Mapping;
|
|
CmdResult::NeedPoint
|
|
}
|
|
|
|
fn accept_mapping(&mut self, text: &str) -> CmdResult {
|
|
let text = text.trim();
|
|
if text == "?" {
|
|
self.step = Step::ListMappings;
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
if !text.is_empty() && !text.eq_ignore_ascii_case("Standard") {
|
|
return CmdResult::ReportError(format!("{text} not found."));
|
|
}
|
|
CmdResult::DgnImport(DgnImportRequest {
|
|
path: self.path.clone(),
|
|
model: self.model.clone(),
|
|
sub_units: self.sub_units,
|
|
explode_text_nodes: false,
|
|
current_drawing: false,
|
|
prefix_duplicates: false,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl CadCommand for DgnImportCommand {
|
|
fn name(&self) -> &'static str {
|
|
"DGNIMPORT"
|
|
}
|
|
|
|
fn prompt(&self) -> String {
|
|
match self.step {
|
|
Step::Path => "Enter filename for DGN import:".to_string(),
|
|
Step::Model => format!("Enter name of model or [?] <{}>:", self.models[0]),
|
|
Step::ListModels => "Enter model(s) to list <*>:".to_string(),
|
|
Step::Conversion => "Specify conversion units [Master/Sub] <Master>:".to_string(),
|
|
Step::Mapping => "Specify mapping setup or [?] <Standard>:".to_string(),
|
|
Step::ListMappings => "Enter mapping setup(s) to list <*>:".to_string(),
|
|
}
|
|
}
|
|
|
|
fn options(&self) -> Vec<CmdOption> {
|
|
match self.step {
|
|
Step::Model | Step::Mapping => vec![CmdOption::new("?", "?")],
|
|
Step::Conversion => vec![CmdOption::new("Master", "M"), CmdOption::new("Sub", "S")],
|
|
_ => Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn input_kind(&self) -> InputKind {
|
|
match self.step {
|
|
Step::Path | Step::Model => InputKind::FreeText,
|
|
_ => InputKind::SingleToken,
|
|
}
|
|
}
|
|
|
|
fn on_point(&mut self, _pt: glam::DVec3) -> CmdResult {
|
|
CmdResult::NeedPoint
|
|
}
|
|
|
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
|
Some(match self.step {
|
|
Step::Path => self.accept_path(text),
|
|
Step::Model => self.accept_model(text),
|
|
Step::ListModels => {
|
|
let models = self.models.clone();
|
|
let names: Vec<&str> = models.iter().map(String::as_str).collect();
|
|
self.list(text, &names, Step::Model)
|
|
}
|
|
Step::Conversion => self.accept_conversion(text),
|
|
Step::Mapping => self.accept_mapping(text),
|
|
Step::ListMappings => {
|
|
match self.list(text, &["Standard"], Step::Mapping) {
|
|
CmdResult::ReportMeasurement(lines) => {
|
|
CmdResult::ReportMeasurement(format!("Import mapping setups:\n{lines}"))
|
|
}
|
|
other => other,
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
fn on_enter(&mut self) -> CmdResult {
|
|
match self.step {
|
|
Step::Path => CmdResult::Cancel,
|
|
_ => self.on_text_input("").unwrap_or(CmdResult::NeedPoint),
|
|
}
|
|
}
|
|
}
|