From afd7b0570c8b687c3b5345f13219ec5966fadfb5 Mon Sep 17 00:00:00 2001 From: maxime Date: Sun, 17 Dec 2023 18:19:31 +0100 Subject: [PATCH] reset arrow curves by deleting the central control point --- front/components/actions/ArrowAction.tsx | 10 +- front/components/actions/BallAction.tsx | 3 +- front/components/arrows/BendableArrow.tsx | 185 ++++++++++++---------- front/components/arrows/Pos.ts | 10 +- front/components/editor/BasketCourt.tsx | 108 ++++++++----- front/components/editor/CourtPlayer.tsx | 29 ++-- front/style/bendable_arrows.css | 9 +- front/tactic/Action.ts | 4 +- front/tactic/Player.ts | 1 - front/views/Editor.tsx | 27 ++-- front/views/editor/CourtAction.tsx | 13 +- 11 files changed, 226 insertions(+), 173 deletions(-) diff --git a/front/components/actions/ArrowAction.tsx b/front/components/actions/ArrowAction.tsx index 54a5527..548d9b9 100644 --- a/front/components/actions/ArrowAction.tsx +++ b/front/components/actions/ArrowAction.tsx @@ -1,7 +1,7 @@ import "../../style/actions/arrow_action.css" import Draggable from "react-draggable" -import {useRef} from "react" +import { useRef } from "react" export interface ArrowActionProps { onHeadDropped: (headBounds: DOMRect) => void @@ -18,7 +18,7 @@ export default function ArrowAction({ return (
-
+
-
+ position={{ x: 0, y: 0 }}> +
) diff --git a/front/components/actions/BallAction.tsx b/front/components/actions/BallAction.tsx index 5dc8c78..e8147c6 100644 --- a/front/components/actions/BallAction.tsx +++ b/front/components/actions/BallAction.tsx @@ -19,4 +19,5 @@ export default function BallAction({onDrop}: BallActionProps) {
) -} \ No newline at end of file +} + diff --git a/front/components/arrows/BendableArrow.tsx b/front/components/arrows/BendableArrow.tsx index d37805d..4e15843 100644 --- a/front/components/arrows/BendableArrow.tsx +++ b/front/components/arrows/BendableArrow.tsx @@ -1,5 +1,12 @@ -import {CSSProperties, ReactElement, useCallback, useEffect, useRef, useState,} from "react" -import {angle, middlePos, Pos, relativeTo} from "./Pos" +import { + CSSProperties, + ReactElement, + useCallback, + useEffect, + useRef, + useState, +} from "react" +import { angle, middlePos, Pos, relativeTo } from "./Pos" import "../../style/bendable_arrows.css" import Draggable from "react-draggable" @@ -39,27 +46,24 @@ function constraintInCircle(pos: Pos, from: Pos, radius: number): Pos { } } - -function Triangle({fill}: {fill: string}) { +function Triangle({ fill }: { fill: string }) { return ( - + ) } export default function BendableArrow({ - startPos, - - segments, - onSegmentsChanges, - - style, - startRadius = 0, - endRadius = 0, - }: BendableArrowProps) { + startPos, + segments, + onSegmentsChanges, + style, + startRadius = 0, + endRadius = 0, +}: BendableArrowProps) { const containerRef = useRef(null) const svgRef = useRef(null) @@ -73,47 +77,45 @@ export default function BendableArrow({ useEffect(() => { setInternalSegments(segments) - }, [segments]); + }, [segments]) const [internalSegments, setInternalSegments] = useState(segments) const [isSelected, setIsSelected] = useState(false) - const headRef = useRef(null) const tailRef = useRef(null) useEffect(() => { - - const basePos = containerRef.current!.parentElement!.getBoundingClientRect() + const basePos = + containerRef.current!.parentElement!.getBoundingClientRect() setControlPointsDots(computeControlPoints(basePos)) const selectionHandler = (e: MouseEvent) => { - if (!(e.target instanceof Node)) - return + if (!(e.target instanceof Node)) return - setIsSelected(containerRef.current!.contains(e.target)) + const isSelected = containerRef.current!.contains(e.target) + setIsSelected(isSelected) } - document.addEventListener('mousedown', selectionHandler) - return () => document.removeEventListener('mousedown', selectionHandler) - - }, []); - + document.addEventListener("mousedown", selectionHandler) + return () => document.removeEventListener("mousedown", selectionHandler) + }, []) function computeControlPoints(basePos: Pos) { - return internalSegments.map(({next, controlPoint}, i) => { + 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, - } + const cpPos = controlPoint + ? relativeTo(controlPoint, basePos) + : { + x: prevRelative.x / 2 + nextRelative.x / 2, + y: prevRelative.y / 2 + nextRelative.y / 2, + } return ( { const segment = internalSegments[i] - const segments = internalSegments.toSpliced(i, 1, {...segment, controlPoint}) + const segments = internalSegments.toSpliced(i, 1, { + ...segment, + controlPoint, + }) onSegmentsChanges(segments) }} onMoves={(controlPoint) => { - setInternalSegments(is => { - return is.toSpliced(i, 1, {...is[i], controlPoint}) + setInternalSegments((is) => { + return is.toSpliced(i, 1, { + ...is[i], + controlPoint, + }) }) }} /> @@ -135,9 +143,7 @@ export default function BendableArrow({ }) } - const update = useCallback(() => { - // only one segment is supported for now, which is the first. // any other segments will be ignored const segment = internalSegments[0] ?? null @@ -146,16 +152,18 @@ export default function BendableArrow({ const endPos = segment.next - const basePos = containerRef.current!.parentElement!.getBoundingClientRect() + const basePos = + containerRef.current!.parentElement!.getBoundingClientRect() const startRelative = relativeTo(startPos, basePos) const endRelative = relativeTo(endPos!, basePos) - const controlPoint = segment.controlPoint ? relativeTo(segment.controlPoint, basePos) : { - x: startRelative.x / 2 + endRelative.x / 2, - y: startRelative.y / 2 + endRelative.y / 2, - } - + const controlPoint = segment.controlPoint + ? relativeTo(segment.controlPoint, basePos) + : { + x: startRelative.x / 2 + endRelative.x / 2, + y: startRelative.y / 2 + endRelative.y / 2, + } const tailPos = constraintInCircle( startRelative, @@ -171,53 +179,56 @@ export default function BendableArrow({ const left = Math.min(tailPos.x, headPos.x) const top = Math.min(tailPos.y, headPos.y) - Object.assign(tailRef.current!.style, { left: tailPos.x + "px", top: tailPos.y + "px", transformOrigin: "top center", - transform: `translateX(-50%) rotate(${-angle(tailPos, controlPoint) * (180 / Math.PI)}deg)` + transform: `translateX(-50%) rotate(${ + -angle(tailPos, controlPoint) * (180 / Math.PI) + }deg)`, } as CSSProperties) - Object.assign(headRef.current!.style, { left: headPos.x + "px", top: headPos.y + "px", transformOrigin: "top center", - transform: `translateX(-50%) rotate(${-angle(headPos, controlPoint) * (180 / Math.PI)}deg)` + transform: `translateX(-50%) rotate(${ + -angle(headPos, controlPoint) * (180 / Math.PI) + }deg)`, } as CSSProperties) - const svgStyle: CSSProperties = { left: left + "px", top: top + "px", } - const segmentsRelatives = internalSegments.map(({next, controlPoint}) => { - return { - next: relativeTo(next, basePos), - cp: controlPoint ? relativeTo(controlPoint, basePos) : undefined - } - }) + const segmentsRelatives = internalSegments.map( + ({ next, controlPoint }) => { + return { + next: relativeTo(next, basePos), + cp: controlPoint + ? relativeTo(controlPoint, basePos) + : undefined, + } + }, + ) const computedSegments = segmentsRelatives - .map(({next: n, cp}, idx) => { + .map(({ next: n, cp }, idx) => { let next = n if (idx == internalSegments.length - 1) { //if it is the last element - next = constraintInCircle( - next, - controlPoint, - endRadius!, - ) + next = constraintInCircle(next, controlPoint, endRadius!) } if (cp == undefined) { return `L${next.x - left} ${next.y - top}` } - return `C${cp.x - left} ${cp.y - top}, ${cp.x - left} ${cp.y - top}, ${next.x - left} ${next.y - top}` + return `C${cp.x - left} ${cp.y - top}, ${cp.x - left} ${ + cp.y - top + }, ${next.x - left} ${next.y - top}` }) .join(" ") @@ -225,16 +236,15 @@ export default function BendableArrow({ pathRef.current!.setAttribute("d", d) Object.assign(svgRef.current!.style, svgStyle) - if (isSelected) { - setControlPointsDots(computeControlPoints(basePos)) - } + setControlPointsDots(computeControlPoints(basePos)) }, [startPos, internalSegments]) - useEffect(update, [update]) return ( -
+
-
- {style?.head?.call(style) ?? } +
+ {style?.head?.call(style) ?? }
-
- {style?.tail?.call(style) ?? } +
+ {style?.tail?.call(style) ?? }
{isSelected && controlPointsDots} @@ -272,18 +282,18 @@ export default function BendableArrow({ interface ControlPointProps { pos: Pos - basePos: Pos, + basePos: Pos onMoves: (currentPos: Pos) => void - onPosValidated: (newPos: Pos) => void, + onPosValidated: (newPos: Pos | undefined) => void radius?: number } function ControlPoint({ - pos, - onMoves, - onPosValidated, - radius = 7, - }: ControlPointProps) { + pos, + onMoves, + onPosValidated, + radius = 7, +}: ControlPointProps) { const ref = useRef(null) return ( @@ -297,8 +307,7 @@ function ControlPoint({ const pointPos = middlePos(ref.current!.getBoundingClientRect()) onMoves(pointPos) }} - position={{x: pos.x - radius, y: pos.y - radius}} - > + position={{ x: pos.x - radius, y: pos.y - radius }}>
{ + if (e.key == "Delete") { + onPosValidated(undefined) + } + }} + tabIndex={0} /> ) diff --git a/front/components/arrows/Pos.ts b/front/components/arrows/Pos.ts index d829015..c3a1863 100644 --- a/front/components/arrows/Pos.ts +++ b/front/components/arrows/Pos.ts @@ -3,7 +3,7 @@ export interface Pos { y: number } -export const NULL_POS: Pos = {x: 0, y: 0} +export const NULL_POS: Pos = { x: 0, y: 0 } /** * Returns position of a relative to b @@ -11,7 +11,7 @@ export const NULL_POS: Pos = {x: 0, y: 0} * @param b */ export function relativeTo(a: Pos, b: Pos): Pos { - return {x: a.x - b.x, y: a.y - b.y} + return { x: a.x - b.x, y: a.y - b.y } } /** @@ -19,7 +19,7 @@ export function relativeTo(a: Pos, b: Pos): Pos { * @param rect */ export function middlePos(rect: DOMRect): Pos { - return {x: rect.x + rect.width / 2, y: rect.y + rect.height / 2} + return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 } } /** @@ -28,11 +28,11 @@ export function middlePos(rect: DOMRect): Pos { * @param b */ export function size(a: Pos, b: Pos): Pos { - return {x: Math.abs(a.x - b.x), y: Math.abs(a.y - b.y)} + return { x: Math.abs(a.x - b.x), y: Math.abs(a.y - b.y) } } export function add(a: Pos, b: Pos): Pos { - return {x: a.x + b.x, y: a.y + b.y} + return { x: a.x + b.x, y: a.y + b.y } } export function angle(a: Pos, b: Pos): number { diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index 822ab62..4e04ea4 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -3,29 +3,31 @@ import {CourtBall} from "./CourtBall"; import {ReactElement, RefObject, useCallback, useState,} from "react" + import CourtPlayer from "./CourtPlayer" -import {Player} from "../../tactic/Player" -import {Action, MovementActionKind} from "../../tactic/Action" +import { Player } from "../../tactic/Player" +import { Action, MovementActionKind } from "../../tactic/Action" import RemoveAction from "../actions/RemoveAction" import ArrowAction from "../actions/ArrowAction" -import BendableArrow, {Segment} from "../arrows/BendableArrow" -import {middlePos, NULL_POS, Pos} from "../arrows/Pos" -import BallAction from "../actions/BallAction"; +import BendableArrow, { Segment } from "../arrows/BendableArrow" +import { middlePos, NULL_POS, Pos } from "../arrows/Pos" +import BallAction from "../actions/BallAction" import {CourtObject} from "../../tactic/CourtObjects"; export interface BasketCourtProps { players: Player[] actions: Action[] objects: CourtObject[] + renderAction: (a: Action, key: number) => ReactElement setActions: (f: (a: Action[]) => Action[]) => void + onPlayerRemove: (p: Player) => void onPlayerChange: (p: Player) => void onBallRemove: () => void - onBallMoved: (ball: DOMRect) => void courtImage: string @@ -33,20 +35,21 @@ export interface BasketCourtProps { } export function BasketCourt({ - objects, - onBallMoved, - onBallRemove, - players, - actions, - renderAction, - setActions, - onPlayerRemove, - onPlayerChange, - courtImage, - courtRef, - }: BasketCourtProps) { - - function bindArrowToPlayer(originRef: HTMLElement, arrowHead: DOMRect) { + players, + actions, + objects, + renderAction, + setActions, + onPlayerRemove, + onPlayerChange, + + onBallMoved, + onBallRemove, + + courtImage, + courtRef, +}: BasketCourtProps) { + function placeArrow(originRef: HTMLElement, arrowHead: DOMRect) { for (const player of players) { if (player.id == originRef.id) { continue @@ -64,17 +67,28 @@ export function BasketCourt({ playerBounds.left > arrowHead.right ) ) { - const targetPos = document.getElementById(player.id)!.getBoundingClientRect() + const targetPos = document + .getElementById(player.id)! + .getBoundingClientRect() const action: Action = { fromPlayerId: originRef.id, toPlayerId: player.id, type: MovementActionKind.SCREEN, moveFrom: middlePos(originRef.getBoundingClientRect()), - segments: [{next: middlePos(targetPos)}], + segments: [{ next: middlePos(targetPos) }], } setActions((actions) => [...actions, action]) + return } } + + const action: Action = { + fromPlayerId: originRef.id, + type: MovementActionKind.MOVE, + moveFrom: middlePos(originRef.getBoundingClientRect()), + segments: [{ next: middlePos(arrowHead) }], + } + setActions((actions) => [...actions, action]) } const [previewArrowOriginPos, setPreviewArrowOriginPos] = @@ -84,27 +98,38 @@ export function BasketCourt({ const [previewArrowEdges, setPreviewArrowEdges] = useState([]) const updateActionsRelatedTo = useCallback((player: Player) => { - const newPos = middlePos(document.getElementById(player.id)!.getBoundingClientRect()) - setActions(actions => actions.map(a => { - if (a.fromPlayerId == player.id) { - return {...a, moveFrom: newPos} - } + const newPos = middlePos( + document.getElementById(player.id)!.getBoundingClientRect(), + ) + setActions((actions) => + actions.map((a) => { + if (a.fromPlayerId == player.id) { + return { ...a, moveFrom: newPos } + } - if (a.toPlayerId == player.id) { - const segments = a.segments.toSpliced(a.segments.length - 1, 1, { - ...a.segments[a.segments.length - 1], - next: newPos - }) - return {...a, segments} - } + if (a.toPlayerId == player.id) { + const segments = a.segments.toSpliced( + a.segments.length - 1, + 1, + { + ...a.segments[a.segments.length - 1], + next: newPos, + }, + ) + return { ...a, segments } + } - return a - })) + return a + }), + ) }, []) return ( -
- {"court"} +
+ {"court"} {actions.map((action, idx) => renderAction(action, idx))} @@ -125,7 +150,7 @@ export function BasketCourt({ key={2} onHeadMoved={(headPos) => setPreviewArrowEdges([ - {next: middlePos(headPos)}, + { next: middlePos(headPos) }, ]) } onHeadPicked={(headPos) => { @@ -133,12 +158,12 @@ export function BasketCourt({ middlePos(pieceRef.getBoundingClientRect()), ) setPreviewArrowEdges([ - {next: middlePos(headPos)}, + { next: middlePos(headPos) }, ]) setPreviewArrowEnabled(true) }} onHeadDropped={(headRect) => { - bindArrowToPlayer(pieceRef, headRect) + placeArrow(pieceRef, headRect) setPreviewArrowEnabled(false) }} />, @@ -167,6 +192,7 @@ export function BasketCourt({ segments={previewArrowEdges} //do nothing on change, not really possible as it's a preview arrow onSegmentsChanges={() => {}} + //TODO place those values in constants endRadius={17} startRadius={26} /> diff --git a/front/components/editor/CourtPlayer.tsx b/front/components/editor/CourtPlayer.tsx index 785516e..f523448 100644 --- a/front/components/editor/CourtPlayer.tsx +++ b/front/components/editor/CourtPlayer.tsx @@ -1,10 +1,9 @@ - -import {ReactNode, RefObject, useRef} from "react" +import { ReactNode, RefObject, useRef } from "react" import "../../style/player.css" import Draggable from "react-draggable" -import {PlayerPiece} from "./PlayerPiece" -import {Player} from "../../tactic/Player" -import {calculateRatio} from "../../Utils" +import { PlayerPiece } from "./PlayerPiece" +import { Player } from "../../tactic/Player" +import { calculateRatio } from "../../Utils" export interface PlayerProps { player: Player @@ -62,12 +61,20 @@ export default function CourtPlayer({ left: `${x * 100}%`, top: `${y * 100}%`, }}> -
{ - if (e.key == "Delete") onRemove() - }}> -
{availableActions(pieceRef.current!)}
- +
{ + if (e.key == "Delete") onRemove() + }}> +
+ {availableActions(pieceRef.current!)} +
+
diff --git a/front/style/bendable_arrows.css b/front/style/bendable_arrows.css index eccfb08..75a1be1 100644 --- a/front/style/bendable_arrows.css +++ b/front/style/bendable_arrows.css @@ -3,18 +3,19 @@ border-radius: 100px; background-color: black; + outline: none; } .arrow-edge-control-point:hover { background-color: var(--selection-color); } - .arrow-path { pointer-events: stroke; cursor: pointer; } -.arrow-path:hover, .arrow-path:active { - stroke: var(--selection-color) -} \ No newline at end of file +.arrow-path:hover, +.arrow-path:active { + stroke: var(--selection-color); +} diff --git a/front/tactic/Action.ts b/front/tactic/Action.ts index d1741ed..993499a 100644 --- a/front/tactic/Action.ts +++ b/front/tactic/Action.ts @@ -11,8 +11,8 @@ export enum MovementActionKind { export type Action = { type: MovementActionKind } & MovementAction export interface MovementAction { - fromPlayerId: PlayerId, - toPlayerId: PlayerId, + fromPlayerId: PlayerId + toPlayerId?: PlayerId moveFrom: Pos segments: Segment[] } diff --git a/front/tactic/Player.ts b/front/tactic/Player.ts index 97c2ef5..1d71d8a 100644 --- a/front/tactic/Player.ts +++ b/front/tactic/Player.ts @@ -3,7 +3,6 @@ import { Team } from "./Team" export type PlayerId = string export interface Player { - readonly id: PlayerId /** diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index cf28f90..5e9d32c 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -108,11 +108,17 @@ function EditorView({ const [content, setContent, saveState] = useContentState( initialContent, isInGuestMode ? SaveStates.Guest : SaveStates.Ok, - useMemo(() => debounceAsync((content) => - onContentChange(content).then((success) => - success ? SaveStates.Ok : SaveStates.Err, - ) - , 250), [onContentChange]) + useMemo( + () => + debounceAsync( + (content) => + onContentChange(content).then((success) => + success ? SaveStates.Ok : SaveStates.Err, + ), + 250, + ), + [onContentChange], + ), ) const [allies, setAllies] = useState( @@ -358,7 +364,7 @@ function EditorView({ }} />
-
+
@@ -490,11 +496,14 @@ function getRackPlayers(team: Team, players: Player[]): RackedPlayer[] { .map((key) => ({ team, key })) } -function debounceAsync(f: (args: A) => Promise, delay = 1000): (args: A) => Promise { - let task = 0; +function debounceAsync( + f: (args: A) => Promise, + delay = 1000, +): (args: A) => Promise { + let task = 0 return (args: A) => { clearTimeout(task) - return new Promise(resolve => { + return new Promise((resolve) => { task = setTimeout(() => f(args).then(resolve), delay) }) } diff --git a/front/views/editor/CourtAction.tsx b/front/views/editor/CourtAction.tsx index 90fadab..b7217ba 100644 --- a/front/views/editor/CourtAction.tsx +++ b/front/views/editor/CourtAction.tsx @@ -1,4 +1,4 @@ -import {Action} from "../../tactic/Action" +import { Action } from "../../tactic/Action" import BendableArrow from "../../components/arrows/BendableArrow" export interface CourtActionProps { @@ -6,19 +6,16 @@ export interface CourtActionProps { onActionChanges: (a: Action) => void } -export function CourtAction({ - action, - onActionChanges, - }: CourtActionProps) { - +export function CourtAction({ action, onActionChanges }: CourtActionProps) { return ( { - onActionChanges({...action, segments: edges}) + onActionChanges({ ...action, segments: edges }) }} - endRadius={26} + //TODO place those magic values in constants + endRadius={action.toPlayerId ? 26 : 17} startRadius={26} /> )