Adds code editing and shape generation

This commit is contained in:
Steve Ruiz 2021-05-15 14:02:13 +01:00
parent afa8f53dff
commit 1a01c47835
34 changed files with 1298 additions and 237 deletions

View file

@ -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

View file

@ -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 }

View file

@ -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],
})
`

View file

@ -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",
})

View file

@ -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],
})
`

View file

@ -10,7 +10,7 @@ export default function Editor() {
<>
<Canvas />
<StatusBar />
{/* <CodePanel /> */}
<CodePanel />
</>
)
}

View file

@ -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
View 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
View 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
View 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
}

View file

@ -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
View 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
View 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,
})
}
}

View file

@ -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
View 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
View 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))
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
}
}

View 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
}
},
})
)
}

View file

@ -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

View file

@ -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,
})
})
},
})

View file

@ -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]],
})
`,
},
},
}

View file

@ -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(

View file

@ -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))
)
},
},

View file

@ -56,6 +56,7 @@ export interface BaseShape {
type: ShapeType
parentId: string
childIndex: number
isGenerated: boolean
name: string
point: number[]
rotation: number

View file

@ -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
}