use super::{
annotation_types::AnnotationPlaneFrame, mapping, reference, source::Source, AppearancePlan,
Mapping, MappingFrame,
};
use ifc_lite_core::{AttributeValue as A, DecodedEntity, IfcType};
use rustc_hash::FxHashMap;
use serde_json::{json, Value};
use std::sync::Arc;
pub(super) fn vector(v: [f64; 3]) -> A {
A::List(v.into_iter().map(A::Float).collect())
}
pub(super) fn refs(ids: &[u32]) -> A {
A::List(ids.iter().copied().map(A::EntityRef).collect())
}
fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a.iter().zip(b).map(|(a, b)| a * b).sum()
}
fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
pub(super) struct Metadata<'a> {
pub schema: &'a str,
pub source_revision: &'a str,
pub next_express_id: u32,
pub container_id: u32,
pub global_id: &'a str,
pub containment_global_id: &'a str,
pub name: &'a str,
}
pub(super) struct Authoring<'a> {
pub author: Author,
pub source: Source<'a>,
pub placement: u32,
pub context_id: u32,
pub owner: A,
pub scale: f64,
pub rtc_offset: [f64; 3],
}
fn valid_guid(value: &str) -> bool {
value.len() == 22
&& value.as_bytes()[0] <= b'3'
&& value.as_bytes()[0] >= b'0'
&& value
.bytes()
.all(|c| c.is_ascii_alphanumeric() || c == b'_' || c == b'$')
}
fn wire(value: &A) -> Value {
match value {
A::EntityRef(id) => reference(*id),
A::Null => Value::Null,
A::Derived => json!("*"),
A::Enum(s) => json!(format!(".{s}.")),
A::String(s) => json!(s),
A::Integer(n) => json!(n),
A::Float(n) => json!(n),
A::List(v) => json!(v.iter().map(wire).collect::<Vec<_>>()),
}
}
pub(super) struct Author {
pub plan: AppearancePlan,
pub entities: FxHashMap<u32, Arc<DecodedEntity>>,
}
impl Author {
pub fn add(&mut self, ty: IfcType, attributes: Vec<A>) -> u32 {
let id = super::add(
&mut self.plan,
ty.name(),
attributes.iter().map(wire).collect(),
);
self.entities
.insert(id, Arc::new(DecodedEntity::new(id, ty, attributes)));
id
}
}
pub(super) fn validate_metadata(r: &Metadata<'_>) -> Result<(),String> {
if !matches!(r.schema, "IFC4" | "IFC4X3")
|| r.source_revision.len() > 4096
|| r.name.len() > 1024
|| !valid_guid(r.global_id)
|| !valid_guid(r.containment_global_id)
|| r.global_id == r.containment_global_id
{
return Err(
"Annotation needs IFC4/IFC4X3, bounded metadata and distinct valid IFC GlobalIds"
.into(),
);
}
super::wire_text::validate(r.name, "Authored product Name")?;
Ok(())
}
pub(super) fn prepare<'a>(
bytes: &'a [u8],
request: &Metadata<'_>,
frame: &AnnotationPlaneFrame,
reserve: usize,
) -> Result<Authoring<'a>, String> {
let r = request;
validate_metadata(r)?;
let f = frame;
mapping::validate(&Mapping::Planar {
frame: MappingFrame::World,
origin: f.origin,
axis_u: f.axis_u,
axis_v: f.axis_v,
metres_per_tile: f.size_metres,
})?;
if (dot(f.axis_u, f.axis_u) - 1.).abs() > 1e-10
|| (dot(f.axis_v, f.axis_v) - 1.).abs() > 1e-10
|| dot(f.axis_u, f.axis_v).abs() > 1e-10
{
return Err("Annotation image frame must have orthonormal axes".into());
}
let mut source = Source::new(bytes)?;
if source
.types
.len()
.checked_add(reserve)
.is_none_or(|n| n > 200_000)
|| r.next_express_id <= source.types.last_key_value().map_or(0, |(id, _)| *id)
|| u32::try_from(reserve)
.ok()
.and_then(|n| r.next_express_id.checked_add(n))
.is_none()
{
return Err("Annotation allocator or entity budget is exhausted/stale".into());
}
let ids: Vec<_> = source
.types
.iter()
.filter(|(_, t)| t.is_subtype_of(IfcType::IfcRoot))
.map(|(id, _)| *id)
.collect();
for id in ids {
let entity = source.entity(id)?;
if entity
.get_string(0)
.is_some_and(|guid| guid == r.global_id || guid == r.containment_global_id)
{
return Err("Annotation GlobalId already exists in the effective model".into());
}
}
let container = source.entity(r.container_id)?;
if !container.ifc_type.is_subtype_of(IfcType::IfcSpatialElement) {
return Err("Choose an effective IfcSpatialElement as annotation container".into());
}
source.validate_world_placement(&container)?;
let projects: Vec<_> = source
.types
.iter()
.filter(|(_, t)| **t == IfcType::IfcProject)
.map(|(id, _)| *id)
.collect();
if projects.len() != 1 {
return Err("Annotation creation needs one unambiguous IfcProject".into());
}
let project = source.entity(projects[0])?;
let mut contexts = Vec::new();
for id in super::source::refs(project.get(7))? {
let context = source.entity(id)?;
if context.ifc_type == IfcType::IfcGeometricRepresentationContext
&& context.get(2).and_then(A::as_int) == Some(3)
{
contexts.push(id);
}
}
if contexts.len() != 1 {
return Err("Choose a model with one unambiguous root 3D representation context".into());
}
let scale = source.decoder.length_unit_scale();
if !scale.is_finite() || scale <= 0. {
return Err("Invalid model length unit".into());
}
let context = source
.context
.as_ref()
.ok_or("Missing canonical load context")?;
let rtc_offset = if context.meta.needs_shift {
context.meta.rtc_offset.into()
} else {
[0.; 3]
};
let transform = context
.router()
.resolve_scaled_placement(&container, &mut source.decoder)
.map_err(|e| e.to_string())?;
let columns: [[f64; 3]; 3] =
std::array::from_fn(|i| std::array::from_fn(|j| transform[i * 4 + j]));
if transform.iter().any(|v| !v.is_finite())
|| (0..3).any(|i| {
(0..3)
.any(|j| (dot(columns[i], columns[j]) - if i == j { 1. } else { 0. }).abs() > 1e-10)
})
|| dot(cross(columns[0], columns[1]), columns[2]) < 0.999999
{
return Err(
"Annotation container placement must be a finite right-handed rigid frame".into(),
);
}
let inverse = |v: [f64; 3]| columns.map(|c| dot(c, v));
let origin =
inverse(std::array::from_fn(|i| f.origin[i] - transform[12 + i])).map(|v| v / scale);
let u = inverse(f.axis_u);
let normal = inverse(cross(f.axis_u, f.axis_v));
let size = f.size_metres.map(|v| v / scale);
if origin.iter().chain(&size).any(|v| !v.is_finite()) {
return Err("Annotation placement exceeds numeric range".into());
}
let mut author = Author {
plan: AppearancePlan {
source_revision: r.source_revision.to_owned(),
next_express_id: r.next_express_id,
next_available_express_id: r.next_express_id,
..Default::default()
},
entities: FxHashMap::default(),
};
let point = author.add(IfcType::IfcCartesianPoint, vec![vector(origin)]);
let axis = author.add(IfcType::IfcDirection, vec![vector(normal)]);
let direction = author.add(IfcType::IfcDirection, vec![vector(u)]);
let axes = author.add(
IfcType::IfcAxis2Placement3D,
vec![
A::EntityRef(point),
A::EntityRef(axis),
A::EntityRef(direction),
],
);
let placement = author.add(
IfcType::IfcLocalPlacement,
vec![
container.get_ref(5).map_or(A::Null, A::EntityRef),
A::EntityRef(axes),
],
);
Ok(Authoring {
author,
source,
placement,
context_id: contexts[0],
owner: project.get_ref(1).map_or(A::Null, A::EntityRef),
scale,
rtc_offset,
})
}