Problem with geometryFunction for draw interaction - javascript

I'm using geometryFunction in draw interaction to change geometry coords of current drawing and I'm also using styling function for handling labels etc.
The problem is that style function gets couple features to handle while drawing but geometryFunction returns only one.
For instance when I'm drawing LineString, geometryFunction returns modified LineString geometry, but style handles 2 features with Point and LineString geometry so feature with Point geometry is not modified.
Same when drawing Polygon: style function handles Point, LineString and Polygon features, but geometryFunction returns only Polygon geometry and in style function only feature with Polygon geometry is modified.
I've tried many ways of changing geometries in style function, but it didn't work as expected.
The question is how to handle coordinate change on pointermove for all features while using draw interaction?
This is my geometryFunction:
const obliqueGeom = (coords, geom) => {
const center = this.obliCoords[0];
const last = this.obliCoords[this.obliCoords.length - 1];
const dx = center[0] - last[0];
const dy = center[1] - last[1];
const radius = Math.sqrt(dx * dx + dy * dy);
if (!geom) {
if (measureType === "LineString") {
geom = new ol.geom.LineString(this.obliCoords)
}
if (measureType === "Polygon") {
geom = new ol.geom.Polygon([this.obliCoords])
}
if (measureType === "Circle") {
geom = new ol.geom.Circle(center, radius)
}
if (measureType === "Point") {
geom = new ol.geom.Point(this.obliCoords)
}
} else {
if (geom instanceof ol.geom.Circle) {
geom.setCenterAndRadius(center, radius)
} else if (geom instanceof ol.geom.Polygon) {
geom.setCoordinates([this.obliCoords])
} else {
geom.setCoordinates(this.obliCoords)
}
}
return geom
}
That's how I'm collecting coords:
this.clickEvt = this.map.on("click", (evt) => {
newPx = [evt.pixel[0], evt.pixel[1] / 0.83]
newCoord = this.map.getCoordinateFromPixel(newPx)
if (measureType !== "Polygon") {
this.obliCoords.push(newCoord)
} else {
if (this.obliCoords.length <= 1) {
this.obliCoords.splice(0, 0, newCoord)
this.obliCoords.push(newCoord)
} else {
this.obliCoords.splice(this.obliCoords.length - 1, 0, newCoord)
}
}
})
this.moveEvt = this.map.on("pointermove", (evt) => {
newPx = [evt.pixel[0], evt.pixel[1] / 0.83]
newCoord = this.map.getCoordinateFromPixel(newPx)
if (measureType !== "Polygon") {
this.obliCoords.pop()
this.obliCoords.push(newCoord)
} else {
if (this.obliCoords.length <= 1) {
this.obliCoords.pop()
this.obliCoords.push(newCoord)
} else {
this.obliCoords.splice(this.obliCoords.length - 2, 1, newCoord)
}
}
})

Depending on the type specified when constructing the interaction the interaction's overlay will have up to 3 features draw.getOverlay().getSource().getFeatures().length One will have the geometry returned by the geometry function, one will have a point geometry using the final coordinate passed to the geometry function (the pointer position), and in the case of polygon type one will be a linestring feature made from the input cooordinates to the geometry function. In a custom style function you might wnat to ignore some geometry types, and if the polygon has been modified by the geometry function return an array of styles based on that to substitute for the auto-generated features which no longer match the polygon, for example
function(feature) {
const geometry = feature.getGeometry();
if (geometry.getType() == 'Polygon') {
const coordinates = geometry.getCoordinates()[0];
return [
new Style({
fill: new Fill({
color: fillColor
})
}),
new Style({
stroke: new Stroke({
width: 2,
color: strokeColor
}),
geometry: new LineString(coordinates.slice(0, -1))
}),
new Style({
image: new CircleStyle({
radius: 5,
fill: circleFill,
stroke: circleStroke
}),
geometry: new Point(coordinates[coordinates.length - 2])
})
];
}
}
Note that you should close a polygon
geom = new ol.geom.Polygon([this.obliCoords.concat([this.obliCoords[0]])])

Related

Cannon.js is there a way to check if an object is colliding with 2 other objects at the same time?

I'm using Cannon.js to make a game where a ball bounces of of your head and the sides of the screen have boxes as boundaries. when the ball collides with the head it adds one more to the score. I want to check if the the ball is colliding with the one of the walls and the head at the same time, because when this happens I don't want it to add one to the score. does someone know how to create a check for this?
My code:
const UIManager = require("./UIManager.js");
var playing = false;
// Create cannon world and setting gravity
const world = new CANNON.World();
world.gravity.set(0, -0.52, 0);
// Create sphere body and setting its shape and properties
var mat1 = new CANNON.Material();
// Radius for the ball object
const Radius = 0.05;
// Radius for the head hitbox
const Radius2 = 0.08;
const BallProp = {
mass: 2,
position: new CANNON.Vec3(0, 1.1, 0),
radius: Radius,
material: mat1,
shape: new CANNON.Sphere(Radius),
}
// Add the ball to the physics world
const BallBody = new CANNON.Body(BallProp);
world.addBody(BallBody);
// Create a material for the head hitbox
var HeadMaterial = new CANNON.Material();
// Create ground body and settings its shape and properties
const HeadProp = {
mass: 0,
position: new CANNON.Vec3(0, 0, 0),
radius: Radius2,
material: HeadMaterial,
shape: new CANNON.Sphere(Radius2),
}
const HeadBody = new CANNON.Body(HeadProp);
// Rotate the ground so it is flat (facing upwards)
const angle = -Math.PI / 2;
const xAxis = new CANNON.Vec3(1, 0, 0);
HeadBody.quaternion.setFromAxisAngle(xAxis, angle);
// Add the hitbox to the physics world
world.addBody(HeadBody);
// Create a new material for the walls
var WallMaterial = new CANNON.Material();
// Create walls and settings its shape and properties
const WallProp1 = {
mass: 0,
position: new CANNON.Vec3(-1.19, 0.6, 0),
material: WallMaterial,
shape: new CANNON.Box(new CANNON.Vec3(1, 1, 1)),
}
const WallProp2 = {
mass: 0,
position: new CANNON.Vec3(1.19, 0.6, 0),
material: WallMaterial,
shape: new CANNON.Box(new CANNON.Vec3(1, 1, 1)),
}
const Wall1Body = new CANNON.Body(WallProp1);
const Wall2Body = new CANNON.Body(WallProp2);
// Add the walls to the physics world
world.addBody(Wall1Body);
world.addBody(Wall2Body);
// Create a death plane and settings its shape and properties
const deathProps = {
mass: 0,
position: new CANNON.Vec3(0, -0.35, 0),
shape: new CANNON.Plane(),
}
const DeathBody = new CANNON.Body(deathProps);
// Set the rotation correctly
DeathBody.quaternion.setFromAxisAngle(xAxis, angle);
// Add the deathplane to the physics world
world.addBody(DeathBody);
// Add new settings to the materials
var mat1_ground = new CANNON.ContactMaterial(HeadMaterial, mat1, { friction: 0.0, restitution: 1});
var mat1_wall = new CANNON.ContactMaterial(WallMaterial, mat1, { friction: 0.0, restitution: 0.65});
world.addContactMaterial(mat1_ground);
world.addContactMaterial(mat1_wall);
// Configure time step for Cannon
const fixedTimeStep = 1.0 / 60.0;
const maxSubSteps = 3;
const timeInterval = 30;
let lastTime;
var score = 0;
//checks for collision with the head hitbox
HeadBody.addEventListener("collide",function(e){
if(playing)
{
score++;
Diagnostics.log("Bounced amount / Score = " + score);
}
else
{
BallBody.velocity.set(0,0,0);
BallBody.position.set(0,1,0);
}
});
//checks for collision with the death plane
DeathBody.addEventListener("collide",function(e){
if(playing)
{
EndGame(score);
}
else
{
BallBody.velocity.set(0,0,0);
BallBody.position.set(0,1,0);
}
});
this might work:
step 1 : create a variable colliding and set it to false
step 2 : set colliding to false every frame
step 3 : in headBody event listener, if it collides with the ball, set colliding to true
step 4 : in deathPlane, check if it collides with the ball and if colliding is true, it means the ball touches both
(you might have to switch, i.e. setting colliding in deathPlane and checking it in headBody)
That's how i used to check if 2 specific items collide, and that check involved checking 2 collisions at same frame + some other verifications.
If you don't know how to check if a body collides with the ball, the condition is event.body.id == ballBody.id with ballBody being the body of the ball use for physical calculations.

WebXR controllers for button pressing in three.js

