From 90f0beb4f9e68f99b9a4ea0625cffa4e60428d08 Mon Sep 17 00:00:00 2001 From: Aleksey Olokhtonov Date: Sun, 12 May 2024 12:44:58 +0300 Subject: [PATCH] Undo is back in! --- README.txt | 3 +- client/bvh.js | 44 ++++++++++++++++------ client/client_recv.js | 79 ++++++++++++++++++++------------------- client/client_send.js | 6 +++ client/index.js | 2 +- client/tools.js | 1 + client/webgl_geometry.js | 10 ++++- client/webgl_listeners.js | 6 ++- 8 files changed, 97 insertions(+), 54 deletions(-) diff --git a/README.txt b/README.txt index 0e8beab..ca6399d 100644 --- a/README.txt +++ b/README.txt @@ -40,7 +40,8 @@ Release: + Dynamic svg cursor to represent the brush - Eraser - Line drawing - - Undo/redo + + Undo + - Redo * Polish + Use typedvector where appropriate - Show what's happening while the desk is loading (downloading, processing, uploading to gpu) diff --git a/client/bvh.js b/client/bvh.js index 73ebaf9..d330e98 100644 --- a/client/bvh.js +++ b/client/bvh.js @@ -89,9 +89,11 @@ function bvh_find_best_sibling(bvh, leaf_index) { return best_index; } -function bvh_add_stroke(bvh, index, stroke) { +function bvh_add_stroke(state, bvh, index, stroke) { const leaf_index = bvh_make_leaf(bvh, index, stroke); + stroke.bvh_node = leaf_index; + if (bvh.nodes.length === 1) { bvh.root = leaf_index; return; @@ -157,7 +159,22 @@ function bvh_add_stroke(bvh, index, stroke) { } } -function bvh_intersect_quad(bvh, quad, result_buffer) { +function bvh_delete_stroke(state, stroke) { + let node = state.bvh.nodes[stroke.bvh_node]; + + while (node.parent_index !== null) { + if (node.is_fullnode) { + let index_index = node.stroke_indices.data.indexOf(stroke.index); + node.stroke_indices.data[index_index] = node.stroke_indices.data[node.stroke_indices.size - 1]; + tv_pop(node.stroke_indices); + break; + } + + node = state.bvh.nodes[node.parent_index]; + } +} + +function bvh_intersect_quad(state, bvh, quad, result_buffer) { if (bvh.root === null) { return; } @@ -181,7 +198,9 @@ function bvh_intersect_quad(bvh, quad, result_buffer) { } if (node.is_leaf) { - tv_add(result_buffer, node.stroke_index); + if (state.events[node.stroke_index].deleted !== true) { + tv_add(result_buffer, node.stroke_index); + } } else { tv_add(bvh.traverse_stack, node.child1); tv_add(bvh.traverse_stack, node.child2); @@ -211,7 +230,7 @@ function bvh_clip(state, context) { 'y2': screen_bottomright.y }; - bvh_intersect_quad(state.bvh, screen, context.clipped_indices); + bvh_intersect_quad(state, state.bvh, screen, context.clipped_indices); tv_data(context.clipped_indices).sort(); // we need to draw back to front still! } @@ -241,7 +260,7 @@ function bvh_point(state, p) { const ys = state.wasm.buffers['ys'].tv.data.subarray(stroke.coords_from, stroke.coords_to); const pressures = state.wasm.buffers['pressures'].tv.data.subarray(stroke.coords_from, stroke.coords_to); - if (point_in_stroke(p, xs, ys, pressures, stroke.width)) { + if (stroke.deleted !== true && point_in_stroke(p, xs, ys, pressures, stroke.width)) { indices.push(node.stroke_index); } } else { @@ -258,7 +277,7 @@ function bvh_point(state, p) { return null; } -function bvh_construct_rec(bvh, vertical, strokes, depth) { +function bvh_construct_rec(state, bvh, vertical, strokes, depth) { if (strokes.length > 1) { // internal let sorted_strokes; @@ -272,8 +291,8 @@ function bvh_construct_rec(bvh, vertical, strokes, depth) { const node_index = bvh_make_internal(bvh); const left_of_split_count = Math.floor(strokes.length / 2); - const child1 = bvh_construct_rec(bvh, !vertical, sorted_strokes.slice(0, left_of_split_count), depth + 1); - const child2 = bvh_construct_rec(bvh, !vertical, sorted_strokes.slice(left_of_split_count, sorted_strokes.length), depth + 1); + const child1 = bvh_construct_rec(state, bvh, !vertical, sorted_strokes.slice(0, left_of_split_count), depth + 1); + const child2 = bvh_construct_rec(state, bvh, !vertical, sorted_strokes.slice(left_of_split_count, sorted_strokes.length), depth + 1); bvh.nodes[child1].parent_index = node_index; bvh.nodes[child2].parent_index = node_index; @@ -296,12 +315,15 @@ function bvh_construct_rec(bvh, vertical, strokes, depth) { return node_index; } else { // leaf - return bvh_make_leaf(bvh, strokes[0].index, strokes[0]); + const leaf_index = bvh_make_leaf(bvh, strokes[0].index, strokes[0]); + state.events[strokes[0].index].bvh_node = leaf_index; + return leaf_index; } } function bvh_construct(state) { - if (state.events.length > 0) { - state.bvh.root = bvh_construct_rec(state.bvh, true, state.events, 0); + const strokes = state.events.filter(e => e.type === EVENT.STROKE && e.deleted !== true); + if (strokes.length > 0) { + state.bvh.root = bvh_construct_rec(state, state.bvh, true, strokes, 0); } } diff --git a/client/client_recv.js b/client/client_recv.js index 93ad814..262c165 100644 --- a/client/client_recv.js +++ b/client/client_recv.js @@ -245,7 +245,17 @@ function handle_event(state, context, event, options = {}) { break; } - wasm_ensure_by(state, 1, event.coords.length); + let last_stroke = null; + + for (let i = state.events.length - 1; i >= 0; --i) { + if (state.events[i].type === EVENT.STROKE) { + last_stroke = state.events[i]; + break; + } + } + + const index_difference = state.events.length - (last_stroke === null ? 0 : last_stroke.index); + wasm_ensure_by(state, index_difference, event.coords.length); const pressures = state.wasm.buffers['pressures']; const xs = state.wasm.buffers['xs']; @@ -254,6 +264,13 @@ function handle_event(state, context, event, options = {}) { event.coords_from = xs.tv.size; event.coords_to = xs.tv.size + point_count; + for (let i = 0; i < index_difference - 1; ++i) { + // Create empty records for all non-stroke events that happened since the last stroke + tv_add(state.wasm.buffers['coords_from'].tv, xs.tv.size); + state.wasm.buffers['coords_from'].used += 4; // 4 bytes, not 4 ints + } + + // Create actual records for this stroke tv_add(state.wasm.buffers['coords_from'].tv, xs.tv.size + point_count); state.wasm.buffers['coords_from'].used += 4; // 4 bytes, not 4 ints @@ -285,44 +302,28 @@ function handle_event(state, context, event, options = {}) { } case EVENT.UNDO: { - need_draw = true; - console.error('todo'); - // 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; - // const stats = stroke_stats(other_event.points, state.cursor.width); - // redraw_region(stats.bbox); - // break; - // } else if (other_event.type === EVENT.IMAGE) { - // other_event.deleted = true; - // const item = document.querySelector(`img[data-image-id="${other_event.image_id}"]`); - // if (item) item.remove(); - // break; - // } else if (other_event.type === EVENT.ERASER) { - // other_event.deleted = true; - // const erased = find_stroke_backwards(other_event.stroke_id); - // if (erased) { - // erased.deleted = false; - // const stats = stroke_stats(erased.points, state.cursor.width); - // redraw_region(stats.bbox); - // } - // break; - // } else if (other_event.type === EVENT.IMAGE_MOVE) { - // const item = document.querySelector(`img[data-image-id="${other_event.image_id}"]`); - - // const ix = state.images[other_event.image_id].x -= other_event.x; - // const iy = state.images[other_event.image_id].y -= other_event.y; - - // item.style.transform = `translate(${ix}px, ${iy}px)`; - - // break; - // } - // } - // } + 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) { + 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 { + console.error('cant undo event type', other_event.type); + break; + } + } + } break; } diff --git a/client/client_send.js b/client/client_send.js index 4877fd8..e595d8e 100644 --- a/client/client_send.js +++ b/client/client_send.js @@ -369,3 +369,9 @@ function movecanvas_event(state) { 'zoom': state.canvas.zoom, }; } + +function undo_event(state) { + return { + 'type': EVENT.UNDO, + }; +} diff --git a/client/index.js b/client/index.js index 5333281..69d77e0 100644 --- a/client/index.js +++ b/client/index.js @@ -15,7 +15,7 @@ const config = { debug_print: false, zoom_delta: 0.05, min_zoom_level: -250, - max_zoom_level: 40, + max_zoom_level: 100, initial_offline_timeout: 1000, default_color: 0x00, default_width: 8, diff --git a/client/tools.js b/client/tools.js index 09be8ea..5b28778 100644 --- a/client/tools.js +++ b/client/tools.js @@ -2,6 +2,7 @@ function switch_tool(state, item) { const tool = item.getAttribute('data-tool'); if (tool === 'undo') { + queue_event(state, undo_event(state)); return; } diff --git a/client/webgl_geometry.js b/client/webgl_geometry.js index e881107..a9be331 100644 --- a/client/webgl_geometry.js +++ b/client/webgl_geometry.js @@ -48,6 +48,14 @@ async function geometry_write_instances(state, context, callback) { return segment_count; } +function geometry_add_dummy_stroke(context) { + context.stroke_data = ser_ensure_by(context.stroke_data, config.bytes_per_stroke); + ser_u16(context.stroke_data, 0); + ser_u16(context.stroke_data, 0); + ser_u16(context.stroke_data, 0); + ser_u16(context.stroke_data, 0); +} + function geometry_add_stroke(state, context, stroke, stroke_index, skip_bvh = false) { if (!state.online || !stroke || stroke.coords_to - stroke.coords_from === 0) return; @@ -66,7 +74,7 @@ function geometry_add_stroke(state, context, stroke, stroke_index, skip_bvh = fa ser_u16(context.stroke_data, b); ser_u16(context.stroke_data, stroke.width); - if (!skip_bvh) bvh_add_stroke(state.bvh, stroke_index, stroke); + if (!skip_bvh) bvh_add_stroke(state, state.bvh, stroke_index, stroke); } function geometry_delete_stroke(state, context, stroke_index) { diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js index b00910c..56dc268 100644 --- a/client/webgl_listeners.js +++ b/client/webgl_listeners.js @@ -146,7 +146,11 @@ function keydown(e, state, context) { } else if (e.code === 'KeyD') { document.querySelector('.debug-window').classList.toggle('dhide'); } else if (e.code === 'KeyZ') { - state.zoomdown = true; + if (e.ctrlKey) { + queue_event(state, undo_event(state)); + } else { + state.zoomdown = true; + } } }