///|
fn coordinate_frame_depth(
index : ModelIndex,
id : Int,
path : Array[Int],
) -> Int {
if path.contains(id) {
return path.length()
}
match index.entity(id) {
Some(entity) if entity.name.to_upper() == "IFCLOCALPLACEMENT" =>
match optional_reference_argument(entity, 0) {
Some(parent) => coordinate_frame_depth(index, parent, path + [id])
None => path.length() + 1
}
_ => path.length() + 1
}
}
///|
fn coordinate_frame_name(entity : Entity) -> String {
entity.name + " #" + entity.id.to_string()
}
///|
fn coordinate_frames_from_index(index : ModelIndex) -> Array[CoordinateFrame] {
index
.all()
.fold(init=Array::new(), (result, entity) => {
if entity.name.to_upper() == "IFCLOCALPLACEMENT" {
let transform = placement_transform(index, entity.id).unwrap_or(
Transform3::identity(),
)
result.push({
entity_id: entity.id,
name: coordinate_frame_name(entity),
parent_id: optional_reference_argument(entity, 0),
transform,
depth: coordinate_frame_depth(index, entity.id, []),
})
}
result
})
}
///|
fn coordinate_has_map_conversion(index : ModelIndex) -> Bool {
index.ids_named("IFCMAPCONVERSION").length() > 0
}
///|
fn coordinate_unit_scale(index : ModelIndex) -> Double {
unit_system(index).length_to_meters(1.0)
}
///|
/// Collect placement frames and world bounds for coordinate-aware consumers.
pub fn coordinate_report(model : Model) -> CoordinateReport raise ParseError {
let index = model.index()
let spatial = SpatialIndex::new(model)
let map_count = index.ids_named("IFCMAPCONVERSION").length()
{
frames: coordinate_frames_from_index(index),
world_bounds: spatial.bounds(),
unit_scale: coordinate_unit_scale(index),
has_map_conversion: coordinate_has_map_conversion(index),
warning_count: if map_count > 1 {
map_count - 1
} else {
0
},
}
}
///|
pub fn CoordinateFrame::is_root(self : CoordinateFrame) -> Bool {
self.parent_id is None
}
///|
pub fn CoordinateFrame::origin(self : CoordinateFrame) -> Point3 {
self.transform.apply({ x: 0.0, y: 0.0, z: 0.0 })
}
///|
pub fn CoordinateFrame::apply(self : CoordinateFrame, point : Point3) -> Point3 {
self.transform.apply(point)
}
///|
pub fn CoordinateFrame::headline(self : CoordinateFrame) -> String {
self.name +
" depth=" +
self.depth.to_string() +
" origin=" +
self.origin().x.to_string() +
"," +
self.origin().y.to_string() +
"," +
self.origin().z.to_string()
}
///|
fn coordinate_transform_determinant(transform : Transform3) -> Double {
transform.m00 *
(transform.m11 * transform.m22 - transform.m12 * transform.m21) -
transform.m01 *
(transform.m10 * transform.m22 - transform.m12 * transform.m20) +
transform.m02 *
(transform.m10 * transform.m21 - transform.m11 * transform.m20)
}
///|
/// Invert a general affine transform, including non-uniform scaling.
pub fn inverse_transform(transform : Transform3) -> Transform3? {
let determinant = coordinate_transform_determinant(transform)
if determinant.abs() <= 0.000000000001 {
return None
}
let inverse = 1.0 / determinant
let m00 = (transform.m11 * transform.m22 - transform.m12 * transform.m21) *
inverse
let m01 = (transform.m02 * transform.m21 - transform.m01 * transform.m22) *
inverse
let m02 = (transform.m01 * transform.m12 - transform.m02 * transform.m11) *
inverse
let m10 = (transform.m12 * transform.m20 - transform.m10 * transform.m22) *
inverse
let m11 = (transform.m00 * transform.m22 - transform.m02 * transform.m20) *
inverse
let m12 = (transform.m02 * transform.m10 - transform.m00 * transform.m12) *
inverse
let m20 = (transform.m10 * transform.m21 - transform.m11 * transform.m20) *
inverse
let m21 = (transform.m01 * transform.m20 - transform.m00 * transform.m21) *
inverse
let m22 = (transform.m00 * transform.m11 - transform.m01 * transform.m10) *
inverse
Some({
m00,
m01,
m02,
m10,
m11,
m12,
m20,
m21,
m22,
tx: -(m00 * transform.tx + m01 * transform.ty + m02 * transform.tz),
ty: -(m10 * transform.tx + m11 * transform.ty + m12 * transform.tz),
tz: -(m20 * transform.tx + m21 * transform.ty + m22 * transform.tz),
})
}
///|
pub fn world_to_local(transform : Transform3, point : Point3) -> Point3? {
inverse_transform(transform).map(inverse => inverse.apply(point))
}
///|
pub fn local_to_world(transform : Transform3, point : Point3) -> Point3 {
transform.apply(point)
}
///|
pub fn transform_bounds(
transform : Transform3,
bounds : BoundingBox,
) -> BoundingBox {
if bounds.empty {
return bounds
}
BoundingBox::from_points([
transform.apply({ x: bounds.min.x, y: bounds.min.y, z: bounds.min.z }),
transform.apply({ x: bounds.min.x, y: bounds.min.y, z: bounds.max.z }),
transform.apply({ x: bounds.min.x, y: bounds.max.y, z: bounds.min.z }),
transform.apply({ x: bounds.min.x, y: bounds.max.y, z: bounds.max.z }),
transform.apply({ x: bounds.max.x, y: bounds.min.y, z: bounds.min.z }),
transform.apply({ x: bounds.max.x, y: bounds.min.y, z: bounds.max.z }),
transform.apply({ x: bounds.max.x, y: bounds.max.y, z: bounds.min.z }),
transform.apply({ x: bounds.max.x, y: bounds.max.y, z: bounds.max.z }),
])
}
///|
pub fn CoordinateReport::frame(
self : CoordinateReport,
id : Int,
) -> CoordinateFrame? {
for frame in self.frames {
if frame.entity_id == id {
return Some(frame)
}
}
None
}
///|
pub fn CoordinateReport::roots(
self : CoordinateReport,
) -> Array[CoordinateFrame] {
self.frames.filter(frame => frame.is_root())
}
///|
pub fn CoordinateReport::max_depth(self : CoordinateReport) -> Int {
self.frames.fold(init=0, (result, frame) => {
if frame.depth > result {
frame.depth
} else {
result
}
})
}
///|
pub fn CoordinateReport::is_consistent(self : CoordinateReport) -> Bool {
self.warning_count == 0 && self.frames.all(frame => frame.depth > 0)
}
///|
pub fn CoordinateReport::headline(self : CoordinateReport) -> String {
"frames=" +
self.frames.length().to_string() +
" roots=" +
self.roots().length().to_string() +
" max_depth=" +
self.max_depth().to_string() +
" unit_scale=" +
self.unit_scale.to_string() +
" map_conversion=" +
self.has_map_conversion.to_string()
}
///|
pub fn CoordinateReport::to_json(self : CoordinateReport) -> String {
let frames = self.frames
.map(frame => {
let origin = frame.origin()
"{\"id\":" +
frame.entity_id.to_string() +
",\"name\":\"" +
frame.name +
"\",\"parent_id\":" +
frame.parent_id.map(id => id.to_string()).unwrap_or("null") +
",\"depth\":" +
frame.depth.to_string() +
",\"origin\":[" +
origin.x.to_string() +
"," +
origin.y.to_string() +
"," +
origin.z.to_string() +
"]}"
})
.join(",")
"{\"unit_scale\":" +
self.unit_scale.to_string() +
",\"has_map_conversion\":" +
self.has_map_conversion.to_string() +
",\"warning_count\":" +
self.warning_count.to_string() +
",\"frames\":[" +
frames +
"]}"
}
///|
pub fn CoordinateReport::to_markdown(self : CoordinateReport) -> String {
let builder = StringBuilder()
builder <+ "# Coordinate report\n\n"
builder <+ "- Frames: \{self.frames.length()}\n"
builder <+ "- Roots: \{self.roots().length()}\n"
builder <+ "- Maximum depth: \{self.max_depth()}\n"
builder <+ "- Unit scale: \{self.unit_scale}\n"
builder <+ "- Map conversion: \{self.has_map_conversion}\n\n"
for frame in self.frames {
builder <+ "- \{frame.headline()}\n"
}
builder.to_string()
}
///|
pub fn coordinate_frames(model : Model) -> Array[CoordinateFrame] {
coordinate_frames_from_index(model.index())
}
///|
pub fn coordinate_transform_for(
model : Model,
placement_id : Int,
) -> Transform3? {
placement_transform(model.index(), placement_id)
}
///|
pub fn model_world_bounds(model : Model) -> BoundingBox raise ParseError {
SpatialIndex::new(model).bounds()
}