mirror of
https://github.com/lubosz/overte.git
synced 2025-04-23 20:34:07 +02:00
Moved some functions to utils, fixed formatting
This commit is contained in:
parent
7ee26c5d4a
commit
e1b720d338
2 changed files with 279 additions and 277 deletions
|
@ -11,6 +11,8 @@
|
|||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
Script.include("libraries/utils.js");
|
||||
|
||||
|
||||
var RIGHT_HAND = 1;
|
||||
var LEFT_HAND = 0;
|
||||
|
@ -44,310 +46,244 @@ var center = Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getFront(Camera.g
|
|||
|
||||
|
||||
function MyController(hand, triggerAction) {
|
||||
this.hand = hand;
|
||||
this.strokes = [];
|
||||
this.painting = false;
|
||||
this.currentStrokeWidth = MIN_STROKE_WIDTH;
|
||||
|
||||
if (this.hand === RIGHT_HAND) {
|
||||
this.getHandPosition = MyAvatar.getRightPalmPosition;
|
||||
this.getHandRotation = MyAvatar.getRightPalmRotation;
|
||||
} else {
|
||||
this.getHandPosition = MyAvatar.getLeftPalmPosition;
|
||||
this.getHandRotation = MyAvatar.getLeftPalmRotation;
|
||||
}
|
||||
|
||||
this.triggerAction = triggerAction;
|
||||
this.palm = SPATIAL_CONTROLLERS_PER_PALM * hand;
|
||||
this.tip = SPATIAL_CONTROLLERS_PER_PALM * hand + TIP_CONTROLLER_OFFSET;
|
||||
|
||||
|
||||
this.strokeColor = {
|
||||
h: 0.8,
|
||||
s: 0.8,
|
||||
l: 0.4
|
||||
};
|
||||
|
||||
|
||||
this.laserPointer = Overlays.addOverlay("circle3d", {
|
||||
color: hslToRgb(this.strokeColor),
|
||||
solid: true,
|
||||
position: center
|
||||
})
|
||||
this.triggerValue = 0;
|
||||
this.prevTriggerValue = 0;
|
||||
var _this = this;
|
||||
|
||||
this.update = function() {
|
||||
this.updateControllerState()
|
||||
this.search();
|
||||
if (this.canPaint === true) {
|
||||
this.paint(this.intersection.intersection, this.intersection.surfaceNormal);
|
||||
}
|
||||
};
|
||||
|
||||
this.paint = function(position, normal) {
|
||||
if (this.painting === false) {
|
||||
if (this.oldPosition) {
|
||||
this.newStroke(this.oldPosition);
|
||||
} else {
|
||||
this.newStroke(position);
|
||||
}
|
||||
this.painting = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
var localPoint = Vec3.subtract(position, this.strokeBasePosition);
|
||||
//Move stroke a bit forward along normal so it doesnt zfight with mesh its drawing on
|
||||
localPoint = Vec3.sum(localPoint, Vec3.multiply(normal, 0.001 + Math.random() * .001)); //rand avoid z fighting
|
||||
|
||||
var distance = Vec3.distance(localPoint, this.strokePoints[this.strokePoints.length - 1]);
|
||||
if (this.strokePoints.length > 0 && distance < MIN_POINT_DISTANCE) {
|
||||
//need a minimum distance to avoid binormal NANs
|
||||
return;
|
||||
}
|
||||
if (this.strokePoints.length > 0 && distance > MAX_POINT_DISTANCE) {
|
||||
//Prevents drawing lines accross models
|
||||
this.painting = false;
|
||||
return;
|
||||
}
|
||||
if (this.strokePoints.length === 0) {
|
||||
localPoint = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0
|
||||
};
|
||||
}
|
||||
|
||||
this.strokePoints.push(localPoint);
|
||||
this.strokeNormals.push(normal);
|
||||
this.strokeWidths.push(this.currentStrokeWidth);
|
||||
Entities.editEntity(this.currentStroke, {
|
||||
linePoints: this.strokePoints,
|
||||
normals: this.strokeNormals,
|
||||
strokeWidths: this.strokeWidths
|
||||
});
|
||||
if (this.strokePoints.length === MAX_POINTS_PER_LINE) {
|
||||
this.painting = false;
|
||||
return;
|
||||
}
|
||||
this.oldPosition = position
|
||||
}
|
||||
|
||||
this.newStroke = function(position) {
|
||||
this.strokeBasePosition = position;
|
||||
this.currentStroke = Entities.addEntity({
|
||||
position: position,
|
||||
type: "PolyLine",
|
||||
color: hslToRgb(this.strokeColor),
|
||||
dimensions: {
|
||||
x: 50,
|
||||
y: 50,
|
||||
z: 50
|
||||
},
|
||||
lifetime: 200
|
||||
});
|
||||
this.strokePoints = [];
|
||||
this.strokeNormals = [];
|
||||
this.strokeWidths = [];
|
||||
|
||||
this.strokes.push(this.currentStroke);
|
||||
|
||||
}
|
||||
|
||||
this.updateControllerState = function() {
|
||||
this.triggerValue = Controller.getActionValue(this.triggerAction);
|
||||
if (this.triggerValue > TRIGGER_ON_VALUE && this.prevTriggerValue <= TRIGGER_ON_VALUE) {
|
||||
this.squeeze();
|
||||
} else if (this.triggerValue < TRIGGER_ON_VALUE && this.prevTriggerValue >= TRIGGER_ON_VALUE) {
|
||||
this.release()
|
||||
}
|
||||
|
||||
this.prevTriggerValue = this.triggerValue;
|
||||
}
|
||||
|
||||
this.squeeze = function() {
|
||||
this.tryPainting = true;
|
||||
|
||||
}
|
||||
this.release = function() {
|
||||
this.hand = hand;
|
||||
this.strokes = [];
|
||||
this.painting = false;
|
||||
this.tryPainting = false;
|
||||
this.canPaint = false;
|
||||
this.oldPosition = null;
|
||||
}
|
||||
this.search = function() {
|
||||
this.currentStrokeWidth = MIN_STROKE_WIDTH;
|
||||
|
||||
// the trigger is being pressed, do a ray test
|
||||
var handPosition = this.getHandPosition();
|
||||
var pickRay = {
|
||||
origin: handPosition,
|
||||
direction: Quat.getUp(this.getHandRotation())
|
||||
if (this.hand === RIGHT_HAND) {
|
||||
this.getHandPosition = MyAvatar.getRightPalmPosition;
|
||||
this.getHandRotation = MyAvatar.getRightPalmRotation;
|
||||
} else {
|
||||
this.getHandPosition = MyAvatar.getLeftPalmPosition;
|
||||
this.getHandRotation = MyAvatar.getLeftPalmRotation;
|
||||
}
|
||||
|
||||
this.triggerAction = triggerAction;
|
||||
this.palm = SPATIAL_CONTROLLERS_PER_PALM * hand;
|
||||
this.tip = SPATIAL_CONTROLLERS_PER_PALM * hand + TIP_CONTROLLER_OFFSET;
|
||||
|
||||
|
||||
this.strokeColor = {
|
||||
h: 0.8,
|
||||
s: 0.8,
|
||||
l: 0.4
|
||||
};
|
||||
|
||||
|
||||
this.intersection = Entities.findRayIntersection(pickRay, true);
|
||||
if (this.intersection.intersects) {
|
||||
var distance = Vec3.distance(handPosition, this.intersection.intersection);
|
||||
if (distance < MAX_DISTANCE) {
|
||||
var displayPoint = this.intersection.intersection;
|
||||
displayPoint = Vec3.sum(displayPoint, Vec3.multiply(this.intersection.surfaceNormal, .01));
|
||||
if (this.tryPainting) {
|
||||
this.canPaint = true;
|
||||
this.laserPointer = Overlays.addOverlay("circle3d", {
|
||||
color: hslToRgb(this.strokeColor),
|
||||
solid: true,
|
||||
position: center
|
||||
});
|
||||
this.triggerValue = 0;
|
||||
this.prevTriggerValue = 0;
|
||||
var _this = this;
|
||||
|
||||
this.update = function() {
|
||||
this.updateControllerState();
|
||||
this.search();
|
||||
if (this.canPaint === true) {
|
||||
this.paint(this.intersection.intersection, this.intersection.surfaceNormal);
|
||||
}
|
||||
this.currentStrokeWidth = map(this.triggerValue, TRIGGER_ON_VALUE, 1, MIN_STROKE_WIDTH, MAX_STROKE_WIDTH);
|
||||
var laserSize = map(distance, 1, MAX_DISTANCE, 0.001, 0.1);
|
||||
laserSize += this.currentStrokeWidth/2;
|
||||
};
|
||||
|
||||
this.paint = function(position, normal) {
|
||||
if (this.painting === false) {
|
||||
if (this.oldPosition) {
|
||||
this.newStroke(this.oldPosition);
|
||||
} else {
|
||||
this.newStroke(position);
|
||||
}
|
||||
this.painting = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
var localPoint = Vec3.subtract(position, this.strokeBasePosition);
|
||||
//Move stroke a bit forward along normal so it doesnt zfight with mesh its drawing on
|
||||
localPoint = Vec3.sum(localPoint, Vec3.multiply(normal, 0.001 + Math.random() * .001)); //rand avoid z fighting
|
||||
|
||||
var distance = Vec3.distance(localPoint, this.strokePoints[this.strokePoints.length - 1]);
|
||||
if (this.strokePoints.length > 0 && distance < MIN_POINT_DISTANCE) {
|
||||
//need a minimum distance to avoid binormal NANs
|
||||
return;
|
||||
}
|
||||
if (this.strokePoints.length > 0 && distance > MAX_POINT_DISTANCE) {
|
||||
//Prevents drawing lines accross models
|
||||
this.painting = false;
|
||||
return;
|
||||
}
|
||||
if (this.strokePoints.length === 0) {
|
||||
localPoint = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0
|
||||
};
|
||||
}
|
||||
|
||||
this.strokePoints.push(localPoint);
|
||||
this.strokeNormals.push(normal);
|
||||
this.strokeWidths.push(this.currentStrokeWidth);
|
||||
Entities.editEntity(this.currentStroke, {
|
||||
linePoints: this.strokePoints,
|
||||
normals: this.strokeNormals,
|
||||
strokeWidths: this.strokeWidths
|
||||
});
|
||||
if (this.strokePoints.length === MAX_POINTS_PER_LINE) {
|
||||
this.painting = false;
|
||||
return;
|
||||
}
|
||||
this.oldPosition = position;
|
||||
}
|
||||
|
||||
this.newStroke = function(position) {
|
||||
this.strokeBasePosition = position;
|
||||
this.currentStroke = Entities.addEntity({
|
||||
position: position,
|
||||
type: "PolyLine",
|
||||
color: hslToRgb(this.strokeColor),
|
||||
dimensions: {
|
||||
x: 50,
|
||||
y: 50,
|
||||
z: 50
|
||||
},
|
||||
lifetime: 200
|
||||
});
|
||||
this.strokePoints = [];
|
||||
this.strokeNormals = [];
|
||||
this.strokeWidths = [];
|
||||
|
||||
this.strokes.push(this.currentStroke);
|
||||
|
||||
}
|
||||
|
||||
this.updateControllerState = function() {
|
||||
this.triggerValue = Controller.getActionValue(this.triggerAction);
|
||||
if (this.triggerValue > TRIGGER_ON_VALUE && this.prevTriggerValue <= TRIGGER_ON_VALUE) {
|
||||
this.squeeze();
|
||||
} else if (this.triggerValue < TRIGGER_ON_VALUE && this.prevTriggerValue >= TRIGGER_ON_VALUE) {
|
||||
this.release();
|
||||
}
|
||||
|
||||
this.prevTriggerValue = this.triggerValue;
|
||||
}
|
||||
|
||||
this.squeeze = function() {
|
||||
this.tryPainting = true;
|
||||
|
||||
}
|
||||
this.release = function() {
|
||||
this.painting = false;
|
||||
this.tryPainting = false;
|
||||
this.canPaint = false;
|
||||
this.oldPosition = null;
|
||||
}
|
||||
this.search = function() {
|
||||
|
||||
// the trigger is being pressed, do a ray test
|
||||
var handPosition = this.getHandPosition();
|
||||
var pickRay = {
|
||||
origin: handPosition,
|
||||
direction: Quat.getUp(this.getHandRotation())
|
||||
};
|
||||
|
||||
|
||||
this.intersection = Entities.findRayIntersection(pickRay, true);
|
||||
if (this.intersection.intersects) {
|
||||
var distance = Vec3.distance(handPosition, this.intersection.intersection);
|
||||
if (distance < MAX_DISTANCE) {
|
||||
var displayPoint = this.intersection.intersection;
|
||||
displayPoint = Vec3.sum(displayPoint, Vec3.multiply(this.intersection.surfaceNormal, .01));
|
||||
if (this.tryPainting) {
|
||||
this.canPaint = true;
|
||||
}
|
||||
this.currentStrokeWidth = map(this.triggerValue, TRIGGER_ON_VALUE, 1, MIN_STROKE_WIDTH, MAX_STROKE_WIDTH);
|
||||
var laserSize = map(distance, 1, MAX_DISTANCE, 0.01, 0.1);
|
||||
laserSize += this.currentStrokeWidth / 2;
|
||||
Overlays.editOverlay(this.laserPointer, {
|
||||
visible: true,
|
||||
position: displayPoint,
|
||||
rotation: orientationOf(this.intersection.surfaceNormal),
|
||||
size: {
|
||||
x: laserSize,
|
||||
y: laserSize
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
this.hitFail();
|
||||
}
|
||||
} else {
|
||||
this.hitFail();
|
||||
}
|
||||
};
|
||||
|
||||
this.hitFail = function() {
|
||||
this.canPaint = false;
|
||||
|
||||
Overlays.editOverlay(this.laserPointer, {
|
||||
visible: true,
|
||||
position: displayPoint,
|
||||
rotation: orientationOf(this.intersection.surfaceNormal),
|
||||
size: {
|
||||
x: laserSize,
|
||||
y: laserSize
|
||||
}
|
||||
visible: false
|
||||
});
|
||||
|
||||
} else {
|
||||
this.hitFail();
|
||||
}
|
||||
} else {
|
||||
this.hitFail();
|
||||
}
|
||||
};
|
||||
|
||||
this.hitFail = function() {
|
||||
this.canPaint = false;
|
||||
|
||||
Overlays.editOverlay(this.laserPointer, {
|
||||
visible: false
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
this.cleanup = function() {
|
||||
Overlays.deleteOverlay(this.laserPointer);
|
||||
this.strokes.forEach(function(stroke) {
|
||||
Entities.deleteEntity(stroke);
|
||||
});
|
||||
}
|
||||
|
||||
this.cycleColorDown = function() {
|
||||
this.strokeColor.h -= HUE_INCREMENT;
|
||||
if (this.strokeColor.h < 0) {
|
||||
this.strokeColor = 1;
|
||||
this.cleanup = function() {
|
||||
Overlays.deleteOverlay(this.laserPointer);
|
||||
this.strokes.forEach(function(stroke) {
|
||||
Entities.deleteEntity(stroke);
|
||||
});
|
||||
}
|
||||
Overlays.editOverlay(this.laserPointer, {
|
||||
color: hslToRgb(this.strokeColor)
|
||||
});
|
||||
}
|
||||
|
||||
this.cycleColorUp = function() {
|
||||
this.strokeColor.h += HUE_INCREMENT;
|
||||
if (this.strokeColor.h > 1) {
|
||||
this.strokeColor.h = 0;
|
||||
this.cycleColorDown = function() {
|
||||
this.strokeColor.h -= HUE_INCREMENT;
|
||||
if (this.strokeColor.h < 0) {
|
||||
this.strokeColor = 1;
|
||||
}
|
||||
Overlays.editOverlay(this.laserPointer, {
|
||||
color: hslToRgb(this.strokeColor)
|
||||
});
|
||||
}
|
||||
|
||||
this.cycleColorUp = function() {
|
||||
this.strokeColor.h += HUE_INCREMENT;
|
||||
if (this.strokeColor.h > 1) {
|
||||
this.strokeColor.h = 0;
|
||||
}
|
||||
Overlays.editOverlay(this.laserPointer, {
|
||||
color: hslToRgb(this.strokeColor)
|
||||
});
|
||||
}
|
||||
Overlays.editOverlay(this.laserPointer, {
|
||||
color: hslToRgb(this.strokeColor)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var rightController = new MyController(RIGHT_HAND, Controller.findAction("RIGHT_HAND_CLICK"));
|
||||
var leftController = new MyController(LEFT_HAND, Controller.findAction("LEFT_HAND_CLICK"));
|
||||
|
||||
Controller.actionEvent.connect(function(action, state) {
|
||||
if (state === 0) {
|
||||
return;
|
||||
}
|
||||
if (action === RIGHT_4_ACTION) {
|
||||
rightController.cycleColorUp();
|
||||
} else if (action === RIGHT_2_ACTION) {
|
||||
rightController.cycleColorDown();
|
||||
}
|
||||
if (action === LEFT_4_ACTION) {
|
||||
leftController.cycleColorUp();
|
||||
} else if (action === LEFT_2_ACTION) {
|
||||
leftController.cycleColorDown();
|
||||
}
|
||||
if (state === 0) {
|
||||
return;
|
||||
}
|
||||
if (action === RIGHT_4_ACTION) {
|
||||
rightController.cycleColorUp();
|
||||
} else if (action === RIGHT_2_ACTION) {
|
||||
rightController.cycleColorDown();
|
||||
}
|
||||
if (action === LEFT_4_ACTION) {
|
||||
leftController.cycleColorUp();
|
||||
} else if (action === LEFT_2_ACTION) {
|
||||
leftController.cycleColorDown();
|
||||
}
|
||||
});
|
||||
|
||||
function update() {
|
||||
rightController.update();
|
||||
leftController.update();
|
||||
rightController.update();
|
||||
leftController.update();
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
rightController.cleanup();
|
||||
leftController.cleanup();
|
||||
rightController.cleanup();
|
||||
leftController.cleanup();
|
||||
}
|
||||
|
||||
Script.scriptEnding.connect(cleanup);
|
||||
Script.update.connect(update);
|
||||
|
||||
|
||||
function orientationOf(vector) {
|
||||
var Y_AXIS = {
|
||||
x: 0,
|
||||
y: 1,
|
||||
z: 0
|
||||
};
|
||||
var X_AXIS = {
|
||||
x: 1,
|
||||
y: 0,
|
||||
z: 0
|
||||
};
|
||||
|
||||
var theta = 0.0;
|
||||
|
||||
var RAD_TO_DEG = 180.0 / Math.PI;
|
||||
var direction, yaw, pitch;
|
||||
direction = Vec3.normalize(vector);
|
||||
yaw = Quat.angleAxis(Math.atan2(direction.x, direction.z) * RAD_TO_DEG, Y_AXIS);
|
||||
pitch = Quat.angleAxis(Math.asin(-direction.y) * RAD_TO_DEG, X_AXIS);
|
||||
return Quat.multiply(yaw, pitch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an HSL color value to RGB. Conversion formula
|
||||
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
|
||||
* Assumes h, s, and l are contained in the set [0, 1] and
|
||||
* returns r, g, and b in the set [0, 255].
|
||||
*
|
||||
* @param Number h The hue
|
||||
* @param Number s The saturation
|
||||
* @param Number l The lightness
|
||||
* @return Array The RGB representation
|
||||
*/
|
||||
function hslToRgb(hsl, hueOffset) {
|
||||
var r, g, b;
|
||||
if (hsl.s == 0) {
|
||||
r = g = b = hsl.l; // achromatic
|
||||
} else {
|
||||
var hue2rgb = function hue2rgb(p, q, t) {
|
||||
if (t < 0) t += 1;
|
||||
if (t > 1) t -= 1;
|
||||
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
||||
if (t < 1 / 2) return q;
|
||||
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
||||
return p;
|
||||
}
|
||||
|
||||
var q = hsl.l < 0.5 ? hsl.l * (1 + hsl.s) : hsl.l + hsl.s - hsl.l * hsl.s;
|
||||
var p = 2 * hsl.l - q;
|
||||
r = hue2rgb(p, q, hsl.h + 1 / 3);
|
||||
g = hue2rgb(p, q, hsl.h);
|
||||
b = hue2rgb(p, q, hsl.h - 1 / 3);
|
||||
}
|
||||
|
||||
return {
|
||||
red: Math.round(r * 255),
|
||||
green: Math.round(g * 255),
|
||||
blue: Math.round(b * 255)
|
||||
};
|
||||
}
|
||||
|
||||
function map(value, min1, max1, min2, max2) {
|
||||
return min2 + (max2 - min2) * ((value - min1) / (max1 - min1));
|
||||
}
|
|
@ -179,3 +179,69 @@ pointInExtents = function(point, minPoint, maxPoint) {
|
|||
(point.y >= minPoint.y && point.y <= maxPoint.y) &&
|
||||
(point.z >= minPoint.z && point.z <= maxPoint.z);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an HSL color value to RGB. Conversion formula
|
||||
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
|
||||
* Assumes h, s, and l are contained in the set [0, 1] and
|
||||
* returns r, g, and b in the set [0, 255].
|
||||
*
|
||||
* @param Number h The hue
|
||||
* @param Number s The saturation
|
||||
* @param Number l The lightness
|
||||
* @return Array The RGB representation
|
||||
*/
|
||||
hslToRgb = function(hsl, hueOffset) {
|
||||
var r, g, b;
|
||||
if (hsl.s == 0) {
|
||||
r = g = b = hsl.l; // achromatic
|
||||
} else {
|
||||
var hue2rgb = function hue2rgb(p, q, t) {
|
||||
if (t < 0) t += 1;
|
||||
if (t > 1) t -= 1;
|
||||
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
||||
if (t < 1 / 2) return q;
|
||||
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
||||
return p;
|
||||
}
|
||||
|
||||
var q = hsl.l < 0.5 ? hsl.l * (1 + hsl.s) : hsl.l + hsl.s - hsl.l * hsl.s;
|
||||
var p = 2 * hsl.l - q;
|
||||
r = hue2rgb(p, q, hsl.h + 1 / 3);
|
||||
g = hue2rgb(p, q, hsl.h);
|
||||
b = hue2rgb(p, q, hsl.h - 1 / 3);
|
||||
}
|
||||
|
||||
return {
|
||||
red: Math.round(r * 255),
|
||||
green: Math.round(g * 255),
|
||||
blue: Math.round(b * 255)
|
||||
};
|
||||
}
|
||||
|
||||
map = function(value, min1, max1, min2, max2) {
|
||||
return min2 + (max2 - min2) * ((value - min1) / (max1 - min1));
|
||||
}
|
||||
|
||||
orientationOf = function(vector) {
|
||||
var Y_AXIS = {
|
||||
x: 0,
|
||||
y: 1,
|
||||
z: 0
|
||||
};
|
||||
var X_AXIS = {
|
||||
x: 1,
|
||||
y: 0,
|
||||
z: 0
|
||||
};
|
||||
|
||||
var theta = 0.0;
|
||||
|
||||
var RAD_TO_DEG = 180.0 / Math.PI;
|
||||
var direction, yaw, pitch;
|
||||
direction = Vec3.normalize(vector);
|
||||
yaw = Quat.angleAxis(Math.atan2(direction.x, direction.z) * RAD_TO_DEG, Y_AXIS);
|
||||
pitch = Quat.angleAxis(Math.asin(-direction.y) * RAD_TO_DEG, X_AXIS);
|
||||
return Quat.multiply(yaw, pitch);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue