@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.cjs CHANGED
@@ -31,13 +31,17 @@ 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,
40
43
  EDITOR_MCP_TOOLS: () => EDITOR_MCP_TOOLS,
44
+ EXPORT_EXCLUDED_SELECTORS: () => EXPORT_EXCLUDED_SELECTORS,
41
45
  EditorController: () => EditorController,
42
46
  FontSize: () => FontSize,
43
47
  FormulaEvaluator: () => FormulaEvaluator,
@@ -45,13 +49,17 @@ __export(src_exports, {
45
49
  GradientText: () => GradientText,
46
50
  HistoryManager: () => HistoryManager,
47
51
  INSTRUCTION_REGEX: () => INSTRUCTION_REGEX,
48
- Image: () => Image2,
52
+ Image: () => Image3,
53
+ ImageEditorController: () => ImageEditorController,
54
+ ImageEditorRenderer: () => ImageEditorRenderer,
49
55
  LoadingNode: () => LoadingNode,
50
56
  MARKDOWN_MENTION_SPLIT_REGEX: () => MARKDOWN_MENTION_SPLIT_REGEX,
51
57
  MCP_TOOL_EDIT_CANVAS: () => MCP_TOOL_EDIT_CANVAS,
52
58
  MCP_TOOL_EDIT_PROMPT: () => MCP_TOOL_EDIT_PROMPT,
53
59
  MCP_TOOL_GET_EDITOR_STATE: () => MCP_TOOL_GET_EDITOR_STATE,
60
+ MCP_TOOL_VIEW_EDITOR: () => MCP_TOOL_VIEW_EDITOR,
54
61
  MENTION_REGEX: () => MENTION_REGEX,
62
+ ManualClock: () => ManualClock,
55
63
  Markdown: () => Markdown2,
56
64
  Mention: () => Mention,
57
65
  SlashCommands: () => SlashCommands,
@@ -61,13 +69,20 @@ __export(src_exports, {
61
69
  TEMPLATE_REGEX: () => TEMPLATE_REGEX,
62
70
  TemplateSuggestions: () => TemplateSuggestions,
63
71
  VARIABLE_REGEX: () => VARIABLE_REGEX,
72
+ applyAdjustments: () => applyAdjustments,
73
+ applyDepthMask: () => applyDepthMask,
74
+ blendLayout: () => blendLayout,
64
75
  boundsOf: () => boundsOf,
76
+ calculateCropDrag: () => calculateCropDrag,
65
77
  calculateDagreReflowDisplacement: () => calculateDagreReflowDisplacement,
66
78
  calculateDirectionalDisplacement: () => calculateDirectionalDisplacement,
79
+ canvasHeight: () => canvasHeight,
80
+ canvasToImagePoint: () => canvasToImagePoint,
67
81
  centerNodes: () => centerNodes,
68
82
  centre: () => centre,
69
83
  colNameToIndex: () => colNameToIndex,
70
84
  computeNodesBoundingBox: () => computeNodesBoundingBox,
85
+ connectedOnly: () => connectedOnly,
71
86
  createBezierTrajectory: () => createBezierTrajectory,
72
87
  createDirtyTracker: () => createDirtyTracker,
73
88
  createEditor: () => createEditor,
@@ -79,6 +94,7 @@ __export(src_exports, {
79
94
  defaultIsEqual: () => defaultIsEqual,
80
95
  ensureLayout: () => ensureLayout,
81
96
  escapeHtml: () => escapeHtml,
97
+ estimateCanvasHeight: () => estimateCanvasHeight,
82
98
  estimateNodeSize: () => estimateNodeSize,
83
99
  extractCellDependencies: () => extractCellDependencies,
84
100
  extractInstructionNames: () => extractInstructionNames,
@@ -88,26 +104,35 @@ __export(src_exports, {
88
104
  extractVariableNames: () => extractVariableNames,
89
105
  findHexColors: () => findHexColors,
90
106
  flowSide: () => flowSide,
107
+ getAnnotationBounds: () => getAnnotationBounds,
91
108
  getCenteredNodePosition: () => getCenteredNodePosition,
109
+ getCropCursor: () => getCropCursor,
92
110
  getEditorMarkdown: () => getEditorMarkdown,
93
111
  getEditorText: () => getEditorText,
94
112
  getLatestVersion: () => getLatestVersion,
95
113
  getLayoutedNodes: () => getLayoutedNodes,
114
+ getViewportMetrics: () => getViewportMetrics,
115
+ globalImageCache: () => globalImageCache,
96
116
  handleImageInsertion: () => handleImageInsertion,
117
+ hitTestAnnotation: () => hitTestAnnotation,
118
+ hitTestCrop: () => hitTestCrop,
97
119
  htmlToPlainText: () => htmlToPlainText,
98
120
  imagePastePluginKey: () => imagePastePluginKey,
121
+ imageToCanvasPoint: () => imageToCanvasPoint,
99
122
  indexToColName: () => indexToColName,
100
123
  insertImageNode: () => insertImageNode,
101
124
  instructionRegex: () => instructionRegex,
102
125
  isEventFromTextInput: () => isEventFromTextInput,
103
126
  isValidCanvasNode: () => isValidCanvasNode,
104
127
  isValidUrl: () => isValidUrl,
128
+ layoutBounds: () => layoutBounds,
105
129
  linearPositionTrajectory: () => linearPositionTrajectory,
106
130
  linearTrajectory: () => linearTrajectory,
107
131
  mapRawOffsetToResolved: () => mapRawOffsetToResolved,
108
132
  mapResolvedOffsetToRaw: () => mapResolvedOffsetToRaw,
109
133
  markdownToTipTapHtml: () => markdownToTipTapHtml,
110
134
  overlaps: () => overlaps,
135
+ pageHidden: () => pageHidden,
111
136
  parseCanvasClipboardData: () => parseCanvasClipboardData,
112
137
  parseCellReference: () => parseCellReference,
113
138
  parseFormula: () => parseFormula,
@@ -118,7 +143,9 @@ __export(src_exports, {
118
143
  planAgentCommand: () => planAgentCommand,
119
144
  preprocessMarkdownFileLinks: () => preprocessMarkdownFileLinks,
120
145
  preprocessMarkdownMentions: () => preprocessMarkdownMentions,
146
+ realClock: () => realClock,
121
147
  remapPastedNodes: () => remapPastedNodes,
148
+ removeAlpha: () => removeAlpha,
122
149
  removeImageNode: () => removeImageNode,
123
150
  repositionNodesTo: () => repositionNodesTo,
124
151
  resolveTemplates: () => resolveTemplates,
@@ -1409,10 +1436,10 @@ var HorizontalRule$1 = HorizontalRule.extend({
1409
1436
  };
1410
1437
  }
1411
1438
  });
1412
- var Image = import_core8.Node.create({
1439
+ var Image2 = import_core8.Node.create({
1413
1440
  name: "image"
1414
1441
  });
1415
- var Image$1 = Image.extend({
1442
+ var Image$1 = Image2.extend({
1416
1443
  /**
1417
1444
  * @return {{markdown: MarkdownNodeSpec}}
1418
1445
  */
@@ -2125,7 +2152,7 @@ function handleImageInsertion(view, file, options) {
2125
2152
  }
2126
2153
  }
2127
2154
  }
2128
- var Image2 = BaseImage.extend({
2155
+ var Image3 = BaseImage.extend({
2129
2156
  addOptions() {
2130
2157
  return {
2131
2158
  ...this.parent?.(),
@@ -2320,7 +2347,7 @@ function createRichTextPreset(options = {}) {
2320
2347
  if (options.image !== false) {
2321
2348
  const imageConfig = typeof options.image === "object" ? options.image : {};
2322
2349
  extensions2.push(
2323
- Image2.configure({
2350
+ Image3.configure({
2324
2351
  HTMLAttributes: {
2325
2352
  class: "rounded-md max-w-full h-auto my-2",
2326
2353
  ...imageConfig.HTMLAttributes || {}
@@ -4585,6 +4612,79 @@ function placeBeside(rects, size, side, nearNodeId, options = {}) {
4585
4612
  };
4586
4613
  }
4587
4614
 
4615
+ // src/canvas/clock.ts
4616
+ var pageHidden = () => typeof document !== "undefined" && document.hidden;
4617
+ var realClock = {
4618
+ live: true,
4619
+ now: () => typeof performance !== "undefined" ? performance.now() : Date.now(),
4620
+ frame: () => new Promise((resolve) => {
4621
+ if (typeof requestAnimationFrame === "function" && !pageHidden()) {
4622
+ requestAnimationFrame(() => resolve());
4623
+ } else {
4624
+ setTimeout(resolve, 16);
4625
+ }
4626
+ }),
4627
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)))
4628
+ };
4629
+ var settle = () => new Promise((resolve) => setTimeout(resolve, 0));
4630
+ var ManualClock = class {
4631
+ live = false;
4632
+ time;
4633
+ frames = [];
4634
+ sleepers = [];
4635
+ seq = 0;
4636
+ constructor(start = 0) {
4637
+ this.time = start;
4638
+ }
4639
+ now() {
4640
+ return this.time;
4641
+ }
4642
+ frame() {
4643
+ return new Promise((resolve) => this.frames.push(resolve));
4644
+ }
4645
+ sleep(ms) {
4646
+ return new Promise(
4647
+ (resolve) => this.sleepers.push({
4648
+ at: this.time + Math.max(0, ms),
4649
+ seq: this.seq++,
4650
+ resolve
4651
+ })
4652
+ );
4653
+ }
4654
+ /** Whether any frames or sleep timers are currently pending. */
4655
+ get pending() {
4656
+ return this.frames.length > 0 || this.sleepers.length > 0;
4657
+ }
4658
+ /** Advances time by `ms`, resolving due sleep timers in order, followed by queued frames. */
4659
+ async advance(ms) {
4660
+ const end = this.time + Math.max(0, ms);
4661
+ await settle();
4662
+ while (true) {
4663
+ let next;
4664
+ for (const s of this.sleepers) {
4665
+ if (s.at <= end && (!next || s.at < next.at || s.at === next.at && s.seq < next.seq)) {
4666
+ next = s;
4667
+ }
4668
+ }
4669
+ if (!next) break;
4670
+ this.sleepers.splice(this.sleepers.indexOf(next), 1);
4671
+ this.time = Math.max(this.time, next.at);
4672
+ next.resolve();
4673
+ await settle();
4674
+ }
4675
+ this.time = end;
4676
+ const dueFrames = this.frames.splice(0, this.frames.length);
4677
+ for (const resolveFrame of dueFrames) {
4678
+ resolveFrame();
4679
+ }
4680
+ await settle();
4681
+ }
4682
+ /** Convenience method to advance by 1 frame at a given fps (default: 60fps = 16.67ms). */
4683
+ async tick(fps = 60) {
4684
+ await this.advance(Math.round(1e3 / fps));
4685
+ }
4686
+ };
4687
+
4588
4688
  // src/canvas/trajectory.ts
4589
4689
  var defaultEasing = (t) => --t * t * t + 1;
4590
4690
  var linearTrajectory = (origin, target, progress) => {
@@ -4652,9 +4752,15 @@ function runMultiNodeTransition(transitions, options = {}) {
4652
4752
  return () => {
4653
4753
  };
4654
4754
  }
4655
- const { defaultDuration = 400, defaultEasing: easingFunc = defaultEasing, onUpdate, onComplete } = options;
4755
+ const {
4756
+ defaultDuration = 400,
4757
+ defaultEasing: easingFunc = defaultEasing,
4758
+ clock = realClock,
4759
+ onUpdate,
4760
+ onComplete
4761
+ } = options;
4656
4762
  let isCancelled = false;
4657
- const startTime = Date.now();
4763
+ const startTime = clock.now();
4658
4764
  const items = transitions.map((tr, i) => ({
4659
4765
  ...tr,
4660
4766
  index: tr.index ?? i,
@@ -4666,7 +4772,7 @@ function runMultiNodeTransition(transitions, options = {}) {
4666
4772
  }));
4667
4773
  function frame() {
4668
4774
  if (isCancelled) return;
4669
- const now = Date.now();
4775
+ const now = clock.now();
4670
4776
  let allCompleted = true;
4671
4777
  for (let i = 0; i < items.length; i++) {
4672
4778
  const item = items[i];
@@ -4698,9 +4804,7 @@ function runMultiNodeTransition(transitions, options = {}) {
4698
4804
  }
4699
4805
  onUpdate?.();
4700
4806
  if (!allCompleted) {
4701
- if (typeof requestAnimationFrame !== "undefined") {
4702
- requestAnimationFrame(frame);
4703
- }
4807
+ clock.frame().then(frame);
4704
4808
  } else {
4705
4809
  onComplete?.();
4706
4810
  }
@@ -4978,10 +5082,76 @@ function repositionNodesTo(nodes, targetOrigin) {
4978
5082
  }));
4979
5083
  }
4980
5084
 
5085
+ // src/canvas/view.ts
5086
+ function clampHeight(contentHeight, options = {}) {
5087
+ const { min = 320, max = 560, padding = 48 } = options;
5088
+ return Math.round(Math.min(max, Math.max(min, contentHeight + padding)));
5089
+ }
5090
+ function layoutBounds(nodes) {
5091
+ if (nodes.length === 0) return null;
5092
+ let minX = Infinity;
5093
+ let minY = Infinity;
5094
+ let maxX = -Infinity;
5095
+ let maxY = -Infinity;
5096
+ for (const node of nodes) {
5097
+ const x = node.position.x;
5098
+ const y = node.position.y;
5099
+ const width = node.width ?? 180;
5100
+ const height = node.height ?? 80;
5101
+ minX = Math.min(minX, x);
5102
+ minY = Math.min(minY, y);
5103
+ maxX = Math.max(maxX, x + width);
5104
+ maxY = Math.max(maxY, y + height);
5105
+ }
5106
+ if (minX === Infinity) return null;
5107
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
5108
+ }
5109
+ function canvasHeight(nodes, options) {
5110
+ return clampHeight(layoutBounds(nodes)?.height ?? 0, options);
5111
+ }
5112
+ var DEFAULT_RANK_HEIGHT = 80 + 72;
5113
+ var NODES_PER_RANK = 2;
5114
+ function estimateCanvasHeight(nodes, edges, options) {
5115
+ const ranks = Math.max(1, Math.ceil(nodes.length / NODES_PER_RANK));
5116
+ return clampHeight(ranks * DEFAULT_RANK_HEIGHT, options);
5117
+ }
5118
+ function mix(a, b, t) {
5119
+ return a + (b - a) * t;
5120
+ }
5121
+ function blendLayout(from, to, t) {
5122
+ if (t >= 1) return to;
5123
+ const fromMap = new Map(from.map((n) => [n.id, n]));
5124
+ return to.map((target) => {
5125
+ const origin = fromMap.get(target.id);
5126
+ if (!origin) return target;
5127
+ return {
5128
+ ...target,
5129
+ position: {
5130
+ x: mix(origin.position.x, target.position.x, t),
5131
+ y: mix(origin.position.y, target.position.y, t)
5132
+ },
5133
+ width: origin.width !== void 0 && target.width !== void 0 ? mix(origin.width, target.width, t) : target.width,
5134
+ height: origin.height !== void 0 && target.height !== void 0 ? mix(origin.height, target.height, t) : target.height
5135
+ };
5136
+ });
5137
+ }
5138
+ function connectedOnly(nodes, edges) {
5139
+ if (edges.length === 0) return nodes;
5140
+ const linkedNodeIds = /* @__PURE__ */ new Set();
5141
+ for (const edge of edges) {
5142
+ linkedNodeIds.add(edge.source);
5143
+ linkedNodeIds.add(edge.target);
5144
+ }
5145
+ const filtered = nodes.filter((node) => linkedNodeIds.has(node.id));
5146
+ return filtered.length > 0 ? filtered : nodes;
5147
+ }
5148
+
4981
5149
  // src/canvas/streaming.ts
4982
5150
  var StreamingLayoutManager = class {
4983
5151
  throttleMs;
4984
5152
  glideDurationMs;
5153
+ connectedOnly;
5154
+ clock;
4985
5155
  layoutOptions;
4986
5156
  onLayoutUpdated;
4987
5157
  pendingTimer = null;
@@ -4990,6 +5160,8 @@ var StreamingLayoutManager = class {
4990
5160
  constructor(options = {}) {
4991
5161
  this.throttleMs = options.throttleMs ?? 200;
4992
5162
  this.glideDurationMs = options.glideDurationMs ?? 300;
5163
+ this.connectedOnly = options.connectedOnly ?? false;
5164
+ this.clock = options.clock ?? realClock;
4993
5165
  this.layoutOptions = options.layoutOptions ?? { direction: "LR" };
4994
5166
  this.onLayoutUpdated = options.onLayoutUpdated;
4995
5167
  }
@@ -5017,7 +5189,8 @@ var StreamingLayoutManager = class {
5017
5189
  this.recalculateAndGlide(nodes, edges, currentPositions);
5018
5190
  }
5019
5191
  recalculateAndGlide(nodes, edges, currentPositions) {
5020
- if (nodes.length === 0) return;
5192
+ const targetNodes = this.connectedOnly ? connectedOnly(nodes, edges) : nodes;
5193
+ if (targetNodes.length === 0) return;
5021
5194
  if (this.activeTransitionCancel) {
5022
5195
  this.activeTransitionCancel();
5023
5196
  this.activeTransitionCancel = null;
@@ -5026,7 +5199,7 @@ var StreamingLayoutManager = class {
5026
5199
  for (const [id, pos] of currentPositions.entries()) {
5027
5200
  stored[id] = pos;
5028
5201
  }
5029
- const layouted = ensureLayout(nodes, edges, {
5202
+ const layouted = ensureLayout(targetNodes, edges, {
5030
5203
  ...this.layoutOptions,
5031
5204
  storedPositions: stored
5032
5205
  });
@@ -5050,6 +5223,7 @@ var StreamingLayoutManager = class {
5050
5223
  this.targetPositions = nextPositions;
5051
5224
  if (transitions.length > 0) {
5052
5225
  this.activeTransitionCancel = runMultiNodeTransition(transitions, {
5226
+ clock: this.clock,
5053
5227
  onUpdate: () => {
5054
5228
  const liveMap = new Map(currentPositions);
5055
5229
  for (const trans of transitions) {
@@ -5257,6 +5431,24 @@ function planAgentCommand(command, snapshot, options = {}) {
5257
5431
  }
5258
5432
  ];
5259
5433
  }
5434
+ case "choreography:say": {
5435
+ return [
5436
+ {
5437
+ type: "step:say",
5438
+ text: command.text,
5439
+ atNodeId: command.atNodeId,
5440
+ position: command.position
5441
+ }
5442
+ ];
5443
+ }
5444
+ case "choreography:wait": {
5445
+ return [
5446
+ {
5447
+ type: "step:wait",
5448
+ ms: command.ms
5449
+ }
5450
+ ];
5451
+ }
5260
5452
  default: {
5261
5453
  return [];
5262
5454
  }
@@ -5381,21 +5573,1342 @@ var MCP_TOOL_EDIT_PROMPT = {
5381
5573
  required: ["operation"]
5382
5574
  }
5383
5575
  };
5576
+ var MCP_TOOL_VIEW_EDITOR = {
5577
+ name: "view_editor",
5578
+ 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.",
5579
+ inputSchema: {
5580
+ type: "object",
5581
+ properties: {
5582
+ scope: {
5583
+ type: "string",
5584
+ enum: ["viewport", "canvas"],
5585
+ description: "Whether to capture only the visible viewport or the full canvas bounding box (default: viewport)."
5586
+ },
5587
+ format: {
5588
+ type: "string",
5589
+ enum: ["jpeg", "png"],
5590
+ description: "Image format of the base64 snapshot (default: jpeg)."
5591
+ },
5592
+ maxEdge: {
5593
+ type: "integer",
5594
+ description: "Maximum width or height dimension in pixels (default: 1024)."
5595
+ }
5596
+ }
5597
+ }
5598
+ };
5599
+ var EXPORT_EXCLUDED_SELECTORS = [
5600
+ ".pixerate-presence",
5601
+ ".agent-presence",
5602
+ ".ursula-presence"
5603
+ ];
5384
5604
  var EDITOR_MCP_TOOLS = [
5385
5605
  MCP_TOOL_GET_EDITOR_STATE,
5386
5606
  MCP_TOOL_EDIT_CANVAS,
5387
- MCP_TOOL_EDIT_PROMPT
5607
+ MCP_TOOL_EDIT_PROMPT,
5608
+ MCP_TOOL_VIEW_EDITOR
5388
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
+ }
5389
6898
  // Annotate the CommonJS export names for ESM import in node:
5390
6899
  0 && (module.exports = {
5391
6900
  ColorHighlighter,
6901
+ DEFAULT_ADJUSTMENTS,
6902
+ DEFAULT_DEPTH_MASK,
5392
6903
  DEFAULT_NODE_HEIGHT,
5393
6904
  DEFAULT_NODE_WIDTH,
5394
6905
  DEFAULT_PLACEMENT_GAP,
6906
+ DEFAULT_TRANSFORM,
5395
6907
  DataSourceBinder,
5396
6908
  DependencyGraph,
5397
6909
  DirtyTracker,
5398
6910
  EDITOR_MCP_TOOLS,
6911
+ EXPORT_EXCLUDED_SELECTORS,
5399
6912
  EditorController,
5400
6913
  FontSize,
5401
6914
  FormulaEvaluator,
@@ -5404,12 +6917,16 @@ var EDITOR_MCP_TOOLS = [
5404
6917
  HistoryManager,
5405
6918
  INSTRUCTION_REGEX,
5406
6919
  Image,
6920
+ ImageEditorController,
6921
+ ImageEditorRenderer,
5407
6922
  LoadingNode,
5408
6923
  MARKDOWN_MENTION_SPLIT_REGEX,
5409
6924
  MCP_TOOL_EDIT_CANVAS,
5410
6925
  MCP_TOOL_EDIT_PROMPT,
5411
6926
  MCP_TOOL_GET_EDITOR_STATE,
6927
+ MCP_TOOL_VIEW_EDITOR,
5412
6928
  MENTION_REGEX,
6929
+ ManualClock,
5413
6930
  Markdown,
5414
6931
  Mention,
5415
6932
  SlashCommands,
@@ -5419,13 +6936,20 @@ var EDITOR_MCP_TOOLS = [
5419
6936
  TEMPLATE_REGEX,
5420
6937
  TemplateSuggestions,
5421
6938
  VARIABLE_REGEX,
6939
+ applyAdjustments,
6940
+ applyDepthMask,
6941
+ blendLayout,
5422
6942
  boundsOf,
6943
+ calculateCropDrag,
5423
6944
  calculateDagreReflowDisplacement,
5424
6945
  calculateDirectionalDisplacement,
6946
+ canvasHeight,
6947
+ canvasToImagePoint,
5425
6948
  centerNodes,
5426
6949
  centre,
5427
6950
  colNameToIndex,
5428
6951
  computeNodesBoundingBox,
6952
+ connectedOnly,
5429
6953
  createBezierTrajectory,
5430
6954
  createDirtyTracker,
5431
6955
  createEditor,
@@ -5437,6 +6961,7 @@ var EDITOR_MCP_TOOLS = [
5437
6961
  defaultIsEqual,
5438
6962
  ensureLayout,
5439
6963
  escapeHtml,
6964
+ estimateCanvasHeight,
5440
6965
  estimateNodeSize,
5441
6966
  extractCellDependencies,
5442
6967
  extractInstructionNames,
@@ -5446,26 +6971,35 @@ var EDITOR_MCP_TOOLS = [
5446
6971
  extractVariableNames,
5447
6972
  findHexColors,
5448
6973
  flowSide,
6974
+ getAnnotationBounds,
5449
6975
  getCenteredNodePosition,
6976
+ getCropCursor,
5450
6977
  getEditorMarkdown,
5451
6978
  getEditorText,
5452
6979
  getLatestVersion,
5453
6980
  getLayoutedNodes,
6981
+ getViewportMetrics,
6982
+ globalImageCache,
5454
6983
  handleImageInsertion,
6984
+ hitTestAnnotation,
6985
+ hitTestCrop,
5455
6986
  htmlToPlainText,
5456
6987
  imagePastePluginKey,
6988
+ imageToCanvasPoint,
5457
6989
  indexToColName,
5458
6990
  insertImageNode,
5459
6991
  instructionRegex,
5460
6992
  isEventFromTextInput,
5461
6993
  isValidCanvasNode,
5462
6994
  isValidUrl,
6995
+ layoutBounds,
5463
6996
  linearPositionTrajectory,
5464
6997
  linearTrajectory,
5465
6998
  mapRawOffsetToResolved,
5466
6999
  mapResolvedOffsetToRaw,
5467
7000
  markdownToTipTapHtml,
5468
7001
  overlaps,
7002
+ pageHidden,
5469
7003
  parseCanvasClipboardData,
5470
7004
  parseCellReference,
5471
7005
  parseFormula,
@@ -5476,7 +7010,9 @@ var EDITOR_MCP_TOOLS = [
5476
7010
  planAgentCommand,
5477
7011
  preprocessMarkdownFileLinks,
5478
7012
  preprocessMarkdownMentions,
7013
+ realClock,
5479
7014
  remapPastedNodes,
7015
+ removeAlpha,
5480
7016
  removeImageNode,
5481
7017
  repositionNodesTo,
5482
7018
  resolveTemplates,