use super::{source::Source, AppearanceItem, AppearanceRequest};
use crate::element::{
produce_element_meshes, ElementJobKind, ElementMeshJob, MeshProductionContext,
MeshProductionOptions,
};
use crate::types::mesh::MeshData;
use ifc_lite_geometry::{ ImageTextureRef, ResolvedTextureMap, TextureSource};
use rustc_hash::FxHashMap;
pub(super) fn align_source_corners(
source: &mut Source<'_>,
product_id: u32,
items: &mut [AppearanceItem],
textures: &FxHashMap<u32, ResolvedTextureMap>,
request: &AppearanceRequest,
) -> Result<(), String> {
let before = produce(source, product_id, textures, None)?;
let replacements = items
.iter()
.map(|item| {
(
item.geometry_item_id,
ResolvedTextureMap {
texture_id: request.next_express_id,
texture: TextureSource::Image(ImageTextureRef {
url: request.image_uri.clone(),
repeat_s: request.repeat_s,
repeat_t: request.repeat_t,
}),
tex_coords: item
.tex_coords
.iter()
.map(|p| p.map(|v| v as f32))
.collect(),
tex_coord_index: Some(item.tex_coord_index.clone()),
},
)
})
.collect();
let after = produce(source, product_id, &replacements, None)?;
if before.len() != items.len() || after.len() != items.len() {
return Err("Canonical representation was split, combined, or rejected".into());
}
for item in items {
let old = one_item(&before, item.geometry_item_id)?;
let new = one_item(&after, item.geometry_item_id)?;
let uvs = new
.uvs
.as_ref()
.ok_or("Canonical geometry did not attach the planned UVs")?;
if old.indices.len() != new.indices.len()
|| old.origin != new.origin
|| uvs.len() != new.positions.len() / 3 * 2
|| new.normals.len() != new.positions.len()
|| new.normals.iter().any(|v| !v.is_finite())
{
return Err("Canonical geometry changed source corner correspondence".into());
}
for (&a, &b) in old.indices.iter().zip(&new.indices) {
let old_position = corner_position(&old.positions, a)?;
let new_position = corner_position(&new.positions, b)?;
if old_position != new_position {
return Err("Appearance would change canonical triangle geometry".into());
}
}
item.target_corner_normals = new.indices.iter().flat_map(|&i| {
let i = i as usize * 3;
[new.normals[i], new.normals[i + 2], -new.normals[i + 1]]
}).collect();
item.source_indices.clone_from(&old.indices);
item.target_indices.clone_from(&new.indices);
item.target_vertex_count = new.positions.len() / 3;
item.preview_corner_uvs = new
.indices
.iter()
.flat_map(|&i| [uvs[i as usize * 2], uvs[i as usize * 2 + 1]])
.collect();
}
Ok(())
}
pub(super) fn corner_position(positions: &[f32], index: u32) -> Result<&[f32], String> {
let index = index as usize;
if index >= positions.len() / 3 {
return Err("Canonical geometry produced an out-of-range corner".into());
}
Ok(&positions[index * 3..index * 3 + 3])
}
fn one_item(meshes: &[MeshData], id: u32) -> Result<&MeshData, String> {
let mut matches = meshes
.iter()
.filter(|mesh| mesh.geometry_item_id == Some(id));
let mesh = matches
.next()
.ok_or("Canonical representation item is missing")?;
if matches.next().is_some() {
return Err("Canonical representation item was split".into());
}
Ok(mesh)
}
pub(super) fn produce(
source: &mut Source<'_>,
product_id: u32,
textures: &FxHashMap<u32, ResolvedTextureMap>,
appearance: Option<&crate::prepass::ResolvedPrepass>,
) -> Result<Vec<MeshData>, String> {
let product = source.entity(product_id)?;
source.validate_world_placement(&product)?;
let scale = source.decoder.length_unit_scale();
if !scale.is_finite() || scale <= 0. {
return Err("Invalid model length unit scale".into());
}
let element_color = appearance.and_then(|styles| {
product.get_ref(6).and_then(|pds| crate::processor::resolve_element_color_for_product_definition_shape(
pds, &styles.geometry_style_index, &mut source.decoder))
.or_else(|| styles.element_material_colors.get(&product_id).and_then(|colors| crate::style::pick_opaque_first(colors)))
});
let context = source.context.as_ref().ok_or("Missing canonical load context")?;
if context.layers.is_sliceable(product_id) {
return Err("Material-layer slicing is unsupported for appearance authoring".into());
}
let router = context.router();
let voids = FxHashMap::default();
let styles = FxHashMap::default();
let colours = FxHashMap::default();
let materials = FxHashMap::default();
let context = MeshProductionContext {
void_index: appearance.map_or(&voids, |a| &a.void_index),
geometry_style_index: appearance.map_or(&styles, |a| &a.geometry_style_index),
indexed_colour_full: appearance.map_or(&colours, |a| &a.indexed_colour_full),
element_material_colors: appearance.map_or(&materials, |a| &a.element_material_colors),
texture_index: textures,
site_local_rotation: None,
};
let produced = produce_element_meshes(
&ElementMeshJob {
id: product_id,
ifc_type: product.ifc_type,
entity: &product,
kind: ElementJobKind::Product,
element_color,
metadata: None,
},
&context,
&MeshProductionOptions::default(),
&mut source.decoder,
&router,
);
if !produced.csg_failures.is_empty() {
return Err("Canonical geometry reported a processing failure".into());
}
Ok(produced.meshes)
}