npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@heliguy-xyz/splat-viewer-react-ui

v1.0.0-alpha.4

Published

Full-featured React UI component for 3D Gaussian Splat viewer with controls, model management, and user permissions

Downloads

270

Readme

@heliguy-xyz/splat-viewer-react-ui

A complete React UI component library for 3D Gaussian Splat viewing with full scene management and file upload capabilities.

Table of Contents

Installation

npm install @heliguy-xyz/splat-viewer-react-ui @heliguy-xyz/splat-viewer

Required peer dependencies:

  • react >= 18.0.0
  • react-dom >= 18.0.0
  • @heliguy-xyz/splat-viewer >= 1.0.0-rc.0

Quick Start

SplatSceneViewer - Full Scene Management

import { SplatSceneViewer } from '@heliguy-xyz/splat-viewer-react-ui';
import '@heliguy-xyz/splat-viewer-react-ui/dist/styles.css'; // Import styles

function App() {
  const [models, setModels] = useState([]);

  return (
    <SplatSceneViewer
      viewOnly={false}
      models={models}
      onBackClick={() => window.history.back()}
      onModelTransformChange={(modelId, transform) => {
        console.log('Transform changed:', modelId, transform);
      }}
    />
  );
}

SplatFileUploader - File Upload with Preview

import { useRef, useState } from 'react';
import {
  SplatFileUploader,
  type SplatFileUploaderHandle
} from '@heliguy-xyz/splat-viewer-react-ui';
import '@heliguy-xyz/splat-viewer-react-ui/dist/styles.css';

function UploadPage() {
  const uploaderRef = useRef<SplatFileUploaderHandle>(null);
  const [modelFile, setModelFile] = useState<File | null>(null);

  const handleSubmit = async () => {
    if (!modelFile) return;

    // Capture preview image
    const previewBlob = await uploaderRef.current?.capturePreview();

    // Upload file and preview to backend
    const formData = new FormData();
    formData.append('modelFile', modelFile);
    if (previewBlob) {
      formData.append('preview', previewBlob, 'preview.png');
    }

    await fetch('/api/upload', { method: 'POST', body: formData });
  };

  return (
    <>
      <SplatFileUploader
        ref={uploaderRef}
        onFileLoad={setModelFile}
        onRotationChange={(rotation) => console.log('Rotation:', rotation)}
      />
      <button onClick={handleSubmit} disabled={!modelFile}>
        Submit
      </button>
    </>
  );
}

Components

This package provides two main components:

SplatSceneViewer

A full-featured 3D scene viewer with model management, timeline, properties panel, and editing tools. Perfect for viewing and managing multiple 3D models in a scene.

📖 Full API Documentation

SplatFileUploader

A drag-and-drop file upload component with 3D preview and rotation controls. Perfect for uploading individual 3D models with initial rotation settings.

📖 Full API Documentation

Key Features:

  • Drag & drop file upload
  • File type validation
  • Real-time 3D preview
  • Interactive rotation controls (X, Y, Z axes)
  • Preview image capture via ref (for thumbnails/backend storage)
  • Callback-based integration
  • Automatic resource cleanup

Component API

Props

Core Props

| Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | viewOnly | boolean | false | No | When true, disables all editing features and hides edit controls | | className | string | '' | No | Additional CSS classes for the root container | | models | ModelItem[] | [] | No | Array of models to display in the timeline panel |

Navigation Callbacks

| Prop | Type | Required | Description | |------|------|----------|-------------| | onBackClick | () => void | No | Called when back button in toolbar is clicked | | onShareClick | () => void | No | Called when share button is clicked |

Lifecycle Callbacks

| Prop | Type | Required | Description | |------|------|----------|-------------| | onReady | () => void | No | Called when viewer initialization completes | | onError | (error: Error) => void | No | Called when an error occurs |

Model Management

| Prop | Type | Required | Description | |------|------|----------|-------------| | onModelImportOpen | () => void | No | Called to open model import modal. If not provided, uses native file picker | | uploadModel | (file: File) => Promise<void> | No | Function to upload model file to backend | | onModelSelect | (modelId: string \| null) => void | No | Called when a model is selected/deselected in timeline | | onModelDelete | (modelId: string) => void | No | Called after model is deleted from scene |

Scene & Transform Updates

| Prop | Type | Required | Description | |------|------|----------|-------------| | onSceneSettingsChange | (settings: Record<string, any>) => void | No | Called when scene settings change (background, FOV, grid, etc.) | | onModelTransformChange | (modelId: string, transform: ModelTransform) => void | No | Called when model transform changes (debounced at 100ms) |

Types

ModelItem

Represents a model in the scene.

