@pixerate/editor 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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 +1327 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1310 -5
- 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 || {}
|
|
@@ -5448,11 +5448,1302 @@ var EDITOR_MCP_TOOLS = [
|
|
|
5448
5448
|
MCP_TOOL_EDIT_PROMPT,
|
|
5449
5449
|
MCP_TOOL_VIEW_EDITOR
|
|
5450
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
|
+
}
|
|
5451
6739
|
export {
|
|
5452
6740
|
ColorHighlighter,
|
|
6741
|
+
DEFAULT_ADJUSTMENTS,
|
|
6742
|
+
DEFAULT_DEPTH_MASK,
|
|
5453
6743
|
DEFAULT_NODE_HEIGHT,
|
|
5454
6744
|
DEFAULT_NODE_WIDTH,
|
|
5455
6745
|
DEFAULT_PLACEMENT_GAP,
|
|
6746
|
+
DEFAULT_TRANSFORM,
|
|
5456
6747
|
DataSourceBinder,
|
|
5457
6748
|
DependencyGraph,
|
|
5458
6749
|
DirtyTracker,
|
|
@@ -5465,7 +6756,9 @@ export {
|
|
|
5465
6756
|
GradientText,
|
|
5466
6757
|
HistoryManager,
|
|
5467
6758
|
INSTRUCTION_REGEX,
|
|
5468
|
-
|
|
6759
|
+
Image3 as Image,
|
|
6760
|
+
ImageEditorController,
|
|
6761
|
+
ImageEditorRenderer,
|
|
5469
6762
|
LoadingNode,
|
|
5470
6763
|
MARKDOWN_MENTION_SPLIT_REGEX,
|
|
5471
6764
|
MCP_TOOL_EDIT_CANVAS,
|
|
@@ -5483,11 +6776,15 @@ export {
|
|
|
5483
6776
|
TEMPLATE_REGEX,
|
|
5484
6777
|
TemplateSuggestions,
|
|
5485
6778
|
VARIABLE_REGEX,
|
|
6779
|
+
applyAdjustments,
|
|
6780
|
+
applyDepthMask,
|
|
5486
6781
|
blendLayout,
|
|
5487
6782
|
boundsOf,
|
|
6783
|
+
calculateCropDrag,
|
|
5488
6784
|
calculateDagreReflowDisplacement,
|
|
5489
6785
|
calculateDirectionalDisplacement,
|
|
5490
6786
|
canvasHeight,
|
|
6787
|
+
canvasToImagePoint,
|
|
5491
6788
|
centerNodes,
|
|
5492
6789
|
centre,
|
|
5493
6790
|
colNameToIndex,
|
|
@@ -5514,14 +6811,21 @@ export {
|
|
|
5514
6811
|
extractVariableNames,
|
|
5515
6812
|
findHexColors,
|
|
5516
6813
|
flowSide,
|
|
6814
|
+
getAnnotationBounds,
|
|
5517
6815
|
getCenteredNodePosition,
|
|
6816
|
+
getCropCursor,
|
|
5518
6817
|
getEditorMarkdown,
|
|
5519
6818
|
getEditorText,
|
|
5520
6819
|
getLatestVersion,
|
|
5521
6820
|
getLayoutedNodes,
|
|
6821
|
+
getViewportMetrics,
|
|
6822
|
+
globalImageCache,
|
|
5522
6823
|
handleImageInsertion,
|
|
6824
|
+
hitTestAnnotation,
|
|
6825
|
+
hitTestCrop,
|
|
5523
6826
|
htmlToPlainText,
|
|
5524
6827
|
imagePastePluginKey,
|
|
6828
|
+
imageToCanvasPoint,
|
|
5525
6829
|
indexToColName,
|
|
5526
6830
|
insertImageNode,
|
|
5527
6831
|
instructionRegex,
|
|
@@ -5548,6 +6852,7 @@ export {
|
|
|
5548
6852
|
preprocessMarkdownMentions,
|
|
5549
6853
|
realClock,
|
|
5550
6854
|
remapPastedNodes,
|
|
6855
|
+
removeAlpha,
|
|
5551
6856
|
removeImageNode,
|
|
5552
6857
|
repositionNodesTo,
|
|
5553
6858
|
resolveTemplates,
|