From d1d4116e6667629d576aadec5ae2ebda29337503 Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Fri, 24 Jan 2025 10:55:57 +0100 Subject: [PATCH] feat: Added unit spawn heading selection --- frontend/react/public/images/others/arrow.png | Bin 0 -> 5427 bytes frontend/react/public/images/others/arrow.svg | 39 +++++ .../public/images/others/arrow_background.png | Bin 0 -> 6144 bytes frontend/react/src/events.ts | 17 ++ frontend/react/src/interfaces.ts | 1 + frontend/react/src/map/map.ts | 19 ++- .../src/map/markers/stylesheets/units.css | 35 +++- .../src/map/markers/temporaryunitmarker.ts | 46 ++++++ frontend/react/src/other/utils.ts | 14 ++ .../src/ui/contextmenus/spawncontextmenu.tsx | 20 ++- frontend/react/src/ui/panels/audiomenu.tsx | 2 +- .../react/src/ui/panels/unitspawnmenu.tsx | 156 ++++++++++++++++-- 12 files changed, 330 insertions(+), 19 deletions(-) create mode 100644 frontend/react/public/images/others/arrow.png create mode 100644 frontend/react/public/images/others/arrow.svg create mode 100644 frontend/react/public/images/others/arrow_background.png diff --git a/frontend/react/public/images/others/arrow.png b/frontend/react/public/images/others/arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..be3b170805ce0c16b29a09a8b181fe4de351d957 GIT binary patch literal 5427 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 literal 0 HcmV?d00001 diff --git a/frontend/react/public/images/others/arrow.svg b/frontend/react/public/images/others/arrow.svg new file mode 100644 index 00000000..8ecc05ae --- /dev/null +++ b/frontend/react/public/images/others/arrow.svg @@ -0,0 +1,39 @@ + + + + + + + diff --git a/frontend/react/public/images/others/arrow_background.png b/frontend/react/public/images/others/arrow_background.png new file mode 100644 index 0000000000000000000000000000000000000000..3ea94954cd93a589bd707cb4f3ac144638c1c6a0 GIT binary patch literal 6144 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 void, singleShot = false) { + document.addEventListener( + this.name, + (ev: CustomEventInit) => { + callback(ev.detail.heading); + }, + { once: singleShot } + ); + } + + static dispatch(heading: number) { + document.dispatchEvent(new CustomEvent(this.name, { detail: { heading } })); + console.log(`Event ${this.name} dispatched`); + } +} + export class HotgroupsChangedEvent { static on(callback: (hotgroups: { [key: number]: Unit[] }) => void, singleShot = false) { document.addEventListener( diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts index 6b190c87..8648c956 100644 --- a/frontend/react/src/interfaces.ts +++ b/frontend/react/src/interfaces.ts @@ -142,6 +142,7 @@ export interface UnitSpawnTable { liveryID: string; altitude?: number; loadout?: string; + heading?: number; } export interface ObjectIconOptions { diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index b1c2cec7..531d6c46 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -56,6 +56,7 @@ import { SelectionEnabledChangedEvent, SessionDataLoadedEvent, SpawnContextMenuRequestEvent, + SpawnHeadingChangedEvent, StarredSpawnsChangedEvent, UnitDeselectedEvent, UnitSelectedEvent, @@ -144,6 +145,7 @@ export class Map extends L.Map { #temporaryMarkers: TemporaryUnitMarker[] = []; #currentSpawnMarker: TemporaryUnitMarker | null = null; #currentEffectMarker: ExplosionMarker | SmokeMarker | null = null; + #spawnHeading: number = 0; /* JTAC tools */ #ECHOPoint: TextMarker | null = null; @@ -565,6 +567,19 @@ export class Map extends L.Map { this.#currentSpawnMarker = this.addTemporaryMarker(spawnRequestTable.unit.location, spawnRequestTable.unit.unitType, spawnRequestTable.coalition, true); } + getSpawnRequestTable() { + return this.#spawnRequestTable; + } + + setSpawnHeading(heading: number) { + this.#spawnHeading = heading; + SpawnHeadingChangedEvent.dispatch(heading); + } + + getSpawnHeading() { + return this.#spawnHeading; + } + addStarredSpawnRequestTable(key, spawnRequestTable: SpawnRequestTable, quickAccessName: string) { this.#starredSpawnRequestTables[key] = spawnRequestTable; this.#starredSpawnRequestTables[key].quickAccessName = quickAccessName; @@ -833,7 +848,7 @@ export class Map extends L.Map { new L.LatLng(0, 0), this.#spawnRequestTable?.unit.unitType ?? "", this.#spawnRequestTable?.coalition ?? "neutral", - false + true ); this.#currentSpawnMarker.addTo(this); } else if (subState === SpawnSubState.SPAWN_EFFECT) { @@ -923,6 +938,7 @@ export class Map extends L.Map { if (getApp().getSubState() === SpawnSubState.SPAWN_UNIT) { if (this.#spawnRequestTable !== null) { this.#spawnRequestTable.unit.location = e.latlng; + this.#spawnRequestTable.unit.heading = deg2rad(this.#spawnHeading); getApp() .getUnitsManager() .spawnUnits( @@ -937,6 +953,7 @@ export class Map extends L.Map { e.latlng, this.#spawnRequestTable?.unit.unitType ?? "unknown", this.#spawnRequestTable?.coalition ?? "blue", + false, hash ); } diff --git a/frontend/react/src/map/markers/stylesheets/units.css b/frontend/react/src/map/markers/stylesheets/units.css index 93b8a5d6..13d326d9 100644 --- a/frontend/react/src/map/markers/stylesheets/units.css +++ b/frontend/react/src/map/markers/stylesheets/units.css @@ -421,7 +421,7 @@ color: var(--secondary-blue-text); } -[data-object|="unit"][data-coalition="blue"][data-is-selected] path { +[data-object|="unit"][data-coalition="blue"][data-is-selected] path:nth-child(1) { fill: var(--secondary-blue-text); } @@ -577,10 +577,41 @@ display: block; } -.ol-temporary-marker { +.ol-temporary-marker .unit-icon { opacity: 0.5; } +.ol-temporary-marker .unit-short-label { + opacity: 0.5; +} + +.ol-temporary-marker .heading-handle { + width: 30px; + height: 30px; + position: absolute; + top: -40px; + left: 50%; + transform: translateX(-50%); + cursor: move; + pointer-events: all; +} + +.ol-temporary-marker .heading-handle svg { + position: absolute; + width: 100%; +} + +.ol-temporary-marker [data-coalition="blue"] .heading-handle svg { + fill: var(--unit-background-blue); +} + +.ol-temporary-marker [data-coalition="red"] .heading-handle svg { + fill: var(--unit-background-red); +} + +.ol-temporary-marker [data-coalition="neutral"] .heading-handle svg { + fill: var(--unit-background-neutral); +} .unit-bullseye, .unit-braa { width: 50%; diff --git a/frontend/react/src/map/markers/temporaryunitmarker.ts b/frontend/react/src/map/markers/temporaryunitmarker.ts index c809aea0..87ad7703 100644 --- a/frontend/react/src/map/markers/temporaryunitmarker.ts +++ b/frontend/react/src/map/markers/temporaryunitmarker.ts @@ -3,6 +3,8 @@ import { DivIcon, LatLng } from "leaflet"; import { SVGInjector } from "@tanem/svg-injector"; import { getApp } from "../../olympusapp"; import { UnitBlueprint } from "../../interfaces"; +import { deg2rad, normalizeAngle, rad2deg } from "../../other/utils"; +import { SpawnHeadingChangedEvent } from "../../events"; export class TemporaryUnitMarker extends CustomMarker { #name: string; @@ -72,6 +74,50 @@ export class TemporaryUnitMarker extends CustomMarker { el.append(shortLabel); } + // Heading handle + if (this.#headingHandle) { + var handle = document.createElement("div"); + var handleImg = document.createElement("img"); + handleImg.src = "/images/others/arrow.svg"; + handleImg.onload = () => SVGInjector(handleImg); + handle.classList.add("heading-handle"); + el.append(handle); + + handle.append(handleImg); + + const rotateHandle = (heading) => { + el.style.transform = `rotate(${heading}deg)`; + unitIcon.style.transform = `rotate(-${heading}deg)`; + shortLabel.style.transform = `rotate(-${heading}deg)`; + }; + + SpawnHeadingChangedEvent.on((heading) => rotateHandle(heading)); + rotateHandle(getApp().getMap().getSpawnHeading()); + + // Add drag and rotate functionality + handle.addEventListener("mousedown", (e) => { + e.preventDefault(); + e.stopPropagation(); + + const onMouseMove = (e) => { + const rect = el.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + let angle = rad2deg(Math.atan2(e.clientY - centerY, e.clientX - centerX)) + 90; + angle = normalizeAngle(angle); + getApp().getMap().setSpawnHeading(angle); + }; + + const onMouseUp = () => { + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + }; + + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + }); + } + this.getElement()?.appendChild(el); this.getElement()?.classList.add("ol-temporary-marker"); } diff --git a/frontend/react/src/other/utils.ts b/frontend/react/src/other/utils.ts index 2c721da6..58a680ce 100644 --- a/frontend/react/src/other/utils.ts +++ b/frontend/react/src/other/utils.ts @@ -600,4 +600,18 @@ export function computeBrightness(color) { let brightness = 0.299 * r + 0.587 * g + 0.114 * b; return brightness; +} + +/** + * Normalizes an angle to be within the range of 0 to 360 degrees. + * @param {number} angle - The angle to normalize. + * @returns {number} - The normalized angle. + */ +export function normalizeAngle(angle: number): number { + // Ensure the angle is within the range of 0 to 360 degrees + angle = angle % 360; + if (angle < 0) { + angle += 360; + } + return angle; } \ No newline at end of file diff --git a/frontend/react/src/ui/contextmenus/spawncontextmenu.tsx b/frontend/react/src/ui/contextmenus/spawncontextmenu.tsx index 089727db..05245da0 100644 --- a/frontend/react/src/ui/contextmenus/spawncontextmenu.tsx +++ b/frontend/react/src/ui/contextmenus/spawncontextmenu.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import { BLUE_COMMANDER, colors, COMMAND_MODE_OPTIONS_DEFAULTS, GAME_MASTER, NO_SUBSTATE, OlympusState, OlympusSubState } from "../../constants/constants"; import { LatLng } from "leaflet"; import { @@ -10,7 +10,7 @@ import { } from "../../events"; import { getApp } from "../../olympusapp"; import { SpawnRequestTable, UnitBlueprint } from "../../interfaces"; -import { faArrowLeft, faEllipsisVertical, faExplosion, faListDots, faSearch, faSmog, faStar } from "@fortawesome/free-solid-svg-icons"; +import { faEllipsisVertical, faExplosion, faSearch, faSmog, faStar } from "@fortawesome/free-solid-svg-icons"; import { EffectSpawnMenu } from "../panels/effectspawnmenu"; import { UnitSpawnMenu } from "../panels/unitspawnmenu"; import { OlEffectListEntry } from "../components/oleffectlistentry"; @@ -28,6 +28,7 @@ import { OlDropdownItem } from "../components/oldropdown"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { OlCoalitionToggle } from "../components/olcoalitiontoggle"; import { Coalition } from "../../types/types"; +import { spawn } from "child_process"; enum CategoryGroup { NONE, @@ -62,6 +63,7 @@ export function SpawnContextMenu(props: {}) { const [spawnCoalition, setSpawnCoalition] = useState("blue" as Coalition); const [showMore, setShowMore] = useState(false); const [height, setHeight] = useState(0); + const [translated, setTranslated] = useState(false); useEffect(() => { if (selectedRole) setBlueprints(getApp()?.getUnitsManager().getDatabase().getByRole(selectedRole)); @@ -110,6 +112,19 @@ export function SpawnContextMenu(props: {}) { setSelectedRole(null); }, [openAccordion]); + const translateMenu = useCallback(() => { + if (blueprint && !translated) { + setTranslated(true); + setXPosition(xPosition + 60); + setYPosition(yPosition + 40); + } else if (!blueprint && translated) { + setTranslated(false); + setXPosition(xPosition - 60); + setYPosition(yPosition - 40); + } + }, [blueprint, translated]) + useEffect(translateMenu, [blueprint, translated]) + /* Filter the blueprints according to the label */ const filteredBlueprints: UnitBlueprint[] = []; if (blueprints && filterString !== "") { @@ -131,6 +146,7 @@ export function SpawnContextMenu(props: {}) { const containerPoint = getApp().getMap().latLngToContainerPoint(latlng); setXPosition(getApp().getMap().getContainer().offsetLeft + containerPoint.x); setYPosition(getApp().getMap().getContainer().offsetTop + containerPoint.y); + setTranslated(false); }); }, []); diff --git a/frontend/react/src/ui/panels/audiomenu.tsx b/frontend/react/src/ui/panels/audiomenu.tsx index e9f37c71..7adeb529 100644 --- a/frontend/react/src/ui/panels/audiomenu.tsx +++ b/frontend/react/src/ui/panels/audiomenu.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useRef, useState } from "react"; import { Menu } from "./components/menu"; import { getApp } from "../../olympusapp"; -import { FaPlus, FaPlusCircle, FaQuestionCircle } from "react-icons/fa"; +import { FaPlus, FaQuestionCircle } from "react-icons/fa"; import { AudioSourcePanel } from "./components/sourcepanel"; import { AudioSource } from "../../audio/audiosource"; import { RadioSinkPanel } from "./components/radiosinkpanel"; diff --git a/frontend/react/src/ui/panels/unitspawnmenu.tsx b/frontend/react/src/ui/panels/unitspawnmenu.tsx index 6aeb67b0..55226c76 100644 --- a/frontend/react/src/ui/panels/unitspawnmenu.tsx +++ b/frontend/react/src/ui/panels/unitspawnmenu.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useRef } from "react"; import { OlUnitSummary } from "../components/olunitsummary"; import { OlCoalitionToggle } from "../components/olcoalitiontoggle"; import { OlNumberInput } from "../components/olnumberinput"; @@ -9,7 +9,7 @@ import { LoadoutBlueprint, SpawnRequestTable, UnitBlueprint } from "../../interf import { OlStateButton } from "../components/olstatebutton"; import { Coalition } from "../../types/types"; import { getApp } from "../../olympusapp"; -import { deepCopyTable, ftToM, hash, mode } from "../../other/utils"; +import { deepCopyTable, deg2rad, ftToM, hash, mode, normalizeAngle } from "../../other/utils"; import { LatLng } from "leaflet"; import { Airbase } from "../../mission/airbase"; import { altitudeIncrements, groupUnitCount, maxAltitudeValues, minAltitudeValues, OlympusState, SpawnSubState } from "../../constants/constants"; @@ -17,8 +17,9 @@ import { faArrowLeft, faStar } from "@fortawesome/free-solid-svg-icons"; import { OlStringInput } from "../components/olstringinput"; import { countryCodes } from "../data/codes"; import { OlAccordion } from "../components/olaccordion"; -import { AppStateChangedEvent } from "../../events"; +import { AppStateChangedEvent, SpawnHeadingChangedEvent } from "../../events"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { FaQuestionCircle } from "react-icons/fa"; export function UnitSpawnMenu(props: { visible: boolean; @@ -123,6 +124,46 @@ export function UnitSpawnMenu(props: { if (props.coalition) setSpawnCoalition(props.coalition); }, [props.coalition]); + /* Heading compass */ + const [compassAngle, setCompassAngle] = useState(0); + const compassRef = useRef(null); + + const updateSpawnRequestTableHeading = useCallback(() => { + getApp()?.getMap().setSpawnHeading(compassAngle); + }, [compassAngle]); + useEffect(updateSpawnRequestTableHeading, [compassAngle]); + + useEffect(() => { + SpawnHeadingChangedEvent.on((heading) => { + setCompassAngle(heading); + }); + }, []); + + useEffect(() => { + setCompassAngle(getApp()?.getMap().getSpawnHeading() ?? 0); + }, [appState]); + + const handleMouseDown = (e: React.MouseEvent) => { + e.preventDefault(); + const onMouseMove = (e: MouseEvent) => { + if (compassRef.current) { + const rect = compassRef.current.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + const angle = Math.atan2(e.clientY - centerY, e.clientX - centerX) * (180 / Math.PI); + setCompassAngle(Math.round(normalizeAngle(angle + 90))); + } + }; + + const onMouseUp = () => { + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + }; + + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + }; + /* Get a list of all the roles */ const roles: string[] = []; props.blueprint?.loadouts?.forEach((loadout) => { @@ -198,9 +239,11 @@ export function UnitSpawnMenu(props: { /> { - if (spawnRequestTable) + if (spawnRequestTable) { + spawnRequestTable.unit.heading = compassAngle; if (key in props.starredSpawns) getApp().getMap().removeStarredSpawnRequestTable(key); else getApp().getMap().addStarredSpawnRequestTable(key, spawnRequestTable, quickAccessName); + } }} tooltip="Save this spawn for quick access" checked={key in props.starredSpawns} @@ -355,10 +398,9 @@ export function UnitSpawnMenu(props: { `} > {props.blueprint?.liveries && props.blueprint?.liveries[id].countries.length == 1 && ( - + )}
@@ -410,6 +452,48 @@ export function UnitSpawnMenu(props: {
+
+
+ Spawn heading +
+
Drag to change
+
+
+ + { + setCompassAngle(Number(ev.target.value)); + }} + onDecrease={() => { + setCompassAngle(normalizeAngle(compassAngle - 1)); + }} + onIncrease={() => { + setCompassAngle(normalizeAngle(compassAngle + 1)); + }} + value={compassAngle} + /> + +
+ + +
+
{ - if (spawnRequestTable) + if (spawnRequestTable){ + spawnRequestTable.unit.heading = deg2rad(compassAngle); getApp() .getUnitsManager() .spawnUnits( @@ -477,6 +562,7 @@ export function UnitSpawnMenu(props: { false, props.airbase?.getName() ?? undefined ); + } getApp().setState(OlympusState.IDLE); }} @@ -694,9 +780,10 @@ export function UnitSpawnMenu(props: { `} > {props.blueprint?.liveries && props.blueprint?.liveries[id].countries.length == 1 && ( - + )}
@@ -748,6 +835,48 @@ export function UnitSpawnMenu(props: { })}
+
+
+ Spawn heading +
+
Drag to change
+
+
+ + { + setCompassAngle(Number(ev.target.value)); + }} + onDecrease={() => { + setCompassAngle(normalizeAngle(compassAngle - 1)); + }} + onIncrease={() => { + setCompassAngle(normalizeAngle(compassAngle + 1)); + }} + value={compassAngle} + /> + +
+ + +
+
{spawnLoadout && spawnLoadout.items.length > 0 && (
{ - if (spawnRequestTable) + if (spawnRequestTable) { getApp() .getUnitsManager() .spawnUnits( @@ -812,6 +941,7 @@ export function UnitSpawnMenu(props: { false, props.airbase?.getName() ); + } }} > Spawn