spread changes of step content to its direct children
continuous-integration/drone/push Build is passing Details

maxime.batista 1 year ago committed by maxime
parent 293d21c162
commit cf89782153

@ -7,15 +7,14 @@ import {
import { ratioWithinBase } from "../geo/Pos"
import {
ComponentId,
TacticComponent,
StepContent,
TacticComponent,
} from "../model/tactic/Tactic"
import { overlaps } from "../geo/Box"
import { Action, ActionKind, moves } from "../model/tactic/Action"
import { removeBall, updateComponent } from "./TacticContentDomains"
import {
areInSamePath,
changePlayerBallState,
getComponent,
getOrigin,
getPlayerNextTo,
@ -411,20 +410,27 @@ export function removeAction(
(origin.type === "player" || origin.type === "phantom")
) {
if (target.type === "player" || target.type === "phantom")
content = changePlayerBallState(target, BallState.NONE, content)
content =
spreadNewStateFromOriginStateChange(
target,
BallState.NONE,
content,
) ?? content
if (origin.ballState === BallState.PASSED) {
content = changePlayerBallState(
origin,
BallState.HOLDS_BY_PASS,
content,
)
content =
spreadNewStateFromOriginStateChange(
origin,
BallState.HOLDS_BY_PASS,
content,
) ?? content
} else if (origin.ballState === BallState.PASSED_ORIGIN) {
content = changePlayerBallState(
origin,
BallState.HOLDS_ORIGIN,
content,
)
content =
spreadNewStateFromOriginStateChange(
origin,
BallState.HOLDS_ORIGIN,
content,
) ?? content
}
}
@ -456,6 +462,7 @@ export function removeAction(
/**
* Spreads the changes to others actions and components, directly or indirectly bound to the origin, implied by the change of the origin's actual state with
* the given newState.
* @returns the new state if it has been updated, or null if no changes were operated
* @param origin
* @param newState
* @param content
@ -464,9 +471,9 @@ export function spreadNewStateFromOriginStateChange(
origin: PlayerLike,
newState: BallState,
content: StepContent,
): StepContent {
): StepContent | null {
if (origin.ballState === newState) {
return content
return null
}
origin = {
@ -552,11 +559,12 @@ export function spreadNewStateFromOriginStateChange(
content = updateComponent(origin, content)
}
content = spreadNewStateFromOriginStateChange(
actionTarget,
targetState,
content,
)
content =
spreadNewStateFromOriginStateChange(
actionTarget,
targetState,
content,
) ?? content
}
return content

@ -55,11 +55,23 @@ export function getPlayerNextTo(
: getComponent<PlayerLike>(pathItems[targetIdx - 1], components)
}
//FIXME this function can be a bottleneck if the phantom's position is
// following another phantom and / or the origin of the phantom is another
export function getPrecomputedPosition(
phantom: PlayerPhantom,
computedPositions: Map<string, Pos>,
): Pos | undefined {
const positioning = phantom.pos
// If the position is already known and fixed, return the pos
if (positioning.type === "fixed") return positioning
return computedPositions.get(phantom.id)
}
export function computePhantomPositioning(
phantom: PlayerPhantom,
content: StepContent,
computedPositions: Map<string, Pos>,
area: DOMRect,
): Pos {
const positioning = phantom.pos
@ -67,6 +79,9 @@ export function computePhantomPositioning(
// If the position is already known and fixed, return the pos
if (positioning.type === "fixed") return positioning
const storedPos = computedPositions.get(phantom.id)
if (storedPos) return storedPos
// If the position is to determine (positioning.type = "follows"), determine the phantom's pos
// by calculating it from the referent position, and the action that targets the referent.
@ -77,7 +92,12 @@ export function computePhantomPositioning(
const referentPos =
referent.type === "player"
? referent.pos
: computePhantomPositioning(referent, content, area)
: computePhantomPositioning(
referent,
content,
computedPositions,
area,
)
// Get the origin
const origin = getOrigin(phantom, components)
@ -109,10 +129,11 @@ export function computePhantomPositioning(
pivotPoint =
playerBeforePhantom.type === "phantom"
? computePhantomPositioning(
playerBeforePhantom,
content,
area,
)
playerBeforePhantom,
content,
computedPositions,
area,
)
: playerBeforePhantom.pos
}
}
@ -126,14 +147,23 @@ export function computePhantomPositioning(
})
const segmentProjectionRatio: Pos = ratioWithinBase(segmentProjection, area)
return add(referentPos, segmentProjectionRatio)
const result = add(referentPos, segmentProjectionRatio)
computedPositions.set(phantom.id, result)
return result
}
export function getComponent<T extends TacticComponent>(
id: string,
components: TacticComponent[],
): T {
return components.find((c) => c.id === id)! as T
return tryGetComponent<T>(id, components)!
}
export function tryGetComponent<T extends TacticComponent>(
id: string,
components: TacticComponent[],
): T | undefined {
return components.find((c) => c.id === id) as T
}
export function areInSamePath(a: PlayerLike, b: PlayerLike) {
@ -254,10 +284,12 @@ export function removePlayer(
const actionTarget = content.components.find(
(c) => c.id === action.target,
)! as PlayerLike
return spreadNewStateFromOriginStateChange(
actionTarget,
BallState.NONE,
content,
return (
spreadNewStateFromOriginStateChange(
actionTarget,
BallState.NONE,
content,
) ?? content
)
}
@ -297,11 +329,3 @@ export function truncatePlayerPath(
content,
)
}
export function changePlayerBallState(
player: PlayerLike,
newState: BallState,
content: StepContent,
): StepContent {
return spreadNewStateFromOriginStateChange(player, newState, content)
}

@ -18,12 +18,24 @@ export function addStepNode(
}
}
export function getStepNode(
root: StepInfoNode,
stepId: number,
): StepInfoNode | undefined {
if (root.id === stepId) return root
for (const child of root.children) {
const result = getStepNode(child, stepId)
if (result) return result
}
}
export function removeStepNode(
root: StepInfoNode,
node: StepInfoNode,
): StepInfoNode | null {
): StepInfoNode | undefined {
if (root.id === node.id) {
return null
return undefined
}
return {

@ -1,4 +1,4 @@
import { Pos, ratioWithinBase } from "../geo/Pos"
import { equals, Pos, ratioWithinBase } from "../geo/Pos"
import {
BallState,
@ -23,12 +23,13 @@ import {
import { overlaps } from "../geo/Box"
import { RackedCourtObject, RackedPlayer } from "./RackedItems"
import {
changePlayerBallState,
computePhantomPositioning,
getComponent,
getOrigin,
getPrecomputedPosition,
tryGetComponent,
} from "./PlayerDomains"
import { ActionKind } from "../model/tactic/Action.ts"
import { spreadNewStateFromOriginStateChange } from "./ActionsDomains.ts"
export function placePlayerAt(
refBounds: DOMRect,
@ -103,7 +104,9 @@ export function dropBallOnComponent(
? BallState.HOLDS_ORIGIN
: BallState.HOLDS_BY_PASS
content = changePlayerBallState(component, newState, content)
content =
spreadNewStateFromOriginStateChange(component, newState, content) ??
content
}
return removeBall(content)
@ -194,9 +197,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(
{
@ -204,18 +207,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,
),
},
@ -228,9 +231,9 @@ export function moveComponent(
...component,
pos: isPhantom
? {
type: "fixed",
...newPos,
}
type: "fixed",
...newPos,
}
: newPos,
},
content,
@ -299,21 +302,21 @@ export function getRackPlayers(
/**
* Returns a step content that only contains the terminal state of each components inside the given content
* @param content
* @param courtArea
* @param computedPositions
*/
export function getTerminalState(
export function computeTerminalState(
content: StepContent,
courtArea: DOMRect,
computedPositions: Map<string, Pos>,
): StepContent {
const nonPhantomComponents: (Player | CourtObject)[] =
content.components.filter((c) => c.type !== "phantom") as (
| Player
| CourtObject
)[]
)[]
const componentsTargetedState = nonPhantomComponents.map((comp) =>
comp.type === "player"
? getPlayerTerminalState(comp, content, courtArea)
? getPlayerTerminalState(comp, content, computedPositions)
: comp,
)
@ -325,7 +328,7 @@ export function getTerminalState(
function getPlayerTerminalState(
player: Player,
content: StepContent,
area: DOMRect,
computedPositions: Map<string, Pos>,
): Player {
function stateAfter(state: BallState): BallState {
switch (state) {
@ -342,9 +345,15 @@ function getPlayerTerminalState(
}
function getTerminalPos(component: PlayerLike): Pos {
return component.type === "phantom"
? computePhantomPositioning(component, content, area)
: component.pos
if (component.type === "phantom") {
const pos = getPrecomputedPosition(component, computedPositions)
if (!pos)
throw new Error(
`Attempted to get the terminal state of a step content with missing position for phantom ${component.id}`,
)
return pos
}
return component.pos
}
const phantoms = player.path?.items
@ -377,3 +386,50 @@ function getPlayerTerminalState(
pos,
}
}
export function drainTerminalStateOnChildContent(
parentTerminalState: StepContent,
childContent: StepContent,
): StepContent | null {
let gotUpdated = false
for (const parentComponent of parentTerminalState.components) {
const childComponent = tryGetComponent(
parentComponent.id,
childContent.components,
)
if (!childComponent) {
//if the child does not contain the parent's component, add it to the children's content.
childContent = {
...childContent,
components: [...childContent.components, parentComponent],
}
gotUpdated = true
continue
}
// ensure that the component is a player
if (parentComponent.type !== "player" || childComponent.type !== "player") continue
const newContentResult = spreadNewStateFromOriginStateChange(
childComponent,
parentComponent.ballState,
childContent,
)
if (newContentResult) {
gotUpdated = true
childContent = newContentResult
}
// also update the position of the player if it has been moved
if (!equals(childComponent.pos, parentComponent.pos)) {
gotUpdated = true
childContent = updateComponent({
...childComponent,
pos: parentComponent.pos,
}, childContent)
}
}
return gotUpdated ? childContent : null
}

@ -3,6 +3,11 @@ export interface Pos {
y: number
}
export function equals(a: Pos, b: Pos): boolean {
return a.x === b.x && a.y === b.y
}
export const NULL_POS: Pos = { x: 0, y: 0 }
/**

@ -71,6 +71,8 @@ export type PhantomPositioning =
| FixedPhantomPositioning
| FollowsPhantomPositioning
/**
* A player phantom is a kind of component that represents the future state of a player
* according to the court's step information

@ -20,6 +20,7 @@ import { Rack } from "../components/Rack"
import { PlayerPiece } from "../components/editor/PlayerPiece"
import {
ComponentId,
CourtType,
StepContent,
StepInfoNode,
@ -39,10 +40,11 @@ import { ActionPreview, BasketCourt } from "../components/editor/BasketCourt"
import { overlaps } from "../geo/Box"
import {
computeTerminalState,
drainTerminalStateOnChildContent,
dropBallOnComponent,
getComponentCollided,
getRackPlayers,
getTerminalState,
moveComponent,
placeBallAt,
placeObjectAt,
@ -66,13 +68,13 @@ import {
getActionKind,
isActionValid,
removeAction,
spreadNewStateFromOriginStateChange,
} from "../editor/ActionsDomains"
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 {
changePlayerBallState,
computePhantomPositioning,
getOrigin,
removePlayer,
@ -84,6 +86,7 @@ import {
addStepNode,
getAvailableId,
getParent,
getStepNode,
removeStepNode,
} from "../editor/StepsDomain"
@ -96,13 +99,19 @@ const GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY = "guest_mode_step_tree"
const GUEST_MODE_TITLE_STORAGE_KEY = "guest_mode_title"
// The step identifier the editor will always open on
const DEFAULT_STEP_ID = 1
const ROOT_STEP_ID = 1
type ComputedRelativePositions = Map<ComponentId, Pos>
type ComputedStepContent = {
content: StepContent
relativePositions: ComputedRelativePositions
}
interface TacticDto {
id: number
name: string
courtType: CourtType
root: StepInfoNode
}
export interface EditorPageProps {
@ -122,13 +131,19 @@ function GuestModeEditor() {
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + "0",
)
const stepInitialContent = {
...(storageContent == null
? { components: [] }
: JSON.parse(storageContent)),
stepId: 0,
const stepInitialContent: ComputedStepContent = {
content: {
...(storageContent == null
? { components: [] }
: JSON.parse(storageContent)),
},
relativePositions: new Map(),
}
const rootStepNode: StepInfoNode = JSON.parse(
localStorage.getItem(GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY)!,
)
// initialize local storage if we launch in guest mode
if (storageContent == null) {
localStorage.setItem(
@ -136,37 +151,72 @@ function GuestModeEditor() {
JSON.stringify({ id: 0, children: [] }),
)
localStorage.setItem(
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + stepInitialContent.stepId,
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + ROOT_STEP_ID,
JSON.stringify(stepInitialContent),
)
}
const [stepId, setStepId] = useState(DEFAULT_STEP_ID)
const [stepId, setStepId] = useState(ROOT_STEP_ID)
const [stepContent, setStepContent, saveState] = useContentState(
stepInitialContent,
SaveStates.Guest,
useMemo(
() =>
debounceAsync(async (content: StepContent) => {
localStorage.setItem(
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + stepId,
JSON.stringify(content),
)
return SaveStates.Guest
}, 250),
[stepId],
debounceAsync(
async ({
content,
relativePositions,
}: ComputedStepContent) => {
localStorage.setItem(
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + stepId,
JSON.stringify(content),
)
const terminalState = computeTerminalState(
content,
relativePositions,
)
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),
)
}
}
return SaveStates.Guest
},
250,
),
[rootStepNode, stepId],
),
)
function getStepContent(step: number): StepContent {
return JSON.parse(
localStorage.getItem(GUEST_MODE_STEP_CONTENT_STORAGE_KEY + step)!,
)
}
return (
<EditorPage
tactic={{
id: -1,
rootStepNode: JSON.parse(
localStorage.getItem(
GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,
)!,
),
rootStepNode,
name:
localStorage.getItem(GUEST_MODE_TITLE_STORAGE_KEY) ??
"Nouvelle Tactique",
@ -184,13 +234,10 @@ function GuestModeEditor() {
(step) => {
setStepId(step)
setStepContent(
{
...JSON.parse(
localStorage.getItem(
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + step,
)!,
),
},
() => ({
content: getStepContent(step),
relativePositions: new Map(),
}),
false,
)
return
@ -235,39 +282,80 @@ function GuestModeEditor() {
function UserModeEditor() {
const [tactic, setTactic] = useState<TacticDto | null>(null)
const [stepsTree, setStepsTree] = useState<StepInfoNode>({ id: ROOT_STEP_ID, children: [] })
const { tacticId: idStr } = useParams()
const id = parseInt(idStr!)
const tacticId = parseInt(idStr!)
const navigation = useNavigate()
const [stepId, setStepId] = useState(1)
const saveContent = useCallback(
async ({ content, relativePositions }: ComputedStepContent) => {
const response = await fetchAPI(
`tactics/${tacticId}/steps/${stepId}`,
{ content },
"PUT",
)
const terminalStateContent = computeTerminalState(
content,
relativePositions,
)
const currentNode = getStepNode(stepsTree!, stepId)!
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 response = await fetchAPI(
`tactics/${tacticId}/steps/${child.id}`,
{ content: childUpdatedContent },
"PUT",
)
if (!response.ok) {
throw new Error(
"Error when updated new children content",
)
}
}
})
for (const task of tasks) {
await task
}
return response.ok ? SaveStates.Ok : SaveStates.Err
},
[tacticId, stepId, stepsTree],
)
const [stepContent, setStepContent, saveState] =
useContentState<StepContent>(
{ components: [] },
useContentState<ComputedStepContent>(
{
content: { components: [] },
relativePositions: new Map(),
},
SaveStates.Ok,
useMemo(
() =>
debounceAsync(async (content: StepContent) => {
const response = await fetchAPI(
`tactics/${id}/steps/${stepId}`,
{
content: {
components: content.components,
},
},
"PUT",
)
return response.ok ? SaveStates.Ok : SaveStates.Err
}, 250),
[id, stepId],
),
useMemo(() => debounceAsync(saveContent, 250), [saveContent]),
)
useEffect(() => {
async function initialize() {
const infoResponsePromise = fetchAPIGet(`tactics/${id}`)
const treeResponsePromise = fetchAPIGet(`tactics/${id}/tree`)
const infoResponsePromise = fetchAPIGet(`tactics/${tacticId}`)
const treeResponsePromise = fetchAPIGet(`tactics/${tacticId}/tree`)
const contentResponsePromise = fetchAPIGet(
`tactics/${id}/steps/${DEFAULT_STEP_ID}`,
`tactics/${tacticId}/steps/${ROOT_STEP_ID}`,
)
const infoResponse = await infoResponsePromise
@ -287,48 +375,59 @@ function UserModeEditor() {
const content = await contentResponse.json()
const { root } = await treeResponse.json()
setTactic({ id, name, courtType, root })
setStepContent(content, false)
setTactic({ id: tacticId, name, courtType })
setStepsTree(root)
setStepContent({ content, relativePositions: new Map() }, false)
}
initialize()
}, [id, idStr, navigation])
if (tactic === null)
initialize()
}, [tactic, tacticId, idStr, navigation, setStepContent])
const onNameChange = useCallback(
(name: string) =>
fetchAPI(`tactics/${id}/name`, { name }, "PUT").then((r) => r.ok),
[id],
fetchAPI(`tactics/${tacticId}/name`, { name }, "PUT").then((r) => r.ok),
[tacticId],
)
const selectStep = useCallback(
async (step: number) => {
const response = await fetchAPIGet(`tactics/${id}/steps/${step}`)
const response = await fetchAPIGet(`tactics/${tacticId}/steps/${step}`)
if (!response.ok) return
setStepId(step)
setStepContent({ ...(await response.json()) }, false)
setStepContent(
{
content: await response.json(),
relativePositions: new Map(),
},
false,
)
},
[id, setStepContent],
[tacticId, setStepContent],
)
const onAddStep = useCallback(
async (parent: StepInfoNode, content: StepContent) => {
const response = await fetchAPI(`tactics/${id}/steps`, {
const response = await fetchAPI(`tactics/${tacticId}/steps`, {
parentId: parent.id,
content,
})
if (!response.ok) return null
const { stepId } = await response.json()
return { id: stepId, children: [] }
const child = { id: stepId, children: [] }
setStepsTree(addStepNode(stepsTree, parent, child))
return child
},
[id],
[tacticId, stepsTree],
)
const onRemoveStep = useCallback(
(step: StepInfoNode) =>
fetchAPI(`tactics/${id}/steps/${step.id}`, {}, "DELETE").then(
(r) => r.ok,
),
[id],
async (step: StepInfoNode) => {
const response = await fetchAPI(`tactics/${tacticId}/steps/${step.id}`, {}, "DELETE")
setStepsTree(removeStepNode(stepsTree, step)!)
return response.ok
},
[tacticId, stepsTree],
)
if (!tactic) return <EditorLoadingScreen />
@ -336,9 +435,9 @@ function UserModeEditor() {
return (
<EditorPage
tactic={{
id,
id: tacticId,
name: tactic?.name ?? "",
rootStepNode: tactic?.root ?? { id: stepId, children: [] },
rootStepNode: stepsTree,
courtType: tactic?.courtType,
}}
currentStepId={stepId}
@ -359,10 +458,10 @@ function EditorLoadingScreen() {
export interface EditorViewProps {
tactic: TacticInfo
currentStepContent: StepContent
currentStepContent: ComputedStepContent
currentStepId: number
saveState: SaveState
setCurrentStepContent: Dispatch<SetStateAction<StepContent>>
setCurrentStepContent: Dispatch<SetStateAction<ComputedStepContent>>
selectStep: (stepId: number) => void
onNameChange: (name: string) => Promise<boolean>
@ -374,16 +473,16 @@ export interface EditorViewProps {
}
function EditorPage({
tactic: { name, rootStepNode: initialStepsNode, courtType },
currentStepId,
setCurrentStepContent: setContent,
currentStepContent: content,
saveState,
onNameChange,
selectStep,
onRemoveStep,
onAddStep,
}: EditorViewProps) {
tactic: { name, rootStepNode: initialStepsNode, courtType },
currentStepId,
setCurrentStepContent,
currentStepContent: { content, relativePositions },
saveState,
onNameChange,
selectStep,
onRemoveStep,
onAddStep,
}: EditorViewProps) {
const [titleStyle, setTitleStyle] = useState<CSSProperties>({})
const [rootStepsNode, setRootStepsNode] = useState(initialStepsNode)
@ -406,6 +505,32 @@ function EditorPage({
const [isStepsTreeVisible, setStepsTreeVisible] = useState(false)
const courtRef = useRef<HTMLDivElement>(null)
const courtBounds = useCallback(
() => courtRef.current!.getBoundingClientRect(),
[courtRef],
)
const setContent = useCallback(
(newState: SetStateAction<StepContent>) => {
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()
console.log("in set: ", relativePositions)
return {
content: state,
relativePositions,
}
})
},
[setCurrentStepContent],
)
const setComponents = (action: SetStateAction<TacticComponent[]>) => {
setContent((c) => ({
@ -415,11 +540,6 @@ function EditorPage({
}))
}
const courtBounds = useCallback(
() => courtRef.current!.getBoundingClientRect(),
[courtRef],
)
useEffect(() => {
setObjects(isBallOnCourt(content) ? [] : [{ key: "ball" }])
}, [setObjects, content])
@ -458,11 +578,12 @@ function EditorPage({
(newBounds: DOMRect, from?: PlayerLike) => {
setContent((content) => {
if (from) {
content = changePlayerBallState(
from,
BallState.NONE,
content,
)
content =
spreadNewStateFromOriginStateChange(
from,
BallState.NONE,
content,
) ?? content
}
content = placeBallAt(newBounds, courtBounds(), content)
@ -560,6 +681,7 @@ function EditorPage({
pos: computePhantomPositioning(
component,
content,
relativePositions,
courtBounds(),
),
ballState: component.ballState,
@ -585,10 +707,12 @@ function EditorPage({
)
},
[
content.components,
content,
relativePositions,
courtBounds,
validatePlayerPosition,
doRemovePlayer,
renderAvailablePlayerActions,
validatePlayerPosition,
],
)
@ -759,7 +883,10 @@ function EditorPage({
async (parent) => {
const addedNode = await onAddStep(
parent,
getTerminalState(content, courtBounds()),
computeTerminalState(
content,
relativePositions,
),
)
if (addedNode == null) {
console.error(
@ -772,7 +899,7 @@ function EditorPage({
addStepNode(root, parent, addedNode),
)
},
[content, courtBounds, onAddStep, selectStep],
[content, onAddStep, selectStep, relativePositions],
)}
onRemoveNode={useCallback(
async (removed) => {
@ -805,13 +932,13 @@ interface EditorStepsTreeProps {
}
function EditorStepsTree({
isVisible,
selectedStepId,
root,
onAddChildren,
onRemoveNode,
onStepSelected,
}: EditorStepsTreeProps) {
isVisible,
selectedStepId,
root,
onAddChildren,
onRemoveNode,
onStepSelected,
}: EditorStepsTreeProps) {
return (
<div
id="steps-div"
@ -840,12 +967,12 @@ interface PlayerRackProps {
}
function PlayerRack({
id,
objects,
setObjects,
courtRef,
setComponents,
}: PlayerRackProps) {
id,
objects,
setObjects,
courtRef,
setComponents,
}: PlayerRackProps) {
const courtBounds = useCallback(
() => courtRef.current!.getBoundingClientRect(),
[courtRef],
@ -899,15 +1026,15 @@ interface CourtPlayerArrowActionProps {
}
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],
@ -1048,12 +1175,8 @@ function debounceAsync<A, B>(
function useContentState<S>(
initialContent: S,
initialSaveState: SaveState,
saveStateCallback: (s: S) => Promise<SaveState>,
): [
S,
(newState: SetStateAction<S>, callSaveCallback: boolean) => void,
SaveState,
] {
applyStateCallback: (content: S) => Promise<SaveState>,
): [S, (newState: SetStateAction<S>, runCallback: boolean) => void, SaveState] {
const [content, setContent] = useState(initialContent)
const [savingState, setSavingState] = useState(initialSaveState)
@ -1067,15 +1190,38 @@ function useContentState<S>(
if (state !== content && callSaveCallback) {
setSavingState(SaveStates.Saving)
saveStateCallback(state)
applyStateCallback(state)
.then(setSavingState)
.catch(() => setSavingState(SaveStates.Err))
.catch((e) => {
setSavingState(SaveStates.Err)
console.error(e)
})
}
return state
})
},
[saveStateCallback],
[applyStateCallback],
)
return [content, setContentSynced, savingState]
}
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,
)
}
console.log("computed bounds: ", relativePositionsCache)
return relativePositionsCache
}
Loading…
Cancel
Save