All files / src/models/cad parseSectorData.ts

4.24% Statements 5/118
0% Branches 0/4
0% Functions 0/18
4.63% Lines 5/108

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280            2x     2x 2x                                                                                             2x                                                                                                                                                                                                                                                                                                                                           2x                                                                                                                
/*!
 * Copyright 2020 Cognite AS
 */
 
import { Sector, SectorQuads, TriangleMesh, InstancedMesh, InstancedMeshFile } from './types';
import { FetchCtmDelegate, ParseSectorDelegate } from './delegates';
import { createOffsetsArray } from '../../utils/arrayUtils';
import { ParseQuadsResult, ParseSectorResult } from '../../workers/types/parser.types';
import { ParserWorker } from '../../workers/parser.worker';
import * as Comlink from 'comlink';
import { createSimpleCache } from '../createCache';
 
type WorkDelegate<T> = (worker: ParserWorker) => Promise<T>;
 
// TODO 2019-11-01 larsmoa: Move PooledWorker.
interface PooledWorker {
  // The worker returned by Comlink.wrap is not strictly speaking a ParserWorker,
  // but it should expose the same functions
  worker: ParserWorker;
  activeJobCount: number;
  messageIdCounter: number;
}
 
async function postWorkToAvailable<T>(workerList: PooledWorker[], work: WorkDelegate<T>): Promise<T> {
  let targetWorker = workerList[0];
  for (const worker of workerList) {
    if (worker.activeJobCount < targetWorker.activeJobCount) {
      targetWorker = worker;
    }
  }
  targetWorker.activeJobCount += 1;
  const result = await work(targetWorker.worker);
  targetWorker.activeJobCount -= 1;
  return result;
}
 
// TODO 20191030 larsmoa: Extract to separate file. Use Comlink (or other library
// for web workers) to prettify.
function createWorkers(): PooledWorker[] {
  const workerList: PooledWorker[] = [];
 
  for (let i = 0; i < window.navigator.hardwareConcurrency; i++) {
    const newWorker = {
      // NOTE: As of Comlink 4.2.0 we need to go through unknown before ParserWorker
      // Please feel free to remove `as unknown` if possible.
      worker: (Comlink.wrap(
        new Worker('../../workers/parser.worker', { name: 'parser', type: 'module' })
      ) as unknown) as ParserWorker,
      activeJobCount: 0,
      messageIdCounter: 0
    };
    workerList.push(newWorker);
  }
 
  return workerList;
}
 
