From 15b47f354e9321ab57dd79434c860f0653f451c9 Mon Sep 17 00:00:00 2001 From: maxime Date: Fri, 22 Mar 2024 23:37:31 +0100 Subject: [PATCH] add visualizer --- src/App.tsx | 17 +- src/components/Visualizer.tsx | 224 ++++++++++++++++++ src/components/editor/BasketCourt.tsx | 34 ++- src/components/editor/CourtBall.tsx | 50 ++-- src/components/editor/StepsTree.tsx | 26 +- src/{editor => domains}/ActionsDomains.ts | 16 +- src/{editor => domains}/PlayerDomains.ts | 56 ++++- src/{editor => domains}/StepsDomain.ts | 2 +- .../TacticContentDomains.ts | 14 +- src/pages/Editor.tsx | 163 ++++++------- src/pages/Visualizer.tsx | 23 -- src/pages/VisualizerPage.tsx | 178 ++++++++++++++ src/service/APITacticService.ts | 8 +- src/service/LocalStorageTacticService.ts | 10 +- ...cticService.ts => MutableTacticService.ts} | 5 +- src/style/court.css | 24 ++ src/style/editor.css | 49 +--- src/style/tactic.css | 7 + src/style/visualizer.css | 68 +++++- src/visualizer/VisualizerState.ts | 45 ++++ 20 files changed, 790 insertions(+), 229 deletions(-) create mode 100644 src/components/Visualizer.tsx rename src/{editor => domains}/ActionsDomains.ts (98%) rename src/{editor => domains}/PlayerDomains.ts (88%) rename src/{editor => domains}/StepsDomain.ts (97%) rename src/{editor => domains}/TacticContentDomains.ts (97%) delete mode 100644 src/pages/Visualizer.tsx create mode 100644 src/pages/VisualizerPage.tsx rename src/service/{TacticService.ts => MutableTacticService.ts} (92%) create mode 100644 src/style/court.css create mode 100644 src/style/tactic.css create mode 100644 src/visualizer/VisualizerState.ts diff --git a/src/App.tsx b/src/App.tsx index d21929a..489a9fc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,6 +23,7 @@ import { import { BASE } from "./Constants.ts" import { Authentication, Fetcher } from "./app/Fetcher.ts" import { User } from "./model/User.ts" +import { VisualizerPage } from "./pages/VisualizerPage.tsx" const HomePage = lazy(() => import("./pages/HomePage.tsx")) const LoginPage = lazy(() => import("./pages/LoginPage.tsx")) @@ -110,7 +111,6 @@ export default function App() { , )} /> - , )} /> - )} @@ -144,6 +143,20 @@ export default function App() { , )} /> + + , + + )} + /> + , + )} + /> (null) + const [courtType, setCourtType] = useState() + const [stepsTree, setStepsTree] = useState() + const fetcher = useAppFetcher() + const service = useMemo(() => new APITacticService(fetcher, tacticId), [tacticId]) + + const isNotInit = !stepsTree || !courtType + + useEffect(() => { + async function init() { + const contextResult = await service.getContext() + if (typeof contextResult === "string") { + setPanicMessage(contextResult) + return + } + + + const rootStep = contextResult.stepsTree + setStepsTree(rootStep) + setCourtType(contextResult.courtType) + } + if (isNotInit) + init() + }, [isNotInit, service]) + + if (panicMessage) { + return

{panicMessage}

+ } + if (isNotInit) { + return

Loading...

+ } + + return +} + +export interface StepVisualizerProps { + stepId?: number + stepsTree: StepInfoNode + courtType: CourtType + service: TacticService +} + +export function StepVisualizer({stepId, stepsTree, courtType, service}: StepVisualizerProps) { + const [panicMessage, setPanicMessage] = useState(null) + const [content, setContent] = useState(null) + const [parentContent, setParentContent] = useState() + + const isNotInit = !content || !parentContent + + useEffect(() => { + async function init() { + + const contentStepId = stepId ?? stepsTree.id + + const contentResult = await service.getContent(contentStepId) + if (typeof contentResult === "string") { + setPanicMessage(contentResult) + return + } + + const stepParent = getParent(stepsTree, contentStepId) + let parentContent = null + if (stepParent) { + const parentResult = await service.getContent(contentStepId) + if (typeof parentResult === "string") { + setPanicMessage(parentResult) + return + } + parentContent = parentResult + } + + setContent(contentResult) + setParentContent(parentContent) + } + + if (isNotInit) init() + }, [isNotInit, service, stepId, stepsTree]) + + if (panicMessage) { + return

{panicMessage}

+ } + + if (isNotInit) { + return

Loading Content...

+ } + + return ( + + ) +} + +export interface VisualizerFrameProps { + content: StepContent + parentContent: StepContent | null + courtType: CourtType +} + +export function VisualizerFrame({ + content, + parentContent, + courtType, + }: VisualizerFrameProps) { + + console.log(content, parentContent) + + const courtRef = useRef(null) + + const courtBounds = useCallback( + () => courtRef.current!.getBoundingClientRect(), + [courtRef], + ) + + const relativePositions = useMemo(() => { + const courtBounds = courtRef.current?.getBoundingClientRect() + return courtBounds + ? computeRelativePositions(courtBounds, content) + : new Map() + }, [content, courtRef]) + + const renderPlayer = useCallback( + (component: PlayerLike, isFromParent: boolean) => { + let info: PlayerInfo + const isPhantom = component.type == "phantom" + const usedContent = isFromParent ? parentContent! : content + + if (isPhantom) { + info = getPhantomInfo( + component, + usedContent, + relativePositions, + courtBounds(), + ) + } else { + info = component + } + + const className = + (isPhantom ? "phantom" : "player") + + " " + + (isFromParent ? "from-parent" : "") + + return ( + []} + /> + ) + }, + [content, courtBounds, parentContent, relativePositions], + ) + + const renderComponent = useCallback( + (component: TacticComponent, isFromParent: boolean): ReactNode => { + if (component.type === "player" || component.type === "phantom") { + return renderPlayer(component, isFromParent) + } + if (component.type === BALL_TYPE) { + return + } + return <> + }, + [renderPlayer], + ) + + const renderActions = useCallback( + (component: TacticComponent, isFromParent: boolean) => + component.actions.map((action, i) => { + return ( + + ) + }), + [courtRef], + ) + + return ( + } + courtRef={courtRef} + renderComponent={renderComponent} + renderActions={renderActions} + /> + ) +} diff --git a/src/components/editor/BasketCourt.tsx b/src/components/editor/BasketCourt.tsx index e3cddd2..7c69d6d 100644 --- a/src/components/editor/BasketCourt.tsx +++ b/src/components/editor/BasketCourt.tsx @@ -1,13 +1,15 @@ -import { ReactElement, ReactNode, RefObject } from "react" +import { ReactElement, ReactNode, RefObject, useEffect, useState } from "react" import { Action } from "../../model/tactic/Action" import { CourtAction } from "./CourtAction" import { ComponentId, TacticComponent } from "../../model/tactic/Tactic" +import PlainCourt from "../../assets/court/full_court.svg?react" +import HalfCourt from "../../assets/court/half_court.svg?react" export interface BasketCourtProps { components: TacticComponent[] parentComponents: TacticComponent[] | null - previewAction: ActionPreview | null + previewAction?: ActionPreview | null renderComponent: (comp: TacticComponent, isFromParent: boolean) => ReactNode renderActions: (comp: TacticComponent, isFromParent: boolean) => ReactNode[] @@ -32,6 +34,14 @@ export function BasketCourt({ courtImage, courtRef, }: BasketCourtProps) { + + const [court, setCourt] = useState(courtRef.current) + + //force update once the court reference is set + useEffect(() => { + setCourt(courtRef.current) + }, [courtRef]) + return (
{courtImage} - {courtRef.current && + {court && parentComponents?.map((i) => renderComponent(i, true))} - {courtRef.current && + {court && parentComponents?.flatMap((i) => renderActions(i, true))} - {courtRef.current && + {court && components.map((i) => renderComponent(i, false))} - {courtRef.current && + {court && components.flatMap((i) => renderActions(i, false))} {previewAction && ( @@ -64,3 +74,15 @@ export function BasketCourt({
) } + +export function Court({ courtType }: { courtType: string }) { + return ( +
+ {courtType == "PLAIN" ? ( + + ) : ( + + )} +
+ ) +} diff --git a/src/components/editor/CourtBall.tsx b/src/components/editor/CourtBall.tsx index 503f44c..4a60335 100644 --- a/src/components/editor/CourtBall.tsx +++ b/src/components/editor/CourtBall.tsx @@ -20,27 +20,6 @@ export function CourtBall({ }: EditableCourtBallProps) { const pieceRef = useRef(null) - function courtBallPiece( - { x, y }: Pos, - pieceRef?: RefObject, - onKeyUp?: KeyboardEventHandler, - ) { - return ( -
- -
- ) - } - if (ball.frozen) { return courtBallPiece(ball.pos) } @@ -58,3 +37,32 @@ export function CourtBall({ ) } + +interface CourtBallPieceProps { + pos: Pos +} + +export function CourtBallPiece({ pos }: CourtBallPieceProps) { + return courtBallPiece(pos) +} + +function courtBallPiece( + { x, y }: Pos, + pieceRef?: RefObject, + onKeyUp?: KeyboardEventHandler, +) { + return ( +
+ +
+ ) +} diff --git a/src/components/editor/StepsTree.tsx b/src/components/editor/StepsTree.tsx index dde6534..0015a18 100644 --- a/src/components/editor/StepsTree.tsx +++ b/src/components/editor/StepsTree.tsx @@ -4,14 +4,14 @@ import BendableArrow from "../arrows/BendableArrow" import { ReactNode, useMemo, useRef } from "react" import AddSvg from "../../assets/icon/add.svg?react" import RemoveSvg from "../../assets/icon/remove.svg?react" -import { getStepName } from "../../editor/StepsDomain.ts" +import { getStepName } from "../../domains/StepsDomain.ts" export interface StepsTreeProps { root: StepInfoNode selectedStepId: number - onAddChildren: (parent: StepInfoNode) => void - onRemoveNode: (node: StepInfoNode) => void - onStepSelected: (node: StepInfoNode) => void + onAddChildren?: (parent: StepInfoNode) => void + onRemoveNode?: (node: StepInfoNode) => void + onStepSelected?: (node: StepInfoNode) => void } export default function StepsTree({ @@ -40,9 +40,9 @@ interface StepsTreeContentProps { rootNode: StepInfoNode selectedStepId: number - onAddChildren: (parent: StepInfoNode) => void - onRemoveNode: (node: StepInfoNode) => void - onStepSelected: (node: StepInfoNode) => void + onAddChildren?: (parent: StepInfoNode) => void + onRemoveNode?: (node: StepInfoNode) => void + onStepSelected?: (node: StepInfoNode) => void } function StepsTreeNode({ @@ -79,13 +79,19 @@ function StepsTreeNode({ onAddChildren(node)} + onAddButtonClicked={() => { + if (onAddChildren) onAddChildren(node) + }} onRemoveButtonClicked={ rootNode.id === node.id ? undefined - : () => onRemoveNode(node) + : () => { + if (onRemoveNode) onRemoveNode(node) + } } - onSelected={() => onStepSelected(node)}> + onSelected={() => { + if (onStepSelected) onStepSelected(node) + }}>

{useMemo( () => getStepName(rootNode, node.id), diff --git a/src/editor/ActionsDomains.ts b/src/domains/ActionsDomains.ts similarity index 98% rename from src/editor/ActionsDomains.ts rename to src/domains/ActionsDomains.ts index 8ad9e5a..2fa3229 100644 --- a/src/editor/ActionsDomains.ts +++ b/src/domains/ActionsDomains.ts @@ -3,16 +3,16 @@ import { Player, PlayerLike, PlayerPhantom, -} from "../model/tactic/Player" -import { ratioWithinBase } from "../geo/Pos" +} from "../model/tactic/Player.ts" +import { ratioWithinBase } from "../geo/Pos.ts" import { ComponentId, StepContent, TacticComponent, -} from "../model/tactic/Tactic" -import { overlaps } from "../geo/Box" -import { Action, ActionKind, moves } from "../model/tactic/Action" -import { removeBall, updateComponent } from "./TacticContentDomains" +} from "../model/tactic/Tactic.ts" +import { overlaps } from "../geo/Box.ts" +import { Action, ActionKind, moves } from "../model/tactic/Action.ts" +import { removeBall, updateComponent } from "./TacticContentDomains.ts" import { areInSamePath, getComponent, @@ -20,8 +20,8 @@ import { getPlayerNextTo, isNextInPath, removePlayer, -} from "./PlayerDomains" -import { BALL_TYPE } from "../model/tactic/CourtObjects" +} from "./PlayerDomains.ts" +import { BALL_TYPE } from "../model/tactic/CourtObjects.ts" export function getActionKind( target: TacticComponent | null, diff --git a/src/editor/PlayerDomains.ts b/src/domains/PlayerDomains.ts similarity index 88% rename from src/editor/PlayerDomains.ts rename to src/domains/PlayerDomains.ts index 2a7e28d..8c13c64 100644 --- a/src/editor/PlayerDomains.ts +++ b/src/domains/PlayerDomains.ts @@ -1,21 +1,22 @@ import { BallState, Player, + PlayerInfo, PlayerLike, PlayerPhantom, -} from "../model/tactic/Player" +} from "../model/tactic/Player.ts" import { ComponentId, StepContent, TacticComponent, -} from "../model/tactic/Tactic" +} from "../model/tactic/Tactic.ts" -import { removeComponent, updateComponent } from "./TacticContentDomains" +import { removeComponent, updateComponent } from "./TacticContentDomains.ts" import { removeAllActionsTargeting, spreadNewStateFromOriginStateChange, -} from "./ActionsDomains" -import { ActionKind } from "../model/tactic/Action" +} from "./ActionsDomains.ts" +import { ActionKind } from "../model/tactic/Action.ts" import { add, minus, @@ -311,7 +312,7 @@ export function truncatePlayerPath( for (let i = truncateStartIdx; i < path.items.length; i++) { const pathPhantomId = path.items[i] - //remove the phantom from the tactic + //remove the phantom from the domains content = removeComponent(pathPhantomId, content) content = removeAllActionsTargeting(pathPhantomId, content) } @@ -330,3 +331,46 @@ export function truncatePlayerPath( content, ) } + +export function getPhantomInfo( + phantom: PlayerPhantom, + content: StepContent, + relativePositions: ComputedRelativePositions, + courtBounds: DOMRect, +): PlayerInfo { + const origin = getOrigin(phantom, content.components) + + return { + id: phantom.id, + team: origin.team, + role: origin.role, + pos: computePhantomPositioning( + phantom, + content, + relativePositions, + courtBounds, + ), + ballState: phantom.ballState, + } +} + +export type ComputedRelativePositions = Map + +export function computeRelativePositions( + courtBounds: DOMRect, + content: StepContent, +) { + const relativePositionsCache: ComputedRelativePositions = new Map() + + for (const component of content.components) { + if (component.type !== "phantom") continue + computePhantomPositioning( + component, + content, + relativePositionsCache, + courtBounds, + ) + } + + return relativePositionsCache +} diff --git a/src/editor/StepsDomain.ts b/src/domains/StepsDomain.ts similarity index 97% rename from src/editor/StepsDomain.ts rename to src/domains/StepsDomain.ts index a2b739a..a120c23 100644 --- a/src/editor/StepsDomain.ts +++ b/src/domains/StepsDomain.ts @@ -1,4 +1,4 @@ -import { StepInfoNode } from "../model/tactic/Tactic" +import { StepInfoNode } from "../model/tactic/Tactic.ts" export function addStepNode( root: StepInfoNode, diff --git a/src/editor/TacticContentDomains.ts b/src/domains/TacticContentDomains.ts similarity index 97% rename from src/editor/TacticContentDomains.ts rename to src/domains/TacticContentDomains.ts index c7b2965..07fba44 100644 --- a/src/editor/TacticContentDomains.ts +++ b/src/domains/TacticContentDomains.ts @@ -1,4 +1,4 @@ -import { equals, Pos, ratioWithinBase } from "../geo/Pos" +import { equals, Pos, ratioWithinBase } from "../geo/Pos.ts" import { BallState, @@ -7,28 +7,28 @@ import { PlayerLike, PlayerPhantom, PlayerTeam, -} from "../model/tactic/Player" +} from "../model/tactic/Player.ts" import { Ball, BALL_ID, BALL_TYPE, CourtObject, -} from "../model/tactic/CourtObjects" +} from "../model/tactic/CourtObjects.ts" import { ComponentId, StepContent, TacticComponent, -} from "../model/tactic/Tactic" +} from "../model/tactic/Tactic.ts" -import { overlaps } from "../geo/Box" -import { RackedCourtObject, RackedPlayer } from "./RackedItems" +import { overlaps } from "../geo/Box.ts" +import { RackedCourtObject, RackedPlayer } from "../editor/RackedItems.ts" import { getComponent, getOrigin, getPrecomputedPosition, removePlayer, tryGetComponent, -} from "./PlayerDomains" +} from "./PlayerDomains.ts" import { Action, ActionKind } from "../model/tactic/Action.ts" import { spreadNewStateFromOriginStateChange } from "./ActionsDomains.ts" diff --git a/src/pages/Editor.tsx b/src/pages/Editor.tsx index f86db7c..eb6d7b1 100644 --- a/src/pages/Editor.tsx +++ b/src/pages/Editor.tsx @@ -1,5 +1,6 @@ import { CSSProperties, + ReactNode, RefObject, SetStateAction, useCallback, @@ -10,8 +11,6 @@ import { } from "react" import "../style/editor.css" 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" @@ -19,7 +18,6 @@ import { Rack } from "../components/Rack" import { PlayerPiece } from "../components/editor/PlayerPiece" import { - ComponentId, CourtType, StepContent, StepInfoNode, @@ -33,7 +31,11 @@ import SavingState, { import { BALL_TYPE } from "../model/tactic/CourtObjects" import { CourtAction } from "../components/editor/CourtAction" -import { ActionPreview, BasketCourt } from "../components/editor/BasketCourt" +import { + ActionPreview, + BasketCourt, + Court, +} from "../components/editor/BasketCourt" import { overlaps } from "../geo/Box" import { @@ -50,7 +52,7 @@ import { removeBall, selectContent, updateComponent, -} from "../editor/TacticContentDomains" +} from "../domains/TacticContentDomains.ts" import { BallState, @@ -71,16 +73,18 @@ import { isActionValid, removeAction, spreadNewStateFromOriginStateChange, -} from "../editor/ActionsDomains" +} from "../domains/ActionsDomains.ts" 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, + ComputedRelativePositions, + computeRelativePositions, getOrigin, + getPhantomInfo, removePlayer, -} from "../editor/PlayerDomains" +} from "../domains/PlayerDomains.ts" import { CourtBall } from "../components/editor/CourtBall" import StepsTree from "../components/editor/StepsTree" import { @@ -88,12 +92,15 @@ import { getParent, getStepNode, removeStepNode, -} from "../editor/StepsDomain" +} from "../domains/StepsDomain.ts" import SplitLayout from "../components/SplitLayout.tsx" -import { ServiceError, TacticService } from "../service/TacticService.ts" +import { + MutableTacticService, + ServiceError, +} from "../service/MutableTacticService.ts" import { LocalStorageTacticService } from "../service/LocalStorageTacticService.ts" import { APITacticService } from "../service/APITacticService.ts" -import { useParams } from "react-router-dom" +import { useNavigate, useParams } from "react-router-dom" import { ContentVersions } from "../editor/ContentVersions.ts" import { useAppFetcher } from "../App.tsx" @@ -101,21 +108,15 @@ const ERROR_STYLE: CSSProperties = { borderColor: "red", } -type ComputedRelativePositions = Map - type ComputedStepContent = { content: StepContent relativePositions: ComputedRelativePositions } -export interface EditorPageProps { +export interface EditorProps { guestMode: boolean } -export default function Editor({ guestMode }: EditorPageProps) { - return -} - interface EditorService { addStep( parent: StepInfoNode, @@ -129,28 +130,47 @@ interface EditorService { setContent(content: SetStateAction): void setName(name: string): Promise + + openVisualizer(): Promise } -function EditorPortal({ guestMode }: EditorPageProps) { +export default function Editor({ guestMode }: EditorProps) { const { tacticId: idStr } = useParams() const fetcher = useAppFetcher() + + const navigate = useNavigate() if (guestMode || !idStr) { - return + return ( + navigate("/tactic/view-guest")} + /> + ) } return ( navigate(`/tactic/${idStr}/view`)} /> ) } -function EditorPageWrapper({ service }: { service: TacticService }) { +interface EditorPageWrapperProps { + service: MutableTacticService + openVisualizer(): void +} + +function EditorPageWrapper({ + service, + openVisualizer, +}: EditorPageWrapperProps) { const [panicMessage, setPanicMessage] = useState() const [stepId, setStepId] = useState() const [tacticName, setTacticName] = useState() const [courtType, setCourtType] = useState() const [stepsTree, setStepsTree] = useState() + const [parentContent, setParentContent] = useState(null) const courtRef = useRef(null) @@ -209,8 +229,6 @@ function EditorPageWrapper({ service }: { service: TacticService }) { [stepsVersions, service, stepId, stepsTree], ) - const [parentContent, setParentContent] = useState(null) - const [stepContent, setStepContent, saveState] = useContentState( { components: [] }, @@ -334,8 +352,12 @@ function EditorPageWrapper({ service }: { service: TacticService }) { setStepId(step) setStepContent(result, false) }, + + async openVisualizer(): Promise { + openVisualizer() + }, } - }, [stepsVersions, service, setStepContent, stepsTree]) + }, [stepsTree, service, stepsVersions, setStepContent, openVisualizer]) if (panicMessage) { return

{panicMessage}

@@ -543,6 +565,7 @@ function EditorPage({ content, courtRef, doMoveBall, + parentContent, previewAction?.isInvalid, service.setContent, ], @@ -558,22 +581,14 @@ function EditorPage({ const usedContent = isFromParent ? parentContent! : content if (isPhantom) { - const origin = getOrigin(component, usedContent.components) - info = { - id: component.id, - team: origin.team, - role: origin.role, - pos: computePhantomPositioning( - component, - usedContent, - relativePositions, - courtBounds(), - ), - ballState: component.ballState, - } + info = getPhantomInfo( + component, + usedContent, + relativePositions, + courtBounds(), + ) } else { info = component - forceFreeze ||= component.frozen } @@ -612,8 +627,9 @@ function EditorPage({ ) }, [ - courtRef, + parentContent, content, + courtRef, relativePositions, courtBounds, renderAvailablePlayerActions, @@ -649,7 +665,7 @@ function EditorPage({ ) const renderComponent = useCallback( - (component: TacticComponent, isFromParent: boolean) => { + (component: TacticComponent, isFromParent: boolean): ReactNode => { if (component.type === "player" || component.type === "phantom") { return renderPlayer(component, isFromParent) } @@ -669,7 +685,7 @@ function EditorPage({ /> ) } - throw new Error("unknown tactic component " + component) + return <> }, [service, renderPlayer, doMoveBall], ) @@ -799,25 +815,26 @@ function EditorPage({
-
- { - service.setName(new_name).then((state) => { - setTitleStyle( - state == SaveStates.Ok - ? {} - : ERROR_STYLE, - ) - }) - }, - [service], - )} - /> -
+ { + service.setName(new_name).then((state) => { + setTitleStyle( + state == SaveStates.Ok ? {} : ERROR_STYLE, + ) + }) + }, + [service], + )} + />
+ +
+ +
+ {isStepsTreeVisible ? ( + + {contentNode} + {stepsTreeNode} + + ) : ( + contentNode + )} +
+ + ) +} diff --git a/src/service/APITacticService.ts b/src/service/APITacticService.ts index 3014294..371a4f5 100644 --- a/src/service/APITacticService.ts +++ b/src/service/APITacticService.ts @@ -1,8 +1,12 @@ -import { TacticService, ServiceError, TacticContext } from "./TacticService.ts" +import { + MutableTacticService, + ServiceError, + TacticContext, +} from "./MutableTacticService.ts" import { StepContent, StepInfoNode } from "../model/tactic/Tactic.ts" import { Fetcher } from "../app/Fetcher.ts" -export class APITacticService implements TacticService { +export class APITacticService implements MutableTacticService { private readonly tacticId: number private readonly fetcher: Fetcher diff --git a/src/service/LocalStorageTacticService.ts b/src/service/LocalStorageTacticService.ts index 2cce23d..7f5100b 100644 --- a/src/service/LocalStorageTacticService.ts +++ b/src/service/LocalStorageTacticService.ts @@ -1,16 +1,20 @@ -import { TacticService, ServiceError, TacticContext } from "./TacticService.ts" +import { + MutableTacticService, + ServiceError, + TacticContext, +} from "./MutableTacticService.ts" import { StepContent, StepInfoNode } from "../model/tactic/Tactic.ts" import { addStepNode, getAvailableId, removeStepNode, -} from "../editor/StepsDomain.ts" +} from "../domains/StepsDomain.ts" const GUEST_MODE_STEP_CONTENT_STORAGE_KEY = "guest_mode_step" const GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY = "guest_mode_step_tree" const GUEST_MODE_TITLE_STORAGE_KEY = "guest_mode_title" -export class LocalStorageTacticService implements TacticService { +export class LocalStorageTacticService implements MutableTacticService { private constructor() {} static init(): LocalStorageTacticService { diff --git a/src/service/TacticService.ts b/src/service/MutableTacticService.ts similarity index 92% rename from src/service/TacticService.ts rename to src/service/MutableTacticService.ts index cafa43d..2132c96 100644 --- a/src/service/TacticService.ts +++ b/src/service/MutableTacticService.ts @@ -13,7 +13,10 @@ export enum ServiceError { export interface TacticService { getContext(): Promise + getContent(step: number): Promise +} +export interface MutableTacticService extends TacticService { addStep( parent: StepInfoNode, content: StepContent, @@ -27,6 +30,4 @@ export interface TacticService { step: number, content: StepContent, ): Promise - - getContent(step: number): Promise } diff --git a/src/style/court.css b/src/style/court.css new file mode 100644 index 0000000..77ca11c --- /dev/null +++ b/src/style/court.css @@ -0,0 +1,24 @@ +.court-image-div { + position: relative; + background-color: white; + height: 80vh; +} + +.court-container { + display: flex; + align-content: center; + align-items: center; + justify-content: center; + + background-color: black; +} + +.court-image { + height: 100%; + width: 100%; + user-select: none; +} + +.court-image * { + stroke: var(--selected-team-secondarycolor); +} diff --git a/src/style/editor.css b/src/style/editor.css index 5ce1a38..bd93172 100644 --- a/src/style/editor.css +++ b/src/style/editor.css @@ -1,4 +1,6 @@ @import "theme/default.css"; +@import "court.css"; +@import "tactic.css"; #main-div { display: flex; @@ -41,7 +43,6 @@ .title-input { width: 25ch; - align-self: center; } #editor-div { @@ -58,6 +59,15 @@ #content-div { overflow: hidden; + display: flex; + flex-direction: column; +} + +#racks { + display: flex; + align-content: space-between; + justify-content: space-between; + width: 100%; } .curtain { @@ -94,14 +104,6 @@ margin-left: 5px; } -.player-piece.opponents { - background-color: var(--player-opponents-color); -} - -.player-piece.allies { - background-color: var(--player-allies-color); -} - #court-div { background-color: var(--background-color); @@ -114,35 +116,6 @@ align-content: center; } -#court-image-div { - position: relative; - background-color: white; - height: 80vh; -} - -.court-container { - display: flex; - align-content: center; - align-items: center; - justify-content: center; - - height: 75%; -} - -#court-image { - height: 100%; - width: 100%; - user-select: none; -} - -#court-image * { - stroke: var(--selected-team-secondarycolor); -} - -.react-draggable { - z-index: 2; -} - .save-state { display: flex; align-items: center; diff --git a/src/style/tactic.css b/src/style/tactic.css new file mode 100644 index 0000000..2d9a099 --- /dev/null +++ b/src/style/tactic.css @@ -0,0 +1,7 @@ +.player-piece.opponents { + background-color: var(--player-opponents-color); +} + +.player-piece.allies { + background-color: var(--player-allies-color); +} diff --git a/src/style/visualizer.css b/src/style/visualizer.css index 2d1a73f..64e1cf0 100644 --- a/src/style/visualizer.css +++ b/src/style/visualizer.css @@ -1,30 +1,72 @@ -#main { - height: 100vh; - width: 100%; +@import "court.css"; +@import "theme/default.css"; +@import "player.css"; +@import "tactic.css"; + +#visualizer { display: flex; + height: 100%; + width: 100%; + background-color: var(--background-color); + flex-direction: column; + + overflow: hidden; } -#topbar { +#editor-div { + height: 100%; +} + +.curtain { + width: 100%; + height: 100%; +} + +#content-div { display: flex; - background-color: var(--main-color); justify-content: center; align-items: center; + width: 100%; + height: 100%; } -h1 { +#topbar-div { + position: relative; + width: 100%; + display: flex; + background-color: var(--main-color); + align-content: flex-end; + justify-content: flex-end; +} + +#show-steps-button { + user-select: none; + align-self: flex-end; + height: 100%; +} + +#title { + align-self: center; + margin: 0; + user-select: none; + + position: absolute; + width: 100%; + text-align: center; - margin-top: 0; + pointer-events: none; } -#court-container { - flex: 1; +#topbar { display: flex; - justify-content: center; background-color: var(--main-color); + justify-content: center; + align-items: center; } -#court { - max-width: 80%; - max-height: 80%; +#steps-div { + background-color: var(--editor-tree-background); + overflow: scroll; + height: 100%; } diff --git a/src/visualizer/VisualizerState.ts b/src/visualizer/VisualizerState.ts new file mode 100644 index 0000000..cea3fc2 --- /dev/null +++ b/src/visualizer/VisualizerState.ts @@ -0,0 +1,45 @@ +import { CourtType, StepContent, StepInfoNode } from "../model/tactic/Tactic.ts" + +export interface VisualizerState { + stepId: number + tacticName: string + courtType: CourtType + stepsTree: StepInfoNode + content: StepContent + parentContent: StepContent | null +} + +export enum VisualizerStateActionKind { + INIT, + SET_CONTENTS, +} + +export type VisualizerStateAction = + | { + type: VisualizerStateActionKind.INIT + state: VisualizerState + } + | { + type: VisualizerStateActionKind.SET_CONTENTS + content: StepContent + parentContent: StepContent | null + stepId: number + } + +export function visualizerStateReducer( + state: VisualizerState | null, + action: VisualizerStateAction, +): VisualizerState | null { + switch (action.type) { + case VisualizerStateActionKind.INIT: + return action.state + case VisualizerStateActionKind.SET_CONTENTS: + if (state === null) throw Error("State is uninitialized !") + return { + ...state, + stepId: action.stepId, + content: action.content, + parentContent: action.parentContent, + } + } +}