@orangecatai/adgen-canvas 0.0.33 → 0.0.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/dev/{chunk-BSCN3CPW.js → chunk-AVI2TDX6.js} +2 -2
  2. package/dist/dev/{chunk-O66HSY73.js → chunk-KJZTX4HK.js} +9 -6
  3. package/dist/dev/{chunk-O66HSY73.js.map → chunk-KJZTX4HK.js.map} +3 -3
  4. package/dist/dev/data/{image-ADAEIYVZ.js → image-JWQCODTZ.js} +3 -3
  5. package/dist/dev/index.css +714 -704
  6. package/dist/dev/index.css.map +3 -3
  7. package/dist/dev/index.js +9779 -8212
  8. package/dist/dev/index.js.map +4 -4
  9. package/dist/dev/subset-shared.chunk.js +1 -1
  10. package/dist/dev/subset-worker.chunk.js +1 -1
  11. package/dist/prod/{chunk-MBLHWEZ7.js → chunk-R3CESCMK.js} +3 -3
  12. package/dist/prod/{chunk-DB27YYNM.js → chunk-TE4JIU3E.js} +1 -1
  13. package/dist/prod/data/image-EQCUXFMW.js +1 -0
  14. package/dist/prod/index.css +1 -1
  15. package/dist/prod/index.js +165 -103
  16. package/dist/prod/subset-shared.chunk.js +1 -1
  17. package/dist/prod/subset-worker.chunk.js +1 -1
  18. package/dist/types/element/src/frame.d.ts +16 -0
  19. package/dist/types/excalidraw/components/ImageGeneratorPanel.d.ts +15 -0
  20. package/dist/types/excalidraw/components/ai-chat/imagePayload.d.ts +2 -19
  21. package/dist/types/excalidraw/components/auto-resize/AutoResizePanel.d.ts +3 -4
  22. package/dist/types/excalidraw/components/auto-resize/autoResizeEngine.d.ts +17 -14
  23. package/dist/types/excalidraw/components/auto-resize/backgroundExtend.d.ts +105 -0
  24. package/dist/types/excalidraw/components/auto-resize/frameSource.d.ts +16 -0
  25. package/dist/types/excalidraw/components/frameToolbarVariant.d.ts +34 -1
  26. package/dist/types/excalidraw/components/video/pendingVideoGenerations.d.ts +46 -0
  27. package/dist/types/excalidraw/renderer/staticSvgScene.d.ts +1 -1
  28. package/dist/types/excalidraw/types.d.ts +23 -4
  29. package/dist/types/excalidraw/utils/brandContextUtils.d.ts +11 -6
  30. package/dist/types/excalidraw/utils/imageApi.d.ts +52 -6
  31. package/dist/types/excalidraw/utils/imageCompression.d.ts +46 -0
  32. package/dist/types/excalidraw/utils/referenceBudget.d.ts +138 -0
  33. package/package.json +1 -1
  34. package/dist/prod/data/image-ALVZ6E6S.js +0 -1
  35. /package/dist/dev/{chunk-BSCN3CPW.js.map → chunk-AVI2TDX6.js.map} +0 -0
  36. /package/dist/dev/data/{image-ADAEIYVZ.js.map → image-JWQCODTZ.js.map} +0 -0
