///|
/// Projection mode used by a render-oriented visibility query.
pub(all) enum SceneProjection {
Perspective
Orthographic
} derive(Debug, Eq)
///|
/// Camera parameters shared by a WebAssembly viewer and native tools.
pub(all) struct SceneCamera {
position : Point3
forward : Vector3
up : Vector3
near_distance : Double
far_distance : Double
vertical_fov : Double
aspect_ratio : Double
projection : SceneProjection
} derive(Debug)
///|
/// Controls culling, LOD selection and the amount of render work returned.
pub(all) struct SceneQueryOptions {
tolerance : Double
minimum_distance : Double
maximum_items : Int
full_detail_distance : Double
medium_detail_distance : Double
include_empty : Bool
include_back_facing : Bool
} derive(Debug)
///|
/// Render detail selected for one visible product.
pub(all) enum SceneLod {
Full
Medium
Coarse
BoundsOnly
} derive(Debug, Eq)
///|
/// A visible item with a conservative screen-independent LOD choice.
pub(all) struct SceneVisibleItem {
product_id : Int
ifc_type : String
distance : Double
depth : Double
projected_radius : Double
lod : SceneLod
triangle_count : Int
bounds : BoundingBox
} derive(Debug)
///|
/// Result of a visibility query over an indexed IFC model.
pub(all) struct SceneSelection {
items : Array[SceneVisibleItem]
tested_count : Int
culled_count : Int
full_count : Int
medium_count : Int
coarse_count : Int
bounds_only_count : Int
visible_bounds : BoundingBox
} derive(Debug)
///|
/// A picking ray and its closest hit.
pub(all) struct ScenePick {
origin : Point3
direction : Vector3
product_id : Int?
distance : Double?
point : Point3?
} derive(Debug)
///|
/// Camera defaults are intentionally conservative for building-scale models.
pub fn default_scene_camera() -> SceneCamera {
{
position: { x: 0.0, y: -10.0, z: 5.0 },
forward: Vector3::{ x: 0.0, y: 1.0, z: -0.15 }.normalized(),
up: Vector3::{ x: 0.0, y: 0.0, z: 1.0 },
near_distance: 0.01,
far_distance: 10000.0,
vertical_fov: 1.0471975511965976,
aspect_ratio: 1.7777777777777777,
projection: Perspective,
}
}
///|
pub fn SceneCamera::looking_at(
self : SceneCamera,
target : Point3,
) -> SceneCamera {
{ ..self, forward: Vector3::from_points(self.position, target).normalized() }
}
///|
pub fn SceneCamera::with_position(
self : SceneCamera,
position : Point3,
) -> SceneCamera {
{ ..self, position, }
}
///|
pub fn SceneCamera::with_forward(
self : SceneCamera,
forward : Vector3,
) -> SceneCamera {
{ ..self, forward: forward.normalized() }
}
///|
pub fn SceneCamera::with_clip(
self : SceneCamera,
near_distance : Double,
far_distance : Double,
) -> SceneCamera {
{
..self,
near_distance: if near_distance < 0.0 {
0.0
} else {
near_distance
},
far_distance: if far_distance < near_distance {
near_distance
} else {
far_distance
},
}
}
///|
pub fn SceneCamera::with_lens(
self : SceneCamera,
vertical_fov : Double,
aspect_ratio : Double,
) -> SceneCamera {
{
..self,
vertical_fov: if vertical_fov <= 0.0 {
1.0471975511965976
} else {
vertical_fov
},
aspect_ratio: if aspect_ratio <= 0.0 {
1.0
} else {
aspect_ratio
},
}
}
///|
pub fn SceneCamera::with_projection(
self : SceneCamera,
projection : SceneProjection,
) -> SceneCamera {
{ ..self, projection, }
}
///|
pub fn SceneCamera::right(self : SceneCamera) -> Vector3 {
self.forward.cross(self.up).normalized()
}
///|
pub fn SceneCamera::real_up(self : SceneCamera) -> Vector3 {
self.right().cross(self.forward).normalized()
}
///|
pub fn SceneCamera::is_valid(self : SceneCamera) -> Bool {
self.forward.length() > 0.0 &&
self.up.length() > 0.0 &&
self.near_distance >= 0.0 &&
self.far_distance >= self.near_distance &&
self.vertical_fov > 0.0 &&
self.aspect_ratio > 0.0
}
///|
pub fn SceneCamera::headline(self : SceneCamera) -> String {
"camera=(" +
self.position.x.to_string() +
"," +
self.position.y.to_string() +
"," +
self.position.z.to_string() +
") near=" +
self.near_distance.to_string() +
" far=" +
self.far_distance.to_string() +
" fov=" +
self.vertical_fov.to_string()
}
///|
pub fn default_scene_query_options() -> SceneQueryOptions {
{
tolerance: 0.001,
minimum_distance: 0.0,
maximum_items: 0,
full_detail_distance: 30.0,
medium_detail_distance: 120.0,
include_empty: false,
include_back_facing: false,
}
}
///|
pub fn SceneQueryOptions::with_tolerance(
self : SceneQueryOptions,
tolerance : Double,
) -> SceneQueryOptions {
{ ..self, tolerance: if tolerance < 0.0 { 0.0 } else { tolerance } }
}
///|
pub fn SceneQueryOptions::with_maximum_items(
self : SceneQueryOptions,
maximum_items : Int,
) -> SceneQueryOptions {
{ ..self, maximum_items: if maximum_items < 0 { 0 } else { maximum_items } }
}
///|
pub fn SceneQueryOptions::with_lod_distances(
self : SceneQueryOptions,
full_detail_distance : Double,
medium_detail_distance : Double,
) -> SceneQueryOptions {
let full = if full_detail_distance <= 0.0 {
1.0
} else {
full_detail_distance
}
let medium = if medium_detail_distance < full {
full
} else {
medium_detail_distance
}
{ ..self, full_detail_distance: full, medium_detail_distance: medium }
}
///|
pub fn SceneQueryOptions::including_empty(
self : SceneQueryOptions,
include_empty : Bool,
) -> SceneQueryOptions {
{ ..self, include_empty, }
}
///|
pub fn SceneQueryOptions::including_back_facing(
self : SceneQueryOptions,
include_back_facing : Bool,
) -> SceneQueryOptions {
{ ..self, include_back_facing, }
}
///|
fn scene_lod_name(lod : SceneLod) -> String {
match lod {
Full => "full"
Medium => "medium"
Coarse => "coarse"
BoundsOnly => "bounds-only"
}
}
///|
fn scene_item_radius(bounds : BoundingBox) -> Double {
Vector3::from_points(bounds.min, bounds.max).length() / 2.0
}
///|
fn scene_item_depth(camera : SceneCamera, center : Point3) -> (Double, Double) {
let delta = Vector3::from_points(camera.position, center)
let distance = delta.length()
let depth = delta.dot(camera.forward)
(distance, depth)
}
///|
fn scene_item_is_visible(
camera : SceneCamera,
bounds : BoundingBox,
options : SceneQueryOptions,
) -> (Bool, Double, Double, Double) {
if bounds.empty {
return (options.include_empty, 0.0, 0.0, 0.0)
}
let center = bounds.center()
let (distance, depth) = scene_item_depth(camera, center)
let radius = scene_item_radius(bounds)
if distance < options.minimum_distance ||
distance - radius > camera.far_distance ||
depth + radius < camera.near_distance {
return (false, distance, depth, radius)
}
if options.include_back_facing || depth >= 0.0 {
let direction = Vector3::from_points(camera.position, center).normalized()
let alignment = direction.dot(camera.forward)
let horizontal = alignment >= @math.cos(camera.vertical_fov / 2.0)
let side_limit = @math.cos(camera.vertical_fov / 2.0)
let side = direction.dot(camera.right()).abs()
let vertical = direction.dot(camera.real_up()).abs()
let angular_radius = if distance == 0.0 { 1.0 } else { radius / distance }
let visible = horizontal ||
side <= side_limit + angular_radius ||
vertical <= side_limit + angular_radius
(visible, distance, depth, radius)
} else {
(false, distance, depth, radius)
}
}
///|
fn scene_item_lod(
distance : Double,
radius : Double,
triangles : Int,
options : SceneQueryOptions,
) -> SceneLod {
if triangles == 0 {
BoundsOnly
} else if distance <= options.full_detail_distance + radius {
Full
} else if distance <= options.medium_detail_distance + radius {
Medium
} else if triangles > 5000 {
Coarse
} else {
Medium
}
}
///|
fn scene_sort_items(items : Array[SceneVisibleItem]) -> Array[SceneVisibleItem] {
let result = items.copy()
for i in 0.. SceneSelection {
let items : Array[SceneVisibleItem] = []
let mut tested = 0
let mut culled = 0
for entry in index.entries() {
tested += 1
let (visible, distance, depth, radius) = scene_item_is_visible(
camera,
entry.bounds,
options,
)
if !visible {
culled += 1
continue
}
let lod = scene_item_lod(distance, radius, entry.triangle_count, options)
items.push({
product_id: entry.product_id,
ifc_type: entry.ifc_type,
distance,
depth,
projected_radius: if distance == 0.0 {
radius
} else {
radius / distance
},
lod,
triangle_count: entry.triangle_count,
bounds: entry.bounds,
})
}
let sorted = scene_sort_items(items)
let limited = if options.maximum_items > 0 &&
sorted.length() > options.maximum_items {
sorted[:options.maximum_items].to_owned()
} else {
sorted
}
let full_count = limited.fold(init=0, (count, item) => {
if item.lod == Full {
count + 1
} else {
count
}
})
let medium_count = limited.fold(init=0, (count, item) => {
if item.lod == Medium {
count + 1
} else {
count
}
})
let coarse_count = limited.fold(init=0, (count, item) => {
if item.lod == Coarse {
count + 1
} else {
count
}
})
let bounds_count = limited.fold(init=0, (count, item) => {
if item.lod == BoundsOnly {
count + 1
} else {
count
}
})
{
items: limited,
tested_count: tested,
culled_count: culled,
full_count,
medium_count,
coarse_count,
bounds_only_count: bounds_count,
visible_bounds: limited.fold(init=BoundingBox::empty(), (result, item) => {
merge_bounds(result, item.bounds)
}),
}
}
///|
pub fn scene_selection(
model : Model,
camera : SceneCamera,
options? : SceneQueryOptions = default_scene_query_options(),
) -> SceneSelection raise ParseError {
SceneSelection::from_index(SpatialIndex::new(model), camera, options~)
}
///|
pub fn SceneSelection::visible_count(self : SceneSelection) -> Int {
self.items.length()
}
///|
pub fn SceneSelection::contains(
self : SceneSelection,
product_id : Int,
) -> Bool {
self.items.any(item => item.product_id == product_id)
}
///|
pub fn SceneSelection::item(
self : SceneSelection,
product_id : Int,
) -> SceneVisibleItem? {
for item in self.items {
if item.product_id == product_id {
return Some(item)
}
}
None
}
///|
pub fn SceneSelection::ids(self : SceneSelection) -> Array[Int] {
self.items.map(item => item.product_id)
}
///|
pub fn SceneSelection::by_lod(
self : SceneSelection,
lod : SceneLod,
) -> Array[SceneVisibleItem] {
self.items.filter(item => item.lod == lod)
}
///|
pub fn SceneSelection::triangle_budget(self : SceneSelection) -> Int {
self.items.fold(init=0, (sum, item) => {
let factor = match item.lod {
Full => 1
Medium => 4
Coarse => 16
BoundsOnly => 0
}
sum + item.triangle_count / factor.max(1)
})
}
///|
pub fn SceneSelection::lod_histogram(self : SceneSelection) -> Map[String, Int] {
let result : Map[String, Int] = Map([])
for item in self.items {
result.update_or_default(scene_lod_name(item.lod), 1, count => count + 1)
}
result
}
///|
pub fn SceneSelection::headline(self : SceneSelection) -> String {
"visible=" +
self.visible_count().to_string() +
" tested=" +
self.tested_count.to_string() +
" culled=" +
self.culled_count.to_string() +
" triangles=" +
self.triangle_budget().to_string()
}
///|
pub fn SceneSelection::to_json(self : SceneSelection) -> String {
let items = self.items
.map(item => {
"{\"product_id\":" +
item.product_id.to_string() +
",\"type\":\"" +
item.ifc_type +
"\",\"distance\":" +
item.distance.to_string() +
",\"depth\":" +
item.depth.to_string() +
",\"lod\":\"" +
scene_lod_name(item.lod) +
"\",\"triangles\":" +
item.triangle_count.to_string() +
"}"
})
.join(",")
"{\"tested\":" +
self.tested_count.to_string() +
",\"culled\":" +
self.culled_count.to_string() +
",\"visible\":" +
self.visible_count().to_string() +
",\"full\":" +
self.full_count.to_string() +
",\"medium\":" +
self.medium_count.to_string() +
",\"coarse\":" +
self.coarse_count.to_string() +
",\"bounds_only\":" +
self.bounds_only_count.to_string() +
",\"items\":[" +
items +
"]}"
}
///|
pub fn SceneSelection::to_markdown(self : SceneSelection) -> String {
let builder = StringBuilder()
builder <+ "# Scene selection\n\n"
builder <+ "- \{self.headline()}\n"
builder <+ "- Full: \{self.full_count}\n"
builder <+ "- Medium: \{self.medium_count}\n"
builder <+ "- Coarse: \{self.coarse_count}\n"
builder <+ "- Bounds only: \{self.bounds_only_count}\n\n"
builder <+ "| Product | IFC type | Distance | LOD | Triangles |\n"
builder <+ "| ---: | --- | ---: | --- | ---: |\n"
for item in self.items {
builder <+
"| \{item.product_id} | \{item.ifc_type} | \{item.distance} | \{scene_lod_name(item.lod)} | \{item.triangle_count} |\n"
}
builder.to_string()
}
///|
fn scene_ray_point(
origin : Point3,
direction : Vector3,
distance : Double,
) -> Point3 {
{
x: origin.x + direction.x * distance,
y: origin.y + direction.y * distance,
z: origin.z + direction.z * distance,
}
}
///|
/// Pick the nearest indexed product along a normalized or non-normalized ray.
pub fn scene_pick(
index : SpatialIndex,
origin : Point3,
direction : Vector3,
) -> ScenePick {
let normalized = direction.normalized()
let hits = index.raycast(origin, normalized)
match hits.get(0) {
Some(hit) => {
let point = scene_ray_point(origin, normalized, hit.distance)
{
origin,
direction: normalized,
product_id: Some(hit.product_id),
distance: Some(hit.distance),
point: Some(point),
}
}
None =>
{
origin,
direction: normalized,
product_id: None,
distance: None,
point: None,
}
}
}
///|
pub fn ScenePick::is_hit(self : ScenePick) -> Bool {
self.product_id is Some(_)
}
///|
pub fn ScenePick::headline(self : ScenePick) -> String {
match (self.product_id, self.distance) {
(Some(id), Some(distance)) =>
"hit=#" + id.to_string() + " distance=" + distance.to_string()
_ => "miss"
}
}
///|
pub fn ScenePick::to_json(self : ScenePick) -> String {
match (self.product_id, self.distance, self.point) {
(Some(id), Some(distance), Some(point)) =>
"{\"product_id\":" +
id.to_string() +
",\"distance\":" +
distance.to_string() +
",\"point\":[" +
point.x.to_string() +
"," +
point.y.to_string() +
"," +
point.z.to_string() +
"]}"
_ => "{\"product_id\":null,\"distance\":null,\"point\":null}"
}
}
///|
pub fn scene_selection_ids_json(selection : SceneSelection) -> String {
"[" + selection.ids().map(id => id.to_string()).join(",") + "]"
}
///|
pub fn scene_selection_lod_json(selection : SceneSelection) -> String {
"[" +
selection.items
.map(item => {
"{\"product_id\":" +
item.product_id.to_string() +
",\"lod\":\"" +
scene_lod_name(item.lod) +
"\"}"
})
.join(",") +
"]"
}
///|
pub fn scene_selection_distance_range(
selection : SceneSelection,
) -> (Double, Double) {
if selection.items.length() == 0 {
return (0.0, 0.0)
}
let mut minimum = selection.items[0].distance
let mut maximum = minimum
for item in selection.items[1:] {
if item.distance < minimum {
minimum = item.distance
}
if item.distance > maximum {
maximum = item.distance
}
}
(minimum, maximum)
}
///|
pub fn scene_selection_type_histogram(
selection : SceneSelection,
) -> Map[String, Int] {
let result : Map[String, Int] = Map([])
for item in selection.items {
result.update_or_default(item.ifc_type, 1, count => count + 1)
}
result
}