This commit is contained in:
Asicosilomu
2025-06-12 17:43:22 +03:00
parent 9dac0239c0
commit 39d072aa89
823 changed files with 78751 additions and 0 deletions

241
app/app.css Normal file
View 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
View 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
View 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);
}
}

View 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 };

View 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
View 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
View 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
View 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;
}

View 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);
}
}

View 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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

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
View 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

File diff suppressed because one or more lines are too long

18021
app/thirdparty/three/three.module.js vendored Normal file

File diff suppressed because one or more lines are too long