Реализовать загрузку 3D-моделей в Scene manager и выгрузку файлов для запуска симуляции
This commit is contained in:
parent
11ca9cdb5e
commit
3fefd60b72
109 changed files with 2726 additions and 1190 deletions
|
@ -2,7 +2,8 @@ import { ArrayExtensions } from "./array";
|
|||
import { MapExtensions } from "./map";
|
||||
import { StringExtensions } from "./string";
|
||||
|
||||
export type CallBackFunction = <T>(value: T) => void;
|
||||
export type CallBackVoidFunction = <T>(value: T) => void;
|
||||
export type CallBackStringVoidFunction = (value: string) => void;
|
||||
|
||||
declare global {
|
||||
interface Array<T> {
|
||||
|
@ -15,9 +16,10 @@ declare global {
|
|||
}
|
||||
interface String {
|
||||
isEmpty(): boolean;
|
||||
isNotEmpty(): boolean;
|
||||
}
|
||||
interface Map<K, V> {
|
||||
addValueOrMakeCallback(key: K, value: V, callBack: CallBackFunction): void;
|
||||
addValueOrMakeCallback(key: K, value: V, callBack: CallBackVoidFunction): void;
|
||||
}
|
||||
}
|
||||
export const extensions = () => {
|
||||
|
|
|
@ -5,4 +5,10 @@ export const StringExtensions = () => {
|
|||
return this.length === 0;
|
||||
};
|
||||
}
|
||||
if ("".isNotEmpty === undefined) {
|
||||
// eslint-disable-next-line no-extend-native
|
||||
String.prototype.isNotEmpty = function () {
|
||||
return this.length !== 0;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
82
ui/src/core/helper/debounce.ts
Normal file
82
ui/src/core/helper/debounce.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
export type Options<Result> = {
|
||||
isImmediate?: boolean;
|
||||
maxWait?: number;
|
||||
callback?: (data: Result) => void;
|
||||
};
|
||||
|
||||
export interface DebouncedFunction<Args extends any[], F extends (...args: Args) => any> {
|
||||
(this: ThisParameterType<F>, ...args: Args & Parameters<F>): Promise<ReturnType<F>>;
|
||||
cancel: (reason?: any) => void;
|
||||
}
|
||||
|
||||
interface DebouncedPromise<FunctionReturn> {
|
||||
resolve: (result: FunctionReturn) => void;
|
||||
reject: (reason?: any) => void;
|
||||
}
|
||||
|
||||
export function debounce<Args extends any[], F extends (...args: Args) => any>(
|
||||
func: F,
|
||||
waitMilliseconds = 50,
|
||||
options: Options<ReturnType<F>> = {}
|
||||
): DebouncedFunction<Args, F> {
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||
const isImmediate = options.isImmediate ?? false;
|
||||
const callback = options.callback ?? false;
|
||||
const maxWait = options.maxWait;
|
||||
let lastInvokeTime = Date.now();
|
||||
|
||||
let promises: DebouncedPromise<ReturnType<F>>[] = [];
|
||||
|
||||
function nextInvokeTimeout() {
|
||||
if (maxWait !== undefined) {
|
||||
const timeSinceLastInvocation = Date.now() - lastInvokeTime;
|
||||
|
||||
if (timeSinceLastInvocation + waitMilliseconds >= maxWait) {
|
||||
return maxWait - timeSinceLastInvocation;
|
||||
}
|
||||
}
|
||||
|
||||
return waitMilliseconds;
|
||||
}
|
||||
|
||||
const debouncedFunction = function (this: ThisParameterType<F>, ...args: Parameters<F>) {
|
||||
const context = this;
|
||||
return new Promise<ReturnType<F>>((resolve, reject) => {
|
||||
const invokeFunction = function () {
|
||||
timeoutId = undefined;
|
||||
lastInvokeTime = Date.now();
|
||||
if (!isImmediate) {
|
||||
const result = func.apply(context, args);
|
||||
callback && callback(result);
|
||||
promises.forEach(({ resolve }) => resolve(result));
|
||||
promises = [];
|
||||
}
|
||||
};
|
||||
|
||||
const shouldCallNow = isImmediate && timeoutId === undefined;
|
||||
|
||||
if (timeoutId !== undefined) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
timeoutId = setTimeout(invokeFunction, nextInvokeTimeout());
|
||||
|
||||
if (shouldCallNow) {
|
||||
const result = func.apply(context, args);
|
||||
callback && callback(result);
|
||||
return resolve(result);
|
||||
}
|
||||
promises.push({ resolve, reject });
|
||||
});
|
||||
};
|
||||
|
||||
debouncedFunction.cancel = function (reason?: any) {
|
||||
if (timeoutId !== undefined) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
promises.forEach(({ reject }) => reject(reason));
|
||||
promises = [];
|
||||
};
|
||||
|
||||
return debouncedFunction;
|
||||
}
|
29
ui/src/core/helper/throttle.ts
Normal file
29
ui/src/core/helper/throttle.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
export const throttle = <R, A extends any[]>(
|
||||
fn: (...args: A) => R,
|
||||
delay: number
|
||||
): [(...args: A) => R | undefined, () => void] => {
|
||||
let wait = false;
|
||||
let timeout: undefined | number;
|
||||
let cancelled = false;
|
||||
|
||||
return [
|
||||
(...args: A) => {
|
||||
if (cancelled) return undefined;
|
||||
if (wait) return undefined;
|
||||
|
||||
const val = fn(...args);
|
||||
|
||||
wait = true;
|
||||
|
||||
timeout = window.setTimeout(() => {
|
||||
wait = false;
|
||||
}, delay);
|
||||
|
||||
return val;
|
||||
},
|
||||
() => {
|
||||
cancelled = true;
|
||||
clearTimeout(timeout);
|
||||
},
|
||||
];
|
||||
};
|
|
@ -1,7 +1,6 @@
|
|||
|
||||
export interface ActivePipeline {
|
||||
pipelineIsRunning: boolean;
|
||||
projectUUID?: string | null;
|
||||
projectId?: string | null;
|
||||
lastProcessCompleteCount: number | null;
|
||||
error: any;
|
||||
}
|
6
ui/src/core/model/ui_base_error.ts
Normal file
6
ui/src/core/model/ui_base_error.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export class UiBaseError {
|
||||
text: string;
|
||||
constructor(text: string) {
|
||||
this.text = text;
|
||||
}
|
||||
}
|
|
@ -1,189 +0,0 @@
|
|||
import {
|
||||
DirectionalLight,
|
||||
Object3D,
|
||||
PerspectiveCamera,
|
||||
Scene,
|
||||
WebGLRenderer,
|
||||
AmbientLight,
|
||||
Vector3,
|
||||
MeshBasicMaterial,
|
||||
Mesh,
|
||||
BoxGeometry,
|
||||
Object3DEventMap,
|
||||
Box3,
|
||||
Sphere,
|
||||
LineBasicMaterial,
|
||||
EdgesGeometry,
|
||||
Raycaster,
|
||||
LineSegments,
|
||||
Vector2,
|
||||
} from "three";
|
||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
|
||||
import { BaseSceneItemModel, StaticAssetItemModel } from "../../features/scene_manager/scene_manager_store";
|
||||
import { TypedEvent } from "../helper/typed_event";
|
||||
import { Result } from "../helper/result";
|
||||
|
||||
interface IEmissiveCache {
|
||||
status: boolean;
|
||||
object3d: Object3D<Object3DEventMap>;
|
||||
}
|
||||
export class CoreThereRepository extends TypedEvent<BaseSceneItemModel> {
|
||||
scene = new Scene();
|
||||
camera: PerspectiveCamera;
|
||||
webGlRender: WebGLRenderer;
|
||||
htmlCanvasRef: HTMLCanvasElement;
|
||||
objectEmissive = new Map<string, IEmissiveCache>();
|
||||
constructor(htmlCanvasRef: HTMLCanvasElement) {
|
||||
super();
|
||||
const renderer = new WebGLRenderer({
|
||||
canvas: htmlCanvasRef as HTMLCanvasElement,
|
||||
antialias: true,
|
||||
alpha: true,
|
||||
});
|
||||
const aspectCamera = window.outerWidth / window.outerHeight;
|
||||
this.camera = new PerspectiveCamera(800, aspectCamera, 0.1, 10000);
|
||||
this.webGlRender = renderer;
|
||||
this.htmlCanvasRef = htmlCanvasRef;
|
||||
this.init();
|
||||
}
|
||||
setRayCastAndGetFirstObject(vector: Vector2): Result<void, string> {
|
||||
const raycaster = new Raycaster();
|
||||
raycaster.setFromCamera(vector, this.camera);
|
||||
const intersects = raycaster.intersectObjects(this.scene.children);
|
||||
if (intersects.length > 0) {
|
||||
return Result.ok(intersects[0].object.name);
|
||||
}
|
||||
|
||||
return Result.error(undefined);
|
||||
}
|
||||
addCube(num: number, translateTo: string = "X") {
|
||||
const geometry = new BoxGeometry(1, 1, 1);
|
||||
const material = new MeshBasicMaterial({ color: 0x00ff00 });
|
||||
const cube = new Mesh(geometry, material);
|
||||
cube.name = "Cube" + String(num);
|
||||
|
||||
eval(`cube.translate${translateTo}(${num * 10})`);
|
||||
this.scene.add(cube);
|
||||
}
|
||||
init() {
|
||||
const directionalLight = new DirectionalLight(0xffffff, 0.2);
|
||||
directionalLight.castShadow = true;
|
||||
directionalLight.position.set(-1, 2, 4);
|
||||
this.scene.add(directionalLight);
|
||||
const ambientLight = new AmbientLight(0xffffff, 0.7);
|
||||
this.scene.add(ambientLight);
|
||||
|
||||
this.addCube(1);
|
||||
this.addCube(2);
|
||||
this.addCube(3);
|
||||
this.addCube(4);
|
||||
|
||||
const onResize = () => {
|
||||
this.camera.aspect = window.outerWidth / window.outerHeight;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.webGlRender.setSize(window.outerWidth, window.outerHeight);
|
||||
};
|
||||
window.addEventListener("resize", onResize, false);
|
||||
new OrbitControls(this.camera, this.htmlCanvasRef);
|
||||
}
|
||||
render() {
|
||||
this.webGlRender.setSize(window.outerWidth, window.outerHeight);
|
||||
this.webGlRender.setAnimationLoop(() => {
|
||||
this.webGlRender.render(this.scene, this.camera);
|
||||
});
|
||||
}
|
||||
getAllSceneModels(): BaseSceneItemModel[] {
|
||||
return this.getAllSceneNameModels().map((e) => new StaticAssetItemModel(e));
|
||||
}
|
||||
getAllSceneNameModels(): string[] {
|
||||
return this.scene.children.filter((el) => el.name !== "").map((el) => el.name);
|
||||
}
|
||||
getObjectsAtName(name: string): Object3D<Object3DEventMap> {
|
||||
return this.scene.children.filter((el) => el.name === name)[0];
|
||||
}
|
||||
loader(urls: string[], callBack: Function) {}
|
||||
fitCameraToCenteredObject(objects: string[], offset = 4) {
|
||||
// https://wejn.org/2020/12/cracking-the-threejs-object-fitting-nut/
|
||||
const boundingBox = new Box3().setFromPoints(
|
||||
objects.map((el) => this.getObjectsAtName(el)).map((el) => el.position)
|
||||
);
|
||||
|
||||
var size = new Vector3();
|
||||
boundingBox.getSize(size);
|
||||
|
||||
const fov = this.camera.fov * (Math.PI / 180);
|
||||
const fovh = 2 * Math.atan(Math.tan(fov / 2) * this.camera.aspect);
|
||||
let dx = size.z / 2 + Math.abs(size.x / 2 / Math.tan(fovh / 2));
|
||||
let dy = size.z / 2 + Math.abs(size.y / 2 / Math.tan(fov / 2));
|
||||
let cameraZ = Math.max(dx, dy);
|
||||
|
||||
if (offset !== undefined && offset !== 0) cameraZ *= offset;
|
||||
|
||||
this.camera.position.set(0, 0, cameraZ);
|
||||
|
||||
const minZ = boundingBox.min.z;
|
||||
const cameraToFarEdge = minZ < 0 ? -minZ + cameraZ : cameraZ - minZ;
|
||||
|
||||
this.camera.far = cameraToFarEdge * 3;
|
||||
this.camera.updateProjectionMatrix();
|
||||
let orbitControls = new OrbitControls(this.camera, this.htmlCanvasRef);
|
||||
|
||||
orbitControls.maxDistance = cameraToFarEdge * 2;
|
||||
new OrbitControls(this.camera, this.htmlCanvasRef);
|
||||
}
|
||||
switchObjectEmissive(name: string) {
|
||||
const mesh = this.getObjectsAtName(name);
|
||||
const result = this.objectEmissive.get(mesh.name);
|
||||
if (result?.status) {
|
||||
this.scene.remove(mesh);
|
||||
this.scene.add(result.object3d);
|
||||
this.objectEmissive.set(mesh.name, {
|
||||
status: false,
|
||||
object3d: mesh,
|
||||
});
|
||||
} else {
|
||||
this.objectEmissive.set(mesh.name, {
|
||||
status: true,
|
||||
object3d: mesh,
|
||||
});
|
||||
|
||||
if (mesh instanceof Mesh) {
|
||||
const newMesh = new LineSegments(mesh.geometry, new LineBasicMaterial({ color: 0x000000 }));
|
||||
newMesh.name = mesh.name;
|
||||
newMesh.translateX(mesh.position.x);
|
||||
newMesh.translateY(mesh.position.y);
|
||||
newMesh.translateZ(mesh.position.z);
|
||||
this.scene.remove(mesh);
|
||||
this.scene.add(newMesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fitSelectedObjectToScreen(objects: string[]) {
|
||||
//https://stackoverflow.com/questions/14614252/how-to-fit-camera-to-object
|
||||
let boundBox = new Box3().setFromPoints(objects.map((el) => this.getObjectsAtName(el)).map((el) => el.position));
|
||||
let boundSphere = boundBox.getBoundingSphere(new Sphere());
|
||||
let vFoV = this.camera.getEffectiveFOV();
|
||||
let hFoV = this.camera.fov * this.camera.aspect;
|
||||
let FoV = Math.min(vFoV, hFoV);
|
||||
let FoV2 = FoV / 2;
|
||||
let dir = new Vector3();
|
||||
this.camera.getWorldDirection(dir);
|
||||
let bsWorld = boundSphere.center.clone();
|
||||
let th = (FoV2 * Math.PI) / 180.0;
|
||||
let sina = Math.sin(th);
|
||||
let R = boundSphere.radius;
|
||||
let FL = R / sina;
|
||||
let cameraDir = new Vector3();
|
||||
let cameraOffs = cameraDir.clone();
|
||||
cameraOffs.multiplyScalar(-FL);
|
||||
|
||||
let newCameraPos = bsWorld.clone().add(cameraOffs);
|
||||
|
||||
this.camera.translateX(newCameraPos.x);
|
||||
this.camera.translateY(newCameraPos.y);
|
||||
this.camera.translateZ(newCameraPos.z);
|
||||
this.camera.lookAt(bsWorld);
|
||||
new OrbitControls(this.camera, this.htmlCanvasRef);
|
||||
}
|
||||
}
|
401
ui/src/core/repository/core_three_repository.ts
Normal file
401
ui/src/core/repository/core_three_repository.ts
Normal file
|
@ -0,0 +1,401 @@
|
|||
import {
|
||||
DirectionalLight,
|
||||
Object3D,
|
||||
PerspectiveCamera,
|
||||
Scene,
|
||||
WebGLRenderer,
|
||||
AmbientLight,
|
||||
Vector3,
|
||||
Mesh,
|
||||
Object3DEventMap,
|
||||
Box3,
|
||||
Sphere,
|
||||
LineBasicMaterial,
|
||||
Intersection,
|
||||
Raycaster,
|
||||
LineSegments,
|
||||
Vector2,
|
||||
Color,
|
||||
GridHelper,
|
||||
CameraHelper,
|
||||
Quaternion,
|
||||
} from "three";
|
||||
import { TypedEvent } from "../helper/typed_event";
|
||||
import { Result } from "../helper/result";
|
||||
import { GLTFLoader, OrbitControls, TransformControls, OBJLoader, STLLoader, ColladaLoader } from "three-stdlib";
|
||||
import {
|
||||
BaseSceneItemModel,
|
||||
CameraViewModel,
|
||||
StaticAssetItemModel,
|
||||
} from "../../features/scene_manager/model/scene_assets";
|
||||
import { SceneMode } from "../../features/scene_manager/model/scene_view";
|
||||
import { throttle } from "../helper/throttle";
|
||||
import {
|
||||
InstanceRgbCamera,
|
||||
RobossemblerAssets,
|
||||
SceneSimpleObject,
|
||||
} from "../../features/scene_manager/model/robossembler_assets";
|
||||
|
||||
export enum UserData {
|
||||
selectedObject = "selected_object",
|
||||
cameraInitialization = "camera_initialization",
|
||||
}
|
||||
|
||||
interface IEventDraggingChange {
|
||||
target: null;
|
||||
type: string;
|
||||
value: boolean;
|
||||
}
|
||||
|
||||
interface IEmissiveCache {
|
||||
status: boolean;
|
||||
object3d: Object3D<Object3DEventMap>;
|
||||
}
|
||||
|
||||
export class CoreThreeRepository extends TypedEvent<BaseSceneItemModel> {
|
||||
scene = new Scene();
|
||||
camera: PerspectiveCamera;
|
||||
webGlRender: WebGLRenderer;
|
||||
htmlCanvasRef: HTMLCanvasElement;
|
||||
objectEmissive = new Map<string, IEmissiveCache>();
|
||||
transformControls: TransformControls;
|
||||
orbitControls: OrbitControls;
|
||||
htmlSceneWidth: number;
|
||||
htmlSceneHeight: number;
|
||||
objLoader = new OBJLoader();
|
||||
glbLoader = new GLTFLoader();
|
||||
daeLoader = new ColladaLoader();
|
||||
stlLoader = new STLLoader();
|
||||
watcherSceneEditorObject: Function;
|
||||
|
||||
constructor(htmlCanvasRef: HTMLCanvasElement, watcherSceneEditorObject: Function) {
|
||||
super();
|
||||
this.htmlSceneWidth = window.innerWidth;
|
||||
this.htmlSceneHeight = window.innerHeight;
|
||||
this.watcherSceneEditorObject = watcherSceneEditorObject;
|
||||
const renderer = new WebGLRenderer({
|
||||
canvas: htmlCanvasRef as HTMLCanvasElement,
|
||||
antialias: true,
|
||||
alpha: true,
|
||||
});
|
||||
const aspectCamera = this.htmlSceneWidth / this.htmlSceneHeight;
|
||||
this.camera = new PerspectiveCamera(800, aspectCamera, 0.1, 10000);
|
||||
this.camera.position.set(60, 20, 10);
|
||||
|
||||
this.webGlRender = renderer;
|
||||
this.htmlCanvasRef = htmlCanvasRef;
|
||||
|
||||
this.transformControls = new TransformControls(this.camera, htmlCanvasRef);
|
||||
|
||||
this.scene.add(this.transformControls);
|
||||
this.orbitControls = new OrbitControls(this.camera, this.htmlCanvasRef);
|
||||
this.scene.background = new Color("black");
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
deleteSceneItem(item: BaseSceneItemModel) {
|
||||
const updateScene = this.scene;
|
||||
updateScene.children = item.deleteToScene(updateScene);
|
||||
}
|
||||
|
||||
loadInstances(robossemblerAssets: RobossemblerAssets) {
|
||||
robossemblerAssets.instances.forEach(async (el) => {
|
||||
if (el instanceof InstanceRgbCamera) {
|
||||
const cameraModel = CameraViewModel.fromInstanceRgbCamera(el);
|
||||
cameraModel.mapPerspectiveCamera(this.htmlSceneWidth, this.htmlSceneHeight).forEach((el) => this.scene.add(el));
|
||||
this.emit(cameraModel);
|
||||
}
|
||||
if (el instanceof SceneSimpleObject) {
|
||||
const asset = robossemblerAssets.getAssetAtInstance(el.instanceAt as string);
|
||||
this.loader(
|
||||
asset.meshPath,
|
||||
() => {},
|
||||
asset.name,
|
||||
new Vector3(el.position.x, el.position.y, el.position.z),
|
||||
new Quaternion(el.quaternion[0], el.quaternion[1], el.quaternion[2], el.quaternion[3])
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setTransformMode(mode?: SceneMode) {
|
||||
switch (mode) {
|
||||
case undefined:
|
||||
this.transformControls.detach();
|
||||
this.transformControls.dispose();
|
||||
break;
|
||||
case SceneMode.MOVING:
|
||||
this.transformControls.setMode("translate");
|
||||
break;
|
||||
case SceneMode.ROTATE:
|
||||
this.transformControls.setMode("rotate");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
addSceneCamera(cameraModel: CameraViewModel) {
|
||||
cameraModel.mapPerspectiveCamera(this.htmlSceneWidth, this.htmlSceneHeight).forEach((el) => this.scene.add(el));
|
||||
}
|
||||
|
||||
disposeTransformControlsMode() {
|
||||
this.transformControls.detach();
|
||||
}
|
||||
|
||||
setRayCastAndGetFirstObjectName(vector: Vector2): Result<void, string> {
|
||||
this.scene.add(this.transformControls);
|
||||
const raycaster = new Raycaster();
|
||||
raycaster.setFromCamera(vector, this.camera);
|
||||
const intersects = raycaster.intersectObjects(this.scene.children);
|
||||
if (intersects.length > 0) {
|
||||
return Result.ok(intersects[0].object.name);
|
||||
}
|
||||
|
||||
return Result.error(undefined);
|
||||
}
|
||||
|
||||
setTransformControlsAttach(object: Object3D<Object3DEventMap>) {
|
||||
if (object instanceof CameraHelper) {
|
||||
this.transformControls.attach(object.camera);
|
||||
return;
|
||||
}
|
||||
this.transformControls.attach(object);
|
||||
}
|
||||
|
||||
setRayCastAndGetFirstObject(vector: Vector2): Result<void, Object3D<Object3DEventMap>> {
|
||||
try {
|
||||
const result = this.setRayCast(vector);
|
||||
return result.fold(
|
||||
(intersects) => {
|
||||
const result = intersects.find((element) => element.object.userData[UserData.selectedObject] !== undefined);
|
||||
if (result === undefined) {
|
||||
return Result.error(undefined);
|
||||
}
|
||||
return Result.ok(result.object);
|
||||
},
|
||||
(_error) => {
|
||||
return Result.error(undefined);
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
return Result.error(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
setRayCast(vector: Vector2): Result<void, Intersection<Object3D<Object3DEventMap>>[]> {
|
||||
const raycaster = new Raycaster();
|
||||
raycaster.setFromCamera(vector, this.camera);
|
||||
const intersects = raycaster.intersectObjects(this.scene.children);
|
||||
if (intersects.length > 0) {
|
||||
return Result.ok(intersects);
|
||||
}
|
||||
|
||||
return Result.error(undefined);
|
||||
}
|
||||
|
||||
setRayCastAndGetFirstObjectAndPointToObject(vector: Vector2): Result<void, Vector3> {
|
||||
this.setRayCast(vector).map((intersects) => {
|
||||
if (intersects.length > 0) {
|
||||
return Result.ok(intersects[0].point);
|
||||
}
|
||||
});
|
||||
return Result.error(undefined);
|
||||
}
|
||||
|
||||
light() {
|
||||
const directionalLight = new DirectionalLight(0xffffff, 0.2);
|
||||
directionalLight.castShadow = true;
|
||||
directionalLight.position.set(-1, 2, 4);
|
||||
this.scene.add(directionalLight);
|
||||
const ambientLight = new AmbientLight(0xffffff, 0.7);
|
||||
this.scene.add(ambientLight);
|
||||
}
|
||||
|
||||
addListeners() {
|
||||
window.addEventListener(
|
||||
"resize",
|
||||
() => {
|
||||
this.camera.aspect = this.htmlSceneWidth / this.htmlSceneHeight;
|
||||
this.camera.updateProjectionMatrix();
|
||||
|
||||
this.webGlRender.setSize(this.htmlSceneWidth, this.htmlSceneHeight);
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
this.transformControls.addEventListener("dragging-changed", (event) => {
|
||||
const e = event as unknown as IEventDraggingChange;
|
||||
this.orbitControls.enabled = !e.value;
|
||||
});
|
||||
this.transformControls.addEventListener("objectChange", (event) => {
|
||||
//@ts-expect-error
|
||||
const sceneObject = event.target.object;
|
||||
//TODO:(IDONTSUDO) Trotting doesn't work, need to figure out why
|
||||
const fn = () => this.watcherSceneEditorObject(sceneObject);
|
||||
const [throttleFn] = throttle(fn, 1000);
|
||||
throttleFn();
|
||||
});
|
||||
}
|
||||
|
||||
init() {
|
||||
this.light();
|
||||
this.addListeners();
|
||||
const floor = new GridHelper(100, 100, 0x888888, 0x444444);
|
||||
floor.userData = {};
|
||||
floor.userData[UserData.cameraInitialization] = true;
|
||||
this.scene.add(floor);
|
||||
}
|
||||
|
||||
render() {
|
||||
this.webGlRender.setSize(this.htmlSceneWidth, this.htmlSceneHeight);
|
||||
this.webGlRender.setAnimationLoop(() => {
|
||||
this.webGlRender.render(this.scene, this.camera);
|
||||
});
|
||||
}
|
||||
|
||||
getAllSceneModels(): BaseSceneItemModel[] {
|
||||
return this.getAllSceneNameModels().map(
|
||||
(name) =>
|
||||
new StaticAssetItemModel(name, this.getObjectsAtName(name).position, this.getObjectsAtName(name).quaternion)
|
||||
);
|
||||
}
|
||||
|
||||
getAllSceneNameModels(): string[] {
|
||||
return this.scene.children.filter((el) => el.name !== "").map((el) => el.name);
|
||||
}
|
||||
|
||||
getObjectsAtName(name: string): Object3D<Object3DEventMap> {
|
||||
return this.scene.children.filter((el) => el.name === name)[0];
|
||||
}
|
||||
|
||||
loader(url: string, callBack: Function, name: string, position?: Vector3, quaternion?: Quaternion) {
|
||||
const ext = url.split(/\./g).pop()!.toLowerCase();
|
||||
|
||||
switch (ext) {
|
||||
case "gltf":
|
||||
case "glb":
|
||||
this.glbLoader.load(
|
||||
url,
|
||||
(result) => {},
|
||||
(err) => {}
|
||||
);
|
||||
break;
|
||||
case "obj":
|
||||
this.objLoader.load(
|
||||
url,
|
||||
(result) => {
|
||||
result.userData[UserData.selectedObject] = true;
|
||||
result.children.forEach((el) => {
|
||||
el.userData[UserData.selectedObject] = true;
|
||||
el.name = name;
|
||||
|
||||
if (position) el.position.copy(position);
|
||||
if (quaternion) el.quaternion.copy(quaternion);
|
||||
|
||||
this.emit(new StaticAssetItemModel(el.name, el.position, el.quaternion));
|
||||
this.scene.add(el);
|
||||
});
|
||||
},
|
||||
(err) => {}
|
||||
);
|
||||
break;
|
||||
case "dae":
|
||||
this.daeLoader.load(
|
||||
url,
|
||||
(result) => {},
|
||||
(err) => {}
|
||||
);
|
||||
break;
|
||||
case "stl":
|
||||
this.stlLoader.load(
|
||||
url,
|
||||
(result) => {},
|
||||
|
||||
(err) => {}
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fitCameraToCenteredObject(objects: string[], offset = 4) {
|
||||
const boundingBox = new Box3().setFromPoints(
|
||||
objects.map((el) => this.getObjectsAtName(el)).map((el) => el.position)
|
||||
);
|
||||
|
||||
var size = new Vector3();
|
||||
boundingBox.getSize(size);
|
||||
|
||||
const fov = this.camera.fov * (Math.PI / 180);
|
||||
const fovh = 2 * Math.atan(Math.tan(fov / 2) * this.camera.aspect);
|
||||
let dx = size.z / 2 + Math.abs(size.x / 2 / Math.tan(fovh / 2));
|
||||
let dy = size.z / 2 + Math.abs(size.y / 2 / Math.tan(fov / 2));
|
||||
let cameraZ = Math.max(dx, dy);
|
||||
|
||||
if (offset !== undefined && offset !== 0) cameraZ *= offset;
|
||||
|
||||
this.camera.position.set(0, 0, cameraZ);
|
||||
|
||||
const minZ = boundingBox.min.z;
|
||||
const cameraToFarEdge = minZ < 0 ? -minZ + cameraZ : cameraZ - minZ;
|
||||
|
||||
this.camera.far = cameraToFarEdge * 3;
|
||||
this.camera.updateProjectionMatrix();
|
||||
let orbitControls = new OrbitControls(this.camera, this.htmlCanvasRef);
|
||||
|
||||
orbitControls.maxDistance = cameraToFarEdge * 2;
|
||||
this.orbitControls = orbitControls;
|
||||
}
|
||||
|
||||
switchObjectEmissive(name: string) {
|
||||
const mesh = this.getObjectsAtName(name);
|
||||
const result = this.objectEmissive.get(mesh.name);
|
||||
if (result?.status) {
|
||||
this.scene.remove(mesh);
|
||||
this.scene.add(result.object3d);
|
||||
this.objectEmissive.set(mesh.name, {
|
||||
status: false,
|
||||
object3d: mesh,
|
||||
});
|
||||
} else {
|
||||
this.objectEmissive.set(mesh.name, {
|
||||
status: true,
|
||||
object3d: mesh,
|
||||
});
|
||||
|
||||
if (mesh instanceof Mesh) {
|
||||
const newMesh = new LineSegments(mesh.geometry, new LineBasicMaterial({ color: 0x000000 }));
|
||||
newMesh.name = mesh.name;
|
||||
newMesh.position.copy(mesh.position);
|
||||
|
||||
this.scene.remove(mesh);
|
||||
this.scene.add(newMesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fitSelectedObjectToScreen(objects: string[]) {
|
||||
//https://stackoverflow.com/questions/14614252/how-to-fit-camera-to-object
|
||||
let boundBox = new Box3().setFromPoints(objects.map((el) => this.getObjectsAtName(el)).map((el) => el.position));
|
||||
let boundSphere = boundBox.getBoundingSphere(new Sphere());
|
||||
let vFoV = this.camera.getEffectiveFOV();
|
||||
let hFoV = this.camera.fov * this.camera.aspect;
|
||||
let FoV = Math.min(vFoV, hFoV);
|
||||
let FoV2 = FoV / 2;
|
||||
let dir = new Vector3();
|
||||
this.camera.getWorldDirection(dir);
|
||||
let bsWorld = boundSphere.center.clone();
|
||||
let th = (FoV2 * Math.PI) / 180.0;
|
||||
let sina = Math.sin(th);
|
||||
let R = boundSphere.radius;
|
||||
let FL = R / sina;
|
||||
let cameraDir = new Vector3();
|
||||
let cameraOffs = cameraDir.clone();
|
||||
cameraOffs.multiplyScalar(-FL);
|
||||
|
||||
let newCameraPos = bsWorld.clone().add(cameraOffs);
|
||||
|
||||
this.camera.position.copy(newCameraPos);
|
||||
this.camera.lookAt(bsWorld);
|
||||
this.orbitControls = new OrbitControls(this.camera, this.htmlCanvasRef);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import { ClassConstructor, plainToInstance } from "class-transformer";
|
||||
import { Result } from "../helper/result";
|
||||
|
||||
export enum HttpMethod {
|
||||
|
@ -16,12 +17,22 @@ export class HttpError extends Error {
|
|||
|
||||
export class HttpRepository {
|
||||
private server = "http://localhost:4001";
|
||||
public async _formDataRequest<T>(method: HttpMethod, url: string, data?: any): Promise<Result<HttpError, T>> {
|
||||
let formData = new FormData();
|
||||
formData.append("file", data);
|
||||
|
||||
public async jsonRequest<T>(
|
||||
method: HttpMethod,
|
||||
url: string,
|
||||
data?: any
|
||||
): Promise<Result<HttpError, T>> {
|
||||
const reqInit = {
|
||||
body: formData,
|
||||
method: method,
|
||||
};
|
||||
|
||||
const response = await fetch(this.server + url, reqInit);
|
||||
if (response.status !== 200) {
|
||||
throw Result.error(new Error(await response.json()));
|
||||
}
|
||||
return Result.ok(response.text as T);
|
||||
}
|
||||
public async _jsonRequest<T>(method: HttpMethod, url: string, data?: any): Promise<Result<HttpError, T>> {
|
||||
try {
|
||||
const reqInit = {
|
||||
body: data,
|
||||
|
@ -37,28 +48,50 @@ export class HttpRepository {
|
|||
return Result.error(new HttpError(this.server + url, response.status));
|
||||
}
|
||||
|
||||
return Result.ok(await response.json());
|
||||
return Result.ok(await response.json());
|
||||
} catch (error) {
|
||||
return Result.error(new HttpError(error, 0));
|
||||
}
|
||||
}
|
||||
|
||||
public async request<T>(
|
||||
method: HttpMethod,
|
||||
url: string,
|
||||
data?: any
|
||||
): Promise<T> {
|
||||
public async _request<T>(method: HttpMethod, url: string, data?: any): Promise<Result<HttpError, T>> {
|
||||
const reqInit = {
|
||||
body: data,
|
||||
method: method,
|
||||
};
|
||||
|
||||
if (data !== undefined) {
|
||||
reqInit["body"] = data;
|
||||
}
|
||||
const response = await fetch(this.server + url, reqInit);
|
||||
if (response.status !== 200) {
|
||||
throw new Error(await response.json());
|
||||
throw Result.error(new Error(await response.json()));
|
||||
}
|
||||
return Result.ok(response.text as T);
|
||||
}
|
||||
public async _jsonToClassInstanceRequest<T>(
|
||||
method: HttpMethod,
|
||||
url: string,
|
||||
instance: ClassConstructor<T>,
|
||||
data?: any
|
||||
) {
|
||||
try {
|
||||
const reqInit = {
|
||||
body: data,
|
||||
method: method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
};
|
||||
if (data !== undefined) {
|
||||
reqInit["body"] = JSON.stringify(data);
|
||||
}
|
||||
const response = await fetch(this.server + url, reqInit);
|
||||
|
||||
if (response.status !== 200) {
|
||||
return Result.error(new HttpError(this.server + url, response.status));
|
||||
}
|
||||
return Result.ok(plainToInstance(instance, await response.json()) as T);
|
||||
} catch (error) {
|
||||
return Result.error(new HttpError(error, 0));
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,18 +21,17 @@ import {
|
|||
CreateProcessScreen,
|
||||
CreateProcessScreenPath,
|
||||
} from "../../features/create_process/presentation/create_process_screen";
|
||||
import { ProjectRepository } from "../../features/all_projects/data/project_repository";
|
||||
import {
|
||||
CreateProjectInstancePath,
|
||||
CreateProjectInstanceScreen,
|
||||
} from "../../features/create_project_instance/create_project_instance";
|
||||
import { SceneManger, SceneManagerPath } from "../../features/scene_manager/presentation/scene_manager";
|
||||
|
||||
const idURL = ":id";
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
path: AllProjectScreenPath,
|
||||
loader: new ProjectRepository().loader,
|
||||
element: <AllProjectScreen />,
|
||||
},
|
||||
{
|
||||
|
@ -63,4 +62,8 @@ export const router = createBrowserRouter([
|
|||
path: CreateProjectInstancePath + idURL,
|
||||
element: <CreateProjectInstanceScreen />,
|
||||
},
|
||||
{
|
||||
path: SceneManagerPath + idURL,
|
||||
element: <SceneManger />,
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -1,22 +1,50 @@
|
|||
// TODO(IDONTSUDO): нужно переписать все запросы под BaseStore
|
||||
|
||||
import { NavigateFunction } from "react-router-dom";
|
||||
import { Result } from "../helper/result";
|
||||
import { UiBaseError } from "../model/ui_base_error";
|
||||
import { HttpError } from "../repository/http_repository";
|
||||
|
||||
export class BaseStore {
|
||||
export type CoreError = HttpError | Error;
|
||||
|
||||
export abstract class UiLoader {
|
||||
isLoading = false;
|
||||
isError = false;
|
||||
|
||||
async loadingHelper<T>(callBack: Promise<Result<any, T>>) {
|
||||
async httpHelper<T>(callBack: Promise<Result<any, T>>) {
|
||||
this.isLoading = true;
|
||||
|
||||
const result = await callBack;
|
||||
if (result.isFailure()) {
|
||||
this.isError = true;
|
||||
this.isLoading = false;
|
||||
this.errorHandingStrategy(result.error);
|
||||
return result.forward();
|
||||
}
|
||||
|
||||
this.isLoading = false;
|
||||
return result;
|
||||
}
|
||||
abstract errorHandingStrategy: (error?: any) => void;
|
||||
|
||||
mapOk = async <T>(property: string, callBack: Promise<Result<CoreError, T>>) => {
|
||||
return (
|
||||
(await this.httpHelper(callBack))
|
||||
// eslint-disable-next-line array-callback-return
|
||||
.map((el) => {
|
||||
// @ts-ignore
|
||||
this[property] = el;
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
||||
export class SimpleErrorState extends UiLoader {
|
||||
errorHandingStrategy = () => {
|
||||
this.isError = true;
|
||||
};
|
||||
isError = false;
|
||||
}
|
||||
|
||||
export abstract class UiErrorState<T> extends UiLoader {
|
||||
abstract errorHandingStrategy: (error: T) => void;
|
||||
abstract init(navigate?: NavigateFunction): Promise<any>;
|
||||
dispose() {}
|
||||
errors: UiBaseError[] = [];
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue