Adds code editing and shape generation
This commit is contained in:
parent
afa8f53dff
commit
1a01c47835
34 changed files with 1298 additions and 237 deletions
|
@ -3,6 +3,7 @@ import styled from "styles"
|
|||
import inputs from "state/inputs"
|
||||
import { useRef } from "react"
|
||||
import { TransformCorner, TransformEdge } from "types"
|
||||
import { lerp } from "utils/utils"
|
||||
|
||||
export default function Bounds() {
|
||||
const zoom = useSelector((state) => state.data.camera.zoom)
|
||||
|
@ -11,7 +12,7 @@ export default function Bounds() {
|
|||
|
||||
if (!bounds) return null
|
||||
|
||||
const { minX, minY, maxX, maxY, width, height } = bounds
|
||||
let { minX, minY, maxX, maxY, width, height } = bounds
|
||||
|
||||
const p = 4 / zoom
|
||||
const cp = p * 2
|
||||
|
|
|
@ -8,6 +8,7 @@ function Shape({ id }: { id: string }) {
|
|||
const rGroup = useRef<SVGGElement>(null)
|
||||
|
||||
const isHovered = useSelector((state) => state.data.hoveredId === id)
|
||||
|
||||
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
|
||||
|
||||
const shape = useSelector(
|
||||
|
@ -87,6 +88,7 @@ const HoverIndicator = styled("path", {
|
|||
pointerEvents: "all",
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
transform: "all .2s",
|
||||
})
|
||||
|
||||
const StyledGroup = styled("g", {
|
||||
|
@ -103,14 +105,45 @@ const StyledGroup = styled("g", {
|
|||
false: {},
|
||||
},
|
||||
isHovered: {
|
||||
true: {
|
||||
true: {},
|
||||
false: {},
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
isSelected: true,
|
||||
isHovered: true,
|
||||
css: {
|
||||
[`& ${HoverIndicator}`]: {
|
||||
opacity: "1",
|
||||
stroke: "$hint",
|
||||
zStrokeWidth: [8, 4],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
isSelected: true,
|
||||
isHovered: false,
|
||||
css: {
|
||||
[`& ${HoverIndicator}`]: {
|
||||
opacity: "1",
|
||||
stroke: "$hint",
|
||||
zStrokeWidth: [6, 3],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
isSelected: false,
|
||||
isHovered: true,
|
||||
css: {
|
||||
[`& ${HoverIndicator}`]: {
|
||||
opacity: "1",
|
||||
stroke: "$hint",
|
||||
zStrokeWidth: [8, 4],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export { Indicator, HoverIndicator }
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
// This is the code library.
|
||||
|
||||
export default `
|
||||
new Circle({
|
||||
point: [200, 200],
|
||||
})
|
||||
|
||||
// Hello world
|
||||
const name = "steve"
|
||||
const age = 93
|
||||
|
||||
new Rectangle({
|
||||
point: [400, 300],
|
||||
})
|
||||
`
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
import React, { useEffect, useRef } from "react"
|
||||
import state, { useSelector } from "state"
|
||||
import { motion } from "framer-motion"
|
||||
import { CodeFile } from "types"
|
||||
import styled from "styles"
|
||||
import { useStateDesigner } from "@state-designer/react"
|
||||
import React, { useEffect, useRef } from "react"
|
||||
import { motion } from "framer-motion"
|
||||
import state, { useSelector } from "state"
|
||||
import { CodeFile } from "types"
|
||||
import CodeDocs from "./code-docs"
|
||||
import CodeEditor from "./code-editor"
|
||||
import { getShapesFromCode } from "lib/code/generate"
|
||||
import {
|
||||
X,
|
||||
Code,
|
||||
|
@ -14,9 +16,6 @@ import {
|
|||
ChevronUp,
|
||||
ChevronDown,
|
||||
} from "react-feather"
|
||||
import styled from "styles"
|
||||
|
||||
// import evalCode from "lib/code"
|
||||
|
||||
const getErrorLineAndColumn = (e: any) => {
|
||||
if ("line" in e) {
|
||||
|
@ -79,12 +78,13 @@ export default function CodePanel() {
|
|||
runCode(data) {
|
||||
let error = null
|
||||
|
||||
// try {
|
||||
// const { nodes, globs } = evalCode(data.code)
|
||||
// state.send("GENERATED_ITEMS", { nodes, globs })
|
||||
// } catch (e) {
|
||||
// error = { message: e.message, ...getErrorLineAndColumn(e) }
|
||||
// }
|
||||
try {
|
||||
const shapes = getShapesFromCode(data.code)
|
||||
state.send("GENERATED_SHAPES_FROM_CODE", { shapes })
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
error = { message: e.message, ...getErrorLineAndColumn(e) }
|
||||
}
|
||||
|
||||
data.error = error
|
||||
},
|
||||
|
@ -182,7 +182,7 @@ const PanelContainer = styled(motion.div, {
|
|||
position: "absolute",
|
||||
top: "8px",
|
||||
right: "8px",
|
||||
bottom: "8px",
|
||||
bottom: "48px",
|
||||
backgroundColor: "$panel",
|
||||
borderRadius: "4px",
|
||||
overflow: "hidden",
|
||||
|
@ -198,9 +198,7 @@ const PanelContainer = styled(motion.div, {
|
|||
variants: {
|
||||
isCollapsed: {
|
||||
true: {},
|
||||
false: {
|
||||
height: "400px",
|
||||
},
|
||||
false: {},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
@ -240,11 +238,11 @@ const Content = styled("div", {
|
|||
display: "grid",
|
||||
gridTemplateColumns: "1fr",
|
||||
gridTemplateRows: "auto 1fr 28px",
|
||||
minWidth: "100%",
|
||||
height: "100%",
|
||||
width: 560,
|
||||
minWidth: "100%",
|
||||
maxWidth: 560,
|
||||
overflow: "hidden",
|
||||
height: "100%",
|
||||
userSelect: "none",
|
||||
pointerEvents: "all",
|
||||
})
|
||||
|
|
|
@ -1,47 +1,8 @@
|
|||
export default `// Basic nodes and globs
|
||||
export default `new Circle({
|
||||
point: [200, 200],
|
||||
})
|
||||
|
||||
const nodeA = new Node({
|
||||
x: -100,
|
||||
y: 0,
|
||||
});
|
||||
|
||||
const nodeB = new Node({
|
||||
x: 100,
|
||||
y: 0,
|
||||
});
|
||||
|
||||
const glob = new Glob({
|
||||
start: nodeA,
|
||||
end: nodeB,
|
||||
D: { x: 0, y: 60 },
|
||||
Dp: { x: 0, y: 90 },
|
||||
});
|
||||
|
||||
// Something more interesting...
|
||||
|
||||
const PI2 = Math.PI * 2,
|
||||
center = { x: 0, y: 0 },
|
||||
radius = 400;
|
||||
|
||||
let prev;
|
||||
|
||||
for (let i = 0; i < 21; i++) {
|
||||
const t = i * (PI2 / 20);
|
||||
|
||||
const node = new Node({
|
||||
x: center.x + radius * Math.sin(t),
|
||||
y: center.y + radius * Math.cos(t),
|
||||
});
|
||||
|
||||
if (prev !== undefined) {
|
||||
new Glob({
|
||||
start: prev,
|
||||
end: node,
|
||||
D: center,
|
||||
Dp: center,
|
||||
});
|
||||
}
|
||||
|
||||
prev = node;
|
||||
}
|
||||
new Rectangle({
|
||||
point: [400, 300],
|
||||
})
|
||||
`
|
||||
|
|
|
@ -10,7 +10,7 @@ export default function Editor() {
|
|||
<>
|
||||
<Canvas />
|
||||
<StatusBar />
|
||||
{/* <CodePanel /> */}
|
||||
<CodePanel />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -7,13 +7,18 @@ export default class Circle extends CodeShape<CircleShape> {
|
|||
super({
|
||||
id: uuid(),
|
||||
type: ShapeType.Circle,
|
||||
isGenerated: true,
|
||||
name: "Circle",
|
||||
parentId: "page0",
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
rotation: 0,
|
||||
radius: 20,
|
||||
style: {},
|
||||
style: {
|
||||
fill: "#777",
|
||||
stroke: "#000",
|
||||
strokeWidth: 1,
|
||||
},
|
||||
...props,
|
||||
})
|
||||
}
|
||||
|
|
24
lib/code/dot.ts
Normal file
24
lib/code/dot.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import CodeShape from "./index"
|
||||
import { v4 as uuid } from "uuid"
|
||||
import { DotShape, ShapeType } from "types"
|
||||
|
||||
export default class Dot extends CodeShape<DotShape> {
|
||||
constructor(props = {} as Partial<DotShape>) {
|
||||
super({
|
||||
id: uuid(),
|
||||
type: ShapeType.Dot,
|
||||
isGenerated: false,
|
||||
name: "Dot",
|
||||
parentId: "page0",
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
rotation: 0,
|
||||
style: {
|
||||
fill: "#777",
|
||||
stroke: "#000",
|
||||
strokeWidth: 1,
|
||||
},
|
||||
...props,
|
||||
})
|
||||
}
|
||||
}
|
30
lib/code/ellipse.ts
Normal file
30
lib/code/ellipse.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import CodeShape from "./index"
|
||||
import { v4 as uuid } from "uuid"
|
||||
import { EllipseShape, ShapeType } from "types"
|
||||
|
||||
export default class Ellipse extends CodeShape<EllipseShape> {
|
||||
constructor(props = {} as Partial<EllipseShape>) {
|
||||
super({
|
||||
id: uuid(),
|
||||
type: ShapeType.Ellipse,
|
||||
isGenerated: false,
|
||||
name: "Ellipse",
|
||||
parentId: "page0",
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
radiusX: 20,
|
||||
radiusY: 20,
|
||||
rotation: 0,
|
||||
style: {
|
||||
fill: "#777",
|
||||
stroke: "#000",
|
||||
strokeWidth: 1,
|
||||
},
|
||||
...props,
|
||||
})
|
||||
}
|
||||
|
||||
get radius() {
|
||||
return this.shape.radius
|
||||
}
|
||||
}
|
29
lib/code/generate.ts
Normal file
29
lib/code/generate.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import Rectangle from "./rectangle"
|
||||
import Circle from "./circle"
|
||||
import Ellipse from "./ellipse"
|
||||
import Polyline from "./polyline"
|
||||
import Dot from "./dot"
|
||||
import Line from "./line"
|
||||
import Vector from "./vector"
|
||||
import Utils from "./utils"
|
||||
import { codeShapes } from "./index"
|
||||
|
||||
const scope = { Dot, Circle, Ellipse, Line, Polyline, Rectangle, Vector, Utils }
|
||||
|
||||
/**
|
||||
* Evaluate code, collecting generated shapes in the shape set. Return the
|
||||
* collected shapes as an array.
|
||||
* @param code
|
||||
*/
|
||||
export function getShapesFromCode(code: string) {
|
||||
codeShapes.clear()
|
||||
|
||||
new Function(...Object.keys(scope), `${code}`)(...Object.values(scope))
|
||||
|
||||
const generatedShapes = Array.from(codeShapes.values()).map((instance) => {
|
||||
instance.shape.isGenerated = true
|
||||
return instance.shape
|
||||
})
|
||||
|
||||
return generatedShapes
|
||||
}
|
|
@ -2,16 +2,22 @@ import { Shape } from "types"
|
|||
import * as vec from "utils/vec"
|
||||
import { getShapeUtils } from "lib/shapes"
|
||||
|
||||
export const codeShapes = new Set<CodeShape<Shape>>([])
|
||||
|
||||
/**
|
||||
* A base class for code shapes. Note that creating a shape adds it to the
|
||||
* shape map, while deleting it removes it from the collected shapes set
|
||||
*/
|
||||
export default class CodeShape<T extends Shape> {
|
||||
private _shape: T
|
||||
|
||||
constructor(props: T) {
|
||||
this._shape = props
|
||||
shapeMap.add(this)
|
||||
codeShapes.add(this)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
shapeMap.delete(this)
|
||||
codeShapes.delete(this)
|
||||
}
|
||||
|
||||
moveTo(point: number[]) {
|
||||
|
@ -50,5 +56,3 @@ export default class CodeShape<T extends Shape> {
|
|||
return this.shape.rotation
|
||||
}
|
||||
}
|
||||
|
||||
export const shapeMap = new Set<CodeShape<Shape>>([])
|
||||
|
|
25
lib/code/line.ts
Normal file
25
lib/code/line.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import CodeShape from "./index"
|
||||
import { v4 as uuid } from "uuid"
|
||||
import { LineShape, ShapeType } from "types"
|
||||
|
||||
export default class Line extends CodeShape<LineShape> {
|
||||
constructor(props = {} as Partial<LineShape>) {
|
||||
super({
|
||||
id: uuid(),
|
||||
type: ShapeType.Line,
|
||||
isGenerated: false,
|
||||
name: "Line",
|
||||
parentId: "page0",
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
direction: [0, 0],
|
||||
rotation: 0,
|
||||
style: {
|
||||
fill: "#777",
|
||||
stroke: "#000",
|
||||
strokeWidth: 1,
|
||||
},
|
||||
...props,
|
||||
})
|
||||
}
|
||||
}
|
25
lib/code/polyline.ts
Normal file
25
lib/code/polyline.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import CodeShape from "./index"
|
||||
import { v4 as uuid } from "uuid"
|
||||
import { PolylineShape, ShapeType } from "types"
|
||||
|
||||
export default class Polyline extends CodeShape<PolylineShape> {
|
||||
constructor(props = {} as Partial<PolylineShape>) {
|
||||
super({
|
||||
id: uuid(),
|
||||
type: ShapeType.Polyline,
|
||||
isGenerated: false,
|
||||
name: "Polyline",
|
||||
parentId: "page0",
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
points: [[0, 0]],
|
||||
rotation: 0,
|
||||
style: {
|
||||
fill: "none",
|
||||
stroke: "#000",
|
||||
strokeWidth: 1,
|
||||
},
|
||||
...props,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -7,13 +7,18 @@ export default class Rectangle extends CodeShape<RectangleShape> {
|
|||
super({
|
||||
id: uuid(),
|
||||
type: ShapeType.Rectangle,
|
||||
isGenerated: true,
|
||||
name: "Rectangle",
|
||||
parentId: "page0",
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
size: [1, 1],
|
||||
size: [100, 100],
|
||||
rotation: 0,
|
||||
style: {},
|
||||
style: {
|
||||
fill: "#777",
|
||||
stroke: "#000",
|
||||
strokeWidth: 1,
|
||||
},
|
||||
...props,
|
||||
})
|
||||
}
|
||||
|
|
151
lib/code/utils.ts
Normal file
151
lib/code/utils.ts
Normal file
|
@ -0,0 +1,151 @@
|
|||
import { Bounds } from "types"
|
||||
import Vector, { Point } from "./vector"
|
||||
|
||||
export default class Utils {
|
||||
static getRayRayIntersection(p0: Vector, n0: Vector, p1: Vector, n1: Vector) {
|
||||
const p0e = Vector.add(p0, n0),
|
||||
p1e = Vector.add(p1, n1),
|
||||
m0 = (p0e.y - p0.y) / (p0e.x - p0.x),
|
||||
m1 = (p1e.y - p1.y) / (p1e.x - p1.x),
|
||||
b0 = p0.y - m0 * p0.x,
|
||||
b1 = p1.y - m1 * p1.x,
|
||||
x = (b1 - b0) / (m0 - m1),
|
||||
y = m0 * x + b0
|
||||
|
||||
return new Vector({ x, y })
|
||||
}
|
||||
|
||||
static getCircleTangentToPoint(
|
||||
A: Point | Vector,
|
||||
r0: number,
|
||||
P: Point | Vector,
|
||||
side: number
|
||||
) {
|
||||
const v0 = Vector.cast(A)
|
||||
const v1 = Vector.cast(P)
|
||||
const B = Vector.lrp(v0, v1, 0.5),
|
||||
r1 = Vector.dist(v0, B),
|
||||
delta = Vector.sub(B, v0),
|
||||
d = Vector.len(delta)
|
||||
|
||||
if (!(d <= r0 + r1 && d >= Math.abs(r0 - r1))) {
|
||||
return
|
||||
}
|
||||
|
||||
const a = (r0 * r0 - r1 * r1 + d * d) / (2.0 * d),
|
||||
n = 1 / d,
|
||||
p = Vector.add(v0, Vector.mul(delta, a * n)),
|
||||
h = Math.sqrt(r0 * r0 - a * a),
|
||||
k = Vector.mul(Vector.per(delta), h * n)
|
||||
|
||||
return side === 0 ? p.add(k) : p.sub(k)
|
||||
}
|
||||
|
||||
static shortAngleDist(a: number, b: number) {
|
||||
const max = Math.PI * 2
|
||||
const da = (b - a) % max
|
||||
return ((2 * da) % max) - da
|
||||
}
|
||||
|
||||
static getSweep(C: Vector, A: Vector, B: Vector) {
|
||||
return Utils.shortAngleDist(Vector.ang(C, A), Vector.ang(C, B))
|
||||
}
|
||||
|
||||
static bez1d(a: number, b: number, c: number, d: number, t: number) {
|
||||
return (
|
||||
a * (1 - t) * (1 - t) * (1 - t) +
|
||||
3 * b * t * (1 - t) * (1 - t) +
|
||||
3 * c * t * t * (1 - t) +
|
||||
d * t * t * t
|
||||
)
|
||||
}
|
||||
|
||||
static getCubicBezierBounds(
|
||||
p0: Point | Vector,
|
||||
c0: Point | Vector,
|
||||
c1: Point | Vector,
|
||||
p1: Point | Vector
|
||||
): Bounds {
|
||||
// solve for x
|
||||
let a = 3 * p1[0] - 9 * c1[0] + 9 * c0[0] - 3 * p0[0]
|
||||
let b = 6 * p0[0] - 12 * c0[0] + 6 * c1[0]
|
||||
let c = 3 * c0[0] - 3 * p0[0]
|
||||
let disc = b * b - 4 * a * c
|
||||
let xl = p0[0]
|
||||
let xh = p0[0]
|
||||
|
||||
if (p1[0] < xl) xl = p1[0]
|
||||
if (p1[0] > xh) xh = p1[0]
|
||||
|
||||
if (disc >= 0) {
|
||||
const t1 = (-b + Math.sqrt(disc)) / (2 * a)
|
||||
if (t1 > 0 && t1 < 1) {
|
||||
const x1 = Utils.bez1d(p0[0], c0[0], c1[0], p1[0], t1)
|
||||
if (x1 < xl) xl = x1
|
||||
if (x1 > xh) xh = x1
|
||||
}
|
||||
const t2 = (-b - Math.sqrt(disc)) / (2 * a)
|
||||
if (t2 > 0 && t2 < 1) {
|
||||
const x2 = Utils.bez1d(p0[0], c0[0], c1[0], p1[0], t2)
|
||||
if (x2 < xl) xl = x2
|
||||
if (x2 > xh) xh = x2
|
||||
}
|
||||
}
|
||||
|
||||
// Solve for y
|
||||
a = 3 * p1[1] - 9 * c1[1] + 9 * c0[1] - 3 * p0[1]
|
||||
b = 6 * p0[1] - 12 * c0[1] + 6 * c1[1]
|
||||
c = 3 * c0[1] - 3 * p0[1]
|
||||
disc = b * b - 4 * a * c
|
||||
let yl = p0[1]
|
||||
let yh = p0[1]
|
||||
if (p1[1] < yl) yl = p1[1]
|
||||
if (p1[1] > yh) yh = p1[1]
|
||||
if (disc >= 0) {
|
||||
const t1 = (-b + Math.sqrt(disc)) / (2 * a)
|
||||
if (t1 > 0 && t1 < 1) {
|
||||
const y1 = Utils.bez1d(p0[1], c0[1], c1[1], p1[1], t1)
|
||||
if (y1 < yl) yl = y1
|
||||
if (y1 > yh) yh = y1
|
||||
}
|
||||
const t2 = (-b - Math.sqrt(disc)) / (2 * a)
|
||||
if (t2 > 0 && t2 < 1) {
|
||||
const y2 = Utils.bez1d(p0[1], c0[1], c1[1], p1[1], t2)
|
||||
if (y2 < yl) yl = y2
|
||||
if (y2 > yh) yh = y2
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
minX: xl,
|
||||
minY: yl,
|
||||
maxX: xh,
|
||||
maxY: yh,
|
||||
width: Math.abs(xl - xh),
|
||||
height: Math.abs(yl - yh),
|
||||
}
|
||||
}
|
||||
|
||||
static getExpandedBounds(a: Bounds, b: Bounds) {
|
||||
const minX = Math.min(a.minX, b.minX),
|
||||
minY = Math.min(a.minY, b.minY),
|
||||
maxX = Math.max(a.maxX, b.maxX),
|
||||
maxY = Math.max(a.maxY, b.maxY),
|
||||
width = Math.abs(maxX - minX),
|
||||
height = Math.abs(maxY - minY)
|
||||
|
||||
return { minX, minY, maxX, maxY, width, height }
|
||||
}
|
||||
|
||||
static getCommonBounds(...b: Bounds[]) {
|
||||
if (b.length < 2) return b[0]
|
||||
|
||||
let bounds = b[0]
|
||||
|
||||
for (let i = 1; i < b.length; i++) {
|
||||
bounds = Utils.getExpandedBounds(bounds, b[i])
|
||||
}
|
||||
|
||||
return bounds
|
||||
}
|
||||
}
|
476
lib/code/vector.ts
Normal file
476
lib/code/vector.ts
Normal file
|
@ -0,0 +1,476 @@
|
|||
export interface VectorOptions {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export interface Point {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export default class Vector {
|
||||
x = 0
|
||||
y = 0
|
||||
|
||||
constructor(x: number, y: number)
|
||||
constructor(vector: Vector, b?: undefined)
|
||||
constructor(options: Point, b?: undefined)
|
||||
constructor(a: VectorOptions | Vector | number, b?: number) {
|
||||
if (typeof a === "number") {
|
||||
this.x = a
|
||||
this.y = b
|
||||
} else {
|
||||
const { x = 0, y = 0 } = a
|
||||
this.x = x
|
||||
this.y = y
|
||||
}
|
||||
}
|
||||
|
||||
set(v: Vector | Point) {
|
||||
this.x = v.x
|
||||
this.y = v.y
|
||||
}
|
||||
|
||||
copy() {
|
||||
return new Vector(this)
|
||||
}
|
||||
|
||||
clone() {
|
||||
return this.copy()
|
||||
}
|
||||
|
||||
toArray() {
|
||||
return [this.x, this.y]
|
||||
}
|
||||
|
||||
add(b: Vector) {
|
||||
this.x += b.x
|
||||
this.y += b.y
|
||||
return this
|
||||
}
|
||||
|
||||
static add(a: Vector, b: Vector) {
|
||||
const n = new Vector(a)
|
||||
n.x += b.x
|
||||
n.y += b.y
|
||||
return n
|
||||
}
|
||||
|
||||
sub(b: Vector) {
|
||||
this.x -= b.x
|
||||
this.y -= b.y
|
||||
return this
|
||||
}
|
||||
|
||||
static sub(a: Vector, b: Vector) {
|
||||
const n = new Vector(a)
|
||||
n.x -= b.x
|
||||
n.y -= b.y
|
||||
return n
|
||||
}
|
||||
|
||||
mul(b: number): Vector
|
||||
mul(b: Vector): Vector
|
||||
mul(b: Vector | number) {
|
||||
if (b instanceof Vector) {
|
||||
this.x *= b.x
|
||||
this.y *= b.y
|
||||
} else {
|
||||
this.x *= b
|
||||
this.y *= b
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
mulScalar(b: number) {
|
||||
return this.mul(b)
|
||||
}
|
||||
|
||||
static mulScalar(a: Vector, b: number) {
|
||||
return Vector.mul(a, b)
|
||||
}
|
||||
|
||||
static mul(a: Vector, b: number): Vector
|
||||
static mul(a: Vector, b: Vector): Vector
|
||||
static mul(a: Vector, b: Vector | number) {
|
||||
const n = new Vector(a)
|
||||
if (b instanceof Vector) {
|
||||
n.x *= b.x
|
||||
n.y *= b.y
|
||||
} else {
|
||||
n.x *= b
|
||||
n.y *= b
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
div(b: number): Vector
|
||||
div(b: Vector): Vector
|
||||
div(b: Vector | number) {
|
||||
if (b instanceof Vector) {
|
||||
if (b.x) {
|
||||
this.x /= b.x
|
||||
}
|
||||
if (b.y) {
|
||||
this.y /= b.y
|
||||
}
|
||||
} else {
|
||||
if (b) {
|
||||
this.x /= b
|
||||
this.y /= b
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
static div(a: Vector, b: number): Vector
|
||||
static div(a: Vector, b: Vector): Vector
|
||||
static div(a: Vector, b: Vector | number) {
|
||||
const n = new Vector(a)
|
||||
if (b instanceof Vector) {
|
||||
if (b.x) n.x /= b.x
|
||||
if (b.y) n.y /= b.y
|
||||
} else {
|
||||
if (b) {
|
||||
n.x /= b
|
||||
n.y /= b
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
divScalar(b: number) {
|
||||
return this.div(b)
|
||||
}
|
||||
|
||||
static divScalar(a: Vector, b: number) {
|
||||
return Vector.div(a, b)
|
||||
}
|
||||
|
||||
vec(b: Vector) {
|
||||
const { x, y } = this
|
||||
this.x = b.x - x
|
||||
this.y = b.y - y
|
||||
return this
|
||||
}
|
||||
|
||||
static vec(a: Vector, b: Vector) {
|
||||
const n = new Vector(a)
|
||||
n.x = b.x - a.x
|
||||
n.y = b.y - a.y
|
||||
return n
|
||||
}
|
||||
|
||||
pry(b: Vector) {
|
||||
return this.dpr(b) / b.len()
|
||||
}
|
||||
|
||||
static pry(a: Vector, b: Vector) {
|
||||
return a.dpr(b) / b.len()
|
||||
}
|
||||
|
||||
dpr(b: Vector) {
|
||||
return this.x * b.x + this.y * b.y
|
||||
}
|
||||
|
||||
static dpr(a: Vector, b: Vector) {
|
||||
return a.x & (b.x + a.y * b.y)
|
||||
}
|
||||
|
||||
cpr(b: Vector) {
|
||||
return this.x * b.y - b.y * this.y
|
||||
}
|
||||
|
||||
static cpr(a: Vector, b: Vector) {
|
||||
return a.x * b.y - b.y * a.y
|
||||
}
|
||||
|
||||
tangent(b: Vector) {
|
||||
return this.sub(b).uni()
|
||||
}
|
||||
|
||||
static tangent(a: Vector, b: Vector) {
|
||||
const n = new Vector(a)
|
||||
return n.sub(b).uni()
|
||||
}
|
||||
|
||||
dist2(b: Vector) {
|
||||
return this.sub(b).len2()
|
||||
}
|
||||
|
||||
static dist2(a: Vector, b: Vector) {
|
||||
const n = new Vector(a)
|
||||
return n.sub(b).len2()
|
||||
}
|
||||
|
||||
dist(b: Vector) {
|
||||
return Math.hypot(b.y - this.y, b.x - this.x)
|
||||
}
|
||||
|
||||
static dist(a: Vector, b: Vector) {
|
||||
const n = new Vector(a)
|
||||
return Math.hypot(b.y - n.y, b.x - n.x)
|
||||
}
|
||||
|
||||
ang(b: Vector) {
|
||||
return Math.atan2(b.y - this.y, b.x - this.x)
|
||||
}
|
||||
|
||||
static ang(a: Vector, b: Vector) {
|
||||
const n = new Vector(a)
|
||||
return Math.atan2(b.y - n.y, b.x - n.x)
|
||||
}
|
||||
|
||||
med(b: Vector) {
|
||||
return this.add(b).mul(0.5)
|
||||
}
|
||||
|
||||
static med(a: Vector, b: Vector) {
|
||||
const n = new Vector(a)
|
||||
return n.add(b).mul(0.5)
|
||||
}
|
||||
|
||||
rot(r: number) {
|
||||
const { x, y } = this
|
||||
this.x = x * Math.cos(r) - y * Math.sin(r)
|
||||
this.y = x * Math.sin(r) + y * Math.cos(r)
|
||||
return this
|
||||
}
|
||||
|
||||
static rot(a: Vector, r: number) {
|
||||
const n = new Vector(a)
|
||||
n.x = a.x * Math.cos(r) - a.y * Math.sin(r)
|
||||
n.y = a.x * Math.sin(r) + a.y * Math.cos(r)
|
||||
return n
|
||||
}
|
||||
|
||||
rotAround(b: Vector, r: number) {
|
||||
const { x, y } = this
|
||||
const s = Math.sin(r)
|
||||
const c = Math.cos(r)
|
||||
|
||||
const px = x - b.x
|
||||
const py = y - b.y
|
||||
|
||||
this.x = px * c - py * s + b.x
|
||||
this.y = px * s + py * c + b.y
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
static rotAround(a: Vector, b: Vector, r: number) {
|
||||
const n = new Vector(a)
|
||||
const s = Math.sin(r)
|
||||
const c = Math.cos(r)
|
||||
|
||||
const px = n.x - b.x
|
||||
const py = n.y - b.y
|
||||
|
||||
n.x = px * c - py * s + b.x
|
||||
n.y = px * s + py * c + b.y
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
lrp(b: Vector, t: number) {
|
||||
const n = new Vector(this)
|
||||
this.vec(b)
|
||||
.mul(t)
|
||||
.add(n)
|
||||
}
|
||||
|
||||
static lrp(a: Vector, b: Vector, t: number) {
|
||||
const n = new Vector(a)
|
||||
n.vec(b)
|
||||
.mul(t)
|
||||
.add(a)
|
||||
return n
|
||||
}
|
||||
|
||||
nudge(b: Vector, d: number) {
|
||||
this.add(b.mul(d))
|
||||
}
|
||||
|
||||
static nudge(a: Vector, b: Vector, d: number) {
|
||||
const n = new Vector(a)
|
||||
return n.add(b.mul(d))
|
||||
}
|
||||
|
||||
nudgeToward(b: Vector, d: number) {
|
||||
return this.nudge(Vector.vec(this, b).uni(), d)
|
||||
}
|
||||
|
||||
static nudgeToward(a: Vector, b: Vector, d: number) {
|
||||
return Vector.nudge(a, Vector.vec(a, b).uni(), d)
|
||||
}
|
||||
|
||||
int(b: Vector, from: number, to: number, s: number) {
|
||||
const t = (Math.max(from, to) - from) / (to - from)
|
||||
this.add(Vector.mul(this, 1 - t).add(Vector.mul(b, s)))
|
||||
return this
|
||||
}
|
||||
|
||||
static int(a: Vector, b: Vector, from: number, to: number, s: number) {
|
||||
const n = new Vector(a)
|
||||
const t = (Math.max(from, to) - from) / (to - from)
|
||||
n.add(Vector.mul(a, 1 - t).add(Vector.mul(b, s)))
|
||||
return n
|
||||
}
|
||||
|
||||
equals(b: Vector) {
|
||||
return this.x === b.x && this.y === b.y
|
||||
}
|
||||
|
||||
static equals(a: Vector, b: Vector) {
|
||||
return a.x === b.x && a.y === b.y
|
||||
}
|
||||
|
||||
abs() {
|
||||
this.x = Math.abs(this.x)
|
||||
this.y = Math.abs(this.y)
|
||||
return this
|
||||
}
|
||||
|
||||
static abs(a: Vector) {
|
||||
const n = new Vector(a)
|
||||
n.x = Math.abs(n.x)
|
||||
n.y = Math.abs(n.y)
|
||||
return n
|
||||
}
|
||||
|
||||
len() {
|
||||
return Math.hypot(this.x, this.y)
|
||||
}
|
||||
|
||||
static len(a: Vector) {
|
||||
return Math.hypot(a.x, a.y)
|
||||
}
|
||||
|
||||
len2() {
|
||||
return this.x * this.x + this.y * this.y
|
||||
}
|
||||
|
||||
static len2(a: Vector) {
|
||||
return a.x * a.x + a.y * a.y
|
||||
}
|
||||
|
||||
per() {
|
||||
const t = this.x
|
||||
this.x = this.y
|
||||
this.y = -t
|
||||
return this
|
||||
}
|
||||
|
||||
static per(a: Vector) {
|
||||
const n = new Vector(a)
|
||||
n.x = n.y
|
||||
n.y = -a.x
|
||||
return n
|
||||
}
|
||||
|
||||
neg() {
|
||||
this.x *= -1
|
||||
this.y *= -1
|
||||
return this
|
||||
}
|
||||
|
||||
static neg(v: Vector) {
|
||||
const n = new Vector(v)
|
||||
n.x *= -1
|
||||
n.y *= -1
|
||||
return n
|
||||
}
|
||||
|
||||
uni() {
|
||||
return this.div(this.len())
|
||||
}
|
||||
|
||||
static uni(v: Vector) {
|
||||
const n = new Vector(v)
|
||||
return n.div(n.len())
|
||||
}
|
||||
|
||||
isLeft(center: Vector, b: Vector) {
|
||||
return (
|
||||
(center.x - this.x) * (b.y - this.y) - (b.x - this.x) * (center.y - b.y)
|
||||
)
|
||||
}
|
||||
|
||||
static isLeft(center: Vector, a: Vector, b: Vector) {
|
||||
return (center.x - a.x) * (b.y - a.y) - (b.x - a.x) * (center.y - b.y)
|
||||
}
|
||||
|
||||
static ang3(center: Vector, a: Vector, b: Vector) {
|
||||
const v1 = Vector.vec(center, a)
|
||||
const v2 = Vector.vec(center, b)
|
||||
return Vector.ang(v1, v2)
|
||||
}
|
||||
|
||||
static clockwise(center: Vector, a: Vector, b: Vector) {
|
||||
return Vector.isLeft(center, a, b) > 0
|
||||
}
|
||||
|
||||
static cast(v: Point | Vector) {
|
||||
return "cast" in v ? v : new Vector(v)
|
||||
}
|
||||
|
||||
static from(v: Vector) {
|
||||
return new Vector(v)
|
||||
}
|
||||
|
||||
nearestPointOnLineThroughPoint(b: Vector, u: Vector) {
|
||||
return this.clone().add(u.clone().mul(Vector.sub(this, b).pry(u)))
|
||||
}
|
||||
|
||||
static nearestPointOnLineThroughPoint(a: Vector, b: Vector, u: Vector) {
|
||||
return a.clone().add(u.clone().mul(Vector.sub(a, b).pry(u)))
|
||||
}
|
||||
|
||||
distanceToLineThroughPoint(b: Vector, u: Vector) {
|
||||
return this.dist(Vector.nearestPointOnLineThroughPoint(b, u, this))
|
||||
}
|
||||
|
||||
static distanceToLineThroughPoint(a: Vector, b: Vector, u: Vector) {
|
||||
return a.dist(Vector.nearestPointOnLineThroughPoint(b, u, a))
|
||||
}
|
||||
|
||||
nearestPointOnLineSegment(p0: Vector, p1: Vector, clamp = true) {
|
||||
return Vector.nearestPointOnLineSegment(this, p0, p1, clamp)
|
||||
}
|
||||
|
||||
static nearestPointOnLineSegment(
|
||||
a: Vector,
|
||||
p0: Vector,
|
||||
p1: Vector,
|
||||
clamp = true
|
||||
) {
|
||||
const delta = Vector.sub(p1, p0)
|
||||
const length = delta.len()
|
||||
const u = Vector.div(delta, length)
|
||||
|
||||
const pt = Vector.add(p0, Vector.mul(u, Vector.pry(Vector.sub(a, p0), u)))
|
||||
|
||||
if (clamp) {
|
||||
const da = p0.dist(pt)
|
||||
const db = p1.dist(pt)
|
||||
|
||||
if (db < da && da > length) return p1
|
||||
if (da < db && db > length) return p0
|
||||
}
|
||||
|
||||
return pt
|
||||
}
|
||||
|
||||
distanceToLineSegment(p0: Vector, p1: Vector, clamp = true) {
|
||||
return Vector.distanceToLineSegment(this, p0, p1, clamp)
|
||||
}
|
||||
|
||||
static distanceToLineSegment(
|
||||
a: Vector,
|
||||
p0: Vector,
|
||||
p1: Vector,
|
||||
clamp = true
|
||||
) {
|
||||
return Vector.dist(a, Vector.nearestPointOnLineSegment(a, p0, p1, clamp))
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { v4 as uuid } from "uuid"
|
||||
import * as vec from "utils/vec"
|
||||
import { CircleShape, ShapeType } from "types"
|
||||
import { CircleShape, ShapeType, TransformCorner, TransformEdge } from "types"
|
||||
import { createShape } from "./index"
|
||||
import { boundsContained } from "utils/bounds"
|
||||
import { intersectCircleBounds } from "utils/intersections"
|
||||
|
@ -13,6 +13,7 @@ const circle = createShape<CircleShape>({
|
|||
return {
|
||||
id: uuid(),
|
||||
type: ShapeType.Circle,
|
||||
isGenerated: false,
|
||||
name: "Circle",
|
||||
parentId: "page0",
|
||||
childIndex: 0,
|
||||
|
@ -90,16 +91,70 @@ const circle = createShape<CircleShape>({
|
|||
return shape
|
||||
},
|
||||
|
||||
transform(shape, bounds) {
|
||||
// shape.point = [bounds.minX, bounds.minY]
|
||||
shape.radius = Math.min(bounds.width, bounds.height) / 2
|
||||
shape.point = [
|
||||
bounds.minX + bounds.width / 2 - shape.radius,
|
||||
bounds.minY + bounds.height / 2 - shape.radius,
|
||||
]
|
||||
transform(shape, bounds, { anchor }) {
|
||||
// Set the new corner or position depending on the anchor
|
||||
switch (anchor) {
|
||||
case TransformCorner.TopLeft: {
|
||||
shape.radius = Math.min(bounds.width, bounds.height) / 2
|
||||
shape.point = [
|
||||
bounds.maxX - shape.radius * 2,
|
||||
bounds.maxY - shape.radius * 2,
|
||||
]
|
||||
break
|
||||
}
|
||||
case TransformCorner.TopRight: {
|
||||
shape.radius = Math.min(bounds.width, bounds.height) / 2
|
||||
shape.point = [bounds.minX, bounds.maxY - shape.radius * 2]
|
||||
break
|
||||
}
|
||||
case TransformCorner.BottomRight: {
|
||||
shape.radius = Math.min(bounds.width, bounds.height) / 2
|
||||
shape.point = [bounds.minX, bounds.minY]
|
||||
break
|
||||
}
|
||||
case TransformCorner.BottomLeft: {
|
||||
shape.radius = Math.min(bounds.width, bounds.height) / 2
|
||||
shape.point = [bounds.maxX - shape.radius * 2, bounds.minY]
|
||||
break
|
||||
}
|
||||
case TransformEdge.Top: {
|
||||
shape.radius = bounds.height / 2
|
||||
shape.point = [
|
||||
bounds.minX + (bounds.width / 2 - shape.radius),
|
||||
bounds.minY,
|
||||
]
|
||||
break
|
||||
}
|
||||
case TransformEdge.Right: {
|
||||
shape.radius = bounds.width / 2
|
||||
shape.point = [
|
||||
bounds.maxX - shape.radius * 2,
|
||||
bounds.minY + (bounds.height / 2 - shape.radius),
|
||||
]
|
||||
break
|
||||
}
|
||||
case TransformEdge.Bottom: {
|
||||
shape.radius = bounds.height / 2
|
||||
shape.point = [
|
||||
bounds.minX + (bounds.width / 2 - shape.radius),
|
||||
bounds.maxY - shape.radius * 2,
|
||||
]
|
||||
break
|
||||
}
|
||||
case TransformEdge.Left: {
|
||||
shape.radius = bounds.width / 2
|
||||
shape.point = [
|
||||
bounds.minX,
|
||||
bounds.minY + (bounds.height / 2 - shape.radius),
|
||||
]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return shape
|
||||
},
|
||||
|
||||
canTransform: true,
|
||||
})
|
||||
|
||||
export default circle
|
||||
|
|
|
@ -12,6 +12,7 @@ const dot = createShape<DotShape>({
|
|||
return {
|
||||
id: uuid(),
|
||||
type: ShapeType.Dot,
|
||||
isGenerated: false,
|
||||
name: "Dot",
|
||||
parentId: "page0",
|
||||
childIndex: 0,
|
||||
|
@ -83,6 +84,8 @@ const dot = createShape<DotShape>({
|
|||
|
||||
return shape
|
||||
},
|
||||
|
||||
canTransform: false,
|
||||
})
|
||||
|
||||
export default dot
|
||||
|
|
|
@ -13,6 +13,7 @@ const ellipse = createShape<EllipseShape>({
|
|||
return {
|
||||
id: uuid(),
|
||||
type: ShapeType.Ellipse,
|
||||
isGenerated: false,
|
||||
name: "Ellipse",
|
||||
parentId: "page0",
|
||||
childIndex: 0,
|
||||
|
@ -103,6 +104,8 @@ const ellipse = createShape<EllipseShape>({
|
|||
|
||||
return shape
|
||||
},
|
||||
|
||||
canTransform: true,
|
||||
})
|
||||
|
||||
export default ellipse
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
import { Bounds, BoundsSnapshot, Shape, Shapes, ShapeType } from "types"
|
||||
import {
|
||||
Bounds,
|
||||
BoundsSnapshot,
|
||||
Shape,
|
||||
Shapes,
|
||||
ShapeType,
|
||||
TransformCorner,
|
||||
TransformEdge,
|
||||
} from "types"
|
||||
import circle from "./circle"
|
||||
import dot from "./dot"
|
||||
import polyline from "./polyline"
|
||||
|
@ -44,10 +52,16 @@ export interface ShapeUtility<K extends Shape> {
|
|||
transform(
|
||||
this: ShapeUtility<K>,
|
||||
shape: K,
|
||||
bounds: Bounds & { isFlippedX: boolean; isFlippedY: boolean },
|
||||
initialShape: K,
|
||||
initialShapeBounds: BoundsSnapshot,
|
||||
initialBounds: Bounds
|
||||
bounds: Bounds,
|
||||
info: {
|
||||
type: TransformEdge | TransformCorner
|
||||
initialShape: K
|
||||
initialShapeBounds: BoundsSnapshot
|
||||
initialBounds: Bounds
|
||||
isFlippedX: boolean
|
||||
isFlippedY: boolean
|
||||
anchor: TransformEdge | TransformCorner
|
||||
}
|
||||
): K
|
||||
|
||||
// Apply a scale to a shape.
|
||||
|
@ -58,6 +72,9 @@ export interface ShapeUtility<K extends Shape> {
|
|||
|
||||
// Render a shape to JSX.
|
||||
render(this: ShapeUtility<K>, shape: K): JSX.Element
|
||||
|
||||
// Whether to show transform controls when this shape is selected.
|
||||
canTransform: boolean
|
||||
}
|
||||
|
||||
// A mapping of shape types to shape utilities.
|
||||
|
|
|
@ -12,6 +12,7 @@ const line = createShape<LineShape>({
|
|||
return {
|
||||
id: uuid(),
|
||||
type: ShapeType.Line,
|
||||
isGenerated: false,
|
||||
name: "Line",
|
||||
parentId: "page0",
|
||||
childIndex: 0,
|
||||
|
@ -63,7 +64,11 @@ const line = createShape<LineShape>({
|
|||
},
|
||||
|
||||
hitTestBounds(this, shape, brushBounds) {
|
||||
return true
|
||||
const shapeBounds = this.getBounds(shape)
|
||||
return (
|
||||
boundsContained(shapeBounds, brushBounds) ||
|
||||
intersectCircleBounds(shape.point, 4, brushBounds).length > 0
|
||||
)
|
||||
},
|
||||
|
||||
rotate(shape) {
|
||||
|
@ -88,6 +93,8 @@ const line = createShape<LineShape>({
|
|||
|
||||
return shape
|
||||
},
|
||||
|
||||
canTransform: false,
|
||||
})
|
||||
|
||||
export default line
|
||||
|
|
|
@ -12,6 +12,7 @@ const polyline = createShape<PolylineShape>({
|
|||
return {
|
||||
id: uuid(),
|
||||
type: ShapeType.Polyline,
|
||||
isGenerated: false,
|
||||
name: "Polyline",
|
||||
parentId: "page0",
|
||||
childIndex: 0,
|
||||
|
@ -101,17 +102,21 @@ const polyline = createShape<PolylineShape>({
|
|||
return shape
|
||||
},
|
||||
|
||||
transform(shape, bounds, initialShape, initialShapeBounds) {
|
||||
transform(
|
||||
shape,
|
||||
bounds,
|
||||
{ initialShape, initialShapeBounds, isFlippedX, isFlippedY }
|
||||
) {
|
||||
shape.points = shape.points.map((_, i) => {
|
||||
const [x, y] = initialShape.points[i]
|
||||
|
||||
return [
|
||||
bounds.width *
|
||||
(bounds.isFlippedX
|
||||
(isFlippedX
|
||||
? 1 - x / initialShapeBounds.width
|
||||
: x / initialShapeBounds.width),
|
||||
bounds.height *
|
||||
(bounds.isFlippedY
|
||||
(isFlippedY
|
||||
? 1 - y / initialShapeBounds.height
|
||||
: y / initialShapeBounds.height),
|
||||
]
|
||||
|
@ -120,6 +125,8 @@ const polyline = createShape<PolylineShape>({
|
|||
shape.point = [bounds.minX, bounds.minY]
|
||||
return shape
|
||||
},
|
||||
|
||||
canTransform: true,
|
||||
})
|
||||
|
||||
export default polyline
|
||||
|
|
|
@ -12,6 +12,7 @@ const ray = createShape<RayShape>({
|
|||
return {
|
||||
id: uuid(),
|
||||
type: ShapeType.Ray,
|
||||
isGenerated: false,
|
||||
name: "Ray",
|
||||
parentId: "page0",
|
||||
childIndex: 0,
|
||||
|
@ -82,6 +83,8 @@ const ray = createShape<RayShape>({
|
|||
transform(shape, bounds) {
|
||||
return shape
|
||||
},
|
||||
|
||||
canTransform: false,
|
||||
})
|
||||
|
||||
export default ray
|
||||
|
|
|
@ -11,6 +11,7 @@ const rectangle = createShape<RectangleShape>({
|
|||
return {
|
||||
id: uuid(),
|
||||
type: ShapeType.Rectangle,
|
||||
isGenerated: false,
|
||||
name: "Rectangle",
|
||||
parentId: "page0",
|
||||
childIndex: 0,
|
||||
|
@ -86,6 +87,8 @@ const rectangle = createShape<RectangleShape>({
|
|||
|
||||
return shape
|
||||
},
|
||||
|
||||
canTransform: true,
|
||||
})
|
||||
|
||||
export default rectangle
|
||||
|
|
|
@ -78,6 +78,8 @@ export default class Command extends BaseCommand<Data> {
|
|||
saveSelectionState = (data: Data) => {
|
||||
const selectedIds = new Set(data.selectedIds)
|
||||
return (data: Data) => {
|
||||
data.hoveredId = undefined
|
||||
data.pointedId = undefined
|
||||
data.selectedIds = selectedIds
|
||||
}
|
||||
}
|
||||
|
|
58
state/commands/generate-shapes.ts
Normal file
58
state/commands/generate-shapes.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import Command from "./command"
|
||||
import history from "../history"
|
||||
import { Data, Shape } from "types"
|
||||
import { current } from "immer"
|
||||
|
||||
export default function setGeneratedShapes(
|
||||
data: Data,
|
||||
currentPageId: string,
|
||||
generatedShapes: Shape[]
|
||||
) {
|
||||
const prevGeneratedShapes = Object.values(
|
||||
current(data).document.pages[currentPageId].shapes
|
||||
).filter((shape) => shape.isGenerated)
|
||||
|
||||
for (let shape of generatedShapes) {
|
||||
data.document.pages[currentPageId].shapes[shape.id] = shape
|
||||
}
|
||||
|
||||
history.execute(
|
||||
data,
|
||||
new Command({
|
||||
name: "translate_shapes",
|
||||
category: "canvas",
|
||||
do(data) {
|
||||
const { shapes } = data.document.pages[currentPageId]
|
||||
|
||||
data.selectedIds.clear()
|
||||
|
||||
// Remove previous generated shapes
|
||||
for (let id in shapes) {
|
||||
if (shapes[id].isGenerated) {
|
||||
delete shapes[id]
|
||||
}
|
||||
}
|
||||
|
||||
// Add new generated shapes
|
||||
for (let shape of generatedShapes) {
|
||||
shapes[shape.id] = shape
|
||||
}
|
||||
},
|
||||
undo(data) {
|
||||
const { shapes } = data.document.pages[currentPageId]
|
||||
|
||||
// Remove generated shapes
|
||||
for (let id in shapes) {
|
||||
if (shapes[id].isGenerated) {
|
||||
delete shapes[id]
|
||||
}
|
||||
}
|
||||
|
||||
// Restore previous generated shapes
|
||||
for (let shape of prevGeneratedShapes) {
|
||||
shapes[shape.id] = shape
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import translate from "./translate-command"
|
||||
import transform from "./transform-command"
|
||||
import translate from "./translate"
|
||||
import transform from "./transform"
|
||||
import generateShapes from "./generate-shapes"
|
||||
|
||||
const commands = { translate, transform }
|
||||
const commands = { translate, transform, generateShapes }
|
||||
|
||||
export default commands
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import Command from "./command"
|
||||
import history from "../history"
|
||||
import { Data } from "types"
|
||||
import { Data, TransformCorner, TransformEdge } from "types"
|
||||
import { TransformSnapshot } from "state/sessions/transform-session"
|
||||
import { getShapeUtils } from "lib/shapes"
|
||||
|
||||
export default function translateCommand(
|
||||
data: Data,
|
||||
before: TransformSnapshot,
|
||||
after: TransformSnapshot
|
||||
after: TransformSnapshot,
|
||||
anchor: TransformCorner | TransformEdge
|
||||
) {
|
||||
history.execute(
|
||||
data,
|
||||
|
@ -15,28 +16,34 @@ export default function translateCommand(
|
|||
name: "translate_shapes",
|
||||
category: "canvas",
|
||||
do(data) {
|
||||
const { shapeBounds, initialBounds, currentPageId, selectedIds } = after
|
||||
const {
|
||||
type,
|
||||
shapeBounds,
|
||||
initialBounds,
|
||||
currentPageId,
|
||||
selectedIds,
|
||||
} = after
|
||||
|
||||
const { shapes } = data.document.pages[currentPageId]
|
||||
|
||||
selectedIds.forEach((id) => {
|
||||
const { initialShape, initialShapeBounds } = shapeBounds[id]
|
||||
const shape = shapes[id]
|
||||
|
||||
getShapeUtils(shape).transform(
|
||||
shape,
|
||||
{
|
||||
...initialShapeBounds,
|
||||
isFlippedX: false,
|
||||
isFlippedY: false,
|
||||
},
|
||||
getShapeUtils(shape).transform(shape, initialShapeBounds, {
|
||||
type,
|
||||
initialShape,
|
||||
initialShapeBounds,
|
||||
initialBounds
|
||||
)
|
||||
initialBounds,
|
||||
isFlippedX: false,
|
||||
isFlippedY: false,
|
||||
anchor,
|
||||
})
|
||||
})
|
||||
},
|
||||
undo(data) {
|
||||
const {
|
||||
type,
|
||||
shapeBounds,
|
||||
initialBounds,
|
||||
currentPageId,
|
||||
|
@ -49,17 +56,15 @@ export default function translateCommand(
|
|||
const { initialShape, initialShapeBounds } = shapeBounds[id]
|
||||
const shape = shapes[id]
|
||||
|
||||
getShapeUtils(shape).transform(
|
||||
shape,
|
||||
{
|
||||
...initialShapeBounds,
|
||||
isFlippedX: false,
|
||||
isFlippedY: false,
|
||||
},
|
||||
getShapeUtils(shape).transform(shape, initialShapeBounds, {
|
||||
type,
|
||||
initialShape,
|
||||
initialShapeBounds,
|
||||
initialBounds
|
||||
)
|
||||
initialBounds,
|
||||
isFlippedX: false,
|
||||
isFlippedY: false,
|
||||
anchor: type,
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
200
state/data.ts
200
state/data.ts
|
@ -9,84 +9,97 @@ export const defaultDocument: Data["document"] = {
|
|||
name: "Page 0",
|
||||
childIndex: 0,
|
||||
shapes: {
|
||||
shape3: shapeUtils[ShapeType.Dot].create({
|
||||
id: "shape3",
|
||||
name: "Shape 3",
|
||||
childIndex: 3,
|
||||
point: [500, 100],
|
||||
style: {
|
||||
fill: "#AAA",
|
||||
stroke: "#777",
|
||||
strokeWidth: 1,
|
||||
},
|
||||
}),
|
||||
shape0: shapeUtils[ShapeType.Circle].create({
|
||||
id: "shape0",
|
||||
name: "Shape 0",
|
||||
childIndex: 1,
|
||||
point: [100, 100],
|
||||
radius: 50,
|
||||
style: {
|
||||
fill: "#AAA",
|
||||
stroke: "#777",
|
||||
strokeWidth: 1,
|
||||
},
|
||||
}),
|
||||
shape5: shapeUtils[ShapeType.Ellipse].create({
|
||||
id: "shape5",
|
||||
name: "Shape 5",
|
||||
childIndex: 5,
|
||||
point: [250, 100],
|
||||
radiusX: 50,
|
||||
radiusY: 30,
|
||||
style: {
|
||||
fill: "#AAA",
|
||||
stroke: "#777",
|
||||
strokeWidth: 1,
|
||||
},
|
||||
}),
|
||||
shape2: shapeUtils[ShapeType.Polyline].create({
|
||||
id: "shape2",
|
||||
name: "Shape 2",
|
||||
childIndex: 2,
|
||||
point: [200, 600],
|
||||
points: [
|
||||
[0, 0],
|
||||
[75, 200],
|
||||
[100, 50],
|
||||
],
|
||||
style: {
|
||||
fill: "none",
|
||||
stroke: "#777",
|
||||
strokeWidth: 2,
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
},
|
||||
}),
|
||||
shape1: shapeUtils[ShapeType.Rectangle].create({
|
||||
id: "shape1",
|
||||
name: "Shape 1",
|
||||
childIndex: 1,
|
||||
point: [300, 300],
|
||||
size: [200, 200],
|
||||
style: {
|
||||
fill: "#AAA",
|
||||
stroke: "#777",
|
||||
strokeWidth: 1,
|
||||
},
|
||||
}),
|
||||
shape6: shapeUtils[ShapeType.Line].create({
|
||||
id: "shape6",
|
||||
name: "Shape 6",
|
||||
childIndex: 1,
|
||||
point: [400, 400],
|
||||
direction: [0.2, 0.2],
|
||||
style: {
|
||||
fill: "#AAA",
|
||||
stroke: "#777",
|
||||
strokeWidth: 1,
|
||||
},
|
||||
}),
|
||||
// shape3: shapeUtils[ShapeType.Dot].create({
|
||||
// id: "shape3",
|
||||
// name: "Shape 3",
|
||||
// childIndex: 3,
|
||||
// point: [400, 500],
|
||||
// style: {
|
||||
// fill: "#AAA",
|
||||
// stroke: "#777",
|
||||
// strokeWidth: 1,
|
||||
// },
|
||||
// }),
|
||||
// shape0: shapeUtils[ShapeType.Circle].create({
|
||||
// id: "shape0",
|
||||
// name: "Shape 0",
|
||||
// childIndex: 1,
|
||||
// point: [100, 600],
|
||||
// radius: 50,
|
||||
// style: {
|
||||
// fill: "#AAA",
|
||||
// stroke: "#777",
|
||||
// strokeWidth: 1,
|
||||
// },
|
||||
// }),
|
||||
// shape5: shapeUtils[ShapeType.Ellipse].create({
|
||||
// id: "shape5",
|
||||
// name: "Shape 5",
|
||||
// childIndex: 5,
|
||||
// point: [400, 600],
|
||||
// radiusX: 50,
|
||||
// radiusY: 30,
|
||||
// style: {
|
||||
// fill: "#AAA",
|
||||
// stroke: "#777",
|
||||
// strokeWidth: 1,
|
||||
// },
|
||||
// }),
|
||||
// shape7: shapeUtils[ShapeType.Ellipse].create({
|
||||
// id: "shape7",
|
||||
// name: "Shape 7",
|
||||
// childIndex: 7,
|
||||
// point: [100, 100],
|
||||
// radiusX: 50,
|
||||
// radiusY: 30,
|
||||
// style: {
|
||||
// fill: "#AAA",
|
||||
// stroke: "#777",
|
||||
// strokeWidth: 1,
|
||||
// },
|
||||
// }),
|
||||
// shape2: shapeUtils[ShapeType.Polyline].create({
|
||||
// id: "shape2",
|
||||
// name: "Shape 2",
|
||||
// childIndex: 2,
|
||||
// point: [200, 600],
|
||||
// points: [
|
||||
// [0, 0],
|
||||
// [75, 200],
|
||||
// [100, 50],
|
||||
// ],
|
||||
// style: {
|
||||
// fill: "none",
|
||||
// stroke: "#777",
|
||||
// strokeWidth: 2,
|
||||
// strokeLinecap: "round",
|
||||
// strokeLinejoin: "round",
|
||||
// },
|
||||
// }),
|
||||
// shape1: shapeUtils[ShapeType.Rectangle].create({
|
||||
// id: "shape1",
|
||||
// name: "Shape 1",
|
||||
// childIndex: 1,
|
||||
// point: [400, 600],
|
||||
// size: [200, 200],
|
||||
// style: {
|
||||
// fill: "#AAA",
|
||||
// stroke: "#777",
|
||||
// strokeWidth: 1,
|
||||
// },
|
||||
// }),
|
||||
// shape6: shapeUtils[ShapeType.Line].create({
|
||||
// id: "shape6",
|
||||
// name: "Shape 6",
|
||||
// childIndex: 1,
|
||||
// point: [400, 400],
|
||||
// direction: [0.2, 0.2],
|
||||
// style: {
|
||||
// fill: "#AAA",
|
||||
// stroke: "#777",
|
||||
// strokeWidth: 1,
|
||||
// },
|
||||
// }),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -94,7 +107,36 @@ export const defaultDocument: Data["document"] = {
|
|||
file0: {
|
||||
id: "file0",
|
||||
name: "index.ts",
|
||||
code: "// Hello world",
|
||||
code: `
|
||||
new Dot({
|
||||
point: [0, 0],
|
||||
})
|
||||
|
||||
new Circle({
|
||||
point: [200, 0],
|
||||
radius: 50,
|
||||
})
|
||||
|
||||
new Ellipse({
|
||||
point: [400, 0],
|
||||
radiusX: 50,
|
||||
radiusY: 75
|
||||
})
|
||||
|
||||
new Rectangle({
|
||||
point: [0, 300],
|
||||
})
|
||||
|
||||
new Line({
|
||||
point: [200, 300],
|
||||
direction: [1,0.2]
|
||||
})
|
||||
|
||||
new Polyline({
|
||||
point: [400, 300],
|
||||
points: [[0, 200], [0,0], [200, 200], [200, 0]],
|
||||
})
|
||||
`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -10,10 +10,12 @@ import BaseSession from "./base-session"
|
|||
import commands from "state/commands"
|
||||
import { current } from "immer"
|
||||
import { getShapeUtils } from "lib/shapes"
|
||||
import { getCommonBounds } from "utils/utils"
|
||||
import { getCommonBounds, getTransformAnchor } from "utils/utils"
|
||||
|
||||
export default class TransformSession extends BaseSession {
|
||||
delta = [0, 0]
|
||||
isFlippedX = false
|
||||
isFlippedY = false
|
||||
transformType: TransformEdge | TransformCorner
|
||||
origin: number[]
|
||||
snapshot: TransformSnapshot
|
||||
|
@ -24,13 +26,13 @@ export default class TransformSession extends BaseSession {
|
|||
|
||||
constructor(
|
||||
data: Data,
|
||||
type: TransformCorner | TransformEdge,
|
||||
transformType: TransformCorner | TransformEdge,
|
||||
point: number[]
|
||||
) {
|
||||
super(data)
|
||||
this.origin = point
|
||||
this.transformType = type
|
||||
this.snapshot = getTransformSnapshot(data)
|
||||
this.transformType = transformType
|
||||
this.snapshot = getTransformSnapshot(data, transformType)
|
||||
|
||||
const { minX, minY, maxX, maxY } = this.snapshot.initialBounds
|
||||
|
||||
|
@ -108,8 +110,8 @@ export default class TransformSession extends BaseSession {
|
|||
height: Math.abs(b[1] - a[1]),
|
||||
}
|
||||
|
||||
const isFlippedX = b[0] < a[0]
|
||||
const isFlippedY = b[1] < a[1]
|
||||
this.isFlippedX = b[0] < a[0]
|
||||
this.isFlippedY = b[1] < a[1]
|
||||
|
||||
// Now work backward to calculate a new bounding box for each of the shapes.
|
||||
|
||||
|
@ -118,8 +120,10 @@ export default class TransformSession extends BaseSession {
|
|||
const { nx, nmx, nw, ny, nmy, nh } = initialShapeBounds
|
||||
const shape = shapes[id]
|
||||
|
||||
const minX = newBounds.minX + (isFlippedX ? nmx : nx) * newBounds.width
|
||||
const minY = newBounds.minY + (isFlippedY ? nmy : ny) * newBounds.height
|
||||
const minX =
|
||||
newBounds.minX + (this.isFlippedX ? nmx : nx) * newBounds.width
|
||||
const minY =
|
||||
newBounds.minY + (this.isFlippedY ? nmy : ny) * newBounds.height
|
||||
const width = nw * newBounds.width
|
||||
const height = nh * newBounds.height
|
||||
|
||||
|
@ -130,8 +134,8 @@ export default class TransformSession extends BaseSession {
|
|||
maxY: minY + height,
|
||||
width,
|
||||
height,
|
||||
isFlippedX,
|
||||
isFlippedY,
|
||||
isFlippedX: this.isFlippedX,
|
||||
isFlippedY: this.isFlippedY,
|
||||
}
|
||||
|
||||
// Pass the new data to the shape's transform utility for mutation.
|
||||
|
@ -139,13 +143,19 @@ export default class TransformSession extends BaseSession {
|
|||
// however some shapes (e.g. those with internal points) will need more
|
||||
// data here too.
|
||||
|
||||
getShapeUtils(shape).transform(
|
||||
shape,
|
||||
newShapeBounds,
|
||||
getShapeUtils(shape).transform(shape, newShapeBounds, {
|
||||
type: this.transformType,
|
||||
initialShape,
|
||||
initialShapeBounds,
|
||||
initialBounds
|
||||
)
|
||||
initialBounds,
|
||||
isFlippedX: this.isFlippedX,
|
||||
isFlippedY: this.isFlippedY,
|
||||
anchor: getTransformAnchor(
|
||||
this.transformType,
|
||||
this.isFlippedX,
|
||||
this.isFlippedY
|
||||
),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -163,26 +173,32 @@ export default class TransformSession extends BaseSession {
|
|||
const shape = shapes.shapes[id]
|
||||
const { initialShape, initialShapeBounds } = shapeBounds[id]
|
||||
|
||||
getShapeUtils(shape).transform(
|
||||
shape,
|
||||
{
|
||||
...initialShapeBounds,
|
||||
isFlippedX: false,
|
||||
isFlippedY: false,
|
||||
},
|
||||
getShapeUtils(shape).transform(shape, initialShapeBounds, {
|
||||
type: this.transformType,
|
||||
initialShape,
|
||||
initialShapeBounds,
|
||||
initialBounds
|
||||
)
|
||||
initialBounds,
|
||||
isFlippedX: false,
|
||||
isFlippedY: false,
|
||||
anchor: getTransformAnchor(this.transformType, false, false),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
complete(data: Data) {
|
||||
commands.transform(data, this.snapshot, getTransformSnapshot(data))
|
||||
commands.transform(
|
||||
data,
|
||||
this.snapshot,
|
||||
getTransformSnapshot(data, this.transformType),
|
||||
getTransformAnchor(this.transformType, false, false)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function getTransformSnapshot(data: Data) {
|
||||
export function getTransformSnapshot(
|
||||
data: Data,
|
||||
transformType: TransformEdge | TransformCorner
|
||||
) {
|
||||
const {
|
||||
document: { pages },
|
||||
selectedIds,
|
||||
|
@ -206,6 +222,7 @@ export function getTransformSnapshot(data: Data) {
|
|||
// positions of the shape's bounds within the common bounds shape.
|
||||
return {
|
||||
currentPageId,
|
||||
type: transformType,
|
||||
initialBounds: bounds,
|
||||
selectedIds: new Set(selectedIds),
|
||||
shapeBounds: Object.fromEntries(
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { createSelectorHook, createState } from "@state-designer/react"
|
||||
import { clamp, getCommonBounds, screenToWorld } from "utils/utils"
|
||||
import * as vec from "utils/vec"
|
||||
import { Data, PointerInfo, TransformCorner, TransformEdge } from "types"
|
||||
import { Data, PointerInfo, Shape, TransformCorner, TransformEdge } from "types"
|
||||
import { defaultDocument } from "./data"
|
||||
import { getShapeUtils } from "lib/shapes"
|
||||
import history from "state/history"
|
||||
import * as Sessions from "./sessions"
|
||||
import commands from "./commands"
|
||||
|
||||
const initialData: Data = {
|
||||
isReadOnly: false,
|
||||
|
@ -41,6 +42,9 @@ const state = createState({
|
|||
on: {
|
||||
UNDO: { do: "undo" },
|
||||
REDO: { do: "redo" },
|
||||
GENERATED_SHAPES_FROM_CODE: "setGeneratedShapes",
|
||||
INCREASED_CODE_FONT_SIZE: "increaseCodeFontSize",
|
||||
DECREASED_CODE_FONT_SIZE: "decreaseCodeFontSize",
|
||||
},
|
||||
initial: "notPointing",
|
||||
states: {
|
||||
|
@ -156,10 +160,6 @@ const state = createState({
|
|||
const shape =
|
||||
data.document.pages[data.currentPageId].shapes[payload.target]
|
||||
|
||||
console.log(
|
||||
getShapeUtils(shape).hitTest(shape, screenToWorld(payload.point, data))
|
||||
)
|
||||
|
||||
return getShapeUtils(shape).hitTest(
|
||||
shape,
|
||||
screenToWorld(payload.point, data)
|
||||
|
@ -181,6 +181,17 @@ const state = createState({
|
|||
history.redo(data)
|
||||
},
|
||||
|
||||
// Code
|
||||
setGeneratedShapes(data, payload: { shapes: Shape[] }) {
|
||||
commands.generateShapes(data, data.currentPageId, payload.shapes)
|
||||
},
|
||||
increaseCodeFontSize(data) {
|
||||
data.settings.fontSize++
|
||||
},
|
||||
decreaseCodeFontSize(data) {
|
||||
data.settings.fontSize--
|
||||
},
|
||||
|
||||
// Sessions
|
||||
cancelSession(data) {
|
||||
session.cancel(data)
|
||||
|
@ -287,15 +298,18 @@ const state = createState({
|
|||
document: { pages },
|
||||
} = data
|
||||
|
||||
const shapes = Array.from(selectedIds.values()).map(
|
||||
(id) => pages[currentPageId].shapes[id]
|
||||
)
|
||||
|
||||
if (selectedIds.size === 0) return null
|
||||
|
||||
if (selectedIds.size === 1 && !getShapeUtils(shapes[0]).canTransform) {
|
||||
return null
|
||||
}
|
||||
|
||||
return getCommonBounds(
|
||||
...Array.from(selectedIds.values())
|
||||
.map((id) => {
|
||||
const shape = pages[currentPageId].shapes[id]
|
||||
return getShapeUtils(shape).getBounds(shape)
|
||||
})
|
||||
.filter(Boolean)
|
||||
...shapes.map((shape) => getShapeUtils(shape).getBounds(shape))
|
||||
)
|
||||
},
|
||||
},
|
||||
|
|
1
types.ts
1
types.ts
|
@ -56,6 +56,7 @@ export interface BaseShape {
|
|||
type: ShapeType
|
||||
parentId: string
|
||||
childIndex: number
|
||||
isGenerated: boolean
|
||||
name: string
|
||||
point: number[]
|
||||
rotation: number
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Data, Bounds } from "types"
|
||||
import { Data, Bounds, TransformEdge, TransformCorner } from "types"
|
||||
import * as svg from "./svg"
|
||||
import * as vec from "./vec"
|
||||
|
||||
|
@ -891,3 +891,57 @@ export function getKeyboardEventInfo(e: KeyboardEvent | React.KeyboardEvent) {
|
|||
export function isDarwin() {
|
||||
return /Mac|iPod|iPhone|iPad/.test(window.navigator.platform)
|
||||
}
|
||||
|
||||
export function getTransformAnchor(
|
||||
type: TransformEdge | TransformCorner,
|
||||
isFlippedX: boolean,
|
||||
isFlippedY: boolean
|
||||
) {
|
||||
let anchor: TransformCorner | TransformEdge = type
|
||||
|
||||
// Change corner anchors if flipped
|
||||
switch (type) {
|
||||
case TransformCorner.TopLeft: {
|
||||
if (isFlippedX && isFlippedY) {
|
||||
anchor = TransformCorner.BottomRight
|
||||
} else if (isFlippedX) {
|
||||
anchor = TransformCorner.TopRight
|
||||
} else if (isFlippedY) {
|
||||
anchor = TransformCorner.BottomLeft
|
||||
}
|
||||
break
|
||||
}
|
||||
case TransformCorner.TopRight: {
|
||||
if (isFlippedX && isFlippedY) {
|
||||
anchor = TransformCorner.BottomLeft
|
||||
} else if (isFlippedX) {
|
||||
anchor = TransformCorner.TopLeft
|
||||
} else if (isFlippedY) {
|
||||
anchor = TransformCorner.BottomRight
|
||||
}
|
||||
break
|
||||
}
|
||||
case TransformCorner.BottomRight: {
|
||||
if (isFlippedX && isFlippedY) {
|
||||
anchor = TransformCorner.TopLeft
|
||||
} else if (isFlippedX) {
|
||||
anchor = TransformCorner.BottomLeft
|
||||
} else if (isFlippedY) {
|
||||
anchor = TransformCorner.TopRight
|
||||
}
|
||||
break
|
||||
}
|
||||
case TransformCorner.BottomLeft: {
|
||||
if (isFlippedX && isFlippedY) {
|
||||
anchor = TransformCorner.TopRight
|
||||
} else if (isFlippedX) {
|
||||
anchor = TransformCorner.BottomRight
|
||||
} else if (isFlippedY) {
|
||||
anchor = TransformCorner.TopLeft
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return anchor
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue