diff options
author | bobzel <zzzman@gmail.com> | 2024-10-01 19:03:05 -0400 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2024-10-01 19:03:05 -0400 |
commit | 3d6c5b151cebc74df4b8fc79e5bb755c51ad65a2 (patch) | |
tree | 9440404e1399f2ca668527366c99654f42980576 /src | |
parent | 5d859cab5fa714860723fa252498c407d5909cdc (diff) |
changed how smoothing curves works - got rid of simplify.js dependency
Diffstat (limited to 'src')
-rw-r--r-- | src/client/documents/Documents.ts | 9 | ||||
-rw-r--r-- | src/client/views/InkStrokeProperties.ts | 49 | ||||
-rw-r--r-- | src/client/views/PropertiesView.tsx | 117 |
3 files changed, 76 insertions, 99 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index bf72a4bd4..b0a1e767e 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1,5 +1,3 @@ -/* eslint-disable prefer-destructuring */ -/* eslint-disable default-param-last */ /* eslint-disable no-use-before-define */ import { reaction } from 'mobx'; import { basename } from 'path'; @@ -671,7 +669,6 @@ export namespace Docs { * only when creating a DockDocument from the current user's already existing * main document. */ - // eslint-disable-next-line default-param-last function InstanceFromProto(proto: Doc, data: FieldType | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = 'data', protoId?: string, placeholderDocIn?: Doc, noView?: boolean) { const placeholderDoc = placeholderDocIn; const viewKeys = ['x', 'y', 'isSystem']; // keys that should be addded to the view document even though they don't begin with an "_" @@ -732,7 +729,6 @@ export namespace Docs { return dataDoc; } - // eslint-disable-next-line default-param-last export function ImageDocument(url: string | ImageField, options: DocumentOptions = {}, overwriteDoc?: Doc) { const imgField = url instanceof ImageField ? url : url ? new ImageField(url) : undefined; return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: basename(imgField?.url.href ?? '-no image-'), ...options }, undefined, undefined, undefined, overwriteDoc); @@ -751,7 +747,6 @@ export namespace Docs { * @param fieldKey the field that the compiled script is written into. * @returns the Scripting Doc */ - // eslint-disable-next-line default-param-last export function ScriptingDocument(script: Opt<ScriptField> | null, options: DocumentOptions = {}, fieldKey?: string) { return InstanceFromProto(Prototypes.get(DocumentType.SCRIPTING), script || undefined, { ...options, layout: fieldKey ? `<ScriptingBox {...props} fieldKey={'${fieldKey}'}/>` /* ScriptingBox.LayoutString(fieldKey) */ : undefined }); } @@ -759,7 +754,6 @@ export namespace Docs { export function ChatDocument(options?: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.CHAT), undefined, { ...(options || {}) }); } - // eslint-disable-next-line default-param-last export function VideoDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) { return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(url), options, undefined, undefined, undefined, overwriteDoc); } @@ -779,7 +773,6 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.DIAGRAM), undefined, options); } - // eslint-disable-next-line default-param-last export function AudioDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) { return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(url), options, undefined, undefined, undefined, overwriteDoc); } @@ -834,7 +827,6 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.RTF), field, options, undefined, fieldKey); } - // eslint-disable-next-line default-param-last export function LinkDocument(source: Doc, target: Doc, options: DocumentOptions = {}, id?: string) { const linkDoc = InstanceFromProto( Prototypes.get(DocumentType.LINK), @@ -878,7 +870,6 @@ export namespace Docs { return ink; } - // eslint-disable-next-line default-param-last export function PdfDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) { const width = options._width || undefined; const height = options._height || undefined; diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 5cacde0d4..358274f0e 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -1,19 +1,18 @@ import { Bezier } from 'bezier-js'; +import * as fitCurve from 'fit-curve'; import * as _ from 'lodash'; import { action, makeObservable, observable, reaction, runInAction } from 'mobx'; -import simplify from 'simplify-js'; import { Doc, NumListCast, Opt } from '../../fields/Doc'; +import { DocData } from '../../fields/DocSymbols'; import { InkData, InkField, InkTool } from '../../fields/InkField'; import { List } from '../../fields/List'; import { listSpec } from '../../fields/Schema'; import { Cast, NumCast } from '../../fields/Types'; -import { Gestures, PointData } from '../../pen-gestures/GestureTypes'; -import { GestureUtils } from '../../pen-gestures/GestureUtils'; +import { PointData } from '../../pen-gestures/GestureTypes'; import { Point } from '../../pen-gestures/ndollar'; import { DocumentType } from '../documents/DocumentTypes'; import { undoable } from '../util/UndoManager'; import { FitOneCurve } from '../util/bezierFit'; -import { GestureOverlay } from './GestureOverlay'; import { InkingStroke } from './InkingStroke'; import { CollectionFreeFormView } from './collections/collectionFreeForm'; import { DocumentView } from './nodes/DocumentView'; @@ -322,7 +321,6 @@ export class InkStrokeProperties { let nearestSeg = -1; let nearestPt = { X: 0, Y: 0 }; for (let i = 0; i < ctrlPoints.length - 3; i += 4) { - // eslint-disable-next-line no-continue if (excludeSegs?.includes(i)) continue; const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]]; const point = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).project({ x: refInkSpacePt.X, y: refInkSpacePt.Y }); @@ -488,31 +486,34 @@ export class InkStrokeProperties { }); }, 'move ink tangent'); + sampleBezier = (curves: InkData) => { + const polylinePoints = [{ x: curves[0].X, y: curves[0].Y }]; + for (let i = 0; i < curves.length / 4; i++) { + const bez = new Bezier(curves.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y }))); + for (let t = 0.05; t < 1; t += 0.05) { + polylinePoints.push(bez.compute(t)); + } + polylinePoints.push(bez.points[3]); + } + return polylinePoints.length > 2 ? polylinePoints : undefined; + }; /** - * Function that "smooths" ink strokes by using the gesture recognizer to detect shapes and - * removing excess control points with the simplify-js package. + * Function that "smooths" ink strokes by sampling the curve, then fitting it with new bezier curves, subject to a + * maximum pixel error tolerance * @param inkDocs - * @param tolerance Determines how strong the smooth effect will be + * @param tolerance how many pixels of error are allowed */ - smoothInkStrokes = undoable((inkDocs: Doc[], tolerance: number = 5) => { + smoothInkStrokes = undoable((inkDocs: Doc[], tolerance = 5) => { inkDocs.forEach(inkDoc => { const inkView = DocumentView.getDocumentView(inkDoc); const inkStroke = inkView?.ComponentView as InkingStroke; - const { inkData } = inkStroke.inkScaledData(); - const polylinePoints = inkData.filter((pt, index) => { return index % 4 === 0 || pt === inkData.lastElement()}).map(pt => { return { x: pt.X, y: pt.Y }; }); // prettier-ignore - if (polylinePoints.length > 2) { - const toKeep = simplify(polylinePoints, tolerance).map(pt => {return { X: pt.x, Y: pt.y }}); // prettier-ignore - for (var i = 4; i < inkData.length - 3; i += 4) { - const contains = toKeep.find(pt => pt.X === inkData[i].X && pt.Y === inkData[i].Y); - if (!contains) { - this._currentPoint = i; - inkView && this.deletePoints(inkView, false); - } - } - // close the curve if the first and last points are really close (based on tolerance) - if (!InkingStroke.IsClosed(inkData) && Math.sqrt((inkData.lastElement().X - inkData[0].X) ** 2 + (inkData.lastElement().Y - inkData[0].Y) ** 2) <= tolerance * 2) { - inkData[inkData.length - 1] = inkData[0]; - } + const polylinePoints = this.sampleBezier(inkStroke?.inkScaledData().inkData ?? [])?.map(pt => [pt.x, pt.y]); + if (polylinePoints) { + inkDoc[DocData].stroke = new InkField( + fitCurve.default(polylinePoints, tolerance) + .reduce((cpts, bez) => + ({n: cpts.push(...bez.map(cpt => ({X:cpt[0], Y:cpt[1]}))), cpts}).cpts, + [] as {X:number, Y:number}[])); // prettier-ignore } }); }, 'smooth ink stroke'); diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 229ceffe2..d0c47875f 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -120,9 +120,6 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps @observable openAddSlide: boolean = false; @observable openSlideOptions: boolean = false; - // For ink groups - @observable inkDoc: Doc | undefined = undefined; - @observable _controlButton: boolean = false; private _disposers: { [name: string]: IReactionDisposer } = {}; @@ -826,7 +823,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps set shapeWid(value) { this.selectedDoc && (this.selectedDoc._width = Math.round(value * 100) / 100); } // prettier-ignore @computed get shapeHgt() { return NumCast(this.selectedDoc?._height); } // prettier-ignore set shapeHgt(value) { this.selectedDoc && (this.selectedDoc._height = Math.round(value * 100) / 100); } // prettier-ignore - @computed get strokeThk(){ return this.containsInkDoc ? NumCast(this.inkDoc?.[DocData].stroke_width) : NumCast(this.selectedDoc?.[DocData].stroke_width); } // prettier-ignore + @computed get strokeThk(){ return NumCast(this.selectedStrokes.lastElement()?.[DocData].stroke_width); } // prettier-ignore set strokeThk(value) { this.selectedStrokes.forEach(doc => { doc[DocData].stroke_width = Math.round(value * 100) / 100; @@ -868,7 +865,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps private _lastDash: string = '2'; - @computed get colorFil() { return this.containsInkDoc ? StrCast(this.inkDoc?.[DocData].fillColor) : StrCast(this.selectedDoc?.[DocData].fillColor); } // prettier-ignore + @computed get colorFil() { return StrCast(this.selectedStrokes.lastElement()?.[DocData].fillColor); } // prettier-ignore set colorFil(value) { this.selectedStrokes.forEach(doc => { const inkStroke = DocumentView.getDocumentView(doc)?.ComponentView as InkingStroke; @@ -878,7 +875,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps } }); } - @computed get colorStk() { return this.containsInkDoc ? StrCast(this.inkDoc?.[DocData].color) : StrCast(this.selectedDoc?.[DocData].color); } // prettier-ignore + @computed get colorStk() { return StrCast(this.selectedStrokes.lastElement()?.[DocData].color); } // prettier-ignore set colorStk(value) { this.selectedStrokes.forEach(doc => { doc[DocData].color = value || undefined; @@ -956,9 +953,21 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps @computed get smoothAndColor() { const targetDoc = this.selectedLayoutDoc; + const smoothNumber = this.getNumber( + 'Smooth Amount', + '', + 1, + Math.max(10, this.smoothAmt), + this.smoothAmt, + (val: number) => { + !isNaN(val) && (this.smoothAmt = val); + }, + 10, + 1 + ); return ( <div> - {!targetDoc.layout_isSvg && ( + {!targetDoc.layout_isSvg && this.containsInkDoc && ( <div className="color"> <Toggle text={'Color with GPT'} @@ -984,29 +993,16 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps fillWidth toggleType={ToggleType.BUTTON} onClick={undoable(() => { - InkStrokeProperties.Instance.smoothInkStrokes(this.containsInkDoc ? DocListCast(targetDoc.data) : [targetDoc], this.smoothAmt); + InkStrokeProperties.Instance.smoothInkStrokes(this.selectedStrokes, this.smoothAmt); }, 'smoothStrokes')} /> </div> - <div className="smooth-slider"> - {this.getNumber( - 'Smooth Amount', - '', - 1, - Math.max(10, this.smoothAmt), - this.smoothAmt, - (val: number) => { - !isNaN(val) && (this.smoothAmt = val); - }, - 10, - 1 - )} - </div> + <div className="smooth-slider">{smoothNumber}</div> </div> ); } - @computed get dashdStk() { return this.containsInkDoc? this.inkDoc?.stroke_dash || '' : this.selectedDoc?.stroke_dash || ''; } // prettier-ignore + @computed get dashdStk() { return this.selectedStrokes[0]?.stroke_dash || ''; } // prettier-ignore set dashdStk(value) { value && (this._lastDash = value as string); this.selectedStrokes.forEach(doc => { @@ -1148,54 +1144,51 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps this.dashdStk = this.dashdStk === '2' ? '0' : '2'; }; - @computed get appearanceEditor() { - return ( - <div className="appearance-editor"> - {this.widthAndDash} - {this.strokeAndFill} - {this.smoothAndColor} - </div> - ); - } - @computed get inkEditor() { return ( <div className="ink-editor"> {this.widthAndDash} {this.strokeAndFill} + {this.smoothAndColor} </div> ); } _sliderBatch: UndoManager.Batch | undefined; + _sliderKey = ''; setFinalNumber = () => { + this._sliderKey = ''; this._sliderBatch?.end(); }; - getNumber = (label: string, unit: string, min: number, max: number, number: number, setNumber: (val: number) => void, autorange?: number, autorangeMinVal?: number) => ( - <div key={label + (this.selectedDoc?.title ?? '')}> - <NumberInput formLabel={label} formLabelPlacement="left" type={Type.SEC} unit={unit} fillWidth color={this.color} number={number} setNumber={setNumber} min={min} max={max} /> - <Slider - key={label} - onPointerDown={() => { - this._sliderBatch = UndoManager.StartBatch('slider ' + label); - }} - multithumb={false} - color={this.color} - size={Size.XSMALL} - min={min} - max={max} - autorangeMinVal={autorangeMinVal} - autorange={autorange} - number={number} - unit={unit} - decimals={1} - setFinalNumber={this.setFinalNumber} - setNumber={setNumber} - fillWidth - /> - </div> - ); + getNumber = (label: string, unit: string, min: number, max: number, number: number, setNumber: (val: number) => void, autorange?: number, autorangeMinVal?: number) => { + const key = this._sliderKey || label + min + max + number; + return ( + <div key={label + (this.selectedDoc?.title ?? '')}> + <NumberInput formLabel={label} formLabelPlacement="left" type={Type.SEC} unit={unit} fillWidth color={this.color} number={number} setNumber={setNumber} min={min} max={max} /> + <Slider + key={key} + onPointerDown={() => { + this._sliderKey = key; + this._sliderBatch = UndoManager.StartBatch('slider ' + label); + }} + multithumb={false} + color={this.color} + size={Size.XSMALL} + min={min} + max={max} + autorangeMinVal={autorangeMinVal} + autorange={autorange} + number={number} + unit={unit} + decimals={1} + setFinalNumber={this.setFinalNumber} + setNumber={setNumber} + fillWidth + /> + </div> + ); + }; setVal = (func: (doc: Doc, val: number) => void) => (val: number) => this.selectedDoc && !isNaN(val) && func(this.selectedDoc, val); @computed get transformEditor() { @@ -1294,7 +1287,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps return ( <> <PropertiesSection title="Appearance" isOpen={this.openAppearance} setIsOpen={bool => { this.openAppearance = bool; }} onDoubleClick={this.CloseAll}> - {this.selectedLayoutDoc?.layout_isSvg ? this.appearanceEditor : null} + {this.selectedStrokes.length ? this.inkEditor : null} </PropertiesSection> <PropertiesSection title="Transform" isOpen={this.openTransform} setIsOpen={bool => { this.openTransform = bool; }} onDoubleClick={this.CloseAll}> {this.transformEditor} @@ -1311,19 +1304,12 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps const childDocs: Doc[] = DocListCast(selectedDoc[DocData].data); for (let i = 0; i < childDocs.length; i++) { if (DocumentView.getDocumentView(childDocs[i])?.layoutDoc?.layout_isSvg) { - this.inkDoc = childDocs[i]; return true; } } return false; }; - @computed get inkCollectionSubMenu() { - return <PropertiesSection title="Ink Appearance" isOpen={this.openAppearance} setIsOpen={bool => { this.openAppearance = bool; }} onDoubleClick={this.CloseAll}> - {this.isGroup && this.containsInk(this.selectedDoc) ? this.appearanceEditor : null} - </PropertiesSection>; // prettier-ignore - } - @computed get fieldsSubMenu() { return ( <PropertiesSection @@ -1875,7 +1861,6 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps {this.linksSubMenu} {!this.selectedLink || !this.openLinks ? null : this.linkProperties} {this.inkSubMenu} - {this.inkCollectionSubMenu} {this.contextsSubMenu} {isNovice ? null : this.sharingSubMenu} {this.filtersSubMenu} |