diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..f2d904434c --- /dev/null +++ b/AGENTS.md @@ -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 diff --git a/packages/element/src/frame.ts b/packages/element/src/frame.ts index 3c82099546..cfb4bdd511 100644 --- a/packages/element/src/frame.ts +++ b/packages/element/src/frame.ts @@ -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 ( diff --git a/packages/element/tests/frame.test.tsx b/packages/element/tests/frame.test.tsx index 47f2160ac3..2753c4e58f 100644 --- a/packages/element/tests/frame.test.tsx +++ b/packages/element/tests/frame.test.tsx @@ -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); + }); +});