nudge-ui 0.2.0 → 0.2.2
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/bin/nudge-ui.mjs +3 -1
- package/dist/cli/agentCommand.d.ts +8 -0
- package/dist/cli/agentCommand.js +59 -0
- package/dist/client.mjs +878 -11
- package/dist/hosts/astro/astroRuntimeConfig.js +1 -1
- package/dist/hosts/astro/clientTransport.js +63 -11
- package/dist/hosts/astro/integration.d.ts +2 -2
- package/dist/hosts/astro/integration.js +4 -3
- package/dist/hosts/environment.d.ts +15 -0
- package/dist/hosts/environment.js +16 -0
- package/dist/hosts/next/sidecar.d.ts +4 -0
- package/dist/hosts/next/sidecar.js +56 -7
- package/dist/hosts/next/wrapper.d.ts +2 -0
- package/dist/hosts/next/wrapper.js +32 -3
- package/dist/hosts/projectBridge.d.ts +21 -0
- package/dist/hosts/projectBridge.js +98 -0
- package/dist/hosts/static/cli.d.ts +1 -1
- package/dist/hosts/static/cli.js +6 -1
- package/dist/hosts/static/manifest.d.ts +1 -1
- package/dist/hosts/static/manifest.js +3 -1
- package/dist/hosts/static/server.js +45 -3
- package/dist/hosts/vite/vite.d.ts +6 -1
- package/dist/hosts/vite/vite.js +82 -15
- package/dist/inspector/agent/McpConnectionDialog.d.ts +5 -4
- package/dist/inspector/agent/McpConnectionDialog.js +20 -37
- package/dist/inspector/agent/client.d.ts +16 -3
- package/dist/inspector/agent/client.js +114 -11
- package/dist/inspector/agent/connectionStatus.d.ts +3 -3
- package/dist/inspector/agent/connectionStatus.js +11 -4
- package/dist/inspector/agent/httpTransport.d.ts +8 -0
- package/dist/inspector/agent/httpTransport.js +78 -18
- package/dist/inspector/agent/protocol.d.ts +11 -3
- package/dist/inspector/agent/protocol.js +1 -1
- package/dist/inspector/agent/verification.d.ts +1 -1
- package/dist/inspector/agent/verification.js +69 -12
- package/dist/inspector/canvas/CanvasCard.d.ts +5 -3
- package/dist/inspector/canvas/CanvasCard.js +181 -91
- package/dist/inspector/canvas/CanvasElementOverlay.js +288 -52
- package/dist/inspector/canvas/CanvasToolbar.d.ts +12 -0
- package/dist/inspector/canvas/CanvasToolbar.js +24 -0
- package/dist/inspector/canvas/CanvasWorkspace.d.ts +4 -1
- package/dist/inspector/canvas/CanvasWorkspace.js +410 -72
- package/dist/inspector/canvas/activeCanvasDocument.d.ts +5 -0
- package/dist/inspector/canvas/activeCanvasDocument.js +23 -0
- package/dist/inspector/canvas/canvasResize.d.ts +30 -0
- package/dist/inspector/canvas/canvasResize.js +52 -0
- package/dist/inspector/canvas/canvasStore.d.ts +30 -6
- package/dist/inspector/canvas/canvasStore.js +128 -61
- package/dist/inspector/canvas/frameProtocol.d.ts +82 -5
- package/dist/inspector/canvas/frameProtocol.js +63 -2
- package/dist/inspector/canvas/keyboardShortcuts.d.ts +4 -0
- package/dist/inspector/canvas/keyboardShortcuts.js +16 -0
- package/dist/inspector/canvas/linkEligibility.d.ts +0 -5
- package/dist/inspector/canvas/linkEligibility.js +0 -29
- package/dist/inspector/canvas/projection.d.ts +7 -0
- package/dist/inspector/canvas/projection.js +118 -25
- package/dist/inspector/canvas/rendererBootstrap.js +42 -29
- package/dist/inspector/canvas/rendererElementSelector.d.ts +1 -1
- package/dist/inspector/canvas/rendererElementSelector.js +385 -58
- package/dist/inspector/canvas/rendererMessageRouter.d.ts +11 -0
- package/dist/inspector/canvas/rendererMessageRouter.js +43 -0
- package/dist/inspector/canvas/rendererSelectionProxy.js +6 -2
- package/dist/inspector/canvas/sessionStore.d.ts +1 -0
- package/dist/inspector/canvas/sessionStore.js +12 -4
- package/dist/inspector/canvas/staleChangeDetector.js +16 -14
- package/dist/inspector/changes/changesLog.js +31 -1
- package/dist/inspector/changes/managedStyleProjection.d.ts +1 -1
- package/dist/inspector/changes/managedStyleProjection.js +3 -3
- package/dist/inspector/changes/structuralTypes.d.ts +4 -0
- package/dist/inspector/changes/workspaceChanges.d.ts +4 -0
- package/dist/inspector/changes/workspaceChanges.js +29 -3
- package/dist/inspector/clientManifest.d.ts +7 -0
- package/dist/inspector/clientManifest.js +12 -0
- package/dist/inspector/componentSemantics/adapterRegistry.js +4 -3
- package/dist/inspector/componentSemantics/runtimeBridge.d.ts +2 -2
- package/dist/inspector/componentSemantics/runtimeBridge.js +16 -3
- package/dist/inspector/index.d.ts +2 -0
- package/dist/inspector/index.js +80 -32
- package/dist/inspector/inline-text/inlineTextDiagnostics.d.ts +10 -0
- package/dist/inspector/inline-text/inlineTextDiagnostics.js +65 -0
- package/dist/inspector/inline-text/inlineTextEditor.d.ts +3 -5
- package/dist/inspector/inline-text/inlineTextEditor.js +74 -206
- package/dist/inspector/inline-text/inlineTextFeedback.d.ts +5 -0
- package/dist/inspector/inline-text/inlineTextFeedback.js +27 -0
- package/dist/inspector/inline-text/inlineTextLifecycle.js +2 -0
- package/dist/inspector/inline-text/textEditChange.d.ts +14 -0
- package/dist/inspector/inline-text/textEditChange.js +165 -0
- package/dist/inspector/inspection/selectionProperty.d.ts +1 -0
- package/dist/inspector/inspection/selectionProperty.js +1 -0
- package/dist/inspector/overlay/DomNavigator.d.ts +10 -0
- package/dist/inspector/overlay/DomNavigator.js +40 -0
- package/dist/inspector/overlay/InspectorOverlay.js +6 -2
- package/dist/inspector/overlay/clickPolicy.d.ts +18 -2
- package/dist/inspector/overlay/clickPolicy.js +34 -3
- package/dist/inspector/overlay/elementSelector.d.ts +1 -1
- package/dist/inspector/overlay/elementSelector.js +18 -7
- package/dist/inspector/overlay/selectedGeometry.js +21 -2
- package/dist/inspector/overlay/spacingGestures.d.ts +45 -0
- package/dist/inspector/overlay/spacingGestures.js +351 -0
- package/dist/inspector/overlay/structuralGestures.d.ts +2 -2
- package/dist/inspector/overlay/structuralGestures.js +12 -10
- package/dist/inspector/projection/managedStylesheet.js +4 -1
- package/dist/inspector/projection/renderedInstance.d.ts +2 -0
- package/dist/inspector/projection/renderedInstance.js +5 -1
- package/dist/inspector/projection/structuralProjection.d.ts +2 -0
- package/dist/inspector/projection/structuralProjection.js +85 -1
- package/dist/inspector/projection/structuralProjectionBoundary.js +17 -1
- package/dist/inspector/projection/textProjection.d.ts +2 -0
- package/dist/inspector/projection/textProjection.js +10 -4
- package/dist/inspector/prompt/clipboardHandoff.d.ts +1 -1
- package/dist/inspector/prompt/clipboardHandoff.js +3 -3
- package/dist/inspector/prompt/generatePrompt.d.ts +2 -1
- package/dist/inspector/prompt/generatePrompt.js +19 -2
- package/dist/inspector/runtime/editorShell.d.ts +2 -0
- package/dist/inspector/runtime/editorShell.js +4 -0
- package/dist/inspector/runtime/runtimeConfig.d.ts +5 -1
- package/dist/inspector/runtime/runtimeConfig.js +4 -0
- package/dist/inspector/selection/hierarchy.d.ts +6 -0
- package/dist/inspector/selection/hierarchy.js +8 -0
- package/dist/inspector/selection/selectionStore.js +2 -0
- package/dist/inspector/settings/SettingsDialog.d.ts +1 -1
- package/dist/inspector/settings/SettingsDialog.js +12 -2
- package/dist/inspector/shell/AppShell.js +5 -1
- package/dist/inspector/shell/ChangesLog.js +26 -22
- package/dist/inspector/shell/CopyPromptButton.js +102 -19
- package/dist/inspector/shell/EmptyState.js +1 -1
- package/dist/inspector/shell/InspectorShell.d.ts +0 -1
- package/dist/inspector/shell/InspectorShell.js +38 -42
- package/dist/inspector/shell/PeekOriginalButton.d.ts +7 -0
- package/dist/inspector/shell/PeekOriginalButton.js +57 -0
- package/dist/inspector/shell/PeekOriginalIcon.d.ts +7 -0
- package/dist/inspector/shell/PeekOriginalIcon.js +9 -0
- package/dist/inspector/shell/originalPreview.d.ts +9 -0
- package/dist/inspector/shell/originalPreview.js +61 -0
- package/dist/inspector/shell/shortcuts.d.ts +3 -0
- package/dist/inspector/shell/shortcuts.js +15 -0
- package/dist/inspector/sketch/SketchChanges.d.ts +2 -0
- package/dist/inspector/sketch/SketchChanges.js +72 -0
- package/dist/inspector/sketch/SketchEditor.d.ts +21 -0
- package/dist/inspector/sketch/SketchEditor.js +402 -0
- package/dist/inspector/sketch/SketchEntryButton.d.ts +4 -0
- package/dist/inspector/sketch/SketchEntryButton.js +11 -0
- package/dist/inspector/sketch/SketchFrameOverlay.d.ts +11 -0
- package/dist/inspector/sketch/SketchFrameOverlay.js +119 -0
- package/dist/inspector/sketch/SketchLayersPanel.d.ts +2 -0
- package/dist/inspector/sketch/SketchLayersPanel.js +63 -0
- package/dist/inspector/sketch/SketchOverlay.d.ts +19 -0
- package/dist/inspector/sketch/SketchOverlay.js +420 -0
- package/dist/inspector/sketch/SketchPromptPanel.d.ts +15 -0
- package/dist/inspector/sketch/SketchPromptPanel.js +18 -0
- package/dist/inspector/sketch/SketchWorkspace.d.ts +13 -0
- package/dist/inspector/sketch/SketchWorkspace.js +152 -0
- package/dist/inspector/sketch/capture.d.ts +55 -0
- package/dist/inspector/sketch/capture.js +350 -0
- package/dist/inspector/sketch/freehand.d.ts +13 -0
- package/dist/inspector/sketch/freehand.js +64 -0
- package/dist/inspector/sketch/geometry.d.ts +17 -0
- package/dist/inspector/sketch/geometry.js +98 -0
- package/dist/inspector/sketch/handoff.d.ts +21 -0
- package/dist/inspector/sketch/handoff.js +72 -0
- package/dist/inspector/sketch/interaction.d.ts +21 -0
- package/dist/inspector/sketch/interaction.js +98 -0
- package/dist/inspector/sketch/model.d.ts +104 -0
- package/dist/inspector/sketch/model.js +282 -0
- package/dist/inspector/sketch/persistence.d.ts +21 -0
- package/dist/inspector/sketch/persistence.js +205 -0
- package/dist/inspector/sketch/raster.d.ts +22 -0
- package/dist/inspector/sketch/raster.js +203 -0
- package/dist/inspector/sketch/sketchNote.d.ts +10 -0
- package/dist/inspector/sketch/sketchNote.js +30 -0
- package/dist/inspector/sketch/store.d.ts +47 -0
- package/dist/inspector/sketch/store.js +434 -0
- package/dist/inspector/styleEditors/BorderEditor.js +15 -19
- package/dist/inspector/styleEditors/BorderRadiusEditor.js +6 -6
- package/dist/inspector/styleEditors/BoxShadowEditor.js +12 -1
- package/dist/inspector/styleEditors/ColorPicker.js +11 -36
- package/dist/inspector/styleEditors/GapField.d.ts +3 -2
- package/dist/inspector/styleEditors/GapField.js +2 -2
- package/dist/inspector/styleEditors/GridSection.js +1 -1
- package/dist/inspector/styleEditors/LayoutSection.js +11 -8
- package/dist/inspector/styleEditors/SpacingBox.js +31 -33
- package/dist/inspector/styleEditors/layoutPreviewState.d.ts +5 -0
- package/dist/inspector/styleEditors/layoutPreviewState.js +19 -0
- package/dist/inspector/styleEditors/layoutValue.js +3 -0
- package/dist/inspector/styleEditors/nudgeValue.d.ts +11 -0
- package/dist/inspector/styleEditors/nudgeValue.js +93 -5
- package/dist/inspector/styleEditors/stylePresence.d.ts +7 -0
- package/dist/inspector/styleEditors/stylePresence.js +22 -0
- package/dist/inspector/styleEditors/textRelevance.d.ts +3 -0
- package/dist/inspector/styleEditors/textRelevance.js +35 -0
- package/dist/inspector/styleEditors/useFieldVisibility.d.ts +6 -0
- package/dist/inspector/styleEditors/useFieldVisibility.js +26 -0
- package/dist/inspector/tokens/TokenField.js +130 -7
- package/dist/inspector/tokens/TokensPanel.js +2 -1
- package/dist/inspector/tokens/dragPointerLock.d.ts +8 -0
- package/dist/inspector/tokens/dragPointerLock.js +70 -0
- package/dist/inspector/tokens/editActions.d.ts +2 -0
- package/dist/inspector/tokens/editActions.js +2 -2
- package/dist/inspector/tokens/resolution/cssomCollector.d.ts +1 -1
- package/dist/inspector/tokens/resolution/cssomCollector.js +9 -5
- package/dist/inspector/tokens/resolution/ruleMatching.d.ts +40 -0
- package/dist/inspector/tokens/resolution/ruleMatching.js +423 -0
- package/dist/inspector/tokens/resolution/selectorSemantics.d.ts +19 -3
- package/dist/inspector/tokens/resolution/selectorSemantics.js +378 -4
- package/dist/inspector/tokens/resolution.d.ts +3 -26
- package/dist/inspector/tokens/resolution.js +44 -735
- package/dist/inspector/tokens/tokenAvailability.d.ts +18 -0
- package/dist/inspector/tokens/tokenAvailability.js +204 -0
- package/dist/inspector/ui/Button.d.ts +1 -1
- package/dist/inspector/ui/Disclosure.d.ts +22 -0
- package/dist/inspector/ui/Disclosure.js +13 -0
- package/dist/inspector/ui/SideValuesField.js +2 -2
- package/dist/inspector/ui/StatusCallout.d.ts +4 -1
- package/dist/inspector/ui/StatusCallout.js +2 -2
- package/dist/inspector/ui/dragCursor.d.ts +8 -0
- package/dist/inspector/ui/dragCursor.js +21 -0
- package/dist/inspector/ui/styles.js +21 -17
- package/dist/internal/agent-protocol.d.ts +65 -1
- package/dist/internal/agent-protocol.js +183 -3
- package/dist/nudge-ui.mjs +276 -58
- package/dist/transport/editor.d.ts +47 -0
- package/dist/transport/editor.js +163 -0
- package/dist/transport/index.d.ts +1 -0
- package/dist/transport/index.js +1 -0
- package/dist/transport/routes.d.ts +10 -0
- package/dist/transport/routes.js +10 -0
- package/package.json +3 -1
- package/dist/inspector/shell/DomNavigation.d.ts +0 -8
- package/dist/inspector/shell/DomNavigation.js +0 -30
- package/dist/inspector/shell/panelLayout.d.ts +0 -2
- package/dist/inspector/shell/panelLayout.js +0 -29
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type SketchCaptureErrorCode = "unsupported" | "cancelled" | "capture-failed" | "processing-timeout";
|
|
2
|
+
export declare class SketchCaptureError extends Error {
|
|
3
|
+
readonly code: SketchCaptureErrorCode;
|
|
4
|
+
constructor(code: SketchCaptureErrorCode, message: string, options?: {
|
|
5
|
+
cause?: unknown;
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
export interface CapturedSketch {
|
|
9
|
+
readonly originalImage: Blob;
|
|
10
|
+
readonly imageWidth: number;
|
|
11
|
+
readonly imageHeight: number;
|
|
12
|
+
readonly capture: {
|
|
13
|
+
readonly url: string;
|
|
14
|
+
readonly title: string;
|
|
15
|
+
readonly timestamp: number;
|
|
16
|
+
readonly viewportWidth: number;
|
|
17
|
+
readonly viewportHeight: number;
|
|
18
|
+
readonly scrollX: number;
|
|
19
|
+
readonly scrollY: number;
|
|
20
|
+
readonly devicePixelRatio: number;
|
|
21
|
+
readonly host: string;
|
|
22
|
+
readonly framework: string;
|
|
23
|
+
readonly imageWidth: number;
|
|
24
|
+
readonly imageHeight: number;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export interface SketchCaptureOptions {
|
|
28
|
+
readonly hostElement: HTMLElement;
|
|
29
|
+
readonly signal?: AbortSignal;
|
|
30
|
+
readonly processingTimeoutMs?: number;
|
|
31
|
+
}
|
|
32
|
+
export interface SketchViewport {
|
|
33
|
+
readonly width: number;
|
|
34
|
+
readonly height: number;
|
|
35
|
+
readonly scrollX: number;
|
|
36
|
+
readonly scrollY: number;
|
|
37
|
+
/** Screen-space placement of the renderer iframe inside the editor. */
|
|
38
|
+
readonly offsetX: number;
|
|
39
|
+
readonly offsetY: number;
|
|
40
|
+
readonly displayWidth: number;
|
|
41
|
+
readonly displayHeight: number;
|
|
42
|
+
}
|
|
43
|
+
export interface SketchScrollPosition {
|
|
44
|
+
readonly x: number;
|
|
45
|
+
readonly y: number;
|
|
46
|
+
}
|
|
47
|
+
/** Reads root scrolling from both window and document APIs. */
|
|
48
|
+
export declare function getSketchScrollPosition(targetWindow: Window): SketchScrollPosition;
|
|
49
|
+
export declare function getSketchViewport(hostElement: HTMLElement): SketchViewport;
|
|
50
|
+
/** Renders the current viewport from its DOM without invoking screen capture permissions. */
|
|
51
|
+
export declare function captureViewport(options: SketchCaptureOptions): Promise<CapturedSketch>;
|
|
52
|
+
export declare function createSketchCaptureOperation(options: SketchCaptureOptions): {
|
|
53
|
+
readonly promise: Promise<CapturedSketch>;
|
|
54
|
+
readonly cancel: () => void;
|
|
55
|
+
};
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { domToBlob } from "modern-screenshot";
|
|
2
|
+
import { getNudgeUiRuntimeConfig } from "../runtime/runtimeConfig.js";
|
|
3
|
+
import { SKETCH_LIMITS } from "./model.js";
|
|
4
|
+
import { getPngDimensions } from "./raster.js";
|
|
5
|
+
const MEDIA_LOAD_TIMEOUT_MS = 500;
|
|
6
|
+
const FONT_READY_TIMEOUT_MS = 2_000;
|
|
7
|
+
const DEFAULT_PROCESSING_TIMEOUT_MS = 20_000;
|
|
8
|
+
const EMPTY_MEDIA_SRC = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
|
|
9
|
+
export class SketchCaptureError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
constructor(code, message, options) {
|
|
12
|
+
super(message, options);
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.name = "SketchCaptureError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function abortError() {
|
|
18
|
+
return new SketchCaptureError("cancelled", "Sketch capture was cancelled.");
|
|
19
|
+
}
|
|
20
|
+
function assertNotAborted(signal) {
|
|
21
|
+
if (signal?.aborted)
|
|
22
|
+
throw abortError();
|
|
23
|
+
}
|
|
24
|
+
function waitForAnimationFrame(signal) {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
if (signal?.aborted) {
|
|
27
|
+
reject(abortError());
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const finish = () => {
|
|
31
|
+
signal?.removeEventListener("abort", onAbort);
|
|
32
|
+
resolve();
|
|
33
|
+
};
|
|
34
|
+
const onAbort = () => {
|
|
35
|
+
signal?.removeEventListener("abort", onAbort);
|
|
36
|
+
reject(abortError());
|
|
37
|
+
};
|
|
38
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
39
|
+
if (typeof requestAnimationFrame === "function")
|
|
40
|
+
requestAnimationFrame(finish);
|
|
41
|
+
else
|
|
42
|
+
setTimeout(finish, 16);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async function waitForLayout(signal, targetDocument) {
|
|
46
|
+
try {
|
|
47
|
+
const fonts = targetDocument?.fonts
|
|
48
|
+
?? (typeof document.fonts !== "undefined" ? document.fonts : undefined);
|
|
49
|
+
if (fonts) {
|
|
50
|
+
await new Promise((resolve, reject) => {
|
|
51
|
+
let timer = setTimeout(resolve, FONT_READY_TIMEOUT_MS);
|
|
52
|
+
const onAbort = () => {
|
|
53
|
+
if (timer)
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
timer = null;
|
|
56
|
+
signal?.removeEventListener("abort", onAbort);
|
|
57
|
+
reject(abortError());
|
|
58
|
+
};
|
|
59
|
+
const finish = () => {
|
|
60
|
+
if (timer)
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
timer = null;
|
|
63
|
+
signal?.removeEventListener("abort", onAbort);
|
|
64
|
+
resolve();
|
|
65
|
+
};
|
|
66
|
+
if (signal?.aborted) {
|
|
67
|
+
onAbort();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
71
|
+
void fonts.ready.then(finish, finish);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Font readiness is best effort; the animation-frame settle still runs.
|
|
77
|
+
}
|
|
78
|
+
await waitForAnimationFrame(signal);
|
|
79
|
+
await waitForAnimationFrame(signal);
|
|
80
|
+
}
|
|
81
|
+
function domContentWidth(hostElement) {
|
|
82
|
+
const panel = hostElement.shadowRoot?.querySelector('.panel[data-open="true"]');
|
|
83
|
+
const panelRect = panel?.getBoundingClientRect();
|
|
84
|
+
if (!panelRect || panelRect.width <= 0)
|
|
85
|
+
return Math.max(1, Math.round(window.innerWidth));
|
|
86
|
+
return Math.max(1, Math.min(Math.round(window.innerWidth), Math.round(panelRect.left)));
|
|
87
|
+
}
|
|
88
|
+
function nonNegativeOffset(...values) {
|
|
89
|
+
return Math.max(0, ...values.filter((value) => Number.isFinite(value)));
|
|
90
|
+
}
|
|
91
|
+
/** Reads root scrolling from both window and document APIs. */
|
|
92
|
+
export function getSketchScrollPosition(targetWindow) {
|
|
93
|
+
const targetDocument = targetWindow.document;
|
|
94
|
+
const scrollingElement = targetDocument.scrollingElement;
|
|
95
|
+
return {
|
|
96
|
+
x: nonNegativeOffset(targetWindow.scrollX, targetWindow.pageXOffset, scrollingElement?.scrollLeft ?? 0, targetDocument.documentElement?.scrollLeft ?? 0, targetDocument.body?.scrollLeft ?? 0),
|
|
97
|
+
y: nonNegativeOffset(targetWindow.scrollY, targetWindow.pageYOffset, scrollingElement?.scrollTop ?? 0, targetDocument.documentElement?.scrollTop ?? 0, targetDocument.body?.scrollTop ?? 0),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function getSketchSurface(hostElement) {
|
|
101
|
+
const iframe = typeof HTMLIFrameElement !== "undefined" && hostElement instanceof HTMLIFrameElement
|
|
102
|
+
? hostElement
|
|
103
|
+
: null;
|
|
104
|
+
if (iframe?.contentDocument && iframe.contentWindow) {
|
|
105
|
+
return { document: iframe.contentDocument, window: iframe.contentWindow, iframe };
|
|
106
|
+
}
|
|
107
|
+
const ownerDocument = hostElement.ownerDocument ?? document;
|
|
108
|
+
return {
|
|
109
|
+
document: ownerDocument,
|
|
110
|
+
window: ownerDocument.defaultView ?? window,
|
|
111
|
+
iframe: null,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export function getSketchViewport(hostElement) {
|
|
115
|
+
const surface = getSketchSurface(hostElement);
|
|
116
|
+
const scroll = getSketchScrollPosition(surface.window);
|
|
117
|
+
if (surface.iframe) {
|
|
118
|
+
const rect = surface.iframe.getBoundingClientRect();
|
|
119
|
+
return {
|
|
120
|
+
width: Math.max(1, Math.round(surface.window.innerWidth || surface.iframe.clientWidth || rect.width)),
|
|
121
|
+
height: Math.max(1, Math.round(surface.window.innerHeight || surface.iframe.clientHeight || rect.height)),
|
|
122
|
+
scrollX: scroll.x,
|
|
123
|
+
scrollY: scroll.y,
|
|
124
|
+
offsetX: rect.left,
|
|
125
|
+
offsetY: rect.top,
|
|
126
|
+
displayWidth: Math.max(1, rect.width),
|
|
127
|
+
displayHeight: Math.max(1, rect.height),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const width = domContentWidth(hostElement);
|
|
131
|
+
const height = Math.max(1, Math.round(surface.window.innerHeight));
|
|
132
|
+
return {
|
|
133
|
+
width,
|
|
134
|
+
height,
|
|
135
|
+
scrollX: scroll.x,
|
|
136
|
+
scrollY: scroll.y,
|
|
137
|
+
offsetX: 0,
|
|
138
|
+
offsetY: 0,
|
|
139
|
+
displayWidth: width,
|
|
140
|
+
displayHeight: height,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function domViewportDimensions(hostElement) {
|
|
144
|
+
const surface = getSketchSurface(hostElement);
|
|
145
|
+
const { width, height } = getSketchViewport(hostElement);
|
|
146
|
+
const devicePixelRatio = Math.max(1, surface.window.devicePixelRatio || 1);
|
|
147
|
+
const scale = Math.min(devicePixelRatio, SKETCH_LIMITS.imageEdge / Math.max(width, height));
|
|
148
|
+
return { width, height, scale };
|
|
149
|
+
}
|
|
150
|
+
function domBackgroundColor(targetDocument) {
|
|
151
|
+
const color = targetDocument.defaultView?.getComputedStyle(targetDocument.body).backgroundColor;
|
|
152
|
+
return color && color !== "transparent" && color !== "rgba(0, 0, 0, 0)" ? color : null;
|
|
153
|
+
}
|
|
154
|
+
function isGoogleFontStylesheet(node) {
|
|
155
|
+
if (node.nodeType !== 1)
|
|
156
|
+
return false;
|
|
157
|
+
const element = node;
|
|
158
|
+
if (element.localName !== "link")
|
|
159
|
+
return false;
|
|
160
|
+
if ((element.getAttribute("rel") ?? "").toLowerCase() !== "stylesheet")
|
|
161
|
+
return false;
|
|
162
|
+
const href = element.getAttribute("href");
|
|
163
|
+
if (!href)
|
|
164
|
+
return false;
|
|
165
|
+
try {
|
|
166
|
+
const url = new URL(href, element.ownerDocument?.baseURI);
|
|
167
|
+
return url.hostname === "fonts.googleapis.com" || url.hostname === "fonts.gstatic.com";
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function hasGoogleFontStylesheet(targetDocument) {
|
|
174
|
+
return Array.from(targetDocument.querySelectorAll("link")).some(isGoogleFontStylesheet);
|
|
175
|
+
}
|
|
176
|
+
function prepareDomClone(node) {
|
|
177
|
+
// Use nodeType/localName instead of instanceof so this works for iframe realms.
|
|
178
|
+
if (node.nodeType !== 1)
|
|
179
|
+
return;
|
|
180
|
+
const element = node;
|
|
181
|
+
if (element.localName === "html")
|
|
182
|
+
element.removeAttribute("data-nudge-ui-panel");
|
|
183
|
+
if (element.localName === "body")
|
|
184
|
+
element.style.setProperty("margin-right", "0", "important");
|
|
185
|
+
if (isGoogleFontStylesheet(node))
|
|
186
|
+
element.remove();
|
|
187
|
+
}
|
|
188
|
+
function suspendOffscreenLazyImages(targetDocument, viewport) {
|
|
189
|
+
const suspended = [];
|
|
190
|
+
for (const element of Array.from(targetDocument.images)) {
|
|
191
|
+
if (element.loading !== "lazy")
|
|
192
|
+
continue;
|
|
193
|
+
const rect = element.getBoundingClientRect();
|
|
194
|
+
const visible = rect.bottom > 0
|
|
195
|
+
&& rect.right > 0
|
|
196
|
+
&& rect.top < viewport.height
|
|
197
|
+
&& rect.left < viewport.width;
|
|
198
|
+
if (visible)
|
|
199
|
+
continue;
|
|
200
|
+
suspended.push({
|
|
201
|
+
element,
|
|
202
|
+
src: element.getAttribute("src") ?? "",
|
|
203
|
+
srcset: element.getAttribute("srcset") ?? "",
|
|
204
|
+
});
|
|
205
|
+
element.removeAttribute("srcset");
|
|
206
|
+
element.src = EMPTY_MEDIA_SRC;
|
|
207
|
+
}
|
|
208
|
+
return () => {
|
|
209
|
+
for (const { element, src, srcset } of suspended) {
|
|
210
|
+
if (srcset)
|
|
211
|
+
element.setAttribute("srcset", srcset);
|
|
212
|
+
else
|
|
213
|
+
element.removeAttribute("srcset");
|
|
214
|
+
if (src)
|
|
215
|
+
element.src = src;
|
|
216
|
+
else
|
|
217
|
+
element.removeAttribute("src");
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
async function runWithProcessingTimeout(operation, timeoutMs, parentSignal) {
|
|
222
|
+
const controller = new AbortController();
|
|
223
|
+
let timer = null;
|
|
224
|
+
let onParentAbort = null;
|
|
225
|
+
try {
|
|
226
|
+
return await Promise.race([
|
|
227
|
+
operation(controller.signal),
|
|
228
|
+
new Promise((_, reject) => {
|
|
229
|
+
onParentAbort = () => {
|
|
230
|
+
reject(abortError());
|
|
231
|
+
controller.abort();
|
|
232
|
+
};
|
|
233
|
+
if (parentSignal?.aborted) {
|
|
234
|
+
onParentAbort();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
parentSignal?.addEventListener("abort", onParentAbort, { once: true });
|
|
238
|
+
}),
|
|
239
|
+
new Promise((_, reject) => {
|
|
240
|
+
timer = setTimeout(() => {
|
|
241
|
+
reject(new SketchCaptureError("processing-timeout", "The selected tab took too long to prepare. Try capturing again."));
|
|
242
|
+
controller.abort();
|
|
243
|
+
}, timeoutMs);
|
|
244
|
+
}),
|
|
245
|
+
]);
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
if (timer)
|
|
249
|
+
clearTimeout(timer);
|
|
250
|
+
if (onParentAbort && parentSignal)
|
|
251
|
+
parentSignal.removeEventListener("abort", onParentAbort);
|
|
252
|
+
controller.abort();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async function captureDomFrame(options, signal) {
|
|
256
|
+
const surface = getSketchSurface(options.hostElement);
|
|
257
|
+
const viewport = domViewportDimensions(options.hostElement);
|
|
258
|
+
let scale = viewport.scale;
|
|
259
|
+
for (let attempt = 0; attempt < 7; attempt += 1) {
|
|
260
|
+
assertNotAborted(signal);
|
|
261
|
+
const restoreMedia = suspendOffscreenLazyImages(surface.document, viewport);
|
|
262
|
+
let blob;
|
|
263
|
+
try {
|
|
264
|
+
blob = await domToBlob(surface.document.documentElement, {
|
|
265
|
+
type: "image/png",
|
|
266
|
+
width: viewport.width,
|
|
267
|
+
height: viewport.height,
|
|
268
|
+
scale,
|
|
269
|
+
backgroundColor: domBackgroundColor(surface.document),
|
|
270
|
+
style: {
|
|
271
|
+
width: `${viewport.width}px`,
|
|
272
|
+
height: `${viewport.height}px`,
|
|
273
|
+
overflow: "hidden",
|
|
274
|
+
},
|
|
275
|
+
filter: (node) => node !== options.hostElement && !isGoogleFontStylesheet(node),
|
|
276
|
+
features: { restoreScrollPosition: true },
|
|
277
|
+
timeout: MEDIA_LOAD_TIMEOUT_MS,
|
|
278
|
+
// Cross-origin Google CSS cannot be read by modern-screenshot. Skipping
|
|
279
|
+
// its font embedding avoids waiting on a stylesheet that the renderer
|
|
280
|
+
// cannot serialize anyway; computed font styles still remain inline.
|
|
281
|
+
font: hasGoogleFontStylesheet(surface.document) ? false : undefined,
|
|
282
|
+
onCloneEachNode: prepareDomClone,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
restoreMedia();
|
|
287
|
+
}
|
|
288
|
+
assertNotAborted(signal);
|
|
289
|
+
const dimensions = await getPngDimensions(blob);
|
|
290
|
+
if (!dimensions
|
|
291
|
+
|| dimensions.width > SKETCH_LIMITS.imageEdge
|
|
292
|
+
|| dimensions.height > SKETCH_LIMITS.imageEdge) {
|
|
293
|
+
throw new SketchCaptureError("capture-failed", "The rendered viewport produced an invalid image.");
|
|
294
|
+
}
|
|
295
|
+
if (blob.size <= SKETCH_LIMITS.imageBytes) {
|
|
296
|
+
return { blob, width: dimensions.width, height: dimensions.height };
|
|
297
|
+
}
|
|
298
|
+
scale *= 0.8;
|
|
299
|
+
}
|
|
300
|
+
throw new SketchCaptureError("capture-failed", "This viewport is too detailed to save within the 2 MiB image limit.");
|
|
301
|
+
}
|
|
302
|
+
/** Renders the current viewport from its DOM without invoking screen capture permissions. */
|
|
303
|
+
export async function captureViewport(options) {
|
|
304
|
+
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
305
|
+
throw new SketchCaptureError("unsupported", "Viewport capture is only available in a browser.");
|
|
306
|
+
}
|
|
307
|
+
assertNotAborted(options.signal);
|
|
308
|
+
try {
|
|
309
|
+
const surface = getSketchSurface(options.hostElement);
|
|
310
|
+
const frame = await runWithProcessingTimeout(async (processingSignal) => {
|
|
311
|
+
await waitForLayout(processingSignal, surface.document);
|
|
312
|
+
return captureDomFrame(options, processingSignal);
|
|
313
|
+
}, options.processingTimeoutMs ?? DEFAULT_PROCESSING_TIMEOUT_MS, options.signal);
|
|
314
|
+
assertNotAborted(options.signal);
|
|
315
|
+
const runtime = getNudgeUiRuntimeConfig();
|
|
316
|
+
const scroll = getSketchScrollPosition(surface.window);
|
|
317
|
+
const capture = {
|
|
318
|
+
url: surface.window.location.href,
|
|
319
|
+
title: surface.document.title,
|
|
320
|
+
timestamp: Date.now(),
|
|
321
|
+
viewportWidth: surface.window.innerWidth,
|
|
322
|
+
viewportHeight: surface.window.innerHeight,
|
|
323
|
+
scrollX: scroll.x,
|
|
324
|
+
scrollY: scroll.y,
|
|
325
|
+
devicePixelRatio: surface.window.devicePixelRatio || 1,
|
|
326
|
+
host: runtime.host,
|
|
327
|
+
framework: runtime.framework,
|
|
328
|
+
imageWidth: frame.width,
|
|
329
|
+
imageHeight: frame.height,
|
|
330
|
+
};
|
|
331
|
+
return { originalImage: frame.blob, imageWidth: frame.width, imageHeight: frame.height, capture };
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
if (error instanceof SketchCaptureError)
|
|
335
|
+
throw error;
|
|
336
|
+
if (error instanceof DOMException && error.name === "AbortError")
|
|
337
|
+
throw abortError();
|
|
338
|
+
throw new SketchCaptureError("capture-failed", "The viewport could not be rendered from the page DOM. Try again.", { cause: error });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
export function createSketchCaptureOperation(options) {
|
|
342
|
+
const controller = new AbortController();
|
|
343
|
+
const onAbort = () => controller.abort();
|
|
344
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
345
|
+
const promise = captureViewport({
|
|
346
|
+
...options,
|
|
347
|
+
signal: controller.signal,
|
|
348
|
+
}).finally(() => options.signal?.removeEventListener("abort", onAbort));
|
|
349
|
+
return { promise, cancel: () => controller.abort() };
|
|
350
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { type SketchAnnotation, type SketchStroke } from "./model.js";
|
|
3
|
+
export type Vec2 = readonly [number, number];
|
|
4
|
+
export declare function getSvgPathFromStroke(points: readonly Vec2[], closed?: boolean): string;
|
|
5
|
+
export declare function sketchStrokeOutline(stroke: Pick<SketchStroke, "kind" | "points" | "width">, scaleX?: number, scaleY?: number): readonly Vec2[];
|
|
6
|
+
export declare function sketchStrokePath(stroke: Pick<SketchStroke, "kind" | "points" | "width">, scaleX?: number, scaleY?: number): string;
|
|
7
|
+
export interface SketchSvgLayerProps {
|
|
8
|
+
readonly strokes: readonly SketchStroke[];
|
|
9
|
+
readonly annotations?: readonly SketchAnnotation[];
|
|
10
|
+
readonly annotationScale?: number;
|
|
11
|
+
}
|
|
12
|
+
/** Renders sketch geometry as DOM-native SVG shapes. */
|
|
13
|
+
export declare function SketchSvgLayer({ strokes, annotations, annotationScale, }: SketchSvgLayerProps): ReactElement;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { getStroke } from "perfect-freehand";
|
|
3
|
+
import { SKETCH_STROKE_COLOR, } from "./model.js";
|
|
4
|
+
const FREEHAND_OPTIONS = {
|
|
5
|
+
thinning: 0.45,
|
|
6
|
+
smoothing: 0.5,
|
|
7
|
+
streamline: 0.35,
|
|
8
|
+
simulatePressure: true,
|
|
9
|
+
last: true,
|
|
10
|
+
};
|
|
11
|
+
function number(value) {
|
|
12
|
+
return Number.isFinite(value) ? value.toFixed(2) : "0";
|
|
13
|
+
}
|
|
14
|
+
function linePath(points, scaleX, scaleY) {
|
|
15
|
+
const first = points[0];
|
|
16
|
+
if (!first)
|
|
17
|
+
return "";
|
|
18
|
+
const commands = [`M ${number(first.x * scaleX)} ${number(first.y * scaleY)}`];
|
|
19
|
+
for (const point of points.slice(1)) {
|
|
20
|
+
commands.push(`L ${number(point.x * scaleX)} ${number(point.y * scaleY)}`);
|
|
21
|
+
}
|
|
22
|
+
return commands.join(" ");
|
|
23
|
+
}
|
|
24
|
+
export function getSvgPathFromStroke(points, closed = true) {
|
|
25
|
+
if (points.length < 4)
|
|
26
|
+
return "";
|
|
27
|
+
const average = (left, right) => (left + right) / 2;
|
|
28
|
+
const first = points[0];
|
|
29
|
+
const second = points[1];
|
|
30
|
+
const third = points[2];
|
|
31
|
+
let result = `M ${number(first[0])} ${number(first[1])} Q ${number(second[0])} ${number(second[1])} ${number(average(second[0], third[0]))} ${number(average(second[1], third[1]))} T`;
|
|
32
|
+
for (let index = 2; index < points.length - 1; index += 1) {
|
|
33
|
+
const current = points[index];
|
|
34
|
+
const next = points[index + 1];
|
|
35
|
+
result += ` ${number(average(current[0], next[0]))} ${number(average(current[1], next[1]))}`;
|
|
36
|
+
}
|
|
37
|
+
return closed ? `${result} Z` : result;
|
|
38
|
+
}
|
|
39
|
+
export function sketchStrokeOutline(stroke, scaleX = 1, scaleY = scaleX) {
|
|
40
|
+
if (stroke.points.length === 0 || stroke.kind === "rectangle")
|
|
41
|
+
return [];
|
|
42
|
+
const size = stroke.width * Math.max(scaleX, scaleY);
|
|
43
|
+
return getStroke(stroke.points.map((point) => [point.x * scaleX, point.y * scaleY]), { ...FREEHAND_OPTIONS, size });
|
|
44
|
+
}
|
|
45
|
+
export function sketchStrokePath(stroke, scaleX = 1, scaleY = scaleX) {
|
|
46
|
+
if (stroke.points.length === 0)
|
|
47
|
+
return "";
|
|
48
|
+
if (stroke.kind === "rectangle")
|
|
49
|
+
return linePath(stroke.points, scaleX, scaleY);
|
|
50
|
+
return getSvgPathFromStroke(sketchStrokeOutline(stroke, scaleX, scaleY));
|
|
51
|
+
}
|
|
52
|
+
/** Renders sketch geometry as DOM-native SVG shapes. */
|
|
53
|
+
export function SketchSvgLayer({ strokes, annotations = [], annotationScale = 1, }) {
|
|
54
|
+
return (_jsxs("g", { "data-test": "sketch-svg-layer", children: [strokes.map((stroke) => {
|
|
55
|
+
const path = sketchStrokePath(stroke);
|
|
56
|
+
if (!path)
|
|
57
|
+
return null;
|
|
58
|
+
const rectangle = stroke.kind === "rectangle";
|
|
59
|
+
return (_jsx("path", { d: path, fill: rectangle ? "none" : stroke.color || SKETCH_STROKE_COLOR, stroke: rectangle ? stroke.color || SKETCH_STROKE_COLOR : "none", strokeWidth: rectangle ? stroke.width : undefined, strokeLinecap: "round", strokeLinejoin: "round", pointerEvents: "none" }, stroke.id));
|
|
60
|
+
}), annotations.map((annotation) => {
|
|
61
|
+
const radius = 16 * annotationScale;
|
|
62
|
+
return (_jsxs("g", { transform: `translate(${number(annotation.point.x)} ${number(annotation.point.y)})`, pointerEvents: "none", children: [_jsx("circle", { r: radius, fill: SKETCH_STROKE_COLOR }), _jsx("text", { x: "0", y: "0", fill: "#ffffff", fontFamily: "system-ui, sans-serif", fontSize: 12 * annotationScale, fontWeight: "600", textAnchor: "middle", dominantBaseline: "central", children: annotation.number })] }, annotation.id));
|
|
63
|
+
})] }));
|
|
64
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type SketchAnnotation, type SketchPoint, type SketchStroke } from "./model.js";
|
|
2
|
+
export interface SketchBounds {
|
|
3
|
+
readonly width: number;
|
|
4
|
+
readonly height: number;
|
|
5
|
+
}
|
|
6
|
+
export type SketchElementReference = {
|
|
7
|
+
readonly kind: "stroke";
|
|
8
|
+
readonly id: string;
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: "annotation";
|
|
11
|
+
readonly id: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function distanceToSegment(point: SketchPoint, start: SketchPoint, end: SketchPoint): number;
|
|
14
|
+
/** Returns the topmost drawable object under a point in canvas coordinates. */
|
|
15
|
+
export declare function findSketchElementAt(point: SketchPoint, strokes: readonly SketchStroke[], annotations: readonly SketchAnnotation[]): SketchElementReference | null;
|
|
16
|
+
export declare function translateSketchStroke(stroke: SketchStroke, delta: SketchPoint, bounds: SketchBounds): SketchStroke;
|
|
17
|
+
export declare function translateSketchAnnotation(annotation: SketchAnnotation, delta: SketchPoint, bounds: SketchBounds): SketchAnnotation;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { SKETCH_ANNOTATION_RADIUS, } from "./model.js";
|
|
2
|
+
function distance(left, right) {
|
|
3
|
+
return Math.hypot(left.x - right.x, left.y - right.y);
|
|
4
|
+
}
|
|
5
|
+
export function distanceToSegment(point, start, end) {
|
|
6
|
+
const deltaX = end.x - start.x;
|
|
7
|
+
const deltaY = end.y - start.y;
|
|
8
|
+
const lengthSquared = deltaX * deltaX + deltaY * deltaY;
|
|
9
|
+
if (lengthSquared === 0)
|
|
10
|
+
return distance(point, start);
|
|
11
|
+
const projection = ((point.x - start.x) * deltaX + (point.y - start.y) * deltaY) / lengthSquared;
|
|
12
|
+
const position = Math.max(0, Math.min(1, projection));
|
|
13
|
+
return distance(point, {
|
|
14
|
+
x: start.x + position * deltaX,
|
|
15
|
+
y: start.y + position * deltaY,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function distanceToStroke(point, stroke) {
|
|
19
|
+
if (stroke.points.length === 1)
|
|
20
|
+
return distance(point, stroke.points[0]);
|
|
21
|
+
let closest = Number.POSITIVE_INFINITY;
|
|
22
|
+
for (let index = 1; index < stroke.points.length; index += 1) {
|
|
23
|
+
closest = Math.min(closest, distanceToSegment(point, stroke.points[index - 1], stroke.points[index]));
|
|
24
|
+
}
|
|
25
|
+
return closest;
|
|
26
|
+
}
|
|
27
|
+
function pointBounds(points) {
|
|
28
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
29
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
30
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
31
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
32
|
+
for (const point of points) {
|
|
33
|
+
minX = Math.min(minX, point.x);
|
|
34
|
+
maxX = Math.max(maxX, point.x);
|
|
35
|
+
minY = Math.min(minY, point.y);
|
|
36
|
+
maxY = Math.max(maxY, point.y);
|
|
37
|
+
}
|
|
38
|
+
return { minX, maxX, minY, maxY };
|
|
39
|
+
}
|
|
40
|
+
/** Returns the topmost drawable object under a point in canvas coordinates. */
|
|
41
|
+
export function findSketchElementAt(point, strokes, annotations) {
|
|
42
|
+
for (let index = annotations.length - 1; index >= 0; index -= 1) {
|
|
43
|
+
const annotation = annotations[index];
|
|
44
|
+
if (distance(point, annotation.point) <= SKETCH_ANNOTATION_RADIUS * 1.5) {
|
|
45
|
+
return { kind: "annotation", id: annotation.id };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (let index = strokes.length - 1; index >= 0; index -= 1) {
|
|
49
|
+
const stroke = strokes[index];
|
|
50
|
+
const tolerance = Math.max(10, stroke.width / 2 + 6);
|
|
51
|
+
const bounds = pointBounds(stroke.points);
|
|
52
|
+
const insideRectangle = stroke.kind === "rectangle"
|
|
53
|
+
&& point.x >= bounds.minX - tolerance
|
|
54
|
+
&& point.x <= bounds.maxX + tolerance
|
|
55
|
+
&& point.y >= bounds.minY - tolerance
|
|
56
|
+
&& point.y <= bounds.maxY + tolerance;
|
|
57
|
+
if (insideRectangle || distanceToStroke(point, stroke) <= tolerance) {
|
|
58
|
+
return { kind: "stroke", id: stroke.id };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function constrainedDelta(points, delta, bounds) {
|
|
64
|
+
if (points.length === 0)
|
|
65
|
+
return delta;
|
|
66
|
+
const { minX, maxX, minY, maxY } = pointBounds(points);
|
|
67
|
+
let x = delta.x;
|
|
68
|
+
let y = delta.y;
|
|
69
|
+
if (minX + x < 0)
|
|
70
|
+
x = -minX;
|
|
71
|
+
else if (maxX + x > bounds.width)
|
|
72
|
+
x = bounds.width - maxX;
|
|
73
|
+
if (minY + y < 0)
|
|
74
|
+
y = -minY;
|
|
75
|
+
else if (maxY + y > bounds.height)
|
|
76
|
+
y = bounds.height - maxY;
|
|
77
|
+
return { x, y };
|
|
78
|
+
}
|
|
79
|
+
export function translateSketchStroke(stroke, delta, bounds) {
|
|
80
|
+
const constrained = constrainedDelta(stroke.points, delta, bounds);
|
|
81
|
+
return {
|
|
82
|
+
...stroke,
|
|
83
|
+
points: stroke.points.map((point) => ({
|
|
84
|
+
x: point.x + constrained.x,
|
|
85
|
+
y: point.y + constrained.y,
|
|
86
|
+
})),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export function translateSketchAnnotation(annotation, delta, bounds) {
|
|
90
|
+
const constrained = constrainedDelta([annotation.point], delta, bounds);
|
|
91
|
+
return {
|
|
92
|
+
...annotation,
|
|
93
|
+
point: {
|
|
94
|
+
x: annotation.point.x + constrained.x,
|
|
95
|
+
y: annotation.point.y + constrained.y,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AgentSketchAttachment, AgentSketchMetadata } from "../agent/protocol.js";
|
|
2
|
+
import { type SketchQueueItem } from "./model.js";
|
|
3
|
+
export interface SketchHandoffEntry {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly revision: number;
|
|
6
|
+
readonly metadata: AgentSketchMetadata;
|
|
7
|
+
readonly annotatedImage: Blob;
|
|
8
|
+
}
|
|
9
|
+
export interface SketchClipboardHandoffSnapshot {
|
|
10
|
+
readonly localBatchId: string;
|
|
11
|
+
readonly entries: readonly SketchHandoffEntry[];
|
|
12
|
+
}
|
|
13
|
+
export declare function createSketchHandoffSnapshot(items: readonly SketchQueueItem[]): SketchClipboardHandoffSnapshot | null;
|
|
14
|
+
export declare function createSketchAttachments(handoff: SketchClipboardHandoffSnapshot): Promise<AgentSketchAttachment[]>;
|
|
15
|
+
export declare function sketchMetadataForHandoff(handoff: SketchClipboardHandoffSnapshot | null): readonly AgentSketchMetadata[];
|
|
16
|
+
export declare function getSketchClipboardHandoffSnapshot(): SketchClipboardHandoffSnapshot | null;
|
|
17
|
+
export declare function subscribeSketchClipboardHandoff(listener: () => void): () => void;
|
|
18
|
+
export declare function useSketchClipboardHandoff(): SketchClipboardHandoffSnapshot | null;
|
|
19
|
+
export declare function recordSketchClipboardHandoff(snapshot: SketchClipboardHandoffSnapshot | null): void;
|
|
20
|
+
export declare function clearSketchClipboardHandoff(): void;
|
|
21
|
+
export declare function resetSketchClipboardHandoff(): void;
|