Updated way that connections are previewed.
This commit is contained in:
parent
cdaff97614
commit
6ec28efe78
@ -211,11 +211,9 @@ export class Base {
|
|||||||
let mesh = this.xr.pointerSelection.getMeshUnderPointer(this.xrInputSource.uniqueId);
|
let mesh = this.xr.pointerSelection.getMeshUnderPointer(this.xrInputSource.uniqueId);
|
||||||
if (this.diagramManager.isDiagramObject(mesh)) {
|
if (this.diagramManager.isDiagramObject(mesh)) {
|
||||||
this.logger.debug("click on " + mesh.id);
|
this.logger.debug("click on " + mesh.id);
|
||||||
if (this.clickMenu && !this.clickMenu.isDisposed) {
|
if (this.diagramManager.diagramMenuManager.connectionPreview) {
|
||||||
if (this.clickMenu.isConnecting) {
|
this.diagramManager.diagramMenuManager.connect(mesh);
|
||||||
this.clickMenu.connect(mesh);
|
|
||||||
this.clickMenu = null;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this.clickMenu = this.diagramManager.diagramMenuManager.createClickMenu(mesh, this.xrInputSource);
|
this.clickMenu = this.diagramManager.diagramMenuManager.createClickMenu(mesh, this.xrInputSource);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,205 +0,0 @@
|
|||||||
import {AbstractMesh, MeshBuilder, Scene, StandardMaterial, TransformNode, Vector3} from "@babylonjs/core";
|
|
||||||
import {v4 as uuidv4} from 'uuid';
|
|
||||||
import log, {Logger} from "loglevel";
|
|
||||||
import {buildStandardMaterial} from "../materials/functions/buildStandardMaterial";
|
|
||||||
|
|
||||||
|
|
||||||
export class DiagramConnection {
|
|
||||||
|
|
||||||
private readonly id: string;
|
|
||||||
private logger: Logger = log.getLogger('DiagramConnection');
|
|
||||||
constructor(from: string, to: string, id: string, scene?: Scene, gripTransform?: TransformNode, clickPoint?: Vector3) {
|
|
||||||
this.logger.debug('buildConnection constructor');
|
|
||||||
if (id) {
|
|
||||||
this.id = id;
|
|
||||||
} else {
|
|
||||||
this.id = "connection_" + uuidv4();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.scene = scene;
|
|
||||||
this._to = to;
|
|
||||||
this._from = from;
|
|
||||||
|
|
||||||
const fromMesh = this.scene.getMeshById(from);
|
|
||||||
if (fromMesh) {
|
|
||||||
this.fromAnchor = fromMesh;
|
|
||||||
}
|
|
||||||
|
|
||||||
const toMesh = this.scene.getMeshById(to);
|
|
||||||
if (toMesh) {
|
|
||||||
this.toAnchor = toMesh;
|
|
||||||
} else {
|
|
||||||
if (fromMesh) {
|
|
||||||
const to = new TransformNode(this.id + "_to", this.scene);
|
|
||||||
to.ignoreNonUniformScaling = true;
|
|
||||||
to.id = this.id + "_to";
|
|
||||||
if (clickPoint) {
|
|
||||||
to.position = clickPoint.clone();
|
|
||||||
} else {
|
|
||||||
to.position = fromMesh.absolutePosition.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gripTransform) {
|
|
||||||
to.setParent(gripTransform);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.toAnchor = to;
|
|
||||||
} else {
|
|
||||||
this.logger.info("no fromMesh yet, will build when toMesh is available");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.buildConnection();
|
|
||||||
}
|
|
||||||
|
|
||||||
private scene: Scene;
|
|
||||||
private toAnchor: TransformNode;
|
|
||||||
private fromAnchor: TransformNode;
|
|
||||||
private transformNode: TransformNode;
|
|
||||||
|
|
||||||
private _mesh: AbstractMesh;
|
|
||||||
|
|
||||||
public get mesh(): AbstractMesh {
|
|
||||||
return this._mesh;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly _to: string;
|
|
||||||
|
|
||||||
public get to(): string {
|
|
||||||
return this?.toAnchor?.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public set to(value: string) {
|
|
||||||
if (!value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const toAnchor = this.scene.getMeshById(value);
|
|
||||||
if (this.fromAnchor && toAnchor) {
|
|
||||||
this.toAnchor.dispose();
|
|
||||||
this.toAnchor = toAnchor;
|
|
||||||
this._mesh.metadata.to = this.to;
|
|
||||||
this._mesh.metadata.exportable = true;
|
|
||||||
this._mesh.id = this.id;
|
|
||||||
this.recalculate();
|
|
||||||
this.setPoints();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly _from: string;
|
|
||||||
|
|
||||||
public get from(): string {
|
|
||||||
return this?.fromAnchor?.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
private tick: number = 0;
|
|
||||||
|
|
||||||
private recalculate() {
|
|
||||||
const start = this.fromAnchor?.absolutePosition;
|
|
||||||
const end = this.toAnchor?.absolutePosition;
|
|
||||||
if (start && end) {
|
|
||||||
this.transformNode.position = start.add(end).scale(.5);
|
|
||||||
this.transformNode.lookAt(end);
|
|
||||||
this._mesh.rotation.x = Math.PI / 2;
|
|
||||||
this._mesh.scaling.y = Math.abs(start.subtract(end).length());
|
|
||||||
const text = this._mesh.getChildren((node) => {
|
|
||||||
return node.metadata?.label == true;
|
|
||||||
});
|
|
||||||
if (text && text.length > 0) {
|
|
||||||
text.forEach((node) => {
|
|
||||||
const t: AbstractMesh = node as AbstractMesh;
|
|
||||||
t.scaling.y = 1 / this._mesh.scaling.y;
|
|
||||||
t.position.x = .05;
|
|
||||||
t.position.z = .05;
|
|
||||||
t.position.y = 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (this.fromAnchor && (this.fromAnchor as AbstractMesh).material) {
|
|
||||||
this._mesh.material = (((this.fromAnchor as AbstractMesh).material as StandardMaterial));
|
|
||||||
} else {
|
|
||||||
this._mesh.material = buildStandardMaterial(this.id + "_material", this.scene, "#FFFFFF");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private setPoints() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildConnection() {
|
|
||||||
this.logger.debug(`buildConnection from ${this._from} to ${this._to}`);
|
|
||||||
this._mesh = MeshBuilder.CreateCylinder(this.id + "_connection", {diameter: .025, height: 1}, this.scene);
|
|
||||||
this.transformNode = new TransformNode(this.id + "_transform", this.scene);
|
|
||||||
this.transformNode.metadata = {exportable: true};
|
|
||||||
this._mesh.setParent(this.transformNode);
|
|
||||||
this.recalculate();
|
|
||||||
this._mesh.id = this.id;
|
|
||||||
if (!this._mesh.metadata) {
|
|
||||||
this._mesh.metadata = {template: "#connection-template", from: this._from};
|
|
||||||
} else {
|
|
||||||
this._mesh.metadata.template = "#connection-template";
|
|
||||||
this._mesh.metadata.from = this._from;
|
|
||||||
}
|
|
||||||
if (this._to) {
|
|
||||||
this._mesh.metadata.to = this.to;
|
|
||||||
|
|
||||||
}
|
|
||||||
this._mesh.metadata.exportable = true;
|
|
||||||
this.setPoints();
|
|
||||||
this.scene.onBeforeRenderObservable.add(this.beforeRender, -1, true, this);
|
|
||||||
this.scene.onNewMeshAddedObservable.add(this.onMeshAdded, -1, true, this);
|
|
||||||
this.mesh.onDisposeObservable.add(this.removeConnection, -1, true, this);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
private beforeRender = () => {
|
|
||||||
if (this.tick++ % 10 == 0) {
|
|
||||||
this.logger.trace('recalculating', this.tick);
|
|
||||||
this.recalculate();
|
|
||||||
this.setPoints();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private removeConnection = () => {
|
|
||||||
this.logger.debug("removeConnection");
|
|
||||||
this.scene.onBeforeRenderObservable.removeCallback(this.beforeRender);
|
|
||||||
this._mesh.onDisposeObservable.removeCallback(this.removeConnection);
|
|
||||||
this.removeObserver();
|
|
||||||
if (this.toAnchor) {
|
|
||||||
this.toAnchor = null;
|
|
||||||
}
|
|
||||||
if (this.fromAnchor) {
|
|
||||||
this.fromAnchor = null;
|
|
||||||
}
|
|
||||||
if (this._mesh) {
|
|
||||||
this._mesh.dispose();
|
|
||||||
this._mesh = null;
|
|
||||||
}
|
|
||||||
if (this.scene) {
|
|
||||||
this.scene = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private onMeshAdded = (mesh: AbstractMesh) => {
|
|
||||||
if (mesh && mesh.id) {
|
|
||||||
if (!this.toAnchor || !this.fromAnchor) {
|
|
||||||
if (mesh?.id == this?._to) {
|
|
||||||
this.logger.debug("Found to anchor");
|
|
||||||
this.toAnchor = mesh;
|
|
||||||
this._mesh.metadata.to = this.to;
|
|
||||||
}
|
|
||||||
if (mesh?.id == this?._from) {
|
|
||||||
this.logger.debug("Found from anchor");
|
|
||||||
this.fromAnchor = mesh;
|
|
||||||
this._mesh.metadata.from = this.from;
|
|
||||||
}
|
|
||||||
if (this.toAnchor && this.fromAnchor) {
|
|
||||||
this.logger.debug(`connection built from ${this._from} to ${this._to}`);
|
|
||||||
this.removeObserver();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private removeObserver() {
|
|
||||||
this.logger.debug("removing observer");
|
|
||||||
this.scene.onNewMeshAddedObservable.removeCallback(this.onMeshAdded);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -8,7 +8,6 @@ import {DefaultScene} from "../defaultScene";
|
|||||||
import {DiagramMenuManager} from "./diagramMenuManager";
|
import {DiagramMenuManager} from "./diagramMenuManager";
|
||||||
import {DiagramEventObserverMask} from "./types/diagramEventObserverMask";
|
import {DiagramEventObserverMask} from "./types/diagramEventObserverMask";
|
||||||
import {DiagramObject} from "../objects/diagramObject";
|
import {DiagramObject} from "../objects/diagramObject";
|
||||||
import {DiagramConnection} from "./diagramConnection";
|
|
||||||
|
|
||||||
|
|
||||||
export class DiagramManager {
|
export class DiagramManager {
|
||||||
@ -20,7 +19,6 @@ export class DiagramManager {
|
|||||||
private readonly _diagramMenuManager: DiagramMenuManager;
|
private readonly _diagramMenuManager: DiagramMenuManager;
|
||||||
private readonly _scene: Scene;
|
private readonly _scene: Scene;
|
||||||
private readonly _diagramObjects: Map<string, DiagramObject> = new Map<string, DiagramObject>();
|
private readonly _diagramObjects: Map<string, DiagramObject> = new Map<string, DiagramObject>();
|
||||||
private readonly _diagramConnections: Map<string, DiagramConnection> = new Map<string, DiagramConnection>();
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._scene = DefaultScene.Scene;
|
this._scene = DefaultScene.Scene;
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {ClickMenu} from "../menus/clickMenu";
|
|||||||
import {ConfigMenu} from "../menus/configMenu";
|
import {ConfigMenu} from "../menus/configMenu";
|
||||||
import {AppConfig} from "../util/appConfig";
|
import {AppConfig} from "../util/appConfig";
|
||||||
import {DiagramEventObserverMask} from "./types/diagramEventObserverMask";
|
import {DiagramEventObserverMask} from "./types/diagramEventObserverMask";
|
||||||
|
import {ConnectionPreview} from "../menus/connectionPreview";
|
||||||
|
|
||||||
|
|
||||||
export class DiagramMenuManager {
|
export class DiagramMenuManager {
|
||||||
@ -20,6 +21,7 @@ export class DiagramMenuManager {
|
|||||||
private readonly _inputTextView: InputTextView;
|
private readonly _inputTextView: InputTextView;
|
||||||
private readonly _scene: Scene;
|
private readonly _scene: Scene;
|
||||||
private logger = log.getLogger('DiagramMenuManager');
|
private logger = log.getLogger('DiagramMenuManager');
|
||||||
|
private _connectionPreview: ConnectionPreview;
|
||||||
|
|
||||||
constructor(notifier: Observable<DiagramEvent>, controllers: Controllers, config: AppConfig) {
|
constructor(notifier: Observable<DiagramEvent>, controllers: Controllers, config: AppConfig) {
|
||||||
this._scene = DefaultScene.Scene;
|
this._scene = DefaultScene.Scene;
|
||||||
@ -65,12 +67,23 @@ export class DiagramMenuManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get connectionPreview(): ConnectionPreview {
|
||||||
|
return this._connectionPreview;
|
||||||
|
}
|
||||||
|
|
||||||
|
public connect(mesh: AbstractMesh) {
|
||||||
|
if (this._connectionPreview) {
|
||||||
|
this._connectionPreview.connect(mesh);
|
||||||
|
this._connectionPreview = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public editText(mesh: AbstractMesh) {
|
public editText(mesh: AbstractMesh) {
|
||||||
this._inputTextView.show(mesh);
|
this._inputTextView.show(mesh);
|
||||||
}
|
}
|
||||||
|
|
||||||
public createClickMenu(mesh: AbstractMesh, input: WebXRInputSource): ClickMenu {
|
public createClickMenu(mesh: AbstractMesh, input: WebXRInputSource): ClickMenu {
|
||||||
const clickMenu = new ClickMenu(mesh, input, this._notifier);
|
const clickMenu = new ClickMenu(mesh);
|
||||||
clickMenu.onClickMenuObservable.add((evt: ActionEvent) => {
|
clickMenu.onClickMenuObservable.add((evt: ActionEvent) => {
|
||||||
console.log(evt);
|
console.log(evt);
|
||||||
switch (evt.source.id) {
|
switch (evt.source.id) {
|
||||||
@ -81,6 +94,7 @@ export class DiagramMenuManager {
|
|||||||
this.editText(clickMenu.mesh);
|
this.editText(clickMenu.mesh);
|
||||||
break;
|
break;
|
||||||
case "connect":
|
case "connect":
|
||||||
|
this._connectionPreview = new ConnectionPreview(clickMenu.mesh.id, input, evt.additionalData.pickedPoint, this._notifier);
|
||||||
break;
|
break;
|
||||||
case "size":
|
case "size":
|
||||||
this.scaleMenu.show(clickMenu.mesh);
|
this.scaleMenu.show(clickMenu.mesh);
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import {AbstractMesh, ActionEvent, Observable, Scene, TransformNode, WebXRInputSource} from "@babylonjs/core";
|
import {AbstractMesh, ActionEvent, Observable, Scene, TransformNode} from "@babylonjs/core";
|
||||||
import {DiagramEvent, DiagramEventType, DiagramTemplates} from "../diagram/types/diagramEntity";
|
|
||||||
import {HtmlButton} from "babylon-html";
|
import {HtmlButton} from "babylon-html";
|
||||||
import {DiagramEventObserverMask} from "../diagram/types/diagramEventObserverMask";
|
|
||||||
|
|
||||||
const POINTER_UP = "pointerup";
|
const POINTER_UP = "pointerup";
|
||||||
|
|
||||||
@ -9,24 +7,9 @@ export class ClickMenu {
|
|||||||
private readonly _mesh: AbstractMesh;
|
private readonly _mesh: AbstractMesh;
|
||||||
private readonly transform: TransformNode;
|
private readonly transform: TransformNode;
|
||||||
public onClickMenuObservable: Observable<ActionEvent> = new Observable<ActionEvent>();
|
public onClickMenuObservable: Observable<ActionEvent> = new Observable<ActionEvent>();
|
||||||
private _diagramEventObservable: Observable<DiagramEvent>;
|
|
||||||
|
|
||||||
private connectFromId: string = null;
|
constructor(mesh: AbstractMesh) {
|
||||||
|
|
||||||
private getTransform(input: WebXRInputSource | TransformNode): TransformNode {
|
|
||||||
if (input == null) return null;
|
|
||||||
if ('grip' in input) {
|
|
||||||
return input.grip;
|
|
||||||
} else {
|
|
||||||
return input as TransformNode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(mesh: AbstractMesh, input: WebXRInputSource | TransformNode, diagramEventObservable: Observable<DiagramEvent>) {
|
|
||||||
|
|
||||||
const grip: TransformNode = this.getTransform(input);
|
|
||||||
this._mesh = mesh;
|
this._mesh = mesh;
|
||||||
this._diagramEventObservable = diagramEventObservable;
|
|
||||||
const scene = mesh.getScene();
|
const scene = mesh.getScene();
|
||||||
this.transform = new TransformNode("transform", scene);
|
this.transform = new TransformNode("transform", scene);
|
||||||
let x = -.54 / 2;
|
let x = -.54 / 2;
|
||||||
@ -51,8 +34,8 @@ export class ClickMenu {
|
|||||||
this.makeNewButton("Connect", "connect", scene, x += .11)
|
this.makeNewButton("Connect", "connect", scene, x += .11)
|
||||||
.onPointerObservable.add((eventData) => {
|
.onPointerObservable.add((eventData) => {
|
||||||
if (isUp(eventData)) {
|
if (isUp(eventData)) {
|
||||||
this.connectFromId = this._mesh.id;
|
this.onClickMenuObservable.notifyObservers(eventData);
|
||||||
//this.createMeshConnection(this._mesh, grip, eventData.additionalData.pickedPoint.clone());
|
this.dispose();
|
||||||
}
|
}
|
||||||
}, -1, false, this, false);
|
}, -1, false, this, false);
|
||||||
|
|
||||||
@ -79,31 +62,6 @@ export class ClickMenu {
|
|||||||
this.transform.rotation.z = 0;
|
this.transform.rotation.z = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isConnecting() {
|
|
||||||
return this.connectFromId != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public connect(mesh: AbstractMesh) {
|
|
||||||
if (this.isConnecting) {
|
|
||||||
if (mesh) {
|
|
||||||
this._diagramEventObservable.notifyObservers({
|
|
||||||
type: DiagramEventType.ADD,
|
|
||||||
entity: {
|
|
||||||
from: this.connectFromId,
|
|
||||||
to: mesh.id,
|
|
||||||
template: DiagramTemplates.CONNECTION,
|
|
||||||
color: '#000000'
|
|
||||||
}
|
|
||||||
}, DiagramEventObserverMask.ALL);
|
|
||||||
this.connectFromId = null;
|
|
||||||
this.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public get isDisposed(): boolean {
|
|
||||||
return this.transform.isDisposed();
|
|
||||||
}
|
|
||||||
|
|
||||||
public get mesh(): AbstractMesh {
|
public get mesh(): AbstractMesh {
|
||||||
return this._mesh;
|
return this._mesh;
|
||||||
}
|
}
|
||||||
|
|||||||
76
src/menus/connectionPreview.ts
Normal file
76
src/menus/connectionPreview.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import {
|
||||||
|
AbstractMesh,
|
||||||
|
LinesMesh,
|
||||||
|
MeshBuilder,
|
||||||
|
Observable,
|
||||||
|
Observer,
|
||||||
|
Scene,
|
||||||
|
TransformNode,
|
||||||
|
Vector3,
|
||||||
|
WebXRInputSource
|
||||||
|
} from "@babylonjs/core";
|
||||||
|
import {DefaultScene} from "../defaultScene";
|
||||||
|
import {DiagramEvent, DiagramEventType, DiagramTemplates} from "../diagram/types/diagramEntity";
|
||||||
|
import {DiagramEventObserverMask} from "../diagram/types/diagramEventObserverMask";
|
||||||
|
|
||||||
|
export class ConnectionPreview {
|
||||||
|
private _fromPoint: Vector3;
|
||||||
|
private _fromId: string;
|
||||||
|
private _renderObserver: Observer<Scene>;
|
||||||
|
private _line: LinesMesh;
|
||||||
|
private _parent: TransformNode;
|
||||||
|
private _transform: TransformNode;
|
||||||
|
private _options: any;
|
||||||
|
private _scene: Scene;
|
||||||
|
private _diagramEventObservable: Observable<DiagramEvent>;
|
||||||
|
|
||||||
|
constructor(fromId: string, input: WebXRInputSource, point: Vector3, diagramEventObservable: Observable<DiagramEvent>) {
|
||||||
|
this._scene = DefaultScene.Scene;
|
||||||
|
this._diagramEventObservable = diagramEventObservable;
|
||||||
|
const fromMesh = this._scene.getMeshById(fromId);
|
||||||
|
|
||||||
|
if (fromMesh) {
|
||||||
|
this._parent = input.pointer;
|
||||||
|
this._fromId = fromMesh.id;
|
||||||
|
this._transform = new TransformNode("transform", this._scene);
|
||||||
|
this._transform.position = point.clone();
|
||||||
|
this._transform.setParent(this._parent);
|
||||||
|
this._fromPoint = fromMesh.getAbsolutePosition();
|
||||||
|
this._options = {
|
||||||
|
points: [this._fromPoint, this._transform.absolutePosition],
|
||||||
|
updatable: true,
|
||||||
|
useAlphaForLines: false,
|
||||||
|
};
|
||||||
|
this._line = MeshBuilder.CreateLines("connectionPreview", this._options, this._scene);
|
||||||
|
this._options.instance = this._line;
|
||||||
|
this._renderObserver = this._scene.onBeforeRenderObservable.add(() => {
|
||||||
|
this._options.points[1] = this._transform.absolutePosition;
|
||||||
|
this._line = MeshBuilder.CreateLines("connectionPreview", this._options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose() {
|
||||||
|
this._scene.onBeforeRenderObservable.remove(this._renderObserver);
|
||||||
|
this._parent = null;
|
||||||
|
this._transform.dispose();
|
||||||
|
this._line.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public connect(mesh: AbstractMesh) {
|
||||||
|
if (mesh) {
|
||||||
|
this._diagramEventObservable.notifyObservers({
|
||||||
|
type: DiagramEventType.ADD,
|
||||||
|
entity: {
|
||||||
|
from: this._fromId,
|
||||||
|
to: mesh.id,
|
||||||
|
template: DiagramTemplates.CONNECTION,
|
||||||
|
color: '#000000'
|
||||||
|
}
|
||||||
|
}, DiagramEventObserverMask.ALL);
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user