зеркало из https://github.com/mozilla/Spoke.git
Merge master
This commit is contained in:
Коммит
2fd230b68e
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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)));
|
||||
|
|
Загрузка…
Ссылка в новой задаче