alpha-1
This commit is contained in:
241
app/app.css
Normal file
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
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
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
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
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
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
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
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
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
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
4
app/thirdparty/7.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
app/thirdparty/jstree/32px.png
vendored
Normal file
BIN
app/thirdparty/jstree/32px.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
5
app/thirdparty/jstree/jquery.min.js
vendored
Normal file
5
app/thirdparty/jstree/jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
app/thirdparty/jstree/jstree.min.js
vendored
Normal file
5
app/thirdparty/jstree/jstree.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
app/thirdparty/jstree/style.min.css
vendored
Normal file
1
app/thirdparty/jstree/style.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
app/thirdparty/jstree/throbber.gif
vendored
Normal file
BIN
app/thirdparty/jstree/throbber.gif
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
BIN
app/thirdparty/jstree/throbber_old.gif
vendored
Normal file
BIN
app/thirdparty/jstree/throbber_old.gif
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
1
app/thirdparty/rbxBinaryParser.js
vendored
Normal file
1
app/thirdparty/rbxBinaryParser.js
vendored
Normal file
File diff suppressed because one or more lines are too long
167
app/thirdparty/three/stats.module.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
57986
app/thirdparty/three/three.core.js
vendored
Normal file
File diff suppressed because one or more lines are too long
18021
app/thirdparty/three/three.module.js
vendored
Normal file
18021
app/thirdparty/three/three.module.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user