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