Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature draw #80

Merged
merged 12 commits into from
Jan 10, 2020
4,838 changes: 4,838 additions & 0 deletions docs/examples/80F9E0AA11E9EDD0CC415BA96B37926C/metadata.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/src/components/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class Main extends Component<Props, State> {
componentDidMount() {
const REPLAY_ID = "80F9E0AA11E9EDD0CC415BA96B37926C"

loadReplay(REPLAY_ID, true).then(([replayData, replayMetadata]) => {
loadReplay(REPLAY_ID, true, true).then(([replayData, replayMetadata]) => {
this.setState({
options: {
replayData,
Expand Down
4 changes: 4 additions & 0 deletions docs/src/components/Viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
PlayerCameraControls,
ReplayViewer,
Slider,
DrawingControls
} from "../../../src"

interface Props extends WithStyles {
Expand Down Expand Up @@ -64,6 +65,9 @@ class Viewer extends Component<Props, State> {
<Grid item>
<PlayerCameraControls />
</Grid>
<Grid item>
<DrawingControls />
</Grid>
<Grid item>
<Slider />
</Grid>
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export {
default as FieldCameraControls,
} from "./viewer/components/FieldCameraControls"
export { default as Slider } from "./viewer/components/Slider"
export { default as DrawingControls } from "./viewer/components/DrawingControls"

// Utilities
export { default as FPSClock } from "./utils/FPSClock"
Expand Down
258 changes: 258 additions & 0 deletions src/managers/DrawingManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
import { Group, Camera, Raycaster, SphereBufferGeometry, MeshBasicMaterial, Mesh, Vector3, BufferGeometry, LineBasicMaterial, Line, BufferAttribute, Material, BoxBufferGeometry } from "three"

import {
addCameraChangeListener,
CameraChangeEvent,
removeCameraChangeListener,
} from "../eventbus/events/cameraChange"
import {
addCanvasResizeListener,
CanvasResizeEvent,
removeCanvasResizeListener,
} from "../eventbus/events/canvasResize"
import SceneManager from "./SceneManager"
import CameraManager from "./CameraManager"
import { GameManager } from "./GameManager"

export type DrawableMeshIndex = "box" | "sphere" | "line"

export interface DrawingState {
color?: string
meshScale?: number
drawObject?: DrawableMeshIndex
is3dMode?: boolean
}

type DrawableMeshes = {
[k in DrawableMeshIndex]: Mesh | Line
}

interface Canvas {
domNode: HTMLCanvasElement
width: number
height: number
}


export default class DrawingManager {
color: string
drawObject: DrawableMeshIndex
is3dMode: boolean
meshScale: number

private MAX_POINTS: number
private isDrawing: boolean = false
private field: Group
private activeCamera: Camera
private canvas: Canvas
private cloneArray: Array<string>
private activeLinePointIndex: number
private drawableMeshes: DrawableMeshes

private constructor({color,meshScale,drawObject,is3dMode}: DrawingState = {}) {
this.color = color || "ff0000"
this.drawObject = drawObject || 'line'
this.is3dMode = is3dMode || false
this.meshScale = meshScale || 200
this.MAX_POINTS = 500
this.field = SceneManager.getInstance().field.field
this.activeCamera = CameraManager.getInstance().activeCamera
this.canvas = this.getCanvas()
this.cloneArray = []
this.activeLinePointIndex = 0
this.drawableMeshes = this.getDrawableMeshes()

addCanvasResizeListener(this.updateSize)
addCameraChangeListener(this.onCameraChange)

this.canvas.domNode.addEventListener("mousedown", this.onMouseDown)
this.canvas.domNode.addEventListener("mousemove", this.onMouseMove)
this.canvas.domNode.addEventListener("mouseup", this.onMouseUp)
}

clearDrawings = () => {
this.removeClones()
}

setColor = (newColor: string) => {
this.color = newColor
const meshMat = this.drawableMeshes.sphere.material as MeshBasicMaterial
meshMat.color.set(newColor)
const lineMat = this.drawableMeshes.line.material as LineBasicMaterial
lineMat.color.set(newColor)
this.isPaused() && this.refreshFrame()
}

private readonly getDrawableMeshes = () => {
const basicMaterial = new MeshBasicMaterial({ color: this.color })
const boxGeometry = new BoxBufferGeometry(0.2, 0.2, 0.2)
const box = new Mesh(boxGeometry, basicMaterial)

const sphereGeometry = new SphereBufferGeometry(0.1, 32, 32)
const sphere = new Mesh(sphereGeometry, basicMaterial)

const lineMaterial = new LineBasicMaterial({ color: this.color })
const lineGeometry = new BufferGeometry()
const positions = new Float32Array(this.MAX_POINTS * 3)
lineGeometry.setAttribute('position', new BufferAttribute(positions, 3))
const line = new Line(lineGeometry, lineMaterial)

this.cloneArray.push(sphere.uuid, line.uuid, box.uuid)

return { box, sphere, line }
}

private readonly getCanvas = () => {
const domNode = GameManager.getInstance().getDOMNode()
const { width, height } = domNode.getBoundingClientRect()

return { domNode, width, height }
}

private readonly onMouseDown = ({ offsetX, offsetY, ctrlKey, altKey }: MouseEvent) => {
this.handleDrawing(offsetX, offsetY)
this.isDrawing = true
}

private readonly onMouseMove = ({ offsetX, offsetY, ctrlKey, altKey }: MouseEvent) => {
if (this.isDrawing) {
this.handleDrawing(offsetX, offsetY)
}
}

private readonly onMouseUp = ({ offsetX, offsetY, ctrlKey, altKey }: MouseEvent) => {
this.isDrawing = false
}

private handleDrawing(offsetX: number, offsetY: number) {
switch (this.drawObject) {
case 'line':
this.drawLine(offsetX, offsetY)
break
default:
this.drawMesh(offsetX, offsetY, this.drawObject)
break
}
}

private readonly getMouseVector = (offsetX: number, offsetY: number) => {
const cam = this.activeCamera
const scale = this.canvas.width / 2
const x = (offsetX / this.canvas.width) * 2 - 1
const y = -(offsetY / this.canvas.height) * 2 + 1
const rayCaster = new Raycaster()
rayCaster.setFromCamera({ x, y }, cam)
const rayDir = new Vector3(rayCaster.ray.direction.x * scale, rayCaster.ray.direction.y * scale, rayCaster.ray.direction.z * scale)
const rayVector = new Vector3(cam.position.x + rayDir.x, cam.position.y + rayDir.y, cam.position.z + rayDir.z)
if (this.is3dMode) {
const intersections = rayCaster.intersectObjects([this.field], true)
return intersections.length ? intersections[0].point : undefined
}

return rayVector
}

private readonly drawLine = (offsetX: number, offsetY: number) => {
const mouseVec = this.getMouseVector(offsetX, offsetY)
if (!mouseVec) return
const index = this.isDrawing ? this.activeLinePointIndex : this.activeLinePointIndex = 0
const activeLine = this.isDrawing ? this.drawableMeshes.line : this.drawableMeshes.line.clone()
if (!this.isDrawing) {
activeLine.geometry = this.drawableMeshes.line.geometry.clone()
this.cloneArray.push(activeLine.uuid)
SceneManager.getInstance().scene.add(activeLine)
this.drawableMeshes.line = activeLine
}

const geo = activeLine.geometry as BufferGeometry
const positionAttribute = geo.attributes.position as BufferAttribute
const positions = positionAttribute.array as any[]

positions[index * 3 + 0] = mouseVec.x
positions[index * 3 + 1] = mouseVec.y
positions[index * 3 + 2] = mouseVec.z

geo.setDrawRange(0, ++this.activeLinePointIndex)
positionAttribute.needsUpdate = true

this.isPaused() && this.refreshFrame()
}

private readonly drawMesh = (offsetX: number, offsetY: number, mesh: keyof DrawableMeshes) => {
const rayVector = this.getMouseVector(offsetX, offsetY)
if (rayVector) {
const clone = this.drawableMeshes[mesh].clone()
this.cloneArray.push(clone.uuid)
if (this.is3dMode) rayVector.y += (this.meshScale*0.1)
clone.position.copy(rayVector)
clone.scale.setScalar(this.meshScale)
SceneManager.getInstance().scene.add(clone)

this.isPaused() && this.refreshFrame()
}
}

private readonly removeClones = () => {
const scene = SceneManager.getInstance().scene
this.cloneArray.map((i: string) => {
const clone = scene.getObjectByProperty('uuid', i) as Mesh
if (clone) {
(clone.geometry as BufferGeometry).dispose(),
(clone.material as Material).dispose()
scene.remove(clone)
}
})

this.isPaused() && this.refreshFrame()
}

private readonly refreshFrame = () => {
window.requestAnimationFrame(() => {
const gameManager = GameManager.getInstance()
gameManager.render()
gameManager.clock.setFrame(gameManager.clock.currentFrame)
})
}

private readonly isPaused = () => {
return GameManager.getInstance().clock.isPaused()
}

private readonly updateSize = ({ width, height }: CanvasResizeEvent) => {
this.canvas.width = width
this.canvas.height = height
}

private readonly onCameraChange = ({ camera }: CameraChangeEvent) => {
this.activeCamera = camera
}

/**
* ========================================
* Managers are singletons
* ========================================
*/
private static instance?: DrawingManager
static getInstance() {
if (!DrawingManager.instance) {
throw new Error("DrawingManager not initialized with call to `init`")
}
return DrawingManager.instance
}
static init(state?: DrawingState) {
DrawingManager.instance = new DrawingManager(state)
return DrawingManager.instance
}
static destruct() {
const { instance } = DrawingManager
if (instance) {
removeCameraChangeListener(instance.onCameraChange)
removeCanvasResizeListener(instance.updateSize)
instance.removeClones()
instance.canvas.domNode.removeEventListener("mousedown", instance.onMouseDown)
instance.canvas.domNode.removeEventListener("mousemove", instance.onMouseMove)
instance.canvas.domNode.removeEventListener("mouseup", instance.onMouseUp)
DrawingManager.instance = undefined
}
}
}
2 changes: 1 addition & 1 deletion src/managers/GameManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export class GameManager {
return this.renderer.domElement
}

private readonly render = () => {
render = () => {
const { scene } = SceneManager.getInstance()
const { activeCamera } = CameraManager.getInstance()
this.renderer.render(scene, activeCamera)
Expand Down
12 changes: 7 additions & 5 deletions src/viewer/clients/loadReplay.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ReplayData } from "../../models/ReplayData"
import { ReplayMetadata } from "../../models/ReplayMetadata"

const fetchByURL = (url: string) =>
fetch(url, {
const fetchByURL = (url: string, local?: boolean) =>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file changes were just to make fewer network calls while testing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should make it a prop that is passed in by the host?
That way we can change it as needed in the future?
Especially options as we use it in more and more different places.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

@ipiv ipiv Dec 24, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Get's a bit tricky when you are in dev and want to load a different replay, instead of just changing local variable to false or not passing it at all:

  • you would need to stop the webpack server
  • run webpack-dev-server manually without "--mode development" argument

edit: it turns out that running running without "--mode development" NODE_ENV is still set to development. I needed to specify "--mode production" to get it to be set to production.

fetch(url, local ? {} : {
method: "GET",
headers: {
Accept: "application/json",
Expand All @@ -14,12 +14,14 @@ const cache: { [key: string]: [ReplayData, ReplayMetadata] } = {}

export const loadReplay = async (
replayId: string,
cached?: boolean
cached?: boolean,
local?: boolean
): Promise<[ReplayData, ReplayMetadata]> => {
const url = local ? "../examples/" : "https://calculated.gg/api/replay/"
const fetch = () =>
Promise.all([
fetchByURL(`https://calculated.gg/api/replay/${replayId}/positions`),
fetchByURL(`https://calculated.gg/api/v1/replay/${replayId}?key=1`),
fetchByURL(`${url+replayId}/positions${local && '.json'}`, local),
fetchByURL(`${url+replayId}${local ? "/metadata.json" : "?key=1"}`, local),
])
if (cached) {
if (!cache[replayId]) {
Expand Down
Loading