@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/agent/index.cjs +52 -1
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +68 -1
- package/dist/agent/index.d.ts +68 -1
- package/dist/agent/index.js +50 -1
- package/dist/agent/index.js.map +1 -1
- package/dist/canvas/index.cjs +171 -8
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +91 -1
- package/dist/canvas/index.d.ts +91 -1
- package/dist/canvas/index.js +163 -8
- package/dist/canvas/index.js.map +1 -1
- package/dist/image-editor/index.cjs +1350 -0
- package/dist/image-editor/index.cjs.map +1 -0
- package/dist/image-editor/index.d.cts +339 -0
- package/dist/image-editor/index.d.ts +339 -0
- package/dist/image-editor/index.js +1307 -0
- package/dist/image-editor/index.js.map +1 -0
- package/dist/index.cjs +1550 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1523 -14
- package/dist/index.js.map +1 -1
- package/package.json +6 -1
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: () =>
|
|
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
|
|
1439
|
+
var Image2 = import_core8.Node.create({
|
|
1413
1440
|
name: "image"
|
|
1414
1441
|
});
|
|
1415
|
-
var Image$1 =
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|