Skip to content

Dev3 laser keepupdating - #244

Open
hiroshisuga wants to merge 111 commits into
v3.0.x-releasefrom
dev3_laser_keepupd
Open

hiroshisuga wants to merge 111 commits into
v3.0.x-releasefrom
dev3_laser_keepupd

Conversation

@hiroshisuga

@hiroshisuga hiroshisuga commented Mar 27, 2026

Copy link
Copy Markdown
Owner

[Laser pointer implementation]

Motivation

  • Color blind people (gradient and green color is better)
  • Larger (or can be larger)
  • Hand tool is most frequently used for univ lecture, zooming and panning the slides, and right clicking is empty for the hand tool (easy implementation)
  • Easy selection of color and size from a pulldown menu (profs do not have time to change setting.yml and execute bbb-conf --restart)
  • Cursor size should not change even with zoomed slide.
  • Laser size has to be customized, with two alternative slots (not more!), colors can also be configurable.
  • has to be compatible with Fix(html5): Make the cursor point respect the pointerDiameter setting bigbluebutton/bigbluebutton#24595
  • For Android it works, but no laser (actually any other cursors) shown as a presenter although laser visible in the viewer's screen
  • For iPad it starts work. Menu appears by a long tap.
  • For mobile devices, the presenter's laser is as smooth now as on PCs (Previously it was slow as I used the viewer's cursor position).

(Aug 2026)

  • Replaced hard-coded red/green laser colors with a configurable laserColors array in settings.yml.
  • Chose red and blue-green as the default colors for better color accessibility.
  • Refactored laserDefs so laser variants are generated dynamically from the configured colors and small/large radii.
  • Updated the laser selection menu to display the actual laser SVG graphics, including their real colors and sizes, instead of emoji/special-font symbols. Default laser color became more accessible.
  • Replaced the hand emoji with BBB’s built-in hand icon for the normal pan mode.
  • Remove "collaborator cursor hint" pointer that emerges when the cursor moves on the right or bottom edges of slide
  • on viewer's screen, now laser vanishes when the presenter turns it off.
  • Quick update of laser size on the viewer's screen by watching it by useEffect
  • remove targetWin to make the PR more portable; less effort for merging to the popup PR
  • massive improvement for the gestures on touch panel
  • the fit-to-width and the infinite WB functions now properly working.

To do and problems

  • Recording not implemented, although events are written in the events.xml file.
  • On Windows or Linux the laser darkens (actually not so bad). This appears to be a platform-specific rendering limitation or bug in the Chrome/Windows custom-cursor path, rather than a problem with the laser SVG itself. This Chromium issue might be related: https://issues.chromium.org/issues/41275905
  • the hand icon on the toolbar has to be modified (for a better accessibility) so that the user easily knows where to turn the laser on
  • Not working for a presenter on iOS (as a viewer there is no problem)
  • akka-apps does not start properly (killed gracefully). bbb-conf --restart sometimes does not work for that reason. -> this is not the problem of the source code. By apt install --reinstall bbb-apps-akka can recover the problem (or simply, sudo systemctl start bbb-apps-akka) : the point is to execute: ln -s /lib/systemd/system/bbb-apps-akka.service /etc/systemd/system/multi-user.target.wants/bbb-apps-akka.service , and ln -s /lib/systemd/system/bbb-apps-akka.service /etc/systemd/system/bigbluebutton.target.wants/bbb-apps-akka.service , which are not included in the deploy.sh in akka-bbb-apps source code. -> fix(akka-apps): [development] Correctly start bbb-apps-akka by creating symlinks bigbluebutton/bigbluebutton#25372 fixed the problem
  • bbb-graphql-client-test not modified yet.
  • The halo of viewer's laser turns black in the dark mode on Google Chrome. Safari is fine. Presenter's laser is also fine both for the presenter and for the viewer. I do not fix it for now because the dark-mode is a browser-dependent extension.

To use this PR with the popup PR, you need:

  • to change -> "const targetDoc = popupWindow?.document || document;" (at many places where "const targetDoc = document"; is used) "const targetDoc = getWhiteboardDocument();" -> not necessary anymore after a7de71a
  • to add-> "popupWindow" at the ends of two React.useEffect()s which defines "const targetDoc" (same functions above) (when refactoring on Apr 26, it turned out that they need not to be changed)
  • This commit (1b46718) is dependent on [3.0 whiteboard] fix: prevent pinch zoom outside allowed range bigbluebutton/bigbluebutton#25671 ). Without this commit, "isTouchZoomRef" would not be defined. If [3.0 whiteboard] fix: prevent pinch zoom outside allowed range bigbluebutton/bigbluebutton#25671 is not merged, I need to backport it independently. For the backporting, all except the lines from "const zoomed..." to "if (next.z < minimum....}}}" in component.jsx should be simply copied, namely, Dev3 subset of restricted pinch zoom PR - keep updated #287.
  • To test a different bbb-html5 code with the graphQL background of this PR, you will have the error "laserType is required by GraphQL". To avoid this, you have to set false for laserType in bbb-graphql-actions/src/actions/presentationPublishCursor.ts -> done, I may need to set an empty string as default when lasertype is not specified.
  • To merge this PR to the popup PR, I need to check the order of the arguments of useCursor in the hooks.js and component.jsx to be the same. This may not be warned by the merging conflict check of Github.

@github-actions

Copy link
Copy Markdown

Automated tests Summary

🚨 Test workflow has failed


Click here to check the action test reports (to view the report locally, see the docs)

@hiroshisuga hiroshisuga changed the title Dev3 laser keepupd Dev3 laser keepupdating Mar 27, 2026
Remove 
<svg class="tl-collaborator__cursor-hint">
that appears typically when the cursor moves near the right or the bottom edge of the viewer's screen.
We do not need it when the laser is shown.
This change made the PR portable and easily mergeable to the popup PR.
Previously a pinching gesture is mis-recognized as a long touch, giving rise to the laser selector popup.
「2本目のタッチをtldrawが処理する前にcamera操作を有効化するため、gesture開始の判定には pointerdown を使用する。一方、multi-touch中に canMoveCamera を切り替えるとtldrawのpinch状態と競合するため、gesture終了は touchend で全指が離れた場合のみとし、それまではcamera状態を維持する。」

今回の修正は、もともと「レーザーポインタ使用中のタッチ操作」を通常の Hand tool と同じように tldraw に任せていたことから始まっています。

通常の Hand tool では、タッチスクリーン上で1本指をドラッグするとキャンバスがパンします。しかしレーザー使用中は、1本指はレーザーを動かすために使いたいため、期待する操作は次のようになります。

1本指:レーザーを動かす。キャンバスはパンしない
2本指:通常どおりパン/ピンチズームする
2本指操作が終わった後:残った1本指を新しいレーザー操作として誤認しない

このため、tldraw の canMoveCamera をタッチ本数に応じて切り替えることにしました。ただし、ここでいくつか問題がありました。

最初は touchstart を使って、

1本 → canMoveCamera = false
2本 → canMoveCamera = true

としていました。しかしこれは、2本目の指が触れたときに canMoveCamera を有効化するタイミングが遅すぎました。

タッチ端末では概念的に、

2本目の pointerdown
↓
tldraw が pointerdown を処理
↓
touchstart

という順序になります。

そのため touchstart で canMoveCamera = true にした時点では、tldraw はすでに2本目のpointerを、camera movementが禁止された状態で処理していました。この中途半端な状態からpinchへ移行することで、pinch開始時の挙動が不安定になったり、camera位置が飛んだりしました。

そこで、gesture開始の判定には pointerdown を使うようにしました。

PointerEvent.isPrimary を使えば、自前で指の本数を数えなくても、

最初の指
pointerdown / isPrimary = true
→ canMoveCamera = false


2本目
pointerdown / isPrimary = false
→ canMoveCamera = originalCanMoveCamera

と判断できます。

特に2本目ではcapture phaseの pointerdown で先にcamera movementを有効化するため、tldrawが2本目を処理する時点ですでにpinch/pan可能な状態になっています。これによって2本指panとpinch zoomが安定しました。

もう一つの問題は、最初の実装で

const activeTouchPointers = new Set();

を使い、pointerdown / pointerup / pointercancel を全部追跡していたことです。

一見正確そうですが、実際のiPad/Androidのmulti-touchでは、pinch中にブラウザやtldraw側のgesture処理によって pointercancel が発生することがあります。その結果、自前の activeTouchPointers とブラウザ/tldrawが認識している実際のgesture状態がずれることがありました。

特に、

2本 → 1本
→ activeTouchPointers.size === 1
→ canMoveCamera = false

とgesture途中で状態を変更すると、tldrawのpinch終了処理と競合しました。その結果、まれに1本指入力が効かなくなったり、laserだけでなくpen toolまで反応しなくなる状態が発生しました。もう一度pinchすると復活することがあったのも、次のmulti-touch gestureによってtldrawの内部入力状態が再構成されていたためと考えられます。

そこで、pointerの終了を一本一本管理することをやめました。

現在の方針は、

最初の pointerdown
→ canMoveCamera = false


2本目の pointerdown
→ multiTouchGesture = true
→ canMoveCamera = originalCanMoveCamera


2本 → 1本
→ 何もしない


全指が離れた touchend
→ gesture終了
→ canMoveCameraを元に戻す

です。

特に重要なのは 2本から1本になったときに何もしないことです。

一度multi-touch gestureが始まったら、そのgestureはすべての指が画面から離れるまでcamera操作として扱います。途中で canMoveCamera を切り替えないため、tldrawのpinch stateと衝突しなくなりました。

つまり、最終的にはイベントの役割を分けたのがポイントです。

pointerdown
→ gestureが「1本指か2本指か」を早い段階で判定するために使う


touchend
→ gesture全体が本当に終了したか
   (touches.length === 0)
   を判断するために使う

この組み合わせにより、

laser使用中だけ1本指panを禁止
通常のHand toolには影響しない
2本指panは正常
pinch zoomも正常
2→1の途中でcamera状態を変更しないため入力が死なない

という状態にできました。

さらに、pinch終了時に片方の指だけが少し遅れて離れると、その残った指をレーザー操作として拾ってしまう問題については、multi-touchから1本に戻った後を「まだ前のgestureの一部」として扱い、次の新しいprimary touchが始まるまでlaser位置更新を無視する、という考え方で抑制できます。
IMPORTANT change!
以前は二本指でズームやパンしていて、一本でも指がスライドの外に出た場合、viewerにズームやパン情報が伝わらなかったが、これ一発で修正できた。
Visually remove the laser when presenter move it outside of the slide by two- or one-finger drag on touch devices.
Laser must be shown beyond the slide area.
<Before this change, fit-to-width button broke the laser UI, namely, the disabled one-finger pan>

Reason for this change

Changing fitToWidth recreates the tldraw subtree because fitToWidth is included in the component key. As a result, both the tldraw editor instance and presentationInnerWrapper are replaced.

The touch-handling effect previously read tlEditorRef.current only when the effect was initialized. Its event handlers therefore retained the old editor and were attached to the old wrapper after tldraw was remounted. Updating tlEditorRef.current alone did not fix this because changing a ref does not trigger a React render or rerun an effect.

Why this fixes the issue

The mounted editor is now also stored in React state and updated from handleTldrawMount. When tldraw creates a new editor, the state update causes the touch-handling effect to run again. The old event listeners are removed, and new listeners are registered using the newly mounted editor and wrapper.

Consequently, after toggling “fit to width,” a primary single-finger touch once again sets canMoveCamera to false, while adding a second finger restores camera movement for pan and pinch gestures.

The editor itself is not duplicated: the ref and state both point to the same editor instance. The ref provides immediate access to the current editor, while the state makes editor replacement observable to React.
当初は、effect内でtlEditorRef.currentを直接参照していたため、通常の初回使用では正常に動作していた。しかし、fitToWidthを切り替えるとTldrawが再マウントされ、editorが入れ替わってもeffectが再実行されず、一本指パンの無効化が効かなくなっていた。

そこでeditorの入れ替わりをstateで検知するよう変更したが、handleTldrawMountでsetMountedTldrawEditor(editor)を呼び出す処理が抜けていた。この行を追加することで、新しいeditorのマウント後にeffectが再実行され、fitToWidth切り替え後も一本指パンが正しく無効化されるようになった。
@github-actions

Copy link
Copy Markdown

🚨 Automated tests failed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant