feat: 新增钢铁厂数字孪生平台基础架构和功能模块
- 添加Three.js基础场景配置和核心功能模块 - 实现模型加载器、动画循环和交互选择器 - 添加温度、压力等仪表盘组件 - 配置Vite构建工具和ESLint规范 - 添加基础UI组件和布局系统 - 实现数据可视化图表组件 - 配置Nginx部署文件 - 添加钢铁厂设备数据模型
This commit is contained in:
291
apps/steelmill/src/three/controls.ts
Normal file
291
apps/steelmill/src/three/controls.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
import { OrbitControls } from 'three/examples/jsm/Addons.js';
|
||||
import TWEEN, { Easing } from '@tweenjs/tween.js';
|
||||
import * as THREE from 'three';
|
||||
|
||||
const focusEvent = { type: 'focus' };
|
||||
const changeEvent = { type: 'change' };
|
||||
|
||||
type ControlOptions = {
|
||||
autoRotate: boolean;
|
||||
autoRotateSpeed: number;
|
||||
minDistance: number;
|
||||
maxDistance: number;
|
||||
dampingFactor: number;
|
||||
enableDamping: boolean;
|
||||
enabled: boolean;
|
||||
};
|
||||
class PlayerControls extends THREE.EventDispatcher<any> {
|
||||
private object: THREE.Camera;
|
||||
private domElement: HTMLElement;
|
||||
private orbitControls: OrbitControls;
|
||||
enabled: boolean = true;
|
||||
constructor(object: THREE.Camera, domElement: HTMLElement, options?: ControlOptions) {
|
||||
super();
|
||||
this.object = object;
|
||||
this.domElement = domElement;
|
||||
this.orbitControls = new OrbitControls(this.object, this.domElement);
|
||||
this.orbitControls.minDistance = options?.minDistance || 5;
|
||||
this.orbitControls.maxDistance = options?.maxDistance || 100;
|
||||
this.orbitControls.enableDamping = options?.enableDamping || false; //启用阻尼
|
||||
this.orbitControls.enabled = options?.enabled ?? true;
|
||||
this.orbitControls.maxPolarAngle = THREE.MathUtils.degToRad(90);
|
||||
this.addDomEvents();
|
||||
|
||||
this.orbitControls.addEventListener('change', () => {
|
||||
// 浏览器控制台查看相机位置变化
|
||||
this.dispatchEvent(changeEvent);
|
||||
});
|
||||
}
|
||||
private addDomEvents() {
|
||||
this.domElement.addEventListener('click', this.onClick);
|
||||
this.domElement.addEventListener('pointermove', this.onPointerMove);
|
||||
this.domElement.addEventListener('dblclick', this.onDoubleClick.bind(this));
|
||||
}
|
||||
|
||||
private onClick(_event: MouseEvent) {}
|
||||
private onDoubleClick() {
|
||||
this.reset();
|
||||
}
|
||||
private onPointerMove() {}
|
||||
|
||||
public reset(animate: boolean = true) {
|
||||
if (!animate) {
|
||||
this.orbitControls.reset();
|
||||
return;
|
||||
}
|
||||
const camera = this.object;
|
||||
const controls = this.orbitControls;
|
||||
|
||||
const target = controls.target0.clone();
|
||||
const position = controls.position0.clone();
|
||||
|
||||
new TWEEN.Tween({
|
||||
// target: controls.target,
|
||||
tx: controls.target.x,
|
||||
ty: controls.target.y,
|
||||
tz: controls.target.z,
|
||||
x: camera.position.x,
|
||||
y: camera.position.y,
|
||||
z: camera.position.z,
|
||||
// position: controls.object.position,
|
||||
// zoom: controls.object.,
|
||||
})
|
||||
.to(
|
||||
{
|
||||
tx: target.x,
|
||||
ty: target.y,
|
||||
tz: target.z,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
z: position.z,
|
||||
// target: target,
|
||||
// position: position,
|
||||
// zoom: zoom,
|
||||
},
|
||||
1000
|
||||
)
|
||||
.easing(Easing.Quadratic.Out)
|
||||
.onUpdate(function (obj) {
|
||||
// 动态改变相机位置
|
||||
camera.position.set(obj.x, obj.y, obj.z);
|
||||
// 动态计算相机视线
|
||||
// camera.lookAt(obj.tx, obj.ty, obj.tz);
|
||||
controls.target.set(obj.tx, obj.ty, obj.tz);
|
||||
controls.update(); //内部会执行.lookAt()
|
||||
})
|
||||
.start();
|
||||
}
|
||||
public saveState() {
|
||||
this.orbitControls.saveState();
|
||||
}
|
||||
public update(deltaTime?: number): boolean {
|
||||
return this.orbitControls.update(deltaTime);
|
||||
}
|
||||
/**
|
||||
* Returns the distance from the camera to the target.
|
||||
* @returns number
|
||||
*/
|
||||
public getDistance() {
|
||||
return this.orbitControls.getDistance();
|
||||
}
|
||||
get maxDistance() {
|
||||
return this.orbitControls.maxDistance;
|
||||
}
|
||||
set maxDistance(value: number) {
|
||||
this.orbitControls.maxDistance = value;
|
||||
}
|
||||
set minDistance(value: number) {
|
||||
this.orbitControls.minDistance = value;
|
||||
}
|
||||
set dampingFactor(value: number) {
|
||||
this.orbitControls.dampingFactor = value;
|
||||
}
|
||||
set enableDamping(value: boolean) {
|
||||
this.orbitControls.enableDamping = value;
|
||||
}
|
||||
set autoRotate(value: boolean) {
|
||||
this.orbitControls.autoRotate = value;
|
||||
}
|
||||
set autoRotateSpeed(value: number) {
|
||||
this.orbitControls.autoRotateSpeed = value;
|
||||
}
|
||||
set maxPolarAngle(value: number) {
|
||||
this.orbitControls.maxPolarAngle = value;
|
||||
}
|
||||
set minPolarAngle(value: number) {
|
||||
this.orbitControls.minPolarAngle = value;
|
||||
}
|
||||
set minAzimuthAngle(value: number) {
|
||||
this.orbitControls.minAzimuthAngle = value;
|
||||
}
|
||||
set maxAzimuthAngle(value: number) {
|
||||
this.orbitControls.maxAzimuthAngle = value;
|
||||
}
|
||||
set enablePan(value: boolean) {
|
||||
this.orbitControls.enablePan = value;
|
||||
}
|
||||
/**
|
||||
* 将相机聚焦到指定的物体,并拉近相机距离
|
||||
* @param target Object3D - 聚焦的目标
|
||||
* @param options.scalar - number default 5 , 相机位置的矩阵乘该标量值用于与聚焦对象拉开一定距离
|
||||
*/
|
||||
public focus(
|
||||
target: THREE.Object3D,
|
||||
options?: {
|
||||
scalar?: number;
|
||||
angle?: number;
|
||||
}
|
||||
) {
|
||||
const _DEFAULT_OPTIONS = {
|
||||
scalar: 5,
|
||||
};
|
||||
const camera = this.object;
|
||||
const controls = this.orbitControls;
|
||||
|
||||
/** 相机位置与focus目标拉的偏移量 */
|
||||
const scalar = options?.scalar !== undefined ? options.scalar : _DEFAULT_OPTIONS.scalar;
|
||||
const angle = options?.angle !== undefined ? options.angle : -Math.PI / 4;
|
||||
|
||||
const box = new THREE.Box3();
|
||||
const center = new THREE.Vector3();
|
||||
const sphere = new THREE.Sphere();
|
||||
// 相机移动的偏移量
|
||||
const delta = new THREE.Vector3();
|
||||
let distance = 0;
|
||||
// 获取目标对象的包围盒
|
||||
box.setFromObject(target);
|
||||
box.getCenter(center);
|
||||
distance = box.getBoundingSphere(sphere).radius;
|
||||
|
||||
const quaternion = new THREE.Quaternion();
|
||||
target.getWorldQuaternion(quaternion);
|
||||
quaternion.copy(camera.quaternion);
|
||||
|
||||
quaternion.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 1), angle));
|
||||
delta.set(0, 0, 1);
|
||||
delta.applyQuaternion(quaternion);
|
||||
delta.multiplyScalar(distance * scalar);
|
||||
|
||||
const cameraPositionStart = camera.position.clone();
|
||||
const cameraPositionEnd = center.clone().add(delta);
|
||||
// 相机初始观察点
|
||||
const targetStartPoint: THREE.Vector3 = controls.target.clone();
|
||||
const targetEndPoint = center.clone();
|
||||
const eventdispatcher = this.dispatchEvent.bind(this);
|
||||
new TWEEN.Tween({
|
||||
// 不管相机此刻处于什么状态,直接读取当前的位置和目标观察点
|
||||
x: cameraPositionStart.x,
|
||||
y: cameraPositionStart.y,
|
||||
z: cameraPositionStart.z,
|
||||
tx: targetStartPoint.x,
|
||||
ty: targetStartPoint.y,
|
||||
tz: targetStartPoint.z,
|
||||
})
|
||||
.to(
|
||||
{
|
||||
// 动画结束相机位置坐标
|
||||
x: cameraPositionEnd.x,
|
||||
y: cameraPositionEnd.y,
|
||||
z: cameraPositionEnd.z,
|
||||
tx: targetEndPoint.x,
|
||||
ty: targetEndPoint.y,
|
||||
tz: targetEndPoint.z,
|
||||
},
|
||||
1000
|
||||
)
|
||||
.easing(Easing.Quadratic.Out)
|
||||
.onUpdate(function (obj) {
|
||||
// 动态改变相机位置
|
||||
camera.position.set(obj.x, obj.y, obj.z);
|
||||
// 设置相机的视线
|
||||
controls.target.set(obj.tx, obj.ty, obj.tz);
|
||||
controls.update();
|
||||
})
|
||||
.onComplete(() => {
|
||||
eventdispatcher(focusEvent);
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
focus2(
|
||||
target: THREE.Object3D,
|
||||
options?: {
|
||||
scalar?: number;
|
||||
}
|
||||
) {
|
||||
const _DEFAULT_OPTIONS = {
|
||||
scalar: 14,
|
||||
};
|
||||
const scalar = options?.scalar !== undefined ? options.scalar : _DEFAULT_OPTIONS.scalar;
|
||||
const camera = this.object;
|
||||
const controls = this.orbitControls;
|
||||
const pos = new THREE.Vector3();
|
||||
target.getWorldPosition(pos);
|
||||
// 相机飞行到的位置和观察目标拉开一定的距离
|
||||
const endPos = pos.clone().addScalar(scalar);
|
||||
|
||||
// const quaternion = new THREE.Quaternion();
|
||||
// target.getWorldQuaternion(quaternion);
|
||||
// quaternion.copy(camera.quaternion);
|
||||
// // //相机以45度角俯视
|
||||
// quaternion.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 1), Math.PI / 4));
|
||||
// endPos.applyQuaternion(quaternion);
|
||||
const endTarget = pos;
|
||||
|
||||
const cameraStartPos = camera.position;
|
||||
const controlsPos = controls.target;
|
||||
new TWEEN.Tween({
|
||||
// 不管相机此刻处于什么状态,直接读取当前的位置和目标观察点
|
||||
x: cameraStartPos.x,
|
||||
y: cameraStartPos.y,
|
||||
z: cameraStartPos.z,
|
||||
tx: controlsPos.x,
|
||||
ty: controlsPos.y,
|
||||
tz: controlsPos.z,
|
||||
})
|
||||
.to(
|
||||
{
|
||||
// 动画结束相机位置坐标
|
||||
x: endPos.x,
|
||||
y: endPos.y,
|
||||
z: endPos.z,
|
||||
// 动画结束相机指向的目标观察点
|
||||
tx: endTarget.x,
|
||||
ty: endTarget.y,
|
||||
tz: endTarget.z,
|
||||
},
|
||||
1000
|
||||
)
|
||||
.easing(Easing.Quadratic.Out)
|
||||
.onUpdate(function (obj) {
|
||||
// 动态改变相机位置
|
||||
camera.position.set(obj.x, obj.y, obj.z);
|
||||
// 动态计算相机视线
|
||||
// camera.lookAt(obj.tx, obj.ty, obj.tz);
|
||||
controls.target.set(obj.tx, obj.ty, obj.tz);
|
||||
controls.update(); //内部会执行.lookAt()
|
||||
})
|
||||
.start();
|
||||
}
|
||||
}
|
||||
export default PlayerControls;
|
||||
1
apps/steelmill/src/three/index.ts
Normal file
1
apps/steelmill/src/three/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './player';
|
||||
66
apps/steelmill/src/three/loader.ts
Normal file
66
apps/steelmill/src/three/loader.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { LoadingManager } from 'three';
|
||||
|
||||
// function formatNumber(num: number) {
|
||||
// return num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
|
||||
// }
|
||||
class Loader {
|
||||
dracoPath: string;
|
||||
ktx2Path: string;
|
||||
constructor(options: { dracoPath: string; ktx2Path: string } = { dracoPath: '/draco/', ktx2Path: '/basis/' }) {
|
||||
this.dracoPath = options?.dracoPath;
|
||||
this.ktx2Path = options.ktx2Path;
|
||||
}
|
||||
|
||||
async loadFile(url: string, manager?: LoadingManager) {
|
||||
const extension = url!.split('.')?.pop()?.toLowerCase();
|
||||
// const reader = new FileReader();
|
||||
// reader.addEventListener('progress', function (event) {
|
||||
// const size = '(' + formatNumber(Math.floor(event.total / 1000)) + ' KB)';
|
||||
// const progress = Math.floor((event.loaded / event.total) * 100) + '%';
|
||||
|
||||
// console.log('Loading', file, size, progress);
|
||||
// });
|
||||
|
||||
switch (extension) {
|
||||
case 'glb':
|
||||
case 'gltf': {
|
||||
const loader = await this.createGLTFLoader(manager);
|
||||
const result = await loader.loadAsync(url);
|
||||
const scene = result.scene;
|
||||
scene.name = url;
|
||||
scene.animations.push(...result.animations);
|
||||
|
||||
loader?.dracoLoader?.dispose();
|
||||
loader?.ktx2Loader?.dispose();
|
||||
return scene;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error('not supported yet');
|
||||
}
|
||||
}
|
||||
|
||||
private async createGLTFLoader(manager?: LoadingManager) {
|
||||
const { GLTFLoader } = await import('three/addons/loaders/GLTFLoader.js');
|
||||
const { DRACOLoader } = await import('three/addons/loaders/DRACOLoader.js');
|
||||
const { KTX2Loader } = await import('three/addons/loaders/KTX2Loader.js');
|
||||
const { MeshoptDecoder } = await import('three/addons/libs/meshopt_decoder.module.js');
|
||||
|
||||
const dracoLoader = new DRACOLoader();
|
||||
dracoLoader.setDecoderPath(this.dracoPath);
|
||||
|
||||
const ktx2Loader = new KTX2Loader();
|
||||
ktx2Loader.setTranscoderPath(this.ktx2Path);
|
||||
|
||||
// editor.signals.rendererDetectKTX2Support.dispatch(ktx2Loader);
|
||||
|
||||
const loader = new GLTFLoader(manager);
|
||||
loader.setDRACOLoader(dracoLoader);
|
||||
loader.setKTX2Loader(ktx2Loader);
|
||||
loader.setMeshoptDecoder(MeshoptDecoder);
|
||||
|
||||
return loader;
|
||||
}
|
||||
}
|
||||
|
||||
export default Loader;
|
||||
341
apps/steelmill/src/three/player.ts
Normal file
341
apps/steelmill/src/three/player.ts
Normal file
@@ -0,0 +1,341 @@
|
||||
import * as THREE from 'three';
|
||||
import signals, { Signal } from 'signals';
|
||||
import Loader from './loader';
|
||||
import { CSS2DRenderer, CSS3DRenderer } from 'three/examples/jsm/Addons.js';
|
||||
import PlayerControls from './controls';
|
||||
import Selector from './selector';
|
||||
import TWEEN from '@tweenjs/tween.js';
|
||||
|
||||
// import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
|
||||
|
||||
const _DEFAULT_CAMERA = new THREE.PerspectiveCamera(50, 1, 0.01, 1000);
|
||||
_DEFAULT_CAMERA.name = 'Camera';
|
||||
_DEFAULT_CAMERA.position.set(0, 5, 10);
|
||||
_DEFAULT_CAMERA.lookAt(new THREE.Vector3());
|
||||
|
||||
let time: number, startTime: number; // prevTime: number;
|
||||
const clock = new THREE.Clock();
|
||||
// type EventListenerHandler = (...params: any[]) => void;
|
||||
type Events = {
|
||||
init: Signal<any>;
|
||||
start: Signal<any>;
|
||||
stop: Signal<any>;
|
||||
keydown: Signal<any>;
|
||||
keyup: Signal<any>;
|
||||
pointerdown: Signal<any>;
|
||||
pointerup: Signal<any>;
|
||||
pointermove: Signal<any>;
|
||||
update: Signal<any>;
|
||||
resize: Signal<any>;
|
||||
intersectionsDetected: Signal<any>;
|
||||
objectFocused: Signal<any>;
|
||||
objectSelected: Signal<any>;
|
||||
};
|
||||
export class Player {
|
||||
width: any;
|
||||
height: any;
|
||||
dom: HTMLElement;
|
||||
renderer?: THREE.WebGLRenderer;
|
||||
scene: THREE.Scene;
|
||||
camera?: THREE.Camera;
|
||||
loader: Loader;
|
||||
private resizeObserver: ResizeObserver;
|
||||
selector: Selector;
|
||||
controls?: PlayerControls;
|
||||
private mixers: THREE.AnimationMixer[] = [];
|
||||
|
||||
constructor() {
|
||||
this.dom = document.createElement('div');
|
||||
this.dom.id = 'player';
|
||||
|
||||
this.loader = new Loader();
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||
this.renderer.shadowMap.enabled = true;
|
||||
this.renderer.localClippingEnabled = true;
|
||||
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
this.renderer.clearDepth();
|
||||
|
||||
this.dom.appendChild(this.renderer.domElement);
|
||||
//set camera
|
||||
this.setCamera(_DEFAULT_CAMERA.clone());
|
||||
// init scene
|
||||
this.scene = new THREE.Scene();
|
||||
this.scene.name = 'Scene';
|
||||
this.resizeObserver = new ResizeObserver((entries) => {
|
||||
const { width, height } = entries[0].contentRect;
|
||||
// console.log('resizeObserver===>', width, height);
|
||||
this.onResize(width, height);
|
||||
});
|
||||
this.resizeObserver.observe(this.dom);
|
||||
this.selector = new Selector(this);
|
||||
this.events.objectFocused.add((object) => {
|
||||
this.controls?.focus2(object);
|
||||
});
|
||||
}
|
||||
|
||||
private onResize(width: number, height: number) {
|
||||
if (width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
this.setSize(width, height);
|
||||
this.setPixelRatio(window.devicePixelRatio);
|
||||
|
||||
this.events.resize.dispatch(width, height);
|
||||
}
|
||||
private _events: Events = {
|
||||
init: new signals.Signal(),
|
||||
start: new signals.Signal(),
|
||||
stop: new signals.Signal(),
|
||||
keydown: new signals.Signal(),
|
||||
keyup: new signals.Signal(),
|
||||
pointerdown: new signals.Signal(),
|
||||
pointerup: new signals.Signal(),
|
||||
pointermove: new signals.Signal(),
|
||||
update: new signals.Signal(),
|
||||
resize: new signals.Signal(),
|
||||
intersectionsDetected: new signals.Signal(),
|
||||
objectFocused: new signals.Signal(),
|
||||
objectSelected: new signals.Signal(),
|
||||
};
|
||||
get events() {
|
||||
return this._events;
|
||||
}
|
||||
get cavans() {
|
||||
return this.renderer!.domElement;
|
||||
}
|
||||
public enableShadows() {
|
||||
this.renderer!.shadowMap.enabled = true;
|
||||
this.renderer!.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||
|
||||
this.scene.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.castShadow = true;
|
||||
child.receiveShadow = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
public addAnimation(animations: Array<THREE.AnimationClip>, animationName: string, target: THREE.Object3D) {
|
||||
const mixer = new THREE.AnimationMixer(target);
|
||||
const clip = THREE.AnimationClip.findByName(animations, animationName);
|
||||
if (!clip) return undefined;
|
||||
const action = mixer.clipAction(clip);
|
||||
action.play();
|
||||
this.mixers.push(mixer);
|
||||
}
|
||||
public addAnimationMixer(mixer: THREE.AnimationMixer) {
|
||||
this.mixers.push(mixer);
|
||||
}
|
||||
public addCSS2DRenderer() {
|
||||
const CSSRenderer = new CSS2DRenderer();
|
||||
CSSRenderer.setSize(this.width, this.height);
|
||||
CSSRenderer.domElement.style.position = 'absolute';
|
||||
CSSRenderer.domElement.style.top = '0px';
|
||||
CSSRenderer.domElement.style.pointerEvents = 'none';
|
||||
this.dom.appendChild(CSSRenderer.domElement);
|
||||
|
||||
this.events.update.add(() => {
|
||||
CSSRenderer.render(this.scene, this.camera!);
|
||||
});
|
||||
this.events.resize.add((width, height) => {
|
||||
CSSRenderer.setSize(width, height);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
public addCSS3Renderer() {
|
||||
const css3Renderer = new CSS3DRenderer();
|
||||
css3Renderer.setSize(this.width, this.height);
|
||||
// HTML标签<div id="tag"></div>外面父元素叠加到canvas画布上且重合
|
||||
css3Renderer.domElement.style.position = 'absolute';
|
||||
css3Renderer.domElement.style.top = '0px';
|
||||
//设置.pointerEvents=none,解决HTML元素标签对threejs canvas画布鼠标事件的遮挡
|
||||
css3Renderer.domElement.style.pointerEvents = 'none';
|
||||
this.dom.appendChild(css3Renderer.domElement);
|
||||
this.events.update.add(() => {
|
||||
css3Renderer.render(this.scene, this.camera!);
|
||||
});
|
||||
this.events.resize.add((width, height) => {
|
||||
css3Renderer.setSize(width, height);
|
||||
});
|
||||
}
|
||||
public setCamera(value: THREE.Camera) {
|
||||
this.camera = value;
|
||||
if (this.camera instanceof THREE.PerspectiveCamera) {
|
||||
this.camera.aspect = this.width / this.height;
|
||||
this.camera.updateProjectionMatrix();
|
||||
}
|
||||
}
|
||||
public addControls() {
|
||||
this.controls = new PlayerControls(this.camera!, this.cavans);
|
||||
this.events.update.add(() => {
|
||||
this.controls?.update();
|
||||
});
|
||||
}
|
||||
public setScene(scene: THREE.Scene) {
|
||||
// this.scene = value;
|
||||
this.scene.background = scene.background;
|
||||
this.scene.environment = scene.environment;
|
||||
this.scene.fog = scene.fog;
|
||||
this.scene.backgroundBlurriness = scene.backgroundBlurriness;
|
||||
this.scene.backgroundIntensity = scene.backgroundIntensity;
|
||||
|
||||
this.scene.userData = JSON.parse(JSON.stringify(scene.userData));
|
||||
while (scene.children.length > 0) {
|
||||
this.addObject(scene.children[0]);
|
||||
}
|
||||
}
|
||||
public setPixelRatio(pixelRatio: number) {
|
||||
this.renderer?.setPixelRatio(pixelRatio);
|
||||
}
|
||||
public setSize(width: number, height: number) {
|
||||
if (width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
|
||||
if (this.camera && this.camera instanceof THREE.PerspectiveCamera) {
|
||||
this.camera.aspect = this.width / this.height;
|
||||
this.camera.updateProjectionMatrix();
|
||||
}
|
||||
|
||||
this.renderer?.setSize(width, height);
|
||||
}
|
||||
|
||||
public addLight(light?: THREE.Light) {
|
||||
if (light) {
|
||||
this.addObject(light);
|
||||
return this;
|
||||
}
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 5);
|
||||
directionalLight.position.set(3, 15, 18);
|
||||
directionalLight.castShadow = true;
|
||||
directionalLight.shadow.camera.left = -100;
|
||||
directionalLight.shadow.camera.right = 100;
|
||||
directionalLight.shadow.camera.top = 100;
|
||||
directionalLight.shadow.camera.bottom = -100;
|
||||
directionalLight.shadow.camera.near = 1;
|
||||
directionalLight.shadow.camera.far = 500;
|
||||
directionalLight.shadow.mapSize.set(512, 512);
|
||||
directionalLight.shadow.radius = 1;
|
||||
const directionalLight1 = directionalLight.clone();
|
||||
directionalLight1.position.set(-3, 15, -18);
|
||||
this.addObject(directionalLight);
|
||||
this.addObject(directionalLight1);
|
||||
const ambientLight = new THREE.AmbientLight(0xfefefe, 3);
|
||||
// scene.value!.add(ambientLight);
|
||||
this.addObject(ambientLight);
|
||||
|
||||
const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 2);
|
||||
hemisphereLight.position.set(0, 8, 0);
|
||||
this.addObject(hemisphereLight);
|
||||
return this;
|
||||
}
|
||||
|
||||
public addGridHelpers() {
|
||||
// helpers
|
||||
const GRID_COLORS_LIGHT = [0x999999, 0x777777];
|
||||
// const GRID_COLORS_DARK = [0x555555, 0x888888];
|
||||
|
||||
const grid = new THREE.Group();
|
||||
|
||||
const grid1 = new THREE.GridHelper(30, 30);
|
||||
grid1.material.color.setHex(GRID_COLORS_LIGHT[0]);
|
||||
grid1.material.vertexColors = false;
|
||||
grid.add(grid1);
|
||||
|
||||
const grid2 = new THREE.GridHelper(30, 6);
|
||||
grid2.material.color.setHex(GRID_COLORS_LIGHT[1]);
|
||||
grid2.material.vertexColors = false;
|
||||
grid.add(grid2);
|
||||
this.events.update.add(() => {
|
||||
this.renderer?.render(grid, this.camera!);
|
||||
});
|
||||
}
|
||||
public addObject(object: THREE.Object3D, parent?: THREE.Object3D, index?: number) {
|
||||
if (parent === undefined) {
|
||||
this.scene?.add(object);
|
||||
} else {
|
||||
parent.children.splice(index || 0, 0, object);
|
||||
object.parent = parent;
|
||||
}
|
||||
}
|
||||
public play() {
|
||||
const animate = () => {
|
||||
time = performance.now();
|
||||
this.renderer?.render(this.scene!, this.camera!);
|
||||
const delta = clock.getDelta();
|
||||
|
||||
try {
|
||||
this.events.update.dispatch({ time: time - startTime, delta: delta });
|
||||
for (const mixer of this.mixers) {
|
||||
mixer.update(delta);
|
||||
}
|
||||
TWEEN.update();
|
||||
} catch (e: any) {
|
||||
console.error(e.message || e, e.stack || '');
|
||||
}
|
||||
|
||||
// prevTime = time;
|
||||
};
|
||||
|
||||
startTime = performance.now();
|
||||
|
||||
this.dom.addEventListener('click', this.onClick.bind(this));
|
||||
|
||||
this.events.start.dispatch();
|
||||
|
||||
this.renderer!.setAnimationLoop(animate);
|
||||
}
|
||||
public render(time?: number) {
|
||||
performance.now();
|
||||
this.events.update.dispatch({ time: time || 0 * 1000, delta: 0 /* TODO */ });
|
||||
this.renderer!.render(this.scene!, this.camera!);
|
||||
}
|
||||
public stop() {
|
||||
this.dom.removeEventListener('click', this.onClick.bind(this));
|
||||
|
||||
this.events.stop.dispatch();
|
||||
|
||||
this.renderer?.setAnimationLoop(null);
|
||||
}
|
||||
public dispose() {
|
||||
this.stop();
|
||||
this.renderer?.dispose();
|
||||
// dispose all objects
|
||||
this.scene?.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.geometry.dispose();
|
||||
child.material.dispose();
|
||||
}
|
||||
});
|
||||
this.scene.clear();
|
||||
this.cavans.remove();
|
||||
this.dom.remove();
|
||||
this.resizeObserver.disconnect();
|
||||
this.renderer = undefined;
|
||||
this.camera = undefined;
|
||||
}
|
||||
private getMousePosition(dom: HTMLElement, x: number, y: number) {
|
||||
const rect = dom.getBoundingClientRect();
|
||||
return [(x - rect.left) / rect.width, (y - rect.top) / rect.height];
|
||||
}
|
||||
|
||||
// private onDoubleClickPosition = new THREE.Vector2();
|
||||
// private onDoubleClick(event: MouseEvent) {
|
||||
// const array = this.getMousePosition(this.dom, event.clientX, event.clientY);
|
||||
// this.onDoubleClickPosition = new THREE.Vector2().fromArray(array);
|
||||
// const intersects = this.selector.getPointerIntersects(this.onDoubleClickPosition, this.camera!);
|
||||
|
||||
// if (intersects.length > 0) {
|
||||
// const intersect = intersects[0];
|
||||
// this.events.objectFocused.dispatch(intersect.object);
|
||||
|
||||
// }
|
||||
// }
|
||||
private onClick(event: MouseEvent) {
|
||||
const array = this.getMousePosition(this.dom, event.clientX, event.clientY);
|
||||
const clickPosition = new THREE.Vector2().fromArray(array);
|
||||
const intersects = this.selector.getPointerIntersects(clickPosition, this.camera!);
|
||||
this.events.intersectionsDetected.dispatch(intersects);
|
||||
}
|
||||
}
|
||||
70
apps/steelmill/src/three/selector.ts
Normal file
70
apps/steelmill/src/three/selector.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import * as THREE from 'three';
|
||||
import { Player } from './player';
|
||||
|
||||
const mouse = new THREE.Vector2();
|
||||
const raycaster = new THREE.Raycaster();
|
||||
class Selector {
|
||||
scene: THREE.Scene;
|
||||
selected: THREE.Object3D<THREE.Object3DEventMap> | null;
|
||||
player: Player;
|
||||
constructor(player: Player) {
|
||||
this.scene = player.scene;
|
||||
this.selected = null;
|
||||
this.player = player;
|
||||
// signals
|
||||
player.events.intersectionsDetected.add((intersects) => {
|
||||
if (intersects.length > 0) {
|
||||
const object = intersects[0].object;
|
||||
this.select(object);
|
||||
// if (object.userData.object !== undefined) {
|
||||
// // helper
|
||||
|
||||
// this.select(object.userData.object);
|
||||
// } else {
|
||||
// this.select(object);
|
||||
// }
|
||||
} else {
|
||||
this.select(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getIntersects(raycaster: THREE.Raycaster) {
|
||||
const objects: THREE.Object3D[] = [];
|
||||
|
||||
this.scene.traverseVisible(function (child) {
|
||||
objects.push(child);
|
||||
});
|
||||
|
||||
return raycaster.intersectObjects(objects, false);
|
||||
}
|
||||
|
||||
getPointerIntersects(point: THREE.Vector2, camera: THREE.Camera) {
|
||||
mouse.set(point.x * 2 - 1, -(point.y * 2) + 1);
|
||||
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
|
||||
return this.getIntersects(raycaster);
|
||||
}
|
||||
|
||||
select(object: THREE.Object3D | null) {
|
||||
if (this.selected === object) return;
|
||||
|
||||
// const uuid = null;
|
||||
|
||||
// if (object !== null) {
|
||||
// uuid = object.uuid;
|
||||
// }
|
||||
|
||||
this.selected = object;
|
||||
// this.editor.config.setKey('selected', uuid);
|
||||
this.player.events.objectSelected.dispatch(object);
|
||||
// this.signals.objectSelected.dispatch(object);
|
||||
}
|
||||
|
||||
deselect() {
|
||||
this.select(null);
|
||||
}
|
||||
}
|
||||
|
||||
export default Selector;
|
||||
Reference in New Issue
Block a user