///|
pub fn default_takeoff_options() -> TakeoffOptions {
{
grouping: ProductType,
include_empty: false,
include_unknown: true,
use_mesh_measurements: true,
include_surface_area: true,
minimum_value: 0.0,
}
}
///|
pub fn TakeoffOptions::group_by(
self : TakeoffOptions,
grouping : QuantityGrouping,
) -> TakeoffOptions {
{ ..self, grouping, }
}
///|
pub fn TakeoffOptions::including_empty(
self : TakeoffOptions,
value : Bool,
) -> TakeoffOptions {
{ ..self, include_empty: value }
}
///|
pub fn TakeoffOptions::including_unknown(
self : TakeoffOptions,
value : Bool,
) -> TakeoffOptions {
{ ..self, include_unknown: value }
}
///|
pub fn TakeoffOptions::using_mesh_measurements(
self : TakeoffOptions,
value : Bool,
) -> TakeoffOptions {
{ ..self, use_mesh_measurements: value }
}
///|
pub fn TakeoffOptions::including_surface_area(
self : TakeoffOptions,
value : Bool,
) -> TakeoffOptions {
{ ..self, include_surface_area: value }
}
///|
pub fn TakeoffOptions::with_minimum_value(
self : TakeoffOptions,
value : Double,
) -> TakeoffOptions {
{ ..self, minimum_value: if value < 0.0 { 0.0 } else { value } }
}
///|
pub fn QuantityFormula::new(
kind : QuantityKind,
unit? : String = "",
) -> QuantityFormula {
{
kind,
property_names: [],
fallback_to_geometry: true,
multiplier: 1.0,
unit,
}
}
///|
pub fn QuantityFormula::from_properties(
self : QuantityFormula,
names : Array[String],
) -> QuantityFormula {
{ ..self, property_names: names, fallback_to_geometry: false }
}
///|
pub fn QuantityFormula::with_fallback(
self : QuantityFormula,
value : Bool,
) -> QuantityFormula {
{ ..self, fallback_to_geometry: value }
}
///|
pub fn QuantityFormula::scaled(
self : QuantityFormula,
multiplier : Double,
) -> QuantityFormula {
{ ..self, multiplier, }
}
///|
fn quantity_unit(kind : QuantityKind) -> String {
match kind {
Count => "count"
Length => "model-length"
Area => "model-area"
Volume => "model-volume"
SurfaceArea => "model-area"
Height => "model-length"
Width => "model-length"
Depth => "model-length"
}
}
///|
fn quantity_kind_text(kind : QuantityKind) -> String {
match kind {
Count => "count"
Length => "length"
Area => "area"
Volume => "volume"
SurfaceArea => "surface_area"
Height => "height"
Width => "width"
Depth => "depth"
}
}
///|
pub fn quantity_kind_name(kind : QuantityKind) -> String {
quantity_kind_text(kind)
}
///|
fn quantity_value(
kind : QuantityKind,
value : Double,
source : String,
estimated : Bool,
unit : String,
) -> QuantityValue {
{
kind,
value,
unit: if unit.length() == 0 {
quantity_unit(kind)
} else {
unit
},
source,
estimated,
}
}
///|
fn quantity_mesh_for_product(
index : ModelIndex,
product : Product,
) -> Mesh raise ParseError {
mesh_for_product_advanced(index, product)
}
///|
fn quantity_curve_length(index : ModelIndex, product : Product) -> Double {
geometries_for_product(index, product).fold(init=0.0, (result, geometry) => {
match geometry.kind {
Polyline => result + polyline_length(geometry.points, closed=false)
_ => result
}
})
}
///|
fn quantity_product_bounds(
index : ModelIndex,
product : Product,
mesh : Mesh,
) -> BoundingBox {
if mesh.points.length() > 0 {
bounds_of_mesh(mesh)
} else {
geometries_for_product(index, product).fold(init=BoundingBox::empty(), (
result,
geometry,
) => {
merge_bounds(
result,
BoundingBox::from_points(
transform_points(index, product, geometry.points),
),
)
})
}
}
///|
fn quantity_geometry_volume(
index : ModelIndex,
product : Product,
mesh : Mesh,
) -> (Double, Bool) {
if mesh.triangles.length() > 0 {
let stats = mesh_stats(mesh)
(stats.volume().abs(), false)
} else {
let estimates = geometries_for_product(index, product).fold(init=0.0, (
result,
geometry,
) => result + estimated_volume(geometry).unwrap_or(0.0))
(estimates, true)
}
}
///|
fn quantity_geometry_area(mesh : Mesh) -> (Double, Bool) {
if mesh.triangles.length() > 0 {
(mesh_stats(mesh).surface_area, false)
} else {
(0.0, true)
}
}
///|
fn quantity_for_product(
index : ModelIndex,
product : Product,
options : TakeoffOptions,
) -> ProductTakeoff raise ParseError {
let geometries = geometries_for_product(index, product)
let mesh = if options.use_mesh_measurements {
quantity_mesh_for_product(index, product)
} else {
{ points: [], triangles: [] }
}
let bounds = quantity_product_bounds(index, product, mesh)
let (volume, volume_estimated) = quantity_geometry_volume(
index, product, mesh,
)
let (surface_area, surface_estimated) = quantity_geometry_area(mesh)
let length = quantity_curve_length(index, product)
let quantities : Array[QuantityValue] = [
quantity_value(Count, 1.0, "product", false, "count"),
quantity_value(
Width,
bounds.width(),
"bounds",
bounds.empty,
"model-length",
),
quantity_value(
Height,
bounds.height(),
"bounds",
bounds.empty,
"model-length",
),
quantity_value(
Depth,
bounds.depth(),
"bounds",
bounds.empty,
"model-length",
),
quantity_value(
Volume,
volume,
if volume_estimated {
"extrusion-estimate"
} else {
"mesh"
},
volume_estimated,
"model-volume",
),
quantity_value(Length, length, "polyline", false, "model-length"),
]
let with_area = if options.include_surface_area {
quantities.push(
quantity_value(
SurfaceArea,
surface_area,
"mesh",
surface_estimated,
"model-area",
),
)
quantities
} else {
quantities
}
{
product,
quantities: with_area,
geometry_count: geometries.length(),
mesh_triangle_count: mesh.triangles.length(),
bounds,
}
}
///|
fn quantity_find(
product : ProductTakeoff,
kind : QuantityKind,
) -> QuantityValue? {
for value in product.quantities {
if value.kind == kind {
return Some(value)
}
}
None
}
///|
pub fn ProductTakeoff::quantity(
self : ProductTakeoff,
kind : QuantityKind,
) -> QuantityValue? {
quantity_find(self, kind)
}
///|
pub fn ProductTakeoff::value(
self : ProductTakeoff,
kind : QuantityKind,
) -> Double {
self.quantity(kind).map(value => value.value).unwrap_or(0.0)
}
///|
pub fn ProductTakeoff::has_geometry(self : ProductTakeoff) -> Bool {
self.geometry_count > 0 && !self.bounds.empty
}
///|
pub fn ProductTakeoff::is_estimated(
self : ProductTakeoff,
kind : QuantityKind,
) -> Bool {
self.quantity(kind).map(value => value.estimated).unwrap_or(false)
}
///|
pub fn ProductTakeoff::headline(self : ProductTakeoff) -> String {
let label = self.product.name.unwrap_or(self.product.ifc_type)
label +
" #" +
self.product.id.to_string() +
" volume=" +
self.value(Volume).to_string() +
" area=" +
self.value(SurfaceArea).to_string()
}
///|
fn quantity_group_key(
index : ModelIndex,
product : Product,
grouping : QuantityGrouping,
) -> String raise ParseError {
match grouping {
ProductType => product.ifc_type.to_upper()
ProductKind => product_kind_name(product.kind)
Container =>
product.container.map(id => "#" + id.to_string()).unwrap_or("uncontained")
Property(name) => {
let sets = property_sets_for_product(index, product)
property_string(sets, name).unwrap_or("(missing)")
}
}
}
///|
fn quantity_accumulate(
values : Array[QuantityValue],
incoming : QuantityValue,
) -> Array[QuantityValue] {
let result = values.copy()
for index in 0.. Unit {
groups.update_or_default(
key,
{ key, product_count: 1, quantities: takeoff.quantities },
group => {
let mut quantities = group.quantities
for value in takeoff.quantities {
quantities = quantity_accumulate(quantities, value)
}
{ ..group, product_count: group.product_count + 1, quantities }
},
)
}
///|
fn quantity_group_array(
groups : Map[String, TakeoffGroup],
) -> Array[TakeoffGroup] {
let result : Array[TakeoffGroup] = []
for _, group in groups {
result.push(group)
}
let sorted = result.copy()
for i in 0.. TakeoffReport raise ParseError {
let index = model.index()
let products = model.products()
let rows : Array[ProductTakeoff] = []
let groups : Map[String, TakeoffGroup] = Map([])
let mut skipped = 0
for product in products {
let takeoff = quantity_for_product(index, product, options)
if (!options.include_empty && takeoff.bounds.empty) ||
(!options.include_unknown && product.kind == Generic) {
skipped += 1
continue
}
if takeoff.quantities.any(value => value.value >= options.minimum_value) {
rows.push(takeoff)
let key = quantity_group_key(index, product, options.grouping)
quantity_group_add(groups, key, takeoff)
} else {
skipped += 1
}
}
{
products: rows,
groups: quantity_group_array(groups),
unit_system: unit_system(index),
source_schema: model.schema(),
skipped_products: skipped,
}
}
///|
pub fn TakeoffReport::product_count(self : TakeoffReport) -> Int {
self.products.length()
}
///|
pub fn TakeoffReport::group_count(self : TakeoffReport) -> Int {
self.groups.length()
}
///|
pub fn TakeoffReport::find_product(
self : TakeoffReport,
id : Int,
) -> ProductTakeoff? {
for row in self.products {
if row.product.id == id {
return Some(row)
}
}
None
}
///|
pub fn TakeoffReport::find_group(
self : TakeoffReport,
key : String,
) -> TakeoffGroup? {
for group in self.groups {
if group.key == key {
return Some(group)
}
}
None
}
///|
pub fn TakeoffReport::total(
self : TakeoffReport,
kind : QuantityKind,
) -> Double {
self.products.fold(init=0.0, (result, row) => result + row.value(kind))
}
///|
pub fn TakeoffReport::estimated_count(
self : TakeoffReport,
kind : QuantityKind,
) -> Int {
self.products.fold(init=0, (result, row) => {
if row.is_estimated(kind) {
result + 1
} else {
result
}
})
}
///|
pub fn TakeoffReport::headline(self : TakeoffReport) -> String {
"products=" +
self.products.length().to_string() +
" groups=" +
self.groups.length().to_string() +
" volume=" +
self.total(Volume).to_string() +
" area=" +
self.total(SurfaceArea).to_string()
}
///|
fn quantity_text(value : QuantityValue) -> String {
value.value.to_string() + " " + value.unit
}
///|
pub fn QuantityValue::headline(self : QuantityValue) -> String {
let suffix = if self.estimated { " (estimated)" } else { "" }
quantity_kind_text(self.kind) + "=" + quantity_text(self) + suffix
}
///|
pub fn TakeoffGroup::value(self : TakeoffGroup, kind : QuantityKind) -> Double {
for value in self.quantities {
if value.kind == kind {
return value.value
}
}
0.0
}
///|
pub fn TakeoffGroup::quantity(
self : TakeoffGroup,
kind : QuantityKind,
) -> QuantityValue? {
for value in self.quantities {
if value.kind == kind {
return Some(value)
}
}
None
}
///|
pub fn TakeoffGroup::headline(self : TakeoffGroup) -> String {
self.key +
" count=" +
self.product_count.to_string() +
" volume=" +
self.value(Volume).to_string()
}
///|
fn quantity_csv_escape(text : String) -> String {
if text.contains(",") || text.contains("\"") || text.contains("\n") {
"\"" + text.replace(old="\"", new="\"\"") + "\""
} else {
text
}
}
///|
pub fn TakeoffReport::to_csv(self : TakeoffReport) -> String {
let builder = StringBuilder()
builder <+
"product_id,type,name,count,width,height,depth,length,area,surface_area,volume\n"
for row in self.products {
let product_name = row.product.name.unwrap_or("")
builder <+
"\{row.product.id},\{quantity_csv_escape(row.product.ifc_type)},\{quantity_csv_escape(product_name)},\{row.value(Count)},\{row.value(Width)},\{row.value(Height)},\{row.value(Depth)},\{row.value(Length)},\{row.value(Area)},\{row.value(SurfaceArea)},\{row.value(Volume)}\n"
}
builder.to_string()
}
///|
fn quantity_json_value(value : QuantityValue) -> String {
"{\"kind\":\"" +
quantity_kind_text(value.kind) +
"\",\"value\":" +
value.value.to_string() +
",\"unit\":\"" +
value.unit +
"\",\"source\":\"" +
value.source +
"\",\"estimated\":" +
value.estimated.to_string() +
"}"
}
///|
pub fn ProductTakeoff::to_json(self : ProductTakeoff) -> String {
"{\"product_id\":" +
self.product.id.to_string() +
",\"type\":\"" +
self.product.ifc_type +
"\",\"quantities\":[" +
self.quantities.map(value => quantity_json_value(value)).join(",") +
"]}"
}
///|
pub fn TakeoffReport::to_json(self : TakeoffReport) -> String {
"{\"schema\":\"" +
self.source_schema +
"\",\"skipped_products\":" +
self.skipped_products.to_string() +
",\"products\":[" +
self.products.map(row => row.to_json()).join(",") +
"],\"groups\":[" +
self.groups
.map(group => {
"{\"key\":\"" +
group.key +
"\",\"product_count\":" +
group.product_count.to_string() +
",\"quantities\":[" +
group.quantities.map(value => quantity_json_value(value)).join(",") +
"]}"
})
.join(",") +
"]}"
}
///|
pub fn TakeoffReport::to_markdown(self : TakeoffReport) -> String {
let builder = StringBuilder()
builder <+ "# Quantity takeoff\n\n"
builder <+ "- Schema: \{self.source_schema}\n"
builder <+ "- Products: \{self.products.length()}\n"
builder <+ "- Groups: \{self.groups.length()}\n"
builder <+ "- Skipped: \{self.skipped_products}\n\n"
builder <+ "| Group | Products | Volume | Surface area |\n"
builder <+ "| --- | ---: | ---: | ---: |\n"
for group in self.groups {
builder <+
"| \{group.key} | \{group.product_count} | \{group.value(Volume)} | \{group.value(SurfaceArea)} |\n"
}
builder.to_string()
}
///|
pub fn TakeoffReport::quantity_kinds(self : TakeoffReport) -> Array[String] {
let result : Array[String] = []
for row in self.products {
for value in row.quantities {
let name = quantity_kind_text(value.kind)
if !result.contains(name) {
result.push(name)
}
}
}
result
}
///|
pub fn TakeoffReport::group_keys(self : TakeoffReport) -> Array[String] {
self.groups.map(group => group.key)
}
///|
pub fn quantity_value_in_si(
units : UnitSystem,
value : QuantityValue,
) -> Double {
match value.kind {
Length | Width | Height | Depth => units.length_to_meters(value.value)
Area | SurfaceArea => units.area_to_square_meters(value.value)
Volume => units.volume_to_cubic_meters(value.value)
Count => value.value
}
}
///|
pub fn TakeoffReport::total_in_si(
self : TakeoffReport,
kind : QuantityKind,
) -> Double {
self.products.fold(init=0.0, (result, row) => {
match row.quantity(kind) {
Some(value) => result + quantity_value_in_si(self.unit_system, value)
None => result
}
})
}
///|
pub fn QuantityNote::headline(self : QuantityNote) -> String {
"#" +
self.product_id.to_string() +
" " +
quantity_kind_text(self.kind) +
": " +
self.message
}
///|
pub fn quantity_note(
product_id : Int,
kind : QuantityKind,
message : String,
severity? : DiagnosticSeverity = Note,
) -> QuantityNote {
{ product_id, kind, message, severity }
}
///|
pub fn takeoff_for_product(
model : Model,
product_id : Int,
options? : TakeoffOptions = default_takeoff_options(),
) -> ProductTakeoff? raise ParseError {
let index = model.index()
match product_by_id(model.products(), product_id) {
Some(product) => Some(quantity_for_product(index, product, options))
None => None
}
}
///|
pub fn takeoff_by_kind(
report : TakeoffReport,
kind : ProductKind,
) -> Array[ProductTakeoff] {
report.products.filter(row => row.product.kind == kind)
}
///|
pub fn takeoff_by_type(
report : TakeoffReport,
ifc_type : String,
) -> Array[ProductTakeoff] {
let wanted = ifc_type.to_upper()
report.products.filter(row => row.product.ifc_type.to_upper() == wanted)
}
///|
pub fn takeoff_area_per_volume(report : TakeoffReport) -> Double {
let volume = report.total(Volume)
if volume == 0.0 {
0.0
} else {
report.total(SurfaceArea) / volume
}
}