diff --git a/README.md b/README.md index f5f1fd5..40af360 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,128 @@ -# lms-fe +# Autonomy Studio LMS -lms 프론트엔드 \ No newline at end of file +Autonomy Studio LMS의 학생용 데스크톱 애플리케이션입니다. Electron 기반으로 동작하며, 학습 워크스페이스에서 ROS 2/NAV2 모듈 설치와 실행, 코드 편집, Git 연동, 터미널, 맵 및 시각화를 제공합니다. + +로컬 PC 시뮬레이션과 원격 플랫폼 워크스페이스를 모두 지원합니다. + +## 주요 기능 + +- 강좌, 학습 단계, 모듈 기반 워크스페이스 관리 +- 로컬 및 원격 플랫폼의 모듈 설치, 빌드, 실행, 종료 +- 코드 편집기, Git 소스 제어, 내장 터미널 +- ROS 토픽 연결 상태와 모듈 간 발행/구독 관계 확인 +- 2D/3D SLAM 맵 저장 및 주행 시각화 +- Docker 기반 ROS 런타임과 SLAM ROS 브리지 관리 +- 원격 플랫폼 에이전트와 연동한 워크스페이스 및 교보재 제어 + +## 프로젝트 구조 + +```text +src/ + main/ Electron main process, Docker/워크스페이스/원격 제어 + preload/ renderer에 노출하는 IPC API + renderer/src/ React 사용자 인터페이스 +resources/ + slam-ros-bridge/ SLAM 및 시각화용 ROS 브리지 + bin/ 패키징에 포함되는 실행 파일 + licenses/ 배포 라이선스 +``` + +## 요구 사항 + +- Node.js와 npm +- Docker Engine +- Git +- Linux AppImage 패키징 시 필요한 시스템 의존성 + +GPU 가속을 사용할 경우에는 NVIDIA 드라이버 외에 Docker용 NVIDIA Container Toolkit이 필요합니다. GPU 가속은 Docker 컨테이너에 `--gpus all` 옵션을 전달하므로, Docker 런타임이 GPU를 인식하지 못하면 모듈 실행이 실패합니다. + +## 개발 환경 실행 + +```bash +cp .env.example .env +npm ci +npm run dev +``` + +renderer 개발 서버는 기본적으로 `5176` 포트를 사용합니다. + +## 환경 변수 + +`.env.example`을 복사해 로컬 환경에 맞게 설정합니다. + +| 변수 | 설명 | +| --- | --- | +| `VITE_API_BASE_URL` | LMS 백엔드 API 주소 | +| `VITE_LAUNCHER_API_BASE_URL` | 런처/플랫폼 API 주소 | +| `LOCAL_AUTOWARE_BRIDGE_IMAGE` | 로컬 Autoware ROS 브리지 이미지 | +| `LOCAL_NAV2_BRIDGE_IMAGE` | 로컬 NAV2 ROS 브리지 이미지 | +| `LOCAL_NAV2_RUNTIME_IMAGE` | 로컬 NAV2 런타임 이미지 | +| `LOCAL_SLAM_BRIDGE_IMAGE` | 로컬 SLAM ROS 브리지 이미지 | +| `LOCAL_SLAM_RUNTIME_IMAGE` | 로컬 SLAM 런타임 이미지 | +| `JETSON_AGENT_URL` | 원격 Jetson 에이전트 주소 | +| `JETSON_AGENT_TOKEN` | 원격 Jetson 에이전트 인증 토큰 | +| `JETSON_AUTOWARE_BRIDGE_IMAGE` | 원격 Autoware ROS 브리지 이미지 | +| `JETSON_NAV2_BRIDGE_IMAGE` | 원격 NAV2 ROS 브리지 이미지 | +| `JETSON_NAV2_RUNTIME_IMAGE` | 원격 NAV2 런타임 이미지 | + +`VITE_` 접두사 변수는 renderer 번들에 포함됩니다. 운영 AppImage를 만들 때는 운영 API 주소로 빌드해야 하며, 토큰이나 비밀번호 같은 민감한 값은 `VITE_` 변수에 넣으면 안 됩니다. + +예시: + +```bash +VITE_API_BASE_URL=https://workspace.autonomystudio.net/lms-api/api \ +VITE_LAUNCHER_API_BASE_URL=https://portal-api.autonomystudio.net/api \ +npm run build:linux +``` + +## 빌드 + +```bash +# Electron main, preload, renderer 빌드 +npm run build + +# Linux AppImage 생성 +npm run build:linux +``` + +패키징 결과물은 `dist/`에 생성됩니다. AppImage에는 아이콘, SLAM ROS 브리지, 실행 도구, 라이선스 리소스가 함께 포함됩니다. + +운영 배포용 AppImage는 관리자 웹의 빌드 변수와 동일한 API 주소, 동일한 소스 커밋을 기준으로 빌드해야 합니다. Kubernetes의 LMS 웹 배포 환경 변수는 Electron AppImage에 런타임으로 주입되지 않습니다. + +## 워크스페이스와 맵 + +모듈, 빌드 결과물, 저장 맵은 선택한 사용자 워크스페이스 루트 아래에 관리됩니다. SLAM 맵은 기본적으로 다음 경로에 저장됩니다. + +```text +/maps +``` + +앱은 해당 워크스페이스를 Docker 컨테이너의 `/workspace`로 마운트하고, SLAM 브리지에는 `SAVED_MAPS_PATH=/workspace/maps`를 전달합니다. + +## GPU 가속 + +GPU 가속을 켠 상태에서 다음과 비슷한 오류가 발생하면 Docker GPU 런타임이 설정되지 않은 상태입니다. + +```text +failed to discover GPU vendor from CDI +``` + +NVIDIA Container Toolkit을 설치하고 Docker를 재시작한 뒤 아래 명령이 정상 동작하는지 확인합니다. + +```bash +docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi +``` + +GPU 설정이 준비되지 않은 PC에서는 앱 설정에서 GPU 가속을 끈 상태로 사용합니다. + +## 로그와 문제 확인 + +- 앱 renderer 로그는 Electron의 사용자 로그 경로에 `renderer.log`로 기록됩니다. +- 모듈 실행 실패는 워크스페이스 화면의 모듈 로그와 Docker 컨테이너 로그를 함께 확인합니다. +- 원격 플랫폼 문제는 에이전트 상태, Tailscale/Headscale 연결, ROS Domain ID, 원격 Docker 이미지를 순서대로 점검합니다. + +## 보안 유의 사항 + +- `.env`는 저장소에 커밋하지 않습니다. +- 에이전트 토큰, API 키, 인증 정보는 빌드 변수 또는 별도 비밀 저장소로 관리합니다. +- 운영 배포 전에는 API 주소와 원격 런타임 이미지 태그가 대상 환경과 일치하는지 확인합니다. diff --git a/resources/slam-ros-bridge/bridge.py b/resources/slam-ros-bridge/bridge.py index 6d4a329..bb306c1 100644 --- a/resources/slam-ros-bridge/bridge.py +++ b/resources/slam-ros-bridge/bridge.py @@ -41,6 +41,7 @@ import uvicorn logging.basicConfig(level=logging.INFO) _POINT_CLOUD_LIMIT = max(1_000, int(os.environ.get("POINT_CLOUD_LIMIT", "50000"))) +_SENSOR_POINT_CLOUD_LIMIT = max(1_000, int(os.environ.get("SENSOR_POINT_CLOUD_LIMIT", "4000"))) _CAMERA_FRAME_MAX_AGE_SECONDS = 3.0 @@ -1254,7 +1255,7 @@ class SlamNode(Node): payload["transform"] = resolve_2d_transform(tf_snapshot, "map", payload["frame_id"]) return payload - def sensor_point_cloud_payload(self, limit: int = 10_000) -> Optional[dict]: + def sensor_point_cloud_payload(self, limit: int = _SENSOR_POINT_CLOUD_LIMIT) -> Optional[dict]: with self._lock: cloud = self._fallback_point_cloud tf_snapshot = dict(self._tf) diff --git a/src/main/index.ts b/src/main/index.ts index 81a31ba..e385c74 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -284,6 +284,52 @@ type SavedMapArtifactDownload = { url: string } +type PointCloudVoxel = { + maxZ: number + samples: number +} + +type PointCloudLayoutCache = { + source: { + fileName: string + size: number + modifiedAtMs: number + } + layout: WorldLayout +} + +type PgmImage = { + width: number + height: number + maxValue: number + pixels: Uint16Array +} + +type PgmMapMetadata = { + resolution: number + originX: number + originY: number + negate: boolean + occupiedThreshold: number +} + +type MapCollisionRectangle = { + startX: number + endX: number + startY: number + endY: number +} + +// Build the Gazebo collision layer from the real LiDAR's horizontal sensing +// band. This rejects floor and ceiling points, then extrudes each occupied +// voxel down to the ground for a stable 2D navigation obstacle. +const pointCloudMinObstacleHeightMeters = 0.25 +const pointCloudMaxObstacleHeightMeters = 1.2 +const maxPointCloudCollisionObjects = 1_200 +const pointCloudVoxelSizes = [0.16, 0.2, 0.25, 0.32, 0.4, 0.5] +const mapCollisionHeightMeters = 1.2 +const maxMapCollisionObjects = 900 + function defaultWorldLayout(): WorldLayout { return { version: 1, @@ -335,6 +381,326 @@ function safeSavedMapFileName(input: string): string { return fileName } +function savedMapPointCloudLayoutFileName(databaseFileName: string): string { + return safeSavedMapFileName(databaseFileName.replace(/\.db$/i, '.gazebo-3d-layout.json')) +} + +function savedMapPointCloudLayoutCacheFileName(databaseFileName: string): string { + return safeSavedMapFileName(databaseFileName.replace(/\.db$/i, '.gazebo-3d-layout-cache.json')) +} + +function savedMapCollisionLayoutFileName(yamlFileName: string): string { + return safeSavedMapFileName(yamlFileName.replace(/\.yaml$/i, '.gazebo-layout.json')) +} + +async function writeJsonAtomically(path: string, value: unknown): Promise { + const tempPath = `${path}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}` + await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, 'utf8') + await rename(tempPath, path) +} + +function pgmWhitespace(byte: number | undefined): boolean { + return byte === 9 || byte === 10 || byte === 13 || byte === 32 +} + +function readPgmImage(contents: Buffer): PgmImage { + let offset = 0 + const nextToken = (): string => { + while (offset < contents.length) { + if (contents[offset] === 35) { + while (offset < contents.length && contents[offset] !== 10) offset += 1 + } else if (pgmWhitespace(contents[offset])) { + offset += 1 + } else break + } + const start = offset + while (offset < contents.length && !pgmWhitespace(contents[offset]) && contents[offset] !== 35) offset += 1 + return contents.subarray(start, offset).toString('ascii') + } + const format = nextToken() + const width = Number(nextToken()) + const height = Number(nextToken()) + const maxValue = Number(nextToken()) + if (!['P2', 'P5'].includes(format) || !Number.isInteger(width) || !Number.isInteger(height) + || width <= 0 || height <= 0 || width * height > 4_000_000 + || !Number.isInteger(maxValue) || maxValue <= 0 || maxValue > 65_535) { + throw new Error('저장된 PGM 맵 형식이 올바르지 않습니다.') + } + const pixels = new Uint16Array(width * height) + if (format === 'P2') { + for (let index = 0; index < pixels.length; index += 1) { + const value = Number(nextToken()) + if (!Number.isInteger(value) || value < 0 || value > maxValue) throw new Error('저장된 PGM 맵 픽셀 데이터가 올바르지 않습니다.') + pixels[index] = value + } + return { width, height, maxValue, pixels } + } + if (!pgmWhitespace(contents[offset])) throw new Error('저장된 PGM 맵 헤더가 올바르지 않습니다.') + if (contents[offset] === 13 && contents[offset + 1] === 10) offset += 2 + else offset += 1 + const bytesPerPixel = maxValue < 256 ? 1 : 2 + if (contents.length - offset < pixels.length * bytesPerPixel) throw new Error('저장된 PGM 맵 데이터가 불완전합니다.') + for (let index = 0; index < pixels.length; index += 1) { + pixels[index] = bytesPerPixel === 1 + ? contents[offset + index] + : (contents[offset + index * 2] << 8) | contents[offset + index * 2 + 1] + } + return { width, height, maxValue, pixels } +} + +function readPgmMapMetadata(contents: string): PgmMapMetadata { + const scalar = (key: string, fallback: number): number => { + const match = contents.match(new RegExp(`^\\s*${key}\\s*:\\s*([^#\\r\\n]+)`, 'mi')) + const value = Number(match?.[1]?.trim()) + return Number.isFinite(value) ? value : fallback + } + const origin = contents.match(/^\s*origin\s*:\s*\[\s*([-+\d.eE]+)\s*,\s*([-+\d.eE]+)/mi) + const originX = Number(origin?.[1]) + const originY = Number(origin?.[2]) + const resolution = scalar('resolution', NaN) + if (!Number.isFinite(resolution) || resolution <= 0 || !Number.isFinite(originX) || !Number.isFinite(originY)) { + throw new Error('저장된 맵 YAML의 해상도 또는 원점 정보가 올바르지 않습니다.') + } + return { resolution, originX, originY, negate: scalar('negate', 0) !== 0, occupiedThreshold: Math.min(1, Math.max(0, scalar('occupied_thresh', 0.65))) } +} + +function mapCollisionRectangles(image: PgmImage, metadata: PgmMapMetadata, scale: number): MapCollisionRectangle[] { + const columns = Math.ceil(image.width / scale) + const rows = Math.ceil(image.height / scale) + const occupied = (x: number, y: number): boolean => { + const endY = Math.min(image.height, y + scale) + const endX = Math.min(image.width, x + scale) + for (let sourceY = y; sourceY < endY; sourceY += 1) { + for (let sourceX = x; sourceX < endX; sourceX += 1) { + const value = image.pixels[sourceY * image.width + sourceX] / image.maxValue + if ((metadata.negate ? value : 1 - value) >= metadata.occupiedThreshold) return true + } + } + return false + } + const rectangles: MapCollisionRectangle[] = [] + let activeRuns = new Map() + for (let row = 0; row < rows; row += 1) { + const nextRuns = new Map() + let column = 0 + while (column < columns) { + if (!occupied(column * scale, row * scale)) { column += 1; continue } + const startColumn = column + while (column < columns && occupied(column * scale, row * scale)) column += 1 + const key = `${startColumn}:${column}` + const active = activeRuns.get(key) + nextRuns.set(key, active ? { ...active, endY: row + 1 } : { startX: startColumn, endX: column, startY: row, endY: row + 1 }) + } + activeRuns.forEach((rectangle, key) => { if (!nextRuns.has(key)) rectangles.push(rectangle) }) + activeRuns = nextRuns + } + activeRuns.forEach((rectangle) => rectangles.push(rectangle)) + return rectangles +} + +function createMapCollisionLayout(yamlContents: string, pgmContents: Buffer): WorldLayout { + const image = readPgmImage(pgmContents) + const metadata = readPgmMapMetadata(yamlContents) + let scale = 1 + let rectangles = mapCollisionRectangles(image, metadata, scale) + while (rectangles.length > maxMapCollisionObjects && scale < 8) { scale *= 2; rectangles = mapCollisionRectangles(image, metadata, scale) } + if (rectangles.length > maxMapCollisionObjects) throw new Error('저장된 맵의 장애물이 너무 많아 시뮬레이션 충돌 환경으로 변환할 수 없습니다.') + const objects = rectangles.map((rectangle, index): WorldLayoutObject => { + const minX = metadata.originX + rectangle.startX * scale * metadata.resolution + const maxX = metadata.originX + Math.min(image.width, rectangle.endX * scale) * metadata.resolution + const minY = metadata.originY + (image.height - Math.min(image.height, rectangle.endY * scale)) * metadata.resolution + const maxY = metadata.originY + (image.height - rectangle.startY * scale) * metadata.resolution + return { id: `saved-map-obstacle-${index + 1}`, type: 'box', position: [(minX + maxX) / 2, (minY + maxY) / 2, mapCollisionHeightMeters / 2], rotation: [0, 0, 0], size: [Math.max(metadata.resolution, maxX - minX), Math.max(metadata.resolution, maxY - minY), mapCollisionHeightMeters] } + }) + return normalizeWorldLayout({ version: 1, bounds: { width: 40, depth: 40 }, objects }) +} + +async function generateWorkspaceMapCollisionLayout(workspaceRoot: string | null | undefined, yamlPath: string, pgmPath: string): Promise { + const root = expandUserPath(workspaceRoot) + if (!root) throw new Error('워크스페이스 경로가 설정되어 있지 않습니다.') + const yamlFileName = savedMapArtifactFileName('yaml', yamlPath) + const pgmFileName = savedMapArtifactFileName('pgm', pgmPath) + const mapsDirectory = resolve(root, 'maps') + const yamlFilePath = resolve(mapsDirectory, yamlFileName) + const pgmFilePath = resolve(mapsDirectory, pgmFileName) + if (!yamlFilePath.startsWith(`${mapsDirectory}/`) || !pgmFilePath.startsWith(`${mapsDirectory}/`)) throw new Error('저장된 맵 경로가 올바르지 않습니다.') + const layout = createMapCollisionLayout(await readFile(yamlFilePath, 'utf8'), await readFile(pgmFilePath)) + await mkdir(mapsDirectory, { recursive: true }) + await writeJsonAtomically(resolve(mapsDirectory, savedMapCollisionLayoutFileName(yamlFileName)), layout) + await writeJsonAtomically(worldLayoutPath(root), layout) + return layout +} + +function readExportedPointCloud(plyContents: Buffer): Array<[number, number, number]> { + const headerEnd = plyContents.indexOf(Buffer.from('end_header\n')) + if (headerEnd < 0 || headerEnd > 32_768) throw new Error('RTAB-Map 포인트 클라우드 헤더가 올바르지 않습니다.') + const headerLength = headerEnd + 'end_header\n'.length + const header = plyContents.subarray(0, headerLength).toString('ascii') + if (!header.includes('format binary_little_endian 1.0')) { + throw new Error('RTAB-Map 포인트 클라우드 형식을 읽을 수 없습니다.') + } + const vertexCount = Number(header.match(/^element vertex\s+(\d+)\s*$/m)?.[1]) + // rtabmap-export produces x/y/z followed by five float fields. + const stride = 32 + if (!Number.isInteger(vertexCount) || vertexCount <= 0 || plyContents.length < headerLength + vertexCount * stride) { + throw new Error('RTAB-Map 포인트 클라우드 데이터가 불완전합니다.') + } + + const points: Array<[number, number, number]> = [] + for (let index = 0; index < vertexCount; index += 1) { + const offset = headerLength + index * stride + const x = plyContents.readFloatLE(offset) + const y = plyContents.readFloatLE(offset + 4) + const z = plyContents.readFloatLE(offset + 8) + if (Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z)) points.push([x, y, z]) + } + return points +} + +function createPointCloudCollisionLayout(points: Array<[number, number, number]>): WorldLayout { + const obstaclePoints = points.filter(([, , z]) => ( + z >= pointCloudMinObstacleHeightMeters && z <= pointCloudMaxObstacleHeightMeters + )) + if (!obstaclePoints.length) throw new Error('3D SLAM 맵에서 Gazebo 장애물로 변환할 포인트를 찾지 못했습니다.') + + let voxels = new Map() + let voxelSize = pointCloudVoxelSizes[pointCloudVoxelSizes.length - 1] + for (const candidateSize of pointCloudVoxelSizes) { + const nextVoxels = new Map() + obstaclePoints.forEach(([x, y, z]) => { + const gridX = Math.floor(x / candidateSize) + const gridY = Math.floor(y / candidateSize) + const key = `${gridX}:${gridY}` + const current = nextVoxels.get(key) + if (current) { + current.maxZ = Math.max(current.maxZ, z) + current.samples += 1 + } else { + nextVoxels.set(key, { maxZ: z, samples: 1 }) + } + }) + voxels = nextVoxels + voxelSize = candidateSize + if (voxels.size <= maxPointCloudCollisionObjects) break + } + if (voxels.size > maxPointCloudCollisionObjects) { + throw new Error('3D SLAM 맵의 장애물이 너무 많아 Gazebo 충돌 환경으로 변환할 수 없습니다.') + } + + let minX = Infinity + let minY = Infinity + let maxX = -Infinity + let maxY = -Infinity + const objects = Array.from(voxels.entries()) + .filter(([, voxel]) => voxel.samples >= 2) + .map(([key, voxel], index): WorldLayoutObject => { + const [gridX, gridY] = key.split(':').map(Number) + const centerX = (gridX + 0.5) * voxelSize + const centerY = (gridY + 0.5) * voxelSize + const floorZ = 0 + const ceilingZ = Math.min(pointCloudMaxObstacleHeightMeters, voxel.maxZ + voxelSize / 2) + const height = Math.max(voxelSize, ceilingZ) + minX = Math.min(minX, centerX - voxelSize / 2) + minY = Math.min(minY, centerY - voxelSize / 2) + maxX = Math.max(maxX, centerX + voxelSize / 2) + maxY = Math.max(maxY, centerY + voxelSize / 2) + return { + id: `saved-3d-map-voxel-${index + 1}`, + type: 'box', + position: [centerX, centerY, floorZ + height / 2], + rotation: [0, 0, 0], + size: [voxelSize, voxelSize, height] + } + }) + + if (!objects.length || !Number.isFinite(minX) || !Number.isFinite(minY) || !Number.isFinite(maxX) || !Number.isFinite(maxY)) { + throw new Error('3D SLAM 맵의 유효한 장애물을 생성하지 못했습니다.') + } + const padding = 2 + return normalizeWorldLayout({ + version: 1, + bounds: { + width: Math.max(10, maxX - minX + padding * 2), + depth: Math.max(10, maxY - minY + padding * 2) + }, + objects + }) +} + +async function ensureSlamRosBridgeImage(): Promise { + const exists = await runCommand('docker', ['image', 'inspect', slamRosBridgeImage], undefined, undefined, 10_000) + .then(() => true) + .catch(() => false) + if (!exists) await ensureBridgeImage(slamRosBridgeImage, 'resources/slam-ros-bridge') +} + +async function exportWorkspaceMapPointCloud(workspaceRoot: string, databaseFileName: string): Promise { + const exportDirectoryName = `.gazebo-3d-export-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}` + const mapsDirectory = resolve(workspaceRoot, 'maps') + const exportDirectory = resolve(mapsDirectory, exportDirectoryName) + const exportedPly = resolve(exportDirectory, 'gazebo-map_cloud.ply') + await mkdir(exportDirectory, { recursive: true }) + try { + await ensureSlamRosBridgeImage() + const userId = typeof process.getuid === 'function' ? process.getuid() : 1000 + const groupId = typeof process.getgid === 'function' ? process.getgid() : 1000 + await runCommand('docker', [ + 'run', '--rm', '--user', `${userId}:${groupId}`, + '-v', `${workspaceRoot}:/workspace`, + '--entrypoint', 'bash', + slamRosBridgeImage, + '-lc', [ + 'source /opt/ros/humble/setup.bash', + `rtabmap-export --cloud --scan --voxel 0.08 --output gazebo-map --output_dir /workspace/maps/${exportDirectoryName} /workspace/maps/${databaseFileName}` + ].join(' && ') + ], undefined, undefined, 180_000) + return await readFile(exportedPly) + } finally { + await rm(exportDirectory, { recursive: true, force: true }).catch(() => undefined) + } +} + +async function generateWorkspacePointCloudCollisionLayout( + workspaceRoot: string | null | undefined, + databasePath: string, +): Promise { + const root = expandUserPath(workspaceRoot) + if (!root) throw new Error('워크스페이스 경로가 설정되어 있지 않습니다.') + const databaseFileName = savedMapArtifactFileName('database', databasePath) + const mapsDirectory = resolve(root, 'maps') + const databaseFilePath = resolve(mapsDirectory, databaseFileName) + if (!databaseFilePath.startsWith(`${mapsDirectory}/`)) throw new Error('저장된 3D 맵 경로가 올바르지 않습니다.') + + const layoutPath = resolve(mapsDirectory, savedMapPointCloudLayoutFileName(databaseFileName)) + const cachePath = resolve(mapsDirectory, savedMapPointCloudLayoutCacheFileName(databaseFileName)) + const sourceStat = await stat(databaseFilePath) + try { + const cached = JSON.parse(await readFile(cachePath, 'utf8')) as Partial + if (cached.source?.fileName === databaseFileName + && cached.source.size === sourceStat.size + && cached.source.modifiedAtMs === sourceStat.mtimeMs + && cached.layout) { + const layout = normalizeWorldLayout(cached.layout) + await writeJsonAtomically(worldLayoutPath(root), layout) + return layout + } + } catch { + // Create the cached layout below when it does not exist or is stale. + } + + const layout = createPointCloudCollisionLayout(readExportedPointCloud( + await exportWorkspaceMapPointCloud(root, databaseFileName) + )) + await mkdir(mapsDirectory, { recursive: true }) + await writeJsonAtomically(layoutPath, layout) + await writeJsonAtomically(cachePath, { + source: { fileName: databaseFileName, size: sourceStat.size, modifiedAtMs: sourceStat.mtimeMs }, + layout, + } satisfies PointCloudLayoutCache) + await writeJsonAtomically(worldLayoutPath(root), layout) + return layout +} + function savedMapArtifactFileName(name: SavedMapArtifactName, path: string): string { const fileName = safeSavedMapFileName(path) const expectedExtension = name === 'yaml' ? '.yaml' : name === 'pgm' ? '.pgm' : '.db' @@ -1600,29 +1966,15 @@ async function startRemoteVisualizationBridge( .map(([key, value]) => `-e ${quoteShellSingle(`${key}=${value}`)}`) .join(' ') let image = resolveVisualizationBridgeImage(stack, environmentProfile?.bridge, environmentProfile?.runtimeImages) - if (stack === 'slam3d' && image === slamRosBridgeImage) { - const jetson3dMapSaveBridgeInstalled = await runSshOutput( - target, - 'docker image inspect slam-ros-bridge:jetson_orin-3d-map-save >/dev/null 2>&1 && printf yes || true', - 10_000, - ) - if (jetson3dMapSaveBridgeInstalled.trim() === 'yes') { - image = 'slam-ros-bridge:jetson_orin-3d-map-save' - } else { - const jetson3dBridgeInstalled = await runSshOutput( - target, - 'docker image inspect slam-ros-bridge:jetson_orin-3d >/dev/null 2>&1 && printf yes || true', - 10_000, - ) - if (jetson3dBridgeInstalled.trim() === 'yes') image = 'slam-ros-bridge:jetson_orin-3d' - } - } else if (stack !== 'autoware' && image === slamRosBridgeImage) { - const jetsonBridgeInstalled = await runSshOutput( - target, - 'docker image inspect slam-ros-bridge:jetson_orin >/dev/null 2>&1 && printf yes || true', - 10_000, - ) - if (jetsonBridgeInstalled.trim() === 'yes') image = 'slam-ros-bridge:jetson_orin' + const remoteJetsonBridgeImage = environmentProfile?.runtimeImages + ?.find((artifact) => ( + /^slam-ros-bridge:jetson_orin(?:[-:].*)?$/i.test(artifact?.image?.trim() ?? '') + && Boolean(artifact?.url?.trim()) + )) + ?.image + ?.trim() + if (stack !== 'autoware' && image === slamRosBridgeImage && remoteJetsonBridgeImage) { + image = remoteJetsonBridgeImage } const imageArtifact = environmentProfile?.runtimeImages?.find((artifact) => artifact?.image?.trim() === image) if (imageArtifact?.url?.trim()) { @@ -1631,11 +1983,19 @@ async function startRemoteVisualizationBridge( `IMAGE=${quoteShellSingle(image)}`, `URL=${quoteShellSingle(imageArtifact.url.trim())}`, `FILE_NAME=${quoteShellSingle(safeArtifactFileName(imageArtifact.url.trim(), imageArtifact.fileName))}`, + `EXPECTED_SHA256=${quoteShellSingle(imageArtifact.checksumSha256?.trim().toLowerCase() ?? '')}`, 'if docker image inspect "$IMAGE" >/dev/null 2>&1; then exit 0; fi', 'CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/autonomy-studio/artifacts"', 'mkdir -p "$CACHE"', 'ARCHIVE="$CACHE/$FILE_NAME"', + 'if [ -f "$ARCHIVE" ] && [ -n "$EXPECTED_SHA256" ]; then', + ' [ "$(sha256sum "$ARCHIVE" | awk \'{print $1}\')" = "$EXPECTED_SHA256" ] || rm -f "$ARCHIVE"', + 'fi', 'if [ ! -f "$ARCHIVE" ]; then curl -fL "$URL" -o "$ARCHIVE"; fi', + 'if [ -n "$EXPECTED_SHA256" ] && [ "$(sha256sum "$ARCHIVE" | awk \'{print $1}\')" != "$EXPECTED_SHA256" ]; then', + ' echo "시각화 이미지 체크섬이 일치하지 않습니다: $FILE_NAME" >&2', + ' exit 1', + 'fi', 'case "$ARCHIVE" in', ' *.tar.zst|*.tzst) zstd -dc "$ARCHIVE" | docker load ;;', ' *) docker load -i "$ARCHIVE" ;;', @@ -5415,7 +5775,7 @@ function registerLocalWorkspaceHandlers(): void { const targetPath = worldLayoutPath(workspaceRoot) const normalized = normalizeWorldLayout(layout) await mkdir(dirname(targetPath), { recursive: true }) - await writeFile(targetPath, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8') + await writeJsonAtomically(targetPath, normalized) return normalized }) ipcMain.handle('local-workspace:maps:read-artifacts', (_event, workspaceRoot: string | null | undefined, artifacts: SavedMapArtifactPath[]) => ( @@ -5424,6 +5784,12 @@ function registerLocalWorkspaceHandlers(): void { ipcMain.handle('local-workspace:maps:sync', (_event, workspaceRoot: string | null | undefined, artifacts: SavedMapArtifactDownload[]) => ( syncWorkspaceSavedMapArtifacts(workspaceRoot, artifacts) )) + ipcMain.handle('local-workspace:maps:generate-collision-layout', (_event, workspaceRoot: string | null | undefined, yamlPath: string, pgmPath: string) => ( + generateWorkspaceMapCollisionLayout(workspaceRoot, yamlPath, pgmPath) + )) + ipcMain.handle('local-workspace:maps:generate-point-cloud-collision-layout', (_event, workspaceRoot: string | null | undefined, databasePath: string) => ( + generateWorkspacePointCloudCollisionLayout(workspaceRoot, databasePath) + )) ipcMain.handle('remote-workspace:modules:list', (_event, target: RemoteWorkspaceTarget, modules?: CourseModuleInput[] | null) => collectRemoteAvailableModules(target, modules)) ipcMain.handle('remote-workspace:ros-graph', (_event, target: RemoteWorkspaceTarget) => collectRemoteRosGraph(target)) ipcMain.handle('remote-workspace:maps:read-artifacts', (_event, target: RemoteWorkspaceTarget, artifacts: SavedMapArtifactPath[]) => ( diff --git a/src/preload/index.ts b/src/preload/index.ts index a55cb08..8e9998f 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -219,7 +219,9 @@ contextBridge.exposeInMainWorld('localWorkspace', { }, maps: { readArtifacts: (workspaceRoot: string | undefined, artifacts: SavedMapArtifactPath[]) => ipcRenderer.invoke('local-workspace:maps:read-artifacts', workspaceRoot, artifacts), - sync: (workspaceRoot: string | undefined, artifacts: SavedMapArtifactDownload[]) => ipcRenderer.invoke('local-workspace:maps:sync', workspaceRoot, artifacts) + sync: (workspaceRoot: string | undefined, artifacts: SavedMapArtifactDownload[]) => ipcRenderer.invoke('local-workspace:maps:sync', workspaceRoot, artifacts), + generateCollisionLayout: (workspaceRoot: string | undefined, yamlPath: string, pgmPath: string) => ipcRenderer.invoke('local-workspace:maps:generate-collision-layout', workspaceRoot, yamlPath, pgmPath), + generatePointCloudCollisionLayout: (workspaceRoot: string | undefined, databasePath: string) => ipcRenderer.invoke('local-workspace:maps:generate-point-cloud-collision-layout', workspaceRoot, databasePath) }, visualization: { status: () => ipcRenderer.invoke('local-workspace:visualization:status'), diff --git a/src/renderer/public/assets/images/module-guide-local.png b/src/renderer/public/assets/images/module-guide-local.png new file mode 100644 index 0000000..71de5d2 Binary files /dev/null and b/src/renderer/public/assets/images/module-guide-local.png differ diff --git a/src/renderer/public/assets/images/module-guide-remote-3d.png b/src/renderer/public/assets/images/module-guide-remote-3d.png new file mode 100644 index 0000000..281a316 Binary files /dev/null and b/src/renderer/public/assets/images/module-guide-remote-3d.png differ diff --git a/src/renderer/public/assets/images/module-guide-remote-platform.png b/src/renderer/public/assets/images/module-guide-remote-platform.png new file mode 100644 index 0000000..987d1b1 Binary files /dev/null and b/src/renderer/public/assets/images/module-guide-remote-platform.png differ diff --git a/src/renderer/public/slam-viewer/assets/room_0731_navigation.glb b/src/renderer/public/slam-viewer/assets/room_0731_navigation.glb deleted file mode 100644 index f2364ad..0000000 Binary files a/src/renderer/public/slam-viewer/assets/room_0731_navigation.glb and /dev/null differ diff --git a/src/renderer/public/slam-viewer/assets/room_0731_navigation.ply b/src/renderer/public/slam-viewer/assets/room_0731_navigation.ply deleted file mode 100644 index 587caf8..0000000 Binary files a/src/renderer/public/slam-viewer/assets/room_0731_navigation.ply and /dev/null differ diff --git a/src/renderer/public/slam-viewer/index.html b/src/renderer/public/slam-viewer/index.html index 8e40ef4..0772127 100644 --- a/src/renderer/public/slam-viewer/index.html +++ b/src/renderer/public/slam-viewer/index.html @@ -239,7 +239,6 @@ #downloadMap, #pathToggle, #sensorToggle, - #splatToggle, #poseToggle, #goalToggle, #obstacleToggle, @@ -258,7 +257,6 @@ #downloadMap, #pathToggle, #sensorToggle, - #splatToggle, #poseToggle, #goalToggle, #obstacleToggle { @@ -269,25 +267,6 @@ align-items:center; gap:6px; } - #splatToggle { - display:inline-flex; - align-items:center; - justify-content:center; - position:absolute; - top:16px; - left:16px; - z-index:10; - width:14px; - min-width:14px; - min-height:14px; - height:14px; - padding:0; - border-color:rgba(203,213,225,.3); - border-radius:2px; - background:rgba(148,163,184,.3); - box-shadow:none; - } - #splatToggle:hover { background:rgba(148,163,184,.5); border-color:rgba(226,232,240,.45); } #cameraPresetSelect { min-width:86px; padding:0 24px 0 9px; @@ -301,7 +280,6 @@ #downloadMap:hover, #pathToggle:hover, #sensorToggle:hover, - #splatToggle:hover, #poseToggle:hover, #goalToggle:hover, #obstacleToggle:hover, @@ -332,10 +310,6 @@ border-color:rgba(103,232,249,.55); color:#fff; } - #splatToggle.active { - background:rgba(148,163,184,.62); - border-color:rgba(226,232,240,.58); - } #goalToggle.active { background:rgba(22,163,74,.62); border-color:rgba(134,239,172,.55); @@ -388,13 +362,11 @@ .editor-mode #rosTopicStatusWrap { display:none; } - #splatToggle { display:none; } .nav2-slam #goalToggle, .nav2-slam #poseToggle { display:none; } .nav2-navigation #downloadWrap { display:none; } .nav2-navigation #topLeftControls { display:block; } .nav2-navigation #keyGuide { display:none; } .nav2-navigation #rosTopicStatusWrap { bottom:16px; } - .nav2-navigation.real-runtime #splatToggle { display:inline-flex; } .real-runtime #topLeftControls { display:none; } .real-runtime #keyGuide { display:none; } @@ -403,7 +375,6 @@
-
@@ -452,8 +423,7 @@ { "imports": { "three": "../vendor/three/build/three.module.js", - "three/addons/": "../vendor/three/examples/jsm/", - "@mkkellogg/gaussian-splats-3d": "./vendor/gaussian-splats-3d.module.js" + "three/addons/": "../vendor/three/examples/jsm/" } } @@ -464,7 +434,6 @@ import { TransformControls } from 'three/addons/controls/TransformControls.js'; import { ColladaLoader } from 'three/addons/loaders/ColladaLoader.js'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; import { STLLoader } from 'three/addons/loaders/STLLoader.js'; -import * as GaussianSplats3D from '@mkkellogg/gaussian-splats-3d'; const viewerMapKey = new URLSearchParams(window.location.search).get('mapKey'); const params = new URLSearchParams(location.search); @@ -479,6 +448,7 @@ const SENSOR_POINT_CLOUD_MAX_AGE_MS = 3_000; // regular visualization sessions never poll `/camera-frame`. const ENABLE_CAMERA_PREVIEW = false; const CUSTOM_WORLD = !REAL_RUNTIME && viewerMapKey === 'nav2-custom'; +const SAVED_GAZEBO_MAP_WORLD = !REAL_RUNTIME && viewerMapKey === 'nav2-saved-gazebo-map'; const activeWorldModel = viewerMapKey === 'nav2-turtlebot3-house' ? 'turtlebot3_house' : 'turtlebot3_world'; const MKMINI_WORLD_SINGLE_PILLAR = USE_MKMINI_MODEL && activeWorldModel === 'turtlebot3_world'; const MKMINI_RENDER_LENGTH = 0.84; @@ -502,7 +472,6 @@ const mapSaveOverlay = document.getElementById('mapSaveOverlay'); const downloadMapButton = document.getElementById('downloadMap'); const pathToggleButton = document.getElementById('pathToggle'); const sensorToggleButton = document.getElementById('sensorToggle'); -const splatToggleButton = document.getElementById('splatToggle'); const poseToggleButton = document.getElementById('poseToggle'); const goalToggleButton = document.getElementById('goalToggle'); const obstacleToggleButton = document.getElementById('obstacleToggle'); @@ -513,7 +482,6 @@ const rosTopicStatusToggle = document.getElementById('rosTopicStatusToggle'); const rosTopicStatusList = document.getElementById('rosTopicStatusList'); const EDITOR_MODE = params.get('editor') === '1'; const NAV2_VIEW_MODE = params.get('mode') === 'navigation' ? 'navigation' : 'slam'; - const SHOW_REMOTE_NAVIGATION_ROOM = REAL_RUNTIME && NAV2_VIEW_MODE === 'navigation'; // A navigation session must keep the selected saved map as its fixed // reference. The runtime /map topic may later emit a partial local update. const LOCK_NAVIGATION_MAP = REAL_RUNTIME && NAV2_VIEW_MODE === 'navigation'; @@ -601,10 +569,10 @@ const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.05, 1 // Three.js OrbitControls 기본 조작이 자연스럽게 동작하도록 사선 시점에서 시작합니다. camera.position.set(4, 6, 5); camera.up.set(0, 1, 0); -const renderer = new THREE.WebGLRenderer({ antialias:!SHOW_REMOTE_NAVIGATION_ROOM }); +const renderer = new THREE.WebGLRenderer({ antialias:true }); renderer.setSize(innerWidth, innerHeight); -renderer.setPixelRatio(SHOW_REMOTE_NAVIGATION_ROOM ? 1 : Math.min(devicePixelRatio, 1.25)); -renderer.shadowMap.enabled = !SHOW_REMOTE_NAVIGATION_ROOM; +renderer.setPixelRatio(Math.min(devicePixelRatio, 1.25)); +renderer.shadowMap.enabled = true; renderer.domElement.tabIndex = 0; document.body.appendChild(renderer.domElement); @@ -615,7 +583,7 @@ controls.mouseButtons = { RIGHT: THREE.MOUSE.DOLLY, }; renderer.domElement.addEventListener('contextmenu', (e) => e.preventDefault()); -controls.enableDamping = !SHOW_REMOTE_NAVIGATION_ROOM; +controls.enableDamping = true; controls.dampingFactor = 0.08; controls.target.set(0, 0, 0); camera.lookAt(controls.target); @@ -658,15 +626,6 @@ gazeboWorldRoot.add(runtimeLayoutObjects); const navigationMapRoot = new THREE.Group(); navigationMapRoot.name = 'navigation-occupancy-map'; sdfRoot.add(navigationMapRoot); -let remoteNavigationSplatViewer = null; -let remoteNavigationSplatScene = null; -let remoteNavigationSplatTransform = null; -let remoteNavigationSplatEnabled = false; -let remoteNavigationSplatLoading = null; -// 화면의 빨간 X축과 평행하게 오른쪽으로 미세 보정합니다. -const remoteNavigationSplatBasePosition = new THREE.Vector3(-1.6822845855746674, 1.3107954323633903, -0.8021416967574936); -const remoteNavigationSplatBaseQuaternion = new THREE.Quaternion(-0.10999094822602469, 0.6984998148234114, 0.6984998148234114, 0.10999094822602469); -const remoteNavigationSplatBaseScale = new THREE.Vector3(3.672624762730334, 3.672624762730334, 3.5309628207501587); const pointCloudGeometry = SHOW_POINT_CLOUD ? new THREE.BufferGeometry() : null; let pointCloudLayer = null; let hasGlobalMapPointCloud = false; @@ -894,6 +853,10 @@ let lastMkMiniSteeringUpdateMs = null; let lastMkMiniMotionSample = null; let hasSimulatorMkMiniSteering = false; let laserGroup = null; +let laserFillMesh = null; +let laserLineSegments = null; +let laserOpenSpaceLineSegments = null; +let laserFrontBeam = null; let slamSocket = null; let controlSocket = null; let latestSlamState = null; @@ -908,6 +871,10 @@ let navigationMapLocked = false; let navigationMapFocused = false; let latestScanMetadata = null; let latestScanRanges = null; +let lastLaserRenderAtMs = 0; +let lastMiniMapRenderAtMs = 0; +let pendingMiniMapState = null; +let miniMapRenderTimer = null; let pointCloudRequestInFlight = false; let pointCloudRefreshTimer = null; let savedMapPointCloudLoaded = false; @@ -1432,70 +1399,10 @@ function createMkMiniFrontWheelPivots(model) { return pivots; } -async function loadRemoteNavigationRoom() { - const viewer = new GaussianSplats3D.Viewer({ - selfDrivenMode: false, - threeScene: scene, - renderer, - camera, - useBuiltInControls: false, - ignoreDevicePixelRatio: true, - gpuAcceleratedSort: false, - sharedMemoryForWorkers: false, - integerBasedSort: true, - halfPrecisionCovariancesOnGPU: true, - dynamicScene: true, - renderMode: GaussianSplats3D.RenderMode.Always, - sceneRevealMode: GaussianSplats3D.SceneRevealMode.Instant, - antialiased: false, - sphericalHarmonicsDegree: 0, - enableOptionalEffects: false, - inMemoryCompressionLevel: 2, - }); - await viewer.addSplatScene('./assets/room_0731_navigation.ply', { - position: remoteNavigationSplatBasePosition.toArray(), - rotation: remoteNavigationSplatBaseQuaternion.toArray(), - scale: remoteNavigationSplatBaseScale.toArray(), - splatAlphaRemovalThreshold: 0, - showLoadingUI: false, - }); - remoteNavigationSplatViewer = viewer; - remoteNavigationSplatScene = viewer.splatMesh.getScene(0); - remoteNavigationSplatTransform = null; - updateRemoteNavigationRoomTransform(); -} - -async function setRemoteNavigationSplatVisible(visible) { - if (!SHOW_REMOTE_NAVIGATION_ROOM) return; - - splatToggleButton.disabled = true; - try { - if (visible && !remoteNavigationSplatViewer) { - if (!remoteNavigationSplatLoading) { - remoteNavigationSplatLoading = loadRemoteNavigationRoom(); - } - await remoteNavigationSplatLoading; - } - remoteNavigationSplatEnabled = visible; - if (remoteNavigationSplatScene) { - remoteNavigationSplatScene.visible = visible; - if (visible) { - remoteNavigationSplatTransform = null; - updateRemoteNavigationRoomTransform(); - } - } - remoteNavigationSplatViewer?.forceRenderNextFrame(); - } catch (error) { - console.warn('remote navigation room render failed', error); - remoteNavigationSplatEnabled = false; - } finally { - splatToggleButton.disabled = false; - splatToggleButton.classList.toggle('active', remoteNavigationSplatEnabled); - splatToggleButton.setAttribute('aria-pressed', String(remoteNavigationSplatEnabled)); - } -} - const LASER_HOLE_R = 0.14; +const LASER_RENDER_INTERVAL_MS = 100; +const LASER_OPEN_SPACE_RAY_STRIDE = 4; +const MINI_MAP_RENDER_INTERVAL_MS = 100; const LASER_OFFSET_X = USE_MKMINI_MODEL ? 0.15 : -0.032; const LASER_OFFSET_Y = 0; // Fallback height when an explicit ROS scan pose is unavailable. @@ -1507,9 +1414,12 @@ function setRobotPose(pose) { targetRobotPose = { x: Number(pose.x ?? targetRobotPose.x), y: Number(pose.y ?? targetRobotPose.y), - z: Number(pose.z ?? targetRobotPose.z), - roll: Number(pose.roll ?? targetRobotPose.roll), - pitch: Number(pose.pitch ?? targetRobotPose.pitch), + // Gazebo navigation is a ground-plane simulation. A saved 3D map can + // include an elevated RTAB-Map pose, but that must not lift the rendered + // vehicle after the learner supplies a 2D initial pose. + z: REAL_RUNTIME ? Number(pose.z ?? targetRobotPose.z) : 0, + roll: REAL_RUNTIME ? Number(pose.roll ?? targetRobotPose.roll) : 0, + pitch: REAL_RUNTIME ? Number(pose.pitch ?? targetRobotPose.pitch) : 0, yaw: Number(pose.yaw ?? targetRobotPose.yaw), }; updateMkMiniSteeringTarget(pose); @@ -1750,11 +1660,72 @@ function laserDirectionFromPose(pose, angle) { return new THREE.Vector3(Math.cos(angle), Math.sin(angle), 0).applyQuaternion(q).normalize(); } +function ensureLaserScanLayer() { + if (laserGroup) { + laserGroup.visible = true; + return; + } + + laserGroup = new THREE.Group(); + laserGroup.name = 'live-laser-scan'; + + const fillGeometry = new THREE.BufferGeometry(); + laserFillMesh = new THREE.Mesh( + fillGeometry, + new THREE.MeshBasicMaterial({ + color: 0x005cff, + transparent: true, + opacity: 0.6, + side: THREE.DoubleSide, + depthWrite: false, + }), + ); + laserFillMesh.frustumCulled = false; + + const lineGeometry = new THREE.BufferGeometry(); + laserLineSegments = new THREE.LineSegments( + lineGeometry, + new THREE.LineBasicMaterial({ color: 0x3366ff, transparent: true, opacity: 0.5 }), + ); + laserLineSegments.frustumCulled = false; + + const openSpaceGeometry = new THREE.BufferGeometry(); + laserOpenSpaceLineSegments = new THREE.LineSegments( + openSpaceGeometry, + new THREE.LineBasicMaterial({ color: 0x6f9dff, transparent: true, opacity: 0.28 }), + ); + laserOpenSpaceLineSegments.frustumCulled = false; + + laserFrontBeam = new THREE.Mesh( + new THREE.CylinderGeometry(0.006, 0.006, 1, 16), + new THREE.MeshBasicMaterial({ color: 0x005cff }), + ); + laserFrontBeam.frustumCulled = false; + + laserGroup.add(laserFillMesh, laserLineSegments, laserOpenSpaceLineSegments, laserFrontBeam); + sdfRoot.add(laserGroup); +} + +function updateLaserGeometry(geometry, values) { + let position = geometry.getAttribute('position'); + if (!position || position.array.length < values.length) { + const capacity = Math.max(1024, Math.ceil(values.length * 1.25)); + position = new THREE.BufferAttribute(new Float32Array(capacity), 3); + position.setUsage(THREE.DynamicDrawUsage); + geometry.setAttribute('position', position); + } + position.array.set(values); + position.needsUpdate = true; + geometry.setDrawRange(0, values.length / 3); +} + function clearLaserScan() { if (!laserGroup) return; - sdfRoot.remove(laserGroup); - disposeObject(laserGroup); - laserGroup = null; + laserGroup.visible = false; + laserFillMesh.visible = false; + laserLineSegments.visible = false; + laserOpenSpaceLineSegments.visible = false; + laserFrontBeam.visible = false; } function setNavigationSensorVisible(visible) { @@ -1767,6 +1738,7 @@ function setNavigationSensorVisible(visible) { window.clearInterval(sensorPointCloudRefreshTimer); sensorPointCloudRefreshTimer = null; } + lastLaserRenderAtMs = 0; clearLaserScan(); clearSensorPointCloud(); drawMiniMap(latestSlamState); @@ -1775,7 +1747,7 @@ function setNavigationSensorVisible(visible) { if (SHOW_SENSOR_POINT_CLOUD) { void refreshSensorPointCloud(); if (!sensorPointCloudRefreshTimer) { - sensorPointCloudRefreshTimer = window.setInterval(() => void refreshSensorPointCloud(), 500); + sensorPointCloudRefreshTimer = window.setInterval(() => void refreshSensorPointCloud(), 750); } } if (latestScanMetadata && latestScanRanges) { @@ -1796,6 +1768,11 @@ function drawLaserScan(scan, scanPose = ( return; } if (!scan?.ranges?.length) return; + // The state socket and scan binary socket can deliver the same scan nearly + // together. Rebuilding transparent scan geometry twice stalls the viewer. + const now = performance.now(); + if (now - lastLaserRenderAtMs < LASER_RENDER_INTERVAL_MS) return; + lastLaserRenderAtMs = now; clearLaserScan(); const sourcePose = scanPose || currentRobotPose; @@ -1830,7 +1807,7 @@ function drawLaserScan(scan, scanPose = ( endpoints.push({ start, point, hit: valid }); if (valid) { linePoints.push(start.x, start.y, start.z, point.x, point.y, point.z); - } else if (showOpenSpaceRays) { + } else if (showOpenSpaceRays && i % LASER_OPEN_SPACE_RAY_STRIDE === 0) { // Match Gazebo's ray visualization: no-return rays remain visible, but // deliberately subdued so detected obstacles stay the primary signal. openSpaceLinePoints.push(start.x, start.y, start.z, point.x, point.y, point.z); @@ -1850,32 +1827,13 @@ function drawLaserScan(scan, scanPose = ( fillPoints.push(next.start.x, next.start.y, next.start.z); } - const grp = new THREE.Group(); - const fillMat = new THREE.MeshBasicMaterial({ - color: 0x005cff, - transparent: true, - opacity: 0.6, - side: THREE.DoubleSide, - depthWrite: false, - }); - const lineMat = new THREE.LineBasicMaterial({ color: 0x3366ff, transparent: true, opacity: 0.5 }); - - if (fillPoints.length) { - const geo = new THREE.BufferGeometry(); - geo.setAttribute('position', new THREE.Float32BufferAttribute(fillPoints, 3)); - grp.add(new THREE.Mesh(geo, fillMat)); - } - const lineGeo = new THREE.BufferGeometry(); - lineGeo.setAttribute('position', new THREE.Float32BufferAttribute(linePoints, 3)); - grp.add(new THREE.LineSegments(lineGeo, lineMat)); - if (openSpaceLinePoints.length) { - const openSpaceGeo = new THREE.BufferGeometry(); - openSpaceGeo.setAttribute('position', new THREE.Float32BufferAttribute(openSpaceLinePoints, 3)); - grp.add(new THREE.LineSegments( - openSpaceGeo, - new THREE.LineBasicMaterial({ color: 0x6f9dff, transparent: true, opacity: 0.28 }), - )); - } + ensureLaserScanLayer(); + updateLaserGeometry(laserFillMesh.geometry, fillPoints); + laserFillMesh.visible = fillPoints.length > 0; + updateLaserGeometry(laserLineSegments.geometry, linePoints); + laserLineSegments.visible = linePoints.length > 0; + updateLaserGeometry(laserOpenSpaceLineSegments.geometry, openSpaceLinePoints); + laserOpenSpaceLineSegments.visible = openSpaceLinePoints.length > 0; const frontIndex = Math.max(0, Math.min(scan.ranges.length - 1, Math.round((0 - angleMin) / angleInc))); const front = endpoints[frontIndex]; @@ -1889,22 +1847,20 @@ function drawLaserScan(scan, scanPose = ( ); const len = delta.length(); if (len > 0.01) { - const beam = new THREE.Mesh( - new THREE.CylinderGeometry(0.006, 0.006, len, 16), - new THREE.MeshBasicMaterial({ color: 0x005cff }) - ); - beam.position.set( + laserFrontBeam.position.set( (front.start.x + front.point.x) / 2, (front.start.y + front.point.y) / 2, (front.start.z + front.point.z) / 2 ); - beam.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), delta.normalize()); - grp.add(beam); + laserFrontBeam.scale.set(1, len, 1); + laserFrontBeam.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), delta.normalize()); + laserFrontBeam.visible = true; + } else { + laserFrontBeam.visible = false; } + } else { + laserFrontBeam.visible = false; } - - laserGroup = grp; - sdfRoot.add(grp); } function updatePointGeometry(geometry, points) { @@ -1923,6 +1879,13 @@ function updatePointGeometry(geometry, points) { positions[count++] = z; } + if (count === 0) { + geometry.deleteAttribute('position'); + geometry.boundingBox = null; + geometry.boundingSphere = null; + return; + } + geometry.setAttribute('position', new THREE.BufferAttribute(positions.slice(0, count), 3)); geometry.computeBoundingSphere(); } @@ -2061,6 +2024,19 @@ async function refreshCameraFrame() { } function drawMiniMap(state) { + pendingMiniMapState = state; + if (miniMapRenderTimer) return; + + const elapsed = performance.now() - lastMiniMapRenderAtMs; + const wait = Math.max(0, MINI_MAP_RENDER_INTERVAL_MS - elapsed); + miniMapRenderTimer = window.setTimeout(() => { + miniMapRenderTimer = null; + lastMiniMapRenderAtMs = performance.now(); + drawMiniMapNow(pendingMiniMapState); + }, wait); +} + +function drawMiniMapNow(state) { if (!miniCtx) return; const w = miniMap.width; const h = miniMap.height; @@ -2247,7 +2223,6 @@ function updateNavigationMapTransform() { // estimate and must not move the static world geometry. gazeboWorldRoot.position.set(0, 0, 0); gazeboWorldRoot.rotation.set(0, 0, 0); - updateRemoteNavigationRoomTransform(); return; } gazeboWorldRoot.position.set(0, 0, 0); @@ -2263,61 +2238,6 @@ function updateNavigationMapTransform() { 0, ); navigationMapRoot.rotation.set(0, 0, -yaw); - updateRemoteNavigationRoomTransform(); -} - -function updateRemoteNavigationRoomTransform() { - if (!remoteNavigationSplatEnabled || !remoteNavigationSplatScene || NAV2_VIEW_MODE !== 'navigation') return; - - if (navigationSceneUsesMapFrame()) { - if (remoteNavigationSplatTransform?.frame === 'map') return; - remoteNavigationSplatScene.position.copy(remoteNavigationSplatBasePosition); - remoteNavigationSplatScene.quaternion.copy(remoteNavigationSplatBaseQuaternion); - remoteNavigationSplatScene.scale.copy(remoteNavigationSplatBaseScale); - remoteNavigationSplatViewer.splatMesh.updateTransforms(); - remoteNavigationSplatTransform = { frame: 'map' }; - return; - } - - const transform = navigationMapToOdom || { x:0, y:0, yaw:0 }; - if (remoteNavigationSplatTransform - && remoteNavigationSplatTransform.frame === 'odom' - && Math.abs(remoteNavigationSplatTransform.x - (transform.x || 0)) < 0.03 - && Math.abs(remoteNavigationSplatTransform.y - (transform.y || 0)) < 0.03 - && Math.abs(remoteNavigationSplatTransform.yaw - (transform.yaw || 0)) < 0.01) { - return; - } - const yaw = transform.yaw || 0; - const c = Math.cos(yaw); - const s = Math.sin(yaw); - // The Gaussian scene is authored in map coordinates. Compose the same - // map <- odom inverse used by the occupancy-map root before each render. - const mapToOdomMatrix = new THREE.Matrix4().compose( - new THREE.Vector3( - -c * (transform.x || 0) - s * (transform.y || 0), - 0, - -s * (transform.x || 0) + c * (transform.y || 0), - ), - new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), -yaw), - new THREE.Vector3(1, 1, 1), - ); - const sceneMatrix = mapToOdomMatrix.multiply(new THREE.Matrix4().compose( - remoteNavigationSplatBasePosition, - remoteNavigationSplatBaseQuaternion, - remoteNavigationSplatBaseScale, - )); - sceneMatrix.decompose( - remoteNavigationSplatScene.position, - remoteNavigationSplatScene.quaternion, - remoteNavigationSplatScene.scale, - ); - remoteNavigationSplatViewer.splatMesh.updateTransforms(); - remoteNavigationSplatTransform = { - frame: 'odom', - x: transform.x || 0, - y: transform.y || 0, - yaw, - }; } function renderNavigationOccupancyMap() { @@ -2615,11 +2535,6 @@ sensorToggleButton.addEventListener('click', () => { focusViewer(); }); -splatToggleButton.addEventListener('click', () => { - void setRemoteNavigationSplatVisible(!remoteNavigationSplatEnabled); - focusViewer(); -}); - cameraPreviewToggle?.addEventListener('click', () => { const collapsed = cameraPreviewWrap?.classList.toggle('is-collapsed') ?? false; cameraPreviewToggle.setAttribute('aria-expanded', String(!collapsed)); @@ -3161,7 +3076,7 @@ function applySlamState(state) { updateNavigationPlan(state?.plan); // Remote navigation is rendered in the map frame, matching the saved map, // room model, AMCL pose, route and goal. Local Gazebo remains odom-based. - const visualizationPose = (SHOW_POINT_CLOUD || (REAL_RUNTIME && NAV2_VIEW_MODE === 'navigation' && state?.pose_3d)) + const visualizationPose = (REAL_RUNTIME && (SHOW_POINT_CLOUD || (NAV2_VIEW_MODE === 'navigation' && state?.pose_3d))) ? (state?.pose_3d || state?.pose) : (NAV2_VIEW_MODE === 'navigation' ? (navigationSceneUsesMapFrame() ? (state?.pose_map || state?.pose) : state?.pose) @@ -3230,7 +3145,7 @@ if (SHOW_POINT_CLOUD) { } if (SHOW_SENSOR_POINT_CLOUD && navigationSensorVisible) { void refreshSensorPointCloud(); - sensorPointCloudRefreshTimer = window.setInterval(() => void refreshSensorPointCloud(), 500); + sensorPointCloudRefreshTimer = window.setInterval(() => void refreshSensorPointCloud(), 750); } if (ENABLE_CAMERA_PREVIEW && REAL_RUNTIME) { void refreshCameraFrame(); @@ -3240,6 +3155,7 @@ if (ENABLE_CAMERA_PREVIEW && REAL_RUNTIME) { window.addEventListener('beforeunload', () => { if (pointCloudRefreshTimer) window.clearInterval(pointCloudRefreshTimer); if (sensorPointCloudRefreshTimer) window.clearInterval(sensorPointCloudRefreshTimer); + if (miniMapRenderTimer) window.clearTimeout(miniMapRenderTimer); if (cameraFrameRefreshTimer) window.clearInterval(cameraFrameRefreshTimer); if (cameraFrameObjectUrl) URL.revokeObjectURL(cameraFrameObjectUrl); }); @@ -3459,7 +3375,7 @@ window.addEventListener('blur', () => { // A static Gazebo model can contain assets that the browser cannot render. // Keep the ROS stream available even when the decorative world mesh fails. -const worldSceneTasks = CUSTOM_WORLD || REAL_RUNTIME +const worldSceneTasks = CUSTOM_WORLD || SAVED_GAZEBO_MAP_WORLD || REAL_RUNTIME ? [] : [buildFromSdf().catch((error) => console.warn('world model render failed', error))]; const remoteNavigationSceneTasks = []; @@ -3488,12 +3404,7 @@ function animate(){ updateSmoothedRobotPose(); updateMkMiniFrontWheelSteering(); controls.update(); - if (remoteNavigationSplatViewer && remoteNavigationSplatEnabled) { - remoteNavigationSplatViewer.update(); - remoteNavigationSplatViewer.render(); - } else { - renderer.render(scene, camera); - } + renderer.render(scene, camera); } animate(); diff --git a/src/renderer/public/slam-viewer/vendor/gaussian-splats-3d.module.js b/src/renderer/public/slam-viewer/vendor/gaussian-splats-3d.module.js deleted file mode 100644 index 5edf354..0000000 --- a/src/renderer/public/slam-viewer/vendor/gaussian-splats-3d.module.js +++ /dev/null @@ -1,14447 +0,0 @@ -import * as THREE from 'three'; -import { Ray as Ray$1, Plane, MathUtils, EventDispatcher, Vector3, MOUSE, TOUCH, Quaternion, Spherical, Vector2 } from 'three'; - -/** - * AbortablePromise: A quick & dirty wrapper for JavaScript's Promise class that allows the underlying - * asynchronous operation to be cancelled. It is only meant for simple situations where no complex promise - * chaining or merging occurs. It needs a significant amount of work to truly replicate the full - * functionality of JavaScript's Promise class. Look at Util.fetchWithProgress() for example usage. - * - * This class was primarily added to allow splat scene downloads to be cancelled. It has not been tested - * very thoroughly and the implementation is kinda janky. If you can at all help it, please avoid using it :) - */ -class AbortablePromise { - - static idGen = 0; - - constructor(promiseFunc, abortHandler) { - - let resolver; - let rejecter; - this.promise = new Promise((resolve, reject) => { - resolver = resolve; - rejecter = reject; - }); - - const promiseResolve = resolver.bind(this); - const promiseReject = rejecter.bind(this); - - const resolve = (...args) => { - promiseResolve(...args); - }; - - const reject = (error) => { - promiseReject(error); - }; - - promiseFunc(resolve.bind(this), reject.bind(this)); - this.abortHandler = abortHandler; - this.id = AbortablePromise.idGen++; - } - - then(onResolve) { - return new AbortablePromise((resolve, reject) => { - this.promise = this.promise - .then((...args) => { - const onResolveResult = onResolve(...args); - if (onResolveResult instanceof Promise || onResolveResult instanceof AbortablePromise) { - onResolveResult.then((...args2) => { - resolve(...args2); - }); - } else { - resolve(onResolveResult); - } - }) - .catch((error) => { - reject(error); - }); - }, this.abortHandler); - } - - catch(onFail) { - return new AbortablePromise((resolve) => { - this.promise = this.promise.then((...args) => { - resolve(...args); - }) - .catch(onFail); - }, this.abortHandler); - } - - abort(reason) { - if (this.abortHandler) this.abortHandler(reason); - } - -} - -class AbortedPromiseError extends Error { - - constructor(msg) { - super(msg); - } - -} - -const floatToHalf = function() { - - const floatView = new Float32Array(1); - const int32View = new Int32Array(floatView.buffer); - - return function(val) { - floatView[0] = val; - const x = int32View[0]; - - let bits = (x >> 16) & 0x8000; - let m = (x >> 12) & 0x07ff; - const e = (x >> 23) & 0xff; - - if (e < 103) return bits; - - if (e > 142) { - bits |= 0x7c00; - bits |= ((e == 255) ? 0 : 1) && (x & 0x007fffff); - return bits; - } - - if (e < 113) { - m |= 0x0800; - bits |= (m >> (114 - e)) + ((m >> (113 - e)) & 1); - return bits; - } - - bits |= (( e - 112) << 10) | (m >> 1); - bits += m & 1; - return bits; - }; - -}(); - -const uintEncodedFloat = function() { - - const floatView = new Float32Array(1); - const int32View = new Int32Array(floatView.buffer); - - return function(f) { - floatView[0] = f; - return int32View[0]; - }; - -}(); - -const rgbaToInteger = function(r, g, b, a) { - return r + (g << 8) + (b << 16) + (a << 24); -}; - -const rgbaArrayToInteger = function(arr, offset) { - return arr[offset] + (arr[offset + 1] << 8) + (arr[offset + 2] << 16) + (arr[offset + 3] << 24); -}; - -const fetchWithProgress = function(path, onProgress, saveChunks = true, headers) { - - const abortController = new AbortController(); - const signal = abortController.signal; - let aborted = false; - const abortHandler = (reason) => { - abortController.abort(reason); - aborted = true; - }; - - let onProgressCalledAtComplete = false; - const localOnProgress = (percent, percentLabel, chunk, fileSize) => { - if (onProgress && !onProgressCalledAtComplete) { - onProgress(percent, percentLabel, chunk, fileSize); - if (percent === 100) { - onProgressCalledAtComplete = true; - } - } - }; - - return new AbortablePromise((resolve, reject) => { - const fetchOptions = { signal }; - if (headers) fetchOptions.headers = headers; - fetch(path, fetchOptions) - .then(async (data) => { - // Handle error conditions where data is still returned - if (!data.ok) { - const errorText = await data.text(); - reject(new Error(`Fetch failed: ${data.status} ${data.statusText} ${errorText}`)); - return; - } - - const reader = data.body.getReader(); - let bytesDownloaded = 0; - let _fileSize = data.headers.get('Content-Length'); - let fileSize = _fileSize ? parseInt(_fileSize) : undefined; - - const chunks = []; - - while (!aborted) { - try { - const { value: chunk, done } = await reader.read(); - if (done) { - localOnProgress(100, '100%', chunk, fileSize); - if (saveChunks) { - const buffer = new Blob(chunks).arrayBuffer(); - resolve(buffer); - } else { - resolve(); - } - break; - } - bytesDownloaded += chunk.length; - let percent; - let percentLabel; - if (fileSize !== undefined) { - percent = bytesDownloaded / fileSize * 100; - percentLabel = `${percent.toFixed(2)}%`; - } - if (saveChunks) { - chunks.push(chunk); - } - localOnProgress(percent, percentLabel, chunk, fileSize); - } catch (error) { - reject(error); - return; - } - } - }) - .catch((error) => { - reject(new AbortedPromiseError(error)); - }); - }, abortHandler); - -}; - -const clamp = function(val, min, max) { - return Math.max(Math.min(val, max), min); -}; - -const getCurrentTime = function() { - return performance.now() / 1000; -}; - -const disposeAllMeshes = (object3D) => { - if (object3D.geometry) { - object3D.geometry.dispose(); - object3D.geometry = null; - } - if (object3D.material) { - object3D.material.dispose(); - object3D.material = null; - } - if (object3D.children) { - for (let child of object3D.children) { - disposeAllMeshes(child); - } - } -}; - -const delayedExecute = (func, fast) => { - return new Promise((resolve) => { - window.setTimeout(() => { - resolve(func ? func() : undefined); - }, fast ? 1 : 50); - }); -}; - - -const getSphericalHarmonicsComponentCountForDegree = (sphericalHarmonicsDegree = 0) => { - let shCoeffPerSplat = 0; - if (sphericalHarmonicsDegree === 1) { - shCoeffPerSplat = 9; - } else if (sphericalHarmonicsDegree === 2) { - shCoeffPerSplat = 24; - } else if (sphericalHarmonicsDegree === 3) { - shCoeffPerSplat = 45; - } else if (sphericalHarmonicsDegree > 3) { - throw new Error('getSphericalHarmonicsComponentCountForDegree() -> Invalid spherical harmonics degree'); - } - return shCoeffPerSplat; -}; - -const nativePromiseWithExtractedComponents = () => { - let resolver; - let rejecter; - const promise = new Promise((resolve, reject) => { - resolver = resolve; - rejecter = reject; - }); - return { - 'promise': promise, - 'resolve': resolver, - 'reject': rejecter - }; -}; - -const abortablePromiseWithExtractedComponents = (abortHandler) => { - let resolver; - let rejecter; - if (!abortHandler) { - abortHandler = () => {}; - } - const promise = new AbortablePromise((resolve, reject) => { - resolver = resolve; - rejecter = reject; - }, abortHandler); - return { - 'promise': promise, - 'resolve': resolver, - 'reject': rejecter - }; -}; - -class Semver { - constructor(major, minor, patch) { - this.major = major; - this.minor = minor; - this.patch = patch; - } - - toString() { - return `${this.major}_${this.minor}_${this.patch}`; - } -} - -function isIOS() { - const ua = navigator.userAgent; - return ua.indexOf('iPhone') > 0 || ua.indexOf('iPad') > 0; -} - -function getIOSSemever() { - if (isIOS()) { - const extract = navigator.userAgent.match(/OS (\d+)_(\d+)_?(\d+)?/); - return new Semver( - parseInt(extract[1] || 0, 10), - parseInt(extract[2] || 0, 10), - parseInt(extract[3] || 0, 10) - ); - } else { - return null; // or [0,0,0] - } -} - -const BASE_COMPONENT_COUNT = 14; - -class UncompressedSplatArray { - - static OFFSET = { - X: 0, - Y: 1, - Z: 2, - SCALE0: 3, - SCALE1: 4, - SCALE2: 5, - ROTATION0: 6, - ROTATION1: 7, - ROTATION2: 8, - ROTATION3: 9, - FDC0: 10, - FDC1: 11, - FDC2: 12, - OPACITY: 13, - FRC0: 14, - FRC1: 15, - FRC2: 16, - FRC3: 17, - FRC4: 18, - FRC5: 19, - FRC6: 20, - FRC7: 21, - FRC8: 22, - FRC9: 23, - FRC10: 24, - FRC11: 25, - FRC12: 26, - FRC13: 27, - FRC14: 28, - FRC15: 29, - FRC16: 30, - FRC17: 31, - FRC18: 32, - FRC19: 33, - FRC20: 34, - FRC21: 35, - FRC22: 36, - FRC23: 37 - }; - - constructor(sphericalHarmonicsDegree = 0) { - this.sphericalHarmonicsDegree = sphericalHarmonicsDegree; - this.sphericalHarmonicsCount = getSphericalHarmonicsComponentCountForDegree(this.sphericalHarmonicsDegree); - this.componentCount = this.sphericalHarmonicsCount + BASE_COMPONENT_COUNT; - this.defaultSphericalHarmonics = new Array(this.sphericalHarmonicsCount).fill(0); - this.splats = []; - this.splatCount = 0; - } - - static createSplat(sphericalHarmonicsDegree = 0) { - const baseSplat = [0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]; - let shEntries = getSphericalHarmonicsComponentCountForDegree(sphericalHarmonicsDegree); - for (let i = 0; i < shEntries; i++) baseSplat.push(0); - return baseSplat; - } - - addSplat(splat) { - this.splats.push(splat); - this.splatCount++; - } - - getSplat(index) { - return this.splats[index]; - } - - addDefaultSplat() { - const newSplat = UncompressedSplatArray.createSplat(this.sphericalHarmonicsDegree); - this.addSplat(newSplat); - return newSplat; - } - - addSplatFromComonents(x, y, z, scale0, scale1, scale2, rot0, rot1, rot2, rot3, r, g, b, opacity, ...rest) { - const newSplat = [x, y, z, scale0, scale1, scale2, rot0, rot1, rot2, rot3, r, g, b, opacity, ...this.defaultSphericalHarmonics]; - for (let i = 0; i < rest.length && i < this.sphericalHarmonicsCount; i++) { - newSplat[i] = rest[i]; - } - this.addSplat(newSplat); - return newSplat; - } - - addSplatFromArray(src, srcIndex) { - const srcSplat = src.splats[srcIndex]; - const newSplat = UncompressedSplatArray.createSplat(this.sphericalHarmonicsDegree); - for (let i = 0; i < this.componentCount && i < srcSplat.length; i++) { - newSplat[i] = srcSplat[i]; - } - this.addSplat(newSplat); - } -} - -class Constants { - - static DefaultSplatSortDistanceMapPrecision = 16; - static MemoryPageSize = 65536; - static BytesPerFloat = 4; - static BytesPerInt = 4; - static MaxScenes = 32; - static ProgressiveLoadSectionSize = 262144; - static ProgressiveLoadSectionDelayDuration = 15; - static SphericalHarmonics8BitCompressionRange = 3; -} - -const DefaultSphericalHarmonics8BitCompressionRange = Constants.SphericalHarmonics8BitCompressionRange; -const DefaultSphericalHarmonics8BitCompressionHalfRange = DefaultSphericalHarmonics8BitCompressionRange / 2.0; - -const toHalfFloat = THREE.DataUtils.toHalfFloat.bind(THREE.DataUtils); -const fromHalfFloat$1 = THREE.DataUtils.fromHalfFloat.bind(THREE.DataUtils); - -const toUncompressedFloat = (f, compressionLevel, isSH = false, range8BitMin, range8BitMax) => { - if (compressionLevel === 0) { - return f; - } else if (compressionLevel === 1 || compressionLevel === 2 && !isSH) { - return THREE.DataUtils.fromHalfFloat(f); - } else if (compressionLevel === 2) { - return fromUint8(f, range8BitMin, range8BitMax); - } -}; - -const toUint8 = (v, rangeMin, rangeMax) => { - v = clamp(v, rangeMin, rangeMax); - const range = (rangeMax - rangeMin); - return clamp(Math.floor((v - rangeMin) / range * 255), 0, 255); -}; - -const fromUint8 = (v, rangeMin, rangeMax) => { - const range = (rangeMax - rangeMin); - return (v / 255 * range + rangeMin); -}; - -const fromHalfFloatToUint8 = (v, rangeMin, rangeMax) => { - return toUint8(fromHalfFloat$1(v, rangeMin, rangeMax)); -}; - -const fromUint8ToHalfFloat = (v, rangeMin, rangeMax) => { - return toHalfFloat(fromUint8(v, rangeMin, rangeMax)); -}; - -const dataViewFloatForCompressionLevel = (dataView, floatIndex, compressionLevel, isSH = false) => { - if (compressionLevel === 0) { - return dataView.getFloat32(floatIndex * 4, true); - } else if (compressionLevel === 1 || compressionLevel === 2 && !isSH) { - return dataView.getUint16(floatIndex * 2, true); - } else { - return dataView.getUint8(floatIndex, true); - } -}; - -const convertBetweenCompressionLevels = function() { - - const noop = (v) => v; - - return function(val, fromLevel, toLevel, isSH = false) { - if (fromLevel === toLevel) return val; - let outputConversionFunc = noop; - - if (fromLevel === 2 && isSH) { - if (toLevel === 1) outputConversionFunc = fromUint8ToHalfFloat; - else if (toLevel == 0) { - outputConversionFunc = fromUint8; - } - } else if (fromLevel === 2 || fromLevel === 1) { - if (toLevel === 0) outputConversionFunc = fromHalfFloat$1; - else if (toLevel == 2) { - if (!isSH) outputConversionFunc = noop; - else outputConversionFunc = fromHalfFloatToUint8; - } - } else if (fromLevel === 0) { - if (toLevel === 1) outputConversionFunc = toHalfFloat; - else if (toLevel == 2) { - if (!isSH) outputConversionFunc = toHalfFloat; - else outputConversionFunc = toUint8; - } - } - - return outputConversionFunc(val); - }; - -}(); - -const copyBetweenBuffers = (srcBuffer, srcOffset, destBuffer, destOffset, byteCount = 0) => { - const src = new Uint8Array(srcBuffer, srcOffset); - const dest = new Uint8Array(destBuffer, destOffset); - for (let i = 0; i < byteCount; i++) { - dest[i] = src[i]; - } -}; - -/** - * SplatBuffer: Container for splat data from a single scene/file and capable of (mediocre) compression. - */ -class SplatBuffer { - - static CurrentMajorVersion = 0; - static CurrentMinorVersion = 1; - - static CenterComponentCount = 3; - static ScaleComponentCount = 3; - static RotationComponentCount = 4; - static ColorComponentCount = 4; - static CovarianceComponentCount = 6; - - static SplatScaleOffsetFloat = 3; - static SplatRotationOffsetFloat = 6; - - static CompressionLevels = { - 0: { - BytesPerCenter: 12, - BytesPerScale: 12, - BytesPerRotation: 16, - BytesPerColor: 4, - ScaleOffsetBytes: 12, - RotationffsetBytes: 24, - ColorOffsetBytes: 40, - SphericalHarmonicsOffsetBytes: 44, - ScaleRange: 1, - BytesPerSphericalHarmonicsComponent: 4, - SphericalHarmonicsOffsetFloat: 11, - SphericalHarmonicsDegrees: { - 0: { BytesPerSplat: 44 }, - 1: { BytesPerSplat: 80 }, - 2: { BytesPerSplat: 140 } - }, - }, - 1: { - BytesPerCenter: 6, - BytesPerScale: 6, - BytesPerRotation: 8, - BytesPerColor: 4, - ScaleOffsetBytes: 6, - RotationffsetBytes: 12, - ColorOffsetBytes: 20, - SphericalHarmonicsOffsetBytes: 24, - ScaleRange: 32767, - BytesPerSphericalHarmonicsComponent: 2, - SphericalHarmonicsOffsetFloat: 12, - SphericalHarmonicsDegrees: { - 0: { BytesPerSplat: 24 }, - 1: { BytesPerSplat: 42 }, - 2: { BytesPerSplat: 72 } - }, - }, - 2: { - BytesPerCenter: 6, - BytesPerScale: 6, - BytesPerRotation: 8, - BytesPerColor: 4, - ScaleOffsetBytes: 6, - RotationffsetBytes: 12, - ColorOffsetBytes: 20, - SphericalHarmonicsOffsetBytes: 24, - ScaleRange: 32767, - BytesPerSphericalHarmonicsComponent: 1, - SphericalHarmonicsOffsetFloat: 12, - SphericalHarmonicsDegrees: { - 0: { BytesPerSplat: 24 }, - 1: { BytesPerSplat: 33 }, - 2: { BytesPerSplat: 48 } - }, - } - }; - - static CovarianceSizeFloats = 6; - - static HeaderSizeBytes = 4096; - static SectionHeaderSizeBytes = 1024; - - static BucketStorageSizeBytes = 12; - static BucketStorageSizeFloats = 3; - - static BucketBlockSize = 5.0; - static BucketSize = 256; - - constructor(bufferData, secLoadedCountsToMax = true) { - this.constructFromBuffer(bufferData, secLoadedCountsToMax); - } - - getSplatCount() { - return this.splatCount; - } - - getMaxSplatCount() { - return this.maxSplatCount; - } - - getMinSphericalHarmonicsDegree() { - let minSphericalHarmonicsDegree = 0; - for (let i = 0; i < this.sections.length; i++) { - const section = this.sections[i]; - if (i === 0 || section.sphericalHarmonicsDegree < minSphericalHarmonicsDegree) { - minSphericalHarmonicsDegree = section.sphericalHarmonicsDegree; - } - } - return minSphericalHarmonicsDegree; - } - - getBucketIndex(section, localSplatIndex) { - let bucketIndex; - const maxSplatIndexInFullBuckets = section.fullBucketCount * section.bucketSize; - if (localSplatIndex < maxSplatIndexInFullBuckets) { - bucketIndex = Math.floor(localSplatIndex / section.bucketSize); - } else { - let bucketSplatIndex = maxSplatIndexInFullBuckets; - bucketIndex = section.fullBucketCount; - let partiallyFullBucketIndex = 0; - while (bucketSplatIndex < section.splatCount) { - let currentPartiallyFilledBucketSize = section.partiallyFilledBucketLengths[partiallyFullBucketIndex]; - if (localSplatIndex >= bucketSplatIndex && localSplatIndex < bucketSplatIndex + currentPartiallyFilledBucketSize) { - break; - } - bucketSplatIndex += currentPartiallyFilledBucketSize; - bucketIndex++; - partiallyFullBucketIndex++; - } - } - return bucketIndex; - } - - getSplatCenter(globalSplatIndex, outCenter, transform) { - const sectionIndex = this.globalSplatIndexToSectionMap[globalSplatIndex]; - const section = this.sections[sectionIndex]; - const localSplatIndex = globalSplatIndex - section.splatCountOffset; - - const srcSplatCentersBase = section.bytesPerSplat * localSplatIndex; - const dataView = new DataView(this.bufferData, section.dataBase + srcSplatCentersBase); - - const x = dataViewFloatForCompressionLevel(dataView, 0, this.compressionLevel); - const y = dataViewFloatForCompressionLevel(dataView, 1, this.compressionLevel); - const z = dataViewFloatForCompressionLevel(dataView, 2, this.compressionLevel); - if (this.compressionLevel >= 1) { - const bucketIndex = this.getBucketIndex(section, localSplatIndex); - const bucketBase = bucketIndex * SplatBuffer.BucketStorageSizeFloats; - const sf = section.compressionScaleFactor; - const sr = section.compressionScaleRange; - outCenter.x = (x - sr) * sf + section.bucketArray[bucketBase]; - outCenter.y = (y - sr) * sf + section.bucketArray[bucketBase + 1]; - outCenter.z = (z - sr) * sf + section.bucketArray[bucketBase + 2]; - } else { - outCenter.x = x; - outCenter.y = y; - outCenter.z = z; - } - if (transform) outCenter.applyMatrix4(transform); - } - - getSplatScaleAndRotation = function() { - - const scaleMatrix = new THREE.Matrix4(); - const rotationMatrix = new THREE.Matrix4(); - const tempMatrix = new THREE.Matrix4(); - const tempPosition = new THREE.Vector3(); - const scale = new THREE.Vector3(); - const rotation = new THREE.Quaternion(); - - return function(index, outScale, outRotation, transform, scaleOverride) { - const sectionIndex = this.globalSplatIndexToSectionMap[index]; - const section = this.sections[sectionIndex]; - const localSplatIndex = index - section.splatCountOffset; - - const srcSplatScalesBase = section.bytesPerSplat * localSplatIndex + - SplatBuffer.CompressionLevels[this.compressionLevel].ScaleOffsetBytes; - - const dataView = new DataView(this.bufferData, section.dataBase + srcSplatScalesBase); - - scale.set(toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 0, this.compressionLevel), this.compressionLevel), - toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 1, this.compressionLevel), this.compressionLevel), - toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 2, this.compressionLevel), this.compressionLevel)); - if (scaleOverride) { - if (scaleOverride.x !== undefined) scale.x = scaleOverride.x; - if (scaleOverride.y !== undefined) scale.y = scaleOverride.y; - if (scaleOverride.z !== undefined) scale.z = scaleOverride.z; - } - - rotation.set(toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 4, this.compressionLevel), this.compressionLevel), - toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 5, this.compressionLevel), this.compressionLevel), - toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 6, this.compressionLevel), this.compressionLevel), - toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 3, this.compressionLevel), this.compressionLevel)); - - if (transform) { - scaleMatrix.makeScale(scale.x, scale.y, scale.z); - rotationMatrix.makeRotationFromQuaternion(rotation); - tempMatrix.copy(scaleMatrix).multiply(rotationMatrix).multiply(transform); - tempMatrix.decompose(tempPosition, outRotation, outScale); - } else { - outScale.copy(scale); - outRotation.copy(rotation); - } - }; - - }(); - - getSplatColor(globalSplatIndex, outColor) { - const sectionIndex = this.globalSplatIndexToSectionMap[globalSplatIndex]; - const section = this.sections[sectionIndex]; - const localSplatIndex = globalSplatIndex - section.splatCountOffset; - - const srcSplatColorsBase = section.bytesPerSplat * localSplatIndex + - SplatBuffer.CompressionLevels[this.compressionLevel].ColorOffsetBytes; - const splatColorsArray = new Uint8Array(this.bufferData, section.dataBase + srcSplatColorsBase, 4); - - outColor.set(splatColorsArray[0], splatColorsArray[1], - splatColorsArray[2], splatColorsArray[3]); - } - - fillSplatCenterArray(outCenterArray, transform, srcFrom, srcTo, destFrom) { - const splatCount = this.splatCount; - - srcFrom = srcFrom || 0; - srcTo = srcTo || splatCount - 1; - if (destFrom === undefined) destFrom = srcFrom; - - const center = new THREE.Vector3(); - for (let i = srcFrom; i <= srcTo; i++) { - const sectionIndex = this.globalSplatIndexToSectionMap[i]; - const section = this.sections[sectionIndex]; - const localSplatIndex = i - section.splatCountOffset; - const centerDestBase = (i - srcFrom + destFrom) * SplatBuffer.CenterComponentCount; - - const srcSplatCentersBase = section.bytesPerSplat * localSplatIndex; - const dataView = new DataView(this.bufferData, section.dataBase + srcSplatCentersBase); - - const x = dataViewFloatForCompressionLevel(dataView, 0, this.compressionLevel); - const y = dataViewFloatForCompressionLevel(dataView, 1, this.compressionLevel); - const z = dataViewFloatForCompressionLevel(dataView, 2, this.compressionLevel); - if (this.compressionLevel >= 1) { - const bucketIndex = this.getBucketIndex(section, localSplatIndex); - const bucketBase = bucketIndex * SplatBuffer.BucketStorageSizeFloats; - const sf = section.compressionScaleFactor; - const sr = section.compressionScaleRange; - center.x = (x - sr) * sf + section.bucketArray[bucketBase]; - center.y = (y - sr) * sf + section.bucketArray[bucketBase + 1]; - center.z = (z - sr) * sf + section.bucketArray[bucketBase + 2]; - } else { - center.x = x; - center.y = y; - center.z = z; - } - if (transform) { - center.applyMatrix4(transform); - } - outCenterArray[centerDestBase] = center.x; - outCenterArray[centerDestBase + 1] = center.y; - outCenterArray[centerDestBase + 2] = center.z; - } - } - - fillSplatScaleRotationArray = function() { - - const scaleMatrix = new THREE.Matrix4(); - const rotationMatrix = new THREE.Matrix4(); - const tempMatrix = new THREE.Matrix4(); - const scale = new THREE.Vector3(); - const rotation = new THREE.Quaternion(); - const tempPosition = new THREE.Vector3(); - - const ensurePositiveW = (quaternion) => { - const flip = quaternion.w < 0 ? -1 : 1; - quaternion.x *= flip; - quaternion.y *= flip; - quaternion.z *= flip; - quaternion.w *= flip; - }; - - return function(outScaleArray, outRotationArray, transform, srcFrom, srcTo, destFrom, - desiredOutputCompressionLevel, scaleOverride) { - const splatCount = this.splatCount; - - srcFrom = srcFrom || 0; - srcTo = srcTo || splatCount - 1; - if (destFrom === undefined) destFrom = srcFrom; - - const outputConversion = (value, srcCompressionLevel) => { - if (srcCompressionLevel === undefined) srcCompressionLevel = this.compressionLevel; - return convertBetweenCompressionLevels(value, srcCompressionLevel, desiredOutputCompressionLevel); - }; - - for (let i = srcFrom; i <= srcTo; i++) { - const sectionIndex = this.globalSplatIndexToSectionMap[i]; - const section = this.sections[sectionIndex]; - const localSplatIndex = i - section.splatCountOffset; - - const srcSplatScalesBase = section.bytesPerSplat * localSplatIndex + - SplatBuffer.CompressionLevels[this.compressionLevel].ScaleOffsetBytes; - - const scaleDestBase = (i - srcFrom + destFrom) * SplatBuffer.ScaleComponentCount; - const rotationDestBase = (i - srcFrom + destFrom) * SplatBuffer.RotationComponentCount; - const dataView = new DataView(this.bufferData, section.dataBase + srcSplatScalesBase); - - const srcScaleX = (scaleOverride && scaleOverride.x !== undefined) ? scaleOverride.x : - dataViewFloatForCompressionLevel(dataView, 0, this.compressionLevel); - const srcScaleY = (scaleOverride && scaleOverride.y !== undefined) ? scaleOverride.y : - dataViewFloatForCompressionLevel(dataView, 1, this.compressionLevel); - const srcScaleZ = (scaleOverride && scaleOverride.z !== undefined) ? scaleOverride.z : - dataViewFloatForCompressionLevel(dataView, 2, this.compressionLevel); - - const srcRotationW = dataViewFloatForCompressionLevel(dataView, 3, this.compressionLevel); - const srcRotationX = dataViewFloatForCompressionLevel(dataView, 4, this.compressionLevel); - const srcRotationY = dataViewFloatForCompressionLevel(dataView, 5, this.compressionLevel); - const srcRotationZ = dataViewFloatForCompressionLevel(dataView, 6, this.compressionLevel); - - scale.set(toUncompressedFloat(srcScaleX, this.compressionLevel), - toUncompressedFloat(srcScaleY, this.compressionLevel), - toUncompressedFloat(srcScaleZ, this.compressionLevel)); - - rotation.set(toUncompressedFloat(srcRotationX, this.compressionLevel), - toUncompressedFloat(srcRotationY, this.compressionLevel), - toUncompressedFloat(srcRotationZ, this.compressionLevel), - toUncompressedFloat(srcRotationW, this.compressionLevel)).normalize(); - - if (transform) { - tempPosition.set(0, 0, 0); - scaleMatrix.makeScale(scale.x, scale.y, scale.z); - rotationMatrix.makeRotationFromQuaternion(rotation); - tempMatrix.identity().premultiply(scaleMatrix).premultiply(rotationMatrix); - tempMatrix.premultiply(transform); - tempMatrix.decompose(tempPosition, rotation, scale); - rotation.normalize(); - } - - ensurePositiveW(rotation); - - if (outScaleArray) { - outScaleArray[scaleDestBase] = outputConversion(scale.x, 0); - outScaleArray[scaleDestBase + 1] = outputConversion(scale.y, 0); - outScaleArray[scaleDestBase + 2] = outputConversion(scale.z, 0); - } - - if (outRotationArray) { - outRotationArray[rotationDestBase] = outputConversion(rotation.x, 0); - outRotationArray[rotationDestBase + 1] = outputConversion(rotation.y, 0); - outRotationArray[rotationDestBase + 2] = outputConversion(rotation.z, 0); - outRotationArray[rotationDestBase + 3] = outputConversion(rotation.w, 0); - } - } - }; - }(); - - static computeCovariance = function() { - - const tempMatrix4 = new THREE.Matrix4(); - const scaleMatrix = new THREE.Matrix3(); - const rotationMatrix = new THREE.Matrix3(); - const covarianceMatrix = new THREE.Matrix3(); - const transformedCovariance = new THREE.Matrix3(); - const transform3x3 = new THREE.Matrix3(); - const transform3x3Transpose = new THREE.Matrix3(); - - return function(scale, rotation, transform, outCovariance, outOffset = 0, desiredOutputCompressionLevel) { - - tempMatrix4.makeScale(scale.x, scale.y, scale.z); - scaleMatrix.setFromMatrix4(tempMatrix4); - - tempMatrix4.makeRotationFromQuaternion(rotation); - rotationMatrix.setFromMatrix4(tempMatrix4); - - covarianceMatrix.copy(rotationMatrix).multiply(scaleMatrix); - transformedCovariance.copy(covarianceMatrix).transpose().premultiply(covarianceMatrix); - - if (transform) { - transform3x3.setFromMatrix4(transform); - transform3x3Transpose.copy(transform3x3).transpose(); - transformedCovariance.multiply(transform3x3Transpose); - transformedCovariance.premultiply(transform3x3); - } - - if (desiredOutputCompressionLevel >= 1) { - outCovariance[outOffset] = toHalfFloat(transformedCovariance.elements[0]); - outCovariance[outOffset + 1] = toHalfFloat(transformedCovariance.elements[3]); - outCovariance[outOffset + 2] = toHalfFloat(transformedCovariance.elements[6]); - outCovariance[outOffset + 3] = toHalfFloat(transformedCovariance.elements[4]); - outCovariance[outOffset + 4] = toHalfFloat(transformedCovariance.elements[7]); - outCovariance[outOffset + 5] = toHalfFloat(transformedCovariance.elements[8]); - } else { - outCovariance[outOffset] = transformedCovariance.elements[0]; - outCovariance[outOffset + 1] = transformedCovariance.elements[3]; - outCovariance[outOffset + 2] = transformedCovariance.elements[6]; - outCovariance[outOffset + 3] = transformedCovariance.elements[4]; - outCovariance[outOffset + 4] = transformedCovariance.elements[7]; - outCovariance[outOffset + 5] = transformedCovariance.elements[8]; - } - - }; - - }(); - - fillSplatCovarianceArray(covarianceArray, transform, srcFrom, srcTo, destFrom, desiredOutputCompressionLevel) { - const splatCount = this.splatCount; - - const scale = new THREE.Vector3(); - const rotation = new THREE.Quaternion(); - - srcFrom = srcFrom || 0; - srcTo = srcTo || splatCount - 1; - if (destFrom === undefined) destFrom = srcFrom; - - for (let i = srcFrom; i <= srcTo; i++) { - const sectionIndex = this.globalSplatIndexToSectionMap[i]; - const section = this.sections[sectionIndex]; - const localSplatIndex = i - section.splatCountOffset; - - const covarianceDestBase = (i - srcFrom + destFrom) * SplatBuffer.CovarianceComponentCount; - const srcSplatScalesBase = section.bytesPerSplat * localSplatIndex + - SplatBuffer.CompressionLevels[this.compressionLevel].ScaleOffsetBytes; - - const dataView = new DataView(this.bufferData, section.dataBase + srcSplatScalesBase); - - scale.set(toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 0, this.compressionLevel), this.compressionLevel), - toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 1, this.compressionLevel), this.compressionLevel), - toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 2, this.compressionLevel), this.compressionLevel)); - - rotation.set(toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 4, this.compressionLevel), this.compressionLevel), - toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 5, this.compressionLevel), this.compressionLevel), - toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 6, this.compressionLevel), this.compressionLevel), - toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 3, this.compressionLevel), this.compressionLevel)); - - SplatBuffer.computeCovariance(scale, rotation, transform, covarianceArray, covarianceDestBase, desiredOutputCompressionLevel); - } - } - - fillSplatColorArray(outColorArray, minimumAlpha, srcFrom, srcTo, destFrom) { - const splatCount = this.splatCount; - - srcFrom = srcFrom || 0; - srcTo = srcTo || splatCount - 1; - if (destFrom === undefined) destFrom = srcFrom; - - for (let i = srcFrom; i <= srcTo; i++) { - - const sectionIndex = this.globalSplatIndexToSectionMap[i]; - const section = this.sections[sectionIndex]; - const localSplatIndex = i - section.splatCountOffset; - - const colorDestBase = (i - srcFrom + destFrom) * SplatBuffer.ColorComponentCount; - const srcSplatColorsBase = section.bytesPerSplat * localSplatIndex + - SplatBuffer.CompressionLevels[this.compressionLevel].ColorOffsetBytes; - - const dataView = new Uint8Array(this.bufferData, section.dataBase + srcSplatColorsBase); - - let alpha = dataView[3]; - alpha = (alpha >= minimumAlpha) ? alpha : 0; - - outColorArray[colorDestBase] = dataView[0]; - outColorArray[colorDestBase + 1] = dataView[1]; - outColorArray[colorDestBase + 2] = dataView[2]; - outColorArray[colorDestBase + 3] = alpha; - } - } - - fillSphericalHarmonicsArray = function() { - - const sphericalHarmonicVectors = []; - for (let i = 0; i < 15; i++) { - sphericalHarmonicVectors[i] = new THREE.Vector3(); - } - - const tempMatrix3 = new THREE.Matrix3(); - const tempMatrix4 = new THREE.Matrix4(); - - const tempTranslation = new THREE.Vector3(); - const tempScale = new THREE.Vector3(); - const tempRotation = new THREE.Quaternion(); - - const sh11 = []; - const sh12 = []; - const sh13 = []; - - const sh21 = []; - const sh22 = []; - const sh23 = []; - const sh24 = []; - const sh25 = []; - - const shIn1 = []; - const shIn2 = []; - const shIn3 = []; - const shIn4 = []; - const shIn5 = []; - - const shOut1 = []; - const shOut2 = []; - const shOut3 = []; - const shOut4 = []; - const shOut5 = []; - - const noop = (v) => v; - - const set3 = (array, val1, val2, val3) => { - array[0] = val1; - array[1] = val2; - array[2] = val3; - }; - - const set3FromArray = (array, srcDestView, stride, srcBase, compressionLevel) => { - array[0] = dataViewFloatForCompressionLevel(srcDestView, srcBase, compressionLevel, true); - array[1] = dataViewFloatForCompressionLevel(srcDestView, srcBase + stride, compressionLevel, true); - array[2] = dataViewFloatForCompressionLevel(srcDestView, srcBase + stride + stride, compressionLevel, true); - }; - - const copy3 = (srcArray, destArray) => { - destArray[0] = srcArray[0]; - destArray[1] = srcArray[1]; - destArray[2] = srcArray[2]; - }; - - const setOutput3 = (srcArray, destArray, destBase, conversionFunc) => { - destArray[destBase] = conversionFunc(srcArray[0]); - destArray[destBase + 1] = conversionFunc(srcArray[1]); - destArray[destBase + 2] = conversionFunc(srcArray[2]); - }; - - const toUncompressedFloatArray3 = (src, dest, compressionLevel, range8BitMin, range8BitMax) => { - dest[0] = toUncompressedFloat(src[0], compressionLevel, true, range8BitMin, range8BitMax); - dest[1] = toUncompressedFloat(src[1], compressionLevel, true, range8BitMin, range8BitMax); - dest[2] = toUncompressedFloat(src[2], compressionLevel, true, range8BitMin, range8BitMax); - return dest; - }; - - return function(outSphericalHarmonicsArray, outSphericalHarmonicsDegree, transform, - srcFrom, srcTo, destFrom, desiredOutputCompressionLevel) { - const splatCount = this.splatCount; - - srcFrom = srcFrom || 0; - srcTo = srcTo || splatCount - 1; - if (destFrom === undefined) destFrom = srcFrom; - - if (transform && outSphericalHarmonicsDegree >= 1) { - tempMatrix4.copy(transform); - tempMatrix4.decompose(tempTranslation, tempRotation, tempScale); - tempRotation.normalize(); - tempMatrix4.makeRotationFromQuaternion(tempRotation); - tempMatrix3.setFromMatrix4(tempMatrix4); - set3(sh11, tempMatrix3.elements[4], -tempMatrix3.elements[7], tempMatrix3.elements[1]); - set3(sh12, -tempMatrix3.elements[5], tempMatrix3.elements[8], -tempMatrix3.elements[2]); - set3(sh13, tempMatrix3.elements[3], -tempMatrix3.elements[6], tempMatrix3.elements[0]); - } - - const localFromHalfFloatToUint8 = (v) => { - return fromHalfFloatToUint8(v, this.minSphericalHarmonicsCoeff, this.maxSphericalHarmonicsCoeff); - }; - - const localToUint8 = (v) => { - return toUint8(v, this.minSphericalHarmonicsCoeff, this.maxSphericalHarmonicsCoeff); - }; - - for (let i = srcFrom; i <= srcTo; i++) { - - const sectionIndex = this.globalSplatIndexToSectionMap[i]; - const section = this.sections[sectionIndex]; - outSphericalHarmonicsDegree = Math.min(outSphericalHarmonicsDegree, section.sphericalHarmonicsDegree); - const outSphericalHarmonicsComponentsCount = getSphericalHarmonicsComponentCountForDegree(outSphericalHarmonicsDegree); - - const localSplatIndex = i - section.splatCountOffset; - - const srcSplatSHBase = section.bytesPerSplat * localSplatIndex + - SplatBuffer.CompressionLevels[this.compressionLevel].SphericalHarmonicsOffsetBytes; - - const dataView = new DataView(this.bufferData, section.dataBase + srcSplatSHBase); - - const shDestBase = (i - srcFrom + destFrom) * outSphericalHarmonicsComponentsCount; - - let compressionLevelForOutputConversion = transform ? 0 : this.compressionLevel; - let outputConversionFunc = noop; - if (compressionLevelForOutputConversion !== desiredOutputCompressionLevel) { - if (compressionLevelForOutputConversion === 1) { - if (desiredOutputCompressionLevel === 0) outputConversionFunc = fromHalfFloat$1; - else if (desiredOutputCompressionLevel == 2) outputConversionFunc = localFromHalfFloatToUint8; - } else if (compressionLevelForOutputConversion === 0) { - if (desiredOutputCompressionLevel === 1) outputConversionFunc = toHalfFloat; - else if (desiredOutputCompressionLevel == 2) outputConversionFunc = localToUint8; - } - } - - const minShCoeff = this.minSphericalHarmonicsCoeff; - const maxShCoeff = this.maxSphericalHarmonicsCoeff; - - if (outSphericalHarmonicsDegree >= 1) { - - set3FromArray(shIn1, dataView, 3, 0, this.compressionLevel); - set3FromArray(shIn2, dataView, 3, 1, this.compressionLevel); - set3FromArray(shIn3, dataView, 3, 2, this.compressionLevel); - - if (transform) { - toUncompressedFloatArray3(shIn1, shIn1, this.compressionLevel, minShCoeff, maxShCoeff); - toUncompressedFloatArray3(shIn2, shIn2, this.compressionLevel, minShCoeff, maxShCoeff); - toUncompressedFloatArray3(shIn3, shIn3, this.compressionLevel, minShCoeff, maxShCoeff); - SplatBuffer.rotateSphericalHarmonics3(shIn1, shIn2, shIn3, sh11, sh12, sh13, shOut1, shOut2, shOut3); - } else { - copy3(shIn1, shOut1); - copy3(shIn2, shOut2); - copy3(shIn3, shOut3); - } - - setOutput3(shOut1, outSphericalHarmonicsArray, shDestBase, outputConversionFunc); - setOutput3(shOut2, outSphericalHarmonicsArray, shDestBase + 3, outputConversionFunc); - setOutput3(shOut3, outSphericalHarmonicsArray, shDestBase + 6, outputConversionFunc); - - if (outSphericalHarmonicsDegree >= 2) { - - set3FromArray(shIn1, dataView, 5, 9, this.compressionLevel); - set3FromArray(shIn2, dataView, 5, 10, this.compressionLevel); - set3FromArray(shIn3, dataView, 5, 11, this.compressionLevel); - set3FromArray(shIn4, dataView, 5, 12, this.compressionLevel); - set3FromArray(shIn5, dataView, 5, 13, this.compressionLevel); - - if (transform) { - toUncompressedFloatArray3(shIn1, shIn1, this.compressionLevel, minShCoeff, maxShCoeff); - toUncompressedFloatArray3(shIn2, shIn2, this.compressionLevel, minShCoeff, maxShCoeff); - toUncompressedFloatArray3(shIn3, shIn3, this.compressionLevel, minShCoeff, maxShCoeff); - toUncompressedFloatArray3(shIn4, shIn4, this.compressionLevel, minShCoeff, maxShCoeff); - toUncompressedFloatArray3(shIn5, shIn5, this.compressionLevel, minShCoeff, maxShCoeff); - SplatBuffer.rotateSphericalHarmonics5(shIn1, shIn2, shIn3, shIn4, shIn5, - sh11, sh12, sh13, sh21, sh22, sh23, sh24, sh25, - shOut1, shOut2, shOut3, shOut4, shOut5); - } else { - copy3(shIn1, shOut1); - copy3(shIn2, shOut2); - copy3(shIn3, shOut3); - copy3(shIn4, shOut4); - copy3(shIn5, shOut5); - } - - setOutput3(shOut1, outSphericalHarmonicsArray, shDestBase + 9, outputConversionFunc); - setOutput3(shOut2, outSphericalHarmonicsArray, shDestBase + 12, outputConversionFunc); - setOutput3(shOut3, outSphericalHarmonicsArray, shDestBase + 15, outputConversionFunc); - setOutput3(shOut4, outSphericalHarmonicsArray, shDestBase + 18, outputConversionFunc); - setOutput3(shOut5, outSphericalHarmonicsArray, shDestBase + 21, outputConversionFunc); - } - } - } - }; - - }(); - - static dot3 = (v1, v2, v3, transformRow, outArray) => { - outArray[0] = outArray[1] = outArray[2] = 0; - const t0 = transformRow[0]; - const t1 = transformRow[1]; - const t2 = transformRow[2]; - SplatBuffer.addInto3(v1[0] * t0, v1[1] * t0, v1[2] * t0, outArray); - SplatBuffer.addInto3(v2[0] * t1, v2[1] * t1, v2[2] * t1, outArray); - SplatBuffer.addInto3(v3[0] * t2, v3[1] * t2, v3[2] * t2, outArray); - }; - - static addInto3 = (val1, val2, val3, destArray) => { - destArray[0] = destArray[0] + val1; - destArray[1] = destArray[1] + val2; - destArray[2] = destArray[2] + val3; - }; - - static dot5 = (v1, v2, v3, v4, v5, transformRow, outArray) => { - outArray[0] = outArray[1] = outArray[2] = 0; - const t0 = transformRow[0]; - const t1 = transformRow[1]; - const t2 = transformRow[2]; - const t3 = transformRow[3]; - const t4 = transformRow[4]; - SplatBuffer.addInto3(v1[0] * t0, v1[1] * t0, v1[2] * t0, outArray); - SplatBuffer.addInto3(v2[0] * t1, v2[1] * t1, v2[2] * t1, outArray); - SplatBuffer.addInto3(v3[0] * t2, v3[1] * t2, v3[2] * t2, outArray); - SplatBuffer.addInto3(v4[0] * t3, v4[1] * t3, v4[2] * t3, outArray); - SplatBuffer.addInto3(v5[0] * t4, v5[1] * t4, v5[2] * t4, outArray); - }; - - static rotateSphericalHarmonics3 = (in1, in2, in3, tsh11, tsh12, tsh13, out1, out2, out3) => { - SplatBuffer.dot3(in1, in2, in3, tsh11, out1); - SplatBuffer.dot3(in1, in2, in3, tsh12, out2); - SplatBuffer.dot3(in1, in2, in3, tsh13, out3); - }; - - static rotateSphericalHarmonics5 = (in1, in2, in3, in4, in5, tsh11, tsh12, tsh13, - tsh21, tsh22, tsh23, tsh24, tsh25, out1, out2, out3, out4, out5) => { - - const kSqrt0104 = Math.sqrt(1.0 / 4.0); - const kSqrt0304 = Math.sqrt(3.0 / 4.0); - const kSqrt0103 = Math.sqrt(1.0 / 3.0); - const kSqrt0403 = Math.sqrt(4.0 / 3.0); - const kSqrt0112 = Math.sqrt(1.0 / 12.0); - - tsh21[0] = kSqrt0104 * ((tsh13[2] * tsh11[0] + tsh13[0] * tsh11[2]) + (tsh11[2] * tsh13[0] + tsh11[0] * tsh13[2])); - tsh21[1] = (tsh13[1] * tsh11[0] + tsh11[1] * tsh13[0]); - tsh21[2] = kSqrt0304 * (tsh13[1] * tsh11[1] + tsh11[1] * tsh13[1]); - tsh21[3] = (tsh13[1] * tsh11[2] + tsh11[1] * tsh13[2]); - tsh21[4] = kSqrt0104 * ((tsh13[2] * tsh11[2] - tsh13[0] * tsh11[0]) + (tsh11[2] * tsh13[2] - tsh11[0] * tsh13[0])); - SplatBuffer.dot5(in1, in2, in3, in4, in5, tsh21, out1); - - tsh22[0] = kSqrt0104 * ((tsh12[2] * tsh11[0] + tsh12[0] * tsh11[2]) + (tsh11[2] * tsh12[0] + tsh11[0] * tsh12[2])); - tsh22[1] = tsh12[1] * tsh11[0] + tsh11[1] * tsh12[0]; - tsh22[2] = kSqrt0304 * (tsh12[1] * tsh11[1] + tsh11[1] * tsh12[1]); - tsh22[3] = tsh12[1] * tsh11[2] + tsh11[1] * tsh12[2]; - tsh22[4] = kSqrt0104 * ((tsh12[2] * tsh11[2] - tsh12[0] * tsh11[0]) + (tsh11[2] * tsh12[2] - tsh11[0] * tsh12[0])); - SplatBuffer.dot5(in1, in2, in3, in4, in5, tsh22, out2); - - tsh23[0] = kSqrt0103 * (tsh12[2] * tsh12[0] + tsh12[0] * tsh12[2]) + -kSqrt0112 * - ((tsh13[2] * tsh13[0] + tsh13[0] * tsh13[2]) + (tsh11[2] * tsh11[0] + tsh11[0] * tsh11[2])); - tsh23[1] = kSqrt0403 * tsh12[1] * tsh12[0] + -kSqrt0103 * (tsh13[1] * tsh13[0] + tsh11[1] * tsh11[0]); - tsh23[2] = tsh12[1] * tsh12[1] + -kSqrt0104 * (tsh13[1] * tsh13[1] + tsh11[1] * tsh11[1]); - tsh23[3] = kSqrt0403 * tsh12[1] * tsh12[2] + -kSqrt0103 * (tsh13[1] * tsh13[2] + tsh11[1] * tsh11[2]); - tsh23[4] = kSqrt0103 * (tsh12[2] * tsh12[2] - tsh12[0] * tsh12[0]) + -kSqrt0112 * - ((tsh13[2] * tsh13[2] - tsh13[0] * tsh13[0]) + (tsh11[2] * tsh11[2] - tsh11[0] * tsh11[0])); - SplatBuffer.dot5(in1, in2, in3, in4, in5, tsh23, out3); - - tsh24[0] = kSqrt0104 * ((tsh12[2] * tsh13[0] + tsh12[0] * tsh13[2]) + (tsh13[2] * tsh12[0] + tsh13[0] * tsh12[2])); - tsh24[1] = tsh12[1] * tsh13[0] + tsh13[1] * tsh12[0]; - tsh24[2] = kSqrt0304 * (tsh12[1] * tsh13[1] + tsh13[1] * tsh12[1]); - tsh24[3] = tsh12[1] * tsh13[2] + tsh13[1] * tsh12[2]; - tsh24[4] = kSqrt0104 * ((tsh12[2] * tsh13[2] - tsh12[0] * tsh13[0]) + (tsh13[2] * tsh12[2] - tsh13[0] * tsh12[0])); - SplatBuffer.dot5(in1, in2, in3, in4, in5, tsh24, out4); - - tsh25[0] = kSqrt0104 * ((tsh13[2] * tsh13[0] + tsh13[0] * tsh13[2]) - (tsh11[2] * tsh11[0] + tsh11[0] * tsh11[2])); - tsh25[1] = (tsh13[1] * tsh13[0] - tsh11[1] * tsh11[0]); - tsh25[2] = kSqrt0304 * (tsh13[1] * tsh13[1] - tsh11[1] * tsh11[1]); - tsh25[3] = (tsh13[1] * tsh13[2] - tsh11[1] * tsh11[2]); - tsh25[4] = kSqrt0104 * ((tsh13[2] * tsh13[2] - tsh13[0] * tsh13[0]) - (tsh11[2] * tsh11[2] - tsh11[0] * tsh11[0])); - SplatBuffer.dot5(in1, in2, in3, in4, in5, tsh25, out5); - }; - - static parseHeader(buffer) { - const headerArrayUint8 = new Uint8Array(buffer, 0, SplatBuffer.HeaderSizeBytes); - const headerArrayUint16 = new Uint16Array(buffer, 0, SplatBuffer.HeaderSizeBytes / 2); - const headerArrayUint32 = new Uint32Array(buffer, 0, SplatBuffer.HeaderSizeBytes / 4); - const headerArrayFloat32 = new Float32Array(buffer, 0, SplatBuffer.HeaderSizeBytes / 4); - const versionMajor = headerArrayUint8[0]; - const versionMinor = headerArrayUint8[1]; - const maxSectionCount = headerArrayUint32[1]; - const sectionCount = headerArrayUint32[2]; - const maxSplatCount = headerArrayUint32[3]; - const splatCount = headerArrayUint32[4]; - const compressionLevel = headerArrayUint16[10]; - const sceneCenter = new THREE.Vector3(headerArrayFloat32[6], headerArrayFloat32[7], headerArrayFloat32[8]); - - const minSphericalHarmonicsCoeff = headerArrayFloat32[9] || -DefaultSphericalHarmonics8BitCompressionHalfRange; - const maxSphericalHarmonicsCoeff = headerArrayFloat32[10] || DefaultSphericalHarmonics8BitCompressionHalfRange; - - return { - versionMajor, - versionMinor, - maxSectionCount, - sectionCount, - maxSplatCount, - splatCount, - compressionLevel, - sceneCenter, - minSphericalHarmonicsCoeff, - maxSphericalHarmonicsCoeff - }; - } - - static writeHeaderCountsToBuffer(sectionCount, splatCount, buffer) { - const headerArrayUint32 = new Uint32Array(buffer, 0, SplatBuffer.HeaderSizeBytes / 4); - headerArrayUint32[2] = sectionCount; - headerArrayUint32[4] = splatCount; - } - - static writeHeaderToBuffer(header, buffer) { - const headerArrayUint8 = new Uint8Array(buffer, 0, SplatBuffer.HeaderSizeBytes); - const headerArrayUint16 = new Uint16Array(buffer, 0, SplatBuffer.HeaderSizeBytes / 2); - const headerArrayUint32 = new Uint32Array(buffer, 0, SplatBuffer.HeaderSizeBytes / 4); - const headerArrayFloat32 = new Float32Array(buffer, 0, SplatBuffer.HeaderSizeBytes / 4); - headerArrayUint8[0] = header.versionMajor; - headerArrayUint8[1] = header.versionMinor; - headerArrayUint8[2] = 0; // unused for now - headerArrayUint8[3] = 0; // unused for now - headerArrayUint32[1] = header.maxSectionCount; - headerArrayUint32[2] = header.sectionCount; - headerArrayUint32[3] = header.maxSplatCount; - headerArrayUint32[4] = header.splatCount; - headerArrayUint16[10] = header.compressionLevel; - headerArrayFloat32[6] = header.sceneCenter.x; - headerArrayFloat32[7] = header.sceneCenter.y; - headerArrayFloat32[8] = header.sceneCenter.z; - headerArrayFloat32[9] = header.minSphericalHarmonicsCoeff || -DefaultSphericalHarmonics8BitCompressionHalfRange; - headerArrayFloat32[10] = header.maxSphericalHarmonicsCoeff || DefaultSphericalHarmonics8BitCompressionHalfRange; - } - - static parseSectionHeaders(header, buffer, offset = 0, secLoadedCountsToMax) { - const compressionLevel = header.compressionLevel; - - const maxSectionCount = header.maxSectionCount; - const sectionHeaderArrayUint16 = new Uint16Array(buffer, offset, maxSectionCount * SplatBuffer.SectionHeaderSizeBytes / 2); - const sectionHeaderArrayUint32 = new Uint32Array(buffer, offset, maxSectionCount * SplatBuffer.SectionHeaderSizeBytes / 4); - const sectionHeaderArrayFloat32 = new Float32Array(buffer, offset, maxSectionCount * SplatBuffer.SectionHeaderSizeBytes / 4); - - const sectionHeaders = []; - let sectionHeaderBase = 0; - let sectionHeaderBaseUint16 = sectionHeaderBase / 2; - let sectionHeaderBaseUint32 = sectionHeaderBase / 4; - let sectionBase = SplatBuffer.HeaderSizeBytes + header.maxSectionCount * SplatBuffer.SectionHeaderSizeBytes; - let splatCountOffset = 0; - for (let i = 0; i < maxSectionCount; i++) { - const maxSplatCount = sectionHeaderArrayUint32[sectionHeaderBaseUint32 + 1]; - const bucketSize = sectionHeaderArrayUint32[sectionHeaderBaseUint32 + 2]; - const bucketCount = sectionHeaderArrayUint32[sectionHeaderBaseUint32 + 3]; - const bucketBlockSize = sectionHeaderArrayFloat32[sectionHeaderBaseUint32 + 4]; - const halfBucketBlockSize = bucketBlockSize / 2.0; - const bucketStorageSizeBytes = sectionHeaderArrayUint16[sectionHeaderBaseUint16 + 10]; - const compressionScaleRange = sectionHeaderArrayUint32[sectionHeaderBaseUint32 + 6] || - SplatBuffer.CompressionLevels[compressionLevel].ScaleRange; - const fullBucketCount = sectionHeaderArrayUint32[sectionHeaderBaseUint32 + 8]; - const partiallyFilledBucketCount = sectionHeaderArrayUint32[sectionHeaderBaseUint32 + 9]; - const bucketsMetaDataSizeBytes = partiallyFilledBucketCount * 4; - const bucketsStorageSizeBytes = bucketStorageSizeBytes * bucketCount + bucketsMetaDataSizeBytes; - - const sphericalHarmonicsDegree = sectionHeaderArrayUint16[sectionHeaderBaseUint16 + 20]; - const { bytesPerSplat } = SplatBuffer.calculateComponentStorage(compressionLevel, sphericalHarmonicsDegree); - - const splatDataStorageSizeBytes = bytesPerSplat * maxSplatCount; - const storageSizeBytes = splatDataStorageSizeBytes + bucketsStorageSizeBytes; - const sectionHeader = { - bytesPerSplat: bytesPerSplat, - splatCountOffset: splatCountOffset, - splatCount: secLoadedCountsToMax ? maxSplatCount : 0, - maxSplatCount: maxSplatCount, - bucketSize: bucketSize, - bucketCount: bucketCount, - bucketBlockSize: bucketBlockSize, - halfBucketBlockSize: halfBucketBlockSize, - bucketStorageSizeBytes: bucketStorageSizeBytes, - bucketsStorageSizeBytes: bucketsStorageSizeBytes, - splatDataStorageSizeBytes: splatDataStorageSizeBytes, - storageSizeBytes: storageSizeBytes, - compressionScaleRange: compressionScaleRange, - compressionScaleFactor: halfBucketBlockSize / compressionScaleRange, - base: sectionBase, - bucketsBase: sectionBase + bucketsMetaDataSizeBytes, - dataBase: sectionBase + bucketsStorageSizeBytes, - fullBucketCount: fullBucketCount, - partiallyFilledBucketCount: partiallyFilledBucketCount, - sphericalHarmonicsDegree: sphericalHarmonicsDegree - }; - sectionHeaders[i] = sectionHeader; - sectionBase += storageSizeBytes; - sectionHeaderBase += SplatBuffer.SectionHeaderSizeBytes; - sectionHeaderBaseUint16 = sectionHeaderBase / 2; - sectionHeaderBaseUint32 = sectionHeaderBase / 4; - splatCountOffset += maxSplatCount; - } - - return sectionHeaders; - } - - - static writeSectionHeaderToBuffer(sectionHeader, compressionLevel, buffer, offset = 0) { - const sectionHeadeArrayUint16 = new Uint16Array(buffer, offset, SplatBuffer.SectionHeaderSizeBytes / 2); - const sectionHeadeArrayUint32 = new Uint32Array(buffer, offset, SplatBuffer.SectionHeaderSizeBytes / 4); - const sectionHeadeArrayFloat32 = new Float32Array(buffer, offset, SplatBuffer.SectionHeaderSizeBytes / 4); - - sectionHeadeArrayUint32[0] = sectionHeader.splatCount; - sectionHeadeArrayUint32[1] = sectionHeader.maxSplatCount; - sectionHeadeArrayUint32[2] = compressionLevel >= 1 ? sectionHeader.bucketSize : 0; - sectionHeadeArrayUint32[3] = compressionLevel >= 1 ? sectionHeader.bucketCount : 0; - sectionHeadeArrayFloat32[4] = compressionLevel >= 1 ? sectionHeader.bucketBlockSize : 0.0; - sectionHeadeArrayUint16[10] = compressionLevel >= 1 ? SplatBuffer.BucketStorageSizeBytes : 0; - sectionHeadeArrayUint32[6] = compressionLevel >= 1 ? sectionHeader.compressionScaleRange : 0; - sectionHeadeArrayUint32[7] = sectionHeader.storageSizeBytes; - sectionHeadeArrayUint32[8] = compressionLevel >= 1 ? sectionHeader.fullBucketCount : 0; - sectionHeadeArrayUint32[9] = compressionLevel >= 1 ? sectionHeader.partiallyFilledBucketCount : 0; - sectionHeadeArrayUint16[20] = sectionHeader.sphericalHarmonicsDegree; - - } - - static writeSectionHeaderSplatCountToBuffer(splatCount, buffer, offset = 0) { - const sectionHeadeArrayUint32 = new Uint32Array(buffer, offset, SplatBuffer.SectionHeaderSizeBytes / 4); - sectionHeadeArrayUint32[0] = splatCount; - } - - constructFromBuffer(bufferData, secLoadedCountsToMax) { - this.bufferData = bufferData; - - this.globalSplatIndexToLocalSplatIndexMap = []; - this.globalSplatIndexToSectionMap = []; - - const header = SplatBuffer.parseHeader(this.bufferData); - this.versionMajor = header.versionMajor; - this.versionMinor = header.versionMinor; - this.maxSectionCount = header.maxSectionCount; - this.sectionCount = secLoadedCountsToMax ? header.maxSectionCount : 0; - this.maxSplatCount = header.maxSplatCount; - this.splatCount = secLoadedCountsToMax ? header.maxSplatCount : 0; - this.compressionLevel = header.compressionLevel; - this.sceneCenter = new THREE.Vector3().copy(header.sceneCenter); - this.minSphericalHarmonicsCoeff = header.minSphericalHarmonicsCoeff; - this.maxSphericalHarmonicsCoeff = header.maxSphericalHarmonicsCoeff; - - this.sections = SplatBuffer.parseSectionHeaders(header, this.bufferData, SplatBuffer.HeaderSizeBytes, secLoadedCountsToMax); - - this.linkBufferArrays(); - this.buildMaps(); - } - - static calculateComponentStorage(compressionLevel, sphericalHarmonicsDegree) { - const bytesPerCenter = SplatBuffer.CompressionLevels[compressionLevel].BytesPerCenter; - const bytesPerScale = SplatBuffer.CompressionLevels[compressionLevel].BytesPerScale; - const bytesPerRotation = SplatBuffer.CompressionLevels[compressionLevel].BytesPerRotation; - const bytesPerColor = SplatBuffer.CompressionLevels[compressionLevel].BytesPerColor; - const sphericalHarmonicsComponentsPerSplat = getSphericalHarmonicsComponentCountForDegree(sphericalHarmonicsDegree); - const sphericalHarmonicsBytesPerSplat = SplatBuffer.CompressionLevels[compressionLevel].BytesPerSphericalHarmonicsComponent * - sphericalHarmonicsComponentsPerSplat; - const bytesPerSplat = bytesPerCenter + bytesPerScale + bytesPerRotation + - bytesPerColor + sphericalHarmonicsBytesPerSplat; - return { - bytesPerCenter, - bytesPerScale, - bytesPerRotation, - bytesPerColor, - sphericalHarmonicsComponentsPerSplat, - sphericalHarmonicsBytesPerSplat, - bytesPerSplat - }; - } - - linkBufferArrays() { - for (let i = 0; i < this.maxSectionCount; i++) { - const section = this.sections[i]; - section.bucketArray = new Float32Array(this.bufferData, section.bucketsBase, - section.bucketCount * SplatBuffer.BucketStorageSizeFloats); - if (section.partiallyFilledBucketCount > 0) { - section.partiallyFilledBucketLengths = new Uint32Array(this.bufferData, section.base, - section.partiallyFilledBucketCount); - } - } - } - - buildMaps() { - let cumulativeSplatCount = 0; - for (let i = 0; i < this.maxSectionCount; i++) { - const section = this.sections[i]; - for (let j = 0; j < section.maxSplatCount; j++) { - const globalSplatIndex = cumulativeSplatCount + j; - this.globalSplatIndexToLocalSplatIndexMap[globalSplatIndex] = j; - this.globalSplatIndexToSectionMap[globalSplatIndex] = i; - } - cumulativeSplatCount += section.maxSplatCount; - } - } - - updateLoadedCounts(newSectionCount, newSplatCount) { - SplatBuffer.writeHeaderCountsToBuffer(newSectionCount, newSplatCount, this.bufferData); - this.sectionCount = newSectionCount; - this.splatCount = newSplatCount; - } - - updateSectionLoadedCounts(sectionIndex, newSplatCount) { - const sectionHeaderOffset = SplatBuffer.HeaderSizeBytes + SplatBuffer.SectionHeaderSizeBytes * sectionIndex; - SplatBuffer.writeSectionHeaderSplatCountToBuffer(newSplatCount, this.bufferData, sectionHeaderOffset); - this.sections[sectionIndex].splatCount = newSplatCount; - } - - static writeSplatDataToSectionBuffer = function() { - - const tempCenterBuffer = new ArrayBuffer(12); - const tempScaleBuffer = new ArrayBuffer(12); - const tempRotationBuffer = new ArrayBuffer(16); - const tempColorBuffer = new ArrayBuffer(4); - const tempSHBuffer = new ArrayBuffer(256); - const tempRot = new THREE.Quaternion(); - const tempScale = new THREE.Vector3(); - const bucketCenterDelta = new THREE.Vector3(); - - const { - X: OFFSET_X, Y: OFFSET_Y, Z: OFFSET_Z, - SCALE0: OFFSET_SCALE0, SCALE1: OFFSET_SCALE1, SCALE2: OFFSET_SCALE2, - ROTATION0: OFFSET_ROT0, ROTATION1: OFFSET_ROT1, ROTATION2: OFFSET_ROT2, ROTATION3: OFFSET_ROT3, - FDC0: OFFSET_FDC0, FDC1: OFFSET_FDC1, FDC2: OFFSET_FDC2, OPACITY: OFFSET_OPACITY, - FRC0: OFFSET_FRC0, FRC9: OFFSET_FRC9, - } = UncompressedSplatArray.OFFSET; - - const compressPositionOffset = (v, compressionScaleFactor, compressionScaleRange) => { - const doubleCompressionScaleRange = compressionScaleRange * 2 + 1; - v = Math.round(v * compressionScaleFactor) + compressionScaleRange; - return clamp(v, 0, doubleCompressionScaleRange); - }; - - return function(targetSplat, sectionBuffer, bufferOffset, compressionLevel, sphericalHarmonicsDegree, - bucketCenter, compressionScaleFactor, compressionScaleRange, - minSphericalHarmonicsCoeff = -DefaultSphericalHarmonics8BitCompressionHalfRange, - maxSphericalHarmonicsCoeff = DefaultSphericalHarmonics8BitCompressionHalfRange) { - - const sphericalHarmonicsComponentsPerSplat = getSphericalHarmonicsComponentCountForDegree(sphericalHarmonicsDegree); - const bytesPerCenter = SplatBuffer.CompressionLevels[compressionLevel].BytesPerCenter; - const bytesPerScale = SplatBuffer.CompressionLevels[compressionLevel].BytesPerScale; - const bytesPerRotation = SplatBuffer.CompressionLevels[compressionLevel].BytesPerRotation; - const bytesPerColor = SplatBuffer.CompressionLevels[compressionLevel].BytesPerColor; - - const centerBase = bufferOffset; - const scaleBase = centerBase + bytesPerCenter; - const rotationBase = scaleBase + bytesPerScale; - const colorBase = rotationBase + bytesPerRotation; - const sphericalHarmonicsBase = colorBase + bytesPerColor; - - if (targetSplat[OFFSET_ROT0] !== undefined) { - tempRot.set(targetSplat[OFFSET_ROT0], targetSplat[OFFSET_ROT1], targetSplat[OFFSET_ROT2], targetSplat[OFFSET_ROT3]); - tempRot.normalize(); - } else { - tempRot.set(1.0, 0.0, 0.0, 0.0); - } - - if (targetSplat[OFFSET_SCALE0] !== undefined) { - tempScale.set(targetSplat[OFFSET_SCALE0] || 0, - targetSplat[OFFSET_SCALE1] || 0, - targetSplat[OFFSET_SCALE2] || 0); - } else { - tempScale.set(0, 0, 0); - } - - if (compressionLevel === 0) { - const center = new Float32Array(sectionBuffer, centerBase, SplatBuffer.CenterComponentCount); - const rot = new Float32Array(sectionBuffer, rotationBase, SplatBuffer.RotationComponentCount); - const scale = new Float32Array(sectionBuffer, scaleBase, SplatBuffer.ScaleComponentCount); - - rot.set([tempRot.x, tempRot.y, tempRot.z, tempRot.w]); - scale.set([tempScale.x, tempScale.y, tempScale.z]); - center.set([targetSplat[OFFSET_X], targetSplat[OFFSET_Y], targetSplat[OFFSET_Z]]); - - if (sphericalHarmonicsDegree > 0) { - const shOut = new Float32Array(sectionBuffer, sphericalHarmonicsBase, sphericalHarmonicsComponentsPerSplat); - if (sphericalHarmonicsDegree >= 1) { - for (let s = 0; s < 9; s++) shOut[s] = targetSplat[OFFSET_FRC0 + s] || 0; - if (sphericalHarmonicsDegree >= 2) { - for (let s = 0; s < 15; s++) shOut[s + 9] = targetSplat[OFFSET_FRC9 + s] || 0; - } - } - } - } else { - const center = new Uint16Array(tempCenterBuffer, 0, SplatBuffer.CenterComponentCount); - const rot = new Uint16Array(tempRotationBuffer, 0, SplatBuffer.RotationComponentCount); - const scale = new Uint16Array(tempScaleBuffer, 0, SplatBuffer.ScaleComponentCount); - - rot.set([toHalfFloat(tempRot.x), toHalfFloat(tempRot.y), toHalfFloat(tempRot.z), toHalfFloat(tempRot.w)]); - scale.set([toHalfFloat(tempScale.x), toHalfFloat(tempScale.y), toHalfFloat(tempScale.z)]); - - bucketCenterDelta.set(targetSplat[OFFSET_X], targetSplat[OFFSET_Y], targetSplat[OFFSET_Z]).sub(bucketCenter); - bucketCenterDelta.x = compressPositionOffset(bucketCenterDelta.x, compressionScaleFactor, compressionScaleRange); - bucketCenterDelta.y = compressPositionOffset(bucketCenterDelta.y, compressionScaleFactor, compressionScaleRange); - bucketCenterDelta.z = compressPositionOffset(bucketCenterDelta.z, compressionScaleFactor, compressionScaleRange); - center.set([bucketCenterDelta.x, bucketCenterDelta.y, bucketCenterDelta.z]); - - if (sphericalHarmonicsDegree > 0) { - const SHArrayType = compressionLevel === 1 ? Uint16Array : Uint8Array; - const bytesPerSHComponent = compressionLevel === 1 ? 2 : 1; - const shOut = new SHArrayType(tempSHBuffer, 0, sphericalHarmonicsComponentsPerSplat); - if (sphericalHarmonicsDegree >= 1) { - for (let s = 0; s < 9; s++) { - const srcVal = targetSplat[OFFSET_FRC0 + s] || 0; - shOut[s] = compressionLevel === 1 ? toHalfFloat(srcVal) : - toUint8(srcVal, minSphericalHarmonicsCoeff, maxSphericalHarmonicsCoeff); - } - const degree1ByteCount = 9 * bytesPerSHComponent; - copyBetweenBuffers(shOut.buffer, 0, sectionBuffer, sphericalHarmonicsBase, degree1ByteCount); - if (sphericalHarmonicsDegree >= 2) { - for (let s = 0; s < 15; s++) { - const srcVal = targetSplat[OFFSET_FRC9 + s] || 0; - shOut[s + 9] = compressionLevel === 1 ? toHalfFloat(srcVal) : - toUint8(srcVal, minSphericalHarmonicsCoeff, maxSphericalHarmonicsCoeff); - } - copyBetweenBuffers(shOut.buffer, degree1ByteCount, sectionBuffer, - sphericalHarmonicsBase + degree1ByteCount, 15 * bytesPerSHComponent); - } - } - } - - copyBetweenBuffers(center.buffer, 0, sectionBuffer, centerBase, 6); - copyBetweenBuffers(scale.buffer, 0, sectionBuffer, scaleBase, 6); - copyBetweenBuffers(rot.buffer, 0, sectionBuffer, rotationBase, 8); - } - - const rgba = new Uint8ClampedArray(tempColorBuffer, 0, 4); - rgba.set([targetSplat[OFFSET_FDC0] || 0, targetSplat[OFFSET_FDC1] || 0, targetSplat[OFFSET_FDC2] || 0]); - rgba[3] = targetSplat[OFFSET_OPACITY] || 0; - - copyBetweenBuffers(rgba.buffer, 0, sectionBuffer, colorBase, 4); - }; - - }(); - - static generateFromUncompressedSplatArrays(splatArrays, minimumAlpha, compressionLevel, - sceneCenter, blockSize, bucketSize, options = []) { - - let shDegree = 0; - for (let sa = 0; sa < splatArrays.length; sa ++) { - const splatArray = splatArrays[sa]; - shDegree = Math.max(splatArray.sphericalHarmonicsDegree, shDegree); - } - - let minSphericalHarmonicsCoeff; - let maxSphericalHarmonicsCoeff; - - for (let sa = 0; sa < splatArrays.length; sa ++) { - const splatArray = splatArrays[sa]; - for (let i = 0; i < splatArray.splats.length; i++) { - const splat = splatArray.splats[i]; - for (let sc = UncompressedSplatArray.OFFSET.FRC0; sc < UncompressedSplatArray.OFFSET.FRC23 && sc < splat.length; sc++) { - if (!minSphericalHarmonicsCoeff || splat[sc] < minSphericalHarmonicsCoeff) { - minSphericalHarmonicsCoeff = splat[sc]; - } - if (!maxSphericalHarmonicsCoeff || splat[sc] > maxSphericalHarmonicsCoeff) { - maxSphericalHarmonicsCoeff = splat[sc]; - } - } - } - } - - minSphericalHarmonicsCoeff = minSphericalHarmonicsCoeff || -DefaultSphericalHarmonics8BitCompressionHalfRange; - maxSphericalHarmonicsCoeff = maxSphericalHarmonicsCoeff || DefaultSphericalHarmonics8BitCompressionHalfRange; - - const { bytesPerSplat } = SplatBuffer.calculateComponentStorage(compressionLevel, shDegree); - const compressionScaleRange = SplatBuffer.CompressionLevels[compressionLevel].ScaleRange; - - const sectionBuffers = []; - const sectionHeaderBuffers = []; - let totalSplatCount = 0; - - for (let sa = 0; sa < splatArrays.length; sa ++) { - const splatArray = splatArrays[sa]; - const validSplats = new UncompressedSplatArray(shDegree); - for (let i = 0; i < splatArray.splatCount; i++) { - const targetSplat = splatArray.splats[i]; - if ((targetSplat[UncompressedSplatArray.OFFSET.OPACITY] || 0) >= minimumAlpha) { - validSplats.addSplat(targetSplat); - } - } - - const sectionOptions = options[sa] || {}; - const sectionBlockSize = (sectionOptions.blockSizeFactor || 1) * (blockSize || SplatBuffer.BucketBlockSize); - const sectionBucketSize = Math.ceil((sectionOptions.bucketSizeFactor || 1) * (bucketSize || SplatBuffer.BucketSize)); - - const bucketInfo = SplatBuffer.computeBucketsForUncompressedSplatArray(validSplats, sectionBlockSize, sectionBucketSize); - const fullBucketCount = bucketInfo.fullBuckets.length; - const partiallyFullBucketLengths = bucketInfo.partiallyFullBuckets.map((bucket) => bucket.splats.length); - const partiallyFilledBucketCount = partiallyFullBucketLengths.length; - const buckets = [...bucketInfo.fullBuckets, ...bucketInfo.partiallyFullBuckets]; - - const sectionDataSizeBytes = validSplats.splats.length * bytesPerSplat; - const bucketMetaDataSizeBytes = partiallyFilledBucketCount * 4; - const bucketDataBytes = compressionLevel >= 1 ? buckets.length * - SplatBuffer.BucketStorageSizeBytes + bucketMetaDataSizeBytes : 0; - const sectionSizeBytes = sectionDataSizeBytes + bucketDataBytes; - const sectionBuffer = new ArrayBuffer(sectionSizeBytes); - - const compressionScaleFactor = compressionScaleRange / (sectionBlockSize * 0.5); - const bucketCenter = new THREE.Vector3(); - - let outSplatCount = 0; - for (let b = 0; b < buckets.length; b++) { - const bucket = buckets[b]; - bucketCenter.fromArray(bucket.center); - for (let i = 0; i < bucket.splats.length; i++) { - let row = bucket.splats[i]; - const targetSplat = validSplats.splats[row]; - const bufferOffset = bucketDataBytes + outSplatCount * bytesPerSplat; - SplatBuffer.writeSplatDataToSectionBuffer(targetSplat, sectionBuffer, bufferOffset, compressionLevel, shDegree, - bucketCenter, compressionScaleFactor, compressionScaleRange, - minSphericalHarmonicsCoeff, maxSphericalHarmonicsCoeff); - outSplatCount++; - } - } - totalSplatCount += outSplatCount; - - if (compressionLevel >= 1) { - const bucketMetaDataArray = new Uint32Array(sectionBuffer, 0, partiallyFullBucketLengths.length * 4); - for (let pfb = 0; pfb < partiallyFullBucketLengths.length; pfb ++) { - bucketMetaDataArray[pfb] = partiallyFullBucketLengths[pfb]; - } - const bucketArray = new Float32Array(sectionBuffer, bucketMetaDataSizeBytes, - buckets.length * SplatBuffer.BucketStorageSizeFloats); - for (let b = 0; b < buckets.length; b++) { - const bucket = buckets[b]; - const base = b * 3; - bucketArray[base] = bucket.center[0]; - bucketArray[base + 1] = bucket.center[1]; - bucketArray[base + 2] = bucket.center[2]; - } - } - sectionBuffers.push(sectionBuffer); - - const sectionHeaderBuffer = new ArrayBuffer(SplatBuffer.SectionHeaderSizeBytes); - SplatBuffer.writeSectionHeaderToBuffer({ - maxSplatCount: outSplatCount, - splatCount: outSplatCount, - bucketSize: sectionBucketSize, - bucketCount: buckets.length, - bucketBlockSize: sectionBlockSize, - compressionScaleRange: compressionScaleRange, - storageSizeBytes: sectionSizeBytes, - fullBucketCount: fullBucketCount, - partiallyFilledBucketCount: partiallyFilledBucketCount, - sphericalHarmonicsDegree: shDegree - }, compressionLevel, sectionHeaderBuffer, 0); - sectionHeaderBuffers.push(sectionHeaderBuffer); - - } - - let sectionsCumulativeSizeBytes = 0; - for (let sectionBuffer of sectionBuffers) sectionsCumulativeSizeBytes += sectionBuffer.byteLength; - const unifiedBufferSize = SplatBuffer.HeaderSizeBytes + - SplatBuffer.SectionHeaderSizeBytes * sectionBuffers.length + sectionsCumulativeSizeBytes; - const unifiedBuffer = new ArrayBuffer(unifiedBufferSize); - - SplatBuffer.writeHeaderToBuffer({ - versionMajor: 0, - versionMinor: 1, - maxSectionCount: sectionBuffers.length, - sectionCount: sectionBuffers.length, - maxSplatCount: totalSplatCount, - splatCount: totalSplatCount, - compressionLevel: compressionLevel, - sceneCenter: sceneCenter, - minSphericalHarmonicsCoeff: minSphericalHarmonicsCoeff, - maxSphericalHarmonicsCoeff: maxSphericalHarmonicsCoeff - }, unifiedBuffer); - - let currentUnifiedBase = SplatBuffer.HeaderSizeBytes; - for (let sectionHeaderBuffer of sectionHeaderBuffers) { - new Uint8Array(unifiedBuffer, currentUnifiedBase, SplatBuffer.SectionHeaderSizeBytes).set(new Uint8Array(sectionHeaderBuffer)); - currentUnifiedBase += SplatBuffer.SectionHeaderSizeBytes; - } - - for (let sectionBuffer of sectionBuffers) { - new Uint8Array(unifiedBuffer, currentUnifiedBase, sectionBuffer.byteLength).set(new Uint8Array(sectionBuffer)); - currentUnifiedBase += sectionBuffer.byteLength; - } - - const splatBuffer = new SplatBuffer(unifiedBuffer); - return splatBuffer; - } - - static computeBucketsForUncompressedSplatArray(splatArray, blockSize, bucketSize) { - let splatCount = splatArray.splatCount; - const halfBlockSize = blockSize / 2.0; - - const min = new THREE.Vector3(); - const max = new THREE.Vector3(); - - for (let i = 0; i < splatCount; i++) { - const targetSplat = splatArray.splats[i]; - const center = [targetSplat[UncompressedSplatArray.OFFSET.X], - targetSplat[UncompressedSplatArray.OFFSET.Y], - targetSplat[UncompressedSplatArray.OFFSET.Z]]; - if (i === 0 || center[0] < min.x) min.x = center[0]; - if (i === 0 || center[0] > max.x) max.x = center[0]; - if (i === 0 || center[1] < min.y) min.y = center[1]; - if (i === 0 || center[1] > max.y) max.y = center[1]; - if (i === 0 || center[2] < min.z) min.z = center[2]; - if (i === 0 || center[2] > max.z) max.z = center[2]; - } - - const dimensions = new THREE.Vector3().copy(max).sub(min); - const yBlocks = Math.ceil(dimensions.y / blockSize); - const zBlocks = Math.ceil(dimensions.z / blockSize); - - const blockCenter = new THREE.Vector3(); - const fullBuckets = []; - const partiallyFullBuckets = {}; - - for (let i = 0; i < splatCount; i++) { - const targetSplat = splatArray.splats[i]; - const center = [targetSplat[UncompressedSplatArray.OFFSET.X], - targetSplat[UncompressedSplatArray.OFFSET.Y], - targetSplat[UncompressedSplatArray.OFFSET.Z]]; - const xBlock = Math.floor((center[0] - min.x) / blockSize); - const yBlock = Math.floor((center[1] - min.y) / blockSize); - const zBlock = Math.floor((center[2] - min.z) / blockSize); - - blockCenter.x = xBlock * blockSize + min.x + halfBlockSize; - blockCenter.y = yBlock * blockSize + min.y + halfBlockSize; - blockCenter.z = zBlock * blockSize + min.z + halfBlockSize; - - const bucketId = xBlock * (yBlocks * zBlocks) + yBlock * zBlocks + zBlock; - let bucket = partiallyFullBuckets[bucketId]; - if (!bucket) { - partiallyFullBuckets[bucketId] = bucket = { - 'splats': [], - 'center': blockCenter.toArray() - }; - } - - bucket.splats.push(i); - if (bucket.splats.length >= bucketSize) { - fullBuckets.push(bucket); - partiallyFullBuckets[bucketId] = null; - } - } - - const partiallyFullBucketArray = []; - for (let bucketId in partiallyFullBuckets) { - if (partiallyFullBuckets.hasOwnProperty(bucketId)) { - const bucket = partiallyFullBuckets[bucketId]; - if (bucket) { - partiallyFullBucketArray.push(bucket); - } - } - } - - return { - 'fullBuckets': fullBuckets, - 'partiallyFullBuckets': partiallyFullBucketArray, - }; - } - - static preallocateUncompressed(splatCount, sphericalHarmonicsDegrees) { - const shDescriptor = SplatBuffer.CompressionLevels[0].SphericalHarmonicsDegrees[sphericalHarmonicsDegrees]; - const splatBufferDataOffsetBytes = SplatBuffer.HeaderSizeBytes + SplatBuffer.SectionHeaderSizeBytes; - const splatBufferSizeBytes = splatBufferDataOffsetBytes + shDescriptor.BytesPerSplat * splatCount; - const outBuffer = new ArrayBuffer(splatBufferSizeBytes); - SplatBuffer.writeHeaderToBuffer({ - versionMajor: SplatBuffer.CurrentMajorVersion, - versionMinor: SplatBuffer.CurrentMinorVersion, - maxSectionCount: 1, - sectionCount: 1, - maxSplatCount: splatCount, - splatCount: splatCount, - compressionLevel: 0, - sceneCenter: new THREE.Vector3() - }, outBuffer); - - SplatBuffer.writeSectionHeaderToBuffer({ - maxSplatCount: splatCount, - splatCount: splatCount, - bucketSize: 0, - bucketCount: 0, - bucketBlockSize: 0, - compressionScaleRange: 0, - storageSizeBytes: 0, - fullBucketCount: 0, - partiallyFilledBucketCount: 0, - sphericalHarmonicsDegree: sphericalHarmonicsDegrees - }, 0, outBuffer, SplatBuffer.HeaderSizeBytes); - - return { - splatBuffer: new SplatBuffer(outBuffer, true), - splatBufferDataOffsetBytes - }; - } -} - -const HeaderMagicBytes = new Uint8Array([112, 108, 121, 10]); -const HeaderEndTokenBytes = new Uint8Array([10, 101, 110, 100, 95, 104, 101, 97, 100, 101, 114, 10]); -const HeaderEndToken = 'end_header'; - -const DataTypeMap = new Map([ - ['char', Int8Array], - ['uchar', Uint8Array], - ['short', Int16Array], - ['ushort', Uint16Array], - ['int', Int32Array], - ['uint', Uint32Array], - ['float', Float32Array], - ['double', Float64Array], -]); - -const unpackUnorm = (value, bits) => { - const t = (1 << bits) - 1; - return (value & t) / t; -}; - -const unpack111011 = (result, value) => { - result.x = unpackUnorm(value >>> 21, 11); - result.y = unpackUnorm(value >>> 11, 10); - result.z = unpackUnorm(value, 11); -}; - -const unpack8888 = (result, value) => { - result.x = unpackUnorm(value >>> 24, 8); - result.y = unpackUnorm(value >>> 16, 8); - result.z = unpackUnorm(value >>> 8, 8); - result.w = unpackUnorm(value, 8); -}; - -// unpack quaternion with 2,10,10,10 format (largest element, 3x10bit element) -const unpackRot = (result, value) => { - const norm = 1.0 / (Math.sqrt(2) * 0.5); - const a = (unpackUnorm(value >>> 20, 10) - 0.5) * norm; - const b = (unpackUnorm(value >>> 10, 10) - 0.5) * norm; - const c = (unpackUnorm(value, 10) - 0.5) * norm; - const m = Math.sqrt(1.0 - (a * a + b * b + c * c)); - - switch (value >>> 30) { - case 0: - result.set(m, a, b, c); - break; - case 1: - result.set(a, m, b, c); - break; - case 2: - result.set(a, b, m, c); - break; - case 3: - result.set(a, b, c, m); - break; - } -}; - -const lerp = (a, b, t) => { - return a * (1 - t) + b * t; -}; - -const getElementPropStorage = (element, name) => { - return element.properties.find((p) => p.name === name && p.storage) - ?.storage; -}; - -class PlayCanvasCompressedPlyParser { - - static decodeHeaderText(headerText) { - - let element; - let chunkElement; - let vertexElement; - let shElement; - - const headerLines = headerText.split('\n').filter((line) => !line.startsWith('comment ')); - - let bytesPerSplat = 0; - let done = false; - for (let i = 1; i < headerLines.length; ++i) { - const words = headerLines[i].split(' '); - - switch (words[0]) { - case 'format': - if (words[1] !== 'binary_little_endian') { - throw new Error('Unsupported ply format'); - } - break; - case 'element': - element = { - name: words[1], - count: parseInt(words[2], 10), - properties: [], - storageSizeBytes: 0 - }; - if (element.name === 'chunk') chunkElement = element; - else if (element.name === 'vertex') vertexElement = element; - else if (element.name === 'sh') shElement = element; - break; - case 'property': { - if (!DataTypeMap.has(words[1])) { - throw new Error( - `Unrecognized property data type '${words[1]}' in ply header` - ); - } - const StorageType = DataTypeMap.get(words[1]); - const storageSizeByes = StorageType.BYTES_PER_ELEMENT * element.count; - if (element.name === 'vertex') bytesPerSplat += StorageType.BYTES_PER_ELEMENT; - element.properties.push({ - type: words[1], - name: words[2], - storage: null, - byteSize: StorageType.BYTES_PER_ELEMENT, - storageSizeByes: storageSizeByes - }); - element.storageSizeBytes += storageSizeByes; - break; - } - case HeaderEndToken: - done = true; - break; - default: - throw new Error( - `Unrecognized header value '${words[0]}' in ply header` - ); - } - if (done) break; - } - - let sphericalHarmonicsDegree = 0; - let sphericalHarmonicsPerSplat = 0; - if (shElement) { - sphericalHarmonicsPerSplat = shElement.properties.length; - if (shElement.properties.length >= 45) { - sphericalHarmonicsDegree = 3; - } else if (shElement.properties.length >= 24) { - sphericalHarmonicsDegree = 2; - } else if (shElement.properties.length >= 9) { - sphericalHarmonicsDegree = 1; - } - } - - return { - 'chunkElement': chunkElement, - 'vertexElement': vertexElement, - 'shElement': shElement, - 'bytesPerSplat': bytesPerSplat, - 'headerSizeBytes': headerText.indexOf(HeaderEndToken) + HeaderEndToken.length + 1, - 'sphericalHarmonicsDegree': sphericalHarmonicsDegree, - 'sphericalHarmonicsPerSplat': sphericalHarmonicsPerSplat - }; - } - - static decodeHeader(plyBuffer) { - - /** - * Searches for the first occurrence of a sequence within a buffer. - * @example - * find(new Uint8Array([1, 2, 3, 4]), new Uint8Array([3, 4])); // 2 - * @param {Uint8Array} buf - The buffer in which to search. - * @param {Uint8Array} search - The sequence to search for. - * @return {number} The index of the first occurrence of the search sequence in the buffer, or -1 if not found. - */ - const find = (buf, search) => { - const endIndex = buf.length - search.length; - let i; - let j; - for (i = 0; i <= endIndex; ++i) { - for (j = 0; j < search.length; ++j) { - if (buf[i + j] !== search[j]) { - break; - } - } - if (j === search.length) { - return i; - } - } - return -1; - }; - - /** - * Checks if array 'a' starts with the same elements as array 'b'. - * @example - * startsWith(new Uint8Array([1, 2, 3, 4]), new Uint8Array([1, 2])); // true - * @param {Uint8Array} a - The array to check against. - * @param {Uint8Array} b - The array of elements to look for at the start of 'a'. - * @return {boolean} - True if 'a' starts with all elements of 'b', otherwise false. - */ - const startsWith = (a, b) => { - if (a.length < b.length) { - return false; - } - - for (let i = 0; i < b.length; ++i) { - if (a[i] !== b[i]) { - return false; - } - } - - return true; - }; - - let buf = new Uint8Array(plyBuffer); - let endHeaderTokenOffset; - - if (buf.length >= HeaderMagicBytes.length && !startsWith(buf, HeaderMagicBytes)) { - throw new Error('Invalid PLY header'); - } - - endHeaderTokenOffset = find(buf, HeaderEndTokenBytes); - if (endHeaderTokenOffset === -1) { - throw new Error('End of PLY header not found'); - } - - const headerText = new TextDecoder('ascii').decode( - buf.slice(0, endHeaderTokenOffset) - ); - - const { - chunkElement, - vertexElement, - shElement, - sphericalHarmonicsDegree, - sphericalHarmonicsPerSplat, - bytesPerSplat - } = PlayCanvasCompressedPlyParser.decodeHeaderText(headerText); - - return { - 'headerSizeBytes': endHeaderTokenOffset + HeaderEndTokenBytes.length, - 'bytesPerSplat': bytesPerSplat, - 'chunkElement': chunkElement, - 'vertexElement': vertexElement, - 'shElement': shElement, - 'sphericalHarmonicsDegree': sphericalHarmonicsDegree, - 'sphericalHarmonicsPerSplat': sphericalHarmonicsPerSplat - }; - } - - static readElementData(element, readBuffer, readOffset, fromIndex, toIndex, propertyFilter = null) { - - let dataView = readBuffer instanceof DataView ? readBuffer : new DataView(readBuffer); - - fromIndex = fromIndex || 0; - toIndex = toIndex || element.count - 1; - for (let e = fromIndex; e <= toIndex; ++e) { - for (let j = 0; j < element.properties.length; ++j) { - const property = element.properties[j]; - - const StorageType = DataTypeMap.get(property.type); - const requiredStorageSizeBytes = StorageType.BYTES_PER_ELEMENT * element.count; - if ((!property.storage || property.storage.byteLength < requiredStorageSizeBytes) && - (!propertyFilter || propertyFilter(property.name))) { - property.storage = new StorageType(element.count); - } - - if (property.storage) { - switch (property.type) { - case 'char': - property.storage[e] = dataView.getInt8(readOffset); - break; - case 'uchar': - property.storage[e] = dataView.getUint8(readOffset); - break; - case 'short': - property.storage[e] = dataView.getInt16(readOffset, true); - break; - case 'ushort': - property.storage[e] = dataView.getUint16(readOffset, true); - break; - case 'int': - property.storage[e] = dataView.getInt32(readOffset, true); - break; - case 'uint': - property.storage[e] = dataView.getUint32(readOffset, true); - break; - case 'float': - property.storage[e] = dataView.getFloat32(readOffset, true); - break; - case 'double': - property.storage[e] = dataView.getFloat64(readOffset, true); - break; - } - } - - readOffset += property.byteSize; - } - } - - return readOffset; - } - - static readPly(plyBuffer, propertyFilter = null) { - - const header = PlayCanvasCompressedPlyParser.decodeHeader(plyBuffer); - - let readIndex = PlayCanvasCompressedPlyParser.readElementData(header.chunkElement, plyBuffer, - header.headerSizeBytes, null, null, propertyFilter); - readIndex = PlayCanvasCompressedPlyParser.readElementData(header.vertexElement, plyBuffer, readIndex, null, null, propertyFilter); - PlayCanvasCompressedPlyParser.readElementData(header.shElement, plyBuffer, readIndex, null, null, propertyFilter); - - return { - 'chunkElement': header.chunkElement, - 'vertexElement': header.vertexElement, - 'shElement': header.shElement, - 'sphericalHarmonicsDegree': header.sphericalHarmonicsDegree, - 'sphericalHarmonicsPerSplat': header.sphericalHarmonicsPerSplat - }; - } - - static getElementStorageArrays(chunkElement, vertexElement, shElement) { - const storageArrays = {}; - - if (vertexElement) { - const minR = getElementPropStorage(chunkElement, 'min_r'); - const minG = getElementPropStorage(chunkElement, 'min_g'); - const minB = getElementPropStorage(chunkElement, 'min_b'); - const maxR = getElementPropStorage(chunkElement, 'max_r'); - const maxG = getElementPropStorage(chunkElement, 'max_g'); - const maxB = getElementPropStorage(chunkElement, 'max_b'); - const minX = getElementPropStorage(chunkElement, 'min_x'); - const minY = getElementPropStorage(chunkElement, 'min_y'); - const minZ = getElementPropStorage(chunkElement, 'min_z'); - const maxX = getElementPropStorage(chunkElement, 'max_x'); - const maxY = getElementPropStorage(chunkElement, 'max_y'); - const maxZ = getElementPropStorage(chunkElement, 'max_z'); - const minScaleX = getElementPropStorage(chunkElement, 'min_scale_x'); - const minScaleY = getElementPropStorage(chunkElement, 'min_scale_y'); - const minScaleZ = getElementPropStorage(chunkElement, 'min_scale_z'); - const maxScaleX = getElementPropStorage(chunkElement, 'max_scale_x'); - const maxScaleY = getElementPropStorage(chunkElement, 'max_scale_y'); - const maxScaleZ = getElementPropStorage(chunkElement, 'max_scale_z'); - const position = getElementPropStorage(vertexElement, 'packed_position'); - const rotation = getElementPropStorage(vertexElement, 'packed_rotation'); - const scale = getElementPropStorage(vertexElement, 'packed_scale'); - const color = getElementPropStorage(vertexElement, 'packed_color'); - - storageArrays['colorExtremes'] = { - minR, maxR, - minG, maxG, - minB, maxB - }; - storageArrays['positionExtremes'] = { - minX, maxX, - minY, maxY, - minZ, maxZ - }; - storageArrays['scaleExtremes'] = { - minScaleX, maxScaleX, minScaleY, - maxScaleY, minScaleZ, maxScaleZ - }; - storageArrays['position'] = position; - storageArrays['rotation'] = rotation; - storageArrays['scale'] = scale; - storageArrays['color'] = color; - } - - if (shElement) { - const shStorageArrays = {}; - for (let i = 0; i < 45; i++) { - const fRestKey = `f_rest_${i}`; - const fRest = getElementPropStorage(shElement, fRestKey); - if (fRest) { - shStorageArrays[fRestKey] = fRest; - } else { - break; - } - } - storageArrays['sh'] = shStorageArrays; - } - - return storageArrays; - } - - static decompressBaseSplat = function() { - - const p = new THREE.Vector3(); - const r = new THREE.Quaternion(); - const s = new THREE.Vector3(); - const c = new THREE.Vector4(); - - const OFFSET = UncompressedSplatArray.OFFSET; - - return function(index, chunkSplatIndexOffset, positionArray, positionExtremes, scaleArray, scaleExtremes, - rotationArray, colorExtremes, colorArray, outSplat) { - outSplat = outSplat || UncompressedSplatArray.createSplat(); - - const chunkIndex = Math.floor((chunkSplatIndexOffset + index) / 256); - - unpack111011(p, positionArray[index]); - unpackRot(r, rotationArray[index]); - unpack111011(s, scaleArray[index]); - unpack8888(c, colorArray[index]); - - outSplat[OFFSET.X] = lerp(positionExtremes.minX[chunkIndex], positionExtremes.maxX[chunkIndex], p.x); - outSplat[OFFSET.Y] = lerp(positionExtremes.minY[chunkIndex], positionExtremes.maxY[chunkIndex], p.y); - outSplat[OFFSET.Z] = lerp(positionExtremes.minZ[chunkIndex], positionExtremes.maxZ[chunkIndex], p.z); - - outSplat[OFFSET.ROTATION0] = r.x; - outSplat[OFFSET.ROTATION1] = r.y; - outSplat[OFFSET.ROTATION2] = r.z; - outSplat[OFFSET.ROTATION3] = r.w; - - outSplat[OFFSET.SCALE0] = Math.exp(lerp(scaleExtremes.minScaleX[chunkIndex], scaleExtremes.maxScaleX[chunkIndex], s.x)); - outSplat[OFFSET.SCALE1] = Math.exp(lerp(scaleExtremes.minScaleY[chunkIndex], scaleExtremes.maxScaleY[chunkIndex], s.y)); - outSplat[OFFSET.SCALE2] = Math.exp(lerp(scaleExtremes.minScaleZ[chunkIndex], scaleExtremes.maxScaleZ[chunkIndex], s.z)); - - if (colorExtremes.minR && colorExtremes.maxR) { - outSplat[OFFSET.FDC0] = clamp(Math.round(lerp(colorExtremes.minR[chunkIndex], colorExtremes.maxR[chunkIndex], c.x) * 255), 0, 255); - } else { - outSplat[OFFSET.FDC0] = clamp(Math.floor(c.x * 255), 0, 255); - } - if (colorExtremes.minG && colorExtremes.maxG) { - outSplat[OFFSET.FDC1] = clamp(Math.round(lerp(colorExtremes.minG[chunkIndex], colorExtremes.maxG[chunkIndex], c.y) * 255), 0, 255); - } else { - outSplat[OFFSET.FDC1] = clamp(Math.floor(c.y * 255), 0, 255); - } - if (colorExtremes.minB && colorExtremes.maxB) { - outSplat[OFFSET.FDC2] = clamp(Math.round(lerp(colorExtremes.minB[chunkIndex], colorExtremes.maxB[chunkIndex], c.z) * 255), 0, 255); - } else { - outSplat[OFFSET.FDC2] = clamp(Math.floor(c.z * 255), 0, 255); - } - outSplat[OFFSET.OPACITY] = clamp(Math.floor(c.w * 255), 0, 255); - - return outSplat; - }; - - }(); - - static decompressSphericalHarmonics = function() { - - const shCoeffMap = [0, 3, 8, 15]; - - const shIndexMap = [ - 0, 1, 2, 9, 10, 11, 12, 13, 24, 25, 26, 27, 28, 29, 30, - 3, 4, 5, 14, 15, 16, 17, 18, 31, 32, 33, 34, 35, 36, 37, - 6, 7, 8, 19, 20, 21, 22, 23, 38, 39, 40, 41, 42, 43, 44 - ]; - - return function(index, shArray, outSphericalHarmonicsDegree, readSphericalHarmonicsDegree, outSplat) { - outSplat = outSplat || UncompressedSplatArray.createSplat(); - let outSHCoeff = shCoeffMap[outSphericalHarmonicsDegree]; - let readSHCoeff = shCoeffMap[readSphericalHarmonicsDegree]; - for (let j = 0; j < 3; ++j) { - for (let k = 0; k < 15; ++k) { - const outIndex = shIndexMap[j * 15 + k]; - if (k < outSHCoeff && k < readSHCoeff) { - outSplat[UncompressedSplatArray.OFFSET.FRC0 + outIndex] = (shArray[j * readSHCoeff + k][index] * (8 / 255) - 4); - } - } - } - - return outSplat; - }; - - }(); - - static parseToUncompressedSplatBufferSection(chunkElement, vertexElement, fromIndex, toIndex, chunkSplatIndexOffset, - vertexDataBuffer, outBuffer, outOffset, propertyFilter = null) { - - PlayCanvasCompressedPlyParser.readElementData(vertexElement, vertexDataBuffer, 0, fromIndex, toIndex, propertyFilter); - - const outBytesPerSplat = SplatBuffer.CompressionLevels[0].SphericalHarmonicsDegrees[0].BytesPerSplat; - - const { positionExtremes, scaleExtremes, colorExtremes, position, rotation, scale, color } = - PlayCanvasCompressedPlyParser.getElementStorageArrays(chunkElement, vertexElement); - - const tempSplat = UncompressedSplatArray.createSplat(); - - for (let i = fromIndex; i <= toIndex; ++i) { - PlayCanvasCompressedPlyParser.decompressBaseSplat(i, chunkSplatIndexOffset, position, positionExtremes, - scale, scaleExtremes, rotation, colorExtremes, color, tempSplat); - const outBase = i * outBytesPerSplat + outOffset; - SplatBuffer.writeSplatDataToSectionBuffer(tempSplat, outBuffer, outBase, 0, 0); - } - } - - static parseToUncompressedSplatArraySection(chunkElement, vertexElement, fromIndex, toIndex, chunkSplatIndexOffset, - vertexDataBuffer, splatArray, propertyFilter = null) { - - PlayCanvasCompressedPlyParser.readElementData(vertexElement, vertexDataBuffer, 0, fromIndex, toIndex, propertyFilter); - - const { positionExtremes, scaleExtremes, colorExtremes, position, rotation, scale, color } = - PlayCanvasCompressedPlyParser.getElementStorageArrays(chunkElement, vertexElement); - - for (let i = fromIndex; i <= toIndex; ++i) { - const tempSplat = UncompressedSplatArray.createSplat(); - PlayCanvasCompressedPlyParser.decompressBaseSplat(i, chunkSplatIndexOffset, position, positionExtremes, - scale, scaleExtremes, rotation, colorExtremes, color, tempSplat); - splatArray.addSplat(tempSplat); - } - } - - static parseSphericalHarmonicsToUncompressedSplatArraySection(chunkElement, shElement, fromIndex, toIndex, - vertexDataBuffer, vertexReadOffset, outSphericalHarmonicsDegree, readSphericalHarmonicsDegree, splatArray, propertyFilter = null) { - - PlayCanvasCompressedPlyParser.readElementData(shElement, vertexDataBuffer, vertexReadOffset, fromIndex, toIndex, propertyFilter); - - const { sh } = PlayCanvasCompressedPlyParser.getElementStorageArrays(chunkElement, undefined, shElement); - const shArrays = Object.values(sh); - - for (let i = fromIndex; i <= toIndex; ++i) { - PlayCanvasCompressedPlyParser.decompressSphericalHarmonics( - i, shArrays, outSphericalHarmonicsDegree, readSphericalHarmonicsDegree, splatArray.splats[i] - ); - } - } - - static parseToUncompressedSplatArray(plyBuffer, outSphericalHarmonicsDegree) { - const { chunkElement, vertexElement, shElement, sphericalHarmonicsDegree } = PlayCanvasCompressedPlyParser.readPly(plyBuffer); - - outSphericalHarmonicsDegree = Math.min(outSphericalHarmonicsDegree, sphericalHarmonicsDegree); - - const splatArray = new UncompressedSplatArray(outSphericalHarmonicsDegree); - - const { positionExtremes, scaleExtremes, colorExtremes, position, rotation, scale, color } = - PlayCanvasCompressedPlyParser.getElementStorageArrays(chunkElement, vertexElement); - - let shArrays; - if (outSphericalHarmonicsDegree > 0) { - const { sh } = PlayCanvasCompressedPlyParser.getElementStorageArrays(chunkElement, undefined, shElement); - shArrays = Object.values(sh); - } - - for (let i = 0; i < vertexElement.count; ++i) { - - splatArray.addDefaultSplat(); - const newSplat = splatArray.getSplat(splatArray.splatCount - 1); - - PlayCanvasCompressedPlyParser.decompressBaseSplat(i, 0, position, positionExtremes, scale, - scaleExtremes, rotation, colorExtremes, color, newSplat); - - if (outSphericalHarmonicsDegree > 0) { - PlayCanvasCompressedPlyParser.decompressSphericalHarmonics( - i, shArrays, outSphericalHarmonicsDegree, sphericalHarmonicsDegree, newSplat - ); - } - } - - return splatArray; - } - - static parseToUncompressedSplatBuffer(plyBuffer, outSphericalHarmonicsDegree) { - const { chunkElement, vertexElement, shElement, sphericalHarmonicsDegree } = PlayCanvasCompressedPlyParser.readPly(plyBuffer); - - outSphericalHarmonicsDegree = Math.min(outSphericalHarmonicsDegree, sphericalHarmonicsDegree); - - const { - splatBuffer, - splatBufferDataOffsetBytes - } = SplatBuffer.preallocateUncompressed(vertexElement.count, outSphericalHarmonicsDegree); - - const { positionExtremes, scaleExtremes, colorExtremes, position, rotation, scale, color } = - PlayCanvasCompressedPlyParser.getElementStorageArrays(chunkElement, vertexElement); - - let shArrays; - if (outSphericalHarmonicsDegree > 0) { - const { sh } = PlayCanvasCompressedPlyParser.getElementStorageArrays(chunkElement, undefined, shElement); - shArrays = Object.values(sh); - } - - const outBytesPerSplat = SplatBuffer.CompressionLevels[0].SphericalHarmonicsDegrees[outSphericalHarmonicsDegree].BytesPerSplat; - - const newSplat = UncompressedSplatArray.createSplat(outSphericalHarmonicsDegree); - - for (let i = 0; i < vertexElement.count; ++i) { - PlayCanvasCompressedPlyParser.decompressBaseSplat( - i, 0, position, positionExtremes, scale, scaleExtremes, rotation, colorExtremes, color, newSplat - ); - if (outSphericalHarmonicsDegree > 0) { - PlayCanvasCompressedPlyParser.decompressSphericalHarmonics( - i, shArrays, outSphericalHarmonicsDegree, sphericalHarmonicsDegree, newSplat - ); - } - - const outBase = i * outBytesPerSplat + splatBufferDataOffsetBytes; - SplatBuffer.writeSplatDataToSectionBuffer(newSplat, splatBuffer.bufferData, outBase, 0, outSphericalHarmonicsDegree); - } - - return splatBuffer; - } - -} - -const PlyFormat = { - 'INRIAV1': 0, - 'INRIAV2': 1, - 'PlayCanvasCompressed': 2 -}; - -const [ - FieldSizeIdDouble, FieldSizeIdInt, FieldSizeIdUInt, FieldSizeIdFloat, FieldSizeIdShort, FieldSizeIdUShort, FieldSizeIdUChar - ] = [0, 1, 2, 3, 4, 5, 6]; - -const FieldSizeStringMap = { - 'double': FieldSizeIdDouble, - 'int': FieldSizeIdInt, - 'uint': FieldSizeIdUInt, - 'float': FieldSizeIdFloat, - 'short': FieldSizeIdShort, - 'ushort': FieldSizeIdUShort, - 'uchar': FieldSizeIdUChar, -}; - -const FieldSize = { - [FieldSizeIdDouble]: 8, - [FieldSizeIdInt]: 4, - [FieldSizeIdUInt]: 4, - [FieldSizeIdFloat]: 4, - [FieldSizeIdShort]: 2, - [FieldSizeIdUShort]: 2, - [FieldSizeIdUChar]: 1, -}; - -class PlyParserUtils { - - static HeaderEndToken = 'end_header'; - - static decodeSectionHeader(headerLines, fieldNameIdMap, headerStartLine = 0) { - - const extractedLines = []; - - let processingSection = false; - let headerEndLine = -1; - let vertexCount = 0; - let endOfHeader = false; - let sectionName = null; - - const fieldIds = []; - const fieldTypes = []; - const allFieldNames = []; - const usedFieldNames = []; - const fieldTypesByName = {}; - - for (let i = headerStartLine; i < headerLines.length; i++) { - const line = headerLines[i].trim(); - if (line.startsWith('element')) { - if (processingSection) { - headerEndLine--; - break; - } else { - processingSection = true; - headerStartLine = i; - headerEndLine = i; - const lineComponents = line.split(' '); - let validComponents = 0; - for (let lineComponent of lineComponents) { - const trimmedComponent = lineComponent.trim(); - if (trimmedComponent.length > 0) { - validComponents++; - if (validComponents === 2) { - sectionName = trimmedComponent; - } else if (validComponents === 3) { - vertexCount = parseInt(trimmedComponent); - } - } - } - } - } else if (line.startsWith('property')) { - const fieldMatch = line.match(/(\w+)\s+(\w+)\s+(\w+)/); - if (fieldMatch) { - const fieldTypeStr = fieldMatch[2]; - const fieldName = fieldMatch[3]; - allFieldNames.push(fieldName); - const fieldId = fieldNameIdMap[fieldName]; - fieldTypesByName[fieldName] = fieldTypeStr; - const fieldType = FieldSizeStringMap[fieldTypeStr]; - if (fieldId !== undefined) { - usedFieldNames.push(fieldName); - fieldIds.push(fieldId); - fieldTypes[fieldId] = fieldType; - } - } - } - if (line === PlyParserUtils.HeaderEndToken) { - endOfHeader = true; - break; - } - if (processingSection) { - extractedLines.push(line); - headerEndLine++; - } - } - - const fieldOffsets = []; - let bytesPerVertex = 0; - for (let fieldName of allFieldNames) { - const fieldType = fieldTypesByName[fieldName]; - if (fieldTypesByName.hasOwnProperty(fieldName)) { - const fieldId = fieldNameIdMap[fieldName]; - if (fieldId !== undefined) { - fieldOffsets[fieldId] = bytesPerVertex; - } - } - bytesPerVertex += FieldSize[FieldSizeStringMap[fieldType]]; - } - - const sphericalHarmonics = PlyParserUtils.decodeSphericalHarmonicsFromSectionHeader(allFieldNames, fieldNameIdMap); - - return { - 'headerLines': extractedLines, - 'headerStartLine': headerStartLine, - 'headerEndLine': headerEndLine, - 'fieldTypes': fieldTypes, - 'fieldIds': fieldIds, - 'fieldOffsets': fieldOffsets, - 'bytesPerVertex': bytesPerVertex, - 'vertexCount': vertexCount, - 'dataSizeBytes': bytesPerVertex * vertexCount, - 'endOfHeader': endOfHeader, - 'sectionName': sectionName, - 'sphericalHarmonicsDegree': sphericalHarmonics.degree, - 'sphericalHarmonicsCoefficientsPerChannel': sphericalHarmonics.coefficientsPerChannel, - 'sphericalHarmonicsDegree1Fields': sphericalHarmonics.degree1Fields, - 'sphericalHarmonicsDegree2Fields': sphericalHarmonics.degree2Fields - }; - - } - - static decodeSphericalHarmonicsFromSectionHeader(fieldNames, fieldNameIdMap) { - let sphericalHarmonicsFieldCount = 0; - let coefficientsPerChannel = 0; - for (let fieldName of fieldNames) { - if (fieldName.startsWith('f_rest')) sphericalHarmonicsFieldCount++; - } - coefficientsPerChannel = sphericalHarmonicsFieldCount / 3; - let degree = 0; - if (coefficientsPerChannel >= 3) degree = 1; - if (coefficientsPerChannel >= 8) degree = 2; - - let degree1Fields = []; - let degree2Fields = []; - - for (let rgb = 0; rgb < 3; rgb++) { - if (degree >= 1) { - for (let i = 0; i < 3; i++) { - degree1Fields.push(fieldNameIdMap['f_rest_' + (i + coefficientsPerChannel * rgb)]); - } - } - if (degree >= 2) { - for (let i = 0; i < 5; i++) { - degree2Fields.push(fieldNameIdMap['f_rest_' + (i + coefficientsPerChannel * rgb + 3)]); - } - } - } - - return { - 'degree': degree, - 'coefficientsPerChannel': coefficientsPerChannel, - 'degree1Fields': degree1Fields, - 'degree2Fields': degree2Fields - }; - } - - static getHeaderSectionNames(headerLines) { - const sectionNames = []; - for (let headerLine of headerLines) { - if (headerLine.startsWith('element')) { - const lineComponents = headerLine.split(' '); - let validComponents = 0; - for (let lineComponent of lineComponents) { - const trimmedComponent = lineComponent.trim(); - if (trimmedComponent.length > 0) { - validComponents++; - if (validComponents === 2) { - sectionNames.push(trimmedComponent); - } - } - } - } - } - return sectionNames; - } - - static checkTextForEndHeader(endHeaderTestText) { - if (endHeaderTestText.includes(PlyParserUtils.HeaderEndToken)) { - return true; - } - return false; - } - - static checkBufferForEndHeader(buffer, searchOfset, chunkSize, decoder) { - const endHeaderTestChunk = new Uint8Array(buffer, Math.max(0, searchOfset - chunkSize), chunkSize); - const endHeaderTestText = decoder.decode(endHeaderTestChunk); - return PlyParserUtils.checkTextForEndHeader(endHeaderTestText); - } - - static extractHeaderFromBufferToText(plyBuffer) { - const decoder = new TextDecoder(); - let headerOffset = 0; - let headerText = ''; - const readChunkSize = 100; - - while (true) { - if (headerOffset + readChunkSize >= plyBuffer.byteLength) { - throw new Error('End of file reached while searching for end of header'); - } - const headerChunk = new Uint8Array(plyBuffer, headerOffset, readChunkSize); - headerText += decoder.decode(headerChunk); - headerOffset += readChunkSize; - - if (PlyParserUtils.checkBufferForEndHeader(plyBuffer, headerOffset, readChunkSize * 2, decoder)) { - break; - } - } - - return headerText; - } - - static readHeaderFromBuffer(plyBuffer) { - const decoder = new TextDecoder(); - let headerOffset = 0; - let headerText = ''; - const readChunkSize = 100; - - while (true) { - if (headerOffset + readChunkSize >= plyBuffer.byteLength) { - throw new Error('End of file reached while searching for end of header'); - } - const headerChunk = new Uint8Array(plyBuffer, headerOffset, readChunkSize); - headerText += decoder.decode(headerChunk); - headerOffset += readChunkSize; - - if (PlyParserUtils.checkBufferForEndHeader(plyBuffer, headerOffset, readChunkSize * 2, decoder)) { - break; - } - } - - return headerText; - } - - static convertHeaderTextToLines(headerText) { - const headerLines = headerText.split('\n'); - const prunedLines = []; - for (let i = 0; i < headerLines.length; i++) { - const line = headerLines[i].trim(); - prunedLines.push(line); - if (line === PlyParserUtils.HeaderEndToken) { - break; - } - } - return prunedLines; - } - - static determineHeaderFormatFromHeaderText(headertText) { - const headerLines = PlyParserUtils.convertHeaderTextToLines(headertText); - let format = PlyFormat.INRIAV1; - for (let i = 0; i < headerLines.length; i++) { - const line = headerLines[i].trim(); - if (line.startsWith('element chunk') || line.match(/[A-Za-z]*packed_[A-Za-z]*/)) { - format = PlyFormat.PlayCanvasCompressed; - } else if (line.startsWith('element codebook_centers')) { - format = PlyFormat.INRIAV2; - } else if (line === PlyParserUtils.HeaderEndToken) { - break; - } - } - return format; - } - - static determineHeaderFormatFromPlyBuffer(plyBuffer) { - const headertText = PlyParserUtils.extractHeaderFromBufferToText(plyBuffer); - return PlyParserUtils.determineHeaderFormatFromHeaderText(headertText); - } - - static readVertex(vertexData, header, row, dataOffset, fieldsToRead, rawVertex, normalize = true) { - const offset = row * header.bytesPerVertex + dataOffset; - const fieldOffsets = header.fieldOffsets; - const fieldTypes = header.fieldTypes; - for (let fieldId of fieldsToRead) { - const fieldType = fieldTypes[fieldId]; - if (fieldType === FieldSizeIdFloat) { - rawVertex[fieldId] = vertexData.getFloat32(offset + fieldOffsets[fieldId], true); - } else if (fieldType === FieldSizeIdShort) { - rawVertex[fieldId] = vertexData.getInt16(offset + fieldOffsets[fieldId], true); - } else if (fieldType === FieldSizeIdUShort) { - rawVertex[fieldId] = vertexData.getUint16(offset + fieldOffsets[fieldId], true); - } else if (fieldType === FieldSizeIdInt) { - rawVertex[fieldId] = vertexData.getInt32(offset + fieldOffsets[fieldId], true); - } else if (fieldType === FieldSizeIdUInt) { - rawVertex[fieldId] = vertexData.getUint32(offset + fieldOffsets[fieldId], true); - } else if (fieldType === FieldSizeIdUChar) { - if (normalize) { - rawVertex[fieldId] = vertexData.getUint8(offset + fieldOffsets[fieldId]) / 255.0; - } else { - rawVertex[fieldId] = vertexData.getUint8(offset + fieldOffsets[fieldId]); - } - } - } - } -} - -const BaseFieldNamesToRead = ['scale_0', 'scale_1', 'scale_2', 'rot_0', 'rot_1', 'rot_2', 'rot_3', 'x', 'y', 'z', - 'f_dc_0', 'f_dc_1', 'f_dc_2', 'opacity', 'red', 'green', 'blue', 'f_rest_0']; - -const BaseFieldsToReadIndexes = BaseFieldNamesToRead.map((e, i) => i); - -const [ - SCALE_0, SCALE_1, SCALE_2, ROT_0, ROT_1, ROT_2, ROT_3, X, Y, Z, F_DC_0, F_DC_1, F_DC_2, OPACITY, RED$1, GREEN$1, BLUE$1, F_REST_0 - ] = BaseFieldsToReadIndexes; - -class INRIAV1PlyParser { - - static decodeHeaderLines(headerLines) { - - let shLineCount = 0; - headerLines.forEach((line) => { - if (line.includes('f_rest_')) shLineCount++; - }); - - let shFieldsToReadCount = 0; - if (shLineCount >= 45) { - shFieldsToReadCount = 45; - } else if (shLineCount >= 24) { - shFieldsToReadCount = 24; - } else if (shLineCount >= 9) { - shFieldsToReadCount = 9; - } - - const shFieldIndexesToMap = Array.from(Array(Math.max(shFieldsToReadCount - 1, 0))); - let shRemainingFieldNamesToRead = shFieldIndexesToMap.map((element, index) => `f_rest_${index + 1}`); - - const fieldNamesToRead = [...BaseFieldNamesToRead, ...shRemainingFieldNamesToRead]; - const fieldsToReadIndexes = fieldNamesToRead.map((e, i) => i); - - const fieldNameIdMap = fieldsToReadIndexes.reduce((acc, element) => { - acc[fieldNamesToRead[element]] = element; - return acc; - }, {}); - const header = PlyParserUtils.decodeSectionHeader(headerLines, fieldNameIdMap, 0); - header.splatCount = header.vertexCount; - header.bytesPerSplat = header.bytesPerVertex; - header.fieldsToReadIndexes = fieldsToReadIndexes; - return header; - } - - static decodeHeaderText(headerText) { - const headerLines = PlyParserUtils.convertHeaderTextToLines(headerText); - const header = INRIAV1PlyParser.decodeHeaderLines(headerLines); - header.headerText = headerText; - header.headerSizeBytes = headerText.indexOf(PlyParserUtils.HeaderEndToken) + PlyParserUtils.HeaderEndToken.length + 1; - return header; - } - - static decodeHeaderFromBuffer(plyBuffer) { - const headerText = PlyParserUtils.readHeaderFromBuffer(plyBuffer); - return INRIAV1PlyParser.decodeHeaderText(headerText); - } - - static findSplatData(plyBuffer, header) { - return new DataView(plyBuffer, header.headerSizeBytes); - } - - static parseToUncompressedSplatBufferSection(header, fromSplat, toSplat, splatData, splatDataOffset, - toBuffer, toOffset, outSphericalHarmonicsDegree = 0) { - outSphericalHarmonicsDegree = Math.min(outSphericalHarmonicsDegree, header.sphericalHarmonicsDegree); - const outBytesPerSplat = SplatBuffer.CompressionLevels[0].SphericalHarmonicsDegrees[outSphericalHarmonicsDegree].BytesPerSplat; - - for (let i = fromSplat; i <= toSplat; i++) { - const parsedSplat = INRIAV1PlyParser.parseToUncompressedSplat(splatData, i, header, - splatDataOffset, outSphericalHarmonicsDegree); - const outBase = i * outBytesPerSplat + toOffset; - SplatBuffer.writeSplatDataToSectionBuffer(parsedSplat, toBuffer, outBase, 0, outSphericalHarmonicsDegree); - } - } - - static parseToUncompressedSplatArraySection(header, fromSplat, toSplat, splatData, splatDataOffset, - splatArray, outSphericalHarmonicsDegree = 0) { - outSphericalHarmonicsDegree = Math.min(outSphericalHarmonicsDegree, header.sphericalHarmonicsDegree); - for (let i = fromSplat; i <= toSplat; i++) { - const parsedSplat = INRIAV1PlyParser.parseToUncompressedSplat(splatData, i, header, - splatDataOffset, outSphericalHarmonicsDegree); - splatArray.addSplat(parsedSplat); - } - } - - static decodeSectionSplatData(sectionSplatData, splatCount, sectionHeader, outSphericalHarmonicsDegree, toSplatArray = true) { - outSphericalHarmonicsDegree = Math.min(outSphericalHarmonicsDegree, sectionHeader.sphericalHarmonicsDegree); - if (toSplatArray) { - const splatArray = new UncompressedSplatArray(outSphericalHarmonicsDegree); - for (let row = 0; row < splatCount; row++) { - const newSplat = INRIAV1PlyParser.parseToUncompressedSplat(sectionSplatData, row, sectionHeader, - 0, outSphericalHarmonicsDegree); - splatArray.addSplat(newSplat); - } - return splatArray; - } else { - const { - splatBuffer, - splatBufferDataOffsetBytes - } = SplatBuffer.preallocateUncompressed(splatCount, outSphericalHarmonicsDegree); - INRIAV1PlyParser.parseToUncompressedSplatBufferSection( - sectionHeader, 0, splatCount - 1, sectionSplatData, 0, - splatBuffer.bufferData, splatBufferDataOffsetBytes, outSphericalHarmonicsDegree - ); - return splatBuffer; - } - } - - static parseToUncompressedSplat = function() { - - let rawSplat = []; - const tempRotation = new THREE.Quaternion(); - - const OFFSET_X = UncompressedSplatArray.OFFSET.X; - const OFFSET_Y = UncompressedSplatArray.OFFSET.Y; - const OFFSET_Z = UncompressedSplatArray.OFFSET.Z; - - const OFFSET_SCALE0 = UncompressedSplatArray.OFFSET.SCALE0; - const OFFSET_SCALE1 = UncompressedSplatArray.OFFSET.SCALE1; - const OFFSET_SCALE2 = UncompressedSplatArray.OFFSET.SCALE2; - - const OFFSET_ROTATION0 = UncompressedSplatArray.OFFSET.ROTATION0; - const OFFSET_ROTATION1 = UncompressedSplatArray.OFFSET.ROTATION1; - const OFFSET_ROTATION2 = UncompressedSplatArray.OFFSET.ROTATION2; - const OFFSET_ROTATION3 = UncompressedSplatArray.OFFSET.ROTATION3; - - const OFFSET_FDC0 = UncompressedSplatArray.OFFSET.FDC0; - const OFFSET_FDC1 = UncompressedSplatArray.OFFSET.FDC1; - const OFFSET_FDC2 = UncompressedSplatArray.OFFSET.FDC2; - const OFFSET_OPACITY = UncompressedSplatArray.OFFSET.OPACITY; - - const OFFSET_FRC = []; - - for (let i = 0; i < 45; i++) { - OFFSET_FRC[i] = UncompressedSplatArray.OFFSET.FRC0 + i; - } - - return function(splatData, row, header, splatDataOffset = 0, outSphericalHarmonicsDegree = 0) { - outSphericalHarmonicsDegree = Math.min(outSphericalHarmonicsDegree, header.sphericalHarmonicsDegree); - INRIAV1PlyParser.readSplat(splatData, header, row, splatDataOffset, rawSplat); - const newSplat = UncompressedSplatArray.createSplat(outSphericalHarmonicsDegree); - if (rawSplat[SCALE_0] !== undefined) { - newSplat[OFFSET_SCALE0] = Math.exp(rawSplat[SCALE_0]); - newSplat[OFFSET_SCALE1] = Math.exp(rawSplat[SCALE_1]); - newSplat[OFFSET_SCALE2] = Math.exp(rawSplat[SCALE_2]); - } else { - newSplat[OFFSET_SCALE0] = 0.01; - newSplat[OFFSET_SCALE1] = 0.01; - newSplat[OFFSET_SCALE2] = 0.01; - } - - if (rawSplat[F_DC_0] !== undefined) { - const SH_C0 = 0.28209479177387814; - newSplat[OFFSET_FDC0] = (0.5 + SH_C0 * rawSplat[F_DC_0]) * 255; - newSplat[OFFSET_FDC1] = (0.5 + SH_C0 * rawSplat[F_DC_1]) * 255; - newSplat[OFFSET_FDC2] = (0.5 + SH_C0 * rawSplat[F_DC_2]) * 255; - } else if (rawSplat[RED$1] !== undefined) { - newSplat[OFFSET_FDC0] = rawSplat[RED$1] * 255; - newSplat[OFFSET_FDC1] = rawSplat[GREEN$1] * 255; - newSplat[OFFSET_FDC2] = rawSplat[BLUE$1] * 255; - } else { - newSplat[OFFSET_FDC0] = 0; - newSplat[OFFSET_FDC1] = 0; - newSplat[OFFSET_FDC2] = 0; - } - - if (rawSplat[OPACITY] !== undefined) { - newSplat[OFFSET_OPACITY] = (1 / (1 + Math.exp(-rawSplat[OPACITY]))) * 255; - } - - newSplat[OFFSET_FDC0] = clamp(Math.floor(newSplat[OFFSET_FDC0]), 0, 255); - newSplat[OFFSET_FDC1] = clamp(Math.floor(newSplat[OFFSET_FDC1]), 0, 255); - newSplat[OFFSET_FDC2] = clamp(Math.floor(newSplat[OFFSET_FDC2]), 0, 255); - newSplat[OFFSET_OPACITY] = clamp(Math.floor(newSplat[OFFSET_OPACITY]), 0, 255); - - if (outSphericalHarmonicsDegree >= 1) { - if (rawSplat[F_REST_0] !== undefined) { - for (let i = 0; i < 9; i++) { - newSplat[OFFSET_FRC[i]] = rawSplat[header.sphericalHarmonicsDegree1Fields[i]]; - } - if (outSphericalHarmonicsDegree >= 2) { - for (let i = 0; i < 15; i++) { - newSplat[OFFSET_FRC[9 + i]] = rawSplat[header.sphericalHarmonicsDegree2Fields[i]]; - } - } - } - } - - tempRotation.set(rawSplat[ROT_0], rawSplat[ROT_1], rawSplat[ROT_2], rawSplat[ROT_3]); - tempRotation.normalize(); - - newSplat[OFFSET_ROTATION0] = tempRotation.x; - newSplat[OFFSET_ROTATION1] = tempRotation.y; - newSplat[OFFSET_ROTATION2] = tempRotation.z; - newSplat[OFFSET_ROTATION3] = tempRotation.w; - - newSplat[OFFSET_X] = rawSplat[X]; - newSplat[OFFSET_Y] = rawSplat[Y]; - newSplat[OFFSET_Z] = rawSplat[Z]; - - return newSplat; - }; - - }(); - - static readSplat(splatData, header, row, dataOffset, rawSplat) { - return PlyParserUtils.readVertex(splatData, header, row, dataOffset, header.fieldsToReadIndexes, rawSplat, true); - } - - static parseToUncompressedSplatArray(plyBuffer, outSphericalHarmonicsDegree = 0) { - const { header, splatCount, splatData } = separatePlyHeaderAndData(plyBuffer); - return INRIAV1PlyParser.decodeSectionSplatData(splatData, splatCount, header, outSphericalHarmonicsDegree, true); - } - - static parseToUncompressedSplatBuffer(plyBuffer, outSphericalHarmonicsDegree = 0) { - const { header, splatCount, splatData } = separatePlyHeaderAndData(plyBuffer); - return INRIAV1PlyParser.decodeSectionSplatData(splatData, splatCount, header, outSphericalHarmonicsDegree, false); - } -} - -function separatePlyHeaderAndData(plyBuffer) { - const header = INRIAV1PlyParser.decodeHeaderFromBuffer(plyBuffer); - const splatCount = header.splatCount; - const splatData = INRIAV1PlyParser.findSplatData(plyBuffer, header); - return { - header, - splatCount, - splatData - }; -} - -const CodeBookEntryNamesToRead = [ - 'features_dc', 'features_rest_0', 'features_rest_1', 'features_rest_2', 'features_rest_3', 'features_rest_4', 'features_rest_5', - 'features_rest_6', 'features_rest_7', 'features_rest_8', 'features_rest_9', 'features_rest_10', 'features_rest_11', 'features_rest_12', - 'features_rest_13', 'features_rest_14', 'opacity', 'scaling', 'rotation_re', 'rotation_im' -]; -const CodeBookEntriesToReadIndexes = CodeBookEntryNamesToRead.map((e, i) => i); - -const [ - CB_FEATURES_DC, CB_FEATURES_REST_0, CB_FEATURES_REST_3, CB_OPACITY, CB_SCALING, CB_ROTATION_RE, CB_ROTATION_IM - ] = [0, 1, 4, 16, 17, 18, 19]; - -const FieldNamesToRead = ['scale_0', 'scale_1', 'scale_2', 'rot_0', 'rot_1', 'rot_2', 'rot_3', - 'x', 'y', 'z', 'f_dc_0', 'f_dc_1', 'f_dc_2', 'opacity', 'red', 'green', 'blue', - 'f_rest_0', 'f_rest_1', 'f_rest_2', 'f_rest_3', 'f_rest_4', 'f_rest_5', 'f_rest_6', 'f_rest_7', 'f_rest_8', - 'f_rest_9', 'f_rest_10', 'f_rest_11', 'f_rest_12', 'f_rest_13', 'f_rest_14', 'f_rest_15', 'f_rest_16', - 'f_rest_17', 'f_rest_18', 'f_rest_19', 'f_rest_20', 'f_rest_21', 'f_rest_22', 'f_rest_23', 'f_rest_24', - 'f_rest_25', 'f_rest_26', 'f_rest_27', 'f_rest_28', 'f_rest_29', 'f_rest_30', 'f_rest_31', 'f_rest_32', - 'f_rest_33', 'f_rest_34', 'f_rest_35', 'f_rest_36', 'f_rest_37', 'f_rest_38', 'f_rest_39', 'f_rest_40', - 'f_rest_41', 'f_rest_42', 'f_rest_43', 'f_rest_44', 'f_rest_45' - ]; -const FieldsToReadIndexes = FieldNamesToRead.map((e, i) => i); - -const [ - PLY_SCALE_0, PLY_SCALE_1, PLY_SCALE_2, PLY_ROT_0, PLY_ROT_1, PLY_ROT_2, PLY_ROT_3, PLY_X, PLY_Y, PLY_Z, - PLY_F_DC_0, PLY_F_DC_1, PLY_F_DC_2, PLY_OPACITY, - ] = FieldsToReadIndexes; - -const PLY_RED = PLY_F_DC_0; -const PLY_GREEN = PLY_F_DC_1; -const PLY_BLUE = PLY_F_DC_2; - -const fromHalfFloat = (hf) =>{ - const t = (31744 & hf) >> 10; - const a = 1023 & hf; - return (hf >> 15 ? -1 : 1)*(t ? t === 31 ? a ? NaN : 1/0 : Math.pow(2, t - 15) *( 1 + a / 1024) : a / 1024*6103515625e-14); -}; - -class INRIAV2PlyParser { - - static decodeSectionHeadersFromHeaderLines(headerLines) { - const fieldNameIdMap = FieldsToReadIndexes.reduce((acc, element) => { - acc[FieldNamesToRead[element]] = element; - return acc; - }, {}); - - const codeBookEntriesToReadIdMap = CodeBookEntriesToReadIndexes.reduce((acc, element) => { - acc[CodeBookEntryNamesToRead[element]] = element; - return acc; - }, {}); - - const sectionNames = PlyParserUtils.getHeaderSectionNames(headerLines); - let codeBookSectionIndex; - for (let s = 0; s < sectionNames.length; s++) { - const sectionName = sectionNames[s]; - if (sectionName === 'codebook_centers') { - codeBookSectionIndex = s; - } - } - - let currentStartLine = 0; - let lastSectionFound = false; - const sectionHeaders = []; - let sectionIndex = 0; - while (!lastSectionFound) { - let sectionHeader; - if (sectionIndex === codeBookSectionIndex) { - sectionHeader = PlyParserUtils.decodeSectionHeader(headerLines, codeBookEntriesToReadIdMap, currentStartLine); - } else { - sectionHeader = PlyParserUtils.decodeSectionHeader(headerLines, fieldNameIdMap, currentStartLine); - } - lastSectionFound = sectionHeader.endOfHeader; - currentStartLine = sectionHeader.headerEndLine + 1; - if (!lastSectionFound) { - sectionHeader.splatCount = sectionHeader.vertexCount; - sectionHeader.bytesPerSplat = sectionHeader.bytesPerVertex; - } - sectionHeaders.push(sectionHeader); - sectionIndex++; - } - return sectionHeaders; - } - - static decodeSectionHeadersFromHeaderText(headerText) { - const headerLines = PlyParserUtils.convertHeaderTextToLines(headerText); - return INRIAV2PlyParser.decodeSectionHeadersFromHeaderLines(headerLines); - } - - static getSplatCountFromSectionHeaders(sectionHeaders) { - let splatCount = 0; - for (let sectionHeader of sectionHeaders) { - if (sectionHeader.sectionName !== 'codebook_centers') { - splatCount += sectionHeader.vertexCount; - } - } - return splatCount; - } - - static decodeHeaderFromHeaderText(headerText) { - const headerSizeBytes = headerText.indexOf(PlyParserUtils.HeaderEndToken) + PlyParserUtils.HeaderEndToken.length + 1; - const sectionHeaders = INRIAV2PlyParser.decodeSectionHeadersFromHeaderText(headerText); - const splatCount = INRIAV2PlyParser.getSplatCountFromSectionHeaders(sectionHeaders); - return { - 'headerSizeBytes': headerSizeBytes, - 'sectionHeaders': sectionHeaders, - 'splatCount': splatCount - }; - } - - static decodeHeaderFromBuffer(plyBuffer) { - const headerText = PlyParserUtils.readHeaderFromBuffer(plyBuffer); - return INRIAV2PlyParser.decodeHeaderFromHeaderText(headerText); - } - - static findVertexData(plyBuffer, header, targetSection) { - let byteOffset = header.headerSizeBytes; - for (let s = 0; s < targetSection && s < header.sectionHeaders.length; s++) { - const sectionHeader = header.sectionHeaders[s]; - byteOffset += sectionHeader.dataSizeBytes; - } - return new DataView(plyBuffer, byteOffset, header.sectionHeaders[targetSection].dataSizeBytes); - } - - static decodeCodeBook(codeBookData, sectionHeader) { - - const rawVertex = []; - const codeBook = []; - for (let row = 0; row < sectionHeader.vertexCount; row++) { - PlyParserUtils.readVertex(codeBookData, sectionHeader, row, 0, CodeBookEntriesToReadIndexes, rawVertex); - for (let index of CodeBookEntriesToReadIndexes) { - const codeBookElementOffset = CodeBookEntriesToReadIndexes[index]; - let codeBookPage = codeBook[codeBookElementOffset]; - if (!codeBookPage) { - codeBook[codeBookElementOffset] = codeBookPage = []; - } - codeBookPage.push(rawVertex[index]); - } - } - for (let page = 0; page < codeBook.length; page++) { - const codeBookPage = codeBook[page]; - const SH_C0 = 0.28209479177387814; - for (let i = 0; i < codeBookPage.length; i++) { - const baseValue = fromHalfFloat(codeBookPage[i]); - if (page === CB_OPACITY) { - codeBookPage[i] = Math.round((1 / (1 + Math.exp(-baseValue))) * 255); - } else if (page === CB_FEATURES_DC) { - codeBookPage[i] = Math.round((0.5 + SH_C0 * baseValue) * 255); - } else if (page === CB_SCALING) { - codeBookPage[i] = Math.exp(baseValue); - } else { - codeBookPage[i] = baseValue; - } - } - } - return codeBook; - } - - static decodeSectionSplatData(sectionSplatData, splatCount, sectionHeader, codeBook, outSphericalHarmonicsDegree) { - outSphericalHarmonicsDegree = Math.min(outSphericalHarmonicsDegree, sectionHeader.sphericalHarmonicsDegree); - const splatArray = new UncompressedSplatArray(outSphericalHarmonicsDegree); - for (let row = 0; row < splatCount; row++) { - const newSplat = INRIAV2PlyParser.parseToUncompressedSplat(sectionSplatData, row, sectionHeader, codeBook, - 0, outSphericalHarmonicsDegree); - splatArray.addSplat(newSplat); - } - return splatArray; - } - - static parseToUncompressedSplat = function() { - - let rawSplat = []; - const tempRotation = new THREE.Quaternion(); - - const OFFSET_X = UncompressedSplatArray.OFFSET.X; - const OFFSET_Y = UncompressedSplatArray.OFFSET.Y; - const OFFSET_Z = UncompressedSplatArray.OFFSET.Z; - - const OFFSET_SCALE0 = UncompressedSplatArray.OFFSET.SCALE0; - const OFFSET_SCALE1 = UncompressedSplatArray.OFFSET.SCALE1; - const OFFSET_SCALE2 = UncompressedSplatArray.OFFSET.SCALE2; - - const OFFSET_ROTATION0 = UncompressedSplatArray.OFFSET.ROTATION0; - const OFFSET_ROTATION1 = UncompressedSplatArray.OFFSET.ROTATION1; - const OFFSET_ROTATION2 = UncompressedSplatArray.OFFSET.ROTATION2; - const OFFSET_ROTATION3 = UncompressedSplatArray.OFFSET.ROTATION3; - - const OFFSET_FDC0 = UncompressedSplatArray.OFFSET.FDC0; - const OFFSET_FDC1 = UncompressedSplatArray.OFFSET.FDC1; - const OFFSET_FDC2 = UncompressedSplatArray.OFFSET.FDC2; - const OFFSET_OPACITY = UncompressedSplatArray.OFFSET.OPACITY; - - const OFFSET_FRC = []; - - for (let i = 0; i < 45; i++) { - OFFSET_FRC[i] = UncompressedSplatArray.OFFSET.FRC0 + i; - } - - return function(splatData, row, header, codeBook, splatDataOffset = 0, outSphericalHarmonicsDegree = 0) { - outSphericalHarmonicsDegree = Math.min(outSphericalHarmonicsDegree, header.sphericalHarmonicsDegree); - INRIAV2PlyParser.readSplat(splatData, header, row, splatDataOffset, rawSplat); - const newSplat = UncompressedSplatArray.createSplat(outSphericalHarmonicsDegree); - if (rawSplat[PLY_SCALE_0] !== undefined) { - newSplat[OFFSET_SCALE0] = codeBook[CB_SCALING][rawSplat[PLY_SCALE_0]]; - newSplat[OFFSET_SCALE1] = codeBook[CB_SCALING][rawSplat[PLY_SCALE_1]]; - newSplat[OFFSET_SCALE2] = codeBook[CB_SCALING][rawSplat[PLY_SCALE_2]]; - } else { - newSplat[OFFSET_SCALE0] = 0.01; - newSplat[OFFSET_SCALE1] = 0.01; - newSplat[OFFSET_SCALE2] = 0.01; - } - - if (rawSplat[PLY_F_DC_0] !== undefined) { - newSplat[OFFSET_FDC0] = codeBook[CB_FEATURES_DC][rawSplat[PLY_F_DC_0]]; - newSplat[OFFSET_FDC1] = codeBook[CB_FEATURES_DC][rawSplat[PLY_F_DC_1]]; - newSplat[OFFSET_FDC2] = codeBook[CB_FEATURES_DC][rawSplat[PLY_F_DC_2]]; - } else if (rawSplat[PLY_RED] !== undefined) { - newSplat[OFFSET_FDC0] = rawSplat[PLY_RED] * 255; - newSplat[OFFSET_FDC1] = rawSplat[PLY_GREEN] * 255; - newSplat[OFFSET_FDC2] = rawSplat[PLY_BLUE] * 255; - } else { - newSplat[OFFSET_FDC0] = 0; - newSplat[OFFSET_FDC1] = 0; - newSplat[OFFSET_FDC2] = 0; - } - - if (rawSplat[PLY_OPACITY] !== undefined) { - newSplat[OFFSET_OPACITY] = codeBook[CB_OPACITY][rawSplat[PLY_OPACITY]]; - } - - newSplat[OFFSET_FDC0] = clamp(Math.floor(newSplat[OFFSET_FDC0]), 0, 255); - newSplat[OFFSET_FDC1] = clamp(Math.floor(newSplat[OFFSET_FDC1]), 0, 255); - newSplat[OFFSET_FDC2] = clamp(Math.floor(newSplat[OFFSET_FDC2]), 0, 255); - newSplat[OFFSET_OPACITY] = clamp(Math.floor(newSplat[OFFSET_OPACITY]), 0, 255); - - if (outSphericalHarmonicsDegree >= 1 && header.sphericalHarmonicsDegree >= 1) { - for (let i = 0; i < 9; i++) { - const codeBookPage = codeBook[CB_FEATURES_REST_0 + i % 3]; - newSplat[OFFSET_FRC[i]] = codeBookPage[rawSplat[header.sphericalHarmonicsDegree1Fields[i]]]; - } - if (outSphericalHarmonicsDegree >= 2 && header.sphericalHarmonicsDegree >= 2) { - for (let i = 0; i < 15; i++) { - const codeBookPage = codeBook[CB_FEATURES_REST_3 + i % 5]; - newSplat[OFFSET_FRC[9 + i]] = codeBookPage[rawSplat[header.sphericalHarmonicsDegree2Fields[i]]]; - } - } - } - - const rot0 = codeBook[CB_ROTATION_RE][rawSplat[PLY_ROT_0]]; - const rot1 = codeBook[CB_ROTATION_IM][rawSplat[PLY_ROT_1]]; - const rot2 = codeBook[CB_ROTATION_IM][rawSplat[PLY_ROT_2]]; - const rot3 = codeBook[CB_ROTATION_IM][rawSplat[PLY_ROT_3]]; - tempRotation.set(rot0, rot1, rot2, rot3); - tempRotation.normalize(); - - newSplat[OFFSET_ROTATION0] = tempRotation.x; - newSplat[OFFSET_ROTATION1] = tempRotation.y; - newSplat[OFFSET_ROTATION2] = tempRotation.z; - newSplat[OFFSET_ROTATION3] = tempRotation.w; - - newSplat[OFFSET_X] = fromHalfFloat(rawSplat[PLY_X]); - newSplat[OFFSET_Y] = fromHalfFloat(rawSplat[PLY_Y]); - newSplat[OFFSET_Z] = fromHalfFloat(rawSplat[PLY_Z]); - - return newSplat; - }; - - }(); - - static readSplat(splatData, header, row, dataOffset, rawSplat) { - return PlyParserUtils.readVertex(splatData, header, row, dataOffset, FieldsToReadIndexes, rawSplat, false); - } - - static parseToUncompressedSplatArray(plyBuffer, outSphericalHarmonicsDegree = 0) { - const splatArrays = []; - const header = INRIAV2PlyParser.decodeHeaderFromBuffer(plyBuffer, outSphericalHarmonicsDegree); - let codeBook; - - for (let s = 0; s < header.sectionHeaders.length; s++) { - const sectionHeader = header.sectionHeaders[s]; - if (sectionHeader.sectionName === 'codebook_centers') { - const codeBookData = INRIAV2PlyParser.findVertexData(plyBuffer, header, s); - codeBook = INRIAV2PlyParser.decodeCodeBook(codeBookData, sectionHeader); - } - } - for (let s = 0; s < header.sectionHeaders.length; s++) { - const sectionHeader = header.sectionHeaders[s]; - if (sectionHeader.sectionName !== 'codebook_centers') { - const splatCount = sectionHeader.vertexCount; - const vertexData = INRIAV2PlyParser.findVertexData(plyBuffer, header, s); - const splatArray = INRIAV2PlyParser.decodeSectionSplatData(vertexData, splatCount, sectionHeader, - codeBook, outSphericalHarmonicsDegree); - splatArrays.push(splatArray); - } - } - - const unified = new UncompressedSplatArray(outSphericalHarmonicsDegree); - for (let splatArray of splatArrays) { - for (let splat of splatArray.splats) { - unified.addSplat(splat); - } - } - - return unified; - } -} - -class PlyParser { - - static parseToUncompressedSplatArray(plyBuffer, outSphericalHarmonicsDegree = 0) { - const plyFormat = PlyParserUtils.determineHeaderFormatFromPlyBuffer(plyBuffer); - if (plyFormat === PlyFormat.PlayCanvasCompressed) { - return PlayCanvasCompressedPlyParser.parseToUncompressedSplatArray(plyBuffer, outSphericalHarmonicsDegree); - } else if (plyFormat === PlyFormat.INRIAV1) { - return INRIAV1PlyParser.parseToUncompressedSplatArray(plyBuffer, outSphericalHarmonicsDegree); - } else if (plyFormat === PlyFormat.INRIAV2) { - return INRIAV2PlyParser.parseToUncompressedSplatArray(plyBuffer, outSphericalHarmonicsDegree); - } - } - - static parseToUncompressedSplatBuffer(plyBuffer, outSphericalHarmonicsDegree = 0) { - const plyFormat = PlyParserUtils.determineHeaderFormatFromPlyBuffer(plyBuffer); - if (plyFormat === PlyFormat.PlayCanvasCompressed) { - return PlayCanvasCompressedPlyParser.parseToUncompressedSplatBuffer(plyBuffer, outSphericalHarmonicsDegree); - } else if (plyFormat === PlyFormat.INRIAV1) { - return INRIAV1PlyParser.parseToUncompressedSplatBuffer(plyBuffer, outSphericalHarmonicsDegree); - } else if (plyFormat === PlyFormat.INRIAV2) { - // TODO: Implement! - throw new Error('parseToUncompressedSplatBuffer() is not implemented for INRIA V2 PLY files'); - } - } - -} - -class SplatPartitioner { - - constructor(sectionCount, sectionFilters, groupingParameters, partitionGenerator) { - this.sectionCount = sectionCount; - this.sectionFilters = sectionFilters; - this.groupingParameters = groupingParameters; - this.partitionGenerator = partitionGenerator; - } - - partitionUncompressedSplatArray(splatArray) { - let groupingParameters; - let sectionCount; - let sectionFilters; - if (this.partitionGenerator) { - const results = this.partitionGenerator(splatArray); - groupingParameters = results.groupingParameters; - sectionCount = results.sectionCount; - sectionFilters = results.sectionFilters; - } else { - groupingParameters = this.groupingParameters; - sectionCount = this.sectionCount; - sectionFilters = this.sectionFilters; - } - - const newArrays = []; - for (let s = 0; s < sectionCount; s++) { - const sectionSplats = new UncompressedSplatArray(splatArray.sphericalHarmonicsDegree); - const sectionFilter = sectionFilters[s]; - for (let i = 0; i < splatArray.splatCount; i++) { - if (sectionFilter(i)) { - sectionSplats.addSplat(splatArray.splats[i]); - } - } - newArrays.push(sectionSplats); - } - return { - splatArrays: newArrays, - parameters: groupingParameters - }; - } - - static getStandardPartitioner(partitionSize = 0, sceneCenter = new THREE.Vector3(), - blockSize = SplatBuffer.BucketBlockSize, bucketSize = SplatBuffer.BucketSize) { - - const partitionGenerator = (splatArray) => { - - const OFFSET_X = UncompressedSplatArray.OFFSET.X; - const OFFSET_Y = UncompressedSplatArray.OFFSET.Y; - const OFFSET_Z = UncompressedSplatArray.OFFSET.Z; - - if (partitionSize <= 0) partitionSize = splatArray.splatCount; - - const center = new THREE.Vector3(); - const clampDistance = 0.5; - const clampPoint = (point) => { - point.x = Math.floor(point.x / clampDistance) * clampDistance; - point.y = Math.floor(point.y / clampDistance) * clampDistance; - point.z = Math.floor(point.z / clampDistance) * clampDistance; - }; - splatArray.splats.forEach((splat) => { - center.set(splat[OFFSET_X], splat[OFFSET_Y], splat[OFFSET_Z]).sub(sceneCenter); - clampPoint(center); - splat.centerDist = center.lengthSq(); - }); - splatArray.splats.sort((a, b) => { - let centerADist = a.centerDist; - let centerBDist = b.centerDist; - if (centerADist > centerBDist) return 1; - else return -1; - }); - - const sectionFilters = []; - const groupingParameters = []; - partitionSize = Math.min(splatArray.splatCount, partitionSize); - const patitionCount = Math.ceil(splatArray.splatCount / partitionSize); - let currentStartSplat = 0; - for (let i = 0; i < patitionCount; i ++) { - let startSplat = currentStartSplat; - sectionFilters.push((splatIndex) => { - return splatIndex >= startSplat && splatIndex < startSplat + partitionSize; - }); - groupingParameters.push({ - 'blocksSize': blockSize, - 'bucketSize': bucketSize, - }); - currentStartSplat += partitionSize; - } - return { - 'sectionCount': sectionFilters.length, - sectionFilters, - groupingParameters - }; - }; - return new SplatPartitioner(undefined, undefined, undefined, partitionGenerator); - } -} - -class SplatBufferGenerator { - - constructor(splatPartitioner, alphaRemovalThreshold, compressionLevel, sectionSize, sceneCenter, blockSize, bucketSize) { - this.splatPartitioner = splatPartitioner; - this.alphaRemovalThreshold = alphaRemovalThreshold; - this.compressionLevel = compressionLevel; - this.sectionSize = sectionSize; - this.sceneCenter = sceneCenter ? new THREE.Vector3().copy(sceneCenter) : undefined; - this.blockSize = blockSize; - this.bucketSize = bucketSize; - } - - generateFromUncompressedSplatArray(splatArray) { - const partitionResults = this.splatPartitioner.partitionUncompressedSplatArray(splatArray); - return SplatBuffer.generateFromUncompressedSplatArrays(partitionResults.splatArrays, - this.alphaRemovalThreshold, this.compressionLevel, - this.sceneCenter, this.blockSize, this.bucketSize, - partitionResults.parameters); - } - - static getStandardGenerator(alphaRemovalThreshold = 1, compressionLevel = 1, sectionSize = 0, sceneCenter = new THREE.Vector3(), - blockSize = SplatBuffer.BucketBlockSize, bucketSize = SplatBuffer.BucketSize) { - const splatPartitioner = SplatPartitioner.getStandardPartitioner(sectionSize, sceneCenter, blockSize, bucketSize); - return new SplatBufferGenerator(splatPartitioner, alphaRemovalThreshold, compressionLevel, - sectionSize, sceneCenter, blockSize, bucketSize); - } -} - -const LoaderStatus = { - 'Downloading': 0, - 'Processing': 1, - 'Done': 2 -}; - -class DirectLoadError extends Error { - - constructor(msg) { - super(msg); - } - -} - -const InternalLoadType = { - ProgressiveToSplatBuffer: 0, - ProgressiveToSplatArray: 1, - DownloadBeforeProcessing: 2 -}; - -function storeChunksInBuffer(chunks, buffer) { - let inBytes = 0; - for (let chunk of chunks) inBytes += chunk.sizeBytes; - - if (!buffer || buffer.byteLength < inBytes) { - buffer = new ArrayBuffer(inBytes); - } - - let offset = 0; - for (let chunk of chunks) { - new Uint8Array(buffer, offset, chunk.sizeBytes).set(chunk.data); - offset += chunk.sizeBytes; - } - - return buffer; -} - -function finalize$1(splatData, optimizeSplatData, minimumAlpha, compressionLevel, sectionSize, sceneCenter, blockSize, bucketSize) { - if (optimizeSplatData) { - const splatBufferGenerator = SplatBufferGenerator.getStandardGenerator(minimumAlpha, compressionLevel, - sectionSize, sceneCenter, - blockSize, bucketSize); - return splatBufferGenerator.generateFromUncompressedSplatArray(splatData); - } else { - return SplatBuffer.generateFromUncompressedSplatArrays([splatData], minimumAlpha, 0, new THREE.Vector3()); - } -} - -class PlyLoader { - - static loadFromURL(fileName, onProgress, progressiveLoadToSplatBuffer, onProgressiveLoadSectionProgress, - minimumAlpha, compressionLevel, optimizeSplatData = true, outSphericalHarmonicsDegree = 0, - headers, sectionSize, sceneCenter, blockSize, bucketSize) { - - let internalLoadType; - if (!progressiveLoadToSplatBuffer && !optimizeSplatData) { - internalLoadType = InternalLoadType.DownloadBeforeProcessing; - } else { - if (optimizeSplatData) internalLoadType = InternalLoadType.ProgressiveToSplatArray; - else internalLoadType = InternalLoadType.ProgressiveToSplatBuffer; - } - - const directLoadSectionSizeBytes = Constants.ProgressiveLoadSectionSize; - const splatBufferDataOffsetBytes = SplatBuffer.HeaderSizeBytes + SplatBuffer.SectionHeaderSizeBytes; - const sectionCount = 1; - - let plyFormat; - let directLoadBufferIn; - let directLoadBufferOut; - let directLoadSplatBuffer; - let compressedPlyHeaderChunksBuffer; - let maxSplatCount = 0; - let processedBaseSplatCount = 0; - let processedSphericalHarmonicsSplatCount = 0; - - let headerLoaded = false; - let readyToLoadSplatData = false; - let baseSplatDataLoaded = false; - - const loadPromise = nativePromiseWithExtractedComponents(); - - let numBytesStreamed = 0; - let numBytesParsed = 0; - let numBytesDownloaded = 0; - let endOfBaseSplatDataBytes = 0; - let headerText = ''; - let header = null; - let chunks = []; - - let standardLoadUncompressedSplatArray; - - const textDecoder = new TextDecoder(); - - const localOnProgress = (percent, percentLabel, chunkData) => { - const loadComplete = percent >= 100; - - if (chunkData) { - chunks.push({ - 'data': chunkData, - 'sizeBytes': chunkData.byteLength, - 'startBytes': numBytesDownloaded, - 'endBytes': numBytesDownloaded + chunkData.byteLength - }); - numBytesDownloaded += chunkData.byteLength; - } - - if (internalLoadType === InternalLoadType.DownloadBeforeProcessing) { - if (loadComplete) { - loadPromise.resolve(chunks); - } - } else { - if (!headerLoaded) { - headerText += textDecoder.decode(chunkData); - if (PlyParserUtils.checkTextForEndHeader(headerText)) { - plyFormat = PlyParserUtils.determineHeaderFormatFromHeaderText(headerText); - if (plyFormat === PlyFormat.INRIAV1) { - header = INRIAV1PlyParser.decodeHeaderText(headerText); - outSphericalHarmonicsDegree = Math.min(outSphericalHarmonicsDegree, header.sphericalHarmonicsDegree); - maxSplatCount = header.splatCount; - readyToLoadSplatData = true; - endOfBaseSplatDataBytes = header.headerSizeBytes + header.bytesPerSplat * maxSplatCount; - } else if (plyFormat === PlyFormat.PlayCanvasCompressed) { - header = PlayCanvasCompressedPlyParser.decodeHeaderText(headerText); - outSphericalHarmonicsDegree = Math.min(outSphericalHarmonicsDegree, header.sphericalHarmonicsDegree); - if (internalLoadType === InternalLoadType.ProgressiveToSplatBuffer && outSphericalHarmonicsDegree > 0) { - throw new DirectLoadError( - 'PlyLoader.loadFromURL() -> Selected PLY format has spherical ' + - 'harmonics data that cannot be progressively loaded.' - ); - } - maxSplatCount = header.vertexElement.count; - endOfBaseSplatDataBytes = header.headerSizeBytes + header.bytesPerSplat * maxSplatCount + - header.chunkElement.storageSizeBytes; - } else { - if (internalLoadType === InternalLoadType.ProgressiveToSplatBuffer) { - throw new DirectLoadError( - 'PlyLoader.loadFromURL() -> Selected PLY format cannot be progressively loaded.' - ); - } else { - internalLoadType = InternalLoadType.DownloadBeforeProcessing; - return; - } - } - - if (internalLoadType === InternalLoadType.ProgressiveToSplatBuffer) { - const shDescriptor = SplatBuffer.CompressionLevels[0].SphericalHarmonicsDegrees[outSphericalHarmonicsDegree]; - const splatBufferSizeBytes = splatBufferDataOffsetBytes + shDescriptor.BytesPerSplat * maxSplatCount; - directLoadBufferOut = new ArrayBuffer(splatBufferSizeBytes); - SplatBuffer.writeHeaderToBuffer({ - versionMajor: SplatBuffer.CurrentMajorVersion, - versionMinor: SplatBuffer.CurrentMinorVersion, - maxSectionCount: sectionCount, - sectionCount: sectionCount, - maxSplatCount: maxSplatCount, - splatCount: 0, - compressionLevel: 0, - sceneCenter: new THREE.Vector3() - }, directLoadBufferOut); - } else { - standardLoadUncompressedSplatArray = new UncompressedSplatArray(outSphericalHarmonicsDegree); - } - - numBytesStreamed = header.headerSizeBytes; - numBytesParsed = header.headerSizeBytes; - headerLoaded = true; - } - } else if (plyFormat === PlyFormat.PlayCanvasCompressed && !readyToLoadSplatData) { - const sizeRequiredForHeaderAndChunks = header.headerSizeBytes + header.chunkElement.storageSizeBytes; - compressedPlyHeaderChunksBuffer = storeChunksInBuffer(chunks, compressedPlyHeaderChunksBuffer); - if (compressedPlyHeaderChunksBuffer.byteLength >= sizeRequiredForHeaderAndChunks) { - PlayCanvasCompressedPlyParser.readElementData(header.chunkElement, compressedPlyHeaderChunksBuffer, - header.headerSizeBytes); - numBytesStreamed = sizeRequiredForHeaderAndChunks; - numBytesParsed = sizeRequiredForHeaderAndChunks; - readyToLoadSplatData = true; - } - } - - if (headerLoaded && readyToLoadSplatData && chunks.length > 0) { - - directLoadBufferIn = storeChunksInBuffer(chunks, directLoadBufferIn); - - const bytesLoadedSinceLastStreamedSection = numBytesDownloaded - numBytesStreamed; - if (bytesLoadedSinceLastStreamedSection > directLoadSectionSizeBytes || - (numBytesDownloaded >= endOfBaseSplatDataBytes && !baseSplatDataLoaded) || - loadComplete) { - const bytesPerSplat = baseSplatDataLoaded ? header.sphericalHarmonicsPerSplat : header.bytesPerSplat; - const endOfBytesToProcess = baseSplatDataLoaded ? numBytesDownloaded : - Math.min(endOfBaseSplatDataBytes, numBytesDownloaded); - const numBytesToProcess = endOfBytesToProcess - numBytesParsed; - const addedSplatCount = Math.floor(numBytesToProcess / bytesPerSplat); - const numBytesToParse = addedSplatCount * bytesPerSplat; - const numBytesLeftOver = numBytesDownloaded - numBytesParsed - numBytesToParse; - const parsedDataViewOffset = numBytesParsed - chunks[0].startBytes; - const dataToParse = new DataView(directLoadBufferIn, parsedDataViewOffset, numBytesToParse); - - if (!baseSplatDataLoaded) { - if (internalLoadType === InternalLoadType.ProgressiveToSplatBuffer) { - const shDesc = SplatBuffer.CompressionLevels[0].SphericalHarmonicsDegrees[outSphericalHarmonicsDegree]; - const outOffset = processedBaseSplatCount * shDesc.BytesPerSplat + splatBufferDataOffsetBytes; - if (plyFormat === PlyFormat.PlayCanvasCompressed) { - PlayCanvasCompressedPlyParser.parseToUncompressedSplatBufferSection( - header.chunkElement, header.vertexElement, 0, addedSplatCount - 1, - processedBaseSplatCount, dataToParse, directLoadBufferOut, outOffset - ); - } else { - INRIAV1PlyParser.parseToUncompressedSplatBufferSection( - header, 0, addedSplatCount - 1, dataToParse, 0, - directLoadBufferOut, outOffset, outSphericalHarmonicsDegree - ); - } - } else { - if (plyFormat === PlyFormat.PlayCanvasCompressed) { - PlayCanvasCompressedPlyParser.parseToUncompressedSplatArraySection( - header.chunkElement, header.vertexElement, 0, addedSplatCount - 1, - processedBaseSplatCount, dataToParse, standardLoadUncompressedSplatArray - ); - } else { - INRIAV1PlyParser.parseToUncompressedSplatArraySection( - header, 0, addedSplatCount - 1, dataToParse, 0, - standardLoadUncompressedSplatArray, outSphericalHarmonicsDegree - ); - } - } - - processedBaseSplatCount += addedSplatCount; - - if (internalLoadType === InternalLoadType.ProgressiveToSplatBuffer) { - if (!directLoadSplatBuffer) { - SplatBuffer.writeSectionHeaderToBuffer({ - maxSplatCount: maxSplatCount, - splatCount: processedBaseSplatCount, - bucketSize: 0, - bucketCount: 0, - bucketBlockSize: 0, - compressionScaleRange: 0, - storageSizeBytes: 0, - fullBucketCount: 0, - partiallyFilledBucketCount: 0, - sphericalHarmonicsDegree: outSphericalHarmonicsDegree - }, 0, directLoadBufferOut, SplatBuffer.HeaderSizeBytes); - directLoadSplatBuffer = new SplatBuffer(directLoadBufferOut, false); - } - directLoadSplatBuffer.updateLoadedCounts(1, processedBaseSplatCount); - } - if (numBytesDownloaded >= endOfBaseSplatDataBytes) { - baseSplatDataLoaded = true; - } - } else { - if (plyFormat === PlyFormat.PlayCanvasCompressed) { - if (internalLoadType === InternalLoadType.ProgressiveToSplatArray) { - PlayCanvasCompressedPlyParser.parseSphericalHarmonicsToUncompressedSplatArraySection( - header.chunkElement, header.shElement, processedSphericalHarmonicsSplatCount, - processedSphericalHarmonicsSplatCount + addedSplatCount - 1, - dataToParse, 0, outSphericalHarmonicsDegree, header.sphericalHarmonicsDegree, - standardLoadUncompressedSplatArray - ); - processedSphericalHarmonicsSplatCount += addedSplatCount; - } - } - } - - if (numBytesLeftOver === 0) { - chunks = []; - } else { - let keepChunks = []; - let keepSize = 0; - for (let i = chunks.length - 1; i >= 0; i--) { - const chunk = chunks[i]; - keepSize += chunk.sizeBytes; - keepChunks.unshift(chunk); - if (keepSize >= numBytesLeftOver) break; - } - chunks = keepChunks; - } - - numBytesStreamed += directLoadSectionSizeBytes; - numBytesParsed += numBytesToParse; - } - } - - if (onProgressiveLoadSectionProgress && directLoadSplatBuffer) { - onProgressiveLoadSectionProgress(directLoadSplatBuffer, loadComplete); - } - - if (loadComplete) { - if (internalLoadType === InternalLoadType.ProgressiveToSplatBuffer) { - loadPromise.resolve(directLoadSplatBuffer); - } else { - loadPromise.resolve(standardLoadUncompressedSplatArray); - } - } - } - - if (onProgress) onProgress(percent, percentLabel, LoaderStatus.Downloading); - }; - - if (onProgress) onProgress(0, '0%', LoaderStatus.Downloading); - return fetchWithProgress(fileName, localOnProgress, false, headers).then(() => { - if (onProgress) onProgress(0, '0%', LoaderStatus.Processing); - return loadPromise.promise.then((splatData) => { - if (onProgress) onProgress(100, '100%', LoaderStatus.Done); - if (internalLoadType === InternalLoadType.DownloadBeforeProcessing) { - const chunkDatas = chunks.map((chunk) => chunk.data); - return new Blob(chunkDatas).arrayBuffer().then((plyFileData) => { - return PlyLoader.loadFromFileData(plyFileData, minimumAlpha, compressionLevel, optimizeSplatData, - outSphericalHarmonicsDegree, sectionSize, sceneCenter, blockSize, bucketSize); - }); - } else if (internalLoadType === InternalLoadType.ProgressiveToSplatBuffer) { - return splatData; - } else { - return delayedExecute(() => { - return finalize$1(splatData, optimizeSplatData, minimumAlpha, compressionLevel, - sectionSize, sceneCenter, blockSize, bucketSize); - }); - } - }); - }); - } - - static loadFromFileData(plyFileData, minimumAlpha, compressionLevel, optimizeSplatData, outSphericalHarmonicsDegree = 0, - sectionSize, sceneCenter, blockSize, bucketSize) { - if (optimizeSplatData) { - return delayedExecute(() => { - return PlyParser.parseToUncompressedSplatArray(plyFileData, outSphericalHarmonicsDegree); - }) - .then((splatArray) => { - return finalize$1(splatArray, optimizeSplatData, minimumAlpha, compressionLevel, - sectionSize, sceneCenter, blockSize, bucketSize); - }); - } else { - return delayedExecute(() => { - return PlyParser.parseToUncompressedSplatBuffer(plyFileData, outSphericalHarmonicsDegree); - }); - } - } -} - -const createStream = (data)=> { - return new ReadableStream({ - async start(controller) { - controller.enqueue(data); - controller.close(); - }, - }); -}; - -async function decompressGzipped(data) { - try { - const stream = createStream(data); - if (!stream) throw new Error('Failed to create stream from data'); - - return await decompressGzipStream(stream); - } catch (error) { - console.error('Error decompressing gzipped data:', error); - throw error; - } -} - -async function decompressGzipStream(stream) { - const decompressedStream = stream.pipeThrough(new DecompressionStream('gzip')); - const response = new Response(decompressedStream); - const buffer = await response.arrayBuffer(); - - return new Uint8Array(buffer); -} - -async function compressGzipped(data) { - try { - const stream = createStream(data); - const compressedStream = stream.pipeThrough(new CompressionStream('gzip')); - const response = new Response(compressedStream); - const buffer = await response.arrayBuffer(); - - return new Uint8Array(buffer); - } catch (error) { - console.error('Error compressing gzipped data:', error); - throw error; - } -} - -const SPZ_MAGIC = 1347635022; -const FLAG_ANTIALIASED = 1; -const COLOR_SCALE = 0.15; - -function halfToFloat(h) { - const sgn = (h >> 15) & 0x1; - const exponent = (h >> 10) & 0x1f; - const mantissa = h & 0x3ff; - - const signMul = sgn === 1 ? -1.0 : 1.0; - if (exponent === 0) { - return signMul * Math.pow(2, -14) * mantissa / 1024; - } - - if (exponent === 31) { - return mantissa !== 0 ? NaN : signMul * Infinity; - } - - return signMul * Math.pow(2, exponent - 15) * (1 + mantissa / 1024); -} - -function unquantizeSH(x) { - return (x - 128.0) / 128.0; -} - -function dimForDegree(degree) { - switch (degree) { - case 0: return 0; - case 1: return 3; - case 2: return 8; - case 3: return 15; - default: - console.error(`[SPZ: ERROR] Unsupported SH degree: ${degree}`); - return 0; - } -} - -const unpackedSplatToUncompressedSplat = function() { - - let rawSplat = []; - const tempRotation = new THREE.Quaternion(); - - const OFFSET_X = UncompressedSplatArray.OFFSET.X; - const OFFSET_Y = UncompressedSplatArray.OFFSET.Y; - const OFFSET_Z = UncompressedSplatArray.OFFSET.Z; - - const OFFSET_SCALE0 = UncompressedSplatArray.OFFSET.SCALE0; - const OFFSET_SCALE1 = UncompressedSplatArray.OFFSET.SCALE1; - const OFFSET_SCALE2 = UncompressedSplatArray.OFFSET.SCALE2; - - const OFFSET_ROTATION0 = UncompressedSplatArray.OFFSET.ROTATION0; - const OFFSET_ROTATION1 = UncompressedSplatArray.OFFSET.ROTATION1; - const OFFSET_ROTATION2 = UncompressedSplatArray.OFFSET.ROTATION2; - const OFFSET_ROTATION3 = UncompressedSplatArray.OFFSET.ROTATION3; - - const OFFSET_FDC0 = UncompressedSplatArray.OFFSET.FDC0; - const OFFSET_FDC1 = UncompressedSplatArray.OFFSET.FDC1; - const OFFSET_FDC2 = UncompressedSplatArray.OFFSET.FDC2; - const OFFSET_OPACITY = UncompressedSplatArray.OFFSET.OPACITY; - - const OFFSET_FRC = []; - - for (let i = 0; i < 45; i++) { - OFFSET_FRC[i] = UncompressedSplatArray.OFFSET.FRC0 + i; - } - - const shCoeffMap = [dimForDegree(0), dimForDegree(1), dimForDegree(2), dimForDegree(3)]; - - const shIndexMap = [ - 0, 1, 2, 9, 10, 11, 12, 13, 24, 25, 26, 27, 28, 29, 30, - 3, 4, 5, 14, 15, 16, 17, 18, 31, 32, 33, 34, 35, 36, 37, - 6, 7, 8, 19, 20, 21, 22, 23, 38, 39, 40, 41, 42, 43, 44 - ]; - - return function(unpackedSplat, unpackedSphericalHarmonicsDegree, outSphericalHarmonicsDegree) { - outSphericalHarmonicsDegree = Math.min(unpackedSphericalHarmonicsDegree, outSphericalHarmonicsDegree); - - const newSplat = UncompressedSplatArray.createSplat(outSphericalHarmonicsDegree); - if (unpackedSplat.scale[0] !== undefined) { - newSplat[OFFSET_SCALE0] = unpackedSplat.scale[0]; - newSplat[OFFSET_SCALE1] = unpackedSplat.scale[1]; - newSplat[OFFSET_SCALE2] = unpackedSplat.scale[2]; - } else { - newSplat[OFFSET_SCALE0] = 0.01; - newSplat[OFFSET_SCALE1] = 0.01; - newSplat[OFFSET_SCALE2] = 0.01; - } - - if (unpackedSplat.color[0] !== undefined) { - newSplat[OFFSET_FDC0] = unpackedSplat.color[0]; - newSplat[OFFSET_FDC1] = unpackedSplat.color[1]; - newSplat[OFFSET_FDC2] = unpackedSplat.color[2]; - } else if (rawSplat[RED] !== undefined) { - newSplat[OFFSET_FDC0] = rawSplat[RED] * 255; - newSplat[OFFSET_FDC1] = rawSplat[GREEN] * 255; - newSplat[OFFSET_FDC2] = rawSplat[BLUE] * 255; - } else { - newSplat[OFFSET_FDC0] = 0; - newSplat[OFFSET_FDC1] = 0; - newSplat[OFFSET_FDC2] = 0; - } - - if (unpackedSplat.alpha !== undefined) { - newSplat[OFFSET_OPACITY] = unpackedSplat.alpha; - } - - newSplat[OFFSET_FDC0] = clamp(Math.floor(newSplat[OFFSET_FDC0]), 0, 255); - newSplat[OFFSET_FDC1] = clamp(Math.floor(newSplat[OFFSET_FDC1]), 0, 255); - newSplat[OFFSET_FDC2] = clamp(Math.floor(newSplat[OFFSET_FDC2]), 0, 255); - newSplat[OFFSET_OPACITY] = clamp(Math.floor(newSplat[OFFSET_OPACITY]), 0, 255); - - let outSHCoeff = shCoeffMap[outSphericalHarmonicsDegree]; - let readSHCoeff = shCoeffMap[unpackedSphericalHarmonicsDegree]; - for (let j = 0; j < 3; ++j) { - for (let k = 0; k < 15; ++k) { - const outIndex = shIndexMap[j * 15 + k]; - if (k < outSHCoeff && k < readSHCoeff) { - newSplat[UncompressedSplatArray.OFFSET.FRC0 + outIndex] = unpackedSplat.sh[j * readSHCoeff + k]; - } - } - } - - tempRotation.set(unpackedSplat.rotation[3], unpackedSplat.rotation[0], unpackedSplat.rotation[1], unpackedSplat.rotation[2]); - tempRotation.normalize(); - - newSplat[OFFSET_ROTATION0] = tempRotation.x; - newSplat[OFFSET_ROTATION1] = tempRotation.y; - newSplat[OFFSET_ROTATION2] = tempRotation.z; - newSplat[OFFSET_ROTATION3] = tempRotation.w; - - newSplat[OFFSET_X] = unpackedSplat.position[0]; - newSplat[OFFSET_Y] = unpackedSplat.position[1]; - newSplat[OFFSET_Z] = unpackedSplat.position[2]; - - return newSplat; - }; - -}(); - -// Helper function to check sizes (matching C++ checkSizes function) -function checkSizes2(packed, numPoints, shDim, usesFloat16) { - if (packed.positions.length !== numPoints * 3 * (usesFloat16 ? 2 : 3)) return false; - if (packed.scales.length !== numPoints * 3) return false; - if (packed.rotations.length !== numPoints * 3) return false; - if (packed.alphas.length !== numPoints) return false; - if (packed.colors.length !== numPoints * 3) return false; - if (packed.sh.length !== numPoints * shDim * 3) return false; - return true; -} - -function unpackGaussians(packed, outSphericalHarmonicsDegree, directToSplatBuffer, outTarget, outTargetOffset) { - outSphericalHarmonicsDegree = Math.min(outSphericalHarmonicsDegree, packed.shDegree); - const numPoints = packed.numPoints; - const shDim = dimForDegree(packed.shDegree); - const usesFloat16 = packed.positions.length === numPoints * 3 * 2; - - // Validate sizes - if (!checkSizes2(packed, numPoints, shDim, usesFloat16)) { - return null; - } - - const splat = { - position: [], - scale: [], - rotation: [], - alpha: undefined, - color: [], - sh: [] - }; - - let halfData; - if (usesFloat16) { - halfData = new Uint16Array(packed.positions.buffer, packed.positions.byteOffset, numPoints * 3); - } - const fullPrecisionPositionScale = 1.0 / (1 << packed.fractionalBits); - const shCoeffPerChannelPerSplat = dimForDegree(packed.shDegree); - const SH_C0 = 0.28209479177387814; - - for (let i = 0; i < numPoints; i++) { - // Splat position - if (usesFloat16) { - // Decode legacy float16 format - for (let j = 0; j < 3; j++) { - splat.position[j] = halfToFloat(halfData[i * 3 + j]); - } - } else { - // Decode 24-bit fixed point coordinates - for (let j = 0; j < 3; j++) { - const base = i * 9 + j * 3; - let fixed32 = packed.positions[base]; - fixed32 |= packed.positions[base + 1] << 8; - fixed32 |= packed.positions[base + 2] << 16; - fixed32 |= (fixed32 & 0x800000) ? 0xff000000 : 0; - splat.position[j] = fixed32 * fullPrecisionPositionScale; - } - } - - // Splat scale - for (let j = 0; j < 3; j++) { - splat.scale[j] = Math.exp(packed.scales[i * 3 + j] / 16.0 - 10.0); - } - - // Splat rotation - const r = packed.rotations.subarray(i * 3, i * 3 + 3); - const xyz = [ - r[0] / 127.5 - 1.0, - r[1] / 127.5 - 1.0, - r[2] / 127.5 - 1.0 - ]; - splat.rotation[0] = xyz[0]; - splat.rotation[1] = xyz[1]; - splat.rotation[2] = xyz[2]; - const squaredNorm = xyz[0] * xyz[0] + xyz[1] * xyz[1] + xyz[2] * xyz[2]; - splat.rotation[3] = Math.sqrt(Math.max(0.0, 1.0 - squaredNorm)); - - // Splat alpha - // splat.alpha = invSigmoid(packed.alphas[i] / 255.0); - splat.alpha = Math.floor(packed.alphas[i]); - - // Splat color - for (let j = 0; j < 3; j++) { - splat.color[j] = Math.floor(((((packed.colors[i * 3 + j] / 255.0) - 0.5) / COLOR_SCALE) * SH_C0 + 0.5) * 255); - } - - // Splat spherical harmonics - for (let j = 0; j < 3; j++) { - for (let k = 0; k < shCoeffPerChannelPerSplat; k++) { - splat.sh[j * shCoeffPerChannelPerSplat + k] = unquantizeSH(packed.sh[shCoeffPerChannelPerSplat * 3 * i + k * 3 + j]); - } - } - - const uncompressedSplat = unpackedSplatToUncompressedSplat(splat, packed.shDegree, outSphericalHarmonicsDegree); - if (directToSplatBuffer) { - const outBytesPerSplat = SplatBuffer.CompressionLevels[0].SphericalHarmonicsDegrees[outSphericalHarmonicsDegree].BytesPerSplat; - const outBase = i * outBytesPerSplat + outTargetOffset; - SplatBuffer.writeSplatDataToSectionBuffer(uncompressedSplat, outTarget, outBase, 0, outSphericalHarmonicsDegree); - } else { - outTarget.addSplat(uncompressedSplat); - } - } -} - -const HEADER_SIZE = 16; // 4 + 4 + 4 + 1 + 1 + 1 + 1 bytes -const MAX_POINTS_TO_READ = 10000000; - -function deserializePackedGaussians(buffer) { - const view = new DataView(buffer); - let offset = 0; - - // Read and validate header - const header = { - magic: view.getUint32(offset, true), - version: view.getUint32(offset + 4, true), - numPoints: view.getUint32(offset + 8, true), - shDegree: view.getUint8(offset + 12), - fractionalBits: view.getUint8(offset + 13), - flags: view.getUint8(offset + 14), - reserved: view.getUint8(offset + 15) - }; - - offset += HEADER_SIZE; - - // Validate header - if (header.magic !== SPZ_MAGIC) { - console.error('[SPZ ERROR] deserializePackedGaussians: header not found'); - return null; - } - if (header.version < 1 || header.version > 2) { - console.error(`[SPZ ERROR] deserializePackedGaussians: version not supported: ${header.version}`); - return null; - } - if (header.numPoints > MAX_POINTS_TO_READ) { - console.error(`[SPZ ERROR] deserializePackedGaussians: Too many points: ${header.numPoints}`); - return null; - } - if (header.shDegree > 3) { - console.error(`[SPZ ERROR] deserializePackedGaussians: Unsupported SH degree: ${header.shDegree}`); - return null; - } - - const numPoints = header.numPoints; - const shDim = dimForDegree(header.shDegree); - const usesFloat16 = header.version === 1; - - // Initialize result object - const result = { - numPoints, - shDegree: header.shDegree, - fractionalBits: header.fractionalBits, - antialiased: (header.flags & FLAG_ANTIALIASED) !== 0, - positions: new Uint8Array(numPoints * 3 * (usesFloat16 ? 2 : 3)), - scales: new Uint8Array(numPoints * 3), - rotations: new Uint8Array(numPoints * 3), - alphas: new Uint8Array(numPoints), - colors: new Uint8Array(numPoints * 3), - sh: new Uint8Array(numPoints * shDim * 3) - }; - - // Read data sections - try { - const uint8View = new Uint8Array(buffer); - let positionsSize = result.positions.length; - let currentOffset = offset; - - result.positions.set(uint8View.slice(currentOffset, currentOffset + positionsSize)); - currentOffset += positionsSize; - - result.alphas.set(uint8View.slice(currentOffset, currentOffset + result.alphas.length)); - currentOffset += result.alphas.length; - - result.colors.set(uint8View.slice(currentOffset, currentOffset + result.colors.length)); - currentOffset += result.colors.length; - - result.scales.set(uint8View.slice(currentOffset, currentOffset + result.scales.length)); - currentOffset += result.scales.length; - - result.rotations.set(uint8View.slice(currentOffset, currentOffset + result.rotations.length)); - currentOffset += result.rotations.length; - - result.sh.set(uint8View.slice(currentOffset, currentOffset + result.sh.length)); - - // Verify we read the expected amount of data - if (currentOffset + result.sh.length !== buffer.byteLength) { - console.error('[SPZ ERROR] deserializePackedGaussians: incorrect buffer size'); - return null; - } - } catch (error) { - console.error('[SPZ ERROR] deserializePackedGaussians: read error', error); - return null; - } - - return result; -} - -async function loadSpzPacked(compressedData) { - try { - const decompressed = await decompressGzipped(compressedData); - return deserializePackedGaussians(decompressed.buffer); - } catch (error) { - console.error('[SPZ ERROR] loadSpzPacked: decompression error', error); - return null; - } -} - -class SpzLoader { - - static loadFromURL(fileName, onProgress, minimumAlpha, compressionLevel, optimizeSplatData = true, - outSphericalHarmonicsDegree = 0, headers, sectionSize, sceneCenter, blockSize, bucketSize) { - if (onProgress) onProgress(0, '0%', LoaderStatus.Downloading); - return fetchWithProgress(fileName, onProgress, true, headers).then((fileData) => { - if (onProgress) onProgress(0, '0%', LoaderStatus.Processing); - return SpzLoader.loadFromFileData(fileData, minimumAlpha, compressionLevel, optimizeSplatData, - outSphericalHarmonicsDegree, sectionSize, sceneCenter, blockSize, bucketSize); - }); - } - - static async loadFromFileData(spzFileData, minimumAlpha, compressionLevel, optimizeSplatData, - outSphericalHarmonicsDegree = 0, sectionSize, sceneCenter, blockSize, bucketSize) { - await delayedExecute(); - const packed = await loadSpzPacked(spzFileData); - outSphericalHarmonicsDegree = Math.min(packed.shDegree, outSphericalHarmonicsDegree); - - const splatArray = new UncompressedSplatArray(outSphericalHarmonicsDegree); - - if (optimizeSplatData) { - unpackGaussians(packed, outSphericalHarmonicsDegree, false, splatArray, 0); - const splatBufferGenerator = SplatBufferGenerator.getStandardGenerator(minimumAlpha, compressionLevel, - sectionSize, sceneCenter, - blockSize, bucketSize); - return splatBufferGenerator.generateFromUncompressedSplatArray(splatArray); - } else { - const { - splatBuffer, - splatBufferDataOffsetBytes - } = SplatBuffer.preallocateUncompressed(packed.numPoints, outSphericalHarmonicsDegree); - unpackGaussians(packed, outSphericalHarmonicsDegree, true, splatBuffer.bufferData, splatBufferDataOffsetBytes); - return splatBuffer; - } - } - -} - -class SplatParser { - - static RowSizeBytes = 32; - static CenterSizeBytes = 12; - static ScaleSizeBytes = 12; - static RotationSizeBytes = 4; - static ColorSizeBytes = 4; - - static parseToUncompressedSplatBufferSection(fromSplat, toSplat, fromBuffer, fromOffset, toBuffer, toOffset) { - - const outBytesPerCenter = SplatBuffer.CompressionLevels[0].BytesPerCenter; - const outBytesPerScale = SplatBuffer.CompressionLevels[0].BytesPerScale; - const outBytesPerRotation = SplatBuffer.CompressionLevels[0].BytesPerRotation; - const outBytesPerSplat = SplatBuffer.CompressionLevels[0].SphericalHarmonicsDegrees[0].BytesPerSplat; - - for (let i = fromSplat; i <= toSplat; i++) { - const inBase = i * SplatParser.RowSizeBytes + fromOffset; - const inCenter = new Float32Array(fromBuffer, inBase, 3); - const inScale = new Float32Array(fromBuffer, inBase + SplatParser.CenterSizeBytes, 3); - const inColor = new Uint8Array(fromBuffer, inBase + SplatParser.CenterSizeBytes + SplatParser.ScaleSizeBytes, 4); - const inRotation = new Uint8Array(fromBuffer, inBase + SplatParser.CenterSizeBytes + SplatParser.ScaleSizeBytes + - SplatParser.RotationSizeBytes, 4); - - const quat = new THREE.Quaternion((inRotation[1] - 128) / 128, (inRotation[2] - 128) / 128, - (inRotation[3] - 128) / 128, (inRotation[0] - 128) / 128); - quat.normalize(); - - const outBase = i * outBytesPerSplat + toOffset; - const outCenter = new Float32Array(toBuffer, outBase, 3); - const outScale = new Float32Array(toBuffer, outBase + outBytesPerCenter, 3); - const outRotation = new Float32Array(toBuffer, outBase + outBytesPerCenter + outBytesPerScale, 4); - const outColor = new Uint8Array(toBuffer, outBase + outBytesPerCenter + outBytesPerScale + outBytesPerRotation, 4); - - outCenter[0] = inCenter[0]; - outCenter[1] = inCenter[1]; - outCenter[2] = inCenter[2]; - - outScale[0] = inScale[0]; - outScale[1] = inScale[1]; - outScale[2] = inScale[2]; - - outRotation[0] = quat.w; - outRotation[1] = quat.x; - outRotation[2] = quat.y; - outRotation[3] = quat.z; - - outColor[0] = inColor[0]; - outColor[1] = inColor[1]; - outColor[2] = inColor[2]; - outColor[3] = inColor[3]; - } - } - - static parseToUncompressedSplatArraySection(fromSplat, toSplat, fromBuffer, fromOffset, splatArray) { - - for (let i = fromSplat; i <= toSplat; i++) { - const inBase = i * SplatParser.RowSizeBytes + fromOffset; - const inCenter = new Float32Array(fromBuffer, inBase, 3); - const inScale = new Float32Array(fromBuffer, inBase + SplatParser.CenterSizeBytes, 3); - const inColor = new Uint8Array(fromBuffer, inBase + SplatParser.CenterSizeBytes + SplatParser.ScaleSizeBytes, 4); - const inRotation = new Uint8Array(fromBuffer, inBase + SplatParser.CenterSizeBytes + SplatParser.ScaleSizeBytes + - SplatParser.RotationSizeBytes, 4); - - const quat = new THREE.Quaternion((inRotation[1] - 128) / 128, (inRotation[2] - 128) / 128, - (inRotation[3] - 128) / 128, (inRotation[0] - 128) / 128); - quat.normalize(); - - splatArray.addSplatFromComonents(inCenter[0], inCenter[1], inCenter[2], inScale[0], inScale[1], inScale[2], - quat.w, quat.x, quat.y, quat.z, inColor[0], inColor[1], inColor[2], inColor[3]); - } - } - - static parseStandardSplatToUncompressedSplatArray(inBuffer) { - // Standard .splat row layout: - // XYZ - Position (Float32) - // XYZ - Scale (Float32) - // RGBA - colors (uint8) - // IJKL - quaternion/rot (uint8) - - const splatCount = inBuffer.byteLength / SplatParser.RowSizeBytes; - - const splatArray = new UncompressedSplatArray(); - - for (let i = 0; i < splatCount; i++) { - const inBase = i * SplatParser.RowSizeBytes; - const inCenter = new Float32Array(inBuffer, inBase, 3); - const inScale = new Float32Array(inBuffer, inBase + SplatParser.CenterSizeBytes, 3); - const inColor = new Uint8Array(inBuffer, inBase + SplatParser.CenterSizeBytes + SplatParser.ScaleSizeBytes, 4); - const inRotation = new Uint8Array(inBuffer, inBase + SplatParser.CenterSizeBytes + - SplatParser.ScaleSizeBytes + SplatParser.ColorSizeBytes, 4); - - const quat = new THREE.Quaternion((inRotation[1] - 128) / 128, (inRotation[2] - 128) / 128, - (inRotation[3] - 128) / 128, (inRotation[0] - 128) / 128); - quat.normalize(); - - splatArray.addSplatFromComonents(inCenter[0], inCenter[1], inCenter[2], inScale[0], inScale[1], inScale[2], - quat.w, quat.x, quat.y, quat.z, inColor[0], inColor[1], inColor[2], inColor[3]); - } - - return splatArray; - } - -} - -function finalize(splatData, optimizeSplatData, minimumAlpha, compressionLevel, sectionSize, sceneCenter, blockSize, bucketSize) { - if (optimizeSplatData) { - const splatBufferGenerator = SplatBufferGenerator.getStandardGenerator(minimumAlpha, compressionLevel, - sectionSize, sceneCenter, - blockSize, bucketSize); - return splatBufferGenerator.generateFromUncompressedSplatArray(splatData); - } else { - // TODO: Implement direct-to-SplatBuffer when not optimizing splat data - return SplatBuffer.generateFromUncompressedSplatArrays([splatData], minimumAlpha, 0, new THREE.Vector3()); - } -} - -class SplatLoader { - - static loadFromURL(fileName, onProgress, progressiveLoadToSplatBuffer, onProgressiveLoadSectionProgress, minimumAlpha, compressionLevel, - optimizeSplatData = true, headers, sectionSize, sceneCenter, blockSize, bucketSize) { - - let internalLoadType = progressiveLoadToSplatBuffer ? InternalLoadType.ProgressiveToSplatBuffer : - InternalLoadType.ProgressiveToSplatArray; - if (optimizeSplatData) internalLoadType = InternalLoadType.ProgressiveToSplatArray; - - const splatDataOffsetBytes = SplatBuffer.HeaderSizeBytes + SplatBuffer.SectionHeaderSizeBytes; - const directLoadSectionSizeBytes = Constants.ProgressiveLoadSectionSize; - const sectionCount = 1; - - let directLoadBufferIn; - let directLoadBufferOut; - let directLoadSplatBuffer; - let maxSplatCount = 0; - let splatCount = 0; - - let standardLoadUncompressedSplatArray; - - const loadPromise = nativePromiseWithExtractedComponents(); - - let numBytesStreamed = 0; - let numBytesLoaded = 0; - let chunks = []; - - const localOnProgress = (percent, percentStr, chunk, fileSize) => { - const loadComplete = percent >= 100; - - if (chunk) { - chunks.push(chunk); - } - - if (internalLoadType === InternalLoadType.DownloadBeforeProcessing) { - if (loadComplete) { - loadPromise.resolve(chunks); - } - return; - } - - if (!fileSize) { - if (progressiveLoadToSplatBuffer) { - throw new DirectLoadError('Cannon directly load .splat because no file size info is available.'); - } else { - internalLoadType = InternalLoadType.DownloadBeforeProcessing; - return; - } - } - - if (!directLoadBufferIn) { - maxSplatCount = fileSize / SplatParser.RowSizeBytes; - directLoadBufferIn = new ArrayBuffer(fileSize); - const bytesPerSplat = SplatBuffer.CompressionLevels[0].SphericalHarmonicsDegrees[0].BytesPerSplat; - const splatBufferSizeBytes = splatDataOffsetBytes + bytesPerSplat * maxSplatCount; - - if (internalLoadType === InternalLoadType.ProgressiveToSplatBuffer) { - directLoadBufferOut = new ArrayBuffer(splatBufferSizeBytes); - SplatBuffer.writeHeaderToBuffer({ - versionMajor: SplatBuffer.CurrentMajorVersion, - versionMinor: SplatBuffer.CurrentMinorVersion, - maxSectionCount: sectionCount, - sectionCount: sectionCount, - maxSplatCount: maxSplatCount, - splatCount: splatCount, - compressionLevel: 0, - sceneCenter: new THREE.Vector3() - }, directLoadBufferOut); - } else { - standardLoadUncompressedSplatArray = new UncompressedSplatArray(0); - } - } - - if (chunk) { - new Uint8Array(directLoadBufferIn, numBytesLoaded, chunk.byteLength).set(new Uint8Array(chunk)); - numBytesLoaded += chunk.byteLength; - - const bytesLoadedSinceLastSection = numBytesLoaded - numBytesStreamed; - if (bytesLoadedSinceLastSection > directLoadSectionSizeBytes || loadComplete) { - const bytesToUpdate = loadComplete ? bytesLoadedSinceLastSection : directLoadSectionSizeBytes; - const addedSplatCount = bytesToUpdate / SplatParser.RowSizeBytes; - const newSplatCount = splatCount + addedSplatCount; - - if (internalLoadType === InternalLoadType.ProgressiveToSplatBuffer) { - SplatParser.parseToUncompressedSplatBufferSection(splatCount, newSplatCount - 1, directLoadBufferIn, 0, - directLoadBufferOut, splatDataOffsetBytes); - } else { - SplatParser.parseToUncompressedSplatArraySection(splatCount, newSplatCount - 1, directLoadBufferIn, 0, - standardLoadUncompressedSplatArray); - } - - splatCount = newSplatCount; - - if (internalLoadType === InternalLoadType.ProgressiveToSplatBuffer) { - if (!directLoadSplatBuffer) { - SplatBuffer.writeSectionHeaderToBuffer({ - maxSplatCount: maxSplatCount, - splatCount: splatCount, - bucketSize: 0, - bucketCount: 0, - bucketBlockSize: 0, - compressionScaleRange: 0, - storageSizeBytes: 0, - fullBucketCount: 0, - partiallyFilledBucketCount: 0 - }, 0, directLoadBufferOut, SplatBuffer.HeaderSizeBytes); - directLoadSplatBuffer = new SplatBuffer(directLoadBufferOut, false); - } - directLoadSplatBuffer.updateLoadedCounts(1, splatCount); - if (onProgressiveLoadSectionProgress) { - onProgressiveLoadSectionProgress(directLoadSplatBuffer, loadComplete); - } - } - - numBytesStreamed += directLoadSectionSizeBytes; - } - } - - if (loadComplete) { - if (internalLoadType === InternalLoadType.ProgressiveToSplatBuffer) { - loadPromise.resolve(directLoadSplatBuffer); - } else { - loadPromise.resolve(standardLoadUncompressedSplatArray); - } - } - - if (onProgress) onProgress(percent, percentStr, LoaderStatus.Downloading); - }; - - if (onProgress) onProgress(0, '0%', LoaderStatus.Downloading); - return fetchWithProgress(fileName, localOnProgress, false, headers).then(() => { - if (onProgress) onProgress(0, '0%', LoaderStatus.Processing); - return loadPromise.promise.then((splatData) => { - if (onProgress) onProgress(100, '100%', LoaderStatus.Done); - if (internalLoadType === InternalLoadType.DownloadBeforeProcessing) { - return new Blob(chunks).arrayBuffer().then((splatData) => { - return SplatLoader.loadFromFileData(splatData, minimumAlpha, compressionLevel, optimizeSplatData, - sectionSize, sceneCenter, blockSize, bucketSize); - }); - } else if (internalLoadType === InternalLoadType.ProgressiveToSplatBuffer) { - return splatData; - } else { - return delayedExecute(() => { - return finalize(splatData, optimizeSplatData, minimumAlpha, compressionLevel, - sectionSize, sceneCenter, blockSize, bucketSize); - }); - } - }); - }); - } - - static loadFromFileData(splatFileData, minimumAlpha, compressionLevel, optimizeSplatData, - sectionSize, sceneCenter, blockSize, bucketSize) { - return delayedExecute(() => { - const splatArray = SplatParser.parseStandardSplatToUncompressedSplatArray(splatFileData); - return finalize(splatArray, optimizeSplatData, minimumAlpha, compressionLevel, - sectionSize, sceneCenter, blockSize, bucketSize); - }); - } - -} - -class KSplatLoader { - - static checkVersion(buffer) { - const minVersionMajor = SplatBuffer.CurrentMajorVersion; - const minVersionMinor = SplatBuffer.CurrentMinorVersion; - const header = SplatBuffer.parseHeader(buffer); - if (header.versionMajor === minVersionMajor && - header.versionMinor >= minVersionMinor || - header.versionMajor > minVersionMajor) { - return true; - } else { - throw new Error(`KSplat version not supported: v${header.versionMajor}.${header.versionMinor}. ` + - `Minimum required: v${minVersionMajor}.${minVersionMinor}`); - } - }; - - static loadFromURL(fileName, externalOnProgress, progressiveLoadToSplatBuffer, onSectionBuilt, headers) { - let directLoadBuffer; - let directLoadSplatBuffer; - - let headerBuffer; - let header; - let headerLoaded = false; - let headerLoading = false; - - let sectionHeadersBuffer; - let sectionHeaders = []; - let sectionHeadersLoaded = false; - let sectionHeadersLoading = false; - - let numBytesLoaded = 0; - let numBytesProgressivelyLoaded = 0; - let totalBytesToDownload = 0; - - let downloadComplete = false; - let loadComplete = false; - let loadSectionQueued = false; - - let chunks = []; - - const directLoadPromise = nativePromiseWithExtractedComponents(); - - const checkAndLoadHeader = () => { - if (!headerLoaded && !headerLoading && numBytesLoaded >= SplatBuffer.HeaderSizeBytes) { - headerLoading = true; - const headerAssemblyPromise = new Blob(chunks).arrayBuffer(); - headerAssemblyPromise.then((bufferData) => { - headerBuffer = new ArrayBuffer(SplatBuffer.HeaderSizeBytes); - new Uint8Array(headerBuffer).set(new Uint8Array(bufferData, 0, SplatBuffer.HeaderSizeBytes)); - KSplatLoader.checkVersion(headerBuffer); - headerLoading = false; - headerLoaded = true; - header = SplatBuffer.parseHeader(headerBuffer); - window.setTimeout(() => { - checkAndLoadSectionHeaders(); - }, 1); - }); - } - }; - - let queuedCheckAndLoadSectionsCount = 0; - const queueCheckAndLoadSections = () => { - if (queuedCheckAndLoadSectionsCount === 0) { - queuedCheckAndLoadSectionsCount++; - window.setTimeout(() => { - queuedCheckAndLoadSectionsCount--; - checkAndLoadSections(); - }, 1); - } - }; - - const checkAndLoadSectionHeaders = () => { - const performLoad = () => { - sectionHeadersLoading = true; - const sectionHeadersAssemblyPromise = new Blob(chunks).arrayBuffer(); - sectionHeadersAssemblyPromise.then((bufferData) => { - sectionHeadersLoading = false; - sectionHeadersLoaded = true; - sectionHeadersBuffer = new ArrayBuffer(header.maxSectionCount * SplatBuffer.SectionHeaderSizeBytes); - new Uint8Array(sectionHeadersBuffer).set(new Uint8Array(bufferData, SplatBuffer.HeaderSizeBytes, - header.maxSectionCount * SplatBuffer.SectionHeaderSizeBytes)); - sectionHeaders = SplatBuffer.parseSectionHeaders(header, sectionHeadersBuffer, 0, false); - let totalSectionStorageStorageByes = 0; - for (let i = 0; i < header.maxSectionCount; i++) { - totalSectionStorageStorageByes += sectionHeaders[i].storageSizeBytes; - } - const totalStorageSizeBytes = SplatBuffer.HeaderSizeBytes + header.maxSectionCount * - SplatBuffer.SectionHeaderSizeBytes + totalSectionStorageStorageByes; - if (!directLoadBuffer) { - directLoadBuffer = new ArrayBuffer(totalStorageSizeBytes); - let offset = 0; - for (let i = 0; i < chunks.length; i++) { - const chunk = chunks[i]; - new Uint8Array(directLoadBuffer, offset, chunk.byteLength).set(new Uint8Array(chunk)); - offset += chunk.byteLength; - } - } - - totalBytesToDownload = SplatBuffer.HeaderSizeBytes + SplatBuffer.SectionHeaderSizeBytes * header.maxSectionCount; - for (let i = 0; i <= sectionHeaders.length && i < header.maxSectionCount; i++) { - totalBytesToDownload += sectionHeaders[i].storageSizeBytes; - } - - queueCheckAndLoadSections(); - }); - }; - - if (!sectionHeadersLoading && !sectionHeadersLoaded && headerLoaded && - numBytesLoaded >= SplatBuffer.HeaderSizeBytes + SplatBuffer.SectionHeaderSizeBytes * header.maxSectionCount) { - performLoad(); - } - }; - - const checkAndLoadSections = () => { - if (loadSectionQueued) return; - loadSectionQueued = true; - const checkAndLoadFunc = () => { - loadSectionQueued = false; - if (sectionHeadersLoaded) { - - if (loadComplete) return; - - downloadComplete = numBytesLoaded >= totalBytesToDownload; - - let bytesLoadedSinceLastSection = numBytesLoaded - numBytesProgressivelyLoaded; - if (bytesLoadedSinceLastSection > Constants.ProgressiveLoadSectionSize || downloadComplete) { - - numBytesProgressivelyLoaded += Constants.ProgressiveLoadSectionSize; - loadComplete = numBytesProgressivelyLoaded >= totalBytesToDownload; - - if (!directLoadSplatBuffer) directLoadSplatBuffer = new SplatBuffer(directLoadBuffer, false); - - const baseDataOffset = SplatBuffer.HeaderSizeBytes + SplatBuffer.SectionHeaderSizeBytes * header.maxSectionCount; - let sectionBase = 0; - let reachedSections = 0; - let loadedSplatCount = 0; - for (let i = 0; i < header.maxSectionCount; i++) { - const sectionHeader = sectionHeaders[i]; - const bucketsDataOffset = sectionBase + sectionHeader.partiallyFilledBucketCount * 4 + - sectionHeader.bucketStorageSizeBytes * sectionHeader.bucketCount; - const bytesRequiredToReachSectionSplatData = baseDataOffset + bucketsDataOffset; - if (numBytesProgressivelyLoaded >= bytesRequiredToReachSectionSplatData) { - reachedSections++; - const bytesPastSSectionSplatDataStart = numBytesProgressivelyLoaded - bytesRequiredToReachSectionSplatData; - const baseDescriptor = SplatBuffer.CompressionLevels[header.compressionLevel]; - const shDesc = baseDescriptor.SphericalHarmonicsDegrees[sectionHeader.sphericalHarmonicsDegree]; - const bytesPerSplat = shDesc.BytesPerSplat; - let loadedSplatsForSection = Math.floor(bytesPastSSectionSplatDataStart / bytesPerSplat); - loadedSplatsForSection = Math.min(loadedSplatsForSection, sectionHeader.maxSplatCount); - loadedSplatCount += loadedSplatsForSection; - directLoadSplatBuffer.updateLoadedCounts(reachedSections, loadedSplatCount); - directLoadSplatBuffer.updateSectionLoadedCounts(i, loadedSplatsForSection); - } else { - break; - } - sectionBase += sectionHeader.storageSizeBytes; - } - - onSectionBuilt(directLoadSplatBuffer, loadComplete); - - const percentComplete = numBytesProgressivelyLoaded / totalBytesToDownload * 100; - const percentLabel = (percentComplete).toFixed(2) + '%'; - - if (externalOnProgress) externalOnProgress(percentComplete, percentLabel, LoaderStatus.Downloading); - - if (loadComplete) { - directLoadPromise.resolve(directLoadSplatBuffer); - } else { - checkAndLoadSections(); - } - } - } - }; - window.setTimeout(checkAndLoadFunc, Constants.ProgressiveLoadSectionDelayDuration); - }; - - const localOnProgress = (percent, percentStr, chunk) => { - if (chunk) { - chunks.push(chunk); - if (directLoadBuffer) { - new Uint8Array(directLoadBuffer, numBytesLoaded, chunk.byteLength).set(new Uint8Array(chunk)); - } - numBytesLoaded += chunk.byteLength; - } - if (progressiveLoadToSplatBuffer) { - checkAndLoadHeader(); - checkAndLoadSectionHeaders(); - checkAndLoadSections(); - } else { - if (externalOnProgress) externalOnProgress(percent, percentStr, LoaderStatus.Downloading); - } - }; - - return fetchWithProgress(fileName, localOnProgress, !progressiveLoadToSplatBuffer, headers).then((fullBuffer) => { - if (externalOnProgress) externalOnProgress(0, '0%', LoaderStatus.Processing); - const loadPromise = progressiveLoadToSplatBuffer ? directLoadPromise.promise : KSplatLoader.loadFromFileData(fullBuffer); - return loadPromise.then((splatBuffer) => { - if (externalOnProgress) externalOnProgress(100, '100%', LoaderStatus.Done); - return splatBuffer; - }); - }); - } - - static loadFromFileData(fileData) { - return delayedExecute(() => { - KSplatLoader.checkVersion(fileData); - return new SplatBuffer(fileData); - }); - } - - static downloadFile = function() { - - let downLoadLink; - - return function(splatBuffer, fileName) { - const blob = new Blob([splatBuffer.bufferData], { - type: 'application/octet-stream', - }); - - if (!downLoadLink) { - downLoadLink = document.createElement('a'); - document.body.appendChild(downLoadLink); - } - downLoadLink.download = fileName; - downLoadLink.href = URL.createObjectURL(blob); - downLoadLink.click(); - }; - - }(); - -} - -const SceneFormat = { - 'Splat': 0, - 'KSplat': 1, - 'Ply': 2, - 'Spz': 3 -}; - -const sceneFormatFromPath = (path) => { - if (path.endsWith('.ply')) return SceneFormat.Ply; - else if (path.endsWith('.splat')) return SceneFormat.Splat; - else if (path.endsWith('.ksplat')) return SceneFormat.KSplat; - else if (path.endsWith('.spz')) return SceneFormat.Spz; - return null; -}; - -var Utils = /*#__PURE__*/Object.freeze({ - __proto__: null, - sceneFormatFromPath: sceneFormatFromPath -}); - -/* -Copyright © 2010-2024 three.js authors & Mark Kellogg - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. -*/ - - -// OrbitControls performs orbiting, dollying (zooming), and panning. -// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). -// -// Orbit - left mouse / touch: one-finger move -// Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish -// Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move - -const _changeEvent = { type: 'change' }; -const _startEvent = { type: 'start' }; -const _endEvent = { type: 'end' }; -const _ray = new Ray$1(); -const _plane = new Plane(); -const TILT_LIMIT = Math.cos( 70 * MathUtils.DEG2RAD ); - -class OrbitControls extends EventDispatcher { - - constructor( object, domElement ) { - - super(); - - this.object = object; - this.domElement = domElement; - this.domElement.style.touchAction = 'none'; // disable touch scroll - - // Set to false to disable this control - this.enabled = true; - - // "target" sets the location of focus, where the object orbits around - this.target = new Vector3(); - - // How far you can dolly in and out ( PerspectiveCamera only ) - this.minDistance = 0; - this.maxDistance = Infinity; - - // How far you can zoom in and out ( OrthographicCamera only ) - this.minZoom = 0; - this.maxZoom = Infinity; - - // How far you can orbit vertically, upper and lower limits. - // Range is 0 to Math.PI radians. - this.minPolarAngle = 0; // radians - this.maxPolarAngle = Math.PI; // radians - - // How far you can orbit horizontally, upper and lower limits. - // If set, the interval [min, max] must be a sub-interval of [- 2 PI, 2 PI], with ( max - min < 2 PI ) - this.minAzimuthAngle = - Infinity; // radians - this.maxAzimuthAngle = Infinity; // radians - - // Set to true to enable damping (inertia) - // If damping is enabled, you must call controls.update() in your animation loop - this.enableDamping = false; - this.dampingFactor = 0.05; - - // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. - // Set to false to disable zooming - this.enableZoom = true; - this.zoomSpeed = 1.0; - - // Set to false to disable rotating - this.enableRotate = true; - this.rotateSpeed = 1.0; - - // Set to false to disable panning - this.enablePan = true; - this.panSpeed = 1.0; - this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up - this.keyPanSpeed = 7.0; // pixels moved per arrow key push - this.zoomToCursor = false; - - // Set to true to automatically rotate around the target - // If auto-rotate is enabled, you must call controls.update() in your animation loop - this.autoRotate = false; - this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60 - - // The four arrow keys - this.keys = { LEFT: 'KeyA', UP: 'KeyW', RIGHT: 'KeyD', BOTTOM: 'KeyS' }; - - // Mouse buttons - this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; - - // Touch fingers - this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }; - - // for reset - this.target0 = this.target.clone(); - this.position0 = this.object.position.clone(); - this.zoom0 = this.object.zoom; - - // the target DOM element for key events - this._domElementKeyEvents = null; - - // - // public methods - // - - this.getPolarAngle = function() { - - return spherical.phi; - - }; - - this.getAzimuthalAngle = function() { - - return spherical.theta; - - }; - - this.getDistance = function() { - - return this.object.position.distanceTo( this.target ); - - }; - - this.listenToKeyEvents = function( domElement ) { - - domElement.addEventListener( 'keydown', onKeyDown ); - this._domElementKeyEvents = domElement; - - }; - - this.stopListenToKeyEvents = function() { - - this._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown ); - this._domElementKeyEvents = null; - - }; - - this.saveState = function() { - - scope.target0.copy( scope.target ); - scope.position0.copy( scope.object.position ); - scope.zoom0 = scope.object.zoom; - - }; - - this.reset = function() { - - scope.target.copy( scope.target0 ); - scope.object.position.copy( scope.position0 ); - scope.object.zoom = scope.zoom0; - this.clearDampedRotation(); - this.clearDampedPan(); - - scope.object.updateProjectionMatrix(); - scope.dispatchEvent( _changeEvent ); - - scope.update(); - - state = STATE.NONE; - - }; - - this.clearDampedRotation = function() { - sphericalDelta.theta = 0.0; - sphericalDelta.phi = 0.0; - }; - - this.clearDampedPan = function() { - panOffset.set(0, 0, 0); - }; - - // this method is exposed, but perhaps it would be better if we can make it private... - this.update = function() { - - const offset = new Vector3(); - - // so camera.up is the orbit axis - const quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) ); - const quatInverse = quat.clone().invert(); - - const lastPosition = new Vector3(); - const lastQuaternion = new Quaternion(); - const lastTargetPosition = new Vector3(); - - const twoPI = 2 * Math.PI; - - return function update() { - - quat.setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) ); - quatInverse.copy(quat).invert(); - - const position = scope.object.position; - - offset.copy( position ).sub( scope.target ); - - // rotate offset to "y-axis-is-up" space - offset.applyQuaternion( quat ); - - // angle from z-axis around y-axis - spherical.setFromVector3( offset ); - - if ( scope.autoRotate && state === STATE.NONE ) { - - rotateLeft( getAutoRotationAngle() ); - - } - - if ( scope.enableDamping ) { - - spherical.theta += sphericalDelta.theta * scope.dampingFactor; - spherical.phi += sphericalDelta.phi * scope.dampingFactor; - - } else { - - spherical.theta += sphericalDelta.theta; - spherical.phi += sphericalDelta.phi; - - } - - // restrict theta to be between desired limits - - let min = scope.minAzimuthAngle; - let max = scope.maxAzimuthAngle; - - if ( isFinite( min ) && isFinite( max ) ) { - - if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI; - - if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI; - - if ( min <= max ) { - - spherical.theta = Math.max( min, Math.min( max, spherical.theta ) ); - - } else { - - spherical.theta = ( spherical.theta > ( min + max ) / 2 ) ? - Math.max( min, spherical.theta ) : - Math.min( max, spherical.theta ); - - } - - } - - // restrict phi to be between desired limits - spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); - - spherical.makeSafe(); - - - // move target to panned location - - if ( scope.enableDamping === true ) { - - scope.target.addScaledVector( panOffset, scope.dampingFactor ); - - } else { - - scope.target.add( panOffset ); - - } - - // adjust the camera position based on zoom only if we're not zooming to the cursor or if it's an ortho camera - // we adjust zoom later in these cases - if ( scope.zoomToCursor && performCursorZoom || scope.object.isOrthographicCamera ) { - - spherical.radius = clampDistance( spherical.radius ); - - } else { - - spherical.radius = clampDistance( spherical.radius * scale ); - - } - - - offset.setFromSpherical( spherical ); - - // rotate offset back to "camera-up-vector-is-up" space - offset.applyQuaternion( quatInverse ); - - position.copy( scope.target ).add( offset ); - - scope.object.lookAt( scope.target ); - - if ( scope.enableDamping === true ) { - - sphericalDelta.theta *= ( 1 - scope.dampingFactor ); - sphericalDelta.phi *= ( 1 - scope.dampingFactor ); - - panOffset.multiplyScalar( 1 - scope.dampingFactor ); - - } else { - - sphericalDelta.set( 0, 0, 0 ); - - panOffset.set( 0, 0, 0 ); - - } - - // adjust camera position - let zoomChanged = false; - if ( scope.zoomToCursor && performCursorZoom ) { - - let newRadius = null; - if ( scope.object.isPerspectiveCamera ) { - - // move the camera down the pointer ray - // this method avoids floating point error - const prevRadius = offset.length(); - newRadius = clampDistance( prevRadius * scale ); - - const radiusDelta = prevRadius - newRadius; - scope.object.position.addScaledVector( dollyDirection, radiusDelta ); - scope.object.updateMatrixWorld(); - - } else if ( scope.object.isOrthographicCamera ) { - - // adjust the ortho camera position based on zoom changes - const mouseBefore = new Vector3( mouse.x, mouse.y, 0 ); - mouseBefore.unproject( scope.object ); - - scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) ); - scope.object.updateProjectionMatrix(); - zoomChanged = true; - - const mouseAfter = new Vector3( mouse.x, mouse.y, 0 ); - mouseAfter.unproject( scope.object ); - - scope.object.position.sub( mouseAfter ).add( mouseBefore ); - scope.object.updateMatrixWorld(); - - newRadius = offset.length(); - - } else { - - console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled.' ); - scope.zoomToCursor = false; - - } - - // handle the placement of the target - if ( newRadius !== null ) { - - if ( this.screenSpacePanning ) { - - // position the orbit target in front of the new camera position - scope.target.set( 0, 0, - 1 ) - .transformDirection( scope.object.matrix ) - .multiplyScalar( newRadius ) - .add( scope.object.position ); - - } else { - - // get the ray and translation plane to compute target - _ray.origin.copy( scope.object.position ); - _ray.direction.set( 0, 0, - 1 ).transformDirection( scope.object.matrix ); - - // if the camera is 20 degrees above the horizon then don't adjust the focus target to avoid - // extremely large values - if ( Math.abs( scope.object.up.dot( _ray.direction ) ) < TILT_LIMIT ) { - - object.lookAt( scope.target ); - - } else { - - _plane.setFromNormalAndCoplanarPoint( scope.object.up, scope.target ); - _ray.intersectPlane( _plane, scope.target ); - - } - - } - - } - - } else if ( scope.object.isOrthographicCamera ) { - - scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) ); - scope.object.updateProjectionMatrix(); - zoomChanged = true; - - } - - scale = 1; - performCursorZoom = false; - - // update condition is: - // min(camera displacement, camera rotation in radians)^2 > EPS - // using small-angle approximation cos(x/2) = 1 - x^2 / 8 - - if ( zoomChanged || - lastPosition.distanceToSquared( scope.object.position ) > EPS || - 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS || - lastTargetPosition.distanceToSquared( scope.target ) > 0 ) { - - scope.dispatchEvent( _changeEvent ); - - lastPosition.copy( scope.object.position ); - lastQuaternion.copy( scope.object.quaternion ); - lastTargetPosition.copy( scope.target ); - - zoomChanged = false; - - return true; - - } - - return false; - - }; - - }(); - - this.dispose = function() { - - scope.domElement.removeEventListener( 'contextmenu', onContextMenu ); - - scope.domElement.removeEventListener( 'pointerdown', onPointerDown ); - scope.domElement.removeEventListener( 'pointercancel', onPointerUp ); - scope.domElement.removeEventListener( 'wheel', onMouseWheel ); - - scope.domElement.removeEventListener( 'pointermove', onPointerMove ); - scope.domElement.removeEventListener( 'pointerup', onPointerUp ); - - - if ( scope._domElementKeyEvents !== null ) { - - scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown ); - scope._domElementKeyEvents = null; - - } - - }; - - // - // internals - // - - const scope = this; - - const STATE = { - NONE: - 1, - ROTATE: 0, - DOLLY: 1, - PAN: 2, - TOUCH_ROTATE: 3, - TOUCH_PAN: 4, - TOUCH_DOLLY_PAN: 5, - TOUCH_DOLLY_ROTATE: 6 - }; - - let state = STATE.NONE; - - const EPS = 0.000001; - - // current position in spherical coordinates - const spherical = new Spherical(); - const sphericalDelta = new Spherical(); - - let scale = 1; - const panOffset = new Vector3(); - - const rotateStart = new Vector2(); - const rotateEnd = new Vector2(); - const rotateDelta = new Vector2(); - - const panStart = new Vector2(); - const panEnd = new Vector2(); - const panDelta = new Vector2(); - - const dollyStart = new Vector2(); - const dollyEnd = new Vector2(); - const dollyDelta = new Vector2(); - - const dollyDirection = new Vector3(); - const mouse = new Vector2(); - let performCursorZoom = false; - - const pointers = []; - const pointerPositions = {}; - - function getAutoRotationAngle() { - - return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; - - } - - function getZoomScale() { - - return Math.pow( 0.95, scope.zoomSpeed ); - - } - - function rotateLeft( angle ) { - - sphericalDelta.theta -= angle; - - } - - function rotateUp( angle ) { - - sphericalDelta.phi -= angle; - - } - - const panLeft = function() { - - const v = new Vector3(); - - return function panLeft( distance, objectMatrix ) { - - v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix - v.multiplyScalar( - distance ); - - panOffset.add( v ); - - }; - - }(); - - const panUp = function() { - - const v = new Vector3(); - - return function panUp( distance, objectMatrix ) { - - if ( scope.screenSpacePanning === true ) { - - v.setFromMatrixColumn( objectMatrix, 1 ); - - } else { - - v.setFromMatrixColumn( objectMatrix, 0 ); - v.crossVectors( scope.object.up, v ); - - } - - v.multiplyScalar( distance ); - - panOffset.add( v ); - - }; - - }(); - - // deltaX and deltaY are in pixels; right and down are positive - const pan = function() { - - const offset = new Vector3(); - - return function pan( deltaX, deltaY ) { - - const element = scope.domElement; - - if ( scope.object.isPerspectiveCamera ) { - - // perspective - const position = scope.object.position; - offset.copy( position ).sub( scope.target ); - let targetDistance = offset.length(); - - // half of the fov is center to top of screen - targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); - - // we use only clientHeight here so aspect ratio does not distort speed - panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); - panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); - - } else if ( scope.object.isOrthographicCamera ) { - - // orthographic - panLeft( deltaX * ( scope.object.right - scope.object.left ) / - scope.object.zoom / element.clientWidth, scope.object.matrix ); - panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / - element.clientHeight, scope.object.matrix ); - - } else { - - // camera neither orthographic nor perspective - console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); - scope.enablePan = false; - - } - - }; - - }(); - - function dollyOut( dollyScale ) { - - if ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) { - - scale /= dollyScale; - - } else { - - console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); - scope.enableZoom = false; - - } - - } - - function dollyIn( dollyScale ) { - - if ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) { - - scale *= dollyScale; - - } else { - - console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); - scope.enableZoom = false; - - } - - } - - function updateMouseParameters( event ) { - - if ( ! scope.zoomToCursor ) { - - return; - - } - - performCursorZoom = true; - - const rect = scope.domElement.getBoundingClientRect(); - const x = event.clientX - rect.left; - const y = event.clientY - rect.top; - const w = rect.width; - const h = rect.height; - - mouse.x = ( x / w ) * 2 - 1; - mouse.y = - ( y / h ) * 2 + 1; - - dollyDirection.set( mouse.x, mouse.y, 1 ).unproject( object ).sub( object.position ).normalize(); - - } - - function clampDistance( dist ) { - - return Math.max( scope.minDistance, Math.min( scope.maxDistance, dist ) ); - - } - - // - // event callbacks - update the object state - // - - function handleMouseDownRotate( event ) { - - rotateStart.set( event.clientX, event.clientY ); - - } - - function handleMouseDownDolly( event ) { - - updateMouseParameters( event ); - dollyStart.set( event.clientX, event.clientY ); - - } - - function handleMouseDownPan( event ) { - - panStart.set( event.clientX, event.clientY ); - - } - - function handleMouseMoveRotate( event ) { - - rotateEnd.set( event.clientX, event.clientY ); - - rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); - - const element = scope.domElement; - - rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height - - rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); - - rotateStart.copy( rotateEnd ); - - scope.update(); - - } - - function handleMouseMoveDolly( event ) { - - dollyEnd.set( event.clientX, event.clientY ); - - dollyDelta.subVectors( dollyEnd, dollyStart ); - - if ( dollyDelta.y > 0 ) { - - dollyOut( getZoomScale() ); - - } else if ( dollyDelta.y < 0 ) { - - dollyIn( getZoomScale() ); - - } - - dollyStart.copy( dollyEnd ); - - scope.update(); - - } - - function handleMouseMovePan( event ) { - - panEnd.set( event.clientX, event.clientY ); - - panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); - - pan( panDelta.x, panDelta.y ); - - panStart.copy( panEnd ); - - scope.update(); - - } - - function handleMouseWheel( event ) { - - updateMouseParameters( event ); - - if ( event.deltaY < 0 ) { - - dollyIn( getZoomScale() ); - - } else if ( event.deltaY > 0 ) { - - dollyOut( getZoomScale() ); - - } - - scope.update(); - - } - - function handleKeyDown( event ) { - - let needsUpdate = false; - - switch ( event.code ) { - - case scope.keys.UP: - - if ( event.ctrlKey || event.metaKey || event.shiftKey ) { - - rotateUp( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight ); - - } else { - - pan( 0, scope.keyPanSpeed ); - - } - - needsUpdate = true; - break; - - case scope.keys.BOTTOM: - - if ( event.ctrlKey || event.metaKey || event.shiftKey ) { - - rotateUp( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight ); - - } else { - - pan( 0, - scope.keyPanSpeed ); - - } - - needsUpdate = true; - break; - - case scope.keys.LEFT: - - if ( event.ctrlKey || event.metaKey || event.shiftKey ) { - - rotateLeft( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight ); - - } else { - - pan( scope.keyPanSpeed, 0 ); - - } - - needsUpdate = true; - break; - - case scope.keys.RIGHT: - - if ( event.ctrlKey || event.metaKey || event.shiftKey ) { - - rotateLeft( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight ); - - } else { - - pan( - scope.keyPanSpeed, 0 ); - - } - - needsUpdate = true; - break; - - } - - if ( needsUpdate ) { - - // prevent the browser from scrolling on cursor keys - event.preventDefault(); - - scope.update(); - - } - - - } - - function handleTouchStartRotate() { - - if ( pointers.length === 1 ) { - - rotateStart.set( pointers[0].pageX, pointers[0].pageY ); - - } else { - - const x = 0.5 * ( pointers[0].pageX + pointers[1].pageX ); - const y = 0.5 * ( pointers[0].pageY + pointers[1].pageY ); - - rotateStart.set( x, y ); - - } - - } - - function handleTouchStartPan() { - - if ( pointers.length === 1 ) { - - panStart.set( pointers[0].pageX, pointers[0].pageY ); - - } else { - - const x = 0.5 * ( pointers[0].pageX + pointers[1].pageX ); - const y = 0.5 * ( pointers[0].pageY + pointers[1].pageY ); - - panStart.set( x, y ); - - } - - } - - function handleTouchStartDolly() { - - const dx = pointers[0].pageX - pointers[1].pageX; - const dy = pointers[0].pageY - pointers[1].pageY; - - const distance = Math.sqrt( dx * dx + dy * dy ); - - dollyStart.set( 0, distance ); - - } - - function handleTouchStartDollyPan() { - - if ( scope.enableZoom ) handleTouchStartDolly(); - - if ( scope.enablePan ) handleTouchStartPan(); - - } - - function handleTouchStartDollyRotate() { - - if ( scope.enableZoom ) handleTouchStartDolly(); - - if ( scope.enableRotate ) handleTouchStartRotate(); - - } - - function handleTouchMoveRotate( event ) { - - if ( pointers.length == 1 ) { - - rotateEnd.set( event.pageX, event.pageY ); - - } else { - - const position = getSecondPointerPosition( event ); - - const x = 0.5 * ( event.pageX + position.x ); - const y = 0.5 * ( event.pageY + position.y ); - - rotateEnd.set( x, y ); - - } - - rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); - - const element = scope.domElement; - - rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height - - rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); - - rotateStart.copy( rotateEnd ); - - } - - function handleTouchMovePan( event ) { - - if ( pointers.length === 1 ) { - - panEnd.set( event.pageX, event.pageY ); - - } else { - - const position = getSecondPointerPosition( event ); - - const x = 0.5 * ( event.pageX + position.x ); - const y = 0.5 * ( event.pageY + position.y ); - - panEnd.set( x, y ); - - } - - panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); - - pan( panDelta.x, panDelta.y ); - - panStart.copy( panEnd ); - - } - - function handleTouchMoveDolly( event ) { - - const position = getSecondPointerPosition( event ); - - const dx = event.pageX - position.x; - const dy = event.pageY - position.y; - - const distance = Math.sqrt( dx * dx + dy * dy ); - - dollyEnd.set( 0, distance ); - - dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); - - dollyOut( dollyDelta.y ); - - dollyStart.copy( dollyEnd ); - - } - - function handleTouchMoveDollyPan( event ) { - - if ( scope.enableZoom ) handleTouchMoveDolly( event ); - - if ( scope.enablePan ) handleTouchMovePan( event ); - - } - - function handleTouchMoveDollyRotate( event ) { - - if ( scope.enableZoom ) handleTouchMoveDolly( event ); - - if ( scope.enableRotate ) handleTouchMoveRotate( event ); - - } - - // - // event handlers - FSM: listen for events and reset state - // - - function onPointerDown( event ) { - - if ( scope.enabled === false ) return; - - if ( pointers.length === 0 ) { - - scope.domElement.setPointerCapture( event.pointerId ); - - scope.domElement.addEventListener( 'pointermove', onPointerMove ); - scope.domElement.addEventListener( 'pointerup', onPointerUp ); - - } - - // - - addPointer( event ); - - if ( event.pointerType === 'touch' ) { - - onTouchStart( event ); - - } else { - - onMouseDown( event ); - - } - - } - - function onPointerMove( event ) { - - if ( scope.enabled === false ) return; - - if ( event.pointerType === 'touch' ) { - - onTouchMove( event ); - - } else { - - onMouseMove( event ); - - } - - } - - function onPointerUp( event ) { - - removePointer( event ); - - if ( pointers.length === 0 ) { - - scope.domElement.releasePointerCapture( event.pointerId ); - - scope.domElement.removeEventListener( 'pointermove', onPointerMove ); - scope.domElement.removeEventListener( 'pointerup', onPointerUp ); - - } - - scope.dispatchEvent( _endEvent ); - - state = STATE.NONE; - - } - - function onMouseDown( event ) { - - let mouseAction; - - switch ( event.button ) { - - case 0: - - mouseAction = scope.mouseButtons.LEFT; - break; - - case 1: - - mouseAction = scope.mouseButtons.MIDDLE; - break; - - case 2: - - mouseAction = scope.mouseButtons.RIGHT; - break; - - default: - - mouseAction = - 1; - - } - - switch ( mouseAction ) { - - case MOUSE.DOLLY: - - if ( scope.enableZoom === false ) return; - - handleMouseDownDolly( event ); - - state = STATE.DOLLY; - - break; - - case MOUSE.ROTATE: - - if ( event.ctrlKey || event.metaKey || event.shiftKey ) { - - if ( scope.enablePan === false ) return; - - handleMouseDownPan( event ); - - state = STATE.PAN; - - } else { - - if ( scope.enableRotate === false ) return; - - handleMouseDownRotate( event ); - - state = STATE.ROTATE; - - } - - break; - - case MOUSE.PAN: - - if ( event.ctrlKey || event.metaKey || event.shiftKey ) { - - if ( scope.enableRotate === false ) return; - - handleMouseDownRotate( event ); - - state = STATE.ROTATE; - - } else { - - if ( scope.enablePan === false ) return; - - handleMouseDownPan( event ); - - state = STATE.PAN; - - } - - break; - - default: - - state = STATE.NONE; - - } - - if ( state !== STATE.NONE ) { - - scope.dispatchEvent( _startEvent ); - - } - - } - - function onMouseMove( event ) { - - switch ( state ) { - - case STATE.ROTATE: - - if ( scope.enableRotate === false ) return; - - handleMouseMoveRotate( event ); - - break; - - case STATE.DOLLY: - - if ( scope.enableZoom === false ) return; - - handleMouseMoveDolly( event ); - - break; - - case STATE.PAN: - - if ( scope.enablePan === false ) return; - - handleMouseMovePan( event ); - - break; - - } - - } - - function onMouseWheel( event ) { - - if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE ) return; - - event.preventDefault(); - - scope.dispatchEvent( _startEvent ); - - handleMouseWheel( event ); - - scope.dispatchEvent( _endEvent ); - - } - - function onKeyDown( event ) { - - if ( scope.enabled === false || scope.enablePan === false ) return; - - handleKeyDown( event ); - - } - - function onTouchStart( event ) { - - trackPointer( event ); - - switch ( pointers.length ) { - - case 1: - - switch ( scope.touches.ONE ) { - - case TOUCH.ROTATE: - - if ( scope.enableRotate === false ) return; - - handleTouchStartRotate(); - - state = STATE.TOUCH_ROTATE; - - break; - - case TOUCH.PAN: - - if ( scope.enablePan === false ) return; - - handleTouchStartPan(); - - state = STATE.TOUCH_PAN; - - break; - - default: - - state = STATE.NONE; - - } - - break; - - case 2: - - switch ( scope.touches.TWO ) { - - case TOUCH.DOLLY_PAN: - - if ( scope.enableZoom === false && scope.enablePan === false ) return; - - handleTouchStartDollyPan(); - - state = STATE.TOUCH_DOLLY_PAN; - - break; - - case TOUCH.DOLLY_ROTATE: - - if ( scope.enableZoom === false && scope.enableRotate === false ) return; - - handleTouchStartDollyRotate(); - - state = STATE.TOUCH_DOLLY_ROTATE; - - break; - - default: - - state = STATE.NONE; - - } - - break; - - default: - - state = STATE.NONE; - - } - - if ( state !== STATE.NONE ) { - - scope.dispatchEvent( _startEvent ); - - } - - } - - function onTouchMove( event ) { - - trackPointer( event ); - - switch ( state ) { - - case STATE.TOUCH_ROTATE: - - if ( scope.enableRotate === false ) return; - - handleTouchMoveRotate( event ); - - scope.update(); - - break; - - case STATE.TOUCH_PAN: - - if ( scope.enablePan === false ) return; - - handleTouchMovePan( event ); - - scope.update(); - - break; - - case STATE.TOUCH_DOLLY_PAN: - - if ( scope.enableZoom === false && scope.enablePan === false ) return; - - handleTouchMoveDollyPan( event ); - - scope.update(); - - break; - - case STATE.TOUCH_DOLLY_ROTATE: - - if ( scope.enableZoom === false && scope.enableRotate === false ) return; - - handleTouchMoveDollyRotate( event ); - - scope.update(); - - break; - - default: - - state = STATE.NONE; - - } - - } - - function onContextMenu( event ) { - - if ( scope.enabled === false ) return; - - event.preventDefault(); - - } - - function addPointer( event ) { - - pointers.push( event ); - - } - - function removePointer( event ) { - - delete pointerPositions[event.pointerId]; - - for ( let i = 0; i < pointers.length; i ++ ) { - - if ( pointers[i].pointerId == event.pointerId ) { - - pointers.splice( i, 1 ); - return; - - } - - } - - } - - function trackPointer( event ) { - - let position = pointerPositions[event.pointerId]; - - if ( position === undefined ) { - - position = new Vector2(); - pointerPositions[event.pointerId] = position; - - } - - position.set( event.pageX, event.pageY ); - - } - - function getSecondPointerPosition( event ) { - - const pointer = ( event.pointerId === pointers[0].pointerId ) ? pointers[1] : pointers[0]; - - return pointerPositions[pointer.pointerId]; - - } - - // - - scope.domElement.addEventListener( 'contextmenu', onContextMenu ); - - scope.domElement.addEventListener( 'pointerdown', onPointerDown ); - scope.domElement.addEventListener( 'pointercancel', onPointerUp ); - scope.domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } ); - - // force an update at start - - this.update(); - - } - -} - -const fadeElement = (element, out, displayStyle, duration, onComplete) => { - const startTime = performance.now(); - - let startOpacity = element.style.display === 'none' ? 0 : parseFloat(element.style.opacity); - if (isNaN(startOpacity)) startOpacity = 1; - - const interval = window.setInterval(() => { - const currentTime = performance.now(); - const elapsed = currentTime - startTime; - - let t = Math.min(elapsed / duration, 1.0); - if (t > 0.999) t = 1; - - let opacity; - if (out) { - opacity = (1.0 - t) * startOpacity; - if (opacity < 0.0001) opacity = 0; - } else { - opacity = (1.0 - startOpacity) * t + startOpacity; - } - - if (opacity > 0) { - element.style.display = displayStyle; - element.style.opacity = opacity; - } else { - element.style.display = 'none'; - } - - if (t >= 1) { - if (onComplete) onComplete(); - window.clearInterval(interval); - } - }, 16); - return interval; -}; - -const cancelFade = (interval) => { - window.clearInterval(interval); -}; - -const STANDARD_FADE_DURATION = 500; - -class LoadingSpinner { - - static elementIDGen = 0; - - constructor(message, container) { - - this.taskIDGen = 0; - this.elementID = LoadingSpinner.elementIDGen++; - - this.tasks = []; - - this.message = message || 'Loading...'; - this.container = container || document.body; - - this.spinnerContainerOuter = document.createElement('div'); - this.spinnerContainerOuter.className = `spinnerOuterContainer${this.elementID}`; - this.spinnerContainerOuter.style.display = 'none'; - - this.spinnerContainerPrimary = document.createElement('div'); - this.spinnerContainerPrimary.className = `spinnerContainerPrimary${this.elementID}`; - this.spinnerPrimary = document.createElement('div'); - this.spinnerPrimary.classList.add(`spinner${this.elementID}`, `spinnerPrimary${this.elementID}`); - this.messageContainerPrimary = document.createElement('div'); - this.messageContainerPrimary.classList.add(`messageContainer${this.elementID}`, `messageContainerPrimary${this.elementID}`); - this.messageContainerPrimary.innerHTML = this.message; - - this.spinnerContainerMin = document.createElement('div'); - this.spinnerContainerMin.className = `spinnerContainerMin${this.elementID}`; - this.spinnerMin = document.createElement('div'); - this.spinnerMin.classList.add(`spinner${this.elementID}`, `spinnerMin${this.elementID}`); - this.messageContainerMin = document.createElement('div'); - this.messageContainerMin.classList.add(`messageContainer${this.elementID}`, `messageContainerMin${this.elementID}`); - this.messageContainerMin.innerHTML = this.message; - - this.spinnerContainerPrimary.appendChild(this.spinnerPrimary); - this.spinnerContainerPrimary.appendChild(this.messageContainerPrimary); - this.spinnerContainerOuter.appendChild(this.spinnerContainerPrimary); - - this.spinnerContainerMin.appendChild(this.spinnerMin); - this.spinnerContainerMin.appendChild(this.messageContainerMin); - this.spinnerContainerOuter.appendChild(this.spinnerContainerMin); - - const style = document.createElement('style'); - style.innerHTML = ` - - .spinnerOuterContainer${this.elementID} { - width: 100%; - height: 100%; - margin: 0; - top: 0; - left: 0; - position: absolute; - pointer-events: none; - } - - .messageContainer${this.elementID} { - height: 20px; - font-family: arial; - font-size: 12pt; - color: #ffffff; - text-align: center; - vertical-align: middle; - } - - .spinner${this.elementID} { - padding: 15px; - background: #07e8d6; - z-index:99999; - - aspect-ratio: 1; - border-radius: 50%; - --_m: - conic-gradient(#0000,#000), - linear-gradient(#000 0 0) content-box; - -webkit-mask: var(--_m); - mask: var(--_m); - -webkit-mask-composite: source-out; - mask-composite: subtract; - box-sizing: border-box; - animation: load 1s linear infinite; - } - - .spinnerContainerPrimary${this.elementID} { - z-index:99999; - background-color: rgba(128, 128, 128, 0.75); - border: #666666 1px solid; - border-radius: 5px; - padding-top: 20px; - padding-bottom: 10px; - margin: 0; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-80px, -80px); - width: 180px; - pointer-events: auto; - } - - .spinnerPrimary${this.elementID} { - width: 120px; - margin-left: 30px; - } - - .messageContainerPrimary${this.elementID} { - padding-top: 15px; - } - - .spinnerContainerMin${this.elementID} { - z-index:99999; - background-color: rgba(128, 128, 128, 0.75); - border: #666666 1px solid; - border-radius: 5px; - padding-top: 20px; - padding-bottom: 15px; - margin: 0; - position: absolute; - bottom: 50px; - left: 50%; - transform: translate(-50%, 0); - display: flex; - flex-direction: left; - pointer-events: auto; - min-width: 250px; - } - - .messageContainerMin${this.elementID} { - margin-right: 15px; - } - - .spinnerMin${this.elementID} { - width: 50px; - height: 50px; - margin-left: 15px; - margin-right: 25px; - } - - .messageContainerMin${this.elementID} { - padding-top: 15px; - } - - @keyframes load { - to{transform: rotate(1turn)} - } - - `; - this.spinnerContainerOuter.appendChild(style); - this.container.appendChild(this.spinnerContainerOuter); - - this.setMinimized(false, true); - - this.fadeTransitions = []; - } - - addTask(message) { - const newTask = { - 'message': message, - 'id': this.taskIDGen++ - }; - this.tasks.push(newTask); - this.update(); - return newTask.id; - } - - removeTask(id) { - let index = 0; - for (let task of this.tasks) { - if (task.id === id) { - this.tasks.splice(index, 1); - break; - } - index++; - } - this.update(); - } - - removeAllTasks() { - this.tasks = []; - this.update(); - } - - setMessageForTask(id, message) { - for (let task of this.tasks) { - if (task.id === id) { - task.message = message; - break; - } - } - this.update(); - } - - update() { - if (this.tasks.length > 0) { - this.show(); - this.setMessage(this.tasks[this.tasks.length - 1].message); - } else { - this.hide(); - } - } - - show() { - this.spinnerContainerOuter.style.display = 'block'; - this.visible = true; - } - - hide() { - this.spinnerContainerOuter.style.display = 'none'; - this.visible = false; - } - - setContainer(container) { - if (this.container && this.spinnerContainerOuter.parentElement === this.container) { - this.container.removeChild(this.spinnerContainerOuter); - } - if (container) { - this.container = container; - this.container.appendChild(this.spinnerContainerOuter); - this.spinnerContainerOuter.style.zIndex = this.container.style.zIndex + 1; - } - } - - setMinimized(minimized, instant) { - const showHideSpinner = (element, show, instant, displayStyle, fadeTransitionsIndex) => { - if (instant) { - element.style.display = show ? displayStyle : 'none'; - } else { - this.fadeTransitions[fadeTransitionsIndex] = fadeElement(element, !show, displayStyle, STANDARD_FADE_DURATION, () => { - this.fadeTransitions[fadeTransitionsIndex] = null; - }); - } - }; - showHideSpinner(this.spinnerContainerPrimary, !minimized, instant, 'block', 0); - showHideSpinner(this.spinnerContainerMin, minimized, instant, 'flex', 1); - this.minimized = minimized; - } - - setMessage(msg) { - this.messageContainerPrimary.innerHTML = msg; - this.messageContainerMin.innerHTML = msg; - } -} - -class LoadingProgressBar { - - constructor(container) { - - this.idGen = 0; - - this.tasks = []; - - this.container = container || document.body; - - this.progressBarContainerOuter = document.createElement('div'); - this.progressBarContainerOuter.className = 'progressBarOuterContainer'; - this.progressBarContainerOuter.style.display = 'none'; - - this.progressBarBox = document.createElement('div'); - this.progressBarBox.className = 'progressBarBox'; - - this.progressBarBackground = document.createElement('div'); - this.progressBarBackground.className = 'progressBarBackground'; - - this.progressBar = document.createElement('div'); - this.progressBar.className = 'progressBar'; - - this.progressBarBackground.appendChild(this.progressBar); - this.progressBarBox.appendChild(this.progressBarBackground); - this.progressBarContainerOuter.appendChild(this.progressBarBox); - - const style = document.createElement('style'); - style.innerHTML = ` - - .progressBarOuterContainer { - width: 100%; - height: 100%; - margin: 0; - top: 0; - left: 0; - position: absolute; - pointer-events: none; - } - - .progressBarBox { - z-index:99999; - padding: 7px 9px 5px 7px; - background-color: rgba(190, 190, 190, 0.75); - border: #555555 1px solid; - border-radius: 15px; - margin: 0; - position: absolute; - bottom: 50px; - left: 50%; - transform: translate(-50%, 0); - width: 180px; - height: 30px; - pointer-events: auto; - } - - .progressBarBackground { - width: 100%; - height: 25px; - border-radius:10px; - background-color: rgba(128, 128, 128, 0.75); - border: #444444 1px solid; - box-shadow: inset 0 0 10px #333333; - } - - .progressBar { - height: 25px; - width: 0px; - border-radius:10px; - background-color: rgba(0, 200, 0, 0.75); - box-shadow: inset 0 0 10px #003300; - } - - `; - this.progressBarContainerOuter.appendChild(style); - this.container.appendChild(this.progressBarContainerOuter); - } - - show() { - this.progressBarContainerOuter.style.display = 'block'; - } - - hide() { - this.progressBarContainerOuter.style.display = 'none'; - } - - setProgress(progress) { - this.progressBar.style.width = progress + '%'; - } - - setContainer(container) { - if (this.container && this.progressBarContainerOuter.parentElement === this.container) { - this.container.removeChild(this.progressBarContainerOuter); - } - if (container) { - this.container = container; - this.container.appendChild(this.progressBarContainerOuter); - this.progressBarContainerOuter.style.zIndex = this.container.style.zIndex + 1; - } - } - -} - -class InfoPanel { - - constructor(container) { - - this.container = container || document.body; - - this.infoCells = {}; - - const layout = [ - ['Camera position', 'cameraPosition'], - ['Camera look-at', 'cameraLookAt'], - ['Camera up', 'cameraUp'], - ['Camera mode', 'orthographicCamera'], - ['Cursor position', 'cursorPosition'], - ['FPS', 'fps'], - ['Rendering:', 'renderSplatCount'], - ['Sort time', 'sortTime'], - ['Render window', 'renderWindow'], - ['Focal adjustment', 'focalAdjustment'], - ['Splat scale', 'splatScale'], - ['Point cloud mode', 'pointCloudMode'] - ]; - - this.infoPanelContainer = document.createElement('div'); - const style = document.createElement('style'); - style.innerHTML = ` - - .infoPanel { - width: 430px; - padding: 10px; - background-color: rgba(50, 50, 50, 0.85); - border: #555555 2px solid; - color: #dddddd; - border-radius: 10px; - z-index: 9999; - font-family: arial; - font-size: 11pt; - text-align: left; - margin: 0; - top: 10px; - left:10px; - position: absolute; - pointer-events: auto; - } - - .info-panel-cell { - margin-bottom: 5px; - padding-bottom: 2px; - } - - .label-cell { - font-weight: bold; - font-size: 12pt; - width: 140px; - } - - `; - this.infoPanelContainer.append(style); - - this.infoPanel = document.createElement('div'); - this.infoPanel.className = 'infoPanel'; - - const infoTable = document.createElement('div'); - infoTable.style.display = 'table'; - - for (let layoutEntry of layout) { - const row = document.createElement('div'); - row.style.display = 'table-row'; - row.className = 'info-panel-row'; - - const labelCell = document.createElement('div'); - labelCell.style.display = 'table-cell'; - labelCell.innerHTML = `${layoutEntry[0]}: `; - labelCell.classList.add('info-panel-cell', 'label-cell'); - - const spacerCell = document.createElement('div'); - spacerCell.style.display = 'table-cell'; - spacerCell.style.width = '10px'; - spacerCell.innerHTML = ' '; - spacerCell.className = 'info-panel-cell'; - - const infoCell = document.createElement('div'); - infoCell.style.display = 'table-cell'; - infoCell.innerHTML = ''; - infoCell.className = 'info-panel-cell'; - - this.infoCells[layoutEntry[1]] = infoCell; - - row.appendChild(labelCell); - row.appendChild(spacerCell); - row.appendChild(infoCell); - - infoTable.appendChild(row); - } - - this.infoPanel.appendChild(infoTable); - this.infoPanelContainer.append(this.infoPanel); - this.infoPanelContainer.style.display = 'none'; - this.container.appendChild(this.infoPanelContainer); - - this.visible = false; - } - - update = function(renderDimensions, cameraPosition, cameraLookAtPosition, cameraUp, orthographicCamera, - meshCursorPosition, currentFPS, splatCount, splatRenderCount, - splatRenderCountPct, lastSortTime, focalAdjustment, splatScale, pointCloudMode) { - - const cameraPosString = `${cameraPosition.x.toFixed(5)}, ${cameraPosition.y.toFixed(5)}, ${cameraPosition.z.toFixed(5)}`; - if (this.infoCells.cameraPosition.innerHTML !== cameraPosString) { - this.infoCells.cameraPosition.innerHTML = cameraPosString; - } - - if (cameraLookAtPosition) { - const cla = cameraLookAtPosition; - const cameraLookAtString = `${cla.x.toFixed(5)}, ${cla.y.toFixed(5)}, ${cla.z.toFixed(5)}`; - if (this.infoCells.cameraLookAt.innerHTML !== cameraLookAtString) { - this.infoCells.cameraLookAt.innerHTML = cameraLookAtString; - } - } - - const cameraUpString = `${cameraUp.x.toFixed(5)}, ${cameraUp.y.toFixed(5)}, ${cameraUp.z.toFixed(5)}`; - if (this.infoCells.cameraUp.innerHTML !== cameraUpString) { - this.infoCells.cameraUp.innerHTML = cameraUpString; - } - - this.infoCells.orthographicCamera.innerHTML = orthographicCamera ? 'Orthographic' : 'Perspective'; - - if (meshCursorPosition) { - const cursPos = meshCursorPosition; - const cursorPosString = `${cursPos.x.toFixed(5)}, ${cursPos.y.toFixed(5)}, ${cursPos.z.toFixed(5)}`; - this.infoCells.cursorPosition.innerHTML = cursorPosString; - } else { - this.infoCells.cursorPosition.innerHTML = 'N/A'; - } - - this.infoCells.fps.innerHTML = currentFPS; - this.infoCells.renderWindow.innerHTML = `${renderDimensions.x} x ${renderDimensions.y}`; - - this.infoCells.renderSplatCount.innerHTML = - `${splatRenderCount} splats out of ${splatCount} (${splatRenderCountPct.toFixed(2)}%)`; - - this.infoCells.sortTime.innerHTML = `${lastSortTime.toFixed(3)} ms`; - this.infoCells.focalAdjustment.innerHTML = `${focalAdjustment.toFixed(3)}`; - this.infoCells.splatScale.innerHTML = `${splatScale.toFixed(3)}`; - this.infoCells.pointCloudMode.innerHTML = `${pointCloudMode}`; - }; - - setContainer(container) { - if (this.container && this.infoPanelContainer.parentElement === this.container) { - this.container.removeChild(this.infoPanelContainer); - } - if (container) { - this.container = container; - this.container.appendChild(this.infoPanelContainer); - this.infoPanelContainer.style.zIndex = this.container.style.zIndex + 1; - } - } - - show() { - this.infoPanelContainer.style.display = 'block'; - this.visible = true; - } - - hide() { - this.infoPanelContainer.style.display = 'none'; - this.visible = false; - } - -} - -const _axis = new THREE.Vector3(); - -class ArrowHelper extends THREE.Object3D { - - constructor(dir = new THREE.Vector3(0, 0, 1), origin = new THREE.Vector3(0, 0, 0), length = 1, - radius = 0.1, color = 0xffff00, headLength = length * 0.2, headRadius = headLength * 0.2) { - super(); - - this.type = 'ArrowHelper'; - - const lineGeometry = new THREE.CylinderGeometry(radius, radius, length, 32); - lineGeometry.translate(0, length / 2.0, 0); - const coneGeometry = new THREE.CylinderGeometry( 0, headRadius, headLength, 32); - coneGeometry.translate(0, length, 0); - - this.position.copy( origin ); - - this.line = new THREE.Mesh(lineGeometry, new THREE.MeshBasicMaterial({color: color, toneMapped: false})); - this.line.matrixAutoUpdate = false; - this.add(this.line); - - this.cone = new THREE.Mesh(coneGeometry, new THREE.MeshBasicMaterial({color: color, toneMapped: false})); - this.cone.matrixAutoUpdate = false; - this.add(this.cone); - - this.setDirection(dir); - } - - setDirection( dir ) { - if (dir.y > 0.99999) { - this.quaternion.set(0, 0, 0, 1); - } else if (dir.y < - 0.99999) { - this.quaternion.set(1, 0, 0, 0); - } else { - _axis.set(dir.z, 0, -dir.x).normalize(); - const radians = Math.acos(dir.y); - this.quaternion.setFromAxisAngle(_axis, radians); - } - } - - setColor( color ) { - this.line.material.color.set(color); - this.cone.material.color.set(color); - } - - copy(source) { - super.copy(source, false); - this.line.copy(source.line); - this.cone.copy(source.cone); - return this; - } - - dispose() { - this.line.geometry.dispose(); - this.line.material.dispose(); - this.cone.geometry.dispose(); - this.cone.material.dispose(); - } - -} - -class SceneHelper { - - constructor(threeScene) { - this.threeScene = threeScene; - this.splatRenderTarget = null; - this.renderTargetCopyQuad = null; - this.renderTargetCopyCamera = null; - this.meshCursor = null; - this.focusMarker = null; - this.controlPlane = null; - this.debugRoot = null; - this.secondaryDebugRoot = null; - } - - updateSplatRenderTargetForRenderDimensions(width, height) { - this.destroySplatRendertarget(); - this.splatRenderTarget = new THREE.WebGLRenderTarget(width, height, { - format: THREE.RGBAFormat, - stencilBuffer: false, - depthBuffer: true, - - }); - this.splatRenderTarget.depthTexture = new THREE.DepthTexture(width, height); - this.splatRenderTarget.depthTexture.format = THREE.DepthFormat; - this.splatRenderTarget.depthTexture.type = THREE.UnsignedIntType; - } - - destroySplatRendertarget() { - if (this.splatRenderTarget) { - this.splatRenderTarget = null; - } - } - - setupRenderTargetCopyObjects() { - const uniforms = { - 'sourceColorTexture': { - 'type': 't', - 'value': null - }, - 'sourceDepthTexture': { - 'type': 't', - 'value': null - }, - }; - const renderTargetCopyMaterial = new THREE.ShaderMaterial({ - vertexShader: ` - varying vec2 vUv; - void main() { - vUv = uv; - gl_Position = vec4( position.xy, 0.0, 1.0 ); - } - `, - fragmentShader: ` - #include - #include - varying vec2 vUv; - uniform sampler2D sourceColorTexture; - uniform sampler2D sourceDepthTexture; - void main() { - vec4 color = texture2D(sourceColorTexture, vUv); - float fragDepth = texture2D(sourceDepthTexture, vUv).x; - gl_FragDepth = fragDepth; - gl_FragColor = vec4(color.rgb, color.a * 2.0); - } - `, - uniforms: uniforms, - depthWrite: false, - depthTest: false, - transparent: true, - blending: THREE.CustomBlending, - blendSrc: THREE.SrcAlphaFactor, - blendSrcAlpha: THREE.SrcAlphaFactor, - blendDst: THREE.OneMinusSrcAlphaFactor, - blendDstAlpha: THREE.OneMinusSrcAlphaFactor - }); - renderTargetCopyMaterial.extensions.fragDepth = true; - this.renderTargetCopyQuad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), renderTargetCopyMaterial); - this.renderTargetCopyCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); - } - - destroyRenderTargetCopyObjects() { - if (this.renderTargetCopyQuad) { - disposeAllMeshes(this.renderTargetCopyQuad); - this.renderTargetCopyQuad = null; - } - } - - setupMeshCursor() { - if (!this.meshCursor) { - const coneGeometry = new THREE.ConeGeometry(0.5, 1.5, 32); - const coneMaterial = new THREE.MeshBasicMaterial({color: 0xFFFFFF}); - - const downArrow = new THREE.Mesh(coneGeometry, coneMaterial); - downArrow.rotation.set(0, 0, Math.PI); - downArrow.position.set(0, 1, 0); - const upArrow = new THREE.Mesh(coneGeometry, coneMaterial); - upArrow.position.set(0, -1, 0); - const leftArrow = new THREE.Mesh(coneGeometry, coneMaterial); - leftArrow.rotation.set(0, 0, Math.PI / 2.0); - leftArrow.position.set(1, 0, 0); - const rightArrow = new THREE.Mesh(coneGeometry, coneMaterial); - rightArrow.rotation.set(0, 0, -Math.PI / 2.0); - rightArrow.position.set(-1, 0, 0); - - this.meshCursor = new THREE.Object3D(); - this.meshCursor.add(downArrow); - this.meshCursor.add(upArrow); - this.meshCursor.add(leftArrow); - this.meshCursor.add(rightArrow); - this.meshCursor.scale.set(0.1, 0.1, 0.1); - this.threeScene.add(this.meshCursor); - this.meshCursor.visible = false; - } - } - - destroyMeshCursor() { - if (this.meshCursor) { - disposeAllMeshes(this.meshCursor); - this.threeScene.remove(this.meshCursor); - this.meshCursor = null; - } - } - - setMeshCursorVisibility(visible) { - this.meshCursor.visible = visible; - } - - getMeschCursorVisibility() { - return this.meshCursor.visible; - } - - setMeshCursorPosition(position) { - this.meshCursor.position.copy(position); - } - - positionAndOrientMeshCursor(position, camera) { - this.meshCursor.position.copy(position); - this.meshCursor.up.copy(camera.up); - this.meshCursor.lookAt(camera.position); - } - - setupFocusMarker() { - if (!this.focusMarker) { - const sphereGeometry = new THREE.SphereGeometry(.5, 32, 32); - const focusMarkerMaterial = SceneHelper.buildFocusMarkerMaterial(); - focusMarkerMaterial.depthTest = false; - focusMarkerMaterial.depthWrite = false; - focusMarkerMaterial.transparent = true; - this.focusMarker = new THREE.Mesh(sphereGeometry, focusMarkerMaterial); - } - } - - destroyFocusMarker() { - if (this.focusMarker) { - disposeAllMeshes(this.focusMarker); - this.focusMarker = null; - } - } - - updateFocusMarker = function() { - - const tempPosition = new THREE.Vector3(); - const tempMatrix = new THREE.Matrix4(); - const toCamera = new THREE.Vector3(); - - return function(position, camera, viewport) { - tempMatrix.copy(camera.matrixWorld).invert(); - tempPosition.copy(position).applyMatrix4(tempMatrix); - tempPosition.normalize().multiplyScalar(10); - tempPosition.applyMatrix4(camera.matrixWorld); - toCamera.copy(camera.position).sub(position); - const toCameraDistance = toCamera.length(); - this.focusMarker.position.copy(position); - this.focusMarker.scale.set(toCameraDistance, toCameraDistance, toCameraDistance); - this.focusMarker.material.uniforms.realFocusPosition.value.copy(position); - this.focusMarker.material.uniforms.viewport.value.copy(viewport); - this.focusMarker.material.uniformsNeedUpdate = true; - }; - - }(); - - setFocusMarkerVisibility(visible) { - this.focusMarker.visible = visible; - } - - setFocusMarkerOpacity(opacity) { - this.focusMarker.material.uniforms.opacity.value = opacity; - this.focusMarker.material.uniformsNeedUpdate = true; - } - - getFocusMarkerOpacity() { - return this.focusMarker.material.uniforms.opacity.value; - } - - setupControlPlane() { - if (!this.controlPlane) { - const planeGeometry = new THREE.PlaneGeometry(1, 1); - planeGeometry.rotateX(-Math.PI / 2); - const planeMaterial = new THREE.MeshBasicMaterial({color: 0xffffff}); - planeMaterial.transparent = true; - planeMaterial.opacity = 0.6; - planeMaterial.depthTest = false; - planeMaterial.depthWrite = false; - planeMaterial.side = THREE.DoubleSide; - const planeMesh = new THREE.Mesh(planeGeometry, planeMaterial); - - const arrowDir = new THREE.Vector3(0, 1, 0); - arrowDir.normalize(); - const arrowOrigin = new THREE.Vector3(0, 0, 0); - const arrowLength = 0.5; - const arrowRadius = 0.01; - const arrowColor = 0x00dd00; - const arrowHelper = new ArrowHelper(arrowDir, arrowOrigin, arrowLength, arrowRadius, arrowColor, 0.1, 0.03); - - this.controlPlane = new THREE.Object3D(); - this.controlPlane.add(planeMesh); - this.controlPlane.add(arrowHelper); - } - } - - destroyControlPlane() { - if (this.controlPlane) { - disposeAllMeshes(this.controlPlane); - this.controlPlane = null; - } - } - - setControlPlaneVisibility(visible) { - this.controlPlane.visible = visible; - } - - positionAndOrientControlPlane = function() { - - const tempQuaternion = new THREE.Quaternion(); - const defaultUp = new THREE.Vector3(0, 1, 0); - - return function(position, up) { - tempQuaternion.setFromUnitVectors(defaultUp, up); - this.controlPlane.position.copy(position); - this.controlPlane.quaternion.copy(tempQuaternion); - }; - - }(); - - addDebugMeshes() { - this.debugRoot = this.createDebugMeshes(); - this.secondaryDebugRoot = this.createSecondaryDebugMeshes(); - this.threeScene.add(this.debugRoot); - this.threeScene.add(this.secondaryDebugRoot); - } - - destroyDebugMeshes() { - for (let debugRoot of [this.debugRoot, this.secondaryDebugRoot]) { - if (debugRoot) { - disposeAllMeshes(debugRoot); - this.threeScene.remove(debugRoot); - } - } - this.debugRoot = null; - this.secondaryDebugRoot = null; - } - - createDebugMeshes(renderOrder) { - const sphereGeometry = new THREE.SphereGeometry(1, 32, 32); - const debugMeshRoot = new THREE.Object3D(); - - const createMesh = (color, position) => { - let sphereMesh = new THREE.Mesh(sphereGeometry, SceneHelper.buildDebugMaterial(color)); - sphereMesh.renderOrder = renderOrder; - debugMeshRoot.add(sphereMesh); - sphereMesh.position.fromArray(position); - }; - - createMesh(0xff0000, [-50, 0, 0]); - createMesh(0xff0000, [50, 0, 0]); - createMesh(0x00ff00, [0, 0, -50]); - createMesh(0x00ff00, [0, 0, 50]); - createMesh(0xffaa00, [5, 0, 5]); - - return debugMeshRoot; - } - - createSecondaryDebugMeshes(renderOrder) { - const boxGeometry = new THREE.BoxGeometry(3, 3, 3); - const debugMeshRoot = new THREE.Object3D(); - - let boxColor = 0xBBBBBB; - const createMesh = (position) => { - let boxMesh = new THREE.Mesh(boxGeometry, SceneHelper.buildDebugMaterial(boxColor)); - boxMesh.renderOrder = renderOrder; - debugMeshRoot.add(boxMesh); - boxMesh.position.fromArray(position); - }; - - let separation = 10; - createMesh([-separation, 0, -separation]); - createMesh([-separation, 0, separation]); - createMesh([separation, 0, -separation]); - createMesh([separation, 0, separation]); - - return debugMeshRoot; - } - - static buildDebugMaterial(color) { - const vertexShaderSource = ` - #include - varying float ndcDepth; - - void main() { - gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position.xyz, 1.0); - ndcDepth = gl_Position.z / gl_Position.w; - gl_Position.x = gl_Position.x / gl_Position.w; - gl_Position.y = gl_Position.y / gl_Position.w; - gl_Position.z = 0.0; - gl_Position.w = 1.0; - - } - `; - - const fragmentShaderSource = ` - #include - uniform vec3 color; - varying float ndcDepth; - void main() { - gl_FragDepth = (ndcDepth + 1.0) / 2.0; - gl_FragColor = vec4(color.rgb, 0.0); - } - `; - - const uniforms = { - 'color': { - 'type': 'v3', - 'value': new THREE.Color(color) - }, - }; - - const material = new THREE.ShaderMaterial({ - uniforms: uniforms, - vertexShader: vertexShaderSource, - fragmentShader: fragmentShaderSource, - transparent: false, - depthTest: true, - depthWrite: true, - side: THREE.FrontSide - }); - material.extensions.fragDepth = true; - - return material; - } - - static buildFocusMarkerMaterial(color) { - const vertexShaderSource = ` - #include - - uniform vec2 viewport; - uniform vec3 realFocusPosition; - - varying vec4 ndcPosition; - varying vec4 ndcCenter; - varying vec4 ndcFocusPosition; - - void main() { - float radius = 0.01; - - vec4 viewPosition = modelViewMatrix * vec4(position.xyz, 1.0); - vec4 viewCenter = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0); - - vec4 viewFocusPosition = modelViewMatrix * vec4(realFocusPosition, 1.0); - - ndcPosition = projectionMatrix * viewPosition; - ndcPosition = ndcPosition * vec4(1.0 / ndcPosition.w); - ndcCenter = projectionMatrix * viewCenter; - ndcCenter = ndcCenter * vec4(1.0 / ndcCenter.w); - - ndcFocusPosition = projectionMatrix * viewFocusPosition; - ndcFocusPosition = ndcFocusPosition * vec4(1.0 / ndcFocusPosition.w); - - gl_Position = projectionMatrix * viewPosition; - - } - `; - - const fragmentShaderSource = ` - #include - uniform vec3 color; - uniform vec2 viewport; - uniform float opacity; - - varying vec4 ndcPosition; - varying vec4 ndcCenter; - varying vec4 ndcFocusPosition; - - void main() { - vec2 screenPosition = vec2(ndcPosition) * viewport; - vec2 screenCenter = vec2(ndcCenter) * viewport; - - vec2 screenVec = screenPosition - screenCenter; - - float projectedRadius = length(screenVec); - - float lineWidth = 0.0005 * viewport.y; - float aaRange = 0.0025 * viewport.y; - float radius = 0.06 * viewport.y; - float radDiff = abs(projectedRadius - radius) - lineWidth; - float alpha = 1.0 - clamp(radDiff / 5.0, 0.0, 1.0); - - gl_FragColor = vec4(color.rgb, alpha * opacity); - } - `; - - const uniforms = { - 'color': { - 'type': 'v3', - 'value': new THREE.Color(color) - }, - 'realFocusPosition': { - 'type': 'v3', - 'value': new THREE.Vector3() - }, - 'viewport': { - 'type': 'v2', - 'value': new THREE.Vector2() - }, - 'opacity': { - 'value': 0.0 - } - }; - - const material = new THREE.ShaderMaterial({ - uniforms: uniforms, - vertexShader: vertexShaderSource, - fragmentShader: fragmentShaderSource, - transparent: true, - depthTest: false, - depthWrite: false, - side: THREE.FrontSide - }); - - return material; - } - - dispose() { - this.destroyMeshCursor(); - this.destroyFocusMarker(); - this.destroyDebugMeshes(); - this.destroyControlPlane(); - this.destroyRenderTargetCopyObjects(); - this.destroySplatRendertarget(); - } -} - -const VectorRight = new THREE.Vector3(1, 0, 0); -const VectorUp = new THREE.Vector3(0, 1, 0); -const VectorBackward = new THREE.Vector3(0, 0, 1); - -class Ray { - - constructor(origin = new THREE.Vector3(), direction = new THREE.Vector3()) { - this.origin = new THREE.Vector3(); - this.direction = new THREE.Vector3(); - this.setParameters(origin, direction); - } - - setParameters(origin, direction) { - this.origin.copy(origin); - this.direction.copy(direction).normalize(); - } - - boxContainsPoint(box, point, epsilon) { - return point.x < box.min.x - epsilon || point.x > box.max.x + epsilon || - point.y < box.min.y - epsilon || point.y > box.max.y + epsilon || - point.z < box.min.z - epsilon || point.z > box.max.z + epsilon ? false : true; - } - - intersectBox = function() { - - const planeIntersectionPoint = new THREE.Vector3(); - const planeIntersectionPointArray = []; - const originArray = []; - const directionArray = []; - - return function(box, outHit) { - - originArray[0] = this.origin.x; - originArray[1] = this.origin.y; - originArray[2] = this.origin.z; - directionArray[0] = this.direction.x; - directionArray[1] = this.direction.y; - directionArray[2] = this.direction.z; - - if (this.boxContainsPoint(box, this.origin, 0.0001)) { - if (outHit) { - outHit.origin.copy(this.origin); - outHit.normal.set(0, 0, 0); - outHit.distance = -1; - } - return true; - } - - for (let i = 0; i < 3; i++) { - if (directionArray[i] == 0.0) continue; - - const hitNormal = i == 0 ? VectorRight : i == 1 ? VectorUp : VectorBackward; - const extremeVec = directionArray[i] < 0 ? box.max : box.min; - let multiplier = -Math.sign(directionArray[i]); - planeIntersectionPointArray[0] = i == 0 ? extremeVec.x : i == 1 ? extremeVec.y : extremeVec.z; - let toSide = planeIntersectionPointArray[0] - originArray[i]; - - if (toSide * multiplier < 0) { - const idx1 = (i + 1) % 3; - const idx2 = (i + 2) % 3; - planeIntersectionPointArray[2] = directionArray[idx1] / directionArray[i] * toSide + originArray[idx1]; - planeIntersectionPointArray[1] = directionArray[idx2] / directionArray[i] * toSide + originArray[idx2]; - planeIntersectionPoint.set(planeIntersectionPointArray[i], - planeIntersectionPointArray[idx2], - planeIntersectionPointArray[idx1]); - if (this.boxContainsPoint(box, planeIntersectionPoint, 0.0001)) { - if (outHit) { - outHit.origin.copy(planeIntersectionPoint); - outHit.normal.copy(hitNormal).multiplyScalar(multiplier); - outHit.distance = planeIntersectionPoint.sub(this.origin).length(); - } - return true; - } - } - } - - return false; - }; - - }(); - - intersectSphere = function() { - - const toSphereCenterVec = new THREE.Vector3(); - - return function(center, radius, outHit) { - toSphereCenterVec.copy(center).sub(this.origin); - const toClosestApproach = toSphereCenterVec.dot(this.direction); - const toClosestApproachSq = toClosestApproach * toClosestApproach; - const toSphereCenterSq = toSphereCenterVec.dot(toSphereCenterVec); - const diffSq = toSphereCenterSq - toClosestApproachSq; - const radiusSq = radius * radius; - - if (diffSq > radiusSq) return false; - - const thc = Math.sqrt(radiusSq - diffSq); - const t0 = toClosestApproach - thc; - const t1 = toClosestApproach + thc; - - if (t1 < 0) return false; - let t = t0 < 0 ? t1 : t0; - - if (outHit) { - outHit.origin.copy(this.origin).addScaledVector(this.direction, t); - outHit.normal.copy(outHit.origin).sub(center).normalize(); - outHit.distance = t; - } - return true; - }; - - }(); -} - -class Hit { - - constructor() { - this.origin = new THREE.Vector3(); - this.normal = new THREE.Vector3(); - this.distance = 0; - this.splatIndex = 0; - } - - set(origin, normal, distance, splatIndex) { - this.origin.copy(origin); - this.normal.copy(normal); - this.distance = distance; - this.splatIndex = splatIndex; - } - - clone() { - const hitClone = new Hit(); - hitClone.origin.copy(this.origin); - hitClone.normal.copy(this.normal); - hitClone.distance = this.distance; - hitClone.splatIndex = this.splatIndex; - return hitClone; - } - -} - -const SplatRenderMode = { - ThreeD: 0, - TwoD: 1 -}; - -class Raycaster { - - constructor(origin, direction, raycastAgainstTrueSplatEllipsoid = false) { - this.ray = new Ray(origin, direction); - this.raycastAgainstTrueSplatEllipsoid = raycastAgainstTrueSplatEllipsoid; - } - - setFromCameraAndScreenPosition = function() { - - const ndcCoords = new THREE.Vector2(); - - return function(camera, screenPosition, screenDimensions) { - ndcCoords.x = screenPosition.x / screenDimensions.x * 2.0 - 1.0; - ndcCoords.y = (screenDimensions.y - screenPosition.y) / screenDimensions.y * 2.0 - 1.0; - if (camera.isPerspectiveCamera) { - this.ray.origin.setFromMatrixPosition(camera.matrixWorld); - this.ray.direction.set(ndcCoords.x, ndcCoords.y, 0.5 ).unproject(camera).sub(this.ray.origin).normalize(); - this.camera = camera; - } else if (camera.isOrthographicCamera) { - this.ray.origin.set(ndcCoords.x, ndcCoords.y, - (camera.near + camera.far) / (camera.near - camera.far)).unproject(camera); - this.ray.direction.set(0, 0, -1).transformDirection(camera.matrixWorld); - this.camera = camera; - } else { - throw new Error('Raycaster::setFromCameraAndScreenPosition() -> Unsupported camera type'); - } - }; - - }(); - - intersectSplatMesh = function() { - - const toLocal = new THREE.Matrix4(); - const fromLocal = new THREE.Matrix4(); - const sceneTransform = new THREE.Matrix4(); - const localRay = new Ray(); - const tempPoint = new THREE.Vector3(); - - return function(splatMesh, outHits = []) { - const splatTree = splatMesh.getSplatTree(); - - if (!splatTree) return; - - for (let s = 0; s < splatTree.subTrees.length; s++) { - const subTree = splatTree.subTrees[s]; - - fromLocal.copy(splatMesh.matrixWorld); - if (splatMesh.dynamicMode) { - splatMesh.getSceneTransform(s, sceneTransform); - fromLocal.multiply(sceneTransform); - } - toLocal.copy(fromLocal).invert(); - - localRay.origin.copy(this.ray.origin).applyMatrix4(toLocal); - localRay.direction.copy(this.ray.origin).add(this.ray.direction); - localRay.direction.applyMatrix4(toLocal).sub(localRay.origin).normalize(); - - const outHitsForSubTree = []; - if (subTree.rootNode) { - this.castRayAtSplatTreeNode(localRay, splatTree, subTree.rootNode, outHitsForSubTree); - } - - outHitsForSubTree.forEach((hit) => { - hit.origin.applyMatrix4(fromLocal); - hit.normal.applyMatrix4(fromLocal).normalize(); - hit.distance = tempPoint.copy(hit.origin).sub(this.ray.origin).length(); - }); - - outHits.push(...outHitsForSubTree); - } - - outHits.sort((a, b) => { - if (a.distance > b.distance) return 1; - else return -1; - }); - - return outHits; - }; - - }(); - - castRayAtSplatTreeNode = function() { - - const tempColor = new THREE.Vector4(); - const tempCenter = new THREE.Vector3(); - const tempScale = new THREE.Vector3(); - const tempRotation = new THREE.Quaternion(); - const tempHit = new Hit(); - const scaleEpsilon = 0.0000001; - - const origin = new THREE.Vector3(0, 0, 0); - const uniformScaleMatrix = new THREE.Matrix4(); - const scaleMatrix = new THREE.Matrix4(); - const rotationMatrix = new THREE.Matrix4(); - const toSphereSpace = new THREE.Matrix4(); - const fromSphereSpace = new THREE.Matrix4(); - const tempRay = new Ray(); - - return function(ray, splatTree, node, outHits = []) { - if (!ray.intersectBox(node.boundingBox)) { - return; - } - if (node.data && node.data.indexes && node.data.indexes.length > 0) { - for (let i = 0; i < node.data.indexes.length; i++) { - - const splatGlobalIndex = node.data.indexes[i]; - const splatSceneIndex = splatTree.splatMesh.getSceneIndexForSplat(splatGlobalIndex); - const splatScene = splatTree.splatMesh.getScene(splatSceneIndex); - if (!splatScene.visible) continue; - - splatTree.splatMesh.getSplatColor(splatGlobalIndex, tempColor); - splatTree.splatMesh.getSplatCenter(splatGlobalIndex, tempCenter); - splatTree.splatMesh.getSplatScaleAndRotation(splatGlobalIndex, tempScale, tempRotation); - - if (tempScale.x <= scaleEpsilon || tempScale.y <= scaleEpsilon || - splatTree.splatMesh.splatRenderMode === SplatRenderMode.ThreeD && tempScale.z <= scaleEpsilon) { - continue; - } - - if (!this.raycastAgainstTrueSplatEllipsoid) { - let radius = (tempScale.x + tempScale.y); - let componentCount = 2; - if (splatTree.splatMesh.splatRenderMode === SplatRenderMode.ThreeD) { - radius += tempScale.z; - componentCount = 3; - } - radius = radius / componentCount; - if (ray.intersectSphere(tempCenter, radius, tempHit)) { - const hitClone = tempHit.clone(); - hitClone.splatIndex = splatGlobalIndex; - outHits.push(hitClone); - } - } else { - scaleMatrix.makeScale(tempScale.x, tempScale.y, tempScale.z); - rotationMatrix.makeRotationFromQuaternion(tempRotation); - const uniformScale = Math.log10(tempColor.w) * 2.0; - uniformScaleMatrix.makeScale(uniformScale, uniformScale, uniformScale); - fromSphereSpace.copy(uniformScaleMatrix).multiply(rotationMatrix).multiply(scaleMatrix); - toSphereSpace.copy(fromSphereSpace).invert(); - tempRay.origin.copy(ray.origin).sub(tempCenter).applyMatrix4(toSphereSpace); - tempRay.direction.copy(ray.origin).add(ray.direction).sub(tempCenter); - tempRay.direction.applyMatrix4(toSphereSpace).sub(tempRay.origin).normalize(); - if (tempRay.intersectSphere(origin, 1.0, tempHit)) { - const hitClone = tempHit.clone(); - hitClone.splatIndex = splatGlobalIndex; - hitClone.origin.applyMatrix4(fromSphereSpace).add(tempCenter); - outHits.push(hitClone); - } - } - } - } - if (node.children && node.children.length > 0) { - for (let child of node.children) { - this.castRayAtSplatTreeNode(ray, splatTree, child, outHits); - } - } - return outHits; - }; - - }(); -} - -class SplatMaterial { - - static buildVertexShaderBase(dynamicMode = false, enableOptionalEffects = false, maxSphericalHarmonicsDegree = 0, customVars = '') { - let vertexShaderSource = ` - precision highp float; - #include - - attribute uint splatIndex; - uniform highp usampler2D centersColorsTexture; - uniform highp sampler2D sphericalHarmonicsTexture; - uniform highp sampler2D sphericalHarmonicsTextureR; - uniform highp sampler2D sphericalHarmonicsTextureG; - uniform highp sampler2D sphericalHarmonicsTextureB; - - uniform highp usampler2D sceneIndexesTexture; - uniform vec2 sceneIndexesTextureSize; - uniform int sceneCount; - `; - - if (enableOptionalEffects) { - vertexShaderSource += ` - uniform float sceneOpacity[${Constants.MaxScenes}]; - uniform int sceneVisibility[${Constants.MaxScenes}]; - `; - } - - if (dynamicMode) { - vertexShaderSource += ` - uniform highp mat4 transforms[${Constants.MaxScenes}]; - `; - } - - vertexShaderSource += ` - ${customVars} - uniform vec2 focal; - uniform float orthoZoom; - uniform int orthographicMode; - uniform int pointCloudModeEnabled; - uniform float inverseFocalAdjustment; - uniform vec2 viewport; - uniform vec2 basisViewport; - uniform vec2 centersColorsTextureSize; - uniform int sphericalHarmonicsDegree; - uniform vec2 sphericalHarmonicsTextureSize; - uniform int sphericalHarmonics8BitMode; - uniform int sphericalHarmonicsMultiTextureMode; - uniform float visibleRegionRadius; - uniform float visibleRegionFadeStartRadius; - uniform float firstRenderTime; - uniform float currentTime; - uniform int fadeInComplete; - uniform vec3 sceneCenter; - uniform float splatScale; - uniform float sphericalHarmonics8BitCompressionRangeMin[${Constants.MaxScenes}]; - uniform float sphericalHarmonics8BitCompressionRangeMax[${Constants.MaxScenes}]; - - varying vec4 vColor; - varying vec2 vUv; - varying vec2 vPosition; - - mat3 quaternionToRotationMatrix(float x, float y, float z, float w) { - float s = 1.0 / sqrt(w * w + x * x + y * y + z * z); - - return mat3( - 1. - 2. * (y * y + z * z), - 2. * (x * y + w * z), - 2. * (x * z - w * y), - 2. * (x * y - w * z), - 1. - 2. * (x * x + z * z), - 2. * (y * z + w * x), - 2. * (x * z + w * y), - 2. * (y * z - w * x), - 1. - 2. * (x * x + y * y) - ); - } - - const float sqrt8 = sqrt(8.0); - const float minAlpha = 1.0 / 255.0; - - const vec4 encodeNorm4 = vec4(1.0 / 255.0, 1.0 / 255.0, 1.0 / 255.0, 1.0 / 255.0); - const uvec4 mask4 = uvec4(uint(0x000000FF), uint(0x0000FF00), uint(0x00FF0000), uint(0xFF000000)); - const uvec4 shift4 = uvec4(0, 8, 16, 24); - vec4 uintToRGBAVec (uint u) { - uvec4 urgba = mask4 & u; - urgba = urgba >> shift4; - vec4 rgba = vec4(urgba) * encodeNorm4; - return rgba; - } - - vec2 getDataUV(in int stride, in int offset, in vec2 dimensions) { - vec2 samplerUV = vec2(0.0, 0.0); - float d = float(splatIndex * uint(stride) + uint(offset)) / dimensions.x; - samplerUV.y = float(floor(d)) / dimensions.y; - samplerUV.x = fract(d); - return samplerUV; - } - - vec2 getDataUVF(in uint sIndex, in float stride, in uint offset, in vec2 dimensions) { - vec2 samplerUV = vec2(0.0, 0.0); - float d = float(uint(float(sIndex) * stride) + offset) / dimensions.x; - samplerUV.y = float(floor(d)) / dimensions.y; - samplerUV.x = fract(d); - return samplerUV; - } - - const float SH_C1 = 0.4886025119029199f; - const float[5] SH_C2 = float[](1.0925484, -1.0925484, 0.3153916, -1.0925484, 0.5462742); - - void main () { - - uint oddOffset = splatIndex & uint(0x00000001); - uint doubleOddOffset = oddOffset * uint(2); - bool isEven = oddOffset == uint(0); - uint nearestEvenIndex = splatIndex - oddOffset; - float fOddOffset = float(oddOffset); - - uvec4 sampledCenterColor = texture(centersColorsTexture, getDataUV(1, 0, centersColorsTextureSize)); - vec3 splatCenter = uintBitsToFloat(uvec3(sampledCenterColor.gba)); - - uint sceneIndex = uint(0); - if (sceneCount > 1) { - sceneIndex = texture(sceneIndexesTexture, getDataUV(1, 0, sceneIndexesTextureSize)).r; - } - `; - - if (enableOptionalEffects) { - vertexShaderSource += ` - float splatOpacityFromScene = sceneOpacity[sceneIndex]; - int sceneVisible = sceneVisibility[sceneIndex]; - if (splatOpacityFromScene <= 0.01 || sceneVisible == 0) { - gl_Position = vec4(0.0, 0.0, 2.0, 1.0); - return; - } - `; - } - - if (dynamicMode) { - vertexShaderSource += ` - mat4 transform = transforms[sceneIndex]; - mat4 transformModelViewMatrix = viewMatrix * transform; - `; - } else { - vertexShaderSource += `mat4 transformModelViewMatrix = modelViewMatrix;`; - } - - vertexShaderSource += ` - float sh8BitCompressionRangeMinForScene = sphericalHarmonics8BitCompressionRangeMin[sceneIndex]; - float sh8BitCompressionRangeMaxForScene = sphericalHarmonics8BitCompressionRangeMax[sceneIndex]; - float sh8BitCompressionRangeForScene = sh8BitCompressionRangeMaxForScene - sh8BitCompressionRangeMinForScene; - float sh8BitCompressionHalfRangeForScene = sh8BitCompressionRangeForScene / 2.0; - vec3 vec8BitSHShift = vec3(sh8BitCompressionRangeMinForScene); - - vec4 viewCenter = transformModelViewMatrix * vec4(splatCenter, 1.0); - - vec4 clipCenter = projectionMatrix * viewCenter; - - float clip = 1.2 * clipCenter.w; - if (clipCenter.z < -clip || clipCenter.x < -clip || clipCenter.x > clip || clipCenter.y < -clip || clipCenter.y > clip) { - gl_Position = vec4(0.0, 0.0, 2.0, 1.0); - return; - } - - vec3 ndcCenter = clipCenter.xyz / clipCenter.w; - - vPosition = position.xy; - vColor = uintToRGBAVec(sampledCenterColor.r); - `; - - // Proceed to sampling and rendering 1st degree spherical harmonics - if (maxSphericalHarmonicsDegree >= 1) { - - vertexShaderSource += ` - if (sphericalHarmonicsDegree >= 1) { - `; - - if (dynamicMode) { - vertexShaderSource += ` - vec3 worldViewDir = normalize(splatCenter - vec3(inverse(transform) * vec4(cameraPosition, 1.0))); - `; - } else { - vertexShaderSource += ` - vec3 worldViewDir = normalize(splatCenter - cameraPosition); - `; - } - - vertexShaderSource += ` - vec3 sh1; - vec3 sh2; - vec3 sh3; - `; - - if (maxSphericalHarmonicsDegree >= 2) { - vertexShaderSource += ` - vec3 sh4; - vec3 sh5; - vec3 sh6; - vec3 sh7; - vec3 sh8; - `; - } - - // Determining how to sample spherical harmonics textures to get the coefficients for calculations for a given degree - // depends on how many total degrees (maxSphericalHarmonicsDegree) are present in the textures. This is because that - // number affects how they are packed in the textures, and therefore the offset & stride required to access them. - - // Sample spherical harmonics textures with 1 degree worth of data for 1st degree calculations, and store in sh1, sh2, and sh3 - if (maxSphericalHarmonicsDegree === 1) { - vertexShaderSource += ` - if (sphericalHarmonicsMultiTextureMode == 0) { - vec2 shUV = getDataUVF(nearestEvenIndex, 2.5, doubleOddOffset, sphericalHarmonicsTextureSize); - vec4 sampledSH0123 = texture(sphericalHarmonicsTexture, shUV); - shUV = getDataUVF(nearestEvenIndex, 2.5, doubleOddOffset + uint(1), sphericalHarmonicsTextureSize); - vec4 sampledSH4567 = texture(sphericalHarmonicsTexture, shUV); - shUV = getDataUVF(nearestEvenIndex, 2.5, doubleOddOffset + uint(2), sphericalHarmonicsTextureSize); - vec4 sampledSH891011 = texture(sphericalHarmonicsTexture, shUV); - sh1 = vec3(sampledSH0123.rgb) * (1.0 - fOddOffset) + vec3(sampledSH0123.ba, sampledSH4567.r) * fOddOffset; - sh2 = vec3(sampledSH0123.a, sampledSH4567.rg) * (1.0 - fOddOffset) + vec3(sampledSH4567.gba) * fOddOffset; - sh3 = vec3(sampledSH4567.ba, sampledSH891011.r) * (1.0 - fOddOffset) + vec3(sampledSH891011.rgb) * fOddOffset; - } else { - vec2 sampledSH01R = texture(sphericalHarmonicsTextureR, getDataUV(2, 0, sphericalHarmonicsTextureSize)).rg; - vec2 sampledSH23R = texture(sphericalHarmonicsTextureR, getDataUV(2, 1, sphericalHarmonicsTextureSize)).rg; - vec2 sampledSH01G = texture(sphericalHarmonicsTextureG, getDataUV(2, 0, sphericalHarmonicsTextureSize)).rg; - vec2 sampledSH23G = texture(sphericalHarmonicsTextureG, getDataUV(2, 1, sphericalHarmonicsTextureSize)).rg; - vec2 sampledSH01B = texture(sphericalHarmonicsTextureB, getDataUV(2, 0, sphericalHarmonicsTextureSize)).rg; - vec2 sampledSH23B = texture(sphericalHarmonicsTextureB, getDataUV(2, 1, sphericalHarmonicsTextureSize)).rg; - sh1 = vec3(sampledSH01R.rg, sampledSH23R.r); - sh2 = vec3(sampledSH01G.rg, sampledSH23G.r); - sh3 = vec3(sampledSH01B.rg, sampledSH23B.r); - } - `; - // Sample spherical harmonics textures with 2 degrees worth of data for 1st degree calculations, and store in sh1, sh2, and sh3 - } else if (maxSphericalHarmonicsDegree === 2) { - vertexShaderSource += ` - vec4 sampledSH0123; - vec4 sampledSH4567; - vec4 sampledSH891011; - - vec4 sampledSH0123R; - vec4 sampledSH0123G; - vec4 sampledSH0123B; - - if (sphericalHarmonicsMultiTextureMode == 0) { - sampledSH0123 = texture(sphericalHarmonicsTexture, getDataUV(6, 0, sphericalHarmonicsTextureSize)); - sampledSH4567 = texture(sphericalHarmonicsTexture, getDataUV(6, 1, sphericalHarmonicsTextureSize)); - sampledSH891011 = texture(sphericalHarmonicsTexture, getDataUV(6, 2, sphericalHarmonicsTextureSize)); - sh1 = sampledSH0123.rgb; - sh2 = vec3(sampledSH0123.a, sampledSH4567.rg); - sh3 = vec3(sampledSH4567.ba, sampledSH891011.r); - } else { - sampledSH0123R = texture(sphericalHarmonicsTextureR, getDataUV(2, 0, sphericalHarmonicsTextureSize)); - sampledSH0123G = texture(sphericalHarmonicsTextureG, getDataUV(2, 0, sphericalHarmonicsTextureSize)); - sampledSH0123B = texture(sphericalHarmonicsTextureB, getDataUV(2, 0, sphericalHarmonicsTextureSize)); - sh1 = vec3(sampledSH0123R.rgb); - sh2 = vec3(sampledSH0123G.rgb); - sh3 = vec3(sampledSH0123B.rgb); - } - `; - } - - // Perform 1st degree spherical harmonics calculations - vertexShaderSource += ` - if (sphericalHarmonics8BitMode == 1) { - sh1 = sh1 * sh8BitCompressionRangeForScene + vec8BitSHShift; - sh2 = sh2 * sh8BitCompressionRangeForScene + vec8BitSHShift; - sh3 = sh3 * sh8BitCompressionRangeForScene + vec8BitSHShift; - } - float x = worldViewDir.x; - float y = worldViewDir.y; - float z = worldViewDir.z; - vColor.rgb += SH_C1 * (-sh1 * y + sh2 * z - sh3 * x); - `; - - // Proceed to sampling and rendering 2nd degree spherical harmonics - if (maxSphericalHarmonicsDegree >= 2) { - - vertexShaderSource += ` - if (sphericalHarmonicsDegree >= 2) { - float xx = x * x; - float yy = y * y; - float zz = z * z; - float xy = x * y; - float yz = y * z; - float xz = x * z; - `; - - // Sample spherical harmonics textures with 2 degrees worth of data for 2nd degree calculations, - // and store in sh4, sh5, sh6, sh7, and sh8 - if (maxSphericalHarmonicsDegree === 2) { - vertexShaderSource += ` - if (sphericalHarmonicsMultiTextureMode == 0) { - vec4 sampledSH12131415 = texture(sphericalHarmonicsTexture, getDataUV(6, 3, sphericalHarmonicsTextureSize)); - vec4 sampledSH16171819 = texture(sphericalHarmonicsTexture, getDataUV(6, 4, sphericalHarmonicsTextureSize)); - vec4 sampledSH20212223 = texture(sphericalHarmonicsTexture, getDataUV(6, 5, sphericalHarmonicsTextureSize)); - sh4 = sampledSH891011.gba; - sh5 = sampledSH12131415.rgb; - sh6 = vec3(sampledSH12131415.a, sampledSH16171819.rg); - sh7 = vec3(sampledSH16171819.ba, sampledSH20212223.r); - sh8 = sampledSH20212223.gba; - } else { - vec4 sampledSH4567R = texture(sphericalHarmonicsTextureR, getDataUV(2, 1, sphericalHarmonicsTextureSize)); - vec4 sampledSH4567G = texture(sphericalHarmonicsTextureG, getDataUV(2, 1, sphericalHarmonicsTextureSize)); - vec4 sampledSH4567B = texture(sphericalHarmonicsTextureB, getDataUV(2, 1, sphericalHarmonicsTextureSize)); - sh4 = vec3(sampledSH0123R.a, sampledSH4567R.rg); - sh5 = vec3(sampledSH4567R.ba, sampledSH0123G.a); - sh6 = vec3(sampledSH4567G.rgb); - sh7 = vec3(sampledSH4567G.a, sampledSH0123B.a, sampledSH4567B.r); - sh8 = vec3(sampledSH4567B.gba); - } - `; - } - - // Perform 2nd degree spherical harmonics calculations - vertexShaderSource += ` - if (sphericalHarmonics8BitMode == 1) { - sh4 = sh4 * sh8BitCompressionRangeForScene + vec8BitSHShift; - sh5 = sh5 * sh8BitCompressionRangeForScene + vec8BitSHShift; - sh6 = sh6 * sh8BitCompressionRangeForScene + vec8BitSHShift; - sh7 = sh7 * sh8BitCompressionRangeForScene + vec8BitSHShift; - sh8 = sh8 * sh8BitCompressionRangeForScene + vec8BitSHShift; - } - - vColor.rgb += - (SH_C2[0] * xy) * sh4 + - (SH_C2[1] * yz) * sh5 + - (SH_C2[2] * (2.0 * zz - xx - yy)) * sh6 + - (SH_C2[3] * xz) * sh7 + - (SH_C2[4] * (xx - yy)) * sh8; - } - `; - } - - vertexShaderSource += ` - - vColor.rgb = clamp(vColor.rgb, vec3(0.), vec3(1.)); - - } - - `; - } - - return vertexShaderSource; - } - - static getVertexShaderFadeIn() { - return ` - if (fadeInComplete == 0) { - float opacityAdjust = 1.0; - float centerDist = length(splatCenter - sceneCenter); - float renderTime = max(currentTime - firstRenderTime, 0.0); - - float fadeDistance = 0.75; - float distanceLoadFadeInFactor = step(visibleRegionFadeStartRadius, centerDist); - distanceLoadFadeInFactor = (1.0 - distanceLoadFadeInFactor) + - (1.0 - clamp((centerDist - visibleRegionFadeStartRadius) / fadeDistance, 0.0, 1.0)) * - distanceLoadFadeInFactor; - opacityAdjust *= distanceLoadFadeInFactor; - vColor.a *= opacityAdjust; - } - `; - } - - static getUniforms(dynamicMode = false, enableOptionalEffects = false, maxSphericalHarmonicsDegree = 0, - splatScale = 1.0, pointCloudModeEnabled = false) { - - const uniforms = { - 'sceneCenter': { - 'type': 'v3', - 'value': new THREE.Vector3() - }, - 'fadeInComplete': { - 'type': 'i', - 'value': 0 - }, - 'orthographicMode': { - 'type': 'i', - 'value': 0 - }, - 'visibleRegionFadeStartRadius': { - 'type': 'f', - 'value': 0.0 - }, - 'visibleRegionRadius': { - 'type': 'f', - 'value': 0.0 - }, - 'currentTime': { - 'type': 'f', - 'value': 0.0 - }, - 'firstRenderTime': { - 'type': 'f', - 'value': 0.0 - }, - 'centersColorsTexture': { - 'type': 't', - 'value': null - }, - 'sphericalHarmonicsTexture': { - 'type': 't', - 'value': null - }, - 'sphericalHarmonicsTextureR': { - 'type': 't', - 'value': null - }, - 'sphericalHarmonicsTextureG': { - 'type': 't', - 'value': null - }, - 'sphericalHarmonicsTextureB': { - 'type': 't', - 'value': null - }, - 'sphericalHarmonics8BitCompressionRangeMin': { - 'type': 'f', - 'value': [] - }, - 'sphericalHarmonics8BitCompressionRangeMax': { - 'type': 'f', - 'value': [] - }, - 'focal': { - 'type': 'v2', - 'value': new THREE.Vector2() - }, - 'orthoZoom': { - 'type': 'f', - 'value': 1.0 - }, - 'inverseFocalAdjustment': { - 'type': 'f', - 'value': 1.0 - }, - 'viewport': { - 'type': 'v2', - 'value': new THREE.Vector2() - }, - 'basisViewport': { - 'type': 'v2', - 'value': new THREE.Vector2() - }, - 'debugColor': { - 'type': 'v3', - 'value': new THREE.Color() - }, - 'centersColorsTextureSize': { - 'type': 'v2', - 'value': new THREE.Vector2(1024, 1024) - }, - 'sphericalHarmonicsDegree': { - 'type': 'i', - 'value': maxSphericalHarmonicsDegree - }, - 'sphericalHarmonicsTextureSize': { - 'type': 'v2', - 'value': new THREE.Vector2(1024, 1024) - }, - 'sphericalHarmonics8BitMode': { - 'type': 'i', - 'value': 0 - }, - 'sphericalHarmonicsMultiTextureMode': { - 'type': 'i', - 'value': 0 - }, - 'splatScale': { - 'type': 'f', - 'value': splatScale - }, - 'pointCloudModeEnabled': { - 'type': 'i', - 'value': pointCloudModeEnabled ? 1 : 0 - }, - 'sceneIndexesTexture': { - 'type': 't', - 'value': null - }, - 'sceneIndexesTextureSize': { - 'type': 'v2', - 'value': new THREE.Vector2(1024, 1024) - }, - 'sceneCount': { - 'type': 'i', - 'value': 1 - } - }; - for (let i = 0; i < Constants.MaxScenes; i++) { - uniforms.sphericalHarmonics8BitCompressionRangeMin.value.push(-Constants.SphericalHarmonics8BitCompressionRange / 2.0); - uniforms.sphericalHarmonics8BitCompressionRangeMax.value.push(Constants.SphericalHarmonics8BitCompressionRange / 2.0); - } - - if (enableOptionalEffects) { - const sceneOpacity = []; - for (let i = 0; i < Constants.MaxScenes; i++) { - sceneOpacity.push(1.0); - } - uniforms['sceneOpacity'] ={ - 'type': 'f', - 'value': sceneOpacity - }; - - const sceneVisibility = []; - for (let i = 0; i < Constants.MaxScenes; i++) { - sceneVisibility.push(1); - } - uniforms['sceneVisibility'] ={ - 'type': 'i', - 'value': sceneVisibility - }; - } - - if (dynamicMode) { - const transformMatrices = []; - for (let i = 0; i < Constants.MaxScenes; i++) { - transformMatrices.push(new THREE.Matrix4()); - } - uniforms['transforms'] = { - 'type': 'mat4', - 'value': transformMatrices - }; - } - - return uniforms; - } - -} - -class SplatMaterial3D { - - /** - * Build the Three.js material that is used to render the splats. - * @param {number} dynamicMode If true, it means the scene geometry represented by this splat mesh is not stationary or - * that the splat count might change - * @param {boolean} enableOptionalEffects When true, allows for usage of extra properties and attributes in the shader for effects - * such as opacity adjustment. Default is false for performance reasons. - * @param {boolean} antialiased If true, calculate compensation factor to deal with gaussians being rendered at a significantly - * different resolution than that of their training - * @param {number} maxScreenSpaceSplatSize The maximum clip space splat size - * @param {number} splatScale Value by which all splats are scaled in screen-space (default is 1.0) - * @param {number} pointCloudModeEnabled Render all splats as screen-space circles - * @param {number} maxSphericalHarmonicsDegree Degree of spherical harmonics to utilize in rendering splats - * @return {THREE.ShaderMaterial} - */ - static build(dynamicMode = false, enableOptionalEffects = false, antialiased = false, maxScreenSpaceSplatSize = 2048, - splatScale = 1.0, pointCloudModeEnabled = false, maxSphericalHarmonicsDegree = 0, kernel2DSize = 0.3) { - - const customVertexVars = ` - uniform vec2 covariancesTextureSize; - uniform highp sampler2D covariancesTexture; - uniform highp usampler2D covariancesTextureHalfFloat; - uniform int covariancesAreHalfFloat; - - void fromCovarianceHalfFloatV4(uvec4 val, out vec4 first, out vec4 second) { - vec2 r = unpackHalf2x16(val.r); - vec2 g = unpackHalf2x16(val.g); - vec2 b = unpackHalf2x16(val.b); - - first = vec4(r.x, r.y, g.x, g.y); - second = vec4(b.x, b.y, 0.0, 0.0); - } - `; - - let vertexShaderSource = SplatMaterial.buildVertexShaderBase(dynamicMode, enableOptionalEffects, - maxSphericalHarmonicsDegree, customVertexVars); - vertexShaderSource += SplatMaterial3D.buildVertexShaderProjection(antialiased, enableOptionalEffects, - maxScreenSpaceSplatSize, kernel2DSize); - const fragmentShaderSource = SplatMaterial3D.buildFragmentShader(); - - const uniforms = SplatMaterial.getUniforms(dynamicMode, enableOptionalEffects, - maxSphericalHarmonicsDegree, splatScale, pointCloudModeEnabled); - - uniforms['covariancesTextureSize'] = { - 'type': 'v2', - 'value': new THREE.Vector2(1024, 1024) - }; - uniforms['covariancesTexture'] = { - 'type': 't', - 'value': null - }; - uniforms['covariancesTextureHalfFloat'] = { - 'type': 't', - 'value': null - }; - uniforms['covariancesAreHalfFloat'] = { - 'type': 'i', - 'value': 0 - }; - - const material = new THREE.ShaderMaterial({ - uniforms: uniforms, - vertexShader: vertexShaderSource, - fragmentShader: fragmentShaderSource, - transparent: true, - alphaTest: 1.0, - blending: THREE.NormalBlending, - depthTest: true, - depthWrite: false, - side: THREE.DoubleSide - }); - - return material; - } - - static buildVertexShaderProjection(antialiased, enableOptionalEffects, maxScreenSpaceSplatSize, kernel2DSize) { - let vertexShaderSource = ` - - vec4 sampledCovarianceA; - vec4 sampledCovarianceB; - vec3 cov3D_M11_M12_M13; - vec3 cov3D_M22_M23_M33; - if (covariancesAreHalfFloat == 0) { - sampledCovarianceA = texture(covariancesTexture, getDataUVF(nearestEvenIndex, 1.5, oddOffset, - covariancesTextureSize)); - sampledCovarianceB = texture(covariancesTexture, getDataUVF(nearestEvenIndex, 1.5, oddOffset + uint(1), - covariancesTextureSize)); - - cov3D_M11_M12_M13 = vec3(sampledCovarianceA.rgb) * (1.0 - fOddOffset) + - vec3(sampledCovarianceA.ba, sampledCovarianceB.r) * fOddOffset; - cov3D_M22_M23_M33 = vec3(sampledCovarianceA.a, sampledCovarianceB.rg) * (1.0 - fOddOffset) + - vec3(sampledCovarianceB.gba) * fOddOffset; - } else { - uvec4 sampledCovarianceU = texture(covariancesTextureHalfFloat, getDataUV(1, 0, covariancesTextureSize)); - fromCovarianceHalfFloatV4(sampledCovarianceU, sampledCovarianceA, sampledCovarianceB); - cov3D_M11_M12_M13 = sampledCovarianceA.rgb; - cov3D_M22_M23_M33 = vec3(sampledCovarianceA.a, sampledCovarianceB.rg); - } - - // Construct the 3D covariance matrix - mat3 Vrk = mat3( - cov3D_M11_M12_M13.x, cov3D_M11_M12_M13.y, cov3D_M11_M12_M13.z, - cov3D_M11_M12_M13.y, cov3D_M22_M23_M33.x, cov3D_M22_M23_M33.y, - cov3D_M11_M12_M13.z, cov3D_M22_M23_M33.y, cov3D_M22_M23_M33.z - ); - - mat3 J; - if (orthographicMode == 1) { - // Since the projection is linear, we don't need an approximation - J = transpose(mat3(orthoZoom, 0.0, 0.0, - 0.0, orthoZoom, 0.0, - 0.0, 0.0, 0.0)); - } else { - // Construct the Jacobian of the affine approximation of the projection matrix. It will be used to transform the - // 3D covariance matrix instead of using the actual projection matrix because that transformation would - // require a non-linear component (perspective division) which would yield a non-gaussian result. - float s = 1.0 / (viewCenter.z * viewCenter.z); - J = mat3( - focal.x / viewCenter.z, 0., -(focal.x * viewCenter.x) * s, - 0., focal.y / viewCenter.z, -(focal.y * viewCenter.y) * s, - 0., 0., 0. - ); - } - - // Concatenate the projection approximation with the model-view transformation - mat3 W = transpose(mat3(transformModelViewMatrix)); - mat3 T = W * J; - - // Transform the 3D covariance matrix (Vrk) to compute the 2D covariance matrix - mat3 cov2Dm = transpose(T) * Vrk * T; - `; - - if (antialiased) { - vertexShaderSource += ` - float detOrig = cov2Dm[0][0] * cov2Dm[1][1] - cov2Dm[0][1] * cov2Dm[0][1]; - cov2Dm[0][0] += ${kernel2DSize}; - cov2Dm[1][1] += ${kernel2DSize}; - float detBlur = cov2Dm[0][0] * cov2Dm[1][1] - cov2Dm[0][1] * cov2Dm[0][1]; - vColor.a *= sqrt(max(detOrig / detBlur, 0.0)); - if (vColor.a < minAlpha) return; - `; - } else { - vertexShaderSource += ` - cov2Dm[0][0] += ${kernel2DSize}; - cov2Dm[1][1] += ${kernel2DSize}; - `; - } - - vertexShaderSource += ` - - // We are interested in the upper-left 2x2 portion of the projected 3D covariance matrix because - // we only care about the X and Y values. We want the X-diagonal, cov2Dm[0][0], - // the Y-diagonal, cov2Dm[1][1], and the correlation between the two cov2Dm[0][1]. We don't - // need cov2Dm[1][0] because it is a symetric matrix. - vec3 cov2Dv = vec3(cov2Dm[0][0], cov2Dm[0][1], cov2Dm[1][1]); - - // We now need to solve for the eigen-values and eigen vectors of the 2D covariance matrix - // so that we can determine the 2D basis for the splat. This is done using the method described - // here: https://people.math.harvard.edu/~knill/teaching/math21b2004/exhibits/2dmatrices/index.html - // After calculating the eigen-values and eigen-vectors, we calculate the basis for rendering the splat - // by normalizing the eigen-vectors and then multiplying them by (sqrt(8) * sqrt(eigen-value)), which is - // equal to scaling them by sqrt(8) standard deviations. - // - // This is a different approach than in the original work at INRIA. In that work they compute the - // max extents of the projected splat in screen space to form a screen-space aligned bounding rectangle - // which forms the geometry that is actually rasterized. The dimensions of that bounding box are 3.0 - // times the square root of the maximum eigen-value, or 3 standard deviations. They then use the inverse - // 2D covariance matrix (called 'conic') in the CUDA rendering thread to determine fragment opacity by - // calculating the full gaussian: exp(-0.5 * (X - mean) * conic * (X - mean)) * splat opacity - float a = cov2Dv.x; - float d = cov2Dv.z; - float b = cov2Dv.y; - float D = a * d - b * b; - float trace = a + d; - float traceOver2 = 0.5 * trace; - float term2 = sqrt(max(0.1f, traceOver2 * traceOver2 - D)); - float eigenValue1 = traceOver2 + term2; - float eigenValue2 = traceOver2 - term2; - - if (pointCloudModeEnabled == 1) { - eigenValue1 = eigenValue2 = 0.2; - } - - if (eigenValue2 <= 0.0) return; - - vec2 eigenVector1 = normalize(vec2(b, eigenValue1 - a)); - // since the eigen vectors are orthogonal, we derive the second one from the first - vec2 eigenVector2 = vec2(eigenVector1.y, -eigenVector1.x); - - // We use sqrt(8) standard deviations instead of 3 to eliminate more of the splat with a very low opacity. - vec2 basisVector1 = eigenVector1 * splatScale * min(sqrt8 * sqrt(eigenValue1), ${parseInt(maxScreenSpaceSplatSize)}.0); - vec2 basisVector2 = eigenVector2 * splatScale * min(sqrt8 * sqrt(eigenValue2), ${parseInt(maxScreenSpaceSplatSize)}.0); - `; - - if (enableOptionalEffects) { - vertexShaderSource += ` - vColor.a *= splatOpacityFromScene; - `; - } - - vertexShaderSource += ` - vec2 ndcOffset = vec2(vPosition.x * basisVector1 + vPosition.y * basisVector2) * - basisViewport * 2.0 * inverseFocalAdjustment; - - vec4 quadPos = vec4(ndcCenter.xy + ndcOffset, ndcCenter.z, 1.0); - gl_Position = quadPos; - - // Scale the position data we send to the fragment shader - vPosition *= sqrt8; - `; - - vertexShaderSource += SplatMaterial.getVertexShaderFadeIn(); - vertexShaderSource += `}`; - - return vertexShaderSource; - } - - static buildFragmentShader() { - let fragmentShaderSource = ` - precision highp float; - #include - - uniform vec3 debugColor; - - varying vec4 vColor; - varying vec2 vUv; - varying vec2 vPosition; - `; - - fragmentShaderSource += ` - void main () { - // Compute the positional squared distance from the center of the splat to the current fragment. - float A = dot(vPosition, vPosition); - // Since the positional data in vPosition has been scaled by sqrt(8), the squared result will be - // scaled by a factor of 8. If the squared result is larger than 8, it means it is outside the ellipse - // defined by the rectangle formed by vPosition. It also means it's farther - // away than sqrt(8) standard deviations from the mean. - if (A > 8.0) discard; - vec3 color = vColor.rgb; - - // Since the rendered splat is scaled by sqrt(8), the inverse covariance matrix that is part of - // the gaussian formula becomes the identity matrix. We're then left with (X - mean) * (X - mean), - // and since 'mean' is zero, we have X * X, which is the same as A: - float opacity = exp(-0.5 * A) * vColor.a; - - gl_FragColor = vec4(color.rgb, opacity); - } - `; - - return fragmentShaderSource; - } - -} - -class SplatMaterial2D { - - /** - * Build the Three.js material that is used to render the splats. - * @param {number} dynamicMode If true, it means the scene geometry represented by this splat mesh is not stationary or - * that the splat count might change - * @param {boolean} enableOptionalEffects When true, allows for usage of extra properties and attributes in the shader for effects - * such as opacity adjustment. Default is false for performance reasons. - * @param {number} splatScale Value by which all splats are scaled in screen-space (default is 1.0) - * @param {number} pointCloudModeEnabled Render all splats as screen-space circles - * @param {number} maxSphericalHarmonicsDegree Degree of spherical harmonics to utilize in rendering splats - * @return {THREE.ShaderMaterial} - */ - static build(dynamicMode = false, enableOptionalEffects = false, splatScale = 1.0, - pointCloudModeEnabled = false, maxSphericalHarmonicsDegree = 0) { - - const customVertexVars = ` - uniform vec2 scaleRotationsTextureSize; - uniform highp sampler2D scaleRotationsTexture; - varying mat3 vT; - varying vec2 vQuadCenter; - varying vec2 vFragCoord; - `; - - let vertexShaderSource = SplatMaterial.buildVertexShaderBase(dynamicMode, enableOptionalEffects, - maxSphericalHarmonicsDegree, customVertexVars); - vertexShaderSource += SplatMaterial2D.buildVertexShaderProjection(); - const fragmentShaderSource = SplatMaterial2D.buildFragmentShader(); - - const uniforms = SplatMaterial.getUniforms(dynamicMode, enableOptionalEffects, - maxSphericalHarmonicsDegree, splatScale, pointCloudModeEnabled); - - uniforms['scaleRotationsTexture'] = { - 'type': 't', - 'value': null - }; - uniforms['scaleRotationsTextureSize'] = { - 'type': 'v2', - 'value': new THREE.Vector2(1024, 1024) - }; - - const material = new THREE.ShaderMaterial({ - uniforms: uniforms, - vertexShader: vertexShaderSource, - fragmentShader: fragmentShaderSource, - transparent: true, - alphaTest: 1.0, - blending: THREE.NormalBlending, - depthTest: true, - depthWrite: false, - side: THREE.DoubleSide - }); - - return material; - } - - static buildVertexShaderProjection() { - - // Original CUDA code for calculating splat-to-screen transformation, for reference - /* - glm::mat3 R = quat_to_rotmat(rot); - glm::mat3 S = scale_to_mat(scale, mod); - glm::mat3 L = R * S; - - // center of Gaussians in the camera coordinate - glm::mat3x4 splat2world = glm::mat3x4( - glm::vec4(L[0], 0.0), - glm::vec4(L[1], 0.0), - glm::vec4(p_orig.x, p_orig.y, p_orig.z, 1) - ); - - glm::mat4 world2ndc = glm::mat4( - projmatrix[0], projmatrix[4], projmatrix[8], projmatrix[12], - projmatrix[1], projmatrix[5], projmatrix[9], projmatrix[13], - projmatrix[2], projmatrix[6], projmatrix[10], projmatrix[14], - projmatrix[3], projmatrix[7], projmatrix[11], projmatrix[15] - ); - - glm::mat3x4 ndc2pix = glm::mat3x4( - glm::vec4(float(W) / 2.0, 0.0, 0.0, float(W-1) / 2.0), - glm::vec4(0.0, float(H) / 2.0, 0.0, float(H-1) / 2.0), - glm::vec4(0.0, 0.0, 0.0, 1.0) - ); - - T = glm::transpose(splat2world) * world2ndc * ndc2pix; - normal = transformVec4x3({L[2].x, L[2].y, L[2].z}, viewmatrix); - */ - - // Compute a 2D-to-2D mapping matrix from a tangent plane into a image plane - // given a 2D gaussian parameters. T = WH (from the paper: https://arxiv.org/pdf/2403.17888) - let vertexShaderSource = ` - - vec4 scaleRotationA = texture(scaleRotationsTexture, getDataUVF(nearestEvenIndex, 1.5, - oddOffset, scaleRotationsTextureSize)); - vec4 scaleRotationB = texture(scaleRotationsTexture, getDataUVF(nearestEvenIndex, 1.5, - oddOffset + uint(1), scaleRotationsTextureSize)); - - vec3 scaleRotation123 = vec3(scaleRotationA.rgb) * (1.0 - fOddOffset) + - vec3(scaleRotationA.ba, scaleRotationB.r) * fOddOffset; - vec3 scaleRotation456 = vec3(scaleRotationA.a, scaleRotationB.rg) * (1.0 - fOddOffset) + - vec3(scaleRotationB.gba) * fOddOffset; - - float missingW = sqrt(1.0 - scaleRotation456.x * scaleRotation456.x - scaleRotation456.y * - scaleRotation456.y - scaleRotation456.z * scaleRotation456.z); - mat3 R = quaternionToRotationMatrix(scaleRotation456.r, scaleRotation456.g, scaleRotation456.b, missingW); - mat3 S = mat3(scaleRotation123.r, 0.0, 0.0, - 0.0, scaleRotation123.g, 0.0, - 0.0, 0.0, scaleRotation123.b); - - mat3 L = R * S; - - mat3x4 splat2World = mat3x4(vec4(L[0], 0.0), - vec4(L[1], 0.0), - vec4(splatCenter.x, splatCenter.y, splatCenter.z, 1.0)); - - mat4 world2ndc = transpose(projectionMatrix * transformModelViewMatrix); - - mat3x4 ndc2pix = mat3x4(vec4(viewport.x / 2.0, 0.0, 0.0, (viewport.x - 1.0) / 2.0), - vec4(0.0, viewport.y / 2.0, 0.0, (viewport.y - 1.0) / 2.0), - vec4(0.0, 0.0, 0.0, 1.0)); - - mat3 T = transpose(splat2World) * world2ndc * ndc2pix; - vec3 normal = vec3(viewMatrix * vec4(L[0][2], L[1][2], L[2][2], 0.0)); - `; - - // Original CUDA code for projection to 2D, for reference - /* - float3 T0 = {T[0][0], T[0][1], T[0][2]}; - float3 T1 = {T[1][0], T[1][1], T[1][2]}; - float3 T3 = {T[2][0], T[2][1], T[2][2]}; - - // Compute AABB - float3 temp_point = {1.0f, 1.0f, -1.0f}; - float distance = sumf3(T3 * T3 * temp_point); - float3 f = (1 / distance) * temp_point; - if (distance == 0.0) return false; - - point_image = { - sumf3(f * T0 * T3), - sumf3(f * T1 * T3) - }; - - float2 temp = { - sumf3(f * T0 * T0), - sumf3(f * T1 * T1) - }; - float2 half_extend = point_image * point_image - temp; - extent = sqrtf2(maxf2(1e-4, half_extend)); - return true; - */ - - // Computing the bounding box of the 2D Gaussian and its center - // The center of the bounding box is used to create a low pass filter. - // This code is based off the reference implementation and creates an AABB aligned - // with the screen for the quad to be rendered. - const referenceQuadGeneration = ` - vec3 T0 = vec3(T[0][0], T[0][1], T[0][2]); - vec3 T1 = vec3(T[1][0], T[1][1], T[1][2]); - vec3 T3 = vec3(T[2][0], T[2][1], T[2][2]); - - vec3 tempPoint = vec3(1.0, 1.0, -1.0); - float distance = (T3.x * T3.x * tempPoint.x) + (T3.y * T3.y * tempPoint.y) + (T3.z * T3.z * tempPoint.z); - vec3 f = (1.0 / distance) * tempPoint; - if (abs(distance) < 0.00001) return; - - float pointImageX = (T0.x * T3.x * f.x) + (T0.y * T3.y * f.y) + (T0.z * T3.z * f.z); - float pointImageY = (T1.x * T3.x * f.x) + (T1.y * T3.y * f.y) + (T1.z * T3.z * f.z); - vec2 pointImage = vec2(pointImageX, pointImageY); - - float tempX = (T0.x * T0.x * f.x) + (T0.y * T0.y * f.y) + (T0.z * T0.z * f.z); - float tempY = (T1.x * T1.x * f.x) + (T1.y * T1.y * f.y) + (T1.z * T1.z * f.z); - vec2 temp = vec2(tempX, tempY); - - vec2 halfExtend = pointImage * pointImage - temp; - vec2 extent = sqrt(max(vec2(0.0001), halfExtend)); - float radius = max(extent.x, extent.y); - - vec2 ndcOffset = ((position.xy * radius * 3.0) * basisViewport * 2.0); - - vec4 quadPos = vec4(ndcCenter.xy + ndcOffset, ndcCenter.z, 1.0); - gl_Position = quadPos; - - vT = T; - vQuadCenter = pointImage; - vFragCoord = (quadPos.xy * 0.5 + 0.5) * viewport; - `; - - const useRefImplementation = false; - if (useRefImplementation) { - vertexShaderSource += referenceQuadGeneration; - } else { - // Create a quad that is aligned with the eigen vectors of the projected gaussian for rendering. - // This is a different approach than the reference implementation, similar to how the rendering of - // 3D gaussians in this viewer differs from the reference implementation. If the quad is too small - // (smaller than a pixel), then revert to the reference implementation. - vertexShaderSource += ` - - mat4 splat2World4 = mat4(vec4(L[0], 0.0), - vec4(L[1], 0.0), - vec4(L[2], 0.0), - vec4(splatCenter.x, splatCenter.y, splatCenter.z, 1.0)); - - mat4 Tt = transpose(transpose(splat2World4) * world2ndc); - - vec4 tempPoint1 = Tt * vec4(1.0, 0.0, 0.0, 1.0); - tempPoint1 /= tempPoint1.w; - - vec4 tempPoint2 = Tt * vec4(0.0, 1.0, 0.0, 1.0); - tempPoint2 /= tempPoint2.w; - - vec4 center = Tt * vec4(0.0, 0.0, 0.0, 1.0); - center /= center.w; - - vec2 basisVector1 = tempPoint1.xy - center.xy; - vec2 basisVector2 = tempPoint2.xy - center.xy; - - vec2 basisVector1Screen = basisVector1 * 0.5 * viewport; - vec2 basisVector2Screen = basisVector2 * 0.5 * viewport; - - const float minPix = 1.; - if (length(basisVector1Screen) < minPix || length(basisVector2Screen) < minPix) { - ${referenceQuadGeneration} - } else { - vec2 ndcOffset = vec2(position.x * basisVector1 + position.y * basisVector2) * 3.0 * inverseFocalAdjustment; - vec4 quadPos = vec4(ndcCenter.xy + ndcOffset, ndcCenter.z, 1.0); - gl_Position = quadPos; - - vT = T; - vQuadCenter = center.xy; - vFragCoord = (quadPos.xy * 0.5 + 0.5) * viewport; - } - `; - } - - vertexShaderSource += SplatMaterial.getVertexShaderFadeIn(); - vertexShaderSource += `}`; - - return vertexShaderSource; - } - - static buildFragmentShader() { - - // Original CUDA code for splat intersection, for reference - /* - const float2 xy = collected_xy[j]; - const float3 Tu = collected_Tu[j]; - const float3 Tv = collected_Tv[j]; - const float3 Tw = collected_Tw[j]; - float3 k = pix.x * Tw - Tu; - float3 l = pix.y * Tw - Tv; - float3 p = cross(k, l); - if (p.z == 0.0) continue; - float2 s = {p.x / p.z, p.y / p.z}; - float rho3d = (s.x * s.x + s.y * s.y); - float2 d = {xy.x - pixf.x, xy.y - pixf.y}; - float rho2d = FilterInvSquare * (d.x * d.x + d.y * d.y); - - // compute intersection and depth - float rho = min(rho3d, rho2d); - float depth = (rho3d <= rho2d) ? (s.x * Tw.x + s.y * Tw.y) + Tw.z : Tw.z; - if (depth < near_n) continue; - float4 nor_o = collected_normal_opacity[j]; - float normal[3] = {nor_o.x, nor_o.y, nor_o.z}; - float opa = nor_o.w; - - float power = -0.5f * rho; - if (power > 0.0f) - continue; - - // Eq. (2) from 3D Gaussian splatting paper. - // Obtain alpha by multiplying with Gaussian opacity - // and its exponential falloff from mean. - // Avoid numerical instabilities (see paper appendix). - float alpha = min(0.99f, opa * exp(power)); - if (alpha < 1.0f / 255.0f) - continue; - float test_T = T * (1 - alpha); - if (test_T < 0.0001f) - { - done = true; - continue; - } - - float w = alpha * T; - */ - let fragmentShaderSource = ` - precision highp float; - #include - - uniform vec3 debugColor; - - varying vec4 vColor; - varying vec2 vUv; - varying vec2 vPosition; - varying mat3 vT; - varying vec2 vQuadCenter; - varying vec2 vFragCoord; - - void main () { - - const float FilterInvSquare = 2.0; - const float near_n = 0.2; - const float T = 1.0; - - vec2 xy = vQuadCenter; - vec3 Tu = vT[0]; - vec3 Tv = vT[1]; - vec3 Tw = vT[2]; - vec3 k = vFragCoord.x * Tw - Tu; - vec3 l = vFragCoord.y * Tw - Tv; - vec3 p = cross(k, l); - if (p.z == 0.0) discard; - vec2 s = vec2(p.x / p.z, p.y / p.z); - float rho3d = (s.x * s.x + s.y * s.y); - vec2 d = vec2(xy.x - vFragCoord.x, xy.y - vFragCoord.y); - float rho2d = FilterInvSquare * (d.x * d.x + d.y * d.y); - - // compute intersection and depth - float rho = min(rho3d, rho2d); - float depth = (rho3d <= rho2d) ? (s.x * Tw.x + s.y * Tw.y) + Tw.z : Tw.z; - if (depth < near_n) discard; - // vec4 nor_o = collected_normal_opacity[j]; - // float normal[3] = {nor_o.x, nor_o.y, nor_o.z}; - float opa = vColor.a; - - float power = -0.5f * rho; - if (power > 0.0f) discard; - - // Eq. (2) from 3D Gaussian splatting paper. - // Obtain alpha by multiplying with Gaussian opacity - // and its exponential falloff from mean. - // Avoid numerical instabilities (see paper appendix). - float alpha = min(0.99f, opa * exp(power)); - if (alpha < 1.0f / 255.0f) discard; - float test_T = T * (1.0 - alpha); - if (test_T < 0.0001)discard; - - float w = alpha * T; - gl_FragColor = vec4(vColor.rgb, w); - } - `; - - return fragmentShaderSource; - } -} - -class SplatGeometry { - - /** - * Build the Three.js geometry that will be used to render the splats. The geometry is instanced and is made up of - * vertices for a single quad as well as an attribute buffer for the splat indexes. - * @param {number} maxSplatCount The maximum number of splats that the geometry will need to accomodate - * @return {THREE.InstancedBufferGeometry} - */ - static build(maxSplatCount) { - - const baseGeometry = new THREE.BufferGeometry(); - baseGeometry.setIndex([0, 1, 2, 0, 2, 3]); - - // Vertices for the instanced quad - const positionsArray = new Float32Array(4 * 3); - const positions = new THREE.BufferAttribute(positionsArray, 3); - baseGeometry.setAttribute('position', positions); - positions.setXYZ(0, -1.0, -1.0, 0.0); - positions.setXYZ(1, -1.0, 1.0, 0.0); - positions.setXYZ(2, 1.0, 1.0, 0.0); - positions.setXYZ(3, 1.0, -1.0, 0.0); - positions.needsUpdate = true; - - const geometry = new THREE.InstancedBufferGeometry().copy(baseGeometry); - - // Splat index buffer - const splatIndexArray = new Uint32Array(maxSplatCount); - const splatIndexes = new THREE.InstancedBufferAttribute(splatIndexArray, 1, false); - splatIndexes.setUsage(THREE.DynamicDrawUsage); - geometry.setAttribute('splatIndex', splatIndexes); - - geometry.instanceCount = 0; - - return geometry; - } -} - -/** - * SplatScene: Descriptor for a single splat scene managed by an instance of SplatMesh. - */ -class SplatScene extends THREE.Object3D { - - constructor(splatBuffer, position = new THREE.Vector3(), quaternion = new THREE.Quaternion(), - scale = new THREE.Vector3(1, 1, 1), minimumAlpha = 1, opacity = 1.0, visible = true) { - super(); - this.splatBuffer = splatBuffer; - this.position.copy(position); - this.quaternion.copy(quaternion); - this.scale.copy(scale); - this.transform = new THREE.Matrix4(); - this.minimumAlpha = minimumAlpha; - this.opacity = opacity; - this.visible = visible; - } - - copyTransformData(otherScene) { - this.position.copy(otherScene.position); - this.quaternion.copy(otherScene.quaternion); - this.scale.copy(otherScene.scale); - this.transform.copy(otherScene.transform); - } - - updateTransform(dynamicMode) { - if (dynamicMode) { - if (this.matrixWorldAutoUpdate) this.updateWorldMatrix(true, false); - this.transform.copy(this.matrixWorld); - } else { - if (this.matrixAutoUpdate) this.updateMatrix(); - this.transform.copy(this.matrix); - } - } -} - -class SplatTreeNode { - - static idGen = 0; - - constructor(min, max, depth, id) { - this.min = new THREE.Vector3().copy(min); - this.max = new THREE.Vector3().copy(max); - this.boundingBox = new THREE.Box3(this.min, this.max); - this.center = new THREE.Vector3().copy(this.max).sub(this.min).multiplyScalar(0.5).add(this.min); - this.depth = depth; - this.children = []; - this.data = null; - this.id = id || SplatTreeNode.idGen++; - } - -} - -class SplatSubTree { - - constructor(maxDepth, maxCentersPerNode) { - this.maxDepth = maxDepth; - this.maxCentersPerNode = maxCentersPerNode; - this.sceneDimensions = new THREE.Vector3(); - this.sceneMin = new THREE.Vector3(); - this.sceneMax = new THREE.Vector3(); - this.rootNode = null; - this.nodesWithIndexes = []; - this.splatMesh = null; - } - - static convertWorkerSubTreeNode(workerSubTreeNode) { - const minVector = new THREE.Vector3().fromArray(workerSubTreeNode.min); - const maxVector = new THREE.Vector3().fromArray(workerSubTreeNode.max); - const convertedNode = new SplatTreeNode(minVector, maxVector, workerSubTreeNode.depth, workerSubTreeNode.id); - if (workerSubTreeNode.data.indexes) { - convertedNode.data = { - 'indexes': [] - }; - for (let index of workerSubTreeNode.data.indexes) { - convertedNode.data.indexes.push(index); - } - } - if (workerSubTreeNode.children) { - for (let child of workerSubTreeNode.children) { - convertedNode.children.push(SplatSubTree.convertWorkerSubTreeNode(child)); - } - } - return convertedNode; - } - - static convertWorkerSubTree(workerSubTree, splatMesh) { - const convertedSubTree = new SplatSubTree(workerSubTree.maxDepth, workerSubTree.maxCentersPerNode); - convertedSubTree.sceneMin = new THREE.Vector3().fromArray(workerSubTree.sceneMin); - convertedSubTree.sceneMax = new THREE.Vector3().fromArray(workerSubTree.sceneMax); - - convertedSubTree.splatMesh = splatMesh; - convertedSubTree.rootNode = SplatSubTree.convertWorkerSubTreeNode(workerSubTree.rootNode); - - - const visitLeavesFromNode = (node, visitFunc) => { - if (node.children.length === 0) visitFunc(node); - for (let child of node.children) { - visitLeavesFromNode(child, visitFunc); - } - }; - - convertedSubTree.nodesWithIndexes = []; - visitLeavesFromNode(convertedSubTree.rootNode, (node) => { - if (node.data && node.data.indexes && node.data.indexes.length > 0) { - convertedSubTree.nodesWithIndexes.push(node); - } - }); - - return convertedSubTree; - } -} - -function createSplatTreeWorker(self) { - - let WorkerSplatTreeNodeIDGen = 0; - - class WorkerBox3 { - - constructor(min, max) { - this.min = [min[0], min[1], min[2]]; - this.max = [max[0], max[1], max[2]]; - } - - containsPoint(point) { - return point[0] >= this.min[0] && point[0] <= this.max[0] && - point[1] >= this.min[1] && point[1] <= this.max[1] && - point[2] >= this.min[2] && point[2] <= this.max[2]; - } - } - - class WorkerSplatSubTree { - - constructor(maxDepth, maxCentersPerNode) { - this.maxDepth = maxDepth; - this.maxCentersPerNode = maxCentersPerNode; - this.sceneDimensions = []; - this.sceneMin = []; - this.sceneMax = []; - this.rootNode = null; - this.addedIndexes = {}; - this.nodesWithIndexes = []; - this.splatMesh = null; - this.disposed = false; - } - - } - - class WorkerSplatTreeNode { - - constructor(min, max, depth, id) { - this.min = [min[0], min[1], min[2]]; - this.max = [max[0], max[1], max[2]]; - this.center = [(max[0] - min[0]) * 0.5 + min[0], - (max[1] - min[1]) * 0.5 + min[1], - (max[2] - min[2]) * 0.5 + min[2]]; - this.depth = depth; - this.children = []; - this.data = null; - this.id = id || WorkerSplatTreeNodeIDGen++; - } - - } - - processSplatTreeNode = function(tree, node, indexToCenter, sceneCenters) { - const splatCount = node.data.indexes.length; - - if (splatCount < tree.maxCentersPerNode || node.depth > tree.maxDepth) { - const newIndexes = []; - for (let i = 0; i < node.data.indexes.length; i++) { - if (!tree.addedIndexes[node.data.indexes[i]]) { - newIndexes.push(node.data.indexes[i]); - tree.addedIndexes[node.data.indexes[i]] = true; - } - } - node.data.indexes = newIndexes; - node.data.indexes.sort((a, b) => { - if (a > b) return 1; - else return -1; - }); - tree.nodesWithIndexes.push(node); - return; - } - - const nodeDimensions = [node.max[0] - node.min[0], - node.max[1] - node.min[1], - node.max[2] - node.min[2]]; - const halfDimensions = [nodeDimensions[0] * 0.5, - nodeDimensions[1] * 0.5, - nodeDimensions[2] * 0.5]; - const nodeCenter = [node.min[0] + halfDimensions[0], - node.min[1] + halfDimensions[1], - node.min[2] + halfDimensions[2]]; - - const childrenBounds = [ - // top section, clockwise from upper-left (looking from above, +Y) - new WorkerBox3([nodeCenter[0] - halfDimensions[0], nodeCenter[1], nodeCenter[2] - halfDimensions[2]], - [nodeCenter[0], nodeCenter[1] + halfDimensions[1], nodeCenter[2]]), - new WorkerBox3([nodeCenter[0], nodeCenter[1], nodeCenter[2] - halfDimensions[2]], - [nodeCenter[0] + halfDimensions[0], nodeCenter[1] + halfDimensions[1], nodeCenter[2]]), - new WorkerBox3([nodeCenter[0], nodeCenter[1], nodeCenter[2]], - [nodeCenter[0] + halfDimensions[0], nodeCenter[1] + halfDimensions[1], nodeCenter[2] + halfDimensions[2]]), - new WorkerBox3([nodeCenter[0] - halfDimensions[0], nodeCenter[1], nodeCenter[2]], - [nodeCenter[0], nodeCenter[1] + halfDimensions[1], nodeCenter[2] + halfDimensions[2]]), - - // bottom section, clockwise from lower-left (looking from above, +Y) - new WorkerBox3([nodeCenter[0] - halfDimensions[0], nodeCenter[1] - halfDimensions[1], nodeCenter[2] - halfDimensions[2]], - [nodeCenter[0], nodeCenter[1], nodeCenter[2]]), - new WorkerBox3([nodeCenter[0], nodeCenter[1] - halfDimensions[1], nodeCenter[2] - halfDimensions[2]], - [nodeCenter[0] + halfDimensions[0], nodeCenter[1], nodeCenter[2]]), - new WorkerBox3([nodeCenter[0], nodeCenter[1] - halfDimensions[1], nodeCenter[2]], - [nodeCenter[0] + halfDimensions[0], nodeCenter[1], nodeCenter[2] + halfDimensions[2]]), - new WorkerBox3([nodeCenter[0] - halfDimensions[0], nodeCenter[1] - halfDimensions[1], nodeCenter[2]], - [nodeCenter[0], nodeCenter[1], nodeCenter[2] + halfDimensions[2]]), - ]; - - const splatCounts = []; - const baseIndexes = []; - for (let i = 0; i < childrenBounds.length; i++) { - splatCounts[i] = 0; - baseIndexes[i] = []; - } - - const center = [0, 0, 0]; - for (let i = 0; i < splatCount; i++) { - const splatGlobalIndex = node.data.indexes[i]; - const centerBase = indexToCenter[splatGlobalIndex]; - center[0] = sceneCenters[centerBase]; - center[1] = sceneCenters[centerBase + 1]; - center[2] = sceneCenters[centerBase + 2]; - for (let j = 0; j < childrenBounds.length; j++) { - if (childrenBounds[j].containsPoint(center)) { - splatCounts[j]++; - baseIndexes[j].push(splatGlobalIndex); - } - } - } - - for (let i = 0; i < childrenBounds.length; i++) { - const childNode = new WorkerSplatTreeNode(childrenBounds[i].min, childrenBounds[i].max, node.depth + 1); - childNode.data = { - 'indexes': baseIndexes[i] - }; - node.children.push(childNode); - } - - node.data = {}; - for (let child of node.children) { - processSplatTreeNode(tree, child, indexToCenter, sceneCenters); - } - return; - }; - - const buildSubTree = (sceneCenters, maxDepth, maxCentersPerNode) => { - - const sceneMin = [0, 0, 0]; - const sceneMax = [0, 0, 0]; - const indexes = []; - const centerCount = Math.floor(sceneCenters.length / 4); - for ( let i = 0; i < centerCount; i ++) { - const base = i * 4; - const x = sceneCenters[base]; - const y = sceneCenters[base + 1]; - const z = sceneCenters[base + 2]; - const index = Math.round(sceneCenters[base + 3]); - if (i === 0 || x < sceneMin[0]) sceneMin[0] = x; - if (i === 0 || x > sceneMax[0]) sceneMax[0] = x; - if (i === 0 || y < sceneMin[1]) sceneMin[1] = y; - if (i === 0 || y > sceneMax[1]) sceneMax[1] = y; - if (i === 0 || z < sceneMin[2]) sceneMin[2] = z; - if (i === 0 || z > sceneMax[2]) sceneMax[2] = z; - indexes.push(index); - } - const subTree = new WorkerSplatSubTree(maxDepth, maxCentersPerNode); - subTree.sceneMin = sceneMin; - subTree.sceneMax = sceneMax; - subTree.rootNode = new WorkerSplatTreeNode(subTree.sceneMin, subTree.sceneMax, 0); - subTree.rootNode.data = { - 'indexes': indexes - }; - - return subTree; - }; - - function createSplatTree(allCenters, maxDepth, maxCentersPerNode) { - const indexToCenter = []; - for (let sceneCenters of allCenters) { - const centerCount = Math.floor(sceneCenters.length / 4); - for ( let i = 0; i < centerCount; i ++) { - const base = i * 4; - const index = Math.round(sceneCenters[base + 3]); - indexToCenter[index] = base; - } - } - const subTrees = []; - for (let sceneCenters of allCenters) { - const subTree = buildSubTree(sceneCenters, maxDepth, maxCentersPerNode); - subTrees.push(subTree); - processSplatTreeNode(subTree, subTree.rootNode, indexToCenter, sceneCenters); - } - self.postMessage({ - 'subTrees': subTrees - }); - } - - self.onmessage = (e) => { - if (e.data.process) { - createSplatTree(e.data.process.centers, e.data.process.maxDepth, e.data.process.maxCentersPerNode); - } - }; -} - -function workerProcessCenters(splatTreeWorker, centers, transferBuffers, maxDepth, maxCentersPerNode) { - splatTreeWorker.postMessage({ - 'process': { - 'centers': centers, - 'maxDepth': maxDepth, - 'maxCentersPerNode': maxCentersPerNode - } - }, transferBuffers); -} - -function checkAndCreateWorker() { - const splatTreeWorker = new Worker( - URL.createObjectURL( - new Blob(['(', createSplatTreeWorker.toString(), ')(self)'], { - type: 'application/javascript', - }), - ), - ); - return splatTreeWorker; -} - -/** - * SplatTree: Octree tailored to splat data from a SplatMesh instance - */ -class SplatTree { - - constructor(maxDepth, maxCentersPerNode) { - this.maxDepth = maxDepth; - this.maxCentersPerNode = maxCentersPerNode; - this.subTrees = []; - this.splatMesh = null; - } - - - dispose() { - this.diposeSplatTreeWorker(); - this.disposed = true; - } - - diposeSplatTreeWorker() { - if (this.splatTreeWorker) this.splatTreeWorker.terminate(); - this.splatTreeWorker = null; - }; - - /** - * Construct this instance of SplatTree from an instance of SplatMesh. - * - * @param {SplatMesh} splatMesh The instance of SplatMesh from which to construct this splat tree. - * @param {function} filterFunc Optional function to filter out unwanted splats. - * @param {function} onIndexesUpload Function to be called when the upload of splat centers to the splat tree - * builder worker starts and finishes. - * @param {function} onSplatTreeConstruction Function to be called when the conversion of the local splat tree from - * the format produced by the splat tree builder worker starts and ends. - * @return {undefined} - */ - processSplatMesh = function(splatMesh, filterFunc = () => true, onIndexesUpload, onSplatTreeConstruction) { - if (!this.splatTreeWorker) this.splatTreeWorker = checkAndCreateWorker(); - - this.splatMesh = splatMesh; - this.subTrees = []; - const center = new THREE.Vector3(); - - const addCentersForScene = (splatOffset, splatCount) => { - const sceneCenters = new Float32Array(splatCount * 4); - let addedCount = 0; - for (let i = 0; i < splatCount; i++) { - const globalSplatIndex = i + splatOffset; - if (filterFunc(globalSplatIndex)) { - splatMesh.getSplatCenter(globalSplatIndex, center); - const addBase = addedCount * 4; - sceneCenters[addBase] = center.x; - sceneCenters[addBase + 1] = center.y; - sceneCenters[addBase + 2] = center.z; - sceneCenters[addBase + 3] = globalSplatIndex; - addedCount++; - } - } - return sceneCenters; - }; - - return new Promise((resolve) => { - - const checkForEarlyExit = () => { - if (this.disposed) { - this.diposeSplatTreeWorker(); - resolve(); - return true; - } - return false; - }; - - if (onIndexesUpload) onIndexesUpload(false); - - delayedExecute(() => { - - if (checkForEarlyExit()) return; - - const allCenters = []; - if (splatMesh.dynamicMode) { - let splatOffset = 0; - for (let s = 0; s < splatMesh.scenes.length; s++) { - const scene = splatMesh.getScene(s); - const splatCount = scene.splatBuffer.getSplatCount(); - const sceneCenters = addCentersForScene(splatOffset, splatCount); - allCenters.push(sceneCenters); - splatOffset += splatCount; - } - } else { - const sceneCenters = addCentersForScene(0, splatMesh.getSplatCount()); - allCenters.push(sceneCenters); - } - - this.splatTreeWorker.onmessage = (e) => { - - if (checkForEarlyExit()) return; - - if (e.data.subTrees) { - - if (onSplatTreeConstruction) onSplatTreeConstruction(false); - - delayedExecute(() => { - - if (checkForEarlyExit()) return; - - for (let workerSubTree of e.data.subTrees) { - const convertedSubTree = SplatSubTree.convertWorkerSubTree(workerSubTree, splatMesh); - this.subTrees.push(convertedSubTree); - } - this.diposeSplatTreeWorker(); - - if (onSplatTreeConstruction) onSplatTreeConstruction(true); - - delayedExecute(() => { - resolve(); - }); - - }); - } - }; - - delayedExecute(() => { - if (checkForEarlyExit()) return; - if (onIndexesUpload) onIndexesUpload(true); - const transferBuffers = allCenters.map((array) => array.buffer); - workerProcessCenters(this.splatTreeWorker, allCenters, transferBuffers, this.maxDepth, this.maxCentersPerNode); - }); - - }); - - }); - - }; - - countLeaves() { - - let leafCount = 0; - this.visitLeaves(() => { - leafCount++; - }); - - return leafCount; - } - - visitLeaves(visitFunc) { - - const visitLeavesFromNode = (node, visitFunc) => { - if (node.children.length === 0) visitFunc(node); - for (let child of node.children) { - visitLeavesFromNode(child, visitFunc); - } - }; - - for (let subTree of this.subTrees) { - visitLeavesFromNode(subTree.rootNode, visitFunc); - } - } - -} - -function WebGLExtensions( gl ) { - - const extensions = {}; - - function getExtension( name ) { - - if ( extensions[name] !== undefined ) { - - return extensions[name]; - - } - - let extension; - - switch ( name ) { - - case 'WEBGL_depth_texture': - extension = gl.getExtension( 'WEBGL_depth_texture' ) || gl.getExtension( 'MOZ_WEBGL_depth_texture' ) || - gl.getExtension( 'WEBKIT_WEBGL_depth_texture' ); - break; - - case 'EXT_texture_filter_anisotropic': - extension = gl.getExtension( 'EXT_texture_filter_anisotropic' ) || - gl.getExtension( 'MOZ_EXT_texture_filter_anisotropic' ) || - gl.getExtension( 'WEBKIT_EXT_texture_filter_anisotropic' ); - break; - - case 'WEBGL_compressed_texture_s3tc': - extension = gl.getExtension( 'WEBGL_compressed_texture_s3tc' ) || - gl.getExtension( 'MOZ_WEBGL_compressed_texture_s3tc' ) || - gl.getExtension( 'WEBKIT_WEBGL_compressed_texture_s3tc' ); - break; - - case 'WEBGL_compressed_texture_pvrtc': - extension = gl.getExtension( 'WEBGL_compressed_texture_pvrtc' ) || - gl.getExtension( 'WEBKIT_WEBGL_compressed_texture_pvrtc' ); - break; - - default: - extension = gl.getExtension( name ); - - } - - extensions[name] = extension; - - return extension; - - } - - return { - - has: function( name ) { - - return getExtension( name ) !== null; - - }, - - init: function( capabilities ) { - - if ( capabilities.isWebGL2 ) { - - getExtension( 'EXT_color_buffer_float' ); - getExtension( 'WEBGL_clip_cull_distance' ); - - } else { - - getExtension( 'WEBGL_depth_texture' ); - getExtension( 'OES_texture_float' ); - getExtension( 'OES_texture_half_float' ); - getExtension( 'OES_texture_half_float_linear' ); - getExtension( 'OES_standard_derivatives' ); - getExtension( 'OES_element_index_uint' ); - getExtension( 'OES_vertex_array_object' ); - getExtension( 'ANGLE_instanced_arrays' ); - - } - - getExtension( 'OES_texture_float_linear' ); - getExtension( 'EXT_color_buffer_half_float' ); - getExtension( 'WEBGL_multisampled_render_to_texture' ); - - }, - - get: function( name ) { - - const extension = getExtension( name ); - - if ( extension === null ) { - - console.warn( 'THREE.WebGLRenderer: ' + name + ' extension not supported.' ); - - } - - return extension; - - } - - }; - -} - -function WebGLCapabilities( gl, extensions, parameters ) { - - let maxAnisotropy; - - function getMaxAnisotropy() { - - if ( maxAnisotropy !== undefined ) return maxAnisotropy; - - if ( extensions.has( 'EXT_texture_filter_anisotropic' ) === true ) { - - const extension = extensions.get( 'EXT_texture_filter_anisotropic' ); - - maxAnisotropy = gl.getParameter( extension.MAX_TEXTURE_MAX_ANISOTROPY_EXT ); - - } else { - - maxAnisotropy = 0; - - } - - return maxAnisotropy; - - } - - function getMaxPrecision( precision ) { - - if ( precision === 'highp' ) { - - if ( gl.getShaderPrecisionFormat( gl.VERTEX_SHADER, gl.HIGH_FLOAT ).precision > 0 && - gl.getShaderPrecisionFormat( gl.FRAGMENT_SHADER, gl.HIGH_FLOAT ).precision > 0 ) { - - return 'highp'; - - } - - precision = 'mediump'; - - } - - if ( precision === 'mediump' ) { - - if ( gl.getShaderPrecisionFormat( gl.VERTEX_SHADER, gl.MEDIUM_FLOAT ).precision > 0 && - gl.getShaderPrecisionFormat( gl.FRAGMENT_SHADER, gl.MEDIUM_FLOAT ).precision > 0 ) { - - return 'mediump'; - - } - - } - - return 'lowp'; - - } - - const isWebGL2 = typeof WebGL2RenderingContext !== 'undefined' && gl.constructor.name === 'WebGL2RenderingContext'; - - let precision = parameters.precision !== undefined ? parameters.precision : 'highp'; - const maxPrecision = getMaxPrecision( precision ); - - if ( maxPrecision !== precision ) { - - console.warn( 'THREE.WebGLRenderer:', precision, 'not supported, using', maxPrecision, 'instead.' ); - precision = maxPrecision; - - } - - const drawBuffers = isWebGL2 || extensions.has( 'WEBGL_draw_buffers' ); - - const logarithmicDepthBuffer = parameters.logarithmicDepthBuffer === true; - - const maxTextures = gl.getParameter( gl.MAX_TEXTURE_IMAGE_UNITS ); - const maxVertexTextures = gl.getParameter( gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS ); - const maxTextureSize = gl.getParameter( gl.MAX_TEXTURE_SIZE ); - const maxCubemapSize = gl.getParameter( gl.MAX_CUBE_MAP_TEXTURE_SIZE ); - - const maxAttributes = gl.getParameter( gl.MAX_VERTEX_ATTRIBS ); - const maxVertexUniforms = gl.getParameter( gl.MAX_VERTEX_UNIFORM_VECTORS ); - const maxVaryings = gl.getParameter( gl.MAX_VARYING_VECTORS ); - const maxFragmentUniforms = gl.getParameter( gl.MAX_FRAGMENT_UNIFORM_VECTORS ); - - const vertexTextures = maxVertexTextures > 0; - const floatFragmentTextures = isWebGL2 || extensions.has( 'OES_texture_float' ); - const floatVertexTextures = vertexTextures && floatFragmentTextures; - - const maxSamples = isWebGL2 ? gl.getParameter( gl.MAX_SAMPLES ) : 0; - - return { - - isWebGL2: isWebGL2, - - drawBuffers: drawBuffers, - - getMaxAnisotropy: getMaxAnisotropy, - getMaxPrecision: getMaxPrecision, - - precision: precision, - logarithmicDepthBuffer: logarithmicDepthBuffer, - - maxTextures: maxTextures, - maxVertexTextures: maxVertexTextures, - maxTextureSize: maxTextureSize, - maxCubemapSize: maxCubemapSize, - - maxAttributes: maxAttributes, - maxVertexUniforms: maxVertexUniforms, - maxVaryings: maxVaryings, - maxFragmentUniforms: maxFragmentUniforms, - - vertexTextures: vertexTextures, - floatFragmentTextures: floatFragmentTextures, - floatVertexTextures: floatVertexTextures, - - maxSamples: maxSamples - - }; - -} - -const SceneRevealMode = { - Default: 0, - Gradual: 1, - Instant: 2 -}; - -const LogLevel = { - None: 0, - Error: 1, - Warning: 2, - Info: 3, - Debug: 4 -}; - -const dummyGeometry = new THREE.BufferGeometry(); -const dummyMaterial = new THREE.MeshBasicMaterial(); - -const COVARIANCES_ELEMENTS_PER_SPLAT = 6; -const CENTER_COLORS_ELEMENTS_PER_SPLAT = 4; - -const COVARIANCES_ELEMENTS_PER_TEXEL_STORED = 4; -const COVARIANCES_ELEMENTS_PER_TEXEL_ALLOCATED = 4; -const COVARIANCES_ELEMENTS_PER_TEXEL_COMPRESSED_STORED = 6; -const COVARIANCES_ELEMENTS_PER_TEXEL_COMPRESSED_ALLOCATED = 8; -const SCALES_ROTATIONS_ELEMENTS_PER_TEXEL = 4; -const CENTER_COLORS_ELEMENTS_PER_TEXEL = 4; -const SCENE_INDEXES_ELEMENTS_PER_TEXEL = 1; - -const SCENE_FADEIN_RATE_FAST = 0.012; -const SCENE_FADEIN_RATE_GRADUAL = 0.003; - -const VISIBLE_REGION_EXPANSION_DELTA = 1; - -// Based on my own observations across multiple devices, OSes and browsers, using textures that have one dimension -// greater than 4096 while the other is greater than or equal to 4096 causes issues (Essentially any texture larger -// than 4096 x 4096 (16777216) texels). Specifically it seems all texture data beyond the 4096 x 4096 texel boundary -// is corrupted, while data below that boundary is usable. In these cases the texture has been valid in the eyes of -// both Three.js and WebGL, and the texel format (RG, RGBA, etc.) has not mattered. More investigation will be needed, -// but for now the work-around is to split the spherical harmonics into three textures (one for each color channel). -const MAX_TEXTURE_TEXELS = 16777216; - -/** - * SplatMesh: Container for one or more splat scenes, abstracting them into a single unified container for - * splat data. Additionally contains data structures and code to make the splat data renderable as a Three.js mesh. - */ -class SplatMesh extends THREE.Mesh { - - constructor(splatRenderMode = SplatRenderMode.ThreeD, dynamicMode = false, enableOptionalEffects = false, - halfPrecisionCovariancesOnGPU = false, devicePixelRatio = 1, enableDistancesComputationOnGPU = true, - integerBasedDistancesComputation = false, antialiased = false, maxScreenSpaceSplatSize = 1024, logLevel = LogLevel.None, - sphericalHarmonicsDegree = 0, sceneFadeInRateMultiplier = 1.0, kernel2DSize = 0.3) { - super(dummyGeometry, dummyMaterial); - - // Reference to a Three.js renderer - this.renderer = undefined; - - // Determine how the splats are rendered - this.splatRenderMode = splatRenderMode; - - // When 'dynamicMode' is true, scenes are assumed to be non-static. Dynamic scenes are handled differently - // and certain optimizations cannot be made for them. Additionally, by default, all splat data retrieved from - // this splat mesh will not have their scene transform applied to them if the splat mesh is dynamic. That - // can be overriden via parameters to the individual functions that are used to retrieve splat data. - this.dynamicMode = dynamicMode; - - // When true, allows for usage of extra properties and attributes during rendering for effects such as opacity adjustment. - // Default is false for performance reasons. These properties are separate from transform properties (scale, rotation, position) - // that are enabled by the 'dynamicScene' parameter. - this.enableOptionalEffects = enableOptionalEffects; - - // Use 16-bit floating point values when storing splat covariance data in textures, instead of 32-bit - this.halfPrecisionCovariancesOnGPU = halfPrecisionCovariancesOnGPU; - - // Ratio of the resolution in physical pixels to the resolution in CSS pixels for the current display device - this.devicePixelRatio = devicePixelRatio; - - // Use a transform feedback to calculate splat distances from the camera - this.enableDistancesComputationOnGPU = enableDistancesComputationOnGPU; - - // Use a faster integer-based approach for calculating splat distances from the camera - this.integerBasedDistancesComputation = integerBasedDistancesComputation; - - // When true, will perform additional steps during rendering to address artifacts caused by the rendering of gaussians at a - // substantially different resolution than that at which they were rendered during training. This will only work correctly - // for models that were trained using a process that utilizes this compensation calculation. For more details: - // https://github.com/nerfstudio-project/gsplat/pull/117 - // https://github.com/graphdeco-inria/gaussian-splatting/issues/294#issuecomment-1772688093 - this.antialiased = antialiased; - - // The size of the 2D kernel used for splat rendering - // This will adjust the 2D kernel size after the projection - this.kernel2DSize = kernel2DSize; - - // Specify the maximum clip space splat size, can help deal with large splats that get too unwieldy - this.maxScreenSpaceSplatSize = maxScreenSpaceSplatSize; - - // The verbosity of console logging - this.logLevel = logLevel; - - // Degree 0 means no spherical harmonics - this.sphericalHarmonicsDegree = sphericalHarmonicsDegree; - this.minSphericalHarmonicsDegree = 0; - - this.sceneFadeInRateMultiplier = sceneFadeInRateMultiplier; - - // The individual splat scenes stored in this splat mesh, each containing their own transform - this.scenes = []; - - // Special octree tailored to SplatMesh instances - this.splatTree = null; - this.baseSplatTree = null; - - // Cache textures and the intermediate data used to populate them - this.splatDataTextures = {}; - - this.distancesTransformFeedback = { - 'id': null, - 'vertexShader': null, - 'fragmentShader': null, - 'program': null, - 'centersBuffer': null, - 'sceneIndexesBuffer': null, - 'outDistancesBuffer': null, - 'centersLoc': -1, - 'modelViewProjLoc': -1, - 'sceneIndexesLoc': -1, - 'transformsLocs': [] - }; - - this.globalSplatIndexToLocalSplatIndexMap = []; - this.globalSplatIndexToSceneIndexMap = []; - - this.lastBuildSplatCount = 0; - this.lastBuildScenes = []; - this.lastBuildMaxSplatCount = 0; - this.lastBuildSceneCount = 0; - this.firstRenderTime = -1; - this.finalBuild = false; - - this.webGLUtils = null; - - this.boundingBox = new THREE.Box3(); - this.calculatedSceneCenter = new THREE.Vector3(); - this.maxSplatDistanceFromSceneCenter = 0; - this.visibleRegionBufferRadius = 0; - this.visibleRegionRadius = 0; - this.visibleRegionFadeStartRadius = 0; - this.visibleRegionChanging = false; - - this.splatScale = 1.0; - this.pointCloudModeEnabled = false; - - this.disposed = false; - this.lastRenderer = null; - this.visible = false; - } - - /** - * Build a container for each scene managed by this splat mesh based on an instance of SplatBuffer, along with optional - * transform data (position, scale, rotation) passed to the splat mesh during the build process. - * @param {Array} splatBuffers SplatBuffer instances containing splats for each scene - * @param {Array} sceneOptions Array of options objects: { - * - * position (Array): Position of the scene, acts as an offset from its default position, defaults to [0, 0, 0] - * - * rotation (Array): Rotation of the scene represented as a quaternion, defaults to [0, 0, 0, 1] - * - * scale (Array): Scene's scale, defaults to [1, 1, 1] - * } - * @return {Array} - */ - static buildScenes(parentObject, splatBuffers, sceneOptions) { - const scenes = []; - scenes.length = splatBuffers.length; - for (let i = 0; i < splatBuffers.length; i++) { - const splatBuffer = splatBuffers[i]; - const options = sceneOptions[i] || {}; - let positionArray = options['position'] || [0, 0, 0]; - let rotationArray = options['rotation'] || [0, 0, 0, 1]; - let scaleArray = options['scale'] || [1, 1, 1]; - const position = new THREE.Vector3().fromArray(positionArray); - const rotation = new THREE.Quaternion().fromArray(rotationArray); - const scale = new THREE.Vector3().fromArray(scaleArray); - const scene = SplatMesh.createScene(splatBuffer, position, rotation, scale, - options.splatAlphaRemovalThreshold || 1, options.opacity, options.visible); - parentObject.add(scene); - scenes[i] = scene; - } - return scenes; - } - - static createScene(splatBuffer, position, rotation, scale, minimumAlpha, opacity = 1.0, visible = true) { - return new SplatScene(splatBuffer, position, rotation, scale, minimumAlpha, opacity, visible); - } - - /** - * Build data structures that map global splat indexes (based on a unified index across all splat buffers) to - * local data within a single scene. - * @param {Array} splatBuffers Instances of SplatBuffer off which to build the maps - * @return {object} - */ - static buildSplatIndexMaps(splatBuffers) { - const localSplatIndexMap = []; - const sceneIndexMap = []; - let totalSplatCount = 0; - for (let s = 0; s < splatBuffers.length; s++) { - const splatBuffer = splatBuffers[s]; - const maxSplatCount = splatBuffer.getMaxSplatCount(); - for (let i = 0; i < maxSplatCount; i++) { - localSplatIndexMap[totalSplatCount] = i; - sceneIndexMap[totalSplatCount] = s; - totalSplatCount++; - } - } - return { - localSplatIndexMap, - sceneIndexMap - }; - } - - /** - * Build an instance of SplatTree (a specialized octree) for the given splat mesh. - * @param {Array} minAlphas Array of minimum splat slphas for each scene - * @param {function} onSplatTreeIndexesUpload Function to be called when the upload of splat centers to the splat tree - * builder worker starts and finishes. - * @param {function} onSplatTreeConstruction Function to be called when the conversion of the local splat tree from - * the format produced by the splat tree builder worker starts and ends. - * @return {SplatTree} - */ - buildSplatTree = function(minAlphas = [], onSplatTreeIndexesUpload, onSplatTreeConstruction) { - return new Promise((resolve) => { - this.disposeSplatTree(); - // TODO: expose SplatTree constructor parameters (maximumDepth and maxCentersPerNode) so that they can - // be configured on a per-scene basis - this.baseSplatTree = new SplatTree(8, 1000); - const buildStartTime = performance.now(); - const splatColor = new THREE.Vector4(); - this.baseSplatTree.processSplatMesh(this, (splatIndex) => { - this.getSplatColor(splatIndex, splatColor); - const sceneIndex = this.getSceneIndexForSplat(splatIndex); - const minAlpha = minAlphas[sceneIndex] || 1; - return splatColor.w >= minAlpha; - }, onSplatTreeIndexesUpload, onSplatTreeConstruction) - .then(() => { - const buildTime = performance.now() - buildStartTime; - if (this.logLevel >= LogLevel.Info) console.log('SplatTree build: ' + buildTime + ' ms'); - if (this.disposed) { - resolve(); - } else { - - this.splatTree = this.baseSplatTree; - this.baseSplatTree = null; - - let leavesWithVertices = 0; - let avgSplatCount = 0; - let maxSplatCount = 0; - let nodeCount = 0; - - this.splatTree.visitLeaves((node) => { - const nodeSplatCount = node.data.indexes.length; - if (nodeSplatCount > 0) { - avgSplatCount += nodeSplatCount; - maxSplatCount = Math.max(maxSplatCount, nodeSplatCount); - nodeCount++; - leavesWithVertices++; - } - }); - if (this.logLevel >= LogLevel.Info) { - console.log(`SplatTree leaves: ${this.splatTree.countLeaves()}`); - console.log(`SplatTree leaves with splats:${leavesWithVertices}`); - avgSplatCount = avgSplatCount / nodeCount; - console.log(`Avg splat count per node: ${avgSplatCount}`); - console.log(`Total splat count: ${this.getSplatCount()}`); - } - resolve(); - } - }); - }); - }; - - /** - * Construct this instance of SplatMesh. - * @param {Array} splatBuffers The base splat data, instances of SplatBuffer - * @param {Array} sceneOptions Dynamic options for each scene { - * - * splatAlphaRemovalThreshold: Ignore any splats with an alpha less than the specified - * value (valid range: 0 - 255), defaults to 1 - * - * position (Array): Position of the scene, acts as an offset from its default position, defaults to [0, 0, 0] - * - * rotation (Array): Rotation of the scene represented as a quaternion, defaults to [0, 0, 0, 1] - * - * scale (Array): Scene's scale, defaults to [1, 1, 1] - * - * } - * @param {boolean} keepSceneTransforms For a scene that already exists and is being overwritten, this flag - * says to keep the transform from the existing scene. - * @param {boolean} finalBuild Will the splat mesh be in its final state after this build? - * @param {function} onSplatTreeIndexesUpload Function to be called when the upload of splat centers to the splat tree - * builder worker starts and finishes. - * @param {function} onSplatTreeConstruction Function to be called when the conversion of the local splat tree from - * the format produced by the splat tree builder worker starts and ends. - * @return {object} Object containing info about the splats that are updated - */ - build(splatBuffers, sceneOptions, keepSceneTransforms = true, finalBuild = false, - onSplatTreeIndexesUpload, onSplatTreeConstruction, preserveVisibleRegion = true) { - - this.sceneOptions = sceneOptions; - this.finalBuild = finalBuild; - - const maxSplatCount = SplatMesh.getTotalMaxSplatCountForSplatBuffers(splatBuffers); - - const newScenes = SplatMesh.buildScenes(this, splatBuffers, sceneOptions); - if (keepSceneTransforms) { - for (let i = 0; i < this.scenes.length && i < newScenes.length; i++) { - const newScene = newScenes[i]; - const existingScene = this.getScene(i); - newScene.copyTransformData(existingScene); - } - } - this.scenes = newScenes; - - let minSphericalHarmonicsDegree = 3; - for (let splatBuffer of splatBuffers) { - const splatBufferSphericalHarmonicsDegree = splatBuffer.getMinSphericalHarmonicsDegree(); - if (splatBufferSphericalHarmonicsDegree < minSphericalHarmonicsDegree) { - minSphericalHarmonicsDegree = splatBufferSphericalHarmonicsDegree; - } - } - this.minSphericalHarmonicsDegree = Math.min(minSphericalHarmonicsDegree, this.sphericalHarmonicsDegree); - - let splatBuffersChanged = false; - if (splatBuffers.length !== this.lastBuildScenes.length) { - splatBuffersChanged = true; - } else { - for (let i = 0; i < splatBuffers.length; i++) { - const splatBuffer = splatBuffers[i]; - if (splatBuffer !== this.lastBuildScenes[i].splatBuffer) { - splatBuffersChanged = true; - break; - } - } - } - - let isUpdateBuild = true; - if (this.scenes.length !== 1 || - this.lastBuildSceneCount !== this.scenes.length || - this.lastBuildMaxSplatCount !== maxSplatCount || - splatBuffersChanged) { - isUpdateBuild = false; - } - - if (!isUpdateBuild) { - this.boundingBox = new THREE.Box3(); - if (!preserveVisibleRegion) { - this.maxSplatDistanceFromSceneCenter = 0; - this.visibleRegionBufferRadius = 0; - this.visibleRegionRadius = 0; - this.visibleRegionFadeStartRadius = 0; - this.firstRenderTime = -1; - } - this.lastBuildScenes = []; - this.lastBuildSplatCount = 0; - this.lastBuildMaxSplatCount = 0; - this.disposeMeshData(); - this.geometry = SplatGeometry.build(maxSplatCount); - if (this.splatRenderMode === SplatRenderMode.ThreeD) { - this.material = SplatMaterial3D.build(this.dynamicMode, this.enableOptionalEffects, this.antialiased, - this.maxScreenSpaceSplatSize, this.splatScale, this.pointCloudModeEnabled, - this.minSphericalHarmonicsDegree, this.kernel2DSize); - } else { - this.material = SplatMaterial2D.build(this.dynamicMode, this.enableOptionalEffects, - this.splatScale, this.pointCloudModeEnabled, this.minSphericalHarmonicsDegree); - } - - const indexMaps = SplatMesh.buildSplatIndexMaps(splatBuffers); - this.globalSplatIndexToLocalSplatIndexMap = indexMaps.localSplatIndexMap; - this.globalSplatIndexToSceneIndexMap = indexMaps.sceneIndexMap; - } - - const splatBufferSplatCount = this.getSplatCount(true); - if (this.enableDistancesComputationOnGPU) this.setupDistancesComputationTransformFeedback(); - const dataUpdateResults = this.refreshGPUDataFromSplatBuffers(isUpdateBuild); - - for (let i = 0; i < this.scenes.length; i++) { - this.lastBuildScenes[i] = this.scenes[i]; - } - this.lastBuildSplatCount = splatBufferSplatCount; - this.lastBuildMaxSplatCount = this.getMaxSplatCount(); - this.lastBuildSceneCount = this.scenes.length; - - if (finalBuild && this.scenes.length > 0) { - this.buildSplatTree(sceneOptions.map(options => options.splatAlphaRemovalThreshold || 1), - onSplatTreeIndexesUpload, onSplatTreeConstruction) - .then(() => { - if (this.onSplatTreeReadyCallback) this.onSplatTreeReadyCallback(this.splatTree); - this.onSplatTreeReadyCallback = null; - }); - } - - this.visible = (this.scenes.length > 0); - - return dataUpdateResults; - } - - freeIntermediateSplatData() { - - const deleteTextureData = (texture) => { - delete texture.source.data; - delete texture.image; - texture.onUpdate = null; - }; - - delete this.splatDataTextures.baseData.covariances; - delete this.splatDataTextures.baseData.centers; - delete this.splatDataTextures.baseData.colors; - delete this.splatDataTextures.baseData.sphericalHarmonics; - - delete this.splatDataTextures.centerColors.data; - delete this.splatDataTextures.covariances.data; - if (this.splatDataTextures.sphericalHarmonics) { - delete this.splatDataTextures.sphericalHarmonics.data; - } - if (this.splatDataTextures.sceneIndexes) { - delete this.splatDataTextures.sceneIndexes.data; - } - - this.splatDataTextures.centerColors.texture.needsUpdate = true; - this.splatDataTextures.centerColors.texture.onUpdate = () => { - deleteTextureData(this.splatDataTextures.centerColors.texture); - }; - - this.splatDataTextures.covariances.texture.needsUpdate = true; - this.splatDataTextures.covariances.texture.onUpdate = () => { - deleteTextureData(this.splatDataTextures.covariances.texture); - }; - - if (this.splatDataTextures.sphericalHarmonics) { - if (this.splatDataTextures.sphericalHarmonics.texture) { - this.splatDataTextures.sphericalHarmonics.texture.needsUpdate = true; - this.splatDataTextures.sphericalHarmonics.texture.onUpdate = () => { - deleteTextureData(this.splatDataTextures.sphericalHarmonics.texture); - }; - } else { - this.splatDataTextures.sphericalHarmonics.textures.forEach((texture) => { - texture.needsUpdate = true; - texture.onUpdate = () => { - deleteTextureData(texture); - }; - }); - } - } - if (this.splatDataTextures.sceneIndexes) { - this.splatDataTextures.sceneIndexes.texture.needsUpdate = true; - this.splatDataTextures.sceneIndexes.texture.onUpdate = () => { - deleteTextureData(this.splatDataTextures.sceneIndexes.texture); - }; - } - } - /** - * Dispose all resources held by the splat mesh - */ - dispose() { - this.disposeMeshData(); - this.disposeTextures(); - this.disposeSplatTree(); - if (this.enableDistancesComputationOnGPU) { - if (this.computeDistancesOnGPUSyncTimeout) { - clearTimeout(this.computeDistancesOnGPUSyncTimeout); - this.computeDistancesOnGPUSyncTimeout = null; - } - this.disposeDistancesComputationGPUResources(); - } - this.scenes = []; - this.distancesTransformFeedback = { - 'id': null, - 'vertexShader': null, - 'fragmentShader': null, - 'program': null, - 'centersBuffer': null, - 'sceneIndexesBuffer': null, - 'outDistancesBuffer': null, - 'centersLoc': -1, - 'modelViewProjLoc': -1, - 'sceneIndexesLoc': -1, - 'transformsLocs': [] - }; - this.renderer = null; - - this.globalSplatIndexToLocalSplatIndexMap = []; - this.globalSplatIndexToSceneIndexMap = []; - - this.lastBuildSplatCount = 0; - this.lastBuildScenes = []; - this.lastBuildMaxSplatCount = 0; - this.lastBuildSceneCount = 0; - this.firstRenderTime = -1; - this.finalBuild = false; - - this.webGLUtils = null; - - this.boundingBox = new THREE.Box3(); - this.calculatedSceneCenter = new THREE.Vector3(); - this.maxSplatDistanceFromSceneCenter = 0; - this.visibleRegionBufferRadius = 0; - this.visibleRegionRadius = 0; - this.visibleRegionFadeStartRadius = 0; - this.visibleRegionChanging = false; - - this.splatScale = 1.0; - this.pointCloudModeEnabled = false; - - this.disposed = true; - this.lastRenderer = null; - this.visible = false; - } - - /** - * Dispose of only the Three.js mesh resources (geometry, material, and texture) - */ - disposeMeshData() { - if (this.geometry && this.geometry !== dummyGeometry) { - this.geometry.dispose(); - this.geometry = null; - } - if (this.material) { - this.material.dispose(); - this.material = null; - } - } - - disposeTextures() { - for (let textureKey in this.splatDataTextures) { - if (this.splatDataTextures.hasOwnProperty(textureKey)) { - const textureContainer = this.splatDataTextures[textureKey]; - if (textureContainer.texture) { - textureContainer.texture.dispose(); - textureContainer.texture = null; - } - } - } - this.splatDataTextures = null; - } - - disposeSplatTree() { - if (this.splatTree) { - this.splatTree.dispose(); - this.splatTree = null; - } - if (this.baseSplatTree) { - this.baseSplatTree.dispose(); - this.baseSplatTree = null; - } - } - - getSplatTree() { - return this.splatTree; - } - - onSplatTreeReady(callback) { - this.onSplatTreeReadyCallback = callback; - } - - /** - * Get copies of data that are necessary for splat distance computation: splat center positions and splat - * scene indexes (necessary for applying dynamic scene transformations during distance computation) - * @param {*} start The index at which to start copying data - * @param {*} end The index at which to stop copying data - * @return {object} - */ - getDataForDistancesComputation(start, end) { - const centers = this.integerBasedDistancesComputation ? - this.getIntegerCenters(start, end, true) : - this.getFloatCenters(start, end, true); - const sceneIndexes = this.getSceneIndexes(start, end); - return { - centers, - sceneIndexes - }; - } - - /** - * Refresh data textures and GPU buffers with splat data from the splat buffers belonging to this mesh. - * @param {boolean} sinceLastBuildOnly Specify whether or not to only update for splats that have been added since the last build. - * @return {object} - */ - refreshGPUDataFromSplatBuffers(sinceLastBuildOnly) { - const splatCount = this.getSplatCount(true); - this.refreshDataTexturesFromSplatBuffers(sinceLastBuildOnly); - const updateStart = sinceLastBuildOnly ? this.lastBuildSplatCount : 0; - const { centers, sceneIndexes } = this.getDataForDistancesComputation(updateStart, splatCount - 1); - if (this.enableDistancesComputationOnGPU) { - this.refreshGPUBuffersForDistancesComputation(centers, sceneIndexes, sinceLastBuildOnly); - } - return { - 'from': updateStart, - 'to': splatCount - 1, - 'count': splatCount - updateStart, - 'centers': centers, - 'sceneIndexes': sceneIndexes - }; - } - - /** - * Update the GPU buffers that are used for computing splat distances on the GPU. - * @param {Array} centers Splat center positions - * @param {Array} sceneIndexes Indexes of the scene to which each splat belongs - * @param {boolean} sinceLastBuildOnly Specify whether or not to only update for splats that have been added since the last build. - */ - refreshGPUBuffersForDistancesComputation(centers, sceneIndexes, sinceLastBuildOnly = false) { - const offset = sinceLastBuildOnly ? this.lastBuildSplatCount : 0; - this.updateGPUCentersBufferForDistancesComputation(sinceLastBuildOnly, centers, offset); - this.updateGPUTransformIndexesBufferForDistancesComputation(sinceLastBuildOnly, sceneIndexes, offset); - } - - /** - * Refresh data textures with data from the splat buffers for this mesh. - * @param {boolean} sinceLastBuildOnly Specify whether or not to only update for splats that have been added since the last build. - */ - refreshDataTexturesFromSplatBuffers(sinceLastBuildOnly) { - const splatCount = this.getSplatCount(true); - const fromSplat = this.lastBuildSplatCount; - const toSplat = splatCount - 1; - - if (!sinceLastBuildOnly) { - this.setupDataTextures(); - this.updateBaseDataFromSplatBuffers(); - } else { - this.updateBaseDataFromSplatBuffers(fromSplat, toSplat); - } - - this.updateDataTexturesFromBaseData(fromSplat, toSplat); - this.updateVisibleRegion(sinceLastBuildOnly); - } - - setupDataTextures() { - const maxSplatCount = this.getMaxSplatCount(); - const splatCount = this.getSplatCount(true); - - this.disposeTextures(); - - const computeDataTextureSize = (elementsPerTexel, elementsPerSplat) => { - const texSize = new THREE.Vector2(4096, 1024); - while (texSize.x * texSize.y * elementsPerTexel < maxSplatCount * elementsPerSplat) texSize.y *= 2; - return texSize; - }; - - const getCovariancesElementsPertexelStored = (compressionLevel) => { - return compressionLevel >= 1 ? COVARIANCES_ELEMENTS_PER_TEXEL_COMPRESSED_STORED : COVARIANCES_ELEMENTS_PER_TEXEL_STORED; - }; - - const getCovariancesInitialTextureSpecs = (compressionLevel) => { - const elementsPerTexelStored = getCovariancesElementsPertexelStored(compressionLevel); - const texSize = computeDataTextureSize(elementsPerTexelStored, 6); - return {elementsPerTexelStored, texSize}; - }; - - let covarianceCompressionLevel = this.getTargetCovarianceCompressionLevel(); - const scaleRotationCompressionLevel = 0; - const shCompressionLevel = this.getTargetSphericalHarmonicsCompressionLevel(); - - let covariances; - let scales; - let rotations; - if (this.splatRenderMode === SplatRenderMode.ThreeD) { - const initialCovTexSpecs = getCovariancesInitialTextureSpecs(covarianceCompressionLevel); - if (initialCovTexSpecs.texSize.x * initialCovTexSpecs.texSize.y > MAX_TEXTURE_TEXELS && covarianceCompressionLevel === 0) { - covarianceCompressionLevel = 1; - } - covariances = new Float32Array(maxSplatCount * COVARIANCES_ELEMENTS_PER_SPLAT); - } else { - scales = new Float32Array(maxSplatCount * 3); - rotations = new Float32Array(maxSplatCount * 4); - } - - const centers = new Float32Array(maxSplatCount * 3); - const colors = new Uint8Array(maxSplatCount * 4); - - let SphericalHarmonicsArrayType = Float32Array; - if (shCompressionLevel === 1) SphericalHarmonicsArrayType = Uint16Array; - else if (shCompressionLevel === 2) SphericalHarmonicsArrayType = Uint8Array; - const shComponentCount = getSphericalHarmonicsComponentCountForDegree(this.minSphericalHarmonicsDegree); - const shData = this.minSphericalHarmonicsDegree ? new SphericalHarmonicsArrayType(maxSplatCount * shComponentCount) : undefined; - - // set up centers/colors data texture - const centersColsTexSize = computeDataTextureSize(CENTER_COLORS_ELEMENTS_PER_TEXEL, 4); - const paddedCentersCols = new Uint32Array(centersColsTexSize.x * centersColsTexSize.y * CENTER_COLORS_ELEMENTS_PER_TEXEL); - SplatMesh.updateCenterColorsPaddedData(0, splatCount - 1, centers, colors, paddedCentersCols); - - const centersColsTex = new THREE.DataTexture(paddedCentersCols, centersColsTexSize.x, centersColsTexSize.y, - THREE.RGBAIntegerFormat, THREE.UnsignedIntType); - centersColsTex.internalFormat = 'RGBA32UI'; - centersColsTex.needsUpdate = true; - this.material.uniforms.centersColorsTexture.value = centersColsTex; - this.material.uniforms.centersColorsTextureSize.value.copy(centersColsTexSize); - this.material.uniformsNeedUpdate = true; - - this.splatDataTextures = { - 'baseData': { - 'covariances': covariances, - 'scales': scales, - 'rotations': rotations, - 'centers': centers, - 'colors': colors, - 'sphericalHarmonics': shData - }, - 'centerColors': { - 'data': paddedCentersCols, - 'texture': centersColsTex, - 'size': centersColsTexSize - } - }; - - if (this.splatRenderMode === SplatRenderMode.ThreeD) { - // set up covariances data texture - - const covTexSpecs = getCovariancesInitialTextureSpecs(covarianceCompressionLevel); - const covariancesElementsPerTexelStored = covTexSpecs.elementsPerTexelStored; - const covTexSize = covTexSpecs.texSize; - - let CovariancesDataType = covarianceCompressionLevel >= 1 ? Uint32Array : Float32Array; - const covariancesElementsPerTexelAllocated = covarianceCompressionLevel >= 1 ? - COVARIANCES_ELEMENTS_PER_TEXEL_COMPRESSED_ALLOCATED : - COVARIANCES_ELEMENTS_PER_TEXEL_ALLOCATED; - const covariancesTextureData = new CovariancesDataType(covTexSize.x * covTexSize.y * covariancesElementsPerTexelAllocated); - - if (covarianceCompressionLevel === 0) { - covariancesTextureData.set(covariances); - } else { - SplatMesh.updatePaddedCompressedCovariancesTextureData(covariances, covariancesTextureData, 0, 0, covariances.length); - } - - let covTex; - if (covarianceCompressionLevel >= 1) { - covTex = new THREE.DataTexture(covariancesTextureData, covTexSize.x, covTexSize.y, - THREE.RGBAIntegerFormat, THREE.UnsignedIntType); - covTex.internalFormat = 'RGBA32UI'; - this.material.uniforms.covariancesTextureHalfFloat.value = covTex; - } else { - covTex = new THREE.DataTexture(covariancesTextureData, covTexSize.x, covTexSize.y, THREE.RGBAFormat, THREE.FloatType); - this.material.uniforms.covariancesTexture.value = covTex; - - // For some reason a usampler2D needs to have a valid texture attached or WebGL complains - const dummyTex = new THREE.DataTexture(new Uint32Array(32), 2, 2, THREE.RGBAIntegerFormat, THREE.UnsignedIntType); - dummyTex.internalFormat = 'RGBA32UI'; - this.material.uniforms.covariancesTextureHalfFloat.value = dummyTex; - dummyTex.needsUpdate = true; - } - covTex.needsUpdate = true; - - this.material.uniforms.covariancesAreHalfFloat.value = (covarianceCompressionLevel >= 1) ? 1 : 0; - this.material.uniforms.covariancesTextureSize.value.copy(covTexSize); - - this.splatDataTextures['covariances'] = { - 'data': covariancesTextureData, - 'texture': covTex, - 'size': covTexSize, - 'compressionLevel': covarianceCompressionLevel, - 'elementsPerTexelStored': covariancesElementsPerTexelStored, - 'elementsPerTexelAllocated': covariancesElementsPerTexelAllocated - }; - } else { - // set up scale & rotations data texture - const elementsPerSplat = 6; - const scaleRotationsTexSize = computeDataTextureSize(SCALES_ROTATIONS_ELEMENTS_PER_TEXEL, elementsPerSplat); - let ScaleRotationsDataType = scaleRotationCompressionLevel >= 1 ? Uint16Array : Float32Array; - let scaleRotationsTextureType = scaleRotationCompressionLevel >= 1 ? THREE.HalfFloatType : THREE.FloatType; - const paddedScaleRotations = new ScaleRotationsDataType(scaleRotationsTexSize.x * scaleRotationsTexSize.y * - SCALES_ROTATIONS_ELEMENTS_PER_TEXEL); - - SplatMesh.updateScaleRotationsPaddedData(0, splatCount - 1, scales, rotations, paddedScaleRotations); - - const scaleRotationsTex = new THREE.DataTexture(paddedScaleRotations, scaleRotationsTexSize.x, scaleRotationsTexSize.y, - THREE.RGBAFormat, scaleRotationsTextureType); - scaleRotationsTex.needsUpdate = true; - this.material.uniforms.scaleRotationsTexture.value = scaleRotationsTex; - this.material.uniforms.scaleRotationsTextureSize.value.copy(scaleRotationsTexSize); - - this.splatDataTextures['scaleRotations'] = { - 'data': paddedScaleRotations, - 'texture': scaleRotationsTex, - 'size': scaleRotationsTexSize, - 'compressionLevel': scaleRotationCompressionLevel - }; - } - - if (shData) { - const shTextureType = shCompressionLevel === 2 ? THREE.UnsignedByteType : THREE.HalfFloatType; - - let paddedSHComponentCount = shComponentCount; - if (paddedSHComponentCount % 2 !== 0) paddedSHComponentCount++; - const shElementsPerTexel = 4; - const texelFormat = shElementsPerTexel === 4 ? THREE.RGBAFormat : THREE.RGFormat; - let shTexSize = computeDataTextureSize(shElementsPerTexel, paddedSHComponentCount); - - // Use one texture for all spherical harmonics data - if (shTexSize.x * shTexSize.y <= MAX_TEXTURE_TEXELS) { - const paddedSHArraySize = shTexSize.x * shTexSize.y * shElementsPerTexel; - const paddedSHArray = new SphericalHarmonicsArrayType(paddedSHArraySize); - for (let c = 0; c < splatCount; c++) { - const srcBase = shComponentCount * c; - const destBase = paddedSHComponentCount * c; - for (let i = 0; i < shComponentCount; i++) { - paddedSHArray[destBase + i] = shData[srcBase + i]; - } - } - - const shTexture = new THREE.DataTexture(paddedSHArray, shTexSize.x, shTexSize.y, texelFormat, shTextureType); - shTexture.needsUpdate = true; - this.material.uniforms.sphericalHarmonicsTexture.value = shTexture; - this.splatDataTextures['sphericalHarmonics'] = { - 'componentCount': shComponentCount, - 'paddedComponentCount': paddedSHComponentCount, - 'data': paddedSHArray, - 'textureCount': 1, - 'texture': shTexture, - 'size': shTexSize, - 'compressionLevel': shCompressionLevel, - 'elementsPerTexel': shElementsPerTexel - }; - // Use three textures for spherical harmonics data, one per color channel - } else { - const shComponentCountPerChannel = shComponentCount / 3; - paddedSHComponentCount = shComponentCountPerChannel; - if (paddedSHComponentCount % 2 !== 0) paddedSHComponentCount++; - shTexSize = computeDataTextureSize(shElementsPerTexel, paddedSHComponentCount); - - const paddedSHArraySize = shTexSize.x * shTexSize.y * shElementsPerTexel; - const textureUniforms = [this.material.uniforms.sphericalHarmonicsTextureR, - this.material.uniforms.sphericalHarmonicsTextureG, - this.material.uniforms.sphericalHarmonicsTextureB]; - const paddedSHArrays = []; - const shTextures = []; - for (let t = 0; t < 3; t++) { - const paddedSHArray = new SphericalHarmonicsArrayType(paddedSHArraySize); - paddedSHArrays.push(paddedSHArray); - for (let c = 0; c < splatCount; c++) { - const srcBase = shComponentCount * c; - const destBase = paddedSHComponentCount * c; - if (shComponentCountPerChannel >= 3) { - for (let i = 0; i < 3; i++) paddedSHArray[destBase + i] = shData[srcBase + t * 3 + i]; - if (shComponentCountPerChannel >= 8) { - for (let i = 0; i < 5; i++) paddedSHArray[destBase + 3 + i] = shData[srcBase + 9 + t * 5 + i]; - } - } - } - - const shTexture = new THREE.DataTexture(paddedSHArray, shTexSize.x, shTexSize.y, texelFormat, shTextureType); - shTextures.push(shTexture); - shTexture.needsUpdate = true; - textureUniforms[t].value = shTexture; - } - - this.material.uniforms.sphericalHarmonicsMultiTextureMode.value = 1; - this.splatDataTextures['sphericalHarmonics'] = { - 'componentCount': shComponentCount, - 'componentCountPerChannel': shComponentCountPerChannel, - 'paddedComponentCount': paddedSHComponentCount, - 'data': paddedSHArrays, - 'textureCount': 3, - 'textures': shTextures, - 'size': shTexSize, - 'compressionLevel': shCompressionLevel, - 'elementsPerTexel': shElementsPerTexel - }; - } - - this.material.uniforms.sphericalHarmonicsTextureSize.value.copy(shTexSize); - this.material.uniforms.sphericalHarmonics8BitMode.value = shCompressionLevel === 2 ? 1 : 0; - for (let s = 0; s < this.scenes.length; s++) { - const splatBuffer = this.scenes[s].splatBuffer; - this.material.uniforms.sphericalHarmonics8BitCompressionRangeMin.value[s] = - splatBuffer.minSphericalHarmonicsCoeff; - this.material.uniforms.sphericalHarmonics8BitCompressionRangeMax.value[s] = - splatBuffer.maxSphericalHarmonicsCoeff; - } - this.material.uniformsNeedUpdate = true; - } - - const sceneIndexesTexSize = computeDataTextureSize(SCENE_INDEXES_ELEMENTS_PER_TEXEL, 4); - const paddedTransformIndexes = new Uint32Array(sceneIndexesTexSize.x * - sceneIndexesTexSize.y * SCENE_INDEXES_ELEMENTS_PER_TEXEL); - for (let c = 0; c < splatCount; c++) paddedTransformIndexes[c] = this.globalSplatIndexToSceneIndexMap[c]; - const sceneIndexesTexture = new THREE.DataTexture(paddedTransformIndexes, sceneIndexesTexSize.x, sceneIndexesTexSize.y, - THREE.RedIntegerFormat, THREE.UnsignedIntType); - sceneIndexesTexture.internalFormat = 'R32UI'; - sceneIndexesTexture.needsUpdate = true; - this.material.uniforms.sceneIndexesTexture.value = sceneIndexesTexture; - this.material.uniforms.sceneIndexesTextureSize.value.copy(sceneIndexesTexSize); - this.material.uniformsNeedUpdate = true; - this.splatDataTextures['sceneIndexes'] = { - 'data': paddedTransformIndexes, - 'texture': sceneIndexesTexture, - 'size': sceneIndexesTexSize - }; - this.material.uniforms.sceneCount.value = this.scenes.length; - } - - updateBaseDataFromSplatBuffers(fromSplat, toSplat) { - const covarancesTextureDesc = this.splatDataTextures['covariances']; - const covarianceCompressionLevel = covarancesTextureDesc ? covarancesTextureDesc.compressionLevel : undefined; - const scaleRotationsTextureDesc = this.splatDataTextures['scaleRotations']; - const scaleRotationCompressionLevel = scaleRotationsTextureDesc ? scaleRotationsTextureDesc.compressionLevel : undefined; - const shITextureDesc = this.splatDataTextures['sphericalHarmonics']; - const shCompressionLevel = shITextureDesc ? shITextureDesc.compressionLevel : 0; - - this.fillSplatDataArrays(this.splatDataTextures.baseData.covariances, this.splatDataTextures.baseData.scales, - this.splatDataTextures.baseData.rotations, this.splatDataTextures.baseData.centers, - this.splatDataTextures.baseData.colors, this.splatDataTextures.baseData.sphericalHarmonics, undefined, - covarianceCompressionLevel, scaleRotationCompressionLevel, shCompressionLevel, - fromSplat, toSplat, fromSplat); - } - - updateDataTexturesFromBaseData(fromSplat, toSplat) { - const covarancesTextureDesc = this.splatDataTextures['covariances']; - const covarianceCompressionLevel = covarancesTextureDesc ? covarancesTextureDesc.compressionLevel : undefined; - const scaleRotationsTextureDesc = this.splatDataTextures['scaleRotations']; - const scaleRotationCompressionLevel = scaleRotationsTextureDesc ? scaleRotationsTextureDesc.compressionLevel : undefined; - const shTextureDesc = this.splatDataTextures['sphericalHarmonics']; - const shCompressionLevel = shTextureDesc ? shTextureDesc.compressionLevel : 0; - - // Update center & color data texture - const centerColorsTextureDescriptor = this.splatDataTextures['centerColors']; - const paddedCenterColors = centerColorsTextureDescriptor.data; - const centerColorsTexture = centerColorsTextureDescriptor.texture; - SplatMesh.updateCenterColorsPaddedData(fromSplat, toSplat, this.splatDataTextures.baseData.centers, - this.splatDataTextures.baseData.colors, paddedCenterColors); - const centerColorsTextureProps = this.renderer ? this.renderer.properties.get(centerColorsTexture) : null; - if (!centerColorsTextureProps || !centerColorsTextureProps.__webglTexture) { - centerColorsTexture.needsUpdate = true; - } else { - this.updateDataTexture(paddedCenterColors, centerColorsTextureDescriptor.texture, centerColorsTextureDescriptor.size, - centerColorsTextureProps, CENTER_COLORS_ELEMENTS_PER_TEXEL, CENTER_COLORS_ELEMENTS_PER_SPLAT, 4, - fromSplat, toSplat); - } - - // update covariance data texture - if (covarancesTextureDesc) { - const covariancesTexture = covarancesTextureDesc.texture; - const covarancesStartElement = fromSplat * COVARIANCES_ELEMENTS_PER_SPLAT; - const covariancesEndElement = toSplat * COVARIANCES_ELEMENTS_PER_SPLAT; - - if (covarianceCompressionLevel === 0) { - for (let i = covarancesStartElement; i <= covariancesEndElement; i++) { - const covariance = this.splatDataTextures.baseData.covariances[i]; - covarancesTextureDesc.data[i] = covariance; - } - } else { - SplatMesh.updatePaddedCompressedCovariancesTextureData(this.splatDataTextures.baseData.covariances, - covarancesTextureDesc.data, - fromSplat * covarancesTextureDesc.elementsPerTexelAllocated, - covarancesStartElement, covariancesEndElement); - } - - const covariancesTextureProps = this.renderer ? this.renderer.properties.get(covariancesTexture) : null; - if (!covariancesTextureProps || !covariancesTextureProps.__webglTexture) { - covariancesTexture.needsUpdate = true; - } else { - if (covarianceCompressionLevel === 0) { - this.updateDataTexture(covarancesTextureDesc.data, covarancesTextureDesc.texture, covarancesTextureDesc.size, - covariancesTextureProps, covarancesTextureDesc.elementsPerTexelStored, - COVARIANCES_ELEMENTS_PER_SPLAT, 4, fromSplat, toSplat); - } else { - this.updateDataTexture(covarancesTextureDesc.data, covarancesTextureDesc.texture, covarancesTextureDesc.size, - covariancesTextureProps, covarancesTextureDesc.elementsPerTexelAllocated, - covarancesTextureDesc.elementsPerTexelAllocated, 2, fromSplat, toSplat); - } - } - } - - // update scale and rotation data texture - if (scaleRotationsTextureDesc) { - const paddedScaleRotations = scaleRotationsTextureDesc.data; - const scaleRotationsTexture = scaleRotationsTextureDesc.texture; - const elementsPerSplat = 6; - const bytesPerElement = scaleRotationCompressionLevel === 0 ? 4 : 2; - - SplatMesh.updateScaleRotationsPaddedData(fromSplat, toSplat, this.splatDataTextures.baseData.scales, - this.splatDataTextures.baseData.rotations, paddedScaleRotations); - const scaleRotationsTextureProps = this.renderer ? this.renderer.properties.get(scaleRotationsTexture) : null; - if (!scaleRotationsTextureProps || !scaleRotationsTextureProps.__webglTexture) { - scaleRotationsTexture.needsUpdate = true; - } else { - this.updateDataTexture(paddedScaleRotations, scaleRotationsTextureDesc.texture, scaleRotationsTextureDesc.size, - scaleRotationsTextureProps, SCALES_ROTATIONS_ELEMENTS_PER_TEXEL, elementsPerSplat, bytesPerElement, - fromSplat, toSplat); - } - } - - // update spherical harmonics data texture - const shData = this.splatDataTextures.baseData.sphericalHarmonics; - if (shData) { - let shBytesPerElement = 4; - if (shCompressionLevel === 1) shBytesPerElement = 2; - else if (shCompressionLevel === 2) shBytesPerElement = 1; - - const updateTexture = (shTexture, shTextureSize, elementsPerTexel, paddedSHArray, paddedSHComponentCount) => { - const shTextureProps = this.renderer ? this.renderer.properties.get(shTexture) : null; - if (!shTextureProps || !shTextureProps.__webglTexture) { - shTexture.needsUpdate = true; - } else { - this.updateDataTexture(paddedSHArray, shTexture, shTextureSize, shTextureProps, elementsPerTexel, - paddedSHComponentCount, shBytesPerElement, fromSplat, toSplat); - } - }; - - const shComponentCount = shTextureDesc.componentCount; - const paddedSHComponentCount = shTextureDesc.paddedComponentCount; - - // Update for the case of a single texture for all spherical harmonics data - if (shTextureDesc.textureCount === 1) { - const paddedSHArray = shTextureDesc.data; - for (let c = fromSplat; c <= toSplat; c++) { - const srcBase = shComponentCount * c; - const destBase = paddedSHComponentCount * c; - for (let i = 0; i < shComponentCount; i++) { - paddedSHArray[destBase + i] = shData[srcBase + i]; - } - } - updateTexture(shTextureDesc.texture, shTextureDesc.size, - shTextureDesc.elementsPerTexel, paddedSHArray, paddedSHComponentCount); - // Update for the case of spherical harmonics data split among three textures, one for each color channel - } else { - const shComponentCountPerChannel = shTextureDesc.componentCountPerChannel; - for (let t = 0; t < 3; t++) { - const paddedSHArray = shTextureDesc.data[t]; - for (let c = fromSplat; c <= toSplat; c++) { - const srcBase = shComponentCount * c; - const destBase = paddedSHComponentCount * c; - if (shComponentCountPerChannel >= 3) { - for (let i = 0; i < 3; i++) paddedSHArray[destBase + i] = shData[srcBase + t * 3 + i]; - if (shComponentCountPerChannel >= 8) { - for (let i = 0; i < 5; i++) paddedSHArray[destBase + 3 + i] = shData[srcBase + 9 + t * 5 + i]; - } - } - } - updateTexture(shTextureDesc.textures[t], shTextureDesc.size, - shTextureDesc.elementsPerTexel, paddedSHArray, paddedSHComponentCount); - } - } - } - - // update scene index & transform data - const sceneIndexesTexDesc = this.splatDataTextures['sceneIndexes']; - const paddedSceneIndexes = sceneIndexesTexDesc.data; - for (let c = this.lastBuildSplatCount; c <= toSplat; c++) { - paddedSceneIndexes[c] = this.globalSplatIndexToSceneIndexMap[c]; - } - const sceneIndexesTexture = sceneIndexesTexDesc.texture; - const sceneIndexesTextureProps = this.renderer ? this.renderer.properties.get(sceneIndexesTexture) : null; - if (!sceneIndexesTextureProps || !sceneIndexesTextureProps.__webglTexture) { - sceneIndexesTexture.needsUpdate = true; - } else { - this.updateDataTexture(paddedSceneIndexes, sceneIndexesTexDesc.texture, sceneIndexesTexDesc.size, - sceneIndexesTextureProps, 1, 1, 1, this.lastBuildSplatCount, toSplat); - } - } - - getTargetCovarianceCompressionLevel() { - return this.halfPrecisionCovariancesOnGPU ? 1 : 0; - } - - getTargetSphericalHarmonicsCompressionLevel() { - return Math.max(1, this.getMaximumSplatBufferCompressionLevel()); - } - - getMaximumSplatBufferCompressionLevel() { - let maxCompressionLevel; - for (let i = 0; i < this.scenes.length; i++) { - const scene = this.getScene(i); - const splatBuffer = scene.splatBuffer; - if (i === 0 || splatBuffer.compressionLevel > maxCompressionLevel) { - maxCompressionLevel = splatBuffer.compressionLevel; - } - } - return maxCompressionLevel; - } - - getMinimumSplatBufferCompressionLevel() { - let minCompressionLevel; - for (let i = 0; i < this.scenes.length; i++) { - const scene = this.getScene(i); - const splatBuffer = scene.splatBuffer; - if (i === 0 || splatBuffer.compressionLevel < minCompressionLevel) { - minCompressionLevel = splatBuffer.compressionLevel; - } - } - return minCompressionLevel; - } - - static computeTextureUpdateRegion(startSplat, endSplat, textureWidth, elementsPerTexel, elementsPerSplat) { - const texelsPerSplat = elementsPerSplat / elementsPerTexel; - - const startSplatTexels = startSplat * texelsPerSplat; - const startRow = Math.floor(startSplatTexels / textureWidth); - const startRowElement = startRow * textureWidth * elementsPerTexel; - - const endSplatTexels = endSplat * texelsPerSplat; - const endRow = Math.floor(endSplatTexels / textureWidth); - const endRowEndElement = endRow * textureWidth * elementsPerTexel + (textureWidth * elementsPerTexel); - - return { - 'dataStart': startRowElement, - 'dataEnd': endRowEndElement, - 'startRow': startRow, - 'endRow': endRow - }; - } - - updateDataTexture(paddedData, texture, textureSize, textureProps, elementsPerTexel, elementsPerSplat, bytesPerElement, from, to) { - const gl = this.renderer.getContext(); - const updateRegion = SplatMesh.computeTextureUpdateRegion(from, to, textureSize.x, elementsPerTexel, elementsPerSplat); - const updateElementCount = updateRegion.dataEnd - updateRegion.dataStart; - const updateDataView = new paddedData.constructor(paddedData.buffer, - updateRegion.dataStart * bytesPerElement, updateElementCount); - const updateHeight = updateRegion.endRow - updateRegion.startRow + 1; - const glType = this.webGLUtils.convert(texture.type); - const glFormat = this.webGLUtils.convert(texture.format, texture.colorSpace); - const currentTexture = gl.getParameter(gl.TEXTURE_BINDING_2D); - gl.bindTexture(gl.TEXTURE_2D, textureProps.__webglTexture); - gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, updateRegion.startRow, - textureSize.x, updateHeight, glFormat, glType, updateDataView); - gl.bindTexture(gl.TEXTURE_2D, currentTexture); - } - - static updatePaddedCompressedCovariancesTextureData(sourceData, textureData, textureDataStartIndex, fromElement, toElement) { - let textureDataView = new DataView(textureData.buffer); - let textureDataIndex = textureDataStartIndex; - let sequentialCount = 0; - for (let i = fromElement; i <= toElement; i+=2) { - textureDataView.setUint16(textureDataIndex * 2, sourceData[i], true); - textureDataView.setUint16(textureDataIndex * 2 + 2, sourceData[i + 1], true); - textureDataIndex += 2; - sequentialCount++; - if (sequentialCount >= 3) { - textureDataIndex += 2; - sequentialCount = 0; - } - } - } - - static updateCenterColorsPaddedData(from, to, centers, colors, paddedCenterColors) { - for (let c = from; c <= to; c++) { - const colorsBase = c * 4; - const centersBase = c * 3; - const centerColorsBase = c * 4; - paddedCenterColors[centerColorsBase] = rgbaArrayToInteger(colors, colorsBase); - paddedCenterColors[centerColorsBase + 1] = uintEncodedFloat(centers[centersBase]); - paddedCenterColors[centerColorsBase + 2] = uintEncodedFloat(centers[centersBase + 1]); - paddedCenterColors[centerColorsBase + 3] = uintEncodedFloat(centers[centersBase + 2]); - } - } - - static updateScaleRotationsPaddedData(from, to, scales, rotations, paddedScaleRotations) { - const combinedSize = 6; - for (let c = from; c <= to; c++) { - const scaleBase = c * 3; - const rotationBase = c * 4; - const scaleRotationsBase = c * combinedSize; - - paddedScaleRotations[scaleRotationsBase] = scales[scaleBase]; - paddedScaleRotations[scaleRotationsBase + 1] = scales[scaleBase + 1]; - paddedScaleRotations[scaleRotationsBase + 2] = scales[scaleBase + 2]; - - paddedScaleRotations[scaleRotationsBase + 3] = rotations[rotationBase]; - paddedScaleRotations[scaleRotationsBase + 4] = rotations[rotationBase + 1]; - paddedScaleRotations[scaleRotationsBase + 5] = rotations[rotationBase + 2]; - } - } - - updateVisibleRegion(sinceLastBuildOnly) { - const splatCount = this.getSplatCount(true); - const tempCenter = new THREE.Vector3(); - if (!sinceLastBuildOnly) { - const avgCenter = new THREE.Vector3(); - this.scenes.forEach((scene) => { - avgCenter.add(scene.splatBuffer.sceneCenter); - }); - avgCenter.multiplyScalar(1.0 / this.scenes.length); - this.calculatedSceneCenter.copy(avgCenter); - this.material.uniforms.sceneCenter.value.copy(this.calculatedSceneCenter); - this.material.uniformsNeedUpdate = true; - } - - const startSplatFormMaxDistanceCalc = sinceLastBuildOnly ? this.lastBuildSplatCount : 0; - for (let i = startSplatFormMaxDistanceCalc; i < splatCount; i++) { - this.getSplatCenter(i, tempCenter, true); - const distFromCSceneCenter = tempCenter.sub(this.calculatedSceneCenter).length(); - if (distFromCSceneCenter > this.maxSplatDistanceFromSceneCenter) this.maxSplatDistanceFromSceneCenter = distFromCSceneCenter; - } - - if (this.maxSplatDistanceFromSceneCenter - this.visibleRegionBufferRadius > VISIBLE_REGION_EXPANSION_DELTA) { - this.visibleRegionBufferRadius = this.maxSplatDistanceFromSceneCenter; - this.visibleRegionRadius = Math.max(this.visibleRegionBufferRadius - VISIBLE_REGION_EXPANSION_DELTA, 0.0); - } - if (this.finalBuild) this.visibleRegionRadius = this.visibleRegionBufferRadius = this.maxSplatDistanceFromSceneCenter; - this.updateVisibleRegionFadeDistance(); - } - - updateVisibleRegionFadeDistance(sceneRevealMode = SceneRevealMode.Default) { - const fastFadeRate = SCENE_FADEIN_RATE_FAST * this.sceneFadeInRateMultiplier; - const gradualFadeRate = SCENE_FADEIN_RATE_GRADUAL * this.sceneFadeInRateMultiplier; - const defaultFadeInRate = this.finalBuild ? fastFadeRate : gradualFadeRate; - const fadeInRate = sceneRevealMode === SceneRevealMode.Default ? defaultFadeInRate : gradualFadeRate; - this.visibleRegionFadeStartRadius = (this.visibleRegionRadius - this.visibleRegionFadeStartRadius) * - fadeInRate + this.visibleRegionFadeStartRadius; - const fadeInPercentage = (this.visibleRegionBufferRadius > 0) ? - (this.visibleRegionFadeStartRadius / this.visibleRegionBufferRadius) : 0; - const fadeInComplete = fadeInPercentage > 0.99; - const shaderFadeInComplete = (fadeInComplete || sceneRevealMode === SceneRevealMode.Instant) ? 1 : 0; - - this.material.uniforms.visibleRegionFadeStartRadius.value = this.visibleRegionFadeStartRadius; - this.material.uniforms.visibleRegionRadius.value = this.visibleRegionRadius; - this.material.uniforms.firstRenderTime.value = this.firstRenderTime; - this.material.uniforms.currentTime.value = performance.now(); - this.material.uniforms.fadeInComplete.value = shaderFadeInComplete; - this.material.uniformsNeedUpdate = true; - this.visibleRegionChanging = !fadeInComplete; - } - - /** - * Set the indexes of splats that should be rendered; should be sorted in desired render order. - * @param {Uint32Array} globalIndexes Sorted index list of splats to be rendered - * @param {number} renderSplatCount Total number of splats to be rendered. Necessary because we may not want to render - * every splat. - */ - updateRenderIndexes(globalIndexes, renderSplatCount) { - const geometry = this.geometry; - geometry.attributes.splatIndex.set(globalIndexes); - geometry.attributes.splatIndex.needsUpdate = true; - if (renderSplatCount > 0 && this.firstRenderTime === -1) this.firstRenderTime = performance.now(); - geometry.instanceCount = renderSplatCount; - geometry.setDrawRange(0, renderSplatCount); - } - - /** - * Update the transforms for each scene in this splat mesh from their individual components (position, - * quaternion, and scale) - */ - updateTransforms() { - for (let i = 0; i < this.scenes.length; i++) { - const scene = this.getScene(i); - scene.updateTransform(this.dynamicMode); - } - } - - updateUniforms = function() { - - const viewport = new THREE.Vector2(); - - return function(renderDimensions, cameraFocalLengthX, cameraFocalLengthY, - orthographicMode, orthographicZoom, inverseFocalAdjustment) { - const splatCount = this.getSplatCount(); - if (splatCount > 0) { - viewport.set(renderDimensions.x * this.devicePixelRatio, - renderDimensions.y * this.devicePixelRatio); - this.material.uniforms.viewport.value.copy(viewport); - this.material.uniforms.basisViewport.value.set(1.0 / viewport.x, 1.0 / viewport.y); - this.material.uniforms.focal.value.set(cameraFocalLengthX, cameraFocalLengthY); - this.material.uniforms.orthographicMode.value = orthographicMode ? 1 : 0; - this.material.uniforms.orthoZoom.value = orthographicZoom; - this.material.uniforms.inverseFocalAdjustment.value = inverseFocalAdjustment; - if (this.dynamicMode) { - for (let i = 0; i < this.scenes.length; i++) { - this.material.uniforms.transforms.value[i].copy(this.getScene(i).transform); - } - } - if (this.enableOptionalEffects) { - for (let i = 0; i < this.scenes.length; i++) { - this.material.uniforms.sceneOpacity.value[i] = clamp(this.getScene(i).opacity, 0.0, 1.0); - this.material.uniforms.sceneVisibility.value[i] = this.getScene(i).visible ? 1 : 0; - this.material.uniformsNeedUpdate = true; - } - } - this.material.uniformsNeedUpdate = true; - } - }; - - }(); - - setSplatScale(splatScale = 1) { - this.splatScale = splatScale; - this.material.uniforms.splatScale.value = splatScale; - this.material.uniformsNeedUpdate = true; - } - - getSplatScale() { - return this.splatScale; - } - - setPointCloudModeEnabled(enabled) { - this.pointCloudModeEnabled = enabled; - this.material.uniforms.pointCloudModeEnabled.value = enabled ? 1 : 0; - this.material.uniformsNeedUpdate = true; - } - - getPointCloudModeEnabled() { - return this.pointCloudModeEnabled; - } - - getSplatDataTextures() { - return this.splatDataTextures; - } - - getSplatCount(includeSinceLastBuild = false) { - if (!includeSinceLastBuild) return this.lastBuildSplatCount; - else return SplatMesh.getTotalSplatCountForScenes(this.scenes); - } - - static getTotalSplatCountForScenes(scenes) { - let totalSplatCount = 0; - for (let scene of scenes) { - if (scene && scene.splatBuffer) totalSplatCount += scene.splatBuffer.getSplatCount(); - } - return totalSplatCount; - } - - static getTotalSplatCountForSplatBuffers(splatBuffers) { - let totalSplatCount = 0; - for (let splatBuffer of splatBuffers) totalSplatCount += splatBuffer.getSplatCount(); - return totalSplatCount; - } - - getMaxSplatCount() { - return SplatMesh.getTotalMaxSplatCountForScenes(this.scenes); - } - - static getTotalMaxSplatCountForScenes(scenes) { - let totalSplatCount = 0; - for (let scene of scenes) { - if (scene && scene.splatBuffer) totalSplatCount += scene.splatBuffer.getMaxSplatCount(); - } - return totalSplatCount; - } - - static getTotalMaxSplatCountForSplatBuffers(splatBuffers) { - let totalSplatCount = 0; - for (let splatBuffer of splatBuffers) totalSplatCount += splatBuffer.getMaxSplatCount(); - return totalSplatCount; - } - - disposeDistancesComputationGPUResources() { - - if (!this.renderer) return; - - const gl = this.renderer.getContext(); - - if (this.distancesTransformFeedback.vao) { - gl.deleteVertexArray(this.distancesTransformFeedback.vao); - this.distancesTransformFeedback.vao = null; - } - if (this.distancesTransformFeedback.program) { - gl.deleteProgram(this.distancesTransformFeedback.program); - gl.deleteShader(this.distancesTransformFeedback.vertexShader); - gl.deleteShader(this.distancesTransformFeedback.fragmentShader); - this.distancesTransformFeedback.program = null; - this.distancesTransformFeedback.vertexShader = null; - this.distancesTransformFeedback.fragmentShader = null; - } - this.disposeDistancesComputationGPUBufferResources(); - if (this.distancesTransformFeedback.id) { - gl.deleteTransformFeedback(this.distancesTransformFeedback.id); - this.distancesTransformFeedback.id = null; - } - } - - disposeDistancesComputationGPUBufferResources() { - - if (!this.renderer) return; - - const gl = this.renderer.getContext(); - - if (this.distancesTransformFeedback.centersBuffer) { - this.distancesTransformFeedback.centersBuffer = null; - gl.deleteBuffer(this.distancesTransformFeedback.centersBuffer); - } - if (this.distancesTransformFeedback.outDistancesBuffer) { - gl.deleteBuffer(this.distancesTransformFeedback.outDistancesBuffer); - this.distancesTransformFeedback.outDistancesBuffer = null; - } - } - - /** - * Set the Three.js renderer used by this splat mesh - * @param {THREE.WebGLRenderer} renderer Instance of THREE.WebGLRenderer - */ - setRenderer(renderer) { - if (renderer !== this.renderer) { - this.renderer = renderer; - const gl = this.renderer.getContext(); - const extensions = new WebGLExtensions(gl); - const capabilities = new WebGLCapabilities(gl, extensions, {}); - extensions.init(capabilities); - this.webGLUtils = new THREE.WebGLUtils(gl, extensions, capabilities); - if (this.enableDistancesComputationOnGPU && this.getSplatCount() > 0) { - this.setupDistancesComputationTransformFeedback(); - const { centers, sceneIndexes } = this.getDataForDistancesComputation(0, this.getSplatCount() - 1); - this.refreshGPUBuffersForDistancesComputation(centers, sceneIndexes); - } - } - } - - setupDistancesComputationTransformFeedback = function() { - - let currentMaxSplatCount; - - return function() { - const maxSplatCount = this.getMaxSplatCount(); - - if (!this.renderer) return; - - const rebuildGPUObjects = (this.lastRenderer !== this.renderer); - const rebuildBuffers = currentMaxSplatCount !== maxSplatCount; - - if (!rebuildGPUObjects && !rebuildBuffers) return; - - if (rebuildGPUObjects) { - this.disposeDistancesComputationGPUResources(); - } else if (rebuildBuffers) { - this.disposeDistancesComputationGPUBufferResources(); - } - - const gl = this.renderer.getContext(); - - const createShader = (gl, type, source) => { - const shader = gl.createShader(type); - if (!shader) { - console.error('Fatal error: gl could not create a shader object.'); - return null; - } - - gl.shaderSource(shader, source); - gl.compileShader(shader); - - const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS); - if (!compiled) { - let typeName = 'unknown'; - if (type === gl.VERTEX_SHADER) typeName = 'vertex shader'; - else if (type === gl.FRAGMENT_SHADER) typeName = 'fragement shader'; - const errors = gl.getShaderInfoLog(shader); - console.error('Failed to compile ' + typeName + ' with these errors:' + errors); - gl.deleteShader(shader); - return null; - } - - return shader; - }; - - let vsSource; - if (this.integerBasedDistancesComputation) { - vsSource = - `#version 300 es - in ivec4 center; - flat out int distance;`; - if (this.dynamicMode) { - vsSource += ` - in uint sceneIndex; - uniform ivec4 transforms[${Constants.MaxScenes}]; - void main(void) { - ivec4 transform = transforms[sceneIndex]; - distance = center.x * transform.x + center.y * transform.y + center.z * transform.z + transform.w * center.w; - } - `; - } else { - vsSource += ` - uniform ivec3 modelViewProj; - void main(void) { - distance = center.x * modelViewProj.x + center.y * modelViewProj.y + center.z * modelViewProj.z; - } - `; - } - } else { - vsSource = - `#version 300 es - in vec4 center; - flat out float distance;`; - if (this.dynamicMode) { - vsSource += ` - in uint sceneIndex; - uniform mat4 transforms[${Constants.MaxScenes}]; - void main(void) { - vec4 transformedCenter = transforms[sceneIndex] * vec4(center.xyz, 1.0); - distance = transformedCenter.z; - } - `; - } else { - vsSource += ` - uniform vec3 modelViewProj; - void main(void) { - distance = center.x * modelViewProj.x + center.y * modelViewProj.y + center.z * modelViewProj.z; - } - `; - } - } - - const fsSource = - `#version 300 es - precision lowp float; - out vec4 fragColor; - void main(){} - `; - - const currentVao = gl.getParameter(gl.VERTEX_ARRAY_BINDING); - const currentProgram = gl.getParameter(gl.CURRENT_PROGRAM); - const currentProgramDeleted = currentProgram ? gl.getProgramParameter(currentProgram, gl.DELETE_STATUS) : false; - - if (rebuildGPUObjects) { - this.distancesTransformFeedback.vao = gl.createVertexArray(); - } - - gl.bindVertexArray(this.distancesTransformFeedback.vao); - - if (rebuildGPUObjects) { - const program = gl.createProgram(); - const vertexShader = createShader(gl, gl.VERTEX_SHADER, vsSource); - const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fsSource); - if (!vertexShader || !fragmentShader) { - throw new Error('Could not compile shaders for distances computation on GPU.'); - } - gl.attachShader(program, vertexShader); - gl.attachShader(program, fragmentShader); - gl.transformFeedbackVaryings(program, ['distance'], gl.SEPARATE_ATTRIBS); - gl.linkProgram(program); - - const linked = gl.getProgramParameter(program, gl.LINK_STATUS); - if (!linked) { - const error = gl.getProgramInfoLog(program); - console.error('Fatal error: Failed to link program: ' + error); - gl.deleteProgram(program); - gl.deleteShader(fragmentShader); - gl.deleteShader(vertexShader); - throw new Error('Could not link shaders for distances computation on GPU.'); - } - - this.distancesTransformFeedback.program = program; - this.distancesTransformFeedback.vertexShader = vertexShader; - this.distancesTransformFeedback.vertexShader = fragmentShader; - } - - gl.useProgram(this.distancesTransformFeedback.program); - - this.distancesTransformFeedback.centersLoc = - gl.getAttribLocation(this.distancesTransformFeedback.program, 'center'); - if (this.dynamicMode) { - this.distancesTransformFeedback.sceneIndexesLoc = - gl.getAttribLocation(this.distancesTransformFeedback.program, 'sceneIndex'); - for (let i = 0; i < this.scenes.length; i++) { - this.distancesTransformFeedback.transformsLocs[i] = - gl.getUniformLocation(this.distancesTransformFeedback.program, `transforms[${i}]`); - } - } else { - this.distancesTransformFeedback.modelViewProjLoc = - gl.getUniformLocation(this.distancesTransformFeedback.program, 'modelViewProj'); - } - - if (rebuildGPUObjects || rebuildBuffers) { - this.distancesTransformFeedback.centersBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, this.distancesTransformFeedback.centersBuffer); - gl.enableVertexAttribArray(this.distancesTransformFeedback.centersLoc); - if (this.integerBasedDistancesComputation) { - gl.vertexAttribIPointer(this.distancesTransformFeedback.centersLoc, 4, gl.INT, 0, 0); - } else { - gl.vertexAttribPointer(this.distancesTransformFeedback.centersLoc, 4, gl.FLOAT, false, 0, 0); - } - - if (this.dynamicMode) { - this.distancesTransformFeedback.sceneIndexesBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, this.distancesTransformFeedback.sceneIndexesBuffer); - gl.enableVertexAttribArray(this.distancesTransformFeedback.sceneIndexesLoc); - gl.vertexAttribIPointer(this.distancesTransformFeedback.sceneIndexesLoc, 1, gl.UNSIGNED_INT, 0, 0); - } - } - - if (rebuildGPUObjects || rebuildBuffers) { - this.distancesTransformFeedback.outDistancesBuffer = gl.createBuffer(); - } - gl.bindBuffer(gl.ARRAY_BUFFER, this.distancesTransformFeedback.outDistancesBuffer); - gl.bufferData(gl.ARRAY_BUFFER, maxSplatCount * 4, gl.STATIC_READ); - - if (rebuildGPUObjects) { - this.distancesTransformFeedback.id = gl.createTransformFeedback(); - } - gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, this.distancesTransformFeedback.id); - gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, this.distancesTransformFeedback.outDistancesBuffer); - - if (currentProgram && currentProgramDeleted !== true) gl.useProgram(currentProgram); - if (currentVao) gl.bindVertexArray(currentVao); - - this.lastRenderer = this.renderer; - currentMaxSplatCount = maxSplatCount; - }; - - }(); - - /** - * Refresh GPU buffers used for computing splat distances with centers data from the scenes for this mesh. - * @param {boolean} isUpdate Specify whether or not to update the GPU buffer or to initialize & fill - * @param {Array} centers The splat centers data - * @param {number} offsetSplats Offset in the GPU buffer at which to start updating data, specified in splats - */ - updateGPUCentersBufferForDistancesComputation(isUpdate, centers, offsetSplats) { - - if (!this.renderer) return; - - const gl = this.renderer.getContext(); - - const currentVao = gl.getParameter(gl.VERTEX_ARRAY_BINDING); - gl.bindVertexArray(this.distancesTransformFeedback.vao); - - const ArrayType = this.integerBasedDistancesComputation ? Uint32Array : Float32Array; - const attributeBytesPerCenter = 16; - const subBufferOffset = offsetSplats * attributeBytesPerCenter; - - gl.bindBuffer(gl.ARRAY_BUFFER, this.distancesTransformFeedback.centersBuffer); - - if (isUpdate) { - gl.bufferSubData(gl.ARRAY_BUFFER, subBufferOffset, centers); - } else { - const maxArray = new ArrayType(this.getMaxSplatCount() * attributeBytesPerCenter); - maxArray.set(centers); - gl.bufferData(gl.ARRAY_BUFFER, maxArray, gl.STATIC_DRAW); - } - - gl.bindBuffer(gl.ARRAY_BUFFER, null); - - if (currentVao) gl.bindVertexArray(currentVao); - } - - /** - * Refresh GPU buffers used for pre-computing splat distances with centers data from the scenes for this mesh. - * @param {boolean} isUpdate Specify whether or not to update the GPU buffer or to initialize & fill - * @param {Array} sceneIndexes The splat scene indexes - * @param {number} offsetSplats Offset in the GPU buffer at which to start updating data, specified in splats - */ - updateGPUTransformIndexesBufferForDistancesComputation(isUpdate, sceneIndexes, offsetSplats) { - - if (!this.renderer || !this.dynamicMode) return; - - const gl = this.renderer.getContext(); - - const currentVao = gl.getParameter(gl.VERTEX_ARRAY_BINDING); - gl.bindVertexArray(this.distancesTransformFeedback.vao); - - const subBufferOffset = offsetSplats * 4; - - gl.bindBuffer(gl.ARRAY_BUFFER, this.distancesTransformFeedback.sceneIndexesBuffer); - - if (isUpdate) { - gl.bufferSubData(gl.ARRAY_BUFFER, subBufferOffset, sceneIndexes); - } else { - const maxArray = new Uint32Array(this.getMaxSplatCount() * 4); - maxArray.set(sceneIndexes); - gl.bufferData(gl.ARRAY_BUFFER, maxArray, gl.STATIC_DRAW); - } - gl.bindBuffer(gl.ARRAY_BUFFER, null); - - if (currentVao) gl.bindVertexArray(currentVao); - } - - /** - * Get a typed array containing a mapping from global splat indexes to their scene index. - * @param {number} start Starting splat index to store - * @param {number} end Ending splat index to store - * @return {Uint32Array} - */ - getSceneIndexes(start, end) { - - let sceneIndexes; - const fillCount = end - start + 1; - sceneIndexes = new Uint32Array(fillCount); - for (let i = start; i <= end; i++) { - sceneIndexes[i] = this.globalSplatIndexToSceneIndexMap[i]; - } - - return sceneIndexes; - } - - /** - * Fill 'array' with the transforms for each scene in this splat mesh. - * @param {Array} array Empty array to be filled with scene transforms. If not empty, contents will be overwritten. - */ - fillTransformsArray = function() { - - const tempArray = []; - - return function(array) { - if (tempArray.length !== array.length) tempArray.length = array.length; - for (let i = 0; i < this.scenes.length; i++) { - const sceneTransform = this.getScene(i).transform; - const sceneTransformElements = sceneTransform.elements; - for (let j = 0; j < 16; j++) { - tempArray[i * 16 + j] = sceneTransformElements[j]; - } - } - array.set(tempArray); - }; - - }(); - - computeDistancesOnGPU = function() { - - const tempMatrix = new THREE.Matrix4(); - - return function(modelViewProjMatrix, outComputedDistances) { - if (!this.renderer) return; - - // console.time("gpu_compute_distances"); - const gl = this.renderer.getContext(); - - const currentVao = gl.getParameter(gl.VERTEX_ARRAY_BINDING); - const currentProgram = gl.getParameter(gl.CURRENT_PROGRAM); - const currentProgramDeleted = currentProgram ? gl.getProgramParameter(currentProgram, gl.DELETE_STATUS) : false; - - gl.bindVertexArray(this.distancesTransformFeedback.vao); - gl.useProgram(this.distancesTransformFeedback.program); - - gl.enable(gl.RASTERIZER_DISCARD); - - if (this.dynamicMode) { - for (let i = 0; i < this.scenes.length; i++) { - tempMatrix.copy(this.getScene(i).transform); - tempMatrix.premultiply(modelViewProjMatrix); - - if (this.integerBasedDistancesComputation) { - const iTempMatrix = SplatMesh.getIntegerMatrixArray(tempMatrix); - const iTransform = [iTempMatrix[2], iTempMatrix[6], iTempMatrix[10], iTempMatrix[14]]; - gl.uniform4i(this.distancesTransformFeedback.transformsLocs[i], iTransform[0], iTransform[1], - iTransform[2], iTransform[3]); - } else { - gl.uniformMatrix4fv(this.distancesTransformFeedback.transformsLocs[i], false, tempMatrix.elements); - } - } - } else { - if (this.integerBasedDistancesComputation) { - const iViewProjMatrix = SplatMesh.getIntegerMatrixArray(modelViewProjMatrix); - const iViewProj = [iViewProjMatrix[2], iViewProjMatrix[6], iViewProjMatrix[10]]; - gl.uniform3i(this.distancesTransformFeedback.modelViewProjLoc, iViewProj[0], iViewProj[1], iViewProj[2]); - } else { - const viewProj = [modelViewProjMatrix.elements[2], modelViewProjMatrix.elements[6], modelViewProjMatrix.elements[10]]; - gl.uniform3f(this.distancesTransformFeedback.modelViewProjLoc, viewProj[0], viewProj[1], viewProj[2]); - } - } - - gl.bindBuffer(gl.ARRAY_BUFFER, this.distancesTransformFeedback.centersBuffer); - gl.enableVertexAttribArray(this.distancesTransformFeedback.centersLoc); - if (this.integerBasedDistancesComputation) { - gl.vertexAttribIPointer(this.distancesTransformFeedback.centersLoc, 4, gl.INT, 0, 0); - } else { - gl.vertexAttribPointer(this.distancesTransformFeedback.centersLoc, 4, gl.FLOAT, false, 0, 0); - } - - if (this.dynamicMode) { - gl.bindBuffer(gl.ARRAY_BUFFER, this.distancesTransformFeedback.sceneIndexesBuffer); - gl.enableVertexAttribArray(this.distancesTransformFeedback.sceneIndexesLoc); - gl.vertexAttribIPointer(this.distancesTransformFeedback.sceneIndexesLoc, 1, gl.UNSIGNED_INT, 0, 0); - } - - gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, this.distancesTransformFeedback.id); - gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, this.distancesTransformFeedback.outDistancesBuffer); - - gl.beginTransformFeedback(gl.POINTS); - gl.drawArrays(gl.POINTS, 0, this.getSplatCount()); - gl.endTransformFeedback(); - - gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null); - gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null); - - gl.disable(gl.RASTERIZER_DISCARD); - - const sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0); - gl.flush(); - - const promise = new Promise((resolve) => { - const checkSync = () => { - if (this.disposed) { - resolve(); - } else { - const timeout = 0; - const bitflags = 0; - const status = gl.clientWaitSync(sync, bitflags, timeout); - switch (status) { - case gl.TIMEOUT_EXPIRED: - this.computeDistancesOnGPUSyncTimeout = setTimeout(checkSync); - return this.computeDistancesOnGPUSyncTimeout; - case gl.WAIT_FAILED: - throw new Error('should never get here'); - default: - this.computeDistancesOnGPUSyncTimeout = null; - gl.deleteSync(sync); - const currentVao = gl.getParameter(gl.VERTEX_ARRAY_BINDING); - gl.bindVertexArray(this.distancesTransformFeedback.vao); - gl.bindBuffer(gl.ARRAY_BUFFER, this.distancesTransformFeedback.outDistancesBuffer); - gl.getBufferSubData(gl.ARRAY_BUFFER, 0, outComputedDistances); - gl.bindBuffer(gl.ARRAY_BUFFER, null); - - if (currentVao) gl.bindVertexArray(currentVao); - - // console.timeEnd("gpu_compute_distances"); - - resolve(); - } - } - }; - this.computeDistancesOnGPUSyncTimeout = setTimeout(checkSync); - }); - - if (currentProgram && currentProgramDeleted !== true) gl.useProgram(currentProgram); - if (currentVao) gl.bindVertexArray(currentVao); - - return promise; - }; - - }(); - - /** - * Given a global splat index, return corresponding local data (splat buffer, index of splat in that splat - * buffer, and the corresponding transform) - * @param {number} globalIndex Global splat index - * @param {object} paramsObj Object in which to store local data - * @param {boolean} returnSceneTransform By default, the transform of the scene to which the splat at 'globalIndex' belongs will be - * returned via the 'sceneTransform' property of 'paramsObj' only if the splat mesh is static. - * If 'returnSceneTransform' is true, the 'sceneTransform' property will always contain the scene - * transform, and if 'returnSceneTransform' is false, the 'sceneTransform' property will always - * be null. - */ - getLocalSplatParameters(globalIndex, paramsObj, returnSceneTransform) { - if (returnSceneTransform === undefined || returnSceneTransform === null) { - returnSceneTransform = this.dynamicMode ? false : true; - } - paramsObj.splatBuffer = this.getSplatBufferForSplat(globalIndex); - paramsObj.localIndex = this.getSplatLocalIndex(globalIndex); - paramsObj.sceneTransform = returnSceneTransform ? this.getSceneTransformForSplat(globalIndex) : null; - } - - /** - * Fill arrays with splat data and apply transforms if appropriate. Each array is optional. - * @param {Float32Array} covariances Target storage for splat covariances - * @param {Float32Array} scales Target storage for splat scales - * @param {Float32Array} rotations Target storage for splat rotations - * @param {Float32Array} centers Target storage for splat centers - * @param {Uint8Array} colors Target storage for splat colors - * @param {Float32Array} sphericalHarmonics Target storage for spherical harmonics - * @param {boolean} applySceneTransform By default, scene transforms are applied to relevant splat data only if the splat mesh is - * static. If 'applySceneTransform' is true, scene transforms will always be applied and if - * it is false, they will never be applied. If undefined, the default behavior will apply. - * @param {number} covarianceCompressionLevel The compression level for covariances in the destination array - * @param {number} sphericalHarmonicsCompressionLevel The compression level for spherical harmonics in the destination array - * @param {number} srcStart The start location from which to pull source data - * @param {number} srcEnd The end location from which to pull source data - * @param {number} destStart The start location from which to write data - */ - fillSplatDataArrays(covariances, scales, rotations, centers, colors, sphericalHarmonics, applySceneTransform, - covarianceCompressionLevel = 0, scaleRotationCompressionLevel = 0, sphericalHarmonicsCompressionLevel = 1, - srcStart, srcEnd, destStart = 0, sceneIndex) { - const scaleOverride = new THREE.Vector3(); - scaleOverride.x = undefined; - scaleOverride.y = undefined; - if (this.splatRenderMode === SplatRenderMode.ThreeD) { - scaleOverride.z = undefined; - } else { - scaleOverride.z = 1; - } - const tempTransform = new THREE.Matrix4(); - - let startSceneIndex = 0; - let endSceneIndex = this.scenes.length - 1; - if (sceneIndex !== undefined && sceneIndex !== null && sceneIndex >= 0 && sceneIndex <= this.scenes.length) { - startSceneIndex = sceneIndex; - endSceneIndex = sceneIndex; - } - for (let i = startSceneIndex; i <= endSceneIndex; i++) { - if (applySceneTransform === undefined || applySceneTransform === null) { - applySceneTransform = this.dynamicMode ? false : true; - } - - const scene = this.getScene(i); - const splatBuffer = scene.splatBuffer; - let sceneTransform; - if (applySceneTransform) { - this.getSceneTransform(i, tempTransform); - sceneTransform = tempTransform; - } - if (covariances) { - splatBuffer.fillSplatCovarianceArray(covariances, sceneTransform, srcStart, srcEnd, destStart, covarianceCompressionLevel); - } - if (scales || rotations) { - if (!scales || !rotations) { - throw new Error('SplatMesh::fillSplatDataArrays() -> "scales" and "rotations" must both be valid.'); - } - splatBuffer.fillSplatScaleRotationArray(scales, rotations, sceneTransform, - srcStart, srcEnd, destStart, scaleRotationCompressionLevel, scaleOverride); - } - if (centers) splatBuffer.fillSplatCenterArray(centers, sceneTransform, srcStart, srcEnd, destStart); - if (colors) splatBuffer.fillSplatColorArray(colors, scene.minimumAlpha, srcStart, srcEnd, destStart); - if (sphericalHarmonics) { - splatBuffer.fillSphericalHarmonicsArray(sphericalHarmonics, this.minSphericalHarmonicsDegree, - sceneTransform, srcStart, srcEnd, destStart, sphericalHarmonicsCompressionLevel); - } - destStart += splatBuffer.getSplatCount(); - } - } - - /** - * Convert splat centers, which are floating point values, to an array of integers and multiply - * each by 1000. Centers will get transformed as appropriate before conversion to integer. - * @param {number} start The index at which to start retrieving data - * @param {number} end The index at which to stop retrieving data - * @param {boolean} padFour Enforce alignment of 4 by inserting a 1 after every 3 values - * @return {Int32Array} - */ - getIntegerCenters(start, end, padFour = false) { - const splatCount = end - start + 1; - const floatCenters = new Float32Array(splatCount * 3); - this.fillSplatDataArrays(null, null, null, floatCenters, null, null, undefined, undefined, undefined, undefined, start); - let intCenters; - let componentCount = padFour ? 4 : 3; - intCenters = new Int32Array(splatCount * componentCount); - for (let i = 0; i < splatCount; i++) { - for (let t = 0; t < 3; t++) { - intCenters[i * componentCount + t] = Math.round(floatCenters[i * 3 + t] * 1000.0); - } - if (padFour) intCenters[i * componentCount + 3] = 1000; - } - return intCenters; - } - - /** - * Returns an array of splat centers, transformed as appropriate, optionally padded. - * @param {number} start The index at which to start retrieving data - * @param {number} end The index at which to stop retrieving data - * @param {boolean} padFour Enforce alignment of 4 by inserting a 1 after every 3 values - * @return {Float32Array} - */ - getFloatCenters(start, end, padFour = false) { - const splatCount = end - start + 1; - const floatCenters = new Float32Array(splatCount * 3); - this.fillSplatDataArrays(null, null, null, floatCenters, null, null, undefined, undefined, undefined, undefined, start); - if (!padFour) return floatCenters; - let paddedFloatCenters = new Float32Array(splatCount * 4); - for (let i = 0; i < splatCount; i++) { - for (let t = 0; t < 3; t++) { - paddedFloatCenters[i * 4 + t] = floatCenters[i * 3 + t]; - } - paddedFloatCenters[i * 4 + 3] = 1.0; - } - return paddedFloatCenters; - } - - /** - * Get the center for a splat, transformed as appropriate. - * @param {number} globalIndex Global index of splat - * @param {THREE.Vector3} outCenter THREE.Vector3 instance in which to store splat center - * @param {boolean} applySceneTransform By default, if the splat mesh is static, the transform of the scene to which the splat at - * 'globalIndex' belongs will be applied to the splat center. If 'applySceneTransform' is true, - * the scene transform will always be applied and if 'applySceneTransform' is false, the - * scene transform will never be applied. If undefined, the default behavior will apply. - */ - getSplatCenter = function() { - - const paramsObj = {}; - - return function(globalIndex, outCenter, applySceneTransform) { - this.getLocalSplatParameters(globalIndex, paramsObj, applySceneTransform); - paramsObj.splatBuffer.getSplatCenter(paramsObj.localIndex, outCenter, paramsObj.sceneTransform); - }; - - }(); - - /** - * Get the scale and rotation for a splat, transformed as appropriate. - * @param {number} globalIndex Global index of splat - * @param {THREE.Vector3} outScale THREE.Vector3 instance in which to store splat scale - * @param {THREE.Quaternion} outRotation THREE.Quaternion instance in which to store splat rotation - * @param {boolean} applySceneTransform By default, if the splat mesh is static, the transform of the scene to which the splat at - * 'globalIndex' belongs will be applied to the splat scale and rotation. If - * 'applySceneTransform' is true, the scene transform will always be applied and if - * 'applySceneTransform' is false, the scene transform will never be applied. If undefined, - * the default behavior will apply. - */ - getSplatScaleAndRotation = function() { - - const paramsObj = {}; - const scaleOverride = new THREE.Vector3(); - - return function(globalIndex, outScale, outRotation, applySceneTransform) { - this.getLocalSplatParameters(globalIndex, paramsObj, applySceneTransform); - scaleOverride.x = undefined; - scaleOverride.y = undefined; - scaleOverride.z = undefined; - if (this.splatRenderMode === SplatRenderMode.TwoD) scaleOverride.z = 0; - paramsObj.splatBuffer.getSplatScaleAndRotation(paramsObj.localIndex, outScale, outRotation, - paramsObj.sceneTransform, scaleOverride); - }; - - }(); - - /** - * Get the color for a splat. - * @param {number} globalIndex Global index of splat - * @param {THREE.Vector4} outColor THREE.Vector4 instance in which to store splat color - */ - getSplatColor = function() { - - const paramsObj = {}; - - return function(globalIndex, outColor) { - this.getLocalSplatParameters(globalIndex, paramsObj); - paramsObj.splatBuffer.getSplatColor(paramsObj.localIndex, outColor); - }; - - }(); - - /** - * Store the transform of the scene at 'sceneIndex' in 'outTransform'. - * @param {number} sceneIndex Index of the desired scene - * @param {THREE.Matrix4} outTransform Instance of THREE.Matrix4 in which to store the scene's transform - */ - getSceneTransform(sceneIndex, outTransform) { - const scene = this.getScene(sceneIndex); - scene.updateTransform(this.dynamicMode); - outTransform.copy(scene.transform); - } - - /** - * Get the scene at 'sceneIndex'. - * @param {number} sceneIndex Index of the desired scene - * @return {SplatScene} - */ - getScene(sceneIndex) { - if (sceneIndex < 0 || sceneIndex >= this.scenes.length) { - throw new Error('SplatMesh::getScene() -> Invalid scene index.'); - } - return this.scenes[sceneIndex]; - } - - getSceneCount() { - return this.scenes.length; - } - - getSplatBufferForSplat(globalIndex) { - return this.getScene(this.globalSplatIndexToSceneIndexMap[globalIndex]).splatBuffer; - } - - getSceneIndexForSplat(globalIndex) { - return this.globalSplatIndexToSceneIndexMap[globalIndex]; - } - - getSceneTransformForSplat(globalIndex) { - return this.getScene(this.globalSplatIndexToSceneIndexMap[globalIndex]).transform; - } - - getSplatLocalIndex(globalIndex) { - return this.globalSplatIndexToLocalSplatIndexMap[globalIndex]; - } - - static getIntegerMatrixArray(matrix) { - const matrixElements = matrix.elements; - const intMatrixArray = []; - for (let i = 0; i < 16; i++) { - intMatrixArray[i] = Math.round(matrixElements[i] * 1000.0); - } - return intMatrixArray; - } - - computeBoundingBox(applySceneTransforms = false, sceneIndex) { - let splatCount = this.getSplatCount(); - if (sceneIndex !== undefined && sceneIndex !== null) { - if (sceneIndex < 0 || sceneIndex >= this.scenes.length) { - throw new Error('SplatMesh::computeBoundingBox() -> Invalid scene index.'); - } - splatCount = this.scenes[sceneIndex].splatBuffer.getSplatCount(); - } - - const floatCenters = new Float32Array(splatCount * 3); - this.fillSplatDataArrays(null, null, null, floatCenters, null, null, applySceneTransforms, - undefined, undefined, undefined, undefined, sceneIndex); - - const min = new THREE.Vector3(); - const max = new THREE.Vector3(); - for (let i = 0; i < splatCount; i++) { - const offset = i * 3; - const x = floatCenters[offset]; - const y = floatCenters[offset + 1]; - const z = floatCenters[offset + 2]; - if (i === 0 || x < min.x) min.x = x; - if (i === 0 || y < min.y) min.y = y; - if (i === 0 || z < min.z) min.z = z; - if (i === 0 || x > max.x) max.x = x; - if (i === 0 || y > max.y) max.y = y; - if (i === 0 || z > max.z) max.z = z; - } - - return new THREE.Box3(min, max); - } -} - -var SorterWasm = "AGFzbQEAAAAADwhkeWxpbmsuMAEEAAAAAAEbA2AAAGAQf39/f39/f39/f39/f39/fwBgAAF/AhIBA2VudgZtZW1vcnkCAwCAgAQDBAMAAQIHVAQRX193YXNtX2NhbGxfY3RvcnMAABhfX3dhc21fYXBwbHlfZGF0YV9yZWxvY3MAAAtzb3J0SW5kZXhlcwABE2Vtc2NyaXB0ZW5fdGxzX2luaXQAAgqWEAMDAAELihAEAXwDewN/A30gCyAKayEMAkACQCAOBEAgDQRAQfj///8HIQpBiICAgHghDSALIAxNDQMgDCEBA0AgAyABQQJ0IgVqIAIgACAFaigCAEECdGooAgAiBTYCACAFIAogBSAKSBshCiAFIA0gBSANShshDSABQQFqIgEgC0cNAAsMAwsgDwRAIAsgDE0NAkF/IQ9B+P///wchCkGIgICAeCENIAwhAgNAIA8gByAAIAJBAnQiFWooAgAiFkECdGooAgAiFEcEQAJ/IAX9CQI4IAggFEEGdGoiDv0JAgwgDioCHP0gASAOKgIs/SACIA4qAjz9IAP95gEgBf0JAiggDv0JAgggDioCGP0gASAOKgIo/SACIA4qAjj9IAP95gEgBf0JAgggDv0JAgAgDioCEP0gASAOKgIg/SACIA4qAjD9IAP95gEgBf0JAhggDv0JAgQgDioCFP0gASAOKgIk/SACIA4qAjT9IAP95gH95AH95AH95AEiEf1f/QwAAAAAAECPQAAAAAAAQI9AIhL98gEiE/0hASIQmUQAAAAAAADgQWMEQCAQqgwBC0GAgICAeAshDgJ/IBP9IQAiEJlEAAAAAAAA4EFjBEAgEKoMAQtBgICAgHgL/REgDv0cAQJ/IBEgEf0NCAkKCwwNDg8AAAAAAAAAAP1fIBL98gEiEf0hACIQmUQAAAAAAADgQWMEQCAQqgwBC0GAgICAeAv9HAICfyAR/SEBIhCZRAAAAAAAAOBBYwRAIBCqDAELQYCAgIB4C/0cAyESIBQhDwsgAyAVaiABIBZBBHRq/QAAACAS/bUBIhH9GwAgEf0bAWogEf0bAmogEf0bA2oiDjYCACAOIAogCiAOShshCiAOIA0gDSAOSBshDSACQQFqIgIgC0cNAAsMAwsCfyAFKgIIu/0UIAUqAhi7/SIB/QwAAAAAAECPQAAAAAAAQI9A/fIBIhH9IQEiEJlEAAAAAAAA4EFjBEAgEKoMAQtBgICAgHgLIQ4CfyAR/SEAIhCZRAAAAAAAAOBBYwRAIBCqDAELQYCAgIB4CyECAn8gBSoCKLtEAAAAAABAj0CiIhCZRAAAAAAAAOBBYwRAIBCqDAELQYCAgIB4CyEFQfj///8HIQpBiICAgHghDSALIAxNDQIgAv0RIA79HAEgBf0cAiESIAwhBQNAIAMgBUECdCICaiABIAAgAmooAgBBBHRq/QAAACAS/bUBIhH9GwAgEf0bAWogEf0bAmoiAjYCACACIAogAiAKSBshCiACIA0gAiANShshDSAFQQFqIgUgC0cNAAsMAgsgDQRAQfj///8HIQpBiICAgHghDSALIAxNDQIgDCEBA0AgAyABQQJ0IgVqAn8gAiAAIAVqKAIAQQJ0aioCALtEAAAAAAAAsECiIhCZRAAAAAAAAOBBYwRAIBCqDAELQYCAgIB4CyIONgIAIAogDiAKIA5IGyEKIA0gDiANIA5KGyENIAFBAWoiASALRw0ACwwCCyAPRQRAIAsgDE0NASAFKgIoIRcgBSoCGCEYIAUqAgghGUH4////ByEKQYiAgIB4IQ0gDCEFA0ACfyAXIAEgACAFQQJ0IgdqKAIAQQR0aiICKgIIlCAZIAIqAgCUIBggAioCBJSSkrtEAAAAAAAAsECiIhCZRAAAAAAAAOBBYwRAIBCqDAELQYCAgIB4CyEOIAMgB2ogDjYCACAKIA4gCiAOSBshCiANIA4gDSAOShshDSAFQQFqIgUgC0cNAAsMAgsgCyAMTQ0AQX8hD0H4////ByEKQYiAgIB4IQ0gDCECA0AgDyAHIAAgAkECdCIUaigCAEECdCIVaigCACIORwRAIAX9CQI4IAggDkEGdGoiD/0JAgwgDyoCHP0gASAPKgIs/SACIA8qAjz9IAP95gEgBf0JAiggD/0JAgggDyoCGP0gASAPKgIo/SACIA8qAjj9IAP95gEgBf0JAgggD/0JAgAgDyoCEP0gASAPKgIg/SACIA8qAjD9IAP95gEgBf0JAhggD/0JAgQgDyoCFP0gASAPKgIk/SACIA8qAjT9IAP95gH95AH95AH95AEhESAOIQ8LIAMgFGoCfyAR/R8DIAEgFUECdCIOQQxyaioCAJQgEf0fAiABIA5BCHJqKgIAlCAR/R8AIAEgDmoqAgCUIBH9HwEgASAOQQRyaioCAJSSkpK7RAAAAAAAALBAoiIQmUQAAAAAAADgQWMEQCAQqgwBC0GAgICAeAsiDjYCACAKIA4gCiAOSBshCiANIA4gDSAOShshDSACQQFqIgIgC0cNAAsMAQtBiICAgHghDUH4////ByEKCyALIAxLBEAgCUEBa7MgDbIgCrKTlSEXIAwhDQNAAn8gFyADIA1BAnRqIgEoAgAgCmuylCIYi0MAAABPXQRAIBioDAELQYCAgIB4CyEOIAEgDjYCACAEIA5BAnRqIgEgASgCAEEBajYCACANQQFqIg0gC0cNAAsLIAlBAk8EQCAEKAIAIQ1BASEKA0AgBCAKQQJ0aiIBIAEoAgAgDWoiDTYCACAKQQFqIgogCUcNAAsLIAxBAEoEQCAMIQoDQCAGIApBAWsiAUECdCICaiAAIAJqKAIANgIAIApBAUshAiABIQogAg0ACwsgCyAMSgRAIAshCgNAIAYgCyAEIAMgCkEBayIKQQJ0IgFqKAIAQQJ0aiICKAIAIgVrQQJ0aiAAIAFqKAIANgIAIAIgBUEBazYCACAKIAxKDQALCwsEAEEACw=="; - -var SorterWasmNoSIMD = "AGFzbQEAAAAADwhkeWxpbmsuMAEEAAAAAAEXAmAAAGAQf39/f39/f39/f39/f39/fwACEgEDZW52Bm1lbW9yeQIDAICABAMDAgABBz4DEV9fd2FzbV9jYWxsX2N0b3JzAAAYX193YXNtX2FwcGx5X2RhdGFfcmVsb2NzAAALc29ydEluZGV4ZXMAAQqiDwICAAucDwMBfAd9Bn8gCyAKayEMAkACQCAOBEAgDQRAQfj///8HIQpBiICAgHghDSALIAxNDQMgDCEFA0AgAyAFQQJ0IgFqIAIgACABaigCAEECdGooAgAiATYCACABIAogASAKSBshCiABIA0gASANShshDSAFQQFqIgUgC0cNAAsMAwsgDwRAIAsgDE0NAkF/IQ9B+P///wchCkGIgICAeCENIAwhAgNAIA8gByAAIAJBAnQiGmooAgBBAnQiG2ooAgAiDkcEQAJ/IAUqAjgiESAIIA5BBnRqIg8qAjyUIAUqAigiEiAPKgI4lCAFKgIIIhMgDyoCMJQgBSoCGCIUIA8qAjSUkpKSu0QAAAAAAECPQKIiEJlEAAAAAAAA4EFjBEAgEKoMAQtBgICAgHgLIRgCfyARIA8qAiyUIBIgDyoCKJQgEyAPKgIglCAUIA8qAiSUkpKSu0QAAAAAAECPQKIiEJlEAAAAAAAA4EFjBEAgEKoMAQtBgICAgHgLIRkCfyARIA8qAhyUIBIgDyoCGJQgEyAPKgIQlCAUIA8qAhSUkpKSu0QAAAAAAECPQKIiEJlEAAAAAAAA4EFjBEAgEKoMAQtBgICAgHgLIRwCfyARIA8qAgyUIBIgDyoCCJQgEyAPKgIAlCAUIA8qAgSUkpKSu0QAAAAAAECPQKIiEJlEAAAAAAAA4EFjBEAgEKoMAQtBgICAgHgLIR0gDiEPCyADIBpqIAEgG0ECdGoiDigCBCAcbCAOKAIAIB1saiAOKAIIIBlsaiAOKAIMIBhsaiIONgIAIA4gCiAKIA5KGyEKIA4gDSANIA5IGyENIAJBAWoiAiALRw0ACwwDCwJ/IAUqAii7RAAAAAAAQI9AoiIQmUQAAAAAAADgQWMEQCAQqgwBC0GAgICAeAshAgJ/IAUqAhi7RAAAAAAAQI9AoiIQmUQAAAAAAADgQWMEQCAQqgwBC0GAgICAeAshByALIAxNAn8gBSoCCLtEAAAAAABAj0CiIhCZRAAAAAAAAOBBYwRAIBCqDAELQYCAgIB4CyEPQfj///8HIQpBiICAgHghDQ0CIAwhBQNAIAMgBUECdCIIaiABIAAgCGooAgBBBHRqIggoAgQgB2wgCCgCACAPbGogCCgCCCACbGoiCDYCACAIIAogCCAKSBshCiAIIA0gCCANShshDSAFQQFqIgUgC0cNAAsMAgsgDQRAQfj///8HIQpBiICAgHghDSALIAxNDQIgDCEFA0AgAyAFQQJ0IgFqAn8gAiAAIAFqKAIAQQJ0aioCALtEAAAAAAAAsECiIhCZRAAAAAAAAOBBYwRAIBCqDAELQYCAgIB4CyIONgIAIAogDiAKIA5IGyEKIA0gDiANIA5KGyENIAVBAWoiBSALRw0ACwwCCyAPRQRAIAsgDE0NASAFKgIoIREgBSoCGCESIAUqAgghE0H4////ByEKQYiAgIB4IQ0gDCEFA0ACfyARIAEgACAFQQJ0IgdqKAIAQQR0aiICKgIIlCATIAIqAgCUIBIgAioCBJSSkrtEAAAAAAAAsECiIhCZRAAAAAAAAOBBYwRAIBCqDAELQYCAgIB4CyEOIAMgB2ogDjYCACAKIA4gCiAOSBshCiANIA4gDSAOShshDSAFQQFqIgUgC0cNAAsMAgsgCyAMTQ0AQX8hD0H4////ByEKQYiAgIB4IQ0gDCECA0AgDyAHIAAgAkECdCIYaigCAEECdCIZaigCACIORwRAIAUqAjgiESAIIA5BBnRqIg8qAjyUIAUqAigiEiAPKgI4lCAFKgIIIhMgDyoCMJQgBSoCGCIUIA8qAjSUkpKSIRUgESAPKgIslCASIA8qAiiUIBMgDyoCIJQgFCAPKgIklJKSkiEWIBEgDyoCHJQgEiAPKgIYlCATIA8qAhCUIBQgDyoCFJSSkpIhFyARIA8qAgyUIBIgDyoCCJQgEyAPKgIAlCAUIA8qAgSUkpKSIREgDiEPCyADIBhqAn8gFSABIBlBAnRqIg4qAgyUIBYgDioCCJQgESAOKgIAlCAXIA4qAgSUkpKSu0QAAAAAAACwQKIiEJlEAAAAAAAA4EFjBEAgEKoMAQtBgICAgHgLIg42AgAgCiAOIAogDkgbIQogDSAOIA0gDkobIQ0gAkEBaiICIAtHDQALDAELQYiAgIB4IQ1B+P///wchCgsgCyAMSwRAIAlBAWuzIA2yIAqyk5UhESAMIQ0DQAJ/IBEgAyANQQJ0aiIBKAIAIAprspQiEotDAAAAT10EQCASqAwBC0GAgICAeAshDiABIA42AgAgBCAOQQJ0aiIBIAEoAgBBAWo2AgAgDUEBaiINIAtHDQALCyAJQQJPBEAgBCgCACENQQEhCgNAIAQgCkECdGoiASABKAIAIA1qIg02AgAgCkEBaiIKIAlHDQALCyAMQQBKBEAgDCEKA0AgBiAKQQFrIgFBAnQiAmogACACaigCADYCACAKQQFLIAEhCg0ACwsgCyAMSgRAIAshCgNAIAYgCyAEIAMgCkEBayIKQQJ0IgFqKAIAQQJ0aiICKAIAIgVrQQJ0aiAAIAFqKAIANgIAIAIgBUEBazYCACAKIAxKDQALCws="; - -var SorterWasmNonShared = "AGFzbQEAAAAADwhkeWxpbmsuMAEEAAAAAAEXAmAAAGAQf39/f39/f39/f39/f39/fwACDwEDZW52Bm1lbW9yeQIAAAMDAgABBz4DEV9fd2FzbV9jYWxsX2N0b3JzAAAYX193YXNtX2FwcGx5X2RhdGFfcmVsb2NzAAALc29ydEluZGV4ZXMAAQrrDwICAAvlDwQBfAN7B30DfyALIAprIQwCQAJAIA4EQCANBEBB+P///wchCkGIgICAeCENIAsgDE0NAyAMIQUDQCADIAVBAnQiAWogAiAAIAFqKAIAQQJ0aigCACIBNgIAIAEgCiABIApIGyEKIAEgDSABIA1KGyENIAVBAWoiBSALRw0ACwwDCyAPBEAgCyAMTQ0CQX8hD0H4////ByEKQYiAgIB4IQ0gDCECA0AgDyAHIAAgAkECdCIcaigCACIdQQJ0aigCACIbRwRAAn8gBf0JAjggCCAbQQZ0aiIO/QkCDCAOKgIc/SABIA4qAiz9IAIgDioCPP0gA/3mASAF/QkCKCAO/QkCCCAOKgIY/SABIA4qAij9IAIgDioCOP0gA/3mASAF/QkCCCAO/QkCACAOKgIQ/SABIA4qAiD9IAIgDioCMP0gA/3mASAF/QkCGCAO/QkCBCAOKgIU/SABIA4qAiT9IAIgDioCNP0gA/3mAf3kAf3kAf3kASIR/V/9DAAAAAAAQI9AAAAAAABAj0AiEv3yASIT/SEBIhCZRAAAAAAAAOBBYwRAIBCqDAELQYCAgIB4CyEOAn8gE/0hACIQmUQAAAAAAADgQWMEQCAQqgwBC0GAgICAeAv9ESAO/RwBAn8gESAR/Q0ICQoLDA0ODwABAgMAAQID/V8gEv3yASIR/SEAIhCZRAAAAAAAAOBBYwRAIBCqDAELQYCAgIB4C/0cAgJ/IBH9IQEiEJlEAAAAAAAA4EFjBEAgEKoMAQtBgICAgHgL/RwDIRIgGyEPCyADIBxqIAEgHUEEdGr9AAAAIBL9tQEiEf0bACAR/RsBaiAR/RsCaiAR/RsDaiIONgIAIA4gCiAKIA5KGyEKIA4gDSANIA5IGyENIAJBAWoiAiALRw0ACwwDCwJ/IAUqAgi7/RQgBSoCGLv9IgH9DAAAAAAAQI9AAAAAAABAj0D98gEiEf0hASIQmUQAAAAAAADgQWMEQCAQqgwBC0GAgICAeAshDgJ/IBH9IQAiEJlEAAAAAAAA4EFjBEAgEKoMAQtBgICAgHgLAn8gBSoCKLtEAAAAAABAj0CiIhCZRAAAAAAAAOBBYwRAIBCqDAELQYCAgIB4CyEFQfj///8HIQpBiICAgHghDSALIAxNDQL9ESAO/RwBIAX9HAIhEiAMIQUDQCADIAVBAnQiAmogASAAIAJqKAIAQQR0av0AAAAgEv21ASIR/RsAIBH9GwFqIBH9GwJqIgI2AgAgAiAKIAIgCkgbIQogAiANIAIgDUobIQ0gBUEBaiIFIAtHDQALDAILIA0EQEH4////ByEKQYiAgIB4IQ0gCyAMTQ0CIAwhBQNAIAMgBUECdCIBagJ/IAIgACABaigCAEECdGoqAgC7RAAAAAAAALBAoiIQmUQAAAAAAADgQWMEQCAQqgwBC0GAgICAeAsiDjYCACAKIA4gCiAOSBshCiANIA4gDSAOShshDSAFQQFqIgUgC0cNAAsMAgsgD0UEQCALIAxNDQEgBSoCKCEUIAUqAhghFSAFKgIIIRZB+P///wchCkGIgICAeCENIAwhBQNAAn8gFCABIAAgBUECdCIHaigCAEEEdGoiAioCCJQgFiACKgIAlCAVIAIqAgSUkpK7RAAAAAAAALBAoiIQmUQAAAAAAADgQWMEQCAQqgwBC0GAgICAeAshDiADIAdqIA42AgAgCiAOIAogDkgbIQogDSAOIA0gDkobIQ0gBUEBaiIFIAtHDQALDAILIAsgDE0NAEF/IQ9B+P///wchCkGIgICAeCENIAwhAgNAIA8gByAAIAJBAnQiG2ooAgBBAnQiHGooAgAiDkcEQCAFKgI4IhQgCCAOQQZ0aiIPKgI8lCAFKgIoIhUgDyoCOJQgBSoCCCIWIA8qAjCUIAUqAhgiFyAPKgI0lJKSkiEYIBQgDyoCLJQgFSAPKgIolCAWIA8qAiCUIBcgDyoCJJSSkpIhGSAUIA8qAhyUIBUgDyoCGJQgFiAPKgIQlCAXIA8qAhSUkpKSIRogFCAPKgIMlCAVIA8qAgiUIBYgDyoCAJQgFyAPKgIElJKSkiEUIA4hDwsgAyAbagJ/IBggASAcQQJ0aiIOKgIMlCAZIA4qAgiUIBQgDioCAJQgGiAOKgIElJKSkrtEAAAAAAAAsECiIhCZRAAAAAAAAOBBYwRAIBCqDAELQYCAgIB4CyIONgIAIAogDiAKIA5IGyEKIA0gDiANIA5KGyENIAJBAWoiAiALRw0ACwwBC0GIgICAeCENQfj///8HIQoLIAsgDEsEQCAJQQFrsyANsiAKspOVIRQgDCENA0ACfyAUIAMgDUECdGoiASgCACAKa7KUIhWLQwAAAE9dBEAgFagMAQtBgICAgHgLIQ4gASAONgIAIAQgDkECdGoiASABKAIAQQFqNgIAIA1BAWoiDSALRw0ACwsgCUECTwRAIAQoAgAhDUEBIQoDQCAEIApBAnRqIgEgASgCACANaiINNgIAIApBAWoiCiAJRw0ACwsgDEEASgRAIAwhCgNAIAYgCkEBayIBQQJ0IgJqIAAgAmooAgA2AgAgCkEBSyABIQoNAAsLIAsgDEoEQCALIQoDQCAGIAsgBCADIApBAWsiCkECdCIBaigCAEECdGoiAigCACIFa0ECdGogACABaigCADYCACACIAVBAWs2AgAgCiAMSg0ACwsL"; - -var SorterWasmNoSIMDNonShared = "AGFzbQEAAAAADwhkeWxpbmsuMAEEAAAAAAEXAmAAAGAQf39/f39/f39/f39/f39/fwACDwEDZW52Bm1lbW9yeQIAAAMDAgABBz4DEV9fd2FzbV9jYWxsX2N0b3JzAAAYX193YXNtX2FwcGx5X2RhdGFfcmVsb2NzAAALc29ydEluZGV4ZXMAAQqiDwICAAucDwMBfAd9Bn8gCyAKayEMAkACQCAOBEAgDQRAQfj///8HIQpBiICAgHghDSALIAxNDQMgDCEFA0AgAyAFQQJ0IgFqIAIgACABaigCAEECdGooAgAiATYCACABIAogASAKSBshCiABIA0gASANShshDSAFQQFqIgUgC0cNAAsMAwsgDwRAIAsgDE0NAkF/IQ9B+P///wchCkGIgICAeCENIAwhAgNAIA8gByAAIAJBAnQiGmooAgBBAnQiG2ooAgAiDkcEQAJ/IAUqAjgiESAIIA5BBnRqIg8qAjyUIAUqAigiEiAPKgI4lCAFKgIIIhMgDyoCMJQgBSoCGCIUIA8qAjSUkpKSu0QAAAAAAECPQKIiEJlEAAAAAAAA4EFjBEAgEKoMAQtBgICAgHgLIRgCfyARIA8qAiyUIBIgDyoCKJQgEyAPKgIglCAUIA8qAiSUkpKSu0QAAAAAAECPQKIiEJlEAAAAAAAA4EFjBEAgEKoMAQtBgICAgHgLIRkCfyARIA8qAhyUIBIgDyoCGJQgEyAPKgIQlCAUIA8qAhSUkpKSu0QAAAAAAECPQKIiEJlEAAAAAAAA4EFjBEAgEKoMAQtBgICAgHgLIRwCfyARIA8qAgyUIBIgDyoCCJQgEyAPKgIAlCAUIA8qAgSUkpKSu0QAAAAAAECPQKIiEJlEAAAAAAAA4EFjBEAgEKoMAQtBgICAgHgLIR0gDiEPCyADIBpqIAEgG0ECdGoiDigCBCAcbCAOKAIAIB1saiAOKAIIIBlsaiAOKAIMIBhsaiIONgIAIA4gCiAKIA5KGyEKIA4gDSANIA5IGyENIAJBAWoiAiALRw0ACwwDCwJ/IAUqAii7RAAAAAAAQI9AoiIQmUQAAAAAAADgQWMEQCAQqgwBC0GAgICAeAshAgJ/IAUqAhi7RAAAAAAAQI9AoiIQmUQAAAAAAADgQWMEQCAQqgwBC0GAgICAeAshByALIAxNAn8gBSoCCLtEAAAAAABAj0CiIhCZRAAAAAAAAOBBYwRAIBCqDAELQYCAgIB4CyEPQfj///8HIQpBiICAgHghDQ0CIAwhBQNAIAMgBUECdCIIaiABIAAgCGooAgBBBHRqIggoAgQgB2wgCCgCACAPbGogCCgCCCACbGoiCDYCACAIIAogCCAKSBshCiAIIA0gCCANShshDSAFQQFqIgUgC0cNAAsMAgsgDQRAQfj///8HIQpBiICAgHghDSALIAxNDQIgDCEFA0AgAyAFQQJ0IgFqAn8gAiAAIAFqKAIAQQJ0aioCALtEAAAAAAAAsECiIhCZRAAAAAAAAOBBYwRAIBCqDAELQYCAgIB4CyIONgIAIAogDiAKIA5IGyEKIA0gDiANIA5KGyENIAVBAWoiBSALRw0ACwwCCyAPRQRAIAsgDE0NASAFKgIoIREgBSoCGCESIAUqAgghE0H4////ByEKQYiAgIB4IQ0gDCEFA0ACfyARIAEgACAFQQJ0IgdqKAIAQQR0aiICKgIIlCATIAIqAgCUIBIgAioCBJSSkrtEAAAAAAAAsECiIhCZRAAAAAAAAOBBYwRAIBCqDAELQYCAgIB4CyEOIAMgB2ogDjYCACAKIA4gCiAOSBshCiANIA4gDSAOShshDSAFQQFqIgUgC0cNAAsMAgsgCyAMTQ0AQX8hD0H4////ByEKQYiAgIB4IQ0gDCECA0AgDyAHIAAgAkECdCIYaigCAEECdCIZaigCACIORwRAIAUqAjgiESAIIA5BBnRqIg8qAjyUIAUqAigiEiAPKgI4lCAFKgIIIhMgDyoCMJQgBSoCGCIUIA8qAjSUkpKSIRUgESAPKgIslCASIA8qAiiUIBMgDyoCIJQgFCAPKgIklJKSkiEWIBEgDyoCHJQgEiAPKgIYlCATIA8qAhCUIBQgDyoCFJSSkpIhFyARIA8qAgyUIBIgDyoCCJQgEyAPKgIAlCAUIA8qAgSUkpKSIREgDiEPCyADIBhqAn8gFSABIBlBAnRqIg4qAgyUIBYgDioCCJQgESAOKgIAlCAXIA4qAgSUkpKSu0QAAAAAAACwQKIiEJlEAAAAAAAA4EFjBEAgEKoMAQtBgICAgHgLIg42AgAgCiAOIAogDkgbIQogDSAOIA0gDkobIQ0gAkEBaiICIAtHDQALDAELQYiAgIB4IQ1B+P///wchCgsgCyAMSwRAIAlBAWuzIA2yIAqyk5UhESAMIQ0DQAJ/IBEgAyANQQJ0aiIBKAIAIAprspQiEotDAAAAT10EQCASqAwBC0GAgICAeAshDiABIA42AgAgBCAOQQJ0aiIBIAEoAgBBAWo2AgAgDUEBaiINIAtHDQALCyAJQQJPBEAgBCgCACENQQEhCgNAIAQgCkECdGoiASABKAIAIA1qIg02AgAgCkEBaiIKIAlHDQALCyAMQQBKBEAgDCEKA0AgBiAKQQFrIgFBAnQiAmogACACaigCADYCACAKQQFLIAEhCg0ACwsgCyAMSgRAIAshCgNAIAYgCyAEIAMgCkEBayIKQQJ0IgFqKAIAQQJ0aiICKAIAIgVrQQJ0aiAAIAFqKAIANgIAIAIgBUEBazYCACAKIAxKDQALCws="; - -function sortWorker(self) { - - let wasmInstance; - let wasmMemory; - let useSharedMemory; - let integerBasedSort; - let dynamicMode; - let splatCount; - let indexesToSortOffset; - let sortedIndexesOffset; - let sceneIndexesOffset; - let transformsOffset; - let precomputedDistancesOffset; - let mappedDistancesOffset; - let frequenciesOffset; - let centersOffset; - let modelViewProjOffset; - let countsZero; - let sortedIndexesOut; - let distanceMapRange; - let uploadedSplatCount; - let Constants; - - function sort(splatSortCount, splatRenderCount, modelViewProj, - usePrecomputedDistances, copyIndexesToSort, copyPrecomputedDistances, copyTransforms) { - const sortStartTime = performance.now(); - - if (!useSharedMemory) { - const indexesToSort = new Uint32Array(wasmMemory, indexesToSortOffset, copyIndexesToSort.byteLength / Constants.BytesPerInt); - indexesToSort.set(copyIndexesToSort); - const transforms = new Float32Array(wasmMemory, transformsOffset, copyTransforms.byteLength / Constants.BytesPerFloat); - transforms.set(copyTransforms); - if (usePrecomputedDistances) { - let precomputedDistances; - if (integerBasedSort) { - precomputedDistances = new Int32Array(wasmMemory, precomputedDistancesOffset, - copyPrecomputedDistances.byteLength / Constants.BytesPerInt); - } else { - precomputedDistances = new Float32Array(wasmMemory, precomputedDistancesOffset, - copyPrecomputedDistances.byteLength / Constants.BytesPerFloat); - } - precomputedDistances.set(copyPrecomputedDistances); - } - } - - if (!countsZero) countsZero = new Uint32Array(distanceMapRange); - new Float32Array(wasmMemory, modelViewProjOffset, 16).set(modelViewProj); - new Uint32Array(wasmMemory, frequenciesOffset, distanceMapRange).set(countsZero); - wasmInstance.exports.sortIndexes(indexesToSortOffset, centersOffset, precomputedDistancesOffset, - mappedDistancesOffset, frequenciesOffset, modelViewProjOffset, - sortedIndexesOffset, sceneIndexesOffset, transformsOffset, distanceMapRange, - splatSortCount, splatRenderCount, splatCount, usePrecomputedDistances, integerBasedSort, - dynamicMode); - - const sortMessage = { - 'sortDone': true, - 'splatSortCount': splatSortCount, - 'splatRenderCount': splatRenderCount, - 'sortTime': 0 - }; - if (!useSharedMemory) { - const sortedIndexes = new Uint32Array(wasmMemory, sortedIndexesOffset, splatRenderCount); - if (!sortedIndexesOut || sortedIndexesOut.length < splatRenderCount) { - sortedIndexesOut = new Uint32Array(splatRenderCount); - } - sortedIndexesOut.set(sortedIndexes); - sortMessage.sortedIndexes = sortedIndexesOut; - } - const sortEndTime = performance.now(); - - sortMessage.sortTime = sortEndTime - sortStartTime; - - self.postMessage(sortMessage); - } - - self.onmessage = (e) => { - if (e.data.centers) { - centers = e.data.centers; - sceneIndexes = e.data.sceneIndexes; - if (integerBasedSort) { - new Int32Array(wasmMemory, centersOffset + e.data.range.from * Constants.BytesPerInt * 4, - e.data.range.count * 4).set(new Int32Array(centers)); - } else { - new Float32Array(wasmMemory, centersOffset + e.data.range.from * Constants.BytesPerFloat * 4, - e.data.range.count * 4).set(new Float32Array(centers)); - } - if (dynamicMode) { - new Uint32Array(wasmMemory, sceneIndexesOffset + e.data.range.from * 4, - e.data.range.count).set(new Uint32Array(sceneIndexes)); - } - uploadedSplatCount = e.data.range.from + e.data.range.count; - } else if (e.data.sort) { - const renderCount = Math.min(e.data.sort.splatRenderCount || 0, uploadedSplatCount); - const sortCount = Math.min(e.data.sort.splatSortCount || 0, uploadedSplatCount); - const usePrecomputedDistances = e.data.sort.usePrecomputedDistances; - - let copyIndexesToSort; - let copyPrecomputedDistances; - let copyTransforms; - if (!useSharedMemory) { - copyIndexesToSort = e.data.sort.indexesToSort; - copyTransforms = e.data.sort.transforms; - if (usePrecomputedDistances) copyPrecomputedDistances = e.data.sort.precomputedDistances; - } - sort(sortCount, renderCount, e.data.sort.modelViewProj, usePrecomputedDistances, - copyIndexesToSort, copyPrecomputedDistances, copyTransforms); - } else if (e.data.init) { - // Yep, this is super hacky and gross :( - Constants = e.data.init.Constants; - - splatCount = e.data.init.splatCount; - useSharedMemory = e.data.init.useSharedMemory; - integerBasedSort = e.data.init.integerBasedSort; - dynamicMode = e.data.init.dynamicMode; - distanceMapRange = e.data.init.distanceMapRange; - uploadedSplatCount = 0; - - const CENTERS_BYTES_PER_ENTRY = integerBasedSort ? (Constants.BytesPerInt * 4) : (Constants.BytesPerFloat * 4); - - const sorterWasmBytes = new Uint8Array(e.data.init.sorterWasmBytes); - - const matrixSize = 16 * Constants.BytesPerFloat; - const memoryRequiredForIndexesToSort = splatCount * Constants.BytesPerInt; - const memoryRequiredForCenters = splatCount * CENTERS_BYTES_PER_ENTRY; - const memoryRequiredForModelViewProjectionMatrix = matrixSize; - const memoryRequiredForPrecomputedDistances = integerBasedSort ? - (splatCount * Constants.BytesPerInt) : (splatCount * Constants.BytesPerFloat); - const memoryRequiredForMappedDistances = splatCount * Constants.BytesPerInt; - const memoryRequiredForSortedIndexes = splatCount * Constants.BytesPerInt; - const memoryRequiredForIntermediateSortBuffers = integerBasedSort ? (distanceMapRange * Constants.BytesPerInt * 2) : - (distanceMapRange * Constants.BytesPerFloat * 2); - const memoryRequiredforTransformIndexes = dynamicMode ? (splatCount * Constants.BytesPerInt) : 0; - const memoryRequiredforTransforms = dynamicMode ? (Constants.MaxScenes * matrixSize) : 0; - const extraMemory = Constants.MemoryPageSize * 32; - - const totalRequiredMemory = memoryRequiredForIndexesToSort + - memoryRequiredForCenters + - memoryRequiredForModelViewProjectionMatrix + - memoryRequiredForPrecomputedDistances + - memoryRequiredForMappedDistances + - memoryRequiredForIntermediateSortBuffers + - memoryRequiredForSortedIndexes + - memoryRequiredforTransformIndexes + - memoryRequiredforTransforms + - extraMemory; - const totalPagesRequired = Math.floor(totalRequiredMemory / Constants.MemoryPageSize ) + 1; - const sorterWasmImport = { - module: {}, - env: { - memory: new WebAssembly.Memory({ - initial: totalPagesRequired, - maximum: totalPagesRequired, - shared: true, - }), - } - }; - WebAssembly.compile(sorterWasmBytes) - .then((wasmModule) => { - return WebAssembly.instantiate(wasmModule, sorterWasmImport); - }) - .then((instance) => { - wasmInstance = instance; - indexesToSortOffset = 0; - centersOffset = indexesToSortOffset + memoryRequiredForIndexesToSort; - modelViewProjOffset = centersOffset + memoryRequiredForCenters; - precomputedDistancesOffset = modelViewProjOffset + memoryRequiredForModelViewProjectionMatrix; - mappedDistancesOffset = precomputedDistancesOffset + memoryRequiredForPrecomputedDistances; - frequenciesOffset = mappedDistancesOffset + memoryRequiredForMappedDistances; - sortedIndexesOffset = frequenciesOffset + memoryRequiredForIntermediateSortBuffers; - sceneIndexesOffset = sortedIndexesOffset + memoryRequiredForSortedIndexes; - transformsOffset = sceneIndexesOffset + memoryRequiredforTransformIndexes; - wasmMemory = sorterWasmImport.env.memory.buffer; - if (useSharedMemory) { - self.postMessage({ - 'sortSetupPhase1Complete': true, - 'indexesToSortBuffer': wasmMemory, - 'indexesToSortOffset': indexesToSortOffset, - 'sortedIndexesBuffer': wasmMemory, - 'sortedIndexesOffset': sortedIndexesOffset, - 'precomputedDistancesBuffer': wasmMemory, - 'precomputedDistancesOffset': precomputedDistancesOffset, - 'transformsBuffer': wasmMemory, - 'transformsOffset': transformsOffset - }); - } else { - self.postMessage({ - 'sortSetupPhase1Complete': true - }); - } - }); - } - }; -} - -function createSortWorker(splatCount, useSharedMemory, enableSIMDInSort, integerBasedSort, dynamicMode, - splatSortDistanceMapPrecision = Constants.DefaultSplatSortDistanceMapPrecision) { - const worker = new Worker( - URL.createObjectURL( - new Blob(['(', sortWorker.toString(), ')(self)'], { - type: 'application/javascript', - }), - ), - ); - - let sourceWasm = SorterWasm; - - // iOS makes choosing the right WebAssembly configuration tricky :( - const iOSSemVer = isIOS() ? getIOSSemever() : null; - if (!enableSIMDInSort && !useSharedMemory) { - sourceWasm = SorterWasmNoSIMD; - // Testing on various devices has shown that even when shared memory is disabled, the WASM module with shared - // memory can still be used most of the time -- the exception seems to be iOS devices below 16.4 - if (iOSSemVer && iOSSemVer.major <= 16 && iOSSemVer.minor < 4) { - sourceWasm = SorterWasmNoSIMDNonShared; - } - } else if (!enableSIMDInSort) { - sourceWasm = SorterWasmNoSIMD; - } else if (!useSharedMemory) { - // Same issue with shared memory as above on iOS devices - if (iOSSemVer && iOSSemVer.major <= 16 && iOSSemVer.minor < 4) { - sourceWasm = SorterWasmNonShared; - } - } - - const sorterWasmBinaryString = atob(sourceWasm); - const sorterWasmBytes = new Uint8Array(sorterWasmBinaryString.length); - for (let i = 0; i < sorterWasmBinaryString.length; i++) { - sorterWasmBytes[i] = sorterWasmBinaryString.charCodeAt(i); - } - - worker.postMessage({ - 'init': { - 'sorterWasmBytes': sorterWasmBytes.buffer, - 'splatCount': splatCount, - 'useSharedMemory': useSharedMemory, - 'integerBasedSort': integerBasedSort, - 'dynamicMode': dynamicMode, - 'distanceMapRange': 1 << splatSortDistanceMapPrecision, - // Super hacky - 'Constants': { - 'BytesPerFloat': Constants.BytesPerFloat, - 'BytesPerInt': Constants.BytesPerInt, - 'MemoryPageSize': Constants.MemoryPageSize, - 'MaxScenes': Constants.MaxScenes - } - } - }); - return worker; -} - -const WebXRMode = { - None: 0, - VR: 1, - AR: 2 -}; - -/* -Copyright © 2010-2024 three.js authors & Mark Kellogg - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. -*/ - -class VRButton { - - static createButton( renderer, sessionInit = {} ) { - - const button = document.createElement( 'button' ); - - function showEnterVR( /* device */ ) { - - let currentSession = null; - - async function onSessionStarted( session ) { - - session.addEventListener( 'end', onSessionEnded ); - - await renderer.xr.setSession( session ); - button.textContent = 'EXIT VR'; - - currentSession = session; - - } - - function onSessionEnded( /* event */ ) { - - currentSession.removeEventListener( 'end', onSessionEnded ); - - button.textContent = 'ENTER VR'; - - currentSession = null; - - } - - // - - button.style.display = ''; - - button.style.cursor = 'pointer'; - button.style.left = 'calc(50% - 50px)'; - button.style.width = '100px'; - - button.textContent = 'ENTER VR'; - - // WebXR's requestReferenceSpace only works if the corresponding feature - // was requested at session creation time. For simplicity, just ask for - // the interesting ones as optional features, but be aware that the - // requestReferenceSpace call will fail if it turns out to be unavailable. - // ('local' is always available for immersive sessions and doesn't need to - // be requested separately.) - - const sessionOptions = { - ...sessionInit, - optionalFeatures: [ - 'local-floor', - 'bounded-floor', - 'layers', - ...( sessionInit.optionalFeatures || [] ) - ], - }; - - button.onmouseenter = function() { - - button.style.opacity = '1.0'; - - }; - - button.onmouseleave = function() { - - button.style.opacity = '0.5'; - - }; - - button.onclick = function() { - - if ( currentSession === null ) { - - navigator.xr.requestSession( 'immersive-vr', sessionOptions ).then( onSessionStarted ); - - } else { - - currentSession.end(); - - if ( navigator.xr.offerSession !== undefined ) { - - navigator.xr.offerSession( 'immersive-vr', sessionOptions ) - .then( onSessionStarted ) - .catch( ( err ) => { - - console.warn( err ); - - } ); - - } - - } - - }; - - if ( navigator.xr.offerSession !== undefined ) { - - navigator.xr.offerSession( 'immersive-vr', sessionOptions ) - .then( onSessionStarted ) - .catch( ( err ) => { - - console.warn( err ); - - } ); - - } - - } - - function disableButton() { - - button.style.display = ''; - - button.style.cursor = 'auto'; - button.style.left = 'calc(50% - 75px)'; - button.style.width = '150px'; - - button.onmouseenter = null; - button.onmouseleave = null; - - button.onclick = null; - - } - - function showWebXRNotFound() { - - disableButton(); - - button.textContent = 'VR NOT SUPPORTED'; - - } - - function showVRNotAllowed( exception ) { - - disableButton(); - - console.warn( 'Exception when trying to call xr.isSessionSupported', exception ); - - button.textContent = 'VR NOT ALLOWED'; - - } - - function stylizeElement( element ) { - - element.style.position = 'absolute'; - element.style.bottom = '20px'; - element.style.padding = '12px 6px'; - element.style.border = '1px solid #fff'; - element.style.borderRadius = '4px'; - element.style.background = 'rgba(0,0,0,0.1)'; - element.style.color = '#fff'; - element.style.font = 'normal 13px sans-serif'; - element.style.textAlign = 'center'; - element.style.opacity = '0.5'; - element.style.outline = 'none'; - element.style.zIndex = '999'; - - } - - if ( 'xr' in navigator ) { - - button.id = 'VRButton'; - button.style.display = 'none'; - - stylizeElement( button ); - - navigator.xr.isSessionSupported( 'immersive-vr' ).then( function( supported ) { - - supported ? showEnterVR() : showWebXRNotFound(); - - if ( supported && VRButton.xrSessionIsGranted ) { - - button.click(); - - } - - } ).catch( showVRNotAllowed ); - - return button; - - } else { - - const message = document.createElement( 'a' ); - - if ( window.isSecureContext === false ) { - - message.href = document.location.href.replace( /^http:/, 'https:' ); - message.innerHTML = 'WEBXR NEEDS HTTPS'; // TODO Improve message - - } else { - - message.href = 'https://immersiveweb.dev/'; - message.innerHTML = 'WEBXR NOT AVAILABLE'; - - } - - message.style.left = 'calc(50% - 90px)'; - message.style.width = '180px'; - message.style.textDecoration = 'none'; - - stylizeElement( message ); - - return message; - - } - - } - - static registerSessionGrantedListener() { - - if ( typeof navigator !== 'undefined' && 'xr' in navigator ) { - - // WebXRViewer (based on Firefox) has a bug where addEventListener - // throws a silent exception and aborts execution entirely. - if ( /WebXRViewer\//i.test( navigator.userAgent ) ) return; - - navigator.xr.addEventListener( 'sessiongranted', () => { - - VRButton.xrSessionIsGranted = true; - - } ); - - } - - } - -} - -VRButton.xrSessionIsGranted = false; -VRButton.registerSessionGrantedListener(); - -/* -Copyright © 2010-2024 three.js authors & Mark Kellogg - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. -*/ - -class ARButton { - - static createButton( renderer, sessionInit = {} ) { - - const button = document.createElement( 'button' ); - - function showStartAR( /* device */ ) { - - if ( sessionInit.domOverlay === undefined ) { - - const overlay = document.createElement( 'div' ); - overlay.style.display = 'none'; - document.body.appendChild( overlay ); - - const svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' ); - svg.setAttribute( 'width', 38 ); - svg.setAttribute( 'height', 38 ); - svg.style.position = 'absolute'; - svg.style.right = '20px'; - svg.style.top = '20px'; - svg.addEventListener( 'click', function() { - - currentSession.end(); - - } ); - overlay.appendChild( svg ); - - const path = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' ); - path.setAttribute( 'd', 'M 12,12 L 28,28 M 28,12 12,28' ); - path.setAttribute( 'stroke', '#fff' ); - path.setAttribute( 'stroke-width', 2 ); - svg.appendChild( path ); - - if ( sessionInit.optionalFeatures === undefined ) { - - sessionInit.optionalFeatures = []; - - } - - sessionInit.optionalFeatures.push( 'dom-overlay' ); - sessionInit.domOverlay = { root: overlay }; - - } - - // - - let currentSession = null; - - async function onSessionStarted( session ) { - - session.addEventListener( 'end', onSessionEnded ); - - renderer.xr.setReferenceSpaceType( 'local' ); - - await renderer.xr.setSession( session ); - - button.textContent = 'STOP AR'; - sessionInit.domOverlay.root.style.display = ''; - - currentSession = session; - - } - - function onSessionEnded( /* event */ ) { - - currentSession.removeEventListener( 'end', onSessionEnded ); - - button.textContent = 'START AR'; - sessionInit.domOverlay.root.style.display = 'none'; - - currentSession = null; - - } - - // - - button.style.display = ''; - - button.style.cursor = 'pointer'; - button.style.left = 'calc(50% - 50px)'; - button.style.width = '100px'; - - button.textContent = 'START AR'; - - button.onmouseenter = function() { - - button.style.opacity = '1.0'; - - }; - - button.onmouseleave = function() { - - button.style.opacity = '0.5'; - - }; - - button.onclick = function() { - - if ( currentSession === null ) { - - navigator.xr.requestSession( 'immersive-ar', sessionInit ).then( onSessionStarted ); - - } else { - - currentSession.end(); - - if ( navigator.xr.offerSession !== undefined ) { - - navigator.xr.offerSession( 'immersive-ar', sessionInit ) - .then( onSessionStarted ) - .catch( ( err ) => { - - console.warn( err ); - - } ); - - } - - } - - }; - - if ( navigator.xr.offerSession !== undefined ) { - - navigator.xr.offerSession( 'immersive-ar', sessionInit ) - .then( onSessionStarted ) - .catch( ( err ) => { - - console.warn( err ); - - } ); - - } - - } - - function disableButton() { - - button.style.display = ''; - - button.style.cursor = 'auto'; - button.style.left = 'calc(50% - 75px)'; - button.style.width = '150px'; - - button.onmouseenter = null; - button.onmouseleave = null; - - button.onclick = null; - - } - - function showARNotSupported() { - - disableButton(); - - button.textContent = 'AR NOT SUPPORTED'; - - } - - function showARNotAllowed( exception ) { - - disableButton(); - - console.warn( 'Exception when trying to call xr.isSessionSupported', exception ); - - button.textContent = 'AR NOT ALLOWED'; - - } - - function stylizeElement( element ) { - - element.style.position = 'absolute'; - element.style.bottom = '20px'; - element.style.padding = '12px 6px'; - element.style.border = '1px solid #fff'; - element.style.borderRadius = '4px'; - element.style.background = 'rgba(0,0,0,0.1)'; - element.style.color = '#fff'; - element.style.font = 'normal 13px sans-serif'; - element.style.textAlign = 'center'; - element.style.opacity = '0.5'; - element.style.outline = 'none'; - element.style.zIndex = '999'; - - } - - if ( 'xr' in navigator ) { - - button.id = 'ARButton'; - button.style.display = 'none'; - - stylizeElement( button ); - - navigator.xr.isSessionSupported( 'immersive-ar' ).then( function( supported ) { - - supported ? showStartAR() : showARNotSupported(); - - } ).catch( showARNotAllowed ); - - return button; - - } else { - - const message = document.createElement( 'a' ); - - if ( window.isSecureContext === false ) { - - message.href = document.location.href.replace( /^http:/, 'https:' ); - message.innerHTML = 'WEBXR NEEDS HTTPS'; // TODO Improve message - - } else { - - message.href = 'https://immersiveweb.dev/'; - message.innerHTML = 'WEBXR NOT AVAILABLE'; - - } - - message.style.left = 'calc(50% - 90px)'; - message.style.width = '180px'; - message.style.textDecoration = 'none'; - - stylizeElement( message ); - - return message; - - } - - } - -} - -const RenderMode = { - Always: 0, - OnChange: 1, - Never: 2 -}; - -const THREE_CAMERA_FOV = 50; -const MINIMUM_DISTANCE_TO_NEW_FOCAL_POINT = .75; -const MIN_SPLAT_COUNT_TO_SHOW_SPLAT_TREE_LOADING_SPINNER = 1500000; -const FOCUS_MARKER_FADE_IN_SPEED = 10.0; -const FOCUS_MARKER_FADE_OUT_SPEED = 2.5; -const CONSECUTIVE_RENDERED_FRAMES_FOR_FPS_CALCULATION = 60; - -/** - * Viewer: Manages the rendering of splat scenes. Manages an instance of SplatMesh as well as a web worker - * that performs the sort for its splats. - */ -class Viewer { - - constructor(options = {}) { - - // The natural 'up' vector for viewing the scene (only has an effect when used with orbit controls and - // when the viewer uses its own camera). - if (!options.cameraUp) options.cameraUp = [0, 1, 0]; - this.cameraUp = new THREE.Vector3().fromArray(options.cameraUp); - - // The camera's initial position (only used when the viewer uses its own camera). - if (!options.initialCameraPosition) options.initialCameraPosition = [0, 10, 15]; - this.initialCameraPosition = new THREE.Vector3().fromArray(options.initialCameraPosition); - - // The initial focal point of the camera and center of the camera's orbit (only used when the viewer uses its own camera). - if (!options.initialCameraLookAt) options.initialCameraLookAt = [0, 0, 0]; - this.initialCameraLookAt = new THREE.Vector3().fromArray(options.initialCameraLookAt); - - // 'dropInMode' is a flag that is used internally to support the usage of the viewer as a Three.js scene object - this.dropInMode = options.dropInMode || false; - - // If 'selfDrivenMode' is true, the viewer manages its own update/animation loop via requestAnimationFrame() - if (options.selfDrivenMode === undefined || options.selfDrivenMode === null) options.selfDrivenMode = true; - this.selfDrivenMode = options.selfDrivenMode && !this.dropInMode; - this.selfDrivenUpdateFunc = this.selfDrivenUpdate.bind(this); - - // If 'useBuiltInControls' is true, the viewer will create its own instance of OrbitControls and attach to the camera - if (options.useBuiltInControls === undefined) options.useBuiltInControls = true; - this.useBuiltInControls = options.useBuiltInControls; - - // parent element of the Three.js renderer canvas - this.rootElement = options.rootElement; - - // Tells the viewer to pretend the device pixel ratio is 1, which can boost performance on devices where it is larger, - // at a small cost to visual quality - this.ignoreDevicePixelRatio = options.ignoreDevicePixelRatio || false; - this.devicePixelRatio = this.ignoreDevicePixelRatio ? 1 : (window.devicePixelRatio || 1); - - // Tells the viewer to use 16-bit floating point values when storing splat covariance data in textures, instead of 32-bit - this.halfPrecisionCovariancesOnGPU = options.halfPrecisionCovariancesOnGPU || false; - - // If 'threeScene' is valid, it will be rendered by the viewer along with the splat mesh - this.threeScene = options.threeScene; - // Allows for usage of an external Three.js renderer - this.renderer = options.renderer; - // Allows for usage of an external Three.js camera - this.camera = options.camera; - - // If 'gpuAcceleratedSort' is true, a partially GPU-accelerated approach to sorting splats will be used. - // Currently this means pre-computing splat distances from the camera on the GPU - this.gpuAcceleratedSort = options.gpuAcceleratedSort || false; - - // if 'integerBasedSort' is true, the integer version of splat centers as well as other values used to calculate - // splat distances are used instead of the float version. This speeds up computation, but introduces the possibility of - // overflow in larger scenes. - if (options.integerBasedSort === undefined || options.integerBasedSort === null) { - options.integerBasedSort = true; - } - this.integerBasedSort = options.integerBasedSort; - - // If 'sharedMemoryForWorkers' is true, a SharedArrayBuffer will be used to communicate with web workers. This method - // is faster than copying memory to or from web workers, but comes with security implications as outlined here: - // https://web.dev/articles/cross-origin-isolation-guide - // If enabled, it requires specific CORS headers to be present in the response from the server that is sent when - // loading the application. More information is available in the README. - if (options.sharedMemoryForWorkers === undefined || options.sharedMemoryForWorkers === null) options.sharedMemoryForWorkers = true; - this.sharedMemoryForWorkers = options.sharedMemoryForWorkers; - - // if 'dynamicScene' is true, it tells the viewer to assume scene elements are not stationary or that the number of splats in the - // scene may change. This prevents optimizations that depend on a static scene from being made. Additionally, if 'dynamicScene' is - // true it tells the splat mesh to not apply scene tranforms to splat data that is returned by functions like - // SplatMesh.getSplatCenter() by default. - this.dynamicScene = !!options.dynamicScene; - - // When true, will perform additional steps during rendering to address artifacts caused by the rendering of gaussians at a - // substantially different resolution than that at which they were rendered during training. This will only work correctly - // for models that were trained using a process that utilizes this compensation calculation. For more details: - // https://github.com/nerfstudio-project/gsplat/pull/117 - // https://github.com/graphdeco-inria/gaussian-splatting/issues/294#issuecomment-1772688093 - this.antialiased = options.antialiased || false; - - // This constant is added to the projected 2D screen-space splat scales - this.kernel2DSize = (options.kernel2DSize === undefined) ? 0.3 : options.kernel2DSize; - - this.webXRMode = options.webXRMode || WebXRMode.None; - if (this.webXRMode !== WebXRMode.None) { - this.gpuAcceleratedSort = false; - } - this.webXRActive = false; - - this.webXRSessionInit = options.webXRSessionInit || {}; - - // if 'renderMode' is RenderMode.Always, then the viewer will rrender the scene on every update. If it is RenderMode.OnChange, - // it will only render when something in the scene has changed. - this.renderMode = options.renderMode || RenderMode.Always; - - // SceneRevealMode.Default results in a nice, slow fade-in effect for progressively loaded scenes, - // and a fast fade-in for non progressively loaded scenes. - // SceneRevealMode.Gradual will force a slow fade-in for all scenes. - // SceneRevealMode.Instant will force all loaded scene data to be immediately visible. - this.sceneRevealMode = options.sceneRevealMode || SceneRevealMode.Default; - - // Hacky, experimental, non-scientific parameter for tweaking focal length related calculations. For scenes with very - // small gaussians, small details, and small dimensions -- increasing this value can help improve visual quality. - this.focalAdjustment = options.focalAdjustment || 1.0; - - // Specify the maximum screen-space splat size, can help deal with large splats that get too unwieldy - this.maxScreenSpaceSplatSize = options.maxScreenSpaceSplatSize || 1024; - - // The verbosity of console logging - this.logLevel = options.logLevel || LogLevel.None; - - // Degree of spherical harmonics to utilize in rendering splats (assuming the data is present in the splat scene). - // Valid values are 0 - 2. Default value is 0. - this.sphericalHarmonicsDegree = options.sphericalHarmonicsDegree || 0; - - // When true, allows for usage of extra properties and attributes during rendering for effects such as opacity adjustment. - // Default is false for performance reasons. These properties are separate from transform properties (scale, rotation, position) - // that are enabled by the 'dynamicScene' parameter. - this.enableOptionalEffects = options.enableOptionalEffects || false; - - // Enable the usage of SIMD WebAssembly instructions for the splat sort - if (options.enableSIMDInSort === undefined || options.enableSIMDInSort === null) options.enableSIMDInSort = true; - this.enableSIMDInSort = options.enableSIMDInSort; - - // Level to compress non KSPLAT files when loading them for direct rendering - if (options.inMemoryCompressionLevel === undefined || options.inMemoryCompressionLevel === null) { - options.inMemoryCompressionLevel = 0; - } - this.inMemoryCompressionLevel = options.inMemoryCompressionLevel; - - // Reorder splat data in memory after loading is complete to optimize cache utilization. Default is true. - // Does not apply if splat scene is progressively loaded. - if (options.optimizeSplatData === undefined || options.optimizeSplatData === null) { - options.optimizeSplatData = true; - } - this.optimizeSplatData = options.optimizeSplatData; - - // When true, the intermediate splat data that is the result of decompressing splat bufffer(s) and is used to - // populate the data textures will be freed. This will reduces memory usage, but if that data needs to be modified - // it will need to be re-populated from the splat buffer(s). Default is false. - if (options.freeIntermediateSplatData === undefined || options.freeIntermediateSplatData === null) { - options.freeIntermediateSplatData = false; - } - this.freeIntermediateSplatData = options.freeIntermediateSplatData; - - // It appears that for certain iOS versions, special actions need to be taken with the - // usage of SIMD instructions and shared memory - if (isIOS()) { - const semver = getIOSSemever(); - if (semver.major < 17) { - this.enableSIMDInSort = false; - } - if (semver.major < 16) { - this.sharedMemoryForWorkers = false; - } - } - - // Tell the viewer how to render the splats - if (options.splatRenderMode === undefined || options.splatRenderMode === null) { - options.splatRenderMode = SplatRenderMode.ThreeD; - } - this.splatRenderMode = options.splatRenderMode; - - // Customize the speed at which the scene is revealed - this.sceneFadeInRateMultiplier = options.sceneFadeInRateMultiplier || 1.0; - - // Set the range for the depth map for the counting sort used to sort the splats - this.splatSortDistanceMapPrecision = options.splatSortDistanceMapPrecision || Constants.DefaultSplatSortDistanceMapPrecision; - const maxPrecision = this.integerBasedSort ? 20 : 24; - this.splatSortDistanceMapPrecision = clamp(this.splatSortDistanceMapPrecision, 10, maxPrecision); - - this.onSplatMeshChangedCallback = null; - this.createSplatMesh(); - - this.controls = null; - this.perspectiveControls = null; - this.orthographicControls = null; - - this.orthographicCamera = null; - this.perspectiveCamera = null; - - this.showMeshCursor = false; - this.showControlPlane = false; - this.showInfo = false; - - this.sceneHelper = null; - - this.sortWorker = null; - this.sortRunning = false; - this.splatRenderCount = 0; - this.splatSortCount = 0; - this.lastSplatSortCount = 0; - this.sortWorkerIndexesToSort = null; - this.sortWorkerSortedIndexes = null; - this.sortWorkerPrecomputedDistances = null; - this.sortWorkerTransforms = null; - this.preSortMessages = []; - this.runAfterNextSort = []; - - this.selfDrivenModeRunning = false; - this.splatRenderReady = false; - - this.raycaster = new Raycaster(); - - this.infoPanel = null; - - this.startInOrthographicMode = false; - - this.currentFPS = 0; - this.lastSortTime = 0; - this.consecutiveRenderFrames = 0; - - this.previousCameraTarget = new THREE.Vector3(); - this.nextCameraTarget = new THREE.Vector3(); - - this.mousePosition = new THREE.Vector2(); - this.mouseDownPosition = new THREE.Vector2(); - this.mouseDownTime = null; - - this.resizeObserver = null; - this.mouseMoveListener = null; - this.mouseDownListener = null; - this.mouseUpListener = null; - this.keyDownListener = null; - - this.sortPromise = null; - this.sortPromiseResolver = null; - this.splatSceneDownloadPromises = {}; - this.splatSceneDownloadAndBuildPromise = null; - this.splatSceneRemovalPromise = null; - - this.loadingSpinner = new LoadingSpinner(null, this.rootElement || document.body); - this.loadingSpinner.hide(); - this.loadingProgressBar = new LoadingProgressBar(this.rootElement || document.body); - this.loadingProgressBar.hide(); - this.infoPanel = new InfoPanel(this.rootElement || document.body); - this.infoPanel.hide(); - - this.usingExternalCamera = (this.dropInMode || this.camera) ? true : false; - this.usingExternalRenderer = (this.dropInMode || this.renderer) ? true : false; - - this.initialized = false; - this.disposing = false; - this.disposed = false; - this.disposePromise = null; - if (!this.dropInMode) this.init(); - } - - createSplatMesh() { - this.splatMesh = new SplatMesh(this.splatRenderMode, this.dynamicScene, this.enableOptionalEffects, - this.halfPrecisionCovariancesOnGPU, this.devicePixelRatio, this.gpuAcceleratedSort, - this.integerBasedSort, this.antialiased, this.maxScreenSpaceSplatSize, this.logLevel, - this.sphericalHarmonicsDegree, this.sceneFadeInRateMultiplier, this.kernel2DSize); - this.splatMesh.frustumCulled = false; - if (this.onSplatMeshChangedCallback) this.onSplatMeshChangedCallback(); - } - - init() { - - if (this.initialized) return; - - if (!this.rootElement) { - if (!this.usingExternalRenderer) { - this.rootElement = document.createElement('div'); - this.rootElement.style.width = '100%'; - this.rootElement.style.height = '100%'; - this.rootElement.style.position = 'absolute'; - document.body.appendChild(this.rootElement); - } else { - this.rootElement = this.renderer.domElement || document.body; - } - } - - this.setupCamera(); - this.setupRenderer(); - this.setupWebXR(this.webXRSessionInit); - this.setupControls(); - this.setupEventHandlers(); - - this.threeScene = this.threeScene || new THREE.Scene(); - this.sceneHelper = new SceneHelper(this.threeScene); - this.sceneHelper.setupMeshCursor(); - this.sceneHelper.setupFocusMarker(); - this.sceneHelper.setupControlPlane(); - - this.loadingProgressBar.setContainer(this.rootElement); - this.loadingSpinner.setContainer(this.rootElement); - this.infoPanel.setContainer(this.rootElement); - - this.initialized = true; - } - - setupCamera() { - if (!this.usingExternalCamera) { - const renderDimensions = new THREE.Vector2(); - this.getRenderDimensions(renderDimensions); - - this.perspectiveCamera = new THREE.PerspectiveCamera(THREE_CAMERA_FOV, renderDimensions.x / renderDimensions.y, 0.1, 1000); - this.orthographicCamera = new THREE.OrthographicCamera(renderDimensions.x / -2, renderDimensions.x / 2, - renderDimensions.y / 2, renderDimensions.y / -2, 0.1, 1000 ); - this.camera = this.startInOrthographicMode ? this.orthographicCamera : this.perspectiveCamera; - this.camera.position.copy(this.initialCameraPosition); - this.camera.up.copy(this.cameraUp).normalize(); - this.camera.lookAt(this.initialCameraLookAt); - } - } - - setupRenderer() { - if (!this.usingExternalRenderer) { - const renderDimensions = new THREE.Vector2(); - this.getRenderDimensions(renderDimensions); - - this.renderer = new THREE.WebGLRenderer({ - antialias: false, - precision: 'highp' - }); - this.renderer.setPixelRatio(this.devicePixelRatio); - this.renderer.autoClear = true; - this.renderer.setClearColor(new THREE.Color( 0x000000 ), 0.0); - this.renderer.setSize(renderDimensions.x, renderDimensions.y); - - this.resizeObserver = new ResizeObserver(() => { - this.getRenderDimensions(renderDimensions); - this.renderer.setSize(renderDimensions.x, renderDimensions.y); - this.forceRenderNextFrame(); - }); - this.resizeObserver.observe(this.rootElement); - this.rootElement.appendChild(this.renderer.domElement); - } - - } - - setupWebXR(webXRSessionInit) { - if (this.webXRMode) { - if (this.webXRMode === WebXRMode.VR) { - this.rootElement.appendChild(VRButton.createButton(this.renderer, webXRSessionInit)); - } else if (this.webXRMode === WebXRMode.AR) { - this.rootElement.appendChild(ARButton.createButton(this.renderer, webXRSessionInit)); - } - this.renderer.xr.addEventListener('sessionstart', (e) => { - this.webXRActive = true; - }); - this.renderer.xr.addEventListener('sessionend', (e) => { - this.webXRActive = false; - }); - this.renderer.xr.enabled = true; - this.camera.position.copy(this.initialCameraPosition); - this.camera.up.copy(this.cameraUp).normalize(); - this.camera.lookAt(this.initialCameraLookAt); - } - } - - setupControls() { - if (this.useBuiltInControls && this.webXRMode === WebXRMode.None) { - if (!this.usingExternalCamera) { - this.perspectiveControls = new OrbitControls(this.perspectiveCamera, this.renderer.domElement); - this.orthographicControls = new OrbitControls(this.orthographicCamera, this.renderer.domElement); - } else { - if (this.camera.isOrthographicCamera) { - this.orthographicControls = new OrbitControls(this.camera, this.renderer.domElement); - } else { - this.perspectiveControls = new OrbitControls(this.camera, this.renderer.domElement); - } - } - for (let controls of [this.orthographicControls, this.perspectiveControls,]) { - if (controls) { - controls.listenToKeyEvents(window); - controls.rotateSpeed = 0.5; - controls.maxPolarAngle = Math.PI * .75; - controls.minPolarAngle = 0.1; - controls.enableDamping = true; - controls.dampingFactor = 0.05; - controls.target.copy(this.initialCameraLookAt); - controls.update(); - } - } - this.controls = this.camera.isOrthographicCamera ? this.orthographicControls : this.perspectiveControls; - this.controls.update(); - } - } - - setupEventHandlers() { - if (this.useBuiltInControls && this.webXRMode === WebXRMode.None) { - this.mouseMoveListener = this.onMouseMove.bind(this); - this.renderer.domElement.addEventListener('pointermove', this.mouseMoveListener, false); - this.mouseDownListener = this.onMouseDown.bind(this); - this.renderer.domElement.addEventListener('pointerdown', this.mouseDownListener, false); - this.mouseUpListener = this.onMouseUp.bind(this); - this.renderer.domElement.addEventListener('pointerup', this.mouseUpListener, false); - this.keyDownListener = this.onKeyDown.bind(this); - window.addEventListener('keydown', this.keyDownListener, false); - } - } - - removeEventHandlers() { - if (this.useBuiltInControls) { - this.renderer.domElement.removeEventListener('pointermove', this.mouseMoveListener); - this.mouseMoveListener = null; - this.renderer.domElement.removeEventListener('pointerdown', this.mouseDownListener); - this.mouseDownListener = null; - this.renderer.domElement.removeEventListener('pointerup', this.mouseUpListener); - this.mouseUpListener = null; - window.removeEventListener('keydown', this.keyDownListener); - this.keyDownListener = null; - } - } - - setRenderMode(renderMode) { - this.renderMode = renderMode; - } - - setActiveSphericalHarmonicsDegrees(activeSphericalHarmonicsDegrees) { - this.splatMesh.material.uniforms.sphericalHarmonicsDegree.value = activeSphericalHarmonicsDegrees; - this.splatMesh.material.uniformsNeedUpdate = true; - } - - onSplatMeshChanged(callback) { - this.onSplatMeshChangedCallback = callback; - } - - onKeyDown = function() { - - const forward = new THREE.Vector3(); - const tempMatrixLeft = new THREE.Matrix4(); - const tempMatrixRight = new THREE.Matrix4(); - - return function(e) { - forward.set(0, 0, -1); - forward.transformDirection(this.camera.matrixWorld); - tempMatrixLeft.makeRotationAxis(forward, Math.PI / 128); - tempMatrixRight.makeRotationAxis(forward, -Math.PI / 128); - switch (e.code) { - case 'KeyG': - this.focalAdjustment += 0.02; - this.forceRenderNextFrame(); - break; - case 'KeyF': - this.focalAdjustment -= 0.02; - this.forceRenderNextFrame(); - break; - case 'ArrowLeft': - this.camera.up.transformDirection(tempMatrixLeft); - break; - case 'ArrowRight': - this.camera.up.transformDirection(tempMatrixRight); - break; - case 'KeyC': - this.showMeshCursor = !this.showMeshCursor; - break; - case 'KeyU': - this.showControlPlane = !this.showControlPlane; - break; - case 'KeyI': - this.showInfo = !this.showInfo; - if (this.showInfo) { - this.infoPanel.show(); - } else { - this.infoPanel.hide(); - } - break; - case 'KeyO': - if (!this.usingExternalCamera) { - this.setOrthographicMode(!this.camera.isOrthographicCamera); - } - break; - case 'KeyP': - if (!this.usingExternalCamera) { - this.splatMesh.setPointCloudModeEnabled(!this.splatMesh.getPointCloudModeEnabled()); - } - break; - case 'Equal': - if (!this.usingExternalCamera) { - this.splatMesh.setSplatScale(this.splatMesh.getSplatScale() + 0.05); - } - break; - case 'Minus': - if (!this.usingExternalCamera) { - this.splatMesh.setSplatScale(Math.max(this.splatMesh.getSplatScale() - 0.05, 0.0)); - } - break; - } - }; - - }(); - - onMouseMove(mouse) { - this.mousePosition.set(mouse.offsetX, mouse.offsetY); - } - - onMouseDown() { - this.mouseDownPosition.copy(this.mousePosition); - this.mouseDownTime = getCurrentTime(); - } - - onMouseUp = function() { - - const clickOffset = new THREE.Vector2(); - - return function(mouse) { - clickOffset.copy(this.mousePosition).sub(this.mouseDownPosition); - const mouseUpTime = getCurrentTime(); - const wasClick = mouseUpTime - this.mouseDownTime < 0.5 && clickOffset.length() < 2; - if (wasClick) { - this.onMouseClick(mouse); - } - }; - - }(); - - onMouseClick(mouse) { - this.mousePosition.set(mouse.offsetX, mouse.offsetY); - this.checkForFocalPointChange(); - } - - checkForFocalPointChange = function() { - - const renderDimensions = new THREE.Vector2(); - const toNewFocalPoint = new THREE.Vector3(); - const outHits = []; - - return function() { - if (!this.transitioningCameraTarget) { - this.getRenderDimensions(renderDimensions); - outHits.length = 0; - this.raycaster.setFromCameraAndScreenPosition(this.camera, this.mousePosition, renderDimensions); - this.raycaster.intersectSplatMesh(this.splatMesh, outHits); - if (outHits.length > 0) { - const hit = outHits[0]; - const intersectionPoint = hit.origin; - toNewFocalPoint.copy(intersectionPoint).sub(this.camera.position); - if (toNewFocalPoint.length() > MINIMUM_DISTANCE_TO_NEW_FOCAL_POINT) { - this.previousCameraTarget.copy(this.controls.target); - this.nextCameraTarget.copy(intersectionPoint); - this.transitioningCameraTarget = true; - this.transitioningCameraTargetStartTime = getCurrentTime(); - } - } - } - }; - - }(); - - getRenderDimensions(outDimensions) { - if (this.rootElement) { - outDimensions.x = this.rootElement.offsetWidth; - outDimensions.y = this.rootElement.offsetHeight; - } else { - this.renderer.getSize(outDimensions); - } - } - - setOrthographicMode(orthographicMode) { - if (orthographicMode === this.camera.isOrthographicCamera) return; - const fromCamera = this.camera; - const toCamera = orthographicMode ? this.orthographicCamera : this.perspectiveCamera; - toCamera.position.copy(fromCamera.position); - toCamera.up.copy(fromCamera.up); - toCamera.rotation.copy(fromCamera.rotation); - toCamera.quaternion.copy(fromCamera.quaternion); - toCamera.matrix.copy(fromCamera.matrix); - this.camera = toCamera; - - if (this.controls) { - - const resetControls = (controls) => { - controls.saveState(); - controls.reset(); - }; - - const fromControls = this.controls; - const toControls = orthographicMode ? this.orthographicControls : this.perspectiveControls; - - resetControls(toControls); - resetControls(fromControls); - - toControls.target.copy(fromControls.target); - if (orthographicMode) { - Viewer.setCameraZoomFromPosition(toCamera, fromCamera, fromControls); - } else { - Viewer.setCameraPositionFromZoom(toCamera, fromCamera, toControls); - } - this.controls = toControls; - this.camera.lookAt(this.controls.target); - } - } - - static setCameraPositionFromZoom = function() { - - const tempVector = new THREE.Vector3(); - - return function(positionCamera, zoomedCamera, controls) { - const toLookAtDistance = 1 / (zoomedCamera.zoom * 0.001); - tempVector.copy(controls.target).sub(positionCamera.position).normalize().multiplyScalar(toLookAtDistance).negate(); - positionCamera.position.copy(controls.target).add(tempVector); - }; - - }(); - - - static setCameraZoomFromPosition = function() { - - const tempVector = new THREE.Vector3(); - - return function(zoomCamera, positionZamera, controls) { - const toLookAtDistance = tempVector.copy(controls.target).sub(positionZamera.position).length(); - zoomCamera.zoom = 1 / (toLookAtDistance * .001); - }; - - }(); - - updateSplatMesh = function() { - - const renderDimensions = new THREE.Vector2(); - - return function() { - if (!this.splatMesh) return; - const splatCount = this.splatMesh.getSplatCount(); - if (splatCount > 0) { - this.splatMesh.updateVisibleRegionFadeDistance(this.sceneRevealMode); - this.splatMesh.updateTransforms(); - this.getRenderDimensions(renderDimensions); - const focalLengthX = this.camera.projectionMatrix.elements[0] * 0.5 * - this.devicePixelRatio * renderDimensions.x; - const focalLengthY = this.camera.projectionMatrix.elements[5] * 0.5 * - this.devicePixelRatio * renderDimensions.y; - - const focalMultiplier = this.camera.isOrthographicCamera ? (1.0 / this.devicePixelRatio) : 1.0; - const focalAdjustment = this.focalAdjustment * focalMultiplier; - const inverseFocalAdjustment = 1.0 / focalAdjustment; - - this.adjustForWebXRStereo(renderDimensions); - this.splatMesh.updateUniforms(renderDimensions, focalLengthX * focalAdjustment, focalLengthY * focalAdjustment, - this.camera.isOrthographicCamera, this.camera.zoom || 1.0, inverseFocalAdjustment); - } - }; - - }(); - - adjustForWebXRStereo(renderDimensions) { - // TODO: Figure out a less hacky way to determine if stereo rendering is active - if (this.camera && this.webXRActive) { - const xrCamera = this.renderer.xr.getCamera(); - const xrCameraProj00 = xrCamera.projectionMatrix.elements[0]; - const cameraProj00 = this.camera.projectionMatrix.elements[0]; - renderDimensions.x *= (cameraProj00 / xrCameraProj00); - } - } - - isLoadingOrUnloading() { - return Object.keys(this.splatSceneDownloadPromises).length > 0 || this.splatSceneDownloadAndBuildPromise !== null || - this.splatSceneRemovalPromise !== null; - } - - isDisposingOrDisposed() { - return this.disposing || this.disposed; - } - - addSplatSceneDownloadPromise(promise) { - this.splatSceneDownloadPromises[promise.id] = promise; - } - - removeSplatSceneDownloadPromise(promise) { - delete this.splatSceneDownloadPromises[promise.id]; - } - - setSplatSceneDownloadAndBuildPromise(promise) { - this.splatSceneDownloadAndBuildPromise = promise; - } - - clearSplatSceneDownloadAndBuildPromise() { - this.splatSceneDownloadAndBuildPromise = null; - } - - /** - * Add a splat scene to the viewer and display any loading UI if appropriate. - * @param {string} path Path to splat scene to be loaded - * @param {object} options { - * - * splatAlphaRemovalThreshold: Ignore any splats with an alpha less than the specified - * value (valid range: 0 - 255), defaults to 1 - * - * showLoadingUI: Display a loading spinner while the scene is loading, defaults to true - * - * position (Array): Position of the scene, acts as an offset from its default position, defaults to [0, 0, 0] - * - * rotation (Array): Rotation of the scene represented as a quaternion, defaults to [0, 0, 0, 1] - * - * scale (Array): Scene's scale, defaults to [1, 1, 1] - * - * onProgress: Function to be called as file data are received, or other processing occurs - * - * headers: Optional HTTP headers to be sent along with splat requests - * } - * @return {AbortablePromise} - */ - addSplatScene(path, options = {}) { - - if (this.isLoadingOrUnloading()) { - throw new Error('Cannot add splat scene while another load or unload is already in progress.'); - } - - if (this.isDisposingOrDisposed()) { - throw new Error('Cannot add splat scene after dispose() is called.'); - } - - if (options.progressiveLoad && this.splatMesh.scenes && this.splatMesh.scenes.length > 0) { - console.log('addSplatScene(): "progressiveLoad" option ignore because there are multiple splat scenes'); - options.progressiveLoad = false; - } - - const format = (options.format !== undefined && options.format !== null) ? options.format : sceneFormatFromPath(path); - const progressiveLoad = Viewer.isProgressivelyLoadable(format) && options.progressiveLoad; - const showLoadingUI = (options.showLoadingUI !== undefined && options.showLoadingUI !== null) ? options.showLoadingUI : true; - - let loadingUITaskId = null; - if (showLoadingUI) { - this.loadingSpinner.removeAllTasks(); - loadingUITaskId = this.loadingSpinner.addTask('Downloading...'); - } - const hideLoadingUI = () => { - this.loadingProgressBar.hide(); - this.loadingSpinner.removeAllTasks(); - }; - - const onProgressUIUpdate = (percentComplete, percentCompleteLabel, loaderStatus) => { - if (showLoadingUI) { - if (loaderStatus === LoaderStatus.Downloading) { - if (percentComplete == 100) { - this.loadingSpinner.setMessageForTask(loadingUITaskId, 'Download complete!'); - } else { - if (progressiveLoad) { - this.loadingSpinner.setMessageForTask(loadingUITaskId, 'Downloading splats...'); - } else { - const suffix = percentCompleteLabel ? `: ${percentCompleteLabel}` : `...`; - this.loadingSpinner.setMessageForTask(loadingUITaskId, `Downloading${suffix}`); - } - } - } else if (loaderStatus === LoaderStatus.Processing) { - this.loadingSpinner.setMessageForTask(loadingUITaskId, 'Processing splats...'); - } - } - }; - - let downloadDone = false; - let downloadedPercentage = 0; - const splatBuffersAddedUIUpdate = (firstBuild, finalBuild) => { - if (showLoadingUI) { - if (firstBuild && progressiveLoad || finalBuild && !progressiveLoad) { - this.loadingSpinner.removeTask(loadingUITaskId); - if (!finalBuild && !downloadDone) this.loadingProgressBar.show(); - } - if (progressiveLoad) { - if (finalBuild) { - downloadDone = true; - this.loadingProgressBar.hide(); - } else { - this.loadingProgressBar.setProgress(downloadedPercentage); - } - } - } - }; - - const onProgress = (percentComplete, percentCompleteLabel, loaderStatus) => { - downloadedPercentage = percentComplete; - onProgressUIUpdate(percentComplete, percentCompleteLabel, loaderStatus); - if (options.onProgress) options.onProgress(percentComplete, percentCompleteLabel, loaderStatus); - }; - - const buildSection = (splatBuffer, firstBuild, finalBuild) => { - if (!progressiveLoad && options.onProgress) options.onProgress(0, '0%', LoaderStatus.Processing); - const addSplatBufferOptions = { - 'rotation': options.rotation || options.orientation, - 'position': options.position, - 'scale': options.scale, - 'splatAlphaRemovalThreshold': options.splatAlphaRemovalThreshold, - }; - return this.addSplatBuffers([splatBuffer], [addSplatBufferOptions], - finalBuild, firstBuild && showLoadingUI, showLoadingUI, - progressiveLoad, progressiveLoad).then(() => { - if (!progressiveLoad && options.onProgress) options.onProgress(100, '100%', LoaderStatus.Processing); - splatBuffersAddedUIUpdate(firstBuild, finalBuild); - }); - }; - - const loadFunc = progressiveLoad ? this.downloadAndBuildSingleSplatSceneProgressiveLoad.bind(this) : - this.downloadAndBuildSingleSplatSceneStandardLoad.bind(this); - return loadFunc(path, format, options.splatAlphaRemovalThreshold, buildSection.bind(this), - onProgress, hideLoadingUI.bind(this), options.headers); - } - - /** - * Download a single splat scene, convert to splat buffer and then rebuild the viewer's splat mesh - * by calling 'buildFunc' -- all before displaying the scene. Also sets/clears relevant instance synchronization objects, - * and calls appropriate functions on success or failure. - * @param {string} path Path to splat scene to be loaded - * @param {SceneFormat} format Format of the splat scene file - * @param {number} splatAlphaRemovalThreshold Ignore any splats with an alpha less than the specified value (valid range: 0 - 255) - * @param {function} buildFunc Function to build the viewer's splat mesh with the downloaded splat buffer - * @param {function} onProgress Function to be called as file data are received, or other processing occurs - * @param {function} onException Function to be called when exception occurs - * @param {object} headers Optional HTTP headers to pass to use for downloading splat scene - * @return {AbortablePromise} - */ - downloadAndBuildSingleSplatSceneStandardLoad(path, format, splatAlphaRemovalThreshold, buildFunc, onProgress, onException, headers) { - - const downloadPromise = this.downloadSplatSceneToSplatBuffer(path, splatAlphaRemovalThreshold, onProgress, false, - undefined, format, headers); - const downloadAndBuildPromise = abortablePromiseWithExtractedComponents(downloadPromise.abortHandler); - - downloadPromise.then((splatBuffer) => { - this.removeSplatSceneDownloadPromise(downloadPromise); - return buildFunc(splatBuffer, true, true).then(() => { - downloadAndBuildPromise.resolve(); - this.clearSplatSceneDownloadAndBuildPromise(); - }); - }) - .catch((e) => { - if (onException) onException(); - this.clearSplatSceneDownloadAndBuildPromise(); - this.removeSplatSceneDownloadPromise(downloadPromise); - downloadAndBuildPromise.reject(this.updateError(e, `Viewer::addSplatScene -> Could not load file ${path}`)); - }); - - this.addSplatSceneDownloadPromise(downloadPromise); - this.setSplatSceneDownloadAndBuildPromise(downloadAndBuildPromise.promise); - - return downloadAndBuildPromise.promise; - } - - /** - * Download a single splat scene and convert to splat buffer in a progressive manner, allowing rendering as the file downloads. - * As each section is downloaded, the viewer's splat mesh is rebuilt by calling 'buildFunc' - * Also sets/clears relevant instance synchronization objects, and calls appropriate functions on success or failure. - * @param {string} path Path to splat scene to be loaded - * @param {SceneFormat} format Format of the splat scene file - * @param {number} splatAlphaRemovalThreshold Ignore any splats with an alpha less than the specified value (valid range: 0 - 255) - * @param {function} buildFunc Function to rebuild the viewer's splat mesh after a new splat buffer section is downloaded - * @param {function} onDownloadProgress Function to be called as file data are received - * @param {function} onDownloadException Function to be called when exception occurs at any point during the full download - * @param {object} headers Optional HTTP headers to pass to use for downloading splat scene - * @return {AbortablePromise} - */ - downloadAndBuildSingleSplatSceneProgressiveLoad(path, format, splatAlphaRemovalThreshold, buildFunc, - onDownloadProgress, onDownloadException, headers) { - let progressiveLoadedSectionBuildCount = 0; - let progressiveLoadedSectionBuilding = false; - const queuedProgressiveLoadSectionBuilds = []; - - const checkAndBuildProgressiveLoadSections = () => { - if (queuedProgressiveLoadSectionBuilds.length > 0 && - !progressiveLoadedSectionBuilding && - !this.isDisposingOrDisposed()) { - progressiveLoadedSectionBuilding = true; - const queuedBuild = queuedProgressiveLoadSectionBuilds.shift(); - buildFunc(queuedBuild.splatBuffer, queuedBuild.firstBuild, queuedBuild.finalBuild) - .then(() => { - progressiveLoadedSectionBuilding = false; - if (queuedBuild.firstBuild) { - progressiveLoadFirstSectionBuildPromise.resolve(); - } else if (queuedBuild.finalBuild) { - splatSceneDownloadAndBuildPromise.resolve(); - this.clearSplatSceneDownloadAndBuildPromise(); - } - if (queuedProgressiveLoadSectionBuilds.length > 0) { - delayedExecute(() => checkAndBuildProgressiveLoadSections()); - } - }); - } - }; - - const onProgressiveLoadSectionProgress = (splatBuffer, finalBuild) => { - if (!this.isDisposingOrDisposed()) { - if (finalBuild || queuedProgressiveLoadSectionBuilds.length === 0 || - splatBuffer.getSplatCount() > queuedProgressiveLoadSectionBuilds[0].splatBuffer.getSplatCount()) { - queuedProgressiveLoadSectionBuilds.push({ - splatBuffer, - firstBuild: progressiveLoadedSectionBuildCount === 0, - finalBuild - }); - progressiveLoadedSectionBuildCount++; - checkAndBuildProgressiveLoadSections(); - } - } - }; - - const splatSceneDownloadPromise = this.downloadSplatSceneToSplatBuffer(path, splatAlphaRemovalThreshold, onDownloadProgress, true, - onProgressiveLoadSectionProgress, format, headers); - - const progressiveLoadFirstSectionBuildPromise = abortablePromiseWithExtractedComponents(splatSceneDownloadPromise.abortHandler); - const splatSceneDownloadAndBuildPromise = abortablePromiseWithExtractedComponents(); - - this.addSplatSceneDownloadPromise(splatSceneDownloadPromise); - this.setSplatSceneDownloadAndBuildPromise(splatSceneDownloadAndBuildPromise.promise); - - splatSceneDownloadPromise.then(() => { - this.removeSplatSceneDownloadPromise(splatSceneDownloadPromise); - }) - .catch((e) => { - this.clearSplatSceneDownloadAndBuildPromise(); - this.removeSplatSceneDownloadPromise(splatSceneDownloadPromise); - const error = this.updateError(e, `Viewer::addSplatScene -> Could not load one or more scenes`); - progressiveLoadFirstSectionBuildPromise.reject(error); - if (onDownloadException) onDownloadException(error); - }); - - return progressiveLoadFirstSectionBuildPromise.promise; - } - - /** - * Add multiple splat scenes to the viewer and display any loading UI if appropriate. - * @param {Array} sceneOptions Array of per-scene options: { - * - * path: Path to splat scene to be loaded - * - * splatAlphaRemovalThreshold: Ignore any splats with an alpha less than the specified - * value (valid range: 0 - 255), defaults to 1 - * - * position (Array): Position of the scene, acts as an offset from its default position, defaults to [0, 0, 0] - * - * rotation (Array): Rotation of the scene represented as a quaternion, defaults to [0, 0, 0, 1] - * - * scale (Array): Scene's scale, defaults to [1, 1, 1] - * - * headers: Optional HTTP headers to be sent along with splat requests - * - * format (SceneFormat) Optional, the format of the scene data (.ply, .ksplat, .splat). If not present, the - * file extension in 'path' will be used to determine the format (if it is present) - * } - * @param {boolean} showLoadingUI Display a loading spinner while the scene is loading, defaults to true - * @param {function} onProgress Function to be called as file data are received - * @return {AbortablePromise} - */ - addSplatScenes(sceneOptions, showLoadingUI = true, onProgress = undefined) { - - if (this.isLoadingOrUnloading()) { - throw new Error('Cannot add splat scene while another load or unload is already in progress.'); - } - - if (this.isDisposingOrDisposed()) { - throw new Error('Cannot add splat scene after dispose() is called.'); - } - - const fileCount = sceneOptions.length; - const percentComplete = []; - - let loadingUITaskId; - if (showLoadingUI) { - this.loadingSpinner.removeAllTasks(); - loadingUITaskId = this.loadingSpinner.addTask('Downloading...'); - } - - const onLoadProgress = (fileIndex, percent, percentLabel, loaderStatus) => { - percentComplete[fileIndex] = percent; - let totalPercent = 0; - for (let i = 0; i < fileCount; i++) totalPercent += percentComplete[i] || 0; - totalPercent = totalPercent / fileCount; - percentLabel = `${totalPercent.toFixed(2)}%`; - if (showLoadingUI) { - if (loaderStatus === LoaderStatus.Downloading) { - this.loadingSpinner.setMessageForTask(loadingUITaskId, totalPercent == 100 ? - `Download complete!` : `Downloading: ${percentLabel}`); - } - } - if (onProgress) onProgress(totalPercent, percentLabel, loaderStatus); - }; - - const baseDownloadPromises = []; - const nativeDownloadPromises = []; - for (let i = 0; i < sceneOptions.length; i++) { - const options = sceneOptions[i]; - const format = (options.format !== undefined && options.format !== null) ? options.format : sceneFormatFromPath(options.path); - const baseDownloadPromise = this.downloadSplatSceneToSplatBuffer(options.path, options.splatAlphaRemovalThreshold, - onLoadProgress.bind(this, i), false, undefined, - format, options.headers); - baseDownloadPromises.push(baseDownloadPromise); - nativeDownloadPromises.push(baseDownloadPromise.promise); - } - - const downloadAndBuildPromise = new AbortablePromise((resolve, reject) => { - Promise.all(nativeDownloadPromises) - .then((splatBuffers) => { - if (showLoadingUI) this.loadingSpinner.removeTask(loadingUITaskId); - if (onProgress) onProgress(0, '0%', LoaderStatus.Processing); - this.addSplatBuffers(splatBuffers, sceneOptions, true, showLoadingUI, showLoadingUI, false, false).then(() => { - if (onProgress) onProgress(100, '100%', LoaderStatus.Processing); - this.clearSplatSceneDownloadAndBuildPromise(); - resolve(); - }); - }) - .catch((e) => { - if (showLoadingUI) this.loadingSpinner.removeTask(loadingUITaskId); - this.clearSplatSceneDownloadAndBuildPromise(); - reject(this.updateError(e, `Viewer::addSplatScenes -> Could not load one or more splat scenes.`)); - }) - .finally(() => { - this.removeSplatSceneDownloadPromise(downloadAndBuildPromise); - }); - }, (reason) => { - for (let baseDownloadPromise of baseDownloadPromises) { - baseDownloadPromise.abort(reason); - } - }); - this.addSplatSceneDownloadPromise(downloadAndBuildPromise); - this.setSplatSceneDownloadAndBuildPromise(downloadAndBuildPromise); - return downloadAndBuildPromise; - } - - /** - * Download a splat scene and convert to SplatBuffer instance. - * @param {string} path Path to splat scene to be loaded - * @param {number} splatAlphaRemovalThreshold Ignore any splats with an alpha less than the specified - * value (valid range: 0 - 255), defaults to 1 - * - * @param {function} onProgress Function to be called as file data are received - * @param {boolean} progressiveBuild Construct file sections into splat buffers as they are downloaded - * @param {function} onSectionBuilt Function to be called when new section is added to the file - * @param {string} format File format of the scene - * @param {object} headers Optional HTTP headers to pass to use for downloading splat scene - * @return {AbortablePromise} - */ - downloadSplatSceneToSplatBuffer(path, splatAlphaRemovalThreshold = 1, onProgress = undefined, - progressiveBuild = false, onSectionBuilt = undefined, format, headers) { - try { - if (format === SceneFormat.Splat || format === SceneFormat.KSplat || format === SceneFormat.Ply) { - const optimizeSplatData = progressiveBuild ? false : this.optimizeSplatData; - if (format === SceneFormat.Splat) { - return SplatLoader.loadFromURL(path, onProgress, progressiveBuild, onSectionBuilt, splatAlphaRemovalThreshold, - this.inMemoryCompressionLevel, optimizeSplatData, headers); - } else if (format === SceneFormat.KSplat) { - return KSplatLoader.loadFromURL(path, onProgress, progressiveBuild, onSectionBuilt, headers); - } else if (format === SceneFormat.Ply) { - return PlyLoader.loadFromURL(path, onProgress, progressiveBuild, onSectionBuilt, splatAlphaRemovalThreshold, - this.inMemoryCompressionLevel, optimizeSplatData, this.sphericalHarmonicsDegree, headers); - } - } else if (format === SceneFormat.Spz) { - return SpzLoader.loadFromURL(path, onProgress, splatAlphaRemovalThreshold, this.inMemoryCompressionLevel, - this.optimizeSplatData, this.sphericalHarmonicsDegree, headers); - } - } catch (e) { - throw this.updateError(e, null); - } - - throw new Error(`Viewer::downloadSplatSceneToSplatBuffer -> File format not supported: ${path}`); - } - - static isProgressivelyLoadable(format) { - return format === SceneFormat.Splat || format === SceneFormat.KSplat || format === SceneFormat.Ply; - } - - /** - * Add one or more instances of SplatBuffer to the SplatMesh instance managed by the viewer and set up the sorting web worker. - * This function will terminate the existing sort worker (if there is one). - */ - addSplatBuffers = function() { - - return function(splatBuffers, splatBufferOptions = [], finalBuild = true, showLoadingUI = true, - showLoadingUIForSplatTreeBuild = true, replaceExisting = false, - enableRenderBeforeFirstSort = false, preserveVisibleRegion = true) { - - if (this.isDisposingOrDisposed()) return Promise.resolve(); - - let splatProcessingTaskId = null; - const removeSplatProcessingTask = () => { - if (splatProcessingTaskId !== null) { - this.loadingSpinner.removeTask(splatProcessingTaskId); - splatProcessingTaskId = null; - } - }; - - this.splatRenderReady = false; - return new Promise((resolve) => { - if (showLoadingUI) { - splatProcessingTaskId = this.loadingSpinner.addTask('Processing splats...'); - } - delayedExecute(() => { - if (this.isDisposingOrDisposed()) { - resolve(); - } else { - const buildResults = this.addSplatBuffersToMesh(splatBuffers, splatBufferOptions, finalBuild, - showLoadingUIForSplatTreeBuild, replaceExisting, - preserveVisibleRegion); - - const maxSplatCount = this.splatMesh.getMaxSplatCount(); - if (this.sortWorker && this.sortWorker.maxSplatCount !== maxSplatCount) this.disposeSortWorker(); - // If we aren't calculating the splat distances from the center on the GPU, the sorting worker needs - // splat centers and transform indexes so that it can calculate those distance values. - if (!this.gpuAcceleratedSort) { - this.preSortMessages.push({ - 'centers': buildResults.centers.buffer, - 'sceneIndexes': buildResults.sceneIndexes.buffer, - 'range': { - 'from': buildResults.from, - 'to': buildResults.to, - 'count': buildResults.count - } - }); - } - const sortWorkerSetupPromise = (!this.sortWorker && maxSplatCount > 0) ? - this.setupSortWorker(this.splatMesh) : Promise.resolve(); - sortWorkerSetupPromise.then(() => { - if (this.isDisposingOrDisposed()) return; - this.runSplatSort(true, true).then((sortRunning) => { - if (!this.sortWorker || !sortRunning) { - this.splatRenderReady = true; - removeSplatProcessingTask(); - resolve(); - } else { - if (enableRenderBeforeFirstSort) { - this.splatRenderReady = true; - } else { - this.runAfterNextSort.push(() => { - this.splatRenderReady = true; - }); - } - this.runAfterNextSort.push(() => { - removeSplatProcessingTask(); - resolve(); - }); - } - }); - }); - } - }, true); - }); - }; - - }(); - - /** - * Add one or more instances of SplatBuffer to the SplatMesh instance managed by the viewer. By default, this function is additive; - * all splat buffers contained by the viewer's splat mesh before calling this function will be preserved. This behavior can be - * changed by passing 'true' for 'replaceExisting'. - * @param {Array} splatBuffers SplatBuffer instances - * @param {Array} splatBufferOptions Array of options objects: { - * - * splatAlphaRemovalThreshold: Ignore any splats with an alpha less than the specified - * value (valid range: 0 - 255), defaults to 1 - * - * position (Array): Position of the scene, acts as an offset from its default position, defaults to [0, 0, 0] - * - * rotation (Array): Rotation of the scene represented as a quaternion, defaults to [0, 0, 0, 1] - * - * scale (Array): Scene's scale, defaults to [1, 1, 1] - * } - * @param {boolean} finalBuild Will the splat mesh be in its final state after this build? - * @param {boolean} showLoadingUIForSplatTreeBuild Whether or not to show the loading spinner during construction of the splat tree. - * @return {object} Object containing info about the splats that are updated - */ - addSplatBuffersToMesh = function() { - - let splatOptimizingTaskId; - - return function(splatBuffers, splatBufferOptions, finalBuild = true, showLoadingUIForSplatTreeBuild = false, - replaceExisting = false, preserveVisibleRegion = true) { - if (this.isDisposingOrDisposed()) return; - let allSplatBuffers = []; - let allSplatBufferOptions = []; - if (!replaceExisting) { - allSplatBuffers = this.splatMesh.scenes.map((scene) => scene.splatBuffer) || []; - allSplatBufferOptions = this.splatMesh.sceneOptions ? this.splatMesh.sceneOptions.map((sceneOptions) => sceneOptions) : []; - } - allSplatBuffers.push(...splatBuffers); - allSplatBufferOptions.push(...splatBufferOptions); - if (this.renderer) this.splatMesh.setRenderer(this.renderer); - const onSplatTreeIndexesUpload = (finished) => { - if (this.isDisposingOrDisposed()) return; - const splatCount = this.splatMesh.getSplatCount(); - if (showLoadingUIForSplatTreeBuild && splatCount >= MIN_SPLAT_COUNT_TO_SHOW_SPLAT_TREE_LOADING_SPINNER) { - if (!finished && !splatOptimizingTaskId) { - this.loadingSpinner.setMinimized(true, true); - splatOptimizingTaskId = this.loadingSpinner.addTask('Optimizing data structures...'); - } - } - }; - const onSplatTreeReady = (finished) => { - if (this.isDisposingOrDisposed()) return; - if (finished && splatOptimizingTaskId) { - this.loadingSpinner.removeTask(splatOptimizingTaskId); - splatOptimizingTaskId = null; - } - }; - const buildResults = this.splatMesh.build(allSplatBuffers, allSplatBufferOptions, true, finalBuild, onSplatTreeIndexesUpload, - onSplatTreeReady, preserveVisibleRegion); - if (finalBuild && this.freeIntermediateSplatData) this.splatMesh.freeIntermediateSplatData(); - return buildResults; - }; - - }(); - - /** - * Set up the splat sorting web worker. - * @param {SplatMesh} splatMesh SplatMesh instance that contains the splats to be sorted - * @return {Promise} - */ - setupSortWorker(splatMesh) { - if (this.isDisposingOrDisposed()) return; - return new Promise((resolve) => { - const DistancesArrayType = this.integerBasedSort ? Int32Array : Float32Array; - const splatCount = splatMesh.getSplatCount(); - const maxSplatCount = splatMesh.getMaxSplatCount(); - this.sortWorker = createSortWorker(maxSplatCount, this.sharedMemoryForWorkers, this.enableSIMDInSort, - this.integerBasedSort, this.splatMesh.dynamicMode, this.splatSortDistanceMapPrecision); - this.sortWorker.onmessage = (e) => { - if (e.data.sortDone) { - this.sortRunning = false; - if (this.sharedMemoryForWorkers) { - this.splatMesh.updateRenderIndexes(this.sortWorkerSortedIndexes, e.data.splatRenderCount); - } else { - const sortedIndexes = new Uint32Array(e.data.sortedIndexes.buffer, 0, e.data.splatRenderCount); - this.splatMesh.updateRenderIndexes(sortedIndexes, e.data.splatRenderCount); - } - - this.lastSplatSortCount = this.splatSortCount; - - this.lastSortTime = e.data.sortTime; - this.sortPromiseResolver(); - this.sortPromiseResolver = null; - this.forceRenderNextFrame(); - if (this.runAfterNextSort.length > 0) { - this.runAfterNextSort.forEach((func) => { - func(); - }); - this.runAfterNextSort.length = 0; - } - } else if (e.data.sortCanceled) { - this.sortRunning = false; - } else if (e.data.sortSetupPhase1Complete) { - if (this.logLevel >= LogLevel.Info) console.log('Sorting web worker WASM setup complete.'); - if (this.sharedMemoryForWorkers) { - this.sortWorkerSortedIndexes = new Uint32Array(e.data.sortedIndexesBuffer, - e.data.sortedIndexesOffset, maxSplatCount); - this.sortWorkerIndexesToSort = new Uint32Array(e.data.indexesToSortBuffer, - e.data.indexesToSortOffset, maxSplatCount); - this.sortWorkerPrecomputedDistances = new DistancesArrayType(e.data.precomputedDistancesBuffer, - e.data.precomputedDistancesOffset, - maxSplatCount); - this.sortWorkerTransforms = new Float32Array(e.data.transformsBuffer, - e.data.transformsOffset, Constants.MaxScenes * 16); - } else { - this.sortWorkerIndexesToSort = new Uint32Array(maxSplatCount); - this.sortWorkerPrecomputedDistances = new DistancesArrayType(maxSplatCount); - this.sortWorkerTransforms = new Float32Array(Constants.MaxScenes * 16); - } - for (let i = 0; i < splatCount; i++) this.sortWorkerIndexesToSort[i] = i; - this.sortWorker.maxSplatCount = maxSplatCount; - - if (this.logLevel >= LogLevel.Info) { - console.log('Sorting web worker ready.'); - const splatDataTextures = this.splatMesh.getSplatDataTextures(); - const covariancesTextureSize = splatDataTextures.covariances.size; - const centersColorsTextureSize = splatDataTextures.centerColors.size; - console.log('Covariances texture size: ' + covariancesTextureSize.x + ' x ' + covariancesTextureSize.y); - console.log('Centers/colors texture size: ' + centersColorsTextureSize.x + ' x ' + centersColorsTextureSize.y); - } - - resolve(); - } - }; - }); - } - - updateError(error, defaultMessage) { - if (error instanceof AbortedPromiseError) return error; - if (error instanceof DirectLoadError) { - return new Error('File type or server does not support progressive loading.'); - } - return defaultMessage ? new Error(defaultMessage) : error; - } - - disposeSortWorker() { - if (this.sortWorker) this.sortWorker.terminate(); - this.sortWorker = null; - this.sortPromise = null; - if (this.sortPromiseResolver) { - this.sortPromiseResolver(); - this.sortPromiseResolver = null; - } - this.preSortMessages = []; - this.sortRunning = false; - } - - removeSplatScene(indexToRemove, showLoadingUI = true) { - return this.removeSplatScenes([indexToRemove], showLoadingUI); - } - - removeSplatScenes(indexesToRemove, showLoadingUI = true) { - if (this.isLoadingOrUnloading()) { - throw new Error('Cannot remove splat scene while another load or unload is already in progress.'); - } - - if (this.isDisposingOrDisposed()) { - throw new Error('Cannot remove splat scene after dispose() is called.'); - } - - let sortPromise; - - this.splatSceneRemovalPromise = new Promise((resolve, reject) => { - let revmovalTaskId; - - if (showLoadingUI) { - this.loadingSpinner.removeAllTasks(); - this.loadingSpinner.show(); - revmovalTaskId = this.loadingSpinner.addTask('Removing splat scene...'); - } - - const checkAndHideLoadingUI = () => { - if (showLoadingUI) { - this.loadingSpinner.hide(); - this.loadingSpinner.removeTask(revmovalTaskId); - } - }; - - const onDone = (error) => { - checkAndHideLoadingUI(); - this.splatSceneRemovalPromise = null; - if (!error) resolve(); - else reject(error); - }; - - const checkForEarlyExit = () => { - if (this.isDisposingOrDisposed()) { - onDone(); - return true; - } - return false; - }; - - sortPromise = this.sortPromise || Promise.resolve(); - sortPromise.then(() => { - if (checkForEarlyExit()) return; - const savedSplatBuffers = []; - const savedSceneOptions = []; - const savedSceneTransformComponents = []; - for (let i = 0; i < this.splatMesh.scenes.length; i++) { - let shouldRemove = false; - for (let indexToRemove of indexesToRemove) { - if (indexToRemove === i) { - shouldRemove = true; - break; - } - } - if (!shouldRemove) { - const scene = this.splatMesh.scenes[i]; - savedSplatBuffers.push(scene.splatBuffer); - savedSceneOptions.push(this.splatMesh.sceneOptions[i]); - savedSceneTransformComponents.push({ - 'position': scene.position.clone(), - 'quaternion': scene.quaternion.clone(), - 'scale': scene.scale.clone() - }); - } - } - this.disposeSortWorker(); - this.splatMesh.dispose(); - this.sceneRevealMode = SceneRevealMode.Instant; - this.createSplatMesh(); - this.addSplatBuffers(savedSplatBuffers, savedSceneOptions, true, false, true) - .then(() => { - if (checkForEarlyExit()) return; - checkAndHideLoadingUI(); - this.splatMesh.scenes.forEach((scene, index) => { - scene.position.copy(savedSceneTransformComponents[index].position); - scene.quaternion.copy(savedSceneTransformComponents[index].quaternion); - scene.scale.copy(savedSceneTransformComponents[index].scale); - }); - this.splatMesh.updateTransforms(); - this.splatRenderReady = false; - - this.runSplatSort(true) - .then(() => { - if (checkForEarlyExit()) { - this.splatRenderReady = true; - return; - } - sortPromise = this.sortPromise || Promise.resolve(); - sortPromise.then(() => { - this.splatRenderReady = true; - onDone(); - }); - }); - }) - .catch((e) => { - onDone(e); - }); - }); - }); - - return this.splatSceneRemovalPromise; - } - - /** - * Start self-driven mode - */ - start() { - if (this.selfDrivenMode) { - if (this.webXRMode) { - this.renderer.setAnimationLoop(this.selfDrivenUpdateFunc); - } else { - this.requestFrameId = requestAnimationFrame(this.selfDrivenUpdateFunc); - } - this.selfDrivenModeRunning = true; - } else { - throw new Error('Cannot start viewer unless it is in self driven mode.'); - } - } - - /** - * Stop self-driven mode - */ - stop() { - if (this.selfDrivenMode && this.selfDrivenModeRunning) { - if (this.webXRMode) { - this.renderer.setAnimationLoop(null); - } else { - cancelAnimationFrame(this.requestFrameId); - } - this.selfDrivenModeRunning = false; - } - } - - /** - * Dispose of all resources held directly and indirectly by this viewer. - */ - async dispose() { - if (this.isDisposingOrDisposed()) return this.disposePromise; - - let waitPromises = []; - let promisesToAbort = []; - for (let promiseKey in this.splatSceneDownloadPromises) { - if (this.splatSceneDownloadPromises.hasOwnProperty(promiseKey)) { - const downloadPromiseToAbort = this.splatSceneDownloadPromises[promiseKey]; - promisesToAbort.push(downloadPromiseToAbort); - waitPromises.push(downloadPromiseToAbort.promise); - } - } - if (this.sortPromise) { - waitPromises.push(this.sortPromise); - } - - this.disposing = true; - this.disposePromise = Promise.all(waitPromises).finally(() => { - this.stop(); - if (this.orthographicControls) { - this.orthographicControls.dispose(); - this.orthographicControls = null; - } - if (this.perspectiveControls) { - this.perspectiveControls.dispose(); - this.perspectiveControls = null; - } - this.controls = null; - if (this.splatMesh) { - this.splatMesh.dispose(); - this.splatMesh = null; - } - if (this.sceneHelper) { - this.sceneHelper.dispose(); - this.sceneHelper = null; - } - if (this.resizeObserver) { - this.resizeObserver.unobserve(this.rootElement); - this.resizeObserver = null; - } - this.disposeSortWorker(); - this.removeEventHandlers(); - - this.loadingSpinner.removeAllTasks(); - this.loadingSpinner.setContainer(null); - this.loadingProgressBar.hide(); - this.loadingProgressBar.setContainer(null); - this.infoPanel.setContainer(null); - - this.camera = null; - this.threeScene = null; - this.splatRenderReady = false; - this.initialized = false; - if (this.renderer) { - if (!this.usingExternalRenderer) { - this.rootElement.removeChild(this.renderer.domElement); - this.renderer.dispose(); - } - this.renderer = null; - } - - if (!this.usingExternalRenderer) { - document.body.removeChild(this.rootElement); - } - - this.sortWorkerSortedIndexes = null; - this.sortWorkerIndexesToSort = null; - this.sortWorkerPrecomputedDistances = null; - this.sortWorkerTransforms = null; - this.disposed = true; - this.disposing = false; - this.disposePromise = null; - }); - promisesToAbort.forEach((toAbort) => { - toAbort.abort('Scene disposed'); - }); - return this.disposePromise; - } - - selfDrivenUpdate() { - if (this.selfDrivenMode && !this.webXRMode) { - this.requestFrameId = requestAnimationFrame(this.selfDrivenUpdateFunc); - } - this.update(); - if (this.shouldRender()) { - this.render(); - this.consecutiveRenderFrames++; - } else { - this.consecutiveRenderFrames = 0; - } - this.renderNextFrame = false; - } - - forceRenderNextFrame() { - this.renderNextFrame = true; - } - - shouldRender = function() { - - let renderCount = 0; - const lastCameraPosition = new THREE.Vector3(); - const lastCameraOrientation = new THREE.Quaternion(); - const changeEpsilon = 0.0001; - - return function() { - if (!this.initialized || !this.splatRenderReady || this.isDisposingOrDisposed()) return false; - - let shouldRender = false; - let cameraChanged = false; - if (this.camera) { - const cp = this.camera.position; - const co = this.camera.quaternion; - cameraChanged = Math.abs(cp.x - lastCameraPosition.x) > changeEpsilon || - Math.abs(cp.y - lastCameraPosition.y) > changeEpsilon || - Math.abs(cp.z - lastCameraPosition.z) > changeEpsilon || - Math.abs(co.x - lastCameraOrientation.x) > changeEpsilon || - Math.abs(co.y - lastCameraOrientation.y) > changeEpsilon || - Math.abs(co.z - lastCameraOrientation.z) > changeEpsilon || - Math.abs(co.w - lastCameraOrientation.w) > changeEpsilon; - } - - shouldRender = this.renderMode !== RenderMode.Never && (renderCount === 0 || this.splatMesh.visibleRegionChanging || - cameraChanged || this.renderMode === RenderMode.Always || this.dynamicMode === true || this.renderNextFrame); - - if (this.camera) { - lastCameraPosition.copy(this.camera.position); - lastCameraOrientation.copy(this.camera.quaternion); - } - - renderCount++; - return shouldRender; - }; - - }(); - - render = function() { - - return function() { - if (!this.initialized || !this.splatRenderReady || this.isDisposingOrDisposed()) return; - - const hasRenderables = (threeScene) => { - for (let child of threeScene.children) { - if (child.visible) return true; - } - return false; - }; - - const savedAuoClear = this.renderer.autoClear; - if (hasRenderables(this.threeScene)) { - this.renderer.render(this.threeScene, this.camera); - this.renderer.autoClear = false; - } - this.renderer.render(this.splatMesh, this.camera); - this.renderer.autoClear = false; - if (this.sceneHelper.getFocusMarkerOpacity() > 0.0) this.renderer.render(this.sceneHelper.focusMarker, this.camera); - if (this.showControlPlane) this.renderer.render(this.sceneHelper.controlPlane, this.camera); - this.renderer.autoClear = savedAuoClear; - }; - - }(); - - update(renderer, camera) { - if (this.dropInMode) this.updateForDropInMode(renderer, camera); - - if (!this.initialized || !this.splatRenderReady || this.isDisposingOrDisposed()) return; - - if (this.controls) { - this.controls.update(); - if (this.camera.isOrthographicCamera && !this.usingExternalCamera) { - Viewer.setCameraPositionFromZoom(this.camera, this.camera, this.controls); - } - } - this.runSplatSort(); - this.updateForRendererSizeChanges(); - this.updateSplatMesh(); - this.updateMeshCursor(); - this.updateFPS(); - this.timingSensitiveUpdates(); - this.updateInfoPanel(); - this.updateControlPlane(); - } - - updateForDropInMode(renderer, camera) { - this.renderer = renderer; - if (this.splatMesh) this.splatMesh.setRenderer(this.renderer); - this.camera = camera; - if (this.controls) this.controls.object = camera; - this.init(); - } - - updateFPS = function() { - - let lastCalcTime = getCurrentTime(); - let frameCount = 0; - - return function() { - if (this.consecutiveRenderFrames > CONSECUTIVE_RENDERED_FRAMES_FOR_FPS_CALCULATION) { - const currentTime = getCurrentTime(); - const calcDelta = currentTime - lastCalcTime; - if (calcDelta >= 1.0) { - this.currentFPS = frameCount; - frameCount = 0; - lastCalcTime = currentTime; - } else { - frameCount++; - } - } else { - this.currentFPS = null; - } - }; - - }(); - - updateForRendererSizeChanges = function() { - - const lastRendererSize = new THREE.Vector2(); - const currentRendererSize = new THREE.Vector2(); - let lastCameraOrthographic; - - return function() { - if (!this.usingExternalCamera) { - this.renderer.getSize(currentRendererSize); - if (lastCameraOrthographic === undefined || lastCameraOrthographic !== this.camera.isOrthographicCamera || - currentRendererSize.x !== lastRendererSize.x || currentRendererSize.y !== lastRendererSize.y) { - if (this.camera.isOrthographicCamera) { - this.camera.left = -currentRendererSize.x / 2.0; - this.camera.right = currentRendererSize.x / 2.0; - this.camera.top = currentRendererSize.y / 2.0; - this.camera.bottom = -currentRendererSize.y / 2.0; - } else { - this.camera.aspect = currentRendererSize.x / currentRendererSize.y; - } - this.camera.updateProjectionMatrix(); - lastRendererSize.copy(currentRendererSize); - lastCameraOrthographic = this.camera.isOrthographicCamera; - } - } - }; - - }(); - - timingSensitiveUpdates = function() { - - let lastUpdateTime; - - return function() { - const currentTime = getCurrentTime(); - if (!lastUpdateTime) lastUpdateTime = currentTime; - const timeDelta = currentTime - lastUpdateTime; - - this.updateCameraTransition(currentTime); - this.updateFocusMarker(timeDelta); - - lastUpdateTime = currentTime; - }; - - }(); - - updateCameraTransition = function() { - - let tempCameraTarget = new THREE.Vector3(); - let toPreviousTarget = new THREE.Vector3(); - let toNextTarget = new THREE.Vector3(); - - return function(currentTime) { - if (this.transitioningCameraTarget) { - toPreviousTarget.copy(this.previousCameraTarget).sub(this.camera.position).normalize(); - toNextTarget.copy(this.nextCameraTarget).sub(this.camera.position).normalize(); - const rotationAngle = Math.acos(toPreviousTarget.dot(toNextTarget)); - const rotationSpeed = rotationAngle / (Math.PI / 3) * .65 + .3; - const t = (rotationSpeed / rotationAngle * (currentTime - this.transitioningCameraTargetStartTime)); - tempCameraTarget.copy(this.previousCameraTarget).lerp(this.nextCameraTarget, t); - this.camera.lookAt(tempCameraTarget); - this.controls.target.copy(tempCameraTarget); - if (t >= 1.0) { - this.transitioningCameraTarget = false; - } - } - }; - - }(); - - updateFocusMarker = function() { - - const renderDimensions = new THREE.Vector2(); - let wasTransitioning = false; - - return function(timeDelta) { - this.getRenderDimensions(renderDimensions); - if (this.transitioningCameraTarget) { - this.sceneHelper.setFocusMarkerVisibility(true); - const currentFocusMarkerOpacity = Math.max(this.sceneHelper.getFocusMarkerOpacity(), 0.0); - let newFocusMarkerOpacity = Math.min(currentFocusMarkerOpacity + FOCUS_MARKER_FADE_IN_SPEED * timeDelta, 1.0); - this.sceneHelper.setFocusMarkerOpacity(newFocusMarkerOpacity); - this.sceneHelper.updateFocusMarker(this.nextCameraTarget, this.camera, renderDimensions); - wasTransitioning = true; - this.forceRenderNextFrame(); - } else { - let currentFocusMarkerOpacity; - if (wasTransitioning) currentFocusMarkerOpacity = 1.0; - else currentFocusMarkerOpacity = Math.min(this.sceneHelper.getFocusMarkerOpacity(), 1.0); - if (currentFocusMarkerOpacity > 0) { - this.sceneHelper.updateFocusMarker(this.nextCameraTarget, this.camera, renderDimensions); - let newFocusMarkerOpacity = Math.max(currentFocusMarkerOpacity - FOCUS_MARKER_FADE_OUT_SPEED * timeDelta, 0.0); - this.sceneHelper.setFocusMarkerOpacity(newFocusMarkerOpacity); - if (newFocusMarkerOpacity === 0.0) this.sceneHelper.setFocusMarkerVisibility(false); - } - if (currentFocusMarkerOpacity > 0.0) this.forceRenderNextFrame(); - wasTransitioning = false; - } - }; - - }(); - - updateMeshCursor = function() { - - const outHits = []; - const renderDimensions = new THREE.Vector2(); - - return function() { - if (this.showMeshCursor) { - this.forceRenderNextFrame(); - this.getRenderDimensions(renderDimensions); - outHits.length = 0; - this.raycaster.setFromCameraAndScreenPosition(this.camera, this.mousePosition, renderDimensions); - this.raycaster.intersectSplatMesh(this.splatMesh, outHits); - if (outHits.length > 0) { - this.sceneHelper.setMeshCursorVisibility(true); - this.sceneHelper.positionAndOrientMeshCursor(outHits[0].origin, this.camera); - } else { - this.sceneHelper.setMeshCursorVisibility(false); - } - } else { - if (this.sceneHelper.getMeschCursorVisibility()) this.forceRenderNextFrame(); - this.sceneHelper.setMeshCursorVisibility(false); - } - }; - - }(); - - updateInfoPanel = function() { - - const renderDimensions = new THREE.Vector2(); - - return function() { - if (!this.showInfo) return; - const splatCount = this.splatMesh.getSplatCount(); - this.getRenderDimensions(renderDimensions); - const cameraLookAtPosition = this.controls ? this.controls.target : null; - const meshCursorPosition = this.showMeshCursor ? this.sceneHelper.meshCursor.position : null; - const splatRenderCountPct = splatCount > 0 ? this.splatRenderCount / splatCount * 100 : 0; - this.infoPanel.update(renderDimensions, this.camera.position, cameraLookAtPosition, - this.camera.up, this.camera.isOrthographicCamera, meshCursorPosition, - this.currentFPS || 'N/A', splatCount, this.splatRenderCount, splatRenderCountPct, - this.lastSortTime, this.focalAdjustment, this.splatMesh.getSplatScale(), - this.splatMesh.getPointCloudModeEnabled()); - }; - - }(); - - updateControlPlane() { - if (this.showControlPlane) { - this.sceneHelper.setControlPlaneVisibility(true); - this.sceneHelper.positionAndOrientControlPlane(this.controls.target, this.camera.up); - } else { - this.sceneHelper.setControlPlaneVisibility(false); - } - } - - runSplatSort = function() { - - const mvpMatrix = new THREE.Matrix4(); - const cameraPositionArray = []; - const lastSortViewDir = new THREE.Vector3(0, 0, -1); - const sortViewDir = new THREE.Vector3(0, 0, -1); - const lastSortViewPos = new THREE.Vector3(); - const sortViewOffset = new THREE.Vector3(); - const queuedSorts = []; - - const partialSorts = [ - { - 'angleThreshold': 0.55, - 'sortFractions': [0.125, 0.33333, 0.75] - }, - { - 'angleThreshold': 0.65, - 'sortFractions': [0.33333, 0.66667] - }, - { - 'angleThreshold': 0.8, - 'sortFractions': [0.5] - } - ]; - - return function(force = false, forceSortAll = false) { - if (!this.initialized) return Promise.resolve(false); - if (this.sortRunning) return Promise.resolve(true); - if (this.splatMesh.getSplatCount() <= 0) { - this.splatRenderCount = 0; - return Promise.resolve(false); - } - - let angleDiff = 0; - let positionDiff = 0; - let needsRefreshForRotation = false; - let needsRefreshForPosition = false; - - sortViewDir.set(0, 0, -1).applyQuaternion(this.camera.quaternion); - angleDiff = sortViewDir.dot(lastSortViewDir); - positionDiff = sortViewOffset.copy(this.camera.position).sub(lastSortViewPos).length(); - - if (!force) { - if (!this.splatMesh.dynamicMode && queuedSorts.length === 0) { - if (angleDiff <= 0.99) needsRefreshForRotation = true; - if (positionDiff >= 1.0) needsRefreshForPosition = true; - if (!needsRefreshForRotation && !needsRefreshForPosition) return Promise.resolve(false); - } - } - - this.sortRunning = true; - let { splatRenderCount, shouldSortAll } = this.gatherSceneNodesForSort(); - shouldSortAll = shouldSortAll || forceSortAll; - this.splatRenderCount = splatRenderCount; - - mvpMatrix.copy(this.camera.matrixWorld).invert(); - const mvpCamera = this.perspectiveCamera || this.camera; - mvpMatrix.premultiply(mvpCamera.projectionMatrix); - if (!this.splatMesh.dynamicMode) mvpMatrix.multiply(this.splatMesh.matrixWorld); - - let gpuAcceleratedSortPromise = Promise.resolve(true); - if (this.gpuAcceleratedSort && (queuedSorts.length <= 1 || queuedSorts.length % 2 === 0)) { - gpuAcceleratedSortPromise = this.splatMesh.computeDistancesOnGPU(mvpMatrix, this.sortWorkerPrecomputedDistances); - } - - gpuAcceleratedSortPromise.then(() => { - if (queuedSorts.length === 0) { - if (this.splatMesh.dynamicMode || shouldSortAll) { - queuedSorts.push(this.splatRenderCount); - } else { - for (let partialSort of partialSorts) { - if (angleDiff < partialSort.angleThreshold) { - for (let sortFraction of partialSort.sortFractions) { - queuedSorts.push(Math.floor(this.splatRenderCount * sortFraction)); - } - break; - } - } - queuedSorts.push(this.splatRenderCount); - } - } - let sortCount = Math.min(queuedSorts.shift(), this.splatRenderCount); - this.splatSortCount = sortCount; - - cameraPositionArray[0] = this.camera.position.x; - cameraPositionArray[1] = this.camera.position.y; - cameraPositionArray[2] = this.camera.position.z; - - const sortMessage = { - 'modelViewProj': mvpMatrix.elements, - 'cameraPosition': cameraPositionArray, - 'splatRenderCount': this.splatRenderCount, - 'splatSortCount': sortCount, - 'usePrecomputedDistances': this.gpuAcceleratedSort - }; - if (this.splatMesh.dynamicMode) { - this.splatMesh.fillTransformsArray(this.sortWorkerTransforms); - } - if (!this.sharedMemoryForWorkers) { - sortMessage.indexesToSort = this.sortWorkerIndexesToSort; - sortMessage.transforms = this.sortWorkerTransforms; - if (this.gpuAcceleratedSort) { - sortMessage.precomputedDistances = this.sortWorkerPrecomputedDistances; - } - } - - this.sortPromise = new Promise((resolve) => { - this.sortPromiseResolver = resolve; - }); - - if (this.preSortMessages.length > 0) { - this.preSortMessages.forEach((message) => { - this.sortWorker.postMessage(message); - }); - this.preSortMessages = []; - } - this.sortWorker.postMessage({ - 'sort': sortMessage - }); - - if (queuedSorts.length === 0) { - lastSortViewPos.copy(this.camera.position); - lastSortViewDir.copy(sortViewDir); - } - - return true; - }); - - return gpuAcceleratedSortPromise; - }; - - }(); - - /** - * Determine which splats to render by checking which are inside or close to the view frustum - */ - gatherSceneNodesForSort = function() { - - const nodeRenderList = []; - let allSplatsSortBuffer = null; - const tempVectorYZ = new THREE.Vector3(); - const tempVectorXZ = new THREE.Vector3(); - const tempVector = new THREE.Vector3(); - const modelView = new THREE.Matrix4(); - const baseModelView = new THREE.Matrix4(); - const sceneTransform = new THREE.Matrix4(); - const renderDimensions = new THREE.Vector3(); - const forward = new THREE.Vector3(0, 0, -1); - - const tempMax = new THREE.Vector3(); - const nodeSize = (node) => { - return tempMax.copy(node.max).sub(node.min).length(); - }; - - return function(gatherAllNodes = false) { - - this.getRenderDimensions(renderDimensions); - const cameraFocalLength = (renderDimensions.y / 2.0) / Math.tan(this.camera.fov / 2.0 * THREE.MathUtils.DEG2RAD); - const fovXOver2 = Math.atan(renderDimensions.x / 2.0 / cameraFocalLength); - const fovYOver2 = Math.atan(renderDimensions.y / 2.0 / cameraFocalLength); - const cosFovXOver2 = Math.cos(fovXOver2); - const cosFovYOver2 = Math.cos(fovYOver2); - - const splatTree = this.splatMesh.getSplatTree(); - - if (splatTree) { - baseModelView.copy(this.camera.matrixWorld).invert(); - if (!this.splatMesh.dynamicMode) baseModelView.multiply(this.splatMesh.matrixWorld); - - let nodeRenderCount = 0; - let splatRenderCount = 0; - - for (let s = 0; s < splatTree.subTrees.length; s++) { - const subTree = splatTree.subTrees[s]; - modelView.copy(baseModelView); - if (this.splatMesh.dynamicMode) { - this.splatMesh.getSceneTransform(s, sceneTransform); - modelView.multiply(sceneTransform); - } - const nodeCount = subTree.nodesWithIndexes.length; - for (let i = 0; i < nodeCount; i++) { - const node = subTree.nodesWithIndexes[i]; - if (!node.data || !node.data.indexes || node.data.indexes.length === 0) continue; - tempVector.copy(node.center).applyMatrix4(modelView); - - const distanceToNode = tempVector.length(); - tempVector.normalize(); - - tempVectorYZ.copy(tempVector).setX(0).normalize(); - tempVectorXZ.copy(tempVector).setY(0).normalize(); - - const cameraAngleXZDot = forward.dot(tempVectorXZ); - const cameraAngleYZDot = forward.dot(tempVectorYZ); - - const ns = nodeSize(node); - const outOfFovY = cameraAngleYZDot < (cosFovYOver2 - .6); - const outOfFovX = cameraAngleXZDot < (cosFovXOver2 - .6); - if (!gatherAllNodes && ((outOfFovX || outOfFovY) && distanceToNode > ns)) { - continue; - } - splatRenderCount += node.data.indexes.length; - nodeRenderList[nodeRenderCount] = node; - node.data.distanceToNode = distanceToNode; - nodeRenderCount++; - } - } - - nodeRenderList.length = nodeRenderCount; - nodeRenderList.sort((a, b) => { - if (a.data.distanceToNode < b.data.distanceToNode) return -1; - else return 1; - }); - - let currentByteOffset = splatRenderCount * Constants.BytesPerInt; - for (let i = 0; i < nodeRenderCount; i++) { - const node = nodeRenderList[i]; - const windowSizeInts = node.data.indexes.length; - const windowSizeBytes = windowSizeInts * Constants.BytesPerInt; - let destView = new Uint32Array(this.sortWorkerIndexesToSort.buffer, - currentByteOffset - windowSizeBytes, windowSizeInts); - destView.set(node.data.indexes); - currentByteOffset -= windowSizeBytes; - } - - return { - 'splatRenderCount': splatRenderCount, - 'shouldSortAll': false - }; - } else { - const totalSplatCount = this.splatMesh.getSplatCount(); - if (!allSplatsSortBuffer || allSplatsSortBuffer.length !== totalSplatCount) { - allSplatsSortBuffer = new Uint32Array(totalSplatCount); - for (let i = 0; i < totalSplatCount; i++) { - allSplatsSortBuffer[i] = i; - } - } - this.sortWorkerIndexesToSort.set(allSplatsSortBuffer); - return { - 'splatRenderCount': totalSplatCount, - 'shouldSortAll': true - }; - } - }; - - }(); - - getSplatMesh() { - return this.splatMesh; - } - - /** - * Get a reference to a splat scene. - * @param {number} sceneIndex The index of the scene to which the reference will be returned - * @return {SplatScene} - */ - getSplatScene(sceneIndex) { - return this.splatMesh.getScene(sceneIndex); - } - - getSceneCount() { - return this.splatMesh.getSceneCount(); - } - - isMobile() { - return navigator.userAgent.includes('Mobi'); - } -} - -/** - * DropInViewer: Wrapper for a Viewer instance that enables it to be added to a Three.js scene like - * any other Three.js scene object (Mesh, Object3D, etc.) - */ -class DropInViewer extends THREE.Group { - - constructor(options = {}) { - super(); - - options.selfDrivenMode = false; - options.useBuiltInControls = false; - options.rootElement = null; - options.dropInMode = true; - options.camera = undefined; - options.renderer = undefined; - - this.viewer = new Viewer(options); - this.splatMesh = null; - this.updateSplatMesh(); - - this.callbackMesh = DropInViewer.createCallbackMesh(); - this.add(this.callbackMesh); - this.callbackMesh.onBeforeRender = DropInViewer.onBeforeRender.bind(this, this.viewer); - - this.viewer.onSplatMeshChanged(() => { - this.updateSplatMesh(); - }); - - } - - updateSplatMesh() { - if (this.splatMesh !== this.viewer.splatMesh) { - if (this.splatMesh) { - this.remove(this.splatMesh); - } - this.splatMesh = this.viewer.splatMesh; - this.add(this.viewer.splatMesh); - } - } - - /** - * Add a single splat scene to the viewer. - * @param {string} path Path to splat scene to be loaded - * @param {object} options { - * - * splatAlphaRemovalThreshold: Ignore any splats with an alpha less than the specified - * value (valid range: 0 - 255), defaults to 1 - * - * showLoadingUI: Display a loading spinner while the scene is loading, defaults to true - * - * position (Array): Position of the scene, acts as an offset from its default position, defaults to [0, 0, 0] - * - * rotation (Array): Rotation of the scene represented as a quaternion, defaults to [0, 0, 0, 1] - * - * scale (Array): Scene's scale, defaults to [1, 1, 1] - * - * onProgress: Function to be called as file data are received - * - * } - * @return {AbortablePromise} - */ - addSplatScene(path, options = {}) { - if (options.showLoadingUI !== false) options.showLoadingUI = true; - return this.viewer.addSplatScene(path, options); - } - - /** - * Add multiple splat scenes to the viewer. - * @param {Array} sceneOptions Array of per-scene options: { - * - * path: Path to splat scene to be loaded - * - * splatAlphaRemovalThreshold: Ignore any splats with an alpha less than the specified - * value (valid range: 0 - 255), defaults to 1 - * - * position (Array): Position of the scene, acts as an offset from its default position, defaults to [0, 0, 0] - * - * rotation (Array): Rotation of the scene represented as a quaternion, defaults to [0, 0, 0, 1] - * - * scale (Array): Scene's scale, defaults to [1, 1, 1] - * } - * @param {boolean} showLoadingUI Display a loading spinner while the scene is loading, defaults to true - * @return {AbortablePromise} - */ - addSplatScenes(sceneOptions, showLoadingUI) { - if (showLoadingUI !== false) showLoadingUI = true; - return this.viewer.addSplatScenes(sceneOptions, showLoadingUI); - } - - /** - * Get a reference to a splat scene. - * @param {number} sceneIndex The index of the scene to which the reference will be returned - * @return {SplatScene} - */ - getSplatScene(sceneIndex) { - return this.viewer.getSplatScene(sceneIndex); - } - - removeSplatScene(index, showLoadingUI = true) { - return this.viewer.removeSplatScene(index, showLoadingUI); - } - - removeSplatScenes(indexes, showLoadingUI = true) { - return this.viewer.removeSplatScenes(indexes, showLoadingUI); - } - - getSceneCount() { - return this.viewer.getSceneCount(); - } - - setActiveSphericalHarmonicsDegrees(activeSphericalHarmonicsDegrees) { - this.viewer.setActiveSphericalHarmonicsDegrees(activeSphericalHarmonicsDegrees); - } - - async dispose() { - return await this.viewer.dispose(); - } - - static onBeforeRender(viewer, renderer, threeScene, camera) { - viewer.update(renderer, camera); - } - - static createCallbackMesh() { - const geometry = new THREE.SphereGeometry(1, 8, 8); - const material = new THREE.MeshBasicMaterial(); - material.colorWrite = false; - material.depthWrite = false; - const mesh = new THREE.Mesh(geometry, material); - mesh.frustumCulled = false; - return mesh; - } - -} - -export { AbortablePromise, DropInViewer, KSplatLoader, Utils as LoaderUtils, LogLevel, OrbitControls, PlayCanvasCompressedPlyParser, PlyLoader, PlyParser, RenderMode, SceneFormat, SceneRevealMode, SplatBuffer, SplatBufferGenerator, SplatLoader, SplatParser, SplatPartitioner, SplatRenderMode, SpzLoader, Viewer, WebXRMode }; -//# sourceMappingURL=gaussian-splats-3d.module.js.map diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 1977ebc..b7d0089 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -44,7 +44,7 @@ import WorkspaceModuleTabContextMenu from './features/workspace/WorkspaceModuleT import ModuleListView from './features/workspace/ModuleListView' import WorkspaceRosConnectionsView from './features/workspace/WorkspaceRosConnectionsView' import WorldEditorModal from './features/workspace/WorldEditorModal' -import WorldMapSelectorModal, { NAV2_MAP_OPTIONS, WorldMapRestartConfirmModal, type SavedMapRuntimeKind, type WorkspaceMapOption } from './features/workspace/WorldMapSelectorModal' +import WorldMapSelectorModal, { NAV2_MAP_OPTIONS, WorldMapRestartConfirmModal, type SavedMapRuntimeKind, type WorkspaceMapOption, type WorkspaceMapScope } from './features/workspace/WorldMapSelectorModal' import WorkspaceQaPanel from './features/workspace/WorkspaceQaPanel' import WorkspaceSettingsView from './features/workspace/WorkspaceSettingsView' import WorkspaceSourceControlView from './features/workspace/WorkspaceSourceControlView' @@ -407,7 +407,7 @@ type WorkspaceVisualizationHost = { viewerUrl: string onStart: () => Promise onTerminate: () => Promise - onDragStart: (event: React.MouseEvent) => void + onDragStart: (event: React.PointerEvent) => void onToggleLarge: () => void onMinimize: () => void onClose: () => void @@ -435,6 +435,7 @@ function LocalPcWorkspaceMockup({ platformEnvironmentConfigOverride, platformOptions, workspaceVisible, + visualizationContainerRef, skipInitialLoading, visualizationOpen, onVisualizationOpenChange, @@ -484,6 +485,7 @@ function LocalPcWorkspaceMockup({ platformEnvironmentConfigOverride?: Record | null platformOptions: PlatformOption[] workspaceVisible: boolean + visualizationContainerRef: React.RefObject skipInitialLoading?: boolean visualizationOpen: boolean onVisualizationOpenChange: WorkspaceVisualizationStateSetter @@ -801,12 +803,16 @@ function LocalPcWorkspaceMockup({ const sessionMapStorageKey = `atnmy:lms:map-session:${studentSlug}:${courseKey}:week-${weekNumber}:selected` const simulationMapStorageKey = `atnmy:lms:map-session:${studentSlug}:${courseKey}:week-${weekNumber}:simulation` const platformMapStorageKey = `atnmy:lms:map-session:${studentSlug}:${courseKey}:week-${weekNumber}:platform` - const [selectedWorldMapKey, setSelectedWorldMapKey] = useState(() => ( - window.localStorage.getItem(sessionMapStorageKey) - ?? window.localStorage.getItem(simulationMapStorageKey) - ?? window.localStorage.getItem(platformMapStorageKey) + const [selectedSimulationMapKey, setSelectedSimulationMapKey] = useState(() => ( + window.localStorage.getItem(simulationMapStorageKey) + ?? window.localStorage.getItem(sessionMapStorageKey) ?? 'nav2-turtlebot3-world' )) + const [selectedPlatformMapKey, setSelectedPlatformMapKey] = useState(() => ( + window.localStorage.getItem(platformMapStorageKey) + ?? window.localStorage.getItem(sessionMapStorageKey) + ?? 'nav2-platform-slam-2d' + )) const [savedSlamMaps, setSavedSlamMaps] = useState([]) const [savedSlamMapsLoaded, setSavedSlamMapsLoaded] = useState(false) const [worldLayoutVersion, setWorldLayoutVersion] = useState(0) @@ -814,14 +820,14 @@ function LocalPcWorkspaceMockup({ const configuredMaps = (curriculumWeek.maps ?? []) .filter((map): map is WorkspaceMapOption => map.platformKey === 'NAV2') .map((map) => { - if (map.key === 'nav2-turtlebot3-world') return { ...map, description: '기본 장애물 환경' } - if (map.key === 'nav2-turtlebot3-house') return { ...map, description: '방과 복도로 구성된 실내 환경' } - return map + if (map.key === 'nav2-turtlebot3-world') return { ...map, scope: 'simulation' as const, description: '기본 장애물 환경' } + if (map.key === 'nav2-turtlebot3-house') return { ...map, scope: 'simulation' as const, description: '방과 복도로 구성된 실내 환경' } + return { ...map, scope: 'simulation' as const } }) const customMap = NAV2_MAP_OPTIONS.find((map) => map.key === 'nav2-custom') const baseMaps = configuredMaps.some((map) => map.key === 'nav2-custom') || !customMap ? [...configuredMaps] - : [...configuredMaps, customMap] + : [...configuredMaps, { ...customMap, scope: 'simulation' as const }] const platformSlamMaps: WorkspaceMapOption[] = [ { key: 'nav2-platform-slam-2d', @@ -832,6 +838,7 @@ function LocalPcWorkspaceMockup({ viewerModel: 'turtlebot3_world', remoteSlam: true, runtimeKind: 'PLATFORM_2D', + scope: 'platform', }, { key: 'nav2-platform-slam-3d', @@ -842,9 +849,26 @@ function LocalPcWorkspaceMockup({ viewerModel: 'turtlebot3_world', remoteSlam: true, runtimeKind: 'PLATFORM_3D', + scope: 'platform', }, ] - const maps = [...baseMaps, ...platformSlamMaps, ...savedSlamMaps] + const persistedMaps = savedSlamMaps.map((map) => ({ + ...map, + scope: map.runtimeKind === 'SIMULATION' ? 'simulation' as const : 'platform' as const, + })) + const simulationCopiesOfPlatformMaps = savedSlamMaps + .filter((map) => map.runtimeKind === 'PLATFORM_2D' || map.runtimeKind === 'PLATFORM_3D') + .map((map) => ({ + ...map, + key: `${map.key}:simulation`, + description: map.runtimeKind === 'PLATFORM_3D' + ? '저장된 3D SLAM 결과를 복셀 Gazebo 환경에서 사용합니다.' + : '저장된 2D SLAM 결과 맵을 Gazebo 환경에서 사용합니다.', + worldFile: 'empty_world.world', + scope: 'simulation' as const, + gazeboWorldFromMap: true, + })) + const maps = [...baseMaps, ...platformSlamMaps, ...persistedMaps, ...simulationCopiesOfPlatformMaps] const keys = new Set() return maps.filter((map) => { if (keys.has(map.key)) return false @@ -852,48 +876,54 @@ function LocalPcWorkspaceMockup({ return true }) }, [curriculumWeek.maps, savedSlamMaps]) + const simulationWorldMaps = useMemo(() => ( + sessionWorldMaps.filter((map) => map.scope === 'simulation') + ), [sessionWorldMaps]) + const platformWorldMaps = useMemo(() => ( + sessionWorldMaps.filter((map) => map.scope === 'platform') + ), [sessionWorldMaps]) useEffect(() => { - const storedMapKey = window.localStorage.getItem(sessionMapStorageKey) - ?? window.localStorage.getItem(simulationMapStorageKey) - ?? window.localStorage.getItem(platformMapStorageKey) - const normalizedStoredMapKey = storedMapKey === 'nav2-platform-slam' - ? 'nav2-platform-slam-2d' - : storedMapKey - const requestedMapKey = selectedWorldMapKey === 'nav2-platform-slam' - ? 'nav2-platform-slam-2d' - : selectedWorldMapKey || normalizedStoredMapKey - if (!savedSlamMapsLoaded && requestedMapKey?.startsWith('saved-slam-')) return - setSelectedWorldMapKey((current) => { - const next = sessionWorldMaps.some((map) => map.key === current) - ? current - : sessionWorldMaps.some((map) => map.key === normalizedStoredMapKey) - ? normalizedStoredMapKey! - : sessionWorldMaps[0]?.key ?? 'nav2-turtlebot3-world' - window.localStorage.setItem(sessionMapStorageKey, next) + const legacyMapKey = window.localStorage.getItem(sessionMapStorageKey) + const normalizeMapKey = (key: string | null, scope: WorkspaceMapScope): string | null => { + if (key === 'nav2-platform-slam') return scope === 'platform' ? 'nav2-platform-slam-2d' : null + return key + } + const resolveSelection = ( + scope: WorkspaceMapScope, + current: string, + storageKey: string, + maps: WorkspaceMapOption[], + fallback: string, + ): string => { + const stored = normalizeMapKey(window.localStorage.getItem(storageKey) ?? legacyMapKey, scope) + const requested = normalizeMapKey(current, scope) ?? stored + if (!savedSlamMapsLoaded && requested?.startsWith('saved-slam-')) return current + const next = maps.some((map) => map.key === requested) + ? requested! + : maps.some((map) => map.key === stored) + ? stored! + : maps.find((map) => map.key === fallback)?.key ?? maps[0]?.key ?? fallback + window.localStorage.setItem(storageKey, next) return next - }) - }, [platformMapStorageKey, savedSlamMapsLoaded, selectedWorldMapKey, sessionMapStorageKey, sessionWorldMaps, simulationMapStorageKey]) - const selectedWorldMap = sessionWorldMaps.find((map) => map.key === selectedWorldMapKey) ?? sessionWorldMaps[0] + } + setSelectedSimulationMapKey((current) => resolveSelection( + 'simulation', current, simulationMapStorageKey, simulationWorldMaps, 'nav2-turtlebot3-world', + )) + setSelectedPlatformMapKey((current) => resolveSelection( + 'platform', current, platformMapStorageKey, platformWorldMaps, 'nav2-platform-slam-2d', + )) + }, [platformMapStorageKey, platformWorldMaps, savedSlamMapsLoaded, sessionMapStorageKey, simulationMapStorageKey, simulationWorldMaps]) + const selectedWorldMapKey = remoteWorkspace ? selectedPlatformMapKey : selectedSimulationMapKey + const selectedWorldMap = sessionWorldMaps.find((map) => map.key === selectedWorldMapKey) + ?? (remoteWorkspace ? platformWorldMaps[0] : simulationWorldMaps[0]) const prepareVisualizationSavedMapRef = useRef<( () => Promise<{ savedMapDatabasePath?: string }> ) | null>(null) - const isPlatformWorldMap = selectedWorldMap?.runtimeKind === 'PLATFORM_2D' - || selectedWorldMap?.runtimeKind === 'PLATFORM_3D' - || selectedWorldMap?.remoteSlam === true - const simulationWorldMaps = useMemo(() => ( - sessionWorldMaps.filter((map) => ( - map.runtimeKind !== 'PLATFORM_2D' - && map.runtimeKind !== 'PLATFORM_3D' - && !map.remoteSlam - )) - ), [sessionWorldMaps]) - const platformWorldMaps = useMemo(() => ( - sessionWorldMaps.filter((map) => ( - map.runtimeKind === 'PLATFORM_2D' - || map.runtimeKind === 'PLATFORM_3D' - || map.remoteSlam - )) - ), [sessionWorldMaps]) + const savedMapRunsInSimulation = !remoteWorkspace + && selectedWorldMap?.saved === true + && selectedWorldMap.gazeboWorldFromMap === true + const saved3dMapUses2dSimulationNavigation = savedMapRunsInSimulation + && selectedWorldMap?.runtimeKind === 'PLATFORM_3D' const visualizationProfiles = useMemo(() => ( [ ...(curriculumWeek.environmentProfiles ?? []), @@ -1040,18 +1070,18 @@ function LocalPcWorkspaceMockup({ bridgeLocation: visualizationBridgeLocation, bridgeWorkspaceRoot: remoteWorkspace ? localVisualizationWorkspaceRoot : clonePathPreview, sessionKey: `${studentSlug}:${courseKey}:week-${weekNumber}`, - mapKey: selectedWorldMap.saved ? selectedWorldMap.sourceMapKey : selectedWorldMap.key, + mapKey: selectedWorldMap.gazeboWorldFromMap + ? 'nav2-saved-gazebo-map' + : selectedWorldMap.saved + ? selectedWorldMap.sourceMapKey + : selectedWorldMap.key, platform: visualizationPlatform, threeDimensional: Boolean( selectedWorldMap.runtimeKind === 'PLATFORM_3D' ), savedMapDatabasePath: selectedWorldMap.databasePath, prepareSavedMap: async () => prepareVisualizationSavedMapRef.current?.() ?? {}, - visualizationRuntime: selectedWorldMap.runtimeKind === 'PLATFORM_2D' - || selectedWorldMap.runtimeKind === 'PLATFORM_3D' - || selectedWorldMap.remoteSlam - ? 'real' - : 'simulation', + visualizationRuntime: remoteWorkspace ? 'real' : 'simulation', worldLayoutVersion, onTitlebarNotice }) @@ -1178,7 +1208,7 @@ function LocalPcWorkspaceMockup({ databasePath, databaseUrl: map.databaseUrl ?? undefined, runtimeKind: map.runtimeKind - ?? (map.sourceMapKey === 'nav2-platform-slam' ? 'PLATFORM_2D' : 'SIMULATION'), + ?? (/^nav2-platform-slam(?:-2d)?$/i.test(map.sourceMapKey) ? 'PLATFORM_2D' : 'SIMULATION'), } })) } catch (error) { @@ -2001,7 +2031,7 @@ function LocalPcWorkspaceMockup({ } } const getPreviewContainerSize = (): { width: number; height: number } => { - const rect = workspaceRootRef.current?.getBoundingClientRect() + const rect = visualizationContainerRef.current?.getBoundingClientRect() return { width: rect?.width ?? window.innerWidth, height: rect?.height ?? window.innerHeight @@ -2105,24 +2135,39 @@ function LocalPcWorkspaceMockup({ window.addEventListener('mousemove', onMove) window.addEventListener('mouseup', onUp) } - const startPreviewDrag = (event: React.MouseEvent): void => { - if (previewLarge) return + const startPreviewDrag = (event: React.PointerEvent): void => { + if (previewLarge || event.button !== 0) return event.preventDefault() + const dragTarget = event.currentTarget + const pointerId = event.pointerId const startX = event.clientX const startY = event.clientY const startPosition = previewPosition - const onMove = (moveEvent: MouseEvent): void => { + const cleanup = (): void => { + dragTarget.removeEventListener('pointermove', onMove) + dragTarget.removeEventListener('pointerup', finishDrag) + dragTarget.removeEventListener('pointercancel', finishDrag) + dragTarget.removeEventListener('lostpointercapture', finishDrag) + } + const onMove = (moveEvent: PointerEvent): void => { + if (moveEvent.pointerId !== pointerId) return setPreviewPosition(clampPreviewPosition({ x: Math.max(12, startPosition.x + moveEvent.clientX - startX), y: Math.max(48, startPosition.y + moveEvent.clientY - startY) })) } - const onUp = (): void => { - window.removeEventListener('mousemove', onMove) - window.removeEventListener('mouseup', onUp) + const finishDrag = (finishEvent: PointerEvent): void => { + if (finishEvent.pointerId !== pointerId) return + cleanup() + if (dragTarget.hasPointerCapture(pointerId)) { + dragTarget.releasePointerCapture(pointerId) + } } - window.addEventListener('mousemove', onMove) - window.addEventListener('mouseup', onUp) + dragTarget.setPointerCapture(pointerId) + dragTarget.addEventListener('pointermove', onMove) + dragTarget.addEventListener('pointerup', finishDrag) + dragTarget.addEventListener('pointercancel', finishDrag) + dragTarget.addEventListener('lostpointercapture', finishDrag) } const startPreviewResize = (event: React.MouseEvent, direction: VisualizationResizeDirection): void => { if (previewLarge) return @@ -2464,8 +2509,9 @@ function LocalPcWorkspaceMockup({ { name: 'pgm' as const, fileName: pgmFileName, url: selectedWorldMap.pgmUrl }, ] let databasePath: string | undefined + let databaseFileName: string | undefined if (selectedWorldMap.databasePath || selectedWorldMap.databaseUrl) { - const databaseFileName = selectedWorldMap.databasePath ? fileName(selectedWorldMap.databasePath) : '' + databaseFileName = selectedWorldMap.databasePath ? fileName(selectedWorldMap.databasePath) : '' if (!databaseFileName.endsWith('.db') || !selectedWorldMap.databaseUrl) { throw new Error('저장된 3D SLAM 데이터가 완전하지 않습니다.') } @@ -2483,12 +2529,29 @@ function LocalPcWorkspaceMockup({ artifacts, ) } + if (!remoteWorkspace && selectedWorldMap.gazeboWorldFromMap) { + if (selectedWorldMap.runtimeKind === 'PLATFORM_3D') { + if (!databaseFileName) throw new Error('시뮬레이션할 3D SLAM 데이터베이스가 없습니다.') + await window.localWorkspace.maps.generatePointCloudCollisionLayout(clonePathPreview, databaseFileName) + } else { + await window.localWorkspace.maps.generateCollisionLayout(clonePathPreview, yamlFileName, pgmFileName) + } + } return { savedMapYamlPath: `/workspace/maps/${yamlFileName}`, savedMapDatabasePath: databasePath, } }, [clonePathPreview, getRemoteWorkspaceTarget, localVisualizationWorkspaceRoot, remoteWorkspace, selectedWorldMap, visualizationBridgeLocation]) prepareVisualizationSavedMapRef.current = prepareSavedMap + const prepareModuleSavedMap = useCallback(async () => { + const preparedMap = await prepareSavedMap() + // The saved 3D database is retained for visualization and Gazebo-world + // generation. Local simulation itself localizes against the exported 2D + // occupancy map using the ordinary /scan pipeline. + return saved3dMapUses2dSimulationNavigation + ? { ...preparedMap, savedMapDatabasePath: undefined } + : preparedMap + }, [prepareSavedMap, saved3dMapUses2dSimulationNavigation]) const vehicleRuntimeProfiles = useMemo(() => { const profiles = new Map>() platformOptions.forEach((platform) => { @@ -2522,10 +2585,12 @@ function LocalPcWorkspaceMockup({ workspaceActive: workspaceVisible, sourceSettings: runtimeSourceSettings, rosNetwork: moduleRosNetworkConfig, - simulatorWorldFile: selectedWorldMap.worldFile, + simulatorWorldFile: savedMapRunsInSimulation ? 'empty_world.world' : selectedWorldMap.worldFile, savedMapYamlPath: selectedWorldMap.saved ? selectedWorldMap.yamlPath : null, - savedMapDatabasePath: selectedWorldMap.saved ? selectedWorldMap.databasePath : null, - prepareSavedMap, + savedMapDatabasePath: saved3dMapUses2dSimulationNavigation + ? null + : selectedWorldMap.saved ? selectedWorldMap.databasePath : null, + prepareSavedMap: prepareModuleSavedMap, bulkExcludedModuleKeys: parkedModuleKeys, activeRuntimeImages: moduleRuntimeImages, vehicleRuntimeProfiles, @@ -2548,12 +2613,23 @@ function LocalPcWorkspaceMockup({ return () => onRegisterRunningModuleNames?.(null) }, [availableModules, onRegisterRunningModuleNames, onRunningModuleCountChange]) const applyWorldMapSelection = async (map: WorkspaceMapOption): Promise => { - window.localStorage.setItem(sessionMapStorageKey, map.key) - setSelectedWorldMapKey(map.key) - if (map.custom && map.layout && !remoteWorkspace) { + const scope = map.scope ?? (map.runtimeKind === 'SIMULATION' ? 'simulation' : 'platform') + const storageKey = scope === 'platform' ? platformMapStorageKey : simulationMapStorageKey + const appliesToActiveWorkspace = (scope === 'platform') === remoteWorkspace + window.localStorage.setItem(storageKey, map.key) + if (scope === 'platform') { + setSelectedPlatformMapKey(map.key) + } else { + setSelectedSimulationMapKey(map.key) + } + if (map.custom && map.layout && scope === 'simulation' && !remoteWorkspace) { await window.localWorkspace.worldLayout.write(clonePathPreview, map.layout as WorldLayout) setWorldLayoutVersion((version) => version + 1) } + if (!appliesToActiveWorkspace) { + onTitlebarNotice(`[${map.name}] 맵이 ${scope === 'platform' ? '플랫폼' : '시뮬레이션'} 환경에 선택되었습니다.`, 'info') + return + } if (map.saved) { setNav2VisualizationMode('navigation') setVisualizationStack(map.runtimeKind === 'PLATFORM_3D' ? 'slam3d' : 'nav2') @@ -2695,7 +2771,16 @@ function LocalPcWorkspaceMockup({ if (restoredActiveModule) { setModuleWorkspaceSnapshots(restored.moduleSnapshots) restoreModuleSnapshot(restoredActiveModule, restored.moduleSnapshots[restoredActiveModule.name]) - setActiveIdeActivity(['search', 'logs', 'git', 'ros-connections'].includes(restored.activeIdeActivity) ? restored.activeIdeActivity : 'workspace') + } + + if (restored.activeIdeActivity === 'examples') { + openExampleCode() + } else if (['modules', 'ros-connections', 'settings'].includes(restored.activeIdeActivity)) { + setActiveIdeActivity(restored.activeIdeActivity) + } else if (restoredActiveModule) { + setActiveIdeActivity(['workspace', 'search', 'logs', 'git'].includes(restored.activeIdeActivity) ? restored.activeIdeActivity : 'workspace') + } else { + setActiveIdeActivity('modules') } restoredWorkspaceUiRef.current = null @@ -3581,7 +3666,6 @@ function LocalPcWorkspaceMockup({ dark={dark} modules={availableModules} remoteWorkspace={remoteWorkspace} - platformName={platformName} rosDomainId={effectiveRosDomainId} onRefreshModules={() => loadAvailableModules({ silent: true })} loadRosGraph={() => ( @@ -3834,10 +3918,10 @@ function LocalPcWorkspaceMockup({ {worldMapSelectorOpen && ( setWorldMapSelectorOpen(false)} onCreate={() => { if (remoteWorkspace) { @@ -3849,8 +3933,11 @@ function LocalPcWorkspaceMockup({ onDelete={(map) => void deleteSavedSlamMap(map)} onSelect={(map) => { setWorldMapSelectorOpen(false) - if (map.key === selectedWorldMapKey) return - if (availableModules.some((module) => module.state === 'running')) { + const scope = map.scope ?? (map.runtimeKind === 'SIMULATION' ? 'simulation' : 'platform') + const currentMapKey = scope === 'platform' ? selectedPlatformMapKey : selectedSimulationMapKey + const appliesToActiveWorkspace = (scope === 'platform') === remoteWorkspace + if (map.key === currentMapKey) return + if (appliesToActiveWorkspace && availableModules.some((module) => module.state === 'running')) { void applyWorldMapSelection(map) setPendingWorldMapRestart(map) return @@ -3948,6 +4035,7 @@ function LocalPcWorkspaceMockup({ } export default function App(): React.JSX.Element { + const appRootRef = useRef(null) const restoredStudioSessionRef = useRef(readStudioRuntimeSessionState()) const previousWeekStateRef = useRef>({}) const curriculumLoadRequestRef = useRef(0) @@ -5606,7 +5694,7 @@ export default function App(): React.JSX.Element { }, [bootLoadingMessage]) return ( -
+
Promise> sync: (workspaceRoot: string | undefined, artifacts: SavedMapArtifactDownload[]) => Promise<{ ok: boolean }> + generateCollisionLayout: (workspaceRoot: string | undefined, yamlPath: string, pgmPath: string) => Promise + generatePointCloudCollisionLayout: (workspaceRoot: string | undefined, databasePath: string) => Promise } visualization: Window['visualization'] git: Window['git'] diff --git a/src/renderer/src/features/workspace/ModuleListView.tsx b/src/renderer/src/features/workspace/ModuleListView.tsx index 56bee54..ab94400 100644 --- a/src/renderer/src/features/workspace/ModuleListView.tsx +++ b/src/renderer/src/features/workspace/ModuleListView.tsx @@ -1,4 +1,4 @@ -import { X } from 'lucide-react' +import { ChevronLeft, ChevronRight, X } from 'lucide-react' import { useEffect, useState } from 'react' import type React from 'react' import ModuleDeleteConfirm from './ModuleDeleteConfirm' @@ -11,6 +11,8 @@ import { createParameterId, parseModuleParameters } from './moduleParameters' import type { ModuleParameterPair, ModuleRow } from './types' import { WorkspaceMapPreview, type WorkspaceMapOption } from './WorldMapSelectorModal' +const MODULE_GUIDE_DISMISSED_STORAGE_KEY = 'autonomy-studio.module-guide.dismissed' + type ModuleListViewProps = { dark: boolean modules: ModuleRow[] @@ -84,6 +86,7 @@ export default function ModuleListView({ const [deleteConfirmModule, setDeleteConfirmModule] = useState(null) const [parameterDialog, setParameterDialog] = useState<{ module: ModuleRow; parameters: ModuleParameterPair[] } | null>(null) const [moduleAddDialogOpen, setModuleAddDialogOpen] = useState(false) + const [moduleGuideOpen, setModuleGuideOpen] = useState(false) const [draggedModule, setDraggedModule] = useState(null) const [dropTargetModuleName, setDropTargetModuleName] = useState(null) const [dropZone, setDropZone] = useState<'active' | 'parking' | null>(null) @@ -97,6 +100,14 @@ export default function ModuleListView({ const installingAll = installingModuleName === '__all__' const bulkControlling = installingModuleName === '__start_all__' || installingModuleName === '__stop_all__' + useEffect(() => { + try { + setModuleGuideOpen(window.localStorage.getItem(MODULE_GUIDE_DISMISSED_STORAGE_KEY) !== 'true') + } catch { + setModuleGuideOpen(true) + } + }, []) + useEffect(() => { if (!parkingHeightStorageKey) { setParkingHeight(232) @@ -200,6 +211,7 @@ export default function ModuleListView({ if (moduleAddDialogOpen) { event.preventDefault() setModuleAddDialogOpen(false) + return } } @@ -278,15 +290,24 @@ export default function ModuleListView({ ) : filteredModuleRows.length === 0 ? ( ) : filteredModuleRows.map((row) => renderModuleRow(row, false))} - {!loading && allowCustomModuleCreation && ( -
+ {!loading && ( +
+ {allowCustomModuleCreation && ( + + )}
)}
@@ -362,10 +383,172 @@ export default function ModuleListView({ }} /> )} + {moduleGuideOpen && ( + { + try { + window.localStorage.setItem(MODULE_GUIDE_DISMISSED_STORAGE_KEY, 'true') + } catch { + // Keep the guide closed for the current session when storage is unavailable. + } + setModuleGuideOpen(false) + }} + /> + )} ) } +const moduleGuidePages = [ + { + environment: '로컬 환경', + imageAlt: '로컬 환경 모듈 가이드', + imageSrc: '/assets/images/module-guide-local.png', + slamModules: ['NAV2-Gazebo', 'NAV2-TurtleBot3Burger', 'NAV2-Perception', 'NAV2-SLAM'], + navigationModules: ['NAV2-Gazebo', 'NAV2-TurtleBot3Burger', 'NAV2-Perception', 'NAV2-Localization', 'NAV2-Planning', 'NAV2-Control'], + parameters: [ + ['기본', 'sim_mode:=true, platform:=turtlebot3, robot:=turtlebot3, level:=2d'], + ['Perception', 'sim_mode:=true'], + ['Localization', 'localization_mode:=amcl'] + ] + }, + { + environment: '원격 플랫폼', + imageAlt: '원격 플랫폼 환경 모듈 가이드', + imageSrc: '/assets/images/module-guide-remote-platform.png', + slamModules: ['NAV2-MKMini', 'NAV2-Perception', 'NAV2-SLAM'], + navigationModules: ['NAV2-MKMini', 'NAV2-Perception', 'NAV2-Localization', 'NAV2-Planning', 'NAV2-Control'], + parameters: [ + ['기본', 'sim_mode:=false, platform:=mkmini, robot:=mkmini, level:=2d'], + ['Perception', 'sim_mode:=false'], + ['Localization', 'localization_mode:=amcl'] + ] + }, + { + environment: '원격 플랫폼 3D', + imageAlt: '원격 3D SLAM 환경 모듈 가이드', + imageSrc: '/assets/images/module-guide-remote-3d.png', + slamModules: ['NAV2-MKMini', 'NAV2-Perception3D', 'NAV2-SLAM3D'], + navigationModules: ['NAV2-MKMini', 'NAV2-Perception3D', 'NAV2-Localization', 'NAV2-Planning', 'NAV2-Control'], + parameters: [ + ['기본', 'sim_mode:=false, platform:=mkmini, robot:=mkmini, level:=3d'] + ] + } +] as const + +function ModuleGuideDialog({ + dark, + onDismiss +}: { + dark: boolean + onDismiss: () => void +}): React.JSX.Element { + const [pageIndex, setPageIndex] = useState(0) + const page = moduleGuidePages[pageIndex] + const atFirstPage = pageIndex === 0 + const atLastPage = pageIndex === moduleGuidePages.length - 1 + + return ( +
+
+ + +
+ {page.imageSrc && {page.imageAlt}} +
+ +
+
+

{page.environment}

+
+
+
+

SLAM

+
    + {page.slamModules.map((module) =>
  • {module}
  • )} +
+
+
+

자율주행

+
    + {page.navigationModules.map((module) =>
  • {module}
  • )} +
+
+
+

파라미터

+
+ {page.parameters.map(([label, value]) => ( +
+ {label} + {value} +
+ ))} +
+
+
+
+ +
+ +
+ {moduleGuidePages.map((guidePage, index) => ( +
+ +
+
+
+ ) +} + export type CustomModuleInput = { name: string category: string diff --git a/src/renderer/src/features/workspace/WorkspaceRosConnectionsView.tsx b/src/renderer/src/features/workspace/WorkspaceRosConnectionsView.tsx index a1f9128..996ca65 100644 --- a/src/renderer/src/features/workspace/WorkspaceRosConnectionsView.tsx +++ b/src/renderer/src/features/workspace/WorkspaceRosConnectionsView.tsx @@ -12,7 +12,6 @@ type RosModule = { module: ModuleRow publishers: TopicRole[] subscribers: TopicRole[] - runtime: 'local' | 'remote' } type RosConnection = { @@ -41,7 +40,6 @@ type WorkspaceRosConnectionsViewProps = { dark: boolean modules: ModuleRow[] remoteWorkspace: boolean - platformName: string rosDomainId: string onRefreshModules: () => Promise loadRosGraph: () => Promise @@ -116,12 +114,6 @@ function moduleTopics(module: ModuleRow): Pick(() => modules .filter((module) => module.state === 'running') - .map((module) => ({ module, runtime: resolveRuntime(module, remoteWorkspace), ...moduleTopics(module) })), [modules, remoteWorkspace]) + .map((module) => ({ module, ...moduleTopics(module) })), [modules]) const [selectedModuleKeys, setSelectedModuleKeys] = useState>({}) const [rosGraph, setRosGraph] = useState({ topics: [] }) const [rosGraphLoading, setRosGraphLoading] = useState(false) @@ -406,22 +397,16 @@ export default function WorkspaceRosConnectionsView({ } }, [selectedConnection, selectedTopicName]) - const localModules = runningRosModules.filter((module) => module.runtime === 'local') - const remoteModules = runningRosModules.filter((module) => module.runtime === 'remote') - const primaryRuntime = remoteWorkspace ? 'remote' : 'local' - const primaryModules = primaryRuntime === 'remote' ? remoteModules : localModules - const secondaryModules = primaryRuntime === 'remote' ? localModules : remoteModules const runningModules = runningRosModules.length const activeConnections = connections.filter((connection) => connection.active).length const toggleModuleConnection = (module: ModuleRow): void => { const key = module.folderName ?? module.name setSelectedModuleKeys((current) => ({ ...current, [key]: current[key] === false })) } - const setRuntimeModuleSelection = (runtime: RosModule['runtime'], selected: boolean): void => { - const runtimeModules = runtime === 'local' ? localModules : remoteModules + const setWorkspaceModuleSelection = (selected: boolean): void => { setSelectedModuleKeys((current) => { const next = { ...current } - runtimeModules.forEach(({ module }) => { + runningRosModules.forEach(({ module }) => { next[module.folderName ?? module.name] = selected }) return next @@ -499,15 +484,15 @@ export default function WorkspaceRosConnectionsView({ style={{ gridTemplateRows: topicDetailOpen && selectedConnection ? `minmax(0, 1fr) ${topicDetailHeight}px` : 'minmax(0, 1fr)' }} >
-
+
setRuntimeModuleSelection(primaryRuntime, true)} - onClearSelection={() => setRuntimeModuleSelection(primaryRuntime, false)} + onSelectAll={() => setWorkspaceModuleSelection(true)} + onClearSelection={() => setWorkspaceModuleSelection(false)} />
@@ -562,15 +547,6 @@ export default function WorkspaceRosConnectionsView({
- setRuntimeModuleSelection(primaryRuntime === 'remote' ? 'local' : 'remote', true)} - onClearSelection={() => setRuntimeModuleSelection(primaryRuntime === 'remote' ? 'local' : 'remote', false)} - />
diff --git a/src/renderer/src/features/workspace/WorkspaceSettingsView.tsx b/src/renderer/src/features/workspace/WorkspaceSettingsView.tsx index 144822b..0424b9c 100644 --- a/src/renderer/src/features/workspace/WorkspaceSettingsView.tsx +++ b/src/renderer/src/features/workspace/WorkspaceSettingsView.tsx @@ -1,5 +1,6 @@ import type { StudioSourceSettings } from '../platforms/types' import type { PlatformOption } from '../platforms/types' +import { useEffect, useRef, useState } from 'react' import { StudioSettingsField, StudioSettingsSwitch } from './StudioSettingsControls' type Props = { @@ -29,6 +30,25 @@ function WorkspaceSettingsView({ onUpdateSourceSetting, onSaveSourceSettings }: Props): React.JSX.Element { + const [saveAcknowledged, setSaveAcknowledged] = useState(false) + const saveAcknowledgedTimerRef = useRef(null) + + useEffect(() => () => { + if (saveAcknowledgedTimerRef.current !== null) { + window.clearTimeout(saveAcknowledgedTimerRef.current) + } + }, []) + + const handleSave = (): void => { + if (saveDisabled || saveAcknowledged) return + setSaveAcknowledged(true) + onSaveSourceSettings() + saveAcknowledgedTimerRef.current = window.setTimeout(() => { + setSaveAcknowledged(false) + saveAcknowledgedTimerRef.current = null + }, 1000) + } + return (
onUpdateSourceSetting('gpuEnabled', checked)} dark={dark} /> + {sourceSettingsDraft.gpuEnabled ? ( +
+ NVIDIA Container Toolkit과 Docker GPU 런타임이 준비된 PC에서만 실행됩니다. 준비되지 않은 경우 GPU 가속을 끄세요. +
+ ) : null}
@@ -175,11 +206,11 @@ function WorkspaceSettingsView({
diff --git a/src/renderer/src/features/workspace/WorkspaceVisualizationPanel.tsx b/src/renderer/src/features/workspace/WorkspaceVisualizationPanel.tsx index 2e869fe..a8d45ba 100644 --- a/src/renderer/src/features/workspace/WorkspaceVisualizationPanel.tsx +++ b/src/renderer/src/features/workspace/WorkspaceVisualizationPanel.tsx @@ -17,7 +17,7 @@ type WorkspaceVisualizationPanelProps = { running: boolean busy: boolean viewerUrl: string - onDragStart: (event: React.MouseEvent) => void + onDragStart: (event: React.PointerEvent) => void onToggleLarge: () => void onMinimize: () => void onClose: () => void @@ -78,9 +78,9 @@ export default function WorkspaceVisualizationPanel({ }} >
event.stopPropagation()} onMouseDown={(event) => event.stopPropagation()} onClick={onMinimize} className={[iconButtonClass, 'group relative'].join(' ')} @@ -105,6 +106,7 @@ export default function WorkspaceVisualizationPanel({
-
+
{maps.map((map) => { const pending = map.key === pendingMap?.key const selected = map.key === selectedMapKey diff --git a/src/renderer/src/features/workspace/useWorkspaceVisualization.ts b/src/renderer/src/features/workspace/useWorkspaceVisualization.ts index 57e851a..57c5326 100644 --- a/src/renderer/src/features/workspace/useWorkspaceVisualization.ts +++ b/src/renderer/src/features/workspace/useWorkspaceVisualization.ts @@ -244,11 +244,12 @@ export function useWorkspaceVisualization({ if (visualizationBusy) return if (mode !== nav2VisualizationMode) setNav2VisualizationMode(mode) setVisualizationBusy(true) - setVisualizationStatusMessage('시작 중') + setVisualizationStatusMessage(prepareSavedMap ? '맵 환경 준비 중' : '시작 중') try { // Download the selected map artifacts before starting the bridge. The // bridge only receives workspace-local paths, never an R2 URL. const preparedMap = await prepareSavedMap?.() + setVisualizationStatusMessage('시작 중') const bridgeSavedMapDatabasePath = preparedMap?.savedMapDatabasePath ?? savedMapDatabasePath const status = bridgeRunsOnPlatform ? bridgeTarget?.host diff --git a/tsconfig.node.tsbuildinfo b/tsconfig.node.tsbuildinfo index b4223b1..7177252 100644 --- a/tsconfig.node.tsbuildinfo +++ b/tsconfig.node.tsbuildinfo @@ -1 +1 @@ -{"fileNames":["./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/typescript/lib/lib.es2023.d.ts","./node_modules/typescript/lib/lib.es2024.d.ts","./node_modules/typescript/lib/lib.esnext.d.ts","./node_modules/typescript/lib/lib.dom.d.ts","./node_modules/typescript/lib/lib.dom.iterable.d.ts","./node_modules/typescript/lib/lib.dom.asynciterable.d.ts","./node_modules/typescript/lib/lib.webworker.importscripts.d.ts","./node_modules/typescript/lib/lib.scripthost.d.ts","./node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/typescript/lib/lib.es2022.object.d.ts","./node_modules/typescript/lib/lib.es2022.string.d.ts","./node_modules/typescript/lib/lib.es2022.regexp.d.ts","./node_modules/typescript/lib/lib.es2023.array.d.ts","./node_modules/typescript/lib/lib.es2023.collection.d.ts","./node_modules/typescript/lib/lib.es2023.intl.d.ts","./node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2024.collection.d.ts","./node_modules/typescript/lib/lib.es2024.object.d.ts","./node_modules/typescript/lib/lib.es2024.promise.d.ts","./node_modules/typescript/lib/lib.es2024.regexp.d.ts","./node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2024.string.d.ts","./node_modules/typescript/lib/lib.esnext.array.d.ts","./node_modules/typescript/lib/lib.esnext.collection.d.ts","./node_modules/typescript/lib/lib.esnext.intl.d.ts","./node_modules/typescript/lib/lib.esnext.disposable.d.ts","./node_modules/typescript/lib/lib.esnext.promise.d.ts","./node_modules/typescript/lib/lib.esnext.decorators.d.ts","./node_modules/typescript/lib/lib.esnext.iterator.d.ts","./node_modules/typescript/lib/lib.esnext.float16.d.ts","./node_modules/typescript/lib/lib.esnext.error.d.ts","./node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/typescript/lib/lib.esnext.full.d.ts","./node_modules/@types/node/compatibility/iterators.d.ts","./node_modules/@types/node/globals.typedarray.d.ts","./node_modules/@types/node/buffer.buffer.d.ts","./node_modules/@types/node/globals.d.ts","./node_modules/@types/node/web-globals/abortcontroller.d.ts","./node_modules/@types/node/web-globals/blob.d.ts","./node_modules/@types/node/web-globals/console.d.ts","./node_modules/@types/node/web-globals/crypto.d.ts","./node_modules/@types/node/web-globals/domexception.d.ts","./node_modules/@types/node/web-globals/encoding.d.ts","./node_modules/@types/node/web-globals/events.d.ts","./node_modules/undici-types/utility.d.ts","./node_modules/undici-types/header.d.ts","./node_modules/undici-types/readable.d.ts","./node_modules/undici-types/fetch.d.ts","./node_modules/undici-types/formdata.d.ts","./node_modules/undici-types/connector.d.ts","./node_modules/undici-types/client-stats.d.ts","./node_modules/undici-types/client.d.ts","./node_modules/undici-types/errors.d.ts","./node_modules/undici-types/dispatcher.d.ts","./node_modules/undici-types/global-dispatcher.d.ts","./node_modules/undici-types/global-origin.d.ts","./node_modules/undici-types/pool-stats.d.ts","./node_modules/undici-types/pool.d.ts","./node_modules/undici-types/handlers.d.ts","./node_modules/undici-types/balanced-pool.d.ts","./node_modules/undici-types/round-robin-pool.d.ts","./node_modules/undici-types/h2c-client.d.ts","./node_modules/undici-types/agent.d.ts","./node_modules/undici-types/mock-interceptor.d.ts","./node_modules/undici-types/mock-call-history.d.ts","./node_modules/undici-types/mock-agent.d.ts","./node_modules/undici-types/mock-client.d.ts","./node_modules/undici-types/mock-pool.d.ts","./node_modules/undici-types/snapshot-agent.d.ts","./node_modules/undici-types/mock-errors.d.ts","./node_modules/undici-types/proxy-agent.d.ts","./node_modules/undici-types/socks5-proxy-agent.d.ts","./node_modules/undici-types/env-http-proxy-agent.d.ts","./node_modules/undici-types/retry-handler.d.ts","./node_modules/undici-types/retry-agent.d.ts","./node_modules/undici-types/api.d.ts","./node_modules/undici-types/cache-interceptor.d.ts","./node_modules/undici-types/interceptors.d.ts","./node_modules/undici-types/util.d.ts","./node_modules/undici-types/cookies.d.ts","./node_modules/undici-types/patch.d.ts","./node_modules/undici-types/websocket.d.ts","./node_modules/undici-types/eventsource.d.ts","./node_modules/undici-types/diagnostics-channel.d.ts","./node_modules/undici-types/content-type.d.ts","./node_modules/undici-types/cache.d.ts","./node_modules/undici-types/index.d.ts","./node_modules/@types/node/web-globals/fetch.d.ts","./node_modules/@types/node/web-globals/importmeta.d.ts","./node_modules/@types/node/web-globals/messaging.d.ts","./node_modules/@types/node/web-globals/navigator.d.ts","./node_modules/@types/node/web-globals/performance.d.ts","./node_modules/@types/node/web-globals/storage.d.ts","./node_modules/@types/node/web-globals/streams.d.ts","./node_modules/@types/node/web-globals/timers.d.ts","./node_modules/@types/node/web-globals/url.d.ts","./node_modules/@types/node/assert.d.ts","./node_modules/@types/node/assert/strict.d.ts","./node_modules/@types/node/async_hooks.d.ts","./node_modules/@types/node/buffer.d.ts","./node_modules/@types/node/child_process.d.ts","./node_modules/@types/node/cluster.d.ts","./node_modules/@types/node/console.d.ts","./node_modules/@types/node/constants.d.ts","./node_modules/@types/node/crypto.d.ts","./node_modules/@types/node/dgram.d.ts","./node_modules/@types/node/diagnostics_channel.d.ts","./node_modules/@types/node/dns.d.ts","./node_modules/@types/node/dns/promises.d.ts","./node_modules/@types/node/domain.d.ts","./node_modules/@types/node/events.d.ts","./node_modules/@types/node/fs.d.ts","./node_modules/@types/node/fs/promises.d.ts","./node_modules/@types/node/http.d.ts","./node_modules/@types/node/http2.d.ts","./node_modules/@types/node/https.d.ts","./node_modules/@types/node/inspector.d.ts","./node_modules/@types/node/inspector.generated.d.ts","./node_modules/@types/node/inspector/promises.d.ts","./node_modules/@types/node/module.d.ts","./node_modules/@types/node/net.d.ts","./node_modules/buffer/index.d.ts","./node_modules/@types/node/os.d.ts","./node_modules/@types/node/path.d.ts","./node_modules/@types/node/path/posix.d.ts","./node_modules/@types/node/path/win32.d.ts","./node_modules/@types/node/perf_hooks.d.ts","./node_modules/@types/node/process.d.ts","./node_modules/@types/node/punycode.d.ts","./node_modules/@types/node/querystring.d.ts","./node_modules/@types/node/quic.d.ts","./node_modules/@types/node/readline.d.ts","./node_modules/@types/node/readline/promises.d.ts","./node_modules/@types/node/repl.d.ts","./node_modules/@types/node/sea.d.ts","./node_modules/@types/node/sqlite.d.ts","./node_modules/@types/node/stream.d.ts","./node_modules/@types/node/stream/consumers.d.ts","./node_modules/@types/node/stream/iter.d.ts","./node_modules/@types/node/stream/promises.d.ts","./node_modules/@types/node/stream/web.d.ts","./node_modules/@types/node/string_decoder.d.ts","./node_modules/@types/node/test.d.ts","./node_modules/@types/node/test/reporters.d.ts","./node_modules/@types/node/timers.d.ts","./node_modules/@types/node/timers/promises.d.ts","./node_modules/@types/node/tls.d.ts","./node_modules/@types/node/trace_events.d.ts","./node_modules/@types/node/tty.d.ts","./node_modules/@types/node/url.d.ts","./node_modules/@types/node/util.d.ts","./node_modules/@types/node/util/types.d.ts","./node_modules/@types/node/v8.d.ts","./node_modules/@types/node/vm.d.ts","./node_modules/@types/node/wasi.d.ts","./node_modules/@types/node/worker_threads.d.ts","./node_modules/@types/node/zlib.d.ts","./node_modules/@types/node/zlib/iter.d.ts","./node_modules/@types/node/index.d.ts","./node_modules/@types/estree/index.d.ts","./node_modules/rollup/dist/rollup.d.ts","./node_modules/rollup/dist/parseAst.d.ts","./node_modules/vite/types/hmrPayload.d.ts","./node_modules/vite/types/customEvent.d.ts","./node_modules/vite/types/hot.d.ts","./node_modules/vite/dist/node/moduleRunnerTransport.d-DJ_mE5sf.d.ts","./node_modules/vite/dist/node/module-runner.d.ts","./node_modules/esbuild/lib/main.d.ts","./node_modules/source-map-js/source-map.d.ts","./node_modules/postcss/lib/previous-map.d.ts","./node_modules/postcss/lib/input.d.ts","./node_modules/postcss/lib/css-syntax-error.d.ts","./node_modules/postcss/lib/declaration.d.ts","./node_modules/postcss/lib/root.d.ts","./node_modules/postcss/lib/warning.d.ts","./node_modules/postcss/lib/lazy-result.d.ts","./node_modules/postcss/lib/no-work-result.d.ts","./node_modules/postcss/lib/processor.d.ts","./node_modules/postcss/lib/result.d.ts","./node_modules/postcss/lib/document.d.ts","./node_modules/postcss/lib/rule.d.ts","./node_modules/postcss/lib/node.d.ts","./node_modules/postcss/lib/comment.d.ts","./node_modules/postcss/lib/container.d.ts","./node_modules/postcss/lib/at-rule.d.ts","./node_modules/postcss/lib/list.d.ts","./node_modules/postcss/lib/postcss.d.ts","./node_modules/postcss/lib/postcss.d.mts","./node_modules/lightningcss/node/ast.d.ts","./node_modules/lightningcss/node/targets.d.ts","./node_modules/lightningcss/node/index.d.ts","./node_modules/vite/types/internal/lightningcssOptions.d.ts","./node_modules/vite/types/internal/cssPreprocessorOptions.d.ts","./node_modules/vite/types/importGlob.d.ts","./node_modules/vite/types/metadata.d.ts","./node_modules/vite/dist/node/index.d.ts","./node_modules/electron-vite/dist/index.d.ts","./node_modules/@babel/types/lib/index.d.ts","./node_modules/@types/babel__generator/index.d.ts","./node_modules/@babel/parser/typings/babel-parser.d.ts","./node_modules/@types/babel__template/index.d.ts","./node_modules/@types/babel__traverse/index.d.ts","./node_modules/@types/babel__core/index.d.ts","./node_modules/@vitejs/plugin-react/dist/index.d.ts","./node_modules/@tailwindcss/vite/dist/index.d.mts","./electron.vite.config.ts","./node_modules/electron/electron.d.ts","./node_modules/@electron-toolkit/utils/dist/index.d.ts","./src/main/index.ts","./node_modules/@electron-toolkit/preload/dist/index.d.ts","./src/preload/index.ts","./node_modules/electron-vite/node.d.ts"],"fileIdsList":[[89,153,161,165,168,170,171,172,177,185,250,257,258],[89,153,161,165,168,170,171,172,185,251],[89,153,161,165,168,170,171,172,185],[89,153,161,165,168,170,171,172,185,260],[89,153,161,165,168,170,171,172,185,249],[89,153,161,165,168,170,171,172,185,251,252,253,254,255],[89,153,161,165,168,170,171,172,185,251,253],[89,150,151,153,161,165,168,170,171,172,185],[89,152,153,161,165,168,170,171,172,185],[153,161,165,168,170,171,172,185],[89,153,161,165,168,170,171,172,185,194],[89,153,154,159,161,164,165,168,170,171,172,174,185,190,203],[89,153,154,155,161,164,165,168,170,171,172,185],[89,153,156,161,165,168,170,171,172,185,204],[89,153,157,158,161,165,168,170,171,172,176,185],[89,153,158,161,165,168,170,171,172,185,190,200],[89,153,159,161,164,165,168,170,171,172,174,185],[89,152,153,160,161,165,168,170,171,172,185],[89,153,161,162,165,168,170,171,172,185],[89,153,161,163,164,165,168,170,171,172,185],[89,152,153,161,164,165,168,170,171,172,185],[89,153,161,164,165,166,168,170,171,172,185,190,203],[89,153,161,164,165,166,168,170,171,172,185,190,192,194],[89,140,153,161,164,165,167,168,170,171,172,174,185,190,203],[89,153,161,164,165,167,168,170,171,172,174,185,190,200,203],[89,153,161,165,167,168,169,170,171,172,185,190,200,203],[87,88,89,90,91,92,93,94,95,96,97,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,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],[89,153,161,164,165,168,170,171,172,185],[89,153,161,165,168,170,172,185],[89,153,161,165,168,170,171,172,173,185,203],[89,153,161,164,165,168,170,171,172,174,185,190],[89,153,161,165,168,170,171,172,176,185],[89,153,161,165,168,170,171,172,177,185],[89,153,161,164,165,168,170,171,172,180,185],[89,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,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210],[89,153,161,165,168,170,171,172,182,185],[89,153,161,165,168,170,171,172,183,185],[89,153,158,161,165,168,170,171,172,174,185,194],[89,153,161,164,165,168,170,171,172,185,186],[89,153,161,165,168,170,171,172,185,187,204,207],[89,153,161,164,165,168,170,171,172,185,190,193,194],[89,153,161,165,168,170,171,172,185,191,194],[89,153,161,165,168,170,171,172,185,192],[89,153,161,165,168,170,171,172,185,194,204],[89,153,161,165,168,170,171,172,185,195],[89,150,153,161,165,168,170,171,172,185,190,197,203],[89,153,161,165,168,170,171,172,185,190,196],[89,153,161,164,165,168,170,171,172,185,198,199],[89,153,161,165,168,170,171,172,185,198,199],[89,153,158,161,165,168,170,171,172,174,185,190,200],[89,153,161,165,168,170,171,172,185,201],[89,153,161,165,168,170,171,172,174,185,202],[89,153,161,165,167,168,170,171,172,183,185,203],[89,153,161,165,168,170,171,172,185,204,205],[89,153,158,161,165,168,170,171,172,185,205],[89,153,161,165,168,170,171,172,185,190,206],[89,153,161,165,168,170,171,172,173,185,207],[89,153,161,165,168,170,171,172,185,208],[89,153,156,161,165,168,170,171,172,185],[89,153,158,161,165,168,170,171,172,185],[89,153,161,165,168,170,171,172,185,204],[89,140,153,161,165,168,170,171,172,185],[89,153,161,165,168,170,171,172,185,203],[89,153,161,165,168,170,171,172,185,209],[89,153,161,165,168,170,171,172,180,185],[89,153,161,165,168,170,171,172,185,199],[89,140,153,161,164,165,166,168,170,171,172,180,185,190,194,203,206,207,209],[89,153,161,165,168,170,171,172,185,190,210],[89,153,161,165,168,170,171,172,185,192,211],[89,153,161,165,168,170,171,172,185,249,256],[89,153,161,164,165,168,170,171,172,185,212],[89,153,161,165,168,170,171,172,185,242,243],[89,153,161,165,168,170,171,172,185,237],[89,153,161,165,168,170,171,172,185,235,237],[89,153,161,165,168,170,171,172,185,226,234,235,236,238,240],[89,153,161,165,168,170,171,172,185,224],[89,153,161,165,168,170,171,172,185,227,232,237,240],[89,153,161,165,168,170,171,172,185,223,240],[89,153,161,165,168,170,171,172,185,227,228,231,232,233,240],[89,153,161,165,168,170,171,172,185,227,228,229,231,232,240],[89,153,161,165,168,170,171,172,185,224,225,226,227,228,232,233,234,236,237,238,240],[89,153,161,165,168,170,171,172,185,240],[89,153,161,165,168,170,171,172,185,222,224,225,226,227,228,229,231,232,233,234,235,236,237,238,239],[89,153,161,165,168,170,171,172,185,222,240],[89,153,161,165,168,170,171,172,185,227,229,230,232,233,240],[89,153,161,165,168,170,171,172,185,231,240],[89,153,161,165,168,170,171,172,185,232,233,237,240],[89,153,161,165,168,170,171,172,185,225,235],[89,153,161,165,168,170,171,172,185,214,248,249],[89,153,161,165,168,170,171,172,185,213,214],[89,104,107,110,111,153,161,165,168,170,171,172,185,203],[89,107,153,161,165,168,170,171,172,185,190,203],[89,107,111,153,161,165,168,170,171,172,185,203],[89,153,161,165,168,170,171,172,185,190],[89,101,153,161,165,168,170,171,172,185],[89,105,153,161,165,168,170,171,172,185],[89,103,104,107,153,161,165,168,170,171,172,185,203],[89,153,161,165,168,170,171,172,174,185,200],[89,153,161,165,168,170,171,172,185,212],[89,101,153,161,165,168,170,171,172,185,212],[89,103,107,153,161,165,168,170,171,172,174,185,203],[89,98,99,100,102,106,153,161,164,165,168,170,171,172,185,190,203],[89,107,116,124,153,161,165,168,170,171,172,185],[89,99,105,153,161,165,168,170,171,172,185],[89,107,134,135,153,161,165,168,170,171,172,185],[89,99,102,107,153,161,165,168,170,171,172,185,194,203,212],[89,107,153,161,165,168,170,171,172,185],[89,103,107,153,161,165,168,170,171,172,185,203],[89,98,153,161,165,168,170,171,172,185],[89,101,102,103,105,106,107,108,109,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,135,136,137,138,139,153,161,165,168,170,171,172,185],[89,107,127,130,153,161,165,168,170,171,172,185],[89,107,116,117,118,153,161,165,168,170,171,172,185],[89,105,107,117,119,153,161,165,168,170,171,172,185],[89,106,153,161,165,168,170,171,172,185],[89,99,101,107,153,161,165,168,170,171,172,185],[89,107,111,117,119,153,161,165,168,170,171,172,185],[89,111,153,161,165,168,170,171,172,185],[89,105,107,110,153,161,165,168,170,171,172,185,203],[89,99,103,107,116,153,161,165,168,170,171,172,185],[89,107,127,153,161,165,168,170,171,172,185],[89,119,153,161,165,168,170,171,172,185],[89,99,103,107,111,153,161,165,168,170,171,172,185],[89,101,107,134,153,161,165,168,170,171,172,185,194,209,212],[89,153,161,164,165,167,168,169,170,171,172,174,185,190,200,203,210,212,214,215,216,217,219,220,221,241,245,246,247,248,249],[89,153,161,165,168,170,171,172,185,216,217,218,219],[89,153,161,165,168,170,171,172,185,216],[89,153,161,165,168,170,171,172,185,217],[89,153,161,165,168,170,171,172,185,244],[89,153,161,165,168,170,171,172,185,214,249],[89,153,154,158,161,165,166,167,168,169,170,171,172,176,177,185,260,261],[89,153,161,165,168,170,171,172,185,260,263]],"fileInfos":[{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"27bdc30a0e32783366a5abeda841bc22757c1797de8681bbe81fbc735eeb1c10","impliedFormat":1},{"version":"8fd575e12870e9944c7e1d62e1f5a73fcf23dd8d3a321f2a2c74c20d022283fe","impliedFormat":1},{"version":"2ab096661c711e4a81cc464fa1e6feb929a54f5340b46b0a07ac6bbf857471f0","impliedFormat":1},{"version":"080941d9f9ff9307f7e27a83bcd888b7c8270716c39af943532438932ec1d0b9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2e80ee7a49e8ac312cc11b77f1475804bee36b3b2bc896bead8b6e1266befb43","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7a3c8b952931daebdfc7a2897c53c0a1c73624593fa070e46bd537e64dcd20a","affectsGlobalScope":true,"impliedFormat":1},{"version":"80e18897e5884b6723488d4f5652167e7bb5024f946743134ecc4aa4ee731f89","affectsGlobalScope":true,"impliedFormat":1},{"version":"cd034f499c6cdca722b60c04b5b1b78e058487a7085a8e0d6fb50809947ee573","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"8cdf8847677ac7d20486e54dd3fcf09eda95812ac8ace44b4418da1bbbab6eb8","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"df83c2a6c73228b625b0beb6669c7ee2a09c914637e2d35170723ad49c0f5cd4","affectsGlobalScope":true,"impliedFormat":1},{"version":"436aaf437562f276ec2ddbee2f2cdedac7664c1e4c1d2c36839ddd582eeb3d0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e3c06ea092138bf9fa5e874a1fdbc9d54805d074bee1de31b99a11e2fec239d","affectsGlobalScope":true,"impliedFormat":1},{"version":"87dc0f382502f5bbce5129bdc0aea21e19a3abbc19259e0b43ae038a9fc4e326","affectsGlobalScope":true,"impliedFormat":1},{"version":"b1cb28af0c891c8c96b2d6b7be76bd394fddcfdb4709a20ba05a7c1605eea0f9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2fef54945a13095fdb9b84f705f2b5994597640c46afeb2ce78352fab4cb3279","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac77cb3e8c6d3565793eb90a8373ee8033146315a3dbead3bde8db5eaf5e5ec6","affectsGlobalScope":true,"impliedFormat":1},{"version":"56e4ed5aab5f5920980066a9409bfaf53e6d21d3f8d020c17e4de584d29600ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ece9f17b3866cc077099c73f4983bddbcb1dc7ddb943227f1ec070f529dedd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a6282c8827e4b9a95f4bf4f5c205673ada31b982f50572d27103df8ceb8013c","affectsGlobalScope":true,"impliedFormat":1},{"version":"1c9319a09485199c1f7b0498f2988d6d2249793ef67edda49d1e584746be9032","affectsGlobalScope":true,"impliedFormat":1},{"version":"e3a2a0cee0f03ffdde24d89660eba2685bfbdeae955a6c67e8c4c9fd28928eeb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811c71eee4aa0ac5f7adf713323a5c41b0cf6c4e17367a34fbce379e12bbf0a4","affectsGlobalScope":true,"impliedFormat":1},{"version":"51ad4c928303041605b4d7ae32e0c1ee387d43a24cd6f1ebf4a2699e1076d4fa","affectsGlobalScope":true,"impliedFormat":1},{"version":"60037901da1a425516449b9a20073aa03386cce92f7a1fd902d7602be3a7c2e9","affectsGlobalScope":true,"impliedFormat":1},{"version":"d4b1d2c51d058fc21ec2629fff7a76249dec2e36e12960ea056e3ef89174080f","affectsGlobalScope":true,"impliedFormat":1},{"version":"22adec94ef7047a6c9d1af3cb96be87a335908bf9ef386ae9fd50eeb37f44c47","affectsGlobalScope":true,"impliedFormat":1},{"version":"196cb558a13d4533a5163286f30b0509ce0210e4b316c56c38d4c0fd2fb38405","affectsGlobalScope":true,"impliedFormat":1},{"version":"73f78680d4c08509933daf80947902f6ff41b6230f94dd002ae372620adb0f60","affectsGlobalScope":true,"impliedFormat":1},{"version":"c5239f5c01bcfa9cd32f37c496cf19c61d69d37e48be9de612b541aac915805b","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"bde31fd423cd93b0eff97197a3f66df7c93e8c0c335cbeb113b7ff1ac35c23f4","impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"0ccdaa19852d25ecd84eec365c3bfa16e7859cadecf6e9ca6d0dbbbee439743f","affectsGlobalScope":true,"impliedFormat":1},{"version":"cc2110f7decca6bfb9392e30421cfa1436479e4a6756e8fec6cbc22625d4f881","affectsGlobalScope":true,"impliedFormat":1},{"version":"096116f8fedc1765d5bd6ef360c257b4a9048e5415054b3bf3c41b07f8951b0b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e5e01375c9e124a83b52ee4b3244ed1a4d214a6cfb54ac73e164a823a4a7860a","affectsGlobalScope":true,"impliedFormat":1},{"version":"f90ae2bbce1505e67f2f6502392e318f5714bae82d2d969185c4a6cecc8af2fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b58e207b93a8f1c88bbf2a95ddc686ac83962b13830fe8ad3f404ffc7051fb4","affectsGlobalScope":true,"impliedFormat":1},{"version":"1fefabcb2b06736a66d2904074d56268753654805e829989a46a0161cd8412c5","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"c18a99f01eb788d849ad032b31cafd49de0b19e083fe775370834c5675d7df8e","affectsGlobalScope":true,"impliedFormat":1},{"version":"5247874c2a23b9a62d178ae84f2db6a1d54e6c9a2e7e057e178cc5eea13757fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"cdcf9ea426ad970f96ac930cd176d5c69c6c24eebd9fc580e1572d6c6a88f62c","impliedFormat":1},{"version":"23cd712e2ce083d68afe69224587438e5914b457b8acf87073c22494d706a3d0","impliedFormat":1},{"version":"156a859e21ef3244d13afeeba4e49760a6afa035c149dda52f0c45ea8903b338","impliedFormat":1},{"version":"10ec5e82144dfac6f04fa5d1d6c11763b3e4dbbac6d99101427219ab3e2ae887","impliedFormat":1},{"version":"615754924717c0b1e293e083b83503c0a872717ad5aa60ed7f1a699eb1b4ea5c","impliedFormat":1},{"version":"14e9acf826baba0ef4b5665704084896e7bcc06f65a9ab13af7e93d27d6b7069","impliedFormat":1},{"version":"68834d631c8838c715f225509cfc3927913b9cc7a4870460b5b60c8dbdb99baf","impliedFormat":1},{"version":"21adf13435b9b748529c8cedf80f884e5130b9684188120a686cd2b26a2059c7","impliedFormat":1},{"version":"eec76bf6b9346f3f95fa402621b889489e96930e72295b0369022f332e9b4a6a","impliedFormat":1},{"version":"0ecd58f413f9bc3b7d4383eae31b0c8fc576985cd7404d6f99f8c643543ade74","impliedFormat":1},{"version":"ea6bc8de8b59f90a7a3960005fd01988f98fd0784e14bc6922dde2e93305ec7d","impliedFormat":1},{"version":"36107995674b29284a115e21a0618c4c2751b32a8766dd4cb3ba740308b16d59","impliedFormat":1},{"version":"914a0ae30d96d71915fc519ccb4efbf2b62c0ddfb3a3fc6129151076bc01dc60","impliedFormat":1},{"version":"9c32412007b5662fd34a8eb04292fb5314ec370d7016d1c2fb8aa193c807fe22","impliedFormat":1},{"version":"7fd1b31fd35876b0aa650811c25ec2c97a3c6387e5473eb18004bed86cdd76b6","impliedFormat":1},{"version":"4d327f7d72ad0918275cea3eee49a6a8dc8114ae1d5b7f3f5d0774de75f7439a","impliedFormat":1},{"version":"6ebe8ebb8659aaa9d1acbf3710d7dae3e923e97610238b9511c25dc39023a166","impliedFormat":1},{"version":"e85d7f8068f6a26710bff0cc8c0fc5e47f71089c3780fbede05857331d2ddec9","impliedFormat":1},{"version":"7befaf0e76b5671be1d47b77fcc65f2b0aad91cc26529df1904f4a7c46d216e9","impliedFormat":1},{"version":"0a60a292b89ca7218b8616f78e5bbd1c96b87e048849469cccb4355e98af959a","impliedFormat":1},{"version":"0b6e25234b4eec6ed96ab138d96eb70b135690d7dd01f3dd8a8ab291c35a683a","impliedFormat":1},{"version":"9666f2f84b985b62400d2e5ab0adae9ff44de9b2a34803c2c5bd3c8325b17dc0","impliedFormat":1},{"version":"40cd35c95e9cf22cfa5bd84e96408b6fcbca55295f4ff822390abb11afbc3dca","impliedFormat":1},{"version":"b1616b8959bf557feb16369c6124a97a0e74ed6f49d1df73bb4b9ddf68acf3f3","impliedFormat":1},{"version":"5b03a034c72146b61573aab280f295b015b9168470f2df05f6080a2122f9b4df","impliedFormat":1},{"version":"40b463c6766ca1b689bfcc46d26b5e295954f32ad43e37ee6953c0a677e4ae2b","impliedFormat":1},{"version":"249b9cab7f5d628b71308c7d9bb0a808b50b091e640ba3ed6e2d0516f4a8d91d","impliedFormat":1},{"version":"d33ce35e3f9cfcc1d94eca415bdd3bde94d5b153ffdd33e6c4455c029986c630","impliedFormat":1},{"version":"80aae6afc67faa5ac0b32b5b8bc8cc9f7fa299cff15cf09cc2e11fd28c6ae29e","impliedFormat":1},{"version":"f473cd2288991ff3221165dcf73cd5d24da30391f87e85b3dd4d0450c787a391","impliedFormat":1},{"version":"499e5b055a5aba1e1998f7311a6c441a369831c70905cc565ceac93c28083d53","impliedFormat":1},{"version":"8aee8b6d4f9f62cf3776cda1305fb18763e2aade7e13cea5bbe699112df85214","impliedFormat":1},{"version":"98498b101803bb3dde9f76a56e65c14b75db1cc8bec5f4db72be541570f74fc5","impliedFormat":1},{"version":"4dc59f6e1dbf3d5f66660fceabe6c174d3261b37b696ae1854f0dbaf255fc753","impliedFormat":1},{"version":"5d0375ca7310efb77e3ef18d068d53784faf62705e0ad04569597ae0e755c401","impliedFormat":1},{"version":"59af37caec41ecf7b2e76059c9672a49e682c1a2aa6f9d7dc78878f53aa284d6","impliedFormat":1},{"version":"addf417b9eb3f938fddf8d81e96393a165e4be0d4a8b6402292f9c634b1cb00d","impliedFormat":1},{"version":"436d7b4543b340b0f3eef4310d524242e41369b9652aa9c70428767c4dcac455","impliedFormat":1},{"version":"adf27937dba6af9f08a68c5b1d3fce0ca7d4b960c57e6d6c844e7d1a8e53adae","impliedFormat":1},{"version":"12950411eeab8563b349cb7959543d92d8d02c289ed893d78499a19becb5a8cc","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"114f493b30f364255290472111b5a4791d5902c308645670cd0401429cbc6930","impliedFormat":1},{"version":"c3f5289820990ab66b70c7fb5b63cb674001009ff84b13de40619619a9c8175f","affectsGlobalScope":true,"impliedFormat":1},{"version":"b3275d55fac10b799c9546804126239baf020d220136163f763b55a74e50e750","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa68a0a3b7cb32c00e39ee3cd31f8f15b80cac97dce51b6ee7fc14a1e8deb30b","affectsGlobalScope":true,"impliedFormat":1},{"version":"1cf059eaf468efcc649f8cf6075d3cb98e9a35a0fe9c44419ec3d2f5428d7123","affectsGlobalScope":true,"impliedFormat":1},{"version":"6c36e755bced82df7fb6ce8169265d0a7bb046ab4e2cb6d0da0cb72b22033e89","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"7a93de4ff8a63bafe62ba86b89af1df0ccb5e40bb85b0c67d6bbcfdcf96bf3d4","affectsGlobalScope":true,"impliedFormat":1},{"version":"90e85f9bc549dfe2b5749b45fe734144e96cd5d04b38eae244028794e142a77e","affectsGlobalScope":true,"impliedFormat":1},{"version":"e0a5deeb610b2a50a6350bd23df6490036a1773a8a71d70f2f9549ab009e67ee","affectsGlobalScope":true,"impliedFormat":1},{"version":"d2ae155afe8a01cc0ae612d99117cf8ef16692ba7c4366590156fdec1bcf2d8c","impliedFormat":1},{"version":"3f5e5d9be35913db9fea42a63f3df0b7e3c8703b97670a2125587b4dbbd56d7c","impliedFormat":1},{"version":"c8b8968311ec4e5e97b7b5fb8a65efaba455db9bdcfd7fff7fb15f6e317bfba0","impliedFormat":1},{"version":"57c23df0b5f7a8e26363a3849b0bc7763f6b241207157c8e40089d1df4116f35","affectsGlobalScope":true,"impliedFormat":1},{"version":"3b8bc0c17b54081b0878673989216229e575d67a10874e84566a21025a2461ee","impliedFormat":1},{"version":"5b0db5a58b73498792a29bfebc333438e61906fef75da898b410e24e52229e6f","impliedFormat":1},{"version":"dbe055b2b29a7bab2c1ca8f259436306adb43f469dca7e639a02cd3695d3f621","impliedFormat":1},{"version":"1678b04557dca52feab73cc67610918a7f5e25bfdba3e7fa081acd625d93106d","impliedFormat":1},{"version":"aecbf1d9e6a18dab7d92ef8a89a1444b47e1eb6134cb2bb776a26d55ff58c29a","impliedFormat":1},{"version":"2ea729503db9793f2691162fec3dd1118cab62e96d025f8eeb376d43ec293395","impliedFormat":1},{"version":"9ec87fea42b92894b0f209931a880789d43c3397d09dd99c631ae40a2f7071d1","impliedFormat":1},{"version":"c68e88cdfadfb6c8ba5fc38e58a3a166b0beae77b1f05b7d921150a32a5ffb8d","impliedFormat":1},{"version":"2bc7aa4fba46df0bd495425a7c8201437a7d465f83854fac859df2d67f664df3","impliedFormat":1},{"version":"41d17e1ad9a002feb11c8cdd2777e5bbc0cdb1e3f595d237e4dded0b6949983b","impliedFormat":1},{"version":"1fede9296beac11ce8e6b425396a1791f64341f2be85deebb6286faf6e16306e","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce697b6a251d9cad53998c7fd3098072df883b525ec45d83530e434dc6d80dc6","impliedFormat":1},{"version":"719412f054e6ecc35489462c9a21bab0323d173a7d04e55b0ace4b5d86fbeb07","impliedFormat":1},{"version":"0eb5d0cbf09de5d34542b977fd6a933bb2e0817bffe8e1a541b2f1ad1b9af1ff","impliedFormat":1},{"version":"fac3e88881b35d3a757ed891ac912b2674792c25e2a1a74e1f5fbc72d19a9792","impliedFormat":1},{"version":"2c2bdaa1d8ead9f68628d6d9d250e46ee8e81aa4898b4769a36956ae15e060fe","impliedFormat":1},{"version":"c32c840c62d8bd7aeb3147aa6754cd2d922b990a6b6634530cb2ebdce5adc8e9","impliedFormat":1},{"version":"5ff4433a2deae4f85ab1377e90a7554ce6b47ae51c69a84ca30a6e22fae85834","impliedFormat":1},{"version":"82b91e4e42e6c41bc7fc1b6c2dc5eba6a2ba98375eb1f210e6ff6bba2d54177e","impliedFormat":1},{"version":"c1fa52b3d014001e8662fa2669d90ea15373958a288e3b83a3b621733d25292a","affectsGlobalScope":true,"impliedFormat":1},{"version":"cbed824fec91efefc7bbdcb8b43d1a531fdbebd0e2ef19481501ff365a93cb70","impliedFormat":1},{"version":"8e9c23ba78aabc2e0a27033f18737a6df754067731e69dc5f52823957d60a4b6","impliedFormat":1},{"version":"d0716593b3f2b0451bcf0c24cfa86dec2235c325c89f201934248b7c742715fc","impliedFormat":1},{"version":"ec501101c2a96133a6c695f934c8f6642149cc728571b29cbb7b770984c1088e","impliedFormat":1},{"version":"b214ebcf76c51b115453f69729ee8aa7b7f8eccdae2a922b568a45c2d7ff52f7","impliedFormat":1},{"version":"429c9cdfa7d126255779efd7e6d9057ced2d69c81859bbab32073bad52e9ba76","impliedFormat":1},{"version":"2991bca2cc0f0628a278df2a2ccdb8d6cbcb700f3761abbed62bba137d5b1790","impliedFormat":1},{"version":"5e66972e83eb4dc7123939bf816e6cbd9ad81af5552db1cab84e6bd9c64d2ecc","affectsGlobalScope":true,"impliedFormat":1},{"version":"230763250f20449fa7b3c9273e1967adb0023dc890d4be1553faca658ee65971","impliedFormat":1},{"version":"c3e9078b60cb329d1221f5878e88cecfa3e74460550e605a58fcfb41a66029ff","impliedFormat":1},{"version":"8413d0641f293aed551c7464615b770d34a02dedede889b9591172287d68e773","impliedFormat":1},{"version":"0ea59f7d3e51440baa64f429253759b106cfcbaf51e474cae606e02265b37cf8","impliedFormat":1},{"version":"bc18a1991ba681f03e13285fa1d7b99b03b67ee671b7bc936254467177543890","impliedFormat":1},{"version":"1b241e24f3227d078c06aeda6e050187ad59a4e591f4467abed44d92b084e08d","impliedFormat":1},{"version":"fa94bbf532b7af8f394b95fa310980d6e20bd2d4c871c6a6cb9f70f03750a44b","impliedFormat":1},{"version":"7fde0e1be5c8be204ffbf428abfcf01da2eb0f130e1bc3f539eb7275f4fd1f58","impliedFormat":1},{"version":"e284328553df5f425a5d33d36a0c3fa66b46af9d097cad6f4d2e8696dfdeb0f1","affectsGlobalScope":true,"impliedFormat":1},{"version":"7fa2214bb0d64701bc6f9ce8cde2fd2ff8c571e0b23065fa04a8a5a6beb91511","impliedFormat":1},{"version":"b58040a9d317060d1a5f6e78bb8bd125894fb660feea534fe0e733cf5fdd7d07","impliedFormat":1},{"version":"f1c93e046fb3d9b7f8249629f4b63dc068dd839b824dd0aa39a5e68476dc9420","impliedFormat":1},{"version":"016b29bf4926b80255a108c53a1451717350059da04fcae64d1075f5e93bbb39","impliedFormat":1},{"version":"841983e39bd4cbb463be385e92fda11057cab368bf27100a801c492f1d86cbaa","impliedFormat":1},{"version":"1c4f139ade4f6ebf45463505f8155173e5d7a5305e50e0aae0a5e712d6ff3b48","impliedFormat":1},{"version":"e16b319e5aca1031168de823c4946ff8e29629c4c8cc0ec0fcfe2a8ab2155043","impliedFormat":1},{"version":"e4156ddb25aa0e3b5303d372f26957b36778f0f6bbd4326359269873295e3058","affectsGlobalScope":true,"impliedFormat":1},{"version":"cc1b433a84cae05ddc5672d4823170af78606ad21ecef60dbc4570190cbf1357","impliedFormat":1},{"version":"9d3821bc75c59577e52643324cec92fc2145642e8d17cf7ee07a3181f21d985d","impliedFormat":1},{"version":"7f78cfb2b343838612c192cb251746e3a7c62ac7675726a47e130d9b213f6580","impliedFormat":1},{"version":"201db9cf1687fab1adf5282fcba861f382b32303dc4f67c89d59655e78a25461","impliedFormat":1},{"version":"2c3c5c0f54055e87640f5d233716fd889f3034fc7911d603b642369b0dbeb2a7","impliedFormat":1},{"version":"0a20eaf2e4b1e3c1e1f87f7bccb0c936375b23b022baeea750519b7c9bc6ce83","impliedFormat":1},{"version":"b484ec11ba00e3a2235562a41898d55372ccabe607986c6fa4f4aba72093749f","impliedFormat":1},{"version":"a16b91b27bd6b706c687c88cbc8a7d4ee98e5ed6043026d6b84bda923c0aed67","impliedFormat":1},{"version":"1c9e5b1a17b1fc9b3711fb36e0690421261ab2880f15b145155b5b2ba2ab6c2d","impliedFormat":1},{"version":"99ab6d0d660ce4d21efb52288a39fd35bb3f556980ec5463b1ae8f304a3bbc85","impliedFormat":1},{"version":"6eeded8c7e352be6e0efb83f4935ec752513c4d22043b52522b90849a49a3a11","impliedFormat":1},{"version":"6c1ad90050ffbb151cacc68e2d06ea1a26a945659391e32651f5d42b86fd7f2c","impliedFormat":1},{"version":"afa1c49f8e559e413d57343339db857d2a8159435cf9cf7d4deb41718fff1b88","impliedFormat":1},{"version":"6953d7597831d0860c7034cf4f0419687d263b6b98a4b32e37ce6d49615c36e2","impliedFormat":1},{"version":"151ff381ef9ff8da2da9b9663ebf657eac35c4c9a19183420c05728f31a6761d","impliedFormat":1},{"version":"ee70b8037ecdf0de6c04f35277f253663a536d7e38f1539d270e4e916d225a3f","affectsGlobalScope":true,"impliedFormat":1},{"version":"a660aa95476042d3fdcc1343cf6bb8fdf24772d31712b1db321c5a4dcc325434","impliedFormat":1},{"version":"a7ca8df4f2931bef2aa4118078584d84a0b16539598eaadf7dce9104dfaa381c","impliedFormat":1},{"version":"11443a1dcfaaa404c68d53368b5b818712b95dd19f188cab1669c39bee8b84b3","impliedFormat":1},{"version":"36977c14a7f7bfc8c0426ae4343875689949fb699f3f84ecbe5b300ebf9a2c55","impliedFormat":1},{"version":"035d0934d304483f07148427a5bd5b98ac265dae914a6b49749fe23fbd893ec7","impliedFormat":99},{"version":"e2ed5b81cbed3a511b21a18ab2539e79ac1f4bc1d1d28f8d35d8104caa3b429f","impliedFormat":99},{"version":"161c8e0690c46021506e32fda85956d785b70f309ae97011fd27374c065cac9b","affectsGlobalScope":true,"impliedFormat":1},{"version":"402e5c534fb2b85fa771170595db3ac0dd532112c8fa44fc23f233bc6967488b","impliedFormat":1},{"version":"52dcc257df5119fb66d864625112ce5033ac51a4c2afe376a0b299d2f7f76e4a","impliedFormat":1},{"version":"e5bab5f871ef708d52d47b3e5d0aa72a08ee7a152f33931d9a60809711a2a9a3","impliedFormat":1},{"version":"e16dc2a81595736024a206c7d5c8a39bfe2e6039208ef29981d0d95434ba8fcf","impliedFormat":1},{"version":"cc4a4903fb698ca1d961d4c10dce658aa3a479faf40509d526f122b044eaf6a4","impliedFormat":1},{"version":"19ee8416e6473ed6c7adb868fa796b5653cf0fa2a337658e677eaa0d134388c3","impliedFormat":1},{"version":"1328ab4e442614b28cdb3d4b414cf68325c0da0dca07287a338d0654b7a00261","impliedFormat":1},{"version":"a039dc21f045919f3cbee2ec13812cc6cc3eebc99dae4be00973230f468d19a6","impliedFormat":1},{"version":"3fbe57af01460e49dcd29df55d6931e1672bc6f1be0fb073d11410bc16f9037d","impliedFormat":1},{"version":"f760be449e8562ec5c09bb5187e8e1eabf3c113c0c58cddda53ef8c69f3e2131","impliedFormat":1},{"version":"44325ed13294fce6ab825b82947bbeed2611db7dad9d9135260192f375e5a189","impliedFormat":1},{"version":"e392e8fb5b514eafc585601c1d781485aa6dd6a320e75daf1064a4c6918a1b45","impliedFormat":1},{"version":"46e4a36e8ddbdfb4e7330e11c81c970dc8b218611df9183d39c41c5f8c653b55","impliedFormat":1},{"version":"370bde134aa8c2abc926d0e99d3a4d5d5dba65c6ee65459137e4f02670cbf841","impliedFormat":1},{"version":"6332f565867cf4a740a70e30f31cefba37ef7cebcf74f22eab8d744fde6d193e","impliedFormat":1},{"version":"2977b7884aedc895a1d0c9c210c7cf3272c29d6959a08a6fa3ff71e0aff08175","impliedFormat":1},{"version":"17f2922d41ddd032830a91371c948cd9ce903b35c95adca72271a54584f19b0b","impliedFormat":1},{"version":"3eed76ede2a1a14d7c9bb0a642041282dcc264811139d3dd275c9fe14efc9840","impliedFormat":1},{"version":"e3cf0611709328b449ec13f8c436712d62003620ce480139fae46ce001c2ee9f","impliedFormat":1},{"version":"8d369483f0c2b9ee388129cfdb6a43bc8112b377e86a41884bd06e19ce04f4c1","impliedFormat":99},{"version":"3fd8a5aefd8c3feb3936ca66f5aa89dff7bf6e6537b4158dbd0f6e0d65ed3b9e","impliedFormat":1},{"version":"a18642ddf216f162052a16cba0944892c4c4c977d3306a87cb673d46abbb0cbf","impliedFormat":1},{"version":"41c41c6e90133bb2a14f7561f29944771886e5535945b2b372e2f6ed6987746e","impliedFormat":1},{"version":"4ec16d7a4e366c06a4573d299e15fe6207fc080f41beac5da06f4af33ea9761e","impliedFormat":1},{"version":"7870becb94cbc11d2d01b77c4422589adcba4d8e59f726246d40cd0d129784d8","affectsGlobalScope":true,"impliedFormat":1},{"version":"7f698624bbbb060ece7c0e51b7236520ebada74b747d7523c7df376453ed6fea","impliedFormat":1},{"version":"f70b8328a15ca1d10b1436b691e134a49bc30dcf3183a69bfaa7ba77e1b78ecd","impliedFormat":1},{"version":"d9030fc0c412a31e7e13d189b9ad032b5177c20217add0f24fd3fff0cf272882","impliedFormat":99},{"version":"e7ba74900e0edb9cb93edb1547cbb65238d26af0e5f10d24be3a61d862262cfd","impliedFormat":99},{"version":"556ccd493ec36c7d7cb130d51be66e147b91cc1415be383d71da0f1e49f742a9","impliedFormat":1},{"version":"b6d03c9cfe2cf0ba4c673c209fcd7c46c815b2619fd2aad59fc4229aaef2ed43","impliedFormat":1},{"version":"95aba78013d782537cc5e23868e736bec5d377b918990e28ed56110e3ae8b958","impliedFormat":1},{"version":"670a76db379b27c8ff42f1ba927828a22862e2ab0b0908e38b671f0e912cc5ed","impliedFormat":1},{"version":"13b77ab19ef7aadd86a1e54f2f08ea23a6d74e102909e3c00d31f231ed040f62","impliedFormat":1},{"version":"069bebfee29864e3955378107e243508b163e77ab10de6a5ee03ae06939f0bb9","impliedFormat":1},{"version":"26e0ffceb2198feb1ef460d5d14111c69ad07d44c5a67fd4bfeb74c969aa9afb","impliedFormat":99},{"version":"7757c6ca7a8ad1992401c6aff33633d6a088515be5a39d7ee188b35bfc8e5f8e","impliedFormat":99},{"version":"2d2dc284419f923a138671df4faa72f22e27e50edfb36be0965828d1e29261a2","signature":"8d916869b630869b2798d91a5cfceab8acc6247d81d4042ce14198d01ddebb60"},{"version":"94a0fcd4f758fe669bc78f1c9146f5fc07879783787364ca89ce486b0cf37920","affectsGlobalScope":true,"impliedFormat":1},{"version":"194779039dc8a41af2eefa2ff740594be9cf44333870346c20602031ad73f168","impliedFormat":1},{"version":"3aaef82d6d7e64660d934e633012b8d3474ae8c5c100c245a6e35f9b8ff56fde","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"2329508bb462ba8f17cc60c4ed5b346618a42eefcaaddcbb0fcbf7f09cfd0a87","impliedFormat":1},{"version":"991a3dc027a78cffb335f3a502fab683cc955fc0e4fab478817ba0fda311b83b","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"2efbc5086f10635b6507392c3d9d073e94d9308a5cf08af5ae4389c250d1791c","affectsGlobalScope":true,"impliedFormat":99}],"root":[259,262,264],"options":{"composite":true,"esModuleInterop":true,"jsx":1,"module":99,"noImplicitAny":false,"noImplicitReturns":true,"noUnusedLocals":true,"noUnusedParameters":true,"skipLibCheck":true,"sourceMap":false,"strict":true,"target":99},"referencedMap":[[259,1],[253,2],[251,3],[263,4],[261,4],[258,5],[256,6],[252,2],[254,7],[255,2],[213,3],[150,8],[151,8],[152,9],[89,10],[153,11],[154,12],[155,13],[87,3],[156,14],[157,15],[158,16],[159,17],[160,18],[161,19],[162,19],[163,20],[164,21],[165,22],[166,23],[90,3],[88,3],[167,24],[168,25],[169,26],[212,27],[170,28],[171,29],[172,28],[173,30],[174,31],[176,32],[177,33],[178,33],[179,33],[180,34],[181,35],[182,36],[183,37],[184,38],[185,39],[186,39],[187,40],[188,3],[189,3],[190,41],[191,42],[192,43],[193,41],[194,44],[195,45],[196,46],[197,47],[198,48],[199,49],[200,50],[201,51],[202,52],[203,53],[204,54],[205,55],[206,56],[207,57],[208,58],[91,28],[92,3],[93,59],[94,60],[95,3],[96,61],[97,3],[141,62],[142,63],[143,64],[144,64],[145,65],[146,3],[147,11],[148,66],[149,63],[209,67],[210,68],[211,69],[257,70],[175,3],[250,5],[265,64],[260,71],[221,3],[242,3],[244,72],[243,3],[238,73],[236,74],[237,75],[225,76],[226,74],[233,77],[224,78],[229,79],[239,3],[230,80],[235,81],[241,82],[240,83],[223,84],[231,85],[232,86],[227,87],[234,73],[228,88],[215,89],[214,90],[222,3],[84,3],[85,3],[15,3],[13,3],[14,3],[19,3],[18,3],[2,3],[20,3],[21,3],[22,3],[23,3],[24,3],[25,3],[26,3],[27,3],[3,3],[28,3],[29,3],[4,3],[30,3],[34,3],[31,3],[32,3],[33,3],[35,3],[36,3],[37,3],[5,3],[38,3],[39,3],[40,3],[41,3],[6,3],[45,3],[42,3],[43,3],[44,3],[46,3],[7,3],[47,3],[52,3],[53,3],[48,3],[49,3],[50,3],[51,3],[8,3],[57,3],[54,3],[55,3],[56,3],[58,3],[9,3],[59,3],[60,3],[61,3],[63,3],[62,3],[64,3],[65,3],[10,3],[66,3],[67,3],[68,3],[11,3],[69,3],[70,3],[71,3],[72,3],[73,3],[1,3],[74,3],[75,3],[12,3],[79,3],[77,3],[82,3],[81,3],[86,3],[76,3],[80,3],[78,3],[83,3],[17,3],[16,3],[116,91],[129,92],[113,93],[130,94],[139,95],[104,96],[105,97],[103,98],[138,99],[133,100],[137,101],[107,102],[126,103],[106,104],[136,105],[101,106],[102,100],[108,107],[109,3],[115,108],[112,107],[99,109],[140,110],[131,111],[119,112],[118,107],[120,113],[123,114],[117,115],[121,116],[134,99],[110,117],[111,118],[124,119],[100,94],[128,120],[127,107],[114,118],[122,121],[125,122],[132,3],[98,3],[135,123],[249,124],[220,125],[219,126],[217,126],[216,3],[218,127],[247,3],[246,3],[245,128],[248,129],[262,130],[264,131]],"affectedFilesPendingEmit":[[259,17],[262,17],[264,17]],"latestChangedDtsFile":"./src/preload/index.d.ts","version":"5.9.3"} \ No newline at end of file +{"fileNames":["./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/typescript/lib/lib.es2023.d.ts","./node_modules/typescript/lib/lib.es2024.d.ts","./node_modules/typescript/lib/lib.esnext.d.ts","./node_modules/typescript/lib/lib.dom.d.ts","./node_modules/typescript/lib/lib.dom.iterable.d.ts","./node_modules/typescript/lib/lib.dom.asynciterable.d.ts","./node_modules/typescript/lib/lib.webworker.importscripts.d.ts","./node_modules/typescript/lib/lib.scripthost.d.ts","./node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/typescript/lib/lib.es2022.object.d.ts","./node_modules/typescript/lib/lib.es2022.string.d.ts","./node_modules/typescript/lib/lib.es2022.regexp.d.ts","./node_modules/typescript/lib/lib.es2023.array.d.ts","./node_modules/typescript/lib/lib.es2023.collection.d.ts","./node_modules/typescript/lib/lib.es2023.intl.d.ts","./node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2024.collection.d.ts","./node_modules/typescript/lib/lib.es2024.object.d.ts","./node_modules/typescript/lib/lib.es2024.promise.d.ts","./node_modules/typescript/lib/lib.es2024.regexp.d.ts","./node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2024.string.d.ts","./node_modules/typescript/lib/lib.esnext.array.d.ts","./node_modules/typescript/lib/lib.esnext.collection.d.ts","./node_modules/typescript/lib/lib.esnext.intl.d.ts","./node_modules/typescript/lib/lib.esnext.disposable.d.ts","./node_modules/typescript/lib/lib.esnext.promise.d.ts","./node_modules/typescript/lib/lib.esnext.decorators.d.ts","./node_modules/typescript/lib/lib.esnext.iterator.d.ts","./node_modules/typescript/lib/lib.esnext.float16.d.ts","./node_modules/typescript/lib/lib.esnext.error.d.ts","./node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/typescript/lib/lib.esnext.full.d.ts","./node_modules/@types/node/compatibility/iterators.d.ts","./node_modules/@types/node/globals.typedarray.d.ts","./node_modules/@types/node/buffer.buffer.d.ts","./node_modules/@types/node/globals.d.ts","./node_modules/@types/node/web-globals/abortcontroller.d.ts","./node_modules/@types/node/web-globals/blob.d.ts","./node_modules/@types/node/web-globals/console.d.ts","./node_modules/@types/node/web-globals/crypto.d.ts","./node_modules/@types/node/web-globals/domexception.d.ts","./node_modules/@types/node/web-globals/encoding.d.ts","./node_modules/@types/node/web-globals/events.d.ts","./node_modules/undici-types/utility.d.ts","./node_modules/undici-types/header.d.ts","./node_modules/undici-types/readable.d.ts","./node_modules/undici-types/fetch.d.ts","./node_modules/undici-types/formdata.d.ts","./node_modules/undici-types/connector.d.ts","./node_modules/undici-types/client-stats.d.ts","./node_modules/undici-types/client.d.ts","./node_modules/undici-types/errors.d.ts","./node_modules/undici-types/dispatcher.d.ts","./node_modules/undici-types/global-dispatcher.d.ts","./node_modules/undici-types/global-origin.d.ts","./node_modules/undici-types/pool-stats.d.ts","./node_modules/undici-types/pool.d.ts","./node_modules/undici-types/handlers.d.ts","./node_modules/undici-types/balanced-pool.d.ts","./node_modules/undici-types/round-robin-pool.d.ts","./node_modules/undici-types/h2c-client.d.ts","./node_modules/undici-types/agent.d.ts","./node_modules/undici-types/mock-interceptor.d.ts","./node_modules/undici-types/mock-call-history.d.ts","./node_modules/undici-types/mock-agent.d.ts","./node_modules/undici-types/mock-client.d.ts","./node_modules/undici-types/mock-pool.d.ts","./node_modules/undici-types/snapshot-agent.d.ts","./node_modules/undici-types/mock-errors.d.ts","./node_modules/undici-types/proxy-agent.d.ts","./node_modules/undici-types/socks5-proxy-agent.d.ts","./node_modules/undici-types/env-http-proxy-agent.d.ts","./node_modules/undici-types/retry-handler.d.ts","./node_modules/undici-types/retry-agent.d.ts","./node_modules/undici-types/api.d.ts","./node_modules/undici-types/cache-interceptor.d.ts","./node_modules/undici-types/interceptors.d.ts","./node_modules/undici-types/util.d.ts","./node_modules/undici-types/cookies.d.ts","./node_modules/undici-types/patch.d.ts","./node_modules/undici-types/websocket.d.ts","./node_modules/undici-types/eventsource.d.ts","./node_modules/undici-types/diagnostics-channel.d.ts","./node_modules/undici-types/content-type.d.ts","./node_modules/undici-types/cache.d.ts","./node_modules/undici-types/index.d.ts","./node_modules/@types/node/web-globals/fetch.d.ts","./node_modules/@types/node/web-globals/importmeta.d.ts","./node_modules/@types/node/web-globals/messaging.d.ts","./node_modules/@types/node/web-globals/navigator.d.ts","./node_modules/@types/node/web-globals/performance.d.ts","./node_modules/@types/node/web-globals/storage.d.ts","./node_modules/@types/node/web-globals/streams.d.ts","./node_modules/@types/node/web-globals/timers.d.ts","./node_modules/@types/node/web-globals/url.d.ts","./node_modules/@types/node/assert.d.ts","./node_modules/@types/node/assert/strict.d.ts","./node_modules/@types/node/async_hooks.d.ts","./node_modules/@types/node/buffer.d.ts","./node_modules/@types/node/child_process.d.ts","./node_modules/@types/node/cluster.d.ts","./node_modules/@types/node/console.d.ts","./node_modules/@types/node/constants.d.ts","./node_modules/@types/node/crypto.d.ts","./node_modules/@types/node/dgram.d.ts","./node_modules/@types/node/diagnostics_channel.d.ts","./node_modules/@types/node/dns.d.ts","./node_modules/@types/node/dns/promises.d.ts","./node_modules/@types/node/domain.d.ts","./node_modules/@types/node/events.d.ts","./node_modules/@types/node/fs.d.ts","./node_modules/@types/node/fs/promises.d.ts","./node_modules/@types/node/http.d.ts","./node_modules/@types/node/http2.d.ts","./node_modules/@types/node/https.d.ts","./node_modules/@types/node/inspector.d.ts","./node_modules/@types/node/inspector.generated.d.ts","./node_modules/@types/node/inspector/promises.d.ts","./node_modules/@types/node/module.d.ts","./node_modules/@types/node/net.d.ts","./node_modules/buffer/index.d.ts","./node_modules/@types/node/os.d.ts","./node_modules/@types/node/path.d.ts","./node_modules/@types/node/path/posix.d.ts","./node_modules/@types/node/path/win32.d.ts","./node_modules/@types/node/perf_hooks.d.ts","./node_modules/@types/node/process.d.ts","./node_modules/@types/node/punycode.d.ts","./node_modules/@types/node/querystring.d.ts","./node_modules/@types/node/quic.d.ts","./node_modules/@types/node/readline.d.ts","./node_modules/@types/node/readline/promises.d.ts","./node_modules/@types/node/repl.d.ts","./node_modules/@types/node/sea.d.ts","./node_modules/@types/node/sqlite.d.ts","./node_modules/@types/node/stream.d.ts","./node_modules/@types/node/stream/consumers.d.ts","./node_modules/@types/node/stream/iter.d.ts","./node_modules/@types/node/stream/promises.d.ts","./node_modules/@types/node/stream/web.d.ts","./node_modules/@types/node/string_decoder.d.ts","./node_modules/@types/node/test.d.ts","./node_modules/@types/node/test/reporters.d.ts","./node_modules/@types/node/timers.d.ts","./node_modules/@types/node/timers/promises.d.ts","./node_modules/@types/node/tls.d.ts","./node_modules/@types/node/trace_events.d.ts","./node_modules/@types/node/tty.d.ts","./node_modules/@types/node/url.d.ts","./node_modules/@types/node/util.d.ts","./node_modules/@types/node/util/types.d.ts","./node_modules/@types/node/v8.d.ts","./node_modules/@types/node/vm.d.ts","./node_modules/@types/node/wasi.d.ts","./node_modules/@types/node/worker_threads.d.ts","./node_modules/@types/node/zlib.d.ts","./node_modules/@types/node/zlib/iter.d.ts","./node_modules/@types/node/index.d.ts","./node_modules/@types/estree/index.d.ts","./node_modules/rollup/dist/rollup.d.ts","./node_modules/rollup/dist/parseAst.d.ts","./node_modules/vite/types/hmrPayload.d.ts","./node_modules/vite/types/customEvent.d.ts","./node_modules/vite/types/hot.d.ts","./node_modules/vite/dist/node/moduleRunnerTransport.d-DJ_mE5sf.d.ts","./node_modules/vite/dist/node/module-runner.d.ts","./node_modules/esbuild/lib/main.d.ts","./node_modules/source-map-js/source-map.d.ts","./node_modules/postcss/lib/previous-map.d.ts","./node_modules/postcss/lib/input.d.ts","./node_modules/postcss/lib/css-syntax-error.d.ts","./node_modules/postcss/lib/declaration.d.ts","./node_modules/postcss/lib/root.d.ts","./node_modules/postcss/lib/warning.d.ts","./node_modules/postcss/lib/lazy-result.d.ts","./node_modules/postcss/lib/no-work-result.d.ts","./node_modules/postcss/lib/processor.d.ts","./node_modules/postcss/lib/result.d.ts","./node_modules/postcss/lib/document.d.ts","./node_modules/postcss/lib/rule.d.ts","./node_modules/postcss/lib/node.d.ts","./node_modules/postcss/lib/comment.d.ts","./node_modules/postcss/lib/container.d.ts","./node_modules/postcss/lib/at-rule.d.ts","./node_modules/postcss/lib/list.d.ts","./node_modules/postcss/lib/postcss.d.ts","./node_modules/postcss/lib/postcss.d.mts","./node_modules/lightningcss/node/ast.d.ts","./node_modules/lightningcss/node/targets.d.ts","./node_modules/lightningcss/node/index.d.ts","./node_modules/vite/types/internal/lightningcssOptions.d.ts","./node_modules/vite/types/internal/cssPreprocessorOptions.d.ts","./node_modules/vite/types/importGlob.d.ts","./node_modules/vite/types/metadata.d.ts","./node_modules/vite/dist/node/index.d.ts","./node_modules/electron-vite/dist/index.d.ts","./node_modules/@babel/types/lib/index.d.ts","./node_modules/@types/babel__generator/index.d.ts","./node_modules/@babel/parser/typings/babel-parser.d.ts","./node_modules/@types/babel__template/index.d.ts","./node_modules/@types/babel__traverse/index.d.ts","./node_modules/@types/babel__core/index.d.ts","./node_modules/@vitejs/plugin-react/dist/index.d.ts","./node_modules/@tailwindcss/vite/dist/index.d.mts","./electron.vite.config.ts","./node_modules/electron/electron.d.ts","./node_modules/node-pty/typings/node-pty.d.ts","./node_modules/@electron-toolkit/utils/dist/index.d.ts","./src/main/index.ts","./node_modules/@electron-toolkit/preload/dist/index.d.ts","./src/preload/index.ts","./node_modules/electron-vite/node.d.ts"],"fileIdsList":[[89,153,161,165,168,170,171,172,177,185,250,257,258],[89,153,161,165,168,170,171,172,185,251],[89,153,161,165,168,170,171,172,185],[89,153,161,165,168,170,171,172,185,260],[89,153,161,165,168,170,171,172,185,249],[89,153,161,165,168,170,171,172,185,251,252,253,254,255],[89,153,161,165,168,170,171,172,185,251,253],[89,150,151,153,161,165,168,170,171,172,185],[89,152,153,161,165,168,170,171,172,185],[153,161,165,168,170,171,172,185],[89,153,161,165,168,170,171,172,185,194],[89,153,154,159,161,164,165,168,170,171,172,174,185,190,203],[89,153,154,155,161,164,165,168,170,171,172,185],[89,153,156,161,165,168,170,171,172,185,204],[89,153,157,158,161,165,168,170,171,172,176,185],[89,153,158,161,165,168,170,171,172,185,190,200],[89,153,159,161,164,165,168,170,171,172,174,185],[89,152,153,160,161,165,168,170,171,172,185],[89,153,161,162,165,168,170,171,172,185],[89,153,161,163,164,165,168,170,171,172,185],[89,152,153,161,164,165,168,170,171,172,185],[89,153,161,164,165,166,168,170,171,172,185,190,203],[89,153,161,164,165,166,168,170,171,172,185,190,192,194],[89,140,153,161,164,165,167,168,170,171,172,174,185,190,203],[89,153,161,164,165,167,168,170,171,172,174,185,190,200,203],[89,153,161,165,167,168,169,170,171,172,185,190,200,203],[87,88,89,90,91,92,93,94,95,96,97,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,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],[89,153,161,164,165,168,170,171,172,185],[89,153,161,165,168,170,172,185],[89,153,161,165,168,170,171,172,173,185,203],[89,153,161,164,165,168,170,171,172,174,185,190],[89,153,161,165,168,170,171,172,176,185],[89,153,161,165,168,170,171,172,177,185],[89,153,161,164,165,168,170,171,172,180,185],[89,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,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210],[89,153,161,165,168,170,171,172,182,185],[89,153,161,165,168,170,171,172,183,185],[89,153,158,161,165,168,170,171,172,174,185,194],[89,153,161,164,165,168,170,171,172,185,186],[89,153,161,165,168,170,171,172,185,187,204,207],[89,153,161,164,165,168,170,171,172,185,190,193,194],[89,153,161,165,168,170,171,172,185,191,194],[89,153,161,165,168,170,171,172,185,192],[89,153,161,165,168,170,171,172,185,194,204],[89,153,161,165,168,170,171,172,185,195],[89,150,153,161,165,168,170,171,172,185,190,197,203],[89,153,161,165,168,170,171,172,185,190,196],[89,153,161,164,165,168,170,171,172,185,198,199],[89,153,161,165,168,170,171,172,185,198,199],[89,153,158,161,165,168,170,171,172,174,185,190,200],[89,153,161,165,168,170,171,172,185,201],[89,153,161,165,168,170,171,172,174,185,202],[89,153,161,165,167,168,170,171,172,183,185,203],[89,153,161,165,168,170,171,172,185,204,205],[89,153,158,161,165,168,170,171,172,185,205],[89,153,161,165,168,170,171,172,185,190,206],[89,153,161,165,168,170,171,172,173,185,207],[89,153,161,165,168,170,171,172,185,208],[89,153,156,161,165,168,170,171,172,185],[89,153,158,161,165,168,170,171,172,185],[89,153,161,165,168,170,171,172,185,204],[89,140,153,161,165,168,170,171,172,185],[89,153,161,165,168,170,171,172,185,203],[89,153,161,165,168,170,171,172,185,209],[89,153,161,165,168,170,171,172,180,185],[89,153,161,165,168,170,171,172,185,199],[89,140,153,161,164,165,166,168,170,171,172,180,185,190,194,203,206,207,209],[89,153,161,165,168,170,171,172,185,190,210],[89,153,161,165,168,170,171,172,185,192,211],[89,153,161,165,168,170,171,172,185,249,256],[89,153,161,164,165,168,170,171,172,185,212],[89,153,161,165,168,170,171,172,185,242,243],[89,153,161,165,168,170,171,172,185,237],[89,153,161,165,168,170,171,172,185,235,237],[89,153,161,165,168,170,171,172,185,226,234,235,236,238,240],[89,153,161,165,168,170,171,172,185,224],[89,153,161,165,168,170,171,172,185,227,232,237,240],[89,153,161,165,168,170,171,172,185,223,240],[89,153,161,165,168,170,171,172,185,227,228,231,232,233,240],[89,153,161,165,168,170,171,172,185,227,228,229,231,232,240],[89,153,161,165,168,170,171,172,185,224,225,226,227,228,232,233,234,236,237,238,240],[89,153,161,165,168,170,171,172,185,240],[89,153,161,165,168,170,171,172,185,222,224,225,226,227,228,229,231,232,233,234,235,236,237,238,239],[89,153,161,165,168,170,171,172,185,222,240],[89,153,161,165,168,170,171,172,185,227,229,230,232,233,240],[89,153,161,165,168,170,171,172,185,231,240],[89,153,161,165,168,170,171,172,185,232,233,237,240],[89,153,161,165,168,170,171,172,185,225,235],[89,153,161,165,168,170,171,172,185,214,248,249],[89,153,161,165,168,170,171,172,185,213,214],[89,104,107,110,111,153,161,165,168,170,171,172,185,203],[89,107,153,161,165,168,170,171,172,185,190,203],[89,107,111,153,161,165,168,170,171,172,185,203],[89,153,161,165,168,170,171,172,185,190],[89,101,153,161,165,168,170,171,172,185],[89,105,153,161,165,168,170,171,172,185],[89,103,104,107,153,161,165,168,170,171,172,185,203],[89,153,161,165,168,170,171,172,174,185,200],[89,153,161,165,168,170,171,172,185,212],[89,101,153,161,165,168,170,171,172,185,212],[89,103,107,153,161,165,168,170,171,172,174,185,203],[89,98,99,100,102,106,153,161,164,165,168,170,171,172,185,190,203],[89,107,116,124,153,161,165,168,170,171,172,185],[89,99,105,153,161,165,168,170,171,172,185],[89,107,134,135,153,161,165,168,170,171,172,185],[89,99,102,107,153,161,165,168,170,171,172,185,194,203,212],[89,107,153,161,165,168,170,171,172,185],[89,103,107,153,161,165,168,170,171,172,185,203],[89,98,153,161,165,168,170,171,172,185],[89,101,102,103,105,106,107,108,109,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,135,136,137,138,139,153,161,165,168,170,171,172,185],[89,107,127,130,153,161,165,168,170,171,172,185],[89,107,116,117,118,153,161,165,168,170,171,172,185],[89,105,107,117,119,153,161,165,168,170,171,172,185],[89,106,153,161,165,168,170,171,172,185],[89,99,101,107,153,161,165,168,170,171,172,185],[89,107,111,117,119,153,161,165,168,170,171,172,185],[89,111,153,161,165,168,170,171,172,185],[89,105,107,110,153,161,165,168,170,171,172,185,203],[89,99,103,107,116,153,161,165,168,170,171,172,185],[89,107,127,153,161,165,168,170,171,172,185],[89,119,153,161,165,168,170,171,172,185],[89,99,103,107,111,153,161,165,168,170,171,172,185],[89,101,107,134,153,161,165,168,170,171,172,185,194,209,212],[89,153,161,164,165,167,168,169,170,171,172,174,185,190,200,203,210,212,214,215,216,217,219,220,221,241,245,246,247,248,249],[89,153,161,165,168,170,171,172,185,216,217,218,219],[89,153,161,165,168,170,171,172,185,216],[89,153,161,165,168,170,171,172,185,217],[89,153,161,165,168,170,171,172,185,244],[89,153,161,165,168,170,171,172,185,214,249],[89,153,154,158,161,165,166,167,168,169,170,171,172,176,177,185,260,261,262],[89,153,161,165,168,170,171,172,185,260,264]],"fileInfos":[{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"27bdc30a0e32783366a5abeda841bc22757c1797de8681bbe81fbc735eeb1c10","impliedFormat":1},{"version":"8fd575e12870e9944c7e1d62e1f5a73fcf23dd8d3a321f2a2c74c20d022283fe","impliedFormat":1},{"version":"2ab096661c711e4a81cc464fa1e6feb929a54f5340b46b0a07ac6bbf857471f0","impliedFormat":1},{"version":"080941d9f9ff9307f7e27a83bcd888b7c8270716c39af943532438932ec1d0b9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2e80ee7a49e8ac312cc11b77f1475804bee36b3b2bc896bead8b6e1266befb43","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7a3c8b952931daebdfc7a2897c53c0a1c73624593fa070e46bd537e64dcd20a","affectsGlobalScope":true,"impliedFormat":1},{"version":"80e18897e5884b6723488d4f5652167e7bb5024f946743134ecc4aa4ee731f89","affectsGlobalScope":true,"impliedFormat":1},{"version":"cd034f499c6cdca722b60c04b5b1b78e058487a7085a8e0d6fb50809947ee573","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"8cdf8847677ac7d20486e54dd3fcf09eda95812ac8ace44b4418da1bbbab6eb8","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"df83c2a6c73228b625b0beb6669c7ee2a09c914637e2d35170723ad49c0f5cd4","affectsGlobalScope":true,"impliedFormat":1},{"version":"436aaf437562f276ec2ddbee2f2cdedac7664c1e4c1d2c36839ddd582eeb3d0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e3c06ea092138bf9fa5e874a1fdbc9d54805d074bee1de31b99a11e2fec239d","affectsGlobalScope":true,"impliedFormat":1},{"version":"87dc0f382502f5bbce5129bdc0aea21e19a3abbc19259e0b43ae038a9fc4e326","affectsGlobalScope":true,"impliedFormat":1},{"version":"b1cb28af0c891c8c96b2d6b7be76bd394fddcfdb4709a20ba05a7c1605eea0f9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2fef54945a13095fdb9b84f705f2b5994597640c46afeb2ce78352fab4cb3279","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac77cb3e8c6d3565793eb90a8373ee8033146315a3dbead3bde8db5eaf5e5ec6","affectsGlobalScope":true,"impliedFormat":1},{"version":"56e4ed5aab5f5920980066a9409bfaf53e6d21d3f8d020c17e4de584d29600ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ece9f17b3866cc077099c73f4983bddbcb1dc7ddb943227f1ec070f529dedd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a6282c8827e4b9a95f4bf4f5c205673ada31b982f50572d27103df8ceb8013c","affectsGlobalScope":true,"impliedFormat":1},{"version":"1c9319a09485199c1f7b0498f2988d6d2249793ef67edda49d1e584746be9032","affectsGlobalScope":true,"impliedFormat":1},{"version":"e3a2a0cee0f03ffdde24d89660eba2685bfbdeae955a6c67e8c4c9fd28928eeb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811c71eee4aa0ac5f7adf713323a5c41b0cf6c4e17367a34fbce379e12bbf0a4","affectsGlobalScope":true,"impliedFormat":1},{"version":"51ad4c928303041605b4d7ae32e0c1ee387d43a24cd6f1ebf4a2699e1076d4fa","affectsGlobalScope":true,"impliedFormat":1},{"version":"60037901da1a425516449b9a20073aa03386cce92f7a1fd902d7602be3a7c2e9","affectsGlobalScope":true,"impliedFormat":1},{"version":"d4b1d2c51d058fc21ec2629fff7a76249dec2e36e12960ea056e3ef89174080f","affectsGlobalScope":true,"impliedFormat":1},{"version":"22adec94ef7047a6c9d1af3cb96be87a335908bf9ef386ae9fd50eeb37f44c47","affectsGlobalScope":true,"impliedFormat":1},{"version":"196cb558a13d4533a5163286f30b0509ce0210e4b316c56c38d4c0fd2fb38405","affectsGlobalScope":true,"impliedFormat":1},{"version":"73f78680d4c08509933daf80947902f6ff41b6230f94dd002ae372620adb0f60","affectsGlobalScope":true,"impliedFormat":1},{"version":"c5239f5c01bcfa9cd32f37c496cf19c61d69d37e48be9de612b541aac915805b","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"bde31fd423cd93b0eff97197a3f66df7c93e8c0c335cbeb113b7ff1ac35c23f4","impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"0ccdaa19852d25ecd84eec365c3bfa16e7859cadecf6e9ca6d0dbbbee439743f","affectsGlobalScope":true,"impliedFormat":1},{"version":"cc2110f7decca6bfb9392e30421cfa1436479e4a6756e8fec6cbc22625d4f881","affectsGlobalScope":true,"impliedFormat":1},{"version":"096116f8fedc1765d5bd6ef360c257b4a9048e5415054b3bf3c41b07f8951b0b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e5e01375c9e124a83b52ee4b3244ed1a4d214a6cfb54ac73e164a823a4a7860a","affectsGlobalScope":true,"impliedFormat":1},{"version":"f90ae2bbce1505e67f2f6502392e318f5714bae82d2d969185c4a6cecc8af2fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b58e207b93a8f1c88bbf2a95ddc686ac83962b13830fe8ad3f404ffc7051fb4","affectsGlobalScope":true,"impliedFormat":1},{"version":"1fefabcb2b06736a66d2904074d56268753654805e829989a46a0161cd8412c5","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"c18a99f01eb788d849ad032b31cafd49de0b19e083fe775370834c5675d7df8e","affectsGlobalScope":true,"impliedFormat":1},{"version":"5247874c2a23b9a62d178ae84f2db6a1d54e6c9a2e7e057e178cc5eea13757fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"cdcf9ea426ad970f96ac930cd176d5c69c6c24eebd9fc580e1572d6c6a88f62c","impliedFormat":1},{"version":"23cd712e2ce083d68afe69224587438e5914b457b8acf87073c22494d706a3d0","impliedFormat":1},{"version":"156a859e21ef3244d13afeeba4e49760a6afa035c149dda52f0c45ea8903b338","impliedFormat":1},{"version":"10ec5e82144dfac6f04fa5d1d6c11763b3e4dbbac6d99101427219ab3e2ae887","impliedFormat":1},{"version":"615754924717c0b1e293e083b83503c0a872717ad5aa60ed7f1a699eb1b4ea5c","impliedFormat":1},{"version":"14e9acf826baba0ef4b5665704084896e7bcc06f65a9ab13af7e93d27d6b7069","impliedFormat":1},{"version":"68834d631c8838c715f225509cfc3927913b9cc7a4870460b5b60c8dbdb99baf","impliedFormat":1},{"version":"21adf13435b9b748529c8cedf80f884e5130b9684188120a686cd2b26a2059c7","impliedFormat":1},{"version":"eec76bf6b9346f3f95fa402621b889489e96930e72295b0369022f332e9b4a6a","impliedFormat":1},{"version":"0ecd58f413f9bc3b7d4383eae31b0c8fc576985cd7404d6f99f8c643543ade74","impliedFormat":1},{"version":"ea6bc8de8b59f90a7a3960005fd01988f98fd0784e14bc6922dde2e93305ec7d","impliedFormat":1},{"version":"36107995674b29284a115e21a0618c4c2751b32a8766dd4cb3ba740308b16d59","impliedFormat":1},{"version":"914a0ae30d96d71915fc519ccb4efbf2b62c0ddfb3a3fc6129151076bc01dc60","impliedFormat":1},{"version":"9c32412007b5662fd34a8eb04292fb5314ec370d7016d1c2fb8aa193c807fe22","impliedFormat":1},{"version":"7fd1b31fd35876b0aa650811c25ec2c97a3c6387e5473eb18004bed86cdd76b6","impliedFormat":1},{"version":"4d327f7d72ad0918275cea3eee49a6a8dc8114ae1d5b7f3f5d0774de75f7439a","impliedFormat":1},{"version":"6ebe8ebb8659aaa9d1acbf3710d7dae3e923e97610238b9511c25dc39023a166","impliedFormat":1},{"version":"e85d7f8068f6a26710bff0cc8c0fc5e47f71089c3780fbede05857331d2ddec9","impliedFormat":1},{"version":"7befaf0e76b5671be1d47b77fcc65f2b0aad91cc26529df1904f4a7c46d216e9","impliedFormat":1},{"version":"0a60a292b89ca7218b8616f78e5bbd1c96b87e048849469cccb4355e98af959a","impliedFormat":1},{"version":"0b6e25234b4eec6ed96ab138d96eb70b135690d7dd01f3dd8a8ab291c35a683a","impliedFormat":1},{"version":"9666f2f84b985b62400d2e5ab0adae9ff44de9b2a34803c2c5bd3c8325b17dc0","impliedFormat":1},{"version":"40cd35c95e9cf22cfa5bd84e96408b6fcbca55295f4ff822390abb11afbc3dca","impliedFormat":1},{"version":"b1616b8959bf557feb16369c6124a97a0e74ed6f49d1df73bb4b9ddf68acf3f3","impliedFormat":1},{"version":"5b03a034c72146b61573aab280f295b015b9168470f2df05f6080a2122f9b4df","impliedFormat":1},{"version":"40b463c6766ca1b689bfcc46d26b5e295954f32ad43e37ee6953c0a677e4ae2b","impliedFormat":1},{"version":"249b9cab7f5d628b71308c7d9bb0a808b50b091e640ba3ed6e2d0516f4a8d91d","impliedFormat":1},{"version":"d33ce35e3f9cfcc1d94eca415bdd3bde94d5b153ffdd33e6c4455c029986c630","impliedFormat":1},{"version":"80aae6afc67faa5ac0b32b5b8bc8cc9f7fa299cff15cf09cc2e11fd28c6ae29e","impliedFormat":1},{"version":"f473cd2288991ff3221165dcf73cd5d24da30391f87e85b3dd4d0450c787a391","impliedFormat":1},{"version":"499e5b055a5aba1e1998f7311a6c441a369831c70905cc565ceac93c28083d53","impliedFormat":1},{"version":"8aee8b6d4f9f62cf3776cda1305fb18763e2aade7e13cea5bbe699112df85214","impliedFormat":1},{"version":"98498b101803bb3dde9f76a56e65c14b75db1cc8bec5f4db72be541570f74fc5","impliedFormat":1},{"version":"4dc59f6e1dbf3d5f66660fceabe6c174d3261b37b696ae1854f0dbaf255fc753","impliedFormat":1},{"version":"5d0375ca7310efb77e3ef18d068d53784faf62705e0ad04569597ae0e755c401","impliedFormat":1},{"version":"59af37caec41ecf7b2e76059c9672a49e682c1a2aa6f9d7dc78878f53aa284d6","impliedFormat":1},{"version":"addf417b9eb3f938fddf8d81e96393a165e4be0d4a8b6402292f9c634b1cb00d","impliedFormat":1},{"version":"436d7b4543b340b0f3eef4310d524242e41369b9652aa9c70428767c4dcac455","impliedFormat":1},{"version":"adf27937dba6af9f08a68c5b1d3fce0ca7d4b960c57e6d6c844e7d1a8e53adae","impliedFormat":1},{"version":"12950411eeab8563b349cb7959543d92d8d02c289ed893d78499a19becb5a8cc","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"114f493b30f364255290472111b5a4791d5902c308645670cd0401429cbc6930","impliedFormat":1},{"version":"c3f5289820990ab66b70c7fb5b63cb674001009ff84b13de40619619a9c8175f","affectsGlobalScope":true,"impliedFormat":1},{"version":"b3275d55fac10b799c9546804126239baf020d220136163f763b55a74e50e750","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa68a0a3b7cb32c00e39ee3cd31f8f15b80cac97dce51b6ee7fc14a1e8deb30b","affectsGlobalScope":true,"impliedFormat":1},{"version":"1cf059eaf468efcc649f8cf6075d3cb98e9a35a0fe9c44419ec3d2f5428d7123","affectsGlobalScope":true,"impliedFormat":1},{"version":"6c36e755bced82df7fb6ce8169265d0a7bb046ab4e2cb6d0da0cb72b22033e89","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"7a93de4ff8a63bafe62ba86b89af1df0ccb5e40bb85b0c67d6bbcfdcf96bf3d4","affectsGlobalScope":true,"impliedFormat":1},{"version":"90e85f9bc549dfe2b5749b45fe734144e96cd5d04b38eae244028794e142a77e","affectsGlobalScope":true,"impliedFormat":1},{"version":"e0a5deeb610b2a50a6350bd23df6490036a1773a8a71d70f2f9549ab009e67ee","affectsGlobalScope":true,"impliedFormat":1},{"version":"d2ae155afe8a01cc0ae612d99117cf8ef16692ba7c4366590156fdec1bcf2d8c","impliedFormat":1},{"version":"3f5e5d9be35913db9fea42a63f3df0b7e3c8703b97670a2125587b4dbbd56d7c","impliedFormat":1},{"version":"c8b8968311ec4e5e97b7b5fb8a65efaba455db9bdcfd7fff7fb15f6e317bfba0","impliedFormat":1},{"version":"57c23df0b5f7a8e26363a3849b0bc7763f6b241207157c8e40089d1df4116f35","affectsGlobalScope":true,"impliedFormat":1},{"version":"3b8bc0c17b54081b0878673989216229e575d67a10874e84566a21025a2461ee","impliedFormat":1},{"version":"5b0db5a58b73498792a29bfebc333438e61906fef75da898b410e24e52229e6f","impliedFormat":1},{"version":"dbe055b2b29a7bab2c1ca8f259436306adb43f469dca7e639a02cd3695d3f621","impliedFormat":1},{"version":"1678b04557dca52feab73cc67610918a7f5e25bfdba3e7fa081acd625d93106d","impliedFormat":1},{"version":"aecbf1d9e6a18dab7d92ef8a89a1444b47e1eb6134cb2bb776a26d55ff58c29a","impliedFormat":1},{"version":"2ea729503db9793f2691162fec3dd1118cab62e96d025f8eeb376d43ec293395","impliedFormat":1},{"version":"9ec87fea42b92894b0f209931a880789d43c3397d09dd99c631ae40a2f7071d1","impliedFormat":1},{"version":"c68e88cdfadfb6c8ba5fc38e58a3a166b0beae77b1f05b7d921150a32a5ffb8d","impliedFormat":1},{"version":"2bc7aa4fba46df0bd495425a7c8201437a7d465f83854fac859df2d67f664df3","impliedFormat":1},{"version":"41d17e1ad9a002feb11c8cdd2777e5bbc0cdb1e3f595d237e4dded0b6949983b","impliedFormat":1},{"version":"1fede9296beac11ce8e6b425396a1791f64341f2be85deebb6286faf6e16306e","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce697b6a251d9cad53998c7fd3098072df883b525ec45d83530e434dc6d80dc6","impliedFormat":1},{"version":"719412f054e6ecc35489462c9a21bab0323d173a7d04e55b0ace4b5d86fbeb07","impliedFormat":1},{"version":"0eb5d0cbf09de5d34542b977fd6a933bb2e0817bffe8e1a541b2f1ad1b9af1ff","impliedFormat":1},{"version":"fac3e88881b35d3a757ed891ac912b2674792c25e2a1a74e1f5fbc72d19a9792","impliedFormat":1},{"version":"2c2bdaa1d8ead9f68628d6d9d250e46ee8e81aa4898b4769a36956ae15e060fe","impliedFormat":1},{"version":"c32c840c62d8bd7aeb3147aa6754cd2d922b990a6b6634530cb2ebdce5adc8e9","impliedFormat":1},{"version":"5ff4433a2deae4f85ab1377e90a7554ce6b47ae51c69a84ca30a6e22fae85834","impliedFormat":1},{"version":"82b91e4e42e6c41bc7fc1b6c2dc5eba6a2ba98375eb1f210e6ff6bba2d54177e","impliedFormat":1},{"version":"c1fa52b3d014001e8662fa2669d90ea15373958a288e3b83a3b621733d25292a","affectsGlobalScope":true,"impliedFormat":1},{"version":"cbed824fec91efefc7bbdcb8b43d1a531fdbebd0e2ef19481501ff365a93cb70","impliedFormat":1},{"version":"8e9c23ba78aabc2e0a27033f18737a6df754067731e69dc5f52823957d60a4b6","impliedFormat":1},{"version":"d0716593b3f2b0451bcf0c24cfa86dec2235c325c89f201934248b7c742715fc","impliedFormat":1},{"version":"ec501101c2a96133a6c695f934c8f6642149cc728571b29cbb7b770984c1088e","impliedFormat":1},{"version":"b214ebcf76c51b115453f69729ee8aa7b7f8eccdae2a922b568a45c2d7ff52f7","impliedFormat":1},{"version":"429c9cdfa7d126255779efd7e6d9057ced2d69c81859bbab32073bad52e9ba76","impliedFormat":1},{"version":"2991bca2cc0f0628a278df2a2ccdb8d6cbcb700f3761abbed62bba137d5b1790","impliedFormat":1},{"version":"5e66972e83eb4dc7123939bf816e6cbd9ad81af5552db1cab84e6bd9c64d2ecc","affectsGlobalScope":true,"impliedFormat":1},{"version":"230763250f20449fa7b3c9273e1967adb0023dc890d4be1553faca658ee65971","impliedFormat":1},{"version":"c3e9078b60cb329d1221f5878e88cecfa3e74460550e605a58fcfb41a66029ff","impliedFormat":1},{"version":"8413d0641f293aed551c7464615b770d34a02dedede889b9591172287d68e773","impliedFormat":1},{"version":"0ea59f7d3e51440baa64f429253759b106cfcbaf51e474cae606e02265b37cf8","impliedFormat":1},{"version":"bc18a1991ba681f03e13285fa1d7b99b03b67ee671b7bc936254467177543890","impliedFormat":1},{"version":"1b241e24f3227d078c06aeda6e050187ad59a4e591f4467abed44d92b084e08d","impliedFormat":1},{"version":"fa94bbf532b7af8f394b95fa310980d6e20bd2d4c871c6a6cb9f70f03750a44b","impliedFormat":1},{"version":"7fde0e1be5c8be204ffbf428abfcf01da2eb0f130e1bc3f539eb7275f4fd1f58","impliedFormat":1},{"version":"e284328553df5f425a5d33d36a0c3fa66b46af9d097cad6f4d2e8696dfdeb0f1","affectsGlobalScope":true,"impliedFormat":1},{"version":"7fa2214bb0d64701bc6f9ce8cde2fd2ff8c571e0b23065fa04a8a5a6beb91511","impliedFormat":1},{"version":"b58040a9d317060d1a5f6e78bb8bd125894fb660feea534fe0e733cf5fdd7d07","impliedFormat":1},{"version":"f1c93e046fb3d9b7f8249629f4b63dc068dd839b824dd0aa39a5e68476dc9420","impliedFormat":1},{"version":"016b29bf4926b80255a108c53a1451717350059da04fcae64d1075f5e93bbb39","impliedFormat":1},{"version":"841983e39bd4cbb463be385e92fda11057cab368bf27100a801c492f1d86cbaa","impliedFormat":1},{"version":"1c4f139ade4f6ebf45463505f8155173e5d7a5305e50e0aae0a5e712d6ff3b48","impliedFormat":1},{"version":"e16b319e5aca1031168de823c4946ff8e29629c4c8cc0ec0fcfe2a8ab2155043","impliedFormat":1},{"version":"e4156ddb25aa0e3b5303d372f26957b36778f0f6bbd4326359269873295e3058","affectsGlobalScope":true,"impliedFormat":1},{"version":"cc1b433a84cae05ddc5672d4823170af78606ad21ecef60dbc4570190cbf1357","impliedFormat":1},{"version":"9d3821bc75c59577e52643324cec92fc2145642e8d17cf7ee07a3181f21d985d","impliedFormat":1},{"version":"7f78cfb2b343838612c192cb251746e3a7c62ac7675726a47e130d9b213f6580","impliedFormat":1},{"version":"201db9cf1687fab1adf5282fcba861f382b32303dc4f67c89d59655e78a25461","impliedFormat":1},{"version":"2c3c5c0f54055e87640f5d233716fd889f3034fc7911d603b642369b0dbeb2a7","impliedFormat":1},{"version":"0a20eaf2e4b1e3c1e1f87f7bccb0c936375b23b022baeea750519b7c9bc6ce83","impliedFormat":1},{"version":"b484ec11ba00e3a2235562a41898d55372ccabe607986c6fa4f4aba72093749f","impliedFormat":1},{"version":"a16b91b27bd6b706c687c88cbc8a7d4ee98e5ed6043026d6b84bda923c0aed67","impliedFormat":1},{"version":"1c9e5b1a17b1fc9b3711fb36e0690421261ab2880f15b145155b5b2ba2ab6c2d","impliedFormat":1},{"version":"99ab6d0d660ce4d21efb52288a39fd35bb3f556980ec5463b1ae8f304a3bbc85","impliedFormat":1},{"version":"6eeded8c7e352be6e0efb83f4935ec752513c4d22043b52522b90849a49a3a11","impliedFormat":1},{"version":"6c1ad90050ffbb151cacc68e2d06ea1a26a945659391e32651f5d42b86fd7f2c","impliedFormat":1},{"version":"afa1c49f8e559e413d57343339db857d2a8159435cf9cf7d4deb41718fff1b88","impliedFormat":1},{"version":"6953d7597831d0860c7034cf4f0419687d263b6b98a4b32e37ce6d49615c36e2","impliedFormat":1},{"version":"151ff381ef9ff8da2da9b9663ebf657eac35c4c9a19183420c05728f31a6761d","impliedFormat":1},{"version":"ee70b8037ecdf0de6c04f35277f253663a536d7e38f1539d270e4e916d225a3f","affectsGlobalScope":true,"impliedFormat":1},{"version":"a660aa95476042d3fdcc1343cf6bb8fdf24772d31712b1db321c5a4dcc325434","impliedFormat":1},{"version":"a7ca8df4f2931bef2aa4118078584d84a0b16539598eaadf7dce9104dfaa381c","impliedFormat":1},{"version":"11443a1dcfaaa404c68d53368b5b818712b95dd19f188cab1669c39bee8b84b3","impliedFormat":1},{"version":"36977c14a7f7bfc8c0426ae4343875689949fb699f3f84ecbe5b300ebf9a2c55","impliedFormat":1},{"version":"035d0934d304483f07148427a5bd5b98ac265dae914a6b49749fe23fbd893ec7","impliedFormat":99},{"version":"e2ed5b81cbed3a511b21a18ab2539e79ac1f4bc1d1d28f8d35d8104caa3b429f","impliedFormat":99},{"version":"161c8e0690c46021506e32fda85956d785b70f309ae97011fd27374c065cac9b","affectsGlobalScope":true,"impliedFormat":1},{"version":"402e5c534fb2b85fa771170595db3ac0dd532112c8fa44fc23f233bc6967488b","impliedFormat":1},{"version":"52dcc257df5119fb66d864625112ce5033ac51a4c2afe376a0b299d2f7f76e4a","impliedFormat":1},{"version":"e5bab5f871ef708d52d47b3e5d0aa72a08ee7a152f33931d9a60809711a2a9a3","impliedFormat":1},{"version":"e16dc2a81595736024a206c7d5c8a39bfe2e6039208ef29981d0d95434ba8fcf","impliedFormat":1},{"version":"cc4a4903fb698ca1d961d4c10dce658aa3a479faf40509d526f122b044eaf6a4","impliedFormat":1},{"version":"19ee8416e6473ed6c7adb868fa796b5653cf0fa2a337658e677eaa0d134388c3","impliedFormat":1},{"version":"1328ab4e442614b28cdb3d4b414cf68325c0da0dca07287a338d0654b7a00261","impliedFormat":1},{"version":"a039dc21f045919f3cbee2ec13812cc6cc3eebc99dae4be00973230f468d19a6","impliedFormat":1},{"version":"3fbe57af01460e49dcd29df55d6931e1672bc6f1be0fb073d11410bc16f9037d","impliedFormat":1},{"version":"f760be449e8562ec5c09bb5187e8e1eabf3c113c0c58cddda53ef8c69f3e2131","impliedFormat":1},{"version":"44325ed13294fce6ab825b82947bbeed2611db7dad9d9135260192f375e5a189","impliedFormat":1},{"version":"e392e8fb5b514eafc585601c1d781485aa6dd6a320e75daf1064a4c6918a1b45","impliedFormat":1},{"version":"46e4a36e8ddbdfb4e7330e11c81c970dc8b218611df9183d39c41c5f8c653b55","impliedFormat":1},{"version":"370bde134aa8c2abc926d0e99d3a4d5d5dba65c6ee65459137e4f02670cbf841","impliedFormat":1},{"version":"6332f565867cf4a740a70e30f31cefba37ef7cebcf74f22eab8d744fde6d193e","impliedFormat":1},{"version":"2977b7884aedc895a1d0c9c210c7cf3272c29d6959a08a6fa3ff71e0aff08175","impliedFormat":1},{"version":"17f2922d41ddd032830a91371c948cd9ce903b35c95adca72271a54584f19b0b","impliedFormat":1},{"version":"3eed76ede2a1a14d7c9bb0a642041282dcc264811139d3dd275c9fe14efc9840","impliedFormat":1},{"version":"e3cf0611709328b449ec13f8c436712d62003620ce480139fae46ce001c2ee9f","impliedFormat":1},{"version":"8d369483f0c2b9ee388129cfdb6a43bc8112b377e86a41884bd06e19ce04f4c1","impliedFormat":99},{"version":"3fd8a5aefd8c3feb3936ca66f5aa89dff7bf6e6537b4158dbd0f6e0d65ed3b9e","impliedFormat":1},{"version":"a18642ddf216f162052a16cba0944892c4c4c977d3306a87cb673d46abbb0cbf","impliedFormat":1},{"version":"41c41c6e90133bb2a14f7561f29944771886e5535945b2b372e2f6ed6987746e","impliedFormat":1},{"version":"4ec16d7a4e366c06a4573d299e15fe6207fc080f41beac5da06f4af33ea9761e","impliedFormat":1},{"version":"7870becb94cbc11d2d01b77c4422589adcba4d8e59f726246d40cd0d129784d8","affectsGlobalScope":true,"impliedFormat":1},{"version":"7f698624bbbb060ece7c0e51b7236520ebada74b747d7523c7df376453ed6fea","impliedFormat":1},{"version":"f70b8328a15ca1d10b1436b691e134a49bc30dcf3183a69bfaa7ba77e1b78ecd","impliedFormat":1},{"version":"d9030fc0c412a31e7e13d189b9ad032b5177c20217add0f24fd3fff0cf272882","impliedFormat":99},{"version":"e7ba74900e0edb9cb93edb1547cbb65238d26af0e5f10d24be3a61d862262cfd","impliedFormat":99},{"version":"556ccd493ec36c7d7cb130d51be66e147b91cc1415be383d71da0f1e49f742a9","impliedFormat":1},{"version":"b6d03c9cfe2cf0ba4c673c209fcd7c46c815b2619fd2aad59fc4229aaef2ed43","impliedFormat":1},{"version":"95aba78013d782537cc5e23868e736bec5d377b918990e28ed56110e3ae8b958","impliedFormat":1},{"version":"670a76db379b27c8ff42f1ba927828a22862e2ab0b0908e38b671f0e912cc5ed","impliedFormat":1},{"version":"13b77ab19ef7aadd86a1e54f2f08ea23a6d74e102909e3c00d31f231ed040f62","impliedFormat":1},{"version":"069bebfee29864e3955378107e243508b163e77ab10de6a5ee03ae06939f0bb9","impliedFormat":1},{"version":"26e0ffceb2198feb1ef460d5d14111c69ad07d44c5a67fd4bfeb74c969aa9afb","impliedFormat":99},{"version":"7757c6ca7a8ad1992401c6aff33633d6a088515be5a39d7ee188b35bfc8e5f8e","impliedFormat":99},{"version":"2d2dc284419f923a138671df4faa72f22e27e50edfb36be0965828d1e29261a2","signature":"8d916869b630869b2798d91a5cfceab8acc6247d81d4042ce14198d01ddebb60"},{"version":"94a0fcd4f758fe669bc78f1c9146f5fc07879783787364ca89ce486b0cf37920","affectsGlobalScope":true,"impliedFormat":1},{"version":"4bb071c41f8f9aaa3e1691c8c84873fa38a3067161ac0d22f085577a9e483be0","impliedFormat":1},{"version":"194779039dc8a41af2eefa2ff740594be9cf44333870346c20602031ad73f168","impliedFormat":1},{"version":"b386c7de28a17396de5b27bb98a981ad37601f3ff8c66c9f19154000c6b6129a","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"2329508bb462ba8f17cc60c4ed5b346618a42eefcaaddcbb0fcbf7f09cfd0a87","impliedFormat":1},{"version":"4d20a6153524e97d5560cb81385467a1ce5bd6c017b4b1e672ff6484926d35f0","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"2efbc5086f10635b6507392c3d9d073e94d9308a5cf08af5ae4389c250d1791c","affectsGlobalScope":true,"impliedFormat":99}],"root":[259,263,265],"options":{"composite":true,"esModuleInterop":true,"jsx":1,"module":99,"noImplicitAny":false,"noImplicitReturns":true,"noUnusedLocals":true,"noUnusedParameters":true,"skipLibCheck":true,"sourceMap":false,"strict":true,"target":99},"referencedMap":[[259,1],[253,2],[251,3],[264,4],[262,4],[258,5],[256,6],[252,2],[254,7],[255,2],[213,3],[150,8],[151,8],[152,9],[89,10],[153,11],[154,12],[155,13],[87,3],[156,14],[157,15],[158,16],[159,17],[160,18],[161,19],[162,19],[163,20],[164,21],[165,22],[166,23],[90,3],[88,3],[167,24],[168,25],[169,26],[212,27],[170,28],[171,29],[172,28],[173,30],[174,31],[176,32],[177,33],[178,33],[179,33],[180,34],[181,35],[182,36],[183,37],[184,38],[185,39],[186,39],[187,40],[188,3],[189,3],[190,41],[191,42],[192,43],[193,41],[194,44],[195,45],[196,46],[197,47],[198,48],[199,49],[200,50],[201,51],[202,52],[203,53],[204,54],[205,55],[206,56],[207,57],[208,58],[91,28],[92,3],[93,59],[94,60],[95,3],[96,61],[97,3],[141,62],[142,63],[143,64],[144,64],[145,65],[146,3],[147,11],[148,66],[149,63],[209,67],[210,68],[211,69],[257,70],[175,3],[250,5],[266,64],[260,71],[221,3],[242,3],[244,72],[243,3],[261,3],[238,73],[236,74],[237,75],[225,76],[226,74],[233,77],[224,78],[229,79],[239,3],[230,80],[235,81],[241,82],[240,83],[223,84],[231,85],[232,86],[227,87],[234,73],[228,88],[215,89],[214,90],[222,3],[84,3],[85,3],[15,3],[13,3],[14,3],[19,3],[18,3],[2,3],[20,3],[21,3],[22,3],[23,3],[24,3],[25,3],[26,3],[27,3],[3,3],[28,3],[29,3],[4,3],[30,3],[34,3],[31,3],[32,3],[33,3],[35,3],[36,3],[37,3],[5,3],[38,3],[39,3],[40,3],[41,3],[6,3],[45,3],[42,3],[43,3],[44,3],[46,3],[7,3],[47,3],[52,3],[53,3],[48,3],[49,3],[50,3],[51,3],[8,3],[57,3],[54,3],[55,3],[56,3],[58,3],[9,3],[59,3],[60,3],[61,3],[63,3],[62,3],[64,3],[65,3],[10,3],[66,3],[67,3],[68,3],[11,3],[69,3],[70,3],[71,3],[72,3],[73,3],[1,3],[74,3],[75,3],[12,3],[79,3],[77,3],[82,3],[81,3],[86,3],[76,3],[80,3],[78,3],[83,3],[17,3],[16,3],[116,91],[129,92],[113,93],[130,94],[139,95],[104,96],[105,97],[103,98],[138,99],[133,100],[137,101],[107,102],[126,103],[106,104],[136,105],[101,106],[102,100],[108,107],[109,3],[115,108],[112,107],[99,109],[140,110],[131,111],[119,112],[118,107],[120,113],[123,114],[117,115],[121,116],[134,99],[110,117],[111,118],[124,119],[100,94],[128,120],[127,107],[114,118],[122,121],[125,122],[132,3],[98,3],[135,123],[249,124],[220,125],[219,126],[217,126],[216,3],[218,127],[247,3],[246,3],[245,128],[248,129],[263,130],[265,131]],"semanticDiagnosticsPerFile":[[263,[{"start":150,"length":30,"messageText":"'ChildProcessWithoutNullStreams' is declared but its value is never read.","category":1,"code":6133,"reportsUnnecessary":true},{"start":30375,"length":20,"code":2345,"category":1,"messageText":{"messageText":"Argument of type 'string | null | undefined' is not assignable to parameter of type 'string'.","category":1,"code":2345,"next":[{"messageText":"Type 'undefined' is not assignable to type 'string'.","category":1,"code":2322}]}},{"start":56329,"length":7,"code":2349,"category":1,"messageText":{"messageText":"This expression is not callable.","category":1,"code":2349,"next":[{"messageText":"Type 'never' has no call signatures.","category":1,"code":2757}]}},{"start":63311,"length":18,"messageText":"This comparison appears to be unintentional because the types 'VisualizationStack' and '\"slam3d\"' have no overlap.","category":1,"code":2367},{"start":67069,"length":18,"messageText":"This comparison appears to be unintentional because the types 'VisualizationStack' and '\"slam3d\"' have no overlap.","category":1,"code":2367},{"start":105916,"length":18,"messageText":"This comparison appears to be unintentional because the types '\"slam\"' and '\"slam3d\"' have no overlap.","category":1,"code":2367},{"start":107698,"length":18,"messageText":"This comparison appears to be unintentional because the types '\"autoware\" | \"nav2\"' and '\"slam3d\"' have no overlap.","category":1,"code":2367},{"start":108475,"length":6,"code":2820,"category":1,"messageText":"Type '\"slam3d\"' is not assignable to type 'VisualizationStack'. Did you mean '\"slam\"'?"},{"start":121526,"length":18,"messageText":"This comparison appears to be unintentional because the types '\"autoware\" | \"nav2\"' and '\"slam3d\"' have no overlap.","category":1,"code":2367},{"start":125942,"length":18,"messageText":"This comparison appears to be unintentional because the types 'VisualizationStack' and '\"slam3d\"' have no overlap.","category":1,"code":2367},{"start":211290,"length":20,"messageText":"'scheduleStopTerminal' is declared but its value is never read.","category":1,"code":6133,"reportsUnnecessary":true}]]],"affectedFilesPendingEmit":[[259,17],263,265],"latestChangedDtsFile":"./src/preload/index.d.ts","version":"5.9.3"} \ No newline at end of file