forked from 1b-agent/opencadstudio-upstream
Escape DGN text in MTEXT and read 3D text by its byte count
Backslashes and braces in a DGN text stay characters in the MTEXT it becomes instead of turning into formatting codes. A 3D V8 text is cut at its stored byte count before it is decoded, so a multi-byte character no longer shifts where the text ends. Doc comments that had slipped onto the wrong functions are back on their own, comments describe the behaviour instead of pointing at another program, and a few lines added in the previous commits are laid out the way the rest of the code is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a5f965ead4
commit
56861aa0bc
4 files changed
+54
-27
No files matched your search
@@ -458,7 +458,8 @@ impl OpenCADStudio {
|
|||||||
.file_stem()
|
.file_stem()
|
||||||
.map(|s| s.to_string_lossy().into_owned())
|
.map(|s| s.to_string_lossy().into_owned())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
Naming::Prefix(dgn_import::symbol_name(&format!("{stem} - {}$0$", request.model)))
|
let prefix = format!("{stem} - {}$0$", request.model);
|
||||||
|
Naming::Prefix(dgn_import::symbol_name(&prefix))
|
||||||
} else {
|
} else {
|
||||||
Naming::Keep
|
Naming::Keep
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
// B-spline curves, complex chains and shapes (polylines with arcs), text,
|
// B-spline curves, complex chains and shapes (polylines with arcs), text,
|
||||||
// text nodes and shared cells (blocks) are converted.
|
// text nodes and shared cells (blocks) are converted.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use codec::entities::{
|
use codec::entities::{
|
||||||
Arc, AttachmentPoint, Circle, DrawingDirection, Ellipse, Insert, Line, LineSpacingStyle,
|
Arc, AttachmentPoint, Circle, DrawingDirection, Ellipse, Insert, Line, LineSpacingStyle,
|
||||||
@@ -114,7 +114,7 @@ pub fn unit_display(short: &str) -> String {
|
|||||||
pub fn symbol_name(name: &str) -> String {
|
pub fn symbol_name(name: &str) -> String {
|
||||||
name.chars()
|
name.chars()
|
||||||
.map(|c| {
|
.map(|c| {
|
||||||
if c.is_control() || "<>/\\\":;?*|,=`".contains(c) {
|
if c.is_control() || "<>/\\\":;?*|=`".contains(c) {
|
||||||
'_'
|
'_'
|
||||||
} else {
|
} else {
|
||||||
c
|
c
|
||||||
@@ -221,9 +221,8 @@ fn flat(points: &[P3]) -> bool {
|
|||||||
/// (with the bulges), else a 3D polyline (no bulges).
|
/// (with the bulges), else a 3D polyline (no bulges).
|
||||||
fn polyline(points: &[P3], bulges: &[f64]) -> EntityType {
|
fn polyline(points: &[P3], bulges: &[f64]) -> EntityType {
|
||||||
if !flat(points) {
|
if !flat(points) {
|
||||||
return EntityType::Polyline3D(Polyline3D::from_points(
|
let points: Vec<Vector3> = points.iter().map(|p| v3(*p)).collect();
|
||||||
points.iter().map(|p| v3(*p)).collect(),
|
return EntityType::Polyline3D(Polyline3D::from_points(points));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
let mut pl = LwPolyline::new();
|
let mut pl = LwPolyline::new();
|
||||||
pl.plinegen = true;
|
pl.plinegen = true;
|
||||||
@@ -253,7 +252,8 @@ fn solid_hatch(points: &[P3], bulges: &[f64]) -> Option<EntityType> {
|
|||||||
}
|
}
|
||||||
let mut edge = PolylineEdge::new(Vec::new(), true);
|
let mut edge = PolylineEdge::new(Vec::new(), true);
|
||||||
for (k, p) in points[..n].iter().enumerate() {
|
for (k, p) in points[..n].iter().enumerate() {
|
||||||
edge.add_vertex(Vector2::new(p[0], p[1]), bulges.get(k).copied().unwrap_or(0.0));
|
let bulge = bulges.get(k).copied().unwrap_or(0.0);
|
||||||
|
edge.add_vertex(Vector2::new(p[0], p[1]), bulge);
|
||||||
}
|
}
|
||||||
let mut hatch = Hatch::solid();
|
let mut hatch = Hatch::solid();
|
||||||
let mut boundary = BoundaryPath::external();
|
let mut boundary = BoundaryPath::external();
|
||||||
@@ -281,7 +281,9 @@ fn chain_points(parts: &[DgnGeom], use_bulges: bool) -> (Vec<P3>, Vec<f64>) {
|
|||||||
let mut points: Vec<P3> = Vec::new();
|
let mut points: Vec<P3> = Vec::new();
|
||||||
let mut bulges: Vec<f64> = Vec::new();
|
let mut bulges: Vec<f64> = Vec::new();
|
||||||
let mut add = |p: P3, bulge_before: Option<f64>| {
|
let mut add = |p: P3, bulge_before: Option<f64>| {
|
||||||
let same = points.last().is_some_and(|q| (0..3).all(|c| (q[c] - p[c]).abs() < 1e-9));
|
let same = points
|
||||||
|
.last()
|
||||||
|
.is_some_and(|q| (0..3).all(|c| (q[c] - p[c]).abs() < 1e-9));
|
||||||
if let (Some(b), Some(last)) = (bulge_before, bulges.last_mut()) {
|
if let (Some(b), Some(last)) = (bulge_before, bulges.last_mut()) {
|
||||||
*last = b;
|
*last = b;
|
||||||
}
|
}
|
||||||
@@ -379,6 +381,14 @@ fn attachment(just: u8) -> AttachmentPoint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Text as MTEXT contents: backslashes and braces stay characters instead
|
||||||
|
/// of becoming formatting codes.
|
||||||
|
fn mtext_escape(text: &str) -> String {
|
||||||
|
text.replace('\\', "\\\\")
|
||||||
|
.replace('{', "\\{")
|
||||||
|
.replace('}', "\\}")
|
||||||
|
}
|
||||||
|
|
||||||
/// A number with six significant digits, trailing zeros dropped.
|
/// A number with six significant digits, trailing zeros dropped.
|
||||||
fn sig6(x: f64) -> String {
|
fn sig6(x: f64) -> String {
|
||||||
let digits = if x == 0.0 {
|
let digits = if x == 0.0 {
|
||||||
@@ -510,7 +520,7 @@ fn mtext_of_text(geom: &DgnGeom, color: u16, style: &str) -> Option<EntityType>
|
|||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
m.value = format!("{{{width}\\C{color};{text}}}");
|
m.value = format!("{{{width}\\C{color};{}}}", mtext_escape(text));
|
||||||
if *just == 2 {
|
if *just == 2 {
|
||||||
let up = -0.33 * height;
|
let up = -0.33 * height;
|
||||||
m.insertion_point = Vector3::new(
|
m.insertion_point = Vector3::new(
|
||||||
@@ -549,7 +559,11 @@ fn mtext_of_node(
|
|||||||
color: Option<u16>,
|
color: Option<u16>,
|
||||||
) -> Option<EntityType> {
|
) -> Option<EntityType> {
|
||||||
let (_, first, height, rotation, _) = lines.iter().find_map(text_line)?;
|
let (_, first, height, rotation, _) = lines.iter().find_map(text_line)?;
|
||||||
let texts: Vec<&str> = lines.iter().filter_map(text_line).map(|l| l.0).collect();
|
let texts: Vec<String> = lines
|
||||||
|
.iter()
|
||||||
|
.filter_map(text_line)
|
||||||
|
.map(|l| mtext_escape(l.0))
|
||||||
|
.collect();
|
||||||
let mut m = MText::new();
|
let mut m = MText::new();
|
||||||
let joined = texts.join("\\P");
|
let joined = texts.join("\\P");
|
||||||
m.value = match color {
|
m.value = match color {
|
||||||
@@ -783,8 +797,11 @@ impl<'a> Converter<'a> {
|
|||||||
DgnGeom::Line(a, b) => vec![EntityType::Line(Line::from_points(v3(*a), v3(*b)))],
|
DgnGeom::Line(a, b) => vec![EntityType::Line(Line::from_points(v3(*a), v3(*b)))],
|
||||||
// A filled shape is its outline and a solid hatch inside it.
|
// A filled shape is its outline and a solid hatch inside it.
|
||||||
DgnGeom::Points(pts) if pts.len() >= 2 => {
|
DgnGeom::Points(pts) if pts.len() >= 2 => {
|
||||||
let fill = element.filled.then(|| solid_hatch(pts, &[])).flatten();
|
let mut out = vec![polyline(pts, &[])];
|
||||||
std::iter::once(polyline(pts, &[])).chain(fill).collect()
|
if element.filled {
|
||||||
|
out.extend(solid_hatch(pts, &[]));
|
||||||
|
}
|
||||||
|
out
|
||||||
}
|
}
|
||||||
DgnGeom::Points(_) => Vec::new(),
|
DgnGeom::Points(_) => Vec::new(),
|
||||||
DgnGeom::Arc {
|
DgnGeom::Arc {
|
||||||
@@ -816,8 +833,11 @@ impl<'a> Converter<'a> {
|
|||||||
if points.len() < 2 {
|
if points.len() < 2 {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
let fill = element.filled.then(|| solid_hatch(&points, &bulges)).flatten();
|
let mut out = vec![polyline(&points, &bulges)];
|
||||||
std::iter::once(polyline(&points, &bulges)).chain(fill).collect()
|
if element.filled {
|
||||||
|
out.extend(solid_hatch(&points, &bulges));
|
||||||
|
}
|
||||||
|
out
|
||||||
}
|
}
|
||||||
// V7 text is TEXT, V8 text an MTEXT.
|
// V7 text is TEXT, V8 text an MTEXT.
|
||||||
DgnGeom::Text { font: Some(_), .. } => {
|
DgnGeom::Text { font: Some(_), .. } => {
|
||||||
@@ -980,8 +1000,8 @@ pub fn convert(import: &DgnImport, sub_units: bool, explode_text_nodes: bool) ->
|
|||||||
c.out.entities = entities;
|
c.out.entities = entities;
|
||||||
// A cell left without a block (the point display cell, an empty or
|
// A cell left without a block (the point display cell, an empty or
|
||||||
// missing definition) places nothing: its instances are dropped.
|
// missing definition) places nothing: its instances are dropped.
|
||||||
let defined: std::collections::HashSet<String> =
|
let blocks = &c.out.blocks;
|
||||||
c.out.blocks.iter().map(|b| b.0.to_ascii_uppercase()).collect();
|
let defined: HashSet<String> = blocks.iter().map(|b| b.0.to_ascii_uppercase()).collect();
|
||||||
let placed = |e: &EntityType| match e {
|
let placed = |e: &EntityType| match e {
|
||||||
EntityType::Insert(i) => defined.contains(&i.block_name.to_ascii_uppercase()),
|
EntityType::Insert(i) => defined.contains(&i.block_name.to_ascii_uppercase()),
|
||||||
_ => true,
|
_ => true,
|
||||||
|
|||||||
@@ -293,7 +293,8 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]], d3: bool, depth: usize) -> Raw {
|
|||||||
// covers; they are already placed, so the cell keeps them as
|
// covers; they are already placed, so the cell keeps them as
|
||||||
// they are. Nesting past `MAX_CELL_DEPTH` is left out.
|
// they are. Nesting past `MAX_CELL_DEPTH` is left out.
|
||||||
2 => {
|
2 => {
|
||||||
let mut left = (38 + uint16(e, 36).unwrap_or(0) as usize * 2).saturating_sub(e.len());
|
let total = 38 + uint16(e, 36).unwrap_or(0) as usize * 2;
|
||||||
|
let mut left = total.saturating_sub(e.len());
|
||||||
let start = index;
|
let start = index;
|
||||||
while left > 0 && index < run.len() && run[index][0] & 0x80 != 0 {
|
while left > 0 && index < run.len() && run[index][0] & 0x80 != 0 {
|
||||||
left = left.saturating_sub(run[index].len());
|
left = left.saturating_sub(run[index].len());
|
||||||
|
|||||||
@@ -284,7 +284,6 @@ fn f64_at(e: &[u8], at: usize) -> Option<f64> {
|
|||||||
Some(f64::from_le_bytes(e.get(at..at + 8)?.try_into().ok()?))
|
Some(f64::from_le_bytes(e.get(at..at + 8)?.try_into().ok()?))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A string stored after the "ff fe 01 00" marker, when the element has one.
|
|
||||||
/// The text style a text element uses: the id in its text style linkage
|
/// The text style a text element uses: the id in its text style linkage
|
||||||
/// (the four bytes after `04 20 e6 04 00 00`).
|
/// (the four bytes after `04 20 e6 04 00 00`).
|
||||||
fn text_style_id(e: &[u8]) -> Option<u32> {
|
fn text_style_id(e: &[u8]) -> Option<u32> {
|
||||||
@@ -313,6 +312,7 @@ fn text_style_table(nm: &[&[u8]]) -> Vec<(u32, String)> {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A string stored after the "ff fe 01 00" marker, when the element has one.
|
||||||
fn marked_string(e: &[u8], from: usize) -> Option<String> {
|
fn marked_string(e: &[u8], from: usize) -> Option<String> {
|
||||||
let i = e
|
let i = e
|
||||||
.get(from..)?
|
.get(from..)?
|
||||||
@@ -446,8 +446,7 @@ impl Styles {
|
|||||||
/// A B-spline curve from its order and poles, with uniform knots. A closed
|
/// A B-spline curve from its order and poles, with uniform knots. A closed
|
||||||
/// curve (its last pole repeating the first) is periodic: its distinct
|
/// curve (its last pole repeating the first) is periodic: its distinct
|
||||||
/// poles wrap around with knots (i + 1) / (count + 1), and the curve is
|
/// poles wrap around with knots (i + 1) / (count + 1), and the curve is
|
||||||
/// clamped at both ends of its domain by knot insertion, as the reference
|
/// clamped at both ends of its domain by knot insertion.
|
||||||
/// converts it.
|
|
||||||
fn bspline_geom(order: usize, poles: Vec<[f64; 3]>, closed: bool) -> DgnGeom {
|
fn bspline_geom(order: usize, poles: Vec<[f64; 3]>, closed: bool) -> DgnGeom {
|
||||||
let degree = order - 1;
|
let degree = order - 1;
|
||||||
let distinct = poles.len() - usize::from(closed && poles.first() == poles.last());
|
let distinct = poles.len() - usize::from(closed && poles.first() == poles.last());
|
||||||
@@ -914,8 +913,14 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
|
|||||||
// The byte count at 110 (with the 4-byte marker)
|
// The byte count at 110 (with the 4-byte marker)
|
||||||
// ends the characters.
|
// ends the characters.
|
||||||
{
|
{
|
||||||
let count = u16::from_le_bytes(e.get(110..112)?.try_into().ok()?) as usize;
|
let count =
|
||||||
marked_string(e, 200)?.chars().take(count.saturating_sub(4)).collect()
|
u16::from_le_bytes(e.get(110..112)?.try_into().ok()?) as usize;
|
||||||
|
let mark = e
|
||||||
|
.get(200..)?
|
||||||
|
.windows(4)
|
||||||
|
.position(|w| w == [0xff, 0xfe, 0x01, 0x00])?
|
||||||
|
+ 200;
|
||||||
|
marked_string(&e[..(mark + count.max(4)).min(e.len())], mark)?
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
})()
|
})()
|
||||||
@@ -1405,9 +1410,6 @@ fn unit_name(metres: f64) -> String {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The named model element by element (DGNIMPORT): in master units with
|
|
||||||
/// the global origin at 0,0, shared cell definitions in master units, and
|
|
||||||
/// every level by id with its own name and colour.
|
|
||||||
/// A design model (not a sheet): the model type byte of its header (4105)
|
/// A design model (not a sheet): the model type byte of its header (4105)
|
||||||
/// is 2 for a sheet.
|
/// is 2 for a sheet.
|
||||||
fn is_design(header: &[u8]) -> bool {
|
fn is_design(header: &[u8]) -> bool {
|
||||||
@@ -1503,6 +1505,9 @@ fn references(control: &[u8]) -> Vec<DgnReference> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The named model element by element (DGNIMPORT): in master units with
|
||||||
|
/// the global origin at 0,0, shared cell definitions in master units, and
|
||||||
|
/// every level by id with its own name and colour.
|
||||||
pub fn import(bytes: &[u8], name: &str) -> Option<DgnImport> {
|
pub fn import(bytes: &[u8], name: &str) -> Option<DgnImport> {
|
||||||
let cfb = Cfb::open(bytes)?;
|
let cfb = Cfb::open(bytes)?;
|
||||||
let all = models(&cfb);
|
let all = models(&cfb);
|
||||||
@@ -1546,8 +1551,8 @@ pub fn import(bytes: &[u8], name: &str) -> Option<DgnImport> {
|
|||||||
while j < nm.len() && is_component(nm[j]) {
|
while j < nm.len() && is_component(nm[j]) {
|
||||||
j += 1;
|
j += 1;
|
||||||
}
|
}
|
||||||
// The cell that draws points is the writer's own; the reference
|
// The cell that draws points belongs to the writing program,
|
||||||
// leaves it out of the drawing.
|
// not to the drawing: it is left out.
|
||||||
if !cell.eq_ignore_ascii_case("POINT_DISPLAY") {
|
if !cell.eq_ignore_ascii_case("POINT_DISPLAY") {
|
||||||
let def = read_run(&nm[i + 1..j], &styles, &IDENTITY).elements;
|
let def = read_run(&nm[i + 1..j], &styles, &IDENTITY).elements;
|
||||||
cells.push((cell, scaled_elements(&def, s, [0.0, 0.0])));
|
cells.push((cell, scaled_elements(&def, s, [0.0, 0.0])));
|
||||||
|
|||||||
Reference in new issue
Block a user