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.19

Published

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

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

📚 Additional Documentation

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([]);
  const [sceneSettings, setSceneSettings] = useState({
    backgroundColor: '#27272A',
    fieldOfView: 65,
    showGrid: false,
  });

  return (
    <SplatSceneViewer
      viewOnly={false}
      models={models}
      sceneSettings={sceneSettings}
      onBackClick={() => window.history.back()}
  onModelTransformChange={(modelId, transform) => {
    console.log('Transform changed:', modelId, transform); // transform is 16-element array (4x4 matrix)
  }}
      onSceneSettingsChange={(settings) => {
        console.log('Settings changed:', settings);
        setSceneSettings(prev => ({ ...prev, ...settings }));
      }}
    />
  );
}

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 previewFile = await uploaderRef.current?.capturePreview();

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

    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 | | hideUI | boolean | false | No | When true, hides all UI elements and shows only the viewer canvas (no toolbar, panels, or tooltips) | | className | string | '' | No | Additional CSS classes for the root container | | models | ModelItem[] | [] | No | Array of models to display in the timeline panel | | sceneSettings | Partial<SceneSettings> | {} | No | Initial scene configuration (background, FOV, grid, etc.) |

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. Upload should be handled in your modal | | 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: number[]) => void | No | Called when model transform changes (debounced at 100ms). Transform is a 16-element array representing a 4x4 matrix |

Types

ModelItem

Represents a model in the scene.

interface ModelItem {
  id: string                        // Unique model identifier
  name: string                      // Model display name
  path: string                      // URL/path to load model from (e.g., S3 URL)
  visible: boolean                  // Model visibility state
  created_at: string                // ISO date string (creation timestamp)
  updated_at?: string               // ISO date string (last update timestamp)
  transform?: number[]              // 4x4 transformation matrix (16 elements, row-major)
  init_rotation?: [number, number, number]  // Initial rotation [x, y, z] in degrees
  collision_mesh_src?: string       // URL to collision mesh file
  info?: any                        // Populated by viewer after model loads
}

Example:

const models: ModelItem[] = [
  {
    id: 'model-123',
    name: 'Building Scan.ply',
    path: 'https://my-bucket.s3.amazonaws.com/models/building.splat',
    visible: true,
    created_at: '2026-01-15T14:30:00Z',
    updated_at: '2026-01-16T10:00:00Z',
    collision_mesh_src: 'https://cdn.example.com/collision/building.obj',
    transform: [
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      5, 0, 10, 1
    ],  // 4x4 transformation matrix (row-major)
    init_rotation: [0, 90, 0]  // Initial rotation [x, y, z] in degrees
  }
];

Loading from URLs: When you provide a path 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.

Transform Matrix: The transform field is a 16-element array representing a 4x4 transformation matrix in row-major order. This includes position, rotation, and scale. This is useful for restoring saved transform states from your database.

Initial Rotation: The init_rotation field is an optional array of three numbers [x, y, z] representing initial rotation in degrees. This is applied when the model first loads.

SceneSettings

Scene configuration settings that can be provided via sceneSettings prop and modified via onSceneSettingsChange:

interface SceneSettings {
  backgroundColor: string           // Scene background color (hex)
  fieldOfView: number               // Camera FOV in degrees
  showGrid: boolean                 // Infinite grid visibility
  showBoundingBox: boolean          // Model bounding box visibility
  flySpeed: number                  // Fly camera movement speed
  sh: number                        // Spherical harmonics bands (0-3)
  tonemapping?: string              // Tonemapping mode: 'none' | 'linear' | 'neutral' | 'aces' | 'aces2' | 'filmic' | 'hejl'
  // Optional legacy fields
  skyboxColor?: string
  centersSize?: number
  outlineSelection?: boolean
  fakeSkyEnabled?: boolean
}

Example:

const sceneSettings: Partial<SceneSettings> = {
  backgroundColor: '#27272A',
  fieldOfView: 65,
  sh: 1,
  flySpeed: 5,
  showGrid: false,
  showBoundingBox: false,
  tonemapping: 'aces'
};

<SplatSceneViewer
  sceneSettings={sceneSettings}
  onSceneSettingsChange={(settings) => {
    // Save changes to database
    updateSceneSettings(settings);
  }}
/>

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)}
/>

{showImportModal && (
  <YourUploadModal
    sceneId={sceneId}
    onClose={() => setShowImportModal(false)}
    onUploadSuccess={async (uploadedModel) => {
      // Update models list after successful upload
      const updated = await fetchModels(sceneId);
      setModels(updated);
      setShowImportModal(false);
      toast.success('Model uploaded successfully');
    }}
  />
)}

Note: Model upload to backend should be handled entirely within your custom modal component. After successful upload, update the models prop and the viewer will automatically load the new models.

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.

The transform parameter is a 16-element number array representing a 4x4 transformation matrix in row-major order.

<SplatSceneViewer
  onModelTransformChange={async (modelId, transform) => {
    try {
      // transform is a 16-element array: [m11, m12, m13, m14, m21, m22, m23, m24, m31, m32, m33, m34, m41, m42, m43, m44]
      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 } 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: number[]) => {
    try {
      await fetch(`/api/scenes/${params.id}/models/${modelId}/transform`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ transform }), // transform is 16-element array (4x4 matrix)
      });
    } 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)}
        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

UI-Hidden Mode (Headless Viewer)

<SplatSceneViewer
  hideUI={true}
  models={models}
  onBackClick={() => {}}
  onShareClick={() => {}}
  // ... other required callbacks
/>

When hideUI is true:

  • All UI elements are completely hidden (toolbar, timeline, properties panel, navigation cube)
  • Only the 3D viewer canvas is shown
  • No tooltips or overlays are displayed
  • Useful for embedding the viewer in custom layouts or full-screen presentations
  • Camera controls still work (mouse/keyboard interactions with the viewer)

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: { transform: number[] }  // 16-element array (4x4 matrix)
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",
    "path": "https://my-bucket.s3.amazonaws.com/models/building.splat",
    "visible": true,
    "created_at": "2026-01-15T14:30:00Z",
    "updated_at": "2026-01-16T10:00:00Z",
    "transform": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
    "init_rotation": [0, 90, 0],
    "collision_mesh_src": "https://cdn.example.com/collision/building.obj"
  }
]

Important:

  • created_at must be an ISO 8601 date string
  • Models are grouped by date and sorted by time in the timeline panel
  • path is the URL to load the model from (required)
  • transform is a 16-element array (4x4 matrix, row-major) - optional
  • init_rotation is [x, y, z] rotation in degrees - optional
  • collision_mesh_src 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. onModelImportOpen callback is implemented
  2. Your upload modal handles file upload to backend
  3. After upload, models prop is updated with new model
  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 } from '@heliguy-xyz/splat-viewer-react-ui';

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

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

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

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

  // Updates (debounced internally)
  onModelTransformChange={async (modelId, transform: number[]) => {}}  // transform is 16-element array
  onSceneSettingsChange={async (settings) => {}}
/>

Support

License

See LICENSE in the root directory.