I would like to figure out how to map out the controls for my oculus quest and other devices, using three.js and webXR. The code works, and allows me to move the controller, maps a cylinder to each control, and allows me to use the trigger to controls to change the color of the cylinders. This is great, but I can't find any documentation on how to use axis controls for the joy stick, the grip and the other buttons. Part of me wants to believe it's as simple as knowing which event to call, because I don't know what other events are available.
Here is a link to the tutorial I based this off of. https://github.com/as-ideas/webvr-with-threejs
Please note that this code works as expected, but I don't know how totake it further and do more.
function createController(controllerID, videoinput) {
//RENDER CONTROLLER AS YELLOW TUBE
const controller = renderer.vr.getController(controllerID);
const cylinderGeometry = new CylinderGeometry(0.025, 0.025, 1, 32);
const cylinderMaterial = new MeshPhongMaterial({ color: 0xffff00 });
const cylinder = new Mesh(cylinderGeometry, cylinderMaterial);
cylinder.geometry.translate(0, 0.5, 0);
cylinder.rotateX(-0.25 * Math.PI);
controller.add(cylinder);
cameraFixture.add(controller);
//TRIGGER
controller.addEventListener('selectstart', () => {
if (controllerID === 0) {
cylinderMaterial.color.set('pink')
} else {
cylinderMaterial.color.set('orange');
videoinput.play()
}
});
controller.addEventListener('selectend', () => {
cylinderMaterial.color.set(0xffff00);
videoinput.pause();
console.log('I pressed play');
});
}
As of three.js 0.119, integrated 'events' from the other buttons, trackpads, haptics, and thumbsticks of a touch controller are not provided, only select and squeeze events are available. three.js has a functional model of 'just working' regardless of what type of input device you have and only provides for managing events that can be produced by all input devices (ie. select)
Luckily, we are not limited by what three.js has made available and can just poll the controller data directly.
Touch controllers follow the model of 'gamepad' controls and just report their instantanous values. We will poll the gamepad for its current values of the various buttons and keep track of their state and create 'events' within our code for button pushes, trackpad and thumbstick axis changes.
To access the instantaneous data from a touch controller while within a webXR session
const session = renderer.xr.getSession();
let i = 0;
if (session) {
for (const source of session.inputSources) {
if (source && source.handedness) {
handedness = source.handedness; //left or right controllers
}
if (!source.gamepad) continue;
const controller = renderer.xr.getController(i++);
const old = prevGamePads.get(source);
const data = {
handedness: handedness,
buttons: source.gamepad.buttons.map((b) => b.value),
axes: source.gamepad.axes.slice(0)
};
//process data accordingly to create 'events'
Haptic feedback is provided through a promise (Note not all browsers currently support the webXR haptic feedback, but Oculus Browser and Firefox Reality on quest do)
When availble, the haptic feedback is produced through a promise:
var didPulse = sourceXR.gamepad.hapticActuators[0].pulse(0.8, 100);
//80% intensity for 100ms
//subsequent promises cancel any previous promise still underway
To demonstrate this solution I have modified threejs.org/examples/#webXR_vr_dragging example by adding the camera to a 'dolly' that can be moved around with the touch controllers thumbsticks when within a webXR session and provide various haptic feedback for events such as raycasting onto an object or axis movements on thumbsticks.
For each frame, we poll the data from the touch controllers and respond accordingly. We have to store the data from frame to frame to detect changes and create our events, and filter out some data (false 0's and up to 20% randomdrift from 0 in thumbstick axis values on some controllers) For proper 'forward and sideways' dolly movement the current heading and attitude of the webXR camera is also needed each frame and accessed via:
let xrCamera = renderer.xr.getCamera(camera);
xrCamera.getWorldDirection(cameraVector);
//heading vector for webXR camera now within cameraVector
Example codepen here:
codepen.io/jason-buchheim/pen/zYqYGXM
With 'ENTER VR' button exposed (debug view) here:cdpn.io/jason-buchheim/debug/zYqYGXM
Full code with modifications of original threejs example highlighted with comment blocks
//// From webxr_vr_dragging example https://threejs.org/examples/#webxr_vr_dragging
import * as THREE from "https://cdn.jsdelivr.net/npm/three#0.119.1/build/three.module.min.js";
import { OrbitControls } from "https://cdn.jsdelivr.net/npm/three#0.119.1/examples/jsm/controls/OrbitControls.min.js";
import { VRButton } from "https://cdn.jsdelivr.net/npm/three#0.119.1/examples/jsm/webxr/VRButton.min.js";
import { XRControllerModelFactory } from "https://cdn.jsdelivr.net/npm/three#0.119.1/examples/jsm/webxr/XRControllerModelFactory.min.js";
var container;
var camera, scene, renderer;
var controller1, controller2;
var controllerGrip1, controllerGrip2;
var raycaster,
intersected = [];
var tempMatrix = new THREE.Matrix4();
var controls, group;
////////////////////////////////////////
//// MODIFICATIONS FROM THREEJS EXAMPLE
//// a camera dolly to move camera within webXR
//// a vector to reuse each frame to store webXR camera heading
//// a variable to store previous frames polling of gamepads
//// a variable to store accumulated accelerations along axis with continuous movement
var dolly;
var cameraVector = new THREE.Vector3(); // create once and reuse it!
const prevGamePads = new Map();
var speedFactor = [0.1, 0.1, 0.1, 0.1];
////
//////////////////////////////////////////
init();
animate();
function init() {
container = document.createElement("div");
document.body.appendChild(container);
scene = new THREE.Scene();
scene.background = new THREE.Color(0x808080);
camera = new THREE.PerspectiveCamera(
50,
window.innerWidth / window.innerHeight,
0.1,
500 //MODIFIED FOR LARGER SCENE
);
camera.position.set(0, 1.6, 3);
controls = new OrbitControls(camera, container);
controls.target.set(0, 1.6, 0);
controls.update();
var geometry = new THREE.PlaneBufferGeometry(100, 100);
var material = new THREE.MeshStandardMaterial({
color: 0xeeeeee,
roughness: 1.0,
metalness: 0.0
});
var floor = new THREE.Mesh(geometry, material);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);
scene.add(new THREE.HemisphereLight(0x808080, 0x606060));
var light = new THREE.DirectionalLight(0xffffff);
light.position.set(0, 200, 0); // MODIFIED SIZE OF SCENE AND SHADOW
light.castShadow = true;
light.shadow.camera.top = 200; // MODIFIED FOR LARGER SCENE
light.shadow.camera.bottom = -200; // MODIFIED FOR LARGER SCENE
light.shadow.camera.right = 200; // MODIFIED FOR LARGER SCENE
light.shadow.camera.left = -200; // MODIFIED FOR LARGER SCENE
light.shadow.mapSize.set(4096, 4096);
scene.add(light);
group = new THREE.Group();
scene.add(group);
var geometries = [
new THREE.BoxBufferGeometry(0.2, 0.2, 0.2),
new THREE.ConeBufferGeometry(0.2, 0.2, 64),
new THREE.CylinderBufferGeometry(0.2, 0.2, 0.2, 64),
new THREE.IcosahedronBufferGeometry(0.2, 3),
new THREE.TorusBufferGeometry(0.2, 0.04, 64, 32)
];
for (var i = 0; i < 100; i++) {
var geometry = geometries[Math.floor(Math.random() * geometries.length)];
var material = new THREE.MeshStandardMaterial({
color: Math.random() * 0xffffff,
roughness: 0.7,
side: THREE.DoubleSide, // MODIFIED TO DoubleSide
metalness: 0.0
});
var object = new THREE.Mesh(geometry, material);
object.position.x = Math.random() * 200 - 100; // MODIFIED FOR LARGER SCENE
object.position.y = Math.random() * 100; // MODIFIED FOR LARGER SCENE
object.position.z = Math.random() * 200 - 100; // MODIFIED FOR LARGER SCENE
object.rotation.x = Math.random() * 2 * Math.PI;
object.rotation.y = Math.random() * 2 * Math.PI;
object.rotation.z = Math.random() * 2 * Math.PI;
object.scale.setScalar(Math.random() * 20 + 0.5); // MODIFIED FOR LARGER SCENE
object.castShadow = true;
object.receiveShadow = true;
group.add(object);
}
// renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.shadowMap.enabled = true;
renderer.xr.enabled = true;
//the following increases the resolution on Quest
renderer.xr.setFramebufferScaleFactor(2.0);
container.appendChild(renderer.domElement);
document.body.appendChild(VRButton.createButton(renderer));
// controllers
controller1 = renderer.xr.getController(0);
controller1.name="left"; ////MODIFIED, added .name="left"
controller1.addEventListener("selectstart", onSelectStart);
controller1.addEventListener("selectend", onSelectEnd);
scene.add(controller1);
controller2 = renderer.xr.getController(1);
controller2.name="right"; ////MODIFIED added .name="right"
controller2.addEventListener("selectstart", onSelectStart);
controller2.addEventListener("selectend", onSelectEnd);
scene.add(controller2);
var controllerModelFactory = new XRControllerModelFactory();
controllerGrip1 = renderer.xr.getControllerGrip(0);
controllerGrip1.add(
controllerModelFactory.createControllerModel(controllerGrip1)
);
scene.add(controllerGrip1);
controllerGrip2 = renderer.xr.getControllerGrip(1);
controllerGrip2.add(
controllerModelFactory.createControllerModel(controllerGrip2)
);
scene.add(controllerGrip2);
//Raycaster Geometry
var geometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0, 0, -1)
]);
var line = new THREE.Line(geometry);
line.name = "line";
line.scale.z = 50; //MODIFIED FOR LARGER SCENE
controller1.add(line.clone());
controller2.add(line.clone());
raycaster = new THREE.Raycaster();
////////////////////////////////////////
//// MODIFICATIONS FROM THREEJS EXAMPLE
//// create group named 'dolly' and add camera and controllers to it
//// will move dolly to move camera and controllers in webXR
dolly = new THREE.Group();
dolly.position.set(0, 0, 0);
dolly.name = "dolly";
scene.add(dolly);
dolly.add(camera);
dolly.add(controller1);
dolly.add(controller2);
dolly.add(controllerGrip1);
dolly.add(controllerGrip2);
////
///////////////////////////////////
window.addEventListener("resize", onWindowResize, false);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function onSelectStart(event) {
var controller = event.target;
var intersections = getIntersections(controller);
if (intersections.length > 0) {
var intersection = intersections[0];
var object = intersection.object;
object.material.emissive.b = 1;
controller.attach(object);
controller.userData.selected = object;
}
}
function onSelectEnd(event) {
var controller = event.target;
if (controller.userData.selected !== undefined) {
var object = controller.userData.selected;
object.material.emissive.b = 0;
group.attach(object);
controller.userData.selected = undefined;
}
}
function getIntersections(controller) {
tempMatrix.identity().extractRotation(controller.matrixWorld);
raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld);
raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix);
return raycaster.intersectObjects(group.children);
}
function intersectObjects(controller) {
// Do not highlight when already selected
if (controller.userData.selected !== undefined) return;
var line = controller.getObjectByName("line");
var intersections = getIntersections(controller);
if (intersections.length > 0) {
var intersection = intersections[0];
////////////////////////////////////////
//// MODIFICATIONS FROM THREEJS EXAMPLE
//// check if in webXR session
//// if so, provide haptic feedback to the controller that raycasted onto object
//// (only if haptic actuator is available)
const session = renderer.xr.getSession();
if (session) { //only if we are in a webXR session
for (const sourceXR of session.inputSources) {
if (!sourceXR.gamepad) continue;
if (
sourceXR &&
sourceXR.gamepad &&
sourceXR.gamepad.hapticActuators &&
sourceXR.gamepad.hapticActuators[0] &&
sourceXR.handedness == controller.name
) {
var didPulse = sourceXR.gamepad.hapticActuators[0].pulse(0.8, 100);
}
}
}
////
////////////////////////////////
var object = intersection.object;
object.material.emissive.r = 1;
intersected.push(object);
line.scale.z = intersection.distance;
} else {
line.scale.z = 50; //MODIFIED AS OUR SCENE IS LARGER
}
}
function cleanIntersected() {
while (intersected.length) {
var object = intersected.pop();
object.material.emissive.r = 0;
}
}
function animate() {
renderer.setAnimationLoop(render);
}
function render() {
cleanIntersected();
intersectObjects(controller1);
intersectObjects(controller2);
////////////////////////////////////////
//// MODIFICATIONS FROM THREEJS EXAMPLE
//add gamepad polling for webxr to renderloop
dollyMove();
////
//////////////////////////////////////
renderer.render(scene, camera);
}
////////////////////////////////////////
//// MODIFICATIONS FROM THREEJS EXAMPLE
//// New dollyMove() function
//// this function polls gamepad and keeps track of its state changes to create 'events'
function dollyMove() {
var handedness = "unknown";
//determine if we are in an xr session
const session = renderer.xr.getSession();
let i = 0;
if (session) {
let xrCamera = renderer.xr.getCamera(camera);
xrCamera.getWorldDirection(cameraVector);
//a check to prevent console errors if only one input source
if (isIterable(session.inputSources)) {
for (const source of session.inputSources) {
if (source && source.handedness) {
handedness = source.handedness; //left or right controllers
}
if (!source.gamepad) continue;
const controller = renderer.xr.getController(i++);
const old = prevGamePads.get(source);
const data = {
handedness: handedness,
buttons: source.gamepad.buttons.map((b) => b.value),
axes: source.gamepad.axes.slice(0)
};
if (old) {
data.buttons.forEach((value, i) => {
//handlers for buttons
if (value !== old.buttons[i] || Math.abs(value) > 0.8) {
//check if it is 'all the way pushed'
if (value === 1) {
//console.log("Button" + i + "Down");
if (data.handedness == "left") {
//console.log("Left Paddle Down");
if (i == 1) {
dolly.rotateY(-THREE.Math.degToRad(1));
}
if (i == 3) {
//reset teleport to home position
dolly.position.x = 0;
dolly.position.y = 5;
dolly.position.z = 0;
}
} else {
//console.log("Right Paddle Down");
if (i == 1) {
dolly.rotateY(THREE.Math.degToRad(1));
}
}
} else {
// console.log("Button" + i + "Up");
if (i == 1) {
//use the paddle buttons to rotate
if (data.handedness == "left") {
//console.log("Left Paddle Down");
dolly.rotateY(-THREE.Math.degToRad(Math.abs(value)));
} else {
//console.log("Right Paddle Down");
dolly.rotateY(THREE.Math.degToRad(Math.abs(value)));
}
}
}
}
});
data.axes.forEach((value, i) => {
//handlers for thumbsticks
//if thumbstick axis has moved beyond the minimum threshold from center, windows mixed reality seems to wander up to about .17 with no input
if (Math.abs(value) > 0.2) {
//set the speedFactor per axis, with acceleration when holding above threshold, up to a max speed
speedFactor[i] > 1 ? (speedFactor[i] = 1) : (speedFactor[i] *= 1.001);
console.log(value, speedFactor[i], i);
if (i == 2) {
//left and right axis on thumbsticks
if (data.handedness == "left") {
// (data.axes[2] > 0) ? console.log('left on left thumbstick') : console.log('right on left thumbstick')
//move our dolly
//we reverse the vectors 90degrees so we can do straffing side to side movement
dolly.position.x -= cameraVector.z * speedFactor[i] * data.axes[2];
dolly.position.z += cameraVector.x * speedFactor[i] * data.axes[2];
//provide haptic feedback if available in browser
if (
source.gamepad.hapticActuators &&
source.gamepad.hapticActuators[0]
) {
var pulseStrength = Math.abs(data.axes[2]) + Math.abs(data.axes[3]);
if (pulseStrength > 0.75) {
pulseStrength = 0.75;
}
var didPulse = source.gamepad.hapticActuators[0].pulse(
pulseStrength,
100
);
}
} else {
// (data.axes[2] > 0) ? console.log('left on right thumbstick') : console.log('right on right thumbstick')
dolly.rotateY(-THREE.Math.degToRad(data.axes[2]));
}
controls.update();
}
if (i == 3) {
//up and down axis on thumbsticks
if (data.handedness == "left") {
// (data.axes[3] > 0) ? console.log('up on left thumbstick') : console.log('down on left thumbstick')
dolly.position.y -= speedFactor[i] * data.axes[3];
//provide haptic feedback if available in browser
if (
source.gamepad.hapticActuators &&
source.gamepad.hapticActuators[0]
) {
var pulseStrength = Math.abs(data.axes[3]);
if (pulseStrength > 0.75) {
pulseStrength = 0.75;
}
var didPulse = source.gamepad.hapticActuators[0].pulse(
pulseStrength,
100
);
}
} else {
// (data.axes[3] > 0) ? console.log('up on right thumbstick') : console.log('down on right thumbstick')
dolly.position.x -= cameraVector.x * speedFactor[i] * data.axes[3];
dolly.position.z -= cameraVector.z * speedFactor[i] * data.axes[3];
//provide haptic feedback if available in browser
if (
source.gamepad.hapticActuators &&
source.gamepad.hapticActuators[0]
) {
var pulseStrength = Math.abs(data.axes[2]) + Math.abs(data.axes[3]);
if (pulseStrength > 0.75) {
pulseStrength = 0.75;
}
var didPulse = source.gamepad.hapticActuators[0].pulse(
pulseStrength,
100
);
}
}
controls.update();
}
} else {
//axis below threshold - reset the speedFactor if it is greater than zero or 0.025 but below our threshold
if (Math.abs(value) > 0.025) {
speedFactor[i] = 0.025;
}
}
});
}
///store this frames data to compate with in the next frame
prevGamePads.set(source, data);
}
}
}
}
function isIterable(obj) { //function to check if object is iterable
// checks for null and undefined
if (obj == null) {
return false;
}
return typeof obj[Symbol.iterator] === "function";
}
////
/////////////////////////////////////

