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 <noreply@anthropic.com>
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())
|
||||
.then(|| crate::io::resolve_image_file(&font, base))
|
||||
.flatten();
|
||||
// The standard ltypeshp.shx numbers map to the bundled
|
||||
// LFF substitute shapes, so the glyph still draws when
|
||||
// the shape file isn't on disk (the usual case — traded
|
||||
// drawings rarely ship their .shx).
|
||||
let sub_name = match *shape_number {
|
||||
130 => "TRACK1",
|
||||
131 => "ZIG",
|
||||
132 => "BOX",
|
||||
133 => "CIRC1",
|
||||
134 => "BAT",
|
||||
_ => "",
|
||||
};
|
||||
// The standard ltypeshp.shx numbers (and the bundled
|
||||
// catalog's own) map to the LFF substitute shapes, so the
|
||||
// glyph still draws when the shape file isn't on disk
|
||||
// (the usual case — traded drawings rarely ship their .shx).
|
||||
let sub_name = bundled_shape_name(&font, *shape_number).unwrap_or("");
|
||||
if resolved.is_none() && sub_name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
@@ -268,20 +261,163 @@ pub fn populate_document(doc: &mut CadDocument) {
|
||||
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.
|
||||
///
|
||||
/// 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 {
|
||||
let complex = parse_complex(source);
|
||||
let mut added = 0;
|
||||
for mut lt in parse(source) {
|
||||
if !doc.line_types.contains(<.name) {
|
||||
lt.set_handle(doc.allocate_handle());
|
||||
doc.line_types.add(lt).ok();
|
||||
added += 1;
|
||||
let segments = complex.get(<.name.to_ascii_uppercase());
|
||||
if let Some(existing) = doc.line_types.get(<.name) {
|
||||
let stripped = segments.is_some()
|
||||
&& !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
|
||||
}
|
||||
|
||||
/// 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 ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -355,6 +355,10 @@ async fn open_path_with_phase_attempt(
|
||||
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);
|
||||
let t_xref = Instant::now();
|
||||
let (xref_infos, xref_dropped) = if let Some(base_dir) = path2.parent() {
|
||||
|
||||
Reference in new issue
Block a user