diff --git a/client/aux.js b/client/aux.js
index 1a1d8b4..67eb18f 100644
--- a/client/aux.js
+++ b/client/aux.js
@@ -28,7 +28,7 @@ async function insert_image(state, context, file) {
if (resp.ok) {
const image_id = await resp.text();
const event = image_event(image_id, canvasp.x, canvasp.y, bitmap.width, bitmap.height);
- await queue_event(state, event);
+ queue_event(state, event);
}
}
diff --git a/client/client_recv.js b/client/client_recv.js
index 34b3f89..3f39dd7 100644
--- a/client/client_recv.js
+++ b/client/client_recv.js
@@ -365,71 +365,7 @@ function handle_event(state, context, event, options = {}) {
case EVENT.UNDO: {
geometry_add_dummy_stroke(context);
-
- for (let i = state.events.length - 1; i >=0; --i) {
- const other_event = state.events[i];
-
- // Users can only undo their own, undeleted (not already undone) events
- if (other_event.user_id === event.user_id && !other_event.deleted) {
- if (other_event.type === EVENT.STROKE) {
- other_event.deleted = true;
- if (other_event.bvh_node && !options.skip_bvh) {
- bvh_delete_stroke(state, other_event);
- }
- need_draw = true;
- break;
- } else if (other_event.type === EVENT.UNDO) {
- // do not undo an undo, we are not Notepad
- } else if (other_event.type === EVENT.IMAGE) {
- other_event.deleted = true;
- const image = get_image(context, other_event.image_id);
- if (image !== null) {
- image.deleted = true;
- }
- need_draw = true;
- break;
- } else if (other_event.type === EVENT.IMAGE_MOVE) {
- other_event.deleted = true;
- const image = get_image(context, other_event.image_id);
- if (image !== null) {
- image.move_head -= 2;
- image.at.x = image.move_history[image.move_head - 2];
- image.at.y = image.move_history[image.move_head - 1];
- need_draw = true;
- } else {
- console.warning('Undo image move for a non-existent image');
- }
- break;
- } else if (other_event.type === EVENT.IMAGE_SCALE) {
- other_event.deleted = true;
- const image = get_image(context, other_event.image_id);
- if (image !== null) {
- image.scale_head -= 4;
- image.at.x = image.scale_history[image.scale_head - 4];
- image.at.y = image.scale_history[image.scale_head - 3];
- image.width = image.scale_history[image.scale_head - 2];
- image.height = image.scale_history[image.scale_head - 1];
- need_draw = true;
- } else {
- console.warning('Undo image scale for a non-existent image');
- }
- break;
- } else if (other_event.type === EVENT.ERASER) {
- const stroke = state.events[other_event.stroke_id];
- stroke.deleted = false;
- if (!options.skip_bvh) {
- bvh_undelete_stroke(state, stroke);
- }
- need_draw = true;
- other_event.deleted = true;
- break;
- } else {
- console.error('cant undo event type', other_event.type);
- break;
- }
- }
- }
-
+ need_draw = undo(state, context, event, options);
break;
}
@@ -530,7 +466,7 @@ function handle_event(state, context, event, options = {}) {
return need_draw;
}
-async function handle_message(state, context, d) {
+function handle_message(state, context, d) {
const message_type = des_u32(d);
let do_draw = false;
@@ -660,7 +596,7 @@ async function handle_message(state, context, d) {
state.sn = sn;
- send_ack(sn); // await?
+ send_ack(sn);
break;
}
diff --git a/client/client_send.js b/client/client_send.js
index e1a749b..fd4b826 100644
--- a/client/client_send.js
+++ b/client/client_send.js
@@ -177,7 +177,7 @@ function ser_event(s, event) {
}
}
-async function send_ack(sn) {
+function send_ack(sn) {
const s = serializer_create(4 + 4);
ser_u32(s, MESSAGE.ACK);
@@ -186,13 +186,13 @@ async function send_ack(sn) {
if (config.debug_print) console.debug(`ack ${sn} out`);
try {
- if (ws) await ws.send(s.buffer);
+ if (ws) ws.send(s.buffer);
} catch(e) {
ws.close();
}
}
-async function send_follow(player_id) {
+function send_follow(player_id) {
const s = serializer_create(4 + 4);
player_id = player_id === null ? -1 : player_id;
@@ -203,13 +203,13 @@ async function send_follow(player_id) {
if (config.debug_print) console.debug(`follow ${player_id} out`);
try {
- if (ws) await ws.send(s.buffer);
+ if (ws) ws.send(s.buffer);
} catch (e) {
ws.close();
}
}
-async function sync_queue(state) {
+function sync_queue(state) {
if (ws === null) {
if (config.debug_print) console.debug('socket has closed, stopping SYNs');
return;
@@ -243,7 +243,7 @@ async function sync_queue(state) {
if (config.debug_print) console.debug(`syn ${state.lsn} out`);
try {
- if (ws) await ws.send(s.buffer);
+ if (ws) ws.send(s.buffer);
} catch(e) {
ws.close();
}
@@ -306,7 +306,7 @@ function queue_event(state, event, skip = false) {
}
// Fire and forget. Doesn't do anything if we are offline
-async function fire_event(state, event) {
+function fire_event(state, event) {
if (!state.online) { return; }
const s = serializer_create(4 + event_size(event));
@@ -315,7 +315,7 @@ async function fire_event(state, event) {
ser_event(s, event);
try {
- if (ws) await ws.send(s.buffer);
+ if (ws) ws.send(s.buffer);
} catch(e) {
ws.close();
}
diff --git a/client/config.js b/client/config.js
new file mode 100644
index 0000000..edd13fe
--- /dev/null
+++ b/client/config.js
@@ -0,0 +1,35 @@
+const config = {
+ ws_url: `wss://${window.location.host}/ws/`,
+ ping_url: `https://${window.location.host}/api/ping`,
+ image_url: `https://${window.location.host}/images/`,
+ sync_timeout: 1000,
+ ws_reconnect_timeout: 2000,
+ brush_preview_timeout: 1000,
+ second_finger_timeout: 500,
+ buffer_first_touchmoves: 5,
+ debug_print: false,
+ zoom_delta: 0.05,
+ min_zoom_level: -250,
+ max_zoom_level: 100,
+ initial_offline_timeout: 1000,
+ default_color: 0x00,
+ default_width: 8,
+ bytes_per_instance: 4 * 2 + 4, // axy, stroke_id
+ bytes_per_stroke: 2 * 3 + 2, // r, g, b, width
+ initial_static_bytes: 4096 * 16,
+ initial_dynamic_bytes: 4096,
+ initial_wasm_bytes: 4096,
+ stroke_texture_size: 1024, // means no more than 1024^2 = 1M strokes in total (this is a LOT. HMH blackboard has like 80K)
+ dynamic_stroke_texture_size: 128, // means no more than 128^2 = 16K dynamic strokes at once
+ ui_texture_size: 16,
+ bvh_fullnode_depth: 5,
+ pattern_fadeout_min: 0.3,
+ pattern_fadeout_max: 0.75,
+ min_pressure: 50,
+ benchmark: {
+ zoom_level: -75,
+ offset: { x: 425, y: -1195 },
+ frames: 500,
+ },
+};
+
diff --git a/client/index.html b/client/index.html
index 4b7b627..9bc8cbc 100644
--- a/client/index.html
+++ b/client/index.html
@@ -21,6 +21,8 @@
+
+
@@ -42,6 +44,7 @@
+
diff --git a/client/index.js b/client/index.js
index 43502f4..a986914 100644
--- a/client/index.js
+++ b/client/index.js
@@ -1,43 +1,5 @@
document.addEventListener('DOMContentLoaded', main);
-const config = {
-// ws_url: 'wss://desk.some.website/ws/',
-// ping_url: 'https://desk.some.website/api/ping',
-// image_url: 'https://desk.some.website/images/',
- ws_url: `wss://${window.location.host}/ws/`,
- ping_url: `https://${window.location.host}/api/ping`,
- image_url: `https://${window.location.host}/images/`,
- sync_timeout: 1000,
- ws_reconnect_timeout: 2000,
- brush_preview_timeout: 1000,
- second_finger_timeout: 500,
- buffer_first_touchmoves: 5,
- debug_print: false,
- zoom_delta: 0.05,
- min_zoom_level: -250,
- max_zoom_level: 100,
- initial_offline_timeout: 1000,
- default_color: 0x00,
- default_width: 8,
- bytes_per_instance: 4 * 2 + 4, // axy, stroke_id
- bytes_per_stroke: 2 * 3 + 2, // r, g, b, width
- initial_static_bytes: 4096 * 16,
- initial_dynamic_bytes: 4096,
- initial_wasm_bytes: 4096,
- stroke_texture_size: 1024, // means no more than 1024^2 = 1M strokes in total (this is a LOT. HMH blackboard has like 80K)
- dynamic_stroke_texture_size: 128, // means no more than 128^2 = 16K dynamic strokes at once
- ui_texture_size: 16,
- bvh_fullnode_depth: 5,
- pattern_fadeout_min: 0.3,
- pattern_fadeout_max: 0.75,
- min_pressure: 50,
- benchmark: {
- zoom_level: -75,
- offset: { x: 425, y: -1195 },
- frames: 500,
- },
-};
-
const EVENT = Object.freeze({
PREDRAW: 10,
SET_COLOR: 11,
@@ -187,6 +149,7 @@ async function main() {
'active_image': null,
'scaling_corner': null,
'ruler_origin': null,
+ 'image_actually_moved': false,
'current_strokes': {},
diff --git a/client/undo.js b/client/undo.js
new file mode 100644
index 0000000..869213d
--- /dev/null
+++ b/client/undo.js
@@ -0,0 +1,103 @@
+function undo(state, context, event, options) {
+ let need_draw = false;
+
+ // Remove effect of latest own event, in a way that is recoverable
+
+ // Iterate back to front to find the _latest_ event
+ for (let i = state.events.length - 1; i >=0; --i) {
+ const other_event = state.events[i];
+ let skipped = false;
+
+ // Users can only undo their own, undeleted (not already undone) events
+ if (other_event.user_id === event.user_id && !other_event.deleted) {
+ // All "persistent" events (those that are pushed using SYN messages) should be handled here
+ // "Transient" events are by design droppable, and should not be undone, nor saved in state.events at all
+ switch (other_event.type) {
+ case EVENT.STROKE: {
+ other_event.deleted = true;
+ if (other_event.bvh_node && !options.skip_bvh) {
+ bvh_delete_stroke(state, other_event);
+ }
+ need_draw = true;
+ break;
+ }
+
+ case EVENT.UNDO: {
+ // do not undo an undo, we are not Notepad
+ skipped = true;
+ break;
+ }
+
+ case EVENT.IMAGE: {
+ other_event.deleted = true;
+ const image = get_image(context, other_event.image_id);
+ if (image !== null) {
+ image.deleted = true;
+ }
+ need_draw = true;
+ break;
+ }
+
+ case EVENT.IMAGE_MOVE: {
+ other_event.deleted = true;
+ const image = get_image(context, other_event.image_id);
+ if (image !== null) {
+ image.move_head -= 2;
+ image.at.x = image.move_history[image.move_head - 2];
+ image.at.y = image.move_history[image.move_head - 1];
+ need_draw = true;
+ } else {
+ console.warning('Undo image move for a non-existent image');
+ }
+ break;
+ }
+
+ case EVENT.IMAGE_SCALE: {
+ other_event.deleted = true;
+ const image = get_image(context, other_event.image_id);
+ if (image !== null) {
+ image.scale_head -= 4;
+
+ // NEXT: merge move and scale. Otherwise we can't know
+ // that there have been move events inbetween scale
+
+ image.at.x = image.scale_history[image.scale_head - 4];
+ image.at.y = image.scale_history[image.scale_head - 3];
+ image.width = image.scale_history[image.scale_head - 2];
+ image.height = image.scale_history[image.scale_head - 1];
+ need_draw = true;
+ } else {
+ console.warning('Undo image scale for a non-existent image');
+ }
+ break;
+ }
+
+ case EVENT.ERASER: {
+ other_event.deleted = true;
+ const stroke = state.events[other_event.stroke_id];
+ stroke.deleted = false;
+ if (!options.skip_bvh) {
+ bvh_undelete_stroke(state, stroke);
+ }
+ need_draw = true;
+ break;
+ }
+
+ default: {
+ console.error('cant undo event type', other_event.type);
+ break;
+ }
+ }
+
+ if (!skipped) {
+ break;
+ }
+ }
+ }
+
+ return need_draw;
+}
+
+function redo() {
+ console.log('TODO');
+}
diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js
index b71d03f..2f6d03f 100644
--- a/client/webgl_listeners.js
+++ b/client/webgl_listeners.js
@@ -25,6 +25,7 @@ function init_listeners(state, context) {
function debug_panel_init(state, context) {
document.getElementById('debug-red').checked = state.debug.red;
document.getElementById('do-snap').checked = state.snap !== null;
+ document.getElementById('debug-print').checked = config.debug_print;
document.getElementById('debug-red').addEventListener('change', (e) => {
state.debug.red = e.target.checked;
@@ -35,6 +36,10 @@ function debug_panel_init(state, context) {
state.snap = e.target.checked ? 'grid' : null;
});
+ document.getElementById('debug-print').addEventListener('change', (e) => {
+ config.debug_print = e.target.checked;
+ });
+
document.getElementById('debug-begin-benchmark').addEventListener('click', (e) => {
state.canvas.zoom_level = config.benchmark.zoom_level;
state.canvas.offset.x = config.benchmark.offset.x;
@@ -263,6 +268,7 @@ function mousedown(e, state, context) {
state.active_image = image.key;
// Allow immediately moving
state.imagemoving = true;
+ state.image_actually_moved = false;
image.raw_at.x = image.at.x;
image.raw_at.y = image.at.y;
} else {
@@ -403,6 +409,7 @@ function mousemove(e, state, context) {
image.at.y = image.raw_at.y;
}
+ state.image_actually_moved = true;
do_draw = true;
}
}
@@ -495,11 +502,14 @@ function mouseup(e, state, context) {
if (state.imagemoving) {
state.imagemoving = false;
- const image = get_image(context, state.active_image);
- image.raw_at.x = image.at.x;
- image.raw_at.y = image.at.y;
- queue_event(state, image_move_event(state.active_image, image.at.x, image.at.y));
- schedule_draw(state, context);
+ if (state.image_actually_moved) {
+ state.image_actually_moved = false;
+ const image = get_image(context, state.active_image);
+ image.raw_at.x = image.at.x;
+ image.raw_at.y = image.at.y;
+ queue_event(state, image_move_event(state.active_image, image.at.x, image.at.y));
+ schedule_draw(state, context);
+ }
return;
}
diff --git a/client/websocket.js b/client/websocket.js
index 2b646e6..2a0b1d4 100644
--- a/client/websocket.js
+++ b/client/websocket.js
@@ -48,7 +48,7 @@ async function on_message(state, context, event) {
message_data = await data.arrayBuffer();
const view = new DataView(message_data);
const d = deserializer_create(message_data, view);
- await handle_message(state, context, d);
+ handle_message(state, context, d);
} else {
/* For all my Safari < 14 bros out there */
const reader = new FileReader();
@@ -56,7 +56,7 @@ async function on_message(state, context, event) {
message_data = e.target.result;
const view = new DataView(message_data);
const d = deserializer_create(message_data, view);
- await handle_message(state, context, d);
+ handle_message(state, context, d);
};
reader.readAsArrayBuffer(data);
diff --git a/server/recv.js b/server/recv.js
index 969874a..d03f2b0 100644
--- a/server/recv.js
+++ b/server/recv.js
@@ -163,7 +163,7 @@ function handle_event(session, event) {
}
}
-export async function handle_message(ws, d) {
+export function handle_message(ws, d) {
if (!(ws.data.session_id in sessions)) {
return;
}
diff --git a/server/send.js b/server/send.js
index c2e38c3..c1326d2 100644
--- a/server/send.js
+++ b/server/send.js
@@ -108,7 +108,7 @@ function create_session(ws, desk_id) {
return session;
}
-export async function send_init(ws) {
+export function send_init(ws) {
if (!ws) {
return;
}
@@ -185,7 +185,7 @@ export async function send_init(ws) {
ser.event(s, event);
}
- await ws.send(s.buffer);
+ ws.send(s.buffer);
}
export function send_ack(ws, lsn) {
@@ -242,7 +242,7 @@ export function fire_event(from_session, event) {
}
}
-async function sync_session(session_id) {
+function sync_session(session_id) {
if (!(session_id in sessions)) {
return;
}
@@ -284,7 +284,7 @@ async function sync_session(session_id) {
if (config.DEBUG_PRINT) console.log(`syn ${desk.sn} out`);
- await session.ws.send(s.buffer);
+ session.ws.send(s.buffer);
if (session.sync_attempts < config.SYNC_MAX_ATTEMPTS) {
session.sync_attempts += 1;
diff --git a/server/server.js b/server/server.js
index edc5428..45817b9 100644
--- a/server/server.js
+++ b/server/server.js
@@ -58,7 +58,7 @@ export function startup() {
async message(ws, u8array) {
const dataview = new DataView(u8array.buffer);
const d = des.create(dataview);
- await recv.handle_message(ws, d);
+ recv.handle_message(ws, d);
},
close(ws, code, message) {