///|
/// The kind of payload described by a WebBIM delivery manifest.
pub(all) enum AssetKind {
Manifest
Geometry
Properties
Spatial
Product
} derive(Debug, Eq)
///|
/// Options for producing a deterministic, cache-friendly asset manifest.
pub(all) struct AssetManifestOptions {
base_uri : String
chunk_size : Int
include_properties : Bool
include_spatial : Bool
include_product_rows : Bool
include_empty_chunks : Bool
} derive(Debug)
///|
/// One downloadable payload and the products it covers.
pub(all) struct AssetManifestEntry {
path : String
kind : AssetKind
product_ids : Array[Int]
byte_length : Int
point_count : Int
triangle_count : Int
bounds : BoundingBox
checksum : Int
compressed : Bool
} derive(Debug)
///|
/// A complete delivery map for a parsed IFC model.
pub(all) struct AssetManifest {
schema : String
model_hash : Int
entries : Array[AssetManifestEntry]
product_count : Int
chunk_count : Int
point_count : Int
triangle_count : Int
bounds : BoundingBox
} derive(Debug)
///|
pub fn default_asset_manifest_options() -> AssetManifestOptions {
{
base_uri: "assets",
chunk_size: 128,
include_properties: true,
include_spatial: true,
include_product_rows: true,
include_empty_chunks: false,
}
}
///|
pub fn AssetManifestOptions::with_base_uri(
self : AssetManifestOptions,
base_uri : String,
) -> AssetManifestOptions {
let trimmed = base_uri.trim().to_owned()
{ ..self, base_uri: if trimmed.length() == 0 { "assets" } else { trimmed } }
}
///|
pub fn AssetManifestOptions::with_chunk_size(
self : AssetManifestOptions,
chunk_size : Int,
) -> AssetManifestOptions {
{ ..self, chunk_size: if chunk_size <= 0 { 128 } else { chunk_size } }
}
///|
pub fn AssetManifestOptions::including_properties(
self : AssetManifestOptions,
value : Bool,
) -> AssetManifestOptions {
{ ..self, include_properties: value }
}
///|
pub fn AssetManifestOptions::including_spatial(
self : AssetManifestOptions,
value : Bool,
) -> AssetManifestOptions {
{ ..self, include_spatial: value }
}
///|
pub fn AssetManifestOptions::including_product_rows(
self : AssetManifestOptions,
value : Bool,
) -> AssetManifestOptions {
{ ..self, include_product_rows: value }
}
///|
pub fn AssetManifestOptions::including_empty_chunks(
self : AssetManifestOptions,
value : Bool,
) -> AssetManifestOptions {
{ ..self, include_empty_chunks: value }
}
///|
fn asset_kind_name(kind : AssetKind) -> String {
match kind {
Manifest => "manifest"
Geometry => "geometry"
Properties => "properties"
Spatial => "spatial"
Product => "product"
}
}
///|
fn asset_schema_name(schema : SchemaVersion) -> String {
match schema {
IFC2X3 => "IFC2X3"
IFC4 => "IFC4"
IFC4X3 => "IFC4X3"
UnknownSchema(name) => name
}
}
///|
fn asset_path(
options : AssetManifestOptions,
kind : AssetKind,
index : Int,
suffix : String,
) -> String {
options.base_uri +
"/" +
asset_kind_name(kind) +
"/" +
index.to_string() +
suffix
}
///|
fn asset_checksum_text(value : String) -> Int {
value.hash()
}
///|
fn asset_entry_json_text(
ids : Array[Int],
bounds : BoundingBox,
point_count : Int,
triangle_count : Int,
) -> String {
"{\"product_ids\":[" +
ids.map(id => id.to_string()).join(",") +
"],\"bounds\":" +
mesh_bounds_json({ points: [bounds.min, bounds.max], triangles: [] }) +
",\"point_count\":" +
point_count.to_string() +
",\"triangle_count\":" +
triangle_count.to_string() +
"}"
}
///|
fn asset_chunk_entry(
options : AssetManifestOptions,
chunk : SceneChunk,
) -> AssetManifestEntry {
let payload = asset_entry_json_text(
chunk.product_ids,
chunk.bounds,
chunk.point_count,
chunk.triangle_count,
)
{
path: asset_path(options, Geometry, chunk.index, ".json"),
kind: Geometry,
product_ids: chunk.product_ids,
byte_length: payload.length(),
point_count: chunk.point_count,
triangle_count: chunk.triangle_count,
bounds: chunk.bounds,
checksum: asset_checksum_text(payload),
compressed: false,
}
}
///|
fn asset_product_entry(
options : AssetManifestOptions,
product : Product,
index : Int,
) -> AssetManifestEntry {
let payload = "{\"id\":" +
product.id.to_string() +
",\"type\":\"" +
product.ifc_type +
"\",\"name\":\"" +
product.name.unwrap_or("") +
"\"}"
{
path: asset_path(options, Product, index, ".json"),
kind: Product,
product_ids: [product.id],
byte_length: payload.length(),
point_count: 0,
triangle_count: 0,
bounds: BoundingBox::empty(),
checksum: asset_checksum_text(payload),
compressed: false,
}
}
///|
fn asset_property_entry(
options : AssetManifestOptions,
table : PropertyTable,
) -> AssetManifestEntry {
let payload = table.to_json()
{
path: options.base_uri + "/properties/table.json",
kind: Properties,
product_ids: table.rows.map(row => row.product_id),
byte_length: payload.length(),
point_count: 0,
triangle_count: 0,
bounds: BoundingBox::empty(),
checksum: asset_checksum_text(payload),
compressed: false,
}
}
///|
fn asset_spatial_entry(
options : AssetManifestOptions,
spatial : SpatialIndex,
) -> AssetManifestEntry {
let payload = spatial.to_json()
{
path: options.base_uri + "/spatial/index.json",
kind: Spatial,
product_ids: spatial.product_ids(),
byte_length: payload.length(),
point_count: spatial
.entries()
.fold(init=0, (sum, entry) => sum + entry.point_count),
triangle_count: spatial.total_triangles(),
bounds: spatial.bounds(),
checksum: asset_checksum_text(payload),
compressed: false,
}
}
///|
fn asset_manifest_entry(
options : AssetManifestOptions,
model : Model,
entries : Array[AssetManifestEntry],
spatial : SpatialIndex,
) -> AssetManifestEntry {
let summary = "schema=" +
asset_schema_name(schema_version(model)) +
";products=" +
spatial.entries().length().to_string() +
";entries=" +
entries.length().to_string()
{
path: options.base_uri + "/manifest.json",
kind: Manifest,
product_ids: spatial.product_ids(),
byte_length: summary.length(),
point_count: spatial
.entries()
.fold(init=0, (sum, entry) => sum + entry.point_count),
triangle_count: spatial.total_triangles(),
bounds: spatial.bounds(),
checksum: asset_checksum_text(summary),
compressed: false,
}
}
///|
/// Build paths and resource statistics without serializing model geometry twice.
pub fn asset_manifest(
model : Model,
options? : AssetManifestOptions = default_asset_manifest_options(),
) -> AssetManifest raise ParseError {
let spatial = SpatialIndex::new(model)
let partition = scene_partition_for_products(
spatial,
spatial.product_ids(),
chunk_size=options.chunk_size,
)
let entries : Array[AssetManifestEntry] = []
for chunk in partition.chunks {
if options.include_empty_chunks || !chunk.is_empty() {
entries.push(asset_chunk_entry(options, chunk))
}
}
if options.include_spatial {
entries.push(asset_spatial_entry(options, spatial))
}
if options.include_product_rows {
let products = model.products()
for index in 0.. sum + entry.point_count),
triangle_count: spatial.total_triangles(),
bounds: spatial.bounds(),
}
}
///|
pub fn AssetManifest::entry_count(self : AssetManifest) -> Int {
self.entries.length()
}
///|
pub fn AssetManifest::entry(
self : AssetManifest,
path : String,
) -> AssetManifestEntry? {
for entry in self.entries {
if entry.path == path {
return Some(entry)
}
}
None
}
///|
pub fn AssetManifest::entries_of_kind(
self : AssetManifest,
kind : AssetKind,
) -> Array[AssetManifestEntry] {
self.entries.filter(entry => entry.kind == kind)
}
///|
pub fn AssetManifest::paths(self : AssetManifest) -> Array[String] {
self.entries.map(entry => entry.path)
}
///|
pub fn AssetManifest::total_bytes(self : AssetManifest) -> Int {
self.entries.fold(init=0, (sum, entry) => sum + entry.byte_length)
}
///|
pub fn AssetManifest::total_points(self : AssetManifest) -> Int {
self.point_count
}
///|
pub fn AssetManifest::total_triangles(self : AssetManifest) -> Int {
self.triangle_count
}
///|
pub fn AssetManifest::is_cacheable(self : AssetManifest) -> Bool {
self.entries.all(entry => entry.checksum != 0 && !entry.compressed)
}
///|
pub fn AssetManifest::geometry_entries(
self : AssetManifest,
) -> Array[AssetManifestEntry] {
self.entries_of_kind(Geometry)
}
///|
pub fn AssetManifest::product_entries(
self : AssetManifest,
) -> Array[AssetManifestEntry] {
self.entries_of_kind(Product)
}
///|
pub fn AssetManifest::spatial_entry(
self : AssetManifest,
) -> AssetManifestEntry? {
self.entries_of_kind(Spatial).get(0)
}
///|
pub fn AssetManifest::headline(self : AssetManifest) -> String {
"schema=" +
self.schema +
" products=" +
self.product_count.to_string() +
" chunks=" +
self.chunk_count.to_string() +
" assets=" +
self.entry_count().to_string() +
" bytes=" +
self.total_bytes().to_string()
}
///|
fn asset_bounds_json(bounds : BoundingBox) -> String {
if bounds.empty {
"null"
} else {
"{\"min\":[" +
bounds.min.x.to_string() +
"," +
bounds.min.y.to_string() +
"," +
bounds.min.z.to_string() +
"],\"max\":[" +
bounds.max.x.to_string() +
"," +
bounds.max.y.to_string() +
"," +
bounds.max.z.to_string() +
"]}"
}
}
///|
pub fn AssetManifest::to_json(self : AssetManifest) -> String {
let entries = self.entries
.map(entry => {
"{\"path\":\"" +
entry.path +
"\",\"kind\":\"" +
asset_kind_name(entry.kind) +
"\",\"product_count\":" +
entry.product_ids.length().to_string() +
",\"byte_length\":" +
entry.byte_length.to_string() +
",\"point_count\":" +
entry.point_count.to_string() +
",\"triangle_count\":" +
entry.triangle_count.to_string() +
",\"checksum\":" +
entry.checksum.to_string() +
",\"bounds\":" +
asset_bounds_json(entry.bounds) +
"}"
})
.join(",")
"{\"schema\":\"" +
self.schema +
"\",\"model_hash\":" +
self.model_hash.to_string() +
",\"product_count\":" +
self.product_count.to_string() +
",\"chunk_count\":" +
self.chunk_count.to_string() +
",\"point_count\":" +
self.point_count.to_string() +
",\"triangle_count\":" +
self.triangle_count.to_string() +
",\"entries\":[" +
entries +
"]}"
}
///|
pub fn AssetManifest::to_csv(self : AssetManifest) -> String {
let builder = StringBuilder()
builder <+
"path,kind,product_count,byte_length,point_count,triangle_count,checksum\n"
for entry in self.entries {
builder <+
"\{entry.path},\{asset_kind_name(entry.kind)},\{entry.product_ids.length()},\{entry.byte_length},\{entry.point_count},\{entry.triangle_count},\{entry.checksum}\n"
}
builder.to_string()
}
///|
pub fn AssetManifest::to_markdown(self : AssetManifest) -> String {
let builder = StringBuilder()
builder <+ "# WebBIM asset manifest\n\n"
builder <+ "- \{self.headline()}\n"
builder <+ "- Model hash: \{self.model_hash}\n"
builder <+ "- Points: \{self.point_count}\n"
builder <+ "- Triangles: \{self.triangle_count}\n"
builder <+ "- Cacheable: \{self.is_cacheable()}\n\n"
builder <+ "| Path | Kind | Products | Bytes | Points | Triangles |\n"
builder <+ "| --- | --- | ---: | ---: | ---: | ---: |\n"
for entry in self.entries {
builder <+
"| \{entry.path} | \{asset_kind_name(entry.kind)} | \{entry.product_ids.length()} | \{entry.byte_length} | \{entry.point_count} | \{entry.triangle_count} |\n"
}
builder.to_string()
}
///|
pub fn asset_manifest_paths(
manifest : AssetManifest,
kind : AssetKind,
) -> Array[String] {
manifest.entries_of_kind(kind).map(entry => entry.path)
}
///|
pub fn asset_manifest_product_paths(
manifest : AssetManifest,
product_id : Int,
) -> Array[String] {
manifest.entries
.filter(entry => entry.product_ids.contains(product_id))
.map(entry => entry.path)
}
///|
pub fn asset_manifest_checksums(manifest : AssetManifest) -> Map[String, Int] {
let result : Map[String, Int] = Map([])
for entry in manifest.entries {
result[entry.path] = entry.checksum
}
result
}
///|
pub fn asset_manifest_kind_histogram(
manifest : AssetManifest,
) -> Map[String, Int] {
let result : Map[String, Int] = Map([])
for entry in manifest.entries {
result.update_or_default(asset_kind_name(entry.kind), 1, count => count + 1)
}
result
}
///|
pub fn asset_manifest_is_complete(manifest : AssetManifest) -> Bool {
manifest.entry_count() > 0 &&
manifest.entries_of_kind(Manifest).length() == 1 &&
manifest.entries_of_kind(Geometry).length() > 0
}
///|
pub fn asset_manifest_summary(manifest : AssetManifest) -> Array[String] {
[
manifest.headline(),
"cacheable=" + manifest.is_cacheable().to_string(),
"complete=" + asset_manifest_is_complete(manifest).to_string(),
"geometry_assets=" + manifest.geometry_entries().length().to_string(),
"product_assets=" + manifest.product_entries().length().to_string(),
]
}
///|
pub fn asset_manifest_entry_json(
manifest : AssetManifest,
path : String,
) -> String? {
match manifest.entry(path) {
Some(entry) =>
Some(
"{\"path\":\"" +
entry.path +
"\",\"kind\":\"" +
asset_kind_name(entry.kind) +
"\",\"bytes\":" +
entry.byte_length.to_string() +
",\"checksum\":" +
entry.checksum.to_string() +
"}",
)
None => None
}
}