Merge branch 'master' of github.com:MozillaReality/Spoke into feature/url-model-resolution

This commit is contained in:
Brian Peiris 2018-09-12 15:41:05 -07:00
Родитель 7d5e92efce 1559156154
Коммит 7fce81b522
15 изменённых файлов: 195 добавлений и 138 удалений

6
package-lock.json сгенерированный
Просмотреть файл

@ -11648,9 +11648,9 @@
}
},
"react-contextmenu": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/react-contextmenu/-/react-contextmenu-2.9.2.tgz",
"integrity": "sha512-DdcO6iLBIJuDVsRpJLG/9N6ine0OVZhuQvnSPCEihfcyJFz+SHU9pQo+w9LWi2PdUxFbFV52BwAuutQkAYJxaA==",
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/react-contextmenu/-/react-contextmenu-2.9.3.tgz",
"integrity": "sha512-KFufA4xbBLyJOcWtw6fpM3nW7GcnRIbP1iv7hzJi4Wt1SN7iV62okubjFOk7GOi6Nz3QHncHIcvtX4NRiqMbmA==",
"requires": {
"classnames": "^2.2.5",
"object-assign": "^4.1.0"

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

@ -39,6 +39,7 @@
"src/**/*.js"
],
"require": [
"babel-register",
"esm",
"./test/helpers/browser-env-entry.js"
]
@ -73,7 +74,7 @@
"rc-slider": "^8.6.1",
"react": "^16.4.0",
"react-color": "^2.14.1",
"react-contextmenu": "^2.9.2",
"react-contextmenu": "^2.9.3",
"react-dnd": "^2.6.0",
"react-dom": "^16.4.0",
"react-hotkeys": "^1.1.4",

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

