Index: ps/trunk/binaries/data/mods/public/simulation/helpers/ObstructionSnap.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/ObstructionSnap.js (revision 24826)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/ObstructionSnap.js (revision 24827)
@@ -1,154 +1,165 @@
/**
* The class allows the player to position structures so that they are aligned
* with nearby structures.
*/
class ObstructionSnap
{
getValidEdges(allEdges, position, maxSide)
{
let edges = [];
let dir1 = new Vector2D();
let dir2 = new Vector2D();
for (let edge of allEdges)
{
let 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.
+ let direction = new Vector2D(0.0, 1.0);
+ direction.rotate(angle);
+ let 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;
- let width = template.Obstruction.Static["@depth"] / 2;
- let depth = template.Obstruction.Static["@width"] / 2;
+ const width = template.Obstruction.Static["@width"] / 2;
+ const depth = template.Obstruction.Static["@depth"] / 2;
const maxSide = Math.max(width, depth);
let 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 (let 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)
{
- // We need to swap sides for orthogonal cases.
- if (dir % 2 == 0)
- [width, depth] = [depth, width];
templateAngle = angleCandidate;
break;
}
}
+ let [sizeToBaseEdge, sizeToPairedEdge] =
+ this.getNearestSizeAlongNormal(width, depth, templateAngle, baseEdge.normal);
let distance = Vector2D.dot(baseEdge.normal, templatePos) - Vector2D.dot(baseEdge.normal, baseEdge.begin);
- templatePos.sub(Vector2D.mult(baseEdge.normal, distance - width - this.getPadding(baseEdge)));
+ templatePos.sub(Vector2D.mult(baseEdge.normal, distance - sizeToBaseEdge - this.getPadding(baseEdge)));
edges = this.getValidEdges(data.snapToEdges, templatePos, maxSide);
if (edges.length > 1)
{
let pairedEdges = [];
for (let 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;
let 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 (let edge of pairedEdges)
if (this.compareEdges(edge, secondEdge) < 0)
secondEdge = edge;
let distance = Vector2D.dot(secondEdge.normal, templatePos) - Vector2D.dot(secondEdge.normal, secondEdge.begin);
- templatePos.sub(Vector2D.mult(secondEdge.normal, distance - depth - this.getPadding(secondEdge)));
+ templatePos.sub(Vector2D.mult(secondEdge.normal, distance - 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);
Index: ps/trunk/source/simulation2/scripting/JSInterface_Simulation.cpp
===================================================================
--- ps/trunk/source/simulation2/scripting/JSInterface_Simulation.cpp (revision 24826)
+++ ps/trunk/source/simulation2/scripting/JSInterface_Simulation.cpp (revision 24827)
@@ -1,220 +1,220 @@
-/* Copyright (C) 2020 Wildfire Games.
+/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "JSInterface_Simulation.h"
#include "graphics/GameView.h"
#include "ps/ConfigDB.h"
#include "ps/Game.h"
#include "ps/GameSetup/Config.h"
#include "ps/Pyrogenesis.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/components/ICmpAIManager.h"
#include "simulation2/components/ICmpCommandQueue.h"
#include "simulation2/components/ICmpGuiInterface.h"
#include "simulation2/components/ICmpObstruction.h"
#include "simulation2/components/ICmpPosition.h"
#include "simulation2/components/ICmpSelectable.h"
#include "simulation2/helpers/Geometry.h"
#include "simulation2/helpers/Selection.h"
#include "simulation2/Simulation2.h"
#include "simulation2/system/Entity.h"
#include
#include
JS::Value JSI_Simulation::GuiInterfaceCall(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& name, JS::HandleValue data)
{
if (!g_Game)
return JS::UndefinedValue();
CSimulation2* sim = g_Game->GetSimulation2();
ENSURE(sim);
CmpPtr cmpGuiInterface(*sim, SYSTEM_ENTITY);
if (!cmpGuiInterface)
return JS::UndefinedValue();
ScriptRequest rqSim(sim->GetScriptInterface());
JS::RootedValue arg(rqSim.cx, sim->GetScriptInterface().CloneValueFromOtherCompartment(*(pCmptPrivate->pScriptInterface), data));
JS::RootedValue ret(rqSim.cx);
cmpGuiInterface->ScriptCall(g_Game->GetViewedPlayerID(), name, arg, &ret);
return pCmptPrivate->pScriptInterface->CloneValueFromOtherCompartment(sim->GetScriptInterface(), ret);
}
void JSI_Simulation::PostNetworkCommand(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue cmd)
{
if (!g_Game)
return;
CSimulation2* sim = g_Game->GetSimulation2();
ENSURE(sim);
CmpPtr cmpCommandQueue(*sim, SYSTEM_ENTITY);
if (!cmpCommandQueue)
return;
ScriptRequest rqSim(sim->GetScriptInterface());
JS::RootedValue cmd2(rqSim.cx,
sim->GetScriptInterface().CloneValueFromOtherCompartment(*(pCmptPrivate->pScriptInterface), cmd));
cmpCommandQueue->PostNetworkCommand(cmd2);
}
void JSI_Simulation::DumpSimState(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate))
{
OsPath path = psLogDir()/"sim_dump.txt";
std::ofstream file (OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc);
g_Game->GetSimulation2()->DumpDebugState(file);
}
entity_id_t JSI_Simulation::PickEntityAtPoint(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), int x, int y)
{
return EntitySelection::PickEntityAtPoint(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x, y, g_Game->GetViewedPlayerID(), false);
}
std::vector JSI_Simulation::PickPlayerEntitiesInRect(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), int x0, int y0, int x1, int y1, int player)
{
return EntitySelection::PickEntitiesInRect(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x0, y0, x1, y1, player, false);
}
std::vector JSI_Simulation::PickPlayerEntitiesOnScreen(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), int player)
{
return EntitySelection::PickEntitiesInRect(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), 0, 0, g_xres, g_yres, player, false);
}
std::vector JSI_Simulation::PickNonGaiaEntitiesOnScreen(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate))
{
return EntitySelection::PickNonGaiaEntitiesInRect(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), 0, 0, g_xres, g_yres, false);
}
std::vector JSI_Simulation::GetEntitiesWithStaticObstructionOnScreen(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate))
{
struct StaticObstructionFilter
{
bool operator()(IComponent* cmp)
{
ICmpObstruction* cmpObstruction = static_cast(cmp);
return cmpObstruction->GetObstructionType() == ICmpObstruction::STATIC;
}
};
return EntitySelection::GetEntitiesWithComponentInRect(*g_Game->GetSimulation2(), IID_Obstruction, *g_Game->GetView()->GetCamera(), 0, 0, g_xres, g_yres);
}
JS::Value JSI_Simulation::GetEdgesOfStaticObstructionsOnScreenNearTo(ScriptInterface::CmptPrivate* pCmptPrivate, entity_pos_t x, entity_pos_t z)
{
if (!g_Game)
return JS::UndefinedValue();
CSimulation2* sim = g_Game->GetSimulation2();
ENSURE(sim);
ScriptRequest rq(pCmptPrivate->pScriptInterface);
JS::RootedValue edgeList(rq.cx);
ScriptInterface::CreateArray(rq, &edgeList);
int edgeListIndex = 0;
float distanceThreshold = 10.0f;
CFG_GET_VAL("gui.session.snaptoedgesdistancethreshold", distanceThreshold);
CFixedVector2D entityPos(x, z);
std::vector entities = GetEntitiesWithStaticObstructionOnScreen(pCmptPrivate);
for (entity_id_t entity : entities)
{
CmpPtr cmpObstruction(sim->GetSimContext(), entity);
if (!cmpObstruction)
continue;
CmpPtr cmpPosition(sim->GetSimContext(), entity);
if (!cmpPosition || !cmpPosition->IsInWorld())
continue;
CFixedVector2D halfSize = cmpObstruction->GetStaticSize() / 2;
if (halfSize.X.IsZero() || halfSize.Y.IsZero() || std::max(halfSize.X, halfSize.Y) <= fixed::FromInt(2))
continue;
std::array corners = {
CFixedVector2D(-halfSize.X, -halfSize.Y),
CFixedVector2D(-halfSize.X, halfSize.Y),
halfSize,
CFixedVector2D(halfSize.X, -halfSize.Y)
};
- fixed angle = cmpPosition->GetRotation().Y;
+ const fixed angle = cmpPosition->GetRotation().Y;
for (CFixedVector2D& corner : corners)
corner = corner.Rotate(angle) + cmpPosition->GetPosition2D();
for (size_t i = 0; i < corners.size(); ++i)
{
JS::RootedValue edge(rq.cx);
const CFixedVector2D& corner = corners[i];
const CFixedVector2D& nextCorner = corners[(i + 1) % corners.size()];
- fixed distanceToEdge =
+ const fixed distanceToEdge =
Geometry::DistanceToSegment(entityPos, corner, nextCorner);
if (distanceToEdge.ToFloat() > distanceThreshold)
continue;
CFixedVector2D normal = -(nextCorner - corner).Perpendicular();
normal.Normalize();
ScriptInterface::CreateObject(
rq,
&edge,
"begin", corner,
"end", nextCorner,
"angle", angle,
"normal", normal,
"order", "cw");
pCmptPrivate->pScriptInterface->SetPropertyInt(edgeList, edgeListIndex++, edge);
}
}
return edgeList;
}
std::vector JSI_Simulation::PickSimilarPlayerEntities(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& templateName, bool includeOffScreen, bool matchRank, bool allowFoundations)
{
return EntitySelection::PickSimilarEntities(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), templateName, g_Game->GetViewedPlayerID(), includeOffScreen, matchRank, false, allowFoundations);
}
JS::Value JSI_Simulation::GetAIs(ScriptInterface::CmptPrivate* pCmptPrivate)
{
return ICmpAIManager::GetAIs(*(pCmptPrivate->pScriptInterface));
}
void JSI_Simulation::SetBoundingBoxDebugOverlay(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), bool enabled)
{
ICmpSelectable::ms_EnableDebugOverlays = enabled;
}
void JSI_Simulation::RegisterScriptFunctions(const ScriptInterface& scriptInterface)
{
scriptInterface.RegisterFunction("GuiInterfaceCall");
scriptInterface.RegisterFunction("PostNetworkCommand");
scriptInterface.RegisterFunction("DumpSimState");
scriptInterface.RegisterFunction("GetAIs");
scriptInterface.RegisterFunction("PickEntityAtPoint");
scriptInterface.RegisterFunction, int, int, int, int, int, &PickPlayerEntitiesInRect>("PickPlayerEntitiesInRect");
scriptInterface.RegisterFunction, int, &PickPlayerEntitiesOnScreen>("PickPlayerEntitiesOnScreen");
scriptInterface.RegisterFunction, &PickNonGaiaEntitiesOnScreen>("PickNonGaiaEntitiesOnScreen");
scriptInterface.RegisterFunction, &GetEntitiesWithStaticObstructionOnScreen>("GetEntitiesWithStaticObstructionOnScreen");
scriptInterface.RegisterFunction("GetEdgesOfStaticObstructionsOnScreenNearTo");
scriptInterface.RegisterFunction, std::string, bool, bool, bool, &PickSimilarPlayerEntities>("PickSimilarPlayerEntities");
scriptInterface.RegisterFunction("SetBoundingBoxDebugOverlay");
}