@pixerate/editor 0.10.0 → 0.12.0

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.
package/dist/index.js CHANGED
@@ -1277,10 +1277,10 @@ var HorizontalRule$1 = HorizontalRule.extend({
1277
1277
  };
1278
1278
  }
1279
1279
  });
1280
- var Image = Node$1.create({
1280
+ var Image2 = Node$1.create({
1281
1281
  name: "image"
1282
1282
  });
1283
- var Image$1 = Image.extend({
1283
+ var Image$1 = Image2.extend({
1284
1284
  /**
1285
1285
  * @return {{markdown: MarkdownNodeSpec}}
1286
1286
  */
@@ -1993,7 +1993,7 @@ function handleImageInsertion(view, file, options) {
1993
1993
  }
1994
1994
  }
1995
1995
  }
1996
- var Image2 = BaseImage.extend({
1996
+ var Image3 = BaseImage.extend({
1997
1997
  addOptions() {
1998
1998
  return {
1999
1999
  ...this.parent?.(),
@@ -2188,7 +2188,7 @@ function createRichTextPreset(options = {}) {
2188
2188
  if (options.image !== false) {
2189
2189
  const imageConfig = typeof options.image === "object" ? options.image : {};
2190
2190
  extensions2.push(
2191
- Image2.configure({
2191
+ Image3.configure({
2192
2192
  HTMLAttributes: {
2193
2193
  class: "rounded-md max-w-full h-auto my-2",
2194
2194
  ...imageConfig.HTMLAttributes || {}
@@ -4453,6 +4453,79 @@ function placeBeside(rects, size, side, nearNodeId, options = {}) {
4453
4453
  };
4454
4454
  }
4455
4455
 
4456
+ // src/canvas/clock.ts
4457
+ var pageHidden = () => typeof document !== "undefined" && document.hidden;
4458
+ var realClock = {
4459
+ live: true,
4460
+ now: () => typeof performance !== "undefined" ? performance.now() : Date.now(),
4461
+ frame: () => new Promise((resolve) => {
4462
+ if (typeof requestAnimationFrame === "function" && !pageHidden()) {
4463
+ requestAnimationFrame(() => resolve());
4464
+ } else {
4465
+ setTimeout(resolve, 16);
4466
+ }
4467
+ }),
4468
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)))
4469
+ };
4470
+ var settle = () => new Promise((resolve) => setTimeout(resolve, 0));
4471
+ var ManualClock = class {
4472
+ live = false;
4473
+ time;
4474
+ frames = [];
4475
+ sleepers = [];
4476
+ seq = 0;
4477
+ constructor(start = 0) {
4478
+ this.time = start;
4479
+ }
4480
+ now() {
4481
+ return this.time;
4482
+ }
4483
+ frame() {
4484
+ return new Promise((resolve) => this.frames.push(resolve));
4485
+ }
4486
+ sleep(ms) {
4487
+ return new Promise(
4488
+ (resolve) => this.sleepers.push({
4489
+ at: this.time + Math.max(0, ms),
4490
+ seq: this.seq++,
4491
+ resolve
4492
+ })
4493
+ );
4494
+ }
4495
+ /** Whether any frames or sleep timers are currently pending. */
4496
+ get pending() {
4497
+ return this.frames.length > 0 || this.sleepers.length > 0;
4498
+ }
4499
+ /** Advances time by `ms`, resolving due sleep timers in order, followed by queued frames. */
4500
+ async advance(ms) {
4501
+ const end = this.time + Math.max(0, ms);
4502
+ await settle();
4503
+ while (true) {
4504
+ let next;
4505
+ for (const s of this.sleepers) {
4506
+ if (s.at <= end && (!next || s.at < next.at || s.at === next.at && s.seq < next.seq)) {
4507
+ next = s;
4508
+ }
4509
+ }
4510
+ if (!next) break;
4511
+ this.sleepers.splice(this.sleepers.indexOf(next), 1);
4512
+ this.time = Math.max(this.time, next.at);
4513
+ next.resolve();
4514
+ await settle();
4515
+ }
4516
+ this.time = end;
4517
+ const dueFrames = this.frames.splice(0, this.frames.length);
4518
+ for (const resolveFrame of dueFrames) {
4519
+ resolveFrame();
4520
+ }
4521
+ await settle();
4522
+ }
4523
+ /** Convenience method to advance by 1 frame at a given fps (default: 60fps = 16.67ms). */
4524
+ async tick(fps = 60) {
4525
+ await this.advance(Math.round(1e3 / fps));
4526
+ }
4527
+ };
4528
+
4456
4529
  // src/canvas/trajectory.ts
4457
4530
  var defaultEasing = (t) => --t * t * t + 1;
4458
4531
  var linearTrajectory = (origin, target, progress) => {
@@ -4520,9 +4593,15 @@ function runMultiNodeTransition(transitions, options = {}) {
4520
4593
  return () => {
4521
4594
  };
4522
4595
  }
4523
- const { defaultDuration = 400, defaultEasing: easingFunc = defaultEasing, onUpdate, onComplete } = options;
4596
+ const {
4597
+ defaultDuration = 400,
4598
+ defaultEasing: easingFunc = defaultEasing,
4599
+ clock = realClock,
4600
+ onUpdate,
4601
+ onComplete
4602
+ } = options;
4524
4603
  let isCancelled = false;
4525
- const startTime = Date.now();
4604
+ const startTime = clock.now();
4526
4605
  const items = transitions.map((tr, i) => ({
4527
4606
  ...tr,
4528
4607
  index: tr.index ?? i,
@@ -4534,7 +4613,7 @@ function runMultiNodeTransition(transitions, options = {}) {
4534
4613
  }));
4535
4614
  function frame() {
4536
4615
  if (isCancelled) return;
4537
- const now = Date.now();
4616
+ const now = clock.now();
4538
4617
  let allCompleted = true;
4539
4618
  for (let i = 0; i < items.length; i++) {
4540
4619
  const item = items[i];
@@ -4566,9 +4645,7 @@ function runMultiNodeTransition(transitions, options = {}) {
4566
4645
  }
4567
4646
  onUpdate?.();
4568
4647
  if (!allCompleted) {
4569
- if (typeof requestAnimationFrame !== "undefined") {
4570
- requestAnimationFrame(frame);
4571
- }
4648
+ clock.frame().then(frame);
4572
4649
  } else {
4573
4650
  onComplete?.();
4574
4651
  }
@@ -4846,10 +4923,76 @@ function repositionNodesTo(nodes, targetOrigin) {
4846
4923
  }));
4847
4924
  }
4848
4925
 
4926
+ // src/canvas/view.ts
4927
+ function clampHeight(contentHeight, options = {}) {
4928
+ const { min = 320, max = 560, padding = 48 } = options;
4929
+ return Math.round(Math.min(max, Math.max(min, contentHeight + padding)));
4930
+ }
4931
+ function layoutBounds(nodes) {
4932
+ if (nodes.length === 0) return null;
4933
+ let minX = Infinity;
4934
+ let minY = Infinity;
4935
+ let maxX = -Infinity;
4936
+ let maxY = -Infinity;
4937
+ for (const node of nodes) {
4938
+ const x = node.position.x;
4939
+ const y = node.position.y;
4940
+ const width = node.width ?? 180;
4941
+ const height = node.height ?? 80;
4942
+ minX = Math.min(minX, x);
4943
+ minY = Math.min(minY, y);
4944
+ maxX = Math.max(maxX, x + width);
4945
+ maxY = Math.max(maxY, y + height);
4946
+ }
4947
+ if (minX === Infinity) return null;
4948
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
4949
+ }
4950
+ function canvasHeight(nodes, options) {
4951
+ return clampHeight(layoutBounds(nodes)?.height ?? 0, options);
4952
+ }
4953
+ var DEFAULT_RANK_HEIGHT = 80 + 72;
4954
+ var NODES_PER_RANK = 2;
4955
+ function estimateCanvasHeight(nodes, edges, options) {
4956
+ const ranks = Math.max(1, Math.ceil(nodes.length / NODES_PER_RANK));
4957
+ return clampHeight(ranks * DEFAULT_RANK_HEIGHT, options);
4958
+ }
4959
+ function mix(a, b, t) {
4960
+ return a + (b - a) * t;
4961
+ }
4962
+ function blendLayout(from, to, t) {
4963
+ if (t >= 1) return to;
4964
+ const fromMap = new Map(from.map((n) => [n.id, n]));
4965
+ return to.map((target) => {
4966
+ const origin = fromMap.get(target.id);
4967
+ if (!origin) return target;
4968
+ return {
4969
+ ...target,
4970
+ position: {
4971
+ x: mix(origin.position.x, target.position.x, t),
4972
+ y: mix(origin.position.y, target.position.y, t)
4973
+ },
4974
+ width: origin.width !== void 0 && target.width !== void 0 ? mix(origin.width, target.width, t) : target.width,
4975
+ height: origin.height !== void 0 && target.height !== void 0 ? mix(origin.height, target.height, t) : target.height
4976
+ };
4977
+ });
4978
+ }
4979
+ function connectedOnly(nodes, edges) {
4980
+ if (edges.length === 0) return nodes;
4981
+ const linkedNodeIds = /* @__PURE__ */ new Set();
4982
+ for (const edge of edges) {
4983
+ linkedNodeIds.add(edge.source);
4984
+ linkedNodeIds.add(edge.target);
4985
+ }
4986
+ const filtered = nodes.filter((node) => linkedNodeIds.has(node.id));
4987
+ return filtered.length > 0 ? filtered : nodes;
4988
+ }
4989
+
4849
4990
  // src/canvas/streaming.ts
4850
4991
  var StreamingLayoutManager = class {
4851
4992
  throttleMs;
4852
4993
  glideDurationMs;
4994
+ connectedOnly;
4995
+ clock;
4853
4996
  layoutOptions;
4854
4997
  onLayoutUpdated;
4855
4998
  pendingTimer = null;
@@ -4858,6 +5001,8 @@ var StreamingLayoutManager = class {
4858
5001
  constructor(options = {}) {
4859
5002
  this.throttleMs = options.throttleMs ?? 200;
4860
5003
  this.glideDurationMs = options.glideDurationMs ?? 300;
5004
+ this.connectedOnly = options.connectedOnly ?? false;
5005
+ this.clock = options.clock ?? realClock;
4861
5006
  this.layoutOptions = options.layoutOptions ?? { direction: "LR" };
4862
5007
  this.onLayoutUpdated = options.onLayoutUpdated;
4863
5008
  }
@@ -4885,7 +5030,8 @@ var StreamingLayoutManager = class {
4885
5030
  this.recalculateAndGlide(nodes, edges, currentPositions);
4886
5031
  }
4887
5032
  recalculateAndGlide(nodes, edges, currentPositions) {
4888
- if (nodes.length === 0) return;
5033
+ const targetNodes = this.connectedOnly ? connectedOnly(nodes, edges) : nodes;
5034
+ if (targetNodes.length === 0) return;
4889
5035
  if (this.activeTransitionCancel) {
4890
5036
  this.activeTransitionCancel();
4891
5037
  this.activeTransitionCancel = null;
@@ -4894,7 +5040,7 @@ var StreamingLayoutManager = class {
4894
5040
  for (const [id, pos] of currentPositions.entries()) {
4895
5041
  stored[id] = pos;
4896
5042
  }
4897
- const layouted = ensureLayout(nodes, edges, {
5043
+ const layouted = ensureLayout(targetNodes, edges, {
4898
5044
  ...this.layoutOptions,
4899
5045
  storedPositions: stored
4900
5046
  });
@@ -4918,6 +5064,7 @@ var StreamingLayoutManager = class {
4918
5064
  this.targetPositions = nextPositions;
4919
5065
  if (transitions.length > 0) {
4920
5066
  this.activeTransitionCancel = runMultiNodeTransition(transitions, {
5067
+ clock: this.clock,
4921
5068
  onUpdate: () => {
4922
5069
  const liveMap = new Map(currentPositions);
4923
5070
  for (const trans of transitions) {
@@ -5125,6 +5272,24 @@ function planAgentCommand(command, snapshot, options = {}) {
5125
5272
  }
5126
5273
  ];
5127
5274
  }
5275
+ case "choreography:say": {
5276
+ return [
5277
+ {
5278
+ type: "step:say",
5279
+ text: command.text,
5280
+ atNodeId: command.atNodeId,
5281
+ position: command.position
5282
+ }
5283
+ ];
5284
+ }
5285
+ case "choreography:wait": {
5286
+ return [
5287
+ {
5288
+ type: "step:wait",
5289
+ ms: command.ms
5290
+ }
5291
+ ];
5292
+ }
5128
5293
  default: {
5129
5294
  return [];
5130
5295
  }
@@ -5249,20 +5414,1341 @@ var MCP_TOOL_EDIT_PROMPT = {
5249
5414
  required: ["operation"]
5250
5415
  }
5251
5416
  };
5417
+ var MCP_TOOL_VIEW_EDITOR = {
5418
+ name: "view_editor",
5419
+ description: "Captures a visual image snapshot (JPEG or PNG) of the editor canvas or viewport for multimodal evaluation. Active agent cursor indicators are automatically excluded from the snapshot.",
5420
+ inputSchema: {
5421
+ type: "object",
5422
+ properties: {
5423
+ scope: {
5424
+ type: "string",
5425
+ enum: ["viewport", "canvas"],
5426
+ description: "Whether to capture only the visible viewport or the full canvas bounding box (default: viewport)."
5427
+ },
5428
+ format: {
5429
+ type: "string",
5430
+ enum: ["jpeg", "png"],
5431
+ description: "Image format of the base64 snapshot (default: jpeg)."
5432
+ },
5433
+ maxEdge: {
5434
+ type: "integer",
5435
+ description: "Maximum width or height dimension in pixels (default: 1024)."
5436
+ }
5437
+ }
5438
+ }
5439
+ };
5440
+ var EXPORT_EXCLUDED_SELECTORS = [
5441
+ ".pixerate-presence",
5442
+ ".agent-presence",
5443
+ ".ursula-presence"
5444
+ ];
5252
5445
  var EDITOR_MCP_TOOLS = [
5253
5446
  MCP_TOOL_GET_EDITOR_STATE,
5254
5447
  MCP_TOOL_EDIT_CANVAS,
5255
- MCP_TOOL_EDIT_PROMPT
5448
+ MCP_TOOL_EDIT_PROMPT,
5449
+ MCP_TOOL_VIEW_EDITOR
5256
5450
  ];
5451
+
5452
+ // src/image-editor/filters.ts
5453
+ function clamp(value, min = 0, max = 255) {
5454
+ return Math.max(min, Math.min(max, value));
5455
+ }
5456
+ function applyAdjustments(imageData, adjustments) {
5457
+ const { brightness, contrast, saturation, exposure, temperature, opacity, blur } = adjustments;
5458
+ const data = imageData.data;
5459
+ const len = data.length;
5460
+ const hasBrightness = brightness !== 0;
5461
+ const hasContrast = contrast !== 0;
5462
+ const hasSaturation = saturation !== 0;
5463
+ const hasExposure = exposure !== 0;
5464
+ const hasTemperature = temperature !== 0;
5465
+ const hasOpacity = opacity < 1;
5466
+ const contrastFactor = hasContrast ? 259 * (contrast + 255) / (255 * (259 - contrast)) : 1;
5467
+ const exposureMultiplier = hasExposure ? Math.pow(2, exposure / 50) : 1;
5468
+ const satMultiplier = hasSaturation ? 1 + saturation / 100 : 1;
5469
+ const tempOffset = hasTemperature ? temperature * 0.8 : 0;
5470
+ if (hasBrightness || hasContrast || hasSaturation || hasExposure || hasTemperature || hasOpacity) {
5471
+ for (let i = 0; i < len; i += 4) {
5472
+ let r = data[i];
5473
+ let g = data[i + 1];
5474
+ let b = data[i + 2];
5475
+ let a = data[i + 3];
5476
+ if (hasExposure) {
5477
+ r = clamp(r * exposureMultiplier);
5478
+ g = clamp(g * exposureMultiplier);
5479
+ b = clamp(b * exposureMultiplier);
5480
+ }
5481
+ if (hasBrightness) {
5482
+ const bOffset = brightness * 2.55;
5483
+ r = clamp(r + bOffset);
5484
+ g = clamp(g + bOffset);
5485
+ b = clamp(b + bOffset);
5486
+ }
5487
+ if (hasContrast) {
5488
+ r = clamp(contrastFactor * (r - 128) + 128);
5489
+ g = clamp(contrastFactor * (g - 128) + 128);
5490
+ b = clamp(contrastFactor * (b - 128) + 128);
5491
+ }
5492
+ if (hasSaturation) {
5493
+ const luminance = 0.2989 * r + 0.587 * g + 0.114 * b;
5494
+ r = clamp(luminance + (r - luminance) * satMultiplier);
5495
+ g = clamp(luminance + (g - luminance) * satMultiplier);
5496
+ b = clamp(luminance + (b - luminance) * satMultiplier);
5497
+ }
5498
+ if (hasTemperature) {
5499
+ r = clamp(r + tempOffset);
5500
+ b = clamp(b - tempOffset);
5501
+ }
5502
+ if (hasOpacity) {
5503
+ a = clamp(a * opacity);
5504
+ }
5505
+ data[i] = r;
5506
+ data[i + 1] = g;
5507
+ data[i + 2] = b;
5508
+ data[i + 3] = a;
5509
+ }
5510
+ }
5511
+ if (blur > 0) {
5512
+ applyFastBoxBlur(imageData, Math.min(Math.round(blur), 40));
5513
+ }
5514
+ }
5515
+ function applyFastBoxBlur(imageData, radius) {
5516
+ if (radius < 1) return;
5517
+ const w = imageData.width;
5518
+ const h = imageData.height;
5519
+ const src = imageData.data;
5520
+ const temp = new Uint8ClampedArray(src.length);
5521
+ for (let y = 0; y < h; y++) {
5522
+ for (let x = 0; x < w; x++) {
5523
+ let r = 0, g = 0, b = 0, a = 0;
5524
+ let count = 0;
5525
+ for (let k = -radius; k <= radius; k++) {
5526
+ const px = Math.min(w - 1, Math.max(0, x + k));
5527
+ const idx = (y * w + px) * 4;
5528
+ r += src[idx];
5529
+ g += src[idx + 1];
5530
+ b += src[idx + 2];
5531
+ a += src[idx + 3];
5532
+ count++;
5533
+ }
5534
+ const dstIdx = (y * w + x) * 4;
5535
+ temp[dstIdx] = Math.round(r / count);
5536
+ temp[dstIdx + 1] = Math.round(g / count);
5537
+ temp[dstIdx + 2] = Math.round(b / count);
5538
+ temp[dstIdx + 3] = Math.round(a / count);
5539
+ }
5540
+ }
5541
+ for (let x = 0; x < w; x++) {
5542
+ for (let y = 0; y < h; y++) {
5543
+ let r = 0, g = 0, b = 0, a = 0;
5544
+ let count = 0;
5545
+ for (let k = -radius; k <= radius; k++) {
5546
+ const py = Math.min(h - 1, Math.max(0, y + k));
5547
+ const idx = (py * w + x) * 4;
5548
+ r += temp[idx];
5549
+ g += temp[idx + 1];
5550
+ b += temp[idx + 2];
5551
+ a += temp[idx + 3];
5552
+ count++;
5553
+ }
5554
+ const dstIdx = (y * w + x) * 4;
5555
+ src[dstIdx] = Math.round(r / count);
5556
+ src[dstIdx + 1] = Math.round(g / count);
5557
+ src[dstIdx + 2] = Math.round(b / count);
5558
+ src[dstIdx + 3] = Math.round(a / count);
5559
+ }
5560
+ }
5561
+ }
5562
+ function applyDepthMask(mainImageData, maskImageData, options) {
5563
+ const { mode, depthRange, softness, invert } = options;
5564
+ const mainData = mainImageData.data;
5565
+ const maskData = maskImageData.data;
5566
+ const len = Math.min(mainData.length, maskData.length);
5567
+ const effectiveSoftness = Math.max(1, softness || 10);
5568
+ const minDepth = depthRange[0];
5569
+ const maxDepth = depthRange[1];
5570
+ for (let i = 0; i < len; i += 4) {
5571
+ let maskValue = maskData[i];
5572
+ if (invert) {
5573
+ maskValue = 255 - maskValue;
5574
+ }
5575
+ let alphaValue;
5576
+ if (mode === "threshold") {
5577
+ if (maskValue >= maxDepth + effectiveSoftness) {
5578
+ alphaValue = 0;
5579
+ } else if (maskValue > maxDepth) {
5580
+ alphaValue = Math.round((maxDepth + effectiveSoftness - maskValue) / effectiveSoftness * 255);
5581
+ } else if (maskValue >= minDepth) {
5582
+ alphaValue = 255;
5583
+ } else if (maskValue < minDepth - effectiveSoftness) {
5584
+ alphaValue = 0;
5585
+ } else {
5586
+ alphaValue = Math.round((maskValue - (minDepth - effectiveSoftness)) / effectiveSoftness * 255);
5587
+ }
5588
+ } else {
5589
+ alphaValue = clamp(maskValue + minDepth);
5590
+ }
5591
+ mainData[i + 3] = clamp(mainData[i + 3] * alphaValue / 255);
5592
+ }
5593
+ }
5594
+ function removeAlpha(imageData) {
5595
+ const data = imageData.data;
5596
+ const len = data.length;
5597
+ for (let i = 3; i < len; i += 4) {
5598
+ data[i] = 255;
5599
+ }
5600
+ }
5601
+
5602
+ // src/image-editor/renderer.ts
5603
+ var ImageCache = class {
5604
+ cache = /* @__PURE__ */ new Map();
5605
+ async get(src) {
5606
+ const existing = this.cache.get(src);
5607
+ if (existing && existing.complete) {
5608
+ return existing;
5609
+ }
5610
+ return new Promise((resolve, reject) => {
5611
+ if (typeof window === "undefined" || typeof Image === "undefined") {
5612
+ reject(new Error("Image loading is only supported in browser/DOM environments"));
5613
+ return;
5614
+ }
5615
+ const img = new Image();
5616
+ img.crossOrigin = "anonymous";
5617
+ img.onload = () => {
5618
+ this.cache.set(src, img);
5619
+ resolve(img);
5620
+ };
5621
+ img.onerror = (e) => reject(new Error(`Failed to load image from: ${src}`));
5622
+ img.src = src;
5623
+ });
5624
+ }
5625
+ set(src, img) {
5626
+ this.cache.set(src, img);
5627
+ }
5628
+ clear() {
5629
+ this.cache.clear();
5630
+ }
5631
+ };
5632
+ var globalImageCache = new ImageCache();
5633
+ var ImageEditorRenderer = class {
5634
+ imageCache = globalImageCache;
5635
+ cachedBaseCanvas = null;
5636
+ cachedBaseKey = "";
5637
+ /**
5638
+ * Helper to create a canvas element safely in browser/JSDOM.
5639
+ */
5640
+ createCanvas(width, height) {
5641
+ if (typeof document !== "undefined" && document.createElement) {
5642
+ const c = document.createElement("canvas");
5643
+ c.width = Math.max(1, Math.round(width));
5644
+ c.height = Math.max(1, Math.round(height));
5645
+ return c;
5646
+ }
5647
+ return {
5648
+ width: Math.max(1, Math.round(width)),
5649
+ height: Math.max(1, Math.round(height)),
5650
+ getContext: () => null,
5651
+ toDataURL: () => "",
5652
+ toBlob: () => {
5653
+ }
5654
+ };
5655
+ }
5656
+ /**
5657
+ * Renders the editor state onto a canvas (viewport or preview).
5658
+ */
5659
+ async renderToCanvas(targetCanvas, imageSource, state, options = {}) {
5660
+ const ctx = targetCanvas.getContext("2d");
5661
+ if (!ctx) return;
5662
+ let img;
5663
+ if (typeof imageSource === "string") {
5664
+ img = await this.imageCache.get(imageSource);
5665
+ } else {
5666
+ img = imageSource;
5667
+ }
5668
+ const naturalWidth = img.naturalWidth || img.videoWidth || img.width || 800;
5669
+ const naturalHeight = img.naturalHeight || img.videoHeight || img.height || 600;
5670
+ const { crop, transform, adjustments, depthMask, annotations, selectedAnnotationId, zoom, pan, isOriginalCompared } = state;
5671
+ const vpWidth = options.viewportWidth || targetCanvas.width;
5672
+ const vpHeight = options.viewportHeight || targetCanvas.height;
5673
+ targetCanvas.width = vpWidth;
5674
+ targetCanvas.height = vpHeight;
5675
+ ctx.clearRect(0, 0, vpWidth, vpHeight);
5676
+ if (isOriginalCompared) {
5677
+ this.drawFittedImage(ctx, img, naturalWidth, naturalHeight, vpWidth, vpHeight, zoom, pan);
5678
+ return;
5679
+ }
5680
+ const isActivelyCropping = Boolean(options.showCropOverlay || state.activeTool === "crop");
5681
+ const cropX = !isActivelyCropping && crop ? Math.max(0, crop.x) : 0;
5682
+ const cropY = !isActivelyCropping && crop ? Math.max(0, crop.y) : 0;
5683
+ const cropW = !isActivelyCropping && crop ? Math.min(naturalWidth - cropX, crop.width) : naturalWidth;
5684
+ const cropH = !isActivelyCropping && crop ? Math.min(naturalHeight - cropY, crop.height) : naturalHeight;
5685
+ const angle = (transform.rotate % 360 + 360) % 360;
5686
+ const isSwapped = angle === 90 || angle === 270;
5687
+ const baseW = isSwapped ? cropH : cropW;
5688
+ const baseH = isSwapped ? cropW : cropH;
5689
+ const baseKey = `${state.sourceUrl}-${naturalWidth}x${naturalHeight}-${cropX},${cropY},${cropW},${cropH}-${angle},${transform.flipH},${transform.flipV}-${adjustments.brightness},${adjustments.contrast},${adjustments.saturation},${adjustments.exposure},${adjustments.temperature},${adjustments.blur},${adjustments.opacity}-${depthMask.enabled},${depthMask.depthRange?.[0]}-${depthMask.depthRange?.[1]},${depthMask.softness},${depthMask.invert},${depthMask.mode}`;
5690
+ let baseCanvas = this.cachedBaseCanvas;
5691
+ if (!baseCanvas || this.cachedBaseKey !== baseKey) {
5692
+ baseCanvas = this.createCanvas(baseW, baseH);
5693
+ const baseCtx = baseCanvas.getContext("2d");
5694
+ if (baseCtx) {
5695
+ baseCtx.save();
5696
+ const rad = angle * Math.PI / 180;
5697
+ baseCtx.translate(baseW / 2, baseH / 2);
5698
+ baseCtx.rotate(rad);
5699
+ baseCtx.scale(transform.flipH ? -1 : 1, transform.flipV ? -1 : 1);
5700
+ baseCtx.drawImage(
5701
+ img,
5702
+ cropX,
5703
+ cropY,
5704
+ cropW,
5705
+ cropH,
5706
+ -cropW / 2,
5707
+ -cropH / 2,
5708
+ cropW,
5709
+ cropH
5710
+ );
5711
+ baseCtx.restore();
5712
+ const imageData = baseCtx.getImageData(0, 0, baseW, baseH);
5713
+ if (imageData) {
5714
+ applyAdjustments(imageData, adjustments);
5715
+ if (depthMask.enabled && depthMask.maskSource) {
5716
+ try {
5717
+ const maskImg = await this.imageCache.get(depthMask.maskSource);
5718
+ const maskCanvas = this.createCanvas(baseW, baseH);
5719
+ const maskCtx = maskCanvas.getContext("2d");
5720
+ if (maskCtx) {
5721
+ maskCtx.drawImage(maskImg, cropX, cropY, cropW, cropH, 0, 0, baseW, baseH);
5722
+ const maskData = maskCtx.getImageData(0, 0, baseW, baseH);
5723
+ if (maskData) {
5724
+ applyDepthMask(imageData, maskData, depthMask);
5725
+ }
5726
+ }
5727
+ } catch {
5728
+ }
5729
+ }
5730
+ baseCtx.putImageData(imageData, 0, 0);
5731
+ }
5732
+ }
5733
+ this.cachedBaseCanvas = baseCanvas;
5734
+ this.cachedBaseKey = baseKey;
5735
+ }
5736
+ ctx.save();
5737
+ ctx.translate(vpWidth / 2 + pan.x, vpHeight / 2 + pan.y);
5738
+ ctx.scale(zoom, zoom);
5739
+ const fitScale = Math.min(vpWidth * 0.85 / baseW, vpHeight * 0.85 / baseH, 1);
5740
+ const renderW = baseW * fitScale;
5741
+ const renderH = baseH * fitScale;
5742
+ ctx.drawImage(baseCanvas, -renderW / 2, -renderH / 2, renderW, renderH);
5743
+ const scaleFactor = fitScale;
5744
+ ctx.save();
5745
+ ctx.translate(-renderW / 2, -renderH / 2);
5746
+ ctx.scale(scaleFactor, scaleFactor);
5747
+ for (const ann of annotations) {
5748
+ await this.drawAnnotation(ctx, ann);
5749
+ if (options.showSelectionOverlay && ann.id === selectedAnnotationId) {
5750
+ this.drawAnnotationSelection(ctx, ann);
5751
+ }
5752
+ }
5753
+ if (options.draftAnnotation) {
5754
+ await this.drawAnnotation(ctx, options.draftAnnotation);
5755
+ }
5756
+ ctx.restore();
5757
+ ctx.restore();
5758
+ const isCroppingActive = Boolean(options.showCropOverlay || state.activeTool === "crop");
5759
+ const activeCrop = options.draftCrop !== void 0 && options.draftCrop !== null ? options.draftCrop : crop ?? (isCroppingActive ? {
5760
+ x: 0,
5761
+ y: 0,
5762
+ width: naturalWidth,
5763
+ height: naturalHeight
5764
+ } : null);
5765
+ if (isCroppingActive && activeCrop) {
5766
+ this.drawCropOverlay(ctx, state, vpWidth, vpHeight, naturalWidth, naturalHeight, activeCrop);
5767
+ }
5768
+ }
5769
+ /**
5770
+ * Draws an individual annotation onto the canvas.
5771
+ */
5772
+ async drawAnnotation(ctx, ann) {
5773
+ ctx.save();
5774
+ if (ann.opacity !== void 0) {
5775
+ ctx.globalAlpha = ann.opacity;
5776
+ }
5777
+ if (ann.rotation) {
5778
+ ctx.translate(ann.x, ann.y);
5779
+ ctx.rotate(ann.rotation * Math.PI / 180);
5780
+ ctx.translate(-ann.x, -ann.y);
5781
+ }
5782
+ switch (ann.type) {
5783
+ case "pen": {
5784
+ const pen = ann;
5785
+ if (pen.points.length > 0) {
5786
+ ctx.beginPath();
5787
+ ctx.strokeStyle = pen.strokeColor;
5788
+ ctx.lineWidth = pen.strokeWidth;
5789
+ ctx.lineCap = "round";
5790
+ ctx.lineJoin = "round";
5791
+ ctx.moveTo(pen.points[0].x, pen.points[0].y);
5792
+ for (let i = 1; i < pen.points.length; i++) {
5793
+ ctx.lineTo(pen.points[i].x, pen.points[i].y);
5794
+ }
5795
+ ctx.stroke();
5796
+ }
5797
+ break;
5798
+ }
5799
+ case "rect": {
5800
+ const rect = ann;
5801
+ ctx.strokeStyle = rect.strokeColor;
5802
+ ctx.lineWidth = rect.strokeWidth;
5803
+ if (rect.fillColor) {
5804
+ ctx.fillStyle = rect.fillColor;
5805
+ ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
5806
+ }
5807
+ if (rect.strokeWidth > 0) {
5808
+ ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
5809
+ }
5810
+ break;
5811
+ }
5812
+ case "circle": {
5813
+ const circle = ann;
5814
+ ctx.beginPath();
5815
+ ctx.strokeStyle = circle.strokeColor;
5816
+ ctx.lineWidth = circle.strokeWidth;
5817
+ ctx.ellipse(circle.x, circle.y, Math.abs(circle.radiusX), Math.abs(circle.radiusY), 0, 0, 2 * Math.PI);
5818
+ if (circle.fillColor) {
5819
+ ctx.fillStyle = circle.fillColor;
5820
+ ctx.fill();
5821
+ }
5822
+ if (circle.strokeWidth > 0) {
5823
+ ctx.stroke();
5824
+ }
5825
+ break;
5826
+ }
5827
+ case "arrow": {
5828
+ const arrow = ann;
5829
+ ctx.strokeStyle = arrow.strokeColor;
5830
+ ctx.fillStyle = arrow.strokeColor;
5831
+ ctx.lineWidth = arrow.strokeWidth;
5832
+ ctx.beginPath();
5833
+ ctx.moveTo(arrow.x, arrow.y);
5834
+ ctx.lineTo(arrow.endX, arrow.endY);
5835
+ ctx.stroke();
5836
+ const angle = Math.atan2(arrow.endY - arrow.y, arrow.endX - arrow.x);
5837
+ const headLen = Math.max(12, arrow.strokeWidth * 3);
5838
+ ctx.beginPath();
5839
+ ctx.moveTo(arrow.endX, arrow.endY);
5840
+ ctx.lineTo(
5841
+ arrow.endX - headLen * Math.cos(angle - Math.PI / 6),
5842
+ arrow.endY - headLen * Math.sin(angle - Math.PI / 6)
5843
+ );
5844
+ ctx.lineTo(
5845
+ arrow.endX - headLen * Math.cos(angle + Math.PI / 6),
5846
+ arrow.endY - headLen * Math.sin(angle + Math.PI / 6)
5847
+ );
5848
+ ctx.closePath();
5849
+ ctx.fill();
5850
+ break;
5851
+ }
5852
+ case "line": {
5853
+ const line = ann;
5854
+ ctx.beginPath();
5855
+ ctx.strokeStyle = line.strokeColor;
5856
+ ctx.lineWidth = line.strokeWidth;
5857
+ ctx.moveTo(line.x, line.y);
5858
+ ctx.lineTo(line.endX, line.endY);
5859
+ ctx.stroke();
5860
+ break;
5861
+ }
5862
+ case "text": {
5863
+ const textAnn = ann;
5864
+ ctx.font = `${textAnn.fontWeight || "normal"} ${textAnn.fontSize}px ${textAnn.fontFamily || "sans-serif"}`;
5865
+ ctx.fillStyle = textAnn.color;
5866
+ ctx.textBaseline = "top";
5867
+ ctx.fillText(textAnn.text, textAnn.x, textAnn.y);
5868
+ break;
5869
+ }
5870
+ case "image": {
5871
+ const imgAnn = ann;
5872
+ try {
5873
+ const loadedImg = await this.imageCache.get(imgAnn.src);
5874
+ ctx.drawImage(loadedImg, imgAnn.x, imgAnn.y, imgAnn.width, imgAnn.height);
5875
+ } catch {
5876
+ }
5877
+ break;
5878
+ }
5879
+ }
5880
+ ctx.restore();
5881
+ }
5882
+ /**
5883
+ * Draws selection box around active annotation.
5884
+ */
5885
+ drawAnnotationSelection(ctx, ann) {
5886
+ ctx.save();
5887
+ ctx.strokeStyle = "#3b82f6";
5888
+ ctx.lineWidth = 1.5;
5889
+ ctx.setLineDash([4, 4]);
5890
+ const bounds = this.getAnnotationBounds(ann);
5891
+ const pad = 4;
5892
+ ctx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.width + pad * 2, bounds.height + pad * 2);
5893
+ ctx.fillStyle = "#ffffff";
5894
+ ctx.strokeStyle = "#3b82f6";
5895
+ ctx.setLineDash([]);
5896
+ const handleSize = 6;
5897
+ const corners = [
5898
+ { x: bounds.x - pad, y: bounds.y - pad },
5899
+ { x: bounds.x + bounds.width + pad, y: bounds.y - pad },
5900
+ { x: bounds.x - pad, y: bounds.y + bounds.height + pad },
5901
+ { x: bounds.x + bounds.width + pad, y: bounds.y + bounds.height + pad }
5902
+ ];
5903
+ for (const c of corners) {
5904
+ ctx.fillRect(c.x - handleSize / 2, c.y - handleSize / 2, handleSize, handleSize);
5905
+ ctx.strokeRect(c.x - handleSize / 2, c.y - handleSize / 2, handleSize, handleSize);
5906
+ }
5907
+ ctx.restore();
5908
+ }
5909
+ getAnnotationBounds(ann) {
5910
+ switch (ann.type) {
5911
+ case "rect":
5912
+ return { x: ann.x, y: ann.y, width: ann.width, height: ann.height };
5913
+ case "circle":
5914
+ return {
5915
+ x: ann.x - Math.abs(ann.radiusX),
5916
+ y: ann.y - Math.abs(ann.radiusY),
5917
+ width: Math.abs(ann.radiusX) * 2,
5918
+ height: Math.abs(ann.radiusY) * 2
5919
+ };
5920
+ case "arrow":
5921
+ case "line": {
5922
+ const minX = Math.min(ann.x, ann.endX);
5923
+ const minY = Math.min(ann.y, ann.endY);
5924
+ return {
5925
+ x: minX,
5926
+ y: minY,
5927
+ width: Math.max(Math.abs(ann.endX - ann.x), 10),
5928
+ height: Math.max(Math.abs(ann.endY - ann.y), 10)
5929
+ };
5930
+ }
5931
+ case "pen": {
5932
+ if (!ann.points.length) return { x: ann.x, y: ann.y, width: 0, height: 0 };
5933
+ let minX = ann.points[0].x, maxX = ann.points[0].x;
5934
+ let minY = ann.points[0].y, maxY = ann.points[0].y;
5935
+ for (const pt of ann.points) {
5936
+ if (pt.x < minX) minX = pt.x;
5937
+ if (pt.x > maxX) maxX = pt.x;
5938
+ if (pt.y < minY) minY = pt.y;
5939
+ if (pt.y > maxY) maxY = pt.y;
5940
+ }
5941
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
5942
+ }
5943
+ case "text":
5944
+ return {
5945
+ x: ann.x,
5946
+ y: ann.y,
5947
+ width: ann.text.length * ann.fontSize * 0.6,
5948
+ height: ann.fontSize * 1.2
5949
+ };
5950
+ case "image":
5951
+ return { x: ann.x, y: ann.y, width: ann.width, height: ann.height };
5952
+ }
5953
+ }
5954
+ /**
5955
+ * Draws a rule-of-thirds crop grid overlay.
5956
+ */
5957
+ drawCropOverlay(ctx, state, vpWidth, vpHeight, imgWidth, imgHeight, cropOverride) {
5958
+ const crop = cropOverride !== void 0 && cropOverride !== null ? cropOverride : state.crop ?? {
5959
+ x: 0,
5960
+ y: 0,
5961
+ width: imgWidth,
5962
+ height: imgHeight
5963
+ };
5964
+ if (!crop) return;
5965
+ const { transform, pan, zoom } = state;
5966
+ const angle = (transform.rotate % 360 + 360) % 360;
5967
+ const isSwapped = angle === 90 || angle === 270;
5968
+ const baseW = isSwapped ? imgHeight : imgWidth;
5969
+ const baseH = isSwapped ? imgWidth : imgHeight;
5970
+ ctx.save();
5971
+ ctx.translate(vpWidth / 2 + pan.x, vpHeight / 2 + pan.y);
5972
+ ctx.scale(zoom, zoom);
5973
+ const fitScale = Math.min(vpWidth * 0.85 / baseW, vpHeight * 0.85 / baseH, 1);
5974
+ const renderW = baseW * fitScale;
5975
+ const renderH = baseH * fitScale;
5976
+ const startX = -renderW / 2 + crop.x / imgWidth * renderW;
5977
+ const startY = -renderH / 2 + crop.y / imgHeight * renderH;
5978
+ const cropBoxW = crop.width / imgWidth * renderW;
5979
+ const cropBoxH = crop.height / imgHeight * renderH;
5980
+ ctx.fillStyle = "rgba(0, 0, 0, 0.45)";
5981
+ ctx.fillRect(-renderW / 2, -renderH / 2, renderW, startY - -renderH / 2);
5982
+ ctx.fillRect(-renderW / 2, startY + cropBoxH, renderW, renderH / 2 - (startY + cropBoxH));
5983
+ ctx.fillRect(-renderW / 2, startY, startX - -renderW / 2, cropBoxH);
5984
+ ctx.fillRect(startX + cropBoxW, startY, renderW / 2 - (startX + cropBoxW), cropBoxH);
5985
+ ctx.strokeStyle = "#ffffff";
5986
+ ctx.lineWidth = 2;
5987
+ ctx.strokeRect(startX, startY, cropBoxW, cropBoxH);
5988
+ ctx.strokeStyle = "rgba(255, 255, 255, 0.4)";
5989
+ ctx.lineWidth = 1;
5990
+ ctx.beginPath();
5991
+ ctx.moveTo(startX + cropBoxW / 3, startY);
5992
+ ctx.lineTo(startX + cropBoxW / 3, startY + cropBoxH);
5993
+ ctx.moveTo(startX + cropBoxW * 2 / 3, startY);
5994
+ ctx.lineTo(startX + cropBoxW * 2 / 3, startY + cropBoxH);
5995
+ ctx.moveTo(startX, startY + cropBoxH / 3);
5996
+ ctx.lineTo(startX + cropBoxW, startY + cropBoxH / 3);
5997
+ ctx.moveTo(startX, startY + cropBoxH * 2 / 3);
5998
+ ctx.lineTo(startX + cropBoxW, startY + cropBoxH * 2 / 3);
5999
+ ctx.stroke();
6000
+ ctx.fillStyle = "#ffffff";
6001
+ ctx.strokeStyle = "#0284c7";
6002
+ ctx.lineWidth = 1.5;
6003
+ const handleSize = 10;
6004
+ const handles = [
6005
+ // Corners
6006
+ { x: startX, y: startY },
6007
+ { x: startX + cropBoxW, y: startY },
6008
+ { x: startX, y: startY + cropBoxH },
6009
+ { x: startX + cropBoxW, y: startY + cropBoxH },
6010
+ // Edges
6011
+ { x: startX + cropBoxW / 2, y: startY },
6012
+ { x: startX + cropBoxW / 2, y: startY + cropBoxH },
6013
+ { x: startX, y: startY + cropBoxH / 2 },
6014
+ { x: startX + cropBoxW, y: startY + cropBoxH / 2 }
6015
+ ];
6016
+ for (const h of handles) {
6017
+ ctx.fillRect(h.x - handleSize / 2, h.y - handleSize / 2, handleSize, handleSize);
6018
+ ctx.strokeRect(h.x - handleSize / 2, h.y - handleSize / 2, handleSize, handleSize);
6019
+ }
6020
+ ctx.restore();
6021
+ }
6022
+ drawFittedImage(ctx, img, imgW, imgH, vpW, vpH, zoom, pan) {
6023
+ ctx.save();
6024
+ ctx.translate(vpW / 2 + pan.x, vpH / 2 + pan.y);
6025
+ ctx.scale(zoom, zoom);
6026
+ const fitScale = Math.min(vpW * 0.85 / imgW, vpH * 0.85 / imgH, 1);
6027
+ const renderW = imgW * fitScale;
6028
+ const renderH = imgH * fitScale;
6029
+ ctx.drawImage(img, -renderW / 2, -renderH / 2, renderW, renderH);
6030
+ ctx.restore();
6031
+ }
6032
+ /**
6033
+ * Performs an export render at exact pixel dimensions without zoom/pan viewport offsets.
6034
+ */
6035
+ async renderExport(imageSource, state, options = {}) {
6036
+ let img;
6037
+ if (typeof imageSource === "string") {
6038
+ img = await this.imageCache.get(imageSource);
6039
+ } else {
6040
+ img = imageSource;
6041
+ }
6042
+ const naturalWidth = img.naturalWidth || img.videoWidth || img.width || 800;
6043
+ const naturalHeight = img.naturalHeight || img.videoHeight || img.height || 600;
6044
+ const { crop, transform, adjustments, depthMask, annotations } = state;
6045
+ const cropX = crop ? Math.max(0, crop.x) : 0;
6046
+ const cropY = crop ? Math.max(0, crop.y) : 0;
6047
+ const cropW = crop ? Math.min(naturalWidth - cropX, crop.width) : naturalWidth;
6048
+ const cropH = crop ? Math.min(naturalHeight - cropY, crop.height) : naturalHeight;
6049
+ const angle = (transform.rotate % 360 + 360) % 360;
6050
+ const isSwapped = angle === 90 || angle === 270;
6051
+ const baseW = isSwapped ? cropH : cropW;
6052
+ const baseH = isSwapped ? cropW : cropH;
6053
+ const exportW = options.width || baseW;
6054
+ const exportH = options.height || baseH;
6055
+ const exportCanvas = this.createCanvas(exportW, exportH);
6056
+ const ctx = exportCanvas.getContext("2d");
6057
+ if (!ctx) return exportCanvas;
6058
+ const intermediateCanvas = this.createCanvas(baseW, baseH);
6059
+ const interCtx = intermediateCanvas.getContext("2d");
6060
+ if (interCtx) {
6061
+ interCtx.save();
6062
+ const rad = angle * Math.PI / 180;
6063
+ interCtx.translate(baseW / 2, baseH / 2);
6064
+ interCtx.rotate(rad);
6065
+ interCtx.scale(transform.flipH ? -1 : 1, transform.flipV ? -1 : 1);
6066
+ interCtx.drawImage(img, cropX, cropY, cropW, cropH, -cropW / 2, -cropH / 2, cropW, cropH);
6067
+ interCtx.restore();
6068
+ const imageData = interCtx.getImageData(0, 0, baseW, baseH);
6069
+ if (imageData) {
6070
+ applyAdjustments(imageData, adjustments);
6071
+ if (depthMask.enabled && depthMask.maskSource) {
6072
+ try {
6073
+ const maskImg = await this.imageCache.get(depthMask.maskSource);
6074
+ const maskCanvas = this.createCanvas(baseW, baseH);
6075
+ const maskCtx = maskCanvas.getContext("2d");
6076
+ if (maskCtx) {
6077
+ maskCtx.drawImage(maskImg, cropX, cropY, cropW, cropH, 0, 0, baseW, baseH);
6078
+ const maskData = maskCtx.getImageData(0, 0, baseW, baseH);
6079
+ if (maskData) {
6080
+ applyDepthMask(imageData, maskData, depthMask);
6081
+ }
6082
+ }
6083
+ } catch {
6084
+ }
6085
+ }
6086
+ interCtx.putImageData(imageData, 0, 0);
6087
+ }
6088
+ }
6089
+ ctx.drawImage(intermediateCanvas, 0, 0, baseW, baseH, 0, 0, exportW, exportH);
6090
+ if (options.includeAnnotations !== false && annotations.length > 0) {
6091
+ const annScaleX = exportW / baseW;
6092
+ const annScaleY = exportH / baseH;
6093
+ ctx.save();
6094
+ ctx.scale(annScaleX, annScaleY);
6095
+ for (const ann of annotations) {
6096
+ await this.drawAnnotation(ctx, ann);
6097
+ }
6098
+ ctx.restore();
6099
+ }
6100
+ return exportCanvas;
6101
+ }
6102
+ };
6103
+
6104
+ // src/image-editor/controller.ts
6105
+ var DEFAULT_ADJUSTMENTS = {
6106
+ brightness: 0,
6107
+ contrast: 0,
6108
+ saturation: 0,
6109
+ exposure: 0,
6110
+ temperature: 0,
6111
+ blur: 0,
6112
+ opacity: 1
6113
+ };
6114
+ var DEFAULT_TRANSFORM = {
6115
+ rotate: 0,
6116
+ flipH: false,
6117
+ flipV: false
6118
+ };
6119
+ var DEFAULT_DEPTH_MASK = {
6120
+ enabled: false,
6121
+ maskSource: null,
6122
+ mode: "threshold",
6123
+ depthRange: [0, 255],
6124
+ softness: 10,
6125
+ invert: false
6126
+ };
6127
+ var ImageEditorController = class {
6128
+ state;
6129
+ renderer;
6130
+ imageElement = null;
6131
+ listeners = /* @__PURE__ */ new Set();
6132
+ onStateChange;
6133
+ // History stacks
6134
+ undoStack = [];
6135
+ redoStack = [];
6136
+ maxHistoryDepth;
6137
+ isApplyingHistory = false;
6138
+ initialSnapshot;
6139
+ constructor(options = {}) {
6140
+ this.maxHistoryDepth = options.maxHistoryDepth ?? 50;
6141
+ this.onStateChange = options.onStateChange;
6142
+ this.renderer = new ImageEditorRenderer();
6143
+ const initialAdjustments = {
6144
+ ...DEFAULT_ADJUSTMENTS,
6145
+ ...options.initialState?.adjustments
6146
+ };
6147
+ const initialTransform = {
6148
+ ...DEFAULT_TRANSFORM,
6149
+ ...options.initialState?.transform
6150
+ };
6151
+ const initialDepthMask = {
6152
+ ...DEFAULT_DEPTH_MASK,
6153
+ ...options.initialState?.depthMask
6154
+ };
6155
+ this.state = {
6156
+ sourceUrl: typeof options.image === "string" ? options.image : null,
6157
+ imageDimensions: { width: 800, height: 600 },
6158
+ crop: options.initialState?.crop ?? null,
6159
+ transform: initialTransform,
6160
+ adjustments: initialAdjustments,
6161
+ depthMask: initialDepthMask,
6162
+ annotations: options.initialState?.annotations ?? [],
6163
+ selectedAnnotationId: null,
6164
+ activeTool: "select",
6165
+ zoom: 1,
6166
+ pan: { x: 0, y: 0 },
6167
+ isOriginalCompared: false
6168
+ };
6169
+ if (options.image instanceof Object && "width" in options.image) {
6170
+ this.imageElement = options.image;
6171
+ this.state.imageDimensions = {
6172
+ width: options.image.naturalWidth || options.image.width || 800,
6173
+ height: options.image.naturalHeight || options.image.height || 600
6174
+ };
6175
+ }
6176
+ this.initialSnapshot = this.serializeState();
6177
+ if (typeof options.image === "string") {
6178
+ this.loadImage(options.image);
6179
+ }
6180
+ }
6181
+ // --- Image Loading ---
6182
+ async loadImage(src) {
6183
+ if (typeof src === "string") {
6184
+ this.state.sourceUrl = src;
6185
+ try {
6186
+ const img = await this.renderer.imageCache.get(src);
6187
+ this.imageElement = img;
6188
+ this.state.imageDimensions = {
6189
+ width: img.naturalWidth || img.width || 800,
6190
+ height: img.naturalHeight || img.height || 600
6191
+ };
6192
+ } catch (err) {
6193
+ }
6194
+ } else {
6195
+ this.imageElement = src;
6196
+ this.state.sourceUrl = src.src || null;
6197
+ this.state.imageDimensions = {
6198
+ width: src.naturalWidth || src.width || 800,
6199
+ height: src.naturalHeight || src.height || 600
6200
+ };
6201
+ }
6202
+ this.initialSnapshot = this.serializeState();
6203
+ this.notify();
6204
+ }
6205
+ getImageElement() {
6206
+ return this.imageElement;
6207
+ }
6208
+ // --- State & Subscription ---
6209
+ getState() {
6210
+ return this.state;
6211
+ }
6212
+ subscribe(listener) {
6213
+ this.listeners.add(listener);
6214
+ listener(this.state);
6215
+ return () => {
6216
+ this.listeners.delete(listener);
6217
+ };
6218
+ }
6219
+ notify() {
6220
+ const currentState = { ...this.state };
6221
+ this.onStateChange?.(currentState);
6222
+ this.listeners.forEach((l) => l(currentState));
6223
+ }
6224
+ // --- History (Undo / Redo / Reset) ---
6225
+ recordHistory() {
6226
+ if (this.isApplyingHistory) return;
6227
+ this.undoStack.push(this.serializeState());
6228
+ if (this.undoStack.length > this.maxHistoryDepth) {
6229
+ this.undoStack.shift();
6230
+ }
6231
+ this.redoStack = [];
6232
+ }
6233
+ get canUndo() {
6234
+ return this.undoStack.length > 0;
6235
+ }
6236
+ get canRedo() {
6237
+ return this.redoStack.length > 0;
6238
+ }
6239
+ undo() {
6240
+ if (!this.canUndo) return false;
6241
+ this.isApplyingHistory = true;
6242
+ const current = this.serializeState();
6243
+ this.redoStack.push(current);
6244
+ const prev = this.undoStack.pop();
6245
+ this.applySerializedState(prev);
6246
+ this.isApplyingHistory = false;
6247
+ this.notify();
6248
+ return true;
6249
+ }
6250
+ redo() {
6251
+ if (!this.canRedo) return false;
6252
+ this.isApplyingHistory = true;
6253
+ const current = this.serializeState();
6254
+ this.undoStack.push(current);
6255
+ const next = this.redoStack.pop();
6256
+ this.applySerializedState(next);
6257
+ this.isApplyingHistory = false;
6258
+ this.notify();
6259
+ return true;
6260
+ }
6261
+ reset() {
6262
+ this.recordHistory();
6263
+ this.applySerializedState(this.initialSnapshot);
6264
+ this.state.zoom = 1;
6265
+ this.state.pan = { x: 0, y: 0 };
6266
+ this.state.activeTool = "select";
6267
+ this.state.selectedAnnotationId = null;
6268
+ this.notify();
6269
+ }
6270
+ // --- Active Tool ---
6271
+ setTool(tool) {
6272
+ if (this.state.activeTool === tool) return;
6273
+ this.state.activeTool = tool;
6274
+ if (tool !== "select") {
6275
+ this.state.selectedAnnotationId = null;
6276
+ }
6277
+ if (tool === "crop" && !this.state.crop) {
6278
+ this.state.crop = {
6279
+ x: 0,
6280
+ y: 0,
6281
+ width: this.state.imageDimensions.width,
6282
+ height: this.state.imageDimensions.height
6283
+ };
6284
+ }
6285
+ this.notify();
6286
+ }
6287
+ // --- Crop ---
6288
+ setCrop(crop, addToHistory = true) {
6289
+ if (addToHistory) {
6290
+ this.recordHistory();
6291
+ }
6292
+ this.state.crop = crop;
6293
+ this.notify();
6294
+ }
6295
+ applyCropPreset(ratio) {
6296
+ this.recordHistory();
6297
+ const { width, height } = this.state.imageDimensions;
6298
+ if (!ratio) {
6299
+ this.state.crop = { x: 0, y: 0, width, height };
6300
+ } else {
6301
+ let targetW = width;
6302
+ let targetH = targetW / ratio;
6303
+ if (targetH > height) {
6304
+ targetH = height;
6305
+ targetW = targetH * ratio;
6306
+ }
6307
+ const x = Math.max(0, Math.round((width - targetW) / 2));
6308
+ const y = Math.max(0, Math.round((height - targetH) / 2));
6309
+ this.state.crop = {
6310
+ x,
6311
+ y,
6312
+ width: Math.round(targetW),
6313
+ height: Math.round(targetH)
6314
+ };
6315
+ }
6316
+ this.notify();
6317
+ }
6318
+ resetCrop() {
6319
+ this.recordHistory();
6320
+ this.state.crop = null;
6321
+ this.notify();
6322
+ }
6323
+ // --- Transformations (Rotate / Flip) ---
6324
+ rotate(degrees) {
6325
+ this.recordHistory();
6326
+ const current = this.state.transform.rotate;
6327
+ this.state.transform.rotate = (current + degrees) % 360;
6328
+ this.notify();
6329
+ }
6330
+ setRotation(degrees) {
6331
+ this.recordHistory();
6332
+ this.state.transform.rotate = degrees % 360;
6333
+ this.notify();
6334
+ }
6335
+ flipHorizontal() {
6336
+ this.recordHistory();
6337
+ this.state.transform.flipH = !this.state.transform.flipH;
6338
+ this.notify();
6339
+ }
6340
+ flipVertical() {
6341
+ this.recordHistory();
6342
+ this.state.transform.flipV = !this.state.transform.flipV;
6343
+ this.notify();
6344
+ }
6345
+ // --- Adjustments ---
6346
+ setAdjustments(partial, commitToHistory = true) {
6347
+ if (commitToHistory) {
6348
+ this.recordHistory();
6349
+ }
6350
+ this.state.adjustments = {
6351
+ ...this.state.adjustments,
6352
+ ...partial
6353
+ };
6354
+ this.notify();
6355
+ }
6356
+ resetAdjustments() {
6357
+ this.recordHistory();
6358
+ this.state.adjustments = { ...DEFAULT_ADJUSTMENTS };
6359
+ this.notify();
6360
+ }
6361
+ // --- Depth Masking (Gleamforge) ---
6362
+ setDepthMask(partial) {
6363
+ this.recordHistory();
6364
+ this.state.depthMask = {
6365
+ ...this.state.depthMask,
6366
+ ...partial
6367
+ };
6368
+ this.notify();
6369
+ }
6370
+ resetDepthMask() {
6371
+ this.recordHistory();
6372
+ this.state.depthMask = { ...DEFAULT_DEPTH_MASK };
6373
+ this.notify();
6374
+ }
6375
+ // --- Annotations & Layers ---
6376
+ addAnnotation(annotation) {
6377
+ this.recordHistory();
6378
+ const id = `ann_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
6379
+ const fullAnnotation = { ...annotation, id };
6380
+ this.state.annotations = [...this.state.annotations, fullAnnotation];
6381
+ this.state.selectedAnnotationId = id;
6382
+ this.notify();
6383
+ return id;
6384
+ }
6385
+ updateAnnotation(id, partial, commitToHistory = true) {
6386
+ if (commitToHistory) {
6387
+ this.recordHistory();
6388
+ }
6389
+ this.state.annotations = this.state.annotations.map((ann) => {
6390
+ if (ann.id === id) {
6391
+ return { ...ann, ...partial };
6392
+ }
6393
+ return ann;
6394
+ });
6395
+ this.notify();
6396
+ }
6397
+ removeAnnotation(id) {
6398
+ this.recordHistory();
6399
+ this.state.annotations = this.state.annotations.filter((ann) => ann.id !== id);
6400
+ if (this.state.selectedAnnotationId === id) {
6401
+ this.state.selectedAnnotationId = null;
6402
+ }
6403
+ this.notify();
6404
+ }
6405
+ clearAnnotations() {
6406
+ this.recordHistory();
6407
+ this.state.annotations = [];
6408
+ this.state.selectedAnnotationId = null;
6409
+ this.notify();
6410
+ }
6411
+ selectAnnotation(id) {
6412
+ this.state.selectedAnnotationId = id;
6413
+ this.notify();
6414
+ }
6415
+ // --- Zoom & Pan ---
6416
+ setZoom(zoom) {
6417
+ this.state.zoom = Math.max(0.1, Math.min(10, zoom));
6418
+ this.notify();
6419
+ }
6420
+ zoomIn(factor = 1.2) {
6421
+ this.setZoom(this.state.zoom * factor);
6422
+ }
6423
+ zoomOut(factor = 1.2) {
6424
+ this.setZoom(this.state.zoom / factor);
6425
+ }
6426
+ resetZoom() {
6427
+ this.state.zoom = 1;
6428
+ this.state.pan = { x: 0, y: 0 };
6429
+ this.notify();
6430
+ }
6431
+ setPan(pan) {
6432
+ this.state.pan = pan;
6433
+ this.notify();
6434
+ }
6435
+ setComparing(comparing) {
6436
+ if (this.state.isOriginalCompared === comparing) return;
6437
+ this.state.isOriginalCompared = comparing;
6438
+ this.notify();
6439
+ }
6440
+ // --- Rendering & Export ---
6441
+ async render(targetCanvas, options) {
6442
+ const source = this.imageElement || this.state.sourceUrl;
6443
+ if (!source) return;
6444
+ await this.renderer.renderToCanvas(targetCanvas, source, this.state, options);
6445
+ }
6446
+ async renderExport(options) {
6447
+ const source = this.imageElement || this.state.sourceUrl;
6448
+ if (!source) {
6449
+ return this.renderer.createCanvas(1, 1);
6450
+ }
6451
+ return this.renderer.renderExport(source, this.state, options);
6452
+ }
6453
+ async toDataURL(options = {}) {
6454
+ const canvas = await this.renderExport(options);
6455
+ const format = options.format || "image/png";
6456
+ const quality = options.quality ?? 0.92;
6457
+ return canvas.toDataURL(format, quality);
6458
+ }
6459
+ async toBlob(options = {}) {
6460
+ const canvas = await this.renderExport(options);
6461
+ const format = options.format || "image/png";
6462
+ const quality = options.quality ?? 0.92;
6463
+ return new Promise((resolve, reject) => {
6464
+ if (canvas.toBlob) {
6465
+ canvas.toBlob((blob) => {
6466
+ if (blob) resolve(blob);
6467
+ else reject(new Error("Failed to create Blob from canvas"));
6468
+ }, format, quality);
6469
+ } else {
6470
+ try {
6471
+ const dataUrl = canvas.toDataURL(format, quality);
6472
+ const parts = dataUrl.split(",");
6473
+ const byteStr = atob(parts[1]);
6474
+ const ab = new ArrayBuffer(byteStr.length);
6475
+ const ia = new Uint8Array(ab);
6476
+ for (let i = 0; i < byteStr.length; i++) {
6477
+ ia[i] = byteStr.charCodeAt(i);
6478
+ }
6479
+ resolve(new Blob([ab], { type: format }));
6480
+ } catch (err) {
6481
+ reject(err);
6482
+ }
6483
+ }
6484
+ });
6485
+ }
6486
+ // --- Serialization ---
6487
+ toJSON() {
6488
+ return this.serializeState();
6489
+ }
6490
+ loadJSON(data) {
6491
+ this.recordHistory();
6492
+ this.applySerializedState(data);
6493
+ this.notify();
6494
+ }
6495
+ serializeState() {
6496
+ return {
6497
+ version: 1,
6498
+ crop: this.state.crop ? { ...this.state.crop } : null,
6499
+ transform: { ...this.state.transform },
6500
+ adjustments: { ...this.state.adjustments },
6501
+ depthMask: {
6502
+ ...this.state.depthMask,
6503
+ depthRange: [...this.state.depthMask.depthRange]
6504
+ },
6505
+ annotations: JSON.parse(JSON.stringify(this.state.annotations))
6506
+ };
6507
+ }
6508
+ applySerializedState(data) {
6509
+ this.state.crop = data.crop ? { ...data.crop } : null;
6510
+ this.state.transform = { ...data.transform };
6511
+ this.state.adjustments = { ...data.adjustments };
6512
+ this.state.depthMask = {
6513
+ ...data.depthMask,
6514
+ depthRange: [...data.depthMask.depthRange]
6515
+ };
6516
+ this.state.annotations = JSON.parse(JSON.stringify(data.annotations || []));
6517
+ }
6518
+ };
6519
+
6520
+ // src/image-editor/geometry.ts
6521
+ function getViewportMetrics(state, vpWidth, vpHeight, isActivelyCropping = false, cropOverride) {
6522
+ const { imageDimensions, crop: stateCrop, transform, zoom, pan } = state;
6523
+ const naturalWidth = imageDimensions.width || 800;
6524
+ const naturalHeight = imageDimensions.height || 600;
6525
+ const activeCrop = cropOverride !== void 0 && cropOverride !== null ? cropOverride : stateCrop ?? (isActivelyCropping ? { x: 0, y: 0, width: naturalWidth, height: naturalHeight } : null);
6526
+ const cropW = !isActivelyCropping && activeCrop ? activeCrop.width : naturalWidth;
6527
+ const cropH = !isActivelyCropping && activeCrop ? activeCrop.height : naturalHeight;
6528
+ const angle = (transform.rotate % 360 + 360) % 360;
6529
+ const isSwapped = angle === 90 || angle === 270;
6530
+ const baseW = isSwapped ? cropH : cropW;
6531
+ const baseH = isSwapped ? cropW : cropH;
6532
+ const fitScale = Math.min(vpWidth * 0.85 / baseW, vpHeight * 0.85 / baseH, 1);
6533
+ const renderW = baseW * fitScale;
6534
+ const renderH = baseH * fitScale;
6535
+ const totalScale = zoom * fitScale;
6536
+ const centerX = vpWidth / 2 + pan.x;
6537
+ const centerY = vpHeight / 2 + pan.y;
6538
+ const imageRect = {
6539
+ x: centerX - renderW * zoom / 2,
6540
+ y: centerY - renderH * zoom / 2,
6541
+ width: renderW * zoom,
6542
+ height: renderH * zoom
6543
+ };
6544
+ let cropRect = null;
6545
+ if (activeCrop) {
6546
+ const startX = -renderW / 2 + activeCrop.x / naturalWidth * renderW;
6547
+ const startY = -renderH / 2 + activeCrop.y / naturalHeight * renderH;
6548
+ const cropBoxW = activeCrop.width / naturalWidth * renderW;
6549
+ const cropBoxH = activeCrop.height / naturalHeight * renderH;
6550
+ cropRect = {
6551
+ x: centerX + zoom * startX,
6552
+ y: centerY + zoom * startY,
6553
+ width: zoom * cropBoxW,
6554
+ height: zoom * cropBoxH
6555
+ };
6556
+ }
6557
+ return {
6558
+ vpWidth,
6559
+ vpHeight,
6560
+ centerX,
6561
+ centerY,
6562
+ baseW,
6563
+ baseH,
6564
+ fitScale,
6565
+ renderW,
6566
+ renderH,
6567
+ totalScale,
6568
+ imageRect,
6569
+ cropRect
6570
+ };
6571
+ }
6572
+ function canvasToImagePoint(canvasPt, state, vpWidth, vpHeight, isActivelyCropping = false) {
6573
+ const metrics = getViewportMetrics(state, vpWidth, vpHeight, isActivelyCropping);
6574
+ const unzoomedX = (canvasPt.x - metrics.centerX) / state.zoom;
6575
+ const unzoomedY = (canvasPt.y - metrics.centerY) / state.zoom;
6576
+ const x = (unzoomedX + metrics.renderW / 2) / metrics.fitScale;
6577
+ const y = (unzoomedY + metrics.renderH / 2) / metrics.fitScale;
6578
+ return { x: Math.round(x), y: Math.round(y) };
6579
+ }
6580
+ function imageToCanvasPoint(imgPt, state, vpWidth, vpHeight, isActivelyCropping = false) {
6581
+ const metrics = getViewportMetrics(state, vpWidth, vpHeight, isActivelyCropping);
6582
+ const unzoomedX = (imgPt.x - metrics.baseW / 2) * metrics.fitScale;
6583
+ const unzoomedY = (imgPt.y - metrics.baseH / 2) * metrics.fitScale;
6584
+ const canvasX = metrics.centerX + unzoomedX * state.zoom;
6585
+ const canvasY = metrics.centerY + unzoomedY * state.zoom;
6586
+ return { x: Math.round(canvasX), y: Math.round(canvasY) };
6587
+ }
6588
+ function hitTestCrop(canvasPt, state, vpWidth, vpHeight, handleThreshold = 14, cropOverride) {
6589
+ const metrics = getViewportMetrics(state, vpWidth, vpHeight, true, cropOverride);
6590
+ if (!metrics.cropRect) return null;
6591
+ const { x, y, width: w, height: h } = metrics.cropRect;
6592
+ const px = canvasPt.x;
6593
+ const py = canvasPt.y;
6594
+ const dist = (x1, y1, x2, y2) => Math.hypot(x1 - x2, y1 - y2);
6595
+ if (dist(px, py, x, y) <= handleThreshold) return "nw";
6596
+ if (dist(px, py, x + w, y) <= handleThreshold) return "ne";
6597
+ if (dist(px, py, x + w, y + h) <= handleThreshold) return "se";
6598
+ if (dist(px, py, x, y + h) <= handleThreshold) return "sw";
6599
+ const nearX = px >= x - handleThreshold && px <= x + w + handleThreshold;
6600
+ const nearY = py >= y - handleThreshold && py <= y + h + handleThreshold;
6601
+ if (nearX && Math.abs(py - y) <= handleThreshold) return "n";
6602
+ if (nearX && Math.abs(py - (y + h)) <= handleThreshold) return "s";
6603
+ if (nearY && Math.abs(px - x) <= handleThreshold) return "w";
6604
+ if (nearY && Math.abs(px - (x + w)) <= handleThreshold) return "e";
6605
+ if (px >= x && px <= x + w && py >= y && py <= y + h) {
6606
+ return "inside";
6607
+ }
6608
+ return null;
6609
+ }
6610
+ function getCropCursor(handle) {
6611
+ switch (handle) {
6612
+ case "nw":
6613
+ case "se":
6614
+ return "nwse-resize";
6615
+ case "ne":
6616
+ case "sw":
6617
+ return "nesw-resize";
6618
+ case "n":
6619
+ case "s":
6620
+ return "ns-resize";
6621
+ case "e":
6622
+ case "w":
6623
+ return "ew-resize";
6624
+ case "inside":
6625
+ return "move";
6626
+ default:
6627
+ return "crosshair";
6628
+ }
6629
+ }
6630
+ function calculateCropDrag(startCrop, handle, startImgPt, currentImgPt, imgWidth, imgHeight, aspectRatio) {
6631
+ const dx = currentImgPt.x - startImgPt.x;
6632
+ const dy = currentImgPt.y - startImgPt.y;
6633
+ if (handle === "inside") {
6634
+ const maxX = Math.max(0, imgWidth - startCrop.width);
6635
+ const maxY = Math.max(0, imgHeight - startCrop.height);
6636
+ const newX = Math.max(0, Math.min(maxX, startCrop.x + dx));
6637
+ const newY = Math.max(0, Math.min(maxY, startCrop.y + dy));
6638
+ return {
6639
+ x: Math.round(newX),
6640
+ y: Math.round(newY),
6641
+ width: startCrop.width,
6642
+ height: startCrop.height
6643
+ };
6644
+ }
6645
+ let left = startCrop.x;
6646
+ let top = startCrop.y;
6647
+ let right = startCrop.x + startCrop.width;
6648
+ let bottom = startCrop.y + startCrop.height;
6649
+ const minSize = 20;
6650
+ if (handle === "e" || handle === "ne" || handle === "se") {
6651
+ right = Math.min(imgWidth, Math.max(left + minSize, startCrop.x + startCrop.width + dx));
6652
+ }
6653
+ if (handle === "w" || handle === "nw" || handle === "sw") {
6654
+ left = Math.max(0, Math.min(right - minSize, startCrop.x + dx));
6655
+ }
6656
+ if (handle === "s" || handle === "se" || handle === "sw") {
6657
+ bottom = Math.min(imgHeight, Math.max(top + minSize, startCrop.y + startCrop.height + dy));
6658
+ }
6659
+ if (handle === "n" || handle === "nw" || handle === "ne") {
6660
+ top = Math.max(0, Math.min(bottom - minSize, startCrop.y + dy));
6661
+ }
6662
+ if (aspectRatio && aspectRatio > 0) {
6663
+ let currentW = right - left;
6664
+ let currentH = bottom - top;
6665
+ if (handle === "e" || handle === "w") {
6666
+ currentH = Math.min(imgHeight, currentW / aspectRatio);
6667
+ bottom = Math.min(imgHeight, top + currentH);
6668
+ } else if (handle === "n" || handle === "s") {
6669
+ currentW = Math.min(imgWidth, currentH * aspectRatio);
6670
+ right = Math.min(imgWidth, left + currentW);
6671
+ } else {
6672
+ const targetH = currentW / aspectRatio;
6673
+ if (top + targetH <= imgHeight) {
6674
+ bottom = top + targetH;
6675
+ } else {
6676
+ const targetW = currentH * aspectRatio;
6677
+ right = Math.min(imgWidth, left + targetW);
6678
+ }
6679
+ }
6680
+ }
6681
+ return {
6682
+ x: Math.round(left),
6683
+ y: Math.round(top),
6684
+ width: Math.round(Math.max(minSize, right - left)),
6685
+ height: Math.round(Math.max(minSize, bottom - top))
6686
+ };
6687
+ }
6688
+ function getAnnotationBounds(ann) {
6689
+ switch (ann.type) {
6690
+ case "rect":
6691
+ return { x: ann.x, y: ann.y, width: ann.width, height: ann.height };
6692
+ case "circle":
6693
+ return {
6694
+ x: ann.x - ann.radiusX,
6695
+ y: ann.y - ann.radiusY,
6696
+ width: ann.radiusX * 2,
6697
+ height: ann.radiusY * 2
6698
+ };
6699
+ case "arrow":
6700
+ case "line": {
6701
+ const minX = Math.min(ann.x, ann.endX);
6702
+ const minY = Math.min(ann.y, ann.endY);
6703
+ return {
6704
+ x: minX,
6705
+ y: minY,
6706
+ width: Math.max(Math.abs(ann.endX - ann.x), 10),
6707
+ height: Math.max(Math.abs(ann.endY - ann.y), 10)
6708
+ };
6709
+ }
6710
+ case "pen": {
6711
+ if (!ann.points.length) return { x: ann.x, y: ann.y, width: 0, height: 0 };
6712
+ let minX = ann.points[0].x;
6713
+ let maxX = ann.points[0].x;
6714
+ let minY = ann.points[0].y;
6715
+ let maxY = ann.points[0].y;
6716
+ for (const pt of ann.points) {
6717
+ if (pt.x < minX) minX = pt.x;
6718
+ if (pt.x > maxX) maxX = pt.x;
6719
+ if (pt.y < minY) minY = pt.y;
6720
+ if (pt.y > maxY) maxY = pt.y;
6721
+ }
6722
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
6723
+ }
6724
+ case "text":
6725
+ return {
6726
+ x: ann.x,
6727
+ y: ann.y,
6728
+ width: ann.text.length * ann.fontSize * 0.6,
6729
+ height: ann.fontSize * 1.2
6730
+ };
6731
+ case "image":
6732
+ return { x: ann.x, y: ann.y, width: ann.width, height: ann.height };
6733
+ }
6734
+ }
6735
+ function hitTestAnnotation(imgPt, ann, padding = 10) {
6736
+ const b = getAnnotationBounds(ann);
6737
+ return imgPt.x >= b.x - padding && imgPt.x <= b.x + b.width + padding && imgPt.y >= b.y - padding && imgPt.y <= b.y + b.height + padding;
6738
+ }
5257
6739
  export {
5258
6740
  ColorHighlighter,
6741
+ DEFAULT_ADJUSTMENTS,
6742
+ DEFAULT_DEPTH_MASK,
5259
6743
  DEFAULT_NODE_HEIGHT,
5260
6744
  DEFAULT_NODE_WIDTH,
5261
6745
  DEFAULT_PLACEMENT_GAP,
6746
+ DEFAULT_TRANSFORM,
5262
6747
  DataSourceBinder,
5263
6748
  DependencyGraph,
5264
6749
  DirtyTracker,
5265
6750
  EDITOR_MCP_TOOLS,
6751
+ EXPORT_EXCLUDED_SELECTORS,
5266
6752
  EditorController,
5267
6753
  FontSize,
5268
6754
  FormulaEvaluator,
@@ -5270,13 +6756,17 @@ export {
5270
6756
  GradientText,
5271
6757
  HistoryManager,
5272
6758
  INSTRUCTION_REGEX,
5273
- Image2 as Image,
6759
+ Image3 as Image,
6760
+ ImageEditorController,
6761
+ ImageEditorRenderer,
5274
6762
  LoadingNode,
5275
6763
  MARKDOWN_MENTION_SPLIT_REGEX,
5276
6764
  MCP_TOOL_EDIT_CANVAS,
5277
6765
  MCP_TOOL_EDIT_PROMPT,
5278
6766
  MCP_TOOL_GET_EDITOR_STATE,
6767
+ MCP_TOOL_VIEW_EDITOR,
5279
6768
  MENTION_REGEX,
6769
+ ManualClock,
5280
6770
  Markdown2 as Markdown,
5281
6771
  Mention,
5282
6772
  SlashCommands,
@@ -5286,13 +6776,20 @@ export {
5286
6776
  TEMPLATE_REGEX,
5287
6777
  TemplateSuggestions,
5288
6778
  VARIABLE_REGEX,
6779
+ applyAdjustments,
6780
+ applyDepthMask,
6781
+ blendLayout,
5289
6782
  boundsOf,
6783
+ calculateCropDrag,
5290
6784
  calculateDagreReflowDisplacement,
5291
6785
  calculateDirectionalDisplacement,
6786
+ canvasHeight,
6787
+ canvasToImagePoint,
5292
6788
  centerNodes,
5293
6789
  centre,
5294
6790
  colNameToIndex,
5295
6791
  computeNodesBoundingBox,
6792
+ connectedOnly,
5296
6793
  createBezierTrajectory,
5297
6794
  createDirtyTracker,
5298
6795
  createEditor,
@@ -5304,6 +6801,7 @@ export {
5304
6801
  defaultIsEqual,
5305
6802
  ensureLayout,
5306
6803
  escapeHtml,
6804
+ estimateCanvasHeight,
5307
6805
  estimateNodeSize,
5308
6806
  extractCellDependencies,
5309
6807
  extractInstructionNames,
@@ -5313,26 +6811,35 @@ export {
5313
6811
  extractVariableNames,
5314
6812
  findHexColors,
5315
6813
  flowSide,
6814
+ getAnnotationBounds,
5316
6815
  getCenteredNodePosition,
6816
+ getCropCursor,
5317
6817
  getEditorMarkdown,
5318
6818
  getEditorText,
5319
6819
  getLatestVersion,
5320
6820
  getLayoutedNodes,
6821
+ getViewportMetrics,
6822
+ globalImageCache,
5321
6823
  handleImageInsertion,
6824
+ hitTestAnnotation,
6825
+ hitTestCrop,
5322
6826
  htmlToPlainText,
5323
6827
  imagePastePluginKey,
6828
+ imageToCanvasPoint,
5324
6829
  indexToColName,
5325
6830
  insertImageNode,
5326
6831
  instructionRegex,
5327
6832
  isEventFromTextInput,
5328
6833
  isValidCanvasNode,
5329
6834
  isValidUrl,
6835
+ layoutBounds,
5330
6836
  linearPositionTrajectory,
5331
6837
  linearTrajectory,
5332
6838
  mapRawOffsetToResolved,
5333
6839
  mapResolvedOffsetToRaw,
5334
6840
  markdownToTipTapHtml,
5335
6841
  overlaps,
6842
+ pageHidden,
5336
6843
  parseCanvasClipboardData,
5337
6844
  parseCellReference,
5338
6845
  parseFormula,
@@ -5343,7 +6850,9 @@ export {
5343
6850
  planAgentCommand,
5344
6851
  preprocessMarkdownFileLinks,
5345
6852
  preprocessMarkdownMentions,
6853
+ realClock,
5346
6854
  remapPastedNodes,
6855
+ removeAlpha,
5347
6856
  removeImageNode,
5348
6857
  repositionNodesTo,
5349
6858
  resolveTemplates,