export function createParser(fetchCtmFile: FetchCtmDelegate): ParseSectorDelegate<Sector> {
  const workerList = createWorkers();
 
  // TODO define the cache outside of the createParser function to make it configurable
  const loadCtmGeometryCache = createSimpleCache((fileId: number) => {
    return loadCtmGeometry(fileId, fetchCtmFile, workerList);
  });
 
  async function parse(sectorId: number, sectorArrayBuffer: Uint8Array): Promise<Sector> {
    try {
      const sectorResult: ParseSectorResult = await postWorkToAvailable(workerList, async (worker: ParserWorker) =>
        worker.parseSector(sectorArrayBuffer)
      );
      const {
        boxes,
        circles,
        cones,
        eccentricCones,
        ellipsoidSegments,
        generalCylinders,
        generalRings,
        instanceMeshes,
        nuts,
        quads,
        sphericalSegments,
        torusSegments,
        trapeziums,
        triangleMeshes
      } = sectorResult;
 
      const finalTriangleMeshes = await (async () => {
        const { fileIds, colors, triangleCounts, treeIndices } = triangleMeshes;
 
        const meshesGroupedByFile = groupMeshesByNumber(fileIds);
 
        const finalMeshes = [];
        // Merge meshes by file
        // TODO do this in Rust instead
        for (const [fileId, meshIndices] of meshesGroupedByFile.entries()) {
          const fileTriangleCounts = meshIndices.map(i => triangleCounts[i]);
          const offsets = createOffsetsArray(fileTriangleCounts);
          // Load CTM (geometry)
          const {
            indices,
            vertices,
            normals,
            colors: sharedColors,
            treeIndices: sharedTreeIndices
          } = await loadCtmGeometryCache.request(fileId);
 
          for (let i = 0; i < meshIndices.length; i++) {
            const meshIdx = meshIndices[i];
            const treeIndex = treeIndices[meshIdx];
            const triOffset = offsets[i];
            const triCount = fileTriangleCounts[i];
            const [r, g, b] = readColorToFloat32s(colors, meshIdx);
 
            sharedTreeIndices.fill(treeIndex, triOffset, triOffset + triCount);
            for (let triIdx = triOffset; triIdx < triOffset + triCount; triIdx++) {
              for (let j = 0; j < 3; j++) {
                const vIdx = indices[3 * triIdx + j];
 
                sharedColors[3 * vIdx] = r;
                sharedColors[3 * vIdx + 1] = g;
                sharedColors[3 * vIdx + 2] = b;
              }
            }
          }
 
          const mesh: TriangleMesh = {
            colors: sharedColors,
            fileId,
            treeIndices: sharedTreeIndices,
            indices,
            vertices,
            normals
          };
          finalMeshes.push(mesh);
        }
        return finalMeshes;
      })();
 
      const finalInstanceMeshes = await (async () => {
        const { fileIds, colors, treeIndices, triangleCounts, triangleOffsets, instanceMatrices } = instanceMeshes;
        const meshesGroupedByFile = groupMeshesByNumber(fileIds);
 
        const finalMeshes: InstancedMeshFile[] = [];
        // Merge meshes by file
        // TODO do this in Rust instead
        // TODO de-duplicate this with the merged meshes above
        for (const [fileId, meshIndices] of meshesGroupedByFile.entries()) {
          const ctm = await loadCtmGeometryCache.request(fileId);
 
          const indices = ctm.indices;
          const vertices = ctm.vertices;
          const normals = ctm.normals;
          const instancedMeshes: InstancedMesh[] = [];
 
          const fileTriangleOffsets = new Float64Array(meshIndices.map(i => triangleOffsets[i]));
          const fileTriangleCounts = new Float64Array(meshIndices.map(i => triangleCounts[i]));
          const fileMeshesGroupedByOffsets = groupMeshesByNumber(fileTriangleOffsets);
 
          for (const [triangleOffset, fileMeshIndices] of fileMeshesGroupedByOffsets) {
            // NOTE the triangle counts should be the same for all meshes with the same offset,
            // hence we can look up only fileMeshIndices[0] instead of enumerating here
            const triangleCount = fileTriangleCounts[fileMeshIndices[0]];
            const instanceMatrixBuffer = new Float32Array(16 * fileMeshIndices.length);
            const treeIndicesBuffer: number[] = new Array<number>(fileMeshIndices.length);
            const colorBuffer = new Uint8Array(4 * fileMeshIndices.length);
            for (let i = 0; i < fileMeshIndices.length; i++) {
              const meshIdx = meshIndices[fileMeshIndices[i]];
              const treeIndex = treeIndices[meshIdx];
              const instanceMatrix = instanceMatrices.slice(meshIdx * 16, meshIdx * 16 + 16);
              instanceMatrixBuffer.set(instanceMatrix, i * 16);
              treeIndicesBuffer[i] = treeIndex;
              const color = colors.slice(meshIdx * 4, meshIdx * 4 + 4);
              colorBuffer.set(color, i * 4);
            }
            instancedMeshes.push({
              triangleCount,
              triangleOffset,
              instanceMatrices: instanceMatrixBuffer,
              colors: colorBuffer,
              treeIndices: Float32Array.from(treeIndicesBuffer)
            });
          }
 
          const mesh: InstancedMeshFile = {
            fileId,
            indices,
            vertices,
            normals,
            instances: instancedMeshes
          };
          finalMeshes.push(mesh);
        }
 
        return finalMeshes;
      })();
 
      const sector: Sector = {
        boxes,
        circles,
        cones,
        eccentricCones,
        ellipsoidSegments,
        generalCylinders,
        generalRings,
        instanceMeshes: finalInstanceMeshes,
        nuts,
        quads,
        sphericalSegments,
        torusSegments,
        trapeziums,
        triangleMeshes: finalTriangleMeshes
      };
      return sector;
    } catch (err) {
      throw new Error(`Parsing sector ${sectorId} failed: ${err}`);
    }
 
    // TODO 20191023 larsmoa: Remember to free data from rust
  }
  return parse;
}
 
export async function createQuadsParser() {
  // TODO consider sharing workers with i3df parser
  const workerList = await createWorkers();
 
  async function parse(sectorId: number, quadsArrayBuffer: Uint8Array): Promise<SectorQuads> {
    try {
      const sectorResult = await postWorkToAvailable<ParseQuadsResult>(workerList, async (worker: ParserWorker) =>
        worker.parseQuads(quadsArrayBuffer)
      );
      return {
        buffer: sectorResult.data
      } as SectorQuads;
    } catch (err) {
      throw new Error(`Parsing quads sector ${sectorId} failed: ${err}`);
    }
 
    // TODO 20191023 larsmoa: Remember to free data from rust
  }
  return parse;
}
 
function groupMeshesByNumber(fileIds: Float64Array) {
  const meshesGroupedByFile = new Map<number, number[]>();
  for (let i = 0; i < fileIds.length; ++i) {
    const fileId = fileIds[i];
    const oldValue = meshesGroupedByFile.get(fileId);
    if (oldValue) {
      meshesGroupedByFile.set(fileId, [...oldValue, i]);
    } else {
      meshesGroupedByFile.set(fileId, [i]);
    }
  }
  return meshesGroupedByFile;
}
 
function readColorToFloat32s(colors: Uint8Array, index: number): [number, number, number, number] {
  const r = colors[4 * index] / 255;
  const g = colors[4 * index + 1] / 255;
  const b = colors[4 * index + 2] / 255;
  const a = colors[4 * index + 3] / 255;
  return [r, g, b, a];
}
 
async function loadCtmGeometry(
  fileId: number,
  fetchCtmFile: FetchCtmDelegate,
  workerList: PooledWorker[]
): Promise<any> {
  try {
    const buffer = await fetchCtmFile(fileId);
    const ctm = await postWorkToAvailable(workerList, async (worker: ParserWorker) => worker.parseCtm(buffer));
    return ctm;
  } catch (err) {
    throw new Error(`Parsing CTM file ${fileId} failed: ${err}`);
  }
}