This commit is contained in:
Robert Long 2018-08-16 14:42:55 -07:00
Родитель bb391fa799 e5265200ff
Коммит 2fd230b68e
20 изменённых файлов: 4103 добавлений и 3887 удалений

5816
package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -68,7 +68,7 @@
"react-tooltip": "^3.6.1",
"selfsigned": "^1.10.3",
"signals": "^1.0.0",
"three": "github:mozillareality/three.js#hubs/dev-v2",
"three": "github:mozillareality/three.js#09b27a6628e7dd5422db2ebfafa24d198ddb0b16",
"throttle-debounce": "^2.0.1",
"ws": "^5.2.0"
},

Просмотреть файл

@ -10,25 +10,33 @@ import SetValueCommand from "./commands/SetValueCommand";
import RemoveComponentCommand from "./commands/RemoveComponentCommand";
import SetComponentPropertyCommand from "./commands/SetComponentPropertyCommand";
import MoveObjectCommand from "./commands/MoveObjectCommand";
import { setStaticMode, StaticModes, getStaticMode, computeStaticMode } from "./StaticMode";
import {
isStatic,
setStaticMode,
StaticModes,
getStaticMode,
computeStaticMode,
computeAndSetStaticModes,
getOriginalStaticMode,
setOriginalStaticMode
} from "./StaticMode";
import { Components } from "./components";
import { types } from "./components/utils";
import SceneReferenceComponent from "./components/SceneReferenceComponent";
import SaveableComponent from "./components/SaveableComponent";
import {
loadScene,
loadSerializedScene,
serializeScene,
exportScene,
serializeFileProps,
resolveFileProps
} from "./SceneLoader";
import { last } from "../utils";
import { textureCache, gltfCache } from "./caches";
import ConflictHandler from "./ConflictHandler";
import ConflictError from "./ConflictError";
import SpokeDirectionalLightHelper from "./helpers/SpokeDirectionalLightHelper";
import SpokeHemisphereLightHelper from "./helpers/SpokeHemisphereLightHelper";
import absoluteToRelativeURL from "./utils/absoluteToRelativeURL";
import StandardMaterialComponent from "../editor/components/StandardMaterialComponent";
import ShadowComponent from "./components/ShadowComponent";
import shallowEquals from "./utils/shallowEquals";
import addChildAtIndex from "./utils/addChildAtIndex";
import SceneLoaderError from "./SceneLoaderError";
import sortEntities from "./utils/sortEntities";
/**
* @author mrdoob / http://mrdoob.com/
@ -45,13 +53,6 @@ export default class Editor {
const Signal = signals.Signal;
this.signals = {
openScene: new Signal(),
popScene: new Signal(),
extendScene: new Signal(),
savingStarted: new Signal(),
savingFinished: new Signal(),
deleteSelectedObject: new Signal(),
transformChanged: new Signal(),
@ -81,9 +82,7 @@ export default class Editor {
historyChanged: new Signal(),
fileChanged: new Signal(),
sceneErrorOccurred: new Signal()
fileChanged: new Signal()
};
this.project.addListener("change", path => {
@ -195,7 +194,7 @@ export default class Editor {
const sceneRefComponentName = SceneReferenceComponent.componentName;
const previousURI = this.getComponentProperty(this._prefabBeingEdited, sceneRefComponentName, "src");
this.updateComponentProperty(this._prefabBeingEdited, sceneRefComponentName, "src", poppedURI);
if (previousURI.endsWith(".gltf")) {
if (previousURI.endsWith(".gltf") || previousURI.endsWith(".glb")) {
const name = last(poppedURI.split("/"));
const displayName = this._conflictHandler.addToDuplicateNameCounters(name);
this._prefabBeingEdited.name = displayName;
@ -211,9 +210,10 @@ export default class Editor {
this.sceneInfo.uri = uri;
}
editScenePrefab(object, uri) {
async editScenePrefab(object, uri) {
this._prefabBeingEdited = object;
this._loadScene(uri);
const scene = await this._loadSceneFromURL(uri);
return scene;
}
_deleteSceneDependencies() {
@ -267,14 +267,14 @@ export default class Editor {
return scene;
}
openRootScene(uri) {
async openScene(uri) {
this.scenes = [];
this._clearCaches();
this._ignoreSceneModification = true;
const scene = this._loadScene(uri).then(scene => {
this._ignoreSceneModification = false;
return scene;
});
const scene = await this._loadSceneFromURL(uri);
this._ignoreSceneModification = false;
return scene;
}
@ -342,14 +342,39 @@ export default class Editor {
this.fileDependencies.set(uri, uriDependencies);
}
async _loadScene(uri) {
async createPrefabFromGLTF(gltfUrl, destUrl) {
// Load glTF and throw errors if there are conflicts
await this._loadGLTF(gltfUrl);
const prefabDef = {
entities: {},
inherits: gltfUrl
};
await this.project.writeJSON(destUrl, prefabDef);
}
async fixConflictError(error) {
if (error.type === "import") {
const originalGLTF = await this.project.readJSON(error.uri);
if (originalGLTF.nodes) {
error.handler.updateNodeNames(originalGLTF.nodes);
await this.project.writeJSON(error.uri, originalGLTF);
}
return true;
}
}
async _loadSceneFromURL(uri) {
this.deselect();
this._deleteSceneDependencies();
this._resetHelpers();
const scene = await loadScene(uri, this._addComponent, this.components, true);
const scene = await this._loadScene(uri, true);
this._conflictHandler = scene.userData._conflictHandler;
this._setSceneInfo(scene, uri);
@ -364,10 +389,227 @@ export default class Editor {
return scene;
}
async _loadGLTF(url) {
const { scene } = await gltfCache.get(url);
if (scene === undefined) {
throw new Error(`Error loading: ${url}. glTF file has no default scene.`);
}
if (!scene.name) {
scene.name = "Scene";
}
scene.userData._conflictHandler = new ConflictHandler();
scene.userData._conflictHandler.findDuplicates(scene, 0, 0);
if (scene.userData._conflictHandler.getDuplicateStatus() || scene.userData._conflictHandler.getMissingStatus()) {
throw new ConflictError("gltf naming conflicts", "import", url, scene.userData._conflictHandler);
}
return scene;
}
async _loadScene(uri, isRoot = true, ancestors) {
let scene;
const url = new URL(uri, window.location).href;
if (url.endsWith(".gltf") || url.endsWith(".glb")) {
scene = await this._loadGLTF(url);
if (isRoot) {
scene.userData._inherits = url;
}
// Inflate components
const addComponentPromises = [];
scene.traverse(async object => {
const extensions = object.userData.gltfExtensions;
if (extensions !== undefined) {
for (const extensionName in extensions) {
addComponentPromises.push(this._addComponent(object, extensionName, extensions[extensionName], true));
}
}
if (object instanceof THREE.Mesh) {
addComponentPromises.push(this._addComponent(object, "mesh", null, true));
const shadowProps = object.userData.components
? object.userData.components.shadow
: { castShadow: true, receiveShadow: true };
addComponentPromises.push(this._addComponent(object, "shadow", shadowProps, true));
if (object.material instanceof THREE.MeshStandardMaterial) {
addComponentPromises.push(this._addComponent(object, "standard-material", null, true));
}
}
});
await Promise.all(addComponentPromises);
return scene;
}
const sceneResponse = await fetch(url);
if (!sceneResponse.ok) {
const error = new SceneLoaderError("Error loading .scene", url, "damaged", null);
throw error;
}
const sceneDef = await sceneResponse.json();
if (isRoot) {
ancestors = [];
}
scene = await this._loadSerializedScene(sceneDef, uri, isRoot, ancestors);
scene.userData._ancestors = ancestors;
return scene;
}
_resolveFileProps(component, props, basePath) {
const clonedProps = Object.assign({}, props);
for (const { name, type } of component.schema) {
if (type === types.file && clonedProps[name]) {
clonedProps[name] = new URL(clonedProps[name], basePath).href;
}
}
return clonedProps;
}
async _loadSerializedScene(sceneDef, baseURI, isRoot = true, ancestors) {
let scene;
const { inherits, root, entities } = sceneDef;
const absoluteBaseURL = new URL(baseURI, window.location);
if (inherits) {
const inheritedSceneURL = new URL(inherits, absoluteBaseURL).href;
scene = await this._loadScene(inheritedSceneURL, false, ancestors);
if (ancestors) {
ancestors.push(inheritedSceneURL);
}
if (isRoot) {
scene.userData._inherits = inheritedSceneURL;
}
} else if (root) {
scene = new THREE.Scene();
scene.name = root;
} else {
throw new Error("Invalid Scene: Scene does not inherit from another scene or have a root entity.");
}
// init scene conflict status
if (!scene.userData._conflictHandler) {
scene.userData._conflictHandler = new ConflictHandler();
scene.userData._conflictHandler.findDuplicates(scene, 0, 0);
}
if (entities) {
// Sort entities by insertion order (uses parent and index to determine order).
const sortedEntities = sortEntities(entities);
const entityComponentPromises = [];
for (const entityName of sortedEntities) {
const entity = entities[entityName];
// Find or create the entity's Object3D
let entityObj = scene.getObjectByName(entityName);
if (entityObj === undefined) {
entityObj = new THREE.Object3D();
entityObj.name = entityName;
}
// Entities defined in the root scene should be saved.
if (isRoot) {
entityObj.userData._saveEntity = true;
}
// Attach the entity to its parent.
// An entity doesn't have a parent defined if the entity is loaded in an inherited scene.
if (entity.parent) {
let parentObject = scene.getObjectByName(entity.parent);
if (!parentObject) {
// parent node got renamed or deleted
parentObject = new THREE.Object3D();
parentObject.name = entity.parent;
parentObject.userData._isMissingRoot = true;
parentObject.userData._missing = true;
scene.userData._conflictHandler.setMissingStatus(true);
scene.add(parentObject);
} else {
if (!parentObject.userData._missing) {
parentObject.userData._isMissingRoot = false;
parentObject.userData._missing = false;
}
}
entityObj.userData._missing = parentObject.userData._missing;
entityObj.userData._duplicate = parentObject.userData._duplicate;
addChildAtIndex(parentObject, entityObj, entity.index);
// Parents defined in the root scene should be saved.
if (isRoot) {
entityObj.userData._saveParent = true;
}
}
// Inflate the entity's components.
if (Array.isArray(entity.components)) {
for (const componentDef of entity.components) {
if (componentDef.src) {
// Process SaveableComponent
componentDef.src = new URL(componentDef.src, absoluteBaseURL.href).href;
const resp = await fetch(componentDef.src);
let json = {};
if (resp.ok) {
json = await resp.json();
}
const props = this._resolveFileProps(this.components.get(componentDef.name), json, componentDef.src);
entityComponentPromises.push(
this._addComponent(entityObj, componentDef.name, props, !isRoot).then(component => {
component.src = componentDef.src;
component.srcIsValid = resp.ok;
})
);
} else {
const props = this._resolveFileProps(
this.components.get(componentDef.name),
componentDef.props,
absoluteBaseURL.href
);
entityComponentPromises.push(this._addComponent(entityObj, componentDef.name, props, !isRoot));
}
}
}
if (entity.staticMode !== undefined) {
setStaticMode(entityObj, entity.staticMode);
if (isRoot) {
setOriginalStaticMode(entityObj, entity.staticMode);
}
}
}
await Promise.all(entityComponentPromises);
}
scene.userData._conflictHandler.findDuplicates(scene, 0, 0);
return scene;
}
async _loadSceneReference(uri, parent) {
this._removeSceneRefDependency(parent);
const scene = await loadScene(uri, this._addComponent, this.components, false);
const scene = await this._loadScene(uri, false);
scene.userData._dontShowInHierarchy = true;
scene.userData._sceneReference = uri;
@ -378,6 +620,7 @@ export default class Editor {
}
scene.traverse(child => {
child.userData._dontSerialize = true;
Object.defineProperty(child.userData, "_selectionRoot", { value: parent, configurable: true, enumerable: false });
});
@ -410,8 +653,8 @@ export default class Editor {
this._resetHelpers();
const sceneURI = this.sceneInfo.uri;
const sceneDef = serializeScene(this.scene, sceneURI);
const scene = await loadSerializedScene(sceneDef, sceneURI, this._addComponent, this.components, true);
const sceneDef = this._serializeScene(this.scene, sceneURI);
const scene = await this._loadSerializedScene(sceneDef, sceneURI, true);
const sceneInfo = this.scenes.find(sceneInfo => sceneInfo.uri === sceneURI);
sceneInfo.scene = scene;
@ -422,7 +665,7 @@ export default class Editor {
}
async saveScene(sceneURI) {
const serializedScene = serializeScene(this.scene, sceneURI || this.sceneInfo.uri);
const serializedScene = this._serializeScene(this.scene, sceneURI || this.sceneInfo.uri);
this.ignoreNextSceneFileChange = true;
@ -431,7 +674,7 @@ export default class Editor {
const sceneUserData = this.scene.userData;
// If the previous URI was a gltf, update the ancestors, since we are now dealing with a .scene file.
if (this.sceneInfo.uri && this.sceneInfo.uri.endsWith(".gltf")) {
if (this.sceneInfo.uri && (this.sceneInfo.uri.endsWith(".gltf") || this.sceneInfo.uri.endsWith(".glb"))) {
sceneUserData._ancestors = [this.sceneInfo.uri];
}
@ -442,11 +685,306 @@ export default class Editor {
this.sceneInfo.modified = false;
}
_serializeScene(scene, scenePath) {
const entities = {};
scene.traverse(entityObject => {
let parent;
let index;
let components;
let staticMode;
if (entityObject.userData._dontSerialize) {
return;
}
// Serialize the parent and index if _saveParent is set.
if (entityObject.userData._saveParent) {
parent = entityObject.parent.name;
const parentIndex = entityObject.parent.children.indexOf(entityObject);
if (parentIndex === -1) {
throw new Error("Entity not found in parent.");
}
index = parentIndex;
}
// Serialize all components with shouldSave set.
const entityComponents = entityObject.userData._components;
if (Array.isArray(entityComponents)) {
for (const component of entityComponents) {
if (component.shouldSave) {
if (components === undefined) {
components = [];
}
if (component.src) {
// Serialize SaveableComponent
const src = absoluteToRelativeURL(scenePath, component.src);
components.push({
name: component.name,
src
});
} else if (component.serialize) {
const props = component.serialize(scenePath);
components.push({
name: component.name,
props
});
}
}
}
}
const curStaticMode = getStaticMode(entityObject);
const originalStaticMode = getOriginalStaticMode(entityObject);
if (curStaticMode !== originalStaticMode) {
staticMode = curStaticMode;
}
const saveEntity = entityObject.userData._saveEntity;
if (parent !== undefined || components !== undefined || staticMode !== undefined || saveEntity) {
entities[entityObject.name] = {
parent,
index,
staticMode,
components
};
}
});
const serializedScene = {
entities
};
if (scene.userData._inherits) {
serializedScene.inherits = absoluteToRelativeURL(scenePath, scene.userData._inherits);
} else {
serializedScene.root = scene.name;
}
return serializedScene;
}
async exportScene(outputPath) {
const scene = this.scene;
const clonedScene = scene.clone();
computeAndSetStaticModes(clonedScene);
const meshesToCombine = [];
// First pass at scene optimization.
clonedScene.traverse(object => {
// Mark objects with meshes for merging
const curShadowComponent = ShadowComponent.getComponent(object);
const curMaterialComponent = StandardMaterialComponent.getComponent(object);
if (isStatic(object) && curShadowComponent && curMaterialComponent) {
let foundMaterial = false;
for (const { shadowComponent, materialComponent, meshes } of meshesToCombine) {
if (
shallowEquals(materialComponent.props, curMaterialComponent.props) &&
shallowEquals(shadowComponent.props, curShadowComponent.props)
) {
meshes.push(object);
foundMaterial = true;
break;
}
}
if (!foundMaterial) {
meshesToCombine.push({
shadowComponent: curShadowComponent,
materialComponent: curMaterialComponent,
meshes: [object]
});
}
}
// Remove objects marked as _dontExport
for (const child of object.children) {
if (child.userData._dontExport) {
object.remove(child);
return;
}
}
});
// Combine meshes and add to scene.
for (const { meshes } of meshesToCombine) {
if (meshes.length > 1) {
const bufferGeometries = [];
for (const mesh of meshes) {
// Clone buffer geometry in case it is re-used across meshes with different materials.
const clonedBufferGeometry = mesh.geometry.clone();
clonedBufferGeometry.applyMatrix(mesh.matrixWorld);
bufferGeometries.push(clonedBufferGeometry);
}
const originalMesh = meshes[0];
const combinedGeometry = THREE.BufferGeometryUtils.mergeBufferGeometries(bufferGeometries);
delete combinedGeometry.userData.mergedUserData;
const combinedMesh = new THREE.Mesh(combinedGeometry, originalMesh.material);
combinedMesh.name = "CombinedMesh";
combinedMesh.receiveShadow = originalMesh.receiveShadow;
combinedMesh.castShadow = originalMesh.castShadow;
clonedScene.add(combinedMesh);
for (const mesh of meshes) {
const meshIndex = mesh.parent.children.indexOf(mesh);
const parent = mesh.parent;
mesh.parent.remove(mesh);
const replacementObj = new THREE.Object3D();
replacementObj.copy(mesh);
replacementObj.children = mesh.children;
addChildAtIndex(parent, replacementObj, meshIndex);
}
}
}
const componentsToExport = Components.filter(c => !c.dontExportProps).map(component => component.componentName);
// Second pass at scene optimization.
clonedScene.traverse(object => {
const userData = object.userData;
// Move component data to userData.components
if (userData._components) {
for (const component of userData._components) {
if (componentsToExport.includes(component.name)) {
if (userData.components === undefined) {
userData.components = {};
}
userData.components[component.name] = component.props;
}
}
}
// Add shadow component to meshes with non-default values.
if (object.isMesh && (object.castShadow || object.receiveShadow)) {
if (!object.userData.components) {
object.userData.components = {};
}
object.userData.components.shadow = {
castShadow: object.castShadow,
receiveShadow: object.receiveShadow
};
}
});
function hasExtrasOrExtensions(object) {
const userData = object.userData;
for (const key in userData) {
if (userData.hasOwnProperty(key) && !key.startsWith("_")) {
return true;
}
}
return false;
}
function removeUnusedObjects(object) {
let canBeRemoved = !!object.parent;
for (const child of object.children.slice(0)) {
if (!removeUnusedObjects(child)) {
canBeRemoved = false;
}
}
const shouldRemove =
canBeRemoved &&
(object.constructor === THREE.Object3D || object.constructor === THREE.Scene) &&
object.children.length === 0 &&
isStatic(object) &&
!hasExtrasOrExtensions(object);
if (canBeRemoved && shouldRemove) {
object.parent.remove(object);
return true;
}
return false;
}
removeUnusedObjects(clonedScene);
clonedScene.traverse(({ userData }) => {
// Remove editor data.
for (const key in userData) {
if (userData.hasOwnProperty(key) && key.startsWith("_")) {
delete userData[key];
}
}
});
// TODO: export animations
const chunks = await new Promise((resolve, reject) => {
new THREE.GLTFExporter().parseChunks(clonedScene, resolve, reject, {
mode: "gltf",
onlyVisible: false
});
});
const bufferDefs = chunks.json.buffers;
if (bufferDefs && bufferDefs.length > 0 && bufferDefs[0].uri === undefined) {
bufferDefs[0].uri = clonedScene.name + ".bin";
}
// De-duplicate images.
const imageDefs = chunks.json.images;
if (imageDefs && imageDefs.length > 0) {
// Map containing imageProp -> newIndex
const uniqueImageProps = new Map();
// Map containing oldIndex -> newIndex
const imageIndexMap = new Map();
// Array containing unique imageDefs
const uniqueImageDefs = [];
// Array containing unique image blobs
const uniqueImages = [];
for (const [index, imageDef] of imageDefs.entries()) {
const imageProp = imageDef.uri === undefined ? imageDef.bufferView : imageDef.uri;
let newIndex = uniqueImageProps.get(imageProp);
if (newIndex === undefined) {
newIndex = uniqueImageDefs.push(imageDef) - 1;
uniqueImageProps.set(imageProp, newIndex);
uniqueImages.push(chunks.images[index]);
}
imageIndexMap.set(index, newIndex);
}
chunks.json.images = uniqueImageDefs;
chunks.images = uniqueImages;
for (const textureDef of chunks.json.textures) {
textureDef.source = imageIndexMap.get(textureDef.source);
}
}
// Export current editor scene using THREE.GLTFExporter
const { json, buffers, images } = await exportScene(scene);
const { json, buffers, images } = chunks;
// Ensure the output directory exists
await this.project.mkdir(outputPath);
@ -487,14 +1025,7 @@ export default class Editor {
this._clearCaches();
const ancestors = [];
const scene = await loadSerializedScene(
extendSceneDef,
inheritedURI,
this._addComponent,
this.components,
true,
ancestors
);
const scene = await this._loadSerializedScene(extendSceneDef, inheritedURI, true, ancestors);
this._setSceneInfo(scene, null);
this.scenes = [this.sceneInfo];
@ -508,6 +1039,10 @@ export default class Editor {
//
addSceneReferenceNode(name, url) {
if (url === this.sceneInfo.uri) {
throw new Error("Scene cannot be added to itself.");
}
const object = new THREE.Object3D();
object.name = name;
setStaticMode(object, StaticModes.Static);
@ -791,7 +1326,7 @@ export default class Editor {
const absoluteAssetURL = new URL(component.src, window.location).href;
let props = await this.props.editor.project.readJSON(component.src);
props = resolveFileProps(component, props, absoluteAssetURL);
props = this._resolveFileProps(component, props, absoluteAssetURL);
await component.constructor.inflate(this.state.object, props);
this.props.editor.signals.objectChanged.dispatch(this.state.object);
@ -799,7 +1334,7 @@ export default class Editor {
async saveComponent(object, componentName) {
const component = this.getComponent(object, componentName);
const props = serializeFileProps(component, component.props, component.src);
const props = component.serialize(component.src);
await this.project.writeJSON(component.src, props);
component.modified = false;
this.signals.objectChanged.dispatch(object);
@ -841,9 +1376,7 @@ export default class Editor {
this.execute(new SetValueCommand(object, "name", value));
} else {
this.signals.objectChanged.dispatch(object);
this.signals.sceneErrorOccurred.dispatch(
new ConflictError("rename error", "rename", this.sceneInfo.uri, handler)
);
throw new ConflictError("rename error", "rename", this.sceneInfo.uri, handler);
}
}

Просмотреть файл

@ -1,666 +0,0 @@
import THREE from "./three";
import { Components } from "./components";
import { types } from "./components/utils";
import ConflictHandler from "./ConflictHandler";
import StandardMaterialComponent from "../editor/components/StandardMaterialComponent";
import ShadowComponent from "./components/ShadowComponent";
import SceneLoaderError from "./SceneLoaderError";
import ConflictError from "./ConflictError";
import {
computeAndSetStaticModes,
isStatic,
getStaticMode,
setStaticMode,
getOriginalStaticMode,
setOriginalStaticMode
} from "./StaticMode";
import { gltfCache } from "./caches";
export function absoluteToRelativeURL(from, to) {
if (to === null) return null;
if (from === to) return to;
const fromURL = new URL(from, window.location);
const toURL = new URL(to, window.location);
if (fromURL.host === toURL.host) {
const relativeParts = [];
const fromParts = fromURL.pathname.split("/");
const toParts = toURL.pathname.split("/");
while (fromParts.length > 0 && toParts.length > 0 && fromParts[0] === toParts[0]) {
fromParts.shift();
toParts.shift();
}
if (fromParts.length > 1) {
for (let j = 0; j < fromParts.length - 1; j++) {
relativeParts.push("..");
}
}
for (let k = 0; k < toParts.length; k++) {
relativeParts.push(toParts[k]);
}
const relativePath = relativeParts.join("/");
if (relativePath.startsWith("../")) {
return relativePath;
}
return "./" + relativePath;
}
return to;
}
function loadGLTF(url) {
return gltfCache
.get(url)
.then(({ scene }) => {
if (scene === undefined) {
throw new Error(`Error loading: ${url}. glTF file has no default scene.`);
}
return scene;
})
.catch(e => {
console.error(e);
throw new SceneLoaderError("Error loading GLTF", url, "damaged", e);
});
}
function shallowEquals(objA, objB) {
for (const key in objA) {
if (objA[key] !== objB[key]) {
return false;
}
}
return true;
}
function hasExtrasOrExtensions(obj) {
const userData = obj.userData;
for (const key in userData) {
if (userData.hasOwnProperty(key) && !key.startsWith("_")) {
return true;
}
}
return false;
}
function removeEditorData(scene) {
scene.traverse(({ userData }) => {
// Remove editor data.
for (const key in userData) {
if (userData.hasOwnProperty(key) && key.startsWith("_")) {
delete userData[key];
}
}
});
}
function removeUnusedObjects(object) {
let canBeRemoved = !!object.parent;
for (const child of object.children.slice(0)) {
if (!removeUnusedObjects(child)) {
canBeRemoved = false;
}
}
const shouldRemove =
canBeRemoved &&
(object.constructor === THREE.Object3D || object.constructor === THREE.Scene) &&
object.children.length === 0 &&
isStatic(object) &&
!hasExtrasOrExtensions(object);
if (canBeRemoved && shouldRemove) {
object.parent.remove(object);
return true;
}
return false;
}
export async function exportScene(scene) {
const clonedScene = scene.clone();
computeAndSetStaticModes(clonedScene);
const meshesToCombine = [];
// First pass at scene optimization.
clonedScene.traverse(object => {
// Mark objects with meshes for merging
const curShadowComponent = ShadowComponent.getComponent(object);
const curMaterialComponent = StandardMaterialComponent.getComponent(object);
if (isStatic(object) && curShadowComponent && curMaterialComponent) {
let foundMaterial = false;
for (const { shadowComponent, materialComponent, meshes } of meshesToCombine) {
if (
shallowEquals(materialComponent.props, curMaterialComponent.props) &&
shallowEquals(shadowComponent.props, curShadowComponent.props)
) {
meshes.push(object);
foundMaterial = true;
break;
}
}
if (!foundMaterial) {
meshesToCombine.push({
shadowComponent: curShadowComponent,
materialComponent: curMaterialComponent,
meshes: [object]
});
}
}
// Remove objects marked as _dontExport
for (const child of object.children) {
if (child.userData._dontExport) {
object.remove(child);
return;
}
}
});
// Combine meshes and add to scene.
for (const { meshes } of meshesToCombine) {
if (meshes.length > 1) {
const bufferGeometries = [];
for (const mesh of meshes) {
// Clone buffer geometry in case it is re-used across meshes with different materials.
const clonedBufferGeometry = mesh.geometry.clone();
clonedBufferGeometry.applyMatrix(mesh.matrixWorld);
bufferGeometries.push(clonedBufferGeometry);
}
const originalMesh = meshes[0];
const combinedGeometry = THREE.BufferGeometryUtils.mergeBufferGeometries(bufferGeometries);
delete combinedGeometry.userData.mergedUserData;
const combinedMesh = new THREE.Mesh(combinedGeometry, originalMesh.material);
combinedMesh.name = "CombinedMesh";
combinedMesh.receiveShadow = originalMesh.receiveShadow;
combinedMesh.castShadow = originalMesh.castShadow;
clonedScene.add(combinedMesh);
for (const mesh of meshes) {
const meshIndex = mesh.parent.children.indexOf(mesh);
const parent = mesh.parent;
mesh.parent.remove(mesh);
const replacementObj = new THREE.Object3D();
replacementObj.copy(mesh);
replacementObj.children = mesh.children;
addChildAtIndex(parent, replacementObj, meshIndex);
}
}
}
const componentsToExport = Components.filter(c => !c.dontExportProps).map(component => component.componentName);
// Second pass at scene optimization.
clonedScene.traverse(object => {
const userData = object.userData;
// Move component data to userData.components
if (userData._components) {
for (const component of userData._components) {
if (componentsToExport.includes(component.name)) {
if (userData.components === undefined) {
userData.components = {};
}
userData.components[component.name] = component.props;
}
}
}
// Add shadow component to meshes with non-default values.
if (object.isMesh && (object.castShadow || object.receiveShadow)) {
if (!object.userData.components) {
object.userData.components = {};
}
object.userData.components.shadow = {
castShadow: object.castShadow,
receiveShadow: object.receiveShadow
};
}
});
removeUnusedObjects(clonedScene);
removeEditorData(clonedScene);
// TODO: export animations
const chunks = await new Promise((resolve, reject) => {
new THREE.GLTFExporter().parseChunks(clonedScene, resolve, reject, {
mode: "gltf",
onlyVisible: false
});
});
const buffers = chunks.json.buffers;
if (buffers && buffers.length > 0 && buffers[0].uri === undefined) {
buffers[0].uri = clonedScene.name + ".bin";
}
// De-duplicate images.
const images = chunks.json.images;
if (images && images.length > 0) {
// Map containing imageProp -> newIndex
const uniqueImageProps = new Map();
// Map containing oldIndex -> newIndex
const imageIndexMap = new Map();
// Array containing unique imageDefs
const uniqueImageDefs = [];
// Array containing unique image blobs
const uniqueImages = [];
for (const [index, imageDef] of images.entries()) {
const imageProp = imageDef.uri === undefined ? imageDef.bufferView : imageDef.uri;
let newIndex = uniqueImageProps.get(imageProp);
if (newIndex === undefined) {
newIndex = uniqueImageDefs.push(imageDef) - 1;
uniqueImageProps.set(imageProp, newIndex);
uniqueImages.push(chunks.images[index]);
}
imageIndexMap.set(index, newIndex);
}
chunks.json.images = uniqueImageDefs;
chunks.images = uniqueImages;
for (const textureDef of chunks.json.textures) {
textureDef.source = imageIndexMap.get(textureDef.source);
}
}
return chunks;
}
async function inflateGLTFComponents(scene, addComponent) {
const addComponentPromises = [];
scene.traverse(async object => {
const extensions = object.userData.gltfExtensions;
if (extensions !== undefined) {
for (const extensionName in extensions) {
addComponentPromises.push(addComponent(object, extensionName, extensions[extensionName], true));
}
}
if (object instanceof THREE.Mesh) {
addComponentPromises.push(addComponent(object, "mesh", null, true));
const shadowProps = object.userData.components
? object.userData.components.shadow
: { castShadow: true, receiveShadow: true };
addComponentPromises.push(addComponent(object, "shadow", shadowProps, true));
if (object.material instanceof THREE.MeshStandardMaterial) {
addComponentPromises.push(addComponent(object, "standard-material", null, true));
}
}
});
await Promise.all(addComponentPromises);
}
function addChildAtIndex(parent, child, index) {
parent.children.splice(index, 0, child);
child.parent = parent;
}
// Sort entities by insertion order
function sortEntities(entitiesObj) {
const sortedEntityNames = [];
let entitiesToSort = [];
// First add entities without parents
for (const entityName in entitiesObj) {
const entity = entitiesObj[entityName];
if (!entity.parent || !entitiesObj[entity.parent]) {
sortedEntityNames.push(entityName);
} else {
entitiesToSort.push(entityName);
}
}
// Then group all entities by their parent
const entitiesByParent = {};
for (const entityName of entitiesToSort) {
const entity = entitiesObj[entityName];
if (!entitiesByParent[entity.parent]) {
entitiesByParent[entity.parent] = [];
}
entitiesByParent[entity.parent].push(entityName);
}
// Then sort child entities by their index
for (const parentName in entitiesByParent) {
entitiesByParent[parentName].sort((a, b) => {
const entityA = entitiesObj[a];
const entityB = entitiesObj[b];
return entityA.index - entityB.index;
});
}
function addEntities(parentName) {
const children = entitiesByParent[parentName];
if (children === undefined) {
return;
}
children.forEach(childName => sortedEntityNames.push(childName));
for (const childName of children) {
addEntities(childName);
}
}
// Clone sortedEntityNames so we can iterate over the initial entities and modify the array
entitiesToSort = sortedEntityNames.concat();
// Then recursively iterate over the child entities.
for (const entityName of entitiesToSort) {
addEntities(entityName);
}
return sortedEntityNames;
}
function resolveFileProps(component, props, basePath) {
const clonedProps = Object.assign({}, props);
for (const { name, type } of component.schema) {
if (type === types.file && props[name]) {
props[name] = new URL(props[name], basePath).href;
}
}
return clonedProps;
}
export function serializeFileProps(component, props, basePath) {
const clonedProps = Object.assign({}, props);
for (const { name, type } of component.schema) {
if (type === types.file) {
clonedProps[name] = absoluteToRelativeURL(basePath, clonedProps[name]);
}
}
return clonedProps;
}
export async function loadSerializedScene(sceneDef, baseURI, addComponent, components, isRoot = true, ancestors) {
let scene;
const { inherits, root, entities } = sceneDef;
const absoluteBaseURL = new URL(baseURI, window.location);
if (inherits) {
const inheritedSceneURL = new URL(inherits, absoluteBaseURL).href;
scene = await loadScene(inheritedSceneURL, addComponent, components, false, ancestors);
if (ancestors) {
ancestors.push(inheritedSceneURL);
}
if (isRoot) {
scene.userData._inherits = inheritedSceneURL;
}
} else if (root) {
scene = new THREE.Scene();
scene.name = root;
} else {
throw new Error("Invalid Scene: Scene does not inherit from another scene or have a root entity.");
}
// init scene conflict status
if (!scene.userData._conflictHandler) {
scene.userData._conflictHandler = new ConflictHandler();
scene.userData._conflictHandler.findDuplicates(scene, 0, 0);
}
if (entities) {
// Sort entities by insertion order (uses parent and index to determine order).
const sortedEntities = sortEntities(entities);
const entityComponentPromises = [];
for (const entityName of sortedEntities) {
const entity = entities[entityName];
// Find or create the entity's Object3D
let entityObj = scene.getObjectByName(entityName);
if (entityObj === undefined) {
entityObj = new THREE.Object3D();
entityObj.name = entityName;
}
// Entities defined in the root scene should be saved.
if (isRoot) {
entityObj.userData._saveEntity = true;
}
// Attach the entity to its parent.
// An entity doesn't have a parent defined if the entity is loaded in an inherited scene.
if (entity.parent) {
let parentObject = scene.getObjectByName(entity.parent);
if (!parentObject) {
// parent node got renamed or deleted
parentObject = new THREE.Object3D();
parentObject.name = entity.parent;
parentObject.userData._isMissingRoot = true;
parentObject.userData._missing = true;
scene.userData._conflictHandler.setMissingStatus(true);
scene.add(parentObject);
} else {
if (!parentObject.userData._missing) {
parentObject.userData._isMissingRoot = false;
parentObject.userData._missing = false;
}
}
entityObj.userData._missing = parentObject.userData._missing;
entityObj.userData._duplicate = parentObject.userData._duplicate;
addChildAtIndex(parentObject, entityObj, entity.index);
// Parents defined in the root scene should be saved.
if (isRoot) {
entityObj.userData._saveParent = true;
}
}
// Inflate the entity's components.
if (Array.isArray(entity.components)) {
for (const componentDef of entity.components) {
const { props } = componentDef;
if (componentDef.src) {
// Process SaveableComponent
componentDef.src = new URL(componentDef.src, absoluteBaseURL.href);
const resp = await fetch(componentDef.src);
let json = {};
if (resp.ok) {
json = await resp.json();
}
const props = resolveFileProps(components.get(componentDef.name), json, componentDef.src);
entityComponentPromises.push(
addComponent(entityObj, componentDef.name, props, !isRoot).then(component => {
component.src = componentDef.src;
component.srcIsValid = resp.ok;
})
);
} else {
entityComponentPromises.push(addComponent(entityObj, componentDef.name, props, !isRoot));
}
}
}
if (entity.staticMode !== undefined) {
setStaticMode(entityObj, entity.staticMode);
if (isRoot) {
setOriginalStaticMode(entityObj, entity.staticMode);
}
}
}
await Promise.all(entityComponentPromises);
}
scene.userData._conflictHandler.findDuplicates(scene, 0, 0);
return scene;
}
export async function loadScene(uri, addComponent, components, isRoot = true, ancestors) {
let scene;
const url = new URL(uri, window.location).href;
if (url.endsWith(".gltf")) {
scene = await loadGLTF(url);
if (isRoot) {
scene.userData._inherits = url;
}
if (!scene.name) {
scene.name = "Scene";
}
scene.userData._conflictHandler = new ConflictHandler();
scene.userData._conflictHandler.findDuplicates(scene, 0, 0);
if (scene.userData._conflictHandler.getDuplicateStatus() || scene.userData._conflictHandler.getMissingStatus()) {
const error = new ConflictError("gltf naming conflicts", "import", url, scene.userData._conflictHandler);
throw error;
}
await inflateGLTFComponents(scene, addComponent);
return scene;
}
const sceneResponse = await fetch(url);
if (!sceneResponse.ok) {
const error = new SceneLoaderError("Error loading .scene", url, "damaged", null);
throw error;
}
const sceneDef = await sceneResponse.json();
if (isRoot) {
ancestors = [];
}
scene = await loadSerializedScene(sceneDef, uri, addComponent, components, isRoot, ancestors);
scene.userData._ancestors = ancestors;
return scene;
}
export function serializeScene(scene, scenePath) {
scene = scene.clone();
const entities = {};
scene.traverse(entityObject => {
let parent;
let index;
let components;
let staticMode;
// Serialize the parent and index if _saveParent is set.
if (entityObject.userData._saveParent) {
parent = entityObject.parent.name;
const parentIndex = entityObject.parent.children.indexOf(entityObject);
if (parentIndex === -1) {
throw new Error("Entity not found in parent.");
}
index = parentIndex;
}
// Serialize all components with shouldSave set.
const entityComponents = entityObject.userData._components;
if (Array.isArray(entityComponents)) {
for (const component of entityComponents) {
if (component.shouldSave) {
if (components === undefined) {
components = [];
}
if (component.src) {
// Serialize SaveableComponent
const src = absoluteToRelativeURL(scenePath, component.src);
components.push({
name: component.name,
src
});
} else {
const props = serializeFileProps(component, component.props, scenePath);
components.push({
name: component.name,
props
});
}
}
}
}
const curStaticMode = getStaticMode(entityObject);
const originalStaticMode = getOriginalStaticMode(entityObject);
if (curStaticMode !== originalStaticMode) {
staticMode = curStaticMode;
}
const saveEntity = entityObject.userData._saveEntity;
if (parent !== undefined || components !== undefined || staticMode !== undefined || saveEntity) {
entities[entityObject.name] = {
parent,
index,
staticMode,
components
};
}
});
const serializedScene = {
entities
};
if (scene.userData._inherits) {
serializedScene.inherits = absoluteToRelativeURL(scenePath, scene.userData._inherits);
} else {
serializedScene.root = scene.name;
}
return serializedScene;
}

Просмотреть файл

@ -6,7 +6,8 @@ class Cache {
_cache = new Map();
evict(url) {
this._cache.delete(url);
const absoluteURL = new URL(url, window.location).href;
this._cache.delete(absoluteURL);
}
_clear() {
@ -16,15 +17,16 @@ class Cache {
class TextureCache extends Cache {
get(url) {
if (!this._cache.has(url)) {
const absoluteURL = new URL(url, window.location).href;
if (!this._cache.has(absoluteURL)) {
this._cache.set(
url,
absoluteURL,
new Promise((resolve, reject) => {
textureLoader.load(url, resolve, null, reject);
textureLoader.load(absoluteURL, resolve, null, reject);
})
);
}
return this._cache.get(url);
return this._cache.get(absoluteURL);
}
disposeAndClear() {
@ -35,6 +37,8 @@ class TextureCache extends Cache {
}
}
export const textureCache = new TextureCache();
function clonable(obj) {
// Punting on skinned meshes for now because of https://github.com/mrdoob/three.js/pull/14494
// We solved this in Hubs here:
@ -48,22 +52,33 @@ function clonable(obj) {
class GLTFCache extends Cache {
get(url) {
if (!this._cache.has(url)) {
const absoluteURL = new URL(url, window.location).href;
if (!this._cache.has(absoluteURL)) {
this._cache.set(
url,
absoluteURL,
new Promise((resolve, reject) => {
const loader = new THREE.GLTFLoader();
loader.load(url, resolve, null, reject);
loader.load(absoluteURL, resolve, null, reject);
})
);
}
return this._cache.get(url).then(gltf => {
return this._cache.get(absoluteURL).then(gltf => {
if (!clonable(gltf.scene)) return gltf;
const clonedGLTF = { scene: gltf.scene.clone() };
clonedGLTF.scene.traverse(obj => {
if (!obj.material) return;
if (obj.material.clone) {
obj.material = obj.material.clone();
for (const key in obj.material) {
const prop = obj.material[key];
if (prop instanceof THREE.Texture) {
if (prop.image.src) {
const absoluteTextureURL = new URL(prop.image.src, window.location).href;
textureCache._cache.set(absoluteTextureURL, Promise.resolve(prop));
}
}
}
} else if (obj.material.length) {
obj.material = obj.material.map(mat => mat.clone());
} else {
@ -93,6 +108,4 @@ class GLTFCache extends Cache {
}
}
export const textureCache = new TextureCache();
export const gltfCache = new GLTFCache();

Просмотреть файл

@ -1,4 +1,6 @@
import THREE from "../three";
import { types } from "./utils";
import absoluteToRelativeURL from "../utils/absoluteToRelativeURL";
export function getDefaultsFromSchema(schema) {
const defaults = {};
@ -31,6 +33,18 @@ export default class BaseComponent {
this.props[propertyName] = value;
}
serialize(basePath) {
const clonedProps = Object.assign({}, this.props);
for (const { name, type } of this.schema) {
if (type === types.file) {
clonedProps[name] = absoluteToRelativeURL(basePath, clonedProps[name]);
}
}
return clonedProps;
}
static getComponent(node) {
if (!node.userData._components) return null;

Просмотреть файл

@ -6,5 +6,5 @@ export default class SceneReferenceComponent extends BaseComponent {
static dontExportProps = true;
static schema = [{ name: "src", type: types.file, filters: [".scene", ".gltf"], default: null }];
static schema = [{ name: "src", type: types.file, filters: [".scene"], default: null }];
}

Просмотреть файл

@ -3,15 +3,10 @@ import { types } from "./utils";
import THREE from "../three";
import { textureCache } from "../caches";
function getURLPath(url) {
const href = window.location.href;
return new URL(url, href.substring(0, href.length - 1)).pathname;
}
function getTextureSrc(texture) {
if (!texture) return null;
if (!texture.image) return null;
return getURLPath(texture.image.src);
return new URL(texture.image.src, window.location).href;
}
const imageFilters = [".jpg", ".png"];
@ -66,11 +61,8 @@ export default class StandardMaterialComponent extends SaveableComponent {
return;
}
const urlPath = getURLPath(url);
if (urlPath === getTextureSrc(this._object[map])) return;
try {
const texture = await textureCache.get(urlPath);
const texture = await textureCache.get(url);
if (sRGB) {
texture.encoding = THREE.sRGBEncoding;
}
@ -80,6 +72,7 @@ export default class StandardMaterialComponent extends SaveableComponent {
texture.wrapT = THREE.RepeatWrapping;
this._object[map] = texture;
} catch (e) {
console.error(e);
this._object[map] = null;
this._object.needsUpdate = true;
this.propValidation[propertyName] = false;

Просмотреть файл

@ -0,0 +1,39 @@
export default function absoluteToRelativeURL(from, to) {
if (to === null) return null;
if (from === to) return to;
const fromURL = new URL(from, window.location);
const toURL = new URL(to, window.location);
if (fromURL.host === toURL.host) {
const relativeParts = [];
const fromParts = fromURL.pathname.split("/");
const toParts = toURL.pathname.split("/");
while (fromParts.length > 0 && toParts.length > 0 && fromParts[0] === toParts[0]) {
fromParts.shift();
toParts.shift();
}
if (fromParts.length > 1) {
for (let j = 0; j < fromParts.length - 1; j++) {
relativeParts.push("..");
}
}
for (let k = 0; k < toParts.length; k++) {
relativeParts.push(toParts[k]);
}
const relativePath = relativeParts.join("/");
if (relativePath.startsWith("../")) {
return relativePath;
}
return "./" + relativePath;
}
return to;
}

Просмотреть файл

@ -0,0 +1,4 @@
export default function addChildAtIndex(parent, child, index) {
parent.children.splice(index, 0, child);
child.parent = parent;
}

Просмотреть файл

@ -0,0 +1,9 @@
export default function shallowEquals(objA, objB) {
for (const key in objA) {
if (objA[key] !== objB[key]) {
return false;
}
}
return true;
}

Просмотреть файл

@ -0,0 +1,63 @@
export default function sortEntities(entitiesObj) {
// Sort entities by insertion order
const sortedEntityNames = [];
let entitiesToSort = [];
// First add entities without parents
for (const entityName in entitiesObj) {
const entity = entitiesObj[entityName];
if (!entity.parent || !entitiesObj[entity.parent]) {
sortedEntityNames.push(entityName);
} else {
entitiesToSort.push(entityName);
}
}
// Then group all entities by their parent
const entitiesByParent = {};
for (const entityName of entitiesToSort) {
const entity = entitiesObj[entityName];
if (!entitiesByParent[entity.parent]) {
entitiesByParent[entity.parent] = [];
}
entitiesByParent[entity.parent].push(entityName);
}
// Then sort child entities by their index
for (const parentName in entitiesByParent) {
entitiesByParent[parentName].sort((a, b) => {
const entityA = entitiesObj[a];
const entityB = entitiesObj[b];
return entityA.index - entityB.index;
});
}
function addEntities(parentName) {
const children = entitiesByParent[parentName];
if (children === undefined) {
return;
}
children.forEach(childName => sortedEntityNames.push(childName));
for (const childName of children) {
addEntities(childName);
}
}
// Clone sortedEntityNames so we can iterate over the initial entities and modify the array
entitiesToSort = sortedEntityNames.concat();
// Then recursively iterate over the child entities.
for (const entityName of entitiesToSort) {
addEntities(entityName);
}
return sortedEntityNames;
}

Просмотреть файл

@ -13,11 +13,13 @@ import PropertiesPanelContainer from "./panels/PropertiesPanelContainer";
import AssetExplorerPanelContainer from "./panels/AssetExplorerPanelContainer";
import { EditorContextProvider } from "./contexts/EditorContext";
import { DialogContextProvider } from "./contexts/DialogContext";
import { OptionDialog } from "./dialogs/OptionDialog";
import { SceneActionsContextProvider } from "./contexts/SceneActionsContext";
import ConfirmDialog from "./dialogs/ConfirmDialog";
import styles from "../common.scss";
import FileDialog from "./dialogs/FileDialog";
import ProgressDialog, { PROGRESS_DIALOG_DELAY } from "./dialogs/ProgressDialog";
import ProgressDialog from "./dialogs/ProgressDialog";
import ErrorDialog from "./dialogs/ErrorDialog";
import ConflictError from "../editor/ConflictError";
class EditorContainer extends Component {
static defaultProps = {
@ -120,7 +122,7 @@ class EditorContainer extends Component {
},
{
name: "Export Scene...",
action: e => this.onOpenExportModal(e)
action: e => this.onExportScene(e)
},
{
name: "Open Project Directory",
@ -158,11 +160,7 @@ class EditorContainer extends Component {
componentDidMount() {
this.props.editor.signals.windowResize.dispatch();
this.props.editor.signals.popScene.add(this.onPopScene);
this.props.editor.signals.openScene.add(this.onOpenScene);
this.props.editor.signals.extendScene.add(this.onExtendScene);
this.props.editor.signals.sceneModified.add(this.onSceneModified);
this.props.editor.signals.sceneErrorOccurred.add(this.onSceneErrorOccurred);
window.onbeforeunload = e => {
if (!this.props.editor.sceneModified()) {
@ -187,6 +185,10 @@ class EditorContainer extends Component {
this.props.editor.signals.windowResize.dispatch();
};
/**
* Dialog Context
*/
showDialog = (DialogComponent, dialogProps = {}) => {
this.setState({
DialogComponent,
@ -201,12 +203,20 @@ class EditorContainer extends Component {
});
};
dialogContext = {
showDialog: this.showDialog,
hideDialog: this.hideDialog
};
onCloseModal = () => {
this.setState({
openModal: null
});
};
/**
* Hotkey / Hamburger Menu Handlers
*/
onUndo = () => {
if (this.state.DialogComponent !== null) {
return;
@ -232,104 +242,51 @@ class EditorContainer extends Component {
}
};
openSaveAsDialog(onSave) {
this.showDialog(FileDialog, {
title: "Save scene as...",
filters: [".scene"],
extension: ".scene",
confirmButtonLabel: "Save",
onConfirm: onSave
});
}
onSave = async e => {
e.preventDefault();
// Disable when dialog is shown.
if (this.state.DialogComponent !== null) {
return;
}
if (!this.props.editor.sceneInfo.uri || this.props.editor.sceneInfo.uri.endsWith(".gltf")) {
this.openSaveAsDialog(this.serializeAndSaveScene);
if (this.props.editor.sceneInfo.uri) {
this.onSaveScene(this.props.editor.sceneInfo.uri);
} else {
this.serializeAndSaveScene(this.props.editor.sceneInfo.uri);
this.onSaveSceneAsDialog();
}
};
onSaveAs = e => {
e.preventDefault();
// Disable when dialog is shown.
if (this.state.DialogComponent !== null) {
return;
}
this.openSaveAsDialog(this.serializeAndSaveScene);
this.onSaveSceneAsDialog();
};
serializeAndSaveScene = async sceneURI => {
let saved = false;
this.hideDialog();
try {
setTimeout(() => {
if (saved) return;
this.showDialog(ProgressDialog, {
title: "Saving Scene",
message: "Saving scene..."
});
}, PROGRESS_DIALOG_DELAY);
await this.props.editor.saveScene(sceneURI);
this.onSceneModified();
this.hideDialog();
} catch (e) {
console.error(e);
this.showDialog(ErrorDialog, {
title: "Error Saving Scene",
message: e.message || "There was an error when saving the scene."
});
} finally {
saved = true;
}
};
onOpenExportModal = e => {
onExportScene = e => {
e.preventDefault();
this.showDialog(FileDialog, {
title: "Select the output directory",
confirmButtonLabel: "Export Scene",
directory: true,
onConfirm: async outputPath => {
let exported = false;
// Disable when dialog is shown.
if (this.state.DialogComponent !== null) {
return;
}
this.hideDialog();
this.onExportSceneDialog();
};
try {
setTimeout(() => {
if (exported) return;
this.showDialog(ProgressDialog, {
title: "Exporting Scene",
message: "Exporting scene..."
});
}, PROGRESS_DIALOG_DELAY);
/**
* Scene Event Handlers
*/
await this.props.editor.exportScene(outputPath);
this.hideDialog();
} catch (e) {
console.error(e);
this.showDialog(ErrorDialog, {
title: "Error Exporting Scene",
message: e.message || "There was an error when exporting the scene."
});
} finally {
exported = true;
}
}
onEditorError = e => {
this.showDialog(ErrorDialog, {
title: e.title || "Error",
message: e.message || "There was an unknown error."
});
};
@ -338,118 +295,269 @@ class EditorContainer extends Component {
document.title = `Spoke - ${this.props.editor.scene.name}${modified}`;
};
confirmSceneChange = () => {
return (
!this.props.editor.sceneModified() ||
confirm("This scene has unsaved changes. Do you really want to really want to change scenes without saving?")
);
/**
* Scene Actions
*/
waitForFile(options) {
return new Promise(resolve => {
const props = Object.assign(
{
onConfirm: filePath => resolve(filePath),
onCancel: () => {
this.hideDialog();
resolve(null);
}
},
options
);
this.showDialog(FileDialog, props);
});
}
waitForConfirm(options) {
return new Promise(resolve => {
const props = Object.assign(
{
onConfirm: () => {
this.hideDialog();
resolve(true);
},
onCancel: () => {
this.hideDialog();
resolve(false);
}
},
options
);
this.showDialog(ConfirmDialog, props);
});
}
confirmSceneChange = async () => {
if (!this.props.editor.sceneModified()) {
return true;
}
return this.waitForConfirm({
title: "Are you sure you wish to change the scene?",
message: "This scene has unsaved changes. Do you really want to really want to change scenes without saving?"
});
};
onPopScene = () => {
if (!this.confirmSceneChange()) return;
this.props.editor.popScene();
};
onNewScene = () => {
if (!this.confirmSceneChange()) return;
onNewScene = async () => {
if (!(await this.confirmSceneChange())) return;
this.props.editor.loadNewScene();
};
onOpenSceneDialog = async () => {
const filePath = await this.waitForFile({
title: "Open scene...",
filters: [".scene"],
extension: ".scene",
confirmButtonLabel: "Open"
});
if (filePath === null) return;
await this.onOpenScene(filePath);
};
onOpenScene = async uri => {
if (this.props.editor.sceneInfo.uri === uri) return;
if (!this.confirmSceneChange()) return;
this._tryLoadSceneFromURI(uri, this.props.editor.openRootScene.bind(this.props.editor), this.onOpenScene);
this.showDialog(ProgressDialog, {
title: "Opening Scene",
message: "Opening scene..."
});
try {
await this.props.editor.openScene(uri);
this.hideDialog();
} catch (e) {
console.error(e);
this.showDialog(ErrorDialog, {
title: "Error opening scene.",
message: e.message || "There was an error when opening the scene."
});
}
};
onSaveSceneAsDialog = async () => {
const filePath = await this.waitForFile({
title: "Save scene as...",
filters: [".scene"],
extension: ".scene",
confirmButtonLabel: "Save"
});
if (filePath === null) return;
await this.onSaveScene(filePath);
};
onSaveScene = async sceneURI => {
this.showDialog(ProgressDialog, {
title: "Saving Scene",
message: "Saving scene..."
});
try {
await this.props.editor.saveScene(sceneURI);
this.hideDialog();
} catch (e) {
console.error(e);
this.showDialog(ErrorDialog, {
title: "Error Saving Scene",
message: e.message || "There was an error when saving the scene."
});
}
};
onExtendScene = async uri => {
if (!this.confirmSceneChange()) return;
this._tryLoadSceneFromURI(uri, this.props.editor.extendScene.bind(this.props.editor), this.onExtendScene);
};
if (!(await this.confirmSceneChange())) return;
_tryLoadSceneFromURI = async (uri, action, reload) => {
let opened = false;
this.showDialog(ProgressDialog, {
title: "Extending Prefab",
message: "Extending prefab..."
});
try {
setTimeout(() => {
if (opened) return;
this.showDialog(ProgressDialog, {
title: "Opening Scene",
message: "Opening scene..."
});
}, PROGRESS_DIALOG_DELAY);
await action(uri);
await this.props.editor.extendScene(uri);
this.hideDialog();
} catch (e) {
if (e.type === "import" || e.type === "rename") {
this.onSceneErrorOccurred(e, uri, reload);
} else {
this.showDialog(OptionDialog, {
title: "Error Opening Scene",
message: `
${e.message}:
${e.url}.
Please make sure the file exists and then press "Resolved" to reload the scene.
`,
options: [
{
label: "Resolved",
onClick: () => {
this.hideDialog();
reload(uri);
}
}
],
cancelLabel: "Cancel"
});
}
} finally {
opened = true;
}
};
console.error(e);
onSceneErrorOccurred = (error, uri, reload) => {
if (error.type === "import") {
// empty/duplicate node names in the importing file
this.showDialog(OptionDialog, {
title: "Resolve Node Conflicts",
message:
"We've found duplicate and/or missing node names in this file.\nWould you like to fix all conflicts?\n*This will modify the original file.",
options: [
{
label: "Okay",
onClick: () => {
this.hideDialog();
this._overwriteConflictsInSource(error.uri, error.handler, () => {
reload(uri);
});
}
}
],
cancelLabel: "Cancel"
});
} else if (error.type === "rename") {
// renaming
this.showDialog(ErrorDialog, {
title: "Name in Use",
message: "Node name is already in use. Please choose another.",
confirmLabel: "Okay"
title: "Error extending prefab.",
message: e.message || "There was an error when extending the prefab."
});
}
};
_overwriteConflictsInSource = async (uri, conflictHandler, callback) => {
const project = this.props.editor.project;
if (uri && uri.endsWith(".gltf")) {
const originalGLTF = await project.readJSON(uri);
const nodes = originalGLTF.nodes;
if (nodes) {
conflictHandler.updateNodeNames(nodes);
await project.writeJSON(uri, originalGLTF);
callback();
}
onEditPrefab = async (object, path) => {
this.showDialog(ProgressDialog, {
title: "Opening Prefab",
message: "Opening prefab..."
});
try {
await this.props.editor.editScenePrefab(object, path);
this.hideDialog();
} catch (e) {
console.error(e);
this.showDialog(ErrorDialog, {
title: "Error opening prefab.",
message: e.message || "There was an error when opening the prefab."
});
}
};
onPopScene = async () => {
if (!(await this.confirmSceneChange())) return;
this.props.editor.popScene();
};
onCreatePrefabFromGLTF = async gltfPath => {
try {
const defaultFileName = gltfPath
.split("/")
.pop()
.replace(".gltf", "")
.replace(".glb", "");
const outputPath = await this.waitForFile({
title: "Save prefab as...",
filters: [".scene"],
extension: ".scene",
defaultFileName,
confirmButtonLabel: "Create Prefab"
});
if (!outputPath) return null;
this.showDialog(ProgressDialog, {
title: "Creating Prefab",
message: "Creating prefab..."
});
await this.props.editor.createPrefabFromGLTF(gltfPath, outputPath);
this.hideDialog();
return outputPath;
} catch (e) {
if (e instanceof ConflictError) {
const result = await this.waitForConfirm({
title: "Resolve Node Conflicts",
message:
"We've found duplicate and/or missing node names in this file.\nWould you like to fix all conflicts?\n*This will modify the original file."
});
if (!result) return null;
if (await this.props.editor.fixConflictError(e)) {
return this.onCreatePrefabFromGLTF(gltfPath);
}
}
console.error(e);
this.showDialog(ErrorDialog, {
title: "Error Creating Prefab",
message: e.message || "There was an error when creating the prefab."
});
return null;
}
};
onExportSceneDialog = async () => {
const outputPath = await this.waitForFile({
title: "Select the output directory",
directory: true,
confirmButtonLabel: "Export scene"
});
if (outputPath === null) return;
this.showDialog(ProgressDialog, {
title: "Exporting Scene",
message: "Exporting scene..."
});
try {
await this.props.editor.exportScene(outputPath);
this.hideDialog();
} catch (e) {
console.error(e);
this.showDialog(ErrorDialog, {
title: "Error Exporting Scene",
message: e.message || "There was an error when exporting the scene."
});
}
};
sceneActionsContext = {
onNewScene: this.onNewScene,
onOpenSceneDialog: this.onOpenSceneDialog,
onOpenScene: this.onOpenScene,
onSaveSceneAsDialog: this.onSaveSceneAsDialog,
onSaveScene: this.onSaveScene,
onExtendScene: this.onExtendScene,
onEditPrefab: this.onEditPrefab,
onPopScene: this.onPopScene,
onCreatePrefabFromGLTF: this.onCreatePrefabFromGLTF,
onExportSceneDialog: this.onExportSceneDialog
};
renderPanel = (panelId, path) => {
const panel = this.state.registeredPanels[panelId];
@ -465,40 +573,40 @@ class EditorContainer extends Component {
const { initialPanels, editor } = this.props;
const dialogContext = { showDialog: this.showDialog, hideDialog: this.hideDialog };
return (
<DragDropContextProvider backend={HTML5Backend}>
<HotKeys keyMap={this.state.keyMap} handlers={this.state.globalHotKeyHandlers} className={styles.flexColumn}>
<EditorContextProvider value={editor}>
<DialogContextProvider value={dialogContext}>
<ToolBar menus={menus} editor={editor} />
<MosaicWithoutDragDropContext
className="mosaic-theme"
renderTile={this.renderPanel}
initialValue={initialPanels}
onChange={this.onPanelChange}
/>
<Modal
ariaHideApp={false}
isOpen={!!openModal}
onRequestClose={this.onCloseModal}
shouldCloseOnOverlayClick={openModal && openModal.shouldCloseOnOverlayClick}
className="Modal"
overlayClassName="Overlay"
>
{openModal && <openModal.component {...openModal.props} />}
</Modal>
<Modal
ariaHideApp={false}
isOpen={!!DialogComponent}
onRequestClose={this.hideDialog}
shouldCloseOnOverlayClick={false}
className="Modal"
overlayClassName="Overlay"
>
{DialogComponent && <DialogComponent {...dialogProps} hideDialog={this.hideDialog} />}
</Modal>
<DialogContextProvider value={this.dialogContext}>
<SceneActionsContextProvider value={this.sceneActionsContext}>
<ToolBar menus={menus} editor={editor} />
<MosaicWithoutDragDropContext
className="mosaic-theme"
renderTile={this.renderPanel}
initialValue={initialPanels}
onChange={this.onPanelChange}
/>
<Modal
ariaHideApp={false}
isOpen={!!openModal}
onRequestClose={this.onCloseModal}
shouldCloseOnOverlayClick={openModal && openModal.shouldCloseOnOverlayClick}
className="Modal"
overlayClassName="Overlay"
>
{openModal && <openModal.component {...openModal.props} />}
</Modal>
<Modal
ariaHideApp={false}
isOpen={!!DialogComponent}
onRequestClose={this.hideDialog}
shouldCloseOnOverlayClick={false}
className="Modal"
overlayClassName="Overlay"
>
{DialogComponent && <DialogComponent {...dialogProps} hideDialog={this.hideDialog} />}
</Modal>
</SceneActionsContextProvider>
</DialogContextProvider>
</EditorContextProvider>
</HotKeys>

Просмотреть файл

@ -1,6 +1,7 @@
import React from "react";
import PropTypes from "prop-types";
import { DropTarget } from "react-dnd";
import { NativeTypes } from "react-dnd-html5-backend";
import styles from "./FileDropTarget.scss";
function FileDropTarget({ connectDropTarget, children }) {
@ -13,13 +14,13 @@ FileDropTarget.propTypes = {
};
export default DropTarget(
"file",
["file", NativeTypes.FILE, NativeTypes.URL],
{
drop(props, monitor) {
const item = monitor.getItem();
if (props.onDropFile) {
props.onDropFile(item.file);
props.onDropFile(item);
}
return item;

Просмотреть файл

@ -0,0 +1,15 @@
import React from "react";
const SceneActionsContext = React.createContext(null);
export const SceneActionsContextProvider = SceneActionsContext.Provider;
export function withSceneActions(Component) {
return function SceneActionsContextComponent(props) {
return (
<SceneActionsContext.Consumer>
{sceneActions => <Component {...props} sceneActions={sceneActions} />}
</SceneActionsContext.Consumer>
);
};
}

Просмотреть файл

@ -0,0 +1,33 @@
import React from "react";
import PropTypes from "prop-types";
import styles from "./dialog.scss";
import Button from "../Button";
import Header from "../Header";
export default function ConfirmDialog({ title, message, confirmLabel, cancelLabel, onConfirm, onCancel }) {
return (
<div className={styles.dialogContainer}>
<Header title={title} />
<div className={styles.content}>{message}</div>
<div className={styles.bottom}>
<Button onClick={onConfirm}>{confirmLabel}</Button>
<Button onClick={onCancel}>{cancelLabel}</Button>
</div>
</div>
);
}
ConfirmDialog.propTypes = {
title: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
confirmLabel: PropTypes.string,
cancelLabel: PropTypes.string,
onConfirm: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired
};
ConfirmDialog.defaultProps = {
title: "Confirm",
confirmLabel: "Ok",
cancelLabel: "Cancel"
};

Просмотреть файл

@ -4,6 +4,8 @@ import Tree from "@robertlong/react-ui-tree";
import "../../vendor/react-ui-tree/index.scss";
import classNames from "classnames";
import { withEditor } from "../contexts/EditorContext";
import { withSceneActions } from "../contexts/SceneActionsContext";
import { withDialog } from "../contexts/DialogContext";
import IconGrid from "../IconGrid";
import Icon from "../Icon";
import iconStyles from "../Icon.scss";
@ -11,6 +13,7 @@ import styles from "./AssetExplorerPanelContainer.scss";
import DraggableFile from "../DraggableFile";
import { ContextMenu, MenuItem, ContextMenuTrigger } from "react-contextmenu";
import folderIcon from "../../assets/folder-icon.svg";
import ErrorDialog from "../dialogs/ErrorDialog";
function collectFileMenuProps({ file }) {
return file;
@ -19,8 +22,10 @@ function collectFileMenuProps({ file }) {
function getFileContextMenuId(file) {
if (file.isDirectory) {
return "directory-menu-default";
} else if ([".gltf", ".scene"].includes(file.ext)) {
} else if (file.ext === ".scene") {
return "file-menu-extend";
} else if (file.ext === ".gltf" || file.ext === ".glb") {
return "file-menu-gltf";
} else {
return "file-menu-default";
}
@ -46,7 +51,10 @@ function getSelectedDirectory(tree, uri) {
class AssetExplorerPanelContainer extends Component {
static propTypes = {
editor: PropTypes.any
editor: PropTypes.object,
sceneActions: PropTypes.object,
showDialog: PropTypes.func,
hideDialog: PropTypes.func
};
constructor(props) {
@ -106,7 +114,7 @@ class AssetExplorerPanelContainer extends Component {
});
};
onClickFile = (e, file) => {
onClickFile = async (e, file) => {
if (this.state.singleClickedFile && file.uri === this.state.singleClickedFile.uri) {
// Handle double click
if (file.isDirectory) {
@ -114,8 +122,15 @@ class AssetExplorerPanelContainer extends Component {
return;
}
if (file.ext === ".gltf" || file.ext === ".scene") {
this.props.editor.signals.openScene.dispatch(file.uri);
if (file.ext === ".scene") {
await this.props.sceneActions.onOpenScene(file.uri);
return;
} else if (file.ext === ".gltf" || file.ext === ".glb") {
const prefabPath = await this.props.sceneActions.onCreatePrefabFromGLTF(file.uri);
if (prefabPath) {
this.props.sceneActions.onOpenScene(prefabPath);
}
return;
}
@ -159,7 +174,10 @@ class AssetExplorerPanelContainer extends Component {
// eslint-disable-next-line no-useless-escape
if (!/^[0-9a-zA-Z\^\&\'\@\{\}\[\]\,\$\=\!\-\#\(\)\.\%\+\~\_ ]+$/.test(folderName)) {
alert('Invalid folder name. The following characters are not allowed: / : * ? " < > |');
this.props.showDialog(ErrorDialog, {
title: "Error renaming folder.",
message: 'Invalid folder name. The following characters are not allowed: / : * ? " < > |'
});
return;
}
@ -188,16 +206,16 @@ class AssetExplorerPanelContainer extends Component {
document.body.removeChild(textArea);
};
onExtend = (e, file) => {
if (!file) {
return;
onOpenScene = (e, file) => this.props.sceneActions.onOpenScene(file.uri);
onExtendScene = (e, file) => this.props.sceneActions.onExtendScene(file.uri);
onCreatePrefabFromGLTF = async (e, file) => {
const prefabPath = await this.props.sceneActions.onCreatePrefabFromGLTF(file.uri);
if (prefabPath) {
this.props.sceneActions.onOpenScene(prefabPath);
}
if (file.ext === ".gltf") {
this.props.editor.signals.openScene.dispatch(file.uri);
} else {
this.props.editor.signals.extendScene.dispatch(file.uri);
}
return;
};
renderNode = node => {
@ -219,7 +237,7 @@ class AssetExplorerPanelContainer extends Component {
render() {
const selectedDirectory = getSelectedDirectory(this.state.tree, this.state.selectedDirectory) || this.state.tree;
const files = (selectedDirectory.files || []).filter(
file => [".gltf", ".scene", ".material"].includes(file.ext) || file.isDirectory
file => [".gltf", ".glb", ".scene", ".material"].includes(file.ext) || file.isDirectory
);
const selectedFile = this.state.selectedFile;
@ -279,10 +297,17 @@ class AssetExplorerPanelContainer extends Component {
<MenuItem onClick={this.onCopyURL}>Copy URL</MenuItem>
</ContextMenu>
<ContextMenu id="file-menu-extend">
<MenuItem>Open File</MenuItem>
<MenuItem onCLick={this.onOpenScene}>Open File</MenuItem>
<MenuItem>Delete File</MenuItem>
<MenuItem onClick={this.onCopyURL}>Copy URL</MenuItem>
<MenuItem onClick={this.onExtend}>Extend</MenuItem>
<MenuItem onClick={this.onExtendScene}>Extend</MenuItem>
</ContextMenu>
<ContextMenu id="file-menu-gltf">
<MenuItem onClick={this.onCreatePrefabFromGLTF}>Create Prefab...</MenuItem>
<MenuItem>Duplicate</MenuItem>
<MenuItem>Rename</MenuItem>
<MenuItem onClick={this.onCopyURL}>Copy URL</MenuItem>
<MenuItem>Delete</MenuItem>
</ContextMenu>
<ContextMenu id="current-directory-menu-default">
<MenuItem onClick={this.onNewFolder}>New Folder</MenuItem>
@ -292,4 +317,4 @@ class AssetExplorerPanelContainer extends Component {
}
}
export default withEditor(AssetExplorerPanelContainer);
export default withEditor(withDialog(withSceneActions(AssetExplorerPanelContainer)));

Просмотреть файл

@ -8,6 +8,7 @@ import { ContextMenu, MenuItem, ContextMenuTrigger, connectMenu } from "react-co
import styles from "./HierarchyPanelContainer.scss";
import { withEditor } from "../contexts/EditorContext";
import { withDialog } from "../contexts/DialogContext";
import { withSceneActions } from "../contexts/SceneActionsContext";
import "../../vendor/react-ui-tree/index.scss";
import "../../vendor/react-contextmenu/index.scss";
import { last } from "../../utils";
@ -23,7 +24,9 @@ class HierarchyPanelContainer extends Component {
static propTypes = {
path: PropTypes.array,
editor: PropTypes.object,
showDialog: PropTypes.func
sceneActions: PropTypes.object,
showDialog: PropTypes.func,
hideDialog: PropTypes.func
};
constructor(props) {
@ -47,17 +50,28 @@ class HierarchyPanelContainer extends Component {
editor.signals.objectSelected.add(this.rebuildNodeHierarchy);
}
onDropFile = file => {
if (file.ext === ".gltf" || file.ext === ".scene") {
if (file.uri === this.props.editor.sceneInfo.uri) {
this.props.showDialog(ErrorDialog, {
title: "Error adding prefab.",
message: "Scene cannot be added to itself."
});
return;
}
onDropFile = async item => {
if (item.file) {
const file = item.file;
this.props.editor.addSceneReferenceNode(file.name, file.uri);
if (file.ext === ".scene") {
try {
this.props.editor.addSceneReferenceNode(file.name, file.uri);
} catch (e) {
console.error(e);
this.props.showDialog(ErrorDialog, {
title: "Error adding prefab.",
message: e.message || "Error adding prefab."
});
}
} else if (file.ext === ".gltf" || file.ext === ".glb") {
const prefabPath = await this.props.sceneActions.onCreatePrefabFromGLTF(file.uri);
if (prefabPath) {
this.props.editor.addSceneReferenceNode(file.name, prefabPath);
}
}
}
};
@ -117,7 +131,7 @@ class HierarchyPanelContainer extends Component {
onEditPrefab = (object, refComponent) => {
const path = refComponent.getProperty("src");
this.props.editor.editScenePrefab(object, path);
this.props.sceneActions.onEditPrefab(object, path);
};
onDeleteSelected = e => {
@ -130,7 +144,7 @@ class HierarchyPanelContainer extends Component {
};
onClickBreadCrumb = () => {
this.props.editor.signals.popScene.dispatch();
this.props.sceneActions.onPopScene();
};
rebuildNodeHierarchy = () => {
@ -269,4 +283,4 @@ class HierarchyPanelContainer extends Component {
}
}
export default withEditor(withDialog(HierarchyPanelContainer));
export default withEditor(withDialog(withSceneActions(HierarchyPanelContainer)));

Просмотреть файл

@ -57,7 +57,7 @@ class PropertiesPanelContainer extends Component {
onObjectSelected = object => {
this.setState({
object,
name: object.name
name: object && object.name
});
};

Просмотреть файл

@ -4,6 +4,7 @@ import { HotKeys } from "react-hotkeys";
import Viewport from "../Viewport";
import { withEditor } from "../contexts/EditorContext";
import { withDialog } from "../contexts/DialogContext";
import { withSceneActions } from "../contexts/SceneActionsContext";
import ErrorDialog from "../dialogs/ErrorDialog";
import styles from "./ViewportPanelContainer.scss";
import FileDropTarget from "../FileDropTarget";
@ -11,7 +12,9 @@ import FileDropTarget from "../FileDropTarget";
class ViewportPanelContainer extends Component {
static propTypes = {
editor: PropTypes.object,
showDialog: PropTypes.func
sceneActions: PropTypes.object,
showDialog: PropTypes.func,
hideDialog: PropTypes.func
};
constructor(props) {
@ -34,17 +37,26 @@ class ViewportPanelContainer extends Component {
this.props.editor.createViewport(this.canvasRef.current);
}
onDropFile = file => {
if (file.ext === ".gltf" || file.ext === ".scene") {
if (file.uri === this.props.editor.sceneInfo.uri) {
this.props.showDialog(ErrorDialog, {
title: "Error adding prefab.",
message: "Scene cannot be added to itself."
});
return;
}
onDropFile = async item => {
if (item.file) {
const file = item.file;
this.props.editor.addSceneReferenceNode(file.name, file.uri);
if (file.ext === ".scene") {
try {
this.props.editor.addSceneReferenceNode(file.name, file.uri);
} catch (e) {
this.props.showDialog(ErrorDialog, {
title: "Error adding prefab.",
message: e.message
});
}
} else if (file.ext === ".gltf" || file.ext === ".glb") {
const prefabPath = await this.props.sceneActions.onCreatePrefabFromGLTF(file.uri);
if (prefabPath) {
this.props.editor.addSceneReferenceNode(file.name, prefabPath);
}
}
}
};
@ -82,4 +94,4 @@ class ViewportPanelContainer extends Component {
}
}
export default withEditor(withDialog(ViewportPanelContainer));
export default withEditor(withDialog(withSceneActions(ViewportPanelContainer)));