Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3d1dee9d0 | ||
|
|
e3f31df747 | ||
|
|
60e75406e0 | ||
|
|
b396e07b90 | ||
|
|
2d1d84a47b | ||
|
|
ee30225062 | ||
|
|
16cae4fc07 | ||
|
|
576bc0dbe5 | ||
|
|
00af35c692 | ||
|
|
ea7c702cfc |
@@ -17,8 +17,6 @@ VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","a
|
|||||||
# put these in your .env.local, or make sure you don't commit!
|
# put these in your .env.local, or make sure you don't commit!
|
||||||
# must be lowercase `true` when turned on
|
# must be lowercase `true` when turned on
|
||||||
#
|
#
|
||||||
# whether to enable Service Workers in development
|
|
||||||
VITE_APP_DEV_ENABLE_SW=
|
|
||||||
# whether to disable live reload / HMR. Usuaully what you want to do when
|
# whether to disable live reload / HMR. Usuaully what you want to do when
|
||||||
# debugging Service Workers.
|
# debugging Service Workers.
|
||||||
VITE_APP_DEV_DISABLE_LIVE_RELOAD=
|
VITE_APP_DEV_DISABLE_LIVE_RELOAD=
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Here is a [complete list](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/components/mainMenu/DefaultItems.tsx) of the default items.
|
Here is a [complete list](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/components/main-menu/DefaultItems.tsx) of the default items.
|
||||||
|
|
||||||
### MainMenu.Group
|
### MainMenu.Group
|
||||||
|
|
||||||
|
|||||||
+38
-1
@@ -120,6 +120,11 @@ import {
|
|||||||
import { appThemeAtom, useHandleAppTheme } from "./useHandleAppTheme";
|
import { appThemeAtom, useHandleAppTheme } from "./useHandleAppTheme";
|
||||||
import { getPreferredLanguage } from "./app-language/language-detector";
|
import { getPreferredLanguage } from "./app-language/language-detector";
|
||||||
import { useAppLangCode } from "./app-language/language-state";
|
import { useAppLangCode } from "./app-language/language-state";
|
||||||
|
import DebugCanvas, {
|
||||||
|
debugRenderer,
|
||||||
|
isVisualDebuggerEnabled,
|
||||||
|
loadSavedDebugState,
|
||||||
|
} from "./components/DebugCanvas";
|
||||||
import { AIComponents } from "./components/AI";
|
import { AIComponents } from "./components/AI";
|
||||||
|
|
||||||
polyfill();
|
polyfill();
|
||||||
@@ -337,6 +342,8 @@ const ExcalidrawWrapper = () => {
|
|||||||
resolvablePromise<ExcalidrawInitialDataState | null>();
|
resolvablePromise<ExcalidrawInitialDataState | null>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const debugCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
trackEvent("load", "frame", getFrame());
|
trackEvent("load", "frame", getFrame());
|
||||||
// Delayed so that the app has a time to load the latest SW
|
// Delayed so that the app has a time to load the latest SW
|
||||||
@@ -362,6 +369,23 @@ const ExcalidrawWrapper = () => {
|
|||||||
migrationAdapter: LibraryLocalStorageMigrationAdapter,
|
migrationAdapter: LibraryLocalStorageMigrationAdapter,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [, forceRefresh] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
const debugState = loadSavedDebugState();
|
||||||
|
|
||||||
|
if (debugState.enabled && !window.visualDebug) {
|
||||||
|
window.visualDebug = {
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
delete window.visualDebug;
|
||||||
|
}
|
||||||
|
forceRefresh((prev) => !prev);
|
||||||
|
}
|
||||||
|
}, [excalidrawAPI]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!excalidrawAPI || (!isCollabDisabled && !collabAPI)) {
|
if (!excalidrawAPI || (!isCollabDisabled && !collabAPI)) {
|
||||||
return;
|
return;
|
||||||
@@ -622,6 +646,11 @@ const ExcalidrawWrapper = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render the debug scene if the debug canvas is available
|
||||||
|
if (debugCanvasRef.current && excalidrawAPI) {
|
||||||
|
debugRenderer(debugCanvasRef.current, appState, window.devicePixelRatio);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const [latestShareableLink, setLatestShareableLink] = useState<string | null>(
|
const [latestShareableLink, setLatestShareableLink] = useState<string | null>(
|
||||||
@@ -820,6 +849,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
isCollabEnabled={!isCollabDisabled}
|
isCollabEnabled={!isCollabDisabled}
|
||||||
theme={appTheme}
|
theme={appTheme}
|
||||||
setTheme={(theme) => setAppTheme(theme)}
|
setTheme={(theme) => setAppTheme(theme)}
|
||||||
|
refresh={() => forceRefresh((prev) => !prev)}
|
||||||
/>
|
/>
|
||||||
<AppWelcomeScreen
|
<AppWelcomeScreen
|
||||||
onCollabDialogOpen={onCollabDialogOpen}
|
onCollabDialogOpen={onCollabDialogOpen}
|
||||||
@@ -845,7 +875,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
</OverwriteConfirmDialog.Action>
|
</OverwriteConfirmDialog.Action>
|
||||||
)}
|
)}
|
||||||
</OverwriteConfirmDialog>
|
</OverwriteConfirmDialog>
|
||||||
<AppFooter />
|
<AppFooter onChange={() => excalidrawAPI?.refresh()} />
|
||||||
{excalidrawAPI && <AIComponents excalidrawAPI={excalidrawAPI} />}
|
{excalidrawAPI && <AIComponents excalidrawAPI={excalidrawAPI} />}
|
||||||
|
|
||||||
<TTDDialogTrigger />
|
<TTDDialogTrigger />
|
||||||
@@ -1077,6 +1107,13 @@ const ExcalidrawWrapper = () => {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
{isVisualDebuggerEnabled() && excalidrawAPI && (
|
||||||
|
<DebugCanvas
|
||||||
|
appState={excalidrawAPI.getAppState()}
|
||||||
|
scale={window.devicePixelRatio}
|
||||||
|
ref={debugCanvasRef}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Excalidraw>
|
</Excalidraw>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export const STORAGE_KEYS = {
|
|||||||
LOCAL_STORAGE_APP_STATE: "excalidraw-state",
|
LOCAL_STORAGE_APP_STATE: "excalidraw-state",
|
||||||
LOCAL_STORAGE_COLLAB: "excalidraw-collab",
|
LOCAL_STORAGE_COLLAB: "excalidraw-collab",
|
||||||
LOCAL_STORAGE_THEME: "excalidraw-theme",
|
LOCAL_STORAGE_THEME: "excalidraw-theme",
|
||||||
|
LOCAL_STORAGE_DEBUG: "excalidraw-debug",
|
||||||
VERSION_DATA_STATE: "version-dataState",
|
VERSION_DATA_STATE: "version-dataState",
|
||||||
VERSION_FILES: "version-files",
|
VERSION_FILES: "version-files",
|
||||||
|
|
||||||
|
|||||||
@@ -3,23 +3,27 @@ import { Footer } from "../../packages/excalidraw/index";
|
|||||||
import { EncryptedIcon } from "./EncryptedIcon";
|
import { EncryptedIcon } from "./EncryptedIcon";
|
||||||
import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink";
|
import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink";
|
||||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||||
|
import { DebugFooter, isVisualDebuggerEnabled } from "./DebugCanvas";
|
||||||
|
|
||||||
export const AppFooter = React.memo(() => {
|
export const AppFooter = React.memo(
|
||||||
return (
|
({ onChange }: { onChange: () => void }) => {
|
||||||
<Footer>
|
return (
|
||||||
<div
|
<Footer>
|
||||||
style={{
|
<div
|
||||||
display: "flex",
|
style={{
|
||||||
gap: ".5rem",
|
display: "flex",
|
||||||
alignItems: "center",
|
gap: ".5rem",
|
||||||
}}
|
alignItems: "center",
|
||||||
>
|
}}
|
||||||
{isExcalidrawPlusSignedUser ? (
|
>
|
||||||
<ExcalidrawPlusAppLink />
|
{isVisualDebuggerEnabled() && <DebugFooter onChange={onChange} />}
|
||||||
) : (
|
{isExcalidrawPlusSignedUser ? (
|
||||||
<EncryptedIcon />
|
<ExcalidrawPlusAppLink />
|
||||||
)}
|
) : (
|
||||||
</div>
|
<EncryptedIcon />
|
||||||
</Footer>
|
)}
|
||||||
);
|
</div>
|
||||||
});
|
</Footer>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import React from "react";
|
|||||||
import {
|
import {
|
||||||
loginIcon,
|
loginIcon,
|
||||||
ExcalLogo,
|
ExcalLogo,
|
||||||
|
eyeIcon,
|
||||||
} from "../../packages/excalidraw/components/icons";
|
} from "../../packages/excalidraw/components/icons";
|
||||||
import type { Theme } from "../../packages/excalidraw/element/types";
|
import type { Theme } from "../../packages/excalidraw/element/types";
|
||||||
import { MainMenu } from "../../packages/excalidraw/index";
|
import { MainMenu } from "../../packages/excalidraw/index";
|
||||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||||
import { LanguageList } from "../app-language/LanguageList";
|
import { LanguageList } from "../app-language/LanguageList";
|
||||||
|
import { saveDebugState } from "./DebugCanvas";
|
||||||
|
|
||||||
export const AppMainMenu: React.FC<{
|
export const AppMainMenu: React.FC<{
|
||||||
onCollabDialogOpen: () => any;
|
onCollabDialogOpen: () => any;
|
||||||
@@ -14,6 +16,7 @@ export const AppMainMenu: React.FC<{
|
|||||||
isCollabEnabled: boolean;
|
isCollabEnabled: boolean;
|
||||||
theme: Theme | "system";
|
theme: Theme | "system";
|
||||||
setTheme: (theme: Theme | "system") => void;
|
setTheme: (theme: Theme | "system") => void;
|
||||||
|
refresh: () => void;
|
||||||
}> = React.memo((props) => {
|
}> = React.memo((props) => {
|
||||||
return (
|
return (
|
||||||
<MainMenu>
|
<MainMenu>
|
||||||
@@ -50,6 +53,23 @@ export const AppMainMenu: React.FC<{
|
|||||||
>
|
>
|
||||||
{isExcalidrawPlusSignedUser ? "Sign in" : "Sign up"}
|
{isExcalidrawPlusSignedUser ? "Sign in" : "Sign up"}
|
||||||
</MainMenu.ItemLink>
|
</MainMenu.ItemLink>
|
||||||
|
{import.meta.env.DEV && (
|
||||||
|
<MainMenu.Item
|
||||||
|
icon={eyeIcon}
|
||||||
|
onClick={() => {
|
||||||
|
if (window.visualDebug) {
|
||||||
|
delete window.visualDebug;
|
||||||
|
saveDebugState({ enabled: false });
|
||||||
|
} else {
|
||||||
|
window.visualDebug = { data: [] };
|
||||||
|
saveDebugState({ enabled: true });
|
||||||
|
}
|
||||||
|
props?.refresh();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Visual Debug
|
||||||
|
</MainMenu.Item>
|
||||||
|
)}
|
||||||
<MainMenu.Separator />
|
<MainMenu.Separator />
|
||||||
<MainMenu.DefaultItems.ToggleTheme
|
<MainMenu.DefaultItems.ToggleTheme
|
||||||
allowSystemTheme
|
allowSystemTheme
|
||||||
|
|||||||
@@ -0,0 +1,293 @@
|
|||||||
|
import { forwardRef, useCallback, useImperativeHandle, useRef } from "react";
|
||||||
|
import { type AppState } from "../../packages/excalidraw/types";
|
||||||
|
import { throttleRAF } from "../../packages/excalidraw/utils";
|
||||||
|
import type { LineSegment } from "../../packages/utils";
|
||||||
|
import {
|
||||||
|
bootstrapCanvas,
|
||||||
|
getNormalizedCanvasDimensions,
|
||||||
|
} from "../../packages/excalidraw/renderer/helpers";
|
||||||
|
import type { DebugElement } from "../../packages/excalidraw/visualdebug";
|
||||||
|
import {
|
||||||
|
ArrowheadArrowIcon,
|
||||||
|
CloseIcon,
|
||||||
|
TrashIcon,
|
||||||
|
} from "../../packages/excalidraw/components/icons";
|
||||||
|
import { STORAGE_KEYS } from "../app_constants";
|
||||||
|
import { isLineSegment } from "../../packages/excalidraw/element/typeChecks";
|
||||||
|
|
||||||
|
const renderLine = (
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
zoom: number,
|
||||||
|
segment: LineSegment,
|
||||||
|
color: string,
|
||||||
|
) => {
|
||||||
|
context.save();
|
||||||
|
context.strokeStyle = color;
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(segment[0][0] * zoom, segment[0][1] * zoom);
|
||||||
|
context.lineTo(segment[1][0] * zoom, segment[1][1] * zoom);
|
||||||
|
context.stroke();
|
||||||
|
context.restore();
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderOrigin = (context: CanvasRenderingContext2D, zoom: number) => {
|
||||||
|
context.strokeStyle = "#888";
|
||||||
|
context.save();
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(-10 * zoom, -10 * zoom);
|
||||||
|
context.lineTo(10 * zoom, 10 * zoom);
|
||||||
|
context.moveTo(10 * zoom, -10 * zoom);
|
||||||
|
context.lineTo(-10 * zoom, 10 * zoom);
|
||||||
|
context.stroke();
|
||||||
|
context.save();
|
||||||
|
};
|
||||||
|
|
||||||
|
const render = (
|
||||||
|
frame: DebugElement[],
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
appState: AppState,
|
||||||
|
) => {
|
||||||
|
frame.forEach((el) => {
|
||||||
|
switch (true) {
|
||||||
|
case isLineSegment(el.data):
|
||||||
|
renderLine(context, appState.zoom.value, el.data, el.color);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const _debugRenderer = (
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
appState: AppState,
|
||||||
|
scale: number,
|
||||||
|
) => {
|
||||||
|
const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
|
||||||
|
canvas,
|
||||||
|
scale,
|
||||||
|
);
|
||||||
|
|
||||||
|
const context = bootstrapCanvas({
|
||||||
|
canvas,
|
||||||
|
scale,
|
||||||
|
normalizedWidth,
|
||||||
|
normalizedHeight,
|
||||||
|
viewBackgroundColor: "transparent",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply zoom
|
||||||
|
context.save();
|
||||||
|
context.translate(
|
||||||
|
appState.scrollX * appState.zoom.value,
|
||||||
|
appState.scrollY * appState.zoom.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
renderOrigin(context, appState.zoom.value);
|
||||||
|
|
||||||
|
if (
|
||||||
|
window.visualDebug?.currentFrame &&
|
||||||
|
window.visualDebug?.data &&
|
||||||
|
window.visualDebug.data.length > 0
|
||||||
|
) {
|
||||||
|
// Render only one frame
|
||||||
|
const [idx] = debugFrameData();
|
||||||
|
|
||||||
|
render(window.visualDebug.data[idx], context, appState);
|
||||||
|
} else {
|
||||||
|
// Render all debug frames
|
||||||
|
window.visualDebug?.data.forEach((frame) => {
|
||||||
|
render(frame, context, appState);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.visualDebug) {
|
||||||
|
window.visualDebug!.data =
|
||||||
|
window.visualDebug?.data.map((frame) =>
|
||||||
|
frame.filter((el) => el.permanent),
|
||||||
|
) ?? [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const debugFrameData = (): [number, number] => {
|
||||||
|
const currentFrame = window.visualDebug?.currentFrame ?? 0;
|
||||||
|
const frameCount = window.visualDebug?.data.length ?? 0;
|
||||||
|
|
||||||
|
if (frameCount > 0) {
|
||||||
|
return [currentFrame % frameCount, window.visualDebug?.currentFrame ?? 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [0, 0];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveDebugState = (debug: { enabled: boolean }) => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
STORAGE_KEYS.LOCAL_STORAGE_DEBUG,
|
||||||
|
JSON.stringify(debug),
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const debugRenderer = throttleRAF(
|
||||||
|
(canvas: HTMLCanvasElement, appState: AppState, scale: number) => {
|
||||||
|
_debugRenderer(canvas, appState, scale);
|
||||||
|
},
|
||||||
|
{ trailing: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const loadSavedDebugState = () => {
|
||||||
|
let debug;
|
||||||
|
try {
|
||||||
|
const savedDebugState = localStorage.getItem(
|
||||||
|
STORAGE_KEYS.LOCAL_STORAGE_DEBUG,
|
||||||
|
);
|
||||||
|
if (savedDebugState) {
|
||||||
|
debug = JSON.parse(savedDebugState) as { enabled: boolean };
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return debug ?? { enabled: false };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isVisualDebuggerEnabled = () =>
|
||||||
|
Array.isArray(window.visualDebug?.data);
|
||||||
|
|
||||||
|
export const DebugFooter = ({ onChange }: { onChange: () => void }) => {
|
||||||
|
const moveForward = useCallback(() => {
|
||||||
|
if (
|
||||||
|
!window.visualDebug?.currentFrame ||
|
||||||
|
isNaN(window.visualDebug?.currentFrame ?? -1)
|
||||||
|
) {
|
||||||
|
window.visualDebug!.currentFrame = 0;
|
||||||
|
}
|
||||||
|
window.visualDebug!.currentFrame += 1;
|
||||||
|
onChange();
|
||||||
|
}, [onChange]);
|
||||||
|
const moveBackward = useCallback(() => {
|
||||||
|
if (
|
||||||
|
!window.visualDebug?.currentFrame ||
|
||||||
|
isNaN(window.visualDebug?.currentFrame ?? -1) ||
|
||||||
|
window.visualDebug?.currentFrame < 1
|
||||||
|
) {
|
||||||
|
window.visualDebug!.currentFrame = 1;
|
||||||
|
}
|
||||||
|
window.visualDebug!.currentFrame -= 1;
|
||||||
|
onChange();
|
||||||
|
}, [onChange]);
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
window.visualDebug!.currentFrame = undefined;
|
||||||
|
onChange();
|
||||||
|
}, [onChange]);
|
||||||
|
const trashFrames = useCallback(() => {
|
||||||
|
if (window.visualDebug) {
|
||||||
|
window.visualDebug.currentFrame = undefined;
|
||||||
|
window.visualDebug.data = [];
|
||||||
|
}
|
||||||
|
onChange();
|
||||||
|
}, [onChange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className="ToolIcon_type_button"
|
||||||
|
data-testid="debug-forward"
|
||||||
|
aria-label="Move forward"
|
||||||
|
type="button"
|
||||||
|
onClick={trashFrames}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="ToolIcon__icon"
|
||||||
|
aria-hidden="true"
|
||||||
|
aria-disabled="false"
|
||||||
|
>
|
||||||
|
{TrashIcon}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="ToolIcon_type_button"
|
||||||
|
data-testid="debug-forward"
|
||||||
|
aria-label="Move forward"
|
||||||
|
type="button"
|
||||||
|
onClick={moveBackward}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="ToolIcon__icon"
|
||||||
|
aria-hidden="true"
|
||||||
|
aria-disabled="false"
|
||||||
|
>
|
||||||
|
<ArrowheadArrowIcon flip />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="ToolIcon_type_button"
|
||||||
|
data-testid="debug-forward"
|
||||||
|
aria-label="Move forward"
|
||||||
|
type="button"
|
||||||
|
onClick={reset}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="ToolIcon__icon"
|
||||||
|
aria-hidden="true"
|
||||||
|
aria-disabled="false"
|
||||||
|
>
|
||||||
|
{CloseIcon}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="ToolIcon_type_button"
|
||||||
|
data-testid="debug-backward"
|
||||||
|
aria-label="Move backward"
|
||||||
|
type="button"
|
||||||
|
onClick={moveForward}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="ToolIcon__icon"
|
||||||
|
aria-hidden="true"
|
||||||
|
aria-disabled="false"
|
||||||
|
>
|
||||||
|
<ArrowheadArrowIcon />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DebugCanvasProps {
|
||||||
|
appState: AppState;
|
||||||
|
scale: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DebugCanvas = forwardRef<HTMLCanvasElement, DebugCanvasProps>(
|
||||||
|
({ appState, scale }, ref) => {
|
||||||
|
const { width, height } = appState;
|
||||||
|
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
useImperativeHandle<HTMLCanvasElement | null, HTMLCanvasElement | null>(
|
||||||
|
ref,
|
||||||
|
() => canvasRef.current,
|
||||||
|
[canvasRef],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
style={{
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
position: "absolute",
|
||||||
|
zIndex: 2,
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
width={width * scale}
|
||||||
|
height={height * scale}
|
||||||
|
ref={canvasRef}
|
||||||
|
>
|
||||||
|
Debug Canvas
|
||||||
|
</canvas>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default DebugCanvas;
|
||||||
@@ -128,29 +128,6 @@
|
|||||||
<script>
|
<script>
|
||||||
window.EXCALIDRAW_ASSET_PATH = window.origin;
|
window.EXCALIDRAW_ASSET_PATH = window.origin;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- in DEV we need to preload from the local server and without the hash -->
|
|
||||||
<link
|
|
||||||
rel="preload"
|
|
||||||
href="../packages/excalidraw/fonts/assets/Excalifont-Regular.woff2"
|
|
||||||
as="font"
|
|
||||||
type="font/woff2"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="preload"
|
|
||||||
href="../packages/excalidraw/fonts/assets/Virgil-Regular.woff2"
|
|
||||||
as="font"
|
|
||||||
type="font/woff2"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="preload"
|
|
||||||
href="../packages/excalidraw/fonts/assets/ComicShanns-Regular.woff2"
|
|
||||||
as="font"
|
|
||||||
type="font/woff2"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
/>
|
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<!-- For Nunito only preload the latin range, which should be good enough for now -->
|
<!-- For Nunito only preload the latin range, which should be good enough for now -->
|
||||||
|
|||||||
@@ -73,8 +73,8 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
|
|
||||||
workbox: {
|
workbox: {
|
||||||
// Don't push fonts and locales to app precache
|
// Don't push fonts, locales and wasm to app precache
|
||||||
globIgnores: ["fonts.css", "**/locales/**", "service-worker.js"],
|
globIgnores: ["fonts.css", "**/locales/**", "service-worker.js", "**/*.wasm-*.js"],
|
||||||
runtimeCaching: [
|
runtimeCaching: [
|
||||||
{
|
{
|
||||||
urlPattern: new RegExp("/.+.(ttf|woff2|otf)"),
|
urlPattern: new RegExp("/.+.(ttf|woff2|otf)"),
|
||||||
@@ -108,6 +108,17 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
urlPattern: new RegExp(".wasm-.+.js"),
|
||||||
|
handler: "CacheFirst",
|
||||||
|
options: {
|
||||||
|
cacheName: "wasm",
|
||||||
|
expiration: {
|
||||||
|
maxEntries: 50,
|
||||||
|
maxAgeSeconds: 60 * 60 * 24 * 90, // <== 90 days
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
manifest: {
|
manifest: {
|
||||||
|
|||||||
@@ -850,7 +850,7 @@ export const actionChangeFontFamily = register({
|
|||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawElement | null
|
ExcalidrawElement | null
|
||||||
>();
|
>();
|
||||||
let uniqueGlyphs = new Set<string>();
|
let uniqueChars = new Set<string>();
|
||||||
let skipFontFaceCheck = false;
|
let skipFontFaceCheck = false;
|
||||||
|
|
||||||
const fontsCache = Array.from(Fonts.loadedFontsCache.values());
|
const fontsCache = Array.from(Fonts.loadedFontsCache.values());
|
||||||
@@ -898,8 +898,8 @@ export const actionChangeFontFamily = register({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!skipFontFaceCheck) {
|
if (!skipFontFaceCheck) {
|
||||||
uniqueGlyphs = new Set([
|
uniqueChars = new Set([
|
||||||
...uniqueGlyphs,
|
...uniqueChars,
|
||||||
...Array.from(newElement.originalText),
|
...Array.from(newElement.originalText),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -919,12 +919,9 @@ export const actionChangeFontFamily = register({
|
|||||||
const fontString = `10px ${getFontFamilyString({
|
const fontString = `10px ${getFontFamilyString({
|
||||||
fontFamily: nextFontFamily,
|
fontFamily: nextFontFamily,
|
||||||
})}`;
|
})}`;
|
||||||
const glyphs = Array.from(uniqueGlyphs.values()).join();
|
const chars = Array.from(uniqueChars.values()).join();
|
||||||
|
|
||||||
if (
|
if (skipFontFaceCheck || window.document.fonts.check(fontString, chars)) {
|
||||||
skipFontFaceCheck ||
|
|
||||||
window.document.fonts.check(fontString, glyphs)
|
|
||||||
) {
|
|
||||||
// we either skip the check (have at least one font face loaded) or do the check and find out all the font faces have loaded
|
// we either skip the check (have at least one font face loaded) or do the check and find out all the font faces have loaded
|
||||||
for (const [element, container] of elementContainerMapping) {
|
for (const [element, container] of elementContainerMapping) {
|
||||||
// trigger synchronous redraw
|
// trigger synchronous redraw
|
||||||
@@ -936,8 +933,8 @@ export const actionChangeFontFamily = register({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// otherwise try to load all font faces for the given glyphs and redraw elements once our font faces loaded
|
// otherwise try to load all font faces for the given chars and redraw elements once our font faces loaded
|
||||||
window.document.fonts.load(fontString, glyphs).then((fontFaces) => {
|
window.document.fonts.load(fontString, chars).then((fontFaces) => {
|
||||||
for (const [element, container] of elementContainerMapping) {
|
for (const [element, container] of elementContainerMapping) {
|
||||||
// use latest element state to ensure we don't have closure over an old instance in order to avoid possible race conditions (i.e. font faces load out-of-order while rapidly switching fonts)
|
// use latest element state to ensure we don't have closure over an old instance in order to avoid possible race conditions (i.e. font faces load out-of-order while rapidly switching fonts)
|
||||||
const latestElement = app.scene.getElement(element.id);
|
const latestElement = app.scene.getElement(element.id);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// place here categories that you want to track. We want to track just a
|
// place here categories that you want to track. We want to track just a
|
||||||
// small subset of categories at a given time.
|
// small subset of categories at a given time.
|
||||||
const ALLOWED_CATEGORIES_TO_TRACK = new Set(["command_palette"]);
|
const ALLOWED_CATEGORIES_TO_TRACK = new Set(["command_palette", "export"]);
|
||||||
|
|
||||||
export const trackEvent = (
|
export const trackEvent = (
|
||||||
category: string,
|
category: string,
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import {
|
|||||||
} from "./icons";
|
} from "./icons";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { useTunnels } from "../context/tunnels";
|
import { useTunnels } from "../context/tunnels";
|
||||||
|
import { CLASSES } from "../constants";
|
||||||
|
|
||||||
export const canChangeStrokeColor = (
|
export const canChangeStrokeColor = (
|
||||||
appState: UIAppState,
|
appState: UIAppState,
|
||||||
@@ -426,7 +427,7 @@ export const ZoomActions = ({
|
|||||||
renderAction: ActionManager["renderAction"];
|
renderAction: ActionManager["renderAction"];
|
||||||
zoom: Zoom;
|
zoom: Zoom;
|
||||||
}) => (
|
}) => (
|
||||||
<Stack.Col gap={1} className="zoom-actions">
|
<Stack.Col gap={1} className={CLASSES.ZOOM_ACTIONS}>
|
||||||
<Stack.Row align="center">
|
<Stack.Row align="center">
|
||||||
{renderAction("zoomOut")}
|
{renderAction("zoomOut")}
|
||||||
{renderAction("resetZoom")}
|
{renderAction("resetZoom")}
|
||||||
|
|||||||
@@ -2515,7 +2515,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
addEventListener(
|
addEventListener(
|
||||||
this.excalidrawContainerRef.current,
|
this.excalidrawContainerRef.current,
|
||||||
EVENT.WHEEL,
|
EVENT.WHEEL,
|
||||||
this.onWheel,
|
this.handleWheel,
|
||||||
{ passive: false },
|
{ passive: false },
|
||||||
),
|
),
|
||||||
addEventListener(window, EVENT.MESSAGE, this.onWindowMessage, false),
|
addEventListener(window, EVENT.MESSAGE, this.onWindowMessage, false),
|
||||||
@@ -2578,6 +2578,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
addEventListener(window, EVENT.RESIZE, this.onResize, false),
|
addEventListener(window, EVENT.RESIZE, this.onResize, false),
|
||||||
addEventListener(window, EVENT.UNLOAD, this.onUnload, false),
|
addEventListener(window, EVENT.UNLOAD, this.onUnload, false),
|
||||||
addEventListener(window, EVENT.BLUR, this.onBlur, false),
|
addEventListener(window, EVENT.BLUR, this.onBlur, false),
|
||||||
|
addEventListener(
|
||||||
|
this.excalidrawContainerRef.current,
|
||||||
|
EVENT.WHEEL,
|
||||||
|
this.handleWheel,
|
||||||
|
),
|
||||||
addEventListener(
|
addEventListener(
|
||||||
this.excalidrawContainerRef.current,
|
this.excalidrawContainerRef.current,
|
||||||
EVENT.DRAG_OVER,
|
EVENT.DRAG_OVER,
|
||||||
@@ -4318,13 +4323,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
private onWheel = withBatchedUpdates((event: WheelEvent) => {
|
|
||||||
// prevent browser pinch zoom on DOM elements
|
|
||||||
if (!(event.target instanceof HTMLCanvasElement) && event.ctrlKey) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
private onKeyUp = withBatchedUpdates((event: KeyboardEvent) => {
|
private onKeyUp = withBatchedUpdates((event: KeyboardEvent) => {
|
||||||
if (event.key === KEYS.SPACE) {
|
if (event.key === KEYS.SPACE) {
|
||||||
if (this.state.viewModeEnabled) {
|
if (this.state.viewModeEnabled) {
|
||||||
@@ -6384,8 +6382,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Returns whether the event is a panning
|
// Returns whether the event is a panning
|
||||||
private handleCanvasPanUsingWheelOrSpaceDrag = (
|
public handleCanvasPanUsingWheelOrSpaceDrag = (
|
||||||
event: React.PointerEvent<HTMLElement>,
|
event: React.PointerEvent<HTMLElement> | MouseEvent,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if (
|
if (
|
||||||
!(
|
!(
|
||||||
@@ -6394,13 +6392,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace) ||
|
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace) ||
|
||||||
isHandToolActive(this.state) ||
|
isHandToolActive(this.state) ||
|
||||||
this.state.viewModeEnabled)
|
this.state.viewModeEnabled)
|
||||||
) ||
|
)
|
||||||
this.state.editingTextElement
|
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
isPanning = true;
|
isPanning = true;
|
||||||
event.preventDefault();
|
|
||||||
|
if (!this.state.editingTextElement) {
|
||||||
|
// preventing defualt while text editing messes with cursor/focus
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
let nextPastePrevented = false;
|
let nextPastePrevented = false;
|
||||||
const isLinux =
|
const isLinux =
|
||||||
@@ -9472,7 +9473,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// NOTE wheel, touchstart, touchend events must be registered outside
|
// NOTE wheel, touchstart, touchend events must be registered outside
|
||||||
// of react because react binds them them passively (so we can't prevent
|
// of react because react binds them them passively (so we can't prevent
|
||||||
// default on them)
|
// default on them)
|
||||||
this.interactiveCanvas.addEventListener(EVENT.WHEEL, this.handleWheel);
|
|
||||||
this.interactiveCanvas.addEventListener(
|
this.interactiveCanvas.addEventListener(
|
||||||
EVENT.TOUCH_START,
|
EVENT.TOUCH_START,
|
||||||
this.onTouchStart,
|
this.onTouchStart,
|
||||||
@@ -9480,10 +9480,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.interactiveCanvas.addEventListener(EVENT.TOUCH_END, this.onTouchEnd);
|
this.interactiveCanvas.addEventListener(EVENT.TOUCH_END, this.onTouchEnd);
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
} else {
|
} else {
|
||||||
this.interactiveCanvas?.removeEventListener(
|
|
||||||
EVENT.WHEEL,
|
|
||||||
this.handleWheel,
|
|
||||||
);
|
|
||||||
this.interactiveCanvas?.removeEventListener(
|
this.interactiveCanvas?.removeEventListener(
|
||||||
EVENT.TOUCH_START,
|
EVENT.TOUCH_START,
|
||||||
this.onTouchStart,
|
this.onTouchStart,
|
||||||
@@ -10078,7 +10074,24 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
(
|
(
|
||||||
event: WheelEvent | React.WheelEvent<HTMLDivElement | HTMLCanvasElement>,
|
event: WheelEvent | React.WheelEvent<HTMLDivElement | HTMLCanvasElement>,
|
||||||
) => {
|
) => {
|
||||||
|
// if not scrolling on canvas/wysiwyg, ignore
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
event.target instanceof HTMLCanvasElement ||
|
||||||
|
event.target instanceof HTMLTextAreaElement ||
|
||||||
|
event.target instanceof HTMLIFrameElement
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// prevent zooming the browser (but allow scrolling DOM)
|
||||||
|
if (event[KEYS.CTRL_OR_CMD]) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (isPanning) {
|
if (isPanning) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ const ColorPickerPopupContent = ({
|
|||||||
return (
|
return (
|
||||||
<PropertiesPopover
|
<PropertiesPopover
|
||||||
container={container}
|
container={container}
|
||||||
style={{ maxWidth: "208px" }}
|
style={{ maxWidth: "13rem" }}
|
||||||
onFocusOutside={(event) => {
|
onFocusOutside={(event) => {
|
||||||
// refocus due to eye dropper
|
// refocus due to eye dropper
|
||||||
focusPickerContent();
|
focusPickerContent();
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
@import "../css/variables.module.scss";
|
@import "../css/variables.module.scss";
|
||||||
|
|
||||||
|
@keyframes successStatusAnimation {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.ExcButton {
|
.ExcButton {
|
||||||
--text-color: transparent;
|
--text-color: transparent;
|
||||||
@@ -22,9 +36,10 @@
|
|||||||
visibility: visible;
|
visibility: visible;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
width: 1rem;
|
width: 1.2rem;
|
||||||
height: 1rem;
|
height: 1.2rem;
|
||||||
font-size: 1rem;
|
|
||||||
|
animation: successStatusAnimation 0.5s cubic-bezier(0.3, 1, 0.6, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.ExcButton--status-loading,
|
&.ExcButton--status-loading,
|
||||||
|
|||||||
@@ -90,7 +90,20 @@ const ImageExportModal = ({
|
|||||||
const previewRef = useRef<HTMLDivElement>(null);
|
const previewRef = useRef<HTMLDivElement>(null);
|
||||||
const [renderError, setRenderError] = useState<Error | null>(null);
|
const [renderError, setRenderError] = useState<Error | null>(null);
|
||||||
|
|
||||||
const { onCopy, copyStatus } = useCopyStatus();
|
const { onCopy, copyStatus, resetCopyStatus } = useCopyStatus();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// if user changes setting right after export to clipboard, reset the status
|
||||||
|
// so they don't have to wait for the timeout to click the button again
|
||||||
|
resetCopyStatus();
|
||||||
|
}, [
|
||||||
|
projectName,
|
||||||
|
exportWithBackground,
|
||||||
|
exportDarkMode,
|
||||||
|
exportScale,
|
||||||
|
embedScene,
|
||||||
|
resetCopyStatus,
|
||||||
|
]);
|
||||||
|
|
||||||
const { exportedElements, exportingFrame } = prepareElementsForExport(
|
const { exportedElements, exportingFrame } = prepareElementsForExport(
|
||||||
elementsSnapshot,
|
elementsSnapshot,
|
||||||
@@ -108,6 +121,7 @@ const ImageExportModal = ({
|
|||||||
if (!maxWidth) {
|
if (!maxWidth) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
exportToCanvas({
|
exportToCanvas({
|
||||||
elements: exportedElements,
|
elements: exportedElements,
|
||||||
appState: {
|
appState: {
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ const SingleLibraryItem = ({
|
|||||||
exportBackground: true,
|
exportBackground: true,
|
||||||
},
|
},
|
||||||
files: null,
|
files: null,
|
||||||
|
skipInliningFonts: true,
|
||||||
});
|
});
|
||||||
node.innerHTML = svg.outerHTML;
|
node.innerHTML = svg.outerHTML;
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export const ENV = {
|
|||||||
|
|
||||||
export const CLASSES = {
|
export const CLASSES = {
|
||||||
SHAPE_ACTIONS_MENU: "App-menu__left",
|
SHAPE_ACTIONS_MENU: "App-menu__left",
|
||||||
|
ZOOM_ACTIONS: "zoom-actions",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -387,7 +387,7 @@ body.excalidraw-cursor-resize * {
|
|||||||
.App-menu__left {
|
.App-menu__left {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
width: 200px;
|
width: 12.5rem;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import { CLASSES, isSafari } from "../constants";
|
import { CLASSES, isSafari, POINTER_BUTTON } from "../constants";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
@@ -38,7 +38,11 @@ import {
|
|||||||
actionDecreaseFontSize,
|
actionDecreaseFontSize,
|
||||||
actionIncreaseFontSize,
|
actionIncreaseFontSize,
|
||||||
} from "../actions/actionProperties";
|
} from "../actions/actionProperties";
|
||||||
import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
|
import {
|
||||||
|
actionResetZoom,
|
||||||
|
actionZoomIn,
|
||||||
|
actionZoomOut,
|
||||||
|
} from "../actions/actionCanvas";
|
||||||
import type App from "../components/App";
|
import type App from "../components/App";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { parseClipboard } from "../clipboard";
|
import { parseClipboard } from "../clipboard";
|
||||||
@@ -379,6 +383,10 @@ export const textWysiwyg = ({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
app.actionManager.executeAction(actionZoomOut);
|
app.actionManager.executeAction(actionZoomOut);
|
||||||
updateWysiwygStyle();
|
updateWysiwygStyle();
|
||||||
|
} else if (!event.shiftKey && actionResetZoom.keyTest(event)) {
|
||||||
|
event.preventDefault();
|
||||||
|
app.actionManager.executeAction(actionResetZoom);
|
||||||
|
updateWysiwygStyle();
|
||||||
} else if (actionDecreaseFontSize.keyTest(event)) {
|
} else if (actionDecreaseFontSize.keyTest(event)) {
|
||||||
app.actionManager.executeAction(actionDecreaseFontSize);
|
app.actionManager.executeAction(actionDecreaseFontSize);
|
||||||
} else if (actionIncreaseFontSize.keyTest(event)) {
|
} else if (actionIncreaseFontSize.keyTest(event)) {
|
||||||
@@ -593,6 +601,7 @@ export const textWysiwyg = ({
|
|||||||
window.removeEventListener("blur", handleSubmit);
|
window.removeEventListener("blur", handleSubmit);
|
||||||
window.removeEventListener("beforeunload", handleSubmit);
|
window.removeEventListener("beforeunload", handleSubmit);
|
||||||
unbindUpdate();
|
unbindUpdate();
|
||||||
|
unbindOnScroll();
|
||||||
|
|
||||||
editable.remove();
|
editable.remove();
|
||||||
};
|
};
|
||||||
@@ -619,10 +628,29 @@ export const textWysiwyg = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const temporarilyDisableSubmit = () => {
|
||||||
|
editable.onblur = null;
|
||||||
|
window.addEventListener("pointerup", bindBlurEvent);
|
||||||
|
// handle edge-case where pointerup doesn't fire e.g. due to user
|
||||||
|
// alt-tabbing away
|
||||||
|
window.addEventListener("blur", handleSubmit);
|
||||||
|
};
|
||||||
|
|
||||||
// prevent blur when changing properties from the menu
|
// prevent blur when changing properties from the menu
|
||||||
const onPointerDown = (event: MouseEvent) => {
|
const onPointerDown = (event: MouseEvent) => {
|
||||||
const target = event?.target;
|
const target = event?.target;
|
||||||
|
|
||||||
|
// panning canvas
|
||||||
|
if (event.button === POINTER_BUTTON.WHEEL) {
|
||||||
|
// trying to pan by clicking inside text area itself -> handle here
|
||||||
|
if (target instanceof HTMLTextAreaElement) {
|
||||||
|
event.preventDefault();
|
||||||
|
app.handleCanvasPanUsingWheelOrSpaceDrag(event);
|
||||||
|
}
|
||||||
|
temporarilyDisableSubmit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const isPropertiesTrigger =
|
const isPropertiesTrigger =
|
||||||
target instanceof HTMLElement &&
|
target instanceof HTMLElement &&
|
||||||
target.classList.contains("properties-trigger");
|
target.classList.contains("properties-trigger");
|
||||||
@@ -630,17 +658,14 @@ export const textWysiwyg = ({
|
|||||||
if (
|
if (
|
||||||
((event.target instanceof HTMLElement ||
|
((event.target instanceof HTMLElement ||
|
||||||
event.target instanceof SVGElement) &&
|
event.target instanceof SVGElement) &&
|
||||||
event.target.closest(`.${CLASSES.SHAPE_ACTIONS_MENU}`) &&
|
event.target.closest(
|
||||||
|
`.${CLASSES.SHAPE_ACTIONS_MENU}, .${CLASSES.ZOOM_ACTIONS}`,
|
||||||
|
) &&
|
||||||
!isWritableElement(event.target)) ||
|
!isWritableElement(event.target)) ||
|
||||||
isPropertiesTrigger
|
isPropertiesTrigger
|
||||||
) {
|
) {
|
||||||
editable.onblur = null;
|
temporarilyDisableSubmit();
|
||||||
window.addEventListener("pointerup", bindBlurEvent);
|
|
||||||
// handle edge-case where pointerup doesn't fire e.g. due to user
|
|
||||||
// alt-tabbing away
|
|
||||||
window.addEventListener("blur", handleSubmit);
|
|
||||||
} else if (
|
} else if (
|
||||||
event.target instanceof HTMLElement &&
|
|
||||||
event.target instanceof HTMLCanvasElement &&
|
event.target instanceof HTMLCanvasElement &&
|
||||||
// Vitest simply ignores stopPropagation, capture-mode, or rAF
|
// Vitest simply ignores stopPropagation, capture-mode, or rAF
|
||||||
// so without introducing crazier hacks, nothing we can do
|
// so without introducing crazier hacks, nothing we can do
|
||||||
@@ -659,7 +684,7 @@ export const textWysiwyg = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// handle updates of textElement properties of editing element
|
// handle updates of textElement properties of editing element
|
||||||
const unbindUpdate = Scene.getScene(element)!.onUpdate(() => {
|
const unbindUpdate = app.scene.onUpdate(() => {
|
||||||
updateWysiwygStyle();
|
updateWysiwygStyle();
|
||||||
const isPopupOpened = !!document.activeElement?.closest(
|
const isPopupOpened = !!document.activeElement?.closest(
|
||||||
".properties-content",
|
".properties-content",
|
||||||
@@ -669,6 +694,10 @@ export const textWysiwyg = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const unbindOnScroll = app.onScrollChangeEmitter.on(() => {
|
||||||
|
updateWysiwygStyle();
|
||||||
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
let isDestroyed = false;
|
let isDestroyed = false;
|
||||||
@@ -699,10 +728,6 @@ export const textWysiwyg = ({
|
|||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
window.addEventListener("pointerdown", onPointerDown, { capture: true });
|
window.addEventListener("pointerdown", onPointerDown, { capture: true });
|
||||||
});
|
});
|
||||||
window.addEventListener("wheel", stopEvent, {
|
|
||||||
passive: false,
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
window.addEventListener("beforeunload", handleSubmit);
|
window.addEventListener("beforeunload", handleSubmit);
|
||||||
excalidrawContainer
|
excalidrawContainer
|
||||||
?.querySelector(".excalidraw-textEditorContainer")!
|
?.querySelector(".excalidraw-textEditorContainer")!
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import type { LineSegment } from "../../utils";
|
||||||
import { ROUNDNESS } from "../constants";
|
import { ROUNDNESS } from "../constants";
|
||||||
import type { ElementOrToolType } from "../types";
|
import type { ElementOrToolType, Point } from "../types";
|
||||||
import type { MarkNonNullable } from "../utility-types";
|
import type { MarkNonNullable } from "../utility-types";
|
||||||
import { assertNever } from "../utils";
|
import { assertNever } from "../utils";
|
||||||
|
import type { Bounds } from "./bounds";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
@@ -322,3 +324,23 @@ export const isFixedPointBinding = (
|
|||||||
): binding is FixedPointBinding => {
|
): binding is FixedPointBinding => {
|
||||||
return binding.fixedPoint != null;
|
return binding.fixedPoint != null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: Move this to @excalidraw/math
|
||||||
|
export const isPoint = (point: unknown): point is Point =>
|
||||||
|
Array.isArray(point) && point.length === 2;
|
||||||
|
|
||||||
|
// TODO: Move this to @excalidraw/math
|
||||||
|
export const isBounds = (box: unknown): box is Bounds =>
|
||||||
|
Array.isArray(box) &&
|
||||||
|
box.length === 4 &&
|
||||||
|
typeof box[0] === "number" &&
|
||||||
|
typeof box[1] === "number" &&
|
||||||
|
typeof box[2] === "number" &&
|
||||||
|
typeof box[3] === "number";
|
||||||
|
|
||||||
|
// TODO: Move this to @excalidraw/math
|
||||||
|
export const isLineSegment = (segment: unknown): segment is LineSegment =>
|
||||||
|
Array.isArray(segment) &&
|
||||||
|
segment.length === 2 &&
|
||||||
|
isPoint(segment[0]) &&
|
||||||
|
isPoint(segment[0]);
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { stringToBase64, toByteString } from "../data/encode";
|
import { stringToBase64, toByteString } from "../data/encode";
|
||||||
import { LOCAL_FONT_PROTOCOL } from "./metadata";
|
import { LOCAL_FONT_PROTOCOL } from "./metadata";
|
||||||
|
import loadWoff2 from "./wasm/woff2.loader";
|
||||||
|
import loadHbSubset from "./wasm/hb-subset.loader";
|
||||||
|
|
||||||
export interface Font {
|
export interface Font {
|
||||||
urls: URL[];
|
urls: URL[];
|
||||||
fontFace: FontFace;
|
fontFace: FontFace;
|
||||||
getContent(): Promise<string>;
|
getContent(codePoints: ReadonlySet<number>): Promise<string>;
|
||||||
}
|
}
|
||||||
export const UNPKG_PROD_URL = `https://unpkg.com/${
|
export const UNPKG_FALLBACK_URL = `https://unpkg.com/${
|
||||||
import.meta.env.VITE_PKG_NAME
|
import.meta.env.VITE_PKG_NAME
|
||||||
? `${import.meta.env.VITE_PKG_NAME}@${import.meta.env.PKG_VERSION}` // should be provided by vite during package build
|
? `${import.meta.env.VITE_PKG_NAME}@${import.meta.env.PKG_VERSION}` // should be provided by vite during package build
|
||||||
: "@excalidraw/excalidraw" // fallback to latest package version (i.e. for app)
|
: "@excalidraw/excalidraw" // fallback to latest package version (i.e. for app)
|
||||||
@@ -32,21 +34,32 @@ export class ExcalidrawFont implements Font {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tries to fetch woff2 content, based on the registered urls.
|
* Tries to fetch woff2 content, based on the registered urls (from first to last, treated as fallbacks).
|
||||||
* Returns last defined url in case of errors.
|
|
||||||
*
|
*
|
||||||
* Note: uses browser APIs for base64 encoding - use dataurl outside the browser environment.
|
* NOTE: assumes usage of `dataurl` outside the browser environment
|
||||||
|
*
|
||||||
|
* @returns base64 with subsetted glyphs based on the passed codepoint, last defined url otherwise
|
||||||
*/
|
*/
|
||||||
public async getContent(): Promise<string> {
|
public async getContent(codePoints: ReadonlySet<number>): Promise<string> {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
const errorMessages = [];
|
const errorMessages = [];
|
||||||
|
|
||||||
while (i < this.urls.length) {
|
while (i < this.urls.length) {
|
||||||
const url = this.urls[i];
|
const url = this.urls[i];
|
||||||
|
|
||||||
|
// it's dataurl (server), the font is inlined as base64, no need to fetch
|
||||||
if (url.protocol === "data:") {
|
if (url.protocol === "data:") {
|
||||||
// it's dataurl, the font is inlined as base64, no need to fetch
|
const arrayBuffer = Buffer.from(
|
||||||
return url.toString();
|
url.toString().split(",")[1],
|
||||||
|
"base64",
|
||||||
|
).buffer;
|
||||||
|
|
||||||
|
const base64 = await ExcalidrawFont.subsetGlyphsByCodePoints(
|
||||||
|
arrayBuffer,
|
||||||
|
codePoints,
|
||||||
|
);
|
||||||
|
|
||||||
|
return base64;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -57,13 +70,13 @@ export class ExcalidrawFont implements Font {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const mimeType = await response.headers.get("Content-Type");
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
const buffer = await response.arrayBuffer();
|
const base64 = await ExcalidrawFont.subsetGlyphsByCodePoints(
|
||||||
|
arrayBuffer,
|
||||||
|
codePoints,
|
||||||
|
);
|
||||||
|
|
||||||
return `data:${mimeType};base64,${await stringToBase64(
|
return base64;
|
||||||
await toByteString(buffer),
|
|
||||||
true,
|
|
||||||
)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// response not ok, try to continue
|
// response not ok, try to continue
|
||||||
@@ -89,6 +102,48 @@ export class ExcalidrawFont implements Font {
|
|||||||
return this.urls.length ? this.urls[this.urls.length - 1].toString() : "";
|
return this.urls.length ? this.urls[this.urls.length - 1].toString() : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to subset glyphs in a font based on the used codepoints, returning the font as daturl.
|
||||||
|
*
|
||||||
|
* @param arrayBuffer font data buffer, preferrably in the woff2 format, though others should work as well
|
||||||
|
* @param codePoints codepoints used to subset the glyphs
|
||||||
|
*
|
||||||
|
* @returns font with subsetted glyphs (all glyphs in case of errors) converted into a dataurl
|
||||||
|
*/
|
||||||
|
private static async subsetGlyphsByCodePoints(
|
||||||
|
arrayBuffer: ArrayBuffer,
|
||||||
|
codePoints: ReadonlySet<number>,
|
||||||
|
): Promise<string> {
|
||||||
|
try {
|
||||||
|
// lazy loaded wasm modules to avoid multiple initializations in case of concurrent triggers
|
||||||
|
const { compress, decompress } = await loadWoff2();
|
||||||
|
const { subset } = await loadHbSubset();
|
||||||
|
|
||||||
|
const decompressedBinary = decompress(arrayBuffer).buffer;
|
||||||
|
const subsetSnft = subset(decompressedBinary, codePoints);
|
||||||
|
const compressedBinary = compress(subsetSnft.buffer);
|
||||||
|
|
||||||
|
return ExcalidrawFont.toBase64(compressedBinary.buffer);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Skipped glyph subsetting", e);
|
||||||
|
// Fallback to encoding whole font in case of errors
|
||||||
|
return ExcalidrawFont.toBase64(arrayBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async toBase64(arrayBuffer: ArrayBuffer) {
|
||||||
|
let base64: string;
|
||||||
|
|
||||||
|
if (typeof Buffer !== "undefined") {
|
||||||
|
// node + server-side
|
||||||
|
base64 = Buffer.from(arrayBuffer).toString("base64");
|
||||||
|
} else {
|
||||||
|
base64 = await stringToBase64(await toByteString(arrayBuffer), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `data:font/woff2;base64,${base64}`;
|
||||||
|
}
|
||||||
|
|
||||||
private static createUrls(uri: string): URL[] {
|
private static createUrls(uri: string): URL[] {
|
||||||
if (uri.startsWith(LOCAL_FONT_PROTOCOL)) {
|
if (uri.startsWith(LOCAL_FONT_PROTOCOL)) {
|
||||||
// no url for local fonts
|
// no url for local fonts
|
||||||
@@ -118,15 +173,14 @@ export class ExcalidrawFont implements Font {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fallback url for bundled fonts
|
// fallback url for bundled fonts
|
||||||
urls.push(new URL(assetUrl, UNPKG_PROD_URL));
|
urls.push(new URL(assetUrl, UNPKG_FALLBACK_URL));
|
||||||
|
|
||||||
return urls;
|
return urls;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getFormat(url: URL) {
|
private static getFormat(url: URL) {
|
||||||
try {
|
try {
|
||||||
const pathname = new URL(url).pathname;
|
const parts = new URL(url).pathname.split(".");
|
||||||
const parts = pathname.split(".");
|
|
||||||
|
|
||||||
if (parts.length === 1) {
|
if (parts.length === 1) {
|
||||||
return "";
|
return "";
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
/**
|
||||||
|
* Modified version of hb-subset bindings from "subset-font" package https://github.com/papandreou/subset-font/blob/3f711c8aa29a426c7f22655861abfb976950f527/index.js
|
||||||
|
*
|
||||||
|
* CHANGELOG:
|
||||||
|
* - removed dependency on node APIs to work inside the browser
|
||||||
|
* - removed dependency on font fontverter for brotli compression
|
||||||
|
* - removed dependencies on lodash and p-limit
|
||||||
|
* - removed options for preserveNameIds, variationAxes, noLayoutClosure (not needed for now)
|
||||||
|
* - replaced text input with codepoints
|
||||||
|
* - rewritten in typescript and with esm modules
|
||||||
|
|
||||||
|
Copyright (c) 2012, Andreas Lind Petersen
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in
|
||||||
|
the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of the author nor the names of contributors may
|
||||||
|
be used to endorse or promote products derived from this
|
||||||
|
software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
||||||
|
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
||||||
|
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// function HB_TAG(str) {
|
||||||
|
// return str.split("").reduce((a, ch) => {
|
||||||
|
// return (a << 8) + ch.charCodeAt(0);
|
||||||
|
// }, 0);
|
||||||
|
// }
|
||||||
|
|
||||||
|
function subset(
|
||||||
|
hbSubsetWasm: any,
|
||||||
|
heapu8: Uint8Array,
|
||||||
|
font: ArrayBuffer,
|
||||||
|
codePoints: ReadonlySet<number>,
|
||||||
|
) {
|
||||||
|
const input = hbSubsetWasm.hb_subset_input_create_or_fail();
|
||||||
|
if (input === 0) {
|
||||||
|
throw new Error(
|
||||||
|
"hb_subset_input_create_or_fail (harfbuzz) returned zero, indicating failure",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fontBuffer = hbSubsetWasm.malloc(font.byteLength);
|
||||||
|
heapu8.set(new Uint8Array(font), fontBuffer);
|
||||||
|
|
||||||
|
// Create the face
|
||||||
|
const blob = hbSubsetWasm.hb_blob_create(
|
||||||
|
fontBuffer,
|
||||||
|
font.byteLength,
|
||||||
|
2, // HB_MEMORY_MODE_WRITABLE
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const face = hbSubsetWasm.hb_face_create(blob, 0);
|
||||||
|
hbSubsetWasm.hb_blob_destroy(blob);
|
||||||
|
|
||||||
|
// Do the equivalent of --font-features=*
|
||||||
|
const layoutFeatures = hbSubsetWasm.hb_subset_input_set(
|
||||||
|
input,
|
||||||
|
6, // HB_SUBSET_SETS_LAYOUT_FEATURE_TAG
|
||||||
|
);
|
||||||
|
hbSubsetWasm.hb_set_clear(layoutFeatures);
|
||||||
|
hbSubsetWasm.hb_set_invert(layoutFeatures);
|
||||||
|
|
||||||
|
// if (preserveNameIds) {
|
||||||
|
// const inputNameIds = harfbuzzJsWasm.hb_subset_input_set(
|
||||||
|
// input,
|
||||||
|
// 4, // HB_SUBSET_SETS_NAME_ID
|
||||||
|
// );
|
||||||
|
// for (const nameId of preserveNameIds) {
|
||||||
|
// harfbuzzJsWasm.hb_set_add(inputNameIds, nameId);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (noLayoutClosure) {
|
||||||
|
// harfbuzzJsWasm.hb_subset_input_set_flags(
|
||||||
|
// input,
|
||||||
|
// harfbuzzJsWasm.hb_subset_input_get_flags(input) | 0x00000200, // HB_SUBSET_FLAGS_NO_LAYOUT_CLOSURE
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Add unicodes indices
|
||||||
|
const inputUnicodes = hbSubsetWasm.hb_subset_input_unicode_set(input);
|
||||||
|
for (const c of codePoints) {
|
||||||
|
hbSubsetWasm.hb_set_add(inputUnicodes, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (variationAxes) {
|
||||||
|
// for (const [axisName, value] of Object.entries(variationAxes)) {
|
||||||
|
// if (typeof value === "number") {
|
||||||
|
// // Simple case: Pin/instance the variation axis to a single value
|
||||||
|
// if (
|
||||||
|
// !harfbuzzJsWasm.hb_subset_input_pin_axis_location(
|
||||||
|
// input,
|
||||||
|
// face,
|
||||||
|
// HB_TAG(axisName),
|
||||||
|
// value,
|
||||||
|
// )
|
||||||
|
// ) {
|
||||||
|
// harfbuzzJsWasm.hb_face_destroy(face);
|
||||||
|
// harfbuzzJsWasm.free(fontBuffer);
|
||||||
|
// throw new Error(
|
||||||
|
// `hb_subset_input_pin_axis_location (harfbuzz) returned zero when pinning ${axisName} to ${value}, indicating failure. Maybe the axis does not exist in the font?`,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// } else if (value && typeof value === "object") {
|
||||||
|
// // Complex case: Reduce the variation space of the axis
|
||||||
|
// if (
|
||||||
|
// typeof value.min === "undefined" ||
|
||||||
|
// typeof value.max === "undefined"
|
||||||
|
// ) {
|
||||||
|
// harfbuzzJsWasm.hb_face_destroy(face);
|
||||||
|
// harfbuzzJsWasm.free(fontBuffer);
|
||||||
|
// throw new Error(
|
||||||
|
// `${axisName}: You must provide both a min and a max value when setting the axis range`,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// if (
|
||||||
|
// !harfbuzzJsWasm.hb_subset_input_set_axis_range(
|
||||||
|
// input,
|
||||||
|
// face,
|
||||||
|
// HB_TAG(axisName),
|
||||||
|
// value.min,
|
||||||
|
// value.max,
|
||||||
|
// // An explicit NaN makes harfbuzz use the existing default value, clamping to the new range if necessary
|
||||||
|
// value.default ?? NaN,
|
||||||
|
// )
|
||||||
|
// ) {
|
||||||
|
// harfbuzzJsWasm.hb_face_destroy(face);
|
||||||
|
// harfbuzzJsWasm.free(fontBuffer);
|
||||||
|
// throw new Error(
|
||||||
|
// `hb_subset_input_set_axis_range (harfbuzz) returned zero when setting the range of ${axisName} to [${value.min}; ${value.max}] and a default value of ${value.default}, indicating failure. Maybe the axis does not exist in the font?`,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
let subset;
|
||||||
|
try {
|
||||||
|
subset = hbSubsetWasm.hb_subset_or_fail(face, input);
|
||||||
|
if (subset === 0) {
|
||||||
|
hbSubsetWasm.hb_face_destroy(face);
|
||||||
|
hbSubsetWasm.free(fontBuffer);
|
||||||
|
throw new Error(
|
||||||
|
"hb_subset_or_fail (harfbuzz) returned zero, indicating failure. Maybe the input file is corrupted?",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Clean up
|
||||||
|
hbSubsetWasm.hb_subset_input_destroy(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get result blob
|
||||||
|
const result = hbSubsetWasm.hb_face_reference_blob(subset);
|
||||||
|
|
||||||
|
const offset = hbSubsetWasm.hb_blob_get_data(result, 0);
|
||||||
|
const subsetByteLength = hbSubsetWasm.hb_blob_get_length(result);
|
||||||
|
if (subsetByteLength === 0) {
|
||||||
|
hbSubsetWasm.hb_blob_destroy(result);
|
||||||
|
hbSubsetWasm.hb_face_destroy(subset);
|
||||||
|
hbSubsetWasm.hb_face_destroy(face);
|
||||||
|
hbSubsetWasm.free(fontBuffer);
|
||||||
|
throw new Error(
|
||||||
|
"Failed to create subset font, maybe the input file is corrupted?",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const subsetFont = new Uint8Array(
|
||||||
|
heapu8.subarray(offset, offset + subsetByteLength),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
hbSubsetWasm.hb_blob_destroy(result);
|
||||||
|
hbSubsetWasm.hb_face_destroy(subset);
|
||||||
|
hbSubsetWasm.hb_face_destroy(face);
|
||||||
|
hbSubsetWasm.free(fontBuffer);
|
||||||
|
|
||||||
|
return subsetFont;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
subset,
|
||||||
|
};
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* Lazy loads wasm and respective bindings for font subsetting based on the harfbuzzjs.
|
||||||
|
*/
|
||||||
|
let loadedWasm: ReturnType<typeof load> | null = null;
|
||||||
|
|
||||||
|
// TODO: add support for fetching the wasm from an URL (external CDN, data URL, etc.)
|
||||||
|
const load = (): Promise<{
|
||||||
|
subset: (
|
||||||
|
fontBuffer: ArrayBuffer,
|
||||||
|
codePoints: ReadonlySet<number>,
|
||||||
|
) => Uint8Array;
|
||||||
|
}> => {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const [binary, bindings] = await Promise.all([
|
||||||
|
import("./hb-subset.wasm"),
|
||||||
|
import("./hb-subset.bindings"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
WebAssembly.instantiate(binary.default).then((module) => {
|
||||||
|
try {
|
||||||
|
const harfbuzzJsWasm = module.instance.exports;
|
||||||
|
// @ts-expect-error since `.buffer` is custom prop
|
||||||
|
const heapu8 = new Uint8Array(harfbuzzJsWasm.memory.buffer);
|
||||||
|
|
||||||
|
const hbSubset = {
|
||||||
|
subset: (
|
||||||
|
fontBuffer: ArrayBuffer,
|
||||||
|
codePoints: ReadonlySet<number>,
|
||||||
|
) => {
|
||||||
|
return bindings.default.subset(
|
||||||
|
harfbuzzJsWasm,
|
||||||
|
heapu8,
|
||||||
|
fontBuffer,
|
||||||
|
codePoints,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
resolve(hbSubset);
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// lazy load the default export
|
||||||
|
export default (): ReturnType<typeof load> => {
|
||||||
|
if (!loadedWasm) {
|
||||||
|
loadedWasm = load();
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadedWasm;
|
||||||
|
};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Lazy loads wasm and respective bindings for woff2 compression and decompression.
|
||||||
|
*/
|
||||||
|
type Vector = any;
|
||||||
|
|
||||||
|
let loadedWasm: ReturnType<typeof load> | null = null;
|
||||||
|
|
||||||
|
// TODO: add support for fetching the wasm from an URL (external CDN, data URL, etc.)
|
||||||
|
const load = (): Promise<{
|
||||||
|
compress: (buffer: ArrayBuffer) => Uint8Array;
|
||||||
|
decompress: (buffer: ArrayBuffer) => Uint8Array;
|
||||||
|
}> => {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const [binary, bindings] = await Promise.all([
|
||||||
|
import("./woff2.wasm"),
|
||||||
|
import("./woff2.bindings"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// initializing the module manually, so that we could pass in the wasm binary
|
||||||
|
bindings
|
||||||
|
.default({ wasmBinary: binary.default })
|
||||||
|
.then(
|
||||||
|
(module: {
|
||||||
|
woff2Enc: (buffer: ArrayBuffer, byteLength: number) => Vector;
|
||||||
|
woff2Dec: (buffer: ArrayBuffer, byteLength: number) => Vector;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
// re-map from internal vector into byte array
|
||||||
|
function convertFromVecToUint8Array(vector: Vector): Uint8Array {
|
||||||
|
const arr = [];
|
||||||
|
for (let i = 0, l = vector.size(); i < l; i++) {
|
||||||
|
arr.push(vector.get(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Uint8Array(arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// re-exporting only compress and decompress functions (also avoids infinite loop inside emscripten bindings)
|
||||||
|
const woff2 = {
|
||||||
|
compress: (buffer: ArrayBuffer) =>
|
||||||
|
convertFromVecToUint8Array(
|
||||||
|
module.woff2Enc(buffer, buffer.byteLength),
|
||||||
|
),
|
||||||
|
decompress: (buffer: ArrayBuffer) =>
|
||||||
|
convertFromVecToUint8Array(
|
||||||
|
module.woff2Dec(buffer, buffer.byteLength),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
resolve(woff2);
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// lazy loaded default export
|
||||||
|
export default (): ReturnType<typeof load> => {
|
||||||
|
if (!loadedWasm) {
|
||||||
|
loadedWasm = load();
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadedWasm;
|
||||||
|
};
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
|||||||
import { useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
|
|
||||||
const TIMEOUT = 2000;
|
const TIMEOUT = 2000;
|
||||||
|
|
||||||
@@ -15,8 +15,13 @@ export const useCopyStatus = () => {
|
|||||||
}, TIMEOUT);
|
}, TIMEOUT);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetCopyStatus = useCallback(() => {
|
||||||
|
setCopyStatus(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
copyStatus,
|
copyStatus,
|
||||||
|
resetCopyStatus,
|
||||||
onCopy,
|
onCopy,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const exportLibraryItemToSvg = async (elements: LibraryItem["elements"]) => {
|
|||||||
},
|
},
|
||||||
files: null,
|
files: null,
|
||||||
renderEmbeddables: false,
|
renderEmbeddables: false,
|
||||||
|
skipInliningFonts: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ export const useLibraryItemSvg = (
|
|||||||
// When there is no svg in cache export it and save to cache
|
// When there is no svg in cache export it and save to cache
|
||||||
(async () => {
|
(async () => {
|
||||||
const exportedSvg = await exportLibraryItemToSvg(elements);
|
const exportedSvg = await exportLibraryItemToSvg(elements);
|
||||||
|
// TODO: should likely be removed for custom fonts
|
||||||
exportedSvg.querySelector(".style-fonts")?.remove();
|
exportedSvg.querySelector(".style-fonts")?.remove();
|
||||||
|
|
||||||
if (exportedSvg) {
|
if (exportedSvg) {
|
||||||
|
|||||||
@@ -113,6 +113,8 @@
|
|||||||
"esbuild-sass-plugin": "2.16.0",
|
"esbuild-sass-plugin": "2.16.0",
|
||||||
"eslint-plugin-react": "7.32.2",
|
"eslint-plugin-react": "7.32.2",
|
||||||
"fake-indexeddb": "3.1.7",
|
"fake-indexeddb": "3.1.7",
|
||||||
|
"fonteditor-core": "2.4.1",
|
||||||
|
"harfbuzzjs": "0.3.6",
|
||||||
"import-meta-loader": "1.1.0",
|
"import-meta-loader": "1.1.0",
|
||||||
"mini-css-extract-plugin": "2.6.1",
|
"mini-css-extract-plugin": "2.6.1",
|
||||||
"postcss-loader": "7.0.1",
|
"postcss-loader": "7.0.1",
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import type { RenderableElementsMap } from "./types";
|
|||||||
import { syncInvalidIndices } from "../fractionalIndex";
|
import { syncInvalidIndices } from "../fractionalIndex";
|
||||||
import { renderStaticScene } from "../renderer/staticScene";
|
import { renderStaticScene } from "../renderer/staticScene";
|
||||||
import { Fonts } from "../fonts";
|
import { Fonts } from "../fonts";
|
||||||
|
import type { Font } from "../fonts/ExcalidrawFont";
|
||||||
|
|
||||||
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
||||||
|
|
||||||
@@ -355,50 +356,14 @@ export const exportToSvg = async (
|
|||||||
</clipPath>`;
|
</clipPath>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fontFamilies = elements.reduce((acc, element) => {
|
const fontFaces = opts?.skipInliningFonts ? [] : await getFontFaces(elements);
|
||||||
if (isTextElement(element)) {
|
|
||||||
acc.add(element.fontFamily);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, new Set<number>());
|
|
||||||
|
|
||||||
const fontFaces = opts?.skipInliningFonts
|
|
||||||
? []
|
|
||||||
: await Promise.all(
|
|
||||||
Array.from(fontFamilies).map(async (x) => {
|
|
||||||
const { fonts, metadata } = Fonts.registered.get(x) ?? {};
|
|
||||||
|
|
||||||
if (!Array.isArray(fonts)) {
|
|
||||||
console.error(
|
|
||||||
`Couldn't find registered fonts for font-family "${x}"`,
|
|
||||||
Fonts.registered,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadata?.local) {
|
|
||||||
// don't inline local fonts
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.all(
|
|
||||||
fonts.map(
|
|
||||||
async (font) => `@font-face {
|
|
||||||
font-family: ${font.fontFace.family};
|
|
||||||
src: url(${await font.getContent()});
|
|
||||||
}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
svgRoot.innerHTML = `
|
svgRoot.innerHTML = `
|
||||||
${SVG_EXPORT_TAG}
|
${SVG_EXPORT_TAG}
|
||||||
${metadata}
|
${metadata}
|
||||||
<defs>
|
<defs>
|
||||||
<style class="style-fonts">
|
<style class="style-fonts">
|
||||||
${fontFaces.flat().filter(Boolean).join("\n")}
|
${fontFaces.join("\n")}
|
||||||
</style>
|
</style>
|
||||||
${exportingFrameClipPath}
|
${exportingFrameClipPath}
|
||||||
</defs>
|
</defs>
|
||||||
@@ -469,3 +434,67 @@ export const getExportSize = (
|
|||||||
|
|
||||||
return [width, height];
|
return [width, height];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getFontFaces = async (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
): Promise<string[]> => {
|
||||||
|
const fontFamilies = new Set<number>();
|
||||||
|
const codePoints = new Set<number>();
|
||||||
|
|
||||||
|
for (const element of elements) {
|
||||||
|
if (!isTextElement(element)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
fontFamilies.add(element.fontFamily);
|
||||||
|
|
||||||
|
// gather unique codepoints only when inlining fonts
|
||||||
|
for (const codePoint of Array.from(element.originalText, (u) =>
|
||||||
|
u.codePointAt(0),
|
||||||
|
)) {
|
||||||
|
if (codePoint) {
|
||||||
|
codePoints.add(codePoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSource = (font: Font) => {
|
||||||
|
try {
|
||||||
|
// retrieve font source as dataurl based on the used codepoints
|
||||||
|
return font.getContent(codePoints);
|
||||||
|
} catch {
|
||||||
|
// fallback to font source as a url
|
||||||
|
return font.urls[0].toString();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fontFaces = await Promise.all(
|
||||||
|
Array.from(fontFamilies).map(async (x) => {
|
||||||
|
const { fonts, metadata } = Fonts.registered.get(x) ?? {};
|
||||||
|
|
||||||
|
if (!Array.isArray(fonts)) {
|
||||||
|
console.error(
|
||||||
|
`Couldn't find registered fonts for font-family "${x}"`,
|
||||||
|
Fonts.registered,
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata?.local) {
|
||||||
|
// don't inline local fonts
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
fonts.map(
|
||||||
|
async (font) => `@font-face {
|
||||||
|
font-family: ${font.fontFace.family};
|
||||||
|
src: url(${await getSource(font)});
|
||||||
|
}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return fontFaces.flat();
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import type { LineSegment } from "../utils";
|
||||||
|
import type { BoundingBox, Bounds } from "./element/bounds";
|
||||||
|
import { isBounds, isLineSegment } from "./element/typeChecks";
|
||||||
|
import type { Point } from "./types";
|
||||||
|
|
||||||
|
// The global data holder to collect the debug operations
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
visualDebug?: {
|
||||||
|
data: DebugElement[][];
|
||||||
|
currentFrame?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DebugElement = {
|
||||||
|
color: string;
|
||||||
|
data: LineSegment;
|
||||||
|
permanent: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const debugDrawLine = (
|
||||||
|
segment: LineSegment | LineSegment[],
|
||||||
|
opts?: {
|
||||||
|
color?: string;
|
||||||
|
permanent?: boolean;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
(isLineSegment(segment) ? [segment] : segment).forEach((data) =>
|
||||||
|
addToCurrentFrame({
|
||||||
|
color: opts?.color ?? "red",
|
||||||
|
data,
|
||||||
|
permanent: !!opts?.permanent,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const debugDrawPoint = (
|
||||||
|
point: Point,
|
||||||
|
opts?: {
|
||||||
|
color?: string;
|
||||||
|
permanent?: boolean;
|
||||||
|
fuzzy?: boolean;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const xOffset = opts?.fuzzy ? Math.random() * 3 : 0;
|
||||||
|
const yOffset = opts?.fuzzy ? Math.random() * 3 : 0;
|
||||||
|
|
||||||
|
debugDrawLine(
|
||||||
|
[
|
||||||
|
[point[0] + xOffset - 10, point[1] + yOffset - 10],
|
||||||
|
[point[0] + xOffset + 10, point[1] + yOffset + 10],
|
||||||
|
],
|
||||||
|
{
|
||||||
|
color: opts?.color ?? "cyan",
|
||||||
|
permanent: opts?.permanent,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
debugDrawLine(
|
||||||
|
[
|
||||||
|
[point[0] + xOffset - 10, point[1] + yOffset + 10],
|
||||||
|
[point[0] + xOffset + 10, point[1] + yOffset - 10],
|
||||||
|
],
|
||||||
|
{
|
||||||
|
color: opts?.color ?? "cyan",
|
||||||
|
permanent: opts?.permanent,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const debugDrawBoundingBox = (
|
||||||
|
box: BoundingBox | BoundingBox[],
|
||||||
|
opts?: {
|
||||||
|
color?: string;
|
||||||
|
permanent?: boolean;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
(Array.isArray(box) ? box : [box]).forEach((bbox) =>
|
||||||
|
debugDrawLine(
|
||||||
|
[
|
||||||
|
[
|
||||||
|
[bbox.minX, bbox.minY],
|
||||||
|
[bbox.maxX, bbox.minY],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[bbox.maxX, bbox.minY],
|
||||||
|
[bbox.maxX, bbox.maxY],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[bbox.maxX, bbox.maxY],
|
||||||
|
[bbox.minX, bbox.maxY],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[bbox.minX, bbox.maxY],
|
||||||
|
[bbox.minX, bbox.minY],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
{
|
||||||
|
color: opts?.color ?? "cyan",
|
||||||
|
permanent: opts?.permanent,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const debugDrawBounds = (
|
||||||
|
box: Bounds | Bounds[],
|
||||||
|
opts?: {
|
||||||
|
color: string;
|
||||||
|
permanent: boolean;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
(isBounds(box) ? [box] : box).forEach((bbox) =>
|
||||||
|
debugDrawLine(
|
||||||
|
[
|
||||||
|
[
|
||||||
|
[bbox[0], bbox[1]],
|
||||||
|
[bbox[2], bbox[1]],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[bbox[2], bbox[1]],
|
||||||
|
[bbox[2], bbox[3]],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[bbox[2], bbox[3]],
|
||||||
|
[bbox[0], bbox[3]],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[bbox[0], bbox[3]],
|
||||||
|
[bbox[0], bbox[1]],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
{
|
||||||
|
color: opts?.color ?? "green",
|
||||||
|
permanent: opts?.permanent,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const debugCloseFrame = () => {
|
||||||
|
window.visualDebug?.data.push([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const debugClear = () => {
|
||||||
|
if (window.visualDebug?.data) {
|
||||||
|
window.visualDebug.data = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addToCurrentFrame = (element: DebugElement) => {
|
||||||
|
if (window.visualDebug?.data && window.visualDebug.data.length === 0) {
|
||||||
|
window.visualDebug.data[0] = [];
|
||||||
|
}
|
||||||
|
window.visualDebug?.data &&
|
||||||
|
window.visualDebug.data[window.visualDebug.data.length - 1].push(element);
|
||||||
|
};
|
||||||
Vendored
-2
@@ -21,8 +21,6 @@ interface ImportMetaEnv {
|
|||||||
|
|
||||||
VITE_APP_FIREBASE_CONFIG: string;
|
VITE_APP_FIREBASE_CONFIG: string;
|
||||||
|
|
||||||
// whether to enable Service Workers in development
|
|
||||||
VITE_APP_DEV_ENABLE_SW: string;
|
|
||||||
// whether to disable live reload / HMR. Usuaully what you want to do when
|
// whether to disable live reload / HMR. Usuaully what you want to do when
|
||||||
// debugging Service Workers.
|
// debugging Service Workers.
|
||||||
VITE_APP_DEV_DISABLE_LIVE_RELOAD: string;
|
VITE_APP_DEV_DISABLE_LIVE_RELOAD: string;
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* This script is used to convert the wasm modules into js modules, with the binary converted into base64 encoded strings.
|
||||||
|
*/
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const wasmModules = [
|
||||||
|
{
|
||||||
|
pkg: `../node_modules/fonteditor-core`,
|
||||||
|
src: `./wasm/woff2.wasm`,
|
||||||
|
dest: `../packages/excalidraw/fonts/wasm/woff2.wasm.ts`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pkg: `../node_modules/harfbuzzjs`,
|
||||||
|
src: `./wasm/hb-subset.wasm`,
|
||||||
|
dest: `../packages/excalidraw/fonts/wasm/hb-subset.wasm.ts`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { pkg, src, dest } of wasmModules) {
|
||||||
|
const packagePath = path.resolve(__dirname, pkg, "package.json");
|
||||||
|
const licensePath = path.resolve(__dirname, pkg, "LICENSE");
|
||||||
|
const sourcePath = path.resolve(__dirname, src);
|
||||||
|
const destPath = path.resolve(__dirname, dest);
|
||||||
|
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
author,
|
||||||
|
license,
|
||||||
|
authors,
|
||||||
|
licenses,
|
||||||
|
} = require(packagePath);
|
||||||
|
|
||||||
|
const licenseContent = fs.readFileSync(licensePath, "utf-8") || "";
|
||||||
|
const base64 = fs.readFileSync(sourcePath, "base64");
|
||||||
|
const content = `// GENERATED CODE -- DO NOT EDIT!
|
||||||
|
/* eslint-disable prettier/prettier */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The following wasm module is generated with \`scripts/buildWasm.js\` and encoded as base64.
|
||||||
|
*
|
||||||
|
* The source of this content is taken from the package "${name}", which contains the following metadata:
|
||||||
|
*
|
||||||
|
* @author ${author || JSON.stringify(authors)}
|
||||||
|
* @license ${license || JSON.stringify(licenses)}
|
||||||
|
* @version ${version}
|
||||||
|
|
||||||
|
${licenseContent}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// faster atob alternative - https://github.com/evanw/esbuild/issues/1534#issuecomment-902738399
|
||||||
|
const __toBinary = /* @__PURE__ */ (() => {
|
||||||
|
const table = new Uint8Array(128);
|
||||||
|
for (let i = 0; i < 64; i++)
|
||||||
|
{table[i < 26 ? i + 65 : i < 52 ? i + 71 : i < 62 ? i - 4 : i * 4 - 205] = i;}
|
||||||
|
return (base64) => {
|
||||||
|
const n = base64.length; const bytes = new Uint8Array((n - (base64[n - 1] == "=") - (base64[n - 2] == "=")) * 3 / 4 | 0);
|
||||||
|
for (let i2 = 0, j = 0; i2 < n; ) {
|
||||||
|
const c0 = table[base64.charCodeAt(i2++)]; const c1 = table[base64.charCodeAt(i2++)];
|
||||||
|
const c2 = table[base64.charCodeAt(i2++)]; const c3 = table[base64.charCodeAt(i2++)];
|
||||||
|
bytes[j++] = c0 << 2 | c1 >> 4;
|
||||||
|
bytes[j++] = c1 << 4 | c2 >> 2;
|
||||||
|
bytes[j++] = c2 << 6 | c3;
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
export default __toBinary(\`${base64}\`);
|
||||||
|
`;
|
||||||
|
|
||||||
|
fs.writeFileSync(destPath, content);
|
||||||
|
}
|
||||||
Executable
BIN
Binary file not shown.
Binary file not shown.
@@ -6194,6 +6194,13 @@ fonteditor-core@2.4.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@xmldom/xmldom" "^0.8.3"
|
"@xmldom/xmldom" "^0.8.3"
|
||||||
|
|
||||||
|
fonteditor-core@2.4.1:
|
||||||
|
version "2.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/fonteditor-core/-/fonteditor-core-2.4.1.tgz#ff4b3cd04b50f98026bedad353d0ef6692464bc9"
|
||||||
|
integrity sha512-nKDDt6kBQGq665tQO5tCRQUClJG/2MAF9YT1eKHl+I4NasdSb6DgXrv/gMjNxjo9NyaVEv9KU9VZxLHMstN1wg==
|
||||||
|
dependencies:
|
||||||
|
"@xmldom/xmldom" "^0.8.3"
|
||||||
|
|
||||||
for-each@^0.3.3:
|
for-each@^0.3.3:
|
||||||
version "0.3.3"
|
version "0.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
|
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
|
||||||
@@ -6457,6 +6464,11 @@ hachure-fill@^0.5.2:
|
|||||||
resolved "https://registry.yarnpkg.com/hachure-fill/-/hachure-fill-0.5.2.tgz#d19bc4cc8750a5962b47fb1300557a85fcf934cc"
|
resolved "https://registry.yarnpkg.com/hachure-fill/-/hachure-fill-0.5.2.tgz#d19bc4cc8750a5962b47fb1300557a85fcf934cc"
|
||||||
integrity sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==
|
integrity sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==
|
||||||
|
|
||||||
|
harfbuzzjs@0.3.6:
|
||||||
|
version "0.3.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/harfbuzzjs/-/harfbuzzjs-0.3.6.tgz#97865c861aa7734af5bd1904570712e9d753fda9"
|
||||||
|
integrity sha512-dzf7y6NS8fiAIvPAL/VKwY8wx2HCzUB0vUfOo6h1J5UilFEEf7iYqFsvgwjHwvM3whbjfOMadNvQekU3KuRnWQ==
|
||||||
|
|
||||||
has-ansi@^2.0.0:
|
has-ansi@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
|
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
|
||||||
@@ -7820,9 +7832,9 @@ micromark@^3.0.0:
|
|||||||
uvu "^0.5.0"
|
uvu "^0.5.0"
|
||||||
|
|
||||||
micromatch@^4.0.0, micromatch@^4.0.4:
|
micromatch@^4.0.0, micromatch@^4.0.4:
|
||||||
version "4.0.7"
|
version "4.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5"
|
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
|
||||||
integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==
|
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
|
||||||
dependencies:
|
dependencies:
|
||||||
braces "^3.0.3"
|
braces "^3.0.3"
|
||||||
picomatch "^2.3.1"
|
picomatch "^2.3.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user