fix ball drop issues

pull/77/head
maxime 2 years ago committed by vivien.dufour
parent 9fd4868d05
commit bfdde852f6

@ -1,27 +1,17 @@
import React, {RefObject, useRef} from "react"
import "../../style/ball.css" import "../../style/ball.css"
import Ball from "../../assets/icon/ball.svg?react" import BallSvg from "../../assets/icon/ball.svg?react"
import Draggable from "react-draggable" import {Ball} from "../../tactic/CourtObjects";
export interface CourtBallProps { export interface CourtBallProps {
onDrop: (pos: DOMRect) => void onMoved: (rect: DOMRect) => void
ball: Ball
} }
export function CourtBall({ onDrop }: CourtBallProps) {
const ref = useRef<HTMLElement>()
return (
<Draggable onStop={() => onDrop(ref.current!.getBoundingClientRect())} nodeRef={ref}>
<BallPiece pieceRef={ref}/>
</Draggable>
)
}
export function BallPiece({pieceRef}: {pieceRef: RefObject<HTMLElement>}) { export function BallPiece() {
return ( return (
<div ref={pieceRef} className={`ball-div`} > <BallSvg className={"ball"}/>
<Ball className={"ball"} />
</div>
) )
} }

@ -1,36 +1,41 @@
import "../../style/basket_court.css" import "../../style/basket_court.css"
import { RefObject, useRef } from "react" import {RefObject} from "react"
import CourtPlayer from "./CourtPlayer" import CourtPlayer from "./CourtPlayer"
import { Player } from "../../tactic/Player"
import {BallPiece, CourtBall} from "./BallPiece"; import {Player} from "../../tactic/Player"
import {Ball} from "../../tactic/Ball"; import {CourtObject} from "../../tactic/CourtObjects";
import {CourtBall} from "./CourtBall";
export interface BasketCourtProps { export interface BasketCourtProps {
players: Player[] players: Player[]
ball: Ball objects: CourtObject[]
onPlayerRemove: (p: Player) => void onPlayerRemove: (p: Player) => void
onBallDrop: (b: DOMRect) => void
onPlayerChange: (p: Player) => void onPlayerChange: (p: Player) => void
onBallMoved: (ball: DOMRect) => void,
courtImage: string courtImage: string
courtRef: RefObject<HTMLDivElement> courtRef: RefObject<HTMLDivElement>
} }
export function BasketCourt({ export function BasketCourt({
players,
ball, players,
onPlayerRemove, objects,
onBallDrop, onPlayerRemove,
onPlayerChange, onBallMoved,
courtImage, onPlayerChange,
courtRef, courtImage,
}: BasketCourtProps) { courtRef,
}: BasketCourtProps) {
return ( return (
<div <div
id="court-container" id="court-container"
ref={courtRef} ref={courtRef}
style={{ position: "relative" }}> style={{position: "relative"}}>
<img src={courtImage} alt={"court"} id="court-svg" /> <img src={courtImage} alt={"court"} id="court-svg"/>
{players.map((player) => { {players.map((player) => {
return ( return (
<CourtPlayer <CourtPlayer
@ -38,12 +43,24 @@ export function BasketCourt({
player={player} player={player}
onChange={onPlayerChange} onChange={onPlayerChange}
onRemove={() => onPlayerRemove(player)} onRemove={() => onPlayerRemove(player)}
onBallDrop={onBallDrop} onBallDrop={onBallMoved}
parentRef={courtRef} parentRef={courtRef}
/> />
) )
})} })}
<CourtBall ball={ball}/>
{objects.map(object => {
if (object.type == "ball") {
return <CourtBall
onMoved={onBallMoved}
ball={object}
key="ball"
/>
}
throw new Error("unknown court object", object.type)
})}
</div> </div>
) )
} }

@ -0,0 +1,28 @@
import React, {useRef} from "react";
import Draggable from "react-draggable";
import {BallPiece, CourtBallProps} from "./BallPiece";
export function CourtBall({onMoved, ball}: CourtBallProps) {
const pieceRef = useRef<HTMLDivElement>(null)
const x = ball.rightRatio
const y = ball.bottomRatio
return (
<Draggable
onStop={() => onMoved(pieceRef.current!.getBoundingClientRect())}
nodeRef={pieceRef}
>
<div className={"ball-div"}
ref={pieceRef}
style={{
position: "absolute",
left: `${x * 100}%`,
top: `${y * 100}%`,
}}
>
<BallPiece/>
</div>
</Draggable>
)
}

@ -1,16 +1,17 @@
import { RefObject, useRef, useState } from "react" import {RefObject, useRef} from "react"
import "../../style/player.css" import "../../style/player.css"
import { BallPiece } from "./BallPiece" import RemoveIcon from "../../assets/icon/remove.svg?react"
import {BallPiece} from "./BallPiece"
import Draggable from "react-draggable" import Draggable from "react-draggable"
import { PlayerPiece } from "./PlayerPiece" import {PlayerPiece} from "./PlayerPiece"
import { Player } from "../../tactic/Player" import {Player} from "../../tactic/Player"
import { calculateRatio } from "../../Utils" import {calculateRatio} from "../../Utils"
export interface PlayerProps { export interface PlayerProps {
player: Player player: Player
onChange: (p: Player) => void onChange: (p: Player) => void
onRemove: () => void onRemove: () => void
onBallDrop: (b: DOMRect) => void onBallDrop: (bounds: DOMRect) => void
parentRef: RefObject<HTMLDivElement> parentRef: RefObject<HTMLDivElement>
} }
@ -25,6 +26,7 @@ export default function CourtPlayer({
parentRef, parentRef,
}: PlayerProps) { }: PlayerProps) {
const pieceRef = useRef<HTMLDivElement>(null) const pieceRef = useRef<HTMLDivElement>(null)
const ballPiece = useRef<HTMLDivElement>(null)
const x = player.rightRatio const x = player.rightRatio
const y = player.bottomRatio const y = player.bottomRatio
@ -67,12 +69,15 @@ export default function CourtPlayer({
if (e.key == "Delete") onRemove() if (e.key == "Delete") onRemove()
}}> }}>
<div className="player-selection-tab"> <div className="player-selection-tab">
{hasBall && ( <RemoveIcon
<BallPiece className="player-selection-tab-remove"
onDrop={() => onBallDrop(ball)} onClick={onRemove}
pieceRef={ball} />
/> {hasBall && (<Draggable nodeRef={ballPiece} onStop={() => onBallDrop(ballPiece.current!.getBoundingClientRect())}>
)} <div ref={ballPiece}>
<BallPiece />
</div>
</Draggable>)}
</div> </div>
<PlayerPiece <PlayerPiece
team={player.team} team={player.team}

