import { Bezier } from 'bezier-js'; import { alias, createSimpleSchema, list, object, serializable } from 'serializr'; import { ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { Deserializable } from '../client/util/SerializationHelper'; import { PointData } from '../pen-gestures/GestureTypes'; import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; import { ObjectField } from './ObjectField'; // Helps keep track of the current ink tool in use. export enum InkTool { None = 'None', Ink = 'Ink', Eraser = 'Eraser', // not a real tool, but a class of tools SmartDraw = 'smartdraw', } export enum InkInkTool { Pen = 'Pen', Highlight = 'Highlight', Write = 'Write', } export enum InkEraserTool { Stroke = 'Stroke', Segment = 'Segment', Radius = 'Radius', } export enum InkProperty { Mask = 'inkMask', Labels = 'labels', StrokeWidth = 'strokeWidth', StrokeColor = 'strokeColor', EraserWidth = ' eraserWidth', } export type Segment = Array; // Defines an ink as an array of points. export type InkData = Array; export interface ControlPoint { X: number; Y: number; I: number; } export interface HandlePoint { X: number; Y: number; I: number; dot1: number; dot2: number; } export interface HandleLine { X1: number; Y1: number; X2: number; Y2: number; X3: number; Y3: number; dot1: number; dot2: number; } const pointSchema = createSimpleSchema({ X: true, Y: true, }); const strokeDataSchema = createSimpleSchema({ pathData: list(object(pointSchema)), '*': true, }); export const InkDataFieldName = '__inkData'; @Deserializable('ink') export class InkField extends ObjectField { @serializable(alias(InkDataFieldName, list(object(strokeDataSchema)))) readonly inkData: InkData; constructor(data: InkData) { super(); this.inkData = data; } /** * Extacts a simple segment from a compound Bezier curve * @param segIndex the start index of the simple bezier segment to extact (eg., 0, 4, 8, ...) */ public static Segment(inkData: InkData, segIndex: number) { return new Bezier(inkData.slice(segIndex, segIndex + 4).map(pt => ({ x: pt.X, y: pt.Y }))); } [Copy]() { return new InkField(this.inkData); } [ToJavascriptString]() { return '[' + this.inkData.map(i => `{X: ${i.X}, Y: ${i.Y}}`) + ']'; } [ToScriptString]() { return 'new InkField([' + this.inkData.map(i => `{X: ${i.X}, Y: ${i.Y}}`) + '])'; } [ToString]() { return 'InkField'; } public static getBounds(stroke: InkData, pad?: boolean) { const padding = pad ? [-20000, 20000] : []; const xs = [...padding, ...stroke.map(p => p.X)]; const ys = [...padding, ...stroke.map(p => p.Y)]; const right = Math.max(...xs); const left = Math.min(...xs); const bottom = Math.max(...ys); const top = Math.min(...ys); return { right, left, bottom, top, width: right - left, height: bottom - top }; } // for some reason bezier.js doesn't handle the case of intersecting a linear curve, so we wrap the intersection // call in a test for linearity public static bintersects(curve: Bezier, otherCurve: Bezier) { // eslint-disable-next-line @typescript-eslint/no-explicit-any if ((curve as any)._linear) { // bezier.js doesn't intersect properly if the curve is actually a line -- so get intersect other curve against this line, then figure out the t coordinates of the intersection on this line const intersections = otherCurve.lineIntersects({ p1: curve.points[0], p2: curve.points[3] }); if (intersections.length) { const intPt = otherCurve.get(intersections[0]); const intT = curve.project(intPt).t; return intT ? [intT] : []; } } // eslint-disable-next-line @typescript-eslint/no-explicit-any if ((otherCurve as any)._linear) { return curve.lineIntersects({ p1: otherCurve.points[0], p2: otherCurve.points[3] }); } return curve.intersects(otherCurve); } } ScriptingGlobals.add('InkField', InkField);