未验证 提交 d3db54dd 编写于 作者: B Boris Sekachev 提交者: GitHub

Fixed: Issues disappear when using a zoom (#4189)

* Fixed: Issues disappear when using a zoom

* Fixed creating issues for ellipses and rotated shapes

* Updated version & changelog
上级 bc4ff49b
......@@ -58,6 +58,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Bug: canvas is busy when start playing, start resizing a shape and do not release the mouse cursor (<https://github.com/openvinotoolkit/cvat/pull/4151>)
- Bug: could not receive frame N. TypeError: Cannot read properties of undefined (reding "filename") (<https://github.com/openvinotoolkit/cvat/pull/4187>)
- Fixed tus upload error over https (<https://github.com/openvinotoolkit/cvat/pull/4154>)
- Issues disappear when rescale a browser (<https://github.com/openvinotoolkit/cvat/pull/4189>)
- Auth token key is not returned when registering without email verification (<https://github.com/openvinotoolkit/cvat/pull/4092>)
### Security
......
......@@ -184,6 +184,7 @@ Standard JS events are used.
- canvas.zoomstart
- canvas.zoomstop
- canvas.zoom
- canvas.reshape
- canvas.fit
- canvas.dragshape => {id: number}
- canvas.roiselected => {points: number[]}
......
{
"name": "cvat-canvas",
"version": "2.12.2",
"version": "2.13.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "cvat-canvas",
"version": "2.12.2",
"version": "2.13.0",
"license": "MIT",
"dependencies": {
"@types/polylabel": "^1.0.5",
......
{
"name": "cvat-canvas",
"version": "2.12.2",
"version": "2.13.0",
"description": "Part of Computer Vision Annotation Tool which presents its canvas library",
"main": "src/canvas.ts",
"scripts": {
......
......@@ -74,7 +74,6 @@ polyline.cvat_shape_drawing_opacity {
}
.cvat_canvas_issue_region {
display: none;
stroke-width: 0;
}
......
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -29,7 +29,7 @@ const CanvasVersion = pjson.version;
interface Canvas {
html(): HTMLDivElement;
setup(frameData: any, objectStates: any[], zLayer?: number): void;
setupIssueRegions(issueRegions: Record<number, number[]>): void;
setupIssueRegions(issueRegions: Record<number, { hidden: boolean; points: number[] }>): void;
activate(clientID: number | null, attributeID?: number): void;
rotate(rotationAngle: number): void;
focus(clientID: number, padding?: number): void;
......@@ -77,7 +77,7 @@ class CanvasImpl implements Canvas {
this.model.setup(frameData, objectStates, zLayer);
}
public setupIssueRegions(issueRegions: Record<number, number[]>): void {
public setupIssueRegions(issueRegions: Record<number, { hidden: boolean; points: number[] }>): void {
this.model.setupIssueRegions(issueRegions);
}
......
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -19,7 +19,7 @@ import {
export interface CanvasController {
readonly objects: any[];
readonly issueRegions: Record<number, number[]>;
readonly issueRegions: Record<number, { hidden: boolean; points: number[] }>;
readonly zLayer: number | null;
readonly focusData: FocusData;
readonly activeElement: ActiveElement;
......@@ -123,7 +123,7 @@ export class CanvasControllerImpl implements CanvasController {
return this.model.zLayer;
}
public get issueRegions(): Record<number, number[]> {
public get issueRegions(): Record<number, { hidden: boolean; points: number[] }> {
return this.model.issueRegions;
}
......
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -172,7 +172,7 @@ export enum Mode {
export interface CanvasModel {
readonly imageBitmap: boolean;
readonly image: Image | null;
readonly issueRegions: Record<number, number[]>;
readonly issueRegions: Record<number, { hidden: boolean; points: number[] }>;
readonly objects: any[];
readonly zLayer: number | null;
readonly gridSize: Size;
......@@ -193,7 +193,7 @@ export interface CanvasModel {
move(topOffset: number, leftOffset: number): void;
setup(frameData: any, objectStates: any[], zLayer: number): void;
setupIssueRegions(issueRegions: Record<number, number[]>): void;
setupIssueRegions(issueRegions: Record<number, { hidden: boolean; points: number[] }>): void;
activate(clientID: number | null, attributeID: number | null): void;
rotate(rotationAngle: number): void;
focus(clientID: number, padding: number): void;
......@@ -234,7 +234,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
gridSize: Size;
left: number;
objects: any[];
issueRegions: Record<number, number[]>;
issueRegions: Record<number, { hidden: boolean; points: number[] }>;
scale: number;
top: number;
zLayer: number | null;
......@@ -353,6 +353,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.notify(UpdateReasons.FITTED_CANVAS);
this.notify(UpdateReasons.OBJECTS_UPDATED);
this.notify(UpdateReasons.ISSUE_REGIONS_UPDATED);
}
public bitmap(enabled: boolean): void {
......@@ -445,7 +446,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
});
}
public setupIssueRegions(issueRegions: Record<number, number[]>): void {
public setupIssueRegions(issueRegions: Record<number, { hidden: boolean; points: number[] }>): void {
this.data.issueRegions = issueRegions;
this.notify(UpdateReasons.ISSUE_REGIONS_UPDATED);
}
......@@ -756,7 +757,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
return this.data.image;
}
public get issueRegions(): Record<number, number[]> {
public get issueRegions(): Record<number, { hidden: boolean; points: number[] }> {
return { ...this.data.issueRegions };
}
......
......@@ -606,7 +606,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
}
private setupIssueRegions(issueRegions: Record<number, number[]>): void {
private setupIssueRegions(issueRegions: Record<number, { hidden: boolean; points: number[] }>): void {
for (const issueRegion of Object.keys(this.drawnIssueRegions)) {
if (!(issueRegion in issueRegions) || !+issueRegion) {
this.drawnIssueRegions[+issueRegion].remove();
......@@ -616,7 +616,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
for (const issueRegion of Object.keys(issueRegions)) {
if (issueRegion in this.drawnIssueRegions) continue;
const points = this.translateToCanvas(issueRegions[+issueRegion]);
const points = this.translateToCanvas(issueRegions[+issueRegion].points);
if (points.length === 2) {
this.drawnIssueRegions[+issueRegion] = this.adoptedContent
.circle((consts.BASE_POINT_SIZE * 3 * 2) / this.geometry.scale)
......@@ -656,6 +656,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
'stroke-width': `${consts.BASE_STROKE_WIDTH / this.geometry.scale}`,
});
}
if (issueRegions[+issueRegion].hidden) {
this.drawnIssueRegions[+issueRegion].style({ display: 'none' });
}
}
}
......@@ -1263,8 +1267,15 @@ export class CanvasViewImpl implements CanvasView, Listener {
} else if (reason === UpdateReasons.FITTED_CANVAS) {
// Canvas geometry is going to be changed. Old object positions aren't valid any more
this.setupObjects([]);
this.setupIssueRegions({});
this.moveCanvas();
this.resizeCanvas();
this.canvas.dispatchEvent(
new CustomEvent('canvas.reshape', {
bubbles: false,
cancelable: true,
}),
);
} else if ([UpdateReasons.IMAGE_ZOOMED, UpdateReasons.IMAGE_FITTED].includes(reason)) {
this.moveCanvas();
this.transformCanvas();
......
{
"name": "cvat-ui",
"version": "1.33.2",
"version": "1.33.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "cvat-ui",
"version": "1.33.2",
"version": "1.33.3",
"license": "MIT",
"dependencies": {
"@ant-design/icons": "^4.6.3",
......
{
"name": "cvat-ui",
"version": "1.33.2",
"version": "1.33.3",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
......
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -10,6 +10,7 @@ import { MenuInfo } from 'rc-menu/lib/interface';
import ObjectItemContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-item';
import { Workspace } from 'reducers/interfaces';
import { rotatePoint } from 'utils/math';
import consts from 'consts';
interface Props {
......@@ -106,7 +107,29 @@ export default function CanvasContextMenu(props: Props): JSX.Element | null {
);
if (param.key === ReviewContextMenuKeys.OPEN_ISSUE) {
if (state) {
onStartIssue(state.points);
let { points } = state;
if (['ellipse', 'rectangle'].includes(state.shapeType)) {
const [cx, cy] = state.shapeType === 'ellipse' ? state.points : [
(state.points[0] + state.points[2]) / 2,
(state.points[1] + state.points[3]) / 2,
];
const [rx, ry] = [state.points[2] - cx, cy - state.points[3]];
points = state.shapeType === 'ellipse' ? [
state.points[0] - rx,
state.points[1] - ry,
state.points[0] + rx,
state.points[1] + ry,
] : state.points;
points = [
[points[0], points[1]],
[points[2], points[1]],
[points[2], points[3]],
[points[0], points[3]],
].map(([x, y]: number[]) => rotatePoint(x, y, state.rotation, cx, cy)).flat();
}
onStartIssue(points);
}
} else if (param.key === ReviewContextMenuKeys.QUICK_ISSUE_POSITION) {
if (state) {
......
......@@ -32,7 +32,6 @@ interface Props {
activatedStateID: number | null;
activatedAttributeID: number | null;
annotations: any[];
frameIssues: any[] | null;
frameData: any;
frameAngle: number;
frameFetching: boolean;
......@@ -136,7 +135,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
});
this.initialSetup();
this.updateIssueRegions();
this.updateCanvas();
}
......@@ -148,7 +146,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
outlined,
outlineColor,
showBitmap,
frameIssues,
frameData,
frameAngle,
annotations,
......@@ -256,10 +253,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
}
if (prevProps.frameIssues !== frameIssues) {
this.updateIssueRegions();
}
if (
prevProps.annotations !== annotations ||
prevProps.frameData !== frameData ||
......@@ -684,23 +677,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
}
private updateIssueRegions(): void {
const { frameIssues } = this.props;
const { canvasInstance } = this.props as { canvasInstance: Canvas };
if (frameIssues === null) {
canvasInstance.setupIssueRegions({});
} else {
const regions = frameIssues.reduce((acc: Record<number, number[]>, issue: any): Record<
number,
number[]
> => {
acc[issue.id] = issue.position;
return acc;
}, {});
canvasInstance.setupIssueRegions(regions);
}
}
private updateCanvas(): void {
const {
curZLayer, annotations, frameData, canvasInstance,
......
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -16,7 +16,7 @@ interface Props {
selectIssuePosition(enabled: boolean): void;
}
function ResizeControl(props: Props): JSX.Element {
function CreateIssueControl(props: Props): JSX.Element {
const { activeControl, canvasInstance, selectIssuePosition } = props;
return (
......@@ -43,4 +43,4 @@ function ResizeControl(props: Props): JSX.Element {
);
}
export default React.memo(ResizeControl);
export default React.memo(CreateIssueControl);
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -16,13 +16,15 @@ import { Store } from 'antd/lib/form/interface';
interface FormProps {
top: number;
left: number;
angle: number;
scale: number;
submit(message: string): void;
cancel(): void;
}
function MessageForm(props: FormProps): JSX.Element {
const {
top, left, submit, cancel,
top, left, angle, scale, submit, cancel,
} = props;
function handleSubmit(values: Store): void {
......@@ -30,7 +32,11 @@ function MessageForm(props: FormProps): JSX.Element {
}
return (
<Form className='cvat-create-issue-dialog' style={{ top, left }} onFinish={handleSubmit}>
<Form
className='cvat-create-issue-dialog'
style={{ top, left, transform: `scale(${scale}) rotate(${angle}deg)` }}
onFinish={(values: Store) => handleSubmit(values)}
>
<Form.Item name='issue_description' rules={[{ required: true, message: 'Please, fill out the field' }]}>
<Input autoComplete='off' placeholder='Please, describe the issue' />
</Form.Item>
......@@ -53,16 +59,22 @@ function MessageForm(props: FormProps): JSX.Element {
interface Props {
top: number;
left: number;
angle: number;
scale: number;
}
export default function CreateIssueDialog(props: Props): ReactPortal {
const dispatch = useDispatch();
const { top, left } = props;
const {
top, left, angle, scale,
} = props;
return ReactDOM.createPortal(
<MessageForm
top={top}
left={left}
angle={angle}
scale={scale}
submit={(message: string) => {
dispatch(finishIssueAsync(message));
}}
......
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -14,6 +14,8 @@ interface Props {
message: string;
top: number;
left: number;
angle: number;
scale: number;
resolved: boolean;
onClick: () => void;
highlight: () => void;
......@@ -22,7 +24,7 @@ interface Props {
export default function HiddenIssueLabel(props: Props): ReactPortal {
const {
id, message, top, left, resolved, onClick, highlight, blur,
id, message, top, left, angle, scale, resolved, onClick, highlight, blur,
} = props;
const ref = useRef<HTMLElement>(null);
......@@ -54,7 +56,7 @@ export default function HiddenIssueLabel(props: Props): ReactPortal {
}
}
}}
style={{ top, left }}
style={{ top, left, transform: `scale(${scale}) rotate(${angle}deg)` }}
className='cvat-hidden-issue-label'
>
{resolved ? (
......
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -29,6 +29,8 @@ interface Props {
top: number;
resolved: boolean;
isFetching: boolean;
angle: number;
scale: number;
collapse: () => void;
resolve: () => void;
reopen: () => void;
......@@ -46,6 +48,8 @@ export default function IssueDialog(props: Props): JSX.Element {
id,
left,
top,
scale,
angle,
resolved,
isFetching,
collapse,
......@@ -112,7 +116,7 @@ export default function IssueDialog(props: Props): JSX.Element {
);
return ReactDOM.createPortal(
<div style={{ top, left }} ref={ref} className='cvat-issue-dialog'>
<div style={{ top, left, transform: `scale(${scale}) rotate(${angle}deg)` }} ref={ref} className='cvat-issue-dialog'>
<Row className='cvat-issue-dialog-header' justify='space-between'>
<Col>
<Title level={4}>{id >= 0 ? `Issue #${id}` : 'Issue'}</Title>
......
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -15,73 +15,80 @@ import CreateIssueDialog from './create-issue-dialog';
import HiddenIssueLabel from './hidden-issue-label';
import IssueDialog from './issue-dialog';
const scaleHandler = (canvasInstance: Canvas): void => {
const { geometry } = canvasInstance;
const createDialogs = window.document.getElementsByClassName('cvat-create-issue-dialog');
const hiddenIssues = window.document.getElementsByClassName('cvat-hidden-issue-label');
const issues = window.document.getElementsByClassName('cvat-issue-dialog');
for (const element of [...Array.from(createDialogs), ...Array.from(hiddenIssues), ...Array.from(issues)]) {
(element as HTMLSpanElement).style.transform = `scale(${1 / geometry.scale}) rotate(${-geometry.angle}deg)`;
}
};
export default function IssueAggregatorComponent(): JSX.Element | null {
const dispatch = useDispatch();
const [expandedIssue, setExpandedIssue] = useState<number | null>(null);
const frameIssues = useSelector((state: CombinedState): any[] => state.review.frameIssues);
const issuesHidden = useSelector((state: CombinedState): boolean => state.review.issuesHidden);
const issuesResolvedHidden = useSelector((state: CombinedState): boolean => state.review.issuesResolvedHidden);
const canvasInstance = useSelector((state: CombinedState) => state.annotation.canvas.instance);
const canvasIsReady = useSelector((state: CombinedState): boolean => state.annotation.canvas.ready);
const newIssuePosition = useSelector((state: CombinedState): number[] | null => state.review.newIssuePosition);
const issuesHidden = useSelector((state: CombinedState): any => state.review.issuesHidden);
const issuesResolvedHidden = useSelector((state: CombinedState): any => state.review.issuesResolvedHidden);
const issueFetching = useSelector((state: CombinedState): number | null => state.review.fetching.issueId);
const [geometry, setGeometry] = useState<Canvas['geometry'] | null>(null);
const issueLabels: JSX.Element[] = [];
const issueDialogs: JSX.Element[] = [];
if (!(canvasInstance instanceof Canvas)) return null;
useEffect(() => {
scaleHandler(canvasInstance);
});
useEffect(() => {
const regions = frameIssues.reduce((acc: Record<number, number[]>, issue: any): Record<number, number[]> => {
acc[issue.id] = issue.position;
return acc;
}, {});
if (newIssuePosition) {
regions[0] = newIssuePosition;
if (canvasInstance instanceof Canvas) {
const { geometry: updatedGeometry } = canvasInstance;
setGeometry(updatedGeometry);
const geometryListener = (): void => {
setGeometry(canvasInstance.geometry);
};
canvasInstance.html().addEventListener('canvas.zoom', geometryListener);
canvasInstance.html().addEventListener('canvas.fit', geometryListener);
canvasInstance.html().addEventListener('canvas.reshape', geometryListener);
return () => {
canvasInstance.html().removeEventListener('canvas.zoom', geometryListener);
canvasInstance.html().removeEventListener('canvas.fit', geometryListener);
canvasInstance.html().addEventListener('canvas.reshape', geometryListener);
};
}
canvasInstance.setupIssueRegions(regions);
if (newIssuePosition) {
setExpandedIssue(null);
const element = window.document.getElementById('cvat_canvas_issue_region_0');
if (element) {
element.style.display = 'block';
}
}
}, [newIssuePosition]);
return () => {};
}, [canvasInstance]);
useEffect(() => {
const listener = (): void => scaleHandler(canvasInstance);
if (canvasInstance instanceof Canvas) {
type IssueRegionSet = Record<number, { hidden: boolean; points: number[] }>;
const regions = !issuesHidden ? frameIssues
.filter((_issue: any) => !issuesResolvedHidden || !_issue.resolved)
.reduce((acc: IssueRegionSet, issue: any): IssueRegionSet => {
acc[issue.id] = {
points: issue.position,
hidden: issue.resolved,
};
return acc;
}, {}) : {};
if (newIssuePosition) {
// regions[0] is always empty because key is an id of an issue (<0, >0 are possible)
regions[0] = {
points: newIssuePosition,
hidden: false,
};
}
canvasInstance.html().addEventListener('canvas.zoom', listener);
canvasInstance.html().addEventListener('canvas.fit', listener);
canvasInstance.setupIssueRegions(regions);
return () => {
canvasInstance.html().removeEventListener('canvas.zoom', listener);
canvasInstance.html().removeEventListener('canvas.fit', listener);
};
}, []);
if (newIssuePosition) {
setExpandedIssue(null);
const element = window.document.getElementById('cvat_canvas_issue_region_0');
if (element) {
element.style.display = 'block';
}
}
}
}, [newIssuePosition, frameIssues, issuesResolvedHidden, issuesHidden, canvasInstance]);
if (!canvasIsReady) {
if (!(canvasInstance instanceof Canvas) || !canvasIsReady || !geometry) {
return null;
}
const { geometry } = canvasInstance;
for (const issue of frameIssues) {
if (issuesHidden) break;
const issueResolved = issue.resolved;
......@@ -102,7 +109,7 @@ export default function IssueAggregatorComponent(): JSX.Element | null {
if (issueResolved) {
const element = window.document.getElementById(`cvat_canvas_issue_region_${id}`);
if (element) {
element.style.display = '';
element.style.display = 'none';
}
}
};
......@@ -114,6 +121,8 @@ export default function IssueAggregatorComponent(): JSX.Element | null {
id={issue.id}
top={minY}
left={minX}
angle={-geometry.angle}
scale={1 / geometry.scale}
isFetching={issueFetching !== null}
comments={issue.comments}
resolved={issueResolved}
......@@ -141,6 +150,8 @@ export default function IssueAggregatorComponent(): JSX.Element | null {
id={issue.id}
top={minY}
left={minX}
angle={-geometry.angle}
scale={1 / geometry.scale}
resolved={issueResolved}
message={issue.comments[issue.comments.length - 1].message}
highlight={highlight}
......@@ -163,7 +174,14 @@ export default function IssueAggregatorComponent(): JSX.Element | null {
return (
<>
{createLeft !== null && createTop !== null && <CreateIssueDialog top={createTop} left={createLeft} />}
{createLeft !== null && createTop !== null ? (
<CreateIssueDialog
top={createTop}
left={createLeft}
angle={-geometry.angle}
scale={1 / geometry.scale}
/>
) : null}
{issueDialogs}
{issueLabels}
</>
......
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -58,7 +58,6 @@ interface StateToProps {
activatedStateID: number | null;
activatedAttributeID: number | null;
annotations: any[];
frameIssues: any[] | null;
frameData: any;
frameAngle: number;
frameFetching: boolean;
......@@ -175,20 +174,13 @@ function mapStateToProps(state: CombinedState): StateToProps {
opacity, colorBy, selectedOpacity, outlined, outlineColor, showBitmap, showProjections,
},
},
review: { frameIssues, issuesHidden, issuesResolvedHidden },
shortcuts: { keyMap },
} = state;
const issues = frameIssues.filter((issue) => (
!issuesHidden && [Workspace.REVIEW_WORKSPACE, Workspace.STANDARD].includes(workspace) &&
!(!!issue.resolvedDate && issuesResolvedHidden)
));
return {
sidebarCollapsed,
canvasInstance,
jobInstance,
frameIssues: issues,
frameData,
frameAngle: frameAngles[frame - jobInstance.startFrame],
frameFetching,
......
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -86,7 +86,8 @@ export default function (state: ReviewState = defaultState, action: any): Review
}
case ReviewActionTypes.FINISH_ISSUE_SUCCESS: {
const { frame, issue } = action.payload;
const frameIssues = [...state.issues, issue].filter((_issue: any): boolean => _issue.frame === frame);
const issues = [...state.issues, issue];
const frameIssues = issues.filter((_issue: any): boolean => _issue.frame === frame);
return {
...state,
......@@ -103,6 +104,7 @@ export default function (state: ReviewState = defaultState, action: any): Review
),
).slice(-consts.LATEST_COMMENTS_SHOWN_QUICK_ISSUE),
frameIssues,
issues,
newIssuePosition: null,
};
}
......
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -37,3 +37,11 @@ export function pointsToNumberArray(points: Point[]): number[] {
return acc;
}, []);
}
export function rotatePoint(x: number, y: number, angle: number, cx = 0, cy = 0): number[] {
const sin = Math.sin((angle * Math.PI) / 180);
const cos = Math.cos((angle * Math.PI) / 180);
const rotX = (x - cx) * cos - (y - cy) * sin + cx;
const rotY = (y - cy) * cos + (x - cx) * sin + cy;
return [rotX, rotY];
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册