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

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