///|
pub fn default_export_options() -> ExportOptions {
{
format: Obj,
include_empty_products: false,
include_normals: true,
include_properties: true,
precision: 6,
}
}
///|
pub fn ExportOptions::with_format(
self : ExportOptions,
format : ExportFormat,
) -> ExportOptions {
{ ..self, format, }
}
///|
pub fn ExportOptions::including_empty(
self : ExportOptions,
enabled : Bool,
) -> ExportOptions {
{ ..self, include_empty_products: enabled }
}
///|
pub fn ExportOptions::including_normals(
self : ExportOptions,
enabled : Bool,
) -> ExportOptions {
{ ..self, include_normals: enabled }
}
///|
pub fn ExportOptions::including_properties(
self : ExportOptions,
enabled : Bool,
) -> ExportOptions {
{ ..self, include_properties: enabled }
}
///|
pub fn ExportOptions::with_precision(
self : ExportOptions,
precision : Int,
) -> ExportOptions {
{ ..self, precision: if precision < 0 { 0 } else { precision } }
}
///|
fn export_format_name(format : ExportFormat) -> String {
match format {
Obj => "obj"
Ply => "ply"
JsonLines => "jsonl"
WebBim => "json"
}
}
///|
fn export_file_name(format : ExportFormat) -> String {
"model." + export_format_name(format)
}
///|
fn point_line(prefix : String, point : Point3) -> String {
prefix +
" " +
point.x.to_string() +
" " +
point.y.to_string() +
" " +
point.z.to_string() +
"\n"
}
///|
fn vector_line(prefix : String, vector : Vector3) -> String {
prefix +
" " +
vector.x.to_string() +
" " +
vector.y.to_string() +
" " +
vector.z.to_string() +
"\n"
}
///|
fn obj_face_line(triangle : (Int, Int, Int), offset : Int) -> String {
let (a, b, c) = triangle
"f " +
(a + offset).to_string() +
" " +
(b + offset).to_string() +
" " +
(c + offset).to_string() +
"\n"
}
///|
fn obj_product(
index : ModelIndex,
product : Product,
options : ExportOptions,
vertex_offset : Int,
output : StringBuilder,
) -> (Int, Int, Int) raise ParseError {
let mesh = mesh_for_product_advanced(index, product)
if mesh.points.length() == 0 && !options.include_empty_products {
return (0, 0, 0)
}
output <+ "o \{product.ifc_type}_\{product.id}\n"
for point in mesh.points {
let line = point_line("v", point)
output <+ "\{line}"
}
if options.include_normals {
let analysis = MeshAnalysis::from_mesh(mesh)
for normal in analysis.normals {
let line = vector_line("vn", normal)
output <+ "\{line}"
}
}
for triangle in mesh.triangles {
output <+ "\{obj_face_line(triangle, vertex_offset + 1)}"
}
(mesh.points.length(), mesh.triangles.length(), 1)
}
///|
fn export_obj(
index : ModelIndex,
products : Array[Product],
options : ExportOptions,
) -> (String, Int, Int, Int) raise ParseError {
let output = StringBuilder()
output <+ "# moonbit-ifc OBJ export\n"
output <+ "# products=\{products.length()}\n"
let mut vertex_offset = 0
let mut product_count = 0
let mut point_count = 0
let mut triangle_count = 0
for product in products {
let (points, triangles, exported) = obj_product(
index, product, options, vertex_offset, output,
)
vertex_offset += points
point_count += points
triangle_count += triangles
product_count += exported
}
(output.to_string(), product_count, point_count, triangle_count)
}
///|
fn ply_header(
point_count : Int,
triangle_count : Int,
include_normals : Bool,
) -> String {
let normal_fields = if include_normals {
"property float nx\nproperty float ny\nproperty float nz\n"
} else {
""
}
"ply\nformat ascii 1.0\ncomment generated by moonbit-ifc\n" +
"element vertex " +
point_count.to_string() +
"\n" +
"property float x\nproperty float y\nproperty float z\n" +
normal_fields +
"element face " +
triangle_count.to_string() +
"\n" +
"property list uchar int vertex_indices\nend_header\n"
}
///|
fn ply_product_meshes(
index : ModelIndex,
products : Array[Product],
options : ExportOptions,
) -> (Array[Mesh], Int) raise ParseError {
let meshes : Array[Mesh] = []
let mut product_count = 0
for product in products {
let mesh = mesh_for_product_advanced(index, product)
if mesh.points.length() > 0 || options.include_empty_products {
meshes.push(mesh)
product_count += 1
}
}
(meshes, product_count)
}
///|
fn export_ply(
index : ModelIndex,
products : Array[Product],
options : ExportOptions,
) -> (String, Int, Int, Int) raise ParseError {
let (meshes, product_count) = ply_product_meshes(index, products, options)
let point_count = meshes.fold(init=0, (count, mesh) => {
count + mesh.points.length()
})
let triangle_count = meshes.fold(init=0, (count, mesh) => {
count + mesh.triangles.length()
})
let output = StringBuilder()
output <+
"\{ply_header(point_count, triangle_count, options.include_normals)}"
let all_points : Array[Point3] = []
let all_normals : Array[Vector3] = []
for mesh in meshes {
all_points.append(mesh.points)
if options.include_normals {
all_normals.append(MeshAnalysis::from_mesh(mesh).normals)
}
}
for index in 0.. String raise ParseError {
let sets = property_sets_for_product(index, product)
"\"properties\":[" + sets.map(property_set_to_json).join(",") + "]"
}
///|
fn export_json_lines(
index : ModelIndex,
products : Array[Product],
options : ExportOptions,
) -> (String, Int, Int, Int) raise ParseError {
let output = StringBuilder()
let mut product_count = 0
let mut point_count = 0
let mut triangle_count = 0
for product in products {
let mesh = mesh_for_product_advanced(index, product)
if mesh.points.length() == 0 && !options.include_empty_products {
continue
}
let mesh_json = mesh_to_json_stats(MeshAnalysis::from_mesh(mesh))
let properties = if options.include_properties {
properties_for_product_json(index, product)
} else {
"\"properties\":[]"
}
let row = "{\"product\":" +
product_to_json(product) +
",\"mesh\":" +
mesh_json +
"," +
properties +
"}\n"
output <+ "\{row}"
product_count += 1
point_count += mesh.points.length()
triangle_count += mesh.triangles.length()
}
(output.to_string(), product_count, point_count, triangle_count)
}
///|
fn export_webbim(
model : Model,
options : ExportOptions,
) -> (String, Int, Int, Int) {
let scene = build_scene(model)
let json = scene.to_json()
let content = "{\"format\":\"webbim\",\"precision\":" +
options.precision.to_string() +
",\"scene\":" +
json +
"}"
(
content,
scene.items.length(),
scene.items.fold(init=0, (count, item) => {
count +
item.geometries.fold(init=0, (inner, geometry) => {
inner + geometry.points.length()
})
}),
0,
)
}
///|
pub fn export_model_asset(
model : Model,
options : ExportOptions,
) -> ExportedAsset raise ParseError {
let index = model.index()
let products = extract_products(index)
let (content, product_count, point_count, triangle_count) = match
options.format {
Obj => export_obj(index, products, options)
Ply => export_ply(index, products, options)
JsonLines => export_json_lines(index, products, options)
WebBim => export_webbim(model, options)
}
{
format: options.format,
file_name: export_file_name(options.format),
content,
product_count,
point_count,
triangle_count,
}
}
///|
pub fn export_step_asset(
source : String,
options : ExportOptions,
) -> ExportedAsset raise ParseError {
export_model_asset(parse(source), options)
}
///|
pub fn ExportedAsset::headline(self : ExportedAsset) -> String {
self.file_name +
": products=" +
self.product_count.to_string() +
", points=" +
self.point_count.to_string() +
", triangles=" +
self.triangle_count.to_string() +
", bytes=" +
self.content.length().to_string()
}
///|
pub fn ExportedAsset::is_non_empty(self : ExportedAsset) -> Bool {
self.content.length() > 0 && self.product_count >= 0
}
///|
pub fn ExportedAsset::line_count(self : ExportedAsset) -> Int {
self.content.split("\n").length()
}
///|
pub fn export_supported_formats() -> Array[String] {
["obj", "ply", "jsonl", "webbim"]
}
///|
pub fn export_format_from_name(name : String) -> ExportFormat? {
match name.to_lower() {
"obj" => Some(Obj)
"ply" => Some(Ply)
"jsonl" | "json-lines" => Some(JsonLines)
"json" | "webbim" => Some(WebBim)
_ => None
}
}
///|
pub fn export_preview(asset : ExportedAsset, maximum_lines : Int) -> String {
if maximum_lines <= 0 {
""
} else {
asset.content.split("\n")[:maximum_lines].join("\n")
}
}