/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** * Local parsing utility functions * Pure functions for geometry processing, bounds calculation, and batch management * * Extracted from useIfc.ts for reusability and testability */ import type { MeshData } from '@ifc-lite/geometry'; // ============================================================================ // Types // ============================================================================ /** * Bounding box for 3D geometry */ export interface Bounds3D { min: { x: number; y: number; z: number }; max: { x: number; y: number; z: number }; } /** * Coordinate information for geometry */ export interface CoordinateInfo { originShift: { x: number; y: number; z: number }; originalBounds: Bounds3D; shiftedBounds: Bounds3D; /** True if model had large coordinates requiring RTC shift. NOT the same as proper georeferencing via IfcMapConversion. */ hasLargeCoordinates: boolean; } /** * Geometry statistics */ export interface GeometryStats { totalVertices: number; totalTriangles: number; } // ============================================================================ // Bounds Calculation // ============================================================================ /** * Maximum coordinate threshold for valid geometry (10km) * Matches CoordinateHandler's NORMAL_COORD_THRESHOLD * Coordinates beyond this are likely corrupted or unshifted original coordinates */ export const MAX_VALID_COORD = 10000; /** * Create an initial bounds object with infinite values * Use this as a starting point for incremental bounds calculation */ export function createEmptyBounds(): Bounds3D { return { min: { x: Infinity, y: Infinity, z: Infinity }, max: { x: -Infinity, y: -Infinity, z: -Infinity }, }; } /** * Update bounds incrementally from a mesh's positions * Mutates the bounds object for performance * * @param bounds - Bounds object to update (mutated) * @param positions - Float32Array of vertex positions (x,y,z triplets) * @param maxCoord - Maximum valid coordinate value (default: 10km) */ export function updateBoundsFromPositions( bounds: Bounds3D, positions: Float32Array | number[], maxCoord: number = MAX_VALID_COORD ): void { for (let i = 0; i < positions.length; i += 3) { const x = positions[i]; const y = positions[i + 1]; const z = positions[i + 2]; // Filter out corrupted/unshifted vertices (> threshold from origin) const isValid = Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z) && Math.abs(x) < maxCoord && Math.abs(y) < maxCoord && Math.abs(z) < maxCoord; if (isValid) { bounds.min.x = Math.min(bounds.min.x, x); bounds.min.y = Math.min(bounds.min.y, y); bounds.min.z = Math.min(bounds.min.z, z); bounds.max.x = Math.max(bounds.max.x, x); bounds.max.y = Math.max(bounds.max.y, y); bounds.max.z = Math.max(bounds.max.z, z); } } } /** * Calculate bounds from an array of meshes * * @param meshes - Array of mesh data * @returns Bounds and geometry statistics */ export function calculateMeshBounds(meshes: MeshData[]): { bounds: Bounds3D; stats: GeometryStats } { const bounds = createEmptyBounds(); let totalVertices = 0; let totalTriangles = 0; for (const mesh of meshes) { updateBoundsFromPositions(bounds, mesh.positions); totalVertices += mesh.positions.length / 3; totalTriangles += mesh.indices.length / 3; } return { bounds, stats: { totalVertices, totalTriangles }, }; } /** * Create coordinate info from bounds * * @param bounds - Calculated geometry bounds * @param originShift - Optional origin shift (defaults to zero) * @param hasLargeCoordinates - Whether model had large coordinates requiring RTC shift * @returns Coordinate info object with cloned bounds and computed shiftedBounds */ export function createCoordinateInfo( bounds: Bounds3D, originShift: { x: number; y: number; z: number } = { x: 0, y: 0, z: 0 }, hasLargeCoordinates: boolean = false ): CoordinateInfo { // Deep-clone the incoming bounds into originalBounds const originalBounds: Bounds3D = { min: { x: bounds.min.x, y: bounds.min.y, z: bounds.min.z }, max: { x: bounds.max.x, y: bounds.max.y, z: bounds.max.z }, }; // Compute shiftedBounds by subtracting originShift from each min/max const shiftedBounds: Bounds3D = { min: { x: bounds.min.x - originShift.x, y: bounds.min.y - originShift.y, z: bounds.min.z - originShift.z, }, max: { x: bounds.max.x - originShift.x, y: bounds.max.y - originShift.y, z: bounds.max.z - originShift.z, }, }; return { originShift: { x: originShift.x, y: originShift.y, z: originShift.z }, originalBounds, shiftedBounds, hasLargeCoordinates, }; } // ============================================================================ // Render Throttling // ============================================================================ /** * Calculate render interval based on file size * Adaptive throttling: smaller files get more frequent updates, larger files fewer * * @param fileSizeMB - File size in megabytes * @returns Render interval in milliseconds */ export function getRenderIntervalMs(fileSizeMB: number): number { if (fileSizeMB > 300) { return 500; // Very large files: 2 updates/sec (fewer GPU fragment creations) } else if (fileSizeMB > 100) { return 200; // Huge files: 5 updates/sec } else if (fileSizeMB > 50) { return 100; // Large files: 10 updates/sec } else if (fileSizeMB > 20) { return 75; // Medium files: ~13 updates/sec } return 50; // Small files: 20 updates/sec } /** * Calculate server streaming render interval * * @param fileSizeMB - File size in megabytes * @returns Render interval in milliseconds */ export function getServerStreamIntervalMs(fileSizeMB: number): number { return fileSizeMB > 100 ? 200 : 100; } // ============================================================================ // Storey Height Calculation // ============================================================================ /** * Calculate storey heights from elevation differences * When property data doesn't provide heights, we can infer them from * the difference between consecutive storey elevations * * @param storeyElevations - Map of storey ID to elevation * @returns Map of storey ID to calculated height */ export function calculateStoreyHeights(storeyElevations: Map): Map { const heights = new Map(); if (storeyElevations.size < 2) { return heights; } const entries = Array.from(storeyElevations.entries()) as Array<[number, number]>; const sortedStoreys = entries.sort((a, b) => a[1] - b[1]); // Sort by elevation ascending for (let i = 0; i < sortedStoreys.length - 1; i++) { const [storeyId, elevation] = sortedStoreys[i]; const nextElevation = sortedStoreys[i + 1][1]; const height = nextElevation - elevation; if (height > 0) { heights.set(storeyId, height); } } return heights; } // ============================================================================ // Progress Calculation // ============================================================================ /** * Calculate progress percentage for geometry streaming * * @param currentMeshes - Number of meshes processed so far * @param estimatedTotal - Estimated total meshes (may be 0 or inaccurate) * @param basePercent - Base percentage (start of geometry phase) * @param maxPercent - Maximum percentage to reach * @returns Progress percentage */ export function calculateStreamingProgress( currentMeshes: number, estimatedTotal: number, basePercent: number = 50, maxPercent: number = 95 ): number { const denominator = Math.max(estimatedTotal / 10, currentMeshes); // Guard against division by zero (both currentMeshes and estimatedTotal are 0) if (denominator === 0) { return basePercent; } const progressRange = maxPercent - basePercent; return Math.min(maxPercent, basePercent + (currentMeshes / denominator) * progressRange); } // ============================================================================ // Mesh Conversion // ============================================================================ /** * Normalize IFCX mesh color to RGBA format * * @param color - Optional color array (RGB or RGBA) * @returns Normalized RGBA color tuple */ export function normalizeColor( color?: [number, number, number, number] | [number, number, number] | number[] ): [number, number, number, number] { // Return default color if no color provided or array is too short if (!color || color.length < 3) { return [0.7, 0.7, 0.7, 1.0]; } if (color.length === 4) { return color as [number, number, number, number]; } return [color[0], color[1], color[2], 1.0]; } /** * Convert server mesh colors from float [0-1] to byte [0-255] * * @param floatColors - Float color array [0-1] * @returns Uint8Array of byte colors [0-255] */ export function convertFloatColorToBytes(floatColors: number[]): Uint8Array { return new Uint8Array(floatColors.map((c: number) => Math.round(c * 255))); }