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"); }