feat: completed formation menu

This commit is contained in:
Davide Passoni
2025-01-20 15:07:58 +01:00
parent 9bd206a750
commit 5b6f58a38e
7 changed files with 278 additions and 199 deletions

View File

@@ -1,10 +1,10 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
export function Draggable(props: {
position: { x: number; y: number };
position: { x: number; y: number, z: number };
children: JSX.Element | JSX.Element[];
disabled: boolean;
onPositionChange: (position: { x: number; y: number }) => void;
onPositionChange: (position: { x: number; y: number, z: number }) => void;
}) {
const [dragging, setDragging] = useState(false);
const [refPosition, setRefPosition] = useState({ x: 0, y: 0 });
@@ -15,7 +15,7 @@ export function Draggable(props: {
e.stopPropagation();
e.preventDefault();
setRefPosition({ x: e.clientX, y: e.clientY });
if (!props.disabled) props.onPositionChange({ x: props.position.x + e.clientX - refPosition.x, y: props.position.y + e.clientY - refPosition.y });
if (!props.disabled) props.onPositionChange({ x: props.position.x + e.clientX - refPosition.x, y: props.position.y + e.clientY - refPosition.y, z: props.position.z });
}
},
[dragging, refPosition]

View File

@@ -1,32 +1,60 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Draggable } from "./draggable";
import { Unit } from "../../../unit/unit";
import { FaArrowDown, FaArrowUp } from "react-icons/fa";
import { nearestNiceNumber } from "../../../other/utils";
export function DraggableSilhouette(props: {
position: { x: number; y: number };
position: { x: number; y: number; z: number };
unit: Unit;
zoom: number;
scale: number;
disabled: boolean;
angle: number;
onPositionChange: (position: { x: number; y: number }) => void;
showVerticalOffset: boolean;
onPositionChange: (position: { x: number; y: number; z: number }) => void;
src?: string;
}) {
const imgHeight = Math.round((props.scale * (props.disabled ? 20 : (props.unit?.getBlueprint()?.length ?? 50))) / Math.min(3, props.disabled ? 1 : props.zoom));
return (
<Draggable position={props.position} onPositionChange={props.onPositionChange} disabled={props.disabled}>
<img
data-disabled = {props.disabled}
data-disabled={props.disabled}
className={`
align-center opacity-80 invert
data-[disabled=false]:cursor-move
`}
src={props.src ?? `./images/units/${props.unit?.getBlueprint()?.filename}`}
style={{
maxWidth: `${Math.round((props.scale * (props.disabled ? 20 : (props.unit?.getBlueprint()?.length ?? 50))) / Math.min(3, props.disabled? 1: props.zoom))}px`,
minWidth: `${Math.round((props.scale * (props.disabled ? 20 : (props.unit?.getBlueprint()?.length ?? 50))) / Math.min(3, props.disabled? 1: props.zoom))}px`,
rotate: `${props.disabled? props.angle: 90}deg`,
maxWidth: `${imgHeight}px`,
minWidth: `${imgHeight}px`,
rotate: `${props.disabled ? props.angle : 90}deg`,
}}
></img>
onWheel={(e) => {
e.stopPropagation();
let delta = nearestNiceNumber(Math.max(1, 0.1 * Math.abs(props.position.z)));
let newZ = props.position.z + (e.deltaY > 0 ? -delta : delta);
newZ = Math.round(newZ / delta) * delta;
props.onPositionChange({ x: props.position.x, y: props.position.y, z: newZ });
}}
/>
{props.showVerticalOffset ? (
<div className={`absolute flex w-full justify-center text-gray-400`}
style={{top: `calc(50% + ${imgHeight / 2}px)`}}>
<div className="flex gap-1">
{Math.round(props.position.z) > 0 ? (
<FaArrowUp className={`my-auto text-xs`} />
) : Math.round(props.position.z) < 0 ? (
<FaArrowDown className={`my-auto text-xs`} />
) : (
<></>
)}
{Math.round(Math.abs(props.position.z))} <div>ft</div>
</div>
</div>
) : (
<></>
)}
</Draggable>
);
}

View File

@@ -1,19 +1,21 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Unit } from "../../../unit/unit";
import { DraggableSilhouette } from "./draggablesilhouette";
import { FaCompressArrowsAlt, FaExclamationTriangle, FaExpand, FaExpandArrowsAlt } from "react-icons/fa";
import { FaArrowDown, FaArrowUp, FaCompressArrowsAlt, FaExclamationTriangle, FaExpand, FaExpandArrowsAlt, FaQuestionCircle } from "react-icons/fa";
import { OlToggle } from "../../components/oltoggle";
const FT_TO_PX = 1;
export function FormationCanvas(props: {
units: Unit[];
unitPositions: { x: number; y: number }[];
setUnitPositions: (positions: { x: number; y: number }[]) => void;
unitPositions: { x: number; y: number; z: number }[];
setUnitPositions: (positions: { x: number; y: number; z: number }[]) => void;
}) {
const [dragging, setDragging] = useState(false);
const [refPosition, setRefPosition] = useState({ x: 0, y: 0 });
const [dragDelta, setDragDelta] = useState({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1);
const [showVerticalOffset, setShowVerticalOffset] = useState(true);
/* Init references and hooks */
const containerRef = useRef(null);
@@ -79,55 +81,118 @@ export function FormationCanvas(props: {
return (
<>
<div className="flex w-fit gap-1 p-1">
<button
type="button"
onDoubleClick={(e) => {
e.stopPropagation();
}}
onClick={() => {
props.setUnitPositions(
props.unitPositions.map((position) => {
return {
x: position.x * 1.1,
y: position.y * 1.1,
};
})
);
}}
className={`
rounded-lg p-2 text-md flex content-center justify-center gap-2
bg-gray-600 font-medium text-white
hover:bg-gray-700
`}
>
<FaExpandArrowsAlt className="my-auto" /> <div> Loosen </div>
</button>
<button
type="button"
onDoubleClick={(e) => {
e.stopPropagation();
}}
onClick={() => {
props.setUnitPositions(
props.unitPositions.map((position) => {
return {
x: position.x * 0.9,
y: position.y * 0.9,
};
})
);
}}
className={`
rounded-lg p-2 text-md flex content-center justify-center gap-2
bg-gray-600 font-medium text-white
hover:bg-gray-700
`}
>
<FaCompressArrowsAlt className="my-auto" /> <div>Tighten</div>
</button>
<div className="flex">
<div className="flex w-fit p-1">
<button
type="button"
onDoubleClick={(e) => {
e.stopPropagation();
}}
onClick={() => {
props.setUnitPositions(
props.unitPositions.map((position) => {
return {
x: position.x * 1.1,
y: position.y * 1.1,
z: position.z,
};
})
);
}}
className={`
rounded-l-lg px-3 py-2 text-md flex content-center justify-center
gap-2 border-r-2 border-gray-400 bg-gray-600 font-medium
text-white
hover:bg-gray-700
`}
>
<FaExpandArrowsAlt className="my-auto" /> <div> Loose </div>
</button>
<button
type="button"
onDoubleClick={(e) => {
e.stopPropagation();
}}
onClick={() => {
props.setUnitPositions(
props.unitPositions.map((position) => {
return {
x: position.x * 0.9,
y: position.y * 0.9,
z: position.z,
};
})
);
}}
className={`
rounded-r-lg px-3 py-2 text-md flex content-center justify-center
gap-2 bg-gray-600 font-medium text-white
hover:bg-gray-700
`}
>
<FaCompressArrowsAlt className="my-auto" /> <div className="">Tight</div>
</button>
</div>
<div className="flex w-fit p-1">
<button
type="button"
onDoubleClick={(e) => {
e.stopPropagation();
}}
onClick={() => {
props.setUnitPositions(
props.unitPositions.map((position, idx) => {
const [dx, dz] = [-(props.unitPositions[idx].y - props.unitPositions[0].y), props.unitPositions[idx].x - props.unitPositions[0].x];
const distance = Math.sqrt(dx ** 2 + dz ** 2);
return {
x: position.x,
y: position.y,
z: position.z + 0.1 * distance,
};
})
);
}}
className={`
rounded-l-lg px-3 py-2 text-md flex content-center justify-center
gap-2 border-r-2 border-gray-400 bg-gray-600 font-medium
text-white
hover:bg-gray-700
`}
>
<FaArrowUp className="my-auto" /> <div> Up </div>
</button>
<button
type="button"
onDoubleClick={(e) => {
e.stopPropagation();
}}
onClick={() => {
props.setUnitPositions(
props.unitPositions.map((position, idx) => {
const [dx, dz] = [-(props.unitPositions[idx].y - props.unitPositions[0].y), props.unitPositions[idx].x - props.unitPositions[0].x];
const distance = Math.sqrt(dx ** 2 + dz ** 2);
return {
x: position.x,
y: position.y,
z: position.z - 0.1 * distance,
};
})
);
}}
className={`
rounded-r-lg px-3 py-2 text-md flex content-center justify-center
gap-2 bg-gray-600 font-medium text-white
hover:bg-gray-700
`}
>
<FaArrowDown className="my-auto" /> <div className="">Down</div>
</button>
</div>
</div>
<div className="flex justify-between"><div className="text-white">Show units vertical offset</div> <OlToggle onClick={() => {setShowVerticalOffset(!showVerticalOffset)}} toggled={showVerticalOffset} /></div>
<div
data-dragging={dragging}
className={`
@@ -164,8 +229,9 @@ export function FormationCanvas(props: {
(((props.unitPositions[idx].y - props.unitPositions[0].y) * 1) / zoom) * FT_TO_PX +
dragDelta.y +
containerCenter.y,
z: props.unitPositions[idx].z,
}
: { x: 0, y: 0 };
: { x: 0, y: 0, z: 0 };
let disabled = false;
let overflowX = null as null | string;
@@ -213,10 +279,13 @@ export function FormationCanvas(props: {
unit={unit}
scale={FT_TO_PX}
disabled={disabled}
onPositionChange={({ x, y }) => {
showVerticalOffset={showVerticalOffset}
onPositionChange={({ x, y, z }) => {
if (idx === 0) return;
props.unitPositions[idx] = {
x: ((x - props.unitPositions[0].x - dragDelta.x - containerCenter.x) * zoom) / FT_TO_PX - props.unitPositions[0].x,
y: ((y - props.unitPositions[0].y - dragDelta.y - containerCenter.y) * zoom) / FT_TO_PX - props.unitPositions[0].y,
z: z,
};
props.setUnitPositions([...props.unitPositions]);
}}
@@ -226,12 +295,12 @@ export function FormationCanvas(props: {
);
})}
</div>
{zoom > 3 && (
<div className="absolute bottom-2 left-2 flex gap-2">
<FaExclamationTriangle className={`text-xl text-yellow-400`} />
<div className="text-white">Silhouettes not to scale!</div>
<FaQuestionCircle className={`text-xl text-gray-400`} />
<div className="text-gray-400">Double click to reset view</div>
</div>
)}
<div className="absolute left-0 top-2 m-[-0.75rem] h-0">
<div
className={`
@@ -243,13 +312,9 @@ export function FormationCanvas(props: {
}}
>
{referenceDistance === 5280 && <div className="translate-y-[-8px]">1 NM</div>}
{referenceDistance === 5280 * 2 && (
<div
className={`translate-y-[-8px]`}
>
2 NM
</div>
)}
{referenceDistance === 5280 * 2 && <div className={`
translate-y-[-8px]
`}>2 NM</div>}
{referenceDistance < 5280 && <div className="translate-y-[-8px]">{referenceDistance} ft</div>}
</div>
</div>

View File

@@ -1,11 +1,10 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { Menu } from "./components/menu";
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
import { Unit } from "../../unit/unit";
import { OlRangeSlider } from "../components/olrangeslider";
import { FormationCreationRequestEvent } from "../../events";
import { computeStandardFormationOffset } from "../../other/utils";
import { formationTypes } from "../../constants/constants";
import { computeStandardFormationOffset, fromDCSFormationOffset, toDCSFormationOffset } from "../../other/utils";
import { formationTypes, UnitState } from "../../constants/constants";
import { FormationCanvas } from "./components/formationcanvas";
export function FormationMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
@@ -14,10 +13,11 @@ export function FormationMenu(props: { open: boolean; onClose: () => void; child
/* Init state variables */
const [formationType, setFormationType] = useState("echelon-lh");
const [verticalScale, setVerticalScale] = useState(30);
const [unitPositions, setUnitPositions] = useState([] as { x: number; y: number }[]);
const [unitPositions, setUnitPositions] = useState([] as { x: number; y: number, z: number }[]);
const verticalRatio = (verticalScale - 50) / 50;
useEffect(() => {
setFormationType("echelon-lh");
}, [props.open])
/* Listen for the setting of a new leader and wingmen and check if the formation is too big */
useEffect(() => {
@@ -40,6 +40,23 @@ export function FormationMenu(props: { open: boolean; onClose: () => void; child
}, [formationType]);
useEffect(setStandardFormation, [formationType]);
const setCurrentFormation = useCallback(() => {
if (
wingmen.every((unit: Unit) => {
return unit.getState() === UnitState.FOLLOW && unit.getLeader() === leader;
})
) {
setUnitPositions([
{ x: 0, y: 0, z: 0 },
...wingmen.map((unit, idx) => {
return fromDCSFormationOffset(unit.getFormationOffset());
}),
]);
setFormationType("custom");
}
}, [leader, wingmen]);
useEffect(setCurrentFormation, [leader, wingmen]);
if (leader && unitPositions.length < [leader, ...wingmen].length) {
/* If more units are added to the group keep the existing positions */
setUnitPositions(
@@ -120,18 +137,6 @@ export function FormationMenu(props: { open: boolean; onClose: () => void; child
Load
</button>
</div>
<span className="text-white">Vertical separation</span>
<div className="flex h-fit content-center gap-4">
<span className="ml-auto min-w-16 text-center text-sm text-white">Down</span>
<OlRangeSlider
className="my-auto"
value={verticalScale}
onChange={(ev) => {
setVerticalScale(Number(ev.target.value));
}}
/>
<span className="my-auto min-w-16 text-center text-sm text-white">Up</span>
</div>
<FormationCanvas
units={leader ? [leader, ...wingmen] : []}
unitPositions={unitPositions}
@@ -148,14 +153,13 @@ export function FormationMenu(props: { open: boolean; onClose: () => void; child
.filter((unit) => unit !== null)
.forEach((unit, idx) => {
if (idx != 0) {
const [dx, dz] = [-(unitPositions[idx].y - unitPositions[0].y), unitPositions[idx].x - unitPositions[0].x];
const distance = Math.sqrt(dx ** 2 + dz ** 2);
const offset = {
const [dx, dy] = [unitPositions[idx].x - unitPositions[0].x, unitPositions[idx].y - unitPositions[0].y];
unit.followUnit(leader.ID, toDCSFormationOffset({
x: dx,
y: distance * verticalRatio,
z: dz,
};
unit.followUnit(leader.ID, offset);
y: dy,
z: unitPositions[idx].z,
}));
}
});
}