forked from 1b-agent/opencadstudio-upstream
fix(linetype): catalog complex linetypes keep their shapes and text
The loader added every catalog linetype to the drawing with its [shape] and ["text"] elements stripped, and the renderer reads the drawing's own definition first — so BATTING, ZIGZAG, PLANT, POINT1/2, DRAINAGE, FENCELINE, TRACKS, GAS_LINE, HOT_WATER_SUPPLY drew as plain dashes (a near-solid line), and a saved file carried no shapes either. - populate_document_from_source now stores each shape / text on the element it follows, with a shape-file STYLE (empty name, one per file) and the named text style; values keep their written decimals. - One table maps shape names to numbers per shape file: the standard ltypeshp 130–134, the bundled catalog's own beyond them. The renderer resolves stored numbers through it instead of a hard-coded list that ignored which file a number belonged to. - A drawing's copy stripped to exactly the catalog's dashes (as this loader saved them) gets its shapes and text back on open. - Opening completes the linetypes before the geometry is prepared, so the first view already draws them. Fixes #665 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
6dbdeb62e8
commit
593f7ca3e9
2 files changed
+157
-17
No files matched your search
+153
-17
@@ -157,18 +157,11 @@ pub fn document_lt_segments(document: &CadDocument, name: &str) -> Option<Comple
|
|||||||
let resolved = (!font.is_empty())
|
let resolved = (!font.is_empty())
|
||||||
.then(|| crate::io::resolve_image_file(&font, base))
|
.then(|| crate::io::resolve_image_file(&font, base))
|
||||||
.flatten();
|
.flatten();
|
||||||
// The standard ltypeshp.shx numbers map to the bundled
|
// The standard ltypeshp.shx numbers (and the bundled
|
||||||
// LFF substitute shapes, so the glyph still draws when
|
// catalog's own) map to the LFF substitute shapes, so the
|
||||||
// the shape file isn't on disk (the usual case — traded
|
// glyph still draws when the shape file isn't on disk
|
||||||
// drawings rarely ship their .shx).
|
// (the usual case — traded drawings rarely ship their .shx).
|
||||||
let sub_name = match *shape_number {
|
let sub_name = bundled_shape_name(&font, *shape_number).unwrap_or("");
|
||||||
130 => "TRACK1",
|
|
||||||
131 => "ZIG",
|
|
||||||
132 => "BOX",
|
|
||||||
133 => "CIRC1",
|
|
||||||
134 => "BAT",
|
|
||||||
_ => "",
|
|
||||||
};
|
|
||||||
if resolved.is_none() && sub_name.is_empty() {
|
if resolved.is_none() && sub_name.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -268,20 +261,163 @@ pub fn populate_document(doc: &mut CadDocument) {
|
|||||||
populate_document_from_source(doc, LIN_SOURCE);
|
populate_document_from_source(doc, LIN_SOURCE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add all simple linetypes parsed from a caller-supplied `.lin` source.
|
/// Add the linetypes of a caller-supplied `.lin` source, embedded shapes and
|
||||||
|
/// text included, so the drawing draws them and a saved file carries them.
|
||||||
/// Returns the number of definitions newly registered in the drawing.
|
/// Returns the number of definitions newly registered in the drawing.
|
||||||
|
///
|
||||||
|
/// A linetype the drawing already has keeps its own definition, with one
|
||||||
|
/// repair: a copy stripped to its dashes (as this loader used to add them,
|
||||||
|
/// and so saved) gets its shapes and text back when its dashes match the
|
||||||
|
/// source's exactly.
|
||||||
pub fn populate_document_from_source(doc: &mut CadDocument, source: &str) -> usize {
|
pub fn populate_document_from_source(doc: &mut CadDocument, source: &str) -> usize {
|
||||||
|
let complex = parse_complex(source);
|
||||||
let mut added = 0;
|
let mut added = 0;
|
||||||
for mut lt in parse(source) {
|
for mut lt in parse(source) {
|
||||||
if !doc.line_types.contains(<.name) {
|
let segments = complex.get(<.name.to_ascii_uppercase());
|
||||||
lt.set_handle(doc.allocate_handle());
|
if let Some(existing) = doc.line_types.get(<.name) {
|
||||||
doc.line_types.add(lt).ok();
|
let stripped = segments.is_some()
|
||||||
added += 1;
|
&& !existing.is_complex()
|
||||||
|
&& existing.elements.len() == lt.elements.len()
|
||||||
|
&& existing
|
||||||
|
.elements
|
||||||
|
.iter()
|
||||||
|
.zip(<.elements)
|
||||||
|
.all(|(a, b)| (a.length - b.length).abs() <= 1e-9 * b.length.abs().max(1.0));
|
||||||
|
if stripped {
|
||||||
|
if let Some(elements) = segments.and_then(|s| complex_elements(doc, s, <.elements)) {
|
||||||
|
if let Some(existing) = doc.line_types.get_mut(<.name) {
|
||||||
|
existing.elements = elements;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
if let Some(elements) = segments.and_then(|s| complex_elements(doc, s, <.elements)) {
|
||||||
|
lt.elements = elements;
|
||||||
|
}
|
||||||
|
lt.set_handle(doc.allocate_handle());
|
||||||
|
doc.line_types.add(lt).ok();
|
||||||
|
added += 1;
|
||||||
}
|
}
|
||||||
added
|
added
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shape numbers for the shape files the bundled catalog references,
|
||||||
|
/// keyed by file stem and shape name. The `ltypeshp` 130–134 are the
|
||||||
|
/// standard ones; the rest exist only in the bundled LFF substitutes, so
|
||||||
|
/// their numbers are this application's own.
|
||||||
|
const BUNDLED_SHAPES: &[(&str, &str, i16)] = &[
|
||||||
|
("ltypeshp", "TRACK1", 130),
|
||||||
|
("ltypeshp", "ZIG", 131),
|
||||||
|
("ltypeshp", "BOX", 132),
|
||||||
|
("ltypeshp", "CIRC1", 133),
|
||||||
|
("ltypeshp", "BAT", 134),
|
||||||
|
("ltypeshp", "PT1", 135),
|
||||||
|
("ltypeshp", "PT2", 136),
|
||||||
|
("ltypeshp", "PLANT1", 137),
|
||||||
|
("ltypeshp", "PLANT2", 138),
|
||||||
|
("ltypeshp", "PLANT3", 139),
|
||||||
|
("ltypeshp", "PLANT4", 140),
|
||||||
|
("ltypeshp", "PLANT5", 141),
|
||||||
|
("ltypeshp", "PLANT6", 142),
|
||||||
|
("qcadshp", "RIGHT_ARROW", 130),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// A catalog value as the decimal it was written as: 2.54 stays 2.54, not
|
||||||
|
/// the 2.5399999618… an `f32` widens to.
|
||||||
|
fn decimal(value: f32) -> f64 {
|
||||||
|
value.to_string().parse().unwrap_or(value as f64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shape_file_stem(file: &str) -> String {
|
||||||
|
let name = file.rsplit(['/', '\\']).next().unwrap_or(file);
|
||||||
|
name.split('.').next().unwrap_or(name).to_ascii_lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The bundled shape a stored shape number names in `file` (an empty file
|
||||||
|
/// reads as `ltypeshp`).
|
||||||
|
fn bundled_shape_name(file: &str, number: i16) -> Option<&'static str> {
|
||||||
|
let stem = shape_file_stem(file);
|
||||||
|
let stem = if stem.is_empty() { "ltypeshp".to_string() } else { stem };
|
||||||
|
BUNDLED_SHAPES
|
||||||
|
.iter()
|
||||||
|
.find(|(file, _, n)| *file == stem && *n == number)
|
||||||
|
.map(|(_, name, _)| *name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The drawing's shape-file STYLE for `file`, added when missing (an empty
|
||||||
|
/// name, as shape files are stored).
|
||||||
|
fn shape_file_style(doc: &mut CadDocument, file: &str) -> codec::Handle {
|
||||||
|
if let Some(style) = doc
|
||||||
|
.text_styles
|
||||||
|
.iter()
|
||||||
|
.find(|s| s.is_shape_file && s.font_file.eq_ignore_ascii_case(file))
|
||||||
|
{
|
||||||
|
return style.handle;
|
||||||
|
}
|
||||||
|
let mut style = codec::tables::TextStyle::new("");
|
||||||
|
style.handle = doc.allocate_handle();
|
||||||
|
style.is_shape_file = true;
|
||||||
|
style.font_file = file.to_string();
|
||||||
|
let handle = style.handle;
|
||||||
|
doc.text_styles.add_allow_duplicate(style);
|
||||||
|
handle
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Document elements for catalog segments: each shape or text rides on the
|
||||||
|
/// element before it, as in the `.lin` token order. `None` when a piece
|
||||||
|
/// cannot be stored (an unknown shape or text style), so the caller keeps
|
||||||
|
/// the plain dashes rather than a half-built pattern.
|
||||||
|
///
|
||||||
|
/// The lengths come from `plain` — the same pattern parsed at full
|
||||||
|
/// precision — rather than the catalog's `f32` segments.
|
||||||
|
fn complex_elements(
|
||||||
|
doc: &mut CadDocument,
|
||||||
|
lt: &ComplexLt,
|
||||||
|
plain: &[LineTypeElement],
|
||||||
|
) -> Option<Vec<LineTypeElement>> {
|
||||||
|
use codec::tables::linetype::LineTypeComplexData;
|
||||||
|
let mut out: Vec<LineTypeElement> = Vec::new();
|
||||||
|
let mut lengths = plain.iter();
|
||||||
|
for segment in <.segments {
|
||||||
|
let complex = match segment {
|
||||||
|
LtSegment::Dash(_) | LtSegment::Space(_) | LtSegment::Dot => {
|
||||||
|
out.push(lengths.next()?.clone());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
LtSegment::Shape { name, shx_file, x, y, scale, rot_deg, .. } => {
|
||||||
|
let stem = shape_file_stem(shx_file);
|
||||||
|
let number = BUNDLED_SHAPES
|
||||||
|
.iter()
|
||||||
|
.find(|(file, shape, _)| *file == stem && shape.eq_ignore_ascii_case(name))?
|
||||||
|
.2;
|
||||||
|
LineTypeComplexData {
|
||||||
|
content: LineTypeComplexContent::Shape { shape_number: number },
|
||||||
|
style_handle: shape_file_style(doc, shx_file),
|
||||||
|
scale: decimal(*scale),
|
||||||
|
rotation: decimal(*rot_deg).to_radians(),
|
||||||
|
absolute_rotation: false,
|
||||||
|
offset: [decimal(*x), decimal(*y)],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LtSegment::Text { text, style, x, y, scale, rot_deg } => LineTypeComplexData {
|
||||||
|
content: LineTypeComplexContent::Text { text: text.clone() },
|
||||||
|
style_handle: doc
|
||||||
|
.text_styles
|
||||||
|
.iter()
|
||||||
|
.find(|s| !s.is_shape_file && s.name.eq_ignore_ascii_case(style))?
|
||||||
|
.handle,
|
||||||
|
scale: decimal(*scale),
|
||||||
|
rotation: decimal(*rot_deg).to_radians(),
|
||||||
|
absolute_rotation: false,
|
||||||
|
offset: [decimal(*x), decimal(*y)],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
out.last_mut()?.complex = Some(complex);
|
||||||
|
}
|
||||||
|
(lengths.next().is_none()).then_some(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ── Parser ────────────────────────────────────────────────────────────────
|
// ── Parser ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -355,6 +355,10 @@ async fn open_path_with_phase_attempt(
|
|||||||
recoverable: true,
|
recoverable: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Before the geometry is prepared below: completing the
|
||||||
|
// drawing's catalog linetypes (and repairing stripped ones)
|
||||||
|
// after it would leave the first view drawn without them.
|
||||||
|
crate::io::linetypes::populate_document(&mut doc);
|
||||||
progress2.set(crate::app::OPEN_PHASE_XREF, 6000, 0, 1);
|
progress2.set(crate::app::OPEN_PHASE_XREF, 6000, 0, 1);
|
||||||
let t_xref = Instant::now();
|
let t_xref = Instant::now();
|
||||||
let (xref_infos, xref_dropped) = if let Some(base_dir) = path2.parent() {
|
let (xref_infos, xref_dropped) = if let Some(base_dir) = path2.parent() {
|
||||||
|
|||||||
Reference in new issue
Block a user