mirror of
https://github.com/AleziaKurdis/overte.git
synced 2025-04-07 13:12:39 +02:00
Merge branch 'master' of https://github.com/highfidelity/hifi into team-teaching
This commit is contained in:
commit
807cc3db24
14 changed files with 762 additions and 137 deletions
66
examples/example/soundToys.js
Normal file
66
examples/example/soundToys.js
Normal file
|
@ -0,0 +1,66 @@
|
|||
"use strict";
|
||||
// Creates some objects that each play a sound when they are hit (or when they hit something else).
|
||||
//
|
||||
// Created by Howard Stearns on June 3, 2015
|
||||
// Copyright 2015 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
var Camera, Vec3, Quat, Entities, Script; // Globals defined by HiFi, var'ed here to keep jslint happy.
|
||||
var HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/";
|
||||
var SOUND_BUCKET = "http://public.highfidelity.io/sounds/Collisions-hitsandslaps/";
|
||||
var MAX_ANGULAR_SPEED = Math.PI;
|
||||
var N_EACH_OBJECTS = 3;
|
||||
|
||||
var ourToys = [];
|
||||
function deleteAll() {
|
||||
ourToys.forEach(Entities.deleteEntity);
|
||||
}
|
||||
function makeAll() {
|
||||
var currentPosition = Vec3.sum(Camera.getPosition(), Vec3.multiply(4, Quat.getFront(Camera.getOrientation()))),
|
||||
right = Vec3.multiply(0.6, Quat.getRight(Camera.getOrientation())),
|
||||
currentDimensions,
|
||||
data = [
|
||||
["models/props/Dice/goldDie.fbx", HIFI_PUBLIC_BUCKET + "sounds/dice/diceCollide.wav"],
|
||||
["models/props/Pool/ball_8.fbx", HIFI_PUBLIC_BUCKET + "sounds/Collisions-ballhitsandcatches/billiards/collision1.wav"],
|
||||
["eric/models/woodFloor.fbx", SOUND_BUCKET + "67LCollision05.wav"]
|
||||
];
|
||||
currentPosition = Vec3.sum(currentPosition, Vec3.multiply(-1 * data.length * N_EACH_OBJECTS / 2, right));
|
||||
function makeOne(model, sound) {
|
||||
var thisEntity;
|
||||
function dropOnce() { // Once gravity is added, it will work if picked up and again dropped.
|
||||
Entities.editEntity(thisEntity, {gravity: {x: 0, y: -9.8, z: 0}});
|
||||
Script.removeEventHandler(thisEntity, 'clickDownOnEntity', dropOnce);
|
||||
}
|
||||
thisEntity = Entities.addEntity({
|
||||
type: "Model",
|
||||
modelURL: HIFI_PUBLIC_BUCKET + model,
|
||||
collisionSoundURL: sound,
|
||||
collisionsWillMove: true,
|
||||
shapeType: "box",
|
||||
restitution: 0.8,
|
||||
dimensions: currentDimensions,
|
||||
position: currentPosition,
|
||||
angularVelocity: {
|
||||
x: Math.random() * MAX_ANGULAR_SPEED,
|
||||
y: Math.random() * MAX_ANGULAR_SPEED,
|
||||
z: Math.random() * MAX_ANGULAR_SPEED
|
||||
}
|
||||
});
|
||||
ourToys.push(thisEntity);
|
||||
Script.addEventHandler(thisEntity, 'clickDownOnEntity', dropOnce);
|
||||
currentDimensions = Vec3.multiply(currentDimensions, 2);
|
||||
currentPosition = Vec3.sum(currentPosition, right);
|
||||
}
|
||||
function makeThree(modelSound) {
|
||||
var i, model = modelSound[0], sound = modelSound[1];
|
||||
currentDimensions = {x: 0.1, y: 0.1, z: 0.1};
|
||||
for (i = 0; i < N_EACH_OBJECTS; i++) {
|
||||
makeOne(model, sound);
|
||||
}
|
||||
}
|
||||
data.forEach(makeThree);
|
||||
}
|
||||
makeAll();
|
||||
Script.scriptEnding.connect(deleteAll);
|
117
examples/lineRider.js
Normal file
117
examples/lineRider.js
Normal file
|
@ -0,0 +1,117 @@
|
|||
//
|
||||
// lineRider.js
|
||||
// examples
|
||||
//
|
||||
// Created by Eric Levin on 6/4/15.
|
||||
// Copyright 2014 High Fidelity, Inc.
|
||||
//
|
||||
// Takes the avatar on a line ride. Meant to be used in conjunction with paint.js
|
||||
// Paint a line and then click on roller coaster icon to start!
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
LineRider = function() {
|
||||
HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/";
|
||||
var screenSize = Controller.getViewportDimensions();
|
||||
|
||||
var BUTTON_SIZE = 32;
|
||||
var PADDING = 3;
|
||||
|
||||
this.buttonOffColor = {
|
||||
red: 250,
|
||||
green: 10,
|
||||
blue: 10
|
||||
};
|
||||
this.buttonOnColor = {
|
||||
red: 10,
|
||||
green: 200,
|
||||
blue: 100
|
||||
};
|
||||
this.riding = false;
|
||||
|
||||
this.startButton = Overlays.addOverlay("image", {
|
||||
x: screenSize.x / 2 - BUTTON_SIZE + PADDING * 2,
|
||||
y: screenSize.y - (BUTTON_SIZE + PADDING),
|
||||
width: BUTTON_SIZE,
|
||||
height: BUTTON_SIZE,
|
||||
imageURL: HIFI_PUBLIC_BUCKET + "images/coaster.png?v2",
|
||||
color: this.buttonOffColor,
|
||||
alpha: 1
|
||||
});
|
||||
|
||||
this.currentPoint = 0;
|
||||
this.shouldUpdate = false;
|
||||
this.moveIntervalTime = 50;
|
||||
|
||||
}
|
||||
|
||||
|
||||
LineRider.prototype.move = function() {
|
||||
if (!this.shouldMove) {
|
||||
return;
|
||||
}
|
||||
MyAvatar.position = this.points[this.currentPoint++];
|
||||
|
||||
if (this.currentPoint === this.points.length) {
|
||||
this.currentPoint = 0;
|
||||
}
|
||||
var self = this;
|
||||
Script.setTimeout(function() {
|
||||
self.move();
|
||||
}, this.moveIntervalTime);
|
||||
}
|
||||
|
||||
LineRider.prototype.setPath = function(points) {
|
||||
this.points = points;
|
||||
}
|
||||
|
||||
LineRider.prototype.addStartHandler = function(callback) {
|
||||
this.onStart = callback;
|
||||
}
|
||||
|
||||
|
||||
LineRider.prototype.mousePressEvent = function(event) {
|
||||
var clickedOverlay = Overlays.getOverlayAtPoint({
|
||||
x: event.x,
|
||||
y: event.y
|
||||
});
|
||||
if (clickedOverlay == this.startButton) {
|
||||
this.toggleRide();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
LineRider.prototype.toggleRide = function() {
|
||||
this.riding = !this.riding;
|
||||
if (this.riding === true) {
|
||||
Overlays.editOverlay(this.startButton, {
|
||||
color: this.buttonOnColor
|
||||
});
|
||||
if (this.onStart) {
|
||||
this.onStart();
|
||||
//make sure we actually have a path
|
||||
if (this.points.length > 2) {
|
||||
this.shouldMove = true;
|
||||
}
|
||||
var self = this;
|
||||
Script.setTimeout(function() {
|
||||
self.move();
|
||||
}, this.moveIntervalTime);
|
||||
}
|
||||
} else {
|
||||
Overlays.editOverlay(this.startButton, {
|
||||
color: this.buttonOffColor
|
||||
})
|
||||
this.shouldMove = false;
|
||||
}
|
||||
|
||||
}
|
||||
LineRider.prototype.startRide = function() {
|
||||
this.shouldUpdate = true;
|
||||
|
||||
}
|
||||
|
||||
LineRider.prototype.cleanup = function() {
|
||||
Overlays.deleteOverlay(this.startButton);
|
||||
}
|
495
examples/paint.js
Normal file
495
examples/paint.js
Normal file
|
@ -0,0 +1,495 @@
|
|||
//
|
||||
// paint.js
|
||||
// examples
|
||||
//
|
||||
// Created by Eric Levin on 6/4/15.
|
||||
// Copyright 2014 High Fidelity, Inc.
|
||||
//
|
||||
// This script allows you to paint with the hydra or mouse!
|
||||
//
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
Script.include('lineRider.js')
|
||||
var MAX_POINTS_PER_LINE = 30;
|
||||
var DRAWING_DISTANCE = 5;
|
||||
|
||||
var colorPalette = [{
|
||||
red: 236,
|
||||
green: 208,
|
||||
blue: 120
|
||||
}, {
|
||||
red: 217,
|
||||
green: 91,
|
||||
blue: 67
|
||||
}, {
|
||||
red: 192,
|
||||
green: 41,
|
||||
blue: 66
|
||||
}, {
|
||||
red: 84,
|
||||
green: 36,
|
||||
blue: 55
|
||||
}, {
|
||||
red: 83,
|
||||
green: 119,
|
||||
blue: 122
|
||||
}];
|
||||
|
||||
var currentColorIndex = 0;
|
||||
var currentColor = colorPalette[currentColorIndex];
|
||||
|
||||
|
||||
|
||||
if (hydraCheck() === true) {
|
||||
HydraPaint();
|
||||
} else {
|
||||
MousePaint();
|
||||
}
|
||||
|
||||
|
||||
function cycleColor() {
|
||||
currentColor = colorPalette[++currentColorIndex];
|
||||
if (currentColorIndex === colorPalette.length - 1) {
|
||||
currentColorIndex = -1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function hydraCheck() {
|
||||
var numberOfButtons = Controller.getNumberOfButtons();
|
||||
var numberOfTriggers = Controller.getNumberOfTriggers();
|
||||
var numberOfSpatialControls = Controller.getNumberOfSpatialControls();
|
||||
var controllersPerTrigger = numberOfSpatialControls / numberOfTriggers;
|
||||
hydrasConnected = (numberOfButtons == 12 && numberOfTriggers == 2 && controllersPerTrigger == 2);
|
||||
return hydrasConnected; //hydrasConnected;
|
||||
}
|
||||
|
||||
//************ Mouse Paint **************************
|
||||
|
||||
function MousePaint() {
|
||||
var lines = [];
|
||||
var deletedLines = [];
|
||||
var isDrawing = false;
|
||||
var path = [];
|
||||
|
||||
var lineRider = new LineRider();
|
||||
lineRider.addStartHandler(function() {
|
||||
var points = [];
|
||||
//create points array from list of all points in path
|
||||
path.forEach(function(point) {
|
||||
points.push(point);
|
||||
});
|
||||
lineRider.setPath(points);
|
||||
});
|
||||
|
||||
|
||||
|
||||
var LINE_WIDTH = 7;
|
||||
var line;
|
||||
var points = [];
|
||||
|
||||
|
||||
var BRUSH_SIZE = 0.08;
|
||||
|
||||
var brush = Entities.addEntity({
|
||||
type: 'Sphere',
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0
|
||||
},
|
||||
color: currentColor,
|
||||
dimensions: {
|
||||
x: BRUSH_SIZE,
|
||||
y: BRUSH_SIZE,
|
||||
z: BRUSH_SIZE
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function newLine(point) {
|
||||
line = Entities.addEntity({
|
||||
position: MyAvatar.position,
|
||||
type: "Line",
|
||||
color: currentColor,
|
||||
dimensions: {
|
||||
x: 10,
|
||||
y: 10,
|
||||
z: 10
|
||||
},
|
||||
lineWidth: LINE_WIDTH
|
||||
});
|
||||
points = [];
|
||||
if (point) {
|
||||
points.push(point);
|
||||
path.push(point);
|
||||
}
|
||||
lines.push(line);
|
||||
}
|
||||
|
||||
|
||||
function mouseMoveEvent(event) {
|
||||
|
||||
|
||||
var pickRay = Camera.computePickRay(event.x, event.y);
|
||||
var addVector = Vec3.multiply(Vec3.normalize(pickRay.direction), DRAWING_DISTANCE);
|
||||
var point = Vec3.sum(Camera.getPosition(), addVector);
|
||||
Entities.editEntity(line, {
|
||||
linePoints: points
|
||||
});
|
||||
Entities.editEntity(brush, {
|
||||
position: point
|
||||
});
|
||||
if (!isDrawing) {
|
||||
return;
|
||||
}
|
||||
|
||||
points.push(point);
|
||||
path.push(point);
|
||||
|
||||
if (points.length === MAX_POINTS_PER_LINE) {
|
||||
//We need to start a new line!
|
||||
newLine(point);
|
||||
}
|
||||
}
|
||||
|
||||
function undoStroke() {
|
||||
var deletedLine = lines.pop();
|
||||
var deletedLineProps = Entities.getEntityProperties(deletedLine);
|
||||
deletedLines.push(deletedLineProps);
|
||||
Entities.deleteEntity(deletedLine);
|
||||
}
|
||||
|
||||
function redoStroke() {
|
||||
var restoredLine = Entities.addEntity(deletedLines.pop());
|
||||
Entities.addEntity(restoredLine);
|
||||
lines.push(restoredLine);
|
||||
}
|
||||
|
||||
function mousePressEvent(event) {
|
||||
if(!event.isLeftButton) {
|
||||
isDrawing = false;
|
||||
return;
|
||||
}
|
||||
lineRider.mousePressEvent(event);
|
||||
path = [];
|
||||
newLine();
|
||||
isDrawing = true;
|
||||
|
||||
|
||||
}
|
||||
|
||||
function mouseReleaseEvent() {
|
||||
isDrawing = false;
|
||||
}
|
||||
|
||||
function keyPressEvent(event) {
|
||||
if (event.text === "SPACE") {
|
||||
cycleColor();
|
||||
Entities.editEntity(brush, {
|
||||
color: currentColor
|
||||
});
|
||||
}
|
||||
if (event.text === "z") {
|
||||
undoStroke();
|
||||
}
|
||||
if(event.text === "x") {
|
||||
redoStroke();
|
||||
}
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
lines.forEach(function(line) {
|
||||
Entities.deleteEntity(line);
|
||||
});
|
||||
Entities.deleteEntity(brush);
|
||||
lineRider.cleanup();
|
||||
|
||||
}
|
||||
|
||||
|
||||
Controller.mousePressEvent.connect(mousePressEvent);
|
||||
Controller.mouseReleaseEvent.connect(mouseReleaseEvent);
|
||||
Controller.mouseMoveEvent.connect(mouseMoveEvent);
|
||||
Script.scriptEnding.connect(cleanup);
|
||||
|
||||
Controller.keyPressEvent.connect(keyPressEvent);
|
||||
}
|
||||
|
||||
|
||||
|
||||
//*****************HYDRA PAINT *******************************************
|
||||
|
||||
|
||||
|
||||
function HydraPaint() {
|
||||
|
||||
|
||||
|
||||
var lineRider = new LineRider();
|
||||
lineRider.addStartHandler(function() {
|
||||
var points = [];
|
||||
//create points array from list of all points in path
|
||||
rightController.path.forEach(function(point) {
|
||||
points.push(point);
|
||||
});
|
||||
lineRider.setPath(points);
|
||||
});
|
||||
|
||||
var LEFT = 0;
|
||||
var RIGHT = 1;
|
||||
|
||||
var currentTime = 0;
|
||||
|
||||
|
||||
var DISTANCE_FROM_HAND = 2;
|
||||
var minBrushSize = .02;
|
||||
var maxBrushSize = .04
|
||||
|
||||
|
||||
var minLineWidth = 5;
|
||||
var maxLineWidth = 10;
|
||||
var currentLineWidth = minLineWidth;
|
||||
var MIN_PAINT_TRIGGER_THRESHOLD = .01;
|
||||
var LINE_LIFETIME = 20;
|
||||
var COLOR_CHANGE_TIME_FACTOR = 0.1;
|
||||
|
||||
var RIGHT_BUTTON_1 = 7
|
||||
var RIGHT_BUTTON_2 = 8
|
||||
var RIGHT_BUTTON_3 = 9;
|
||||
var RIGHT_BUTTON_4 = 10
|
||||
|
||||
var LEFT_BUTTON_1 = 1;
|
||||
var LEFT_BUTTON_2 = 2;
|
||||
var LEFT_BUTTON_3 = 3;
|
||||
var LEFT_BUTTON_4 = 4;
|
||||
|
||||
var STROKE_SMOOTH_FACTOR = 1;
|
||||
|
||||
var MIN_DRAW_DISTANCE = 1;
|
||||
var MAX_DRAW_DISTANCE = 2;
|
||||
|
||||
function controller(side, undoButton, redoButton, cycleColorButton, startRideButton) {
|
||||
this.triggerHeld = false;
|
||||
this.triggerThreshold = 0.9;
|
||||
this.side = side;
|
||||
this.palm = 2 * side;
|
||||
this.tip = 2 * side + 1;
|
||||
this.trigger = side;
|
||||
this.lines = [];
|
||||
this.deletedLines = [] //just an array of properties objects
|
||||
this.isPainting = false;
|
||||
|
||||
this.undoButton = undoButton;
|
||||
this.undoButtonPressed = false;
|
||||
this.prevUndoButtonPressed = false;
|
||||
|
||||
this.redoButton = redoButton;
|
||||
this.redoButtonPressed = false;
|
||||
this.prevRedoButtonPressed = false;
|
||||
|
||||
this.cycleColorButton = cycleColorButton;
|
||||
this.cycleColorButtonPressed = false;
|
||||
this.prevColorCycleButtonPressed = false;
|
||||
|
||||
this.startRideButton = startRideButton;
|
||||
this.startRideButtonPressed = false;
|
||||
this.prevStartRideButtonPressed = false;
|
||||
|
||||
this.strokeCount = 0;
|
||||
this.currentBrushSize = minBrushSize;
|
||||
this.points = [];
|
||||
this.path = [];
|
||||
|
||||
this.brush = Entities.addEntity({
|
||||
type: 'Sphere',
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0
|
||||
},
|
||||
color: currentColor,
|
||||
dimensions: {
|
||||
x: minBrushSize,
|
||||
y: minBrushSize,
|
||||
z: minBrushSize
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this.newLine = function(point) {
|
||||
this.line = Entities.addEntity({
|
||||
position: MyAvatar.position,
|
||||
type: "Line",
|
||||
color: currentColor,
|
||||
dimensions: {
|
||||
x: 10,
|
||||
y: 10,
|
||||
z: 10
|
||||
},
|
||||
lineWidth: 5,
|
||||
// lifetime: LINE_LIFETIME
|
||||
});
|
||||
this.points = [];
|
||||
if (point) {
|
||||
this.points.push(point);
|
||||
this.path.push(point);
|
||||
}
|
||||
this.lines.push(this.line);
|
||||
}
|
||||
|
||||
this.update = function(deltaTime) {
|
||||
this.updateControllerState();
|
||||
this.avatarPalmOffset = Vec3.subtract(this.palmPosition, MyAvatar.position);
|
||||
this.projectedForwardDistance = Vec3.dot(Quat.getFront(Camera.getOrientation()), this.avatarPalmOffset);
|
||||
this.mappedPalmOffset = map(this.projectedForwardDistance, -.5, .5, MIN_DRAW_DISTANCE, MAX_DRAW_DISTANCE);
|
||||
this.tipDirection = Vec3.normalize(Vec3.subtract(this.tipPosition, this.palmPosition));
|
||||
this.offsetVector = Vec3.multiply(this.mappedPalmOffset, this.tipDirection);
|
||||
this.drawPoint = Vec3.sum(this.palmPosition, this.offsetVector);
|
||||
this.currentBrushSize = map(this.triggerValue, 0, 1, minBrushSize, maxBrushSize);
|
||||
Entities.editEntity(this.brush, {
|
||||
position: this.drawPoint,
|
||||
dimensions: {
|
||||
x: this.currentBrushSize,
|
||||
y: this.currentBrushSize,
|
||||
z: this.currentBrushSize
|
||||
},
|
||||
color: currentColor
|
||||
});
|
||||
if (this.triggerValue > MIN_PAINT_TRIGGER_THRESHOLD) {
|
||||
if (!this.isPainting) {
|
||||
this.isPainting = true;
|
||||
this.newLine();
|
||||
this.path = [];
|
||||
}
|
||||
if (this.strokeCount % STROKE_SMOOTH_FACTOR === 0) {
|
||||
this.paint(this.drawPoint);
|
||||
}
|
||||
this.strokeCount++;
|
||||
} else if (this.triggerValue < MIN_PAINT_TRIGGER_THRESHOLD && this.isPainting) {
|
||||
this.releaseTrigger();
|
||||
}
|
||||
|
||||
this.oldPalmPosition = this.palmPosition;
|
||||
this.oldTipPosition = this.tipPosition;
|
||||
}
|
||||
|
||||
this.releaseTrigger = function() {
|
||||
this.isPainting = false;
|
||||
|
||||
}
|
||||
|
||||
|
||||
this.updateControllerState = function() {
|
||||
this.undoButtonPressed = Controller.isButtonPressed(this.undoButton);
|
||||
this.redoButtonPressed = Controller.isButtonPressed(this.redoButton);
|
||||
this.cycleColorButtonPressed = Controller.isButtonPressed(this.cycleColorButton);
|
||||
this.startRideButtonPressed = Controller.isButtonPressed(this.startRideButton);
|
||||
|
||||
//This logic gives us button release
|
||||
if (this.prevUndoButtonPressed === true && this.undoButtonPressed === false) {
|
||||
//User released undo button, so undo
|
||||
this.undoStroke();
|
||||
}
|
||||
if (this.prevRedoButtonPressed === true && this.redoButtonPressed === false) {
|
||||
this.redoStroke();
|
||||
}
|
||||
|
||||
if (this.prevCycleColorButtonPressed === true && this.cycleColorButtonPressed === false) {
|
||||
cycleColor();
|
||||
Entities.editEntity(this.brush, {
|
||||
color: currentColor
|
||||
});
|
||||
}
|
||||
if (this.prevStartRideButtonPressed === true && this.startRideButtonPressed === false) {
|
||||
lineRider.toggleRide();
|
||||
}
|
||||
this.prevRedoButtonPressed = this.redoButtonPressed;
|
||||
this.prevUndoButtonPressed = this.undoButtonPressed;
|
||||
this.prevCycleColorButtonPressed = this.cycleColorButtonPressed;
|
||||
this.prevStartRideButtonPressed = this.startRideButtonPressed;
|
||||
|
||||
this.palmPosition = Controller.getSpatialControlPosition(this.palm);
|
||||
this.tipPosition = Controller.getSpatialControlPosition(this.tip);
|
||||
this.triggerValue = Controller.getTriggerValue(this.trigger);
|
||||
}
|
||||
|
||||
this.undoStroke = function() {
|
||||
var deletedLine = this.lines.pop();
|
||||
var deletedLineProps = Entities.getEntityProperties(deletedLine);
|
||||
this.deletedLines.push(deletedLineProps);
|
||||
Entities.deleteEntity(deletedLine);
|
||||
}
|
||||
|
||||
this.redoStroke = function() {
|
||||
var restoredLine = Entities.addEntity(this.deletedLines.pop());
|
||||
Entities.addEntity(restoredLine);
|
||||
this.lines.push(restoredLine);
|
||||
}
|
||||
|
||||
this.paint = function(point) {
|
||||
|
||||
currentLineWidth = map(this.triggerValue, 0, 1, minLineWidth, maxLineWidth);
|
||||
this.points.push(point);
|
||||
this.path.push(point);
|
||||
Entities.editEntity(this.line, {
|
||||
linePoints: this.points,
|
||||
lineWidth: currentLineWidth,
|
||||
});
|
||||
if (this.points.length > MAX_POINTS_PER_LINE) {
|
||||
this.newLine(point);
|
||||
}
|
||||
}
|
||||
|
||||
this.cleanup = function() {
|
||||
Entities.deleteEntity(this.brush);
|
||||
this.lines.forEach(function(line) {
|
||||
Entities.deleteEntity(line);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function update(deltaTime) {
|
||||
rightController.update(deltaTime);
|
||||
leftController.update(deltaTime);
|
||||
currentTime += deltaTime;
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
rightController.cleanup();
|
||||
leftController.cleanup();
|
||||
lineRider.cleanup();
|
||||
}
|
||||
|
||||
function mousePressEvent(event) {
|
||||
lineRider.mousePressEvent(event);
|
||||
}
|
||||
|
||||
function vectorIsZero(v) {
|
||||
return v.x === 0 && v.y === 0 && v.z === 0;
|
||||
}
|
||||
|
||||
|
||||
var rightController = new controller(RIGHT, RIGHT_BUTTON_3, RIGHT_BUTTON_4, RIGHT_BUTTON_1, RIGHT_BUTTON_2);
|
||||
var leftController = new controller(LEFT, LEFT_BUTTON_3, LEFT_BUTTON_4, LEFT_BUTTON_1, LEFT_BUTTON_2);
|
||||
|
||||
Script.update.connect(update);
|
||||
Script.scriptEnding.connect(cleanup);
|
||||
Controller.mousePressEvent.connect(mousePressEvent);
|
||||
|
||||
}
|
||||
|
||||
function randFloat(low, high) {
|
||||
return low + Math.random() * (high - low);
|
||||
}
|
||||
|
||||
|
||||
function randInt(low, high) {
|
||||
return Math.floor(randFloat(low, high));
|
||||
}
|
||||
|
||||
function map(value, min1, max1, min2, max2) {
|
||||
return min2 + (max2 - min2) * ((value - min1) / (max1 - min1));
|
||||
}
|
|
@ -1,30 +1,17 @@
|
|||
// pointer.js
|
||||
// examples
|
||||
//
|
||||
// Created by Eric Levin on May 26, 2015
|
||||
// Created by Seth Alves on May 15th
|
||||
// Modified by Eric Levin on June 4
|
||||
// Copyright 2015 High Fidelity, Inc.
|
||||
//
|
||||
// Provides a pointer with option to draw on surfaces
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
var lineEntityID = null;
|
||||
var lineIsRezzed = false;
|
||||
var altHeld = false;
|
||||
var lineCreated = false;
|
||||
var position, positionOffset, prevPosition;
|
||||
var nearLinePosition;
|
||||
var strokes = [];
|
||||
var STROKE_ADJUST = 0.005;
|
||||
var DISTANCE_DRAW_THRESHOLD = .02;
|
||||
var drawDistance = 0;
|
||||
|
||||
var LINE_WIDTH = 20;
|
||||
|
||||
var userCanPoint = false;
|
||||
var userCanDraw = false;
|
||||
|
||||
var BUTTON_SIZE = 32;
|
||||
var PADDING = 3;
|
||||
|
@ -43,16 +30,7 @@ var buttonOnColor = {
|
|||
HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/";
|
||||
var screenSize = Controller.getViewportDimensions();
|
||||
|
||||
var drawButton = Overlays.addOverlay("image", {
|
||||
x: screenSize.x / 2 - BUTTON_SIZE + PADDING * 2,
|
||||
y: screenSize.y - (BUTTON_SIZE + PADDING),
|
||||
width: BUTTON_SIZE,
|
||||
height: BUTTON_SIZE,
|
||||
imageURL: HIFI_PUBLIC_BUCKET + "images/pencil.png?v2",
|
||||
color: buttonOffColor,
|
||||
alpha: 1
|
||||
});
|
||||
|
||||
var userCanPoint = false;
|
||||
var pointerButton = Overlays.addOverlay("image", {
|
||||
x: screenSize.x / 2 - BUTTON_SIZE * 2 + PADDING,
|
||||
y: screenSize.y - (BUTTON_SIZE + PADDING),
|
||||
|
@ -61,14 +39,12 @@ var pointerButton = Overlays.addOverlay("image", {
|
|||
imageURL: HIFI_PUBLIC_BUCKET + "images/laser.png",
|
||||
color: buttonOffColor,
|
||||
alpha: 1
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
|
||||
var center = Vec3.sum(MyAvatar.position, Vec3.multiply(2.0, Quat.getFront(Camera.getOrientation())));
|
||||
center.y += 0.5;
|
||||
|
||||
function calculateNearLinePosition(targetPosition) {
|
||||
function nearLinePoint(targetPosition) {
|
||||
var handPosition = MyAvatar.getRightPalmPosition();
|
||||
var along = Vec3.subtract(targetPosition, handPosition);
|
||||
along = Vec3.normalize(along);
|
||||
|
@ -87,39 +63,40 @@ function removeLine() {
|
|||
|
||||
|
||||
function createOrUpdateLine(event) {
|
||||
if (!userCanPoint) {
|
||||
return;
|
||||
}
|
||||
var pickRay = Camera.computePickRay(event.x, event.y);
|
||||
var intersection = Entities.findRayIntersection(pickRay, true); // accurate picking
|
||||
var props = Entities.getEntityProperties(intersection.entityID);
|
||||
|
||||
if (intersection.intersects) {
|
||||
startPosition = intersection.intersection;
|
||||
var subtractVec = Vec3.multiply(Vec3.normalize(pickRay.direction), STROKE_ADJUST);
|
||||
startPosition = Vec3.subtract(startPosition, subtractVec);
|
||||
nearLinePosition = calculateNearLinePosition(intersection.intersection);
|
||||
positionOffset = Vec3.subtract(startPosition, nearLinePosition);
|
||||
if (intersection.intersects && userCanPoint) {
|
||||
var points = [nearLinePoint(intersection.intersection), intersection.intersection]
|
||||
if (lineIsRezzed) {
|
||||
Entities.editEntity(lineEntityID, {
|
||||
position: nearLinePosition,
|
||||
dimensions: positionOffset,
|
||||
position: nearLinePoint(intersection.intersection),
|
||||
linePoints: points,
|
||||
dimensions: {
|
||||
x: 1,
|
||||
y: 1,
|
||||
z: 1
|
||||
},
|
||||
lifetime: 15 + props.lifespan // renew lifetime
|
||||
});
|
||||
if (userCanDraw) {
|
||||
draw();
|
||||
}
|
||||
} else {
|
||||
lineIsRezzed = true;
|
||||
prevPosition = startPosition;
|
||||
lineEntityID = Entities.addEntity({
|
||||
type: "Line",
|
||||
position: nearLinePosition,
|
||||
dimensions: positionOffset,
|
||||
position: nearLinePoint(intersection.intersection),
|
||||
linePoints: points,
|
||||
dimensions: {
|
||||
x: 1,
|
||||
y: 1,
|
||||
z: 1
|
||||
},
|
||||
color: {
|
||||
red: 255,
|
||||
green: 255,
|
||||
blue: 255
|
||||
},
|
||||
lifetime: 15 // if someone crashes while pointing, don't leave the line there forever.
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
@ -127,120 +104,50 @@ function createOrUpdateLine(event) {
|
|||
}
|
||||
}
|
||||
|
||||
function draw() {
|
||||
|
||||
//We only want to draw line if distance between starting and previous point is large enough
|
||||
drawDistance = Vec3.distance(startPosition, prevPosition);
|
||||
if (drawDistance < DISTANCE_DRAW_THRESHOLD) {
|
||||
function mousePressEvent(event) {
|
||||
if (!event.isLeftButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
var offset = Vec3.subtract(startPosition, prevPosition);
|
||||
strokes.push(Entities.addEntity({
|
||||
type: "Line",
|
||||
position: prevPosition,
|
||||
dimensions: offset,
|
||||
color: {
|
||||
red: 200,
|
||||
green: 40,
|
||||
blue: 200
|
||||
},
|
||||
lineWidth: LINE_WIDTH
|
||||
}));
|
||||
prevPosition = startPosition;
|
||||
}
|
||||
|
||||
function mousePressEvent(event) {
|
||||
var clickedOverlay = Overlays.getOverlayAtPoint({
|
||||
createOrUpdateLine(event);
|
||||
var clickedOverlay = Overlays.getOverlayAtPoint({
|
||||
x: event.x,
|
||||
y: event.y
|
||||
});
|
||||
if (clickedOverlay == drawButton) {
|
||||
userCanDraw = !userCanDraw;
|
||||
if (userCanDraw === true) {
|
||||
Overlays.editOverlay(drawButton, {
|
||||
color: buttonOnColor
|
||||
});
|
||||
} else {
|
||||
Overlays.editOverlay(drawButton, {
|
||||
color: buttonOffColor
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (clickedOverlay == pointerButton) {
|
||||
userCanPoint = !userCanPoint;
|
||||
if (userCanPoint === true) {
|
||||
Overlays.editOverlay(pointerButton, {
|
||||
color: buttonOnColor
|
||||
});
|
||||
if (userCanDraw === true) {
|
||||
|
||||
Overlays.editOverlay(drawButton, {
|
||||
color: buttonOnColor
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Overlays.editOverlay(pointerButton, {
|
||||
color: buttonOffColor
|
||||
});
|
||||
Overlays.editOverlay(drawButton, {
|
||||
color: buttonOffColor
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!event.isLeftButton || altHeld) {
|
||||
return;
|
||||
}
|
||||
Controller.mouseMoveEvent.connect(mouseMoveEvent);
|
||||
createOrUpdateLine(event);
|
||||
lineCreated = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function mouseMoveEvent(event) {
|
||||
createOrUpdateLine(event);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function mouseReleaseEvent(event) {
|
||||
if (!lineCreated) {
|
||||
if (!event.isLeftButton) {
|
||||
return;
|
||||
}
|
||||
Controller.mouseMoveEvent.disconnect(mouseMoveEvent);
|
||||
removeLine();
|
||||
lineCreated = false;
|
||||
}
|
||||
|
||||
function keyPressEvent(event) {
|
||||
if (event.text == "ALT") {
|
||||
altHeld = true;
|
||||
}
|
||||
}
|
||||
|
||||
function keyReleaseEvent(event) {
|
||||
if (event.text == "ALT") {
|
||||
altHeld = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
for (var i = 0; i < strokes.length; i++) {
|
||||
Entities.deleteEntity(strokes[i]);
|
||||
}
|
||||
|
||||
Overlays.deleteOverlay(drawButton);
|
||||
Overlays.deleteOverlay(pointerButton);
|
||||
}
|
||||
|
||||
|
||||
Script.scriptEnding.connect(cleanup);
|
||||
Controller.mousePressEvent.connect(mousePressEvent);
|
||||
Controller.mouseReleaseEvent.connect(mouseReleaseEvent);
|
||||
|
||||
Controller.keyPressEvent.connect(keyPressEvent);
|
||||
Controller.keyReleaseEvent.connect(keyReleaseEvent);
|
||||
Controller.mouseMoveEvent.connect(mouseMoveEvent);
|
||||
Controller.mousePressEvent.connect(mousePressEvent);
|
||||
Controller.mouseReleaseEvent.connect(mouseReleaseEvent);
|
|
@ -3096,8 +3096,11 @@ PickRay Application::computePickRay(float x, float y) const {
|
|||
if (isHMDMode()) {
|
||||
getApplicationOverlay().computeHmdPickRay(glm::vec2(x, y), result.origin, result.direction);
|
||||
} else {
|
||||
auto frustum = activeRenderingThread ? getDisplayViewFrustum() : getViewFrustum();
|
||||
frustum->computePickRay(x, y, result.origin, result.direction);
|
||||
if (QThread::currentThread() == activeRenderingThread) {
|
||||
getDisplayViewFrustum()->computePickRay(x, y, result.origin, result.direction);
|
||||
} else {
|
||||
getViewFrustum()->computePickRay(x, y, result.origin, result.direction);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -1081,11 +1081,18 @@ void EntityTreeRenderer::playEntityCollisionSound(const QUuid& myNodeID, EntityT
|
|||
return;
|
||||
}
|
||||
const float mass = entity->computeMass();
|
||||
const float COLLISION_PENTRATION_TO_VELOCITY = 50; // as a subsitute for RELATIVE entity->getVelocity()
|
||||
const float linearVelocity = glm::length(collision.penetration) * COLLISION_PENTRATION_TO_VELOCITY;
|
||||
const float COLLISION_PENETRATION_TO_VELOCITY = 50; // as a subsitute for RELATIVE entity->getVelocity()
|
||||
// The collision.penetration is a pretty good indicator of changed velocity AFTER the initial contact,
|
||||
// but that first contact depends on exactly where we hit in the physics step.
|
||||
// We can get a more consistent initial-contact energy reading by using the changed velocity.
|
||||
// Note that velocityChange is not a good indicator for continuing collisions, because it does not distinguish
|
||||
// between bounce and sliding along a surface.
|
||||
const float linearVelocity = (collision.type == CONTACT_EVENT_TYPE_START) ?
|
||||
glm::length(collision.velocityChange) :
|
||||
glm::length(collision.penetration) * COLLISION_PENETRATION_TO_VELOCITY;
|
||||
const float energy = mass * linearVelocity * linearVelocity / 2.0f;
|
||||
const glm::vec3 position = collision.contactPoint;
|
||||
const float COLLISION_ENERGY_AT_FULL_VOLUME = 0.5f;
|
||||
const float COLLISION_ENERGY_AT_FULL_VOLUME = (collision.type == CONTACT_EVENT_TYPE_START) ? 150.0f : 5.0f;
|
||||
const float COLLISION_MINIMUM_VOLUME = 0.005f;
|
||||
const float energyFactorOfFull = fmin(1.0f, energy / COLLISION_ENERGY_AT_FULL_VOLUME);
|
||||
if (energyFactorOfFull < COLLISION_MINIMUM_VOLUME) {
|
||||
|
@ -1118,7 +1125,7 @@ void EntityTreeRenderer::playEntityCollisionSound(const QUuid& myNodeID, EntityT
|
|||
soxr_io_spec_t spec = soxr_io_spec(SOXR_INT16_I, SOXR_INT16_I);
|
||||
soxr_quality_spec_t qualitySpec = soxr_quality_spec(SOXR_MQ, 0);
|
||||
const int channelCount = sound->isStereo() ? 2 : 1;
|
||||
const float factor = log(1.0f + (entity->getMaximumAACube().getLargestDimension() / COLLISION_SIZE_FOR_STANDARD_PITCH)) / log(2);
|
||||
const float factor = log(1.0f + (entity->getMinimumAACube().getLargestDimension() / COLLISION_SIZE_FOR_STANDARD_PITCH)) / log(2);
|
||||
const int standardRate = AudioConstants::SAMPLE_RATE;
|
||||
const int resampledRate = standardRate * factor;
|
||||
const int nInputSamples = samples.size() / sizeof(int16_t);
|
||||
|
|
|
@ -48,7 +48,9 @@ void RenderableLineEntityItem::render(RenderArgs* args) {
|
|||
batch.setModelTransform(Transform());
|
||||
|
||||
batch._glLineWidth(getLineWidth());
|
||||
DependencyManager::get<GeometryCache>()->renderVertices(batch, gpu::LINE_STRIP, _lineVerticesID);
|
||||
if (getLinePoints().size() > 1) {
|
||||
DependencyManager::get<GeometryCache>()->renderVertices(batch, gpu::LINE_STRIP, _lineVerticesID);
|
||||
}
|
||||
batch._glLineWidth(1.0f);
|
||||
|
||||
RenderableDebugableEntityItem::render(this, args);
|
||||
|
|
|
@ -85,7 +85,18 @@ bool LineEntityItem::setProperties(const EntityItemProperties& properties) {
|
|||
}
|
||||
|
||||
void LineEntityItem::setLinePoints(const QVector<glm::vec3>& points) {
|
||||
_points = points;
|
||||
QVector<glm::vec3> sanitizedPoints;
|
||||
for (int i = 0; i < points.size(); i++) {
|
||||
glm::vec3 point = points.at(i);
|
||||
// Make sure all of our points are valid numbers.
|
||||
// Must be greater than 0 because vector component is set to 0 if it is invalid data
|
||||
if (point.x > 0 && point.y > 0 && point.z > 0){
|
||||
sanitizedPoints << point;
|
||||
} else {
|
||||
qDebug() << "INVALID POINT";
|
||||
}
|
||||
}
|
||||
_points = sanitizedPoints;
|
||||
_pointsChanged = true;
|
||||
}
|
||||
|
||||
|
|
|
@ -494,6 +494,7 @@ void EntityMotionState::measureBodyAcceleration() {
|
|||
float dt = ((float)numSubsteps * PHYSICS_ENGINE_FIXED_SUBSTEP);
|
||||
float invDt = 1.0f / dt;
|
||||
_lastMeasureStep = thisStep;
|
||||
_measuredDeltaTime = dt;
|
||||
|
||||
// Note: the integration equation for velocity uses damping: v1 = (v0 + a * dt) * (1 - D)^dt
|
||||
// hence the equation for acceleration is: a = (v1 / (1 - D)^dt - v0) / dt
|
||||
|
@ -502,6 +503,12 @@ void EntityMotionState::measureBodyAcceleration() {
|
|||
_lastVelocity = velocity;
|
||||
}
|
||||
}
|
||||
glm::vec3 EntityMotionState::getObjectLinearVelocityChange() const {
|
||||
// This is the dampened change in linear velocity, as calculated in measureBodyAcceleration: dv = a * dt
|
||||
// It is generally only meaningful during the lifespan of collision. In particular, it is not meaningful
|
||||
// when the entity first starts moving via direct user action.
|
||||
return _measuredAcceleration * _measuredDeltaTime;
|
||||
}
|
||||
|
||||
// virtual
|
||||
void EntityMotionState::setMotionType(MotionType motionType) {
|
||||
|
|
|
@ -64,6 +64,7 @@ public:
|
|||
virtual glm::vec3 getObjectLinearVelocity() const { return _entity->getVelocity(); }
|
||||
virtual glm::vec3 getObjectAngularVelocity() const { return _entity->getAngularVelocity(); }
|
||||
virtual glm::vec3 getObjectGravity() const { return _entity->getGravity(); }
|
||||
virtual glm::vec3 getObjectLinearVelocityChange() const;
|
||||
|
||||
virtual const QUuid& getObjectID() const { return _entity->getID(); }
|
||||
|
||||
|
@ -101,6 +102,7 @@ protected:
|
|||
uint32_t _lastMeasureStep;
|
||||
glm::vec3 _lastVelocity;
|
||||
glm::vec3 _measuredAcceleration;
|
||||
float _measuredDeltaTime;
|
||||
|
||||
quint8 _accelerationNearlyGravityCount;
|
||||
bool _candidateForOwnership;
|
||||
|
|
|
@ -82,6 +82,9 @@ void ObjectMotionState::setBodyGravity(const glm::vec3& gravity) const {
|
|||
glm::vec3 ObjectMotionState::getBodyLinearVelocity() const {
|
||||
return bulletToGLM(_body->getLinearVelocity());
|
||||
}
|
||||
glm::vec3 ObjectMotionState::getObjectLinearVelocityChange() const {
|
||||
return glm::vec3(0.0f); // Subclasses override where meaningful.
|
||||
}
|
||||
|
||||
glm::vec3 ObjectMotionState::getBodyAngularVelocity() const {
|
||||
return bulletToGLM(_body->getAngularVelocity());
|
||||
|
|
|
@ -90,6 +90,7 @@ public:
|
|||
|
||||
glm::vec3 getBodyLinearVelocity() const;
|
||||
glm::vec3 getBodyAngularVelocity() const;
|
||||
virtual glm::vec3 getObjectLinearVelocityChange() const;
|
||||
|
||||
virtual uint32_t getAndClearIncomingDirtyFlags() = 0;
|
||||
|
||||
|
|
|
@ -317,6 +317,8 @@ CollisionEvents& PhysicsEngine::getCollisionEvents() {
|
|||
if(type != CONTACT_EVENT_TYPE_CONTINUE || _numSubsteps % CONTINUE_EVENT_FILTER_FREQUENCY == 0) {
|
||||
ObjectMotionState* A = static_cast<ObjectMotionState*>(contactItr->first._a);
|
||||
ObjectMotionState* B = static_cast<ObjectMotionState*>(contactItr->first._b);
|
||||
glm::vec3 velocityChange = (A ? A->getObjectLinearVelocityChange() : glm::vec3(0.0f)) +
|
||||
(B ? B->getObjectLinearVelocityChange() : glm::vec3(0.0f));
|
||||
|
||||
if (A && A->getType() == MOTIONSTATE_TYPE_ENTITY) {
|
||||
QUuid idA = A->getObjectID();
|
||||
|
@ -326,14 +328,14 @@ CollisionEvents& PhysicsEngine::getCollisionEvents() {
|
|||
}
|
||||
glm::vec3 position = bulletToGLM(contact.getPositionWorldOnB()) + _originOffset;
|
||||
glm::vec3 penetration = bulletToGLM(contact.distance * contact.normalWorldOnB);
|
||||
_collisionEvents.push_back(Collision(type, idA, idB, position, penetration));
|
||||
_collisionEvents.push_back(Collision(type, idA, idB, position, penetration, velocityChange));
|
||||
} else if (B && B->getType() == MOTIONSTATE_TYPE_ENTITY) {
|
||||
QUuid idB = B->getObjectID();
|
||||
glm::vec3 position = bulletToGLM(contact.getPositionWorldOnA()) + _originOffset;
|
||||
// NOTE: we're flipping the order of A and B (so that the first objectID is never NULL)
|
||||
// hence we must negate the penetration.
|
||||
glm::vec3 penetration = - bulletToGLM(contact.distance * contact.normalWorldOnB);
|
||||
_collisionEvents.push_back(Collision(type, idB, QUuid(), position, penetration));
|
||||
_collisionEvents.push_back(Collision(type, idB, QUuid(), position, penetration, velocityChange));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -78,15 +78,17 @@ enum ContactEventType {
|
|||
|
||||
class Collision {
|
||||
public:
|
||||
Collision() : type(CONTACT_EVENT_TYPE_START), idA(), idB(), contactPoint(0.0f), penetration(0.0f) { }
|
||||
Collision(ContactEventType cType, const QUuid& cIdA, const QUuid& cIdB, const glm::vec3& cPoint, const glm::vec3& cPenetration)
|
||||
: type(cType), idA(cIdA), idB(cIdB), contactPoint(cPoint), penetration(cPenetration) { }
|
||||
Collision() : type(CONTACT_EVENT_TYPE_START), idA(), idB(), contactPoint(0.0f), penetration(0.0f), velocityChange(0.0f) { }
|
||||
Collision(ContactEventType cType, const QUuid& cIdA, const QUuid& cIdB, const glm::vec3& cPoint,
|
||||
const glm::vec3& cPenetration, const glm::vec3& velocityChange)
|
||||
: type(cType), idA(cIdA), idB(cIdB), contactPoint(cPoint), penetration(cPenetration), velocityChange(velocityChange) { }
|
||||
|
||||
ContactEventType type;
|
||||
QUuid idA;
|
||||
QUuid idB;
|
||||
glm::vec3 contactPoint;
|
||||
glm::vec3 penetration;
|
||||
glm::vec3 velocityChange;
|
||||
};
|
||||
Q_DECLARE_METATYPE(Collision)
|
||||
QScriptValue collisionToScriptValue(QScriptEngine* engine, const Collision& collision);
|
||||
|
|
Loading…
Reference in a new issue