diff --git a/src/components/editor/CourtPlayer.tsx b/src/components/editor/CourtPlayer.tsx index d85e51f..c25b36a 100644 --- a/src/components/editor/CourtPlayer.tsx +++ b/src/components/editor/CourtPlayer.tsx @@ -1,4 +1,10 @@ -import React, { KeyboardEventHandler, ReactNode, RefObject, useCallback, useRef } from "react" +import React, { + KeyboardEventHandler, + ReactNode, + RefObject, + useCallback, + useRef, +} from "react" import "../../style/player.css" import Draggable from "react-draggable" import { PlayerPiece } from "./PlayerPiece" @@ -22,11 +28,10 @@ const MOVE_AREA_SENSIBILITY = 0.001 export const PLAYER_RADIUS_PIXELS = 20 export function CourtPlayer({ - playerInfo, - className, - availableActions, - }: CourtPlayerProps) { - + playerInfo, + className, + availableActions, +}: CourtPlayerProps) { const pieceRef = useRef(null) return courtPlayerPiece({ @@ -41,18 +46,17 @@ export function CourtPlayer({ * A player that is placed on the court, which can be selected, and moved in the associated bounds * */ export function EditableCourtPlayer({ - playerInfo, - className, - courtRef, - - onPositionValidated, - onRemove, - availableActions, - }: EditableCourtPlayerProps) { + playerInfo, + className, + courtRef, + + onPositionValidated, + onRemove, + availableActions, +}: EditableCourtPlayerProps) { const pieceRef = useRef(null) const { x, y } = playerInfo.pos - return ( = MOVE_AREA_SENSIBILITY || Math.abs(pos.y - y) >= MOVE_AREA_SENSIBILITY - ) + ) { onPositionValidated(pos) + } }, [courtRef, onPositionValidated, x, y])}> - {courtPlayerPiece({ playerInfo, className, @@ -87,23 +91,24 @@ export function EditableCourtPlayer({ ) } -interface CourtPlayerPieceProps extends CourtPlayerProps { +interface CourtPlayerPieceProps { + playerInfo: PlayerInfo + className?: string pieceRef?: RefObject availableActions?: () => ReactNode[] onKeyUp?: KeyboardEventHandler } function courtPlayerPiece({ - playerInfo, - className, - pieceRef, - onKeyUp, - availableActions, - }: CourtPlayerPieceProps) { + playerInfo, + className, + pieceRef, + onKeyUp, + availableActions, +}: CourtPlayerPieceProps) { const usesBall = playerInfo.ballState != BallState.NONE const { x, y } = playerInfo.pos - return (
-
- { - availableActions && ( -
- {availableActions()} -
- ) - } +
+ {availableActions && ( +
{availableActions()}
+ )}
) -} \ No newline at end of file +} diff --git a/src/editor/TacticContentDomains.ts b/src/editor/TacticContentDomains.ts index 15191a8..28ab879 100644 --- a/src/editor/TacticContentDomains.ts +++ b/src/editor/TacticContentDomains.ts @@ -1,12 +1,33 @@ import { equals, Pos, ratioWithinBase } from "../geo/Pos" -import { BallState, Player, PlayerInfo, PlayerLike, PlayerPhantom, PlayerTeam } from "../model/tactic/Player" -import { Ball, BALL_ID, BALL_TYPE, CourtObject } from "../model/tactic/CourtObjects" -import { ComponentId, StepContent, TacticComponent } from "../model/tactic/Tactic" +import { + BallState, + Player, + PlayerInfo, + PlayerLike, + PlayerPhantom, + PlayerTeam, +} from "../model/tactic/Player" +import { + Ball, + BALL_ID, + BALL_TYPE, + CourtObject, +} from "../model/tactic/CourtObjects" +import { + ComponentId, + StepContent, + TacticComponent, +} from "../model/tactic/Tactic" import { overlaps } from "../geo/Box" import { RackedCourtObject, RackedPlayer } from "./RackedItems" -import { getComponent, getOrigin, getPrecomputedPosition, tryGetComponent } from "./PlayerDomains" +import { + getComponent, + getOrigin, + getPrecomputedPosition, + tryGetComponent, +} from "./PlayerDomains" import { ActionKind } from "../model/tactic/Action.ts" import { spreadNewStateFromOriginStateChange } from "./ActionsDomains.ts" @@ -26,7 +47,7 @@ export function placePlayerAt( ballState: BallState.NONE, path: null, actions: [], - frozen: false + frozen: false, } } @@ -365,7 +386,7 @@ function getPlayerTerminalState( ballState: stateAfter(lastPhantom.ballState), id: player.id, pos, - frozen: true + frozen: true, } } @@ -395,8 +416,9 @@ export function drainTerminalStateOnChildContent( if ( parentComponent.type !== "player" || childComponent.type !== "player" - ) + ) { continue + } const newContentResult = spreadNewStateFromOriginStateChange( childComponent, diff --git a/src/pages/Editor.tsx b/src/pages/Editor.tsx index 0c1b792..da9697d 100644 --- a/src/pages/Editor.tsx +++ b/src/pages/Editor.tsx @@ -19,10 +19,20 @@ import { BallPiece } from "../components/editor/BallPiece" import { Rack } from "../components/Rack" import { PlayerPiece } from "../components/editor/PlayerPiece" -import { ComponentId, CourtType, StepContent, StepInfoNode, TacticComponent, TacticInfo } from "../model/tactic/Tactic" +import { + ComponentId, + CourtType, + StepContent, + StepInfoNode, + TacticComponent, + TacticInfo, +} from "../model/tactic/Tactic" import { fetchAPI, fetchAPIGet } from "../Fetcher" -import SavingState, { SaveState, SaveStates } from "../components/editor/SavingState" +import SavingState, { + SaveState, + SaveStates, +} from "../components/editor/SavingState" import { BALL_TYPE } from "../model/tactic/CourtObjects" import { CourtAction } from "../components/editor/CourtAction" @@ -43,10 +53,19 @@ import { updateComponent, } from "../editor/TacticContentDomains" -import { BallState, Player, PlayerInfo, PlayerLike, PlayerTeam } from "../model/tactic/Player" +import { + BallState, + Player, + PlayerInfo, + PlayerLike, + PlayerTeam, +} from "../model/tactic/Player" import { RackedCourtObject, RackedPlayer } from "../editor/RackedItems" -import { CourtPlayer, EditableCourtPlayer } from "../components/editor/CourtPlayer.tsx" +import { + CourtPlayer, + EditableCourtPlayer, +} from "../components/editor/CourtPlayer.tsx" import { createAction, getActionKind, @@ -58,11 +77,21 @@ import ArrowAction from "../components/actions/ArrowAction" import { middlePos, Pos, ratioWithinBase } from "../geo/Pos" import { Action, ActionKind } from "../model/tactic/Action" import BallAction from "../components/actions/BallAction" -import { computePhantomPositioning, getOrigin, removePlayer } from "../editor/PlayerDomains" +import { + computePhantomPositioning, + getOrigin, + removePlayer, +} from "../editor/PlayerDomains" import { CourtBall } from "../components/editor/CourtBall" import { useNavigate, useParams } from "react-router-dom" import StepsTree from "../components/editor/StepsTree" -import { addStepNode, getAvailableId, getParent, getStepNode, removeStepNode } from "../editor/StepsDomain" +import { + addStepNode, + getAvailableId, + getParent, + getStepNode, + removeStepNode, +} from "../editor/StepsDomain" const ERROR_STYLE: CSSProperties = { borderColor: "red", @@ -105,13 +134,10 @@ function GuestModeEditor() { GUEST_MODE_STEP_CONTENT_STORAGE_KEY + "0", ) - const stepInitialContent: ComputedStepContent = { - content: { - ...(storageContent == null - ? { components: [] } - : JSON.parse(storageContent)), - }, - relativePositions: new Map(), + const stepInitialContent: StepContent = { + ...(storageContent == null + ? { components: [] } + : JSON.parse(storageContent)), } const rootStepNode: StepInfoNode = JSON.parse( @@ -130,6 +156,7 @@ function GuestModeEditor() { ) } + const courtRef = useRef(null) const [stepId, setStepId] = useState(ROOT_STEP_ID) const [stepContent, setStepContent, saveState] = useContentState( stepInitialContent, @@ -137,46 +164,40 @@ function GuestModeEditor() { useMemo( () => debounceAsync( - async ({ - content, - relativePositions, - }: ComputedStepContent) => { + async (content: StepContent) => { localStorage.setItem( GUEST_MODE_STEP_CONTENT_STORAGE_KEY + stepId, JSON.stringify(content), ) - const terminalState = computeTerminalState( - content, - relativePositions, + const stepsTree: StepInfoNode = JSON.parse( + localStorage.getItem( + GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY, + )!, ) - const currentStepNode = getStepNode( - rootStepNode, - stepId, - )! - - for (const child of currentStepNode.children) { - const childCurrentContent = getStepContent(child.id) - const childUpdatedContent = - drainTerminalStateOnChildContent( - terminalState, - childCurrentContent, - ) - if (childUpdatedContent) { - localStorage.setItem( - GUEST_MODE_STEP_CONTENT_STORAGE_KEY + - stepId, - JSON.stringify(childUpdatedContent), - ) - } - } + await updateStepContents( + stepId, + stepsTree, + async (stepId) => { + const content = JSON.parse(localStorage.getItem( + GUEST_MODE_STEP_CONTENT_STORAGE_KEY + stepId, + )!) + const courtBounds = courtRef.current!.getBoundingClientRect() + const relativePositions = computeRelativePositions(courtBounds, content) + return { content, relativePositions } + }, + async (stepId, content) => localStorage.setItem( + GUEST_MODE_STEP_CONTENT_STORAGE_KEY + stepId, + JSON.stringify(content), + ), + ) return SaveStates.Guest }, 250, ), - [rootStepNode, stepId], + [stepId], ), ) @@ -196,6 +217,7 @@ function GuestModeEditor() { "Nouvelle Tactique", courtType: "PLAIN", }} + courtRef={courtRef} currentStepContent={stepContent} setCurrentStepContent={(content) => setStepContent(content, true)} saveState={saveState} @@ -207,13 +229,7 @@ function GuestModeEditor() { selectStep={useCallback( (step) => { setStepId(step) - setStepContent( - () => ({ - content: getStepContent(step), - relativePositions: new Map(), - }), - false, - ) + setStepContent(getStepContent(step), false) return }, [setStepContent], @@ -264,38 +280,41 @@ function UserModeEditor() { const tacticId = parseInt(idStr!) const navigation = useNavigate() + + const courtRef = useRef(null) const [stepId, setStepId] = useState(1) const saveContent = useCallback( - async ({ content, relativePositions }: ComputedStepContent) => { + async (content: StepContent) => { const response = await fetchAPI( `tactics/${tacticId}/steps/${stepId}`, { content }, "PUT", ) - const terminalStateContent = computeTerminalState( - content, - relativePositions, - ) - const currentNode = getStepNode(stepsTree!, stepId)! + await updateStepContents( + stepId, + stepsTree, + async (id) => { + const response = await fetchAPIGet( + `tactics/${tacticId}/steps/${id}`, + ) + if (!response.ok) + throw new Error("Error when retrieving children content") - const tasks = currentNode.children.map(async (child) => { - const response = await fetchAPIGet( - `tactics/${tacticId}/steps/${child.id}`, - ) - if (!response.ok) - throw new Error("Error when retrieving children content") - const childContent: StepContent = await response.json() - const childUpdatedContent = drainTerminalStateOnChildContent( - terminalStateContent, - childContent, - ) - if (childUpdatedContent) { + const content = await response.json() + const courtBounds = courtRef.current!.getBoundingClientRect() + const relativePositions = computeRelativePositions(courtBounds, content) + return { + content, + relativePositions, + } + }, + async (id, content) => { const response = await fetchAPI( - `tactics/${tacticId}/steps/${child.id}`, - { content: childUpdatedContent }, + `tactics/${tacticId}/steps/${id}`, + { content }, "PUT", ) if (!response.ok) { @@ -303,12 +322,8 @@ function UserModeEditor() { "Error when updated new children content", ) } - } - }) - - for (const task of tasks) { - await task - } + }, + ) return response.ok ? SaveStates.Ok : SaveStates.Err }, @@ -316,11 +331,8 @@ function UserModeEditor() { ) const [stepContent, setStepContent, saveState] = - useContentState( - { - content: { components: [] }, - relativePositions: new Map(), - }, + useContentState( + { components: [] }, SaveStates.Ok, useMemo(() => debounceAsync(saveContent, 250), [saveContent]), ) @@ -352,7 +364,7 @@ function UserModeEditor() { setTactic({ id: tacticId, name, courtType }) setStepsTree(root) - setStepContent({ content, relativePositions: new Map() }, false) + setStepContent(content, false) } if (tactic === null) initialize() @@ -374,10 +386,7 @@ function UserModeEditor() { if (!response.ok) return setStepId(step) setStepContent( - { - content: await response.json(), - relativePositions: new Map(), - }, + await response.json(), false, ) }, @@ -422,6 +431,7 @@ function UserModeEditor() { rootStepNode: stepsTree, courtType: tactic?.courtType, }} + courtRef={courtRef} currentStepId={stepId} currentStepContent={stepContent} setCurrentStepContent={(content) => setStepContent(content, true)} @@ -440,10 +450,12 @@ function EditorLoadingScreen() { export interface EditorViewProps { tactic: TacticInfo - currentStepContent: ComputedStepContent + currentStepContent: StepContent currentStepId: number saveState: SaveState - setCurrentStepContent: Dispatch> + setCurrentStepContent: Dispatch> + + courtRef: RefObject selectStep: (stepId: number) => void onNameChange: (name: string) => Promise @@ -457,13 +469,15 @@ export interface EditorViewProps { function EditorPage({ tactic: { name, rootStepNode: initialStepsNode, courtType }, currentStepId, - setCurrentStepContent, - currentStepContent: { content, relativePositions }, + setCurrentStepContent: setContent, + currentStepContent: content, saveState, onNameChange, selectStep, onRemoveStep, onAddStep, + + courtRef, }: EditorViewProps) { const [titleStyle, setTitleStyle] = useState({}) @@ -486,33 +500,34 @@ function EditorPage({ const [isStepsTreeVisible, setStepsTreeVisible] = useState(false) - const courtRef = useRef(null) const courtBounds = useCallback( () => courtRef.current!.getBoundingClientRect(), [courtRef], ) - const setContent = useCallback( - (newState: SetStateAction) => { - setCurrentStepContent((c) => { - const state = - typeof newState === "function" - ? newState(c.content) - : newState - - const courtBounds = courtRef.current?.getBoundingClientRect() - const relativePositions: ComputedRelativePositions = courtBounds - ? computeRelativePositions(courtBounds, state) - : new Map() - - return { - content: state, - relativePositions, - } - }) - }, - [setCurrentStepContent], - ) + const relativePositions = useMemo(() => { + const courtBounds = courtRef.current?.getBoundingClientRect() + return courtBounds ? computeRelativePositions(courtBounds, content) : new Map() + }, [content, courtRef]) + + // const setContent = useCallback( + // (newState: SetStateAction) => { + // setCurrentStepContent((c) => { + // const state = + // typeof newState === "function" + // ? newState(c.content) + // : newState + // + // const courtBounds = courtRef.current?.getBoundingClientRect() + // const relativePositions: ComputedRelativePositions = courtBounds + // ? computeRelativePositions(courtBounds, state) + // : new Map() + // + // return state + // }) + // }, + // [setCurrentStepContent], + // ) const setComponents = (action: SetStateAction) => { setContent((c) => ({ @@ -638,7 +653,8 @@ function EditorPage({ setContent={setContent} /> ), - !isFrozen && (info.ballState === BallState.HOLDS_ORIGIN || + !isFrozen && + (info.ballState === BallState.HOLDS_ORIGIN || info.ballState === BallState.PASSED_ORIGIN) && ( renderAvailablePlayerActions(info, component)} - /> + return ( + + renderAvailablePlayerActions(info, component) + } + /> + ) } } @@ -1213,3 +1234,40 @@ function computeRelativePositions(courtBounds: DOMRect, content: StepContent) { return relativePositionsCache } + +async function updateStepContents(stepId: number, + stepsTree: StepInfoNode, + getStepContent: (stepId: number) => Promise, + setStepContent: (stepId: number, content: StepContent) => Promise, +) { + + + + async function updateSteps(step: StepInfoNode, content: StepContent, relativePositions: ComputedRelativePositions) { + const terminalStateContent = computeTerminalState( + content, + relativePositions, + ) + + const tasks = step.children.map(async (child) => { + const { content: childContent, relativePositions: childRelativePositions } = await getStepContent(child.id) + const childUpdatedContent = drainTerminalStateOnChildContent( + terminalStateContent, + childContent, + ) + if (childUpdatedContent) { + await setStepContent(child.id, childUpdatedContent) + await updateSteps(child, childUpdatedContent, childRelativePositions) + } + }) + + for (const task of tasks) { + await task + } + } + + const { content, relativePositions } = await getStepContent(stepId) + const startNode = getStepNode(stepsTree!, stepId)! + + await updateSteps(startNode, content, relativePositions) +} \ No newline at end of file