diff --git a/packages/suika/src/components/Cards/ElementsInfoCard/ElementsInfoCard.tsx b/packages/suika/src/components/Cards/ElementsInfoCard/ElementsInfoCard.tsx index 63cca34f..220dc2e4 100644 --- a/packages/suika/src/components/Cards/ElementsInfoCard/ElementsInfoCard.tsx +++ b/packages/suika/src/components/Cards/ElementsInfoCard/ElementsInfoCard.tsx @@ -2,7 +2,7 @@ import { FC, useContext, useEffect, useState } from 'react'; import { EditorContext } from '../../../context'; import { MutateGraphsAndRecord } from '../../../editor/service/mutate_graphs_and_record'; import { remainDecimal } from '../../../utils/common'; -import { getElementRotatedXY } from '../../../utils/graphics'; +import { getRectRotatedXY } from '../../../utils/geo'; import { deg2Rad, normalizeRadian, rad2Deg } from '@suika/geo'; import { BaseCard } from '../BaseCard'; import NumberInput from '../../input/NumberInput'; @@ -46,7 +46,7 @@ export const ElementsInfoCards: FC = () => { }: { x: number | typeof MIXED; y: number | typeof MIXED; - } = getElementRotatedXY(items[0]); + } = getRectRotatedXY(items[0]); let newGraphType: GraphType | typeof MIXED = items[0].type; let newWidth: number | typeof MIXED = items[0].width; let newHeight: number | typeof MIXED = items[0].height; @@ -59,7 +59,7 @@ export const ElementsInfoCards: FC = () => { newGraphType = MIXED; } const { x: currentRotatedX, y: currentRotatedY } = - getElementRotatedXY(element); + getRectRotatedXY(element); if (!isEqual(newRotatedX, currentRotatedX)) { newRotatedX = MIXED; } diff --git a/packages/suika/src/editor/ref_line.ts b/packages/suika/src/editor/ref_line.ts index 4ee685f1..dab468ee 100644 --- a/packages/suika/src/editor/ref_line.ts +++ b/packages/suika/src/editor/ref_line.ts @@ -6,7 +6,7 @@ import { isRectIntersect2, pointsToHLines, pointsToVLines, -} from '../utils/graphics'; +} from '../utils/geo'; import { getClosestValInSortedArr } from '../utils/common'; import { drawLine, drawXShape } from '../utils/canvas'; import { arrMap, forEach } from '../utils/array_util'; diff --git a/packages/suika/src/editor/scene/control_handle_manager/control_handle.ts b/packages/suika/src/editor/scene/control_handle_manager/control_handle.ts index b7e04c5d..3753f57d 100644 --- a/packages/suika/src/editor/scene/control_handle_manager/control_handle.ts +++ b/packages/suika/src/editor/scene/control_handle_manager/control_handle.ts @@ -9,7 +9,11 @@ type HitTest = ( rect: IRectWithRotation, ) => boolean; -type GetCursor = (type: string, rotation: number) => ICursor; +type GetCursor = ( + type: string, + rotation: number, + selectedBox: IRectWithRotation, +) => ICursor; export class ControlHandle { cx: number; diff --git a/packages/suika/src/editor/scene/control_handle_manager/control_handle_manager.ts b/packages/suika/src/editor/scene/control_handle_manager/control_handle_manager.ts index 52709adb..2aef8494 100644 --- a/packages/suika/src/editor/scene/control_handle_manager/control_handle_manager.ts +++ b/packages/suika/src/editor/scene/control_handle_manager/control_handle_manager.ts @@ -190,7 +190,7 @@ export class ControlHandleManager { ...(this.customHandlesVisible ? this.customHandles : []), ]; - const box = this.editor.selectedBox.getBox(); + const selectedBox = this.editor.selectedBox.getBox()!; for (let i = handles.length - 1; i >= 0; i--) { const handle = handles[i]; @@ -201,13 +201,18 @@ export class ControlHandleManager { } const isHit = handle.hitTest - ? handle.hitTest(hitPointVW.x, hitPointVW.y, handle.padding, box!) + ? handle.hitTest( + hitPointVW.x, + hitPointVW.y, + handle.padding, + selectedBox, + ) : handle.graph.hitTest(hitPointVW.x, hitPointVW.y, handle.padding); if (isHit) { return { handleName: type, - cursor: handle.getCursor(type, rotation), + cursor: handle.getCursor(type, rotation, selectedBox), }; } } diff --git a/packages/suika/src/editor/scene/control_handle_manager/util.ts b/packages/suika/src/editor/scene/control_handle_manager/util.ts index ae12cc74..3577c87c 100644 --- a/packages/suika/src/editor/scene/control_handle_manager/util.ts +++ b/packages/suika/src/editor/scene/control_handle_manager/util.ts @@ -3,10 +3,19 @@ import { ITexture, TextureType } from '../../texture'; import { Rect } from '../rect'; import { ControlHandle } from './control_handle'; import { ICursor } from '../../cursor_manager'; -import { normalizeDegree, rad2Deg } from '@suika/geo'; +import { IRectWithRotation, normalizeDegree, rad2Deg } from '@suika/geo'; import { ITransformHandleType } from './type'; -const getResizeCursor = (type: string, rotation: number): ICursor => { +const getResizeCursor = ( + type: string, + rotation: number, + selectedBox: IRectWithRotation, +): ICursor => { + if (selectedBox.height === 0) { + // be considered as a line + return 'move'; + } + let dDegree = 0; switch (type) { case 'se': diff --git a/packages/suika/src/editor/scene/graph.ts b/packages/suika/src/editor/scene/graph.ts index b5ee9166..7bfafe49 100644 --- a/packages/suika/src/editor/scene/graph.ts +++ b/packages/suika/src/editor/scene/graph.ts @@ -4,16 +4,17 @@ import { calcCoverScale, genId, objectNameGenerator } from '../../utils/common'; import { IRectWithRotation, isPointInRect, isRectIntersect } from '@suika/geo'; import { getAbsoluteCoords, - getElementRotatedXY, + getRectRotatedXY, getRectCenterPoint, -} from '../../utils/graphics'; -import { normalizeRect, normalizeRadian, isRectContain } from '@suika/geo'; +} from '../../utils/geo'; +import { normalizeRadian, isRectContain } from '@suika/geo'; import { transformRotate } from '@suika/geo'; import { DEFAULT_IMAGE, ITexture, TextureImage } from '../texture'; import { ImgManager } from '../Img_manager'; import { HALF_PI } from '../../constant'; import { drawRoundRectPath, rotateInCanvas } from '../../utils/canvas'; import { ControlHandle } from './control_handle_manager'; +import { getResizedLine } from './utils'; export interface GraphAttrs { type?: GraphType; @@ -310,11 +311,11 @@ export class Graph { } setRotatedX(rotatedX: number) { - const { x: prevRotatedX } = getElementRotatedXY(this); + const { x: prevRotatedX } = getRectRotatedXY(this); this.x = this.x + rotatedX - prevRotatedX; } setRotatedY(rotatedY: number) { - const { y: prevRotatedY } = getElementRotatedXY(this); + const { y: prevRotatedY } = getRectRotatedXY(this); this.y = this.y + rotatedY - prevRotatedY; } @@ -322,17 +323,14 @@ export class Graph { type: string, // 'se' | 'ne' | 'nw' | 'sw' | 'n' | 'e' | 's' | 'w', newPos: IPoint, oldBox: IBox2WithRotation, - keepRatio = false, - scaleFromCenter = false, + isShiftPressing = false, + isAltPressing = false, ) { - const rect = getResizedRect( - type, - newPos, - oldBox, - keepRatio, - scaleFromCenter, - ); - this.setAttrs(normalizeRect(rect)); + const rect = + this.height === 0 + ? getResizedLine(type, newPos, oldBox, isShiftPressing, isAltPressing) + : getResizedRect(type, newPos, oldBox, isShiftPressing, isAltPressing); + this.setAttrs(rect); } draw( // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/packages/suika/src/editor/scene/utils.ts b/packages/suika/src/editor/scene/utils.ts new file mode 100644 index 00000000..b7edc7b4 --- /dev/null +++ b/packages/suika/src/editor/scene/utils.ts @@ -0,0 +1,107 @@ +import { IPoint, IRect, normalizeRadian, transformRotate } from '@suika/geo'; +import { + adjustSizeToKeepPolarSnap, + getRectCenterPoint, + getRectRotatedXY, +} from '../../utils/geo'; +import { IBox2WithRotation } from '../../type'; + +/** + * Get the new position of the line when resizing + * we consider the graph with 0 height as a line + * + * TODO: reuse, this is something same code in tool_draw_graph.ts + */ +export const getResizedLine = ( + /** control type, 'se' | 'ne' | 'nw' | 'sw' */ + type: string, + newPos: IPoint, + oldBox: IBox2WithRotation, + /** keep rotation in 0 45 90 ... */ + keepPolarSnap: boolean, + scaleFromCenter: boolean, +) => { + const isControlInLeft = type === 'nw' || type === 'sw'; + + const { x, y } = newPos; + let startX: number; + let startY: number; + if (isControlInLeft) { + const [cx, cy] = getRectCenterPoint(oldBox); + const rightTop = transformRotate( + oldBox.x + oldBox.width, + oldBox.y + oldBox.height, + oldBox.rotation || 0, + cx, + cy, + ); + startX = rightTop.x; + startY = rightTop.y; + } else { + const leftTop = getRectRotatedXY(oldBox); + startX = leftTop.x; + startY = leftTop.y; + } + + const width = x - startX; + const height = y - startY; + + let rect = { + x: startX, + y: startY, + width, // width may be negative + height, // also height + }; + + let cx = 0; + let cy = 0; + if (scaleFromCenter) { + const [oldCx, oldCy] = getRectCenterPoint(oldBox); + const w = x - oldCx; + const h = y - oldCy; + rect = { + x: oldCx - w, + y: oldCy - h, + width: w * 2, + height: h * 2, + }; + + cx = rect.x + rect.width / 2; + cy = rect.y + rect.height / 2; + } + + if (keepPolarSnap) { + rect = adjustSizeToKeepPolarSnap(rect); + } + + if (scaleFromCenter) { + rect.x = cx - rect.width / 2; + rect.y = cy - rect.height / 2; + } + + if (isControlInLeft) { + rect.width = -rect.width; + rect.height = -rect.height; + } + const attrs = getLineAttrsByRect(rect); + if (isControlInLeft) { + attrs.x -= rect.width; + attrs.y -= rect.height; + } + + return attrs; +}; + +const getLineAttrsByRect = ({ x, y, width, height }: IRect) => { + const rotation = normalizeRadian(Math.atan2(height, width)); + const cx = x + width / 2; + const cy = y + height / 2; + const p = transformRotate(x, y, -rotation, cx, cy); + width = Math.sqrt(width * width + height * height); + return { + x: p.x, + y: p.y, + width, + rotation, + }; +}; diff --git a/packages/suika/src/editor/selected_box.ts b/packages/suika/src/editor/selected_box.ts index 07e86f5a..8f9e5aa7 100644 --- a/packages/suika/src/editor/selected_box.ts +++ b/packages/suika/src/editor/selected_box.ts @@ -1,6 +1,6 @@ import { IPoint, IRectWithRotation, isPointInRect } from '@suika/geo'; import { Editor } from './editor'; -import { getRectCenterPoint } from '../utils/graphics'; +import { getRectCenterPoint } from '../utils/geo'; import { rotateInCanvas } from '../utils/canvas'; import EventEmitter from '../utils/event_emitter'; diff --git a/packages/suika/src/editor/selected_elements.ts b/packages/suika/src/editor/selected_elements.ts index c59ec936..bdc21b9d 100644 --- a/packages/suika/src/editor/selected_elements.ts +++ b/packages/suika/src/editor/selected_elements.ts @@ -2,7 +2,7 @@ import { Graph } from './scene/graph'; import { IBox } from '../type'; import { isSameArray } from '../utils/common'; import EventEmitter from '../utils/event_emitter'; -import { getRectCenterPoint } from '../utils/graphics'; +import { getRectCenterPoint } from '../utils/geo'; import { getMergedRect } from '@suika/geo'; import { RemoveElement } from './commands/remove_element'; import { Editor } from './editor'; diff --git a/packages/suika/src/editor/service/mutate_graphs_and_record.ts b/packages/suika/src/editor/service/mutate_graphs_and_record.ts index 859b0b3c..93410dbe 100644 --- a/packages/suika/src/editor/service/mutate_graphs_and_record.ts +++ b/packages/suika/src/editor/service/mutate_graphs_and_record.ts @@ -1,4 +1,4 @@ -import { getElementRotatedXY } from '../../utils/graphics'; +import { getRectRotatedXY } from '../../utils/geo'; import { SetElementsAttrs } from '../commands/set_elements_attrs'; import { Editor } from '../editor'; import { Graph } from '../scene/graph'; @@ -57,9 +57,9 @@ export const MutateGraphsAndRecord = { width: el.width, })); elements.forEach((el) => { - const { x: preRotatedX, y: preRotatedY } = getElementRotatedXY(el); + const { x: preRotatedX, y: preRotatedY } = getRectRotatedXY(el); el.width = width; - const { x: rotatedX, y: rotatedY } = getElementRotatedXY(el); + const { x: rotatedX, y: rotatedY } = getRectRotatedXY(el); const dx = rotatedX - preRotatedX; const dy = rotatedY - preRotatedY; el.x -= dx; @@ -85,9 +85,9 @@ export const MutateGraphsAndRecord = { height: el.height, })); elements.forEach((el) => { - const { x: preRotatedX, y: preRotatedY } = getElementRotatedXY(el); + const { x: preRotatedX, y: preRotatedY } = getRectRotatedXY(el); el.height = height; - const { x: rotatedX, y: rotatedY } = getElementRotatedXY(el); + const { x: rotatedX, y: rotatedY } = getRectRotatedXY(el); const dx = rotatedX - preRotatedX; const dy = rotatedY - preRotatedY; el.x -= dx; diff --git a/packages/suika/src/editor/tools/tool_draw_graph.ts b/packages/suika/src/editor/tools/tool_draw_graph.ts index e29d0518..c29f527f 100644 --- a/packages/suika/src/editor/tools/tool_draw_graph.ts +++ b/packages/suika/src/editor/tools/tool_draw_graph.ts @@ -126,6 +126,7 @@ export abstract class DrawGraphTool implements ITool { const size = Math.max(Math.abs(width), Math.abs(height)); rect.height = (Math.sign(height) || 1) * size; rect.width = (Math.sign(width) || 1) * size; + return rect; } /** @@ -190,7 +191,7 @@ export abstract class DrawGraphTool implements ITool { } if (keepSquare) { - this.adjustSizeWhenShiftPressing(rect); + rect = this.adjustSizeWhenShiftPressing(rect); } if (isStartPtAsCenter) { diff --git a/packages/suika/src/editor/tools/tool_draw_line.tsx b/packages/suika/src/editor/tools/tool_draw_line.tsx index 9c678084..7237c198 100644 --- a/packages/suika/src/editor/tools/tool_draw_line.tsx +++ b/packages/suika/src/editor/tools/tool_draw_line.tsx @@ -4,9 +4,8 @@ import { Line } from '../scene/line'; import { DrawGraphTool } from './tool_draw_graph'; import { ITool } from './type'; import { IRect } from '@suika/geo'; -import { calcVectorRadian } from '../../utils/graphics'; +import { adjustSizeToKeepPolarSnap } from '../../utils/geo'; import { transformRotate } from '@suika/geo'; -import { HALF_PI } from '../../constant'; import { normalizeRadian } from '@suika/geo'; export class DrawLineTool extends DrawGraphTool implements ITool { @@ -34,29 +33,7 @@ export class DrawLineTool extends DrawGraphTool implements ITool { } protected override adjustSizeWhenShiftPressing(rect: IRect) { - const radian = calcVectorRadian( - rect.x, - rect.y, - rect.x + rect.width, - rect.y + rect.height, - ); - - const { width, height } = rect; - const remainder = radian % HALF_PI; - if (remainder < Math.PI / 8 || remainder > (Math.PI * 3) / 8) { - if (Math.abs(width) > Math.abs(height)) { - rect.height = 0; - } else { - rect.width = 0; - } - } else { - const min = Math.min(Math.abs(width), Math.abs(height)); - const max = Math.max(Math.abs(width), Math.abs(height)); - const size = min + (max - min) / 2; - - rect.height = (Math.sign(height) || 1) * size; - rect.width = (Math.sign(width) || 1) * size; - } + return adjustSizeToKeepPolarSnap(rect); } protected override updateGraph(rect: IRect) { diff --git a/packages/suika/src/editor/tools/tool_select/tool_select_rotation.ts b/packages/suika/src/editor/tools/tool_select/tool_select_rotation.ts index f4bef27b..419ea4a1 100644 --- a/packages/suika/src/editor/tools/tool_select/tool_select_rotation.ts +++ b/packages/suika/src/editor/tools/tool_select/tool_select_rotation.ts @@ -1,6 +1,6 @@ import { IPoint } from '../../../type'; import { getClosestTimesVal } from '../../../utils/common'; -import { calcVectorRadian } from '../../../utils/graphics'; +import { calcVectorRadian } from '../../../utils/geo'; import { SetElementsAttrs } from '../../commands/set_elements_attrs'; import { Editor } from '../../editor'; import { IBaseTool } from '../type'; diff --git a/packages/suika/src/utils/graphics.ts b/packages/suika/src/utils/geo.ts similarity index 70% rename from packages/suika/src/utils/graphics.ts rename to packages/suika/src/utils/geo.ts index d791a622..703fa55d 100644 --- a/packages/suika/src/utils/graphics.ts +++ b/packages/suika/src/utils/geo.ts @@ -1,6 +1,6 @@ -import { DOUBLE_PI } from '../constant'; +import { DOUBLE_PI, HALF_PI } from '../constant'; import { IBox, IBox2, IBox2WithMid, IPoint } from '../type'; -import { IRect } from '@suika/geo'; +import { IRect, IRectWithRotation } from '@suika/geo'; import { transformRotate } from '@suika/geo'; export function isRectIntersect2(rect1: IBox2, rect2: IBox2) { @@ -71,15 +71,9 @@ export function getAbsoluteCoords( /** * 计算一个形状左上角的坐标,考虑旋转 */ -export function getElementRotatedXY(element: { - x: number; - y: number; - width: number; - height: number; - rotation?: number; -}) { - const [cx, cy] = getRectCenterPoint(element); - return transformRotate(element.x, element.y, element.rotation || 0, cx, cy); +export function getRectRotatedXY(rect: IRectWithRotation) { + const [cx, cy] = getRectCenterPoint(rect); + return transformRotate(rect.x, rect.y, rect.rotation || 0, cx, cy); } export const bboxToBbox2 = (bbox: IBox): IBox2 => { @@ -122,3 +116,30 @@ export const pointsToHLines = (points: IPoint[]): Map => { } return map; }; + +export const adjustSizeToKeepPolarSnap = (rect: IRect): IRect => { + const radian = calcVectorRadian( + rect.x, + rect.y, + rect.x + rect.width, + rect.y + rect.height, + ); + + const { width, height } = rect; + const remainder = radian % HALF_PI; + if (remainder < Math.PI / 8 || remainder > (Math.PI * 3) / 8) { + if (Math.abs(width) > Math.abs(height)) { + rect.height = 0; + } else { + rect.width = 0; + } + } else { + const min = Math.min(Math.abs(width), Math.abs(height)); + const max = Math.max(Math.abs(width), Math.abs(height)); + const size = min + (max - min) / 2; + + rect.height = (Math.sign(height) || 1) * size; + rect.width = (Math.sign(width) || 1) * size; + } + return rect; +};