ifc_lite_processing/appearance/
transfer.rs1use super::{
6 page_raster::Raster,
7 transfer_budget::TransferBudget,
8 transfer_math::validate_frame,
9 transfer_sampler::{accumulate, TransferSampler},
10 transfer_surface::Surface,
11 transfer_types::*,
12 *,
13};
14use sha2::{Digest, Sha256};
15
16pub fn plan_mesh_transfer(
17 bytes: &[u8],
18 request: &MeshTransferRequest,
19 rgba: &[u8],
20) -> Result<MeshTransferPlan, String> {
21 if request.product_ids.is_empty()
22 || request.product_ids.len() > 10_000
23 || bytes.len() > 128 * 1024 * 1024
24 || rgba.len() > 128 * 1024 * 1024
25 {
26 return Err("Transfer input or product scope exceeds its budget".into());
27 }
28 if !request.max_distance_metres.is_finite()
29 || request.max_distance_metres <= 0.
30 || request.max_distance_metres > 10.
31 || !request.min_normal_dot.is_finite()
32 || request.min_normal_dot <= 0.
33 || request.min_normal_dot > 1.
34 || !request.ambiguity_distance_metres.is_finite()
35 || request.ambiguity_distance_metres < 0.
36 || request.ambiguity_distance_metres > request.max_distance_metres
37 {
38 return Err("Transfer requires bounded positive distance, oriented normal threshold, and nonnegative ambiguity distance".into());
39 }
40 if !matches!(request.schema.as_str(), "IFC4" | "IFC4X3")
41 || request.source_revision.len() > 256
42 || request.source_images.len() > 10_000
43 || request
44 .source_images
45 .iter()
46 .any(|image| image.image_uri.is_empty() || image.image_uri.len() > 4096)
47 {
48 return Err(
49 "Transfer schema, revision or target raster identities exceed their bounds".into(),
50 );
51 }
52 validate_frame(&request.target_from_ifc_world)?;
53 let registration = register_scan_correspondences(&request.registration)?;
54 if registration.request_sha256 != request.registration_sha256 {
55 return Err(
56 "Transfer registration digest does not match its frozen correspondence request".into(),
57 );
58 }
59 if format!("{:x}", Sha256::digest(bytes)) != registration.target_frame.asset_sha256 {
60 return Err("Transfer target snapshot does not match the frozen registration frame".into());
61 }
62 let source_frame = TransferFrame {
63 rotation: registration.rotation,
64 source_anchor: registration.source_anchor,
65 target_anchor: registration.target_anchor,
66 };
67 let mut budget = TransferBudget::new();
68 budget.reserve(rgba.len())?;
69 let image = Raster::supplied(&request.source_image, rgba)?;
70 budget.charge(image.rgba.len() / 4)?;
71 if image.rgba.chunks_exact(4).any(|p| p[3] != 255) {
72 return Err("Transfer source image must be opaque; alpha appearance is unsupported".into());
73 }
74 let surface = Surface::new(request, &source_frame, &mut budget)?;
75 let prepared_sha256 = digest(bytes, request, rgba)?;
76 let mut sampler = TransferSampler::new(
77 surface,
78 budget,
79 image,
80 &request.target_from_ifc_world,
81 [request.source_mesh.repeat_s, request.source_mesh.repeat_t],
82 );
83 let spec = AppearanceRequest {
86 representation_policy: RepresentationPolicy::Preserve,
87 schema: request.schema.clone(),
88 source_revision: request.source_revision.clone(),
89 next_express_id: request.next_express_id,
90 product_ids: request.product_ids.clone(),
91 image_uri: "textures/prepared-transfer.png".into(),
92 repeat_s: false,
93 repeat_t: false,
94 mapping: Mapping::Box {
95 frame: MappingFrame::Item,
96 origin: [0.; 3],
97 metres_per_tile: [1.; 3],
98 },
99 };
100 let output = atlas_plan::plan_sampled_appearance(
101 bytes,
102 &spec,
103 &request.source_images,
104 rgba,
105 request.texels_per_metre,
106 &mut sampler,
107 )?;
108 let mut coverage = TransferCoverage::default();
109 for item in &sampler.items {
110 accumulate(&mut coverage, &item.coverage);
111 }
112 let sufficient_counts =
113 request.registration.fit.len() >= 4 && request.registration.held_out.len() >= 4;
114 let applicable = coverage.observed_raster_interior_texels > 0 && sufficient_counts;
115 let mut diagnostics=vec!["Coverage is a triangle-area-weighted centroid/interior-texel estimate, not a registration accuracy approval".into(),
116 "Unknown samples preserve the existing target albedo through the shared atlas; source GLB byte-to-decoded-mesh/image identity is verified by the host".into()];
117 diagnostics.extend(registration.diagnostics.clone());
118 if !sufficient_counts {
119 diagnostics.push("Insufficient operational registration evidence: application acceptance requires at least 4 fit and 4 spatially distributed held-out observations; this is a calculation-only plan".into());
120 }
121 if coverage.observed_samples == 0 {
122 diagnostics
123 .push("No observed target samples; no applicable mutation plan was produced".into());
124 }
125 if coverage.observed_samples > 0 && coverage.observed_raster_interior_texels == 0 {
126 diagnostics.push("No observed interior raster texels; centroid observations alone do not establish emitted scan appearance, so no applicable mutation plan was produced".into());
127 }
128 let exclusions = output.plan.exclusions.clone();
129 Ok(MeshTransferPlan {
130 output: applicable.then_some(output),
131 texels_per_metre: request.texels_per_metre,
132 transfer: MeshTransferSummary {
133 prepared_sha256,
134 registration_sha256: registration.request_sha256.clone(),
135 registration,
136 applicable,
137 coverage,
138 items: sampler.items,
139 exclusions,
140 diagnostics,
141 },
142 })
143}
144fn digest(bytes: &[u8], request: &MeshTransferRequest, rgba: &[u8]) -> Result<String, String> {
145 struct Writer(Sha256);
146 impl std::io::Write for Writer {
147 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
148 self.0.update(bytes);
149 Ok(bytes.len())
150 }
151 fn flush(&mut self) -> std::io::Result<()> {
152 Ok(())
153 }
154 }
155 let mut hash = Writer(Sha256::new());
156 hash.0.update(b"ifclite-mesh-transfer-v2-raster-guards\0");
157 hash.0.update((bytes.len() as u64).to_le_bytes());
159 hash.0.update(bytes);
160 hash.0.update((rgba.len() as u64).to_le_bytes());
161 hash.0.update(rgba);
162 serde_json::to_writer(&mut hash, request).map_err(|e| e.to_string())?;
163 Ok(format!("{:x}", hash.0.finalize()))
164}
165#[cfg(test)]
166#[path = "transfer_tests.rs"]
167mod tests;