Skip to content

Annotation load from config and display #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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