From e1da73f6c5d41ca9f46467db0a3ca9961fd66f9e Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Wed, 20 Dec 2023 15:28:35 +0100 Subject: [PATCH] arrows now works with relative positions ratio from their parent --- front/Utils.ts | 12 -- front/assets/court/full_court.svg | 1 - front/components/actions/ArrowAction.tsx | 8 +- front/components/actions/BallAction.tsx | 17 +-- front/components/arrows/BendableArrow.tsx | 137 ++++++++++++---------- front/components/arrows/Pos.ts | 14 +++ front/components/editor/BasketCourt.tsx | 92 +++++++++++---- front/components/editor/CourtPlayer.tsx | 4 +- front/style/basket_court.css | 20 ---- front/style/editor.css | 27 ++++- front/views/Editor.tsx | 77 ++++++++---- front/views/editor/CourtAction.tsx | 9 +- 12 files changed, 255 insertions(+), 163 deletions(-) delete mode 100644 front/Utils.ts delete mode 100644 front/style/basket_court.css diff --git a/front/Utils.ts b/front/Utils.ts deleted file mode 100644 index 523d813..0000000 --- a/front/Utils.ts +++ /dev/null @@ -1,12 +0,0 @@ -export function calculateRatio( - it: { x: number; y: number }, - parent: DOMRect, -): { x: number; y: number } { - const relativeXPixels = it.x - parent.x - const relativeYPixels = it.y - parent.y - - const xRatio = relativeXPixels / parent.width - const yRatio = relativeYPixels / parent.height - - return { x: xRatio, y: yRatio } -} diff --git a/front/assets/court/full_court.svg b/front/assets/court/full_court.svg index cb59a6b..5bfc0de 100644 --- a/front/assets/court/full_court.svg +++ b/front/assets/court/full_court.svg @@ -1,5 +1,4 @@ - diff --git a/front/components/actions/ArrowAction.tsx b/front/components/actions/ArrowAction.tsx index 548d9b9..0c8058b 100644 --- a/front/components/actions/ArrowAction.tsx +++ b/front/components/actions/ArrowAction.tsx @@ -10,10 +10,10 @@ export interface ArrowActionProps { } export default function ArrowAction({ - onHeadDropped, - onHeadPicked, - onHeadMoved, - }: ArrowActionProps) { + onHeadDropped, + onHeadPicked, + onHeadMoved, +}: ArrowActionProps) { const arrowHeadRef = useRef(null) return ( diff --git a/front/components/actions/BallAction.tsx b/front/components/actions/BallAction.tsx index e8147c6..a26785c 100644 --- a/front/components/actions/BallAction.tsx +++ b/front/components/actions/BallAction.tsx @@ -1,23 +1,18 @@ -import {BallPiece} from "../editor/BallPiece"; -import Draggable from "react-draggable"; -import {useRef} from "react"; - +import { BallPiece } from "../editor/BallPiece" +import Draggable from "react-draggable" +import { useRef } from "react" export interface BallActionProps { onDrop: (el: HTMLElement) => void } -export default function BallAction({onDrop}: BallActionProps) { +export default function BallAction({ onDrop }: BallActionProps) { const ref = useRef(null) return ( - onDrop(ref.current!)} - nodeRef={ref} - > + onDrop(ref.current!)} nodeRef={ref}>
- +
) } - diff --git a/front/components/arrows/BendableArrow.tsx b/front/components/arrows/BendableArrow.tsx index 4e15843..f6d698e 100644 --- a/front/components/arrows/BendableArrow.tsx +++ b/front/components/arrows/BendableArrow.tsx @@ -1,17 +1,26 @@ import { CSSProperties, ReactElement, + RefObject, useCallback, useEffect, useRef, useState, } from "react" -import { angle, middlePos, Pos, relativeTo } from "./Pos" +import { + add, + angle, + middlePos, + Pos, + posWithinBase, + ratioWithinBase, +} from "./Pos" import "../../style/bendable_arrows.css" import Draggable from "react-draggable" export interface BendableArrowProps { + area: RefObject startPos: Pos segments: Segment[] onSegmentsChanges: (edges: Segment[]) => void @@ -55,6 +64,7 @@ function Triangle({ fill }: { fill: string }) { } export default function BendableArrow({ + area, startPos, segments, @@ -66,15 +76,10 @@ export default function BendableArrow({ }: BendableArrowProps) { const containerRef = useRef(null) const svgRef = useRef(null) - const pathRef = useRef(null) const styleWidth = style?.width ?? ArrowStyleDefaults.width - const [controlPointsDots, setControlPointsDots] = useState( - [], - ) - useEffect(() => { setInternalSegments(segments) }, [segments]) @@ -86,49 +91,38 @@ export default function BendableArrow({ const headRef = useRef(null) const tailRef = useRef(null) - useEffect(() => { - const basePos = - containerRef.current!.parentElement!.getBoundingClientRect() - - setControlPointsDots(computeControlPoints(basePos)) - - const selectionHandler = (e: MouseEvent) => { - if (!(e.target instanceof Node)) return - - const isSelected = containerRef.current!.contains(e.target) - setIsSelected(isSelected) - } - - document.addEventListener("mousedown", selectionHandler) - return () => document.removeEventListener("mousedown", selectionHandler) - }, []) - - function computeControlPoints(basePos: Pos) { - return internalSegments.map(({ next, controlPoint }, i) => { - const prev = i == 0 ? startPos : internalSegments[i - 1].next - - const prevRelative = relativeTo(prev, basePos) - const nextRelative = relativeTo(next, basePos) - - const cpPos = controlPoint - ? relativeTo(controlPoint, basePos) - : { - x: prevRelative.x / 2 + nextRelative.x / 2, - y: prevRelative.y / 2 + nextRelative.y / 2, - } + function computeControlPoints(parentBase: DOMRect) { + return segments.map(({ next, controlPoint }, i) => { + const prev = i == 0 ? startPos : segments[i - 1].next + + const prevRelative = posWithinBase(prev, parentBase) + const nextRelative = posWithinBase(next, parentBase) + + const cpPos = + controlPoint || + ratioWithinBase( + add( + { + x: prevRelative.x / 2 + nextRelative.x / 2, + y: prevRelative.y / 2 + nextRelative.y / 2, + }, + parentBase, + ), + parentBase, + ) return ( { - const segment = internalSegments[i] - const segments = internalSegments.toSpliced(i, 1, { + const segment = segments[i] + const newSegments = segments.toSpliced(i, 1, { ...segment, controlPoint, }) - onSegmentsChanges(segments) + onSegmentsChanges(newSegments) }} onMoves={(controlPoint) => { setInternalSegments((is) => { @@ -144,6 +138,8 @@ export default function BendableArrow({ } const update = useCallback(() => { + const parentBase = area.current!.getBoundingClientRect() + // only one segment is supported for now, which is the first. // any other segments will be ignored const segment = internalSegments[0] ?? null @@ -152,14 +148,11 @@ export default function BendableArrow({ const endPos = segment.next - const basePos = - containerRef.current!.parentElement!.getBoundingClientRect() - - const startRelative = relativeTo(startPos, basePos) - const endRelative = relativeTo(endPos!, basePos) + const startRelative = posWithinBase(startPos, parentBase) + const endRelative = posWithinBase(endPos, parentBase) const controlPoint = segment.controlPoint - ? relativeTo(segment.controlPoint, basePos) + ? posWithinBase(segment.controlPoint, parentBase) : { x: startRelative.x / 2 + endRelative.x / 2, y: startRelative.y / 2 + endRelative.y / 2, @@ -205,10 +198,13 @@ export default function BendableArrow({ const segmentsRelatives = internalSegments.map( ({ next, controlPoint }) => { return { - next: relativeTo(next, basePos), + next: posWithinBase(next, parentBase), cp: controlPoint - ? relativeTo(controlPoint, basePos) - : undefined, + ? posWithinBase(controlPoint, parentBase) + : { + x: startRelative.x / 2 + endRelative.x / 2, + y: startRelative.y / 2 + endRelative.y / 2, + }, } }, ) @@ -219,11 +215,7 @@ export default function BendableArrow({ if (idx == internalSegments.length - 1) { //if it is the last element - next = constraintInCircle(next, controlPoint, endRadius!) - } - - if (cp == undefined) { - return `L${next.x - left} ${next.y - top}` + next = constraintInCircle(next, cp, endRadius!) } return `C${cp.x - left} ${cp.y - top}, ${cp.x - left} ${ @@ -235,12 +227,27 @@ export default function BendableArrow({ const d = `M${tailPos.x - left} ${tailPos.y - top} ` + computedSegments pathRef.current!.setAttribute("d", d) Object.assign(svgRef.current!.style, svgStyle) - - setControlPointsDots(computeControlPoints(basePos)) }, [startPos, internalSegments]) useEffect(update, [update]) + useEffect(() => { + const selectionHandler = (e: MouseEvent) => { + if (!(e.target instanceof Node)) return + + const isSelected = containerRef.current!.contains(e.target) + setIsSelected(isSelected) + } + + document.addEventListener("mousedown", selectionHandler) + window.addEventListener("resize", update) + + return () => { + document.removeEventListener("mousedown", selectionHandler) + window.removeEventListener("resize", update) + } + }, [update, containerRef]) + return (
}
- {isSelected && controlPointsDots} + {isSelected && + computeControlPoints(area.current!.getBoundingClientRect())} ) } interface ControlPointProps { - pos: Pos - basePos: Pos + posRatio: Pos + parentBase: DOMRect onMoves: (currentPos: Pos) => void onPosValidated: (newPos: Pos | undefined) => void radius?: number } function ControlPoint({ - pos, + posRatio, + parentBase, onMoves, onPosValidated, radius = 7, }: ControlPointProps) { const ref = useRef(null) + const pos = posWithinBase(posRatio, parentBase) + return ( { const pointPos = middlePos(ref.current!.getBoundingClientRect()) - onPosValidated(pointPos) + onPosValidated(ratioWithinBase(pointPos, parentBase)) }} onDrag={() => { const pointPos = middlePos(ref.current!.getBoundingClientRect()) - onMoves(pointPos) + onMoves(ratioWithinBase(pointPos, parentBase)) }} position={{ x: pos.x - radius, y: pos.y - radius }}>
void onBallMoved: (ball: DOMRect) => void - courtImage: string + courtImage: () => ReactElement courtRef: RefObject } @@ -70,12 +72,21 @@ export function BasketCourt({ const targetPos = document .getElementById(player.id)! .getBoundingClientRect() + + const courtBounds = courtRef.current!.getBoundingClientRect() + + const start = ratioWithinBase( + middlePos(originRef.getBoundingClientRect()), + courtBounds, + ) + const end = ratioWithinBase(middlePos(targetPos), courtBounds) + const action: Action = { fromPlayerId: originRef.id, toPlayerId: player.id, type: MovementActionKind.SCREEN, - moveFrom: middlePos(originRef.getBoundingClientRect()), - segments: [{ next: middlePos(targetPos) }], + moveFrom: start, + segments: [{ next: end }], } setActions((actions) => [...actions, action]) return @@ -98,8 +109,11 @@ export function BasketCourt({ const [previewArrowEdges, setPreviewArrowEdges] = useState([]) const updateActionsRelatedTo = useCallback((player: Player) => { - const newPos = middlePos( - document.getElementById(player.id)!.getBoundingClientRect(), + const newPos = ratioWithinBase( + middlePos( + document.getElementById(player.id)!.getBoundingClientRect(), + ), + courtRef.current!.getBoundingClientRect(), ) setActions((actions) => actions.map((a) => { @@ -124,14 +138,18 @@ export function BasketCourt({ ) }, []) + const [internActions, setInternActions] = useState([]) + + useLayoutEffect(() => setInternActions(actions), [actions]) + return (
- {"court"} + {courtImage()} - {actions.map((action, idx) => renderAction(action, idx))} + {internActions.map((action, idx) => renderAction(action, idx))} {players.map((player) => ( , + onHeadMoved={(headPos) => { + const baseBounds = + courtRef.current!.getBoundingClientRect() setPreviewArrowEdges([ - { next: middlePos(headPos) }, + { + next: ratioWithinBase( + middlePos(headPos), + baseBounds, + ), + }, ]) - } + }} onHeadPicked={(headPos) => { + const baseBounds = + courtRef.current!.getBoundingClientRect() + setPreviewArrowOriginPos( - middlePos(pieceRef.getBoundingClientRect()), + ratioWithinBase( + middlePos( + pieceRef.getBoundingClientRect(), + ), + baseBounds, + ), ) setPreviewArrowEdges([ - { next: middlePos(headPos) }, + { + next: ratioWithinBase( + middlePos(headPos), + baseBounds, + ), + }, ]) setPreviewArrowEnabled(true) }} @@ -167,7 +205,14 @@ export function BasketCourt({ setPreviewArrowEnabled(false) }} />, - player.hasBall && onBallMoved(ref.getBoundingClientRect())}/> + player.hasBall && ( + + onBallMoved(ref.getBoundingClientRect()) + } + /> + ), ]} /> ))} @@ -188,6 +233,7 @@ export function BasketCourt({ {isPreviewArrowEnabled && ( { player: Player @@ -41,7 +41,7 @@ export default function CourtPlayer({ const pieceBounds = pieceRef.current!.getBoundingClientRect() const parentBounds = parentRef.current!.getBoundingClientRect() - const { x, y } = calculateRatio(pieceBounds, parentBounds) + const { x, y } = ratioWithinBase(pieceBounds, parentBounds) onChange({ id: player.id, diff --git a/front/style/basket_court.css b/front/style/basket_court.css deleted file mode 100644 index 92a520c..0000000 --- a/front/style/basket_court.css +++ /dev/null @@ -1,20 +0,0 @@ -#court-container { - display: flex; - align-content: center; - align-items: center; - justify-content: center; - height: 100%; - - background-color: var(--main-color); -} - -#court-svg { - margin: 35px 0 35px 0; - height: 87%; - user-select: none; - -webkit-user-drag: none; -} - -#court-svg * { - stroke: var(--selected-team-secondarycolor); -} diff --git a/front/style/editor.css b/front/style/editor.css index 196b40e..a305323 100644 --- a/front/style/editor.css +++ b/front/style/editor.css @@ -82,7 +82,9 @@ #court-div { background-color: var(--background-color); + height: 100%; + width: 100%; display: flex; align-items: center; @@ -90,11 +92,32 @@ align-content: center; } -#court-div-bounds { - padding: 20px 20px 20px 20px; +#court-image-div { + background-color: white; + height: 100%; + width: 100%; +} + +.court-container { + display: flex; + align-content: center; + align-items: center; + justify-content: center; + height: 75%; } +#court-image { + height: 100%; + width: 100%; + user-select: none; + -webkit-user-drag: none; +} + +#court-image * { + stroke: var(--selected-team-secondarycolor); +} + .react-draggable { z-index: 2; } diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index 5e9d32c..a44a2ad 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -1,25 +1,36 @@ -import {CSSProperties, Dispatch, SetStateAction, useCallback, useMemo, useRef, useState,} from "react" +import { + CSSProperties, + Dispatch, + SetStateAction, + useCallback, + useMemo, + useRef, + useState, +} from "react" import "../style/editor.css" import TitleInput from "../components/TitleInput" -import plainCourt from "../assets/court/full_court.svg" -import halfCourt from "../assets/court/half_court.svg" +import PlainCourt from "../assets/court/full_court.svg?react" +import HalfCourt from "../assets/court/half_court.svg?react" -import {BallPiece} from "../components/editor/BallPiece" +import { BallPiece } from "../components/editor/BallPiece" -import {Rack} from "../components/Rack" -import {PlayerPiece} from "../components/editor/PlayerPiece" -import {Player} from "../tactic/Player" +import { Rack } from "../components/Rack" +import { PlayerPiece } from "../components/editor/PlayerPiece" +import { Player } from "../tactic/Player" -import {Tactic, TacticContent} from "../tactic/Tactic" -import {fetchAPI} from "../Fetcher" -import {Team} from "../tactic/Team" -import {calculateRatio} from "../Utils" +import { Tactic, TacticContent } from "../tactic/Tactic" +import { fetchAPI } from "../Fetcher" +import { Team } from "../tactic/Team" -import SavingState, {SaveState, SaveStates,} from "../components/editor/SavingState" +import SavingState, { + SaveState, + SaveStates, +} from "../components/editor/SavingState" -import {CourtObject} from "../tactic/CourtObjects" -import {CourtAction} from "./editor/CourtAction" -import {BasketCourt} from "../components/editor/BasketCourt"; +import { CourtObject } from "../tactic/CourtObjects" +import { CourtAction } from "./editor/CourtAction" +import { BasketCourt } from "../components/editor/BasketCourt" +import { ratioWithinBase } from "../components/arrows/Pos" const ERROR_STYLE: CSSProperties = { borderColor: "red", @@ -150,7 +161,7 @@ function EditorView({ const refBounds = ref.getBoundingClientRect() const courtBounds = courtDivContentRef.current!.getBoundingClientRect() - const { x, y } = calculateRatio(refBounds, courtBounds) + const { x, y } = ratioWithinBase(refBounds, courtBounds) setContent((content) => { return { @@ -178,7 +189,7 @@ function EditorView({ const refBounds = ref.getBoundingClientRect() const courtBounds = courtDivContentRef.current!.getBoundingClientRect() - const { x, y } = calculateRatio(refBounds, courtBounds) + const { x, y } = ratioWithinBase(refBounds, courtBounds) let courtObject: CourtObject @@ -283,7 +294,7 @@ function EditorView({ } const courtBounds = courtDivContentRef.current!.getBoundingClientRect() - const { x, y } = calculateRatio(refBounds, courtBounds) + const { x, y } = ratioWithinBase(refBounds, courtBounds) let courtObject: CourtObject courtObject = { @@ -309,7 +320,10 @@ function EditorView({ ...content, players: toSplicedPlayers(content.players, player, false), objects: [...content.objects], - actions: content.actions.filter(a => a.toPlayerId !== player.id && a.fromPlayerId !== player.id), + actions: content.actions.filter( + (a) => + a.toPlayerId !== player.id && a.fromPlayerId !== player.id, + ), })) let setter switch (player.team) { @@ -420,12 +434,10 @@ function EditorView({ } courtRef={courtDivContentRef} - actions={content.actions} setActions={(actions) => setContent((content) => ({ ...content, @@ -437,10 +449,15 @@ function EditorView({ setContent((content) => ({ ...content, - actions: content.actions.toSpliced(i, 1, a), + actions: content.actions.toSpliced( + i, + 1, + a, + ), })) } /> @@ -486,6 +503,18 @@ function renderCourtObject(courtObject: RackedCourtObject) { throw new Error("unknown racked court object ", courtObject.key) } +function Court({ courtType }: { courtType: string }) { + return ( +
+ {courtType == "PLAIN" ? ( + + ) : ( + + )} +
+ ) +} + function getRackPlayers(team: Team, players: Player[]): RackedPlayer[] { return ["1", "2", "3", "4", "5"] .filter( diff --git a/front/views/editor/CourtAction.tsx b/front/views/editor/CourtAction.tsx index b7217ba..47dadad 100644 --- a/front/views/editor/CourtAction.tsx +++ b/front/views/editor/CourtAction.tsx @@ -1,14 +1,21 @@ import { Action } from "../../tactic/Action" import BendableArrow from "../../components/arrows/BendableArrow" +import { RefObject } from "react" export interface CourtActionProps { action: Action onActionChanges: (a: Action) => void + courtRef: RefObject } -export function CourtAction({ action, onActionChanges }: CourtActionProps) { +export function CourtAction({ + action, + onActionChanges, + courtRef, +}: CourtActionProps) { return ( {