528 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import './css/Editor.css';
import { BorderLayout, LoadMask } from '../third_party';
import EditorMenuBar from './menu/EditorMenuBar.jsx';
import EditorStatusBar from './status/EditorStatusBar.jsx';
import EditorToolbar from './toolbar/EditorToolbar.jsx';
import Viewport from './viewport/Viewport.jsx';
import TimelinePanel from './timeline/TimelinePanel.jsx';
import EditorSideBar from './sidebar/EditorSideBar.jsx';
import AssetsPanel from './assets/AssetsPanel.jsx';
import History from '../command/History';
import Helpers from '../helper/Helpers';
import Visualization from '../visual/Visualization';
/**
* 编辑器
* @author tengge / https://github.com/tengge1
*/
class Editor extends React.Component {
constructor(props) {
super(props);
this.state = {
showMask: false,
maskText: _t('Waiting...'),
elements: [],
};
this.type = 'scene'; // 编辑器类型scene, mesh, texture, material, terrain, ai
this.onToggle = this.onToggle.bind(this);
}
render() {
const { showMask, maskText, elements } = this.state;
return <>
<BorderLayout className={'Editor'}>
<EditorMenuBar region={'north'}></EditorMenuBar>
<EditorStatusBar region={'south'}></EditorStatusBar>
<AssetsPanel region={'west'} split={true} onToggle={this.onToggle}></AssetsPanel>
<EditorSideBar region={'east'} split={true} onToggle={this.onToggle}></EditorSideBar>
<BorderLayout region={'center'}>
<EditorToolbar region={'west'}></EditorToolbar>
<Viewport region={'center'}></Viewport>
<TimelinePanel region={'south'} split={true} onToggle={this.onToggle}></TimelinePanel>
</BorderLayout>
</BorderLayout>
{elements.map((n, i) => {
return <div key={i}>{n}</div>;
})}
<LoadMask text={maskText} show={showMask}></LoadMask>
</>;
}
componentDidMount() {
app.editor = this;
// 基础
this.history = new History(this);
// 场景
this.scene = new THREE.Scene();
this.scene.name = _t('Scene');
this.scene.background = new THREE.Color(0xaaaaaa);
this.sceneHelpers = new THREE.Scene();
this.sceneID = null; // 当前场景ID
this.sceneName = null; // 当前场景名称
const width = app.viewport.clientWidth;
const height = app.viewport.clientHeight;
// 相机
this.DEFAULT_CAMERA = new THREE.PerspectiveCamera(50, width / height, 0.1, 10000);
this.DEFAULT_CAMERA.name = _t('DefaultCamera');
this.DEFAULT_CAMERA.userData.isDefault = true;
this.DEFAULT_CAMERA.userData.control = 'OrbitControls'; // 场景控制类型
this.DEFAULT_CAMERA.position.set(20, 10, 20);
this.DEFAULT_CAMERA.lookAt(new THREE.Vector3());
// 视图
this.view = 'perspective'; // perspective, front, side, top
// 透视相机
this.camera = this.DEFAULT_CAMERA.clone();
// 正交相机
this.orthCamera = new THREE.OrthographicCamera(-width / 4, width / 4, height / 4, -height / 4, 0.1, 10000);
// 渲染器
this.renderer = new THREE.WebGLRenderer({
antialias: true
});
this.renderer.gammaInput = false;
this.renderer.gammaOutput = false;
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
this.renderer.autoClear = false;
this.renderer.autoUpdateScene = false;
this.renderer.setPixelRatio(window.devicePixelRatio);
app.viewport.appendChild(this.renderer.domElement);
this.renderer.setSize(width, height);
// 物体
this.objects = [];
// 脚本 格式:{ uuid: { id: 'MongoDB _id', name: 'Script Name', type: 'Script Type', source: 'Source Code', uuid: 'uuid' }}
// 其中uuid是创建脚本时自动生成不可改变关联时使用id是mongo数据库ID字段name随便填写typejavascriptvertexShader, fragmentShader, jsonsource源码。
this.scripts = {};
// 动画格式:[{ id: 'MongoDB _id', uuid: 'uuid', layer: '动画层序号', layerName: '动画层名称', animations: '动画' }, ...]
// 其中,动画:[{ id: 'MongoDB _id', uuid: 'uuid', name: '动画名称', target: '动画对象uuid', type: '动画类型', beginTime: '开始时间(s)', endTime: '结束时间(s)', data: '动画参数' }, ...]
// 其中uuid是创建脚本时自动生成不可改变关联时使用。
// 动画层序号在时间面板显示位置从0开始计算。
// 动画类型Tween-补间动画Skeletal-骨骼动画Audio-音频播放Shader-着色器动画Filter-滤镜动画Particle-粒子动画
// 动画参数:是一个字典,根据动画类型不同,参数也不同
this.animations = [];
// 当前选中物体
this.selected = null;
// 平移旋转缩放控件
this.transformControls = new THREE.TransformControls(this.camera, app.viewport);
this.sceneHelpers.add(this.transformControls);
// 编辑器控件
this.controls = new THREE.EditorControls(this.camera, app.viewport);
// 碰撞检测
this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2();
// 帮助器场景灯光
var light = new THREE.DirectionalLight(0xffffff, 1.0);
light.position.z = 10;
this.sceneHelpers.add(light);
this.showViewHelper = true;
// 可视化
this.svg = app.svgRef;
this.visual = new Visualization();
// 事件
app.on(`appStarted.Editor`, this.onAppStarted.bind(this));
app.on(`mousedown.Editor`, this.onMouseDown.bind(this));
app.on(`mousemove.Editor`, this.onMouseMove.bind(this));
app.on(`showMask.Editor`, this.onShowMask.bind(this));
// 帮助器
this.helpers = new Helpers(app);
app.call('appStart', this);
app.call('appStarted', this);
app.call('resize', this);
app.log(_t('Program started.'));
}
componentWillUnmount() {
app.call('appStop', this);
app.call('appStoped', this);
app.log(_t('Program stoped.'));
app.event.stop();
}
onAppStarted() {
this.helpers.start();
this.clear();
this._addAudioListener = this._addAudioListener.bind(this);
document.addEventListener('click', this._addAudioListener);
}
onToggle(expanded) {
app.call('resize', this);
}
// -------------------- 场景 --------------------------
setScene(scene) { // 设置场景
// 移除原有物体
var objects = this.scene.children;
while (objects.length > 0) {
this.removeObject(objects[0]);
}
// 添加新物体
var children = scene.children.slice();
scene.children.length = 0;
this.scene = scene;
children.forEach(n => {
this.addObject(n);
});
app.call('sceneGraphChanged', this);
}
clear(addObject = true) { // 清空场景
this.history.clear();
this.camera.copy(this.DEFAULT_CAMERA);
if (this.audioListener && this.camera.children.findIndex(o => o instanceof THREE.AudioListener) === -1) {
this.camera.add(this.audioListener);
}
if (this.scene.background instanceof THREE.Texture) {
this.scene.background = new THREE.Color(0xaaaaaa);
} else if (this.scene.background instanceof THREE.Color) {
this.scene.background.setHex(0xaaaaaa);
}
this.scene.fog = null;
this.deselect();
// 移除场景物体
var objects = this.scene.children;
while (objects.length > 0) {
this.removeObject(objects[0]);
}
this.scripts = {};
this.animations = [{
id: null,
uuid: THREE.Math.generateUUID(),
layer: 0,
layerName: _t('AnimLayer1'),
animations: []
}, {
id: null,
uuid: THREE.Math.generateUUID(),
layer: 1,
layerName: _t('AnimLayer2'),
animations: []
}, {
id: null,
uuid: THREE.Math.generateUUID(),
layer: 2,
layerName: _t('AnimLayer3'),
animations: []
}];
// 添加默认元素
if (addObject) {
var light1 = new THREE.AmbientLight(0xffffff, 0.24);
light1.name = _t('Ambient');
this.addObject(light1);
var light2 = new THREE.DirectionalLight(0xffffff, 0.56);
light2.name = _t('Directional');
light2.castShadow = true;
light2.position.set(5, 10, 7.5);
light2.shadow.radius = 0;
light2.shadow.mapSize.x = 2048;
light2.shadow.mapSize.y = 2048;
light2.shadow.camera.left = -20;
light2.shadow.camera.right = 20;
light2.shadow.camera.top = 20;
light2.shadow.camera.bottom = -20;
light2.shadow.camera.near = 0.1;
light2.shadow.camera.far = 500;
this.addObject(light2);
}
app.call('editorCleared', this);
app.call('scriptChanged', this);
app.call('animationChanged', this);
}
// 点击编辑器时才添加AudioListener避免警告信息
_addAudioListener() {
document.removeEventListener('click', this._addAudioListener);
this.audioListener = new THREE.AudioListener();
this.audioListener.name = _t('AudioListener');
if (this.camera.children.findIndex(o => o instanceof THREE.AudioListener) === -1) {
this.camera.add(this.audioListener);
}
}
// ---------------------- 物体 ---------------------------
objectByUuid(uuid) { // 根据uuid获取物体
return this.scene.getObjectByProperty('uuid', uuid, true);
}
addObject(object) { // 添加物体
this.scene.add(object);
app.call('objectAdded', this, object);
app.call('sceneGraphChanged', this);
}
moveObject(object, parent, before) { // 移动物体
if (parent === undefined) {
parent = this.scene;
}
parent.add(object);
// sort children array
if (before !== undefined) {
var index = parent.children.indexOf(before);
parent.children.splice(index, 0, object);
parent.children.pop();
}
app.call('sceneGraphChanged', this);
}
removeObject(object) { // 移除物体
if (object.parent === null) { // 避免删除相机或场景
return;
}
object.parent.remove(object);
app.call('objectRemoved', this, object);
app.call('sceneGraphChanged', this);
}
// ------------------------- 帮助 ------------------------------
addPhysicsHelper(helper) {
var geometry = new THREE.SphereBufferGeometry(2, 4, 2);
var material = new THREE.MeshBasicMaterial({
color: 0xff0000,
visible: false
});
var picker = new THREE.Mesh(geometry, material);
picker.name = 'picker';
picker.userData.object = helper.object;
helper.add(picker);
this.sceneHelpers.add(helper);
this.helpers[helper.object.id] = helper;
this.objects.push(picker);
}
removePhysicsHelper(helper) {
if (this.helpers[helper.object.id] !== undefined) {
var helper = this.helpers[helper.object.id];
helper.parent.remove(helper);
delete this.helpers[helper.object.id];
var objects = this.objects;
objects.splice(objects.indexOf(helper.getObjectByName('picker')), 1);
}
}
// ------------------------ 脚本 ----------------------------
addScript(object, script) { // 添加脚本
if (this.scripts[object.uuid] === undefined) {
this.scripts[object.uuid] = [];
}
this.scripts[object.uuid].push(script);
app.call('scriptAdded', this, script);
}
removeScript(object, script) { // 移除脚本
if (this.scripts[object.uuid] === undefined) {
return;
}
var index = this.scripts[object.uuid].indexOf(script);
if (index !== -1) {
this.scripts[object.uuid].splice(index, 1);
}
app.call('scriptRemoved', this);
}
// ------------------------ 选中事件 --------------------------------
select(object) { // 选中物体
if (this.selected === object) {
return;
}
this.selected = object;
if (!object) {
this.transformControls.detach()
}
app.call('objectSelected', this, object);
}
selectById(id) { // 根据id选中物体
if (id === this.camera.id) {
this.select(this.camera);
return;
}
this.select(this.scene.getObjectById(id, true));
}
selectByUuid(uuid) { // 根据uuid选中物体
if (uuid === this.camera.uuid) {
this.select(this.camera);
return;
}
this.scene.traverse(child => {
if (child.uuid === uuid) {
this.select(child);
}
});
}
deselect() { // 取消选中物体
this.select(null);
}
// ---------------------- 焦点事件 --------------------------
focus(object) { // 设置焦点
app.call('objectFocused', this, object);
}
focusById(id) { // 根据id设置交点
var obj = this.scene.getObjectById(id, true);
if (obj) {
this.focus(obj);
}
}
focusByUUID(uuid) { // 根据uuid设置焦点
if (uuid === this.camera.uuid) {
this.focus(this.camera);
return;
}
this.scene.traverse(child => {
if (child.uuid === uuid) {
this.focus(child);
}
});
}
// ----------------------- 命令事件 --------------------------
execute(cmd, optionalName) { // 执行事件
this.history.execute(cmd, optionalName);
}
undo() { // 撤销事件
this.history.undo();
}
redo() { // 重做事件
this.history.redo();
}
// ---------------------- 碰撞检测 -----------------------------
onMouseDown(event) {
this.raycaster.setFromCamera(this.mouse, this.camera);
var intersect = this.raycaster.intersectObjects(this.scene.children, true)[0];
if (intersect) {
app.call(`intersect`, this, intersect, event);
}
}
onMouseMove(event) {
this.mouse.x = (event.offsetX / this.renderer.domElement.clientWidth) * 2 - 1;
this.mouse.y = -(event.offsetY / this.renderer.domElement.clientHeight) * 2 + 1;
}
// ---------------------- 用户界面 --------------------------------
createElement(type, props = {}, children = undefined) {
let ref = React.createRef();
props.ref = ref;
return React.createElement(type, props, children);
}
addElement(element, callback) {
let elements = this.state.elements;
elements.push(element);
this.setState({ elements }, callback);
}
removeElement(element, callback) {
let elements = this.state.elements;
let index = elements.findIndex(n => n === element || n.ref && n.ref.current === element)
if (index > -1) {
elements.splice(index, 1);
}
this.setState({ elements }, callback);
}
onShowMask(enabled, text) {
this.setState({
showMask: enabled,
maskText: text || _t('Waiting...'),
});
}
}
export default Editor;