parent
9e8606184c
commit
15b47f354e
@ -0,0 +1,224 @@
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { APITacticService } from "../service/APITacticService.ts"
|
||||
import { CourtType, StepContent, StepInfoNode, TacticComponent } from "../model/tactic/Tactic.ts"
|
||||
import { getParent } from "../domains/StepsDomain.ts"
|
||||
import { computeRelativePositions, getPhantomInfo } from "../domains/PlayerDomains.ts"
|
||||
import { PlayerInfo, PlayerLike } from "../model/tactic/Player.ts"
|
||||
import { CourtPlayer } from "./editor/CourtPlayer.tsx"
|
||||
import { BALL_TYPE } from "../model/tactic/CourtObjects.ts"
|
||||
import { CourtBallPiece } from "./editor/CourtBall.tsx"
|
||||
import { CourtAction } from "./editor/CourtAction.tsx"
|
||||
import { BasketCourt, Court } from "./editor/BasketCourt.tsx"
|
||||
import { TacticService } from "../service/MutableTacticService.ts"
|
||||
import { useAppFetcher } from "../App.tsx"
|
||||
|
||||
|
||||
export interface VisualizerProps {
|
||||
tacticId: number
|
||||
stepId?: number
|
||||
}
|
||||
|
||||
export function Visualizer({ tacticId, stepId }: VisualizerProps) {
|
||||
const [panicMessage, setPanicMessage] = useState<string | null>(null)
|
||||
const [courtType, setCourtType] = useState<CourtType | null>()
|
||||
const [stepsTree, setStepsTree] = useState<StepInfoNode | null>()
|
||||
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 <p>{panicMessage}</p>
|
||||
}
|
||||
if (isNotInit) {
|
||||
return <p>Loading...</p>
|
||||
}
|
||||
|
||||
return <StepVisualizer
|
||||
courtType={courtType}
|
||||
stepsTree={stepsTree}
|
||||
stepId={stepId}
|
||||
service={service}
|
||||
/>
|
||||
}
|
||||
|
||||
export interface StepVisualizerProps {
|
||||
stepId?: number
|
||||
stepsTree: StepInfoNode
|
||||
courtType: CourtType
|
||||
service: TacticService
|
||||
}
|
||||
|
||||
export function StepVisualizer({stepId, stepsTree, courtType, service}: StepVisualizerProps) {
|
||||
const [panicMessage, setPanicMessage] = useState<string | null>(null)
|
||||
const [content, setContent] = useState<StepContent | null>(null)
|
||||
const [parentContent, setParentContent] = useState<StepContent | null>()
|
||||
|
||||
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 <p>{panicMessage}</p>
|
||||
}
|
||||
|
||||
if (isNotInit) {
|
||||
return <p>Loading Content...</p>
|
||||
}
|
||||
|
||||
return (
|
||||
<VisualizerFrame
|
||||
content={content}
|
||||
parentContent={parentContent}
|
||||
courtType={courtType}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export interface VisualizerFrameProps {
|
||||
content: StepContent
|
||||
parentContent: StepContent | null
|
||||
courtType: CourtType
|
||||
}
|
||||
|
||||
export function VisualizerFrame({
|
||||
content,
|
||||
parentContent,
|
||||
courtType,
|
||||
}: VisualizerFrameProps) {
|
||||
|
||||
console.log(content, parentContent)
|
||||
|
||||
const courtRef = useRef<HTMLDivElement>(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 (
|
||||
<CourtPlayer
|
||||
key={component.id}
|
||||
playerInfo={info}
|
||||
className={className}
|
||||
availableActions={() => []}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[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 <CourtBallPiece key="ball" pos={component.pos} />
|
||||
}
|
||||
return <></>
|
||||
},
|
||||
[renderPlayer],
|
||||
)
|
||||
|
||||
const renderActions = useCallback(
|
||||
(component: TacticComponent, isFromParent: boolean) =>
|
||||
component.actions.map((action, i) => {
|
||||
return (
|
||||
<CourtAction
|
||||
key={"action-" + component.id + "-" + i}
|
||||
action={action}
|
||||
origin={component.id}
|
||||
courtRef={courtRef}
|
||||
color={isFromParent ? "gray" : "black"}
|
||||
isEditable={false}
|
||||
/>
|
||||
)
|
||||
}),
|
||||
[courtRef],
|
||||
)
|
||||
|
||||
return (
|
||||
<BasketCourt
|
||||
parentComponents={parentContent?.components ?? null}
|
||||
components={content.components}
|
||||
courtImage={<Court courtType={courtType} />}
|
||||
courtRef={courtRef}
|
||||
renderComponent={renderComponent}
|
||||
renderActions={renderActions}
|
||||
/>
|
||||
)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { StepInfoNode } from "../model/tactic/Tactic"
|
||||
import { StepInfoNode } from "../model/tactic/Tactic.ts"
|
||||
|
||||
export function addStepNode(
|
||||
root: StepInfoNode,
|
@ -1,23 +0,0 @@
|
||||
// import React, { CSSProperties, useState } from "react"
|
||||
// import "../style/visualizer.css"
|
||||
// import Court from "../assets/court/full_court.svg"
|
||||
//
|
||||
// export default function Visualizer({ id, name }: { id: number; name: string }) {
|
||||
// const [style, setStyle] = useState<CSSProperties>({})
|
||||
//
|
||||
// return (
|
||||
// <div id="main">
|
||||
// <div id="topbar">
|
||||
// <h1>{name}</h1>
|
||||
// </div>
|
||||
// <div id="court-container">
|
||||
// <img
|
||||
// id="court"
|
||||
// src={Court}
|
||||
// style={style}
|
||||
// alt="Basketball Court"
|
||||
// />
|
||||
// </div>
|
||||
// </div>
|
||||
// )
|
||||
// }
|
@ -0,0 +1,178 @@
|
||||
import { ServiceError, TacticService } from "../service/MutableTacticService.ts"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { useCallback, useEffect, useMemo, useReducer, useState } from "react"
|
||||
import { VisualizerState, VisualizerStateActionKind, visualizerStateReducer } from "../visualizer/VisualizerState.ts"
|
||||
import { getParent } from "../domains/StepsDomain.ts"
|
||||
import { mapToParentContent } from "../domains/TacticContentDomains.ts"
|
||||
import StepsTree from "../components/editor/StepsTree.tsx"
|
||||
import { StepInfoNode } from "../model/tactic/Tactic.ts"
|
||||
import SplitLayout from "../components/SplitLayout.tsx"
|
||||
import { LocalStorageTacticService } from "../service/LocalStorageTacticService.ts"
|
||||
import { APITacticService } from "../service/APITacticService.ts"
|
||||
|
||||
import "../style/visualizer.css"
|
||||
import { VisualizerFrame } from "../components/Visualizer.tsx"
|
||||
import { useAppFetcher } from "../App.tsx"
|
||||
|
||||
export interface VisualizerPageProps {
|
||||
guestMode: boolean
|
||||
}
|
||||
|
||||
export function VisualizerPage({ guestMode }: VisualizerPageProps) {
|
||||
const { tacticId: idStr } = useParams()
|
||||
|
||||
const fetcher = useAppFetcher()
|
||||
if (guestMode || !idStr) {
|
||||
return (
|
||||
<ServedVisualizerPage service={LocalStorageTacticService.init()} />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ServedVisualizerPage service={new APITacticService(fetcher, parseInt(idStr))} />
|
||||
)
|
||||
}
|
||||
|
||||
interface VisualizerService {
|
||||
selectStep(step: number): Promise<void | ServiceError>
|
||||
}
|
||||
|
||||
function ServedVisualizerPage({ service }: { service: TacticService }) {
|
||||
const [panicMessage, setPanicMessage] = useState<string>()
|
||||
const [state, dispatch] = useReducer(visualizerStateReducer, null)
|
||||
|
||||
useEffect(() => {
|
||||
async function init() {
|
||||
const contextResult = await service.getContext()
|
||||
if (typeof contextResult === "string") {
|
||||
setPanicMessage(
|
||||
"There has been an error retrieving the editor initial context : " +
|
||||
contextResult,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const stepId = contextResult.stepsTree.id
|
||||
const contentResult = await service.getContent(stepId)
|
||||
|
||||
if (typeof contentResult === "string") {
|
||||
setPanicMessage(
|
||||
"There has been an error retrieving the tactic's root step content : " +
|
||||
contentResult,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: VisualizerStateActionKind.INIT,
|
||||
state: {
|
||||
stepId,
|
||||
stepsTree: contextResult.stepsTree,
|
||||
courtType: contextResult.courtType,
|
||||
tacticName: contextResult.name,
|
||||
content: contentResult,
|
||||
parentContent: null,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (state === null) init()
|
||||
}, [service, state])
|
||||
|
||||
const visualizerService: VisualizerService = useMemo(
|
||||
() => ({
|
||||
async selectStep(step: number): Promise<void | ServiceError> {
|
||||
const result = await service.getContent(step)
|
||||
if (typeof result === "string") return result
|
||||
const stepParent = getParent(state!.stepsTree!, step)?.id
|
||||
let parentContent = null
|
||||
if (stepParent) {
|
||||
const parentResult = await service.getContent(stepParent)
|
||||
if (typeof parentResult === "string") return parentResult
|
||||
parentContent = mapToParentContent(parentResult)
|
||||
}
|
||||
dispatch({
|
||||
type: VisualizerStateActionKind.SET_CONTENTS,
|
||||
content: result,
|
||||
parentContent,
|
||||
stepId: step,
|
||||
})
|
||||
},
|
||||
}),
|
||||
[service, state],
|
||||
)
|
||||
|
||||
if (panicMessage) {
|
||||
return <p>{panicMessage}</p>
|
||||
}
|
||||
|
||||
if (state === null) {
|
||||
return <p>Retrieving tactic context. Please wait...</p>
|
||||
}
|
||||
|
||||
return <VisualizerPageContent state={state} service={visualizerService} />
|
||||
}
|
||||
|
||||
interface VisualizerPageContentProps {
|
||||
state: VisualizerState
|
||||
service: VisualizerService
|
||||
}
|
||||
|
||||
function VisualizerPageContent({
|
||||
state: { content, parentContent, stepId, stepsTree, courtType, tacticName },
|
||||
service,
|
||||
}: VisualizerPageContentProps) {
|
||||
const [isStepsTreeVisible, setStepsTreeVisible] = useState(false)
|
||||
|
||||
const [editorContentCurtainWidth, setEditorContentCurtainWidth] =
|
||||
useState(80)
|
||||
|
||||
const stepsTreeNode = (
|
||||
<div id={"steps-div"}>
|
||||
<StepsTree
|
||||
root={stepsTree}
|
||||
selectedStepId={stepId}
|
||||
onStepSelected={useCallback(
|
||||
(node: StepInfoNode) => service.selectStep(node.id),
|
||||
[service],
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
const contentNode = (
|
||||
<div id="content-div">
|
||||
<VisualizerFrame
|
||||
content={content}
|
||||
parentContent={parentContent}
|
||||
courtType={courtType}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div id="visualizer">
|
||||
<div id="topbar-div">
|
||||
<p id="title">{tacticName}</p>
|
||||
<button
|
||||
id={"show-steps-button"}
|
||||
onClick={() => setStepsTreeVisible((b) => !b)}>
|
||||
ETAPES
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="editor-div">
|
||||
{isStepsTreeVisible ? (
|
||||
<SplitLayout
|
||||
rightWidth={editorContentCurtainWidth}
|
||||
onRightWidthChange={setEditorContentCurtainWidth}>
|
||||
{contentNode}
|
||||
{stepsTreeNode}
|
||||
</SplitLayout>
|
||||
) : (
|
||||
contentNode
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -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);
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
.player-piece.opponents {
|
||||
background-color: var(--player-opponents-color);
|
||||
}
|
||||
|
||||
.player-piece.allies {
|
||||
background-color: var(--player-allies-color);
|
||||
}
|
@ -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%;
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue