diff --git a/server/src/core/controllers/app.ts b/server/src/core/controllers/app.ts index bc48b95..c312f72 100644 --- a/server/src/core/controllers/app.ts +++ b/server/src/core/controllers/app.ts @@ -74,6 +74,7 @@ export class App { this.app.use("/", route.router); }); } + async loadAppDependencies() { await new DataBaseConnectUseCase().call(); await new CheckAndCreateStaticFilesFolderUseCase().call(); diff --git a/ui/package.json b/ui/package.json index 64113dd..35f227d 100644 --- a/ui/package.json +++ b/ui/package.json @@ -26,6 +26,7 @@ "react-scripts": "5.0.1", "sass": "^1.66.1", "socket.io-client": "^4.7.2", + "three": "^0.159.0", "typescript": "^4.9.5", "uuid": "^9.0.0", "web-vitals": "^2.1.4" diff --git a/ui/src/core/extensions/array.ts b/ui/src/core/extensions/array.ts index 0259bbe..4431efc 100644 --- a/ui/src/core/extensions/array.ts +++ b/ui/src/core/extensions/array.ts @@ -45,4 +45,10 @@ export const ArrayExtensions = () => { return this.length !== 0; }; } + if ([].hasIncludeElement === undefined) { + // eslint-disable-next-line no-extend-native + Array.prototype.hasIncludeElement = function (element) { + return this.indexOf(element) !== -1; + }; + } }; diff --git a/ui/src/core/extensions/extensions.ts b/ui/src/core/extensions/extensions.ts index b3476a5..5723394 100644 --- a/ui/src/core/extensions/extensions.ts +++ b/ui/src/core/extensions/extensions.ts @@ -1,19 +1,27 @@ import { ArrayExtensions } from "./array"; +import { MapExtensions } from "./map"; import { StringExtensions } from "./string"; +export type CallBackFunction = (value: T) => void; + declare global { interface Array { // @strict: The parameter is determined whether the arrays must be exactly the same in content and order of this relationship or simply follow the same requirements. equals(array: Array, strict: boolean): boolean; lastElement(): T | undefined; isEmpty(): boolean; - isNotEmpty():boolean; + isNotEmpty(): boolean; + hasIncludeElement(element: T): boolean; } interface String { isEmpty(): boolean; } + interface Map { + addValueOrMakeCallback(key: K, value: V, callBack: CallBackFunction): void; + } } export const extensions = () => { ArrayExtensions(); StringExtensions(); + MapExtensions(); }; diff --git a/ui/src/core/extensions/map.ts b/ui/src/core/extensions/map.ts new file mode 100644 index 0000000..22fd3e7 --- /dev/null +++ b/ui/src/core/extensions/map.ts @@ -0,0 +1,15 @@ +export const MapExtensions = () => { + if (Map.prototype.addValueOrMakeCallback === undefined) { + // eslint-disable-next-line no-extend-native + Map.prototype.addValueOrMakeCallback = function (key, value, fn) { + if (this.has(key)) { + this.set(key, value); + fn(this); + return; + } else { + this.set(key, value); + } + }; + } +}; +Object(); diff --git a/ui/src/core/hook/key_listner.tsx b/ui/src/core/hook/key_listner.tsx new file mode 100644 index 0000000..7c7bb60 --- /dev/null +++ b/ui/src/core/hook/key_listner.tsx @@ -0,0 +1,26 @@ +import React from "react"; + +export const useKeyLister = (fn: Function) => { + // eslint-disable-next-line react-hooks/exhaustive-deps + const pressed = new Map(); + + const registerKeyPress = React.useCallback( + (event: KeyboardEvent, codes: string[], callBack: Function) => { + if (codes.hasIncludeElement(event.code)) { + pressed.addValueOrMakeCallback(event.code, event.type, (e) => { + if (Array.from(pressed.values()).equals(["keydown", "keydown"], false)) { + callBack(); + } + }); + } + }, + [pressed] + ); + + React.useEffect(() => { + window.addEventListener("keyup", (e) => registerKeyPress(e, ["KeyQ", "KeyW"], () => fn)); + window.addEventListener("keydown", (e) => registerKeyPress(e, ["KeyQ", "KeyW"], () => {})); + }, [fn, registerKeyPress]); + + return []; +}; diff --git a/ui/src/core/repository/core_there_repository.ts b/ui/src/core/repository/core_there_repository.ts new file mode 100644 index 0000000..2946834 --- /dev/null +++ b/ui/src/core/repository/core_there_repository.ts @@ -0,0 +1,144 @@ +import { + DirectionalLight, + Object3D, + PerspectiveCamera, + Scene, + WebGLRenderer, + AmbientLight, + Vector3, + MeshBasicMaterial, + Mesh, + BoxGeometry, + Object3DEventMap, + Box3, + Sphere, +} from "three"; +import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; + +export class CoreThereRepository { + scene = new Scene(); + camera: PerspectiveCamera; + webGlRender: WebGLRenderer; + htmlCanvasRef: HTMLCanvasElement; + constructor(htmlCanvasRef: HTMLCanvasElement) { + 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(); + } + 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); + // cube.translateX(position.x); + // cube.translateY(position.y); + // cube.translateZ(position.z); + 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); + }); + } + getAllSceneNameModels(): string[] { + return this.scene.children.filter((el) => el.name !== "").map((el) => el.name); + } + getObjectsAtName(name: string): Object3D { + return this.scene.children.filter((el) => el.name === name)[0]; + } + 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); + } + + 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(); + console.log(cameraOffs); + cameraOffs.multiplyScalar(-FL); + console.log(-FL); + let newCameraPos = bsWorld.clone().add(cameraOffs); + console.log(newCameraPos); + 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); + } +} diff --git a/ui/src/features/scene_manager/scene_manager.tsx b/ui/src/features/scene_manager/scene_manager.tsx new file mode 100644 index 0000000..b1b3b30 --- /dev/null +++ b/ui/src/features/scene_manager/scene_manager.tsx @@ -0,0 +1,43 @@ +import * as React from "react"; +import { CoreThereRepository } from "../../core/repository/core_there_repository"; + +const useKeyLister = (fn: Function) => { + const pressed = new Map(); + + const registerKeyPress = React.useCallback( + (event: KeyboardEvent, codes: string[], callBack: Function) => { + if (codes.hasIncludeElement(event.code)) { + pressed.addValueOrMakeCallback(event.code, event.type, (e) => { + if (Array.from(pressed.values()).equals(["keydown", "keydown"], false)) { + callBack(); + } + }); + } + }, + [pressed] + ); + + React.useEffect(() => { + window.addEventListener("keyup", (e) => registerKeyPress(e, ["KeyQ", "KeyW"], () => fn)); + window.addEventListener("keydown", (e) => registerKeyPress(e, ["KeyQ", "KeyW"], () => {})); + }, [fn, registerKeyPress]); + + return []; +}; +export function SceneManger() { + const canvasRef = React.useRef(null); + let thereRepository: null | CoreThereRepository = null; + React.useEffect(() => { + // eslint-disable-next-line react-hooks/exhaustive-deps + thereRepository = new CoreThereRepository(canvasRef.current as HTMLCanvasElement); + thereRepository.render(); + // thereRepository.fitSelectedObjectToScreen(thereRepository.getAllSceneNameModels()); + thereRepository.fitCameraToCenteredObject(thereRepository.getAllSceneNameModels()); + }); + + return ( +
+ +
+ ); +} diff --git a/ui/src/index.test.tsx b/ui/src/index.test.tsx new file mode 100644 index 0000000..c4ad0ab --- /dev/null +++ b/ui/src/index.test.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { SceneManger } from "./features/scene_manager/scene_manager"; + +test("Content contains var image", () => { + render(); + const car = screen; + expect(car).toBeInTheDocument(); +}); diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 0547828..e8fd3a3 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -7,6 +7,7 @@ import { RouterProvider } from "react-router-dom"; import { router } from "./core/routers/routers"; import { SocketLister } from "./features/socket_lister/socket_lister"; import { extensions } from "./core/extensions/extensions"; +import { SceneManger } from "./features/scene_manager/scene_manager"; extensions(); const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); @@ -14,7 +15,8 @@ const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement) root.render( <> - + {/* */} + );