mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
Completed formation tool
This commit is contained in:
parent
e11f4a6c11
commit
b13689c09a
@ -44,8 +44,8 @@ export const useDrag = (props: { ref, initialPosition, count}) => {
|
||||
const [parentTop, parentLeft, parentWidth, parentHeight] = [parentRect.top, parentRect.left, parentRect.width, parentRect.height];
|
||||
|
||||
setFinalPosition({
|
||||
x: Math.max(width / 2, Math.min(mouseX - parentLeft, parentWidth - width / 2)),
|
||||
y: Math.max(height / 2, Math.min(mouseY - parentTop, parentHeight - height / 2)),
|
||||
x: Math.round(Math.max(width / 2, Math.min(mouseX - parentLeft, parentWidth - width / 2)) / 10) * 10,
|
||||
y: Math.round(Math.max(height / 2, Math.min(mouseY - parentTop, parentHeight - height / 2)) / 10) * 10,
|
||||
});
|
||||
},
|
||||
[isDragging, props.ref]
|
||||
|
||||
@ -41,7 +41,7 @@ export function Menu(props: {
|
||||
>
|
||||
<h5
|
||||
className={`
|
||||
inline-flex w-full items-center px-5 py-3 pb-2 font-semibold
|
||||
inline-flex h-12 w-full items-center px-5 py-3 pb-2 font-semibold
|
||||
text-gray-800 shadow-lg
|
||||
dark:text-gray-400
|
||||
`}
|
||||
@ -51,7 +51,7 @@ export function Menu(props: {
|
||||
onClick={props.onBack ?? (() => {})}
|
||||
icon={faArrowLeft}
|
||||
className={`
|
||||
mr-1 cursor-pointer rounded-md p-2
|
||||
mr-1 h-8 cursor-pointer rounded-md p-2
|
||||
dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-white
|
||||
`}
|
||||
/>
|
||||
@ -68,7 +68,9 @@ export function Menu(props: {
|
||||
`}
|
||||
/>
|
||||
</h5>
|
||||
<div className="h-[calc(100%-3rem)]">
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
{props.canBeHidden == true && (
|
||||
<div
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Menu } from "./components/menu";
|
||||
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
|
||||
import { useDrag } from "../libs/useDrag";
|
||||
@ -12,23 +12,39 @@ export function FormationMenu(props: {
|
||||
wingmen: Unit[] | null;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
}) {
|
||||
/* The useDrag custom hook used to handle the dragging of the units requires that the number of hooks remains unchanged.
|
||||
The units array is therefore initialized to 128 units maximum. */
|
||||
let units = Array(128).fill(null) as (Unit | null)[];
|
||||
units[0] = props.leader;
|
||||
props.wingmen?.forEach((unit, idx) => {
|
||||
if (idx < units.length) units[idx + 1] = unit;
|
||||
});
|
||||
|
||||
/* Init state variables */
|
||||
const [formationType, setFormationType] = useState("echelon-lh");
|
||||
const [horizontalScale, setHorizontalScale] = useState(0);
|
||||
const [verticalScale, setVerticalScale] = useState(30);
|
||||
const [offsets, setOffsets] = useState(
|
||||
units.map((unit, idx) => {
|
||||
return computeFormationOffset(formationType, idx);
|
||||
})
|
||||
);
|
||||
|
||||
/* The count state is used to force the reset of the initial position of the silhouettes */
|
||||
// TODO it works but I don't like it, it feels like a hack
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
let units = Array(128).fill(null) as (Unit | null)[];
|
||||
units[0] = props.leader;
|
||||
props.wingmen?.forEach((unit, idx) => (units[idx + 1] = unit));
|
||||
|
||||
/* Init references and hooks */
|
||||
const containerRef = useRef(null);
|
||||
const silhouetteReferences = units.map((unit) => useRef(null));
|
||||
const silhouetteHandles = units.map((unit, idx) => {
|
||||
let offset = computeFormationOffset(formationType, idx);
|
||||
const scrollRef = useRef(null);
|
||||
const silhouetteReferences = units.map((_) => useRef(null));
|
||||
const silhouetteHandles = units.map((_, idx) => {
|
||||
/* Set the initial position of the unit to be centered in the drawing canvas, depending on the currently loaded formation */
|
||||
let offset = offsets[idx] ?? { x: 0, y: 0, z: 0 };
|
||||
let center = { x: 0, y: 0 };
|
||||
if (containerRef.current) {
|
||||
center.x = (containerRef.current as HTMLDivElement).getBoundingClientRect().width / 2;
|
||||
center.y = 150;
|
||||
center.y = (containerRef.current as HTMLDivElement).getBoundingClientRect().height / 2;
|
||||
}
|
||||
return useDrag({
|
||||
ref: silhouetteReferences[idx],
|
||||
@ -37,53 +53,168 @@ export function FormationMenu(props: {
|
||||
});
|
||||
});
|
||||
|
||||
let formationTypes = {
|
||||
"echelon-lh": "Echelon left",
|
||||
"echelon-rh": "Echelon right",
|
||||
"line-abreast-rh": "Line abreast right",
|
||||
"line-abreast-lh": "Line abreast left",
|
||||
trail: "Trail",
|
||||
front: "Front",
|
||||
diamond: "Diamond",
|
||||
};
|
||||
/* When the formation type is changed, reset the position to the center and the position of the silhouettes depending on the aircraft */
|
||||
useEffect(() => {
|
||||
if (scrollRef.current && containerRef.current) {
|
||||
const containerDiv = containerRef.current as HTMLDivElement;
|
||||
const scrollDiv = scrollRef.current as HTMLDivElement;
|
||||
scrollDiv.scrollTop = (containerDiv.clientHeight - scrollDiv.clientHeight) / 2 + 150;
|
||||
scrollDiv.scrollLeft = (containerDiv.clientWidth - scrollDiv.clientWidth) / 2;
|
||||
}
|
||||
|
||||
if (formationType !== "custom") {
|
||||
setOffsets(
|
||||
units.map((unit, idx) => {
|
||||
return computeFormationOffset(formationType, idx);
|
||||
})
|
||||
);
|
||||
setCount(count + 1);
|
||||
}
|
||||
}, [formationType]);
|
||||
|
||||
const horizontalRatio = 1 + (horizontalScale / 100) ** 2 * 100;
|
||||
const verticalRatio = (verticalScale - 50) / 50;
|
||||
|
||||
let referenceDistance = 200 * horizontalRatio;
|
||||
if (referenceDistance < 250) {
|
||||
referenceDistance = 100;
|
||||
} else if (referenceDistance < 500) {
|
||||
referenceDistance = 250;
|
||||
} else if (referenceDistance < 1000) {
|
||||
referenceDistance = 500;
|
||||
} else if (referenceDistance < 3000) {
|
||||
referenceDistance = 1000;
|
||||
} else if (referenceDistance < 10000) {
|
||||
referenceDistance = 5000;
|
||||
} else {
|
||||
referenceDistance = 10000;
|
||||
}
|
||||
|
||||
const referenceWidth = referenceDistance / horizontalRatio;
|
||||
|
||||
return (
|
||||
<Menu title="Formation menu" open={props.open} showBackButton={false} onClose={props.onClose}>
|
||||
<div className="flex h-full flex-col gap-4 p-4">
|
||||
<span className="text-white">Formation type presets</span>
|
||||
<OlDropdown label={formationTypes[formationType]}>
|
||||
{Object.keys(formationTypes).map((optionFormationType) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
onClick={() => {
|
||||
setCount(count + 1);
|
||||
setFormationType(optionFormationType);
|
||||
}}
|
||||
>
|
||||
{formationTypes[optionFormationType]}
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
<div className="flex">
|
||||
<span>Parade</span>
|
||||
<div className="flex gap-2">
|
||||
<OlDropdown label={formationTypes[formationType]} className="w-full">
|
||||
{Object.keys(formationTypes)
|
||||
.filter((type) => type !== "custom")
|
||||
.map((optionFormationType) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
onClick={() => {
|
||||
setCount(count + 1);
|
||||
setFormationType(optionFormationType);
|
||||
}}
|
||||
>
|
||||
{formationTypes[optionFormationType]}
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
let content = JSON.stringify(
|
||||
units
|
||||
.filter((unit) => unit !== null)
|
||||
.map((unit, idx) => {
|
||||
if (units.length > 0 && units[0] !== null) {
|
||||
const [dx, dz] = [
|
||||
-(silhouetteHandles[idx].position.y - silhouetteHandles[0].position.y),
|
||||
silhouetteHandles[idx].position.x - silhouetteHandles[0].position.x,
|
||||
];
|
||||
const distance = Math.sqrt(dx ** 2 + dz ** 2);
|
||||
const offset = {
|
||||
x: dx * horizontalRatio,
|
||||
y: distance * verticalRatio,
|
||||
z: dz * horizontalRatio,
|
||||
};
|
||||
return offset;
|
||||
}
|
||||
})
|
||||
);
|
||||
var a = document.createElement("a");
|
||||
var file = new Blob([content], { type: "text/plain" });
|
||||
a.href = URL.createObjectURL(file);
|
||||
a.download = "formation.json";
|
||||
a.click();
|
||||
}}
|
||||
className={`
|
||||
mb-2 rounded-lg bg-blue-700 px-5 py-2.5 text-md font-medium
|
||||
text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
var input = document.createElement("input");
|
||||
input.type = "file";
|
||||
|
||||
input.onchange = async (e) => {
|
||||
// @ts-ignore TODO
|
||||
var file = e.target?.files[0];
|
||||
var reader = new FileReader();
|
||||
reader.readAsText(file, "UTF-8");
|
||||
reader.onload = (readerEvent) => {
|
||||
// @ts-ignore TODO
|
||||
var content = readerEvent.target.result;
|
||||
if (content) {
|
||||
setOffsets(JSON.parse(content.toString()));
|
||||
setCount(count + 1);
|
||||
setFormationType("custom");
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
input.click();
|
||||
}}
|
||||
className={`
|
||||
mb-2 rounded-lg bg-blue-700 px-5 py-2.5 text-md font-medium
|
||||
text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Load
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-white">Formation distance</span>
|
||||
<div className="flex h-fit content-center gap-4">
|
||||
<span
|
||||
className={`
|
||||
my-auto min-w-16 text-center align-middle text-sm text-white
|
||||
`}
|
||||
>
|
||||
Parade
|
||||
</span>
|
||||
<OlRangeSlider
|
||||
className="my-auto"
|
||||
value={horizontalScale}
|
||||
onChange={(ev) => {
|
||||
setHorizontalScale(Number(ev.target.value));
|
||||
}}
|
||||
/>
|
||||
<span>Tactical</span>
|
||||
<span className="my-auto min-w-16 text-center text-sm text-white">Tactical</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span>Down</span>
|
||||
<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>Up</span>
|
||||
<span className="my-auto min-w-16 text-center text-sm text-white">Up</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@ -92,7 +223,7 @@ export function FormationMenu(props: {
|
||||
|
||||
if (containerRef.current) {
|
||||
center.x = (containerRef.current as HTMLDivElement).getBoundingClientRect().width / 2;
|
||||
center.y = 150;
|
||||
center.y = (containerRef.current as HTMLDivElement).getBoundingClientRect().height / 2;
|
||||
}
|
||||
|
||||
units
|
||||
@ -100,11 +231,10 @@ export function FormationMenu(props: {
|
||||
.forEach((unit, idx) => {
|
||||
if (units.length > 0 && units[0] !== null && idx != 0) {
|
||||
const ID = units[0].ID;
|
||||
const horizontalRatio = 1 + horizontalScale;
|
||||
const verticalRatio = (verticalScale - 50) / 50;
|
||||
|
||||
const [dx, dz] = [
|
||||
-(silhouetteHandles[idx].position.y - silhouetteHandles[0].position.y),
|
||||
silhouetteHandles[idx].position.x - silhouetteHandles[0].position.x
|
||||
silhouetteHandles[idx].position.x - silhouetteHandles[0].position.x,
|
||||
];
|
||||
const distance = Math.sqrt(dx ** 2 + dz ** 2);
|
||||
const offset = {
|
||||
@ -126,40 +256,94 @@ export function FormationMenu(props: {
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
<div className="relative m-[-0.75rem] h-0">
|
||||
<div
|
||||
className={`
|
||||
relative left-6 top-4 h-4 border-2 border-white
|
||||
border-t-transparent text-center text-white
|
||||
`}
|
||||
style={{
|
||||
width: `${referenceWidth}px`,
|
||||
}}
|
||||
><div className="translate-y-[-8px]">{referenceDistance}ft</div></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`
|
||||
relative h-full w-full rounded-md border-[1px] border-white/20
|
||||
bg-white/10
|
||||
relative h-full w-full overflow-scroll rounded-md border-[1px]
|
||||
border-white/20 bg-white/10
|
||||
`}
|
||||
ref={containerRef}
|
||||
ref={scrollRef}
|
||||
>
|
||||
<>
|
||||
{units.map((unit, idx) => {
|
||||
return (
|
||||
<div
|
||||
key={`${count}-${idx}`}
|
||||
className={`
|
||||
absolute
|
||||
${unit ? "" : "hidden"}
|
||||
`}
|
||||
ref={silhouetteReferences[idx]}
|
||||
style={{
|
||||
top: silhouetteHandles[idx].position.y,
|
||||
left: silhouetteHandles[idx].position.x,
|
||||
}}
|
||||
onMouseDown={silhouetteHandles[idx].handleMouseDown}
|
||||
>
|
||||
<img
|
||||
<div className={`h-[1000px] w-[1000px] h-max-[1000px] w-max-[1000px]`} ref={containerRef}>
|
||||
<>
|
||||
{Array(100)
|
||||
.fill(0)
|
||||
.map((_, idx) => {
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={`
|
||||
absolute top-0 h-[1000px] w-[1px] border-[1px]
|
||||
border-white/10
|
||||
`}
|
||||
style={{
|
||||
left: `${idx * 10}px`,
|
||||
}}
|
||||
></div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
<>
|
||||
{Array(100)
|
||||
.fill(0)
|
||||
.map((_, idx) => {
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={`
|
||||
absolute left-0 h-[1px] w-[1000px] border-[1px]
|
||||
border-white/5
|
||||
`}
|
||||
style={{
|
||||
top: `${idx * 10}px`,
|
||||
}}
|
||||
></div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
<>
|
||||
{units.map((unit, idx) => {
|
||||
return (
|
||||
<div
|
||||
key={`${count}-${idx}`}
|
||||
className={`
|
||||
h-10 min-h-10 w-10 min-w-10 translate-x-[-50%]
|
||||
translate-y-[-50%] rotate-90 opacity-80 invert
|
||||
absolute
|
||||
${unit ? "" : "hidden"}
|
||||
`}
|
||||
src="public\images\units\general1.png"
|
||||
></img>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
ref={silhouetteReferences[idx]}
|
||||
style={{
|
||||
top: silhouetteHandles[idx].position.y,
|
||||
left: silhouetteHandles[idx].position.x,
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
silhouetteHandles[idx].handleMouseDown(e);
|
||||
setFormationType("custom");
|
||||
}}
|
||||
>
|
||||
<img
|
||||
className={`
|
||||
h-10 min-h-10 w-10 min-w-10 translate-x-[-50%]
|
||||
translate-y-[-50%] rotate-90 cursor-move opacity-80
|
||||
invert
|
||||
`}
|
||||
src={`public/images/units/${unit?.getBlueprint()?.filename}`}
|
||||
></img>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Menu>
|
||||
@ -167,30 +351,24 @@ export function FormationMenu(props: {
|
||||
}
|
||||
|
||||
function computeFormationOffset(formation, idx) {
|
||||
let offset = { x: 0, y: 0, z: 0 };
|
||||
let offset = { x: 0, z: 0 };
|
||||
if (formation === "trail") {
|
||||
offset.x = -50 * idx;
|
||||
offset.y = -30 * idx;
|
||||
offset.z = 0;
|
||||
} else if (formation === "echelon-lh") {
|
||||
offset.x = -50 * idx;
|
||||
offset.y = -10 * idx;
|
||||
offset.z = -50 * idx;
|
||||
} else if (formation === "echelon-rh") {
|
||||
offset.x = -50 * idx;
|
||||
offset.y = -10 * idx;
|
||||
offset.z = 50 * idx;
|
||||
} else if (formation === "line-abreast-lh") {
|
||||
offset.x = 0;
|
||||
offset.y = 0;
|
||||
offset.z = -50 * idx;
|
||||
} else if (formation === "line-abreast-rh") {
|
||||
offset.x = 0;
|
||||
offset.y = 0;
|
||||
offset.z = 50 * idx;
|
||||
} else if (formation === "front") {
|
||||
offset.x = 100 * idx;
|
||||
offset.y = 0;
|
||||
offset.z = 0;
|
||||
} else if (formation === "diamond") {
|
||||
var xr = 0;
|
||||
@ -201,7 +379,7 @@ function computeFormationOffset(formation, idx) {
|
||||
for (let i = 0; i < idx; i++) {
|
||||
var xl = xr * Math.cos(Math.PI / 4) - yr * Math.sin(Math.PI / 4);
|
||||
var yl = xr * Math.sin(Math.PI / 4) + yr * Math.cos(Math.PI / 4);
|
||||
offset = { x: -yl * 50, y: zr * 10, z: xl * 50 };
|
||||
offset = { x: -yl * 50, z: xl * 50 };
|
||||
if (yr == 0) {
|
||||
layer++;
|
||||
xr = 0;
|
||||
@ -220,3 +398,14 @@ function computeFormationOffset(formation, idx) {
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
let formationTypes = {
|
||||
"echelon-lh": "Echelon left",
|
||||
"echelon-rh": "Echelon right",
|
||||
"line-abreast-rh": "Line abreast right",
|
||||
"line-abreast-lh": "Line abreast left",
|
||||
trail: "Trail",
|
||||
front: "Front",
|
||||
diamond: "Diamond",
|
||||
custom: "Custom",
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user