js-draw
Version:
Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript.
321 lines (320 loc) • 13.4 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const types_1 = require("../types");
const math_1 = require("@js-draw/math");
const PanZoom_1 = __importStar(require("./PanZoom"));
const Pen_1 = __importDefault(require("./Pen"));
const ToolEnabledGroup_1 = __importDefault(require("./ToolEnabledGroup"));
const Eraser_1 = __importDefault(require("./Eraser"));
const SelectionTool_1 = __importDefault(require("./SelectionTool/SelectionTool"));
const UndoRedoShortcut_1 = __importDefault(require("./UndoRedoShortcut"));
const TextTool_1 = __importDefault(require("./TextTool"));
const PipetteTool_1 = __importDefault(require("./PipetteTool"));
const ToolSwitcherShortcut_1 = __importDefault(require("./ToolSwitcherShortcut"));
const PasteHandler_1 = __importDefault(require("./PasteHandler"));
const ToolbarShortcutHandler_1 = __importDefault(require("./ToolbarShortcutHandler"));
const PressureSensitiveFreehandLineBuilder_1 = require("../components/builders/PressureSensitiveFreehandLineBuilder");
const FindTool_1 = __importDefault(require("./FindTool"));
const SelectAllShortcutHandler_1 = __importDefault(require("./SelectionTool/SelectAllShortcutHandler"));
const SoundUITool_1 = __importDefault(require("./SoundUITool"));
const inputEvents_1 = require("../inputEvents");
const InputPipeline_1 = __importDefault(require("./InputFilter/InputPipeline"));
const InputStabilizer_1 = __importDefault(require("./InputFilter/InputStabilizer"));
const ScrollbarTool_1 = __importDefault(require("./ScrollbarTool"));
class ToolController {
/** @internal */
constructor(editor, localization) {
this.activeTool = null;
this.isEditorReadOnly = editor.isReadOnlyReactiveValue();
this.inputPipeline = new InputPipeline_1.default();
this.inputPipeline.setEmitListener((event) => this.onEventInternal(event));
const primaryToolGroup = new ToolEnabledGroup_1.default();
this.primaryToolGroup = primaryToolGroup;
const panZoomTool = new PanZoom_1.default(editor, PanZoom_1.PanZoomMode.TwoFingerTouchGestures | PanZoom_1.PanZoomMode.RightClickDrags, localization.touchPanTool);
const keyboardPanZoomTool = new PanZoom_1.default(editor, PanZoom_1.PanZoomMode.Keyboard, localization.keyboardPanZoom);
const primaryPenTool = new Pen_1.default(editor, localization.penTool(1), {
color: math_1.Color4.purple,
thickness: 8,
});
const secondaryPenTool = new Pen_1.default(editor, localization.penTool(2), {
color: math_1.Color4.clay,
thickness: 4,
});
// Stabilize the secondary pen tool.
secondaryPenTool.setInputMapper(new InputStabilizer_1.default(editor.viewport));
const eraser = new Eraser_1.default(editor, localization.eraserTool);
const primaryTools = [
// Three pens
primaryPenTool,
secondaryPenTool,
// Highlighter-like pen with width=40
new Pen_1.default(editor, localization.penTool(3), {
color: math_1.Color4.ofRGBA(1, 1, 0, 0.5),
thickness: 40,
factory: PressureSensitiveFreehandLineBuilder_1.makePressureSensitiveFreehandLineBuilder,
}),
eraser,
new SelectionTool_1.default(editor, localization.selectionTool),
new TextTool_1.default(editor, localization.textTool, localization),
new PanZoom_1.default(editor, PanZoom_1.PanZoomMode.SinglePointerGestures, localization.anyDevicePanning),
];
// Accessibility tools
const soundExplorer = new SoundUITool_1.default(editor, localization.soundExplorer);
soundExplorer.setEnabled(false);
this.tools = [
new ScrollbarTool_1.default(editor),
new PipetteTool_1.default(editor, localization.pipetteTool),
soundExplorer,
panZoomTool,
...primaryTools,
keyboardPanZoomTool,
new UndoRedoShortcut_1.default(editor),
new ToolbarShortcutHandler_1.default(editor),
new ToolSwitcherShortcut_1.default(editor),
eraser.makeEraserSwitcherTool(),
new FindTool_1.default(editor),
new PasteHandler_1.default(editor),
new SelectAllShortcutHandler_1.default(editor),
];
primaryTools.forEach((tool) => tool.setToolGroup(primaryToolGroup));
panZoomTool.setEnabled(true);
primaryPenTool.setEnabled(true);
editor.notifier.on(types_1.EditorEventType.ToolEnabled, (event) => {
if (event.kind === types_1.EditorEventType.ToolEnabled) {
editor.announceForAccessibility(localization.toolEnabledAnnouncement(event.tool.description));
}
});
editor.notifier.on(types_1.EditorEventType.ToolDisabled, (event) => {
if (event.kind === types_1.EditorEventType.ToolDisabled) {
editor.announceForAccessibility(localization.toolDisabledAnnouncement(event.tool.description));
}
});
this.activeTool = null;
}
/**
* Replaces the current set of tools with `tools`. This should only be done before
* the creation of the app's toolbar (if using `AbstractToolbar`).
*
* If no `primaryToolGroup` is given, an empty one will be created.
*/
setTools(tools, primaryToolGroup) {
this.tools = tools;
this.primaryToolGroup = primaryToolGroup ?? new ToolEnabledGroup_1.default();
}
/**
* Add a tool that acts like one of the primary tools (only one primary tool can be enabled at a time).
*
* If the tool is already added to this, the tool is converted to a primary tool.
*
* This should be called before creating the app's toolbar.
*/
addPrimaryTool(tool) {
tool.setToolGroup(this.primaryToolGroup);
if (tool.isEnabled()) {
this.primaryToolGroup.notifyEnabled(tool);
}
if (!this.tools.includes(tool)) {
this.addTool(tool);
}
}
getPrimaryTools() {
return this.tools.filter((tool) => {
return tool.getToolGroup() === this.primaryToolGroup;
});
}
/**
* Add a tool to the end of this' tool list (the added tool receives events after tools already added to this).
* This should be called before creating the app's toolbar.
*
* If `options.addToFront`, the tool is added to the beginning of this' tool list.
*
* Does nothing if the tool is already present.
*/
addTool(tool, options) {
// Only add if not already present.
if (!this.tools.includes(tool)) {
if (options?.addToFront) {
this.tools.splice(0, 0, tool);
}
else {
this.tools.push(tool);
}
}
}
/**
* Removes **and destroys** all tools in `tools` from this.
*/
removeAndDestroyTools(tools) {
const newTools = [];
for (const tool of this.tools) {
if (tools.includes(tool)) {
if (this.activeTool === tool) {
this.activeTool = null;
}
tool.onDestroy();
}
else {
newTools.push(tool);
}
}
this.tools = newTools;
}
insertTools(insertNear, toolsToInsert, mode) {
this.tools = this.tools.filter((tool) => !toolsToInsert.includes(tool));
const newTools = [];
for (const tool of this.tools) {
if (mode === 'after') {
newTools.push(tool);
}
if (tool === insertNear) {
newTools.push(...toolsToInsert);
}
if (mode === 'before') {
newTools.push(tool);
}
}
this.tools = newTools;
}
/**
* Removes a tool from this' tool list and replaces it with `replaceWith`.
*
* If any of `toolsToInsert` have already been added to this, the tools are
* moved.
*
* This should be called before creating the editor's toolbar.
*/
insertToolsAfter(insertAfter, toolsToInsert) {
this.insertTools(insertAfter, toolsToInsert, 'after');
}
/** @see {@link insertToolsAfter} */
insertToolsBefore(insertBefore, toolsToInsert) {
this.insertTools(insertBefore, toolsToInsert, 'before');
}
// @internal use `dispatchEvent` rather than calling `onEvent` directly.
onEventInternal(event) {
const isEditorReadOnly = this.isEditorReadOnly.get();
const canToolReceiveInput = (tool) => {
return tool.isEnabled() && (!isEditorReadOnly || tool.canReceiveInputInReadOnlyEditor());
};
let handled = false;
if (event.kind === inputEvents_1.InputEvtType.PointerDownEvt) {
let canOnlySendToActiveTool = false;
if (this.activeTool && !this.activeTool.eventCanBeDeliveredToNonActiveTool(event)) {
canOnlySendToActiveTool = true;
}
for (const tool of this.tools) {
if (canOnlySendToActiveTool && tool !== this.activeTool) {
continue;
}
if (canToolReceiveInput(tool) && tool.onEvent(event)) {
if (this.activeTool !== tool) {
this.activeTool?.onEvent({ kind: inputEvents_1.InputEvtType.GestureCancelEvt });
}
this.activeTool = tool;
handled = true;
break;
}
}
}
else if (event.kind === inputEvents_1.InputEvtType.PointerUpEvt) {
const upResult = this.activeTool?.onEvent(event);
const continueHandlingEvents = upResult && event.allPointers.length > 1;
// Should the active tool continue handling events (without an additional pointer down?)
if (!continueHandlingEvents) {
// No -- Remove the current tool
this.activeTool = null;
}
handled = true;
}
else if (event.kind === inputEvents_1.InputEvtType.PointerMoveEvt) {
if (this.activeTool !== null) {
this.activeTool.onEvent(event);
handled = true;
}
}
else if (event.kind === inputEvents_1.InputEvtType.GestureCancelEvt) {
if (this.activeTool !== null) {
this.activeTool.onEvent(event);
this.activeTool = null;
}
}
else {
for (const tool of this.tools) {
if (!canToolReceiveInput(tool)) {
continue;
}
handled = tool.onEvent(event);
if (handled) {
break;
}
}
}
return handled;
}
/** Alias for {@link dispatchInputEvent}. */
onEvent(event) {
return this.dispatchInputEvent(event);
}
// Returns true if the event was handled.
dispatchInputEvent(event) {
// Feed the event through the input pipeline
return this.inputPipeline.onEvent(event);
}
/**
* Adds a new `InputMapper` to this' input pipeline.
*
* A `mapper` is really a relation that maps each event to no, one,
* or many other events.
*
* @see {@link InputMapper}.
*/
addInputMapper(mapper) {
this.inputPipeline.addToTail(mapper);
}
getMatchingTools(type) {
return this.tools.filter((tool) => tool instanceof type);
}
// @internal
onEditorDestroyed() {
for (const tool of this.tools) {
tool.onDestroy();
}
}
}
exports.default = ToolController;