1
0
Fork 0
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:
Starbeamrainbowlabs 2017-02-04 21:26:48 +00:00
parent 199ffbfe40
commit 690abc6445
14 changed files with 1653 additions and 17 deletions

View 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()
{
}
}
}

View 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()
{
}
}
}

View file

@ -1,6 +1,10 @@
using System; using System;
using System.Drawing; 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 namespace Nibriboard.Client.Messages
{ {
public class HandshakeRequestMessage : Message public class HandshakeRequestMessage : Message
@ -9,10 +13,12 @@ namespace Nibriboard.Client.Messages
/// The initial visible area on the client's screen. /// The initial visible area on the client's screen.
/// Very useful for determining which chunks we should send a client when they first connect. /// Very useful for determining which chunks we should send a client when they first connect.
/// </summary> /// </summary>
[JsonConverter(typeof(RectangleConverter))]
public Rectangle InitialViewport = Rectangle.Empty; public Rectangle InitialViewport = Rectangle.Empty;
/// <summary> /// <summary>
/// The initial position of the user's cursor. /// The initial position of the user's cursor.
/// </summary> /// </summary>
[JsonConverter(typeof(RectangleConverter))]
public Point InitialAbsCursorPosition = Point.Empty; public Point InitialAbsCursorPosition = Point.Empty;
public HandshakeRequestMessage() public HandshakeRequestMessage()

View file

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using System.Text; using System.Text;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.Linq;
using IotWeb.Common.Http; using IotWeb.Common.Http;
using SBRL.Utilities; using SBRL.Utilities;
@ -88,12 +89,19 @@ namespace Nibriboard.Client
{ {
string eventName = JsonUtilities.DeserializeProperty<string>(frame, "event"); string eventName = JsonUtilities.DeserializeProperty<string>(frame, "event");
if (!messageEventTypes.ContainsKey(eventName)) if(eventName == null) {
Log.WriteLine("Received message with invalid event {1} from Client #{0}", Id, eventName); 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 messageType = messageEventTypes[eventName];
Type jsonNet = typeof(JsonConvert); 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); MethodInfo genericInfo = deserialiserInfo.MakeGenericMethod(messageType);
var decodedMessage = genericInfo.Invoke(null, new object[] { frame }); var decodedMessage = genericInfo.Invoke(null, new object[] { frame });
@ -147,6 +155,7 @@ namespace Nibriboard.Client
CurrentViewPort = message.InitialViewport; CurrentViewPort = message.InitialViewport;
AbsoluteCursorPosition = message.InitialAbsCursorPosition; AbsoluteCursorPosition = message.InitialAbsCursorPosition;
// Tell everyone else about the new client
Send(GenerateClientStateUpdate()); Send(GenerateClientStateUpdate());
return Task.CompletedTask; return Task.CompletedTask;
@ -158,6 +167,11 @@ namespace Nibriboard.Client
protected Task handleCursorPositionMessage(CursorPositionMessage message) { protected Task handleCursorPositionMessage(CursorPositionMessage message) {
AbsoluteCursorPosition = message.AbsCursorPosition; 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; return Task.CompletedTask;
} }
#endregion #endregion

View file

@ -3,6 +3,7 @@ using IotWeb.Common.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using Nibriboard.Client.Messages;
namespace Nibriboard.Client namespace Nibriboard.Client
{ {
@ -51,7 +52,8 @@ namespace Nibriboard.Client
Clients.Add(client); Clients.Add(client);
} }
public void Broadcast(NibriClient sendingClient, string message)
public void Broadcast(NibriClient sendingClient, Message message)
{ {
foreach(NibriClient client in Clients) foreach(NibriClient client in Clients)
{ {
@ -59,7 +61,7 @@ namespace Nibriboard.Client
if (client == sendingClient) if (client == sendingClient)
continue; continue;
client.SendRaw(message); client.Send(message);
} }
} }
} }

View file

@ -3,6 +3,7 @@
// npm modules // npm modules
window.EventEmitter = require("event-emitter-es6"); window.EventEmitter = require("event-emitter-es6");
window.FaviconNotification = require("favicon-notification"); window.FaviconNotification = require("favicon-notification");
window.panzoom = require("pan-zoom");
// Our files // Our files
import RippleLink from './RippleLink'; import RippleLink from './RippleLink';
@ -14,18 +15,31 @@ class BoardWindow extends EventEmitter
{ {
super(); // Run the parent constructor super(); // Run the parent constructor
// The maximum target fps.
this.maxFps = 60; this.maxFps = 60;
// Setup the fps indicator in the corner
this.renderTimeIndicator = document.createElement("span"); this.renderTimeIndicator = document.createElement("span");
this.renderTimeIndicator.innerHTML = "0ms"; this.renderTimeIndicator.innerHTML = "0ms";
document.querySelector(".fps").appendChild(this.renderTimeIndicator); document.querySelector(".fps").appendChild(this.renderTimeIndicator);
// Setup the canvas
this.canvas = canvas; this.canvas = canvas;
this.context = canvas.getContext("2d"); this.context = canvas.getContext("2d");
// --~~~--
// Setup the favicon thingy
FaviconNotification.init({ FaviconNotification.init({
color: '#ff6333' color: '#ff6333'
}); });
FaviconNotification.add(); 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) { get("/Settings.json").then(JSON.parse).then((function(settings) {
console.info("[setup]", "Obtained settings from server:", settings); console.info("[setup]", "Obtained settings from server:", settings);
this.settings = settings; this.settings = settings;
@ -34,13 +48,39 @@ class BoardWindow extends EventEmitter
console.error(`Error: Failed to fetch settings from server! Response: ${errorMessage}`); console.error(`Error: Failed to fetch settings from server! Response: ${errorMessage}`);
}); });
// Make the canvas track the window size
this.trackWindowSize(); 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() { setup() {
this.rippleLink = new RippleLink(this.settings.WebsocketUri, this); 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() nextFrame()
{ {
// The time at which the current frame started rendering. // The time at which the current frame started rendering.
@ -63,11 +103,17 @@ class BoardWindow extends EventEmitter
requestAnimationFrame(this.nextFrame.bind(this)); requestAnimationFrame(this.nextFrame.bind(this));
} }
/**
* Updates everything ready for the next frame to be rendered.
*/
update() update()
{ {
} }
/**
* Renders the next frame.
*/
render(canvas, context) render(canvas, context)
{ {
context.clearRect(0, 0, canvas.width, canvas.height); context.clearRect(0, 0, canvas.width, canvas.height);
@ -93,6 +139,23 @@ class BoardWindow extends EventEmitter
this.matchWindowSize(); this.matchWindowSize();
window.addEventListener("resize", this.matchWindowSize.bind(this)); 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; export default BoardWindow;

File diff suppressed because it is too large Load diff

View file

@ -1,15 +1,59 @@
"use strict"; "use strict";
import WebSocketStates from './Utilities/WebsocketStates';
class RippleLink const EventEmitter = require("event-emitter-es6");
class RippleLink extends EventEmitter
{ {
constructor(inSocketUrl, inBoardWindow) constructor(inSocketUrl, inBoardWindow)
{ {
super();
this.socketUrl = inSocketUrl; this.socketUrl = inSocketUrl;
this.boardWindow = inBoardWindow; this.boardWindow = inBoardWindow;
this.settings = this.boardWindow.settings; this.settings = this.boardWindow.settings;
// Create the websocket and commect to the server
this.websocket = new WebSocket(this.socketUrl, [ this.settings.WebsocketProtocol ]); 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;
} }
} }

View 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;

View file

@ -5,9 +5,9 @@
"homepage": "https://git.starbeamrainbowlabs.com/sbrl/Nibriboard#nibriboard", "homepage": "https://git.starbeamrainbowlabs.com/sbrl/Nibriboard#nibriboard",
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"pan-zoom": "^2.0.0",
"rollupify": "^0.3.8" "rollupify": "^0.3.8"
}, },
"devDependencies": { "devDependencies": {
"rollupify": "^0.3.8" "rollupify": "^0.3.8"
}, },

View file

@ -77,6 +77,8 @@
<Compile Include="Client\Messages\CursorPositionMessage.cs" /> <Compile Include="Client\Messages\CursorPositionMessage.cs" />
<Compile Include="Client\Messages\ClientStateMessage.cs" /> <Compile Include="Client\Messages\ClientStateMessage.cs" />
<Compile Include="RippleSpace\ClientState.cs" /> <Compile Include="RippleSpace\ClientState.cs" />
<Compile Include="Utilities\JsonConverters\RectangleConverter.cs" />
<Compile Include="Utilities\JsonConverters\PointConverter.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="ClientFiles\index.html" /> <EmbeddedResource Include="ClientFiles\index.html" />
@ -90,6 +92,7 @@
<Folder Include="ClientFiles\" /> <Folder Include="ClientFiles\" />
<Folder Include="Client\" /> <Folder Include="Client\" />
<Folder Include="Client\Messages\" /> <Folder Include="Client\Messages\" />
<Folder Include="Utilities\JsonConverters\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="packages.config" /> <None Include="packages.config" />

View 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()
{
}
}
}

View 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;
}
}
}

View 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;
}
}
}