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