diff options
Diffstat (limited to 'src/client/views')
| -rw-r--r-- | src/client/views/DocumentDecorations.tsx | 8 | ||||
| -rw-r--r-- | src/client/views/GlobalKeyHandler.ts | 2 | ||||
| -rw-r--r-- | src/client/views/InkControlPtHandles.tsx | 34 | ||||
| -rw-r--r-- | src/client/views/InkStroke.scss | 5 | ||||
| -rw-r--r-- | src/client/views/InkStrokeProperties.ts | 15 | ||||
| -rw-r--r-- | src/client/views/InkTangentHandles.tsx | 28 | ||||
| -rw-r--r-- | src/client/views/InkingStroke.tsx | 174 | ||||
| -rw-r--r-- | src/client/views/MainView.tsx | 1 | ||||
| -rw-r--r-- | src/client/views/PropertiesView.tsx | 21 | ||||
| -rw-r--r-- | src/client/views/StyleProvider.tsx | 2 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 2 | ||||
| -rw-r--r-- | src/client/views/search/SearchBox.tsx | 18 |
12 files changed, 157 insertions, 153 deletions
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 1c0b1b995..7d3959eba 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -203,9 +203,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P const centerPoint = { X: (left + right) / 2, Y: (top + bottom) / 2 }; const previousPoint = { X: e.clientX, Y: e.clientY }; const movedPoint = { X: e.clientX - delta[0], Y: e.clientY - delta[1] }; - const angle = InkStrokeProperties.Instance?.angleChange(previousPoint, movedPoint, centerPoint); + const angle = InkStrokeProperties.Instance.angleChange(previousPoint, movedPoint, centerPoint); const selectedInk = SelectionManager.Views().filter(i => Document(i.rootDoc).type === DocumentType.INK); - angle && InkStrokeProperties.Instance?.rotateInk(selectedInk, -angle, pt); + angle && InkStrokeProperties.Instance.rotateInk(selectedInk, -angle, pt); return false; }, () => { @@ -226,7 +226,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P this._inkDragDocs = DragManager.docsBeingDragged .filter(doc => doc.type === DocumentType.INK) .map(doc => { - if (InkStrokeProperties.Instance?._lock) { + if (InkStrokeProperties.Instance._lock) { Doc.SetNativeHeight(doc, NumCast(doc._height)); Doc.SetNativeWidth(doc, NumCast(doc._width)); } @@ -249,7 +249,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P const first = SelectionManager.Views()[0]; let thisPt = { x: e.clientX - this._offX, y: e.clientY - this._offY }; var fixedAspect = Doc.NativeAspect(first.layoutDoc); - InkStrokeProperties.Instance?._lock && SelectionManager.Views().filter(dv => dv.rootDoc.type === DocumentType.INK) + InkStrokeProperties.Instance._lock && SelectionManager.Views().filter(dv => dv.rootDoc.type === DocumentType.INK) .forEach(dv => fixedAspect = Doc.NativeAspect(dv.rootDoc)); const resizeHdl = this._resizeHdlId.split(" ")[0]; diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 364bf05e2..d5e0ed962 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -112,7 +112,7 @@ export class KeyManager { case "escape": DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; - InkStrokeProperties.Instance && (InkStrokeProperties.Instance._controlButton = false); + InkStrokeProperties.Instance._controlButton = false; CurrentUserUtils.SelectedTool = InkTool.None; var doDeselect = true; if (SnappingManager.GetIsDragging()) { diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx index a91e74c44..76ce73b0d 100644 --- a/src/client/views/InkControlPtHandles.tsx +++ b/src/client/views/InkControlPtHandles.tsx @@ -44,23 +44,23 @@ export class InkControlPtHandles extends React.Component<InkControlProps> { @action onControlDown = (e: React.PointerEvent, controlIndex: number): void => { const ptFromScreen = this.props.inkView.ComponentView?.ptFromScreen; - if (InkStrokeProperties.Instance && ptFromScreen) { + if (ptFromScreen) { const order = controlIndex % 4; const handleIndexA = ((order === 3 ? controlIndex - 1 : controlIndex - 2) + this.props.inkCtrlPoints.length) % this.props.inkCtrlPoints.length; const handleIndexB = (order === 3 ? controlIndex + 2 : controlIndex + 1) % this.props.inkCtrlPoints.length; const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number")); - const wasSelected = InkStrokeProperties.Instance?._currentPoint === controlIndex; + const wasSelected = InkStrokeProperties.Instance._currentPoint === controlIndex; setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("drag ink ctrl pt"); const inkMoveEnd = ptFromScreen({ X: delta[0], Y: delta[1] }); const inkMoveStart = ptFromScreen({ X: 0, Y: 0 }); - InkStrokeProperties.Instance?.moveControlPtHandle(this.props.inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex); + InkStrokeProperties.Instance.moveControlPtHandle(this.props.inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex); return false; }), action(() => { if (this.controlUndo) { - InkStrokeProperties.Instance?.snapControl(this.props.inkView, controlIndex); + InkStrokeProperties.Instance.snapControl(this.props.inkView, controlIndex); } this.controlUndo?.end(); this.controlUndo = undefined; @@ -75,11 +75,11 @@ export class InkControlPtHandles extends React.Component<InkControlProps> { } else { if (brokenIndices?.includes(equivIndex)) { if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth"); - InkStrokeProperties.Instance?.snapHandleTangent(this.props.inkView, equivIndex, handleIndexA, handleIndexB); + InkStrokeProperties.Instance.snapHandleTangent(this.props.inkView, equivIndex, handleIndexA, handleIndexB); } if (equivIndex !== controlIndex && brokenIndices?.includes(controlIndex)) { if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth"); - InkStrokeProperties.Instance?.snapHandleTangent(this.props.inkView, controlIndex, handleIndexA, handleIndexB); + InkStrokeProperties.Instance.snapHandleTangent(this.props.inkView, controlIndex, handleIndexA, handleIndexB); } } this.controlUndo?.end(); @@ -102,7 +102,7 @@ export class InkControlPtHandles extends React.Component<InkControlProps> { @action onDelete = (e: KeyboardEvent) => { if (["-", "Backspace", "Delete"].includes(e.key)) { - InkStrokeProperties.Instance?.deletePoints(this.props.inkView, e.shiftKey); + InkStrokeProperties.Instance.deletePoints(this.props.inkView, e.shiftKey); e.stopPropagation(); } } @@ -111,11 +111,7 @@ export class InkControlPtHandles extends React.Component<InkControlProps> { * Changes the current selected control point. */ @action - changeCurrPoint = (i: number) => { - if (InkStrokeProperties.Instance) { - InkStrokeProperties.Instance._currentPoint = i; - } - } + changeCurrPoint = (i: number) => InkStrokeProperties.Instance._currentPoint = i render() { // Accessing the current ink's data and extracting all control points. @@ -202,9 +198,9 @@ export class InkEndPtHandles extends React.Component<InkEndProps> { const scaling = v2len / v1len; const v1n = { x: v1.x / v1len, y: v1.y / v1len }; const v2n = { x: v2.x / v2len, y: v2.y / v2len }; - const angle = Math.acos(v1n.x * v2n.x + v1n.y * v2n.y) * Math.sign(v1.x * v2.y - v2.x * v1.y) - InkStrokeProperties.Instance?.stretchInk([this.props.inkView], scaling, { x: p2().X, y: p2().Y }, v1n); - InkStrokeProperties.Instance?.rotateInk([this.props.inkView], angle, { x: p2().X, y: p2().Y }); + const angle = Math.acos(v1n.x * v2n.x + v1n.y * v2n.y) * Math.sign(v1.x * v2.y - v2.x * v1.y); + InkStrokeProperties.Instance.stretchInk([this.props.inkView], scaling, { x: p2().X, y: p2().Y }, v1n); + InkStrokeProperties.Instance.rotateInk([this.props.inkView], angle, { x: p2().X, y: p2().Y }); return false; }, action(() => { this.controlUndo?.end(); @@ -214,7 +210,7 @@ export class InkEndPtHandles extends React.Component<InkEndProps> { } render() { - const hdl = (pt: PointData, dragFunc: (e: React.PointerEvent) => void) => <circle key={"npt"} + const hdl = (key: string, pt: PointData, dragFunc: (e: React.PointerEvent) => void) => <circle key={key} cx={pt.X} cy={pt.Y} r={this.props.screenSpaceLineWidth * 2} @@ -223,12 +219,12 @@ export class InkEndPtHandles extends React.Component<InkEndProps> { strokeWidth={0} onPointerLeave={action(() => this._overStart = false)} onPointerEnter={action(() => this._overStart = true)} - onPointerDown={e => dragFunc(e)} + onPointerDown={dragFunc} pointerEvents="all" />; return (<svg> - {hdl(this.props.startPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.startPt, () => this.props.endPt))} - {hdl(this.props.endPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.endPt, () => this.props.startPt))} + {hdl("start", this.props.startPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.startPt, () => this.props.endPt))} + {hdl("end", this.props.endPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.endPt, () => this.props.startPt))} </svg> ); } diff --git a/src/client/views/InkStroke.scss b/src/client/views/InkStroke.scss index 2127826b4..664f2448b 100644 --- a/src/client/views/InkStroke.scss +++ b/src/client/views/InkStroke.scss @@ -14,6 +14,9 @@ } .inkStroke-wrapper { + display: flex; + align-items: center; + height: 100%; .inkStroke { mix-blend-mode: multiply; stroke-linejoin: round; @@ -22,7 +25,7 @@ transform-origin: top left; width: 100%; height: 100%; - + pointer-events: none; svg:not(:root) { overflow: visible !important; } diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 4808bbc77..7ab631b03 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -15,14 +15,15 @@ import { InkingStroke } from "./InkingStroke"; import { DocumentView } from "./nodes/DocumentView"; export class InkStrokeProperties { - static Instance: InkStrokeProperties | undefined; + static _Instance: InkStrokeProperties | undefined; + public static get Instance() { return this._Instance || new InkStrokeProperties(); } @observable _lock = false; @observable _controlButton = false; @observable _currentPoint = -1; constructor() { - InkStrokeProperties.Instance = this; + InkStrokeProperties._Instance = this; reaction(() => this._controlButton, button => button && (CurrentUserUtils.SelectedTool = InkTool.None)); reaction(() => CurrentUserUtils.SelectedTool, tool => (tool !== InkTool.None) && (this._controlButton = false)); } @@ -150,15 +151,15 @@ export class InkStrokeProperties { } else { const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4; const splicedPoints = ink.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8)); - var samples: Point[] = []; + const samples: Point[] = []; var startDir = { x: 0, y: 0 }; var endDir = { x: 0, y: 0 }; for (var i = 0; i < splicedPoints.length / 4; i++) { - var bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y }))); + const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y }))); if (i === 0) startDir = bez.derivative(0); if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(1); for (var t = 0; t < (i === splicedPoints.length / 4 - 1 ? 1 + 1e-7 : 1); t += 0.05) { - var pt = bez.compute(t); + const pt = bez.compute(t); samples.push(new Point(pt.x, pt.y)); } } @@ -209,12 +210,12 @@ export class InkStrokeProperties { const ptFromScreen = view.ComponentView?.ptFromScreen; const ptToScreen = view.ComponentView?.ptToScreen; return !ptToScreen || !ptFromScreen ? ink : - ink.map(i => ptToScreen(i)).map(i => { + ink.map(ptToScreen).map(i => { const pvec = { X: i.X - scrpt.x, Y: i.Y - scrpt.y }; const svec = pvec.X * scrVec.x * scaling + pvec.Y * scrVec.y * scaling; const ovec = -pvec.X * scrVec.y + pvec.Y * (scrVec.x); const newscrpt = { X: scrpt.x + svec * scrVec.x - ovec * scrVec.y, Y: scrpt.y + svec * scrVec.y + ovec * scrVec.x }; - const newpt = ptFromScreen!(newscrpt); + const newpt = ptFromScreen(newscrpt); return newpt; }); }); diff --git a/src/client/views/InkTangentHandles.tsx b/src/client/views/InkTangentHandles.tsx index f88a20448..ab73e58a4 100644 --- a/src/client/views/InkTangentHandles.tsx +++ b/src/client/views/InkTangentHandles.tsx @@ -29,24 +29,23 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> { * @param handleNum The index of the currently selected handle point. */ onHandleDown = (e: React.PointerEvent, handleIndex: number): void => { - if (InkStrokeProperties.Instance) { - var controlUndo: UndoManager.Batch | undefined; - const screenScale = this.props.ScreenToLocalTransform().Scale; - const order = handleIndex % 4; - const oppositeHandleRawIndex = order === 1 ? handleIndex - 3 : handleIndex + 3; - const oppositeHandleIndex = (oppositeHandleRawIndex < 0 ? this.props.screenCtrlPoints.length + oppositeHandleRawIndex : oppositeHandleRawIndex) % this.props.screenCtrlPoints.length; - const controlIndex = (order === 1 ? handleIndex - 1 : handleIndex + 2) % this.props.screenCtrlPoints.length; - setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => { + var controlUndo: UndoManager.Batch | undefined; + const screenScale = this.props.ScreenToLocalTransform().Scale; + const order = handleIndex % 4; + const oppositeHandleRawIndex = order === 1 ? handleIndex - 3 : handleIndex + 3; + const oppositeHandleIndex = (oppositeHandleRawIndex < 0 ? this.props.screenCtrlPoints.length + oppositeHandleRawIndex : oppositeHandleRawIndex) % this.props.screenCtrlPoints.length; + const controlIndex = (order === 1 ? handleIndex - 1 : handleIndex + 2) % this.props.screenCtrlPoints.length; + setupMoveUpEvents(this, e, + (e: PointerEvent, down: number[], delta: number[]) => { if (!controlUndo) controlUndo = UndoManager.StartBatch("DocDecs move tangent"); if (e.altKey) this.onBreakTangent(controlIndex); - InkStrokeProperties.Instance?.moveTangentHandle(this.props.inkView, -delta[0] * screenScale, -delta[1] * screenScale, handleIndex, oppositeHandleIndex, controlIndex); + InkStrokeProperties.Instance.moveTangentHandle(this.props.inkView, -delta[0] * screenScale, -delta[1] * screenScale, handleIndex, oppositeHandleIndex, controlIndex); return false; }, () => { controlUndo?.end(); UndoManager.FilterBatches(["data", "x", "y", "width", "height"]); }, emptyFunction - ); - } + ); } /** @@ -66,9 +65,6 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> { } render() { - const formatInstance = InkStrokeProperties.Instance; - if (!formatInstance) return (null); - // Accessing the current ink's data and extracting all handle points and handle lines. const data = this.props.screenCtrlPoints; const tangentHandles: HandlePoint[] = []; @@ -107,7 +103,7 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> { onPointerDown={e => this.onHandleDown(e, pts.I)} pointerEvents="all" cursor="default" - display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} /> + display={(pts.dot1 === InkStrokeProperties.Instance._currentPoint || pts.dot2 === InkStrokeProperties.Instance._currentPoint) ? "inherit" : "none"} /> </svg>)} {tangentLines.map((pts, i) => { const tangentLine = (x1: number, y1: number, x2: number, y2: number) => @@ -119,7 +115,7 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> { stroke={Colors.MEDIUM_BLUE} strokeDasharray={"1 1"} strokeWidth={1} - display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} />; + display={(pts.dot1 === InkStrokeProperties.Instance._currentPoint || pts.dot2 === InkStrokeProperties.Instance._currentPoint) ? "inherit" : "none"} />; return <svg height="100" width="100" key={`line${i}`}> {tangentLine(pts.X1, pts.Y1, pts.X2, pts.Y2)} {tangentLine(pts.X2, pts.Y2, pts.X3, pts.Y3)} diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 8ff080f81..0b3619b22 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -1,3 +1,25 @@ +/* + 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"; @@ -30,35 +52,32 @@ 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); @@ -80,6 +99,12 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume 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; @@ -88,70 +113,47 @@ 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) => { - const ptFromScreen = this.ptFromScreen; this._handledClick = false; - if (InkStrokeProperties.Instance && ptFromScreen) { - 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 = this._properties?._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 = ptFromScreen({ X: delta[0], Y: delta[1] }); - const inkMoveStart = 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(() => { - if (controlUndo) { - InkStrokeProperties.Instance?.snapControl(inkView, controlIndex); - InkStrokeProperties.Instance?.snapControl(inkView, controlIndex + 3); - } - 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 && 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 (isEditing) { - 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()); } - }), isEditing, isEditing, action(() => wasSelected && InkStrokeProperties.Instance && (InkStrokeProperties.Instance._currentPoint = -1))); - } - } - - /** - * Ensures the ink controls and handles aren't rendered when the current ink stroke is reselected. - */ - @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)"); - } + } + }), isEditing, isEditing, action(() => wasSelected && (InkStrokeProperties.Instance._currentPoint = -1))); } ptFromScreen = (scrPt: { X: number, Y: number }) => { @@ -173,13 +175,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); @@ -229,7 +241,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume const startMarker = StrCast(this.layoutDoc.strokeStartMarker); const endMarker = StrCast(this.layoutDoc.strokeEndMarker); return SnappingManager.GetIsDragging() ? (null) : - !this._properties?._controlButton ? + !InkStrokeProperties.Instance._controlButton ? (!this.props.isSelected() || InkingStroke.IsClosed(inkData) ? (null) : <div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}> <InkEndPtHandles @@ -287,15 +299,11 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume false, downHdlr); // Set of points rendered upon the ink that can be added if a user clicks on one. - return <div className="inkStroke-wrapper" style={{ display: "flex", alignItems: "center", height: "100%" }}> + return <div className="inkStroke-wrapper"> <svg className="inkStroke" style={{ - width: "100%", - height: "100%", - 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)} @@ -305,7 +313,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume 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(this.onPointerDown)} diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 546b0e360..a9fea4a78 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -116,7 +116,6 @@ export class MainView extends React.Component { }, 0); setTimeout(() => ele.outerHTML = '', 1000); } - new InkStrokeProperties(); this._sidebarContent.proto = undefined; if (!MainView.Live) { DocServer.setPlaygroundFields(["dataTransition", "treeViewOpen", "autoHeight", "showSidebar", "sidebarWidthPercent", "viewTransition", diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 1083e0075..18d5f1642 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -535,16 +535,17 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @computed get controlPointsButton() { - const formatInstance = InkStrokeProperties.Instance; - return !formatInstance ? (null) : <div className="inking-button"> + return <div className="inking-button"> <Tooltip title={<div className="dash-tooltip">{"Edit points"}</div>}> - <div className="inking-button-points" onPointerDown={action(() => formatInstance._controlButton = !formatInstance._controlButton)} style={{ backgroundColor: formatInstance._controlButton ? "black" : "" }}> + <div className="inking-button-points" + style={{ backgroundColor: InkStrokeProperties.Instance._controlButton ? "black" : "" }} + onPointerDown={action(() => InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton)} > <FontAwesomeIcon icon="bezier-curve" color="white" size="lg" /> </div> </Tooltip> - <Tooltip title={<div className="dash-tooltip">{formatInstance._lock ? "Unlock ratio" : "Lock ratio"}</div>}> - <div className="inking-button-lock" onPointerDown={action(() => formatInstance._lock = !formatInstance._lock)} > - <FontAwesomeIcon icon={formatInstance._lock ? "lock" : "unlock"} color="white" size="lg" /> + <Tooltip title={<div className="dash-tooltip">{InkStrokeProperties.Instance._lock ? "Unlock ratio" : "Lock ratio"}</div>}> + <div className="inking-button-lock" onPointerDown={action(() => InkStrokeProperties.Instance._lock = !InkStrokeProperties.Instance._lock)} > + <FontAwesomeIcon icon={InkStrokeProperties.Instance._lock ? "lock" : "unlock"} color="white" size="lg" /> </div> </Tooltip> <Tooltip title={<div className="dash-tooltip">{"Rotate 90˚"}</div>}> @@ -603,7 +604,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { const oldX = NumCast(this.selectedDoc?.x); const oldY = NumCast(this.selectedDoc?.y); this.selectedDoc && (this.selectedDoc._width = oldWidth + (dirs === "up" ? 10 : - 10)); - InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) / oldWidth * NumCast(this.selectedDoc?._height))); + InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) / oldWidth * NumCast(this.selectedDoc?._height))); const doc = this.selectedDoc; if (doc?.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) { const ink = Cast(doc.data, InkField)?.inkData; @@ -625,7 +626,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { const oX = NumCast(this.selectedDoc?.x); const oY = NumCast(this.selectedDoc?.y); this.selectedDoc && (this.selectedDoc._height = oHeight + (dirs === "up" ? 10 : - 10)); - InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) / oHeight * NumCast(this.selectedDoc?._width))); + InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) / oHeight * NumCast(this.selectedDoc?._width))); const docu = this.selectedDoc; if (docu?.type === DocumentType.INK && docu.x && docu.y && docu._height && docu._width) { const ink = Cast(docu.data, InkField)?.inkData; @@ -663,12 +664,12 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { set shapeWid(value) { const oldWidth = NumCast(this.selectedDoc?._width); this.selectedDoc && (this.selectedDoc._width = Number(value)); - InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) * NumCast(this.selectedDoc?._height)) / oldWidth); + InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) * NumCast(this.selectedDoc?._height)) / oldWidth); } set shapeHgt(value) { const oldHeight = NumCast(this.selectedDoc?._height); this.selectedDoc && (this.selectedDoc._height = Number(value)); - InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) * NumCast(this.selectedDoc?._width)) / oldHeight); + InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) * NumCast(this.selectedDoc?._width)) / oldHeight); } @computed get hgtInput() { return this.inputBoxDuo("hgt", this.shapeHgt, (val: string) => { if (!isNaN(Number(val))) { this.shapeHgt = val; } return true; }, "H:", "wid", this.shapeWid, (val: string) => { if (!isNaN(Number(val))) { this.shapeWid = val; } return true; }, "W:"); } diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 8d8630907..f09d532ad 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -93,7 +93,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps case StyleProp.Opacity: return Cast(doc?._opacity, "number", Cast(doc?.opacity, "number", null)); case StyleProp.HideLinkButton: return props?.hideLinkButton || (!selected && (doc?.isLinkButton || doc?.hideLinkButton)); case StyleProp.FontSize: return StrCast(doc?.[fieldKey + "fontSize"], StrCast(doc?.fontSize, StrCast(Doc.UserDoc().fontSize))); - case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + "fontFamily"], StrCast(doc?.["fontFamily"], StrCast(Doc.UserDoc().fontFamily))); + case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + "fontFamily"], StrCast(doc?.fontFamily, StrCast(Doc.UserDoc().fontFamily))); case StyleProp.ShowTitle: return (doc && !doc.presentationTargetDoc && StrCast(doc._showTitle, props?.showTitle?.() || diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 9d0402075..72e84c141 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1146,7 +1146,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if (startupText) { dispatch(state.tr.insertText(startupText)); } else if (!FormattedTextBox.LiveTextUndo) { - selectAll(this._editorView!.state, (tr) => { + selectAll(this._editorView.state, (tr) => { this._editorView!.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: "center" }))); }); } diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index fe297782c..09cfb2077 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -251,7 +251,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc this._results.forEach((_, doc) => { this._pageRanks.set(doc, 1.0 / this._results.size); - if (Doc.GetProto(doc)[DirectLinksSym].size == 0) { + if (Doc.GetProto(doc)[DirectLinksSym].size === 0) { this._linkedDocsOut.set(doc, new Set(this._results.keys())); this._results.forEach((_, linkedDoc) => { @@ -259,20 +259,20 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc }); } else { - let linkedDocSet: Set<Doc> = new Set(); + const linkedDocSet = new Set<Doc>(); Doc.GetProto(doc)[DirectLinksSym].forEach((link) => { - let d1 = link?.anchor1 as Doc; - let d2 = link?.anchor2 as Doc; - if (doc == d1 && this._results.has(d2)) { + const d1 = link?.anchor1 as Doc; + const d2 = link?.anchor2 as Doc; + if (doc === d1 && this._results.has(d2)) { linkedDocSet.add(d2); this._linkedDocsIn.get(d2)?.add(doc); } - else if (doc == d2 && this._results.has(d1)) { + else if (doc === d2 && this._results.has(d1)) { linkedDocSet.add(d1); this._linkedDocsIn.get(d1)?.add(doc); } - }) + }); this._linkedDocsOut.set(doc, linkedDocSet); } @@ -291,7 +291,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc let converged = true; const pageRankFromAll = (1 - DAMPENING_FACTOR) / this._results.size; - let nextPageRanks: Map<Doc, number> = new Map<Doc, number>(); + const nextPageRanks = new Map<Doc, number>(); this._results.forEach((_, doc) => { let nextPageRank = pageRankFromAll; @@ -397,7 +397,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc const isLinkSearch: boolean = this.props.linkSearch; - const sortedResults = Array.from(this._results.entries()).sort((a, b) => (this._pageRanks.get(b[0]) ?? 0) - (this._pageRanks.get(a[0]) ?? 0)) // sorted by page rank + const sortedResults = Array.from(this._results.entries()).sort((a, b) => (this._pageRanks.get(b[0]) ?? 0) - (this._pageRanks.get(a[0]) ?? 0)); // sorted by page rank const resultsJSX = Array(); |
