Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ drawnix/
├── dist/ # 构建产物
├── packages/
│ └── drawnix/ # 白板应用
│ └── freehand/ # 自由画笔插件
│ └── react-board/ # 白板 React 视图层
│ └── react-text/ # 文本渲染模块
├── package.json
Expand Down
1 change: 1 addition & 0 deletions README_en.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ drawnix/
├── dist/ # Build artifacts
├── packages/
│ └── drawnix/ # Whiteboard application core
│ └── freehand/ # Reusable freehand plugin
│ └── react-board/ # Whiteboard react view layer
│ └── react-text/ # Text rendering module
├── package.json
Expand Down
1 change: 1 addition & 0 deletions packages/drawnix/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
},
"dependencies": {
"@floating-ui/react": "^0.26.24",
"@plait-board/freehand": "^0.4.0",
"@plait-board/markdown-to-drawnix": "^0.0.8",
"@plait-board/mermaid-to-drawnix": "^0.0.7",
"laser-pen": "^1.0.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { ShapePicker, SHAPES } from '../shape-picker';
import { ArrowPicker, ARROWS } from '../arrow-picker';
import { useEffect, useState } from 'react';
import { Popover, PopoverContent, PopoverTrigger } from '../popover/popover';
import { FreehandShape } from '../../plugins/freehand/type';
import { FreehandShape } from '@plait-board/freehand';
import {
DrawnixFreehandPointer,
DrawnixPointerType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import { BoardCreationMode, setCreationMode } from '@plait/common';
import { useBoard } from '@plait-board/react-board';
import { DrawnixPointerType } from '../../../hooks/use-drawnix';
import { useI18n } from '../../../i18n';
import { FreehandShape } from '../../../plugins/freehand/type';
import { FREEHANDS, FREEHAND_PRESET_IDS, type FreehandPresetId } from '../../../constants/freehand';
import { FreehandStylePresetItem, type FreehandStylePreset } from './freehand-style-preset-item';
import './freehand-style-preset-item.scss';
import { FreehandShape, type FreehandDrawOptions } from '@plait-board/freehand';
import {
DEFAULT_FREEHAND_PRESETS,
type FreehandDrawOptions,
} from '../../../plugins/freehand/presets';
FREEHANDS,
FREEHAND_PRESET_IDS,
type FreehandPresetId,
} from '../../../constants/freehand';
import { FreehandStylePresetItem, type FreehandStylePreset } from './freehand-style-preset-item';
import './freehand-style-preset-item.scss';

const FreehandStyleDivider = () => <span className="freehand-style-divider" aria-hidden="true" />;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,8 @@ vi.mock('@plait-board/react-board', () => ({
}),
}));

