diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AddTldrawShapeWhiteboardRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AddTldrawShapeWhiteboardRecordEvent.scala new file mode 100755 index 0000000000..cdba60459d --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/AddTldrawShapeWhiteboardRecordEvent.scala @@ -0,0 +1,65 @@ +/** + * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ + * + * Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below). + * + * This program is free software; you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation; either version 3.0 of the License, or (at your option) any later + * version. + * + * BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with BigBlueButton; if not, see . + * + */ + +package org.bigbluebutton.core.record.events + +import org.bigbluebutton.common2.domain.SimpleVoteOutVO +import scala.collection.immutable.List +import scala.collection.Map +import scala.collection.mutable.ArrayBuffer +import spray.json._ +import DefaultJsonProtocol._ + +class AddTldrawShapeWhiteboardRecordEvent extends AbstractWhiteboardRecordEvent { + import AddTldrawShapeWhiteboardRecordEvent._ + + implicit object AnyJsonFormat extends JsonFormat[Any] { + def write(x: Any) = x match { + case n: Int => JsNumber(n) + case s: String => JsString(s) + case d: Double => JsNumber(d) + case m: scala.collection.immutable.Map[String, _] => mapFormat[String, Any].write(m) + case l: List[_] => listFormat[Any].write(l) + case b: Boolean if b == true => JsTrue + case b: Boolean if b == false => JsFalse + } + + def read(value: JsValue) = {} + } + + setEvent("AddTldrawShapeEvent") + + def setUserId(id: String) { + eventMap.put(USER_ID, id) + } + + def setAnnotationId(id: String) { + eventMap.put(SHAPE_ID, id) + } + + def addAnnotation(annotation: scala.collection.immutable.Map[String, Any]) { + eventMap.put(SHAPE_DATA, annotation.toJson.compactPrint) + } +} + +object AddTldrawShapeWhiteboardRecordEvent { + protected final val USER_ID = "userId" + protected final val SHAPE_ID = "shapeId" + protected final val SHAPE_DATA = "shapeData" +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/DeleteTldrawShapeRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/DeleteTldrawShapeRecordEvent.scala new file mode 100755 index 0000000000..4322e03895 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/DeleteTldrawShapeRecordEvent.scala @@ -0,0 +1,39 @@ +/** + * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ + * + * Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below). + * + * This program is free software; you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation; either version 3.0 of the License, or (at your option) any later + * version. + * + * BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with BigBlueButton; if not, see . + * + */ + +package org.bigbluebutton.core.record.events + +class DeleteTldrawShapeRecordEvent extends AbstractWhiteboardRecordEvent { + import DeleteTldrawShapeRecordEvent._ + + setEvent("DeleteTldrawShapeEvent") + + def setUserId(userId: String) { + eventMap.put(USER_ID, userId) + } + + def setShapeId(shapeId: String) { + eventMap.put(SHAPE_ID, shapeId) + } +} + +object DeleteTldrawShapeRecordEvent { + protected final val USER_ID = "userId" + protected final val SHAPE_ID = "shapeId" +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/ResizeAndMoveSlideRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/ResizeAndMoveSlideRecordEvent.scala index 1797a89ada..0905fd5d8a 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/ResizeAndMoveSlideRecordEvent.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/ResizeAndMoveSlideRecordEvent.scala @@ -32,23 +32,28 @@ class ResizeAndMoveSlideRecordEvent extends AbstractPresentationRecordEvent { eventMap.put(ID, id) } - def setXCamera(xCamera: Double) { - eventMap.put(X_CAMERA, xCamera.toString) + def setXOffset(offset: Double) { + eventMap.put(X_OFFSET, offset.toString) } - def setYCamera(yCamera: Double) { - eventMap.put(Y_CAMERA, yCamera.toString) + def setYOffset(offset: Double) { + eventMap.put(Y_OFFSET, offset.toString) } - def setZoom(zoom: Double) { - eventMap.put(ZOOM, zoom.toString) + def setWidthRatio(ratio: Double) { + eventMap.put(WIDTH_RATIO, ratio.toString) + } + + def setHeightRatio(ratio: Double) { + eventMap.put(HEIGHT_RATIO, ratio.toString) } } object ResizeAndMoveSlideRecordEvent { protected final val PRES_NAME = "presentationName" protected final val ID = "id" - protected final val X_CAMERA = "xCamera" - protected final val Y_CAMERA = "yCamera" - protected final val ZOOM = "zoom" + protected final val X_OFFSET = "xOffset" + protected final val Y_OFFSET = "yOffset" + protected final val WIDTH_RATIO = "widthRatio" + protected final val HEIGHT_RATIO = "heightRatio" } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/TldrawCameraChangedRecordEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/TldrawCameraChangedRecordEvent.scala new file mode 100755 index 0000000000..f5ea3deba8 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/TldrawCameraChangedRecordEvent.scala @@ -0,0 +1,54 @@ +/** + * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ + * + * Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below). + * + * This program is free software; you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation; either version 3.0 of the License, or (at your option) any later + * version. + * + * BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along + * with BigBlueButton; if not, see . + * + */ + +package org.bigbluebutton.core.record.events + +class TldrawCameraChangedRecordEvent extends AbstractPresentationRecordEvent { + import TldrawCameraChangedRecordEvent._ + + setEvent("TldrawCameraChangedEvent") + + def setPresentationName(name: String) { + eventMap.put(PRES_NAME, name) + } + + def setId(id: String) { + eventMap.put(ID, id) + } + + def setXCamera(xCamera: Double) { + eventMap.put(X_CAMERA, xCamera.toString) + } + + def setYCamera(yCamera: Double) { + eventMap.put(Y_CAMERA, yCamera.toString) + } + + def setZoom(zoom: Double) { + eventMap.put(ZOOM, zoom.toString) + } +} + +object TldrawCameraChangedRecordEvent { + protected final val PRES_NAME = "presentationName" + protected final val ID = "id" + protected final val X_CAMERA = "xCamera" + protected final val Y_CAMERA = "yCamera" + protected final val ZOOM = "zoom" +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala index 0954bed385..04097fc8ab 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala @@ -184,7 +184,7 @@ class RedisRecorderActor( } private def handleResizeAndMovePageEvtMsg(msg: ResizeAndMovePageEvtMsg) { - val ev = new ResizeAndMoveSlideRecordEvent() + val ev = new TldrawCameraChangedRecordEvent() ev.setMeetingId(msg.header.meetingId) ev.setPodId(msg.body.podId) ev.setPresentationName(msg.body.presentationId) @@ -280,7 +280,7 @@ class RedisRecorderActor( private def handleSendWhiteboardAnnotationsEvtMsg(msg: SendWhiteboardAnnotationsEvtMsg) { msg.body.annotations.foreach(annotation => { - val ev = new AddShapeWhiteboardRecordEvent() + val ev = new AddTldrawShapeWhiteboardRecordEvent() ev.setMeetingId(msg.header.meetingId) ev.setPresentation(getPresentationId(annotation.wbId)) ev.setPageNumber(getPageNum(annotation.wbId)) @@ -320,7 +320,7 @@ class RedisRecorderActor( private def handleDeleteWhiteboardAnnotationsEvtMsg(msg: DeleteWhiteboardAnnotationsEvtMsg) { msg.body.annotationsIds.foreach(annotationId => { - val ev = new UndoAnnotationRecordEvent() + val ev = new DeleteTldrawShapeRecordEvent() ev.setMeetingId(msg.header.meetingId) ev.setPresentation(getPresentationId(msg.body.whiteboardId)) ev.setPageNumber(getPageNum(msg.body.whiteboardId)) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/cursors/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/cursors/component.jsx index 387685915e..8aaf70dd0a 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/cursors/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/cursors/component.jsx @@ -1,6 +1,5 @@ import * as React from "react"; import ReactCursorPosition from "react-cursor-position"; -import Vec from "@tldraw/vec"; import { _ } from "lodash"; function usePrevious(value) { @@ -79,7 +78,7 @@ const PositionLabel = (props) => { whiteboardId, } = props; - const { name, color, userId, presenter } = currentUser; + const { name, color } = currentUser; const prevCurrentPoint = usePrevious(currentPoint); React.useEffect(() => { @@ -128,8 +127,8 @@ export default function Cursors(props) { !cursorWrapper.hasOwnProperty("mouseleave") && cursorWrapper?.addEventListener("mouseleave", (event) => { publishCursorUpdate({ - xPercent: null, - yPercent: null, + xPercent: -1.0, + yPercent: -1.0, whiteboardId: whiteboardId, }); setActive(false); diff --git a/record-and-playback/core/lib/recordandplayback/generators/events.rb b/record-and-playback/core/lib/recordandplayback/generators/events.rb index 97beb67cdf..e145cf3972 100755 --- a/record-and-playback/core/lib/recordandplayback/generators/events.rb +++ b/record-and-playback/core/lib/recordandplayback/generators/events.rb @@ -960,7 +960,7 @@ module BigBlueButton # The following events are considered to indicate that the presentation # area was actively used during the session. when 'AddShapeEvent', 'ModifyTextEvent', 'UndoShapeEvent', - 'ClearPageEvent' + 'ClearPageEvent', 'AddTldrawShapeEvent', 'DeleteTldrawShapeEvent' BigBlueButton.logger.debug("Seen a #{event['eventname']} event, presentation area used.") return true # We ignore the first SharePresentationEvent, since it's the default @@ -1093,5 +1093,11 @@ module BigBlueButton return false end + # Check if doc has tldraw events + def self.check_for_tldraw_events(events) + return !(events.xpath("recording/event[@eventname='TldrawCameraChangedEvent']").empty? && + events.xpath("recording/event[@eventname='AddTldrawShapeEvent']").empty?) + end + end -end \ No newline at end of file +end diff --git a/record-and-playback/presentation/scripts/process/presentation.rb b/record-and-playback/presentation/scripts/process/presentation.rb index aa64702af2..d0b0630b1b 100755 --- a/record-and-playback/presentation/scripts/process/presentation.rb +++ b/record-and-playback/presentation/scripts/process/presentation.rb @@ -192,6 +192,10 @@ unless FileTest.directory?(target_dir) # Copy thumbnails from raw files FileUtils.cp_r("#{pres_dir}/thumbnails", "#{target_pres_dir}/thumbnails") if File.exist?("#{pres_dir}/thumbnails") + tldraw = BigBlueButton::Events.check_for_tldraw_events(@doc); + if (tldraw) + FileUtils.cp_r("#{pres_dir}/svgs", "#{target_pres_dir}/svgs") if File.exist?("#{pres_dir}/svgs") + end end BigBlueButton.logger.info('Generating closed captions') diff --git a/record-and-playback/presentation/scripts/publish/presentation.rb b/record-and-playback/presentation/scripts/publish/presentation.rb index 38ad1049e5..503f64e6a0 100755 --- a/record-and-playback/presentation/scripts/publish/presentation.rb +++ b/record-and-playback/presentation/scripts/publish/presentation.rb @@ -382,6 +382,30 @@ def svg_render_shape_poll(g, slide, shape) width: width, height: height, x: x, y: y) end +def build_tldraw_shape(image_shapes, slide, shape) + shape_in = shape[:in] + shape_out = shape[:out] + + if shape_in == shape_out + BigBlueButton.logger.info("Draw #{shape[:shape_id]} Shape #{shape[:shape_unique_id]} is never shown (duration rounds to 0)") + return + end + + if (shape_in >= slide[:out]) || (!shape[:out].nil? && shape[:out] <= slide[:in]) + BigBlueButton.logger.info("Draw #{shape[:shape_id]} Shape #{shape[:shape_unique_id]} is not visible during image time span") + return + end + + tldraw_shape = { + id: shape[:shape_id], + timestamp: shape_in, + undo: (shape[:undo].nil? ? -1 : shape[:undo]), + shape_data: shape[:shape_data] + } + + image_shapes.push(tldraw_shape) +end + def svg_render_shape(canvas, slide, shape, image_id) shape_in = shape[:in] shape_out = shape[:out] @@ -426,7 +450,7 @@ def svg_render_shape(canvas, slide, shape, image_id) end @svg_image_id = 1 -def svg_render_image(svg, slide, shapes) +def svg_render_image(svg, slide, shapes, tldraw, tldraw_shapes) slide_number = slide[:slide] presentation = slide[:presentation] slide_in = slide[:in] @@ -458,15 +482,25 @@ def svg_render_image(svg, slide, shapes) shapes = shapes[presentation][slide_number] - canvas = doc.create_element('g', - class: 'canvas', id: "canvas#{image_id}", - image: "image#{image_id}", display: 'none') + if !tldraw + canvas = doc.create_element('g', + class: 'canvas', id: "canvas#{image_id}", + image: "image#{image_id}", display: 'none') - shapes.each do |shape| - svg_render_shape(canvas, slide, shape, image_id) + shapes.each do |shape| + svg_render_shape(canvas, slide, shape, image_id) + end + + svg << canvas unless canvas.element_children.empty? + else + image_shapes = [] + + shapes.each do |shape| + build_tldraw_shape(image_shapes, slide, shape) + end + + tldraw_shapes[image_id] = { :shapes=>image_shapes, :timestamp=> slide_in} end - - svg << canvas unless canvas.element_children.empty? end def panzoom_viewbox(panzoom) @@ -483,13 +517,19 @@ def panzoom_viewbox(panzoom) [x, y, w, h] end -def panzooms_emit_event(rec, panzoom) +def panzooms_emit_event(rec, panzoom, tldraw) panzoom_in = panzoom[:in] return if panzoom_in == panzoom[:out] - rec.event(timestamp: panzoom_in) do - x, y, w, h = panzoom_viewbox(panzoom) - rec.viewBox("#{x} #{y} #{w} #{h}") + if !tldraw + rec.event(timestamp: panzoom_in) do + x, y, w, h = panzoom_viewbox(panzoom) + rec.viewBox("#{x} #{y} #{w} #{h}") + end + else + rec.event(timestamp: panzoom_in) do + rec.cameraAndZoom("#{panzoom[:x_camera]} #{panzoom[:y_camera]} #{panzoom[:zoom]}") + end end end @@ -497,14 +537,14 @@ def convert_cursor_coordinate(cursor_coord, panzoom_offset, panzoom_ratio) (((cursor_coord / 100.0) + (panzoom_offset * MAGIC_MYSTERY_NUMBER / 100.0)) / (panzoom_ratio / 100.0)).round(5) end -def cursors_emit_event(rec, cursor) +def cursors_emit_event(rec, cursor, tldraw) cursor_in = cursor[:in] return if cursor_in == cursor[:out] rec.event(timestamp: cursor_in) do panzoom = cursor[:panzoom] if cursor[:visible] - if @version_atleast_2_0_0 + if @version_atleast_2_0_0 && !tldraw # In BBB 2.0, the cursor now uses the same coordinate system as annotations # Use the panzoom information to convert it to be relative to viewbox x = convert_cursor_coordinate(cursor[:x], panzoom[:x_offset], panzoom[:width_ratio]) @@ -535,6 +575,53 @@ def determine_slide_number(slide, current_slide) slide end +def events_parse_tldraw_shape(shapes, event, current_presentation, current_slide, timestamp) + presentation = event.at_xpath('presentation') + slide = event.at_xpath('pageNumber') + + presentation = determine_presentation(presentation, current_presentation) + slide = determine_slide_number(slide, current_slide) + + # Set up the shapes data structures if needed + shapes[presentation] ||= {} + shapes[presentation][slide] ||= [] + + # We only need to deal with shapes for this slide + shapes = shapes[presentation][slide] + + # Set up the structure for this shape + shape = {} + # Common properties + shape[:in] = timestamp + shape_data = shape[:shape_data] = JSON.parse(event.at_xpath('shapeData')) + + user_id = event.at_xpath('userId')&.text + shape[:user_id] = user_id if user_id + + shape_id = event.at_xpath('shapeId')&.text + shape[:id] = shape_id if shape_id + + draw_id = shape[:shape_id] = @svg_shape_id + @svg_shape_id += 1 + + # Find the previous shape, for updates + prev_shape = nil + if shape_id + # If we have a shape ID, look up the previous shape by ID + prev_shape_pos = shapes.rindex { |s| s[:shade_id] == shape_id } + prev_shape = prev_shape_pos ? shapes[prev_shape_pos] : nil + end + if prev_shape + prev_shape[:out] = timestamp + shape[:shape_unique_id] = prev_shape[:shape_unique_id] + else + shape[:shape_unique_id] = @svg_shape_unique_id + @svg_shape_unique_id += 1 + end + + shapes << shape +end + def events_parse_shape(shapes, event, current_presentation, current_slide, timestamp) # Figure out what presentation+slide this shape is for, with fallbacks # for old BBB where this info isn't in the shape messages @@ -734,7 +821,7 @@ def events_parse_clear(shapes, event, current_presentation, current_slide, times end end -def events_get_image_info(slide) +def events_get_image_info(slide, tldraw) slide_deskshare = slide[:deskshare] slide_presentation = slide[:presentation] @@ -744,7 +831,8 @@ def events_get_image_info(slide) slide[:src] = 'presentation/logo.png' else slide_nr = slide[:slide] + 1 - slide[:src] = "presentation/#{slide_presentation}/slide-#{slide_nr}.png" + tldraw ? slide[:src] = "presentation/#{slide_presentation}/svgs/slide#{slide_nr}.svg" + : slide[:src] = "presentation/#{slide_presentation}/slide-#{slide_nr}.png" slide[:text] = "presentation/#{slide_presentation}/textfiles/slide-#{slide_nr}.txt" end image_path = "#{@process_dir}/#{slide[:src]}" @@ -792,6 +880,7 @@ def process_presentation(package_dir) # Current pan/zoom state current_x_offset = current_y_offset = 0.0 current_width_ratio = current_height_ratio = 100.0 + current_x_camera = current_y_camera = current_zoom = 0.0 # Current cursor status cursor_x = cursor_y = -1.0 cursor_visible = false @@ -802,6 +891,8 @@ def process_presentation(package_dir) panzooms = [] cursors = [] shapes = {} + tldraw = BigBlueButton::Events.check_for_tldraw_events(@doc) + tldraw_shapes = {} # Iterate through the events.xml and store the events, building the # xml files as we go @@ -836,6 +927,12 @@ def process_presentation(package_dir) current_height_ratio = event.at_xpath('heightRatio').text.to_f panzoom_changed = true + when 'TldrawCameraChangedEvent' + current_x_camera = event.at_xpath('xCamera').text.to_f + current_y_camera = event.at_xpath('yCamera').text.to_f + current_zoom = event.at_xpath('zoom').text.to_f + panzoom_changed = true + when 'DeskshareStartedEvent', 'StartWebRTCDesktopShareEvent' deskshare = slide_changed = true if @presentation_props['include_deskshare'] @@ -848,7 +945,10 @@ def process_presentation(package_dir) when 'AddShapeEvent', 'ModifyTextEvent' events_parse_shape(shapes, event, current_presentation, current_slide, timestamp) - when 'UndoShapeEvent', 'UndoAnnotationEvent' + when 'AddTldrawShapeEvent' + events_parse_tldraw_shape(shapes, event, current_presentation, current_slide, timestamp) + + when 'UndoShapeEvent', 'UndoAnnotationEvent', 'DeleteTldrawShapeEvent' events_parse_undo(shapes, event, current_presentation, current_slide, timestamp) when 'ClearPageEvent', 'ClearWhiteboardEvent' @@ -887,7 +987,7 @@ def process_presentation(package_dir) else if slide slide[:out] = timestamp - svg_render_image(svg, slide, shapes) + svg_render_image(svg, slide, shapes, tldraw, tldraw_shapes) end BigBlueButton.logger.info("Presentation #{current_presentation} Slide #{current_slide} Deskshare #{deskshare}") @@ -897,7 +997,7 @@ def process_presentation(package_dir) in: timestamp, deskshare: deskshare, } - events_get_image_info(slide) + events_get_image_info(slide, tldraw) slides << slide end end @@ -909,40 +1009,59 @@ def process_presentation(package_dir) slide_width = slide[:width] slide_height = slide[:height] if panzoom && + (panzoom[:deskshare] == deskshare) && + ((!tldraw && (panzoom[:x_offset] == current_x_offset) && (panzoom[:y_offset] == current_y_offset) && (panzoom[:width_ratio] == current_width_ratio) && (panzoom[:height_ratio] == current_height_ratio) && (panzoom[:width] == slide_width) && - (panzoom[:height] == slide_height) && - (panzoom[:deskshare] == deskshare) + (panzoom[:height] == slide_height)) || + (tldraw && + (panzoom[:x_camera] == current_x_camera) && + (panzoom[:y_camera] == current_y_camera) && + (panzoom[:zoom] == current_zoom)) + ) BigBlueButton.logger.info('Panzoom: skipping, no changes') panzoom_changed = false else if panzoom panzoom[:out] = timestamp - panzooms_emit_event(panzooms_rec, panzoom) + panzooms_emit_event(panzooms_rec, panzoom, tldraw) + end + if !tldraw + BigBlueButton.logger.info("Panzoom: #{current_x_offset} #{current_y_offset} #{current_width_ratio} #{current_height_ratio} (#{slide_width}x#{slide_height})") + panzoom = { + x_offset: current_x_offset, + y_offset: current_y_offset, + width_ratio: current_width_ratio, + height_ratio: current_height_ratio, + width: slide[:width], + height: slide[:height], + in: timestamp, + deskshare: deskshare, + } + else + BigBlueButton.logger.info("Panzoom: #{current_x_camera} #{current_y_camera} #{current_zoom} (#{slide_width}x#{slide_height})") + panzoom = { + x_camera: current_x_camera, + y_camera: current_y_camera, + zoom: current_zoom, + in: timestamp, + deskshare: deskshare, + } end - BigBlueButton.logger.info("Panzoom: #{current_x_offset} #{current_y_offset} #{current_width_ratio} #{current_height_ratio} (#{slide_width}x#{slide_height})") - panzoom = { - x_offset: current_x_offset, - y_offset: current_y_offset, - width_ratio: current_width_ratio, - height_ratio: current_height_ratio, - width: slide[:width], - height: slide[:height], - in: timestamp, - deskshare: deskshare, - } panzooms << panzoom end end # Perform cursor finalization if cursor_changed || panzoom_changed - unless cursor_x >= 0 && cursor_x <= 100 && - cursor_y >= 0 && cursor_y <= 100 - cursor_visible = false + if !tldraw + unless cursor_x >= 0 && cursor_x <= 100 && + cursor_y >= 0 && cursor_y <= 100 + cursor_visible = false + end end panzoom = panzooms.last @@ -955,7 +1074,7 @@ def process_presentation(package_dir) else if cursor cursor[:out] = timestamp - cursors_emit_event(cursors_rec, cursor) + cursors_emit_event(cursors_rec, cursor, tldraw) end cursor = { visible: cursor_visible, @@ -972,26 +1091,27 @@ def process_presentation(package_dir) # Add the last slide, panzoom, and cursor slide = slides.last slide[:out] = last_timestamp - svg_render_image(svg, slide, shapes) + svg_render_image(svg, slide, shapes, tldraw, tldraw_shapes) panzoom = panzooms.last panzoom[:out] = last_timestamp - panzooms_emit_event(panzooms_rec, panzoom) + panzooms_emit_event(panzooms_rec, panzoom, tldraw) cursor = cursors.last cursor[:out] = last_timestamp - cursors_emit_event(cursors_rec, cursor) + cursors_emit_event(cursors_rec, cursor, tldraw) cursors_doc = Builder::XmlMarkup.new(indent: 2) cursors_doc.instruct! - cursors_doc.recording(id: 'cursor_events') { |xml| xml << cursors_rec.target! } + cursors_doc.recording(id: 'cursor_events', tldraw: tldraw) { |xml| xml << cursors_rec.target! } panzooms_doc = Builder::XmlMarkup.new(indent: 2) panzooms_doc.instruct! - panzooms_doc.recording(id: 'panzoom_events') { |xml| xml << panzooms_rec.target! } + panzooms_doc.recording(id: 'panzoom_events', tldraw: tldraw) { |xml| xml << panzooms_rec.target! } # And save the result File.write("#{package_dir}/#{@shapes_svg_filename}", shapes_doc.to_xml) File.write("#{package_dir}/#{@panzooms_xml_filename}", panzooms_doc.target!) File.write("#{package_dir}/#{@cursor_xml_filename}", cursors_doc.target!) + generate_json_file(package_dir, @tldraw_shapes_filename, tldraw_shapes) if tldraw end def process_chat_messages(events, bbb_props) @@ -1170,6 +1290,7 @@ end @panzooms_xml_filename = 'panzooms.xml' @cursor_xml_filename = 'cursor.xml' @deskshare_xml_filename = 'deskshare.xml' +@tldraw_shapes_filename = 'tldraw.json' @svg_shape_id = 1 @svg_shape_unique_id = 1