From 30df4e74bf0faefee0a630cf39f33605317d2978 Mon Sep 17 00:00:00 2001 From: maxime Date: Wed, 31 Jan 2024 21:14:15 +0100 Subject: [PATCH] add a steps visualizer (fake data) --- src/components/editor/BasketCourt.tsx | 2 +- src/components/editor/CourtAction.tsx | 4 +- src/components/editor/StepsTree.tsx | 58 +++++ src/editor/ActionsDomains.ts | 61 ++--- src/editor/PlayerDomains.ts | 63 ++--- src/editor/TacticContentDomains.ts | 84 +++---- src/model/tactic/Tactic.ts | 22 +- src/pages/Editor.tsx | 318 ++++++++++++++++---------- src/style/editor.css | 24 +- src/style/steps-tree.css | 48 ++++ src/style/theme/default.css | 2 + 11 files changed, 446 insertions(+), 240 deletions(-) create mode 100644 src/components/editor/StepsTree.tsx create mode 100644 src/style/steps-tree.css diff --git a/src/components/editor/BasketCourt.tsx b/src/components/editor/BasketCourt.tsx index 4f4c216..68021f3 100644 --- a/src/components/editor/BasketCourt.tsx +++ b/src/components/editor/BasketCourt.tsx @@ -1,7 +1,7 @@ import { ReactElement, ReactNode, RefObject } from "react" import { Action } from "../../model/tactic/Action" -import { CourtAction } from "./CourtAction.tsx" +import { CourtAction } from "./CourtAction" import { ComponentId, TacticComponent } from "../../model/tactic/Tactic" export interface BasketCourtProps { diff --git a/src/components/editor/CourtAction.tsx b/src/components/editor/CourtAction.tsx index c26c0d9..84f7fd5 100644 --- a/src/components/editor/CourtAction.tsx +++ b/src/components/editor/CourtAction.tsx @@ -1,7 +1,7 @@ import { Action, ActionKind } from "../../model/tactic/Action" -import BendableArrow from "../../components/arrows/BendableArrow" +import BendableArrow from "../arrows/BendableArrow" import { RefObject } from "react" -import { MoveToHead, ScreenHead } from "../../components/actions/ArrowAction" +import { MoveToHead, ScreenHead } from "../actions/ArrowAction" import { ComponentId } from "../../model/tactic/Tactic" export interface CourtActionProps { diff --git a/src/components/editor/StepsTree.tsx b/src/components/editor/StepsTree.tsx new file mode 100644 index 0000000..45d6492 --- /dev/null +++ b/src/components/editor/StepsTree.tsx @@ -0,0 +1,58 @@ +import {StepInfoNode} from "../../model/tactic/Tactic"; +import "../../style/steps-tree.css" +import BendableArrow from "../arrows/BendableArrow"; +import {useRef} from "react"; + +export interface StepsTreeProps { + root: StepInfoNode +} + +export default function StepsTree({root}: StepsTreeProps) { + return
+ +
+} + +interface StepsTreeContentProps { + node: StepInfoNode +} + +function StepsTreeNode({node}: StepsTreeContentProps) { + const ref = useRef(null) + return ( +
+ + {node.children.map(child => ( + { + }} + forceStraight={true} + wavy={false} + //TODO remove magic constant + startRadius={10} + endRadius={10} + /> + ))} +
+ {node.children.map(child => )} +
+
+ ) +} + +interface StepPieceProps { + id: number +} + +function StepPiece({id}: StepPieceProps) { + return ( +
+

{id}

+
+ ) +} diff --git a/src/editor/ActionsDomains.ts b/src/editor/ActionsDomains.ts index c9e7e66..3e94bc8 100644 --- a/src/editor/ActionsDomains.ts +++ b/src/editor/ActionsDomains.ts @@ -4,15 +4,15 @@ import { PlayerLike, PlayerPhantom, } from "../model/tactic/Player" -import { ratioWithinBase } from "../geo/Pos" +import {ratioWithinBase} from "../geo/Pos" import { ComponentId, TacticComponent, - TacticContent, + StepContent, } from "../model/tactic/Tactic" -import { overlaps } from "../geo/Box" -import { Action, ActionKind, moves } from "../model/tactic/Action" -import { removeBall, updateComponent } from "./TacticContentDomains" +import {overlaps} from "../geo/Box" +import {Action, ActionKind, moves} from "../model/tactic/Action" +import {removeBall, updateComponent} from "./TacticContentDomains" import { areInSamePath, changePlayerBallState, @@ -22,7 +22,7 @@ import { isNextInPath, removePlayer, } from "./PlayerDomains" -import { BALL_TYPE } from "../model/tactic/CourtObjects" +import {BALL_TYPE} from "../model/tactic/CourtObjects" export function getActionKind( target: TacticComponent | null, @@ -31,12 +31,12 @@ export function getActionKind( switch (ballState) { case BallState.HOLDS_ORIGIN: return target - ? { kind: ActionKind.SHOOT, nextState: BallState.PASSED_ORIGIN } - : { kind: ActionKind.DRIBBLE, nextState: ballState } + ? {kind: ActionKind.SHOOT, nextState: BallState.PASSED_ORIGIN} + : {kind: ActionKind.DRIBBLE, nextState: ballState} case BallState.HOLDS_BY_PASS: return target - ? { kind: ActionKind.SHOOT, nextState: BallState.PASSED } - : { kind: ActionKind.DRIBBLE, nextState: ballState } + ? {kind: ActionKind.SHOOT, nextState: BallState.PASSED} + : {kind: ActionKind.DRIBBLE, nextState: ballState} case BallState.PASSED_ORIGIN: case BallState.PASSED: case BallState.NONE: @@ -212,8 +212,8 @@ export function createAction( origin: PlayerLike, courtBounds: DOMRect, arrowHead: DOMRect, - content: TacticContent, -): { createdAction: Action; newContent: TacticContent } { + content: StepContent, +): { createdAction: Action; newContent: StepContent } { /** * Creates a new phantom component. * Be aware that this function will reassign the `content` parameter. @@ -222,7 +222,7 @@ export function createAction( forceHasBall: boolean, attachedTo?: ComponentId, ): ComponentId { - const { x, y } = ratioWithinBase(arrowHead, courtBounds) + const {x, y} = ratioWithinBase(arrowHead, courtBounds) let itemIndex: number let originPlayer: Player @@ -274,14 +274,14 @@ export function createAction( id: phantomId, pos: attachedTo ? { - type: "follows", - attach: attachedTo, - } + type: "follows", + attach: attachedTo, + } : { - type: "fixed", - x, - y, - }, + type: "fixed", + x, + y, + }, originPlayerId: originPlayer.id, ballState: phantomState, actions: [], @@ -320,13 +320,13 @@ export function createAction( action = { target: toId, type: actionKind, - segments: [{ next: toId }], + segments: [{next: toId}], } } else { action = { target: toId, type: actionKind, - segments: [{ next: toId }], + segments: [{next: toId}], } } @@ -355,7 +355,7 @@ export function createAction( const action: Action = { target: phantomId, type: actionKind, - segments: [{ next: phantomId }], + segments: [{next: phantomId}], } return { newContent: updateComponent( @@ -371,8 +371,8 @@ export function createAction( export function removeAllActionsTargeting( componentId: ComponentId, - content: TacticContent, -): TacticContent { + content: StepContent, +): StepContent { const components = [] for (let i = 0; i < content.components.length; i++) { const component = content.components[i] @@ -391,9 +391,10 @@ export function removeAllActionsTargeting( export function removeAction( origin: TacticComponent, actionIdx: number, - content: TacticContent, -): TacticContent { + content: StepContent, +): StepContent { const action = origin.actions[actionIdx] + origin = { ...origin, actions: origin.actions.toSpliced(actionIdx, 1), @@ -462,8 +463,8 @@ export function removeAction( export function spreadNewStateFromOriginStateChange( origin: PlayerLike, newState: BallState, - content: TacticContent, -): TacticContent { + content: StepContent, +): StepContent { if (origin.ballState === newState) { return content } @@ -534,7 +535,7 @@ export function spreadNewStateFromOriginStateChange( i-- // step back } else { // do not change the action type if it is a shoot action - const { kind, nextState } = getActionKindBetween( + const {kind, nextState} = getActionKindBetween( origin, actionTarget, newState, diff --git a/src/editor/PlayerDomains.ts b/src/editor/PlayerDomains.ts index 0473d72..40da34d 100644 --- a/src/editor/PlayerDomains.ts +++ b/src/editor/PlayerDomains.ts @@ -1,30 +1,11 @@ -import { - BallState, - Player, - PlayerLike, - PlayerPhantom, -} from "../model/tactic/Player" -import { - ComponentId, - TacticComponent, - TacticContent, -} from "../model/tactic/Tactic" -import { removeComponent, updateComponent } from "./TacticContentDomains" -import { - removeAllActionsTargeting, - spreadNewStateFromOriginStateChange, -} from "./ActionsDomains" -import { ActionKind } from "../model/tactic/Action" -import { - add, - minus, - norm, - Pos, - posWithinBase, - ratioWithinBase, - relativeTo, -} from "../geo/Pos.ts" -import { PLAYER_RADIUS_PIXELS } from "../components/editor/CourtPlayer.tsx" +import {BallState, Player, PlayerLike, PlayerPhantom,} from "../model/tactic/Player" +import {ComponentId, StepContent, TacticComponent,} from "../model/tactic/Tactic" + +import {removeComponent, updateComponent} from "./TacticContentDomains" +import {removeAllActionsTargeting, spreadNewStateFromOriginStateChange,} from "./ActionsDomains" +import {ActionKind} from "../model/tactic/Action" +import {add, minus, norm, Pos, posWithinBase, ratioWithinBase, relativeTo,} from "../geo/Pos.ts" +import {PLAYER_RADIUS_PIXELS} from "../components/editor/CourtPlayer.tsx" export function getOrigin( pathItem: PlayerPhantom, @@ -58,7 +39,7 @@ export function getPlayerNextTo( // following another phantom and / or the origin of the phantom is another export function computePhantomPositioning( phantom: PlayerPhantom, - content: TacticContent, + content: StepContent, area: DOMRect, ): Pos { const positioning = phantom.pos @@ -171,8 +152,8 @@ export function isNextInPath( export function clearPlayerPath( player: Player, - content: TacticContent, -): TacticContent { + content: StepContent, +): StepContent { if (player.path == null) { return content } @@ -192,8 +173,8 @@ export function clearPlayerPath( function removeAllPhantomsAttached( to: ComponentId, - content: TacticContent, -): TacticContent { + content: StepContent, +): StepContent { let i = 0 while (i < content.components.length) { const component = content.components[i] @@ -213,8 +194,8 @@ function removeAllPhantomsAttached( export function removePlayer( player: PlayerLike, - content: TacticContent, -): TacticContent { + content: StepContent, +): StepContent { content = removeAllActionsTargeting(player.id, content) content = removeAllPhantomsAttached(player.id, content) @@ -266,8 +247,8 @@ export function removePlayer( export function truncatePlayerPath( player: Player, phantom: PlayerPhantom, - content: TacticContent, -): TacticContent { + content: StepContent, +): StepContent { if (player.path == null) return content const path = player.path! @@ -289,9 +270,9 @@ export function truncatePlayerPath( truncateStartIdx == 0 ? null : { - ...path, - items: path.items.toSpliced(truncateStartIdx), - }, + ...path, + items: path.items.toSpliced(truncateStartIdx), + }, }, content, ) @@ -300,7 +281,7 @@ export function truncatePlayerPath( export function changePlayerBallState( player: PlayerLike, newState: BallState, - content: TacticContent, -): TacticContent { + content: StepContent, +): StepContent { return spreadNewStateFromOriginStateChange(player, newState, content) } diff --git a/src/editor/TacticContentDomains.ts b/src/editor/TacticContentDomains.ts index 1f3a9cc..0fc76e1 100644 --- a/src/editor/TacticContentDomains.ts +++ b/src/editor/TacticContentDomains.ts @@ -1,4 +1,4 @@ -import { Pos, ratioWithinBase } from "../geo/Pos" +import {Pos, ratioWithinBase} from "../geo/Pos" import { BallState, Player, @@ -15,12 +15,13 @@ import { import { ComponentId, TacticComponent, - TacticContent, + StepContent, } from "../model/tactic/Tactic" -import { overlaps } from "../geo/Box" -import { RackedCourtObject, RackedPlayer } from "./RackedItems" -import { changePlayerBallState, getComponent, getOrigin } from "./PlayerDomains" -import { ActionKind } from "../model/tactic/Action.ts" + +import {overlaps} from "../geo/Box" +import {RackedCourtObject, RackedPlayer} from "./RackedItems" +import {changePlayerBallState, getComponent, getOrigin} from "./PlayerDomains" +import {ActionKind} from "../model/tactic/Action.ts" export function placePlayerAt( refBounds: DOMRect, @@ -45,14 +46,15 @@ export function placeObjectAt( refBounds: DOMRect, courtBounds: DOMRect, rackedObject: RackedCourtObject, - content: TacticContent, -): TacticContent { + content: StepContent, +): StepContent { const pos = ratioWithinBase(refBounds, courtBounds) + let courtObject: CourtObject switch (rackedObject.key) { - case BALL_TYPE: + case BALL_TYPE: { const playerCollidedIdx = getComponentCollided( refBounds, content.components, @@ -69,7 +71,7 @@ export function placeObjectAt( actions: [], } break - + } default: throw new Error("unknown court object " + rackedObject.key) } @@ -82,9 +84,9 @@ export function placeObjectAt( export function dropBallOnComponent( targetedComponentIdx: number, - content: TacticContent, + content: StepContent, setAsOrigin: boolean, -): TacticContent { +): StepContent { const component = content.components[targetedComponentIdx] if (component.type === "player" || component.type === "phantom") { @@ -101,7 +103,7 @@ export function dropBallOnComponent( return removeBall(content) } -export function removeBall(content: TacticContent): TacticContent { +export function removeBall(content: StepContent): StepContent { const ballObjIdx = content.components.findIndex((o) => o.type == "ball") if (ballObjIdx == -1) { @@ -117,8 +119,8 @@ export function removeBall(content: TacticContent): TacticContent { export function placeBallAt( refBounds: DOMRect, courtBounds: DOMRect, - content: TacticContent, -): TacticContent { + content: StepContent, +): StepContent { if (!overlaps(courtBounds, refBounds)) { return removeBall(content) } @@ -162,9 +164,9 @@ export function moveComponent( component: TacticComponent, info: PlayerInfo, courtBounds: DOMRect, - content: TacticContent, - removed: (content: TacticContent) => TacticContent, -): TacticContent { + content: StepContent, + removed: (content: StepContent) => StepContent, +): StepContent { const playerBounds = document .getElementById(info.id)! .getBoundingClientRect() @@ -186,9 +188,9 @@ export function moveComponent( phantomIdx == 0 ? origin : getComponent( - originPathItems[phantomIdx - 1], - content.components, - ) + originPathItems[phantomIdx - 1], + content.components, + ) // detach the action from the screen target and transform it to a regular move action to the phantom. content = updateComponent( { @@ -196,18 +198,18 @@ export function moveComponent( actions: playerBeforePhantom.actions.map((a) => a.target === referent ? { - ...a, - segments: a.segments.toSpliced( - a.segments.length - 2, - 1, - { - ...a.segments[a.segments.length - 1], - next: component.id, - }, - ), - target: component.id, - type: ActionKind.MOVE, - } + ...a, + segments: a.segments.toSpliced( + a.segments.length - 2, + 1, + { + ...a.segments[a.segments.length - 1], + next: component.id, + }, + ), + target: component.id, + type: ActionKind.MOVE, + } : a, ), }, @@ -220,9 +222,9 @@ export function moveComponent( ...component, pos: isPhantom ? { - type: "fixed", - ...newPos, - } + type: "fixed", + ...newPos, + } : newPos, }, content, @@ -232,8 +234,8 @@ export function moveComponent( export function removeComponent( componentId: ComponentId, - content: TacticContent, -): TacticContent { + content: StepContent, +): StepContent { return { ...content, components: content.components.filter((c) => c.id !== componentId), @@ -242,8 +244,8 @@ export function removeComponent( export function updateComponent( component: TacticComponent, - content: TacticContent, -): TacticContent { + content: StepContent, +): StepContent { return { ...content, components: content.components.map((c) => @@ -285,5 +287,5 @@ export function getRackPlayers( c.type == "player" && c.team == team && c.role == role, ) == -1, ) - .map((key) => ({ team, key })) + .map((key) => ({team, key})) } diff --git a/src/model/tactic/Tactic.ts b/src/model/tactic/Tactic.ts index 0ad312c..4dadca8 100644 --- a/src/model/tactic/Tactic.ts +++ b/src/model/tactic/Tactic.ts @@ -2,21 +2,27 @@ import { Player, PlayerPhantom } from "./Player" import { Action } from "./Action" import { CourtObject } from "./CourtObjects" -export type CourtType = "HALF" | "PLAIN" - export interface Tactic { - id: number - name: string - courtType: CourtType - content: TacticContent + readonly id: number + readonly name: string + readonly courtType: CourtType + readonly currentStepContent: StepContent + readonly rootStepNode: StepInfoNode +} + +export interface StepContent { + readonly stepId: number + readonly components: TacticComponent[] } -export interface TacticContent { - components: TacticComponent[] +export interface StepInfoNode { + readonly id: number + readonly children: StepInfoNode[] } export type TacticComponent = Player | CourtObject | PlayerPhantom export type ComponentId = string +export type CourtType = "PLAIN" | "HALF" export interface Component { /** diff --git a/src/pages/Editor.tsx b/src/pages/Editor.tsx index 83c4dac..e65a852 100644 --- a/src/pages/Editor.tsx +++ b/src/pages/Editor.tsx @@ -14,28 +14,25 @@ import TitleInput from "../components/TitleInput" import PlainCourt from "../assets/court/full_court.svg?react" import HalfCourt from "../assets/court/half_court.svg?react" -import { BallPiece } from "../components/editor/BallPiece" +import {BallPiece} from "../components/editor/BallPiece" -import { Rack } from "../components/Rack" -import { PlayerPiece } from "../components/editor/PlayerPiece" +import {Rack} from "../components/Rack" +import {PlayerPiece} from "../components/editor/PlayerPiece" import { - CourtType, + CourtType, StepContent, StepInfoNode, Tactic, TacticComponent, - TacticContent, } 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" import { ActionPreview, BasketCourt } from "../components/editor/BasketCourt" import { overlaps } from "../geo/Box" + import { dropBallOnComponent, getComponentCollided, @@ -47,24 +44,14 @@ import { removeBall, updateComponent, } from "../editor/TacticContentDomains" -import { - BallState, - Player, - PlayerInfo, - PlayerLike, - PlayerTeam, -} from "../model/tactic/Player" -import { RackedCourtObject, RackedPlayer } from "../editor/RackedItems" + +import {BallState, Player, PlayerInfo, PlayerLike, PlayerTeam,} from "../model/tactic/Player" +import {RackedCourtObject, RackedPlayer} from "../editor/RackedItems" import CourtPlayer from "../components/editor/CourtPlayer" -import { - createAction, - getActionKind, - isActionValid, - removeAction, -} from "../editor/ActionsDomains" +import {createAction, getActionKind, isActionValid, removeAction,} from "../editor/ActionsDomains" import ArrowAction from "../components/actions/ArrowAction" -import { middlePos, Pos, ratioWithinBase } from "../geo/Pos" -import { Action, ActionKind } from "../model/tactic/Action" +import {middlePos, Pos, ratioWithinBase} from "../geo/Pos" +import {Action, ActionKind} from "../model/tactic/Action" import BallAction from "../components/actions/BallAction" import { changePlayerBallState, @@ -75,6 +62,7 @@ import { import { CourtBall } from "../components/editor/CourtBall" import { useNavigate, useParams } from "react-router-dom" import { DEFAULT_TACTIC_NAME } from "./NewTacticPage.tsx" +import StepsTree from "../components/editor/StepsTree" const ERROR_STYLE: CSSProperties = { borderColor: "red", @@ -85,7 +73,7 @@ const GUEST_MODE_TITLE_STORAGE_KEY = "guest_mode_title" export interface EditorViewProps { tactic: Tactic - onContentChange: (tactic: TacticContent) => Promise + onContentChange: (tactic: StepContent) => Promise onNameChange: (name: string) => Promise } interface TacticDto { @@ -93,6 +81,7 @@ interface TacticDto { name: string courtType: CourtType content: string + root: StepInfoNode } interface EditorPageProps { @@ -107,6 +96,7 @@ export default function EditorPage({ guestMode }: EditorPageProps) { courtType: "PLAIN", content: '{"components": []}', name: DEFAULT_TACTIC_NAME, + root: {id: 1, children: []} } } return null @@ -120,9 +110,11 @@ export default function EditorPage({ guestMode }: EditorPageProps) { async function initialize() { const infoResponsePromise = fetchAPIGet(`tactics/${id}`) + const treeResponsePromise = fetchAPIGet(`tactics/${id}/tree`) const contentResponsePromise = fetchAPIGet(`tactics/${id}/steps/1`) const infoResponse = await infoResponsePromise + const treeResponse = await treeResponsePromise const contentResponse = await contentResponsePromise if (infoResponse.status == 401 || contentResponse.status == 401) { @@ -132,8 +124,9 @@ export default function EditorPage({ guestMode }: EditorPageProps) { const { name, courtType } = await infoResponse.json() const content = await contentResponse.text() + const { root } = await treeResponse.json() - setTactic({ id, name, courtType, content }) + setTactic({ id, name, courtType, content, root }) } initialize() @@ -143,6 +136,7 @@ export default function EditorPage({ guestMode }: EditorPageProps) { return ( { + onContentChange={async (content: StepContent) => { if (isInGuestMode) { localStorage.setItem( GUEST_MODE_CONTENT_STORAGE_KEY, @@ -196,6 +195,7 @@ function Editor({ id, name, courtType, content }: EditorProps) { `tactics/${id}/steps/1`, { content }, "PUT", + ) if (response.status == 401) { navigate("/login") @@ -212,21 +212,39 @@ function Editor({ id, name, courtType, content }: EditorProps) { `tactics/${id}/name`, { name }, "PUT", + ) if (response.status == 401) { navigate("/login") } return response.ok }} + onStepSelected={() => { + }} + stepsContentsRoot={rootStepNode} + courtType={courtType} /> ) } +export interface EditorViewProps { + tactic: Tactic + onContentChange: (tactic: StepContent) => Promise + onStepSelected: (stepId: number) => void, + onNameChange: (name: string) => Promise + stepsContentsRoot: StepInfoNode + courtType: "PLAIN" | "HALF" +} + function EditorView({ - tactic: { id, name, content: initialContent, courtType }, - onContentChange, - onNameChange, -}: EditorViewProps) { + + tactic: {id, name, currentStepContent: initialContent}, + onContentChange, + onNameChange, + onStepSelected, + stepsContentsRoot, + courtType, + }: EditorViewProps) { const isInGuestMode = id == -1 const [titleStyle, setTitleStyle] = useState({}) @@ -244,13 +262,15 @@ function EditorView({ ) const [objects, setObjects] = useState(() => - isBallOnCourt(content) ? [] : [{ key: "ball" }], + isBallOnCourt(content) ? [] : [{key: "ball"}], ) const [previewAction, setPreviewAction] = useState( null, ) + const [isStepsTreeVisible, setStepsTreeVisible] = useState(false) + const courtRef = useRef(null) const setComponents = (action: SetStateAction) => { @@ -267,7 +287,7 @@ function EditorView({ ) useEffect(() => { - setObjects(isBallOnCourt(content) ? [] : [{ key: "ball" }]) + setObjects(isBallOnCourt(content) ? [] : [{key: "ball"}]) }, [setObjects, content]) const insertRackedPlayer = (player: Player) => { @@ -280,7 +300,7 @@ function EditorView({ setter = setAllies } if (player.ballState == BallState.HOLDS_BY_PASS) { - setObjects([{ key: "ball" }]) + setObjects([{key: "ball"}]) } setter((players) => [ ...players, @@ -479,7 +499,7 @@ function EditorView({ setContent((content) => removeBall(content)) setObjects((objects) => [ ...objects, - { key: "ball" }, + {key: "ball"}, ]) }} /> @@ -516,7 +536,7 @@ function EditorView({
- +
-
+
+ +
-
-
- +
+
+
+ - - overlaps( - courtBounds(), - div.getBoundingClientRect(), - ), - [courtBounds], - )} - onElementDetached={useCallback( - (r, e: RackedCourtObject) => - setContent((content) => - placeObjectAt( - r.getBoundingClientRect(), + + overlaps( courtBounds(), - e, - content, + div.getBoundingClientRect(), ), - ), - [courtBounds, setContent], - )} - render={renderCourtObject} - /> + [courtBounds], + )} + onElementDetached={useCallback( + (r, e: RackedCourtObject) => + setContent((content) => + placeObjectAt( + r.getBoundingClientRect(), + courtBounds(), + e, + content, + ), + ), + [courtBounds, setContent], + )} + render={renderCourtObject} + /> - -
-
-
- } +
+
+
+ } + courtRef={courtRef} + previewAction={previewAction} + renderComponent={renderComponent} + renderActions={renderActions} + /> +
+
+
) } +interface EditorStepsTreeProps { + isVisible: boolean + root: StepInfoNode +} + +function EditorStepsTree({isVisible, root}: EditorStepsTreeProps) { + const fakeRoot: StepInfoNode = { + id: 0, + children: [ + { + id: 1, + children: [ + { + id: 2, + children: [] + }, + { + id: 3, + children: [{ + id: 4, + children: [] + }] + } + ] + }, + { + id: 5, + children: [ + { + id: 6, + children: [] + }, + { + id: 7, + children: [] + } + ] + }, + { + id: 8, + children: [ + { + id: 9, + children: [ + { + id: 10, + children: [] + } + ] + } + ] + } + ] + } + return ( +
+ +
+ ) +} + interface PlayerRackProps { id: string objects: RackedPlayer[] @@ -607,12 +695,12 @@ interface PlayerRackProps { } function PlayerRack({ - id, - objects, - setObjects, - courtRef, - setComponents, -}: PlayerRackProps) { + id, + objects, + setObjects, + courtRef, + setComponents, + }: PlayerRackProps) { const courtBounds = useCallback( () => courtRef.current!.getBoundingClientRect(), [courtRef], @@ -640,7 +728,7 @@ function PlayerRack({ [courtBounds, setComponents], )} render={useCallback( - ({ team, key }: { team: PlayerTeam; key: string }) => ( + ({team, key}: { team: PlayerTeam; key: string }) => ( ) => void + content: StepContent + setContent: (state: SetStateAction) => void setPreviewAction: (state: SetStateAction) => void courtRef: RefObject } function CourtPlayerArrowAction({ - playerInfo, - player, - isInvalid, - - content, - setContent, - setPreviewAction, - courtRef, -}: CourtPlayerArrowActionProps) { + playerInfo, + player, + isInvalid, + + content, + setContent, + setPreviewAction, + courtRef, + }: CourtPlayerArrowActionProps) { const courtBounds = useCallback( () => courtRef.current!.getBoundingClientRect(), [courtRef], @@ -729,7 +817,7 @@ function CourtPlayerArrowAction({ } setContent((content) => { - let { createdAction, newContent } = createAction( + let {createdAction, newContent} = createAction( player, courtBounds(), headRect, @@ -768,7 +856,7 @@ function CourtPlayerArrowAction({ ) } -function isBallOnCourt(content: TacticContent) { +function isBallOnCourt(content: StepContent) { return ( content.components.findIndex( (c) => @@ -782,18 +870,18 @@ function isBallOnCourt(content: TacticContent) { function renderCourtObject(courtObject: RackedCourtObject) { if (courtObject.key == "ball") { - return + return } throw new Error("unknown racked court object " + courtObject.key) } -function Court({ courtType }: { courtType: string }) { +function Court({courtType}: { courtType: string }) { return (
{courtType == "PLAIN" ? ( - + ) : ( - + )}
) diff --git a/src/style/editor.css b/src/style/editor.css index b6a8ea4..8f506b2 100644 --- a/src/style/editor.css +++ b/src/style/editor.css @@ -26,13 +26,13 @@ width: 100%; display: flex; background-color: var(--main-color); - margin-bottom: 3px; justify-content: space-between; align-items: stretch; } #racks { + margin: 3px 6px 0 6px; display: flex; justify-content: space-between; align-items: center; @@ -44,8 +44,28 @@ align-self: center; } -#edit-div { +#editor-div { + display: flex; + flex-direction: row; +} + +#content-div, +#editor-div { height: 100%; + width: 100%; +} + +#content-div { + width: 100%; +} + +#steps-div { + background-color: var(--editor-tree-background); + overflow: hidden; + width: 20%; + + transform: translateX(100%); + transition: transform 500ms; } #allies-rack, diff --git a/src/style/steps-tree.css b/src/style/steps-tree.css new file mode 100644 index 0000000..28e3f4f --- /dev/null +++ b/src/style/steps-tree.css @@ -0,0 +1,48 @@ +.step-piece { + font-family: monospace; + pointer-events: all; + + background-color: var(--editor-tree-step-piece); + color: var(--selected-team-secondarycolor); + + border-radius: 100px; + + width: 20px; + height: 20px; + + display: flex; + + align-items: center; + justify-content: center; + + user-select: none; +} + +.step-children { + margin-top: 10vh; + display: flex; + flex-direction: row; + width: 100%; + height: 100%; +} + +.step-group { + position: relative; + + display: flex; + flex-direction: column; + align-items: center; + + width: 100%; + height: 100%; +} + +.steps-tree { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding-top: 10%; + + height: 100%; +} \ No newline at end of file diff --git a/src/style/theme/default.css b/src/style/theme/default.css index caa5162..62bf848 100644 --- a/src/style/theme/default.css +++ b/src/style/theme/default.css @@ -29,4 +29,6 @@ --main-contrast-color: #e6edf3; --font-title: Helvetica; --font-content: Helvetica; + --editor-tree-background: #503636; + --editor-tree-step-piece: #0bd9d9; }