Sort units spatially for freehand selection
Some checks are pending
checkrefs / lfscheck (push) Waiting to run
checkrefs / checkrefs (push) Waiting to run
lint / cppcheck (push) Waiting to run
lint / copyright (push) Waiting to run
lint / jenkinsfiles (push) Waiting to run
pre-commit / build (push) Waiting to run

This commit is contained in:
Atrik 2026-03-26 18:35:36 +01:00 committed by Phosit
parent 139b08fe6d
commit 91a37e32f6
2 changed files with 47 additions and 27 deletions

View file

@ -442,3 +442,17 @@ Vector3D.div = function(v, f)
{ {
return new Vector3D(v.x / f, v.y / f, v.z / f); return new Vector3D(v.x / f, v.y / f, v.z / f);
}; };
/**
* Linear interpolation between this vector and another.
* @param {Vector2D} v - The target vector.
* @param {number} t - Interpolation factor (0 = this, 1 = v).
* @returns {Vector2D} A new vector at the interpolated position.
*/
Vector2D.prototype.lerp = function(v, t)
{
return new Vector2D(
this.x + (v.x - this.x) * t,
this.y + (v.y - this.y) * t
);
};

View file

@ -1441,6 +1441,7 @@ function positionUnitsFreehandSelectionMouseUp(ev)
for (let i = 1; i < inputLine.length; ++i) for (let i = 1; i < inputLine.length; ++i)
lengthOfLine += inputLine[i].distanceTo(inputLine[i - 1]); lengthOfLine += inputLine[i].distanceTo(inputLine[i - 1]);
// Filter units that can move
const selection = g_Selection.filter(ent => !!GetEntityState(ent).unitAI).sort((a, b) => a - b); const selection = g_Selection.filter(ent => !!GetEntityState(ent).unitAI).sort((a, b) => a - b);
// Checking the line for a minimum length to save performance. // Checking the line for a minimum length to save performance.
@ -1450,39 +1451,43 @@ function positionUnitsFreehandSelectionMouseUp(ev)
return !!action && doAction(action, ev); return !!action && doAction(action, ev);
} }
// Even distribution of the units on the line. // Sort units by their projection onto the line direction (parallel sorting)
let p0 = inputLine[0]; const sortedSelection = sortEntitiesAlongLine(selection, inputLine[0], inputLine[inputLine.length - 1], true);
let entityDistribution = [p0];
const distanceBetweenEnts = lengthOfLine / (selection.length - 1);
let freeDist = -distanceBetweenEnts;
for (let i = 1; i < inputLine.length; ++i) const selectionCount = sortedSelection.length;
const targetPositions = [];
// Distribute units evenly along the polyline
for (let i = 0; i < selectionCount; ++i)
{ {
const p1 = inputLine[i]; const t = i / (selectionCount - 1);
freeDist += inputLine[i - 1].distanceTo(p1); const targetDistance = t * lengthOfLine;
while (freeDist >= 0) let accumulated = 0;
let targetPoint = inputLine[0];
for (let j = 1; j < inputLine.length; ++j)
{ {
p0 = Vector2D.sub(p0, p1).normalize().mult(freeDist).add(p1); const segStart = inputLine[j - 1];
entityDistribution.push(p0); const segEnd = inputLine[j];
freeDist -= distanceBetweenEnts; const segLength = segEnd.distanceTo(segStart);
if (accumulated + segLength >= targetDistance)
{
// Interpolate within this segment
const remaining = targetDistance - accumulated;
const frac = remaining / segLength;
targetPoint = segStart.lerp(segEnd, frac);
break;
}
accumulated += segLength;
} }
targetPositions.push(targetPoint);
} }
// Rounding errors can lead to missing or too many points.
entityDistribution = entityDistribution.slice(0, selection.length);
entityDistribution = entityDistribution.concat(new Array(selection.length - entityDistribution.length).fill(inputLine[inputLine.length - 1]));
if (Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[0]) +
Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[selection.length - 1]) >
Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[selection.length - 1]) +
Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[0]))
entityDistribution.reverse();
Engine.PostNetworkCommand({ Engine.PostNetworkCommand({
"type": isAttackMovePressed() ? "attack-walk-custom" : "walk-custom", "type": isAttackMovePressed() ? "attack-walk-custom" : "walk-custom",
"entities": selection, "entities": sortedSelection,
"targetPositions": entityDistribution.map(pos => pos.toFixed(2)), "targetPositions": targetPositions.map(pos => pos.toFixed(2)),
"targetClasses": Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] }, "targetClasses": Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] },
"queued": Engine.HotkeyIsPressed("session.queue"), "queued": Engine.HotkeyIsPressed("session.queue"),
"pushFront": Engine.HotkeyIsPressed("session.pushorderfront"), "pushFront": Engine.HotkeyIsPressed("session.pushorderfront"),
@ -1490,9 +1495,10 @@ function positionUnitsFreehandSelectionMouseUp(ev)
}); });
// Add target markers with a minimum distance of 5 to each other. // Add target markers with a minimum distance of 5 to each other.
const entitiesBetweenMarker = Math.ceil(5 / distanceBetweenEnts); const stepDistance = lengthOfLine / (selectionCount - 1);
for (let i = 0; i < entityDistribution.length; i += entitiesBetweenMarker) const entitiesBetweenMarker = Math.max(1, Math.ceil(5 / stepDistance));
DrawTargetMarker({ "x": entityDistribution[i].x, "z": entityDistribution[i].y }); for (let i = 0; i < targetPositions.length; i += entitiesBetweenMarker)
DrawTargetMarker({ "x": targetPositions[i].x, "z": targetPositions[i].y });
Engine.GuiInterfaceCall("PlaySound", { Engine.GuiInterfaceCall("PlaySound", {
"name": "order_walk", "name": "order_walk",