/*

```js*/
let dirty=false;
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.23.0")) {
new Notice("This script requires Excalidraw 2.23.0 or later. Please update the plugin.");
return;
}
const EXCALIAI_SETTINGS_VERSION = 1;
const DEFAULT_IMAGE_SIZE = "1024x1024";
const TASK_EXECUTION_MODES = {
TEXT_RESULT: "text-result",
IMAGE_PROMPT: "image-prompt",
IMAGE_DIRECT: "image-direct",
IMAGE_EDIT: "image-edit",
};
const TASK_RESULT_TYPES = {
HTML: "html",
MINDMAP: "mindmap",
MERMAID: "mermaid",
SVG: "svg",
IMAGE: "image",
IMAGE_SILENT: "image-silent",
};
const TASK_INPUT_RULES = {
DISABLED: "disabled",
OPTIONAL: "optional",
REQUIRED: "required",
};
const TASK_MASK_MODES = {
DISABLED: "disabled",
OPTIONAL: "optional",
REQUIRED: "required",
};
const TASK_RUNTIME_APIS = {
NONE: "",
MINDMAP_BUILDER: "mindmap-builder",
};
const VALID_TASK_EXECUTION_MODES = Object.values(TASK_EXECUTION_MODES);
const VALID_TASK_RESULT_TYPES = Object.values(TASK_RESULT_TYPES);
const VALID_TASK_INPUT_RULES = Object.values(TASK_INPUT_RULES);
const VALID_TASK_MASK_MODES = Object.values(TASK_MASK_MODES);
const VALID_TASK_RUNTIME_APIS = Object.values(TASK_RUNTIME_APIS);
const TASK_EXECUTION_MODE_META = {
[TASK_EXECUTION_MODES.TEXT_RESULT]: {
label: "Text response",
description: "Uses the text or multimodal model and returns structured output such as HTML, Mermaid, a mind map, or Excalidraw strokes.",
},
[TASK_EXECUTION_MODES.IMAGE_PROMPT]: {
label: "Write prompt, then generate image",
description: "Uses the text model to write an image prompt from the canvas selection and/or user prompt, then sends that prompt to the image model.",
},
[TASK_EXECUTION_MODES.IMAGE_DIRECT]: {
label: "Send prompt straight to image model",
description: "Sends only the user's prompt to the image model. No text-model prompt writing step and no canvas image input.",
},
[TASK_EXECUTION_MODES.IMAGE_EDIT]: {
label: "Edit selected image",
description: "Uses the selected image as the source and applies either a mask edit or a prompt-based transform.",
},
};
const TASK_RESULT_TYPE_META = {
[TASK_RESULT_TYPES.HTML]: {
label: "HTML",
description: "Embeds the response as a single HTML result.",
},
[TASK_RESULT_TYPES.MINDMAP]: {
label: "Mind Map",
description: "Imports the response into MindMap Builder.",
runtimeRequirement: TASK_RUNTIME_APIS.MINDMAP_BUILDER,
},
[TASK_RESULT_TYPES.MERMAID]: {
label: "Mermaid",
description: "Creates a Mermaid diagram.",
},
[TASK_RESULT_TYPES.SVG]: {
label: "Excalidraw Strokes",
description: "Uses SVG behind the scenes to generate Excalidraw strokes.",
},
[TASK_RESULT_TYPES.IMAGE]: {
label: "Image + prompt note",
description: "Generates an image and adds the model's revised prompt underneath when available.",
},
[TASK_RESULT_TYPES.IMAGE_SILENT]: {
label: "Image only",
description: "Generates only the image, without adding the revised prompt underneath.",
},
};
const getTaskExecutionModeMeta = (mode) => (
TASK_EXECUTION_MODE_META[mode] ?? TASK_EXECUTION_MODE_META[TASK_EXECUTION_MODES.TEXT_RESULT]
);
const getTaskExecutionModeLabel = (mode) => (
getTaskExecutionModeMeta(mode).label
);
const getTaskExecutionModeDescription = (mode) => (
getTaskExecutionModeMeta(mode).description
);
const getTaskResultTypeMeta = (resultType) => (
TASK_RESULT_TYPE_META[resultType] ?? TASK_RESULT_TYPE_META[TASK_RESULT_TYPES.HTML]
);
const getTaskResultTypeLabel = (resultType) => (
getTaskResultTypeMeta(resultType).label
);
const getTaskResultTypeDescription = (resultType) => (
getTaskResultTypeMeta(resultType).description
);
const getTaskRuntimeRequirement = (taskConfig) => (
getTaskResultTypeMeta(taskConfig?.execution?.resultType ?? TASK_RESULT_TYPES.HTML).runtimeRequirement
?? TASK_RUNTIME_APIS.NONE
);
const getTaskRuntimeRequirementLabel = (runtimeRequirement) => {
switch(runtimeRequirement) {
case TASK_RUNTIME_APIS.MINDMAP_BUILDER:
return "MindMap Builder";
default:
return "None";
}
};
const normalizeEnumValue = (value, validValues, fallbackValue) => (
validValues.includes(value) ? value : fallbackValue
);
const cloneJSON = (value) => {
if(value == null) {
return value;
}
return JSON.parse(JSON.stringify(value));
};
const createTaskIdFromName = (value = "") => {
const normalizedValue = String(value ?? "")
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
return normalizedValue || "task";
};
const getDefaultOutputInstruction = (resultType) => {
switch(resultType) {
case TASK_RESULT_TYPES.HTML:
return "Turn this into a single html file using tailwind. Return a single message containing only the html file in a codeblock.";
case TASK_RESULT_TYPES.MINDMAP:
return "Return only the mind map as plain text. Use one # heading for the central node, then nested - bullets for branches. Do not use bold, italics, code fences, tables, or explanatory text.";
case TASK_RESULT_TYPES.MERMAID:
return "Return a single message containing only the mermaid diagram in a codeblock.";
case TASK_RESULT_TYPES.SVG:
return "Return a single message containing only the SVG code in an html codeblock.";
case TASK_RESULT_TYPES.IMAGE:
case TASK_RESULT_TYPES.IMAGE_SILENT:
return "Return a single message with the generated image prompt in a codeblock";
default:
return "";
}
};
const getDefaultResultTypeForMode = (mode) => {
switch(mode) {
case TASK_EXECUTION_MODES.IMAGE_PROMPT:
case TASK_EXECUTION_MODES.IMAGE_DIRECT:
case TASK_EXECUTION_MODES.IMAGE_EDIT:
return TASK_RESULT_TYPES.IMAGE;
default:
return TASK_RESULT_TYPES.HTML;
}
};
const normalizeTaskConfig = (task = {}, index = 0) => {
const execution = task.execution ?? {};
const mode = normalizeEnumValue(
execution.mode,
VALID_TASK_EXECUTION_MODES,
TASK_EXECUTION_MODES.TEXT_RESULT,
);
const resultType = normalizeEnumValue(
execution.resultType,
VALID_TASK_RESULT_TYPES,
getDefaultResultTypeForMode(mode),
);
const imageInputFallback = mode === TASK_EXECUTION_MODES.IMAGE_DIRECT
? TASK_INPUT_RULES.DISABLED
: mode === TASK_EXECUTION_MODES.IMAGE_EDIT
? TASK_INPUT_RULES.REQUIRED
: TASK_INPUT_RULES.OPTIONAL;
return {
id: createTaskIdFromName(task.id ?? task.name ?? `task-${index + 1}`),
name: String(task.name ?? "").trim() || `Task ${index + 1}`,
help: String(task.help ?? "").trim(),
systemPrompt: task.systemPrompt == null ? null : String(task.systemPrompt),
outputInstruction: String(task.outputInstruction ?? getDefaultOutputInstruction(resultType)),
execution: {
mode,
resultType,
userPrompt: normalizeEnumValue(
execution.userPrompt,
VALID_TASK_INPUT_RULES,
TASK_INPUT_RULES.OPTIONAL,
),
imageInput: normalizeEnumValue(
execution.imageInput,
VALID_TASK_INPUT_RULES,
imageInputFallback,
),
maskMode: normalizeEnumValue(
execution.maskMode,
VALID_TASK_MASK_MODES,
mode === TASK_EXECUTION_MODES.IMAGE_EDIT
? TASK_MASK_MODES.OPTIONAL
: TASK_MASK_MODES.DISABLED,
),
requiresApi: getTaskResultTypeMeta(resultType).runtimeRequirement ?? TASK_RUNTIME_APIS.NONE,
},
};
};
const normalizeTaskConfigs = (tasks, {fallbackToDefaults = false} = {}) => {
if(!Array.isArray(tasks)) {
return fallbackToDefaults ? normalizeTaskConfigs(createDefaultTaskConfigs()) : [];
}
const usedIds = new Set();
return tasks.map((task, index) => {
const normalizedTask = normalizeTaskConfig(task, index);
if(normalizedTask.execution.mode !== TASK_EXECUTION_MODES.IMAGE_EDIT) {
normalizedTask.execution.maskMode = TASK_MASK_MODES.DISABLED;
}
let nextId = normalizedTask.id || createTaskIdFromName(normalizedTask.name) || `task-${index + 1}`;
let duplicateCount = 2;
while(usedIds.has(nextId)) {
nextId = `${normalizedTask.id}-${duplicateCount++}`;
}
usedIds.add(nextId);
normalizedTask.id = nextId;
return normalizedTask;
});
};
const createDefaultTaskConfigs = () => ([
{
id: "challenge-my-thinking",
name: "Challenge my thinking",
help: "Turn the selected image and optional prompt into a Mermaid mind map. If conversion fails, open More Tools > Mermaid to Excalidraw and edit the generated script.",
systemPrompt: `Your task is to interpret a screenshot of a whiteboard, translating its ideas into a Mermaid graph. The whiteboard will encompass thoughts on a subject. Within the mind map, distinguish ideas that challenge, dispute, or contradict the whiteboard content. Additionally, include concepts that expand, complement, or advance the user's thinking. Utilize the Mermaid graph diagram type and present the resulting Mermaid diagram within a code block. Ensure the Mermaid script excludes the use of parentheses ().`,
outputInstruction: getDefaultOutputInstruction(TASK_RESULT_TYPES.MERMAID),
execution: {
mode: TASK_EXECUTION_MODES.TEXT_RESULT,
resultType: TASK_RESULT_TYPES.MERMAID,
userPrompt: TASK_INPUT_RULES.OPTIONAL,
imageInput: TASK_INPUT_RULES.OPTIONAL,
maskMode: TASK_MASK_MODES.DISABLED,
requiresApi: TASK_RUNTIME_APIS.NONE,
},
},
{
id: "convert-sketch-to-shapes",
name: "Convert sketch to shapes",
help: "Convert selected sketches into Excalidraw strokes. Works best with a small number of simple shapes. Experimental.",
systemPrompt: `Given an image featuring various geometric shapes drawn by the user, your objective is to analyze the input and generate SVG code that accurately represents these shapes. Your output will be the SVG code enclosed in an HTML code block.`,
outputInstruction: getDefaultOutputInstruction(TASK_RESULT_TYPES.SVG),
execution: {
mode: TASK_EXECUTION_MODES.TEXT_RESULT,
resultType: TASK_RESULT_TYPES.SVG,
userPrompt: TASK_INPUT_RULES.OPTIONAL,
imageInput: TASK_INPUT_RULES.OPTIONAL,
maskMode: TASK_MASK_MODES.DISABLED,
requiresApi: TASK_RUNTIME_APIS.NONE,
},
},
{
id: "create-a-simple-excalidraw-icon",
name: "Create a simple Excalidraw icon",
help: "Turn a text prompt into a simple icon and insert it into Excalidraw as strokes. Text prompt only. Experimental.",
systemPrompt: `Given a description of an SVG image from the user, your objective is to generate the corresponding SVG code. Avoid incorporating textual elements within the generated SVG. Your output should be the resulting SVG code enclosed in an HTML code block.`,
outputInstruction: getDefaultOutputInstruction(TASK_RESULT_TYPES.SVG),
execution: {
mode: TASK_EXECUTION_MODES.TEXT_RESULT,
resultType: TASK_RESULT_TYPES.SVG,
userPrompt: TASK_INPUT_RULES.OPTIONAL,
imageInput: TASK_INPUT_RULES.DISABLED,
maskMode: TASK_MASK_MODES.DISABLED,
requiresApi: TASK_RUNTIME_APIS.NONE,
},
},
{
id: "create-a-stick-figure",
name: "Create a stick figure",
help: "Send only the text prompt to the configured image model. Be specific. To keep the prompt unchanged, start with: 'DO NOT add any detail, just use it AS-IS:'",
systemPrompt: "You will receive a prompt from the user. Your task involves drawing a simple stick figure or a scene involving a few stick figures based on the user's prompt. Create the stick figure based on the following style description. DO NOT add any detail, just use it AS-IS: Create a simple stick figure character with a large round head and a face in the style of sketchy caricatures. The stick figure should have a rudimentary body composed of straight lines representing the arms and legs. Hands and toes should be represented with round shapes, do not add details such as fingers or toes. Use fine lines, smooth curves, rounded shapes. The stick figure should retain a playful and childlike simplicity, reminiscent of a doodle someone might draw on the corner of a notebook page. Create a black and white drawing, a hand-drawn figure on white background.",
outputInstruction: getDefaultOutputInstruction(TASK_RESULT_TYPES.IMAGE),
execution: {
mode: TASK_EXECUTION_MODES.IMAGE_PROMPT,
resultType: TASK_RESULT_TYPES.IMAGE,
userPrompt: TASK_INPUT_RULES.OPTIONAL,
imageInput: TASK_INPUT_RULES.DISABLED,
maskMode: TASK_MASK_MODES.DISABLED,
requiresApi: TASK_RUNTIME_APIS.NONE,
},
},
{
id: "edit-an-image",
name: "Edit an image",
help: "Image elements are used as the source image. In mask mode, shapes on top become the mask. Turn mask edit off to flatten non-image elements into the source image and apply a prompt-based transform.",
systemPrompt: null,
outputInstruction: "",
execution: {
mode: TASK_EXECUTION_MODES.IMAGE_EDIT,
resultType: TASK_RESULT_TYPES.IMAGE,
userPrompt: TASK_INPUT_RULES.REQUIRED,
imageInput: TASK_INPUT_RULES.REQUIRED,
maskMode: TASK_MASK_MODES.OPTIONAL,
requiresApi: TASK_RUNTIME_APIS.NONE,
},
},
{
id: "generate-an-image-from-image-and-prompt",
name: "Generate an image from image and prompt",
help: "Generate an image from the selected image and your prompt. Add context in the prompt to guide how the image should be interpreted.",
systemPrompt: "Your task involves receiving an image and a textual prompt from the user. Your goal is to craft a detailed, accurate, and descriptive narrative of the image, tailored for effective image generation. Utilize the user-provided text prompt to inform and guide your depiction of the image. Ensure the resulting image remains text-free.",
outputInstruction: getDefaultOutputInstruction(TASK_RESULT_TYPES.IMAGE),
execution: {
mode: TASK_EXECUTION_MODES.IMAGE_PROMPT,
resultType: TASK_RESULT_TYPES.IMAGE,
userPrompt: TASK_INPUT_RULES.OPTIONAL,
imageInput: TASK_INPUT_RULES.OPTIONAL,
maskMode: TASK_MASK_MODES.DISABLED,
requiresApi: TASK_RUNTIME_APIS.NONE,
},
},
{
id: "generate-an-image-from-prompt",
name: "Generate an image from prompt",
help: "Send only the text prompt to the configured image model. Be specific. To keep the prompt unchanged, start with: 'DO NOT add any detail, just use it AS-IS:'",
systemPrompt: null,
outputInstruction: getDefaultOutputInstruction(TASK_RESULT_TYPES.IMAGE),
execution: {
mode: TASK_EXECUTION_MODES.IMAGE_DIRECT,
resultType: TASK_RESULT_TYPES.IMAGE,
userPrompt: TASK_INPUT_RULES.OPTIONAL,
imageInput: TASK_INPUT_RULES.DISABLED,
maskMode: TASK_MASK_MODES.DISABLED,
requiresApi: TASK_RUNTIME_APIS.NONE,
},
},
{
id: "generate-an-image-to-illustrate-a-quote",
name: "Generate an image to illustrate a quote",
help: "Turn a quote into an illustrated scene. Include the author's name if you want the result to reference them.",
systemPrompt: "Your task involves transforming a user-provided quote into a detailed and imaginative illustration. Craft a visual representation that captures the essence of the quote and resonates well with a broad audience. If the Author's name is provided, aim to establish a connection between the illustration and the Author. This can be achieved by referencing a well-known story from the Author, situating the image in the Author's era or setting, or employing other creative methods of association. Additionally, provide preferences for styling, such as the chosen medium and artistic direction, to guide the image creation process. Ensure the resulting image remains text-free. Your task output should comprise a descriptive and detailed narrative aimed at facilitating the creation of a captivating illustration from the quote.",
outputInstruction: getDefaultOutputInstruction(TASK_RESULT_TYPES.IMAGE),
execution: {
mode: TASK_EXECUTION_MODES.IMAGE_PROMPT,
resultType: TASK_RESULT_TYPES.IMAGE,
userPrompt: TASK_INPUT_RULES.OPTIONAL,
imageInput: TASK_INPUT_RULES.DISABLED,
maskMode: TASK_MASK_MODES.DISABLED,
requiresApi: TASK_RUNTIME_APIS.NONE,
},
},
{
id: "generate-4-icon-variants-based-on-input-image",
name: "Generate 4 icon-variants based on input image",
help: "Generate a 2x2 sheet of four icon variations from the selected sketch. Add a prompt if you want to steer the result.",
systemPrompt: "Given a simple sketch and an optional text prompt from the user, your task is to generate a descriptive narrative tailored for effective image generation, capturing the style of the sketch. Utilize the text prompt to guide the description. Your objective is to instruct DALL-E to create a collage of four minimalist black and white hand-drawn pencil sketches in a 2x2 matrix format. Each sketch should convert the user's sketch into simple artistic SVG icons with transparent backgrounds. Ensure the resulting images remain text-free, maintaining a minimalist, easy-to-understand style, and omit framing borders. Only include a pencil in the drawing if it is explicitly mentioned in the user prompt or included in the sketch.",
outputInstruction: getDefaultOutputInstruction(TASK_RESULT_TYPES.IMAGE_SILENT),
execution: {
mode: TASK_EXECUTION_MODES.IMAGE_PROMPT,
resultType: TASK_RESULT_TYPES.IMAGE_SILENT,
userPrompt: TASK_INPUT_RULES.OPTIONAL,
imageInput: TASK_INPUT_RULES.OPTIONAL,
maskMode: TASK_MASK_MODES.DISABLED,
requiresApi: TASK_RUNTIME_APIS.NONE,
},
},
{
id: "visual-brainstorm",
name: "Visual brainstorm",
help: "Generate an image from the selected image and prompt to spark new ideas.",
systemPrompt: "Your objective is to interpret a screenshot of a whiteboard, creating an image aimed at sparking further thoughts on the subject. The whiteboard will present diverse ideas about a specific topic. Your generated image should achieve one of two purposes: highlighting concepts that challenge, dispute, or contradict the whiteboard content, or introducing ideas that expand, complement, or enrich the user's thinking. You have the option to include multiple tiles in the resulting image, resembling a sequence akin to a comic strip. Ensure that the image remains devoid of text.",
outputInstruction: getDefaultOutputInstruction(TASK_RESULT_TYPES.IMAGE),
execution: {
mode: TASK_EXECUTION_MODES.IMAGE_PROMPT,
resultType: TASK_RESULT_TYPES.IMAGE,
userPrompt: TASK_INPUT_RULES.OPTIONAL,
imageInput: TASK_INPUT_RULES.OPTIONAL,
maskMode: TASK_MASK_MODES.DISABLED,
requiresApi: TASK_RUNTIME_APIS.NONE,
},
},
{
id: "wireframe-to-code",
name: "Wireframe to code",
help: "Interpret the selected wireframe and generate a web app as a single HTML file. You can copy the result from the embeddable menu.",
systemPrompt: `You are an expert tailwind developer. A user will provide you with a low-fidelity wireframe of an application and you will return a single html file that uses tailwind to create the website. Use creative license to make the application more fleshed out. Write the necessary javascript code. If you need to insert an image, use placehold.co to create a placeholder image.`,
outputInstruction: getDefaultOutputInstruction(TASK_RESULT_TYPES.HTML),
execution: {
mode: TASK_EXECUTION_MODES.TEXT_RESULT,
resultType: TASK_RESULT_TYPES.HTML,
userPrompt: TASK_INPUT_RULES.OPTIONAL,
imageInput: TASK_INPUT_RULES.OPTIONAL,
maskMode: TASK_MASK_MODES.DISABLED,
requiresApi: TASK_RUNTIME_APIS.NONE,
},
},
{
id: "create-mindmap",
name: "Create Mindmap",
help: "Create a hierarchical mind map from the selected image, if any, and your prompt, then import it into MindMap Builder. Requires MindMap Builder to be available.",
systemPrompt: "You will receive a text prompt and may also receive an image. Create a mind map as a hierarchical plain-text outline based on the image content, if provided, and the text prompt. Return only the mind map. Use exactly one markdown H1 heading for the central node, then - bullets for branches and indented - bullets for sub-branches. Do not use bold, italics, code fences, numbering, commentary, or any markdown formatting other than the heading and bullet list.",
outputInstruction: getDefaultOutputInstruction(TASK_RESULT_TYPES.MINDMAP),
execution: {
mode: TASK_EXECUTION_MODES.TEXT_RESULT,
resultType: TASK_RESULT_TYPES.MINDMAP,
userPrompt: TASK_INPUT_RULES.OPTIONAL,
imageInput: TASK_INPUT_RULES.OPTIONAL,
maskMode: TASK_MASK_MODES.DISABLED,
requiresApi: TASK_RUNTIME_APIS.MINDMAP_BUILDER,
},
},
]);
const DEFAULT_TASK_CONFIGS = normalizeTaskConfigs(createDefaultTaskConfigs());
const DEFAULT_TASK_ID = DEFAULT_TASK_CONFIGS.find(task => task.id === "wireframe-to-code")?.id
?? DEFAULT_TASK_CONFIGS[0]?.id
?? "";
const createDefaultState = (taskConfigs = []) => ({
selectedTaskId: taskConfigs.find(task => task.id === DEFAULT_TASK_ID)?.id ?? taskConfigs[0]?.id ?? "",
userPrompt: "",
maskEdit: true,
textModel: "",
imageModel: "",
maxTokens: "",
imageSize: DEFAULT_IMAGE_SIZE,
});
const normalizeState = (state = {}, taskConfigs = []) => {
const defaultState = createDefaultState(taskConfigs);
const knownTaskIds = new Set(taskConfigs.map(task => task.id));
const selectedTaskId = String(state.selectedTaskId ?? defaultState.selectedTaskId).trim();
return {
selectedTaskId: knownTaskIds.has(selectedTaskId) ? selectedTaskId : defaultState.selectedTaskId,
userPrompt: String(state.userPrompt ?? defaultState.userPrompt),
maskEdit: state.maskEdit !== false,
textModel: String(state.textModel ?? defaultState.textModel),
imageModel: String(state.imageModel ?? defaultState.imageModel),
maxTokens: String(state.maxTokens ?? defaultState.maxTokens).trim(),
imageSize: String(state.imageSize ?? defaultState.imageSize).trim() || defaultState.imageSize,
};
};
const createDefaultExcaliAISettings = () => ({
schemaVersion: EXCALIAI_SETTINGS_VERSION,
config: {
tasks: cloneJSON(DEFAULT_TASK_CONFIGS),
},
state: createDefaultState(DEFAULT_TASK_CONFIGS),
});
const LEGACY_TASK_NAME_TO_ID = Object.fromEntries(
DEFAULT_TASK_CONFIGS.map(task => [task.name, task.id]),
);
const loadExcaliAISettings = (rawSettings) => {
const sourceSettings = rawSettings && typeof rawSettings === "object" ? rawSettings : {};
const normalizedSettings = createDefaultExcaliAISettings();
normalizedSettings.config.tasks = normalizeTaskConfigs(sourceSettings.config?.tasks, {fallbackToDefaults: true});
const legacyState = {
selectedTaskId: LEGACY_TASK_NAME_TO_ID[String(sourceSettings["Agent's Task"] ?? "").trim()] ?? normalizedSettings.state.selectedTaskId,
userPrompt: sourceSettings["User Prompt"] ?? normalizedSettings.state.userPrompt,
maskEdit: sourceSettings["Mask Edit"] !== false,
textModel: sourceSettings["Text Model"] ?? normalizedSettings.state.textModel,
imageModel: sourceSettings["Image Model"] ?? normalizedSettings.state.imageModel,
maxTokens: sourceSettings["Max Tokens"] ?? normalizedSettings.state.maxTokens,
imageSize: sourceSettings["Image Size"] ?? normalizedSettings.state.imageSize,
};
normalizedSettings.state = normalizeState({
...legacyState,
...(sourceSettings.state ?? {}),
}, normalizedSettings.config.tasks);
return {
settings: normalizedSettings,
needsSave: JSON.stringify(sourceSettings) !== JSON.stringify(normalizedSettings),
};
};
// --------------------------------------
// Initialize values and settings
// --------------------------------------
let settings = ea.getScriptSettings();
const loadedExcaliAISettings = loadExcaliAISettings(settings);
settings = loadedExcaliAISettings.settings;
if(loadedExcaliAISettings.needsSave) {
await ea.setScriptSettings(settings);
}
let userPrompt = settings.state.userPrompt ?? "";
let selectedTaskId = settings.state.selectedTaskId;
let imageSize = settings.state.imageSize ?? DEFAULT_IMAGE_SIZE;
let selectedTextModel = settings.state.textModel ?? "";
let selectedImageModel = settings.state.imageModel ?? "";
let selectedMaxTokens = String(settings.state.maxTokens ?? "").trim();
let prefersMaskEdit = settings.state.maskEdit !== false;
const aiSettings = ea.getAISettings();
if(!aiSettings?.enabled) {
new Notice("Excalidraw AI is disabled or unavailable. Enable it in plugin settings.");
return;
}
let textModel, imageModel, validSizes;
let imageDataURL = null;
let maskDataURL = null;
const parsePositiveInteger = (value) => {
const normalizedValue = String(value ?? "").trim();
if(!normalizedValue) {
return null;
}
const parsedValue = parseInt(normalizedValue, 10);
if(Number.isNaN(parsedValue) || parsedValue <= 0) {
return null;
}
return parsedValue;
};
const getTaskConfigs = () => settings.config?.tasks ?? [];
const isTaskRuntimeAvailable = (taskConfig) => {
switch(getTaskRuntimeRequirement(taskConfig)) {
case TASK_RUNTIME_APIS.MINDMAP_BUILDER:
return Boolean(window?.MindMapBuilderAPI);
default:
return true;
}
};
const getVisibleTaskConfigs = () => getTaskConfigs().filter(isTaskRuntimeAvailable);
const getTaskConfigById = (taskId = selectedTaskId) => (
getTaskConfigs().find(taskConfig => taskConfig.id === taskId) ?? null
);
const getVisibleTaskConfigById = (taskId = selectedTaskId) => (
getVisibleTaskConfigs().find(taskConfig => taskConfig.id === taskId) ?? null
);
const ensureSelectedTaskId = () => {
const activeVisibleTask = getVisibleTaskConfigById(selectedTaskId);
const fallbackTask = activeVisibleTask ?? getVisibleTaskConfigs()[0] ?? null;
const nextTaskId = fallbackTask?.id ?? "";
if(selectedTaskId !== nextTaskId) {
selectedTaskId = nextTaskId;
dirty = true;
}
return fallbackTask;
};
const getActiveTaskConfig = () => ensureSelectedTaskId();
const getTaskExecutionConfig = (taskId = selectedTaskId) => (
getTaskConfigById(taskId)?.execution ?? null
);
const getTaskOutputType = (taskId = selectedTaskId) => {
const taskConfig = typeof taskId === "string" ? getTaskConfigById(taskId) : taskId;
return {
instruction: taskConfig?.outputInstruction ?? "",
blocktype: taskConfig?.execution?.resultType ?? TASK_RESULT_TYPES.HTML,
};
};
const isImageEditTask = (taskId = selectedTaskId) => (
getTaskExecutionConfig(taskId)?.mode === TASK_EXECUTION_MODES.IMAGE_EDIT
);
const isImageGenerationTask = (taskId = selectedTaskId) => {
const mode = getTaskExecutionConfig(taskId)?.mode;
return mode === TASK_EXECUTION_MODES.IMAGE_PROMPT
|| mode === TASK_EXECUTION_MODES.IMAGE_DIRECT
|| mode === TASK_EXECUTION_MODES.IMAGE_EDIT;
};
const doesTaskUseTextModel = (taskId = selectedTaskId) => {
const mode = getTaskExecutionConfig(taskId)?.mode;
return mode === TASK_EXECUTION_MODES.TEXT_RESULT || mode === TASK_EXECUTION_MODES.IMAGE_PROMPT;
};
const doesTaskAllowUserPrompt = (taskId = selectedTaskId) => (
getTaskExecutionConfig(taskId)?.userPrompt !== TASK_INPUT_RULES.DISABLED
);
const taskRequiresUserPrompt = (taskId = selectedTaskId) => (
getTaskExecutionConfig(taskId)?.userPrompt === TASK_INPUT_RULES.REQUIRED
);
const doesTaskAllowImageInput = (taskId = selectedTaskId) => (
getTaskExecutionConfig(taskId)?.imageInput !== TASK_INPUT_RULES.DISABLED
);
const taskRequiresImageInput = (taskId = selectedTaskId) => (
getTaskExecutionConfig(taskId)?.imageInput === TASK_INPUT_RULES.REQUIRED
);
const taskUsesDirectImageModel = (taskId = selectedTaskId) => (
getTaskExecutionConfig(taskId)?.mode === TASK_EXECUTION_MODES.IMAGE_DIRECT
);
const taskUsesImagePromptPipeline = (taskId = selectedTaskId) => (
getTaskExecutionConfig(taskId)?.mode === TASK_EXECUTION_MODES.IMAGE_PROMPT
);
const getTaskMaskMode = (taskId = selectedTaskId) => (
getTaskExecutionConfig(taskId)?.maskMode ?? TASK_MASK_MODES.DISABLED
);
const getTaskConfigValidationMessage = (taskConfig = getActiveTaskConfig()) => {
if(!taskConfig) {
return "No runnable AI tasks are configured. Open Task Editor to add a task or reset the shipped presets.";
}
const {mode, resultType, maskMode} = taskConfig.execution;
const isImageResultType = resultType === TASK_RESULT_TYPES.IMAGE || resultType === TASK_RESULT_TYPES.IMAGE_SILENT;
if(mode === TASK_EXECUTION_MODES.TEXT_RESULT && isImageResultType) {
return `Task \"${taskConfig.name}\" uses an image result with ${getTaskExecutionModeLabel(TASK_EXECUTION_MODES.TEXT_RESULT)}. Use ${getTaskExecutionModeLabel(TASK_EXECUTION_MODES.IMAGE_PROMPT)} or ${getTaskExecutionModeLabel(TASK_EXECUTION_MODES.IMAGE_DIRECT)} instead.`;
}
if(mode === TASK_EXECUTION_MODES.IMAGE_PROMPT && !isImageResultType) {
return `Task \"${taskConfig.name}\" must use an image result when ${getTaskExecutionModeLabel(TASK_EXECUTION_MODES.IMAGE_PROMPT)} is selected.`;
}
if(mode === TASK_EXECUTION_MODES.IMAGE_DIRECT && !isImageResultType) {
return `Task \"${taskConfig.name}\" must use an image result when ${getTaskExecutionModeLabel(TASK_EXECUTION_MODES.IMAGE_DIRECT)} is selected.`;
}
if(mode === TASK_EXECUTION_MODES.IMAGE_DIRECT && taskConfig.execution.imageInput !== TASK_INPUT_RULES.DISABLED) {
return `Task \"${taskConfig.name}\" cannot send a canvas image when ${getTaskExecutionModeLabel(TASK_EXECUTION_MODES.IMAGE_DIRECT)} is selected.`;
}
if(mode === TASK_EXECUTION_MODES.IMAGE_EDIT && resultType !== TASK_RESULT_TYPES.IMAGE) {
return `Task \"${taskConfig.name}\" must use the image result when ${getTaskExecutionModeLabel(TASK_EXECUTION_MODES.IMAGE_EDIT)} is selected.`;
}
if(mode !== TASK_EXECUTION_MODES.IMAGE_EDIT && maskMode !== TASK_MASK_MODES.DISABLED) {
return `Task \"${taskConfig.name}\" can only enable mask mode when ${getTaskExecutionModeLabel(TASK_EXECUTION_MODES.IMAGE_EDIT)} is selected.`;
}
return "";
};
const getConfiguredTextMaxTokens = () => {
const scriptOverride = parsePositiveInteger(selectedMaxTokens);
if(scriptOverride) {
return scriptOverride;
}
const pluginDefault = parsePositiveInteger(aiSettings.defaultMaxResponseTokens);
return pluginDefault;
};
const getProviderProfiles = () => (
aiSettings.providerProfiles ?? {}
);
const getTextModelConfigs = () => (
aiSettings.textModels ?? {}
);
const getImageModelConfigs = () => (
aiSettings.imageModels ?? {}
);
const hasConfiguredProviderApiKey = (providerId) => (
Boolean(getProviderProfiles()[providerId]?.hasApiKey)
);
const getConfiguredModelIdsForKind = (kind) => {
const configs = kind === "text"
? getTextModelConfigs()
: getImageModelConfigs();
return Object.keys(configs)
.filter(modelId => hasConfiguredProviderApiKey(configs[modelId]?.providerId))
.sort((left, right) => left.localeCompare(right));
};
const getMissingModelConfigurationMessage = (kind) => {
if(kind === "text") {
return "No text or multimodal models are ready to use. Add an API key to a provider profile and assign at least one text model in Excalidraw AI settings.";
}
return "No image models are ready to use. Add an API key to a provider profile and assign at least one image model in Excalidraw AI settings.";
};
const getConfiguredTextModel = () => (
((doesTaskAllowImageInput() && imageDataURL) ? aiSettings.defaultMultimodalTextModel : aiSettings.defaultTextModel)
|| aiSettings.defaultTextModel
|| aiSettings.defaultMultimodalTextModel
|| getConfiguredModelIdsForKind("text")[0]
|| ""
);
const getConfiguredImageModel = () => (
aiSettings.defaultImageModel
|| getConfiguredModelIdsForKind("image")[0]
|| ""
);
const getModelConfigId = (configs, modelId) => {
if(configs[modelId]) {
return modelId;
}
return Object.keys(configs).find(configId => configs[configId]?.model === modelId) ?? "";
};
const getTextModelConfigId = (modelId) => getModelConfigId(getTextModelConfigs(), modelId);
const getImageModelConfigId = (modelId) => {
return getModelConfigId(getImageModelConfigs(), modelId);
};
const getTextModelConfig = (modelId) => {
const configId = getTextModelConfigId(modelId);
return configId ? getTextModelConfigs()[configId] ?? null : null;
};
const getImageModelConfig = (modelId) => {
const configId = getImageModelConfigId(modelId);
return configId ? getImageModelConfigs()[configId] ?? null : null;
};
const getAvailableTextModels = () => {
const configuredModels = getConfiguredModelIdsForKind("text");
if(configuredModels.length > 0) {
return configuredModels;
}
return [];
};
const getAvailableImageModels = () => {
const configuredModels = getConfiguredModelIdsForKind("image");
if(configuredModels.length > 0) {
return configuredModels;
}
return [];
};
const getValidSizesForModel = (model) => {
if(!model) {
return [];
}
const configuredSizes = getImageModelConfig(model)?.supportedSizes
?.map(size => size?.trim())
.filter(Boolean);
if(configuredSizes?.length) {
return configuredSizes;
}
return ["1024x1024"];
};
const parseImageSizeDimensions = (size) => {
const match = String(size ?? "").trim().match(/^(\d+)x(\d+)$/i);
if(!match) {
return null;
}
const width = parseInt(match[1], 10);
const height = parseInt(match[2], 10);
if(Number.isNaN(width) || Number.isNaN(height) || width <= 0 || height <= 0) {
return null;
}
return {width, height};
};
const greatestCommonDivisor = (a, b) => {
let x = Math.abs(a);
let y = Math.abs(b);
while(y !== 0) {
const remainder = x % y;
x = y;
y = remainder;
}
return x || 1;
};
const CANONICAL_ASPECT_RATIOS = [
"1:8",
"1:4",
"2:3",
"3:4",
"4:5",
"9:16",
"1:1",
"16:9",
"5:4",
"4:3",
"3:2",
"4:1",
"8:1",
"21:9",
].map((label) => {
const [width, height] = label.split(":").map((value) => parseInt(value, 10));
return {
label,
ratio: width/height,
};
});
const ASPECT_RATIO_LABEL_RELATIVE_EPSILON = 0.02;
const getCanonicalAspectRatioLabel = (width, height) => {
const ratio = width/height;
const nearest = CANONICAL_ASPECT_RATIOS
.map((candidate) => ({
...candidate,
delta: Math.abs(candidate.ratio - ratio),
}))
.sort((left, right) => left.delta - right.delta)[0];
if(
nearest &&
nearest.delta/Math.max(nearest.ratio, Number.EPSILON)
<= ASPECT_RATIO_LABEL_RELATIVE_EPSILON
) {
return nearest.label;
}
const divisor = greatestCommonDivisor(width, height);
return `${Math.round(width/divisor)}:${Math.round(height/divisor)}`;
};
const getAspectRatioLabelFromDimensions = (width, height) => {
return getCanonicalAspectRatioLabel(width, height);
};
const getImageSizeDropdownOptions = (sizes = []) => {
return sizes
.map((size) => {
const dimensions = parseImageSizeDimensions(size);
if(!dimensions) {
return {
value: size,
label: String(size ?? ""),
ratioOrder: Number.POSITIVE_INFINITY,
pixels: Number.POSITIVE_INFINITY,
width: Number.POSITIVE_INFINITY,
height: Number.POSITIVE_INFINITY,
};
}
const ratioLabel = getAspectRatioLabelFromDimensions(dimensions.width, dimensions.height);
return {
value: size,
label: `(${ratioLabel}) ${dimensions.width}x${dimensions.height}`,
ratioOrder: dimensions.width/dimensions.height,
pixels: dimensions.width * dimensions.height,
width: dimensions.width,
height: dimensions.height,
};
})
.sort((left, right) => {
if(left.ratioOrder !== right.ratioOrder) {
return left.ratioOrder - right.ratioOrder;
}
if(left.pixels !== right.pixels) {
return left.pixels - right.pixels;
}
if(left.width !== right.width) {
return left.width - right.width;
}
if(left.height !== right.height) {
return left.height - right.height;
}
return String(left.value).localeCompare(String(right.value));
});
};
const getResolvedTextModelSelection = (requestedModelId = selectedTextModel) => {
const availableModels = getAvailableTextModels();
const requestedConfigId = getTextModelConfigId(requestedModelId);
const configuredDefaultId = getTextModelConfigId(getConfiguredTextModel());
const resolvedModelId = [requestedConfigId, configuredDefaultId, availableModels[0], requestedModelId]
.find(modelId => modelId && availableModels.includes(modelId))
|| "";
const modelConfig = getTextModelConfig(resolvedModelId);
const providerProfiles = getProviderProfiles();
const providerId = modelConfig?.providerId ?? Object.keys(providerProfiles)[0] ?? "";
const providerProfile = providerProfiles[providerId] ?? null;
return {
modelId: resolvedModelId,
modelConfig,
providerId,
providerProfile,
requestConfig: {
textModelId: resolvedModelId,
},
};
};
const getResolvedImageModelSelection = (requestedModelId = selectedImageModel) => {
const availableModels = getAvailableImageModels();
const requestedConfigId = getImageModelConfigId(requestedModelId);
const configuredDefaultId = getImageModelConfigId(getConfiguredImageModel());
const resolvedModelId = [requestedConfigId, configuredDefaultId, availableModels[0], requestedModelId]
.find(modelId => modelId && availableModels.includes(modelId))
|| "";
const modelConfig = getImageModelConfig(resolvedModelId);
const providerProfiles = getProviderProfiles();
const providerId = modelConfig?.providerId ?? Object.keys(providerProfiles)[0] ?? "";
const providerProfile = providerProfiles[providerId] ?? null;
return {
modelId: resolvedModelId,
modelConfig,
providerId,
providerProfile,
requestConfig: {
imageModelId: resolvedModelId,
},
};
};
const getTextModelValidationMessage = (modelId, {requireMultimodal = false} = {}) => {
if(getAvailableTextModels().length === 0) {
return getMissingModelConfigurationMessage("text");
}
const textSelection = getResolvedTextModelSelection(modelId);
if(!textSelection.modelConfig) {
return `The selected text model (${modelId || "unknown"}) isn't configured in Excalidraw AI settings.`;
}
if(!textSelection.providerProfile) {
return `The provider profile (${textSelection.providerId || "unknown"}) for text model ${textSelection.modelId} is missing from Excalidraw AI settings.`;
}
if(!textSelection.providerProfile.hasApiKey) {
return `The selected provider profile (${textSelection.providerId}) doesn't have an API key configured.`;
}
if(requireMultimodal && textSelection.modelConfig.multimodalSupport === false) {
return `The selected text model (${textSelection.modelId}) is set to text-only. Choose a multimodal model for image analysis tasks.`;
}
return "";
};
const getImageModelValidationMessage = (modelId, {requirePromptTransformSupport = false, requireMaskEditSupport = false} = {}) => {
if(getAvailableImageModels().length === 0) {
return getMissingModelConfigurationMessage("image");
}
const imageSelection = getResolvedImageModelSelection(modelId);
if(!imageSelection.modelConfig) {
return getMissingModelConfigurationMessage("image");
}
if(!imageSelection.providerProfile) {
return `The provider profile (${imageSelection.providerId || "unknown"}) for image model ${imageSelection.modelId} is missing from Excalidraw AI settings.`;
}
if(!imageSelection.providerProfile.hasApiKey) {
return `The selected provider profile (${imageSelection.providerId}) doesn't have an API key configured.`;
}
if(requirePromptTransformSupport && imageSelection.modelConfig.supportsPromptImageTransforms === false) {
return `The selected image model (${imageSelection.modelId}) doesn't support prompt-based transforms in Excalidraw AI settings.`;
}
if(requireMaskEditSupport && imageSelection.modelConfig.supportsMaskImageEdits === false) {
return `The selected image model (${imageSelection.modelId}) doesn't support mask-based edits in Excalidraw AI settings.`;
}
return "";
};
const getImageRequestErrorMessage = (result, modelId) => {
const baseMessage = result?.json?.error?.message ?? "The image request failed.";
const errorContext = result?.json?.error;
const imageSelection = getResolvedImageModelSelection(modelId);
const contextParts = [];
if(errorContext?.provider) {
contextParts.push(`provider=${errorContext.provider}`);
}
if(errorContext?.status) {
contextParts.push(`status=${errorContext.status}`);
}
if(errorContext?.endpoint) {
contextParts.push(`endpoint=${errorContext.endpoint}`);
}
if(errorContext?.imageRequest?.model) {
contextParts.push(`model=${errorContext.imageRequest.model}`);
}
if(errorContext?.imageRequest?.size) {
contextParts.push(`size=${errorContext.imageRequest.size}`);
}
if(errorContext?.imageRequest?.mode) {
contextParts.push(`mode=${errorContext.imageRequest.mode}`);
}
const contextText = contextParts.length > 0 ? ` (${contextParts.join(", ")})` : "";
return `${baseMessage}${contextText}`;
};
const getTaskValidationMessage = ({
usesTextModel,
requiresMultimodalText,
isImageGenRequest,
isImageEditRequest,
requiresPromptTransformSupport,
requiresMaskEditSupport,
activeTextSelection,
activeImageSelection,
}) => {
if(usesTextModel) {
const textValidationMessage = getTextModelValidationMessage(activeTextSelection.modelId, {
requireMultimodal: requiresMultimodalText,
});
if(textValidationMessage) {
return textValidationMessage;
}
}
if(isImageGenRequest || isImageEditRequest) {
return getImageModelValidationMessage(activeImageSelection.modelId, {
requirePromptTransformSupport: requiresPromptTransformSupport,
requireMaskEditSupport: requiresMaskEditSupport,
});
}
return "";
};
const getActiveTextModel = () => {
return getResolvedTextModelSelection(selectedTextModel).modelId;
};
const getActiveImageModel = () => {
return getResolvedImageModelSelection(selectedImageModel).modelId;
};
const activeImageModelSupportsMaskEdits = () => {
const modelConfig = getResolvedImageModelSelection(selectedImageModel).modelConfig;
return Boolean(modelConfig) && modelConfig.supportsMaskImageEdits !== false;
};
const canUseMaskEdit = () => (
isImageEditTask()
&& getTaskMaskMode() !== TASK_MASK_MODES.DISABLED
&& activeImageModelSupportsMaskEdits()
);
const shouldUseMaskEdit = () => {
switch(getTaskMaskMode()) {
case TASK_MASK_MODES.REQUIRED:
return canUseMaskEdit();
case TASK_MASK_MODES.OPTIONAL:
return canUseMaskEdit() && prefersMaskEdit;
default:
return false;
}
};
const shouldGenerateMaskPreview = () => (
shouldUseMaskEdit()
);
const hasAvailableTextModels = () => getAvailableTextModels().length > 0;
const hasAvailableImageModels = () => getAvailableImageModels().length > 0;
const parseImageSize = (size) => {
const [width, height] = (size ?? "1024x1024").split("x").map(value => parseInt(value, 10));
if(Number.isNaN(width) || Number.isNaN(height) || width <= 0 || height <= 0) {
return {width: 1024, height: 1024};
}
return {width, height};
};
const getEditTargetBoundingBox = (bb, size) => {
const {width: targetWidth, height: targetHeight} = parseImageSize(size);
const targetRatio = targetWidth/targetHeight;
const sourceRatio = bb.width/bb.height;
let width = bb.width;
let height = bb.height;
let topX = bb.topX;
let topY = bb.topY;
if(sourceRatio > targetRatio) {
height = width/targetRatio;
topY = bb.topY - (height - bb.height)/2;
} else if(sourceRatio < targetRatio) {
width = height*targetRatio;
topX = bb.topX - (width - bb.width)/2;
}
return {topX, topY, width, height, targetWidth, targetHeight};
};
const setTextAndImageModels = () => {
const nextTextModel = getActiveTextModel();
if(selectedTextModel !== nextTextModel) {
dirty = true;
}
textModel = nextTextModel;
selectedTextModel = textModel;
const nextImageModel = getActiveImageModel();
if(selectedImageModel !== nextImageModel) {
dirty = true;
}
imageModel = nextImageModel;
selectedImageModel = imageModel;
validSizes = imageModel ? getValidSizesForModel(imageModel) : [];
if(imageModel && !validSizes.includes(imageSize)) {
imageSize = validSizes[0] ?? "1024x1024";
dirty = true;
}
}
setTextAndImageModels();
// --------------------------------------
// Generate Image Blob From Selected Excalidraw Elements
// --------------------------------------
const calculateImageScale = (elements) => {
const bb = ea.getBoundingBox(elements);
const size = (bb.width*bb.height);
const minRatio = Math.sqrt(360000/size);
const maxRatio = Math.sqrt(size/16000000);
return minRatio > 1
? minRatio
: (
maxRatio > 1
? 1/maxRatio
: 1
);
}
const createMask = async (dataURL) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
// If opaque (alpha > 0), make it transparent
if (data[i + 3] > 0) {
data[i + 3] = 0; // Set alpha to 0 (transparent)
} else if (data[i + 3] === 0) {
// If fully transparent, make it red
data[i] = 255; // Red
data[i + 1] = 0; // Green
data[i + 2] = 0; // Blue
data[i + 3] = 255; // make it opaque
}
}
ctx.putImageData(imageData, 0, 0);
const maskDataURL = canvas.toDataURL();
resolve(maskDataURL);
};
img.onerror = error => {
reject(error);
};
img.src = dataURL;
});
}
// For image edits, the selected content is padded to the requested output aspect ratio
// so the exported image and mask match the requested model size.
const generateCanvasDataURL = async (view, targetImageEdit=false) => {
let PADDING = 5;
await view.forceSave(true); //to ensure recently embedded PNG and other images are saved to file
const viewElements = ea.getViewSelectedElements();
if(viewElements.length === 0) {
return {imageDataURL: null, maskDataURL: null} ;
}
ea.copyViewElementsToEAforEditing(viewElements, true); //copying the images objects over to EA for PNG generation
let maskDataURL;
const loader = ea.getEmbeddedFilesLoader(false);
let scale = calculateImageScale(ea.getElements());
const bb = ea.getBoundingBox(viewElements);
if(ea.getElements()
.filter(el=>el.type==="image")
.some(el=>Math.round(el.width) === Math.round(bb.width) && Math.round(el.height) === Math.round(bb.height))
) { PADDING = 0; }
let exportSettings = {withBackground: true, withTheme: true};
if(targetImageEdit) {
PADDING = 0;
const strokeColor = ea.style.strokeColor;
const backgroundColor = ea.style.backgroundColor;
ea.style.backgroundColor = "transparent";
ea.style.strokeColor = "transparent";
const targetBounds = getEditTargetBoundingBox(bb, imageSize);
const rectID = ea.addRect(targetBounds.topX, targetBounds.topY, targetBounds.width, targetBounds.height);
const rect = ea.getElement(rectID);
ea.style.strokeColor = strokeColor;
ea.style.backgroundColor = backgroundColor;
ea.getElements().filter(el=>el.type === "image").forEach(el=>{el.isDeleted = true});
scale = targetBounds.targetWidth/rect.width;
exportSettings = {withBackground: false, withTheme: true};
maskDataURL= await ea.createPNGBase64(
null, scale, exportSettings, loader, "light", PADDING
);
maskDataURL = await createMask(maskDataURL)
ea.getElements().filter(el=>el.type === "image").forEach(el=>{el.isDeleted = false});
ea.getElements().filter(el=>el.type !== "image" && el.id !== rectID).forEach(el=>{el.isDeleted = true});
}
const imageDataURL = await ea.createPNGBase64(
null, scale, exportSettings, loader, "light", PADDING
);
ea.clear();
return {imageDataURL, maskDataURL};
}
({imageDataURL, maskDataURL} = await generateCanvasDataURL(ea.targetView, shouldGenerateMaskPreview()));
// --------------------------------------
// Support functions - embeddable spinner and error
// --------------------------------------
const spinner = await ea.convertStringToDataURL(`
${message}
` : ""; const errorDataURL = await ea.convertStringToDataURL(`