├── src ├── Pages │ ├── Error │ │ ├── index.css │ │ └── index.jsx │ ├── View │ │ ├── index.css │ │ └── index.jsx │ ├── Home │ │ ├── index.css │ │ └── index.jsx │ └── Drop │ │ ├── index.css │ │ └── index.jsx ├── index.js ├── Components │ ├── Loading │ │ ├── index.jsx │ │ └── index.css │ └── Github │ │ ├── index.css │ │ └── index.jsx ├── index.css └── App.jsx ├── .gitignore ├── README.md ├── package.json └── public ├── index.html └── assets ├── undraw_progressive_app_m9ms.svg └── icon.svg /src/Pages/Error/index.css: -------------------------------------------------------------------------------- 1 | .error { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | width: 100%; 6 | height: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /src/Pages/Error/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./index.css"; 3 | 4 | const Error = () => { 5 | return ( 6 |
7 |

404

8 |
9 | ); 10 | }; 11 | 12 | export default Error; 13 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById("root") 11 | ); 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/Components/Loading/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./index.css"; 3 | 4 | const Loading = ({ visible }) => { 5 | return ( 6 |
12 |
13 |
14 |
15 |
16 |
17 | ); 18 | }; 19 | 20 | export default Loading; 21 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://rsms.me/inter/inter.css"); 2 | html { 3 | font-family: "Inter", sans-serif; 4 | } 5 | @supports (font-variation-settings: normal) { 6 | html { 7 | font-family: "Inter var", sans-serif; 8 | } 9 | } 10 | 11 | html, 12 | body, 13 | #root { 14 | margin: 0; 15 | padding: 0; 16 | width: 100%; 17 | height: 100%; 18 | -webkit-font-smoothing: antialiased; 19 | -moz-osx-font-smoothing: grayscale; 20 | } 21 | 22 | * { 23 | margin-block-start: 0; 24 | margin-block-end: 0; 25 | } 26 | -------------------------------------------------------------------------------- /src/Components/Github/index.css: -------------------------------------------------------------------------------- 1 | .github-corner:hover .octo-arm { 2 | animation: octocat-wave 560ms ease-in-out; 3 | } 4 | @keyframes octocat-wave { 5 | 0%, 6 | 100% { 7 | transform: rotate(0); 8 | } 9 | 20%, 10 | 60% { 11 | transform: rotate(-25deg); 12 | } 13 | 40%, 14 | 80% { 15 | transform: rotate(10deg); 16 | } 17 | } 18 | @media (max-width: 500px) { 19 | .github-corner:hover .octo-arm { 20 | animation: none; 21 | } 22 | .github-corner .octo-arm { 23 | animation: octocat-wave 560ms ease-in-out; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Pages/View/index.css: -------------------------------------------------------------------------------- 1 | .view { 2 | width: 100%; 3 | height: 100%; 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | } 8 | 9 | .view > model-viewer { 10 | width: 100%; 11 | height: 100%; 12 | } 13 | 14 | .view > .title { 15 | position: absolute; 16 | left: 20px; 17 | top: 20px; 18 | font-size: 200%; 19 | } 20 | 21 | .view > .sub-title { 22 | position: absolute; 23 | left: 20px; 24 | top: 60px; 25 | font-size: 100%; 26 | font-weight: normal; 27 | cursor: pointer; 28 | } 29 | 30 | .view > .waiting-message { 31 | font-weight: 300; 32 | font-size: 125%; 33 | } 34 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BrowserRouter, Switch, Route } from "react-router-dom"; 3 | import Home from "./Pages/Home"; 4 | import Drop from "./Pages/Drop"; 5 | import View from "./Pages/View"; 6 | import Error from "./Pages/Error"; 7 | 8 | function App() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Model Viewer Bridge 2 | 3 | > An easy to use bridge between your 3D models on desktop 4 | and 3D viewer in your mobile + View in AR 5 | 6 | - No account, no installation 7 | - Models never saved on any servers 8 | - View in Augmented Reality using WebXR-compatible browser / Android Scene Viewer 9 | - Supports gltf binary format 3d model 10 | 11 | Made using 12 | 13 | - [Model Viewer](https://github.com/google/model-viewer) 14 | - [Piping Server](https://github.com/nwtgck/piping-server) 15 | - [React](https://reactjs.org/) 16 | - [React Router](https://reactrouter.com/) 17 | 18 | [![Netlify Status](https://api.netlify.com/api/v1/badges/bc45393e-b35e-4f02-a6f5-8d559f861627/deploy-status)](https://app.netlify.com/sites/model-viewer-bridge/deploys) 19 | -------------------------------------------------------------------------------- /src/Pages/Home/index.css: -------------------------------------------------------------------------------- 1 | .home { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | flex-direction: column; 6 | width: 100%; 7 | height: 100%; 8 | } 9 | 10 | .home > .image { 11 | height: 40%; 12 | margin: 35px; 13 | } 14 | 15 | .home > .title { 16 | font-size: 40px; 17 | } 18 | 19 | .home > .sub-title { 20 | margin-top: 7.5px; 21 | font-size: 20px; 22 | text-align: center; 23 | font-weight: 300; 24 | } 25 | 26 | .home > .features { 27 | font-size: 15px; 28 | } 29 | 30 | .home > .drop { 31 | margin-top: 40px; 32 | text-decoration: none; 33 | color: black; 34 | width: 100px; 35 | height: 40px; 36 | text-align: center; 37 | font-size: 20px; 38 | box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.25); 39 | border-radius: 20px; 40 | display: flex; 41 | justify-content: center; 42 | align-items: center; 43 | transition: all 0.2s ease-in; 44 | } 45 | 46 | .home > .drop:hover { 47 | box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.25); 48 | } 49 | 50 | .home > .drop h1 { 51 | font-weight: 300; 52 | } 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "model-viewer-bridge", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.5.0", 8 | "@testing-library/user-event": "^7.2.1", 9 | "meaningful-string": "^1.3.1", 10 | "qrcode-react": "^0.1.16", 11 | "react": "^16.13.1", 12 | "react-dom": "^16.13.1", 13 | "react-router-dom": "^5.2.0", 14 | "react-scripts": "3.4.3" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build && echo '/* /index.html 200' | cat >build/_redirects ", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": "react-app" 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Pages/Drop/index.css: -------------------------------------------------------------------------------- 1 | .drop { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .drop > model-viewer { 7 | width: 100%; 8 | height: 100%; 9 | } 10 | 11 | .drop > .title { 12 | position: absolute; 13 | left: 20px; 14 | top: 20px; 15 | font-size: 200%; 16 | } 17 | 18 | .drop > .sub-title { 19 | position: absolute; 20 | left: 20px; 21 | top: 60px; 22 | font-size: 100%; 23 | font-weight: normal; 24 | cursor: pointer; 25 | } 26 | 27 | .drop > .qr-code { 28 | position: absolute; 29 | right: 20px; 30 | top: 20px; 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | flex-direction: column; 35 | } 36 | 37 | .drop > .qr-code > .url { 38 | margin-top: 10px; 39 | } 40 | 41 | .drop > .hidden-input { 42 | display: none; 43 | } 44 | 45 | .drop > .help-details { 46 | position: absolute; 47 | left: 20px; 48 | bottom: 20px; 49 | } 50 | 51 | .drop > .help-details summary:focus { 52 | outline: none; 53 | } 54 | 55 | .drop > .credits { 56 | position: absolute; 57 | left: 50%; 58 | bottom: 10px; 59 | transform: translateX(-50%); 60 | } 61 | -------------------------------------------------------------------------------- /src/Pages/Home/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Github from "../../Components/Github"; 3 | import "./index.css"; 4 | 5 | const Home = () => { 6 | return ( 7 |
8 | 9 |

