├── .eslintrc.cjs ├── .gitignore ├── README.md ├── index.html ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── font │ └── Inter_Bold.json ├── img │ ├── favicon.png │ ├── no-image.png │ └── none-texture.jpg └── svg │ └── spotlight-svgrepo-com.svg ├── src ├── components │ ├── App.tsx │ ├── form │ │ ├── InputMaterial.tsx │ │ ├── InputModel.tsx │ │ ├── InputSound.tsx │ │ ├── InputTexture.tsx │ │ ├── PropertiesForm.tsx │ │ └── SelectMaterial.tsx │ ├── general │ │ ├── Actions.tsx │ │ ├── CameraControls.tsx │ │ ├── ContextMenu.tsx │ │ ├── EditorControls.tsx │ │ ├── Element.tsx │ │ ├── ElementsRender.tsx │ │ ├── FileIcon.tsx │ │ ├── FolderIcon.tsx │ │ ├── GlbHelper.tsx │ │ ├── Helpers.tsx │ │ ├── Icon.tsx │ │ ├── Layout.tsx │ │ ├── MeshMaterial.tsx │ │ └── Notifications.tsx │ ├── modal │ │ ├── ModalCommands.tsx │ │ ├── ModalExport.tsx │ │ ├── ModalHub.tsx │ │ ├── ModalNewDocument.tsx │ │ ├── ModalNewFile.tsx │ │ ├── ModalSearch.tsx │ │ ├── ModalSettings.tsx │ │ └── ModalTerms.tsx │ ├── properties │ │ └── DocumentProps.tsx │ ├── resources │ │ ├── AudioPicker.tsx │ │ ├── CodePicker.tsx │ │ ├── EnvironmentPicker.tsx │ │ ├── ImagePicker.tsx │ │ ├── MaterialPicker.tsx │ │ ├── Resources.tsx │ │ └── TexturePicker.tsx │ ├── screens │ │ ├── CodeEditor.tsx │ │ ├── Graph.tsx │ │ └── Mesa.tsx │ ├── toolbar │ │ ├── ToolbarBottom.tsx │ │ ├── ToolbarLeft.tsx │ │ ├── ToolbarRight.tsx │ │ └── ToolbarTop.tsx │ ├── ui │ │ ├── accordion.tsx │ │ ├── alert.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── checkbox.tsx │ │ ├── command.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── popover.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── slider.tsx │ │ ├── switch.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ └── tooltip.tsx │ └── window │ │ ├── History.tsx │ │ ├── Layers.tsx │ │ ├── LogContainer.tsx │ │ ├── MediaMaker.tsx │ │ ├── ProjectFiles.tsx │ │ └── Properties.tsx ├── constants │ ├── blueprint.json │ ├── commands.json │ ├── document.json │ ├── environment.json │ ├── general.json │ ├── index.ts │ ├── layout.json │ ├── light.json │ ├── material.json │ ├── materials.json │ ├── models.json │ ├── object.json │ ├── settings.json │ ├── sounds.json │ ├── textures.json │ ├── toolbar.json │ └── tree_theme.json ├── elements │ ├── general │ │ ├── Browser │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ │ ├── Environment │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ │ ├── Glb │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ │ ├── Image │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ │ ├── PositionalAudio │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ │ ├── Sky │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ │ ├── Stars │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ │ ├── Svg │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ │ ├── Text │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ │ └── Text3D │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ ├── geometry │ │ ├── BoxGeometry │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ │ ├── CapsuleGeometry │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ │ ├── CircleGeometry │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ │ ├── ConeGeometry │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ │ ├── CylinderGeometry │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ │ ├── DodecahedronGeometry │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ │ ├── ExtrudeGeometry │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ │ ├── IcosahedronGeometry │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ │ ├── LatheGeometry │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ │ ├── OctahedronGeometry │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ │ ├── PlaneGeometry │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ │ ├── RingGeometry │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ │ ├── SphereGeometry │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ │ ├── TetrahedronGeometry │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ │ ├── TorusGeometry │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ │ ├── TorusKnotGeometry │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ │ └── TubeGeometry │ │ │ ├── Controls.tsx │ │ │ ├── index.tsx │ │ │ └── props.ts │ ├── index.tsx │ └── light │ │ ├── AmbientLight │ │ ├── Controls.tsx │ │ ├── index.tsx │ │ └── props.ts │ │ ├── DirectionalLight │ │ ├── Controls.tsx │ │ ├── index.tsx │ │ └── props.ts │ │ ├── HemisphereLight │ │ ├── Controls.tsx │ │ ├── index.tsx │ │ └── props.ts │ │ ├── PointLight │ │ ├── Controls.tsx │ │ ├── index.tsx │ │ └── props.ts │ │ └── SpotLight │ │ ├── Controls.tsx │ │ ├── index.tsx │ │ └── props.ts ├── hooks │ ├── useBlueprint.ts │ ├── useCodeEditor.ts │ ├── useComponentFunctions.ts │ ├── useElement.ts │ ├── useKeyboardListener.ts │ ├── useModal.ts │ ├── usePlayer.ts │ ├── useProjectFiles.ts │ ├── useProviderControls.ts │ ├── useScrollToElement.ts │ ├── useSdk.ts │ ├── useStorageProvider.ts │ └── useToolbar.ts ├── lib │ └── utils.ts ├── main.tsx ├── matrix │ ├── element.ts │ └── material.ts ├── store │ ├── commands.ts │ ├── document.ts │ ├── elements.ts │ ├── files.ts │ ├── graph.ts │ ├── history.ts │ ├── layout.ts │ └── settings.ts ├── styles │ ├── SidebarCode.css │ ├── globals.css │ ├── index.css │ └── layout.css ├── types.ts ├── utils │ ├── clearLocalStorage.ts │ ├── getEditorLang.ts │ ├── getFunctionByName.ts │ ├── getObjectDiff.ts │ ├── getOpenedFiles.ts │ ├── mountNewElement.ts │ ├── mountToolbarActions.ts │ ├── neverUsed.ts │ └── processElements.ts └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:react-hooks/recommended', 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 10 | plugins: ['react-refresh'], 11 | rules: { 12 | 'react-refresh/only-export-components': 'warn', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /.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 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ThreeD Studio - Truly beautiful experiences on the web 8 | 9 | 10 |

11 | 12 | reset-document 13 | 14 |

