aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/Utils.ts4
-rw-r--r--src/client/util/DragManager.ts2
-rw-r--r--src/client/views/DocumentDecorations.scss56
-rw-r--r--src/client/views/DocumentDecorations.tsx135
-rw-r--r--src/client/views/StyleProvider.tsx2
-rw-r--r--src/client/views/nodes/DocumentView.tsx77
6 files changed, 175 insertions, 101 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index 6fc00040f..bf1f72774 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -243,6 +243,10 @@ export namespace Utils {
return [Math.sqrt((ys2 - ye) * (ys2 - ye) + (x2 - x) * (x2 - x)), [x, ye, x, ye]];
}
+ export function rotPt(x: number, y: number, radAng: number) {
+ return { x: x * Math.cos(radAng) - y * Math.sin(radAng), y: x * Math.sin(radAng) + y * Math.cos(radAng) };
+ }
+
function project(px: number, py: number, ax: number, ay: number, bx: number, by: number) {
if (ax === bx && ay === by) return { point: { x: ax, y: ay }, left: false, dot: 0, t: 0 };
const atob = { x: bx - ax, y: by - ay };
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 664933de0..160aba294 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -360,7 +360,7 @@ export namespace DragManager {
let useDim = false;
if (ele?.parentElement?.parentElement?.parentElement?.className === 'collectionFreeFormDocumentView-container') {
ele = ele.parentElement.parentElement.parentElement;
- const rotStr = ele.style.transform.replace(/.*rotate\(([-0-9.]*)deg\).*/, '$1');
+ const rotStr = ele.style.transform.replace(/.*rotate\(([-0-9.e]*)deg\).*/, '$1');
if (rotStr) rot = Number(rotStr);
} else {
useDim = true;
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 2e8d31478..e915fc88d 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -8,12 +8,38 @@ $resizeHandler: 8px;
.documentDecorations {
position: absolute;
z-index: 2000;
+
+ // Rotation handler
+ .documentDecorations-rotation {
+ border-radius: 100%;
+ height: 30;
+ width: 30;
+ right: -17;
+ top: calc(50% - 15px);
+ position: absolute;
+ pointer-events: all;
+ cursor: pointer;
+ background: white;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ font-size: 30px;
+ }
+ .documentDecorations-rotationCenter {
+ position: absolute;
+ width: 6px;
+ height: 6px;
+ pointer-events: all;
+ background: green;
+ border-radius: 50%;
+ }
}
.documentDecorations-Dark {
background: dimgray;
}
+
.documentDecorations-container {
- z-index: $docDecorations-zindex;
position: absolute;
top: 0;
left: 0;
@@ -29,7 +55,6 @@ $resizeHandler: 8px;
flex-direction: row;
gap: 2px;
-
.documentDecorations-openButton {
display: flex;
align-items: center;
@@ -51,7 +76,7 @@ $resizeHandler: 8px;
color: #02600d;
}
}
-
+
.documentDecorations-closeButton {
display: flex;
align-items: center;
@@ -71,7 +96,7 @@ $resizeHandler: 8px;
&:hover {
color: #a94442;
}
-
+
> svg {
margin: 0;
}
@@ -98,7 +123,7 @@ $resizeHandler: 8px;
&:hover {
color: #a94442;
}
-
+
> svg {
margin: 0;
}
@@ -207,25 +232,6 @@ $resizeHandler: 8px;
grid-column: 3;
}
- // Rotation handler
- .documentDecorations-rotation {
- border-radius: 100%;
- height: 30;
- width: 30;
- right: -10;
- top: 50%;
- z-index: 1000000;
- position: absolute;
- pointer-events: all;
- cursor: pointer;
- background: white;
- display: flex;
- justify-content: center;
- align-items: center;
- text-align: center;
- font-size: 30px;
- }
-
// Border radius handler
.documentDecorations-borderRadius {
position: absolute;
@@ -353,8 +359,6 @@ $resizeHandler: 8px;
}
}
-
-
.documentDecorations-background {
background: lightblue;
position: absolute;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 8b8c32642..8f66eaca7 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -12,7 +12,7 @@ import { InkField } from '../../fields/InkField';
import { ScriptField } from '../../fields/ScriptField';
import { Cast, NumCast, StrCast } from '../../fields/Types';
import { GetEffectiveAcl } from '../../fields/util';
-import { emptyFunction, numberValue, returnFalse, setupMoveUpEvents } from '../../Utils';
+import { emptyFunction, numberValue, returnFalse, setupMoveUpEvents, Utils } from '../../Utils';
import { Docs } from '../documents/Documents';
import { DocumentType } from '../documents/DocumentTypes';
import { DragManager } from '../util/DragManager';
@@ -30,7 +30,6 @@ import { LightboxView } from './LightboxView';
import { DocumentView } from './nodes/DocumentView';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
import { ImageBox } from './nodes/ImageBox';
-import { VideoBox } from './nodes/VideoBox';
import React = require('react');
@observer
@@ -311,49 +310,83 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
e => {} // clickEvent
);
};
+ @action
+ onRotateCenterDown = (e: React.PointerEvent): void => {
+ this._isRotating = true;
+ const seldocview = SelectionManager.Views()[0];
+ setupMoveUpEvents(
+ this,
+ e,
+ action((e: PointerEvent, down: number[], delta: number[]) => {
+ const newloccentern = seldocview.props.ScreenToLocalTransform().transformPoint(this.rotCenter[0] + delta[0], this.rotCenter[1] + delta[1]);
+ const newlocenter = [newloccentern[0] - NumCast(seldocview.layoutDoc._width) / 2, newloccentern[1] - NumCast(seldocview.layoutDoc._height) / 2];
+ const final = Utils.rotPt(newlocenter[0], newlocenter[1], -(NumCast(seldocview.rootDoc._jitterRotation) / 180) * Math.PI);
+ seldocview.rootDoc.rotateCenterX = final.x / NumCast(seldocview.layoutDoc._width);
+ seldocview.rootDoc.rotateCenterY = final.y / NumCast(seldocview.layoutDoc._height);
+
+ return false;
+ }), // moveEvent
+ action(() => {}), // upEvent
+ action((e, doubleTap) => {
+ if (doubleTap) {
+ seldocview.rootDoc.rotateCenterX = 0.5;
+ seldocview.rootDoc.rotateCenterY = 0.5;
+ }
+ })
+ );
+ };
@action
onRotateDown = (e: React.PointerEvent): void => {
this._isRotating = true;
+ const rcScreen = { X: this.rotCenter[0], Y: this.rotCenter[1] };
const rotateUndo = UndoManager.StartBatch('rotatedown');
const selectedInk = SelectionManager.Views().filter(i => i.ComponentView instanceof InkingStroke);
const centerPoint = { X: (this.Bounds.x + this.Bounds.r) / 2, Y: (this.Bounds.y + this.Bounds.b) / 2 };
+ const infos = new Map<Doc, { unrotatedDocPos: { x: number; y: number }; startRotCtr: { x: number; y: number }; accumRot: number }>();
+ SelectionManager.Views().forEach(dv => {
+ const accumRot = (NumCast(dv.rootDoc._jitterRotation) / 180) * Math.PI;
+ const localRotCtr = dv.props.ScreenToLocalTransform().transformPoint(rcScreen.X, rcScreen.Y);
+ const localRotCtrOffset = [localRotCtr[0] - NumCast(dv.rootDoc.width) / 2, localRotCtr[1] - NumCast(dv.rootDoc.height) / 2];
+ const startRotCtr = Utils.rotPt(localRotCtrOffset[0], localRotCtrOffset[1], -accumRot);
+ const unrotatedDocPos = { x: NumCast(dv.rootDoc.x) + localRotCtrOffset[0] - startRotCtr.x, y: NumCast(dv.rootDoc.y) + localRotCtrOffset[1] - startRotCtr.y };
+ infos.set(dv.rootDoc, { unrotatedDocPos, startRotCtr, accumRot });
+ });
+ const infoRot = (angle: number, isAbs = false) => {
+ SelectionManager.Views().forEach(
+ action(dv => {
+ const { unrotatedDocPos, startRotCtr, accumRot } = infos.get(dv.rootDoc)!;
+ const endRotCtr = Utils.rotPt(startRotCtr.x, startRotCtr.y, isAbs ? angle : accumRot + angle);
+ infos.set(dv.rootDoc, { unrotatedDocPos, startRotCtr, accumRot: isAbs ? angle : accumRot + angle });
+ dv.rootDoc.x = infos.get(dv.rootDoc)!.unrotatedDocPos.x - (endRotCtr.x - startRotCtr.x);
+ dv.rootDoc.y = infos.get(dv.rootDoc)!.unrotatedDocPos.y - (endRotCtr.y - startRotCtr.y);
+ dv.rootDoc._jitterRotation = ((isAbs ? 0 : NumCast(dv.rootDoc._jitterRotation)) + (angle * 180) / Math.PI) % 360; // Rotation between -360 and 360
+ })
+ );
+ };
setupMoveUpEvents(
this,
e,
(e: PointerEvent, down: number[], delta: number[]) => {
const previousPoint = { X: e.clientX, Y: e.clientY };
const movedPoint = { X: e.clientX - delta[0], Y: e.clientY - delta[1] };
- const angle = InkStrokeProperties.angleChange(previousPoint, movedPoint, centerPoint);
+ const deltaAng = InkStrokeProperties.angleChange(movedPoint, previousPoint, rcScreen);
if (selectedInk.length) {
- angle && InkStrokeProperties.Instance.rotateInk(selectedInk, -angle, centerPoint);
+ deltaAng && InkStrokeProperties.Instance.rotateInk(selectedInk, -deltaAng, centerPoint);
} else {
- SelectionManager.Views().forEach(dv => {
- const oldRotation = NumCast(dv.rootDoc._jitterRotation);
- // Rotation between -360 and 360
- let newRotation = (oldRotation - (angle * 180) / Math.PI) % 360;
-
- const diff = Math.round(newRotation / 45) - newRotation / 45;
- if (diff < 0.05) {
- console.log('show lines');
- }
- dv.rootDoc._jitterRotation = newRotation;
- });
+ infoRot(deltaAng);
}
return false;
}, // moveEvent
action(() => {
- SelectionManager.Views().forEach(dv => {
- const oldRotation = NumCast(dv.rootDoc._jitterRotation);
- const diff = Math.round(oldRotation / 45) - oldRotation / 45;
- if (diff < 0.05) {
- let newRotation = Math.round(oldRotation / 45) * 45;
- dv.rootDoc._jitterRotation = newRotation;
- }
- });
+ const dv = SelectionManager.Views()[0];
+ const oldRotation = NumCast(dv.rootDoc._jitterRotation);
+ const diff = oldRotation - Math.round(oldRotation / 45) * 45;
+ if (Math.abs(diff) < 5) {
+ infoRot(((Math.round(oldRotation / 45) * 45) / 180) * Math.PI, true);
+ }
this._isRotating = false;
rotateUndo?.end();
- UndoManager.FilterBatches(['data', 'x', 'y', 'width', 'height']);
}), // upEvent
emptyFunction
);
@@ -603,8 +636,26 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
return SelectionManager.Views().some(docView => docView.rootDoc.layoutKey === 'layout_icon');
}
+ @observable _rotCenter = [0, 0];
+ @computed get rotCenter() {
+ if (SelectionManager.Views().length) {
+ const seldocview = SelectionManager.Views()[0];
+ const loccenter = Utils.rotPt(
+ NumCast(seldocview.rootDoc.rotateCenterX) * NumCast(seldocview.layoutDoc._width),
+ NumCast(seldocview.rootDoc.rotateCenterY) * NumCast(seldocview.layoutDoc._height),
+ (NumCast(seldocview.rootDoc._jitterRotation) / 180) * Math.PI
+ );
+ return seldocview.props
+ .ScreenToLocalTransform()
+ .inverse()
+ .transformPoint(loccenter.x + NumCast(seldocview.layoutDoc._width) / 2, loccenter.y + NumCast(seldocview.layoutDoc._height) / 2);
+ }
+ return this._rotCenter;
+ }
+
render() {
- const bounds = this.Bounds;
+ const { b, c, r, x, y } = this.Bounds;
+ const bounds = { b, c, r, x, y };
const seldocview = SelectionManager.Views().slice(-1)[0];
if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldocview || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {
return null;
@@ -695,8 +746,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
<div
className="documentDecorations-background"
style={{
- transform: `rotate(${rotation}deg)`,
- transformOrigin: 'top left',
width: bounds.r - bounds.x + this._resizeBorderWidth + 'px',
height: bounds.b - bounds.y + this._resizeBorderWidth + 'px',
left: bounds.x - this._resizeBorderWidth / 2,
@@ -742,13 +791,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
{seldocview.props.renderDepth <= 1 || !seldocview.props.ContainingCollectionView ? null : topBtn('selector', 'arrow-alt-circle-up', undefined, this.onSelectorClick, 'tap to select containing document')}
</>
)}
-
- {useRotation && (
- <div key="rot" className={`documentDecorations-rotation`} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
- <IconButton icon={<FaUndo />} isCircle={true} hoverStyle={'lighten'} backgroundColor={Colors.DARK_GRAY} color={Colors.LIGHT_GRAY} />
- </div>
- )}
-
{useRounding && (
<div
key="rad"
@@ -774,6 +816,31 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
</div>
)}
</div>
+
+ {useRotation && (
+ <>
+ <div
+ style={{
+ position: 'absolute',
+ transform: `rotate(${rotation}deg)`,
+ width: this.Bounds.r - this.Bounds.x + 'px',
+ height: this.Bounds.b - this.Bounds.y + 'px',
+ left: this.Bounds.x,
+ top: this.Bounds.y,
+ pointerEvents: 'none',
+ }}>
+ <div className="documentDecorations-rotation" style={{ pointerEvents: 'all' }} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
+ <IconButton icon={<FaUndo />} isCircle={true} hoverStyle={'lighten'} backgroundColor={Colors.DARK_GRAY} color={Colors.LIGHT_GRAY} />
+ </div>
+ </div>
+ <div
+ className="documentDecorations-rotationCenter"
+ style={{ transform: `translate(${this.rotCenter[0] - 3}px, ${this.rotCenter[1] - 3}px)` }}
+ onPointerDown={this.onRotateCenterDown}
+ onContextMenu={e => e.preventDefault()}
+ />
+ </>
+ )}
</div>
)}
</div>
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 8b256923a..aadcd7169 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -131,7 +131,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
StrCast(
doc._showTitle,
props?.showTitle?.() ||
- (!Doc.IsSystem(doc) && [DocumentType.COL, DocumentType.LABEL, DocumentType.RTF, DocumentType.IMG, DocumentType.VID].includes(doc.type as any)
+ (!Doc.IsSystem(doc) && [DocumentType.COL, DocumentType.FUNCPLOT, DocumentType.LABEL, DocumentType.RTF, DocumentType.IMG, DocumentType.VID].includes(doc.type as any)
? doc.author === Doc.CurrentUserEmail
? StrCast(Doc.UserDoc().showTitle)
: remoteDocHeader
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 3da628cf1..1c48d47e9 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1262,47 +1262,46 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
SharingManager.Instance.users.find(users => users.user.email === this.dataDoc.author)?.sharingDoc.userColor,
Doc.UserDoc().showTitle && [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().userColor) : 'rgba(0,0,0,0.4)'
);
- const titleView =
- !showTitle || Doc.noviceMode ? null : (
- <div
- className={`documentView-titleWrapper${showTitleHover ? '-hover' : ''}`}
- key="title"
- style={{
- position: this.headerMargin ? 'relative' : 'absolute',
- height: this.titleHeight,
- width: !this.headerMargin ? `calc(100% - 18px)` : '100%', // leave room for annotation button
- color: lightOrDark(background),
- background,
- pointerEvents: this.onClickHandler || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
- }}>
- <EditableView
- ref={this._titleRef}
- contents={showTitle
- .split(';')
- .map(field => field.trim())
- .map(field => targetDoc[field]?.toString())
- .join('\\')}
- display={'block'}
- fontSize={10}
- GetValue={() => (showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle)}
- SetValue={undoBatch((input: string) => {
- if (input?.startsWith('#')) {
- if (this.props.showTitle) {
- this.rootDoc._showTitle = input?.substring(1) ? input.substring(1) : undefined;
- } else {
- Doc.UserDoc().showTitle = input?.substring(1) ? input.substring(1) : 'creationDate';
- }
+ const titleView = !showTitle ? null : (
+ <div
+ className={`documentView-titleWrapper${showTitleHover ? '-hover' : ''}`}
+ key="title"
+ style={{
+ position: this.headerMargin ? 'relative' : 'absolute',
+ height: this.titleHeight,
+ width: !this.headerMargin ? `calc(100% - 18px)` : '100%', // leave room for annotation button
+ color: lightOrDark(background),
+ background,
+ pointerEvents: this.onClickHandler || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
+ }}>
+ <EditableView
+ ref={this._titleRef}
+ contents={showTitle
+ .split(';')
+ .map(field => field.trim())
+ .map(field => targetDoc[field]?.toString())
+ .join('\\')}
+ display={'block'}
+ fontSize={10}
+ GetValue={() => (showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle)}
+ SetValue={undoBatch((input: string) => {
+ if (input?.startsWith('#')) {
+ if (this.props.showTitle) {
+ this.rootDoc._showTitle = input?.substring(1) ? input.substring(1) : undefined;
} else {
- var value = input.replace(new RegExp(showTitle + '='), '') as string | number;
- if (showTitle !== 'title' && Number(value).toString() === value) value = Number(value);
- if (showTitle.includes('Date') || showTitle === 'author') return true;
- Doc.SetInPlace(targetDoc, showTitle, value, true);
+ Doc.UserDoc().showTitle = input?.substring(1) ? input.substring(1) : 'creationDate';
}
- return true;
- })}
- />
- </div>
- );
+ } else {
+ var value = input.replace(new RegExp(showTitle + '='), '') as string | number;
+ if (showTitle !== 'title' && Number(value).toString() === value) value = Number(value);
+ if (showTitle.includes('Date') || showTitle === 'author') return true;
+ Doc.SetInPlace(targetDoc, showTitle, value, true);
+ }
+ return true;
+ })}
+ />
+ </div>
+ );
return this.props.hideTitle || (!showTitle && !showCaption) ? (
this.contents
) : (