bringing back scroll constraints debug
This commit is contained in:
parent
4208c97b62
commit
baa7b3293a
@ -51,7 +51,10 @@ import {
|
|||||||
import { isElementLink } from "@excalidraw/element/elementLink";
|
import { isElementLink } from "@excalidraw/element/elementLink";
|
||||||
import { restore, restoreAppState } from "@excalidraw/excalidraw/data/restore";
|
import { restore, restoreAppState } from "@excalidraw/excalidraw/data/restore";
|
||||||
import { newElementWith } from "@excalidraw/element/mutateElement";
|
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||||
import { isInitializedImageElement } from "@excalidraw/element/typeChecks";
|
import {
|
||||||
|
isFrameLikeElement,
|
||||||
|
isInitializedImageElement,
|
||||||
|
} from "@excalidraw/element/typeChecks";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {
|
import {
|
||||||
parseLibraryTokensFromUrl,
|
parseLibraryTokensFromUrl,
|
||||||
@ -142,6 +145,10 @@ import "./index.scss";
|
|||||||
|
|
||||||
import type { CollabAPI } from "./collab/Collab";
|
import type { CollabAPI } from "./collab/Collab";
|
||||||
import { getSelectedElements } from "@excalidraw/element/selection";
|
import { getSelectedElements } from "@excalidraw/element/selection";
|
||||||
|
import {
|
||||||
|
decodeConstraints,
|
||||||
|
encodeConstraints,
|
||||||
|
} from "@excalidraw/excalidraw/scene/scrollConstraints";
|
||||||
|
|
||||||
polyfill();
|
polyfill();
|
||||||
|
|
||||||
@ -160,19 +167,37 @@ const ConstraintsSettings = ({
|
|||||||
const [constraints, setConstraints] =
|
const [constraints, setConstraints] =
|
||||||
useState<DebugScrollConstraints>(initialConstraints);
|
useState<DebugScrollConstraints>(initialConstraints);
|
||||||
|
|
||||||
|
const frames = excalidrawAPI
|
||||||
|
.getSceneElements()
|
||||||
|
.filter((e) => isFrameLikeElement(e));
|
||||||
|
const [activeFrameId, setActiveFrameId] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// add JSON-stringified constraints into url hash for easy sharing
|
|
||||||
const hash = new URLSearchParams(window.location.hash.slice(1));
|
const hash = new URLSearchParams(window.location.hash.slice(1));
|
||||||
hash.set(
|
hash.set("constraints", encodeConstraints(constraints));
|
||||||
"constraints",
|
|
||||||
encodeURIComponent(
|
|
||||||
window.btoa(JSON.stringify(constraints)).replace(/=+/, ""),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
window.location.hash = decodeURIComponent(hash.toString());
|
window.location.hash = decodeURIComponent(hash.toString());
|
||||||
excalidrawAPI.setScrollConstraints(constraints);
|
|
||||||
|
constraints.enabled
|
||||||
|
? excalidrawAPI.setScrollConstraints(constraints)
|
||||||
|
: excalidrawAPI.setScrollConstraints(null);
|
||||||
}, [constraints]);
|
}, [constraints]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const frame = frames.find((frame) => frame.id === activeFrameId);
|
||||||
|
if (frame) {
|
||||||
|
const { x, y, width, height } = frame;
|
||||||
|
setConstraints((s) => ({
|
||||||
|
x: Math.round(x),
|
||||||
|
y: Math.round(y),
|
||||||
|
width: Math.round(width),
|
||||||
|
height: Math.round(height),
|
||||||
|
enabled: s.enabled,
|
||||||
|
viewportZoomFactor: s.viewportZoomFactor,
|
||||||
|
lockZoom: s.lockZoom,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [activeFrameId]);
|
||||||
|
|
||||||
const [selection, setSelection] = useState<ExcalidrawElement[]>([]);
|
const [selection, setSelection] = useState<ExcalidrawElement[]>([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return excalidrawAPI.onChange((elements, appState) => {
|
return excalidrawAPI.onChange((elements, appState) => {
|
||||||
@ -183,109 +208,179 @@ const ConstraintsSettings = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
bottom: 10,
|
bottom: 10,
|
||||||
left: "calc(50%)",
|
left: "calc(50%)",
|
||||||
transform: "translateX(-50%)",
|
transform: "translateX(-50%)",
|
||||||
gap: "0.6rem",
|
|
||||||
zIndex: 999999,
|
zIndex: 999999,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "0.5rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
enabled:{" "}
|
<div
|
||||||
<input
|
style={{
|
||||||
type="checkbox"
|
display: "flex",
|
||||||
defaultChecked={!!constraints.enabled}
|
gap: "0.6rem",
|
||||||
onChange={(e) =>
|
}}
|
||||||
setConstraints((s) => ({ ...s, enabled: e.target.checked }))
|
>
|
||||||
}
|
enabled:{" "}
|
||||||
/>
|
<input
|
||||||
x:{" "}
|
type="checkbox"
|
||||||
<input
|
defaultChecked={!!constraints.enabled}
|
||||||
placeholder="x"
|
onChange={(e) =>
|
||||||
size={4}
|
setConstraints((s) => ({ ...s, enabled: e.target.checked }))
|
||||||
value={constraints.x.toString()}
|
}
|
||||||
onChange={(e) =>
|
/>
|
||||||
setConstraints((s) => ({
|
x:{" "}
|
||||||
...s,
|
<input
|
||||||
x: parseInt(e.target.value) ?? 0,
|
placeholder="x"
|
||||||
}))
|
size={4}
|
||||||
}
|
value={constraints.x.toString()}
|
||||||
/>
|
onChange={(e) =>
|
||||||
y:{" "}
|
|
||||||
<input
|
|
||||||
placeholder="y"
|
|
||||||
size={4}
|
|
||||||
value={constraints.y.toString()}
|
|
||||||
onChange={(e) =>
|
|
||||||
setConstraints((s) => ({
|
|
||||||
...s,
|
|
||||||
y: parseInt(e.target.value) ?? 0,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
w:{" "}
|
|
||||||
<input
|
|
||||||
placeholder="width"
|
|
||||||
size={4}
|
|
||||||
value={constraints.width.toString()}
|
|
||||||
onChange={(e) =>
|
|
||||||
setConstraints((s) => ({
|
|
||||||
...s,
|
|
||||||
width: parseInt(e.target.value) ?? 200,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
h:{" "}
|
|
||||||
<input
|
|
||||||
placeholder="height"
|
|
||||||
size={4}
|
|
||||||
value={constraints.height.toString()}
|
|
||||||
onChange={(e) =>
|
|
||||||
setConstraints((s) => ({
|
|
||||||
...s,
|
|
||||||
height: parseInt(e.target.value) ?? 200,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
zoomFactor:
|
|
||||||
<input
|
|
||||||
placeholder="height"
|
|
||||||
type="number"
|
|
||||||
min="0.1"
|
|
||||||
max="1"
|
|
||||||
step="0.1"
|
|
||||||
value={constraints.viewportZoomFactor.toString()}
|
|
||||||
onChange={(e) =>
|
|
||||||
setConstraints((s) => ({
|
|
||||||
...s,
|
|
||||||
viewportZoomFactor: parseFloat(e.target.value.toString()) ?? 0.7,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
lockZoom:{" "}
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
defaultChecked={!!constraints.lockZoom}
|
|
||||||
onChange={(e) =>
|
|
||||||
setConstraints((s) => ({ ...s, lockZoom: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{selection.length > 0 && (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
const bbox = getCommonBounds(selection);
|
|
||||||
setConstraints((s) => ({
|
setConstraints((s) => ({
|
||||||
...s,
|
...s,
|
||||||
x: Math.round(bbox[0]),
|
x: parseInt(e.target.value) ?? 0,
|
||||||
y: Math.round(bbox[1]),
|
}))
|
||||||
width: Math.round(bbox[2] - bbox[0]),
|
}
|
||||||
height: Math.round(bbox[3] - bbox[1]),
|
/>
|
||||||
}));
|
y:{" "}
|
||||||
|
<input
|
||||||
|
placeholder="y"
|
||||||
|
size={4}
|
||||||
|
value={constraints.y.toString()}
|
||||||
|
onChange={(e) =>
|
||||||
|
setConstraints((s) => ({
|
||||||
|
...s,
|
||||||
|
y: parseInt(e.target.value) ?? 0,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
w:{" "}
|
||||||
|
<input
|
||||||
|
placeholder="width"
|
||||||
|
size={4}
|
||||||
|
value={constraints.width.toString()}
|
||||||
|
onChange={(e) =>
|
||||||
|
setConstraints((s) => ({
|
||||||
|
...s,
|
||||||
|
width: parseInt(e.target.value) ?? 200,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
h:{" "}
|
||||||
|
<input
|
||||||
|
placeholder="height"
|
||||||
|
size={4}
|
||||||
|
value={constraints.height.toString()}
|
||||||
|
onChange={(e) =>
|
||||||
|
setConstraints((s) => ({
|
||||||
|
...s,
|
||||||
|
height: parseInt(e.target.value) ?? 200,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
zoomFactor:
|
||||||
|
<input
|
||||||
|
placeholder="height"
|
||||||
|
type="number"
|
||||||
|
min="0.1"
|
||||||
|
max="1"
|
||||||
|
step="0.1"
|
||||||
|
value={constraints.viewportZoomFactor.toString()}
|
||||||
|
onChange={(e) =>
|
||||||
|
setConstraints((s) => ({
|
||||||
|
...s,
|
||||||
|
viewportZoomFactor: parseFloat(e.target.value.toString()) ?? 0.7,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
overscrollAllowance:
|
||||||
|
<input
|
||||||
|
placeholder="height"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.1"
|
||||||
|
value={constraints.overscrollAllowance?.toString()}
|
||||||
|
onChange={(e) =>
|
||||||
|
setConstraints((s) => ({
|
||||||
|
...s,
|
||||||
|
overscrollAllowance: parseFloat(e.target.value.toString()) ?? 0.5,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
lockZoom:{" "}
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
defaultChecked={!!constraints.lockZoom}
|
||||||
|
onChange={(e) =>
|
||||||
|
setConstraints((s) => ({ ...s, lockZoom: e.target.checked }))
|
||||||
|
}
|
||||||
|
value={constraints.lockZoom?.toString()}
|
||||||
|
/>
|
||||||
|
{selection.length > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const bbox = getCommonBounds(selection);
|
||||||
|
setConstraints((s) => ({
|
||||||
|
...s,
|
||||||
|
x: Math.round(bbox[0]),
|
||||||
|
y: Math.round(bbox[1]),
|
||||||
|
width: Math.round(bbox[2] - bbox[0]),
|
||||||
|
height: Math.round(bbox[3] - bbox[1]),
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
use selection
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{frames.length > 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "0.6rem",
|
||||||
|
flexDirection: "row",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
use selection
|
<button
|
||||||
</button>
|
onClick={() => {
|
||||||
|
const currentIndex = frames.findIndex(
|
||||||
|
(frame) => frame.id === activeFrameId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentIndex === -1) {
|
||||||
|
setActiveFrameId(frames[frames.length - 1].id);
|
||||||
|
} else {
|
||||||
|
const nextIndex =
|
||||||
|
(currentIndex - 1 + frames.length) % frames.length;
|
||||||
|
setActiveFrameId(frames[nextIndex].id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Prev
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const currentIndex = frames.findIndex(
|
||||||
|
(frame) => frame.id === activeFrameId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentIndex === -1) {
|
||||||
|
setActiveFrameId(frames[0].id);
|
||||||
|
} else {
|
||||||
|
const nextIndex = (currentIndex + 1) % frames.length;
|
||||||
|
setActiveFrameId(frames[nextIndex].id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -909,8 +1004,10 @@ const ExcalidrawWrapper = () => {
|
|||||||
let storedConstraints = {};
|
let storedConstraints = {};
|
||||||
if (stored) {
|
if (stored) {
|
||||||
try {
|
try {
|
||||||
storedConstraints = JSON.parse(window.atob(stored));
|
storedConstraints = decodeConstraints(stored);
|
||||||
} catch {}
|
} catch {
|
||||||
|
console.error("Invalid scroll constraints in URL");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -920,13 +1017,12 @@ const ExcalidrawWrapper = () => {
|
|||||||
height: document.body.clientHeight,
|
height: document.body.clientHeight,
|
||||||
lockZoom: false,
|
lockZoom: false,
|
||||||
viewportZoomFactor: 0.7,
|
viewportZoomFactor: 0.7,
|
||||||
|
overscrollAllowance: 0.5,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
...storedConstraints,
|
...storedConstraints,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(constraints);
|
|
||||||
|
|
||||||
// browsers generally prevent infinite self-embedding, there are
|
// browsers generally prevent infinite self-embedding, there are
|
||||||
// cases where it still happens, and while we disallow self-embedding
|
// cases where it still happens, and while we disallow self-embedding
|
||||||
// by not whitelisting our own origin, this serves as an additional guard
|
// by not whitelisting our own origin, this serves as an additional guard
|
||||||
|
@ -21,7 +21,6 @@ import { getNormalizedZoom } from "./normalize";
|
|||||||
export const calculateConstrainedScrollCenter = (
|
export const calculateConstrainedScrollCenter = (
|
||||||
state: AppState,
|
state: AppState,
|
||||||
{ scrollX, scrollY }: Pick<AppState, "scrollX" | "scrollY">,
|
{ scrollX, scrollY }: Pick<AppState, "scrollX" | "scrollY">,
|
||||||
overscrollAllowance?: number,
|
|
||||||
): {
|
): {
|
||||||
scrollX: AppState["scrollX"];
|
scrollX: AppState["scrollX"];
|
||||||
scrollY: AppState["scrollY"];
|
scrollY: AppState["scrollY"];
|
||||||
@ -59,7 +58,6 @@ export const calculateConstrainedScrollCenter = (
|
|||||||
height,
|
height,
|
||||||
zoom: _zoom,
|
zoom: _zoom,
|
||||||
allowOverscroll: false,
|
allowOverscroll: false,
|
||||||
overscrollAllowance,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -69,6 +67,8 @@ export const calculateConstrainedScrollCenter = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DEFAULT_OVERSCROLL_ALLOWANCE = 0.2;
|
||||||
|
|
||||||
interface EncodedConstraints {
|
interface EncodedConstraints {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
@ -76,6 +76,8 @@ interface EncodedConstraints {
|
|||||||
h: number;
|
h: number;
|
||||||
l: boolean;
|
l: boolean;
|
||||||
v: number;
|
v: number;
|
||||||
|
// overscrollAllowance
|
||||||
|
oa: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -91,6 +93,7 @@ export const encodeConstraints = (constraints: ScrollConstraints): string => {
|
|||||||
h: constraints.height,
|
h: constraints.height,
|
||||||
l: !!constraints.lockZoom,
|
l: !!constraints.lockZoom,
|
||||||
v: constraints.viewportZoomFactor ?? 1,
|
v: constraints.viewportZoomFactor ?? 1,
|
||||||
|
oa: constraints.overscrollAllowance ?? DEFAULT_OVERSCROLL_ALLOWANCE,
|
||||||
};
|
};
|
||||||
|
|
||||||
const serialized = JSON.stringify(payload);
|
const serialized = JSON.stringify(payload);
|
||||||
@ -115,6 +118,7 @@ export const decodeConstraints = (encoded: string): ScrollConstraints => {
|
|||||||
lockZoom: parsed.l ?? false,
|
lockZoom: parsed.l ?? false,
|
||||||
viewportZoomFactor: parsed.v ?? 1,
|
viewportZoomFactor: parsed.v ?? 1,
|
||||||
animateOnNextUpdate: false,
|
animateOnNextUpdate: false,
|
||||||
|
overscrollAllowance: parsed.oa ?? DEFAULT_OVERSCROLL_ALLOWANCE,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// return safe defaults if decoding fails
|
// return safe defaults if decoding fails
|
||||||
@ -170,22 +174,21 @@ const calculateConstraints = ({
|
|||||||
height,
|
height,
|
||||||
zoom,
|
zoom,
|
||||||
allowOverscroll,
|
allowOverscroll,
|
||||||
overscrollAllowance,
|
|
||||||
}: {
|
}: {
|
||||||
scrollConstraints: ScrollConstraints;
|
scrollConstraints: ScrollConstraints;
|
||||||
width: AppState["width"];
|
width: AppState["width"];
|
||||||
height: AppState["height"];
|
height: AppState["height"];
|
||||||
zoom: AppState["zoom"];
|
zoom: AppState["zoom"];
|
||||||
allowOverscroll: boolean;
|
allowOverscroll: boolean;
|
||||||
overscrollAllowance?: number;
|
|
||||||
}) => {
|
}) => {
|
||||||
// Validate the overscroll allowance percentage
|
// Validate the overscroll allowance percentage
|
||||||
|
const overscrollAllowance = scrollConstraints.overscrollAllowance;
|
||||||
const validatedOverscroll =
|
const validatedOverscroll =
|
||||||
overscrollAllowance != null &&
|
overscrollAllowance != null &&
|
||||||
overscrollAllowance >= 0 &&
|
overscrollAllowance >= 0 &&
|
||||||
overscrollAllowance <= 1
|
overscrollAllowance <= 1
|
||||||
? overscrollAllowance
|
? overscrollAllowance
|
||||||
: 0.2;
|
: DEFAULT_OVERSCROLL_ALLOWANCE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the center position of the constrained scroll area.
|
* Calculates the center position of the constrained scroll area.
|
||||||
|
@ -911,6 +911,7 @@ export type ScrollConstraints = {
|
|||||||
animateOnNextUpdate?: boolean;
|
animateOnNextUpdate?: boolean;
|
||||||
viewportZoomFactor?: number;
|
viewportZoomFactor?: number;
|
||||||
lockZoom?: boolean;
|
lockZoom?: boolean;
|
||||||
|
overscrollAllowance?: number;
|
||||||
};
|
};
|
||||||
export type PendingExcalidrawElements = ExcalidrawElement[];
|
export type PendingExcalidrawElements = ExcalidrawElement[];
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user