From 1dda8be77e275cc5adaa40081b6ecbbfb8a8ab3c Mon Sep 17 00:00:00 2001 From: chenshenhai Date: Sat, 3 Aug 2024 21:41:11 +0800 Subject: [PATCH] feat: improve control of middleware selector --- packages/board/src/index.ts | 12 ++ packages/board/src/lib/watcher.ts | 12 +- packages/core/src/middleware/info/config.ts | 2 + .../core/src/middleware/info/draw-info.ts | 2 +- packages/core/src/middleware/info/index.ts | 54 +++++--- .../core/src/middleware/selector/config.ts | 5 + .../src/middleware/selector/draw-debug.ts | 49 ++++++++ .../src/middleware/selector/draw-wrapper.ts | 30 ++++- .../core/src/middleware/selector/index.ts | 89 +++++++++----- .../selector/pattern/icon-rotate.ts | 102 ++++++++++++++++ .../src/middleware/selector/pattern/index.ts | 45 +++++++ packages/core/src/middleware/selector/util.ts | 2 +- packages/renderer/src/draw/index.ts | 1 + packages/renderer/src/index.ts | 14 ++- packages/types/src/lib/board.ts | 1 + packages/types/src/lib/controller.ts | 1 + packages/util/src/lib/controller.ts | 115 ++++++++++-------- packages/util/src/lib/rotate.ts | 26 ++-- 18 files changed, 446 insertions(+), 116 deletions(-) create mode 100644 packages/core/src/middleware/selector/draw-debug.ts create mode 100644 packages/core/src/middleware/selector/pattern/icon-rotate.ts create mode 100644 packages/core/src/middleware/selector/pattern/index.ts diff --git a/packages/board/src/index.ts b/packages/board/src/index.ts index cba4da8..7864d52 100644 --- a/packages/board/src/index.ts +++ b/packages/board/src/index.ts @@ -122,6 +122,8 @@ export class Board { // }, throttleTime) // ); this.#watcher.on('pointMove', this.#handlePointMove.bind(this)); + this.#watcher.on('pointLeave', this.#handlePointLeave.bind(this)); + this.#watcher.on('hover', this.#handleHover.bind(this)); this.#watcher.on('wheel', this.#handleWheel.bind(this)); this.#watcher.on('wheelScale', this.#handleWheelScale.bind(this)); @@ -166,6 +168,16 @@ export class Board { } } + #handlePointLeave(e: BoardWatcherEventMap['pointLeave']) { + for (let i = 0; i < this.#activeMiddlewareObjs.length; i++) { + const obj = this.#activeMiddlewareObjs[i]; + const result = obj?.pointLeave?.(e); + if (result === false) { + return; + } + } + } + #handleHover(e: BoardWatcherEventMap['hover']) { for (let i = 0; i < this.#activeMiddlewareObjs.length; i++) { const obj = this.#activeMiddlewareObjs[i]; diff --git a/packages/board/src/lib/watcher.ts b/packages/board/src/lib/watcher.ts index a88587f..2529ff9 100644 --- a/packages/board/src/lib/watcher.ts +++ b/packages/board/src/lib/watcher.ts @@ -11,7 +11,7 @@ export class BoardWatcher extends EventEmitter { #hasDestroyed: boolean = false; constructor(opts: BoardWatcherOptions) { super(); - const store = new Store({ defaultStorage: { hasPointDown: false, prevClickPoint: null } }); + const store = new Store({ defaultStorage: { hasPointDown: false, prevClickPoint: null, inCanvas: true } }); this.#store = store; this.#opts = opts; this.#init(); @@ -30,7 +30,7 @@ export class BoardWatcher extends EventEmitter { container.addEventListener('mousedown', this.#onPointStart); container.addEventListener('mousemove', this.#onPointMove); container.addEventListener('mouseup', this.#onPointEnd); - container.addEventListener('mouseleave', this.#onPointLeave); + // container.addEventListener('mouseleave', this.#onPointLeave); container.addEventListener('wheel', this.#onWheel, { passive: false }); container.addEventListener('click', this.#onClick); container.addEventListener('contextmenu', this.#onContextMenu); @@ -110,9 +110,6 @@ export class BoardWatcher extends EventEmitter { #onPointLeave = (e: MouseEvent) => { this.#store.set('hasPointDown', false); - if (!this.#isInTarget(e)) { - return; - } e.preventDefault(); const point = this.#getPoint(e); this.trigger('pointLeave', { point }); @@ -169,8 +166,13 @@ export class BoardWatcher extends EventEmitter { #onHover = (e: MouseEvent) => { if (!this.#isInTarget(e)) { + if (this.#store.get('inCanvas') === true) { + this.#store.set('inCanvas', false); + this.#onPointLeave(e); + } return; } + this.#store.set('inCanvas', true); // if (!this.#store.get('hasPointDown')) { // return; // } diff --git a/packages/core/src/middleware/info/config.ts b/packages/core/src/middleware/info/config.ts index 5706495..9bdc995 100644 --- a/packages/core/src/middleware/info/config.ts +++ b/packages/core/src/middleware/info/config.ts @@ -6,6 +6,8 @@ const infoTextColor = '#ffffff'; export const infoFontSize = 10; export const infoLineHeight = 16; +export const MIDDLEWARE_INTERNAL_EVENT_SHOW_INFO_ANGLE = '@middleware/internal-event/show-info-angle'; + export const defaltStyle: MiddlewareInfoStyle = { textBackground: infoBackground, textColor: infoTextColor diff --git a/packages/core/src/middleware/info/draw-info.ts b/packages/core/src/middleware/info/draw-info.ts index 3455050..9fced8b 100644 --- a/packages/core/src/middleware/info/draw-info.ts +++ b/packages/core/src/middleware/info/draw-info.ts @@ -1,6 +1,6 @@ import type { PointSize, ViewContext2D } from '@idraw/types'; import { rotateByCenter } from '@idraw/util'; -import type { MiddlewareInfoStyle } from './types'; +import type { MiddlewareInfoStyle } from '@idraw/types'; const fontFamily = 'monospace'; diff --git a/packages/core/src/middleware/info/index.ts b/packages/core/src/middleware/info/index.ts index 3db9b26..bc7786a 100644 --- a/packages/core/src/middleware/info/index.ts +++ b/packages/core/src/middleware/info/index.ts @@ -3,13 +3,21 @@ import { formatNumber, getViewScaleInfoFromSnapshot, getViewSizeInfoFromSnapshot import { keySelectedElementList, keyActionType, keyGroupQueue } from '../selector'; import { drawSizeInfoText, drawPositionInfoText, drawAngleInfoText } from './draw-info'; import type { DeepInfoSharedStorage } from './types'; -import { defaltStyle } from './config'; +import { defaltStyle, MIDDLEWARE_INTERNAL_EVENT_SHOW_INFO_ANGLE } from './config'; + +export { MIDDLEWARE_INTERNAL_EVENT_SHOW_INFO_ANGLE }; const infoFontSize = 10; const infoLineHeight = 16; -export const MiddlewareInfo: BoardMiddleware = (opts, config) => { - const { boardContent, calculator } = opts; +export const MiddlewareInfo: BoardMiddleware< + DeepInfoSharedStorage, + CoreEventMap & { + [MIDDLEWARE_INTERNAL_EVENT_SHOW_INFO_ANGLE]: { show: boolean }; + }, + MiddlewareInfoConfig +> = (opts, config) => { + const { boardContent, calculator, eventHub } = opts; const { overlayContext } = boardContent; const innerConfig = { ...defaltStyle, @@ -21,9 +29,23 @@ export const MiddlewareInfo: BoardMiddleware { + showAngleInfo = show; + }; + return { name: '@middleware/info', + use() { + eventHub.on(MIDDLEWARE_INTERNAL_EVENT_SHOW_INFO_ANGLE, showInfoAngleCallback); + }, + + disuse() { + eventHub.off(MIDDLEWARE_INTERNAL_EVENT_SHOW_INFO_ANGLE, showInfoAngleCallback); + }, + beforeDrawFrame({ snapshot }) { const { sharedStore } = snapshot; @@ -123,18 +145,20 @@ export const MiddlewareInfo: BoardMiddleware = (opts, config) => { +export const MiddlewareSelector: BoardMiddleware< + DeepSelectorSharedStorage, + CoreEventMap & { + [MIDDLEWARE_INTERNAL_EVENT_SHOW_INFO_ANGLE]: { show: boolean }; + }, + MiddlewareSelectorConfig +> = (opts, config) => { const innerConfig = { ...defaultStyle, ...config @@ -102,6 +113,11 @@ export const MiddlewareSelector: BoardMiddleware[], opts?: { triggerEvent?: boolean }) => { - sharer.setSharedStorage(keySelectedElementList, list); + const updateSelectedElemenetController = () => { + const list = sharer.getSharedStorage(keySelectedElementList); if (list.length === 1) { const controller = calcElementSizeController(list[0], { groupQueue: sharer.getSharedStorage(keyGroupQueue), controllerSize, - viewScaleInfo: sharer.getActiveViewScaleInfo() + viewScaleInfo: sharer.getActiveViewScaleInfo(), + rotateControllerPosition, + rotateControllerSize }); sharer.setSharedStorage(keySelectedElementController, controller); + } + }; + + const updateSelectedElementList = (list: Element[], opts?: { triggerEvent?: boolean }) => { + sharer.setSharedStorage(keySelectedElementList, list); + if (list.length === 1) { + updateSelectedElemenetController(); sharer.setSharedStorage(keySelectedElementPosition, getElementPositionFromList(list[0].uuid, sharer.getActiveStorage('data')?.elements || [])); } else { sharer.setSharedStorage(keySelectedElementController, null); @@ -260,6 +285,7 @@ export const MiddlewareSelector: BoardMiddleware { if (layoutIsSelected === true) { return; @@ -327,6 +353,7 @@ export const MiddlewareSelector: BoardMiddleware { const groupQueue = sharer.getSharedStorage(keyGroupQueue); @@ -825,16 +866,8 @@ export const MiddlewareSelector: BoardMiddleware { + const iconRotate: Element<'path'> = { + uuid: createUUID(), + type: 'path', + x: 0, + y: 0, + w: 200, + h: 200, + detail: { + commands: [ + { + type: 'M', + params: [512, 0] + }, + { + type: 'c', + params: [282.8, 0, 512, 229.2, 512, 512] + }, + { + type: 's', + params: [-229.2, 512, -512, 512] + }, + { + type: 'S', + params: [0, 794.8, 0, 512, 229.2, 0, 512, 0] + }, + { + type: 'z', + params: [] + }, + { + type: 'm', + params: [309.8, 253.8] + }, + { + type: 'c', + params: [0, -10.5, -6.5, -19.8, -15.7, -23.8, -9.7, -4, -21, -2, -28.2, 5.6] + }, + { + type: 'l', + params: [-52.5, 52] + }, + { + type: 'c', + params: [ + -56.9, -53.7, -133.9, -85.5, -213.4, -85.5, -170.7, 0, -309.8, 139.2, -309.8, 309.8, 0, 170.6, 139.2, 309.8, 309.8, 309.8, 92.4, 0, 179.5, -40.8, + 238.4, -111.8, 4, -5.2, 4, -12.9, -0.8, -17.3 + ] + }, + { + type: 'L', + params: [694.3, 637] + }, + { + type: 'c', + params: [ + -2.8, -2.4, -6.5, -3.6, -10.1, -3.6, -3.6, 0.4, -7.3, 2, -9.3, 4.8, -39.5, 51.2, -98.8, 80.3, -163, 80.3, -113.8, 0, -206.5, -92.8, -206.5, -206.5, + 0, -113.8, 92.8, -206.5, 206.5, -206.5, 52.8, 0, 102.9, 20.2, 140.8, 55.3 + ] + }, + { + type: 'L', + params: [597, 416.5] + }, + { + type: 'c', + params: [-7.7, 7.3, -9.7, 18.6, -5.6, 27.9, 4, 9.7, 13.3, 16.1, 23.8, 16.1] + }, + { + type: 'H', + params: [796] + }, + { + type: 'c', + params: [14.1, 0, 25.8, -11.7, 25.8, -25.8] + }, + { + type: 'V', + params: [253.8] + }, + { + type: 'z', + params: [] + } + ], + fill: '#2c2c2c', + stroke: 'transparent', + strokeWidth: 0, + originX: 0, + originY: 0, + originW: 1024, + originH: 1024, + opacity: 1, + ...opts + } + }; + + return iconRotate; +}; diff --git a/packages/core/src/middleware/selector/pattern/index.ts b/packages/core/src/middleware/selector/pattern/index.ts new file mode 100644 index 0000000..9623271 --- /dev/null +++ b/packages/core/src/middleware/selector/pattern/index.ts @@ -0,0 +1,45 @@ +import type { ViewContext2D } from '@idraw/types'; +import { createOffscreenContext2D } from '@idraw/util'; +import { drawElement } from '@idraw/renderer'; +import { createIconRotate } from './icon-rotate'; + +export function createRotateControllerPattern(opts: { fill: string; devicePixelRatio: number }): ViewContext2D { + const { fill, devicePixelRatio } = opts; + const iconRotate = createIconRotate({ fill }); + const { w, h } = iconRotate; + const context2d = createOffscreenContext2D({ + width: w, + height: h, + devicePixelRatio + }); + + // context2d.fillStyle = 'red'; // TODO + // context2d.fillRect(0, 0, size, size); + + drawElement(context2d, iconRotate, { + loader: undefined as any, + viewScaleInfo: { + scale: 1, + offsetTop: 0, + offsetBottom: 0, + offsetLeft: 0, + offsetRight: 0 + }, + viewSizeInfo: { + width: w, + height: h, + devicePixelRatio, + contextWidth: w, + contextHeight: h + }, + parentElementSize: { + x: 0, + y: 0, + w, + h + }, + parentOpacity: 1 + }); + + return context2d; +} diff --git a/packages/core/src/middleware/selector/util.ts b/packages/core/src/middleware/selector/util.ts index 2921175..cb57624 100644 --- a/packages/core/src/middleware/selector/util.ts +++ b/packages/core/src/middleware/selector/util.ts @@ -839,7 +839,7 @@ export function rotateElement( }); const startAngle = limitAngle(angle); const changedRadian = calcRadian(elemCenter, start, end); - const endAngle = startAngle + parseRadianToAngle(changedRadian); + const endAngle = limitAngle(startAngle + parseRadianToAngle(changedRadian)); return { x, diff --git a/packages/renderer/src/draw/index.ts b/packages/renderer/src/draw/index.ts index b7cd272..a15b709 100644 --- a/packages/renderer/src/draw/index.ts +++ b/packages/renderer/src/draw/index.ts @@ -4,6 +4,7 @@ export { drawImage } from './image'; export { drawSVG } from './svg'; export { drawHTML } from './html'; export { drawText } from './text'; +export { drawGroup, drawElement } from './group'; export { drawElementList } from './elements'; export { drawLayout } from './layout'; export { drawGlobalBackground } from './global'; diff --git a/packages/renderer/src/index.ts b/packages/renderer/src/index.ts index aeb28a1..d8b5cab 100644 --- a/packages/renderer/src/index.ts +++ b/packages/renderer/src/index.ts @@ -123,4 +123,16 @@ export class Renderer extends EventEmitter implements BoardRen } } -export { drawRect } from './draw'; +export { + drawCircle, + drawRect, + drawImage, + drawSVG, + drawHTML, + drawText, + drawGroup, + drawElement, + drawElementList, + drawLayout, + drawGlobalBackground +} from './draw'; diff --git a/packages/types/src/lib/board.ts b/packages/types/src/lib/board.ts index 26d1f8b..db66e4a 100644 --- a/packages/types/src/lib/board.ts +++ b/packages/types/src/lib/board.ts @@ -169,5 +169,6 @@ export interface BoardWatcherOptions { export interface BoardWatcherStore { hasPointDown: boolean; + inCanvas: boolean; prevClickPoint: Point | null; } diff --git a/packages/types/src/lib/controller.ts b/packages/types/src/lib/controller.ts index 9614422..dad49a1 100644 --- a/packages/types/src/lib/controller.ts +++ b/packages/types/src/lib/controller.ts @@ -20,6 +20,7 @@ export interface ElementSizeControllerItem { type: ElementSizeControllerType; vertexes: ViewRectVertexes; center: PointSize; + size: number; } export interface ElementSizeController { diff --git a/packages/util/src/lib/controller.ts b/packages/util/src/lib/controller.ts index 523d7b1..262633f 100644 --- a/packages/util/src/lib/controller.ts +++ b/packages/util/src/lib/controller.ts @@ -21,15 +21,20 @@ export function calcElementSizeController( elemSize: ElementSize, opts: { groupQueue: Element<'group'>[]; - controllerSize?: number; + controllerSize: number; + rotateControllerSize: number; + rotateControllerPosition: number; viewScaleInfo: ViewScaleInfo; } ): ElementSizeController { - const { groupQueue, controllerSize, viewScaleInfo } = opts; + const { groupQueue, controllerSize, viewScaleInfo, rotateControllerSize, rotateControllerPosition } = opts; const ctrlSize = (controllerSize && controllerSize > 0 ? controllerSize : 8) / viewScaleInfo.scale; const { x, y, w, h, angle = 0 } = elemSize; + const rotateCtrlSize = rotateControllerSize; + const rotateCtrlPos = rotateControllerPosition; + const ctrlGroupQueue = [ ...[ { @@ -53,10 +58,10 @@ export function calcElementSizeController( const vertexes = calcElementVertexesInGroup(elemSize, { groupQueue }) as ViewRectVertexes; const rotateElemVertexes = calcElementVertexesInGroup( { - x: x - ctrlSize * 2, - y: y - ctrlSize * 2, - h: h + ctrlSize * 4, - w: w + ctrlSize * 4, + x: x, + y: y - (rotateCtrlPos + rotateCtrlSize / 2) / viewScaleInfo.scale, + h: h + (rotateCtrlPos * 2 + rotateCtrlSize) / viewScaleInfo.scale, + w: w, angle }, { groupQueue: [...groupQueue] } @@ -97,92 +102,94 @@ export function calcElementSizeController( const bottomMiddleVertexes = calcElementVertexes(bottomMiddleSize); const leftMiddleVertexes = calcElementVertexes(leftMiddleSize); - // const originRotateCenter: PointSize = { - // x: x + w / 2, - // y: y - ctrlSize * 4 - // }; - - // const rotateCenter = topCenter; const rotateCenter = getCenterFromTwoPoints(rotateElemVertexes[0], rotateElemVertexes[1]); - const rotateSize = createControllerElementSizeFromCenter(rotateCenter, { size: ctrlSize, angle: totalAngle }); + // TODO + const tempRotateSizeRepairRatio = 1.1; + const rotateSize = createControllerElementSizeFromCenter(rotateCenter, { + size: (rotateControllerSize * tempRotateSizeRepairRatio) / viewScaleInfo.scale, + angle: totalAngle + }); const rotateVertexes = calcElementVertexes(rotateSize); - // const rotateCtrlElem: ElementSize = { - // x: originRotateCenter.x - ctrlSize / 2, - // y: originRotateCenter.x - ctrlSize / 2, - // w: ctrlSize, - // h: ctrlSize, - // angle - // }; - // const rotateVertexes = calcElementVertexesInGroup(rotateCtrlElem, { groupQueue }) as ViewRectVertexes; - // const rotateCenter = getCenterFromTwoPoints(rotateVertexes[0], rotateVertexes[2]); - const sizeController: ElementSizeController = { elementWrapper: vertexes, left: { type: 'left', vertexes: leftVertexes, - center: leftCenter + center: leftCenter, + size: ctrlSize }, right: { type: 'right', vertexes: rightVertexes, - center: rightCenter + center: rightCenter, + size: ctrlSize }, top: { type: 'top', vertexes: topVertexes, - center: topCenter + center: topCenter, + size: ctrlSize }, bottom: { type: 'bottom', vertexes: bottomVertexes, - center: bottomCenter + center: bottomCenter, + size: ctrlSize }, topLeft: { type: 'top-left', vertexes: topLeftVertexes, - center: topLeftCenter + center: topLeftCenter, + size: ctrlSize }, topRight: { type: 'top-right', vertexes: topRightVertexes, - center: topRightCenter + center: topRightCenter, + size: ctrlSize }, bottomLeft: { type: 'bottom-left', vertexes: bottomLeftVertexes, - center: bottomLeftCenter + center: bottomLeftCenter, + size: ctrlSize }, bottomRight: { type: 'bottom-right', vertexes: bottomRightVertexes, - center: bottomRightCenter + center: bottomRightCenter, + size: ctrlSize }, leftMiddle: { type: 'left-middle', vertexes: leftMiddleVertexes, - center: leftCenter + center: leftCenter, + size: ctrlSize }, rightMiddle: { type: 'right-middle', vertexes: rightMiddleVertexes, - center: rightCenter + center: rightCenter, + size: ctrlSize }, topMiddle: { type: 'top-middle', vertexes: topMiddleVertexes, - center: topCenter + center: topCenter, + size: ctrlSize }, bottomMiddle: { type: 'bottom-middle', vertexes: bottomMiddleVertexes, - center: bottomCenter + center: bottomCenter, + size: ctrlSize }, rotate: { type: 'rotate', vertexes: rotateVertexes, - center: rotateCenter + center: rotateCenter, + size: rotateControllerSize } }; return sizeController; @@ -241,62 +248,74 @@ export function calcLayoutSizeController( left: { type: 'left', vertexes: leftVertexes, - center: leftCenter + center: leftCenter, + size: ctrlSize }, right: { type: 'right', vertexes: rightVertexes, - center: rightCenter + center: rightCenter, + size: ctrlSize }, top: { type: 'top', vertexes: topVertexes, - center: topCenter + center: topCenter, + size: ctrlSize }, bottom: { type: 'bottom', vertexes: bottomVertexes, - center: bottomCenter + center: bottomCenter, + size: ctrlSize }, topLeft: { type: 'top-left', vertexes: topLeftVertexes, - center: topLeftCenter + center: topLeftCenter, + size: ctrlSize }, topRight: { type: 'top-right', vertexes: topRightVertexes, - center: topRightCenter + center: topRightCenter, + size: ctrlSize }, bottomLeft: { type: 'bottom-left', vertexes: bottomLeftVertexes, - center: bottomLeftCenter + center: bottomLeftCenter, + size: ctrlSize }, bottomRight: { type: 'bottom-right', vertexes: bottomRightVertexes, - center: bottomRightCenter + center: bottomRightCenter, + size: ctrlSize }, leftMiddle: { type: 'left-middle', vertexes: leftMiddleVertexes, - center: leftCenter + center: leftCenter, + size: ctrlSize }, rightMiddle: { type: 'right-middle', vertexes: rightMiddleVertexes, - center: rightCenter + center: rightCenter, + size: ctrlSize }, topMiddle: { type: 'top-middle', vertexes: topMiddleVertexes, - center: topCenter + center: topCenter, + size: ctrlSize }, bottomMiddle: { type: 'bottom-middle', vertexes: bottomMiddleVertexes, - center: bottomCenter + center: bottomCenter, + size: ctrlSize } }; return sizeController; diff --git a/packages/util/src/lib/rotate.ts b/packages/util/src/lib/rotate.ts index b658966..133f9b3 100644 --- a/packages/util/src/lib/rotate.ts +++ b/packages/util/src/lib/rotate.ts @@ -72,16 +72,18 @@ export function calcElementCenterFromVertexes(ves: ViewRectVertexes): PointSize } export function calcRadian(center: PointSize, start: PointSize, end: PointSize): number { - const startAngle = calcLineRadian(center, start); - const endAngle = calcLineRadian(center, end); - if (endAngle !== null && startAngle !== null) { - if (startAngle > (Math.PI * 3) / 2 && endAngle < Math.PI / 2) { - return endAngle + (Math.PI * 2 - startAngle); - } else if (endAngle > (Math.PI * 3) / 2 && startAngle < Math.PI / 2) { - return startAngle + (Math.PI * 2 - endAngle); - } else { - return endAngle - startAngle; - } + const startRadian = calcLineRadian(center, start); + const endRadian = calcLineRadian(center, end); + + if (endRadian !== null && startRadian !== null) { + // if (startRadian > (Math.PI * 3) / 2 && endRadian < Math.PI / 2) { + // return endRadian + (Math.PI * 2 - startRadian); + // } else if (endRadian > (Math.PI * 3) / 2 && startRadian < Math.PI / 2) { + // return startRadian + (Math.PI * 2 - endRadian); + // } else { + // return endRadian - startRadian; + // } + return endRadian - startRadian; } else { return 0; } @@ -221,12 +223,14 @@ export function rotateVertexes(center: PointSize, ves: ViewRectVertexes, radian: // [0, 360], eg. 370 to 10, -10 to 350 export function limitAngle(angle: number): number { - if (!(angle > 0 || angle < 0) || angle === 0) { + if (!(angle > 0 || angle < 0) || angle === 0 || angle === 360) { return 0; } let num = angle % 360; if (num < 0) { num += 360; + } else if (angle === 360) { + num = 0; } return num; }