interface ModelItem {
  id: string;                    // Unique identifier
  name: string;                  // Display name
  uploadedAt: string;            // ISO date string (e.g., "2026-01-15T14:30:00Z")
  visible: boolean;              // Visibility state
  info?: ModelInfo;              // Model metadata from viewer (optional)
  collisionMeshUrl?: string;     // URL to collision mesh (optional)
  url?: string;                  // URL to load model from (optional, e.g., S3, CDN)
}

Example:

const models: ModelItem[] = [
  {
    id: 'model-123',
    name: 'Building Scan.ply',
    uploadedAt: '2026-01-15T14:30:00Z',
    visible: true,
    url: 'https://my-bucket.s3.amazonaws.com/models/building.splat',
    collisionMeshUrl: 'https://cdn.example.com/collision/building.obj'
  }
];

Loading from URLs: When you provide a url field, the model will be automatically loaded from that URL when the scene opens. The URL must be publicly accessible or properly configured for CORS.

ModelTransform

Represents a model's 3D transformation.

interface ModelTransform {
  position: { x: number; y: number; z: number };
  rotation: { x: number; y: number; z: number };
  scale: { x: number; y: number; z: number };
}

Example:

const transform: ModelTransform = {
  position: { x: 0, y: 0, z: 0 },
  rotation: { x: 0, y: 45, z: 0 },
  scale: { x: 1, y: 1, z: 1 }
};

Scene Settings

Settings that can be changed via onSceneSettingsChange:

{
  backgroundColor?: string;      // Hex color (e.g., "#27272A")
  fieldOfView?: number;          // Camera FOV in degrees (30-120)
  shBands?: number;              // Spherical harmonics bands (0-3)
  flySpeed?: number;             // Fly camera speed (0.1-20)
  showGrid?: boolean;            // Grid visibility
  fakeSkyEnabled?: boolean;      // Skybox visibility
  showBound?: boolean;           // Bounding box visibility
}

Events & Callbacks

Navigation Events

onBackClick

Called when the back button in the top toolbar is clicked. Use this for navigation.

<SplatSceneViewer
  onBackClick={() => {
    router.push('/scenes'); // Next.js
    // or window.history.back();
  }}
/>

onShareClick

Called when the share button is clicked. Implement your sharing logic here.

<SplatSceneViewer
  onShareClick={() => {
    const url = window.location.href;
    navigator.clipboard.writeText(url)
      .then(() => toast.success('Link copied!'))
      .catch(() => setShowShareModal(true));
  }}
/>

Model Management Events

onModelImportOpen

Called when user clicks the import button. If not provided, falls back to native file picker.

const [showImportModal, setShowImportModal] = useState(false);

<SplatSceneViewer
  onModelImportOpen={() => setShowImportModal(true)}
  uploadModel={handleUpload}
/>

{showImportModal && (
  <ModelImportModal
    onClose={() => setShowImportModal(false)}
    onSelect={(file) => handleUpload(file)}
  />
)}

uploadModel

Function to handle model file upload. The component will load the model into the viewer after upload completes.

const handleUpload = async (file: File) => {
  try {
    const formData = new FormData();
    formData.append('file', file);
    formData.append('sceneId', sceneId);

    const response = await fetch('/api/models/upload', {
      method: 'POST',
      body: formData,
    });

    if (!response.ok) throw new Error('Upload failed');

    // Refresh models list
    const updated = await fetchModels(sceneId);
    setModels(updated);

    toast.success('Model uploaded successfully');
  } catch (error) {
    console.error('Upload error:', error);
    toast.error('Failed to upload model');
  }
};

<SplatSceneViewer uploadModel={handleUpload} />

onModelSelect

Called when a model is selected or deselected in the timeline panel.

<SplatSceneViewer
  onModelSelect={(modelId) => {
    if (modelId) {
      console.log('Model selected:', modelId);
      setSelectedModelId(modelId);
      // Fetch additional model details if needed
    } else {
      console.log('Model deselected');
      setSelectedModelId(null);
    }
  }}
/>

onModelDelete

Called after a model is successfully deleted from the scene. Update your state and backend here.

<SplatSceneViewer
  onModelDelete={async (modelId) => {
    try {
      // Delete from backend
      await fetch(`/api/scenes/${sceneId}/models/${modelId}`, {
        method: 'DELETE',
      });

      // Update local state
      setModels(models.filter(m => m.id !== modelId));

      toast.success('Model deleted');
    } catch (error) {
      console.error('Delete failed:', error);
      toast.error('Failed to delete model');
    }
  }}
/>

Transform & Settings Events

onModelTransformChange

Called when a model's transform changes. Automatically debounced at 100ms to avoid excessive API calls.

<SplatSceneViewer
  onModelTransformChange={async (modelId, transform) => {
    try {
      await fetch(`/api/scenes/${sceneId}/models/${modelId}/transform`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(transform),
      });
    } catch (error) {
      console.error('Failed to save transform:', error);
      // Optionally show error to user
    }
  }}
/>

Note: You don't need to implement your own debouncing - the component handles this internally.

onSceneSettingsChange

Called when any scene setting changes. Each setting change triggers a separate callback with a partial settings object.

<SplatSceneViewer
  onSceneSettingsChange={async (settings) => {
    try {
      await fetch(`/api/scenes/${sceneId}/settings`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(settings),
      });

      console.log('Settings updated:', settings);
      // Example: { backgroundColor: "#27272A" }
      // or: { fieldOfView: 75 }
      // or: { showGrid: true }
    } catch (error) {
      console.error('Failed to save settings:', error);
    }
  }}
/>

Usage Examples

Basic Scene Viewer

'use client';

import { useState, useEffect } from 'react';
import { SplatSceneViewer } from '@heliguy-xyz/splat-viewer-react-ui';
import type { ModelItem } from '@heliguy-xyz/splat-viewer-react-ui';

export default function ScenePage({ params }: { params: { id: string } }) {
  const [models, setModels] = useState<ModelItem[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // Fetch models from backend
    fetch(`/api/scenes/${params.id}/models`)
      .then(res => res.json())
      .then(data => {
        setModels(data);
        setLoading(false);
      });
  }, [params.id]);

  if (loading) return <div>Loading...</div>;

  return (
    <SplatSceneViewer
      models={models}
      onBackClick={() => window.history.back()}
    />
  );
}

With Full Backend Integration

'use client';

import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { SplatSceneViewer } from '@heliguy-xyz/splat-viewer-react-ui';
import type { ModelItem, ModelTransform } from '@heliguy-xyz/splat-viewer-react-ui';
import { toast } from 'sonner';

export default function ScenePage({ params }: { params: { id: string } }) {
  const router = useRouter();
  const [models, setModels] = useState<ModelItem[]>([]);
  const [viewOnly, setViewOnly] = useState(false);
  const [showImportModal, setShowImportModal] = useState(false);

  useEffect(() => {
    // Fetch initial data
    Promise.all([
      fetch(`/api/scenes/${params.id}/models`).then(r => r.json()),
      fetch(`/api/scenes/${params.id}/permissions`).then(r => r.json()),
    ]).then(([modelsData, permissionsData]) => {
      setModels(modelsData);
      setViewOnly(!permissionsData.canEdit);
    });
  }, [params.id]);

  const handleUploadModel = async (file: File) => {
    const formData = new FormData();
    formData.append('file', file);
    formData.append('sceneId', params.id);

    try {
      const response = await fetch('/api/models/upload', {
        method: 'POST',
        body: formData,
      });

      if (!response.ok) throw new Error('Upload failed');

      // Refresh models
      const updated = await fetch(`/api/scenes/${params.id}/models`).then(r => r.json());
      setModels(updated);

      toast.success('Model uploaded successfully');
      setShowImportModal(false);
    } catch (error) {
      console.error('Upload error:', error);
      toast.error('Failed to upload model');
    }
  };

  const handleModelDelete = async (modelId: string) => {
    try {
      await fetch(`/api/scenes/${params.id}/models/${modelId}`, {
        method: 'DELETE',
      });

      setModels(models.filter(m => m.id !== modelId));
      toast.success('Model deleted');
    } catch (error) {
      console.error('Delete failed:', error);
      toast.error('Failed to delete model');
    }
  };

  const handleTransformChange = async (modelId: string, transform: ModelTransform) => {
    try {
      await fetch(`/api/scenes/${params.id}/models/${modelId}/transform`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(transform),
      });
    } catch (error) {
      console.error('Failed to save transform:', error);
    }
  };

  const handleSettingsChange = async (settings: Record<string, any>) => {
    try {
      await fetch(`/api/scenes/${params.id}/settings`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(settings),
      });
    } catch (error) {
      console.error('Failed to save settings:', error);
    }
  };

  return (
    <>
      <SplatSceneViewer
        viewOnly={viewOnly}
        models={models}

        // Navigation
        onBackClick={() => router.push('/scenes')}
        onShareClick={() => {
          navigator.clipboard.writeText(window.location.href);
          toast.success('Link copied to clipboard');
        }}

        // Model management
        onModelImportOpen={() => setShowImportModal(true)}
        uploadModel={handleUploadModel}
        onModelSelect={(id) => console.log('Selected:', id)}
        onModelDelete={handleModelDelete}

        // Updates
        onModelTransformChange={handleTransformChange}
        onSceneSettingsChange={handleSettingsChange}

        // Lifecycle
        onReady={() => console.log('Viewer ready')}
        onError={(error) => {
          console.error('Viewer error:', error);
          toast.error(`Viewer error: ${error.message}`);
        }}
      />

      {showImportModal && (
        <ModelImportModal
          onClose={() => setShowImportModal(false)}
          onUpload={handleUploadModel}
        />
      )}
    </>
  );
}

