progress
This commit is contained in:
parent
50822a031d
commit
d8b5018cb2
69 changed files with 3302 additions and 3652 deletions
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
|
@ -1,9 +1,5 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
|
|
|
@ -41,8 +41,9 @@ git clone https://gitlab.com/robossembler/webservice
|
|||
export PYTHON_BLENDER="/путь_к_директории_с_файлами_из/rcg_pipeline"
|
||||
export PYTHON_BLENDER_PROC="/путь_к_генератору_датасетов_/renderBOPdataset.py"
|
||||
export PYTHON_EDUCATION="absolute_path/webp/education.py"
|
||||
```
|
||||
export PYTHON_ROBOT_BUILDER="/путь_к_генератору_датасетов_/robot_builder.py"
|
||||
|
||||
```
|
||||
## Запуск сервера
|
||||
|
||||
Из директории `server` в корне репозитория
|
||||
|
|
2478
server/package-lock.json
generated
2478
server/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -24,24 +24,18 @@
|
|||
"eslint": "^8.47.0",
|
||||
"mocha": "latest",
|
||||
"node-watch": "^0.7.4",
|
||||
"nodemon": "^3.0.1",
|
||||
"nyc": "latest",
|
||||
"source-map-support": "latest",
|
||||
"ts-node": "^10.9.1",
|
||||
"tslint": "latest",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.9.0",
|
||||
"axios": "^1.6.2",
|
||||
"babel-register": "^6.26.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"express-fileupload": "^1.4.2",
|
||||
"first-di": "^1.0.11",
|
||||
"md5": "^2.3.0",
|
||||
"mongoose": "^7.6.2",
|
||||
"mongoose-autopopulate": "^1.1.0",
|
||||
"pattern-matching-ts": "^2.0.0",
|
||||
|
@ -50,8 +44,6 @@
|
|||
"rimraf": "^5.0.5",
|
||||
"socket.io": "^4.7.2",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"spark-md5": "^3.0.2",
|
||||
"ts-md5": "^1.3.1",
|
||||
"ts-pattern": "^5.1.1",
|
||||
"tsc-watch": "^6.0.4",
|
||||
"uuid": "^9.0.1"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export enum StaticFiles {
|
||||
robossembler_assets = "robossembler_assets.json",
|
||||
assets = "/assets/assets.json",
|
||||
parts = '/assets/parts.json'
|
||||
parts = '/assets/parts.json',
|
||||
robots = '/robots/'
|
||||
}
|
||||
|
|
39
server/src/features/scene/create_robot_scenario.ts
Normal file
39
server/src/features/scene/create_robot_scenario.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { CallbackStrategyWithValidationModel, ResponseBase } from "../../core/controllers/http_controller";
|
||||
import { Result } from "../../core/helpers/result";
|
||||
import { RobotModel } from "./robot_model";
|
||||
import { StaticFiles } from "../../core/models/static_files";
|
||||
import { GetServerAddressUseCase } from "../../core/usecases/get_server_address_usecase";
|
||||
import { SearchManyDataBaseModelUseCase } from "../../core/usecases/search_many_database_model_usecase";
|
||||
import { IProjectModel, ProjectDBModel } from "../projects/models/project_model_database_model";
|
||||
import { ExecProcessUseCase } from "../../core/usecases/exec_process_usecase";
|
||||
|
||||
export class CreateRobotScenario extends CallbackStrategyWithValidationModel<RobotModel> {
|
||||
validationModel: RobotModel = new RobotModel();
|
||||
call = async (model: RobotModel): ResponseBase =>
|
||||
(
|
||||
await new SearchManyDataBaseModelUseCase<IProjectModel>(ProjectDBModel).call(
|
||||
{ isActive: true },
|
||||
"is dont active projects"
|
||||
)
|
||||
).map((projectModel) => {
|
||||
const { rootDir } = projectModel[0];
|
||||
|
||||
return new GetServerAddressUseCase().call().map(async (serverAddress) =>
|
||||
(
|
||||
await new ExecProcessUseCase().call(
|
||||
rootDir,
|
||||
`python3 $PYTHON_ROBOT_BUILDER --path ${projectModel[0].rootDir + StaticFiles.robots} --name ${
|
||||
model.name
|
||||
} --nDOF ${model.nDof} --toolType ${model.toolType}`,
|
||||
""
|
||||
)
|
||||
).map(() =>
|
||||
Result.ok({
|
||||
robotUrl: `${serverAddress}/${
|
||||
rootDir.match(new RegExp(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gm))[0]
|
||||
}${StaticFiles.robots}${model.name}/robot.xml`,
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
12
server/src/features/scene/robot_model.ts
Normal file
12
server/src/features/scene/robot_model.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { IsNotEmpty, IsNumber, IsString } from "class-validator";
|
||||
|
||||
export class RobotModel {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
name: string;
|
||||
@IsNumber()
|
||||
nDof: number;
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
toolType: string;
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
import { CrudController } from "../../core/controllers/crud_controller";
|
||||
import { CreateRobotScenario } from "./create_robot_scenario";
|
||||
import { SceneDBModel } from "./scene_database_model";
|
||||
import { SceneValidationModel } from "./scene_validation_model";
|
||||
|
||||
|
||||
export class ScenePresentation extends CrudController<SceneValidationModel, typeof SceneDBModel> {
|
||||
constructor() {
|
||||
super({
|
||||
|
@ -9,5 +10,10 @@ export class ScenePresentation extends CrudController<SceneValidationModel, type
|
|||
validationModel: SceneValidationModel,
|
||||
databaseModel: SceneDBModel,
|
||||
});
|
||||
this.subRoutes.push({
|
||||
method: "POST",
|
||||
subUrl: "create/robot",
|
||||
fn: new CreateRobotScenario(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
30
ui/package-lock.json
generated
30
ui/package-lock.json
generated
|
@ -28,6 +28,7 @@
|
|||
"react-infinite-scroll-component": "^6.1.0",
|
||||
"react-router-dom": "^6.18.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-slider": "^2.0.6",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rete-connection-plugin": "^2.0.0",
|
||||
"rete-react-plugin": "^2.0.4",
|
||||
|
@ -35,7 +36,7 @@
|
|||
"socket.io-client": "^4.7.2",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"styled-components": "^6.1.8",
|
||||
"three": "^0.165.0",
|
||||
"three": "^0.152.2",
|
||||
"ts-pattern": "^5.1.1",
|
||||
"typescript": "^5.0.2",
|
||||
"urdf-loader": "^0.12.1",
|
||||
|
@ -49,6 +50,7 @@
|
|||
"@types/node": "^16.18.46",
|
||||
"@types/react": "^18.2.21",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/react-slider": "^1.3.6",
|
||||
"@types/socket.io-client": "^3.0.0",
|
||||
"@types/three": "^0.158.3",
|
||||
"@types/uuid": "^9.0.2"
|
||||
|
@ -3902,6 +3904,15 @@
|
|||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-slider": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-slider/-/react-slider-1.3.6.tgz",
|
||||
"integrity": "sha512-RS8XN5O159YQ6tu3tGZIQz1/9StMLTg/FCIPxwqh2gwVixJnlfIodtVx+fpXVMZHe7A58lAX1Q4XTgAGOQaCQg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
|
||||
|
@ -14156,6 +14167,17 @@
|
|||
"webpack": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-slider": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/react-slider/-/react-slider-2.0.6.tgz",
|
||||
"integrity": "sha512-gJxG1HwmuMTJ+oWIRCmVWvgwotNCbByTwRkFZC6U4MBsHqJBmxwbYRJUmxy4Tke1ef8r9jfXjgkmY/uHOCEvbA==",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16 || ^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
|
@ -16121,9 +16143,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.165.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.165.0.tgz",
|
||||
"integrity": "sha512-cc96IlVYGydeceu0e5xq70H8/yoVT/tXBxV/W8A/U6uOq7DXc4/s1Mkmnu6SqoYGhSRWWYFOhVwvq6V0VtbplA=="
|
||||
"version": "0.152.2",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.152.2.tgz",
|
||||
"integrity": "sha512-Ff9zIpSfkkqcBcpdiFo2f35vA9ZucO+N8TNacJOqaEE6DrB0eufItVMib8bK8Pcju/ZNT6a7blE1GhTpkdsILw=="
|
||||
},
|
||||
"node_modules/throat": {
|
||||
"version": "6.0.2",
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
"@foxglove/rosmsg": "^5.0.4",
|
||||
"@foxglove/rosmsg2-serialization": "^2.0.3",
|
||||
"@foxglove/ws-protocol": "^0.7.3",
|
||||
"antd": "^4.24.15",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"antd": "^4.24.15",
|
||||
"i18next": "^23.6.0",
|
||||
"just-clone": "^6.2.0",
|
||||
"mobx": "^6.10.0",
|
||||
|
@ -30,7 +30,7 @@
|
|||
"socket.io-client": "^4.7.2",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"styled-components": "^6.1.8",
|
||||
"three": "^0.165.0",
|
||||
"three": "^0.152.2",
|
||||
"ts-pattern": "^5.1.1",
|
||||
"typescript": "^5.0.2",
|
||||
"urdf-loader": "^0.12.1",
|
||||
|
@ -67,12 +67,12 @@
|
|||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/three": "^0.158.3",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.46",
|
||||
"@types/react": "^18.2.21",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/socket.io-client": "^3.0.0",
|
||||
"@types/three": "^0.158.3",
|
||||
"@types/uuid": "^9.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ mason new my_brick_name
|
|||
|
||||
Для большей информации [читайте и смотрите примеры](https://github.com/felangel/mason/blob/master/packages/mason_cli/README.md)
|
||||
|
||||
# Добавить новую форму в деревья поведения
|
||||
# Добавить новую форму в Behavior tree builder
|
||||
|
||||
mason make form -o ./src/features/behavior_tree_builder/presentation/ui/forms
|
||||
|
||||
|
|
|
@ -71,4 +71,10 @@ export const ArrayExtensions = () => {
|
|||
}
|
||||
};
|
||||
}
|
||||
if ([].add === undefined) {
|
||||
Array.prototype.add = function (element) {
|
||||
this.push(element);
|
||||
return this;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -23,6 +23,7 @@ declare global {
|
|||
repeat(quantity: number): Array<T>;
|
||||
rFind<T>(predicate: (value: T, index: number, obj: never[]) => boolean, thisArg?: any): Result<void, T>;
|
||||
maxLength(length: number): Array<T>;
|
||||
add(element: T): Array<T>;
|
||||
}
|
||||
interface Number {
|
||||
fromArray(): number[];
|
||||
|
@ -34,12 +35,13 @@ declare global {
|
|||
isNegative(): boolean;
|
||||
isEven(): boolean;
|
||||
isOdd(): boolean;
|
||||
isEqualR(num: number): Result<void, void>;
|
||||
isEqualR(number: number): Result<void, void>;
|
||||
}
|
||||
|
||||
interface String {
|
||||
isEmpty(): boolean;
|
||||
isNotEmpty(): boolean;
|
||||
isNotEmptyR(): Result<void, string>;
|
||||
replaceMany(searchValues: string[], replaceValue: string): string;
|
||||
isEqual(str: string): boolean;
|
||||
isEqualMany(str: string[]): boolean;
|
||||
|
@ -58,6 +60,9 @@ declare global {
|
|||
getPredicateValue(callBack: (value: V) => boolean): K[];
|
||||
incrementValue(key: K): void;
|
||||
}
|
||||
interface Boolean {
|
||||
r(): Result<void, void>;
|
||||
}
|
||||
}
|
||||
export const extensions = () => {
|
||||
StringExtensions();
|
||||
|
|
|
@ -12,6 +12,14 @@ export const StringExtensions = () => {
|
|||
return this.length !== 0;
|
||||
};
|
||||
}
|
||||
if ("".isNotEmptyR === undefined) {
|
||||
String.prototype.isNotEmptyR = function () {
|
||||
if (this.isEmpty()) {
|
||||
return Result.error(undefined);
|
||||
}
|
||||
return Result.ok(String(this));
|
||||
};
|
||||
}
|
||||
if ("".isEqualR === undefined) {
|
||||
String.prototype.isEqualR = function (str) {
|
||||
if (this === str) {
|
||||
|
|
52
ui/src/core/model/camera_model.ts
Normal file
52
ui/src/core/model/camera_model.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { Quaternion, Vector3, PerspectiveCamera, CameraHelper } from "three";
|
||||
import { CoreThreeRepository, UserData } from "../repository/core_three_repository";
|
||||
import { Result } from "../helper/result";
|
||||
import { SceneModelsTypes } from "./scene_models_type";
|
||||
import { Instance } from "./scene_asset";
|
||||
|
||||
export enum CameraTypes {
|
||||
RGB = "RGB",
|
||||
}
|
||||
export class CameraModel implements Instance {
|
||||
type = SceneModelsTypes.CAMERA;
|
||||
constructor(
|
||||
public quaternion: number[],
|
||||
public vector3: Vector3,
|
||||
public name: string,
|
||||
public cameraType: CameraTypes,
|
||||
public width: number,
|
||||
public updateRate: number,
|
||||
public fov: number,
|
||||
public near: number,
|
||||
public far: number,
|
||||
public height: number,
|
||||
public topic: string,
|
||||
public aspect: number,
|
||||
public parent?: string,
|
||||
public fixed?: string
|
||||
) {}
|
||||
update = (coreThreeRepository: CoreThreeRepository) => this.toWebGl(coreThreeRepository);
|
||||
icon: string = "Camera";
|
||||
toWebGl = (coreThreeRepository: CoreThreeRepository) => {
|
||||
const camera = coreThreeRepository.scene.getObjectByName(this.name);
|
||||
|
||||
if (camera) {
|
||||
coreThreeRepository.scene.remove(coreThreeRepository.scene.getObjectByName(this.name + "camera_helper")!);
|
||||
coreThreeRepository.scene.remove(camera);
|
||||
}
|
||||
const perspectiveCamera = new PerspectiveCamera(this.fov, this.aspect, this.near, this.far);
|
||||
perspectiveCamera.name = this.name;
|
||||
perspectiveCamera.position.copy(this.vector3);
|
||||
perspectiveCamera.quaternion.copy(new Quaternion().fromArray(this.quaternion));
|
||||
const cameraHelper = new CameraHelper(perspectiveCamera);
|
||||
cameraHelper.name = this.name + "camera_helper";
|
||||
perspectiveCamera.userData[UserData.selectedObject] = true;
|
||||
cameraHelper.userData[UserData.selectedObject] = true;
|
||||
coreThreeRepository.scene.add(...[perspectiveCamera, cameraHelper]);
|
||||
};
|
||||
validate = (): Result<string, CameraModel> => {
|
||||
return Result.ok(this);
|
||||
};
|
||||
|
||||
static empty = () => new CameraModel([], new Vector3(0, 0, 0), "", CameraTypes.RGB, 0, 0, 50, 0.1, 2000, 0, "", 0);
|
||||
}
|
57
ui/src/core/model/light_model.ts
Normal file
57
ui/src/core/model/light_model.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { DirectionalLight, Object3D, PointLight, Quaternion, SpotLight, Vector3 } from "three";
|
||||
import { Result } from "../helper/result";
|
||||
import { SceneModelsTypes } from "./scene_models_type";
|
||||
import { CoreThreeRepository } from "../repository/core_three_repository";
|
||||
import { Instance } from "./scene_asset";
|
||||
import { Type } from "class-transformer";
|
||||
export enum TypeLight {
|
||||
POINT = "POINT",
|
||||
DIRECTIONAL = "DIRECTIONAL",
|
||||
SPOT = "SPOT",
|
||||
}
|
||||
export class LightModel implements Instance {
|
||||
type = SceneModelsTypes.LIGHT;
|
||||
color: string;
|
||||
typeLight: TypeLight;
|
||||
intensity: number;
|
||||
width: number;
|
||||
height: number;
|
||||
distance: number;
|
||||
angle: number;
|
||||
penumbra: number;
|
||||
decay: number;
|
||||
@Type(() => Vector3)
|
||||
vector3: Vector3;
|
||||
quaternion: number[];
|
||||
constructor() {}
|
||||
update = (coreThreeRepository: CoreThreeRepository) => this.toWebGl(coreThreeRepository);
|
||||
icon: string = "Light";
|
||||
name: string;
|
||||
isValid = (): Result<void, LightModel> => {
|
||||
// SDF -> point, directional, spot.
|
||||
// THREE -> PointLight,DirectionalLight,SpotLight
|
||||
return Result.ok(this);
|
||||
};
|
||||
toWebGl = (coreThreeRepository: CoreThreeRepository) => {
|
||||
// TODO: maybe change mapper
|
||||
let light: Object3D;
|
||||
this.typeLight
|
||||
.isEqualR(TypeLight.DIRECTIONAL)
|
||||
.map(() => (light = new DirectionalLight(this.color, this.intensity)));
|
||||
this.typeLight
|
||||
.isEqualR(TypeLight.POINT)
|
||||
.map(() => (light = new PointLight(this.color, this.intensity, this.distance, this.decay)));
|
||||
this.typeLight
|
||||
.isEqualR(TypeLight.SPOT)
|
||||
.map(
|
||||
() => (light = new SpotLight(this.color, this.intensity, this.distance, this.angle, this.penumbra, this.decay))
|
||||
);
|
||||
|
||||
light!.castShadow = true;
|
||||
light!.position.copy(this.vector3);
|
||||
light!.quaternion.copy(new Quaternion().fromArray(this.quaternion));
|
||||
|
||||
coreThreeRepository.scene.add(light!);
|
||||
};
|
||||
static empty = () => new LightModel();
|
||||
}
|
35
ui/src/core/model/point_model.ts
Normal file
35
ui/src/core/model/point_model.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { Quaternion, Vector3 } from "three";
|
||||
import { Result } from "../helper/result";
|
||||
import { SceneModelsTypes } from "./scene_models_type";
|
||||
import { Instance } from "./scene_asset";
|
||||
import { CoreThreeRepository } from "../repository/core_three_repository";
|
||||
import { Type } from "class-transformer";
|
||||
|
||||
export class PointModel implements Instance {
|
||||
type = SceneModelsTypes.POINT;
|
||||
name: string;
|
||||
@Type(() => Vector3)
|
||||
vector3: Vector3;
|
||||
color: string = "#E91E63";
|
||||
size: number = 0.5;
|
||||
quaternion: number[];
|
||||
icon: string = "Point";
|
||||
|
||||
constructor() {}
|
||||
update = (coreThreeRepository: CoreThreeRepository) => this.toWebGl(coreThreeRepository);
|
||||
toWebGl = (coreThreeRepository: CoreThreeRepository) =>
|
||||
coreThreeRepository.makePoint(
|
||||
this.name,
|
||||
this.vector3,
|
||||
new Quaternion().fromArray(this.quaternion),
|
||||
this.color,
|
||||
this.size
|
||||
);
|
||||
|
||||
isValid(): Result<string, PointModel> {
|
||||
return Result.ok();
|
||||
}
|
||||
static empty() {
|
||||
return new PointModel();
|
||||
}
|
||||
}
|
|
@ -1,172 +0,0 @@
|
|||
import { IsArray, IsEnum, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
|
||||
export class Gravity {
|
||||
@IsNumber()
|
||||
x: number;
|
||||
@IsNumber()
|
||||
y: number;
|
||||
@IsNumber()
|
||||
z: number;
|
||||
}
|
||||
|
||||
export class Pose {
|
||||
@IsNumber()
|
||||
x: number;
|
||||
@IsNumber()
|
||||
y: number;
|
||||
@IsNumber()
|
||||
z: number;
|
||||
@IsNumber()
|
||||
roll: number;
|
||||
@IsNumber()
|
||||
pitch: number;
|
||||
@IsNumber()
|
||||
yaw: number;
|
||||
}
|
||||
|
||||
export class Position {
|
||||
@IsNumber()
|
||||
x: number;
|
||||
@IsNumber()
|
||||
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;
|
||||
@IsString()
|
||||
ixx: string;
|
||||
@IsString()
|
||||
ixy: string;
|
||||
@IsString()
|
||||
ixz: string;
|
||||
@IsString()
|
||||
iyy: string;
|
||||
@IsString()
|
||||
izz: string;
|
||||
@IsString()
|
||||
mass: string;
|
||||
@IsString()
|
||||
posX: string;
|
||||
@IsString()
|
||||
posY: string;
|
||||
@IsString()
|
||||
posZ: string;
|
||||
@IsString()
|
||||
eulerX: string;
|
||||
@IsString()
|
||||
eulerY: string;
|
||||
@IsString()
|
||||
eulerZ: string;
|
||||
@IsString()
|
||||
iyz: string;
|
||||
@IsString()
|
||||
meshPath: string;
|
||||
@IsString()
|
||||
friction: string;
|
||||
@IsString()
|
||||
centerMassX: string;
|
||||
@IsString()
|
||||
centerMassY: string;
|
||||
@IsString()
|
||||
centerMassZ: string;
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
actions: string[];
|
||||
}
|
||||
|
||||
export class Physics {
|
||||
@IsString()
|
||||
engine_name: string;
|
||||
@Type(() => Gravity)
|
||||
gravity: Gravity;
|
||||
}
|
||||
|
||||
export class RobossemblerAssets {
|
||||
@ValidateNested()
|
||||
@Type(() => Asset, {
|
||||
discriminator: {
|
||||
property: "type",
|
||||
subTypes: [
|
||||
{ value: InstanceRgbCamera, name: InstanceType.RGB_CAMERA },
|
||||
{ value: SceneSimpleObject, name: InstanceType.SCENE_SIMPLE_OBJECT },
|
||||
],
|
||||
},
|
||||
keepDiscriminatorProperty: true,
|
||||
})
|
||||
assets: Asset[];
|
||||
|
||||
@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;
|
||||
return el;
|
||||
});
|
||||
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];
|
||||
}
|
||||
}
|
75
ui/src/core/model/robot_model.ts
Normal file
75
ui/src/core/model/robot_model.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import { Quaternion, Vector3 } from "three";
|
||||
import { SceneModelsTypes } from "./scene_models_type";
|
||||
import { Result } from "../helper/result";
|
||||
import { Instance } from "./scene_asset";
|
||||
import { CoreThreeRepository } from "../repository/core_three_repository";
|
||||
import { Type } from "class-transformer";
|
||||
import { URDFRobot } from "urdf-loader";
|
||||
import { SceneItems, SceneMangerStore } from "../../features/scene_manager/presentation/scene_manager_store";
|
||||
|
||||
export enum ToolTypes {
|
||||
RBS_GRIPPER = "RBS_GRIPPER",
|
||||
}
|
||||
|
||||
interface RobotJoint {
|
||||
limit: {
|
||||
lower: Number;
|
||||
upper: Number;
|
||||
};
|
||||
angel: Number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class RobotModel implements Instance {
|
||||
type = SceneModelsTypes.ROBOT;
|
||||
@Type(() => Vector3)
|
||||
vector3: Vector3;
|
||||
jointPosition: RobotJoint[];
|
||||
quaternion: number[];
|
||||
name: string;
|
||||
httpUrl: string;
|
||||
nDof: number;
|
||||
toolType: string;
|
||||
constructor(
|
||||
vector3: Vector3,
|
||||
quaternion: number[],
|
||||
name: string,
|
||||
httpUrl: string,
|
||||
nDof: number,
|
||||
toolType: string,
|
||||
jointPosition: RobotJoint[]
|
||||
) {
|
||||
this.quaternion = quaternion;
|
||||
this.vector3 = vector3;
|
||||
this.name = name;
|
||||
this.httpUrl = httpUrl;
|
||||
this.nDof = nDof;
|
||||
this.toolType = toolType;
|
||||
this.jointPosition = jointPosition;
|
||||
}
|
||||
icon: string = "Robot";
|
||||
toSceneItems = (sceneMangerStore: SceneMangerStore): SceneItems => {
|
||||
return {
|
||||
fn: () => { },
|
||||
name: this.name,
|
||||
isSelected: false,
|
||||
icon: "Robot",
|
||||
};
|
||||
};
|
||||
toWebGl = (coreThreeRepository: CoreThreeRepository) => {
|
||||
console.log(JSON.stringify(this));
|
||||
coreThreeRepository.loadUrdfRobot(this);
|
||||
};
|
||||
update(coreThreeRepository: CoreThreeRepository): any {
|
||||
const robot = coreThreeRepository.scene.getObjectByName(this.name) as URDFRobot;
|
||||
robot.position.copy(this.vector3);
|
||||
robot.quaternion.copy(new Quaternion().fromArray(this.quaternion));
|
||||
this.jointPosition.forEach((el) => {
|
||||
robot.setJointValue(el.name, el.angel);
|
||||
});
|
||||
}
|
||||
isValid(): Result<string, RobotModel> {
|
||||
return Result.ok(this);
|
||||
}
|
||||
static empty = () => new RobotModel(new Vector3(0, 0, 0), [0, 0, 0, 1], "", "", 1, ToolTypes.RBS_GRIPPER, []);
|
||||
}
|
40
ui/src/core/model/scene_asset.ts
Normal file
40
ui/src/core/model/scene_asset.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { IsArray, IsEnum, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { CameraModel } from "./camera_model";
|
||||
import { SceneModelsTypes } from "./scene_models_type";
|
||||
import { RobotModel } from "./robot_model";
|
||||
import { SolidModel } from "./solid_model";
|
||||
import { PointModel } from "./point_model";
|
||||
import { ZoneModel } from "./zone_model";
|
||||
import { CoreThreeRepository } from "../repository/core_three_repository";
|
||||
import { LightModel } from "./light_model";
|
||||
import { Vector3 } from "three";
|
||||
|
||||
export abstract class Instance {
|
||||
abstract icon: string;
|
||||
@Type(() => Vector3)
|
||||
vector3: Vector3;
|
||||
quaternion: number[];
|
||||
name: string;
|
||||
toWebGl = (coreThreeRepository: CoreThreeRepository) => {};
|
||||
update = (coreThreeRepository: CoreThreeRepository) => {};
|
||||
}
|
||||
|
||||
export class SceneAsset {
|
||||
@IsArray()
|
||||
@Type(() => Instance, {
|
||||
discriminator: {
|
||||
property: "type",
|
||||
subTypes: [
|
||||
{ value: SolidModel, name: SceneModelsTypes.SOLID },
|
||||
{ value: CameraModel, name: SceneModelsTypes.CAMERA },
|
||||
{ value: RobotModel, name: SceneModelsTypes.ROBOT },
|
||||
{ value: PointModel, name: SceneModelsTypes.POINT },
|
||||
{ value: LightModel, name: SceneModelsTypes.LIGHT },
|
||||
{ value: ZoneModel, name: SceneModelsTypes.ZONE },
|
||||
],
|
||||
},
|
||||
keepDiscriminatorProperty: true,
|
||||
})
|
||||
scene: (Instance | SolidModel | CameraModel | RobotModel | PointModel | ZoneModel)[];
|
||||
}
|
8
ui/src/core/model/scene_models_type.ts
Normal file
8
ui/src/core/model/scene_models_type.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export enum SceneModelsTypes {
|
||||
SOLID = "SOLID",
|
||||
ROBOT = "ROBOT",
|
||||
LIGHT = "LIGHT",
|
||||
CAMERA = "CAMERA",
|
||||
POINT = "POINT",
|
||||
ZONE = "ZONE",
|
||||
}
|
26
ui/src/core/model/solid_model.ts
Normal file
26
ui/src/core/model/solid_model.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { Quaternion, Scene, Vector3 } from "three";
|
||||
import { SceneModelsTypes } from "./scene_models_type";
|
||||
import { Instance } from "./scene_asset";
|
||||
import { CoreThreeRepository } from "../repository/core_three_repository";
|
||||
import { Type } from "class-transformer";
|
||||
|
||||
export class SolidModel implements Instance {
|
||||
type = SceneModelsTypes.SOLID;
|
||||
@Type(() => Vector3)
|
||||
public vector3: Vector3;
|
||||
constructor(
|
||||
vector3:Vector3,
|
||||
public quaternion: number[],
|
||||
public name: string,
|
||||
public solidType: string,
|
||||
public mesh: string,
|
||||
public collisionMesh: string
|
||||
) {
|
||||
this.vector3 = vector3
|
||||
}
|
||||
update = (coreThreeRepository: CoreThreeRepository) => this.toWebGl(coreThreeRepository)
|
||||
icon: string = "Solid";
|
||||
toWebGl = (coreThreeRepository: CoreThreeRepository) => coreThreeRepository.loader(this.mesh, () => {}, this.name);
|
||||
|
||||
static empty = () => new SolidModel(new Vector3(0, 0, 0), [], "", "", "", "");
|
||||
}
|
33
ui/src/core/model/zone_model.ts
Normal file
33
ui/src/core/model/zone_model.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { Vector3 } from "three";
|
||||
import { Result } from "../helper/result";
|
||||
import { SceneModelsTypes } from "./scene_models_type";
|
||||
import { Instance } from "./scene_asset";
|
||||
import { CoreThreeRepository } from "../repository/core_three_repository";
|
||||
import { Type } from "class-transformer";
|
||||
|
||||
export class ZoneModel implements Instance {
|
||||
type = SceneModelsTypes.ZONE;
|
||||
color: string = "#E91E63";
|
||||
@Type(() => Vector3)
|
||||
vector3: Vector3;
|
||||
|
||||
constructor(
|
||||
vector3: Vector3,
|
||||
public quaternion: number[],
|
||||
public name: string,
|
||||
public width: number,
|
||||
public height: number,
|
||||
public length: number
|
||||
) {
|
||||
this.vector3 = vector3;
|
||||
}
|
||||
update = (coreThreeRepository: CoreThreeRepository) => this.toWebGl(coreThreeRepository)
|
||||
icon: string = "Zone";
|
||||
toWebGl = (coreThreeRepository: CoreThreeRepository) => coreThreeRepository.makeZone(this);
|
||||
isValid(): Result<string, ZoneModel> {
|
||||
return Result.ok();
|
||||
}
|
||||
static empty() {
|
||||
return new ZoneModel(new Vector3(0, 0, 0), [0, 0, 0, 0], "", 0, 0, 0);
|
||||
}
|
||||
}
|
|
@ -22,25 +22,25 @@ import {
|
|||
MeshBasicMaterial,
|
||||
BoxGeometry,
|
||||
MeshStandardMaterial,
|
||||
LoadingManager,
|
||||
} from "three";
|
||||
import { TypedEvent } from "../helper/typed_event";
|
||||
import { Result } from "../helper/result";
|
||||
import URDFLoader, { URDFLink } from "urdf-loader";
|
||||
import { ColladaLoader } from "three/examples/jsm/loaders/ColladaLoader";
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
|
||||
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader";
|
||||
import { STLLoader } from "three/examples/jsm/loaders/STLLoader";
|
||||
import { OrbitControls, TransformControls } from "three/examples/jsm/Addons";
|
||||
import {
|
||||
BaseSceneItemModel,
|
||||
CameraViewModel,
|
||||
StaticAssetItemModel,
|
||||
} from "../../features/scene_manager/model/scene_assets";
|
||||
import { LoadingManager } from "three";
|
||||
|
||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
|
||||
import { TransformControls } from "three/examples/jsm/controls/TransformControls";
|
||||
import { SceneMode } from "../../features/scene_manager/model/scene_view";
|
||||
import { UrdfTransforms, coordsToQuaternion } from "../../features/simulations/tranforms_model";
|
||||
import URDFLoader, { URDFLink } from "urdf-loader";
|
||||
import { ISolidSpawnHelper } from "../../features/scene_manager/presentation/scene_manager_store";
|
||||
import { ISpawnHelper, SceneItems } from "../../features/scene_manager/presentation/scene_manager_store";
|
||||
import { CameraModel } from "../model/camera_model";
|
||||
import { SolidModel } from "../model/solid_model";
|
||||
import { Instance, SceneAsset } from "../model/scene_asset";
|
||||
import { ZoneModel } from "../model/zone_model";
|
||||
import { TypedEvent } from "../helper/typed_event";
|
||||
import { Result } from "../helper/result";
|
||||
import { RobotModel } from "../model/robot_model";
|
||||
|
||||
Object3D.DEFAULT_UP = new Vector3(0, 0, 1);
|
||||
|
||||
|
@ -63,7 +63,8 @@ interface IEmissiveCache {
|
|||
}
|
||||
type SceneFrames = { [K in string]: URDFLink };
|
||||
|
||||
export class CoreThreeRepository extends TypedEvent<BaseSceneItemModel> {
|
||||
export class CoreThreeRepository extends TypedEvent<any> {
|
||||
|
||||
scene = new Scene();
|
||||
camera: PerspectiveCamera;
|
||||
webGlRender: WebGLRenderer;
|
||||
|
@ -95,7 +96,7 @@ export class CoreThreeRepository extends TypedEvent<BaseSceneItemModel> {
|
|||
});
|
||||
const aspectCamera = this.htmlSceneWidth / this.htmlSceneHeight;
|
||||
this.camera = new PerspectiveCamera(100, aspectCamera, 0.1, 2000);
|
||||
this.camera.position.set(60, 20, 10);
|
||||
this.camera.position.set(1, 1, 1);
|
||||
|
||||
this.webGlRender = renderer;
|
||||
this.htmlCanvasRef = htmlCanvasRef;
|
||||
|
@ -108,6 +109,9 @@ export class CoreThreeRepository extends TypedEvent<BaseSceneItemModel> {
|
|||
|
||||
this.init();
|
||||
}
|
||||
updateInstance = (viewModel: Instance) => (
|
||||
this.scene.remove(this.scene.getObjectByName(viewModel.name)!), viewModel.toWebGl(this)
|
||||
);
|
||||
|
||||
deleteAllObjectsScene = () => {
|
||||
this.getAllSceneNameModels().forEach((el) =>
|
||||
|
@ -124,25 +128,40 @@ export class CoreThreeRepository extends TypedEvent<BaseSceneItemModel> {
|
|||
this.scene.add(cube);
|
||||
return cube;
|
||||
}
|
||||
loadScene = (sceneAssets: SceneAsset) => sceneAssets.scene.forEach((el) => el.toWebGl(this));
|
||||
|
||||
getCenterPoint = (object: Object3D<Object3DEventMap>) =>
|
||||
object.getWorldPosition(new Box3().setFromObject(object).getCenter(object.position));
|
||||
|
||||
makeZone = (zoneModel: ZoneModel) => {
|
||||
const mesh = new Mesh(
|
||||
new BoxGeometry(zoneModel.width, zoneModel.height, zoneModel.length),
|
||||
new MeshBasicMaterial({
|
||||
color: zoneModel.color,
|
||||
transparent: true,
|
||||
})
|
||||
);
|
||||
mesh.name = zoneModel.name;
|
||||
mesh.userData[UserData.selectedObject] = "";
|
||||
mesh.position.copy(zoneModel.vector3);
|
||||
mesh.quaternion.copy(new Quaternion().fromArray(zoneModel.quaternion));
|
||||
this.scene.add(mesh);
|
||||
};
|
||||
makeCube(inc: number, vector?: Vector3, color?: string, size?: number) {
|
||||
const cube = new Mesh(
|
||||
const mesh = new Mesh(
|
||||
new BoxGeometry(size ?? 10, size ?? 10, size ?? 10),
|
||||
new MeshBasicMaterial({ color: color ?? 0x0095dd, transparent: true, opacity: 0.5 })
|
||||
);
|
||||
cube.userData[UserData.objectForMagnetism] = true;
|
||||
cube.userData[UserData.selectedObject] = true;
|
||||
cube.name = "cube" + String(inc);
|
||||
mesh.userData[UserData.objectForMagnetism] = true;
|
||||
mesh.userData[UserData.selectedObject] = true;
|
||||
mesh.name = "cube" + String(inc);
|
||||
|
||||
if (vector) {
|
||||
cube.position.copy(vector);
|
||||
mesh.position.copy(vector);
|
||||
}
|
||||
this.scene.add(cube);
|
||||
this.scene.add(mesh);
|
||||
}
|
||||
|
||||
makePoint(vector?: Vector3, color?: string, size?: number) {
|
||||
makePoint(name: string, vector?: Vector3, quaternion?: Quaternion, color?: string, size?: number) {
|
||||
const cube = new Mesh(
|
||||
new BoxGeometry(size ?? 10, size ?? 10, size ?? 10),
|
||||
new MeshBasicMaterial({ color: color ?? 0x0095dd, transparent: true, opacity: 0.5 })
|
||||
|
@ -153,24 +172,68 @@ export class CoreThreeRepository extends TypedEvent<BaseSceneItemModel> {
|
|||
if (vector) {
|
||||
cube.position.copy(vector);
|
||||
}
|
||||
if (quaternion) {
|
||||
cube.quaternion.copy(quaternion);
|
||||
}
|
||||
cube.name = name;
|
||||
this.scene.add(cube);
|
||||
}
|
||||
deleteSceneItem = (item: string) =>
|
||||
this.scene.children.forEach((el) => el.name.isEqualR(item).map(() => this.scene.remove(el)));
|
||||
deleteSceneItemByName = (name: string) =>
|
||||
this.scene.children.forEach((el) => el.name.isEqualR(name).map(() => this.scene.remove(el)));
|
||||
|
||||
deleteSceneItem = (item: SceneItems) =>
|
||||
item.icon.isEqualR("Camera").fold(
|
||||
() => this.deleteSceneItemByName(item.name + "camera_helper"),
|
||||
() => this.deleteSceneItemByName(item.name)
|
||||
);
|
||||
loadUrdfRobot = (robotModel: RobotModel) =>
|
||||
this.urdfLoader.load(robotModel.httpUrl, (robot) => {
|
||||
robot.userData[UserData.selectedObject] = true;
|
||||
robot.name = robotModel.name;
|
||||
if (robotModel.vector3) robot.position.copy(new Vector3(0, 0, 0));
|
||||
if (robotModel.quaternion) robot.quaternion.copy(new Quaternion().fromArray(robotModel.quaternion));
|
||||
// robot.setJointValue("ee_link_joint", 4);
|
||||
if (robotModel.jointPosition.isEmpty()) {
|
||||
Object.entries(robot.joints).forEach(([name, uRDFJoint]) => {
|
||||
if (uRDFJoint.jointType !== "fixed") {
|
||||
robotModel.jointPosition.push({
|
||||
angel: uRDFJoint.angle,
|
||||
limit: {
|
||||
lower: uRDFJoint.limit.lower,
|
||||
upper: uRDFJoint.limit.upper,
|
||||
},
|
||||
name: name,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Object.entries(robot.joints).forEach(([k, v]) => robot.setJointValue(k, 1));
|
||||
|
||||
loadUrdf = (urlPath: string) => {
|
||||
this.urdfLoader.load(urlPath, (robot) => {
|
||||
this.scene.add(robot);
|
||||
|
||||
// @ts-expect-error
|
||||
this.sceneFrame = robot.frames;
|
||||
});
|
||||
};
|
||||
loadUrdf = (urlPath: string, vector3?: Vector3, quaternion?: Quaternion) =>
|
||||
this.urdfLoader.load(urlPath, (robot) => {
|
||||
robot.userData[UserData.selectedObject] = true;
|
||||
|
||||
solidSpawn(
|
||||
solidSpawn: ISolidSpawnHelper,
|
||||
if (vector3) robot.position.copy(new Vector3(0, 0, 0));
|
||||
if (quaternion) robot.quaternion.copy(quaternion);
|
||||
Object.entries(robot.joints).forEach(([k, v]) => robot.setJointValue(k, 1));
|
||||
|
||||
this.scene.add(robot);
|
||||
|
||||
// @ts-expect-error
|
||||
this.sceneFrame = robot.frames;
|
||||
});
|
||||
|
||||
solidSpawn = (
|
||||
solidSpawn: ISpawnHelper,
|
||||
loadCallback?: (obj: Object3D<Object3DEventMap> | undefined) => void,
|
||||
vector3?: Vector3
|
||||
) {
|
||||
) =>
|
||||
this.loader(
|
||||
solidSpawn.url,
|
||||
() => {
|
||||
|
@ -180,7 +243,7 @@ export class CoreThreeRepository extends TypedEvent<BaseSceneItemModel> {
|
|||
solidSpawn.name,
|
||||
vector3
|
||||
);
|
||||
}
|
||||
|
||||
loadHttpAndPreview(path: string, name: string, loadCallback?: Function) {
|
||||
this.loader(
|
||||
path,
|
||||
|
@ -208,13 +271,15 @@ export class CoreThreeRepository extends TypedEvent<BaseSceneItemModel> {
|
|||
}
|
||||
}
|
||||
|
||||
addSceneCamera(cameraModel: CameraViewModel) {
|
||||
cameraModel.mapPerspectiveCamera(this.htmlSceneWidth, this.htmlSceneHeight).forEach((el) => this.scene.add(el));
|
||||
}
|
||||
addSceneCamera = (cameraModel: CameraModel) => cameraModel.toWebGl(this);
|
||||
|
||||
disposeTransformControlsMode() {
|
||||
this.transformControls.detach();
|
||||
}
|
||||
updateSolidBody = (solidBodyModel: SolidModel) => {
|
||||
const mesh = this.scene.getObjectByName(solidBodyModel.name);
|
||||
mesh?.position.copy(solidBodyModel.vector3);
|
||||
mesh?.quaternion.copy(new Quaternion().fromArray(solidBodyModel.quaternion));
|
||||
};
|
||||
|
||||
disposeTransformControlsMode = () => this.transformControls.detach();
|
||||
|
||||
setRayCastAndGetFirstObjectName(vector: Vector2): Result<void, string> {
|
||||
this.scene.add(this.transformControls);
|
||||
|
@ -310,13 +375,13 @@ export class CoreThreeRepository extends TypedEvent<BaseSceneItemModel> {
|
|||
init() {
|
||||
this.light();
|
||||
this.addListeners();
|
||||
const floor = new GridHelper(1000, 100, 0x888888, 0x444444);
|
||||
floor.geometry.rotateX(Math.PI * 0.5);
|
||||
floor.userData = {};
|
||||
floor.userData[UserData.cameraInitialization] = true;
|
||||
floor.up.copy(new Vector3(0, 0, 1));
|
||||
const gridHelper = new GridHelper(1000, 100, 0x888888, 0x444444);
|
||||
gridHelper.geometry.rotateX(Math.PI * 0.5);
|
||||
gridHelper.userData = {};
|
||||
gridHelper.userData[UserData.cameraInitialization] = true;
|
||||
gridHelper.up.copy(new Vector3(0, 0, 1));
|
||||
|
||||
this.scene.add(floor);
|
||||
this.scene.add(gridHelper);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -326,13 +391,6 @@ export class CoreThreeRepository extends TypedEvent<BaseSceneItemModel> {
|
|||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
@ -367,7 +425,7 @@ export class CoreThreeRepository extends TypedEvent<BaseSceneItemModel> {
|
|||
if (position) el.position.copy(position);
|
||||
if (quaternion) el.quaternion.copy(quaternion);
|
||||
|
||||
this.emit(new StaticAssetItemModel(el.name, el.position, el.quaternion));
|
||||
// this.emit(new StaticAssetItemModel(el.name, el.position, el.quaternion));
|
||||
this.scene.add(el);
|
||||
callBack();
|
||||
});
|
||||
|
@ -379,6 +437,13 @@ export class CoreThreeRepository extends TypedEvent<BaseSceneItemModel> {
|
|||
this.daeLoader.load(
|
||||
url,
|
||||
(result) => {
|
||||
result.scene.children.forEach((el) => {
|
||||
el.userData[UserData.selectedObject] = true;
|
||||
el.name = name;
|
||||
});
|
||||
result.scene.name = name;
|
||||
if (position) result.scene.position.copy(position);
|
||||
if (quaternion) result.scene.quaternion.copy(quaternion);
|
||||
this.scene.add(result.scene);
|
||||
},
|
||||
(err) => console.log(err)
|
||||
|
@ -420,13 +485,13 @@ export class CoreThreeRepository extends TypedEvent<BaseSceneItemModel> {
|
|||
objects.map((el) => this.getObjectsAtName(el)).map((el) => el.position)
|
||||
);
|
||||
|
||||
var size = new Vector3();
|
||||
boundingBox.getSize(size);
|
||||
var vector3 = new Vector3();
|
||||
boundingBox.getSize(vector3);
|
||||
|
||||
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 dx = vector3.z / 2 + Math.abs(vector3.x / 2 / Math.tan(fovh / 2));
|
||||
let dy = vector3.z / 2 + Math.abs(vector3.y / 2 / Math.tan(fov / 2));
|
||||
let cameraZ = Math.max(dx, dy);
|
||||
|
||||
if (offset !== undefined && offset !== 0) cameraZ *= offset;
|
||||
|
@ -473,8 +538,9 @@ export class CoreThreeRepository extends TypedEvent<BaseSceneItemModel> {
|
|||
|
||||
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 boundSphere = new Box3()
|
||||
.setFromPoints(objects.map((el) => this.getObjectsAtName(el)).map((el) => el.position))
|
||||
.getBoundingSphere(new Sphere());
|
||||
let vFoV = this.camera.getEffectiveFOV();
|
||||
let hFoV = this.camera.fov * this.camera.aspect;
|
||||
let FoV = Math.min(vFoV, hFoV);
|
||||
|
|
|
@ -62,21 +62,21 @@ export class SimpleErrorState extends UiLoader {
|
|||
export class ModalStore extends SimpleErrorState {
|
||||
isModalOpen: boolean = false;
|
||||
showModal = () => {
|
||||
this.isModalOpen = true
|
||||
this.isModalOpen = true;
|
||||
};
|
||||
|
||||
handleOk = () => {
|
||||
this.isModalOpen = false
|
||||
this.isModalOpen = false;
|
||||
};
|
||||
|
||||
handleCancel = () => {
|
||||
this.isModalOpen = false
|
||||
this.isModalOpen = false;
|
||||
};
|
||||
}
|
||||
export abstract class UiErrorState<T> extends UiLoader {
|
||||
abstract errorHandingStrategy: (error: T) => void;
|
||||
abstract init(navigate?: NavigateFunction): Promise<any>;
|
||||
dispose() { }
|
||||
dispose() {}
|
||||
errors: UiBaseError[] = [];
|
||||
}
|
||||
|
||||
|
@ -108,6 +108,9 @@ export abstract class UiDrawerFormState<V, E> extends DrawerState<E> {
|
|||
//@ts-ignore
|
||||
this.viewModel = Object.assign(this.viewModel, value);
|
||||
}
|
||||
loadDependency = (viewModel: V) => {
|
||||
this.viewModel = viewModel;
|
||||
};
|
||||
}
|
||||
export abstract class FormState<V, E> extends UiErrorState<E> {
|
||||
abstract viewModel: V;
|
||||
|
@ -115,4 +118,7 @@ export abstract class FormState<V, E> extends UiErrorState<E> {
|
|||
//@ts-ignore
|
||||
this.viewModel = Object.assign(this.viewModel, value);
|
||||
}
|
||||
loadDependency = (viewModel: V) => {
|
||||
this.viewModel = viewModel;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ export interface IIconsProps extends IStyle {
|
|||
onClick?: Function;
|
||||
height?: number;
|
||||
width?: number;
|
||||
isNeedStopPropagation?: boolean;
|
||||
}
|
||||
|
||||
export function Icon(props: IIconsProps) {
|
||||
|
@ -14,8 +15,9 @@ export function Icon(props: IIconsProps) {
|
|||
(node) => {
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
if (props.onClick) props.onClick();
|
||||
if (props.isNeedStopPropagation) e.stopPropagation();
|
||||
}}
|
||||
style={props.style}
|
||||
>
|
||||
|
@ -480,6 +482,150 @@ const getIconSvg = (
|
|||
/>
|
||||
</svg>
|
||||
);
|
||||
case "Plus":
|
||||
return Result.ok(
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width ? width : "20"}
|
||||
height={height ? height : "20"}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path d="M4 12H20M12 4V20" stroke="#000000" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
case "Minus":
|
||||
return Result.ok(
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width ? width : "20"}
|
||||
height={height ? height : "20"}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path d="M6 12L18 12" stroke="#000000" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
case "Zone":
|
||||
return Result.ok(
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
fill="#000000"
|
||||
width={width ? width : "20"}
|
||||
height={height ? height : "20"}
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
viewBox="0 0 512 512"
|
||||
xmlSpace="preserve"
|
||||
>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M256,168.306c-4.513,0-8.17,3.658-8.17,8.17v23.421c0,4.512,3.657,8.17,8.17,8.17c4.513,0,8.17-3.658,8.17-8.17v-23.421 C264.17,171.963,260.513,168.306,256,168.306z" />
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M256,112.204c-4.513,0-8.17,3.658-8.17,8.17v23.421c0,4.512,3.657,8.17,8.17,8.17c4.513,0,8.17-3.658,8.17-8.17v-23.421 C264.17,115.862,260.513,112.204,256,112.204z" />
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M256,56.102c-4.513,0-8.17,3.658-8.17,8.17v23.421c0,4.512,3.657,8.17,8.17,8.17c4.513,0,8.17-3.658,8.17-8.17V64.272 C264.17,59.76,260.513,56.102,256,56.102z" />
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M328.956,288.686l-20.284-11.711c-3.91-2.257-8.906-0.918-11.161,2.99c-2.256,3.908-0.917,8.904,2.99,11.161 l20.284,11.711c1.288,0.743,2.692,1.096,4.077,1.096c2.824,0,5.57-1.466,7.083-4.086 C334.202,295.939,332.863,290.942,328.956,288.686z" />
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M377.54,316.737l-20.283-11.711c-3.906-2.257-8.906-0.918-11.16,2.99c-2.256,3.908-0.917,8.904,2.99,11.161l20.283,11.711 c1.287,0.743,2.692,1.096,4.078,1.096c2.824,0,5.57-1.465,7.083-4.086C382.788,323.991,381.448,318.995,377.54,316.737z" />
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M426.127,344.788l-20.283-11.711c-3.906-2.258-8.906-0.918-11.16,2.99c-2.256,3.908-0.917,8.904,2.99,11.16l20.283,11.711 c1.287,0.743,2.692,1.096,4.078,1.096c2.824,0,5.57-1.465,7.083-4.086C431.373,352.042,430.034,347.046,426.127,344.788z" />
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M214.489,279.967c-2.255-3.909-7.254-5.246-11.161-2.99l-20.284,11.711c-3.908,2.256-5.246,7.253-2.99,11.161 c1.513,2.621,4.259,4.086,7.083,4.086c1.386,0,2.792-0.353,4.078-1.096l20.284-11.711 C215.406,288.871,216.745,283.875,214.489,279.967z" />
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M165.903,308.018c-2.257-3.909-7.254-5.246-11.161-2.99l-20.283,11.711c-3.908,2.256-5.246,7.253-2.99,11.161 c1.513,2.621,4.259,4.086,7.083,4.086c1.386,0,2.792-0.353,4.077-1.096l20.283-11.711 C166.82,316.923,168.159,311.926,165.903,308.018z" />
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M117.317,336.069c-2.257-3.908-7.254-5.247-11.161-2.99L85.873,344.79c-3.908,2.256-5.246,7.253-2.99,11.16 c1.514,2.621,4.259,4.086,7.083,4.086c1.386,0,2.792-0.353,4.077-1.096l20.283-11.711 C118.234,344.974,119.573,339.977,117.317,336.069z" />
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M474.712,125.01L260.085,1.095c-2.527-1.46-5.643-1.46-8.17,0L37.288,125.01c-2.527,1.46-4.085,4.156-4.085,7.075v247.83 c0,2.919,1.558,5.616,4.085,7.075l214.627,123.915c1.264,0.73,2.674,1.095,4.085,1.095c1.411,0,2.821-0.365,4.085-1.095 L474.712,386.99c2.527-1.46,4.085-4.156,4.085-7.075v-247.83C478.797,129.166,477.24,126.469,474.712,125.01z M247.83,489.679 L57.713,379.915l8.027-4.634c3.909-2.256,5.247-7.253,2.991-11.162c-2.255-3.908-7.254-5.246-11.16-2.99l-8.028,4.634V146.236 L239.66,256l-8.029,4.635c-3.908,2.256-5.246,7.253-2.99,11.161c1.514,2.621,4.259,4.086,7.083,4.086 c1.386,0,2.792-0.353,4.077-1.096l8.029-4.635V489.679z M256,224.408c-4.513,0-8.17,3.658-8.17,8.17v9.27L57.713,132.085 L247.83,22.321v9.27c0,4.512,3.657,8.17,8.17,8.17c4.513,0,8.17-3.658,8.17-8.17v-9.27l190.116,109.764L264.17,241.849v-9.27 C264.17,228.067,260.513,224.408,256,224.408z M462.457,365.764l-8.029-4.635c-3.91-2.257-8.907-0.917-11.161,2.99 c-2.256,3.908-0.917,8.904,2.99,11.161l8.027,4.634L264.17,489.679V270.151l8.029,4.635c1.288,0.743,2.692,1.096,4.078,1.096 c2.824,0,5.57-1.465,7.083-4.086c2.256-3.908,0.917-8.904-2.99-11.161L272.34,256l190.116-109.764V365.764z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
case "Point":
|
||||
return Result.ok(
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
width={width ? width : "20"}
|
||||
height={height ? height : "20"}
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
>
|
||||
<g id="🔍-Product-Icons" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
|
||||
<g id="ic_fluent_point_scan_24_regular" fill="#212121" fillRule="nonzero">
|
||||
<path
|
||||
d="M10.2502491,2 C10.6299449,2 10.9437401,2.28215388 10.9934025,2.64822944 L11.0002491,2.75 L11.0006909,7.54409036 C13.8530802,7.88540535 16.1151657,10.1477856 16.4560297,13.0003134 L21.25,13.0000501 C21.6642136,13.0000501 22,13.3358365 22,13.7500501 C22,14.1297458 21.7178461,14.443541 21.3517706,14.4932034 L21.25,14.5000501 L16.4558272,14.5008817 C16.1142704,17.3526115 13.8525157,19.614164 11.0006909,19.9554115 L11.0002491,21.25 C11.0002491,21.6642136 10.6644627,22 10.2502491,22 C9.87055333,22 9.55675813,21.7178461 9.50709571,21.3517706 L9.50024909,21.25 L9.50011804,19.9554486 C6.64814674,19.6143283 4.38624018,17.3527151 4.04467102,14.5008817 L2.75,14.5000501 C2.33578644,14.5000501 2,14.1642636 2,13.7500501 C2,13.3703543 2.28215388,13.0565591 2.64822944,13.0068967 L2.75,13.0000501 L4.04446845,13.0003134 C4.3853449,10.147682 6.64758231,7.88524105 9.50011804,7.54405318 L9.50024909,2.75 C9.50024909,2.33578644 9.83603553,2 10.2502491,2 Z M9.5,14.5 L5.55904782,14.5008541 C5.88028911,16.5231258 7.47773525,18.1202911 9.50014726,18.4411111 L9.5,14.5 Z M14.9414504,14.5008541 L11,14.5 L11.0008468,18.4410324 C13.0230241,18.12003 14.6202353,16.5229605 14.9414504,14.5008541 Z M9.50014726,9.05839069 C7.47723457,9.37929011 5.87949819,10.9771669 5.55880947,13.0001498 L9.5,13 L9.50014726,9.05839069 Z M11.0008468,9.05846938 L11,13 L14.9416887,13.0001498 C14.6324784,11.0495757 13.1360034,9.49422061 11.2158966,9.09771754 L11.0008468,9.05846938 Z"
|
||||
id="🎨-Color"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
case "Light":
|
||||
return Result.ok(
|
||||
<svg
|
||||
width={width ? width : "20"}
|
||||
height={height ? height : "20"}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
xmlSpace="preserve"
|
||||
>
|
||||
<g fill="black">
|
||||
<path
|
||||
fill="black"
|
||||
d="M1028 1556c-3.283 0-6 2.441-6 5.5 0 1.706.905 3.146 2.152 4.36l.994.994a.5.5 0 0 0 .303.148.5.5 0 0 0 .051.998h5a.5.5 0 0 0 .045-.998.5.5 0 0 0 .309-.148l.998-.999c1.212-1.198 2.146-2.651 2.146-4.353 0-3.059-2.716-5.5-6-5.5zm0 1c2.792 0 5 2.036 5 4.5 0 1.334-.743 2.549-1.852 3.644l-1 1a.5.5 0 0 0 .28.854h-4.86a.5.5 0 0 0 .287-.854l-1-1a.5.5 0 0 0-.01-.01c-1.14-1.108-1.847-2.286-1.847-3.64 0-2.464 2.208-4.5 5-4.5zm-2.5 12a.5.5 0 1 0 0 1h1.5v.5a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-.5h1.5a.5.5 0 1 0 0-1z"
|
||||
transform="translate(-1018 -1553.5)"
|
||||
/>
|
||||
<path
|
||||
fill="black"
|
||||
d="M1025.49 1561.006a.5.5 0 0 0-.344.848l.854.853v1.543a.5.5 0 1 0 1 0V1563h2v1.25a.5.5 0 1 0 1 0v-1.543l.854-.854a.5.5 0 0 0-.708-.706l-.853.853h-2.586l-.854-.854a.5.5 0 0 0-.363-.14z"
|
||||
transform="translate(-1018 -1553.5)"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
case "Robot":
|
||||
return Result.ok(
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="#000000"
|
||||
width={width ? width : "20"}
|
||||
height={height ? height : "20"}
|
||||
viewBox="0 0 50 50"
|
||||
>
|
||||
<path d="M39 2.03125C38.738281 2.0625 38.503906 2.199219 38.34375 2.40625L35.40625 6.09375C34.835938 5.378906 33.972656 4.90625 33 4.90625L29 4.90625C27.6875 4.90625 26.566406 5.765625 26.125 6.9375L16.4375 6.9375C14.8125 4.5625 12.082031 3 9 3C4.042969 3 0 7.042969 0 12C0 15.082031 1.5625 17.8125 3.9375 19.4375C3.949219 19.445313 3.957031 19.460938 3.96875 19.46875C3.96875 19.480469 3.96875 19.488281 3.96875 19.5L14.75 39.9375L6.875 39.9375C5.226563 39.9375 3.9375 41.359375 3.9375 43L3.9375 49C3.933594 49.28125 4.046875 49.554688 4.246094 49.753906C4.445313 49.953125 4.71875 50.066406 5 50.0625L39 50.0625C39.28125 50.066406 39.554688 49.953125 39.753906 49.753906C39.953125 49.554688 40.066406 49.28125 40.0625 49L40.0625 43C40.0625 41.359375 38.773438 39.9375 37.125 39.9375L29.90625 39.9375L16.96875 17.0625L26.125 17.0625C26.566406 18.234375 27.6875 19.09375 29 19.09375L33 19.09375C33.972656 19.09375 34.835938 18.621094 35.40625 17.90625L38.34375 21.59375C38.636719 21.960938 39.148438 22.066406 39.5625 21.84375L49.46875 16.4375C49.882813 16.214844 50.074219 15.730469 49.929688 15.285156C49.78125 14.835938 49.339844 14.5625 48.875 14.625C48.753906 14.644531 48.636719 14.6875 48.53125 14.75L39.375 19.78125L36.09375 15.625L36.09375 8.40625L39.375 4.25L48.53125 9.25C48.835938 9.457031 49.230469 9.472656 49.550781 9.292969C49.871094 9.113281 50.0625 8.769531 50.042969 8.402344C50.027344 8.035156 49.804688 7.710938 49.46875 7.5625L39.5625 2.15625C39.390625 2.058594 39.195313 2.015625 39 2.03125 Z M 9 5C11.480469 5 13.664063 6.28125 14.90625 8.21875C14.917969 8.238281 14.925781 8.261719 14.9375 8.28125C14.957031 8.324219 14.976563 8.367188 15 8.40625C15.023438 8.472656 15.054688 8.535156 15.09375 8.59375C15.109375 8.621094 15.140625 8.628906 15.15625 8.65625C15.175781 8.6875 15.195313 8.71875 15.21875 8.75C15.722656 9.71875 16 10.828125 16 12C16 13.160156 15.714844 14.257813 15.21875 15.21875C15.210938 15.230469 15.195313 15.238281 15.1875 15.25C15.175781 15.269531 15.164063 15.292969 15.15625 15.3125C15.152344 15.320313 15.160156 15.335938 15.15625 15.34375C15.132813 15.363281 15.113281 15.382813 15.09375 15.40625C15.027344 15.492188 14.976563 15.585938 14.9375 15.6875C14.925781 15.71875 14.914063 15.75 14.90625 15.78125C13.664063 17.71875 11.480469 19 9 19C5.125 19 2 15.875 2 12C2 8.125 5.125 5 9 5 Z M 9 6.34375C8.933594 6.34375 8.878906 6.371094 8.8125 6.375C8.75 6.390625 8.683594 6.410156 8.625 6.4375C5.707031 6.644531 3.34375 9.03125 3.34375 12C3.34375 15.101563 5.898438 17.65625 9 17.65625C12.101563 17.65625 14.65625 15.101563 14.65625 12C14.65625 9.042969 12.308594 6.660156 9.40625 6.4375C9.394531 6.4375 9.386719 6.40625 9.375 6.40625C9.304688 6.386719 9.230469 6.378906 9.15625 6.375C9.101563 6.375 9.054688 6.34375 9 6.34375 Z M 29 7.09375L33 7.09375C33.511719 7.09375 33.90625 7.488281 33.90625 8L33.90625 16C33.90625 16.511719 33.511719 16.90625 33 16.90625L29 16.90625C28.488281 16.90625 28.09375 16.511719 28.09375 16L28.09375 8C28.09375 7.488281 28.488281 7.09375 29 7.09375 Z M 8.9375 8.34375C8.957031 8.34375 8.980469 8.34375 9 8.34375C11.015625 8.34375 12.65625 9.984375 12.65625 12C12.65625 14.015625 11.015625 15.65625 9 15.65625C6.984375 15.65625 5.34375 14.015625 5.34375 12C5.34375 10.003906 6.949219 8.378906 8.9375 8.34375 Z M 17.5 9.0625L25.90625 9.0625L25.90625 14.9375L17.5 14.9375C17.820313 14.011719 18 13.03125 18 12C18 10.96875 17.820313 9.988281 17.5 9.0625 Z M 15.375 18.3125L27.59375 39.9375L16.96875 39.9375L6.84375 20.71875C7.539063 20.890625 8.253906 21 9 21C11.496094 21 13.742188 19.960938 15.375 18.3125 Z M 6.875 42.0625L16.09375 42.0625C16.34375 42.167969 16.625 42.167969 16.875 42.0625L37.125 42.0625C37.558594 42.0625 37.9375 42.4375 37.9375 43L37.9375 47.9375L6.0625 47.9375L6.0625 43C6.0625 42.4375 6.441406 42.0625 6.875 42.0625Z" />
|
||||
</svg>
|
||||
);
|
||||
case "Camera":
|
||||
return Result.ok(
|
||||
<svg width="18" height="14" viewBox="0 0 18 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import * as React from "react";
|
||||
import { CoreText, CoreTextType } from "../text/text";
|
||||
import { IStyle } from "../../model/style";
|
||||
|
||||
export enum CoreInputType {
|
||||
small = "small",
|
||||
default = "default",
|
||||
}
|
||||
interface IInputProps extends IStyle {
|
||||
label: string;
|
||||
value?: string;
|
||||
|
@ -9,6 +12,7 @@ interface IInputProps extends IStyle {
|
|||
onChange?: (value: string) => void;
|
||||
validation?: (value: string) => boolean;
|
||||
error?: string;
|
||||
type?: CoreInputType;
|
||||
}
|
||||
|
||||
export const CoreInput = (props: IInputProps) => {
|
||||
|
@ -21,34 +25,38 @@ export const CoreInput = (props: IInputProps) => {
|
|||
setAppendInnerText(false);
|
||||
}
|
||||
}, [ref, value, isAppendInnerText, setAppendInnerText, props]);
|
||||
|
||||
const isSmall = props.type !== undefined && props.type.isEqual(CoreInputType.small);
|
||||
return (
|
||||
<div
|
||||
style={Object.assign(
|
||||
{
|
||||
backgroundColor: "rgba(230, 224, 233, 1)",
|
||||
height: 58,
|
||||
|
||||
height: isSmall ? 40 : 58,
|
||||
borderRadius: "4px 4px 0px 0px",
|
||||
borderBottom: "solid 1px black",
|
||||
padding: "10px 10px 10px 10px",
|
||||
|
||||
},
|
||||
props.style
|
||||
)}
|
||||
>
|
||||
<CoreText type={CoreTextType.small} text={props.label} />
|
||||
<CoreText type={CoreTextType.small} style={isSmall ? { fontSize: 8, position:'relative',top:-8 } : {}} text={props.label} />
|
||||
|
||||
<input
|
||||
defaultValue={props.value}
|
||||
style={{
|
||||
backgroundColor: "#00008000",
|
||||
border: 1,
|
||||
fontSize: 16,
|
||||
fontSize: isSmall ? 12 : 16,
|
||||
fontFamily: "Roboto",
|
||||
color: "#1D1B20",
|
||||
height: 24,
|
||||
width: "100%",
|
||||
userSelect: "none",
|
||||
outline: "none",
|
||||
position: isSmall ? "relative" : undefined,
|
||||
top: isSmall ? -8 : undefined,
|
||||
}}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
|
|
109
ui/src/core/ui/inputNumber/input_number.tsx
Normal file
109
ui/src/core/ui/inputNumber/input_number.tsx
Normal file
|
@ -0,0 +1,109 @@
|
|||
import * as React from "react";
|
||||
import { CoreText, CoreTextType } from "../text/text";
|
||||
import { IStyle } from "../../model/style";
|
||||
import { Icon } from "../icons/icons";
|
||||
|
||||
interface IInputProps extends IStyle {
|
||||
label: string;
|
||||
value?: number;
|
||||
step: number;
|
||||
max: number;
|
||||
min: number;
|
||||
subLabel?: React.ReactNode;
|
||||
onChange?: (value: number) => void;
|
||||
validation?: (value: number) => boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const CoreInputNumber = (props: IInputProps) => {
|
||||
const [value, setValue] = React.useState<number>(() => props.value ?? 0);
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
const [isAppendInnerText, setAppendInnerText] = React.useState(true);
|
||||
React.useEffect(() => {
|
||||
if (ref.current && isAppendInnerText) {
|
||||
ref.current.innerText = String(value);
|
||||
setAppendInnerText(false);
|
||||
}
|
||||
}, [ref, value, isAppendInnerText, setAppendInnerText, props]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <div style={{ position: "absolute" }}>
|
||||
<div style={{ width: width, display: "flex", justifyContent: "flex-end" }}>
|
||||
<Icon type="Plus" />
|
||||
<Icon type="Minus" />
|
||||
</div>
|
||||
</div> */}
|
||||
<div
|
||||
style={Object.assign(
|
||||
{
|
||||
backgroundColor: "rgba(230, 224, 233, 1)",
|
||||
height: 58,
|
||||
borderRadius: "4px 4px 0px 0px",
|
||||
borderBottom: "solid 1px black",
|
||||
padding: "10px 10px 10px 10px",
|
||||
},
|
||||
props.style
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<CoreText type={CoreTextType.small} text={props.label} />
|
||||
</div>
|
||||
|
||||
<div style={{ width: "100%", display: "flex", justifyContent: "flex-end" }}>
|
||||
<input
|
||||
style={{
|
||||
backgroundColor: "#00008000",
|
||||
border: 1,
|
||||
fontSize: 16,
|
||||
fontFamily: "Roboto",
|
||||
color: "#1D1B20",
|
||||
height: 24,
|
||||
width: "100%",
|
||||
userSelect: "none",
|
||||
outline: "none",
|
||||
}}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
|
||||
setValue(Number(val));
|
||||
if (val) {
|
||||
if (props.validation !== undefined && props.validation(Number(val)) && props.onChange) {
|
||||
props.onChange(Number(val));
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.onChange && props.validation === undefined) {
|
||||
props.onChange(Number(val));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{value ? (
|
||||
props.validation ? (
|
||||
props.validation(value) ? null : (
|
||||
<div style={{ color: "#ff1d0c" }}>{props.error ? props.error : "error"}</div>
|
||||
)
|
||||
) : null
|
||||
) : null}
|
||||
<Icon
|
||||
type="Plus"
|
||||
onClick={() => {
|
||||
setValue(value + props.step);
|
||||
if (props.onChange) props.onChange(value);
|
||||
}}
|
||||
/>
|
||||
<Icon
|
||||
type="Minus"
|
||||
onClick={() => {
|
||||
setValue(value - props.step);
|
||||
if (props.onChange) props.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -43,8 +43,9 @@ export const CoreSelect = (props: ICoreSelectProps) => {
|
|||
}}
|
||||
>
|
||||
{cursorIsCorses
|
||||
? props.items.map((el) => (
|
||||
? props.items.map((el, i) => (
|
||||
<div
|
||||
key={i}
|
||||
onClick={() => {
|
||||
setValue(el);
|
||||
props.onChange(el);
|
||||
|
|
43
ui/src/core/ui/slider/slider.tsx
Normal file
43
ui/src/core/ui/slider/slider.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import React, { useRef, useState } from "react";
|
||||
// TODO: need
|
||||
export const CoreSlider = ({
|
||||
max,
|
||||
min,
|
||||
step,
|
||||
initialValue,
|
||||
onChange,
|
||||
}: {
|
||||
max: number;
|
||||
min: number;
|
||||
step: number;
|
||||
initialValue: number;
|
||||
onChange: (value: number) => void;
|
||||
}) => {
|
||||
const refSlider = useRef<HTMLDivElement>(null);
|
||||
const refWheel = useRef<HTMLDivElement>(null);
|
||||
const [value, setValue] = useState<number>(initialValue);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (refSlider.current && refWheel.current) {
|
||||
console.log((max - min) / step);
|
||||
console.log(refSlider.current.clientWidth / ((max - min) / step));
|
||||
const oneStep = refSlider.current.clientWidth / ((max - min) / step);
|
||||
|
||||
refWheel.current.style.left = ((value - min) / step) * oneStep + "px";
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<div
|
||||
ref={refSlider}
|
||||
onClick={(event) => {
|
||||
console.log(event)
|
||||
}}
|
||||
style={{ width: "100%", height: 11, backgroundColor: "rgba(104, 80, 164, 1)" }}
|
||||
>
|
||||
<div
|
||||
style={{ position: "relative", width: 21, height: 18, background: "#D9D9D9", borderRadius: "20px", bottom: 3 }}
|
||||
ref={refWheel}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -48,7 +48,6 @@ const getStyle = (type: CoreTextType, color: string | undefined) => {
|
|||
fontFamily: "Roboto",
|
||||
fontWeight: 500,
|
||||
textOverflow: "ellipsis",
|
||||
|
||||
fontSizeAdjust: 16,
|
||||
}
|
||||
if (type.isEqual(CoreTextType.big)) return {
|
||||
|
@ -84,4 +83,4 @@ export function CoreText(props: ITextProps) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
// CREATE COMPONENT , HTML WRITE , JSX->JS TSX -> TS, PROPS , LIFE CYCLE , REACT HOOK, STATE MANAGMENT -> PUBSUB -> Конечный автомат -> ROUTER
|
||||
|
|
@ -1,10 +1,103 @@
|
|||
import { plainToInstance } from "class-transformer";
|
||||
import { Result } from "../../../core/helper/result";
|
||||
import { HttpMethod, CoreHttpRepository } from "../../../core/repository/http_repository";
|
||||
import { CoreError } from "../../../core/store/base_store";
|
||||
import { SceneModel } from "../model/scene_model";
|
||||
import { SceneViewModel } from "../model/scene_view_model";
|
||||
import { SceneAsset } from "../../../core/model/scene_asset";
|
||||
|
||||
export class SceneHttpRepository extends CoreHttpRepository {
|
||||
getScene = () => {
|
||||
return plainToInstance(SceneAsset, {
|
||||
scene: [
|
||||
// {
|
||||
// quaternion: [0, 0, 0, 1],
|
||||
// vector3: { x: 1, y: 1, z: 1 },
|
||||
// name: "body_down",
|
||||
// solidType: "active",
|
||||
// mesh: "http://localhost:4001/d370204b-205b-44bb-9698-3bf083b4b7a7/assets/libs/objects/body_down.dae",
|
||||
// collisionMesh: "http://localhost:4001/d370204b-205b-44bb-9698-3bf083b4b7a7/assets/libs/objects/body_down.dae",
|
||||
// type: "SOLID",
|
||||
// },
|
||||
// {
|
||||
// quaternion: [0.3797976276673656, 0.5269410603416079, 0.6168038601812418, 0.44456706919183975],
|
||||
// vector3: { x: 60, y: 20.000000000000004, z: 9.999999999999993 },
|
||||
// name: "231",
|
||||
// cameraType: "RGB",
|
||||
// width: 0,
|
||||
// updateRate: 0,
|
||||
// fov: 50,
|
||||
// near: 0.1,
|
||||
// far: 2000,
|
||||
// height: 0,
|
||||
// topic: "231",
|
||||
// type: "CAMERA",
|
||||
// aspect: 0.48721804511278194,
|
||||
// },
|
||||
|
||||
// {
|
||||
// type: "POINT",
|
||||
// name: "POINT 1",
|
||||
// quaternion: [0, 0, 0, 1],
|
||||
// vector3: { x: 0.504300334422403, y: 0.66062343475878, z: 1 },
|
||||
// },
|
||||
// {
|
||||
// type: "ZONE",
|
||||
// name: "ZONE 1",
|
||||
// quaternion: [0, 0, 0, 1],
|
||||
// vector3: { x: 0, y: 0, z: 0 },
|
||||
// width: 100,
|
||||
// height: 100,
|
||||
// length: 10,
|
||||
// },
|
||||
{
|
||||
quaternion: [0, 0, 0, 1],
|
||||
vector3: { x: 0, y: 10, z: 0 },
|
||||
name: "ARM0",
|
||||
nDof: 6,
|
||||
httpUrl: "http://localhost:4001/d370204b-205b-44bb-9698-3bf083b4b7a7/robots/124/robot.xml",
|
||||
toolType: "",
|
||||
jointPosition: [],
|
||||
type: "ROBOT",
|
||||
},
|
||||
// {
|
||||
// type: "LIGHT",
|
||||
// name:"light1S",
|
||||
|
||||
// typeLight: "SPOT",
|
||||
// color: "0xffffff",
|
||||
// intensity: 100,
|
||||
// distance: 100,
|
||||
// angle: 100,
|
||||
// penumbra: 100,
|
||||
// decay: 100,
|
||||
// quaternion: [0, 0, 0, 1],
|
||||
// vector3: { x: 0, y: 0, z: 0 },
|
||||
// },
|
||||
// {
|
||||
// type: "LIGHT",
|
||||
// name:"light1P",
|
||||
|
||||
// typeLight: "POINT",
|
||||
// color: "0xffffff",
|
||||
// intensity: 100,
|
||||
// distance: 100,
|
||||
// decay: 100,
|
||||
// quaternion: [0, 0, 0, 1],
|
||||
// vector3: { x: 0, y: 0, z: 0 },
|
||||
// },
|
||||
// {
|
||||
// name:"light1D",
|
||||
// type: "LIGHT",
|
||||
// typeLight: "DIRECTIONAL",
|
||||
// color: "0xffffff",
|
||||
// intensity: 100,
|
||||
// quaternion: [0, 0, 0, 1],
|
||||
// vector3: { x: 0, y: 0, z: 0 },
|
||||
// },
|
||||
],
|
||||
});
|
||||
};
|
||||
getAllScenes = () => this._jsonRequest<SceneModel[]>(HttpMethod.GET, "/scenes");
|
||||
|
||||
newScene = (sceneViewModel: SceneViewModel) =>
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
import { Quaternion, Vector3 } from "three";
|
||||
import { SceneModelsTypes } from "./solid_body_model";
|
||||
import { Result } from "../../../core/helper/result";
|
||||
export enum CameraTypes {
|
||||
RGB = "RGB",
|
||||
}
|
||||
export class CameraModel {
|
||||
type = SceneModelsTypes.CAMERA;
|
||||
|
||||
constructor(
|
||||
public quaternion: Quaternion,
|
||||
public vector3: Vector3,
|
||||
public name: string,
|
||||
public cameraType: CameraTypes,
|
||||
public width: number,
|
||||
public updateRate: number,
|
||||
public fov: number,
|
||||
public near: number,
|
||||
public far: number,
|
||||
public height: number,
|
||||
public topic: string,
|
||||
public parent?: string,
|
||||
public fixed?: string
|
||||
) {}
|
||||
validate = (): Result<string, CameraModel> => {
|
||||
return Result.ok(this);
|
||||
};
|
||||
static empty = () =>
|
||||
new CameraModel(new Quaternion(0, 0, 0, 0), new Vector3(0, 0, 0), "", CameraTypes.RGB, 0, 0, 0, 0, 0, 0, "");
|
||||
}
|
|
@ -1,163 +0,0 @@
|
|||
import { CameraHelper, Object3D, PerspectiveCamera, Quaternion, Scene, Vector3 } from "three";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { UserData } from "../../../core/repository/core_three_repository";
|
||||
import {
|
||||
Asset,
|
||||
Instance,
|
||||
InstanceRgbCamera,
|
||||
InstanceType,
|
||||
SceneSimpleObject,
|
||||
} from "../../../core/model/robossembler_assets";
|
||||
import { Result } from "../../../core/helper/result";
|
||||
|
||||
export enum RobossemblerFiles {
|
||||
robossemblerAssets = "robossembler_assets.json",
|
||||
}
|
||||
|
||||
export enum SceneModelsType {
|
||||
ASSET,
|
||||
}
|
||||
|
||||
export abstract class BaseSceneItemModel {
|
||||
id: string;
|
||||
name: string;
|
||||
position: Vector3;
|
||||
quaternion: Quaternion;
|
||||
|
||||
constructor(name: string) {
|
||||
this.id = uuidv4();
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
abstract toInstance(): Instance;
|
||||
|
||||
abstract deleteToScene(scene: Scene): Object3D[];
|
||||
|
||||
toQuaternionArray() {
|
||||
return [this.quaternion.x, this.quaternion.y, this.quaternion.z, this.quaternion.w];
|
||||
}
|
||||
}
|
||||
|
||||
export class StaticAssetItemModel extends BaseSceneItemModel {
|
||||
name: string;
|
||||
type = SceneModelsType.ASSET;
|
||||
|
||||
constructor(name: string, position: Vector3, quaternion: Quaternion) {
|
||||
super(name);
|
||||
this.name = name;
|
||||
this.position = position;
|
||||
this.quaternion = quaternion;
|
||||
}
|
||||
|
||||
static fromSceneSimpleObjectAndAsset(sceneSimpleObject: SceneSimpleObject, asset: Asset) {
|
||||
const { x, y, z } = sceneSimpleObject.position;
|
||||
const quaternion = sceneSimpleObject.quaternion;
|
||||
return new StaticAssetItemModel(
|
||||
asset.name,
|
||||
new Vector3(x, y, z),
|
||||
new Quaternion(quaternion[0], quaternion[1], quaternion[2], quaternion[3])
|
||||
);
|
||||
}
|
||||
|
||||
toInstance(): Instance {
|
||||
const instance = new Instance();
|
||||
instance.instanceType = InstanceType.SCENE_SIMPLE_OBJECT;
|
||||
instance.position = this.position;
|
||||
instance.instanceAt = this.name;
|
||||
instance.quaternion = this.toQuaternionArray();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
deleteToScene(scene: Scene): Object3D[] {
|
||||
return scene.children.filter((el) => el.name !== this.name);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ICoreInstance {
|
||||
instanceAt: null | string;
|
||||
type: string;
|
||||
position: Vector3;
|
||||
quaternion: Quaternion;
|
||||
}
|
||||
|
||||
export interface ICameraInstance extends ICoreInstance {
|
||||
cameraLink: string;
|
||||
topicImage: string;
|
||||
topicCameraInfo: string;
|
||||
topicDepth: string | null;
|
||||
}
|
||||
|
||||
export class CameraViewModel extends BaseSceneItemModel {
|
||||
cameraLink: string;
|
||||
topicImage: string;
|
||||
topicCameraInfo: string;
|
||||
topicDepth: null | string;
|
||||
|
||||
constructor(cameraLink: string, topicImage: string, topicCameraInfo: string, topicDepth: string | null) {
|
||||
super(cameraLink);
|
||||
this.cameraLink = cameraLink;
|
||||
this.topicImage = topicImage;
|
||||
this.topicCameraInfo = topicCameraInfo;
|
||||
this.topicDepth = topicDepth;
|
||||
}
|
||||
|
||||
static fromInstanceRgbCamera(instanceRgbCamera: InstanceRgbCamera) {
|
||||
const { cameraLink, topicImage, topicCameraInfo, topicDepth, position, quaternion } = instanceRgbCamera;
|
||||
const instance = new CameraViewModel(cameraLink, topicImage, topicCameraInfo, topicDepth);
|
||||
const { x, y, z } = position;
|
||||
|
||||
instance.position = new Vector3(x, y, z);
|
||||
instance.quaternion = new Quaternion(quaternion[0], quaternion[1], quaternion[2], quaternion[3]);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
deleteToScene(scene: Scene): Object3D[] {
|
||||
return scene.children.filter((el) => {
|
||||
if (el.name === this.name) {
|
||||
return false;
|
||||
}
|
||||
if (el.name === this.cameraLink + "camera_helper") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
toInstance(): InstanceRgbCamera {
|
||||
return {
|
||||
instanceType: InstanceType.RGB_CAMERA,
|
||||
position: this.position,
|
||||
quaternion: this.toQuaternionArray(),
|
||||
instanceAt: null,
|
||||
cameraLink: this.cameraLink,
|
||||
topicCameraInfo: this.topicCameraInfo,
|
||||
topicDepth: this.topicDepth,
|
||||
topicImage: this.topicImage,
|
||||
};
|
||||
}
|
||||
|
||||
validate(): Result<string, CameraViewModel> {
|
||||
if (this.cameraLink.isEmpty()) {
|
||||
return Result.error("cameraLink is empty");
|
||||
}
|
||||
return Result.ok(this);
|
||||
}
|
||||
|
||||
mapPerspectiveCamera(htmlSceneWidth: number, htmlSceneHeight: number) {
|
||||
const perspectiveCamera = new PerspectiveCamera(48, htmlSceneWidth / htmlSceneHeight, 7.1, 28.5);
|
||||
perspectiveCamera.position.copy(this.position);
|
||||
perspectiveCamera.quaternion.copy(this.quaternion);
|
||||
perspectiveCamera.name = this.cameraLink;
|
||||
const cameraHelper = new CameraHelper(perspectiveCamera);
|
||||
perspectiveCamera.userData[UserData.selectedObject] = true;
|
||||
cameraHelper.userData[UserData.selectedObject] = true;
|
||||
cameraHelper.name = this.cameraLink + "camera_helper";
|
||||
return [cameraHelper, perspectiveCamera];
|
||||
}
|
||||
|
||||
static empty() {
|
||||
return new CameraViewModel("", "", "", "");
|
||||
}
|
||||
}
|
|
@ -1,18 +1,3 @@
|
|||
export class SceneMenu {
|
||||
x?: number;
|
||||
y?: number;
|
||||
isShow?: boolean;
|
||||
|
||||
constructor(x: number | undefined, y: number | undefined, isShow: boolean | undefined) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.isShow = isShow;
|
||||
}
|
||||
|
||||
static empty() {
|
||||
return new SceneMenu(undefined, undefined, false);
|
||||
}
|
||||
}
|
||||
|
||||
export interface SceneManagerView {
|
||||
name: string;
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
import { Quaternion, Vector3 } from "three";
|
||||
|
||||
export enum SceneModelsTypes {
|
||||
SOLID = "SOLID",
|
||||
ROBOT = "ROBOT",
|
||||
LIGHT = "LIGHT",
|
||||
CAMERA = 'CAMERA'
|
||||
}
|
||||
export enum SolidBodyTypes {
|
||||
ACTIVE = "ACTIVE",
|
||||
STATIC = "STATIC",
|
||||
}
|
||||
interface ISpawnTypes {
|
||||
type: "POINT";
|
||||
name: "123";
|
||||
}
|
||||
export class SolidBodyModel {
|
||||
type = SceneModelsTypes.SOLID;
|
||||
spawn?: ISpawnTypes;
|
||||
constructor(
|
||||
public quaternion: Quaternion,
|
||||
public vector3: Vector3,
|
||||
public name: string,
|
||||
public solidType: SolidBodyTypes | string,
|
||||
public mesh: string,
|
||||
public collisionMesh: string,
|
||||
public inertia?: number,
|
||||
public mass?: number
|
||||
) {}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import { FormState } from "../../../../core/store/base_store";
|
||||
import { CoreInput } from "../../../../core/ui/input/input";
|
||||
import { CoreText, CoreTextType } from "../../../../core/ui/text/text";
|
||||
|
||||
export const CoordsForm = ({ store, update }: { store: FormState<any, any>; update: Function }) => {
|
||||
return (
|
||||
<>
|
||||
<CoreText text="Координаты" type={CoreTextType.header} />
|
||||
<CoreInput
|
||||
value={String(store.viewModel.vector3.x)}
|
||||
label="Вектор X"
|
||||
onChange={(text) => {
|
||||
store.updateForm({ vector3: store.viewModel.vector3.setX(Number(text)) });
|
||||
update();
|
||||
}}
|
||||
/>
|
||||
<CoreInput
|
||||
value={String(store.viewModel.vector3.x)}
|
||||
label="Вектор Y"
|
||||
onChange={(text) => {
|
||||
store.updateForm({ vector3: store.viewModel.vector3.setY(Number(text)) });
|
||||
update();
|
||||
}}
|
||||
/>
|
||||
<CoreInput
|
||||
value={String(store.viewModel.vector3.x)}
|
||||
label="Вектор Z"
|
||||
onChange={(text) => {
|
||||
store.updateForm({ vector3: store.viewModel.vector3.setZ(Number(text)) });
|
||||
update();
|
||||
}}
|
||||
/>
|
||||
<CoreText text="Координаты" type={CoreTextType.header} />
|
||||
<CoreInput
|
||||
value={String(store.viewModel.vector3.x)}
|
||||
label="Вектор X"
|
||||
onChange={(text) => {
|
||||
store.updateForm({ vector3: store.viewModel.vector3.setX(Number(text)) });
|
||||
update();
|
||||
}}
|
||||
/>
|
||||
<CoreInput
|
||||
value={String(store.viewModel.vector3.x)}
|
||||
label="Вектор Y"
|
||||
onChange={(text) => {
|
||||
store.updateForm({ vector3: store.viewModel.vector3.setY(Number(text)) });
|
||||
update();
|
||||
}}
|
||||
/>
|
||||
<CoreInput
|
||||
value={String(store.viewModel.vector3.x)}
|
||||
label="Вектор Z"
|
||||
onChange={(text) => {
|
||||
store.updateForm({ vector3: store.viewModel.vector3.setZ(Number(text)) });
|
||||
update();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,43 +0,0 @@
|
|||
import * as React from "react";
|
||||
import { BaseSceneItemModel, CameraViewModel, StaticAssetItemModel } from "../../model/scene_assets";
|
||||
import { Button } from "antd";
|
||||
|
||||
export interface IStaticAssetModelViewProps {
|
||||
model: BaseSceneItemModel;
|
||||
onTap: Function;
|
||||
}
|
||||
|
||||
export function StaticAssetModelView(props: IStaticAssetModelViewProps) {
|
||||
if (props.model instanceof CameraViewModel) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "ActiveBorder",
|
||||
padding: "10px",
|
||||
color: "white",
|
||||
textAlignLast: "center",
|
||||
}}
|
||||
>
|
||||
{props.model.cameraLink}
|
||||
<Button onClick={() => props.onTap()}>delete</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.model instanceof StaticAssetItemModel) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "brown",
|
||||
padding: "10px",
|
||||
color: "white",
|
||||
textAlignLast: "center",
|
||||
}}
|
||||
>
|
||||
{props.model.name}
|
||||
<Button onClick={() => props.onTap()}>delete</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
}
|
|
@ -1,25 +1,19 @@
|
|||
import { NavigateFunction } from "react-router-dom";
|
||||
import { CoreError, FormState } from "../../../../../core/store/base_store";
|
||||
import { CoreButton } from "../../../../../core/ui/button/button";
|
||||
import { CameraViewModel } from "../../../model/scene_assets";
|
||||
import { IDefaultSceneManagerFormProps } from "../scene_manager_forms";
|
||||
import React from "react";
|
||||
import { message } from "antd";
|
||||
import { CoreButton } from "../../../../../core/ui/button/button";
|
||||
import { IDefaultSceneManagerFormProps, isPreviewMode } from "../scene_manager_forms";
|
||||
import { CoreInput } from "../../../../../core/ui/input/input";
|
||||
import { CoreText, CoreTextType } from "../../../../../core/ui/text/text";
|
||||
import { message } from "antd";
|
||||
import { CameraModel, CameraTypes } from "../../../model/camera_view_model";
|
||||
import { CoreSelect } from "../../../../../core/ui/select/select";
|
||||
import { CameraFormStore } from "./camera_store";
|
||||
import { Loader } from "../../../../../core/ui/loader/loader";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { CameraTypes } from "../../../../../core/model/camera_model";
|
||||
|
||||
class CameraFormStore extends FormState<CameraModel, CoreError> {
|
||||
errorHandingStrategy = (error: CoreError) => {};
|
||||
init = async (navigate?: NavigateFunction | undefined) => {};
|
||||
viewModel: CameraModel = CameraModel.empty();
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
export const CameraForm = (props: IDefaultSceneManagerFormProps) => {
|
||||
const [store] = React.useState(() => new CameraFormStore());
|
||||
|
||||
|
||||
export const CameraForm = observer((props: IDefaultSceneManagerFormProps) => {
|
||||
const [store] = React.useState(() => new CameraFormStore(props.store));
|
||||
|
||||
React.useEffect(() => {
|
||||
store.init();
|
||||
|
@ -27,7 +21,6 @@ export const CameraForm = (props: IDefaultSceneManagerFormProps) => {
|
|||
return (
|
||||
<div style={{ margin: 10, padding: 10, overflowY: "auto", height: "100%" }}>
|
||||
<CoreText text="Камера" type={CoreTextType.header} />
|
||||
|
||||
<CoreInput value={store.viewModel.topic} label={"Топик"} onChange={(text) => store.updateForm({ topic: text })} />
|
||||
<CoreInput value={store.viewModel.name} label={"Имя"} onChange={(text) => store.updateForm({ name: text })} />
|
||||
<CoreInput
|
||||
|
@ -43,33 +36,50 @@ export const CameraForm = (props: IDefaultSceneManagerFormProps) => {
|
|||
onChange={(text) => store.updateForm({ height: Number(text) })}
|
||||
/>
|
||||
<CoreInput
|
||||
value={String(store.viewModel.width)}
|
||||
|
||||
value={String(store.viewModel.width)}
|
||||
validation={(text) => Number().isValid(text)}
|
||||
label={"Width"}
|
||||
onChange={(text) => store.updateForm({ width: Number(text) })}
|
||||
/>
|
||||
<CoreInput
|
||||
value={String(store.viewModel.fov)}
|
||||
label={"Fov"}
|
||||
validation={(text) => Number().isValid(text)}
|
||||
onChange={(text) => (store.updateForm({ fov: Number(text) }), store.updateCameraScene())}
|
||||
/>
|
||||
<CoreInput
|
||||
value={String(store.viewModel.far)}
|
||||
label={"Far"}
|
||||
validation={(text) => Number().isValid(text)}
|
||||
onChange={(text) => (store.updateForm({ far: Number(text) }), store.updateCameraScene())}
|
||||
/>
|
||||
<CoreInput
|
||||
value={String(store.viewModel.near)}
|
||||
label={"Far"}
|
||||
validation={(text) => Number().isValid(text)}
|
||||
onChange={(text) => (store.updateForm({ near: Number(text) }), store.updateCameraScene())}
|
||||
/>
|
||||
|
||||
<CoreSelect
|
||||
items={Object.entries(CameraTypes).map(([_, v]) => v)}
|
||||
value={store.viewModel.cameraType}
|
||||
label={"Типы камер"}
|
||||
label={"Тип камеры"}
|
||||
onChange={(text) => store.updateForm({ cameraType: text as CameraTypes })}
|
||||
/>
|
||||
<div style={{ height: 10 }} />
|
||||
<CoreButton
|
||||
text="Создать"
|
||||
style={{ width: 96 }}
|
||||
onClick={() =>
|
||||
onClick={() => {
|
||||
store.viewModel.validate().fold(
|
||||
(model) => {
|
||||
// props.store.addNewCamera(model);
|
||||
props.store.addNewCamera(model);
|
||||
},
|
||||
(error) => message.error(error)
|
||||
)
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div style={{ height: 50 }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// onClick={props.store.addNewCamera()}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import makeAutoObservable from "mobx-store-inheritance";
|
||||
import { NavigateFunction } from "react-router-dom";
|
||||
import { FormState, CoreError } from "../../../../../core/store/base_store";
|
||||
import { SceneMangerStore } from "../../scene_manager_store";
|
||||
import { isPreviewMode } from "../scene_manager_forms";
|
||||
import { CameraModel } from "../../../../../core/model/camera_model";
|
||||
|
||||
export class CameraFormStore extends FormState<CameraModel, CoreError> {
|
||||
viewModel: CameraModel = CameraModel.empty();
|
||||
sceneMangerStore: SceneMangerStore;
|
||||
type?: string;
|
||||
constructor(sceneMangerStore: SceneMangerStore) {
|
||||
super();
|
||||
makeAutoObservable(this);
|
||||
this.sceneMangerStore = sceneMangerStore;
|
||||
}
|
||||
|
||||
errorHandingStrategy = (error: CoreError) => {};
|
||||
updateCameraScene = () =>
|
||||
this.type?.isNotEmptyR().map(() => this.viewModel.toWebGl(this.sceneMangerStore.coreThreeRepository!));
|
||||
|
||||
init = async (navigate?: NavigateFunction | undefined) => {
|
||||
isPreviewMode(this.sceneMangerStore.activeFormDependency).map(() =>
|
||||
this.sceneMangerStore.scene
|
||||
.rFind<CameraModel>((el) => el.name.isEqual(this.sceneMangerStore.selectedItemName ?? ""))
|
||||
.fold(
|
||||
(cameraModel) => {
|
||||
this.loadDependency(cameraModel);
|
||||
this.type = "preview";
|
||||
},
|
||||
() => console.log(`Unknown FormType`)
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import { NavigateFunction } from "react-router-dom";
|
||||
import { FormState, CoreError } from "../../../../../core/store/base_store";
|
||||
import { LightViewModel } from "./light_view_model";
|
||||
import { LightModel } from "../../../../../core/model/light_model";
|
||||
|
||||
export class LightStore extends FormState<LightViewModel, CoreError> {
|
||||
viewModel: LightViewModel = LightViewModel.empty();
|
||||
export class LightStore extends FormState<LightModel, CoreError> {
|
||||
viewModel: LightModel = LightModel.empty();
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import { Result } from "../../../../../core/helper/result";
|
||||
|
||||
export class LightViewModel {
|
||||
constructor() {}
|
||||
isValid = (): Result<void, LightViewModel> => {
|
||||
return Result.ok(this);
|
||||
};
|
||||
static empty = () => new LightViewModel();
|
||||
}
|
|
@ -9,7 +9,7 @@ import { match } from "ts-pattern";
|
|||
import { SpawnPositionTypesForm } from "../../components/spawn_position_types";
|
||||
|
||||
export const PointForm = observer((props: IDefaultSceneManagerFormProps) => {
|
||||
const [store] = React.useState(() => new PointStore());
|
||||
const [store] = React.useState(() => new PointStore(props.store));
|
||||
React.useEffect(() => {
|
||||
store.init();
|
||||
}, [store]);
|
||||
|
@ -17,6 +17,28 @@ export const PointForm = observer((props: IDefaultSceneManagerFormProps) => {
|
|||
return (
|
||||
<div>
|
||||
{match(store.storeType)
|
||||
.with(PointStoreType.previewPoint, () => (
|
||||
<>
|
||||
<CoreText text="Точка" type={CoreTextType.header} />
|
||||
<CoreText text={store.viewModel.name} type={CoreTextType.medium} />
|
||||
<CoreText text="Позиция" type={CoreTextType.header} />
|
||||
<CoreInput
|
||||
label="X"
|
||||
value={String(store.viewModel.vector3.x)}
|
||||
onChange={(text) => (store.updateForm({ vector3: store.viewModel.vector3.setX(Number(text)) }), store.updateWebGl())}
|
||||
/>
|
||||
<CoreInput
|
||||
label="Y"
|
||||
value={String(store.viewModel.vector3.y)}
|
||||
onChange={(text) => (store.updateForm({ vector3: store.viewModel.vector3.setY(Number(text)) }),store.updateWebGl())}
|
||||
/>
|
||||
<CoreInput
|
||||
label="Z"
|
||||
value={String(store.viewModel.vector3.z)}
|
||||
onChange={(text) => (store.updateForm({ vector3: store.viewModel.vector3.setZ(Number(text)) }), store.updateWebGl())}
|
||||
/>
|
||||
</>
|
||||
))
|
||||
.with(PointStoreType.initNewPoint, () => (
|
||||
<>
|
||||
<CoreText text="Точка" type={CoreTextType.header} />
|
||||
|
|
|
@ -1,26 +1,54 @@
|
|||
import makeAutoObservable from "mobx-store-inheritance";
|
||||
import { NavigateFunction } from "react-router-dom";
|
||||
import { PointViewModel } from "./point_view_model";
|
||||
import { PointModel } from "../../../../../core/model/point_model";
|
||||
import { PointHttpRepository } from "./point_http_repository";
|
||||
import { FormState, CoreError } from "../../../../../core/store/base_store";
|
||||
import { SpawnPositionTypes } from "../../../../../core/model/spawn_position_types";
|
||||
import { isPreviewMode } from "../scene_manager_forms";
|
||||
import { SceneMangerStore } from "../../scene_manager_store";
|
||||
|
||||
export enum PointStoreType {
|
||||
makeSceneSolidAndEditPosition = "makeSceneSolidAndEditPosition",
|
||||
initNewPoint = "initNewPoint",
|
||||
previewPoint = "previewPoint",
|
||||
}
|
||||
export class PointStore extends FormState<PointViewModel, CoreError> {
|
||||
onClickNext = (pointStoreType: PointStoreType) => (this.storeType = pointStoreType);
|
||||
viewModel: PointViewModel = PointViewModel.empty();
|
||||
|
||||
export class PointStore extends FormState<PointModel, CoreError> {
|
||||
viewModel: PointModel = PointModel.empty();
|
||||
cameraDeviceHttpRepository: PointHttpRepository = new PointHttpRepository();
|
||||
storeType: PointStoreType = PointStoreType.initNewPoint;
|
||||
spawnPositionTypes: SpawnPositionTypes;
|
||||
constructor() {
|
||||
sceneMangerStore: SceneMangerStore;
|
||||
constructor(sceneMangerStore: SceneMangerStore) {
|
||||
super();
|
||||
makeAutoObservable(this);
|
||||
this.sceneMangerStore = sceneMangerStore;
|
||||
}
|
||||
selectSpawnType = (type: SpawnPositionTypes) => {
|
||||
this.spawnPositionTypes = type;
|
||||
};
|
||||
updateWebGl = () =>
|
||||
this.storeType
|
||||
.isEqualR(PointStoreType.previewPoint)
|
||||
.map(() => this.sceneMangerStore.coreThreeRepository!.updateInstance(this.viewModel));
|
||||
onClickNext = (pointStoreType: PointStoreType) => (this.storeType = pointStoreType);
|
||||
|
||||
errorHandingStrategy = (error: CoreError) => {};
|
||||
init = async (navigate?: NavigateFunction | undefined) => {};
|
||||
init = async (navigate?: NavigateFunction | undefined) =>
|
||||
isPreviewMode(this.sceneMangerStore.activeFormDependency).map(() =>
|
||||
this.sceneMangerStore.scene
|
||||
.rFind<PointModel>((el) => el.name.isEqual(this.sceneMangerStore.selectedItemName ?? ""))
|
||||
.fold(
|
||||
(solidBodyModel) => {
|
||||
this.loadDependency(solidBodyModel);
|
||||
this.storeType = PointStoreType.previewPoint;
|
||||
},
|
||||
() =>
|
||||
console.log(
|
||||
`Unknown FormType ${this.sceneMangerStore.selectedItemName} : ${JSON.stringify(
|
||||
this.sceneMangerStore.scene
|
||||
)}`
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
import { Quaternion, Vector3 } from "three";
|
||||
import { Result } from "../../../../../core/helper/result";
|
||||
|
||||
export class PointViewModel {
|
||||
type = "POINT";
|
||||
name: string;
|
||||
vector3: Vector3;
|
||||
quaternion: Quaternion;
|
||||
constructor() {}
|
||||
isValid(): Result<string, PointViewModel> {
|
||||
return Result.ok();
|
||||
}
|
||||
static empty() {
|
||||
return new PointViewModel();
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
import { IDefaultSceneManagerFormProps } from "../scene_manager_forms";
|
||||
|
||||
|
||||
export const RobotForm = (props: IDefaultSceneManagerFormProps) => {
|
||||
return <div>ROBOT</div>;
|
||||
};
|
|
@ -0,0 +1,97 @@
|
|||
import React from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { RobotFormStore, RobotStoreType } from "./robot_form_store";
|
||||
import { IDefaultSceneManagerFormProps } from "../scene_manager_forms";
|
||||
import { CoreText, CoreTextType } from "../../../../../core/ui/text/text";
|
||||
import { CoreInputNumber } from "../../../../../core/ui/inputNumber/input_number";
|
||||
import { CoreInput, CoreInputType } from "../../../../../core/ui/input/input";
|
||||
import { match } from "ts-pattern";
|
||||
import { CoreSelect } from "../../../../../core/ui/select/select";
|
||||
import { ToolTypes } from "../../../../../core/model/robot_model";
|
||||
import { CoreButton } from "../../../../../core/ui/button/button";
|
||||
import { SpawnPositionTypesForm } from "../../components/spawn_position_types";
|
||||
import { CoordsForm } from "../../components/coords_form";
|
||||
|
||||
export const RobotForm = observer((props: IDefaultSceneManagerFormProps) => {
|
||||
const [store] = React.useState(() => new RobotFormStore(props.store));
|
||||
React.useEffect(() => {
|
||||
store.init();
|
||||
}, []);
|
||||
return (
|
||||
<div style={{ overflowY: "scroll", height: "100%" }}>
|
||||
{match(store.storeType)
|
||||
.with(RobotStoreType.awaitMouseClick, () => (
|
||||
<>
|
||||
<CoreText type={CoreTextType.medium} text="Ожидание клика мыши" />
|
||||
</>
|
||||
))
|
||||
.with(RobotStoreType.selectSpawnPosition, () => (
|
||||
<>
|
||||
<SpawnPositionTypesForm onClick={store.selectSpawnType} />
|
||||
</>
|
||||
))
|
||||
.with(RobotStoreType.initNewRobot, () => (
|
||||
<>
|
||||
<CoreText text="Новый робот" type={CoreTextType.header} />
|
||||
<CoreInput
|
||||
type={CoreInputType.small}
|
||||
value={store.viewModel.nDof.toString()}
|
||||
label="Имя"
|
||||
onChange={(text) => store.updateForm({ name: text })}
|
||||
/>
|
||||
<CoreInput
|
||||
type={CoreInputType.small}
|
||||
value={store.viewModel.nDof.toString()}
|
||||
label="Ndof"
|
||||
onChange={(text) => store.updateForm({ nDof: Number(text) })}
|
||||
/>
|
||||
<CoreSelect
|
||||
items={Object.keys(ToolTypes)}
|
||||
value={store.viewModel.toolType}
|
||||
label={"ToolType"}
|
||||
onChange={(text) => store.updateForm({ toolType: text })}
|
||||
/>
|
||||
<CoreButton onClick={() => store.createNewRobot()} text="Создать" />
|
||||
</>
|
||||
))
|
||||
.with(RobotStoreType.previewRobot, () => (
|
||||
<>
|
||||
<CoordsForm store={store} update={() => store.updateScene()} />
|
||||
|
||||
<div style={{ height: 40 }} />
|
||||
<CoreText text="Управление соединениями" type={CoreTextType.header} />
|
||||
{store.viewModel.jointPosition.map((el, i) => (
|
||||
<div>
|
||||
<CoreInputNumber
|
||||
key={i}
|
||||
step={0.1}
|
||||
max={Number(el.limit.upper)}
|
||||
min={Number(el.limit.lower)}
|
||||
value={0}
|
||||
onChange={(value) => (
|
||||
store.updateForm({
|
||||
jointPosition: store.viewModel.jointPosition.map((element, index) =>
|
||||
index.isEqualR(i).fold(
|
||||
() => {
|
||||
element.angel = value;
|
||||
return element;
|
||||
},
|
||||
() => element
|
||||
)
|
||||
),
|
||||
}),
|
||||
store.updateScene()
|
||||
)}
|
||||
label={el.name}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ height: 40 }} />
|
||||
</>
|
||||
))
|
||||
.otherwise(() => (
|
||||
<></>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
import { RobotModel } from "../../../../../core/model/robot_model";
|
||||
import { HttpMethod, HttpRepository } from "../../../../../core/repository/http_repository";
|
||||
export interface RobotURL {
|
||||
robotUrl: string;
|
||||
}
|
||||
|
||||
export class RobotFormHttpRepository extends HttpRepository {
|
||||
createNewRobot = (robotModel: RobotModel) =>
|
||||
this._jsonRequest<RobotURL>(HttpMethod.POST, "/scenes/create/robot", robotModel);
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
import makeAutoObservable from "mobx-store-inheritance";
|
||||
import { NavigateFunction } from "react-router-dom";
|
||||
import { RobotFormHttpRepository } from "./robot_form_http_repository";
|
||||
import { FormState, CoreError } from "../../../../../core/store/base_store";
|
||||
import { RobotModel } from "../../../../../core/model/robot_model";
|
||||
import { SceneMangerStore } from "../../scene_manager_store";
|
||||
import { isPreviewMode } from "../scene_manager_forms";
|
||||
import { message } from "antd";
|
||||
import { SceneModelsTypes } from "../../../../../core/model/scene_models_type";
|
||||
import { Vector3 } from "three";
|
||||
|
||||
export enum RobotStoreType {
|
||||
previewRobot = "previewRobot",
|
||||
selectSpawnPosition = "selectSpawnPosition",
|
||||
initNewRobot = "createNewRobot",
|
||||
awaitMouseClick = "awaitMouseClick",
|
||||
}
|
||||
export class RobotFormStore extends FormState<RobotModel, CoreError> {
|
||||
sceneMangerStore: SceneMangerStore;
|
||||
storeType: RobotStoreType = RobotStoreType.initNewRobot;
|
||||
viewModel: RobotModel = RobotModel.empty();
|
||||
robotFormHttpRepository: RobotFormHttpRepository = new RobotFormHttpRepository();
|
||||
spawnType: string;
|
||||
listener: Function;
|
||||
clickLister = (event: MouseEvent) =>
|
||||
this.storeType.isEqualR(RobotStoreType.awaitMouseClick).map(() =>
|
||||
this.sceneMangerStore!.clickScene(event, this.sceneMangerStore!.canvasOffsetX).map((vector3) => {
|
||||
this.viewModel.vector3 = vector3;
|
||||
this.viewModel.toWebGl(this.sceneMangerStore.coreThreeRepository!);
|
||||
this.sceneMangerStore.activeFormType = undefined;
|
||||
this.sceneMangerStore.sceneItems.push(this.viewModel.toSceneItems(this.sceneMangerStore))
|
||||
this.sceneMangerStore.scene.push(this.viewModel)
|
||||
window.removeEventListener("click", this.clickLister);
|
||||
})
|
||||
);
|
||||
|
||||
selectSpawnType = (type: string) => {
|
||||
this.spawnType = type;
|
||||
setTimeout(() => window.addEventListener("click", this.clickLister), 1000);
|
||||
this.storeType = RobotStoreType.awaitMouseClick;
|
||||
};
|
||||
dispose = (): void => {
|
||||
window.removeEventListener("click", this.clickLister);
|
||||
};
|
||||
constructor(sceneMangerStore: SceneMangerStore) {
|
||||
super();
|
||||
makeAutoObservable(this);
|
||||
this.sceneMangerStore = sceneMangerStore;
|
||||
}
|
||||
updateScene = () =>
|
||||
this.storeType
|
||||
.isEqualR(RobotStoreType.previewRobot)
|
||||
.map(() => this.viewModel.update(this.sceneMangerStore.coreThreeRepository!));
|
||||
createNewRobot = () =>
|
||||
this.viewModel.isValid().fold(
|
||||
async (s) =>
|
||||
(await this.robotFormHttpRepository.createNewRobot(s)).fold(
|
||||
(s) => {
|
||||
this.viewModel.httpUrl = s.robotUrl;
|
||||
this.storeType = RobotStoreType.selectSpawnPosition;
|
||||
},
|
||||
(e) => {
|
||||
message.error(e.error);
|
||||
}
|
||||
),
|
||||
async (e) => message.error(e)
|
||||
);
|
||||
errorHandingStrategy = (error: CoreError) => {};
|
||||
init = async (navigate?: NavigateFunction | undefined) =>
|
||||
isPreviewMode(this.sceneMangerStore.activeFormDependency).map(() =>
|
||||
this.sceneMangerStore.scene
|
||||
.rFind<RobotModel>((el) => el.name.isEqual(this.sceneMangerStore.selectedItemName ?? ""))
|
||||
.fold(
|
||||
(solidBodyModel) => {
|
||||
this.loadDependency(solidBodyModel);
|
||||
this.storeType = RobotStoreType.previewRobot;
|
||||
},
|
||||
() =>
|
||||
console.log(
|
||||
`Unknown FormType ${this.sceneMangerStore.selectedItemName} : ${JSON.stringify(
|
||||
this.sceneMangerStore.scene
|
||||
)}`
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
import { Result } from "../../../../core/helper/result";
|
||||
import { SceneMangerStore } from "../scene_manager_store";
|
||||
import { CameraForm } from "./camera/camera_form";
|
||||
import { LightForm } from "./light/light_form";
|
||||
import { PointForm } from "./point/point_form";
|
||||
import { RobotForm } from "./robot/robot_form";
|
||||
import { RobotForm } from "./robot_form/robot_form";
|
||||
import { SolidBodyForm } from "./solid_body/solid_body_form";
|
||||
import { Trajectory } from "./trajectory/trajectory_form";
|
||||
import { ZoneForm } from "./zone/zone_form";
|
||||
|
@ -16,6 +17,7 @@ export enum SceneManagerForms {
|
|||
point = "point",
|
||||
trajectory = "trajectory",
|
||||
zone = "zone",
|
||||
capturePoints = "capturePoints",
|
||||
}
|
||||
interface IForms {
|
||||
name: string;
|
||||
|
@ -25,7 +27,10 @@ export interface IDefaultSceneManagerFormProps {
|
|||
dependency: Object;
|
||||
store: SceneMangerStore;
|
||||
}
|
||||
|
||||
export const isPreviewMode = (dependency: Object): Result<void, void> => {
|
||||
if (Object.hasOwn(dependency, "type")) return Result.ok();
|
||||
return Result.error(undefined);
|
||||
};
|
||||
export const sceneManagerForms = (props: Object, store: SceneMangerStore): IForms[] => [
|
||||
{ name: SceneManagerForms.camera, component: <CameraForm dependency={props} store={store} /> },
|
||||
{ name: SceneManagerForms.robot, component: <RobotForm dependency={props} store={store} /> },
|
||||
|
@ -34,4 +39,5 @@ export const sceneManagerForms = (props: Object, store: SceneMangerStore): IForm
|
|||
{ name: SceneManagerForms.light, component: <LightForm dependency={props} store={store} /> },
|
||||
{ name: SceneManagerForms.zone, component: <ZoneForm dependency={props} store={store} /> },
|
||||
{ name: SceneManagerForms.trajectory, component: <Trajectory dependency={props} store={store} /> },
|
||||
{ name: SceneManagerForms.capturePoints, component: <CameraForm dependency={props} store={store} /> },
|
||||
];
|
||||
|
|
|
@ -6,57 +6,79 @@ import { observer } from "mobx-react-lite";
|
|||
import { CoreButton } from "../../../../../core/ui/button/button";
|
||||
import { match } from "ts-pattern";
|
||||
import { SpawnPositionTypesForm } from "../../components/spawn_position_types";
|
||||
import { CoreInput } from "../../../../../core/ui/input/input";
|
||||
|
||||
export const SolidBodyForm = observer((props: IDefaultSceneManagerFormProps) => {
|
||||
const [store] = React.useState(() => new SolidBodyStore(props.store));
|
||||
React.useEffect(() => {
|
||||
store.init();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
|
||||
{Object.hasOwn(props.dependency, "type") && Object.hasOwn(props.dependency, "name") ? (
|
||||
<>
|
||||
x:{props.store.scene.find((el) => el.name.isEqual(props.store.selectedItemName ?? ""))?.vector3.x}
|
||||
y:{props.store.scene.find((el) => el.name.isEqual(props.store.selectedItemName ?? ""))?.vector3.y}
|
||||
y:{props.store.scene.find((el) => el.name.isEqual(props.store.selectedItemName ?? ""))?.vector3.z}
|
||||
solidType:{props.store.scene.find((el) => el.name.isEqual(props.store.selectedItemName ?? ""))?.solidType}
|
||||
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{match(store.solidBodyStoreType)
|
||||
.with(SolidBodyStoreType.selectBody, () => (
|
||||
<>
|
||||
<CoreText text={"Твердое тело"} type={CoreTextType.header} />
|
||||
|
||||
{store.parts.map((el, i) => (
|
||||
<div key={i} style={{ padding: 10, margin: 10 }}>
|
||||
<CoreText text={el.name} type={CoreTextType.medium} />
|
||||
<img src={el.image} />
|
||||
<CoreButton text="Выбрать" onClick={() => store.clickSelectBody(el)} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
))
|
||||
.with(SolidBodyStoreType.selectSpawnPositionType, () => (
|
||||
<>
|
||||
<SpawnPositionTypesForm onClick={store.selectSpawnType} />
|
||||
</>
|
||||
))
|
||||
.with(SolidBodyStoreType.spawn2DVector, () => (
|
||||
<>
|
||||
{props.store.mousePosition ? (
|
||||
""
|
||||
) : (
|
||||
<CoreText text={"Выберите точку размещения"} type={CoreTextType.header} />
|
||||
<>
|
||||
{match(store.solidBodyStoreType)
|
||||
.with(SolidBodyStoreType.previewSolid, () => (
|
||||
<>
|
||||
<CoreText text={"Твердое тело"} type={CoreTextType.header} />
|
||||
<CoreInput
|
||||
value={String(store.viewModel.vector3.x)}
|
||||
label="Вектор X"
|
||||
onChange={(text) => (
|
||||
store.updateForm({ vector3: store.viewModel.vector3.setX(Number(text)) }),
|
||||
store.updateBodySimulation()
|
||||
)}
|
||||
</>
|
||||
))
|
||||
.otherwise(() => (
|
||||
<></>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<CoreInput
|
||||
value={String(store.viewModel.vector3.x)}
|
||||
label="Вектор Y"
|
||||
onChange={(text) => (
|
||||
store.updateForm({ vector3: store.viewModel.vector3.setY(Number(text)) }),
|
||||
store.updateBodySimulation()
|
||||
)}
|
||||
/>
|
||||
<CoreInput
|
||||
value={String(store.viewModel.vector3.x)}
|
||||
label="Вектор Z"
|
||||
onChange={(text) => (
|
||||
store.updateForm({ vector3: store.viewModel.vector3.setZ(Number(text)) }),
|
||||
store.updateBodySimulation()
|
||||
)}
|
||||
/>
|
||||
<CoreText text={"Тип твердого тела: " + store.viewModel.solidType} type={CoreTextType.header} />
|
||||
</>
|
||||
))
|
||||
.with(SolidBodyStoreType.selectBody, () => (
|
||||
<>
|
||||
<CoreText text={"Твердое тело"} type={CoreTextType.header} />
|
||||
|
||||
{store.parts.map((el, i) => (
|
||||
<div key={i} style={{ padding: 10, margin: 10 }}>
|
||||
<CoreText text={el.name} type={CoreTextType.medium} />
|
||||
<img src={el.image} />
|
||||
<CoreButton text="Выбрать" onClick={() => store.clickSelectBody(el)} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
))
|
||||
.with(SolidBodyStoreType.selectSpawnPositionType, () => (
|
||||
<>
|
||||
<SpawnPositionTypesForm onClick={store.selectSpawnType} />
|
||||
</>
|
||||
))
|
||||
.with(SolidBodyStoreType.spawn2DVector, () => (
|
||||
<>
|
||||
{props.store.mousePosition ? (
|
||||
""
|
||||
) : (
|
||||
<CoreText text={"Выберите точку размещения"} type={CoreTextType.header} />
|
||||
)}
|
||||
</>
|
||||
))
|
||||
.otherwise(() => (
|
||||
<></>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,34 +1,53 @@
|
|||
import makeAutoObservable from "mobx-store-inheritance";
|
||||
import { SolidBodyViewModel } from "./solid_body_view_model";
|
||||
import { FormState, CoreError } from "../../../../../core/store/base_store";
|
||||
import { CoreHttpRepository } from "../../../../../core/repository/http_repository";
|
||||
import { Parts } from "../../../../details/details_http_repository";
|
||||
import { Vector2 } from "three";
|
||||
import { SceneMangerStore } from "../../scene_manager_store";
|
||||
import { SpawnPositionTypes } from "../../../../../core/model/spawn_position_types";
|
||||
import { isPreviewMode } from "../scene_manager_forms";
|
||||
import { SolidModel } from "../../../../../core/model/solid_model";
|
||||
import { SceneModelsTypes } from "../../../../../core/model/scene_models_type";
|
||||
export enum SolidBodyStoreType {
|
||||
selectBody = "selectBody",
|
||||
selectSpawnPositionType = "selectSpawnPositionType",
|
||||
spawn2DVector = "spawn2DVector",
|
||||
previewSolid = "previewSolid",
|
||||
}
|
||||
|
||||
export class SolidBodyStore extends FormState<SolidBodyViewModel, CoreError> {
|
||||
viewModel: SolidBodyViewModel = SolidBodyViewModel.empty();
|
||||
export class SolidBodyStore extends FormState<SolidModel, CoreError> {
|
||||
viewModel: SolidModel = SolidModel.empty();
|
||||
parts: Parts[] = [];
|
||||
spawnPositionTypes = Object.keys(SpawnPositionTypes);
|
||||
coreHttpRepository: CoreHttpRepository = new CoreHttpRepository();
|
||||
solidBodyStoreType: SolidBodyStoreType = SolidBodyStoreType.selectBody;
|
||||
selectBody: Parts;
|
||||
spawnType: string;
|
||||
sceneManagerStore: SceneMangerStore;
|
||||
sceneMangerStore: SceneMangerStore;
|
||||
vector2d?: Vector2;
|
||||
|
||||
constructor(sceneManagerStore: SceneMangerStore) {
|
||||
constructor(sceneMangerStore: SceneMangerStore) {
|
||||
super();
|
||||
this.sceneManagerStore = sceneManagerStore;
|
||||
this.sceneMangerStore = sceneMangerStore;
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
init = async () => {
|
||||
isPreviewMode(this.sceneMangerStore.activeFormDependency).map(() =>
|
||||
this.sceneMangerStore.scene
|
||||
.rFind<SolidModel>((el) => el.name.isEqual(this.sceneMangerStore.selectedItemName ?? ""))
|
||||
.fold(
|
||||
(solidBodyModel) => {
|
||||
this.loadDependency(solidBodyModel);
|
||||
this.solidBodyStoreType = SolidBodyStoreType.previewSolid;
|
||||
},
|
||||
() =>
|
||||
console.log(
|
||||
`Unknown FormType ${this.sceneMangerStore.selectedItemName} : ${JSON.stringify(
|
||||
this.sceneMangerStore.scene
|
||||
)}`
|
||||
)
|
||||
)
|
||||
);
|
||||
this.mapOk("parts", this.coreHttpRepository.getAssetsActiveProject());
|
||||
};
|
||||
errorHandingStrategy = (error: CoreError) => {};
|
||||
|
@ -37,16 +56,20 @@ export class SolidBodyStore extends FormState<SolidBodyViewModel, CoreError> {
|
|||
selectSpawnType = (type: string) => {
|
||||
this.spawnType = type;
|
||||
this.solidBodyStoreType = SolidBodyStoreType.spawn2DVector;
|
||||
this.sceneManagerStore.mousePositionAwait = true;
|
||||
this.sceneManagerStore.solidSpawnHelper = {
|
||||
url: this.selectBody.objUrl,
|
||||
this.sceneMangerStore.mousePositionAwait = true;
|
||||
this.sceneMangerStore.spawnHelper = {
|
||||
url: this.selectBody.daeUrl,
|
||||
name: this.selectBody.name,
|
||||
spawn: SceneModelsTypes.SOLID,
|
||||
isFinished: false,
|
||||
solidType: this.selectBody.solidType,
|
||||
type: this.spawnType,
|
||||
};
|
||||
this.sceneManagerStore.activeFormType = undefined;
|
||||
this.sceneMangerStore.activeFormType = undefined;
|
||||
};
|
||||
updateBodySimulation = () =>
|
||||
this.solidBodyStoreType
|
||||
.isEqualR(SolidBodyStoreType.previewSolid)
|
||||
.map(() => this.viewModel.update(this.sceneMangerStore.coreThreeRepository!));
|
||||
clickSelectBody = (el: Parts) => {
|
||||
this.selectBody = el;
|
||||
this.solidBodyStoreType = SolidBodyStoreType.selectSpawnPositionType;
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
export class SolidBodyViewModel {
|
||||
name: string;
|
||||
|
||||
static empty() {
|
||||
return new SolidBodyViewModel();
|
||||
}
|
||||
}
|
|
@ -2,14 +2,34 @@ import React from "react";
|
|||
import { observer } from "mobx-react-lite";
|
||||
import { ZoneStore } from "./zone_store";
|
||||
import { IDefaultSceneManagerFormProps } from "../scene_manager_forms";
|
||||
|
||||
|
||||
import { CoreText, CoreTextType } from "../../../../../core/ui/text/text";
|
||||
import { CoreInput } from "../../../../../core/ui/input/input";
|
||||
|
||||
export const ZoneForm = observer((props: IDefaultSceneManagerFormProps) => {
|
||||
const [store] = React.useState(() => new ZoneStore());
|
||||
const [store] = React.useState(() => new ZoneStore(props.store));
|
||||
React.useEffect(() => {
|
||||
store.init();
|
||||
}, [store]);
|
||||
|
||||
return <>zone</>;
|
||||
return (
|
||||
<>
|
||||
<CoreText text={"Зона"} type={CoreTextType.header} />
|
||||
<CoreText text={store.viewModel.name} type={CoreTextType.medium} />
|
||||
<CoreInput
|
||||
label="Длина"
|
||||
value={String(store.viewModel.length)}
|
||||
onChange={(text) => (store.updateForm({ length: Number(text) }), store.updateWebGl())}
|
||||
/>
|
||||
<CoreInput
|
||||
label="Высота"
|
||||
value={String(store.viewModel.height)}
|
||||
onChange={(text) => (store.updateForm({ height: Number(text) }), store.updateWebGl())}
|
||||
/>
|
||||
<CoreInput
|
||||
label="Ширина"
|
||||
value={String(store.viewModel.width)}
|
||||
onChange={(text) => (store.updateForm({ width: Number(text) }), store.updateWebGl())}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,19 +1,42 @@
|
|||
import makeAutoObservable from "mobx-store-inheritance";
|
||||
import { NavigateFunction } from "react-router-dom";
|
||||
import { ZoneViewModel } from "./zone_view_model";
|
||||
import { ZoneHttpRepository } from "./zone_http_repository";
|
||||
import { ZoneModel } from "../../../../../core/model/zone_model";
|
||||
import { ZoneHttpRepository } from "./zone_http_repository";
|
||||
import { FormState, CoreError } from "../../../../../core/store/base_store";
|
||||
|
||||
export class ZoneStore extends FormState<ZoneViewModel, CoreError> {
|
||||
constructor() {
|
||||
super();
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
viewModel: ZoneViewModel = ZoneViewModel.empty();
|
||||
cameraDeviceHttpRepository: ZoneHttpRepository = new ZoneHttpRepository();
|
||||
errorHandingStrategy = (error: CoreError) => { }
|
||||
init = async (navigate?: NavigateFunction | undefined) => {
|
||||
|
||||
}
|
||||
|
||||
import { isPreviewMode } from "../scene_manager_forms";
|
||||
import { SceneMangerStore } from "../../scene_manager_store";
|
||||
export enum ZoneStoreType {
|
||||
preview = "preview",
|
||||
}
|
||||
export class ZoneStore extends FormState<ZoneModel, CoreError> {
|
||||
|
||||
constructor(sceneMangerStore: SceneMangerStore) {
|
||||
super();
|
||||
this.sceneMangerStore = sceneMangerStore;
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
storeType: ZoneStoreType;
|
||||
sceneMangerStore: SceneMangerStore;
|
||||
viewModel: ZoneModel = ZoneModel.empty();
|
||||
cameraDeviceHttpRepository: ZoneHttpRepository = new ZoneHttpRepository();
|
||||
errorHandingStrategy = (error: CoreError) => {};
|
||||
init = async (navigate?: NavigateFunction | undefined) => {
|
||||
isPreviewMode(this.sceneMangerStore.activeFormDependency).map(() =>
|
||||
this.sceneMangerStore.scene
|
||||
.rFind<ZoneModel>((el) => el.name.isEqual(this.sceneMangerStore.selectedItemName ?? ""))
|
||||
.fold(
|
||||
(zoneModel) => {
|
||||
this.loadDependency(zoneModel);
|
||||
this.storeType = ZoneStoreType.preview;
|
||||
},
|
||||
() =>
|
||||
console.log(
|
||||
`Unknown FormType ${this.sceneMangerStore.selectedItemName} : ${JSON.stringify(
|
||||
this.sceneMangerStore.scene
|
||||
)}`
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
updateWebGl = () => this.storeType.isEqualR(ZoneStoreType.preview).map(() => this.sceneMangerStore.coreThreeRepository!.updateInstance(this.viewModel))
|
||||
}
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
import { Result } from "../../../../../core/helper/result";
|
||||
|
||||
export class ZoneViewModel {
|
||||
name: string;
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
isValid(): Result<string, ZoneViewModel> {
|
||||
return Result.ok();
|
||||
}
|
||||
static empty() {
|
||||
return new ZoneViewModel("");
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ import { CoreInput } from "../../../core/ui/input/input";
|
|||
import { DrawersDataset } from "../../dataset/dataset_store";
|
||||
import { Icon } from "../../../core/ui/icons/icons";
|
||||
import { sceneManagerForms } from "./forms/scene_manager_forms";
|
||||
import { SceneMode } from "../model/scene_view";
|
||||
|
||||
export const SceneManagerPath = "/scene/manager/";
|
||||
|
||||
|
@ -33,7 +34,12 @@ export const SceneManger = observer(() => {
|
|||
return (
|
||||
<MainPage
|
||||
page={"Сцена"}
|
||||
panelStyle={{ display: store.storeMode.isEqual(StoreMode.sceneInstance) ? "" : "none" }}
|
||||
panelStyle={{
|
||||
display: store.storeMode.isEqualR(StoreMode.sceneInstance).fold(
|
||||
() => "",
|
||||
() => "none"
|
||||
),
|
||||
}}
|
||||
panelChildren={
|
||||
<div style={{ width: "100%", height: "100%" }}>
|
||||
<div style={{ height: 260, width: "100%", padding: 10 }}>
|
||||
|
@ -75,6 +81,23 @@ export const SceneManger = observer(() => {
|
|||
<Icon type={el.icon} />
|
||||
</div>
|
||||
))}
|
||||
{store.isVisibleSaveButton ? (
|
||||
<>
|
||||
<div
|
||||
onClick={() => store.sceneSave()}
|
||||
style={{
|
||||
marginTop: 4,
|
||||
width: 50,
|
||||
height: 50,
|
||||
backgroundColor: "rgba(99, 81, 159, 1)",
|
||||
border: "1px solid",
|
||||
borderRadius: 5,
|
||||
}}
|
||||
>
|
||||
<Icon type={SceneMode.Save} />
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -104,7 +127,7 @@ export const SceneManger = observer(() => {
|
|||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
}}
|
||||
onClick={() => store.selectSceneItems(el.name, index, !el.isSelected)}
|
||||
onClick={() => store.selectSceneItems(el.name, index, !el.isSelected, el.icon)}
|
||||
>
|
||||
<Icon width={13} height={13} type={el.icon} style={{ marginLeft: 10 }} />
|
||||
<div style={{ width: 10 }} />
|
||||
|
@ -112,6 +135,7 @@ export const SceneManger = observer(() => {
|
|||
<Icon
|
||||
type="DeleteCircle"
|
||||
width={13}
|
||||
isNeedStopPropagation={true}
|
||||
height={13}
|
||||
onClick={() => store.deleteSceneItem(el)}
|
||||
style={{ marginRight: 10 }}
|
||||
|
@ -124,19 +148,18 @@ export const SceneManger = observer(() => {
|
|||
<div
|
||||
style={{
|
||||
width: "-webkit-fill-available",
|
||||
height: "100%",
|
||||
height: "60%",
|
||||
borderRadius: 7,
|
||||
backgroundColor: "white",
|
||||
margin: 10,
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{sceneManagerForms(store.activeFormDependency ?? {}, store).map((el, i) => {
|
||||
if (el.name.isEqual(store.activeFormType ?? "")) {
|
||||
return <span key={i}>{el.component}</span>;
|
||||
}
|
||||
return <></>;
|
||||
})}
|
||||
{sceneManagerForms(store.activeFormDependency ?? {}, store).map((el, i) =>
|
||||
el.name.isEqualR(store.activeFormType ?? "").fold(
|
||||
() => <span key={i}>{el.component}</span>,
|
||||
() => <span key={i}></span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -1,19 +1,23 @@
|
|||
import makeAutoObservable from "mobx-store-inheritance";
|
||||
import { Object3D, Object3DEventMap, Vector2 } from "three";
|
||||
import { Object3D, Object3DEventMap, Vector2, Vector3 } from "three";
|
||||
import { message } from "antd";
|
||||
import { CoreThreeRepository } from "../../../core/repository/core_three_repository";
|
||||
import { HttpError } from "../../../core/repository/http_repository";
|
||||
import { UiDrawerFormState } from "../../../core/store/base_store";
|
||||
import { UiBaseError } from "../../../core/model/ui_base_error";
|
||||
import { SceneMenu, SceneMode } from "../model/scene_view";
|
||||
import { BaseSceneItemModel, CameraViewModel, RobossemblerFiles, StaticAssetItemModel } from "../model/scene_assets";
|
||||
import { SceneMode } from "../model/scene_view";
|
||||
import { SceneHttpRepository } from "../data/scene_http_repository";
|
||||
import { RobossemblerAssets } from "../../../core/model/robossembler_assets";
|
||||
import { Instance, SceneAsset } from "../../../core/model/scene_asset";
|
||||
import { SceneViewModel } from "../model/scene_view_model";
|
||||
import { SceneModel } from "../model/scene_model";
|
||||
import { SceneManagerForms } from "./forms/scene_manager_forms";
|
||||
import { SolidBodyViewModel } from "./forms/solid_body/solid_body_view_model";
|
||||
import { SolidBodyModel } from "../model/solid_body_model";
|
||||
import { CameraModel } from "../../../core/model/camera_model";
|
||||
import { SolidModel } from "../../../core/model/solid_model";
|
||||
import { PointModel } from "../../../core/model/point_model";
|
||||
import { RobotModel } from "../../../core/model/robot_model";
|
||||
import { ZoneModel } from "../../../core/model/zone_model";
|
||||
import { SceneModelsTypes } from "../../../core/model/scene_models_type";
|
||||
import { Result } from "../../../core/helper/result";
|
||||
|
||||
export enum DrawersSceneManager {
|
||||
NewScene = "Новая сцена",
|
||||
|
@ -22,9 +26,9 @@ export enum StoreMode {
|
|||
sceneInstance = "sceneInstance",
|
||||
allScenes = "allScenes",
|
||||
}
|
||||
export interface ISolidSpawnHelper {
|
||||
export interface ISpawnHelper {
|
||||
url: string;
|
||||
solidType: string;
|
||||
spawn: SceneModelsTypes;
|
||||
name: string;
|
||||
isFinished: boolean;
|
||||
type: string;
|
||||
|
@ -33,7 +37,7 @@ interface IPopoverItem {
|
|||
name: string;
|
||||
fn: Function;
|
||||
}
|
||||
interface SceneItems {
|
||||
export interface SceneItems {
|
||||
fn: Function;
|
||||
name: string;
|
||||
isSelected: boolean;
|
||||
|
@ -42,17 +46,15 @@ interface SceneItems {
|
|||
export class SceneMangerStore extends UiDrawerFormState<SceneViewModel, HttpError> {
|
||||
activeFormType?: string;
|
||||
selectedItemName?: string;
|
||||
activeFormDependency?: Object;
|
||||
activeFormDependency: Object = {};
|
||||
viewModel: SceneViewModel = SceneViewModel.empty();
|
||||
sceneMode: SceneMode;
|
||||
sceneMenu: SceneMenu;
|
||||
sceneItems: SceneItems[];
|
||||
sceneItems: SceneItems[] = [];
|
||||
isVisibleSaveButton: boolean = false;
|
||||
coreThreeRepository: null | CoreThreeRepository = null;
|
||||
sceneHttpRepository: SceneHttpRepository;
|
||||
sceneModels: BaseSceneItemModel[] = [];
|
||||
isSceneMenuShow = false;
|
||||
robossemblerAssets?: RobossemblerAssets;
|
||||
robossemblerAssets?: SceneAsset;
|
||||
objectForMagnetism: string;
|
||||
objectMagnetism: string;
|
||||
scenes: SceneModel[] = [];
|
||||
|
@ -60,46 +62,80 @@ export class SceneMangerStore extends UiDrawerFormState<SceneViewModel, HttpErro
|
|||
canvasRef?: HTMLCanvasElement;
|
||||
mousePositionAwait: boolean = false;
|
||||
mousePosition?: Vector2;
|
||||
solidSpawnHelper?: ISolidSpawnHelper;
|
||||
spawnHelper?: ISpawnHelper;
|
||||
selectSceneObject?: Object;
|
||||
isLoadingForm: boolean = false;
|
||||
scene: SolidBodyModel[] = [];
|
||||
scene: (Instance | SolidModel | CameraModel | RobotModel | PointModel | ZoneModel)[] = [];
|
||||
|
||||
sceneHelperInstruments: { icon: string; onClick: Function; isSelected: boolean }[] = [
|
||||
sceneHelperInstruments: {
|
||||
icon: string;
|
||||
onClick: Function;
|
||||
isSelected: boolean;
|
||||
}[] = [
|
||||
{ icon: SceneMode.Select, onClick: () => this.setMode(SceneMode.Select), isSelected: false },
|
||||
{ icon: SceneMode.Move, onClick: () => this.setMode(SceneMode.Move), isSelected: false },
|
||||
{ icon: SceneMode.Rotate, onClick: () => this.setMode(SceneMode.Rotate), isSelected: false },
|
||||
{
|
||||
icon: SceneMode.Save,
|
||||
onClick: () => {
|
||||
this.setMode(SceneMode.Rotate);
|
||||
this.sceneSave();
|
||||
},
|
||||
isSelected: false,
|
||||
},
|
||||
];
|
||||
popoverItems: IPopoverItem[] = [
|
||||
{ name: "Камера", fn: () => this.createNewForm(SceneManagerForms.camera) },
|
||||
{ name: "Твердое тело", fn: () => this.createNewForm(SceneManagerForms.solidBody) },
|
||||
{ name: "Источник света", fn: () => this.createNewForm(SceneManagerForms.light) },
|
||||
{ name: "Робот", fn: () => this.createNewForm(SceneManagerForms.robot) },
|
||||
{ name: "Точка", fn: () => this.createNewForm(SceneManagerForms.point) },
|
||||
{ name: "Траектория", fn: () => this.createNewForm(SceneManagerForms.trajectory) },
|
||||
{ name: "Зона", fn: () => this.createNewForm(SceneManagerForms.zone) },
|
||||
{ name: "Камера", fn: () => this.createNewForm(SceneManagerForms.camera, true) },
|
||||
{ name: "Твердое тело", fn: () => this.createNewForm(SceneManagerForms.solidBody, true) },
|
||||
{ name: "Источник света", fn: () => this.createNewForm(SceneManagerForms.light, true) },
|
||||
{ name: "Робот", fn: () => this.createNewForm(SceneManagerForms.robot, true) },
|
||||
{ name: "Точка", fn: () => this.createNewForm(SceneManagerForms.point, true) },
|
||||
{ name: "Траектория", fn: () => this.createNewForm(SceneManagerForms.trajectory, true) },
|
||||
{ name: "Зона", fn: () => this.createNewForm(SceneManagerForms.zone, true) },
|
||||
{ name: "Точки захвата", fn: () => this.createNewForm(SceneManagerForms.capturePoints, true) },
|
||||
];
|
||||
canvasOffsetX?: number;
|
||||
constructor() {
|
||||
super(DrawersSceneManager);
|
||||
makeAutoObservable(this);
|
||||
this.sceneItems = [];
|
||||
|
||||
this.sceneHttpRepository = new SceneHttpRepository();
|
||||
this.sceneMode = SceneMode.Select;
|
||||
this.sceneMenu = SceneMenu.empty();
|
||||
}
|
||||
sceneSave = () => {};
|
||||
selectSceneItems = (name: string, index: number, selected: boolean) => {
|
||||
sceneSave = () => {
|
||||
console.log(JSON.stringify(this.scene));
|
||||
};
|
||||
iconToSceneManagerForm = (icon: string): SceneManagerForms => {
|
||||
if (icon.isEqual("Camera")) {
|
||||
this.activeFormDependency = {
|
||||
type: "Preview",
|
||||
};
|
||||
return SceneManagerForms.camera;
|
||||
}
|
||||
if (icon.isEqual("Zone")) {
|
||||
this.activeFormDependency = {
|
||||
type: "Preview",
|
||||
};
|
||||
return SceneManagerForms.zone;
|
||||
}
|
||||
if (icon.isEqual("Robot")) {
|
||||
this.activeFormDependency = {
|
||||
type: "Preview",
|
||||
};
|
||||
return SceneManagerForms.robot;
|
||||
}
|
||||
if (icon.isEqual("Point")) {
|
||||
this.activeFormDependency = {
|
||||
type: "Preview",
|
||||
};
|
||||
return SceneManagerForms.point;
|
||||
}
|
||||
|
||||
this.activeFormDependency = {
|
||||
type: "Preview",
|
||||
};
|
||||
return SceneManagerForms.solidBody;
|
||||
};
|
||||
selectSceneItems = (name: string, index: number, selected: boolean, icon: string) => {
|
||||
this.sceneItems.map((el) => {
|
||||
el.isSelected = false;
|
||||
return el;
|
||||
});
|
||||
this.sceneItems.map((el, i) => i.isEqualR(index).map(() => (el.isSelected = selected)));
|
||||
if (selected) {
|
||||
this.createNewForm(SceneManagerForms.solidBody);
|
||||
this.createNewForm(this.iconToSceneManagerForm(icon));
|
||||
this.selectedItemName = name;
|
||||
}
|
||||
|
||||
|
@ -107,28 +143,21 @@ export class SceneMangerStore extends UiDrawerFormState<SceneViewModel, HttpErro
|
|||
this.createNewForm(undefined);
|
||||
this.selectedItemName = undefined;
|
||||
}
|
||||
|
||||
this.activeFormDependency = {
|
||||
type: "Preview",
|
||||
name: name,
|
||||
};
|
||||
};
|
||||
|
||||
setMode = (mode: SceneMode) => {
|
||||
this.sceneHelperInstruments.map((el) => {
|
||||
el.isSelected = false;
|
||||
if (el.icon.isEqual(mode)) {
|
||||
el.isSelected = true;
|
||||
}
|
||||
el.icon.isEqualR(mode).map(() => (el.isSelected = true));
|
||||
return el;
|
||||
});
|
||||
this.sceneMode = mode;
|
||||
this.coreThreeRepository?.setTransformMode(this.sceneMode);
|
||||
this.sceneModeWatcher();
|
||||
};
|
||||
createNewForm = (formType: SceneManagerForms | undefined) => {
|
||||
this.activeFormDependency = {
|
||||
store: this,
|
||||
};
|
||||
createNewForm = (formType: SceneManagerForms | undefined, isNeedClearDependency?: boolean) => {
|
||||
this.activeFormDependency = Object.assign(this.activeFormDependency, { store: this });
|
||||
if (isNeedClearDependency) this.activeFormDependency = { store: this };
|
||||
this.activeFormType = formType;
|
||||
};
|
||||
createNewScene = () =>
|
||||
|
@ -143,7 +172,8 @@ export class SceneMangerStore extends UiDrawerFormState<SceneViewModel, HttpErro
|
|||
|
||||
deleteSceneItem = (item: SceneItems) => {
|
||||
this.sceneItems = this.sceneItems.filter((el) => !el.name.isEqual(item.name));
|
||||
this.coreThreeRepository?.deleteSceneItem(item.name);
|
||||
|
||||
this.coreThreeRepository?.deleteSceneItem(item);
|
||||
this.visibleSaveButton();
|
||||
};
|
||||
|
||||
|
@ -151,48 +181,26 @@ export class SceneMangerStore extends UiDrawerFormState<SceneViewModel, HttpErro
|
|||
this.isVisibleSaveButton = true;
|
||||
};
|
||||
|
||||
addNewCamera = (model: CameraViewModel) => {
|
||||
model.position = this.coreThreeRepository!.camera.position;
|
||||
model.quaternion = this.coreThreeRepository!.camera.quaternion;
|
||||
this.sceneItems.push({ name: model.cameraLink, icon: "Camera", fn: () => {}, isSelected: false });
|
||||
|
||||
addNewCamera = (model: CameraModel) => {
|
||||
model.vector3 = this.coreThreeRepository!.camera.position;
|
||||
// model.quaternion = this.coreThreeRepository!.camera.quaternion;
|
||||
this.sceneItems.push({ name: model.name, icon: "Camera", fn: () => {}, isSelected: false });
|
||||
this.scene.push(model);
|
||||
this.coreThreeRepository?.addSceneCamera(model);
|
||||
this.visibleSaveButton();
|
||||
};
|
||||
|
||||
getCameraLinkNames = (): string[] => {
|
||||
return this.sceneModels.map((el) => {
|
||||
if (el instanceof CameraViewModel) {
|
||||
return el.cameraLink;
|
||||
}
|
||||
return "";
|
||||
});
|
||||
};
|
||||
|
||||
loaderWatcher = () => {};
|
||||
|
||||
loadSceneRobossemblerAsset = (name: string) => {
|
||||
try {
|
||||
const assetPath = this.robossemblerAssets?.getAssetPath(name) as string;
|
||||
|
||||
this.coreThreeRepository?.loader(assetPath, this.loaderWatcher, name);
|
||||
this.visibleSaveButton();
|
||||
// this.coreThreeRepository?.loader(assetPath, this.loaderWatcher, name);
|
||||
// this.visibleSaveButton();
|
||||
} catch (error) {
|
||||
message.error(String(error));
|
||||
}
|
||||
};
|
||||
|
||||
isRenderedAsset(name: string): boolean {
|
||||
return this.sceneModels
|
||||
.filter((el) => {
|
||||
if (el instanceof StaticAssetItemModel) {
|
||||
return el.name === name;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.isNotEmpty();
|
||||
}
|
||||
|
||||
hiddenMenu = () => (this.isSceneMenuShow = false);
|
||||
|
||||
sceneModeWatcher() {}
|
||||
|
@ -207,9 +215,7 @@ export class SceneMangerStore extends UiDrawerFormState<SceneViewModel, HttpErro
|
|||
}
|
||||
};
|
||||
errorHandingStrategy = (error: HttpError) =>
|
||||
error.status
|
||||
.isEqualR(404)
|
||||
.map(() => this.errors.push(new UiBaseError(`${RobossemblerFiles.robossemblerAssets} not found to project`)));
|
||||
error.status.isEqualR(404).map(() => this.errors.push(new UiBaseError(`not found to project`)));
|
||||
|
||||
loadScene = (canvasRef: HTMLCanvasElement) => {
|
||||
this.canvasRef = canvasRef;
|
||||
|
@ -222,36 +228,52 @@ export class SceneMangerStore extends UiDrawerFormState<SceneViewModel, HttpErro
|
|||
|
||||
loadWebGl(canvasRef: HTMLCanvasElement): void {
|
||||
this.coreThreeRepository = new CoreThreeRepository(canvasRef as HTMLCanvasElement, this.watcherSceneEditorObject);
|
||||
this.coreThreeRepository.on(this.watcherThereObjects);
|
||||
this.coreThreeRepository.render();
|
||||
this.sceneModels = this.coreThreeRepository.getAllSceneModels();
|
||||
|
||||
this.coreThreeRepository.render();
|
||||
|
||||
this.canvasOffsetX = canvasRef.getBoundingClientRect().x;
|
||||
canvasRef.addEventListener("click", (event) => this.clickLister(event, canvasRef.getBoundingClientRect().x));
|
||||
|
||||
canvasRef.addEventListener("mousedown", (e) => this.sceneContextMenu(e));
|
||||
const sceneAssets = this.sceneHttpRepository.getScene();
|
||||
this.coreThreeRepository?.loadScene(sceneAssets);
|
||||
this.sceneItems = sceneAssets.scene.map((el) => {
|
||||
return {
|
||||
fn: () => {},
|
||||
name: el.name,
|
||||
icon: el.icon,
|
||||
isSelected: false,
|
||||
};
|
||||
});
|
||||
this.scene = sceneAssets.scene;
|
||||
}
|
||||
clickScene = (event: MouseEvent, offset: number = 0): Result<void, Vector3> => {
|
||||
const vector = new Vector2();
|
||||
const boundingRect = this.canvasRef!.getBoundingClientRect();
|
||||
|
||||
vector.x = ((event.clientX - offset) / boundingRect.width) * 2 - 1;
|
||||
vector.y = -(event.clientY / boundingRect.height) * 2 + 1;
|
||||
return this.coreThreeRepository!.setRayCastAndGetFirstObjectAndPointToObject(vector);
|
||||
};
|
||||
clickLister = (event: MouseEvent, offset: number = 0) => {
|
||||
const vector = new Vector2();
|
||||
const boundingRect = this.canvasRef!.getBoundingClientRect();
|
||||
|
||||
vector.x = ((event.clientX - offset) / boundingRect.width) * 2 - 1;
|
||||
vector.y = -(event.clientY / boundingRect.height) * 2 + 1;
|
||||
if (this.mousePositionAwait && this.solidSpawnHelper) {
|
||||
if (this.mousePositionAwait && this.spawnHelper) {
|
||||
this.mousePositionAwait = false;
|
||||
this.mousePosition = vector;
|
||||
this.coreThreeRepository?.setRayCastAndGetFirstObjectAndPointToObject(vector).map((v3) => {
|
||||
this.coreThreeRepository?.solidSpawn(
|
||||
this.solidSpawnHelper as ISolidSpawnHelper,
|
||||
this.spawnHelper as ISpawnHelper,
|
||||
(obj: Object3D<Object3DEventMap> | undefined) => {
|
||||
const { solidType, name, url } = this.solidSpawnHelper as ISolidSpawnHelper;
|
||||
this.scene.push(new SolidBodyModel(obj!.quaternion, obj!.position, name, solidType, url, url));
|
||||
this.sceneItems.push({
|
||||
name: String(this.solidSpawnHelper?.name),
|
||||
name: String(this.spawnHelper?.name),
|
||||
icon: "Solid",
|
||||
isSelected: false,
|
||||
fn: () => this.createNewForm(SceneManagerForms.solidBody),
|
||||
});
|
||||
this.visibleSaveButton();
|
||||
},
|
||||
v3
|
||||
);
|
||||
|
@ -261,38 +283,27 @@ export class SceneMangerStore extends UiDrawerFormState<SceneViewModel, HttpErro
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.sceneMode === SceneMode.Move || this.sceneMode === SceneMode.Rotate) {
|
||||
if (this.sceneMode.isEqualMany([SceneMode.Move, SceneMode.Rotate])) {
|
||||
this.transformContollsCall(vector);
|
||||
}
|
||||
};
|
||||
|
||||
sceneContextMenu = (e: MouseEvent) =>
|
||||
e.button.isEqualR(2).map(() => {
|
||||
this.isSceneMenuShow = true;
|
||||
this.sceneMenu.x = e.clientX;
|
||||
this.sceneMenu.y = e.clientY;
|
||||
});
|
||||
|
||||
watcherThereObjects = (sceneItemModel: BaseSceneItemModel) => {
|
||||
// this.sceneModels.push(sceneItemModel);
|
||||
console.log(sceneItemModel);
|
||||
};
|
||||
|
||||
watcherSceneEditorObject = (mesh: Object3D) => {
|
||||
this.scene = this.scene.map((el) =>
|
||||
watcherSceneEditorObject = (mesh: Object3D) => (
|
||||
(this.scene = this.scene.map((el) =>
|
||||
el.name.isEqualR(mesh.name).fold(
|
||||
() => {
|
||||
el.vector3 = mesh.position;
|
||||
el.quaternion = mesh.quaternion;
|
||||
el.quaternion = mesh.quaternion.toArray();
|
||||
return el;
|
||||
},
|
||||
() => el
|
||||
)
|
||||
);
|
||||
this.visibleSaveButton();
|
||||
};
|
||||
)),
|
||||
this.visibleSaveButton()
|
||||
);
|
||||
|
||||
transformContollsCall = (vector: Vector2) =>
|
||||
// @ts-ignore
|
||||
this.coreThreeRepository?.setRayCastAndGetFirstObject(vector).fold(
|
||||
(object) => this.coreThreeRepository?.setTransformControlsAttach(object),
|
||||
(_) => this.coreThreeRepository?.disposeTransformControlsMode()
|
||||
|
@ -300,6 +311,5 @@ export class SceneMangerStore extends UiDrawerFormState<SceneViewModel, HttpErro
|
|||
|
||||
dispose = () => {
|
||||
window.removeEventListener("click", this.clickLister);
|
||||
window.removeEventListener("mousedown", (e) => this.sceneContextMenu(e));
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,11 +5,11 @@ 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 { configure } from "mobx"
|
||||
import { configure } from "mobx";
|
||||
|
||||
configure({
|
||||
enforceActions: "never",
|
||||
})
|
||||
enforceActions: "never",
|
||||
});
|
||||
extensions();
|
||||
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
27.04.2024 @shalenikol release 0.1
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from train_Yolo import train_YoloV8
|
||||
from train_Dope import train_Dope_i
|
||||
|
|
26
web_p/robot_builder.py
Normal file
26
web_p/robot_builder.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
import shutil
|
||||
import argparse
|
||||
import os.path
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--path")
|
||||
parser.add_argument("--nDOF")
|
||||
parser.add_argument("--toolType")
|
||||
parser.add_argument("--name")
|
||||
|
||||
args = parser.parse_args()
|
||||
2
|
||||
|
||||
def copy_and_move_folder(src, dst):
|
||||
try:
|
||||
if os.path.exists(dst):
|
||||
shutil.rmtree(dst)
|
||||
shutil.copytree(src, dst)
|
||||
print(f"Folder {src} successfully copied")
|
||||
except shutil.Error as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
|
||||
source_folder = os.path.dirname(os.path.abspath(__file__)) + "/robot_builder/"
|
||||
|
||||
copy_and_move_folder(source_folder, args.path + args.name)
|
|
@ -1 +0,0 @@
|
|||
/home/idontsudo/robossembler_ws/src/rbs_gripper/meshes/visual/base.dae
|
168
web_p/robot_builder/base.dae
Normal file
168
web_p/robot_builder/base.dae
Normal file
File diff suppressed because one or more lines are too long
|
@ -1 +0,0 @@
|
|||
/home/idontsudo/robossembler_ws/src/rbs_arm/meshes/visual/ee_link.dae
|
101
web_p/robot_builder/ee_link.dae
Normal file
101
web_p/robot_builder/ee_link.dae
Normal file
File diff suppressed because one or more lines are too long
|
@ -1 +0,0 @@
|
|||
/home/idontsudo/robossembler_ws/src/rbs_gripper/meshes/visual/finger.dae
|
92
web_p/robot_builder/finger.dae
Normal file
92
web_p/robot_builder/finger.dae
Normal file
File diff suppressed because one or more lines are too long
|
@ -1 +0,0 @@
|
|||
/home/idontsudo/robossembler_ws/src/rbs_arm/meshes/visual/fork_link.dae
|
101
web_p/robot_builder/fork_link.dae
Normal file
101
web_p/robot_builder/fork_link.dae
Normal file
File diff suppressed because one or more lines are too long
|
@ -1 +0,0 @@
|
|||
/home/idontsudo/robossembler_ws/src/rbs_arm/meshes/visual/main_link.dae
|
101
web_p/robot_builder/main_link.dae
Normal file
101
web_p/robot_builder/main_link.dae
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
@ -1 +0,0 @@
|
|||
/home/idontsudo/robossembler_ws/src/rbs_gripper/meshes/visual/rotor.dae
|
121
web_p/robot_builder/rotor.dae
Normal file
121
web_p/robot_builder/rotor.dae
Normal file
File diff suppressed because one or more lines are too long
|
@ -1 +0,0 @@
|
|||
/home/idontsudo/robossembler_ws/src/rbs_arm/meshes/visual/start_link.dae
|
101
web_p/robot_builder/start_link.dae
Normal file
101
web_p/robot_builder/start_link.dae
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue