mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
feat: completed formation menu
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user