Merge branch 'master' of https://github.com/specklesystems/Server
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
.circleci
|
||||
node_modules
|
||||
test-queries
|
||||
.editorconfig
|
||||
.gitignore
|
||||
.git
|
||||
Contributing.md
|
||||
ISSUE_TEMPLATE.md
|
||||
lerna.json
|
||||
LICENSE
|
||||
package-lock.json
|
||||
package.json
|
||||
.env.example
|
||||
.eslintrc.json
|
||||
.mocharc.js
|
||||
readme.md
|
||||
.env
|
||||
+7
-4
@@ -1,8 +1,11 @@
|
||||
node_modules
|
||||
frontend/node_modules
|
||||
frontend/dist
|
||||
packages/server/node_modules
|
||||
packages/server/.env
|
||||
packages/frontend/node_modules
|
||||
packages/frontend/dist
|
||||
packages/viewer/node_modules
|
||||
packages/viewer/dist
|
||||
*.env
|
||||
.nyc_output
|
||||
coverage/
|
||||
.env
|
||||
.vscode
|
||||
test-queries
|
||||
|
||||
+12
-11
@@ -1,4 +1,5 @@
|
||||
# Speckle Contribution Guidelines
|
||||
|
||||
## Introduction
|
||||
|
||||
Thank you for reading this! Speckle's a rather wide network of parts that depend on each other, either directly, indirectly or even just cosmetically.
|
||||
@@ -9,41 +10,41 @@ This means that what might look like a simple quick change in one repo may have
|
||||
|
||||
## Bugs & Issues 🐞
|
||||
|
||||
### Found a new bug?
|
||||
### Found a new bug?
|
||||
|
||||
- First step is to check whether this is a new bug! We encourage you to search through the issues of the project in question **and** associated repos!
|
||||
|
||||
- If you come up with nothing, **open a new issue with a clear title and description**, as much relevant information as possible: system configuration, code samples & steps to reproduce the problem.
|
||||
- If you come up with nothing, **open a new issue with a clear title and description**, as much relevant information as possible: system configuration, code samples & steps to reproduce the problem.
|
||||
|
||||
- Can't mention this often enough: tells us how to reproduce the problem! We will ignore or flag as such issues without reproduction steps.
|
||||
- Can't mention this often enough: tells us how to reproduce the problem! We will ignore or flag as such issues without reproduction steps.
|
||||
|
||||
- Try to reference & note all potentially affected projects.
|
||||
|
||||
### Sending a PR for Bug Fixes
|
||||
|
||||
You fixed something! Great! We hope you logged it first :) Make sure though that you've covered the lateral thinking needed for a bug report, as described above, also in your implementation! If there any tests, make sure they all pass. If there are none, it means they're missing - so add them!
|
||||
You fixed something! Great! We hope you logged it first :) Make sure though that you've covered the lateral thinking needed for a bug report, as described above, also in your implementation! If there any tests, make sure they all pass. If there are none, it means they're missing - so add them!
|
||||
|
||||
## New Features 🎉
|
||||
|
||||
The golden rule is to Discuss First!
|
||||
|
||||
- Before embarking on adding a new feature, suggest it first as an issue with the `enhancement` label and/or title - this will allow relevant people to pitch in
|
||||
- We'll now discuss your requirements and see how and if they fit within the Speckle ecosystem.
|
||||
- The last step is to actually start writing code & submit a PR so we can follow along!
|
||||
- All new features should, if and where possible, come with tests. We won't merge without!
|
||||
- We'll now discuss your requirements and see how and if they fit within the Speckle ecosystem.
|
||||
- The last step is to actually start writing code & submit a PR so we can follow along!
|
||||
- All new features should, if and where possible, come with tests. We won't merge without!
|
||||
|
||||
> Many clients may potentially have overlapping scopes, some features might already be in dev somewhere else, or might have been postponed to the next major release due to api instability in that area. For example, adding a delete stream button in the accounts panel in rhino: this feature was planned for speckle admin, and the whole functionality of the accounts panel in rhino is to be greatly reduced!
|
||||
|
||||
## Cosmetic Patches ✨
|
||||
|
||||
Changes that are cosmetic in nature and do not add anything substantial to the stability or functionality of Speckle **will generally not be accepted**.
|
||||
Changes that are cosmetic in nature and do not add anything substantial to the stability or functionality of Speckle **will generally not be accepted**.
|
||||
|
||||
Why? However trivial the changes might seem, there might be subtle reasons for the original code to be as it is. Furthermore, there are a lot of potential hidden costs (that even maintainers themselves are not aware of fully!) and they eat up review time unncessarily.
|
||||
|
||||
> **Examples**: modifying the colour of an UI element in one client may have a big hidden cost and need propagation in several other clients that implement a similar ui element. Changing the default port or specifiying `localhost` instead of `0.0.0.0` breaks cross-vm debugging and developing.
|
||||
|
||||
> **Examples**: modifying the colour of an UI element in one client may have a big hidden cost and need propagation in several other clients that implement a similar ui element. Changing the default port or specifiying `localhost` instead of `0.0.0.0` breaks cross-vm debugging and developing.
|
||||
|
||||
## Wrap up
|
||||
Don't worry if you get things wrong. We all do, including project owners: this document should've been here a long time ago. There's plenty of room for discussion either on our community [forum](https://discourse.speckle.works) or [chat](https://speckle-works.slack.com/join/shared_invite/enQtNjY5Mzk2NTYxNTA4LTU4MWI5ZjdhMjFmMTIxZDIzOTAzMzRmMTZhY2QxMmM1ZjVmNzJmZGMzMDVlZmJjYWQxYWU0MWJkYmY3N2JjNGI).
|
||||
|
||||
Don't worry if you get things wrong. We all do, including project owners: this document should've been here a long time ago. There's plenty of room for discussion on our community [forum](https://discourse.speckle.works).
|
||||
|
||||
🙌❤️💙💚💜🙌
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
FROM node:12.20.1-alpine3.12@sha256:42998ae4420998ff3255fc2d6884e882bd32f06d45b057f4b042e33bf48a1240 as build
|
||||
# Having multiple steps in builder doesn't increase the final image size
|
||||
# So having verbose steps for readability and caching should be the target
|
||||
|
||||
WORKDIR /opt
|
||||
|
||||
# Copy package defs first they are the least likely to change
|
||||
# Keeping this order will least likely trigger full rebuild
|
||||
COPY packages/frontend/package*.json frontend/
|
||||
RUN npm --prefix frontend ci frontend
|
||||
|
||||
COPY packages/server/package*.json server/
|
||||
ENV NODE_ENV production
|
||||
RUN npm --prefix server ci server
|
||||
|
||||
# Copy remaining files across for frontend. Changes to these files
|
||||
# will be more common than changes to the dependencies. This should
|
||||
# speed up rebuilds.
|
||||
COPY packages/frontend frontend
|
||||
|
||||
WORKDIR /opt/frontend
|
||||
RUN npm run build
|
||||
|
||||
FROM node:12.20.1-alpine3.12@sha256:42998ae4420998ff3255fc2d6884e882bd32f06d45b057f4b042e33bf48a1240
|
||||
|
||||
RUN apk add --no-cache tini=0.19.0-r0
|
||||
|
||||
# Use a non-root user for increased security.
|
||||
USER node
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
# Copy dependencies and static files from build layer
|
||||
COPY --from=build --chown=node /opt/frontend/dist /home/node/frontend/dist
|
||||
COPY --from=build --chown=node /opt/server /home/node/server
|
||||
|
||||
# Run the application from the non root users home directory
|
||||
WORKDIR /home/node/server
|
||||
|
||||
# Copy remaining files across for the server. Changes to these
|
||||
# files will be more common than changes to the dependencies.
|
||||
# This should speed up rebuilds.
|
||||
COPY --chown=node packages/server /home/node/server
|
||||
|
||||
# Init for containers https://github.com/krallin/tini
|
||||
ENTRYPOINT [ "/sbin/tini", "--" ]
|
||||
|
||||
CMD ["node", "bin/www"]
|
||||
@@ -1,9 +1,9 @@
|
||||
# The Speckle Frontend App
|
||||
|
||||
[](https://twitter.com/SpeckleSystems) [](https://discourse.speckle.works)
|
||||
[](https://speckle-works.slack.com/join/shared_invite/enQtNjY5Mzk2NTYxNTA4LTU4MWI5ZjdhMjFmMTIxZDIzOTAzMzRmMTZhY2QxMmM1ZjVmNzJmZGMzMDVlZmJjYWQxYWU0MWJkYmY3N2JjNGI) [](https://speckle.systems)
|
||||
[](https://twitter.com/SpeckleSystems) [](https://discourse.speckle.works) [](https://speckle.systems)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
We're working to stabilize the 2.0 API, and until then there will be breaking changes.
|
||||
|
||||
Notes:
|
||||
@@ -12,30 +12,28 @@ Notes:
|
||||
|
||||
- In **production** mode, the Speckle Server will statically serve the frontend app from `/dist`. You will need to run `npm run build` to populate this folder.
|
||||
|
||||
|
||||
## Project setup
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
|
||||
### Compiles and minifies for production
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Community
|
||||
|
||||
If in trouble, the Speckle Community hangs out in two main places, usually:
|
||||
- on [the forum](https://discourse.speckle.works)
|
||||
- on [the chat](https://speckle-works.slack.com/join/shared_invite/enQtNjY5Mzk2NTYxNTA4LTU4MWI5ZjdhMjFmMTIxZDIzOTAzMzRmMTZhY2QxMmM1ZjVmNzJmZGMzMDVlZmJjYWQxYWU0MWJkYmY3N2JjNGI)
|
||||
|
||||
Do join and introduce yourself! We're happy to help.
|
||||
If in trouble, the Speckle Community hangs out on [the forum](https://discourse.speckle.works). Do join and introduce yourself! We're happy to help.
|
||||
|
||||
## License
|
||||
|
||||
Unless otherwise described, the code in this repository is licensed under the Apache-2.0 License. Please note that some modules, extensions or code herein might be otherwise licensed. This is indicated either in the root of the containing folder under a different license file, or in the respective file's header. If you have any questions, don't hesitate to get in touch with us via [email](mailto:hello@speckle.systems).
|
||||
|
||||
Vendored
+64
-3
@@ -1,6 +1,67 @@
|
||||
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel=icon href=/favicon.ico><title>Speckle!</title><link rel=stylesheet href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900"><link rel=stylesheet href=https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css><link href=/js/chunk-2d22d746.95802905.js rel=prefetch><link href=/css/chunk-vendors.166fd10e.css rel=preload as=style><link href=/js/app.d87c0378.js rel=preload as=script><link href=/js/chunk-vendors.d0101c4c.js rel=preload as=script><link href=/css/chunk-vendors.166fd10e.css rel=stylesheet></head><body><noscript><strong>We're sorry but Speckle doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app><div style="width: 500px;
|
||||
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel=icon href=/favicon.ico><title>Speckle</title><link rel=stylesheet href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900"><link rel=stylesheet href=https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css><style type=text/css>body {
|
||||
background-color: #333333;
|
||||
color: #0A66FF;
|
||||
}
|
||||
@media screen and (prefers-color-scheme: light) {
|
||||
body {
|
||||
background-color: white;
|
||||
color: #0A66FF;
|
||||
}
|
||||
}
|
||||
.tada {
|
||||
-webkit-animation-name: tada;
|
||||
animation-name: tada;
|
||||
-webkit-animation-duration: 1s;
|
||||
animation-duration: 1s;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
@-webkit-keyframes tada {
|
||||
0% {
|
||||
-webkit-transform: scale3d(1, 1, 1);
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
10%, 20% {
|
||||
-webkit-transform: scale3d(.8, .8, .8) rotate3d(0, 0, 1, -3deg);
|
||||
transform: scale3d(.8, .8, .8) rotate3d(0, 0, 1, -3deg);
|
||||
}
|
||||
30%, 50%, 70%, 90% {
|
||||
-webkit-transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, 3deg);
|
||||
transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, 3deg);
|
||||
}
|
||||
40%, 60%, 80% {
|
||||
-webkit-transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, -3deg);
|
||||
transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, -3deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: scale3d(1, 1, 1);
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
}
|
||||
@keyframes tada {
|
||||
0% {
|
||||
-webkit-transform: scale3d(1, 1, 1);
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
10%, 20% {
|
||||
-webkit-transform: scale3d(.8, .8, .8) rotate3d(0, 0, 1, -3deg);
|
||||
transform: scale3d(.8, .8, .8) rotate3d(0, 0, 1, -3deg);
|
||||
}
|
||||
30%, 50%, 70%, 90% {
|
||||
-webkit-transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, 3deg);
|
||||
transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, 3deg);
|
||||
}
|
||||
40%, 60%, 80% {
|
||||
-webkit-transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, -3deg);
|
||||
transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, -3deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: scale3d(1, 1, 1);
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
}</style><link href=/css/chunk-0226d98a.b487c253.css rel=prefetch><link href=/css/chunk-0795574d.57e8e936.css rel=prefetch><link href=/css/chunk-087a4dc2.807c857c.css rel=prefetch><link href=/css/chunk-096b3c01.9814ad34.css rel=prefetch><link href=/css/chunk-14db91b4.fab89c46.css rel=prefetch><link href=/css/chunk-19772257.1bbdaac2.css rel=prefetch><link href=/css/chunk-25397a3a.f8b8fe72.css rel=prefetch><link href=/css/chunk-36241c9a.9814ad34.css rel=prefetch><link href=/css/chunk-39862bf6.4b83cbbc.css rel=prefetch><link href=/css/chunk-43d2f673.7bdd0c43.css rel=prefetch><link href=/css/chunk-450e6695.085bcd31.css rel=prefetch><link href=/css/chunk-48576b80.a81e8c89.css rel=prefetch><link href=/css/chunk-4b468322.1db2e0dc.css rel=prefetch><link href=/css/chunk-548f0a04.06dc1efa.css rel=prefetch><link href=/css/chunk-5dfad28e.ec7650a9.css rel=prefetch><link href=/css/chunk-732ba71e.c91751e1.css rel=prefetch><link href=/css/chunk-7b743cbb.be6ce596.css rel=prefetch><link href=/css/chunk-8714b1d6.1f2409f5.css rel=prefetch><link href=/css/chunk-a71fa94e.e2661ae6.css rel=prefetch><link href=/css/chunk-aa0d0836.d07f36a8.css rel=prefetch><link href=/css/chunk-e850d9f8.47056576.css rel=prefetch><link href=/js/chunk-0226d98a.a6aa6690.js rel=prefetch><link href=/js/chunk-0795574d.30510593.js rel=prefetch><link href=/js/chunk-087a4dc2.5f5801fd.js rel=prefetch><link href=/js/chunk-096b3c01.1f534773.js rel=prefetch><link href=/js/chunk-14db91b4.82d563d8.js rel=prefetch><link href=/js/chunk-19772257.18517826.js rel=prefetch><link href=/js/chunk-25397a3a.43a91db7.js rel=prefetch><link href=/js/chunk-2d0c8c18.262ebe54.js rel=prefetch><link href=/js/chunk-2d0c8ff6.aeda151c.js rel=prefetch><link href=/js/chunk-2d22cdf7.cdd6b65e.js rel=prefetch><link href=/js/chunk-36241c9a.8f38a52d.js rel=prefetch><link href=/js/chunk-39862bf6.a29f4798.js rel=prefetch><link href=/js/chunk-43d2f673.850713f5.js rel=prefetch><link href=/js/chunk-450e6695.ce53888c.js rel=prefetch><link href=/js/chunk-48576b80.be0d7cd6.js rel=prefetch><link href=/js/chunk-4b468322.6882c34f.js rel=prefetch><link href=/js/chunk-548f0a04.0869eeb3.js rel=prefetch><link href=/js/chunk-5dfad28e.e97e8983.js rel=prefetch><link href=/js/chunk-732ba71e.68a369ad.js rel=prefetch><link href=/js/chunk-7b743cbb.60417f72.js rel=prefetch><link href=/js/chunk-8714b1d6.ced4a341.js rel=prefetch><link href=/js/chunk-a71fa94e.15cfe94c.js rel=prefetch><link href=/js/chunk-aa0d0836.c05e2de4.js rel=prefetch><link href=/js/chunk-e850d9f8.8e1fb453.js rel=prefetch><link href=/css/app.fa86b527.css rel=preload as=style><link href=/css/chunk-vendors.78cc2cdf.css rel=preload as=style><link href=/js/app.79f658da.js rel=preload as=script><link href=/js/chunk-vendors.0f88b451.js rel=preload as=script><link href=/css/chunk-vendors.78cc2cdf.css rel=stylesheet><link href=/css/app.fa86b527.css rel=stylesheet></head><body><noscript><strong>We're sorry but Speckle doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app><div style="width: 100%;
|
||||
height: 300px;
|
||||
line-height: 300px;
|
||||
font-family: sans-serif !important;
|
||||
position: absolute;
|
||||
top:0;
|
||||
@@ -10,4 +71,4 @@
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
font-size: 60px;">🔑</div><script src=/js/chunk-vendors.d0101c4c.js></script><script src=/js/app.d87c0378.js></script></div></body></html>
|
||||
font-size: 10px;"><img src=/logo.svg style="max-width: 50px" class=tada></div><script src=/js/chunk-vendors.0f88b451.js></script><script src=/js/app.79f658da.js></script></div></body></html>
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel=icon href=/favicon.ico><title>Speckle Authentication</title><link rel=stylesheet href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900"><link rel=stylesheet href=https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css><link href=/css/chunk-8a745654.cc701a71.css rel=prefetch><link href=/css/chunk-a84ba648.811ab999.css rel=prefetch><link href=/js/chunk-2d0c7e58.005edb5a.js rel=prefetch><link href=/js/chunk-2d226189.28513e35.js rel=prefetch><link href=/js/chunk-8a745654.4f583db6.js rel=prefetch><link href=/js/chunk-a84ba648.95f05f26.js rel=prefetch><link href=/css/chunk-vendors.166fd10e.css rel=preload as=style><link href=/js/auth.7008088e.js rel=preload as=script><link href=/js/chunk-vendors.d0101c4c.js rel=preload as=script><link href=/css/chunk-vendors.166fd10e.css rel=stylesheet></head><body><noscript><strong>We're sorry but Speckle Authentication doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=/js/chunk-vendors.d0101c4c.js></script><script src=/js/auth.7008088e.js></script></body></html>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 15 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.5 100"><defs><style>.cls-1{fill:#1697f6;}.cls-2{fill:#7bc6ff;}.cls-3{fill:#1867c0;}.cls-4{fill:#aeddff;}</style></defs><title>Artboard 46</title><polyline class="cls-1" points="43.75 0 23.31 0 43.75 48.32"/><polygon class="cls-2" points="43.75 62.5 43.75 100 0 14.58 22.92 14.58 43.75 62.5"/><polyline class="cls-3" points="43.75 0 64.19 0 43.75 48.32"/><polygon class="cls-4" points="64.58 14.58 87.5 14.58 43.75 100 43.75 62.5 64.58 14.58"/></svg>
|
||||
|
Before Width: | Height: | Size: 539 B |
@@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 871 400" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,-4531,-5539)">
|
||||
<g id="Artboard1" transform="matrix(1,0,0,1,4187.76,4786.57)">
|
||||
<rect x="0" y="0" width="3840" height="2160" style="fill:none;"/>
|
||||
<g transform="matrix(0.464023,-0.0256584,0,0.465442,142.078,679.424)">
|
||||
<rect x="1294.87" y="614.156" width="204.453" height="204.719" style="fill:rgb(4,126,251);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.459998,-0.0254359,0.24376,0.219132,280.374,798.898)">
|
||||
<rect x="643.94" y="618.105" width="206.242" height="64.29" style="fill:rgb(123,188,255);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.168269,0.151268,0,0.624912,434.991,941.085)">
|
||||
<rect x="1736.88" y="-457.431" width="93.132" height="152.477" style="fill:rgb(49,59,207);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.606506,-0.0335371,0,0.609944,-2858.9,-955.205)">
|
||||
<text x="5970.62px" y="3532.8px" style="font-family:'Iosevka', monospace;font-size:92.552px;fill:white;">S2</text>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
-2
File diff suppressed because one or more lines are too long
-2
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
|
||||
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2d0c7e58"],{5326:function(e,r,t){"use strict";t.r(r);var a=function(){var e=this,r=e.$createElement,t=e._self._c||r;return e.hasLocalStrategy?t("v-container",{attrs:{fluid:""}},[t("v-form",{ref:"form"},[t("v-row",{staticStyle:{"margin-top":"-10px"},attrs:{dense:""}},[t("v-col",{attrs:{cols:"12"}},[t("v-text-field",{attrs:{label:"your email",rules:e.validation.emailRules,solo:""},model:{value:e.form.email,callback:function(r){e.$set(e.form,"email",r)},expression:"form.email"}})],1),t("v-col",{attrs:{cols:"12"}},[t("v-text-field",{staticStyle:{"margin-top":"-12px"},attrs:{label:"password",type:"password",rules:e.validation.passwordRules,solo:""},model:{value:e.form.password,callback:function(r){e.$set(e.form,"password",r)},expression:"form.password"}}),t("v-btn",{staticStyle:{top:"-22px"},attrs:{block:"",large:"",color:"accent"},on:{click:e.loginUser}},[e._v("Log in")]),t("p",{staticClass:"text-center"},[t("v-btn",{attrs:{text:"",small:"",block:"",color:"accent",to:{name:"Register",query:{appId:e.$route.query.appId}}}},[e._v("Create Account")])],1)],1)],1)],1),t("v-snackbar",{attrs:{"multi-line":""},model:{value:e.registrationError,callback:function(r){e.registrationError=r},expression:"registrationError"}},[e._v(" "+e._s(e.errorMessage)+" "),t("v-btn",{attrs:{color:"red",text:""},on:{click:function(r){e.registrationError=!1}}},[e._v(" Close ")])],1)],1):e._e()},o=[],n=(t("99af"),t("c740"),t("d3b7"),t("ac1f"),t("3ca3"),t("841c"),t("ddb0"),t("2b3d"),t("96cf"),t("1da1")),l=t("8785"),s=t("9530"),i=t.n(s),c=(t("ca94"),t("f7fe"),t("4556")),u=t.n(c);function d(){var e=Object(l["a"])([" query { serverInfo { name company adminContact termsOfService scopes { name description } authStrategies { id name color icon url } } } "]);return d=function(){return e},e}var p={name:"Login",apollo:{serverInfo:{query:i()(d())}},computed:{hasLocalStrategy:function(){return-1!==this.serverInfo.authStrategies.findIndex((function(e){return"local"===e.id}))}},methods:{loginUser:function(){var e=this;return Object(n["a"])(regeneratorRuntime.mark((function r(){var t,a;return regeneratorRuntime.wrap((function(r){while(1)switch(r.prev=r.next){case 0:if(r.prev=0,t=e.$refs.form.validate(),t){r.next=4;break}throw new Error("Form validation failed");case 4:return r.next=6,fetch("/auth/local/login?appId=".concat(e.appId,"&challenge=").concat(e.challenge),{method:"POST",headers:{"Content-Type":"application/json"},redirect:"follow",body:JSON.stringify({email:e.form.email,password:e.form.password})});case 6:if(a=r.sent,console.log(a),a.redirected&&(window.location=a.url),a.ok){r.next=11;break}throw new Error("Login failed");case 11:r.next=17;break;case 13:r.prev=13,r.t0=r["catch"](0),e.errorMessage=r.t0.message,e.registrationError=!0;case 17:case"end":return r.stop()}}),r,null,[[0,13]])})))()}},data:function(){return{serverInfo:{authStrategies:[]},form:{email:null,password:null},validation:{passwordRules:[function(e){return!!e||"Required"}],emailRules:[function(e){return!!e||"E-mail is required"},function(e){return/.+@.+\..+/.test(e)||"E-mail must be valid"}]},registrationError:!1,errorMessage:"",appId:null,serverApp:null}},mounted:function(){var e=new URLSearchParams(window.location.search),r=e.get("appId"),t=e.get("challenge");this.appId=r||"spklwebapp",t||"spklwebapp"!==this.appId?t&&(this.challenge=t):(this.challenge=u()({length:10}),localStorage.setItem("appChallenge",this.challenge))}},f=p,m=t("2877"),g=t("6544"),v=t.n(g),h=t("8336"),w=t("62ad"),b=t("a523"),k=t("4bd4"),x=t("0fd9"),y=t("2db4"),I=t("8654"),S=Object(m["a"])(f,a,o,!1,null,null,null);r["default"]=S.exports;v()(S,{VBtn:h["a"],VCol:w["a"],VContainer:b["a"],VForm:k["a"],VRow:x["a"],VSnackbar:y["a"],VTextField:I["a"]})}}]);
|
||||
//# sourceMappingURL=chunk-2d0c7e58.005edb5a.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
|
||||
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2d22d746"],{f820:function(t,e,n){"use strict";n.r(e);var a=function(){var t=this,e=t.$createElement;t._self._c;return t._m(0)},s=[function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"about"},[n("h1",[t._v("This is an about page")])])}],u=n("2877"),c={},i=Object(u["a"])(c,a,s,!1,null,null,null);e["default"]=i.exports}}]);
|
||||
//# sourceMappingURL=chunk-2d22d746.95802905.js.map
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Generated
+5
@@ -9580,6 +9580,11 @@
|
||||
"lodash._reinterpolate": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"lodash.throttle": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
|
||||
"integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
|
||||
},
|
||||
"lodash.transform": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.transform/-/lodash.transform-4.6.0.tgz",
|
||||
|
||||
@@ -10,12 +10,14 @@
|
||||
"serve:setup": "vue-cli-service serve"
|
||||
},
|
||||
"dependencies": {
|
||||
"@speckle/viewer": "^2.0.0",
|
||||
"@vuejs-community/vue-filter-date-format": "^1.6.3",
|
||||
"@vuejs-community/vue-filter-date-parse": "^1.1.6",
|
||||
"core-js": "^3.8.1",
|
||||
"crypto-random-string": "^3.3.0",
|
||||
"dompurify": "^2.2.4",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"marked": "^1.2.6",
|
||||
"v-tooltip": "^2.0.3",
|
||||
"vue": "^2.6.12",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="text-center" style="position: absolute">
|
||||
<div class="text-center py-3" style="position: absolute">
|
||||
<user-avatar :id="user.id" :avatar="user.avatar" :name="user.name" :size="30" />
|
||||
</div>
|
||||
<div class="ml-12">
|
||||
<v-row class="caption">
|
||||
<v-row class="caption py-3">
|
||||
<v-col class="pb-2">
|
||||
<v-icon small>mdi-history</v-icon>
|
||||
You have
|
||||
|
||||
@@ -9,8 +9,14 @@
|
||||
/>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
<v-list-item-title class="my-2 py-1">
|
||||
{{ commit.message }}
|
||||
<span class="caption" v-if="commit.branchName">
|
||||
<v-chip small style="top:-3px">
|
||||
<v-icon small class="mr-2">mdi-source-branch</v-icon>
|
||||
{{ commit.branchName }}
|
||||
</v-chip>
|
||||
</span>
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="caption">
|
||||
<b>{{ commit.authorName }}</b>
|
||||
@@ -19,13 +25,17 @@
|
||||
({{ commitDate }})
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<source-app-avatar :application-name="commit.sourceApplication" />
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<script>
|
||||
import UserAvatar from './UserAvatar'
|
||||
import SourceAppAvatar from './SourceAppAvatar'
|
||||
|
||||
export default {
|
||||
components: { UserAvatar },
|
||||
components: { UserAvatar, SourceAppAvatar },
|
||||
props: ['commit', 'streamId'],
|
||||
computed: {
|
||||
commitDate() {
|
||||
@@ -34,6 +44,15 @@ export default {
|
||||
let options = { year: 'numeric', month: 'long', day: 'numeric' }
|
||||
|
||||
return date.toLocaleString(undefined, options)
|
||||
},
|
||||
branchUrl() {
|
||||
if (!this.commit) return null
|
||||
return `${window.location.origin}/streams/${this.$route.params.streamId}/branches/${this.commit.branchName}`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goToBranch() {
|
||||
this.$router.push(this.branchUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<v-sheet style="height: 100%" class="transparent">
|
||||
<div
|
||||
id="rendererparent"
|
||||
ref="rendererparent"
|
||||
:class="`${fullScreen ? 'fullscreen' : ''} ${darkMode ? 'dark' : ''}`"
|
||||
>
|
||||
<v-fade-transition>
|
||||
<div v-show="!hasLoadedModel" class="overlay cover-all">
|
||||
<v-btn large class="vertical-center" @click="load()">
|
||||
<v-icon class="mr-3">mdi-cube-outline</v-icon>
|
||||
View Data
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-fade-transition>
|
||||
<v-progress-linear
|
||||
v-if="hasLoadedModel && loadProgress < 99"
|
||||
v-model="loadProgress"
|
||||
height="4"
|
||||
rounded
|
||||
class="vertical-center elevation-10"
|
||||
style="position: absolute; width: 80%; left: 10%; opacity: 0.5"
|
||||
></v-progress-linear>
|
||||
<v-card
|
||||
v-show="hasLoadedModel"
|
||||
style="position: absolute; bottom: 0px; z-index: 2; width: 100%"
|
||||
class="pa-0 text-center transparent elevation-0 pb-3"
|
||||
>
|
||||
<v-btn-toggle class="elevation-0">
|
||||
<v-tooltip top>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn :small="!fullScreen" v-bind="attrs" v-on="on" @click="zoomEx()">
|
||||
<v-icon small>mdi-cube-scan</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
Focus entire model
|
||||
</v-tooltip>
|
||||
<v-tooltip top>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn :small="!fullScreen" v-bind="attrs" @click="sectionToggle()" v-on="on">
|
||||
<v-icon small>mdi-scissors-cutting</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
Show / Hide Section plane
|
||||
</v-tooltip>
|
||||
<v-tooltip top>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn
|
||||
:small="!fullScreen"
|
||||
v-bind="attrs"
|
||||
@click="fullScreen = !fullScreen"
|
||||
v-on="on"
|
||||
>
|
||||
<v-icon small>{{ fullScreen ? 'mdi-fullscreen-exit' : 'mdi-fullscreen' }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
Full screen
|
||||
</v-tooltip>
|
||||
<v-tooltip top>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn :small="!fullScreen" v-bind="attrs" @click="showHelp = !showHelp" v-on="on">
|
||||
<v-icon small>mdi-help</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
Show viewer help
|
||||
</v-tooltip>
|
||||
<v-dialog v-model="showHelp" max-width="290">
|
||||
<v-card>
|
||||
<v-card-text class="pt-7">
|
||||
<v-icon class="mr-2">mdi-rotate-orbit</v-icon>
|
||||
Use your
|
||||
<b>left mouse button</b>
|
||||
to rotate the view.
|
||||
<br />
|
||||
<br />
|
||||
<v-icon class="mr-2">mdi-pan</v-icon>
|
||||
Use your
|
||||
<b>right mouse button</b>
|
||||
to pan the view.
|
||||
<br />
|
||||
<br />
|
||||
<v-icon class="mr-2">mdi-cursor-default-click</v-icon>
|
||||
<b>Double clicking an object</b>
|
||||
focus it in the camera view.
|
||||
<br />
|
||||
<br />
|
||||
<v-icon class="mr-2">mdi-cursor-default-click-outline</v-icon>
|
||||
<b>Double clicking on the background</b>
|
||||
will focus again the entire scene.
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-btn-toggle>
|
||||
</v-card>
|
||||
</div>
|
||||
</v-sheet>
|
||||
</template>
|
||||
<script>
|
||||
import throttle from 'lodash.throttle'
|
||||
import { Viewer } from '@speckle/viewer'
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
props: {
|
||||
autoLoad: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
objectUrl: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasLoadedModel: false,
|
||||
loadProgress: 0,
|
||||
fullScreen: false,
|
||||
showHelp: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
darkMode() {
|
||||
return this.$vuetify.theme.dark
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
fullScreen() {
|
||||
setTimeout(() => window.__viewer.onWindowResize(), 20)
|
||||
}
|
||||
},
|
||||
// TODO: pause rendering on destroy, reinit on mounted.
|
||||
mounted() {
|
||||
// NOTE: we're doing some globals and dom shennanigans in here for the purpose
|
||||
// of having a unique global renderer and it's container dom element. The principles
|
||||
// are simple enough:
|
||||
// - create a single 'renderer' container div
|
||||
// - initialise the actual renderer **once** (per app lifecycle, on refresh it's fine)
|
||||
// - juggle the container div out of this component's dom when the component is managed out by vue
|
||||
// - juggle the container div back in of this component's dom when it's back.
|
||||
let renderDomElement = document.getElementById('renderer')
|
||||
|
||||
if (!renderDomElement) {
|
||||
renderDomElement = document.createElement('div')
|
||||
renderDomElement.id = 'renderer'
|
||||
}
|
||||
|
||||
this.domElement = renderDomElement
|
||||
this.domElement.style.display = 'inline-block'
|
||||
this.$refs.rendererparent.appendChild(renderDomElement)
|
||||
window.__viewer = window.__viewer || new Viewer({ container: renderDomElement })
|
||||
window.__viewer.onWindowResize()
|
||||
|
||||
if (window.__viewerLastLoadedUrl !== this.objectUrl) {
|
||||
window.__viewer.sceneManager.removeAllObjects()
|
||||
window.__viewerLastLoadedUrl = null
|
||||
} else {
|
||||
this.hasLoadedModel = true
|
||||
this.loadProgress = 100
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
// NOTE: here's where we juggle the container div out, and do cleanup on the
|
||||
// viewer end.
|
||||
// hide renderer dom element.
|
||||
this.domElement.style.display = 'none'
|
||||
// move renderer dom element outside this component so it doesn't get deleted.
|
||||
document.body.appendChild(this.domElement)
|
||||
},
|
||||
methods: {
|
||||
zoomEx() {
|
||||
window.__viewer.sceneManager.zoomExtents()
|
||||
},
|
||||
sectionToggle() {
|
||||
window.__viewer.sectionPlaneHelper.toggleSectionPlanes()
|
||||
},
|
||||
load() {
|
||||
if (!this.objectUrl) return
|
||||
this.hasLoadedModel = true
|
||||
window.__viewer.loadObject(this.objectUrl)
|
||||
window.__viewerLastLoadedUrl = this.objectUrl
|
||||
window.__viewer.on(
|
||||
'load-progress',
|
||||
throttle(
|
||||
function (args) {
|
||||
this.loadProgress = args.progress * 100
|
||||
this.zoomEx()
|
||||
}.bind(this),
|
||||
200
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
#rendererparent {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.fullscreen {
|
||||
position: fixed !important;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
/*background-color: rgb(58, 59, 60);*/
|
||||
background-color: rgb(238, 238, 238);
|
||||
}
|
||||
|
||||
.dark {
|
||||
background-color: rgb(58, 59, 60) !important;
|
||||
}
|
||||
|
||||
#renderer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cover-all {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(60, 94, 128, 0.8519782913165266) 0%,
|
||||
rgba(63, 123, 135, 0.13489145658263302) 100%
|
||||
);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vertical-center {
|
||||
margin: 0;
|
||||
top: 50%;
|
||||
-ms-transform: translateY(-50%);
|
||||
transform: translateY(-50%);
|
||||
z-index: 2;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<v-tooltip bottom>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-chip
|
||||
small
|
||||
class="ma-1 caption white--text"
|
||||
:color="color"
|
||||
:size="size"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
{{ shortName }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<span>Source application: {{ applicationName ? applicationName : 'unknown' }}</span>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
size: {
|
||||
type: Number,
|
||||
default: 30
|
||||
},
|
||||
applicationName: {
|
||||
type: String,
|
||||
default: '?'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
color() {
|
||||
if (!this.applicationName) return 'grey'
|
||||
switch (this.applicationName.toLowerCase()) {
|
||||
case 'revit2020':
|
||||
case 'revit2021':
|
||||
case 'revit2022':
|
||||
case 'revit2023':
|
||||
return 'blue darken-1'
|
||||
case 'rhino':
|
||||
return 'black'
|
||||
case 'grasshopper':
|
||||
return 'green darken-2'
|
||||
case 'dynamo':
|
||||
return 'purple darken-1'
|
||||
default:
|
||||
return 'grey'
|
||||
}
|
||||
},
|
||||
shortName() {
|
||||
if (!this.applicationName) return '?'
|
||||
switch (this.applicationName.toLowerCase()) {
|
||||
case 'revit2020':
|
||||
case 'revit2021':
|
||||
case 'revit2022':
|
||||
case 'revit2023':
|
||||
return 'RVT'
|
||||
case 'rhino':
|
||||
return 'RH'
|
||||
case 'grasshopper':
|
||||
return 'GH'
|
||||
case 'dynamo':
|
||||
return 'DYN'
|
||||
default:
|
||||
return '?'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -9,6 +9,8 @@ query Stream($streamid: String!, $id: String!) {
|
||||
authorId
|
||||
authorAvatar
|
||||
createdAt
|
||||
branchName
|
||||
sourceApplication
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ query StreamCommits($id: String!) {
|
||||
authorAvatar
|
||||
createdAt
|
||||
message
|
||||
referencedObject
|
||||
branchName
|
||||
sourceApplication
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<v-row v-if="stream">
|
||||
<v-col cols="12">
|
||||
<v-card class="pa-0 mb-3" elevation="0" rounded="lg" color="transparent" style="height: 50vh">
|
||||
<renderer :object-url="commitObjectUrl" />
|
||||
</v-card>
|
||||
<v-card class="pa-4" elevation="0" rounded="lg" color="background2">
|
||||
<v-card-title class="mr-8">
|
||||
<v-icon class="mr-2">mdi-source-commit</v-icon>
|
||||
@@ -19,6 +22,21 @@
|
||||
class="ml-1"
|
||||
></user-avatar>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
Branch:
|
||||
<v-chip
|
||||
small
|
||||
:to="`/streams/${$route.params.streamId}/branches/${encodeURIComponent(
|
||||
stream.commit.branchName
|
||||
)}`"
|
||||
>
|
||||
<v-icon small class="mr-2">mdi-source-branch</v-icon>
|
||||
{{ stream.commit.branchName }}
|
||||
</v-chip>
|
||||
<br />
|
||||
Source Application:
|
||||
<source-app-avatar :application-name="stream.commit.sourceApplication" />
|
||||
</v-card-text>
|
||||
<commit-dialog ref="commitDialog"></commit-dialog>
|
||||
<v-btn
|
||||
v-tooltip="'Edit commit details'"
|
||||
@@ -70,16 +88,19 @@
|
||||
</template>
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import SidebarStream from '../components/SidebarStream'
|
||||
import UserAvatar from '../components/UserAvatar'
|
||||
import ObjectSpeckleViewer from '../components/ObjectSpeckleViewer'
|
||||
import Renderer from '../components/Renderer'
|
||||
import streamCommitQuery from '../graphql/commit.gql'
|
||||
import CommitDialog from '../components/dialogs/CommitDialog'
|
||||
import SourceAppAvatar from '../components/SourceAppAvatar'
|
||||
|
||||
export default {
|
||||
name: 'Commit',
|
||||
components: { SidebarStream, CommitDialog, UserAvatar, ObjectSpeckleViewer },
|
||||
data: () => ({}),
|
||||
components: { CommitDialog, UserAvatar, ObjectSpeckleViewer, Renderer, SourceAppAvatar },
|
||||
data: () => ({
|
||||
loadedModel: false
|
||||
}),
|
||||
apollo: {
|
||||
stream: {
|
||||
prefetch: true,
|
||||
@@ -106,6 +127,9 @@ export default {
|
||||
speckle_type: 'reference',
|
||||
referencedId: this.stream.commit.referencedObject
|
||||
}
|
||||
},
|
||||
commitObjectUrl() {
|
||||
return `${window.location.origin}/streams/${this.stream.id}/objects/${this.commitObject.referencedId}`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12">
|
||||
<v-card class="pa-4" elevation="0" rounded="lg" color="background2">
|
||||
<v-card class="pa-0" elevation="0" rounded="lg" color="transparent" style="height: 50vh">
|
||||
<renderer :object-url="commitObjectUrl" />
|
||||
</v-card>
|
||||
<v-card class="pa-4 mt-3" elevation="0" rounded="lg" color="background2">
|
||||
<v-card-title class="mr-8">
|
||||
<v-icon class="mr-2">mdi-database</v-icon>
|
||||
Object {{ $route.params.objectId }}
|
||||
@@ -19,18 +22,22 @@
|
||||
</v-row>
|
||||
</template>
|
||||
<script>
|
||||
import ObjectSpeckleViewer from "../components/ObjectSpeckleViewer"
|
||||
import ObjectSpeckleViewer from '../components/ObjectSpeckleViewer'
|
||||
import Renderer from '../components/Renderer'
|
||||
|
||||
export default {
|
||||
name: "ObjectViewer",
|
||||
components: { ObjectSpeckleViewer },
|
||||
name: 'ObjectViewer',
|
||||
components: { ObjectSpeckleViewer, Renderer },
|
||||
computed: {
|
||||
commitObject() {
|
||||
return {
|
||||
// eslint-disable-next-line camelcase
|
||||
speckle_type: "reference",
|
||||
speckle_type: 'reference',
|
||||
referencedId: this.$route.params.objectId
|
||||
}
|
||||
},
|
||||
commitObjectUrl() {
|
||||
return `${window.location.origin}/streams/${this.$route.params.streamId}/objects/${this.$route.params.objectId}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col sm="12">
|
||||
<v-card class="mb-4 transparent" rounded="lg" elevation="0">
|
||||
<div v-if="latestCommit" style="height: 50vh">
|
||||
<renderer :object-url="latestCommitObjectUrl" />
|
||||
</div>
|
||||
<v-sheet v-if="latestCommit">
|
||||
<list-item-commit
|
||||
:commit="latestCommit"
|
||||
:stream-id="$route.params.streamId"
|
||||
></list-item-commit>
|
||||
</v-sheet>
|
||||
<v-sheet v-else>
|
||||
<v-card-title>This stream does not have any data yet.</v-card-title>
|
||||
</v-sheet>
|
||||
</v-card>
|
||||
<v-card v-if="$apollo.queries.description.loading">
|
||||
<v-skeleton-loader type="article"></v-skeleton-loader>
|
||||
</v-card>
|
||||
@@ -109,13 +123,15 @@ import StreamDescriptionDialog from '../components/dialogs/StreamDescriptionDial
|
||||
import ListItemCommit from '../components/ListItemCommit'
|
||||
import streamCommitsQuery from '../graphql/streamCommits.gql'
|
||||
import streamBranchesQuery from '../graphql/streamBranches.gql'
|
||||
import Renderer from '../components/Renderer'
|
||||
|
||||
export default {
|
||||
name: 'StreamMain',
|
||||
components: {
|
||||
NewBranchDialog,
|
||||
ListItemCommit,
|
||||
StreamDescriptionDialog
|
||||
StreamDescriptionDialog,
|
||||
Renderer
|
||||
},
|
||||
props: {
|
||||
stream: {
|
||||
@@ -182,6 +198,14 @@ export default {
|
||||
if (!this.description) return ''
|
||||
let md = marked(this.description)
|
||||
return DOMPurify.sanitize(md)
|
||||
},
|
||||
latestCommit() {
|
||||
if (!this.commits) return null
|
||||
return this.commits.items[0]
|
||||
},
|
||||
latestCommitObjectUrl() {
|
||||
if (!this.latestCommit) return null
|
||||
return `${window.location.origin}/streams/${this.$route.params.streamId}/objects/${this.latestCommit.referencedObject}`
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
const http = require( 'http' )
|
||||
const url = require( 'url' )
|
||||
const WebSocket = require( 'ws' )
|
||||
const express = require( 'express' )
|
||||
const compression = require( 'compression' )
|
||||
const appRoot = require( 'app-root-path' )
|
||||
@@ -87,7 +86,6 @@ exports.init = async ( ) => {
|
||||
return { app, graphqlServer }
|
||||
}
|
||||
|
||||
const { createProxyMiddleware } = require( 'http-proxy-middleware' )
|
||||
|
||||
/**
|
||||
* Starts a http server, hoisting the express app to it.
|
||||
@@ -103,6 +101,7 @@ exports.startHttp = async ( app ) => {
|
||||
// Handles frontend proxying:
|
||||
// Dev mode -> proxy form the local webpack server
|
||||
if ( process.env.NODE_ENV === 'development' ) {
|
||||
const { createProxyMiddleware } = require( 'http-proxy-middleware' )
|
||||
const frontendProxy = createProxyMiddleware( { target: `http://localhost:${frontendPort}`, changeOrigin: true, ws: false, logLevel: 'silent' } )
|
||||
app.use( '/', frontendProxy )
|
||||
|
||||
|
||||
@@ -128,7 +128,6 @@ module.exports = {
|
||||
subscribe: withFilter( () => pubsub.asyncIterator( [ COMMIT_CREATED ] ),
|
||||
async ( payload, variables, context ) => {
|
||||
await authorizeResolver( context.userId, payload.streamId, 'stream:reviewer' )
|
||||
|
||||
return payload.streamId === variables.streamId
|
||||
} )
|
||||
},
|
||||
|
||||
+17
-17
@@ -1,33 +1,38 @@
|
||||
# Speckle Server
|
||||
|
||||
[](https://twitter.com/SpeckleSystems) [](https://discourse.speckle.works)
|
||||
[](https://speckle-works.slack.com/join/shared_invite/enQtNjY5Mzk2NTYxNTA4LTU4MWI5ZjdhMjFmMTIxZDIzOTAzMzRmMTZhY2QxMmM1ZjVmNzJmZGMzMDVlZmJjYWQxYWU0MWJkYmY3N2JjNGI) [](https://speckle.systems)
|
||||
[](https://twitter.com/SpeckleSystems) [](https://discourse.speckle.works) [](https://speckle.systems)
|
||||
|
||||
#### Status
|
||||
|
||||
[](https://github.com/Speckle-Next/SpeckleServer/) [](https://codecov.io/gh/specklesystems/speckle-server)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
We're working to stabilize the 2.0 API, and until then there will be breaking changes.
|
||||
|
||||
## Introduction
|
||||
|
||||
The Speckle Server is a node application. To start it locally, simply:
|
||||
The Speckle Server is a node application. To start it locally:
|
||||
|
||||
- ensure you have a local instance of postgres & redis running
|
||||
First, ensure you have postgres and redis ready and running:
|
||||
|
||||
- ensure you have a local instance of postgres running
|
||||
- create a postgres db called `speckle2_dev`
|
||||
- then run `npm install`
|
||||
- finally `npm run dev` will start the server.
|
||||
- ensure you have an instance of redis running
|
||||
|
||||
Finally, in the `packages/server` folder:
|
||||
|
||||
- copy the `.env-example` file to `.env`,
|
||||
- open and edit the `.env` file, filling in the required variables,
|
||||
- run `npm install`,
|
||||
- finally `npm run dev`,
|
||||
- check `localhost:3000/graphql` out!
|
||||
|
||||
You can customise your local deployment by editing and filling in a `.env` file. To do so:
|
||||
|
||||
- copy the `.env-example` file to `.env`
|
||||
- open and edit the `.env` file.
|
||||
|
||||
## Developing
|
||||
|
||||
The server consists of several semi-related components, or modules. These can be found in `/modules`. Module composition:
|
||||
|
||||
- an `index.js` file that exposes two functions, `init` and `finalize` (mandatory)
|
||||
- a `graph` folder, with two subfolders, namely `resolvers` and `schemas` (optional - these will be picked up and merged).
|
||||
|
||||
@@ -56,13 +61,8 @@ To run all tests, simply run `npm run test`. To run specific tests, use the `moc
|
||||
|
||||
## Community
|
||||
|
||||
If in trouble, the Speckle Community hangs out in two main places, usually:
|
||||
- on [the forum](https://discourse.speckle.works)
|
||||
- on [the chat](https://speckle-works.slack.com/join/shared_invite/enQtNjY5Mzk2NTYxNTA4LTU4MWI5ZjdhMjFmMTIxZDIzOTAzMzRmMTZhY2QxMmM1ZjVmNzJmZGMzMDVlZmJjYWQxYWU0MWJkYmY3N2JjNGI)
|
||||
|
||||
Do join and introduce yourself! We're happy to help.
|
||||
The Speckle Community hangs out on [the forum](https://discourse.speckle.works), do join and introduce yourself & feel free to ask us questions!
|
||||
|
||||
## License
|
||||
|
||||
Unless otherwise described, the code in this repository is licensed under the Apache-2.0 License. Please note that some modules, extensions or code herein might be otherwise licensed. This is indicated either in the root of the containing folder under a different license file, or in the respective file's header. If you have any questions, don't hesitate to get in touch with us via [email](mailto:hello@speckle.systems).
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"ignore": ["node_modules/**/*"],
|
||||
"presets": [
|
||||
["@babel/preset-typescript"],
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"loose": true,
|
||||
"targets": {
|
||||
"browsers": "last 2 versions, > 0.5%, ie >= 11",
|
||||
"esmodules": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"@babel/preset-react"
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"babel-plugin-add-module-exports",
|
||||
"@babel/plugin-transform-classes"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// eslint-disable-next-line no-undef
|
||||
module.exports = {
|
||||
'env': {
|
||||
'browser': true,
|
||||
'commonjs': true,
|
||||
'es2021': true
|
||||
},
|
||||
'extends': 'eslint:recommended',
|
||||
'parserOptions': {
|
||||
'ecmaVersion': 12,
|
||||
'sourceType': 'module'
|
||||
},
|
||||
'ignorePatterns': [ 'node_modules/*' ],
|
||||
'rules': {
|
||||
'indent': [
|
||||
'error',
|
||||
2
|
||||
],
|
||||
'linebreak-style': [
|
||||
'error',
|
||||
'unix'
|
||||
],
|
||||
'quotes': [
|
||||
'error',
|
||||
'single'
|
||||
],
|
||||
'semi': [
|
||||
'error',
|
||||
'never'
|
||||
],
|
||||
'arrow-spacing': [
|
||||
2,
|
||||
{
|
||||
'before': true,
|
||||
'after': true
|
||||
}
|
||||
],
|
||||
'array-bracket-spacing': [ 2, 'always' ],
|
||||
'object-curly-spacing': [ 1, 'always' ],
|
||||
'block-spacing': [ 2, 'always' ],
|
||||
'space-in-parens': [ 2, 'always' ],
|
||||
'keyword-spacing': 2,
|
||||
'space-unary-ops': [
|
||||
2,
|
||||
{
|
||||
'words': true,
|
||||
'nonwords': false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Vendored
+2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Speckle Viewer</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css" integrity="sha512-EZLkOqwILORob+p0BXZc+Vm3RgJBOe1Iq/0fiI7r/wJgzOFZMlsqTa29UEl6v6U6gsV4uIpsNZoV32YZqrCRCQ==" crossorigin="anonymous" />
|
||||
<style type="text/css">
|
||||
body{
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
}
|
||||
button {
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
border-color: #0A66FF;
|
||||
}
|
||||
#renderer{
|
||||
height: 700px;
|
||||
width: 100%;
|
||||
border: 5px dashed;
|
||||
border-color: rgba(100,100,100, 0.1);
|
||||
}
|
||||
</style>
|
||||
<script defer src="demo.js"></script></head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row" style="padding-top: 20px">
|
||||
<div class="twelve columns">
|
||||
<h3>Viewer</h3>
|
||||
<h5>Controls summary:</h5>
|
||||
<p>Click an object to select it. Double click it to focus on it. Press `esc` to clear the selection. Press `shift-s` to toggle a section plane. Press `s` while the section plane is active to toggle its control mode. Double click anywhere outside an object to zoom extents to the entire scene.</p>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<button onclick="v.sceneManager.zoomExtents()">Zoom Extents</button>
|
||||
<button onclick="v.postprocessing = !v.postprocessing">Postprocessing Toggle</button>
|
||||
<button onclick="v.sectionPlaneHelper.toggleSectionPlanes()">Show Section Plane</button>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<input id="objectUrlInput" type="text" name="objectId" placeholder="Object Url" style="width:49%" value="https://staging.speckle.dev/streams/a75ab4f10f/objects/a59617590b0bec4e9b5ee487ee75b1a7"/>
|
||||
<button class="" onclick="LoadData()" style="width:49%">Load Object URL</button>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<input id="objectIdInput" type="text" name="objectId" placeholder="Object Id" style="width:49%"/>
|
||||
<button class="" onclick="LoadDataOld()" style="width:49%">Load Object URL</button>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<button onclick="LoadDataOld('cd2d10cafb01a3e76954ae5906d00dfc')">Load Boat</button>
|
||||
<button onclick="LoadDataOld('9a00ad76438d06612968190546552c56')">Load Jet Fuselage</button>
|
||||
<button onclick="LoadDataOld('3242eb9db2199b83b7045507917689ae')">Load Strange Thing</button>
|
||||
<button onclick="LoadDataOld('eb7e163cfe9e8afdbc74bd7451f96096')">Load Lots of Strange Things</button>
|
||||
<button onclick="LoadDataOld('145432210c277bef7e02e0040f70f283')">Load Boring Stuff</button>
|
||||
<button onclick="LoadDataOld('9bdac9ee6c95a809ea7fe7218eb87b48')">Load Revit Thing</button>
|
||||
<button onclick="v.sceneManager.removeAllObjects()">Dispose Everything</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="twelve columns">
|
||||
<div id="renderer"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="twelve columns">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+11778
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "@speckle/viewer",
|
||||
"version": "2.0.0",
|
||||
"description": "Speckle js utilities.",
|
||||
"main": "dist/Speckle.js",
|
||||
"files":[ "dist" ],
|
||||
"scripts": {
|
||||
"serve": "webpack serve --env dev --config webpack.config.example.js",
|
||||
"dev": "webpack --progress --watch --env dev",
|
||||
"build": "webpack --env dev && webpack --env build"
|
||||
},
|
||||
"author": "AEC Systems",
|
||||
"license": "SEE LICENSE NOTE IN readme.md",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.12.10",
|
||||
"@babel/core": "7.12.10",
|
||||
"@babel/eslint-parser": "^7.12.1",
|
||||
"@babel/plugin-proposal-class-properties": "7.12.1",
|
||||
"@babel/preset-env": "7.12.11",
|
||||
"@babel/preset-react": "7.12.10",
|
||||
"@babel/preset-typescript": "7.12.7",
|
||||
"babel-jest": "26.6.3",
|
||||
"babel-loader": "^8.0.0-beta.4",
|
||||
"babel-plugin-add-module-exports": "1.0.4",
|
||||
"babel-plugin-transform-class-properties": "6.24.1",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "^7.17.0",
|
||||
"html-webpack-plugin": "^5.0.0-beta.4",
|
||||
"jest": "26.6.3",
|
||||
"mocha": "^4.0.1",
|
||||
"webpack": "5.11.0",
|
||||
"webpack-cli": "^4.3.1",
|
||||
"webpack-dev-server": "^3.11.1",
|
||||
"yargs": "^10.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"three": "^0.124.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# The Speckle Viewer
|
||||
|
||||
[](https://twitter.com/SpeckleSystems) [](https://discourse.speckle.works)
|
||||
[](https://speckle-works.slack.com/join/shared_invite/enQtNjY5Mzk2NTYxNTA4LTU4MWI5ZjdhMjFmMTIxZDIzOTAzMzRmMTZhY2QxMmM1ZjVmNzJmZGMzMDVlZmJjYWQxYWU0MWJkYmY3N2JjNGI) [](https://speckle.systems)
|
||||
|
||||
## Disclaimer
|
||||
We're working to stabilize the 2.0 API, and until then there will be breaking changes.
|
||||
|
||||
## Getting started
|
||||
|
||||
Note, these are WIP instructions. For development purposes, to start a webpack live reload server run:
|
||||
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
To build the library, you should run:
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Community
|
||||
|
||||
If in trouble, the Speckle Community hangs out on [the forum](https://discourse.speckle.works). Do join and introduce yourself! We're happy to help.
|
||||
|
||||
## License
|
||||
Unless otherwise described, the code in this repository is licensed under the Apache-2.0 License. Please note that some modules, extensions or code herein might be otherwise licensed. This is indicated either in the root of the containing folder under a different license file, or in the respective file's header. If you have any questions, don't hesitate to get in touch with us via [email](mailto:hello@speckle.systems).
|
||||
@@ -0,0 +1,41 @@
|
||||
|
||||
import Viewer from './modules/Viewer'
|
||||
import ObjectLoader from './modules/ObjectLoader'
|
||||
import Converter from './modules/Converter'
|
||||
|
||||
let v = new Viewer( { container: document.getElementById( 'renderer' ) } )
|
||||
v.on( 'load-progress', args => console.log( args ) )
|
||||
|
||||
window.v = v
|
||||
|
||||
const token = 'e844747dc6f6b0b5c7d5fbd82d66de6e9529531d75'
|
||||
|
||||
window.LoadData = async function LoadData( url ) {
|
||||
url = url || document.getElementById( 'objectUrlInput' ).value
|
||||
await v.loadObject( url, token )
|
||||
}
|
||||
|
||||
window.LoadDataOld = async function LoadData( id ) {
|
||||
|
||||
// v.sceneManager.removeAllObjects()
|
||||
|
||||
id = id || document.getElementById( 'objectIdInput' ).value
|
||||
let loader = new ObjectLoader( {
|
||||
serverUrl: 'https://staging.speckle.dev',
|
||||
streamId: '5486aa9fc7',
|
||||
token,
|
||||
objectId: id
|
||||
} )
|
||||
|
||||
let converter = new Converter( loader )
|
||||
let first = true
|
||||
// Note: it's important the loop continues to load.
|
||||
for await ( let obj of loader.getObjectIterator() ) {
|
||||
if ( first ) {
|
||||
( async() => {
|
||||
await converter.traverseAndConvert( obj, ( o ) => v.sceneManager.addObject( o ) )
|
||||
} )()
|
||||
first = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Speckle Viewer</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css" integrity="sha512-EZLkOqwILORob+p0BXZc+Vm3RgJBOe1Iq/0fiI7r/wJgzOFZMlsqTa29UEl6v6U6gsV4uIpsNZoV32YZqrCRCQ==" crossorigin="anonymous" />
|
||||
<style type="text/css">
|
||||
body{
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
}
|
||||
button {
|
||||
font-family: 'Space Mono', monospace !important;
|
||||
border-color: #0A66FF;
|
||||
}
|
||||
#renderer{
|
||||
height: 700px;
|
||||
width: 100%;
|
||||
border: 5px dashed;
|
||||
border-color: rgba(100,100,100, 0.1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row" style="padding-top: 20px">
|
||||
<div class="twelve columns">
|
||||
<h3>Viewer</h3>
|
||||
<h5>Controls summary:</h5>
|
||||
<p>Click an object to select it. Double click it to focus on it. Press `esc` to clear the selection. Press `shift-s` to toggle a section plane. Press `s` while the section plane is active to toggle its control mode. Double click anywhere outside an object to zoom extents to the entire scene.</p>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<button onclick="v.sceneManager.zoomExtents()">Zoom Extents</button>
|
||||
<button onclick="v.postprocessing = !v.postprocessing">Postprocessing Toggle</button>
|
||||
<button onclick="v.sectionPlaneHelper.toggleSectionPlanes()">Show Section Plane</button>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<input id="objectUrlInput" type="text" name="objectId" placeholder="Object Url" style="width:49%" value="https://staging.speckle.dev/streams/a75ab4f10f/objects/a59617590b0bec4e9b5ee487ee75b1a7"/>
|
||||
<button class="" onclick="LoadData()" style="width:49%">Load Object URL</button>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<input id="objectIdInput" type="text" name="objectId" placeholder="Object Id" style="width:49%"/>
|
||||
<button class="" onclick="LoadDataOld()" style="width:49%">Load Object URL</button>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<button onclick="LoadDataOld('cd2d10cafb01a3e76954ae5906d00dfc')">Load Boat</button>
|
||||
<button onclick="LoadDataOld('9a00ad76438d06612968190546552c56')">Load Jet Fuselage</button>
|
||||
<button onclick="LoadDataOld('3242eb9db2199b83b7045507917689ae')">Load Strange Thing</button>
|
||||
<button onclick="LoadDataOld('eb7e163cfe9e8afdbc74bd7451f96096')">Load Lots of Strange Things</button>
|
||||
<button onclick="LoadDataOld('145432210c277bef7e02e0040f70f283')">Load Boring Stuff</button>
|
||||
<button onclick="LoadDataOld('9bdac9ee6c95a809ea7fe7218eb87b48')">Load Revit Thing</button>
|
||||
<button onclick="v.sceneManager.removeAllObjects()">Dispose Everything</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="twelve columns">
|
||||
<div id="renderer"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="twelve columns">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,5 @@
|
||||
import Viewer from './modules/Viewer'
|
||||
import ObjectLoader from './modules/ObjectLoader'
|
||||
import Converter from './modules/Converter'
|
||||
|
||||
export { Viewer, ObjectLoader, Converter }
|
||||
@@ -0,0 +1,216 @@
|
||||
import * as THREE from 'three'
|
||||
import ObjectWrapper from './ObjectWrapper'
|
||||
import { getConversionFactor } from './Units'
|
||||
|
||||
/**
|
||||
* Utility class providing some top level conversion methods.
|
||||
*/
|
||||
export default class Coverter {
|
||||
|
||||
constructor( objectLoader ) {
|
||||
if ( !objectLoader ) {
|
||||
console.warn( 'Converter initialized without a corresponding object loader. Any objects that include references will throw errors.' )
|
||||
}
|
||||
|
||||
this.objectLoader = objectLoader
|
||||
}
|
||||
|
||||
/**
|
||||
* If the object is convertable (there is a direct conversion routine), it will invoke the callback with the conversion result.
|
||||
* If the object is not convertable, it will recursively iterate through it (arrays & objects) and invoke the callback on any postive conversion result.
|
||||
* @param {[type]} obj [description]
|
||||
* @param {Function} callback [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
async traverseAndConvert( obj, callback ) {
|
||||
// Exit on primitives (string, ints, bools, bigints, etc.)
|
||||
if ( typeof obj !== 'object' ) return
|
||||
|
||||
if ( obj.referencedId ) obj = await this.resolveReference( obj )
|
||||
|
||||
// Traverse arrays, and exit early (we don't want to iterate through many numbers)
|
||||
if ( Array.isArray( obj ) ) {
|
||||
for ( let element of obj ) {
|
||||
if ( typeof element !== 'object' ) return // exit early for non-object based arrays
|
||||
( async() => await this.traverseAndConvert( element, callback ) )() //iife so we don't block
|
||||
}
|
||||
}
|
||||
|
||||
// If we can convert it, we should invoke the respective conversion routine.
|
||||
const type = this.getSpeckleType( obj )
|
||||
if ( this[`${type}ToBufferGeometry`] ) {
|
||||
try {
|
||||
callback( await this[`${type}ToBufferGeometry`]( obj.data || obj ) )
|
||||
return
|
||||
} catch ( e ) {
|
||||
console.warn( `(Traversing - direct) Failed to convert ${type} with id: ${obj.id}` )
|
||||
}
|
||||
}
|
||||
|
||||
let target = obj.data || obj
|
||||
|
||||
// Check if the object has a display value of sorts
|
||||
let displayValue = target['displayMesh'] || target['@displayMesh'] || target['displayValue']|| target['@displayValue']
|
||||
if ( displayValue ) {
|
||||
displayValue = await this.resolveReference( displayValue )
|
||||
if ( !displayValue.units ) displayValue.units = obj.units
|
||||
|
||||
try {
|
||||
let { bufferGeometry } = await this.convert( displayValue )
|
||||
callback( new ObjectWrapper( bufferGeometry, obj ) ) // use the parent's metadata!
|
||||
|
||||
// return // returning here is faster but excludes objects that have a display value and displayable children (ie, a wall with windows)
|
||||
} catch ( e ) {
|
||||
console.warn( `(Traversing) Failed to convert obj with id: ${obj.id}` )
|
||||
}
|
||||
}
|
||||
|
||||
// Last attempt: iterate through all object keys and see if we can display anything!
|
||||
// traverses the object in case there's any sub-objects we can convert.
|
||||
for ( let prop in target ) {
|
||||
if ( typeof target[prop] !== 'object' ) continue
|
||||
( async() => await this.traverseAndConvert( target[prop], callback ) )() //iife so we don't block
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Directly converts an object and invokes the callback with the the conversion result.
|
||||
* @param {[type]} obj [description]
|
||||
* @param {Function} callback [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
async convert( obj ) {
|
||||
if ( obj.referencedId ) obj = await this.resolveReference( obj )
|
||||
try {
|
||||
let type = this.getSpeckleType( obj )
|
||||
if ( this[`${type}ToBufferGeometry`] ) {
|
||||
return await this[`${type}ToBufferGeometry`]( obj.data || obj )
|
||||
}
|
||||
else return null
|
||||
} catch ( e ) {
|
||||
console.warn( `(Direct convert) Failed to convert object with id: ${obj.id}` )
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an array composed of chunked references and dechunks it.
|
||||
* @param {[type]} arr [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
async dechunk( arr ) {
|
||||
if ( !arr ) return arr
|
||||
// Handles pre-chunking objects, or arrs that have not been chunked
|
||||
if ( !arr[0].referencedId ) return arr
|
||||
|
||||
let dechunked = []
|
||||
for ( let ref of arr ) {
|
||||
let real = await this.objectLoader.getObject( ref.referencedId )
|
||||
dechunked.push( ...real.data )
|
||||
}
|
||||
return dechunked
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves an object reference by waiting for the loader to load it up.
|
||||
* @param {[type]} obj [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
async resolveReference( obj ) {
|
||||
if ( obj.referencedId )
|
||||
return await this.objectLoader.getObject( obj.referencedId )
|
||||
else return obj
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the speckle type of an object in various scenarios.
|
||||
* @param {[type]} obj [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
getSpeckleType( obj ) {
|
||||
let type = 'Base'
|
||||
if ( obj.data )
|
||||
type = obj.data.speckle_type ? obj.data.speckle_type.split( '.' ).reverse()[0] : type
|
||||
else
|
||||
type = obj.speckle_type ? obj.speckle_type.split( '.' ).reverse()[0] : type
|
||||
return type
|
||||
}
|
||||
|
||||
async BrepToBufferGeometry( obj ) {
|
||||
try {
|
||||
if ( !obj ) return
|
||||
let { bufferGeometry } = await this.MeshToBufferGeometry( await this.resolveReference( obj.displayValue || obj.displayMesh ) )
|
||||
|
||||
// deletes known uneeded fields
|
||||
delete obj.displayMesh
|
||||
delete obj.displayValue
|
||||
delete obj.Edges
|
||||
delete obj.Faces
|
||||
delete obj.Loops
|
||||
delete obj.Trims
|
||||
delete obj.Curve2D
|
||||
delete obj.Curve3D
|
||||
delete obj.Surfaces
|
||||
delete obj.Vertices
|
||||
|
||||
return new ObjectWrapper( bufferGeometry, obj )
|
||||
} catch ( e ) {
|
||||
console.warn( `Failed to convert brep id: ${obj.id}` )
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async MeshToBufferGeometry( obj ) {
|
||||
try {
|
||||
if ( !obj ) return
|
||||
|
||||
let conversionFactor = getConversionFactor( obj.units )
|
||||
// console.log( conversionFactor )
|
||||
let buffer = new THREE.BufferGeometry( )
|
||||
let indices = [ ]
|
||||
|
||||
let vertices = await this.dechunk( obj.vertices )
|
||||
let faces = await this.dechunk( obj.faces )
|
||||
|
||||
let k = 0
|
||||
while ( k < faces.length ) {
|
||||
if ( faces[ k ] === 1 ) { // QUAD FACE
|
||||
indices.push( faces[ k + 1 ], faces[ k + 2 ], faces[ k + 3 ] )
|
||||
indices.push( faces[ k + 1 ], faces[ k + 3 ], faces[ k + 4 ] )
|
||||
k += 5
|
||||
} else if ( faces[ k ] === 0 ) { // TRIANGLE FACE
|
||||
indices.push( faces[ k + 1 ], faces[ k + 2 ], faces[ k + 3 ] )
|
||||
k += 4
|
||||
} else throw new Error( `Mesh type not supported. Face topology indicator: ${faces[k]}` )
|
||||
}
|
||||
buffer.setIndex( indices )
|
||||
|
||||
buffer.setAttribute(
|
||||
'position',
|
||||
new THREE.Float32BufferAttribute( conversionFactor === 1 ? vertices : vertices.map( v => v * conversionFactor ), 3 ) )
|
||||
|
||||
buffer.computeVertexNormals( )
|
||||
buffer.computeFaceNormals( )
|
||||
buffer.computeBoundingSphere( )
|
||||
|
||||
delete obj.vertices
|
||||
delete obj.faces
|
||||
|
||||
return new ObjectWrapper( buffer, obj )
|
||||
} catch ( e ) {
|
||||
console.warn( `Failed to convert mesh with id: ${obj.id}` )
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// TODOs:
|
||||
// async PointToBufferGeometry( obj ) {}
|
||||
// async LineToBufferGeometry( obj ) {}
|
||||
// async PolylineToBufferGeometry( obj ) {}
|
||||
// async PolycurveToBufferGeometry( obj ) {}
|
||||
// async CurveToBufferGeometry( obj ) {}
|
||||
// async CircleToBufferGeometry( obj ) {}
|
||||
// async ArcToBufferGeometry( obj ) {}
|
||||
// async EllipseToBufferGeometry( obj ) {}
|
||||
// async SurfaceToBufferGeometry( obj ) {}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
|
||||
export default class EventEmitter {
|
||||
constructor() {
|
||||
this._events = {}
|
||||
}
|
||||
|
||||
on( name, listener ) {
|
||||
if ( !this._events[name] ) {
|
||||
this._events[name] = []
|
||||
}
|
||||
|
||||
this._events[name].push( listener )
|
||||
}
|
||||
|
||||
removeListener( name, listenerToRemove ) {
|
||||
if ( !this._events[name] ) return
|
||||
|
||||
const filterListeners = ( listener ) => listener !== listenerToRemove
|
||||
|
||||
this._events[name] = this._events[name].filter( filterListeners )
|
||||
}
|
||||
|
||||
emit( name, data ) {
|
||||
if ( !this._events[name] ) return
|
||||
|
||||
const fireCallbacks = ( callback ) => {
|
||||
callback( data )
|
||||
}
|
||||
|
||||
this._events[name].forEach( fireCallbacks )
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Simple client that streams object info from a Speckle Server.
|
||||
* TODO: This should be split from the viewer into its own package.
|
||||
*/
|
||||
export default class ObjectLoader {
|
||||
|
||||
constructor( { serverUrl, streamId, token, objectId } ) {
|
||||
this.INTERVAL_MS = 20
|
||||
this.TIMEOUT_MS = 180000 // three mins
|
||||
|
||||
this.serverUrl = serverUrl || window.location.origin
|
||||
this.streamId = streamId
|
||||
this.objectId = objectId
|
||||
this.token = token || localStorage.getItem( 'AuthToken' )
|
||||
this.headers = {
|
||||
'Authorization': `Bearer ${this.token}`,
|
||||
'Accept': 'text/plain'
|
||||
}
|
||||
this.requestUrl = `${this.serverUrl}/objects/${this.streamId}/${this.objectId}`
|
||||
this.promises = []
|
||||
this.intervals = {}
|
||||
this.buffer = []
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.buffer = []
|
||||
this.intervals.forEach( i => clearInterval( i.interval ) )
|
||||
}
|
||||
|
||||
async getObject( id ){
|
||||
if ( this.buffer[id] ) return this.buffer[id]
|
||||
|
||||
let promise = new Promise( ( resolve, reject ) => {
|
||||
this.promises.push( { id, resolve, reject } )
|
||||
// Only create a new interval checker if none is already present!
|
||||
if ( this.intervals[id] ) {
|
||||
this.intervals[id].elapsed = 0 // reset elapsed
|
||||
} else {
|
||||
let intervalId = setInterval( this.tryResolvePromise.bind( this ), this.INTERVAL_MS, id )
|
||||
this.intervals[id] = { interval: intervalId, elapsed: 0 }
|
||||
}
|
||||
} )
|
||||
return promise
|
||||
}
|
||||
|
||||
tryResolvePromise( id ) {
|
||||
this.intervals[id].elapsed += this.INTERVAL_MS
|
||||
if ( this.buffer[id] ) {
|
||||
for ( let p of this.promises.filter( p => p.id === id ) ) {
|
||||
p.resolve( this.buffer[id] )
|
||||
}
|
||||
|
||||
clearInterval( this.intervals[id].interval )
|
||||
delete this.intervals[id]
|
||||
// this.promises = this.promises.filter( p => p.id !== p.id ) // clearing out promises too early seems to nuke loading
|
||||
return
|
||||
}
|
||||
|
||||
if ( this.intervals[id].elapsed > this.TIMEOUT_MS ) {
|
||||
console.warn( `Timeout resolving ${id}. HIC SVNT DRACONES.` )
|
||||
clearInterval( this.intervals[id].interval )
|
||||
this.promises.filter( p => p.id === id ).forEach( p => p.reject() )
|
||||
this.promises = this.promises.filter( p => p.id !== p.id ) // clear out
|
||||
}
|
||||
}
|
||||
|
||||
async * getObjectIterator( ) {
|
||||
for await ( let line of this.getRawObjectIterator() ) {
|
||||
let { id, obj } = this.processLine( line )
|
||||
this.buffer[ id ] = obj
|
||||
yield obj
|
||||
}
|
||||
}
|
||||
|
||||
processLine( chunk ) {
|
||||
var pieces = chunk.split( '\t' )
|
||||
return { id: pieces[0], obj: JSON.parse( pieces[1] ) }
|
||||
}
|
||||
|
||||
async * getRawObjectIterator() {
|
||||
const decoder = new TextDecoder()
|
||||
const response = await fetch( this.requestUrl, { headers: this.headers } )
|
||||
const reader = response.body.getReader()
|
||||
let { value: chunk, done: readerDone } = await reader.read()
|
||||
chunk = chunk ? decoder.decode( chunk ) : ''
|
||||
|
||||
let re = /\r\n|\n|\r/gm
|
||||
let startIndex = 0
|
||||
|
||||
while ( true ) {
|
||||
let result = re.exec( chunk )
|
||||
if ( !result ) {
|
||||
if ( readerDone ) break
|
||||
let remainder = chunk.substr( startIndex );
|
||||
( { value: chunk, done: readerDone } = await reader.read() )
|
||||
chunk = remainder + ( chunk ? decoder.decode( chunk ) : '' )
|
||||
startIndex = re.lastIndex = 0
|
||||
continue
|
||||
}
|
||||
yield chunk.substring( startIndex, result.index )
|
||||
startIndex = re.lastIndex
|
||||
}
|
||||
|
||||
if ( startIndex < chunk.length ) {
|
||||
yield chunk.substr( startIndex )
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Class that wraps around a buffer geometry and any remaining speckle object
|
||||
* metadata. Used to match the two in the renderer.
|
||||
*/
|
||||
export default class ObjectWrapper {
|
||||
constructor( bufferGeometry, meta, geometryType ) {
|
||||
this.bufferGeometry = bufferGeometry
|
||||
this.meta = meta
|
||||
this.geometryType = geometryType || 'solid'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
import * as THREE from 'three'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
|
||||
/**
|
||||
* Manages objects and provides some convenience methods to focus on the entire scene, or one specific object.
|
||||
*/
|
||||
export default class SceneObjectManager {
|
||||
|
||||
constructor( viewer ) {
|
||||
this.viewer = viewer
|
||||
this.scene = viewer.scene
|
||||
this.userObjects = new THREE.Group()
|
||||
this.solidObjects = new THREE.Group()
|
||||
this.lineObjects = new THREE.Group()
|
||||
this.pointObjects = new THREE.Group()
|
||||
this.transparentObjects = new THREE.Group()
|
||||
|
||||
this.userObjects.add( this.solidObjects )
|
||||
this.userObjects.add( this.transparentObjects )
|
||||
this.userObjects.add( this.lineObjects )
|
||||
this.userObjects.add( this.pointObjects )
|
||||
this.scene.add( this.userObjects )
|
||||
|
||||
this.solidMaterial = new THREE.MeshStandardMaterial( {
|
||||
color: 0x8D9194,
|
||||
emissive: 0x0,
|
||||
roughness: 1,
|
||||
metalness: 0,
|
||||
side: THREE.DoubleSide,
|
||||
envMap: this.viewer.cubeCamera.renderTarget.texture
|
||||
} )
|
||||
|
||||
this.transparentMaterial = new THREE.MeshStandardMaterial( {
|
||||
color: 0xA0A4A8,
|
||||
emissive: 0x0,
|
||||
roughness: 0,
|
||||
metalness: 0.5,
|
||||
side: THREE.DoubleSide,
|
||||
transparent: true,
|
||||
opacity: 0.4,
|
||||
envMap: this.viewer.cubeCamera.renderTarget.texture
|
||||
} )
|
||||
|
||||
// this.lineMaterial = new
|
||||
|
||||
|
||||
this.objectIds = []
|
||||
this.postLoad = debounce( () => { this._postLoadFunction() }, 200 )
|
||||
|
||||
this.loaders = []
|
||||
}
|
||||
|
||||
get objects() {
|
||||
return [ ...this.solidObjects.children, ...this.transparentObjects.children ]
|
||||
}
|
||||
|
||||
// Note: we might switch later down the line from cloning materials to solely
|
||||
// using a few "default" ones and controlling color through vertex colors.
|
||||
// For now a small compromise to speed up dev; it is not the most memory
|
||||
// efficient approach.
|
||||
// To support big models we might need to merge everything in buffer geometries,
|
||||
// and control things separately to squeeze those sweet FPS (esp mobile); but
|
||||
// this conflicts a bit with the interactivity requirements of the viewer, esp.
|
||||
// the TODO ones (colour by property).
|
||||
addObject( wrapper ) {
|
||||
if ( !wrapper || !wrapper.bufferGeometry ) return
|
||||
|
||||
|
||||
switch ( wrapper.geometryType ) {
|
||||
case 'solid':
|
||||
// Do we have a defined material?
|
||||
if ( wrapper.meta.renderMaterial ) {
|
||||
|
||||
let renderMat = wrapper.meta.renderMaterial
|
||||
let color = new THREE.Color( this._argbToRGB( renderMat.diffuse ) )
|
||||
this._normaliseColor( color )
|
||||
// Is it a transparent material?
|
||||
if ( renderMat.opacity !== 1 ) {
|
||||
let material = this.transparentMaterial.clone()
|
||||
material.clippingPlanes = this.viewer.sectionPlaneHelper.planes
|
||||
material.color = color
|
||||
material.opacity = renderMat.opacity !== 0 ? renderMat.opacity : 0.2
|
||||
this.addTransparentSolid( wrapper, material )
|
||||
|
||||
// It's not a transparent material!
|
||||
} else {
|
||||
let material = this.solidMaterial.clone()
|
||||
material.clippingPlanes = this.viewer.sectionPlaneHelper.planes
|
||||
material.color = color
|
||||
material.metalness = renderMat.metalness
|
||||
if ( material.metalness !== 0 ) material.roughness = 0.1
|
||||
if ( material.metalness > 0.8 ) material.color = new THREE.Color( '#CDCDCD' ) // hack for rhino metal materials being black FFS
|
||||
this.addSolid( wrapper, material )
|
||||
}
|
||||
} else {
|
||||
// If we don't have defined material, just use the default
|
||||
let material = this.solidMaterial.clone()
|
||||
material.clippingPlanes = this.viewer.sectionPlaneHelper.planes
|
||||
this.addSolid( wrapper, material )
|
||||
}
|
||||
break
|
||||
case 'line':
|
||||
this.addLine( wrapper )
|
||||
break
|
||||
case 'point':
|
||||
this.addPoint( wrapper )
|
||||
break
|
||||
}
|
||||
|
||||
this.postLoad()
|
||||
}
|
||||
|
||||
addSolid( wrapper, material ) {
|
||||
const mesh = new THREE.Mesh( wrapper.bufferGeometry, material ? material : this.solidMaterial )
|
||||
mesh.userData = wrapper.meta
|
||||
mesh.uuid = wrapper.meta.id
|
||||
this.objectIds.push( mesh.uuid )
|
||||
this.solidObjects.add( mesh )
|
||||
}
|
||||
|
||||
addTransparentSolid( wrapper, material ) {
|
||||
const mesh = new THREE.Mesh( wrapper.bufferGeometry, material ? material : this.transparentMaterial )
|
||||
mesh.userData = wrapper.meta
|
||||
mesh.uuid = wrapper.meta.id
|
||||
this.objectIds.push( mesh.uuid )
|
||||
this.transparentObjects.add( mesh )
|
||||
}
|
||||
|
||||
addLine( wrapper ) {
|
||||
const line = new THREE.Line( wrapper.bufferGeometry, this.lineMaterial )
|
||||
}
|
||||
|
||||
addPoint( wrapper ){
|
||||
// TODO
|
||||
}
|
||||
|
||||
removeObject( id ) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
removeAllObjects() {
|
||||
for ( let obj of this.objects ) {
|
||||
if ( obj.geometry ){
|
||||
obj.geometry.dispose()
|
||||
}
|
||||
}
|
||||
this.solidObjects.clear()
|
||||
this.transparentObjects.clear()
|
||||
this.viewer.selectionHelper.unselect()
|
||||
this.objectIds = []
|
||||
|
||||
this._postLoadFunction()
|
||||
}
|
||||
|
||||
_postLoadFunction() {
|
||||
this.zoomExtents()
|
||||
this.viewer.reflectionsNeedUpdate = true
|
||||
this.viewer.sectionPlaneHelper._matchSceneSize()
|
||||
}
|
||||
|
||||
zoomToObject( target ) {
|
||||
const box = new THREE.Box3().setFromObject( target )
|
||||
this.zoomToBox( box )
|
||||
}
|
||||
|
||||
zoomExtents() {
|
||||
let bboxTarget = this.userObjects
|
||||
if ( this.objects.length === 0 ) {
|
||||
let box = new THREE.Box3( new THREE.Vector3( -1,-1,-1 ), new THREE.Vector3( 1,1,1 ) )
|
||||
this.zoomToBox( box )
|
||||
return
|
||||
}
|
||||
let box = new THREE.Box3().setFromObject( bboxTarget )
|
||||
this.zoomToBox( box )
|
||||
}
|
||||
|
||||
// see this discussion: https://github.com/mrdoob/three.js/pull/14526#issuecomment-497254491
|
||||
// Notes: seems that zooming in to a box 'rescales' the SSAO pass somehow and makes it
|
||||
// look better. Could we do the same thing somehow when controls stop moving?
|
||||
zoomToBox( box ) {
|
||||
const fitOffset = 1.2
|
||||
|
||||
const size = box.getSize( new THREE.Vector3() )
|
||||
const center = box.getCenter( new THREE.Vector3() )
|
||||
|
||||
const maxSize = Math.max( size.x, size.y, size.z )
|
||||
const fitHeightDistance = maxSize / ( 2 * Math.atan( Math.PI * this.viewer.camera.fov / 360 ) )
|
||||
const fitWidthDistance = fitHeightDistance / this.viewer.camera.aspect
|
||||
const distance = fitOffset * Math.max( fitHeightDistance, fitWidthDistance )
|
||||
|
||||
const direction = this.viewer.controls.target.clone()
|
||||
.sub( this.viewer.camera.position )
|
||||
.normalize()
|
||||
.multiplyScalar( distance )
|
||||
|
||||
// this.viewer.controls.maxDistance = distance * 20
|
||||
this.viewer.controls.target.copy( center )
|
||||
|
||||
this.viewer.camera.near = distance / 100
|
||||
this.viewer.camera.far = distance * 100
|
||||
this.viewer.camera.updateProjectionMatrix()
|
||||
|
||||
this.viewer.camera.position.copy( this.viewer.controls.target ).sub( direction )
|
||||
|
||||
this.viewer.controls.update()
|
||||
}
|
||||
|
||||
_argbToRGB( argb ) {
|
||||
return '#'+ ( '000000' + ( argb & 0xFFFFFF ).toString( 16 ) ).slice( -6 )
|
||||
}
|
||||
|
||||
_normaliseColor( color ) {
|
||||
// Note: full of **magic numbers** that will need changing once global scene
|
||||
// is properly set up; also to test with materials coming from other software too...
|
||||
let hsl = {}
|
||||
color.getHSL( hsl )
|
||||
|
||||
if ( hsl.s + hsl.l > 1 ) {
|
||||
while ( hsl.s + hsl.l > 1 ){
|
||||
hsl.s -= 0.05
|
||||
hsl.l -= 0.05
|
||||
}
|
||||
}
|
||||
|
||||
if ( hsl.l > 0.6 ) {
|
||||
hsl.l = 0.6
|
||||
}
|
||||
|
||||
if ( hsl.l < 0.3 ) {
|
||||
hsl.l = 0.3
|
||||
}
|
||||
|
||||
color.setHSL( hsl.h, hsl.s, hsl.l )
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import * as THREE from 'three'
|
||||
import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js'
|
||||
|
||||
/**
|
||||
* WIP: A utility class for adding section planes to the scene.
|
||||
* - 'S' shows/hides section planes
|
||||
* - 's' toggles controls from translate to rotate
|
||||
*/
|
||||
export default class SectionPlaneHelper {
|
||||
|
||||
constructor( parent ) {
|
||||
this.viewer = parent
|
||||
this.cutters = []
|
||||
this.visible = false
|
||||
|
||||
window.addEventListener( 'keydown', ( event ) => {
|
||||
if ( event.key === 's' ) {
|
||||
this.toggleTransformControls()
|
||||
}
|
||||
if ( event.key === 'S' ) {
|
||||
this.toggleSectionPlanes()
|
||||
}
|
||||
}, false )
|
||||
}
|
||||
|
||||
get planes() {
|
||||
return this.cutters.map( cutter => cutter.plane )
|
||||
}
|
||||
|
||||
get activePlanes() {
|
||||
return this.cutters.filter( cutter => cutter.visible ).map( cutter => cutter.plane )
|
||||
}
|
||||
|
||||
toggleTransformControls() {
|
||||
this.cutters.forEach( cutter => {
|
||||
if ( cutter.control.mode === 'rotate' ) {
|
||||
cutter.control.setMode( 'translate' )
|
||||
cutter.control.showX = false
|
||||
cutter.control.showY = false
|
||||
cutter.control.showZ = true
|
||||
return
|
||||
}
|
||||
cutter.control.setMode( 'rotate' )
|
||||
cutter.control.showX = true
|
||||
cutter.control.showY = true
|
||||
cutter.control.showZ = false
|
||||
} )
|
||||
}
|
||||
|
||||
createSectionPlane() {
|
||||
let cutter = { }
|
||||
|
||||
cutter.id = this.cutters.length
|
||||
cutter.visible = false
|
||||
cutter.plane = new THREE.Plane( new THREE.Vector3( 0, 0, -1 ), 1 )
|
||||
|
||||
cutter.helper = new THREE.Mesh( new THREE.PlaneGeometry( 1, 1, 1 ), new THREE.MeshBasicMaterial( { color: 0xAFAFAF, transparent: true, opacity: 0.1, side: THREE.DoubleSide } ) )
|
||||
cutter.helper.visible = false
|
||||
this.viewer.scene.add( cutter.helper )
|
||||
|
||||
cutter.control = new TransformControls( this.viewer.camera, this.viewer.renderer.domElement )
|
||||
cutter.control.setSize( 0.5 )
|
||||
cutter.control.space = 'local'
|
||||
cutter.control.showX = false
|
||||
cutter.control.showY = false
|
||||
cutter.control.setRotationSnap( THREE.MathUtils.degToRad( 15 ) )
|
||||
|
||||
cutter.control.addEventListener( 'change', () => this.viewer.render )
|
||||
cutter.control.addEventListener( 'dragging-changed', ( event ) => {
|
||||
if ( !cutter.visible ) return
|
||||
this.viewer.controls.enabled = !event.value
|
||||
|
||||
// Reference: https://stackoverflow.com/a/52124409
|
||||
let normal = new THREE.Vector3()
|
||||
let point = new THREE.Vector3()
|
||||
normal.set( 0, 0, -1 ).applyQuaternion( cutter.helper.quaternion )
|
||||
point.copy( cutter.helper.position )
|
||||
cutter.plane.setFromNormalAndCoplanarPoint( normal, point )
|
||||
} )
|
||||
|
||||
cutter.control.attach( cutter.helper )
|
||||
cutter.control.visible = false
|
||||
this.viewer.scene.add( cutter.control )
|
||||
|
||||
this.cutters.push( cutter )
|
||||
|
||||
// adds local clipping planes to all materials
|
||||
let objs = this.viewer.sceneManager.objects
|
||||
objs.forEach( obj => {
|
||||
obj.material.clippingPlanes = this.cutters.map( c => c.plane )
|
||||
} )
|
||||
}
|
||||
|
||||
toggleSectionPlanes() {
|
||||
if ( this.visible ) this.hideSectionPlanes()
|
||||
else this.showSectionPlanes()
|
||||
|
||||
this.visible = !this.visible
|
||||
}
|
||||
|
||||
showSectionPlanes() {
|
||||
this._matchSceneSize()
|
||||
|
||||
this.cutters.forEach( cutter => {
|
||||
cutter.visible = true
|
||||
cutter.helper.visible = true
|
||||
cutter.control.visible = true
|
||||
} )
|
||||
|
||||
this.viewer.renderer.localClippingEnabled = true
|
||||
}
|
||||
|
||||
hideSectionPlanes() {
|
||||
this.cutters.forEach( cutter => {
|
||||
cutter.visible = false
|
||||
cutter.helper.visible = false
|
||||
cutter.control.visible = false
|
||||
} )
|
||||
this.viewer.renderer.localClippingEnabled = false
|
||||
}
|
||||
|
||||
_matchSceneSize() {
|
||||
// Scales and translate helper to scene bbox center and origin
|
||||
const sceneBox = new THREE.Box3().setFromObject( this.viewer.sceneManager.userObjects )
|
||||
const sceneSize = new THREE.Vector3()
|
||||
sceneBox.getSize( sceneSize )
|
||||
const sceneCenter = new THREE.Vector3()
|
||||
sceneBox.getCenter( sceneCenter )
|
||||
|
||||
this.cutters.forEach( cutter => {
|
||||
cutter.helper.scale.set( sceneSize.x > 0 ? sceneSize.x : 1, sceneSize.y > 0 ? sceneSize.y : 1, sceneSize.z >0 ? sceneSize.z : 1 )
|
||||
cutter.helper.position.set( sceneCenter.x, sceneCenter.y, sceneCenter.z )
|
||||
|
||||
let normal = new THREE.Vector3()
|
||||
let point = new THREE.Vector3()
|
||||
normal.set( 0, 0, -1 ).applyQuaternion( cutter.helper.quaternion )
|
||||
point.copy( cutter.helper.position )
|
||||
cutter.plane.setFromNormalAndCoplanarPoint( normal, point )
|
||||
} )
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import * as THREE from 'three'
|
||||
import debounce from 'lodash.debounce'
|
||||
import EventEmitter from './EventEmitter'
|
||||
|
||||
/**
|
||||
* Selects and deselects user added objects in the scene. Emits the array of all intersected objects on click.
|
||||
* Behaviours:
|
||||
* - Clicking on one object will select it.
|
||||
* - Double clicking on one object will focus on it.
|
||||
* - Double clicking anywhere else will focus the scene.
|
||||
* - Pressing escape will clear any selection present.
|
||||
* TODOs:
|
||||
* - Ensure clipped geometry is not selected.
|
||||
* - When objects are disposed, ensure selection is reset.
|
||||
*/
|
||||
export default class SelectionHelper extends EventEmitter {
|
||||
|
||||
constructor( parent ) {
|
||||
super()
|
||||
this.viewer = parent
|
||||
this.raycaster = new THREE.Raycaster()
|
||||
|
||||
// Handle clicks during camera moves
|
||||
this.orbiting = false
|
||||
this.viewer.controls.addEventListener( 'change', debounce( () => { this.orbiting = false }, 100 ) )
|
||||
this.viewer.controls.addEventListener( 'start', debounce( () => { this.orbiting = true }, 200 ) )
|
||||
this.viewer.controls.addEventListener( 'end', debounce( () => { this.orbiting = false }, 200 ) )
|
||||
|
||||
// Handle mouseclicks
|
||||
this.viewer.renderer.domElement.addEventListener( 'pointerup', ( e ) => {
|
||||
if ( this.orbiting ) return
|
||||
|
||||
let selectionObjects = this.getClickedObjects( e )
|
||||
this.handleSelection( selectionObjects )
|
||||
} )
|
||||
|
||||
// Doubleclicks on touch devices
|
||||
// http://jsfiddle.net/brettwp/J4djY/
|
||||
this.tapTimeout
|
||||
this.lastTap = 0
|
||||
this.touchLocation
|
||||
this.viewer.renderer.domElement.addEventListener( 'touchstart', ( e ) => { this.touchLocation = e.targetTouches[0] } )
|
||||
this.viewer.renderer.domElement.addEventListener( 'touchend', ( event ) => {
|
||||
var currentTime = new Date().getTime()
|
||||
var tapLength = currentTime - this.lastTap
|
||||
clearTimeout( this.tapTimeout )
|
||||
if ( tapLength < 500 && tapLength > 0 ) {
|
||||
let selectionObjects = this.getClickedObjects( this.touchLocation )
|
||||
this.emit( 'object-doubleclicked', selectionObjects )
|
||||
if ( !this.orbiting )
|
||||
this.handleDoubleClick( selectionObjects )
|
||||
event.preventDefault()
|
||||
} else {
|
||||
this.tapTimeout = setTimeout( function() {
|
||||
clearTimeout( this.tapTimeout )
|
||||
}, 500 )
|
||||
}
|
||||
this.lastTap = currentTime
|
||||
} )
|
||||
|
||||
this.viewer.renderer.domElement.addEventListener( 'dblclick', ( e ) => {
|
||||
// if ( this.orbiting ) return // not needed for zoom to thing?
|
||||
|
||||
let selectionObjects = this.getClickedObjects( e )
|
||||
|
||||
this.emit( 'object-doubleclicked', selectionObjects )
|
||||
this.handleDoubleClick( selectionObjects )
|
||||
} )
|
||||
|
||||
// Handle multiple object selection
|
||||
this.multiSelect = false
|
||||
document.addEventListener( 'keydown', ( e ) => {
|
||||
if ( e.isComposing || e.keyCode === 229 ) return
|
||||
if ( e.key === 'Shift' ) this.multiSelect = true
|
||||
if ( e.key === 'Escape' ) this.unselect( )
|
||||
} )
|
||||
document.addEventListener( 'keyup', ( e ) => {
|
||||
if ( e.isComposing || e.keyCode === 229 ) return
|
||||
if ( e.key === 'Shift' ) this.multiSelect = false
|
||||
} )
|
||||
|
||||
this.selectionMaterial = new THREE.MeshLambertMaterial( { color: 0x0B55D2, emissive: 0x0B55D2, side: THREE.DoubleSide } )
|
||||
this.selectedObjects = new THREE.Group()
|
||||
this.selectedObjects.renderOrder = 1000
|
||||
this.viewer.scene.add( this.selectedObjects )
|
||||
|
||||
this.originalSelectionObjects = []
|
||||
}
|
||||
|
||||
handleSelection( objects ) {
|
||||
this.select( objects[0] )
|
||||
}
|
||||
|
||||
handleDoubleClick( objects ) {
|
||||
if ( !objects || objects.length === 0 ) this.viewer.sceneManager.zoomExtents()
|
||||
else this.viewer.sceneManager.zoomToObject( objects[0].object )
|
||||
}
|
||||
|
||||
select( obj ) {
|
||||
if ( !this.multiSelect ) this.unselect()
|
||||
if ( !obj ) {
|
||||
this.emit( 'object-clicked', this.originalSelectionObjects )
|
||||
return
|
||||
}
|
||||
|
||||
let mesh = new THREE.Mesh( obj.object.geometry, this.selectionMaterial )
|
||||
this.selectedObjects.add( mesh )
|
||||
this.originalSelectionObjects.push( obj )
|
||||
this.emit( 'object-clicked', this.originalSelectionObjects )
|
||||
}
|
||||
|
||||
unselect() {
|
||||
this.selectedObjects.clear()
|
||||
this.originalSelectionObjects = []
|
||||
}
|
||||
|
||||
getClickedObjects( e ) {
|
||||
const normalizedPosition = this._getNormalisedClickPosition( e )
|
||||
this.raycaster.setFromCamera( normalizedPosition, this.viewer.camera )
|
||||
|
||||
let intersectedObjects = this.raycaster.intersectObjects( this.viewer.sceneManager.objects )
|
||||
intersectedObjects = intersectedObjects.filter( obj => this.viewer.sectionPlaneHelper.activePlanes.every( pl => pl.distanceToPoint( obj.point ) > 0 ) )
|
||||
|
||||
return intersectedObjects
|
||||
}
|
||||
|
||||
_getNormalisedClickPosition( e ) {
|
||||
// Reference: https://threejsfundamentals.org/threejs/lessons/threejs-picking.html
|
||||
const canvas = this.viewer.renderer.domElement
|
||||
const rect = this.viewer.renderer.domElement.getBoundingClientRect()
|
||||
|
||||
const pos = {
|
||||
x: ( e.clientX - rect.left ) * canvas.width / rect.width,
|
||||
y: ( e.clientY - rect.top ) * canvas.height / rect.height
|
||||
}
|
||||
return {
|
||||
x: ( pos.x / canvas.width ) * 2 - 1,
|
||||
y: ( pos.y / canvas.height ) * -2 + 1
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.viewer.scene.remove( this.selectedObjects )
|
||||
this.unselect()
|
||||
this.originalSelectionObjects = null
|
||||
this.selectionMaterial = null
|
||||
this.selectedObjects = null
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
export const Units = {
|
||||
Millimeters: 'mm',
|
||||
Centimeters: 'cm',
|
||||
Meters: 'm',
|
||||
Kilometers: 'km',
|
||||
Inches: 'in',
|
||||
Feet: 'ft',
|
||||
Yards: 'yd',
|
||||
Miles: 'mi'
|
||||
}
|
||||
|
||||
export function getConversionFactor( from, to = Units.Meters ){
|
||||
from = normaliseName( from )
|
||||
to = normaliseName( to )
|
||||
switch ( from )
|
||||
{
|
||||
// METRIC
|
||||
case Units.Millimeters:
|
||||
switch ( to )
|
||||
{
|
||||
case Units.Centimeters: return 0.1
|
||||
case Units.Meters: return 0.001
|
||||
case Units.Kilometers: return 1e-6
|
||||
case Units.Inches: return 0.0393701
|
||||
case Units.Feet: return 0.00328084
|
||||
case Units.Yards: return 0.00109361
|
||||
case Units.Miles: return 6.21371e-7
|
||||
}
|
||||
break
|
||||
case Units.Centimeters:
|
||||
switch ( to )
|
||||
{
|
||||
case Units.Millimeters: return 10
|
||||
case Units.Meters: return 0.01
|
||||
case Units.Kilometers: return 1e-5
|
||||
case Units.Inches: return 0.393701
|
||||
case Units.Feet: return 0.0328084
|
||||
case Units.Yards: return 0.0109361
|
||||
case Units.Miles: return 6.21371e-6
|
||||
}
|
||||
break
|
||||
case Units.Meters:
|
||||
switch ( to )
|
||||
{
|
||||
case Units.Millimeters: return 1000
|
||||
case Units.Centimeters: return 100
|
||||
case Units.Kilometers: return 1000
|
||||
case Units.Inches: return 39.3701
|
||||
case Units.Feet: return 3.28084
|
||||
case Units.Yards: return 1.09361
|
||||
case Units.Miles: return 0.000621371
|
||||
}
|
||||
break
|
||||
case Units.Kilometers:
|
||||
switch ( to )
|
||||
{
|
||||
case Units.Millimeters: return 1000000
|
||||
case Units.Centimeters: return 100000
|
||||
case Units.Meters: return 1000
|
||||
case Units.Inches: return 39370.1
|
||||
case Units.Feet: return 3280.84
|
||||
case Units.Yards: return 1093.61
|
||||
case Units.Miles: return 0.621371
|
||||
}
|
||||
break
|
||||
|
||||
// IMPERIAL
|
||||
case Units.Inches:
|
||||
switch ( to )
|
||||
{
|
||||
case Units.Millimeters: return 25.4
|
||||
case Units.Centimeters: return 2.54
|
||||
case Units.Meters: return 0.0254
|
||||
case Units.Kilometers: return 2.54e-5
|
||||
case Units.Feet: return 0.0833333
|
||||
case Units.Yards: return 0.027777694
|
||||
case Units.Miles: return 1.57828e-5
|
||||
}
|
||||
break
|
||||
case Units.Feet:
|
||||
switch ( to )
|
||||
{
|
||||
case Units.Millimeters: return 304.8
|
||||
case Units.Centimeters: return 30.48
|
||||
case Units.Meters: return 0.3048
|
||||
case Units.Kilometers: return 0.0003048
|
||||
case Units.Inches: return 12
|
||||
case Units.Yards: return 0.333332328
|
||||
case Units.Miles: return 0.000189394
|
||||
}
|
||||
break
|
||||
case Units.Miles:
|
||||
switch ( to )
|
||||
{
|
||||
case Units.Millimeters: return 1.609e+6
|
||||
case Units.Centimeters: return 160934
|
||||
case Units.Meters: return 1609.34
|
||||
case Units.Kilometers: return 1.60934
|
||||
case Units.Inches: return 63360
|
||||
case Units.Feet: return 5280
|
||||
case Units.Yards: return 1759.99469184
|
||||
}
|
||||
break
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
export function normaliseName( unit ) {
|
||||
if ( !unit ) return Units.Meters
|
||||
switch ( unit.toLowerCase() )
|
||||
{
|
||||
case 'mm':
|
||||
case 'mil':
|
||||
case 'millimeters':
|
||||
case 'millimetres':
|
||||
return Units.Millimeters
|
||||
case 'cm':
|
||||
case 'centimetre':
|
||||
case 'centimeter':
|
||||
case 'centimetres':
|
||||
case 'centimeters':
|
||||
return Units.Centimeters
|
||||
case 'm':
|
||||
case 'meter':
|
||||
case 'metre':
|
||||
case 'meters':
|
||||
case 'metres':
|
||||
return Units.Meters
|
||||
case 'inches':
|
||||
case 'inch':
|
||||
case 'in':
|
||||
return Units.Inches
|
||||
case 'feet':
|
||||
case 'foot':
|
||||
case 'ft':
|
||||
return Units.Feet
|
||||
case 'yard':
|
||||
case 'yards':
|
||||
case 'yd':
|
||||
return Units.Yards
|
||||
case 'miles':
|
||||
case 'mile':
|
||||
case 'mi':
|
||||
return Units.Miles
|
||||
default:
|
||||
return Units.Meters
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
||||
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
|
||||
import { SSAOPass } from 'three/examples/jsm/postprocessing/SSAOPass.js'
|
||||
import Stats from 'three/examples/jsm/libs/stats.module.js'
|
||||
|
||||
import ObjectManager from './SceneObjectManager'
|
||||
import SelectionHelper from './SelectionHelper'
|
||||
import SectionPlaneHelper from './SectionPlaneHelper'
|
||||
import ViewerObjectLoader from './ViewerObjectLoader'
|
||||
import EventEmitter from './EventEmitter'
|
||||
|
||||
export default class Viewer extends EventEmitter {
|
||||
|
||||
constructor( { container, postprocessing = true, reflections = true, showStats = false } ) {
|
||||
super()
|
||||
this.container = container || document.getElementById( 'renderer' )
|
||||
this.postprocessing = postprocessing
|
||||
this.scene = new THREE.Scene()
|
||||
|
||||
this.camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight )
|
||||
this.camera.up.set( 0, 0, 1 )
|
||||
this.camera.position.set( 1, 1, 1 )
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } )
|
||||
this.renderer.setClearColor( 0xcccccc, 0 )
|
||||
this.renderer.setPixelRatio( window.devicePixelRatio )
|
||||
this.renderer.setSize( this.container.offsetWidth, this.container.offsetHeight )
|
||||
this.container.appendChild( this.renderer.domElement )
|
||||
|
||||
// commented out because the ssao flash is annoying
|
||||
// this.renderer.gammaFactor = 2.2
|
||||
// this.renderer.outputEncoding = THREE.sRGBEncoding
|
||||
|
||||
this.reflections = reflections
|
||||
this.reflectionsNeedUpdate = true
|
||||
const cubeRenderTarget = new THREE.WebGLCubeRenderTarget( 512, { format: THREE.RGBFormat, generateMipmaps: true, minFilter: THREE.LinearMipmapLinearFilter } )
|
||||
this.cubeCamera = new THREE.CubeCamera( 0.1, 10_000, cubeRenderTarget )
|
||||
this.scene.add( this.cubeCamera )
|
||||
|
||||
this.controls = new OrbitControls( this.camera, this.renderer.domElement )
|
||||
this.controls.enableDamping = true
|
||||
this.controls.dampingFactor = 0.1
|
||||
this.controls.screenSpacePanning = true
|
||||
this.controls.maxPolarAngle = Math.PI / 2
|
||||
this.controls.panSpeed = 0.8
|
||||
this.controls.rotateSpeed = 0.8
|
||||
|
||||
this.composer = new EffectComposer( this.renderer )
|
||||
|
||||
this.ssaoPass = new SSAOPass( this.scene, this.camera, this.container.offsetWidth, this.container.offsetHeight )
|
||||
this.ssaoPass.kernelRadius = 0.03
|
||||
this.ssaoPass.kernelSize = 16
|
||||
this.ssaoPass.minDistance = 0.0002
|
||||
this.ssaoPass.maxDistance = 10
|
||||
this.ssaoPass.output = SSAOPass.OUTPUT.Default
|
||||
this.composer.addPass( this.ssaoPass )
|
||||
|
||||
this.pauseSSAO = false
|
||||
this.controls.addEventListener( 'start', () => { this.pauseSSAO = true } )
|
||||
this.controls.addEventListener( 'end', () => { this.pauseSSAO = false } )
|
||||
|
||||
if ( showStats ) {
|
||||
this.stats = new Stats()
|
||||
this.container.appendChild( this.stats.dom )
|
||||
}
|
||||
|
||||
window.addEventListener( 'resize', this.onWindowResize.bind( this ), false )
|
||||
|
||||
this.sectionPlaneHelper = new SectionPlaneHelper( this )
|
||||
this.sceneManager = new ObjectManager( this )
|
||||
this.selectionHelper = new SelectionHelper( this )
|
||||
|
||||
this.sectionPlaneHelper.createSectionPlane()
|
||||
|
||||
this.sceneLights()
|
||||
this.animate()
|
||||
|
||||
this.loaders = []
|
||||
}
|
||||
|
||||
sceneLights() {
|
||||
let ambientLight = new THREE.AmbientLight( 0xffffff )
|
||||
this.scene.add( ambientLight )
|
||||
|
||||
const lights = []
|
||||
lights[ 0 ] = new THREE.PointLight( 0xffffff, 0.21, 0 )
|
||||
lights[ 1 ] = new THREE.PointLight( 0xffffff, 0.21, 0 )
|
||||
lights[ 2 ] = new THREE.PointLight( 0xffffff, 0.21, 0 )
|
||||
lights[ 3 ] = new THREE.PointLight( 0xffffff, 0.21, 0 )
|
||||
|
||||
let factor = 1000
|
||||
lights[ 0 ].position.set( 1 * factor, 1 * factor, 1 * factor )
|
||||
lights[ 1 ].position.set( 1 * factor, -1 * factor, 1 * factor )
|
||||
lights[ 2 ].position.set( -1 * factor, -1 * factor, 1 * factor )
|
||||
lights[ 3 ].position.set( -1 * factor, 1 * factor, 1 * factor )
|
||||
|
||||
this.scene.add( lights[ 0 ] )
|
||||
this.scene.add( lights[ 1 ] )
|
||||
this.scene.add( lights[ 2 ] )
|
||||
this.scene.add( lights[ 3 ] )
|
||||
|
||||
// let sphereSize = 0.2
|
||||
// this.scene.add( new THREE.PointLightHelper( lights[ 0 ], sphereSize ) )
|
||||
// this.scene.add( new THREE.PointLightHelper( lights[ 1 ], sphereSize ) )
|
||||
// this.scene.add( new THREE.PointLightHelper( lights[ 2 ], sphereSize ) )
|
||||
// this.scene.add( new THREE.PointLightHelper( lights[ 3 ], sphereSize ) )
|
||||
|
||||
|
||||
const hemiLight = new THREE.HemisphereLight( 0xffffff, 0x0, 0.2 )
|
||||
hemiLight.color.setHSL( 1, 1, 1 )
|
||||
hemiLight.groundColor.setHSL( 0.095, 1, 0.75 )
|
||||
hemiLight.up.set( 0, 0, 1 )
|
||||
this.scene.add( hemiLight )
|
||||
|
||||
let axesHelper = new THREE.AxesHelper( 1 )
|
||||
this.scene.add( axesHelper )
|
||||
|
||||
let group = new THREE.Group()
|
||||
this.scene.add( group )
|
||||
}
|
||||
|
||||
onWindowResize() {
|
||||
this.camera.aspect = this.container.offsetWidth / this.container.offsetHeight
|
||||
this.camera.updateProjectionMatrix()
|
||||
this.renderer.setSize( this.container.offsetWidth, this.container.offsetHeight )
|
||||
this.composer.setSize( this.container.offsetWidth, this.container.offsetHeight )
|
||||
}
|
||||
|
||||
animate() {
|
||||
requestAnimationFrame( this.animate.bind( this ) )
|
||||
this.controls.update()
|
||||
if ( this.stats ) this.stats.begin()
|
||||
this.render()
|
||||
if ( this.stats ) this.stats.end()
|
||||
}
|
||||
|
||||
render() {
|
||||
if ( this.reflections && this.reflectionsNeedUpdate ) {
|
||||
// Note: scene based "dynamic" reflections need to be handled a bit more carefully, or else:
|
||||
// GL ERROR :GL_INVALID_OPERATION : glDrawElements: Source and destination textures of the draw are the same.
|
||||
// First remove the env map from all materials
|
||||
for ( let obj of this.sceneManager.objects ) {
|
||||
obj.material.envMap = null
|
||||
}
|
||||
|
||||
// Second, set a scene background color (renderer is transparent by default)
|
||||
// and then finally update the cubemap camera.
|
||||
this.scene.background = new THREE.Color( '#F0F3F8' )
|
||||
this.cubeCamera.update( this.renderer, this.scene )
|
||||
this.scene.background = null
|
||||
|
||||
// Finally, re-set the env maps of all materials
|
||||
for ( let obj of this.sceneManager.objects ) {
|
||||
obj.material.envMap = this.cubeCamera.renderTarget.texture
|
||||
}
|
||||
this.reflectionsNeedUpdate = false
|
||||
}
|
||||
|
||||
|
||||
// Render as usual
|
||||
// TODO: post processing SSAO sucks so much currently it's off by default
|
||||
if ( this.postprocessing && !this.pauseSSAO && !this.renderer.localClippingEnabled ){
|
||||
this.composer.render( this.scene, this.camera )
|
||||
}
|
||||
else {
|
||||
this.renderer.render( this.scene, this.camera )
|
||||
}
|
||||
}
|
||||
|
||||
async loadObject( url, token ) {
|
||||
let loader = new ViewerObjectLoader( this, url, token )
|
||||
this.loaders.push( loader )
|
||||
await loader.load()
|
||||
}
|
||||
|
||||
dispose() {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import ObjectLoader from './ObjectLoader'
|
||||
import Converter from './Converter'
|
||||
|
||||
/**
|
||||
* Helper wrapper around the ObjectLoader class, with some built in assumptions.
|
||||
*/
|
||||
|
||||
export default class ViewerObjectLoader {
|
||||
|
||||
|
||||
constructor( parent, objectUrl, authToken ) {
|
||||
this.viewer = parent
|
||||
this.token = authToken || localStorage.getItem( 'AuthToken' )
|
||||
|
||||
if ( !this.token ) {
|
||||
throw new Error( 'No suitable authorization token found.' )
|
||||
}
|
||||
|
||||
// example url: `https://staging.speckle.dev/streams/a75ab4f10f/objects/f33645dc9a702de8af0af16bd5f655b0`
|
||||
let url = new URL( objectUrl )
|
||||
|
||||
let segments = url.pathname.split( '/' )
|
||||
if ( segments.length < 5 || url.pathname.indexOf( 'streams' ) === -1 || url.pathname.indexOf( 'objects' ) === -1 ) {
|
||||
throw new Error( 'Unexpected object url format.' )
|
||||
}
|
||||
|
||||
this.serverUrl = url.origin
|
||||
this.streamId = segments[2]
|
||||
this.objectId = segments[4]
|
||||
|
||||
this.loader = new ObjectLoader( {
|
||||
serverUrl: this.serverUrl,
|
||||
token: this.token,
|
||||
streamId: this.streamId,
|
||||
objectId: this.objectId,
|
||||
} )
|
||||
|
||||
this.converter = new Converter( this.loader )
|
||||
}
|
||||
|
||||
async load( ) {
|
||||
let first = true
|
||||
let current = 0
|
||||
let total = 0
|
||||
for await ( let obj of this.loader.getObjectIterator() ) {
|
||||
if ( first ) {
|
||||
( async() => {
|
||||
await this.converter.traverseAndConvert( obj, ( o ) => this.viewer.sceneManager.addObject( o ) )
|
||||
} )()
|
||||
first = false
|
||||
total = obj.totalChildrenCount
|
||||
}
|
||||
current++
|
||||
this.viewer.emit( 'load-progress', { progress: current/total, id: this.objectId } )
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/* global __dirname, require, module*/
|
||||
const HtmlWebpackPlugin = require( 'html-webpack-plugin' )
|
||||
const { CleanWebpackPlugin } = require( 'clean-webpack-plugin' )
|
||||
const path = require( 'path' )
|
||||
const yargs = require( 'yargs' )
|
||||
const env = yargs.argv.env
|
||||
|
||||
let filename = 'demo'
|
||||
|
||||
let outputFile, mode
|
||||
|
||||
if ( env === 'build' ) {
|
||||
mode = 'production'
|
||||
outputFile = filename + '.min.js'
|
||||
} else {
|
||||
mode = 'development'
|
||||
outputFile = filename + '.js'
|
||||
}
|
||||
|
||||
const config = {
|
||||
mode: mode,
|
||||
entry: path.resolve( __dirname + '/src/app.js' ),
|
||||
target: 'web',
|
||||
devtool: 'source-map',
|
||||
output: {
|
||||
path: path.resolve( __dirname + '/example' ) ,
|
||||
filename: outputFile,
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /(\.jsx|\.js|\.ts|\.tsx)$/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
},
|
||||
exclude: /(node_modules|bower_components)/,
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin( { cleanStaleWebpackAssets: false } ),
|
||||
new HtmlWebpackPlugin( { title: 'Speckle Viewer Example', template: 'src/assets/example.html', filename: 'example.html' } )
|
||||
],
|
||||
resolve: {
|
||||
modules: [ path.resolve( './node_modules' ), path.resolve( './src' ) ],
|
||||
extensions: [ '.json', '.js' ],
|
||||
},
|
||||
devServer: {
|
||||
contentBase: path.join( __dirname, 'example' ),
|
||||
compress: false,
|
||||
port: 9000,
|
||||
serveIndex: true,
|
||||
writeToDisk: true
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = config
|
||||
@@ -0,0 +1,51 @@
|
||||
/* global __dirname */
|
||||
const { CleanWebpackPlugin } = require( 'clean-webpack-plugin' )
|
||||
const path = require( 'path' )
|
||||
const yargs = require( 'yargs' )
|
||||
const env = yargs.argv.env
|
||||
|
||||
let libraryName = 'Speckle'
|
||||
|
||||
let outputFile, mode
|
||||
|
||||
if ( env === 'build' ) {
|
||||
mode = 'production'
|
||||
outputFile = libraryName + '.js'
|
||||
} else {
|
||||
mode = 'development'
|
||||
outputFile = libraryName + '.js'
|
||||
}
|
||||
|
||||
const config = {
|
||||
mode: mode,
|
||||
entry: path.resolve( __dirname + '/src/index.js' ),
|
||||
target: 'web',
|
||||
devtool: 'source-map',
|
||||
output: {
|
||||
path: path.resolve( __dirname + '/dist' ) ,
|
||||
filename: outputFile,
|
||||
library: libraryName,
|
||||
libraryTarget: 'umd',
|
||||
globalObject: 'this',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /(\.jsx|\.js|\.ts|\.tsx)$/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
},
|
||||
exclude: /(node_modules|bower_components)/,
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin( { cleanStaleWebpackAssets: false } ),
|
||||
],
|
||||
resolve: {
|
||||
modules: [ path.resolve( './node_modules' ), path.resolve( './src' ) ],
|
||||
extensions: [ '.json', '.js' ],
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = config
|
||||
@@ -1,13 +1,13 @@
|
||||
# Speckle Web
|
||||
|
||||
[](https://twitter.com/SpeckleSystems) [](https://discourse.speckle.works)
|
||||
[](https://speckle-works.slack.com/join/shared_invite/enQtNjY5Mzk2NTYxNTA4LTU4MWI5ZjdhMjFmMTIxZDIzOTAzMzRmMTZhY2QxMmM1ZjVmNzJmZGMzMDVlZmJjYWQxYWU0MWJkYmY3N2JjNGI) [](https://speckle.systems)
|
||||
[](https://twitter.com/SpeckleSystems) [](https://discourse.speckle.works) [](https://speckle.systems)
|
||||
|
||||
#### Status
|
||||
|
||||
[](https://github.com/Speckle-Next/SpeckleServer/) [](https://codecov.io/gh/specklesystems/speckle-server)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
We're working to stabilize the 2.0 API, and until then there will be breaking changes.
|
||||
|
||||
## Introduction
|
||||
@@ -26,9 +26,7 @@ The frontend is a static Vue app.
|
||||
|
||||
## Developing and Debugging
|
||||
|
||||
To get started, first clone this repo & run `npm install`. Next, you'll need to run `lerna boostrap` to initialize the dependencies of all packages (server & frontend).
|
||||
|
||||
After these steps are complete, run `lerna run dev --stream`. Alternatively, you can `npm run dev` independently in each separate package (this will make for less spammy output).
|
||||
To get started, first clone this repo. Check out the detailed instructions for each module in their respective folder (see links above).
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -40,14 +38,10 @@ When pushing commits to this repo, please follow the following guidelines:
|
||||
- When ready to commit, `git cz` & follow the prompts.
|
||||
- Please use either `server` or `frontend` as the scope of your commit.
|
||||
|
||||
|
||||
## Community
|
||||
|
||||
The Speckle Community hangs out in two main places, usually:
|
||||
- on [the forum](https://discourse.speckle.works)
|
||||
- on [the chat](https://speckle-works.slack.com/join/shared_invite/enQtNjY5Mzk2NTYxNTA4LTU4MWI5ZjdhMjFmMTIxZDIzOTAzMzRmMTZhY2QxMmM1ZjVmNzJmZGMzMDVlZmJjYWQxYWU0MWJkYmY3N2JjNGI)
|
||||
|
||||
Do join and introduce yourself!
|
||||
The Speckle Community hangs out on [the forum](https://discourse.speckle.works), do join and introduce yourself & feel free to ask us questions!
|
||||
|
||||
## License
|
||||
|
||||
Unless otherwise described, the code in this repository is licensed under the Apache-2.0 License. Please note that some modules, extensions or code herein might be otherwise licensed. This is indicated either in the root of the containing folder under a different license file, or in the respective file's header. If you have any questions, don't hesitate to get in touch with us via [email](mailto:hello@speckle.systems).
|
||||
|
||||
Reference in New Issue
Block a user