Raycaster cannot get the correct mesh . Three.js

I know this question has been asked I lot of times but I cannot understand how does raycasting work. I have a function creating country borders from a geo json file as well as the fill of the country which I want to be clicked. When clicked some information should be displayed about the clicked country.
var borders = new THREE.Group();
function createBorders(dataJSON) {
var SIZE_AMPLIFIER = 20;
var WIDTH = 2500 * SIZE_AMPLIFIER;
var projection = d3.geoTransverseMercator().rotate([-10, 0, 0]) //center meridian
.center([10, 52]) //longitude, latitude
.scale(WIDTH * 1.5 - 505); //scale
var path = d3.geoPath().projection(projection);
var svg = d3.select("#Map").append("svg");
svg.selectAll(".country")
.data(dataJSON)
.enter()
.append("path")
.attr("class", ".country")
.attr("d", path);
var svgMarkup = svg.node().outerHTML;
var loader = new SVGLoader();
var svgData = loader.parse(svgMarkup);
svgData.paths.forEach((path, i) => {
var shapes = path.toShapes(true);
shapes.forEach((shape, j) => {
var geomSVG = new THREE.ExtrudeBufferGeometry(shape, {
depth: 50,
bevelEnabled: false
})
//needed for click event!
var materialSVG = new THREE.MeshLambertMaterial({
color: 0xFFFFFF,
transparent: true,
opacity: 0.8,
});
var meshSVG = new THREE.Mesh(geomSVG, materialSVG);
this.borders.add(meshSVG);
//create borders
var borderMaterial = new THREE.LineBasicMaterial({ color: 0x000000, linewidth: 3 })
var borderGeometry = new THREE.EdgesGeometry(geomSVG, 15);
var bordermesh = new THREE.LineSegments(borderGeometry, borderMaterial);
this.borders.add(bordermesh);
})
})
this.borders.rotateX(Math.PI / 2)
this.borders.position.z = -300;
this.borders.position.x = 16300;
this.borders.position.y = 0;
//get only meshes, no linesegments
var countryMeshes = [];
for(let m in this.borders.children){
if(this.borders.children[m] instanceof THREE.Mesh){
countryMeshes.push(this.borders.children[m])
}
}
//add callback for each mesh
for(let c in countryMeshes){
countryMeshes[c].callback = function(){console.log(dataJSON[c].properties.name_long)}
}
this.scene.add(this.state.borders);
svg.remove();
}
Then I make the raycasting function:
var raycaster = new THREE.Raycaster();
function onMeshClick(event) {
event.preventDefault();
var vector = new THREE.Vector3((event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1, 0.5);
raycaster.setFromCamera(vector, camera);
var intersects = raycaster.intersectObjects(this.borders.children, true);
if (intersects.length > 0) {
intersects[0].object.callback();
}
}
and at the end I add an event listener:
window.addEventListener('click', onMeshClick, false);
The callback works but not in the correct order. I have to click on another country or even outside the map to be able to call some of the countries.
You're grouping your LineSegments together with your Meshes. Maybe when clicking, it's capturing a line instead of the mesh, since the Raycaster has a precision threshold of 1 world unit when measuring lines, which might be too large. Think of it as ray-casting with a wide brush instead of a single pixel.
You have 2 options:
Group your country areas separately from its borders.
var areas = new THREE.Group();
var borders = new THREE.Group();
scene.add(areas);
scene.add(borders);
svgData.paths.forEach((path, i) => {
// ...
areas.add(meshSVG);
// ...
borders.add(bordermesh);
})
// When raycasting, only test country areas, leaving borders out
var intersects = raycaster.intersectObjects(areas, true);
Lower the threshold of your raycaster so borders don't take up as much space in the raycaster's eyes:
raycaster.params.Line.threshold = 0.1

Render plane from 3 vertices in three.js

I'm trying to render a plane a set of 3 vertices (as shown). However every method I tried (mostly from SO or the official three.js forum) doesn't work for me.
// example vertices
const vert1 = new THREE.Vector3(768, -512, 40)
const vert2 = new THREE.Vector3(768, -496, 40)
const vert3 = new THREE.Vector3(616, -496, 40)
I already tried the following code for calculating the width and height of the plane, but I think it's way over-complicated (as I only calculate the X and Y coords and I think my code would grow exponentially if I'd also add the Z-coordinate and the plane's position to this logic).
const width = vert1.x !== vert2.x ? Math.abs(vert1.x - vert2.x) : Math.abs(vert1.x - vert3.x)
const height = vert1.y !== vert2.y ? Math.abs(vert1.y - vert2.y) : Math.abs(vert1.y - vert3.y)
Example:
I want to create a plane with 3 corners of points A, B and C and a plane with 3 corners of points D, E and F.
Example Video
You can use THREE.Plane.setFromCoplanarPoints() to create a plane from three coplanar points. However, an instance of THREE.Plane is just a mathematical representation of an infinite plane dividing the 3D space in two half spaces. If you want to visualize it, consider to use THREE.PlaneHelper. Or you use the approach from the following thread to derive a plane mesh from your instance of THREE.Plane.
Three.js - PlaneGeometry from Math.Plane
I create algorithm which compute mid point of longest edge of triangle. After this compute vector from point which isn't on longest edge to midpoint. On end just add computed vector to midpoint and you have coordinates of fourth point.
On end just create PlaneGeometry from this points and create mesh. Code is in typescript.
Code here:
type Line = {
startPoint: Vector3;
startPointIdx: number;
endPoint: Vector3;
endPointIdx: number;
vector: Vector3;
length: Vector3;
}
function createTestPlaneWithTexture(): void {
const pointsIn = [new Vector3(28, 3, 3), new Vector3(20, 15, 20), new Vector3(1, 13, 3)]
const lines = Array<Line>();
for (let i = 0; i < pointsIn.length; i++) {
let length, distVect;
if (i <= pointsIn.length - 2) {
distVect = new Vector3().subVectors(pointsIn[i], pointsIn[i + 1]);
length = distVect.length()
lines.push({ vector: distVect, startPoint: pointsIn[i], startPointIdx: i, endPoint: pointsIn[i + 1], endPointIdx: i + 1, length: length })
} else {
const distVect = new Vector3().subVectors(pointsIn[i], pointsIn[0]);
length = distVect.length()
lines.push({ vector: distVect, startPoint: pointsIn[i], startPointIdx: i, endPoint: pointsIn[0], endPointIdx: 0, length: length })
}
}
// find longest edge of triangle
let maxLine: LineType;
lines.forEach(line => {
if (maxLine) {
if (line.length > maxLine.length)
maxLine = line;
} else {
maxLine = line;
}
})
//get midpoint of longest edge
const midPoint = maxLine.endPoint.clone().add(maxLine.vector.clone().multiplyScalar(0.5));
//get idx unused point
const idx = [0, 1, 2].filter(value => value !== maxLine.endPointIdx && value !== maxLine.startPointIdx)[0];
//diagonal point one
const thirdPoint = pointsIn[idx];
const vec = new Vector3().subVectors(midPoint, thirdPoint);
//diagonal point two diagonal === longer diagonal of reactangle
const fourthPoint = midPoint.clone().add(vec);
const edge1 = thirdPoint.clone().sub(maxLine.endPoint).length();
const edge2 = fourthPoint.clone().sub(maxLine.endPoint).length();
//const topLeft = new Vector3(bottomLeft.x, topRight.y, bottomLeft.y);
const points = [thirdPoint, maxLine.startPoint, maxLine.endPoint, fourthPoint];
// console.log(points)
const geo = new PlaneGeometry().setFromPoints(points)
const texture = new TextureLoader().load(textureImage);
texture.wrapS = RepeatWrapping;
texture.wrapT = RepeatWrapping;
texture.repeat.set(edge2, edge1);
const mat = new MeshBasicMaterial({ color: 0xFFFFFFF, side: DoubleSide, map: texture });
const plane = new Mesh(geo, mat);
}

Measure tool with graduation like Google Maps for OpenLayers

I need to implement a measure tool with OpenLayers, and I would like to display distance on the segment, with a smart management of the scale (a mark on the segment each 10m, then each 50m, 100m, 1km, 5km, for example...), very much like the GoogleMaps "measure distance" tool.
Is there any library doing that ? What would be the good approach to implement it ?
In short: No, I don't know any lib or class that provides what you want out of the box.
Option 1: Customize ScaleLine
ScaleLine (see api docs) has the option to provide your own render function (see code). The default implementation just calculates the distance and shows it as {number} {scale} by calling the internal function updateElement_, which then updates the ScaleLine's innerHtml.
You could theoretically replace that method and set the innerHTML yourself. That approach might limit you to the development-variant of the library, because the production code is minified and those elements (innerElement_, element_) are not marked as api.
new ol.control.ScaleLine({
render: function(mapEvent) {
// do stuff
}
});
Option 2: Use the Draw Feature with customized LineString styles
so that might be too complicated and I suggest you go for the ol.interaction.Draw feature. The Measure Example shows us how one could draw stuff while the user is drawing a line. You can combine that with custom styles on a LineString.
// TODO split the uses drawn line into segments, like this mockup
const line = new ol.geom.LineString([
[20.0, 50.0],
[30.0, 47.0],
[40.0, 47.0],
[50.0, 47.0]
]);
line.transform('EPSG:4326', 'EPSG:3857');
const lineFeature = new ol.Feature(line);
const lineSource = new ol.source.Vector({
features: [lineFeature]
});
function segmentText(coord, coord2) {
const coord_t = ol.proj.transform(coord, 'EPSG:3857', 'EPSG:4326');
let coordText = coord_t[1].toFixed(0) + '/' + coord_t[0].toFixed(0);
if(coord2) {
const length = ol.Sphere.getLength(new ol.geom.LineString([coord2, coord]));
const distance = (Math.round(length / 1000 * 100) / 100) + ' km';
coordText = coordText + '\n' + distance;
} else {
coordText = coordText + '\n0';
}
return new ol.style.Text({
text: coordText,
fill: new ol.style.Fill({
color: "#00f"
}),
offsetY: 25,
align: 'center',
scale: 1,
});
}
function styleFunction(feature) {
var geometry = feature.getGeometry();
var styles = [
// linestring style
new ol.style.Style({
stroke: new ol.style.Stroke({
color: '#ff0000',
width: 2
})
})
];
function createSegmentStyle(coord, coord2, rotation) {
return new ol.style.Style({
geometry: new ol.geom.Point(coord),
image: new ol.style.Icon({
src: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAADCAIAAADdv/LVAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAUSURBVBhXY1Da6MPEwMDAxMDAAAALMAEkQYjH8gAAAABJRU5ErkJggg==',
anchor: [0.75, 0.5],
rotateWithView: true,
rotation: -rotation,
scale: 4
}),
text: segmentText(coord, coord2)
})
};
const firstCoord = geometry.getFirstCoordinate();
geometry.forEachSegment(function (start, end) {
var dx = end[0] - start[0];
var dy = end[1] - start[1];
var rotation = Math.atan2(dy, dx);
if (firstCoord[0] === start[0] && firstCoord[1] === start[1]) {
styles.push(createSegmentStyle(start, null, rotation));
}
styles.push(createSegmentStyle(end, firstCoord, rotation));
});
return styles;
}
const map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.Stamen({ layer:'toner-lite' })
}),
new ol.layer.Vector({
source: lineSource,
style: styleFunction
})
],
view: new ol.View({
center: ol.proj.transform(
[35, 45], 'EPSG:4326', 'EPSG:3857'),
zoom: 4
})
});
<link href="https://cdnjs.cloudflare.com/ajax/libs/openlayers/4.5.0/ol.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/openlayers/4.5.0/ol-debug.js"></script>
<div id="map" style="height:300px"></div>

Categories

Resources