├── .gitattributes ├── src ├── vite-env.d.ts ├── example │ ├── components │ │ ├── App.css │ │ ├── RemoteSetup.tsx │ │ ├── Wrapper.tsx │ │ ├── AppWrapper.tsx │ │ └── App.tsx │ ├── constants.ts │ ├── main.tsx │ ├── index.scss │ ├── three │ │ ├── FBXAnimation.ts │ │ ├── scenes │ │ │ ├── BaseScene.ts │ │ │ └── RTTScene.ts │ │ ├── loader.ts │ │ └── CustomShaderMaterial.ts │ └── CustomEditor.tsx ├── editor │ ├── multiView │ │ ├── MultiViewData.ts │ │ ├── InfiniteGridHelper.ts │ │ ├── Toggle.tsx │ │ ├── UVMaterial.ts │ │ ├── CameraWindow.tsx │ │ └── MultiView.scss │ ├── components │ │ ├── icons │ │ │ ├── CloseIcon.tsx │ │ │ └── DragIcon.tsx │ │ ├── NavButton.tsx │ │ ├── DraggableItem.tsx │ │ ├── Dropdown.tsx │ │ ├── types.ts │ │ ├── DropdownItem.tsx │ │ ├── Draggable.tsx │ │ └── content.ts │ ├── scss │ │ ├── theme.scss │ │ ├── debug.scss │ │ ├── draggable.scss │ │ ├── index.scss │ │ └── dropdown.scss │ ├── sidePanel │ │ ├── ContainerObject.tsx │ │ ├── ToggleBtn.tsx │ │ ├── inspector │ │ │ ├── InspectGrid4.tsx │ │ │ ├── utils │ │ │ │ ├── InspectLight.tsx │ │ │ │ ├── DragNumber.tsx │ │ │ │ ├── InspectCamera.tsx │ │ │ │ ├── InspectAnimation.tsx │ │ │ │ └── InspectTransform.tsx │ │ │ ├── InspectNumber.tsx │ │ │ ├── InspectGrid3.tsx │ │ │ ├── InspectImage.tsx │ │ │ └── Inspector.tsx │ │ ├── Accordion.tsx │ │ ├── types.ts │ │ ├── ChildObject.tsx │ │ └── SidePanel.tsx │ ├── Editor.tsx │ ├── ThreeEditor.tsx │ ├── utils.ts │ └── tools │ │ └── Transform.ts ├── glsl.d.ts ├── webworkers │ ├── types.ts │ ├── ProxyManager.ts │ └── EventHandling.ts ├── core │ ├── remote │ │ └── BaseRemote.ts │ ├── types.ts │ └── Application.ts ├── index.ts └── utils │ ├── detectSettings.ts │ └── math.ts ├── dist ├── models │ └── Flair.fbx ├── images │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── uv_grid_opengl.jpg │ ├── apple-touch-icon.png │ ├── milkyWay │ │ ├── dark-s_nx.jpg │ │ ├── dark-s_ny.jpg │ │ ├── dark-s_nz.jpg │ │ ├── dark-s_px.jpg │ │ ├── dark-s_py.jpg │ │ └── dark-s_pz.jpg │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ └── site.webmanifest ├── .vite │ └── manifest.json └── index.html ├── images └── dragMultiplier.gif ├── public ├── images │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── uv_grid_opengl.jpg │ ├── apple-touch-icon.png │ ├── milkyWay │ │ ├── dark-s_nx.jpg │ │ ├── dark-s_ny.jpg │ │ ├── dark-s_nz.jpg │ │ ├── dark-s_px.jpg │ │ ├── dark-s_py.jpg │ │ └── dark-s_pz.jpg │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ └── site.webmanifest └── models │ └── Flair.fbx ├── .npmignore ├── types ├── editor │ ├── components │ │ ├── icons │ │ │ ├── CloseIcon.d.ts │ │ │ └── DragIcon.d.ts │ │ ├── Dropdown.d.ts │ │ ├── Draggable.d.ts │ │ ├── DraggableItem.d.ts │ │ ├── DropdownItem.d.ts │ │ ├── NavButton.d.ts │ │ ├── types.d.ts │ │ └── content.d.ts │ ├── multiView │ │ ├── UVMaterial.d.ts │ │ ├── MultiViewData.d.ts │ │ ├── InfiniteGridHelper.d.ts │ │ ├── Toggle.d.ts │ │ ├── InfiniteGridMaterial.d.ts │ │ ├── CameraWindow.d.ts │ │ └── MultiView.d.ts │ ├── sidePanel │ │ ├── ChildObject.d.ts │ │ ├── ContainerObject.d.ts │ │ ├── SidePanel.d.ts │ │ ├── inspector │ │ │ ├── Inspector.d.ts │ │ │ ├── utils │ │ │ │ ├── InspectCamera.d.ts │ │ │ │ ├── InspectLight.d.ts │ │ │ │ ├── InspectAnimation.d.ts │ │ │ │ ├── DragNumber.d.ts │ │ │ │ ├── InspectMaterial.d.ts │ │ │ │ ├── InspectTransform.d.ts │ │ │ │ └── InspectRenderer.d.ts │ │ │ ├── InspectGrid4.d.ts │ │ │ ├── InspectVector2.d.ts │ │ │ ├── InspectImage.d.ts │ │ │ ├── InspectGrid3.d.ts │ │ │ ├── InspectNumber.d.ts │ │ │ ├── InspectorField.d.ts │ │ │ └── InspectorGroup.d.ts │ │ ├── ToggleBtn.d.ts │ │ ├── Accordion.d.ts │ │ ├── utils.d.ts │ │ ├── DebugData.d.ts │ │ └── types.d.ts │ ├── utils.d.ts │ ├── Editor.d.ts │ ├── ThreeEditor.d.ts │ └── tools │ │ ├── Transform.d.ts │ │ └── splineEditor │ │ ├── index.d.ts │ │ └── Spline.d.ts ├── webworkers │ ├── EventHandling.d.ts │ ├── types.d.ts │ └── ProxyManager.d.ts ├── utils │ ├── post.d.ts │ ├── detectSettings.d.ts │ ├── math.d.ts │ ├── theatre.d.ts │ └── three.d.ts ├── core │ ├── remote │ │ ├── BaseRemote.d.ts │ │ ├── RemoteTheatre.d.ts │ │ └── RemoteThree.d.ts │ ├── Application.d.ts │ └── types.d.ts └── index.d.ts ├── .eslintignore ├── tsconfig.node.json ├── vite.config.example.ts ├── .gitignore ├── index.html ├── .eslintrc.json ├── tsconfig.json ├── server └── index.mjs ├── vite.config.ts ├── package.json └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /dist/models/Flair.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/dist/models/Flair.fbx -------------------------------------------------------------------------------- /dist/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/dist/images/favicon.ico -------------------------------------------------------------------------------- /images/dragMultiplier.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/images/dragMultiplier.gif -------------------------------------------------------------------------------- /public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/public/images/favicon.ico -------------------------------------------------------------------------------- /public/models/Flair.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/public/models/Flair.fbx -------------------------------------------------------------------------------- /dist/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/dist/images/favicon-16x16.png -------------------------------------------------------------------------------- /dist/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/dist/images/favicon-32x32.png -------------------------------------------------------------------------------- /dist/images/uv_grid_opengl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/dist/images/uv_grid_opengl.jpg -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .vscode/ 3 | .DS_Store 4 | .npmignore 5 | dist/images 6 | dist/models 7 | node_modules 8 | src/example -------------------------------------------------------------------------------- /dist/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/dist/images/apple-touch-icon.png -------------------------------------------------------------------------------- /public/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/public/images/favicon-16x16.png -------------------------------------------------------------------------------- /public/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/public/images/favicon-32x32.png -------------------------------------------------------------------------------- /public/images/uv_grid_opengl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/public/images/uv_grid_opengl.jpg -------------------------------------------------------------------------------- /dist/images/milkyWay/dark-s_nx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/dist/images/milkyWay/dark-s_nx.jpg -------------------------------------------------------------------------------- /dist/images/milkyWay/dark-s_ny.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/dist/images/milkyWay/dark-s_ny.jpg -------------------------------------------------------------------------------- /dist/images/milkyWay/dark-s_nz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/dist/images/milkyWay/dark-s_nz.jpg -------------------------------------------------------------------------------- /dist/images/milkyWay/dark-s_px.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/dist/images/milkyWay/dark-s_px.jpg -------------------------------------------------------------------------------- /dist/images/milkyWay/dark-s_py.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/dist/images/milkyWay/dark-s_py.jpg -------------------------------------------------------------------------------- /dist/images/milkyWay/dark-s_pz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/dist/images/milkyWay/dark-s_pz.jpg -------------------------------------------------------------------------------- /public/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/public/images/apple-touch-icon.png -------------------------------------------------------------------------------- /public/images/milkyWay/dark-s_nx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/public/images/milkyWay/dark-s_nx.jpg -------------------------------------------------------------------------------- /public/images/milkyWay/dark-s_ny.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/public/images/milkyWay/dark-s_ny.jpg -------------------------------------------------------------------------------- /public/images/milkyWay/dark-s_nz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/public/images/milkyWay/dark-s_nz.jpg -------------------------------------------------------------------------------- /public/images/milkyWay/dark-s_px.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/public/images/milkyWay/dark-s_px.jpg -------------------------------------------------------------------------------- /public/images/milkyWay/dark-s_py.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/public/images/milkyWay/dark-s_py.jpg -------------------------------------------------------------------------------- /public/images/milkyWay/dark-s_pz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/public/images/milkyWay/dark-s_pz.jpg -------------------------------------------------------------------------------- /dist/images/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/dist/images/android-chrome-192x192.png -------------------------------------------------------------------------------- /dist/images/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/dist/images/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/images/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/public/images/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/images/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomorrowevening/hermes/HEAD/public/images/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/example/components/App.css: -------------------------------------------------------------------------------- 1 | #box { 2 | background: #FF0000; 3 | width: 100px; 4 | height: 100px; 5 | position: absolute; 6 | } -------------------------------------------------------------------------------- /types/editor/components/icons/CloseIcon.d.ts: -------------------------------------------------------------------------------- 1 | declare const _default: import("react/jsx-runtime").JSX.Element; 2 | export default _default; 3 | -------------------------------------------------------------------------------- /types/editor/components/icons/DragIcon.d.ts: -------------------------------------------------------------------------------- 1 | declare const _default: import("react/jsx-runtime").JSX.Element; 2 | export default _default; 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .github 2 | .history 3 | .husky 4 | .vscode 5 | coverage 6 | dist 7 | public 8 | types 9 | node_modules 10 | *.vert 11 | *.frag 12 | *.glsl -------------------------------------------------------------------------------- /types/editor/multiView/UVMaterial.d.ts: -------------------------------------------------------------------------------- 1 | import { ShaderMaterial } from 'three'; 2 | export default class UVMaterial extends ShaderMaterial { 3 | constructor(); 4 | } 5 | -------------------------------------------------------------------------------- /types/editor/components/Dropdown.d.ts: -------------------------------------------------------------------------------- 1 | import { DropdownProps } from './types'; 2 | export default function Dropdown(props: DropdownProps): import("react/jsx-runtime").JSX.Element; 3 | -------------------------------------------------------------------------------- /types/editor/components/Draggable.d.ts: -------------------------------------------------------------------------------- 1 | import { DraggableProps } from './types'; 2 | export default function Draggable(props: DraggableProps): import("react/jsx-runtime").JSX.Element; 3 | -------------------------------------------------------------------------------- /types/editor/components/DraggableItem.d.ts: -------------------------------------------------------------------------------- 1 | import { DraggableItemProps } from './types'; 2 | export default function DraggableItem(props: DraggableItemProps): import("react/jsx-runtime").JSX.Element; 3 | -------------------------------------------------------------------------------- /types/editor/components/DropdownItem.d.ts: -------------------------------------------------------------------------------- 1 | import { DropdownItemProps } from './types'; 2 | export default function DropdownItem(props: DropdownItemProps): import("react/jsx-runtime").JSX.Element; 3 | -------------------------------------------------------------------------------- /types/editor/sidePanel/ChildObject.d.ts: -------------------------------------------------------------------------------- 1 | import { ChildObjectProps } from './types'; 2 | export default function ChildObject(props: ChildObjectProps): import("react/jsx-runtime").JSX.Element | null; 3 | -------------------------------------------------------------------------------- /types/editor/sidePanel/ContainerObject.d.ts: -------------------------------------------------------------------------------- 1 | import { ChildObjectProps } from './types'; 2 | export default function ContainerObject(props: ChildObjectProps): import("react/jsx-runtime").JSX.Element; 3 | -------------------------------------------------------------------------------- /types/editor/components/NavButton.d.ts: -------------------------------------------------------------------------------- 1 | type NavButtonProps = { 2 | title: string; 3 | }; 4 | export default function NavButton(props: NavButtonProps): import("react/jsx-runtime").JSX.Element; 5 | export {}; 6 | -------------------------------------------------------------------------------- /types/editor/sidePanel/SidePanel.d.ts: -------------------------------------------------------------------------------- 1 | import { SidePanelState } from './types'; 2 | import '../scss/sidePanel.scss'; 3 | export default function SidePanel(props: SidePanelState): import("react/jsx-runtime").JSX.Element; 4 | -------------------------------------------------------------------------------- /types/editor/sidePanel/inspector/Inspector.d.ts: -------------------------------------------------------------------------------- 1 | import { CoreComponentProps } from '../types'; 2 | import './inspector.scss'; 3 | export default function Inspector(props: CoreComponentProps): import("react/jsx-runtime").JSX.Element; 4 | -------------------------------------------------------------------------------- /src/editor/multiView/MultiViewData.ts: -------------------------------------------------------------------------------- 1 | export type MultiViewMode = 'Single' | 'Side by Side' | 'Stacked' |'Quad'; 2 | export type RenderMode = 'Depth' | 'Normals' | 'Renderer' | 'UVs' | 'Wireframe'; 3 | export type InteractionMode = 'Orbit' | 'Selection'; 4 | -------------------------------------------------------------------------------- /types/editor/multiView/MultiViewData.d.ts: -------------------------------------------------------------------------------- 1 | export type MultiViewMode = 'Single' | 'Side by Side' | 'Stacked' | 'Quad'; 2 | export type RenderMode = 'Depth' | 'Normals' | 'Renderer' | 'UVs' | 'Wireframe'; 3 | export type InteractionMode = 'Orbit' | 'Selection'; 4 | -------------------------------------------------------------------------------- /types/editor/sidePanel/inspector/utils/InspectCamera.d.ts: -------------------------------------------------------------------------------- 1 | import RemoteThree from '../../../../core/remote/RemoteThree'; 2 | import { RemoteObject } from '../../types'; 3 | export declare function InspectCamera(object: RemoteObject, three: RemoteThree): any; 4 | -------------------------------------------------------------------------------- /dist/.vite/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "src/index.ts": { 3 | "file": "hermes.es.js", 4 | "name": "index", 5 | "src": "src/index.ts", 6 | "isEntry": true 7 | }, 8 | "style.css": { 9 | "file": "hermes.css", 10 | "src": "style.css" 11 | } 12 | } -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /types/editor/sidePanel/ToggleBtn.d.ts: -------------------------------------------------------------------------------- 1 | type ToggleBtnProps = { 2 | expanded: boolean; 3 | label: string; 4 | onClick: (expanded: boolean) => void; 5 | }; 6 | export default function ToggleBtn(props: ToggleBtnProps): import("react/jsx-runtime").JSX.Element; 7 | export {}; 8 | -------------------------------------------------------------------------------- /dist/images/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /public/images/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /types/editor/sidePanel/inspector/utils/InspectLight.d.ts: -------------------------------------------------------------------------------- 1 | import RemoteThree from '../../../../core/remote/RemoteThree'; 2 | import { RemoteObject } from '../../types'; 3 | export declare function InspectLight(object: RemoteObject, three: RemoteThree): import("react/jsx-runtime").JSX.Element; 4 | -------------------------------------------------------------------------------- /src/glsl.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.glsl' { 2 | const value: string; 3 | export default value; 4 | } 5 | 6 | declare module '*.vert' { 7 | const value: string; 8 | export default value; 9 | } 10 | 11 | declare module '*.frag' { 12 | const value: string; 13 | export default value; 14 | } -------------------------------------------------------------------------------- /types/editor/utils.d.ts: -------------------------------------------------------------------------------- 1 | export declare function capitalize(value: string): string; 2 | export declare function copyToClipboard(data: any): string; 3 | export declare function randomID(): string; 4 | export declare function isColor(obj: any): boolean; 5 | export declare function colorToHex(obj: any): string; 6 | -------------------------------------------------------------------------------- /src/editor/components/icons/CloseIcon.tsx: -------------------------------------------------------------------------------- 1 | export default ( 2 | 3 | 4 | 5 | 6 | 7 | ); -------------------------------------------------------------------------------- /types/editor/multiView/InfiniteGridHelper.d.ts: -------------------------------------------------------------------------------- 1 | import { Mesh } from 'three'; 2 | import InfiniteGridMaterial, { InfiniteGridProps } from './InfiniteGridMaterial'; 3 | export default class InfiniteGridHelper extends Mesh { 4 | gridMaterial: InfiniteGridMaterial; 5 | constructor(props?: InfiniteGridProps); 6 | update(): void; 7 | } 8 | -------------------------------------------------------------------------------- /vite.config.example.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | resolve: { 8 | }, 9 | build: { 10 | emptyOutDir: false, 11 | assetsDir: '', 12 | outDir: 'dist' 13 | } 14 | }); -------------------------------------------------------------------------------- /types/editor/sidePanel/inspector/InspectGrid4.d.ts: -------------------------------------------------------------------------------- 1 | import { Matrix4, Vector4 } from 'three'; 2 | interface InspectGrid4Props { 3 | value: Vector4 | Matrix4; 4 | step?: number; 5 | onChange: (evt: any) => void; 6 | } 7 | export default function InspectGrid4(props: InspectGrid4Props): import("react/jsx-runtime").JSX.Element; 8 | export {}; 9 | -------------------------------------------------------------------------------- /types/editor/sidePanel/inspector/InspectVector2.d.ts: -------------------------------------------------------------------------------- 1 | interface InspectVector2Props { 2 | min: number; 3 | max: number; 4 | value: any; 5 | step?: number; 6 | onChange: (evt: any) => void; 7 | } 8 | export default function InspectVector2(props: InspectVector2Props): import("react/jsx-runtime").JSX.Element; 9 | export {}; 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | *.local 12 | 13 | # Editor directories and files 14 | .vscode/* 15 | !.vscode/extensions.json 16 | .idea 17 | .DS_Store 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | .vercel -------------------------------------------------------------------------------- /types/editor/sidePanel/inspector/InspectImage.d.ts: -------------------------------------------------------------------------------- 1 | type InspectImageProps = { 2 | title: string; 3 | prop?: string; 4 | value?: any; 5 | step?: number; 6 | onChange?: (prop: string, value: any) => void; 7 | }; 8 | export default function InspectImage(props: InspectImageProps): import("react/jsx-runtime").JSX.Element; 9 | export {}; 10 | -------------------------------------------------------------------------------- /src/editor/components/NavButton.tsx: -------------------------------------------------------------------------------- 1 | type NavButtonProps = { 2 | title: string 3 | } 4 | 5 | export default function NavButton(props: NavButtonProps) { 6 | return props.title.search('<') > -1 ? ( 7 | 8 | ) : ( 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /types/editor/sidePanel/inspector/InspectGrid3.d.ts: -------------------------------------------------------------------------------- 1 | import { Euler, Matrix3, Vector3 } from 'three'; 2 | interface InspectGrid3Props { 3 | value: Vector3 | Matrix3 | Euler; 4 | step?: number; 5 | onChange: (evt: any) => void; 6 | } 7 | export default function InspectGrid3(props: InspectGrid3Props): import("react/jsx-runtime").JSX.Element; 8 | export {}; 9 | -------------------------------------------------------------------------------- /types/editor/multiView/Toggle.d.ts: -------------------------------------------------------------------------------- 1 | type ToggleProps = { 2 | name: string; 3 | icon: string; 4 | selected: boolean; 5 | onClick: (selected: boolean) => void; 6 | height: number; 7 | width?: number; 8 | top?: number; 9 | }; 10 | export default function Toggle(props: ToggleProps): import("react/jsx-runtime").JSX.Element; 11 | export {}; 12 | -------------------------------------------------------------------------------- /types/webworkers/EventHandling.d.ts: -------------------------------------------------------------------------------- 1 | type EventHandler = (event: Event, sendFn: (data: any) => void) => void; 2 | export declare const WebworkerEventHandlers: Record; 3 | export declare class ElementProxy { 4 | id: number; 5 | worker: Worker; 6 | constructor(element: HTMLElement, worker: Worker, eventHandlers: Record); 7 | } 8 | export {}; 9 | -------------------------------------------------------------------------------- /src/editor/scss/theme.scss: -------------------------------------------------------------------------------- 1 | $ROW_HEIGHT: 22px; 2 | $BAR_COLOR: rgba(34, 34, 34, 0.8); 3 | $BORDER_COLOR: rgba(17, 17, 17, 0.9); 4 | $BTN_SIZE: 32px; 5 | $BTN_HOVER: rgba(51, 51, 51, 0.8); 6 | $BTN_DRAG: rgba(64, 64, 64, 1); 7 | $BTN_SELECTED: rgba(68, 68, 68, 0.8); 8 | $BTN_SELECTED_HOVER: rgba(85, 85, 85, 0.8); 9 | $PANEL: rgba(25, 25, 25, 0.8); 10 | $PANEL_DARK: rgba(17, 17, 17, 0.8); -------------------------------------------------------------------------------- /types/editor/sidePanel/inspector/utils/InspectAnimation.d.ts: -------------------------------------------------------------------------------- 1 | import RemoteThree from '../../../../core/remote/RemoteThree'; 2 | import { RemoteObject } from '../../types'; 3 | type InspectAnimationProps = { 4 | object: RemoteObject; 5 | three: RemoteThree; 6 | }; 7 | export default function InspectAnimation(props: InspectAnimationProps): import("react/jsx-runtime").JSX.Element; 8 | export {}; 9 | -------------------------------------------------------------------------------- /src/editor/components/icons/DragIcon.tsx: -------------------------------------------------------------------------------- 1 | export default ( 2 | 3 | 8 | 9 | ); -------------------------------------------------------------------------------- /src/example/constants.ts: -------------------------------------------------------------------------------- 1 | import { EventDispatcher } from 'three'; 2 | 3 | export const IS_DEV = true; 4 | export const IS_EDITOR = IS_DEV && document.location.hash.search('editor') > -1; 5 | 6 | export enum Events { 7 | LOAD_COMPLETE = 'Events::loadComplete' 8 | } 9 | 10 | type WebGLEvent = { 11 | [key in Events]: { value?: unknown } 12 | } 13 | 14 | export const threeDispatcher = new EventDispatcher(); 15 | -------------------------------------------------------------------------------- /types/editor/multiView/InfiniteGridMaterial.d.ts: -------------------------------------------------------------------------------- 1 | import { Color, ShaderMaterial } from 'three'; 2 | export type InfiniteGridProps = { 3 | divisions?: number; 4 | scale?: number; 5 | color?: Color; 6 | distance?: number; 7 | subgridOpacity?: number; 8 | gridOpacity?: number; 9 | }; 10 | export default class InfiniteGridMaterial extends ShaderMaterial { 11 | constructor(props?: InfiniteGridProps); 12 | } 13 | -------------------------------------------------------------------------------- /types/editor/Editor.d.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties, Ref } from 'react'; 2 | import './scss/index.scss'; 3 | type EditorProps = { 4 | header?: JSX.Element | JSX.Element[]; 5 | children?: JSX.Element | JSX.Element[]; 6 | footer?: JSX.Element | JSX.Element[]; 7 | ref?: Ref; 8 | style?: CSSProperties; 9 | }; 10 | export default function Editor(props: EditorProps): import("react/jsx-runtime").JSX.Element; 11 | export {}; 12 | -------------------------------------------------------------------------------- /types/editor/sidePanel/inspector/utils/DragNumber.d.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from 'react'; 2 | interface DragProps { 3 | label: RefObject; 4 | input: RefObject; 5 | sliderRef?: RefObject; 6 | defaultValue: number; 7 | min?: number; 8 | max?: number; 9 | step?: number; 10 | onChange?: (value: number) => void; 11 | } 12 | export default function DragNumber(props: DragProps): number; 13 | export {}; 14 | -------------------------------------------------------------------------------- /types/editor/ThreeEditor.d.ts: -------------------------------------------------------------------------------- 1 | import { Scene } from 'three'; 2 | import RemoteThree from '../core/remote/RemoteThree'; 3 | interface ThreeEditorProps { 4 | three: RemoteThree; 5 | scenes: Map; 6 | onSceneAdd?: (scene: Scene) => void; 7 | onSceneUpdate?: (scene: Scene) => void; 8 | onSceneResize?: (scene: Scene, width: number, height: number) => void; 9 | } 10 | export default function ThreeEditor(props: ThreeEditorProps): import("react/jsx-runtime").JSX.Element; 11 | export {}; 12 | -------------------------------------------------------------------------------- /types/editor/sidePanel/Accordion.d.ts: -------------------------------------------------------------------------------- 1 | import RemoteThree from '../../core/remote/RemoteThree'; 2 | type AccordionProps = { 3 | three: RemoteThree; 4 | label: string; 5 | scene?: any; 6 | button?: JSX.Element; 7 | children?: JSX.Element | JSX.Element[]; 8 | open?: boolean; 9 | visible?: boolean; 10 | onToggle?: (value: boolean) => void; 11 | onRefresh?: () => void; 12 | }; 13 | export default function Accordion(props: AccordionProps): import("react/jsx-runtime").JSX.Element; 14 | export {}; 15 | -------------------------------------------------------------------------------- /types/utils/post.d.ts: -------------------------------------------------------------------------------- 1 | import { PerspectiveCamera, WebGLRenderer } from 'three'; 2 | import RemoteThree from '../core/remote/RemoteThree'; 3 | export declare function inspectComposerPass(pass: any, three: RemoteThree, includeTextures?: boolean): void; 4 | export declare function inspectComposer(composer: any, three: RemoteThree): void; 5 | export declare function clearComposerGroups(three: RemoteThree): void; 6 | export declare function generateCubemap(renderer: WebGLRenderer, camera: PerspectiveCamera, composer: any, size?: number): Promise; 7 | -------------------------------------------------------------------------------- /src/editor/sidePanel/ContainerObject.tsx: -------------------------------------------------------------------------------- 1 | import ChildObject from './ChildObject'; 2 | import { ChildObjectProps, RemoteObject } from './types'; 3 | 4 | export default function ContainerObject(props: ChildObjectProps) { 5 | const children: Array = []; 6 | props.child?.children.map((child: RemoteObject, index: number) => { 7 | children.push(); 8 | }); 9 | return
{children}
; 10 | } 11 | -------------------------------------------------------------------------------- /src/example/components/RemoteSetup.tsx: -------------------------------------------------------------------------------- 1 | import studio from '@theatre/studio'; 2 | import Application from '../../core/Application'; 3 | import RemoteTheatre from '../../core/remote/RemoteTheatre'; 4 | 5 | type RemoteProps = { 6 | app: Application 7 | } 8 | 9 | export default function RemoteSetup(props: RemoteProps) { 10 | const app = props.app; 11 | 12 | // Remote Theatre setup 13 | const theatre = app.components.get('theatre') as RemoteTheatre; 14 | theatre.studio = studio; 15 | theatre.handleEditorApp(); 16 | 17 | return null; 18 | } 19 | -------------------------------------------------------------------------------- /types/utils/detectSettings.d.ts: -------------------------------------------------------------------------------- 1 | export type QualityType = 'High' | 'Medium' | 'Low'; 2 | export type AppSettings = { 3 | dpr: number; 4 | fps: number; 5 | width: number; 6 | height: number; 7 | mobile: boolean; 8 | supportOffScreenCanvas: boolean; 9 | supportWebGPU: boolean; 10 | quality: QualityType; 11 | dev: boolean; 12 | editor: boolean; 13 | }; 14 | export declare function detectMaxFrameRate(callback: (fps: number) => void): void; 15 | export declare function detectSettings(dev?: boolean, editor?: boolean): Promise; 16 | -------------------------------------------------------------------------------- /src/editor/Editor.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, Ref } from 'react'; 2 | import './scss/index.scss'; 3 | 4 | type EditorProps = { 5 | header?: JSX.Element | JSX.Element[] 6 | children?: JSX.Element | JSX.Element[] 7 | footer?: JSX.Element | JSX.Element[] 8 | ref?: Ref 9 | style?: CSSProperties 10 | } 11 | 12 | export default function Editor(props: EditorProps) { 13 | return ( 14 |
15 |
{props.header}
16 | {props.children} 17 |
{props.footer}
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /types/editor/sidePanel/inspector/InspectNumber.d.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from 'react'; 2 | import { InspectorFieldType } from './InspectorField'; 3 | export interface InspectNumberProps { 4 | alt?: string; 5 | value: number; 6 | prop: string; 7 | type: InspectorFieldType; 8 | min?: number; 9 | max?: number; 10 | step?: number; 11 | disabled?: boolean; 12 | className?: string; 13 | labelRef: RefObject; 14 | onChange?: (prop: string, value: number) => void; 15 | } 16 | export default function InspectNumber(props: InspectNumberProps): import("react/jsx-runtime").JSX.Element; 17 | -------------------------------------------------------------------------------- /types/core/remote/BaseRemote.d.ts: -------------------------------------------------------------------------------- 1 | import type { BroadcastData } from '../types'; 2 | export default class BaseRemote { 3 | name: string; 4 | protected _debug: boolean; 5 | protected _editor: boolean; 6 | protected broadcastChannel?: BroadcastChannel; 7 | constructor(name: string, debug?: boolean, editor?: boolean); 8 | dispose(): void; 9 | get debug(): boolean; 10 | get editor(): boolean; 11 | protected send(data: BroadcastData): void; 12 | protected messageHandler(evt: MessageEvent): void; 13 | protected handleApp(msg: BroadcastData): void; 14 | protected handleEditor(msg: BroadcastData): void; 15 | } 16 | -------------------------------------------------------------------------------- /types/editor/sidePanel/utils.d.ts: -------------------------------------------------------------------------------- 1 | import { Object3D, Texture } from 'three'; 2 | import { MinimumObject, RemoteObject } from './types'; 3 | export declare function determineIcon(obj: RemoteObject): string; 4 | export declare function stripScene(obj: Object3D): MinimumObject; 5 | export declare function convertImageToBase64(imgElement: HTMLImageElement): string; 6 | export declare function stripObject(obj: Object3D): RemoteObject; 7 | export declare function getSubItem(child: any, key: string): any; 8 | export declare function setItemProps(child: any, key: string, value: any): void; 9 | export declare function textureFromSrc(imgSource: string): Promise; 10 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Application 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/example/main.tsx: -------------------------------------------------------------------------------- 1 | // Libs 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom/client'; 4 | import studio from '@theatre/studio'; 5 | // Models 6 | import { IS_DEV } from './constants'; 7 | // Components 8 | import './index.scss'; 9 | import AppWrapper from './components/AppWrapper'; 10 | 11 | // Debug tools 12 | if (IS_DEV) { 13 | studio.initialize(); 14 | } 15 | 16 | // React 17 | 18 | ReactDOM.createRoot(document.getElementById('root')!).render( 19 | <> 20 | {IS_DEV ? ( 21 | <> 22 | 23 | 24 | ) : ( 25 | 26 | 27 | 28 | )} 29 | , 30 | ); 31 | -------------------------------------------------------------------------------- /types/editor/sidePanel/inspector/utils/InspectMaterial.d.ts: -------------------------------------------------------------------------------- 1 | import { RemoteMaterial, RemoteObject } from '../../types'; 2 | import RemoteThree from '../../../../core/remote/RemoteThree'; 3 | export declare function acceptedMaterialNames(name: string): boolean; 4 | export declare function imageNames(name: string): string; 5 | export declare function prettyName(name: string): string; 6 | export declare function clampedNames(name: string): boolean; 7 | export declare function uploadLocalImage(): Promise; 8 | export declare function inspectMaterialItems(material: RemoteMaterial, object: RemoteObject, three: RemoteThree): any[]; 9 | export declare function InspectMaterial(object: RemoteObject, three: RemoteThree): any; 10 | -------------------------------------------------------------------------------- /src/editor/sidePanel/ToggleBtn.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { capitalize } from '../../editor/utils'; 3 | 4 | type ToggleBtnProps = { 5 | expanded: boolean 6 | label: string 7 | onClick: (expanded: boolean) => void; 8 | } 9 | 10 | export default function ToggleBtn(props: ToggleBtnProps) { 11 | const [expanded, setExpanded] = useState(props.expanded); 12 | return ( 13 | 26 | ); 27 | } -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Application 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /src/editor/multiView/InfiniteGridHelper.ts: -------------------------------------------------------------------------------- 1 | import { Mesh, PlaneGeometry } from 'three'; 2 | import InfiniteGridMaterial, { InfiniteGridProps } from './InfiniteGridMaterial'; 3 | 4 | /** 5 | * Copied from: 6 | * https://github.com/theatre-js/theatre/blob/main/packages/r3f/src/extension/InfiniteGridHelper/index.ts 7 | */ 8 | 9 | export default class InfiniteGridHelper extends Mesh { 10 | gridMaterial: InfiniteGridMaterial; 11 | 12 | constructor(props?: InfiniteGridProps) { 13 | const material = new InfiniteGridMaterial(props); 14 | super(new PlaneGeometry(), material); 15 | this.gridMaterial = material; 16 | this.frustumCulled = false; 17 | this.name = 'InfiniteGridHelper'; 18 | } 19 | 20 | update() { 21 | this.gridMaterial.needsUpdate = true; 22 | } 23 | } -------------------------------------------------------------------------------- /types/editor/sidePanel/inspector/InspectorField.d.ts: -------------------------------------------------------------------------------- 1 | import { KeyboardEvent } from 'react'; 2 | import { OptionInfo } from '../../../core/types'; 3 | export type InspectorFieldType = 'string' | 'field' | 'number' | 'boolean' | 'range' | 'color' | 'button' | 'image' | 'option' | 'vector2' | 'grid3' | 'grid4' | 'euler'; 4 | export interface InspectorFieldProps { 5 | title: string; 6 | type: InspectorFieldType; 7 | prop?: string; 8 | value?: any; 9 | min?: number; 10 | max?: number; 11 | step?: number; 12 | disabled?: boolean; 13 | options?: OptionInfo[]; 14 | onChange?: (prop: string, value: any) => void; 15 | onKeyDown?: (evt: KeyboardEvent) => void; 16 | } 17 | export default function InspectorField(props: InspectorFieldProps): import("react/jsx-runtime").JSX.Element; 18 | -------------------------------------------------------------------------------- /src/webworkers/types.ts: -------------------------------------------------------------------------------- 1 | import { AnimationClip, Object3DJSON } from 'three'; 2 | 3 | export type FileType = 'audio' | 'blob' | 'buffer' | 'fbx' | 'gltf' | 'image' | 'json' | 'video'; 4 | 5 | export type File = { 6 | name: string 7 | file: string 8 | type: FileType 9 | } 10 | 11 | export type ModelInfo = { 12 | animations: AnimationClip[] 13 | cameras?: Object3DJSON[] 14 | scene: Object3DJSON 15 | } 16 | 17 | export type ModelLite = { 18 | animations: AnimationClip[] 19 | cameras?: Object3DJSON[] 20 | scene: Object3DJSON 21 | } 22 | 23 | export type Assets = { 24 | audio: Map 25 | blob: Map 26 | buffer: Map 27 | image: Map 28 | json: Map 29 | model: Map 30 | video: Map 31 | } 32 | -------------------------------------------------------------------------------- /types/webworkers/types.d.ts: -------------------------------------------------------------------------------- 1 | import { AnimationClip, Object3DJSON } from 'three'; 2 | export type FileType = 'audio' | 'blob' | 'buffer' | 'fbx' | 'gltf' | 'image' | 'json' | 'video'; 3 | export type File = { 4 | name: string; 5 | file: string; 6 | type: FileType; 7 | }; 8 | export type ModelInfo = { 9 | animations: AnimationClip[]; 10 | cameras?: Object3DJSON[]; 11 | scene: Object3DJSON; 12 | }; 13 | export type ModelLite = { 14 | animations: AnimationClip[]; 15 | cameras?: Object3DJSON[]; 16 | scene: Object3DJSON; 17 | }; 18 | export type Assets = { 19 | audio: Map; 20 | blob: Map; 21 | buffer: Map; 22 | image: Map; 23 | json: Map; 24 | model: Map; 25 | video: Map; 26 | }; 27 | -------------------------------------------------------------------------------- /types/editor/multiView/CameraWindow.d.ts: -------------------------------------------------------------------------------- 1 | import { Camera } from 'three'; 2 | import { RenderMode } from './MultiViewData'; 3 | interface DropdownProps { 4 | index: number; 5 | open: boolean; 6 | title: string; 7 | onToggle: (value: boolean) => void; 8 | onSelect: (value: string) => void; 9 | options: string[]; 10 | up?: boolean; 11 | } 12 | export declare const Dropdown: (props: DropdownProps) => import("react/jsx-runtime").JSX.Element; 13 | interface CameraWindowProps { 14 | name: string; 15 | camera: Camera; 16 | onSelectCamera: (value: string) => void; 17 | onSelectRenderMode: (value: RenderMode) => void; 18 | options: string[]; 19 | } 20 | declare const CameraWindow: import("react").ForwardRefExoticComponent>; 21 | export default CameraWindow; 22 | -------------------------------------------------------------------------------- /src/editor/components/DraggableItem.tsx: -------------------------------------------------------------------------------- 1 | import CloseIcon from './icons/CloseIcon'; 2 | import DragIcon from './icons/DragIcon'; 3 | import { DraggableItemProps } from './types'; 4 | 5 | export default function DraggableItem(props: DraggableItemProps) { 6 | return ( 7 |
  • props.onDragStart(props.index)} 11 | onDragOver={(e) => { 12 | e.preventDefault(); 13 | props.onDragOver(props.index); 14 | }} 15 | onDragEnd={props.onDragEnd} 16 | > 17 |
    18 | {DragIcon} 19 | {props.title} 20 | 23 |
    24 |
  • 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/example/index.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | html, body { 18 | min-width: 100%; 19 | min-height: 100%; 20 | width: 100%; 21 | height: 100%; 22 | } 23 | 24 | body { 25 | padding: 0; 26 | margin: 0; 27 | overflow: hidden; 28 | } 29 | 30 | button { 31 | &:focus { 32 | outline: none; 33 | } 34 | 35 | &:hover { 36 | cursor: pointer; 37 | } 38 | } 39 | 40 | .loading { 41 | position: absolute; 42 | left: 50%; 43 | top: 50%; 44 | transform: translate(calc(-50% - 150px), -50%); 45 | } 46 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:react/recommended" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "ecmaVersion": "latest", 14 | "sourceType": "module" 15 | }, 16 | "plugins": [ 17 | "@typescript-eslint", 18 | "react" 19 | ], 20 | "rules": { 21 | "@typescript-eslint/ban-ts-comment": "off", 22 | "@typescript-eslint/no-explicit-any": "off", 23 | "@typescript-eslint/no-unused-vars": "off", 24 | "max-len": ["error", 200], 25 | "quotes": [ 26 | "error", 27 | "single", 28 | { 29 | "avoidEscape": true, 30 | "allowTemplateLiterals": true 31 | } 32 | ], 33 | "react/prop-types": "off", 34 | "react/react-in-jsx-scope": "off", 35 | "semi": "error" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/example/components/Wrapper.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Events, threeDispatcher } from '../constants'; 3 | import { loadAssets } from '../three/loader'; 4 | import './App.css'; 5 | import App from './App'; 6 | import Application from '../../core/Application'; 7 | 8 | type WrapperProps = { 9 | app: Application 10 | } 11 | 12 | export default function Wrapper(props: WrapperProps) { 13 | const [loaded, setLoaded] = useState(false); 14 | 15 | useEffect(() => { 16 | const onLoad = () => { 17 | threeDispatcher.removeEventListener(Events.LOAD_COMPLETE, onLoad); 18 | setLoaded(true); 19 | }; 20 | 21 | threeDispatcher.addEventListener(Events.LOAD_COMPLETE, onLoad); 22 | loadAssets(props.app); 23 | }, [setLoaded]); 24 | 25 | return ( 26 | <> 27 | {!loaded &&

    Loading...

    } 28 | {loaded && props.app.isApp && } 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /types/editor/tools/Transform.d.ts: -------------------------------------------------------------------------------- 1 | import { Camera, EventDispatcher } from 'three'; 2 | import { TransformControls } from 'three/examples/jsm/controls/TransformControls'; 3 | import RemoteThree from '../../core/remote/RemoteThree'; 4 | export default class Transform extends EventDispatcher { 5 | static DRAG_START: string; 6 | static DRAG_END: string; 7 | private static _instance; 8 | three: RemoteThree; 9 | activeCamera: Camera; 10 | controls: Map; 11 | private visibility; 12 | setApp(three: RemoteThree): void; 13 | clear(): void; 14 | add(name: string): TransformControls; 15 | get(name: string): TransformControls | undefined; 16 | remove(name: string): boolean; 17 | enabled(value: boolean): void; 18 | updateCamera(camera: Camera, element: HTMLElement): void; 19 | show(): void; 20 | hide(): void; 21 | private setScene; 22 | static get instance(): Transform; 23 | } 24 | -------------------------------------------------------------------------------- /types/editor/sidePanel/inspector/utils/InspectTransform.d.ts: -------------------------------------------------------------------------------- 1 | import { Euler, Matrix4, Vector3 } from 'three'; 2 | import { Component, ReactNode } from 'react'; 3 | import { RemoteObject } from '../../types'; 4 | import RemoteThree from '../../../../core/remote/RemoteThree'; 5 | type InspectTransformProps = { 6 | object: RemoteObject; 7 | three: RemoteThree; 8 | }; 9 | type InspectTransformState = { 10 | lastUpdated: number; 11 | expanded: boolean; 12 | }; 13 | export declare class InspectTransform extends Component { 14 | static instance: InspectTransform; 15 | matrix: Matrix4; 16 | position: Vector3; 17 | rotation: Euler; 18 | scale: Vector3; 19 | open: boolean; 20 | constructor(props: InspectTransformProps); 21 | update(): void; 22 | render(): ReactNode; 23 | private updateTransform; 24 | private saveExpanded; 25 | get expandedName(): string; 26 | } 27 | export {}; 28 | -------------------------------------------------------------------------------- /types/utils/math.d.ts: -------------------------------------------------------------------------------- 1 | export declare function clamp(min: number, max: number, value: number): number; 2 | export declare function normalize(min: number, max: number, value: number): number; 3 | export declare function mix(min: number, max: number, value: number): number; 4 | export declare function map(min1: number, max1: number, min2: number, max2: number, value: number): number; 5 | export declare function distance(x: number, y: number): number; 6 | export declare function damp(start: number, end: number, easing: number, dt: number): number; 7 | export declare function roundTo(value: number, digits?: number): number; 8 | export declare function getAngle(x1: number, y1: number, x2: number, y2: number): number; 9 | export declare function cubicBezier(percent: number, x0: number, y0: number, x1: number, y1: number): number; 10 | export declare function rgbaToHex({ r, g, b, a }: { 11 | r: number; 12 | g: number; 13 | b: number; 14 | a?: number; 15 | }): string; 16 | -------------------------------------------------------------------------------- /src/editor/ThreeEditor.tsx: -------------------------------------------------------------------------------- 1 | import { Scene } from 'three'; 2 | import Editor from './Editor'; 3 | import MultiView from './multiView/MultiView'; 4 | import RemoteThree from '../core/remote/RemoteThree'; 5 | import SidePanel from './sidePanel/SidePanel'; 6 | 7 | interface ThreeEditorProps { 8 | three: RemoteThree 9 | scenes: Map 10 | onSceneAdd?: (scene: Scene) => void 11 | onSceneUpdate?: (scene: Scene) => void 12 | onSceneResize?: (scene: Scene, width: number, height: number) => void 13 | } 14 | 15 | export default function ThreeEditor(props: ThreeEditorProps) { 16 | return ( 17 | 18 | <> 19 | 26 | 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /types/utils/theatre.d.ts: -------------------------------------------------------------------------------- 1 | import RemoteTheatre from '../core/remote/RemoteTheatre'; 2 | export declare function useStudio(): any; 3 | export declare function customizeTheatreElements(): Promise; 4 | export declare function animateObjectTransform(sheet: string, key: string, obj: any, theatre: RemoteTheatre): void; 5 | type PropType = 'array' | 'boolean' | 'color' | 'euler' | 'matrix2' | 'matrix3' | 'matrix4' | 'number' | 'object' | 'string' | 'texture' | 'vector2' | 'vector3' | 'vector4'; 6 | type PropToAdd = { 7 | name: string; 8 | type: PropType; 9 | value: any; 10 | }; 11 | export declare function getObjectMaterialProps(material: any): PropToAdd[]; 12 | export declare function getObjectMaterialObject(props: PropToAdd[]): {}; 13 | export declare function applyObjectMaterial(material: any, props: PropToAdd[], values: any): void; 14 | export declare function animateObjectMaterial(sheet: string, key: string, material: any, theatre: RemoteTheatre): void; 15 | export {}; 16 | -------------------------------------------------------------------------------- /src/example/three/FBXAnimation.ts: -------------------------------------------------------------------------------- 1 | import { AnimationMixer, Object3D } from 'three'; 2 | // @ts-ignore 3 | import { clone } from 'three/examples/jsm/utils/SkeletonUtils'; 4 | import { models } from './loader'; 5 | 6 | export default class FBXAnimation extends Object3D { 7 | private mixer: AnimationMixer; 8 | 9 | constructor(name: string) { 10 | super(); 11 | this.name = name; 12 | this.scale.setScalar(0.5); 13 | 14 | const model = models.get(name)!; 15 | const modelInstance = clone(model); 16 | this.add(modelInstance); 17 | 18 | this.mixer = new AnimationMixer(modelInstance); 19 | modelInstance['mixer'] = this.mixer; 20 | const action = this.mixer.clipAction(modelInstance.animations[0]); 21 | action.play(); 22 | } 23 | 24 | update(delta: number) { 25 | this.mixer.update(delta); 26 | } 27 | 28 | get timeScale(): number { 29 | return this.mixer.timeScale; 30 | } 31 | 32 | set timeScale(value: number) { 33 | this.mixer.timeScale = value; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /types/editor/sidePanel/inspector/utils/InspectRenderer.d.ts: -------------------------------------------------------------------------------- 1 | import { Component, ReactNode } from 'react'; 2 | import RemoteThree from '../../../../core/remote/RemoteThree'; 3 | type InspectRendererProps = { 4 | three: RemoteThree; 5 | }; 6 | type InspectRendererState = { 7 | expanded: boolean; 8 | lastUpdated: number; 9 | }; 10 | export default class InspectRenderer extends Component { 11 | private autoClear; 12 | private autoClearColor; 13 | private autoClearDepth; 14 | private autoClearStencil; 15 | private outputColorSpace; 16 | private localClippingEnabled; 17 | private clearColor; 18 | private clearAlpha; 19 | private toneMapping; 20 | private toneMappingExposure; 21 | private type; 22 | constructor(props: InspectRendererProps); 23 | componentwillunmount(): void; 24 | private onAddRenderer; 25 | render(): ReactNode; 26 | private saveExpanded; 27 | get expandedName(): string; 28 | } 29 | export {}; 30 | -------------------------------------------------------------------------------- /src/editor/scss/debug.scss: -------------------------------------------------------------------------------- 1 | body { 2 | .tp-dfwv, 3 | .tp-dfwv button, 4 | .tp-dfwv input { 5 | text-transform: none; 6 | } 7 | } 8 | 9 | .tp-ckbv { 10 | float: right; 11 | } 12 | 13 | .tp-dfwv { 14 | left: 50%; 15 | top: 0; 16 | max-height: 100%; 17 | overflow-x: hidden; 18 | overflow-y: auto; 19 | transform: translateX(-50%); 20 | width: 400px; 21 | z-index: 100; 22 | 23 | .tp-lblv { 24 | position: relative; 25 | } 26 | 27 | .tp-lblv_v { 28 | display: inline-block; 29 | 30 | .tp-ckbv { 31 | width: 20px; 32 | } 33 | 34 | .tp-fpsv { 35 | width: 280px; 36 | } 37 | } 38 | 39 | .tp-btnv_b { 40 | padding: 0 5px; 41 | } 42 | 43 | .tp-btngridv { 44 | max-height: 100px; 45 | overflow-x: hidden; 46 | overflow-y: auto; 47 | } 48 | 49 | .tp-tabv { 50 | max-height: 90vh; 51 | overflow: hidden auto; 52 | } 53 | } 54 | 55 | .tp-dfwv { 56 | font-family: Roboto Mono, Source Code Pro, Menlo, Courier, monospace; 57 | font-size: 10px; 58 | } -------------------------------------------------------------------------------- /types/core/Application.d.ts: -------------------------------------------------------------------------------- 1 | import BaseRemote from './remote/BaseRemote'; 2 | import { AppSettings } from '../utils/detectSettings'; 3 | export default class Application { 4 | assets: { 5 | audio: Map; 6 | image: Map; 7 | json: Map; 8 | model: Map; 9 | video: Map; 10 | }; 11 | components: Map; 12 | settings: AppSettings; 13 | onUpdateCallback?: () => void; 14 | protected playing: boolean; 15 | protected rafID: number; 16 | dispose(): void; 17 | detectSettings(dev?: boolean, editor?: boolean): Promise; 18 | update(): void; 19 | draw(): void; 20 | play: () => void; 21 | pause: () => void; 22 | private onUpdate; 23 | addComponent(name: string, component: BaseRemote): void; 24 | get debugEnabled(): boolean; 25 | get isApp(): boolean; 26 | set isApp(value: boolean); 27 | get editor(): boolean; 28 | set editor(value: boolean); 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "allowSyntheticDefaultImports": true, 5 | "esModuleInterop": false, 6 | "forceConsistentCasingInFileNames": true, 7 | "isolatedModules": true, 8 | "jsx": "react-jsx", 9 | "lib": [ 10 | "DOM", 11 | "DOM.Iterable", 12 | "ESNext" 13 | ], 14 | "module": "ESNext", 15 | "moduleResolution": "Node", 16 | "removeComments": true, 17 | "resolveJsonModule": true, 18 | "skipLibCheck": true, 19 | "strict": true, 20 | "target": "ESNext", 21 | "useDefineForClassFields": true, 22 | "noImplicitAny": false, 23 | "baseUrl": "./", 24 | "types": [ 25 | "vite-plugin-glsl/ext" 26 | ] 27 | }, 28 | "include": [ 29 | "src" 30 | ], 31 | "exclude": [ 32 | "dist", 33 | "node_modules", 34 | "src/example", 35 | "types", 36 | "*.vert", 37 | "*.frag", 38 | "*.glsl" 39 | ], 40 | "references": [ 41 | { 42 | "path": "./tsconfig.node.json" 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /types/editor/sidePanel/DebugData.d.ts: -------------------------------------------------------------------------------- 1 | import { Component, ReactNode, RefObject } from 'react'; 2 | import RemoteThree from '../../core/remote/RemoteThree'; 3 | import { GroupData } from '../../core/types'; 4 | import InspectorGroup from './inspector/InspectorGroup'; 5 | interface DebugDataProps { 6 | three: RemoteThree; 7 | } 8 | type DebugDataState = { 9 | lastUpdate: number; 10 | }; 11 | export default class DebugData extends Component { 12 | static instance: DebugData; 13 | static groups: JSX.Element[]; 14 | static groupsRefs: RefObject[]; 15 | static groupTitles: string[]; 16 | static three: RemoteThree; 17 | constructor(props: DebugDataProps); 18 | componentWillUnmount(): void; 19 | render(): ReactNode; 20 | private addGroup; 21 | private removeGroup; 22 | static addEditorGroup(data: GroupData): RefObject | null; 23 | static removeEditorGroup(name: string): void; 24 | static removeAllGroups(): void; 25 | } 26 | export {}; 27 | -------------------------------------------------------------------------------- /server/index.mjs: -------------------------------------------------------------------------------- 1 | import { WebSocketServer } from 'ws'; 2 | 3 | const users = new Map(); 4 | const server = new WebSocketServer({ port: 8080 }); 5 | let totalUsers = 0; 6 | 7 | function createNewUser(ws) { 8 | const userID = `user_${totalUsers}`; 9 | totalUsers++; 10 | 11 | // Create user 12 | const newUser = { 13 | userID: userID, 14 | socket: ws, 15 | }; 16 | users.set(userID, newUser); 17 | console.log('User connected:', userID); 18 | 19 | return newUser; 20 | } 21 | 22 | server.on('connection', (ws) => { 23 | const newUser = createNewUser(ws); 24 | ws.on('message', (message) => { 25 | const content = JSON.parse(message); 26 | const data = JSON.stringify(content); 27 | users.forEach((user) => { 28 | if (user.userID !== newUser.userID) { 29 | user.socket.send(data); 30 | } 31 | }); 32 | }); 33 | 34 | // User left 35 | ws.on('close', () => { 36 | users.delete(newUser.userID); 37 | console.log('User disconnected:', newUser.userID, 'Total users:', users.size); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/editor/multiView/Toggle.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | type ToggleProps = { 4 | name: string; 5 | icon: string; 6 | selected: boolean; 7 | onClick: (selected: boolean) => void; 8 | height: number; 9 | width?: number; 10 | top?: number; 11 | } 12 | 13 | export default function Toggle(props: ToggleProps) { 14 | const [selected, setSelected] = useState(props.selected); 15 | const className = 'toggle' + (selected ? ' selected' : ''); 16 | return ( 17 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/editor/utils.ts: -------------------------------------------------------------------------------- 1 | export function capitalize(value: string): string { 2 | return value.substring(0, 1).toUpperCase() + value.substring(1); 3 | } 4 | 5 | export function copyToClipboard(data: any): string { 6 | const content = JSON.stringify(data); 7 | navigator.clipboard.writeText(content); 8 | return content; 9 | } 10 | 11 | export function randomID(): string { 12 | return Math.round(Math.random() * 1000000).toString(); 13 | } 14 | 15 | export function isColor(obj: any) { 16 | return ( 17 | obj.r !== undefined && 18 | obj.g !== undefined && 19 | obj.b !== undefined 20 | ); 21 | } 22 | 23 | export function colorToHex(obj: any) { 24 | const r = Math.round(obj.r * 255); 25 | const g = Math.round(obj.g * 255); 26 | const b = Math.round(obj.b * 255); 27 | 28 | const toHex = (value: number) => { 29 | const hex = value.toString(16); 30 | return hex.length === 1 ? '0' + hex : hex; 31 | }; 32 | 33 | const red = toHex(r); 34 | const green = toHex(g); 35 | const blue = toHex(b); 36 | 37 | return '#' + red + green + blue; 38 | } 39 | -------------------------------------------------------------------------------- /types/editor/sidePanel/inspector/InspectorGroup.d.ts: -------------------------------------------------------------------------------- 1 | import { Component, ReactNode, RefObject } from 'react'; 2 | import { InspectorFieldProps } from './InspectorField'; 3 | import { GroupData } from '../../../core/types'; 4 | import RemoteThree from '../../../core/remote/RemoteThree'; 5 | export interface InspectorGroupProps { 6 | three: RemoteThree; 7 | title: string; 8 | expanded?: boolean; 9 | items: InspectorFieldProps[] | InspectorGroupProps[]; 10 | onToggle?: (value: boolean) => void; 11 | } 12 | type InspectorGroupState = { 13 | lastUpdated: number; 14 | }; 15 | export default class InspectorGroup extends Component { 16 | subgroupNames: string[]; 17 | subgroupElements: JSX.Element[]; 18 | valueOverrides: Map; 19 | three: RemoteThree; 20 | constructor(props: InspectorGroupProps); 21 | addGroup(data: GroupData): RefObject; 22 | removeGroup(name: string): void; 23 | setField(name: string, value: any): void; 24 | render(): ReactNode; 25 | } 26 | export {}; 27 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import glsl from 'vite-plugin-glsl'; 4 | 5 | export default defineConfig({ 6 | build: { 7 | target: 'esnext', 8 | lib: { 9 | entry: 'src/index.ts', 10 | name: 'Hermes', 11 | formats: ['cjs', 'es'], 12 | fileName: (format) => `hermes.${format}.js`, 13 | }, 14 | manifest: true, 15 | rollupOptions: { 16 | external: [ 17 | 'react', 18 | 'three', 19 | 'framer-motion', 20 | '@theatre/core', 21 | '@theatre/studio', 22 | 'postprocessing', 23 | ], 24 | output: { 25 | globals: { 26 | react: 'React', 27 | three: 'THREE', 28 | } 29 | } 30 | } 31 | }, 32 | plugins: [ 33 | glsl({ 34 | include: ['**/*.glsl', '**/*.vert', '**/*.frag'], 35 | warnDuplicatedImports: true, 36 | defaultExtension: 'glsl', 37 | watch: true, 38 | }), 39 | react() 40 | ], 41 | resolve: { 42 | alias: { 43 | '@': '/src', 44 | '~@': '/src', 45 | } 46 | }, 47 | }); 48 | -------------------------------------------------------------------------------- /src/editor/components/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | // Libs 2 | import { useState } from 'react'; 3 | // Views 4 | import NavButton from './NavButton'; 5 | import DropdownItem from './DropdownItem'; 6 | import { DropdownOption, DropdownProps } from './types'; 7 | 8 | export default function Dropdown(props: DropdownProps) { 9 | const [expanded, setExpanded] = useState(false); 10 | 11 | const list: Array = []; 12 | { 13 | props.options.map((option: DropdownOption, index: number) => { 14 | if (props.onSelect !== undefined) { 15 | option.onSelect = props.onSelect; 16 | } 17 | list.push(); 18 | }); 19 | } 20 | 21 | let ddClassName = 'dropdown'; 22 | if (props.subdropdown) ddClassName += ' subdropdown'; 23 | 24 | return ( 25 |
    setExpanded(true)} 28 | onMouseLeave={() => setExpanded(false)} 29 | > 30 | 31 |
      34 | {list} 35 |
    36 |
    37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /types/editor/components/types.d.ts: -------------------------------------------------------------------------------- 1 | export type DropdownType = 'option' | 'dropdown' | 'draggable'; 2 | export interface DropdownOption { 3 | title: string; 4 | value: any | Array; 5 | type: DropdownType; 6 | onSelect?: (value: any) => void; 7 | selectable?: boolean; 8 | onDragComplete?: (options: string[]) => void; 9 | } 10 | export interface DropdownProps { 11 | title: string; 12 | options: Array; 13 | onSelect?: (value: any) => void; 14 | subdropdown?: boolean; 15 | } 16 | export interface DropdownItemProps { 17 | option: DropdownOption; 18 | onSelect?: (value: any) => void; 19 | onDragComplete?: (options: string[]) => void; 20 | } 21 | export interface DraggableItemProps { 22 | index: number; 23 | title: string; 24 | draggingIndex: number | null; 25 | onDelete: (index: number) => void; 26 | onDragStart: (value: number) => void; 27 | onDragOver: (value: number) => void; 28 | onDragEnd: () => void; 29 | } 30 | export interface DraggableProps { 31 | title: string; 32 | options: string[]; 33 | onDragComplete: (options: string[]) => void; 34 | subdropdown?: boolean; 35 | } 36 | -------------------------------------------------------------------------------- /src/editor/components/types.ts: -------------------------------------------------------------------------------- 1 | export type DropdownType = 'option' | 'dropdown' | 'draggable' 2 | 3 | export interface DropdownOption { 4 | title: string 5 | value: any | Array 6 | type: DropdownType 7 | // Option 8 | onSelect?: (value: any) => void 9 | selectable?: boolean 10 | // Draggable 11 | onDragComplete?: (options: string[]) => void 12 | } 13 | 14 | export interface DropdownProps { 15 | title: string 16 | options: Array 17 | onSelect?: (value: any) => void 18 | subdropdown?: boolean 19 | } 20 | 21 | export interface DropdownItemProps { 22 | option: DropdownOption 23 | onSelect?: (value: any) => void 24 | // Draggable 25 | onDragComplete?: (options: string[]) => void 26 | } 27 | 28 | // Draggable 29 | 30 | export interface DraggableItemProps { 31 | index: number 32 | title: string 33 | draggingIndex: number | null 34 | onDelete: (index: number) => void 35 | onDragStart: (value: number) => void 36 | onDragOver: (value: number) => void 37 | onDragEnd: () => void 38 | } 39 | 40 | export interface DraggableProps { 41 | title: string 42 | options: string[] 43 | onDragComplete: (options: string[]) => void 44 | subdropdown?: boolean 45 | } 46 | -------------------------------------------------------------------------------- /src/editor/scss/draggable.scss: -------------------------------------------------------------------------------- 1 | @use './theme'; 2 | 3 | .editor .header .draggable { 4 | li { 5 | cursor: grab; 6 | transition: transform 0.25s ease-out; 7 | &.dragging { 8 | transform: scale(1.1); 9 | div { 10 | background-color: theme.$BTN_DRAG; 11 | } 12 | } 13 | div { 14 | background-color: theme.$BAR_COLOR; 15 | line-height: 14px; 16 | padding: 5px 10px; 17 | transition: background-color 0.25s linear; 18 | &:hover { 19 | background-color: theme.$BTN_HOVER; 20 | } 21 | span { 22 | font-size: 12px; 23 | margin: 0px 15px 0 10px; 24 | padding: 0px 5px; 25 | } 26 | .dragIcon { 27 | position: absolute; 28 | left: 10px; 29 | } 30 | .closeIcon { 31 | background-color: transparent; 32 | padding: 0; 33 | position: absolute; 34 | right: 5px; 35 | top: 50%; 36 | min-width: 14px; 37 | width: 14px; 38 | height: 14px; 39 | transform: translateY(-50%); 40 | visibility: inherit; 41 | svg { 42 | background-color: transparent; 43 | left: 0px; 44 | position: relative; 45 | } 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/editor/scss/index.scss: -------------------------------------------------------------------------------- 1 | @use './debug'; 2 | @use './sidePanel'; 3 | @use './dropdown'; 4 | @use './draggable'; 5 | 6 | .editor { 7 | background: linear-gradient(45deg, #000, #333); 8 | font-family: Arial, Helvetica, sans-serif; 9 | font-size: 12px; 10 | left: 0; 11 | pointer-events: none; 12 | position: absolute; 13 | top: 0; 14 | width: 100%; 15 | height: 100%; 16 | z-index: 100; 17 | 18 | button { 19 | background: none; 20 | border: none; 21 | color: white; 22 | display: inline-block; 23 | margin: 0; 24 | padding: 0; 25 | text-align: left; 26 | } 27 | 28 | .header { 29 | display: inline-block; 30 | pointer-events: visible; 31 | position: relative; 32 | left: 130px; 33 | z-index: 101; 34 | } 35 | 36 | .footer { 37 | position: absolute; 38 | right: 5px; 39 | bottom: 0; 40 | } 41 | } 42 | 43 | .fsAbsolute { 44 | position: absolute; 45 | left: 0; 46 | right: 0; 47 | top: 0; 48 | bottom: 0; 49 | } 50 | 51 | .absoluteCenter { 52 | position: absolute; 53 | left: 50%; 54 | top: 50%; 55 | transform: translate(-50%, -50%); 56 | } 57 | 58 | .hidden { 59 | display: none; 60 | visibility: hidden; 61 | } 62 | 63 | .hideText { 64 | text-indent: -9999px; 65 | white-space: nowrap; 66 | } 67 | -------------------------------------------------------------------------------- /src/example/components/AppWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { IS_DEV, IS_EDITOR } from '../constants'; 3 | import Application from '../../core/Application'; 4 | import RemoteTheatre from '../../core/remote/RemoteTheatre'; 5 | import RemoteThree from '../../core/remote/RemoteThree'; 6 | import RemoteSetup from './RemoteSetup'; 7 | import CustomEditor from '../CustomEditor'; 8 | import Wrapper from './Wrapper'; 9 | 10 | export default function AppWrapper() { 11 | const [app, setApp] = useState(null); 12 | 13 | useEffect(() => { 14 | const instance = new Application(); 15 | instance.detectSettings(IS_DEV, IS_EDITOR).then(() => { 16 | // TheatreJS 17 | instance.addComponent('theatre', new RemoteTheatre(IS_DEV, IS_EDITOR)); 18 | 19 | // ThreeJS 20 | const three = new RemoteThree('Hermes Example', IS_DEV, IS_EDITOR); 21 | instance.addComponent('three', three); 22 | 23 | // Ready 24 | setApp(instance); 25 | }); 26 | }, []); 27 | 28 | return ( 29 | <> 30 | {app !== null && ( 31 | <> 32 | {app.debugEnabled && } 33 | {app.editor && } 34 | 35 | 36 | )} 37 | 38 | ); 39 | } -------------------------------------------------------------------------------- /types/webworkers/ProxyManager.d.ts: -------------------------------------------------------------------------------- 1 | import { EventDispatcher } from 'three'; 2 | interface SizeData { 3 | left: number; 4 | top: number; 5 | width: number; 6 | height: number; 7 | } 8 | interface EventData extends SizeData { 9 | type: string; 10 | id?: number; 11 | data?: any; 12 | preventDefault?: () => void; 13 | stopPropagation?: () => void; 14 | } 15 | export declare class ElementProxyReceiver extends EventDispatcher { 16 | style: Record; 17 | left: number; 18 | top: number; 19 | width: number; 20 | height: number; 21 | ownerDocument: any; 22 | constructor(); 23 | get clientWidth(): number; 24 | set clientWidth(value: number); 25 | get clientHeight(): number; 26 | set clientHeight(value: number); 27 | setPointerCapture(): void; 28 | releasePointerCapture(): void; 29 | getBoundingClientRect(): DOMRect; 30 | handleEvent(data: EventData): void; 31 | focus(): void; 32 | getRootNode(): any; 33 | } 34 | export declare class ProxyManager { 35 | targets: Record; 36 | constructor(); 37 | makeProxy(data: { 38 | id: number; 39 | }): void; 40 | getProxy(id: number): ElementProxyReceiver; 41 | handleEvent(data: { 42 | id: number; 43 | data: EventData; 44 | }): void; 45 | } 46 | export {}; 47 | -------------------------------------------------------------------------------- /types/editor/tools/splineEditor/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Camera, CatmullRomCurve3, Object3D, Vector3 } from 'three'; 2 | import Spline from './Spline'; 3 | import RemoteThree from '../../../core/remote/RemoteThree'; 4 | export type SplineJSON = { 5 | name: string; 6 | points: Array; 7 | tension: number; 8 | closed: boolean; 9 | subdivide: number; 10 | type: string; 11 | }; 12 | export default class SplineEditor extends Object3D { 13 | defaultScale: number; 14 | currentSpline: Spline | null; 15 | private _camera; 16 | private group; 17 | private three; 18 | private splineDataText; 19 | constructor(camera: Camera, three: RemoteThree); 20 | initDebug(): void; 21 | dispose(): void; 22 | addSpline(spline: Spline, visible: boolean): void; 23 | createSpline: (defaultPoints?: Array) => Spline; 24 | createSplineFromArray: (points: Array) => Spline; 25 | createSplineFromCatmullRom: (curve: CatmullRomCurve3) => Spline; 26 | createSplineFromJSON: (data: SplineJSON) => Spline; 27 | showPoints: (visible?: boolean) => void; 28 | private onAddSpline; 29 | private isMouseDown; 30 | private enableClickToDraw; 31 | private disableClickToDraw; 32 | private onClickCanvas; 33 | private onMouseMove; 34 | private onMouseUp; 35 | private mouseToSplinePos; 36 | get camera(): Camera; 37 | set camera(value: Camera); 38 | } 39 | -------------------------------------------------------------------------------- /src/editor/multiView/UVMaterial.ts: -------------------------------------------------------------------------------- 1 | import { ShaderMaterial } from 'three'; 2 | 3 | const vertex = `#include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | void main() { 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #if defined ( USE_SKINNING ) 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #endif 25 | 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | }`; 34 | 35 | const fragment = ` 36 | #include 37 | #include 38 | #include 39 | 40 | void main() { 41 | #include 42 | gl_FragColor = vec4(vec3(vUv, 0.0), 1.0); 43 | }`; 44 | 45 | export default class UVMaterial extends ShaderMaterial { 46 | constructor() { 47 | super({ 48 | defines: { 49 | USE_UV: '' 50 | }, 51 | vertexShader: vertex, 52 | fragmentShader: fragment, 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/example/three/scenes/BaseScene.ts: -------------------------------------------------------------------------------- 1 | import { Clock, Object3D, PerspectiveCamera, Scene } from 'three'; 2 | import Application from '../../../core/Application'; 3 | import RemoteTheatre from '../../../core/remote/RemoteTheatre'; 4 | 5 | export default class BaseScene extends Scene { 6 | app!: Application; 7 | camera: PerspectiveCamera; 8 | renderer?: any; 9 | clock: Clock; 10 | 11 | constructor() { 12 | super(); 13 | this.clock = new Clock(); 14 | this.clock.start(); 15 | 16 | const cameras = new Object3D(); 17 | cameras.name = 'cameras'; 18 | this.add(cameras); 19 | 20 | this.camera = new PerspectiveCamera(90, 1, 10, 1000); 21 | this.camera.name = 'SceneCamera'; 22 | this.camera.position.set(0, 100, 125); 23 | this.camera.lookAt(0, 50, 0); 24 | cameras.add(this.camera); 25 | } 26 | 27 | setup(app: Application, renderer?: any) { 28 | this.app = app; 29 | this.renderer = renderer; 30 | } 31 | 32 | init() { 33 | // 34 | } 35 | 36 | dispose() { 37 | const theatre = this.app.components.get('theatre') as RemoteTheatre; 38 | theatre.clearSheetObjects(this.name); 39 | } 40 | 41 | resize(width: number, height: number) { 42 | this.camera.aspect = width / height; 43 | this.camera.updateProjectionMatrix(); 44 | } 45 | 46 | update() { 47 | // 48 | } 49 | 50 | draw() { 51 | if (this.renderer.isWebGLRenderer) { 52 | this.renderer.render(this, this.camera); 53 | } else { 54 | this.renderer.renderAsync(this, this.camera); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/example/CustomEditor.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import Application from '../core/Application'; 3 | import RemoteThree from '../core/remote/RemoteThree'; 4 | // Views 5 | import ThreeEditor from '../editor/ThreeEditor'; 6 | import BaseScene from './three/scenes/BaseScene'; 7 | // Utils 8 | import MultiView from '../editor/multiView/MultiView'; 9 | import { customizeTheatreElements } from '../utils/theatre'; 10 | import Scene1 from './three/scenes/Scene1'; 11 | import Scene2 from './three/scenes/Scene2'; 12 | import RTTScene from './three/scenes/RTTScene'; 13 | 14 | type CustomEditorProps = { 15 | app: Application 16 | } 17 | 18 | export default function CustomEditor(props: CustomEditorProps) { 19 | useEffect(() => { 20 | customizeTheatreElements(); 21 | }, []); 22 | 23 | // Pass in references to all your scenes so the Multiview can instantiate them 24 | const three = props.app.components.get('three') as RemoteThree; 25 | const scenes: Map = new Map(); 26 | scenes.set('Scene1', Scene1); 27 | scenes.set('Scene2', Scene2); 28 | scenes.set('RTTScene', RTTScene); 29 | 30 | return ( 31 | { 35 | scene.setup(props.app, MultiView.instance?.renderer); 36 | scene.init(); 37 | }} 38 | onSceneUpdate={(scene: any) => { 39 | // Custom callback for animation updates 40 | const baseScene = scene as BaseScene; 41 | baseScene.update(); 42 | }} 43 | /> 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /types/core/types.d.ts: -------------------------------------------------------------------------------- 1 | import { InspectorFieldType } from '../editor/sidePanel/inspector/InspectorField'; 2 | export interface BroadcastData { 3 | target: ApplicationMode; 4 | event: EditorEvent; 5 | data?: any; 6 | } 7 | export type OptionInfo = { 8 | title: string; 9 | value: any; 10 | }; 11 | export interface GroupItemData { 12 | type: InspectorFieldType; 13 | prop: string; 14 | title?: string; 15 | value?: any; 16 | min?: number; 17 | max?: number; 18 | step?: number; 19 | disabled?: boolean; 20 | options?: OptionInfo[]; 21 | } 22 | export interface GroupData { 23 | title: string; 24 | expanded?: boolean; 25 | items: GroupItemData[]; 26 | onUpdate: (prop: string, value: any) => void; 27 | } 28 | export interface GroupCallback { 29 | title: string; 30 | onUpdate: (prop: string, value: any) => void; 31 | } 32 | export type ApplicationMode = 'app' | 'editor'; 33 | export type VoidCallback = () => void; 34 | export type DataUpdateCallback = (data: any) => void; 35 | export type EditorEvent = 'custom' | 'setSheet' | 'setSheetObject' | 'updateSheetObject' | 'updateTimeline' | 'playSheet' | 'pauseSheet' | 'updateObject' | 'addScene' | 'refreshScene' | 'removeScene' | 'setScene' | 'createTexture' | 'addCamera' | 'removeCamera' | 'addSpline' | 'addRenderer' | 'updateRenderer' | 'requestSize' | 'addFolder' | 'bindObject' | 'updateBind' | 'addButton' | 'clickButton' | 'addGroup' | 'removeGroup' | 'updateGroup'; 36 | export type VoidFunc = () => void; 37 | export type BroadcastCallback = (data: BroadcastData) => void; 38 | export type TheatreUpdateCallback = (data: any) => void; 39 | export declare const noop: () => void; 40 | export declare const defaultTheatreCallback: TheatreUpdateCallback; 41 | -------------------------------------------------------------------------------- /types/core/remote/RemoteTheatre.d.ts: -------------------------------------------------------------------------------- 1 | import { IProject, ISheet, ISheetObject } from '@theatre/core'; 2 | import BaseRemote from './BaseRemote'; 3 | import { BroadcastData, DataUpdateCallback, VoidCallback } from '../types'; 4 | type KeyframeVector = { 5 | position: number; 6 | x: number; 7 | y: number; 8 | z: number; 9 | }; 10 | export default class RemoteTheatre extends BaseRemote { 11 | project: IProject | undefined; 12 | sheets: Map; 13 | sheetObjects: Map; 14 | sheetObjectCBs: Map; 15 | sheetObjectUnsubscribe: Map; 16 | activeSheet: ISheet | undefined; 17 | studio: any; 18 | rafDriver?: any; 19 | constructor(debug?: boolean, editor?: boolean); 20 | dispose(): void; 21 | loadProject(id: string, createRaf: boolean, json?: any): Promise; 22 | getSheetInstance(name: string, instanceId?: string): string; 23 | sheet(name: string, instanceId?: string): ISheet | undefined; 24 | playSheet(name: string, params?: any, instanceId?: string): Promise; 25 | pauseSheet(name: string, instanceId?: string): void; 26 | clearSheetObjects(sheetName: string): void; 27 | sheetObject(sheetName: string, key: string, props: any, onUpdate?: DataUpdateCallback, instanceId?: string): ISheetObject | undefined; 28 | getSheetObjectKeyframes(sheetName: string, sheetObject: string, prop: string): any[]; 29 | getSheetObjectVectors(sheetName: string, sheetObject: string): KeyframeVector[]; 30 | update(now: number): void; 31 | unsubscribe(sheetObject: ISheetObject): undefined; 32 | protected handleApp(msg: BroadcastData): void; 33 | protected handleEditor(msg: BroadcastData): void; 34 | getSheetNames(): string[]; 35 | handleEditorApp(): void; 36 | } 37 | export {}; 38 | -------------------------------------------------------------------------------- /src/core/remote/BaseRemote.ts: -------------------------------------------------------------------------------- 1 | import type { BroadcastData } from '../types'; 2 | 3 | /** 4 | * Base class for remote-related extensions 5 | */ 6 | export default class BaseRemote { 7 | name: string; 8 | protected _debug = false; 9 | protected _editor = false; 10 | protected broadcastChannel?: BroadcastChannel; 11 | 12 | constructor(name: string, debug = false, editor = false) { 13 | this.name = name; 14 | this._debug = debug; 15 | this._editor = editor; 16 | 17 | if (!debug) return; 18 | this.broadcastChannel = new BroadcastChannel(name); 19 | this.broadcastChannel.addEventListener('message', this.messageHandler.bind(this)); 20 | } 21 | 22 | dispose() { 23 | this.broadcastChannel?.removeEventListener('message', this.messageHandler.bind(this)); 24 | this.broadcastChannel?.close(); 25 | } 26 | 27 | get debug(): boolean { 28 | return this._debug; 29 | } 30 | 31 | get editor(): boolean { 32 | return this._editor; 33 | } 34 | 35 | // Broadcast 36 | 37 | protected send(data: BroadcastData) { 38 | const send = (this.editor && data.target === 'app') || (!this.editor && data.target === 'editor'); 39 | if (send) { 40 | try { 41 | this.broadcastChannel?.postMessage(data); 42 | } catch (err: any) { 43 | console.log('Hermes - Error sending message:'); 44 | console.log(err); 45 | console.log(data); 46 | } 47 | } 48 | } 49 | 50 | protected messageHandler(evt: MessageEvent) { 51 | const data: BroadcastData = evt.data; 52 | if (data.target === 'app') { 53 | this.handleApp(data); 54 | } else { 55 | this.handleEditor(data); 56 | } 57 | } 58 | 59 | protected handleApp(msg: BroadcastData) { 60 | // 61 | } 62 | 63 | protected handleEditor(msg: BroadcastData) { 64 | // 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/editor/scss/dropdown.scss: -------------------------------------------------------------------------------- 1 | @use './theme'; 2 | 3 | .editor .header .dropdown { 4 | color: #FFF; 5 | display: inline-block; 6 | margin-right: 1px; 7 | text-align: left; 8 | height: fit-content; 9 | min-width: auto; 10 | width: max-content; 11 | 12 | button { 13 | backdrop-filter: blur(2px); 14 | background-color: theme.$BAR_COLOR; 15 | border: none; 16 | color: rgba($color: #FFF, $alpha: 0.5); 17 | font-size: 12px; 18 | padding: 5px 10px; 19 | position: relative; 20 | text-align: left; 21 | min-width: theme.$ROW_HEIGHT; 22 | width: 100%; 23 | height: theme.$BTN_SIZE; 24 | transition: all 0.2s linear; 25 | 26 | &:hover { 27 | background-color: theme.$BTN_HOVER; 28 | color: #FFF; 29 | } 30 | 31 | &.svg { 32 | line-height: 0; 33 | width: theme.$BTN_SIZE; 34 | } 35 | } 36 | 37 | p { 38 | background-color: theme.$BAR_COLOR; 39 | display: inline-block; 40 | height: theme.$ROW_HEIGHT; 41 | margin: 0; 42 | min-width: theme.$ROW_HEIGHT; 43 | padding: 5px; 44 | } 45 | 46 | svg { 47 | position: relative; 48 | left: 50%; 49 | transform: translate(-50%, 0); 50 | } 51 | 52 | ul { 53 | list-style: none; 54 | margin: 0; 55 | margin-block: 0; 56 | padding-inline: 0; 57 | position: absolute; 58 | width: max-content; 59 | 60 | li { 61 | border-top: 1px solid theme.$BORDER_COLOR; 62 | display: block; 63 | position: relative; 64 | 65 | &.selected button { 66 | background-color: theme.$BTN_SELECTED; 67 | 68 | &:hover { 69 | background-color: theme.$BTN_SELECTED_HOVER; 70 | } 71 | } 72 | } 73 | } 74 | 75 | &.subdropdown { 76 | min-width: 100%; 77 | 78 | ul { 79 | border-left: 1px solid theme.$BORDER_COLOR; 80 | left: 100%; 81 | top: -1px; 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /src/editor/components/DropdownItem.tsx: -------------------------------------------------------------------------------- 1 | // Libs 2 | import { useState } from 'react'; 3 | // Components 4 | import Draggable from './Draggable'; 5 | import Dropdown from './Dropdown'; 6 | import { DropdownItemProps, DropdownOption } from './types'; 7 | // Utils 8 | import { randomID } from '../utils'; 9 | 10 | export default function DropdownItem(props: DropdownItemProps) { 11 | const { option } = props; 12 | const [selected, setSelected] = useState(''); 13 | 14 | let element; 15 | switch (option.type) { 16 | case 'draggable': 17 | element = ( 18 | } 21 | onDragComplete={(options: string[]) => { 22 | if (option.onDragComplete !== undefined) option.onDragComplete(options); 23 | }} 24 | subdropdown={true} 25 | /> 26 | ); 27 | break; 28 | case 'dropdown': 29 | element = ( 30 | } 33 | onSelect={option.onSelect} 34 | subdropdown={true} 35 | /> 36 | ); 37 | break; 38 | case 'option': 39 | element = ( 40 | 55 | ); 56 | break; 57 | } 58 | 59 | return ( 60 |
  • 61 | {element} 62 |
  • 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/core/types.ts: -------------------------------------------------------------------------------- 1 | // Interfaces 2 | 3 | import { InspectorFieldType } from '../editor/sidePanel/inspector/InspectorField'; 4 | 5 | export interface BroadcastData { 6 | target: ApplicationMode 7 | event: EditorEvent 8 | data?: any 9 | } 10 | 11 | export type OptionInfo = { 12 | title: string 13 | value: any 14 | } 15 | 16 | export interface GroupItemData { 17 | type: InspectorFieldType 18 | prop: string 19 | title?: string 20 | value?: any 21 | min?: number 22 | max?: number 23 | step?: number 24 | disabled?: boolean 25 | options?: OptionInfo[] 26 | } 27 | 28 | export interface GroupData { 29 | title: string 30 | expanded?: boolean 31 | items: GroupItemData[] 32 | onUpdate: (prop: string, value: any) => void 33 | } 34 | 35 | export interface GroupCallback { 36 | title: string 37 | onUpdate: (prop: string, value: any) => void 38 | } 39 | 40 | // Types 41 | 42 | export type ApplicationMode = 'app' | 'editor' 43 | 44 | export type VoidCallback = () => void 45 | 46 | export type DataUpdateCallback = (data: any) => void 47 | 48 | export type EditorEvent = 49 | | 'custom' 50 | // Theatre 51 | | 'setSheet' 52 | | 'setSheetObject' 53 | | 'updateSheetObject' 54 | | 'updateTimeline' 55 | | 'playSheet' 56 | | 'pauseSheet' 57 | // Three 58 | | 'updateObject' 59 | | 'addScene' 60 | | 'refreshScene' 61 | | 'removeScene' 62 | | 'setScene' 63 | | 'createTexture' 64 | | 'addCamera' 65 | | 'removeCamera' 66 | | 'addSpline' 67 | | 'addRenderer' 68 | | 'updateRenderer' 69 | | 'requestSize' 70 | // GUI 71 | | 'addFolder' 72 | | 'bindObject' 73 | | 'updateBind' 74 | | 'addButton' 75 | | 'clickButton' 76 | // Groups 77 | | 'addGroup' 78 | | 'removeGroup' 79 | | 'updateGroup' 80 | 81 | export type VoidFunc = () => void 82 | 83 | export type BroadcastCallback = (data: BroadcastData) => void 84 | 85 | export type TheatreUpdateCallback = (data: any) => void 86 | 87 | // Consts 88 | 89 | export const noop = () => {}; 90 | 91 | export const defaultTheatreCallback: TheatreUpdateCallback = () => {}; 92 | 93 | -------------------------------------------------------------------------------- /types/editor/tools/splineEditor/Spline.d.ts: -------------------------------------------------------------------------------- 1 | import { Camera, CatmullRomCurve3, Line, Mesh, Object3D, Vector3 } from 'three'; 2 | import InspectorGroup from '../../sidePanel/inspector/InspectorGroup'; 3 | export type CurveType = 'catmullrom' | 'centripetal' | 'chordal'; 4 | export default class Spline extends Object3D { 5 | curve: CatmullRomCurve3; 6 | line: Line; 7 | draggable: Object3D; 8 | curvePos: Mesh; 9 | tension: number; 10 | closed: boolean; 11 | subdivide: number; 12 | curveType: CurveType; 13 | offset: number; 14 | private lineMaterial; 15 | private _camera; 16 | private _curvePercentage; 17 | private _draggableScale; 18 | private _transform?; 19 | private raycaster; 20 | private draggedMat; 21 | private parentGroup; 22 | private group; 23 | constructor(name: string, camera: Camera); 24 | enable(): void; 25 | disable(): void; 26 | dispose: () => void; 27 | hideTransform: () => void; 28 | exportSpline: () => void; 29 | showPoints: (visible?: boolean) => void; 30 | addPoints: (pts?: Array) => void; 31 | addPoint: (position: Vector3, update?: boolean) => Mesh; 32 | addNextPt: () => void; 33 | removePoint: (child: Object3D) => void; 34 | removePointAt: (index: number) => void; 35 | removeSelectedPt: () => void; 36 | updateLastPoint(value: Vector3): void; 37 | updateSpline: () => void; 38 | updateField(position: Vector3): void; 39 | private onMouseClick; 40 | getPointAt(percentage: number): Vector3; 41 | getTangentAt(percentage: number): Vector3; 42 | get points(): Array; 43 | get total(): number; 44 | get draggableScale(): number; 45 | set draggableScale(value: number); 46 | get camera(): Camera; 47 | set camera(value: Camera); 48 | get curvePercentage(): number; 49 | set curvePercentage(value: number); 50 | private updateCurrentPoint; 51 | private onUpdateTransform; 52 | initDebug(parentGroup: InspectorGroup, visible: boolean): void; 53 | private debugPoint; 54 | } 55 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './core/types'; 2 | export * from './editor/utils'; 3 | export * from './utils/detectSettings'; 4 | export * from './utils/math'; 5 | export * from './utils/theatre'; 6 | export * from './utils/three'; 7 | export * from './utils/post'; 8 | export * from './webworkers/types'; 9 | export * from './webworkers/EventHandling'; 10 | export * from './webworkers/ProxyManager'; 11 | export { default as Application } from './core/Application'; 12 | export { default as BaseRemote } from './core/remote/BaseRemote'; 13 | export { default as RemoteTheatre } from './core/remote/RemoteTheatre'; 14 | export { default as RemoteThree } from './core/remote/RemoteThree'; 15 | export { default as NavButton } from './editor/components/NavButton'; 16 | export { default as DraggableItem } from './editor/components/DraggableItem'; 17 | export { default as Draggable } from './editor/components/Draggable'; 18 | export { default as DropdownItem } from './editor/components/DropdownItem'; 19 | export { default as Dropdown } from './editor/components/Dropdown'; 20 | export { default as SidePanel } from './editor/sidePanel/SidePanel'; 21 | export { default as Accordion } from './editor/sidePanel/Accordion'; 22 | export { default as ChildObject } from './editor/sidePanel/ChildObject'; 23 | export { default as ContainerObject } from './editor/sidePanel/ContainerObject'; 24 | export { default as Inspector } from './editor/sidePanel/inspector/Inspector'; 25 | export { default as MultiView } from './editor/multiView/MultiView'; 26 | export { default as Editor } from './editor/Editor'; 27 | export { default as ThreeEditor } from './editor/ThreeEditor'; 28 | export { default as Transform } from './editor/tools/Transform'; 29 | export { default as Spline } from './editor/tools/splineEditor/Spline'; 30 | export { default as SplineEditor } from './editor/tools/splineEditor'; 31 | export { default as InfiniteGridMaterial } from './editor/multiView/InfiniteGridMaterial'; 32 | export { default as InfiniteGridHelper } from './editor/multiView/InfiniteGridHelper'; 33 | export { default as UVMaterial } from './editor/multiView/UVMaterial'; 34 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Core 2 | export * from './core/types'; 3 | export * from './editor/utils'; 4 | export * from './utils/detectSettings'; 5 | export * from './utils/math'; 6 | export * from './utils/theatre'; 7 | export * from './utils/three'; 8 | export * from './utils/post'; 9 | export * from './webworkers/types'; 10 | export * from './webworkers/EventHandling'; 11 | export * from './webworkers/ProxyManager'; 12 | export { default as Application } from './core/Application'; 13 | export { default as BaseRemote } from './core/remote/BaseRemote'; 14 | export { default as RemoteTheatre } from './core/remote/RemoteTheatre'; 15 | export { default as RemoteThree } from './core/remote/RemoteThree'; 16 | // Components 17 | export { default as NavButton } from './editor/components/NavButton'; 18 | export { default as DraggableItem } from './editor/components/DraggableItem'; 19 | export { default as Draggable } from './editor/components/Draggable'; 20 | export { default as DropdownItem } from './editor/components/DropdownItem'; 21 | export { default as Dropdown } from './editor/components/Dropdown'; 22 | // RemoteThree 23 | export { default as SidePanel } from './editor/sidePanel/SidePanel'; 24 | export { default as Accordion } from './editor/sidePanel/Accordion'; 25 | export { default as ChildObject } from './editor/sidePanel/ChildObject'; 26 | export { default as ContainerObject } from './editor/sidePanel/ContainerObject'; 27 | export { default as Inspector } from './editor/sidePanel/inspector/Inspector'; 28 | export { default as MultiView } from './editor/multiView/MultiView'; 29 | export { default as Editor } from './editor/Editor'; 30 | export { default as ThreeEditor } from './editor/ThreeEditor'; 31 | export { default as Transform } from './editor/tools/Transform'; 32 | export { default as Spline } from './editor/tools/splineEditor/Spline'; 33 | export { default as SplineEditor } from './editor/tools/splineEditor'; 34 | export { default as InfiniteGridMaterial } from './editor/multiView/InfiniteGridMaterial'; 35 | export { default as InfiniteGridHelper } from './editor/multiView/InfiniteGridHelper'; 36 | export { default as UVMaterial } from './editor/multiView/UVMaterial'; -------------------------------------------------------------------------------- /src/editor/components/Draggable.tsx: -------------------------------------------------------------------------------- 1 | // Libs 2 | import { useState } from 'react'; 3 | // Components 4 | import NavButton from './NavButton'; 5 | import DraggableItem from './DraggableItem'; 6 | import { DraggableProps } from './types'; 7 | 8 | export default function Draggable(props: DraggableProps) { 9 | const [expanded, setExpanded] = useState(false); 10 | const [list, setList] = useState(props.options); 11 | const [draggingIndex, setDraggingIndex] = useState(null); 12 | 13 | const updateList = (updated: string[]) => { 14 | props.onDragComplete(updated); 15 | setList(updated); 16 | }; 17 | 18 | const onDelete = (index: number) => { 19 | const newArray = [...list]; 20 | newArray.splice(index, 1); 21 | updateList(newArray); 22 | }; 23 | 24 | const onDragStart = (index: number) => { 25 | setDraggingIndex(index); 26 | }; 27 | 28 | const onDragOver = (index: number) => { 29 | if (draggingIndex === index || draggingIndex === null) return; 30 | 31 | const updatedItems = [...list]; 32 | const draggedItem = updatedItems.splice(draggingIndex, 1)[0]; 33 | updatedItems.splice(index, 0, draggedItem); 34 | 35 | setDraggingIndex(index); 36 | setList(updatedItems); 37 | }; 38 | 39 | const onDragEnd = () => { 40 | props.onDragComplete(list); 41 | setDraggingIndex(null); 42 | }; 43 | 44 | let ddClassName = 'dropdown draggable'; 45 | if (props.subdropdown) ddClassName += ' subdropdown'; 46 | 47 | return ( 48 |
    setExpanded(true)} onMouseLeave={() => setExpanded(false)}> 49 | 50 |
      51 | {list.map((item: string, index: number) => ( 52 | 62 | ))} 63 |
    64 |
    65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /src/editor/sidePanel/inspector/InspectGrid4.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo, useRef } from 'react'; 2 | import { Matrix4, Vector4 } from 'three'; 3 | import InspectNumber from './InspectNumber'; 4 | 5 | interface InspectGrid4Props { 6 | value: Vector4 | Matrix4; 7 | step?: number; 8 | onChange: (evt: any) => void; 9 | } 10 | 11 | export default function InspectGrid4(props: InspectGrid4Props) { 12 | const isVector = props.value['x'] !== undefined; 13 | const step = props.step !== undefined ? props.step : 0.01; 14 | const children: any[] = []; 15 | 16 | if (isVector) { 17 | const vector = useMemo(() => props.value as Vector4, []); 18 | const onChange = (prop: string, value: number) => { 19 | vector[prop] = value; 20 | props.onChange({ target: { value: vector } }); 21 | }; 22 | 23 | const params = ['x', 'y', 'z', 'w']; 24 | params.forEach((param: string) => { 25 | const labelRef = useRef(null); 26 | children.push( 27 |
    28 | 29 | 37 |
    38 | ); 39 | }); 40 | } else { 41 | const matrix = useMemo(() => props.value as Matrix4, []); 42 | const onChange = (prop: string, value: number) => { 43 | const index = Number(prop); 44 | matrix.elements[index] = value; 45 | props.onChange({ target: { value: matrix } }); 46 | }; 47 | 48 | for (let i = 0; i < 16; i++) { 49 | const labelRef = useRef(null); 50 | children.push( 51 |
    52 | {i + 1} 53 | 61 |
    62 | ); 63 | } 64 | } 65 | 66 | return ( 67 |
    {children}
    68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /src/editor/sidePanel/Accordion.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { capitalize } from '../../editor/utils'; 3 | import RemoteThree, { ToolEvents } from '../../core/remote/RemoteThree'; 4 | 5 | type AccordionProps = { 6 | three: RemoteThree; 7 | label: string 8 | scene?: any 9 | button?: JSX.Element 10 | children?: JSX.Element | JSX.Element[] 11 | open?: boolean 12 | visible?: boolean 13 | onToggle?: (value: boolean) => void 14 | onRefresh?: () => void 15 | } 16 | 17 | export default function Accordion(props: AccordionProps) { 18 | const [open, setOpen] = useState(props.open !== undefined ? props.open : false); 19 | const [visible, setVisible] = useState(props.visible !== undefined ? props.visible : false); 20 | const hide = !open || props.children === undefined; 21 | 22 | const onRemove = () => { 23 | props.three.dispatchEvent({ type: ToolEvents.REMOVE_SCENE, value: props.scene }); 24 | }; 25 | 26 | return ( 27 |
    28 | 43 | {props.onRefresh ? ( 44 | <> 45 | 60 | 61 | 62 | 63 | ) : null} 64 | {props.button} 65 |
    66 |
    67 | {props.children} 68 |
    69 |
    70 |
    71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/utils/detectSettings.ts: -------------------------------------------------------------------------------- 1 | import { getGPUTier, TierResult } from 'detect-gpu'; 2 | 3 | export type QualityType = 'High' | 'Medium' | 'Low' 4 | 5 | export type AppSettings = { 6 | dpr: number; 7 | fps: number; 8 | width: number; 9 | height: number; 10 | mobile: boolean; 11 | supportOffScreenCanvas: boolean; 12 | supportWebGPU: boolean; 13 | quality: QualityType; 14 | dev: boolean; 15 | editor: boolean; 16 | } 17 | 18 | export function detectMaxFrameRate(callback: (fps: number) => void) { 19 | let frameCount = 0; 20 | const startTime = performance.now(); 21 | 22 | function measureFrameRate() { 23 | frameCount++; 24 | const currentTime = performance.now(); 25 | if (currentTime - startTime >= 100) { 26 | const frameRate = frameCount / ((currentTime - startTime) / 1000); 27 | const roundedFPS = Math.round(frameRate / 30) * 30; 28 | callback(roundedFPS); 29 | } else { 30 | requestAnimationFrame(measureFrameRate); 31 | } 32 | } 33 | 34 | requestAnimationFrame(measureFrameRate); 35 | } 36 | 37 | export function detectSettings(dev: boolean = false, editor: boolean = false): Promise { 38 | return new Promise((resolve) => { 39 | getGPUTier().then((gpuTier: TierResult) => { 40 | let supportOffScreenWebGL = false; 41 | const canvas = document.createElement('canvas'); 42 | const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); 43 | supportOffScreenWebGL = 'transferControlToOffscreen' in canvas; 44 | 45 | // If it's Safari, then check the version because Safari < 17 doesn't support OffscreenCanvas with a WebGL context. 46 | if (isSafari) { 47 | const versionMatch = navigator.userAgent.match(/version\/(\d+)/i); 48 | const safariVersion = versionMatch ? parseInt(versionMatch[1]) : 0; 49 | supportOffScreenWebGL = safariVersion >= 17; 50 | } 51 | 52 | const settings: AppSettings = { 53 | dpr: devicePixelRatio, 54 | fps: 30, 55 | width: innerWidth, 56 | height: innerHeight, 57 | mobile: gpuTier.isMobile !== undefined ? gpuTier.isMobile : false, 58 | supportOffScreenCanvas: supportOffScreenWebGL, 59 | supportWebGPU: !!navigator.gpu, 60 | quality: 'Low', 61 | dev, 62 | editor, 63 | }; 64 | if (gpuTier.tier === 3) settings.quality = 'High'; 65 | else if (gpuTier.tier === 2) settings.quality = 'Medium'; 66 | 67 | detectMaxFrameRate((fps: number) => { 68 | settings.fps = fps; 69 | resolve(settings); 70 | }); 71 | }); 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /types/utils/three.d.ts: -------------------------------------------------------------------------------- 1 | import { AnimationClip, AnimationMixer, BufferGeometry, Camera, Material, Mesh, Object3D, Object3DEventMap, OrthographicCamera, Texture, WebGLRenderer, WebGLRenderTarget } from 'three'; 2 | import { ModelLite } from '../webworkers/types'; 3 | export declare const triangle: BufferGeometry; 4 | export declare const orthoCamera: OrthographicCamera; 5 | export declare const disposeTexture: (texture?: Texture) => void; 6 | export declare const disposeMaterial: (material?: Material | Material[]) => void; 7 | export declare const dispose: (object: Object3D) => void; 8 | export declare let totalThreeObjects: number; 9 | export declare const resetThreeObjects: () => void; 10 | export declare const hierarchyUUID: (object: Object3D) => void; 11 | export declare class ExportTexture { 12 | static renderer: WebGLRenderer; 13 | private static canvas; 14 | private static context; 15 | private static scene; 16 | private static camera; 17 | private static material; 18 | private static inited; 19 | private static width; 20 | private static height; 21 | private static init; 22 | static renderToBlob(texture: Texture): string; 23 | private static renderToCanvas; 24 | } 25 | export type ParsedModel = { 26 | animations: AnimationClip[]; 27 | cameras: Object3D[]; 28 | model: Object3D; 29 | mixer: AnimationMixer; 30 | }; 31 | export declare function parseModelLite(model: ModelLite): Promise; 32 | export declare const renderToTexture: (renderer: WebGLRenderer, scene: Object3D, camera: Camera, target: WebGLRenderTarget) => void; 33 | export declare function anchorGeometry(geometry: BufferGeometry, x: number, y: number, z: number): void; 34 | export declare function anchorGeometryTL(geometry: BufferGeometry): void; 35 | export declare function updateCameraOrtho(camera: OrthographicCamera, width: number, height: number): void; 36 | export declare function updateCameraOrtho16x9(camera: OrthographicCamera, width: number, height: number): void; 37 | export declare function supportsOffscreenCanvas(): boolean; 38 | export declare function createMask(mesh: Mesh, id: number, colorWrite?: boolean, depthWrite?: boolean): void; 39 | export declare function useMask(mesh: Mesh, id: number, inverse?: boolean): void; 40 | export declare function setMaterialBlendNormal(material: Material): void; 41 | export declare function setMaterialBlendAdd(material: Material): void; 42 | export declare function setMaterialBlendMultiply(material: Material): void; 43 | export declare function setMaterialBlendScreen(material: Material): void; 44 | -------------------------------------------------------------------------------- /src/core/Application.ts: -------------------------------------------------------------------------------- 1 | import BaseRemote from './remote/BaseRemote'; 2 | import { AppSettings, detectSettings } from '../utils/detectSettings'; 3 | 4 | export default class Application { 5 | assets = { 6 | audio: new Map(), 7 | image: new Map(), 8 | json: new Map(), 9 | model: new Map(), 10 | video: new Map(), 11 | }; 12 | components: Map = new Map(); 13 | settings: AppSettings = { 14 | dpr: 1, 15 | fps: 30, 16 | width: 0, 17 | height: 0, 18 | mobile: false, 19 | supportOffScreenCanvas: false, 20 | supportWebGPU: false, 21 | quality: 'Low', 22 | dev: false, 23 | editor: false, 24 | }; 25 | onUpdateCallback?: () => void; 26 | 27 | // Protected 28 | protected playing = false; 29 | protected rafID = -1; 30 | 31 | dispose() { 32 | this.pause(); 33 | this.components.forEach((value: BaseRemote) => value.dispose()); 34 | this.components.clear(); 35 | } 36 | 37 | detectSettings(dev: boolean = false, editor: boolean = false): Promise { 38 | return new Promise((resolve) => { 39 | detectSettings(dev, editor).then((settings: AppSettings) => { 40 | this.settings = settings; 41 | resolve(); 42 | }); 43 | }); 44 | } 45 | 46 | // Playback 47 | 48 | update() { 49 | // 50 | } 51 | 52 | draw() { 53 | // 54 | } 55 | 56 | play = () => { 57 | if (this.playing) return; 58 | this.playing = true; 59 | this.onUpdate(); 60 | }; 61 | 62 | pause = () => { 63 | if (!this.playing) return; 64 | this.playing = false; 65 | cancelAnimationFrame(this.rafID); 66 | this.rafID = -1; 67 | }; 68 | 69 | private onUpdate = () => { 70 | this.update(); 71 | if (this.isApp) this.draw(); 72 | if (this.onUpdateCallback) this.onUpdateCallback(); 73 | this.rafID = requestAnimationFrame(this.onUpdate); 74 | }; 75 | 76 | // Remote Components 77 | 78 | addComponent(name: string, component: BaseRemote) { 79 | this.components.set(name, component); 80 | } 81 | 82 | // Getters 83 | 84 | get debugEnabled(): boolean { 85 | return this.settings.dev; 86 | } 87 | 88 | get isApp(): boolean { 89 | return !this.editor; 90 | } 91 | 92 | set isApp(value: boolean) { 93 | this.editor = !value; 94 | } 95 | 96 | get editor(): boolean { 97 | return this.settings.editor; 98 | } 99 | 100 | set editor(value: boolean) { 101 | this.settings.editor = value; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tomorrowevening/hermes", 3 | "author": "Colin Duffy ", 4 | "description": "An extendable set of Web Tools controlled via a separate window for non-intereference with content.", 5 | "license": "GPL-3.0-or-later", 6 | "main": "./dist/hermes.cjs.js", 7 | "module": "./dist/hermes.esm.js", 8 | "types": "./types/index.d.ts", 9 | "type": "module", 10 | "version": "0.1.18", 11 | "homepage": "https://github.com/tomorrowevening/hermes#readme", 12 | "bugs": { 13 | "url": "https://github.com/tomorrowevening/hermes/issues" 14 | }, 15 | "keywords": [ 16 | "Editor", 17 | "Remote", 18 | "TheatreJS", 19 | "ThreeJS" 20 | ], 21 | "files": [ 22 | "dist/hermes.es.js", 23 | "dist/hermes.cjs.js", 24 | "dist/hermes.css", 25 | "types/**/*.d.ts" 26 | ], 27 | "exports": { 28 | ".": { 29 | "types": "./types/index.d.ts", 30 | "import": "./dist/hermes.es.js", 31 | "require": "./dist/hermes.cjs.js" 32 | }, 33 | "./hermes.css": "./dist/hermes.css" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "git+https://github.com/tomorrowevening/hermes.git" 38 | }, 39 | "scripts": { 40 | "server": "node server/index.mjs", 41 | "clean": "rm -r dist && rm -r types", 42 | "dev": "vite", 43 | "declare": "tsc --declaration --emitDeclarationOnly --declarationDir types", 44 | "buildLib": "yarn declare && vite build", 45 | "buildExample": "vite build --config vite.config.example.ts", 46 | "build": "yarn buildLib && yarn buildExample", 47 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 48 | "preview": "vite preview" 49 | }, 50 | "devDependencies": { 51 | "@theatre/core": "^0.7.2", 52 | "@theatre/studio": "^0.7.2", 53 | "@types/react": "^18.2.15", 54 | "@types/react-dom": "^18.2.7", 55 | "@types/three": "^0.177.0", 56 | "@typescript-eslint/eslint-plugin": "^6.4.0", 57 | "@typescript-eslint/parser": "^6.4.0", 58 | "@vitejs/plugin-react": "^4.0.3", 59 | "camera-controls": "^2.9.0", 60 | "detect-gpu": "^5.0.70", 61 | "eslint": "^8.45.0", 62 | "eslint-plugin-react": "^7.33.2", 63 | "eslint-plugin-react-hooks": "^4.6.0", 64 | "eslint-plugin-react-refresh": "^0.4.3", 65 | "glslify": "^7.1.1", 66 | "glslify-loader": "^2.0.0", 67 | "path": "^0.12.7", 68 | "postprocessing": "^6.37.6", 69 | "react": "^18.2.0", 70 | "react-dom": "^18.2.0", 71 | "sass": "^1.89.2", 72 | "stats-gl": "^3.6.0", 73 | "three": "^0.177.0", 74 | "typescript": "^5.0.2", 75 | "vite": "^6.3.5", 76 | "vite-plugin-glsl": "1.2.1", 77 | "ws": "^8.16.0" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/webworkers/ProxyManager.ts: -------------------------------------------------------------------------------- 1 | import { EventDispatcher } from 'three'; 2 | import { noop } from '../core/types'; 3 | 4 | /** 5 | * Code referenced from: 6 | * https://threejs.org/manual/#en/offscreencanvas 7 | */ 8 | 9 | interface SizeData { 10 | left: number; 11 | top: number; 12 | width: number; 13 | height: number; 14 | } 15 | 16 | interface EventData extends SizeData { 17 | type: string; 18 | id?: number; 19 | data?: any; 20 | preventDefault?: () => void; 21 | stopPropagation?: () => void; 22 | } 23 | 24 | export class ElementProxyReceiver extends EventDispatcher { 25 | style: Record = {}; 26 | left: number = 0; 27 | top: number = 0; 28 | width: number = 0; 29 | height: number = 0; 30 | ownerDocument: any = undefined; 31 | 32 | constructor() { 33 | super(); 34 | this.ownerDocument = this; 35 | } 36 | 37 | get clientWidth(): number { 38 | return this.width; 39 | } 40 | 41 | set clientWidth(value: number) { 42 | this.width = value; 43 | } 44 | 45 | get clientHeight(): number { 46 | return this.height; 47 | } 48 | 49 | set clientHeight(value: number) { 50 | this.height = value; 51 | } 52 | 53 | // OrbitControls call these as of r132. Implementing as no-ops 54 | setPointerCapture(): void {} 55 | releasePointerCapture(): void {} 56 | 57 | getBoundingClientRect(): DOMRect { 58 | return { 59 | x: this.left, 60 | y: this.top, 61 | left: this.left, 62 | top: this.top, 63 | width: this.width, 64 | height: this.height, 65 | right: this.left + this.width, 66 | bottom: this.top + this.height, 67 | toJSON: () => ({}) // Satisfies the DOMRect interface 68 | }; 69 | } 70 | 71 | handleEvent(data: EventData): void { 72 | if (data.type === 'size') { 73 | this.left = data.left; 74 | this.top = data.top; 75 | this.width = data.width; 76 | this.height = data.height; 77 | return; 78 | } 79 | 80 | // Extend event data to include preventDefault and stopPropagation as no-ops 81 | data.preventDefault = noop; 82 | data.stopPropagation = noop; 83 | // @ts-ignore 84 | this.dispatchEvent(data); 85 | } 86 | 87 | focus(): void { 88 | // No-op 89 | } 90 | 91 | getRootNode(): any { 92 | return this; 93 | } 94 | } 95 | 96 | export class ProxyManager { 97 | targets: Record = {}; 98 | 99 | constructor() { 100 | this.handleEvent = this.handleEvent.bind(this); 101 | } 102 | 103 | makeProxy(data: { id: number }): void { 104 | const { id } = data; 105 | const proxy = new ElementProxyReceiver(); 106 | this.targets[id] = proxy; 107 | } 108 | 109 | getProxy(id: number): ElementProxyReceiver { 110 | return this.targets[id]; 111 | } 112 | 113 | handleEvent(data: { id: number; data: EventData }): void { 114 | this.targets[data.id]?.handleEvent(data.data); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/editor/sidePanel/inspector/utils/InspectLight.tsx: -------------------------------------------------------------------------------- 1 | import { Color } from 'three'; 2 | import RemoteThree from '../../../../core/remote/RemoteThree'; 3 | import InspectorGroup from '../InspectorGroup'; 4 | import { RemoteObject } from '../../types'; 5 | import { setItemProps } from '../../utils'; 6 | 7 | function prettyName(value: string): string { 8 | switch (value) { 9 | case 'color': return 'Color'; 10 | case 'intensity': return 'Intensity'; 11 | case 'decay': return 'Decay'; 12 | case 'distance': return 'Distance'; 13 | case 'angle': return 'Angle'; 14 | case 'penumbra': return 'Penumbra'; 15 | case 'groundColor': return 'Ground Color'; 16 | case 'width': return 'Width'; 17 | case 'height': return 'Height'; 18 | } 19 | return value; 20 | } 21 | 22 | export function InspectLight(object: RemoteObject, three: RemoteThree) { 23 | function expandedName(): string { 24 | return `${three.name}_light`; 25 | } 26 | 27 | const expandedValue = localStorage.getItem(expandedName()); 28 | const expanded = expandedValue !== null ? expandedValue === 'open' : false; 29 | 30 | function saveExpanded(value: boolean) { 31 | localStorage.setItem(expandedName(), value ? 'open' : 'closed'); 32 | } 33 | 34 | const items: any[] = []; 35 | if (object.lightInfo !== undefined) { 36 | for (const i in object.lightInfo) { 37 | const value = object.lightInfo[i]; 38 | if (value === undefined) continue; 39 | 40 | if (value.isColor !== undefined) { 41 | items.push({ 42 | title: prettyName(i), 43 | prop: i, 44 | type: 'color', 45 | value: value, 46 | onChange: (prop: string, value: any) => { 47 | const color = new Color(value); 48 | // App 49 | three.updateObject(object.uuid, prop, color); 50 | 51 | // Editor 52 | const scene = three.getScene(object.uuid); 53 | if (scene !== null) { 54 | const child = scene.getObjectByProperty('uuid', object.uuid); 55 | setItemProps(child, prop, color); 56 | } 57 | } 58 | }); 59 | } else { 60 | items.push({ 61 | title: prettyName(i), 62 | prop: i, 63 | type: typeof value, 64 | value: value, 65 | step: typeof value === 'number' ? 0.01 : undefined, 66 | onChange: (prop: string, value: any) => { 67 | // App 68 | three.updateObject(object.uuid, prop, value); 69 | 70 | // Editor 71 | const scene = three.getScene(object.uuid); 72 | if (scene !== null) { 73 | const child = scene.getObjectByProperty('uuid', object.uuid); 74 | setItemProps(child, prop, value); 75 | } 76 | } 77 | }); 78 | } 79 | } 80 | } 81 | return ( 82 | { 88 | saveExpanded(value); 89 | }} 90 | /> 91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /src/editor/sidePanel/inspector/InspectNumber.tsx: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect, useRef, useState } from 'react'; 2 | import { InspectorFieldType } from './InspectorField'; 3 | import DragNumber from './utils/DragNumber'; 4 | import { noop } from '../../../core/types'; 5 | import { randomID } from '../../../editor/utils'; 6 | 7 | export interface InspectNumberProps { 8 | alt?: string 9 | value: number 10 | prop: string 11 | type: InspectorFieldType 12 | min?: number 13 | max?: number 14 | step?: number 15 | disabled?: boolean 16 | className?: string 17 | labelRef: RefObject 18 | onChange?: (prop: string, value: number) => void 19 | } 20 | 21 | export default function InspectNumber(props: InspectNumberProps) { 22 | const inputRef = useRef(null); 23 | const sliderRef = useRef(null); 24 | const [value, setValue] = useState(props.value); 25 | 26 | // Hooks 27 | DragNumber({ 28 | label: props.labelRef, 29 | input: inputRef, 30 | sliderRef: sliderRef, 31 | defaultValue: value, 32 | min: props.min, 33 | max: props.max, 34 | step: props.step, 35 | onChange: (newValue: number) => { 36 | setValue(newValue); 37 | if (props.onChange !== undefined) props.onChange(props.prop, newValue); 38 | } 39 | }); 40 | 41 | return ( 42 | <> 43 | {props.type === 'number' && ( 44 | { 56 | setValue(evt.target.value); 57 | if (evt.target.value.length === 0) return; 58 | const value = Number(evt.target.value); 59 | if (isNaN(value)) return; 60 | if (props.onChange !== undefined) props.onChange(props.prop, value); 61 | }} 62 | /> 63 | )} 64 | 65 | {props.type === 'range' && ( 66 | <> 67 | { 75 | if (evt.target.value.length === 0) return; 76 | const updatedValue = Number(evt.target.value); 77 | if (isNaN(updatedValue)) return; 78 | setValue(updatedValue); 79 | if (props.onChange !== undefined) props.onChange(props.prop, updatedValue); 80 | }} 81 | /> 82 | 93 | 94 | )} 95 | 96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /src/editor/sidePanel/inspector/utils/DragNumber.tsx: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect, useState } from 'react'; 2 | 3 | interface DragProps { 4 | label: RefObject 5 | input: RefObject 6 | sliderRef?: RefObject 7 | defaultValue: number 8 | min?: number 9 | max?: number 10 | step?: number 11 | onChange?: (value: number) => void 12 | } 13 | 14 | export default function DragNumber(props: DragProps) { 15 | const [fieldValue, setFieldValue] = useState(props.defaultValue); 16 | 17 | useEffect(() => { 18 | let mouseDown = false; 19 | let mouseStart = -1; 20 | let valueStart = 0; 21 | let value = props.defaultValue; 22 | let multiplyAmount = false; 23 | 24 | const onKeyEvent = (evt: KeyboardEvent) => { 25 | multiplyAmount = evt.ctrlKey; 26 | }; 27 | 28 | const onMouseDown = (evt: MouseEvent) => { 29 | mouseDown = true; 30 | valueStart = Number(props.input.current?.value); 31 | mouseStart = evt.clientX; 32 | document.addEventListener('mouseup', onMouseUp, false); 33 | document.addEventListener('mousemove', onMouseMove, false); 34 | document.addEventListener('contextmenu', onMouseUp, false); 35 | }; 36 | 37 | const onMouseMove = (evt: MouseEvent) => { 38 | if (!mouseDown) return; 39 | const deltaAmt = props.step !== undefined ? props.step : 1; 40 | const delta = (evt.clientX - mouseStart) * deltaAmt * (multiplyAmount ? 10 : 1); 41 | value = Number((valueStart + delta).toFixed(4)); 42 | if (props.min !== undefined) value = Math.max(value, props.min); 43 | if (props.max !== undefined) value = Math.min(value, props.max); 44 | if (props.onChange !== undefined) props.onChange(value); 45 | setFieldValue(value); 46 | }; 47 | 48 | const onMouseUp = () => { 49 | mouseDown = false; 50 | document.removeEventListener('mouseup', onMouseUp); 51 | document.removeEventListener('mousemove', onMouseMove); 52 | document.removeEventListener('contextmenu', onMouseUp); 53 | }; 54 | 55 | const onSlide = (evt: any) => { 56 | const newValue = Number(evt.target.value); 57 | if (props.onChange !== undefined) props.onChange(newValue); 58 | setFieldValue(newValue); 59 | }; 60 | 61 | props.label.current?.addEventListener('mousedown', onMouseDown, false); 62 | if (props.sliderRef !== undefined) { 63 | props.sliderRef.current?.addEventListener('input', onSlide); 64 | } 65 | document.addEventListener('keydown', onKeyEvent, false); 66 | document.addEventListener('keyup', onKeyEvent, false); 67 | 68 | return () => { 69 | props.label.current?.removeEventListener('mousedown', onMouseDown); 70 | if (props.sliderRef !== undefined) { 71 | props.sliderRef.current?.removeEventListener('input', onSlide); 72 | } 73 | document.removeEventListener('mouseup', onMouseUp); 74 | document.removeEventListener('mousemove', onMouseMove); 75 | document.removeEventListener('contextmenu', onMouseUp); 76 | document.removeEventListener('keydown', onKeyEvent); 77 | document.addEventListener('keyup', onKeyEvent, false); 78 | }; 79 | }, []); 80 | 81 | return fieldValue; 82 | } -------------------------------------------------------------------------------- /src/example/three/scenes/RTTScene.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Color, 3 | DirectionalLight, 4 | Mesh, 5 | MeshNormalMaterial, 6 | MeshPhysicalMaterial, 7 | PlaneGeometry, 8 | RenderTarget, 9 | SphereGeometry, 10 | TorusKnotGeometry, 11 | Vector3, 12 | } from 'three'; 13 | import { hierarchyUUID } from '../../../utils/three'; 14 | import { cubeTextures } from '../loader'; 15 | import BaseScene from './BaseScene'; 16 | import RemoteThree from '../../../core/remote/RemoteThree'; 17 | 18 | const zero3 = new Vector3(); 19 | 20 | export default class RTTScene extends BaseScene { 21 | renderTarget!: RenderTarget; 22 | mesh!: Mesh; 23 | 24 | constructor() { 25 | super(); 26 | this.name = 'RTTScene'; 27 | this.camera.position.set(0, 0, 100); 28 | this.camera.lookAt(zero3); 29 | } 30 | 31 | override init(): void { 32 | const envMap = cubeTextures.get('environment')!.clone(); 33 | this.background = envMap; 34 | 35 | const light = new DirectionalLight(new Color(0xffffff), 1); 36 | light.name = 'sun'; 37 | this.add(light); 38 | 39 | this.mesh = new Mesh( 40 | new TorusKnotGeometry(10, 3, 100, 6), 41 | new MeshPhysicalMaterial({ envMap: envMap, envMapIntensity: 10 }) 42 | ); 43 | this.mesh.name = 'normalMesh'; 44 | this.add(this.mesh); 45 | 46 | const ball = new Mesh(new SphereGeometry(10), new MeshNormalMaterial()); 47 | ball.name = 'ball'; 48 | ball.position.set(25, 25, 0); 49 | this.add(ball); 50 | 51 | const floor = new Mesh( 52 | new PlaneGeometry(200, 200), 53 | new MeshPhysicalMaterial({ 54 | envMap: envMap, 55 | }) 56 | ); 57 | floor.name = 'floor'; 58 | floor.rotation.x = -Math.PI / 2; 59 | floor.position.set(0, -20, 0); 60 | this.add(floor); 61 | 62 | this.renderTarget = new RenderTarget(512, 512); 63 | 64 | hierarchyUUID(this); 65 | 66 | const three = this.app.components.get('three') as RemoteThree; 67 | three.addScene(this); 68 | three.addCamera(this.camera); 69 | } 70 | 71 | override dispose(): void { 72 | const three = this.app.components.get('three') as RemoteThree; 73 | three.removeCamera(this.camera); 74 | three.removeScene(this); 75 | super.dispose(); 76 | } 77 | 78 | override draw() { 79 | const time = this.clock.getElapsedTime(); 80 | const radius = 100; 81 | const angle = time * 0.05 * Math.PI * 2; 82 | const x = Math.cos(angle) * radius; 83 | const z = Math.sin(angle) * radius; 84 | this.camera.position.set(x, 0, z); 85 | this.camera.lookAt(zero3); 86 | 87 | // Draw 88 | if (this.renderer) { 89 | if (this.renderer.isWebGLRenderer) { 90 | this.renderer.setRenderTarget(this.renderTarget); 91 | this.renderer.clear(); 92 | this.renderer.render(this, this.camera); 93 | this.renderer.setRenderTarget(null); 94 | } else { 95 | this.renderer.setRenderTarget(this.renderTarget); 96 | this.renderer.clearAsync(); 97 | this.renderer.renderAsync(this, this.camera); 98 | this.renderer.setRenderTarget(null); 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /types/editor/sidePanel/types.d.ts: -------------------------------------------------------------------------------- 1 | import RemoteThree from '../../core/remote/RemoteThree'; 2 | import { Color } from 'three'; 3 | export interface CoreComponentProps { 4 | class?: string; 5 | three: RemoteThree; 6 | } 7 | export interface ChildObjectProps extends CoreComponentProps { 8 | child?: RemoteObject; 9 | scene?: RemoteObject; 10 | three: RemoteThree; 11 | } 12 | export interface SidePanelState { 13 | scene?: RemoteObject; 14 | three: RemoteThree; 15 | } 16 | export interface MinimumObject { 17 | name: string; 18 | uuid: string; 19 | type: string; 20 | children: MinimumObject[]; 21 | } 22 | export interface RemoteMaterial { 23 | blending: number; 24 | blendSrc: number; 25 | blendDst: number; 26 | blendEquation: number; 27 | blendColor: Color; 28 | blendAlpha: number; 29 | depthFunc: number; 30 | depthTest: boolean; 31 | depthWrite: boolean; 32 | stencilWriteMask: number; 33 | stencilFunc: number; 34 | stencilRef: number; 35 | stencilFuncMask: number; 36 | stencilFail: number; 37 | stencilZFail: number; 38 | stencilZPass: number; 39 | stencilWrite: boolean; 40 | clipIntersection: boolean; 41 | polygonOffset: boolean; 42 | polygonOffsetFactor: number; 43 | polygonOffsetUnits: number; 44 | dithering: boolean; 45 | name: string; 46 | opacity: number; 47 | premultipliedAlpha: boolean; 48 | side: number; 49 | toneMapped: boolean; 50 | transparent: boolean; 51 | type: string; 52 | uuid: string; 53 | vertexColors: boolean; 54 | defines: any; 55 | extensions: any; 56 | uniforms: any; 57 | anisotropy: number; 58 | attenuationDistance: number; 59 | clearcoat: number; 60 | dispersion: number; 61 | iridescence: number; 62 | sheen: number; 63 | gradientMap: any; 64 | color?: Color; 65 | attenuationColor?: Color; 66 | sheenColor?: Color; 67 | specularColor?: Color; 68 | } 69 | export interface AnimationClipInfo { 70 | name: string; 71 | duration: number; 72 | blendMode: number; 73 | } 74 | export interface PerspectiveCameraInfo { 75 | fov: number; 76 | zoom: number; 77 | near: number; 78 | far: number; 79 | focus: number; 80 | aspect: number; 81 | filmGauge: number; 82 | filmOffset: number; 83 | } 84 | export interface OrthographicCameraInfo { 85 | zoom: number; 86 | near: number; 87 | far: number; 88 | left: number; 89 | right: number; 90 | top: number; 91 | bottom: number; 92 | } 93 | export interface LightInfo { 94 | color: Color; 95 | intensity: number; 96 | decay?: number; 97 | distance?: number; 98 | width?: number; 99 | height?: number; 100 | angle?: number; 101 | penumbra?: number; 102 | groundColor?: Color; 103 | } 104 | export interface RemoteObject { 105 | name: string; 106 | uuid: string; 107 | type: string; 108 | visible: boolean; 109 | matrix: number[]; 110 | animations: AnimationClipInfo[]; 111 | material?: RemoteMaterial | RemoteMaterial[]; 112 | perspectiveCameraInfo?: PerspectiveCameraInfo; 113 | orthographicCameraInfo?: OrthographicCameraInfo; 114 | lightInfo?: LightInfo; 115 | children: RemoteObject[]; 116 | } 117 | -------------------------------------------------------------------------------- /src/editor/sidePanel/inspector/InspectGrid3.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo, useRef } from 'react'; 2 | import { Euler, Matrix3, Vector3 } from 'three'; 3 | import InspectNumber from './InspectNumber'; 4 | import { degToRad, radToDeg } from 'three/src/math/MathUtils'; 5 | 6 | interface InspectGrid3Props { 7 | value: Vector3 | Matrix3 | Euler; 8 | step?: number; 9 | onChange: (evt: any) => void; 10 | } 11 | 12 | export default function InspectGrid3(props: InspectGrid3Props) { 13 | const isVector = props.value['x'] !== undefined && props.value['y'] !== undefined && props.value['z'] !== undefined; 14 | const isEuler = props.value['isEuler'] !== undefined; 15 | const isMatrix = props.value['elements'] !== undefined; 16 | const step = props.step !== undefined ? props.step : 0.01; 17 | const children: any[] = []; 18 | 19 | if (isEuler) { 20 | const euler = useMemo(() => props.value as Euler, []); 21 | const params = ['_x', '_y', '_z']; 22 | params.forEach((param: string) => { 23 | const labelRef = useRef(null); 24 | children.push( 25 |
    26 | {param.substring(1).toUpperCase()} 27 | { 34 | euler[prop] = degToRad(value); 35 | props.onChange({ target: { value: euler } }); 36 | }} 37 | /> 38 |
    39 | ); 40 | }); 41 | } else if (isVector) { 42 | const vector = useMemo(() => props.value as Vector3, []); 43 | const onChange = (prop: string, value: number) => { 44 | vector[prop] = value; 45 | props.onChange({ target: { value: vector } }); 46 | }; 47 | 48 | const params = ['x', 'y', 'z']; 49 | params.forEach((param: string) => { 50 | const labelRef = useRef(null); 51 | children.push( 52 |
    53 | 54 | 62 |
    63 | ); 64 | }); 65 | } else if (isMatrix) { 66 | const matrix = useMemo(() => props.value as Matrix3, []); 67 | const onChange = (prop: string, value: number) => { 68 | const index = Number(prop); 69 | matrix.elements[index] = value; 70 | props.onChange({ target: { value: matrix } }); 71 | }; 72 | 73 | for (let i = 0; i < 9; i++) { 74 | const labelRef = useRef(null); 75 | children.push( 76 |
    77 | 78 | 86 |
    87 | ); 88 | } 89 | } 90 | 91 | return ( 92 |
    {children}
    93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /src/editor/sidePanel/types.ts: -------------------------------------------------------------------------------- 1 | import RemoteThree from '../../core/remote/RemoteThree'; 2 | import { Color } from 'three'; 3 | 4 | export interface CoreComponentProps { 5 | class?: string 6 | three: RemoteThree 7 | } 8 | 9 | export interface ChildObjectProps extends CoreComponentProps { 10 | child?: RemoteObject 11 | scene?: RemoteObject 12 | three: RemoteThree 13 | } 14 | 15 | export interface SidePanelState { 16 | scene?: RemoteObject 17 | three: RemoteThree 18 | } 19 | 20 | export interface MinimumObject { 21 | name: string 22 | uuid: string 23 | type: string 24 | children: MinimumObject[] 25 | } 26 | 27 | export interface RemoteMaterial { 28 | // Blending 29 | blending: number 30 | blendSrc: number 31 | blendDst: number 32 | blendEquation: number 33 | blendColor: Color 34 | blendAlpha: number 35 | // Depth 36 | depthFunc: number 37 | depthTest: boolean 38 | depthWrite: boolean 39 | // Stencil 40 | stencilWriteMask: number 41 | stencilFunc: number 42 | stencilRef: number 43 | stencilFuncMask: number 44 | stencilFail: number 45 | stencilZFail: number 46 | stencilZPass: number 47 | stencilWrite: boolean 48 | // Clipping 49 | clipIntersection: boolean 50 | // Polygon 51 | polygonOffset: boolean 52 | polygonOffsetFactor: number 53 | polygonOffsetUnits: number 54 | // ETC 55 | dithering: boolean 56 | name: string 57 | opacity: number 58 | premultipliedAlpha: boolean 59 | side: number 60 | toneMapped: boolean 61 | transparent: boolean 62 | type: string 63 | uuid: string 64 | vertexColors: boolean 65 | defines: any 66 | extensions: any 67 | uniforms: any 68 | anisotropy: number 69 | attenuationDistance: number 70 | clearcoat: number 71 | dispersion: number 72 | iridescence: number 73 | sheen: number 74 | gradientMap: any 75 | // Colors 76 | color?: Color 77 | attenuationColor?: Color 78 | sheenColor?: Color 79 | specularColor?: Color 80 | } 81 | 82 | // Animation Info 83 | 84 | export interface AnimationClipInfo { 85 | name: string; 86 | duration: number; 87 | blendMode: number; 88 | } 89 | 90 | // Camera Info 91 | 92 | export interface PerspectiveCameraInfo { 93 | fov: number 94 | zoom: number 95 | near: number 96 | far: number 97 | focus: number 98 | aspect: number 99 | filmGauge: number 100 | filmOffset: number 101 | } 102 | 103 | export interface OrthographicCameraInfo { 104 | zoom: number 105 | near: number 106 | far: number 107 | left: number 108 | right: number 109 | top: number 110 | bottom: number 111 | } 112 | 113 | // Light Info 114 | export interface LightInfo { 115 | color: Color 116 | intensity: number 117 | // Point 118 | decay?: number 119 | distance?: number 120 | // Rect 121 | width?: number 122 | height?: number 123 | // Spot 124 | angle?: number 125 | penumbra?: number 126 | // Hemisphere 127 | groundColor?: Color 128 | } 129 | 130 | export interface RemoteObject { 131 | name: string 132 | uuid: string 133 | type: string 134 | visible: boolean 135 | matrix: number[] // based on Matrix4.elements 136 | animations: AnimationClipInfo[] 137 | material?: RemoteMaterial | RemoteMaterial[] 138 | perspectiveCameraInfo?: PerspectiveCameraInfo 139 | orthographicCameraInfo?: OrthographicCameraInfo 140 | lightInfo?: LightInfo 141 | children: RemoteObject[] 142 | } 143 | -------------------------------------------------------------------------------- /src/editor/sidePanel/inspector/utils/InspectCamera.tsx: -------------------------------------------------------------------------------- 1 | import RemoteThree from '../../../../core/remote/RemoteThree'; 2 | import InspectorGroup from '../InspectorGroup'; 3 | import { RemoteObject } from '../../types'; 4 | import { setItemProps } from '../../utils'; 5 | 6 | function prettyName(name: string): string { 7 | switch (name) { 8 | case 'fov': return 'FOV'; 9 | case 'zoom': return 'Zoom'; 10 | case 'near': return 'Near'; 11 | case 'far': return 'Far'; 12 | case 'focus': return 'Focus'; 13 | case 'aspect': return 'Aspect'; 14 | case 'filmGauge': return 'Film Gauge'; 15 | case 'filmOffset': return 'Film Offset'; 16 | case 'left': return 'Left'; 17 | case 'right': return 'Right'; 18 | case 'top': return 'Top'; 19 | case 'bottom': return 'Bottom'; 20 | } 21 | return name; 22 | } 23 | 24 | export function InspectCamera(object: RemoteObject, three: RemoteThree): any { 25 | function expandedName(): string { 26 | return `${three.name}_camera`; 27 | } 28 | 29 | const expandedValue = localStorage.getItem(expandedName()); 30 | const expanded = expandedValue !== null ? expandedValue === 'open' : false; 31 | 32 | function saveExpanded(value: boolean) { 33 | localStorage.setItem(expandedName(), value ? 'open' : 'closed'); 34 | } 35 | 36 | const items: any[] = []; 37 | 38 | if (object.perspectiveCameraInfo !== undefined) { 39 | for (const i in object.perspectiveCameraInfo) { 40 | items.push({ 41 | title: prettyName(i), 42 | prop: i, 43 | type: 'number', 44 | step: 0.01, 45 | value: object.perspectiveCameraInfo[i], 46 | onChange: (prop: string, value: any) => { 47 | // App 48 | three.updateObject(object.uuid, prop, value); 49 | three.requestMethod(object.uuid, 'updateProjectionMatrix'); 50 | 51 | // Editor 52 | const scene = three.getScene(object.uuid); 53 | if (scene !== null) { 54 | const child = scene.getObjectByProperty('uuid', object.uuid); 55 | if (child !== undefined) { 56 | setItemProps(child, prop, value); 57 | child['updateProjectionMatrix'](); 58 | } 59 | } 60 | } 61 | }); 62 | } 63 | } else if (object.orthographicCameraInfo !== undefined) { 64 | for (const i in object.orthographicCameraInfo) { 65 | items.push({ 66 | title: prettyName(i), 67 | prop: i, 68 | type: 'number', 69 | step: 0.01, 70 | value: object.orthographicCameraInfo![i], 71 | onChange: (prop: string, value: any) => { 72 | // App 73 | three.updateObject(object.uuid, prop, value); 74 | three.requestMethod(object.uuid, 'updateProjectionMatrix'); 75 | 76 | // Editor 77 | const scene = three.getScene(object.uuid); 78 | if (scene !== null) { 79 | const child = scene.getObjectByProperty('uuid', object.uuid); 80 | if (child !== undefined) { 81 | setItemProps(child, prop, value); 82 | child['updateProjectionMatrix'](); 83 | } 84 | } 85 | } 86 | }); 87 | } 88 | } 89 | 90 | return ( 91 | { 97 | saveExpanded(value); 98 | }} 99 | /> 100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /types/editor/components/content.d.ts: -------------------------------------------------------------------------------- 1 | export declare const gridImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAABkCAYAAAA8AQ3AAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEoklEQVR4nO3aQc4aMRAFYR+AOQv3Px3ZRYoSoh8YxvX61YJdSdPuD++81lqPZ7/b7fa43++P4zieNnbPu+M40PO1dXqwujc9WIeY0nk5WJ0erO4DD84hpnReDlanB6v70INxiCmdl4PV6cHqTvDYf4gpnZeD1enB6k7yyDgsvfNysDo9WN2JHvzD0jsvB6vTg9Wd6kE/LL3zcrA6PVjd2R6LfFh65+VgdXqwum94LOph6Z2Xg9Xpweq+5bGIh6V3Xg5Wpwer+7IH67D0zsvB6vRgdRd4cA5L77wcrE4PVneRB+Ow9M7Lwer0YHUXeuw/LL3zcrA6PVjdxR4ZSynBsNMjqtvgwV9KEYadHjHdFg/6Uqow7PQI6XZ5+NIdhGGnR0K308OX7iAMOz3o3W6PRVxKK4adHuQO4sFaSjmGnR7IDuTBWYoYdnrwOpgHYyli2OnB64Ae+5cihp0evA7qkbG8EozaTg9WB/bgL68Io7LTg9WhPejLq8Io7PRgdXSPqpfudIy2Tg9Wl+BR89J9F0bCn6BpL3qw9vLqdxdxebuXkv5deqcHqwvzYC0PspTY79I7PVhdoAdneaClRH6X3unB6kI9GMuDLSXuu/ROD1YX7LF/ecClRH2X3unB6sI9MpZcgjGu04PVDfDgL7kIY1SnB6sb4UFfchXGoE4PVjfFY8RL9ykYUzo9WN0kj/iX7pMwJnR6sLppHou45N1LoX6X3unB6oZ6sJYMWQruu/ROD1Y32IOzZNBSUN+ld3qwuuEejCXTOy8Hq9OD1V3osf+w9M7Lwer0YHUXe2QspQTDTo+oboMHfylFGHZ6xHRbPOhLqcKw0yOk2+Ux4qX7FAw7PRK6nR7xL90nYdjpQe92eyziUlox7PQgdxAP1lLKMez0QHYgD85SxLDTg9fBPBhLEcNOD14H9Ni/FDHs9OB1UI+M5ZVg1HZ6sDqwB395RRiVnR6sDu1BX14VRmGnB6uje1S9dKdjtHV6sLoEj5qX7gkYTZ0erC7FY5GHa8No6fRgdWEe6OHaMMZ3erC6QA/0cG0Yozs9WF2oB3q4NoyxnR6sLtgDPVwbxshOD1YX7oEerg1jXKcHqxvggR6uDWNUpwerG+GBHq4NY1CnB6ub4jHipfsUjCmdHqxukkf8S/dJGBM6PVjdNI9FHq4NI73Tg9UN9UAP14YR2+nB6gZ7oIdrw4js9GB1wz3Qw7VhxHV6sLoCD/RwbRhRnR6srsQDPVwbRkynB6sr8kAP14YR0enB6qo80MO1YQR0erC6Ng/0S/c2DHqnB6tr9MC+dG/EIHd6sLpWj0UcrhWD2unB6so9WMOVY+A6PVidHqDhxGB1erA6PdZjUYYTg9Xpwer0+P3bP5wYrE4PVqfHHz8x7PSgdnr89RPDTg9ip8c/fmLY6cHr9HjyE8NOD1anx/Pu8pfuYrA6PVidHv/vlhi9nR6sTo8fdWI0dnqwOj1+3InR1unB6vR4qROjqdOD1enxcidGS6cHq9PjrU6Mhk4PVqfH250Y0zs9WJ0eH3RizO70YHV6fNad+tJdDFanB6vT4/PutJfuYrA6PVidHud0vwA7LKl1AiiR5gAAAABJRU5ErkJggg=="; 2 | export declare const noImage = "data:image/gif;base64,R0lGODlhDgFkAIAAAP///wAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDIgNzkuZGJhM2RhM2I1LCAyMDIzLzEyLzE1LTEwOjQyOjM3ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjUuNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoyMDk3M0NEODAxQjQxMUVGODVGNENDMkUyMUExNDk1NSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoyMDk3M0NEOTAxQjQxMUVGODVGNENDMkUyMUExNDk1NSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkE4ODc3Qzg5MDFCMzExRUY4NUY0Q0MyRTIxQTE0OTU1IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkE4ODc3QzhBMDFCMzExRUY4NUY0Q0MyRTIxQTE0OTU1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAA4BZAAAAv+Mj6nL7Q+jnLTai7PevPsPhuJIluaJpurKtu4Lx/JM1/aN5/rO9/4PDAqHxKLxiEwql8ym8wmNSqfUqvWKzWq33K73Cw6Lx+Sy+YxOq9fstvsNj8vn9Lr9js/r9/y+/w8YKDhIWGh4iJiouMjY6PgIGSk5SVlpeYmZqTkJAGDQ+dnpuekmGgAKejpKuiZqmprKqoZKGyrbOlqrejub6xvLGyw8TFzcprurGuvqybxq7ETbrItsCz0l7Zpc+6p9/cS967w9/S2FTF0u/mzehK4Oqz3eTl9vf4+fr7/P3+//DzCgwIEECxo8iDChwoUMGzp8CDGixIkUK1q8iDGjxo0XHDt6/AgypMiRJEuaPIkypcqVLFt+KwAAOw=="; 3 | -------------------------------------------------------------------------------- /src/editor/components/content.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | export const gridImage = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAABkCAYAAAA8AQ3AAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEoklEQVR4nO3aQc4aMRAFYR+AOQv3Px3ZRYoSoh8YxvX61YJdSdPuD++81lqPZ7/b7fa43++P4zieNnbPu+M40PO1dXqwujc9WIeY0nk5WJ0erO4DD84hpnReDlanB6v70INxiCmdl4PV6cHqTvDYf4gpnZeD1enB6k7yyDgsvfNysDo9WN2JHvzD0jsvB6vTg9Wd6kE/LL3zcrA6PVjd2R6LfFh65+VgdXqwum94LOph6Z2Xg9Xpweq+5bGIh6V3Xg5Wpwer+7IH67D0zsvB6vRgdRd4cA5L77wcrE4PVneRB+Ow9M7Lwer0YHUXeuw/LL3zcrA6PVjdxR4ZSynBsNMjqtvgwV9KEYadHjHdFg/6Uqow7PQI6XZ5+NIdhGGnR0K308OX7iAMOz3o3W6PRVxKK4adHuQO4sFaSjmGnR7IDuTBWYoYdnrwOpgHYyli2OnB64Ae+5cihp0evA7qkbG8EozaTg9WB/bgL68Io7LTg9WhPejLq8Io7PRgdXSPqpfudIy2Tg9Wl+BR89J9F0bCn6BpL3qw9vLqdxdxebuXkv5deqcHqwvzYC0PspTY79I7PVhdoAdneaClRH6X3unB6kI9GMuDLSXuu/ROD1YX7LF/ecClRH2X3unB6sI9MpZcgjGu04PVDfDgL7kIY1SnB6sb4UFfchXGoE4PVjfFY8RL9ykYUzo9WN0kj/iX7pMwJnR6sLppHou45N1LoX6X3unB6oZ6sJYMWQruu/ROD1Y32IOzZNBSUN+ld3qwuuEejCXTOy8Hq9OD1V3osf+w9M7Lwer0YHUXe2QspQTDTo+oboMHfylFGHZ6xHRbPOhLqcKw0yOk2+Ux4qX7FAw7PRK6nR7xL90nYdjpQe92eyziUlox7PQgdxAP1lLKMez0QHYgD85SxLDTg9fBPBhLEcNOD14H9Ni/FDHs9OB1UI+M5ZVg1HZ6sDqwB395RRiVnR6sDu1BX14VRmGnB6uje1S9dKdjtHV6sLoEj5qX7gkYTZ0erC7FY5GHa8No6fRgdWEe6OHaMMZ3erC6QA/0cG0Yozs9WF2oB3q4NoyxnR6sLtgDPVwbxshOD1YX7oEerg1jXKcHqxvggR6uDWNUpwerG+GBHq4NY1CnB6ub4jHipfsUjCmdHqxukkf8S/dJGBM6PVjdNI9FHq4NI73Tg9UN9UAP14YR2+nB6gZ7oIdrw4js9GB1wz3Qw7VhxHV6sLoCD/RwbRhRnR6srsQDPVwbRkynB6sr8kAP14YR0enB6qo80MO1YQR0erC6Ng/0S/c2DHqnB6tr9MC+dG/EIHd6sLpWj0UcrhWD2unB6so9WMOVY+A6PVidHqDhxGB1erA6PdZjUYYTg9Xpwer0+P3bP5wYrE4PVqfHHz8x7PSgdnr89RPDTg9ip8c/fmLY6cHr9HjyE8NOD1anx/Pu8pfuYrA6PVidHv/vlhi9nR6sTo8fdWI0dnqwOj1+3InR1unB6vR4qROjqdOD1enxcidGS6cHq9PjrU6Mhk4PVqfH250Y0zs9WJ0eH3RizO70YHV6fNad+tJdDFanB6vT4/PutJfuYrA6PVidHud0vwA7LKl1AiiR5gAAAABJRU5ErkJggg==`; 3 | export const noImage = `data:image/gif;base64,R0lGODlhDgFkAIAAAP///wAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDIgNzkuZGJhM2RhM2I1LCAyMDIzLzEyLzE1LTEwOjQyOjM3ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjUuNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoyMDk3M0NEODAxQjQxMUVGODVGNENDMkUyMUExNDk1NSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoyMDk3M0NEOTAxQjQxMUVGODVGNENDMkUyMUExNDk1NSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkE4ODc3Qzg5MDFCMzExRUY4NUY0Q0MyRTIxQTE0OTU1IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkE4ODc3QzhBMDFCMzExRUY4NUY0Q0MyRTIxQTE0OTU1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEAAAAAAAsAAAAAA4BZAAAAv+Mj6nL7Q+jnLTai7PevPsPhuJIluaJpurKtu4Lx/JM1/aN5/rO9/4PDAqHxKLxiEwql8ym8wmNSqfUqvWKzWq33K73Cw6Lx+Sy+YxOq9fstvsNj8vn9Lr9js/r9/y+/w8YKDhIWGh4iJiouMjY6PgIGSk5SVlpeYmZqTkJAGDQ+dnpuekmGgAKejpKuiZqmprKqoZKGyrbOlqrejub6xvLGyw8TFzcprurGuvqybxq7ETbrItsCz0l7Zpc+6p9/cS967w9/S2FTF0u/mzehK4Oqz3eTl9vf4+fr7/P3+//DzCgwIEECxo8iDChwoUMGzp8CDGixIkUK1q8iDGjxo0XHDt6/AgypMiRJEuaPIkypcqVLFt+KwAAOw==`; 4 | -------------------------------------------------------------------------------- /types/core/remote/RemoteThree.d.ts: -------------------------------------------------------------------------------- 1 | import { Camera, Curve, EventDispatcher, EventListener, Object3D, RenderTargetOptions, Scene, WebGLRenderTarget } from 'three'; 2 | import BaseRemote from './BaseRemote'; 3 | import { BroadcastData, GroupData } from '../types'; 4 | export declare enum ToolEvents { 5 | CUSTOM = "ToolEvents::custom", 6 | SELECT_DROPDOWN = "ToolEvents::selectDropdown", 7 | DRAG_UPDATE = "ToolEvents::dragUpdate", 8 | ADD_SCENE = "ToolEvents::addScene", 9 | REFRESH_SCENE = "ToolEvents::refreshScene", 10 | REMOVE_SCENE = "ToolEvents::removeScene", 11 | SET_SCENE = "ToolEvents::setScene", 12 | SET_OBJECT = "ToolEvents::setObject", 13 | CLEAR_OBJECT = "ToolEvents::clearObject", 14 | ADD_CAMERA = "ToolEvents::addCamera", 15 | REMOVE_CAMERA = "ToolEvents::removeCamera", 16 | ADD_GROUP = "ToolEvents::addGroup", 17 | REMOVE_GROUP = "ToolEvents::removeGroup", 18 | ADD_SPLINE = "ToolEvents::addSpline", 19 | ADD_RENDERER = "ToolEvents::addRenderer", 20 | UPDATE_RENDERER = "ToolEvents::updateRenderer" 21 | } 22 | export type ToolEvent = { 23 | [key in ToolEvents]: { 24 | value?: unknown; 25 | }; 26 | }; 27 | export default class RemoteThree extends BaseRemote implements EventDispatcher { 28 | name: string; 29 | canvas: HTMLCanvasElement | null; 30 | inputElement: any; 31 | scene?: Scene; 32 | scenes: Map; 33 | renderer?: any; 34 | renderTargets: Map; 35 | private renderTargetsResize; 36 | private groups; 37 | private _listeners; 38 | constructor(name: string, debug?: boolean, editor?: boolean); 39 | dispose(): void; 40 | addEventListener(type: T, listener: EventListener): void; 41 | hasEventListener(type: T, listener: EventListener): boolean; 42 | removeEventListener(type: T, listener: EventListener): void; 43 | dispatchEvent(event: ToolEvent[T] & { 44 | type: T; 45 | }): void; 46 | getObjectByUUID(uuid: string): Object3D | undefined; 47 | getObject(uuid: string): void; 48 | setObject(value: any): void; 49 | requestMethod(uuid: string, key: string, value?: any, subitem?: string): void; 50 | updateObject(uuid: string, key: string, value: any): void; 51 | createTexture(uuid: string, key: string, value: any): void; 52 | private onUpdateObject; 53 | private onCreateTexture; 54 | addGroup(data: GroupData): void; 55 | removeGroup(name: string): void; 56 | updateGroup(group: string, prop: string, value: any): void; 57 | addSplineCurve(spline: Curve): void; 58 | addSplineObject(spline: any): void; 59 | setRenderer(value: any, inputElement?: any): void; 60 | updateRenderer(data: any): void; 61 | addScene(value: Scene): void; 62 | refreshScene(value: string): void; 63 | removeScene(value: Scene): void; 64 | removeAllScenes(): void; 65 | getScene(uuid: string): Scene | null; 66 | setScene(value: Scene): void; 67 | requestSize(): void; 68 | addCamera(camera: Camera): void; 69 | removeCamera(camera: Camera): void; 70 | handleApp(msg: BroadcastData): void; 71 | handleEditor(msg: BroadcastData): void; 72 | protected messageHandler(evt: MessageEvent): void; 73 | addRT(name: string, resize?: boolean, params?: RenderTargetOptions): void; 74 | removeRT(name: string): void; 75 | resize(width: number, height: number): void; 76 | set dpr(value: number); 77 | get dpr(): number; 78 | get width(): number; 79 | get height(): number; 80 | } 81 | -------------------------------------------------------------------------------- /src/utils/math.ts: -------------------------------------------------------------------------------- 1 | export function clamp(min: number, max: number, value: number) { 2 | return Math.min(max, Math.max(min, value)); 3 | } 4 | 5 | export function normalize(min: number, max: number, value: number) { 6 | return (value - min) / (max - min); 7 | } 8 | 9 | export function mix(min: number, max: number, value: number) { 10 | return min * (1 - value) + max * value; 11 | } 12 | 13 | export function map(min1: number, max1: number, min2: number, max2: number, value: number) { 14 | return mix(min2, max2, normalize(min1, max1, value)); 15 | } 16 | 17 | export function distance(x: number, y: number): number { 18 | const d = x - y; 19 | return Math.sqrt(d * d); 20 | } 21 | 22 | export function damp(start: number, end: number, easing: number, dt: number) { 23 | return mix(start, end, 1 - Math.exp(-easing * dt)); 24 | } 25 | 26 | export function roundTo(value: number, digits = 1): number { 27 | return Number(value.toFixed(digits)); 28 | } 29 | 30 | export function getAngle(x1: number, y1: number, x2: number, y2: number): number { 31 | return Math.atan2(y2 - y1, x2 - x1); 32 | } 33 | 34 | // Bezier 35 | 36 | function isLinear(x0: number, y0: number, x1: number, y1: number): boolean { 37 | return x0 === y0 && x1 === y1; 38 | } 39 | 40 | function slopeFromT(t: number, A: number, B: number, C: number): number { 41 | return 1.0 / (3.0 * A * t * t + 2.0 * B * t + C); 42 | } 43 | 44 | function xFromT(t: number, A: number, B: number, C: number, D: number): number { 45 | return A * (t * t * t) + B * (t * t) + C * t + D; 46 | } 47 | 48 | function yFromT(t: number, E: number, F: number, G: number, H: number): number { 49 | const tt = t * t; 50 | return E * (tt * t) + F * tt + G * t + H; 51 | } 52 | 53 | /** 54 | * Cubic Bezier easing 55 | * @param percent A number between 0 - 1 56 | * @param x0 First vector X 57 | * @param y0 First vector Y 58 | * @param x1 Second vector X 59 | * @param y1 Second vector Y 60 | */ 61 | export function cubicBezier(percent: number, x0: number, y0: number, x1: number, y1: number): number { 62 | if (percent <= 0) return 0; 63 | if (percent >= 1) return 1; 64 | if (isLinear(x0, y0, x1, y1)) return percent; // linear 65 | 66 | const x0a = 0; // initial x 67 | const y0a = 0; // initial y 68 | const x1a = x0; // 1st influence x 69 | const y1a = y0; // 1st influence y 70 | const x2a = x1; // 2nd influence x 71 | const y2a = y1; // 2nd influence y 72 | const x3a = 1; // final x 73 | const y3a = 1; // final y 74 | 75 | const A = x3a - 3.0 * x2a + 3.0 * x1a - x0a; 76 | const B = 3.0 * x2a - 6.0 * x1a + 3.0 * x0a; 77 | const C = 3.0 * x1a - 3.0 * x0a; 78 | const D = x0a; 79 | 80 | const E = y3a - 3.0 * y2a + 3.0 * y1a - y0a; 81 | const F = 3.0 * y2a - 6.0 * y1a + 3.0 * y0a; 82 | const G = 3.0 * y1a - 3.0 * y0a; 83 | const H = y0a; 84 | 85 | let current = percent; 86 | for (let i = 0; i < 5; i++) { 87 | const currentx = xFromT(current, A, B, C, D); 88 | let currentslope = slopeFromT(current, A, B, C); 89 | if (currentslope === Infinity) currentslope = percent; 90 | current -= (currentx - percent) * currentslope; 91 | current = Math.min(Math.max(current, 0.0), 1.0); 92 | } 93 | 94 | return yFromT(current, E, F, G, H); 95 | } 96 | 97 | const toHex = (v: number) => { 98 | const n = Math.round(Math.min(1, Math.max(0, v)) * 255); 99 | return n.toString(16).padStart(2, '0'); 100 | }; 101 | 102 | export function rgbaToHex({ r, g, b, a = 1 }: { r: number; g: number; b: number; a?: number }) { 103 | const hex = `#${toHex(r)}${toHex(g)}${toHex(b)}`; 104 | // Only include alpha if it's not fully opaque 105 | return a < 1 ? `${hex}${toHex(a)}` : hex; 106 | } 107 | -------------------------------------------------------------------------------- /src/editor/multiView/CameraWindow.tsx: -------------------------------------------------------------------------------- 1 | import { ForwardedRef, forwardRef, useState } from 'react'; 2 | import { Camera } from 'three'; 3 | import { RenderMode } from './MultiViewData'; 4 | 5 | interface DropdownProps { 6 | index: number; 7 | open: boolean; 8 | title: string; 9 | onToggle: (value: boolean) => void; 10 | onSelect: (value: string) => void; 11 | options: string[]; 12 | up?: boolean; 13 | } 14 | 15 | export const Dropdown = (props: DropdownProps) => { 16 | const [selectedOption, setSelectedOption] = useState(props.options[props.index]); 17 | 18 | const handleToggle = () => { 19 | props.onToggle(!props.open); 20 | }; 21 | 22 | const handleSelect = (option: any) => { 23 | if (option !== selectedOption) { 24 | props.onSelect(option); 25 | setSelectedOption(option); 26 | } 27 | props.onToggle(false); 28 | }; 29 | 30 | const height = props.open ? `${props.options.length * 31 - 1}px` : '0px'; 31 | 32 | return ( 33 |
    34 |
    35 | {`${props.title}: ${selectedOption}`} 36 |
    37 |
      38 | {props.options.map((option) => ( 39 |
    • handleSelect(option)}> 40 | {option} 41 |
    • 42 | ))} 43 |
    44 |
    45 | ); 46 | }; 47 | 48 | interface CameraWindowProps { 49 | name: string; 50 | camera: Camera 51 | onSelectCamera: (value: string) => void; 52 | onSelectRenderMode: (value: RenderMode) => void; 53 | options: string[]; 54 | } 55 | 56 | const CameraWindow = forwardRef(function CameraWindow(props: CameraWindowProps, ref: ForwardedRef) { 57 | const renderOptions: RenderMode[] = [ 58 | 'Renderer', 59 | 'Depth', 60 | 'Normals', 61 | 'UVs', 62 | 'Wireframe', 63 | ]; 64 | 65 | // States 66 | const [currentRenderMode, setCurrentRenderMode] = useState('Renderer'); 67 | const [modeOpen, setModeOpen] = useState(false); 68 | const [renderModeOpen, setRenderModeOpen] = useState(false); 69 | const [open, setOpen] = useState(false); 70 | 71 | return ( 72 |
    73 |
    { 74 | if (open) setOpen(false); 75 | }} /> 76 | 77 |
    78 | {props.camera !== null && ( 79 | { 86 | if (value && renderModeOpen) setRenderModeOpen(false); 87 | setOpen(value); 88 | }} 89 | up={true} 90 | /> 91 | )} 92 | { 98 | if (value === currentRenderMode) return; 99 | const newRenderMode = value as RenderMode; 100 | props.onSelectRenderMode(newRenderMode); 101 | setCurrentRenderMode(newRenderMode); 102 | }} 103 | onToggle={(value: boolean) => { 104 | if (value && open) setOpen(false); 105 | if (modeOpen) setModeOpen(false); 106 | setRenderModeOpen(value); 107 | }} 108 | up={true} 109 | /> 110 |
    111 |
    112 | ); 113 | }); 114 | 115 | export default CameraWindow; 116 | -------------------------------------------------------------------------------- /src/editor/multiView/MultiView.scss: -------------------------------------------------------------------------------- 1 | $padding: 2px; 2 | 3 | .editor .multiview { 4 | display: grid; 5 | font-family: Arial, Helvetica, sans-serif; 6 | font-size: 10px; 7 | grid-template-columns: auto; 8 | position: absolute; 9 | overflow: hidden; 10 | left: 0; 11 | top: 0; 12 | right: 300px; 13 | bottom: 0; 14 | z-index: 1; 15 | 16 | canvas { 17 | pointer-events: none; 18 | } 19 | 20 | .dropdown { 21 | background-color: #222; 22 | display: inline-block; 23 | font-size: 10px; 24 | text-align: center; 25 | 26 | .dropdown-toggle { 27 | cursor: pointer; 28 | padding: 0px 10px; 29 | height: 30px; 30 | line-height: 30px; 31 | overflow: hidden; 32 | &:hover { 33 | background-color: #333; 34 | } 35 | } 36 | 37 | .dropdown-menu { 38 | overflow: hidden; 39 | position: absolute; 40 | top: 30px; 41 | left: 50%; 42 | z-index: 1; 43 | list-style: none; 44 | padding: 0; 45 | margin: 0; 46 | min-width: 100%; 47 | width: auto; 48 | transform: translateX(-50%); 49 | transition: all 0.33s cubic-bezier(0.750, 0, 0.25, 1.000); 50 | 51 | li { 52 | background-color: #222; 53 | border-top: 1px solid #191919; 54 | cursor: pointer; 55 | height: 30px; 56 | line-height: 30px; 57 | padding: 0px 10px; 58 | transition: 0.2s linear background-color; 59 | &:hover { 60 | background-color: #333; 61 | } 62 | } 63 | } 64 | } 65 | 66 | .cameras { 67 | display: grid; 68 | grid-template-columns: repeat(2, 1fr); 69 | pointer-events: visible; 70 | position: absolute; 71 | width: 100%; 72 | height: 100%; 73 | 74 | &.single { 75 | grid-template-columns: repeat(1, 1fr); 76 | } 77 | 78 | .CameraWindow { 79 | border: 1px dotted #333; 80 | border-top: none; 81 | border-left: none; 82 | pointer-events: visible; 83 | position: relative; 84 | 85 | .clickable { 86 | display: inline-block; 87 | width: 100%; 88 | height: 100%; 89 | } 90 | 91 | .options { 92 | position: absolute; 93 | height: 30px; 94 | top: initial; 95 | bottom: -1px; 96 | left: 50%; 97 | transform: translateX(-50%); 98 | width: max-content; 99 | 100 | .dropdown { 101 | position: relative; 102 | top: 0; 103 | transition: background-color 0.25s linear; 104 | &.up { 105 | background-color: #333; 106 | bottom: 0; 107 | top: initial; 108 | .dropdown-menu { 109 | top: initial; 110 | bottom: 100%; 111 | } 112 | } 113 | } 114 | } 115 | } 116 | } 117 | 118 | .settings { 119 | box-shadow: rgba(0, 0, 0, 0.25) 0px 1px 1px, rgba(0, 0, 0, 0.15) 0px 2px 6px; 120 | pointer-events: visible; 121 | position: absolute; 122 | left: 50%; 123 | transform: translateX(-50%); 124 | 125 | .toggle { 126 | background-blend-mode: overlay; 127 | background-color: #222; 128 | background-position: 2px 2px; 129 | background-repeat: no-repeat; 130 | background-size: 26px 26px; 131 | display: inline-block; 132 | position: relative; 133 | left: 0; 134 | transform: none; 135 | width: 30px; 136 | height: 30px; 137 | overflow: hidden; 138 | &.selected { 139 | background-blend-mode: normal; 140 | } 141 | } 142 | } 143 | 144 | .connectionStatus { 145 | background-color: red; 146 | font-weight: bold; 147 | line-height: 30px; 148 | padding: 0 10px; 149 | text-transform: uppercase; 150 | position: absolute; 151 | left: 0; 152 | bottom: 0; 153 | z-index: 10; 154 | } 155 | } -------------------------------------------------------------------------------- /src/editor/sidePanel/inspector/utils/InspectAnimation.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { AnimationMixer, SkeletonHelper } from 'three'; 3 | import RemoteThree from '../../../../core/remote/RemoteThree'; 4 | import InspectorGroup from '../InspectorGroup'; 5 | import { AnimationClipInfo, RemoteObject } from '../../types'; 6 | import MultiView from '../../../multiView/MultiView'; 7 | import { dispose } from '../../../../utils/three'; 8 | 9 | type InspectAnimationProps = { 10 | object: RemoteObject; 11 | three: RemoteThree; 12 | } 13 | 14 | export default function InspectAnimation(props: InspectAnimationProps) { 15 | const object = props.object; 16 | const three = props.three; 17 | function expandedName(): string { 18 | return `${three.name}_animation`; 19 | } 20 | 21 | const expandedValue = localStorage.getItem(expandedName()); 22 | const expanded = expandedValue !== null ? expandedValue === 'open' : false; 23 | 24 | function saveExpanded(value: boolean) { 25 | localStorage.setItem(expandedName(), value ? 'open' : 'closed'); 26 | } 27 | 28 | const items: any[] = []; 29 | const animations: any[] = []; 30 | let maxDuration = 0; 31 | object.animations.forEach((clipInfo: AnimationClipInfo) => { 32 | // Add animation 33 | maxDuration = Math.max(maxDuration, clipInfo.duration); 34 | if (clipInfo.duration > 0) { 35 | animations.push({ 36 | title: clipInfo.name, 37 | items: [ 38 | { 39 | title: 'Duration', 40 | type: 'number', 41 | value: clipInfo.duration, 42 | disabled: true, 43 | }, 44 | { 45 | title: 'Blend Mode', 46 | type: 'option', 47 | disabled: true, 48 | options: [ 49 | { 50 | title: 'Normal', 51 | value: 2500, 52 | }, 53 | { 54 | title: 'Additive', 55 | value: 2501, 56 | }, 57 | ], 58 | } 59 | ] 60 | }); 61 | } 62 | }); 63 | items.push({ 64 | title: 'Animations', 65 | items: animations 66 | }); 67 | 68 | let helper: SkeletonHelper | undefined = undefined; 69 | const scene = three.getScene(object.uuid); 70 | if (scene !== null) { 71 | const child = scene.getObjectByProperty('uuid', object.uuid); 72 | if (child !== undefined) { 73 | const mixer = child['mixer'] as AnimationMixer; 74 | const hasMixer = mixer !== undefined; 75 | if (hasMixer) { 76 | const mixerItems: any[] = [ 77 | { 78 | title: 'Time Scale', 79 | type: 'range', 80 | value: mixer.timeScale, 81 | step: 0.01, 82 | min: -1, 83 | max: 2, 84 | onChange: (_: string, value: any) => { 85 | mixer.timeScale = value; 86 | three.updateObject(object.uuid, 'mixer.timeScale', value); 87 | }, 88 | }, 89 | ]; 90 | mixerItems.push({ 91 | title: 'Stop All', 92 | type: 'button', 93 | onChange: () => { 94 | mixer.stopAllAction(); 95 | three.requestMethod(object.uuid, 'stopAllAction', undefined, 'mixer'); 96 | } 97 | }); 98 | items.push({ 99 | title: 'Mixer', 100 | items: mixerItems 101 | }); 102 | 103 | helper = new SkeletonHelper(child); 104 | MultiView.instance?.scene.add(helper); 105 | } 106 | } 107 | } 108 | 109 | useEffect(() => { 110 | return () => { 111 | if (helper !== undefined) dispose(helper); 112 | }; 113 | }, []); 114 | 115 | return ( 116 | { 122 | saveExpanded(value); 123 | }} 124 | /> 125 | ); 126 | } 127 | -------------------------------------------------------------------------------- /src/example/three/loader.ts: -------------------------------------------------------------------------------- 1 | import { CubeTexture, CubeTextureLoader, Group, Object3D, RepeatWrapping, Texture, TextureLoader } from 'three'; 2 | // @ts-ignore 3 | import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'; 4 | import { Events, threeDispatcher } from '../constants'; 5 | import Application from '../../core/Application'; 6 | import RemoteTheatre from '../../core/remote/RemoteTheatre'; 7 | 8 | export const cubeTextures: Map = new Map(); 9 | export const json: Map = new Map(); 10 | export const models: Map = new Map(); 11 | export const textures: Map = new Map(); 12 | 13 | export function loadCube(name: string, source: string[]): Promise { 14 | return new Promise((resolve, reject) => { 15 | new CubeTextureLoader() 16 | .setPath('images/milkyWay/') 17 | .load(source, (value: CubeTexture) => { 18 | cubeTextures.set(name, value); 19 | resolve(value); 20 | }, undefined, () => { 21 | reject(); 22 | }); 23 | }); 24 | } 25 | 26 | export function loadModel(name: string, source: string): Promise { 27 | return new Promise((resolve, reject) => { 28 | new FBXLoader() 29 | .setPath('./models/') 30 | .loadAsync(source) 31 | .then((model: Group) => { 32 | // Shadows 33 | model.traverse((obj: Object3D) => { 34 | // @ts-ignore 35 | if (obj['isMesh']) { 36 | obj.castShadow = true; 37 | obj.receiveShadow = true; 38 | } 39 | }); 40 | 41 | models.set(name, model); 42 | resolve(model); 43 | }) 44 | .catch((reason: any) => { 45 | console.log(`Couldn't load:`, source); 46 | console.log(reason); 47 | reject(); 48 | }); 49 | }); 50 | } 51 | 52 | export function loadTexture(name: string, source: string): Promise { 53 | return new Promise((resolve, reject) => { 54 | new TextureLoader() 55 | .load(source, (value: Texture) => { 56 | value.wrapS = RepeatWrapping; 57 | value.wrapT = RepeatWrapping; 58 | value.needsUpdate = true; 59 | textures.set(name, value); 60 | resolve(value); 61 | }, undefined, () => { 62 | reject(); 63 | }); 64 | }); 65 | } 66 | 67 | export function loadJSON(name: string, source: string): Promise { 68 | return new Promise((resolve, reject) => { 69 | fetch(source) 70 | .then(response => { 71 | if (!response.ok) { 72 | throw new Error(`Network response was not ok: ${response.status}`); 73 | } 74 | return response.json(); 75 | }) 76 | .then(data => { 77 | json.set(name, data); 78 | resolve(data); 79 | }) 80 | .catch(() => { 81 | console.log(`Couldn't load: ${source}`); 82 | reject(); 83 | }); 84 | }); 85 | } 86 | 87 | export function loadAssets(app: Application): Promise { 88 | return new Promise((resolve, reject) => { 89 | const assets: (() => Promise)[] = [ 90 | () => loadCube('environment', [ 91 | 'dark-s_px.jpg', 92 | 'dark-s_nx.jpg', 93 | 'dark-s_py.jpg', 94 | 'dark-s_ny.jpg', 95 | 'dark-s_pz.jpg', 96 | 'dark-s_nz.jpg' 97 | ]), 98 | () => loadTexture('uv_grid', 'images/uv_grid_opengl.jpg'), 99 | () => loadModel('Flair', 'Flair.fbx'), 100 | () => loadJSON('animation', 'json/animation.json'), 101 | ]; 102 | 103 | Promise.all(assets.map(load => load())) 104 | .then(() => { 105 | const theatre = app.components.get('theatre') as RemoteTheatre; 106 | const state = json.get('animation'); 107 | theatre.loadProject('RemoteApp', state).then(() => { 108 | threeDispatcher.dispatchEvent({ type: Events.LOAD_COMPLETE }); 109 | resolve(); 110 | }); 111 | }) 112 | .catch((reason) => { 113 | console.log(reason); 114 | reject(); 115 | }); 116 | }); 117 | } 118 | -------------------------------------------------------------------------------- /src/editor/sidePanel/inspector/InspectImage.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | import { uploadLocalImage } from './utils/InspectMaterial'; 3 | import { noImage } from '../../../editor/components/content'; 4 | import { randomID } from '../../../editor/utils'; 5 | 6 | type InspectImageProps = { 7 | title: string; 8 | prop?: string; 9 | value?: any; 10 | step?: number; 11 | onChange?: (prop: string, value: any) => void; 12 | } 13 | 14 | export default function InspectImage(props: InspectImageProps) { 15 | const step = props.step !== undefined ? props.step : 0.01; 16 | // References 17 | const imgRefRef = useRef(null); 18 | const offXRef = useRef(null); 19 | const offYRef = useRef(null); 20 | const repeatXRef = useRef(null); 21 | const repeatYRef = useRef(null); 22 | 23 | // States 24 | const [fieldValue] = useState(props.value); 25 | const [offsetX, setOffsetX] = useState(props.value.offset[0]); 26 | const [offsetY, setOffsetY] = useState(props.value.offset[1]); 27 | const [repeatX, setRepeatX] = useState(props.value.repeat[0]); 28 | const [repeatY, setRepeatY] = useState(props.value.repeat[1]); 29 | 30 | function onChange(src: string, ox: number, oy: number, rx: number, ry: number) { 31 | if (props.onChange !== undefined) { 32 | const title = props.prop !== undefined ? props.prop : props.title; 33 | props.onChange(title, { 34 | src: src, 35 | offset: [ox, oy], 36 | repeat: [rx, ry], 37 | }); 38 | } 39 | } 40 | 41 | function changeInput(evt: any) { 42 | const src = imgRefRef.current!.src; 43 | const value = evt.target.value; 44 | switch (evt.target) { 45 | case offXRef.current: 46 | setOffsetX(value); 47 | onChange(src, value, offsetY, repeatX, repeatY); 48 | break; 49 | case offYRef.current: 50 | setOffsetY(value); 51 | onChange(src, offsetX, value, repeatX, repeatY); 52 | break; 53 | case repeatXRef.current: 54 | setRepeatX(value); 55 | onChange(src, offsetX, offsetY, value, repeatY); 56 | break; 57 | case repeatYRef.current: 58 | setRepeatY(value); 59 | onChange(src, offsetX, offsetY, repeatX, value); 60 | break; 61 | } 62 | } 63 | 64 | return ( 65 |
    66 | {props.title} { 67 | uploadLocalImage() 68 | .then((value: string) => { 69 | imgRefRef.current!.src = value; 70 | onChange(value, offsetX, offsetY, repeatX, repeatY); 71 | }); 72 | }} src={fieldValue.src.length > 0 ? fieldValue.src : noImage} /> 73 |
    74 |
    75 | Offset: 76 | 84 | 92 |
    93 |
    94 | Repeat: 95 | 103 | 111 |
    112 | 116 |
    117 |
    118 | ); 119 | } 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hermes 2 | 3 | An extendable set of Web Tools controlled over a separate window for non-intereference with content (like a remote controller!) 4 | 5 | Open the [Application](https://hermes-lovat.vercel.app/) and [editor](https://hermes-lovat.vercel.app/#editor) side-by-side. 6 | 7 | ## Setup 8 | 9 | This example uses [React](https://react.dev/), [ThreeJS](https://threejs.org/), and [TheatreJS](https://theatrejs.com/). 10 | 11 | ### Create an `Application` 12 | 13 | An application isn't required, however it's nice to maintain multiple remotes. Alternatively, Remotes can be created independently. 14 | 15 | The `ThreeEditor` is used as a multi-view editor for [ThreeJS](https://threejs.org/), and should be limited to only the Editor app. 16 | 17 | ``` 18 | const IS_DEV = true; 19 | const IS_EDITOR = IS_DEV && document.location.hash.search('editor') > -1; 20 | 21 | const theatre = new RemoteTheatre(IS_DEV, IS_EDITOR); 22 | const three = new RemoteThree('Hermes Example', IS_DEV, IS_EDITOR); 23 | 24 | export default function AppWrapper() { 25 | const [app, setApp] = useState(null); 26 | 27 | useEffect(() => { 28 | const instance = new Application(); 29 | instance.detectSettings(IS_DEV, IS_EDITOR).then(() => { 30 | // TheatreJS 31 | instance.addComponent('theatre', theatre); 32 | 33 | // ThreeJS 34 | instance.addComponent('three', three); 35 | 36 | // Ready 37 | setApp(instance); 38 | }); 39 | }, []); 40 | 41 | // MultiView requires you identify each scene so they can be instantiated by the editor 42 | const scenes: Map = new Map(); 43 | scenes.set('Scene1', Scene1); 44 | scenes.set('Scene2', Scene2); 45 | scenes.set('RTTScene', RTTScene); 46 | 47 | return ( 48 | <> 49 | {app !== null && ( 50 | <> 51 | {IS_DEV && ( 52 | <> 53 | {IS_EDITOR && ( 54 | { 58 | scene.update(); 59 | }} 60 | /> 61 | )} 62 | 63 | )} 64 | 65 | )} 66 | 67 | ); 68 | } 69 | ``` 70 | 71 | ### Scene setup 72 | 73 | After all object's have been added to your scene, run `hierarchyUUID(yourScene)` to update the UUIDs of every object. This helps communicate back and forth between the app and your editor. 74 | 75 | ### Custom remote commands 76 | 77 | This component is added only in debug-mode to add extra support for remote-components. 78 | 79 | In this example it's added to add custom Remote Component support for: 80 | 81 | - [TheatreJS](https://theatrejs.com/) - Communicates with the `studio` instance 82 | 83 | ``` 84 | type RemoteProps = { 85 | three: RemoteThree 86 | theatre: RemoteTheatre 87 | } 88 | 89 | export default function RemoteSetup(props: RemoteProps) { 90 | // Remote Theatre setup 91 | props.theatre.studio = studio; 92 | props.theatre.handleEditorApp(); 93 | return null; 94 | } 95 | ``` 96 | 97 | ## Editor 98 | 99 | ### Tools for: 100 | 101 | - Customizable Navigation Dropdowns + Draggable components for Triggers/Event Dispatching 102 | - [TheatreJS](https://www.theatrejs.com/) 103 | - [ThreeJS](https://threejs.org/) 104 | - Custom ThreeJS Scene + Object Inspector 105 | 106 | ### ThreeJS Editor 107 | 108 | | Action | Keys | 109 | | ------ | ------ | 110 | | Zoom to Selected Item | CTRL + 0 | 111 | | Rotate to Front of Selected Item | CTRL + 1 | 112 | | Rotate to Top of Selected Item | CTRL + 2 | 113 | | Rotate to Right of Selected Item | CTRL + 3 | 114 | | Rotate to Back of Selected Item | CTRL + 4 | 115 | | Set Transform Controls to Rotate | r | 116 | | Set Transform Controls to Scale | s | 117 | | Set Transform Controls to Translate | t | 118 | | Toggles Transform Controls between **world** and **local** | q | 119 | 120 | ### Side Panel 121 | 122 | Holding down the **CTRL** key while dragging a number's label will multiply the delta by 10 123 | 124 | ![Drag Multiplier](images/dragMultiplier.gif) 125 | 126 | ### Assets 127 | 128 | Animation / Models found at [Mixamo](https://www.mixamo.com/) 129 | -------------------------------------------------------------------------------- /src/webworkers/EventHandling.ts: -------------------------------------------------------------------------------- 1 | // Transfer Events 2 | 3 | type EventHandler = (event: Event, sendFn: (data: any) => void) => void; 4 | 5 | const mouseEventHandler = makeSendPropertiesHandler([ 6 | 'ctrlKey', 7 | 'metaKey', 8 | 'shiftKey', 9 | 'button', 10 | 'pointerId', 11 | 'pointerType', 12 | 'clientX', 13 | 'clientY', 14 | 'pageX', 15 | 'pageY', 16 | ]); 17 | 18 | const wheelEventHandlerImpl = makeSendPropertiesHandler([ 19 | 'clientX', 20 | 'clientY', 21 | 'deltaX', 22 | 'deltaY', 23 | 'deltaMode', 24 | ]); 25 | 26 | const keydownEventHandler = makeSendPropertiesHandler([ 27 | 'ctrlKey', 28 | 'metaKey', 29 | 'shiftKey', 30 | 'keyCode', 31 | ]); 32 | 33 | function wheelEventHandler(event: WheelEvent, sendFn: (data: any) => void): void { 34 | event.preventDefault(); 35 | wheelEventHandlerImpl(event, sendFn); 36 | } 37 | 38 | function preventDefaultHandler(event: Event): void { 39 | event.preventDefault(); 40 | } 41 | 42 | function copyProperties( 43 | src: Record, 44 | properties: string[], 45 | dst: Record 46 | ): void { 47 | for (const name of properties) { 48 | dst[name] = src[name]; 49 | } 50 | } 51 | 52 | function makeSendPropertiesHandler(properties: string[]): EventHandler { 53 | return function sendProperties(event: Event, sendFn: (data: any) => void): void { 54 | const data: Record = { type: event.type }; 55 | copyProperties(event as Record, properties, data); 56 | sendFn(data); 57 | }; 58 | } 59 | 60 | function touchEventHandler(event: TouchEvent, sendFn: (data: any) => void): void { 61 | const touches: Array<{ pageX: number; pageY: number }> = []; 62 | const data = { type: event.type, touches }; 63 | 64 | for (let i = 0; i < event.touches.length; ++i) { 65 | const touch = event.touches[i]; 66 | touches.push({ 67 | pageX: touch.pageX, 68 | pageY: touch.pageY, 69 | }); 70 | } 71 | 72 | sendFn(data); 73 | } 74 | 75 | // The four arrow keys 76 | const orbitKeys: Record = { 77 | '37': true, // left 78 | '38': true, // up 79 | '39': true, // right 80 | '40': true, // down 81 | }; 82 | 83 | function filteredKeydownEventHandler( 84 | event: KeyboardEvent, 85 | sendFn: (data: any) => void 86 | ): void { 87 | const { keyCode } = event; 88 | if (orbitKeys[keyCode]) { 89 | event.preventDefault(); 90 | keydownEventHandler(event, sendFn); 91 | } 92 | } 93 | 94 | // Proxy 95 | 96 | export const WebworkerEventHandlers: Record = { 97 | contextmenu: preventDefaultHandler, 98 | mousedown: mouseEventHandler, 99 | mousemove: mouseEventHandler, 100 | mouseup: mouseEventHandler, 101 | pointerdown: mouseEventHandler, 102 | pointermove: mouseEventHandler, 103 | pointerup: mouseEventHandler, 104 | touchstart: touchEventHandler, 105 | touchmove: touchEventHandler, 106 | touchend: touchEventHandler, 107 | wheel: wheelEventHandler, 108 | keydown: filteredKeydownEventHandler, 109 | }; 110 | 111 | let nextProxyId = 0; 112 | 113 | export class ElementProxy { 114 | id: number; 115 | worker: Worker; 116 | 117 | constructor( 118 | element: HTMLElement, 119 | worker: Worker, 120 | eventHandlers: Record 121 | ) { 122 | this.id = nextProxyId++; 123 | this.worker = worker; 124 | 125 | const sendEvent = (data: any): void => { 126 | this.worker.postMessage({ 127 | type: 'event', 128 | id: this.id, 129 | data, 130 | }); 131 | }; 132 | 133 | // Register an ID 134 | worker.postMessage({ 135 | type: 'makeProxy', 136 | id: this.id, 137 | }); 138 | 139 | for (const [eventName, handler] of Object.entries(eventHandlers)) { 140 | element.addEventListener(eventName, (event) => { 141 | handler(event as any, sendEvent); 142 | }); 143 | } 144 | 145 | function sendSize(): void { 146 | sendEvent({ 147 | type: 'resize', 148 | left: 0, 149 | top: 0, 150 | width: innerWidth, 151 | height: innerHeight, 152 | }); 153 | } 154 | 155 | // Really need to use ResizeObserver 156 | window.addEventListener('resize', sendSize); 157 | sendSize(); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/editor/sidePanel/ChildObject.tsx: -------------------------------------------------------------------------------- 1 | // Libs 2 | import { useEffect, useRef, useState } from 'react'; 3 | // Models 4 | import { ChildObjectProps, RemoteObject } from './types'; 5 | // Utils 6 | import { determineIcon, setItemProps } from './utils'; 7 | 8 | export default function ChildObject(props: ChildObjectProps) { 9 | if (props.child === undefined) { 10 | console.log(`Hermes - No child attached`); 11 | return null; 12 | } 13 | 14 | const visibleRef = useRef(null); 15 | const [open, setOpen] = useState(false); 16 | 17 | const hasChildren = props.child.children.length > 0; 18 | const children: Array = []; 19 | if (props.child.children.length > 0) { 20 | props.child.children.map((child: RemoteObject, index: number) => { 21 | children.push(); 22 | }); 23 | } 24 | 25 | useEffect(() => { 26 | if (props.child) { 27 | const sceneUUID = props.child.uuid.split('.')[0]; 28 | const scene = props.three.getScene(sceneUUID); 29 | if (scene !== null) { 30 | try { 31 | const child = scene.getObjectByProperty('uuid', props.child.uuid); 32 | if (child !== undefined) { 33 | visibleRef.current!.style.opacity = child.visible ? '1' : '0.25'; 34 | } else { 35 | console.log(`Hermes - Can't find child: ${props.child.uuid}`); 36 | } 37 | } catch (err: any) { 38 | console.log(`Error looking for child:`, err); 39 | console.log(props.child); 40 | console.log(props.three.scenes); 41 | console.log(scene); 42 | } 43 | } else { 44 | console.log(`Hermes (ChildObject) - Can't find Scene: ${sceneUUID} with child UUID: ${props.child.uuid}`, props.three.scenes, props.three.scene, scene); 45 | } 46 | } 47 | }, [open]); 48 | 49 | return ( 50 |
    51 |
    52 | {hasChildren ? ( 53 | 62 | ) : null} 63 | 81 | 104 |
    105 |
    106 |
    107 |
    108 | {children} 109 |
    110 |
    111 |
    112 | ); 113 | } -------------------------------------------------------------------------------- /src/editor/tools/Transform.ts: -------------------------------------------------------------------------------- 1 | // Libs 2 | import { Camera, EventDispatcher } from 'three'; 3 | import { TransformControls } from 'three/examples/jsm/controls/TransformControls'; 4 | // Remote 5 | import RemoteThree, { ToolEvents } from '../../core/remote/RemoteThree'; 6 | import MultiView from '../multiView/MultiView'; 7 | // Utils 8 | import { dispose } from '../../utils/three'; 9 | 10 | export default class Transform extends EventDispatcher { 11 | static DRAG_START = 'Transform::dragStart'; 12 | static DRAG_END = 'Transform::dragEnd'; 13 | 14 | private static _instance: Transform; 15 | 16 | three!: RemoteThree; 17 | activeCamera!: Camera; 18 | controls: Map = new Map(); 19 | 20 | private visibility: Map = new Map(); 21 | 22 | setApp(three: RemoteThree) { 23 | this.three = three; 24 | this.three.addEventListener(ToolEvents.SET_SCENE, this.setScene); 25 | } 26 | 27 | clear(): void { 28 | for (const controls of this.controls.values()) { 29 | controls.detach(); 30 | controls.disconnect(); 31 | const helper = controls.getHelper(); 32 | dispose(helper); 33 | } 34 | this.controls = new Map(); 35 | this.visibility = new Map(); 36 | } 37 | 38 | add(name: string): TransformControls { 39 | let controls = this.controls.get(name); 40 | if (controls === undefined) { 41 | const element = document.querySelector('.clickable') as HTMLDivElement; 42 | controls = new TransformControls(this.activeCamera, element); 43 | controls.getHelper().name = name; 44 | controls.setSize(0.5); 45 | controls.setSpace('local'); 46 | this.controls.set(name, controls); 47 | this.visibility.set(name, true); 48 | 49 | controls.addEventListener('mouseDown', () => { 50 | // @ts-ignore 51 | this.dispatchEvent({ type: Transform.DRAG_START }); 52 | }); 53 | controls.addEventListener('mouseUp', () => { 54 | // @ts-ignore 55 | this.dispatchEvent({ type: Transform.DRAG_END }); 56 | }); 57 | 58 | controls.addEventListener('dragging-changed', (evt: any) => { 59 | MultiView.instance?.toggleOrbitControls(evt.value); 60 | }); 61 | } 62 | return controls; 63 | } 64 | 65 | get(name: string): TransformControls | undefined { 66 | return this.controls.get(name); 67 | } 68 | 69 | remove(name: string): boolean { 70 | const controls = this.get(name); 71 | if (controls === undefined) return false; 72 | 73 | controls.detach(); 74 | controls.disconnect(); 75 | dispose(controls.getHelper()); 76 | this.controls.delete(name); 77 | return true; 78 | } 79 | 80 | enabled(value: boolean) { 81 | this.controls.forEach((controls: TransformControls) => { 82 | controls.enabled = value; 83 | }); 84 | } 85 | 86 | updateCamera(camera: Camera, element: HTMLElement): void { 87 | this.activeCamera = camera; 88 | this.controls.forEach((controls: TransformControls) => { 89 | // Update camera 90 | if (controls.camera !== camera) { 91 | controls.camera = camera; 92 | // @ts-ignore 93 | camera.getWorldPosition(controls.cameraPosition); 94 | // @ts-ignore 95 | camera.getWorldQuaternion(controls.cameraQuaternion); 96 | } 97 | 98 | // Update element 99 | if (controls.domElement !== element) { 100 | controls.disconnect(); 101 | controls.domElement = element; 102 | controls.connect(element); 103 | } 104 | }); 105 | } 106 | 107 | show() { 108 | this.controls.forEach((controls: TransformControls) => { 109 | const helper = controls.getHelper(); 110 | const value = this.visibility.get(helper.name); 111 | if (value !== undefined) helper.visible = value; 112 | }); 113 | } 114 | 115 | hide() { 116 | this.controls.forEach((controls: TransformControls) => { 117 | const helper = controls.getHelper(); 118 | this.visibility.set(helper.name, helper.visible); 119 | helper.visible = false; 120 | }); 121 | } 122 | 123 | private setScene = () => { 124 | this.clear(); 125 | }; 126 | 127 | public static get instance(): Transform { 128 | if (!Transform._instance) { 129 | Transform._instance = new Transform(); 130 | } 131 | return Transform._instance; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/editor/sidePanel/SidePanel.tsx: -------------------------------------------------------------------------------- 1 | // Libs 2 | import { useEffect, useState } from 'react'; 3 | // Models 4 | import { RemoteObject, SidePanelState } from './types'; 5 | // Components 6 | import '../scss/sidePanel.scss'; 7 | import Accordion from './Accordion'; 8 | import ContainerObject from './ContainerObject'; 9 | import DebugData from './DebugData'; 10 | import Inspector from './inspector/Inspector'; 11 | import InspectRenderer from './inspector/utils/InspectRenderer'; 12 | import { ToolEvents } from '../../core/remote/RemoteThree'; 13 | 14 | export default function SidePanel(props: SidePanelState) { 15 | const [scenes] = useState([]); 16 | const [sceneComponents] = useState([]); 17 | const [lastUpdate, setLastUpdate] = useState(0); 18 | 19 | const onAddScene = (evt: any) => { 20 | const scene = evt.value; 21 | scenes.push(scene); 22 | sceneComponents.push( 23 | { 31 | props.three.refreshScene(scene.name); 32 | }} 33 | > 34 | 35 | 36 | ); 37 | setLastUpdate(Date.now()); 38 | }; 39 | 40 | const onRefreshScene = (evt: any) => { 41 | const scene = evt.value; 42 | for (let i = 0; i < scenes.length; i++) { 43 | if (scene.uuid === scenes[i].uuid) { 44 | scenes[i] = scene; 45 | sceneComponents[i] = ( 46 | { 54 | props.three.refreshScene(scene.name); 55 | }} 56 | > 57 | 58 | 59 | ); 60 | setLastUpdate(Date.now()); 61 | return; 62 | } 63 | } 64 | }; 65 | 66 | const onRemoveScene = (evt: any) => { 67 | const scene = evt.value; 68 | for (let i = 0; i < scenes.length; i++) { 69 | if (scene.uuid === scenes[i].uuid) { 70 | scenes.splice(i, 1); 71 | sceneComponents.splice(i, 1); 72 | setLastUpdate(Date.now()); 73 | return; 74 | } 75 | } 76 | }; 77 | 78 | const onSetScene = (evt: any) => { 79 | const name = evt.value.name; 80 | for (let i = 0; i < scenes.length; i++) { 81 | const scene = scenes[i]; 82 | const isActive = scene.name === name; 83 | sceneComponents[i] = ( 84 | { 92 | props.three.refreshScene(scene.name); 93 | }} 94 | > 95 | 96 | 97 | ); 98 | } 99 | setLastUpdate(Date.now()); 100 | }; 101 | 102 | useEffect(() => { 103 | props.three.addEventListener(ToolEvents.ADD_SCENE, onAddScene); 104 | props.three.addEventListener(ToolEvents.SET_SCENE, onSetScene); 105 | props.three.addEventListener(ToolEvents.REFRESH_SCENE, onRefreshScene); 106 | props.three.addEventListener(ToolEvents.REMOVE_SCENE, onRemoveScene); 107 | return () => { 108 | props.three.removeEventListener(ToolEvents.ADD_SCENE, onAddScene); 109 | props.three.removeEventListener(ToolEvents.SET_SCENE, onSetScene); 110 | props.three.removeEventListener(ToolEvents.REFRESH_SCENE, onRefreshScene); 111 | props.three.removeEventListener(ToolEvents.REMOVE_SCENE, onRemoveScene); 112 | }; 113 | }, []); 114 | 115 | return ( 116 |
    117 |
    118 | {sceneComponents} 119 |
    120 | 121 | 122 | 123 |
    124 | ); 125 | } 126 | -------------------------------------------------------------------------------- /src/example/components/App.tsx: -------------------------------------------------------------------------------- 1 | // Libs 2 | import { useEffect, useRef } from 'react'; 3 | import { WebGLRenderer } from 'three'; 4 | import WebGPURenderer from 'three/src/renderers/webgpu/WebGPURenderer.js'; 5 | import Stats from 'stats-gl'; 6 | // Models 7 | import Application from '../../core/Application'; 8 | // Components 9 | import RemoteThree from '../../core/remote/RemoteThree'; 10 | // Three 11 | import BaseScene from '../three/scenes/BaseScene'; 12 | import Scene1 from '../three/scenes/Scene1'; 13 | import Scene2 from '../three/scenes/Scene2'; 14 | // Utils 15 | import { dispose } from '../../utils/three'; 16 | import { clearComposerGroups } from '../../utils/post'; 17 | 18 | let renderer: WebGLRenderer | WebGPURenderer; 19 | let currentScene: BaseScene; 20 | let sceneName = ''; 21 | 22 | type AppProps = { 23 | app: Application 24 | } 25 | 26 | function App(props: AppProps) { 27 | const app = props.app; 28 | const canvasRef = useRef(null); 29 | const three = props.app.components.get('three') as RemoteThree; 30 | 31 | console.log('Settings', app.settings); 32 | 33 | // Renderer setup 34 | if (app.isApp) { 35 | useEffect(() => { 36 | const canvas = canvasRef.current!; 37 | // TODO - Add WebGPU support 38 | const useWebGPU = false; 39 | if (useWebGPU) { 40 | renderer = new WebGPURenderer({ 41 | canvas, 42 | stencil: false 43 | }); 44 | } else { 45 | renderer = new WebGLRenderer({ 46 | canvas, 47 | stencil: false 48 | }); 49 | } 50 | renderer.shadowMap.enabled = true; 51 | renderer.setPixelRatio(devicePixelRatio); 52 | renderer.setClearColor(0x000000); 53 | three.setRenderer(renderer, canvas); 54 | 55 | // ThreeJS 56 | const stats = new Stats(); 57 | stats.init(renderer); 58 | document.body.appendChild(stats.dom); 59 | 60 | // Start RAF 61 | let raf = -1; 62 | 63 | const onResize = () => { 64 | const width = window.innerWidth; 65 | const height = window.innerHeight; 66 | currentScene?.resize(width, height); 67 | renderer.setSize(width, height); 68 | }; 69 | 70 | const updateApp = () => { 71 | currentScene?.update(); 72 | currentScene?.draw(); 73 | stats.update(); 74 | raf = requestAnimationFrame(updateApp); 75 | }; 76 | 77 | window.addEventListener('resize', onResize); 78 | onResize(); 79 | updateApp(); 80 | 81 | // Dispose 82 | return () => { 83 | if (currentScene !== undefined) { 84 | three.removeCamera(currentScene.camera); 85 | three.removeScene(currentScene); 86 | dispose(currentScene); 87 | } 88 | window.removeEventListener('resize', onResize); 89 | cancelAnimationFrame(raf); 90 | raf = -1; 91 | renderer.dispose(); 92 | }; 93 | }, []); 94 | } 95 | 96 | // Load the scenes 97 | 98 | const createScene = () => { 99 | if (currentScene !== undefined) { 100 | if (currentScene.camera !== undefined) three.removeCamera(currentScene.camera); 101 | three.removeScene(currentScene); 102 | dispose(currentScene); 103 | clearComposerGroups(three); 104 | } 105 | if (sceneName === 'scene1') { 106 | currentScene = new Scene1(); 107 | } else { 108 | currentScene = new Scene2(); 109 | } 110 | currentScene.setup(app, renderer); 111 | currentScene.init(); 112 | currentScene.resize(window.innerWidth, window.innerHeight); 113 | }; 114 | 115 | const createScene1 = () => { 116 | if (sceneName === 'scene1') return; 117 | sceneName = 'scene1'; 118 | createScene(); 119 | }; 120 | 121 | const createScene2 = () => { 122 | if (sceneName === 'scene2') return; 123 | sceneName = 'scene2'; 124 | createScene(); 125 | }; 126 | 127 | return ( 128 | <> 129 | {app.isApp && ( 130 | <> 131 | 132 |
    137 | 138 | 139 |
    140 | 141 | )} 142 | 143 | ); 144 | } 145 | 146 | export default App; -------------------------------------------------------------------------------- /src/editor/sidePanel/inspector/Inspector.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { CoreComponentProps, RemoteObject } from '../types'; 3 | // Components 4 | import './inspector.scss'; 5 | import Accordion from '../Accordion'; 6 | import InspectorField from './InspectorField'; 7 | // Utils 8 | import { InspectCamera } from './utils/InspectCamera'; 9 | import { InspectMaterial } from './utils/InspectMaterial'; 10 | import { InspectTransform } from './utils/InspectTransform'; 11 | import { InspectLight } from './utils/InspectLight'; 12 | import InspectAnimation from './utils/InspectAnimation'; 13 | import Transform from '../../../editor/tools/Transform'; 14 | import { ToolEvents } from '../../../core/remote/RemoteThree'; 15 | 16 | const defaultObject: RemoteObject = { 17 | name: '', 18 | uuid: '', 19 | type: '', 20 | visible: false, 21 | matrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 22 | animations: [], 23 | material: undefined, 24 | perspectiveCameraInfo: undefined, 25 | orthographicCameraInfo: undefined, 26 | lightInfo: undefined, 27 | children: [], 28 | }; 29 | 30 | export default function Inspector(props: CoreComponentProps) { 31 | const [currentObject, setCurrentObject] = useState(defaultObject); 32 | 33 | useEffect(() => { 34 | function onSelectItem(evt: any) { 35 | setCurrentObject(evt.value as RemoteObject); 36 | } 37 | 38 | function setScene() { 39 | setCurrentObject(defaultObject); 40 | } 41 | 42 | props.three.addEventListener(ToolEvents.CLEAR_OBJECT, setScene); 43 | props.three.addEventListener(ToolEvents.SET_SCENE, setScene); 44 | props.three.addEventListener(ToolEvents.SET_OBJECT, onSelectItem); 45 | return () => { 46 | props.three.removeEventListener(ToolEvents.CLEAR_OBJECT, setScene); 47 | props.three.removeEventListener(ToolEvents.SET_SCENE, setScene); 48 | props.three.removeEventListener(ToolEvents.SET_OBJECT, onSelectItem); 49 | }; 50 | }, []); 51 | 52 | const objType = currentObject.type.toLowerCase(); 53 | const hasAnimation = currentObject.animations.length > 0 54 | || currentObject['mixer'] !== undefined; 55 | const hasMaterial = objType.search('mesh') > -1 56 | || objType.search('line') > -1 57 | || objType.search('points') > -1; 58 | 59 | return ( 60 | 0 ? ( 66 | 70 | ) : undefined 71 | } 72 | > 73 |
    74 | {currentObject.uuid.length > 0 && ( 75 | <> 76 | {/* Core */} 77 | <> 78 | 85 | 92 | 99 | 100 | 101 | {/* Data */} 102 | <> 103 | {/* Transform */} 104 | 105 | {/* Animations */} 106 | {hasAnimation ? : null} 107 | {/* Cameras */} 108 | {objType.search('camera') > -1 ? InspectCamera(currentObject, props.three) : null} 109 | {/* Lights */} 110 | {objType.search('light') > -1 ? InspectLight(currentObject, props.three) : null} 111 | {/* Material */} 112 | {hasMaterial ? InspectMaterial(currentObject, props.three) : null} 113 | 114 | 115 | )} 116 |
    117 |
    118 | ); 119 | } -------------------------------------------------------------------------------- /src/editor/sidePanel/inspector/utils/InspectTransform.tsx: -------------------------------------------------------------------------------- 1 | import { Euler, Matrix4, Vector3 } from 'three'; 2 | import { Component, ReactNode } from 'react'; 3 | import InspectorGroup from '../InspectorGroup'; 4 | import { RemoteObject } from '../../types'; 5 | import { setItemProps } from '../../utils'; 6 | import MultiView from '../../../multiView/MultiView'; 7 | import RemoteThree from '../../../../core/remote/RemoteThree'; 8 | import { roundTo } from '../../../../utils/math'; 9 | 10 | type InspectTransformProps = { 11 | object: RemoteObject; 12 | three: RemoteThree; 13 | } 14 | 15 | type InspectTransformState = { 16 | lastUpdated: number; 17 | expanded: boolean; 18 | } 19 | 20 | export class InspectTransform extends Component { 21 | static instance: InspectTransform; 22 | 23 | matrix = new Matrix4(); 24 | position = new Vector3(); 25 | rotation = new Euler(); 26 | scale = new Vector3(); 27 | open = false; 28 | 29 | constructor(props: InspectTransformProps) { 30 | super(props); 31 | 32 | const expandedValue = localStorage.getItem(this.expandedName); 33 | const expanded = expandedValue !== null ? expandedValue === 'open' : false; 34 | this.open = expanded; 35 | this.saveExpanded(); 36 | 37 | this.state = { 38 | lastUpdated: 0, 39 | expanded: expanded, 40 | }; 41 | 42 | // @ts-ignore 43 | this.matrix.elements = props.object.matrix; 44 | if (props.object.uuid.length > 0) { 45 | this.position.setFromMatrixPosition(this.matrix); 46 | this.rotation.setFromRotationMatrix(this.matrix); 47 | this.scale.setFromMatrixScale(this.matrix); 48 | } 49 | 50 | InspectTransform.instance = this; 51 | } 52 | 53 | update() { 54 | if (MultiView.instance) { 55 | const selectedItem = MultiView.instance.selectedItem; 56 | if (selectedItem === undefined) return; 57 | this.position.x = roundTo(selectedItem.position.x, 3); 58 | this.position.y = roundTo(selectedItem.position.y, 3); 59 | this.position.z = roundTo(selectedItem.position.z, 3); 60 | this.rotation.copy(selectedItem.rotation); 61 | this.scale.x = roundTo(selectedItem.scale.x, 3); 62 | this.scale.y = roundTo(selectedItem.scale.y, 3); 63 | this.scale.z = roundTo(selectedItem.scale.z, 3); 64 | this.setState({ lastUpdated: Date.now() }); 65 | } 66 | } 67 | 68 | render(): ReactNode { 69 | return ( 70 | { 107 | this.open = value; 108 | this.saveExpanded(); 109 | }} 110 | /> 111 | ); 112 | } 113 | 114 | private updateTransform = (prop: string, value: any) => { 115 | const realValue = prop === 'rotation' ? { x: value._x, y: value._y, z: value._z } : value; 116 | 117 | // App 118 | this.props.three.updateObject(this.props.object.uuid, prop, realValue); 119 | 120 | // Editor 121 | const scene = this.props.three.getScene(this.props.object.uuid); 122 | if (scene) { 123 | const child = scene.getObjectByProperty('uuid', this.props.object.uuid); 124 | setItemProps(child, prop, realValue); 125 | } 126 | }; 127 | 128 | private saveExpanded() { 129 | localStorage.setItem(this.expandedName, this.open ? 'open' : 'closed'); 130 | } 131 | 132 | get expandedName(): string { 133 | return `${this.props.three.name}_transform`; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /types/editor/multiView/MultiView.d.ts: -------------------------------------------------------------------------------- 1 | import { Component, ReactNode } from 'react'; 2 | import { Camera, Group, Object3D, OrthographicCamera, PerspectiveCamera, Scene, WebGLRenderer } from 'three'; 3 | import WebGPURenderer from 'three/src/renderers/webgpu/WebGPURenderer'; 4 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; 5 | import RemoteThree from '../../core/remote/RemoteThree'; 6 | import InfiniteGridHelper from './InfiniteGridHelper'; 7 | import { InteractionMode, MultiViewMode } from './MultiViewData'; 8 | import './MultiView.scss'; 9 | type MultiViewProps = { 10 | three: RemoteThree; 11 | scenes: Map; 12 | onSceneAdd?: (scene: Scene) => void; 13 | onSceneUpdate?: (scene: Scene) => void; 14 | onSceneResize?: (scene: Scene, width: number, height: number) => void; 15 | }; 16 | type MultiViewState = { 17 | mode: MultiViewMode; 18 | modeOpen: boolean; 19 | renderModeOpen: boolean; 20 | interactionMode: InteractionMode; 21 | interactionModeOpen: boolean; 22 | lastUpdate: number; 23 | }; 24 | export default class MultiView extends Component { 25 | static instance: MultiView | null; 26 | scene: Scene; 27 | renderer?: WebGLRenderer | WebGPURenderer | null; 28 | currentScene?: Scene; 29 | scenes: Map; 30 | cameras: Map; 31 | controls: Map; 32 | currentCamera: PerspectiveCamera | OrthographicCamera; 33 | currentWindow: any; 34 | helpersContainer: Group; 35 | grid: InfiniteGridHelper; 36 | private cameraHelpers; 37 | private lightHelpers; 38 | private interactionHelper; 39 | private currentTransform?; 40 | private splineEditor; 41 | private depthMaterial; 42 | private normalsMaterial; 43 | private uvMaterial; 44 | private wireframeMaterial; 45 | private playing; 46 | private rafID; 47 | private cameraControlsRafID; 48 | private width; 49 | private height; 50 | private tlCam; 51 | private trCam; 52 | private blCam; 53 | private brCam; 54 | private tlRender; 55 | private trRender; 56 | private blRender; 57 | private brRender; 58 | private cameraVisibility; 59 | private lightVisibility; 60 | private gridVisibility; 61 | selectedItem: Object3D | undefined; 62 | private debugCamera; 63 | private raycaster; 64 | private pointer; 65 | private cameraControls; 66 | private canvasRef; 67 | private containerRef; 68 | private tlWindow; 69 | private trWindow; 70 | private blWindow; 71 | private brWindow; 72 | private editorCameras; 73 | constructor(props: MultiViewProps); 74 | componentDidMount(): void; 75 | componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void; 76 | componentWillUnmount(): void; 77 | render(): ReactNode; 78 | private setupRenderer; 79 | private setupScene; 80 | private setupTools; 81 | play(): void; 82 | pause(): void; 83 | toggleOrbitControls(value: boolean): void; 84 | clear(): void; 85 | setGridVisibility(value: boolean): void; 86 | private update; 87 | private draw; 88 | private onUpdate; 89 | private enable; 90 | private disable; 91 | private resize; 92 | private addScene; 93 | private sceneUpdate; 94 | private removeScene; 95 | private addCamera; 96 | private removeCamera; 97 | private onMouseMove; 98 | private onClick; 99 | private onKey; 100 | private onSetSelectedItem; 101 | private updateSelectedItemHelper; 102 | private onUpdateTransform; 103 | private clearLightHelpers; 104 | private addLightHelpers; 105 | private createControls; 106 | private clearCamera; 107 | private killControls; 108 | private assignControls; 109 | private updateCamera; 110 | private updateCameraControls; 111 | private clearControls; 112 | private saveExpandedCameraVisibility; 113 | private saveExpandedLightVisibility; 114 | private saveExpandedGridVisibility; 115 | private getSceneOverride; 116 | private drawTo; 117 | private drawSingle; 118 | private drawDouble; 119 | private drawQuad; 120 | get appID(): string; 121 | get mode(): MultiViewMode; 122 | get three(): RemoteThree; 123 | get expandedCameraVisibility(): string; 124 | get expandedLightVisibility(): string; 125 | get expandedGridVisibility(): string; 126 | } 127 | export {}; 128 | -------------------------------------------------------------------------------- /src/example/three/CustomShaderMaterial.ts: -------------------------------------------------------------------------------- 1 | import { Color, Euler, Matrix3, Matrix4, ShaderMaterial, Texture, Vector2, Vector3, Vector4 } from 'three'; 2 | import { textureFromSrc } from '../../editor/sidePanel/utils'; 3 | 4 | const vertex = `varying vec2 vUv; 5 | 6 | void main() { 7 | vUv = uv; 8 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 9 | }`; 10 | 11 | const fragment = `struct Light { 12 | float intensity; 13 | vec3 position; 14 | vec3 color; 15 | }; 16 | 17 | uniform float time; 18 | uniform float opacity; 19 | uniform vec2 resolution; 20 | uniform vec3 diffuse; 21 | uniform vec3 mouse; 22 | uniform sampler2D map; 23 | uniform Light light; 24 | uniform Light lights[3]; 25 | varying vec2 vUv; 26 | 27 | #define MIN_ALPHA 2.0 / 255.0 28 | 29 | void main() { 30 | if (opacity < MIN_ALPHA) discard; 31 | vec2 imageUV = vUv * 10.0; 32 | vec3 image = texture2D(map, imageUV).rgb; 33 | vec3 col = image * diffuse; 34 | col += (sin(time * 0.1) * 0.5 + 0.5) * 0.2; 35 | col += vec3(resolution, 0.0); 36 | col += mouse; 37 | gl_FragColor = vec4(col, opacity); 38 | }`; 39 | 40 | // eslint-disable-next-line max-len 41 | const smile = `data:image/png;base64,R0lGODlhCAAIAIAAAP///wAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDEgNzkuMTQ2Mjg5OTc3NywgMjAyMy8wNi8yNS0yMzo1NzoxNCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI1LjIgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NjVENjQyODM4QTYzMTFFRUJFQTBFOTJFNjA1OTQ5N0YiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NjVENjQyODQ4QTYzMTFFRUJFQTBFOTJFNjA1OTQ5N0YiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo2NUQ2NDI4MThBNjMxMUVFQkVBMEU5MkU2MDU5NDk3RiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo2NUQ2NDI4MjhBNjMxMUVFQkVBMEU5MkU2MDU5NDk3RiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PgH//v38+/r5+Pf29fTz8vHw7+7t7Ovq6ejn5uXk4+Lh4N/e3dzb2tnY19bV1NPS0dDPzs3My8rJyMfGxcTDwsHAv769vLu6ubi3trW0s7KxsK+urayrqqmop6alpKOioaCfnp2cm5qZmJeWlZSTkpGQj46NjIuKiYiHhoWEg4KBgH9+fXx7enl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmFgX15dXFtaWVhXVlVUU1JRUE9OTUxLSklIR0ZFRENCQUA/Pj08Ozo5ODc2NTQzMjEwLy4tLCsqKSgnJiUkIyIhIB8eHRwbGhkYFxYVFBMSERAPDg0MCwoJCAcGBQQDAgEAACH5BAAAAAAALAAAAAAIAAgAAAIOjH8AprzRkHyspqSoNAUAOw==`; 42 | 43 | export default class CustomShaderMaterial extends ShaderMaterial { 44 | constructor() { 45 | super({ 46 | vertexShader: vertex, 47 | fragmentShader: fragment, 48 | name: 'ExampleScene/SimpleShader', 49 | transparent: true, 50 | uniforms: { 51 | diffuse: { 52 | value: new Color(0xffffff) 53 | }, 54 | opacity: { 55 | value: 1, 56 | }, 57 | time: { 58 | value: 0, 59 | }, 60 | map: { 61 | value: null, 62 | }, 63 | resolution: { 64 | value: new Vector2(), 65 | }, 66 | mouse: { 67 | value: new Vector3() 68 | }, 69 | v4: { 70 | value: new Vector4() 71 | }, 72 | euler: { 73 | value: new Euler() 74 | }, 75 | testM3: { 76 | value: new Matrix3() 77 | }, 78 | testM4: { 79 | value: new Matrix4() 80 | }, 81 | light: { 82 | value: { 83 | position: new Vector2(5, 10), 84 | intensity: 1, 85 | color: new Color(0xff00ff), 86 | } 87 | }, 88 | lights: { 89 | value: [ 90 | { 91 | position: new Vector2(1, 1), 92 | intensity: 1, 93 | color: new Color(0xff0000), 94 | }, 95 | { 96 | position: new Vector2(2, 2), 97 | intensity: 2, 98 | color: new Color(0x00ff00), 99 | }, 100 | { 101 | position: new Vector2(3, 3), 102 | intensity: 3, 103 | color: new Color(0x0000ff), 104 | }, 105 | ], 106 | }, 107 | }, 108 | }); 109 | 110 | textureFromSrc(smile).then((texture: Texture) => { 111 | this.uniforms.map.value = texture; 112 | }); 113 | } 114 | 115 | update(delta: number) { 116 | this.uniforms.opacity.value = this.opacity; 117 | this.uniforms.time.value += delta; 118 | } 119 | } 120 | --------------------------------------------------------------------------------