@merittdev/horus-lens 0.1.1 → 0.2.1
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/browser.cjs +1262 -195
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.cts +2 -2
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +2 -2
- package/dist/{chunk-CRV65EEY.js → chunk-SDMTZ5Y6.js} +1016 -167
- package/dist/chunk-SDMTZ5Y6.js.map +1 -0
- package/dist/{chunk-UB7YYHI4.js → chunk-U6GBOUCE.js} +246 -28
- package/dist/chunk-U6GBOUCE.js.map +1 -0
- package/dist/index.cjs +245 -27
- package/dist/index.cjs.map +1 -1
- package/dist/{index.d-DeLYS02n.d.ts → index.d-BtnbCqUx.d.ts} +26 -1
- package/dist/index.d-Cyvb07sp.d.cts +301 -0
- package/dist/index.d-Cyvb07sp.d.ts +301 -0
- package/dist/{index.d-sNoEEgRg.d.cts → index.d-QzZn4EMM.d.cts} +26 -1
- package/dist/index.d.cts +343 -38
- package/dist/index.d.ts +343 -38
- package/dist/index.js +1 -1
- package/dist/lens.min.js +163 -43
- package/dist/lens.min.js.map +1 -1
- package/dist/react.cjs +1262 -195
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +2 -2
- package/package.json +4 -3
- package/dist/chunk-CRV65EEY.js.map +0 -1
- package/dist/chunk-UB7YYHI4.js.map +0 -1
- package/dist/index.d-Bzgtl7-B.d.cts +0 -161
- package/dist/index.d-Bzgtl7-B.d.ts +0 -161
package/dist/browser.cjs
CHANGED
|
@@ -28892,15 +28892,86 @@ var TargetElementSchema = external_exports.object({
|
|
|
28892
28892
|
boundingRect: external_exports.object({ x: external_exports.number(), y: external_exports.number(), width: external_exports.number(), height: external_exports.number() }).optional(),
|
|
28893
28893
|
reactComponents: external_exports.array(external_exports.string()).optional()
|
|
28894
28894
|
});
|
|
28895
|
-
var
|
|
28896
|
-
|
|
28897
|
-
|
|
28898
|
-
|
|
28899
|
-
|
|
28900
|
-
|
|
28901
|
-
|
|
28902
|
-
|
|
28903
|
-
|
|
28895
|
+
var annotationCommon = {
|
|
28896
|
+
id: external_exports.string().optional(),
|
|
28897
|
+
/** Any CSS color string. */
|
|
28898
|
+
color: external_exports.string().optional(),
|
|
28899
|
+
strokeWidth: external_exports.number().optional()
|
|
28900
|
+
};
|
|
28901
|
+
var AnnotationPointSchema = external_exports.object({ x: external_exports.number(), y: external_exports.number() });
|
|
28902
|
+
var AnnotationSchema = external_exports.discriminatedUnion("kind", [
|
|
28903
|
+
external_exports.object({
|
|
28904
|
+
...annotationCommon,
|
|
28905
|
+
kind: external_exports.literal("rect"),
|
|
28906
|
+
x: external_exports.number(),
|
|
28907
|
+
y: external_exports.number(),
|
|
28908
|
+
width: external_exports.number(),
|
|
28909
|
+
height: external_exports.number(),
|
|
28910
|
+
/**
|
|
28911
|
+
* OPTIONAL, kept for back-compat with 0.1.x clients that emit
|
|
28912
|
+
* viewport-anchored rects. New clients emit document-pixel rects with no
|
|
28913
|
+
* viewport.
|
|
28914
|
+
*/
|
|
28915
|
+
viewport: external_exports.object({ width: external_exports.number(), height: external_exports.number() }).optional()
|
|
28916
|
+
}),
|
|
28917
|
+
external_exports.object({
|
|
28918
|
+
...annotationCommon,
|
|
28919
|
+
kind: external_exports.literal("ellipse"),
|
|
28920
|
+
x: external_exports.number(),
|
|
28921
|
+
y: external_exports.number(),
|
|
28922
|
+
width: external_exports.number(),
|
|
28923
|
+
height: external_exports.number()
|
|
28924
|
+
}),
|
|
28925
|
+
external_exports.object({
|
|
28926
|
+
...annotationCommon,
|
|
28927
|
+
kind: external_exports.literal("blur"),
|
|
28928
|
+
x: external_exports.number(),
|
|
28929
|
+
y: external_exports.number(),
|
|
28930
|
+
width: external_exports.number(),
|
|
28931
|
+
height: external_exports.number()
|
|
28932
|
+
}),
|
|
28933
|
+
external_exports.object({
|
|
28934
|
+
...annotationCommon,
|
|
28935
|
+
kind: external_exports.literal("arrow"),
|
|
28936
|
+
x1: external_exports.number(),
|
|
28937
|
+
y1: external_exports.number(),
|
|
28938
|
+
x2: external_exports.number(),
|
|
28939
|
+
y2: external_exports.number()
|
|
28940
|
+
}),
|
|
28941
|
+
external_exports.object({
|
|
28942
|
+
...annotationCommon,
|
|
28943
|
+
kind: external_exports.literal("line"),
|
|
28944
|
+
x1: external_exports.number(),
|
|
28945
|
+
y1: external_exports.number(),
|
|
28946
|
+
x2: external_exports.number(),
|
|
28947
|
+
y2: external_exports.number()
|
|
28948
|
+
}),
|
|
28949
|
+
external_exports.object({
|
|
28950
|
+
...annotationCommon,
|
|
28951
|
+
kind: external_exports.literal("pencil"),
|
|
28952
|
+
points: external_exports.array(AnnotationPointSchema).min(2)
|
|
28953
|
+
}),
|
|
28954
|
+
external_exports.object({
|
|
28955
|
+
...annotationCommon,
|
|
28956
|
+
kind: external_exports.literal("highlighter"),
|
|
28957
|
+
points: external_exports.array(AnnotationPointSchema).min(2)
|
|
28958
|
+
}),
|
|
28959
|
+
external_exports.object({
|
|
28960
|
+
...annotationCommon,
|
|
28961
|
+
kind: external_exports.literal("text"),
|
|
28962
|
+
x: external_exports.number(),
|
|
28963
|
+
y: external_exports.number(),
|
|
28964
|
+
text: external_exports.string(),
|
|
28965
|
+
fontSize: external_exports.number().optional()
|
|
28966
|
+
}),
|
|
28967
|
+
external_exports.object({
|
|
28968
|
+
...annotationCommon,
|
|
28969
|
+
kind: external_exports.literal("step"),
|
|
28970
|
+
x: external_exports.number(),
|
|
28971
|
+
y: external_exports.number(),
|
|
28972
|
+
n: external_exports.number()
|
|
28973
|
+
})
|
|
28974
|
+
]);
|
|
28904
28975
|
var AssetKindSchema = external_exports.enum(["replay", "console", "network", "dom", "screenshot", "attachment"]);
|
|
28905
28976
|
var AssetRefSchema = external_exports.object({
|
|
28906
28977
|
kind: AssetKindSchema,
|
|
@@ -29003,7 +29074,7 @@ var ApiErrorSchema = external_exports.object({
|
|
|
29003
29074
|
|
|
29004
29075
|
// ../core/dist/index.js
|
|
29005
29076
|
var SDK_NAME = "@merittdev/horus-lens";
|
|
29006
|
-
var SDK_VERSION = "0.
|
|
29077
|
+
var SDK_VERSION = true ? "0.2.1" : "0.0.0-dev";
|
|
29007
29078
|
var DEFAULT_ENDPOINT = "https://api.horus.sh";
|
|
29008
29079
|
var DEFAULT_REPLAY_WINDOW_MS = 15e3;
|
|
29009
29080
|
var DEFAULT_MAX_CONSOLE_ENTRIES = 200;
|
|
@@ -29464,7 +29535,20 @@ var ReplayRecorder = class {
|
|
|
29464
29535
|
console.warn("[lens] rrweb recording unavailable; replay disabled", err);
|
|
29465
29536
|
}
|
|
29466
29537
|
};
|
|
29467
|
-
|
|
29538
|
+
var SVG_RASTERIZE_TIMEOUT_MS = 3e3;
|
|
29539
|
+
function computeCropRect(crop, scale, rasterWidth, rasterHeight) {
|
|
29540
|
+
let sx = Math.round(crop.x * scale);
|
|
29541
|
+
let sy = Math.round(crop.y * scale);
|
|
29542
|
+
let sw = Math.round(crop.width * scale);
|
|
29543
|
+
let sh = Math.round(crop.height * scale);
|
|
29544
|
+
sx = Math.max(0, Math.min(sx, rasterWidth));
|
|
29545
|
+
sy = Math.max(0, Math.min(sy, rasterHeight));
|
|
29546
|
+
sw = Math.min(sw, rasterWidth - sx);
|
|
29547
|
+
sh = Math.min(sh, rasterHeight - sy);
|
|
29548
|
+
if (sw <= 0 || sh <= 0) return null;
|
|
29549
|
+
return { sx, sy, sw, sh };
|
|
29550
|
+
}
|
|
29551
|
+
async function captureScreenshot(excludeHost, crop, composite) {
|
|
29468
29552
|
if (typeof document === "undefined" || typeof window === "undefined") return null;
|
|
29469
29553
|
try {
|
|
29470
29554
|
const viewportPixels = window.innerWidth * window.innerHeight || 1;
|
|
@@ -29474,6 +29558,15 @@ async function captureScreenshot(excludeHost, crop) {
|
|
|
29474
29558
|
scale,
|
|
29475
29559
|
type: "image/jpeg",
|
|
29476
29560
|
quality: 0.82,
|
|
29561
|
+
// JPEG has no alpha — transparent page regions (and any capture box
|
|
29562
|
+
// extending past short content) would otherwise encode as BLACK.
|
|
29563
|
+
backgroundColor: "#ffffff",
|
|
29564
|
+
// When compositing annotations, force the capture box to the editor's
|
|
29565
|
+
// document measurement (scrollHeight-based). modern-screenshot otherwise
|
|
29566
|
+
// sizes from documentElement's bounding box, which on pages SHORTER than
|
|
29567
|
+
// the viewport is the content height only — marks drawn in the lower
|
|
29568
|
+
// viewport band would fall off the raster and the crop would clamp short.
|
|
29569
|
+
...composite ? { width: composite.docWidth, height: composite.docHeight } : {},
|
|
29477
29570
|
// Exclude the overlay from the capture CLONE — never hide the live node.
|
|
29478
29571
|
filter: (node2) => node2 !== excludeHost,
|
|
29479
29572
|
// Per-resource fetch budget: a single slow cross-origin image must not
|
|
@@ -29498,6 +29591,16 @@ async function captureScreenshot(excludeHost, crop) {
|
|
|
29498
29591
|
);
|
|
29499
29592
|
return null;
|
|
29500
29593
|
}
|
|
29594
|
+
if (composite) {
|
|
29595
|
+
const baked = await compositeScreenshot(blob2, composite, scale, crop);
|
|
29596
|
+
if (baked) return baked;
|
|
29597
|
+
if (composite.blurRegions.length > 0) {
|
|
29598
|
+
console.warn(
|
|
29599
|
+
"[lens] screenshot compositing failed with active redactions; dropping screenshot to avoid leaking un-blurred content"
|
|
29600
|
+
);
|
|
29601
|
+
return null;
|
|
29602
|
+
}
|
|
29603
|
+
}
|
|
29501
29604
|
if (crop) {
|
|
29502
29605
|
const cropped = await cropBlob(blob2, crop, scale);
|
|
29503
29606
|
return cropped ?? blob2;
|
|
@@ -29508,35 +29611,146 @@ async function captureScreenshot(excludeHost, crop) {
|
|
|
29508
29611
|
return null;
|
|
29509
29612
|
}
|
|
29510
29613
|
}
|
|
29614
|
+
async function compositeScreenshot(blob2, composite, scale, crop) {
|
|
29615
|
+
try {
|
|
29616
|
+
if (typeof createImageBitmap !== "function" || typeof document === "undefined") return null;
|
|
29617
|
+
const bitmap = await createImageBitmap(blob2);
|
|
29618
|
+
const canvas = document.createElement("canvas");
|
|
29619
|
+
canvas.width = bitmap.width;
|
|
29620
|
+
canvas.height = bitmap.height;
|
|
29621
|
+
const ctx = canvas.getContext("2d");
|
|
29622
|
+
if (!ctx) {
|
|
29623
|
+
bitmap.close?.();
|
|
29624
|
+
return null;
|
|
29625
|
+
}
|
|
29626
|
+
ctx.fillStyle = "#ffffff";
|
|
29627
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
29628
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
29629
|
+
bitmap.close?.();
|
|
29630
|
+
applyBlurRegions(ctx, canvas, composite.blurRegions, scale);
|
|
29631
|
+
await drawSvgOverlay(ctx, composite, scale);
|
|
29632
|
+
if (crop) {
|
|
29633
|
+
const rect = computeCropRect(crop, scale, canvas.width, canvas.height);
|
|
29634
|
+
if (rect) {
|
|
29635
|
+
const out = document.createElement("canvas");
|
|
29636
|
+
out.width = rect.sw;
|
|
29637
|
+
out.height = rect.sh;
|
|
29638
|
+
const octx = out.getContext("2d");
|
|
29639
|
+
if (octx) {
|
|
29640
|
+
octx.drawImage(canvas, rect.sx, rect.sy, rect.sw, rect.sh, 0, 0, rect.sw, rect.sh);
|
|
29641
|
+
return await canvasToJpegBlob(out);
|
|
29642
|
+
}
|
|
29643
|
+
}
|
|
29644
|
+
}
|
|
29645
|
+
return await canvasToJpegBlob(canvas);
|
|
29646
|
+
} catch (err) {
|
|
29647
|
+
console.warn("[lens] screenshot compositing failed:", err);
|
|
29648
|
+
return null;
|
|
29649
|
+
}
|
|
29650
|
+
}
|
|
29651
|
+
function applyBlurRegions(ctx, canvas, regions, scale) {
|
|
29652
|
+
if (regions.length === 0) return;
|
|
29653
|
+
const filterSupported = typeof ctx.filter === "string";
|
|
29654
|
+
const blurPx = Math.max(8, 12 * scale);
|
|
29655
|
+
const pad = Math.ceil(blurPx);
|
|
29656
|
+
for (const region of regions) {
|
|
29657
|
+
const x = Math.max(0, Math.min(Math.round(region.x * scale), canvas.width));
|
|
29658
|
+
const y = Math.max(0, Math.min(Math.round(region.y * scale), canvas.height));
|
|
29659
|
+
const w = Math.min(Math.round(region.width * scale), canvas.width - x);
|
|
29660
|
+
const h = Math.min(Math.round(region.height * scale), canvas.height - y);
|
|
29661
|
+
if (w <= 0 || h <= 0) continue;
|
|
29662
|
+
if (!filterSupported) {
|
|
29663
|
+
ctx.fillStyle = "rgba(127,127,127,1)";
|
|
29664
|
+
ctx.fillRect(x, y, w, h);
|
|
29665
|
+
continue;
|
|
29666
|
+
}
|
|
29667
|
+
const px = Math.max(0, x - pad);
|
|
29668
|
+
const py = Math.max(0, y - pad);
|
|
29669
|
+
const pw = Math.min(canvas.width, x + w + pad) - px;
|
|
29670
|
+
const ph = Math.min(canvas.height, y + h + pad) - py;
|
|
29671
|
+
if (pw <= 0 || ph <= 0) continue;
|
|
29672
|
+
const temp = document.createElement("canvas");
|
|
29673
|
+
temp.width = pw;
|
|
29674
|
+
temp.height = ph;
|
|
29675
|
+
const tctx = temp.getContext("2d");
|
|
29676
|
+
if (!tctx) {
|
|
29677
|
+
ctx.fillStyle = "rgba(127,127,127,1)";
|
|
29678
|
+
ctx.fillRect(x, y, w, h);
|
|
29679
|
+
continue;
|
|
29680
|
+
}
|
|
29681
|
+
tctx.filter = `blur(${blurPx}px)`;
|
|
29682
|
+
tctx.drawImage(canvas, px, py, pw, ph, 0, 0, pw, ph);
|
|
29683
|
+
tctx.filter = "none";
|
|
29684
|
+
ctx.save();
|
|
29685
|
+
ctx.beginPath();
|
|
29686
|
+
ctx.rect(x, y, w, h);
|
|
29687
|
+
ctx.clip();
|
|
29688
|
+
ctx.drawImage(temp, px, py);
|
|
29689
|
+
ctx.restore();
|
|
29690
|
+
}
|
|
29691
|
+
}
|
|
29692
|
+
async function drawSvgOverlay(ctx, composite, scale) {
|
|
29693
|
+
const { svgMarkup, docWidth, docHeight } = composite;
|
|
29694
|
+
if (!svgMarkup) return;
|
|
29695
|
+
const img = await loadSvgImage(svgMarkup);
|
|
29696
|
+
if (!img) return;
|
|
29697
|
+
try {
|
|
29698
|
+
ctx.drawImage(img, 0, 0, docWidth * scale, docHeight * scale);
|
|
29699
|
+
} catch (err) {
|
|
29700
|
+
console.warn("[lens] annotation overlay draw failed:", err);
|
|
29701
|
+
}
|
|
29702
|
+
}
|
|
29703
|
+
function loadSvgImage(svg2) {
|
|
29704
|
+
return new Promise((resolve) => {
|
|
29705
|
+
if (typeof Image !== "function") {
|
|
29706
|
+
resolve(null);
|
|
29707
|
+
return;
|
|
29708
|
+
}
|
|
29709
|
+
let settled = false;
|
|
29710
|
+
const finish = (result2) => {
|
|
29711
|
+
if (settled) return;
|
|
29712
|
+
settled = true;
|
|
29713
|
+
clearTimeout(timer);
|
|
29714
|
+
resolve(result2);
|
|
29715
|
+
};
|
|
29716
|
+
const timer = setTimeout(() => {
|
|
29717
|
+
console.warn("[lens] annotation overlay rasterization timed out");
|
|
29718
|
+
finish(null);
|
|
29719
|
+
}, SVG_RASTERIZE_TIMEOUT_MS);
|
|
29720
|
+
const img = new Image();
|
|
29721
|
+
img.onload = () => finish(img);
|
|
29722
|
+
img.onerror = () => {
|
|
29723
|
+
console.warn("[lens] annotation overlay failed to rasterize");
|
|
29724
|
+
finish(null);
|
|
29725
|
+
};
|
|
29726
|
+
img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg2);
|
|
29727
|
+
});
|
|
29728
|
+
}
|
|
29729
|
+
function canvasToJpegBlob(canvas) {
|
|
29730
|
+
return new Promise((resolve) => {
|
|
29731
|
+
canvas.toBlob((out) => resolve(out), "image/jpeg", 0.82);
|
|
29732
|
+
});
|
|
29733
|
+
}
|
|
29511
29734
|
async function cropBlob(blob2, crop, scale) {
|
|
29512
29735
|
try {
|
|
29513
29736
|
if (typeof createImageBitmap !== "function" || typeof document === "undefined") return null;
|
|
29514
29737
|
const bitmap = await createImageBitmap(blob2);
|
|
29515
|
-
|
|
29516
|
-
|
|
29517
|
-
let sw = Math.round(crop.width * scale);
|
|
29518
|
-
let sh = Math.round(crop.height * scale);
|
|
29519
|
-
sx = Math.max(0, Math.min(sx, bitmap.width));
|
|
29520
|
-
sy = Math.max(0, Math.min(sy, bitmap.height));
|
|
29521
|
-
sw = Math.min(sw, bitmap.width - sx);
|
|
29522
|
-
sh = Math.min(sh, bitmap.height - sy);
|
|
29523
|
-
if (sw <= 0 || sh <= 0) {
|
|
29738
|
+
const rect = computeCropRect(crop, scale, bitmap.width, bitmap.height);
|
|
29739
|
+
if (!rect) {
|
|
29524
29740
|
bitmap.close?.();
|
|
29525
29741
|
return null;
|
|
29526
29742
|
}
|
|
29527
29743
|
const canvas = document.createElement("canvas");
|
|
29528
|
-
canvas.width = sw;
|
|
29529
|
-
canvas.height = sh;
|
|
29744
|
+
canvas.width = rect.sw;
|
|
29745
|
+
canvas.height = rect.sh;
|
|
29530
29746
|
const ctx = canvas.getContext("2d");
|
|
29531
29747
|
if (!ctx) {
|
|
29532
29748
|
bitmap.close?.();
|
|
29533
29749
|
return null;
|
|
29534
29750
|
}
|
|
29535
|
-
ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, sw, sh);
|
|
29751
|
+
ctx.drawImage(bitmap, rect.sx, rect.sy, rect.sw, rect.sh, 0, 0, rect.sw, rect.sh);
|
|
29536
29752
|
bitmap.close?.();
|
|
29537
|
-
return await
|
|
29538
|
-
canvas.toBlob((out) => resolve(out), "image/jpeg", 0.82);
|
|
29539
|
-
});
|
|
29753
|
+
return await canvasToJpegBlob(canvas);
|
|
29540
29754
|
} catch (err) {
|
|
29541
29755
|
console.warn("[lens] screenshot crop failed:", err);
|
|
29542
29756
|
return null;
|
|
@@ -29806,7 +30020,11 @@ var LensClient = class {
|
|
|
29806
30020
|
const wantErrors = include.errors !== false;
|
|
29807
30021
|
const wantScreenshot = include.screenshot !== false;
|
|
29808
30022
|
const wantDom = include.dom !== false;
|
|
29809
|
-
const screenshotPromise = this.screenshotEnabled && wantScreenshot ? captureScreenshot(
|
|
30023
|
+
const screenshotPromise = this.screenshotEnabled && wantScreenshot ? captureScreenshot(
|
|
30024
|
+
input2.excludeFromScreenshot,
|
|
30025
|
+
input2.screenshotCrop,
|
|
30026
|
+
input2.annotationComposite
|
|
30027
|
+
) : Promise.resolve(null);
|
|
29810
30028
|
const replayEvents = wantReplay ? this.replay?.snapshot() ?? [] : [];
|
|
29811
30029
|
const errorEntries = this.errorBuffer.drain();
|
|
29812
30030
|
const consoleTail = this.consoleBuffer.tail(CONSOLE_TAIL_SIZE);
|
|
@@ -30017,177 +30235,933 @@ function el(tag, props = {}, children = []) {
|
|
|
30017
30235
|
function clear(node2) {
|
|
30018
30236
|
while (node2.firstChild) node2.removeChild(node2.firstChild);
|
|
30019
30237
|
}
|
|
30020
|
-
var
|
|
30238
|
+
var THIN_MIN_DIST = 3;
|
|
30239
|
+
var MAX_POINTS = 2e3;
|
|
30240
|
+
var HIGHLIGHTER_WIDTH = 14;
|
|
30241
|
+
var STEP_RADIUS = 12;
|
|
30242
|
+
function distance(a, b) {
|
|
30243
|
+
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
30244
|
+
}
|
|
30245
|
+
function shouldKeepPoint(last, candidate, minDist = THIN_MIN_DIST) {
|
|
30246
|
+
return distance(last, candidate) >= minDist;
|
|
30247
|
+
}
|
|
30248
|
+
function perpendicularDistance(p, a, b) {
|
|
30249
|
+
const dx = b.x - a.x;
|
|
30250
|
+
const dy = b.y - a.y;
|
|
30251
|
+
const len2 = dx * dx + dy * dy;
|
|
30252
|
+
if (len2 === 0) return distance(p, a);
|
|
30253
|
+
let t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2;
|
|
30254
|
+
t = Math.max(0, Math.min(1, t));
|
|
30255
|
+
return distance(p, { x: a.x + t * dx, y: a.y + t * dy });
|
|
30256
|
+
}
|
|
30257
|
+
function simplifyPoints(points, tolerance = 1) {
|
|
30258
|
+
if (points.length <= 2 || tolerance <= 0) return points.map((p) => ({ ...p }));
|
|
30259
|
+
const first = points[0];
|
|
30260
|
+
const last = points[points.length - 1];
|
|
30261
|
+
let maxDist = 0;
|
|
30262
|
+
let index2 = 0;
|
|
30263
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
30264
|
+
const d = perpendicularDistance(points[i], first, last);
|
|
30265
|
+
if (d > maxDist) {
|
|
30266
|
+
maxDist = d;
|
|
30267
|
+
index2 = i;
|
|
30268
|
+
}
|
|
30269
|
+
}
|
|
30270
|
+
if (maxDist > tolerance) {
|
|
30271
|
+
const left = simplifyPoints(points.slice(0, index2 + 1), tolerance);
|
|
30272
|
+
const right = simplifyPoints(points.slice(index2), tolerance);
|
|
30273
|
+
return left.slice(0, -1).concat(right);
|
|
30274
|
+
}
|
|
30275
|
+
return [{ ...first }, { ...last }];
|
|
30276
|
+
}
|
|
30277
|
+
function arrowheadPolygon(x1, y1, x2, y2, size = 14) {
|
|
30278
|
+
const angle = Math.atan2(y2 - y1, x2 - x1);
|
|
30279
|
+
const spread = Math.PI / 7;
|
|
30280
|
+
return [
|
|
30281
|
+
{ x: x2, y: y2 },
|
|
30282
|
+
{ x: x2 - size * Math.cos(angle - spread), y: y2 - size * Math.sin(angle - spread) },
|
|
30283
|
+
{ x: x2 - size * Math.cos(angle + spread), y: y2 - size * Math.sin(angle + spread) }
|
|
30284
|
+
];
|
|
30285
|
+
}
|
|
30286
|
+
function boundsOfAnnotation(a) {
|
|
30287
|
+
switch (a.kind) {
|
|
30288
|
+
case "rect":
|
|
30289
|
+
case "ellipse":
|
|
30290
|
+
case "blur": {
|
|
30291
|
+
const x = Math.min(a.x, a.x + a.width);
|
|
30292
|
+
const y = Math.min(a.y, a.y + a.height);
|
|
30293
|
+
return { x, y, width: Math.abs(a.width), height: Math.abs(a.height) };
|
|
30294
|
+
}
|
|
30295
|
+
case "arrow":
|
|
30296
|
+
case "line": {
|
|
30297
|
+
const x = Math.min(a.x1, a.x2);
|
|
30298
|
+
const y = Math.min(a.y1, a.y2);
|
|
30299
|
+
return { x, y, width: Math.abs(a.x2 - a.x1), height: Math.abs(a.y2 - a.y1) };
|
|
30300
|
+
}
|
|
30301
|
+
case "pencil":
|
|
30302
|
+
case "highlighter": {
|
|
30303
|
+
if (a.points.length === 0) return null;
|
|
30304
|
+
let minX = Infinity;
|
|
30305
|
+
let minY = Infinity;
|
|
30306
|
+
let maxX = -Infinity;
|
|
30307
|
+
let maxY = -Infinity;
|
|
30308
|
+
for (const p of a.points) {
|
|
30309
|
+
minX = Math.min(minX, p.x);
|
|
30310
|
+
minY = Math.min(minY, p.y);
|
|
30311
|
+
maxX = Math.max(maxX, p.x);
|
|
30312
|
+
maxY = Math.max(maxY, p.y);
|
|
30313
|
+
}
|
|
30314
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
30315
|
+
}
|
|
30316
|
+
case "text": {
|
|
30317
|
+
const fontSize = a.fontSize ?? 16;
|
|
30318
|
+
const width = Math.max(1, a.text.length * fontSize * 0.6);
|
|
30319
|
+
return { x: a.x, y: a.y, width, height: fontSize * 1.2 };
|
|
30320
|
+
}
|
|
30321
|
+
case "step": {
|
|
30322
|
+
return {
|
|
30323
|
+
x: a.x - STEP_RADIUS,
|
|
30324
|
+
y: a.y - STEP_RADIUS,
|
|
30325
|
+
width: STEP_RADIUS * 2,
|
|
30326
|
+
height: STEP_RADIUS * 2
|
|
30327
|
+
};
|
|
30328
|
+
}
|
|
30329
|
+
}
|
|
30330
|
+
}
|
|
30331
|
+
function unionBounds(list) {
|
|
30332
|
+
if (list.length === 0) return null;
|
|
30333
|
+
let minX = Infinity;
|
|
30334
|
+
let minY = Infinity;
|
|
30335
|
+
let maxX = -Infinity;
|
|
30336
|
+
let maxY = -Infinity;
|
|
30337
|
+
for (const b of list) {
|
|
30338
|
+
minX = Math.min(minX, b.x);
|
|
30339
|
+
minY = Math.min(minY, b.y);
|
|
30340
|
+
maxX = Math.max(maxX, b.x + b.width);
|
|
30341
|
+
maxY = Math.max(maxY, b.y + b.height);
|
|
30342
|
+
}
|
|
30343
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
30344
|
+
}
|
|
30345
|
+
function padAndClampBounds(b, padding, maxWidth, maxHeight) {
|
|
30346
|
+
const x = Math.max(0, b.x - padding);
|
|
30347
|
+
const y = Math.max(0, b.y - padding);
|
|
30348
|
+
const right = Math.min(maxWidth, b.x + b.width + padding);
|
|
30349
|
+
const bottom = Math.min(maxHeight, b.y + b.height + padding);
|
|
30350
|
+
return { x, y, width: Math.max(0, right - x), height: Math.max(0, bottom - y) };
|
|
30351
|
+
}
|
|
30352
|
+
var svg = (inner) => `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${inner}</svg>`;
|
|
30353
|
+
var iconEye = svg(
|
|
30354
|
+
'<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>'
|
|
30355
|
+
);
|
|
30356
|
+
var iconCrosshair = svg(
|
|
30357
|
+
'<circle cx="12" cy="12" r="8"/><line x1="12" y1="2" x2="12" y2="6"/><line x1="12" y1="18" x2="12" y2="22"/><line x1="2" y1="12" x2="6" y2="12"/><line x1="18" y1="12" x2="22" y2="12"/>'
|
|
30358
|
+
);
|
|
30359
|
+
var iconClose = svg('<line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/>');
|
|
30360
|
+
var iconMark = svg(
|
|
30361
|
+
'<rect x="3" y="3" width="18" height="18" rx="2" stroke-dasharray="4 3"/>'
|
|
30362
|
+
);
|
|
30363
|
+
var iconCopy = svg(
|
|
30364
|
+
'<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/>'
|
|
30365
|
+
);
|
|
30366
|
+
var iconCheck = svg('<polyline points="20 6 9 17 4 12"/>');
|
|
30367
|
+
var iconSend = svg('<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>');
|
|
30368
|
+
var iconRecord = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="12" cy="12" r="6" fill="#ef4444"/></svg>`;
|
|
30369
|
+
var iconStop = `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>`;
|
|
30370
|
+
var iconRectTool = svg('<rect x="4" y="6" width="16" height="12" rx="1"/>');
|
|
30371
|
+
var iconEllipseTool = svg('<ellipse cx="12" cy="12" rx="8" ry="6"/>');
|
|
30372
|
+
var iconArrowTool = svg('<line x1="5" y1="19" x2="18" y2="6"/><polyline points="18 12 18 6 12 6"/>');
|
|
30373
|
+
var iconLineTool = svg('<line x1="5" y1="19" x2="19" y2="5"/>');
|
|
30374
|
+
var iconPencil = svg('<path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/>');
|
|
30375
|
+
var iconHighlighter = svg(
|
|
30376
|
+
'<path d="m9 11-6 6v3h9l3-3"/><path d="m22 12-4.6 4.6a1 1 0 0 1-1.4 0l-5.6-5.6a1 1 0 0 1 0-1.4L14 4"/>'
|
|
30377
|
+
);
|
|
30378
|
+
var iconText = svg(
|
|
30379
|
+
'<polyline points="4 7 4 4 20 4 20 7"/><line x1="12" y1="4" x2="12" y2="20"/><line x1="9" y1="20" x2="15" y2="20"/>'
|
|
30380
|
+
);
|
|
30381
|
+
var iconStepTool = svg('<circle cx="12" cy="12" r="9"/><path d="M11.2 9 13 8v8"/>');
|
|
30382
|
+
var iconBlur = svg('<path d="M12 3s6 6.4 6 10a6 6 0 0 1-12 0c0-3.6 6-10 6-10Z"/>');
|
|
30383
|
+
var iconUndo = svg('<path d="M9 14 4 9l5-5"/><path d="M4 9h11a5 5 0 0 1 0 10h-1"/>');
|
|
30384
|
+
var iconTrash = svg(
|
|
30385
|
+
'<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>'
|
|
30386
|
+
);
|
|
30387
|
+
var iconAnnotate = iconPencil;
|
|
30388
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
30389
|
+
var FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
|
|
30390
|
+
var MIN_DRAG = 6;
|
|
30391
|
+
var TOOLS = [
|
|
30392
|
+
{ tool: "rect", icon: iconRectTool, label: "Rectangle" },
|
|
30393
|
+
{ tool: "ellipse", icon: iconEllipseTool, label: "Ellipse" },
|
|
30394
|
+
{ tool: "arrow", icon: iconArrowTool, label: "Arrow" },
|
|
30395
|
+
{ tool: "line", icon: iconLineTool, label: "Line" },
|
|
30396
|
+
{ tool: "pencil", icon: iconPencil, label: "Pencil" },
|
|
30397
|
+
{ tool: "highlighter", icon: iconHighlighter, label: "Highlighter" },
|
|
30398
|
+
{ tool: "text", icon: iconText, label: "Text" },
|
|
30399
|
+
{ tool: "step", icon: iconStepTool, label: "Step number" },
|
|
30400
|
+
{ tool: "blur", icon: iconBlur, label: "Blur / redact" }
|
|
30401
|
+
];
|
|
30402
|
+
var COLORS = ["#ef4444", "#f59e0b", "#22c55e", "#3b82f6", "#8b5cf6", "#111827"];
|
|
30403
|
+
var WIDTHS = [
|
|
30404
|
+
{ label: "S", value: 2 },
|
|
30405
|
+
{ label: "M", value: 3.5 },
|
|
30406
|
+
{ label: "L", value: 6 }
|
|
30407
|
+
];
|
|
30408
|
+
var AnnotationEditor = class {
|
|
30021
30409
|
constructor(host2) {
|
|
30022
30410
|
this.host = host2;
|
|
30023
30411
|
}
|
|
30024
30412
|
host;
|
|
30025
|
-
|
|
30026
|
-
|
|
30027
|
-
|
|
30028
|
-
|
|
30029
|
-
|
|
30030
|
-
|
|
30031
|
-
dragging = false;
|
|
30413
|
+
svg = null;
|
|
30414
|
+
bg = null;
|
|
30415
|
+
toolbar = null;
|
|
30416
|
+
textInputEl = null;
|
|
30417
|
+
textAnchor = null;
|
|
30418
|
+
textColor = COLORS[0];
|
|
30032
30419
|
annotations = [];
|
|
30033
|
-
|
|
30420
|
+
markNodes = [];
|
|
30421
|
+
drag = null;
|
|
30422
|
+
active = false;
|
|
30034
30423
|
sessionBase = 0;
|
|
30424
|
+
stepCounter = 0;
|
|
30425
|
+
idSeq = 0;
|
|
30035
30426
|
onExit = null;
|
|
30427
|
+
tool = "rect";
|
|
30428
|
+
color = COLORS[0];
|
|
30429
|
+
width = 3.5;
|
|
30430
|
+
docW = 0;
|
|
30431
|
+
docH = 0;
|
|
30432
|
+
resizeObserver = null;
|
|
30433
|
+
toolBtns = /* @__PURE__ */ new Map();
|
|
30434
|
+
colorBtns = /* @__PURE__ */ new Map();
|
|
30435
|
+
widthBtns = /* @__PURE__ */ new Map();
|
|
30436
|
+
undoBtn = null;
|
|
30437
|
+
clearBtn = null;
|
|
30036
30438
|
get isActive() {
|
|
30037
30439
|
return this.active;
|
|
30038
30440
|
}
|
|
30039
30441
|
get count() {
|
|
30040
30442
|
return this.annotations.length;
|
|
30041
30443
|
}
|
|
30444
|
+
/** Deep-copied document-coord annotations per the schema contract. */
|
|
30042
30445
|
list() {
|
|
30043
|
-
return this.annotations.map((a) =>
|
|
30044
|
-
|
|
30045
|
-
|
|
30046
|
-
|
|
30047
|
-
|
|
30048
|
-
|
|
30049
|
-
|
|
30050
|
-
|
|
30446
|
+
return this.annotations.map((a) => {
|
|
30447
|
+
const copy2 = { ...a };
|
|
30448
|
+
if ("points" in copy2 && Array.isArray(copy2.points)) {
|
|
30449
|
+
copy2.points = copy2.points.map((p) => ({ ...p }));
|
|
30450
|
+
}
|
|
30451
|
+
if (copy2.kind === "rect" && copy2.viewport) copy2.viewport = { ...copy2.viewport };
|
|
30452
|
+
return copy2;
|
|
30453
|
+
});
|
|
30051
30454
|
}
|
|
30455
|
+
/** Remove every mark. Tears the layer down when not actively drawing. */
|
|
30052
30456
|
clear() {
|
|
30457
|
+
for (const node2 of this.markNodes) node2.remove();
|
|
30458
|
+
this.markNodes = [];
|
|
30053
30459
|
this.annotations = [];
|
|
30054
|
-
|
|
30055
|
-
this.
|
|
30460
|
+
this.stepCounter = 0;
|
|
30461
|
+
if (!this.active) this.teardownLayer();
|
|
30462
|
+
this.syncActionButtons();
|
|
30056
30463
|
}
|
|
30057
30464
|
start(onExit) {
|
|
30058
30465
|
if (this.active) return;
|
|
30466
|
+
this.ensureLayer();
|
|
30467
|
+
const svg2 = this.svg;
|
|
30059
30468
|
this.active = true;
|
|
30060
30469
|
this.onExit = onExit;
|
|
30061
30470
|
this.sessionBase = this.annotations.length;
|
|
30062
|
-
this.
|
|
30063
|
-
this.
|
|
30064
|
-
this.
|
|
30065
|
-
|
|
30471
|
+
this.stepCounter = 0;
|
|
30472
|
+
this.sizeLayer();
|
|
30473
|
+
this.updateTransform();
|
|
30474
|
+
svg2.style.pointerEvents = "auto";
|
|
30475
|
+
svg2.style.touchAction = "none";
|
|
30476
|
+
svg2.style.cursor = "crosshair";
|
|
30477
|
+
svg2.addEventListener("pointerdown", this.onPointerDown);
|
|
30478
|
+
this.buildToolbar();
|
|
30479
|
+
window.addEventListener("keydown", this.onKey, true);
|
|
30480
|
+
}
|
|
30481
|
+
/** Done — keep this session's marks, exit draw mode. */
|
|
30482
|
+
exit() {
|
|
30483
|
+
if (!this.active) return;
|
|
30484
|
+
this.active = false;
|
|
30485
|
+
this.commitTextInput();
|
|
30486
|
+
if (this.drag) {
|
|
30487
|
+
this.drag.node.remove();
|
|
30488
|
+
this.detachDragListeners();
|
|
30489
|
+
this.drag = null;
|
|
30490
|
+
}
|
|
30491
|
+
const svg2 = this.svg;
|
|
30492
|
+
if (svg2) {
|
|
30493
|
+
svg2.removeEventListener("pointerdown", this.onPointerDown);
|
|
30494
|
+
svg2.style.pointerEvents = "none";
|
|
30495
|
+
svg2.style.touchAction = "";
|
|
30496
|
+
svg2.style.cursor = "";
|
|
30497
|
+
}
|
|
30498
|
+
window.removeEventListener("keydown", this.onKey, true);
|
|
30499
|
+
this.removeToolbar();
|
|
30500
|
+
const cb = this.onExit;
|
|
30501
|
+
this.onExit = null;
|
|
30502
|
+
cb?.();
|
|
30503
|
+
}
|
|
30504
|
+
/** Cancel — discard marks drawn during this session, then exit. */
|
|
30505
|
+
cancel() {
|
|
30506
|
+
this.cancelTextInput();
|
|
30507
|
+
const removed = this.markNodes.splice(this.sessionBase);
|
|
30508
|
+
for (const node2 of removed) node2.remove();
|
|
30509
|
+
this.annotations.splice(this.sessionBase);
|
|
30510
|
+
this.exit();
|
|
30511
|
+
}
|
|
30512
|
+
undo() {
|
|
30513
|
+
const node2 = this.markNodes.pop();
|
|
30514
|
+
if (!node2) return;
|
|
30515
|
+
node2.remove();
|
|
30516
|
+
this.annotations.pop();
|
|
30517
|
+
this.syncActionButtons();
|
|
30518
|
+
}
|
|
30519
|
+
// --------------------------------------------------------------------------
|
|
30520
|
+
// Composite / crop
|
|
30521
|
+
// --------------------------------------------------------------------------
|
|
30522
|
+
/**
|
|
30523
|
+
* Complete standalone SVG of every NON-blur mark, plus blur regions returned
|
|
30524
|
+
* separately. `null` when there are no marks.
|
|
30525
|
+
*/
|
|
30526
|
+
getComposite() {
|
|
30527
|
+
if (this.annotations.length === 0 || !this.svg) return null;
|
|
30528
|
+
this.sizeLayer();
|
|
30529
|
+
const docWidth = this.docW;
|
|
30530
|
+
const docHeight = this.docH;
|
|
30531
|
+
const clone2 = this.svg.cloneNode(true);
|
|
30532
|
+
clone2.removeAttribute("style");
|
|
30533
|
+
clone2.removeAttribute("class");
|
|
30534
|
+
clone2.querySelectorAll("[data-lens-chrome],[data-lens-draft],[data-lens-blur]").forEach((n2) => n2.remove());
|
|
30535
|
+
clone2.querySelector("defs")?.remove();
|
|
30536
|
+
clone2.setAttribute("xmlns", SVG_NS);
|
|
30537
|
+
clone2.setAttribute("viewBox", `0 0 ${docWidth} ${docHeight}`);
|
|
30538
|
+
clone2.setAttribute("width", String(docWidth));
|
|
30539
|
+
clone2.setAttribute("height", String(docHeight));
|
|
30540
|
+
const svgMarkup = new XMLSerializer().serializeToString(clone2);
|
|
30541
|
+
const blurRegions = this.annotations.filter((a) => a.kind === "blur").map((a) => ({ x: a.x, y: a.y, width: a.width, height: a.height }));
|
|
30542
|
+
return { svgMarkup, docWidth, docHeight, blurRegions };
|
|
30543
|
+
}
|
|
30544
|
+
/** Union bbox of all marks + 24px padding, clamped to the document. */
|
|
30545
|
+
getCropBounds() {
|
|
30546
|
+
if (this.annotations.length === 0) return void 0;
|
|
30547
|
+
const boxes = [];
|
|
30548
|
+
for (let i = 0; i < this.annotations.length; i++) {
|
|
30549
|
+
const a = this.annotations[i];
|
|
30550
|
+
const b = a.kind === "text" ? this.textBounds(i) : boundsOfAnnotation(a);
|
|
30551
|
+
if (b) boxes.push(b);
|
|
30552
|
+
}
|
|
30553
|
+
const u = unionBounds(boxes);
|
|
30554
|
+
if (!u) return void 0;
|
|
30555
|
+
this.sizeLayer();
|
|
30556
|
+
return padAndClampBounds(u, 24, this.docW, this.docH);
|
|
30557
|
+
}
|
|
30558
|
+
/** Prefer a live getBBox() for text; fall back to the geometric estimate. */
|
|
30559
|
+
textBounds(index2) {
|
|
30560
|
+
const node2 = this.markNodes[index2];
|
|
30561
|
+
const annotation = this.annotations[index2];
|
|
30562
|
+
if (node2 && typeof node2.getBBox === "function") {
|
|
30563
|
+
try {
|
|
30564
|
+
const box = node2.getBBox();
|
|
30565
|
+
if (box.width > 0 && box.height > 0) {
|
|
30566
|
+
return { x: box.x, y: box.y, width: box.width, height: box.height };
|
|
30567
|
+
}
|
|
30568
|
+
} catch {
|
|
30569
|
+
}
|
|
30570
|
+
}
|
|
30571
|
+
return boundsOfAnnotation(annotation);
|
|
30572
|
+
}
|
|
30573
|
+
// --------------------------------------------------------------------------
|
|
30574
|
+
// Layer lifecycle
|
|
30575
|
+
// --------------------------------------------------------------------------
|
|
30576
|
+
ensureLayer() {
|
|
30577
|
+
if (this.svg) return;
|
|
30578
|
+
const svg2 = document.createElementNS(SVG_NS, "svg");
|
|
30579
|
+
svg2.setAttribute("class", "lens-annotate-svg");
|
|
30580
|
+
svg2.setAttribute("xmlns", SVG_NS);
|
|
30581
|
+
const defs = document.createElementNS(SVG_NS, "defs");
|
|
30582
|
+
defs.setAttribute("data-lens-chrome", "1");
|
|
30583
|
+
const pattern = document.createElementNS(SVG_NS, "pattern");
|
|
30584
|
+
pattern.setAttribute("id", "lens-blur-hatch");
|
|
30585
|
+
pattern.setAttribute("width", "8");
|
|
30586
|
+
pattern.setAttribute("height", "8");
|
|
30587
|
+
pattern.setAttribute("patternUnits", "userSpaceOnUse");
|
|
30588
|
+
pattern.setAttribute("patternTransform", "rotate(45)");
|
|
30589
|
+
const patternBase = document.createElementNS(SVG_NS, "rect");
|
|
30590
|
+
patternBase.setAttribute("width", "8");
|
|
30591
|
+
patternBase.setAttribute("height", "8");
|
|
30592
|
+
patternBase.setAttribute("fill", "rgba(17,24,39,0.12)");
|
|
30593
|
+
const patternLine = document.createElementNS(SVG_NS, "line");
|
|
30594
|
+
patternLine.setAttribute("x1", "0");
|
|
30595
|
+
patternLine.setAttribute("y1", "0");
|
|
30596
|
+
patternLine.setAttribute("x2", "0");
|
|
30597
|
+
patternLine.setAttribute("y2", "8");
|
|
30598
|
+
patternLine.setAttribute("stroke", "rgba(17,24,39,0.55)");
|
|
30599
|
+
patternLine.setAttribute("stroke-width", "4");
|
|
30600
|
+
pattern.append(patternBase, patternLine);
|
|
30601
|
+
defs.append(pattern);
|
|
30602
|
+
svg2.append(defs);
|
|
30603
|
+
const bg = document.createElementNS(SVG_NS, "rect");
|
|
30604
|
+
bg.setAttribute("data-lens-chrome", "1");
|
|
30605
|
+
bg.setAttribute("x", "0");
|
|
30606
|
+
bg.setAttribute("y", "0");
|
|
30607
|
+
bg.setAttribute("fill", "transparent");
|
|
30608
|
+
bg.style.pointerEvents = "all";
|
|
30609
|
+
svg2.append(bg);
|
|
30610
|
+
this.svg = svg2;
|
|
30611
|
+
this.bg = bg;
|
|
30612
|
+
this.host.append(svg2);
|
|
30613
|
+
window.addEventListener("scroll", this.onScroll, { passive: true, capture: true });
|
|
30614
|
+
window.addEventListener("resize", this.onResize);
|
|
30615
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
30616
|
+
this.resizeObserver = new ResizeObserver(this.onResize);
|
|
30617
|
+
this.resizeObserver.observe(document.documentElement);
|
|
30618
|
+
}
|
|
30619
|
+
this.sizeLayer();
|
|
30620
|
+
this.updateTransform();
|
|
30621
|
+
}
|
|
30622
|
+
teardownLayer() {
|
|
30623
|
+
window.removeEventListener("scroll", this.onScroll, true);
|
|
30624
|
+
window.removeEventListener("resize", this.onResize);
|
|
30625
|
+
this.resizeObserver?.disconnect();
|
|
30626
|
+
this.resizeObserver = null;
|
|
30627
|
+
this.svg?.remove();
|
|
30628
|
+
this.svg = null;
|
|
30629
|
+
this.bg = null;
|
|
30630
|
+
}
|
|
30631
|
+
docSize() {
|
|
30632
|
+
const d = document.documentElement;
|
|
30633
|
+
const b = document.body;
|
|
30634
|
+
const width = Math.max(d.scrollWidth, b?.scrollWidth ?? 0, d.clientWidth);
|
|
30635
|
+
const height = Math.max(d.scrollHeight, b?.scrollHeight ?? 0, d.clientHeight);
|
|
30636
|
+
return { width, height };
|
|
30637
|
+
}
|
|
30638
|
+
sizeLayer() {
|
|
30639
|
+
if (!this.svg) return;
|
|
30640
|
+
const { width, height } = this.docSize();
|
|
30641
|
+
this.docW = width;
|
|
30642
|
+
this.docH = height;
|
|
30643
|
+
this.svg.setAttribute("width", String(width));
|
|
30644
|
+
this.svg.setAttribute("height", String(height));
|
|
30645
|
+
this.svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
|
30646
|
+
this.svg.style.width = `${width}px`;
|
|
30647
|
+
this.svg.style.height = `${height}px`;
|
|
30648
|
+
if (this.bg) {
|
|
30649
|
+
this.bg.setAttribute("width", String(width));
|
|
30650
|
+
this.bg.setAttribute("height", String(height));
|
|
30651
|
+
}
|
|
30652
|
+
}
|
|
30653
|
+
updateTransform() {
|
|
30654
|
+
if (!this.svg) return;
|
|
30655
|
+
const scrollX = window.scrollX || window.pageXOffset || 0;
|
|
30656
|
+
const scrollY = window.scrollY || window.pageYOffset || 0;
|
|
30657
|
+
this.svg.style.left = `${-scrollX}px`;
|
|
30658
|
+
this.svg.style.top = `${-scrollY}px`;
|
|
30659
|
+
}
|
|
30660
|
+
onScroll = () => this.updateTransform();
|
|
30661
|
+
onResize = () => {
|
|
30662
|
+
this.sizeLayer();
|
|
30663
|
+
this.updateTransform();
|
|
30664
|
+
};
|
|
30665
|
+
// --------------------------------------------------------------------------
|
|
30666
|
+
// Toolbar
|
|
30667
|
+
// --------------------------------------------------------------------------
|
|
30668
|
+
buildToolbar() {
|
|
30669
|
+
this.toolBtns.clear();
|
|
30670
|
+
this.colorBtns.clear();
|
|
30671
|
+
this.widthBtns.clear();
|
|
30672
|
+
const toolGroup = el("div", { class: "lens-tool-group" });
|
|
30673
|
+
for (const { tool, icon, label } of TOOLS) {
|
|
30674
|
+
const btn = el("button", {
|
|
30675
|
+
class: "lens-tool-btn",
|
|
30676
|
+
ariaLabel: label,
|
|
30677
|
+
title: label,
|
|
30678
|
+
html: icon,
|
|
30679
|
+
attrs: { type: "button", "aria-pressed": String(tool === this.tool) },
|
|
30680
|
+
on: { click: () => this.selectTool(tool) }
|
|
30681
|
+
});
|
|
30682
|
+
this.toolBtns.set(tool, btn);
|
|
30683
|
+
toolGroup.append(btn);
|
|
30684
|
+
}
|
|
30685
|
+
const colorGroup = el("div", { class: "lens-tool-group lens-swatches" });
|
|
30686
|
+
for (const c of COLORS) {
|
|
30687
|
+
const btn = el("button", {
|
|
30688
|
+
class: "lens-swatch",
|
|
30689
|
+
ariaLabel: `Color ${c}`,
|
|
30690
|
+
title: c,
|
|
30691
|
+
attrs: { type: "button", "aria-pressed": String(c === this.color) },
|
|
30692
|
+
style: { background: c },
|
|
30693
|
+
on: { click: () => this.selectColor(c) }
|
|
30694
|
+
});
|
|
30695
|
+
this.colorBtns.set(c, btn);
|
|
30696
|
+
colorGroup.append(btn);
|
|
30697
|
+
}
|
|
30698
|
+
const widthGroup = el("div", { class: "lens-tool-group" });
|
|
30699
|
+
for (const { label, value } of WIDTHS) {
|
|
30700
|
+
const btn = el("button", {
|
|
30701
|
+
class: "lens-width-btn",
|
|
30702
|
+
text: label,
|
|
30703
|
+
ariaLabel: `Stroke width ${label}`,
|
|
30704
|
+
title: `Stroke ${label} (${value}px)`,
|
|
30705
|
+
attrs: { type: "button", "aria-pressed": String(value === this.width) },
|
|
30706
|
+
on: { click: () => this.selectWidth(value) }
|
|
30707
|
+
});
|
|
30708
|
+
this.widthBtns.set(value, btn);
|
|
30709
|
+
widthGroup.append(btn);
|
|
30710
|
+
}
|
|
30711
|
+
this.undoBtn = el("button", {
|
|
30712
|
+
class: "lens-tool-btn",
|
|
30713
|
+
ariaLabel: "Undo",
|
|
30714
|
+
title: "Undo",
|
|
30715
|
+
html: iconUndo,
|
|
30716
|
+
attrs: { type: "button" },
|
|
30717
|
+
on: { click: () => this.undo() }
|
|
30718
|
+
});
|
|
30719
|
+
this.clearBtn = el("button", {
|
|
30720
|
+
class: "lens-tool-btn",
|
|
30721
|
+
ariaLabel: "Clear all",
|
|
30722
|
+
title: "Clear all",
|
|
30723
|
+
html: iconTrash,
|
|
30724
|
+
attrs: { type: "button" },
|
|
30725
|
+
on: { click: () => this.clear() }
|
|
30726
|
+
});
|
|
30727
|
+
const editGroup = el("div", { class: "lens-tool-group" }, [this.undoBtn, this.clearBtn]);
|
|
30728
|
+
const doneGroup = el("div", { class: "lens-tool-group" }, [
|
|
30066
30729
|
el("button", {
|
|
30067
30730
|
class: "lens-pick-chip__btn",
|
|
30068
30731
|
text: "Cancel",
|
|
30732
|
+
attrs: { type: "button" },
|
|
30069
30733
|
on: { click: () => this.cancel() }
|
|
30070
30734
|
}),
|
|
30071
30735
|
el("button", {
|
|
30072
30736
|
class: "lens-pick-chip__btn lens-pick-chip__btn--primary",
|
|
30073
30737
|
text: "Done",
|
|
30738
|
+
attrs: { type: "button" },
|
|
30074
30739
|
on: { click: () => this.exit() }
|
|
30075
30740
|
})
|
|
30076
30741
|
]);
|
|
30077
|
-
this.
|
|
30078
|
-
|
|
30079
|
-
|
|
30080
|
-
|
|
30081
|
-
|
|
30082
|
-
|
|
30083
|
-
|
|
30084
|
-
|
|
30085
|
-
|
|
30742
|
+
this.toolbar = el(
|
|
30743
|
+
"div",
|
|
30744
|
+
{ class: "lens-annotate-toolbar", attrs: { role: "toolbar", "aria-label": "Annotation tools" } },
|
|
30745
|
+
[
|
|
30746
|
+
toolGroup,
|
|
30747
|
+
el("span", { class: "lens-tool-sep" }),
|
|
30748
|
+
colorGroup,
|
|
30749
|
+
el("span", { class: "lens-tool-sep" }),
|
|
30750
|
+
widthGroup,
|
|
30751
|
+
el("span", { class: "lens-tool-sep" }),
|
|
30752
|
+
editGroup,
|
|
30753
|
+
el("span", { class: "lens-tool-sep" }),
|
|
30754
|
+
doneGroup
|
|
30755
|
+
]
|
|
30756
|
+
);
|
|
30757
|
+
this.host.append(this.toolbar);
|
|
30758
|
+
this.syncActionButtons();
|
|
30759
|
+
}
|
|
30760
|
+
removeToolbar() {
|
|
30761
|
+
this.toolbar?.remove();
|
|
30762
|
+
this.toolbar = null;
|
|
30763
|
+
this.toolBtns.clear();
|
|
30764
|
+
this.colorBtns.clear();
|
|
30765
|
+
this.widthBtns.clear();
|
|
30766
|
+
this.undoBtn = null;
|
|
30767
|
+
this.clearBtn = null;
|
|
30768
|
+
}
|
|
30769
|
+
selectTool(tool) {
|
|
30770
|
+
this.commitTextInput();
|
|
30771
|
+
this.tool = tool;
|
|
30772
|
+
for (const [t, btn] of this.toolBtns) {
|
|
30773
|
+
const on2 = t === tool;
|
|
30774
|
+
btn.classList.toggle("is-active", on2);
|
|
30775
|
+
btn.setAttribute("aria-pressed", String(on2));
|
|
30776
|
+
}
|
|
30777
|
+
if (this.svg) this.svg.style.cursor = tool === "text" ? "text" : "crosshair";
|
|
30778
|
+
}
|
|
30779
|
+
selectColor(color) {
|
|
30780
|
+
this.color = color;
|
|
30781
|
+
for (const [c, btn] of this.colorBtns) {
|
|
30782
|
+
const on2 = c === color;
|
|
30783
|
+
btn.classList.toggle("is-active", on2);
|
|
30784
|
+
btn.setAttribute("aria-pressed", String(on2));
|
|
30785
|
+
}
|
|
30786
|
+
}
|
|
30787
|
+
selectWidth(value) {
|
|
30788
|
+
this.width = value;
|
|
30789
|
+
for (const [v, btn] of this.widthBtns) {
|
|
30790
|
+
const on2 = v === value;
|
|
30791
|
+
btn.classList.toggle("is-active", on2);
|
|
30792
|
+
btn.setAttribute("aria-pressed", String(on2));
|
|
30793
|
+
}
|
|
30794
|
+
}
|
|
30795
|
+
syncActionButtons() {
|
|
30796
|
+
const empty = this.markNodes.length === 0;
|
|
30797
|
+
if (this.undoBtn) this.undoBtn.disabled = empty;
|
|
30798
|
+
if (this.clearBtn) this.clearBtn.disabled = empty;
|
|
30799
|
+
}
|
|
30800
|
+
// --------------------------------------------------------------------------
|
|
30801
|
+
// Pointer / drawing
|
|
30802
|
+
// --------------------------------------------------------------------------
|
|
30803
|
+
toDoc(e) {
|
|
30804
|
+
return {
|
|
30805
|
+
x: e.clientX + (window.scrollX || window.pageXOffset || 0),
|
|
30806
|
+
y: e.clientY + (window.scrollY || window.pageYOffset || 0)
|
|
30807
|
+
};
|
|
30086
30808
|
}
|
|
30087
|
-
|
|
30809
|
+
onPointerDown = (e) => {
|
|
30088
30810
|
if (!this.active) return;
|
|
30089
|
-
|
|
30090
|
-
|
|
30091
|
-
|
|
30092
|
-
|
|
30093
|
-
|
|
30094
|
-
|
|
30095
|
-
|
|
30096
|
-
this.draft = null;
|
|
30811
|
+
if (e.button !== void 0 && e.button !== 0) return;
|
|
30812
|
+
e.preventDefault();
|
|
30813
|
+
const p = this.toDoc(e);
|
|
30814
|
+
const tool = this.tool;
|
|
30815
|
+
if (tool === "text") {
|
|
30816
|
+
this.openTextInput(e.clientX, e.clientY, p);
|
|
30817
|
+
return;
|
|
30097
30818
|
}
|
|
30098
|
-
if (
|
|
30099
|
-
this.
|
|
30100
|
-
|
|
30101
|
-
this.layer = null;
|
|
30819
|
+
if (tool === "step") {
|
|
30820
|
+
this.placeStep(p);
|
|
30821
|
+
return;
|
|
30102
30822
|
}
|
|
30103
|
-
|
|
30104
|
-
|
|
30105
|
-
|
|
30823
|
+
const color = this.color;
|
|
30824
|
+
const width = this.width;
|
|
30825
|
+
let drag;
|
|
30826
|
+
if (tool === "pencil" || tool === "highlighter") {
|
|
30827
|
+
const poly = this.svgEl("polyline", {
|
|
30828
|
+
"data-lens-draft": "1",
|
|
30829
|
+
fill: "none",
|
|
30830
|
+
stroke: color,
|
|
30831
|
+
"stroke-width": tool === "highlighter" ? HIGHLIGHTER_WIDTH : width,
|
|
30832
|
+
"stroke-linecap": "round",
|
|
30833
|
+
"stroke-linejoin": "round",
|
|
30834
|
+
...tool === "highlighter" ? { "stroke-opacity": "0.35" } : {},
|
|
30835
|
+
points: `${p.x},${p.y}`
|
|
30836
|
+
});
|
|
30837
|
+
this.svg.append(poly);
|
|
30838
|
+
drag = { tool, color, width, start: p, node: poly, points: [p], last: p };
|
|
30839
|
+
} else if (tool === "arrow") {
|
|
30840
|
+
const g = this.svgEl("g", { "data-lens-draft": "1" });
|
|
30841
|
+
const line = this.svgEl("line", {
|
|
30842
|
+
stroke: color,
|
|
30843
|
+
"stroke-width": width,
|
|
30844
|
+
"stroke-linecap": "round"
|
|
30845
|
+
});
|
|
30846
|
+
const head = this.svgEl("polygon", { fill: color });
|
|
30847
|
+
g.append(line, head);
|
|
30848
|
+
this.svg.append(g);
|
|
30849
|
+
drag = { tool, color, width, start: p, node: g, line, head };
|
|
30850
|
+
this.updateArrow(drag, p);
|
|
30851
|
+
} else {
|
|
30852
|
+
const node2 = this.createShapeNode(tool, color, width);
|
|
30853
|
+
this.svg.append(node2);
|
|
30854
|
+
drag = { tool, color, width, start: p, node: node2 };
|
|
30855
|
+
this.updateShape(drag, p);
|
|
30856
|
+
}
|
|
30857
|
+
this.drag = drag;
|
|
30858
|
+
window.addEventListener("pointermove", this.onPointerMove, true);
|
|
30859
|
+
window.addEventListener("pointerup", this.onPointerUp, true);
|
|
30860
|
+
};
|
|
30861
|
+
onPointerMove = (e) => {
|
|
30862
|
+
const d = this.drag;
|
|
30863
|
+
if (!d) return;
|
|
30864
|
+
const p = this.toDoc(e);
|
|
30865
|
+
if (d.tool === "pencil" || d.tool === "highlighter") {
|
|
30866
|
+
const pts = d.points;
|
|
30867
|
+
if (d.last && shouldKeepPoint(d.last, p) && pts.length < MAX_POINTS) {
|
|
30868
|
+
pts.push(p);
|
|
30869
|
+
d.last = p;
|
|
30870
|
+
d.node.setAttribute("points", this.pointsAttr(pts));
|
|
30871
|
+
}
|
|
30872
|
+
} else if (d.tool === "arrow") {
|
|
30873
|
+
this.updateArrow(d, p);
|
|
30874
|
+
} else {
|
|
30875
|
+
this.updateShape(d, p);
|
|
30876
|
+
}
|
|
30877
|
+
};
|
|
30878
|
+
onPointerUp = (e) => {
|
|
30879
|
+
const d = this.drag;
|
|
30880
|
+
this.detachDragListeners();
|
|
30881
|
+
this.drag = null;
|
|
30882
|
+
if (!d) return;
|
|
30883
|
+
const p = this.toDoc(e);
|
|
30884
|
+
let annotation = null;
|
|
30885
|
+
if (d.tool === "pencil" || d.tool === "highlighter") {
|
|
30886
|
+
let pts = d.points;
|
|
30887
|
+
if (pts.length >= 2) pts = simplifyPoints(pts, 1);
|
|
30888
|
+
if (pts.length < 2) {
|
|
30889
|
+
d.node.remove();
|
|
30890
|
+
return;
|
|
30891
|
+
}
|
|
30892
|
+
d.node.setAttribute("points", this.pointsAttr(pts));
|
|
30893
|
+
annotation = d.tool === "pencil" ? { kind: "pencil", points: pts, color: d.color, strokeWidth: d.width, id: this.nextId() } : {
|
|
30894
|
+
kind: "highlighter",
|
|
30895
|
+
points: pts,
|
|
30896
|
+
color: d.color,
|
|
30897
|
+
strokeWidth: HIGHLIGHTER_WIDTH,
|
|
30898
|
+
id: this.nextId()
|
|
30899
|
+
};
|
|
30900
|
+
} else if (d.tool === "arrow" || d.tool === "line") {
|
|
30901
|
+
if (distance(d.start, p) < MIN_DRAG) {
|
|
30902
|
+
d.node.remove();
|
|
30903
|
+
return;
|
|
30904
|
+
}
|
|
30905
|
+
if (d.tool === "arrow") {
|
|
30906
|
+
this.updateArrow(d, p);
|
|
30907
|
+
annotation = {
|
|
30908
|
+
kind: "arrow",
|
|
30909
|
+
x1: d.start.x,
|
|
30910
|
+
y1: d.start.y,
|
|
30911
|
+
x2: p.x,
|
|
30912
|
+
y2: p.y,
|
|
30913
|
+
color: d.color,
|
|
30914
|
+
strokeWidth: d.width,
|
|
30915
|
+
id: this.nextId()
|
|
30916
|
+
};
|
|
30917
|
+
} else {
|
|
30918
|
+
this.updateShape(d, p);
|
|
30919
|
+
annotation = {
|
|
30920
|
+
kind: "line",
|
|
30921
|
+
x1: d.start.x,
|
|
30922
|
+
y1: d.start.y,
|
|
30923
|
+
x2: p.x,
|
|
30924
|
+
y2: p.y,
|
|
30925
|
+
color: d.color,
|
|
30926
|
+
strokeWidth: d.width,
|
|
30927
|
+
id: this.nextId()
|
|
30928
|
+
};
|
|
30929
|
+
}
|
|
30930
|
+
} else {
|
|
30931
|
+
const x = Math.min(d.start.x, p.x);
|
|
30932
|
+
const y = Math.min(d.start.y, p.y);
|
|
30933
|
+
const width = Math.abs(p.x - d.start.x);
|
|
30934
|
+
const height = Math.abs(p.y - d.start.y);
|
|
30935
|
+
if (width < MIN_DRAG || height < MIN_DRAG) {
|
|
30936
|
+
d.node.remove();
|
|
30937
|
+
return;
|
|
30938
|
+
}
|
|
30939
|
+
this.updateShape(d, p);
|
|
30940
|
+
if (d.tool === "rect") {
|
|
30941
|
+
annotation = {
|
|
30942
|
+
kind: "rect",
|
|
30943
|
+
x,
|
|
30944
|
+
y,
|
|
30945
|
+
width,
|
|
30946
|
+
height,
|
|
30947
|
+
color: d.color,
|
|
30948
|
+
strokeWidth: d.width,
|
|
30949
|
+
id: this.nextId()
|
|
30950
|
+
};
|
|
30951
|
+
} else if (d.tool === "ellipse") {
|
|
30952
|
+
annotation = {
|
|
30953
|
+
kind: "ellipse",
|
|
30954
|
+
x,
|
|
30955
|
+
y,
|
|
30956
|
+
width,
|
|
30957
|
+
height,
|
|
30958
|
+
color: d.color,
|
|
30959
|
+
strokeWidth: d.width,
|
|
30960
|
+
id: this.nextId()
|
|
30961
|
+
};
|
|
30962
|
+
} else {
|
|
30963
|
+
annotation = { kind: "blur", x, y, width, height, id: this.nextId() };
|
|
30964
|
+
}
|
|
30106
30965
|
}
|
|
30107
|
-
|
|
30108
|
-
this.
|
|
30109
|
-
exit?.();
|
|
30110
|
-
}
|
|
30111
|
-
onDown = (e) => {
|
|
30112
|
-
e.preventDefault();
|
|
30113
|
-
this.dragging = true;
|
|
30114
|
-
this.startX = e.clientX;
|
|
30115
|
-
this.startY = e.clientY;
|
|
30116
|
-
this.draft = el("div", { class: "lens-rect" });
|
|
30117
|
-
this.host.append(this.draft);
|
|
30118
|
-
this.position(this.draft, e.clientX, e.clientY);
|
|
30119
|
-
window.addEventListener("pointermove", this.onMove, true);
|
|
30120
|
-
window.addEventListener("pointerup", this.onUp, true);
|
|
30966
|
+
d.node.removeAttribute("data-lens-draft");
|
|
30967
|
+
this.commitMark(annotation, d.node);
|
|
30121
30968
|
};
|
|
30122
|
-
|
|
30123
|
-
|
|
30124
|
-
|
|
30125
|
-
|
|
30126
|
-
|
|
30127
|
-
|
|
30128
|
-
this.
|
|
30129
|
-
|
|
30130
|
-
|
|
30131
|
-
|
|
30132
|
-
|
|
30133
|
-
|
|
30134
|
-
|
|
30135
|
-
|
|
30136
|
-
|
|
30137
|
-
|
|
30138
|
-
|
|
30139
|
-
|
|
30140
|
-
|
|
30141
|
-
|
|
30142
|
-
|
|
30143
|
-
|
|
30969
|
+
detachDragListeners() {
|
|
30970
|
+
window.removeEventListener("pointermove", this.onPointerMove, true);
|
|
30971
|
+
window.removeEventListener("pointerup", this.onPointerUp, true);
|
|
30972
|
+
}
|
|
30973
|
+
commitMark(annotation, node2) {
|
|
30974
|
+
this.annotations.push(annotation);
|
|
30975
|
+
this.markNodes.push(node2);
|
|
30976
|
+
this.syncActionButtons();
|
|
30977
|
+
}
|
|
30978
|
+
createShapeNode(tool, color, width) {
|
|
30979
|
+
if (tool === "ellipse") {
|
|
30980
|
+
return this.svgEl("ellipse", {
|
|
30981
|
+
"data-lens-draft": "1",
|
|
30982
|
+
fill: "none",
|
|
30983
|
+
stroke: color,
|
|
30984
|
+
"stroke-width": width
|
|
30985
|
+
});
|
|
30986
|
+
}
|
|
30987
|
+
if (tool === "line") {
|
|
30988
|
+
return this.svgEl("line", {
|
|
30989
|
+
"data-lens-draft": "1",
|
|
30990
|
+
stroke: color,
|
|
30991
|
+
"stroke-width": width,
|
|
30992
|
+
"stroke-linecap": "round"
|
|
30993
|
+
});
|
|
30994
|
+
}
|
|
30995
|
+
if (tool === "blur") {
|
|
30996
|
+
return this.svgEl("rect", {
|
|
30997
|
+
"data-lens-draft": "1",
|
|
30998
|
+
"data-lens-blur": "1",
|
|
30999
|
+
fill: "url(#lens-blur-hatch)",
|
|
31000
|
+
stroke: "#111827",
|
|
31001
|
+
"stroke-width": 1,
|
|
31002
|
+
"stroke-dasharray": "4 3"
|
|
31003
|
+
});
|
|
31004
|
+
}
|
|
31005
|
+
return this.svgEl("rect", {
|
|
31006
|
+
"data-lens-draft": "1",
|
|
31007
|
+
fill: "none",
|
|
31008
|
+
stroke: color,
|
|
31009
|
+
"stroke-width": width,
|
|
31010
|
+
rx: 2
|
|
30144
31011
|
});
|
|
30145
|
-
|
|
30146
|
-
|
|
30147
|
-
|
|
30148
|
-
|
|
30149
|
-
|
|
30150
|
-
|
|
30151
|
-
|
|
30152
|
-
|
|
30153
|
-
|
|
31012
|
+
}
|
|
31013
|
+
updateShape(d, cur) {
|
|
31014
|
+
const x = Math.min(d.start.x, cur.x);
|
|
31015
|
+
const y = Math.min(d.start.y, cur.y);
|
|
31016
|
+
const w = Math.abs(cur.x - d.start.x);
|
|
31017
|
+
const h = Math.abs(cur.y - d.start.y);
|
|
31018
|
+
const node2 = d.node;
|
|
31019
|
+
if (d.tool === "ellipse") {
|
|
31020
|
+
node2.setAttribute("cx", String(x + w / 2));
|
|
31021
|
+
node2.setAttribute("cy", String(y + h / 2));
|
|
31022
|
+
node2.setAttribute("rx", String(w / 2));
|
|
31023
|
+
node2.setAttribute("ry", String(h / 2));
|
|
31024
|
+
} else if (d.tool === "line") {
|
|
31025
|
+
node2.setAttribute("x1", String(d.start.x));
|
|
31026
|
+
node2.setAttribute("y1", String(d.start.y));
|
|
31027
|
+
node2.setAttribute("x2", String(cur.x));
|
|
31028
|
+
node2.setAttribute("y2", String(cur.y));
|
|
31029
|
+
} else {
|
|
31030
|
+
node2.setAttribute("x", String(x));
|
|
31031
|
+
node2.setAttribute("y", String(y));
|
|
31032
|
+
node2.setAttribute("width", String(w));
|
|
31033
|
+
node2.setAttribute("height", String(h));
|
|
31034
|
+
}
|
|
31035
|
+
}
|
|
31036
|
+
updateArrow(d, cur) {
|
|
31037
|
+
if (!d.line || !d.head) return;
|
|
31038
|
+
d.line.setAttribute("x1", String(d.start.x));
|
|
31039
|
+
d.line.setAttribute("y1", String(d.start.y));
|
|
31040
|
+
d.line.setAttribute("x2", String(cur.x));
|
|
31041
|
+
d.line.setAttribute("y2", String(cur.y));
|
|
31042
|
+
const size = Math.max(12, d.width * 3);
|
|
31043
|
+
const poly = arrowheadPolygon(d.start.x, d.start.y, cur.x, cur.y, size);
|
|
31044
|
+
d.head.setAttribute("points", this.pointsAttr(poly));
|
|
31045
|
+
}
|
|
31046
|
+
// --------------------------------------------------------------------------
|
|
31047
|
+
// Text tool
|
|
31048
|
+
// --------------------------------------------------------------------------
|
|
31049
|
+
openTextInput(clientX, clientY, docPoint) {
|
|
31050
|
+
this.commitTextInput();
|
|
31051
|
+
const input2 = el("input", {
|
|
31052
|
+
class: "lens-text-input",
|
|
31053
|
+
attrs: { type: "text", "aria-label": "Annotation text" },
|
|
31054
|
+
style: { left: `${clientX}px`, top: `${clientY}px`, color: this.color }
|
|
30154
31055
|
});
|
|
30155
|
-
|
|
30156
|
-
|
|
30157
|
-
|
|
30158
|
-
|
|
30159
|
-
|
|
30160
|
-
|
|
30161
|
-
|
|
30162
|
-
|
|
30163
|
-
|
|
31056
|
+
input2.addEventListener("blur", () => this.commitTextInput());
|
|
31057
|
+
this.host.append(input2);
|
|
31058
|
+
this.textInputEl = input2;
|
|
31059
|
+
this.textAnchor = docPoint;
|
|
31060
|
+
this.textColor = this.color;
|
|
31061
|
+
input2.focus();
|
|
31062
|
+
}
|
|
31063
|
+
commitTextInput() {
|
|
31064
|
+
const input2 = this.textInputEl;
|
|
31065
|
+
const anchor = this.textAnchor;
|
|
31066
|
+
if (!input2 || !anchor) return;
|
|
31067
|
+
const value = input2.value.trim();
|
|
31068
|
+
this.textInputEl = null;
|
|
31069
|
+
this.textAnchor = null;
|
|
31070
|
+
input2.remove();
|
|
31071
|
+
if (!value) return;
|
|
31072
|
+
const node2 = this.svgEl("text", {
|
|
31073
|
+
x: anchor.x,
|
|
31074
|
+
y: anchor.y,
|
|
31075
|
+
fill: this.textColor,
|
|
31076
|
+
"font-size": 16,
|
|
31077
|
+
"font-family": FONT,
|
|
31078
|
+
"dominant-baseline": "hanging"
|
|
30164
31079
|
});
|
|
31080
|
+
node2.textContent = value;
|
|
31081
|
+
this.svg.append(node2);
|
|
31082
|
+
this.commitMark(
|
|
31083
|
+
{
|
|
31084
|
+
kind: "text",
|
|
31085
|
+
x: anchor.x,
|
|
31086
|
+
y: anchor.y,
|
|
31087
|
+
text: value,
|
|
31088
|
+
fontSize: 16,
|
|
31089
|
+
color: this.textColor,
|
|
31090
|
+
id: this.nextId()
|
|
31091
|
+
},
|
|
31092
|
+
node2
|
|
31093
|
+
);
|
|
30165
31094
|
}
|
|
31095
|
+
cancelTextInput() {
|
|
31096
|
+
if (!this.textInputEl) return;
|
|
31097
|
+
this.textInputEl.remove();
|
|
31098
|
+
this.textInputEl = null;
|
|
31099
|
+
this.textAnchor = null;
|
|
31100
|
+
}
|
|
31101
|
+
// --------------------------------------------------------------------------
|
|
31102
|
+
// Step tool
|
|
31103
|
+
// --------------------------------------------------------------------------
|
|
31104
|
+
placeStep(p) {
|
|
31105
|
+
const n2 = ++this.stepCounter;
|
|
31106
|
+
const g = this.svgEl("g", {});
|
|
31107
|
+
const circle = this.svgEl("circle", {
|
|
31108
|
+
cx: p.x,
|
|
31109
|
+
cy: p.y,
|
|
31110
|
+
r: STEP_RADIUS,
|
|
31111
|
+
fill: this.color
|
|
31112
|
+
});
|
|
31113
|
+
const text = this.svgEl("text", {
|
|
31114
|
+
x: p.x,
|
|
31115
|
+
y: p.y,
|
|
31116
|
+
fill: "#ffffff",
|
|
31117
|
+
"font-size": 13,
|
|
31118
|
+
"font-weight": 700,
|
|
31119
|
+
"font-family": FONT,
|
|
31120
|
+
"text-anchor": "middle",
|
|
31121
|
+
"dominant-baseline": "central"
|
|
31122
|
+
});
|
|
31123
|
+
text.textContent = String(n2);
|
|
31124
|
+
g.append(circle, text);
|
|
31125
|
+
this.svg.append(g);
|
|
31126
|
+
this.commitMark({ kind: "step", x: p.x, y: p.y, n: n2, color: this.color, id: this.nextId() }, g);
|
|
31127
|
+
}
|
|
31128
|
+
// --------------------------------------------------------------------------
|
|
31129
|
+
// Keyboard
|
|
31130
|
+
// --------------------------------------------------------------------------
|
|
30166
31131
|
onKey = (e) => {
|
|
30167
|
-
if (
|
|
30168
|
-
|
|
30169
|
-
|
|
30170
|
-
|
|
31132
|
+
if (this.textInputEl) {
|
|
31133
|
+
if (e.key === "Escape") {
|
|
31134
|
+
e.preventDefault();
|
|
31135
|
+
e.stopPropagation();
|
|
31136
|
+
this.cancelTextInput();
|
|
31137
|
+
} else if (e.key === "Enter") {
|
|
31138
|
+
e.preventDefault();
|
|
31139
|
+
e.stopPropagation();
|
|
31140
|
+
this.commitTextInput();
|
|
31141
|
+
}
|
|
31142
|
+
return;
|
|
31143
|
+
}
|
|
31144
|
+
if (e.key === "Escape") {
|
|
31145
|
+
e.preventDefault();
|
|
31146
|
+
e.stopPropagation();
|
|
31147
|
+
this.exit();
|
|
31148
|
+
}
|
|
30171
31149
|
};
|
|
31150
|
+
// --------------------------------------------------------------------------
|
|
31151
|
+
// Small helpers
|
|
31152
|
+
// --------------------------------------------------------------------------
|
|
31153
|
+
svgEl(tag, attrs) {
|
|
31154
|
+
const node2 = document.createElementNS(SVG_NS, tag);
|
|
31155
|
+
for (const [k, v] of Object.entries(attrs)) node2.setAttribute(k, String(v));
|
|
31156
|
+
return node2;
|
|
31157
|
+
}
|
|
31158
|
+
pointsAttr(points) {
|
|
31159
|
+
return points.map((p) => `${p.x},${p.y}`).join(" ");
|
|
31160
|
+
}
|
|
31161
|
+
nextId() {
|
|
31162
|
+
return `lens-${++this.idSeq}`;
|
|
31163
|
+
}
|
|
30172
31164
|
};
|
|
30173
|
-
var svg = (inner) => `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${inner}</svg>`;
|
|
30174
|
-
var iconEye = svg(
|
|
30175
|
-
'<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>'
|
|
30176
|
-
);
|
|
30177
|
-
var iconCrosshair = svg(
|
|
30178
|
-
'<circle cx="12" cy="12" r="8"/><line x1="12" y1="2" x2="12" y2="6"/><line x1="12" y1="18" x2="12" y2="22"/><line x1="2" y1="12" x2="6" y2="12"/><line x1="18" y1="12" x2="22" y2="12"/>'
|
|
30179
|
-
);
|
|
30180
|
-
var iconClose = svg('<line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/>');
|
|
30181
|
-
var iconMark = svg(
|
|
30182
|
-
'<rect x="3" y="3" width="18" height="18" rx="2" stroke-dasharray="4 3"/>'
|
|
30183
|
-
);
|
|
30184
|
-
var iconCopy = svg(
|
|
30185
|
-
'<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/>'
|
|
30186
|
-
);
|
|
30187
|
-
var iconCheck = svg('<polyline points="20 6 9 17 4 12"/>');
|
|
30188
|
-
var iconSend = svg('<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>');
|
|
30189
|
-
var iconRecord = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="12" cy="12" r="6" fill="#ef4444"/></svg>`;
|
|
30190
|
-
var iconStop = `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>`;
|
|
30191
31165
|
var INCLUDE_ITEMS = [
|
|
30192
31166
|
{ key: "replay", label: "Replay" },
|
|
30193
31167
|
{ key: "console", label: "Console" },
|
|
@@ -30344,7 +31318,7 @@ var Panel = class {
|
|
|
30344
31318
|
buildActions() {
|
|
30345
31319
|
const mark = el("button", {
|
|
30346
31320
|
class: "lens-abtn lens-abtn--ghost lens-abtn--compact",
|
|
30347
|
-
html:
|
|
31321
|
+
html: iconAnnotate + "<span>Annotate</span>",
|
|
30348
31322
|
on: { click: () => this.cb.onMarkArea() }
|
|
30349
31323
|
});
|
|
30350
31324
|
const record3 = el("button", {
|
|
@@ -30728,21 +31702,33 @@ var OVERLAY_CSS = (
|
|
|
30728
31702
|
pointer-events: none;
|
|
30729
31703
|
}
|
|
30730
31704
|
|
|
30731
|
-
/* ---------------- Annotation layer ---------------- */
|
|
30732
|
-
|
|
30733
|
-
|
|
30734
|
-
|
|
31705
|
+
/* ---------------- Annotation SVG layer ---------------- */
|
|
31706
|
+
/* One full-DOCUMENT SVG anchored to page content (translated by -scroll so
|
|
31707
|
+
marks survive scrolling). pointer-events flip to auto only while drawing. */
|
|
31708
|
+
.lens-annotate-svg {
|
|
31709
|
+
position: absolute;
|
|
31710
|
+
left: 0;
|
|
31711
|
+
top: 0;
|
|
30735
31712
|
z-index: calc(var(--lens-z) - 1);
|
|
30736
|
-
|
|
30737
|
-
|
|
30738
|
-
touch-action: none;
|
|
31713
|
+
pointer-events: none;
|
|
31714
|
+
overflow: visible;
|
|
30739
31715
|
}
|
|
30740
|
-
|
|
31716
|
+
|
|
31717
|
+
/* Floating text-entry input for the Text tool (viewport-positioned). */
|
|
31718
|
+
.lens-text-input {
|
|
30741
31719
|
position: fixed;
|
|
30742
|
-
|
|
30743
|
-
|
|
30744
|
-
|
|
30745
|
-
|
|
31720
|
+
z-index: var(--lens-z);
|
|
31721
|
+
min-width: 120px;
|
|
31722
|
+
padding: 2px 6px;
|
|
31723
|
+
border: 1px solid var(--lens-accent);
|
|
31724
|
+
border-radius: 4px;
|
|
31725
|
+
background: rgba(255, 255, 255, 0.96);
|
|
31726
|
+
color: #111827;
|
|
31727
|
+
font-family: var(--lens-font);
|
|
31728
|
+
font-size: 15px;
|
|
31729
|
+
line-height: 1.3;
|
|
31730
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
|
|
31731
|
+
outline: none;
|
|
30746
31732
|
}
|
|
30747
31733
|
|
|
30748
31734
|
/* ---------------- Panel ---------------- */
|
|
@@ -31033,6 +32019,114 @@ var OVERLAY_CSS = (
|
|
|
31033
32019
|
}
|
|
31034
32020
|
.lens-annotate-chips { z-index: var(--lens-z); }
|
|
31035
32021
|
|
|
32022
|
+
/* ---------------- Annotation toolbar ---------------- */
|
|
32023
|
+
/* Same floating top-center anchor as the chips row; wraps on narrow screens. */
|
|
32024
|
+
.lens-annotate-toolbar {
|
|
32025
|
+
position: fixed;
|
|
32026
|
+
top: calc(16px + env(safe-area-inset-top));
|
|
32027
|
+
left: 50%;
|
|
32028
|
+
transform: translateX(-50%);
|
|
32029
|
+
z-index: var(--lens-z);
|
|
32030
|
+
display: inline-flex;
|
|
32031
|
+
flex-wrap: wrap;
|
|
32032
|
+
align-items: center;
|
|
32033
|
+
justify-content: center;
|
|
32034
|
+
gap: 6px;
|
|
32035
|
+
max-width: calc(100vw - 24px);
|
|
32036
|
+
padding: 6px 8px;
|
|
32037
|
+
border-radius: 14px;
|
|
32038
|
+
background: rgba(20, 20, 23, 0.92);
|
|
32039
|
+
-webkit-backdrop-filter: blur(12px) saturate(180%);
|
|
32040
|
+
backdrop-filter: blur(12px) saturate(180%);
|
|
32041
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
32042
|
+
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35);
|
|
32043
|
+
color: #f4f4f5;
|
|
32044
|
+
}
|
|
32045
|
+
.lens-tool-group {
|
|
32046
|
+
display: inline-flex;
|
|
32047
|
+
align-items: center;
|
|
32048
|
+
gap: 4px;
|
|
32049
|
+
flex-wrap: wrap;
|
|
32050
|
+
}
|
|
32051
|
+
.lens-tool-sep {
|
|
32052
|
+
width: 1px;
|
|
32053
|
+
align-self: stretch;
|
|
32054
|
+
min-height: 22px;
|
|
32055
|
+
background: rgba(255, 255, 255, 0.16);
|
|
32056
|
+
margin: 0 2px;
|
|
32057
|
+
}
|
|
32058
|
+
.lens-tool-btn {
|
|
32059
|
+
display: inline-flex;
|
|
32060
|
+
align-items: center;
|
|
32061
|
+
justify-content: center;
|
|
32062
|
+
width: 30px;
|
|
32063
|
+
height: 30px;
|
|
32064
|
+
padding: 0;
|
|
32065
|
+
border: 1px solid transparent;
|
|
32066
|
+
border-radius: 8px;
|
|
32067
|
+
background: transparent;
|
|
32068
|
+
color: #d4d4d8;
|
|
32069
|
+
cursor: pointer;
|
|
32070
|
+
transition: background 150ms ease, color 150ms ease, border-color 150ms ease;
|
|
32071
|
+
}
|
|
32072
|
+
.lens-tool-btn:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
|
|
32073
|
+
.lens-tool-btn:focus-visible { outline: 2px solid var(--lens-accent); outline-offset: 1px; }
|
|
32074
|
+
.lens-tool-btn.is-active {
|
|
32075
|
+
background: var(--lens-accent);
|
|
32076
|
+
border-color: var(--lens-accent);
|
|
32077
|
+
color: #fff;
|
|
32078
|
+
}
|
|
32079
|
+
.lens-tool-btn:disabled { opacity: 0.4; cursor: default; }
|
|
32080
|
+
.lens-tool-btn:disabled:hover { background: transparent; color: #d4d4d8; }
|
|
32081
|
+
|
|
32082
|
+
.lens-swatch {
|
|
32083
|
+
width: 22px;
|
|
32084
|
+
height: 22px;
|
|
32085
|
+
padding: 0;
|
|
32086
|
+
border-radius: 999px;
|
|
32087
|
+
border: 2px solid transparent;
|
|
32088
|
+
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.25) inset;
|
|
32089
|
+
cursor: pointer;
|
|
32090
|
+
transition: transform 120ms ease, border-color 120ms ease;
|
|
32091
|
+
}
|
|
32092
|
+
.lens-swatch:hover { transform: scale(1.1); }
|
|
32093
|
+
.lens-swatch:focus-visible { outline: 2px solid #fff; outline-offset: 1px; }
|
|
32094
|
+
.lens-swatch.is-active {
|
|
32095
|
+
border-color: #fff;
|
|
32096
|
+
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.4);
|
|
32097
|
+
}
|
|
32098
|
+
|
|
32099
|
+
.lens-width-btn {
|
|
32100
|
+
display: inline-flex;
|
|
32101
|
+
align-items: center;
|
|
32102
|
+
justify-content: center;
|
|
32103
|
+
min-width: 26px;
|
|
32104
|
+
height: 30px;
|
|
32105
|
+
padding: 0 6px;
|
|
32106
|
+
border: 1px solid transparent;
|
|
32107
|
+
border-radius: 8px;
|
|
32108
|
+
background: transparent;
|
|
32109
|
+
color: #d4d4d8;
|
|
32110
|
+
font-family: var(--lens-font);
|
|
32111
|
+
font-size: 12px;
|
|
32112
|
+
font-weight: 700;
|
|
32113
|
+
cursor: pointer;
|
|
32114
|
+
transition: background 150ms ease, color 150ms ease, border-color 150ms ease;
|
|
32115
|
+
}
|
|
32116
|
+
.lens-width-btn:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
|
|
32117
|
+
.lens-width-btn:focus-visible { outline: 2px solid var(--lens-accent); outline-offset: 1px; }
|
|
32118
|
+
.lens-width-btn.is-active {
|
|
32119
|
+
background: var(--lens-accent);
|
|
32120
|
+
border-color: var(--lens-accent);
|
|
32121
|
+
color: #fff;
|
|
32122
|
+
}
|
|
32123
|
+
|
|
32124
|
+
/* Coarse pointers: bump the tool targets toward 40px. */
|
|
32125
|
+
@media (pointer: coarse) {
|
|
32126
|
+
.lens-tool-btn, .lens-width-btn { width: 38px; height: 38px; }
|
|
32127
|
+
.lens-swatch { width: 30px; height: 30px; }
|
|
32128
|
+
}
|
|
32129
|
+
|
|
31036
32130
|
/* Stacking inside the shadow layer: the panel, pill and floating chips always
|
|
31037
32131
|
sit above the annotation layer/rects and picker highlight, so marked areas
|
|
31038
32132
|
never paint over interactive UI. */
|
|
@@ -31158,7 +32252,7 @@ var Overlay = class {
|
|
|
31158
32252
|
this.layer = el("div", { class: "lens-layer" });
|
|
31159
32253
|
this.shadow.append(this.layer);
|
|
31160
32254
|
this.picker = new ElementPicker(this.layer, this.host);
|
|
31161
|
-
this.annotator = new
|
|
32255
|
+
this.annotator = new AnnotationEditor(this.layer);
|
|
31162
32256
|
this.layer.append(this.buildPill());
|
|
31163
32257
|
document.documentElement.append(this.host);
|
|
31164
32258
|
window.addEventListener("keydown", this.onGlobalKey, true);
|
|
@@ -31326,13 +32420,15 @@ var Overlay = class {
|
|
|
31326
32420
|
}
|
|
31327
32421
|
async submit(comment2, include) {
|
|
31328
32422
|
const annotations = this.annotator.list();
|
|
31329
|
-
const
|
|
32423
|
+
const composite = this.annotator.getComposite() ?? void 0;
|
|
32424
|
+
const screenshotCrop = this.annotator.getCropBounds();
|
|
31330
32425
|
const result2 = await this.deps.client.submitReport({
|
|
31331
32426
|
comment: comment2 || void 0,
|
|
31332
32427
|
annotations,
|
|
31333
32428
|
target: this.target ?? void 0,
|
|
31334
32429
|
excludeFromScreenshot: this.host,
|
|
31335
32430
|
screenshotCrop,
|
|
32431
|
+
annotationComposite: composite,
|
|
31336
32432
|
include
|
|
31337
32433
|
});
|
|
31338
32434
|
this.target = null;
|
|
@@ -31341,35 +32437,6 @@ var Overlay = class {
|
|
|
31341
32437
|
this.deps.client.discardRecording();
|
|
31342
32438
|
return result2;
|
|
31343
32439
|
}
|
|
31344
|
-
/**
|
|
31345
|
-
* Union the marked rects into a single bounding box in DOCUMENT coordinates,
|
|
31346
|
-
* or `undefined` if nothing is marked (→ full-page screenshot). Each rect is
|
|
31347
|
-
* fixed-positioned, so getBoundingClientRect() gives viewport coords; add the
|
|
31348
|
-
* page scroll offset to convert to document space.
|
|
31349
|
-
*/
|
|
31350
|
-
computeAnnotationCrop() {
|
|
31351
|
-
const rects = this.annotator.rects();
|
|
31352
|
-
if (rects.length === 0) return void 0;
|
|
31353
|
-
const scrollX = window.scrollX || window.pageXOffset || 0;
|
|
31354
|
-
const scrollY = window.scrollY || window.pageYOffset || 0;
|
|
31355
|
-
let minX = Infinity;
|
|
31356
|
-
let minY = Infinity;
|
|
31357
|
-
let maxX = -Infinity;
|
|
31358
|
-
let maxY = -Infinity;
|
|
31359
|
-
for (const rect of rects) {
|
|
31360
|
-
const r = rect.getBoundingClientRect();
|
|
31361
|
-
const left = r.left + scrollX;
|
|
31362
|
-
const top = r.top + scrollY;
|
|
31363
|
-
minX = Math.min(minX, left);
|
|
31364
|
-
minY = Math.min(minY, top);
|
|
31365
|
-
maxX = Math.max(maxX, left + r.width);
|
|
31366
|
-
maxY = Math.max(maxY, top + r.height);
|
|
31367
|
-
}
|
|
31368
|
-
const width = maxX - minX;
|
|
31369
|
-
const height = maxY - minY;
|
|
31370
|
-
if (!(width > 0) || !(height > 0)) return void 0;
|
|
31371
|
-
return { x: minX, y: minY, width, height };
|
|
31372
|
-
}
|
|
31373
32440
|
dismiss() {
|
|
31374
32441
|
this.deps.onDismiss();
|
|
31375
32442
|
this.destroy();
|