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) {