diff --git a/.gitignore b/.gitignore
index afc01d3..9029ada 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
server/images
doca.txt
data/
+client/*.dot
diff --git a/client/bvh.js b/client/bvh.js
new file mode 100644
index 0000000..b1d43e4
--- /dev/null
+++ b/client/bvh.js
@@ -0,0 +1,284 @@
+// TODO: get rid of node_count
+//
+function bvh_make_leaf(bvh, index, stroke) {
+ const leaf = {
+ 'stroke_index': index,
+ 'bbox': stroke.bbox,
+ 'area': stroke.area,
+ 'parent_index': null,
+ 'is_leaf': true,
+ };
+
+ bvh.nodes.push(leaf);
+
+ return bvh.nodes.length - 1;
+}
+
+function bvh_make_internal(bvh) {
+ const node = {
+ 'child1': null,
+ 'child2': null,
+ 'parent_index': null,
+ 'is_leaf': false,
+ };
+
+ bvh.nodes.push(node);
+
+ return bvh.nodes.length - 1;
+}
+
+function bvh_compute_sah(bvh, new_leaf, potential_sibling, only_parent = false) {
+ let cost = 0;
+ let union_box;
+
+ if (!only_parent) {
+ union_box = quad_union(new_leaf.bbox, potential_sibling.bbox);
+
+ const internal_node_would_be = { 'bbox': union_box };
+ const new_internal_node_cost = (union_box.x2 - union_box.x1) * (union_box.y2 - union_box.y1);
+
+ cost += new_internal_node_cost;
+ } else {
+ union_box = new_leaf.bbox;
+ }
+
+ let parent_index = potential_sibling.parent_index;
+
+ while (parent_index !== null) {
+ const current_node = bvh.nodes[parent_index];
+ const old_cost = current_node.area;
+ union_box = quad_union(current_node.bbox, union_box);
+ const new_cost = (union_box.x2 - union_box.x1) * (union_box.y2 - union_box.y1);
+ cost += new_cost - old_cost;
+ parent_index = current_node.parent_index;
+ }
+
+ return cost;
+}
+
+// todo area func
+
+function bvh_find_best_sibling(bvh, leaf_index) {
+ // branch and bound
+
+ const leaf = bvh.nodes[leaf_index];
+ const leaf_cost = (leaf.bbox.x2 - leaf.bbox.x1) * (leaf.bbox.y2 - leaf.bbox.y1);
+
+ let best_cost = bvh_compute_sah(bvh, leaf, bvh.nodes[bvh.root]);
+ let best_index = bvh.root;
+
+ bvh.pqueue.clear();
+ bvh.pqueue.push(best_index, best_cost);
+
+ while (bvh.pqueue.size > 0) {
+ const current_index = bvh.pqueue.pop();
+ const current_node = bvh.nodes[current_index];
+ const cost = bvh_compute_sah(bvh, current_node, leaf);
+
+ if (cost < best_cost) {
+ best_cost = cost;
+ best_index = current_index;
+ }
+
+ if (!current_node.is_leaf) {
+ const child1 = bvh.nodes[current_node.child1];
+ const lower_bound_for_children = bvh_compute_sah(bvh, child1, leaf, true) + leaf_cost;
+ if (lower_bound_for_children < best_cost) {
+ bvh.pqueue.push(current_node.child1, lower_bound_for_children);
+ bvh.pqueue.push(current_node.child2, lower_bound_for_children);
+ }
+ }
+ }
+
+ return best_index;
+}
+
+function bvh_rotate(bvh, index) {
+
+}
+
+function bvh_add_stroke(bvh, index, stroke) {
+ const leaf_index = bvh_make_leaf(bvh, index, stroke);
+
+ if (bvh.node_count === 0) {
+ bvh.root = leaf_index;
+ bvh.node_count++;
+ return;
+ }
+
+ bvh.node_count++;
+
+ if (bvh.pqueue.capacity < Math.ceil(bvh.node_count * 1.2)) {
+ bvh.pqueue = new MinQueue(bvh.pqueue.capacity * 2);
+ }
+
+ // It's as easy as 1-2-3
+
+ // 1. Find best sibling for leaf
+ const sibling = bvh_find_best_sibling(bvh, leaf_index);
+
+ // 2. Create new parent
+ const old_parent = bvh.nodes[sibling].parent_index;
+ const new_parent = bvh_make_internal(bvh);
+
+ bvh.nodes[new_parent].parent_index = old_parent;
+ bvh.nodes[new_parent].bbox = quad_union(stroke.bbox, bvh.nodes[sibling].bbox);
+
+ if (old_parent !== null) {
+ // The sibling was not the root
+ if (bvh.nodes[old_parent].child1 === sibling) {
+ bvh.nodes[old_parent].child1 = new_parent;
+ } else {
+ bvh.nodes[old_parent].child2 = new_parent;
+ }
+
+ bvh.nodes[new_parent].child1 = sibling;
+ bvh.nodes[new_parent].child2 = leaf_index;
+
+ bvh.nodes[sibling].parent_index = new_parent;
+ bvh.nodes[leaf_index].parent_index = new_parent;
+ } else {
+ // The sibling was the root
+ bvh.nodes[new_parent].child1 = sibling;
+ bvh.nodes[new_parent].child2 = leaf_index;
+
+ bvh.nodes[sibling].parent_index = new_parent;
+ bvh.nodes[leaf_index].parent_index = new_parent;
+
+ bvh.root = new_parent;
+ }
+
+ const new_bbox = quad_union(bvh.nodes[bvh.nodes[new_parent].child1].bbox, bvh.nodes[bvh.nodes[new_parent].child2].bbox);
+ bvh.nodes[new_parent].bbox = new_bbox;
+ bvh.nodes[new_parent].area = (new_bbox.x2 - new_bbox.x1) * (new_bbox.y2 - new_bbox.y1);
+
+ bvh.node_count++;
+
+ // 3. Refit and rotate
+ let refit_index = bvh.nodes[leaf_index].parent_index;
+ while (refit_index !== null) {
+ const child1 = bvh.nodes[refit_index].child1;
+ const child2 = bvh.nodes[refit_index].child2;
+
+ bvh.nodes[refit_index].bbox = quad_union(bvh.nodes[child1].bbox, bvh.nodes[child2].bbox);
+
+ bvh_rotate(bvh, refit_index);
+
+ refit_index = bvh.nodes[refit_index].parent_index;
+ }
+}
+
+function bvh_intersect_quad(bvh, quad) {
+ if (bvh.root === null) {
+ return [];
+ }
+
+ const stack = [bvh.root];
+ const result = [];
+
+ while (stack.length > 0) {
+ const node_index = stack.pop();
+ const node = bvh.nodes[node_index];
+
+ if (!quads_intersect(node.bbox, quad)) {
+ continue;
+ }
+
+ if (node.is_leaf) {
+ result.push(node.stroke_index);
+ } else {
+ stack.push(node.child1, node.child2);
+ }
+ }
+
+ return result;
+}
+
+function bvh_clip(state, context) {
+ if (state.onscreen_segments === null) {
+ let total_points = 0;
+
+ for (const event of state.events) {
+ if (event.type === EVENT.STROKE && !event.deleted && event.points.length > 0) {
+ total_points += event.points.length - 1;
+ }
+ }
+
+ if (total_points > 0) {
+ state.onscreen_segments = new Uint32Array(total_points * 6);
+ }
+ }
+
+ let at = 0;
+
+ const screen_topleft = screen_to_canvas(state, {'x': 0, 'y': 0});
+ const screen_bottomright = screen_to_canvas(state, {'x': context.canvas.width, 'y': context.canvas.height});
+ const screen_topright = { 'x': screen_bottomright.x, 'y': screen_topleft.y };
+ const screen_bottomleft = { 'x': screen_topleft.x, 'y': screen_bottomright.y };
+
+ const screen = {'x1': screen_topleft.x, 'y1': screen_topleft.y, 'x2': screen_bottomright.x, 'y2': screen_bottomright.y};
+
+ const stroke_indices = bvh_intersect_quad(state.bvh, screen);
+ stroke_indices.sort();
+
+ for (const i of stroke_indices) {
+ if (state.debug.limit_to && i >= state.debug.render_to) break;
+
+ const event = state.events[i];
+
+ if (!(state.debug.limit_from && i < state.debug.render_from)) {
+ if (event.type === EVENT.STROKE && !event.deleted && event.points.length > 0) {
+ for (let j = 0; j < event.points.length - 1; ++j) {
+ let base = event.starting_index + j * 4;
+ // We draw quads as [1, 2, 3, 4, 3, 2]
+ state.onscreen_segments[at + 0] = base + 0;
+ state.onscreen_segments[at + 1] = base + 1;
+ state.onscreen_segments[at + 2] = base + 2;
+ state.onscreen_segments[at + 3] = base + 3;
+ state.onscreen_segments[at + 4] = base + 2;
+ state.onscreen_segments[at + 5] = base + 1;
+
+ at += 6;
+ }
+ }
+ }
+ }
+
+ return at;
+
+}
+
+function bvh_construct_rec(bvh, vertical, strokes) {
+ if (strokes.length > 1) {
+ // internal
+ let sorted_strokes;
+
+ if (vertical) {
+ sorted_strokes = strokes.toSorted((a, b) => a.bbox.cy - b.bbox.cy);
+ } else {
+ sorted_strokes = strokes.toSorted((a, b) => a.bbox.cx - b.bbox.cx);
+ }
+
+ 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));
+ const child2 = bvh_construct_rec(bvh, !vertical, sorted_strokes.slice(left_of_split_count, sorted_strokes.length));
+
+ bvh.nodes[child1].parent_index = node_index;
+ bvh.nodes[child2].parent_index = node_index;
+
+ bvh.nodes[node_index].child1 = child1;
+ bvh.nodes[node_index].child2 = child2;
+ bvh.nodes[node_index].bbox = quad_union(bvh.nodes[child1].bbox, bvh.nodes[child2].bbox);
+
+ return node_index;
+ } else {
+ // leaf
+ return bvh_make_leaf(bvh, strokes[0].index, strokes[0]);
+ }
+}
+
+function bvh_construct(state) {
+ state.bvh.root = bvh_construct_rec(state.bvh, true, state.events);
+}
diff --git a/client/client_recv.js b/client/client_recv.js
index 74d4f38..2bf76b1 100644
--- a/client/client_recv.js
+++ b/client/client_recv.js
@@ -124,7 +124,7 @@ function bitmap_bbox(event) {
'xmin': event.x,
'xmax': event.x + event.bitmap.width,
'ymin': event.y,
- 'ymax': event.y + event.bitmap.height
+ 'ymax': event.y + event.bitmap.height,
};
return bbox;
@@ -138,7 +138,7 @@ function init_player_defaults(state, player_id, color = config.default_color, wi
};
}
-function handle_event(state, context, event) {
+function handle_event(state, context, event, options = {}) {
if (config.debug_print) console.debug(`event type ${event.type} from user ${event.user_id}`);
let need_draw = false;
@@ -174,13 +174,17 @@ function handle_event(state, context, event) {
geometry_clear_player(state, context, event.user_id);
need_draw = true;
}
+
+ event.index = state.events.length;
+ event.starting_index = state.starting_index;
- geometry_add_stroke(state, context, event, state.events.length);
+ if (event.points.length > 0) {
+ state.starting_index += (event.points.length - 1) * 4;
+ }
- state.stroke_count++;
+ geometry_add_stroke(state, context, event, state.events.length, options.skip_bvh === true);
- document.getElementById('debug-render-from').max = state.stroke_count;
- document.getElementById('debug-render-to').max = state.stroke_count;
+ state.stroke_count++;
break;
}
@@ -357,10 +361,18 @@ async function handle_message(state, context, d) {
for (let i = 0; i < event_count; ++i) {
const event = des_event(d);
- handle_event(state, context, event);
- state.events.push(event);
+ handle_event(state, context, event, {'skip_bvh': true});
+
+ if (event.type !== EVENT.STROKE || event.points.length > 0) {
+ state.events.push(event);
+ }
}
+ bvh_construct(state);
+
+ document.getElementById('debug-render-from').max = state.stroke_count;
+ document.getElementById('debug-render-to').max = state.stroke_count;
+
do_draw = true;
send_ack(event_count);
diff --git a/client/heapify.js b/client/heapify.js
new file mode 100644
index 0000000..6b15936
--- /dev/null
+++ b/client/heapify.js
@@ -0,0 +1,130 @@
+// translated by esbuild to js. original typescript source MIT licensed at https://github.com/luciopaiva/heapify
+
+const ROOT_INDEX = 1;
+class MinQueue {
+ constructor(capacity = 64, keys = [], priorities = [], KeysBackingArrayType = Uint32Array, PrioritiesBackingArrayType = Uint32Array) {
+ this._capacity = capacity;
+ this._keys = new KeysBackingArrayType(capacity + ROOT_INDEX);
+ this._priorities = new PrioritiesBackingArrayType(capacity + ROOT_INDEX);
+ this._hasPoppedElement = false;
+ if (keys.length !== priorities.length) {
+ throw new Error("Number of keys does not match number of priorities provided.");
+ }
+ if (capacity < keys.length) {
+ throw new Error("Capacity less than number of provided keys.");
+ }
+ for (let i = 0; i < keys.length; i++) {
+ this._keys[i + ROOT_INDEX] = keys[i];
+ this._priorities[i + ROOT_INDEX] = priorities[i];
+ }
+ this.length = keys.length;
+ for (let i = keys.length >>> 1; i >= ROOT_INDEX; i--) {
+ this.bubbleDown(i);
+ }
+ }
+ get capacity() {
+ return this._capacity;
+ }
+ clear() {
+ this.length = 0;
+ this._hasPoppedElement = false;
+ }
+ bubbleUp(index) {
+ const key = this._keys[index];
+ const priority = this._priorities[index];
+ while (index > ROOT_INDEX) {
+ const parentIndex = index >>> 1;
+ if (this._priorities[parentIndex] <= priority) {
+ break;
+ }
+ this._keys[index] = this._keys[parentIndex];
+ this._priorities[index] = this._priorities[parentIndex];
+ index = parentIndex;
+ }
+ this._keys[index] = key;
+ this._priorities[index] = priority;
+ }
+ bubbleDown(index) {
+ const key = this._keys[index];
+ const priority = this._priorities[index];
+ const halfLength = ROOT_INDEX + (this.length >>> 1);
+ const lastIndex = this.length + ROOT_INDEX;
+ while (index < halfLength) {
+ const left = index << 1;
+ let childPriority = this._priorities[left];
+ let childKey = this._keys[left];
+ let childIndex = left;
+ const right = left + 1;
+ if (right < lastIndex) {
+ const rightPriority = this._priorities[right];
+ if (rightPriority < childPriority) {
+ childPriority = rightPriority;
+ childKey = this._keys[right];
+ childIndex = right;
+ }
+ }
+ if (childPriority >= priority) {
+ break;
+ }
+ this._keys[index] = childKey;
+ this._priorities[index] = childPriority;
+ index = childIndex;
+ }
+ this._keys[index] = key;
+ this._priorities[index] = priority;
+ }
+ push(key, priority) {
+ if (this.length === this._capacity) {
+ throw new Error("Heap has reached capacity, can't push new items");
+ }
+ if (this._hasPoppedElement) {
+ this._keys[ROOT_INDEX] = key;
+ this._priorities[ROOT_INDEX] = priority;
+ this.length++;
+ this.bubbleDown(ROOT_INDEX);
+ this._hasPoppedElement = false;
+ } else {
+ const pos = this.length + ROOT_INDEX;
+ this._keys[pos] = key;
+ this._priorities[pos] = priority;
+ this.length++;
+ this.bubbleUp(pos);
+ }
+ }
+ pop() {
+ if (this.length === 0) {
+ return void 0;
+ }
+ this.removePoppedElement();
+ this.length--;
+ this._hasPoppedElement = true;
+ return this._keys[ROOT_INDEX];
+ }
+ peekPriority() {
+ this.removePoppedElement();
+ return this._priorities[ROOT_INDEX];
+ }
+ peek() {
+ this.removePoppedElement();
+ return this._keys[ROOT_INDEX];
+ }
+ removePoppedElement() {
+ if (this._hasPoppedElement) {
+ this._keys[ROOT_INDEX] = this._keys[this.length + ROOT_INDEX];
+ this._priorities[ROOT_INDEX] = this._priorities[this.length + ROOT_INDEX];
+ this.bubbleDown(ROOT_INDEX);
+ this._hasPoppedElement = false;
+ }
+ }
+ get size() {
+ return this.length;
+ }
+ dumpRawPriorities() {
+ this.removePoppedElement();
+ const result = Array(this.length - ROOT_INDEX);
+ for (let i = 0; i < this.length; i++) {
+ result[i] = this._priorities[i + ROOT_INDEX];
+ }
+ return `[${result.join(" ")}]`;
+ }
+}
diff --git a/client/index.html b/client/index.html
index 369fe7f..97c8c3f 100644
--- a/client/index.html
+++ b/client/index.html
@@ -10,6 +10,8 @@
+
+
@@ -39,6 +41,8 @@
+
+
diff --git a/client/index.js b/client/index.js
index 250869f..317936b 100644
--- a/client/index.js
+++ b/client/index.js
@@ -8,15 +8,15 @@ 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://localhost/ws/',
- ping_url: 'https://localhost/api/ping',
- image_url: 'https://localhost/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: true,
+ debug_print: false,
min_zoom: 0.0000005,
max_zoom: 10.0,
initial_offline_timeout: 1000,
@@ -27,7 +27,7 @@ const config = {
initial_dynamic_bytes: 4096,
frametime_window_size: 100,
tile_size: 16,
- clip_zoom_threshold: 0.1,
+ clip_zoom_threshold: 0.00003,
};
const EVENT = Object.freeze({
@@ -164,6 +164,14 @@ function main() {
'queue': [],
'events': [],
'stroke_count': 0,
+ 'starting_index': 0,
+
+ 'bvh': {
+ 'nodes': [],
+ 'node_count': 0,
+ 'root': null,
+ 'pqueue': new MinQueue(1024),
+ },
'tools': {
'active': null,
@@ -190,6 +198,8 @@ function main() {
'limit_to': false,
'render_from': 0,
'render_to': 0,
+ 'force_clip_off': false,
+ 'draw_bvh': false,
}
};
diff --git a/client/math.js b/client/math.js
index ef82c92..5d108e5 100644
--- a/client/math.js
+++ b/client/math.js
@@ -182,25 +182,27 @@ function segment_interesects_quad(a, b, quad_topleft, quad_bottomright, quad_top
return false;
}
-function stroke_bbox(points) {
- let min_x = points[0].x;
- let max_x = min_x;
-
- let min_y = points[0].y;
- let max_y = min_y;
-
- for (const p of points) {
- min_x = Math.min(min_x, p.x);
- min_y = Math.min(min_y, p.y);
- max_x = Math.max(max_x, p.x);
- max_y = Math.max(max_y, p.y);
+function stroke_bbox(stroke) {
+ const radius = stroke.width / 2;
+
+ let min_x = stroke.points[0].x - radius;
+ let max_x = stroke.points[0].x + radius;
+
+ let min_y = stroke.points[0].y - radius;
+ let max_y = stroke.points[0].y + radius;
+
+ for (const p of stroke.points) {
+ min_x = Math.min(min_x, p.x - radius);
+ min_y = Math.min(min_y, p.y - radius);
+ max_x = Math.max(max_x, p.x + radius);
+ max_y = Math.max(max_y, p.y + radius);
}
- return {'x1': min_x, 'y1': min_y, 'x2': max_x, 'y2': max_y};
+ return {'x1': min_x, 'y1': min_y, 'x2': max_x, 'y2': max_y, 'cx': (max_x + min_x) / 2, 'cy': (max_y + min_y) / 2};
}
-function quad_onscreen(screen, bbox) {
- if (screen.x1 < bbox.x2 && screen.x2 > bbox.x1 && screen.y2 > bbox.y1 && screen.y1 < bbox.y2) {
+function quads_intersect(a, b) {
+ if (a.x1 < b.x2 && a.x2 > b.x1 && a.y2 > b.y1 && a.y1 < b.y2) {
return true;
}
@@ -215,6 +217,15 @@ function quad_fully_onscreen(screen, bbox) {
return false;
}
+function quad_union(a, b) {
+ return {
+ 'x1': Math.min(a.x1, b.x1),
+ 'y1': Math.min(a.y1, b.y1),
+ 'x2': Math.max(a.x2, b.x2),
+ 'y2': Math.max(a.y2, b.y2),
+ };
+}
+
function segments_onscreen(state, context, do_clip) {
// TODO: handle stroke width
@@ -222,7 +233,7 @@ function segments_onscreen(state, context, do_clip) {
let total_points = 0;
for (const event of state.events) {
- if (event.type === EVENT.STROKE && !event.deleted) {
+ if (event.type === EVENT.STROKE && !event.deleted && event.points.length > 0) {
total_points += event.points.length - 1;
}
}
@@ -256,25 +267,19 @@ function segments_onscreen(state, context, do_clip) {
const event = state.events[i];
if (!(state.debug.limit_from && i < state.debug.render_from)) {
- if (event.type === EVENT.STROKE && !event.deleted) {
- if (!do_clip || quad_onscreen(screen, event.bbox)) {
- const fully_onscreen = !do_clip || quad_fully_onscreen(screen, event.bbox);
+ if (event.type === EVENT.STROKE && !event.deleted && event.points.length > 0) {
+ if (!do_clip || quads_intersect(screen, event.bbox)) {
for (let j = 0; j < event.points.length - 1; ++j) {
- const a = event.points[j + 0];
- const b = event.points[j + 1];
-
- if (fully_onscreen || segment_interesects_quad(a, b, screen_topleft, screen_bottomright, screen_topright, screen_bottomleft)) {
- let base = head + j * 4;
- // We draw quads as [1, 2, 3, 4, 3, 2]
- state.onscreen_segments[at + 0] = base + 0;
- state.onscreen_segments[at + 1] = base + 1;
- state.onscreen_segments[at + 2] = base + 2;
- state.onscreen_segments[at + 3] = base + 3;
- state.onscreen_segments[at + 4] = base + 2;
- state.onscreen_segments[at + 5] = base + 1;
-
- at += 6;
- }
+ let base = head + j * 4;
+ // We draw quads as [1, 2, 3, 4, 3, 2]
+ state.onscreen_segments[at + 0] = base + 0;
+ state.onscreen_segments[at + 1] = base + 1;
+ state.onscreen_segments[at + 2] = base + 2;
+ state.onscreen_segments[at + 3] = base + 3;
+ state.onscreen_segments[at + 4] = base + 2;
+ state.onscreen_segments[at + 5] = base + 1;
+
+ at += 6;
}
}
}
diff --git a/client/webgl_draw.js b/client/webgl_draw.js
index 9679194..56183ce 100644
--- a/client/webgl_draw.js
+++ b/client/webgl_draw.js
@@ -57,7 +57,7 @@ function draw(state, context) {
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
let index_count;
- const do_clip = false; //(state.canvas.zoom > config.clip_zoom_threshold);
+ const do_clip = !state.debug.force_clip_off; //(state.canvas.zoom > config.clip_zoom_threshold);
if (do_clip) {
context.need_index_upload = true;
@@ -65,19 +65,19 @@ function draw(state, context) {
if (do_clip || context.need_index_upload) {
const before_clip = performance.now();
- index_count = segments_onscreen(state, context, do_clip);
+ index_count = bvh_clip(state, context);
const after_clip = performance.now();
}
if (!do_clip && !context.need_index_upload) {
index_count = context.full_index_count;
}
- //console.debug('clip', after_clip - before_clip);
+
document.getElementById('debug-stats').innerHTML = `
Segments onscreen: ${do_clip ? index_count : '-' }
Canvas offset: (${state.canvas.offset.x}, ${state.canvas.offset.y})
- Canvas zoom: ${Math.round(state.canvas.zoom * 100) / 100}`;
+ Canvas zoom: ${Math.round(state.canvas.zoom * 100000) / 100000}`;
if (index_count > 0) {
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers['b_packed_static_index']);
@@ -160,6 +160,46 @@ function draw(state, context) {
}
}
+ if (state.debug.draw_bvh) {
+ const points = new Float32Array(state.bvh.nodes.length * 6 * 2);
+
+ for (let i = 0; i < state.bvh.nodes.length; ++i) {
+ const box = state.bvh.nodes[i].bbox;
+
+ points[i * 12 + 0] = box.x1;
+ points[i * 12 + 1] = box.y1;
+ points[i * 12 + 2] = box.x2;
+ points[i * 12 + 3] = box.y1;
+ points[i * 12 + 4] = box.x1;
+ points[i * 12 + 5] = box.y2;
+
+ points[i * 12 + 6] = box.x2;
+ points[i * 12 + 7] = box.y2;
+ points[i * 12 + 8] = box.x1;
+ points[i * 12 + 9] = box.y2;
+ points[i * 12 + 10] = box.x2;
+ points[i * 12 + 11] = box.y1;
+ }
+
+ locations = context.locations['debug'];
+ buffers = context.buffers['debug'];
+
+ gl.useProgram(context.programs['debug']);
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_packed']);
+
+ gl.uniform2f(locations['u_res'], context.canvas.width, context.canvas.height);
+ gl.uniform2f(locations['u_scale'], state.canvas.zoom, state.canvas.zoom);
+ gl.uniform2f(locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y);
+
+ gl.enableVertexAttribArray(locations['a_pos']);
+ gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, 8, 0);
+
+ gl.clear(gl.DEPTH_BUFFER_BIT);
+
+ gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);
+ gl.drawArrays(gl.TRIANGLES, 0, points.length / 2);
+ }
+
/*
if (dynamic_points > 0) {
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_packed_dynamic']);
diff --git a/client/webgl_geometry.js b/client/webgl_geometry.js
index 94f7157..33fd2dc 100644
--- a/client/webgl_geometry.js
+++ b/client/webgl_geometry.js
@@ -97,10 +97,11 @@ function geometry_prepare_stroke(state) {
};
}
-function geometry_add_stroke(state, context, stroke, stroke_index) {
- if (!state.online || !stroke) return;
+function geometry_add_stroke(state, context, stroke, stroke_index, skip_bvh) {
+ if (!state.online || !stroke || stroke.points.length === 0) return;
- stroke.bbox = stroke_bbox(stroke.points);
+ stroke.bbox = stroke_bbox(stroke);
+ stroke.area = (stroke.bbox.x2 - stroke.bbox.x1) * (stroke.bbox.y2 - stroke.bbox.y1);
let bytes_left = context.static_serializer.size - context.static_serializer.offset;
let bytes_needed = stroke.points.length * 4 * config.bytes_per_point;
@@ -112,6 +113,8 @@ function geometry_add_stroke(state, context, stroke, stroke_index) {
}
push_stroke(context.static_serializer, stroke, stroke_index);
+ if (!skip_bvh) bvh_add_stroke(state.bvh, stroke_index, stroke);
+
context.need_static_upload = true;
}
diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js
index 00b0822..3d23714 100644
--- a/client/webgl_listeners.js
+++ b/client/webgl_listeners.js
@@ -26,23 +26,35 @@ function debug_panel_init(state, context) {
document.getElementById('debug-do-prepass').checked = state.debug.do_prepass;
document.getElementById('debug-limit-from').checked = state.debug.limit_from;
document.getElementById('debug-limit-to').checked = state.debug.limit_to;
+ document.getElementById('debug-force-clip-off').checked = state.debug.force_clip_off;
+ document.getElementById('debug-draw-bvh').checked = state.debug.draw_bvh;
- document.getElementById('debug-red').addEventListener('click', (e) => {
+ document.getElementById('debug-draw-bvh').addEventListener('change', (e) => {
+ state.debug.draw_bvh = e.target.checked;
+ schedule_draw(state, context);
+ });
+
+ document.getElementById('debug-force-clip-off').addEventListener('change', (e) => {
+ state.debug.force_clip_off = e.target.checked;
+ schedule_draw(state, context);
+ });
+
+ document.getElementById('debug-red').addEventListener('change', (e) => {
state.debug.red = e.target.checked;
schedule_draw(state, context);
});
- document.getElementById('debug-do-prepass').addEventListener('click', (e) => {
+ document.getElementById('debug-do-prepass').addEventListener('change', (e) => {
state.debug.do_prepass = e.target.checked;
schedule_draw(state, context);
});
- document.getElementById('debug-limit-from').addEventListener('click', (e) => {
+ document.getElementById('debug-limit-from').addEventListener('change', (e) => {
state.debug.limit_from = e.target.checked;
schedule_draw(state, context);
});
- document.getElementById('debug-limit-to').addEventListener('click', (e) => {
+ document.getElementById('debug-limit-to').addEventListener('change', (e) => {
state.debug.limit_to = e.target.checked;
schedule_draw(state, context);
});
diff --git a/client/webgl_shaders.js b/client/webgl_shaders.js
index bac05b2..6d8b696 100644
--- a/client/webgl_shaders.js
+++ b/client/webgl_shaders.js
@@ -1,3 +1,55 @@
+const simple_vs_src = `#version 300 es
+ in vec2 a_pos;
+
+ uniform vec2 u_scale;
+ uniform vec2 u_res;
+ uniform vec2 u_translation;
+
+ out vec2 v_uv;
+ flat out int v_quad_id;
+
+ void main() {
+ vec2 screen01 = (a_pos * u_scale + u_translation) / u_res;
+ vec2 screen02 = screen01 * 2.0;
+ screen02.y = 2.0 - screen02.y;
+
+ int vertex_index = gl_VertexID % 6;
+
+ if (vertex_index == 0) {
+ v_uv = vec2(0.0, 0.0);
+ } else if (vertex_index == 1 || vertex_index == 5) {
+ v_uv = vec2(1.0, 0.0);
+ } else if (vertex_index == 2 || vertex_index == 4) {
+ v_uv = vec2(0.0, 1.0);
+ } else {
+ v_uv = vec2(1.0, 1.0);
+ }
+
+ v_quad_id = gl_VertexID / 6;
+
+ gl_Position = vec4(screen02 - 1.0, 0.0, 1.0);
+ }
+`;
+
+const simple_fs_src = `#version 300 es
+ precision highp float;
+ in vec2 v_uv;
+ flat in int v_quad_id;
+ out vec4 FragColor;
+ void main() {
+ vec2 pixel = fwidth(v_uv);
+ vec2 border = 2.0 * pixel;
+
+ if (border.x <= v_uv.x && v_uv.x <= 1.0 - border.x && border.y <= v_uv.y && v_uv.y <= 1.0 - border.y) {
+ discard;
+ } else {
+ vec3 color = vec3(float(v_quad_id * 869363 % 255) / 255.0, float(v_quad_id * 278975 % 255) / 255.0, float(v_quad_id * 587286 % 255) / 255.0);
+ float alpha = 0.5;
+ FragColor = vec4(color * alpha, alpha);
+ }
+ }
+`;
+
const opaque_vs_src = `#version 300 es
in vec3 a_pos; // .z is radius
in vec4 a_line;
@@ -191,7 +243,6 @@ function init_webgl(state, context) {
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
- //gl.blendFunc(gl.SRC_ALPHA, gl.DST_ALPHA);
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.GEQUAL);
@@ -210,12 +261,24 @@ function init_webgl(state, context) {
const opaque_vs = create_shader(gl, gl.VERTEX_SHADER, opaque_vs_src);
const nop_fs = create_shader(gl, gl.FRAGMENT_SHADER, nop_fs_src);
+ const simple_vs = create_shader(gl, gl.VERTEX_SHADER, simple_vs_src);
+ const simple_fs = create_shader(gl, gl.FRAGMENT_SHADER, simple_fs_src);
+
context.programs['image'] = create_program(gl, quad_vs, quad_fs);
+ context.programs['debug'] = create_program(gl, simple_vs, simple_fs);
context.programs['sdf'] = {
'opaque': create_program(gl, opaque_vs, nop_fs),
'main': create_program(gl, sdf_vs, sdf_fs),
};
+ context.locations['debug'] = {
+ 'a_pos': gl.getAttribLocation(context.programs['debug'], 'a_pos'),
+
+ 'u_res': gl.getUniformLocation(context.programs['debug'], 'u_res'),
+ 'u_scale': gl.getUniformLocation(context.programs['debug'], 'u_scale'),
+ 'u_translation': gl.getUniformLocation(context.programs['debug'], 'u_translation'),
+ };
+
context.locations['sdf'] = {
'opaque': {
'a_pos': gl.getAttribLocation(context.programs['sdf'].opaque, 'a_pos'),
@@ -243,6 +306,10 @@ function init_webgl(state, context) {
}
};
+ context.buffers['debug'] = {
+ 'b_packed': gl.createBuffer(),
+ };
+
context.buffers['sdf'] = {
'b_packed_static': gl.createBuffer(),
'b_packed_dynamic': gl.createBuffer(),
diff --git a/client/websocket.js b/client/websocket.js
index 01c50ac..2b646e6 100644
--- a/client/websocket.js
+++ b/client/websocket.js
@@ -28,7 +28,7 @@ async function ws_connect(state, context, first_connect = false) {
}
}
} catch (e) {
- // console.log('Could not ping the server:', e);
+ console.log('Could not ping the server:', e);
}
state.timers.offline_toast = setTimeout(() => ws_connect(state, context, first_connect), config.ws_reconnect_timeout);
@@ -72,4 +72,4 @@ function on_close(state, context) {
function on_error(state, context) {
ws.close(state, context);
-}
\ No newline at end of file
+}
diff --git a/server/milton.js b/server/milton.js
index 6b1e6b3..bdb442a 100644
--- a/server/milton.js
+++ b/server/milton.js
@@ -8,12 +8,11 @@ let first_point_y = null;
function parse_and_insert_stroke(desk_id, line) {
const stroke_id = math.fast_random32();
- const points = new Float32Array(line.split(' ').filter(s => s.length > 0).map(i => parseFloat(i)));
+ const words = line.split(' ');
+ const width = parseInt(words.shift());
+ const points = new Float32Array(words.map(i => parseFloat(i)));
if (first_point_x === null) {
- first_point_x = 1;
- first_point_y = 1;
- } else if (first_point_x === 1) {
first_point_x = points[0];
first_point_y = points[1];
}
@@ -23,9 +22,8 @@ function parse_and_insert_stroke(desk_id, line) {
points[i + 1] -= first_point_y;
}
- storage.queries.insert_stroke.run({
- '$id': stroke_id,
- '$width': 8, // possibly handle pressure here if we ever support it
+ const stroke_res = storage.queries.insert_stroke.get({
+ '$width': width,
'$color': 0,
'$points': points
});
@@ -34,7 +32,7 @@ function parse_and_insert_stroke(desk_id, line) {
'$type': EVENT.STROKE,
'$desk_id': desk_id,
'$session_id': 0,
- '$stroke_id': stroke_id,
+ '$stroke_id': stroke_res.id,
'$image_id': 0,
'$x': 0,
'$y': 0,
@@ -44,7 +42,7 @@ function parse_and_insert_stroke(desk_id, line) {
async function import_milton_file_to_sqlite(fullpath) {
storage.startup();
- const desk_id = 542; // math.fast_random32();
+ const desk_id = 9881; // math.fast_random32();
console.log(`Importing ${fullpath} into desk ${desk_id}`);
@@ -62,7 +60,7 @@ async function import_milton_file_to_sqlite(fullpath) {
parse_and_insert_stroke(desk_id, input_lines[i]);
}
- consoe.log(`Finished importing desk ${desk_id}`);
+ console.log(`Finished importing desk ${desk_id}`);
}
-import_milton_file_to_sqlite("/home/aolo2/desk2/server/points.txt");
+import_milton_file_to_sqlite("/code/desk2/server/points.txt");
diff --git a/server/storage.js b/server/storage.js
index 23fa0da..ba9c274 100644
--- a/server/storage.js
+++ b/server/storage.js
@@ -8,7 +8,7 @@ export const sessions = {};
export const desks = {};
export const queries = {};
-let db = null;
+export let db = null;
export function startup() {
const path = `${config.DATADIR}/db.sqlite`;
@@ -69,7 +69,7 @@ export function startup() {
// INSERT
queries.insert_desk = db.query('INSERT INTO desks (id, title, sn) VALUES ($id, $title, 0)');
- queries.insert_stroke = db.query('INSERT INTO strokes (id, width, color, points) VALUES ($id, $width, $color, $points)');
+ queries.insert_stroke = db.query('INSERT INTO strokes (id, width, color, points) VALUES ($id, $width, $color, $points) RETURNING id');
queries.insert_session = db.query('INSERT INTO sessions (id, desk_id, lsn) VALUES ($id, $desk_id, 0)');
queries.insert_event = db.query('INSERT INTO events (type, desk_id, session_id, stroke_id, image_id, x, y) VALUES ($type, $desk_id, $session_id, $stroke_id, $image_id, $x, $y)');