Skip to content

Fix shapes jumping to center after drawing and cursor drift under zoom/pan#35

Merged
Kripu77 merged 4 commits into
mainfrom
fix/shape-jump-and-cursor-coords
Jul 5, 2026
Merged

Fix shapes jumping to center after drawing and cursor drift under zoom/pan#35
Kripu77 merged 4 commits into
mainfrom
fix/shape-jump-and-cursor-coords

Conversation

@Kripu77

@Kripu77 Kripu77 commented Jul 5, 2026

Copy link
Copy Markdown
Owner

What

  • Board: drawing any shape made it jump to the screen center once auto-save ran. The saved board echoed back through the store into Plait's Wrapper as a new value, which Plait treats as an external board replacement and re-fits the viewport. BoardCanvas now seeds value once per board (keyed remount on board switch), and remote sync keeps board.children in step so the refit can't fire there either.
  • Collab: remote cursors drifted under zoom/pan because the math assumed viewport.offsetX/offsetY, which Plait doesn't have. Cursor transforms now use viewport origination via shared screenToDocument/documentToScreen helpers and resolve the viewport container instead of the scrolling SVG host.
  • Docs: CLAUDE.md conventions expanded (zero-comment policy, simplicity rules, atomic commits).

Testing

  • New e2e regression: shape bounding box is stable after the auto-save debounce (failed with a 360px jump before the fix).
  • New integration case: same board point renders consistently for two clients with different zoom, pan, and window size.
  • Full Vitest suite (1311 tests), shapes + board-management e2e specs, and typecheck all pass.

Summary by CodeRabbit

  • Bug Fixes
    • Improved cursor and pointer syncing so remote users’ positions stay aligned more reliably across zoom and pan changes.
    • Fixed board updates so incoming collaborative changes are reflected immediately in the local canvas.
    • Improved shape stability after saving, reducing layout shifts and size drift.

Kripu77 added 4 commits July 5, 2026 20:00
Drawing any shape made it jump to the screen center ~0.5s later: auto-save published a new currentBoard into the store, BoardCanvas re-fed the re-themed elements array into Plait's Wrapper as value, and Wrapper treats a value/children mismatch as an external board replacement and calls fitViewport.

Seed Wrapper's value once per board via a keyed inner component so store echoes for the same board never flow back into the live Plait board. Keep board.children in sync when applying remote collab changes so the same refit cannot fire there.
Regression test for the viewport refit: draws a rectangle, waits past the 500ms auto-save debounce, and asserts the rendered bounding box has not moved.
Plait's viewport has no offsetX/offsetY, so remote cursors drifted under zoom and pan. Replace the offset-based math with origination-based screenToDocument/documentToScreen transforms, resolve the viewport container via getViewportContainerElement instead of the scrolling SVG host, and update tests to the new semantics, including a cross-client zoom/pan/window-size case.
Add top-level rules and concrete guidance for zero-comment code, simplicity over cleverness, naming, and atomic commits.
@vercel

vercel Bot commented Jul 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
thinkix Ready Ready Preview, Comment Jul 5, 2026 10:02am

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR renames viewport offset fields to origination-based coordinates across collaboration utilities, hooks, and components, updating coordinate conversion logic and container resolution throughout cursor tracking. Separately, BoardCanvas gains remote sync fixes and a lazy state initializer, a new e2e stability test is added, and CLAUDE.md is expanded with stricter conventions.

Changes

Viewport Origination Refactor

Layer / File(s) Summary
Viewport type and conversion utilities
packages/collaboration/src/utils/viewport.ts, packages/collaboration/src/utils/index.ts, packages/collaboration/src/hooks/use-viewport.ts
Viewport switches from offsetX/offsetY to originationX/originationY; getViewport, screenToDocument, documentToScreen updated accordingly, and getViewportContainerElement is added.
Cursor manager coordinate consumption
packages/collaboration/src/cursor-manager.ts
handlePointerMove, getCursorScreenState, and getVisibleCursors switch to shared screenToDocument/documentToScreen helpers.
Cursor tracking and sync hooks
packages/collaboration/src/hooks/use-cursor-tracking.ts, packages/collaboration/src/hooks/use-sync.ts
Pointer move handlers use getViewportContainerElement instead of svg lookups; screen state uses documentToScreen.
Cursor overlay rendering
packages/collaboration/src/components/cursor-overlay.tsx
Container resolution simplified, effect dependencies expanded, and cursor positions computed via documentToScreen.
Viewport and cursor conversion tests
tests/unit/viewport.test.ts, tests/unit/cursor-manager.test.ts, tests/unit/cursor-overlay.test.tsx, tests/unit/use-cursor-tracking.test.tsx, tests/integration/cursor-collaboration.test.ts
Test fixtures and expectations updated to origination-based formulas, including a new cross-client consistency test.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Board Canvas and Testing Updates

Layer / File(s) Summary
BoardCanvas sync and state initialization
features/board/components/BoardCanvas.tsx
Remote updates now set board.children directly; BoardCanvas is split into an exported wrapper (keyed by boardData?.id) and an internal component; state initialization switches to a lazy useState.
Shape position stability e2e test
tests/e2e/shapes.spec.ts
New test verifies a drawn rectangle's bounding box remains stable after an auto-save delay.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Documentation Conventions Update

Layer / File(s) Summary
CLAUDE.md guidance expansion
CLAUDE.md
Adds top-level override rules and expands code conventions covering comments, simplicity, naming, atomic commits, and general rules.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Pointer
  participant useCursorTracking
  participant getViewportContainerElement
  participant screenToDocument
  Pointer->>useCursorTracking: pointermove event
  useCursorTracking->>getViewportContainerElement: resolve container(board)
  getViewportContainerElement-->>useCursorTracking: container element
  useCursorTracking->>screenToDocument: convert clientX/clientY with viewport
  screenToDocument-->>useCursorTracking: documentX, documentY
Loading

Possibly related PRs

  • Kripu77/thinkix#25: Both PRs modify the collaboration cursor stack—cursor-overlay.tsx and cursor-manager.ts—to change how cursor positions are computed and rendered from pointer/viewport/container coordinates.
  • Kripu77/thinkix#32: Both PRs modify features/board/components/BoardCanvas.tsx around RemoteSyncHandler/element normalization to keep the local board model in sync with incoming elements.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main user-facing fixes in the PR: shape recentering and cursor drift under zoom/pan.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/shape-jump-and-cursor-coords

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown

❌ Test Results

Metric Coverage
Lines 0%
Functions 64.51%
Branches 45.26%
Statements 57.95%
Average 41.9%

📦 Download Coverage Report

How to view coverage report
  1. Download the coverage artifact from the link above
  2. Extract the ZIP file
  3. Open index.html in your browser

Commit: 8326d48952c27c3107424f960fe69a37a982c1d7

@Kripu77 Kripu77 merged commit be6cf00 into main Jul 5, 2026
8 of 9 checks passed
@Kripu77 Kripu77 deleted the fix/shape-jump-and-cursor-coords branch July 5, 2026 10:07

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
tests/unit/viewport.test.ts (1)

281-309: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Second half of the cross-client test is tautological.

screenB's expected value is computed by re-applying documentToScreen's exact formula inline (using boardPoint instead of wire), rather than an independently-derived pixel value. Since the first assertion already proves wire ≈ boardPoint, this second assertion would still pass even if documentToScreen's math were subtly wrong — it isn't an independent oracle.

Consider asserting against literal expected numbers instead, e.g. for viewportB (zoom 1.5, origination 350/220) and rectB (left 60, top 40): expected screenB.x + rectB.left === 285 and screenB.y + rectB.top === 160.

✅ Suggested hardened assertion
       const screenB = documentToScreen(wire.x, wire.y, viewportB);
-      expect(screenB.x + rectB.left).toBeCloseTo(
-        (boardPoint.x - viewportB.originationX) * viewportB.zoom + rectB.left,
-        10
-      );
-      expect(screenB.y + rectB.top).toBeCloseTo(
-        (boardPoint.y - viewportB.originationY) * viewportB.zoom + rectB.top,
-        10
-      );
+      expect(screenB.x + rectB.left).toBeCloseTo(285, 10);
+      expect(screenB.y + rectB.top).toBeCloseTo(160, 10);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/viewport.test.ts` around lines 281 - 309, The second half of the
cross-client consistency test is tautological because it reuses the same
`documentToScreen` math inline instead of checking against an independent
expected value. Update the `cross-client consistency` test in `viewport.test.ts`
so the `screenB` assertions in `documentToScreen` compare against fixed expected
screen coordinates for `viewportB` and `rectB` (using the existing `screenB`
result), rather than recomputing the formula from `boardPoint`.
packages/collaboration/src/utils/viewport.ts (1)

44-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add JSDoc to this new exported package API.

getViewportContainerElement is a new exported function from packages/collaboration. As per path instructions, packages/**/*.{ts,tsx}: "Use JSDoc only on exported package APIs in packages/*; do not add JSDoc to internal functions."

📝 Suggested JSDoc
+/**
+ * Resolves the DOM element representing the Plait board's viewport container,
+ * used as the reference frame for screen/document coordinate conversions.
+ */
 export function getViewportContainerElement(board: PlaitBoard | null): Element | null {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/collaboration/src/utils/viewport.ts` around lines 44 - 56, Add JSDoc
to the exported package API getViewportContainerElement in the viewport utility
so it is documented as part of packages/collaboration; keep the comment on this
exported function only and do not add JSDoc to any internal helpers. Describe
what the function returns for a given PlaitBoard or null input, and make sure
the doc block sits directly above getViewportContainerElement so it remains
attached if the implementation moves.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@features/board/components/BoardCanvas.tsx`:
- Line 100: The direct assignment to board.children in BoardCanvas should be
treated as an intentional Plait mutation and suppressed for the
react-hooks/immutability lint rule, matching the pattern already used in
collaborative-board. Add the immutability disable comment at the board.children
= normalized update site in BoardCanvas so the lint check does not fail on this
deliberate mutation.

In `@packages/collaboration/src/utils/viewport.ts`:
- Around line 44-56: The fallback in getViewportContainerElement is using
document.querySelector, which can pick the first board in the page instead of
the active one. Update the fallback to resolve the viewport container from the
provided PlaitBoard instance or its own container scope, keeping the lookup
board-specific when PlaitBoard.getViewportContainer fails. Use
getViewportContainerElement and PlaitBoard.getViewportContainer as the key
places to adjust the logic.

---

Nitpick comments:
In `@packages/collaboration/src/utils/viewport.ts`:
- Around line 44-56: Add JSDoc to the exported package API
getViewportContainerElement in the viewport utility so it is documented as part
of packages/collaboration; keep the comment on this exported function only and
do not add JSDoc to any internal helpers. Describe what the function returns for
a given PlaitBoard or null input, and make sure the doc block sits directly
above getViewportContainerElement so it remains attached if the implementation
moves.

In `@tests/unit/viewport.test.ts`:
- Around line 281-309: The second half of the cross-client consistency test is
tautological because it reuses the same `documentToScreen` math inline instead
of checking against an independent expected value. Update the `cross-client
consistency` test in `viewport.test.ts` so the `screenB` assertions in
`documentToScreen` compare against fixed expected screen coordinates for
`viewportB` and `rectB` (using the existing `screenB` result), rather than
recomputing the formula from `boardPoint`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: feffcceb-e53b-4ccc-97b1-c023a4a1afe4

📥 Commits

Reviewing files that changed from the base of the PR and between f0a1b01 and cfdfec6.

📒 Files selected for processing (15)
  • CLAUDE.md
  • features/board/components/BoardCanvas.tsx
  • packages/collaboration/src/components/cursor-overlay.tsx
  • packages/collaboration/src/cursor-manager.ts
  • packages/collaboration/src/hooks/use-cursor-tracking.ts
  • packages/collaboration/src/hooks/use-sync.ts
  • packages/collaboration/src/hooks/use-viewport.ts
  • packages/collaboration/src/utils/index.ts
  • packages/collaboration/src/utils/viewport.ts
  • tests/e2e/shapes.spec.ts
  • tests/integration/cursor-collaboration.test.ts
  • tests/unit/cursor-manager.test.ts
  • tests/unit/cursor-overlay.test.tsx
  • tests/unit/use-cursor-tracking.test.tsx
  • tests/unit/viewport.test.ts

const unsubscribe = syncBusContext.syncBus.subscribeToRemoteChanges((elements: BoardElement[]) => {
const normalized = normalizeElements(elements);
onElementsChange(normalized);
board.children = normalized;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether react-hooks/immutability rule is enabled and how the sibling file guards the same mutation
fd -e cjs -e js -e json -e mjs -e ts eslint.config -H | xargs -r rg -n "immutability|react-hooks" 
rg -n "board.children\s*=" -C2

Repository: Kripu77/thinkix

Length of output: 153


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== ESLint config files ==\n'
fd -H -a 'eslint.config.*' . || true
fd -H -a '.eslintrc*' . || true

printf '\n== Search for react-hooks/immutability ==\n'
rg -n --hidden -S "react-hooks/immutability|immutability" . || true

printf '\n== Relevant files ==\n'
fd -H -a 'BoardCanvas.tsx' features || true
fd -H -a 'collaborative-board.tsx' features || true

printf '\n== BoardCanvas mutation context ==\n'
rg -n -C 4 "board\.children\s*=" features/board/components/BoardCanvas.tsx || true

printf '\n== Collaborative board mutation context ==\n'
rg -n -C 6 "board\.children\s*=" features/collaboration/components/collaborative-board.tsx || true

Repository: Kripu77/thinkix

Length of output: 2306


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== eslint.config.mjs relevant rules ==\n'
rg -n -C 3 "react-hooks/immutability|immutability" eslint.config.mjs || true

printf '\n== BoardCanvas surrounding context ==\n'
sed -n '1,140p' features/board/components/BoardCanvas.tsx

printf '\n== Collaborative board surrounding context ==\n'
sed -n '80,120p' features/collaboration/components/collaborative-board.tsx

Repository: Kripu77/thinkix

Length of output: 6205


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== eslint.config.mjs ==\n'
sed -n '1,260p' eslint.config.mjs

printf '\n== package.json eslint-related deps ==\n'
rg -n '"eslint|react-hooks|typescript-eslint|prettier"' package.json

printf '\n== any repo-wide react-hooks config imports ==\n'
rg -n "react-hooks|eslint-plugin-react-hooks|reactHooks" . --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**'

Repository: Kripu77/thinkix

Length of output: 4263


Add the immutability suppression in features/board/components/BoardCanvas.tsx:100. board.children = normalized uses the same direct Plait mutation already guarded in features/collaboration/components/collaborative-board.tsx; without the react-hooks/immutability disable, this file will fail the lint rule.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@features/board/components/BoardCanvas.tsx` at line 100, The direct assignment
to board.children in BoardCanvas should be treated as an intentional Plait
mutation and suppressed for the react-hooks/immutability lint rule, matching the
pattern already used in collaborative-board. Add the immutability disable
comment at the board.children = normalized update site in BoardCanvas so the
lint check does not fail on this deliberate mutation.

