mirror of
https://gitea.wildfiregames.com/0ad/0ad.git
synced 2026-07-25 12:52:36 +00:00
7b4cf11e25
Manual fixes needed for: eslint --no-config-lookup --rule '"no-shadow": 1' Ref: #7812 Signed-off-by: Ralph Sennhauser <ralph.sennhauser@gmail.com>
166 lines
5.4 KiB
JavaScript
166 lines
5.4 KiB
JavaScript
/**
|
|
* The class allows the player to position structures so that they are aligned
|
|
* with nearby structures.
|
|
*/
|
|
class ObstructionSnap
|
|
{
|
|
getValidEdges(allEdges, position, maxSide)
|
|
{
|
|
const edges = [];
|
|
const dir1 = new Vector2D();
|
|
const dir2 = new Vector2D();
|
|
for (const edge of allEdges)
|
|
{
|
|
const signedDistance = Vector2D.dot(edge.normal, position) -
|
|
Vector2D.dot(edge.normal, edge.begin);
|
|
// Negative signed distance means that the template position
|
|
// lays behind the edge.
|
|
if (signedDistance < -this.MinimalDistanceToSnap - maxSide ||
|
|
signedDistance > this.MinimalDistanceToSnap + maxSide)
|
|
continue;
|
|
dir1.setFrom(edge.begin).sub(edge.end).normalize();
|
|
dir2.setFrom(dir1).mult(-1);
|
|
let offsetDistance = Math.max(
|
|
Vector2D.dot(dir1, position) - Vector2D.dot(dir1, edge.begin),
|
|
Vector2D.dot(dir2, position) - Vector2D.dot(dir2, edge.end));
|
|
if (offsetDistance > this.MinimalDistanceToSnap + maxSide)
|
|
continue;
|
|
// If a projection of the template position on the edge is
|
|
// lying inside the edge then obviously we don't need to
|
|
// account the offset distance.
|
|
if (offsetDistance < 0)
|
|
offsetDistance = 0;
|
|
edge.signedDistance = signedDistance;
|
|
edge.offsetDistance = offsetDistance;
|
|
edges.push(edge);
|
|
}
|
|
return edges;
|
|
}
|
|
|
|
// We need a small padding to avoid unnecessary collisions
|
|
// because of loss of accuracy.
|
|
getPadding(edge)
|
|
{
|
|
const snapPadding = 0.05;
|
|
// We don't need to padding for edges with normals directed inside
|
|
// its entity, as we try to snap from an internal side of the edge.
|
|
return edge.order == "ccw" ? 0 : snapPadding;
|
|
}
|
|
|
|
// Pick a base edge, it will be the first axis and fix the angle.
|
|
// We can't just pick an edge by signed distance, because we might have
|
|
// a case when one segment is closer by signed distance than another
|
|
// one but much farther by actual (euclid) distance.
|
|
compareEdges(a, b)
|
|
{
|
|
const behindA = a.signedDistance < -this.EPS;
|
|
const behindB = b.signedDistance < -this.EPS;
|
|
const scoreA = Math.abs(a.signedDistance) + a.offsetDistance;
|
|
const scoreB = Math.abs(b.signedDistance) + b.offsetDistance;
|
|
if (Math.abs(scoreA - scoreB) < this.EPS)
|
|
{
|
|
if (behindA != behindB)
|
|
return behindA - behindB;
|
|
if (!behindA)
|
|
return a.offsetDistance - b.offsetDistance;
|
|
return -a.signedDistance - -b.signedDistance;
|
|
}
|
|
return scoreA - scoreB;
|
|
}
|
|
|
|
getNearestSizeAlongNormal(width, depth, angle, normal)
|
|
{
|
|
// Front face direction.
|
|
const direction = new Vector2D(0.0, 1.0);
|
|
direction.rotate(angle);
|
|
const dot = direction.dot(normal);
|
|
const threshold = Math.cos(Math.PI / 4.0);
|
|
if (Math.abs(dot) > threshold)
|
|
return [depth, width];
|
|
return [width, depth];
|
|
}
|
|
|
|
getPosition(data, template)
|
|
{
|
|
if (!data.snapToEdges || !template.Obstruction || !template.Obstruction.Static)
|
|
return undefined;
|
|
|
|
const width = template.Obstruction.Static["@width"] / 2;
|
|
const depth = template.Obstruction.Static["@depth"] / 2;
|
|
const maxSide = Math.max(width, depth);
|
|
const templatePos = Vector2D.from3D(data);
|
|
let templateAngle = data.angle || 0;
|
|
|
|
let edges = this.getValidEdges(data.snapToEdges, templatePos, maxSide);
|
|
if (!edges.length)
|
|
return undefined;
|
|
|
|
let baseEdge = edges[0];
|
|
for (const edge of edges)
|
|
if (this.compareEdges(edge, baseEdge) < 0)
|
|
baseEdge = edge;
|
|
// Now we have the normal, we need to determine an angle,
|
|
// which side will be snapped first.
|
|
for (let dir = 0; dir < 4; ++dir)
|
|
{
|
|
const angleCandidate = baseEdge.angle + dir * Math.PI / 2;
|
|
// We need to find a minimal angle difference.
|
|
let difference = Math.abs(angleCandidate - templateAngle);
|
|
difference = Math.min(difference, Math.PI * 2 - difference);
|
|
if (difference < Math.PI / 4 + this.EPS)
|
|
{
|
|
templateAngle = angleCandidate;
|
|
break;
|
|
}
|
|
}
|
|
const [sizeToBaseEdge, sizeToPairedEdge] =
|
|
this.getNearestSizeAlongNormal(width, depth, templateAngle, baseEdge.normal);
|
|
|
|
const distance = Vector2D.dot(baseEdge.normal, templatePos) - Vector2D.dot(baseEdge.normal, baseEdge.begin);
|
|
templatePos.sub(Vector2D.mult(baseEdge.normal, distance - sizeToBaseEdge - this.getPadding(baseEdge)));
|
|
edges = this.getValidEdges(data.snapToEdges, templatePos, maxSide);
|
|
if (edges.length > 1)
|
|
{
|
|
const pairedEdges = [];
|
|
for (const edge of edges)
|
|
{
|
|
// We have to place a rectangle, so the angle between
|
|
// edges should be 90 degrees.
|
|
if (Math.abs(Vector2D.dot(baseEdge.normal, edge.normal)) > this.EPS)
|
|
continue;
|
|
const newEdge = {
|
|
"begin": edge.end,
|
|
"end": edge.begin,
|
|
"normal": Vector2D.mult(edge.normal, -1),
|
|
"signedDistance": -edge.signedDistance,
|
|
"offsetDistance": edge.offsetDistance,
|
|
"order": "ccw",
|
|
};
|
|
pairedEdges.push(edge);
|
|
pairedEdges.push(newEdge);
|
|
}
|
|
pairedEdges.sort(this.compareEdges.bind(this));
|
|
if (pairedEdges.length)
|
|
{
|
|
let secondEdge = pairedEdges[0];
|
|
for (const edge of pairedEdges)
|
|
if (this.compareEdges(edge, secondEdge) < 0)
|
|
secondEdge = edge;
|
|
const dist = Vector2D.dot(secondEdge.normal, templatePos) - Vector2D.dot(secondEdge.normal, secondEdge.begin);
|
|
templatePos.sub(Vector2D.mult(secondEdge.normal, dist - sizeToPairedEdge - this.getPadding(secondEdge)));
|
|
}
|
|
}
|
|
return {
|
|
"x": templatePos.x,
|
|
"z": templatePos.y,
|
|
"angle": templateAngle
|
|
};
|
|
}
|
|
}
|
|
|
|
ObstructionSnap.prototype.MinimalDistanceToSnap = 5;
|
|
|
|
ObstructionSnap.prototype.EPS = 1e-3;
|
|
|
|
Engine.RegisterGlobal("ObstructionSnap", ObstructionSnap);
|