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