зеркало из https://github.com/mozilla/Spoke.git
Merge branch 'master' of github.com:MozillaReality/Spoke into feature/url-model-resolution
This commit is contained in:
Коммит
7fce81b522
|
@ -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"));
|
||||
});
|
Загрузка…
Ссылка в новой задаче