You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
261 lines
7.4 KiB
261 lines
7.4 KiB
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"
|
|
import { mapIdentifiers } from "../domains/TacticContentDomains.ts"
|
|
|
|
export interface VisualizerProps {
|
|
tacticId: number
|
|
stepId?: number
|
|
visualizerId: string | number
|
|
}
|
|
|
|
export function Visualizer({
|
|
visualizerId,
|
|
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
|
|
visualizerId={visualizerId}
|
|
courtType={courtType}
|
|
stepsTree={stepsTree}
|
|
stepId={stepId}
|
|
service={service}
|
|
/>
|
|
)
|
|
}
|
|
|
|
export interface StepVisualizerProps {
|
|
stepId?: number
|
|
visualizerId: string | number
|
|
stepsTree: StepInfoNode
|
|
courtType: CourtType
|
|
service: TacticService
|
|
}
|
|
|
|
export function StepVisualizer({
|
|
stepId,
|
|
visualizerId,
|
|
stepsTree,
|
|
courtType,
|
|
service,
|
|
}: StepVisualizerProps) {
|
|
const [panicMessage, setPanicMessage] = useState<string | null>(null)
|
|
const [content, setContent] = useState<StepContent | null>(null)
|
|
const [parentContent, setParentContent] = useState<StepContent | null>(null)
|
|
|
|
const isNotInit = !content
|
|
|
|
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(
|
|
mapIdentifiers(contentResult, (id) => `${id}-${visualizerId}`),
|
|
)
|
|
if (parentContent) {
|
|
setParentContent(
|
|
mapIdentifiers(
|
|
parentContent,
|
|
(id) => `${id}-${visualizerId}-parent`,
|
|
),
|
|
)
|
|
}
|
|
}
|
|
|
|
if (isNotInit) init()
|
|
}, [isNotInit, visualizerId, 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) {
|
|
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}
|
|
/>
|
|
)
|
|
}
|