Comment on lines +44 to +56
export function getViewportContainerElement(board: PlaitBoard | null): Element | null {
if (board) {
try {
const container = PlaitBoard.getViewportContainer(board);
if (container) return container;
} catch {
}
}
return (
document.querySelector('.plait-board-container .viewport-container') ??
document.querySelector('.plait-board-container')
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does @plait/core0.92.1 export a staticPlaitBoard.getViewportContainer method, and what does it return when the board has no viewport container yet?

💡 Result:

Based on the available documentation and codebase for @plait/core 0.92.1, there is no static PlaitBoard.getViewportContainer method [1][2][3]. The framework does, however, provide a standalone function named initializeViewportContainer, which is exported by @plait/core and used to initialize the viewport container [4]. In the Plait framework's architecture, viewport and board management are typically handled through specific utility functions rather than static methods on the PlaitBoard class [4][5]. Since the method PlaitBoard.getViewportContainer does not exist in the public API of @plait/core 0.92.1, it does not return any value for a board that has no viewport container [4][5]. If you are looking for the viewport container of a board, the framework expects the container to be managed and initialized via functions like initializeViewportContainer [4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and find its call sites/usages.
git ls-files 'packages/collaboration/src/utils/viewport.ts' 'packages/collaboration/src/**' | sed -n '1,120p'

echo
echo '--- outline viewport.ts ---'
ast-grep outline packages/collaboration/src/utils/viewport.ts --view expanded

echo
echo '--- references to getViewportContainerElement ---'
rg -n "getViewportContainerElement|getViewportContainer" packages/collaboration/src packages -g '!**/dist/**' -g '!**/build/**'

echo
echo '--- files mentioning viewport-container selector ---'
rg -n "\.plait-board-container|viewport-container" packages -g '!**/dist/**' -g '!**/build/**'

Repository: Kripu77/thinkix

Length of output: 4939


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- use-cursor-tracking.ts excerpt ---'
sed -n '1,180p' packages/collaboration/src/hooks/use-cursor-tracking.ts

echo
echo '--- use-sync.ts excerpt ---'
sed -n '1,180p' packages/collaboration/src/hooks/use-sync.ts

echo
echo '--- cursor-overlay.tsx excerpt ---'
sed -n '1,220p' packages/collaboration/src/components/cursor-overlay.tsx

echo
echo '--- viewport.ts excerpt ---'
sed -n '1,120p' packages/collaboration/src/utils/viewport.ts

Repository: Kripu77/thinkix

Length of output: 14792


Scope the viewport fallback to the active board container.

document.querySelector('.plait-board-container ...') returns the first matching board in the document, so if PlaitBoard.getViewportContainer() throws or is unavailable, cursor math can attach to the wrong board when more than one is mounted. Use a board-scoped lookup instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/collaboration/src/utils/viewport.ts` around lines 44 - 56, The
fallback in getViewportContainerElement is using document.querySelector, which
can pick the first board in the page instead of the active one. Update the
fallback to resolve the viewport container from the provided PlaitBoard instance
or its own container scope, keeping the lookup board-specific when
PlaitBoard.getViewportContainer fails. Use getViewportContainerElement and
PlaitBoard.getViewportContainer as the key places to adjust the logic.

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