alpha-1
138
README.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Studio Lite
|
||||
A web viewer for _Roblox[^1]_ place and model files, with a gorgeous interface.
|
||||
|
||||

|
||||
|
||||
## This is still in alpha
|
||||
Expect bugs and unfinished features
|
||||
|
||||
## Acknowledgements
|
||||
Studio Lite uses the [rbxBinaryParser](https://github.com/MrSprinkleToes/rbxBinaryParser) library by [MrSprinkleToes](https://github.com/MrSprinkleToes). Additionally, some code, mostly mesh parsing code, is taken from [roblox-in-webbrowser](https://github.com/MrSprinkleToes/roblox-in-webbrowser) and [roblox-web-viewer](https://github.com/MrSprinkleToes/roblox-web-viewer).
|
||||
|
||||
These other libraries are also used:
|
||||
* [THREE.js](https://threejs.org/) for rendering
|
||||
* [7.css](https://khang-nd.github.io/7.css/) for that gorgeous user interface
|
||||
* [jsTree](https://www.jstree.com/) for displaying interactive trees
|
||||
|
||||
## Included assets
|
||||
For demo purposes, Studio Lite includes some downloaded assets in the `asset/` directory and some place files in `examples/`. Please note that these files are copyright of their respective owners.
|
||||
|
||||
The _Roblox[^1]_ Studio icons, used in the title bar and Explorer view, and stored under the `content/icons/` directory, are copyright of Roblox Corporation.
|
||||
|
||||
Other assets within the `content/` directory, as well as the `favicon.ico` in the root of the source tree, are also copyright of Roblox Corporation.
|
||||
|
||||
Studio Lite is not affiliated with, or endorsed by Roblox Corporation.
|
||||
|
||||
## Features
|
||||
### Read RBXL and RBXM files
|
||||

|
||||

|
||||

|
||||
|
||||
These file types can be opened by Studio Lite. Keep in mind that there is no support for files in the XML format (though it is a high priority), so you'll encounter issues especially with older places (which often have RBXL extension but contain XML data).
|
||||
|
||||
You can use the Explorer and Properties views to browse the contents of the DataModel. You can click on scripts to view their code. Parts selected in the Explorer are highlighted in the renderer, and you can also zoom to the currently selected part.
|
||||
|
||||
### Asset support
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
Studio Lite can load decals and meshes accurately! In light of _Roblox[^1]_'s API changes, you must manually download assets using AssetDelivery and place them in the `asset/` folder. This is made easy as you get a list of all missing assets along with their type upon loading a Place.
|
||||
|
||||
### Perfect for renders
|
||||

|
||||
|
||||
You can render the current view to a PNG file at any resolution you want. Do you want [a top-down view of Crossroads in 8K resolution](images/crossroads8k.png)? No problem.
|
||||
|
||||
_Also, here's [an 8K render of the Suburban map](images/suburban8k.png), for those interested. Caution, large file, about 2 MB!_
|
||||
|
||||

|
||||

|
||||
|
||||
You can also hide the skybox, leaving a transparent background. This is useful for thumbnails. You can open a model file, hide the skybox and render a 420x420 image. This will produce a thumbnail similar to those on the _Roblox[^1]_ website. (see examples above)
|
||||
|
||||
## How to use
|
||||
Studio Lite makes many network requests, so I've refrained from hosting a live demo in fear of rate limits. It is very easy to run yourself, though, being a fully client-side application.
|
||||
|
||||
All you need is a decent computer and an HTTP server. Download these files (you can clone the repo or just download as ZIP) and run a web server in the root directory.
|
||||
|
||||
If you have Python installed, it's very easy to run a local server: `python -m http.server`. Then you can access Studio Lite on `localhost:8000`. Otherwise, use your preferred method of starting up a quick server (you can use Node.js or PHP).
|
||||
|
||||
If you have any issues or are unsatisfied with your experience, please open an issue!
|
||||
|
||||
## How to download assets
|
||||
Read the first section to learn how the download process works. Then read the second section, which shows you how to get the AssetIDs you need to download.
|
||||
|
||||
### The download process itself
|
||||
Due to _Roblox[^1]_ API changes, you must be logged in to access AssetDelivery. Therefore, you must be signed into your _Roblox[^1]_ account on the browser.
|
||||
|
||||
While my experimentation has shown that you can download _Roblox[^1]_-owned assets anonymously, authentication is required otherwise.
|
||||
|
||||
To download an asset, copy the Asset ID (the long number) and navigate to the URL below, first replacing `[assetid]` with the Asset ID you've copied.
|
||||
|
||||
AssetDelivery URL: `https://assetdelivery.roblox.com/v1/asset?id=[assetid]`
|
||||
|
||||
This will download a file with a random name. Rename it like you're told in the second section.
|
||||
|
||||
### Downloading all assets in a Place
|
||||
1. Open a Place containing un-downloaded assets.
|
||||
2. Look in the console. If it says that there are missing assets, then go to `File > Missing assets` like it tells you.
|
||||
3. Browse the list. For each line, if it is a decal, download the asset and rename it to `[assetid].png`, where `[assetid]` is the AssetID. If it is a mesh, do the same but use `.mesh` extension. Download all of the mesh's textures (if any) with the `.png` extension.
|
||||
4. If you're done, reload the page and open your Place again. You shouldn't get the console warning, and `File > Missing assets` should say all assets have loaded successfully.
|
||||
|
||||
### Possible errors while downloading
|
||||
You may encounter some errors during asset downloads, which will get you a JSON response.
|
||||
|
||||
##### Authentication required / User not authorized
|
||||
JSON response: `{"errors":[{"code":0,"message":"Authentication required to access Asset."}]}`
|
||||
|
||||
Alternatively: `{"errors":[{"code":1,"message":"User is not authorized to access Asset."}]}`
|
||||
|
||||
You need to log in to download the asset. If you're logged in on the _Roblox[^1]_ website, but still get this error, the asset could be private or something. Idk.
|
||||
|
||||
##### Requested asset is archived
|
||||
JSON response: `{"errors":[{"code":0,"message":"Requested asset is archived"}]}`
|
||||
|
||||
Exactly what it says on the tin. You're not getting the asset :)
|
||||
|
||||
You could put a placeholder image in its place, to silence the Missing assets error...
|
||||
|
||||
## Roadmap (WIP)
|
||||
- [ ] Fix bugs
|
||||
- - [ ] Issues with mesh Scale and Offset (see comments in `StudioLiteRenderer.renderPart`, around where it says `// Give meshes special treatment`)
|
||||
- - [ ] Upside down decals (see paintings in suburban house and piano in western lounge)
|
||||
- - [ ] Merry-go-round in Suburban playground is broken (base renders sideways)
|
||||
- [ ] Support XML format
|
||||
- [ ] Support all primitive shapes
|
||||
- - [x] Block
|
||||
- - [x] Ball
|
||||
- - [x] Wedge
|
||||
- - [x] Cylinder (for the most part, see bugs)
|
||||
- - [ ] Truss
|
||||
- [ ] Cool effects
|
||||
- - [ ] Show light for parts with [Light](https://create.roblox.com/docs/reference/engine/classes/Light) children
|
||||
- - [ ] ParticleEmitter support
|
||||
- - [ ] Fire and Smoke
|
||||
- - [ ] Glow for parts with Neon material
|
||||
- [ ] Completionist questline (impossible)
|
||||
- - [ ] Support part materials
|
||||
- - [ ] Basic support for rendering Smooth Terrain (really, this is a problem of reading terrain voxels, which might very well be impossible)
|
||||
- - [ ] hmm... ~~Edit Mode~~ (lol no... but maybe one day?)
|
||||
|
||||
## Licensing
|
||||
Studio Lite is **MIT licensed**. However, the following files and directories in the source tree are exempt from licensing:
|
||||
* `asset/` (non-free assets)
|
||||
* `content/` (non-free assets)
|
||||
* `examples/` (non-free assets)
|
||||
* `favicon.ico` (non-free asset)
|
||||
* `app/rendering/mesh/MeshParser.js` (contains mesh parsing code from `roblox-in-webbrowser`/`roblox-web-viewer`, which are unlicensed. Please see **Acknowledgements**)
|
||||
* `app/controls/FlyCamera.js` (contains camera code from `roblox-web-viewer`, which is unlicensed. See **Acknowledgements**)
|
||||
|
||||
## How you can help
|
||||
Contributions are welcome! You don't need to write any code, I'll be very happy if you told me what you didn't like, or what you'd like added.
|
||||
|
||||
[^1]: _Roblox_ is a registered trademark of Roblox Corporation.
|
||||
241
app/app.css
Normal file
@@ -0,0 +1,241 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
--window-background-color: #805ba5;
|
||||
}
|
||||
|
||||
dialog {
|
||||
background-color: #ffffffff;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.rightPane {
|
||||
height: calc(100vh - 29px - 29px - 27px + 4px);
|
||||
width: 400px;
|
||||
position: absolute;
|
||||
top: 56px;
|
||||
left: calc(100vw - 400px);
|
||||
background-color: white;
|
||||
overflow: hidden;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.console {
|
||||
position: absolute;
|
||||
height: calc(200px - 22px);
|
||||
width: calc(100vw - 400px);
|
||||
top: calc(100vh - 206px);
|
||||
background-color: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.title {
|
||||
height: 20px;
|
||||
padding: 5px;
|
||||
border-bottom: 5px solid black;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.title-bar-text {
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tree, .tree2, .properties {
|
||||
--height: calc(50vh - 29px - 14.5px - 17px - 13.5px - 6px + 2px);
|
||||
overflow: scroll;
|
||||
width: 385px;
|
||||
}
|
||||
|
||||
.tree, .tree2 {
|
||||
height: calc(var(--height) - 23px);
|
||||
}
|
||||
|
||||
.properties {
|
||||
height: var(--height);
|
||||
}
|
||||
|
||||
table {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
th, td {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* taken from 7.css "highlighted" rule */
|
||||
table > tbody > tr:hover {
|
||||
background: var(--item-highlighted-background);
|
||||
border: var(--listview-border) var(--item-highlighted-border);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.message, .onboarding {
|
||||
position: absolute;
|
||||
width: calc(100vw - 400px);
|
||||
height: calc(100vh - 200px - 29px - 29px);
|
||||
background-color: rgba(221, 221, 221, .5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
font-size: 22px;
|
||||
text-shadow: 2px 0 #000, -2px 0 #000, 0 2px #000, 0 -2px #000,
|
||||
1px 1px #000, -1px -1px #000, 1px -1px #000, -1px 1px #000;
|
||||
cursor: url(../content/Textures/ArrowFarCursor.png), auto;
|
||||
}
|
||||
|
||||
.onboarding {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.onboarding > h1 {
|
||||
width: fit-content;
|
||||
font-size: 44px;
|
||||
}
|
||||
|
||||
.onboarding > button {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.onboarding > :is(button, input, label),
|
||||
#stats, #stats > * {
|
||||
cursor: url(../content/Textures/ArrowCursor.png), auto !important;
|
||||
}
|
||||
|
||||
.editor {
|
||||
position: absolute;
|
||||
width: calc(100vw - 410px);
|
||||
height: calc(100vh - 215px - 29px - 29px);
|
||||
background-color: white;
|
||||
color: black;
|
||||
padding: 5px;
|
||||
font-size: 22px;
|
||||
font-family: monospace;
|
||||
overflow: scroll;
|
||||
z-index: 10001;
|
||||
}
|
||||
|
||||
.log {
|
||||
height: calc(100% - 35px);
|
||||
width: calc(100% - 12px);
|
||||
overflow: scroll;
|
||||
font-family: monospace;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
pre {
|
||||
border: none;
|
||||
}
|
||||
|
||||
body > .title-bar {
|
||||
position: absolute;
|
||||
width: calc(100vw - 14px);
|
||||
z-index: 10002;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
body > ul[role=menubar] {
|
||||
position: absolute;
|
||||
top: 29px;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
z-index: 10002;
|
||||
}
|
||||
|
||||
canvas {
|
||||
margin-top: 58px;
|
||||
cursor: url(../content/Textures/ArrowFarCursor.png), auto;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
position: absolute;
|
||||
top: calc(100vh - 27px);
|
||||
width: calc(100vw - 2px);
|
||||
margin: 0;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.flexcenter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.status-bar-field > *[role=progressbar] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.marquee.error:before {
|
||||
background: linear-gradient(to right,transparent,var(--progress-color-error),transparent 35%);
|
||||
}
|
||||
|
||||
/* yellow is not for paused, but for fetching :) */
|
||||
.marquee.paused:before {
|
||||
background: linear-gradient(to right,transparent,var(--progress-color-paused),transparent 35%);
|
||||
}
|
||||
|
||||
.title-bar-icon {
|
||||
padding-left: 20px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: left bottom;
|
||||
}
|
||||
|
||||
.icon-studio {
|
||||
background-image: url(../content/icons/StudioService.png);
|
||||
}
|
||||
|
||||
#notepad .window-body {
|
||||
width: 700px;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
#notepad textarea {
|
||||
width: 100%;
|
||||
height: calc(100% - 29px);
|
||||
resize: none;
|
||||
font-size: 16px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.title-bar, .status-bar-field {
|
||||
height: 21px;
|
||||
}
|
||||
|
||||
*[role=menubar] {
|
||||
height: 29px;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.title-bar, *[role=menubar], *[role=tablist], .tree2, .message, .onboarding, .tree, td:first-child, .status-bar {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.menu[role=tablist] {
|
||||
height: 23px;
|
||||
}
|
||||
|
||||
.github {
|
||||
color: black;
|
||||
padding-left: 5px !important;
|
||||
padding-right: 5px !important;
|
||||
text-shadow: 0 0 10px #fff,0 0 10px #fff,0 0 10px #fff,0 0 10px #fff,0 0 10px #fff,0 0 10px #fff,0 0 10px #fff,0 0 10px #fff;
|
||||
}
|
||||
|
||||
.github::before {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.title-bar.active .github, .title-bar.active .github::before {
|
||||
background: var(--control-background) !important;
|
||||
}
|
||||
697
app/app.js
Normal file
@@ -0,0 +1,697 @@
|
||||
import Stats from './thirdparty/three/stats.module.js';
|
||||
import {
|
||||
StudioLiteRenderer,
|
||||
partClasses
|
||||
} from './rendering/StudioLiteRenderer.js';
|
||||
import {
|
||||
sleep,
|
||||
rgbToHex
|
||||
} from './etc/Helpers.js';
|
||||
|
||||
const titleBar = document.body.querySelector(".title-bar");
|
||||
const sstat = document.body.querySelector("#menuitem-stats");
|
||||
const saxes = document.body.querySelector("#menuitem-axes");
|
||||
const sbx2 = document.body.querySelector("#menuitem-hoverbox");
|
||||
const sbox = document.body.querySelector("#menuitem-selectionbox");
|
||||
const szoom = document.body.querySelector("#menuitem-zoomto");
|
||||
const ssky = document.body.querySelector("#menuitem-skybox");
|
||||
const log = document.body.querySelector(".log");
|
||||
const msg = document.body.querySelector(".message");
|
||||
const editor = document.body.querySelector(".editor");
|
||||
const bar = document.body.querySelector(".status-bar-field > *[role=progressbar]");
|
||||
const status = document.body.querySelector(".status-bar-field.status");
|
||||
const menurender = document.body.querySelector("#menuitem-renderimage");
|
||||
menurender.setAttribute("aria-disabled", true);
|
||||
let sal = document.body.querySelector(".onboarding");
|
||||
let statusLastText = "Ready";
|
||||
szoom.setAttribute("aria-disabled", true);
|
||||
|
||||
var jsTreeData = [];
|
||||
|
||||
const threeIcons = {
|
||||
"AmbientLight": "Lighting",
|
||||
"DirectionalLight": "Lighting",
|
||||
"Mesh": "Part",
|
||||
"BoxHelper": "SelectionBox"
|
||||
}
|
||||
|
||||
function threeNodeToTreeData(node) {
|
||||
let data = [];
|
||||
for (const child of node) {
|
||||
data.push({
|
||||
node: child,
|
||||
text: child.name.length > 0 ? child.name : child.type,
|
||||
icon: `content/icons/${threeIcons[child.type] ? threeIcons[child.type] : "BaseScript"}.png`,
|
||||
children: threeNodeToTreeData(child.children)
|
||||
});
|
||||
}
|
||||
return data;
|
||||
}
|
||||
let treeRefresh2 = () => {};
|
||||
|
||||
let renderer = new StudioLiteRenderer({
|
||||
get width() {
|
||||
return window.innerWidth - 400;
|
||||
},
|
||||
get height() {
|
||||
return window.innerHeight - 200 - 29 - 34;
|
||||
},
|
||||
sharedFunctions: {
|
||||
print: print,
|
||||
treeRefresh2: () => { treeRefresh2(); }
|
||||
},
|
||||
sharedObjects: {
|
||||
jsTreeData: jsTreeData
|
||||
}
|
||||
});
|
||||
document.body.appendChild(renderer.domElement);
|
||||
|
||||
const stats = Stats();
|
||||
document.body.appendChild(stats.dom);
|
||||
stats.dom.id = "stats";
|
||||
stats.dom.style.display = "none";
|
||||
if (sstat.checked) stats.dom.style.display = "block";
|
||||
sstat.addEventListener("input", () => {
|
||||
if (sstat.checked) {
|
||||
stats.dom.style.display = "block";
|
||||
} else {
|
||||
stats.dom.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
let rendering = false;
|
||||
var animate = function () {
|
||||
if (rendering) renderer.render();
|
||||
stats.update();
|
||||
}
|
||||
|
||||
renderer.setAnimationLoop(animate);
|
||||
|
||||
function print(s) {
|
||||
const date = new Date();
|
||||
const el = document.createElement("span");
|
||||
const br = document.createElement("br");
|
||||
if (typeof s === "object") s = JSON.stringify(s);
|
||||
el.innerText = `${date.toLocaleTimeString("us")}.${date.getMilliseconds().toString().padStart(3, '0')} - ${s}`;
|
||||
log.appendChild(el);
|
||||
log.appendChild(br);
|
||||
log.scrollTo(0, log.scrollHeight);
|
||||
}
|
||||
|
||||
window.console.log = (...args) => {
|
||||
let s = "";
|
||||
for (let t of args) {
|
||||
if (typeof t === "object") t = JSON.stringify(t);
|
||||
s += t + " ";
|
||||
}
|
||||
s = s.substring(0, s.length - 1).split("\n");
|
||||
for (const t of s) print(t);
|
||||
}
|
||||
|
||||
function setTitle(t) {
|
||||
t = `${t} - Studio Lite (alpha-1)`;
|
||||
document.title = t;
|
||||
titleBar.querySelector(".title-bar-text").innerText = t;
|
||||
}
|
||||
|
||||
let file = "";
|
||||
|
||||
let placeName = "Start Page";
|
||||
|
||||
function barError() {
|
||||
bar.classList.add("error");
|
||||
}
|
||||
|
||||
function objToTreeData(obj) {
|
||||
let keys = Object.keys(obj);
|
||||
let data = [];
|
||||
for (let key of keys) {
|
||||
let d = {};
|
||||
d.text = key;
|
||||
if (typeof obj[key] === "object") {
|
||||
d.children = objToTreeData(obj[key]);
|
||||
} else {
|
||||
d.text += ": " + obj[key].toString();
|
||||
d.children = [];
|
||||
}
|
||||
data.push(d);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async function start() {
|
||||
bar.className = "marquee paused";
|
||||
status.innerText = "Busy";
|
||||
rendering = false;
|
||||
titleBar.classList.add("active");
|
||||
try { document.body.removeChild(document.body.querySelector("#remoteload")); } catch (ignored) {};
|
||||
document.body.removeChild(sal);
|
||||
sal = null;
|
||||
msg.querySelector("span").innerText = "Loading Place. Please wait...";
|
||||
msg.style.display = "flex";
|
||||
const url = new URL(file, window.location.href);
|
||||
try {
|
||||
if (typeof file === "string") {
|
||||
print (`DataModel Loading ${url}`);
|
||||
placeName = url.href.split("/").pop();
|
||||
status.innerText = `Fetching ${placeName}`;
|
||||
let fileData = null;
|
||||
try {
|
||||
fileData = await fetch(url);
|
||||
if (fileData.status !== 200) throw new Error();
|
||||
} catch (ignored) {
|
||||
print('Failed to fetch. Please reload.');
|
||||
barError();
|
||||
throw new Error("Failed to fetch. Please reload.");
|
||||
}
|
||||
window.data = await fileData.arrayBuffer();
|
||||
// we read it successfully, make the bar green from now on!
|
||||
bar.classList.remove("paused");
|
||||
status.innerText = `Loading ${placeName}`;
|
||||
await sleep(200);
|
||||
} else {
|
||||
print (`DataModel Loading ${file.name}`);
|
||||
placeName = file.name;
|
||||
status.innerText = `Fetching ${placeName}`;
|
||||
window.data = await file.arrayBuffer();
|
||||
// we read it successfully, make the bar green from now on!
|
||||
bar.classList.remove("paused");
|
||||
status.innerText = `Loading ${placeName}`;
|
||||
await sleep(200);
|
||||
}
|
||||
} catch (ignored) {
|
||||
print('The file could not be read. Please reload.');
|
||||
barError();
|
||||
throw new Error("The file could not be read. Please reload.");
|
||||
}
|
||||
setTitle(placeName);
|
||||
statusLastText = status.innerText;
|
||||
try {
|
||||
await renderer.loadPlace(window.data);
|
||||
} catch (ignored) {
|
||||
print('The file could not be read. Please reload.');
|
||||
barError();
|
||||
throw new Error("The file could not be read. Please reload.");
|
||||
}
|
||||
delete window.data;
|
||||
|
||||
renderer.toggleAxes(saxes.checked);
|
||||
saxes.addEventListener("input", () => {
|
||||
renderer.toggleAxes(saxes.checked);
|
||||
treeRefresh2();
|
||||
})
|
||||
|
||||
renderer.showSkybox(ssky.checked);
|
||||
ssky.addEventListener("input", () => {
|
||||
renderer.showSkybox(ssky.checked);
|
||||
})
|
||||
renderer.useSelectionBox = sbox.checked;
|
||||
sbox.addEventListener("input", () => {
|
||||
renderer.useSelectionBox = sbox.checked;
|
||||
renderer.removeSelectionBoxesIfNeeded();
|
||||
})
|
||||
renderer.useSelectionBox2 = sbx2.checked;
|
||||
sbx2.addEventListener("input", () => {
|
||||
renderer.useSelectionBox2 = sbx2.checked;
|
||||
renderer.removeSelectionBoxesIfNeeded();
|
||||
})
|
||||
|
||||
$(".tree").jstree({'core' : {
|
||||
'data' : jsTreeData
|
||||
}});
|
||||
|
||||
let treeRef = $(".tree").jstree(true);
|
||||
|
||||
const zoomTo = () => {
|
||||
try {
|
||||
const instance = treeRef.get_selected(true)[0].original.instance;
|
||||
if (partClasses.indexOf(instance.ClassName) !== -1) {
|
||||
try {
|
||||
renderer.zoomTo(instance.CFrame || instance.CoordinateFrame, false);
|
||||
} catch (ignored) {};
|
||||
}
|
||||
} catch (ignored) {};
|
||||
};
|
||||
|
||||
szoom.setAttribute("aria-disabled", true);
|
||||
szoom.addEventListener("click", zoomTo);
|
||||
document.addEventListener("keydown", e => {
|
||||
switch (e.code) {
|
||||
case "KeyF":
|
||||
zoomTo();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
const hiddenProperties = ["Children", "Source", "CollisionGroupData", "PhysicsGrid", "SmoothGrid", "MaterialColors"];
|
||||
const dropdownProperties = [
|
||||
"BackSurface", "BackSurfaceInput", "BottomSurfaceInput",
|
||||
"FrontSurface", "FrontSurfaceInput", "LeftSurface",
|
||||
"LeftSurfaceInput", "Material", "RightSurface",
|
||||
"RightSurfaceInput", "TopSurface", "TopSurfaceInput",
|
||||
"Shape", "Classic", "DevCameraOcclusionMode",
|
||||
"DevComputerCameraMovementMode", "DevComputerMovementMode", "DevTouchCameraMovementMode",
|
||||
"DevTouchMovementMode", "EnableDynamicHeads", "GameSettingsAvatar",
|
||||
"GameSettingsR15Collision", "ScreenOrientation", "VirtualCursorMode",
|
||||
"Technology", "VolumetricAudio", "AmbientReverb",
|
||||
"ClientAnimatorThrottling", "InterpolationThrottling", "InterpolationThrottling",
|
||||
"MeshPartHeadsAndAccessories", "ModelStreamingMode", "PhysicsSteppingMethod",
|
||||
"RejectCharacterDeletions", "ReplicateInstanceDestroySetting", "Retargeting",
|
||||
"StreamOutBehavior", "StreamingIntegrityMode", "Face",
|
||||
"RenderFidelity", "CameraType", "FieldOfViewMode",
|
||||
"RunContext", "SelectionBehaviorDown", "SelectionBehaviorLeft",
|
||||
"SelectionBehaviorRight", "SelectionBehaviorUp", "SizingMode",
|
||||
"ZIndexBehavior", "RollOffMode", "AutomaticSize",
|
||||
"BorderMode", "SizeConstraint", "Style",
|
||||
"LevelOfDetail", "HorizontalAlignment", "VerticalAlignment",
|
||||
"TextXAlignment", "TextYAlignment", "ScaleType",
|
||||
"AspectType", "DominantAxis", "BottomSurface",
|
||||
"CollisionType", "DisplayDistanceType", "HealthDisplayType",
|
||||
"NameOcclusion", "RigType"
|
||||
];
|
||||
const colorProperties = [
|
||||
"Color3", "TintColor", "Ambient", "ColorShift_Top", "ColorShift_Bottom",
|
||||
"FogColor", "OutdoorAmbient", "BackgroundColor3", "BorderColor3", "ImageColor3",
|
||||
"TextColor3", "PlaceholderColor3", "TextStrokeColor3"
|
||||
];
|
||||
|
||||
function parsePropValue(prop, value) {
|
||||
switch (prop) {
|
||||
default:
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
$('.tree').on("changed.jstree", async function (e, data) {
|
||||
const original = treeRef.get_node(data.selected[0]).original;
|
||||
let instance = original.instance;
|
||||
(async () => {
|
||||
if (original.id) renderer.updateSelectionBox(renderer.getThreeId(original.id));
|
||||
})();
|
||||
if (instance.CFrame) renderer.setAxesPosition(instance.CFrame.Position.X, instance.CFrame.Position.Y, instance.CFrame.Position.Z);
|
||||
if (partClasses.indexOf(instance.ClassName) !== -1 && (instance.CFrame || instance.CoordinateFrame))
|
||||
szoom.removeAttribute("aria-disabled");
|
||||
else
|
||||
szoom.setAttribute("aria-disabled", true);
|
||||
document.body.querySelector("#propertiesTitle").innerText = `Properties - ${instance.ClassName} "${instance.Name}"`;
|
||||
let keys = Object.keys(instance);
|
||||
let objviewIndex = 0;
|
||||
let treedatatemp = null;
|
||||
propTable.innerHTML = "";
|
||||
propSpinner.style.display = "block";
|
||||
propTable.style.display = "none";
|
||||
if (["Script", "LocalScript", "ModuleScript"].indexOf(instance.ClassName) !== -1) {
|
||||
setTitle(instance.Name);
|
||||
editor.style.display = "block";
|
||||
editor.innerHTML = "";
|
||||
const pre = document.createElement("pre");
|
||||
pre.innerText = instance.Source;
|
||||
editor.appendChild(pre);
|
||||
rendering = false;
|
||||
} else {
|
||||
rendering = true;
|
||||
setTitle(placeName);
|
||||
editor.innerHTML = "";
|
||||
editor.style.display = "none";
|
||||
}
|
||||
for (let key of keys) {
|
||||
if (hiddenProperties.indexOf(key) === -1) {
|
||||
let row = document.createElement("tr");
|
||||
let one = document.createElement("td");
|
||||
let two = document.createElement("td");
|
||||
one.innerText = key;
|
||||
if (typeof instance[key] === "object") {
|
||||
let objview = document.createElement("div");
|
||||
if (colorProperties.indexOf(key) !== -1) {
|
||||
const val = instance[key];
|
||||
let clr = document.createElement("input");
|
||||
clr.type = "color";
|
||||
clr.disabled = true;
|
||||
clr.value = rgbToHex(val.R*255, val.G*255, val.B*255);
|
||||
two.appendChild(clr);
|
||||
}
|
||||
two.appendChild(objview);
|
||||
objview.id = `objview-${objviewIndex}`;
|
||||
treedatatemp = {'core' : {
|
||||
'data' : objToTreeData(instance[key])
|
||||
}};
|
||||
} else if ([true, false].indexOf(instance[key]) !== -1) {
|
||||
let chk = document.createElement("input");
|
||||
chk.disabled = "true";
|
||||
chk.type = "checkbox";
|
||||
chk.id = "checkbox-" + key;
|
||||
if (instance[key]) chk.checked = true;
|
||||
let txt = document.createElement("label");
|
||||
txt.for = "checkbox-" + key;
|
||||
two.appendChild(chk);
|
||||
two.appendChild(txt);
|
||||
} else if (typeof instance[key] === "number") {
|
||||
let nr = document.createElement("input");
|
||||
nr.type = "number";
|
||||
nr.min = instance[key];
|
||||
nr.max = instance[key];
|
||||
nr.value = instance[key];
|
||||
nr.disabled = true;
|
||||
two.appendChild(nr);
|
||||
} else if (dropdownProperties.indexOf(key) !== -1) {
|
||||
let sel = document.createElement("select");
|
||||
let opt = document.createElement("option");
|
||||
opt.innerText = instance[key];
|
||||
opt.selected = true;
|
||||
sel.disabled = true;
|
||||
sel.appendChild(opt);
|
||||
two.appendChild(sel);
|
||||
} else {
|
||||
two.innerText = parsePropValue(key, instance[key]);
|
||||
}
|
||||
row.appendChild(one);
|
||||
row.appendChild(two);
|
||||
propTable.appendChild(row);
|
||||
if (treedatatemp !== null) {
|
||||
$(`#objview-${objviewIndex}`).jstree(treedatatemp);
|
||||
objviewIndex++;
|
||||
treedatatemp = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
propSpinner.style.display = "none";
|
||||
propTable.style.display = "table-row-group";
|
||||
});
|
||||
|
||||
rendering = true;
|
||||
msg.style.display = "none";
|
||||
statusLastText = "Busy";
|
||||
status.innerText = "Busy";
|
||||
|
||||
async function postQueue() {
|
||||
if (renderer.mAssetManager.hasRbxGameAsset) print("This Place contains rbxgameasset:// paths. Studio Lite cannot resolve them, as they are relative to the published game!");
|
||||
if (renderer.mAssetManager.computeMissingAssetsList()) print("Some assets failed to load! See File > Missing assets for more info.");
|
||||
treeRefresh2();
|
||||
menurender.removeAttribute("aria-disabled");
|
||||
menurender.addEventListener("click", () => {
|
||||
rendering = false;
|
||||
const dlg = document.body.querySelector("#render");
|
||||
const btn = dlg.querySelector("#dorender");
|
||||
const img = dlg.querySelector("img");
|
||||
const width = dlg.querySelector("#renderwidth");
|
||||
const height = dlg.querySelector("#renderheight");
|
||||
width.value = renderer.conf.width;
|
||||
height.value = renderer.conf.height;
|
||||
let doRender = () => {
|
||||
img.src = renderer.renderImage(width.value, height.value);
|
||||
}
|
||||
let close = () => {
|
||||
dlg.removeEventListener("cancel", close);
|
||||
dlg.querySelector(".close").removeEventListener("click", close);
|
||||
dlg.querySelector(".ok").removeEventListener("click", close);
|
||||
btn.removeEventListener("click", doRender);
|
||||
img.src = "";
|
||||
dlg.close();
|
||||
titleBar.classList.add("active");
|
||||
rendering = true;
|
||||
}
|
||||
dlg.addEventListener("cancel", close);
|
||||
titleBar.classList.remove("active");
|
||||
dlg.showModal();
|
||||
dlg.querySelector(".close").addEventListener("click", close);
|
||||
dlg.querySelector(".ok").addEventListener("click", close);
|
||||
btn.addEventListener("click", doRender);
|
||||
});
|
||||
}
|
||||
|
||||
let waitForQueuedOps = setInterval(() => {
|
||||
if (renderer.queueSize === 0) {
|
||||
clearInterval(waitForQueuedOps);
|
||||
waitForQueuedOps = null;
|
||||
bar.className = "animate";
|
||||
statusLastText = "Ready";
|
||||
status.innerText = "Ready";
|
||||
postQueue();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
sal.style.display = "flex";
|
||||
|
||||
const menuabout = document.body.querySelector("#menuitem-about");
|
||||
document.body.querySelector("#menuitem-reload").addEventListener("click", () => window.location.reload());
|
||||
menuabout.addEventListener("click", () => {
|
||||
const dlg = document.body.querySelector("#about");
|
||||
let close = () => {
|
||||
dlg.removeEventListener("cancel", close);
|
||||
dlg.querySelector(".close").removeEventListener("click", close);
|
||||
dlg.querySelector(".ok").removeEventListener("click", close);
|
||||
dlg.querySelector(".view-notice").removeEventListener("click", viewNotice);
|
||||
dlg.close();
|
||||
titleBar.classList.add("active");
|
||||
}
|
||||
dlg.addEventListener("cancel", close);
|
||||
titleBar.classList.remove("active");
|
||||
dlg.showModal();
|
||||
dlg.querySelector(".close").addEventListener("click", close);
|
||||
dlg.querySelector(".ok").addEventListener("click", close);
|
||||
dlg.querySelector(".view-notice").addEventListener("click", viewNotice);
|
||||
});
|
||||
|
||||
const menuassets = document.body.querySelector("#menuitem-missingassets");
|
||||
menuassets.addEventListener("click", () => {
|
||||
openNotepad("missing_assets.txt", renderer.mAssetManager.missingAssets);
|
||||
});
|
||||
|
||||
function viewNotice() {
|
||||
openNotepad("notice.txt", `Studio Lite is built upon MrSprinkleToes' rbxBinaryParser [https://github.com/MrSprinkleToes/rbxBinaryParser].
|
||||
Additionally, some code from roblox-in-webbrowser [https://github.com/MrSprinkleToes/roblox-in-webbrowser] and roblox-web-viewer [https://github.com/MrSprinkleToes/roblox-web-viewer] is used.
|
||||
Please see the README for more information.
|
||||
|
||||
The THREE.js library [https://threejs.org] is used for rendering the game view.
|
||||
The THREE.js add-ons PointerLockControls and Stats are also included within the application.
|
||||
|
||||
jsTree [https://www.jstree.com] is used for displaying interactive trees in the Explorer and Properties views.
|
||||
|
||||
7.css [https://khang-nd.github.io/7.css] is used for theming the user interface.
|
||||
|
||||
Roblox Studio icons and other assets stored within the content/ directory, as well as the favicon.ico, are Copyright © Roblox Corporation.
|
||||
The assets saved within the asset/ directory and the example files in examples/ are copyright of their respective owners.
|
||||
|
||||
Studio Lite is not affiliated in any way, shape or form with Roblox Corporation.`, true);
|
||||
}
|
||||
|
||||
function openNotepad(fileName, text, nested = false) {
|
||||
const dlg = document.body.querySelector("#notepad");
|
||||
const title = dlg.querySelector("#dialog-title");
|
||||
const textarea = dlg.querySelector("textarea");
|
||||
title.innerText = `${fileName} - Notepad`;
|
||||
textarea.value = text;
|
||||
let close = () => {
|
||||
dlg.removeEventListener("cancel", close);
|
||||
dlg.querySelector(".close").removeEventListener("click", close);
|
||||
dlg.close();
|
||||
title.innerText = "Notepad";
|
||||
textarea.innerText = "";
|
||||
if (!nested) titleBar.classList.add("active");
|
||||
}
|
||||
dlg.addEventListener("cancel", close);
|
||||
titleBar.classList.remove("active");
|
||||
dlg.showModal();
|
||||
dlg.querySelector(".close").addEventListener("click", close);
|
||||
}
|
||||
|
||||
const menuchangelog = document.body.querySelector("#menuitem-changelog");
|
||||
menuchangelog.addEventListener("click", () => openNotepad("changelog.txt", `Studio Lite Changelog
|
||||
|
||||
New in alpha-1:
|
||||
- Initial release.`));
|
||||
|
||||
const examples = [
|
||||
"examples/Classic-Crossroads.rbxl",
|
||||
"examples/City.rbxl",
|
||||
"examples/Suburban.rbxl",
|
||||
"examples/Western-Lounge.rbxm",
|
||||
"examples/Dilapidated-House.rbxm"
|
||||
];
|
||||
|
||||
const buttons = sal.querySelectorAll("button");
|
||||
buttons[0].addEventListener("click", async () => {
|
||||
let dlg = document.body.querySelector("#remoteload");
|
||||
let txt = dlg.querySelector("input");
|
||||
let btn = dlg.querySelector(".open");
|
||||
let exp = dlg.querySelector("#examples");
|
||||
exp.innerHTML = "";
|
||||
for (const e of examples) {
|
||||
let el = document.createElement("li");
|
||||
el.role = "option";
|
||||
el.innerText = e;
|
||||
el.addEventListener("click", () => {
|
||||
txt.value = el.innerText;
|
||||
txt.dispatchEvent(new Event("input"));
|
||||
});
|
||||
exp.appendChild(el);
|
||||
}
|
||||
let close = () => {
|
||||
dlg.removeEventListener("cancel", close);
|
||||
dlg.querySelector(".close").removeEventListener("click", close);
|
||||
dlg.querySelector(".cancel").removeEventListener("click", close);
|
||||
btn.removeEventListener("click", doLoad);
|
||||
txt.removeEventListener("keyup", keyup);
|
||||
txt.removeEventListener("input", input);
|
||||
titleBar.classList.add("active");
|
||||
dlg.close();
|
||||
}
|
||||
dlg.addEventListener("cancel", close);
|
||||
titleBar.classList.remove("active");
|
||||
dlg.showModal();
|
||||
dlg.querySelector(".close").addEventListener("click", close);
|
||||
dlg.querySelector(".cancel").addEventListener("click", close);
|
||||
let doLoad = () => {
|
||||
doLoad = null;
|
||||
file = txt.value;
|
||||
close(); close = null;
|
||||
try { document.body.removeChild(dlg); } catch (ignored) {};
|
||||
dlg = null; txt = null; btn = null;
|
||||
start();
|
||||
};
|
||||
btn.addEventListener("click", doLoad);
|
||||
let keyup = ({key}) => {
|
||||
if (!btn.disabled && key === "Enter") doLoad();
|
||||
};
|
||||
txt.addEventListener("keyup", keyup);
|
||||
let input = () => {
|
||||
if (txt.value.trim().length > 0) {
|
||||
btn.disabled = false;
|
||||
} else {
|
||||
btn.disabled = true;
|
||||
}
|
||||
};
|
||||
txt.addEventListener("input", input);
|
||||
})
|
||||
|
||||
const browse = sal.querySelector("input[type=file]");
|
||||
browse.addEventListener("input", () => {
|
||||
file = browse.files[0];
|
||||
start();
|
||||
})
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
renderer.resize();
|
||||
})
|
||||
|
||||
const tree = document.body.querySelector(".tree");
|
||||
const tree2 = document.body.querySelector(".tree2");
|
||||
const datamodeltab = document.body.querySelector("button[aria-controls=datamodel-explorer]");
|
||||
const scenetab = document.body.querySelector("button[aria-controls=scene-explorer]");
|
||||
tree2.style.display = "none";
|
||||
datamodeltab.addEventListener("mousedown", () => {
|
||||
datamodeltab.ariaSelected = true;
|
||||
scenetab.ariaSelected = false;
|
||||
tree2.style.display = "none";
|
||||
tree.style.display = "block";
|
||||
});
|
||||
let treeRefresh2Pending = false;
|
||||
scenetab.addEventListener("mousedown", () => {
|
||||
datamodeltab.ariaSelected = false;
|
||||
scenetab.ariaSelected = true;
|
||||
tree.style.display = "none";
|
||||
rendering = true;
|
||||
setTitle(placeName);
|
||||
editor.innerHTML = "";
|
||||
editor.style.display = "none";
|
||||
tree2.style.display = "block";
|
||||
(async () => {
|
||||
if (treeRefresh2Pending) {
|
||||
treeRefresh2();
|
||||
treeRefresh2Pending = false;
|
||||
}
|
||||
})();
|
||||
});
|
||||
datamodeltab.disabled = false;
|
||||
scenetab.disabled = false;
|
||||
|
||||
$(".tree2").jstree({'core' : {
|
||||
'data': []
|
||||
}});
|
||||
|
||||
let treeRef2 = $(".tree2").jstree(true);
|
||||
let propTable = document.body.querySelector(".properties > table > tbody");
|
||||
let propSpinner = document.body.querySelector(".properties > .loader");
|
||||
|
||||
treeRefresh2 = () => {
|
||||
// don't render what we can't see!
|
||||
if (tree2.style.display === "none") { treeRefresh2Pending = true; return; }
|
||||
treeRef2.settings.core.data = threeNodeToTreeData([renderer.scene]);
|
||||
treeRef2.refresh();
|
||||
}
|
||||
|
||||
$('.tree2').on("changed.jstree", async function (e, data) {
|
||||
let node = treeRef2.get_node(data.selected[0]).original;
|
||||
if (node) {} else return;
|
||||
node = node.node.object;
|
||||
document.body.querySelector("#propertiesTitle").innerText = `Properties - ${node.type} "${node.name ? node.name : node.type}"`;
|
||||
let keys = Object.keys(node);
|
||||
let objviewIndex = 0;
|
||||
let treedatatemp = null;
|
||||
propTable.innerHTML = "";
|
||||
propSpinner.style.display = "block";
|
||||
propTable.style.display = "none";
|
||||
for (let key of keys) {
|
||||
if (key !== "children") {
|
||||
let row = document.createElement("tr");
|
||||
let one = document.createElement("td");
|
||||
let two = document.createElement("td");
|
||||
one.innerText = key;
|
||||
if (typeof node[key] === "object") {
|
||||
let objview = document.createElement("div");
|
||||
two.appendChild(objview);
|
||||
objview.id = `objview-${objviewIndex}`;
|
||||
treedatatemp = {'core' : {
|
||||
'data' : objToTreeData(node[key])
|
||||
}};
|
||||
} else if (key === "color") {
|
||||
let clr = document.createElement("input");
|
||||
clr.type = "color";
|
||||
clr.disabled = true;
|
||||
clr.value = "#" + node[key].toString(16);
|
||||
two.appendChild(clr);
|
||||
let span = document.createElement("span");
|
||||
span.innerHTML = ` ${clr.value}`;
|
||||
two.appendChild(span);
|
||||
} else if ([true, false].indexOf(node[key]) !== -1) {
|
||||
let chk = document.createElement("input");
|
||||
chk.disabled = "true";
|
||||
chk.type = "checkbox";
|
||||
chk.id = "checkbox-" + key;
|
||||
if (node[key]) chk.checked = true;
|
||||
let txt = document.createElement("label");
|
||||
txt.for = "checkbox-" + key;
|
||||
two.appendChild(chk);
|
||||
two.appendChild(txt);
|
||||
} else if (typeof node[key] === "number") {
|
||||
let nr = document.createElement("input");
|
||||
nr.type = "number";
|
||||
nr.min = node[key];
|
||||
nr.max = node[key];
|
||||
nr.value = node[key];
|
||||
nr.disabled = true;
|
||||
two.appendChild(nr);
|
||||
} else {
|
||||
two.innerText = node[key].toString();
|
||||
}
|
||||
row.appendChild(one);
|
||||
row.appendChild(two);
|
||||
propTable.appendChild(row);
|
||||
if (treedatatemp !== null) {
|
||||
$(`#objview-${objviewIndex}`).jstree(treedatatemp);
|
||||
objviewIndex++;
|
||||
treedatatemp = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
propSpinner.style.display = "none";
|
||||
propTable.style.display = "table-row-group";
|
||||
});
|
||||
|
||||
setTitle("Start Page");
|
||||
print("Welcome to Studio Lite (alpha-1)!");
|
||||
|
||||
rendering = true;
|
||||
|
||||
treeRefresh2();
|
||||
135
app/controls/FlyCamera.js
Normal file
@@ -0,0 +1,135 @@
|
||||
import { PointerLockControls } from "./PointerLockControls.js";
|
||||
|
||||
export default class FlyCamera {
|
||||
/**
|
||||
*
|
||||
* @param {*} cam THREE.PerspectiveCamera to control
|
||||
* @param {*} domElement The DOM element to listen for click & key events on
|
||||
*/
|
||||
constructor(cam, domElement) {
|
||||
this.cam = cam;
|
||||
this.domElement = domElement;
|
||||
this.movementSpeed = 0;
|
||||
this.horizontalMovementSpeed = 0;
|
||||
this.verticalMovementSpeed = 0;
|
||||
this.flySpeed = 5;
|
||||
// this.lookSpeed = 0.005;
|
||||
this.controls = new PointerLockControls(this.cam, this.domElement);
|
||||
|
||||
/**
|
||||
* Locks mouse on click
|
||||
*/
|
||||
domElement.addEventListener('click', () => {
|
||||
this.controls.lock();
|
||||
});
|
||||
/**
|
||||
* Sets movement directions based on key presses
|
||||
*/
|
||||
window.addEventListener("keydown", (e) => {
|
||||
if (!this.controls.isLocked) return;
|
||||
switch (e.code) {
|
||||
case "KeyW":
|
||||
this.moveForward = true;
|
||||
break;
|
||||
case "KeyS":
|
||||
this.moveBackward = true;
|
||||
break;
|
||||
case "KeyA":
|
||||
this.moveLeft = true;
|
||||
break;
|
||||
case "KeyD":
|
||||
this.moveRight = true;
|
||||
break;
|
||||
case "KeyE":
|
||||
this.moveUp = true;
|
||||
break;
|
||||
case "KeyQ":
|
||||
this.moveDown = true;
|
||||
break;
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Sets movement directions based on key releases
|
||||
*/
|
||||
window.addEventListener("keyup", (e) => {
|
||||
if (!this.controls.isLocked) return;
|
||||
switch (e.code) {
|
||||
case "KeyW":
|
||||
this.moveForward = false;
|
||||
break;
|
||||
case "KeyS":
|
||||
this.moveBackward = false;
|
||||
break;
|
||||
case "KeyA":
|
||||
this.moveLeft = false;
|
||||
break;
|
||||
case "KeyD":
|
||||
this.moveRight = false;
|
||||
break;
|
||||
case "KeyE":
|
||||
this.moveUp = false;
|
||||
break;
|
||||
case "KeyQ":
|
||||
this.moveDown = false;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Updates this.cam position based on movement directions
|
||||
* @param {number} dt
|
||||
*/
|
||||
update(dt) {
|
||||
if (!this.controls.isLocked) return;
|
||||
// console.log(this.moveForward);
|
||||
if (this.moveForward) {
|
||||
// console.log("forward");
|
||||
if (this.movementSpeed < 50) {
|
||||
this.movementSpeed += this.flySpeed;
|
||||
}
|
||||
} else if (this.moveBackward) {
|
||||
if (this.movementSpeed > -50) {
|
||||
this.movementSpeed -= this.flySpeed;
|
||||
}
|
||||
} else {
|
||||
if (this.movementSpeed > 0) {
|
||||
this.movementSpeed -= this.flySpeed;
|
||||
} else if (this.movementSpeed < 0) {
|
||||
this.movementSpeed += this.flySpeed;
|
||||
}
|
||||
}
|
||||
if (this.moveLeft) {
|
||||
if (this.horizontalMovementSpeed > -50) {
|
||||
this.horizontalMovementSpeed -= this.flySpeed;
|
||||
}
|
||||
} else if (this.moveRight) {
|
||||
if (this.horizontalMovementSpeed < 50) {
|
||||
this.horizontalMovementSpeed += this.flySpeed;
|
||||
}
|
||||
} else {
|
||||
if (this.horizontalMovementSpeed > 0) {
|
||||
this.horizontalMovementSpeed -= this.flySpeed;
|
||||
} else if (this.horizontalMovementSpeed < 0) {
|
||||
this.horizontalMovementSpeed += this.flySpeed;
|
||||
}
|
||||
}
|
||||
if (this.moveUp) {
|
||||
if (this.verticalMovementSpeed < 50) {
|
||||
this.verticalMovementSpeed += this.flySpeed;
|
||||
}
|
||||
} else if (this.moveDown) {
|
||||
if (this.verticalMovementSpeed > -50) {
|
||||
this.verticalMovementSpeed -= this.flySpeed;
|
||||
}
|
||||
} else {
|
||||
if (this.verticalMovementSpeed > 0) {
|
||||
this.verticalMovementSpeed -= this.flySpeed;
|
||||
} else if (this.verticalMovementSpeed < 0) {
|
||||
this.verticalMovementSpeed += this.flySpeed;
|
||||
}
|
||||
}
|
||||
this.cam.translateX(this.horizontalMovementSpeed * dt);
|
||||
this.cam.translateY(this.verticalMovementSpeed * dt);
|
||||
this.cam.translateZ(-this.movementSpeed * dt);
|
||||
}
|
||||
}
|
||||
272
app/controls/PointerLockControls.js
Normal file
@@ -0,0 +1,272 @@
|
||||
import {
|
||||
Controls,
|
||||
Euler,
|
||||
Vector3
|
||||
} from '../thirdparty/three/three.core.js';
|
||||
|
||||
const _euler = new Euler( 0, 0, 0, 'YXZ' );
|
||||
const _vector = new Vector3();
|
||||
|
||||
/**
|
||||
* Fires when the user moves the mouse.
|
||||
*
|
||||
* @event PointerLockControls#change
|
||||
* @type {Object}
|
||||
*/
|
||||
const _changeEvent = { type: 'change' };
|
||||
|
||||
/**
|
||||
* Fires when the pointer lock status is "locked" (in other words: the mouse is captured).
|
||||
*
|
||||
* @event PointerLockControls#lock
|
||||
* @type {Object}
|
||||
*/
|
||||
const _lockEvent = { type: 'lock' };
|
||||
|
||||
/**
|
||||
* Fires when the pointer lock status is "unlocked" (in other words: the mouse is not captured anymore).
|
||||
*
|
||||
* @event PointerLockControls#unlock
|
||||
* @type {Object}
|
||||
*/
|
||||
const _unlockEvent = { type: 'unlock' };
|
||||
|
||||
const _MOUSE_SENSITIVITY = 0.002;
|
||||
const _PI_2 = Math.PI / 2;
|
||||
|
||||
/**
|
||||
* The implementation of this class is based on the [Pointer Lock API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Pointer_Lock_API}.
|
||||
* `PointerLockControls` is a perfect choice for first person 3D games.
|
||||
*
|
||||
* ```js
|
||||
* const controls = new PointerLockControls( camera, document.body );
|
||||
*
|
||||
* // add event listener to show/hide a UI (e.g. the game's menu)
|
||||
* controls.addEventListener( 'lock', function () {
|
||||
*
|
||||
* menu.style.display = 'none';
|
||||
*
|
||||
* } );
|
||||
*
|
||||
* controls.addEventListener( 'unlock', function () {
|
||||
*
|
||||
* menu.style.display = 'block';
|
||||
*
|
||||
* } );
|
||||
* ```
|
||||
*
|
||||
* @augments Controls
|
||||
* @three_import import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
|
||||
*/
|
||||
class PointerLockControls extends Controls {
|
||||
|
||||
/**
|
||||
* Constructs a new controls instance.
|
||||
*
|
||||
* @param {Camera} camera - The camera that is managed by the controls.
|
||||
* @param {?HTMLDOMElement} domElement - The HTML element used for event listeners.
|
||||
*/
|
||||
constructor( camera, domElement = null ) {
|
||||
|
||||
super( camera, domElement );
|
||||
|
||||
/**
|
||||
* Whether the controls are locked or not.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
* @default false
|
||||
*/
|
||||
this.isLocked = false;
|
||||
|
||||
/**
|
||||
* Camera pitch, lower limit. Range is '[0, Math.PI]' in radians.
|
||||
*
|
||||
* @type {number}
|
||||
* @default 0
|
||||
*/
|
||||
this.minPolarAngle = 0;
|
||||
|
||||
/**
|
||||
* Camera pitch, upper limit. Range is '[0, Math.PI]' in radians.
|
||||
*
|
||||
* @type {number}
|
||||
* @default Math.PI
|
||||
*/
|
||||
this.maxPolarAngle = Math.PI;
|
||||
|
||||
/**
|
||||
* Multiplier for how much the pointer movement influences the camera rotation.
|
||||
*
|
||||
* @type {number}
|
||||
* @default 1
|
||||
*/
|
||||
this.pointerSpeed = 1.0;
|
||||
|
||||
// event listeners
|
||||
|
||||
this._onMouseMove = onMouseMove.bind( this );
|
||||
this._onPointerlockChange = onPointerlockChange.bind( this );
|
||||
this._onPointerlockError = onPointerlockError.bind( this );
|
||||
|
||||
if ( this.domElement !== null ) {
|
||||
|
||||
this.connect( this.domElement );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
connect( element ) {
|
||||
|
||||
super.connect( element );
|
||||
|
||||
this.domElement.ownerDocument.addEventListener( 'mousemove', this._onMouseMove );
|
||||
this.domElement.ownerDocument.addEventListener( 'pointerlockchange', this._onPointerlockChange );
|
||||
this.domElement.ownerDocument.addEventListener( 'pointerlockerror', this._onPointerlockError );
|
||||
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
|
||||
this.domElement.ownerDocument.removeEventListener( 'mousemove', this._onMouseMove );
|
||||
this.domElement.ownerDocument.removeEventListener( 'pointerlockchange', this._onPointerlockChange );
|
||||
this.domElement.ownerDocument.removeEventListener( 'pointerlockerror', this._onPointerlockError );
|
||||
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
||||
this.disconnect();
|
||||
|
||||
}
|
||||
|
||||
getObject() {
|
||||
|
||||
console.warn( 'THREE.PointerLockControls: getObject() has been deprecated. Use controls.object instead.' ); // @deprecated r169
|
||||
|
||||
return this.object;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the look direction of the camera.
|
||||
*
|
||||
* @param {Vector3} v - The target vector that is used to store the method's result.
|
||||
* @return {Vector3} The normalized direction vector.
|
||||
*/
|
||||
getDirection( v ) {
|
||||
|
||||
return v.set( 0, 0, - 1 ).applyQuaternion( this.object.quaternion );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the camera forward parallel to the xz-plane. Assumes camera.up is y-up.
|
||||
*
|
||||
* @param {number} distance - The signed distance.
|
||||
*/
|
||||
moveForward( distance ) {
|
||||
|
||||
if ( this.enabled === false ) return;
|
||||
|
||||
// move forward parallel to the xz-plane
|
||||
// assumes camera.up is y-up
|
||||
|
||||
const camera = this.object;
|
||||
|
||||
_vector.setFromMatrixColumn( camera.matrix, 0 );
|
||||
|
||||
_vector.crossVectors( camera.up, _vector );
|
||||
|
||||
camera.position.addScaledVector( _vector, distance );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the camera sidewards parallel to the xz-plane.
|
||||
*
|
||||
* @param {number} distance - The signed distance.
|
||||
*/
|
||||
moveRight( distance ) {
|
||||
|
||||
if ( this.enabled === false ) return;
|
||||
|
||||
const camera = this.object;
|
||||
|
||||
_vector.setFromMatrixColumn( camera.matrix, 0 );
|
||||
|
||||
camera.position.addScaledVector( _vector, distance );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the pointer lock.
|
||||
*
|
||||
* @param {boolean} [unadjustedMovement=false] - Disables OS-level adjustment for mouse acceleration, and accesses raw mouse input instead.
|
||||
* Setting it to true will disable mouse acceleration.
|
||||
*/
|
||||
lock( unadjustedMovement = false ) {
|
||||
|
||||
this.domElement.requestPointerLock( {
|
||||
unadjustedMovement
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Exits the pointer lock.
|
||||
*/
|
||||
unlock() {
|
||||
|
||||
this.domElement.ownerDocument.exitPointerLock();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// event listeners
|
||||
|
||||
function onMouseMove( event ) {
|
||||
|
||||
if ( this.enabled === false || this.isLocked === false ) return;
|
||||
|
||||
const camera = this.object;
|
||||
_euler.setFromQuaternion( camera.quaternion );
|
||||
|
||||
_euler.y -= event.movementX * _MOUSE_SENSITIVITY * this.pointerSpeed;
|
||||
_euler.x -= event.movementY * _MOUSE_SENSITIVITY * this.pointerSpeed;
|
||||
|
||||
_euler.x = Math.max( _PI_2 - this.maxPolarAngle, Math.min( _PI_2 - this.minPolarAngle, _euler.x ) );
|
||||
|
||||
camera.quaternion.setFromEuler( _euler );
|
||||
|
||||
this.dispatchEvent( _changeEvent );
|
||||
|
||||
}
|
||||
|
||||
function onPointerlockChange() {
|
||||
|
||||
if ( this.domElement.ownerDocument.pointerLockElement === this.domElement ) {
|
||||
|
||||
this.dispatchEvent( _lockEvent );
|
||||
|
||||
this.isLocked = true;
|
||||
|
||||
} else {
|
||||
|
||||
this.dispatchEvent( _unlockEvent );
|
||||
|
||||
this.isLocked = false;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function onPointerlockError() {
|
||||
|
||||
console.error( 'THREE.PointerLockControls: Unable to use Pointer Lock API' );
|
||||
|
||||
}
|
||||
|
||||
export { PointerLockControls };
|
||||
5
app/datamodel/DataModelUtils.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export function findByClassName(parent, className) {
|
||||
for (let child of parent) {
|
||||
if (child && child.ClassName === className) return child;
|
||||
}
|
||||
}
|
||||
12
app/etc/Helpers.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export function sleep(ms) { return new Promise((r) => { setTimeout(r, ms); }); }
|
||||
|
||||
/* https://stackoverflow.com/a/5624139 */
|
||||
function componentToHex(c) {
|
||||
var hex = c.toString(16);
|
||||
return hex.length == 1 ? "0" + hex : hex;
|
||||
}
|
||||
|
||||
export function rgbToHex(r, g, b) {
|
||||
return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
|
||||
}
|
||||
/* https://stackoverflow.com/a/5624139 */
|
||||
79
app/http/AssetManager.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import { tryToFetch_status } from "./TryToFetch.js";
|
||||
|
||||
export default class AssetManager {
|
||||
constructor(conf) {
|
||||
this.conf = conf;
|
||||
this.parseCache = {};
|
||||
this.failedAssets = [];
|
||||
this.texturesOfFailedMeshes = {};
|
||||
this.missingAssets = `You haven't opened a Place yet. Open a Place to get insights on missing assets.`;
|
||||
this.hasRbxGameAsset = false;
|
||||
}
|
||||
|
||||
getAssetId(path) {
|
||||
if (path.split("://")[0] === "rbxassetid") return path.split("://").pop().trim();
|
||||
if (path.split("://")[0] === "http") return path.split("=").pop().trim();
|
||||
if (!this.hasRbxGameAsset && path.split("://")[0] === "rbxgameasset") this.hasRbxGameAsset = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
pushFailedMesh(assetId, meshTexture) {
|
||||
if (this.texturesOfFailedMeshes[assetId]) {} else this.texturesOfFailedMeshes[assetId] = [];
|
||||
if (this.texturesOfFailedMeshes[assetId].indexOf(meshTexture) === -1) this.texturesOfFailedMeshes[assetId].push(meshTexture);
|
||||
}
|
||||
|
||||
async parseAssetPath(path, type = "decal", meshTexture = "N/A") {
|
||||
if (meshTexture === false) meshTexture = "N/A";
|
||||
const assetId = this.getAssetId(path);
|
||||
if (type === "mesh" && this.parseCache[path] && this.parseCache[path].length === 0) this.pushFailedMesh(assetId, meshTexture);
|
||||
if (this.parseCache[path]) return this.parseCache[path];
|
||||
//status.innerText = "Loading assets...";
|
||||
let href = "";
|
||||
if (path.split("://")[0] === "rbxasset") href = "content/" + path.split("://").pop();
|
||||
if (assetId) href = "asset/" + assetId.trim() + (type === "decal" ? ".png" : ".mesh");
|
||||
if (this.failedAssets.indexOf(assetId) !== -1) { this.parseCache[path] = ""; return this.parseCache[path]; }
|
||||
if (href.trim().length === 0) {
|
||||
if (path.trim().length === 0)
|
||||
//print("parseAssetPath was called with an empty string!");
|
||||
;
|
||||
else
|
||||
if (path.split("://")[0] !== "rbxgameasset") this.conf.sharedFunctions.print(`Asset path ${path} could not be resolved! This might be a deficiency in Studio Lite, please open an issue!`);
|
||||
href = "";
|
||||
} else {
|
||||
if (path.trim().length === 0) { this.parseCache[path] = ""; return this.parseCache[path]; }
|
||||
const fetchResult = await tryToFetch_status(href);
|
||||
if (fetchResult === 200) { this.parseCache[path] = href; return href; } else {
|
||||
if (fetchResult === 404 && assetId !== false && this.failedAssets.indexOf(assetId) === -1) {
|
||||
this.failedAssets.push(assetId);
|
||||
}
|
||||
if (type === "mesh" && fetchResult === 404 && assetId !== false && meshTexture !== false) this.pushFailedMesh(assetId, meshTexture);
|
||||
this.parseCache[path] = "";
|
||||
return this.parseCache[path];
|
||||
}
|
||||
}
|
||||
this.parseCache[path] = href;
|
||||
//status.innerText = statusLastText;
|
||||
return href;
|
||||
}
|
||||
|
||||
computeMissingAssetsList() {
|
||||
if (this.failedAssets.length > 0) {
|
||||
this.missingAssets = `Below are listed all the assets in this Place that have failed to load, likely because they aren't saved on the server.
|
||||
To fix this, download the missing assets using the AssetDelivery API and place them in the asset/ directory.
|
||||
The name must be given as [assetid].[extension], where the extension is:
|
||||
.png for image assets
|
||||
.mesh for meshes
|
||||
|
||||
AssetID listing starts here:`;
|
||||
for (const i in this.failedAssets) {
|
||||
const asset = this.failedAssets[i];
|
||||
const textures = this.texturesOfFailedMeshes[asset];
|
||||
this.missingAssets += `\n${textures ? "Mesh" : "Decal"} ${asset.trim()}${textures ? `, texture ${textures.join(", ")}` : ""}`;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
this.missingAssets = "All assets in this Place have loaded successfully.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
app/http/TryToFetch.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export async function tryToFetch_status(url) {
|
||||
if (url.trim().length === 0) return false;
|
||||
try {
|
||||
let r = await fetch(url, {method: "HEAD"});
|
||||
return r.status;
|
||||
} catch (ignored) { return false; }
|
||||
}
|
||||
|
||||
export async function tryToFetch(url) {
|
||||
if (await tryToFetch_status(url) === 200) return true;
|
||||
return false;
|
||||
}
|
||||
495
app/rendering/StudioLiteRenderer.js
Normal file
@@ -0,0 +1,495 @@
|
||||
import {
|
||||
Scene,
|
||||
PerspectiveCamera,
|
||||
PCFSoftShadowMap,
|
||||
TextureLoader,
|
||||
Clock,
|
||||
Matrix4,
|
||||
DirectionalLight,
|
||||
AmbientLight,
|
||||
Color,
|
||||
SphereGeometry,
|
||||
CylinderGeometry,
|
||||
BufferAttribute,
|
||||
BufferGeometry,
|
||||
Mesh,
|
||||
MeshPhongMaterial,
|
||||
BackSide,
|
||||
MeshBasicMaterial,
|
||||
BoxGeometry,
|
||||
DataTexture,
|
||||
SRGBColorSpace,
|
||||
AxesHelper,
|
||||
BoxHelper,
|
||||
Raycaster,
|
||||
Vector2
|
||||
} from '../thirdparty/three/three.core.js';
|
||||
import { decode } from "../thirdparty/rbxBinaryParser.js";
|
||||
import FlyCamera from '../controls/FlyCamera.js';
|
||||
import { WebGLRenderer } from '../thirdparty/three/three.module.js';
|
||||
import {
|
||||
findByClassName
|
||||
} from '../datamodel/DataModelUtils.js';
|
||||
import { parseMesh } from './mesh/MeshParser.js';
|
||||
import {
|
||||
tryToFetch
|
||||
} from '../http/TryToFetch.js';
|
||||
import AssetManager from '../http/AssetManager.js';
|
||||
|
||||
const decalSides = ["Right", "Left", "Top", "Bottom", "Back", "Front"];
|
||||
export const partClasses = ["Part", "SpawnLocation", "WedgePart", "Seat", "VehicleSeat"];
|
||||
|
||||
export class StudioLiteRenderer {
|
||||
constructor(conf) {
|
||||
this.conf = conf;
|
||||
this.queue = {
|
||||
total: 0,
|
||||
completed: 0
|
||||
}
|
||||
this.texturesOfFailedMeshes = {};
|
||||
this.imagePlaceholder = false;
|
||||
this.mAssetManager = new AssetManager({
|
||||
sharedFunctions: {
|
||||
print: this.conf.sharedFunctions.print
|
||||
}
|
||||
});
|
||||
this.scene = new Scene();
|
||||
this.camera = new PerspectiveCamera(75, this.conf.width/this.conf.height, 0.1, 15000);
|
||||
this.renderer = new WebGLRenderer({
|
||||
antialias: true,
|
||||
alpha: true
|
||||
});
|
||||
this.renderer.autoClear = false;
|
||||
this.renderer.setSize(this.conf.width, this.conf.height);
|
||||
this.renderer.shadowMap.enabled = true;
|
||||
this.renderer.shadowMap.style = PCFSoftShadowMap;
|
||||
this.domElement = this.renderer.domElement;
|
||||
this.loader = new TextureLoader();
|
||||
this.loader.crossOrigin = "Anonymous";
|
||||
this.clock = new Clock();
|
||||
let sun = new DirectionalLight(new Color(1, 1, 1), 1);
|
||||
sun.position.set(0, 10, 1);
|
||||
this.scene.add(sun);
|
||||
let light = new AmbientLight(new Color(1, 1, 1), .5);
|
||||
this.scene.add(light);
|
||||
this.controls = new FlyCamera(this.camera, this.domElement);
|
||||
this.setSkybox({
|
||||
SkyboxBk: "rbxasset://sky/null_plainsky512_bk.jpg",
|
||||
SkyboxDn: "rbxasset://sky/null_plainsky512_dn.jpg",
|
||||
SkyboxFt: "rbxasset://sky/null_plainsky512_ft.jpg",
|
||||
SkyboxLf: "rbxasset://sky/null_plainsky512_lf.jpg",
|
||||
SkyboxRt: "rbxasset://sky/null_plainsky512_rt.jpg",
|
||||
SkyboxUp: "rbxasset://sky/null_plainsky512_up.jpg"
|
||||
});
|
||||
this.axesHelper = new AxesHelper(20);
|
||||
this.scene.add(this.axesHelper);
|
||||
this.raycaster = new Raycaster();
|
||||
this.threeids = [];
|
||||
this.pointer = new Vector2();
|
||||
this.useSelectionBox = false;
|
||||
this.useSelectionBox2 = false;
|
||||
this.domElement.addEventListener("mousemove", (event) => {
|
||||
if (!this.useSelectionBox2) return;
|
||||
const rect = event.target.getBoundingClientRect();
|
||||
this.pointer.x = ( (event.clientX - rect.left) / this.conf.width ) * 2 - 1;
|
||||
this.pointer.y = - ( (event.clientY - rect.top) / this.conf.height ) * 2 + 1;
|
||||
});
|
||||
this.noWorkspace = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.controls) this.controls.update(this.clock.getDelta());
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
if (this.useSelectionBox2) {
|
||||
this.raycaster.setFromCamera(this.pointer, this.camera);
|
||||
const intersects = this.raycaster.intersectObjects(this.scene.children, false);
|
||||
if (intersects.length > 0) {
|
||||
this.updateSelectionBox2(intersects[0].object);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderImage(width = this.conf.width, height = this.conf.height) {
|
||||
if (this.selectionBox) this.scene.remove(this.selectionBox);
|
||||
if (this.selectionBox2) this.scene.remove(this.selectionBox2);
|
||||
this.renderer.setSize(width, height);
|
||||
this.camera.aspect = width / height;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
const result = this.domElement.toDataURL();
|
||||
this.resize();
|
||||
if (this.selectionBox) this.scene.add(this.selectionBox);
|
||||
if (this.selectionBox2) this.scene.add(this.selectionBox2);
|
||||
return result;
|
||||
}
|
||||
|
||||
setAnimationLoop(cb) {
|
||||
this.renderer.setAnimationLoop(cb);
|
||||
}
|
||||
|
||||
resize() {
|
||||
this.renderer.setSize(this.conf.width, this.conf.height);
|
||||
this.camera.aspect = this.conf.width / this.conf.height;
|
||||
this.camera.updateProjectionMatrix();
|
||||
}
|
||||
|
||||
toggleAxes(axes) {
|
||||
if (axes) {
|
||||
this.scene.add(this.axesHelper);
|
||||
} else {
|
||||
this.scene.remove(this.axesHelper);
|
||||
}
|
||||
}
|
||||
|
||||
setAxesPosition(x, y, z) {
|
||||
this.axesHelper.position.set(x, y, z);
|
||||
}
|
||||
|
||||
zoomTo(cameraFrame, rotate = true) {
|
||||
this.camera.position.set(cameraFrame.Position.X, cameraFrame.Position.Y, cameraFrame.Position.Z);
|
||||
if (rotate) this.camera.setRotationFromMatrix(new Matrix4().fromArray([
|
||||
cameraFrame.Components[3], cameraFrame.Components[6], cameraFrame.Components[9], 0,
|
||||
cameraFrame.Components[4], cameraFrame.Components[7], cameraFrame.Components[10], 0,
|
||||
cameraFrame.Components[5], cameraFrame.Components[8], cameraFrame.Components[11], 0,
|
||||
0, 0, 0, 1
|
||||
]));
|
||||
}
|
||||
|
||||
updateSelectionBox(threeid) {
|
||||
if (!this.useSelectionBox) return;
|
||||
if (threeid !== 1) {
|
||||
const mesh = this.scene.getObjectById(threeid);
|
||||
if (this.selectionBox) {
|
||||
this.selectionBox.setFromObject(mesh);
|
||||
} else {
|
||||
this.selectionBox = new BoxHelper(mesh, new Color(1, 1, 1));
|
||||
this.scene.add(this.selectionBox);
|
||||
this.conf.sharedFunctions.treeRefresh2();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectionBox2(mesh) {
|
||||
if (!this.useSelectionBox2) return;
|
||||
if (this.selectionBox2) {
|
||||
if (mesh !== this.skybox) this.selectionBox2.setFromObject(mesh);
|
||||
} else {
|
||||
this.selectionBox2 = new BoxHelper(mesh, new Color(1, 1, 1));
|
||||
this.scene.add(this.selectionBox2);
|
||||
this.conf.sharedFunctions.treeRefresh2();
|
||||
}
|
||||
}
|
||||
|
||||
removeSelectionBoxesIfNeeded() {
|
||||
if (!this.useSelectionBox && this.selectionBox) {
|
||||
this.scene.remove(this.selectionBox);
|
||||
this.selectionBox.dispose();
|
||||
this.selectionBox = null;
|
||||
}
|
||||
if (!this.useSelectionBox2 && this.selectionBox2) {
|
||||
this.scene.remove(this.selectionBox2);
|
||||
this.selectionBox2.dispose();
|
||||
this.selectionBox2 = null;
|
||||
}
|
||||
this.conf.sharedFunctions.treeRefresh2();
|
||||
}
|
||||
|
||||
showSkybox(show) {
|
||||
if (show)
|
||||
this.scene.add(this.skybox);
|
||||
else
|
||||
this.scene.remove(this.skybox);
|
||||
}
|
||||
|
||||
getThreeId(id) {
|
||||
return this.threeids[id];
|
||||
}
|
||||
|
||||
async setSkybox(sky) {
|
||||
if (this.skybox) this.scene.remove(this.skybox);
|
||||
this.skybox = null
|
||||
var material = []
|
||||
material[0] = await this.mAssetManager.parseAssetPath(sky.SkyboxFt);
|
||||
material[1] = await this.mAssetManager.parseAssetPath(sky.SkyboxBk);
|
||||
material[2] = await this.mAssetManager.parseAssetPath(sky.SkyboxUp);
|
||||
material[3] = await this.mAssetManager.parseAssetPath(sky.SkyboxDn);
|
||||
material[4] = await this.mAssetManager.parseAssetPath(sky.SkyboxRt);
|
||||
material[5] = await this.mAssetManager.parseAssetPath(sky.SkyboxLf);
|
||||
for (var i = 0; i < material.length; i++) {
|
||||
material[i] = new MeshBasicMaterial({map: this.loadColorTexture(material[i])})
|
||||
material[i].side = BackSide
|
||||
}
|
||||
var geometry = new BoxGeometry(10000, 10000, 10000)
|
||||
this.skybox = new Mesh(geometry, material)
|
||||
this.skybox.position.set(0, 0, 0)
|
||||
this.skybox.name = "Skybox";
|
||||
this.scene.add(this.skybox)
|
||||
console.log("skybox added")
|
||||
this.conf.sharedFunctions.treeRefresh2();
|
||||
}
|
||||
|
||||
queueOperation(cb) {
|
||||
(async () => {
|
||||
this.queue.total++;
|
||||
await cb();
|
||||
this.queue.completed++;
|
||||
})();
|
||||
}
|
||||
|
||||
get queueSize() {
|
||||
return this.queue.total - this.queue.completed;
|
||||
}
|
||||
|
||||
async renderPart(part, forceNoMesh = false, mappedid) {
|
||||
const transparent = part.Transparency > 0;
|
||||
let geometry, material, decals = [];
|
||||
let offset = {x: 0, y: 0, z: 0};
|
||||
let scale = {x: 1, y: 1, z: 1};
|
||||
|
||||
// Look for mesh data
|
||||
const mesh = findByClassName(part.Children, "SpecialMesh");
|
||||
if (typeof mesh !== "object" && part.ClassName === "MeshPart") mesh = part;
|
||||
|
||||
// Decide part color
|
||||
let color = new Color(1, 1, 1);
|
||||
if (part.Color3) {
|
||||
color = new Color(part.Color3.R, part.Color3.G, part.Color3.B);
|
||||
} else if (part.BrickColor) {
|
||||
color = new Color(part.BrickColor.Color.R/255, part.BrickColor.Color.G/255, part.BrickColor.Color.B/255)
|
||||
}
|
||||
|
||||
// Give meshes special treatment
|
||||
const blockMesh = mesh && mesh !== part ? mesh : findByClassName(part.Children, "BlockMesh");
|
||||
if (blockMesh) {
|
||||
scale = {
|
||||
x: blockMesh.Scale.X,
|
||||
y: blockMesh.Scale.Y,
|
||||
z: blockMesh.Scale.Z
|
||||
};
|
||||
// offset stays y-only for now. offset coordinates are somehow relative to rotation? tf??
|
||||
// to see what I mean, uncomment X and Z, then open City.rbxl from the examples.
|
||||
// Look at the stop signs. whaat? I ain't figuring this one out.
|
||||
offset = {
|
||||
x: 0,//blockMesh.Offset.X,
|
||||
y: blockMesh.Offset.Y,
|
||||
z: 0,//blockMesh.Offset.Z
|
||||
};
|
||||
}
|
||||
|
||||
// If ball or cylinder, decide geometry
|
||||
if (part.Shape == "Ball") {
|
||||
geometry = new SphereGeometry(part.Size.X / 2, 10, 10)
|
||||
} else if (part.Shape === "Cylinder" || (mesh && mesh.MeshType === "Head")) {
|
||||
let x, y, z;
|
||||
if (part.Shape === "Cylinder") {
|
||||
x = part.Size.X;
|
||||
y = part.Size.Y;
|
||||
z = part.Size.Z;
|
||||
} else {
|
||||
x = part.Size.Y;
|
||||
y = part.Size.X;
|
||||
z = part.Size.Z;
|
||||
}
|
||||
const cylinderRadius = Math.min(y, z) / 2;
|
||||
geometry = new CylinderGeometry(cylinderRadius, cylinderRadius, x, 16, 16);
|
||||
} else if (!forceNoMesh && mesh && (mesh.MeshType === "FileMesh" || part.ClassName === "MeshPart")) {
|
||||
// Look, a mesh! Let's queue that so it doesn't block place loading.
|
||||
this.queueOperation(async () => {
|
||||
try {
|
||||
var meshData = await this.getMesh(mesh.MeshId, this.mAssetManager.getAssetId(mesh.TextureId));
|
||||
var { positions, normal, uv } = await parseMesh(meshData, mesh.MeshId);
|
||||
const geometry = new BufferGeometry();
|
||||
const positionNumComponents = 3;
|
||||
const normalNumComponents = 3;
|
||||
const uvNumComponents = 3;
|
||||
geometry.setAttribute(
|
||||
"position",
|
||||
new BufferAttribute(
|
||||
new Float32Array(positions),
|
||||
positionNumComponents
|
||||
)
|
||||
);
|
||||
geometry.setAttribute(
|
||||
"normal",
|
||||
new BufferAttribute(new Float32Array(normal), normalNumComponents)
|
||||
);
|
||||
geometry.setAttribute(
|
||||
"uv",
|
||||
new BufferAttribute(new Float32Array(uv), uvNumComponents)
|
||||
);
|
||||
geometry.computeBoundingBox();
|
||||
let box = geometry.boundingBox;
|
||||
let x = box.max.x - box.min.x;
|
||||
let y = box.max.y - box.min.y;
|
||||
let z = box.max.z - box.min.z;
|
||||
geometry.scale(part.Size.X / x, part.Size.Y / y, part.Size.Z / z);
|
||||
|
||||
material = new MeshPhongMaterial( {
|
||||
map: this.loadColorTexture(await this.mAssetManager.parseAssetPath(mesh.TextureId)),
|
||||
transparent: transparent,
|
||||
opacity: part.transparency*-1+1,
|
||||
specular: 0x222222
|
||||
} )
|
||||
|
||||
let cube = new Mesh(geometry, material);
|
||||
cube.receiveShadow = true
|
||||
cube.castShadow = true
|
||||
cube.name = part.Name;
|
||||
cube.position.set(part.CFrame.Position.X, part.CFrame.Position.Y, part.CFrame.Position.Z);
|
||||
cube.setRotationFromMatrix(new Matrix4().fromArray([
|
||||
part.CFrame.Components[3], part.CFrame.Components[6], part.CFrame.Components[9], 0,
|
||||
part.CFrame.Components[4], part.CFrame.Components[7], part.CFrame.Components[10], 0,
|
||||
part.CFrame.Components[5], part.CFrame.Components[8], part.CFrame.Components[11], 0,
|
||||
0, 0, 0, 1
|
||||
]));
|
||||
this.scene.add(cube);
|
||||
if (mappedid) this.threeids[mappedid] = cube.id;
|
||||
// That didn't work! Let's load the part meshless instead.
|
||||
} catch (e) {
|
||||
// forceNoMesh = true skips the mesh code, so this part will follow the regular code path.
|
||||
this.renderPart(part, true);
|
||||
}
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
geometry = new BoxGeometry( part.Size.X * scale.x, part.Size.Y * scale.y, part.Size.Z * scale.z )
|
||||
}
|
||||
|
||||
// Render wedgepart
|
||||
if (part.ClassName === "WedgePart") {
|
||||
let pos = geometry.attributes.position;
|
||||
for(let i = 0; i < pos.count; i++){
|
||||
if (pos.getZ(i) < 0 && pos.getY(i) > 0) pos.setY(i, (part.Size.Y / 2) * -1); // change Y-coord by condition
|
||||
}
|
||||
geometry.computeVertexNormals(); // don't forget to re-compute normals
|
||||
}
|
||||
|
||||
// Enumerate decals
|
||||
for (let child of part.Children) {
|
||||
if (child.ClassName === "Decal" || child.ClassName === "Texture") {
|
||||
decals.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
// Stage two. We will queue this if there are decals, to not block.
|
||||
const renderPartStageTwo = async () => {
|
||||
if (decals.length > 0) {
|
||||
// Build a decal material.
|
||||
material = [null, null, null, null, null, null];
|
||||
let index;
|
||||
for (let decal of decals) {
|
||||
index = decalSides.indexOf(decal.Face);
|
||||
if (index !== -1) {
|
||||
material[index] = new MeshPhongMaterial({
|
||||
map: this.loadColorTexture(await this.mAssetManager.parseAssetPath(decal.Texture)),
|
||||
transparent: decal.Transparency > 0,
|
||||
opacity: decal.Transparency*-1+1,
|
||||
specular: 0x222222
|
||||
});
|
||||
}
|
||||
}
|
||||
for (let i in material) {
|
||||
if (material[i] === null) material[i] = new MeshPhongMaterial({
|
||||
color: color,
|
||||
transparent: transparent,
|
||||
opacity: part.Transparency*-1+1,
|
||||
specular: 0x222222
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// No decals? Just one material, then.
|
||||
material = new MeshPhongMaterial( {
|
||||
color: color,
|
||||
transparent: transparent,
|
||||
opacity: part.Transparency*-1+1,
|
||||
specular: 0x222222
|
||||
} )
|
||||
}
|
||||
|
||||
// Create the mesh for our part.
|
||||
var cube = new Mesh( geometry, material )
|
||||
cube.receiveShadow = true
|
||||
cube.castShadow = true
|
||||
cube.name = part.Name;
|
||||
cube.position.set(part.CFrame.Position.X + offset.x, part.CFrame.Position.Y + offset.y, part.CFrame.Position.Z + offset.z);
|
||||
// Degrees aren't enough! We need to set the exact rotation matrix.
|
||||
// If we use just CFrame.Orientation, it will lead to incorrect placements => destroyed buildings.
|
||||
cube.setRotationFromMatrix(new Matrix4().fromArray([
|
||||
part.CFrame.Components[3], part.CFrame.Components[6], part.CFrame.Components[9], 0,
|
||||
part.CFrame.Components[4], part.CFrame.Components[7], part.CFrame.Components[10], 0,
|
||||
part.CFrame.Components[5], part.CFrame.Components[8], part.CFrame.Components[11], 0,
|
||||
0, 0, 0, 1
|
||||
]));
|
||||
// This is needed for cylinders to rotate correctly. No idea why.
|
||||
if (part.Shape === "Cylinder") cube.rotation.z += Math.PI / 2;
|
||||
//cube.rotation.set(MathUtils.degToRad(part.CFrame.Orientation.X), MathUtils.degToRad(part.CFrame.Orientation.Y), MathUtils.degToRad(part.CFrame.Orientation.Z));
|
||||
this.scene.add( cube )
|
||||
if (mappedid) this.threeids[mappedid] = cube.id;
|
||||
};
|
||||
|
||||
// If there are no assets to load, render the part synchronously.
|
||||
// If there are assets, move rendering to the background,
|
||||
// so it doesn't block everything else if the user's connection is slow.
|
||||
// (it is just a HEAD request, tho...)
|
||||
if (decals.length > 0) {
|
||||
this.queueOperation(renderPartStageTwo);
|
||||
} else await renderPartStageTwo();
|
||||
}
|
||||
|
||||
async traverse(instance, treeData, render = false) {
|
||||
for (let child of instance.Children) {
|
||||
if (child) {
|
||||
let item = {'text': child.Name, 'icon': `content/icons/${child.ClassName}.png`, 'children': [], 'instance': child, 'id': this.threeids.push(1) - 1};
|
||||
treeData.push(item);
|
||||
if (render && partClasses.indexOf(child.ClassName) !== -1) {
|
||||
await this.renderPart(child, false, item.id);
|
||||
}
|
||||
await this.traverse(child, item.children, render ? true : child.ClassName === "Workspace" || this.noWorkspace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadColorTexture( path ) {
|
||||
if (path.trim().length === 0) return new DataTexture(new Uint8Array(4), 1, 1);
|
||||
this.conf.sharedFunctions.print("Load texture " + new URL(path, window.location.href));
|
||||
const texture = this.loader.load( path );
|
||||
texture.colorSpace = SRGBColorSpace;
|
||||
return texture;
|
||||
}
|
||||
|
||||
async getMesh(mesh, texture) {
|
||||
const path = await this.mAssetManager.parseAssetPath(mesh, "mesh", texture);
|
||||
if (path.trim().length === 0) throw new Error();
|
||||
var d = await fetch(path);
|
||||
var data = await d.arrayBuffer();
|
||||
return data;
|
||||
}
|
||||
|
||||
async loadPlace(ab) {
|
||||
this.data = decode(ab);
|
||||
const workspace = findByClassName(this.data, "Workspace");
|
||||
if (workspace) this.noWorkspace = false; else this.noWorkspace = true;
|
||||
try {
|
||||
const robloxCamera = findByClassName((this.noWorkspace ? findByClassName(this.data, "Model") : workspace).Children, "Camera");
|
||||
const cameraFrame = robloxCamera.CFrame || robloxCamera.CoordinateFrame;
|
||||
this.zoomTo(cameraFrame);
|
||||
} catch (ignored) {
|
||||
this.conf.sharedFunctions.print("Could not determine camera position!");
|
||||
}
|
||||
this.queueOperation(async () => {
|
||||
try {
|
||||
const sky = findByClassName(findByClassName(this.data, "Lighting").Children, "Sky");
|
||||
if (!sky) throw new Error();
|
||||
try {
|
||||
for (const key of ["Bk", "Dn", "Ft", "Lf", "Rt", "Up"]) {
|
||||
if (await tryToFetch(await this.mAssetManager.parseAssetPath(sky["Skybox" + key]))) {} else { throw new Error(); }
|
||||
}
|
||||
await this.setSkybox(sky);
|
||||
} catch (ignored) {
|
||||
this.conf.sharedFunctions.print("Could not load sky!");
|
||||
}
|
||||
} catch (ignored) {};
|
||||
});
|
||||
await this.traverse({"Children": this.data}, this.conf.sharedObjects.jsTreeData);
|
||||
}
|
||||
}
|
||||
153
app/rendering/mesh/MeshParser.js
Normal file
@@ -0,0 +1,153 @@
|
||||
function parse1xMesh(MESHDATA, is10) {
|
||||
var vectors = MESHDATA.replace(/]/g, "").split("[");
|
||||
var h = vectors.shift();
|
||||
var positions = [];
|
||||
var normal = [];
|
||||
var uv = [];
|
||||
// 1.00 assets are scaled up by 2x in the file-
|
||||
var offset = is10 ? 0.5 : 1;
|
||||
|
||||
function toVector(vstring, offst) {
|
||||
var a = [];
|
||||
for (var i of vstring.split(",")) {
|
||||
a.push(parseFloat(i) * offst);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
for (var i = 0; i < vectors.length; i += 3) {
|
||||
positions.push(...toVector(vectors[i], offset));
|
||||
normal.push(...toVector(vectors[i + 1], 1));
|
||||
uv.push(...toVector(vectors[i + 2], 1));
|
||||
}
|
||||
console.log(
|
||||
`Parsed version 1 mesh\n\nDetails:\nVectors: ${vectors.length} (/ 3 = ${
|
||||
vectors.length / 3
|
||||
})\n\nHeader:`,
|
||||
h
|
||||
);
|
||||
vectors = null;
|
||||
return {
|
||||
positions,
|
||||
normal,
|
||||
uv
|
||||
};
|
||||
}
|
||||
|
||||
function parse2x3xMesh(dv) {
|
||||
var headerStart = 13;
|
||||
var MeshHeader = {
|
||||
sizeof_MeshHeader: dv.getUint16(headerStart, true),
|
||||
sizeof_Vertex: dv.getUint8(headerStart + 2, true),
|
||||
sizeof_Face: dv.getUint8(headerStart + 3, true),
|
||||
numVerts: dv.getUint32(headerStart + 4, true),
|
||||
numFaces: dv.getUint32(headerStart + 8, true)
|
||||
};
|
||||
if (MeshHeader.sizeof_MeshHeader > 12) {
|
||||
// v3 header
|
||||
MeshHeader.sizeof_LOD = dv.getUint16(headerStart + 4, true);
|
||||
MeshHeader.numLODs = dv.getUint16(headerStart + 6, true);
|
||||
MeshHeader.numVerts = dv.getUint16(headerStart + 8, true);
|
||||
MeshHeader.numFaces = dv.getUint16(headerStart + 12, true);
|
||||
}
|
||||
console.log("Parsing version 2/3 mesh\n\nDetails:\n" + JSON.stringify(MeshHeader));
|
||||
var i = headerStart + MeshHeader.sizeof_MeshHeader;
|
||||
var verticies = [];
|
||||
var verticiesEnd =
|
||||
headerStart +
|
||||
MeshHeader.sizeof_MeshHeader +
|
||||
MeshHeader.numVerts * MeshHeader.sizeof_Vertex;
|
||||
while (i < verticiesEnd) {
|
||||
var vertex = {
|
||||
px: dv.getFloat32(i, true),
|
||||
py: dv.getFloat32(i + 4, true),
|
||||
pz: dv.getFloat32(i + 8, true),
|
||||
nx: dv.getFloat32(i + 12, true),
|
||||
ny: dv.getFloat32(i + 16, true),
|
||||
nz: dv.getFloat32(i + 20, true),
|
||||
u: dv.getFloat32(i + 24, true),
|
||||
v: dv.getFloat32(i + 28, true),
|
||||
w: dv.getFloat32(i + 32, true),
|
||||
r: 255,
|
||||
g: 255,
|
||||
b: 255,
|
||||
a: 255
|
||||
};
|
||||
if (MeshHeader.sizeof_Vertex >= 40) {
|
||||
vertex.r = dv.getUint8(i + 36, true);
|
||||
vertex.g = dv.getUint8(i + 37, true);
|
||||
vertex.b = dv.getUint8(i + 38, true);
|
||||
vertex.a = dv.getUint8(i + 39, true);
|
||||
}
|
||||
if (MeshHeader.sizeof_MeshHeader > 12) vertex.u = vertex.u;
|
||||
verticies.push(vertex);
|
||||
i += MeshHeader.sizeof_Vertex;
|
||||
}
|
||||
|
||||
var faces = [];
|
||||
var facesEnd = verticiesEnd + MeshHeader.numFaces * MeshHeader.sizeof_Face;
|
||||
while (i < facesEnd) {
|
||||
faces.push({
|
||||
a: dv.getUint32(i, true),
|
||||
b: dv.getUint32(i + 4, true),
|
||||
c: dv.getUint32(i + 8, true)
|
||||
});
|
||||
i += MeshHeader.sizeof_Face;
|
||||
}
|
||||
|
||||
var LODs = [];
|
||||
if (MeshHeader.sizeof_MeshHeader > 12) {
|
||||
var lodsEnd = facesEnd + MeshHeader.numLODs * MeshHeader.sizeof_LOD;
|
||||
while (i < lodsEnd) {
|
||||
LODs.push(dv.getUint32(i, true));
|
||||
i += MeshHeader.sizeof_LOD;
|
||||
}
|
||||
}
|
||||
|
||||
console.log({
|
||||
MeshHeader,
|
||||
verticies,
|
||||
faces,
|
||||
LODs
|
||||
});
|
||||
|
||||
var positions = [];
|
||||
var normal = [];
|
||||
var uv = [];
|
||||
|
||||
for (var faceIdx in faces) {
|
||||
if (LODs.length > 1 && faceIdx > LODs[1]) break;
|
||||
var face = faces[faceIdx];
|
||||
for (var i in face) {
|
||||
var vertex = verticies[face[i]];
|
||||
positions.push(vertex.px, vertex.py, vertex.pz);
|
||||
normal.push(vertex.nx, vertex.ny, vertex.nz);
|
||||
uv.push(vertex.u, 1 - vertex.v, vertex.w);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
positions,
|
||||
normal,
|
||||
uv
|
||||
};
|
||||
}
|
||||
|
||||
let meshCache = {};
|
||||
export function parseMesh(data, meshId) {
|
||||
if (meshCache[meshId]) return meshCache[meshId];
|
||||
let returnValue;
|
||||
var stringData = new TextDecoder().decode(data);
|
||||
if (stringData.startsWith("version 1.0")) {
|
||||
returnValue = parse1xMesh(stringData, stringData.startsWith("version 1.0"));
|
||||
} else if (
|
||||
stringData.startsWith("version 2.0") ||
|
||||
stringData.startsWith("version 3.0")
|
||||
) {
|
||||
console.log("Parsing v2/3 mesh, header: ", stringData.substring(0, 12));
|
||||
returnValue = parse2x3xMesh(new DataView(data));
|
||||
} else {
|
||||
console.log("unsupported mesh " + stringData.split("\n")[0]);
|
||||
}
|
||||
meshCache[meshId] = returnValue;
|
||||
return returnValue;
|
||||
}
|
||||
4
app/thirdparty/7.css
vendored
Normal file
BIN
app/thirdparty/jstree/32px.png
vendored
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
5
app/thirdparty/jstree/jquery.min.js
vendored
Normal file
5
app/thirdparty/jstree/jstree.min.js
vendored
Normal file
1
app/thirdparty/jstree/style.min.css
vendored
Normal file
BIN
app/thirdparty/jstree/throbber.gif
vendored
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
app/thirdparty/jstree/throbber_old.gif
vendored
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
1
app/thirdparty/rbxBinaryParser.js
vendored
Normal file
167
app/thirdparty/three/stats.module.js
vendored
Normal file
@@ -0,0 +1,167 @@
|
||||
var Stats = function () {
|
||||
|
||||
var mode = 0;
|
||||
|
||||
var container = document.createElement( 'div' );
|
||||
container.style.cssText = 'position:fixed;top:0;left:0;cursor:pointer;opacity:0.9;z-index:10000';
|
||||
container.addEventListener( 'click', function ( event ) {
|
||||
|
||||
event.preventDefault();
|
||||
showPanel( ++ mode % container.children.length );
|
||||
|
||||
}, false );
|
||||
|
||||
//
|
||||
|
||||
function addPanel( panel ) {
|
||||
|
||||
container.appendChild( panel.dom );
|
||||
return panel;
|
||||
|
||||
}
|
||||
|
||||
function showPanel( id ) {
|
||||
|
||||
for ( var i = 0; i < container.children.length; i ++ ) {
|
||||
|
||||
container.children[ i ].style.display = i === id ? 'block' : 'none';
|
||||
|
||||
}
|
||||
|
||||
mode = id;
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
var beginTime = ( performance || Date ).now(), prevTime = beginTime, frames = 0;
|
||||
|
||||
var fpsPanel = addPanel( new Stats.Panel( 'FPS', '#0ff', '#002' ) );
|
||||
var msPanel = addPanel( new Stats.Panel( 'MS', '#0f0', '#020' ) );
|
||||
|
||||
if ( self.performance && self.performance.memory ) {
|
||||
|
||||
var memPanel = addPanel( new Stats.Panel( 'MB', '#f08', '#201' ) );
|
||||
|
||||
}
|
||||
|
||||
showPanel( 0 );
|
||||
|
||||
return {
|
||||
|
||||
REVISION: 16,
|
||||
|
||||
dom: container,
|
||||
|
||||
addPanel: addPanel,
|
||||
showPanel: showPanel,
|
||||
|
||||
begin: function () {
|
||||
|
||||
beginTime = ( performance || Date ).now();
|
||||
|
||||
},
|
||||
|
||||
end: function () {
|
||||
|
||||
frames ++;
|
||||
|
||||
var time = ( performance || Date ).now();
|
||||
|
||||
msPanel.update( time - beginTime, 200 );
|
||||
|
||||
if ( time >= prevTime + 1000 ) {
|
||||
|
||||
fpsPanel.update( ( frames * 1000 ) / ( time - prevTime ), 100 );
|
||||
|
||||
prevTime = time;
|
||||
frames = 0;
|
||||
|
||||
if ( memPanel ) {
|
||||
|
||||
var memory = performance.memory;
|
||||
memPanel.update( memory.usedJSHeapSize / 1048576, memory.jsHeapSizeLimit / 1048576 );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return time;
|
||||
|
||||
},
|
||||
|
||||
update: function () {
|
||||
|
||||
beginTime = this.end();
|
||||
|
||||
},
|
||||
|
||||
// Backwards Compatibility
|
||||
|
||||
domElement: container,
|
||||
setMode: showPanel
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
Stats.Panel = function ( name, fg, bg ) {
|
||||
|
||||
var min = Infinity, max = 0, round = Math.round;
|
||||
var PR = round( window.devicePixelRatio || 1 );
|
||||
|
||||
var WIDTH = 80 * PR, HEIGHT = 48 * PR,
|
||||
TEXT_X = 3 * PR, TEXT_Y = 2 * PR,
|
||||
GRAPH_X = 3 * PR, GRAPH_Y = 15 * PR,
|
||||
GRAPH_WIDTH = 74 * PR, GRAPH_HEIGHT = 30 * PR;
|
||||
|
||||
var canvas = document.createElement( 'canvas' );
|
||||
canvas.width = WIDTH;
|
||||
canvas.height = HEIGHT;
|
||||
canvas.style.cssText = 'width:80px;height:48px';
|
||||
|
||||
var context = canvas.getContext( '2d' );
|
||||
context.font = 'bold ' + ( 9 * PR ) + 'px Helvetica,Arial,sans-serif';
|
||||
context.textBaseline = 'top';
|
||||
|
||||
context.fillStyle = bg;
|
||||
context.fillRect( 0, 0, WIDTH, HEIGHT );
|
||||
|
||||
context.fillStyle = fg;
|
||||
context.fillText( name, TEXT_X, TEXT_Y );
|
||||
context.fillRect( GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT );
|
||||
|
||||
context.fillStyle = bg;
|
||||
context.globalAlpha = 0.9;
|
||||
context.fillRect( GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT );
|
||||
|
||||
return {
|
||||
|
||||
dom: canvas,
|
||||
|
||||
update: function ( value, maxValue ) {
|
||||
|
||||
min = Math.min( min, value );
|
||||
max = Math.max( max, value );
|
||||
|
||||
context.fillStyle = bg;
|
||||
context.globalAlpha = 1;
|
||||
context.fillRect( 0, 0, WIDTH, GRAPH_Y );
|
||||
context.fillStyle = fg;
|
||||
context.fillText( round( value ) + ' ' + name + ' (' + round( min ) + '-' + round( max ) + ')', TEXT_X, TEXT_Y );
|
||||
|
||||
context.drawImage( canvas, GRAPH_X + PR, GRAPH_Y, GRAPH_WIDTH - PR, GRAPH_HEIGHT, GRAPH_X, GRAPH_Y, GRAPH_WIDTH - PR, GRAPH_HEIGHT );
|
||||
|
||||
context.fillRect( GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, GRAPH_HEIGHT );
|
||||
|
||||
context.fillStyle = bg;
|
||||
context.globalAlpha = 0.9;
|
||||
context.fillRect( GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, round( ( 1 - ( value / maxValue ) ) * GRAPH_HEIGHT ) );
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
export default Stats;
|
||||
57986
app/thirdparty/three/three.core.js
vendored
Normal file
18021
app/thirdparty/three/three.module.js
vendored
Normal file
BIN
asset/10055744.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
asset/1008743.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
asset/1008744.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
asset/1008745.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
asset/1008748.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
asset/10124534.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
asset/1013849.png
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
asset/1013850.png
Normal file
|
After Width: | Height: | Size: 176 KiB |
BIN
asset/1013851.png
Normal file
|
After Width: | Height: | Size: 175 KiB |
BIN
asset/1013852.png
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
asset/1013853.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
asset/1013854.png
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
asset/101840086.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
3
asset/101840172.mesh
Normal file
BIN
asset/10470600.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
3
asset/10470609.mesh
Normal file
BIN
asset/10759411.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
asset/107706048.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
3
asset/1091940.mesh
Normal file
BIN
asset/1091942.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
3
asset/1136139.mesh
Normal file
BIN
asset/1136146.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
asset/1136349.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
asset/1136350.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
asset/1138386.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
asset/1138387.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
asset/1139750.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
asset/1139751.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
asset/1139752.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
asset/1140961.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
asset/1140964.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
asset/1143109.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
asset/1143110.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
3
asset/115289510.mesh
Normal file
3
asset/115296503.mesh
Normal file
BIN
asset/115340918.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
3
asset/115955313.mesh
Normal file
BIN
asset/115955343.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
3
asset/116439976.mesh
Normal file
BIN
asset/116440028.png
Normal file
|
After Width: | Height: | Size: 265 KiB |
BIN
asset/116620938.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
asset/116620941.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
asset/1181642.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
asset/11820196.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
3
asset/11821197.mesh
Normal file
BIN
asset/118869704.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
asset/1193159.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
asset/119364093.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
3
asset/119875584.mesh
Normal file
BIN
asset/119875721.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
3
asset/12212520.mesh
Normal file
3
asset/123115084.mesh
Normal file
BIN
asset/123115105.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
asset/1239456.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
3
asset/1290033.mesh
Normal file
3
asset/13319240.mesh
Normal file
BIN
asset/13319242.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
3
asset/13425802.mesh
Normal file
BIN
asset/13425822.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
asset/143675742.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
asset/14655345.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
3
asset/14655367.mesh
Normal file
BIN
asset/146872602.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
asset/147037195.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
3
asset/147831825.mesh
Normal file
BIN
asset/147831861.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
asset/15952494.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
3
asset/15952512.mesh
Normal file
BIN
asset/161240005.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
asset/165443860.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
asset/165853672.png
Normal file
|
After Width: | Height: | Size: 992 KiB |
BIN
asset/16606141.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
3
asset/16606212.mesh
Normal file
BIN
asset/16659355.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
3
asset/16659363.mesh
Normal file
BIN
asset/16922886.png
Normal file
|
After Width: | Height: | Size: 238 B |
BIN
asset/17230185.png
Normal file
|
After Width: | Height: | Size: 92 KiB |