@ -1,11 +1,20 @@
export type CourtObject = {type: "ball"} & Ball
export interface Ball { export interface Ball {
/** /**
* Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle) * The ball is a "ball" court object
*/ */
bottomRatio: number readonly type: "ball",
/**
* Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle)
*/
readonly bottomRatio: number
/** /**
* Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle) * Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle)
*/ */
rightRatio: number readonly rightRatio: number
} }

@ -1,5 +1,5 @@
import { Player } from "./Player" import { Player } from "./Player"
import {Ball} from "./Ball"; import {CourtObject} from "./CourtObjects";
export interface Tactic { export interface Tactic {
id: number id: number
@ -9,5 +9,5 @@ export interface Tactic {
export interface TacticContent { export interface TacticContent {
players: Player[] players: Player[]
ball : Ball objects: CourtObject[]
} }

@ -1,11 +1,4 @@
import { import {CSSProperties, Dispatch, SetStateAction, useCallback, useRef, useState,} from "react"
CSSProperties,
Dispatch,
SetStateAction,
useCallback,
useRef,
useState,
} from "react"
import "../style/editor.css" import "../style/editor.css"
import TitleInput from "../components/TitleInput" import TitleInput from "../components/TitleInput"
import { BasketCourt } from "../components/editor/BasketCourt" import { BasketCourt } from "../components/editor/BasketCourt"
@ -13,13 +6,13 @@ import { BasketCourt } from "../components/editor/BasketCourt"
import plainCourt from "../assets/court/full_court.svg" import plainCourt from "../assets/court/full_court.svg"
import halfCourt from "../assets/court/half_court.svg" import halfCourt from "../assets/court/half_court.svg"
import { Rack } from "../components/Rack" import { Rack } from "../components/Rack"
import { PlayerPiece } from "../components/editor/PlayerPiece" import { PlayerPiece } from "../components/editor/PlayerPiece"
import {BallPiece, CourtBall} from "../components/editor/BallPiece"; import {BallPiece, CourtBall} from "../components/editor/BallPiece";
import { Player } from "../tactic/Player"
import { Tactic, TacticContent } from "../tactic/Tactic" import { Tactic, TacticContent } from "../tactic/Tactic"
import { fetchAPI } from "../Fetcher" import { fetchAPI } from "../Fetcher"
import { Team } from "../tactic/Team" import { Team } from "../tactic/Team"
@ -28,8 +21,9 @@ import SavingState, {
SaveState, SaveState,
SaveStates, SaveStates,
} from "../components/editor/SavingState" } from "../components/editor/SavingState"
import {Ball} from "../tactic/Ball";
import Draggable from "react-draggable"; import {CourtObject} from "../tactic/CourtObjects";
const ERROR_STYLE: CSSProperties = { const ERROR_STYLE: CSSProperties = {
borderColor: "red", borderColor: "red",
@ -60,6 +54,8 @@ interface RackedPlayer {
key: string key: string
} }
type RackedCourtObject = { key: "ball" }
export default function Editor({ export default function Editor({
id, id,
name, name,
@ -130,15 +126,11 @@ function EditorView({
const [opponents, setOpponents] = useState( const [opponents, setOpponents] = useState(
getRackPlayers(Team.Opponents, content.players), getRackPlayers(Team.Opponents, content.players),
) )
const [objects, setObjects] = useState<RackedCourtObject[]>([{key: "ball"}])
const [showBall, setShowBall] = useState(
content.players.find((p) => p.hasBall) == undefined,
)
const ballPiece = useRef<HTMLDivElement>(null)
const courtDivContentRef = useRef<HTMLDivElement>(null) const courtDivContentRef = useRef<HTMLDivElement>(null)
const canDetach = (bounds: DOMRect) => { const canDetach = (bounds: DOMRect) => {
const courtBounds = courtDivContentRef.current!.getBoundingClientRect() const courtBounds = courtDivContentRef.current!.getBoundingClientRect()
@ -159,6 +151,7 @@ function EditorView({
setContent((content) => { setContent((content) => {
return { return {
...content,
players: [ players: [
...content.players, ...content.players,
{ {
@ -170,14 +163,43 @@ function EditorView({
hasBall: false, hasBall: false,
}, },
], ],
ball: content.ball
} }
}) })
} }
const onObjectDetach = (ref: HTMLDivElement, rackedObject: RackedCourtObject) => {
const refBounds = ref.getBoundingClientRect()
const courtBounds = courtDivContentRef.current!.getBoundingClientRect()
const {x, y} = calculateRatio(refBounds, courtBounds)
let courtObject: CourtObject
switch (rackedObject.key) {
case "ball":
courtObject = {
type: "ball",
rightRatio: x,
bottomRatio: y
}
break
default:
throw new Error("unknown court object ", rackedObject.key)
}
setContent((content) =>
({
...content,
objects: [
...content.objects,
courtObject,
]
})
)
}
const onBallDrop = (ballBounds: DOMRect) => { const onBallDrop = (ballBounds: DOMRect) => {
let ballAssigned = false let ballAssigned = false
@ -200,7 +222,14 @@ function EditorView({
} }
return { ...player, hasBall: doesOverlap } return { ...player, hasBall: doesOverlap }
}) })
return {players: players, ball: content.ball}
let objects = content.objects
if (ballAssigned) {
const ballPieceIdx = content.objects.findIndex(obj => obj.type === "ball")
objects = objects.toSpliced(ballPieceIdx, 1)
}
return {...content, objects, players}
}) })
} }
@ -231,7 +260,7 @@ function EditorView({
id="allies-rack" id="allies-rack"
objects={allies} objects={allies}
onChange={setAllies} onChange={setAllies}
canDetach={canDetach} canDetach={div => canDetach(div.getBoundingClientRect())}
onElementDetached={onPieceDetach} onElementDetached={onPieceDetach}
render={({ team, key }) => ( render={({ team, key }) => (
<PlayerPiece <PlayerPiece
@ -243,17 +272,18 @@ function EditorView({
)} )}
/> />
{rackBall && <CourtBall onDrop={pos => { <Rack id={"objects"}
if (canDetach(pos)) { objects={objects}
onBallDetach(pos) onChange={setObjects}
} canDetach={div => canDetach(div.getBoundingClientRect())}
}}/>} onElementDetached={onObjectDetach}
render={renderCourtObject}/>
<Rack <Rack
id="opponent-rack" id="opponent-rack"
objects={opponents} objects={opponents}
onChange={setOpponents} onChange={setOpponents}
canDetach={canDetach} canDetach={div => canDetach(div.getBoundingClientRect())}
onElementDetached={onPieceDetach} onElementDetached={onPieceDetach}
render={({ team, key }) => ( render={({ team, key }) => (
<PlayerPiece <PlayerPiece
@ -269,30 +299,30 @@ function EditorView({
<div id="court-div-bounds"> <div id="court-div-bounds">
<BasketCourt <BasketCourt
players={content.players} players={content.players}
ball={content.ball} objects={content.objects}
onBallDrop={onBallDrop} onBallMoved={onBallDrop}
courtImage={ courtImage={
courtType == "PLAIN" ? plainCourt : halfCourt courtType == "PLAIN" ? plainCourt : halfCourt
} }
courtRef={courtDivContentRef} courtRef={courtDivContentRef}
onPlayerChange={(player) => { onPlayerChange={(player) => {
setContent((content) => ({ setContent((content) => ({
...content,
players: toSplicedPlayers( players: toSplicedPlayers(
content.players, content.players,
player, player,
true, true,
), ),
ball: content.ball
})) }))
}} }}
onPlayerRemove={(player) => { onPlayerRemove={(player) => {
setContent((content) => ({ setContent((content) => ({
...content,
players: toSplicedPlayers( players: toSplicedPlayers(
content.players, content.players,
player, player,
false, false,
), ),
ball: content.ball
})) }))
let setter let setter
switch (player.team) { switch (player.team) {
@ -303,9 +333,8 @@ function EditorView({
setter = setAllies setter = setAllies
} }
if (player.hasBall) { if (player.hasBall) {
setShowBall(true) /// add an instance of RackedBall back to the rack (objects)
} }
setter((players) => [ setter((players) => [
...players, ...players,
{ {
@ -323,6 +352,13 @@ function EditorView({
) )
} }
function renderCourtObject(courtObject: RackedCourtObject) {
if (courtObject.key == "ball") {
return <BallPiece/>
}
throw new Error("unknown racked court object ", courtObject.key)
}
function getRackPlayers(team: Team, players: Player[]): RackedPlayer[] { function getRackPlayers(team: Team, players: Player[]): RackedPlayer[] {
return ["1", "2", "3", "4", "5"] return ["1", "2", "3", "4", "5"]
.filter( .filter(

@ -19,7 +19,7 @@ CREATE TABLE Tactic
name varchar NOT NULL, name varchar NOT NULL,
creation_date timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, creation_date timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
owner integer NOT NULL, owner integer NOT NULL,
content varchar DEFAULT '{"players": []}' NOT NULL, content varchar DEFAULT '{"players": [], "objects": []}' NOT NULL,
court_type varchar CHECK ( court_type IN ('HALF', 'PLAIN')) NOT NULL, court_type varchar CHECK ( court_type IN ('HALF', 'PLAIN')) NOT NULL,
FOREIGN KEY (owner) REFERENCES Account FOREIGN KEY (owner) REFERENCES Account
); );

@ -42,7 +42,7 @@ class EditorController {
return ViewHttpResponse::react("views/Editor.tsx", [ return ViewHttpResponse::react("views/Editor.tsx", [
"id" => -1, //-1 id means that the editor will not support saves "id" => -1, //-1 id means that the editor will not support saves
"name" => TacticModel::TACTIC_DEFAULT_NAME, "name" => TacticModel::TACTIC_DEFAULT_NAME,
"content" => '{"players": []}', "content" => '{"players": [], "objects": []}',
"courtType" => $courtType->name(), "courtType" => $courtType->name(),
]); ]);
} }

Loading…
Cancel
Save