ifc_lite_processing/appearance/
mod.rs1mod budget;
7mod evaluated;
8mod evaluated_source;
9mod evaluated_openings;
10mod evaluated_replacement;
11mod evaluated_precision;
12mod evaluated_allocation;
13mod annotation;
14mod authored;
15mod pdf_fill;
16mod pdf_fill_types;
17pub use pdf_fill::plan_pdf_fill_annotation;
18pub use pdf_fill_types::{PdfFillAnnotationRequest,PdfFillAnnotationPlan,PdfFillRegion};
19mod captured;
20mod captured_types;
21pub use captured::plan_captured_mesh;
22pub use captured_types::{CapturedMesh, CapturedMeshRequest, CapturedMeshPlan};
23mod annotation_types;
24mod calibration;
25mod registration;
26mod registration_types;
27pub use registration::register_scan_correspondences;
28pub use registration_types::*;
29mod canonical;
30mod catalog;
31mod context;
32mod mapping;
33mod page;
34mod transfer;
35mod transfer_types;
36mod transfer_math;
37mod transfer_budget;
38mod transfer_surface;
39mod transfer_target;
40mod transfer_sampler;
41pub use transfer::plan_mesh_transfer;
42pub use transfer_types::*;
43mod atlas_plan;
44mod page_atlas;
45mod page_raster;
46mod page_types;
47mod page_source;
48mod page_material;
49mod source;
50mod texture_budget;
51mod types;
52use ifc_lite_core::IfcType;
53use serde_json::{json, Value};
54use source::{refs, Source};
55use std::collections::BTreeSet;
56pub use types::*;
57pub use annotation::plan_annotation_plane;
58pub use annotation_types::{AnnotationPlaneFrame, AnnotationPlaneRequest, AnnotationPlanePlan};
59pub use page::plan_page_appearance;
60pub use page_types::*;
61pub use calibration::{calibrate_appearance_plane, CalibratedPlane, PlaneCalibrationRequest};
62pub use catalog::{catalog_appearance, AppearanceCatalog, AppearanceCatalogProduct, AppearanceCatalogRequest, AppearanceCatalogType};
63
64fn reference(id: u32) -> Value {
65 Value::String(format!("#{id}"))
66}
67fn add(plan: &mut AppearancePlan, name: &str, attributes: Vec<Value>) -> u32 {
68 let id = plan.next_available_express_id;
69 plan.next_available_express_id += 1; plan.created.push(CreatedEntity {
71 express_id: id,
72 r#type: name.into(),
73 attributes,
74 });
75 id
76}
77
78mod wire_text;
79
80fn validate_image_uri(uri: &str) -> Result<(), String> {
81 wire_text::validate(uri, "Image URI")?;
82 if uri.is_empty()
83 || uri.len() > 240
84 || !uri.is_ascii()
85 || uri.starts_with('/')
86 || uri.contains(['\\', ':', '?', '#', '%', '\''])
87 || uri.chars().any(char::is_control)
88 || uri
89 .split('/')
90 .any(|part| part.is_empty() || part == "." || part == "..")
91 {
92 return Err("Image URI must be a safe relative asset path".into());
93 }
94 Ok(())
95}
96
97pub fn plan_appearance(
103 bytes: &[u8],
104 request: &AppearanceRequest,
105) -> Result<AppearancePlan, String> {
106 if request.product_ids.is_empty() { return Err("Appearance scope must contain 1..10000 products".into()); }
107 let mut source=Source::new(bytes)?;
108 if request.representation_policy==RepresentationPolicy::EvaluatedOccurrence {
109 let normalized=evaluated::prepare(bytes,request,&mut source)?;
110 let plan=plan_with_source(bytes,normalized.request(),&mut source)?;
111 return Ok(normalized.compose(plan,&source)?.0);
112 }
113 plan_with_source(bytes,request,&mut source)
114}
115
116fn plan_with_source(bytes:&[u8], request:&AppearanceRequest, source:&mut Source<'_>) -> Result<AppearancePlan,String> {
117 if request.schema != "IFC4" && request.schema != "IFC4X3" {
118 return Err("Appearance authoring requires IFC4 or IFC4X3".into());
119 }
120 if request.product_ids.len() > 10_000 {
121 return Err("Appearance scope must contain 1..10000 products".into());
122 }
123 let uri = &request.image_uri;
124 validate_image_uri(uri)?;
125 mapping::validate(&request.mapping)?;
126 texture_budget::preflight(source)?;
127 let textures = ifc_lite_geometry::build_texture_index(bytes, &mut source.decoder);
128 let max_id = source
129 .types
130 .last_key_value()
131 .map(|(id, _)| *id)
132 .unwrap_or(0);
133 if request.next_express_id <= max_id {
134 return Err("Stale allocator watermark overlaps effective IFC source".into());
135 }
136 let mut plan = AppearancePlan {
137 source_revision: request.source_revision.clone(),
138 next_express_id: request.next_express_id,
139 next_available_express_id: request.next_express_id,
140 ..Default::default()
141 };
142 let mut seen = BTreeSet::new();
143 let mut budget = budget::PlanBudget::default();
144 for &product in &request.product_ids {
145 if !seen.insert(product) {
146 continue;
147 }
148 let prepared = (|| {
149 let ids = source.product_items(product)?;
150 let mut items = Vec::new();
151 for id in ids {
152 if let Some(styled) = source
155 .styled_items
156 .get(&id)
157 .and_then(|ids| ids.first())
158 .copied()
159 {
160 let entity = source.entity(styled)?;
161 for style in refs(entity.get(1))? {
162 if !source
163 .types
164 .get(&style)
165 .is_some_and(|t| t.is_subtype_of(IfcType::IfcPresentationStyle))
166 {
167 return Err("Indirect or invalid presentation style assignment".into());
168 }
169 }
170 }
171 items.push(mapping::map_item(source, request, product, id, &mut budget)?);
172 }
173 canonical::align_source_corners(source, product, &mut items, &textures, request)?;
174 Ok::<_, String>(items)
175 })();
176 if budget.exhausted { return Err(budget::BUDGET_ERROR.into()); }
178 match prepared {
179 Ok(items) => plan.items.extend(items),
180 Err(reason) => plan.exclusions.push(Exclusion {
181 product_id: product,
182 reason,
183 }),
184 }
185 }
186 if plan.items.is_empty() {
187 return Ok(plan);
188 }
189 let needed = plan
190 .items
191 .len()
192 .checked_mul(3)
193 .and_then(|n| n.checked_add(5))
194 .ok_or("Appearance plan too large")?;
195 if u64::from(plan.next_express_id) + needed as u64 >= u64::from(u32::MAX) {
196 return Err("Appearance entity id capacity exceeded".into());
197 }
198 let image = add(
199 &mut plan,
200 "IfcImageTexture",
201 vec![
202 json!(if request.repeat_s { ".T." } else { ".F." }),
203 json!(if request.repeat_t { ".T." } else { ".F." }),
204 Value::Null,
205 Value::Null,
206 Value::Null,
207 json!(uri),
208 ],
209 );
210 let colour = add(
211 &mut plan,
212 "IfcColourRgb",
213 vec![Value::Null, json!(1.0), json!(1.0), json!(1.0)],
214 );
215 let shading = add(
216 &mut plan,
217 "IfcSurfaceStyleShading",
218 vec![reference(colour), json!(0.0)],
219 );
220 let texture_style = add(
221 &mut plan,
222 "IfcSurfaceStyleWithTextures",
223 vec![json!([reference(image)])],
224 );
225 let surface_style = add(
226 &mut plan,
227 "IfcSurfaceStyle",
228 vec![
229 json!("Image appearance"),
230 json!(".BOTH."),
231 json!([reference(shading), reference(texture_style)]),
232 ],
233 );
234 let items = std::mem::take(&mut plan.items);
237 for item in &items {
238 let list = add(
239 &mut plan,
240 "IfcTextureVertexList",
241 vec![json!(item.tex_coords)],
242 );
243 add(
244 &mut plan,
245 "IfcIndexedTriangleTextureMap",
246 vec![
247 json!([reference(image)]),
248 reference(item.geometry_item_id),
249 reference(list),
250 json!(item.tex_coord_index),
251 ],
252 );
253 if let Some(old_maps) = source.texture_maps.get(&item.geometry_item_id) {
254 plan.removed.extend(old_maps);
255 }
256 if let Some(styled) = source
257 .styled_items
258 .get(&item.geometry_item_id)
259 .and_then(|ids| ids.first())
260 .copied()
261 {
262 let old = source.entity(styled)?;
263 let mut styles: Vec<Value> = refs(old.get(1))?
264 .into_iter()
265 .filter(|id| source.types.get(id) != Some(&IfcType::IfcSurfaceStyle))
266 .map(reference)
267 .collect();
268 styles.push(reference(surface_style));
269 plan.edits.push(PositionalEdit {
270 express_id: styled,
271 index: 1,
272 value: Value::Array(styles),
273 });
274 } else {
275 add(
276 &mut plan,
277 "IfcStyledItem",
278 vec![
279 reference(item.geometry_item_id),
280 json!([reference(surface_style)]),
281 Value::Null,
282 ],
283 );
284 }
285 }
286 plan.items = items;
287 plan.removed.sort_unstable();
288 plan.removed.dedup();
289 Ok(plan)
290}
291
292#[cfg(test)]
293mod tests;