Read-Only Viewer

<SplatSceneViewer
  viewOnly={true}
  models={models}
  onBackClick={() => router.push('/scenes')}
  onShareClick={() => {/* share logic */}}
/>

When viewOnly is true:

  • Edit/View toggle is hidden
  • Properties panel is hidden
  • All transform controls are disabled
  • Delete buttons are hidden
  • Model import is disabled

Styling

The component requires Tailwind CSS in your project.

1. Install Tailwind CSS

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

2. Configure Tailwind

Update your tailwind.config.js:

module.exports = {
  content: [
    './src/**/*.{js,ts,jsx,tsx,mdx}',
    './app/**/*.{js,ts,jsx,tsx,mdx}',
    // Add the package path
    './node_modules/@heliguy-xyz/splat-viewer-react-ui/**/*.{js,jsx}',
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};

3. Import Styles

// In your root layout or _app.tsx
import '@heliguy-xyz/splat-viewer-react-ui/dist/styles.css';

Custom Styling

You can customize the component appearance using the className prop:

<SplatSceneViewer
  className="custom-viewer"
  // ... other props
/>

Backend Integration

API Endpoints Example

Your backend should provide these endpoints:

// Get models for a scene
GET /api/scenes/:sceneId/models
Response: ModelItem[]

// Upload new model
POST /api/models/upload
Body: FormData with 'file' and 'sceneId'
Response: { success: boolean, modelId: string }

// Update model transform
PATCH /api/scenes/:sceneId/models/:modelId/transform
Body: ModelTransform
Response: { success: boolean }

// Update scene settings
PATCH /api/scenes/:sceneId/settings
Body: Partial<SceneSettings>
Response: { success: boolean }

// Delete model
DELETE /api/scenes/:sceneId/models/:modelId
Response: { success: boolean }

// Get permissions
GET /api/scenes/:sceneId/permissions
Response: { canEdit: boolean, canView: boolean }

Data Format

Models should be returned in this format:

[
  {
    "id": "model-123",
    "name": "Building.ply",
    "uploadedAt": "2026-01-15T14:30:00Z",
    "visible": true,
    "collisionMeshUrl": "https://cdn.example.com/collision/building.obj"
  }
]

Important:

  • uploadedAt must be an ISO 8601 date string
  • Models are grouped by date and sorted by time in the timeline panel
  • collisionMeshUrl is optional

Troubleshooting

Component doesn't render

Check:

  1. Both packages are installed: @heliguy-xyz/splat-viewer and @heliguy-xyz/splat-viewer-react-ui
  2. Tailwind CSS is properly configured
  3. Styles are imported in your app

Styles are missing/broken

Fix:

  1. Add the package to Tailwind content array
  2. Import the CSS file: import '@heliguy-xyz/splat-viewer-react-ui/dist/styles.css'
  3. Ensure Tailwind CSS is processing the component files

Transform changes not saving

Check:

  1. onModelTransformChange callback is implemented
  2. API endpoint is working correctly
  3. Check browser console for errors
  4. Changes are debounced at 100ms - wait before checking

Models not appearing in timeline

Check:

  1. models prop is provided and has correct format
  2. Each model has required fields: id, name, uploadedAt, visible
  3. uploadedAt is a valid ISO date string
  4. Check browser console for errors

File upload not working

Check:

  1. uploadModel prop is provided
  2. Backend endpoint accepts multipart/form-data
  3. File size limits on backend
  4. CORS is configured correctly

Performance issues

Optimize:

  1. Limit number of models loaded at once
  2. Use proper backend pagination
  3. Optimize model file sizes before upload
  4. Consider using lower-quality models for preview

API Quick Reference

import { SplatSceneViewer } from '@heliguy-xyz/splat-viewer-react-ui';
import type { ModelItem, ModelTransform } from '@heliguy-xyz/splat-viewer-react-ui';

<SplatSceneViewer
  // Core
  viewOnly={false}
  className=""
  models={[]}

  // Navigation
  onBackClick={() => {}}
  onShareClick={() => {}}

  // Lifecycle
  onReady={() => {}}
  onError={(error) => {}}

  // Models
  onModelImportOpen={() => {}}
  uploadModel={async (file) => {}}
  onModelSelect={(modelId) => {}}
  onModelDelete={async (modelId) => {}}

  // Updates (debounced internally)
  onModelTransformChange={async (modelId, transform) => {}}
  onSceneSettingsChange={async (settings) => {}}
/>

Support

License

See LICENSE in the root directory.