Related
I want to test if a particular Matter body is a circle or not, as in:
const compounds = Matter.Composite.allBodies(engine.world)
compounds.forEach(compound => compound.parts.forEach(part => {
const isCircle = ???
if (isCircle) console.log(part.id, 'is a circle')
else console.log(part.id, 'is not a circle')
})
I can't find an official way to test if a Matter body was created as a circle. How can I test if a body was created with new Matter.Body.Circle versus another Body constructor?
You can console.log(a_circle) and check for something to identify a circle by.
I think you can check for a_circle.circleRadius or a_circle.label=='Circle Body'
EDIT: I have looked at the source code before posting this. It's a safe bet (for now as there is no documentation) because you can see that otherwise is just a polygon.
Matter.Bodies.circle = function(x, y, radius, options, maxSides) {
options = options || {};
var circle = {
label: 'Circle Body',
circleRadius: radius
};
// approximate circles with polygons until true circles implemented in SAT
maxSides = maxSides || 25;
var sides = Math.ceil(Math.max(10, Math.min(maxSides, radius)));
// optimisation: always use even number of sides (half the number of unique axes)
if (sides % 2 === 1)
sides += 1;
return Bodies.polygon(x, y, sides, radius, Common.extend({}, circle, options));
}
The other answer suggests looking for circle-specific properties. One problem is, these can change in future Matter.js releases. Also, it doesn't make for readable, intuitive code, and can lead to surprising bugs when additional body types wind up containing a property unexpectedly.
Better is to use the internal label property (also suggested in that answer), which should be stable and defaults to the seemingly-useful "Rectangle Body" and "Circle Body". For simple use cases, this works. Since it's possible to set the label to an object to store arbitrary custom data on the body, it's tempting to go further and use labels for just about everything.
However, I generally ignore labels. The reason is that it pushes too much of the client logic into a physics library that's not really designed for entity management. Furthermore, either of these approaches (labels or body-specific properties) involves iterating all of the bodies to filter out the type you're interested in.
Although no context was provided about the app, having to call allBodies often seems like a potential antipattern. It might be time to consider a redesign so you don't have to. What if you have 5 circles and 500 other bodies? Recursively iterating all 500 on every frame just to find the 5 is a huge waste of resources to achieve something that should be easy and efficient.
My preferred solution for entity management is to simply keep track of each type upon creation, putting them into data structures that are tuned to application-specific needs.
For example, the following script shows a method of efficiently determining body type by presenting the body as a key to a pre-built types map.
const engine = Matter.Engine.create();
engine.gravity.y = 0; // enable top-down
const map = {width: 300, height: 300};
const render = Matter.Render.create({
element: document.querySelector("#container"),
engine,
options: {...map, wireframes: false},
});
const rnd = n => ~~(Math.random() * n);
const rects = [...Array(20)].map(() => Matter.Bodies.rectangle(
rnd(map.width), // x
rnd(map.height), // y
rnd(10) + 15, // w
rnd(10) + 15, // h
{
angle: rnd(Math.PI * 2),
render: {fillStyle: "pink"}
}
));
const circles = [...Array(20)].map(() => Matter.Bodies.circle(
rnd(map.width), // x
rnd(map.height), // y
rnd(5) + 10, // r
{render: {fillStyle: "red"}}
));
const walls = [
Matter.Bodies.rectangle(
0, map.height / 2, 20, map.height, {
isStatic: true, render: {fillStyle: "yellow"}
}
),
Matter.Bodies.rectangle(
map.width / 2, 0, map.width, 20, {
isStatic: true, render: {fillStyle: "yellow"}
}
),
Matter.Bodies.rectangle(
map.width, map.height / 2, 20, map.height, {
isStatic: true, render: {fillStyle: "yellow"}
}
),
Matter.Bodies.rectangle(
map.width / 2, map.height, map.width, 20, {
isStatic: true, render: {fillStyle: "yellow"}
}
),
];
const rectangle = Symbol("rectangle");
const circle = Symbol("circle");
const wall = Symbol("wall");
const types = new Map([
...rects.map(e => [e, rectangle]),
...circles.map(e => [e, circle]),
...walls.map(e => [e, wall]),
]);
const bodies = [...types.keys()];
const mouseConstraint = Matter.MouseConstraint.create(
engine, {element: document.querySelector("#container")}
);
Matter.Composite.add(engine.world, [
...bodies, mouseConstraint
]);
const runner = Matter.Runner.create();
Matter.Events.on(runner, "tick", event => {
const underMouse = Matter.Query.point(
bodies,
mouseConstraint.mouse.position
);
if (underMouse.length) {
const descriptions = underMouse.map(e =>
types.get(e).description
);
document.querySelector("#type-hover").textContent = `
${descriptions.join(", ")} hovered
`;
}
else {
document.querySelector("#type-hover").textContent = `
[hover a body]
`;
}
if (mouseConstraint.body) {
document.querySelector("#type-click").textContent = `
${types.get(mouseConstraint.body).description} selected
`;
}
else {
document.querySelector("#type-click").textContent = `
[click and drag a body]
`;
}
});
Matter.Render.run(render);
Matter.Runner.run(runner, engine);
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script>
<h3 id="type-click">[click and drag a body]</h3>
<h3 id="type-hover">[hover a body]</h3>
<div id="container"></div>
If creating and destroying bodies can happen dynamically, a function would need to be written to handle data structure bookkeeping.
Another approach that might work well for some apps is to have a few type-specific sets or maps. This allows you to quickly access all entities of a particular type. This becomes particularly useful once you begin composing bodies as properties of custom classes.
There's no reason you can't have both structures--a reverse lookup that gives the type or custom class given a MJS body, and a structure that contains references to all entities or MJS bodies of a particlar type.
The reverse lookup could enable logic like "on click, take an action on a specific MJS body depending on its type" or "on click, locate my custom class/model associated with this MJS body", while collections support logic like "destroy all enemies".
Ideally, code shouldn't be doing much type-checking. With proper OOP design, you can implement classes that respond correctly to methods regardless of their type. For example, if you have Enemy and Ally classes that each respond to a click, you might create a method called handleClick in each class. This allows you to use code like clickedEntity.handleClick(); without having to know whether clickedEntity is an Enemy or Ally, thereby avoiding the need for a "get type" operation entirely.
For more design suggestions for Matter.js projects, see:
How do you access a body by its label in MatterJS?
My own model in matter.js
I have an existing line initiated
// material
const material = new THREE.LineBasicMaterial({ color: 0xffffff });
// array of vertices
vertices.push(new THREE.Vector3(0, 0, 0));
vertices.push(new THREE.Vector3(0, 0, 5));
//
const geometry = new THREE.BufferGeometry().setFromPoints(vertices);
const line = new THREE.Line(geometry, material);
And what I want to do is extend this line following its initiation. I've read this page on how to update things and I don't think it fits this situation because instead of adding vertices to my shape, I want to move them. Then again, it's very likely I misunderstood. I've tried deleting the line and then redrawing it longer, but I can't get it to work without my browser crashing.
The BufferGeometry exposes its vertices through its positions BufferAttribute. To change the positions, you should do something like the following:
//
// Assuming we want to move your line segment (0, 0, 0)-(0, 0, 5) by
// one unit in the direction of positive x, to (1, 0, 0)-(1, 0, 5).
//
// Get a reference to the "position" buffer attribute
const pos = geometry.getAttribute("position");
// Set the new positions
pos.setXYZ(0, vertices[0].x + 1, vertices[0].y, vertices[0].z);
pos.setXYZ(1, vertices[1].x + 1, vertices[1].y, vertices[1].z);
// Update the vertex buffer in graphics memory
pos.needsUpdate = true;
// Update the bounds to support, e.g., frustum culling
geometry.computeBoundingBox();
geometry.computeBoundingSphere();
Other methods exist, such as modifying the attribute's backing array directly and copying in a new array, but the general process will be the same.
We have a specific design challenge for polygon display within leaflet (latest version).
We have polygons which are rendered with a solid border as well as a semi-transparent background.
We are looking for a way to draw a solid borderline as well as a wider "inline" border and no background.
Note: the question is for polygons not rectangular. The below image
and code is just for example.
Is there any way to achieve this?
var polygon = L.polygon([
[ 51.72872938200587, -2.415618896484375 ],
[ 51.72872938200587, -2.080535888671875 ],
[ 51.901918172561714, -2.080535888671875 ],
[ 51.901918172561714, -2.415618896484375 ],
[ 51.72872938200587, -2.415618896484375 ]
],{
color:'#2F538F',
fillOpacity: 0.9,
fillColor: '#BFBFBF',
}).addTo(map);
This is achievable by utilizing leaftlet's class extension system.
To start with, leaflet's class diagram could be consulted to determine where the extension is needed. As a general rule, first try to extend classes towards the root, and prefer L.Class.extend over L.Class.include.
Working Solution:
Codesandbox
One approach is hooking into the rendering process. In the following example, L.Canvas is extended to a custom L.Canvas.WithExtraStyles class (leaflet's plugin building guidelines). The custom Renderer is then provided to map.
In this approach, note that multiple borders and fills (both inset and outset) could be provided using the extraStyles config.
extraStyle custom property accepts Array of PathOptions. With an additional inset, whose value could be positive or a negative number of pixels representing the offset form the border of the main geometry. A negative value of inset will put the border outside of the original polygon.
While implementing such customizations, special care must be taken to make sure leaflet is not considering the added customizations as separate geometric shapes. Otherwise interactive functionalities e.g. Polygon Edit or Leaflet Draw will have unexpected behaviour.
// CanvasWithExtraStyles.js
// First step is to provide a special renderer which accept configuration for extra borders.
// Here L.Canvas is extended using Leaflet's class system
const styleProperties = ['stroke', 'color', 'weight', 'opacity', 'fill', 'fillColor', 'fillOpacity'];
/*
* #class Polygon.MultiStyle
* #aka L.Polygon.MultiStyle
* #inherits L.Polygon
*/
L.Canvas.WithExtraStyles = L.Canvas.extend({
_updatePoly: function(layer, closed) {
const centerCoord = layer.getCenter();
const center = this._map.latLngToLayerPoint(centerCoord);
const originalParts = layer._parts.slice();
// Draw extra styles
if (Array.isArray(layer.options.extraStyles)) {
const originalStyleProperties = styleProperties.reduce(
(acc, cur) => ({ ...acc, [cur]: layer.options[cur] }),
{}
);
const cx = center.x;
const cy = center.y;
for (let eS of layer.options.extraStyles) {
const i = eS.inset || 0;
// For now, the algo doesn't support MultiPolygon
// To have it support MultiPolygon, find centroid
// of each MultiPolygon and perform the following
layer._parts[0] = layer._parts[0].map(p => {
return {
x: p.x < cx ? p.x + i : p.x - i,
y: p.y < cy ? p.y + i : p.y - i
};
});
//Object.keys(eS).map(k => layer.options[k] = eS[k]);
Object.keys(eS).map(k => (layer.options[k] = eS[k]));
L.Canvas.prototype._updatePoly.call(this, layer, closed);
}
// Resetting original conf
layer._parts = originalParts;
Object.assign(layer.options, originalStyleProperties);
}
L.Canvas.prototype._updatePoly.call(this, layer, closed);
}
});
// Leaflet's conventions to also provide factory methods for classes
L.Canvas.withExtraStyles = function(options) {
return new L.Canvas.WithExtraStyles(options);
};
// --------------------------------------------------------------
// map.js
const map = L.map("map", {
center: [52.5145206, 13.3499977],
zoom: 18,
renderer: new L.Canvas.WithExtraStyles()
});
new L.tileLayer(
"https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_nolabels/{z}/{x}/{y}.png",
{
attribution: `attribution: '© OpenStreetMap, © CARTO`,
detectRetina: true
}
).addTo(map);
// Map center
const { x, y } = map.getSize();
// Left Polygon
const polyStyle1 = {
color: '#2f528f',
extraStyles: [
{
color: 'transparent',
weight: 10,
fillColor: '#d9d9d9'
}
]
};
// Sudo coordinates are generated form map container pixels
const polygonCoords1 = [
[0, 10],
[300, 10],
[300, 310],
[0, 310]
].map(point => map.containerPointToLatLng(point));
const polygon1 = new L.Polygon(polygonCoords1, polyStyle1);
polygon1.addTo(map);
// Right Polygon
const polyStyle2 = {
fillColor: "transparent",
color: '#2f528f',
extraStyles: [
{
inset: 6,
color: '#d9d9d9',
weight: 10
}
]
};
const polygonCoords2 = [
[340, 10],
[640, 10],
[640, 310],
[340, 310]
].map(point => map.containerPointToLatLng(point));
const polygon2 = new L.Polygon(polygonCoords2, polyStyle2);
polygon2.addTo(map);
<script src="https://unpkg.com/leaflet#1.6.0/dist/leaflet.js"></script>
<link href="https://unpkg.com/leaflet#1.6.0/dist/leaflet.css" rel="stylesheet"/>
<div id="map" style="width: 100vw; height: 100vw">0012</div>
Ideal Solution:
Implement a plugin as a separate npm module.
Try to extend or hook into Renderer itself instead of separately extending L.Canvas and L.SVG.
Hook the cusomization into base class Path instead of individual shapes: Polygon, Polyline or Circle.
Use the Recatngle/Polygon method.
// define rectangle geographical bounds
var bounds = [[54.559322, -5.767822], [56.1210604, -3.021240]];
// create an orange rectangle
L.rectangle(bounds, {}).addTo(map);
The Use options to get the desired effect on the lines. Options are inherited from polyline options
There you can tweak color, opacity, fill, fillColor, fillOpacity and fillRule to get the desired effect on the lines
I'm using Matter.js and I want two rectangles with a constraint to make them act if they where a single rigid object.
I am basically setting stiffness to 1, so the contraint acts like a rigid bar instead of a spring.
Also to prevent the object from rotating, I'm setting the intertia to Infinity.
// a 20x20 square with 0 friction and infinite inertia
let objectA = Bodies.rectangle(0, 0, 20, 20, {
frictionAir: 0,
inertia: 'Infinity'
});
let objectB = Bodies.rectangle(30, 0, 20, 20, {
frictionAir: 0,
inertia: 'Infinity'
});
let constraint = Constraint.create({
bodyA: objectB,
bodyB: objectB,
length: 30,
stiffness: 1);
This indeed creates 2 objects with a fixed distance and they do not rotate (both squares always have the same absolute orientation)
However the objects can rotate between them, the constrain acts as a linear constraint but not as an angular constraint.
This picture shows how the distance between objects is kept, how the absolute orientation of the objects has not changed but how the objects rotate around each other.
How can I get rid of this rotation and have the two objects act if they were a single object?
I use a different approach: building a Body from parts instead of using constraints. The result is a single rigid object. Matter handles the parts still separately, so you can e.g. drop a ball in the cart created with the code below.
let cart = bodyWithParts(200, 150, { isStatic: true, friction: 0.0 });
function bodyWithParts(x, y, options) {
options = options || {}
let w = 4;
options.parts = [];
options.parts.push(Matter.Bodies.rectangle(w, 20, 5, 20));
options.parts.push(Matter.Bodies.rectangle(40 - w, 20, 5, 20));
options.parts.push(Matter.Bodies.rectangle(20, 40 - w, 50, 5))
let body = Matter.Body.create(options)
Matter.Body.setPosition(body, { x: x, y: y });
return body;
}
Building a Body to of parts can be useful, however, the strength of the orientation "constraint" cannot be lowered. The orientation stays fixed in any situation.
Therefore, I've written the following TypeScript function which adds two constraints of zero length to two Body objects. Constraints allows us to set the stiffness parameter.
Note that removing one the constraints allows one of the bodies to rotate around its own position ("midpoint") but it cannot change its position relative the other body.
/**
* Adds constraints to `bodyA` and `bodyB` such that their current
* relative position and orientaion is preseved (depending on `stiffness`).
*
* #param bodyA the first body of the constraint
* #param bodyB the second body of the constraint
* #param stiffness the stiffness of the constraint connecting `bodyA` and `bodyB`
* #param offsetA the constraint offset on `bodyA` in its coordinate system
* #param offsetB the constraint offset on `bodyB` in its coordinate system
*/
function addRigidBodyConstraints(
bodyA: Body, bodyB: Body,
stiffness: number = 0.1,
offsetA: Vector = Vector.create(0, 0),
offsetB: Vector = Vector.create(0, 0)
) {
function makeConstraint(posA: Vector, posB: Vector): Constraint {
return Constraint.create({
bodyA: bodyA, bodyB: bodyB,
pointA: posA, pointB: posB,
// stiffness larger than 0.1 is sometimes unstable
stiffness: stiffness
})
}
// add constraints to world or compound body
World.add(world, [
makeConstraint(Vector.sub(bodyB.position, bodyA.position), offsetB),
makeConstraint(offsetA, Vector.sub(bodyA.position, bodyB.position))
])
}
I've looked for resources online, but I have not seen a way to extrude a colored image in Three.js. I'm trying to create something like a Minecraft item where the image is used to then create an extruded geometry. An example would be: https://minecraft.gamepedia.com/File:BowSpinning3.gif
I've tried looking at this resource: https://muffinman.io/three-js-extrude-svg-path/ but this only extrudes uncolored SVGs.
loader.load('./textures/diamondbleu.svg', function (data) {
// Group we'll use for all SVG paths
const svgGroup = new THREE.Group();
// When importing SVGs paths are inverted on Y axis
// it happens in the process of mapping from 2d to 3d coordinate system
svgGroup.scale.y *= -1;
const material = new THREE.MeshLambertMaterial();
// Loop through all of the parsed paths
data.paths.forEach((path, i) => {
const shapes = path.toShapes(true);
// Each path has array of shapes
shapes.forEach((shape, j) => {
// Finally we can take each shape and extrude it
const geometry = new THREE.ExtrudeGeometry(shape, {
depth: 20,
bevelEnabled: false
});
// Create a mesh and add it to the group
const mesh = new THREE.Mesh(geometry, material);
svgGroup.add(mesh);
});
});
// Get group's size
const box = new THREE.Box3().setFromObject(svgGroup);
const size = new THREE.Vector3();
box.getSize(size);
const yOffset = size.y / -2;
const xOffset = size.x / -2;
// Offset all of group's elements, to center them
svgGroup.children.forEach(item => {
item.position.x = xOffset;
item.position.y = yOffset;
});
svgGroup.position.set(0, blockSize*75, 0);
// Finally we add svg group to the scene
scene.add(svgGroup);
})
Is there a way to modify the code to allow for colored SVGs? Thanks!
You can use the SVGLoader that's available in the "examples/jsm/loaders/" folder.
The docs have outlined how to generate SVGs in 3D space, it looks like your code sample is missing the part where the paths loop makes a new material and assigns a color for each path:
var material = new THREE.MeshBasicMaterial( {
color: path.color,
side: THREE.DoubleSide,
depthWrite: false
} );
Your code seems to create a single LambertMaterial with no colors assigned, and no lights. Lambert materials need lights to be illuminated, whereas BasicMaterial just shows the color without need of lights.
Look at the code in this demo for another example. Instead of using path.color, this demo finds the color by accessing path.userData.style.fill. I think you'll want the latter approach, depending on your SVG file.