Index: binaries/data/config/default.cfg =================================================================== --- binaries/data/config/default.cfg +++ binaries/data/config/default.cfg @@ -313,6 +313,7 @@ deselectgroup = Ctrl ; Modifier to deselect units when clicking group icon, instead of selecting rotate.cw = RightBracket ; Rotate building placement preview clockwise rotate.ccw = LeftBracket ; Rotate building placement preview anticlockwise +snaptoedge = Shift ; Snap building placement by edge [hotkey.session.gui] toggle = "Alt+G" ; Toggle visibility of session GUI Index: binaries/data/mods/public/gui/session/input.js =================================================================== --- binaries/data/mods/public/gui/session/input.js +++ binaries/data/mods/public/gui/session/input.js @@ -720,10 +720,13 @@ placementSupport.SetDefaultAngle(); } + var snapToEdge = Engine.HotkeyIsPressed("session.snaptoedge"); var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { "template": placementSupport.template, "x": placementSupport.position.x, - "z": placementSupport.position.z + "z": placementSupport.position.z, + "angle": placementSupport.angle, + "snapToEdgeEntities": snapToEdge && Engine.PickPlayerEntitiesOnScreen(g_ViewedPlayer) }); if (snapData) { @@ -1060,10 +1063,12 @@ return true; } + var snapToEdge = Engine.HotkeyIsPressed("session.snaptoedge"); var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { "template": placementSupport.template, "x": placementSupport.position.x, "z": placementSupport.position.z, + "snapToEdgeEntities": snapToEdge && Engine.PickPlayerEntitiesOnScreen(g_ViewedPlayer) }); if (snapData) { @@ -1088,6 +1093,24 @@ else { placementSupport.position = Engine.GetTerrainAtScreenPoint(ev.x, ev.y); + + var snapToEdge = Engine.HotkeyIsPressed("session.snaptoedge"); + if (snapToEdge) + { + var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { + "template": placementSupport.template, + "x": placementSupport.position.x, + "z": placementSupport.position.z, + "snapToEdgeEntities": Engine.PickPlayerEntitiesOnScreen(g_ViewedPlayer) + }); + if (snapData) + { + placementSupport.angle = snapData.angle; + placementSupport.position.x = snapData.x; + placementSupport.position.z = snapData.z; + } + } + g_DragStart = new Vector2D(ev.x, ev.y); inputState = INPUT_BUILDING_CLICK; } Index: binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- binaries/data/mods/public/simulation/components/GuiInterface.js +++ binaries/data/mods/public/simulation/components/GuiInterface.js @@ -1641,6 +1641,149 @@ return minDistEntitySnapData; } + if (data.snapToEdgeEntities && template.Obstruction && template.Obstruction.Static) + { + let width = template.Obstruction.Static["@depth"] / 2; + let depth = template.Obstruction.Static["@width"] / 2; + const maxSide = Math.max(width, depth); + let templatePos = new Vector2D(data.x, data.z); + let templateAngle = data.angle ? data.angle : 0.0; + + // Get edges of all entities together. + let allEdges = []; + for (let ent of data.snapToEdgeEntities) + allEdges = allEdges.concat(GetObstructionEdges(ent)); + const minimalDistanceToSnap = 5.0; + const getValidEdges = (allEdges, position) => { + let edges = []; + for (let edge of allEdges) + { + let signedDistance = edge.normal.dot(position) - edge.normal.dot(edge.begin); + // Negative signed distance means that the template position + // is lying behind the edge. + if (signedDistance < -minimalDistanceToSnap - maxSide || + signedDistance > minimalDistanceToSnap + maxSide) + continue; + let dir1 = edge.begin.clone().sub(edge.end).normalize(); + let dir2 = new Vector2D(-dir1.x, -dir1.y); + let offsetDistance = Math.max( + dir1.dot(position) - dir1.dot(edge.begin), + dir2.dot(position) - dir2.dot(edge.end) + ); + if (offsetDistance > 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.0) + offsetDistance = 0.0; + edge.signedDistance = signedDistance; + edge.offsetDistance = offsetDistance; + edges.push(edge); + } + return edges; + } + + let edges = getValidEdges(allEdges, templatePos); + if (edges.length) + { + // 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. + const compare = (a, b) => { + const EPS = 1e-3; + const behindA = a.signedDistance < -EPS; + const behindB = b.signedDistance < -EPS; + const scoreA = Math.abs(a.signedDistance) + a.offsetDistance; + const scoreB = Math.abs(b.signedDistance) + b.offsetDistance; + if (Math.abs(scoreA - scoreB) < EPS) + { + if (behindA != behindB) + return behindA - behindB; + if (!behindA) + return a.offsetDistance - b.offsetDistance; + else + return -a.signedDistance - -b.signedDistance; + } + return scoreA - scoreB; + }; + + let baseEdge = edges[0]; + for (let edge of edges) + if (compare(edge, baseEdge) < 0.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 EPS = 1e-3; + const angleCandidate = baseEdge.angle + dir * Math.PI / 2.0; + // We need to find a minimal angle difference. + let difference = Math.abs(angleCandidate - templateAngle); + difference = Math.min(difference, Math.PI * 2.0 - difference); + if (difference < Math.PI / 4.0 + EPS) + { + // We need to swap sides for orthogonal cases. + if (dir % 2 == 0) + [width, depth] = [depth, width]; + templateAngle = angleCandidate; + break; + } + } + + // We need a small padding to avoid unnecessary collisions + // because of loss of accuracy. + const 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.0 : snapPadding; + }; + + let distance = baseEdge.normal.dot(templatePos) - baseEdge.normal.dot(baseEdge.begin); + templatePos.sub(Vector2D.mult(baseEdge.normal, distance - width - getPadding(baseEdge))); + edges = getValidEdges(allEdges, templatePos); + if (edges.length > 1) + { + let pairedEdges = []; + for (let edge of edges) + { + const EPS = 1e-3; + // We have to place a rectangle, so the angle between + // edges should be 90 degrees. + if (Math.abs(baseEdge.normal.dot(edge.normal)) > EPS) + continue; + let newEdge = { + 'begin': edge.end, + 'end': edge.begin, + 'normal': new Vector2D(-edge.normal.x, -edge.normal.y), + 'signedDistance': -edge.signedDistance, + 'offsetDistance': edge.offsetDistance, + 'order': 'ccw', + }; + pairedEdges.push(edge); + pairedEdges.push(newEdge); + } + pairedEdges.sort(compare); + if (pairedEdges.length) + { + let secondEdge = pairedEdges[0]; + for (let edge of pairedEdges) + if (compare(edge, secondEdge) < 0.0) + secondEdge = edge; + let distance = secondEdge.normal.dot(templatePos) - secondEdge.normal.dot(secondEdge.begin); + templatePos.sub(Vector2D.mult(secondEdge.normal, distance - depth - getPadding(secondEdge))); + } + } + return { + "x": templatePos.x, + "z": templatePos.y, + "angle": templateAngle + }; + } + } + if (template.BuildRestrictions.PlacementType == "shore") { let angle = GetDockAngle(template, data.x, data.z); Index: binaries/data/mods/public/simulation/helpers/Placement.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/helpers/Placement.js @@ -0,0 +1,39 @@ +function GetObstructionEdges(entity) +{ + let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); + let template = cmpTemplateManager.GetTemplate(cmpTemplateManager.GetCurrentTemplateName(entity)); + if (!template || !template.Obstruction || !template.Obstruction.Static) + return []; + let cmpPosition = Engine.QueryInterface(entity, IID_Position); + if (!cmpPosition) + return []; + + let halfWidth = template.Obstruction.Static["@width"] / 2; + let halfDepth = template.Obstruction.Static["@depth"] / 2; + + // All corners of a foundation in clockwise order. + let corners = [ + new Vector2D(-halfWidth, -halfDepth), + new Vector2D(-halfWidth, halfDepth), + new Vector2D(halfWidth, halfDepth), + new Vector2D(halfWidth, -halfDepth) + ]; + + let angle = cmpPosition.GetRotation().y; + for (let i = 0; i < 4; ++i) + corners[i].rotate(angle).add(cmpPosition.GetPosition2D()); + + let edges = []; + for (let i = 0; i < 4; ++i) + edges.push({ + 'entity': entity, + 'begin': corners[i], + 'end': corners[(i + 1) % 4], + 'angle': angle, + 'normal': corners[(i + 1) % 4].clone().sub(corners[i]).perpendicular().normalize(), + 'order': 'cw', + }); + return edges; +} + +Engine.RegisterGlobal("GetObstructionEdges", GetObstructionEdges);