@netless/appliance-plugin
v1.1.39
Published
[中文文档](https://github.com/netless-io/fastboard/blob/main/docs/zh/appliance-plugin.md)
Readme
appliance-plugin
This plugin is based on the plugin mechanism of white-web-sdk, and realizes a set of whiteboard teaching AIDS drawing tools. At the same time, it is also based on @netless/window-manager, which can be used on multiple Windows.
Introduction
appliance-plugin is a high-performance whiteboard drawing plugin that depends on white-web-sdk and @netless/window-manager, and is based on Web API support for OffscreenCanvas.
Key Features
- 🎨 Rich Drawing Tools: Supports pencil, eraser, shape tools, text, images, and more
- ⚡ High Performance Rendering: Uses dual WebWorker + OffscreenCanvas mechanism, improving drawing efficiency by more than 40% compared to the main thread
- 🖼️ Multi-window Support: Supports multi-window scenarios, can be used independently on different windows
- 🎯 Laser Pen Tool: Supports laser pen functionality, suitable for presentation scenarios
- 📝 Text Editing: Supports text insertion, editing, and style settings
- 🗺️ Minimap Function: Provides minimap navigation for viewing overall content
- 🔄 Undo/Redo: Supports global undo/redo functionality
- 🎭 Custom Styles: Supports custom brush styles, text styles, etc.
- 🔌 Plugin Extension: Supports extending functionality through plugin mechanism (e.g., autoDraw handwriting graphics auto-association)
Principle
- Rendering Engine: The plugin is mainly based on SpriteJS's 2D functionality, supports WebGL2 rendering, and is backward compatible with downgrades to WebGL and Canvas2D.
- Multi-threaded Architecture: The plugin uses the dual WebWorker + OffscreenCanvas mechanism to process drawing calculations and rendering logic in independent worker threads, not occupying the CPU tasks of the main thread.
- Full Worker: Thread responsible for drawing complete data
- Sub Worker: Thread responsible for drawing one frame of data
- Compatibility Handling: For mobile terminals that do not support OffscreenCanvas, it will automatically downgrade to main thread processing.
Supported Drawing Tools
The plugin supports the following drawing tools:
- Basic Tools: Pencil, eraser, partial eraser, bitmap eraser, selector tool, hand tool
- Shape Tools: Straight line, arrow, rectangle, circle, triangle, diamond, polygon, star, speech balloon
- Text Tool: Supports text input, editing, and style settings
- Image Tool: Supports image insertion and editing
- Special Tools: Laser pen, background SVG
- Interactive Tools: Click interactive tool (for plugin custom behavior)
Plugin usage
Install
npm install @netless/appliance-pluginFastboard full packages: If you are using
@netless/fastboard-fullor@netless/fastboard-react-fulltogether withenableAppliancePlugin, install an@netless/appliance-pluginversion that exposes./bridge. We recommend>= 1.1.35(./bridgefirst appeared in>= 1.1.34-beta.2).Keep importing
@netless/appliance-pluginin app code. You do not need, and generally should not, replace it with@netless/appliance-plugin/bridge. The/bridgeentry is loaded internally by fastboard full to reuse the bundledwhite-web-sdkruntime.
Register Plugin
Plugins can support two scenarios, they have different plugin names:
- Multi-window
ApplianceMultiPlugin
import { ApplianceMultiPlugin } from '@netless/appliance-plugin';- Single whiteboard
ApplianceSinglePlugin
import { ApplianceSinglePlugin } from '@netless/appliance-plugin';Access Mode Reference
Preparing worker URLs
import fullWorkerString from '@netless/appliance-plugin/dist/fullWorker.js?raw';
import subWorkerString from '@netless/appliance-plugin/dist/subWorker.js?raw';
const fullWorkerUrl = URL.createObjectURL(new Blob([fullWorkerString], { type: 'text/javascript' }));
const subWorkerUrl = URL.createObjectURL(new Blob([subWorkerString], { type: 'text/javascript' }));fastboard (Direct integration with fastboard)
// Integration with fastboard-react
// Full package mode reference
// import { useFastboard, Fastboard } from "@netless/fastboard-react/full";
// Subpackage reference
import { useFastboard, Fastboard } from "@netless/fastboard-react";
// Prepare worker URLs as shown above:
const fullWorkerUrl = ...;
const subWorkerUrl = ...;
const app = useFastboard(() => ({
sdkConfig: {
...
},
joinRoom: {
...
},
managerConfig: {
cursor: true,
enableAppliancePlugin: true,
...
},
enableAppliancePlugin: {
cdn: {
fullWorkerUrl,
subWorkerUrl,
}
...
}
}));
// Integration with fastboard
// Full package mode reference
// import { createFastboard, createUI } from "@netless/fastboard/full";
// Subpackage reference
import { createFastboard, createUI } from "@netless/fastboard";
// Prepare worker URLs as shown above:
const fullWorkerUrl = ...;
const subWorkerUrl = ...;
const fastboard = await createFastboard({
sdkConfig: {
...
},
joinRoom: {
...
},
managerConfig: {
cursor: true,
supportAppliancePlugin: true,
...
},
enableAppliancePlugin: {
cdn: {
fullWorkerUrl,
subWorkerUrl,
}
...
}
});Note: In fastboard full mode,
@netless/fastboard-full/@netless/fastboard-react-fullload@netless/appliance-plugin/bridgeinternally. App code only needs to configureenableAppliancePlugin, provide worker URLs, and keep importing@netless/appliance-plugin; do not manually switch your import to@netless/appliance-plugin/bridge.
Multi-window (Direct integration with window-manager)
import '@netless/window-manager/dist/style.css';
import '@netless/appliance-plugin/dist/style.css';
import { WhiteWebSdk } from "white-web-sdk";
import { WindowManager } from "@netless/window-manager";
import { ApplianceMultiPlugin } from '@netless/appliance-plugin';
// Prepare worker URLs as shown above:
const fullWorkerUrl = ...;
const subWorkerUrl = ...;
const whiteWebSdk = new WhiteWebSdk(...)
const room = await whiteWebSdk.joinRoom({
...
invisiblePlugins: [WindowManager, ApplianceMultiPlugin],
useMultiViews: true,
})
const manager = await WindowManager.mount({ room, container: elm, chessboard: true, cursor: true, supportAppliancePlugin: true});
if (manager) {
await manager.switchMainViewToWriter();
await ApplianceMultiPlugin.getInstance(manager,
{
options: {
cdn: {
fullWorkerUrl,
subWorkerUrl,
},
...
}
}
);
}Note The project needs to import the CSS file
import '@netless/appliance-plugin/dist/style.css';
Single whiteboard (Direct integration with white-web-sdk)
import '@netless/appliance-plugin/dist/style.css';
import { WhiteWebSdk } from "white-web-sdk";
import { ApplianceSinglePlugin, ApplianceSigleWrapper } from '@netless/appliance-plugin';
// Prepare worker URLs as shown above:
const fullWorkerUrl = ...;
const subWorkerUrl = ...;
const whiteWebSdk = new WhiteWebSdk(...)
const room = await whiteWebSdk.joinRoom({
...
invisiblePlugins: [ApplianceSinglePlugin],
wrappedComponents: [ApplianceSigleWrapper]
})
await ApplianceSinglePlugin.getInstance(room,
{
options: {
cdn: {
fullWorkerUrl,
subWorkerUrl,
}
...
}
}
);Note The project needs to import the CSS file
import '@netless/appliance-plugin/dist/style.css';
About ?raw webpack configuration
module: {
rules: [
// ...
{
test: /\.m?js$/,
resourceQuery: { not: [/raw/] },
use: [ ... ]
},
{
resourceQuery: /raw/,
type: 'asset/source',
}
]
},API Introduction
Optimize Original Interfaces
The plugin re-implements some interfaces with the same name on room or windowmanager, but we have internally re-injected them back into the original object through injectMethodToObject. Therefore, external users do not need to make any changes. As follows:
// Internal hack
injectMethodToObject(windowmanager, 'undo');
injectMethodToObject(windowmanager, 'redo');
injectMethodToObject(windowmanager,'cleanCurrentScene');
injectMethodToObject(windowmanager,'insertImage');
injectMethodToObject(windowmanager,'completeImageUpload');
injectMethodToObject(windowmanager,'lockImage');
injectMethodToObject(room,'getImagesInformation');
injectMethodToObject(room,'callbacks');
injectMethodToObject(room,'screenshotToCanvasAsync');
injectMethodToObject(room,'getBoundingRectAsync');
injectMethodToObject(room,'scenePreviewAsync');
injectMethodToObject(windowmanager.mainView,'setMemberState');
// These we can see the call behavior through the front-end log, for example:
// [ApplianceMultiPlugin] setMemberState
// [ApplianceMultiPlugin] cleanCurrentSceneThe following interfaces are involved:
- Interfaces on room
setMemberStateundoredocallbacksinsertImagelockImagecompleteImageUploadgetImagesInformationcleanCurrentScene
- WindowManager interfaces
- Interfaces on WindowManager's mainView
setMemberStateundoredocallbacksinsertImagelockImagecompleteImageUploadgetImagesInformationcleanCurrentScene
- Custom interfaces
getBoundingRectAsync- Replace interfaceroom.getBoundingRectscreenshotToCanvasAsync- Replace interface room.screenshotToCanvasAsyncscenePreviewAsync- Replace interface room.scenePreviewfillSceneSnapshotAsync- Replace interface room.fillSceneSnapshotdestroy- Destroy the instance of appliance-pluginaddListener- Add appliance-plugin internal event listenerremoveListener- Remove appliance-plugin internal event listenerdisableDeviceInputs- Replace interface room.disableDeviceInputsdisableEraseImage- Replace interface room.disableEraseImage This method only prohibits the eraser that erases the entire image from erasing images, partial eraser is invaliddisableCameraTransform- Replace interface room.disableCameraTransform (Version >=1.1.17)insertText- Insert text at the specified position (Version >=1.1.18)updateText- Edit the content of the specified text (Version >=1.1.18)blurText- Remove text focus (Version >=1.1.19)hasElements- Check if there are elements in the specified scene (Version >=1.1.19)getElements- Get all elements in the specified scene (Version >=1.1.19)stopDraw- Stop Draw event (Version >=1.1.19)setViewLocalScenePathChange- Set the local scene path change for the whiteboard view (Version >=1.1.27)insertMarkmap- Insert markdown text to whiteboard (Version >=1.1.32) This method requires enabling extras.useBackgroundThread. It cannot be used directly from the default package entry. Business projects that need Markmap must installmarkmap-lib,markmap-view, andmermaid, then import@netless/appliance-plugin/markmapbefore calling it.updateMarkmap- Update markdown text in whiteboard (Version >=1.1.32) This method requires enabling extras.useBackgroundThread. It cannot be used directly from the default package entry. Business projects that need Markmap must installmarkmap-lib,markmap-view, andmermaid, then import@netless/appliance-plugin/markmapbefore calling it.insertBackgroundImage- Insert whiteboard background image (Version >=1.1.32) This method requires enabling extras.useBackgroundThread
- Selector / Element extension APIs
getSelectedElements(viewId?)- Get the current selector snapshotisElementPropertySupported(toolsType, field)- Check whether a property field is supported by a specific tools type, useful for custom floatbar button visibilityblurSelector(viewId?)- Clear the current selector selectionupdateSelectedElements(viewId?, changes)- Update properties of the current selector selection setcopySelectedElements(viewId?)- Copy the current selector selection setdeleteSelectedElements(viewId?)- Delete the current selector selection setupdateElement(elementId, scenePath, viewId, updateElementInfo, useUndoRedoStack?)- Single-element final-state update API, returnsPromise<boolean>getFloatbarContainer(viewId?)- Get the built-in custom floatbar mount container for the current viewgetViewOffsetToContainer(container, viewId?)- Convert coordinates from the current view to an external container, useful when mounting to a business overlay root
Notes:
updateSelectedElements(...)is selector-only and only operates on the current selection setblurSelector(...),updateSelectedElements(...),copySelectedElements(...),deleteSelectedElements(...), andupdateElement(...)should be called only when the room is writableupdateElement(...)is a storage-first single-element final-state update APIupdateElement(...)does not imply rendering has finished when the Promise resolvesupdateElement(...)does not support selector itselfupdateElementInfomust explicitly includetoolsType, and fields must match that tools typeupdateElement(...)returningPromise<boolean>only means the call passed validation and the update flow was started successfullygetFloatbarContainer(...)is the recommended mount point for a custom floatbar; when mounted there,selectorGeometryChange.viewRectcan be used directly for positioning- If you must mount to an external container, use
getViewOffsetToContainer(...)to convert coordinates
- Selector extension events
selectedElementsChange- Fires only when
selectedIdschange - Represents only the final selection set change
- Fires only when
selectorGeometryChange- Represents only the final geometry change
- Coordinates are unified as
viewRect
selectorTransformChange- Represents only transform process states such as drag, resize, rotate, and endpoint editing
- Exposes only
viewId + emitEventType + workState
remoteSelectorChange- Represents selector sync result changes from remote / synced clients
- Custom selector / floatbar capability
- You can disable the built-in floatbar while keeping selector selection, drag, resize, rotate, and endpoint-edit interactions
- You can render your own custom UI based on
selectedElementsChange,selectorGeometryChange,selectorTransformChange, andremoteSelectorChange - You can override selector visuals such as the highlight box, control points, endpoint dots, and locked icon through
overwriteSelectorStyles - You can mount your custom floatbar into the plugin-provided internal container or your own external overlay container
- Recommended reading:
- Incompatible interfaces
exportScene- After appliance-plugin is enabled, notes cannot be exported in room mode- Server-side screenshot - After appliance-plugin is enabled, notes cannot be obtained by calling server-side screenshot, but need to use
screenshotToCanvasAsyncto obtain the screenshot
New Features
Laser Pen Tool (Version >=1.1.1)
import { EStrokeType, ApplianceNames } from '@netless/appliance-plugin';
room.setMemberState({currentApplianceName: ApplianceNames.laserPen, strokeType: EStrokeType.Normal});Auto Shape: One-stroke Quick Shape Drawing (Version >=1.1.33)
When enabled, users still draw with the Pencil tool. On pointer up, the plugin tries to recognize the completed stroke as a regular shape and outputs the corresponding shape instead of a normal pencil path.
import { ApplianceNames, EStrokeType } from '@netless/appliance-plugin';
room.setMemberState({
currentApplianceName: ApplianceNames.pencil,
autoShape: true,
strokeType: EStrokeType.Normal,
});The current version supports single-stroke recognition for:
StraightArrowRectangleEllipse / CircleTriangleRhombusFive-point Star
Recommendations:
- Use the
Penciltool - Complete the gesture in one stroke
- Draw
Rectangle,Ellipse / Circle,Triangle, andFive-point Staras closed strokes - Draw
ArrowandStraightas open single strokes
Custom Selector / Floatbar and Selector Extension APIs (Version >=1.1.36-beta.2)
This version adds a selector-focused event and imperative API set so that integrators can build custom selector UI, floatbar UI, and selected-element property panels in white-web-sdk / @netless/window-manager scenarios.
New events:
selectedElementsChange- final selection set changes onlyselectorGeometryChange- final geometry changes only, with unifiedviewRectselectorTransformChange- process states for drag, resize, rotate, and endpoint editingremoteSelectorChange- selector sync result changes from remote / synced clients
New APIs:
getSelectedElements(viewId?)isElementPropertySupported(toolsType, field)blurSelector(viewId?)updateSelectedElements(viewId?, changes)copySelectedElements(viewId?)deleteSelectedElements(viewId?)updateElement(elementId, scenePath, viewId, updateElementInfo, useUndoRedoStack?)getFloatbarContainer(viewId?)getViewOffsetToContainer(container, viewId?)
Typical usage:
- Disable the built-in floatbar and render your own buttons, palettes, font-size, and text-style UI
- Use
getSelectedElements()to read the current selector snapshot - Use
updateSelectedElements()to batch-update the current selection - Use
updateElement()to update a specific element outside the current selection - Use
getFloatbarContainer()orgetViewOffsetToContainer()to decide where custom floatbar UI should be mounted - Use
overwriteSelectorStylesto customize selector visuals
For design details and integration examples, see:
Extended Tools (Version >=1.1.1)
On the original whiteboard tools type, some extended function attributes have been added, as follows:
export enum EStrokeType {
/** Solid line */
Normal = 'Normal',
/** Line with pen edge */
Stroke = 'Stroke',
/** Dotted line */
Dotted = 'Dotted',
/** Long dotted line */
LongDotted = 'LongDotted'
};
export type ExtendMemberState = {
/** The tool selected by the current user */
currentApplianceName: ApplianceNames;
/** Whether to enable pen edge */
strokeType?: EStrokeType;
/** Whether to delete the entire line segment */
isLine?: boolean;
/** Stroke transparency */
strokeOpacity?: number;
/** Whether to enable laser pointer */
useLaserPen?: boolean;
/** Whether to enable one-stroke auto shape recognition */
autoShape?: boolean;
/** Laser pointer holding time, second */
duration?: number;
/** Fill style */
fillColor?: Color;
/** Fill transparency */
fillOpacity?: number;
/** The specific type of graph to draw when using ``shape`` tool */
shapeType?: ShapeType;
/** Number of polygon vertices */
vertices?:number;
/** Inner vertex step length of polygon */
innerVerticeStep?:number;
/** Ratio of inner vertex radius to outer vertex radius of polygon */
innerRatio?: number;
/** Text transparency */
textOpacity?: number;
/** Text background color */
textBgColor?: Color;
/** Text background color transparency */
textBgOpacity?: number;
/** Placement */
placement?: SpeechBalloonPlacement;
};
import { ExtendMemberState, ApplianceNames } from '@netless/appliance-plugin';
/** Set tool state */
room.setMemberState({ ... } as ExtendMemberState);
manager.mainView.setMemberState({ ... } as ExtendMemberState);
appliance.setMemberState({ ... } as ExtendMemberState);- Set stroke type:
// Solid line
setMemberState({strokeType: EStrokeType.Normal });
// Line with pen edge
setMemberState({strokeType: EStrokeType.Stroke });
// Dotted line
setMemberState({strokeType: EStrokeType.Dotted });
// Long dotted line
setMemberState({strokeType: EStrokeType.LongDotted });- Set stroke and shape border opacity (marker):
setMemberState({strokeOpacity: 0.5 });- Set text color, opacity, background color, and opacity
setMemberState({textOpacity: 0.5, textBgOpacity: 0.5, textBgColor:[0, 0, 0]});- Set shape fill color and opacity
setMemberState({fillOpacity: 0.5, fillColor:[0, 0, 0]});- Custom regular polygon
// Regular pentagon
setMemberState({currentApplianceName: ApplianceNames.shape, shapeType: ShapeType.Polygon, vertices: 5});- Custom star shape
// Fat hexagonal star
setMemberState({currentApplianceName: ApplianceNames.shape, shapeType: ShapeType.Star, vertices: 12, innerVerticeStep: 2, innerRatio: 0.8});- Custom speech balloon placement
// Speech balloon in the lower left corner
setMemberState({currentApplianceName: ApplianceNames.shape, shapeType: ShapeType.SpeechBalloon, placement: 'bottomLeft'});Split screen display notes (little whiteboard feature), need to combine @netless/app-little-white-board (Version >=1.1.3)
Minimap function (Version >=1.1.6)
/** Create a minimap
* @param viewId ID of the whiteboard under multi-whiteboard, the main whiteboard ID is `mainView`, other whiteboard IDs are the appID returned by addApp()
* @param div Minimap DOM container
*/
createMiniMap(viewId: string, div: HTMLElement): Promise<void>;
/** Destroy minimap */
destroyMiniMap(viewId: string): Promise<boolean>;Text editing API (Version >=1.1.18)
/** Insert text at the specified position
* @param x The x coordinate of the left edge midpoint of the first character in the world coordinate system
* @param y The y coordinate of the left edge midpoint of the first character in the world coordinate system
* @param textContent Initial text content, empty if not provided
* @returns The identifier of the text
*/
insertText(x: number, y: number, textContent?: string): string | undefined;
/** Edit the content of the specified text
* @param identifier The identifier of the text, returned by insertText()
* @param textContent The new content of the text
*/
updateText(identifier: string, textContent: string): void;
/** Remove text focus */
blurText(): void;Element query API (Version >=1.1.19)
/** Check if there are elements in the specified scene
* @param scenePath Scene path, defaults to the currently focused scene
* @param filter Filter condition
* @returns Whether elements exist
*/
hasElements(
scenePath?: string,
filter?: (toolsType: EToolsKey) => boolean,
): boolean;
/** Get all elements in the specified scene
* @param scenePath Scene path, defaults to the currently focused scene
* @param filter Filter condition
* @returns All elements
*/
getElements(
scenePath?: string,
filter?: (toolsType: EToolsKey) => boolean,
): BaseCollectorReducerAction[];Filter notes (Version >=1.1.6)
/** Filter notes
* @param viewId ID of the whiteboard under multi-whiteboard, the main whiteboard ID is `mainView`, other whiteboard IDs are the appID returned by addApp()
* @param filter Filter condition
* render: Whether notes can be rendered, [uid1, uid2, ...] or true. true means all will be rendered; [uid1, uid2, ...] is the specified set of user uids to render
* hide: Whether notes are hidden, [uid1, uid2, ...] or true. true means all will be hidden; [uid1, uid2, ...] is the specified set of user uids to hide
* clear: Whether notes can be erased, [uid1, uid2, ...] or true. true means all can be erased; [uid1, uid2, ...] is the specified set of user uids that can be erased
* @param isSync Whether to synchronize to the whiteboard room, default is true, meaning the setting will be synchronized to all users
*/
filterRenderByUid(viewId: string, filter: { render?: _ArrayTrue, hide?: _ArrayTrue, clear?: _ArrayTrue}, isSync?:boolean): void;
/** Cancel filter notes
* @param viewId ID of the whiteboard under multi-whiteboard, the main whiteboard ID is `mainView`, other whiteboard IDs are the appID returned by addApp()
* @param isSync Whether to synchronize to the whiteboard room, default is true, meaning it will be synchronized to other users. Please keep it consistent with the filterRenderByUid setting
*/
cancelFilterRender(viewId: string, isSync?:boolean): void;Set whiteboard local scene path change (Version >=1.1.27)
/** Set whiteboard local scene path change
* @param viewId ID of the whiteboard under multi-whiteboard, the main whiteboard ID is `mainView`, other whiteboard IDs are the appID returned by addApp()
* @param scenePath The scene path to set
*/
setViewLocalScenePathChange(viewId: string, scenePath: string): Promise<void>;ExtrasOption custom tool configuration
Custom stroke styles
- Short dotted line style
export type DottedOpt = { /** Dotted line endpoint style, square: flat, round: round, default is round */ lineCap: "square" | "round"; /** Dotted line, single segment length, default is 1, meaning single segment length is 1 */ segment: number; /** Dotted line, single segment gap, default is 2, meaning single segment gap is 2 * thickness */ gap: number; }; /** Short dotted line style */ dottedStroke: { lineCap: "round", segment: 1, gap: 2, },- Long dotted line style
export type LongDottedOpt = { /** Long dotted line endpoint style, square: flat, round: round, default is round */ lineCap: "square" | "round"; /** Long dotted line, single segment length, default is 1, meaning single segment length is 1 * thickness */ segment: number; /** Long dotted line, single segment gap, default is 2, meaning single segment gap is 2 * thickness */ gap: number; }; /** Long dotted line style */ longDottedStroke: { lineCap: "round", segment: 2, gap: 3, },- Normal stroke style
export type NormalOpt = { /** Endpoint style, square: flat, round: round, default is round */ lineCap: "square" | "round"; }; /** Normal stroke style */ normalStroke: { lineCap: "round", }Text custom styles
export type TextEditorOpt = {
/** Whether to show float bar */
showFloatBar?: boolean;
/** Whether can switch by selector tool */
canSelectorSwitch?: boolean;
/** Whether right boundary auto wrap */
rightBoundBreak?: boolean;
/** Extended font list */
extendFontFaces?: { fontFamily: string; src: string }[];
/** Font loading timeout, unit: milliseconds */
loadFontFacesTimeout?: number;
};
// For example: set unified font library
textEditor: {
showFloatBar: false,
canSelectorSwitch: false,
rightBoundBreak: true,
extendFontFaces: [
{
fontFamily: "Noto Sans SC",
src: "https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-mu0SC55I.woff2",
},
],
loadFontFacesTimeout: 20000,
},Need to combine CSS style implementation
@font-face {
font-family: "Noto Sans SC";
src: url("https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-mu0SC55I.woff2")
format("woff2");
font-display: swap;
}
html {
font-family: "Noto Sans SC";
}Insert Mind Map (requires markdown text) (Version >=1.1.32)
insertMarkmapandupdateMarkmapare optional Markmap APIs. They cannot be used directly after importing only@netless/appliance-plugin. If your project needs them, installmarkmap-lib,markmap-view, andmermaid, and import@netless/appliance-plugin/markmaponce before calling either API.
import { ApplianceMultiPlugin } from '@netless/appliance-plugin';
import '@netless/appliance-plugin/markmap';
const plugin = await ApplianceMultiPlugin.getInstance(manager, {
options: {
cdn: {...}
extras: {
...,
useBackgroundThread: true,
}
},
});
const markId = await plugin.insertMarkmap(viewId, {
data: `# First Level Title
## Second Level Title 1
### Third Level Title 1
### Third Level Title 2
#### Fourth Level Title 1
#### Fourth Level Title 2
#### Fourth Level Title 3
## Second Level Title 2
### Third Level Title 1
### Third Level Title 2`,
uuid: 'unique identifier',
centerX: 0,
centerY: 0,
width: 200,
height: 200,
locked: false,
});
plugin.updateMarkmap(viewId, markId, {
data: `# First Level Title
## Second Level Title 1
## Second Level Title 2
### Third Level Title 1
### Third Level Title 2`,
uuid: 'unique identifier',
centerX: 0,
centerY: 0,
width: 200,
height: 200,
locked: false,
} )
Insert Background Image (Version >=1.1.32)
import { ApplianceMultiPlugin } from '@netless/appliance-plugin';
const plugin = await ApplianceMultiPlugin.getInstance(manager, {
options: {
cdn: {...}
extras: {
...,
useBackgroundThread: true,
}
},
});
plugin.insertBackgroundImage(viewId, {
src: 'https://example.com/background.png'
uuid: 'unique identifier',
centerX: 0,
centerY: 0,
width: 200,
height: 200,
locked: true,
})Handwriting graphics automatic association function:autoDraw, need to combine @netless/appliance-extend-auto-draw-plugin
export interface AutoDrawOptions {
/** API key for accessing all OpenRouter models */
apiKey?: string;
/** Custom model to use */
customModel?: string;
/** Container for rendering icons */
container: HTMLDivElement;
/** Delay time for rendering icons, default is 2000ms */
delay?: number;
/**
* Upload file to OSS server and return URL address, if returns undefined then this feature will not be used
* @param file File object
* @returns Image URL string
*/
uploadFile?: (file: File) => Promise<string | undefined>;
}
import { ApplianceMultiPlugin } from '@netless/appliance-plugin';
import { AutoDrawPlugin } from '@netless/appliance-extend-auto-draw-plugin';
const plugin = await ApplianceMultiPlugin.getInstance(...);
const autoDrawPlugin = new AutoDrawPlugin({
container: topBarDiv,
delay: 2000
});
plugin.usePlugin(autoDrawPlugin);Configuration Parameters
getInstance(wm: WindowManager | Room | Player, adaptor: ApplianceAdaptor)
wm:WindowManager | Room | Player. In multi-window mode, passWindowManager, in single-window mode, passRoomorPlayer(whiteboard playback mode).adaptor: Configuration adapter.options: AppliancePluginOptions- Must be configured, wherecdncontains worker URLs.export type AppliancePluginOptions = { /** Worker URL configuration item */ cdn: CdnOpt; /** Additional configuration items */ extras?: ExtrasOptions; }; export type CdnOpt = { /** Full worker URL address, thread for drawing complete data */ fullWorkerUrl?: string; /** Sub worker URL address, thread for drawing one frame of data */ subWorkerUrl?: string; }; export type ExtrasOptions = { /** Whether to use simple mode, default value is ``false`` * true: Simple mode: 1. Drawing will use single worker, bezier smoothing cannot be used during drawing. 2. Remove some new features: minimap, pointerPen (laser pen), autoDraw plugin. */ useSimple: boolean; /** Whether to use worker, default value is ``auto`` * auto: Automatically select (use webWorker if browser supports offscreenCanvas, otherwise use main thread) * mainThread: Use main thread, canvas drawing data. */ useWorker?: UseWorkerType; /** Worker render blacklist by runtime version. A major-only key such as * `"12"` applies to every `12.x` version. Render mode level `2` falls * back completely to main-thread rendering. */ workerRenderModeBlacklist?: WorkerBlacklistByRuntime; /** Whether to use backgroundThread, default value is ``false`` * true: Use backgroundThread, can call ``insertMarkmap``, ``updateMarkmap``, ``insertBackgroundImage`` * false: Do not use backgroundThread */ useBackgroundThread?: boolean; /** Synchronization data configuration item */ syncOpt?: SyncOpt; /** Canvas configuration item */ canvasOpt?: CanvasOpt; /** Pointer configuration item */ cursor?: CursorOpt; /** Canvas cache configuration item */ bufferSize?: BufferSizeOpt; /** Bezier optimization configuration item */ bezier?: BezierOpt; /** Partial eraser configuration item */ pencilEraser?: PencilEraserOpt; /** Stroke width range configuration item */ strokeWidth?: StrokeWidthOpt, /** Text editor configuration item */ textEditor?: TextEditorOpt; /** Undo redo configuration item */ undoRedo?: { /** Whether to enable global undo redo, default value is false (Version >=1.1.27) */ enableGlobal?: boolean; /** Maximum stack length for undo redo, default value is 20 */ maxStackLength?: number; }; }cursorAdapter?: CursorAdapter- Optional, in single whiteboard mode, configure custom mouse style.logger?: Logger- Optional, configure log printer object. If not provided, defaults to local console output. If logs need to be uploaded to a specified server, manual configuration is required.If you need to upload to the whiteboard log server, you can configure
room.loggerto this item.
Front-end Debugging
During the integration process, if you want to understand and track the internal status of the plugin, you can view internal data through the following console commands.
const appliancePlugin = await ApplianceSinglePlugin.getInstance(...)
appliancePlugin.currentManager // Can view package version number, internal status, etc.
appliancePlugin.currentManager.consoleWorkerInfo() // Can view drawing information on workerUsage Examples
Basic Usage Example
import { ApplianceSinglePlugin } from '@netless/appliance-plugin';
import '@netless/appliance-plugin/dist/style.css';
import fullWorkerString from '@netless/appliance-plugin/dist/fullWorker.js?raw';
import subWorkerString from '@netless/appliance-plugin/dist/subWorker.js?raw';
const fullWorkerBlob = new Blob([fullWorkerString], {type: 'text/javascript'});
const fullWorkerUrl = URL.createObjectURL(fullWorkerBlob);
const subWorkerBlob = new Blob([subWorkerString], {type: 'text/javascript'});
const subWorkerUrl = URL.createObjectURL(subWorkerBlob);
const plugin = await ApplianceSinglePlugin.getInstance(room, {
options: {
cdn: {
fullWorkerUrl,
subWorkerUrl,
},
},
});Switch Drawing Tools
import { ApplianceNames, EStrokeType } from '@netless/appliance-plugin';
// Switch to pencil tool
room.setMemberState({ currentApplianceName: ApplianceNames.pencil });
// Switch to rectangle tool
room.setMemberState({ currentApplianceName: ApplianceNames.rectangle });
// Switch to laser pen tool
room.setMemberState({
currentApplianceName: ApplianceNames.laserPen,
strokeType: EStrokeType.Normal
});
// Switch to text tool
room.setMemberState({ currentApplianceName: ApplianceNames.text });Custom Style Example
// Set brush style to dotted line
room.setMemberState({
strokeType: EStrokeType.Dotted,
strokeOpacity: 0.8
});
// Set shape fill
room.setMemberState({
fillColor: [255, 0, 0], // Red
fillOpacity: 0.5
});
// Set text style
room.setMemberState({
textOpacity: 0.9,
textBgColor: [255, 255, 0], // Yellow background
textBgOpacity: 0.3
});Text Editing Example
// Insert text at specified position
const textId = plugin.insertText(100, 100, 'Hello World');
// Edit text content
plugin.updateText(textId, 'Updated Text');
// Remove text focus
plugin.blurText();Minimap Function Example
// Create minimap
const minimapDiv = document.getElementById('minimap');
await plugin.createMiniMap('mainView', minimapDiv);
// Destroy minimap
await plugin.destroyMiniMap('mainView');Undo/Redo Example
// Undo operation
const undoSteps = plugin.undo();
// Redo operation
const redoSteps = plugin.redo();
// Check if undo/redo is possible
const canUndo = plugin.canUndoSteps() > 0;
const canRedo = plugin.canRedoSteps() > 0;FAQ
1. How to choose the right integration method?
- fastboard: If you are using the fastboard framework, it is recommended to use fastboard's integration method, which has the simplest configuration
- Multi-window scenario: If you need multi-window functionality, use
ApplianceMultiPlugin - Single whiteboard scenario: If you only need single whiteboard functionality, use
ApplianceSinglePlugin
2. Performance optimization recommendations
- Reasonably configure
bufferSizeto adjust canvas cache size according to device performance - On mobile or low-performance devices, consider using
useSimple: truesimple mode - If there are unnecessary features, you can avoid enabling
useBackgroundThread: true
3. Compatibility notes
- Supports modern browsers (Chrome, Firefox, Safari, Edge)
- Mobile browser support depends on OffscreenCanvas support
- Devices that do not support OffscreenCanvas will automatically downgrade to main thread mode
Version History
For detailed version update records, please refer to CHANGELOG.md
License
MIT License
