From d603b38fd53b7f314af72794ec9dbbdaa3a9f843 Mon Sep 17 00:00:00 2001 From: FateLee Date: Tue, 23 Jul 2024 19:14:57 +0800 Subject: [PATCH] feat: use a more accurate algorithm to obtain the selected element --- src/common/Point.ts | 24 +- src/core/coreCommands.ts | 6 +- src/core/cursor/cursorMoveOperations.ts | 7 +- src/math/sat.ts | 276 +++++++++++++++++++++ src/render/fill/image.ts | 4 +- src/workbench/workspace/fill/fillPanel.tsx | 1 - 6 files changed, 306 insertions(+), 12 deletions(-) create mode 100644 src/math/sat.ts diff --git a/src/common/Point.ts b/src/common/Point.ts index 79e425c..b25fea0 100644 --- a/src/common/Point.ts +++ b/src/common/Point.ts @@ -30,7 +30,27 @@ export class Point { export const subtract = (a: IPoint, b: IPoint) => new Point(a.x - b.x, a.y - b.y) -export const dotProduct = (a: IPoint, b: IPoint) => - new Point(a.x * b.x, a.y * b.y) +export const dot = (a: IPoint, b: IPoint) => new Point(a.x * b.x, a.y * b.y) export const add = (a: IPoint, b: IPoint) => new Point(a.x + b.x, a.y + b.y) export const divide = (a: IPoint, b: IPoint) => new Point(a.x / b.x, a.y / b.y) + +export const dotProduct = (a: IPoint, b: IPoint) => a.x * b.x + a.y * b.y + +export const crossProduct = (a: IPoint, b: IPoint) => a.x * b.y - b.x * a.y + +export const magnitude = (point: IPoint) => + point.x * point.x + point.y * point.y + +export const len = (point: IPoint) => Math.sqrt(magnitude(point)) + +export const edge = (point: IPoint) => new Point(-point.y, point.x) + +export const normal = (point: IPoint) => { + const p = new Point(0, 0) + const m = len(point) + if (m !== 0) { + p.x = point.x / m + p.y = point.y / m + } + return p +} diff --git a/src/core/coreCommands.ts b/src/core/coreCommands.ts index 158cb04..0eeec67 100644 --- a/src/core/coreCommands.ts +++ b/src/core/coreCommands.ts @@ -20,6 +20,7 @@ import { KeyCode, KeyMod } from 'Latte/common/keyCodes' import { calcPosition } from 'Latte/math/zIndex' import { CursorMoveOperations } from 'Latte/core/cursor/cursorMoveOperations' import { CursorUpdateOperations } from 'Latte/core/cursor/cursorUpdateOperations' +import { SAT } from 'Latte/math/sat' export const isLogicTarget = (node?: any): node is DisplayObject => node instanceof DisplayObject && @@ -166,9 +167,10 @@ export namespace CoreNavigationCommands { const { minX, minY, maxX, maxY } = selectBoxBounds const selectNode = rTreeRoot.search({ minX, minY, maxX, maxY }) const displayObjects = selectNode.map(item => item.displayObject) + const result = SAT.testCollision(selectBoxBounds, displayObjects) viewModel.discardActiveSelection() - if (displayObjects.length) { - displayObjects.forEach(viewModel.addSelectElement) + if (result.length) { + result.forEach(viewModel.addSelectElement) } } })() diff --git a/src/core/cursor/cursorMoveOperations.ts b/src/core/cursor/cursorMoveOperations.ts index 17e65d9..8a44fa1 100644 --- a/src/core/cursor/cursorMoveOperations.ts +++ b/src/core/cursor/cursorMoveOperations.ts @@ -2,7 +2,7 @@ import type { DisplayObject } from 'Latte/core/displayObject' import { Matrix } from 'Latte/math/matrix' import type { ISingleEditOperation } from 'Latte/core/modelChange' import { EditOperation } from 'Latte/core/modelChange' -import { Point, subtract, dotProduct, add, divide } from 'Latte/common/point' +import { Point, subtract, dot, add, divide } from 'Latte/common/point' import type { MouseControllerTarget, ActiveSelection, @@ -113,10 +113,7 @@ export class CursorMoveOperations { ) { const pointInSelectBox = subtract(point, selectTL) const pointInSelectBoxScale = divide(pointInSelectBox, selectBox) - const pointSizeOnNewSelectBox = dotProduct( - newSelectBox, - pointInSelectBoxScale - ) + const pointSizeOnNewSelectBox = dot(newSelectBox, pointInSelectBoxScale) return add(newSelectBoxTL, pointSizeOnNewSelectBox) } diff --git a/src/math/sat.ts b/src/math/sat.ts new file mode 100644 index 0000000..ba91b30 --- /dev/null +++ b/src/math/sat.ts @@ -0,0 +1,276 @@ +import { + Point, + dotProduct, + subtract, + magnitude, + add, + normal, +} from 'Latte/common/point' +import { EditorElementTypeKind } from 'Latte/constants' +import type { Bounds } from 'Latte/core/bounds' +import type DisplayObject from 'Latte/core/container' +import type Rect from 'Latte/elements/rect' +import type Ellipse from 'Latte/elements/ellipse' +import { Matrix } from 'Latte/math/matrix' + +const DEFAULT_SELECT_BOX_AXIS = [new Point(0, 1), new Point(1, 0)] + +const tmp = new Point(0, 0) + +function pointCircleCollision(point: IPoint, circle: IPoint, r: number) { + if (r === 0) return false + return magnitude(subtract(circle, point)) <= r * r +} + +type TrianglePoint = [IPoint, IPoint, IPoint] + +class TriangleCircleCollision { + private static _pointInTriangle( + point: IPoint, + triangle: [IPoint, IPoint, IPoint] + ) { + // compute vectors & dot products + const center = new Point() + center.copyFrom(point) + const t0 = triangle[0] + const t1 = triangle[1] + const t2 = triangle[2] + const v0 = subtract(t2, t0) + const v1 = subtract(t1, t0) + const v2 = subtract(center, t0) + const dot00 = dotProduct(v0, v0) + const dot01 = dotProduct(v0, v1) + const dot02 = dotProduct(v0, v2) + const dot11 = dotProduct(v1, v1) + const dot12 = dotProduct(v1, v2) + + // Compute barycentric coordinates + const b = dot00 * dot11 - dot01 * dot01 + const inv = b === 0 ? 0 : 1 / b + const u = (dot11 * dot02 - dot01 * dot12) * inv + const v = (dot00 * dot12 - dot01 * dot02) * inv + return u >= 0 && v >= 0 && u + v < 1 + } + + private static _lineCircleCollide( + a: IPoint, + b: IPoint, + center: IPoint, + radius: number, + nearest?: Point + ) { + // check to see if start or end points lie within circle + if (pointCircleCollision(a, center, radius)) { + if (nearest) { + nearest.copyFrom(a) + } + return true + } + if (pointCircleCollision(b, center, radius)) { + if (nearest) { + nearest.copyFrom(b) + } + return true + } + + // vector d + const d = subtract(b, a) + + // vector lc + const lc = subtract(center, a) + + // project lc onto d, resulting in vector p + const dLen2 = magnitude(d) // len2 of d + const p = new Point(0, 0) + if (dLen2 > 0) { + const dp = dotProduct(lc, d) / dLen2 + p.x = d.x * dp + p.y = d.y * dp + } + + if (!nearest) nearest = tmp + nearest = add(a, p) + + // len2 of p + const pLen2 = magnitude(p) + + // check collision + return ( + pointCircleCollision(nearest, center, radius) && + pLen2 <= dLen2 && + dotProduct(p, d) >= 0 + ) + } + private static _singleTriangleCircleCollision( + triangle: [IPoint, IPoint, IPoint], + circle: IPoint, + radius: number + ) { + if (this._pointInTriangle(circle, triangle)) return true + if (this._lineCircleCollide(triangle[0], triangle[1], circle, radius)) + return true + if (this._lineCircleCollide(triangle[1], triangle[2], circle, radius)) + return true + if (this._lineCircleCollide(triangle[2], triangle[0], circle, radius)) + return true + return false + } + public static collision( + triangles: TrianglePoint | TrianglePoint[], + circle: IPoint, + radius: number + ) { + const firstElement = triangles[0] + if (firstElement instanceof Array) { + return triangles.some(triangle => + this._singleTriangleCircleCollision(triangle, circle, radius) + ) + } + return this._singleTriangleCircleCollision( + triangles as TrianglePoint, + circle, + radius + ) + } +} + +class Projection { + constructor(public min: number, public max: number) {} + + overlaps(projection) { + return this.max > projection.min && projection.max > this.min + } +} + +const project = (axes: IPoint, axis: IPoint[]) => { + const scalars: number[] = [] + const v = new Point() + + axis.forEach(point => { + v.copyFrom(point) + scalars.push(dotProduct(v, axes)) + }) + return new Projection(Math.min(...scalars), Math.max(...scalars)) +} + +export class SAT { + public static test(a: Projection, b: Projection): boolean { + return a.overlaps(b) + } + + private static _getRectPointFromTopLeft(rect: Rect) { + const { width, height } = rect + return [ + new Point(0, 0), + new Point(width, 0), + new Point(width, height), + new Point(0, height), + ].map(item => Matrix.apply(item, rect.transform)) + } + + private static _getRectangleAxis(rect: Rect) { + const transformPoint = this._getRectPointFromTopLeft(rect) + const axesList: IPoint[] = [] + for (let i = 1, len = transformPoint.length; i < len; i++) { + const edge = subtract(transformPoint[i], transformPoint[i - 1]) + axesList.push(normal(edge)) + } + return axesList + } + + private static _getSelectBoxPoint(selectBox: Bounds) { + const { x, y, width, height } = selectBox.getRectangle() + return [ + new Point(x, y), + new Point(x + width, y), + new Point(x + width, y + height), + new Point(x, y + height), + ] + } + + private static _testRectangle(selectVector: IPoint[], rect: Rect) { + let projectionSelectBox: Projection + let projectionTestRect: Projection + const axes = this._getRectangleAxis(rect).concat(DEFAULT_SELECT_BOX_AXIS) + const rectVectors = this._getRectPointFromTopLeft(rect) + let result = true + axes.forEach(axis => { + projectionSelectBox = project(axis, selectVector) + projectionTestRect = project(axis, rectVectors) + if (!projectionSelectBox.overlaps(projectionTestRect)) { + result = false + } + }) + return result + } + + private static _transformEllipseToCircle(object: Ellipse) { + const { width, height, x, y } = object + const tempMatrix = new Matrix() + if (width > height) { + tempMatrix.a = height / width + } else { + tempMatrix.d = width / height + } + const centerOriginTL = Matrix.apply( + { x: width / 2, y: height / 2 }, + { + ...object.transform, + tx: 0, + ty: 0, + } + ) + centerOriginTL.x += x + centerOriginTL.y += y + const newCenter = Matrix.apply(centerOriginTL, tempMatrix) + Matrix.multiply(tempMatrix, tempMatrix, object.transform) + tempMatrix.b = 0 + tempMatrix.c = 0 + tempMatrix.tx = 0 + tempMatrix.ty = 0 + return { + currentMatrix: tempMatrix, + newCenter, + radius: Math.min(width, height) / 2, + } + } + + private static _testEllipse(selectVector: IPoint[], object: Ellipse) { + const { currentMatrix, newCenter, radius } = + this._transformEllipseToCircle(object) + console.log(currentMatrix, newCenter) + const [newTL, newTR, newBR, newBL] = selectVector.map(point => + Matrix.apply(point, currentMatrix) + ) + return TriangleCircleCollision.collision( + [ + [newTL, newTR, newBL], + [newBR, newTR, newBL], + ], + newCenter, + radius + ) + } + + public static testObject(box: Bounds, object: DisplayObject) { + const selectVector = this._getSelectBoxPoint(box) + let result = false + switch (object.type) { + case EditorElementTypeKind.ELLIPSE: + result = this._testEllipse(selectVector, object) + console.log(result) + break + case EditorElementTypeKind.RECTANGLE: + result = this._testRectangle(selectVector, object) + break + } + return result + } + + public static testCollision( + box: Bounds, + objects: DisplayObject[] + ): DisplayObject[] { + return objects.filter(object => this.testObject(box, object)) + } +} diff --git a/src/render/fill/image.ts b/src/render/fill/image.ts index 880487e..f6e2630 100644 --- a/src/render/fill/image.ts +++ b/src/render/fill/image.ts @@ -4,7 +4,7 @@ import type { FillRenderOptions, } from 'Latte/render/renderContributionRegistry' import { textureManager } from 'Latte/core/texture' -import { divide, dotProduct, subtract } from 'Latte/common/point' +import { divide, dot, subtract } from 'Latte/common/point' enum ImageFillScaleMode { FILL = 'FILL', @@ -59,7 +59,7 @@ export class ImageFillRender } else { ratio.y = ratio.x } - const renderSize = dotProduct(ratio, imageSize) + const renderSize = dot(ratio, imageSize) const client = divide(subtract(boxSize, renderSize), { x: 2, y: 2, diff --git a/src/workbench/workspace/fill/fillPanel.tsx b/src/workbench/workspace/fill/fillPanel.tsx index a6ef199..a49f022 100644 --- a/src/workbench/workspace/fill/fillPanel.tsx +++ b/src/workbench/workspace/fill/fillPanel.tsx @@ -88,7 +88,6 @@ export const FillPanel = (props: PropsWithChildren) => { const handleSelectionChange = useCallback((e: DisplayObject) => { const fills = e.getFills() - console.log(fills) setFills(fills) }, [])