Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
43a2126
Replace GLTF primitive with drei Clone to support multiple instances …
JulieWinchester Feb 17, 2025
e4b875f
Import annotations from scene config, display annotations in scene mode
JulieWinchester Mar 18, 2025
602522b
Add additional radix component primitives
JulieWinchester Mar 25, 2025
8b88d91
Toolbars for annotation and simple scene controls
JulieWinchester Mar 25, 2025
53f4c03
Add rotation controls menu section
JulieWinchester Mar 25, 2025
2996844
Restyle annotations tab
JulieWinchester Mar 25, 2025
f35d45a
Reconfigure src config and add srcCollections
JulieWinchester Mar 26, 2025
e23928f
Flip annotation coloring, selected annotation now white and unselecte…
JulieWinchester Mar 26, 2025
4e0d857
Restore tooltip trigger asChild attribute (causes issues if not present)
JulieWinchester Mar 26, 2025
0af6d80
Small UI tweaks, remove false-firing annotation error message
JulieWinchester Mar 26, 2025
8ff1f48
Fix spacing issue, changing scene source resets box/grid/axis state
JulieWinchester Mar 27, 2025
6b109f0
Fix issue with annotation transitions
JulieWinchester Mar 27, 2025
ff4c35e
Fix measurement label overflow, add border to measurement label
JulieWinchester Mar 27, 2025
876b7a7
Move duplicated anno and measurment functions to utils
JulieWinchester Mar 27, 2025
df2cfa4
Bounding box shows dimension labels with physical units
JulieWinchester Mar 27, 2025
5be321b
Viewer can set initial measurement units
JulieWinchester Mar 27, 2025
8bc22c4
Update src/components/source-selector.tsx
JulieWinchester Mar 27, 2025
f866a53
Merge branch 'annotation_display' into bounds_text_labels
JulieWinchester Apr 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,154 changes: 1,816 additions & 338 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,16 @@
"dependencies": {
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-hover-card": "^1.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@react-three/drei": "^9.77.4",
"@radix-ui/react-toolbar": "^1.1.2",
"@react-three/drei": "^9.121.5",
"@react-three/fiber": "^8.15.12",
"@use-gesture/react": "^10.3.0",
"class-variance-authority": "^0.7.0",
Expand Down
208 changes: 130 additions & 78 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import './App.css';
import { useEffect, useRef } from 'react';
import { useControls } from 'leva';
import { normalizeSrc, ViewerRef, SrcObj, Viewer, ControlPanel } from '../index';
// import { Leva, useControls } from 'leva';
import { ViewerRef, SrcObj, Viewer, ControlPanel } from '../index';
import { Toaster } from '@/components/ui/sonner';
import { toast } from 'sonner';
// @ts-ignore
Expand All @@ -14,115 +14,167 @@ function App() {
const loadedUrlsRef = useRef<string[]>([]);

const {
cameraMode,
environmentMap,
setAmbientLightIntensity,
setEnvironmentMap
} = useStore();

// Configurable app data, includes list of models and scene/UI presets
const config = {
srcs: {
src: 'https://raw.githubusercontent.com/IIIF/3d/main/assets/astronaut/astronaut.glb',
srcCollections: [
// 'Measurement Cube': {
// url: 'https://cdn.glitch.global/afd88411-0206-477e-b65f-3d1f201de994/measurement_cube.glb?v=1710500461208',
// label: 'Measurement Cube',
// },
'Flight Helmet':
'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/FlightHelmet/glTF/FlightHelmet.gltf',
'Roberto Clemente Batting Helmet':
'https://cdn.glitch.global/2658666b-2aa1-4395-8dfe-44a4aaaa0b16/nmah-1981_0706_06-clemente_helmet-100k-2048_std_draco.glb?v=1729600102458',
Shoe: {
url: 'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/MaterialsVariantsShoe/glTF-Binary/MaterialsVariantsShoe.glb',
requiredStatement:
'© 2021, Shopify. <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank">CC BY 4.0 International</a> <br/> - Shopify for Everthing',
{
label: 'Flight Helmet',
src: 'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/FlightHelmet/glTF/FlightHelmet.gltf',
},
'Mosquito in Amber': {
url: 'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/MosquitoInAmber/glTF-Binary/MosquitoInAmber.glb',
requiredStatement:
'© 2018, Sketchfab. <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank">CC BY 4.0 International</a> <br/> - Loic Norgeot for Model <br/> - Sketchfab for Real-time refraction',
{
label: 'Roberto Clemente Batting Helmet',
src: 'https://cdn.glitch.global/2658666b-2aa1-4395-8dfe-44a4aaaa0b16/nmah-1981_0706_06-clemente_helmet-100k-2048_std_draco.glb?v=1729600102458',
},
'Thor and the Midgard Serpent': {
url: 'https://modelviewer.dev/assets/SketchfabModels/ThorAndTheMidgardSerpent.glb',
position: [0, 0, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1],
requiredStatement:
'© 2019, <a href="https://sketchfab.com/MrTheRich">Mr. The Rich</a>. <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank">CC BY 4.0 International</a>',
} as SrcObj,
'Multiple Objects': [
{
url: 'https://modelviewer.dev/assets/ShopifyModels/Mixer.glb',
position: [0, 0, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1],
},
{
url: 'https://modelviewer.dev/assets/ShopifyModels/GeoPlanter.glb',
position: [0.5, 0, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1],
{
label: 'Shoe',
src: {
url: 'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/MaterialsVariantsShoe/glTF-Binary/MaterialsVariantsShoe.glb',
requiredStatement:
'© 2021, Shopify. <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank">CC BY 4.0 International</a> <br/> - Shopify for Everthing',
},
{
url: 'https://modelviewer.dev/assets/ShopifyModels/ToyTrain.glb',
position: [1, 0, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1],
},
{
label: 'Mosquito in Amber',
src: {
url: 'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/MosquitoInAmber/glTF-Binary/MosquitoInAmber.glb',
requiredStatement:
'© 2018, Sketchfab. <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank">CC BY 4.0 International</a> <br/> - Loic Norgeot for Model <br/> - Sketchfab for Real-time refraction',
},
{
url: 'https://modelviewer.dev/assets/ShopifyModels/Chair.glb',
position: [1.5, 0, 0],
},
{
label: 'Thor and the Midgard Serpent',
src: {
url: 'https://modelviewer.dev/assets/SketchfabModels/ThorAndTheMidgardSerpent.glb',
position: [0, 0, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1],
},
] as SrcObj[],
'Stanford Bunny':
'https://raw.githubusercontent.com/JulieWinchester/aleph-assets/main/bunny.glb',
// 'Frog (Draco) URL': 'https://aleph-gltf-models.netlify.app/Frog.glb',
},
requiredStatement:
'© 2019, <a href="https://sketchfab.com/MrTheRich">Mr. The Rich</a>. <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank">CC BY 4.0 International</a>',
} as SrcObj,
},
{
label: 'Multiple Objects',
src: [
{
url: 'https://modelviewer.dev/assets/ShopifyModels/Mixer.glb',
position: [0, 0, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1],
},
{
url: 'https://modelviewer.dev/assets/ShopifyModels/GeoPlanter.glb',
position: [0.5, 0, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1],
},
{
url: 'https://modelviewer.dev/assets/ShopifyModels/ToyTrain.glb',
position: [1, 0, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1],
},
{
url: 'https://modelviewer.dev/assets/ShopifyModels/Chair.glb',
position: [1.5, 0, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1],
},
] as SrcObj[],
},
{
label: 'Stanford Bunny',
src: 'https://raw.githubusercontent.com/JulieWinchester/aleph-assets/main/bunny.glb',
},
{
label: 'Astronaut Annotated',
src: {
url: 'https://raw.githubusercontent.com/IIIF/3d/main/assets/astronaut/astronaut.glb',
annotations: [
{
"position": {
"x": 0.013300441763413414,
"y": 3.49898729918975,
"z": 0.7009494188636793
},
"normal": {
"x": 0.2913311155479682,
"y": -0.19759283797457303,
"z": 0.9359931898762569
},
"cameraPosition": {
"x": 0,
"y": 2.010847091674805,
"z": 9.11616179783789
},
"cameraTarget": {
"x": 0,
"y": 2.0108470916748047,
"z": -0.012333005666732798
},
"label": "Helmet",
"description": "Helmet Description"
},
{
"position": {
"x": 1.0324531718276508,
"y": 1.7976133376627947,
"z": 0.2435945547861511
},
"normal": {
"x": 0.6063132429509818,
"y": 0.04805783885403426,
"z": 0.7937724456964624
},
"cameraPosition": {
"x": 0,
"y": 2.010847091674805,
"z": 9.11616179783789
},
"cameraTarget": {
"x": 0,
"y": 2.0108470916748047,
"z": -0.012333005666732798
},
"label": "Glove",
"description": "Glove Description"
}
],
} as SrcObj,
},
],
scene: {
ambientLightIntensity: 0,
environmentMap: 'apartment',
rotation: [0, 0, 0], // Default rotation in radians
}
};

// https://github.com/KhronosGroup/glTF-Sample-Assets/blob/main/Models/Models-showcase.md
// https://github.com/google/model-viewer/tree/master/packages/modelviewer.dev/assets
const [{ src }, _setLevaControls] = useControls(() => ({
src: {
options: config.srcs,
},
}));

useEffect(() => {
setAmbientLightIntensity(config.scene.ambientLightIntensity);
}, [config.scene.ambientLightIntensity]);

useEffect(() => {
setEnvironmentMap(config.scene.environmentMap as PresetsType);
}, [config.scene.environmentMap]);

// src or camera mode changed
useEffect(() => {
const normalizedSrc = normalizeSrc(src);
// if the src is already loaded, recenter the camera
if (normalizedSrc.every((src) => loadedUrlsRef.current.includes(src.url))) {
setTimeout(() => {
viewerRef.current?.recenter(true);
}, 100);
}
}, [src, cameraMode]);
if (config.scene.ambientLightIntensity) setAmbientLightIntensity(config.scene.ambientLightIntensity);
if (config.scene.environmentMap) setEnvironmentMap(config.scene.environmentMap as PresetsType);
}, []);

return (
<div id="container">
<div id="control-panel" className="block md:hidden">
<div id="control-panel" className="block md:hidden control-component">
<ControlPanel></ControlPanel>
</div>
<div id="viewer">
<Viewer
ref={viewerRef}
envPreset={environmentMap}
src={src}
// src={config.src} // Uncomment this line and comment srcCollections to load single src
srcCollections={config.srcCollections}
environmentMap={environmentMap}
rotationPreset={config.scene.rotation as [number, number, number]}
onLoad={(srcs: SrcObj[]) => {
console.log(`model${srcs.length > 1 ? 's' : ''} loaded`, srcs);
Expand Down
30 changes: 23 additions & 7 deletions src/Store.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { create } from 'zustand';
import { Annotation, CameraMode, SrcObj, Mode, ObjectMeasurement, ScreenMeasurement, MeasurementMode } from './types/';
import { Annotation, CameraMode, SrcCollections, SrcObj, Mode, ObjectMeasurement, ScreenMeasurement, MeasurementMode } from './types/';
import { Euler } from 'three';
import { PresetsType } from '@react-three/drei/helpers/environment-assets';

Expand All @@ -23,9 +23,11 @@ type State = {
rotationXDegrees: number;
rotationYDegrees: number;
rotationZDegrees: number;
sceneControlsEnabled: boolean;
rotationControlsEnabled: boolean;
screenMeasurements: ScreenMeasurement[];
selectedAnnotation: number | null;
srcCollections: SrcCollections;
srcCollectionSelected: number | null;
srcs: SrcObj[];
setAmbientLightIntensity: (ambientLightIntensity: number) => void;
setAnnotations: (annotations: Annotation[]) => void;
Expand All @@ -44,9 +46,11 @@ type State = {
setRotationXDegrees: (rotationXDegrees: number) => void;
setRotationYDegrees: (rotationYDegrees: number) => void;
setRotationZDegrees: (rotationZDegrees: number) => void;
setSceneControlsEnabled: (sceneControlsEnabled: boolean) => void;
setRotationControlsEnabled: (rotationControlsEnabled: boolean) => void;
setScreenMeasurements: (measurements: ScreenMeasurement[]) => void;
setSelectedAnnotation: (selectedAnnotation: number | null) => void;
setSrcCollections: (srcsCollections: SrcCollections) => void;
setSrcCollectionSelected: (srcCollectionSelected: number | null) => void;
setSrcs: (srcs: SrcObj[]) => void;
};

Expand All @@ -68,11 +72,13 @@ const useStore = create<State>((set) => ({
rotationXDegrees: 0.0,
rotationYDegrees: 0.0,
rotationZDegrees: 0.0,
sceneControlsEnabled: false,
rotationControlsEnabled: false,
screenMeasurements: [],
selectedAnnotation: null,
srcCollections: [],
srcCollectionSelected: null,
srcs: [],

setAmbientLightIntensity: (ambientLightIntensity: number) =>
set({
ambientLightIntensity,
Expand Down Expand Up @@ -168,9 +174,9 @@ const useStore = create<State>((set) => ({
rotationZDegrees,
}),

setSceneControlsEnabled: (sceneControlsEnabled: boolean) =>
setRotationControlsEnabled: (rotationControlsEnabled: boolean) =>
set({
sceneControlsEnabled,
rotationControlsEnabled,
}),

setScreenMeasurements: (measurements: ScreenMeasurement[]) =>
Expand All @@ -183,6 +189,16 @@ const useStore = create<State>((set) => ({
selectedAnnotation,
}),

setSrcCollections: (srcCollections: SrcCollections) =>
set({
srcCollections,
}),

setSrcCollectionSelected: (srcCollectionSelected: number | null) =>
set({
srcCollectionSelected,
}),

setSrcs: (srcs: SrcObj[]) =>
set({
srcs,
Expand Down
13 changes: 6 additions & 7 deletions src/components/annotation-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,23 +124,22 @@ function AnnotationTab() {
<div className='flex flex-col justify-between grow'>
<div className='grid gap-y-4'>
<div className="overflow-y-auto overflow-x-hidden">
<Instructions>Double-click to create annotations, drag to reorder.</Instructions>
{annotations.length ? (
annotations.map((anno: Annotation, idx) => {
return (
<div
key={idx}
className={cn('flex items-center justify-between my-2', {
'cursor-move': editIdx === null,
})}
className={'my-4 cursor-pointer'}
draggable={editIdx === null}
onDragStart={(e) => dragStart(e)}
onDragEnter={(e) => dragEnter(e)}
onDragOver={(e) => e.preventDefault()}
onDragEnd={drop}
data-idx={idx}>
{editIdx === idx && (
<form onSubmit={handleSubmit} className="flex items-end justify-between w-full py-2">
<div className="flex flex-col w-full mr-2">
<form onSubmit={handleSubmit} className="flex flex-col items-end gap-2 w-full">
<div className="flex flex-col w-full">
<input
type="text"
placeholder="Label"
Expand Down Expand Up @@ -183,10 +182,10 @@ function AnnotationTab() {
{
'text-white': selectedAnnotation === idx,
}
)}>{`${idx + 1}. ${anno.label || 'no label'}`}</h3>
)}>{`${idx + 1}. ${anno.label || 'No Label'}`}</h3>
<p className="text-xs text-zinc-400 line-clamp-1 pr-1 whitespace-normal">{anno.description}</p>
</div>
<div className="flex items-center gap-2">
<div className="flex gap-2 mt-2">
{/* set default camera view button */}
<Tooltip content="Set Default View">
<Button
Expand Down
Loading