├── .npmrc
├── .prettierrc
├── apps
└── official-website
│ ├── .gitignore
│ ├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── mstile-70x70.png
│ ├── favicon-16x16.png
│ ├── favicon-194x194.png
│ ├── favicon-32x32.png
│ ├── mstile-144x144.png
│ ├── mstile-150x150.png
│ ├── mstile-310x150.png
│ ├── mstile-310x310.png
│ ├── apple-touch-icon.png
│ ├── assets
│ │ ├── vans-shoe.glb
│ │ └── DamagedHelmet.glb
│ ├── screenshot-mobile.png
│ ├── draco
│ │ ├── draco_decoder.wasm
│ │ └── README.md
│ ├── screenshot-desktop.png
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── apple-touch-icon-57x57.png
│ ├── apple-touch-icon-60x60.png
│ ├── apple-touch-icon-72x72.png
│ ├── apple-touch-icon-76x76.png
│ ├── apple-touch-icon-114x114.png
│ ├── apple-touch-icon-120x120.png
│ ├── apple-touch-icon-144x144.png
│ ├── apple-touch-icon-152x152.png
│ ├── apple-touch-icon-180x180.png
│ ├── apple-touch-icon-precomposed.png
│ ├── apple-touch-icon-57x57-precomposed.png
│ ├── apple-touch-icon-60x60-precomposed.png
│ ├── apple-touch-icon-72x72-precomposed.png
│ ├── apple-touch-icon-76x76-precomposed.png
│ ├── apple-touch-icon-114x114-precomposed.png
│ ├── apple-touch-icon-120x120-precomposed.png
│ ├── apple-touch-icon-144x144-precomposed.png
│ ├── apple-touch-icon-152x152-precomposed.png
│ ├── apple-touch-icon-180x180-precomposed.png
│ ├── browserconfig.xml
│ ├── sitemap.xml
│ ├── site.webmanifest
│ └── safari-pinned-tab.svg
│ ├── src
│ ├── vite-env.d.ts
│ ├── pages
│ │ ├── editor
│ │ │ ├── index.ts
│ │ │ ├── loading-overlay.tsx
│ │ │ ├── color-picker.tsx
│ │ │ ├── drop-zone.tsx
│ │ │ ├── upload-info-dialog.tsx
│ │ │ └── editor.tsx
│ │ ├── contact.tsx
│ │ └── help.tsx
│ ├── components
│ │ ├── assets
│ │ │ ├── react-graphic.png
│ │ │ ├── google-icon.tsx
│ │ │ └── vectreal-logo.tsx
│ │ ├── providers
│ │ │ ├── index.ts
│ │ │ ├── router-provider.tsx
│ │ │ ├── theme-provider.tsx
│ │ │ └── editor-provider.tsx
│ │ ├── typography
│ │ │ ├── typography-muted.tsx
│ │ │ └── typography-lead.tsx
│ │ ├── modal-close-button.tsx
│ │ ├── title-section.tsx
│ │ ├── footer.tsx
│ │ ├── title-model-scene.tsx
│ │ └── base-layout.tsx
│ ├── lib
│ │ ├── hooks
│ │ │ ├── index.ts
│ │ │ ├── use-is-mobile.ts
│ │ │ ├── use-init-ga.ts
│ │ │ ├── use-media.ts
│ │ │ └── use-accept-pattern.ts
│ │ └── utils
│ │ │ └── ga-utils.ts
│ ├── app.tsx
│ ├── main.tsx
│ └── globals.css
│ ├── .eslintignore
│ ├── package.json
│ ├── project.json
│ ├── nginx.conf
│ ├── .eslintrc.json
│ ├── jest.config.ts
│ ├── tsconfig.json
│ ├── tsconfig.spec.json
│ ├── tsconfig.app.json
│ ├── Dockerfile
│ ├── tailwind.config.js
│ ├── vite.config.mts
│ └── index.html
├── shared
├── src
│ ├── index.ts
│ ├── lib
│ │ └── utils.ts
│ └── components
│ │ ├── skeleton.tsx
│ │ ├── index.ts
│ │ ├── default-spinner.tsx
│ │ ├── label.tsx
│ │ ├── textarea.tsx
│ │ ├── input.tsx
│ │ ├── progress.tsx
│ │ ├── sonner.tsx
│ │ ├── hero-highlight.tsx
│ │ ├── badge.tsx
│ │ ├── tooltip.tsx
│ │ ├── popover.tsx
│ │ ├── avatar.tsx
│ │ ├── button.tsx
│ │ ├── accordion.tsx
│ │ ├── card.tsx
│ │ ├── dialog.tsx
│ │ ├── glowing-stars.tsx
│ │ └── sheet.tsx
├── README.md
├── .babelrc
├── project.json
├── tsconfig.json
├── .eslintrc.json
└── tsconfig.lib.json
├── .eslintignore
├── packages
├── viewer
│ ├── src
│ │ ├── index.ts
│ │ ├── components
│ │ │ ├── assets
│ │ │ │ ├── types.ts
│ │ │ │ ├── vectreal-logo.tsx
│ │ │ │ ├── cross-icon.tsx
│ │ │ │ └── info-icon.tsx
│ │ │ ├── index.ts
│ │ │ ├── spinner-wrapper.tsx
│ │ │ ├── scene
│ │ │ │ ├── index.ts
│ │ │ │ ├── scene-model.tsx
│ │ │ │ ├── scene-environment.tsx
│ │ │ │ ├── scene-controls.tsx
│ │ │ │ ├── scene-camera.tsx
│ │ │ │ └── scene-grid.tsx
│ │ │ └── info-popover.tsx
│ │ └── styles.module.css
│ ├── .babelrc
│ ├── .storybook
│ │ ├── preview.ts
│ │ └── main.ts
│ ├── tsconfig.json
│ ├── tsconfig.lib.json
│ ├── tsconfig.storybook.json
│ ├── project.json
│ ├── package.json
│ ├── vite.config.ts
│ └── .eslintrc.json
└── hooks
│ ├── src
│ ├── use-export-model
│ │ ├── index.ts
│ │ ├── utils
│ │ │ ├── index.ts
│ │ │ ├── data-uri-to-blob.ts
│ │ │ ├── file-helpers.ts
│ │ │ └── export-handlers.ts
│ │ ├── types.ts
│ │ └── use-export-model.ts
│ ├── use-optimize-model
│ │ ├── index.ts
│ │ ├── types.ts
│ │ └── state.ts
│ ├── index.ts
│ └── use-load-model
│ │ ├── file-type-hooks
│ │ ├── index.ts
│ │ ├── use-load-binary.ts
│ │ └── use-load-gltf.ts
│ │ ├── utils
│ │ ├── index.ts
│ │ ├── array-buffer-to-base64.ts
│ │ └── read-directory.ts
│ │ ├── loaders
│ │ ├── index.ts
│ │ ├── create-usdz-loader.ts
│ │ └── create-gltf-loader.ts
│ │ ├── index.ts
│ │ ├── model-context.tsx
│ │ ├── event-system.ts
│ │ ├── state.ts
│ │ └── types.ts
│ ├── .babelrc
│ ├── tsconfig.json
│ ├── tsconfig.lib.json
│ ├── .eslintrc.json
│ ├── project.json
│ ├── package.json
│ └── vite.config.ts
├── jest.preset.js
├── .vscode
└── extensions.json
├── .prettierignore
├── jest.config.ts
├── .gitignore
├── .github
├── workflows
│ ├── lint-build.yaml
│ ├── version-release.yaml
│ ├── chromatic-vctrl-viewer.yaml
│ └── deploy-official-website.yaml
└── dependabot.yml
├── .eslintrc.json
├── tsconfig.base.json
├── nx.json
├── CONTRIBUTING.md
└── package.json
/.npmrc:
--------------------------------------------------------------------------------
1 | git-urlrewrite=false
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true
3 | }
4 |
--------------------------------------------------------------------------------
/apps/official-website/.gitignore:
--------------------------------------------------------------------------------
1 | dev-dist
2 | .env
--------------------------------------------------------------------------------
/shared/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './components';
2 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | draco
3 | public
4 | assets
--------------------------------------------------------------------------------
/apps/official-website/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow: /
--------------------------------------------------------------------------------
/apps/official-website/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/apps/official-website/src/pages/editor/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './editor';
2 |
--------------------------------------------------------------------------------
/apps/official-website/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | draco
3 | public
4 | assets
5 | dev-dist
--------------------------------------------------------------------------------
/packages/viewer/src/index.ts:
--------------------------------------------------------------------------------
1 | export { default as VectrealViewer } from './vectreal-viewer';
2 |
--------------------------------------------------------------------------------
/packages/hooks/src/use-export-model/index.ts:
--------------------------------------------------------------------------------
1 | export { default as useExportModel } from './use-export-model';
2 |
--------------------------------------------------------------------------------
/packages/viewer/src/components/assets/types.ts:
--------------------------------------------------------------------------------
1 | export interface IconProps {
2 | className?: string;
3 | }
4 |
--------------------------------------------------------------------------------
/jest.preset.js:
--------------------------------------------------------------------------------
1 | const nxPreset = require('@nx/jest/preset').default;
2 |
3 | module.exports = { ...nxPreset };
4 |
--------------------------------------------------------------------------------
/packages/hooks/src/use-optimize-model/index.ts:
--------------------------------------------------------------------------------
1 | export { default as useOptimizeModel } from './use-optimize-model';
2 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["esbenp.prettier-vscode", "firsttris.vscode-jest-runner"]
3 | }
4 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Add files here to ignore them from prettier formatting
2 | /dist
3 | /coverage
4 | /.nx/cache
5 | /.nx/workspace-data
--------------------------------------------------------------------------------
/apps/official-website/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/favicon.ico
--------------------------------------------------------------------------------
/packages/hooks/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './use-load-model';
2 | export * from './use-optimize-model';
3 | export * from './use-export-model';
4 |
--------------------------------------------------------------------------------
/apps/official-website/public/mstile-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/mstile-70x70.png
--------------------------------------------------------------------------------
/apps/official-website/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/favicon-16x16.png
--------------------------------------------------------------------------------
/apps/official-website/public/favicon-194x194.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/favicon-194x194.png
--------------------------------------------------------------------------------
/apps/official-website/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/favicon-32x32.png
--------------------------------------------------------------------------------
/apps/official-website/public/mstile-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/mstile-144x144.png
--------------------------------------------------------------------------------
/apps/official-website/public/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/mstile-150x150.png
--------------------------------------------------------------------------------
/apps/official-website/public/mstile-310x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/mstile-310x150.png
--------------------------------------------------------------------------------
/apps/official-website/public/mstile-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/mstile-310x310.png
--------------------------------------------------------------------------------
/apps/official-website/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/apps/official-website/public/assets/vans-shoe.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/assets/vans-shoe.glb
--------------------------------------------------------------------------------
/apps/official-website/public/screenshot-mobile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/screenshot-mobile.png
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | import { getJestProjectsAsync } from '@nx/jest';
2 |
3 | export default async () => ({
4 | projects: await getJestProjectsAsync(),
5 | });
6 |
--------------------------------------------------------------------------------
/apps/official-website/public/assets/DamagedHelmet.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/assets/DamagedHelmet.glb
--------------------------------------------------------------------------------
/apps/official-website/public/draco/draco_decoder.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/draco/draco_decoder.wasm
--------------------------------------------------------------------------------
/apps/official-website/public/screenshot-desktop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/screenshot-desktop.png
--------------------------------------------------------------------------------
/apps/official-website/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/apps/official-website/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/apps/official-website/public/apple-touch-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/apple-touch-icon-57x57.png
--------------------------------------------------------------------------------
/apps/official-website/public/apple-touch-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/apple-touch-icon-60x60.png
--------------------------------------------------------------------------------
/apps/official-website/public/apple-touch-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/apple-touch-icon-72x72.png
--------------------------------------------------------------------------------
/apps/official-website/public/apple-touch-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/apple-touch-icon-76x76.png
--------------------------------------------------------------------------------
/apps/official-website/public/apple-touch-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/apple-touch-icon-114x114.png
--------------------------------------------------------------------------------
/apps/official-website/public/apple-touch-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/apple-touch-icon-120x120.png
--------------------------------------------------------------------------------
/apps/official-website/public/apple-touch-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/apple-touch-icon-144x144.png
--------------------------------------------------------------------------------
/apps/official-website/public/apple-touch-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/apple-touch-icon-152x152.png
--------------------------------------------------------------------------------
/apps/official-website/public/apple-touch-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/apple-touch-icon-180x180.png
--------------------------------------------------------------------------------
/apps/official-website/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/apple-touch-icon-precomposed.png
--------------------------------------------------------------------------------
/apps/official-website/src/components/assets/react-graphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/src/components/assets/react-graphic.png
--------------------------------------------------------------------------------
/packages/hooks/src/use-load-model/file-type-hooks/index.ts:
--------------------------------------------------------------------------------
1 | export { default as useLoadGltf } from './use-load-gltf';
2 | export { default as useLoadBinary } from './use-load-binary';
3 |
--------------------------------------------------------------------------------
/packages/hooks/src/use-export-model/utils/index.ts:
--------------------------------------------------------------------------------
1 | export { default as dataURItoBlob } from './data-uri-to-blob';
2 | export * from './export-handlers';
3 | export * from './file-helpers';
4 |
--------------------------------------------------------------------------------
/packages/hooks/src/use-load-model/utils/index.ts:
--------------------------------------------------------------------------------
1 | export { default as arrayBufferToBase64 } from './array-buffer-to-base64';
2 | export { default as readDirectory } from './read-directory';
3 |
--------------------------------------------------------------------------------
/apps/official-website/public/apple-touch-icon-57x57-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/apple-touch-icon-57x57-precomposed.png
--------------------------------------------------------------------------------
/apps/official-website/public/apple-touch-icon-60x60-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/apple-touch-icon-60x60-precomposed.png
--------------------------------------------------------------------------------
/apps/official-website/public/apple-touch-icon-72x72-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/apple-touch-icon-72x72-precomposed.png
--------------------------------------------------------------------------------
/apps/official-website/public/apple-touch-icon-76x76-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/apple-touch-icon-76x76-precomposed.png
--------------------------------------------------------------------------------
/packages/hooks/src/use-load-model/loaders/index.ts:
--------------------------------------------------------------------------------
1 | export { default as createGltfLoader } from './create-gltf-loader';
2 | export { default as createUsdzLoader } from './create-usdz-loader';
3 |
--------------------------------------------------------------------------------
/apps/official-website/public/apple-touch-icon-114x114-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/apple-touch-icon-114x114-precomposed.png
--------------------------------------------------------------------------------
/apps/official-website/public/apple-touch-icon-120x120-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/apple-touch-icon-120x120-precomposed.png
--------------------------------------------------------------------------------
/apps/official-website/public/apple-touch-icon-144x144-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/apple-touch-icon-144x144-precomposed.png
--------------------------------------------------------------------------------
/apps/official-website/public/apple-touch-icon-152x152-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/apple-touch-icon-152x152-precomposed.png
--------------------------------------------------------------------------------
/apps/official-website/public/apple-touch-icon-180x180-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vectreal/vectreal-platform/HEAD/apps/official-website/public/apple-touch-icon-180x180-precomposed.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | .nx/
4 | vite.config.mts.timestamp-*
5 | vite.config.ts.timestamp-*
6 | .DS_Store
7 | **/vite.config.{js,ts,mjs,mts,cjs,cts}.timestamp*
8 | storybook-static
9 | *.log
--------------------------------------------------------------------------------
/packages/hooks/src/use-load-model/index.ts:
--------------------------------------------------------------------------------
1 | export { default as useLoadModel } from './use-load-model';
2 | export { type ModelFile, ModelFileTypes } from './types';
3 | export * from './model-context';
4 |
--------------------------------------------------------------------------------
/shared/README.md:
--------------------------------------------------------------------------------
1 | # shared
2 |
3 | This library was generated with [Nx](https://nx.dev).
4 |
5 | ## Running unit tests
6 |
7 | Run `nx test shared` to execute the unit tests via [Jest](https://jestjs.io).
8 |
--------------------------------------------------------------------------------
/shared/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from 'clsx';
2 | import { twMerge } from 'tailwind-merge';
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/shared/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@nx/react/babel",
5 | {
6 | "runtime": "automatic",
7 | "useBuiltIns": "usage"
8 | }
9 | ]
10 | ],
11 | "plugins": []
12 | }
13 |
--------------------------------------------------------------------------------
/packages/viewer/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export { default as SpinnerWrapper } from './spinner-wrapper';
2 | export {
3 | default as InfoPopover,
4 | type InfoPopoverProps,
5 | defaultInfoPopoverProps,
6 | } from './info-popover';
7 |
--------------------------------------------------------------------------------
/apps/official-website/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@vctrl/official-website",
3 | "version": "0.0.1",
4 | "description": "",
5 | "main": "index.js",
6 | "keywords": [],
7 | "author": "",
8 | "license": "ISC"
9 | }
10 |
--------------------------------------------------------------------------------
/packages/hooks/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@nx/react/babel",
5 | {
6 | "runtime": "automatic",
7 | "useBuiltIns": "usage"
8 | }
9 | ]
10 | ],
11 | "plugins": []
12 | }
13 |
--------------------------------------------------------------------------------
/packages/viewer/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@nx/react/babel",
5 | {
6 | "runtime": "automatic",
7 | "useBuiltIns": "usage"
8 | }
9 | ]
10 | ],
11 | "plugins": []
12 | }
13 |
--------------------------------------------------------------------------------
/apps/official-website/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vctrl/official-website",
3 | "$schema": "../../node_modules/nx/schemas/project-schema.json",
4 | "sourceRoot": "apps/official-website/src",
5 | "projectType": "application",
6 | "tags": []
7 | }
8 |
--------------------------------------------------------------------------------
/apps/official-website/src/components/providers/index.ts:
--------------------------------------------------------------------------------
1 | export { default as EditorProvider, useEditorContext } from './editor-provider';
2 | export { default as ThemeProvider, useTheme } from './theme-provider';
3 | export { default as RouterProvider } from './router-provider';
4 |
--------------------------------------------------------------------------------
/apps/official-website/src/lib/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export { default as useAcceptPattern } from './use-accept-pattern';
2 | export { default as useInitGA } from './use-init-ga';
3 | export { default as useIsMobile } from './use-is-mobile';
4 | export { default as useMedia } from './use-media';
5 |
--------------------------------------------------------------------------------
/shared/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "shared",
3 | "$schema": "../node_modules/nx/schemas/project-schema.json",
4 | "sourceRoot": "shared/src",
5 | "projectType": "library",
6 | "tags": [],
7 | "// targets": "to see all targets run: nx show project shared --web",
8 | "targets": {}
9 | }
10 |
--------------------------------------------------------------------------------
/apps/official-website/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 8080;
3 |
4 | add_header Cross-Origin-Resource-Policy cross-origin;
5 |
6 | location / {
7 | root /usr/share/nginx/html;
8 | index index.html index.htm;
9 | try_files $uri $uri/ /index.html;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/apps/official-website/src/app.tsx:
--------------------------------------------------------------------------------
1 | import { RouterProvider, ThemeProvider } from './components/providers';
2 |
3 | import './globals.css';
4 |
5 | const App = () => {
6 | return (
7 |
8 |
9 |
10 | );
11 | };
12 |
13 | export default App;
14 |
--------------------------------------------------------------------------------
/packages/viewer/src/components/spinner-wrapper.tsx:
--------------------------------------------------------------------------------
1 | import styles from '../styles.module.css';
2 |
3 | interface Props {
4 | loader: JSX.Element;
5 | }
6 |
7 | const SpinnerWrapper = ({ loader }: Props) => {
8 | return
{loader}
;
9 | };
10 |
11 | export default SpinnerWrapper;
12 |
--------------------------------------------------------------------------------
/packages/viewer/.storybook/preview.ts:
--------------------------------------------------------------------------------
1 | // Replace your-renderer with the renderer you are using (e.g., react, vue3)
2 | import type { Preview } from '@storybook/react';
3 |
4 | const preview: Preview = {
5 | // ...rest of preview
6 | //👇 Enables auto-generated documentation for all stories
7 | tags: ['autodocs'],
8 | };
9 |
10 | export default preview;
11 |
--------------------------------------------------------------------------------
/shared/src/components/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "../lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/apps/official-website/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 |
4 | import App from './app';
5 |
6 | const container = document.getElementById('root') as HTMLElement;
7 | const root = ReactDOM.createRoot(container);
8 |
9 | root.render(
10 |
11 |
12 | ,
13 | );
14 |
--------------------------------------------------------------------------------
/apps/official-website/src/pages/editor/loading-overlay.tsx:
--------------------------------------------------------------------------------
1 | import { DefaultSpinner } from '@vctrl/shared/components';
2 |
3 | const LoadingOverlay = () => {
4 | return (
5 |
6 |
7 |
8 | );
9 | };
10 |
11 | export default LoadingOverlay;
12 |
--------------------------------------------------------------------------------
/shared/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react-jsx",
4 | "allowJs": false,
5 | "esModuleInterop": false,
6 | "allowSyntheticDefaultImports": true,
7 | "strict": true
8 | },
9 | "files": [],
10 | "include": [],
11 | "references": [
12 | {
13 | "path": "./tsconfig.lib.json"
14 | }
15 | ],
16 | "extends": "../tsconfig.base.json"
17 | }
18 |
--------------------------------------------------------------------------------
/shared/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["plugin:@nx/react", "../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "overrides": [
5 | {
6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7 | "rules": {}
8 | },
9 | {
10 | "files": ["*.ts", "*.tsx"],
11 | "rules": {}
12 | },
13 | {
14 | "files": ["*.js", "*.jsx"],
15 | "rules": {}
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/apps/official-website/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["plugin:@nx/react", "../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "overrides": [
5 | {
6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7 | "rules": {}
8 | },
9 | {
10 | "files": ["*.ts", "*.tsx"],
11 | "rules": {}
12 | },
13 | {
14 | "files": ["*.js", "*.jsx"],
15 | "rules": {}
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/apps/official-website/jest.config.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | export default {
3 | displayName: 'official-website',
4 | preset: '../../jest.preset.js',
5 | transform: {
6 | '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
7 | '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],
8 | },
9 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
10 | coverageDirectory: '../../coverage/apps/official-website',
11 | };
12 |
--------------------------------------------------------------------------------
/packages/hooks/src/use-load-model/loaders/create-usdz-loader.ts:
--------------------------------------------------------------------------------
1 | import { USDZLoader } from 'three/examples/jsm/loaders/USDZLoader';
2 |
3 | import eventSystem from '../event-system';
4 |
5 | function createUsdzLoader() {
6 | const usdzLoader = new USDZLoader();
7 |
8 | usdzLoader.manager.onError = (url) => {
9 | eventSystem.emit('load-error', `Failed to load file ${url}`);
10 | };
11 |
12 | return usdzLoader;
13 | }
14 |
15 | export default createUsdzLoader;
16 |
--------------------------------------------------------------------------------
/apps/official-website/src/components/typography/typography-muted.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren } from 'react';
2 |
3 | import { cn } from '@vctrl/shared/lib/utils';
4 |
5 | interface Props extends PropsWithChildren {
6 | className?: string;
7 | }
8 | export function TypographyMuted({ children, className }: Props) {
9 | return (
10 | {children}
11 | );
12 | }
13 |
14 | export default TypographyMuted;
15 |
--------------------------------------------------------------------------------
/apps/official-website/public/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | #9f00a7
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/packages/hooks/src/use-export-model/types.ts:
--------------------------------------------------------------------------------
1 | export interface URIBufferObject {
2 | uri: string;
3 | byteLength: number;
4 | }
5 |
6 | export interface TexturesObject {
7 | name: string;
8 | sampler: number;
9 | source: number;
10 | }
11 |
12 | export interface ImageDataObject {
13 | data?: Uint8Array | ArrayBuffer;
14 | uri?: string;
15 | mimeType?: string;
16 | }
17 |
18 | export interface ExportResult {
19 | buffers?: ArrayBuffer[] | URIBufferObject[];
20 | images?: ImageDataObject[];
21 | textures?: TexturesObject[];
22 | }
23 |
--------------------------------------------------------------------------------
/packages/hooks/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react-jsx",
4 | "lib": ["ESNext", "DOM", "DOM.Iterable", "DOM.AsyncIterable"],
5 | "allowJs": false,
6 | "esModuleInterop": false,
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true,
9 | "isolatedModules": true,
10 | "noEmit": true,
11 | "types": ["vite/client"]
12 | },
13 | "files": [],
14 | "include": [],
15 | "references": [
16 | {
17 | "path": "./tsconfig.lib.json"
18 | }
19 | ],
20 | "extends": "../../tsconfig.base.json"
21 | }
22 |
--------------------------------------------------------------------------------
/apps/official-website/src/components/modal-close-button.tsx:
--------------------------------------------------------------------------------
1 | import { Cross2Icon } from '@radix-ui/react-icons';
2 | import { Button, ButtonProps } from '@vctrl/shared/components';
3 |
4 | const CloseButton = ({
5 | onClick,
6 | title,
7 | }: Pick) => (
8 |
15 |
16 |
17 | );
18 |
19 | export default CloseButton;
20 |
--------------------------------------------------------------------------------
/packages/viewer/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react-jsx",
4 | "allowJs": false,
5 | "esModuleInterop": false,
6 | "allowSyntheticDefaultImports": true,
7 | "strict": true,
8 | "lib": ["ESNext", "DOM", "DOM.Iterable", "DOM.AsyncIterable"],
9 | "types": ["vite/client"]
10 | },
11 | "files": [],
12 | "include": [],
13 | "references": [
14 | {
15 | "path": "./tsconfig.lib.json"
16 | },
17 | {
18 | "path": "./tsconfig.storybook.json"
19 | }
20 | ],
21 | "extends": "../../tsconfig.base.json"
22 | }
23 |
--------------------------------------------------------------------------------
/apps/official-website/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react-jsx",
4 | "allowJs": false,
5 | "esModuleInterop": false,
6 | "allowSyntheticDefaultImports": true,
7 | "strict": true,
8 | "lib": ["ESNext", "DOM", "DOM.Iterable", "DOM.AsyncIterable"],
9 | "types": ["vite/client", "node"],
10 | },
11 | "files": [],
12 | "include": [],
13 | "references": [
14 | {
15 | "path": "./tsconfig.app.json"
16 | },
17 | {
18 | "path": "./tsconfig.spec.json"
19 | }
20 | ],
21 | "extends": "../../tsconfig.base.json"
22 | }
23 |
--------------------------------------------------------------------------------
/packages/viewer/src/components/scene/index.ts:
--------------------------------------------------------------------------------
1 | export { default as SceneModel } from './scene-model';
2 | export {
3 | default as SceneCamera,
4 | type CameraProps,
5 | defaultCameraOptions,
6 | } from './scene-camera';
7 | export {
8 | default as SceneControls,
9 | type ControlsProps,
10 | defaultControlsOptions,
11 | } from './scene-controls';
12 | export {
13 | default as SceneGrid,
14 | type GridProps,
15 | defaultGridOptions,
16 | } from './scene-grid';
17 | export {
18 | default as SceneEnvironment,
19 | type EnvProps,
20 | defaultEnvOptions,
21 | } from './scene-environment';
22 |
--------------------------------------------------------------------------------
/apps/official-website/src/components/typography/typography-lead.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren } from "react";
2 |
3 | interface Props extends PropsWithChildren {
4 | isHighlighted?: boolean;
5 | }
6 |
7 | export function TypographyLead({ children, isHighlighted }: Props) {
8 | const isHighlightedClass = isHighlighted
9 | ? "text-primary"
10 | : "text-muted-foreground";
11 |
12 | return (
13 |
16 | {children}
17 |
18 | );
19 | }
20 |
21 | export default TypographyLead;
22 |
--------------------------------------------------------------------------------
/apps/official-website/src/lib/hooks/use-is-mobile.ts:
--------------------------------------------------------------------------------
1 | import { useMedia } from '.';
2 |
3 | /**
4 | * Returns true if the user is using a mobile device, determined by the
5 | * presence of a mobile user agent or if the screen width is less than
6 | * 768px.
7 | *
8 | * @returns {boolean} Whether the user is using a mobile device.
9 | */
10 | const useIsMobile = (): boolean => {
11 | const isMobileSize = useMedia('(max-width: 768px)');
12 |
13 | return (
14 | /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent) ||
15 | isMobileSize
16 | );
17 | };
18 |
19 | export default useIsMobile;
20 |
--------------------------------------------------------------------------------
/shared/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../dist/out-tsc",
5 | "types": [
6 | "node",
7 |
8 | "@nx/react/typings/cssmodule.d.ts",
9 | "@nx/react/typings/image.d.ts"
10 | ]
11 | },
12 | "exclude": [
13 | "jest.config.ts",
14 | "src/**/*.spec.ts",
15 | "src/**/*.test.ts",
16 | "src/**/*.spec.tsx",
17 | "src/**/*.test.tsx",
18 | "src/**/*.spec.js",
19 | "src/**/*.test.js",
20 | "src/**/*.spec.jsx",
21 | "src/**/*.test.jsx"
22 | ],
23 | "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
24 | }
25 |
--------------------------------------------------------------------------------
/apps/official-website/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": [
7 | "jest",
8 | "node",
9 | "@nx/react/typings/cssmodule.d.ts",
10 | "@nx/react/typings/image.d.ts"
11 | ]
12 | },
13 | "include": [
14 | "jest.config.ts",
15 | "src/**/*.test.ts",
16 | "src/**/*.spec.ts",
17 | "src/**/*.test.tsx",
18 | "src/**/*.spec.tsx",
19 | "src/**/*.test.js",
20 | "src/**/*.spec.js",
21 | "src/**/*.test.jsx",
22 | "src/**/*.spec.jsx",
23 | "src/**/*.d.ts"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/apps/official-website/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "types": [
6 | "node",
7 | "@nx/react/typings/cssmodule.d.ts",
8 | "@nx/react/typings/image.d.ts",
9 | "vite/client"
10 | ]
11 | },
12 | "exclude": [
13 | "src/**/*.spec.ts",
14 | "src/**/*.test.ts",
15 | "src/**/*.spec.tsx",
16 | "src/**/*.test.tsx",
17 | "src/**/*.spec.js",
18 | "src/**/*.test.js",
19 | "src/**/*.spec.jsx",
20 | "src/**/*.test.jsx"
21 | ],
22 | "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
23 | }
24 |
--------------------------------------------------------------------------------
/packages/hooks/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "noEmit": true,
5 | "isolatedModules": true,
6 | "declaration": true,
7 | "types": [
8 | "node",
9 | "@nx/react/typings/cssmodule.d.ts",
10 | "@nx/react/typings/image.d.ts",
11 | "vite/client"
12 | ]
13 | },
14 | "exclude": [
15 | "**/*.spec.ts",
16 | "**/*.test.ts",
17 | "**/*.spec.tsx",
18 | "**/*.test.tsx",
19 | "**/*.spec.js",
20 | "**/*.test.js",
21 | "**/*.spec.jsx",
22 | "**/*.test.jsx"
23 | ],
24 | "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
25 | }
26 |
--------------------------------------------------------------------------------
/packages/hooks/src/use-export-model/utils/data-uri-to-blob.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Converts a data URI to a Blob.
3 | *
4 | * @param {string} dataURI - The data URI to convert.
5 | *
6 | * @returns {Blob} The Blob object.
7 | */
8 | const dataURItoBlob = (dataURI: string): Blob => {
9 | const byteString = atob(dataURI.split(',')[1]);
10 | const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
11 |
12 | const ab = new ArrayBuffer(byteString.length);
13 | const ia = new Uint8Array(ab);
14 |
15 | for (let i = 0; i < byteString.length; i++) {
16 | ia[i] = byteString.charCodeAt(i);
17 | }
18 |
19 | return new Blob([ab], { type: mimeString });
20 | };
21 |
22 | export default dataURItoBlob;
23 |
--------------------------------------------------------------------------------
/packages/viewer/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "types": [
6 | "node",
7 | "@nx/react/typings/cssmodule.d.ts",
8 | "@nx/react/typings/image.d.ts",
9 | "vite/client"
10 | ]
11 | },
12 | "exclude": [
13 | "**/*.spec.ts",
14 | "**/*.test.ts",
15 | "**/*.spec.tsx",
16 | "**/*.test.tsx",
17 | "**/*.spec.js",
18 | "**/*.test.js",
19 | "**/*.spec.jsx",
20 | "**/*.test.jsx",
21 | "**/*.stories.ts",
22 | "**/*.stories.js",
23 | "**/*.stories.jsx",
24 | "**/*.stories.tsx"
25 | ],
26 | "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
27 | }
28 |
--------------------------------------------------------------------------------
/packages/viewer/src/components/assets/vectreal-logo.tsx:
--------------------------------------------------------------------------------
1 | import { IconProps } from './types';
2 |
3 | const VectrealLogo = ({ className }: IconProps) => {
4 | return (
5 |
6 |
7 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default VectrealLogo;
17 |
--------------------------------------------------------------------------------
/shared/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './accordion';
2 | export * from './avatar';
3 | export * from './background-beams';
4 | export * from './badge';
5 | export * from './button';
6 | export * from './card';
7 | export * from './default-spinner';
8 | export * from './dialog';
9 | export * from './dropdown-menu';
10 | export * from './glowing-stars';
11 | export * from './hero-highlight';
12 | export * from './input';
13 | export * from './label';
14 | export * from './menubar';
15 | export * from './navigation-menu';
16 | export * from './popover';
17 | export * from './progress';
18 | export * from './sheet';
19 | export * from './skeleton';
20 | export * from './sonner';
21 | export * from './textarea';
22 | export * from './tooltip';
23 | export * from './typewriter-effect';
24 |
--------------------------------------------------------------------------------
/apps/official-website/src/components/assets/google-icon.tsx:
--------------------------------------------------------------------------------
1 | interface GoogleIconProps {
2 | className?: string;
3 | }
4 |
5 | const GoogleLogoIcon = ({ className }: GoogleIconProps) => {
6 | return (
7 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default GoogleLogoIcon;
--------------------------------------------------------------------------------
/packages/hooks/src/use-load-model/utils/array-buffer-to-base64.ts:
--------------------------------------------------------------------------------
1 | // Helper function to convert ArrayBuffer to base64
2 | async function arrayBufferToBase64(arrayBuffer: ArrayBuffer) {
3 | // Create a Blob from the ArrayBuffer
4 | const blob = new Blob([arrayBuffer]);
5 |
6 | // Use the FileReader to convert the Blob to a Base64 string
7 | const reader = new FileReader();
8 | return new Promise((resolve, reject) => {
9 | reader.onloadend = () => {
10 | if (!reader?.result) return;
11 | // Extract the Base64 string from the data URL
12 | const base64String = (reader?.result as string).split(',')[1];
13 | resolve(base64String);
14 | };
15 | reader.onerror = reject;
16 | reader.readAsDataURL(blob);
17 | });
18 | }
19 |
20 | export default arrayBufferToBase64;
21 |
--------------------------------------------------------------------------------
/apps/official-website/src/components/title-section.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren } from 'react';
2 |
3 | import { BackgroundBeams } from '@vctrl/shared/components';
4 |
5 | import { useIsMobile } from '../lib/hooks';
6 |
7 | interface TitleSectionProps extends PropsWithChildren {
8 | className?: string;
9 | heading: string;
10 | }
11 |
12 | const TitleSection = ({ children, heading, className }: TitleSectionProps) => {
13 | const isMobile = useIsMobile();
14 |
15 | return (
16 |
17 | {!isMobile && }
18 | {heading}
19 | {children}
20 |
21 | );
22 | };
23 |
24 | export default TitleSection;
25 |
--------------------------------------------------------------------------------
/shared/src/components/default-spinner.tsx:
--------------------------------------------------------------------------------
1 | export const DefaultSpinner = () => {
2 | return (
3 |
9 |
13 |
14 |
21 |
22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/shared/src/components/label.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "../lib/utils"
6 |
7 | const labelVariants = cva(
8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
9 | )
10 |
11 | const Label = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef &
14 | VariantProps
15 | >(({ className, ...props }, ref) => (
16 |
21 | ))
22 | Label.displayName = LabelPrimitive.Root.displayName
23 |
24 | export { Label }
25 |
--------------------------------------------------------------------------------
/.github/workflows/lint-build.yaml:
--------------------------------------------------------------------------------
1 | name: Lint and build affected NX projects
2 |
3 | on:
4 | pull_request:
5 | types: [opened, synchronize, reopened]
6 |
7 | jobs:
8 | lint-build:
9 | runs-on: ubuntu-22.04
10 | strategy:
11 | matrix:
12 | node-version: [22]
13 | steps:
14 | - uses: actions/checkout@v4
15 | with:
16 | fetch-depth: 0
17 |
18 | - name: Use Node.js ${{ matrix.node-version }}
19 | uses: actions/setup-node@v4
20 | with:
21 | node-version: ${{ matrix.node-version }}
22 | cache: 'npm'
23 |
24 | - name: Install dependencies
25 | run: npm ci
26 |
27 | - name: Lint
28 | run: npx nx affected --target=lint --base=origin/${{ github.event.pull_request.base.ref }} --head=origin/${{ github.event.pull_request.head.ref }}
29 |
--------------------------------------------------------------------------------
/apps/official-website/Dockerfile:
--------------------------------------------------------------------------------
1 | # Build stage
2 | FROM node:20-alpine AS build-stage
3 |
4 | WORKDIR /app
5 |
6 | # Copy only package.json and package-lock.json (or yarn.lock) first
7 | COPY package*.json ./
8 |
9 | # Install dependencies
10 | RUN npm i
11 |
12 | # Copy the rest of the application code
13 | COPY . .
14 |
15 | # Build the application
16 | RUN npx nx build vctrl/official-website
17 |
18 | # Production stage
19 | FROM nginx:alpine
20 |
21 | # Copy the built app from the previous stage
22 | COPY --from=build-stage /app/dist/apps/official-website /usr/share/nginx/html
23 |
24 | # Remove the default nginx configuration
25 | RUN rm /etc/nginx/conf.d/default.conf
26 |
27 | # Copy the nginx configuration file
28 | COPY ./apps/official-website/nginx.conf /etc/nginx/conf.d
29 |
30 | EXPOSE 80
31 |
32 | CMD ["nginx", "-g", "daemon off;"]
--------------------------------------------------------------------------------
/shared/src/components/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '../lib/utils';
4 |
5 | export type TextareaProps = React.TextareaHTMLAttributes;
6 |
7 | const Textarea = React.forwardRef(
8 | ({ className, ...props }, ref) => {
9 | return (
10 |
18 | );
19 | },
20 | );
21 | Textarea.displayName = 'Textarea';
22 |
23 | export { Textarea };
24 |
--------------------------------------------------------------------------------
/apps/official-website/src/lib/hooks/use-init-ga.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useLocation } from 'react-router-dom';
3 | import ReactGA from 'react-ga4';
4 |
5 | import { pageview } from '../utils/ga-utils';
6 |
7 | /**
8 | * Custom hook to initialize Google Analytics (GA) and track page views.
9 | *
10 | * This hook uses the `useLocation` hook to get the current router location and
11 | * the `useEffect` hook to initialize GA if it hasn't been initialized yet.
12 | * It also tracks page views whenever the pathname changes.
13 | */
14 | const useInitGA = () => {
15 | const router = useLocation();
16 |
17 | useEffect(() => {
18 | if (!ReactGA.isInitialized) {
19 | ReactGA.initialize('G-NQ71FMDTPE');
20 | }
21 |
22 | pageview(router.pathname);
23 | }, [router.pathname]);
24 | };
25 |
26 | export default useInitGA;
27 |
--------------------------------------------------------------------------------
/apps/official-website/public/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | https://core.vectreal.com
5 | 2024-09-11T21:41:33+00:00
6 | 1.00
7 |
8 |
9 | https://core.vectreal.com/editor
10 | 2024-09-11T21:41:33+00:00
11 | 1.00
12 |
13 |
14 | https://core.vectreal.com/faq
15 | 2024-09-11T21:41:33+00:00
16 | 0.50
17 |
18 |
19 | https://core.vectreal.com/contact
20 | 2024-09-11T21:41:33+00:00
21 | 0.20
22 |
23 |
--------------------------------------------------------------------------------
/packages/viewer/tsconfig.storybook.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "emitDecoratorMetadata": true,
5 | "outDir": ""
6 | },
7 | "files": [
8 | "../../node_modules/@nx/react/typings/styled-jsx.d.ts",
9 | "../../node_modules/@nx/react/typings/cssmodule.d.ts",
10 | "../../node_modules/@nx/react/typings/image.d.ts"
11 | ],
12 | "exclude": [
13 | "src/**/*.spec.ts",
14 | "src/**/*.test.ts",
15 | "src/**/*.spec.js",
16 | "src/**/*.test.js",
17 | "src/**/*.spec.tsx",
18 | "src/**/*.test.tsx",
19 | "src/**/*.spec.jsx",
20 | "src/**/*.test.js"
21 | ],
22 | "include": [
23 | "src/**/*.stories.ts",
24 | "src/**/*.stories.js",
25 | "src/**/*.stories.jsx",
26 | "src/**/*.stories.tsx",
27 | "src/**/*.stories.mdx",
28 | ".storybook/*.js",
29 | ".storybook/*.ts"
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "ignorePatterns": ["**/*"],
4 | "plugins": ["@nx"],
5 | "overrides": [
6 | {
7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
8 | "rules": {
9 | "@nx/enforce-module-boundaries": [
10 | "error",
11 | {
12 | "enforceBuildableLibDependency": true,
13 | "allow": [],
14 | "depConstraints": [
15 | {
16 | "sourceTag": "*",
17 | "onlyDependOnLibsWithTags": ["*"]
18 | }
19 | ]
20 | }
21 | ]
22 | }
23 | },
24 | {
25 | "files": ["*.ts", "*.tsx"],
26 | "extends": ["plugin:@nx/typescript"],
27 | "rules": {}
28 | },
29 | {
30 | "files": ["*.js", "*.jsx"],
31 | "extends": ["plugin:@nx/javascript"],
32 | "rules": {}
33 | }
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/apps/official-website/src/lib/hooks/use-media.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | /**
4 | * React hook that tracks whether a CSS media query matches the current viewport.
5 | *
6 | * @param {string} query - The media query string (e.g. "(max-width: 600px)")
7 | * @returns {boolean} Whether the media query matches the current viewport
8 | */
9 | const useMedia = (query: string): boolean => {
10 | const [matches, setMatches] = useState(false);
11 |
12 | useEffect(() => {
13 | const media = window.matchMedia(query);
14 | if (media.matches !== matches) {
15 | setMatches(media.matches);
16 | }
17 |
18 | const listener = () => setMatches(media.matches);
19 | media.addEventListener('change', listener);
20 | return () => media.removeEventListener('change', listener);
21 | }, [matches, query]);
22 |
23 | return matches;
24 | };
25 |
26 | export default useMedia;
27 |
--------------------------------------------------------------------------------
/shared/src/components/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '../lib/utils';
4 |
5 | export type InputProps = React.InputHTMLAttributes;
6 |
7 | const Input = React.forwardRef(
8 | ({ className, type, ...props }, ref) => {
9 | return (
10 |
19 | );
20 | },
21 | );
22 | Input.displayName = 'Input';
23 |
24 | export { Input };
25 |
--------------------------------------------------------------------------------
/shared/src/components/progress.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ProgressPrimitive from "@radix-ui/react-progress"
3 |
4 | import { cn } from "../lib/utils"
5 |
6 | const Progress = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, value, ...props }, ref) => (
10 |
18 |
22 |
23 | ))
24 | Progress.displayName = ProgressPrimitive.Root.displayName
25 |
26 | export { Progress }
27 |
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "rootDir": ".",
5 | "sourceMap": true,
6 | "declaration": false,
7 | "moduleResolution": "node",
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "importHelpers": true,
11 | "target": "es2015",
12 | "module": "esnext",
13 | "lib": ["es2020", "dom"],
14 | "skipLibCheck": true,
15 | "skipDefaultLibCheck": true,
16 | "baseUrl": ".",
17 | "paths": {
18 | "@vctrl/hooks": ["packages/hooks/src/index.ts"],
19 | "@vctrl/hooks/*": ["packages/hooks/src/*"],
20 | "@vctrl/official-website/*": ["apps/official-website/src/*"],
21 | "@vctrl/shared/*": ["shared/src/*"],
22 | "@vctrl/viewer": ["packages/viewer/src/index.ts"],
23 | "@vctrl/viewer/*": ["packages/viewer/src/*"]
24 | }
25 | },
26 | "exclude": ["node_modules", "tmp"]
27 | }
28 |
--------------------------------------------------------------------------------
/shared/src/components/sonner.tsx:
--------------------------------------------------------------------------------
1 | import { Toaster as Sonner } from 'sonner';
2 |
3 | type ToasterProps = React.ComponentProps;
4 |
5 | const Toaster = ({ ...props }: ToasterProps) => {
6 | return (
7 |
23 | );
24 | };
25 |
26 | export { Toaster };
27 |
--------------------------------------------------------------------------------
/packages/hooks/src/use-load-model/utils/read-directory.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Recursively reads a directory
3 | *
4 | * @param directoryHandle - The directory handle to read
5 | * @returns - An array of file objects
6 | */
7 | async function readDirectory(
8 | directoryHandle: FileSystemDirectoryHandle,
9 | ): Promise {
10 | const files: File[] = [];
11 |
12 | async function* getFilesRecursively(
13 | entry: FileSystemDirectoryHandle,
14 | ): AsyncGenerator {
15 | for await (const [, handle] of entry) {
16 | if (handle.kind === 'file') {
17 | yield await (handle as FileSystemFileHandle).getFile();
18 | } else if (handle.kind === 'directory') {
19 | yield* getFilesRecursively(handle as FileSystemDirectoryHandle);
20 | }
21 | }
22 | }
23 |
24 | for await (const file of getFilesRecursively(directoryHandle)) {
25 | files.push(file);
26 | }
27 |
28 | return files;
29 | }
30 |
31 | export default readDirectory;
32 |
--------------------------------------------------------------------------------
/apps/official-website/src/lib/utils/ga-utils.ts:
--------------------------------------------------------------------------------
1 | import ReactGA from 'react-ga4';
2 |
3 | /**
4 | * Sends a custom event to Google Analytics.
5 | *
6 | * @param args - The event parameters.
7 | * @param args.category - The category of the event.
8 | * @param args.action - The action of the event.
9 | * @param args.label - The label of the event.
10 | *
11 | * @remarks
12 | * This function will only send the event if Google Analytics is initialized.
13 | */
14 | export function sendCustomEvent(args: {
15 | category: string;
16 | action: string;
17 | label: string;
18 | }) {
19 | if (!ReactGA.isInitialized) return;
20 |
21 | ReactGA.event(args);
22 | }
23 |
24 | /**
25 | * Sends a pageview event to Google Analytics.
26 | *
27 | * @param pathname - The path of the page being viewed.
28 | */
29 | export function pageview(pathname: string) {
30 | ReactGA.send({
31 | hitType: 'pageview',
32 | page: pathname,
33 | title: document.title,
34 | });
35 | }
36 |
--------------------------------------------------------------------------------
/packages/hooks/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["plugin:@nx/react", "../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "overrides": [
5 | {
6 | "files": ["*.json"],
7 | "parser": "jsonc-eslint-parser",
8 | "rules": {
9 | "@nx/dependency-checks": [
10 | "error",
11 | {
12 | "includeTransitiveDependencies": true,
13 | "ignoredDependencies": [
14 | "@nx/vite",
15 | "@vitejs/plugin-react",
16 | "clsx",
17 | "react-loader-spinner",
18 | "vite",
19 | "vite-plugin-dts"
20 | ]
21 | }
22 | ]
23 | }
24 | },
25 | {
26 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
27 | "rules": {}
28 | },
29 | {
30 | "files": ["*.ts", "*.tsx"],
31 | "rules": {}
32 | },
33 | {
34 | "files": ["*.js", "*.jsx"],
35 | "rules": {}
36 | }
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/packages/hooks/src/use-export-model/utils/file-helpers.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Determines the extension of a file based on its URI and MIME type.
3 | *
4 | * If the MIME type is not provided, it is inferred from the URI.
5 | *
6 | * Returns a default `png` extension if the extension is unknown.
7 | *
8 | * @param {string} uri - The URI of the image
9 | * @param {string} mimeType - The MIME type of the image
10 | */
11 | export function getFileExtension(uri: string, mimeType?: string): string {
12 | const uriExtension = uri.split('.').pop()?.split('?')[0];
13 | if (uriExtension && uriExtension !== uri) return uriExtension;
14 |
15 | if (mimeType) {
16 | return mimeType.split('/').pop() || 'png';
17 | }
18 |
19 | return 'png';
20 | }
21 |
22 | /**
23 | * Extracts the base name of a file from its URI.
24 | *
25 | * @param {string} filename - The name of the file
26 | */
27 | export function getFileBasename(filename: string) {
28 | return filename?.split('.').shift() || 'scene';
29 | }
30 |
--------------------------------------------------------------------------------
/packages/hooks/src/use-optimize-model/types.ts:
--------------------------------------------------------------------------------
1 | import { Document } from '@gltf-transform/core';
2 | import { InspectReport } from '@gltf-transform/functions';
3 |
4 | /**
5 | * Interface representing the size details of the model.
6 | */
7 | export interface ModelSize {
8 | /** The file size in bytes. */
9 | fileSize: number;
10 | /** The file size formatted as a human-readable string. */
11 | displayFileSize: string;
12 | }
13 |
14 | /**
15 | * Interface representing the state of the model optimizer.
16 | */
17 | export interface State {
18 | modelDoc: Document | null;
19 | modelReport: InspectReport | null;
20 | error: Error | null;
21 | loading: boolean;
22 | }
23 |
24 | /**
25 | * Types of actions for the reducer.
26 | */
27 | export type Action =
28 | | { type: 'LOAD_START' }
29 | | {
30 | type: 'LOAD_SUCCESS';
31 | payload: { modelDoc: Document; modelReport: InspectReport };
32 | }
33 | | { type: 'LOAD_ERROR'; payload: Error }
34 | | { type: 'RESET' };
35 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "npm" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 | labels:
13 | - "npm"
14 | - "dependencies"
15 | - "nx-updates"
16 | groups:
17 | npm-dependencies:
18 | patterns:
19 | - "@nx" # groups nx npm dependencies
20 |
21 |
22 | # GitHub Actions dependencies
23 | - package-ecosystem: "github-actions"
24 | directory: "/"
25 | schedule:
26 | interval: "weekly"
27 | labels:
28 | - "github-actions"
29 | - "dependencies"
--------------------------------------------------------------------------------
/shared/src/components/hero-highlight.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from 'framer-motion';
2 | import { cn } from '../lib/utils';
3 |
4 | export const Highlight = ({
5 | children,
6 | className,
7 | }: {
8 | children: React.ReactNode;
9 | className?: string;
10 | }) => {
11 | return (
12 |
34 | {children}
35 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/packages/viewer/src/components/assets/cross-icon.tsx:
--------------------------------------------------------------------------------
1 | import { IconProps } from './types';
2 |
3 | const CrossIcon = ({ className }: IconProps) => (
4 |
12 |
18 |
19 | );
20 |
21 | export default CrossIcon;
22 |
--------------------------------------------------------------------------------
/packages/viewer/.storybook/main.ts:
--------------------------------------------------------------------------------
1 | import { dirname, join } from 'path';
2 | import type { StorybookConfig } from '@storybook/react-vite';
3 |
4 | const config: StorybookConfig = {
5 | stories: ['../src/**/*.@(mdx|stories.@(js|jsx|ts|tsx))'],
6 | addons: [
7 | getAbsolutePath('@storybook/addon-essentials'),
8 | getAbsolutePath('@storybook/addon-interactions'),
9 | getAbsolutePath('storybook-addon-deep-controls'),
10 | ],
11 | framework: {
12 | name: getAbsolutePath('@storybook/react-vite'),
13 | options: {
14 | builder: {
15 | viteConfigPath: 'vite.config.ts',
16 | },
17 | },
18 | },
19 | };
20 |
21 | export default config;
22 |
23 | // To customize your Vite configuration you can use the viteFinal field.
24 | // Check https://storybook.js.org/docs/react/builders/vite#configuration
25 | // and https://nx.dev/recipes/storybook/custom-builder-configs
26 |
27 | function getAbsolutePath(value: string): string {
28 | return dirname(require.resolve(join(value, 'package.json')));
29 | }
30 |
--------------------------------------------------------------------------------
/apps/official-website/src/components/footer.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 |
3 | const Footer = () => (
4 |
5 |
6 |
7 | Home
8 | Help
9 |
10 |
11 | Editor
12 |
13 | Vectreal
14 |
15 |
16 |
17 | Privacy Policy
18 | Terms of Service
19 | Contact
20 |
21 |
22 |
23 | © 2024 All Rights Reserved - Vectreal Core
24 |
25 |
26 | );
27 |
28 | export default Footer;
29 |
--------------------------------------------------------------------------------
/apps/official-website/src/lib/hooks/use-accept-pattern.ts:
--------------------------------------------------------------------------------
1 | import { useIsMobile } from '.';
2 |
3 | /**
4 | * Returns an accept pattern for use with the file input component.
5 | *
6 | * The returned value is either the default accept pattern for the file input component,
7 | * or a pattern that includes all the allowed file types if the user is using a mobile device.
8 | *
9 | * This is because the accept pattern does not correctly include files when trying to upload from an iphone.
10 | *
11 | * @returns The accept pattern for the file input component.
12 | */
13 | const useAcceptPattern = (): string => {
14 | const isMobile = useIsMobile();
15 |
16 | /**
17 | * The default accept pattern for the file input component.
18 | * This includes all the allowed file types.
19 | */
20 | const acceptPattern =
21 | 'model/gltf-binary,.glb,model/gltf+json,.gltf,.bin,model/vnd.usdz+zip,.usdz,model/vnd.usda,.usda,image/jpeg,.jpeg,.jpg,image/png,.png';
22 |
23 | return isMobile ? '*' : acceptPattern;
24 | };
25 |
26 | export default useAcceptPattern;
--------------------------------------------------------------------------------
/packages/viewer/src/components/scene/scene-model.tsx:
--------------------------------------------------------------------------------
1 | import { Object3D } from 'three';
2 | import { Stage } from '@react-three/drei';
3 | import { defaultEnvOptions, EnvProps } from './scene-environment';
4 |
5 | interface ModelProps {
6 | /**
7 | * The 3D object (three.js `Object3D`) to render in the scene.
8 | */
9 | object: null | Object3D;
10 |
11 | /**
12 | * Environment options - specifically the props available on the "@react-three/drei" `Stage` component.
13 | */
14 | envOptions?: EnvProps['stage'];
15 | }
16 |
17 | /**
18 | * SceneModel component that renders a 3D model in a `Stage`.
19 | */
20 | const SceneModel = (props: ModelProps) => {
21 | const { object, envOptions } = {
22 | ...props,
23 | envOptions: {
24 | ...defaultEnvOptions.stage,
25 | ...props.envOptions,
26 | },
27 | };
28 |
29 | if (!object) return null;
30 |
31 | return (
32 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default SceneModel;
39 |
--------------------------------------------------------------------------------
/packages/viewer/src/components/scene/scene-environment.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps, CSSProperties } from 'react';
2 | import { Environment, EnvironmentProps, Stage } from '@react-three/drei';
3 |
4 | export interface EnvProps {
5 | /**
6 | * Optional environment properties.
7 | */
8 | env?: EnvironmentProps;
9 |
10 | /**
11 | * Optional properties for the Stage component.
12 | */
13 | stage?: ComponentProps;
14 |
15 | /**
16 | * Optional background color for the viewer.
17 | */
18 | backgroundColor?: CSSProperties['backgroundColor'];
19 | }
20 |
21 | export const defaultEnvOptions: EnvProps = {
22 | env: {
23 | preset: 'apartment',
24 | },
25 | stage: {
26 | intensity: 0.1,
27 | adjustCamera: 1.5,
28 | environment: null,
29 | },
30 | };
31 |
32 | /**
33 | * SceneEnvironment component that sets up the environment for a scene.
34 | */
35 | const SceneEnvironment = (props: EnvProps['env']) => {
36 | return ;
37 | };
38 |
39 | export default SceneEnvironment;
40 |
--------------------------------------------------------------------------------
/packages/hooks/src/use-optimize-model/state.ts:
--------------------------------------------------------------------------------
1 | import { Action, State } from './types';
2 |
3 | /**
4 | * Initial state for the reducer.
5 | */
6 | export const initialState: State = {
7 | modelDoc: null,
8 | modelReport: null,
9 | error: null,
10 | loading: false,
11 | };
12 |
13 | /**
14 | * Reducer function to manage state transitions.
15 | *
16 | * @param state - Current state.
17 | * @param action - Action to perform.
18 | * @returns New state after applying the action.
19 | */
20 | export const reducer = (state: State, action: Action): State => {
21 | switch (action.type) {
22 | case 'LOAD_START':
23 | return { ...state, loading: true, error: null };
24 | case 'LOAD_SUCCESS':
25 | return {
26 | ...state,
27 | loading: false,
28 | modelDoc: action.payload.modelDoc,
29 | modelReport: action.payload.modelReport,
30 | };
31 | case 'LOAD_ERROR':
32 | return { ...state, loading: false, error: action.payload };
33 | case 'RESET':
34 | return { ...initialState };
35 | default:
36 | return state;
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/packages/viewer/src/components/scene/scene-controls.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { OrbitControls, OrbitControlsProps } from '@react-three/drei';
3 |
4 | export interface ControlsProps extends OrbitControlsProps {
5 | /**
6 | * The timeout duration in milliseconds before enabling the controls.
7 | */
8 | controlsTimeout?: number;
9 | }
10 |
11 | export const defaultControlsOptions: ControlsProps = {
12 | controlsTimeout: 1500,
13 | maxPolarAngle: Math.PI / 2,
14 | autoRotate: true,
15 | makeDefault: true,
16 | };
17 |
18 | /**
19 | * SceneControls component that enables orbit controls after a specified timeout.
20 | */
21 | const SceneControls = (props: ControlsProps) => {
22 | const { controlsTimeout, ...rest } = { ...defaultControlsOptions, ...props };
23 | const [isControlsEnabled, setIsControlsEnabled] = useState(false);
24 |
25 | useEffect(() => {
26 | const timeoutId = setTimeout(() => {
27 | setIsControlsEnabled(true);
28 | }, controlsTimeout);
29 |
30 | return () => clearTimeout(timeoutId);
31 | }, [controlsTimeout]);
32 |
33 | return isControlsEnabled && ;
34 | };
35 |
36 | export default SceneControls;
37 |
--------------------------------------------------------------------------------
/packages/hooks/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vctrl/hooks",
3 | "$schema": "../../node_modules/nx/schemas/project-schema.json",
4 | "sourceRoot": "packages/hooks/src",
5 | "projectType": "library",
6 | "tags": [],
7 | "targets": {
8 | "lint": {
9 | "executor": "@nx/eslint:lint",
10 | "outputs": ["{options.outputFile}"],
11 | "options": {
12 | "fix": true,
13 | "lintFilePatterns": [
14 | "packages/hooks/**/*.{ts,tsx,js,jsx}",
15 | "packages/hooks/package.json"
16 | ]
17 | }
18 | },
19 | "nx-release-publish": {
20 | "dependsOn": ["copy-md"],
21 | "options": {
22 | "packageRoot": "dist/packages/vctrl/hooks"
23 | }
24 | },
25 | "copy-md": {
26 | "dependsOn": ["build"],
27 | "executor": "nx:run-commands",
28 | "outputs": [],
29 | "options": {
30 | "commands": [
31 | "rsync -rat packages/hooks/*.md dist/packages/vctrl/hooks/"
32 | ]
33 | }
34 | },
35 | "build": {
36 | "executor": "@nx/vite:build",
37 | "options": {
38 | "outputPath": "dist/packages/vctrl/hooks",
39 | "configFile": "packages/hooks/vite.config.ts"
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/.github/workflows/version-release.yaml:
--------------------------------------------------------------------------------
1 | name: Version and Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*.*.*'
7 |
8 | jobs:
9 | versioning:
10 | name: Versioning and Publishing to NPM
11 | environment: packages-releasing
12 | runs-on: ubuntu-22.04
13 | strategy:
14 | matrix:
15 | node-version: [22]
16 |
17 | steps:
18 | - name: Checkout code
19 | uses: actions/checkout@v4
20 | with:
21 | fetch-depth: 0
22 |
23 | - name: Setup Node.js
24 | uses: actions/setup-node@v4
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | cache: 'npm'
28 | registry-url: 'https://registry.npmjs.org'
29 |
30 | - name: Install dependencies
31 | run: npm install
32 |
33 | - name: Setup Git
34 | run: |
35 | git config --global user.email "<>"
36 | git config --global user.name "github-actions[bot]"
37 |
38 | - name: Print Environment Info
39 | run: npx nx report
40 | shell: bash
41 |
42 | - name: Publish packages
43 | run: npx nx release publish
44 | shell: bash
45 | env:
46 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
47 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48 |
--------------------------------------------------------------------------------
/shared/src/components/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "../lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/shared/src/components/tooltip.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as TooltipPrimitive from '@radix-ui/react-tooltip';
5 |
6 | import { cn } from '../lib/utils';
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider;
9 |
10 | const Tooltip = TooltipPrimitive.Root;
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger;
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ));
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
31 |
--------------------------------------------------------------------------------
/packages/viewer/src/components/assets/info-icon.tsx:
--------------------------------------------------------------------------------
1 | import { IconProps } from './types';
2 |
3 | const InfoIcon = ({ className }: IconProps) => (
4 |
12 |
18 |
19 | );
20 |
21 | export default InfoIcon;
22 |
--------------------------------------------------------------------------------
/shared/src/components/popover.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as PopoverPrimitive from '@radix-ui/react-popover';
5 |
6 | import { cn } from '../lib/utils';
7 |
8 | const Popover = PopoverPrimitive.Root;
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger;
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ));
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
30 |
31 | export { Popover, PopoverTrigger, PopoverContent };
32 |
--------------------------------------------------------------------------------
/packages/viewer/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vctrl/viewer",
3 | "$schema": "../../node_modules/nx/schemas/project-schema.json",
4 | "sourceRoot": "packages/viewer/src",
5 | "projectType": "library",
6 | "tags": [],
7 | "targets": {
8 | "lint": {
9 | "executor": "@nx/eslint:lint",
10 | "outputs": ["{options.outputFile}"],
11 | "options": {
12 | "fix": true,
13 | "lintFilePatterns": [
14 | "packages/viewer/**/*.{ts,tsx,js,jsx}",
15 | "packages/viewer/package.json"
16 | ]
17 | }
18 | },
19 | "nx-release-publish": {
20 | "dependsOn": ["copy-md"],
21 | "options": {
22 | "packageRoot": "dist/packages/vctrl/viewer"
23 | }
24 | },
25 | "copy-md": {
26 | "dependsOn": ["build"],
27 | "executor": "nx:run-commands",
28 | "outputs": [],
29 | "options": {
30 | "commands": [
31 | "rsync -rat packages/viewer/*.md dist/packages/vctrl/viewer/"
32 | ]
33 | }
34 | },
35 | "build": {
36 | "executor": "@nx/vite:build",
37 | "options": {
38 | "outputPath": "dist/packages/vctrl/viewer",
39 | "configFile": "packages/viewer/vite.config.ts"
40 | }
41 | },
42 | "chromatic": {
43 | "executor": "nx:run-commands",
44 | "options": {
45 | "commands": ["npm --prefix packages/viewer run chromatic"]
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/apps/official-website/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Vectreal Core - Your Online 3D Optimizing Toolkit | React 3D Viewer & Hooks",
3 | "short_name": "Vectreal Core",
4 | "description": "Vectreal Core: Open-source React components and hooks for seamless 3D model integration. Optimize and view GLTF, GLB, OBJ, USDZ models easily. Start your 3D web journey today!",
5 | "start_url": "/",
6 | "display": "standalone",
7 | "background_color": "#141414",
8 | "theme_color": "#141414",
9 | "icons": [
10 | {
11 | "src": "/android-chrome-192x192.png?v=0.8.9",
12 | "sizes": "192x192",
13 | "type": "image/png",
14 | "purpose": "maskable"
15 | },
16 | {
17 | "src": "/android-chrome-512x512.png?v=0.8.9",
18 | "sizes": "512x512",
19 | "type": "image/png",
20 | "purpose": "maskable"
21 | },
22 | {
23 | "src": "/apple-touch-icon.png?v=0.8.9",
24 | "sizes": "180x180",
25 | "type": "image/png"
26 | }
27 | ],
28 | "screenshots": [
29 | {
30 | "src": "/screenshot-desktop.png",
31 | "sizes": "1280x720",
32 | "type": "image/png",
33 | "label": "Desktop Screenshot"
34 | },
35 | {
36 | "src": "/screenshot-mobile.png",
37 | "sizes": "360x640",
38 | "type": "image/png",
39 | "label": "Mobile Screenshot"
40 | }
41 | ],
42 | "msapplication-TileColor": "#141414",
43 | "msapplication-TileImage": "/mstile-144x144.png?v=0.8.9"
44 | }
45 |
--------------------------------------------------------------------------------
/.github/workflows/chromatic-vctrl-viewer.yaml:
--------------------------------------------------------------------------------
1 | name: Publish vctrl/viewer Storybook to Chromatic
2 |
3 | #exclude dependabot and renovate branches
4 | on:
5 | push:
6 | branches-ignore:
7 | - 'dependabot/**'
8 | - 'renovate/**'
9 | - 'chore/**'
10 |
11 | concurrency:
12 | group: ${{ github.workflow }}-${{ github.ref }}-chromatic
13 | cancel-in-progress: true
14 |
15 | permissions:
16 | actions: write
17 | contents: read
18 |
19 | jobs:
20 | chromatic:
21 | name: Chromatic
22 | runs-on: ubuntu-20.04
23 | environment: chromatic-publishing
24 |
25 | steps:
26 | - name: ⬇️ Checkout repo
27 | uses: actions/checkout@v4
28 | with:
29 | fetch-depth: 0
30 |
31 | - name: ⎔ Setup node
32 | uses: actions/setup-node@v4
33 | with:
34 | cache: npm
35 | cache-dependency-path: ./package.json
36 | node-version: 20
37 |
38 | - name: 📥 Install deps
39 | run: npm install
40 |
41 | - name: Build Storybook
42 | run: npx nx build-storybook vctrl/viewer
43 |
44 | - name: ⚡ Run chromatic
45 | uses: chromaui/action@latest
46 | # Chromatic GitHub Action options
47 | with:
48 | projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN_1 }}
49 | workingDir: packages/viewer
50 | autoAcceptChanges: 'main'
51 | exitZeroOnChanges: true
52 | exitOnceUploaded: true
53 | onlyChanged: true
54 | skip: '@(renovate/**|dependabot/**|chore/)'
55 |
--------------------------------------------------------------------------------
/apps/official-website/src/components/providers/router-provider.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | RouterProvider as BaseRouterProvider,
3 | createBrowserRouter,
4 | } from 'react-router-dom';
5 | import { lazy, Suspense } from 'react';
6 | import { DefaultSpinner } from '@vctrl/shared/components';
7 |
8 | import BaseLayout from '../base-layout';
9 | import Home from '../../pages/home';
10 | const Help = lazy(() => import('../../pages/help'));
11 | const Contact = lazy(() => import('../../pages/contact'));
12 | const Editor = lazy(() => import('../../pages/editor'));
13 |
14 | const RouterProvider = () => {
15 | const router = createBrowserRouter([
16 | {
17 | path: '/',
18 | element: ,
19 | children: [
20 | {
21 | path: '/editor',
22 | element: ,
23 | },
24 | {
25 | path: '/help',
26 | element: ,
27 | },
28 | {
29 | path: '/contact',
30 | element: ,
31 | },
32 | {
33 | path: '/',
34 | element: ,
35 | },
36 | {
37 | path: '*',
38 | element: ,
39 | },
40 | ],
41 | },
42 | ]);
43 |
44 | return (
45 |
48 |
49 |
50 | }
51 | >
52 |
53 |
54 | );
55 | };
56 |
57 | export default RouterProvider;
58 |
--------------------------------------------------------------------------------
/packages/hooks/src/use-load-model/model-context.tsx:
--------------------------------------------------------------------------------
1 | /* vectreal-core | vctrl/hooks
2 | Copyright (C) 2024 Moritz Becker
3 |
4 | This program is free software: you can redistribute it and/or modify
5 | it under the terms of the GNU Affero General Public License as published by
6 | the Free Software Foundation, either version 3 of the License, or
7 | (at your option) any later version.
8 |
9 | This program is distributed in the hope that it will be useful,
10 | but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | GNU Affero General Public License for more details.
13 |
14 | You should have received a copy of the GNU Affero General Public License
15 | along with this program. If not, see . */
16 |
17 | import { createContext, useContext } from 'react';
18 |
19 | import { useOptimizeModel } from '../use-optimize-model';
20 | import useLoadModel from './use-load-model';
21 |
22 | interface ModelProviderProps extends React.PropsWithChildren {
23 | optimizer?: ReturnType;
24 | }
25 |
26 | const ModelContext = createContext({} as ReturnType);
27 |
28 | const ModelProvider = ({ children, optimizer }: ModelProviderProps) => {
29 | const value = useLoadModel(optimizer);
30 |
31 | return (
32 | {children}
33 | );
34 | };
35 |
36 | const useModelContext = () => {
37 | return useContext(ModelContext);
38 | };
39 |
40 | export { ModelContext, ModelProvider, useModelContext };
41 |
--------------------------------------------------------------------------------
/shared/src/components/avatar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
3 |
4 | import { cn } from "../lib/utils"
5 |
6 | const Avatar = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 | ))
19 | Avatar.displayName = AvatarPrimitive.Root.displayName
20 |
21 | const AvatarImage = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, ...props }, ref) => (
25 |
30 | ))
31 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
32 |
33 | const AvatarFallback = React.forwardRef<
34 | React.ElementRef,
35 | React.ComponentPropsWithoutRef
36 | >(({ className, ...props }, ref) => (
37 |
45 | ))
46 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
47 |
48 | export { Avatar, AvatarImage, AvatarFallback }
49 |
--------------------------------------------------------------------------------
/apps/official-website/public/draco/README.md:
--------------------------------------------------------------------------------
1 | # Draco 3D Data Compression
2 |
3 | Draco is an open-source library for compressing and decompressing 3D geometric meshes and point clouds. It is intended to improve the storage and transmission of 3D graphics.
4 |
5 | [Website](https://google.github.io/draco/) | [GitHub](https://github.com/google/draco)
6 |
7 | ## Contents
8 |
9 | This folder contains three utilities:
10 |
11 | * `draco_decoder.js` — Emscripten-compiled decoder, compatible with any modern browser.
12 | * `draco_decoder.wasm` — WebAssembly decoder, compatible with newer browsers and devices.
13 | * `draco_wasm_wrapper.js` — JavaScript wrapper for the WASM decoder.
14 |
15 | Each file is provided in two variations:
16 |
17 | * **Default:** Latest stable builds, tracking the project's [master branch](https://github.com/google/draco).
18 | * **glTF:** Builds targeted by the [glTF mesh compression extension](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_draco_mesh_compression), tracking the [corresponding Draco branch](https://github.com/google/draco/tree/gltf_2.0_draco_extension).
19 |
20 | Either variation may be used with `THREE.DRACOLoader`:
21 |
22 | ```js
23 | var dracoLoader = new THREE.DRACOLoader();
24 | dracoLoader.setDecoderPath('path/to/decoders/');
25 | dracoLoader.setDecoderConfig({type: 'js'}); // (Optional) Override detection of WASM support.
26 | ```
27 |
28 | Further [documentation on GitHub](https://github.com/google/draco/tree/master/javascript/example#static-loading-javascript-decoder).
29 |
30 | ## License
31 |
32 | [Apache License 2.0](https://github.com/google/draco/blob/master/LICENSE)
33 |
--------------------------------------------------------------------------------
/packages/viewer/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.9.5",
3 | "name": "@vctrl/viewer",
4 | "description": "vctrl/viewer is a React component library for rendering and interacting with 3D models. It's part of the vectreal ecosystem and is designed to work seamlessly with the vctrl/hooks package for model loading and management.",
5 | "bugs": {
6 | "url": "https://github.com/vectreal/vectreal-core/issues"
7 | },
8 | "homepage": "https://core.vectreal.com",
9 | "keywords": [
10 | "vectreal",
11 | "react",
12 | "3d",
13 | "model",
14 | "viewer",
15 | "react-component",
16 | "react-three-fiber",
17 | "react-three-drei",
18 | "threejs",
19 | "react-three-gltf",
20 | "gltf",
21 | "glb",
22 | "usdz",
23 | "model-viewer"
24 | ],
25 | "repository": {
26 | "url": "git+https://github.com/vectreal/vectreal-core.git",
27 | "type": "https"
28 | },
29 | "license": "AGPL-3.0-only",
30 | "main": "./index.js",
31 | "module": "./index.mjs",
32 | "types": "./index.d.ts",
33 | "type": "module",
34 | "publishConfig": {
35 | "access": "public"
36 | },
37 | "exports": {
38 | ".": {
39 | "import": "./index.js",
40 | "require": "./index.cjs",
41 | "types": "./index.d.ts"
42 | },
43 | "./css": "./style.css"
44 | },
45 | "scripts": {
46 | "build-storybook": "storybook build",
47 | "chromatic": "npx chromatic --project-token=chpt_b3f4d26ac3d3190"
48 | },
49 | "peerDependencies": {
50 | "react": "^18.0.0 || ^19.0.0"
51 | },
52 | "dependencies": {
53 | "@react-three/drei": "^10.0.4",
54 | "@react-three/fiber": "^9.1.0",
55 | "three": "^0.168.0"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/packages/hooks/src/use-load-model/event-system.ts:
--------------------------------------------------------------------------------
1 | /* vectreal-core | vctrl/hooks
2 | Copyright (C) 2024 Moritz Becker
3 |
4 | This program is free software: you can redistribute it and/or modify
5 | it under the terms of the GNU Affero General Public License as published by
6 | the Free Software Foundation, either version 3 of the License, or
7 | (at your option) any later version.
8 |
9 | This program is distributed in the hope that it will be useful,
10 | but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | GNU Affero General Public License for more details.
13 |
14 | You should have received a copy of the GNU Affero General Public License
15 | along with this program. If not, see . */
16 |
17 | import { EventData, EventHandler, EventTypes } from './types';
18 |
19 | const listeners = {} as Record[]>;
20 | const eventSystem = {
21 | emit(event: T, data?: EventData[T]): void {
22 | const handlers = listeners[event];
23 | if (handlers) {
24 | handlers.forEach((handler) => handler(data));
25 | }
26 | },
27 |
28 | on(event: T, handler?: EventHandler): void {
29 | if (!listeners[event]) {
30 | listeners[event] = [];
31 | }
32 | listeners[event].push(handler as EventHandler);
33 | },
34 |
35 | off(event: T, handler?: EventHandler): void {
36 | const handlers = listeners[event];
37 | if (handlers) {
38 | listeners[event] = handlers.filter(
39 | (h) => h !== handler,
40 | ) as EventHandler[];
41 | }
42 | },
43 | };
44 |
45 | export default eventSystem;
46 |
--------------------------------------------------------------------------------
/packages/hooks/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.9.5",
3 | "name": "@vctrl/hooks",
4 | "description": "vctrl/hooks is a React hooks package designed to simplify 3D model loading and management within React applications. It's part of the vectreal-core ecosystem and is primarily used in the vctrl/viewer React component and the official website application.",
5 | "bugs": {
6 | "url": "https://github.com/vectreal/vectreal-core/issues"
7 | },
8 | "homepage": "https://core.vectreal.com",
9 | "keywords": [
10 | "vectreal",
11 | "react",
12 | "3d",
13 | "model",
14 | "viewer",
15 | "react-component",
16 | "react-three-fiber",
17 | "react-three-drei",
18 | "threejs",
19 | "react-three-gltf",
20 | "gltf",
21 | "glb",
22 | "usdz",
23 | "three-model-loading",
24 | "model-viewer"
25 | ],
26 | "repository": {
27 | "url": "https://github.com/vectreal/vectreal-core",
28 | "type": "https"
29 | },
30 | "license": "AGPL-3.0-only",
31 | "main": "./index.cjs.js",
32 | "module": "./index.es.js",
33 | "types": "./index.d.ts",
34 | "type": "module",
35 | "publishConfig": {
36 | "access": "public"
37 | },
38 | "exports": {
39 | "./use-load-model": {
40 | "import": "./use-load-model.es.js",
41 | "require": "./use-load-model.cjs.js",
42 | "types": "./use-load-model/index.d.ts"
43 | }
44 | },
45 | "peerDependencies": {
46 | "react": "^18.0.0 || ^19.0.0"
47 | },
48 | "dependencies": {
49 | "@gltf-transform/core": "^4.0.8",
50 | "@gltf-transform/extensions": "^4.0.10",
51 | "@gltf-transform/functions": "^4.0.8",
52 | "file-saver": "^2.0.5",
53 | "jszip": "^3.10.1",
54 | "meshoptimizer": "^0.21.0",
55 | "three": "^0.168.0"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/packages/hooks/src/use-load-model/loaders/create-gltf-loader.ts:
--------------------------------------------------------------------------------
1 | /* vectreal-core | vctrl/hooks
2 | Copyright (C) 2024 Moritz Becker
3 |
4 | This program is free software: you can redistribute it and/or modify
5 | it under the terms of the GNU Affero General Public License as published by
6 | the Free Software Foundation, either version 3 of the License, or
7 | (at your option) any later version.
8 |
9 | This program is distributed in the hope that it will be useful,
10 | but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | GNU Affero General Public License for more details.
13 |
14 | You should have received a copy of the GNU Affero General Public License
15 | along with this program. If not, see . */
16 |
17 | import { WebGLRenderer } from 'three';
18 | import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
19 | import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
20 | import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader';
21 | import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module';
22 |
23 | import eventSystem from '../event-system';
24 |
25 | function createGltfLoader() {
26 | const dracoLoader = new DRACOLoader();
27 | dracoLoader.setDecoderPath('/draco/');
28 |
29 | const ktxLoader = new KTX2Loader();
30 | ktxLoader.setTranscoderPath('/draco/');
31 |
32 | const gltfLoader = new GLTFLoader()
33 | .setDRACOLoader(dracoLoader)
34 | .setKTX2Loader(ktxLoader.detectSupport(new WebGLRenderer()))
35 | .setMeshoptDecoder(MeshoptDecoder);
36 |
37 | gltfLoader.manager.onError = (url) => {
38 | eventSystem.emit('load-error', `Failed to load file ${url}`);
39 | };
40 |
41 | return gltfLoader;
42 | }
43 |
44 | export default createGltfLoader;
45 |
--------------------------------------------------------------------------------
/apps/official-website/src/components/title-model-scene.tsx:
--------------------------------------------------------------------------------
1 | import { Vector3 } from 'three';
2 | import { useGLTF } from '@react-three/drei';
3 | import { VectrealViewer } from '@vctrl/viewer';
4 |
5 | interface ModelSceneProps {
6 | url: string;
7 | }
8 |
9 | const InfoContent = () => {
10 | return (
11 |
12 | This work is based on{' '}
13 |
19 | "Unused Blue Vans Shoe"
20 | {' '}
21 | by{' '}
22 |
28 | Lassi Kaukonen
29 | {' '}
30 | licensed under{' '}
31 |
37 | CC-BY-4.0
38 |
39 |
40 | );
41 | };
42 |
43 | const HomeModelScene = ({ url }: ModelSceneProps) => {
44 | const { scene } = useGLTF(url);
45 |
46 | return (
47 | }
50 | cameraOptions={{ initialCameraPosition: new Vector3(-5, 2, 0) }}
51 | controlsOptions={{ enableZoom: false, dampingFactor: 0.1 }}
52 | gridOptions={{ showGrid: true }}
53 | envOptions={{
54 | backgroundColor: '#141414',
55 | env: { preset: 'city', background: false, backgroundBlurriness: 1 },
56 | stage: { adjustCamera: 1 },
57 | }}
58 | />
59 | );
60 | };
61 |
62 | export default HomeModelScene;
63 |
--------------------------------------------------------------------------------
/apps/official-website/src/pages/editor/color-picker.tsx:
--------------------------------------------------------------------------------
1 | import { HexColorPicker } from 'react-colorful';
2 | import {
3 | Card,
4 | CardContent,
5 | CardDescription,
6 | CardHeader,
7 | CardTitle,
8 | } from '@vctrl/shared/components';
9 | import { cn } from '@vctrl/shared/lib/utils';
10 |
11 | import { useEditorContext } from '../../components/providers';
12 | import CloseButton from '../../components/modal-close-button';
13 |
14 | interface ColorPickerProps {
15 | show: boolean;
16 | setShow: (showColorPicker: boolean) => void;
17 | }
18 |
19 | const ColorPicker = ({ show, setShow }: ColorPickerProps) => {
20 | const { color, setColor } = useEditorContext();
21 |
22 | function handleColorPickerChange(event: React.ChangeEvent) {
23 | const value = event.target.value;
24 | if (value.length > 7) return;
25 |
26 | setColor(value.includes('#') ? value : `#${value}`);
27 | }
28 |
29 | return (
30 |
37 | setShow(false)} />
38 |
39 | Set Background-color
40 | Pick a color
41 |
42 |
43 |
44 |
45 | Current color:
46 |
51 |
52 |
53 |
54 | );
55 | };
56 |
57 | export default ColorPicker;
58 |
--------------------------------------------------------------------------------
/packages/viewer/src/components/scene/scene-camera.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { PerspectiveCamera, Vector3 } from 'three';
3 | import { useThree } from '@react-three/fiber';
4 |
5 | type CameraPosition =
6 | | Vector3
7 | | { x: number; y: number; z: number }
8 | | [number, number, number];
9 |
10 | export interface CameraProps {
11 | /**
12 | * Initial camera position.
13 | */
14 | initialCameraPosition?: CameraPosition;
15 | /**
16 | * Field of view.
17 | */
18 | fov?: number;
19 | /**
20 | * Aspect ratio.
21 | */
22 | aspect?: number;
23 | /**
24 | * Near clipping plane.
25 | */
26 | near?: number;
27 | /**
28 | * Near clipping plane.
29 | */
30 | far?: number;
31 | }
32 |
33 | export const defaultCameraOptions: Required = {
34 | aspect: 1,
35 | initialCameraPosition: new Vector3(0, 0, 5),
36 | fov: 69,
37 | near: 0.01,
38 | far: 1000,
39 | };
40 |
41 | /**
42 | * Configures the Three.js camera using provided props.
43 | *
44 | * @param {CameraProps} props - Camera configuration.
45 | * @returns {null} No rendered output.
46 | */
47 | const SceneCamera = (props: CameraProps) => {
48 | const cameraOptions = { ...defaultCameraOptions, ...props };
49 | const { initialCameraPosition, fov, near, far } = cameraOptions;
50 | const { camera } = useThree();
51 |
52 | useEffect(() => {
53 | camera.far = far;
54 | camera.near = near;
55 | (camera as PerspectiveCamera).fov = fov;
56 |
57 | const pos = initialCameraPosition;
58 |
59 | if (pos instanceof Vector3) {
60 | camera.position.copy(pos);
61 | } else if (Array.isArray(pos)) {
62 | camera.position.fromArray(pos);
63 | } else {
64 | camera.position.set(pos.x, pos.y, pos.z);
65 | }
66 |
67 | camera.updateProjectionMatrix();
68 | }, [camera, initialCameraPosition, fov, near, far]);
69 |
70 | return null;
71 | };
72 |
73 | export default SceneCamera;
74 |
--------------------------------------------------------------------------------
/apps/official-website/src/globals.css:
--------------------------------------------------------------------------------
1 | @config "../tailwind.config.js";
2 |
3 | @tailwind base;
4 | @tailwind components;
5 | @tailwind utilities;
6 |
7 | @layer base {
8 | :root {
9 | --background: 0 0% 100%;
10 | --foreground: 20 14.3% 4.1%;
11 |
12 | --card: 0 0% 100%;
13 | --card-foreground: 20 14.3% 4.1%;
14 |
15 | --popover: 0 0% 100%;
16 | --popover-foreground: 20 14.3% 4.1%;
17 |
18 | --primary: 24 9.8% 10%;
19 | --primary-foreground: 60 9.1% 97.8%;
20 |
21 | --secondary: 60 4.8% 95.9%;
22 | --secondary-foreground: 24 9.8% 10%;
23 |
24 | --muted: 60 4.8% 95.9%;
25 | --muted-foreground: 25 5.3% 44.7%;
26 |
27 | --accent: 60 4.8% 95.9%;
28 | --accent-foreground: 24 9.8% 10%;
29 |
30 | --destructive: 0 84.2% 60.2%;
31 | --destructive-foreground: 60 9.1% 97.8%;
32 |
33 | --border: 20 5.9% 90%;
34 | --input: 20 5.9% 90%;
35 | --ring: 20 14.3% 4.1%;
36 |
37 | --radius: 0.5rem;
38 | }
39 |
40 | .dark {
41 | --background: 20 14.3% 4.1%;
42 | --foreground: 60 9.1% 97.8%;
43 |
44 | --card: 20 14.3% 4.1%;
45 | --card-foreground: 60 9.1% 97.8%;
46 |
47 | --popover: 20 14.3% 4.1%;
48 | --popover-foreground: 60 9.1% 97.8%;
49 |
50 | --primary: 60 9.1% 97.8%;
51 | --primary-foreground: 24 9.8% 10%;
52 |
53 | --secondary: 12 6.5% 15.1%;
54 | --secondary-foreground: 60 9.1% 97.8%;
55 |
56 | --muted: 12 6.5% 15.1%;
57 | --muted-foreground: 24 5.4% 63.9%;
58 |
59 | --accent: 12 6.5% 15.1%;
60 | --accent-foreground: 60 9.1% 97.8%;
61 |
62 | --destructive: 0 62.8% 30.6%;
63 | --destructive-foreground: 60 9.1% 97.8%;
64 |
65 | --border: 12 6.5% 15.1%;
66 | --input: 12 6.5% 15.1%;
67 | --ring: 24 5.7% 82.9%;
68 |
69 | --orange: #fc6c18;
70 | --dark-orange: #f54444;
71 | }
72 |
73 | * {
74 | @apply border-border;
75 | @apply font-poppins;
76 | }
77 |
78 | body {
79 | @apply bg-background text-foreground;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/apps/official-website/src/components/assets/vectreal-logo.tsx:
--------------------------------------------------------------------------------
1 | const VectrealLogo = ({ small }: { small?: boolean }) => {
2 | return (
3 |
7 |
8 |
12 |
16 |
20 |
24 |
28 |
32 |
36 |
37 |
38 | );
39 | };
40 |
41 | export default VectrealLogo;
42 |
--------------------------------------------------------------------------------
/apps/official-website/src/components/providers/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useLayoutEffect, useState } from 'react';
2 |
3 | type Theme = 'dark' | 'light' | 'system';
4 |
5 | type ThemeProviderProps = {
6 | children: React.ReactNode;
7 | defaultTheme?: Theme;
8 | storageKey?: string;
9 | };
10 |
11 | type ThemeProviderState = {
12 | theme: Theme;
13 | setTheme: (theme: Theme) => void;
14 | };
15 |
16 | const initialState: ThemeProviderState = {
17 | theme: 'system',
18 | setTheme: () => null,
19 | };
20 |
21 | const ThemeProviderContext = createContext(initialState);
22 |
23 | function ThemeProvider({
24 | children,
25 | defaultTheme = 'system',
26 | storageKey = 'vite-ui-theme',
27 | ...props
28 | }: ThemeProviderProps) {
29 | const [theme, setTheme] = useState(
30 | () => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
31 | );
32 |
33 | useLayoutEffect(() => {
34 | const root = window.document.documentElement;
35 |
36 | root.classList.remove('light', 'dark');
37 |
38 | if (theme === 'system') {
39 | const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
40 | .matches
41 | ? 'dark'
42 | : 'light';
43 |
44 | root.classList.add(systemTheme);
45 | return;
46 | }
47 |
48 | root.classList.add(theme);
49 | }, [theme]);
50 |
51 | const value = {
52 | theme,
53 | setTheme: (theme: Theme) => {
54 | localStorage.setItem(storageKey, theme);
55 | setTheme(theme);
56 | },
57 | };
58 |
59 | return (
60 |
61 | {children}
62 |
63 | );
64 | }
65 |
66 | export default ThemeProvider;
67 |
68 | export const useTheme = () => {
69 | const context = useContext(ThemeProviderContext);
70 |
71 | if (context === undefined)
72 | throw new Error('useTheme must be used within a ThemeProvider');
73 |
74 | return context;
75 | };
76 |
--------------------------------------------------------------------------------
/packages/viewer/vite.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { defineConfig } from 'vite';
3 | import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
4 | import react from '@vitejs/plugin-react';
5 | import dts from 'vite-plugin-dts';
6 | import path from 'path';
7 |
8 | // Import PostCSS plugins.
9 | import autoprefixer from 'autoprefixer';
10 | import postcssMixins from 'postcss-mixins';
11 | import postcssNested from 'postcss-nested';
12 |
13 | export default defineConfig({
14 | root: __dirname,
15 | cacheDir: '../../node_modules/.vite/packages/@vctrl/viewer',
16 |
17 | plugins: [
18 | react(),
19 | nxViteTsPaths(),
20 | dts({
21 | entryRoot: 'src',
22 | tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'),
23 | }),
24 | ],
25 |
26 | resolve: {
27 | alias: {
28 | '@vctrl/shared': path.resolve(__dirname, '../../shared/src'),
29 | '@vctrl/hooks': path.resolve(__dirname, '../../packages/hooks/src'),
30 | },
31 | },
32 |
33 | css: {
34 | postcss: {
35 | plugins: [autoprefixer, postcssNested, postcssMixins],
36 | },
37 | },
38 | build: {
39 | emptyOutDir: true,
40 | reportCompressedSize: true,
41 | commonjsOptions: {
42 | transformMixedEsModules: true,
43 | },
44 |
45 | lib: {
46 | entry: 'src/index.ts',
47 | name: '@vctrl/viewer',
48 | fileName: 'index',
49 | // Don't forget to update your package.json as well.
50 | formats: ['es', 'cjs'],
51 | },
52 | rollupOptions: {
53 | // External packages that should not be bundled into the library.
54 | external: [
55 | 'react',
56 | 'react-dom',
57 | 'react/jsx-runtime',
58 | 'three',
59 | '@react-three/fiber',
60 | '@react-three/drei',
61 | '@vctrl/hooks',
62 | 'react-loader-spinner',
63 | '@gltf-transform/core',
64 | '@gltf-transform/extensions',
65 | '@gltf-transform/functions',
66 | 'meshoptimizer',
67 | ],
68 | },
69 | },
70 | });
71 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-official-website.yaml:
--------------------------------------------------------------------------------
1 | name: Deploy apps/official-website to Google Cloud Run
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - 'apps/official-website/**'
9 | - 'packages/hooks/**'
10 | - 'packages/viewer/**'
11 | - 'package*.json'
12 | - '.github/workflows/deploy-official-website.yaml'
13 |
14 | concurrency:
15 | group: ${{ github.workflow }}-${{ github.ref }}
16 | cancel-in-progress: true
17 |
18 | env:
19 | GCR_HOSTNAME: gcr.io
20 | GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}
21 | IMAGE_NAME: official-website
22 |
23 | jobs:
24 | build-and-deploy:
25 | runs-on: ubuntu-22.04
26 |
27 | steps:
28 | - name: Checkout repository
29 | uses: actions/checkout@v4
30 |
31 | - name: Set up QEMU
32 | uses: docker/setup-qemu-action@v3
33 |
34 | - name: Set up Docker Buildx
35 | uses: docker/setup-buildx-action@v3
36 |
37 | - name: Authenticate to Google Cloud
38 | uses: google-github-actions/auth@v2
39 | with:
40 | credentials_json: '${{ secrets.GCP_CREDENTIALS }}'
41 |
42 | - name: Log in to Google Container Registry
43 | uses: docker/login-action@v3
44 | with:
45 | registry: gcr.io
46 | username: _json_key
47 | password: '${{ secrets.GCP_CREDENTIALS }}'
48 |
49 | - name: Build and push Docker image
50 | uses: docker/build-push-action@v6
51 | with:
52 | context: .
53 | file: ./apps/official-website/Dockerfile
54 | push: true
55 | tags: ${{ env.GCR_HOSTNAME }}/${{ env.GCP_PROJECT_ID }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
56 | cache-from: type=gha
57 | cache-to: type=gha,mode=max
58 |
59 | - name: Deploy to Cloud Run
60 | uses: google-github-actions/deploy-cloudrun@v2
61 | with:
62 | service: ${{ env.IMAGE_NAME }}
63 | image: ${{ env.GCR_HOSTNAME }}/${{ env.GCP_PROJECT_ID }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
64 | region: us-central1
65 |
--------------------------------------------------------------------------------
/shared/src/components/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "../lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/shared/src/components/accordion.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
3 | import { ChevronDownIcon } from "@radix-ui/react-icons"
4 |
5 | import { cn } from "../lib/utils"
6 |
7 | const Accordion = AccordionPrimitive.Root
8 |
9 | const AccordionItem = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
18 | ))
19 | AccordionItem.displayName = "AccordionItem"
20 |
21 | const AccordionTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, children, ...props }, ref) => (
25 |
26 | svg]:rotate-180",
30 | className
31 | )}
32 | {...props}
33 | >
34 | {children}
35 |
36 |
37 |
38 | ))
39 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
40 |
41 | const AccordionContent = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef
44 | >(({ className, children, ...props }, ref) => (
45 |
50 | {children}
51 |
52 | ))
53 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
54 |
55 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
56 |
--------------------------------------------------------------------------------
/packages/hooks/vite.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { defineConfig } from 'vite';
3 | import react from '@vitejs/plugin-react';
4 | import dts from 'vite-plugin-dts';
5 | import * as path from 'path';
6 | import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
7 |
8 | export default defineConfig({
9 | root: __dirname,
10 | cacheDir: '../../node_modules/.vite/packages/@vctrl/hooks',
11 |
12 | plugins: [
13 | react(),
14 | nxViteTsPaths(),
15 | dts({
16 | entryRoot: 'src',
17 | tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'),
18 | }),
19 | ],
20 |
21 | // Uncomment this if you are using workers.
22 | // worker: {
23 | // plugins: [ nxViteTsPaths() ],
24 | // },
25 |
26 | // Configuration for building your library.
27 | // See: https://vitejs.dev/guide/build.html#library-mode
28 | build: {
29 | emptyOutDir: true,
30 | reportCompressedSize: true,
31 | commonjsOptions: {
32 | transformMixedEsModules: true,
33 | },
34 | lib: {
35 | entry: {
36 | index: path.resolve(__dirname, 'src/index.ts'),
37 | 'use-load-model': path.resolve(
38 | __dirname,
39 | 'src/use-load-model/index.ts',
40 | ),
41 | 'use-optimize-model': path.resolve(
42 | __dirname,
43 | 'src/use-optimize-model/index.ts',
44 | ),
45 | 'use-export-model': path.resolve(
46 | __dirname,
47 | 'src/use-export-model/index.ts',
48 | ),
49 | },
50 | name: '@vctrl/hooks',
51 | formats: ['es', 'cjs'],
52 | fileName: (format, entry) => `${entry}.${format}.js`,
53 | },
54 |
55 | rollupOptions: {
56 | // External packages that should not be bundled into your library.
57 | external: [
58 | 'react',
59 | 'react-dom',
60 | 'react/jsx-runtime',
61 | 'three',
62 | 'file-saver',
63 | 'jszip',
64 | '@gltf-transform/core',
65 | '@gltf-transform/functions',
66 | '@gltf-transform/extensions',
67 | 'meshoptimizer',
68 | ],
69 | output: {
70 | globals: {
71 | react: 'React',
72 | 'react-dom': 'ReactDOM',
73 | },
74 | },
75 | },
76 | },
77 | });
78 |
--------------------------------------------------------------------------------
/shared/src/components/card.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import * as React from "react"
3 |
4 | import { cn } from "../lib/utils"
5 |
6 | const Card = React.forwardRef<
7 | HTMLDivElement,
8 | React.HTMLAttributes
9 | >(({ className, ...props }, ref) => (
10 |
18 | ))
19 | Card.displayName = "Card"
20 |
21 | const CardHeader = React.forwardRef<
22 | HTMLDivElement,
23 | React.HTMLAttributes
24 | >(({ className, ...props }, ref) => (
25 |
30 | ))
31 | CardHeader.displayName = "CardHeader"
32 |
33 | const CardTitle = React.forwardRef<
34 | HTMLParagraphElement,
35 | React.HTMLAttributes
36 | >(({ className, ...props }, ref) => (
37 | // eslint-disable-next-line jsx-a11y/heading-has-content
38 |
43 | ))
44 | CardTitle.displayName = "CardTitle"
45 |
46 | const CardDescription = React.forwardRef<
47 | HTMLParagraphElement,
48 | React.HTMLAttributes
49 | >(({ className, ...props }, ref) => (
50 |
55 | ))
56 | CardDescription.displayName = "CardDescription"
57 |
58 | const CardContent = React.forwardRef<
59 | HTMLDivElement,
60 | React.HTMLAttributes
61 | >(({ className, ...props }, ref) => (
62 |
63 | ))
64 | CardContent.displayName = "CardContent"
65 |
66 | const CardFooter = React.forwardRef<
67 | HTMLDivElement,
68 | React.HTMLAttributes
69 | >(({ className, ...props }, ref) => (
70 |
75 | ))
76 | CardFooter.displayName = "CardFooter"
77 |
78 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
79 |
--------------------------------------------------------------------------------
/apps/official-website/src/pages/editor/drop-zone.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useDropzone } from 'react-dropzone';
3 | import { PlusIcon } from '@radix-ui/react-icons';
4 |
5 | import { useModelContext } from '@vctrl/hooks/use-load-model';
6 | import { Card, CardContent } from '@vctrl/shared/components';
7 |
8 | import TypographyLead from '../../components/typography/typography-lead';
9 | import { useAcceptPattern } from '../../lib/hooks';
10 |
11 | declare module 'react' {
12 | interface InputHTMLAttributes extends HTMLAttributes {
13 | // extends React's HTMLAttributes
14 | directory?: string;
15 | webkitdirectory?: string;
16 | }
17 | }
18 |
19 | const DropZone = () => {
20 | const acceptPattern = useAcceptPattern();
21 | const { getRootProps, getInputProps, isDragActive, acceptedFiles } =
22 | useDropzone();
23 |
24 | const { load } = useModelContext();
25 |
26 | useEffect(() => {
27 | if (acceptedFiles.length > 0) {
28 | load(acceptedFiles);
29 | }
30 | }, [acceptedFiles, load]);
31 |
32 | const easeTransition = 'transition ease-in-out duration-300';
33 | const shadow = isDragActive
34 | ? 'shadow-xl border-zinc-500'
35 | : 'shadow-s border-zinc-800';
36 |
37 | return (
38 |
39 |
42 |
43 |
48 |
49 |
50 | Click here, or drag and drop your 3D model files
51 |
52 |
59 |
60 |
61 |
62 |
63 | );
64 | };
65 |
66 | export default DropZone;
67 |
--------------------------------------------------------------------------------
/packages/hooks/src/use-load-model/state.ts:
--------------------------------------------------------------------------------
1 | /* vectreal-core | vctrl/hooks
2 | Copyright (C) 2024 Moritz Becker
3 |
4 | This program is free software: you can redistribute it and/or modify
5 | it under the terms of the GNU Affero General Public License as published by
6 | the Free Software Foundation, either version 3 of the License, or
7 | (at your option) any later version.
8 |
9 | This program is distributed in the hope that it will be useful,
10 | but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | GNU Affero General Public License for more details.
13 |
14 | You should have received a copy of the GNU Affero General Public License
15 | along with this program. If not, see . */
16 |
17 | import { Action, ModelFileTypes, State } from './types';
18 |
19 | /**
20 | * Initial state for the useReadModelFiles hook
21 | *
22 | * @property file - Currently loaded file
23 | * @property isFileLoading - Flag indicating if a file is currently being loaded
24 | * @property progress - Current loading progress (0-100)
25 | * @property supportedFileTypes - List of supported file types
26 | */
27 | export const initialState: State = {
28 | /**
29 | * Currently loaded file
30 | */
31 | file: null,
32 | /**
33 | * Flag indicating if a file is currently being loaded
34 | */
35 | isFileLoading: false,
36 | /**
37 | * Current loading progress (0-100)
38 | */
39 | progress: 0,
40 | /**
41 | * List of supported file types
42 | * @type {ModelFileTypes[]}
43 | */
44 | supportedFileTypes: Object.values(ModelFileTypes),
45 | };
46 |
47 | /**
48 | * Reducer function for the useReadModelFiles hook
49 | *
50 | * @param state - The current state of the reducer
51 | * @param action - The action to be performed
52 | * @returns The updated state
53 | */
54 | function reducer(state: State, action: Action): State {
55 | switch (action.type) {
56 | case 'set-file':
57 | return { ...state, file: action.payload };
58 | case 'set-file-loading':
59 | return { ...state, isFileLoading: action.payload };
60 | case 'set-progress':
61 | return { ...state, progress: action.payload };
62 | case 'reset-state':
63 | return { ...initialState };
64 | default:
65 | return state;
66 | }
67 | }
68 |
69 | export default reducer;
70 |
--------------------------------------------------------------------------------
/packages/hooks/src/use-load-model/types.ts:
--------------------------------------------------------------------------------
1 | /* vectreal-core | vctrl/hooks
2 | Copyright (C) 2024 Moritz Becker
3 |
4 | This program is free software: you can redistribute it and/or modify
5 | it under the terms of the GNU Affero General Public License as published by
6 | the Free Software Foundation, either version 3 of the License, or
7 | (at your option) any later version.
8 |
9 | This program is distributed in the hope that it will be useful,
10 | but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | GNU Affero General Public License for more details.
13 |
14 | You should have received a copy of the GNU Affero General Public License
15 | along with this program. If not, see . */
16 |
17 | import { Object3D } from 'three';
18 |
19 | export enum ModelFileTypes {
20 | gltf = 'gltf',
21 | glb = 'glb',
22 | usdz = 'usdz',
23 | }
24 |
25 | export type InputFileOrDirectory = (File | FileSystemDirectoryHandle)[];
26 |
27 | export interface ReducedGltf {
28 | images: { name: string; uri: string }[];
29 | buffers: { bytelength: string; uri: string }[];
30 | }
31 |
32 | export interface ModelFile {
33 | model: Object3D;
34 | type: ModelFileTypes;
35 | name: string;
36 | }
37 |
38 | // Updated State interface
39 | export interface State {
40 | file: ModelFile | null;
41 | isFileLoading: boolean;
42 | progress: number;
43 | supportedFileTypes: ModelFileTypes[];
44 | }
45 |
46 | // Updated Action types
47 | export type Action =
48 | | { type: 'set-file'; payload: ModelFile }
49 | | { type: 'set-file-loading'; payload: boolean }
50 | | { type: 'set-progress'; payload: number }
51 | | { type: 'reset-state' };
52 |
53 | // Updated Event types
54 | export type EventTypes =
55 | | 'multiple-models'
56 | | 'not-loaded-files'
57 | | 'load-start'
58 | | 'load-progress'
59 | | 'load-complete'
60 | | 'load-reset'
61 | | 'load-error';
62 |
63 | // Updated event data types
64 | export type EventData = {
65 | 'multiple-models': File[];
66 | 'not-loaded-files': File[];
67 | 'load-start': null;
68 | 'load-progress': number;
69 | 'load-complete': State['file'];
70 | 'load-reset': null;
71 | 'load-error': Error | unknown;
72 | };
73 |
74 | // Updated EventHandler type
75 | export type EventHandler = (data?: EventData[T]) => void;
76 |
--------------------------------------------------------------------------------
/packages/viewer/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["plugin:@nx/react", "../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*", "storybook-static"],
4 | "overrides": [
5 | {
6 | "files": ["*.json"],
7 | "parser": "jsonc-eslint-parser",
8 | "rules": {
9 | "@nx/dependency-checks": [
10 | "error",
11 | {
12 | "includeTransitiveDependencies": true,
13 | "ignoredDependencies": [
14 | "storybook-addon-deep-controls",
15 | "@nx/vite",
16 | "@nx/react",
17 | "autoprefixer",
18 | "postcss-modules",
19 | "framer-motion",
20 | "class-variance-authority",
21 | "@radix-ui/react-accordion",
22 | "@radix-ui/react-icons",
23 | "@radix-ui/react-avatar",
24 | "@radix-ui/react-slot",
25 | "@radix-ui/react-dialog",
26 | "@radix-ui/react-dropdown-menu",
27 | "@radix-ui/react-label",
28 | "@radix-ui/react-menubar",
29 | "@radix-ui/react-navigation-menu",
30 | "@radix-ui/react-popover",
31 | "@radix-ui/react-progress",
32 | "@radix-ui/react-tooltip",
33 | "sonner",
34 | "@vctrl/hooks",
35 | "@gltf-transform/core",
36 | "@gltf-transform/functions",
37 | "@gltf-transform/extensions",
38 | "meshoptimizer",
39 | "file-saver",
40 | "jszip",
41 | "vite",
42 | "vite-plugin-dts",
43 | "@vitejs/plugin-react",
44 | "clsx",
45 | "tailwind-merge",
46 | "postcss-mixins",
47 | "postcss-nested",
48 | "@storybook/react-vite",
49 | "@storybook/react"
50 | ]
51 | }
52 | ]
53 | }
54 | },
55 | {
56 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
57 | "rules": {
58 | "@nx/enforce-module-boundaries": [
59 | 1,
60 | {
61 | "enforceBuildableLibDependency": false
62 | }
63 | ]
64 | }
65 | },
66 | {
67 | "files": ["*.ts", "*.tsx"],
68 | "rules": {}
69 | },
70 | {
71 | "files": ["*.js", "*.jsx"],
72 | "rules": {}
73 | }
74 | ]
75 | }
76 |
--------------------------------------------------------------------------------
/nx.json:
--------------------------------------------------------------------------------
1 | {
2 | "namedInputs": {
3 | "default": ["{projectRoot}/**/*"],
4 | "production": [
5 | "!{projectRoot}/**/*.spec.tsx",
6 | "!{projectRoot}/**/*.stories.@(js|jsx|ts|tsx|mdx)",
7 | "!{projectRoot}/.storybook/**/*",
8 | "!{projectRoot}/tsconfig.storybook.json"
9 | ]
10 | },
11 | "targetDefaults": {
12 | "lint": {
13 | "executor": "@nx/linter:eslint",
14 | "cache": true
15 | },
16 | "typecheck": {
17 | "executor": "@nx/js:tsc",
18 | "cache": true
19 | },
20 | "build": {
21 | "dependsOn": ["lint", "typecheck"],
22 | "executor": "@nx/vite:build"
23 | }
24 | },
25 | "defaultBase": "main",
26 | "release": {
27 | "projects": ["packages/*"],
28 | "version": {
29 | "conventionalCommits": true
30 | },
31 | "changelog": {
32 | "workspaceChangelog": {
33 | "createRelease": "github"
34 | }
35 | },
36 | "conventionalCommits": {
37 | "types": {
38 | "docs": {
39 | "semverBump": "patch"
40 | }
41 | }
42 | }
43 | },
44 | "plugins": [
45 | {
46 | "plugin": "@nx/rollup/plugin",
47 | "options": {
48 | "buildTargetName": "build"
49 | }
50 | },
51 | {
52 | "plugin": "@nx/eslint/plugin",
53 | "options": {
54 | "targetName": "lint"
55 | }
56 | },
57 | {
58 | "plugin": "@nx/vite/plugin",
59 | "options": {
60 | "buildTargetName": "build",
61 | "testTargetName": "test",
62 | "serveTargetName": "serve",
63 | "previewTargetName": "preview",
64 | "serveStaticTargetName": "serve-static"
65 | }
66 | },
67 | {
68 | "plugin": "@nx/storybook/plugin",
69 | "options": {
70 | "serveStorybookTargetName": "storybook",
71 | "buildStorybookTargetName": "build-storybook",
72 | "testStorybookTargetName": "test-storybook",
73 | "staticStorybookTargetName": "static-storybook"
74 | }
75 | }
76 | ],
77 | "generators": {
78 | "@nx/react": {
79 | "library": {
80 | "style": "css",
81 | "linter": "eslint",
82 | "unitTestRunner": "none"
83 | },
84 | "application": {
85 | "babel": true,
86 | "style": "css",
87 | "linter": "eslint",
88 | "bundler": "vite"
89 | },
90 | "component": {
91 | "style": "css"
92 | }
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/apps/official-website/src/pages/contact.tsx:
--------------------------------------------------------------------------------
1 | import { FormEvent } from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | import { Button, Input, Label, Textarea } from '@vctrl/shared/components';
5 |
6 | import TitleSection from '../components/title-section';
7 |
8 | const Contact = () => {
9 | const handleSubmit = (event: FormEvent) => {
10 | event.preventDefault();
11 | console.log('Form submitted');
12 |
13 | const formData = new FormData(event.currentTarget as HTMLFormElement);
14 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
15 | const data = Object.fromEntries(formData.entries());
16 | };
17 |
18 | return (
19 |
20 |
21 |
22 | We'd love to hear from you. Please fill out the form below and
23 | we'll get back to you as soon as possible.
24 |
25 |
26 |
57 |
58 |
59 | Or write an email to:{' '}
60 |
64 | info@vectreal.com
65 | {' '}
66 | /{' '}
67 |
71 | ken@vectreal.com
72 | {' '}
73 | ( or reach us via our{' '}
74 |
79 | Discord Server
80 | {' '}
81 | ^^)
82 |
83 |
84 |
85 | );
86 | };
87 |
88 | export default Contact;
89 |
--------------------------------------------------------------------------------
/packages/hooks/src/use-export-model/utils/export-handlers.ts:
--------------------------------------------------------------------------------
1 | import JSZip from 'jszip';
2 | import { ImageDataObject, TexturesObject, URIBufferObject } from '../types';
3 | import dataURItoBlob from './data-uri-to-blob';
4 | import { getFileExtension } from './file-helpers';
5 |
6 | /**
7 | * Handles processing and adding images to a zip file.
8 | *
9 | * @param images The images property of the GLTF model's asset.
10 | * @param textures The textures property of the GLTF model's asset.
11 | * @param zip The zip file to add images to.
12 | */
13 | export const handleImages = (
14 | images: Array,
15 | textures: Array,
16 | zip: JSZip,
17 | ): void => {
18 | textures.forEach((texture, index) => {
19 | const image = images[texture.source];
20 | if (!image.uri) return;
21 |
22 | const extension = getFileExtension(image.uri, image.mimeType);
23 | const textureName = texture.name || `texture_${index}`;
24 | const fileName = `${textureName}.${extension}`;
25 |
26 | if (image.data instanceof ArrayBuffer || image.data instanceof Uint8Array) {
27 | const buffer =
28 | image.data instanceof Uint8Array ? image.data.buffer : image.data;
29 | const uint8Array = new Uint8Array(buffer);
30 | zip.file(fileName, uint8Array, { binary: true });
31 | } else if (image.uri.startsWith('data:')) {
32 | // Handle data URIs
33 | const binaryData = dataURItoBlob(image.uri);
34 | zip.file(fileName, binaryData, { binary: true });
35 | } else {
36 | console.warn(`Image at index ${index} has unsupported data format`);
37 | }
38 | });
39 | };
40 |
41 | /**
42 | * Handles processing and adding buffers to a zip file.
43 | *
44 | * @param buffers The buffers property of the GLTF model's asset.
45 | * @param zip The zip file to add buffers to.
46 | * @param baseFileName The base file name to use for the exported file.
47 | *
48 | * Only ArrayBuffers and URIBufferObjects with a uri of type data URL are supported.
49 | * If the buffer is not an ArrayBuffer, it must have a uri that starts with 'data:'.
50 | * If the buffer is not an ArrayBuffer and does not have a uri, a warning is logged
51 | * to the console.
52 | */
53 | export const handleBuffers = (
54 | buffers: Array,
55 | zip: JSZip,
56 | baseFileName: string,
57 | ): void => {
58 | buffers.forEach((buffer, index) => {
59 | const fileName = `${baseFileName}.bin`;
60 | if (buffer instanceof ArrayBuffer) {
61 | const uint8Array = new Uint8Array(buffer);
62 | zip.file(fileName, uint8Array, { binary: true });
63 | } else if (buffer.uri) {
64 | // If buffer.uri is a data URL, we need to convert it to binary data
65 | const binaryData = dataURItoBlob(buffer.uri);
66 | zip.file(fileName, binaryData, { binary: true });
67 | } else {
68 | console.warn(
69 | `Buffer at index ${index} is not an ArrayBuffer or does not have a URI`,
70 | );
71 | }
72 | });
73 | };
74 |
--------------------------------------------------------------------------------
/apps/official-website/src/components/providers/editor-provider.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, useState, createContext, ComponentProps } from 'react';
2 | import { Stage } from '@react-three/drei';
3 | import { PresetsType } from '@react-three/drei/helpers/environment-assets';
4 | const EditorContext = createContext({} as ReturnType);
5 |
6 | const useEditor = () => {
7 | const [autoRotate, setAutoRotate] = useState({
8 | enabled: true,
9 | speed: 0.5,
10 | });
11 |
12 | const [hdr, setHdr] = useState<{
13 | asBackground: boolean;
14 | backgroundIntensity: number;
15 | exposure: number;
16 | preset: PresetsType;
17 | blurriness: number;
18 | stagePreset: ComponentProps['preset'];
19 | }>({
20 | asBackground: false,
21 | backgroundIntensity: 1,
22 | exposure: 1,
23 | blurriness: 0.5,
24 | preset: 'apartment',
25 | stagePreset: 'soft',
26 | });
27 |
28 | const [showGrid, setShowGrid] = useState(true);
29 |
30 | const [backgroundColor, setBackgroundColor] = useState('#302d2a');
31 |
32 | function setShowAsBackground(value: boolean) {
33 | setHdr((prev) => ({ ...prev, asBackground: value }));
34 | }
35 |
36 | function setBackgroundIntensity(value: number) {
37 | setHdr((prev) => ({ ...prev, backgroundIntensity: value }));
38 | }
39 |
40 | function setHdrExposure(value: number) {
41 | setHdr((prev) => ({ ...prev, exposure: value }));
42 | }
43 |
44 | function setHdrPreset(value: PresetsType) {
45 | setHdr((prev) => ({ ...prev, preset: value }));
46 | }
47 |
48 | function setHdrBluriness(value: number) {
49 | setHdr((prev) => ({ ...prev, blurriness: value }));
50 | }
51 |
52 | function setLightingStagePreset(
53 | value: ComponentProps['preset'],
54 | ) {
55 | setHdr((prev) => ({ ...prev, stagePreset: value }));
56 | }
57 |
58 | function setAutoRotateEnabled(value: boolean) {
59 | setAutoRotate((prev) => ({ ...prev, enabled: value }));
60 | }
61 |
62 | function setAutoRotateSpeed(value: number) {
63 | setAutoRotate((prev) => ({ ...prev, speed: value }));
64 | }
65 |
66 | return {
67 | color: backgroundColor,
68 | setColor: setBackgroundColor,
69 |
70 | hdr,
71 | setShowAsBackground,
72 | setBackgroundIntensity,
73 | setHdrBluriness,
74 | setHdrExposure,
75 | setHdrPreset,
76 | setLightingStagePreset,
77 |
78 | autoRotate,
79 | setAutoRotateEnabled,
80 | setAutoRotateSpeed,
81 |
82 | showGrid,
83 | setShowGrid,
84 | };
85 | };
86 |
87 | const EditorProvider = ({ children }: { children: React.ReactNode }) => {
88 | return (
89 |
90 | {children}
91 |
92 | );
93 | };
94 |
95 | export const useEditorContext = () => {
96 | return useContext(EditorContext);
97 | };
98 |
99 | export default EditorProvider;
100 |
--------------------------------------------------------------------------------
/packages/hooks/src/use-export-model/use-export-model.ts:
--------------------------------------------------------------------------------
1 | import JSZip from 'jszip';
2 | import { saveAs } from 'file-saver';
3 |
4 | import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter';
5 |
6 | import { ModelFile } from '../use-load-model';
7 | import { ExportResult } from './types';
8 | import { getFileBasename, handleBuffers, handleImages } from './utils';
9 |
10 | /**
11 | * Processes the result of a GLTF export and saves it as a ZIP file.
12 | *
13 | * @param {ExportResult} result The result of the GLTF export.
14 | * @param {string} baseFileName The base file name to use for the exported file.
15 | */
16 | const processGltfResult = (
17 | result: ExportResult,
18 | baseFileName: string,
19 | ): void => {
20 | const zip = new JSZip();
21 | zip.file(`${baseFileName}.gltf`, JSON.stringify(result, null, 2));
22 |
23 | if (result.buffers) {
24 | handleBuffers(result.buffers, zip, baseFileName);
25 | }
26 |
27 | if (result.images && result.textures) {
28 | handleImages(result.images, result.textures, zip);
29 | }
30 |
31 | zip.generateAsync({ type: 'blob' }).then((content) => {
32 | saveAs(content, `${baseFileName}.zip`);
33 | });
34 | };
35 |
36 | /**
37 | * A React hook for exporting a 3D model from a Three.js scene.
38 | *
39 | * @param {Function} [onSaved] Called when the export is complete.
40 | * @param {Function} [onError] Called when an error occurs during exporting.
41 | * @returns An object with a single property, `handleGltfExport`. `handleGltfExport` is a function
42 | * that takes a file object and a boolean indicating whether to export in binary format.
43 | * It exports the scene in the format specified by the boolean parameter.
44 | */
45 | const useExportModel = (
46 | onSaved?: () => void,
47 | onError?: (error: ErrorEvent) => void,
48 | ) => {
49 | // Main function to handle exporting the scene in GLTF or GLB format
50 | const handleGltfExport = (file: ModelFile | null, binary: boolean): void => {
51 | const scene = file?.model;
52 | if (!scene) {
53 | console.error('Scene not initialized');
54 | return;
55 | }
56 |
57 | const exporter = new GLTFExporter();
58 | const options = {
59 | binary,
60 | includeCustomExtensions: true,
61 | };
62 |
63 | exporter.parse(
64 | scene,
65 | (result: ArrayBuffer | ExportResult) => {
66 | const baseFileName = getFileBasename(file.name);
67 |
68 | if (result instanceof ArrayBuffer) {
69 | // GLB format
70 | saveAs(new Blob([result]), `${baseFileName}.glb`);
71 | if (onSaved) onSaved();
72 | } else {
73 | // GLTF format
74 | processGltfResult(result, baseFileName);
75 | if (onSaved) onSaved();
76 | }
77 | },
78 | (error) => {
79 | console.error(error);
80 |
81 | if (onError) onError(error);
82 | },
83 | options,
84 | );
85 | };
86 |
87 | return {
88 | handleGltfExport,
89 | };
90 | };
91 |
92 | export default useExportModel;
93 |
--------------------------------------------------------------------------------
/apps/official-website/src/components/base-layout.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { Link, Outlet, useLocation } from 'react-router-dom';
3 | import { Pencil2Icon } from '@radix-ui/react-icons';
4 |
5 | import { TypewriterEffect, Button, Toaster } from '@vctrl/shared/components';
6 |
7 | import { useInitGA } from '../lib/hooks';
8 | import { sendCustomEvent } from '../lib/utils/ga-utils';
9 |
10 | import NavMenu from './nav-menu';
11 | import Footer from './footer';
12 |
13 | const words = ['Make', 'your', '3D', 'models', 'Perfect.'];
14 |
15 | const cta = words.map((word, i) => ({
16 | text: word,
17 | // make last word different color
18 | ...(i === words.length - 1 && {
19 | className: 'text-zinc-500 dark:text-orange',
20 | }),
21 | }));
22 |
23 | /**
24 | * BaseLayout component that sets up the main layout for the application.
25 | * It includes navigation, main content area, and footer.
26 | *
27 | * @component
28 | *
29 | * @returns {JSX.Element} The rendered component.
30 | *
31 | * @remarks
32 | * - Uses `useLocation` to get the current URL path.
33 | * - Uses `useEffect` to scroll to the top of the page on path change.
34 | * - Initializes Google Analytics with `useInitGA`.
35 | * - Displays a navigation menu, main content, and a footer.
36 | * - Conditionally renders additional sections and footer based on the current path.
37 | * */
38 | const BaseLayout = () => {
39 | const currentLocation = useLocation();
40 |
41 | // Scroll to top on path change
42 | useEffect(() => {
43 | window.scrollTo(0, 0);
44 | }, [currentLocation.pathname]);
45 |
46 | // Initialize Google Analytics
47 | useInitGA();
48 |
49 | return (
50 | <>
51 |
52 |
53 |
54 |
55 | {!currentLocation.pathname.includes('editor') && (
56 |
57 |
58 |
59 | Try out the one-click solution to web 3D efficiency
60 |
61 |
62 |
63 |
66 | sendCustomEvent({
67 | category: 'Footer',
68 | action: 'click',
69 | label: 'CTA - Open Editor',
70 | })
71 | }
72 | >
73 |
74 | Open the Free Editor
75 |
76 |
77 |
78 |
79 |
80 | )}
81 |
82 |
83 | {!currentLocation.pathname.includes('editor') && }
84 |
85 |
86 | >
87 | );
88 | };
89 |
90 | export default BaseLayout;
91 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to vectreal-core
2 |
3 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's:
4 |
5 | - Reporting a bug
6 | - Discussing the current state of the code
7 | - Submitting a fix
8 | - Proposing new features
9 | - Becoming a maintainer
10 |
11 | ## We Develop with Github
12 |
13 | We use github to host code, to track issues and feature requests, as well as accept pull requests.
14 |
15 | ## We Use [Github Flow](https://guides.github.com/introduction/flow/index.html), So All Code Changes Happen Through Pull Requests
16 |
17 | Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests:
18 |
19 | 1. Fork the repo and create your branch from `develop`.
20 | 2. If you've added code that should be tested, add tests.
21 | 3. If you've changed APIs, update the documentation.
22 | 4. Ensure the test suite passes.
23 | 5. Make sure your code lints.
24 | 6. Use [Convetional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for your commit messages - here's a [cheet-sheet](https://kapeli.com/cheat_sheets/Conventional_Commits.docset/Contents/Resources/Documents/index).
25 | 7. Issue that pull request and link relevant issues and labels!
26 |
27 | ## Working with NX
28 |
29 | When working with this repo and therefore [NX](https://https://nx.dev/getting-started/intro), there is a certain ways to do things.
30 |
31 | 1. Always stay inside the root of the project when installing packages or running scripts.
32 | 2. To run packages/apps use: `npx nx run :`
33 | 3. To create a new tag and update package versions globally run: `npm run publish-release`
34 | 4. When using VSCode the
35 | [Nx Console extension](https://marketplace.visualstudio.com/items?itemName=nrwl.angular-console) may be helpful.
36 |
37 | ## Any contributions you make will be under the License
38 |
39 | In short, when you submit code changes, your submissions are understood to be under the same [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html) that covers the project. Feel free to contact the maintainers if that's a concern.
40 |
41 | ## Report bugs using Github's [issues](https://github.com/vectreal/vectreal-core/issues)
42 |
43 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/vectreal/vectreal-core/issues/new); it's that easy!
44 |
45 | ## Write bug reports with detail, background, and sample code
46 |
47 | **Great Bug Reports** tend to have:
48 |
49 | - A quick summary and/or background
50 | - Steps to reproduce
51 | - Be specific!
52 | - Give sample code if you can.
53 | - What you expected would happen
54 | - What actually happens
55 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
56 |
57 | People _love_ thorough bug reports. I'm not even kidding.
58 |
59 | ## Use a Consistent Coding Style
60 |
61 | - You can try running `npm run lint` for style unification.
62 |
63 | ## GNU Affero General Public License
64 |
65 | By contributing, you agree that your contributions will be licensed under the [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html).
66 |
--------------------------------------------------------------------------------
/packages/hooks/src/use-load-model/file-type-hooks/use-load-binary.ts:
--------------------------------------------------------------------------------
1 | /* vectreal-core | vctrl/hooks
2 | Copyright (C) 2024 Moritz Becker
3 |
4 | This program is free software: you can redistribute it and/or modify
5 | it under the terms of the GNU Affero General Public License as published by
6 | the Free Software Foundation, either version 3 of the License, or
7 | (at your option) any later version.
8 |
9 | This program is distributed in the hope that it will be useful,
10 | but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | GNU Affero General Public License for more details.
13 |
14 | You should have received a copy of the GNU Affero General Public License
15 | along with this program. If not, see . */
16 |
17 | import { useCallback } from 'react';
18 |
19 | import { Action, ModelFileTypes } from '../types';
20 | import { createGltfLoader, createUsdzLoader } from '../loaders';
21 |
22 | /**
23 | * Hook to load binary files
24 | *
25 | * @param dispatch - The dispatch function from the useReducer hook
26 | * @returns An object containing the loadBinary function
27 | */
28 | function useLoadBinary(dispatch: React.Dispatch) {
29 | /**
30 | * Function to read binary files
31 | *
32 | * @param file - The binary file to read
33 | * @param fileType - The type of file to read
34 | * @param onProgress - The function to call when progress is made
35 | * @returns {void}
36 | */
37 | const loadBinary = useCallback(
38 | (file: File, fileType: ModelFileTypes, onProgress: () => void) => {
39 | const reader = new FileReader();
40 |
41 | reader.onload = async (event: ProgressEvent) => {
42 | if (event.target?.result) {
43 | const arrayBuffer = event.target.result as ArrayBuffer;
44 |
45 | if (fileType === ModelFileTypes.glb) {
46 | const gltfLoader = createGltfLoader();
47 |
48 | gltfLoader.parse(arrayBuffer, '', (gltf) => {
49 | dispatch({
50 | type: 'set-file',
51 | payload: {
52 | model: gltf.scene,
53 | type: ModelFileTypes.glb,
54 | name: file.name,
55 | },
56 | });
57 | });
58 | } else if (fileType === ModelFileTypes.usdz) {
59 | const usdzLoader = createUsdzLoader();
60 | const model = usdzLoader.parse(arrayBuffer);
61 |
62 | dispatch({
63 | type: 'set-file',
64 | payload: {
65 | model: model,
66 | type: ModelFileTypes.usdz,
67 | name: file.name,
68 | },
69 | });
70 | }
71 |
72 | onProgress(); // Call progress callback when binary file is loaded
73 | dispatch({ type: 'set-file-loading', payload: false });
74 | }
75 | };
76 |
77 | reader.onerror = (error) => {
78 | console.error('Error reading file:', error);
79 | dispatch({ type: 'set-file-loading', payload: false });
80 | };
81 |
82 | reader.readAsArrayBuffer(file);
83 | },
84 | [dispatch],
85 | );
86 |
87 | return { loadBinary };
88 | }
89 |
90 | export default useLoadBinary;
91 |
--------------------------------------------------------------------------------
/apps/official-website/src/pages/editor/upload-info-dialog.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ColumnSpacingIcon,
3 | FilePlusIcon,
4 | GlobeIcon,
5 | ImageIcon,
6 | } from '@radix-ui/react-icons';
7 |
8 | import {
9 | Dialog,
10 | DialogContent,
11 | DialogDescription,
12 | DialogHeader,
13 | DialogTitle,
14 | } from '@vctrl/shared/components';
15 |
16 | interface UploadInfoDialogProps {
17 | open: boolean;
18 | onClose: () => void;
19 | }
20 |
21 | const UploadInfoDialog = ({ open, onClose }: UploadInfoDialogProps) => {
22 | return (
23 | !open && onClose()}>
24 |
25 |
26 | 3D Editor Info
27 |
28 | A quick overview our 3D asset support, including upload formats and
29 | optimization features.
30 |
31 |
32 |
33 |
34 |
35 |
36 |
Supported 3D Formats
37 |
38 | glTF, glTF-Draco, glTF-embedded, glTF + textures, USDZ (USDA),
39 | OBJ
40 |
41 |
42 |
43 |
44 |
45 |
46 |
Texture Support
47 |
48 | PNG and JPG textures have been tested and are fully supported
49 |
50 |
51 |
52 |
53 |
54 |
55 |
Export & Optimization
56 |
57 | Export and optimize your 3D assets in all supported formats
58 |
59 |
60 |
61 |
62 |
63 |
64 |
Web-Ready Assets
65 |
66 | Our goal is to make your 3D assets fully optimized for web use
67 |
68 |
69 |
70 |
71 |
72 | For more detailed information on file preparation and best practices,
73 | please refer to our documentation.
74 |
75 |
76 |
77 | );
78 | };
79 |
80 | export default UploadInfoDialog;
81 |
--------------------------------------------------------------------------------
/apps/official-website/public/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 | Created by potrace 1.14, written by Peter Selinger 2001-2017
9 |
10 |
12 |
40 |
45 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/apps/official-website/src/pages/help.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 |
3 | import {
4 | Button,
5 | Accordion,
6 | AccordionContent,
7 | AccordionItem,
8 | AccordionTrigger,
9 | } from '@vctrl/shared/components';
10 |
11 | import TitleSection from '../components/title-section';
12 |
13 | const Help = () => {
14 | return (
15 | <>
16 |
17 |
18 |
24 |
25 |
26 | What file types are supported?
27 |
28 |
29 |
30 |
Supported import file types:
31 |
32 | .usdz (USDA), .gltf, .glb, .obj
33 |
34 |
35 |
36 |
37 |
Supported export file types:
38 |
39 |
40 | Free tier
41 | .usdz (USDA) .glb (glTF)
42 |
43 |
44 | Pro tier
45 | .usdz (USDC | Apple), .gltf + textures, .obj
46 |
47 |
48 |
49 |
50 |
51 |
52 | Can I use the tool for free?
53 |
54 | Yes. Using the free tier is completely free.
55 |
56 |
57 |
58 |
59 | Do I have to register an account?
60 |
61 |
62 | No. You will only need an account to export and save files.
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | Still have a question?
72 |
73 |
74 | We're here to help. If you have a question or comment, please
75 | reach out to us.
76 |
77 | Either use our contact form or get in contact using social media.
78 |
79 |
80 |
81 | Get in touch
82 |
83 |
88 | info@vectreal.com
89 |
90 |
91 |
92 |
93 | >
94 | );
95 | };
96 |
97 | export default Help;
98 |
--------------------------------------------------------------------------------
/apps/official-website/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const tailwindAnimate = require('tailwindcss-animate');
2 | const {
3 | default: flattenColorPalette,
4 | } = require('tailwindcss/lib/util/flattenColorPalette');
5 | const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
6 | const { join } = require('path');
7 |
8 | /** @type {import('tailwindcss').Config} */
9 | // eslint-disable-next-line no-undef
10 | module.exports = {
11 | darkMode: ['class'],
12 | content: [
13 | join(
14 | __dirname,
15 | '{src,pages,components,app}/**/*!(*.stories|*.spec).{ts,tsx,html}',
16 | ),
17 | ...createGlobPatternsForDependencies(__dirname),
18 | ],
19 | prefix: '',
20 | theme: {
21 | container: {
22 | center: true,
23 | padding: '2rem',
24 | screens: {
25 | '2xl': '1400px',
26 | },
27 | },
28 | fontFamily: {
29 | poppins: ['Poppins', 'sans-serif'],
30 | },
31 | extend: {
32 | colors: {
33 | orange: 'var(--orange)',
34 | 'dark-orange': 'var(--dark-orange)',
35 | border: 'hsl(var(--border))',
36 | input: 'hsl(var(--input))',
37 | ring: 'hsl(var(--ring))',
38 | background: 'hsl(var(--background))',
39 | foreground: 'hsl(var(--foreground))',
40 | primary: {
41 | DEFAULT: 'hsl(var(--primary))',
42 | foreground: 'hsl(var(--primary-foreground))',
43 | },
44 | secondary: {
45 | DEFAULT: 'hsl(var(--secondary))',
46 | foreground: 'hsl(var(--secondary-foreground))',
47 | },
48 | destructive: {
49 | DEFAULT: 'hsl(var(--destructive))',
50 | foreground: 'hsl(var(--destructive-foreground))',
51 | },
52 | muted: {
53 | DEFAULT: 'hsl(var(--muted))',
54 | foreground: 'hsl(var(--muted-foreground))',
55 | },
56 | accent: {
57 | DEFAULT: 'hsl(var(--accent))',
58 | foreground: 'hsl(var(--accent-foreground))',
59 | },
60 | popover: {
61 | DEFAULT: 'hsl(var(--popover))',
62 | foreground: 'hsl(var(--popover-foreground))',
63 | },
64 | card: {
65 | DEFAULT: 'hsl(var(--card))',
66 | foreground: 'hsl(var(--card-foreground))',
67 | },
68 | },
69 | borderRadius: {
70 | lg: 'var(--radius)',
71 | md: 'calc(var(--radius) - 2px)',
72 | sm: 'calc(var(--radius) - 4px)',
73 | },
74 | keyframes: {
75 | 'accordion-down': {
76 | from: { height: '0' },
77 | to: { height: 'var(--radix-accordion-content-height)' },
78 | },
79 | 'accordion-up': {
80 | from: { height: 'var(--radix-accordion-content-height)' },
81 | to: { height: '0' },
82 | },
83 | scroll: {
84 | to: {
85 | transform: 'translate(calc(-50% - 0.5rem))',
86 | },
87 | },
88 | },
89 | animation: {
90 | 'accordion-down': 'accordion-down 0.2s ease-out',
91 | 'accordion-up': 'accordion-up 0.2s ease-out',
92 | scroll:
93 | 'scroll var(--animation-duration, 40s) var(--animation-direction, forwards) linear infinite',
94 | },
95 | },
96 | },
97 | // eslint-disable-next-line no-undef
98 | plugins: [tailwindAnimate, addVariablesForColors],
99 | };
100 |
101 | function addVariablesForColors({ addBase, theme }) {
102 | let allColors = flattenColorPalette(theme('colors'));
103 | let newVars = Object.fromEntries(
104 | Object.entries(allColors).map(([key, val]) => [`--${key}`, val]),
105 | );
106 |
107 | addBase({
108 | ':root': newVars,
109 | });
110 | }
111 |
--------------------------------------------------------------------------------
/packages/viewer/src/components/scene/scene-grid.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from 'react';
2 | import { Grid, GridProps as ThreeGridProps } from '@react-three/drei';
3 | import { Box3, Mesh, Object3D, Vector3 } from 'three';
4 | import { useThree } from '@react-three/fiber';
5 |
6 | /**
7 | * Props for the SceneGrid component, extending ThreeGridProps from '@react-three/drei'.
8 | */
9 | export interface GridProps extends ThreeGridProps {
10 | /**
11 | * Whether to show the grid.
12 | */
13 | showGrid?: boolean;
14 |
15 | /**
16 | * Snap grid position to bottom of scene bounding box (y axis).
17 | */
18 | snapToBottom?: boolean;
19 | }
20 |
21 | /**
22 | * Default grid options.
23 | */
24 | export const defaultGridOptions: GridProps = {
25 | showGrid: false,
26 | snapToBottom: true,
27 | cellSize: 0.5,
28 | sectionSize: 5,
29 | sectionColor: 'rgb(134, 73, 33)',
30 | cellColor: 'rgb(100, 100, 100)',
31 | fadeDistance: 25,
32 | fadeStrength: 1,
33 | args: [10, 10],
34 | followCamera: false,
35 | infiniteGrid: true,
36 | };
37 |
38 | /**
39 | * SceneGrid component that renders a grid based on the size of the scene's content.
40 | * The grid adjusts its position and size according to the bounding box of the scene.
41 | */
42 | const SceneGrid = (props: GridProps) => {
43 | // State to hold the size of the scene's bounding box
44 | const [size, setSize] = useState(null);
45 |
46 | // Access the current scene from the three.js context
47 | const scene = useThree((state) => state.scene);
48 |
49 | /**
50 | * Recursively computes the union of bounding boxes of the given node(s).
51 | *
52 | * @param {Object3D | Object3D[]} node - The node or array of nodes to compute the bounding box for.
53 | * @returns {Box3} The union of the bounding boxes.
54 | */
55 | const computeBoundingBox = useCallback(
56 | (node: Object3D | Object3D[]): Box3 => {
57 | const bbox = new Box3();
58 | const nodes = Array.isArray(node) ? node : [node];
59 |
60 | nodes.forEach((child) => {
61 | if (child instanceof Mesh && child.name !== 'Grid') {
62 | // Include the bounding box of Meshes that are not the grid itself
63 | try {
64 | const childBbox = new Box3().setFromObject(child);
65 | bbox.union(childBbox);
66 | } catch (error) {
67 | console.error('Error computing mesh size:', error);
68 | }
69 | } else if (child instanceof Object3D) {
70 | // Recursively compute bounding boxes for child nodes
71 | const childBbox = computeBoundingBox(child.children);
72 | bbox.union(childBbox);
73 | }
74 | });
75 |
76 | return bbox;
77 | },
78 | [],
79 | );
80 |
81 | // Effect to compute the bounding box of the scene whenever the scene changes
82 | useEffect(() => {
83 | if (!scene) return;
84 |
85 | const bbox = computeBoundingBox(scene.children);
86 | setSize(bbox.getSize(new Vector3()));
87 | }, [scene.children, scene, computeBoundingBox]);
88 |
89 | // Merge default grid options with props
90 | const { showGrid, snapToBottom, ...gridOptions } = {
91 | ...defaultGridOptions,
92 | ...props,
93 | };
94 |
95 | // If showGrid is false or size is not available, don't render anything
96 | if (!showGrid) {
97 | return null;
98 | }
99 |
100 | const position =
101 | snapToBottom && size
102 | ? new Vector3(0, -size.y / 2, 0)
103 | : new Vector3(0, 0, 0);
104 |
105 | // Render the Grid component, positioning it at the bottom of the scene's bounding box
106 | return ;
107 | };
108 |
109 | export default SceneGrid;
110 |
--------------------------------------------------------------------------------
/packages/viewer/src/styles.module.css:
--------------------------------------------------------------------------------
1 | /* Color schemes */
2 | @define-mixin colors-light {
3 | --vctrl-bg: #f3f4f6;
4 | --vctrl-hover-bg: #e5e7eb;
5 | --vctrl-active-bg: #d1d5db;
6 | --vctrl-text: #1f2937;
7 | --vctrl-border: #181818;
8 | }
9 |
10 | @define-mixin colors-dark {
11 | --vctrl-bg: #141414;
12 | --vctrl-hover-bg: #363636;
13 | --vctrl-active-bg: #585858;
14 | --vctrl-text: #959595;
15 | --vctrl-border: rgba(229, 231, 235, 0.5);
16 | }
17 |
18 | /* Mixins for reusable styles */
19 | @define-mixin vctrl-bg-hover-active {
20 | background-color: var(--vctrl-bg);
21 | cursor: pointer;
22 |
23 | &:hover {
24 | background-color: var(--vctrl-hover-bg);
25 | }
26 |
27 | &:active {
28 | background-color: var(--vctrl-active-bg);
29 | }
30 | }
31 |
32 | /* Base light theme */
33 | .viewer {
34 | @mixin colors-light;
35 | }
36 |
37 | /* Dark mode variants */
38 | @media (prefers-color-scheme: dark) {
39 | .viewer {
40 | @mixin colors-dark;
41 | }
42 | }
43 |
44 | .viewer.dark,
45 | html.dark {
46 | @mixin colors-dark;
47 | }
48 |
49 | .viewer.light,
50 | html.light {
51 | @mixin colors-light;
52 | }
53 |
54 | .viewer {
55 | overflow: clip;
56 | font-family: poppins, sans-serif;
57 | font-size: 16px;
58 |
59 | button {
60 | border: none;
61 | }
62 |
63 | a {
64 | text-decoration: none;
65 | color: unset;
66 | }
67 |
68 | p {
69 | margin: 0;
70 | }
71 | }
72 |
73 | .viewer,
74 | .viewer-canvas,
75 | .spinner-wrapper {
76 | width: 100%;
77 | height: 100%;
78 | }
79 |
80 | .spinner-wrapper {
81 | display: flex;
82 | align-items: center;
83 | justify-content: center;
84 | }
85 |
86 | .popover {
87 | position: absolute;
88 | bottom: 0;
89 | margin: 0.5rem;
90 |
91 | .popover-trigger {
92 | position: relative;
93 | width: 1.5rem;
94 | height: 1.5rem;
95 |
96 | button {
97 | @mixin vctrl-bg-hover-active;
98 | width: inherit;
99 | height: inherit;
100 | padding: 0.25rem;
101 | border-radius: 50%;
102 | z-index: 10;
103 |
104 | svg {
105 | color: var(--vctrl-text);
106 | width: 1rem;
107 | height: 1rem;
108 | }
109 | }
110 | }
111 |
112 | .popover-modal {
113 | position: absolute;
114 | bottom: 0;
115 | left: 0;
116 | width: 16rem;
117 |
118 | display: flex;
119 | flex-direction: column;
120 |
121 | border-radius: 0.5rem;
122 | overflow: hidden;
123 | background-color: var(--vctrl-bg);
124 |
125 | transition: all 0.3s ease-out;
126 |
127 | &.hide {
128 | translate: -0.5rem 0.5rem;
129 | opacity: 0;
130 | visibility: hidden;
131 | }
132 |
133 | &.show {
134 | translate: 0 0;
135 | opacity: 1;
136 | visibility: visible;
137 | }
138 |
139 | .text-container {
140 | flex-grow: 1;
141 | padding: 1rem;
142 |
143 | p {
144 | font-size: 0.875rem;
145 | color: var(--vctrl-text);
146 | }
147 | }
148 |
149 | .popover-close {
150 | @mixin vctrl-bg-hover-active;
151 | position: absolute;
152 | right: 0;
153 | top: 0;
154 | width: 2rem;
155 | height: 2rem;
156 | margin: 0.5rem;
157 | padding: 0.5rem;
158 | border-radius: 0.25rem;
159 | color: var(--vctrl-text);
160 | transition: all 0.3s ease-in-out;
161 | }
162 |
163 | .popover-footer {
164 | @mixin vctrl-bg-hover-active;
165 | display: flex;
166 | align-items: center;
167 | justify-content: space-between;
168 | gap: 0.5rem;
169 | padding: 0.5rem 1rem;
170 | font-size: 0.75rem;
171 | border-top: 1px solid var(--vctrl-border);
172 | color: var(--vctrl-text);
173 | transition:
174 | color 0.3s,
175 | background-color 0.3s;
176 |
177 | svg {
178 | height: 1rem;
179 | width: 1rem;
180 | }
181 | }
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/apps/official-website/src/pages/editor/editor.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { toast } from 'sonner';
3 | import { Vector3 } from 'three';
4 |
5 | import { VectrealViewer } from '@vctrl/viewer';
6 | import {
7 | ModelFile,
8 | ModelProvider,
9 | useModelContext,
10 | } from '@vctrl/hooks/use-load-model';
11 | import { useOptimizeModel } from '@vctrl/hooks/use-optimize-model';
12 |
13 | import { sendCustomEvent } from '../../lib/utils/ga-utils';
14 | import { EditorProvider, useEditorContext } from '../../components/providers';
15 |
16 | import UploadInfoDialog from './upload-info-dialog';
17 | import DropZone from './drop-zone';
18 | import FileMenu from './file-menu';
19 |
20 | const Editor = () => {
21 | const { isFileLoading, file, on, off, reset } = useModelContext();
22 | const { autoRotate, hdr, showGrid, color } = useEditorContext();
23 |
24 | const [hasShownInfo, setHasShownInfo] = useState(
25 | !!window.sessionStorage.getItem('hasShownInfo'),
26 | );
27 |
28 | function handleReset() {
29 | reset();
30 | }
31 |
32 | function handleNotLoadedFiles(files?: File[]) {
33 | toast.error(`Not loaded files: ${files?.map((f) => f.name).join(', ')}`);
34 | }
35 |
36 | function handleLoadComplete(data?: ModelFile | null) {
37 | if (data) {
38 | toast.success(`Loaded ${data.name}`);
39 | }
40 | }
41 |
42 | function handleLoadError(error: unknown) {
43 | console.error('Load error:', error);
44 | toast.error(error as string);
45 | }
46 |
47 | function handleLoadStart() {
48 | sendCustomEvent({
49 | category: 'Editor Page',
50 | action: 'upload',
51 | label: 'Model Load Start',
52 | });
53 | }
54 |
55 | useEffect(() => {
56 | on('load-reset', handleReset);
57 | on('not-loaded-files', handleNotLoadedFiles);
58 | on('load-complete', handleLoadComplete);
59 | on('load-error', handleLoadError);
60 | on('load-start', handleLoadStart);
61 |
62 | return () => {
63 | off('load-reset', handleReset);
64 | off('not-loaded-files', handleNotLoadedFiles);
65 | off('load-complete', handleLoadComplete);
66 | off('load-error', handleLoadError);
67 | off('load-start', handleLoadStart);
68 | };
69 | // eslint-disable-next-line react-hooks/exhaustive-deps
70 | }, []);
71 |
72 | function handleClose() {
73 | setHasShownInfo(true);
74 | // To avoid showing the dialog again during the session
75 | window.sessionStorage.setItem('hasShownInfo', 'true');
76 | }
77 |
78 | return (
79 |
80 |
81 |
82 | {!isFileLoading && file ? (
83 |
110 | ) : (
111 |
112 | )}
113 |
114 |
115 |
116 | );
117 | };
118 |
119 | const EditorPage = () => {
120 | const optimizer = useOptimizeModel();
121 | return (
122 |
123 |
124 |
125 |
126 |
127 | );
128 | };
129 |
130 | export default EditorPage;
131 |
--------------------------------------------------------------------------------
/apps/official-website/vite.config.mts:
--------------------------------------------------------------------------------
1 | ///
2 | import { defineConfig } from 'vite';
3 | import { VitePWA } from 'vite-plugin-pwa';
4 | import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
5 | import react from '@vitejs/plugin-react';
6 | import path from 'path';
7 |
8 | import tailwindcss from 'tailwindcss';
9 | import autoprefixer from 'autoprefixer';
10 | import postcssMixins from 'postcss-mixins';
11 | import postcssNested from 'postcss-nested';
12 |
13 | export default defineConfig({
14 | root: __dirname,
15 | cacheDir: '../../node_modules/.vite/apps/official-website',
16 |
17 | plugins: [
18 | react(),
19 | nxViteTsPaths(),
20 | VitePWA({
21 | registerType: 'autoUpdate',
22 | devOptions: { enabled: true },
23 | workbox: {
24 | offlineGoogleAnalytics: true,
25 | globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
26 | runtimeCaching: [
27 | {
28 | urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
29 | handler: 'CacheFirst',
30 | options: {
31 | cacheName: 'google-fonts-cache',
32 | expiration: {
33 | maxEntries: 10,
34 | maxAgeSeconds: 60 * 60 * 24 * 365, // <== 365 days
35 | },
36 | cacheableResponse: {
37 | statuses: [0, 200],
38 | },
39 | },
40 | },
41 | {
42 | urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
43 | handler: 'CacheFirst',
44 | options: {
45 | cacheName: 'gstatic-fonts-cache',
46 | expiration: {
47 | maxEntries: 10,
48 | maxAgeSeconds: 60 * 60 * 24 * 365, // <== 365 days
49 | },
50 | cacheableResponse: {
51 | statuses: [0, 200],
52 | },
53 | },
54 | },
55 | ],
56 | },
57 | }),
58 | ],
59 |
60 | css: {
61 | postcss: {
62 | plugins: [autoprefixer, postcssNested, postcssMixins, tailwindcss],
63 | },
64 | },
65 |
66 | // Uncomment this if you are using workers.
67 | // worker: {
68 | // plugins: [ nxViteTsPaths() ],
69 | // },
70 |
71 | resolve: {
72 | alias: {
73 | '@vctrl/hooks': path.resolve(__dirname, '../../packages/hooks/src'),
74 | '@vctrl/viewer': path.resolve(__dirname, '../../packages/viewer/src'),
75 | '@vctrl/shared': path.resolve(__dirname, '../../shared/src'),
76 | },
77 | },
78 |
79 | build: {
80 | chunkSizeWarningLimit: 700, // three lib is always around 680kb
81 | rollupOptions: {
82 | output: {
83 | manualChunks: {
84 | three: ['three'],
85 | 'three-loaders': [
86 | 'three/examples/jsm/loaders/GLTFLoader',
87 | 'three/examples/jsm/loaders/DRACOLoader',
88 | 'three/examples/jsm/loaders/KTX2Loader',
89 | 'three/examples/jsm/loaders/USDZLoader',
90 | ],
91 | 'gltf-transform': [
92 | '@gltf-transform/extensions',
93 | '@gltf-transform/functions',
94 | '@gltf-transform/core',
95 | ],
96 | 'react-three-fiber': ['@react-three/fiber'],
97 | 'react-three-drei': ['@react-three/drei'],
98 | 'react-vendor': ['react', 'react-dom', 'react-router-dom'],
99 | 'ui-vendor': [
100 | '@radix-ui/react-accordion',
101 | '@radix-ui/react-avatar',
102 | '@radix-ui/react-dialog',
103 | '@radix-ui/react-dropdown-menu',
104 | '@radix-ui/react-icons',
105 | '@radix-ui/react-label',
106 | '@radix-ui/react-menubar',
107 | '@radix-ui/react-navigation-menu',
108 | '@radix-ui/react-progress',
109 | '@radix-ui/react-slot',
110 | 'sonner',
111 | 'tailwind-merge',
112 | ],
113 | },
114 | },
115 | },
116 |
117 | outDir: '../../dist/apps/official-website',
118 | emptyOutDir: true,
119 | reportCompressedSize: true,
120 | commonjsOptions: {
121 | transformMixedEsModules: true,
122 | },
123 | },
124 | });
125 |
--------------------------------------------------------------------------------
/apps/official-website/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Vectreal Core - Your Online 3D Optimizing Toolkit | React 3D Viewer &
17 | Hooks
18 |
19 |
23 |
24 |
25 |
26 |
27 |
31 |
35 |
39 |
40 |
41 |
42 |
43 |
47 |
51 |
55 |
56 |
57 |
78 |
79 |
80 |
81 |
82 |
87 |
92 |
93 |
98 |
99 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/packages/viewer/src/components/info-popover.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useRef, useEffect } from 'react';
2 | import { cn } from '@vctrl/shared/lib/utils';
3 |
4 | import styles from '../styles.module.css';
5 |
6 | import VectrealLogo from './assets/vectreal-logo';
7 | import InfoIcon from './assets/info-icon';
8 | import CrossIcon from './assets/cross-icon';
9 |
10 | export interface InfoPopoverProps {
11 | /**
12 | * Whether to add the info popover and it's trigger to the viewer.
13 | */
14 | showInfo?: boolean;
15 |
16 | /**
17 | * The content to display in the popover. Can be a JSX element or a string.
18 | */
19 | content?: JSX.Element | string;
20 | }
21 |
22 | export const defaultInfoPopoverProps: InfoPopoverProps = {
23 | showInfo: true,
24 | };
25 |
26 | const InfoPopover = (props: InfoPopoverProps) => {
27 | const { content, showInfo } = { ...defaultInfoPopoverProps, ...props };
28 | const [isOpen, setIsOpen] = useState(false);
29 | const popoverRef = useRef(null);
30 |
31 | // Handle focus when modal opens and closes
32 | useEffect(() => {
33 | const handleKeyDown = (event: KeyboardEvent) => {
34 | if (event.key === 'Escape' && isOpen) {
35 | setIsOpen(false);
36 | }
37 | };
38 |
39 | if (isOpen) {
40 | // Move focus to the modal
41 | popoverRef.current?.focus();
42 | document.addEventListener('keydown', handleKeyDown);
43 | }
44 |
45 | return () => {
46 | document.removeEventListener('keydown', handleKeyDown);
47 | };
48 | }, [isOpen]);
49 |
50 | // Trap focus within the modal when it's open
51 | useEffect(() => {
52 | const trapFocus = (event: FocusEvent) => {
53 | if (
54 | isOpen &&
55 | popoverRef.current &&
56 | !popoverRef.current.contains(event.target as Node)
57 | ) {
58 | event.preventDefault();
59 | popoverRef.current?.focus();
60 | }
61 | };
62 |
63 | if (isOpen) {
64 | document.addEventListener('focusin', trapFocus);
65 | } else {
66 | document.removeEventListener('focusin', trapFocus);
67 | }
68 |
69 | return () => {
70 | document.removeEventListener('focusin', trapFocus);
71 | };
72 | }, [isOpen]);
73 |
74 | return (
75 | showInfo && (
76 |
77 |
83 | setIsOpen(true)}
85 | aria-haspopup="dialog"
86 | aria-expanded={isOpen}
87 | aria-controls="info-popover"
88 | aria-label="Open information popover"
89 | >
90 |
91 |
92 |
93 |
105 |
setIsOpen(false)}
107 | aria-label="Close information popover"
108 | className={styles['popover-close']}
109 | >
110 |
111 |
112 |
113 | {content ? (
114 | typeof content === 'string' ? (
115 |
{content}
116 | ) : (
117 | content
118 | )
119 | ) : (
120 |
No additional info
121 | )}
122 |
123 |
124 |
130 | Vectreal viewer
131 |
132 |
133 |
134 |
135 | )
136 | );
137 | };
138 |
139 | export default InfoPopover;
140 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@vctrl/root",
3 | "private": true,
4 | "workspaces": [
5 | "packages/*",
6 | "apps/*",
7 | "shared/*"
8 | ],
9 | "repository": {
10 | "url": "https://github.com/vectreal/vectreal-core",
11 | "type": "https"
12 | },
13 | "scripts": {
14 | "publish-release": "npx nx release --skip-publish"
15 | },
16 | "dependencies": {
17 | "@gltf-transform/core": "^4.0.8",
18 | "@gltf-transform/extensions": "^4.0.10",
19 | "@gltf-transform/functions": "^4.0.8",
20 | "@radix-ui/react-accordion": "^1.2.3",
21 | "@radix-ui/react-avatar": "^1.1.3",
22 | "@radix-ui/react-dialog": "^1.1.6",
23 | "@radix-ui/react-dropdown-menu": "^2.1.6",
24 | "@radix-ui/react-icons": "^1.3.2",
25 | "@radix-ui/react-label": "^2.1.2",
26 | "@radix-ui/react-menubar": "^1.1.6",
27 | "@radix-ui/react-navigation-menu": "^1.2.5",
28 | "@radix-ui/react-popover": "^1.1.6",
29 | "@radix-ui/react-progress": "^1.1.2",
30 | "@radix-ui/react-slot": "^1.1.2",
31 | "@radix-ui/react-tooltip": "^1.1.8",
32 | "@react-three/drei": "^10.0.4",
33 | "@react-three/fiber": "^9.1.0",
34 | "@storybook/test": "^8.6.4",
35 | "class-variance-authority": "^0.7.0",
36 | "clsx": "^2.1.1",
37 | "file-saver": "^2.0.5",
38 | "framer-motion": "^11.14.4",
39 | "jszip": "^3.10.1",
40 | "meshoptimizer": "^0.21.0",
41 | "react": "^19.0.0",
42 | "react-colorful": "^5.6.1",
43 | "react-dom": "^19.0.0",
44 | "react-dropzone": "^14.2.3",
45 | "react-ga4": "^2.1.0",
46 | "react-router-dom": "^6.25.1",
47 | "sonner": "^2.0.1",
48 | "tailwind-merge": "^2.5.2",
49 | "tailwindcss-animate": "^1.0.7",
50 | "three": "^0.168.0"
51 | },
52 | "devDependencies": {
53 | "@babel/core": "^7.14.5",
54 | "@babel/preset-react": "^7.25.7",
55 | "@nx/devkit": "20.5.0",
56 | "@nx/eslint": "20.5.0",
57 | "@nx/eslint-plugin": "20.5.0",
58 | "@nx/jest": "20.5.0",
59 | "@nx/js": "20.5.0",
60 | "@nx/react": "^20.5.0",
61 | "@nx/rollup": "20.5.0",
62 | "@nx/storybook": "20.5.0",
63 | "@nx/vite": "20.5.0",
64 | "@nx/web": "20.5.0",
65 | "@nx/workspace": "20.5.0",
66 | "@storybook/addon-essentials": "8.6.4",
67 | "@storybook/addon-interactions": "8.6.4",
68 | "@storybook/core-server": "8.6.4",
69 | "@storybook/jest": "^0.2.3",
70 | "@storybook/react-vite": "^8.6.4",
71 | "@storybook/test-runner": "0.19.1",
72 | "@storybook/testing-library": "^0.2.2",
73 | "@swc-node/register": "~1.9.1",
74 | "@swc/cli": "^0.5.1",
75 | "@swc/core": "1.5.7",
76 | "@swc/helpers": "~0.5.11",
77 | "@testing-library/react": "16.1.0",
78 | "@types/file-saver": "^2.0.7",
79 | "@types/jest": "^29.4.0",
80 | "@types/node": "^22.2.0",
81 | "@types/postcss-mixins": "^9.0.6",
82 | "@types/postcss-nested": "^4.1.0",
83 | "@types/react": "^18.3.3",
84 | "@types/react-dom": "^18.3.0",
85 | "@types/three": "0.167.1",
86 | "@types/wicg-file-system-access": "^2023.10.5",
87 | "@typescript-eslint/eslint-plugin": "8.26.1",
88 | "@typescript-eslint/parser": "8.26.1",
89 | "@vitejs/plugin-react": "^4.3.4",
90 | "@vitejs/plugin-react-swc": "^3.8.0",
91 | "@vitest/ui": "^1.3.1",
92 | "autoprefixer": "^10.4.19",
93 | "babel-jest": "^29.4.1",
94 | "chromatic": "^11.16.5",
95 | "eslint": "~8.57.1",
96 | "eslint-config-prettier": "^9.0.0",
97 | "eslint-plugin-import": "2.31.0",
98 | "eslint-plugin-jsx-a11y": "6.10.1",
99 | "eslint-plugin-react": "7.32.2",
100 | "eslint-plugin-react-hooks": "5.0.0",
101 | "generate-license-file": "^3.6.0",
102 | "jest": "^29.4.1",
103 | "jest-environment-jsdom": "^29.4.1",
104 | "jsdom": "~22.1.0",
105 | "lerna": "^8.1.6",
106 | "postcss": "^8.4.47",
107 | "postcss-mixins": "^11.0.3",
108 | "postcss-modules": "^6.0.0",
109 | "prettier": "^3.3.3",
110 | "rollup": "^4.14.0",
111 | "storybook": "^8.4.5",
112 | "storybook-addon-deep-controls": "^0.9.2",
113 | "tailwindcss": "^3.4.6",
114 | "ts-jest": "^29.1.0",
115 | "ts-node": "10.9.1",
116 | "tslib": "^2.8.1",
117 | "typescript": "^5.8.2",
118 | "vite": "^6.2.2",
119 | "vite-plugin-dts": "4.5.3",
120 | "vite-plugin-pwa": "^0.21.1",
121 | "vitest": "^1.3.1"
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/shared/src/components/dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as DialogPrimitive from '@radix-ui/react-dialog';
5 | import { Cross2Icon } from '@radix-ui/react-icons';
6 |
7 | import { cn } from '../lib/utils';
8 |
9 | const Dialog = DialogPrimitive.Root;
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger;
12 |
13 | const DialogPortal = DialogPrimitive.Portal;
14 |
15 | const DialogClose = DialogPrimitive.Close;
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ));
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ));
54 | DialogContent.displayName = DialogPrimitive.Content.displayName;
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | );
68 | DialogHeader.displayName = 'DialogHeader';
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | );
82 | DialogFooter.displayName = 'DialogFooter';
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ));
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ));
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogClose,
116 | DialogTrigger,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | };
123 |
--------------------------------------------------------------------------------
/shared/src/components/glowing-stars.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useEffect, useRef, useState } from 'react';
4 | import { AnimatePresence, motion } from 'framer-motion';
5 | import { cn } from '../lib/utils';
6 |
7 | export const GlowingStarsBackgroundCard = ({
8 | className,
9 | children,
10 | }: {
11 | className?: string;
12 | children?: React.ReactNode;
13 | }) => {
14 | const [mouseEnter, setMouseEnter] = useState(false);
15 |
16 | return (
17 | {
19 | setMouseEnter(true);
20 | }}
21 | onMouseLeave={() => {
22 | setMouseEnter(false);
23 | }}
24 | className={cn(
25 | 'bg-[linear-gradient(110deg,#333_0.6%,#222)] p-4 max-w-md max-h-[20rem] h-full w-full rounded-xl border border-[#eaeaea] dark:border-neutral-600',
26 | className,
27 | )}
28 | >
29 |
30 |
31 |
32 |
{children}
33 |
34 | );
35 | };
36 |
37 | export const GlowingStarsDescription = ({
38 | className,
39 | children,
40 | }: {
41 | className?: string;
42 | children?: React.ReactNode;
43 | }) => {
44 | return (
45 |
46 | {children}
47 |
48 | );
49 | };
50 |
51 | export const GlowingStarsTitle = ({
52 | className,
53 | children,
54 | }: {
55 | className?: string;
56 | children?: React.ReactNode;
57 | }) => {
58 | return (
59 |
60 | {children}
61 |
62 | );
63 | };
64 |
65 | export const Illustration = ({ mouseEnter }: { mouseEnter: boolean }) => {
66 | const stars = 108;
67 | const columns = 18;
68 |
69 | const [glowingStars, setGlowingStars] = useState([]);
70 |
71 | const highlightedStars = useRef([]);
72 |
73 | useEffect(() => {
74 | const interval = setInterval(() => {
75 | highlightedStars.current = Array.from({ length: 5 }, () =>
76 | Math.floor(Math.random() * stars),
77 | );
78 | setGlowingStars([...highlightedStars.current]);
79 | }, 3000);
80 |
81 | return () => clearInterval(interval);
82 | }, []);
83 |
84 | return (
85 |
93 | {[...Array(stars)].map((_, starIdx) => {
94 | const isGlowing = glowingStars.includes(starIdx);
95 | const delay = (starIdx % 10) * 0.1;
96 | const staticDelay = starIdx * 0.01;
97 | return (
98 |
102 |
106 | {mouseEnter &&
}
107 |
108 | {isGlowing && }
109 |
110 |
111 | );
112 | })}
113 |
114 | );
115 | };
116 |
117 | const Star = ({ isGlowing, delay }: { isGlowing: boolean; delay: number }) => {
118 | return (
119 |
135 | );
136 | };
137 |
138 | const Glow = ({ delay }: { delay: number }) => {
139 | return (
140 |
157 | );
158 | };
159 |
--------------------------------------------------------------------------------
/packages/hooks/src/use-load-model/file-type-hooks/use-load-gltf.ts:
--------------------------------------------------------------------------------
1 | /* vectreal-core | vctrl/hooks
2 | Copyright (C) 2024 Moritz Becker
3 |
4 | This program is free software: you can redistribute it and/or modify
5 | it under the terms of the GNU Affero General Public License as published by
6 | the Free Software Foundation, either version 3 of the License, or
7 | (at your option) any later version.
8 |
9 | This program is distributed in the hope that it will be useful,
10 | but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | GNU Affero General Public License for more details.
13 |
14 | You should have received a copy of the GNU Affero General Public License
15 | along with this program. If not, see . */
16 |
17 | import { useCallback } from 'react';
18 |
19 | import { Action, ModelFileTypes, ReducedGltf } from '../types';
20 | import { arrayBufferToBase64 } from '../utils';
21 | import { createGltfLoader } from '../loaders';
22 |
23 | function useLoadGltf(dispatch: React.Dispatch) {
24 | const embedExternalResources = useCallback(
25 | async (
26 | gltfContent: ReducedGltf,
27 | otherFiles: File[],
28 | onProgress: (progress: number) => void,
29 | ) => {
30 | const fileMap = new Map(otherFiles.map((file) => [file.name, file]));
31 | const totalFiles =
32 | (gltfContent.buffers?.length || 0) + (gltfContent.images?.length || 0);
33 | let processedFiles = 0;
34 |
35 | const updateProgress = () => {
36 | processedFiles++;
37 | onProgress((processedFiles / totalFiles) * 100);
38 | };
39 |
40 | // Embed buffers
41 | if (gltfContent.buffers) {
42 | for (let i = 0; i < gltfContent.buffers.length; i++) {
43 | const buffer = gltfContent.buffers[i];
44 | if (!buffer.uri || buffer.uri.startsWith('data:')) {
45 | updateProgress();
46 | continue;
47 | }
48 |
49 | const fileName = buffer.uri.split('/').pop() || '';
50 | const file = fileMap.get(fileName);
51 | if (file) {
52 | const arrayBuffer = await file.arrayBuffer();
53 | const base64 = await arrayBufferToBase64(arrayBuffer);
54 | buffer.uri = `data:application/octet-stream;base64,${base64}`;
55 | }
56 | updateProgress();
57 | }
58 | }
59 |
60 | // Embed images
61 | if (gltfContent.images) {
62 | for (let i = 0; i < gltfContent.images.length; i++) {
63 | const image = gltfContent.images[i];
64 | if (!image.uri || image.uri.startsWith('data:')) {
65 | updateProgress();
66 | continue;
67 | }
68 |
69 | const fileName = image.uri.split('/').pop() || '';
70 | const file = fileMap.get(fileName);
71 | if (file) {
72 | const arrayBuffer = await file.arrayBuffer();
73 | const base64 = await arrayBufferToBase64(arrayBuffer);
74 | const mimeType = file.type || 'image/png';
75 | image.uri = `data:${mimeType};base64,${base64}`;
76 | }
77 | updateProgress();
78 | }
79 | }
80 |
81 | return gltfContent;
82 | },
83 | [],
84 | );
85 |
86 | const loadGltf = useCallback(
87 | (
88 | gltfFile: File,
89 | otherFiles: File[],
90 | onProgress: (progress: number) => void,
91 | ) => {
92 | const reader = new FileReader();
93 | reader.onload = async (e) => {
94 | const gltfContent = JSON.parse(e.target?.result as string);
95 | onProgress(10); // Initial progress after parsing GLTF
96 |
97 | const modifiedGLTF = await embedExternalResources(
98 | gltfContent,
99 | otherFiles,
100 | (embeddingProgress) => {
101 | // Map embedding progress to 10-90% range
102 | onProgress(10 + embeddingProgress * 0.8);
103 | },
104 | );
105 |
106 | const gltfLoader = createGltfLoader();
107 |
108 | gltfLoader.parse(JSON.stringify(modifiedGLTF), '', (gltf) => {
109 | dispatch({
110 | type: 'set-file',
111 | payload: {
112 | model: gltf.scene,
113 | type: ModelFileTypes.gltf,
114 | name: gltfFile.name,
115 | },
116 | });
117 | });
118 |
119 | onProgress(100); // Final progress
120 | dispatch({ type: 'set-file-loading', payload: false });
121 | };
122 |
123 | reader.readAsText(gltfFile);
124 | },
125 | [dispatch, embedExternalResources],
126 | );
127 |
128 | return { loadGltf };
129 | }
130 |
131 | export default useLoadGltf;
132 |
--------------------------------------------------------------------------------
/shared/src/components/sheet.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SheetPrimitive from "@radix-ui/react-dialog"
3 | import { Cross2Icon } from "@radix-ui/react-icons"
4 | import { cva, type VariantProps } from "class-variance-authority"
5 |
6 | import { cn } from "../lib/utils"
7 |
8 | const Sheet = SheetPrimitive.Root
9 |
10 | const SheetTrigger = SheetPrimitive.Trigger
11 |
12 | const SheetClose = SheetPrimitive.Close
13 |
14 | const SheetPortal = SheetPrimitive.Portal
15 |
16 | const SheetOverlay = React.forwardRef<
17 | React.ElementRef,
18 | React.ComponentPropsWithoutRef
19 | >(({ className, ...props }, ref) => (
20 |
28 | ))
29 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
30 |
31 | const sheetVariants = cva(
32 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
33 | {
34 | variants: {
35 | side: {
36 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
37 | bottom:
38 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
39 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
40 | right:
41 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
42 | },
43 | },
44 | defaultVariants: {
45 | side: "right",
46 | },
47 | }
48 | )
49 |
50 | interface SheetContentProps
51 | extends React.ComponentPropsWithoutRef,
52 | VariantProps {}
53 |
54 | const SheetContent = React.forwardRef<
55 | React.ElementRef,
56 | SheetContentProps
57 | >(({ side = "right", className, children, ...props }, ref) => (
58 |
59 |
60 |
65 | {children}
66 |
67 |
68 | Close
69 |
70 |
71 |
72 | ))
73 | SheetContent.displayName = SheetPrimitive.Content.displayName
74 |
75 | const SheetHeader = ({
76 | className,
77 | ...props
78 | }: React.HTMLAttributes) => (
79 |
86 | )
87 | SheetHeader.displayName = "SheetHeader"
88 |
89 | const SheetFooter = ({
90 | className,
91 | ...props
92 | }: React.HTMLAttributes) => (
93 |
100 | )
101 | SheetFooter.displayName = "SheetFooter"
102 |
103 | const SheetTitle = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
112 | ))
113 | SheetTitle.displayName = SheetPrimitive.Title.displayName
114 |
115 | const SheetDescription = React.forwardRef<
116 | React.ElementRef,
117 | React.ComponentPropsWithoutRef
118 | >(({ className, ...props }, ref) => (
119 |
124 | ))
125 | SheetDescription.displayName = SheetPrimitive.Description.displayName
126 |
127 | export {
128 | Sheet,
129 | SheetPortal,
130 | SheetOverlay,
131 | SheetTrigger,
132 | SheetClose,
133 | SheetContent,
134 | SheetHeader,
135 | SheetFooter,
136 | SheetTitle,
137 | SheetDescription,
138 | }
139 |
--------------------------------------------------------------------------------