@pixerate/editor 0.11.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.cjs CHANGED
@@ -31,9 +31,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
33
  ColorHighlighter: () => ColorHighlighter,
34
+ DEFAULT_ADJUSTMENTS: () => DEFAULT_ADJUSTMENTS,
35
+ DEFAULT_DEPTH_MASK: () => DEFAULT_DEPTH_MASK,
34
36
  DEFAULT_NODE_HEIGHT: () => DEFAULT_NODE_HEIGHT,
35
37
  DEFAULT_NODE_WIDTH: () => DEFAULT_NODE_WIDTH,
36
38
  DEFAULT_PLACEMENT_GAP: () => DEFAULT_PLACEMENT_GAP,
39
+ DEFAULT_TRANSFORM: () => DEFAULT_TRANSFORM,
37
40
  DataSourceBinder: () => DataSourceBinder,
38
41
  DependencyGraph: () => DependencyGraph,
39
42
  DirtyTracker: () => DirtyTracker,
@@ -46,7 +49,9 @@ __export(src_exports, {
46
49
  GradientText: () => GradientText,
47
50
  HistoryManager: () => HistoryManager,
48
51
  INSTRUCTION_REGEX: () => INSTRUCTION_REGEX,
49
- Image: () => Image2,
52
+ Image: () => Image3,
53
+ ImageEditorController: () => ImageEditorController,
54
+ ImageEditorRenderer: () => ImageEditorRenderer,
50
55
  LoadingNode: () => LoadingNode,
51
56
  MARKDOWN_MENTION_SPLIT_REGEX: () => MARKDOWN_MENTION_SPLIT_REGEX,
52
57
  MCP_TOOL_EDIT_CANVAS: () => MCP_TOOL_EDIT_CANVAS,
@@ -64,11 +69,15 @@ __export(src_exports, {
64
69
  TEMPLATE_REGEX: () => TEMPLATE_REGEX,
65
70
  TemplateSuggestions: () => TemplateSuggestions,
66
71
  VARIABLE_REGEX: () => VARIABLE_REGEX,
72
+ applyAdjustments: () => applyAdjustments,
73
+ applyDepthMask: () => applyDepthMask,
67
74
  blendLayout: () => blendLayout,
68
75
  boundsOf: () => boundsOf,
76
+ calculateCropDrag: () => calculateCropDrag,
69
77
  calculateDagreReflowDisplacement: () => calculateDagreReflowDisplacement,
70
78
  calculateDirectionalDisplacement: () => calculateDirectionalDisplacement,
71
79
  canvasHeight: () => canvasHeight,
80
+ canvasToImagePoint: () => canvasToImagePoint,
72
81
  centerNodes: () => centerNodes,
73
82
  centre: () => centre,
74
83
  colNameToIndex: () => colNameToIndex,
@@ -95,14 +104,21 @@ __export(src_exports, {
95
104
  extractVariableNames: () => extractVariableNames,
96
105
  findHexColors: () => findHexColors,
97
106
  flowSide: () => flowSide,
107
+ getAnnotationBounds: () => getAnnotationBounds,
98
108
  getCenteredNodePosition: () => getCenteredNodePosition,
109
+ getCropCursor: () => getCropCursor,
99
110
  getEditorMarkdown: () => getEditorMarkdown,
100
111
  getEditorText: () => getEditorText,
101
112
  getLatestVersion: () => getLatestVersion,
102
113
  getLayoutedNodes: () => getLayoutedNodes,
114
+ getViewportMetrics: () => getViewportMetrics,
115
+ globalImageCache: () => globalImageCache,
103
116
  handleImageInsertion: () => handleImageInsertion,
117
+ hitTestAnnotation: () => hitTestAnnotation,
118
+ hitTestCrop: () => hitTestCrop,
104
119
  htmlToPlainText: () => htmlToPlainText,
105
120
  imagePastePluginKey: () => imagePastePluginKey,
121
+ imageToCanvasPoint: () => imageToCanvasPoint,
106
122
  indexToColName: () => indexToColName,
107
123
  insertImageNode: () => insertImageNode,
108
124
  instructionRegex: () => instructionRegex,
@@ -129,6 +145,7 @@ __export(src_exports, {
129
145
  preprocessMarkdownMentions: () => preprocessMarkdownMentions,
130
146
  realClock: () => realClock,
131
147
  remapPastedNodes: () => remapPastedNodes,
148
+ removeAlpha: () => removeAlpha,
132
149
  removeImageNode: () => removeImageNode,
133
150
  repositionNodesTo: () => repositionNodesTo,
134
151
  resolveTemplates: () => resolveTemplates,
@@ -1419,10 +1436,10 @@ var HorizontalRule$1 = HorizontalRule.extend({
1419
1436
  };
1420
1437
  }
1421
1438
  });
1422
- var Image = import_core8.Node.create({
1439
+ var Image2 = import_core8.Node.create({
1423
1440
  name: "image"
1424
1441
  });
1425
- var Image$1 = Image.extend({
1442
+ var Image$1 = Image2.extend({
1426
1443
  /**
1427
1444
  * @return {{markdown: MarkdownNodeSpec}}
1428
1445
  */
@@ -2135,7 +2152,7 @@ function handleImageInsertion(view, file, options) {
2135
2152
  }
2136
2153
  }
2137
2154
  }
2138
- var Image2 = BaseImage.extend({
2155
+ var Image3 = BaseImage.extend({
2139
2156
  addOptions() {
2140
2157
  return {
2141
2158
  ...this.parent?.(),
@@ -2330,7 +2347,7 @@ function createRichTextPreset(options = {}) {
2330
2347
  if (options.image !== false) {
2331
2348
  const imageConfig = typeof options.image === "object" ? options.image : {};
2332
2349
  extensions2.push(
2333
- Image2.configure({
2350
+ Image3.configure({
2334
2351
  HTMLAttributes: {
2335
2352
  class: "rounded-md max-w-full h-auto my-2",
2336
2353
  ...imageConfig.HTMLAttributes || {}
@@ -5590,12 +5607,1303 @@ var EDITOR_MCP_TOOLS = [
5590
5607
  MCP_TOOL_EDIT_PROMPT,
5591
5608
  MCP_TOOL_VIEW_EDITOR
5592
5609
  ];
5610
+
5611
+ // src/image-editor/filters.ts
5612
+ function clamp(value, min = 0, max = 255) {
5613
+ return Math.max(min, Math.min(max, value));
5614
+ }
5615
+ function applyAdjustments(imageData, adjustments) {
5616
+ const { brightness, contrast, saturation, exposure, temperature, opacity, blur } = adjustments;
5617
+ const data = imageData.data;
5618
+ const len = data.length;
5619
+ const hasBrightness = brightness !== 0;
5620
+ const hasContrast = contrast !== 0;
5621
+ const hasSaturation = saturation !== 0;
5622
+ const hasExposure = exposure !== 0;
5623
+ const hasTemperature = temperature !== 0;
5624
+ const hasOpacity = opacity < 1;
5625
+ const contrastFactor = hasContrast ? 259 * (contrast + 255) / (255 * (259 - contrast)) : 1;
5626
+ const exposureMultiplier = hasExposure ? Math.pow(2, exposure / 50) : 1;
5627
+ const satMultiplier = hasSaturation ? 1 + saturation / 100 : 1;
5628
+ const tempOffset = hasTemperature ? temperature * 0.8 : 0;
5629
+ if (hasBrightness || hasContrast || hasSaturation || hasExposure || hasTemperature || hasOpacity) {
5630
+ for (let i = 0; i < len; i += 4) {
5631
+ let r = data[i];
5632
+ let g = data[i + 1];
5633
+ let b = data[i + 2];
5634
+ let a = data[i + 3];
5635
+ if (hasExposure) {
5636
+ r = clamp(r * exposureMultiplier);
5637
+ g = clamp(g * exposureMultiplier);
5638
+ b = clamp(b * exposureMultiplier);
5639
+ }
5640
+ if (hasBrightness) {
5641
+ const bOffset = brightness * 2.55;
5642
+ r = clamp(r + bOffset);
5643
+ g = clamp(g + bOffset);
5644
+ b = clamp(b + bOffset);
5645
+ }
5646
+ if (hasContrast) {
5647
+ r = clamp(contrastFactor * (r - 128) + 128);
5648
+ g = clamp(contrastFactor * (g - 128) + 128);
5649
+ b = clamp(contrastFactor * (b - 128) + 128);
5650
+ }
5651
+ if (hasSaturation) {
5652
+ const luminance = 0.2989 * r + 0.587 * g + 0.114 * b;
5653
+ r = clamp(luminance + (r - luminance) * satMultiplier);
5654
+ g = clamp(luminance + (g - luminance) * satMultiplier);
5655
+ b = clamp(luminance + (b - luminance) * satMultiplier);
5656
+ }
5657
+ if (hasTemperature) {
5658
+ r = clamp(r + tempOffset);
5659
+ b = clamp(b - tempOffset);
5660
+ }
5661
+ if (hasOpacity) {
5662
+ a = clamp(a * opacity);
5663
+ }
5664
+ data[i] = r;
5665
+ data[i + 1] = g;
5666
+ data[i + 2] = b;
5667
+ data[i + 3] = a;
5668
+ }
5669
+ }
5670
+ if (blur > 0) {
5671
+ applyFastBoxBlur(imageData, Math.min(Math.round(blur), 40));
5672
+ }
5673
+ }
5674
+ function applyFastBoxBlur(imageData, radius) {
5675
+ if (radius < 1) return;
5676
+ const w = imageData.width;
5677
+ const h = imageData.height;
5678
+ const src = imageData.data;
5679
+ const temp = new Uint8ClampedArray(src.length);
5680
+ for (let y = 0; y < h; y++) {
5681
+ for (let x = 0; x < w; x++) {
5682
+ let r = 0, g = 0, b = 0, a = 0;
5683
+ let count = 0;
5684
+ for (let k = -radius; k <= radius; k++) {
5685
+ const px = Math.min(w - 1, Math.max(0, x + k));
5686
+ const idx = (y * w + px) * 4;
5687
+ r += src[idx];
5688
+ g += src[idx + 1];
5689
+ b += src[idx + 2];
5690
+ a += src[idx + 3];
5691
+ count++;
5692
+ }
5693
+ const dstIdx = (y * w + x) * 4;
5694
+ temp[dstIdx] = Math.round(r / count);
5695
+ temp[dstIdx + 1] = Math.round(g / count);
5696
+ temp[dstIdx + 2] = Math.round(b / count);
5697
+ temp[dstIdx + 3] = Math.round(a / count);
5698
+ }
5699
+ }
5700
+ for (let x = 0; x < w; x++) {
5701
+ for (let y = 0; y < h; y++) {
5702
+ let r = 0, g = 0, b = 0, a = 0;
5703
+ let count = 0;
5704
+ for (let k = -radius; k <= radius; k++) {
5705
+ const py = Math.min(h - 1, Math.max(0, y + k));
5706
+ const idx = (py * w + x) * 4;
5707
+ r += temp[idx];
5708
+ g += temp[idx + 1];
5709
+ b += temp[idx + 2];
5710
+ a += temp[idx + 3];
5711
+ count++;
5712
+ }
5713
+ const dstIdx = (y * w + x) * 4;
5714
+ src[dstIdx] = Math.round(r / count);
5715
+ src[dstIdx + 1] = Math.round(g / count);
5716
+ src[dstIdx + 2] = Math.round(b / count);
5717
+ src[dstIdx + 3] = Math.round(a / count);
5718
+ }
5719
+ }
5720
+ }
5721
+ function applyDepthMask(mainImageData, maskImageData, options) {
5722
+ const { mode, depthRange, softness, invert } = options;
5723
+ const mainData = mainImageData.data;
5724
+ const maskData = maskImageData.data;
5725
+ const len = Math.min(mainData.length, maskData.length);
5726
+ const effectiveSoftness = Math.max(1, softness || 10);
5727
+ const minDepth = depthRange[0];
5728
+ const maxDepth = depthRange[1];
5729
+ for (let i = 0; i < len; i += 4) {
5730
+ let maskValue = maskData[i];
5731
+ if (invert) {
5732
+ maskValue = 255 - maskValue;
5733
+ }
5734
+ let alphaValue;
5735
+ if (mode === "threshold") {
5736
+ if (maskValue >= maxDepth + effectiveSoftness) {
5737
+ alphaValue = 0;
5738
+ } else if (maskValue > maxDepth) {
5739
+ alphaValue = Math.round((maxDepth + effectiveSoftness - maskValue) / effectiveSoftness * 255);
5740
+ } else if (maskValue >= minDepth) {
5741
+ alphaValue = 255;
5742
+ } else if (maskValue < minDepth - effectiveSoftness) {
5743
+ alphaValue = 0;
5744
+ } else {
5745
+ alphaValue = Math.round((maskValue - (minDepth - effectiveSoftness)) / effectiveSoftness * 255);
5746
+ }
5747
+ } else {
5748
+ alphaValue = clamp(maskValue + minDepth);
5749
+ }
5750
+ mainData[i + 3] = clamp(mainData[i + 3] * alphaValue / 255);
5751
+ }
5752
+ }
5753
+ function removeAlpha(imageData) {
5754
+ const data = imageData.data;
5755
+ const len = data.length;
5756
+ for (let i = 3; i < len; i += 4) {
5757
+ data[i] = 255;
5758
+ }
5759
+ }
5760
+
5761
+ // src/image-editor/renderer.ts
5762
+ var ImageCache = class {
5763
+ cache = /* @__PURE__ */ new Map();
5764
+ async get(src) {
5765
+ const existing = this.cache.get(src);
5766
+ if (existing && existing.complete) {
5767
+ return existing;
5768
+ }
5769
+ return new Promise((resolve, reject) => {
5770
+ if (typeof window === "undefined" || typeof Image === "undefined") {
5771
+ reject(new Error("Image loading is only supported in browser/DOM environments"));
5772
+ return;
5773
+ }
5774
+ const img = new Image();
5775
+ img.crossOrigin = "anonymous";
5776
+ img.onload = () => {
5777
+ this.cache.set(src, img);
5778
+ resolve(img);
5779
+ };
5780
+ img.onerror = (e) => reject(new Error(`Failed to load image from: ${src}`));
5781
+ img.src = src;
5782
+ });
5783
+ }
5784
+ set(src, img) {
5785
+ this.cache.set(src, img);
5786
+ }
5787
+ clear() {
5788
+ this.cache.clear();
5789
+ }
5790
+ };
5791
+ var globalImageCache = new ImageCache();
5792
+ var ImageEditorRenderer = class {
5793
+ imageCache = globalImageCache;
5794
+ cachedBaseCanvas = null;
5795
+ cachedBaseKey = "";
5796
+ /**
5797
+ * Helper to create a canvas element safely in browser/JSDOM.
5798
+ */
5799
+ createCanvas(width, height) {
5800
+ if (typeof document !== "undefined" && document.createElement) {
5801
+ const c = document.createElement("canvas");
5802
+ c.width = Math.max(1, Math.round(width));
5803
+ c.height = Math.max(1, Math.round(height));
5804
+ return c;
5805
+ }
5806
+ return {
5807
+ width: Math.max(1, Math.round(width)),
5808
+ height: Math.max(1, Math.round(height)),
5809
+ getContext: () => null,
5810
+ toDataURL: () => "",
5811
+ toBlob: () => {
5812
+ }
5813
+ };
5814
+ }
5815
+ /**
5816
+ * Renders the editor state onto a canvas (viewport or preview).
5817
+ */
5818
+ async renderToCanvas(targetCanvas, imageSource, state, options = {}) {
5819
+ const ctx = targetCanvas.getContext("2d");
5820
+ if (!ctx) return;
5821
+ let img;
5822
+ if (typeof imageSource === "string") {
5823
+ img = await this.imageCache.get(imageSource);
5824
+ } else {
5825
+ img = imageSource;
5826
+ }
5827
+ const naturalWidth = img.naturalWidth || img.videoWidth || img.width || 800;
5828
+ const naturalHeight = img.naturalHeight || img.videoHeight || img.height || 600;
5829
+ const { crop, transform, adjustments, depthMask, annotations, selectedAnnotationId, zoom, pan, isOriginalCompared } = state;
5830
+ const vpWidth = options.viewportWidth || targetCanvas.width;
5831
+ const vpHeight = options.viewportHeight || targetCanvas.height;
5832
+ targetCanvas.width = vpWidth;
5833
+ targetCanvas.height = vpHeight;
5834
+ ctx.clearRect(0, 0, vpWidth, vpHeight);
5835
+ if (isOriginalCompared) {
5836
+ this.drawFittedImage(ctx, img, naturalWidth, naturalHeight, vpWidth, vpHeight, zoom, pan);
5837
+ return;
5838
+ }
5839
+ const isActivelyCropping = Boolean(options.showCropOverlay || state.activeTool === "crop");
5840
+ const cropX = !isActivelyCropping && crop ? Math.max(0, crop.x) : 0;
5841
+ const cropY = !isActivelyCropping && crop ? Math.max(0, crop.y) : 0;
5842
+ const cropW = !isActivelyCropping && crop ? Math.min(naturalWidth - cropX, crop.width) : naturalWidth;
5843
+ const cropH = !isActivelyCropping && crop ? Math.min(naturalHeight - cropY, crop.height) : naturalHeight;
5844
+ const angle = (transform.rotate % 360 + 360) % 360;
5845
+ const isSwapped = angle === 90 || angle === 270;
5846
+ const baseW = isSwapped ? cropH : cropW;
5847
+ const baseH = isSwapped ? cropW : cropH;
5848
+ 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}`;
5849
+ let baseCanvas = this.cachedBaseCanvas;
5850
+ if (!baseCanvas || this.cachedBaseKey !== baseKey) {
5851
+ baseCanvas = this.createCanvas(baseW, baseH);
5852
+ const baseCtx = baseCanvas.getContext("2d");
5853
+ if (baseCtx) {
5854
+ baseCtx.save();
5855
+ const rad = angle * Math.PI / 180;
5856
+ baseCtx.translate(baseW / 2, baseH / 2);
5857
+ baseCtx.rotate(rad);
5858
+ baseCtx.scale(transform.flipH ? -1 : 1, transform.flipV ? -1 : 1);
5859
+ baseCtx.drawImage(
5860
+ img,
5861
+ cropX,
5862
+ cropY,
5863
+ cropW,
5864
+ cropH,
5865
+ -cropW / 2,
5866
+ -cropH / 2,
5867
+ cropW,
5868
+ cropH
5869
+ );
5870
+ baseCtx.restore();
5871
+ const imageData = baseCtx.getImageData(0, 0, baseW, baseH);
5872
+ if (imageData) {
5873
+ applyAdjustments(imageData, adjustments);
5874
+ if (depthMask.enabled && depthMask.maskSource) {
5875
+ try {
5876
+ const maskImg = await this.imageCache.get(depthMask.maskSource);
5877
+ const maskCanvas = this.createCanvas(baseW, baseH);
5878
+ const maskCtx = maskCanvas.getContext("2d");
5879
+ if (maskCtx) {
5880
+ maskCtx.drawImage(maskImg, cropX, cropY, cropW, cropH, 0, 0, baseW, baseH);
5881
+ const maskData = maskCtx.getImageData(0, 0, baseW, baseH);
5882
+ if (maskData) {
5883
+ applyDepthMask(imageData, maskData, depthMask);
5884
+ }
5885
+ }
5886
+ } catch {
5887
+ }
5888
+ }
5889
+ baseCtx.putImageData(imageData, 0, 0);
5890
+ }
5891
+ }
5892
+ this.cachedBaseCanvas = baseCanvas;
5893
+ this.cachedBaseKey = baseKey;
5894
+ }
5895
+ ctx.save();
5896
+ ctx.translate(vpWidth / 2 + pan.x, vpHeight / 2 + pan.y);
5897
+ ctx.scale(zoom, zoom);
5898
+ const fitScale = Math.min(vpWidth * 0.85 / baseW, vpHeight * 0.85 / baseH, 1);
5899
+ const renderW = baseW * fitScale;
5900
+ const renderH = baseH * fitScale;
5901
+ ctx.drawImage(baseCanvas, -renderW / 2, -renderH / 2, renderW, renderH);
5902
+ const scaleFactor = fitScale;
5903
+ ctx.save();
5904
+ ctx.translate(-renderW / 2, -renderH / 2);
5905
+ ctx.scale(scaleFactor, scaleFactor);
5906
+ for (const ann of annotations) {
5907
+ await this.drawAnnotation(ctx, ann);
5908
+ if (options.showSelectionOverlay && ann.id === selectedAnnotationId) {
5909
+ this.drawAnnotationSelection(ctx, ann);
5910
+ }
5911
+ }
5912
+ if (options.draftAnnotation) {
5913
+ await this.drawAnnotation(ctx, options.draftAnnotation);
5914
+ }
5915
+ ctx.restore();
5916
+ ctx.restore();
5917
+ const isCroppingActive = Boolean(options.showCropOverlay || state.activeTool === "crop");
5918
+ const activeCrop = options.draftCrop !== void 0 && options.draftCrop !== null ? options.draftCrop : crop ?? (isCroppingActive ? {
5919
+ x: 0,
5920
+ y: 0,
5921
+ width: naturalWidth,
5922
+ height: naturalHeight
5923
+ } : null);
5924
+ if (isCroppingActive && activeCrop) {
5925
+ this.drawCropOverlay(ctx, state, vpWidth, vpHeight, naturalWidth, naturalHeight, activeCrop);
5926
+ }
5927
+ }
5928
+ /**
5929
+ * Draws an individual annotation onto the canvas.
5930
+ */
5931
+ async drawAnnotation(ctx, ann) {
5932
+ ctx.save();
5933
+ if (ann.opacity !== void 0) {
5934
+ ctx.globalAlpha = ann.opacity;
5935
+ }
5936
+ if (ann.rotation) {
5937
+ ctx.translate(ann.x, ann.y);
5938
+ ctx.rotate(ann.rotation * Math.PI / 180);
5939
+ ctx.translate(-ann.x, -ann.y);
5940
+ }
5941
+ switch (ann.type) {
5942
+ case "pen": {
5943
+ const pen = ann;
5944
+ if (pen.points.length > 0) {
5945
+ ctx.beginPath();
5946
+ ctx.strokeStyle = pen.strokeColor;
5947
+ ctx.lineWidth = pen.strokeWidth;
5948
+ ctx.lineCap = "round";
5949
+ ctx.lineJoin = "round";
5950
+ ctx.moveTo(pen.points[0].x, pen.points[0].y);
5951
+ for (let i = 1; i < pen.points.length; i++) {
5952
+ ctx.lineTo(pen.points[i].x, pen.points[i].y);
5953
+ }
5954
+ ctx.stroke();
5955
+ }
5956
+ break;
5957
+ }
5958
+ case "rect": {
5959
+ const rect = ann;
5960
+ ctx.strokeStyle = rect.strokeColor;
5961
+ ctx.lineWidth = rect.strokeWidth;
5962
+ if (rect.fillColor) {
5963
+ ctx.fillStyle = rect.fillColor;
5964
+ ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
5965
+ }
5966
+ if (rect.strokeWidth > 0) {
5967
+ ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
5968
+ }
5969
+ break;
5970
+ }
5971
+ case "circle": {
5972
+ const circle = ann;
5973
+ ctx.beginPath();
5974
+ ctx.strokeStyle = circle.strokeColor;
5975
+ ctx.lineWidth = circle.strokeWidth;
5976
+ ctx.ellipse(circle.x, circle.y, Math.abs(circle.radiusX), Math.abs(circle.radiusY), 0, 0, 2 * Math.PI);
5977
+ if (circle.fillColor) {
5978
+ ctx.fillStyle = circle.fillColor;
5979
+ ctx.fill();
5980
+ }
5981
+ if (circle.strokeWidth > 0) {
5982
+ ctx.stroke();
5983
+ }
5984
+ break;
5985
+ }
5986
+ case "arrow": {
5987
+ const arrow = ann;
5988
+ ctx.strokeStyle = arrow.strokeColor;
5989
+ ctx.fillStyle = arrow.strokeColor;
5990
+ ctx.lineWidth = arrow.strokeWidth;
5991
+ ctx.beginPath();
5992
+ ctx.moveTo(arrow.x, arrow.y);
5993
+ ctx.lineTo(arrow.endX, arrow.endY);
5994
+ ctx.stroke();
5995
+ const angle = Math.atan2(arrow.endY - arrow.y, arrow.endX - arrow.x);
5996
+ const headLen = Math.max(12, arrow.strokeWidth * 3);
5997
+ ctx.beginPath();
5998
+ ctx.moveTo(arrow.endX, arrow.endY);
5999
+ ctx.lineTo(
6000
+ arrow.endX - headLen * Math.cos(angle - Math.PI / 6),
6001
+ arrow.endY - headLen * Math.sin(angle - Math.PI / 6)
6002
+ );
6003
+ ctx.lineTo(
6004
+ arrow.endX - headLen * Math.cos(angle + Math.PI / 6),
6005
+ arrow.endY - headLen * Math.sin(angle + Math.PI / 6)
6006
+ );
6007
+ ctx.closePath();
6008
+ ctx.fill();
6009
+ break;
6010
+ }
6011
+ case "line": {
6012
+ const line = ann;
6013
+ ctx.beginPath();
6014
+ ctx.strokeStyle = line.strokeColor;
6015
+ ctx.lineWidth = line.strokeWidth;
6016
+ ctx.moveTo(line.x, line.y);
6017
+ ctx.lineTo(line.endX, line.endY);
6018
+ ctx.stroke();
6019
+ break;
6020
+ }
6021
+ case "text": {
6022
+ const textAnn = ann;
6023
+ ctx.font = `${textAnn.fontWeight || "normal"} ${textAnn.fontSize}px ${textAnn.fontFamily || "sans-serif"}`;
6024
+ ctx.fillStyle = textAnn.color;
6025
+ ctx.textBaseline = "top";
6026
+ ctx.fillText(textAnn.text, textAnn.x, textAnn.y);
6027
+ break;
6028
+ }
6029
+ case "image": {
6030
+ const imgAnn = ann;
6031
+ try {
6032
+ const loadedImg = await this.imageCache.get(imgAnn.src);
6033
+ ctx.drawImage(loadedImg, imgAnn.x, imgAnn.y, imgAnn.width, imgAnn.height);
6034
+ } catch {
6035
+ }
6036
+ break;
6037
+ }
6038
+ }
6039
+ ctx.restore();
6040
+ }
6041
+ /**
6042
+ * Draws selection box around active annotation.
6043
+ */
6044
+ drawAnnotationSelection(ctx, ann) {
6045
+ ctx.save();
6046
+ ctx.strokeStyle = "#3b82f6";
6047
+ ctx.lineWidth = 1.5;
6048
+ ctx.setLineDash([4, 4]);
6049
+ const bounds = this.getAnnotationBounds(ann);
6050
+ const pad = 4;
6051
+ ctx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.width + pad * 2, bounds.height + pad * 2);
6052
+ ctx.fillStyle = "#ffffff";
6053
+ ctx.strokeStyle = "#3b82f6";
6054
+ ctx.setLineDash([]);
6055
+ const handleSize = 6;
6056
+ const corners = [
6057
+ { x: bounds.x - pad, y: bounds.y - pad },
6058
+ { x: bounds.x + bounds.width + pad, y: bounds.y - pad },
6059
+ { x: bounds.x - pad, y: bounds.y + bounds.height + pad },
6060
+ { x: bounds.x + bounds.width + pad, y: bounds.y + bounds.height + pad }
6061
+ ];
6062
+ for (const c of corners) {
6063
+ ctx.fillRect(c.x - handleSize / 2, c.y - handleSize / 2, handleSize, handleSize);
6064
+ ctx.strokeRect(c.x - handleSize / 2, c.y - handleSize / 2, handleSize, handleSize);
6065
+ }
6066
+ ctx.restore();
6067
+ }
6068
+ getAnnotationBounds(ann) {
6069
+ switch (ann.type) {
6070
+ case "rect":
6071
+ return { x: ann.x, y: ann.y, width: ann.width, height: ann.height };
6072
+ case "circle":
6073
+ return {
6074
+ x: ann.x - Math.abs(ann.radiusX),
6075
+ y: ann.y - Math.abs(ann.radiusY),
6076
+ width: Math.abs(ann.radiusX) * 2,
6077
+ height: Math.abs(ann.radiusY) * 2
6078
+ };
6079
+ case "arrow":
6080
+ case "line": {
6081
+ const minX = Math.min(ann.x, ann.endX);
6082
+ const minY = Math.min(ann.y, ann.endY);
6083
+ return {
6084
+ x: minX,
6085
+ y: minY,
6086
+ width: Math.max(Math.abs(ann.endX - ann.x), 10),
6087
+ height: Math.max(Math.abs(ann.endY - ann.y), 10)
6088
+ };
6089
+ }
6090
+ case "pen": {
6091
+ if (!ann.points.length) return { x: ann.x, y: ann.y, width: 0, height: 0 };
6092
+ let minX = ann.points[0].x, maxX = ann.points[0].x;
6093
+ let minY = ann.points[0].y, maxY = ann.points[0].y;
6094
+ for (const pt of ann.points) {
6095
+ if (pt.x < minX) minX = pt.x;
6096
+ if (pt.x > maxX) maxX = pt.x;
6097
+ if (pt.y < minY) minY = pt.y;
6098
+ if (pt.y > maxY) maxY = pt.y;
6099
+ }
6100
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
6101
+ }
6102
+ case "text":
6103
+ return {
6104
+ x: ann.x,
6105
+ y: ann.y,
6106
+ width: ann.text.length * ann.fontSize * 0.6,
6107
+ height: ann.fontSize * 1.2
6108
+ };
6109
+ case "image":
6110
+ return { x: ann.x, y: ann.y, width: ann.width, height: ann.height };
6111
+ }
6112
+ }
6113
+ /**
6114
+ * Draws a rule-of-thirds crop grid overlay.
6115
+ */
6116
+ drawCropOverlay(ctx, state, vpWidth, vpHeight, imgWidth, imgHeight, cropOverride) {
6117
+ const crop = cropOverride !== void 0 && cropOverride !== null ? cropOverride : state.crop ?? {
6118
+ x: 0,
6119
+ y: 0,
6120
+ width: imgWidth,
6121
+ height: imgHeight
6122
+ };
6123
+ if (!crop) return;
6124
+ const { transform, pan, zoom } = state;
6125
+ const angle = (transform.rotate % 360 + 360) % 360;
6126
+ const isSwapped = angle === 90 || angle === 270;
6127
+ const baseW = isSwapped ? imgHeight : imgWidth;
6128
+ const baseH = isSwapped ? imgWidth : imgHeight;
6129
+ ctx.save();
6130
+ ctx.translate(vpWidth / 2 + pan.x, vpHeight / 2 + pan.y);
6131
+ ctx.scale(zoom, zoom);
6132
+ const fitScale = Math.min(vpWidth * 0.85 / baseW, vpHeight * 0.85 / baseH, 1);
6133
+ const renderW = baseW * fitScale;
6134
+ const renderH = baseH * fitScale;
6135
+ const startX = -renderW / 2 + crop.x / imgWidth * renderW;
6136
+ const startY = -renderH / 2 + crop.y / imgHeight * renderH;
6137
+ const cropBoxW = crop.width / imgWidth * renderW;
6138
+ const cropBoxH = crop.height / imgHeight * renderH;
6139
+ ctx.fillStyle = "rgba(0, 0, 0, 0.45)";
6140
+ ctx.fillRect(-renderW / 2, -renderH / 2, renderW, startY - -renderH / 2);
6141
+ ctx.fillRect(-renderW / 2, startY + cropBoxH, renderW, renderH / 2 - (startY + cropBoxH));
6142
+ ctx.fillRect(-renderW / 2, startY, startX - -renderW / 2, cropBoxH);
6143
+ ctx.fillRect(startX + cropBoxW, startY, renderW / 2 - (startX + cropBoxW), cropBoxH);
6144
+ ctx.strokeStyle = "#ffffff";
6145
+ ctx.lineWidth = 2;
6146
+ ctx.strokeRect(startX, startY, cropBoxW, cropBoxH);
6147
+ ctx.strokeStyle = "rgba(255, 255, 255, 0.4)";
6148
+ ctx.lineWidth = 1;
6149
+ ctx.beginPath();
6150
+ ctx.moveTo(startX + cropBoxW / 3, startY);
6151
+ ctx.lineTo(startX + cropBoxW / 3, startY + cropBoxH);
6152
+ ctx.moveTo(startX + cropBoxW * 2 / 3, startY);
6153
+ ctx.lineTo(startX + cropBoxW * 2 / 3, startY + cropBoxH);
6154
+ ctx.moveTo(startX, startY + cropBoxH / 3);
6155
+ ctx.lineTo(startX + cropBoxW, startY + cropBoxH / 3);
6156
+ ctx.moveTo(startX, startY + cropBoxH * 2 / 3);
6157
+ ctx.lineTo(startX + cropBoxW, startY + cropBoxH * 2 / 3);
6158
+ ctx.stroke();
6159
+ ctx.fillStyle = "#ffffff";
6160
+ ctx.strokeStyle = "#0284c7";
6161
+ ctx.lineWidth = 1.5;
6162
+ const handleSize = 10;
6163
+ const handles = [
6164
+ // Corners
6165
+ { x: startX, y: startY },
6166
+ { x: startX + cropBoxW, y: startY },
6167
+ { x: startX, y: startY + cropBoxH },
6168
+ { x: startX + cropBoxW, y: startY + cropBoxH },
6169
+ // Edges
6170
+ { x: startX + cropBoxW / 2, y: startY },
6171
+ { x: startX + cropBoxW / 2, y: startY + cropBoxH },
6172
+ { x: startX, y: startY + cropBoxH / 2 },
6173
+ { x: startX + cropBoxW, y: startY + cropBoxH / 2 }
6174
+ ];
6175
+ for (const h of handles) {
6176
+ ctx.fillRect(h.x - handleSize / 2, h.y - handleSize / 2, handleSize, handleSize);
6177
+ ctx.strokeRect(h.x - handleSize / 2, h.y - handleSize / 2, handleSize, handleSize);
6178
+ }
6179
+ ctx.restore();
6180
+ }
6181
+ drawFittedImage(ctx, img, imgW, imgH, vpW, vpH, zoom, pan) {
6182
+ ctx.save();
6183
+ ctx.translate(vpW / 2 + pan.x, vpH / 2 + pan.y);
6184
+ ctx.scale(zoom, zoom);
6185
+ const fitScale = Math.min(vpW * 0.85 / imgW, vpH * 0.85 / imgH, 1);
6186
+ const renderW = imgW * fitScale;
6187
+ const renderH = imgH * fitScale;
6188
+ ctx.drawImage(img, -renderW / 2, -renderH / 2, renderW, renderH);
6189
+ ctx.restore();
6190
+ }
6191
+ /**
6192
+ * Performs an export render at exact pixel dimensions without zoom/pan viewport offsets.
6193
+ */
6194
+ async renderExport(imageSource, state, options = {}) {
6195
+ let img;
6196
+ if (typeof imageSource === "string") {
6197
+ img = await this.imageCache.get(imageSource);
6198
+ } else {
6199
+ img = imageSource;
6200
+ }
6201
+ const naturalWidth = img.naturalWidth || img.videoWidth || img.width || 800;
6202
+ const naturalHeight = img.naturalHeight || img.videoHeight || img.height || 600;
6203
+ const { crop, transform, adjustments, depthMask, annotations } = state;
6204
+ const cropX = crop ? Math.max(0, crop.x) : 0;
6205
+ const cropY = crop ? Math.max(0, crop.y) : 0;
6206
+ const cropW = crop ? Math.min(naturalWidth - cropX, crop.width) : naturalWidth;
6207
+ const cropH = crop ? Math.min(naturalHeight - cropY, crop.height) : naturalHeight;
6208
+ const angle = (transform.rotate % 360 + 360) % 360;
6209
+ const isSwapped = angle === 90 || angle === 270;
6210
+ const baseW = isSwapped ? cropH : cropW;
6211
+ const baseH = isSwapped ? cropW : cropH;
6212
+ const exportW = options.width || baseW;
6213
+ const exportH = options.height || baseH;
6214
+ const exportCanvas = this.createCanvas(exportW, exportH);
6215
+ const ctx = exportCanvas.getContext("2d");
6216
+ if (!ctx) return exportCanvas;
6217
+ const intermediateCanvas = this.createCanvas(baseW, baseH);
6218
+ const interCtx = intermediateCanvas.getContext("2d");
6219
+ if (interCtx) {
6220
+ interCtx.save();
6221
+ const rad = angle * Math.PI / 180;
6222
+ interCtx.translate(baseW / 2, baseH / 2);
6223
+ interCtx.rotate(rad);
6224
+ interCtx.scale(transform.flipH ? -1 : 1, transform.flipV ? -1 : 1);
6225
+ interCtx.drawImage(img, cropX, cropY, cropW, cropH, -cropW / 2, -cropH / 2, cropW, cropH);
6226
+ interCtx.restore();
6227
+ const imageData = interCtx.getImageData(0, 0, baseW, baseH);
6228
+ if (imageData) {
6229
+ applyAdjustments(imageData, adjustments);
6230
+ if (depthMask.enabled && depthMask.maskSource) {
6231
+ try {
6232
+ const maskImg = await this.imageCache.get(depthMask.maskSource);
6233
+ const maskCanvas = this.createCanvas(baseW, baseH);
6234
+ const maskCtx = maskCanvas.getContext("2d");
6235
+ if (maskCtx) {
6236
+ maskCtx.drawImage(maskImg, cropX, cropY, cropW, cropH, 0, 0, baseW, baseH);
6237
+ const maskData = maskCtx.getImageData(0, 0, baseW, baseH);
6238
+ if (maskData) {
6239
+ applyDepthMask(imageData, maskData, depthMask);
6240
+ }
6241
+ }
6242
+ } catch {
6243
+ }
6244
+ }
6245
+ interCtx.putImageData(imageData, 0, 0);
6246
+ }
6247
+ }
6248
+ ctx.drawImage(intermediateCanvas, 0, 0, baseW, baseH, 0, 0, exportW, exportH);
6249
+ if (options.includeAnnotations !== false && annotations.length > 0) {
6250
+ const annScaleX = exportW / baseW;
6251
+ const annScaleY = exportH / baseH;
6252
+ ctx.save();
6253
+ ctx.scale(annScaleX, annScaleY);
6254
+ for (const ann of annotations) {
6255
+ await this.drawAnnotation(ctx, ann);
6256
+ }
6257
+ ctx.restore();
6258
+ }
6259
+ return exportCanvas;
6260
+ }
6261
+ };
6262
+
6263
+ // src/image-editor/controller.ts
6264
+ var DEFAULT_ADJUSTMENTS = {
6265
+ brightness: 0,
6266
+ contrast: 0,
6267
+ saturation: 0,
6268
+ exposure: 0,
6269
+ temperature: 0,
6270
+ blur: 0,
6271
+ opacity: 1
6272
+ };
6273
+ var DEFAULT_TRANSFORM = {
6274
+ rotate: 0,
6275
+ flipH: false,
6276
+ flipV: false
6277
+ };
6278
+ var DEFAULT_DEPTH_MASK = {
6279
+ enabled: false,
6280
+ maskSource: null,
6281
+ mode: "threshold",
6282
+ depthRange: [0, 255],
6283
+ softness: 10,
6284
+ invert: false
6285
+ };
6286
+ var ImageEditorController = class {
6287
+ state;
6288
+ renderer;
6289
+ imageElement = null;
6290
+ listeners = /* @__PURE__ */ new Set();
6291
+ onStateChange;
6292
+ // History stacks
6293
+ undoStack = [];
6294
+ redoStack = [];
6295
+ maxHistoryDepth;
6296
+ isApplyingHistory = false;
6297
+ initialSnapshot;
6298
+ constructor(options = {}) {
6299
+ this.maxHistoryDepth = options.maxHistoryDepth ?? 50;
6300
+ this.onStateChange = options.onStateChange;
6301
+ this.renderer = new ImageEditorRenderer();
6302
+ const initialAdjustments = {
6303
+ ...DEFAULT_ADJUSTMENTS,
6304
+ ...options.initialState?.adjustments
6305
+ };
6306
+ const initialTransform = {
6307
+ ...DEFAULT_TRANSFORM,
6308
+ ...options.initialState?.transform
6309
+ };
6310
+ const initialDepthMask = {
6311
+ ...DEFAULT_DEPTH_MASK,
6312
+ ...options.initialState?.depthMask
6313
+ };
6314
+ this.state = {
6315
+ sourceUrl: typeof options.image === "string" ? options.image : null,
6316
+ imageDimensions: { width: 800, height: 600 },
6317
+ crop: options.initialState?.crop ?? null,
6318
+ transform: initialTransform,
6319
+ adjustments: initialAdjustments,
6320
+ depthMask: initialDepthMask,
6321
+ annotations: options.initialState?.annotations ?? [],
6322
+ selectedAnnotationId: null,
6323
+ activeTool: "select",
6324
+ zoom: 1,
6325
+ pan: { x: 0, y: 0 },
6326
+ isOriginalCompared: false
6327
+ };
6328
+ if (options.image instanceof Object && "width" in options.image) {
6329
+ this.imageElement = options.image;
6330
+ this.state.imageDimensions = {
6331
+ width: options.image.naturalWidth || options.image.width || 800,
6332
+ height: options.image.naturalHeight || options.image.height || 600
6333
+ };
6334
+ }
6335
+ this.initialSnapshot = this.serializeState();
6336
+ if (typeof options.image === "string") {
6337
+ this.loadImage(options.image);
6338
+ }
6339
+ }
6340
+ // --- Image Loading ---
6341
+ async loadImage(src) {
6342
+ if (typeof src === "string") {
6343
+ this.state.sourceUrl = src;
6344
+ try {
6345
+ const img = await this.renderer.imageCache.get(src);
6346
+ this.imageElement = img;
6347
+ this.state.imageDimensions = {
6348
+ width: img.naturalWidth || img.width || 800,
6349
+ height: img.naturalHeight || img.height || 600
6350
+ };
6351
+ } catch (err) {
6352
+ }
6353
+ } else {
6354
+ this.imageElement = src;
6355
+ this.state.sourceUrl = src.src || null;
6356
+ this.state.imageDimensions = {
6357
+ width: src.naturalWidth || src.width || 800,
6358
+ height: src.naturalHeight || src.height || 600
6359
+ };
6360
+ }
6361
+ this.initialSnapshot = this.serializeState();
6362
+ this.notify();
6363
+ }
6364
+ getImageElement() {
6365
+ return this.imageElement;
6366
+ }
6367
+ // --- State & Subscription ---
6368
+ getState() {
6369
+ return this.state;
6370
+ }
6371
+ subscribe(listener) {
6372
+ this.listeners.add(listener);
6373
+ listener(this.state);
6374
+ return () => {
6375
+ this.listeners.delete(listener);
6376
+ };
6377
+ }
6378
+ notify() {
6379
+ const currentState = { ...this.state };
6380
+ this.onStateChange?.(currentState);
6381
+ this.listeners.forEach((l) => l(currentState));
6382
+ }
6383
+ // --- History (Undo / Redo / Reset) ---
6384
+ recordHistory() {
6385
+ if (this.isApplyingHistory) return;
6386
+ this.undoStack.push(this.serializeState());
6387
+ if (this.undoStack.length > this.maxHistoryDepth) {
6388
+ this.undoStack.shift();
6389
+ }
6390
+ this.redoStack = [];
6391
+ }
6392
+ get canUndo() {
6393
+ return this.undoStack.length > 0;
6394
+ }
6395
+ get canRedo() {
6396
+ return this.redoStack.length > 0;
6397
+ }
6398
+ undo() {
6399
+ if (!this.canUndo) return false;
6400
+ this.isApplyingHistory = true;
6401
+ const current = this.serializeState();
6402
+ this.redoStack.push(current);
6403
+ const prev = this.undoStack.pop();
6404
+ this.applySerializedState(prev);
6405
+ this.isApplyingHistory = false;
6406
+ this.notify();
6407
+ return true;
6408
+ }
6409
+ redo() {
6410
+ if (!this.canRedo) return false;
6411
+ this.isApplyingHistory = true;
6412
+ const current = this.serializeState();
6413
+ this.undoStack.push(current);
6414
+ const next = this.redoStack.pop();
6415
+ this.applySerializedState(next);
6416
+ this.isApplyingHistory = false;
6417
+ this.notify();
6418
+ return true;
6419
+ }
6420
+ reset() {
6421
+ this.recordHistory();
6422
+ this.applySerializedState(this.initialSnapshot);
6423
+ this.state.zoom = 1;
6424
+ this.state.pan = { x: 0, y: 0 };
6425
+ this.state.activeTool = "select";
6426
+ this.state.selectedAnnotationId = null;
6427
+ this.notify();
6428
+ }
6429
+ // --- Active Tool ---
6430
+ setTool(tool) {
6431
+ if (this.state.activeTool === tool) return;
6432
+ this.state.activeTool = tool;
6433
+ if (tool !== "select") {
6434
+ this.state.selectedAnnotationId = null;
6435
+ }
6436
+ if (tool === "crop" && !this.state.crop) {
6437
+ this.state.crop = {
6438
+ x: 0,
6439
+ y: 0,
6440
+ width: this.state.imageDimensions.width,
6441
+ height: this.state.imageDimensions.height
6442
+ };
6443
+ }
6444
+ this.notify();
6445
+ }
6446
+ // --- Crop ---
6447
+ setCrop(crop, addToHistory = true) {
6448
+ if (addToHistory) {
6449
+ this.recordHistory();
6450
+ }
6451
+ this.state.crop = crop;
6452
+ this.notify();
6453
+ }
6454
+ applyCropPreset(ratio) {
6455
+ this.recordHistory();
6456
+ const { width, height } = this.state.imageDimensions;
6457
+ if (!ratio) {
6458
+ this.state.crop = { x: 0, y: 0, width, height };
6459
+ } else {
6460
+ let targetW = width;
6461
+ let targetH = targetW / ratio;
6462
+ if (targetH > height) {
6463
+ targetH = height;
6464
+ targetW = targetH * ratio;
6465
+ }
6466
+ const x = Math.max(0, Math.round((width - targetW) / 2));
6467
+ const y = Math.max(0, Math.round((height - targetH) / 2));
6468
+ this.state.crop = {
6469
+ x,
6470
+ y,
6471
+ width: Math.round(targetW),
6472
+ height: Math.round(targetH)
6473
+ };
6474
+ }
6475
+ this.notify();
6476
+ }
6477
+ resetCrop() {
6478
+ this.recordHistory();
6479
+ this.state.crop = null;
6480
+ this.notify();
6481
+ }
6482
+ // --- Transformations (Rotate / Flip) ---
6483
+ rotate(degrees) {
6484
+ this.recordHistory();
6485
+ const current = this.state.transform.rotate;
6486
+ this.state.transform.rotate = (current + degrees) % 360;
6487
+ this.notify();
6488
+ }
6489
+ setRotation(degrees) {
6490
+ this.recordHistory();
6491
+ this.state.transform.rotate = degrees % 360;
6492
+ this.notify();
6493
+ }
6494
+ flipHorizontal() {
6495
+ this.recordHistory();
6496
+ this.state.transform.flipH = !this.state.transform.flipH;
6497
+ this.notify();
6498
+ }
6499
+ flipVertical() {
6500
+ this.recordHistory();
6501
+ this.state.transform.flipV = !this.state.transform.flipV;
6502
+ this.notify();
6503
+ }
6504
+ // --- Adjustments ---
6505
+ setAdjustments(partial, commitToHistory = true) {
6506
+ if (commitToHistory) {
6507
+ this.recordHistory();
6508
+ }
6509
+ this.state.adjustments = {
6510
+ ...this.state.adjustments,
6511
+ ...partial
6512
+ };
6513
+ this.notify();
6514
+ }
6515
+ resetAdjustments() {
6516
+ this.recordHistory();
6517
+ this.state.adjustments = { ...DEFAULT_ADJUSTMENTS };
6518
+ this.notify();
6519
+ }
6520
+ // --- Depth Masking (Gleamforge) ---
6521
+ setDepthMask(partial) {
6522
+ this.recordHistory();
6523
+ this.state.depthMask = {
6524
+ ...this.state.depthMask,
6525
+ ...partial
6526
+ };
6527
+ this.notify();
6528
+ }
6529
+ resetDepthMask() {
6530
+ this.recordHistory();
6531
+ this.state.depthMask = { ...DEFAULT_DEPTH_MASK };
6532
+ this.notify();
6533
+ }
6534
+ // --- Annotations & Layers ---
6535
+ addAnnotation(annotation) {
6536
+ this.recordHistory();
6537
+ const id = `ann_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
6538
+ const fullAnnotation = { ...annotation, id };
6539
+ this.state.annotations = [...this.state.annotations, fullAnnotation];
6540
+ this.state.selectedAnnotationId = id;
6541
+ this.notify();
6542
+ return id;
6543
+ }
6544
+ updateAnnotation(id, partial, commitToHistory = true) {
6545
+ if (commitToHistory) {
6546
+ this.recordHistory();
6547
+ }
6548
+ this.state.annotations = this.state.annotations.map((ann) => {
6549
+ if (ann.id === id) {
6550
+ return { ...ann, ...partial };
6551
+ }
6552
+ return ann;
6553
+ });
6554
+ this.notify();
6555
+ }
6556
+ removeAnnotation(id) {
6557
+ this.recordHistory();
6558
+ this.state.annotations = this.state.annotations.filter((ann) => ann.id !== id);
6559
+ if (this.state.selectedAnnotationId === id) {
6560
+ this.state.selectedAnnotationId = null;
6561
+ }
6562
+ this.notify();
6563
+ }
6564
+ clearAnnotations() {
6565
+ this.recordHistory();
6566
+ this.state.annotations = [];
6567
+ this.state.selectedAnnotationId = null;
6568
+ this.notify();
6569
+ }
6570
+ selectAnnotation(id) {
6571
+ this.state.selectedAnnotationId = id;
6572
+ this.notify();
6573
+ }
6574
+ // --- Zoom & Pan ---
6575
+ setZoom(zoom) {
6576
+ this.state.zoom = Math.max(0.1, Math.min(10, zoom));
6577
+ this.notify();
6578
+ }
6579
+ zoomIn(factor = 1.2) {
6580
+ this.setZoom(this.state.zoom * factor);
6581
+ }
6582
+ zoomOut(factor = 1.2) {
6583
+ this.setZoom(this.state.zoom / factor);
6584
+ }
6585
+ resetZoom() {
6586
+ this.state.zoom = 1;
6587
+ this.state.pan = { x: 0, y: 0 };
6588
+ this.notify();
6589
+ }
6590
+ setPan(pan) {
6591
+ this.state.pan = pan;
6592
+ this.notify();
6593
+ }
6594
+ setComparing(comparing) {
6595
+ if (this.state.isOriginalCompared === comparing) return;
6596
+ this.state.isOriginalCompared = comparing;
6597
+ this.notify();
6598
+ }
6599
+ // --- Rendering & Export ---
6600
+ async render(targetCanvas, options) {
6601
+ const source = this.imageElement || this.state.sourceUrl;
6602
+ if (!source) return;
6603
+ await this.renderer.renderToCanvas(targetCanvas, source, this.state, options);
6604
+ }
6605
+ async renderExport(options) {
6606
+ const source = this.imageElement || this.state.sourceUrl;
6607
+ if (!source) {
6608
+ return this.renderer.createCanvas(1, 1);
6609
+ }
6610
+ return this.renderer.renderExport(source, this.state, options);
6611
+ }
6612
+ async toDataURL(options = {}) {
6613
+ const canvas = await this.renderExport(options);
6614
+ const format = options.format || "image/png";
6615
+ const quality = options.quality ?? 0.92;
6616
+ return canvas.toDataURL(format, quality);
6617
+ }
6618
+ async toBlob(options = {}) {
6619
+ const canvas = await this.renderExport(options);
6620
+ const format = options.format || "image/png";
6621
+ const quality = options.quality ?? 0.92;
6622
+ return new Promise((resolve, reject) => {
6623
+ if (canvas.toBlob) {
6624
+ canvas.toBlob((blob) => {
6625
+ if (blob) resolve(blob);
6626
+ else reject(new Error("Failed to create Blob from canvas"));
6627
+ }, format, quality);
6628
+ } else {
6629
+ try {
6630
+ const dataUrl = canvas.toDataURL(format, quality);
6631
+ const parts = dataUrl.split(",");
6632
+ const byteStr = atob(parts[1]);
6633
+ const ab = new ArrayBuffer(byteStr.length);
6634
+ const ia = new Uint8Array(ab);
6635
+ for (let i = 0; i < byteStr.length; i++) {
6636
+ ia[i] = byteStr.charCodeAt(i);
6637
+ }
6638
+ resolve(new Blob([ab], { type: format }));
6639
+ } catch (err) {
6640
+ reject(err);
6641
+ }
6642
+ }
6643
+ });
6644
+ }
6645
+ // --- Serialization ---
6646
+ toJSON() {
6647
+ return this.serializeState();
6648
+ }
6649
+ loadJSON(data) {
6650
+ this.recordHistory();
6651
+ this.applySerializedState(data);
6652
+ this.notify();
6653
+ }
6654
+ serializeState() {
6655
+ return {
6656
+ version: 1,
6657
+ crop: this.state.crop ? { ...this.state.crop } : null,
6658
+ transform: { ...this.state.transform },
6659
+ adjustments: { ...this.state.adjustments },
6660
+ depthMask: {
6661
+ ...this.state.depthMask,
6662
+ depthRange: [...this.state.depthMask.depthRange]
6663
+ },
6664
+ annotations: JSON.parse(JSON.stringify(this.state.annotations))
6665
+ };
6666
+ }
6667
+ applySerializedState(data) {
6668
+ this.state.crop = data.crop ? { ...data.crop } : null;
6669
+ this.state.transform = { ...data.transform };
6670
+ this.state.adjustments = { ...data.adjustments };
6671
+ this.state.depthMask = {
6672
+ ...data.depthMask,
6673
+ depthRange: [...data.depthMask.depthRange]
6674
+ };
6675
+ this.state.annotations = JSON.parse(JSON.stringify(data.annotations || []));
6676
+ }
6677
+ };
6678
+
6679
+ // src/image-editor/geometry.ts
6680
+ function getViewportMetrics(state, vpWidth, vpHeight, isActivelyCropping = false, cropOverride) {
6681
+ const { imageDimensions, crop: stateCrop, transform, zoom, pan } = state;
6682
+ const naturalWidth = imageDimensions.width || 800;
6683
+ const naturalHeight = imageDimensions.height || 600;
6684
+ const activeCrop = cropOverride !== void 0 && cropOverride !== null ? cropOverride : stateCrop ?? (isActivelyCropping ? { x: 0, y: 0, width: naturalWidth, height: naturalHeight } : null);
6685
+ const cropW = !isActivelyCropping && activeCrop ? activeCrop.width : naturalWidth;
6686
+ const cropH = !isActivelyCropping && activeCrop ? activeCrop.height : naturalHeight;
6687
+ const angle = (transform.rotate % 360 + 360) % 360;
6688
+ const isSwapped = angle === 90 || angle === 270;
6689
+ const baseW = isSwapped ? cropH : cropW;
6690
+ const baseH = isSwapped ? cropW : cropH;
6691
+ const fitScale = Math.min(vpWidth * 0.85 / baseW, vpHeight * 0.85 / baseH, 1);
6692
+ const renderW = baseW * fitScale;
6693
+ const renderH = baseH * fitScale;
6694
+ const totalScale = zoom * fitScale;
6695
+ const centerX = vpWidth / 2 + pan.x;
6696
+ const centerY = vpHeight / 2 + pan.y;
6697
+ const imageRect = {
6698
+ x: centerX - renderW * zoom / 2,
6699
+ y: centerY - renderH * zoom / 2,
6700
+ width: renderW * zoom,
6701
+ height: renderH * zoom
6702
+ };
6703
+ let cropRect = null;
6704
+ if (activeCrop) {
6705
+ const startX = -renderW / 2 + activeCrop.x / naturalWidth * renderW;
6706
+ const startY = -renderH / 2 + activeCrop.y / naturalHeight * renderH;
6707
+ const cropBoxW = activeCrop.width / naturalWidth * renderW;
6708
+ const cropBoxH = activeCrop.height / naturalHeight * renderH;
6709
+ cropRect = {
6710
+ x: centerX + zoom * startX,
6711
+ y: centerY + zoom * startY,
6712
+ width: zoom * cropBoxW,
6713
+ height: zoom * cropBoxH
6714
+ };
6715
+ }
6716
+ return {
6717
+ vpWidth,
6718
+ vpHeight,
6719
+ centerX,
6720
+ centerY,
6721
+ baseW,
6722
+ baseH,
6723
+ fitScale,
6724
+ renderW,
6725
+ renderH,
6726
+ totalScale,
6727
+ imageRect,
6728
+ cropRect
6729
+ };
6730
+ }
6731
+ function canvasToImagePoint(canvasPt, state, vpWidth, vpHeight, isActivelyCropping = false) {
6732
+ const metrics = getViewportMetrics(state, vpWidth, vpHeight, isActivelyCropping);
6733
+ const unzoomedX = (canvasPt.x - metrics.centerX) / state.zoom;
6734
+ const unzoomedY = (canvasPt.y - metrics.centerY) / state.zoom;
6735
+ const x = (unzoomedX + metrics.renderW / 2) / metrics.fitScale;
6736
+ const y = (unzoomedY + metrics.renderH / 2) / metrics.fitScale;
6737
+ return { x: Math.round(x), y: Math.round(y) };
6738
+ }
6739
+ function imageToCanvasPoint(imgPt, state, vpWidth, vpHeight, isActivelyCropping = false) {
6740
+ const metrics = getViewportMetrics(state, vpWidth, vpHeight, isActivelyCropping);
6741
+ const unzoomedX = (imgPt.x - metrics.baseW / 2) * metrics.fitScale;
6742
+ const unzoomedY = (imgPt.y - metrics.baseH / 2) * metrics.fitScale;
6743
+ const canvasX = metrics.centerX + unzoomedX * state.zoom;
6744
+ const canvasY = metrics.centerY + unzoomedY * state.zoom;
6745
+ return { x: Math.round(canvasX), y: Math.round(canvasY) };
6746
+ }
6747
+ function hitTestCrop(canvasPt, state, vpWidth, vpHeight, handleThreshold = 14, cropOverride) {
6748
+ const metrics = getViewportMetrics(state, vpWidth, vpHeight, true, cropOverride);
6749
+ if (!metrics.cropRect) return null;
6750
+ const { x, y, width: w, height: h } = metrics.cropRect;
6751
+ const px = canvasPt.x;
6752
+ const py = canvasPt.y;
6753
+ const dist = (x1, y1, x2, y2) => Math.hypot(x1 - x2, y1 - y2);
6754
+ if (dist(px, py, x, y) <= handleThreshold) return "nw";
6755
+ if (dist(px, py, x + w, y) <= handleThreshold) return "ne";
6756
+ if (dist(px, py, x + w, y + h) <= handleThreshold) return "se";
6757
+ if (dist(px, py, x, y + h) <= handleThreshold) return "sw";
6758
+ const nearX = px >= x - handleThreshold && px <= x + w + handleThreshold;
6759
+ const nearY = py >= y - handleThreshold && py <= y + h + handleThreshold;
6760
+ if (nearX && Math.abs(py - y) <= handleThreshold) return "n";
6761
+ if (nearX && Math.abs(py - (y + h)) <= handleThreshold) return "s";
6762
+ if (nearY && Math.abs(px - x) <= handleThreshold) return "w";
6763
+ if (nearY && Math.abs(px - (x + w)) <= handleThreshold) return "e";
6764
+ if (px >= x && px <= x + w && py >= y && py <= y + h) {
6765
+ return "inside";
6766
+ }
6767
+ return null;
6768
+ }
6769
+ function getCropCursor(handle) {
6770
+ switch (handle) {
6771
+ case "nw":
6772
+ case "se":
6773
+ return "nwse-resize";
6774
+ case "ne":
6775
+ case "sw":
6776
+ return "nesw-resize";
6777
+ case "n":
6778
+ case "s":
6779
+ return "ns-resize";
6780
+ case "e":
6781
+ case "w":
6782
+ return "ew-resize";
6783
+ case "inside":
6784
+ return "move";
6785
+ default:
6786
+ return "crosshair";
6787
+ }
6788
+ }
6789
+ function calculateCropDrag(startCrop, handle, startImgPt, currentImgPt, imgWidth, imgHeight, aspectRatio) {
6790
+ const dx = currentImgPt.x - startImgPt.x;
6791
+ const dy = currentImgPt.y - startImgPt.y;
6792
+ if (handle === "inside") {
6793
+ const maxX = Math.max(0, imgWidth - startCrop.width);
6794
+ const maxY = Math.max(0, imgHeight - startCrop.height);
6795
+ const newX = Math.max(0, Math.min(maxX, startCrop.x + dx));
6796
+ const newY = Math.max(0, Math.min(maxY, startCrop.y + dy));
6797
+ return {
6798
+ x: Math.round(newX),
6799
+ y: Math.round(newY),
6800
+ width: startCrop.width,
6801
+ height: startCrop.height
6802
+ };
6803
+ }
6804
+ let left = startCrop.x;
6805
+ let top = startCrop.y;
6806
+ let right = startCrop.x + startCrop.width;
6807
+ let bottom = startCrop.y + startCrop.height;
6808
+ const minSize = 20;
6809
+ if (handle === "e" || handle === "ne" || handle === "se") {
6810
+ right = Math.min(imgWidth, Math.max(left + minSize, startCrop.x + startCrop.width + dx));
6811
+ }
6812
+ if (handle === "w" || handle === "nw" || handle === "sw") {
6813
+ left = Math.max(0, Math.min(right - minSize, startCrop.x + dx));
6814
+ }
6815
+ if (handle === "s" || handle === "se" || handle === "sw") {
6816
+ bottom = Math.min(imgHeight, Math.max(top + minSize, startCrop.y + startCrop.height + dy));
6817
+ }
6818
+ if (handle === "n" || handle === "nw" || handle === "ne") {
6819
+ top = Math.max(0, Math.min(bottom - minSize, startCrop.y + dy));
6820
+ }
6821
+ if (aspectRatio && aspectRatio > 0) {
6822
+ let currentW = right - left;
6823
+ let currentH = bottom - top;
6824
+ if (handle === "e" || handle === "w") {
6825
+ currentH = Math.min(imgHeight, currentW / aspectRatio);
6826
+ bottom = Math.min(imgHeight, top + currentH);
6827
+ } else if (handle === "n" || handle === "s") {
6828
+ currentW = Math.min(imgWidth, currentH * aspectRatio);
6829
+ right = Math.min(imgWidth, left + currentW);
6830
+ } else {
6831
+ const targetH = currentW / aspectRatio;
6832
+ if (top + targetH <= imgHeight) {
6833
+ bottom = top + targetH;
6834
+ } else {
6835
+ const targetW = currentH * aspectRatio;
6836
+ right = Math.min(imgWidth, left + targetW);
6837
+ }
6838
+ }
6839
+ }
6840
+ return {
6841
+ x: Math.round(left),
6842
+ y: Math.round(top),
6843
+ width: Math.round(Math.max(minSize, right - left)),
6844
+ height: Math.round(Math.max(minSize, bottom - top))
6845
+ };
6846
+ }
6847
+ function getAnnotationBounds(ann) {
6848
+ switch (ann.type) {
6849
+ case "rect":
6850
+ return { x: ann.x, y: ann.y, width: ann.width, height: ann.height };
6851
+ case "circle":
6852
+ return {
6853
+ x: ann.x - ann.radiusX,
6854
+ y: ann.y - ann.radiusY,
6855
+ width: ann.radiusX * 2,
6856
+ height: ann.radiusY * 2
6857
+ };
6858
+ case "arrow":
6859
+ case "line": {
6860
+ const minX = Math.min(ann.x, ann.endX);
6861
+ const minY = Math.min(ann.y, ann.endY);
6862
+ return {
6863
+ x: minX,
6864
+ y: minY,
6865
+ width: Math.max(Math.abs(ann.endX - ann.x), 10),
6866
+ height: Math.max(Math.abs(ann.endY - ann.y), 10)
6867
+ };
6868
+ }
6869
+ case "pen": {
6870
+ if (!ann.points.length) return { x: ann.x, y: ann.y, width: 0, height: 0 };
6871
+ let minX = ann.points[0].x;
6872
+ let maxX = ann.points[0].x;
6873
+ let minY = ann.points[0].y;
6874
+ let maxY = ann.points[0].y;
6875
+ for (const pt of ann.points) {
6876
+ if (pt.x < minX) minX = pt.x;
6877
+ if (pt.x > maxX) maxX = pt.x;
6878
+ if (pt.y < minY) minY = pt.y;
6879
+ if (pt.y > maxY) maxY = pt.y;
6880
+ }
6881
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
6882
+ }
6883
+ case "text":
6884
+ return {
6885
+ x: ann.x,
6886
+ y: ann.y,
6887
+ width: ann.text.length * ann.fontSize * 0.6,
6888
+ height: ann.fontSize * 1.2
6889
+ };
6890
+ case "image":
6891
+ return { x: ann.x, y: ann.y, width: ann.width, height: ann.height };
6892
+ }
6893
+ }
6894
+ function hitTestAnnotation(imgPt, ann, padding = 10) {
6895
+ const b = getAnnotationBounds(ann);
6896
+ 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;
6897
+ }
5593
6898
  // Annotate the CommonJS export names for ESM import in node:
5594
6899
  0 && (module.exports = {
5595
6900
  ColorHighlighter,
6901
+ DEFAULT_ADJUSTMENTS,
6902
+ DEFAULT_DEPTH_MASK,
5596
6903
  DEFAULT_NODE_HEIGHT,
5597
6904
  DEFAULT_NODE_WIDTH,
5598
6905
  DEFAULT_PLACEMENT_GAP,
6906
+ DEFAULT_TRANSFORM,
5599
6907
  DataSourceBinder,
5600
6908
  DependencyGraph,
5601
6909
  DirtyTracker,
@@ -5609,6 +6917,8 @@ var EDITOR_MCP_TOOLS = [
5609
6917
  HistoryManager,
5610
6918
  INSTRUCTION_REGEX,
5611
6919
  Image,
6920
+ ImageEditorController,
6921
+ ImageEditorRenderer,
5612
6922
  LoadingNode,
5613
6923
  MARKDOWN_MENTION_SPLIT_REGEX,
5614
6924
  MCP_TOOL_EDIT_CANVAS,
@@ -5626,11 +6936,15 @@ var EDITOR_MCP_TOOLS = [
5626
6936
  TEMPLATE_REGEX,
5627
6937
  TemplateSuggestions,
5628
6938
  VARIABLE_REGEX,
6939
+ applyAdjustments,
6940
+ applyDepthMask,
5629
6941
  blendLayout,
5630
6942
  boundsOf,
6943
+ calculateCropDrag,
5631
6944
  calculateDagreReflowDisplacement,
5632
6945
  calculateDirectionalDisplacement,
5633
6946
  canvasHeight,
6947
+ canvasToImagePoint,
5634
6948
  centerNodes,
5635
6949
  centre,
5636
6950
  colNameToIndex,
@@ -5657,14 +6971,21 @@ var EDITOR_MCP_TOOLS = [
5657
6971
  extractVariableNames,
5658
6972
  findHexColors,
5659
6973
  flowSide,
6974
+ getAnnotationBounds,
5660
6975
  getCenteredNodePosition,
6976
+ getCropCursor,
5661
6977
  getEditorMarkdown,
5662
6978
  getEditorText,
5663
6979
  getLatestVersion,
5664
6980
  getLayoutedNodes,
6981
+ getViewportMetrics,
6982
+ globalImageCache,
5665
6983
  handleImageInsertion,
6984
+ hitTestAnnotation,
6985
+ hitTestCrop,
5666
6986
  htmlToPlainText,
5667
6987
  imagePastePluginKey,
6988
+ imageToCanvasPoint,
5668
6989
  indexToColName,
5669
6990
  insertImageNode,
5670
6991
  instructionRegex,
@@ -5691,6 +7012,7 @@ var EDITOR_MCP_TOOLS = [
5691
7012
  preprocessMarkdownMentions,
5692
7013
  realClock,
5693
7014
  remapPastedNodes,
7015
+ removeAlpha,
5694
7016
  removeImageNode,
5695
7017
  repositionNodesTo,
5696
7018
  resolveTemplates,