diff --git a/.vscode/settings.json b/.vscode/settings.json index da63cc5..30e0524 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,6 +15,6 @@ "*ui": false, "*ui.*": false }, - "cSpell.words": ["antd", "fileupload", "metadatas", "undici", "uuidv"], + "cSpell.words": ["antd", "Collada", "Contolls", "fileupload", "metadatas", "undici", "uuidv"], "editor.rulers": [100] } diff --git a/server/.gitignore b/server/.gitignore index 10894c2..b8422f0 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -9,3 +9,4 @@ package-lock.json build/ model_create.ts public +p.ts \ No newline at end of file diff --git a/server/src/core/controllers/http_controller.ts b/server/src/core/controllers/http_controller.ts index 346e8ea..7d9d6b9 100644 --- a/server/src/core/controllers/http_controller.ts +++ b/server/src/core/controllers/http_controller.ts @@ -12,7 +12,7 @@ export abstract class CallbackStrategyWithEmpty { } export abstract class CallbackStrategyWithValidationModel { abstract validationModel: V; - abstract call(a: V): ResponseBase; + abstract call(model: V): ResponseBase; } export abstract class CallbackStrategyWithIdQuery { abstract idValidationExpression: CoreValidation; @@ -25,7 +25,8 @@ export abstract class CallBackStrategyWithQueryPage { export abstract class CallbackStrategyWithFileUpload { abstract checkingFileExpression: RegExp; - abstract call(file: File): ResponseBase; + abstract idValidationExpression: CoreValidation; + abstract call(file: File, id: string, description: string): ResponseBase; } interface ISubSetFeatureRouter { @@ -116,17 +117,32 @@ export class CoreHttpController implements ICoreHttpController { res.status(400).json("need files to form-data request"); return; } + if (req["files"]["file"] === undefined) { res.status(400).json("need file to form data request"); return; } + if (req.query.description === undefined) { + res + .status(400) + .json("request query description is null, need query description &description={description:String}"); + return; + } + if (req.query.id === undefined) { + res.status(400).json("request query id is null, need query id ?id={id:String}"); + return; + } + if (!el.fn.idValidationExpression.regExp.test(req.query.id)) { + res.status(400).json(el.fn.idValidationExpression.message); + return; + } if (el.fn instanceof CallbackStrategyWithFileUpload) { if (!el.fn.checkingFileExpression.test(req["files"]["file"]["name"])) { res.status(400).json("a file with this extension is expected: " + String(el.fn.checkingFileExpression)); return; } } - await this.responseHelper(res, el.fn.call(req["files"]["file"])); + await this.responseHelper(res, el.fn.call(req["files"]["file"], req.query.id, req.query.description)); } }); }); diff --git a/server/src/core/controllers/routes.ts b/server/src/core/controllers/routes.ts index 76e8f90..b76963d 100644 --- a/server/src/core/controllers/routes.ts +++ b/server/src/core/controllers/routes.ts @@ -1,7 +1,10 @@ import { NixStoreManagerPresentation } from "../../features/nix_store_manager/nix_store_manager"; import { PipelinePresentation } from "../../features/pipelines/pipeline_presentation"; import { ProcessPresentation } from "../../features/process/process_presentation"; -import { ProjectInstancePresentation } from "../../features/project_instance/project_instance_presentation"; +import { + ProjectInstancePresentation, + RobossemblerAssetsPresentation, +} from "../../features/project_instance/project_instance_presentation"; import { ProjectsPresentation } from "../../features/projects/projects_presentation"; import { RealTimePresentation } from "../../features/realtime/realtime_presentation"; import { TriggerPresentation } from "../../features/triggers/triggers_presentation"; @@ -21,6 +24,7 @@ export const httpRoutes: Routes[] = [ new RealTimePresentation(), new ProjectInstancePresentation(), new NixStoreManagerPresentation(), + new RobossemblerAssetsPresentation(), ] .concat(routersImplementPureCrud) .map((el) => el.call()); diff --git a/server/src/core/models/active_pipeline_model.ts b/server/src/core/models/active_pipeline_model.ts index 9fa4c4e..3c1d920 100644 --- a/server/src/core/models/active_pipeline_model.ts +++ b/server/src/core/models/active_pipeline_model.ts @@ -1,24 +1,27 @@ export class ActivePipeline { pipelineIsRunning: boolean; - projectUUID?: string | null; + projectId?: string | null; lastProcessCompleteCount: number | null; error: any; + rootDir: string; path: string; constructor( pipelineIsRunning: boolean, lastProcessCompleteCount: number | null, error: any, path: string | null, - projectUUID?: string | null + projectId: string | null, + rootDir: string | null ) { this.pipelineIsRunning = pipelineIsRunning; - this.projectUUID = projectUUID; + this.projectId = projectId; this.lastProcessCompleteCount = lastProcessCompleteCount; this.error = error; this.path = path; + this.rootDir = rootDir; } static empty() { - return new ActivePipeline(false, null, null, null, null); + return new ActivePipeline(false, null, null, null, null, null); } } diff --git a/server/src/core/models/robossembler_assets.ts b/server/src/core/models/robossembler_assets.ts index 8fcf172..d0a99f2 100644 --- a/server/src/core/models/robossembler_assets.ts +++ b/server/src/core/models/robossembler_assets.ts @@ -1,11 +1,6 @@ -import { IsEnum, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator"; +import { IsArray, IsEnum, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator"; import { Type } from "class-transformer"; -export enum AssetsType { - Asset = "asset", - Light = "light", -} - export class Gravity { @IsNumber() x: number; @@ -14,6 +9,7 @@ export class Gravity { @IsNumber() z: number; } + export class Pose { @IsNumber() x: number; @@ -29,37 +25,47 @@ export class Pose { yaw: number; } -export class Instance { - @IsString() - model_name: string; - @IsString() - model_id: string; - @IsString() - id: string; - @ValidateNested() - @Type(() => Pose) - pose: Pose; +export class Position { @IsNumber() - scale: number; - @IsEnum(AssetsType) - type: AssetsType; - @IsOptional() - @IsString() - parent?: string; - @IsOptional() - @IsString() - light_type?: string; - @IsOptional() - @IsString() - intencity?: number; - @IsOptional() - @IsString() - diffuse?: number[]; - @IsOptional() + x: number; @IsNumber() - spot_angle?: number; + y: number; + @IsNumber() + z: number; } +export enum InstanceType { + RGB_CAMERA = "rgb_camera", + SCENE_SIMPLE_OBJECT = "scene_simple_object", +} + +abstract class CoreInstances {} + +export class Instance extends CoreInstances { + @IsEnum(InstanceType) + instanceType: InstanceType; + @Type(() => Position) + position: Position; + @IsArray() + quaternion: number[]; + @IsOptional() + @IsString() + instanceAt: null | string = null; +} + +export class SceneSimpleObject extends Instance {} + +export class InstanceRgbCamera extends Instance { + @IsString() + cameraLink: string; + @IsString() + topicCameraInfo: string; + @IsOptional() + @IsString() + topicDepth: string | null; + @IsString() + topicImage: string; +} export class Asset { @IsString() name: string; @@ -113,14 +119,24 @@ export class RobossemblerAssets { @Type(() => Asset) assets: Asset[]; - @ValidateNested() - @Type(() => Instance) + @IsArray() + @Type(() => Instance, { + discriminator: { + property: "type", + subTypes: [ + { value: InstanceRgbCamera, name: InstanceType.RGB_CAMERA }, + { value: SceneSimpleObject, name: InstanceType.SCENE_SIMPLE_OBJECT }, + ], + }, + keepDiscriminatorProperty: true, + }) instances: Instance[]; @IsOptional() @ValidateNested() @Type(() => Physics) physics: Physics; + convertLocalPathsToServerPaths(server_address: string): RobossemblerAssets { this.assets = this.assets.map((el) => { el.meshPath = server_address + el.meshPath; @@ -128,4 +144,17 @@ export class RobossemblerAssets { }); return this; } + + getAssetPath(assetName: string): string { + const findElement = this.assets.find((el) => el.name === assetName); + + if (findElement === undefined) { + throw new Error("RobossemblerAssets.getAssetPath not found asset by name:" + assetName); + } + return findElement.meshPath; + } + + getAssetAtInstance(instanceAt: string): Asset { + return this.assets.filter((el) => el.name === instanceAt)[0]; + } } diff --git a/server/src/core/scenarios/read_file_and_json_to_plain_instance_class_scenario.ts b/server/src/core/scenarios/read_file_and_json_to_plain_instance_class_scenario.ts index 8281896..8a93795 100644 --- a/server/src/core/scenarios/read_file_and_json_to_plain_instance_class_scenario.ts +++ b/server/src/core/scenarios/read_file_and_json_to_plain_instance_class_scenario.ts @@ -6,24 +6,28 @@ const skipMissingProperties = false, whitelist = true, forbidNonWhitelisted = true; -export class ReadingJsonFileAndConvertingToInstanceClass { +export class ReadingJsonFileAndConvertingToInstanceClassScenario { model: ClassConstructor; constructor(cls: ClassConstructor) { this.model = cls; } call = async (path: string): Promise> => { - const result = await new ReadFileAndParseJsonUseCase().call(path); - if (result.isFailure()) { - return result.forward(); - } - const json = result.value; - const model = plainToInstance(this.model, json); - const errors = await validate(model as object, { skipMissingProperties, whitelist, forbidNonWhitelisted }); - if (errors.length > 0) { - const message = errors.map((error: ValidationError) => Object.values(error.constraints)).join(", "); - return Result.error(message); - } else { - return Result.ok(model as T); + try { + const result = await new ReadFileAndParseJsonUseCase().call(path); + if (result.isFailure()) { + return result.forward(); + } + const json = result.value; + const model = plainToInstance(this.model, json); + const errors = await validate(model as object, { skipMissingProperties, whitelist, forbidNonWhitelisted }); + if (errors.length > 0) { + const message = errors.map((error: ValidationError) => Object.values(error.constraints)).join(", "); + return Result.error("ReadingJsonFileAndConvertingToInstanceClassScenario:" + message); + } else { + return Result.ok(model as T); + } + } catch (error) { + return Result.error("ReadingJsonFileAndConvertingToInstanceClassScenario" + String(error)); } }; } diff --git a/server/src/core/scenarios/set_active_pipeline_to_realtime_service_scenario.ts b/server/src/core/scenarios/set_active_pipeline_to_realtime_service_scenario.ts index a52eb2a..5a8398a 100644 --- a/server/src/core/scenarios/set_active_pipeline_to_realtime_service_scenario.ts +++ b/server/src/core/scenarios/set_active_pipeline_to_realtime_service_scenario.ts @@ -9,14 +9,24 @@ import { SearchDataBaseModelUseCase } from "../usecases/search_database_model_us export class SetLastActivePipelineToRealTimeServiceScenario { call = async (): Promise => { - const result = await new SearchDataBaseModelUseCase(ProjectInstanceDbModel).call({ - isActive: true, - }); - await result.map(async (el) => { - const projectModel = el; - const projectPath = App.staticFilesStoreDir() + projectModel.rootDir + "/"; - await new CreateFolderUseCase().call(projectPath); - pipelineRealTimeService.setPipelineDependency(projectModel.project.pipelines, projectPath, projectModel._id); - }); + return ( + await new SearchDataBaseModelUseCase(ProjectInstanceDbModel).call({ + isActive: true, + }) + ).fold( + async (projectModel) => { + const projectPath = App.staticFilesStoreDir() + projectModel.rootDir + "/"; + await new CreateFolderUseCase().call(projectPath); + pipelineRealTimeService.setPipelineDependency( + projectModel.project.pipelines, + projectPath, + projectModel._id, + projectModel.rootDir + ); + }, + async (_e) => { + console.log("not found active pipeline"); + } + ); }; } diff --git a/server/src/core/services/pipeline_real_time_service.ts b/server/src/core/services/pipeline_real_time_service.ts index cea7af3..3213df4 100644 --- a/server/src/core/services/pipeline_real_time_service.ts +++ b/server/src/core/services/pipeline_real_time_service.ts @@ -32,9 +32,8 @@ export class PipelineRealTimeService extends TypedEvent { this.iterationLogSaver(iteration); } - iterationLogSaver(iteration: Iteration): void { + iterationLogSaver(_iteration: Iteration): void { // throw new Error("Method not implemented."); - // citeration.result.data } iterationErrorObserver(iteration: Iteration): void { @@ -59,10 +58,11 @@ export class PipelineRealTimeService extends TypedEvent { this.status.pipelineIsRunning = false; } } - setPipelineDependency(pipelineModels: IPipeline[], path: string, projectUUID: string) { + setPipelineDependency(pipelineModels: IPipeline[], path: string, projectId: string, rootDir: string) { this.pipelineModels = pipelineModels; - this.status["projectUUID"] = projectUUID; + this.status["projectId"] = projectId; this.status["path"] = path; + this.status["rootDir"] = rootDir; } runPipeline(): void { const stack = new StackService(this.pipelineModels, this.status.path); diff --git a/server/src/core/usecases/create_file_usecase.ts b/server/src/core/usecases/create_file_usecase.ts index f2370ed..e36b399 100644 --- a/server/src/core/usecases/create_file_usecase.ts +++ b/server/src/core/usecases/create_file_usecase.ts @@ -8,7 +8,8 @@ export class CreateFileUseCase { } async call(path: string, buffer: Buffer): Promise> { try { - await this.fileSystemRepository.writeFileAsync(path, buffer); + await this.fileSystemRepository.writeFileAsync(path.pathNormalize(), buffer); + return Result.ok(); } catch (err) { return Result.error(err as Error); diff --git a/server/src/core/usecases/read_file_and_parse_json.ts b/server/src/core/usecases/read_file_and_parse_json.ts index 9e67ec0..4669621 100644 --- a/server/src/core/usecases/read_file_and_parse_json.ts +++ b/server/src/core/usecases/read_file_and_parse_json.ts @@ -13,9 +13,13 @@ export class ReadFileAndParseJsonUseCase { return Result.error(`ReadFileAndParseJsonUseCase got the bad way: ${path}`); } const file = await this.fileSystemRepository.readFileAsync(path); - return Result.ok(JSON.parse(file.toString())); + try { + return Result.ok(JSON.parse(file.toString())); + } catch { + return Result.error(`ReadFileAndParseJsonUseCase is not json type file parse path: ${path}`); + } } catch (error) { - return Result.error(`ReadFileAndParseJsonUseCase is not json type file parse path:${path}`); + return Result.error(`ReadFileAndParseJsonUseCase error:${error}`); } } } diff --git a/server/src/core/usecases/write_file_system_file_usecase.ts b/server/src/core/usecases/write_file_system_file_usecase.ts new file mode 100644 index 0000000..57d9734 --- /dev/null +++ b/server/src/core/usecases/write_file_system_file_usecase.ts @@ -0,0 +1,13 @@ +import { Result } from "../helpers/result"; +import { FileSystemRepository } from "../repository/file_system_repository"; + +export class WriteFileSystemFileUseCase { + call = async (path: string, fileData: string): Promise> => { + try { + await new FileSystemRepository().writeFileAsync(path, fileData); + return Result.ok(undefined); + } catch (error) { + return Result.error(`WriteFileSystemFileUseCase error: ${error}`); + } + }; +} diff --git a/server/src/core/validations/core_validation.ts b/server/src/core/validations/core_validation.ts index e9571a0..e6ef0f8 100644 --- a/server/src/core/validations/core_validation.ts +++ b/server/src/core/validations/core_validation.ts @@ -1,4 +1,8 @@ +interface IMessage { + message: string; +} + export abstract class CoreValidation { abstract regExp: RegExp; - abstract message: string; + abstract message: IMessage; } diff --git a/server/src/core/validations/mongo_id_validation.ts b/server/src/core/validations/mongo_id_validation.ts index 9cc8275..68e9d3a 100644 --- a/server/src/core/validations/mongo_id_validation.ts +++ b/server/src/core/validations/mongo_id_validation.ts @@ -2,5 +2,5 @@ import { CoreValidation } from "./core_validation"; export class MongoIdValidation extends CoreValidation { regExp = RegExp("^[0-9a-fA-F]{24}$"); - message = "is do not mongo db object uuid"; + message = { message: "is do not mongo db object uuid" }; } diff --git a/server/src/features/project_instance/domain/create_new_project_scenario.ts b/server/src/features/project_instance/domain/create_new_project_scenario.ts index bc9bfab..76023b1 100644 --- a/server/src/features/project_instance/domain/create_new_project_scenario.ts +++ b/server/src/features/project_instance/domain/create_new_project_scenario.ts @@ -2,28 +2,20 @@ import { App } from "../../../core/controllers/app"; import { Result } from "../../../core/helpers/result"; import { CreateDataBaseModelUseCase } from "../../../core/usecases/create_database_model_usecase"; import { CreateFolderUseCase } from "../../../core/usecases/create_folder_usecase"; +import { pipelineRealTimeService } from "../../realtime/realtime_presentation"; import { ProjectInstanceDbModel } from "../models/project_instance_database_model"; import { ProjectInstanceValidationModel } from "../models/project_instance_validation_model"; import { v4 as uuidv4 } from "uuid"; +import { SetActiveProjectScenario } from "./set_active_project_use_scenario"; export class CreateNewProjectInstanceScenario { - call = async (model: ProjectInstanceValidationModel): Promise> => { + call = async (): Promise> => { try { - const folderName = uuidv4() + "/"; - const createFolderUseCase = await new CreateFolderUseCase().call(App.staticFilesStoreDir() + folderName); - if (createFolderUseCase.isFailure()) { - return createFolderUseCase.forward(); - } - model.rootDir = folderName; - - const createDataBaseModelUseCase = await new CreateDataBaseModelUseCase(ProjectInstanceDbModel).call(model); - - if (createDataBaseModelUseCase.isFailure()) { - return createDataBaseModelUseCase.forward(); - } - - return Result.ok({ status: "ok" }); + // (await new SetActiveProjectScenario().call(id)).map(() => { + // return Result.ok({ status: "ok" }); + // }); } catch (error) { + console.log(error); return Result.error(error as Error); } }; diff --git a/server/src/features/project_instance/domain/robossembler_assets_network_mapper_scenario.ts b/server/src/features/project_instance/domain/robossembler_assets_network_mapper_scenario.ts index d96a60f..ead9008 100644 --- a/server/src/features/project_instance/domain/robossembler_assets_network_mapper_scenario.ts +++ b/server/src/features/project_instance/domain/robossembler_assets_network_mapper_scenario.ts @@ -2,7 +2,7 @@ import { CallbackStrategyWithEmpty, ResponseBase } from "../../../core/controlle import { Result } from "../../../core/helpers/result"; import { RobossemblerAssets } from "../../../core/models/robossembler_assets"; import { StaticFiles } from "../../../core/models/static_files"; -import { ReadingJsonFileAndConvertingToInstanceClass } from "../../../core/scenarios/read_file_and_json_to_plain_instance_class_scenario"; +import { ReadingJsonFileAndConvertingToInstanceClassScenario } from "../../../core/scenarios/read_file_and_json_to_plain_instance_class_scenario"; import { GetServerAddressUseCase } from "../../../core/usecases/get_server_address_usecase"; import { PipelineStatusUseCase } from "../../realtime/domain/pipeline_status_usecase"; @@ -13,13 +13,15 @@ export class RobossemblerAssetsNetworkMapperScenario extends CallbackStrategyWit return await result.map(async (activeInstanceModel) => { return ( - await new ReadingJsonFileAndConvertingToInstanceClass(RobossemblerAssets).call( + await new ReadingJsonFileAndConvertingToInstanceClassScenario(RobossemblerAssets).call( `${activeInstanceModel.path}${StaticFiles.robossembler_assets}` ) ).map((robossemblerAssets) => { return new GetServerAddressUseCase().call().map((address) => { return Result.ok( - robossemblerAssets.convertLocalPathsToServerPaths(`${address}/${activeInstanceModel.projectUUID}`) + robossemblerAssets.convertLocalPathsToServerPaths( + `${address}/${activeInstanceModel.rootDir.pathNormalize()}` + ) ); }); }); diff --git a/server/src/features/project_instance/domain/save_active_scene_scenario.ts b/server/src/features/project_instance/domain/save_active_scene_scenario.ts new file mode 100644 index 0000000..6ab753a --- /dev/null +++ b/server/src/features/project_instance/domain/save_active_scene_scenario.ts @@ -0,0 +1,28 @@ +import { CallbackStrategyWithValidationModel, ResponseBase } from "../../../core/controllers/http_controller"; +import { Result } from "../../../core/helpers/result"; +import { RobossemblerAssets } from "../../../core/models/robossembler_assets"; +import { StaticFiles } from "../../../core/models/static_files"; +import { ReadingJsonFileAndConvertingToInstanceClassScenario } from "../../../core/scenarios/read_file_and_json_to_plain_instance_class_scenario"; +import { WriteFileSystemFileUseCase } from "../../../core/usecases/write_file_system_file_usecase"; +import { PipelineStatusUseCase } from "../../realtime/domain/pipeline_status_usecase"; + +export class SaveActiveSceneScenario extends CallbackStrategyWithValidationModel { + validationModel: RobossemblerAssets = new RobossemblerAssets(); + async call(model: RobossemblerAssets): ResponseBase { + return (await new PipelineStatusUseCase().call()).map(async (activeInstanceModel) => + ( + await new ReadingJsonFileAndConvertingToInstanceClassScenario(RobossemblerAssets).call( + `${activeInstanceModel.path}${StaticFiles.robossembler_assets}`.pathNormalize() + ) + ).map(async (prevModel) => { + model.assets = prevModel.assets; + return ( + await new WriteFileSystemFileUseCase().call( + `${activeInstanceModel.path}${StaticFiles.robossembler_assets}`.pathNormalize(), + JSON.stringify(model) + ) + ).map(() => Result.ok("assets is rewrite")); + }) + ); + } +} diff --git a/server/src/features/project_instance/domain/set_active_project_use_scenario.ts b/server/src/features/project_instance/domain/set_active_project_use_scenario.ts index 698d4ed..2d18dc8 100644 --- a/server/src/features/project_instance/domain/set_active_project_use_scenario.ts +++ b/server/src/features/project_instance/domain/set_active_project_use_scenario.ts @@ -1,8 +1,8 @@ import { App } from "../../../core/controllers/app"; import { CallbackStrategyWithIdQuery, ResponseBase } from "../../../core/controllers/http_controller"; import { Result } from "../../../core/helpers/result"; +import { SetLastActivePipelineToRealTimeServiceScenario } from "../../../core/scenarios/set_active_pipeline_to_realtime_service_scenario"; import { CreateFolderUseCase } from "../../../core/usecases/create_folder_usecase"; -import { FindPredicateModelAndUpdateDatabaseModelUseCase } from "../../../core/usecases/find_and_update_database_model_usecase"; import { ReadByIdDataBaseModelUseCase } from "../../../core/usecases/read_by_id_database_model_usecase"; import { UpdateDataBaseModelUseCase } from "../../../core/usecases/update_database_model_usecase"; import { MongoIdValidation } from "../../../core/validations/mongo_id_validation"; @@ -13,16 +13,21 @@ export class SetActiveProjectScenario extends CallbackStrategyWithIdQuery { async call(id: string): ResponseBase { const result = await new ReadByIdDataBaseModelUseCase(ProjectInstanceDbModel).call(id); + // id if (result.isFailure()) { return result.forward(); } - const model = result.value; - return (await new CreateFolderUseCase().call(App.staticFilesStoreDir() + model.rootDir)).map(async () => { + + return await ( + await new CreateFolderUseCase().call(App.staticFilesStoreDir() + model.rootDir) + ).map(async () => { model.isActive = true; - new FindPredicateModelAndUpdateDatabaseModelUseCase(ProjectInstanceDbModel).call({}, {}); - return (await new UpdateDataBaseModelUseCase(ProjectInstanceDbModel).call(model)).map(() => { + return (await new UpdateDataBaseModelUseCase(ProjectInstanceDbModel).call(model)).map(async (el) => { + // TODO(IDONTSUDO): move it to a separate UseCase + await ProjectInstanceDbModel.updateMany({ _id: { $ne: el._id }, isActive: { $eq: true } }, { isActive: false }); + await new SetLastActivePipelineToRealTimeServiceScenario().call(); return Result.ok(`project ${id} is active`); }); }); diff --git a/server/src/features/project_instance/domain/upload_file_to_to_project_scenario.ts b/server/src/features/project_instance/domain/upload_file_to_to_project_scenario.ts index e2ab3e5..deb06e8 100644 --- a/server/src/features/project_instance/domain/upload_file_to_to_project_scenario.ts +++ b/server/src/features/project_instance/domain/upload_file_to_to_project_scenario.ts @@ -1,23 +1,37 @@ +import { App } from "../../../core/controllers/app"; import { CallbackStrategyWithFileUpload, ResponseBase } from "../../../core/controllers/http_controller"; import { Result } from "../../../core/helpers/result"; import { IFile } from "../../../core/interfaces/file"; +import { CreateDataBaseModelUseCase } from "../../../core/usecases/create_database_model_usecase"; import { CreateFileUseCase } from "../../../core/usecases/create_file_usecase"; -import { PipelineStatusUseCase } from "../../realtime/domain/pipeline_status_usecase"; +import { CreateFolderUseCase } from "../../../core/usecases/create_folder_usecase"; +import { MongoIdValidation } from "../../../core/validations/mongo_id_validation"; +import { ProjectInstanceDbModel } from "../models/project_instance_database_model"; +import { ProjectInstanceValidationModel } from "../models/project_instance_validation_model"; +import { v4 as uuidv4 } from "uuid"; +import { SetActiveProjectScenario } from "./set_active_project_use_scenario"; export class UploadCadFileToProjectScenario extends CallbackStrategyWithFileUpload { checkingFileExpression: RegExp = RegExp(".FCStd"); + idValidationExpression = new MongoIdValidation(); - async call(file: IFile): ResponseBase { - const pipelineStatusUseCase = await new PipelineStatusUseCase().call(); - if (pipelineStatusUseCase.isFailure()) { - return pipelineStatusUseCase.forward(); - } - const projectFolder = pipelineStatusUseCase.value.path; - const createFileUseCase = await new CreateFileUseCase().call(projectFolder + file.name, file.data); - if (createFileUseCase.isFailure()) { - return createFileUseCase.forward(); - } - - return Result.ok("ok"); + async call(file: IFile, id: string, description: string): ResponseBase { + const folderName = uuidv4() + "/"; + const model = new ProjectInstanceValidationModel(); + model["project"] = id; + model["description"] = description; + model["rootDir"] = folderName; + model["isActive"] = true; + return (await new CreateFolderUseCase().call(App.staticFilesStoreDir() + folderName)).map(async () => + (await new CreateDataBaseModelUseCase(ProjectInstanceDbModel).call(model)).map(async (databaseModel) => + (await new SetActiveProjectScenario().call(databaseModel.id)).map(async () => + (await new CreateFileUseCase().call(App.staticFilesStoreDir() + folderName + file.name, file.data)).map( + () => { + return Result.ok("ok"); + } + ) + ) + ) + ); } } diff --git a/server/src/features/project_instance/models/project_instance_validation_model.ts b/server/src/features/project_instance/models/project_instance_validation_model.ts index 709cc94..cb3149a 100644 --- a/server/src/features/project_instance/models/project_instance_validation_model.ts +++ b/server/src/features/project_instance/models/project_instance_validation_model.ts @@ -9,4 +9,7 @@ export class ProjectInstanceValidationModel { @IsOptional() public rootDir: string; + + @IsOptional() + public isActive: boolean; } diff --git a/server/src/features/project_instance/project_instance_presentation.ts b/server/src/features/project_instance/project_instance_presentation.ts index cd64ef1..5732113 100644 --- a/server/src/features/project_instance/project_instance_presentation.ts +++ b/server/src/features/project_instance/project_instance_presentation.ts @@ -1,7 +1,10 @@ import { CrudController } from "../../core/controllers/crud_controller"; +import { CoreHttpController } from "../../core/controllers/http_controller"; +import { RobossemblerAssets } from "../../core/models/robossembler_assets"; import { CreateNewProjectInstanceScenario } from "./domain/create_new_project_scenario"; import { RobossemblerAssetsNetworkMapperScenario } from "./domain/robossembler_assets_network_mapper_scenario"; +import { SaveActiveSceneScenario } from "./domain/save_active_scene_scenario"; import { SetActiveProjectScenario } from "./domain/set_active_project_use_scenario"; import { UploadCadFileToProjectScenario } from "./domain/upload_file_to_to_project_scenario"; import { ProjectInstanceDbModel } from "./models/project_instance_database_model"; @@ -31,11 +34,16 @@ export class ProjectInstancePresentation extends CrudController< subUrl: "upload", fn: new UploadCadFileToProjectScenario(), }); - - this.subRoutes.push({ - method: "GET", - subUrl: "assets", - fn: new RobossemblerAssetsNetworkMapperScenario(), - }); + } +} + +export class RobossemblerAssetsPresentation extends CoreHttpController { + constructor() { + super({ + url: "robossembler_assets", + validationModel: RobossemblerAssets, + }); + super.get(new RobossemblerAssetsNetworkMapperScenario().call); + super.post(new SaveActiveSceneScenario().call); } } diff --git a/server/src/features/realtime/domain/pipeline_status_usecase.ts b/server/src/features/realtime/domain/pipeline_status_usecase.ts index 4df9d34..df47fe1 100644 --- a/server/src/features/realtime/domain/pipeline_status_usecase.ts +++ b/server/src/features/realtime/domain/pipeline_status_usecase.ts @@ -6,11 +6,11 @@ export class PipelineStatusUseCase { async call(): Promise> { try { const status = pipelineRealTimeService.status; - if (status.projectUUID !== null) { + if (status.projectId !== null) { return Result.ok(status); } - if (status.projectUUID === null) { + if (status.projectId === null) { return Result.error(new Error("pipelineRealTimeService does not have an active project instance")); } } catch (error) { diff --git a/server/src/features/realtime/domain/run_instance_pipeline_usecase.ts b/server/src/features/realtime/domain/run_instance_pipeline_usecase.ts index 483ad34..37c1c1a 100644 --- a/server/src/features/realtime/domain/run_instance_pipeline_usecase.ts +++ b/server/src/features/realtime/domain/run_instance_pipeline_usecase.ts @@ -1,4 +1,5 @@ import { App } from "../../../core/controllers/app"; +import { CallbackStrategyWithEmpty } from "../../../core/controllers/http_controller"; import { Result } from "../../../core/helpers/result"; import { IPipeline } from "../../../core/models/process_model"; import { ReadByIdDataBaseModelUseCase } from "../../../core/usecases/read_by_id_database_model_usecase"; @@ -28,30 +29,32 @@ const mongoPipelineModelMapper = (el: PipelineValidationModel): IPipeline => { }; return mapObj; }; -export class RunInstancePipelineUseCase { - async call(): Promise> { - const r = await new PipelineStatusUseCase().call(); - if (r.isFailure()) { - return r; - } - const readByIdDataBaseModelUseCase = await new ReadByIdDataBaseModelUseCase( - ProjectInstanceDbModel - ).call(r.value.projectUUID); - if (readByIdDataBaseModelUseCase.isFailure()) { - return readByIdDataBaseModelUseCase.forward(); - } - const projectModel = readByIdDataBaseModelUseCase.value; - const resultMapper = projectModel.project.pipelines.map((el) => mongoPipelineModelMapper(el)); +export class RunInstancePipelineUseCase extends CallbackStrategyWithEmpty { + async call(): Promise> { + return (await new PipelineStatusUseCase().call()).map(async (activePipelineModel) => { + if (activePipelineModel.pipelineIsRunning) { + return Result.error("pipeline is running"); + } + const readByIdDataBaseModelUseCase = await new ReadByIdDataBaseModelUseCase( + ProjectInstanceDbModel + ).call(activePipelineModel.projectId); - pipelineRealTimeService.setPipelineDependency( - resultMapper, - App.staticFilesStoreDir() + projectModel.rootDir + "/", - projectModel._id - ); + if (readByIdDataBaseModelUseCase.isFailure()) { + return readByIdDataBaseModelUseCase.forward(); + } + const projectModel = readByIdDataBaseModelUseCase.value; + const resultMapper = projectModel.project.pipelines.map((el) => mongoPipelineModelMapper(el)); - pipelineRealTimeService.runPipeline(); + pipelineRealTimeService.setPipelineDependency( + resultMapper, + App.staticFilesStoreDir() + projectModel.rootDir + "/", + projectModel._id, + projectModel.rootDir + ); - return Result.ok({ status: "ok" }); + pipelineRealTimeService.runPipeline(); + return Result.ok("ok"); + }); } } diff --git a/server/src/features/realtime/realtime_presentation.ts b/server/src/features/realtime/realtime_presentation.ts index 406ec5d..249fca1 100644 --- a/server/src/features/realtime/realtime_presentation.ts +++ b/server/src/features/realtime/realtime_presentation.ts @@ -18,7 +18,11 @@ export class RealTimePresentation extends CoreHttpController { addValueOrMakeCallback(key: K, value: V, callBack: CallBackVoidFunction): void; diff --git a/ui/src/core/extensions/string.ts b/ui/src/core/extensions/string.ts index 030607e..86b5246 100644 --- a/ui/src/core/extensions/string.ts +++ b/ui/src/core/extensions/string.ts @@ -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; + }; + } }; diff --git a/ui/src/core/helper/debounce.ts b/ui/src/core/helper/debounce.ts new file mode 100644 index 0000000..83ef205 --- /dev/null +++ b/ui/src/core/helper/debounce.ts @@ -0,0 +1,82 @@ +export type Options = { + isImmediate?: boolean; + maxWait?: number; + callback?: (data: Result) => void; +}; + +export interface DebouncedFunction any> { + (this: ThisParameterType, ...args: Args & Parameters): Promise>; + cancel: (reason?: any) => void; +} + +interface DebouncedPromise { + resolve: (result: FunctionReturn) => void; + reject: (reason?: any) => void; +} + +export function debounce any>( + func: F, + waitMilliseconds = 50, + options: Options> = {} +): DebouncedFunction { + let timeoutId: ReturnType | undefined; + const isImmediate = options.isImmediate ?? false; + const callback = options.callback ?? false; + const maxWait = options.maxWait; + let lastInvokeTime = Date.now(); + + let promises: DebouncedPromise>[] = []; + + function nextInvokeTimeout() { + if (maxWait !== undefined) { + const timeSinceLastInvocation = Date.now() - lastInvokeTime; + + if (timeSinceLastInvocation + waitMilliseconds >= maxWait) { + return maxWait - timeSinceLastInvocation; + } + } + + return waitMilliseconds; + } + + const debouncedFunction = function (this: ThisParameterType, ...args: Parameters) { + const context = this; + return new Promise>((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; +} diff --git a/ui/src/core/helper/throttle.ts b/ui/src/core/helper/throttle.ts new file mode 100644 index 0000000..81eb399 --- /dev/null +++ b/ui/src/core/helper/throttle.ts @@ -0,0 +1,29 @@ +export const throttle = ( + 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); + }, + ]; +}; diff --git a/ui/src/core/model/active_pipeline.ts b/ui/src/core/model/active_pipeline.ts index 2079613..d375a1a 100644 --- a/ui/src/core/model/active_pipeline.ts +++ b/ui/src/core/model/active_pipeline.ts @@ -1,7 +1,6 @@ - export interface ActivePipeline { pipelineIsRunning: boolean; - projectUUID?: string | null; + projectId?: string | null; lastProcessCompleteCount: number | null; error: any; } diff --git a/ui/src/core/repository/core_there_repository.ts b/ui/src/core/repository/core_there_repository.ts index 82b8da9..54be617 100644 --- a/ui/src/core/repository/core_there_repository.ts +++ b/ui/src/core/repository/core_there_repository.ts @@ -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; } + export class CoreThereRepository extends TypedEvent { scene = new Scene(); camera: PerspectiveCamera; webGlRender: WebGLRenderer; htmlCanvasRef: HTMLCanvasElement; objectEmissive = new Map(); - 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 { + + 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 { + 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 { 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) { + if (object instanceof CameraHelper) { + this.transformControls.attach(object.camera); + return; + } + this.transformControls.attach(object); } - init() { + + setRayCastAndGetFirstObject(vector: Vector2): Result> { + 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>[]> { + 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 { + 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 { 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 { 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 { 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 { 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); } } diff --git a/ui/src/core/repository/http_repository.ts b/ui/src/core/repository/http_repository.ts index e62eedc..dc6b2ef 100644 --- a/ui/src/core/repository/http_repository.ts +++ b/ui/src/core/repository/http_repository.ts @@ -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(method: HttpMethod, url: string, data?: any): Promise> { + public async _formDataRequest(method: HttpMethod, url: string, data?: any): Promise> { 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(method: HttpMethod, url: string, data?: any): Promise> { + public async _jsonRequest(method: HttpMethod, url: string, data?: any): Promise> { 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(method: HttpMethod, url: string, data?: any): Promise> { + public async _request(method: HttpMethod, url: string, data?: any): Promise> { const reqInit = { body: data, method: method, @@ -72,4 +69,29 @@ export class HttpRepository { } return Result.ok(response.text as T); } + public async _jsonToClassInstanceRequest( + method: HttpMethod, + url: string, + instance: ClassConstructor, + 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)); + } + } } diff --git a/ui/src/core/routers/routers.tsx b/ui/src/core/routers/routers.tsx index 7d58fb5..75731d4 100644 --- a/ui/src/core/routers/routers.tsx +++ b/ui/src/core/routers/routers.tsx @@ -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"; diff --git a/ui/src/index.tsx b/ui/src/index.tsx index dd0f8c8..574c7f8 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -1,5 +1,6 @@ import React from "react"; import ReactDOM from "react-dom/client"; +import "reflect-metadata"; import "antd/dist/antd.min.css"; @@ -7,14 +8,19 @@ import { extensions } from "./core/extensions/extensions"; import { SocketLister } from "./features/socket_lister/socket_lister"; import { RouterProvider } from "react-router-dom"; import { router } from "./core/routers/routers"; +import { SceneManger } from "./features/scene_manager/presentation/scene_manager"; extensions(); const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); root.render( <> - - - + {/* */} + {/* */} + + {/* */} + <> + + ); diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 397b455..f13dfaf 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -15,7 +15,10 @@ "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", - "useDefineForClassFields": true + "useDefineForClassFields": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "strictPropertyInitialization": false }, "include": ["src"] }