vi.mock('../../../plugins/freehand/utils', () => ({
vi.mock('@plait-board/freehand', () => ({
getFreehandDefaultStrokeColor: () => '#000000',
}));

vi.mock('../../../plugins/freehand/type', () => ({
FREEHAND_STROKE_WIDTH_STEP: 0.25,
MAX_FREEHAND_STROKE_WIDTH: 24,
MIN_FREEHAND_STROKE_WIDTH: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import { SizeSlider } from '../../size-slider';
import { useI18n } from '../../../i18n';
import { ColorPicker } from '../../color-picker';
import { useBoard } from '@plait-board/react-board';
import { getFreehandDefaultStrokeColor } from '../../../plugins/freehand/utils';
import { getFreehandDefaultStrokeColor } from '@plait-board/freehand';
import {
FREEHAND_STROKE_WIDTH_STEP,
MAX_FREEHAND_STROKE_WIDTH,
MIN_FREEHAND_STROKE_WIDTH,
} from '../../../plugins/freehand/type';
} from '@plait-board/freehand';
import { isNoColor, isWhite } from '../../../utils/color';

const formatSize = (value: number) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { PopupStrokeButton } from './stroke-button';
import { PopupFillButton } from './fill-button';
import { isWhite, removeHexAlpha } from '../../../utils/color';
import { NO_COLOR } from '../../../constants/color';
import { Freehand } from '../../../plugins/freehand/type';
import { Freehand } from '@plait-board/freehand';
import { PopupLinkButton } from './link-button';
import { ArrowMarkButton } from './arrow-mark-button';
import { MoreOptionsButton } from './more-options-button';
Expand Down
25 changes: 24 additions & 1 deletion packages/drawnix/src/constants/freehand.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,35 @@
import type React from 'react';
import { EraseIcon, FeltTipPenIcon } from '../components/icons';
import { FreehandShape } from '../plugins/freehand/type';
import {
DEFAULT_FREEHAND_STROKE_WIDTH,
FreehandShape,
type FreehandDrawOptions,
} from '@plait-board/freehand';
import type { Translations } from '../i18n';
import { CLASSIC_COLORS } from './color';

export const FREEHAND_PRESET_IDS = ['preset-1', 'preset-2', 'preset-3'] as const;

export type FreehandPresetId = (typeof FREEHAND_PRESET_IDS)[number];

const getClassicColorValue = (name: string) => {
return CLASSIC_COLORS.find((item) => item.name === name)?.value;
};

export const DEFAULT_FREEHAND_PRESETS: FreehandDrawOptions[] = [
{
strokeWidth: DEFAULT_FREEHAND_STROKE_WIDTH,
},
{
strokeColor: getClassicColorValue('color.red') || CLASSIC_COLORS[5].value,
strokeWidth: 6,
},
{
strokeColor: getClassicColorValue('color.green') || CLASSIC_COLORS[6].value,
strokeWidth: 10,
},
];

export type FreehandToolItem = {
titleKey: keyof Translations;
icon: React.ReactNode;
Expand Down
4 changes: 2 additions & 2 deletions packages/drawnix/src/drawnix.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import classNames from 'classnames';
import './styles/index.scss';
import { buildDrawnixHotkeyPlugin } from './plugins/with-hotkey';
import { buildToolStateSyncPlugin } from './plugins/with-tool-state-sync';
import { withFreehand } from './plugins/freehand/with-freehand';
import { withDrawnixFreehand } from './plugins/with-freehand';
import { ThemeToolbar } from './components/toolbar/theme-toolbar';
import { buildPencilPlugin } from './plugins/with-pencil';
import {
Expand Down Expand Up @@ -204,7 +204,7 @@ export const Drawnix: React.FC<DrawnixProps> = ({
withMindExtend,
withCommonPlugin,
buildDrawnixHotkeyPlugin(updateAppState),
withFreehand,
withDrawnixFreehand,
buildPencilPlugin(updateAppState),
buildTextLinkPlugin(updateAppState),
buildToolStateSyncPlugin(syncBoardPointerToToolState),
Expand Down
2 changes: 1 addition & 1 deletion packages/drawnix/src/hooks/use-drawnix.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PlaitPointerType } from '@plait/core';
import { ArrowLineShape, BasicShapes } from '@plait/draw';
import { describe, expect, it, vi } from 'vitest';
import { FreehandShape } from '../plugins/freehand/type';
import { FreehandShape } from '@plait-board/freehand';
import { createDefaultToolState, mergeToolState } from './use-drawnix';

vi.mock('@plait/core', () => ({
Expand Down
4 changes: 2 additions & 2 deletions packages/drawnix/src/hooks/use-drawnix.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { PlaitBoard, PlaitPointerType } from '@plait/core';
import { createContext, useContext, type Dispatch, type SetStateAction } from 'react';
import { MindPointerType } from '@plait/mind';
import { ArrowLineShape, BasicShapes, DrawPointerType } from '@plait/draw';
import { FreehandShape } from '../plugins/freehand/type';
import { FreehandShape, type FreehandDrawOptions } from '@plait-board/freehand';
import { Editor } from 'slate';
import { LinkElement } from '@plait/common';
import { DEFAULT_FREEHAND_PRESETS, FreehandDrawOptions } from '../plugins/freehand/presets';
import { DEFAULT_FREEHAND_PRESETS } from '../constants/freehand';
import { DrawnixFileHandle } from '../data/json';
import type { DrawnixToastOptions } from '../components/toast/toast';

Expand Down
27 changes: 27 additions & 0 deletions packages/drawnix/src/plugins/with-freehand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {
createFreehandPlugin,
resolveFreehandDrawOptions,
type FreehandDrawOptions,
} from '@plait-board/freehand';
import { isTwoFingerMode } from '@plait-board/react-board';
import type { PlaitBoard } from '@plait/core';
import { DEFAULT_FREEHAND_PRESETS } from '../constants/freehand';
import type { DrawnixBoard } from '../hooks/use-drawnix';
import { LaserPointer } from '../utils/laser-pointer';

const getDrawOptions = (board: PlaitBoard): Partial<FreehandDrawOptions> => {
const toolState = (board as DrawnixBoard).appState?.toolState;
const activePresetIndex = toolState?.activeFreehandPresetIndex || 0;
const activePreset =
toolState?.freehandPresets?.[activePresetIndex] ||
DEFAULT_FREEHAND_PRESETS[activePresetIndex] ||
DEFAULT_FREEHAND_PRESETS[0];

return resolveFreehandDrawOptions(activePreset);
};

export const withDrawnixFreehand = createFreehandPlugin({
createEraseTrail: () => new LaserPointer(),
getDrawOptions,
isInteractionBlocked: isTwoFingerMode,
});
2 changes: 1 addition & 1 deletion packages/drawnix/src/plugins/with-hotkey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from '../hooks/use-drawnix';
import { BoardCreationMode, setCreationMode } from '@plait/common';
import { MindPointerType } from '@plait/mind';
import { FreehandShape } from './freehand/type';
import { FreehandShape } from '@plait-board/freehand';
import { ArrowLineShape, BasicShapes } from '@plait/draw';

export const buildDrawnixHotkeyPlugin = (
Expand Down
2 changes: 1 addition & 1 deletion packages/drawnix/src/plugins/with-tool-state-sync.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ vi.mock('@plait/mind', () => ({
},
}));

vi.mock('../plugins/freehand/type', () => ({
vi.mock('@plait-board/freehand', () => ({
FreehandShape: {
feltTipPen: 'feltTipPen',
eraser: 'eraser',
Expand Down
1 change: 1 addition & 0 deletions packages/drawnix/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export default defineConfig({
'react-dom/client',
'react/jsx-runtime',
'@plait-board/react-board',
'@plait-board/freehand',
'@plait-board/mermaid-to-drawnix',
'@plait-board/markdown-to-drawnix',
'classnames',
Expand Down
26 changes: 26 additions & 0 deletions packages/freehand/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# @plait-board/freehand

Reusable freehand drawing and erasing plugins for Plait boards.

```ts
import { createFreehandPlugin } from '@plait-board/freehand';
import { withOptions } from '@plait/core';
import { withDraw } from '@plait/draw';

const withFreehand = createFreehandPlugin({
getDrawOptions: () => ({ strokeWidth: 2 }),
isInteractionBlocked: (board) => isPinching(board),
createEraseTrail: () => new MyEraseTrail(),
});

const plugins = [withOptions, withDraw, withFreehand];
```

Application-specific toolbars, persisted presets, gesture policy, and eraser-trail presentation can be supplied through `createFreehandPlugin` options.

## Development

```sh
npx nx test freehand
npx nx build freehand
```
24 changes: 24 additions & 0 deletions packages/freehand/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@plait-board/freehand",
"version": "0.4.0",
"private": false,
"main": "./index.js",
"types": "./index.d.ts",
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js",
"types": "./index.d.ts"
}
},
"dependencies": {
"@plait/common": "^0.93.4",
"@plait/core": "^0.93.4",
"@plait/draw": "^0.93.4",
"@plait/text-plugins": "^0.93.4",
"roughjs": "^4.6.6"
},
"peerDependencies": {
"slate": "^0.116.0"
}
}
8 changes: 8 additions & 0 deletions packages/freehand/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "freehand",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/freehand/src",
"projectType": "library",
"tags": [],
"targets": {}
}
9 changes: 9 additions & 0 deletions packages/freehand/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type {
FreehandEraseTrail,
FreehandPluginOptions,
FreehandThemeColor,
} from './plugin-options';
export * from './presets';
export * from './type';
export * from './utils';
export * from './with-freehand';
23 changes: 23 additions & 0 deletions packages/freehand/src/plugin-options.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ThemeColorMode, type PlaitBoard } from '@plait/core';
import { describe, expect, it } from 'vitest';
import { getFreehandPluginOptions, setFreehandPluginOptions } from './plugin-options';

describe('freehand plugin options', () => {
it('merges a theme override without dropping package defaults', () => {
const board = {} as PlaitBoard;

setFreehandPluginOptions(board, {
themeColors: {
[ThemeColorMode.default]: {
strokeColor: '#123456',
fill: 'none',
},
},
});

const options = getFreehandPluginOptions(board);

expect(options.themeColors[ThemeColorMode.default].strokeColor).toBe('#123456');
expect(options.themeColors[ThemeColorMode.dark].strokeColor).toBe('#FFFFFF');
});
});
64 changes: 64 additions & 0 deletions packages/freehand/src/plugin-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { PlaitBoard, ThemeColorMode } from '@plait/core';
import { DEFAULT_FREEHAND_DRAW_OPTIONS, type FreehandDrawOptions } from './presets';
import { FreehandThemeColors } from './type';

export interface FreehandEraseTrail {
init(board: PlaitBoard): void;
destroy(): void;
}

export type FreehandThemeColor = {
strokeColor: string;
fill: string;
};

export type FreehandPluginOptions = {
createEraseTrail?: () => FreehandEraseTrail;
getDrawOptions?: (board: PlaitBoard) => Partial<FreehandDrawOptions>;
isInteractionBlocked?: (board: PlaitBoard) => boolean;
themeColors?: Partial<Record<ThemeColorMode, FreehandThemeColor>>;
};

type ResolvedFreehandPluginOptions = {
createEraseTrail: () => FreehandEraseTrail;
getDrawOptions: (board: PlaitBoard) => Partial<FreehandDrawOptions>;
isInteractionBlocked: (board: PlaitBoard) => boolean;
themeColors: Record<ThemeColorMode, FreehandThemeColor>;
};

const FREEHAND_PLUGIN_OPTIONS = new WeakMap<PlaitBoard, ResolvedFreehandPluginOptions>();

const createNoopEraseTrail = (): FreehandEraseTrail => ({
init: () => {},
destroy: () => {},
});

const resolveFreehandPluginOptions = (
options: FreehandPluginOptions = {}
): ResolvedFreehandPluginOptions => ({
createEraseTrail: options.createEraseTrail || createNoopEraseTrail,
getDrawOptions: options.getDrawOptions || (() => DEFAULT_FREEHAND_DRAW_OPTIONS),
isInteractionBlocked: options.isInteractionBlocked || (() => false),
themeColors: {
...FreehandThemeColors,
...options.themeColors,
},
});

export const setFreehandPluginOptions = (
board: PlaitBoard,
options: FreehandPluginOptions = {}
) => {
FREEHAND_PLUGIN_OPTIONS.set(board, resolveFreehandPluginOptions(options));
};

export const getFreehandPluginOptions = (board: PlaitBoard) => {
const options = FREEHAND_PLUGIN_OPTIONS.get(board);
if (options) {
return options;
}

const defaultOptions = resolveFreehandPluginOptions();
FREEHAND_PLUGIN_OPTIONS.set(board, defaultOptions);
return defaultOptions;
};
Loading