1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
use super::output_cap::SymbolicAccumulator;
use super::rebase::RenderFrameRebase;
use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcType};
use std::collections::HashMap;
use super::color::resolve_color_via_styles;
use super::primitives::{SymbolicText};
use super::transform::{compose_transforms, parse_axis2_placement_2d, Transform2D};
// ────────────────────────────────────────────────────────────────────────────
// Text extraction (IfcTextLiteral / IfcTextLiteralWithExtent).
// ────────────────────────────────────────────────────────────────────────────
#[allow(clippy::too_many_arguments)]
pub(super) fn extract_text_literal(
item: &DecodedEntity,
decoder: &mut EntityDecoder,
express_id: u32,
ifc_type: &str,
rep_identifier: &str,
unit_scale: f32,
transform: &Transform2D,
rebase: RenderFrameRebase,
styled_items: &HashMap<u32, Vec<u32>>,
out: &mut SymbolicAccumulator,
) {
let content = match item.get(0).and_then(|a| a.as_string()) {
Some(s) => s.to_string(),
None => return,
};
// `Placement` (IfcTextLiteral attribute 1) is MANDATORY per schema — not
// OPTIONAL like IfcProduct.ObjectPlacement (see transform.rs). A dangling
// ref or an absent/null attribute here is malformed data, not a
// legitimate zero, so both are genuine failures (#2256).
let placement_transform = match item.get_ref(1) {
Some(p_ref) => match decoder.decode_by_id(p_ref) {
Ok(p) => parse_axis2_placement_2d(&p, decoder, unit_scale),
Err(_) => Transform2D::unresolved(), // dangling ref (#2256)
},
None => Transform2D::unresolved(), // mandatory attr absent (#2256)
};
let composed = compose_transforms(transform, &placement_transform);
const CAP_TO_BOX_RATIO: f32 = 0.7;
const FALLBACK_CAP_HEIGHT_M: f32 = 0.18;
let height_model_units = if item.ifc_type == IfcType::IfcTextLiteralWithExtent {
item.get_ref(3)
.and_then(|extent_ref| decoder.decode_by_id(extent_ref).ok())
.and_then(|extent| extent.get(1).and_then(|a| a.as_float()))
.map(|h| (h as f32) * CAP_TO_BOX_RATIO)
.unwrap_or(FALLBACK_CAP_HEIGHT_M / unit_scale.max(1e-6))
} else {
FALLBACK_CAP_HEIGHT_M / unit_scale.max(1e-6)
};
let alignment = if item.ifc_type == IfcType::IfcTextLiteralWithExtent {
item.get(4)
.and_then(|a| a.as_string())
.unwrap_or("")
.to_string()
} else {
String::new()
};
let (wx, wy) = composed.transform_point(0.0, 0.0);
let plan = rebase.plan(wx, wy);
let raw_scale = composed.scale();
// Height keeps a ZERO scale (the glyph collapses exactly as the symbol does);
// only a non-finite scale falls back. The direction below needs the stricter
// positive test, since it divides.
let text_scale = if raw_scale.is_finite() { raw_scale } else { 1.0 };
// Only the X-axis column (m00, m10) feeds the direction — NOT the Y
// column, which is where a mirroring MappingTarget's reflection lives
// (see `parse_cartesian_transformation_operator`). Glyphs therefore stay
// readable (non-mirrored) under a mirroring transform, same as before
// #1994: Axis1 is unaffected by an Axis2 mirror by construction, so this
// is not a special case — it falls out of reading only the X column.
let dir = if raw_scale.is_finite() && raw_scale > 0.0 {
(composed.m00 / raw_scale, -composed.m10 / raw_scale)
} else {
(1.0, 0.0)
};
let color = resolve_color_via_styles(item.id, styled_items, decoder)
.unwrap_or([0.05, 0.05, 0.05, 1.0]);
out.push_text(SymbolicText {
express_id,
ifc_type: ifc_type.to_string(),
x: plan.0,
y: plan.1,
// Direction must stay UNIT: `composed`'s linear block can carry a
// mapped-item Scale (#1985), and consumers read this pair as a bare
// direction vector. Any scale that is not finite and positive (a
// degenerate transform) falls back to +X rather than emitting NaN.
dir_x: dir.0,
dir_y: dir.1,
// The glyph height has to pick that same scale up here: a height never
// passes through `transform_point`.
height: height_model_units * unit_scale * text_scale,
content,
alignment,
world_y: rebase.elevation(composed.tz),
color,
target_px: 0.0,
representation: rep_identifier.to_string(),
});
}