aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2024-10-01 19:03:05 -0400
committerbobzel <zzzman@gmail.com>2024-10-01 19:03:05 -0400
commit3d6c5b151cebc74df4b8fc79e5bb755c51ad65a2 (patch)
tree9440404e1399f2ca668527366c99654f42980576 /src
parent5d859cab5fa714860723fa252498c407d5909cdc (diff)
changed how smoothing curves works - got rid of simplify.js dependency
Diffstat (limited to 'src')
-rw-r--r--src/client/documents/Documents.ts9
-rw-r--r--src/client/views/InkStrokeProperties.ts49
-rw-r--r--src/client/views/PropertiesView.tsx117
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}