mirror of
https://github.com/sbrl/Nibriboard.git
synced 2018-01-10 21:33:49 +00:00
Begin working on the client, and fix a ton of bugs in the server deserialisation process.
This commit is contained in:
parent
199ffbfe40
commit
690abc6445
14 changed files with 1653 additions and 17 deletions
20
Nibriboard/Client/Messages/ClientStateMessage.cs
Normal file
20
Nibriboard/Client/Messages/ClientStateMessage.cs
Normal file
|
@ -0,0 +1,20 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using RippleSpace;
|
||||
|
||||
namespace Nibriboard.Client.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an update of where a group of connected clients are.
|
||||
/// </summary>
|
||||
public class ClientStateMessage : Message
|
||||
{
|
||||
public List<ClientState> ClientStates = new List<ClientState>();
|
||||
|
||||
public ClientStateMessage()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
21
Nibriboard/Client/Messages/CursorPositionMessage.cs
Normal file
21
Nibriboard/Client/Messages/CursorPositionMessage.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
namespace Nibriboard.Client.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an update by the client of their cursor position.
|
||||
/// </summary>
|
||||
public class CursorPositionMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The absolute cursor position.
|
||||
/// </summary>
|
||||
public Point AbsCursorPosition;
|
||||
|
||||
public CursorPositionMessage()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
// TODO: In C# you can either have namespaces or types in a namespace - not both.
|
||||
using Nibriboard.Utilities.JsonConverters;
|
||||
|
||||
namespace Nibriboard.Client.Messages
|
||||
{
|
||||
public class HandshakeRequestMessage : Message
|
||||
|
@ -9,10 +13,12 @@ namespace Nibriboard.Client.Messages
|
|||
/// The initial visible area on the client's screen.
|
||||
/// Very useful for determining which chunks we should send a client when they first connect.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(RectangleConverter))]
|
||||
public Rectangle InitialViewport = Rectangle.Empty;
|
||||
/// <summary>
|
||||
/// The initial position of the user's cursor.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(RectangleConverter))]
|
||||
public Point InitialAbsCursorPosition = Point.Empty;
|
||||
|
||||
public HandshakeRequestMessage()
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Threading.Tasks;
|
|||
using System.Text;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
|
||||
using IotWeb.Common.Http;
|
||||
using SBRL.Utilities;
|
||||
|
@ -88,12 +89,19 @@ namespace Nibriboard.Client
|
|||
{
|
||||
string eventName = JsonUtilities.DeserializeProperty<string>(frame, "event");
|
||||
|
||||
if (!messageEventTypes.ContainsKey(eventName))
|
||||
Log.WriteLine("Received message with invalid event {1} from Client #{0}", Id, eventName);
|
||||
if(eventName == null) {
|
||||
Log.WriteLine("[NibriClient#{0}] Received message that didn't have an event.", Id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!messageEventTypes.ContainsKey(eventName)) {
|
||||
Log.WriteLine("[NibriClient#{0}] Received message with invalid event {1}.", Id, eventName);
|
||||
return;
|
||||
}
|
||||
|
||||
Type messageType = messageEventTypes[eventName];
|
||||
Type jsonNet = typeof(JsonConvert);
|
||||
MethodInfo deserialiserInfo = jsonNet.GetMethod("DeserailizeObject");
|
||||
MethodInfo deserialiserInfo = jsonNet.GetMethods().First(method => method.Name == "DeserializeObject" && method.IsGenericMethod);
|
||||
MethodInfo genericInfo = deserialiserInfo.MakeGenericMethod(messageType);
|
||||
var decodedMessage = genericInfo.Invoke(null, new object[] { frame });
|
||||
|
||||
|
@ -147,6 +155,7 @@ namespace Nibriboard.Client
|
|||
CurrentViewPort = message.InitialViewport;
|
||||
AbsoluteCursorPosition = message.InitialAbsCursorPosition;
|
||||
|
||||
// Tell everyone else about the new client
|
||||
Send(GenerateClientStateUpdate());
|
||||
|
||||
return Task.CompletedTask;
|
||||
|
@ -158,6 +167,11 @@ namespace Nibriboard.Client
|
|||
protected Task handleCursorPositionMessage(CursorPositionMessage message) {
|
||||
AbsoluteCursorPosition = message.AbsCursorPosition;
|
||||
|
||||
// Send the update to the other clients
|
||||
ClientStateMessage updateMessage = new ClientStateMessage();
|
||||
updateMessage.ClientStates.Add(this.GenerateStateSnapshot());
|
||||
manager.Broadcast(this, updateMessage);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
#endregion
|
||||
|
|
|
@ -3,6 +3,7 @@ using IotWeb.Common.Http;
|
|||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using Nibriboard.Client.Messages;
|
||||
|
||||
namespace Nibriboard.Client
|
||||
{
|
||||
|
@ -51,7 +52,8 @@ namespace Nibriboard.Client
|
|||
Clients.Add(client);
|
||||
}
|
||||
|
||||
public void Broadcast(NibriClient sendingClient, string message)
|
||||
|
||||
public void Broadcast(NibriClient sendingClient, Message message)
|
||||
{
|
||||
foreach(NibriClient client in Clients)
|
||||
{
|
||||
|
@ -59,7 +61,7 @@ namespace Nibriboard.Client
|
|||
if (client == sendingClient)
|
||||
continue;
|
||||
|
||||
client.SendRaw(message);
|
||||
client.Send(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
// npm modules
|
||||
window.EventEmitter = require("event-emitter-es6");
|
||||
window.FaviconNotification = require("favicon-notification");
|
||||
window.panzoom = require("pan-zoom");
|
||||
|
||||
// Our files
|
||||
import RippleLink from './RippleLink';
|
||||
|
@ -14,18 +15,31 @@ class BoardWindow extends EventEmitter
|
|||
{
|
||||
super(); // Run the parent constructor
|
||||
|
||||
// The maximum target fps.
|
||||
this.maxFps = 60;
|
||||
// Setup the fps indicator in the corner
|
||||
this.renderTimeIndicator = document.createElement("span");
|
||||
this.renderTimeIndicator.innerHTML = "0ms";
|
||||
document.querySelector(".fps").appendChild(this.renderTimeIndicator);
|
||||
|
||||
// Setup the canvas
|
||||
this.canvas = canvas;
|
||||
this.context = canvas.getContext("2d");
|
||||
|
||||
// --~~~--
|
||||
|
||||
// Setup the favicon thingy
|
||||
|
||||
FaviconNotification.init({
|
||||
color: '#ff6333'
|
||||
});
|
||||
FaviconNotification.add();
|
||||
|
||||
// Setup the input controls
|
||||
window.panzoom(canvas, this.handleCanvasMovement.bind(this));
|
||||
|
||||
// Fetch the RippleLink connection information and other settings from
|
||||
// the server
|
||||
get("/Settings.json").then(JSON.parse).then((function(settings) {
|
||||
console.info("[setup]", "Obtained settings from server:", settings);
|
||||
this.settings = settings;
|
||||
|
@ -34,13 +48,39 @@ class BoardWindow extends EventEmitter
|
|||
console.error(`Error: Failed to fetch settings from server! Response: ${errorMessage}`);
|
||||
});
|
||||
|
||||
// Make the canvas track the window size
|
||||
this.trackWindowSize();
|
||||
// Track the mouse position
|
||||
this.trackMousePosition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup ready for user input.
|
||||
* This mainly consists of establishing the RippleLink connection to the server.
|
||||
*/
|
||||
setup() {
|
||||
this.rippleLink = new RippleLink(this.settings.WebsocketUri, this);
|
||||
this.rippleLink.on("connect", (function(event) {
|
||||
// Send the handshake request
|
||||
this.rippleLink.send({
|
||||
event: "handshakeRequest",
|
||||
InitialViewport: { // TODO: Add support for persisting this between sessions
|
||||
X: 0,
|
||||
Y: 0,
|
||||
Width: window.innerWidth,
|
||||
Height: window.innerHeight
|
||||
},
|
||||
InitialAbsCursorPosition: this.cursorPosition
|
||||
});
|
||||
}).bind(this));
|
||||
|
||||
// RippleLink message bindings
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the next frame.
|
||||
*/
|
||||
nextFrame()
|
||||
{
|
||||
// The time at which the current frame started rendering.
|
||||
|
@ -63,11 +103,17 @@ class BoardWindow extends EventEmitter
|
|||
requestAnimationFrame(this.nextFrame.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates everything ready for the next frame to be rendered.
|
||||
*/
|
||||
update()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the next frame.
|
||||
*/
|
||||
render(canvas, context)
|
||||
{
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
@ -93,6 +139,23 @@ class BoardWindow extends EventEmitter
|
|||
this.matchWindowSize();
|
||||
window.addEventListener("resize", this.matchWindowSize.bind(this));
|
||||
}
|
||||
|
||||
trackMousePosition() {
|
||||
document.addEventListener("mousemove", (function(event) {
|
||||
this.cursorPosition = {
|
||||
X: event.clientX,
|
||||
Y: event.clientY
|
||||
};
|
||||
}).bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles events generated by pan-zoom, the package that handles the
|
||||
* dragging and zooming of the whiteboard.
|
||||
*/
|
||||
handleCanvasMovement(event) {
|
||||
this.viewportState = event; // Store the viewport information for later
|
||||
}
|
||||
}
|
||||
|
||||
export default BoardWindow;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,15 +1,59 @@
|
|||
"use strict";
|
||||
|
||||
import WebSocketStates from './Utilities/WebsocketStates';
|
||||
|
||||
class RippleLink
|
||||
const EventEmitter = require("event-emitter-es6");
|
||||
|
||||
class RippleLink extends EventEmitter
|
||||
{
|
||||
constructor(inSocketUrl, inBoardWindow)
|
||||
{
|
||||
super();
|
||||
|
||||
this.socketUrl = inSocketUrl;
|
||||
this.boardWindow = inBoardWindow;
|
||||
this.settings = this.boardWindow.settings;
|
||||
|
||||
// Create the websocket and commect to the server
|
||||
this.websocket = new WebSocket(this.socketUrl, [ this.settings.WebsocketProtocol ]);
|
||||
this.websocket.addEventListener("open", this.handleConnection.bind(this));
|
||||
this.websocket.addEventListener("message", this.handleMessage.bind(this));
|
||||
this.websocket.addEventListener("close", this.handleDisconnection.bind(this));
|
||||
|
||||
}
|
||||
|
||||
handleConnection(event) {
|
||||
console.info("[ripple link] Established connection successfully.");
|
||||
// Tell everyone about it
|
||||
this.emit("connect", event);
|
||||
}
|
||||
|
||||
handleDisconnection(event) {
|
||||
console.error("[ripple link] Lost connection.");
|
||||
this.emit("disconnect", event);
|
||||
}
|
||||
|
||||
handleMessage(event) {
|
||||
// Decode the message form the server
|
||||
var message = JSON.parse(event.data);
|
||||
console.debug(message);
|
||||
|
||||
// Pass it on to the board manager by triggering the appropriate event
|
||||
this.emit(message.event, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message object to the server.
|
||||
*/
|
||||
send(message) {
|
||||
if(this.websocket.readyState !== WebSocketStates.ready)
|
||||
{
|
||||
console.error(`Attempt to send data on the RippleLine when it is not ready (state ${this.websocket.readyState})`);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.websocket.send(JSON.stringify(message));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
30
Nibriboard/ClientFiles/Utilities/WebsocketStates.js
Normal file
30
Nibriboard/ClientFiles/Utilities/WebsocketStates.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
"use strict";
|
||||
|
||||
/**
|
||||
* Constants for the different readyStates that a WebSocket can be in.
|
||||
* @type {Object}
|
||||
*/
|
||||
const WebSocketStates = {
|
||||
/**
|
||||
* Indicates that the WebSocket is connecting to the remote server.
|
||||
* @type {Number}
|
||||
*/
|
||||
connecting: 0,
|
||||
/**
|
||||
* Indicates that the WebSocket is connected to the remote server and ready to send / receive data.
|
||||
* @type {Number}
|
||||
*/
|
||||
ready: 1,
|
||||
/**
|
||||
* Indicates that the WebSocket is in the process of closing the connection to the remote server.
|
||||
* @type {Number}
|
||||
*/
|
||||
closing: 2,
|
||||
/**
|
||||
* Indicates that hte WebSocket is not connected to the remote server (either because the connection was closed, or dropped by the remote server).
|
||||
* @type {Number}
|
||||
*/
|
||||
closed: 3
|
||||
};
|
||||
|
||||
export default WebSocketStates;
|
|
@ -5,9 +5,9 @@
|
|||
"homepage": "https://git.starbeamrainbowlabs.com/sbrl/Nibriboard#nibriboard",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"pan-zoom": "^2.0.0",
|
||||
"rollupify": "^0.3.8"
|
||||
},
|
||||
|
||||
"devDependencies": {
|
||||
"rollupify": "^0.3.8"
|
||||
},
|
||||
|
|
|
@ -77,6 +77,8 @@
|
|||
<Compile Include="Client\Messages\CursorPositionMessage.cs" />
|
||||
<Compile Include="Client\Messages\ClientStateMessage.cs" />
|
||||
<Compile Include="RippleSpace\ClientState.cs" />
|
||||
<Compile Include="Utilities\JsonConverters\RectangleConverter.cs" />
|
||||
<Compile Include="Utilities\JsonConverters\PointConverter.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="ClientFiles\index.html" />
|
||||
|
@ -90,6 +92,7 @@
|
|||
<Folder Include="ClientFiles\" />
|
||||
<Folder Include="Client\" />
|
||||
<Folder Include="Client\Messages\" />
|
||||
<Folder Include="Utilities\JsonConverters\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
|
|
40
Nibriboard/RippleSpace/ClientState.cs
Normal file
40
Nibriboard/RippleSpace/ClientState.cs
Normal file
|
@ -0,0 +1,40 @@
|
|||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
namespace RippleSpace
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a client's state at a particular point in time.
|
||||
/// </summary>
|
||||
public class ClientState
|
||||
{
|
||||
/// <summary>
|
||||
/// The id of the client.
|
||||
/// </summary>
|
||||
public int Id;
|
||||
|
||||
/// <summary>
|
||||
/// The date and time at which this client state snapshot was captured.
|
||||
/// </summary>
|
||||
public DateTime TimeCaptured = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// The name the client chose to identify themselves with.
|
||||
/// </summary>
|
||||
public string Name;
|
||||
|
||||
/// <summary>
|
||||
/// The size and position of the client's viewport.
|
||||
/// </summary>
|
||||
public Rectangle Viewport = Rectangle.Empty;
|
||||
/// <summary>
|
||||
/// The absolute position of the client's cursor.
|
||||
/// </summary>
|
||||
public Point AbsCursorPosition = Point.Empty;
|
||||
|
||||
public ClientState()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
37
Nibriboard/Utilities/JsonConverters/PointConverter.cs
Normal file
37
Nibriboard/Utilities/JsonConverters/PointConverter.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Nibriboard.Utilities.JsonConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// Deserialises objects into points from the System.Drawing namespace.
|
||||
/// </summary>
|
||||
public class PointConverter : JsonConverter
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
JObject jsonObject = JObject.Load(reader);
|
||||
|
||||
return new Point(
|
||||
jsonObject.Value<int>("X"),
|
||||
jsonObject.Value<int>("Y")
|
||||
);
|
||||
}
|
||||
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
if (objectType != typeof(Rectangle))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
39
Nibriboard/Utilities/JsonConverters/RectangleConverter.cs
Normal file
39
Nibriboard/Utilities/JsonConverters/RectangleConverter.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Nibriboard.Utilities.JsonConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// Deserialises objects into rectangles from the System.Drawing namespace.
|
||||
/// </summary>
|
||||
public class RectangleConverter : JsonConverter
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
JObject jsonObject = JObject.Load(reader);
|
||||
|
||||
return new Rectangle(
|
||||
jsonObject.Value<int>("X"),
|
||||
jsonObject.Value<int>("Y"),
|
||||
jsonObject.Value<int>("Width"),
|
||||
jsonObject.Value<int>("Height")
|
||||
);
|
||||
}
|
||||
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
if (objectType != typeof(Rectangle))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in a new issue