├── 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 |
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 | [](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 |

20 |
21 |
22 |
23 | - No account, no installation
24 | - Models never saved on any servers
25 | -
26 | View in Augmented Reality using WebXR-compatible browser / Android
27 | Scene Viewer
28 |
29 | - Supports gltf binary format 3d model
30 |
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 |
111 |
112 |
113 |
114 | Help
115 |
116 | -
117 | Scan the QR Code / open the view link
on top right corner on
118 | your mobile
119 |
120 | - Drop the glb file anywhere on the page
121 | - Check your mobile page to view
122 | -
123 | You can also use the same links to drop
another model and
124 | view the same in mobile
125 |
126 |
127 |
128 |
129 |
144 |
145 | );
146 | };
147 |
148 | export default Drop;
149 |
--------------------------------------------------------------------------------
/public/assets/undraw_progressive_app_m9ms.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/icon.svg:
--------------------------------------------------------------------------------
1 |
279 |
--------------------------------------------------------------------------------