aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/InkingStroke.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/InkingStroke.tsx')
-rw-r--r--src/client/views/InkingStroke.tsx233
1 files changed, 160 insertions, 73 deletions
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index ecb46a5b3..5c7fc94bd 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -1,62 +1,82 @@
+/*
+ InkingStroke - a document that represents an individual vector stroke drawn as a Bezier curve (open or closed) and optionally filled.
+
+ The primary data is:
+ data - an InkField which is an array of PointData (X,Y values). The data is laid out as a sequence of simple bezier segments:
+ point 1, tangent pt 1, tangent pt 2, point 2, point 3, tangent pt 3, ... (Note that segment endpoints are duplicated ie Point2 = Point 3)
+ brokenIndices - an array of indexes into the data field where the incoming and outgoing tangents are not constrained to be equal
+ text - a text field that will be centered within a closed ink stroke
+ isInkMask - a flag that makes the ink stroke render as a mask over its collection where the stroke itself is mixBlendMode multiplied by
+ the underlying collection content, and everything outside the stroke is covered by a semi-opaque dark gray mask.
+
+ The coordinates of the ink data need to be mapped to the screen since ink points are not changed when the DocumentView is translated or scaled.
+ Thus the mapping can roughly be described by:
+ the Top/Left of the ink data (minus 1/2 the ink width) maps to the Top/Left of the DocumentView
+ the Width/Height of the ink data (minus the ink width) is scaled to the PanelWidth/PanelHeight of the documentView
+ NOTE: use ptToScreen() and ptFromScreen() to transform between ink and screen space
+
+ InkStrokes have a specialized 'componentUI' method that is called by MainView to render all of the interactive editing controls in
+ screen space (to avoid scaling artifacts)
+
+ Most of the operations that can be performed on an InkStroke (eg delete a point, rotate, stretch) are implemented in the InkStrokeProperties helper class
+*/
import React = require("react");
import { action, IReactionDisposer, observable, reaction } from "mobx";
import { observer } from "mobx-react";
-import { Doc } from "../../fields/Doc";
+import { Doc, WidthSym } from "../../fields/Doc";
import { documentSchema } from "../../fields/documentSchemas";
import { InkData, InkField, InkTool } from "../../fields/InkField";
import { makeInterface } from "../../fields/Schema";
import { BoolCast, Cast, NumCast, StrCast } from "../../fields/Types";
import { TraceMobx } from "../../fields/util";
-import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../Utils";
+import { OmitKeys, returnFalse, setupMoveUpEvents } from "../../Utils";
import { CognitiveServices } from "../cognitive_services/CognitiveServices";
-import { CurrentUserUtils } from "../util/CurrentUserUtils";
import { InteractionUtils } from "../util/InteractionUtils";
import { SnappingManager } from "../util/SnappingManager";
+import { Transform } from "../util/Transform";
+import { UndoManager } from "../util/UndoManager";
import { ContextMenu } from "./ContextMenu";
import { ViewBoxBaseComponent } from "./DocComponent";
import { Colors } from "./global/globalEnums";
-import { InkControlPtHandles } from "./InkControlPtHandles";
+import { InkControlPtHandles, InkEndPtHandles } from "./InkControlPtHandles";
import "./InkStroke.scss";
import { InkStrokeProperties } from "./InkStrokeProperties";
import { InkTangentHandles } from "./InkTangentHandles";
import { FieldView, FieldViewProps } from "./nodes/FieldView";
+import { FormattedTextBox } from "./nodes/formattedText/FormattedTextBox";
import Color = require("color");
-import { Transform } from "../util/Transform";
type InkDocument = makeInterface<[typeof documentSchema]>;
const InkDocument = makeInterface(documentSchema);
@observer
export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocument>(InkDocument) {
+ static readonly MaskDim = 50000; // choose a really big number to make sure mask fits over container (which in theory can be arbitrarily big)
public static LayoutString(fieldStr: string) { return FieldView.LayoutString(InkingStroke, fieldStr); }
- static readonly MaskDim = 50000;
public static IsClosed(inkData: InkData) {
return inkData && inkData.lastElement().X === inkData[0].X && inkData.lastElement().Y === inkData[0].Y;
}
- @observable private _properties?: InkStrokeProperties;
- _handledClick = false; // flag denoting whether ink stroke has handled a psuedo-click onPointerUp so that the real onClick event can be stopPropagated
- _selDisposer: IReactionDisposer | undefined;
+ private _handledClick = false; // flag denoting whether ink stroke has handled a psuedo-click onPointerUp so that the real onClick event can be stopPropagated
+ private _selDisposer?: IReactionDisposer;
- @observable _nearestT: number | undefined;
- @observable _nearestSeg: number | undefined;
- @observable _nearestScrPt: { X: number, Y: number } | undefined;
- @observable _inkSamplePts: { X: number, Y: number }[] | undefined;
-
- constructor(props: FieldViewProps & InkDocument) {
- super(props);
-
- this._properties = InkStrokeProperties.Instance;
- }
+ @observable _nearestSeg?: number; // nearest Bezier segment along the ink stroke to the cursor (used for displaying the Add Point highlight)
+ @observable _nearestT?: number; // nearest t value within the nearest Bezier segment "
+ @observable _nearestScrPt?: { X: number, Y: number }; // nearst screen point on the ink stroke ""
componentDidMount() {
this.props.setContentView?.(this);
this._selDisposer = reaction(() => this.props.isSelected(), // react to stroke being deselected by turning off ink handles
- selected => !selected && this.toggleControlButton());
+ selected => !selected && (InkStrokeProperties.Instance._controlButton = false));
}
componentWillUnmount() {
this._selDisposer?.();
}
+ /**
+ * @returns the center of the ink stroke in the ink document's coordinate space (not screen space, and not the ink data coordinate space);
+ * DocumentDecorations calls getBounds() on DocumentViews which call getCenter() if defined - in the case of ink it needs to be defined since
+ * the center of the ink stroke changes as the stroke is rotated.
+ */
getCenter = (xf: Transform) => {
const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
const angle = -NumCast(this.layoutDoc.rotation);
@@ -73,11 +93,21 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
return { X: tc[0], Y: tc[1] };
}
+ /**
+ * analyzes the ink stroke and saves the analysis of the stroke to the 'inkAnalysis' field,
+ * and the recognized words to the 'handwriting'
+ */
analyzeStrokes() {
const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], [data]);
}
+ /**
+ * Toggles whether the ink stroke is displayed as an overlay mask or as a regular stroke.
+ * When displayed as a mask, the stroke is rendered with mixBlendMode set to multiply so that the stroke will
+ * appear to illuminate what it covers up. At the same time, all pixels that are not under the stroke will be
+ * dimmed by a semi-opaque overlay mask.
+ */
public static toggleMask = action((inkDoc: Doc) => {
inkDoc.isInkMask = !inkDoc.isInkMask;
inkDoc._backgroundColor = inkDoc.isInkMask ? "rgba(0,0,0,0.7)" : undefined;
@@ -86,44 +116,53 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
inkDoc._stayInCollection = inkDoc.isInkMask ? true : undefined;
});
/**
- * Handles the movement of the entire ink object when the user clicks and drags.
+ * Drags the a simple bezier segment of the stroke.
+ * Also adds a control point when double clicking on the stroke.
*/
+ @action
onPointerDown = (e: React.PointerEvent) => {
this._handledClick = false;
- if (this.props.isSelected(true)) {
- setupMoveUpEvents(this, e, returnFalse, emptyFunction,
- action((e: PointerEvent, doubleTap: boolean | undefined) => {
- doubleTap = doubleTap || this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick;
- if (doubleTap && this._properties) {
- this._properties._controlButton = true;
- InkStrokeProperties.Instance && (InkStrokeProperties.Instance._currentPoint = -1);
- this._handledClick = true; // mark the double-click pseudo pointerevent so we can block the real mouse event from propagating to DocumentView
- } else if (this._properties?._controlButton) {
- this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance?.addPoints(this.props.docViewPath().lastElement(), this._nearestT, this._nearestSeg, this.inkScaledData().inkData.slice());
+ const inkView = this.props.docViewPath().lastElement();
+ const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
+ const screenPts = inkData.map(point => this.props.ScreenToLocalTransform().inverse().transformPoint(
+ (point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2,
+ (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2)).map(p => ({ X: p[0], Y: p[1] }));
+ const { nearestSeg } = InkStrokeProperties.nearestPtToStroke(screenPts, { X: e.clientX, Y: e.clientY });
+ const controlIndex = nearestSeg;
+ const wasSelected = InkStrokeProperties.Instance._currentPoint === controlIndex;
+ var controlUndo: UndoManager.Batch | undefined;
+ const isEditing = InkStrokeProperties.Instance._controlButton && this.props.isSelected();
+ setupMoveUpEvents(this, e,
+ !isEditing ? returnFalse : action((e: PointerEvent, down: number[], delta: number[]) => {
+ if (!controlUndo) controlUndo = UndoManager.StartBatch("drag ink ctrl pt");
+ const inkMoveEnd = this.ptFromScreen({ X: delta[0], Y: delta[1] });
+ const inkMoveStart = this.ptFromScreen({ X: 0, Y: 0 });
+ InkStrokeProperties.Instance.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex);
+ InkStrokeProperties.Instance.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex + 3);
+ return false;
+ }),
+ !isEditing ? returnFalse : action(() => {
+ controlUndo?.end();
+ controlUndo = undefined;
+ UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
+ }),
+ action((e: PointerEvent, doubleTap: boolean | undefined) => {
+ doubleTap = doubleTap || this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick;
+ if (doubleTap) {
+ InkStrokeProperties.Instance._controlButton = true;
+ InkStrokeProperties.Instance._currentPoint = -1;
+ this._handledClick = true; // mark the double-click pseudo pointerevent so we can block the real mouse event from propagating to DocumentView
+ if (isEditing) {
+ this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance.addPoints(this.props.docViewPath().lastElement(), this._nearestT, this._nearestSeg, this.inkScaledData().inkData.slice());
}
- }), this._properties?._controlButton, this._properties?._controlButton
- );
- }
+ }
+ }), isEditing, isEditing, action(() => wasSelected && (InkStrokeProperties.Instance._currentPoint = -1)));
}
/**
- * Ensures the ink controls and handles aren't rendered when the current ink stroke is reselected.
+ * @param scrPt a point in the screen coordinate space
+ * @returns the point in the ink data's coordinate space.
*/
- @action
- toggleControlButton = () => {
- if (!this.props.isSelected() && this._properties) {
- this._properties._controlButton = false;
- }
- }
-
- @action
- checkHighlighter = () => {
- if (CurrentUserUtils.SelectedTool === InkTool.Highlighter) {
- // this._previousColor = ActiveInkColor();
- SetActiveInkColor("rgba(245, 230, 95, 0.75)");
- }
- }
-
ptFromScreen = (scrPt: { X: number, Y: number }) => {
const { inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
const docPt = this.props.ScreenToLocalTransform().transformPoint(scrPt.X, scrPt.Y);
@@ -133,6 +172,11 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
};
return inkPt;
}
+
+ /**
+ * @param inkPt a point in the ink data's coordinate space
+ * @returns the screen point corresponding to the ink point
+ */
ptToScreen = (inkPt: { X: number, Y: number }) => {
const { inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
const docPt = {
@@ -143,13 +187,23 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
return { X: scrPt[0], Y: scrPt[1] };
}
+ /**
+ * Snaps a screen space point to this stroke, optionally skipping bezier segments indicated by 'excludeSegs'
+ * @param scrPt - the point to snap to this stroke
+ * @param excludeSegs - optional segments in this stroke to skip (this is used when dragging a point on the stroke and not wanting the drag point to snap to its neighboring segments)
+ *
+ * @returns the nearest ink space point on this stroke to the screen point AND the screen space distance from the snapped point to the nearest point
+ */
snapPt = (scrPt: { X: number, Y: number }, excludeSegs?: number[]) => {
const { inkData } = this.inkScaledData();
- const inkPt = this.ptFromScreen(scrPt);
- const { nearestPt, distance } = InkStrokeProperties.nearestPtToStroke(inkData, inkPt, excludeSegs ?? []);
+ const { nearestPt, distance } = InkStrokeProperties.nearestPtToStroke(inkData, this.ptFromScreen(scrPt), excludeSegs ?? []);
return { nearestPt, distance: distance * this.props.ScreenToLocalTransform().inverse().Scale };
}
+ /**
+ * extracts key features from the inkData, including: the data points, the ink width, the ink bounds (top,left, width, height), and the scale
+ * factor for converting between ink and screen space.
+ */
inkScaledData = () => {
const inkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
const inkStrokeWidth = NumCast(this.rootDoc.strokeWidth, 1);
@@ -184,8 +238,16 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
this._nearestScrPt = nearestPt;
}
-
+ /**
+ * @returns the nearest screen point to the cursor (to render a highlight for the point to be added)
+ */
nearestScreenPt = () => this._nearestScrPt;
+
+ /**
+ * @param boundsLeft the screen space left coordinate of the ink stroke
+ * @param boundsTop the screen space top coordinate of the ink stroke
+ * @returns the JSX controls for displaying an editing UI for the stroke (control point & tangent handles)
+ */
componentUI = (boundsLeft: number, boundsTop: number) => {
const inkDoc = this.props.Document;
const screenSpaceCenterlineStrokeWidth = 3; // the width of the blue line widget that shows the centerline of the ink stroke
@@ -199,14 +261,21 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
const startMarker = StrCast(this.layoutDoc.strokeStartMarker);
const endMarker = StrCast(this.layoutDoc.strokeEndMarker);
- return SnappingManager.GetIsDragging() ? (null) : <div className="inkstroke-UI" style={{
- clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)`
- }} >
- {!this._properties?._controlButton ? (null) :
- <>
+ const markerScale = NumCast(this.layoutDoc.strokeMarkerScale, 1);
+ return SnappingManager.GetIsDragging() ? (null) :
+ !InkStrokeProperties.Instance._controlButton ?
+ (!this.props.isSelected() || InkingStroke.IsClosed(inkData) ? (null) :
+ <div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
+ <InkEndPtHandles
+ inkView={this.props.docViewPath().lastElement()}
+ inkDoc={inkDoc}
+ startPt={this.ptToScreen(inkData[0])}
+ endPt={this.ptToScreen(inkData.lastElement())}
+ screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} /></div>) :
+ <div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
{InteractionUtils.CreatePolyline(screenPts, 0, 0, Colors.MEDIUM_BLUE, screenInkWidth[0], screenSpaceCenterlineStrokeWidth,
StrCast(inkDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap), StrCast(inkDoc.strokeBezier),
- "none", startMarker, endMarker, StrCast(inkDoc.strokeDash), 1, 1, "", "none", 1.0, false)}
+ "none", startMarker, endMarker, markerScale, StrCast(inkDoc.strokeDash), 1, 1, "", "none", 1.0, false)}
<InkControlPtHandles
inkView={this.props.docViewPath().lastElement()}
inkDoc={inkDoc}
@@ -221,8 +290,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
screenCtrlPoints={screenHdlPts}
screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth}
ScreenToLocalTransform={this.props.ScreenToLocalTransform} />
- </>}
- </div>;
+ </div>;
}
render() {
@@ -231,6 +299,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
const startMarker = StrCast(this.layoutDoc.strokeStartMarker);
const endMarker = StrCast(this.layoutDoc.strokeEndMarker);
+ const markerScale = NumCast(this.layoutDoc.strokeMarkerScale, 1);
const closed = InkingStroke.IsClosed(inkData);
const fillColor = StrCast(this.layoutDoc.fillColor, "transparent");
const strokeColor = !closed && fillColor && fillColor !== "transparent" ? fillColor : StrCast(this.layoutDoc.color);
@@ -239,43 +308,59 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
const inkLine = InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, strokeColor, inkStrokeWidth, inkStrokeWidth,
StrCast(this.layoutDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap),
StrCast(this.layoutDoc.strokeBezier), !closed ? "none" : fillColor === "transparent" ? "none" : fillColor, startMarker, endMarker,
- StrCast(this.layoutDoc.strokeDash), inkScaleX, inkScaleY, "", "none", 1.0, false);
+ markerScale, StrCast(this.layoutDoc.strokeDash), inkScaleX, inkScaleY, "", "none", 1.0, false);
const highlightIndex = BoolCast(this.props.Document.isLinkButton) && Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString
const highlightColor = !highlightIndex ?
StrCast(this.layoutDoc.strokeOutlineColor, !closed && fillColor && fillColor !== "transparent" ? StrCast(this.layoutDoc.color, "transparent") : "transparent") :
["transparent", "rgb(68, 118, 247)", "rgb(68, 118, 247)", "yellow", "magenta", "cyan", "orange"][highlightIndex];
// Invisible polygonal line that enables the ink to be selected by the user.
- const clickableLine = InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, highlightColor,
- inkStrokeWidth, inkStrokeWidth + (highlightIndex && closed && (new Color(fillColor)).alpha() < 1 ? 6 : 15),
+ const clickableLine = (downHdlr?: (e: React.PointerEvent) => void) => InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, highlightColor,
+ inkStrokeWidth, inkStrokeWidth + (highlightIndex && closed && fillColor && (new Color(fillColor)).alpha() < 1 ? 6 : 15),
StrCast(this.layoutDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap),
StrCast(this.layoutDoc.strokeBezier), !closed ? "none" : fillColor === "transparent" ? "none" : fillColor, startMarker, endMarker,
- undefined, inkScaleX, inkScaleY, "", this.props.pointerEvents ?? (this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted"), 0.0, false);
- // Set of points rendered upon the ink that can be added if a user clicks on one.
+ markerScale, undefined, inkScaleX, inkScaleY, "", this.props.pointerEvents ?? (this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted"), 0.0,
+ false, downHdlr);
- return (
+ return <div className="inkStroke-wrapper">
<svg className="inkStroke"
style={{
- pointerEvents: "none",
transform: this.props.Document.isInkMask ? `translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined,
mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset",
- overflow: "visible",
cursor: this.props.isSelected() ? "default" : undefined
}}
onPointerLeave={action(e => this._nearestScrPt = undefined)}
onPointerMove={this.props.isSelected() ? this.onPointerMove : undefined}
- onPointerDown={this.onPointerDown}
onClick={e => this._handledClick && e.stopPropagation()}
onContextMenu={() => {
const cm = ContextMenu.Instance;
!Doc.UserDoc().noviceMode && cm?.addItem({ description: "Recognize Writing", event: this.analyzeStrokes, icon: "paint-brush" });
cm?.addItem({ description: "Toggle Mask", event: () => InkingStroke.toggleMask(this.rootDoc), icon: "paint-brush" });
- cm?.addItem({ description: "Edit Points", event: action(() => { if (this._properties) { this._properties._controlButton = !this._properties._controlButton; } }), icon: "paint-brush" });
+ cm?.addItem({ description: "Edit Points", event: action(() => InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton), icon: "paint-brush" });
}}
>
- {clickableLine}
+ {clickableLine(this.onPointerDown)}
{inkLine}
</svg>
- );
+ {!closed ? (null) :
+ <div className="inkStroke-text" style={{
+ color: StrCast(this.layoutDoc.textColor, "black"),
+ pointerEvents: this.props.isDocumentActive?.() ? "all" : undefined,
+ width: this.layoutDoc[WidthSym](),
+ }}>
+ <FormattedTextBox
+ {...OmitKeys(this.props, ['children']).omit}
+ yPadding={10}
+ xPadding={10}
+ fieldKey={"text"}
+ fontSize={12}
+ dontRegisterView={true}
+ noSidebar={true}
+ dontScale={true}
+ isContentActive={this.isContentActive}
+ />
+ </div>
+ }
+ </div>;
}
}
@@ -286,12 +371,14 @@ export function SetActiveInkColor(value: string) { ActiveInkPen() && (ActiveInkP
export function SetActiveFillColor(value: string) { ActiveInkPen() && (ActiveInkPen().activeFillColor = value); }
export function SetActiveArrowStart(value: string) { ActiveInkPen() && (ActiveInkPen().activeArrowStart = value); }
export function SetActiveArrowEnd(value: string) { ActiveInkPen() && (ActiveInkPen().activeArrowEnd = value); }
+export function SetActiveArrowScale(value: number) { ActiveInkPen() && (ActiveInkPen().activeArrowScale = value); }
export function SetActiveDash(dash: string): void { !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash); }
export function ActiveInkPen(): Doc { return Doc.UserDoc(); }
export function ActiveInkColor(): string { return StrCast(ActiveInkPen()?.activeInkColor, "black"); }
export function ActiveFillColor(): string { return StrCast(ActiveInkPen()?.activeFillColor, ""); }
export function ActiveArrowStart(): string { return StrCast(ActiveInkPen()?.activeArrowStart, ""); }
export function ActiveArrowEnd(): string { return StrCast(ActiveInkPen()?.activeArrowEnd, ""); }
+export function ActiveArrowScale(): number { return NumCast(ActiveInkPen()?.activeArrowScale, 1); }
export function ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, "0"); }
export function ActiveInkWidth(): number { return Number(ActiveInkPen()?.activeInkWidth); }
export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); }