15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "builder", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@mantine/hooks": "^6.0.11", 14 | "@monaco-editor/react": "^4.5.1", 15 | "@radix-ui/react-accordion": "^1.1.2", 16 | "@radix-ui/react-checkbox": "^1.0.4", 17 | "@radix-ui/react-dialog": "^1.0.4", 18 | "@radix-ui/react-dropdown-menu": "^2.0.5", 19 | "@radix-ui/react-label": "^2.0.2", 20 | "@radix-ui/react-popover": "^1.0.6", 21 | "@radix-ui/react-scroll-area": "^1.0.4", 22 | "@radix-ui/react-select": "^1.2.2", 23 | "@radix-ui/react-slider": "^1.1.2", 24 | "@radix-ui/react-slot": "^1.0.2", 25 | "@radix-ui/react-switch": "^1.0.3", 26 | "@radix-ui/react-tabs": "^1.0.4", 27 | "@radix-ui/react-tooltip": "^1.0.6", 28 | "@react-three/drei": "^9.68.3", 29 | "@react-three/fiber": "^8.13.0", 30 | "@react-three/postprocessing": "^2.11.4", 31 | "@tabler/icons-react": "^2.23.0", 32 | "@types/three": "^0.152.0", 33 | "@uiw/react-textarea-code-editor": "^2.1.6", 34 | "animate.css": "^4.1.1", 35 | "class-variance-authority": "^0.6.0", 36 | "clsx": "^1.2.1", 37 | "cmdk": "^0.2.0", 38 | "console-feed": "^3.5.0", 39 | "leva": "^0.9.35", 40 | "lucide-react": "^0.241.0", 41 | "nanoid": "^4.0.2", 42 | "r3f-perf": "^7.1.2", 43 | "react": "^18.2.0", 44 | "react-accessible-treeview": "^2.5.4", 45 | "react-cmdk": "^1.3.9", 46 | "react-color": "^2.19.3", 47 | "react-dom": "^18.2.0", 48 | "react-headless-pagination": "^1.1.4", 49 | "react-hot-toast": "^2.4.1", 50 | "react-json-tree": "^0.18.0", 51 | "reactflow": "^11.7.2", 52 | "tailwind-merge": "^1.13.1", 53 | "tailwindcss-animate": "^1.0.6", 54 | "three": "^0.152.2", 55 | "utility-types": "^3.10.0", 56 | "zustand": "^4.3.8" 57 | }, 58 | "devDependencies": { 59 | "@types/react": "^18.0.28", 60 | "@types/react-color": "^3.0.6", 61 | "@types/react-dom": "^18.0.11", 62 | "@types/uuid": "^9.0.1", 63 | "@typescript-eslint/eslint-plugin": "^5.57.1", 64 | "@typescript-eslint/parser": "^5.57.1", 65 | "@vitejs/plugin-react": "^4.0.0", 66 | "autoprefixer": "^10.4.14", 67 | "eslint": "^8.38.0", 68 | "eslint-plugin-react-hooks": "^4.6.0", 69 | "eslint-plugin-react-refresh": "^0.3.4", 70 | "icons-react@latest": "link:@types/@tabler/icons-react@latest", 71 | "postcss": "^8.4.24", 72 | "tailwindcss": "^3.3.2", 73 | "typescript": "^5.0.2", 74 | "vite": "^4.3.2", 75 | "vite-tsconfig-paths": "^4.2.0" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-threed/studio/a69dcfe3d2ef67c439caf96c8de0bd1ba3e3da5d/public/img/favicon.png -------------------------------------------------------------------------------- /public/img/no-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-threed/studio/a69dcfe3d2ef67c439caf96c8de0bd1ba3e3da5d/public/img/no-image.png -------------------------------------------------------------------------------- /public/img/none-texture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-threed/studio/a69dcfe3d2ef67c439caf96c8de0bd1ba3e3da5d/public/img/none-texture.jpg -------------------------------------------------------------------------------- /src/components/form/InputMaterial.tsx: -------------------------------------------------------------------------------- 1 | export default function InputMaterial() { 2 | return ( 3 |
4 | lorem 5 |
6 | ) 7 | } -------------------------------------------------------------------------------- /src/components/form/InputModel.tsx: -------------------------------------------------------------------------------- 1 | export default function InputModel() { 2 | return ( 3 |
4 | lorem 5 |
6 | ) 7 | } -------------------------------------------------------------------------------- /src/components/form/InputSound.tsx: -------------------------------------------------------------------------------- 1 | import { IconFileMusic } from "@tabler/icons-react" 2 | import CONSTANTS from "../../constants" 3 | import { useElementsStore } from "../../store/elements" 4 | import { Button } from "@/components/ui/button" 5 | import { 6 | Popover, 7 | PopoverContent, 8 | PopoverTrigger, 9 | } from "@/components/ui/popover" 10 | 11 | import { ScrollArea } from "@/components/ui/scroll-area" 12 | 13 | export default function InputSound() { 14 | const { current, updateCurrent } = useElementsStore() 15 | 16 | function handleChange(value:string) { 17 | updateCurrent({ 18 | soundUrl: value 19 | }) 20 | } 21 | 22 | return ( 23 | <> 24 | 25 | 26 | 32 | 33 | 34 | 35 |
36 |
37 | {CONSTANTS.sounds.map((item:string) => ( 38 |
39 |
handleChange(item)} 42 | > 43 | 44 |

45 | {item?.replaceAll('-', ' ')} 46 |

47 |
48 |
49 | ))} 50 |
51 |
52 |
53 |
54 |
55 | 56 | ) 57 | } -------------------------------------------------------------------------------- /src/components/form/InputTexture.tsx: -------------------------------------------------------------------------------- 1 | import CONSTANTS from "../../constants" 2 | import { useElementsStore } from "../../store/elements" 3 | import { Button } from "@/components/ui/button" 4 | 5 | import { 6 | Popover, 7 | PopoverContent, 8 | PopoverTrigger, 9 | } from "@/components/ui/popover" 10 | 11 | import { ScrollArea } from "@/components/ui/scroll-area" 12 | 13 | export default function InputTexture() { 14 | const { current, updateElement } = useElementsStore() 15 | 16 | const handleChange = (value:string) => { 17 | updateElement({ 18 | id: current.id, 19 | texture: value 20 | }) 21 | } 22 | 23 | return ( 24 | <> 25 | 26 | 27 | 30 | 31 | 32 | 33 |
34 |
35 |
36 |
handleChange('none')} 38 | style={{ 39 | borderColor: current.texture !== 'none' ?'transparent':'white' 40 | }}> 41 |

none

42 |
43 |
44 | {CONSTANTS.textures.map((item:string) => ( 45 |
46 |
handleChange(item)} 48 | style={{ 49 | backgroundImage: `url(${CONSTANTS.cdn}/textures/wood_board/${item}.jpg)`, 50 | borderColor: current.texture !== item ?'transparent':'white' 51 | }}> 52 |

{item}

53 |
54 |
55 | ))} 56 |
57 |
58 |
59 |
60 |
61 | 62 | ) 63 | } -------------------------------------------------------------------------------- /src/components/form/SelectMaterial.tsx: -------------------------------------------------------------------------------- 1 | import PropertiesForm from "./PropertiesForm" 2 | import CONSTANTS from "../../constants" 3 | import { useElementsStore } from "../../store/elements" 4 | import { Button } from "@/components/ui/button" 5 | 6 | import { 7 | Popover, 8 | PopoverContent, 9 | PopoverTrigger, 10 | } from "@/components/ui/popover" 11 | 12 | import { 13 | Select, 14 | SelectContent, 15 | SelectGroup, 16 | SelectItem, 17 | SelectTrigger, 18 | SelectValue, 19 | } from "@/components/ui/select" 20 | 21 | import { ScrollArea } from "@/components/ui/scroll-area" 22 | import { MatetialType } from "@/types" 23 | 24 | const options = Object 25 | .entries(CONSTANTS.materials) 26 | .filter((item) => JSON.stringify(item[1]) !== '{}') 27 | .map((item, index) => ({ 28 | value: item[0], 29 | label: item[0], 30 | disabled: JSON.stringify(item[1]) === '{}', 31 | group: index>7? 'Basic' : 'Advanced' 32 | })) 33 | 34 | export default function SelectMaterial() { 35 | const { current, updateElement } = useElementsStore() 36 | 37 | const handleChange = (value:string) => { 38 | updateElement({ 39 | id: current.id, 40 | material_type: value, 41 | material: CONSTANTS.materials[value] 42 | }) 43 | } 44 | 45 | const handleChangeMaterial = (value: MatetialType) => { 46 | updateElement({ 47 | id: current.id, 48 | material: value 49 | }) 50 | } 51 | 52 | return ( 53 | <> 54 | 55 | 56 | 59 | 60 | 61 | 62 |
63 |

type:

64 | 79 |
80 | handleChangeMaterial(e)} 83 | selects={{ 84 | wireframeLinejoin:[ 85 | "round", 86 | "bevel", 87 | "miter" 88 | ], 89 | wireframeLinecap: [ 90 | "butt", 91 | "round", 92 | "square" 93 | ] 94 | }} 95 | /> 96 |
97 |
98 |
99 | 100 | ) 101 | } -------------------------------------------------------------------------------- /src/components/general/CameraControls.tsx: -------------------------------------------------------------------------------- 1 | import { MapControls, OrbitControls } from '@react-three/drei' 2 | import { useDocumentStore } from '../../store/document' 3 | 4 | export default function CameraControls() { 5 | const { document } = useDocumentStore() 6 | 7 | return ( 8 | <> 9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/components/general/ContextMenu.tsx: -------------------------------------------------------------------------------- 1 | import { useMouse } from '@mantine/hooks'; 2 | import { IconPhoto, IconMessageCircle } from '@tabler/icons-react'; 3 | import { useElementsStore } from '../../store/elements'; 4 | import { ElementType } from '../../types'; 5 | 6 | import { 7 | DropdownMenu, 8 | DropdownMenuContent, 9 | DropdownMenuItem, 10 | DropdownMenuTrigger 11 | } from "@/components/ui/dropdown-menu" 12 | 13 | export default function ContextMenu() { 14 | const { x, y } = useMouse() 15 | const { elements, contextElementId, updateElements, setContextElementId, setCurrent } = useElementsStore() 16 | 17 | function handleClose() { 18 | setContextElementId(null) 19 | } 20 | 21 | if(!contextElementId) { 22 | return
23 | } 24 | 25 | const remove = () => { 26 | updateElements([ 27 | ...elements.filter((item: ElementType) => item.id !== contextElementId) 28 | ]) 29 | setContextElementId(null) 30 | setCurrent(null) 31 | } 32 | 33 | return ( 34 | 35 | 36 | 37 |
38 | 39 | 40 | 41 | 42 | Duplicate element 43 | 44 | 45 | 46 | Remove element 47 | 48 | 49 | 50 | ); 51 | } -------------------------------------------------------------------------------- /src/components/general/EditorControls.tsx: -------------------------------------------------------------------------------- 1 | import { TransformControls } from '@react-three/drei' 2 | 3 | import usePlayer from '../../hooks/usePlayer' 4 | import { useSettingsStore } from '../../store/settings' 5 | import { useElementsStore } from '../../store/elements' 6 | 7 | export default function EditorControls() { 8 | const { isPlayerScreen } = usePlayer() 9 | const { settings } = useSettingsStore() 10 | const { current, updateElement } = useElementsStore() 11 | 12 | const transformControlsObject = !settings.tool.selectGroup 13 | ? current?.object 14 | : current?.eventObject 15 | 16 | const isActive = (current?.object && !isPlayerScreen) 17 | 18 | if(!isActive) { 19 | return null 20 | } 21 | 22 | return ( 23 | { 27 | updateElement({ 28 | id: current.id, 29 | position: Object.entries(transformControlsObject.position).map((item) =>item[1]), 30 | // rotation: Object.entries({x, y, z}).map((item) =>item[1]), 31 | scale: Object.entries(transformControlsObject.scale).map((item) =>item[1]) 32 | }) 33 | }} 34 | /> 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /src/components/general/Element.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | 3 | import element from '../../matrix/element' 4 | import useBlueprint from '../../hooks/useBlueprint' 5 | import usePlayer from '../../hooks/usePlayer' 6 | import { useElementsStore } from '../../store/elements' 7 | import { ElementType } from '@/types' 8 | 9 | type ElementProps = { 10 | item: ElementType 11 | onHover: React.Dispatch> 12 | } 13 | 14 | export default function Element({ item, onHover }: ElementProps) { 15 | const ref = useRef(null) 16 | const { run } = useBlueprint() 17 | const { isPlayerScreen } = usePlayer() 18 | const { setCurrent, setContextElementId } = useElementsStore() 19 | 20 | useEffect(() => { 21 | run('onMount', item.id) 22 | }, []) 23 | 24 | const handleElementClick = (item: ElementType) => { 25 | const hasClick = item.editable 26 | if(hasClick) { 27 | if (!isPlayerScreen) { 28 | setCurrent(item) 29 | } else { 30 | run('onClick', item.id) 31 | } 32 | } 33 | } 34 | 35 | const { 36 | eventObject, 37 | object, 38 | id, 39 | type, 40 | editable, 41 | position, 42 | rotation, 43 | scale, 44 | ...restOfProps 45 | } = item 46 | 47 | const Component = element[item.type] 48 | 49 | return ( 50 | { 56 | stopPropagation(); 57 | setContextElementId(item.id) 58 | }} 59 | onClick={({ stopPropagation }) => { 60 | stopPropagation(); 61 | handleElementClick({ 62 | ...item, 63 | eventObject: ref?.current, 64 | object: ref?.current 65 | }); 66 | }} 67 | ref={ref} 68 | > 69 | 70 | 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /src/components/general/ElementsRender.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { EffectComposer, Outline } from '@react-three/postprocessing' 4 | import Element from '../general/Element' 5 | import { useElementsStore } from '../../store/elements' 6 | import { ElementType } from '../../types' 7 | 8 | export default function ElementsRender() { 9 | const { elements } = useElementsStore() 10 | 11 | const [hovered, onHover] = useState(null) 12 | const selected = hovered ? [hovered] : undefined 13 | 14 | return ( 15 | <> 16 | {elements.map((item: ElementType) => ( 17 | 22 | ))} 23 | 24 | 29 | 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /src/components/general/FileIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconBrandCss3, IconFile, IconCode, IconPackage } from "@tabler/icons-react"; 2 | 3 | type FileIconProps = { 4 | filename: string 5 | } 6 | 7 | export default function FileIcon({ filename }: FileIconProps) { 8 | const extension = filename.slice(filename.lastIndexOf(".") + 1); 9 | switch (extension) { 10 | case "js": 11 | return ; 12 | case "css": 13 | return ; 14 | case "json": 15 | return ; 16 | case "npmignore": 17 | return ; 18 | default: 19 | return null; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/general/FolderIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconFolderCode, IconFolderFilled } from "@tabler/icons-react"; 2 | 3 | type FolderIconProps = { 4 | isOpen: boolean 5 | } 6 | 7 | export default function FolderIcon({ isOpen }: FolderIconProps) { 8 | return isOpen ? ( 9 | 10 | ) : ( 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/components/general/GlbHelper.tsx: -------------------------------------------------------------------------------- 1 | import { useDocumentStore } from "../../store/document" 2 | 3 | export function GlbHelper() { 4 | const { document } = useDocumentStore() 5 | 6 | if(!document.helpers) { 7 | return null 8 | } 9 | 10 | return ( 11 | 12 | ) 13 | } -------------------------------------------------------------------------------- /src/components/general/Helpers.tsx: -------------------------------------------------------------------------------- 1 | import { Perf } from 'r3f-perf' 2 | 3 | import usePlayer from '../../hooks/usePlayer' 4 | import { useSettingsStore } from '../../store/settings' 5 | import { useDocumentStore } from '../../store/document' 6 | 7 | export default function Helpers() { 8 | const { isPlayerScreen } = usePlayer() 9 | const { settings } = useSettingsStore() 10 | const { document } = useDocumentStore() 11 | 12 | if(isPlayerScreen) { 13 | return null 14 | } 15 | 16 | return ( 17 | 18 | <> 19 | {document.axes && } 20 | {document.helpers && } 21 | 22 | {settings.performanceMonitor.enabled && ( 23 | 24 | )} 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/components/general/Icon.tsx: -------------------------------------------------------------------------------- 1 | import { IconType } from '@/types' 2 | import * as icons from '@tabler/icons-react' 3 | 4 | type IconProps = { 5 | name: string 6 | label?: string 7 | } 8 | 9 | export default function Icon({ name }: IconProps) { 10 | const listIcons: IconType = icons 11 | const BaseIcon = listIcons[name] || icons.IconHelpCircle 12 | return ( 13 | 14 | ) 15 | } -------------------------------------------------------------------------------- /src/components/general/MeshMaterial.tsx: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | import materialz from "../../matrix/material" 4 | import { useTexture } from '@react-three/drei' 5 | import CONSTANTS from '../../constants' 6 | import { MaterialType } from '@/types' 7 | 8 | type materialName = 'meshDistortMaterial' 9 | | 'meshReflectorMaterial' 10 | | 'meshRefractionMaterial' 11 | | 'meshTransmissionMaterial' 12 | | 'meshWobbleMaterial' 13 | | 'meshDiscardMaterial' 14 | | 'pointMaterial' 15 | | 'shaderMaterial' 16 | | 'lineBasicMaterial' 17 | | 'lineDashedMaterial' 18 | | 'material' 19 | | 'meshBasicMaterial' 20 | | 'meshDepthMaterial' 21 | | 'meshDistanceMaterial' 22 | | 'meshLambertMaterial' 23 | | 'meshMatcapMaterial' 24 | | 'meshNormalMaterial' 25 | | 'meshPhongMaterial' 26 | | 'meshPhysicalMaterial' 27 | | 'meshStandardMaterial' 28 | | 'meshToonMaterial' 29 | | 'pointsMaterial' 30 | | 'rawShaderMaterial' 31 | | 'shadowMaterial' 32 | | 'spriteMaterial' 33 | 34 | type MeshMaterialProps = { 35 | name: materialName 36 | material: MaterialType 37 | texture?: string 38 | } 39 | 40 | export default function MeshMaterial({ 41 | name, material, texture 42 | }: MeshMaterialProps) { 43 | const hasTexture = texture !== 'none' 44 | const textureUrl = !hasTexture ? '/img/none-texture.jpg' : `${CONSTANTS.cdn}/textures/wood_board/${texture}.jpg` 45 | const map = useTexture(textureUrl) 46 | 47 | const Comp = materialz[name] 48 | return ( 49 | <> 50 | 51 | 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /src/components/general/Notifications.tsx: -------------------------------------------------------------------------------- 1 | import { IconCheck, IconLoader, IconX } from '@tabler/icons-react' 2 | import toast, { useToaster } from 'react-hot-toast/headless'; 3 | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" 4 | 5 | export default function Notifications() { 6 | const { toasts, handlers } = useToaster(); 7 | const { startPause, endPause } = handlers; 8 | 9 | return ( 10 |
11 | {toasts 12 | .filter((current) => current.visible) 13 | .map((current) => ( 14 |
15 | 16 | {current.type === 'loading' ? (
) : null} 17 | {current.type === 'success' ? (
) : null} 18 | {current.type === 'error' ? (
) : null} 19 |
20 | 21 | {String(current?.message).split('\n')[0]} 22 | 23 | {String(current?.message).split('\n')[1] && ( 24 | 25 | {String(current?.message).split('\n')[1]} 26 | 27 | )} 28 |
29 | 30 | {current.type !== 'loading' && ( 31 | 34 | )} 35 |
36 |
37 | ))} 38 |
39 | ); 40 | } -------------------------------------------------------------------------------- /src/components/modal/ModalCommands.tsx: -------------------------------------------------------------------------------- 1 | import { useCommandsStore } from '../../store/commands' 2 | import useModal from '../../hooks/useModal' 3 | 4 | import { 5 | Dialog, 6 | DialogContent, 7 | DialogHeader, 8 | DialogTitle, 9 | } from "@/components/ui/dialog" 10 | 11 | export default function ModalCommands() { 12 | const { isOpenedCommandsModal, closeCommandsModal } = useModal() 13 | const { commands } = useCommandsStore() 14 | 15 | return ( 16 | 20 | 21 | 22 | Commands 23 | 24 |
25 | {commands.map((item, index) => ( 26 |
27 | {item.label} 28 |
29 | {item.data.map((subitem, subindex) => ( 30 | 31 | {subindex === 0 ? '' : ' + '} 32 | 33 | {subitem} 34 | 35 | 36 | ))} 37 |
38 |
39 | ))} 40 |
41 |
42 |
43 | ) 44 | } -------------------------------------------------------------------------------- /src/components/modal/ModalExport.tsx: -------------------------------------------------------------------------------- 1 | import useModal from "../../hooks/useModal" 2 | import { JSONTree } from 'react-json-tree' 3 | import CONSTANTS from "../../constants" 4 | import { useDocumentStore } from "../../store/document" 5 | 6 | import { 7 | Dialog, 8 | DialogContent, 9 | DialogHeader, 10 | DialogTitle, 11 | } from "@/components/ui/dialog" 12 | import { ScrollArea } from "@/components/ui/scroll-area" 13 | import { Button } from "@/components/ui/button" 14 | import { useFilesStore } from "@/store/files" 15 | import { useGraphStore } from "@/store/graph" 16 | import { useLayoutStore } from "@/store/layout" 17 | import { useHistoryStore } from "@/store/history" 18 | import { useSettingsStore } from "@/store/settings" 19 | import { useCommandsStore } from "@/store/commands" 20 | import { useElementsStore } from "@/store/elements" 21 | 22 | export default function ModalExport() { 23 | const { isOpenedExportModal, closeExportModal } = useModal() 24 | const { document: documentStore } = useDocumentStore() 25 | const { files } = useFilesStore() 26 | const { graph, graphLock } = useGraphStore() 27 | const { layout, footer } = useLayoutStore() 28 | const { history } = useHistoryStore() 29 | const { loadingView, settings } = useSettingsStore() 30 | const { commands } = useCommandsStore() 31 | const { elements, current, contextElementId } = useElementsStore() 32 | 33 | const data = { 34 | document: documentStore, 35 | files, 36 | graph, 37 | graphLock, 38 | layout, 39 | footer, 40 | history, 41 | loadingView, 42 | settings, 43 | commands, 44 | elements, 45 | current, 46 | contextElementId, 47 | } 48 | 49 | function download(){ 50 | const element = document.createElement("a"); 51 | const textFile = new Blob([JSON.stringify(data, null, 2)]); 52 | element.href = URL.createObjectURL(textFile); 53 | element.download = `${documentStore.title}-${Date.now()}-threed-studio.json`; 54 | document.body.appendChild(element); 55 | element.click(); 56 | } 57 | 58 | return ( 59 | 63 | 64 | 65 | Export 66 | 67 | 68 |
69 | 70 |
71 | 72 |
73 |
74 |
75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /src/components/modal/ModalNewFile.tsx: -------------------------------------------------------------------------------- 1 | import { FormEvent, useEffect, useRef } from "react"; 2 | import useModal from "../../hooks/useModal"; 3 | import { nanoid } from 'nanoid'; 4 | import { useLayoutStore } from "../../store/layout"; 5 | import { useFilesStore } from "../../store/files"; 6 | 7 | import { 8 | Dialog, 9 | DialogContent, 10 | DialogHeader, 11 | DialogTitle, 12 | } from "@/components/ui/dialog" 13 | import { Button } from "@/components/ui/button" 14 | import { Input } from "@/components/ui/input" 15 | 16 | export default function ModalNewFile() { 17 | const { isOpeneNewFileModal, closeNewFileModal } = useModal() 18 | const { setModalOpen } = useLayoutStore() 19 | const { updateFiles } = useFilesStore() 20 | 21 | const titleRef = useRef(null) 22 | 23 | useEffect(() => { 24 | setTimeout(() => { 25 | if(isOpeneNewFileModal && titleRef.current) { 26 | titleRef.current.focus() 27 | } 28 | }, 100) 29 | }, [isOpeneNewFileModal]) 30 | 31 | function handleSubmit(event: FormEvent) { 32 | event.preventDefault() 33 | updateFiles([ 34 | { 35 | id: nanoid(6), 36 | name: titleRef.current?.value.split('.')[0]+'.js', 37 | opened: true, 38 | selected: true, 39 | content: `// Welcome to ThreeD Studio Code! 40 | // Use sdk.help() to see more details. 41 | 42 | function onMount() { 43 | sdk.notifications.show({ 44 | title: 'file started ID: ' + sdk.component.fileId, 45 | text: 'haha' 46 | }) 47 | } 48 | 49 | function onClick() { 50 | sdk.notifications.show({ 51 | title: 'event type: ' + sdk.component.type, 52 | text: 'haha' 53 | }) 54 | } 55 | 56 | ` 57 | } 58 | ]) 59 | 60 | setModalOpen({ name: 'newFile', value: false }) 61 | 62 | } 63 | 64 | return ( 65 | 69 | 70 | 71 | New Document 72 | 73 |
74 |
75 | 76 | 77 |
78 |
79 |
80 |
81 | ) 82 | } -------------------------------------------------------------------------------- /src/components/modal/ModalSearch.tsx: -------------------------------------------------------------------------------- 1 | import useModal from "@/hooks/useModal"; 2 | 3 | import { 4 | CommandDialog, 5 | CommandEmpty, 6 | CommandGroup, 7 | CommandInput, 8 | CommandItem, 9 | CommandList, 10 | CommandSeparator, 11 | } from "@/components/ui/command" 12 | 13 | const ModalSearch = () => { 14 | const { isOpeneSearchModal, closeSearchModal, openSearchModal } = useModal() 15 | 16 | return ( 17 | 18 | 19 | 20 | No results found. 21 | 22 | Create element 23 | Search Model 24 | 25 | 26 | 27 | Logout 28 | Settings 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default ModalSearch; -------------------------------------------------------------------------------- /src/components/properties/DocumentProps.tsx: -------------------------------------------------------------------------------- 1 | import { useDocumentStore } from '../../store/document'; 2 | import { useControls, folder } from "leva"; 3 | 4 | export default function DocumentProps() { 5 | const { document, updateDocument, updateDocumentOrbitalControls, 6 | updateDocumentCanvas } = useDocumentStore() 7 | const doc: { 8 | [key: string]: any 9 | } = document 10 | 11 | const mountObject = (arrlv: string[]) => arrlv.reduce( 12 | (obj, item) => Object.assign(obj, { 13 | [item]: { 14 | value: doc[item], 15 | onChange: (e: any) => updateDocument({ [item]: e }) 16 | } 17 | }), {}); 18 | 19 | const mountOrbitalControlsObject = (arrlv: string[]) => arrlv.reduce( 20 | (obj, item) => Object.assign(obj, { 21 | [item]: { 22 | value: doc.orbitalControls[item], 23 | onChange: (e: any) => updateDocumentOrbitalControls({ [item]: e }) 24 | } 25 | }), {}); 26 | 27 | const mountCanvasObject = (arrlv: string[]) => arrlv.reduce( 28 | (obj, item) => Object.assign(obj, { 29 | [item]: { 30 | value: doc.canvas[item], 31 | onChange: (e: any) => updateDocumentCanvas({ [item]: e }) 32 | } 33 | }), {}); 34 | 35 | useControls({ 36 | Document: folder(mountObject([ 37 | 'title', 38 | 'description', 39 | 'helpers', 40 | 'axes', 41 | ])), 42 | OrbitalControls: folder(mountOrbitalControlsObject([ 43 | 'enableDamping', 44 | 'enablePan', 45 | 'enableRotate', 46 | 'enableZoom', 47 | 'reverseOrbit', 48 | 'makeDefault', 49 | 'enabled', 50 | 'minPolarAngle', 51 | 'screenSpacePanning', 52 | 'autoRotate', 53 | 'regress', 54 | ])), 55 | Canvas: folder({ 56 | ...mountCanvasObject([ 57 | 'shadows', 58 | 'frameloop', 59 | 'orthographic', 60 | 'dpr', 61 | 'legacy', 62 | 'linear', 63 | 'eventPrefix', 64 | 'flat', 65 | ]), 66 | // gl: {}, 67 | // scene: {}, 68 | // raycaster: {}, 69 | // Camera: folder({ 70 | // fov: document.canvas.camera.fov, 71 | // near: document.canvas.camera.near, 72 | // far: document.canvas.camera.far, 73 | // position: document.canvas.camera.position, 74 | // }), 75 | // Resize: folder({ 76 | // scroll: document.canvas.resize.scroll, 77 | // Debounce: folder({ 78 | // scroll: document.canvas.resize.debounce.scroll, 79 | // resize: document.canvas.resize.debounce.resize 80 | // }) 81 | // }), 82 | }), 83 | }) 84 | 85 | return
86 | } 87 | -------------------------------------------------------------------------------- /src/components/resources/AudioPicker.tsx: -------------------------------------------------------------------------------- 1 | import CONSTANTS from "@/constants" 2 | import { useElementsStore } from "@/store/elements" 3 | import { IconMusic } from "@tabler/icons-react" 4 | 5 | export default function AudioPicker() { 6 | const { current, updateElement } = useElementsStore() 7 | 8 | const handleChange = (value:string) => { 9 | updateElement({ 10 | id: current.id, 11 | soundUrl: value 12 | }) 13 | } 14 | 15 | if(!current || !current.soundUrl) { 16 | return null 17 | } 18 | 19 | return ( 20 |
21 |
22 | {CONSTANTS.sounds.map((item:string) => ( 23 |
24 |
handleChange(item)} 26 | style={{ 27 | borderColor: current.soundUrl !== item ?'#1d283a':'white' 28 | }}> 29 |
30 | 31 |
32 | 33 |

{item}

34 |
35 |
36 | ))} 37 |
38 |
39 | ) 40 | } -------------------------------------------------------------------------------- /src/components/resources/CodePicker.tsx: -------------------------------------------------------------------------------- 1 | import { useElementsStore } from "@/store/elements" 2 | import { useFilesStore } from "@/store/files" 3 | import { IconFile, IconFileOff } from "@tabler/icons-react" 4 | 5 | export default function CodePicker() { 6 | const { current, updateElement } = useElementsStore() 7 | const { files } = useFilesStore() 8 | 9 | const handleChange = (value:string) => { 10 | updateElement({ 11 | id: current.id, 12 | fileId: value 13 | }) 14 | } 15 | 16 | if(!current) { 17 | return null 18 | } 19 | 20 | return ( 21 |
22 |
23 |
24 |
handleChange('')} 26 | style={{ 27 | borderColor: current.fileId !== '' ?'#1d283a':'white' 28 | }}> 29 |
30 | 31 |
32 |

none

33 |
34 |
35 | {files.map((item) => ( 36 |
37 |
handleChange(item.id)} 39 | style={{ 40 | borderColor: current.fileId !== item.id ?'#1d283a':'white' 41 | }}> 42 |
43 | 44 |
45 | 46 |

{item.name}

47 |
48 |
49 | ))} 50 |
51 |
52 | ) 53 | } -------------------------------------------------------------------------------- /src/components/resources/EnvironmentPicker.tsx: -------------------------------------------------------------------------------- 1 | import CONSTANTS from "@/constants" 2 | import { useElementsStore } from "@/store/elements" 3 | 4 | export default function EnvironmentPicker() { 5 | const { current, updateElement } = useElementsStore() 6 | 7 | const handleChange = (value:string) => { 8 | updateElement({ 9 | id: current.id, 10 | environment: value 11 | }) 12 | } 13 | 14 | if(!current || !current.environment) { 15 | return null 16 | } 17 | 18 | return ( 19 |
20 |
21 | {CONSTANTS.environment.map((item:string) => ( 22 |
23 |
handleChange(item)} 25 | style={{ 26 | borderColor: current.environment !== item ?'#1d283a':'white' 27 | }}> 28 | 29 | 30 |

{item}

31 |
32 |
33 | ))} 34 |
35 |
36 | ) 37 | } -------------------------------------------------------------------------------- /src/components/resources/ImagePicker.tsx: -------------------------------------------------------------------------------- 1 | import CONSTANTS from "@/constants" 2 | import { useElementsStore } from "@/store/elements" 3 | 4 | const images = [ 5 | 'react-threejs.png', 6 | 'threejs-react.png', 7 | ] 8 | 9 | export default function ImagePicker() { 10 | const { current, updateElement } = useElementsStore() 11 | 12 | const handleChange = (value:string) => { 13 | updateElement({ 14 | id: current.id, 15 | file: value 16 | }) 17 | } 18 | 19 | if(!current || !current.file) { 20 | return null 21 | } 22 | 23 | return ( 24 |
25 |
26 | {images.map((item:string) => ( 27 |
28 |
handleChange(item)} 30 | style={{ 31 | borderColor: current.file !== item ?'#1d283a':'white' 32 | }}> 33 | 34 | 35 |

{item}

36 |
37 |
38 | ))} 39 |
40 |
41 | ) 42 | } -------------------------------------------------------------------------------- /src/components/resources/MaterialPicker.tsx: -------------------------------------------------------------------------------- 1 | import CONSTANTS from "@/constants" 2 | import { useElementsStore } from "@/store/elements" 3 | 4 | export default function MaterialPicker() { 5 | const { current, updateElement } = useElementsStore() 6 | 7 | const handleChange = (value:string) => { 8 | updateElement({ 9 | id: current.id, 10 | material_type: value 11 | }) 12 | } 13 | 14 | const isDisable = (value: string) => JSON.stringify(CONSTANTS.materials[value]) === '{}' 15 | 16 | if(!current || !current.material_type) { 17 | return null 18 | } 19 | 20 | return ( 21 |
22 |
23 | {Object.keys(CONSTANTS.materials).filter((itm) => !isDisable(itm)).map((item:string) => ( 24 |
25 |
handleChange(item)} 27 | style={{ 28 | borderColor: current.material_type !== item ?'#1d283a':'white' 29 | }}> 30 | 31 |

{item}

32 |
33 |
34 | ))} 35 |
36 |
37 | ) 38 | } -------------------------------------------------------------------------------- /src/components/resources/Resources.tsx: -------------------------------------------------------------------------------- 1 | import MaterialPicker from "./MaterialPicker"; 2 | import TexturePicker from "./TexturePicker"; 3 | import CodePicker from "./CodePicker"; 4 | import EnvironmentPicker from "./EnvironmentPicker"; 5 | import ImagePicker from "./ImagePicker"; 6 | import AudioPicker from "./AudioPicker"; 7 | import { useSettingsStore } from "@/store/settings"; 8 | import { Button } from "../ui/button"; 9 | import { IconArrowLeft } from "@tabler/icons-react"; 10 | 11 | export default function Resources() { 12 | const {settings, setPicker} = useSettingsStore() 13 | 14 | if(settings.picker === '') { 15 | return null 16 | } 17 | 18 | return ( 19 |
20 |
21 | 24 |

{settings.picker}

25 |
26 | {(settings.picker === 'material') && } 27 | {(settings.picker === 'texture') && } 28 | {(settings.picker === 'code') && } 29 | {(settings.picker === 'environment') && } 30 | {(settings.picker === 'image') && } 31 | {(settings.picker === 'audio') && } 32 |
33 | ) 34 | } -------------------------------------------------------------------------------- /src/components/resources/TexturePicker.tsx: -------------------------------------------------------------------------------- 1 | import CONSTANTS from "@/constants" 2 | import { useElementsStore } from "@/store/elements" 3 | 4 | export default function TexturePicker() { 5 | const { current, updateElement } = useElementsStore() 6 | 7 | const handleChange = (value:string) => { 8 | updateElement({ 9 | id: current.id, 10 | texture: value 11 | }) 12 | } 13 | 14 | if(!current || !current.texture) { 15 | return null 16 | } 17 | 18 | return ( 19 |
20 |
21 |
22 |
handleChange('none')} 24 | style={{ 25 | borderColor: current.texture !== 'none' ?'#1d283a':'white' 26 | }}> 27 |
28 |

none

29 |
30 |
31 | {CONSTANTS.textures.map((item:string) => ( 32 |
33 |
handleChange(item)} 35 | style={{ 36 | borderColor: current.texture !== item ?'#1d283a':'white' 37 | }}> 38 | 39 | 40 |

{item}

41 |
42 |
43 | ))} 44 |
45 |
46 | ) 47 | } -------------------------------------------------------------------------------- /src/components/screens/CodeEditor.tsx: -------------------------------------------------------------------------------- 1 | import { Editor, useMonaco } from '@monaco-editor/react' 2 | import { IconX } from '@tabler/icons-react'; 3 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" 4 | 5 | import getEditorLang from '../../utils/getEditorLang' 6 | import useCodeEditor from '../../hooks/useCodeEditor' 7 | import { useCommandsStore } from '../../store/commands' 8 | import { Button } from '@/components/ui/button' 9 | 10 | function EmptyScreen() { 11 | const { commands } = useCommandsStore() 12 | 13 | return ( 14 |
15 | {commands.map((item, index) => ( 16 |
17 | {item.label} 18 |
19 | {item.data.map((subitem, subindex) => ( 20 | 21 | {subindex === 0 ? '' : ' + '} 22 | {subitem} 23 | 24 | ))} 25 |
26 |
27 | ))} 28 |
29 | ) 30 | } 31 | 32 | export default function CodeEditor() { 33 | const { 34 | openedObjects, 35 | closeFile, 36 | updateCode, 37 | openFile 38 | } = useCodeEditor() 39 | 40 | const monaco = useMonaco() 41 | monaco?.editor.defineTheme('my-theme', { 42 | base: 'vs-dark', 43 | inherit: true, 44 | rules: [], 45 | colors: { 46 | 'editor.background': '#000000', 47 | 'minimap.background': '#060606', 48 | }, 49 | }); 50 | 51 | 52 | if(openedObjects.length < 1) { 53 | return 54 | } 55 | 56 | return ( 57 |
58 | item.selected)[0]?.id} className="h-full bg-transparent"> 59 | 60 | {openedObjects.map((item) => ( 61 | item.selected)[0]?.id ? 'border' : ''}`}> 62 | <> 63 |
openFile(item.id)}>{item.name}
64 | 71 | 72 |
73 | ))} 74 |
75 | {openedObjects.map((item) => ( 76 | 77 | updateCode(item.id, e)} 82 | /> 83 | 84 | ))} 85 |
86 |
87 | ) 88 | } -------------------------------------------------------------------------------- /src/components/screens/Mesa.tsx: -------------------------------------------------------------------------------- 1 | import { Canvas } from '@react-three/fiber' 2 | 3 | import EditorControls from '../general/EditorControls' 4 | import ElementsRender from '../general/ElementsRender' 5 | import CameraControls from '../general/CameraControls' 6 | import Helpers from '../general/Helpers' 7 | import { useDocumentStore } from '../../store/document' 8 | import { useElementsStore } from '../../store/elements' 9 | import { useSettingsStore } from '@/store/settings' 10 | 11 | export default function Mesa() { 12 | const { document } = useDocumentStore() 13 | const { setCurrent } = useElementsStore() 14 | const { loadingView } = useSettingsStore() 15 | 16 | const canvas = document.canvas 17 | const onPointerMissed = () => setCurrent(null) 18 | 19 | return ( 20 | 21 | <> 22 | 23 | 24 | 25 | 26 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/components/toolbar/ToolbarBottom.tsx: -------------------------------------------------------------------------------- 1 | import Actions from '../general/Actions' 2 | import useToolbar from '../../hooks/useToolbar' 3 | import mountToolbarActions from '../../utils/mountToolbarActions' 4 | import CONSTANTS from '../../constants' 5 | import { useElementsStore } from '../../store/elements' 6 | import { useLayoutStore } from '../../store/layout' 7 | import { Badge } from "@/components/ui/badge" 8 | export default function ToolbarBottom() { 9 | const { showHorizontalBottom } = useToolbar() 10 | const { current } = useElementsStore() 11 | const { footer, setFooter } = useLayoutStore() 12 | 13 | const toolbarHorizontalBottom = mountToolbarActions( 14 | CONSTANTS.toolbar.bottom, 15 | { 16 | log: { onClick: () => setFooter(footer === 'log' ? null : 'log') }, 17 | previus_change: { onClick: null }, 18 | next_change: { onClick: null }, 19 | studio: { onClick: () => setFooter(footer === 'studio' ? null : 'studio') }, 20 | }) 21 | 22 | if(!showHorizontalBottom) { 23 | return
24 | } 25 | 26 | return ( 27 | <> 28 |
29 | <> 30 | 31 |
32 |
33 | 34 | {current 35 | ? ( 36 | <> 37 | 38 | {current.id} 39 | 40 | 41 | {current.type} 42 | 43 | 44 | ) 45 | : 'No item selected'} 46 | 47 |
48 | 49 |
50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/components/toolbar/ToolbarRight.tsx: -------------------------------------------------------------------------------- 1 | import Actions from '../general/Actions' 2 | import Properties from '../window/Properties' 3 | import Layers from '../window/Layers' 4 | import History from '../window/History' 5 | import useToolbar from '../../hooks/useToolbar' 6 | import { useState } from 'react' 7 | import mountToolbarActions from '../../utils/mountToolbarActions' 8 | import CONSTANTS from '../../constants' 9 | 10 | import { 11 | Accordion, 12 | AccordionContent, 13 | AccordionItem, 14 | AccordionTrigger, 15 | } from "@/components/ui/accordion" 16 | 17 | 18 | import Icon from '../general/Icon' 19 | 20 | export default function ToolbarRight() { 21 | const [opened, setOpen] = useState('0') 22 | 23 | const { 24 | toggleOpenRight, 25 | showVerticalRight, 26 | isRightOpen 27 | } = useToolbar() 28 | 29 | const setOpenNumber = (value: number) => { 30 | toggleOpenRight(); 31 | setOpen(String(value)); 32 | } 33 | 34 | const toolbarVerticalRight = mountToolbarActions( 35 | CONSTANTS.toolbar.right, 36 | { 37 | properties: { 38 | onClick: () => setOpenNumber(0), 39 | component: 40 | }, 41 | layers: { 42 | onClick: () => setOpenNumber(1), 43 | component: 44 | }, 45 | history: { 46 | onClick: () => setOpenNumber(2), 47 | component: 48 | }, 49 | } 50 | ) 51 | 52 | if(!showVerticalRight) { 53 | return
54 | } 55 | 56 | return ( 57 |
58 | {!isRightOpen ? ( 59 | 60 | ) : ( 61 |
62 | 63 | 64 | {toolbarVerticalRight.map((item, index:number) => ( 65 | 66 | { 69 | if(String(index)===opened) { 70 | toggleOpenRight() 71 | } else { 72 | setOpen(String(index)) 73 | } 74 | }} 75 | > 76 |
77 | 78 | {item.label} 79 |
80 |
81 | 82 |
83 | {item.component} 84 |
85 |
86 |
87 | ))} 88 |
89 |
90 | )} 91 |
92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 5 | import { ChevronDown } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Accordion = AccordionPrimitive.Root 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )) 21 | AccordionItem.displayName = "AccordionItem" 22 | 23 | const AccordionTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | svg]:rotate-180", 32 | className 33 | )} 34 | {...props} 35 | > 36 | {children} 37 | 38 | 39 | 40 | )) 41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName 42 | 43 | const AccordionContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, children, ...props }, ref) => ( 47 | 55 |
{children}
56 |
57 | )) 58 | AccordionContent.displayName = AccordionPrimitive.Content.displayName 59 | 60 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 61 | -------------------------------------------------------------------------------- /src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border p-4 [&>svg]:absolute [&>svg]:text-foreground [&>svg]:left-4 [&>svg]:top-4 [&>svg+div]:translate-y-[-3px] [&:has(svg)]:pl-11", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "text-destructive border-destructive/50 dark:border-destructive [&>svg]:text-destructive text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ) 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )) 33 | Alert.displayName = "Alert" 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )) 45 | AlertTitle.displayName = "AlertTitle" 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )) 57 | AlertDescription.displayName = "AlertDescription" 58 | 59 | export { Alert, AlertTitle, AlertDescription } 60 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center border rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "bg-primary hover:bg-primary/80 border-transparent text-primary-foreground", 13 | secondary: 14 | "bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground", 15 | destructive: 16 | "bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "underline-offset-4 hover:underline text-primary", 21 | }, 22 | size: { 23 | default: "h-10 py-2 px-4", 24 | sm: "h-9 px-3 rounded-md", 25 | lg: "h-11 px-8 rounded-md", 26 | }, 27 | }, 28 | defaultVariants: { 29 | variant: "default", 30 | size: "default", 31 | }, 32 | } 33 | ) 34 | 35 | export interface ButtonProps 36 | extends React.ButtonHTMLAttributes, 37 | VariantProps { 38 | asChild?: boolean 39 | } 40 | 41 | const Button = React.forwardRef( 42 | ({ className, variant, size, asChild = false, ...props }, ref) => { 43 | const Comp = asChild ? Slot : "button" 44 | return ( 45 | 50 | ) 51 | } 52 | ) 53 | Button.displayName = "Button" 54 | 55 | export { Button, buttonVariants } 56 | -------------------------------------------------------------------------------- /src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 5 | import { Check } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 24 | 25 | 26 | 27 | )) 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 29 | 30 | export { Checkbox } 31 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export type InputProps = React.InputHTMLAttributes 6 | 7 | const Input = React.forwardRef( 8 | ({ className, type, ...props }, ref) => { 9 | return ( 10 | 19 | ) 20 | } 21 | ) 22 | Input.displayName = "Input" 23 | 24 | export { Input } 25 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Popover = PopoverPrimitive.Root 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )) 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 30 | 31 | export { Popover, PopoverTrigger, PopoverContent } 32 | -------------------------------------------------------------------------------- /src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | )) 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | 43 | 44 | 45 | )) 46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 47 | 48 | export { ScrollArea, ScrollBar } 49 | -------------------------------------------------------------------------------- /src/components/ui/slider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SliderPrimitive from "@radix-ui/react-slider" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Slider = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | 21 | 22 | 23 | 24 | 25 | )) 26 | Slider.displayName = SliderPrimitive.Root.displayName 27 | 28 | export { Slider } 29 | -------------------------------------------------------------------------------- /src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SwitchPrimitives from "@radix-ui/react-switch" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Switch = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | 25 | 26 | )) 27 | Switch.displayName = SwitchPrimitives.Root.displayName 28 | 29 | export { Switch } 30 | -------------------------------------------------------------------------------- /src/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TabsPrimitive from "@radix-ui/react-tabs" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Tabs = TabsPrimitive.Root 9 | 10 | const TabsList = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )) 23 | TabsList.displayName = TabsPrimitive.List.displayName 24 | 25 | const TabsTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 37 | )) 38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName 39 | 40 | const TabsContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | )) 53 | TabsContent.displayName = TabsPrimitive.Content.displayName 54 | 55 | export { Tabs, TabsList, TabsTrigger, TabsContent } 56 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |