fix: clip frame children completely out of bounds

This commit is contained in:
Ryan Di
2026-03-18 21:16:40 +11:00
parent 2b0e4c9623
commit 3b6de119b3
3 changed files with 125 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
# AGENTS.md
## Project Structure
Excalidraw is a **monorepo** with a clear separation between the core library and the application:
- **`packages/excalidraw/`** - Main React component library published to npm as `@excalidraw/excalidraw`
- **`excalidraw-app/`** - Full-featured web application (excalidraw.com) that uses the library
- **`packages/`** - Core packages: `@excalidraw/common`, `@excalidraw/element`, `@excalidraw/math`, `@excalidraw/utils`
- **`examples/`** - Integration examples (NextJS, browser script)
## Development Workflow
1. **Package Development**: Work in `packages/*` for editor features
2. **App Development**: Work in `excalidraw-app/` for app-specific features
3. **Testing**: Always run `yarn test:update` before committing
4. **Type Safety**: Use `yarn test:typecheck` to verify TypeScript
## Development Commands
```bash
yarn test:typecheck # TypeScript type checking
yarn test:update # Run all tests (with snapshot updates)
yarn fix # Auto-fix formatting and linting issues
```
## Architecture Notes
### Package System
- Uses Yarn workspaces for monorepo management
- Internal packages use path aliases (see `vitest.config.mts`)
- Build system uses esbuild for packages, Vite for the app
- TypeScript throughout with strict configuration
+13
View File
@@ -872,6 +872,19 @@ export const shouldApplyFrameClip = (
return true;
}
// Elements that belong to a frame should still render through that frame's
// clip, even when fully outside the frame bounds (e.g. generated content).
if (
!appState.selectedElementsAreBeingDragged &&
element.frameId === frame.id
) {
for (const groupId of element.groupIds) {
checkedGroups?.set(groupId, true);
}
return true;
}
// if an element is outside the frame, but is part of a group that has some elements
// "in" the frame, we should clip the element
if (
+78
View File
@@ -2,6 +2,7 @@ import {
convertToExcalidrawElements,
Excalidraw,
} from "@excalidraw/excalidraw";
import { arrayToMap } from "@excalidraw/common";
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
import { Keyboard, Pointer } from "@excalidraw/excalidraw/tests/helpers/ui";
@@ -10,6 +11,8 @@ import {
render,
} from "@excalidraw/excalidraw/tests/test-utils";
import { shouldApplyFrameClip } from "../src/frame";
import type { ExcalidrawElement } from "../src/types";
const { h } = window;
@@ -561,3 +564,78 @@ describe("adding elements to frames", () => {
});
});
});
describe("frame clipping", () => {
const getAppStateForFrameClip = () =>
({
frameRendering: {
enabled: true,
clip: true,
},
selectedElementsAreBeingDragged: false,
selectedElementIds: {},
frameToHighlight: null,
editingGroupId: null,
} as any);
it("clips a frame child even when fully outside the frame bounds", () => {
const frame = API.createElement({
type: "frame",
id: "frame",
x: 0,
y: 0,
width: 100,
height: 100,
});
const outsideChild = API.createElement({
type: "rectangle",
id: "outside-child",
x: 250,
y: 250,
width: 50,
height: 50,
frameId: frame.id,
});
const elementsMap = arrayToMap([outsideChild, frame]);
expect(
shouldApplyFrameClip(
outsideChild,
frame,
getAppStateForFrameClip(),
elementsMap,
),
).toBe(true);
});
it("does not clip an outside element that does not belong to the frame", () => {
const frame = API.createElement({
type: "frame",
id: "frame",
x: 0,
y: 0,
width: 100,
height: 100,
});
const outsideElement = API.createElement({
type: "rectangle",
id: "outside",
x: 250,
y: 250,
width: 50,
height: 50,
});
const elementsMap = arrayToMap([outsideElement, frame]);
expect(
shouldApplyFrameClip(
outsideElement,
frame,
getAppStateForFrameClip(),
elementsMap,
),
).toBe(false);
});
});