@@ -1 +1 @@
1
- import{a,b,c,d}from"./chunk-Z5NKEFVG.js";import"./chunk-DB27YYNM.js";import"./chunk-SRAX5OIU.js";export{a as Commands,b as subsetToBase64,c as subsetToBinary,d as toBase64};
1
+ import{a,b,c,d}from"./chunk-Z5NKEFVG.js";import"./chunk-TE4JIU3E.js";import"./chunk-SRAX5OIU.js";export{a as Commands,b as subsetToBase64,c as subsetToBinary,d as toBase64};
@@ -1 +1 @@
1
- import{a as r,c as t}from"./chunk-Z5NKEFVG.js";import"./chunk-DB27YYNM.js";import"./chunk-SRAX5OIU.js";var s=import.meta.url?new URL(import.meta.url):void 0;typeof window>"u"&&typeof self<"u"&&(self.onmessage=async e=>{switch(e.data.command){case r.Subset:let a=await t(e.data.arrayBuffer,e.data.codePoints);self.postMessage(a,{transfer:[a]});break}});export{s as WorkerUrl};
1
+ import{a as r,c as t}from"./chunk-Z5NKEFVG.js";import"./chunk-TE4JIU3E.js";import"./chunk-SRAX5OIU.js";var s=import.meta.url?new URL(import.meta.url):void 0;typeof window>"u"&&typeof self<"u"&&(self.onmessage=async e=>{switch(e.data.command){case r.Subset:let a=await t(e.data.arrayBuffer,e.data.codePoints);self.postMessage(a,{transfer:[a]});break}});export{s as WorkerUrl};
@@ -20,6 +20,22 @@ export declare const groupsAreCompletelyOutOfFrame: (elements: readonly NonDelet
20
20
  */
21
21
  export declare const groupByFrameLikes: (elements: readonly ExcalidrawElement[]) => Map<string, ExcalidrawElement[]>;
22
22
  export declare const getFrameChildren: (allElements: ElementsMapOrArray, frameId: string) => ExcalidrawElement[];
23
+ /**
24
+ * Frames are containers, not artwork: a frame drawn above the elements sitting
25
+ * on or near it hides them behind its fill and steals clicks meant for them,
26
+ * which reads to the user as "my image disappeared". Element order is z-order,
27
+ * so this pushes every frame-like element below every other element.
28
+ *
29
+ * Applied at the presentation layer only — rendering and hit testing — never to
30
+ * the scene itself: the scene array is mirrored by fractional indices that the
31
+ * store, history and collab reconciliation all reason about, and rewriting them
32
+ * on every scene update fights undo/redo. Relative order within each bucket is
33
+ * preserved, so frame-vs-frame and image-vs-image stacking survives untouched.
34
+ *
35
+ * Returns the passed array unchanged (same reference) when it already satisfies
36
+ * the invariant, which is the common case.
37
+ */
38
+ export declare const orderFramesToBack: <T extends ExcalidrawElement>(elements: readonly T[]) => readonly T[];
23
39
  export declare const getFrameLikeElements: (allElements: ExcalidrawElementsIncludingDeleted) => ExcalidrawFrameLikeElement[];
24
40
  /**
25
41
  * Returns ExcalidrawFrameElements and non-frame-children elements.
@@ -87,6 +87,21 @@ export declare function getDimensions(model: ModelName, ratio: string, resolutio
87
87
  width: number;
88
88
  height: number;
89
89
  };
90
+ /**
91
+ * Turn the panel's picked ratio + resolution into the request fields the model
92
+ * actually accepts.
93
+ *
94
+ * gpt-image-* is the awkward one: its `aspect_ratio` enum does NOT accept 4:5
95
+ * or 5:4 at the 2K/4K tiers, and `supportsImageSize: false` meant the picked
96
+ * resolution was dropped on the floor — so "4:5 at 2K" silently asked for a
97
+ * ratio the API rejects and gave no size at all. The panel already knows the
98
+ * exact pixel dimensions for every ratio/resolution pair, so hand those over
99
+ * as an authoritative `size` and skip the enum entirely.
100
+ */
101
+ export declare function resolveRequestSize(model: ModelName, ratio: string, resolution: Resolution): {
102
+ aspectRatio: string | null;
103
+ imageSize: string | null;
104
+ };
90
105
  export declare const ImageGeneratorPanel: ({ element, app, onBeforeImageGen, onAfterImageGen, }: {
91
106
  element: NonDeletedExcalidrawElement & ExcalidrawFrameLikeElement;
92
107
  app: AppClassProperties;
@@ -19,25 +19,8 @@
19
19
  * multi-megabyte data URLs, and those are re-sent on every subsequent turn.
20
20
  * Normalising here fixes existing conversations, not just new uploads.
21
21
  */
22
- /** Estimated wire size of a data URL: the base64 payload is what gets sent. */
23
- export declare function dataUrlWireBytes(dataUrl: string): number;
24
- /**
25
- * Whether a data URL is a raster format safe to send as an `image_url` block.
26
- * SVG is commonly rejected outright by vision models, and canvas re-encoding
27
- * of an SVG is unreliable across browsers, so it is neither shrunk nor sent.
28
- */
29
- export declare function isRasterImageDataUrl(dataUrl: string): boolean;
30
- /**
31
- * Downscale + re-encode a raster data URL so it is safe to embed in a request.
32
- *
33
- * Never throws: on any failure (decode error, no canvas context, non-raster
34
- * input) the original data URL is returned unchanged. Callers treat this as
35
- * best-effort normalisation, with `enforceImagePayloadBudget` as the backstop
36
- * for anything that could not be shrunk.
37
- */
38
- export declare function shrinkImageDataUrl(dataUrl: string): Promise<string>;
39
- /** Shrink a batch in parallel, preserving order. */
40
- export declare function shrinkImageDataUrls(dataUrls: string[]): Promise<string[]>;
22
+ import { dataUrlWireBytes, isRasterImageDataUrl, shrinkImageDataUrl, shrinkImageDataUrls } from "../../utils/imageCompression";
23
+ export { dataUrlWireBytes, isRasterImageDataUrl, shrinkImageDataUrl, shrinkImageDataUrls, };
41
24
  /**
42
25
  * Normalise every image in an outgoing request so the body cannot exceed the
43
26
  * platform request cap.
@@ -1,7 +1,7 @@
1
1
  import "./AutoResizePanel.scss";
2
2
  import type { ExcalidrawFrameLikeElement, NonDeletedExcalidrawElement } from "@orangecatai/element/types";
3
3
  import type { AppClassProperties } from "../../types";
4
- export declare const AutoResizePanel: ({ element, app, onClose, onBeforeAutoResize, onAfterAutoResize, onRunningChange, autoResizeUrl, customResizeUrl, tier1CropUrl, chatModel, agentImageModel, }: {
4
+ export declare const AutoResizePanel: ({ element, app, onClose, onBeforeAutoResize, onAfterAutoResize, onRunningChange, autoResizeUrl, chatModel, reviewerModel, outpaintModel, }: {
5
5
  element: NonDeletedExcalidrawElement & ExcalidrawFrameLikeElement;
6
6
  app: AppClassProperties;
7
7
  onClose: () => void;
@@ -12,8 +12,7 @@ export declare const AutoResizePanel: ({ element, app, onClose, onBeforeAutoResi
12
12
  onAfterAutoResize?: () => void;
13
13
  onRunningChange?: (running: boolean) => void;
14
14
  autoResizeUrl: string;
15
- customResizeUrl?: string;
16
- tier1CropUrl?: string;
17
15
  chatModel: string;
18
- agentImageModel?: string;
16
+ reviewerModel?: string;
17
+ outpaintModel?: string;
19
18
  }) => import("react/jsx-runtime").JSX.Element;
@@ -152,6 +152,19 @@ export declare function enforceMinProductBox({ x, y, width, height, targetW, tar
152
152
  width: number;
153
153
  height: number;
154
154
  };
155
+ /**
156
+ * Checks generated HTML against the strict contract in `buildAutoResizePrompt`
157
+ * BEFORE handing it to the parser.
158
+ *
159
+ * The parser is deliberately narrow — absolutely-positioned divs, no flex, no
160
+ * grid, no transforms — because that is what maps cleanly onto Excalidraw
161
+ * elements. Models drift off strict DOM contracts, and a violation shows up as
162
+ * silently mangled geometry rather than an error. Catching it here buys one
163
+ * targeted retry instead of a wrong layout.
164
+ *
165
+ * Returns the list of violations; empty means the HTML conforms.
166
+ */
167
+ export declare function validateHtmlContract(html: string, expectedChildCount: number): string[];
155
168
  export declare function buildAutoResizeImagePrompt(srcW: number, srcH: number, tgtW: number, tgtH: number, aspectRatio: string, brandImages?: {
156
169
  logo?: string;
157
170
  fontPreview?: string;
@@ -171,10 +184,11 @@ export declare function runAutoResize(opts: {
171
184
  targetDimensions: TargetDimension[];
172
185
  app: AppClassProperties;
173
186
  autoResizeUrl: string;
174
- customResizeUrl?: string;
175
- tier1CropUrl?: string;
176
187
  chatModel?: string;
177
- agentImageModel?: string;
188
+ /** Vision model for the layout critique loop. Falls back to `chatModel`. */
189
+ reviewerModel?: string;
190
+ /** Image model used ONLY to outpaint photographic backgrounds. */
191
+ outpaintModel?: string;
178
192
  customFontMap?: Record<string, number>;
179
193
  onBeforeAutoResize?: () => Promise<{
180
194
  allowed: boolean;
@@ -185,16 +199,5 @@ export declare function runAutoResize(opts: {
185
199
  signal?: AbortSignal;
186
200
  /** Pass the result of preCreateAllFrames() to skip redundant frame creation */
187
201
  preCreatedFrameInfos?: PreCreatedFrameInfo[];
188
- /**
189
- * Brand assets handed to the image model so the recomposed ad keeps the brand
190
- * logo, typeface, and (for font-aware models) the real font file. Sourced from
191
- * `app.state.brandContext` by the panel.
192
- */
193
- brandImages?: {
194
- logo?: string;
195
- fontPreview?: string;
196
- fontUrl?: string;
197
- name?: string;
198
- };
199
202
  }): Promise<DimensionResult[]>;
200
203
  export {};
@@ -0,0 +1,105 @@
1
+ export type BgKind = "solid" | "texture" | "panels" | "gradient" | "photo";
2
+ export type PixelBuffer = {
3
+ data: Uint8ClampedArray;
4
+ width: number;
5
+ height: number;
6
+ };
7
+ export type Rect = {
8
+ x: number;
9
+ y: number;
10
+ w: number;
11
+ h: number;
12
+ };
13
+ /** Where a panel boundary sits, as a fraction (0-1) along `axis`. */
14
+ export type BgSplit = {
15
+ axis: "x" | "y";
16
+ at: number;
17
+ };
18
+ export type BgClassification = {
19
+ kind: BgKind;
20
+ /** Present only for `kind: "panels"`. */
21
+ split?: BgSplit;
22
+ /** Present for `kind: "solid"` — the plate's average colour as hex. */
23
+ solidColor?: string;
24
+ /** Per-region verdicts; length 2 when a split was found, else 1. */
25
+ regions: RegionStats[];
26
+ };
27
+ export type RegionStats = {
28
+ rect: Rect;
29
+ /** Mean luminance stdev within patches — how "busy" the region is. */
30
+ textureEnergy: number;
31
+ /** Spread of patch MEANS — how much the region changes across itself. */
32
+ meanSpread: number;
33
+ /** True when patch means march in one direction along x or y. */
34
+ monotonic: boolean;
35
+ meanColor: {
36
+ r: number;
37
+ g: number;
38
+ b: number;
39
+ };
40
+ verdict: "solid" | "texture" | "gradient" | "photo";
41
+ };
42
+ export declare function toHex({ r, g, b, }: {
43
+ r: number;
44
+ g: number;
45
+ b: number;
46
+ }): string;
47
+ /**
48
+ * Finds the single strongest panel boundary, if any. Deliberately detects at
49
+ * most ONE split — that covers the dominant real-world ad layout (a two-panel
50
+ * composition) without overfitting to noise in busier plates.
51
+ */
52
+ export declare function detectSplit(buf: PixelBuffer): BgSplit | null;
53
+ /** Patch-grid statistics for one region, and the verdict they imply. */
54
+ export declare function analyzeRegion(buf: PixelBuffer, rect: Rect): RegionStats;
55
+ /** Full classification: split detection, then per-region analysis. */
56
+ export declare function classifyBackground(buf: PixelBuffer): BgClassification;
57
+ export declare function loadImageElement(dataUrl: string): Promise<HTMLImageElement>;
58
+ /**
59
+ * Reads an image into a pixel buffer, downscaled to at most `maxSide` on its
60
+ * long edge. Classification only needs coarse statistics, and a 4K plate would
61
+ * otherwise cost tens of megabytes of ImageData for no extra accuracy.
62
+ */
63
+ export declare function imageToPixelBuffer(dataUrl: string, maxSide?: number): Promise<PixelBuffer>;
64
+ export type BackgroundSource = {
65
+ type: "solid";
66
+ color: string;
67
+ elementId?: string;
68
+ } | {
69
+ type: "image";
70
+ dataUrl: string;
71
+ elementId?: string;
72
+ } | {
73
+ type: "none";
74
+ };
75
+ export type BackgroundResult = {
76
+ kind: "none";
77
+ } | {
78
+ kind: "solid";
79
+ color: string;
80
+ } | {
81
+ kind: "image";
82
+ dataUrl: string;
83
+ strategy: BgKind | "scaled";
84
+ };
85
+ export type BuildBackgroundOpts = {
86
+ /**
87
+ * Called ONLY for photographic plates that cannot be extended locally.
88
+ * Supplied by the caller so this module stays free of network concerns and
89
+ * fully unit-testable. Returning null falls back to edge-extension.
90
+ */
91
+ outpaint?: (args: {
92
+ dataUrl: string;
93
+ targetWidth: number;
94
+ targetHeight: number;
95
+ }) => Promise<string | null>;
96
+ /** When false, skip analysis and just cover-scale (small AR delta). */
97
+ needsExtension: boolean;
98
+ onProgress?: (phase: string) => void;
99
+ };
100
+ /**
101
+ * Produces a background sized EXACTLY to the target frame. The caller inserts
102
+ * the result 1:1 — no cover-crop afterwards, unlike the old image-gen path
103
+ * where the model returned approximate dimensions.
104
+ */
105
+ export declare function buildTargetBackground(bg: BackgroundSource, tgtW: number, tgtH: number, opts: BuildBackgroundOpts): Promise<BackgroundResult>;
@@ -0,0 +1,16 @@
1
+ import type { ExcalidrawElement, ExcalidrawFrameLikeElement } from "@orangecatai/element/types";
2
+ /** Live (non-deleted) children of a frame. */
3
+ export declare function getLiveFrameChildren(elements: readonly ExcalidrawElement[], frame: ExcalidrawFrameLikeElement): readonly ExcalidrawElement[];
4
+ /**
5
+ * True when this frame holds separable layers Auto Resize can re-arrange.
6
+ *
7
+ * Any ONE of these is sufficient:
8
+ * - more than one live child → there is something to arrange
9
+ * - a child tagged `decomposeType` → came from "Make editable"
10
+ * - a text child → real editable copy, so the frame is not a flat render
11
+ *
12
+ * False for the flat case: zero or one child (typically one full-bleed image).
13
+ */
14
+ export declare function isStructuredFrame(elements: readonly ExcalidrawElement[], frame: ExcalidrawFrameLikeElement): boolean;
15
+ /** Human-readable reason shown when Auto Resize is unavailable. */
16
+ export declare const AUTO_RESIZE_FLAT_FRAME_REASON = "Make this ad editable first \u2014 Auto Resize needs separate layers to re-lay-out.";
@@ -1,4 +1,4 @@
1
- import type { ElementsMapOrArray, ExcalidrawElement, ExcalidrawFrameLikeElement, ExcalidrawVideoElement } from "@orangecatai/element/types";
1
+ import type { ElementsMapOrArray, ExcalidrawElement, ExcalidrawFrameLikeElement, ExcalidrawImageElement, ExcalidrawVideoElement } from "@orangecatai/element/types";
2
2
  export type FrameToolbarVariant = "frame" | "video";
3
3
  export type FrameToolbarVariantResult = {
4
4
  variant: FrameToolbarVariant;
@@ -34,3 +34,36 @@ export declare const getFrameToolbarVariant: (frame: ExcalidrawFrameLikeElement,
34
34
  * toolbar, so the child never surfaces an action the frame does not have.
35
35
  */
36
36
  export declare const getResizeTargetFrame: (element: ExcalidrawElement, elements: readonly ExcalidrawElement[]) => ExcalidrawFrameLikeElement | null;
37
+ /**
38
+ * Resolves the image a *frame's* toolbar should act on.
39
+ *
40
+ * A generated ad covers its frame edge to edge, so whether a click lands on the
41
+ * frame or on the artwork is arbitrary from the user's side — but only the
42
+ * artwork carries the image actions (Quick Edit, and the host's Make editable).
43
+ * This lets a selected frame offer those same actions, targeting its artwork,
44
+ * so they no longer come and go with where the user happened to click. It is
45
+ * the mirror of `getResizeTargetFrame`, which lets a selected image reach the
46
+ * frame's Resize.
47
+ *
48
+ * When a frame holds several images (artwork plus a logo overlay, say), the
49
+ * largest one is the artwork.
50
+ */
51
+ export declare const getFrameChildImage: (frame: ExcalidrawFrameLikeElement, elements: ElementsMapOrArray) => ExcalidrawImageElement | null;
52
+ export type AutoResizeAvailability = {
53
+ /** False when the frame is a flat render with nothing to re-arrange. */
54
+ enabled: boolean;
55
+ /** Tooltip text — the reason when disabled, the action when enabled. */
56
+ title: string;
57
+ };
58
+ /**
59
+ * Whether Auto Resize can act on this frame, and what to say if it cannot.
60
+ *
61
+ * Auto Resize re-lays-out an ad by repositioning its individual layers, so it
62
+ * needs a frame that HAS layers. A freshly generated ad is one flat image; the
63
+ * button stays visible but disabled, pointing at "Make editable", rather than
64
+ * silently vanishing.
65
+ *
66
+ * Shared by both entry points (a selected frame, and a selected image resolved
67
+ * to its parent frame) so the rule cannot drift between them.
68
+ */
69
+ export declare const getAutoResizeAvailability: (frame: ExcalidrawFrameLikeElement | null, elements: readonly ExcalidrawElement[]) => AutoResizeAvailability;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Pending video generation state — module-level so a run survives the panel
3
+ * unmounting. VideoGeneratorPanel is rendered only while its frame is
4
+ * selected (see App.tsx), so clicking empty canvas mid-generation tears the
5
+ * component down; without this store the spinner, progress and prompt would
6
+ * vanish and reselecting the frame would show an idle panel even though the
7
+ * request is still in flight.
8
+ *
9
+ * Mirrors image-generation/pendingGenerations.ts. Flow:
10
+ * 1. Generate → startPendingVideoGeneration(frameId, prompt, controller).
11
+ * 2. The panel seeds its local state from getPendingVideoState(frameId) on
12
+ * mount and subscribes for the rest, so a remount picks the run back up.
13
+ * 3. Status/progress updates flow through the store, so the in-flight closure
14
+ * (which may belong to an already-unmounted instance) and whichever panel
15
+ * is currently mounted stay in sync.
16
+ * 4. On success → clearVideoGenerationState(frameId) so reselecting the frame
17
+ * shows a clean prompt. On error the entry is kept so the message survives.
18
+ * 5. Stop button → abortPendingVideoGeneration(frameId).
19
+ *
20
+ * Subscribers are tracked separately from entries so a panel can subscribe
21
+ * before any generation has started on its frame.
22
+ */
23
+ export type PendingVideoStatus = "submitting" | "polling" | "done" | "error";
24
+ export interface PendingVideoState {
25
+ prompt: string;
26
+ status: PendingVideoStatus;
27
+ progress: number;
28
+ error: string | null;
29
+ }
30
+ export type VideoGenerationUpdateCallback = (state: PendingVideoState | null) => void;
31
+ export declare function startPendingVideoGeneration(frameId: string, prompt: string, controller: AbortController): void;
32
+ export declare function setPendingVideoStatus(frameId: string, status: PendingVideoStatus): void;
33
+ export declare function setPendingVideoProgress(frameId: string, progress: number): void;
34
+ /**
35
+ * Terminal update. `null` → success and `"aborted"` → stopped both drop the
36
+ * entry (nothing left to show); any other string parks the entry in "error"
37
+ * so a panel remounted afterwards still explains what happened.
38
+ */
39
+ export declare function finishPendingVideoGeneration(frameId: string, error: string | null): void;
40
+ export declare function subscribeVideoGeneration(frameId: string, fn: VideoGenerationUpdateCallback): void;
41
+ export declare function unsubscribeVideoGeneration(frameId: string, fn: VideoGenerationUpdateCallback): void;
42
+ export declare function clearVideoGenerationState(frameId: string): void;
43
+ export declare function abortPendingVideoGeneration(frameId: string): void;
44
+ export declare function hasPendingVideoGeneration(frameId: string): boolean;
45
+ export declare function getPendingVideoState(frameId: string): PendingVideoState | null;
46
+ export declare function getPendingVideoPrompt(frameId: string): string;
@@ -2,4 +2,4 @@ import type { NonDeletedExcalidrawElement } from "@orangecatai/element/types";
2
2
  import type { RenderableElementsMap, SVGRenderConfig } from "../scene/types";
3
3
  import type { BinaryFiles } from "../types";
4
4
  import type { RoughSVG } from "roughjs/bin/svg";
5
- export declare const renderSceneToSvg: (elements: readonly NonDeletedExcalidrawElement[], elementsMap: RenderableElementsMap, rsvg: RoughSVG, svgRoot: SVGElement, files: BinaryFiles, renderConfig: SVGRenderConfig) => void;
5
+ export declare const renderSceneToSvg: (unorderedElements: readonly NonDeletedExcalidrawElement[], elementsMap: RenderableElementsMap, rsvg: RoughSVG, svgRoot: SVGElement, files: BinaryFiles, renderConfig: SVGRenderConfig) => void;
@@ -767,12 +767,31 @@ export interface ExcalidrawProps {
767
767
  agentImageModel?: string;
768
768
  /**
769
769
  * OpenRouter model tag used by the auto-resize engine for HTML layout
770
- * generation. This is the LLM call that figures out how to reflow the ad
771
- * content into the target dimensions before Gemini regenerates the background.
772
- * @default "google/gemini-3.1-flash-lite-preview"
773
- * @example "openai/gpt-4o"
770
+ * generation — the LLM call that decides how to reflow the ad's layers into
771
+ * the target dimensions. Must be a chat model.
772
+ * @default "x-ai/grok-4.6"
774
773
  */
775
774
  chatModel?: string;
775
+ /**
776
+ * Vision model for the auto-resize layout critique loop. Falls back to
777
+ * `chatModel` when omitted, so pass a cheap fast vision model here — the
778
+ * reviewer only compares a screenshot against the brief, it does not carry
779
+ * an agentic conversation.
780
+ * @example "openai/gpt-5.6-luna"
781
+ */
782
+ reviewerModel?: string;
783
+ /**
784
+ * Image model used ONLY to outpaint a photographic background when the
785
+ * aspect-ratio change is too large to cover-crop. Solid, textured and
786
+ * two-panel backgrounds are extended locally on a canvas and never reach a
787
+ * model at all.
788
+ *
789
+ * Must support extreme aspect ratios (4:1, 1:4, 8:1, 1:8) and be callable
790
+ * from `chat/completions`. `openai/gpt-image-2` satisfies NEITHER — it is
791
+ * generation-only on OpenRouter and its ratio enum stops at 3:1 / 1:3.
792
+ * @default "google/gemini-3.1-flash-image-preview"
793
+ */
794
+ outpaintModel?: string;
776
795
  /**
777
796
  * Show the Library sidebar trigger button. Defaults to `false`.
778
797
  * Set to `true` to show the library icon in the top-right area.
@@ -19,12 +19,17 @@ export declare function buildBrandContextMessage(ctx: BrandContext): string;
19
19
  */
20
20
  export declare function buildImageGenSafePrompt(prompt: string, brandContext: BrandContext, opts?: {
21
21
  /**
22
- * True when the user attached their own reference images. The brand logo
23
- * and font specimens are then not attached at all (see imageApi), so the
24
- * mandates that point at them — "embed the logo VERBATIM", "render ALL text
25
- * in this exact typeface" — must go too: they would order the model to
26
- * reproduce images it can no longer see, and they compete with the
27
- * reference the user actually chose.
22
+ * True when the user attached their own reference images (or, on quick
23
+ * edit, when there is a source image).
24
+ *
25
+ * This no longer suppresses the logo and font mandates. Brand assets are
26
+ * always attached now — the budget in utils/referenceBudget sheds them only
27
+ * under real payload pressure, not because the user supplied something —
28
+ * so the mandates still point at images the model can see.
29
+ *
30
+ * What it still does is demote brand *direction*: the "this ad is for brand
31
+ * X, everything must reflect it" line, which genuinely does fight a
32
+ * user-chosen reference for subject and style.
28
33
  */
29
34
  hasUserReferences?: boolean;
30
35
  }): string;
@@ -1,9 +1,25 @@
1
+ import { MAX_USER_REFERENCE_IMAGES } from "./referenceBudget";
2
+ export { MAX_USER_REFERENCE_IMAGES };
3
+ /** True for OpenAI's Images-API models, which are routed and shaped differently. */
4
+ export declare const isOpenAIImageModel: (modelId: string) => boolean;
5
+ /** Nearest preset ratio to `frameW / frameH`. */
6
+ export declare function nearestPresetAspectRatio(frameW: number, frameH: number): string;
1
7
  /**
2
- * Hard cap on user-supplied reference images per generation. Providers differ on
3
- * how many references they accept; six covers every realistic ad brief and keeps
4
- * the request well inside the limits of the models we route to.
8
+ * Picks the output size for a frame.
9
+ *
10
+ * For gpt-image-* this returns an exact `"1088x1360"` size matching the frame's
11
+ * real aspect ratio, so a 1080×1350 (4:5) frame no longer comes back as a 3:4
12
+ * image that has to be letterboxed to 1013×1350. It travels in the request's
13
+ * `size` field, NOT `resolution` — see the Images API body below. `aspectRatio`
14
+ * is null in that case: an exact size is authoritative and OpenRouter 400s if a
15
+ * ratio is sent alongside it (4:5 is not a valid preset anyway).
16
+ *
17
+ * Every other model keeps the preset-snapping behaviour it has always had.
5
18
  */
6
- export declare const MAX_USER_REFERENCE_IMAGES = 6;
19
+ export declare function resolveImageOutputSize(modelId: string, frameW: number, frameH: number): {
20
+ aspectRatio: string | null;
21
+ imageSize: string | null;
22
+ };
7
23
  /**
8
24
  * Describes each attached reference image by its real 1-based position, and
9
25
  * states the precedence rule between them.
@@ -20,8 +36,18 @@ export declare const MAX_USER_REFERENCE_IMAGES = 6;
20
36
  * Returns "" when there is nothing worth numbering, so prompts that carry no
21
37
  * attachments are left byte-identical to before.
22
38
  */
23
- export declare function buildReferenceManifest(userReferenceCount: number, brandLabels: string[]): string;
24
- export declare function callOpenRouterImageAPI(prompt: string, modelId: string, aspectRatio: string, imageSize: string | null, textOutput: boolean, imageGenUrl: string, referenceImageDataUrl?: string | string[], signal?: AbortSignal, brandImages?: {
39
+ export declare function buildReferenceManifest(userReferenceCount: number, brandLabels: string[],
40
+ /** The brand logo survived the budget and is attached. */
41
+ logoIsAttached?: boolean,
42
+ /** A source image (quick edit / resize) is attached below the user's refs. */
43
+ hasSourceImage?: boolean): string;
44
+ export declare function callOpenRouterImageAPI(prompt: string, modelId: string,
45
+ /**
46
+ * Preset ratio label ("4:5" is NOT one). Pass null when `imageSize` carries an
47
+ * exact `WIDTHxHEIGHT` resolution — that is authoritative, and sending a
48
+ * conflicting (or unsupported) ratio alongside it only muddies the request.
49
+ */
50
+ aspectRatio: string | null, imageSize: string | null, textOutput: boolean, imageGenUrl: string, referenceImageDataUrl?: string | string[], signal?: AbortSignal, brandImages?: {
25
51
  logo?: string;
26
52
  fontPreview?: string;
27
53
  /** Optional second font preview (brand body typeface) shown as a reference. */
@@ -56,4 +82,24 @@ export declare function callOpenRouterImageAPI(prompt: string, modelId: string,
56
82
  * manifest, so those callers are unaffected by this option.
57
83
  */
58
84
  referenceMode?: "user-priority" | "legacy-positional";
85
+ /**
86
+ * The image being edited or resized. Ranks directly below the user's own
87
+ * references and above the logo — on those surfaces the source IS the
88
+ * subject, so it must never be shed, and the logo becomes droppable.
89
+ */
90
+ sourceImageDataUrl?: string;
91
+ /**
92
+ * Whether the logo is part of the protected core. True on generate flows
93
+ * (the floor is 6 user references + the logo); false on quick edit and
94
+ * resize, where the source image outranks it.
95
+ */
96
+ protectLogo?: boolean;
97
+ /**
98
+ * How many brand-bank assets / ad references the host proxy may append.
99
+ * The SDK cannot see those images, so it reserves
100
+ * {@link HOST_ASSET_BYTE_RESERVATION} of budget per slot and tells the host
101
+ * how many survived; the host compresses each one to fit its reservation.
102
+ */
103
+ hostBrandAssetSlots?: number;
104
+ hostAdReferenceSlots?: number;
59
105
  }): Promise<string>;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Shared image-compression primitives.
3
+ *
4
+ * Extracted from `components/ai-chat/imagePayload` so the image-generation
5
+ * surfaces (canvas chat, generator panel, quick edit, auto-resize) can use the
6
+ * same downscaling the chat request path has always used. Previously only chat
7
+ * shrank its images, which is why an image-gen request could still carry a raw
8
+ * 1 MB PNG straight at the host proxy's 4.5 MB platform cap.
9
+ *
10
+ * Everything here is best-effort and never throws: on any failure the original
11
+ * data URL is returned unchanged, and the caller's budget pass is the backstop.
12
+ */
13
+ /** Long-edge cap in CSS pixels. Well above what vision models resolve. */
14
+ export declare const MAX_EDGE_PX = 1280;
15
+ /** JPEG quality for re-encoded images. */
16
+ export declare const JPEG_QUALITY = 0.78;
17
+ /**
18
+ * Images already smaller than this are passed through untouched — re-encoding
19
+ * a 40KB thumbnail costs a canvas round-trip and can make it *larger*.
20
+ */
21
+ export declare const PASSTHROUGH_BYTES: number;
22
+ /** Estimated wire size of a data URL: the base64 payload is what gets sent. */
23
+ export declare function dataUrlWireBytes(dataUrl: string): number;
24
+ /**
25
+ * Whether a data URL is a raster format safe to send as an `image_url` block.
26
+ * SVG is commonly rejected outright by vision models, and canvas re-encoding
27
+ * of an SVG is unreliable across browsers, so it is neither shrunk nor sent.
28
+ */
29
+ export declare function isRasterImageDataUrl(dataUrl: string): boolean;
30
+ /**
31
+ * Downscale + re-encode a raster data URL so it is safe to embed in a request.
32
+ *
33
+ * Never throws: on any failure (decode error, no canvas context, non-raster
34
+ * input) the original data URL is returned unchanged.
35
+ */
36
+ export declare function shrinkImageDataUrl(dataUrl: string): Promise<string>;
37
+ /** Shrink a batch in parallel, preserving order. */
38
+ export declare function shrinkImageDataUrls(dataUrls: string[]): Promise<string[]>;
39
+ /**
40
+ * Force a re-encode at a specific long-edge cap, ignoring the passthrough rule.
41
+ * Used to escalate compression when a request is still over budget after the
42
+ * standard downscale — always preferable to dropping a reference outright.
43
+ */
44
+ export declare function reencodeAtEdge(dataUrl: string, edge: number, quality?: number): Promise<string>;
45
+ /** Escalation ladder used when the standard downscale is not enough. */
46
+ export declare const ESCALATION_EDGES: readonly [960, 720, 512];