@ -1,4 +1,4 @@
const namePattern = new RegExp("(.*)_d+$");
const namePattern = new RegExp("(.*)_\\d+$");
function nodesToTree(nodes) {
if (!nodes) {
return;
@ -75,17 +75,16 @@ export default class ConflictHandler {
const name = node.name;
if (this._duplicateNameCounters.has(name)) {
const n = this._duplicateNameCounters.get(name) + 1;
this._duplicateNameCounters.set(name, n);
node.userData._resolvedName = name + "_" + n;
const nameObj = this._duplicateNameCounters.get(name);
nameObj.count++;
node.userData._resolvedName = name + "_" + nameObj.count;
this._updatedNodes.set(this._hashTreePath(node.userData._path), node.userData._resolvedName);
} else {
this._duplicateNameCounters.set(name, 0);
this._duplicateNameCounters.set(name, { used: true, count: 0 });
const cacheName = getNameWithoutIndex(name);
if (name !== cacheName) {
if (!this.isUniqueObjectName(cacheName)) {
const n = this._duplicateNameCounters.get(cacheName) + 1;
this._duplicateNameCounters.set(cacheName, n);
this._duplicateNameCounters.get(cacheName).count++;
}
}
}
@ -130,8 +129,8 @@ export default class ConflictHandler {
};
_updateDuplicateStatus = () => {
for (const value of this._duplicateNameCounters.values()) {
if (value > 0) {
for (const nameObj of this._duplicateNameCounters.values()) {
if (nameObj.count > 0) {
this.setDuplicateStatus(true);
break;
}
@ -216,7 +215,8 @@ export default class ConflictHandler {
};
isUniqueObjectName = name => {
return !this._duplicateNameCounters.has(name);
const nameObj = this._duplicateNameCounters.get(name);
return !nameObj || !nameObj.used;
};
updateDuplicateNameCounters = scene => {
@ -230,26 +230,52 @@ export default class ConflictHandler {
if (difference.length > 0) {
for (const name of difference) {
this._duplicateNameCounters.delete(name);
const nameObj = this._duplicateNameCounters.get(name);
if (nameObj.count === 0) {
this._duplicateNameCounters.delete(name);
} else {
nameObj.used = false;
}
}
}
};
addToDuplicateNameCounters = name => {
if (this.isUniqueObjectName(name)) {
this._duplicateNameCounters.set(name, 0);
removeFromDuplicateNameCounters = name => {
const cacheName = getNameWithoutIndex(name);
const nameObj = this._duplicateNameCounters.get(name);
if (nameObj === undefined) return;
if (name === cacheName) {
nameObj.used = false;
} else {
let n = this._duplicateNameCounters.get(name) + 1;
let newName = name + "_" + n;
this._duplicateNameCounters.delete(name);
}
};
addToDuplicateNameCounters = name => {
const cacheName = getNameWithoutIndex(name);
if (this.isUniqueObjectName(name)) {
const nameObj = this._duplicateNameCounters.get(cacheName);
if (name === cacheName) {
if (nameObj) {
nameObj.used = true;
} else {
this._duplicateNameCounters.set(cacheName, { used: true, count: 0 });
}
} else {
this._duplicateNameCounters.set(name, { used: true, count: 0 });
}
return name;
} else {
let n = this._duplicateNameCounters.get(cacheName).count + 1;
let newName = cacheName + "_" + n;
while (!this.isUniqueObjectName(newName)) {
n += 1;
newName = name + "_" + n;
newName = cacheName + "_" + n;
}
this._duplicateNameCounters.set(name, n);
this._duplicateNameCounters.set(newName, 0);
this._duplicateNameCounters.get(cacheName).count = n;
this._duplicateNameCounters.set(newName, { used: true });
return newName;
}
return name;
};
resolveConflicts = scene => {

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

@ -6,7 +6,6 @@ import Viewport from "./Viewport";
import RemoveObjectCommand from "./commands/RemoveObjectCommand";
import AddObjectCommand from "./commands/AddObjectCommand";
import AddComponentCommand from "./commands/AddComponentCommand";
import SetValueCommand from "./commands/SetValueCommand";
import RemoveComponentCommand from "./commands/RemoveComponentCommand";
import SetComponentPropertyCommand from "./commands/SetComponentPropertyCommand";
import MoveObjectCommand from "./commands/MoveObjectCommand";
@ -259,6 +258,8 @@ export default class Editor {
const scene = new THREE.Scene();
scene.name = "Scene";
this._conflictHandler = null;
this._setSceneInfo(scene, null);
this.scenes = [this.sceneInfo];
@ -823,7 +824,13 @@ export default class Editor {
index[i] = i + 1;
}
const { navPosition, navIndex } = await this.project.generateNavMesh(position, index);
const box = new THREE.Box3().setFromBufferAttribute(finalGeo.attributes.position);
const size = new THREE.Vector3();
box.getSize(size);
const area = size.x * size.z;
// Tuned to produce cell sizes from ~0.5 to ~1.5 for areas from ~200 to ~350,000.
const cellSize = Math.pow(area, 1 / 3) / 50;
const { navPosition, navIndex } = await this.project.generateNavMesh(position, index, cellSize);
const navGeo = new THREE.BufferGeometry();
navGeo.setIndex(navIndex);
@ -1179,6 +1186,7 @@ export default class Editor {
});
object.parent.remove(object);
this._conflictHandler.removeFromDuplicateNameCounters(object.name);
this.signals.objectRemoved.dispatch(object);
this.signals.sceneGraphChanged.dispatch();
@ -1502,11 +1510,11 @@ export default class Editor {
setObjectName(object, value) {
const handler = this._conflictHandler;
if (handler.isUniqueObjectName(value)) {
object.name = value;
const prevName = object.name;
handler.addToDuplicateNameCounters(value);
this.execute(new SetValueCommand(object, "name", value));
object.name = value;
handler.removeFromDuplicateNameCounters(prevName);
} else {
this.signals.objectChanged.dispatch(object);
throw new ConflictError("rename error", "rename", this.sceneInfo.uri, handler);
@ -1629,8 +1637,9 @@ export default class Editor {
}
deleteObject(object) {
const objectName = object.name;
this.execute(new RemoveObjectCommand(object));
this._conflictHandler.updateDuplicateNameCounters(this.scene);
this._conflictHandler.removeFromDuplicateNameCounters(objectName);
}
deleteSelectedObject() {

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

@ -162,12 +162,13 @@ export default class Project extends EventEmitter {
return Promise.resolve(this);
}
async generateNavMesh(position, index) {
async generateNavMesh(position, index, cellSize) {
const positionBlob = new Blob([new Float32Array(position)], { type: "application/octet-stream" });
const indexBlob = new Blob([new Int32Array(index)], { type: "application/octet-stream" });
const formData = new FormData();
formData.append("position", positionBlob);
formData.append("index", indexBlob);
formData.append("cellSize", cellSize);
const res = await this.fetch("/api/navmesh", { method: "POST", body: formData });
const json = await res.json();

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

@ -0,0 +1,23 @@
import Command from "../Command";
export default class SetNameCommand extends Command {
constructor(object, newName) {
super();
this.type = "SetNameCommand";
this.object = object;
this.oldName = object !== undefined ? object.name : undefined;
this.newName = newName;
}
execute() {
this.editor.setObjectName(this.object, this.newName);
this.editor.signals.objectChanged.dispatch(this.object);
}
undo() {
this.editor.setObjectName(this.object, this.oldName);
this.editor.signals.objectChanged.dispatch(this.object);
}
}

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

@ -1,42 +0,0 @@
import Command from "../Command";
/**
* @author dforrer / https://github.com/dforrer
* Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
*/
/**
* @param object THREE.Object3D
* @param attributeName string
* @param newValue number, string, boolean or object
* @constructor
*/
export default class SetValueCommand extends Command {
constructor(object, attributeName, newValue) {
super();
this.type = "SetValueCommand";
this.name = "Set " + attributeName;
this.updatable = true;
this.object = object;
this.attributeName = attributeName;
this.oldValue = object !== undefined ? object[attributeName] : undefined;
this.newValue = newValue;
}
execute() {
this.object[this.attributeName] = this.newValue;
this.editor.signals.objectChanged.dispatch(this.object);
}
undo() {
this.object[this.attributeName] = this.oldValue;
this.editor.signals.objectChanged.dispatch(this.object);
}
update(cmd) {
this.newValue = cmd.newValue;
}
}

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

@ -3,6 +3,7 @@ import React from "react";
import EditorContainer from "./ui/EditorContainer";
import Editor from "./editor/Editor";
import Project from "./editor/Project";
import qsTruthy from "./utils/qs-truthy.js";
import "./global.scss";
const rootEl = document.createElement("div");
@ -13,4 +14,6 @@ const project = new Project();
const editor = new Editor(project);
window.editor = editor;
ReactDOM.render(<EditorContainer editor={editor} />, rootEl);
const uiMode = qsTruthy("advanced") ? "advanced" : "basic";
ReactDOM.render(<EditorContainer uiMode={uiMode} editor={editor} />, rootEl);

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

@ -29,8 +29,20 @@ function isInputSelected() {
}
class EditorContainer extends Component {
static defaultProps = {
initialPanels: {
static initialPanels = {
basic: {
direction: "row",
first: {
direction: "row",
first: "hierarchy",
second: "viewport",
splitPercentage: 25
},
second: "properties",
splitPercentage: 75
},
advanced: {
direction: "column",
first: {
direction: "row",
@ -49,8 +61,8 @@ class EditorContainer extends Component {
};
static propTypes = {
initialPanels: PropTypes.object,
editor: PropTypes.object
editor: PropTypes.object,
uiMode: PropTypes.string
};
constructor(props) {
@ -679,7 +691,7 @@ class EditorContainer extends Component {
render() {
const { openModal, menus, DialogComponent, dialogProps } = this.state;
const { initialPanels, editor } = this.props;
const { editor } = this.props;
return (
<DragDropContextProvider backend={HTML5Backend}>
@ -691,7 +703,7 @@ class EditorContainer extends Component {
<MosaicWithoutDragDropContext
className="mosaic-theme"
renderTile={this.renderPanel}
initialValue={initialPanels}
initialValue={EditorContainer.initialPanels[this.props.uiMode]}
onChange={this.onPanelChange}
/>
<Modal

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

@ -13,6 +13,7 @@ import FileDialog from "../dialogs/FileDialog";
import ErrorDialog from "../dialogs/ErrorDialog";
import ProgressDialog, { PROGRESS_DIALOG_DELAY } from "../dialogs/ProgressDialog";
import AddComponentDropdown from "../AddComponentDropdown";
import SetNameCommand from "../../editor/commands/SetNameCommand";
export function getDisplayName(name) {
if (name.includes("-")) {
@ -75,7 +76,15 @@ class PropertiesPanelContainer extends Component {
};
onBlurName = () => {
this.props.editor.setObjectName(this.state.object, this.state.name);
if (this.state.object.name !== this.state.name) {
this.props.editor.execute(new SetNameCommand(this.state.object, this.state.name));
}
};
onKeyUpName = e => {
if (e.key === "Enter") {
this.props.editor.execute(new SetNameCommand(this.state.object, this.state.name));
}
};
onUpdateStatic = ({ value }) => {
@ -352,7 +361,12 @@ class PropertiesPanelContainer extends Component {
>
<div className={styles.propertiesPanelTopBar}>
<InputGroup className={styles.topBarName} name="Name">
<StringInput value={this.state.name} onChange={this.onUpdateName} onBlur={this.onBlurName} />
<StringInput
value={this.state.name}
onChange={this.onUpdateName}
onBlur={this.onBlurName}
onKeyUp={this.onKeyUpName}
/>
</InputGroup>
<InputGroup className={styles.topBarStatic} name="Static">
<Select

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

@ -1,42 +0,0 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { withEditor } from "../contexts/EditorContext";
import styles from "./ViewportPanelToolbarContainer.scss";
class ViewportPanelToolbarContainer extends Component {
static propTypes = {
editor: PropTypes.object
};
constructor(props) {
super(props);
this.state = {
snapEnabled: false
};
this.props.editor.signals.viewportInitialized.add(viewport => {
this.setState({ snapEnabled: viewport.snapEnabled });
});
}
toggleSnap(snapEnabled) {
this.setState({ snapEnabled }, () => {
this.props.editor.signals.snapToggled.dispatch(snapEnabled);
});
}
render() {
return (
<label className={styles.snap}>
Snap:{" "}
<input
type="checkbox"
className={styles.snapInput}
checked={this.state.snapEnabled}
onChange={e => this.toggleSnap(e.target.checked)}
/>
</label>
);
}
}
export default withEditor(ViewportPanelToolbarContainer);

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

@ -1,13 +0,0 @@
@import "../../common";
@import "../../theme";
:local(.snap) {
@include no-select;
font-size: 12px;
}
:local(.snapInput) {
margin: 0;
vertical-align: top;
}

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

@ -0,0 +1,7 @@
const qs = new URLSearchParams(location.search);
export default function qsTruthy(param) {
const val = qs.get(param);
// if the param exists but is not set (e.g. "?foo&bar"), its value is the empty string.
return val === "" || /1|on|true/i.test(val);
}

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

@ -290,7 +290,7 @@ export default async function startServer(options) {
]);
recast.load(new Float32Array(position.buffer), new Int32Array(index.buffer));
const objMesh = recast.build(
0.15, // cellSize
parseFloat(ctx.request.body.cellSize),
0.1, // cellHeight
1.0, // agentHeight
0.0001, // agentRadius

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

@ -0,0 +1,58 @@
import ConflictHandler from "../../../src/client/editor/ConflictHandler";
import test from "ava";
test("Reused names are given an index", t => {
const handler = new ConflictHandler();
t.is("new", handler.addToDuplicateNameCounters("new"));
t.is("new_1", handler.addToDuplicateNameCounters("new"));
t.is("new_2", handler.addToDuplicateNameCounters("new"));
t.is("foo", handler.addToDuplicateNameCounters("foo"));
});
test("Name can be reused if removed", t => {
const handler = new ConflictHandler();
t.is("new", handler.addToDuplicateNameCounters("new"));
handler.removeFromDuplicateNameCounters("new");
t.is("new", handler.addToDuplicateNameCounters("new"));
});
test("Indexed name can be reused if removed", t => {
const handler = new ConflictHandler();
t.is("foo_1", handler.addToDuplicateNameCounters("foo_1"));
handler.removeFromDuplicateNameCounters("foo_1");
t.is("foo_1", handler.addToDuplicateNameCounters("foo_1"));
});
test("Indexing works when previous name is removed and readded", t => {
const handler = new ConflictHandler();
t.is("new", handler.addToDuplicateNameCounters("new"));
t.is("new_1", handler.addToDuplicateNameCounters("new"));
handler.removeFromDuplicateNameCounters("new");
t.is("new", handler.addToDuplicateNameCounters("new"));
t.is("new_2", handler.addToDuplicateNameCounters("new"));
});
test("Name can be used if index is used first", t => {
const handler = new ConflictHandler();
t.is("new_1", handler.addToDuplicateNameCounters("new_1"));
t.is("new", handler.addToDuplicateNameCounters("new"));
t.is("new_2", handler.addToDuplicateNameCounters("new"));
});
test("Reuse should be disallowed even after indexed name is removed", t => {
const handler = new ConflictHandler();
t.is("foo", handler.addToDuplicateNameCounters("foo"));
t.is("foo_1", handler.addToDuplicateNameCounters("foo_1"));
handler.removeFromDuplicateNameCounters("foo_1");
t.false(handler.isUniqueObjectName("foo"));
});
test("Indexing increments even after intermediate index is removed", t => {
const handler = new ConflictHandler();
t.is("foo", handler.addToDuplicateNameCounters("foo"));
t.is("foo_1", handler.addToDuplicateNameCounters("foo"));
handler.removeFromDuplicateNameCounters("foo_1");
t.is("foo_2", handler.addToDuplicateNameCounters("foo"));
});