Model Viewer Bridge

10 |

11 | An easy to use bridge between your 3D models on desktop
and 3D 12 | viewer in your mobile + View in AR 13 |

14 | 15 | computer to mobile bridge 20 | 21 |
22 | 31 |
32 | 33 | 34 |

Start

35 |
36 |
37 | ); 38 | }; 39 | 40 | export default Home; 41 | -------------------------------------------------------------------------------- /src/Components/Loading/index.css: -------------------------------------------------------------------------------- 1 | .lds-ellipsis { 2 | position: absolute; 3 | right: 0px; 4 | bottom: -20px; 5 | width: 80px; 6 | height: 80px; 7 | } 8 | .lds-ellipsis div { 9 | position: absolute; 10 | top: 33px; 11 | width: 13px; 12 | height: 13px; 13 | border-radius: 50%; 14 | background: rgba(0, 0, 0, 1); 15 | animation-timing-function: cubic-bezier(0, 1, 1, 0); 16 | } 17 | .lds-ellipsis div:nth-child(1) { 18 | left: 8px; 19 | animation: lds-ellipsis1 0.6s infinite; 20 | } 21 | .lds-ellipsis div:nth-child(2) { 22 | left: 8px; 23 | animation: lds-ellipsis2 0.6s infinite; 24 | } 25 | .lds-ellipsis div:nth-child(3) { 26 | left: 32px; 27 | animation: lds-ellipsis2 0.6s infinite; 28 | } 29 | .lds-ellipsis div:nth-child(4) { 30 | left: 56px; 31 | animation: lds-ellipsis3 0.6s infinite; 32 | } 33 | @keyframes lds-ellipsis1 { 34 | 0% { 35 | transform: scale(0); 36 | } 37 | 100% { 38 | transform: scale(1); 39 | } 40 | } 41 | @keyframes lds-ellipsis3 { 42 | 0% { 43 | transform: scale(1); 44 | } 45 | 100% { 46 | transform: scale(0); 47 | } 48 | } 49 | @keyframes lds-ellipsis2 { 50 | 0% { 51 | transform: translate(0, 0); 52 | } 53 | 100% { 54 | transform: translate(24px, 0); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Pages/View/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import "./index.css"; 3 | import { useParams } from "react-router-dom"; 4 | 5 | const View = () => { 6 | const params = useParams(); 7 | const modelRef = useRef(null); 8 | const [src, setSrc] = useState(null); 9 | 10 | useEffect(() => { 11 | document.title = "View Page"; 12 | const { id } = params; 13 | 14 | (async () => { 15 | try { 16 | let response = await fetch("https://ppng.io/MVB-" + id); 17 | const blob = await response.blob(); 18 | if (typeof blob === "object" && blob.type === "model/gltf+json") { 19 | setSrc(URL.createObjectURL(blob)); 20 | } 21 | } catch (error) { 22 | // bad request 23 | console.error(error); 24 | } 25 | })(); 26 | 27 | // eslint-disable-next-line react-hooks/exhaustive-deps 28 | }, [params]); 29 | return ( 30 |
31 | {typeof src === "string" && ( 32 | 40 | )} 41 | {src === null && ( 42 |

Waiting to receive model...

43 | )} 44 |

View Page

45 |

46 | go to{" "} 47 | 48 | drop 49 | {" "} 50 | page 51 |
to send a model 52 |

53 |
54 | ); 55 | }; 56 | 57 | export default View; 58 | -------------------------------------------------------------------------------- /src/Components/Github/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./index.css"; 3 | 4 | const Github = () => { 5 | return ( 6 | 13 | 42 | 43 | ); 44 | }; 45 | 46 | export default Github; 47 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Model Viewer Bridge 11 | 12 | 16 | 17 | 18 | 19 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 36 | 37 | 41 | 42 | 43 | 47 | 51 | 52 | 53 | 54 |
55 | 56 | 57 | -------------------------------------------------------------------------------- /src/Pages/Drop/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import "./index.css"; 3 | import { meaningful } from "meaningful-string"; 4 | import QRCode from "qrcode-react"; 5 | import Loading from "../../Components/Loading"; 6 | 7 | const Drop = () => { 8 | const modelRef = useRef(null); 9 | const inputRef = useRef(null); 10 | const detailsRef = useRef(null); 11 | 12 | const [pipingServerId, setPipingServerId] = useState(""); 13 | const [loadingVisibility, setLoadingVisibility] = useState(false); 14 | const [creditsVisibility, setCreditsVisibility] = useState(true); 15 | 16 | useEffect(() => { 17 | const options = { 18 | numberUpto: 60, 19 | joinBy: "-", 20 | }; 21 | const id = meaningful(options); 22 | setPipingServerId(id.toLowerCase()); 23 | document.title = "Drop Page"; 24 | }, []); 25 | 26 | const sendBlob = async (blob) => { 27 | if (blob.name.includes(".glb")) { 28 | if (creditsVisibility) { 29 | setCreditsVisibility(false); 30 | } 31 | 32 | modelRef.current.src = URL.createObjectURL(blob); 33 | setLoadingVisibility(true); 34 | await fetch("https://ppng.io/MVB-" + pipingServerId, { 35 | method: "POST", 36 | body: blob, 37 | }); 38 | setLoadingVisibility(false); 39 | } else { 40 | alert("no glb was found"); 41 | } 42 | }; 43 | 44 | const handleDropModel = async (e) => { 45 | e.stopPropagation(); 46 | e.preventDefault(); 47 | const blob = e.nativeEvent.dataTransfer.files[0]; 48 | sendBlob(blob); 49 | }; 50 | 51 | const handleInputModel = (event) => { 52 | const blob = event.target.files[0]; 53 | sendBlob(blob); 54 | }; 55 | 56 | const dismissDetails = () => { 57 | if (detailsRef.current.hasAttribute("open")) { 58 | detailsRef.current.removeAttribute("open"); 59 | } 60 | }; 61 | 62 | return ( 63 |
{ 67 | e.stopPropagation(); 68 | e.preventDefault(); 69 | e.dataTransfer.dropEffect = "copy"; 70 | }} 71 | onClick={dismissDetails} 72 | > 73 | 80 | 87 |

Drop Page

88 |

{ 91 | inputRef.current.click(); 92 | }} 93 | > 94 | click here to select 95 |
or simply drop a .glb 96 |

97 |
98 | 102 | 108 |

{pipingServerId}

109 |
110 |
111 | 112 | 113 |
114 | Help 115 |
    116 |
  1. 117 | Scan the QR Code / open the view link
    on top right corner on 118 | your mobile 119 |
  2. 120 |
  3. Drop the glb file anywhere on the page
  4. 121 |
  5. Check your mobile page to view
  6. 122 |
  7. 123 | You can also use the same links to drop
    another model and 124 | view the same in mobile 125 |
  8. 126 |
127 |
128 | 129 |
135 | Astronaut by{" "} 136 | 141 | Google Poly 142 | 143 |
144 |
145 | ); 146 | }; 147 | 148 | export default Drop; 149 | -------------------------------------------------------------------------------- /public/assets/undraw_progressive_app_m9ms.svg: -------------------------------------------------------------------------------- 1 | progressive_app -------------------------------------------------------------------------------- /public/assets/icon.svg: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 36 | 37 | 38 | 42 | 43 | 44 | 48 | 49 | 50 | 54 | 55 | 56 | 60 | 61 | 62 | 66 | 67 | 68 | 72 | 73 | 74 | 78 | 79 | 80 | 84 | 85 | 86 | 90 | 91 | 92 | 96 | 97 | 98 | 102 | 103 | 104 | 108 | 109 | 110 | 114 | 115 | 116 | 120 | 121 | 122 | 126 | 127 | 128 | 132 | 133 | 134 | 138 | 139 | 140 | 144 | 145 | 146 | 150 | 151 | 152 | 156 | 157 | 158 | 162 | 163 | 164 | 168 | 169 | 170 | 174 | 175 | 176 | 180 | 181 | 182 | 186 | 187 | 188 | 192 | 193 | 194 | 198 | 199 | 200 | 205 | 206 | 207 | 211 | 212 | 213 | 217 | 218 | 219 | 220 | 221 | 222 | 226 | 227 | 228 | 232 | 233 | 234 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 276 | 277 | 278 | 279 | --------------------------------------------------------------------------------