added scene manager

This commit is contained in:
IDONTSUDO 2024-01-23 17:23:10 +03:00
parent ae9842d5e1
commit 2adb939f37
36 changed files with 703 additions and 196 deletions

View file

@ -16,6 +16,7 @@ declare global {
}
interface String {
isEmpty(): boolean;
isNotEmpty(): boolean;
}
interface Map<K, V> {
addValueOrMakeCallback(key: K, value: V, callBack: CallBackVoidFunction): void;

View file

@ -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;
};
}
};

View 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;
}

View 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);
},
];
};

View file

@ -1,7 +1,6 @@
export interface ActivePipeline {
pipelineIsRunning: boolean;
projectUUID?: string | null;
projectId?: string | null;
lastProcessCompleteCount: number | null;
error: any;
}

View file

@ -6,47 +6,137 @@ import {
WebGLRenderer,
AmbientLight,
Vector3,
MeshBasicMaterial,
Mesh,
BoxGeometry,
Object3DEventMap,
Box3,
Sphere,
LineBasicMaterial,
EdgesGeometry,
Intersection,
Raycaster,
LineSegments,
Vector2,
Color,
GridHelper,
CameraHelper,
} 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";
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 CoreThereRepository extends TypedEvent<BaseSceneItemModel> {
scene = new Scene();
camera: PerspectiveCamera;
webGlRender: WebGLRenderer;
htmlCanvasRef: HTMLCanvasElement;
objectEmissive = new Map<string, IEmissiveCache>();
constructor(htmlCanvasRef: HTMLCanvasElement) {
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 = window.outerWidth / window.outerHeight;
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();
}
setRayCastAndGetFirstObject(vector: Vector2): Result<void, string> {
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);
}
});
}
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);
@ -56,54 +146,169 @@ export class CoreThereRepository extends TypedEvent<BaseSceneItemModel> {
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);
setTransformControlsAttach(object: Object3D<Object3DEventMap>) {
if (object instanceof CameraHelper) {
this.transformControls.attach(object.camera);
return;
}
this.transformControls.attach(object);
}
init() {
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);
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);
}
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(window.outerWidth, window.outerHeight);
this.webGlRender.setSize(this.htmlSceneWidth, this.htmlSceneHeight);
this.webGlRender.setAnimationLoop(() => {
this.webGlRender.render(this.scene, this.camera);
});
}
getAllSceneModels(): BaseSceneItemModel[] {
return this.getAllSceneNameModels().map((e) => new StaticAssetItemModel(e));
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(urls: string[], callBack: Function) {}
loader(urls: string[], callBack: Function, name: string) {
urls.map((el) => {
const ext = el.split(/\./g).pop()!.toLowerCase();
switch (ext) {
case "gltf":
case "glb":
this.glbLoader.load(
el,
(result) => {},
(err) => {}
);
break;
case "obj":
this.objLoader.load(
el,
(result) => {
result.userData[UserData.selectedObject] = true;
result.children.forEach((el) => {
el.userData[UserData.selectedObject] = true;
el.name = name;
this.emit(new StaticAssetItemModel(el.name, el.position, el.quaternion));
this.scene.add(el);
});
},
(err) => {}
);
break;
case "dae":
this.daeLoader.load(
el,
(result) => {},
(err) => {}
);
break;
case "stl":
this.stlLoader.load(
el,
(result) => {},
(err) => {}
);
break;
}
});
}
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)
);
@ -129,8 +334,9 @@ export class CoreThereRepository extends TypedEvent<BaseSceneItemModel> {
let orbitControls = new OrbitControls(this.camera, this.htmlCanvasRef);
orbitControls.maxDistance = cameraToFarEdge * 2;
new OrbitControls(this.camera, this.htmlCanvasRef);
this.orbitControls = orbitControls;
}
switchObjectEmissive(name: string) {
const mesh = this.getObjectsAtName(name);
const result = this.objectEmissive.get(mesh.name);
@ -150,9 +356,8 @@ export class CoreThereRepository extends TypedEvent<BaseSceneItemModel> {
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);
newMesh.position.copy(mesh.position);
this.scene.remove(mesh);
this.scene.add(newMesh);
}
@ -180,10 +385,8 @@ export class CoreThereRepository extends TypedEvent<BaseSceneItemModel> {
let newCameraPos = bsWorld.clone().add(cameraOffs);
this.camera.translateX(newCameraPos.x);
this.camera.translateY(newCameraPos.y);
this.camera.translateZ(newCameraPos.z);
this.camera.position.copy(newCameraPos);
this.camera.lookAt(bsWorld);
new OrbitControls(this.camera, this.htmlCanvasRef);
this.orbitControls = new OrbitControls(this.camera, this.htmlCanvasRef);
}
}

View file

@ -1,3 +1,4 @@
import { ClassConstructor, plainToInstance } from "class-transformer";
import { Result } from "../helper/result";
export enum HttpMethod {
@ -16,7 +17,7 @@ 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>> {
public async _formDataRequest<T>(method: HttpMethod, url: string, data?: any): Promise<Result<HttpError, T>> {
let formData = new FormData();
formData.append("file", data);
@ -25,23 +26,19 @@ export class HttpRepository {
method: method,
};
if (data !== undefined) {
reqInit["body"] = data;
}
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>> {
public async _jsonRequest<T>(method: HttpMethod, url: string, data?: any): Promise<Result<HttpError, T>> {
try {
const reqInit = {
body: data,
method: method,
headers: { "Content-Type": "application/json" },
};
console.log(reqInit);
if (data !== undefined) {
reqInit["body"] = JSON.stringify(data);
}
@ -57,7 +54,7 @@ export class HttpRepository {
}
}
public async request<T>(method: HttpMethod, url: string, data?: any): Promise<Result<HttpError, T>> {
public async _request<T>(method: HttpMethod, url: string, data?: any): Promise<Result<HttpError, T>> {
const reqInit = {
body: data,
method: method,
@ -72,4 +69,29 @@ export class HttpRepository {
}
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));
}
}
}

View file

@ -25,7 +25,7 @@ import {
CreateProjectInstancePath,
CreateProjectInstanceScreen,
} from "../../features/create_project_instance/create_project_instance";
import { SceneManger, SceneManagerPath } from "../../features/scene_manager/scene_manager";
import { SceneManger, SceneManagerPath } from "../../features/scene_manager/presentation/scene_manager";
const idURL = ":id";