Skip to content

✨ feat(renderer): ABSOLUTE 子は親 clip 外で描画し、drag 中の bypass frame は clip 一時無効化#5

Merged
cardene777 merged 3 commits into
masterfrom
feature/clip-absolute-children-and-drag
Jun 3, 2026
Merged

✨ feat(renderer): ABSOLUTE 子は親 clip 外で描画し、drag 中の bypass frame は clip 一時無効化#5
cardene777 merged 3 commits into
masterfrom
feature/clip-absolute-children-and-drag

Conversation

@cardene777

@cardene777 cardene777 commented Jun 3, 2026

Copy link
Copy Markdown
Owner

概要

PR #4 (Cmd+ ドラッグで auto-layout 内ノードを自由配置できる bypass キー) でマージした機能の続きで、実機検証で見つかった clip 関連の不具合を解消する。
具体的には Cmd+ ドラッグで auto-layout 親 frame の外に子を移動させた時、子が clipsContent=true の clip mask に隠されて見えなくなる問題と、commit 後の ABSOLUTE 子が同じく親 bbox 外で消える問題の両方を Figma 互換の振る舞いに揃える。

課題

PR #4 マージ直後の実機ドラッグ確認で以下が判明した。

  • Cmd を押しながら auto-layout 内の子を親 frame の外まで drag すると、drag 中から子が消える (= 背景に行くのではなく描画が削られている)
  • mouseup で layoutPositioning='ABSOLUTE' が確定した後も、親 frame bbox の外にある子は同じ理由で見えない
  • 元の親 bbox 内に戻すと再び見える = scene-graph は生きていて clip mask だけが原因

Figma では auto-layout 親の clipsContent が true でも、ABSOLUTE 子だけは親 clip 範囲を越えて描画される。
本 PR はこの挙動に揃える。

詳細

scene.ts の renderChildren は親 frame の bbox で全子を一律にクリップしていた。

graph LR
    A[Parent frame clipsContent=true] --> B[clipRect 0,0 width,height]
    B --> C[全子を clip 内で描画]
    C --> D[bbox 外の子は削られる]
Loading

drag 中の preview もこの経路を通るため、Cmd+ ドラッグで親 frame を越えると preview 位置が clip 範囲外になり画面から消える。
さらに mouseup 後 ABSOLUTE 化された子も同じ clip mask の対象になり、親 bbox 外に置いた瞬間に消える。

解決方法

scene.ts の renderChildren を以下の順序で書き換えた。

  1. 親が auto-layout (layoutMode !== 'NONE') でない場合は従来通り全子を clip 内で描画する (regression 防止)
  2. 親が auto-layout の場合、子を「clip 内 (AUTO)」と「clip 外 (ABSOLUTE)」の連続 run に分割して z-order を維持しつつ run ごとに clip on/off を切り替える
  3. mask 子に遭遇したら mask とその target を同一 run にまとめ、mask chain (renderMaskedChildIds の sibling pair) を壊さない
  4. drag 中で draggingClipBypassFrameId === node.id の時は clip 自体を一時無効化して preview が親 bbox 外でも見えるようにする
  5. drag interrupted (blur / visibilitychange / tool:changed / onScopeDispose) では drag.value を null にして bypass state も同時にクリア

仕組み

修正後の描画フローと state cleanup の関係。

graph TD
    A[renderChildren] --> B{auto-layout?}
    B -->|No| C[全子を clip 内で描画 = 旧挙動]
    B -->|Yes| D[getRenderableChildRuns]
    D --> E[run 分割 mask 群は同一 run]
    E --> F{run.shouldClip}
    F -->|true AUTO 含む| G[save + clip + render + restore]
    F -->|false 全 ABSOLUTE| H[clip 外で render]
    A --> I{dragging bypass frame?}
    I -->|Yes| J[clip skip 全子を clip 外で render]
Loading
sequenceDiagram
    participant User
    participant useCanvasInput
    participant editor.state
    participant scene.ts
    User->>useCanvasInput: mousedown + Cmd hold + drag
    useCanvasInput->>editor.state: setDraggingClipBypassFrameId parent
    scene.ts->>scene.ts: draggingClipBypassFrameId === node.id なら clip skip
    User->>useCanvasInput: 別 window へ blur (interruption)
    useCanvasInput->>useCanvasInput: cancelMoveDragInterruption
    useCanvasInput->>editor.state: preview を originals に戻す
    useCanvasInput->>useCanvasInput: drag.value = null
    useCanvasInput->>editor.state: setDraggingClipBypassFrameId null
Loading

変更したファイルと内容。

ファイル 何を変えたか
packages/core/src/canvas/scene.ts getRenderableChildRuns を新規 export し、auto-layout 親では子を AUTO/ABSOLUTE run に分割。mask + target を同一 run にまとめて mask chain を維持。getVisibleMaskType helper も抽出して SSOT 化。renderChildren は run ごとに save/clip/restore を切り替え、drag bypass frame は clip 全体を skip
packages/core/src/canvas/renderer/pipeline.ts editor.state の draggingClipBypassFrameId を RenderOverlays 経由で scene.ts に伝播
packages/core/src/canvas/renderer/types.ts RenderOverlays に draggingClipBypassFrameId フィールドを追加
packages/core/src/editor/types.ts EditorState に draggingClipBypassFrameId を追加
packages/core/src/editor/state.ts デフォルトを null に
packages/core/src/editor/selection/overlays.ts setDraggingClipBypassFrameId setter を idempotent guard 付きで追加
packages/vue/src/shared/input/move.ts handleMoveMove で bypass 中 frame ID を set、handleMoveUp / 早期 return path でクリア
packages/vue/src/canvas/useCanvasInput.ts clearDraggingClipBypassFrame helper を追加し、blur / visibilitychange (hidden) / tool:changed / onScopeDispose 4 経路で呼出。さらに cancelMoveDragInterruption で interruption 時は preview を originals に戻して drag.value を null にし、bypass 再 arm を構造的に防ぐ
tests/engine/render/canvas/clips-content.test.ts (新規) AC1-4 + mask chain regression を固定 (5 tests)
tests/engine/vue/input/cmd-bypass-auto-layout.test.ts AC2/3/5 と R2-001 (interruption 後の bypass 再 arm 防止) の regression test を追加
CHANGELOG.md Unreleased に追記

テストと確認

  • 新規 unit テスト clips-content.test.ts 5 件 + cmd-bypass-auto-layout.test.ts 既存 + 追加 = 25 pass / 0 fail
  • 既存 e2e 13 件 (auto-layout/drag.spec.ts + canvas/manipulation.spec.ts + move-live.spec.ts) 全 PASS regression なし
  • bun --filter @open-pencil/core buildbun --filter @open-pencil/vue build 成功
  • 既存 dev server (http://localhost:1420) HTTP 200 OK
  • PR 起因 lint / typecheck エラー 0 件 (scene.ts:704 renderText の complexity 25 は上流 commit 7f3eb53 由来の既存違反で本 PR は変更外)

レビューで見てほしいところ。

  • getRenderableChildRuns の mask + target を同一 run にまとめる判定が Figma 互換で正しいか (mask group 内に AUTO/ABSOLUTE 混在時は clip ON 側を優先する設計)
  • cancelMoveDragInterruption が drag preview を originals に戻して drag.value を null にする復元順序が安全か (commit せず破棄、layoutInsertIndicator / snapGuides / dropTarget も同時クリア)
  • parent.layoutMode !== undefined && parent.layoutMode !== 'NONE' の auto-layout 判定で SECTION / CANVAS / 古い node の取り扱いが意図通りか

既知の限界 / follow-up

関連

このリポジトリは fork で Issues 機能が無効化されているため、本 PR は Issue を伴わない直接 PR として提出する。

Summary by CodeRabbit

リリースノート

  • Bug Fixes
    • オートレイアウトコンテナの配下にある絶対配置の子要素が、親フレームのクリップにより誤って隠れてしまう問題を修正しました。
    • Cmd/Ctrl キーを使用したドラッグ操作中のプレビュー表示が正しく動作するように改善しました。

cardene777 and others added 3 commits June 3, 2026 11:19
… 一時無効化

Cmd+ ドラッグで auto-layout 親の外に出した子が clipsContent=true の clip mask で見えなくなる問題を解決する。
Figma 同様に layoutPositioning='ABSOLUTE' の子は親 frame の clipsContent でも clip 外で描画されるよう scene.ts の renderChildren を変更。

主な変更:
- editor.state に draggingClipBypassFrameId を追加し handleMoveMove で drag 中の bypass 親 frame ID を保持、handleMoveUp でクリア
- renderer の RenderOverlays / pipeline で draggingClipBypassFrameId を scene.ts に伝播
- scene.ts renderChildren で getChildClipRuns helper を導入し、子の layoutPositioning ごとに clip run を切り替えて z-order を維持しつつ ABSOLUTE 子だけ clip 外描画
- drag bypass 中の frame は clip 自体を skip して preview が親 bbox 外でも見えるように
- tests/engine/render/canvas/clips-content.test.ts (新規) と cmd-bypass-auto-layout.test.ts (追加) で AC1-5 を固定

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e 厳格化

Round 1 で発見された 3 件の指摘を修正:

- F-A001 (HIGH): renderChildren の AUTO/ABSOLUTE run 分割で mask とその target が別 run に分かれ mask が機能しなくなる regression を修正。getRenderableChildRuns で mask 検出時は続く target まで同一 run にまとめ、内部に非 ABSOLUTE が 1 つでもあれば clip 維持
- F-A002 (MEDIUM): draggingClipBypassFrameId の state leak を防ぐため clearDraggingClipBypassFrame helper を追加し、window blur / document visibilitychange / editor tool:changed / onScopeDispose の 4 経路でクリーンアップ
- F-A003 (MEDIUM): unclip rule を parent.layoutMode !== 'NONE' で auto-layout 親に限定。layoutMode='NONE' の通常 frame では ABSOLUTE 子も従来通り clip 内に留まる

getVisibleMaskType helper を抽出して mask 判定を SSOT 化し、scene.ts 内の重複を排除。
regression test 4 件追加 (clips-content 3 + cmd-bypass 1)、24 pass / 0 fail。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round 2 で発見した R2-001 (MEDIUM) を修正。
従来の cleanup paths (blur / visibilitychange / tool:changed / onScopeDispose) は draggingClipBypassFrameId を瞬間的にクリアするだけで、drag.value はそのまま残るため、次の mousemove で handleMoveMove が syncDraggingClipBypassFrame を再実行して bypass が復活していた。

cancelMoveDragInterruption helper を追加し、interruption 時は:
- preview を originals 位置に戻す (commit せず破棄)
- drag.value = null で drag 自体を cancel
- layoutInsertIndicator / snapGuides / dropTarget も同時クリア
- 最後に draggingClipBypassFrameId をクリア (idempotent)

これで drag が cancel された後の mousemove は `if (!drag.value) return` で no-op、bypass が再 arm されない。
regression test 追加で interruption 後の drag null + preview 復元 + bypass 非復活を固定。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

ドラッグ中の絶対配置 auto-layout 子と Cmd/Ctrl プレビューが親フレームのクリップで誤って隠れないようにする機能を実装。エディタ状態にバイパスフレームIDを追加し、シーン描画ロジックを子ノード単位の選別クリップへ改造し、入力ハンドラで状態同期と後片付けを追加。

Changes

Auto-layout クリップバイパス

Layer / File(s) Summary
変更ログ
CHANGELOG.md
絶対配置 auto-layout 子と Cmd/Ctrl ドラッグプレビューが親クリップで隠れない修正を記録。
型定義とエディタ状態
packages/core/src/editor/types.ts, packages/core/src/editor/state.ts, packages/core/src/canvas/renderer/types.ts, packages/core/src/editor/selection/overlays.ts
EditorStateRenderOverlaysdraggingClipBypassFrameId を追加し、状態初期化と setter メソッドで更新・再描画を管理。
子描画ラン単位の選別クリップ
packages/core/src/canvas/scene.ts
可視マスク型判定の getVisibleMaskType、ノード形状ベースのクリップ処理 clipToNodeBounds、auto-layout と絶対配置を区分して shouldClip を決める getRenderableChildRuns を新設。renderChildren を各ラン単位で個別クリップ可能に改造。
レンダラーパイプライン統合
packages/core/src/canvas/renderer/pipeline.ts
エディタ状態のバイパスフレームIDをオーバーレイ経由でレンダー関数に引き渡し、volatile キャッシュ判定に追加。
入力ハンドラ後片付けと共有ユーティリティ
packages/vue/src/canvas/useCanvasInput.ts
ウインドウフォーカス喪失・tool 変更・document 可視性変更時にクリップバイパス状態をクリア。move ドラッグ中断時にプレビューと状態を復元する cancelMoveDragInterruption をエクスポート。
move オペレーション中のバイパス同期
packages/vue/src/shared/input/move.ts
move ドラッグ中に bypassingAutoLayout 状態に応じてバイパスフレームIDを同期。ドラッグ終了時に解除。
クリッピング動作検証テスト
tests/engine/render/canvas/clips-content.test.ts
親クリップ下の絶対配置 auto-layout 子、draggingClipBypassFrameId による選別バイパス、マスク+絶対ターゲット、全絶対マスク、layoutMode: NONE フレームの各シナリオでクリッピング動作を検証。
入力ハンドラテスト更新
tests/engine/vue/input/cmd-bypass-auto-layout.test.ts
Cmd/Ctrl auto-layout bypass テストを拡張。バイパス状態の設定・解除・中断時の復元を検証。

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Clips that bypass with grace,
Absolute children find their place,
Drag and preview, free from shade,
Through the maze of layouts made,
Canvas renders, fresh and bright!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed プルリクエストのタイトルは、ABSOLUTE 子要素の描画処理と drag 中のクリップバイパス機能という PR の主要な変更内容を正確に要約しており、変更セットの主要な目的を明確に示しています。
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 feature/clip-absolute-children-and-drag

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 and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/vue/src/canvas/useCanvasInput.ts (1)

28-40: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

JSDoc が誤った関数に付与されています。

28-34 行の説明は useCanvasInput を記述したものですが、新規の clearDraggingClipBypassFrame の直上に移動してしまっています。useCanvasInput(Line 71)の上に戻すか、各新規エクスポート関数に専用の説明を付けてください。

🤖 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/vue/src/canvas/useCanvasInput.ts` around lines 28 - 40, ファイル内の JSDoc
ブロックは useCanvasInput の説明が意図せず clearDraggingClipBypassFrame の上に移動しているため、JSDoc
を正しい場所に戻すか各エクスポートに適切な説明を付けてください;具体的には現在の JSDoc を useCanvasInput(関数名
useCanvasInput を参照)直上へ移動するか、clearDraggingClipBypassFrame(関数名
clearDraggingClipBypassFrame と引数 editor
を参照)に別個の短い説明コメントを追加して、各関数の責務が正しく文書化されるようにしてください。
🧹 Nitpick comments (2)
tests/engine/vue/input/cmd-bypass-auto-layout.test.ts (1)

119-119: ⚡ Quick win

重複するテストラベル。

"AC5"(Line 119 と 147)および "R2-001"(Line 206 と 230)が重複しています。テスト失敗時にどのケースか判別しづらいため、一意なラベルに整理することを検討してください。

Also applies to: 147-147, 206-206, 230-230

🤖 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/engine/vue/input/cmd-bypass-auto-layout.test.ts` at line 119, The test
labels "AC5" and "R2-001" are duplicated which makes failures ambiguous; locate
the test declarations like test('AC5: mouseup always clears
draggingClipBypassFrameId', ...) and the other test(s) using 'AC5' and similarly
the test(s) using 'R2-001', and rename each duplicated label to unique,
descriptive identifiers (e.g., 'AC5-1' / 'AC5-2' or include the scenario detail)
so every test title is unique; update all duplicate occurrences consistently to
avoid collisions in test output.
packages/vue/src/canvas/useCanvasInput.ts (1)

339-343: ⚡ Quick win

useEditorEvent コンポーザブルの利用を検討。

editor.onEditorEvent('tool:changed', ...) を手動購読し onScopeDispose で解除していますが、Vue SDK には scope cleanup で自動解除される useEditorEvent(event, handler) が用意されています。これを使うと購読解除の取りこぼしリスクが減ります(onScopeDispose 内の clearClipBypassFrame() 呼び出しは別途残してください)。

Based on learnings: "Vue SDK provides useEditorEvent(event, handler) composable that auto-disposes on scope cleanup".

🤖 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/vue/src/canvas/useCanvasInput.ts` around lines 339 - 343,
editor.onEditorEvent('tool:changed', ...) を手動で購読して stopToolChangeCleanup を
onScopeDispose で解除している箇所は、Vue SDK の自動解除される useEditorEvent
を使って置き換えてください:editor.onEditorEvent の呼び出しを削除し代わりに useEditorEvent('tool:changed',
clearClipBypassFrame) を登録し、onScopeDispose 内の clearClipBypassFrame()
の呼び出しはそのまま残す(stopToolChangeCleanup 変数とその呼び出しを削除)。これで購読解除の取りこぼしを防げます。
🤖 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 `@tests/engine/render/canvas/clips-content.test.ts`:
- Around line 118-251: Add Playwright canvas snapshot tests under tests/e2e that
render the same scenarios exercised in clips-content.test.ts: (1) auto-layout
frame with both auto and ABSOLUTE children (auto-layout + ABSOLUTE), (2) drag
clip bypass limited to the targeted frame using draggingClipBypassFrameId, (3)
mask with absolute target kept in same clipped run and an all-absolute mask
group outside the clip, and (4) layoutMode: 'NONE' frame showing absolute
children still clipped; reuse the scene construction helpers
(createClippingFrame, renderChildRecords/getRenderableChildRuns or equivalent
scene setup) and capture PNG snapshots named to reflect
clip|absolute|auto-layout|bypass so the e2e snapshot suite verifies the actual
canvas pixel output for those cases.

---

Outside diff comments:
In `@packages/vue/src/canvas/useCanvasInput.ts`:
- Around line 28-40: ファイル内の JSDoc ブロックは useCanvasInput の説明が意図せず
clearDraggingClipBypassFrame の上に移動しているため、JSDoc
を正しい場所に戻すか各エクスポートに適切な説明を付けてください;具体的には現在の JSDoc を useCanvasInput(関数名
useCanvasInput を参照)直上へ移動するか、clearDraggingClipBypassFrame(関数名
clearDraggingClipBypassFrame と引数 editor
を参照)に別個の短い説明コメントを追加して、各関数の責務が正しく文書化されるようにしてください。

---

Nitpick comments:
In `@packages/vue/src/canvas/useCanvasInput.ts`:
- Around line 339-343: editor.onEditorEvent('tool:changed', ...) を手動で購読して
stopToolChangeCleanup を onScopeDispose で解除している箇所は、Vue SDK の自動解除される
useEditorEvent を使って置き換えてください:editor.onEditorEvent の呼び出しを削除し代わりに
useEditorEvent('tool:changed', clearClipBypassFrame) を登録し、onScopeDispose 内の
clearClipBypassFrame() の呼び出しはそのまま残す(stopToolChangeCleanup
変数とその呼び出しを削除)。これで購読解除の取りこぼしを防げます。

In `@tests/engine/vue/input/cmd-bypass-auto-layout.test.ts`:
- Line 119: The test labels "AC5" and "R2-001" are duplicated which makes
failures ambiguous; locate the test declarations like test('AC5: mouseup always
clears draggingClipBypassFrameId', ...) and the other test(s) using 'AC5' and
similarly the test(s) using 'R2-001', and rename each duplicated label to
unique, descriptive identifiers (e.g., 'AC5-1' / 'AC5-2' or include the scenario
detail) so every test title is unique; update all duplicate occurrences
consistently to avoid collisions in test output.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1c9728bc-9bd7-4248-bf55-d185c06c8afa

📥 Commits

Reviewing files that changed from the base of the PR and between 0846243 and e587389.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • packages/core/src/canvas/renderer/pipeline.ts
  • packages/core/src/canvas/renderer/types.ts
  • packages/core/src/canvas/scene.ts
  • packages/core/src/editor/selection/overlays.ts
  • packages/core/src/editor/state.ts
  • packages/core/src/editor/types.ts
  • packages/vue/src/canvas/useCanvasInput.ts
  • packages/vue/src/shared/input/move.ts
  • tests/engine/render/canvas/clips-content.test.ts
  • tests/engine/vue/input/cmd-bypass-auto-layout.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (17)
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Keep TypeScript files under ~600 lines and split by domain when they grow (reference: packages/core/src/tools/ pattern)

Files:

  • packages/core/src/editor/types.ts
  • packages/core/src/canvas/renderer/types.ts
  • packages/core/src/editor/state.ts
  • packages/core/src/editor/selection/overlays.ts
  • packages/core/src/canvas/renderer/pipeline.ts
  • packages/vue/src/shared/input/move.ts
  • tests/engine/vue/input/cmd-bypass-auto-layout.test.ts
  • packages/vue/src/canvas/useCanvasInput.ts
  • packages/core/src/canvas/scene.ts
  • tests/engine/render/canvas/clips-content.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Do not use any type in TypeScript; use proper types and generics instead
Do not use ! non-null assertions in TypeScript; use guards, ?., or ?? instead
No inline type definitions when a named type exists; use Color, Vector, SceneNode, Effect, Fill, Stroke instead of redefining inline
Use culori for color conversions; do not reimplement parseColor/colorToRgba
Use structuredClone for deep copies; never use shallow spread when mutating nested objects
Do not hand-roll what a dependency already does; check existing deps in package.json and packages/*/package.json first

Files:

  • packages/core/src/editor/types.ts
  • packages/core/src/canvas/renderer/types.ts
  • packages/core/src/editor/state.ts
  • packages/core/src/editor/selection/overlays.ts
  • packages/core/src/canvas/renderer/pipeline.ts
  • packages/vue/src/shared/input/move.ts
  • tests/engine/vue/input/cmd-bypass-auto-layout.test.ts
  • packages/vue/src/canvas/useCanvasInput.ts
  • packages/core/src/canvas/scene.ts
  • tests/engine/render/canvas/clips-content.test.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use Math.random() anywhere in codebase; use crypto.getRandomValues() instead

Files:

  • packages/core/src/editor/types.ts
  • packages/core/src/canvas/renderer/types.ts
  • packages/core/src/editor/state.ts
  • packages/core/src/editor/selection/overlays.ts
  • packages/core/src/canvas/renderer/pipeline.ts
  • packages/vue/src/shared/input/move.ts
  • tests/engine/vue/input/cmd-bypass-auto-layout.test.ts
  • packages/vue/src/canvas/useCanvasInput.ts
  • packages/core/src/canvas/scene.ts
  • tests/engine/render/canvas/clips-content.test.ts
packages/**/*.{ts,tsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use package-local aliases inside workspace packages: #vue/* in packages/vue, #cli/* in packages/cli, #mcp/* in packages/mcp, #core/* when core needs alias; prefer relative imports within nearby core modules

Files:

  • packages/core/src/editor/types.ts
  • packages/core/src/canvas/renderer/types.ts
  • packages/core/src/editor/state.ts
  • packages/core/src/editor/selection/overlays.ts
  • packages/core/src/canvas/renderer/pipeline.ts
  • packages/vue/src/shared/input/move.ts
  • packages/vue/src/canvas/useCanvasInput.ts
  • packages/core/src/canvas/scene.ts
packages/core/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

packages/core/**/*.ts: es-toolkit is available in core for small utility helpers; use subpath imports like es-toolkit/object, es-toolkit/array, es-toolkit/predicate; avoid es-toolkit/compat
Core code must guard browser APIs: use typeof window !== 'undefined', typeof document === 'undefined' for headless compatibility

Files:

  • packages/core/src/editor/types.ts
  • packages/core/src/canvas/renderer/types.ts
  • packages/core/src/editor/state.ts
  • packages/core/src/editor/selection/overlays.ts
  • packages/core/src/canvas/renderer/pipeline.ts
  • packages/core/src/canvas/scene.ts
**/*.{ts,tsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,vue}: Mac keyboards: use e.code not e.key for shortcuts with modifiers (Option transforms characters)
showOpenFilePicker/showSaveFilePicker are File System Access API (Chrome/Edge), not Tauri-only — code has fallbacks
Tauri detection: IS_TAURI constant from packages/core/src/constants.ts — do not use '__TAURI_INTERNALS__' in window inline
.fig export: compression with fflate (browser) or Tauri Rust commands

Files:

  • packages/core/src/editor/types.ts
  • packages/core/src/canvas/renderer/types.ts
  • packages/core/src/editor/state.ts
  • packages/core/src/editor/selection/overlays.ts
  • packages/core/src/canvas/renderer/pipeline.ts
  • packages/vue/src/shared/input/move.ts
  • tests/engine/vue/input/cmd-bypass-auto-layout.test.ts
  • packages/vue/src/canvas/useCanvasInput.ts
  • packages/core/src/canvas/scene.ts
  • tests/engine/render/canvas/clips-content.test.ts
packages/core/src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

packages/core/src/**/*.ts: Shared types (GUID, Color, Vector, Matrix, Rect) live in packages/core/src/types.ts
Yoga WASM handles flexbox; CSS Grid blocked on upstream (react/yoga#1893)
Runtime canvaskit-wasm import exists only in canvaskit.ts — all other files use import type; CanvasKit instance passed as parameter everywhere

Files:

  • packages/core/src/editor/types.ts
  • packages/core/src/canvas/renderer/types.ts
  • packages/core/src/editor/state.ts
  • packages/core/src/editor/selection/overlays.ts
  • packages/core/src/canvas/renderer/pipeline.ts
  • packages/core/src/canvas/scene.ts
packages/core/src/editor/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

packages/core/src/editor/**/*.ts: When creating auto-layout, sort children by geometric position first
Dragging a child outside a frame should reparent it, not clip it
Groups: creating a group must preserve children's visual positions
Editing a component must call syncIfInsideComponent() to propagate to instances
computeAllLayouts() must be called after demo creation and after opening .fig files
Auto-layout creation (Shift+A) must recompute layout immediately to update selection bounds
renderVersion bumped by requestRender(); renderVersion vs sceneVersion: renderVersion = canvas repaint (pan/zoom/hover), sceneVersion = scene graph mutations
requestRepaint() bumps only renderVersion; renderNow() only for surface recreation and font loading (need immediate draw)
All selection mutations in core use ctx.setSelectedIds() and all tool changes use ctx.setActiveTool() so event bus fires consistently

Files:

  • packages/core/src/editor/types.ts
  • packages/core/src/editor/state.ts
  • packages/core/src/editor/selection/overlays.ts
{src,packages}/**/*.{ts,tsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

{src,packages}/**/*.{ts,tsx,vue}: Code outside core editor internals must not assign editor.state.selectedIds or editor.state.activeTool directly; use ctx.setSelectedIds() and ctx.setActiveTool() or editor.clearSelection(), editor.select(), editor.setTool()
Committed code must not import scratch/generated/vendor internals

Files:

  • packages/core/src/editor/types.ts
  • packages/core/src/canvas/renderer/types.ts
  • packages/core/src/editor/state.ts
  • packages/core/src/editor/selection/overlays.ts
  • packages/core/src/canvas/renderer/pipeline.ts
  • packages/vue/src/shared/input/move.ts
  • packages/vue/src/canvas/useCanvasInput.ts
  • packages/core/src/canvas/scene.ts
{packages,src}/**/[a-z]*/

📄 CodeRabbit inference engine (AGENTS.md)

Non-component domain folders use lowercase or kebab-case: scene-graph/, figma-api/, node-edit/

Files:

  • packages/core/src/editor/types.ts
  • packages/core/src/canvas/renderer/types.ts
  • packages/core/src/editor/state.ts
  • packages/core/src/editor/selection/overlays.ts
  • packages/core/src/canvas/renderer/pipeline.ts
  • packages/vue/src/shared/input/move.ts
  • packages/vue/src/canvas/useCanvasInput.ts
  • packages/core/src/canvas/scene.ts
{packages,src}/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

{packages,src}/**/*.ts: Non-component TypeScript files use lowercase or kebab-case unless they are conventional entrypoints: index.ts, types.ts, context.ts, use.ts
Use subfolders for multi-file domains instead of sibling files with repeated prefixes: prefer selection/container.ts, selection/hit-test.ts over selection-container.ts, selection-hit-test.ts
When adding second file for domain (e.g., eval-wrap.ts next to eval.ts), create folder immediately (eval/index.ts + eval/wrap.ts) instead of prefixing

Files:

  • packages/core/src/editor/types.ts
  • packages/core/src/canvas/renderer/types.ts
  • packages/core/src/editor/state.ts
  • packages/core/src/editor/selection/overlays.ts
  • packages/core/src/canvas/renderer/pipeline.ts
  • packages/vue/src/shared/input/move.ts
  • packages/vue/src/canvas/useCanvasInput.ts
  • packages/core/src/canvas/scene.ts
packages/{core,vue,mcp,cli}/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Core, Vue, MCP, and CLI build with tsdown before publishing

Files:

  • packages/core/src/editor/types.ts
  • packages/core/src/canvas/renderer/types.ts
  • packages/core/src/editor/state.ts
  • packages/core/src/editor/selection/overlays.ts
  • packages/core/src/canvas/renderer/pipeline.ts
  • packages/vue/src/shared/input/move.ts
  • packages/vue/src/canvas/useCanvasInput.ts
  • packages/core/src/canvas/scene.ts
CHANGELOG.md

📄 CodeRabbit inference engine (AGENTS.md)

Update CHANGELOG.md by moving "Unreleased" items under new version heading with date during release

Files:

  • CHANGELOG.md
packages/core/src/canvas/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

packages/core/src/canvas/**/*.ts: Viewport culling skips off-screen nodes; unclipped parents are NOT culled (children may extend beyond bounds)
Selection border width must be constant regardless of zoom — divide by scale
Section/frame title text never scales — render at fixed font size, ellipsize to fit
Rulers rendered on canvas (not DOM) with selection range badges that do not overlap tick numbers
Remote cursors: Figma-style colored arrows with white border + name pill, rendered in screen space
Canvas is CanvasKit (Skia WASM) on WebGL surface, not DOM

Files:

  • packages/core/src/canvas/renderer/types.ts
  • packages/core/src/canvas/renderer/pipeline.ts
  • packages/core/src/canvas/scene.ts
packages/vue/src/**/*.{ts,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Store portable shortcuts such as MOD+D, MOD+SHIFT+H, MOD+ALT+K; format them with formatShortcut() at render time

Files:

  • packages/vue/src/shared/input/move.ts
  • packages/vue/src/canvas/useCanvasInput.ts
{tests/e2e,tests/figma,tests/engine}/**/*.{spec,test}.ts

📄 CodeRabbit inference engine (AGENTS.md)

Test placement is strict: app E2E tests live under tests/e2e/** using *.spec.ts; Figma automation tests live under tests/figma/** using *.spec.ts; engine/unit tests live under tests/engine/** using *.test.ts

Files:

  • tests/engine/vue/input/cmd-bypass-auto-layout.test.ts
  • tests/engine/render/canvas/clips-content.test.ts
tests/engine/**/*.{ts,bench.ts}

📄 CodeRabbit inference engine (AGENTS.md)

Test helpers, benchmarks, and visual test scripts allowed in engine tests: helpers.ts, *.bench.ts, and visual-* support scripts

Files:

  • tests/engine/vue/input/cmd-bypass-auto-layout.test.ts
  • tests/engine/render/canvas/clips-content.test.ts
🧠 Learnings (36)
📓 Common learnings
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to packages/core/src/editor/**/*.ts : Dragging a child outside a frame should reparent it, not clip it
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to tests/e2e/**/*.spec.ts : Add or update Playwright canvas snapshot for changes to fills, gradients, images, blend modes, masks, boolean geometry, corners, strokes, shadows, blur, text rendering, or demo showcase
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to packages/core/src/scene-graph/**/*.ts : Frames clip content by default is OFF (unlike what you'd assume)
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to packages/core/src/editor/**/*.ts : Dragging a child outside a frame should reparent it, not clip it

Applied to files:

  • packages/core/src/editor/types.ts
  • packages/core/src/canvas/renderer/types.ts
  • packages/core/src/editor/state.ts
  • packages/core/src/editor/selection/overlays.ts
  • packages/core/src/canvas/renderer/pipeline.ts
  • packages/vue/src/shared/input/move.ts
  • tests/engine/vue/input/cmd-bypass-auto-layout.test.ts
  • packages/vue/src/canvas/useCanvasInput.ts
  • packages/core/src/canvas/scene.ts
  • tests/engine/render/canvas/clips-content.test.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to CHANGELOG.md : Update `CHANGELOG.md` by moving "Unreleased" items under new version heading with date during release

Applied to files:

  • CHANGELOG.md
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to packages/core/src/editor/**/*.ts : `renderVersion` bumped by `requestRender()`; `renderVersion` vs `sceneVersion`: renderVersion = canvas repaint (pan/zoom/hover), sceneVersion = scene graph mutations

Applied to files:

  • packages/core/src/canvas/renderer/types.ts
  • packages/core/src/canvas/renderer/pipeline.ts
  • packages/vue/src/canvas/useCanvasInput.ts
  • packages/core/src/canvas/scene.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to tests/e2e/**/*.spec.ts : Add or update Playwright canvas snapshot for changes to fills, gradients, images, blend modes, masks, boolean geometry, corners, strokes, shadows, blur, text rendering, or demo showcase

Applied to files:

  • packages/core/src/canvas/renderer/types.ts
  • packages/core/src/canvas/renderer/pipeline.ts
  • tests/engine/vue/input/cmd-bypass-auto-layout.test.ts
  • packages/vue/src/canvas/useCanvasInput.ts
  • packages/core/src/canvas/scene.ts
  • tests/engine/render/canvas/clips-content.test.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to packages/core/src/canvas/**/*.ts : Rulers rendered on canvas (not DOM) with selection range badges that do not overlap tick numbers

Applied to files:

  • packages/core/src/canvas/renderer/types.ts
  • packages/core/src/canvas/scene.ts
  • tests/engine/render/canvas/clips-content.test.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to {src,packages}/**/*.{ts,tsx,vue} : Code outside core editor internals must not assign `editor.state.selectedIds` or `editor.state.activeTool` directly; use `ctx.setSelectedIds()` and `ctx.setActiveTool()` or `editor.clearSelection()`, `editor.select()`, `editor.setTool()`

Applied to files:

  • packages/core/src/editor/selection/overlays.ts
  • packages/vue/src/canvas/useCanvasInput.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to packages/core/src/editor/**/*.ts : All selection mutations in core use `ctx.setSelectedIds()` and all tool changes use `ctx.setActiveTool()` so event bus fires consistently

Applied to files:

  • packages/core/src/editor/selection/overlays.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to packages/core/src/editor/**/*.ts : `requestRepaint()` bumps only `renderVersion`; `renderNow()` only for surface recreation and font loading (need immediate draw)

Applied to files:

  • packages/core/src/canvas/renderer/pipeline.ts
  • packages/vue/src/canvas/useCanvasInput.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to src/components/Canvas*.vue : Canvas/editor overlay code must not import property-panel internals

Applied to files:

  • packages/core/src/canvas/renderer/pipeline.ts
  • tests/engine/vue/input/cmd-bypass-auto-layout.test.ts
  • packages/vue/src/canvas/useCanvasInput.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to src/app/**/*.vue : ScrubInput (drag-to-change number) — cursor and pointerdown on outer container, not inner spans

Applied to files:

  • packages/vue/src/shared/input/move.ts
  • tests/engine/vue/input/cmd-bypass-auto-layout.test.ts
  • packages/vue/src/canvas/useCanvasInput.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to packages/vue/src/editor/**/*.{ts,vue} : Editor commands share `packages/vue/src/editor/commands/registry.ts` as canonical source for shortcut display tokens, keyboard bindings, and context-menu test IDs

Applied to files:

  • tests/engine/vue/input/cmd-bypass-auto-layout.test.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to packages/vue/src/editor/**/*.{ts,vue} : Canvas context-menu structure lives in `packages/vue/src/editor/menu-model/canvas.ts`; do not hand-build command grouping in components

Applied to files:

  • tests/engine/vue/input/cmd-bypass-auto-layout.test.ts
  • packages/vue/src/canvas/useCanvasInput.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to src/**/*.{ts,vue} : App consumes `open-pencil/core` through targeted core subpath exports and `open-pencil/vue` through public Vue SDK entrypoint

Applied to files:

  • tests/engine/vue/input/cmd-bypass-auto-layout.test.ts
  • packages/vue/src/canvas/useCanvasInput.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to tests/e2e/**/*.spec.ts : Pixel-affecting renderer features need committed visual coverage via Playwright canvas snapshot, not just mock/geometry assertions

Applied to files:

  • tests/engine/vue/input/cmd-bypass-auto-layout.test.ts
  • tests/engine/render/canvas/clips-content.test.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to tests/**/*.spec.ts : Test .fig round-trip by exporting and reimporting in Figma

Applied to files:

  • tests/engine/vue/input/cmd-bypass-auto-layout.test.ts
  • tests/engine/render/canvas/clips-content.test.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to src/app/**/*.vue : CSS `contain: paint layout style` on side panels to isolate repaints from WebGL canvas

Applied to files:

  • tests/engine/vue/input/cmd-bypass-auto-layout.test.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to tests/e2e/**/*.spec.ts : Use targeted snapshot updates such as `bunx playwright test tests/e2e/canvas/renderer-visuals.spec.ts --project=openpencil --update-snapshots`

Applied to files:

  • tests/engine/vue/input/cmd-bypass-auto-layout.test.ts
  • tests/engine/render/canvas/clips-content.test.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to packages/core/src/editor/**/*.ts : Auto-layout creation (Shift+A) must recompute layout immediately to update selection bounds

Applied to files:

  • tests/engine/vue/input/cmd-bypass-auto-layout.test.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to packages/core/src/editor/**/*.ts : When creating auto-layout, sort children by geometric position first

Applied to files:

  • tests/engine/vue/input/cmd-bypass-auto-layout.test.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to packages/core/src/editor/**/*.ts : Groups: creating a group must preserve children's visual positions

Applied to files:

  • tests/engine/vue/input/cmd-bypass-auto-layout.test.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to packages/vue/src/editor/**/*.ts : Vue SDK provides `useEditorEvent(event, handler)` composable that auto-disposes on scope cleanup

Applied to files:

  • packages/vue/src/canvas/useCanvasInput.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to packages/core/src/canvas/**/*.ts : Remote cursors: Figma-style colored arrows with white border + name pill, rendered in screen space

Applied to files:

  • packages/vue/src/canvas/useCanvasInput.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to src/app/**/*.{ts,vue} : Resize observer uses rAF throttle, not debounce — debounce causes canvas skew

Applied to files:

  • packages/vue/src/canvas/useCanvasInput.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to **/*.vue : Use `vueuse/core` hooks; prefer higher-level composables like `useBreakpoints`, `useEventListener`, `onClickOutside` over raw APIs

Applied to files:

  • packages/vue/src/canvas/useCanvasInput.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to **/*.vue : Use VueUse for DOM refs/focus: `templateRef`, `unrefElement`, `useFocus` instead of ref callback plumbing through slots

Applied to files:

  • packages/vue/src/canvas/useCanvasInput.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to src/**/*.vue : No module-level mutable state in components; use the editor store instead

Applied to files:

  • packages/vue/src/canvas/useCanvasInput.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to src/app/editor/session/**/*.ts : App editor session (`src/app/editor/session/create.ts`) is thin Vue wrapper: creates `shallowReactive` state, calls `createEditor()`, assembles app-specific modules

Applied to files:

  • packages/vue/src/canvas/useCanvasInput.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to packages/core/src/canvas/**/*.ts : Viewport culling skips off-screen nodes; unclipped parents are NOT culled (children may extend beyond bounds)

Applied to files:

  • packages/core/src/canvas/scene.ts
  • tests/engine/render/canvas/clips-content.test.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to packages/core/src/scene-graph/**/*.ts : Frames clip content by default is OFF (unlike what you'd assume)

Applied to files:

  • packages/core/src/canvas/scene.ts
  • tests/engine/render/canvas/clips-content.test.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to packages/core/src/scene-graph/**/*.ts : Domain types (SceneNode, Fill, Stroke, Effect, BlendMode) live in `packages/core/src/scene-graph/` and exported from `open-pencil/core/scene-graph`

Applied to files:

  • packages/core/src/canvas/scene.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to packages/core/src/scene-graph/**/*.ts : Instance children map to component children via `componentId` for 1:1 sync

Applied to files:

  • packages/core/src/canvas/scene.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to packages/core/src/scene-graph/**/*.ts : Nodes live in flat `Map<string, SceneNode>`, tree via `parentIndex` references

Applied to files:

  • packages/core/src/canvas/scene.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to packages/core/src/canvas/**/*.ts : Canvas is CanvasKit (Skia WASM) on WebGL surface, not DOM

Applied to files:

  • tests/engine/render/canvas/clips-content.test.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to packages/core/src/canvas/**/*.ts : Section/frame title text never scales — render at fixed font size, ellipsize to fit

Applied to files:

  • tests/engine/render/canvas/clips-content.test.ts
📚 Learning: 2026-06-02T15:53:00.948Z
Learnt from: CR
Repo: cardene777/open-pencil PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-02T15:53:00.948Z
Learning: Applies to tests/engine/**/*.{ts,bench.ts} : Test helpers, benchmarks, and visual test scripts allowed in engine tests: `helpers.ts`, `*.bench.ts`, and `visual-*` support scripts

Applied to files:

  • tests/engine/render/canvas/clips-content.test.ts
🔇 Additional comments (13)
CHANGELOG.md (1)

16-16: LGTM!

packages/core/src/editor/types.ts (1)

38-38: LGTM!

packages/core/src/editor/state.ts (1)

13-13: LGTM!

packages/core/src/canvas/renderer/types.ts (1)

22-22: LGTM!

packages/core/src/editor/selection/overlays.ts (1)

33-37: LGTM!

Also applies to: 59-68

packages/core/src/canvas/scene.ts (3)

156-170: LGTM!


172-218: LGTM!


220-252: LGTM!

packages/core/src/canvas/renderer/pipeline.ts (1)

64-64: LGTM!

Also applies to: 82-90

tests/engine/render/canvas/clips-content.test.ts (1)

23-116: LGTM!

packages/vue/src/shared/input/move.ts (1)

54-62: LGTM!

Also applies to: 82-82, 189-190

tests/engine/vue/input/cmd-bypass-auto-layout.test.ts (1)

218-251: LGTM!

packages/vue/src/canvas/useCanvasInput.ts (1)

42-69: ⚡ Quick win

duplicate ドラッグ中断時の後始末不足(複製ノード残留の可能性)

DragState には duplicatedduplicatedPreviousSelection があり、packages/vue/src/shared/input/move.tsd.duplicated && !moved 経路では editor.graph.deleteNode(...) で複製ノードを削除し、editor.select(...) で選択を復元しています。一方 packages/vue/src/canvas/useCanvasInput.tscancelMoveDragInterruptiondrag.value = null やプレビュー復元のみで、複製ノード削除/選択復元の d.duplicated 相当処理がありません(packages/vue/src/shared/input/duplicate-drag.ts では duplicated: true を付与して複製ドラッグを開始)。

cancelMoveDragInterruption 側でも activeDrag.duplicated を見て、handleMoveUp!moved と同等に「複製ノード削除+duplicatedPreviousSelection で選択復元」を入れることを検討してください。

Comment on lines +118 to +251
describe('clipsContent rendering', () => {
test('renders absolute children outside the parent clip while auto-layout children stay clipped', () => {
const graph = new SceneGraph()
const frame = createClippingFrame(graph, 'Frame')
const autoChild = graph.createNode('RECTANGLE', frame.id, {
x: 0,
y: 0,
width: 40,
height: 40
})
const absoluteChild = graph.createNode('RECTANGLE', frame.id, {
x: 140,
y: 0,
width: 40,
height: 40,
layoutPositioning: 'ABSOLUTE'
})

const records = renderChildRecords(graph, frame.id)

expect(records).toEqual([
{ nodeId: autoChild.id, clipped: true },
{ nodeId: absoluteChild.id, clipped: false }
])
})

test('limits drag clip bypass to the targeted frame', () => {
const graph = new SceneGraph()
const bypassFrame = createClippingFrame(graph, 'BypassFrame')
const normalFrame = createClippingFrame(graph, 'NormalFrame', 200)
const bypassChild = graph.createNode('RECTANGLE', bypassFrame.id, {
x: 120,
y: 0,
width: 40,
height: 40
})
const normalChild = graph.createNode('RECTANGLE', normalFrame.id, {
x: 120,
y: 0,
width: 40,
height: 40
})

const bypassRecords = renderChildRecords(graph, bypassFrame.id, {
draggingClipBypassFrameId: bypassFrame.id
})
const normalRecords = renderChildRecords(graph, normalFrame.id, {
draggingClipBypassFrameId: bypassFrame.id
})

expect(bypassRecords).toEqual([{ nodeId: bypassChild.id, clipped: false }])
expect(normalRecords).toEqual([{ nodeId: normalChild.id, clipped: true }])
})

test('keeps a mask and its absolute target in the same clipped auto-layout run', () => {
const graph = new SceneGraph()
const frame = createClippingFrame(graph, 'MaskedFrame')
const mask = graph.createNode('RECTANGLE', frame.id, {
x: 0,
y: 0,
width: 50,
height: 50,
isMask: true
})
const target = graph.createNode('RECTANGLE', frame.id, {
x: 120,
y: 0,
width: 40,
height: 40,
layoutPositioning: 'ABSOLUTE'
})

const parent = getNodeOrThrow(graph, frame.id)

expect(getRenderableChildRuns(graph, parent, parent.childIds)).toEqual([
{ childIds: [mask.id, target.id], shouldClip: true }
])
})

test('keeps an all-absolute mask group outside the clip in auto-layout frames', () => {
const graph = new SceneGraph()
const frame = createClippingFrame(graph, 'AbsoluteMaskedFrame')
const mask = graph.createNode('RECTANGLE', frame.id, {
x: 0,
y: 0,
width: 50,
height: 50,
isMask: true,
layoutPositioning: 'ABSOLUTE'
})
const target = graph.createNode('RECTANGLE', frame.id, {
x: 120,
y: 0,
width: 40,
height: 40,
layoutPositioning: 'ABSOLUTE'
})

const parent = getNodeOrThrow(graph, frame.id)

expect(getRenderableChildRuns(graph, parent, parent.childIds)).toEqual([
{ childIds: [mask.id, target.id], shouldClip: false }
])
})

test('keeps absolute children clipped for layoutMode NONE frames', () => {
const graph = new SceneGraph()
const frame = graph.createNode('FRAME', pageId(graph), {
name: 'RegularFrame',
x: 0,
y: 0,
width: 100,
height: 100,
clipsContent: true,
layoutMode: 'NONE'
})
const absoluteChild = graph.createNode('RECTANGLE', frame.id, {
x: 140,
y: 0,
width: 40,
height: 40,
layoutPositioning: 'ABSOLUTE'
})

const parent = getNodeOrThrow(graph, frame.id)

expect(getRenderableChildRuns(graph, parent, parent.childIds)).toEqual([
{ childIds: [absoluteChild.id], shouldClip: true }
])
expect(renderChildRecords(graph, frame.id)).toEqual([
{ nodeId: absoluteChild.id, clipped: true }
])
})
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for committed Playwright snapshots / specs touching clip/absolute/auto-layout rendering
fd -e spec.ts . tests/e2e | xargs rg -ln -i 'clip|absolute|auto-?layout|bypass' 2>/dev/null
echo '--- snapshot image artifacts ---'
fd -i -e png . tests/e2e | rg -i 'clip|absolute|layout|bypass' || echo 'no matching snapshot pngs found'

Repository: cardene777/open-pencil

Length of output: 490


clipsContent/クリップバイパス/ABSOLUTE の視覚スナップショット追加を確認してください
tests/engine/render/canvas/clips-content.test.ts は mock によるクリップ判定(__clipActive)のジオメトリ検証中心のため、ピクセル影響のあるクリッピング挙動は Playwright canvas スナップショットでの視覚カバレッジも必要です。tests/e2e 配下では clip|absolute|auto-layout|bypass に関連する png スナップショットの一致が見つかりませんでした。該当シナリオ(auto-layout + ABSOLUTE、draggingClipBypassFrameId の限定、mask/absolute run、layoutMode: 'NONE')が反映される canvas snapshot を追加してください。

🤖 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/engine/render/canvas/clips-content.test.ts` around lines 118 - 251, Add
Playwright canvas snapshot tests under tests/e2e that render the same scenarios
exercised in clips-content.test.ts: (1) auto-layout frame with both auto and
ABSOLUTE children (auto-layout + ABSOLUTE), (2) drag clip bypass limited to the
targeted frame using draggingClipBypassFrameId, (3) mask with absolute target
kept in same clipped run and an all-absolute mask group outside the clip, and
(4) layoutMode: 'NONE' frame showing absolute children still clipped; reuse the
scene construction helpers (createClippingFrame,
renderChildRecords/getRenderableChildRuns or equivalent scene setup) and capture
PNG snapshots named to reflect clip|absolute|auto-layout|bypass so the e2e
snapshot suite verifies the actual canvas pixel output for those cases.

@cardene777 cardene777 merged commit 692fa81 into master Jun 3, 2026
1 check passed
@cardene777 cardene777 deleted the feature/clip-absolute-children-and-drag branch June 3, 2026 04:25
cardene777 added a commit that referenced this pull request Jun 3, 2026
* ✨ feat(renderer): Cmd+ ドラッグで Screen 直下の子も clip 外で見えるよう bypass を Set 化

PR #5 が救えていなかった「layoutMode='NONE' な Screen 直下の子を Cmd+ ドラッグして Screen の bbox 外に出した時、drag 中に本体が消えて枠だけ残る」regression を解消する。

主な変更:
- EditorState.draggingClipBypassFrameId (string | null) を draggingClipBypassFrameIds (Set<string> | null) に変更
- setDraggingClipBypassFrameIds setter で Set 等価判定 (size + every has) で idempotent
- handleMoveMove の syncDraggingClipBypassFrame で d.autoLayoutParentId に加えて d.originals の各 parentId (Screen 直下の親も含む) を isClippableFrameType でフィルタして Set に集約
- scene.ts の renderChildren で overlays.draggingClipBypassFrameIds?.has(node.id) で clip skip 判定
- useCanvasInput の cancelMoveDragInterruption / clearDraggingClipBypassFrame helper / 4 cleanup paths も Set ベースに統一
- mouseup 後の commit 挙動は PR #4/#5 と同じく変更なし

regression test: layoutMode='NONE' Screen 直下の Cmd+ ドラッグで bypass Set に Screen ID が含まれ、scene.ts の renderChildren が clip skip 経路に流れることを assert。
27 tests / 0 fail (PR #5 既存 + 新規 2 件)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🔧 refactor(renderer): drag bypass を Set から boolean draggingClipBypassAll に簡略化

直前 commit 542b655 の Set ベース管理では「drag 対象の親 frame」だけを bypass していたが、実機検証で drag 中の子が通過する他の Screen の clip mask で本体が消える regression が判明した。

ユーザー期待値「drag 中はどこに動かしても本体が見える」(Figma 同等) に合わせて、Cmd+ ドラッグ中は全 clipsContent frame の clip を一律 skip するシンプル実装に変更。

主な変更:
- EditorState.draggingClipBypassFrameIds (Set<string> | null) を削除し draggingClipBypassAll (boolean) に置換
- setter setDraggingClipBypassAll で boolean 等価判定で idempotent
- handleMoveMove で Cmd 押下中なら setDraggingClipBypassAll(true)、handleMoveUp と 4 cleanup paths で false
- scene.ts の renderChildren で !overlays.draggingClipBypassAll を shouldClip に追加
- PR #5 の getRenderableChildRuns (auto-layout 限定 unclip + mask chain 維持) はそのまま残し、drag 中だけ全 clip skip 経路に流れる layered design

これにより 35 行削減し、Set 管理の複雑度を排除。drag 対象が任意の Screen を通過しても本体が見え続ける。

regression test (clips-content + cmd-bypass) を boolean ベースに更新、27 pass / 0 fail。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🔧 fix(editor): Cmd+drag mouseup 後の auto-layout 元位置詰めと layoutMode=NONE Screen 直下の ABSOLUTE 子可視化

実機検証で発覚した 2 regression を修正:

問題 1: mouseup 後に auto-layout 元位置に空白が残る
  → commitMoveWithAbsolutePin の forward / inverse で originals と finals の parentId 集約 Set を作り、各 parentId に runLayoutForNode を呼んで yoga 再計算。ABSOLUTE 化と同時に sibling が詰まる、undo でも sibling 配置が戻る

問題 2: mouseup 後に ABSOLUTE 化された子が layoutMode='NONE' の Screen 直下では見えない
  → getRenderableChildRuns の parent.layoutMode !== 'NONE' 早期 return を削除し、全 clipsContent frame で run 分割ロジック (mask chain 維持 + ABSOLUTE 子 clip 外描画) を適用。PR #5 の F-A003 で意図的に絞った auto-layout 限定 unclip を Figma 互換に緩和

regression test 追加: mouseup 後の sibling 詰め + undo 復元 (cmd-bypass) / layoutMode='NONE' Screen 直下の ABSOLUTE 子 clip 外描画 (clips-content)。27 pass / 0 fail。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ✨ feat(editor): Cmd+drag で要素を全画面どこでも自由配置できるよう描画と layout を整備

PR #5 で対応した clip 修正の続き。実機検証で発覚した複数 regression を一括解消し、Cmd+drag mouseup 後の挙動を Figma 完全互換に揃える。

主な修正:
- 祖先 clip も bypass する遅延キュー render pass を scene.ts に追加 (祖先 frame の clipsContent に関わらず ABSOLUTE 子が見える)
- getRenderableChildRuns から auto-layout 限定 early return を削除、layoutMode=NONE Screen 直下でも ABSOLUTE 子は clip 外で描画
- getRenderableChildRuns で ABSOLUTE 子を末尾 run に並べ替え、z-order が AUTO 子の前面に来るよう統一
- mask + ABSOLUTE sibling が同一 run に閉じ込められて mask path に消える regression を修正、mask 直後の ABSOLUTE sibling を独立 run に切り出し
- geometry.ts collectDescendantVisualBounds で ABSOLUTE 子の直近の親 clip だけ bypass、祖先 clip は維持
- commitMoveWithAbsolutePin で元 parent と新 parent の runLayoutForNode を集約し yoga を再計算 (auto-layout 元位置の gap が詰まる)
- ABSOLUTE pin commit 時に Page 直下へ reparent し canvas 最前面に持ち上げる (他 Screen に重なっても隠れない)
- DragMove に frozenParentSizes / frozenSiblingSizes を追加し drag 開始時の auto-layout 親と兄弟要素の width/height を記録 / commit 後に強制復元 (残った要素が parent の全幅に広がる現象を防止)
- draggingClipBypassFrameId(s) を boolean draggingClipBypassAll に簡略化し drag 中の全 clip skip と cleanup paths を統一
- useCanvasInput に blur / visibilitychange / tool:changed / onScopeDispose の 4 cleanup paths と cancelMoveDragInterruption を追加し state leak を防止

regression test を tests/engine/render/canvas/clips-content.test.ts と cmd-bypass-auto-layout.test.ts と clips-content + cache + visual-bounds + silhouette-autopsy に多数追加。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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