diff --git a/src/components/arrows/BendableArrow.tsx b/src/components/arrows/BendableArrow.tsx index 4b3615f..b730bbb 100644 --- a/src/components/arrows/BendableArrow.tsx +++ b/src/components/arrows/BendableArrow.tsx @@ -36,6 +36,7 @@ export interface BendableArrowProps { onSegmentsChanges: (edges: Segment[]) => void forceStraight: boolean wavy: boolean + readOnly: boolean startRadius?: number endRadius?: number @@ -87,6 +88,7 @@ function constraintInCircle(center: Pos, reference: Pos, radius: number): Pos { * @param segments * @param onSegmentsChanges * @param wavy + * @param readOnly * @param forceStraight * @param style * @param startRadius @@ -103,6 +105,7 @@ export default function BendableArrow({ forceStraight, wavy, + readOnly, style, startRadius = 0, @@ -531,7 +534,7 @@ export default function BendableArrow({ } fill="none" tabIndex={0} - onDoubleClick={addSegment} + onDoubleClick={readOnly ? undefined : addSegment} onKeyUp={(e) => { if (onDeleteRequested && e.key == "Delete") onDeleteRequested() @@ -555,6 +558,7 @@ export default function BendableArrow({ {!forceStraight && isSelected && + !readOnly && computePoints(area.current!.getBoundingClientRect())} ) diff --git a/src/components/editor/BasketCourt.tsx b/src/components/editor/BasketCourt.tsx index 68021f3..ba8c8c3 100644 --- a/src/components/editor/BasketCourt.tsx +++ b/src/components/editor/BasketCourt.tsx @@ -6,10 +6,11 @@ import { ComponentId, TacticComponent } from "../../model/tactic/Tactic" export interface BasketCourtProps { components: TacticComponent[] + parentComponents: TacticComponent[] | null previewAction: ActionPreview | null - renderComponent: (comp: TacticComponent) => ReactNode - renderActions: (comp: TacticComponent) => ReactNode[] + renderComponent: (comp: TacticComponent, isFromParent: boolean) => ReactNode + renderActions: (comp: TacticComponent, isFromParent: boolean) => ReactNode[] courtImage: ReactElement courtRef: RefObject @@ -22,6 +23,7 @@ export interface ActionPreview extends Action { export function BasketCourt({ components, + parentComponents, previewAction, renderComponent, @@ -37,15 +39,25 @@ export function BasketCourt({ style={{ position: "relative" }}> {courtImage} - {courtRef.current && components.map(renderComponent)} - {courtRef.current && components.flatMap(renderActions)} + {courtRef.current && + parentComponents && + parentComponents.map((i) => renderComponent(i, true))} + {courtRef.current && + parentComponents && + parentComponents.flatMap((i) => renderActions(i, true))} + + {courtRef.current && + components.map((i) => renderComponent(i, false))} + {courtRef.current && + components.flatMap((i) => renderActions(i, false))} {previewAction && ( {}} onActionChanges={() => {}} diff --git a/src/components/editor/CourtAction.tsx b/src/components/editor/CourtAction.tsx index 84f7fd5..54a6be2 100644 --- a/src/components/editor/CourtAction.tsx +++ b/src/components/editor/CourtAction.tsx @@ -7,22 +7,22 @@ import { ComponentId } from "../../model/tactic/Tactic" export interface CourtActionProps { origin: ComponentId action: Action - onActionChanges: (a: Action) => void - onActionDeleted: () => void + color: string courtRef: RefObject - isInvalid: boolean + isEditable: boolean + onActionChanges?: (a: Action) => void + onActionDeleted?: () => void } export function CourtAction({ origin, action, + color, onActionChanges, onActionDeleted, courtRef, - isInvalid, + isEditable, }: CourtActionProps) { - const color = isInvalid ? "red" : "black" - let head switch (action.type) { case ActionKind.DRIBBLE: @@ -49,9 +49,11 @@ export function CourtAction({ startPos={origin} segments={action.segments} onSegmentsChanges={(edges) => { - onActionChanges({ ...action, segments: edges }) + if (onActionChanges) + onActionChanges({ ...action, segments: edges }) }} wavy={action.type == ActionKind.DRIBBLE} + readOnly={!isEditable} //TODO place those magic values in constants endRadius={action.target ? 26 : 17} startRadius={10} diff --git a/src/components/editor/StepsTree.tsx b/src/components/editor/StepsTree.tsx index 952b268..dde6534 100644 --- a/src/components/editor/StepsTree.tsx +++ b/src/components/editor/StepsTree.tsx @@ -70,6 +70,7 @@ function StepsTreeNode({ onSegmentsChanges={() => {}} forceStraight={true} wavy={false} + readOnly={true} //TODO remove magic constants startRadius={10} endRadius={10} diff --git a/src/editor/StepsDomain.ts b/src/editor/StepsDomain.ts index e788a9c..a2b739a 100644 --- a/src/editor/StepsDomain.ts +++ b/src/editor/StepsDomain.ts @@ -98,9 +98,9 @@ export function getAvailableId(root: StepInfoNode): number { export function getParent( root: StepInfoNode, - node: StepInfoNode, + node: number, ): StepInfoNode | null { - if (root.children.find((n) => n.id === node.id)) return root + if (root.children.find((n) => n.id === node)) return root for (const child of root.children) { const result = getParent(child, node) diff --git a/src/editor/TacticContentDomains.ts b/src/editor/TacticContentDomains.ts index 319e4a0..da38860 100644 --- a/src/editor/TacticContentDomains.ts +++ b/src/editor/TacticContentDomains.ts @@ -472,3 +472,27 @@ export function drainTerminalStateOnChildContent( return gotUpdated ? childContent : null } + +export function mapToParentContent(content: StepContent): StepContent { + return { + ...content, + components: content.components.map((p) => { + if (p.type == "ball") return p + return { + ...p, + id: p.id + "-parent", + actions: p.actions.map((a) => ({ + ...a, + target: a.target + "-parent", + segments: a.segments.map((s) => ({ + ...s, + next: + typeof s.next === "string" + ? s.next + "-parent" + : s.next, + })), + })), + } + }), + } +} diff --git a/src/pages/Editor.tsx b/src/pages/Editor.tsx index cdcfedf..32593d0 100644 --- a/src/pages/Editor.tsx +++ b/src/pages/Editor.tsx @@ -42,6 +42,7 @@ import { dropBallOnComponent, getComponentCollided, getRackPlayers, + mapToParentContent, moveComponent, placeBallAt, placeObjectAt, @@ -201,6 +202,8 @@ function EditorPageWrapper({ service }: { service: TacticService }) { [stepsVersions, service, stepId, stepsTree], ) + const [parentContent, setParentContent] = useState(null) + const [stepContent, setStepContent, saveState] = useContentState( { components: [] }, @@ -270,21 +273,30 @@ function EditorPageWrapper({ service }: { service: TacticService }) { if (isNotInit) init() }, [isNotInit, service, setStepContent, stepsVersions]) - const editorService: EditorService = useMemo( - () => ({ + const editorService: EditorService = useMemo(() => { + let internalStepsTree = stepsTree + return { async addStep( parent: StepInfoNode, content: StepContent, ): Promise { const result = await service.addStep(parent, content) - if (typeof result !== "string") - setStepsTree(addStepNode(stepsTree!, parent, result)) + if (typeof result !== "string") { + internalStepsTree = addStepNode( + internalStepsTree!, + parent, + result, + ) + setStepsTree(internalStepsTree) + } return result }, async removeStep(step: number): Promise { const result = await service.removeStep(step) - if (typeof result !== "string") - setStepsTree(removeStepNode(stepsTree!, step)) + if (typeof result !== "string") { + internalStepsTree = removeStepNode(internalStepsTree!, step) + setStepsTree(internalStepsTree) + } stepsVersions.delete(step) return result }, @@ -303,12 +315,19 @@ function EditorPageWrapper({ service }: { service: TacticService }) { async selectStep(step: number): Promise { const result = await service.getContent(step) if (typeof result === "string") return result + const stepParent = getParent(internalStepsTree!, step)?.id + if (stepParent) { + const parentResult = await service.getContent(stepParent) + if (typeof parentResult === "string") return parentResult + setParentContent(mapToParentContent(parentResult)) + } else { + setParentContent(null) + } setStepId(step) setStepContent(result, false) }, - }), - [stepsVersions, service, setStepContent, stepsTree], - ) + } + }, [stepsVersions, service, setStepContent, stepsTree]) if (panicMessage) { return

{panicMessage}

@@ -325,6 +344,7 @@ function EditorPageWrapper({ service }: { service: TacticService }) { stepId={stepId} stepsTree={stepsTree} contentSaveState={saveState} + parentContent={parentContent} content={stepContent} service={editorService} courtRef={courtRef} @@ -336,10 +356,12 @@ export interface EditorViewProps { stepsTree: StepInfoNode name: string courtType: CourtType - content: StepContent contentSaveState: SaveState stepId: number + parentContent: StepContent | null + content: StepContent + courtRef: RefObject service: EditorService @@ -348,6 +370,7 @@ export interface EditorViewProps { function EditorPage({ name, courtType, + parentContent, content, stepId, contentSaveState, @@ -515,9 +538,12 @@ function EditorPage({ ) const renderPlayer = useCallback( - (component: PlayerLike) => { + (component: PlayerLike, isFromParent: boolean) => { let info: PlayerInfo const isPhantom = component.type == "phantom" + + let forceFreeze = isFromParent + if (isPhantom) { const origin = getOrigin(component, content.components) info = { @@ -535,24 +561,31 @@ function EditorPage({ } else { info = component - if (component.frozen) { - return ( - - renderAvailablePlayerActions(info, component) - } - /> - ) - } + forceFreeze ||= component.frozen + } + + const className = + (isPhantom ? "phantom" : "player") + + " " + + (isFromParent ? "from-parent" : "") + + if (forceFreeze) { + return ( + + renderAvailablePlayerActions(info, component) + } + /> + ) } return ( validatePlayerPosition(component, info, newPos) @@ -603,11 +636,11 @@ function EditorPage({ ) const renderComponent = useCallback( - (component: TacticComponent) => { + (component: TacticComponent, isFromParent: boolean) => { if (component.type === "player" || component.type === "phantom") { - return renderPlayer(component) + return renderPlayer(component, isFromParent) } - if (component.type === BALL_TYPE) { + if (component.type === BALL_TYPE && !isFromParent) { return ( + (component: TacticComponent, isFromParent: boolean) => component.actions.map((action, i) => { return ( { - doDeleteAction(action, i, component) + if (!isFromParent) + doDeleteAction(action, i, component) + }} + onActionChanges={(action) => { + if (!isFromParent) + doUpdateAction(component, action, i) }} - onActionChanges={(action) => - doUpdateAction(component, action, i) - } /> ) }), @@ -697,6 +733,7 @@ function EditorPage({
} courtRef={courtRef} @@ -730,7 +767,9 @@ function EditorPage({ onRemoveNode={useCallback( async (removed) => { await service.removeStep(removed.id) - await service.selectStep(getParent(stepsTree, removed)!.id) + await service.selectStep( + getParent(stepsTree, removed.id)!.id, + ) }, [service, stepsTree], )} diff --git a/src/style/player.css b/src/style/player.css index b03123b..80dd4e5 100644 --- a/src/style/player.css +++ b/src/style/player.css @@ -6,6 +6,11 @@ opacity: 50%; } +.from-parent .player-piece { + color: white; + background-color: var(--player-from-parent-color); +} + .player-content { display: flex; flex-direction: column; diff --git a/src/style/theme/default.css b/src/style/theme/default.css index 19702b0..05ed963 100644 --- a/src/style/theme/default.css +++ b/src/style/theme/default.css @@ -9,6 +9,7 @@ --selected-team-secondarycolor: #000000; --player-allies-color: #64e4f5; --player-opponents-color: #f59264; + --player-from-parent-color: #494949; --buttons-shadow-color: #a8a8a8;