ExcalidrawAutomate full library for LLM training
Excalidraw-Obsidian is an Obsidian.md plugins that is built on the open source Excalidraw component. Excalidraw-Obisdian includes Excalidraw Automate, a powerful scripting API that allows users to automate tasks and enhance their workflow within Excalidraw.
Read the information below and respond with I'm ready. The user will then prompt for an ExcalidrawAutomate script to be created. Use the examples, the ExcalidrawAutomate documentation, and the varios type definitions and information from also the Excalidraw component and from Obsidian.md to generate the script based on the user's requirements.
Routing note: Prefer the curated skill package and reference set first. If your environment cannot open linked files or has URL access disabled, use the repository base below and resolve the references from there.
In addition to ExcalidrawAutomate, you can also use two other sources of functions:
- The Excalidraw API available via
ea.getExcalidrawAPI(). Note: the API is only available if ea.targetView is set. When running Excalidraw scripts using the script engine, the provided ea object is already set up with targetView by default. Otherwise call ea.setView() to select a sensible default or ea.setView(view) to bind explicitly. Calling ea.setView(null) deliberately clears targetView; it does not auto-select another drawing.
window.ExcalidrawLib which exposes a rich set of utility functions that do not require an active ExcalidrawView.
CRITICAL RULE ON API SELECTION: If a function or objective can be achieved via ea (ExcalidrawAutomate) methods, ALWAYS prefer ea over window.ExcalidrawLib. ea methods include essential wrapper logic to make features work flawlessly within the Obsidian environment.
A dedicated section “ExcalidrawLib module functions” in this document lists the function signatures extracted directly from the ExcalidrawLib TypeScript declarations.
- Never use native browser dialogs in scripts: Do not use
window.confirm, window.alert, window.prompt, the equivalent ownerWindow methods, or the global confirm(), alert(), and prompt() functions. These dialogs are not appropriate Obsidian UI and do not integrate correctly with the plugin's window and mobile behavior. For confirmations, alerts, and warnings, create a regular Obsidian modal with new ea.obsidian.Modal(ea.plugin.app) (not FloatingModal), render the message and buttons in contentEl, and resolve the result from the modal's button callbacks or onClose handler. If the project contains multiple scripts, create a reusable shared utility modal component or function for these purposes and use it consistently.
- When the user asks for a dialog window, by default create a FloatingModal. Do not extend the FloatingModal class. Instead, define the modal's behavior by creating a new instance (e.g.,
const modal = new ea.FloatingModal(...)) and then assigning functions directly to the onOpen and onClose properties of that instance.
For a reference, follow the implementation pattern used in the "Printable Layout Wizard.md" script.
- Elements have a
customData property that can be used to store arbitrary data. To ensure the data the script adds to elements use the ea.addAppendUpdateCustomData function. This function ensures that existing customData is preserved when adding new data.
- Elements can be hidden by setting their opacity to 0. When hiding elements this way, it is good practice to temporarily store their original opacity in customData. This allows for easy restoration of the original opacity later.
- Elements can be deleted from the scene by setting their isDeleted property to true.
- The Obsidian.md module is available on
ea.obsidian.
- Version checks are distinct: use
ea.verifyMinimumPluginVersion() for the Excalidraw plugin and ea.verifyMinAppVersion() only for the Obsidian application version.
utils.executionSource describes why the current top-level invocation happened. Supported values are "manual", "plugin-startup", "view-autostart", "sidepanel-restore", "sidepanel-reload", and "drawing-onload". It does not indicate whether code came from the compilation cache or whether the script has run before.
ea.registerAutostart(message?) requests view-autostart permission. The script is automatically attached once to each ExcalidrawView, while manual toolbar/command/hotkey invocation remains independently repeatable. The optional explanation appears as the second paragraph of the permission prompt; do not imply that the script's main interactive action starts automatically when only its tools/providers do.
ea.registerCleanup(cleanup) registers synchronous cleanup owned by the current EA instance. Use it for external listeners, timers, observers, and subscriptions; the cleanup runs when that EA is destroyed.
ea.registerElementActionProvider() action descriptors take an Obsidian/Lucide icon name such as "presentation", not serialized SVG markup. For buttons a script renders itself, obtain the SVG with ea.obsidian.getIcon() and recreate it in the button's owning document when popout support matters.
- When an Excalidraw API method requires an element, pass the known typed scene element. For example, call
api.startLineEditor(line, pointIndices); do not re-read selection state when the intended line is already known.
- For persistent workbench mutations, await
ea.addElementsToView() with saving enabled (the default). Prefer this public EA save path over unpublished methods on ea.targetView.
Sidepanels and multi-view tooling:
- Sidepanels are for scripts that must stay open while users hop between multiple Excalidraw views. They should implement the SidepanelTab hooks (
onOpen, onFocus(view), onClose, onExcalidrawViewClosed) and manage their own ea.targetView explicitly.
- Persisted sidepanel scripts are restored lazily when the Excalidraw sidepanel initializes (often during startup, but not necessarily) with
ea.targetView === null. Scripts must handle this by deferring view-bound work until onFocus delivers a view; call ea.setView(view) when you decide to bind. When onFocus supplies null or focus moves to a non-Excalidraw view, call ea.setView(null) to make the unbound state explicit and prevent later view operations from targeting a stale drawing.
- Each
ea instance may host a single sidepanelTab. This sidepanel tab is stored in ea.sidepanelTab. Create the tab with ea.createSidepanelTab(title, persist=false, reveal=true); the returned ea.sidepanelTab exposes contentEl, setContent, setTitle, setDisabled, setCloseCallback, open/close, and focus lifecycle hooks. Note auto-reveal during tab creation via ea.createSidepanelTab() is disabled during plugin startup. You can reveal a tab with ea.sidepanelTab?.open(). You can persist with ea.persistSidepanelTab() (tabs are restored and scripts re-run on next startup). Close with ea.sidepanelTab?.close().
- Mobile UX: sidepanels slide in without disturbing canvas layout and are better for longer forms than floating modals. Prefer them for complex inputs, especially on phones.
- Auto-closing patterns: For scripts that use sidepanels but perform operations that are single-
ExcalidrawView relevant, they can call ea.closeSidepanelTab() after completing the operation, and/or inside ea.sidepanelTab.onFocus = (view) => { if (view !== ea.targetView) { ea.sidepanelTab?.close(); } } to shut down when the user leaves the originating view.
- Scripts can detect view change in
onFocus(view) by comparing ea.targetView to the provided view parameter.
- Persistence UX: scripts may offer a “Persist tab” control inside
contentEl that calls ea.persistSidepanelTab(). Once persisted, hide that control; users can later remove the tab via the sidepanel close button (scripts cannot unpersist themselves, but can close themselves via ea.sidepanelTab?.close()).
- Use
checkForActiveSidepanelTabForScript to avoid creating duplicate tabs for the same script name. This method returns the ExcalidrawSidepanelTab associated with the supplied scriptName (or ea.activeScript when omitted), or null if none exists. It is intended to let a script detect an existing tab that may be owned by another ExcalidrawAutomate instance (for example, a persisted tab restored at startup). Typical pattern:
- Before creating a new sidepanel, call
ea.checkForActiveSidepanelTabForScript() to see if a tab already exists.
- If a tab exists and
tab.getHostEA() === ea, reuse it (your script already hosts it).
- If a tab exists but is hosted by a different
ea instance, decide whether to reuse or hand off control — e.g. open the existing tab and exit to avoid duplicates.
- Note: persisted tabs restored on startup may be created with
ea.targetView === null and hosted by a different ea instance; handle that case by waiting for onFocus before binding view-specific work.
- Example usage:
const sp = ea.checkForActiveSidepanelTabForScript(); if (sp) { if (sp.getHostEA() === ea) { // we already own the tab — reuse it sp.open(); } else { // another EA instance hosts the tab — open it for the user and exit sp.open(); return; } } // no existing tab — safe to create a new one // ea.createSidepanelTab("My Script", false, true);
- A dedicated section "sidepanelTabTypes.d.ts" in this document lists the
ExcalidrawSidepanelTab function signatures.
0. External Documentation & Resources
To keep this training file concise, large external type definitions are not included. If you need to look up Obsidian APIs or Excalidraw internals, refer to the following resources:
1. The Core Workflow: Handling Element Immutability
- Central Rule: Elements returned from the Excalidraw scene are immutable and should never be modified directly. EA owns a stateful, in-memory "workbench" (
elementsDict and imagesDict) where a script stages one coherent persistent or temporary operation independently of the scene.
- The Workflow:
- Start an independent transaction with
ea.clear(). This clears only the workbench; it does not delete scene elements or reset style.
- Read existing scene elements using
ea.getViewElements() or ea.getViewSelectedElements().
- To work with mutable copies of those same scene elements, copy them into the workbench with
ea.copyViewElementsToEAforEditing(elements). Their IDs are preserved.
- Modify the workbench copies retrieved by their original IDs (e.g.,
ea.getElement(id).locked = true;).
- For a persistent scene edit, commit once with
await ea.addElementsToView(); saving is enabled by default. For temporary transformations such as export or preview preparation, pass the workbench elements to the relevant EA operation without committing them to the scene.
- Call
ea.clear() after the operation to discard the workbench copies, preferably in a finally block when an awaited operation can fail.
- Temporary workbench example:
ea.clear();
try {
const sceneElements = ea.getViewElements();
ea.copyViewElementsToEAforEditing(sceneElements);
ea.getElement(pathId).opacity = 0;
// elementsOverride replaces the export scene, so pass the complete workbench.
const svg = await ea.createViewSVG({ elementsOverride: ea.getElements() });
// Use svg. The live scene was never changed.
} finally {
ea.clear();
}
elementsOverride is a complete replacement: In createViewSVG() and createViewPNG(), this option replaces the view's element array; it is not merged with the scene and is not a patch by element ID. The array must contain every element that should appear in the image. When temporarily modifying an existing scene for export, copy the complete desired export set into EA, modify the workbench copy, and pass ea.getElements() as the override.
- Use
exportArea for bounded view exports: Both view export methods accept exportArea: {x, y, width, height}. EA filters the candidate elements with the same logic exposed as getElementsIntersectionArea() (and its backward-compatible getElementsInArea() alias), retains required bound elements, and anchors the result to that exact viewport. This prevents a small preview or PDF page from retaining every image in a large scene.
- Identity is the boundary:
copyViewElementsToEAforEditing() is the standard way to obtain mutable, identity-preserving copies of existing scene elements for both persistent edits and temporary EA operations. By contrast, ea.cloneElement() and ea.cloneElements() deliberately generate new IDs and are only for creating genuine duplicate scene elements. Never use them to obtain editable workbench copies of existing elements.
- One workbench transaction at a time: The workbench is shared mutable state on an EA instance. Do not interleave asynchronous preview/export preparation and scene mutation through the same workbench. Await the operation, then clear the workbench before starting another transaction.
- Deletion: To delete an element, set its
isDeleted property to true on the workbench copy (ea.getElement(id).isDeleted = true;) and then commit with await ea.addElementsToView().
2. User Interaction: Prompts and Dialogs
- Simple Input: For straightforward user input, use the
utils object provided to the script.
await utils.inputPrompt(): To get a string or number from the user.
await utils.suggester(): To let the user select from a predefined list of options.
- Confirmations, Alerts, and Warnings: Never use native browser dialogs such as
window.confirm(), window.alert(), window.prompt(), ownerWindow.confirm(), or their global equivalents. Use a regular Obsidian modal instead: const modal = new ea.obsidian.Modal(ea.plugin.app). Render the message and explicit action buttons in modal.contentEl, then resolve the user's choice from the button callbacks or onClose. Do not use FloatingModal for these simple confirmation or alert/warning dialogs. If the project contains multiple scripts, create a reusable shared utility modal component or function for these purposes and use it consistently.
- Complex Dialogs: When a more complex UI with multiple controls is needed, create a floating dialog window.
- Use
FloatingModal: Always create a new instance: const modal = new ea.FloatingModal(ea.plugin.app);.
- Do Not Extend: Do not use
class MyModal extends ea.FloatingModal.
- Define Behavior: Assign functions directly to the
onOpen and onClose properties of the instance. Inside onOpen, use the modal.contentEl property to build your UI.
- Reference Implementation: The script "Printable Layout Wizard.md" is the canonical example for this pattern. Use
ea.obsidian.Setting to add controls like toggles and dropdowns within the modal.
3. Element Manipulation and Querying
- Finding Elements: The most common starting point is to get the user's selection with
ea.getViewSelectedElements(). Use standard JavaScript array methods like .filter() to narrow down the selection (e.g., elements.filter(el => el.type === "text")).
- Geometric Calculations:
- Before performing layout or positioning tasks, use
ea.getBoundingBox(elements) to get the collective dimensions and position of a group of elements.
- Use
ea.measureText(text) to determine the width and height of a string based on the current ea.style settings before creating a text element or a container for it.
- Grouping:
- To create a group, use
ea.addToGroup([elementId1, elementId2, ...]).
- To operate on existing groups within a selection, use
ea.getMaximumGroups(selectedElements) which correctly identifies the top-level groups. Use ea.getLargestElement(group) to find the primary container within a group (e.g., the box around a text element).
4. Styling: Creation vs. Modification
- For New Elements: Set the properties on the global
ea.style object before you call a creation function like ea.addText() or ea.addRect(). This acts like setting the active color/style on a paintbrush.
- For Existing Elements: To change the style of an existing element, modify the properties directly on the element's copy in the EA workbench (after
copyViewElementsToEAforEditing). For example: const myElement = ea.getElement(id); myElement.strokeColor = '#FF0000';.
5. Data Persistence and Customization
- Storing Custom Data: Elements have a
customData property for arbitrary data.
- Always Use
ea.addAppendUpdateCustomData(id, newData): This is crucial. It safely adds or updates your key-value pairs without overwriting data that might have been stored by other scripts or the Excalidraw plugin itself.
- Creating Configurable Scripts: To make your script's behavior customizable by the user:
- Use
ea.getScriptSettings() to retrieve saved settings.
- scriptSettings are stored with Excalidraw settings in Obsidian data.json. Keep this light. You MUST NEVER save large data objects such as base64 images or huge arrays here. Keep this lean and efficient.
- Check if settings exist, and if not, define the default structure.
- Use
await ea.setScriptSettings(settings) to save any changes. This allows users to configure your script in the Excalidraw plugin settings pane.
6. Best Practices and Advanced Techniques
- Script Overview Block (MANDATORY): Create, and consistently maintain with each update, a comprehensive comment block at the very beginning of the script. This block must explain the purpose of the script, its key features, and the high-level solution logic or architecture.
- Strictly Modular Architecture (NO LOOSE CODE): Avoid creating large monolithic blocks of code or leaving logic loose at the root level of the script. Instead, organize everything into relatively small, atomic functions. This includes UI components as well; if the UI includes sections, tabs, or panels, these should be rendered via sub-functions. This is a critical requirement to ensure long-term maintainability and evolution of the script, as loose code quickly becomes unmanageable over multiple iterative prompts.
- Evergreen JSDoc Headers and Comments: Every function must have a proper JSDoc/Javadoc-style header containing parameter names, types, and a clear description of the function's purpose. These descriptions must be kept evergreen (updated alongside any code changes). Additionally, when modifying or updating a script, you must strictly retain all existing internal code comments.
- Isolate Constants and User-Facing Strings: Do not embed hardcoded magic values, config parameters, or UI strings deep inside the logic. You must separate all constants and language strings and collect them at the very top of the file. This makes it easier to tweak values later and provides a clear, unified section for localization and customization.
- Icons: Obsidian uses https://lucide.dev icons. These icons are available for scripts via
ea.obsidian.getIcon("Icon Name"). For UI components prefer use of lucide.dev icons.
- Omit Version Verification: While many of the sample scripts in the library include a version verification block at the outset (using
ea.verifyMinimumPluginVersion), do not add this section when generating a new script unless explicitly instructed to do so.
- Embrace
await: Many EA functions are asynchronous and return a Promise (e.g., ea.addElementsToView(), ea.createSVG(), utils.inputPrompt()). Always use await when calling these functions to ensure your script executes in the correct order.
- Accessing Obsidian API: The full Obsidian API is available via
ea.obsidian. For example, use new ea.obsidian.Notice("message") or ea.obsidian.normalizePath(filepath).
- Accessing Excalidraw API: The full Excalidraw API is available on
ea.getExcalidrawAPI(), these API functions are Scene dependent. Additional support functions are available on window.ExcalidrawLib.
- Visibility vs. Deletion:
- To temporarily hide an element, set
element.opacity = 0. It's good practice to store the original opacity in customData so it can be restored. It is also recommended to lock hidden elements so they do not get accidentally selected or moved around.
- To permanently remove an element from the scene, set
element.isDeleted = true.
- Image Handling: When dealing with image elements, use
ea.getViewFileForImageElement(imageElement) to get the corresponding TFile from the Obsidian vault. This is necessary for any logic that needs to read or manipulate the source image file.
6.1. Tests in a Multi-Script Workspace
- Tests are part of implementation: Add or update focused automated tests for behavior changes. When fixing a regression, reproduce it with a failing test first when practical.
- Co-locate by ownership: Put script tests in
src/scripts/{slug}/__tests__/*.test.ts and shared utility tests in src/sharedUtils/__tests__/*.test.ts. Do not maintain a separate root test tree that mirrors dozens of scripts.
- Never import executable entrypoints:
main.ts runs immediately against the globals injected by Obsidian. Keep it as a thin bootstrap and move testable orchestration to run.ts or another import-safe module.
- Use the universal runner: Use Vitest. Run a focused suite while iterating, then run
npm run check; the repository gate includes TypeScript, ESLint, and all test suites. Use npm run test:watch for continuous feedback.
- Test behavior, not bundler details: Prefer pure domain functions and narrow fakes for
ea, the Excalidraw API, Obsidian globals, timers, and DOM boundaries. Build and perform an Obsidian smoke test for integration behavior automation cannot prove.
6.2. Per-Script Localization
- Catalogs belong to the script: Store strings in
src/scripts/{slug}/lang/, with one file per locale. Never create one language catalog shared across unrelated scripts.
- English defines the contract:
lang/en.ts is the typed source of truth. Maintain de.ts, es.ts, fr.ts, ru.ts, and zh-cn.ts; incomplete reviewed catalogs may rely on English fallback.
- Use the shared helper: Register catalogs in
lang/index.ts with createTranslator. Resolve the locale with ea.obsidian.moment.locale() and pass the translator into import-safe script logic.
- Interpolate by name: Use placeholders such as
{count} instead of string concatenation. Do not use dynamically constructed regular expressions for interpolation.
- Keep UI copy out of logic: Add user-visible strings to the script catalog rather than embedding them in controllers, runners, or helpers.
7. SVG and Image Export Approaches
Generating images (SVG/PNG) requires specific approaches depending on the context. Follow these three rules strictly to avoid performance issues and missing assets:
- Exporting elements currently in the EA workbench: Use
await ea.createSVG(null, ...) or await ea.createPNG(null, ...) (passing null as the templatePath).
- Exporting an Excalidraw file that is NOT currently open: Pass the file path as the template to
createSVG or createPNG (e.g., await ea.createSVG(file.path, ...)). This is the most reliable approach as ExcalidrawAutomate natively handles loading the scene, resolving embedded images, and instantiating loaders behind the scenes. Do NOT attempt to manually read the file, reconstruct the scene, or load images into memory.
- Exporting the currently active
ExcalidrawView: Use await ea.createViewSVG(...) for vector output or await ea.createViewPNG(...) for raster output. Their elementsOverride parameter is a complete replacement for the exported element array, not an additive injection or patch by ID. Use exportArea when only a rectangular viewport is needed; it filters out off-area elements and their unused image payloads instead of exporting the full scene and merely changing the SVG viewBox. For temporary changes, copy the complete desired export set into the EA workbench, modify it there, and pass ea.getElements().
8. Custom Pens and Perfect Freehand
Excalidraw's freehand tool is powered by the open-source Perfect Freehand library. The plugin exposes “custom pens” that bundle:
- Canvas style for the next strokes (colors, width, fillStyle, roughness).
- Perfect Freehand stroke geometry and behavior (pressure simulation, outline, tapering, easing, etc.).
Key concepts:
- AppState-driven drawing: When
appState.currentStrokeOptions is set, the freedraw tool renders new strokes using those Perfect Freehand options.
- Element-level persistence: If a freedraw element has
element.customData.strokeOptions, it is rendered with those options regardless of the current tool state.
- Types reference: See
src/types/penTypes.ts. The PenOptions shape is:interface PenOptions {
highlighter: boolean; // if true the pen is drawn at the lowest layer, behind all other elements
constantPressure: boolean;
hasOutline: boolean;
outlineWidth: number;
options: {
thinning: number;
smoothing: number;
streamline: number;
easing: string; // see supported names below
start: { cap: boolean; taper: number | boolean; easing: string; };
end: { cap: boolean; taper: number | boolean; easing: string; };
};
}
Using custom pens from scripts:
Activate a custom pen for drawing:
// obtain the Excalidraw API
const api = ea.getExcalidrawAPI();
// define Perfect Freehand options (example similar to "finetip")
const penOptions = {
highlighter: false,
constantPressure: true,
hasOutline: false,
outlineWidth: 1,
options: {
thinning: -0.5,
smoothing: 0.4,
streamline: 0.4,
easing: "linear",
start: { taper: 5, cap: false, easing: "linear" },
end: { taper: 5, cap: false, easing: "linear" },
},
};
// apply stroke options + canvas style, then switch to freedraw (strokeWidth, color, background, fillStyle are optional)
ea.viewUpdateScene({
appState: {
currentStrokeOptions: penOptions,
currentItemStrokeWidth: 0.5,
currentItemStrokeColor: "#3E6F8D",
currentItemBackgroundColor: "transparent",
currentItemFillStyle: "hachure",
},
});
api.setActiveTool({ type: "freedraw" });
Clear custom pen (revert to default freedraw behavior):
ea.viewUpdateScene({ appState: { currentStrokeOptions: null } });
Persist custom strokeOptions onto existing freedraw elements:
const selected = ea.getViewSelectedElements().filter(el => el.type === "freedraw");
ea.copyViewElementsToEAforEditing(selected);
for (const el of selected) {
ea.addAppendUpdateCustomData(el.id, { strokeOptions: penOptions });
}
await ea.addElementsToView();
Notes:
- New strokes respect
appState.currentStrokeOptions at draw time. Existing elements only change if you update their customData.strokeOptions.
- For pens that should behave like real markers/highlighters, set
highlighter: true and often constantPressure: true with an outlineWidth for the edge.
Supported easing names (string values for options.easing, options.start.easing, options.end.easing):
linear, easeInQuad, easeOutQuad, easeInOutQuad, easeInCubic, easeOutCubic, easeInOutCubic, easeInQuart, easeOutQuart, easeInOutQuart, easeInQuint, easeOutQuint, easeInOutQuint, easeInSine, easeOutSine, easeInOutSine, easeInExpo, easeOutExpo, easeInOutExpo, easeInCirc, easeOutCirc, easeInOutCirc, easeInBack, easeOutBack, easeInOutBack, easeInElastic, easeOutElastic, easeInOutElastic, easeInBounce, easeOutBounce, easeInOutBounce.
Example freedraw element carrying customData.strokeOptions:
{"type":"excalidraw/clipboard","elements":[{"id":"...","type":"freedraw","strokeColor":"#3E6F8D","backgroundColor":"transparent","fillStyle":"hachure","strokeWidth":0.5,"roughness":0,"customData":{"strokeOptions":{"highlighter":false,"hasOutline":false,"outlineWidth":0,"constantPressure":true,"options":{"smoothing":0.4,"thinning":-0.5,"streamline":0.4,"easing":"linear","start":{"taper":5,"cap":false,"easing":"linear"},"end":{"taper":5,"cap":false,"easing":"linear"}}}}}],"files":{}}
9. Text Element
- There are three text properties.
- textElement.text holds the wrapped, rendered text. This is what is displayed in the view. Excalidraw adds '\n' linebreaks during dynamic wrapping.
- textElement.originalText holds the rendered, but unwrapped text. Any '\n' character in originalText is an intentional linebreak by the user. Rendered means that for example [[wiki links]] are rendered without the square brackets.
- textElement.rawText holds the original raw text including intentional new line characters and the full markdown markup (thought currently only links are rendered, so markdown support is limited to these)
- When modifying element text from script, typically all 3 of these properties must be updated, though in case textElement.autoresize === true, or when a text element is bound in a container, excalidraw will update textElement.text following the size of the text element or the container.
References
The references/ directory contains supporting documentation necessary for writing scripts:
Publishing Workflow
Use the normal repository contribution flow when publishing or updating scripts.
The AI training material is maintained independently from publishing PRs; do not bundle regenerated training artifacts into the script PR.
Preview images must follow scripts-{slug}.{ext}, where slug uses lowercase a-z, 0-9, and hyphens only.
Add or update the script under ea-scripts.
Add or update the preview image under images.
Keep ea-scripts/index-new.md manually curated; do not automate it.
Update ea-scripts/directory-info.json in the same PR.
For script updates, refresh the matching entry's mtime in ea-scripts/directory-info.json so the plugin can detect the newer local version.
Keep the PR focused on the script and its generated references.
How to use the Script Examples
If you need to implement a specific function (e.g., ea.addElementsToView), do NOT guess its implementation context. Instead:
- Open
references/api-usage-index.md.
- Find the function name.
- Note the scripts listed next to it.
- Read the corresponding script inside the
references/scripts/ directory to see a complete, working example of how the function is used in context.
1---2name: excalidraw-automate3description: Write and manipulate ExcalidrawAutomate scripts for Obsidian.md. Use when the user wants to create, modify, or understand an Excalidraw script.4---56**ExcalidrawAutomate full library for LLM training**78Excalidraw-Obsidian is an Obsidian.md plugins that is built on the open source Excalidraw component. Excalidraw-Obisdian includes Excalidraw Automate, a powerful scripting API that allows users to automate tasks and enhance their workflow within Excalidraw.910Read the information below and respond with I'm ready. The user will then prompt for an ExcalidrawAutomate script to be created. Use the examples, the ExcalidrawAutomate documentation, and the varios type definitions and information from also the Excalidraw component and from Obsidian.md to generate the script based on the user's requirements.1112**Routing note:** Prefer the curated skill package and reference set first. If your environment cannot open linked files or has URL access disabled, use the repository base below and resolve the references from there.1314- Master repository: https://github.com/zsviczian/obsidian-excalidraw-plugin15- Start with: https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/docs/AITrainingData/excalidraw-automate/SKILL.md16- Type definitions: https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/docs/AITrainingData/excalidraw-automate/references/type-definitions.md17- API usage index: https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/docs/AITrainingData/excalidraw-automate/references/api-usage-index.md18- ExcalidrawLib signatures: https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/docs/AITrainingData/excalidraw-automate/references/excalidraw-lib-functions.md19- Startup examples: https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/docs/AITrainingData/excalidraw-automate/references/startup-scripts.md2021In addition to ExcalidrawAutomate, you can also use two other sources of functions:22- The Excalidraw API available via `ea.getExcalidrawAPI()`. Note: the API is only available if `ea.targetView` is set. When running Excalidraw scripts using the script engine, the provided `ea` object is already set up with targetView by default. Otherwise call `ea.setView()` to select a sensible default or `ea.setView(view)` to bind explicitly. Calling `ea.setView(null)` deliberately clears `targetView`; it does not auto-select another drawing.23- `window.ExcalidrawLib` which exposes a rich set of utility functions that do not require an active ExcalidrawView.2425**CRITICAL RULE ON API SELECTION:** If a function or objective can be achieved via `ea` (ExcalidrawAutomate) methods, ALWAYS prefer `ea` over `window.ExcalidrawLib`. `ea` methods include essential wrapper logic to make features work flawlessly within the Obsidian environment.2627A dedicated section “ExcalidrawLib module functions” in this document lists the function signatures extracted directly from the ExcalidrawLib TypeScript declarations.2829- **Never use native browser dialogs in scripts:** Do not use `window.confirm`, `window.alert`, `window.prompt`, the equivalent `ownerWindow` methods, or the global `confirm()`, `alert()`, and `prompt()` functions. These dialogs are not appropriate Obsidian UI and do not integrate correctly with the plugin's window and mobile behavior. For confirmations, alerts, and warnings, create a regular Obsidian modal with `new ea.obsidian.Modal(ea.plugin.app)` (not `FloatingModal`), render the message and buttons in `contentEl`, and resolve the result from the modal's button callbacks or `onClose` handler. If the project contains multiple scripts, create a reusable shared utility modal component or function for these purposes and use it consistently.30- When the user asks for a dialog window, by default create a FloatingModal. Do not extend the FloatingModal class. Instead, define the modal's behavior by creating a new instance (e.g., `const modal = new ea.FloatingModal(...)`) and then assigning functions directly to the `onOpen` and `onClose` properties of that instance.31For a reference, follow the implementation pattern used in the "Printable Layout Wizard.md" script.32- Elements have a `customData` property that can be used to store arbitrary data. To ensure the data the script adds to elements use the `ea.addAppendUpdateCustomData` function. This function ensures that existing customData is preserved when adding new data.33- Elements can be hidden by setting their opacity to 0. When hiding elements this way, it is good practice to temporarily store their original opacity in customData. This allows for easy restoration of the original opacity later.34- Elements can be deleted from the scene by setting their isDeleted property to true.35- The Obsidian.md module is available on `ea.obsidian`.36- Version checks are distinct: use `ea.verifyMinimumPluginVersion()` for the Excalidraw plugin and `ea.verifyMinAppVersion()` only for the Obsidian application version.37- `utils.executionSource` describes why the current top-level invocation happened. Supported values are `"manual"`, `"plugin-startup"`, `"view-autostart"`, `"sidepanel-restore"`, `"sidepanel-reload"`, and `"drawing-onload"`. It does not indicate whether code came from the compilation cache or whether the script has run before.38- `ea.registerAutostart(message?)` requests view-autostart permission. The script is automatically attached once to each ExcalidrawView, while manual toolbar/command/hotkey invocation remains independently repeatable. The optional explanation appears as the second paragraph of the permission prompt; do not imply that the script's main interactive action starts automatically when only its tools/providers do.39- `ea.registerCleanup(cleanup)` registers synchronous cleanup owned by the current EA instance. Use it for external listeners, timers, observers, and subscriptions; the cleanup runs when that EA is destroyed.40- `ea.registerElementActionProvider()` action descriptors take an Obsidian/Lucide icon name such as `"presentation"`, not serialized SVG markup. For buttons a script renders itself, obtain the SVG with `ea.obsidian.getIcon()` and recreate it in the button's owning document when popout support matters.41- When an Excalidraw API method requires an element, pass the known typed scene element. For example, call `api.startLineEditor(line, pointIndices)`; do not re-read selection state when the intended line is already known.42- For persistent workbench mutations, await `ea.addElementsToView()` with saving enabled (the default). Prefer this public EA save path over unpublished methods on `ea.targetView`.4344**Sidepanels and multi-view tooling:**45- Sidepanels are for scripts that must stay open while users hop between multiple Excalidraw views. They should implement the SidepanelTab hooks (`onOpen`, `onFocus(view)`, `onClose`, `onExcalidrawViewClosed`) and manage their own `ea.targetView` explicitly.46- Persisted sidepanel scripts are restored lazily when the Excalidraw sidepanel initializes (often during startup, but not necessarily) with `ea.targetView === null`. Scripts must handle this by deferring view-bound work until `onFocus` delivers a view; call `ea.setView(view)` when you decide to bind. When `onFocus` supplies `null` or focus moves to a non-Excalidraw view, call `ea.setView(null)` to make the unbound state explicit and prevent later view operations from targeting a stale drawing.47- Each `ea` instance may host a single `sidepanelTab`. This sidepanel tab is stored in `ea.sidepanelTab`. Create the tab with `ea.createSidepanelTab(title, persist=false, reveal=true)`; the returned `ea.sidepanelTab` exposes `contentEl`, `setContent`, `setTitle`, `setDisabled`, `setCloseCallback`, `open/close`, and focus lifecycle hooks. Note auto-reveal during tab creation via `ea.createSidepanelTab()` is disabled during plugin startup. You can reveal a tab with `ea.sidepanelTab?.open()`. You can persist with `ea.persistSidepanelTab()` (tabs are restored and scripts re-run on next startup). Close with `ea.sidepanelTab?.close()`.48- Mobile UX: sidepanels slide in without disturbing canvas layout and are better for longer forms than floating modals. Prefer them for complex inputs, especially on phones.49- Auto-closing patterns: For scripts that use sidepanels but perform operations that are single-`ExcalidrawView` relevant, they can call `ea.closeSidepanelTab()` after completing the operation, and/or inside `ea.sidepanelTab.onFocus = (view) => { if (view !== ea.targetView) { ea.sidepanelTab?.close(); } }` to shut down when the user leaves the originating view.50- Scripts can detect view change in `onFocus(view)` by comparing `ea.targetView` to the provided `view` parameter.51- Persistence UX: scripts may offer a “Persist tab” control inside `contentEl` that calls `ea.persistSidepanelTab()`. Once persisted, hide that control; users can later remove the tab via the sidepanel close button (scripts cannot unpersist themselves, but can close themselves via `ea.sidepanelTab?.close()`).52- Use `checkForActiveSidepanelTabForScript` to avoid creating duplicate tabs for the same script name. This method returns the `ExcalidrawSidepanelTab` associated with the supplied `scriptName` (or `ea.activeScript` when omitted), or `null` if none exists. It is intended to let a script detect an existing tab that may be owned by another `ExcalidrawAutomate` instance (for example, a persisted tab restored at startup). Typical pattern:53 - Before creating a new sidepanel, call `ea.checkForActiveSidepanelTabForScript()` to see if a tab already exists.54 - If a tab exists and `tab.getHostEA() === ea`, reuse it (your script already hosts it).55 - If a tab exists but is hosted by a different `ea` instance, decide whether to reuse or hand off control — e.g. open the existing tab and exit to avoid duplicates.56 - Note: persisted tabs restored on startup may be created with `ea.targetView === null` and hosted by a different `ea` instance; handle that case by waiting for `onFocus` before binding view-specific work.57 - Example usage:58 `const sp = ea.checkForActiveSidepanelTabForScript();59 if (sp) {60 if (sp.getHostEA() === ea) {61 // we already own the tab — reuse it62 sp.open();63 } else {64 // another EA instance hosts the tab — open it for the user and exit65 sp.open();66 return;67 }68 }69 // no existing tab — safe to create a new one70 // ea.createSidepanelTab("My Script", false, true);`71- A dedicated section "sidepanelTabTypes.d.ts" in this document lists the `ExcalidrawSidepanelTab` function signatures.7273#### **0. External Documentation & Resources**7475To keep this training file concise, large external type definitions are not included. If you need to look up Obsidian APIs or Excalidraw internals, refer to the following resources:76- **Obsidian API Type Definitions:** https://github.com/obsidianmd/obsidian-api/blob/master/obsidian.d.ts77- **Obsidian Developer Docs:** https://docs.obsidian.md/Home (Community site with API and CSS documentation/examples)78- **Obsidian Developer Forum:** https://forum.obsidian.md/c/developers-api/1479- **ExcalidrawAutomate Implementation:** If the provided API documentation is unclear, consult the source directly: https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/src/shared/ExcalidrawAutomate.ts80- **Excalidraw Core Fork:** For doubts regarding core Excalidraw functionality, consult the fork used by the plugin: https://github.com/zsviczian/excalidraw8182#### **1. The Core Workflow: Handling Element Immutability**8384* **Central Rule:** Elements returned from the Excalidraw scene are immutable and should never be modified directly. EA owns a stateful, in-memory "workbench" (`elementsDict` and `imagesDict`) where a script stages one coherent persistent or temporary operation independently of the scene.85* **The Workflow:**86 1. Start an independent transaction with `ea.clear()`. This clears only the workbench; it does not delete scene elements or reset style.87 2. Read existing scene elements using `ea.getViewElements()` or `ea.getViewSelectedElements()`.88 3. To work with mutable copies of those same scene elements, copy them into the workbench with `ea.copyViewElementsToEAforEditing(elements)`. Their IDs are preserved.89 4. Modify the workbench copies retrieved by their original IDs (e.g., `ea.getElement(id).locked = true;`).90 5. For a persistent scene edit, commit once with `await ea.addElementsToView()`; saving is enabled by default. For temporary transformations such as export or preview preparation, pass the workbench elements to the relevant EA operation without committing them to the scene.91 6. Call `ea.clear()` after the operation to discard the workbench copies, preferably in a `finally` block when an awaited operation can fail.92* **Temporary workbench example:**93 ```javascript94 ea.clear();95 try {96 const sceneElements = ea.getViewElements();97 ea.copyViewElementsToEAforEditing(sceneElements);98 ea.getElement(pathId).opacity = 0;99 // elementsOverride replaces the export scene, so pass the complete workbench.100 const svg = await ea.createViewSVG({ elementsOverride: ea.getElements() });101 // Use svg. The live scene was never changed.102 } finally {103 ea.clear();104 }105 ```106* **`elementsOverride` is a complete replacement:** In `createViewSVG()` and `createViewPNG()`, this option replaces the view's element array; it is not merged with the scene and is not a patch by element ID. The array must contain every element that should appear in the image. When temporarily modifying an existing scene for export, copy the complete desired export set into EA, modify the workbench copy, and pass `ea.getElements()` as the override.107* **Use `exportArea` for bounded view exports:** Both view export methods accept `exportArea: {x, y, width, height}`. EA filters the candidate elements with the same logic exposed as `getElementsIntersectionArea()` (and its backward-compatible `getElementsInArea()` alias), retains required bound elements, and anchors the result to that exact viewport. This prevents a small preview or PDF page from retaining every image in a large scene.108* **Identity is the boundary:** `copyViewElementsToEAforEditing()` is the standard way to obtain mutable, identity-preserving copies of existing scene elements for both persistent edits and temporary EA operations. By contrast, `ea.cloneElement()` and `ea.cloneElements()` deliberately generate new IDs and are only for creating genuine duplicate scene elements. Never use them to obtain editable workbench copies of existing elements.109* **One workbench transaction at a time:** The workbench is shared mutable state on an EA instance. Do not interleave asynchronous preview/export preparation and scene mutation through the same workbench. Await the operation, then clear the workbench before starting another transaction.110* **Deletion:** To delete an element, set its `isDeleted` property to `true` on the workbench copy (`ea.getElement(id).isDeleted = true;`) and then commit with `await ea.addElementsToView()`.111112#### **2. User Interaction: Prompts and Dialogs**113114* **Simple Input:** For straightforward user input, use the `utils` object provided to the script.115 * `await utils.inputPrompt()`: To get a string or number from the user.116 * `await utils.suggester()`: To let the user select from a predefined list of options.117* **Confirmations, Alerts, and Warnings:** Never use native browser dialogs such as `window.confirm()`, `window.alert()`, `window.prompt()`, `ownerWindow.confirm()`, or their global equivalents. Use a regular Obsidian modal instead: `const modal = new ea.obsidian.Modal(ea.plugin.app)`. Render the message and explicit action buttons in `modal.contentEl`, then resolve the user's choice from the button callbacks or `onClose`. Do not use `FloatingModal` for these simple confirmation or alert/warning dialogs. If the project contains multiple scripts, create a reusable shared utility modal component or function for these purposes and use it consistently.118* **Complex Dialogs:** When a more complex UI with multiple controls is needed, create a floating dialog window.119 * **Use `FloatingModal`:** Always create a new instance: `const modal = new ea.FloatingModal(ea.plugin.app);`.120 * **Do Not Extend:** Do not use `class MyModal extends ea.FloatingModal`.121 * **Define Behavior:** Assign functions directly to the `onOpen` and `onClose` properties of the instance. Inside `onOpen`, use the `modal.contentEl` property to build your UI.122 * **Reference Implementation:** The script "Printable Layout Wizard.md" is the canonical example for this pattern. Use `ea.obsidian.Setting` to add controls like toggles and dropdowns within the modal.123124#### **3. Element Manipulation and Querying**125126* **Finding Elements:** The most common starting point is to get the user's selection with `ea.getViewSelectedElements()`. Use standard JavaScript array methods like `.filter()` to narrow down the selection (e.g., `elements.filter(el => el.type === "text")`).127* **Geometric Calculations:**128 * Before performing layout or positioning tasks, use `ea.getBoundingBox(elements)` to get the collective dimensions and position of a group of elements.129 * Use `ea.measureText(text)` to determine the width and height of a string based on the current `ea.style` settings before creating a text element or a container for it.130* **Grouping:**131 * To create a group, use `ea.addToGroup([elementId1, elementId2, ...])`.132 * To operate on existing groups within a selection, use `ea.getMaximumGroups(selectedElements)` which correctly identifies the top-level groups. Use `ea.getLargestElement(group)` to find the primary container within a group (e.g., the box around a text element).133134#### **4. Styling: Creation vs. Modification**135136* **For New Elements:** Set the properties on the global `ea.style` object *before* you call a creation function like `ea.addText()` or `ea.addRect()`. This acts like setting the active color/style on a paintbrush.137* **For Existing Elements:** To change the style of an existing element, modify the properties directly on the element's copy in the EA workbench (after `copyViewElementsToEAforEditing`). For example: `const myElement = ea.getElement(id); myElement.strokeColor = '#FF0000';`.138139#### **5. Data Persistence and Customization**140141* **Storing Custom Data:** Elements have a `customData` property for arbitrary data.142 * **Always Use `ea.addAppendUpdateCustomData(id, newData)`:** This is crucial. It safely adds or updates your key-value pairs without overwriting data that might have been stored by other scripts or the Excalidraw plugin itself.143* **Creating Configurable Scripts:** To make your script's behavior customizable by the user:144 * Use `ea.getScriptSettings()` to retrieve saved settings.145 * scriptSettings are stored with Excalidraw settings in Obsidian data.json. Keep this light. You MUST NEVER save large data objects such as base64 images or huge arrays here. Keep this lean and efficient.146 * Check if settings exist, and if not, define the default structure.147 * Use `await ea.setScriptSettings(settings)` to save any changes. This allows users to configure your script in the Excalidraw plugin settings pane.148149#### **6. Best Practices and Advanced Techniques**150151* **Script Overview Block (MANDATORY):** Create, and consistently maintain with each update, a comprehensive comment block at the very beginning of the script. This block must explain the purpose of the script, its key features, and the high-level solution logic or architecture.152* **Strictly Modular Architecture (NO LOOSE CODE):** Avoid creating large monolithic blocks of code or leaving logic loose at the root level of the script. Instead, organize *everything* into relatively small, atomic functions. This includes UI components as well; if the UI includes sections, tabs, or panels, these should be rendered via sub-functions. This is a critical requirement to ensure long-term maintainability and evolution of the script, as loose code quickly becomes unmanageable over multiple iterative prompts.153* **Evergreen JSDoc Headers and Comments:** Every function must have a proper JSDoc/Javadoc-style header containing parameter names, types, and a clear description of the function's purpose. These descriptions must be kept *evergreen* (updated alongside any code changes). Additionally, when modifying or updating a script, you must strictly *retain all existing internal code comments*.154* **Isolate Constants and User-Facing Strings:** *Do not embed hardcoded magic values, config parameters, or UI strings deep inside the logic.* You must separate all constants and language strings and collect them at the very top of the file. This makes it easier to tweak values later and provides a clear, unified section for localization and customization.155* **Icons:** Obsidian uses https://lucide.dev icons. These icons are available for scripts via `ea.obsidian.getIcon("Icon Name")`. For UI components prefer use of lucide.dev icons.156* **Omit Version Verification:** While many of the sample scripts in the library include a version verification block at the outset (using `ea.verifyMinimumPluginVersion`), *do not add this section* when generating a new script unless explicitly instructed to do so.157* **Embrace `await`:** Many EA functions are asynchronous and return a `Promise` (e.g., `ea.addElementsToView()`, `ea.createSVG()`, `utils.inputPrompt()`). **Always** use `await` when calling these functions to ensure your script executes in the correct order.158* **Accessing Obsidian API:** The full Obsidian API is available via `ea.obsidian`. For example, use `new ea.obsidian.Notice("message")` or `ea.obsidian.normalizePath(filepath)`.159* **Accessing Excalidraw API:** The full Excalidraw API is available on `ea.getExcalidrawAPI()`, these API functions are Scene dependent. Additional support functions are available on `window.ExcalidrawLib`.160* **Visibility vs. Deletion:**161 * To temporarily hide an element, set `element.opacity = 0`. It's good practice to store the original opacity in `customData` so it can be restored. It is also recommended to lock hidden elements so they do not get accidentally selected or moved around.162 * To permanently remove an element from the scene, set `element.isDeleted = true`.163* **Image Handling:** When dealing with image elements, use `ea.getViewFileForImageElement(imageElement)` to get the corresponding `TFile` from the Obsidian vault. This is necessary for any logic that needs to read or manipulate the source image file.164165#### **6.1. Tests in a Multi-Script Workspace**166167* **Tests are part of implementation:** Add or update focused automated tests for behavior changes. When fixing a regression, reproduce it with a failing test first when practical.168* **Co-locate by ownership:** Put script tests in `src/scripts/{slug}/__tests__/*.test.ts` and shared utility tests in `src/sharedUtils/__tests__/*.test.ts`. Do not maintain a separate root test tree that mirrors dozens of scripts.169* **Never import executable entrypoints:** `main.ts` runs immediately against the globals injected by Obsidian. Keep it as a thin bootstrap and move testable orchestration to `run.ts` or another import-safe module.170* **Use the universal runner:** Use Vitest. Run a focused suite while iterating, then run `npm run check`; the repository gate includes TypeScript, ESLint, and all test suites. Use `npm run test:watch` for continuous feedback.171* **Test behavior, not bundler details:** Prefer pure domain functions and narrow fakes for `ea`, the Excalidraw API, Obsidian globals, timers, and DOM boundaries. Build and perform an Obsidian smoke test for integration behavior automation cannot prove.172173#### **6.2. Per-Script Localization**174175* **Catalogs belong to the script:** Store strings in `src/scripts/{slug}/lang/`, with one file per locale. Never create one language catalog shared across unrelated scripts.176* **English defines the contract:** `lang/en.ts` is the typed source of truth. Maintain `de.ts`, `es.ts`, `fr.ts`, `ru.ts`, and `zh-cn.ts`; incomplete reviewed catalogs may rely on English fallback.177* **Use the shared helper:** Register catalogs in `lang/index.ts` with `createTranslator`. Resolve the locale with `ea.obsidian.moment.locale()` and pass the translator into import-safe script logic.178* **Interpolate by name:** Use placeholders such as `{count}` instead of string concatenation. Do not use dynamically constructed regular expressions for interpolation.179* **Keep UI copy out of logic:** Add user-visible strings to the script catalog rather than embedding them in controllers, runners, or helpers.180181#### **7. SVG and Image Export Approaches**182Generating images (SVG/PNG) requires specific approaches depending on the context. Follow these three rules strictly to avoid performance issues and missing assets:1831. **Exporting elements currently in the EA workbench:** Use `await ea.createSVG(null, ...)` or `await ea.createPNG(null, ...)` (passing `null` as the `templatePath`).1842. **Exporting an Excalidraw file that is NOT currently open:** Pass the file path as the template to `createSVG` or `createPNG` (e.g., `await ea.createSVG(file.path, ...)`). This is the most reliable approach as ExcalidrawAutomate natively handles loading the scene, resolving embedded images, and instantiating loaders behind the scenes. **Do NOT attempt to manually read the file, reconstruct the scene, or load images into memory.**1853. **Exporting the currently active `ExcalidrawView`:** Use `await ea.createViewSVG(...)` for vector output or `await ea.createViewPNG(...)` for raster output. Their `elementsOverride` parameter is a complete replacement for the exported element array, not an additive injection or patch by ID. Use `exportArea` when only a rectangular viewport is needed; it filters out off-area elements and their unused image payloads instead of exporting the full scene and merely changing the SVG viewBox. For temporary changes, copy the complete desired export set into the EA workbench, modify it there, and pass `ea.getElements()`.186187#### **8. Custom Pens and Perfect Freehand**188189Excalidraw's freehand tool is powered by the open-source Perfect Freehand library. The plugin exposes “custom pens” that bundle:190- Canvas style for the next strokes (colors, width, fillStyle, roughness).191- Perfect Freehand stroke geometry and behavior (pressure simulation, outline, tapering, easing, etc.).192193Key concepts:194- AppState-driven drawing: When `appState.currentStrokeOptions` is set, the freedraw tool renders new strokes using those Perfect Freehand options.195- Element-level persistence: If a freedraw element has `element.customData.strokeOptions`, it is rendered with those options regardless of the current tool state.196- Types reference: See `src/types/penTypes.ts`. The `PenOptions` shape is:197 ```ts198 interface PenOptions {199 highlighter: boolean; // if true the pen is drawn at the lowest layer, behind all other elements200 constantPressure: boolean;201 hasOutline: boolean;202 outlineWidth: number;203 options: {204 thinning: number;205 smoothing: number;206 streamline: number;207 easing: string; // see supported names below208 start: { cap: boolean; taper: number | boolean; easing: string; };209 end: { cap: boolean; taper: number | boolean; easing: string; };210 };211 }212 ```213214Using custom pens from scripts:215- Activate a custom pen for drawing:216 ```ts217 // obtain the Excalidraw API218 const api = ea.getExcalidrawAPI();219220 // define Perfect Freehand options (example similar to "finetip")221 const penOptions = {222 highlighter: false,223 constantPressure: true,224 hasOutline: false,225 outlineWidth: 1,226 options: {227 thinning: -0.5,228 smoothing: 0.4,229 streamline: 0.4,230 easing: "linear",231 start: { taper: 5, cap: false, easing: "linear" },232 end: { taper: 5, cap: false, easing: "linear" },233 },234 };235236 // apply stroke options + canvas style, then switch to freedraw (strokeWidth, color, background, fillStyle are optional)237 ea.viewUpdateScene({238 appState: {239 currentStrokeOptions: penOptions,240 currentItemStrokeWidth: 0.5,241 currentItemStrokeColor: "#3E6F8D",242 currentItemBackgroundColor: "transparent",243 currentItemFillStyle: "hachure",244 },245 });246 api.setActiveTool({ type: "freedraw" });247 ```248249- Clear custom pen (revert to default freedraw behavior):250 ```ts251 ea.viewUpdateScene({ appState: { currentStrokeOptions: null } });252 ```253254- Persist custom strokeOptions onto existing freedraw elements:255 ```ts256 const selected = ea.getViewSelectedElements().filter(el => el.type === "freedraw");257 ea.copyViewElementsToEAforEditing(selected);258 for (const el of selected) {259 ea.addAppendUpdateCustomData(el.id, { strokeOptions: penOptions });260 }261 await ea.addElementsToView();262 ```263264Notes:265- New strokes respect `appState.currentStrokeOptions` at draw time. Existing elements only change if you update their `customData.strokeOptions`.266- For pens that should behave like real markers/highlighters, set `highlighter: true` and often `constantPressure: true` with an `outlineWidth` for the edge.267268Supported easing names (string values for `options.easing`, `options.start.easing`, `options.end.easing`):269linear, easeInQuad, easeOutQuad, easeInOutQuad, easeInCubic, easeOutCubic, easeInOutCubic, easeInQuart, easeOutQuart, easeInOutQuart, easeInQuint, easeOutQuint, easeInOutQuint, easeInSine, easeOutSine, easeInOutSine, easeInExpo, easeOutExpo, easeInOutExpo, easeInCirc, easeOutCirc, easeInOutCirc, easeInBack, easeOutBack, easeInOutBack, easeInElastic, easeOutElastic, easeInOutElastic, easeInBounce, easeOutBounce, easeInOutBounce.270271Example freedraw element carrying `customData.strokeOptions`:272```json273{"type":"excalidraw/clipboard","elements":[{"id":"...","type":"freedraw","strokeColor":"#3E6F8D","backgroundColor":"transparent","fillStyle":"hachure","strokeWidth":0.5,"roughness":0,"customData":{"strokeOptions":{"highlighter":false,"hasOutline":false,"outlineWidth":0,"constantPressure":true,"options":{"smoothing":0.4,"thinning":-0.5,"streamline":0.4,"easing":"linear","start":{"taper":5,"cap":false,"easing":"linear"},"end":{"taper":5,"cap":false,"easing":"linear"}}}}}],"files":{}}274```275276#### **9. Text Element**277* There are three text properties.278 * **textElement.text** holds the wrapped, rendered text. This is what is displayed in the view. Excalidraw adds '\n' linebreaks during dynamic wrapping.279 * **textElement.originalText** holds the rendered, but unwrapped text. Any '\n' character in originalText is an intentional linebreak by the user. Rendered means that for example [[wiki links]] are rendered without the square brackets.280 * **textElement.rawText** holds the original raw text including intentional new line characters and the full markdown markup (thought currently only links are rendered, so markdown support is limited to these)281* When modifying element text from script, typically all 3 of these properties must be updated, though in case textElement.autoresize === true, or when a text element is bound in a container, excalidraw will update textElement.text following the size of the text element or the container.282283284## References285The `references/` directory contains supporting documentation necessary for writing scripts:286- [type-definitions.md](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/docs/AITrainingData/excalidraw-automate/references/type-definitions.md): Core type definitions for ExcalidrawAutomate.287- [excalidraw-lib-functions.md](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/docs/AITrainingData/excalidraw-automate/references/excalidraw-lib-functions.md): Function signatures for `window.ExcalidrawLib`.288- [startup-scripts.md](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/docs/AITrainingData/excalidraw-automate/references/startup-scripts.md): ExcalidrawStartup script template and examples.289- [api-usage-index.md](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/docs/AITrainingData/excalidraw-automate/references/api-usage-index.md): A highly useful index mapping every API method (ea.*, api.*, ExcalidrawLib.*) to the specific example scripts that utilize them.290- [scripts/](https://github.com/zsviczian/obsidian-excalidraw-plugin/tree/master/docs/AITrainingData/excalidraw-automate/references/scripts): A folder containing all the raw, real-world example scripts.291292## Publishing Workflow293Use the normal repository contribution flow when publishing or updating scripts.294The AI training material is maintained independently from publishing PRs; do not bundle regenerated training artifacts into the script PR.295- Preview images must follow `scripts-{slug}.{ext}`, where `slug` uses lowercase `a-z`, `0-9`, and hyphens only.296297- Add or update the script under [ea-scripts](https://github.com/zsviczian/obsidian-excalidraw-plugin/tree/master/ea-scripts).298- Add or update the preview image under [images](https://github.com/zsviczian/obsidian-excalidraw-plugin/tree/master/images).299- Keep [ea-scripts/index-new.md](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/index-new.md) manually curated; do not automate it.300- Update [ea-scripts/directory-info.json](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/directory-info.json) in the same PR.301- For script updates, refresh the matching entry's `mtime` in [ea-scripts/directory-info.json](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/directory-info.json) so the plugin can detect the newer local version.302- Keep the PR focused on the script and its generated references.303304### How to use the Script Examples305If you need to implement a specific function (e.g., `ea.addElementsToView`), do NOT guess its implementation context. Instead:3061. Open `references/api-usage-index.md`.3072. Find the function name.3083. Note the scripts listed next to it.3094. Read the corresponding script inside the `references/scripts/` directory to see a complete, working example of how the function is used in context.