Fixed up config and color change events.
This commit is contained in:
parent
658b65e216
commit
3954619d60
51
src/app.ts
51
src/app.ts
@ -17,24 +17,15 @@ import {GamepadManager} from "./controllers/gamepadManager";
|
||||
import {CustomEnvironment} from "./util/customEnvironment";
|
||||
import {Controllers} from "./controllers/controllers";
|
||||
import {Introduction} from "./tutorial/introduction";
|
||||
import {IndexdbPersistenceManager} from "./integration/indexdbPersistenceManager";
|
||||
|
||||
|
||||
export class App {
|
||||
//preTasks = [havokModule];
|
||||
constructor() {
|
||||
const config = AppConfig.config;
|
||||
const logger = log.getLogger('App');
|
||||
//log.enableAll(true);
|
||||
log.setDefaultLevel('info');
|
||||
|
||||
//log.getLogger('App').setLevel('info');
|
||||
//log.getLogger('IndexdbPersistenceManager').setLevel('info');
|
||||
//log.getLogger('DiagramManager').setLevel('info');
|
||||
//log.getLogger('DiagramConnection').setLevel('debug');
|
||||
//log.getLogger('DrawioManager').setLevel('warn');
|
||||
//log.getLogger('VoiceManager').setLevel('debug');
|
||||
//log.getLogger('EntityTree').setLevel('warn');
|
||||
//log.getLogger('EditMenu').setLevel('warn');
|
||||
const logger = log.getLogger('App');
|
||||
log.setDefaultLevel('debug');
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.style.width = "100%";
|
||||
canvas.style.height = "100%";
|
||||
@ -51,12 +42,23 @@ export class App {
|
||||
}
|
||||
|
||||
async initialize(canvas) {
|
||||
const config = AppConfig.config;
|
||||
|
||||
const logger = log.getLogger('App');
|
||||
const engine = new Engine(canvas, true);
|
||||
const scene = new Scene(engine);
|
||||
const environment = new CustomEnvironment(scene);
|
||||
|
||||
const persistenceManager = new IndexdbPersistenceManager("diagram");
|
||||
const controllers = new Controllers();
|
||||
const toolbox = new Toolbox(scene, controllers);
|
||||
const diagramManager = new DiagramManager(scene, controllers, toolbox);
|
||||
diagramManager.setPersistenceManager(persistenceManager);
|
||||
const config = new AppConfig(persistenceManager);
|
||||
const environment = new CustomEnvironment(scene, "default", config);
|
||||
persistenceManager.initialize().then(() => {
|
||||
if (!config.current?.demoCompleted) {
|
||||
const intro = new Introduction(scene, config);
|
||||
intro.start();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const camera: ArcRotateCamera = new ArcRotateCamera("Camera", -Math.PI / 2, Math.PI / 2, 4,
|
||||
@ -92,27 +94,14 @@ export class App {
|
||||
|
||||
}
|
||||
});
|
||||
const controllers = new Controllers();
|
||||
const diagramManager = new DiagramManager(scene, xr.baseExperience, controllers);
|
||||
|
||||
const rig = new Rigplatform(scene, xr, diagramManager, controllers);
|
||||
const toolbox = new Toolbox(scene, xr.baseExperience, diagramManager, controllers);
|
||||
|
||||
|
||||
//const dioManager = new DrawioManager(scene, diagramManager);
|
||||
import ('./integration/indexdbPersistenceManager').then((module) => {
|
||||
const persistenceManager = new module.IndexdbPersistenceManager("diagram");
|
||||
diagramManager.setPersistenceManager(persistenceManager);
|
||||
AppConfig.config.setPersistenceManager(persistenceManager);
|
||||
persistenceManager.initialize().then(() => {
|
||||
if (!AppConfig.config?.demoCompleted) {
|
||||
const intro = new Introduction(scene);
|
||||
intro.start();
|
||||
}
|
||||
});
|
||||
//const newRelicData = new NewRelicData(persistenceManager, scene);
|
||||
|
||||
|
||||
//const newRelicData = new NewRelicData(persistenceManager, scene);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -14,7 +14,6 @@ import {DiagramManager} from "../diagram/diagramManager";
|
||||
import {DiagramEvent, DiagramEventType} from "../diagram/diagramEntity";
|
||||
import log from "loglevel";
|
||||
import {Controllers} from "./controllers";
|
||||
import {AppConfig} from "../util/appConfig";
|
||||
|
||||
export class Base {
|
||||
static stickVector = Vector3.Zero();
|
||||
@ -217,8 +216,8 @@ export class Base {
|
||||
|
||||
this.reparent(mesh);
|
||||
if (!mesh.physicsBody) {
|
||||
mesh.position = AppConfig.config.snapGridVal(mesh.position);
|
||||
mesh.rotation = AppConfig.config.snapRotateVal(mesh.rotation);
|
||||
mesh.position = this.diagramManager.config.snapGridVal(mesh.position);
|
||||
mesh.rotation = this.diagramManager.config.snapRotateVal(mesh.rotation);
|
||||
}
|
||||
this.previousParentId = null;
|
||||
this.previousScaling = null;
|
||||
|
||||
@ -13,7 +13,7 @@ export class Left extends Base {
|
||||
WebXRInputSource, scene: Scene, xr: WebXRDefaultExperience, diagramManager: DiagramManager, controllers: Controllers) {
|
||||
|
||||
super(controller, scene, xr, controllers, diagramManager);
|
||||
this.configMenu = new ConfigMenu(this.scene, xr.baseExperience, this.controllers);
|
||||
this.configMenu = new ConfigMenu(this.scene, xr.baseExperience, this.controllers, this.diagramManager.config);
|
||||
this.controller.onMotionControllerInitObservable.add((init) => {
|
||||
if (init.components['xr-standard-thumbstick']) {
|
||||
init.components['xr-standard-thumbstick']
|
||||
|
||||
@ -20,7 +20,6 @@ import {EditMenu} from "../menus/editMenu";
|
||||
import {Controllers} from "./controllers";
|
||||
import log from "loglevel";
|
||||
import {DiagramManager} from "../diagram/diagramManager";
|
||||
import {AppConfig} from "../util/appConfig";
|
||||
|
||||
|
||||
export class Rigplatform {
|
||||
@ -62,9 +61,9 @@ export class Rigplatform {
|
||||
this.velocity.y = (val * this.velocityArray[this.velocityIndex])*-1;
|
||||
}
|
||||
public turn(val: number) {
|
||||
const snap = AppConfig.config.currentTurnSnap.value;
|
||||
const snap = this.diagramManager.config.current?.turnSnap;
|
||||
|
||||
if (snap > 0) {
|
||||
if (snap && snap > 0) {
|
||||
if (!this.turning) {
|
||||
if (Math.abs(val) > .1) {
|
||||
this.turning = true;
|
||||
@ -192,7 +191,9 @@ export class Rigplatform {
|
||||
}
|
||||
private fixRotation() {
|
||||
this.scene.onAfterPhysicsObservable.add(() => {
|
||||
if (AppConfig?.config?.currentTurnSnap?.value > 0) {
|
||||
const turnSnap = this.diagramManager.config.current?.turnSnap;
|
||||
|
||||
if (turnSnap && turnSnap > 0) {
|
||||
const q = this.rigMesh.rotationQuaternion;
|
||||
this.body.setAngularVelocity(Vector3.Zero());
|
||||
if (q) {
|
||||
|
||||
@ -11,7 +11,7 @@ import {
|
||||
PhysicsShapeType,
|
||||
PlaySoundAction,
|
||||
Scene,
|
||||
WebXRExperienceHelper
|
||||
Vector3
|
||||
} from "@babylonjs/core";
|
||||
import {DiagramEntity, DiagramEvent, DiagramEventType} from "./diagramEntity";
|
||||
import {IPersistenceManager} from "../integration/iPersistenceManager";
|
||||
@ -21,37 +21,21 @@ import {Controllers} from "../controllers/controllers";
|
||||
import {DiaSounds} from "../util/diaSounds";
|
||||
import {AppConfig} from "../util/appConfig";
|
||||
import {TextLabel} from "./textLabel";
|
||||
import {Toolbox} from "../toolbox/toolbox";
|
||||
|
||||
|
||||
export class DiagramManager {
|
||||
public readonly onDiagramEventObservable: Observable<DiagramEvent> = new Observable();
|
||||
private readonly logger = log.getLogger('DiagramManager');
|
||||
private persistenceManager: IPersistenceManager = null;
|
||||
private readonly toolbox: Toolbox;
|
||||
private readonly scene: Scene;
|
||||
private xr: WebXRExperienceHelper;
|
||||
private sounds: DiaSounds;
|
||||
|
||||
|
||||
public setPersistenceManager(persistenceManager: IPersistenceManager) {
|
||||
this.persistenceManager = persistenceManager;
|
||||
this.persistenceManager.updateObserver.add(this.onRemoteEvent, -1, true, this);
|
||||
}
|
||||
|
||||
private getPersistenceManager(): IPersistenceManager {
|
||||
if (!this.persistenceManager) {
|
||||
this.logger.warn("persistenceManager not set");
|
||||
return null;
|
||||
}
|
||||
return this.persistenceManager;
|
||||
}
|
||||
|
||||
private readonly actionManager: ActionManager;
|
||||
private config: AppConfig;
|
||||
private controllers: Controllers;
|
||||
|
||||
constructor(scene: Scene, xr: WebXRExperienceHelper, controllers: Controllers) {
|
||||
constructor(scene: Scene, controllers: Controllers, toolbox: Toolbox) {
|
||||
this.sounds = new DiaSounds(scene);
|
||||
this.scene = scene;
|
||||
this.xr = xr;
|
||||
this.toolbox = toolbox;
|
||||
this.controllers = controllers;
|
||||
this.actionManager = new ActionManager(this.scene);
|
||||
this.actionManager.registerAction(
|
||||
@ -86,6 +70,30 @@ export class DiagramManager {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _config: AppConfig;
|
||||
|
||||
private getPersistenceManager(): IPersistenceManager {
|
||||
if (!this.persistenceManager) {
|
||||
this.logger.warn("persistenceManager not set");
|
||||
return null;
|
||||
}
|
||||
return this.persistenceManager;
|
||||
}
|
||||
|
||||
private readonly actionManager: ActionManager;
|
||||
private controllers: Controllers;
|
||||
|
||||
public get config(): AppConfig {
|
||||
return this._config;
|
||||
}
|
||||
|
||||
public setPersistenceManager(persistenceManager: IPersistenceManager) {
|
||||
this.persistenceManager = persistenceManager;
|
||||
this._config = new AppConfig(persistenceManager);
|
||||
this.persistenceManager.updateObserver.add(this.onRemoteEvent, -1, true, this);
|
||||
}
|
||||
|
||||
public createCopy(mesh: AbstractMesh, copy: boolean = false): AbstractMesh {
|
||||
let newMesh;
|
||||
if (!mesh.isAnInstance) {
|
||||
@ -103,12 +111,23 @@ export class DiagramManager {
|
||||
if (copy) {
|
||||
newMesh.scaling = mesh.scaling.clone();
|
||||
} else {
|
||||
newMesh.scaling = AppConfig.config.createSnapVal;
|
||||
if (this.config.current?.createSnap) {
|
||||
newMesh.scaling.x = this.config.current?.createSnap;
|
||||
newMesh.scaling.y = this.config.current?.createSnap;
|
||||
newMesh.scaling.z = this.config.current?.createSnap;
|
||||
} else {
|
||||
newMesh.scaling = Vector3.One();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
newMesh.material = mesh.material;
|
||||
|
||||
newMesh.metadata = this.deepCopy(mesh.metadata);
|
||||
DiagramShapePhysics.applyPhysics(this.sounds, newMesh, this.scene);
|
||||
if (this.config.current?.physicsEnabled) {
|
||||
DiagramShapePhysics.applyPhysics(this.sounds, newMesh, this.scene);
|
||||
}
|
||||
|
||||
this.persistenceManager.add(newMesh);
|
||||
return newMesh;
|
||||
}
|
||||
@ -135,17 +154,17 @@ export class DiagramManager {
|
||||
const toolMesh = this.scene.getMeshById("tool-" + event.template + "-" + event.color);
|
||||
if (!toolMesh && (event.template != '#connection-template')) {
|
||||
log.debug('no mesh found for ' + event.template + "-" + event.color, 'adding it');
|
||||
this.onDiagramEventObservable.notifyObservers({
|
||||
type: DiagramEventType.CHANGECOLOR,
|
||||
entity: event
|
||||
});
|
||||
//this.getPersistenceManager()?.changeColor(null, Color3.FromHexString(event.color));
|
||||
this.toolbox.updateToolbox(event.color);
|
||||
}
|
||||
const mesh = MeshConverter.fromDiagramEntity(event, this.scene);
|
||||
mesh.actionManager = this.actionManager;
|
||||
if (event.parent) {
|
||||
mesh.parent = this.scene.getMeshById(event.parent);
|
||||
}
|
||||
DiagramShapePhysics.applyPhysics(this.sounds, mesh, this.scene, PhysicsMotionType.DYNAMIC);
|
||||
if (this.config.current?.physicsEnabled) {
|
||||
DiagramShapePhysics.applyPhysics(this.sounds, mesh, this.scene, PhysicsMotionType.DYNAMIC);
|
||||
}
|
||||
}
|
||||
|
||||
private onDiagramEvent(event: DiagramEvent) {
|
||||
@ -157,17 +176,17 @@ export class DiagramManager {
|
||||
}
|
||||
if (!mesh && event?.entity?.template) {
|
||||
const toolMesh = this.scene.getMeshById("tool-" + event.entity.template + "-" + event.entity.color);
|
||||
if (!toolMesh) {
|
||||
if (!toolMesh && event.type != DiagramEventType.CHANGECOLOR) {
|
||||
log.debug('no mesh found for ' + event.entity.template + "-" + event.entity.color, 'adding it');
|
||||
this.onDiagramEventObservable.notifyObservers({
|
||||
type: DiagramEventType.CHANGECOLOR,
|
||||
entity: event.entity
|
||||
});
|
||||
this.toolbox.updateToolbox(event.entity.color);
|
||||
}
|
||||
mesh = MeshConverter.fromDiagramEntity(event.entity, this.scene);
|
||||
if (mesh) {
|
||||
mesh.actionManager = this.actionManager;
|
||||
DiagramShapePhysics.applyPhysics(this.sounds, mesh, this.scene, PhysicsMotionType.DYNAMIC);
|
||||
if (this.config.current.physicsEnabled) {
|
||||
DiagramShapePhysics.applyPhysics(this.sounds, mesh, this.scene, PhysicsMotionType.DYNAMIC);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -189,13 +208,19 @@ export class DiagramManager {
|
||||
if (!mesh.actionManager) {
|
||||
mesh.actionManager = this.actionManager;
|
||||
}
|
||||
DiagramShapePhysics
|
||||
.applyPhysics(this.sounds, mesh, this.scene);
|
||||
if (this.config.current.physicsEnabled) {
|
||||
DiagramShapePhysics
|
||||
.applyPhysics(this.sounds, mesh, this.scene);
|
||||
}
|
||||
|
||||
break;
|
||||
case DiagramEventType.MODIFY:
|
||||
this.getPersistenceManager()?.modify(mesh);
|
||||
DiagramShapePhysics
|
||||
.applyPhysics(this.sounds, mesh, this.scene);
|
||||
if (this.config.current.physicsEnabled) {
|
||||
DiagramShapePhysics
|
||||
.applyPhysics(this.sounds, mesh, this.scene);
|
||||
}
|
||||
|
||||
break;
|
||||
case DiagramEventType.CHANGECOLOR:
|
||||
if (!event.oldColor) {
|
||||
@ -234,9 +259,6 @@ class DiagramShapePhysics {
|
||||
private static logger: log.Logger = log.getLogger('DiagramShapePhysics');
|
||||
|
||||
public static applyPhysics(sounds: DiaSounds, mesh: AbstractMesh, scene: Scene, motionType?: PhysicsMotionType) {
|
||||
if (!AppConfig.config.physicsEnabled) {
|
||||
return;
|
||||
}
|
||||
if (!mesh?.metadata?.template) {
|
||||
this.logger.error("applyPhysics: mesh.metadata.template is null", mesh);
|
||||
return;
|
||||
|
||||
@ -43,6 +43,8 @@ export interface IPersistenceManager {
|
||||
|
||||
setConfig(config: AppConfigType);
|
||||
|
||||
getConfig(): Promise<AppConfigType>;
|
||||
|
||||
modifyDiagram(diagram: DiagramListing);
|
||||
|
||||
updateObserver: Observable<DiagramEntity>;
|
||||
|
||||
@ -95,6 +95,12 @@ export class IndexdbPersistenceManager implements IPersistenceManager {
|
||||
return this.db["newRelicData"].toArray();
|
||||
}
|
||||
|
||||
public async getConfig(): Promise<AppConfigType> {
|
||||
const configs = await this.db['config'].toArray();
|
||||
const config = configs[0];
|
||||
return config;
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
this.logger.info('initialize', this.db['entities'].length);
|
||||
const configs = await this.db['config'].toArray();
|
||||
|
||||
@ -12,19 +12,14 @@ export class ConfigMenu extends BaseMenu {
|
||||
private configPlane: AbstractMesh = null;
|
||||
|
||||
private yObserver;
|
||||
|
||||
constructor(scene: Scene, xr: WebXRExperienceHelper, controllers: Controllers) {
|
||||
super(scene, xr, controllers);
|
||||
this.sounds = new DiaSounds(scene);
|
||||
if (!this.yObserver) {
|
||||
this.controllers.controllerObserver.add((event) => {
|
||||
if (event.type == 'y-button') {
|
||||
this.toggle();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
private config: AppConfig;
|
||||
private gridSnaps: Array<{ label: string, value: number }> = [
|
||||
{label: "Off", value: 0},
|
||||
{label: "0.01", value: 0.01},
|
||||
{label: "0.1", value: 0.1},
|
||||
{label: "0.5", value: 0.5},
|
||||
{label: "1", value: 1},
|
||||
]
|
||||
|
||||
public toggle() {
|
||||
if (this.configPlane) {
|
||||
@ -55,19 +50,35 @@ export class ConfigMenu extends BaseMenu {
|
||||
|
||||
CameraHelper.setMenuPosition(this.configPlane, this.scene);
|
||||
}
|
||||
private rotationSnaps: Array<{ label: string, value: number }> = [
|
||||
{label: "Off", value: 0},
|
||||
{label: "22.5", value: 22.5},
|
||||
{label: "45", value: 45},
|
||||
{label: "90", value: 90},
|
||||
|
||||
]
|
||||
|
||||
constructor(scene: Scene, xr: WebXRExperienceHelper, controllers: Controllers, config: AppConfig) {
|
||||
super(scene, xr, controllers);
|
||||
this.config = config;
|
||||
this.sounds = new DiaSounds(scene);
|
||||
if (!this.yObserver) {
|
||||
this.controllers.controllerObserver.add((event) => {
|
||||
if (event.type == 'y-button') {
|
||||
this.toggle();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private createVal(value) {
|
||||
AppConfig.config.currentCreateSnapIndex = value;
|
||||
log.debug("configMenu", "create Snap", value);
|
||||
}
|
||||
|
||||
private buildCreateScaleControl(selectionPanel: SelectionPanel): RadioGroup {
|
||||
const radio = new RadioGroup("Create Scale");
|
||||
selectionPanel.addGroup(radio);
|
||||
|
||||
for (const [index, snap] of AppConfig.config.createSnaps().entries()) {
|
||||
const selected = AppConfig.config.currentCreateSnapIndex == index;
|
||||
radio.addRadio(snap.label, this.createVal, selected);
|
||||
for (const [index, snap] of this.gridSnaps.entries()) {
|
||||
const selected = this.config.current.createSnap == snap.value
|
||||
radio.addRadio(snap.label, this.createVal.bind(this), selected);
|
||||
}
|
||||
return radio;
|
||||
}
|
||||
@ -75,9 +86,9 @@ export class ConfigMenu extends BaseMenu {
|
||||
private buildRotationSnapControl(selectionPanel: SelectionPanel): RadioGroup {
|
||||
const radio = new RadioGroup("Rotation Snap");
|
||||
selectionPanel.addGroup(radio);
|
||||
for (const [index, snap] of AppConfig.config.rotateSnaps().entries()) {
|
||||
const selected = AppConfig.config.currentRotateSnapIndex == index;
|
||||
radio.addRadio(snap.label, this.rotateVal, selected);
|
||||
for (const [index, snap] of this.rotationSnaps.entries()) {
|
||||
const selected = this.config.current.rotateSnap == snap.value
|
||||
radio.addRadio(snap.label, this.rotateVal.bind(this), selected);
|
||||
}
|
||||
return radio;
|
||||
}
|
||||
@ -85,9 +96,9 @@ export class ConfigMenu extends BaseMenu {
|
||||
private buildGridSizeControl(selectionPanel: SelectionPanel): RadioGroup {
|
||||
const radio = new RadioGroup("Grid Snap");
|
||||
selectionPanel.addGroup(radio);
|
||||
for (const [index, snap] of AppConfig.config.gridSnaps().entries()) {
|
||||
const selected = AppConfig.config.currentGridSnapIndex == index;
|
||||
radio.addRadio(snap.label, this.gridVal, selected);
|
||||
for (const [index, snap] of this.gridSnaps.entries()) {
|
||||
const selected = this.config.current.gridSnap == snap.value;
|
||||
radio.addRadio(snap.label, this.gridVal.bind(this), selected);
|
||||
}
|
||||
return radio;
|
||||
}
|
||||
@ -95,25 +106,39 @@ export class ConfigMenu extends BaseMenu {
|
||||
private buildTurnSnapControl(selectionPanel: SelectionPanel): RadioGroup {
|
||||
const radio = new RadioGroup("Turn Snap");
|
||||
selectionPanel.addGroup(radio);
|
||||
for (const [index, snap] of AppConfig.config.turnSnaps().entries()) {
|
||||
const selected = AppConfig.config.currentTurnSnapIndex == index;
|
||||
radio.addRadio(snap.label, this.turnVal, selected);
|
||||
for (const [index, snap] of this.rotationSnaps.entries()) {
|
||||
const selected = this.config.current.rotateSnap == snap.value;
|
||||
radio.addRadio(snap.label, this.turnVal.bind(this), selected);
|
||||
}
|
||||
return radio;
|
||||
}
|
||||
|
||||
private createVal(value) {
|
||||
const config = this.config.current;
|
||||
config.createSnap = this.gridSnaps[value].value;
|
||||
this.config.current = config;
|
||||
|
||||
log.debug("configMenu", "create Snap", value);
|
||||
}
|
||||
|
||||
private rotateVal(value) {
|
||||
AppConfig.config.currentRotateSnapIndex = value;
|
||||
const config = this.config.current;
|
||||
config.rotateSnap = this.rotationSnaps[value].value;
|
||||
this.config.current = config;
|
||||
log.debug("configMenu", "rotate Snap", value);
|
||||
}
|
||||
|
||||
private turnVal(value) {
|
||||
AppConfig.config.currentTurnSnapIndex = value;
|
||||
const config = this.config.current;
|
||||
config.turnSnap = this.rotationSnaps[value].value;
|
||||
this.config.current = config;
|
||||
log.debug("configMenu", "turn Snap", value);
|
||||
}
|
||||
|
||||
private gridVal(value) {
|
||||
AppConfig.config.currentGridSnapIndex = value;
|
||||
const config = this.config.current;
|
||||
config.gridSnap = this.gridSnaps[value].value;
|
||||
this.config.current = config;
|
||||
log.debug("configMenu", "grid Snap", value);
|
||||
}
|
||||
|
||||
|
||||
@ -23,7 +23,6 @@ import {CameraHelper} from "../util/cameraHelper";
|
||||
import {TextLabel} from "../diagram/textLabel";
|
||||
import {DiagramConnection} from "../diagram/diagramConnection";
|
||||
import {GLTF2Export} from "@babylonjs/serializers";
|
||||
import {AppConfig} from "../util/appConfig";
|
||||
|
||||
export class EditMenu {
|
||||
private state: EditMenuState = EditMenuState.NONE;
|
||||
@ -263,11 +262,15 @@ export class EditMenu {
|
||||
inputTextView.show();
|
||||
inputTextView.onTextObservable.addOnce((value) => {
|
||||
console.log(value.text);
|
||||
AppConfig.config.newRelicKey = value.text;
|
||||
const config = this.diagramManager.config.current;
|
||||
config.newRelicKey = value.text;
|
||||
this.diagramManager.config.current = config;
|
||||
inputTextView.show();
|
||||
inputTextView.onTextObservable.addOnce((value) => {
|
||||
console.log(value.text);
|
||||
AppConfig.config.newRelicAccount = value.text;
|
||||
const config = this.diagramManager.config.current;
|
||||
config.newRelicAccount = value.text;
|
||||
this.diagramManager.config.current = config;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -7,14 +7,11 @@ import {
|
||||
Scene,
|
||||
StandardMaterial,
|
||||
TransformNode,
|
||||
Vector3,
|
||||
WebXRExperienceHelper
|
||||
Vector3
|
||||
} from "@babylonjs/core";
|
||||
|
||||
import {CameraHelper} from "../util/cameraHelper";
|
||||
import {AdvancedDynamicTexture, Button3D, ColorPicker, GUI3DManager, StackPanel3D, TextBlock} from "@babylonjs/gui";
|
||||
import {DiagramManager} from "../diagram/diagramManager";
|
||||
import {DiagramEventType} from "../diagram/diagramEntity";
|
||||
import {Controllers} from "../controllers/controllers";
|
||||
|
||||
export enum ToolType {
|
||||
@ -30,24 +27,16 @@ export class Toolbox {
|
||||
private index = 0;
|
||||
public static instance: Toolbox;
|
||||
private readonly scene: Scene;
|
||||
private readonly xr: WebXRExperienceHelper;
|
||||
public readonly node: TransformNode;
|
||||
private readonly diagramManager: DiagramManager;
|
||||
private readonly manager: GUI3DManager;
|
||||
private readonly gridsize = 5;
|
||||
private readonly addPanel: StackPanel3D;
|
||||
private readonly controllers: Controllers;
|
||||
private xObserver;
|
||||
|
||||
constructor(scene: Scene, xr: WebXRExperienceHelper, diagramManager: DiagramManager, controllers: Controllers) {
|
||||
constructor(scene: Scene, controllers: Controllers) {
|
||||
this.scene = scene;
|
||||
this.controllers = controllers;
|
||||
this.diagramManager = diagramManager;
|
||||
this.diagramManager.onDiagramEventObservable.add((evt) => {
|
||||
if (evt?.entity?.color && evt.type == DiagramEventType.CHANGECOLOR) {
|
||||
this.updateToolbox(evt.entity.color);
|
||||
}
|
||||
}, -1, true, this);
|
||||
this.addPanel = new StackPanel3D();
|
||||
this.manager = new GUI3DManager(scene);
|
||||
this.manager.addControl(this.addPanel);
|
||||
@ -65,12 +54,8 @@ export class Toolbox {
|
||||
handle.position = Vector3.Zero();
|
||||
|
||||
this.node.parent = handle;
|
||||
this.xr = xr;
|
||||
if (!this.scene.activeCamera) {
|
||||
return;
|
||||
} else {
|
||||
this.buildToolbox();
|
||||
}
|
||||
|
||||
this.buildToolbox();
|
||||
|
||||
Toolbox.instance = this;
|
||||
if (!this.xObserver) {
|
||||
@ -227,13 +212,6 @@ export class Toolbox {
|
||||
material.name = "material-" + newColorHex;
|
||||
mesh.id = "toolbox-color-" + newColorHex;
|
||||
mesh.name = "toolbox-color-" + newColorHex;
|
||||
this.diagramManager.onDiagramEventObservable.notifyObservers(
|
||||
{
|
||||
type: DiagramEventType.CHANGECOLOR,
|
||||
oldColor: oldColor,
|
||||
newColor: newColor
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
colorPickerTexture.addControl(colorPicker);
|
||||
|
||||
@ -26,13 +26,16 @@ export class Introduction {
|
||||
private items: AbstractMesh[] = [];
|
||||
private advance: Button3D;
|
||||
private sounds: DiaSounds;
|
||||
private config: AppConfig;
|
||||
|
||||
constructor(scene: Scene) {
|
||||
constructor(scene: Scene, config: AppConfig) {
|
||||
this.sounds = new DiaSounds(scene);
|
||||
this.scene = scene;
|
||||
this.config = config;
|
||||
this.manager = new GUI3DManager(scene);
|
||||
this.physicsHelper = new PhysicsHelper(scene);
|
||||
}
|
||||
|
||||
public start() {
|
||||
this.scene.physicsEnabled = true;
|
||||
this.advance = new Button3D("advance");
|
||||
@ -167,7 +170,9 @@ export class Introduction {
|
||||
});
|
||||
this.advance.dispose();
|
||||
this.manager.dispose();
|
||||
AppConfig.config.demoCompleted = true;
|
||||
const config = this.config.current;
|
||||
config.demoCompleted = true;
|
||||
this.config.current = config;
|
||||
this.items = [];
|
||||
}
|
||||
this.step++;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import {Angle, Vector3} from "@babylonjs/core";
|
||||
import {Angle, Observable, Vector3} from "@babylonjs/core";
|
||||
import log from "loglevel";
|
||||
import round from "round";
|
||||
import {IPersistenceManager} from "../integration/iPersistenceManager";
|
||||
@ -9,244 +9,84 @@ export type SnapValue = {
|
||||
label: string
|
||||
}
|
||||
|
||||
|
||||
export class AppConfig {
|
||||
private readonly logger = log.getLogger('AppConfig');
|
||||
private _demoCompleted = false;
|
||||
private gridSnap = 1;
|
||||
private _turnSnap = 0;
|
||||
private rotateSnap = 0;
|
||||
private createSnap = 0;
|
||||
private _newRelicKey: string = null;
|
||||
public readonly onConfigChangedObservable = new Observable<AppConfigType>();
|
||||
private _currentConfig: AppConfigType;
|
||||
private persistenceManager: IPersistenceManager;
|
||||
|
||||
public get newRelicKey(): string {
|
||||
return this._newRelicKey;
|
||||
}
|
||||
|
||||
_physicsEnabled = false;
|
||||
private readonly defaultGridSnapIndex = 1;
|
||||
private persistenceManager: IPersistenceManager = null;
|
||||
private gridSnapArray: SnapValue[] =
|
||||
[{value: 0, label: "Off"},
|
||||
{value: 0.05, label: "(Default)"},
|
||||
{value: 0.01, label: "1 cm"},
|
||||
{value: 0.1, label: "10 cm"},
|
||||
{value: 0.25, label: "25 cm"},
|
||||
{value: .5, label: ".5 m"}]
|
||||
private createSnapArray: SnapValue[] =
|
||||
[{value: .1, label: "Default (10 cm)"},
|
||||
{value: 0.2, label: "20 cm"},
|
||||
{value: 0.5, label: ".5 m"},
|
||||
{value: 1, label: "1 m"}];
|
||||
private rotateSnapArray: SnapValue[] =
|
||||
[{value: 0, label: "Off"},
|
||||
{value: 22.5, label: "22.5 Degrees"},
|
||||
{value: 45, label: "45 Degrees"},
|
||||
{value: 90, label: "90 Degrees"}];
|
||||
private turnSnapArray: SnapValue[] =
|
||||
[{value: 0, label: "Off"},
|
||||
{value: 22.5, label: "22.5 Degrees"},
|
||||
{value: 45, label: "45 Degrees"},
|
||||
{value: 90, label: "90 Degrees"}];
|
||||
|
||||
public get currentGridSnap(): SnapValue {
|
||||
return this.gridSnapArray[this.gridSnap || 0];
|
||||
}
|
||||
|
||||
public get demoCompleted(): boolean {
|
||||
return this._demoCompleted || false;
|
||||
}
|
||||
|
||||
public set demoCompleted(val: boolean) {
|
||||
this._demoCompleted = val;
|
||||
this.save();
|
||||
}
|
||||
|
||||
public set newRelicKey(val: string) {
|
||||
this._newRelicKey = val;
|
||||
this.save();
|
||||
}
|
||||
|
||||
private _newRelicAccount: string = null;
|
||||
|
||||
public get newRelicAccount(): string {
|
||||
return this._newRelicAccount;
|
||||
}
|
||||
|
||||
public set newRelicAccount(val: string) {
|
||||
this._newRelicAccount = val;
|
||||
this.save();
|
||||
}
|
||||
|
||||
public get physicsEnabled(): boolean {
|
||||
return this._physicsEnabled || false;
|
||||
}
|
||||
|
||||
public set phsyicsEnabled(val: boolean) {
|
||||
this._physicsEnabled = val;
|
||||
this.save();
|
||||
}
|
||||
|
||||
private static _config: AppConfig;
|
||||
|
||||
public static get config() {
|
||||
if (!AppConfig._config) {
|
||||
AppConfig._config = new AppConfig();
|
||||
}
|
||||
return AppConfig._config;
|
||||
}
|
||||
|
||||
public get currentRotateSnap(): SnapValue {
|
||||
return this.rotateSnapArray[this.rotateSnap || 0];
|
||||
}
|
||||
|
||||
public get currentCreateSnap(): SnapValue {
|
||||
return this.createSnapArray[this.createSnap || 0];
|
||||
}
|
||||
|
||||
public get currentTurnSnap(): SnapValue {
|
||||
return this.turnSnapArray[this._turnSnap || 0];
|
||||
}
|
||||
|
||||
public get currentGridSnapIndex(): number {
|
||||
return this.gridSnap || 0;
|
||||
}
|
||||
|
||||
public set currentTurnSnapIndex(val: number) {
|
||||
this._turnSnap = val;
|
||||
this.save();
|
||||
}
|
||||
|
||||
public set currentGridSnapIndex(val: number) {
|
||||
this.gridSnap = val;
|
||||
this.save();
|
||||
}
|
||||
|
||||
public get currentCreateSnapIndex(): number {
|
||||
return this.createSnap || 0;
|
||||
}
|
||||
|
||||
public set currentCreateSnapIndex(val: number) {
|
||||
this.createSnap = val;
|
||||
if (this.currentGridSnapIndex == this.defaultGridSnapIndex) {
|
||||
this.currentGridSnap.value = this.currentCreateSnap.value / 2;
|
||||
this.logger.debug("Set grid snap to " + this.currentGridSnap.value);
|
||||
}
|
||||
this.save();
|
||||
}
|
||||
|
||||
public get currentRotateSnapIndex(): number {
|
||||
return this.rotateSnap || 0;
|
||||
}
|
||||
|
||||
public set currentRotateSnapIndex(val: number) {
|
||||
this.rotateSnap = val;
|
||||
this.save();
|
||||
}
|
||||
|
||||
public get createSnapVal(): Vector3 {
|
||||
return new Vector3(this.currentCreateSnap.value, this.currentCreateSnap.value, this.currentCreateSnap.value);
|
||||
}
|
||||
|
||||
public setPersistenceManager(persistenceManager: IPersistenceManager) {
|
||||
constructor(persistenceManager: IPersistenceManager) {
|
||||
this.persistenceManager = persistenceManager;
|
||||
this.persistenceManager.configObserver.add(this.configObserver, -1, false, this);
|
||||
this.persistenceManager.configObserver.add(this.load, -1, false, this, false);
|
||||
}
|
||||
|
||||
public gridSnaps(): SnapValue[] {
|
||||
return this.gridSnapArray;
|
||||
public get current(): AppConfigType {
|
||||
if (!this._currentConfig) {
|
||||
this.persistenceManager.getConfig().then((config) => {
|
||||
if (!config) {
|
||||
const newconfig = {
|
||||
id: 1,
|
||||
gridSnap: .1,
|
||||
rotateSnap: 45,
|
||||
createSnap: .1,
|
||||
turnSnap: 22.5,
|
||||
newRelicKey: null,
|
||||
newRelicAccount: null,
|
||||
physicsEnabled: false,
|
||||
demoCompleted: false,
|
||||
}
|
||||
this._currentConfig = newconfig;
|
||||
this.save();
|
||||
} else {
|
||||
this._currentConfig = config;
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
return this._currentConfig;
|
||||
}
|
||||
|
||||
public turnSnaps(): SnapValue[] {
|
||||
return this.turnSnapArray;
|
||||
public set current(config: AppConfigType) {
|
||||
this._currentConfig = config;
|
||||
this.save();
|
||||
}
|
||||
|
||||
public createSnaps(): SnapValue[] {
|
||||
return this.createSnapArray;
|
||||
public save() {
|
||||
this.persistenceManager.setConfig(this._currentConfig);
|
||||
}
|
||||
|
||||
public rotateSnaps(): SnapValue[] {
|
||||
return this.rotateSnapArray;
|
||||
public load(config: AppConfigType) {
|
||||
this._currentConfig = config;
|
||||
this.onConfigChangedObservable.notifyObservers(this._currentConfig);
|
||||
}
|
||||
|
||||
public snapGridVal(value: Vector3): Vector3 {
|
||||
if (this.currentGridSnapIndex == 0) {
|
||||
public snapGridVal(value: Vector3, snap: number): Vector3 {
|
||||
if (!snap) {
|
||||
return value;
|
||||
}
|
||||
const position = value.clone();
|
||||
position.x = round(position.x, this.currentGridSnap.value);
|
||||
position.y = round(position.y, this.currentGridSnap.value);
|
||||
position.z = round(position.z, this.currentGridSnap.value);
|
||||
position.x = round(value.x, snap);
|
||||
position.y = round(value.y, snap);
|
||||
position.z = round(value.z, snap);
|
||||
return position;
|
||||
}
|
||||
|
||||
public snapRotateVal(value: Vector3): Vector3 {
|
||||
if (this.currentRotateSnapIndex == 0) {
|
||||
public snapRotateVal(value: Vector3, snap: number): Vector3 {
|
||||
if (!snap) {
|
||||
return value;
|
||||
}
|
||||
const rotation = new Vector3();
|
||||
rotation.x = this.snapAngle(value.x);
|
||||
rotation.y = this.snapAngle(value.y);
|
||||
rotation.z = this.snapAngle(value.z);
|
||||
rotation.x = this.snapAngle(value.x, snap);
|
||||
rotation.y = this.snapAngle(value.y, snap);
|
||||
rotation.z = this.snapAngle(value.z, snap);
|
||||
return rotation;
|
||||
}
|
||||
|
||||
private snapAngle(val: number): number {
|
||||
private snapAngle(val: number, snap: number): number {
|
||||
const angle = snap;
|
||||
const deg = Angle.FromRadians(val).degrees();
|
||||
const snappedDegrees = round(deg, this.currentRotateSnap.value);
|
||||
this.logger.debug("deg", val, deg, snappedDegrees, this.currentRotateSnap.value);
|
||||
const snappedDegrees = round(deg, angle);
|
||||
this.logger.debug("deg", val, deg, snappedDegrees, angle);
|
||||
return Angle.FromDegrees(snappedDegrees).radians();
|
||||
}
|
||||
|
||||
private save() {
|
||||
this.persistenceManager.setConfig(
|
||||
{
|
||||
gridSnap: this.currentGridSnap.value,
|
||||
rotateSnap: this.currentRotateSnap.value,
|
||||
createSnap: this.currentCreateSnap.value,
|
||||
turnSnap: this.currentTurnSnap.value,
|
||||
physicsEnabled: this._physicsEnabled,
|
||||
newRelicKey: this._newRelicKey,
|
||||
newRelicAccount: this._newRelicAccount,
|
||||
demoCompleted: this._demoCompleted
|
||||
});
|
||||
}
|
||||
|
||||
private configObserver(config: AppConfigType) {
|
||||
if (config) {
|
||||
if (config.physicsEnabled && config.physicsEnabled != this._physicsEnabled) {
|
||||
this._physicsEnabled = config.physicsEnabled;
|
||||
this.logger.debug("Physics enabled changed to " + this._physicsEnabled);
|
||||
}
|
||||
if (config.demoCompleted) {
|
||||
this._demoCompleted = config.demoCompleted;
|
||||
}
|
||||
if (config.createSnap != this.currentCreateSnap.value ||
|
||||
config.gridSnap != this.currentGridSnap.value ||
|
||||
config.rotateSnap != this.currentRotateSnap.value) {
|
||||
this.logger.debug("Config changed", config);
|
||||
this._turnSnap = this.turnSnapArray.findIndex((snap) => snap.value == config.turnSnap);
|
||||
if (!this._turnSnap || this._turnSnap == -1) {
|
||||
this._turnSnap = 0;
|
||||
}
|
||||
this.rotateSnap = this.rotateSnapArray.findIndex((snap) => snap.value == config.rotateSnap);
|
||||
if (!this.rotateSnap || this.rotateSnap == -1) {
|
||||
this.rotateSnap = 0;
|
||||
}
|
||||
this.createSnap = this.createSnapArray.findIndex((snap) => snap.value == config.createSnap);
|
||||
if (!this.createSnap || this.createSnap == -1) {
|
||||
this.createSnap = 0;
|
||||
}
|
||||
const gridSnap = this.gridSnapArray.findIndex((snap) => snap.value == config.gridSnap);
|
||||
if (gridSnap == -1) {
|
||||
this.gridSnap = this.defaultGridSnapIndex;
|
||||
this.currentGridSnap.value = config.gridSnap;
|
||||
}
|
||||
} else {
|
||||
this.logger.debug("Config unchanged", config);
|
||||
}
|
||||
} else {
|
||||
this.logger.debug("Config not set");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,21 +7,24 @@ import {
|
||||
PhysicsAggregate,
|
||||
PhysicsShapeType,
|
||||
Scene,
|
||||
Texture
|
||||
Sound,
|
||||
Texture,
|
||||
Vector3
|
||||
} from "@babylonjs/core";
|
||||
import {CustomPhysics} from "./customPhysics";
|
||||
import {DiaSounds} from "./diaSounds";
|
||||
import {AppConfig} from "./appConfig";
|
||||
|
||||
|
||||
export class CustomEnvironment {
|
||||
private readonly scene: Scene;
|
||||
private readonly name: string;
|
||||
private readonly _groundMeshObservable: Observable<GroundMesh> = new Observable<GroundMesh>();
|
||||
|
||||
constructor(scene: Scene, name: string = "default") {
|
||||
constructor(scene: Scene, name: string = "default", config: AppConfig) {
|
||||
this.scene = scene;
|
||||
this.name = name;
|
||||
|
||||
const physics = new CustomPhysics(this.scene);
|
||||
const physics = new CustomPhysics(this.scene, config);
|
||||
physics
|
||||
.initializeAsync()
|
||||
.then(() => {
|
||||
@ -32,7 +35,29 @@ export class CustomEnvironment {
|
||||
'/assets/textures/outdoor_field2.jpeg', {},
|
||||
scene);
|
||||
try {
|
||||
new DiaSounds(scene);
|
||||
const sounds = new DiaSounds(scene);
|
||||
window.setTimeout((sound) => {
|
||||
sound.play()
|
||||
}, 2000, sounds.background);
|
||||
const birds: Array<Sound> = [sounds.birds, sounds.dove];
|
||||
window.setInterval((sounds: Array<Sound>) => {
|
||||
if (Math.random() < .6) {
|
||||
return;
|
||||
}
|
||||
const sound = Math.floor(Math.random() * sounds.length);
|
||||
const x = Math.floor(Math.random() * 20);
|
||||
const z = Math.floor(Math.random() * 20);
|
||||
|
||||
const position = new Vector3(x, 0, z);
|
||||
if (sounds[sound].isPlaying) {
|
||||
|
||||
} else {
|
||||
sounds[sound].setPosition(position);
|
||||
sounds[sound].setVolume(Math.random() * .5);
|
||||
sounds[sound].play();
|
||||
}
|
||||
|
||||
}, 2000, birds);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
@ -4,9 +4,11 @@ import {AppConfig} from "./appConfig";
|
||||
|
||||
export class CustomPhysics {
|
||||
private scene: Scene;
|
||||
private config: AppConfig;
|
||||
|
||||
constructor(scene: Scene) {
|
||||
constructor(scene: Scene, config: AppConfig) {
|
||||
this.scene = scene;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public async initializeAsync() {
|
||||
@ -24,10 +26,10 @@ export class CustomPhysics {
|
||||
if (true) {
|
||||
body.disablePreStep = false;
|
||||
const pos: Vector3 = body.getObjectCenterWorld();
|
||||
const val: Vector3 = AppConfig.config.snapGridVal(pos);
|
||||
const val: Vector3 = this.config.snapGridVal(pos);
|
||||
body.transformNode.position.set(val.x, val.y, val.z);
|
||||
const rot: Quaternion =
|
||||
Quaternion.FromEulerVector(AppConfig.config.snapRotateVal(body.transformNode.rotationQuaternion.toEulerAngles()))
|
||||
Quaternion.FromEulerVector(this.config.snapRotateVal(body.transformNode.rotationQuaternion.toEulerAngles()))
|
||||
|
||||
body.transformNode.rotationQuaternion.set(
|
||||
rot.x, rot.y, rot.z, rot.w
|
||||
|
||||
@ -8,32 +8,16 @@ export class DiaSounds {
|
||||
public get tick() {
|
||||
return new Sound("tick", '/assets/sounds/tick.mp3', this.scene);
|
||||
}
|
||||
|
||||
private volume: number = 0.8;
|
||||
private readonly _bounce;
|
||||
private readonly _bounce: Sound;
|
||||
private readonly _background: Sound;
|
||||
private readonly _enter: Sound;
|
||||
|
||||
public get enter() {
|
||||
return this._enter;
|
||||
}
|
||||
|
||||
private readonly _exit: Sound;
|
||||
|
||||
public get exit() {
|
||||
return this._exit;
|
||||
}
|
||||
|
||||
private readonly _high: Sound;
|
||||
|
||||
public get high() {
|
||||
return this._high;
|
||||
}
|
||||
|
||||
private readonly _low: Sound;
|
||||
|
||||
public get low() {
|
||||
return this._low;
|
||||
}
|
||||
|
||||
constructor(scene: Scene) {
|
||||
this.scene = scene;
|
||||
this._enter = new Sound("enter", "/assets/sounds/sounds.mp3", this.scene, null, {
|
||||
@ -72,15 +56,62 @@ export class DiaSounds {
|
||||
offset: 0,
|
||||
length: 0.990
|
||||
});
|
||||
|
||||
|
||||
this._birds = new Sound("birds", "/assets/sounds/birds.mp3", this.scene, null, {
|
||||
autoplay: true,
|
||||
this._background = new Sound("brown", "/assets/sounds/brown.mp3", this.scene, null, {
|
||||
autoplay: false,
|
||||
volume: 1,
|
||||
loop: true
|
||||
});
|
||||
this._birds = new Sound("warbler", "/assets/sounds/warbler.mp3", this.scene, null, {
|
||||
spatialSound: true,
|
||||
autoplay: false,
|
||||
volume: .5,
|
||||
loop: false
|
||||
});
|
||||
this.birds.switchPanningModelToHRTF();
|
||||
this.birds.maxDistance = 40;
|
||||
this._dove = new Sound("dove", "/assets/sounds/dove.mp3", this.scene, null, {
|
||||
spatialSound: true,
|
||||
autoplay: false,
|
||||
volume: .5,
|
||||
loop: false
|
||||
});
|
||||
this._dove.switchPanningModelToHRTF();
|
||||
this._dove.maxDistance = 40;
|
||||
|
||||
//this._enter.autoplay = true;
|
||||
}
|
||||
|
||||
public get background(): Sound {
|
||||
return this._background;
|
||||
}
|
||||
|
||||
private readonly _exit: Sound;
|
||||
|
||||
public get exit() {
|
||||
return this._exit;
|
||||
}
|
||||
|
||||
private readonly _high: Sound;
|
||||
|
||||
public get high() {
|
||||
return this._high;
|
||||
}
|
||||
|
||||
private readonly _low: Sound;
|
||||
|
||||
public get low() {
|
||||
return this._low;
|
||||
}
|
||||
|
||||
public get birds(): Sound {
|
||||
return this._birds;
|
||||
}
|
||||
|
||||
_dove: Sound;
|
||||
public get dove() {
|
||||
return this._dove;
|
||||
}
|
||||
|
||||
public get bounce() {
|
||||
const bounce = this._bounce.clone();
|
||||
bounce.updateOptions({offset: 0, volume: this.volume, length: .990});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user