Add heart geo shape (#3787)
This PR adds a heart geo shape. ❤️
It also:
- adds `toSvgPathData` to geometry2d
- uses geometry2d in places where previously we recalculated things like
perimeter of ellipse
- flattens geo shape util components
- [x] Calculate the path length for the DashStyleHeart
### Change Type
- [x] `sdk` — Changes the tldraw SDK
- [x] `feature` — New feature
### Release Notes
- Adds a heart shape to the geo shape set.
This commit is contained in:
parent
6c9ead0309
commit
ef44d71ee2
56 changed files with 1403 additions and 1379 deletions
|
@ -19,9 +19,10 @@ const clickableShapeCreators = [
|
||||||
{ tool: 'hexagon', shape: 'geo' },
|
{ tool: 'hexagon', shape: 'geo' },
|
||||||
// { tool: 'octagon', shape: 'geo' },
|
// { tool: 'octagon', shape: 'geo' },
|
||||||
{ tool: 'star', shape: 'geo' },
|
{ tool: 'star', shape: 'geo' },
|
||||||
|
{ tool: 'heart', shape: 'geo' },
|
||||||
{ tool: 'rhombus', shape: 'geo' },
|
{ tool: 'rhombus', shape: 'geo' },
|
||||||
{ tool: 'oval', shape: 'geo' },
|
{ tool: 'oval', shape: 'geo' },
|
||||||
{ tool: 'trapezoid', shape: 'geo' },
|
// { tool: 'trapezoid', shape: 'geo' },
|
||||||
{ tool: 'arrow-right', shape: 'geo' },
|
{ tool: 'arrow-right', shape: 'geo' },
|
||||||
{ tool: 'arrow-left', shape: 'geo' },
|
{ tool: 'arrow-left', shape: 'geo' },
|
||||||
{ tool: 'arrow-up', shape: 'geo' },
|
{ tool: 'arrow-up', shape: 'geo' },
|
||||||
|
@ -47,7 +48,8 @@ const draggableShapeCreators = [
|
||||||
{ tool: 'star', shape: 'geo' },
|
{ tool: 'star', shape: 'geo' },
|
||||||
{ tool: 'rhombus', shape: 'geo' },
|
{ tool: 'rhombus', shape: 'geo' },
|
||||||
{ tool: 'oval', shape: 'geo' },
|
{ tool: 'oval', shape: 'geo' },
|
||||||
{ tool: 'trapezoid', shape: 'geo' },
|
{ tool: 'heart', shape: 'geo' },
|
||||||
|
// { tool: 'trapezoid', shape: 'geo' },
|
||||||
{ tool: 'arrow-right', shape: 'geo' },
|
{ tool: 'arrow-right', shape: 'geo' },
|
||||||
{ tool: 'arrow-left', shape: 'geo' },
|
{ tool: 'arrow-left', shape: 'geo' },
|
||||||
{ tool: 'arrow-up', shape: 'geo' },
|
{ tool: 'arrow-up', shape: 'geo' },
|
||||||
|
|
3
assets/icons/icon/geo-heart.svg
Normal file
3
assets/icons/icon/geo-heart.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2 10.1181C2 0.953581 14.025 0.953581 15 8.30928C15.975 0.953581 28 0.953581 28 10.1181C28 17.9561 18.25 20.9707 15 27C11.75 20.9707 2 17.9561 2 10.1181Z" stroke="#1D1D1D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 350 B |
|
@ -92,6 +92,7 @@ import iconsGeoCheckBox from './icons/icon/geo-check-box.svg'
|
||||||
import iconsGeoCloud from './icons/icon/geo-cloud.svg'
|
import iconsGeoCloud from './icons/icon/geo-cloud.svg'
|
||||||
import iconsGeoDiamond from './icons/icon/geo-diamond.svg'
|
import iconsGeoDiamond from './icons/icon/geo-diamond.svg'
|
||||||
import iconsGeoEllipse from './icons/icon/geo-ellipse.svg'
|
import iconsGeoEllipse from './icons/icon/geo-ellipse.svg'
|
||||||
|
import iconsGeoHeart from './icons/icon/geo-heart.svg'
|
||||||
import iconsGeoHexagon from './icons/icon/geo-hexagon.svg'
|
import iconsGeoHexagon from './icons/icon/geo-hexagon.svg'
|
||||||
import iconsGeoOctagon from './icons/icon/geo-octagon.svg'
|
import iconsGeoOctagon from './icons/icon/geo-octagon.svg'
|
||||||
import iconsGeoOval from './icons/icon/geo-oval.svg'
|
import iconsGeoOval from './icons/icon/geo-oval.svg'
|
||||||
|
@ -283,6 +284,7 @@ export function getAssetUrlsByImport(opts) {
|
||||||
'geo-cloud': formatAssetUrl(iconsGeoCloud, opts),
|
'geo-cloud': formatAssetUrl(iconsGeoCloud, opts),
|
||||||
'geo-diamond': formatAssetUrl(iconsGeoDiamond, opts),
|
'geo-diamond': formatAssetUrl(iconsGeoDiamond, opts),
|
||||||
'geo-ellipse': formatAssetUrl(iconsGeoEllipse, opts),
|
'geo-ellipse': formatAssetUrl(iconsGeoEllipse, opts),
|
||||||
|
'geo-heart': formatAssetUrl(iconsGeoHeart, opts),
|
||||||
'geo-hexagon': formatAssetUrl(iconsGeoHexagon, opts),
|
'geo-hexagon': formatAssetUrl(iconsGeoHexagon, opts),
|
||||||
'geo-octagon': formatAssetUrl(iconsGeoOctagon, opts),
|
'geo-octagon': formatAssetUrl(iconsGeoOctagon, opts),
|
||||||
'geo-oval': formatAssetUrl(iconsGeoOval, opts),
|
'geo-oval': formatAssetUrl(iconsGeoOval, opts),
|
||||||
|
|
|
@ -92,6 +92,7 @@ import iconsGeoCheckBox from './icons/icon/geo-check-box.svg?url'
|
||||||
import iconsGeoCloud from './icons/icon/geo-cloud.svg?url'
|
import iconsGeoCloud from './icons/icon/geo-cloud.svg?url'
|
||||||
import iconsGeoDiamond from './icons/icon/geo-diamond.svg?url'
|
import iconsGeoDiamond from './icons/icon/geo-diamond.svg?url'
|
||||||
import iconsGeoEllipse from './icons/icon/geo-ellipse.svg?url'
|
import iconsGeoEllipse from './icons/icon/geo-ellipse.svg?url'
|
||||||
|
import iconsGeoHeart from './icons/icon/geo-heart.svg?url'
|
||||||
import iconsGeoHexagon from './icons/icon/geo-hexagon.svg?url'
|
import iconsGeoHexagon from './icons/icon/geo-hexagon.svg?url'
|
||||||
import iconsGeoOctagon from './icons/icon/geo-octagon.svg?url'
|
import iconsGeoOctagon from './icons/icon/geo-octagon.svg?url'
|
||||||
import iconsGeoOval from './icons/icon/geo-oval.svg?url'
|
import iconsGeoOval from './icons/icon/geo-oval.svg?url'
|
||||||
|
@ -283,6 +284,7 @@ export function getAssetUrlsByImport(opts) {
|
||||||
'geo-cloud': formatAssetUrl(iconsGeoCloud, opts),
|
'geo-cloud': formatAssetUrl(iconsGeoCloud, opts),
|
||||||
'geo-diamond': formatAssetUrl(iconsGeoDiamond, opts),
|
'geo-diamond': formatAssetUrl(iconsGeoDiamond, opts),
|
||||||
'geo-ellipse': formatAssetUrl(iconsGeoEllipse, opts),
|
'geo-ellipse': formatAssetUrl(iconsGeoEllipse, opts),
|
||||||
|
'geo-heart': formatAssetUrl(iconsGeoHeart, opts),
|
||||||
'geo-hexagon': formatAssetUrl(iconsGeoHexagon, opts),
|
'geo-hexagon': formatAssetUrl(iconsGeoHexagon, opts),
|
||||||
'geo-octagon': formatAssetUrl(iconsGeoOctagon, opts),
|
'geo-octagon': formatAssetUrl(iconsGeoOctagon, opts),
|
||||||
'geo-oval': formatAssetUrl(iconsGeoOval, opts),
|
'geo-oval': formatAssetUrl(iconsGeoOval, opts),
|
||||||
|
|
|
@ -86,6 +86,7 @@ export function getAssetUrls(opts) {
|
||||||
'geo-cloud': formatAssetUrl('./icons/icon/geo-cloud.svg', opts),
|
'geo-cloud': formatAssetUrl('./icons/icon/geo-cloud.svg', opts),
|
||||||
'geo-diamond': formatAssetUrl('./icons/icon/geo-diamond.svg', opts),
|
'geo-diamond': formatAssetUrl('./icons/icon/geo-diamond.svg', opts),
|
||||||
'geo-ellipse': formatAssetUrl('./icons/icon/geo-ellipse.svg', opts),
|
'geo-ellipse': formatAssetUrl('./icons/icon/geo-ellipse.svg', opts),
|
||||||
|
'geo-heart': formatAssetUrl('./icons/icon/geo-heart.svg', opts),
|
||||||
'geo-hexagon': formatAssetUrl('./icons/icon/geo-hexagon.svg', opts),
|
'geo-hexagon': formatAssetUrl('./icons/icon/geo-hexagon.svg', opts),
|
||||||
'geo-octagon': formatAssetUrl('./icons/icon/geo-octagon.svg', opts),
|
'geo-octagon': formatAssetUrl('./icons/icon/geo-octagon.svg', opts),
|
||||||
'geo-oval': formatAssetUrl('./icons/icon/geo-oval.svg', opts),
|
'geo-oval': formatAssetUrl('./icons/icon/geo-oval.svg', opts),
|
||||||
|
|
1
packages/assets/types.d.ts
vendored
1
packages/assets/types.d.ts
vendored
|
@ -76,6 +76,7 @@ export type AssetUrls = {
|
||||||
'geo-cloud': string
|
'geo-cloud': string
|
||||||
'geo-diamond': string
|
'geo-diamond': string
|
||||||
'geo-ellipse': string
|
'geo-ellipse': string
|
||||||
|
'geo-heart': string
|
||||||
'geo-hexagon': string
|
'geo-hexagon': string
|
||||||
'geo-octagon': string
|
'geo-octagon': string
|
||||||
'geo-oval': string
|
'geo-oval': string
|
||||||
|
|
|
@ -257,6 +257,10 @@ export function getAssetUrlsByMetaUrl(opts) {
|
||||||
new URL('./icons/icon/geo-ellipse.svg', import.meta.url).href,
|
new URL('./icons/icon/geo-ellipse.svg', import.meta.url).href,
|
||||||
opts
|
opts
|
||||||
),
|
),
|
||||||
|
'geo-heart': formatAssetUrl(
|
||||||
|
new URL('./icons/icon/geo-heart.svg', import.meta.url).href,
|
||||||
|
opts
|
||||||
|
),
|
||||||
'geo-hexagon': formatAssetUrl(
|
'geo-hexagon': formatAssetUrl(
|
||||||
new URL('./icons/icon/geo-hexagon.svg', import.meta.url).href,
|
new URL('./icons/icon/geo-hexagon.svg', import.meta.url).href,
|
||||||
opts
|
opts
|
||||||
|
|
|
@ -112,7 +112,6 @@ export class Arc2d extends Geometry2d {
|
||||||
center: Vec;
|
center: Vec;
|
||||||
end: Vec;
|
end: Vec;
|
||||||
largeArcFlag: number;
|
largeArcFlag: number;
|
||||||
radius: number;
|
|
||||||
start: Vec;
|
start: Vec;
|
||||||
sweepFlag: number;
|
sweepFlag: number;
|
||||||
});
|
});
|
||||||
|
@ -125,11 +124,15 @@ export class Arc2d extends Geometry2d {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
end: Vec;
|
end: Vec;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
getLength(): number;
|
||||||
|
// (undocumented)
|
||||||
|
getSvgPathData(first?: boolean): string;
|
||||||
|
// (undocumented)
|
||||||
getVertices(): Vec[];
|
getVertices(): Vec[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
hitTestLineSegment(A: Vec, B: Vec): boolean;
|
hitTestLineSegment(A: Vec, B: Vec): boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
length: number;
|
largeArcFlag: number;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
measure: number;
|
measure: number;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -138,6 +141,8 @@ export class Arc2d extends Geometry2d {
|
||||||
radius: number;
|
radius: number;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
start: Vec;
|
start: Vec;
|
||||||
|
// (undocumented)
|
||||||
|
sweepFlag: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
|
@ -392,6 +397,9 @@ export const CAMERA_SLIDE_FRICTION = 0.09;
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function canonicalizeRotation(a: number): number;
|
export function canonicalizeRotation(a: number): number;
|
||||||
|
|
||||||
|
// @public
|
||||||
|
export function centerOfCircleFromThreePoints(a: VecLike, b: VecLike, c: VecLike): Vec;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export class Circle2d extends Geometry2d {
|
export class Circle2d extends Geometry2d {
|
||||||
constructor(config: Omit<Geometry2dOptions, 'isClosed'> & {
|
constructor(config: Omit<Geometry2dOptions, 'isClosed'> & {
|
||||||
|
@ -412,6 +420,8 @@ export class Circle2d extends Geometry2d {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getBounds(): Box;
|
getBounds(): Box;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
getSvgPathData(): string;
|
||||||
|
// (undocumented)
|
||||||
getVertices(): Vec[];
|
getVertices(): Vec[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
hitTestLineSegment(A: Vec, B: Vec, distance?: number): boolean;
|
hitTestLineSegment(A: Vec, B: Vec, distance?: number): boolean;
|
||||||
|
@ -481,6 +491,12 @@ export class CubicBezier2d extends Polyline2d {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
d: Vec;
|
d: Vec;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
static GetAtT(segment: CubicBezier2d, t: number): Vec;
|
||||||
|
// (undocumented)
|
||||||
|
getLength(precision?: number): number;
|
||||||
|
// (undocumented)
|
||||||
|
getSvgPathData(first?: boolean): string;
|
||||||
|
// (undocumented)
|
||||||
getVertices(): Vec[];
|
getVertices(): Vec[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
midPoint(): Vec;
|
midPoint(): Vec;
|
||||||
|
@ -494,14 +510,14 @@ export class CubicSpline2d extends Geometry2d {
|
||||||
points: Vec[];
|
points: Vec[];
|
||||||
});
|
});
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
getLength(): number;
|
||||||
|
// (undocumented)
|
||||||
|
getSvgPathData(): string;
|
||||||
|
// (undocumented)
|
||||||
getVertices(): Vec[];
|
getVertices(): Vec[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
hitTestLineSegment(A: Vec, B: Vec): boolean;
|
hitTestLineSegment(A: Vec, B: Vec): boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
get length(): number;
|
|
||||||
// (undocumented)
|
|
||||||
_length?: number;
|
|
||||||
// (undocumented)
|
|
||||||
nearestPoint(A: Vec): Vec;
|
nearestPoint(A: Vec): Vec;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
points: Vec[];
|
points: Vec[];
|
||||||
|
@ -646,14 +662,14 @@ export class Edge2d extends Geometry2d {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
end: Vec;
|
end: Vec;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
getLength(): number;
|
||||||
|
// (undocumented)
|
||||||
|
getSvgPathData(first?: boolean): string;
|
||||||
|
// (undocumented)
|
||||||
getVertices(): Vec[];
|
getVertices(): Vec[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
hitTestLineSegment(A: Vec, B: Vec, distance?: number): boolean;
|
hitTestLineSegment(A: Vec, B: Vec, distance?: number): boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
get length(): number;
|
|
||||||
// (undocumented)
|
|
||||||
_length?: number;
|
|
||||||
// (undocumented)
|
|
||||||
midPoint(): Vec;
|
midPoint(): Vec;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
nearestPoint(point: Vec): Vec;
|
nearestPoint(point: Vec): Vec;
|
||||||
|
@ -1104,6 +1120,10 @@ export class Ellipse2d extends Geometry2d {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getBounds(): Box;
|
getBounds(): Box;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
getLength(): number;
|
||||||
|
// (undocumented)
|
||||||
|
getSvgPathData(first?: boolean): string;
|
||||||
|
// (undocumented)
|
||||||
getVertices(): any[];
|
getVertices(): any[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
h: number;
|
h: number;
|
||||||
|
@ -1180,6 +1200,10 @@ export abstract class Geometry2d {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getBounds(): Box;
|
getBounds(): Box;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
getLength(): number;
|
||||||
|
// (undocumented)
|
||||||
|
abstract getSvgPathData(first: boolean): string;
|
||||||
|
// (undocumented)
|
||||||
abstract getVertices(): Vec[];
|
abstract getVertices(): Vec[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
hitTestLineSegment(A: Vec, B: Vec, distance?: number): boolean;
|
hitTestLineSegment(A: Vec, B: Vec, distance?: number): boolean;
|
||||||
|
@ -1196,6 +1220,8 @@ export abstract class Geometry2d {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
isPointInBounds(point: Vec, margin?: number): boolean;
|
isPointInBounds(point: Vec, margin?: number): boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
get length(): number;
|
||||||
|
// (undocumented)
|
||||||
abstract nearestPoint(point: Vec): Vec;
|
abstract nearestPoint(point: Vec): Vec;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
nearestPointOnLineSegment(A: Vec, B: Vec): Vec;
|
nearestPointOnLineSegment(A: Vec, B: Vec): Vec;
|
||||||
|
@ -1238,6 +1264,9 @@ export function getPointInArcT(mAB: number, A: number, B: number, P: number): nu
|
||||||
// @public
|
// @public
|
||||||
export function getPointOnCircle(center: VecLike, r: number, a: number): Vec;
|
export function getPointOnCircle(center: VecLike, r: number, a: number): Vec;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export function getPointsOnArc(startPoint: VecLike, endPoint: VecLike, center: null | VecLike, radius: number, numPoints: number): Vec[];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function getPolygonVertices(width: number, height: number, sides: number): Vec[];
|
export function getPolygonVertices(width: number, height: number, sides: number): Vec[];
|
||||||
|
|
||||||
|
@ -1271,6 +1300,10 @@ export class Group2d extends Geometry2d {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getArea(): number;
|
getArea(): number;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
getLength(): number;
|
||||||
|
// (undocumented)
|
||||||
|
getSvgPathData(): string;
|
||||||
|
// (undocumented)
|
||||||
getVertices(): Vec[];
|
getVertices(): Vec[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
hitTestLineSegment(A: Vec, B: Vec, zoom: number): boolean;
|
hitTestLineSegment(A: Vec, B: Vec, zoom: number): boolean;
|
||||||
|
@ -1599,6 +1632,8 @@ export class Point2d extends Geometry2d {
|
||||||
point: Vec;
|
point: Vec;
|
||||||
});
|
});
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
getSvgPathData(): string;
|
||||||
|
// (undocumented)
|
||||||
getVertices(): Vec[];
|
getVertices(): Vec[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
hitTestLineSegment(A: Vec, B: Vec, margin: number): boolean;
|
hitTestLineSegment(A: Vec, B: Vec, margin: number): boolean;
|
||||||
|
@ -1640,14 +1675,14 @@ export class Polyline2d extends Geometry2d {
|
||||||
points: Vec[];
|
points: Vec[];
|
||||||
});
|
});
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
getLength(): number;
|
||||||
|
// (undocumented)
|
||||||
|
getSvgPathData(): string;
|
||||||
|
// (undocumented)
|
||||||
getVertices(): Vec[];
|
getVertices(): Vec[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
hitTestLineSegment(A: Vec, B: Vec, distance?: number): boolean;
|
hitTestLineSegment(A: Vec, B: Vec, distance?: number): boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
get length(): number;
|
|
||||||
// (undocumented)
|
|
||||||
_length?: number;
|
|
||||||
// (undocumented)
|
|
||||||
nearestPoint(A: Vec): Vec;
|
nearestPoint(A: Vec): Vec;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
points: Vec[];
|
points: Vec[];
|
||||||
|
@ -1705,6 +1740,8 @@ export class Rectangle2d extends Polygon2d {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getBounds(): Box;
|
getBounds(): Box;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
getSvgPathData(): string;
|
||||||
|
// (undocumented)
|
||||||
h: number;
|
h: number;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
w: number;
|
w: number;
|
||||||
|
@ -1917,18 +1954,40 @@ export class SnapManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export class Stadium2d extends Ellipse2d {
|
export class Stadium2d extends Geometry2d {
|
||||||
constructor(config: Omit<Geometry2dOptions, 'isClosed'> & {
|
constructor(config: Omit<Geometry2dOptions, 'isClosed'> & {
|
||||||
height: number;
|
height: number;
|
||||||
width: number;
|
width: number;
|
||||||
});
|
});
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
a: Arc2d;
|
||||||
|
// (undocumented)
|
||||||
|
b: Edge2d;
|
||||||
|
// (undocumented)
|
||||||
|
c: Arc2d;
|
||||||
|
// (undocumented)
|
||||||
config: Omit<Geometry2dOptions, 'isClosed'> & {
|
config: Omit<Geometry2dOptions, 'isClosed'> & {
|
||||||
height: number;
|
height: number;
|
||||||
width: number;
|
width: number;
|
||||||
};
|
};
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
d: Edge2d;
|
||||||
|
// (undocumented)
|
||||||
|
getBounds(): Box;
|
||||||
|
// (undocumented)
|
||||||
|
getLength(): number;
|
||||||
|
// (undocumented)
|
||||||
|
getSvgPathData(): string;
|
||||||
|
// (undocumented)
|
||||||
getVertices(): Vec[];
|
getVertices(): Vec[];
|
||||||
|
// (undocumented)
|
||||||
|
h: number;
|
||||||
|
// (undocumented)
|
||||||
|
hitTestLineSegment(A: Vec, B: Vec): boolean;
|
||||||
|
// (undocumented)
|
||||||
|
nearestPoint(A: Vec): Vec;
|
||||||
|
// (undocumented)
|
||||||
|
w: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
|
@ -3154,10 +3213,14 @@ export class Vec {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
toArray(): number[];
|
toArray(): number[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
static ToFixed(A: VecLike, n?: number): Vec;
|
static ToCss(A: VecLike): string;
|
||||||
|
// (undocumented)
|
||||||
|
static ToFixed(A: VecLike): Vec;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
toFixed(): Vec;
|
toFixed(): Vec;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
static ToInt(A: VecLike): Vec;
|
||||||
|
// (undocumented)
|
||||||
static ToJson(A: VecLike): {
|
static ToJson(A: VecLike): {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
|
|
@ -297,6 +297,7 @@ export {
|
||||||
areAnglesCompatible,
|
areAnglesCompatible,
|
||||||
average,
|
average,
|
||||||
canonicalizeRotation,
|
canonicalizeRotation,
|
||||||
|
centerOfCircleFromThreePoints,
|
||||||
clamp,
|
clamp,
|
||||||
clampRadians,
|
clampRadians,
|
||||||
clockwiseAngleDist,
|
clockwiseAngleDist,
|
||||||
|
@ -305,6 +306,7 @@ export {
|
||||||
getArcMeasure,
|
getArcMeasure,
|
||||||
getPointInArcT,
|
getPointInArcT,
|
||||||
getPointOnCircle,
|
getPointOnCircle,
|
||||||
|
getPointsOnArc,
|
||||||
getPolygonVertices,
|
getPolygonVertices,
|
||||||
isSafeFloat,
|
isSafeFloat,
|
||||||
perimeterOfEllipse,
|
perimeterOfEllipse,
|
||||||
|
|
|
@ -253,8 +253,7 @@ describe('Vec.IsClockwise', () => {
|
||||||
|
|
||||||
describe('Vec.ToFixed', () => {
|
describe('Vec.ToFixed', () => {
|
||||||
it('Rounds a vector to the a given precision.', () => {
|
it('Rounds a vector to the a given precision.', () => {
|
||||||
expect(Vec.ToFixed(new Vec(1.2345, 5.678), 1)).toMatchObject(new Vec(1.2, 5.7))
|
expect(Vec.ToFixed(new Vec(1.2345, 5.678))).toMatchObject(new Vec(1.23, 5.68))
|
||||||
expect(Vec.ToFixed(new Vec(1.2345, 5.678), 2)).toMatchObject(new Vec(1.23, 5.68))
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { VecModel } from '@tldraw/tlschema'
|
import { VecModel } from '@tldraw/tlschema'
|
||||||
import { EASINGS } from './easings'
|
import { EASINGS } from './easings'
|
||||||
|
import { toFixed } from './utils'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type VecLike = Vec | VecModel
|
export type VecLike = Vec | VecModel
|
||||||
|
@ -504,8 +505,20 @@ export class Vec {
|
||||||
return Vec.Sub(A, origin).mul(scale).add(origin)
|
return Vec.Sub(A, origin).mul(scale).add(origin)
|
||||||
}
|
}
|
||||||
|
|
||||||
static ToFixed(A: VecLike, n = 2) {
|
static ToFixed(A: VecLike) {
|
||||||
return new Vec(+A.x.toFixed(n), +A.y.toFixed(n), +A.z!.toFixed(n))
|
return new Vec(toFixed(A.x), toFixed(A.y))
|
||||||
|
}
|
||||||
|
|
||||||
|
static ToInt(A: VecLike) {
|
||||||
|
return new Vec(
|
||||||
|
parseInt(A.x.toFixed(0)),
|
||||||
|
parseInt(A.y.toFixed(0)),
|
||||||
|
parseInt((A.z ?? 0).toFixed(0))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static ToCss(A: VecLike) {
|
||||||
|
return `${A.x},${A.y}`
|
||||||
}
|
}
|
||||||
|
|
||||||
static Nudge(A: VecLike, B: VecLike, distance: number) {
|
static Nudge(A: VecLike, B: VecLike, distance: number) {
|
||||||
|
|
|
@ -10,16 +10,16 @@ export class Arc2d extends Geometry2d {
|
||||||
radius: number
|
radius: number
|
||||||
start: Vec
|
start: Vec
|
||||||
end: Vec
|
end: Vec
|
||||||
|
largeArcFlag: number
|
||||||
|
sweepFlag: number
|
||||||
|
|
||||||
measure: number
|
measure: number
|
||||||
length: number
|
|
||||||
angleStart: number
|
angleStart: number
|
||||||
angleEnd: number
|
angleEnd: number
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
config: Omit<Geometry2dOptions, 'isFilled' | 'isClosed'> & {
|
config: Omit<Geometry2dOptions, 'isFilled' | 'isClosed'> & {
|
||||||
center: Vec
|
center: Vec
|
||||||
radius: number
|
|
||||||
start: Vec
|
start: Vec
|
||||||
end: Vec
|
end: Vec
|
||||||
sweepFlag: number
|
sweepFlag: number
|
||||||
|
@ -27,20 +27,21 @@ export class Arc2d extends Geometry2d {
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
super({ ...config, isFilled: false, isClosed: false })
|
super({ ...config, isFilled: false, isClosed: false })
|
||||||
const { center, radius, sweepFlag, largeArcFlag, start, end } = config
|
const { center, sweepFlag, largeArcFlag, start, end } = config
|
||||||
if (start.equals(end)) throw Error(`Arc must have different start and end points.`)
|
if (start.equals(end)) throw Error(`Arc must have different start and end points.`)
|
||||||
|
|
||||||
// ensure that the start and end are clockwise
|
// ensure that the start and end are clockwise
|
||||||
this.angleStart = Vec.Angle(center, start)
|
this.angleStart = Vec.Angle(center, start)
|
||||||
this.angleEnd = Vec.Angle(center, end)
|
this.angleEnd = Vec.Angle(center, end)
|
||||||
|
this.radius = Vec.Dist(center, start)
|
||||||
this.measure = getArcMeasure(this.angleStart, this.angleEnd, sweepFlag, largeArcFlag)
|
this.measure = getArcMeasure(this.angleStart, this.angleEnd, sweepFlag, largeArcFlag)
|
||||||
this.length = this.measure * radius
|
|
||||||
|
|
||||||
this.start = start
|
this.start = start
|
||||||
this.end = end
|
this.end = end
|
||||||
|
|
||||||
|
this.sweepFlag = sweepFlag
|
||||||
|
this.largeArcFlag = largeArcFlag
|
||||||
this._center = center
|
this._center = center
|
||||||
this.radius = radius
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nearestPoint(point: Vec): Vec {
|
nearestPoint(point: Vec): Vec {
|
||||||
|
@ -80,13 +81,20 @@ export class Arc2d extends Geometry2d {
|
||||||
getVertices(): Vec[] {
|
getVertices(): Vec[] {
|
||||||
const { _center, measure, length, radius, angleStart } = this
|
const { _center, measure, length, radius, angleStart } = this
|
||||||
const vertices: Vec[] = []
|
const vertices: Vec[] = []
|
||||||
|
|
||||||
for (let i = 0, n = getVerticesCountForLength(Math.abs(length)); i < n + 1; i++) {
|
for (let i = 0, n = getVerticesCountForLength(Math.abs(length)); i < n + 1; i++) {
|
||||||
const t = (i / n) * measure
|
const t = (i / n) * measure
|
||||||
const angle = angleStart + t
|
const angle = angleStart + t
|
||||||
vertices.push(getPointOnCircle(_center, radius, angle))
|
vertices.push(getPointOnCircle(_center, radius, angle))
|
||||||
}
|
}
|
||||||
|
|
||||||
return vertices
|
return vertices
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSvgPathData(first = true) {
|
||||||
|
const { start, end, radius, largeArcFlag, sweepFlag } = this
|
||||||
|
return `${first ? `M${start.toFixed()}` : ``} A${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${end.toFixed()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
override getLength() {
|
||||||
|
return this.measure * this.radius
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,4 +53,9 @@ export class Circle2d extends Geometry2d {
|
||||||
const { _center, radius } = this
|
const { _center, radius } = this
|
||||||
return intersectLineSegmentCircle(A, B, _center, radius + distance) !== null
|
return intersectLineSegmentCircle(A, B, _center, radius + distance) !== null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSvgPathData(): string {
|
||||||
|
const { _center, radius } = this
|
||||||
|
return `M${_center.x + radius},${_center.y} a${radius},${radius} 0 1,0 ${radius * 2},0a${radius},${radius} 0 1,0 -${radius * 2},0`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,7 @@ export class CubicBezier2d extends Polyline2d {
|
||||||
}
|
}
|
||||||
|
|
||||||
midPoint() {
|
midPoint() {
|
||||||
return getAtT(this, 0.5)
|
return CubicBezier2d.GetAtT(this, 0.5)
|
||||||
}
|
}
|
||||||
|
|
||||||
nearestPoint(A: Vec): Vec {
|
nearestPoint(A: Vec): Vec {
|
||||||
|
@ -69,9 +69,13 @@ export class CubicBezier2d extends Polyline2d {
|
||||||
if (!nearest) throw Error('nearest point not found')
|
if (!nearest) throw Error('nearest point not found')
|
||||||
return nearest
|
return nearest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSvgPathData(first = true) {
|
||||||
|
const { a, b, c, d } = this
|
||||||
|
return `${first ? `M ${a.toFixed()} ` : ``} C${b.toFixed()} ${c.toFixed()} ${d.toFixed()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAtT(segment: CubicBezier2d, t: number) {
|
static GetAtT(segment: CubicBezier2d, t: number) {
|
||||||
const { a, b, c, d } = segment
|
const { a, b, c, d } = segment
|
||||||
return new Vec(
|
return new Vec(
|
||||||
(1 - t) * (1 - t) * (1 - t) * a.x +
|
(1 - t) * (1 - t) * (1 - t) * a.x +
|
||||||
|
@ -84,3 +88,16 @@ function getAtT(segment: CubicBezier2d, t: number) {
|
||||||
t * t * t * d.y
|
t * t * t * d.y
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override getLength(precision = 32) {
|
||||||
|
let n1: Vec,
|
||||||
|
p1 = this.a,
|
||||||
|
length = 0
|
||||||
|
for (let i = 1; i <= precision; i++) {
|
||||||
|
n1 = CubicBezier2d.GetAtT(this, i / precision)
|
||||||
|
length += Vec.Dist(p1, n1)
|
||||||
|
p1 = n1
|
||||||
|
}
|
||||||
|
return length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -46,14 +46,8 @@ export class CubicSpline2d extends Geometry2d {
|
||||||
return this._segments
|
return this._segments
|
||||||
}
|
}
|
||||||
|
|
||||||
_length?: number
|
override getLength() {
|
||||||
|
return this.segments.reduce((acc, segment) => acc + segment.length, 0)
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
|
||||||
get length() {
|
|
||||||
if (!this._length) {
|
|
||||||
this._length = this.segments.reduce((acc, segment) => acc + segment.length, 0)
|
|
||||||
}
|
|
||||||
return this._length
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getVertices() {
|
getVertices() {
|
||||||
|
@ -84,4 +78,16 @@ export class CubicSpline2d extends Geometry2d {
|
||||||
hitTestLineSegment(A: Vec, B: Vec): boolean {
|
hitTestLineSegment(A: Vec, B: Vec): boolean {
|
||||||
return this.segments.some((segment) => segment.hitTestLineSegment(A, B))
|
return this.segments.some((segment) => segment.hitTestLineSegment(A, B))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSvgPathData() {
|
||||||
|
let d = this.segments.reduce((d, segment, i) => {
|
||||||
|
return d + segment.getSvgPathData(i === 0)
|
||||||
|
}, '')
|
||||||
|
|
||||||
|
if (this.isClosed) {
|
||||||
|
d += 'Z'
|
||||||
|
}
|
||||||
|
|
||||||
|
return d
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,15 +22,9 @@ export class Edge2d extends Geometry2d {
|
||||||
this.ul = this.u.len() // the length of the unit vector
|
this.ul = this.u.len() // the length of the unit vector
|
||||||
}
|
}
|
||||||
|
|
||||||
_length?: number
|
override getLength() {
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
|
||||||
get length() {
|
|
||||||
if (!this._length) {
|
|
||||||
return this.d.len()
|
return this.d.len()
|
||||||
}
|
}
|
||||||
return this._length
|
|
||||||
}
|
|
||||||
|
|
||||||
midPoint(): Vec {
|
midPoint(): Vec {
|
||||||
return this.start.lrp(this.end, 0.5)
|
return this.start.lrp(this.end, 0.5)
|
||||||
|
@ -58,4 +52,9 @@ export class Edge2d extends Geometry2d {
|
||||||
linesIntersect(A, B, this.start, this.end) || this.distanceToLineSegment(A, B) <= distance
|
linesIntersect(A, B, this.start, this.end) || this.distanceToLineSegment(A, B) <= distance
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSvgPathData(first = true) {
|
||||||
|
const { start, end } = this
|
||||||
|
return `${first ? `M${start.toFixed()}` : ``} L${end.toFixed()}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Box } from '../Box'
|
import { Box } from '../Box'
|
||||||
import { Vec } from '../Vec'
|
import { Vec } from '../Vec'
|
||||||
import { PI, PI2 } from '../utils'
|
import { PI, PI2, perimeterOfEllipse } from '../utils'
|
||||||
import { Edge2d } from './Edge2d'
|
import { Edge2d } from './Edge2d'
|
||||||
import { Geometry2d, Geometry2dOptions } from './Geometry2d'
|
import { Geometry2d, Geometry2dOptions } from './Geometry2d'
|
||||||
import { getVerticesCountForLength } from './geometry-constants'
|
import { getVerticesCountForLength } from './geometry-constants'
|
||||||
|
@ -97,4 +97,22 @@ export class Ellipse2d extends Geometry2d {
|
||||||
getBounds() {
|
getBounds() {
|
||||||
return new Box(0, 0, this.w, this.h)
|
return new Box(0, 0, this.w, this.h)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLength(): number {
|
||||||
|
const { w, h } = this
|
||||||
|
const cx = w / 2
|
||||||
|
const cy = h / 2
|
||||||
|
const rx = Math.max(0, cx)
|
||||||
|
const ry = Math.max(0, cy)
|
||||||
|
return perimeterOfEllipse(rx, ry)
|
||||||
|
}
|
||||||
|
|
||||||
|
getSvgPathData(first = false) {
|
||||||
|
const { w, h } = this
|
||||||
|
const cx = w / 2
|
||||||
|
const cy = h / 2
|
||||||
|
const rx = Math.max(0, cx)
|
||||||
|
const ry = Math.max(0, cy)
|
||||||
|
return `${first ? `M${cx - rx},${cy}` : ``} a${rx},${ry},0,1,1,${rx * 2},0a${rx},${ry},0,1,1,-${rx * 2},0`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -178,4 +178,28 @@ export abstract class Geometry2d {
|
||||||
|
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _length?: number
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
get length() {
|
||||||
|
if (this._length) return this._length
|
||||||
|
this._length = this.getLength()
|
||||||
|
return this._length
|
||||||
|
}
|
||||||
|
|
||||||
|
getLength() {
|
||||||
|
const { vertices } = this
|
||||||
|
let n1: Vec,
|
||||||
|
p1 = vertices[0],
|
||||||
|
length = 0
|
||||||
|
for (let i = 1; i < vertices.length; i++) {
|
||||||
|
n1 = vertices[i]
|
||||||
|
length += Vec.Dist2(p1, n1)
|
||||||
|
p1 = n1
|
||||||
|
}
|
||||||
|
return Math.sqrt(length)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract getSvgPathData(first: boolean): string
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,4 +96,12 @@ export class Group2d extends Geometry2d {
|
||||||
}
|
}
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLength(): number {
|
||||||
|
return this.children.reduce((a, c) => (c.isLabel ? a : a + c.length), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
getSvgPathData(): string {
|
||||||
|
return this.children.map((c, i) => (c.isLabel ? '' : c.getSvgPathData(i === 0))).join(' ')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,4 +25,9 @@ export class Point2d extends Geometry2d {
|
||||||
hitTestLineSegment(A: Vec, B: Vec, margin: number): boolean {
|
hitTestLineSegment(A: Vec, B: Vec, margin: number): boolean {
|
||||||
return Vec.DistanceToLineSegment(A, B, this.point) < margin
|
return Vec.DistanceToLineSegment(A, B, this.point) < margin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSvgPathData() {
|
||||||
|
const { point } = this
|
||||||
|
return `M${point.toFixed()}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,14 +33,8 @@ export class Polyline2d extends Geometry2d {
|
||||||
return this._segments
|
return this._segments
|
||||||
}
|
}
|
||||||
|
|
||||||
_length?: number
|
override getLength() {
|
||||||
|
return this.segments.reduce((acc, segment) => acc + segment.length, 0)
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
|
||||||
get length() {
|
|
||||||
if (!this._length) {
|
|
||||||
this._length = this.segments.reduce((acc, segment) => acc + segment.length, 0)
|
|
||||||
}
|
|
||||||
return this._length
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getVertices() {
|
getVertices() {
|
||||||
|
@ -74,4 +68,13 @@ export class Polyline2d extends Geometry2d {
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSvgPathData(): string {
|
||||||
|
const { vertices } = this
|
||||||
|
if (vertices.length < 2) return ''
|
||||||
|
return vertices.reduce((acc, vertex, i) => {
|
||||||
|
if (i === 0) return `M ${vertex.x} ${vertex.y}`
|
||||||
|
return `${acc} L ${vertex.x} ${vertex.y}`
|
||||||
|
}, '')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,4 +37,9 @@ export class Rectangle2d extends Polygon2d {
|
||||||
getBounds() {
|
getBounds() {
|
||||||
return new Box(this.x, this.y, this.w, this.h)
|
return new Box(this.x, this.y, this.w, this.h)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSvgPathData(): string {
|
||||||
|
const { x, y, w, h } = this
|
||||||
|
return `M${x},${y} h${w} v${h} h-${w}z`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,47 +1,114 @@
|
||||||
|
import { Box } from '../Box'
|
||||||
import { Vec } from '../Vec'
|
import { Vec } from '../Vec'
|
||||||
import { HALF_PI, PI } from '../utils'
|
import { PI } from '../utils'
|
||||||
import { Ellipse2d } from './Ellipse2d'
|
import { Arc2d } from './Arc2d'
|
||||||
import { Geometry2dOptions } from './Geometry2d'
|
import { Edge2d } from './Edge2d'
|
||||||
|
import { Geometry2d, Geometry2dOptions } from './Geometry2d'
|
||||||
const STADIUM_VERTICES_LENGTH = 18
|
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export class Stadium2d extends Ellipse2d {
|
export class Stadium2d extends Geometry2d {
|
||||||
|
w: number
|
||||||
|
h: number
|
||||||
|
|
||||||
|
a: Arc2d
|
||||||
|
b: Edge2d
|
||||||
|
c: Arc2d
|
||||||
|
d: Edge2d
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public config: Omit<Geometry2dOptions, 'isClosed'> & { width: number; height: number }
|
public config: Omit<Geometry2dOptions, 'isClosed'> & {
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
super({ ...config })
|
super({ ...config, isClosed: true })
|
||||||
|
const { width: w, height: h } = config
|
||||||
|
this.w = w
|
||||||
|
this.h = h
|
||||||
|
|
||||||
|
if (h > w) {
|
||||||
|
const r = w / 2
|
||||||
|
this.a = new Arc2d({
|
||||||
|
start: new Vec(0, r),
|
||||||
|
end: new Vec(w, r),
|
||||||
|
center: new Vec(w / 2, r),
|
||||||
|
sweepFlag: 1,
|
||||||
|
largeArcFlag: 1,
|
||||||
|
})
|
||||||
|
this.b = new Edge2d({ start: new Vec(w, r), end: new Vec(w, h - r) })
|
||||||
|
this.c = new Arc2d({
|
||||||
|
start: new Vec(w, h - r),
|
||||||
|
end: new Vec(0, h - r),
|
||||||
|
center: new Vec(w / 2, h - r),
|
||||||
|
sweepFlag: 1,
|
||||||
|
largeArcFlag: 1,
|
||||||
|
})
|
||||||
|
this.d = new Edge2d({ start: new Vec(0, h - r), end: new Vec(0, r) })
|
||||||
|
} else {
|
||||||
|
const r = h / 2
|
||||||
|
this.a = new Arc2d({
|
||||||
|
start: new Vec(r, h),
|
||||||
|
end: new Vec(r, 0),
|
||||||
|
center: new Vec(r, r),
|
||||||
|
sweepFlag: 1,
|
||||||
|
largeArcFlag: 1,
|
||||||
|
})
|
||||||
|
this.b = new Edge2d({ start: new Vec(r, 0), end: new Vec(w - r, 0) })
|
||||||
|
this.c = new Arc2d({
|
||||||
|
start: new Vec(w - r, 0),
|
||||||
|
end: new Vec(w - r, h),
|
||||||
|
center: new Vec(w - r, r),
|
||||||
|
sweepFlag: 1,
|
||||||
|
largeArcFlag: 1,
|
||||||
|
})
|
||||||
|
this.d = new Edge2d({ start: new Vec(w - r, h), end: new Vec(r, h) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nearestPoint(A: Vec): Vec {
|
||||||
|
let nearest: Vec | undefined
|
||||||
|
let dist = Infinity
|
||||||
|
let _d: number
|
||||||
|
let p: Vec
|
||||||
|
|
||||||
|
const { a, b, c, d } = this
|
||||||
|
for (const part of [a, b, c, d]) {
|
||||||
|
p = part.nearestPoint(A)
|
||||||
|
_d = Vec.Dist2(p, A)
|
||||||
|
if (_d < dist) {
|
||||||
|
nearest = p
|
||||||
|
dist = _d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!nearest) throw Error('nearest point not found')
|
||||||
|
return nearest
|
||||||
|
}
|
||||||
|
|
||||||
|
hitTestLineSegment(A: Vec, B: Vec): boolean {
|
||||||
|
const { a, b, c, d } = this
|
||||||
|
return [a, b, c, d].some((edge) => edge.hitTestLineSegment(A, B))
|
||||||
}
|
}
|
||||||
|
|
||||||
getVertices() {
|
getVertices() {
|
||||||
const w = Math.max(1, this.w)
|
const { a, b, c, d } = this
|
||||||
const h = Math.max(1, this.h)
|
return [a, b, c, d].reduce<Vec[]>((a, p) => {
|
||||||
const cx = w / 2
|
a.push(...p.vertices)
|
||||||
const cy = h / 2
|
return a
|
||||||
const points: Vec[] = Array(STADIUM_VERTICES_LENGTH)
|
}, [])
|
||||||
let t1: number, t2: number
|
|
||||||
if (h > w) {
|
|
||||||
for (let i = 0; i < STADIUM_VERTICES_LENGTH - 1; i++) {
|
|
||||||
t1 = -PI + (PI * i) / (STADIUM_VERTICES_LENGTH - 2)
|
|
||||||
t2 = (PI * i) / (STADIUM_VERTICES_LENGTH - 2)
|
|
||||||
points[i] = new Vec(cx + cx * Math.cos(t1), cx + cx * Math.sin(t1))
|
|
||||||
points[i + (STADIUM_VERTICES_LENGTH - 1)] = new Vec(
|
|
||||||
cx + cx * Math.cos(t2),
|
|
||||||
h - cx + cx * Math.sin(t2)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (let i = 0; i < STADIUM_VERTICES_LENGTH - 1; i++) {
|
|
||||||
t1 = -HALF_PI + (PI * i) / (STADIUM_VERTICES_LENGTH - 2)
|
|
||||||
t2 = HALF_PI + (PI * -i) / (STADIUM_VERTICES_LENGTH - 2)
|
|
||||||
points[i] = new Vec(w - cy + cy * Math.cos(t1), h - cy + cy * Math.sin(t1))
|
|
||||||
points[i + (STADIUM_VERTICES_LENGTH - 1)] = new Vec(
|
|
||||||
cy - cy * Math.cos(t2),
|
|
||||||
h - cy + cy * Math.sin(t2)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return points
|
getBounds() {
|
||||||
|
return new Box(0, 0, this.w, this.h)
|
||||||
|
}
|
||||||
|
|
||||||
|
getLength() {
|
||||||
|
const { h, w } = this
|
||||||
|
if (h > w) return (PI * (w / 2) + (h - w)) * 2
|
||||||
|
else return (PI * (h / 2) + (w - h)) * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
getSvgPathData() {
|
||||||
|
const { a, b, c, d } = this
|
||||||
|
return [a, b, c, d].map((p, i) => p.getSvgPathData(i === 0)).join(' ') + ' Z'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,8 +86,7 @@ export function approximately(a: number, b: number, precision = 0.000001) {
|
||||||
*/
|
*/
|
||||||
export function perimeterOfEllipse(rx: number, ry: number): number {
|
export function perimeterOfEllipse(rx: number, ry: number): number {
|
||||||
const h = Math.pow(rx - ry, 2) / Math.pow(rx + ry, 2)
|
const h = Math.pow(rx - ry, 2) / Math.pow(rx + ry, 2)
|
||||||
const p = PI * (rx + ry) * (1 + (3 * h) / (10 + Math.sqrt(4 - 3 * h)))
|
return PI * (rx + ry) * (1 + (3 * h) / (10 + Math.sqrt(4 - 3 * h)))
|
||||||
return p
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -427,3 +426,52 @@ export function getArcMeasure(A: number, B: number, sweepFlag: number, largeArcF
|
||||||
if (!largeArcFlag) return m
|
if (!largeArcFlag) return m
|
||||||
return (PI2 - Math.abs(m)) * (sweepFlag ? 1 : -1)
|
return (PI2 - Math.abs(m)) * (sweepFlag ? 1 : -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the center of a circle from three points.
|
||||||
|
*
|
||||||
|
* @param a - The first point
|
||||||
|
* @param b - The second point
|
||||||
|
* @param c - The third point
|
||||||
|
*
|
||||||
|
* @returns The center of the circle
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export function centerOfCircleFromThreePoints(a: VecLike, b: VecLike, c: VecLike) {
|
||||||
|
const u = -2 * (a.x * (b.y - c.y) - a.y * (b.x - c.x) + b.x * c.y - c.x * b.y)
|
||||||
|
return new Vec(
|
||||||
|
((a.x * a.x + a.y * a.y) * (c.y - b.y) +
|
||||||
|
(b.x * b.x + b.y * b.y) * (a.y - c.y) +
|
||||||
|
(c.x * c.x + c.y * c.y) * (b.y - a.y)) /
|
||||||
|
u,
|
||||||
|
((a.x * a.x + a.y * a.y) * (b.x - c.x) +
|
||||||
|
(b.x * b.x + b.y * b.y) * (c.x - a.x) +
|
||||||
|
(c.x * c.x + c.y * c.y) * (a.x - b.x)) /
|
||||||
|
u
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export function getPointsOnArc(
|
||||||
|
startPoint: VecLike,
|
||||||
|
endPoint: VecLike,
|
||||||
|
center: VecLike | null,
|
||||||
|
radius: number,
|
||||||
|
numPoints: number
|
||||||
|
): Vec[] {
|
||||||
|
if (center === null) {
|
||||||
|
return [Vec.From(startPoint), Vec.From(endPoint)]
|
||||||
|
}
|
||||||
|
const results: Vec[] = []
|
||||||
|
const startAngle = Vec.Angle(center, startPoint)
|
||||||
|
const endAngle = Vec.Angle(center, endPoint)
|
||||||
|
const l = clockwiseAngleDist(startAngle, endAngle)
|
||||||
|
for (let i = 0; i < numPoints; i++) {
|
||||||
|
const t = i / (numPoints - 1)
|
||||||
|
const angle = startAngle + l * t
|
||||||
|
const point = getPointOnCircle(center, radius, angle)
|
||||||
|
results.push(point)
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
|
@ -702,7 +702,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
dash: "dashed" | "dotted" | "draw" | "solid";
|
dash: "dashed" | "dotted" | "draw" | "solid";
|
||||||
fill: "none" | "pattern" | "semi" | "solid";
|
fill: "none" | "pattern" | "semi" | "solid";
|
||||||
font: "draw" | "mono" | "sans" | "serif";
|
font: "draw" | "mono" | "sans" | "serif";
|
||||||
geo: "arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "cloud" | "diamond" | "ellipse" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box";
|
geo: "arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "cloud" | "diamond" | "ellipse" | "heart" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box";
|
||||||
growY: number;
|
growY: number;
|
||||||
h: number;
|
h: number;
|
||||||
labelColor: "black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow";
|
labelColor: "black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow";
|
||||||
|
@ -732,7 +732,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
dash: "dashed" | "dotted" | "draw" | "solid";
|
dash: "dashed" | "dotted" | "draw" | "solid";
|
||||||
fill: "none" | "pattern" | "semi" | "solid";
|
fill: "none" | "pattern" | "semi" | "solid";
|
||||||
font: "draw" | "mono" | "sans" | "serif";
|
font: "draw" | "mono" | "sans" | "serif";
|
||||||
geo: "arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "cloud" | "diamond" | "ellipse" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box";
|
geo: "arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "cloud" | "diamond" | "ellipse" | "heart" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box";
|
||||||
growY: number;
|
growY: number;
|
||||||
h: number;
|
h: number;
|
||||||
labelColor: "black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow";
|
labelColor: "black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow";
|
||||||
|
@ -791,7 +791,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
|
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
|
||||||
fill: EnumStyleProp<"none" | "pattern" | "semi" | "solid">;
|
fill: EnumStyleProp<"none" | "pattern" | "semi" | "solid">;
|
||||||
font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
|
font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
|
||||||
geo: EnumStyleProp<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "cloud" | "diamond" | "ellipse" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">;
|
geo: EnumStyleProp<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "cloud" | "diamond" | "ellipse" | "heart" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">;
|
||||||
growY: Validator<number>;
|
growY: Validator<number>;
|
||||||
h: Validator<number>;
|
h: Validator<number>;
|
||||||
labelColor: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
labelColor: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
||||||
|
@ -2261,7 +2261,7 @@ export interface TLUiIconProps extends React.HTMLProps<HTMLDivElement> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLUiIconType = 'align-bottom' | 'align-center-horizontal' | 'align-center-vertical' | 'align-left' | 'align-right' | 'align-top' | 'arrow-left' | 'arrowhead-arrow' | 'arrowhead-bar' | 'arrowhead-diamond' | 'arrowhead-dot' | 'arrowhead-none' | 'arrowhead-square' | 'arrowhead-triangle-inverted' | 'arrowhead-triangle' | 'blob' | 'bring-forward' | 'bring-to-front' | 'broken' | 'check-circle' | 'check' | 'chevron-down' | 'chevron-left' | 'chevron-right' | 'chevron-up' | 'chevrons-ne' | 'chevrons-sw' | 'clipboard-copied' | 'clipboard-copy' | 'color' | 'cross-2' | 'cross-circle' | 'dash-dashed' | 'dash-dotted' | 'dash-draw' | 'dash-solid' | 'disconnected' | 'discord' | 'distribute-horizontal' | 'distribute-vertical' | 'dot' | 'dots-horizontal' | 'dots-vertical' | 'drag-handle-dots' | 'duplicate' | 'edit' | 'external-link' | 'fill-none' | 'fill-pattern' | 'fill-semi' | 'fill-solid' | 'follow' | 'following' | 'font-draw' | 'font-mono' | 'font-sans' | 'font-serif' | 'geo-arrow-down' | 'geo-arrow-left' | 'geo-arrow-right' | 'geo-arrow-up' | 'geo-check-box' | 'geo-cloud' | 'geo-diamond' | 'geo-ellipse' | 'geo-hexagon' | 'geo-octagon' | 'geo-oval' | 'geo-pentagon' | 'geo-rectangle' | 'geo-rhombus-2' | 'geo-rhombus' | 'geo-star' | 'geo-trapezoid' | 'geo-triangle' | 'geo-x-box' | 'github' | 'group' | 'horizontal-align-end' | 'horizontal-align-middle' | 'horizontal-align-start' | 'info-circle' | 'leading' | 'link' | 'lock' | 'menu' | 'minus' | 'mixed' | 'pack' | 'plus' | 'question-mark-circle' | 'question-mark' | 'redo' | 'reset-zoom' | 'rotate-ccw' | 'rotate-cw' | 'send-backward' | 'send-to-back' | 'share-1' | 'size-extra-large' | 'size-large' | 'size-medium' | 'size-small' | 'spline-cubic' | 'spline-line' | 'stack-horizontal' | 'stack-vertical' | 'stretch-horizontal' | 'stretch-vertical' | 'text-align-center' | 'text-align-left' | 'text-align-right' | 'toggle-off' | 'toggle-on' | 'tool-arrow' | 'tool-eraser' | 'tool-frame' | 'tool-hand' | 'tool-highlight' | 'tool-laser' | 'tool-line' | 'tool-media' | 'tool-note' | 'tool-pencil' | 'tool-pointer' | 'tool-screenshot' | 'tool-text' | 'trash' | 'twitter' | 'undo' | 'ungroup' | 'unlock' | 'vertical-align-end' | 'vertical-align-middle' | 'vertical-align-start' | 'warning-triangle' | 'zoom-in' | 'zoom-out';
|
export type TLUiIconType = 'align-bottom' | 'align-center-horizontal' | 'align-center-vertical' | 'align-left' | 'align-right' | 'align-top' | 'arrow-left' | 'arrowhead-arrow' | 'arrowhead-bar' | 'arrowhead-diamond' | 'arrowhead-dot' | 'arrowhead-none' | 'arrowhead-square' | 'arrowhead-triangle-inverted' | 'arrowhead-triangle' | 'blob' | 'bring-forward' | 'bring-to-front' | 'broken' | 'check-circle' | 'check' | 'chevron-down' | 'chevron-left' | 'chevron-right' | 'chevron-up' | 'chevrons-ne' | 'chevrons-sw' | 'clipboard-copied' | 'clipboard-copy' | 'color' | 'cross-2' | 'cross-circle' | 'dash-dashed' | 'dash-dotted' | 'dash-draw' | 'dash-solid' | 'disconnected' | 'discord' | 'distribute-horizontal' | 'distribute-vertical' | 'dot' | 'dots-horizontal' | 'dots-vertical' | 'drag-handle-dots' | 'duplicate' | 'edit' | 'external-link' | 'fill-none' | 'fill-pattern' | 'fill-semi' | 'fill-solid' | 'follow' | 'following' | 'font-draw' | 'font-mono' | 'font-sans' | 'font-serif' | 'geo-arrow-down' | 'geo-arrow-left' | 'geo-arrow-right' | 'geo-arrow-up' | 'geo-check-box' | 'geo-cloud' | 'geo-diamond' | 'geo-ellipse' | 'geo-heart' | 'geo-hexagon' | 'geo-octagon' | 'geo-oval' | 'geo-pentagon' | 'geo-rectangle' | 'geo-rhombus-2' | 'geo-rhombus' | 'geo-star' | 'geo-trapezoid' | 'geo-triangle' | 'geo-x-box' | 'github' | 'group' | 'horizontal-align-end' | 'horizontal-align-middle' | 'horizontal-align-start' | 'info-circle' | 'leading' | 'link' | 'lock' | 'menu' | 'minus' | 'mixed' | 'pack' | 'plus' | 'question-mark-circle' | 'question-mark' | 'redo' | 'reset-zoom' | 'rotate-ccw' | 'rotate-cw' | 'send-backward' | 'send-to-back' | 'share-1' | 'size-extra-large' | 'size-large' | 'size-medium' | 'size-small' | 'spline-cubic' | 'spline-line' | 'stack-horizontal' | 'stack-vertical' | 'stretch-horizontal' | 'stretch-vertical' | 'text-align-center' | 'text-align-left' | 'text-align-right' | 'toggle-off' | 'toggle-on' | 'tool-arrow' | 'tool-eraser' | 'tool-frame' | 'tool-hand' | 'tool-highlight' | 'tool-laser' | 'tool-line' | 'tool-media' | 'tool-note' | 'tool-pencil' | 'tool-pointer' | 'tool-screenshot' | 'tool-text' | 'trash' | 'twitter' | 'undo' | 'ungroup' | 'unlock' | 'vertical-align-end' | 'vertical-align-middle' | 'vertical-align-start' | 'warning-triangle' | 'zoom-in' | 'zoom-out';
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export interface TLUiInputProps {
|
export interface TLUiInputProps {
|
||||||
|
@ -2613,6 +2613,11 @@ export function ToggleTransparentBgMenuItem(): JSX_2.Element;
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function ToggleWrapModeItem(): JSX_2.Element;
|
export function ToggleWrapModeItem(): JSX_2.Element;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export function ToolbarItem({ tool }: {
|
||||||
|
tool: string;
|
||||||
|
}): JSX_2.Element;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function TrapezoidToolbarItem(): JSX_2.Element;
|
export function TrapezoidToolbarItem(): JSX_2.Element;
|
||||||
|
|
||||||
|
|
|
@ -407,6 +407,7 @@ export {
|
||||||
SelectToolbarItem,
|
SelectToolbarItem,
|
||||||
StarToolbarItem,
|
StarToolbarItem,
|
||||||
TextToolbarItem,
|
TextToolbarItem,
|
||||||
|
ToolbarItem,
|
||||||
TrapezoidToolbarItem,
|
TrapezoidToolbarItem,
|
||||||
TriangleToolbarItem,
|
TriangleToolbarItem,
|
||||||
XBoxToolbarItem,
|
XBoxToolbarItem,
|
||||||
|
|
|
@ -121,7 +121,6 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
})
|
})
|
||||||
: new Arc2d({
|
: new Arc2d({
|
||||||
center: Vec.Cast(info.handleArc.center),
|
center: Vec.Cast(info.handleArc.center),
|
||||||
radius: info.handleArc.radius,
|
|
||||||
start: Vec.Cast(info.start.point),
|
start: Vec.Cast(info.start.point),
|
||||||
end: Vec.Cast(info.end.point),
|
end: Vec.Cast(info.end.point),
|
||||||
sweepFlag: info.bodyArc.sweepFlag,
|
sweepFlag: info.bodyArc.sweepFlag,
|
||||||
|
|
|
@ -43,7 +43,6 @@ function getArrowLabelSize(editor: Editor, shape: TLArrowShape) {
|
||||||
})
|
})
|
||||||
: new Arc2d({
|
: new Arc2d({
|
||||||
center: Vec.Cast(info.handleArc.center),
|
center: Vec.Cast(info.handleArc.center),
|
||||||
radius: info.handleArc.radius,
|
|
||||||
start: Vec.Cast(info.start.point),
|
start: Vec.Cast(info.start.point),
|
||||||
end: Vec.Cast(info.end.point),
|
end: Vec.Cast(info.end.point),
|
||||||
sweepFlag: info.bodyArc.sweepFlag,
|
sweepFlag: info.bodyArc.sweepFlag,
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
TLArrowShape,
|
TLArrowShape,
|
||||||
Vec,
|
Vec,
|
||||||
VecLike,
|
VecLike,
|
||||||
|
centerOfCircleFromThreePoints,
|
||||||
clockwiseAngleDist,
|
clockwiseAngleDist,
|
||||||
counterClockwiseAngleDist,
|
counterClockwiseAngleDist,
|
||||||
intersectCirclePolygon,
|
intersectCirclePolygon,
|
||||||
|
@ -373,20 +374,7 @@ export function getCurvedArrowInfo(
|
||||||
*/
|
*/
|
||||||
function getArcInfo(a: VecLike, b: VecLike, c: VecLike): TLArcInfo {
|
function getArcInfo(a: VecLike, b: VecLike, c: VecLike): TLArcInfo {
|
||||||
// find a circle from the three points
|
// find a circle from the three points
|
||||||
const u = -2 * (a.x * (b.y - c.y) - a.y * (b.x - c.x) + b.x * c.y - c.x * b.y)
|
const center = centerOfCircleFromThreePoints(a, b, c)
|
||||||
|
|
||||||
const center = {
|
|
||||||
x:
|
|
||||||
((a.x * a.x + a.y * a.y) * (c.y - b.y) +
|
|
||||||
(b.x * b.x + b.y * b.y) * (a.y - c.y) +
|
|
||||||
(c.x * c.x + c.y * c.y) * (b.y - a.y)) /
|
|
||||||
u,
|
|
||||||
y:
|
|
||||||
((a.x * a.x + a.y * a.y) * (b.x - c.x) +
|
|
||||||
(b.x * b.x + b.y * b.y) * (c.x - a.x) +
|
|
||||||
(c.x * c.x + c.y * c.y) * (a.x - b.x)) /
|
|
||||||
u,
|
|
||||||
}
|
|
||||||
|
|
||||||
const radius = Vec.Dist(center, a)
|
const radius = Vec.Dist(center, a)
|
||||||
|
|
||||||
|
|
|
@ -43,11 +43,16 @@ import {
|
||||||
getFillDefForExport,
|
getFillDefForExport,
|
||||||
getFontDefForExport,
|
getFontDefForExport,
|
||||||
} from '../shared/defaultStyleDefs'
|
} from '../shared/defaultStyleDefs'
|
||||||
import { getRoundedInkyPolygonPath, getRoundedPolygonPoints } from '../shared/polygon-helpers'
|
|
||||||
import { cloudOutline, cloudSvgPath } from './cloudOutline'
|
|
||||||
import { getEllipseIndicatorPath } from './components/DrawStyleEllipse'
|
|
||||||
import { GeoShapeBody } from './components/GeoShapeBody'
|
import { GeoShapeBody } from './components/GeoShapeBody'
|
||||||
import { getOvalIndicatorPath } from './components/SolidStyleOval'
|
import {
|
||||||
|
cloudOutline,
|
||||||
|
getCloudPath,
|
||||||
|
getEllipseDrawIndicatorPath,
|
||||||
|
getHeartParts,
|
||||||
|
getHeartPath,
|
||||||
|
getRoundedInkyPolygonPath,
|
||||||
|
getRoundedPolygonPoints,
|
||||||
|
} from './geo-shape-helpers'
|
||||||
import { getLines } from './getLines'
|
import { getLines } from './getLines'
|
||||||
|
|
||||||
const MIN_SIZE_WITH_LABEL = 17 * 3
|
const MIN_SIZE_WITH_LABEL = 17 * 3
|
||||||
|
@ -292,6 +297,23 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case 'heart': {
|
||||||
|
// kind of expensive (creating the primitives to create a different primitive) but hearts are rare and beautiful things
|
||||||
|
const parts = getHeartParts(w, h)
|
||||||
|
const points = parts.reduce<Vec[]>((acc, part) => {
|
||||||
|
acc.push(...part.vertices)
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
body = new Polygon2d({
|
||||||
|
points,
|
||||||
|
isFilled,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
exhaustiveSwitchError(shape.props.geo)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const labelSize = getLabelSize(this.editor, shape)
|
const labelSize = getLabelSize(this.editor, shape)
|
||||||
|
@ -359,6 +381,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
return { outline: outline, points: [...outline.getVertices(), geometry.bounds.center] }
|
return { outline: outline, points: [...outline.getVertices(), geometry.bounds.center] }
|
||||||
case 'cloud':
|
case 'cloud':
|
||||||
case 'ellipse':
|
case 'ellipse':
|
||||||
|
case 'heart':
|
||||||
case 'oval':
|
case 'oval':
|
||||||
// blobby shapes only have a snap point in their center
|
// blobby shapes only have a snap point in their center
|
||||||
return { outline: outline, points: [geometry.bounds.center] }
|
return { outline: outline, points: [geometry.bounds.center] }
|
||||||
|
@ -438,19 +461,24 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
|
|
||||||
const strokeWidth = STROKE_SIZES[size]
|
const strokeWidth = STROKE_SIZES[size]
|
||||||
|
|
||||||
|
const geometry = this.editor.getShapeGeometry(shape)
|
||||||
|
|
||||||
switch (props.geo) {
|
switch (props.geo) {
|
||||||
case 'ellipse': {
|
case 'ellipse': {
|
||||||
if (props.dash === 'draw') {
|
if (props.dash === 'draw') {
|
||||||
return <path d={getEllipseIndicatorPath(id, w, h, strokeWidth)} />
|
return <path d={getEllipseDrawIndicatorPath(id, w, h, strokeWidth)} />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ellipse cx={w / 2} cy={h / 2} rx={w / 2} ry={h / 2} />
|
return <path d={geometry.getSvgPathData(true)} />
|
||||||
|
}
|
||||||
|
case 'heart': {
|
||||||
|
return <path d={getHeartPath(w, h)} />
|
||||||
}
|
}
|
||||||
case 'oval': {
|
case 'oval': {
|
||||||
return <path d={getOvalIndicatorPath(w, h)} />
|
return <path d={geometry.getSvgPathData(true)} />
|
||||||
}
|
}
|
||||||
case 'cloud': {
|
case 'cloud': {
|
||||||
return <path d={cloudSvgPath(w, h, id, size)} />
|
return <path d={getCloudPath(w, h, id, size)} />
|
||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
|
|
|
@ -1,384 +0,0 @@
|
||||||
import {
|
|
||||||
PI,
|
|
||||||
TLDefaultSizeStyle,
|
|
||||||
Vec,
|
|
||||||
VecModel,
|
|
||||||
clockwiseAngleDist,
|
|
||||||
getPointOnCircle,
|
|
||||||
rng,
|
|
||||||
toDomPrecision,
|
|
||||||
} from '@tldraw/editor'
|
|
||||||
|
|
||||||
function getPillCircumference(width: number, height: number) {
|
|
||||||
const radius = Math.min(width, height) / 2
|
|
||||||
const longSide = Math.max(width, height) - radius * 2
|
|
||||||
|
|
||||||
return Math.PI * (radius * 2) + 2 * longSide
|
|
||||||
}
|
|
||||||
|
|
||||||
type PillSection =
|
|
||||||
| {
|
|
||||||
type: 'straight'
|
|
||||||
start: VecModel
|
|
||||||
delta: VecModel
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'arc'
|
|
||||||
center: VecModel
|
|
||||||
startAngle: number
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPillPoints(width: number, height: number, numPoints: number) {
|
|
||||||
const radius = Math.min(width, height) / 2
|
|
||||||
const longSide = Math.max(width, height) - radius * 2
|
|
||||||
|
|
||||||
const circumference = Math.PI * (radius * 2) + 2 * longSide
|
|
||||||
|
|
||||||
const spacing = circumference / numPoints
|
|
||||||
|
|
||||||
const sections: PillSection[] =
|
|
||||||
width > height
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
type: 'straight',
|
|
||||||
start: new Vec(radius, 0),
|
|
||||||
delta: new Vec(1, 0),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'arc',
|
|
||||||
center: new Vec(width - radius, radius),
|
|
||||||
startAngle: -PI / 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'straight',
|
|
||||||
start: new Vec(width - radius, height),
|
|
||||||
delta: new Vec(-1, 0),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'arc',
|
|
||||||
center: new Vec(radius, radius),
|
|
||||||
startAngle: PI / 2,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
type: 'straight',
|
|
||||||
start: new Vec(width, radius),
|
|
||||||
delta: new Vec(0, 1),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'arc',
|
|
||||||
center: new Vec(radius, height - radius),
|
|
||||||
startAngle: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'straight',
|
|
||||||
start: new Vec(0, height - radius),
|
|
||||||
delta: new Vec(0, -1),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'arc',
|
|
||||||
center: new Vec(radius, radius),
|
|
||||||
startAngle: PI,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
let sectionOffset = 0
|
|
||||||
|
|
||||||
const points: Vec[] = []
|
|
||||||
for (let i = 0; i < numPoints; i++) {
|
|
||||||
const section = sections[0]
|
|
||||||
if (section.type === 'straight') {
|
|
||||||
points.push(Vec.Add(section.start, Vec.Mul(section.delta, sectionOffset)))
|
|
||||||
} else {
|
|
||||||
points.push(
|
|
||||||
getPointOnCircle(section.center, radius, section.startAngle + sectionOffset / radius)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
sectionOffset += spacing
|
|
||||||
let sectionLength = section.type === 'straight' ? longSide : PI * radius
|
|
||||||
while (sectionOffset > sectionLength) {
|
|
||||||
sectionOffset -= sectionLength
|
|
||||||
sections.push(sections.shift()!)
|
|
||||||
sectionLength = sections[0].type === 'straight' ? longSide : PI * radius
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return points
|
|
||||||
}
|
|
||||||
|
|
||||||
const switchSize = <T>(size: TLDefaultSizeStyle, s: T, m: T, l: T, xl: T) => {
|
|
||||||
switch (size) {
|
|
||||||
case 's':
|
|
||||||
return s
|
|
||||||
case 'm':
|
|
||||||
return m
|
|
||||||
case 'l':
|
|
||||||
return l
|
|
||||||
case 'xl':
|
|
||||||
return xl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCloudArcs(
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
seed: string,
|
|
||||||
size: TLDefaultSizeStyle
|
|
||||||
) {
|
|
||||||
const getRandom = rng(seed)
|
|
||||||
const pillCircumference = getPillCircumference(width, height)
|
|
||||||
const numBumps = Math.max(
|
|
||||||
Math.ceil(pillCircumference / switchSize(size, 50, 70, 100, 130)),
|
|
||||||
6,
|
|
||||||
Math.ceil(pillCircumference / Math.min(width, height))
|
|
||||||
)
|
|
||||||
const targetBumpProtrusion = (pillCircumference / numBumps) * 0.2
|
|
||||||
|
|
||||||
// if the aspect ratio is high, innerWidth should be smaller
|
|
||||||
const innerWidth = Math.max(width - targetBumpProtrusion * 2, 1)
|
|
||||||
const innerHeight = Math.max(height - targetBumpProtrusion * 2, 1)
|
|
||||||
const paddingX = (width - innerWidth) / 2
|
|
||||||
const paddingY = (height - innerHeight) / 2
|
|
||||||
|
|
||||||
const distanceBetweenPointsOnPerimeter = getPillCircumference(innerWidth, innerHeight) / numBumps
|
|
||||||
|
|
||||||
const bumpPoints = getPillPoints(innerWidth, innerHeight, numBumps).map((p) => {
|
|
||||||
return p.addXY(paddingX, paddingY)
|
|
||||||
})
|
|
||||||
|
|
||||||
const maxWiggleX = width < 20 ? 0 : targetBumpProtrusion * 0.3
|
|
||||||
const maxWiggleY = height < 20 ? 0 : targetBumpProtrusion * 0.3
|
|
||||||
|
|
||||||
// wiggle the points from either end so that the bumps 'pop'
|
|
||||||
// in at the bottom-right and the top-left looks relatively stable
|
|
||||||
const wiggledPoints = bumpPoints.slice(0)
|
|
||||||
for (let i = 0; i < Math.floor(numBumps / 2); i++) {
|
|
||||||
wiggledPoints[i] = Vec.AddXY(
|
|
||||||
wiggledPoints[i],
|
|
||||||
getRandom() * maxWiggleX,
|
|
||||||
getRandom() * maxWiggleY
|
|
||||||
)
|
|
||||||
wiggledPoints[numBumps - i - 1] = Vec.AddXY(
|
|
||||||
wiggledPoints[numBumps - i - 1],
|
|
||||||
getRandom() * maxWiggleX,
|
|
||||||
getRandom() * maxWiggleY
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const arcs: Arc[] = []
|
|
||||||
|
|
||||||
for (let i = 0; i < wiggledPoints.length; i++) {
|
|
||||||
const j = i === wiggledPoints.length - 1 ? 0 : i + 1
|
|
||||||
const leftWigglePoint = wiggledPoints[i]
|
|
||||||
const rightWigglePoint = wiggledPoints[j]
|
|
||||||
const leftPoint = bumpPoints[i]
|
|
||||||
const rightPoint = bumpPoints[j]
|
|
||||||
|
|
||||||
const midPoint = Vec.Average([leftPoint, rightPoint])
|
|
||||||
const offsetAngle = Vec.Angle(leftPoint, rightPoint) - Math.PI / 2
|
|
||||||
// when the points are on the curvy part of a pill, there is a natural arc that we need to extends past
|
|
||||||
// otherwise it looks like the bumps get less bumpy on the curvy parts
|
|
||||||
const distanceBetweenOriginalPoints = Vec.Dist(leftPoint, rightPoint)
|
|
||||||
const curvatureOffset = distanceBetweenPointsOnPerimeter - distanceBetweenOriginalPoints
|
|
||||||
const distanceBetweenWigglePoints = Vec.Dist(leftWigglePoint, rightWigglePoint)
|
|
||||||
const relativeSize = distanceBetweenWigglePoints / distanceBetweenOriginalPoints
|
|
||||||
const finalDistance = (Math.max(paddingX, paddingY) + curvatureOffset) * relativeSize
|
|
||||||
|
|
||||||
const arcPoint = Vec.Add(midPoint, Vec.FromAngle(offsetAngle, finalDistance))
|
|
||||||
if (arcPoint.x < 0) {
|
|
||||||
arcPoint.x = 0
|
|
||||||
} else if (arcPoint.x > width) {
|
|
||||||
arcPoint.x = width
|
|
||||||
}
|
|
||||||
if (arcPoint.y < 0) {
|
|
||||||
arcPoint.y = 0
|
|
||||||
} else if (arcPoint.y > height) {
|
|
||||||
arcPoint.y = height
|
|
||||||
}
|
|
||||||
|
|
||||||
const center = getCenterOfCircleGivenThreePoints(leftWigglePoint, rightWigglePoint, arcPoint)
|
|
||||||
const radius = Vec.Dist(
|
|
||||||
center ? center : Vec.Average([leftWigglePoint, rightWigglePoint]),
|
|
||||||
leftWigglePoint
|
|
||||||
)
|
|
||||||
|
|
||||||
arcs.push({
|
|
||||||
leftPoint: leftWigglePoint,
|
|
||||||
rightPoint: rightWigglePoint,
|
|
||||||
arcPoint,
|
|
||||||
center,
|
|
||||||
radius,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return arcs
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Arc {
|
|
||||||
leftPoint: Vec
|
|
||||||
rightPoint: Vec
|
|
||||||
arcPoint: Vec
|
|
||||||
center: Vec | null
|
|
||||||
radius: number
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCenterOfCircleGivenThreePoints(a: Vec, b: Vec, c: Vec) {
|
|
||||||
const A = a.x * (b.y - c.y) - a.y * (b.x - c.x) + b.x * c.y - c.x * b.y
|
|
||||||
const B =
|
|
||||||
(a.x * a.x + a.y * a.y) * (c.y - b.y) +
|
|
||||||
(b.x * b.x + b.y * b.y) * (a.y - c.y) +
|
|
||||||
(c.x * c.x + c.y * c.y) * (b.y - a.y)
|
|
||||||
const C =
|
|
||||||
(a.x * a.x + a.y * a.y) * (b.x - c.x) +
|
|
||||||
(b.x * b.x + b.y * b.y) * (c.x - a.x) +
|
|
||||||
(c.x * c.x + c.y * c.y) * (a.x - b.x)
|
|
||||||
|
|
||||||
const x = -B / (2 * A)
|
|
||||||
const y = -C / (2 * A)
|
|
||||||
|
|
||||||
// handle situations where the points are colinear (this happens when the cloud is very small)
|
|
||||||
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Vec(x, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cloudOutline(
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
seed: string,
|
|
||||||
size: TLDefaultSizeStyle
|
|
||||||
) {
|
|
||||||
const path: Vec[] = []
|
|
||||||
|
|
||||||
const arcs = getCloudArcs(width, height, seed, size)
|
|
||||||
|
|
||||||
for (const { center, radius, leftPoint, rightPoint } of arcs) {
|
|
||||||
path.push(...pointsOnArc(leftPoint, rightPoint, center, radius, 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cloudSvgPath(
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
seed: string,
|
|
||||||
size: TLDefaultSizeStyle
|
|
||||||
) {
|
|
||||||
// const points = cloudOutline(width, height, seed, size)
|
|
||||||
// {
|
|
||||||
// let path = `M${toDomPrecision(points[0].x)},${toDomPrecision(points[0].y)}`
|
|
||||||
// for (const point of points.slice(1)) {
|
|
||||||
// path += ` L${toDomPrecision(point.x)},${toDomPrecision(point.y)}`
|
|
||||||
// }
|
|
||||||
// return path
|
|
||||||
// }
|
|
||||||
|
|
||||||
const arcs = getCloudArcs(width, height, seed, size)
|
|
||||||
let path = `M${toDomPrecision(arcs[0].leftPoint.x)},${toDomPrecision(arcs[0].leftPoint.y)}`
|
|
||||||
|
|
||||||
// now draw arcs for each circle, starting where it intersects the previous circle, and ending where it intersects the next circle
|
|
||||||
for (const { leftPoint, rightPoint, radius, center } of arcs) {
|
|
||||||
if (center === null) {
|
|
||||||
// draw a line to rightPoint instead
|
|
||||||
path += ` L${toDomPrecision(rightPoint.x)},${toDomPrecision(rightPoint.y)}`
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// use the large arc if the center of the circle is to the left of the line between the two points
|
|
||||||
const arc = isLeft(leftPoint, rightPoint, center) ? '0' : '1'
|
|
||||||
path += ` A${toDomPrecision(radius)},${toDomPrecision(radius)} 0 ${arc},1 ${toDomPrecision(
|
|
||||||
rightPoint.x
|
|
||||||
)},${toDomPrecision(rightPoint.y)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
path += ' Z'
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
export function inkyCloudSvgPath(
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
seed: string,
|
|
||||||
size: TLDefaultSizeStyle
|
|
||||||
) {
|
|
||||||
const getRandom = rng(seed)
|
|
||||||
const mutMultiplier = size === 's' ? 0.5 : size === 'm' ? 0.7 : size === 'l' ? 0.9 : 1.6
|
|
||||||
const mut = (n: number) => {
|
|
||||||
return n + getRandom() * mutMultiplier * 2
|
|
||||||
}
|
|
||||||
const arcs = getCloudArcs(width, height, seed, size)
|
|
||||||
const avgArcLength =
|
|
||||||
arcs.reduce((sum, arc) => sum + Vec.Dist2(arc.leftPoint, arc.rightPoint), 0) / arcs.length
|
|
||||||
const shouldMutatePoints = avgArcLength > (mutMultiplier * 15) ** 2
|
|
||||||
|
|
||||||
const mutPoint = shouldMutatePoints ? (p: Vec) => new Vec(mut(p.x), mut(p.y)) : (p: Vec) => p
|
|
||||||
let pathA = `M${toDomPrecision(arcs[0].leftPoint.x)},${toDomPrecision(arcs[0].leftPoint.y)}`
|
|
||||||
let leftMutPoint = mutPoint(arcs[0].leftPoint)
|
|
||||||
let pathB = `M${toDomPrecision(leftMutPoint.x)},${toDomPrecision(leftMutPoint.y)}`
|
|
||||||
|
|
||||||
for (const { leftPoint, center, rightPoint, radius, arcPoint } of arcs) {
|
|
||||||
if (center === null) {
|
|
||||||
// draw a line to rightPoint instead
|
|
||||||
pathA += ` L${toDomPrecision(rightPoint.x)},${toDomPrecision(rightPoint.y)}`
|
|
||||||
const rightMutPoint = mutPoint(rightPoint)
|
|
||||||
pathB += ` L${toDomPrecision(rightMutPoint.x)},${toDomPrecision(rightMutPoint.y)}`
|
|
||||||
leftMutPoint = rightMutPoint
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const arc = isLeft(leftPoint, rightPoint, center) ? '0' : '1'
|
|
||||||
pathA += ` A${toDomPrecision(radius)},${toDomPrecision(radius)} 0 ${arc},1 ${toDomPrecision(
|
|
||||||
rightPoint.x
|
|
||||||
)},${toDomPrecision(rightPoint.y)}`
|
|
||||||
const rightMutPoint = mutPoint(rightPoint)
|
|
||||||
const mutArcPoint = mutPoint(arcPoint)
|
|
||||||
const mutCenter = getCenterOfCircleGivenThreePoints(leftMutPoint, rightMutPoint, mutArcPoint)
|
|
||||||
if (!mutCenter) {
|
|
||||||
// draw a line to rightMutPoint instead
|
|
||||||
pathB += ` L${toDomPrecision(rightMutPoint.x)},${toDomPrecision(rightMutPoint.y)}`
|
|
||||||
leftMutPoint = rightMutPoint
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const mutRadius = Math.abs(Vec.Dist(mutCenter, leftMutPoint))
|
|
||||||
|
|
||||||
pathB += ` A${toDomPrecision(mutRadius)},${toDomPrecision(
|
|
||||||
mutRadius
|
|
||||||
)} 0 ${arc},1 ${toDomPrecision(rightMutPoint.x)},${toDomPrecision(rightMutPoint.y)}`
|
|
||||||
leftMutPoint = rightMutPoint
|
|
||||||
}
|
|
||||||
|
|
||||||
return pathA + pathB + ' Z'
|
|
||||||
}
|
|
||||||
|
|
||||||
function pointsOnArc(
|
|
||||||
startPoint: VecModel,
|
|
||||||
endPoint: VecModel,
|
|
||||||
center: VecModel | null,
|
|
||||||
radius: number,
|
|
||||||
numPoints: number
|
|
||||||
): Vec[] {
|
|
||||||
if (center === null) {
|
|
||||||
return [Vec.From(startPoint), Vec.From(endPoint)]
|
|
||||||
}
|
|
||||||
const results: Vec[] = []
|
|
||||||
|
|
||||||
const startAngle = Vec.Angle(center, startPoint)
|
|
||||||
const endAngle = Vec.Angle(center, endPoint)
|
|
||||||
|
|
||||||
const l = clockwiseAngleDist(startAngle, endAngle)
|
|
||||||
|
|
||||||
for (let i = 0; i < numPoints; i++) {
|
|
||||||
const t = i / (numPoints - 1)
|
|
||||||
const angle = startAngle + l * t
|
|
||||||
const point = getPointOnCircle(center, radius, angle)
|
|
||||||
results.push(point)
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLeft(a: Vec, b: Vec, c: Vec) {
|
|
||||||
return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x) > 0
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
import { TLGeoShape, TLShapeId, Vec, canonicalizeRotation } from '@tldraw/editor'
|
|
||||||
import * as React from 'react'
|
|
||||||
import { ShapeFill, useDefaultColorTheme } from '../../shared/ShapeFill'
|
|
||||||
import { getPerfectDashProps } from '../../shared/getPerfectDashProps'
|
|
||||||
import { cloudSvgPath, getCloudArcs } from '../cloudOutline'
|
|
||||||
|
|
||||||
export const DashStyleCloud = React.memo(function DashStylePolygon({
|
|
||||||
dash,
|
|
||||||
fill,
|
|
||||||
color,
|
|
||||||
strokeWidth,
|
|
||||||
w,
|
|
||||||
h,
|
|
||||||
id,
|
|
||||||
size,
|
|
||||||
}: Pick<TLGeoShape['props'], 'dash' | 'fill' | 'color' | 'w' | 'h' | 'size'> & {
|
|
||||||
strokeWidth: number
|
|
||||||
id: TLShapeId
|
|
||||||
}) {
|
|
||||||
const theme = useDefaultColorTheme()
|
|
||||||
const innerPath = cloudSvgPath(w, h, id, size)
|
|
||||||
const arcs = getCloudArcs(w, h, id, size)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ShapeFill theme={theme} d={innerPath} fill={fill} color={color} />
|
|
||||||
<g strokeWidth={strokeWidth} stroke={theme[color].solid} fill="none" pointerEvents="all">
|
|
||||||
{arcs.map(({ leftPoint, rightPoint, center, radius }, i) => {
|
|
||||||
const arcLength = center
|
|
||||||
? radius *
|
|
||||||
canonicalizeRotation(
|
|
||||||
canonicalizeRotation(Vec.Angle(center, rightPoint)) -
|
|
||||||
canonicalizeRotation(Vec.Angle(center, leftPoint))
|
|
||||||
)
|
|
||||||
: Vec.Dist(leftPoint, rightPoint)
|
|
||||||
|
|
||||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
|
||||||
arcLength,
|
|
||||||
strokeWidth,
|
|
||||||
{
|
|
||||||
style: dash,
|
|
||||||
start: 'outset',
|
|
||||||
end: 'outset',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<path
|
|
||||||
key={i}
|
|
||||||
d={
|
|
||||||
center
|
|
||||||
? `M${leftPoint.x},${leftPoint.y}A${radius},${radius},0,0,1,${rightPoint.x},${rightPoint.y}`
|
|
||||||
: `M${leftPoint.x},${leftPoint.y}L${rightPoint.x},${rightPoint.y}`
|
|
||||||
}
|
|
||||||
strokeDasharray={strokeDasharray}
|
|
||||||
strokeDashoffset={strokeDashoffset}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</g>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})
|
|
|
@ -1,53 +0,0 @@
|
||||||
import { TLGeoShape, TLShapeId, perimeterOfEllipse, toDomPrecision } from '@tldraw/editor'
|
|
||||||
import * as React from 'react'
|
|
||||||
import { ShapeFill, useDefaultColorTheme } from '../../shared/ShapeFill'
|
|
||||||
import { getPerfectDashProps } from '../../shared/getPerfectDashProps'
|
|
||||||
|
|
||||||
export const DashStyleEllipse = React.memo(function DashStyleEllipse({
|
|
||||||
w,
|
|
||||||
h,
|
|
||||||
strokeWidth: sw,
|
|
||||||
dash,
|
|
||||||
color,
|
|
||||||
fill,
|
|
||||||
}: Pick<TLGeoShape['props'], 'w' | 'h' | 'dash' | 'color' | 'fill'> & {
|
|
||||||
strokeWidth: number
|
|
||||||
id: TLShapeId
|
|
||||||
}) {
|
|
||||||
const theme = useDefaultColorTheme()
|
|
||||||
const cx = w / 2
|
|
||||||
const cy = h / 2
|
|
||||||
const rx = Math.max(0, cx)
|
|
||||||
const ry = Math.max(0, cy)
|
|
||||||
|
|
||||||
const perimeter = perimeterOfEllipse(rx, ry)
|
|
||||||
|
|
||||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
|
||||||
perimeter < 64 ? perimeter * 2 : perimeter,
|
|
||||||
sw,
|
|
||||||
{
|
|
||||||
style: dash,
|
|
||||||
snap: 4,
|
|
||||||
closed: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const d = `M${cx - rx},${cy}a${rx},${ry},0,1,1,${rx * 2},0a${rx},${ry},0,1,1,-${rx * 2},0`
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ShapeFill theme={theme} d={d} color={color} fill={fill} />
|
|
||||||
<path
|
|
||||||
d={d}
|
|
||||||
strokeWidth={sw}
|
|
||||||
width={toDomPrecision(w)}
|
|
||||||
height={toDomPrecision(h)}
|
|
||||||
fill="none"
|
|
||||||
stroke={theme[color].solid}
|
|
||||||
strokeDasharray={strokeDasharray}
|
|
||||||
strokeDashoffset={strokeDashoffset}
|
|
||||||
pointerEvents="all"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})
|
|
|
@ -1,50 +0,0 @@
|
||||||
import { TLGeoShape, TLShapeId, toDomPrecision } from '@tldraw/editor'
|
|
||||||
import * as React from 'react'
|
|
||||||
import { ShapeFill, useDefaultColorTheme } from '../../shared/ShapeFill'
|
|
||||||
import { getPerfectDashProps } from '../../shared/getPerfectDashProps'
|
|
||||||
import { getOvalPerimeter, getOvalSolidPath } from '../helpers'
|
|
||||||
|
|
||||||
export const DashStyleOval = React.memo(function DashStyleOval({
|
|
||||||
w,
|
|
||||||
h,
|
|
||||||
strokeWidth: sw,
|
|
||||||
dash,
|
|
||||||
color,
|
|
||||||
fill,
|
|
||||||
}: Pick<TLGeoShape['props'], 'w' | 'h' | 'dash' | 'color' | 'fill'> & {
|
|
||||||
strokeWidth: number
|
|
||||||
id: TLShapeId
|
|
||||||
}) {
|
|
||||||
const theme = useDefaultColorTheme()
|
|
||||||
const d = getOvalSolidPath(w, h)
|
|
||||||
const perimeter = getOvalPerimeter(w, h)
|
|
||||||
|
|
||||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
|
||||||
perimeter < 64 ? perimeter * 2 : perimeter,
|
|
||||||
sw,
|
|
||||||
{
|
|
||||||
style: dash,
|
|
||||||
snap: 4,
|
|
||||||
start: 'outset',
|
|
||||||
end: 'outset',
|
|
||||||
closed: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ShapeFill theme={theme} d={d} color={color} fill={fill} />
|
|
||||||
<path
|
|
||||||
d={d}
|
|
||||||
strokeWidth={sw}
|
|
||||||
width={toDomPrecision(w)}
|
|
||||||
height={toDomPrecision(h)}
|
|
||||||
fill="none"
|
|
||||||
stroke={theme[color].solid}
|
|
||||||
strokeDasharray={strokeDasharray}
|
|
||||||
strokeDashoffset={strokeDashoffset}
|
|
||||||
pointerEvents="all"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})
|
|
|
@ -1,75 +0,0 @@
|
||||||
import { TLGeoShape, Vec, VecLike } from '@tldraw/editor'
|
|
||||||
import * as React from 'react'
|
|
||||||
import { ShapeFill, useDefaultColorTheme } from '../../shared/ShapeFill'
|
|
||||||
import { getPerfectDashProps } from '../../shared/getPerfectDashProps'
|
|
||||||
|
|
||||||
export const DashStylePolygon = React.memo(function DashStylePolygon({
|
|
||||||
dash,
|
|
||||||
fill,
|
|
||||||
color,
|
|
||||||
strokeWidth,
|
|
||||||
outline,
|
|
||||||
lines,
|
|
||||||
}: Pick<TLGeoShape['props'], 'dash' | 'fill' | 'color'> & {
|
|
||||||
strokeWidth: number
|
|
||||||
outline: VecLike[]
|
|
||||||
lines?: VecLike[][]
|
|
||||||
}) {
|
|
||||||
const theme = useDefaultColorTheme()
|
|
||||||
const innerPath = 'M' + outline[0] + 'L' + outline.slice(1) + 'Z'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ShapeFill theme={theme} d={innerPath} fill={fill} color={color} />
|
|
||||||
<g strokeWidth={strokeWidth} stroke={theme[color].solid} fill="none" pointerEvents="all">
|
|
||||||
{Array.from(Array(outline.length)).map((_, i) => {
|
|
||||||
const A = outline[i]
|
|
||||||
const B = outline[(i + 1) % outline.length]
|
|
||||||
|
|
||||||
const dist = Vec.Dist(A, B)
|
|
||||||
|
|
||||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(dist, strokeWidth, {
|
|
||||||
style: dash,
|
|
||||||
start: 'outset',
|
|
||||||
end: 'outset',
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<line
|
|
||||||
key={i}
|
|
||||||
x1={A.x}
|
|
||||||
y1={A.y}
|
|
||||||
x2={B.x}
|
|
||||||
y2={B.y}
|
|
||||||
strokeDasharray={strokeDasharray}
|
|
||||||
strokeDashoffset={strokeDashoffset}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
{lines &&
|
|
||||||
lines.map(([A, B], i) => {
|
|
||||||
const dist = Vec.Dist(A, B)
|
|
||||||
|
|
||||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(dist, strokeWidth, {
|
|
||||||
style: dash,
|
|
||||||
start: 'skip',
|
|
||||||
end: 'outset',
|
|
||||||
snap: dash === 'dotted' ? 4 : undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<path
|
|
||||||
key={`line_fg_${i}`}
|
|
||||||
d={`M${A.x},${A.y}L${B.x},${B.y}`}
|
|
||||||
stroke={theme[color].solid}
|
|
||||||
strokeWidth={strokeWidth}
|
|
||||||
fill="none"
|
|
||||||
strokeDasharray={strokeDasharray}
|
|
||||||
strokeDashoffset={strokeDashoffset}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</g>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})
|
|
|
@ -1,27 +0,0 @@
|
||||||
import { TLGeoShape, TLShapeId } from '@tldraw/editor'
|
|
||||||
import * as React from 'react'
|
|
||||||
import { ShapeFill, useDefaultColorTheme } from '../../shared/ShapeFill'
|
|
||||||
import { inkyCloudSvgPath } from '../cloudOutline'
|
|
||||||
|
|
||||||
export const DrawStyleCloud = React.memo(function StyleCloud({
|
|
||||||
fill,
|
|
||||||
color,
|
|
||||||
strokeWidth,
|
|
||||||
w,
|
|
||||||
h,
|
|
||||||
id,
|
|
||||||
size,
|
|
||||||
}: Pick<TLGeoShape['props'], 'fill' | 'color' | 'w' | 'h' | 'size'> & {
|
|
||||||
strokeWidth: number
|
|
||||||
id: TLShapeId
|
|
||||||
}) {
|
|
||||||
const theme = useDefaultColorTheme()
|
|
||||||
const path = inkyCloudSvgPath(w, h, id, size)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ShapeFill theme={theme} d={path} fill={fill} color={color} />
|
|
||||||
<path d={path} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})
|
|
|
@ -1,58 +0,0 @@
|
||||||
import { EASINGS, HALF_PI, PI2, Vec, perimeterOfEllipse, rng } from '@tldraw/editor'
|
|
||||||
import { getStrokePoints } from '../../shared/freehand/getStrokePoints'
|
|
||||||
import { getSvgPathFromStrokePoints } from '../../shared/freehand/svg'
|
|
||||||
|
|
||||||
function getEllipseStrokeOptions(strokeWidth: number) {
|
|
||||||
return {
|
|
||||||
size: 1 + strokeWidth,
|
|
||||||
thinning: 0.25,
|
|
||||||
end: { taper: strokeWidth },
|
|
||||||
start: { taper: strokeWidth },
|
|
||||||
streamline: 0,
|
|
||||||
smoothing: 1,
|
|
||||||
simulatePressure: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEllipseStrokePoints(id: string, width: number, height: number, strokeWidth: number) {
|
|
||||||
const getRandom = rng(id)
|
|
||||||
|
|
||||||
const rx = width / 2
|
|
||||||
const ry = height / 2
|
|
||||||
const perimeter = perimeterOfEllipse(rx, ry)
|
|
||||||
|
|
||||||
const points: Vec[] = []
|
|
||||||
|
|
||||||
const start = PI2 * getRandom()
|
|
||||||
const length = PI2 + HALF_PI / 2 + Math.abs(getRandom()) * HALF_PI
|
|
||||||
const count = Math.max(16, perimeter / 10)
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const t = i / (count - 1)
|
|
||||||
const r = start + t * length
|
|
||||||
const c = Math.cos(r)
|
|
||||||
const s = Math.sin(r)
|
|
||||||
points.push(
|
|
||||||
new Vec(
|
|
||||||
rx * c + width * 0.5 + 0.05 * getRandom(),
|
|
||||||
ry * s + height / 2 + 0.05 * getRandom(),
|
|
||||||
Math.min(
|
|
||||||
1,
|
|
||||||
0.5 +
|
|
||||||
Math.abs(0.5 - (getRandom() > 0 ? EASINGS.easeInOutSine(t) : EASINGS.easeInExpo(t))) / 2
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return getStrokePoints(points, getEllipseStrokeOptions(strokeWidth))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getEllipseIndicatorPath(
|
|
||||||
id: string,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
strokeWidth: number
|
|
||||||
) {
|
|
||||||
return getSvgPathFromStrokePoints(getEllipseStrokePoints(id, width, height, strokeWidth))
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
import { TLGeoShape, VecLike } from '@tldraw/editor'
|
|
||||||
import * as React from 'react'
|
|
||||||
import { ShapeFill, useDefaultColorTheme } from '../../shared/ShapeFill'
|
|
||||||
import { getRoundedInkyPolygonPath, getRoundedPolygonPoints } from '../../shared/polygon-helpers'
|
|
||||||
|
|
||||||
export const DrawStylePolygon = React.memo(function DrawStylePolygon({
|
|
||||||
id,
|
|
||||||
outline,
|
|
||||||
lines,
|
|
||||||
fill,
|
|
||||||
color,
|
|
||||||
strokeWidth,
|
|
||||||
}: Pick<TLGeoShape['props'], 'fill' | 'color'> & {
|
|
||||||
id: TLGeoShape['id']
|
|
||||||
outline: VecLike[]
|
|
||||||
strokeWidth: number
|
|
||||||
lines?: VecLike[][]
|
|
||||||
}) {
|
|
||||||
const theme = useDefaultColorTheme()
|
|
||||||
const polygonPoints = getRoundedPolygonPoints(id, outline, strokeWidth / 3, strokeWidth * 2, 2)
|
|
||||||
let strokePathData = getRoundedInkyPolygonPath(polygonPoints)
|
|
||||||
|
|
||||||
if (lines) {
|
|
||||||
for (const [A, B] of lines) {
|
|
||||||
strokePathData += `M${A.x},${A.y}L${B.x},${B.y}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const innerPolygonPoints = getRoundedPolygonPoints(id, outline, 0, strokeWidth * 2, 1)
|
|
||||||
const innerPathData = getRoundedInkyPolygonPath(innerPolygonPoints)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ShapeFill d={innerPathData} fill={fill} color={color} theme={theme} />
|
|
||||||
<path d={strokePathData} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})
|
|
|
@ -1,19 +1,22 @@
|
||||||
import { Group2d, TLGeoShape, useEditor } from '@tldraw/editor'
|
import { Group2d, TLGeoShape, Vec, canonicalizeRotation, useEditor } from '@tldraw/editor'
|
||||||
|
import { ShapeFill, useDefaultColorTheme } from '../../shared/ShapeFill'
|
||||||
import { STROKE_SIZES } from '../../shared/default-shape-constants'
|
import { STROKE_SIZES } from '../../shared/default-shape-constants'
|
||||||
|
import { getPerfectDashProps } from '../../shared/getPerfectDashProps'
|
||||||
|
import {
|
||||||
|
getCloudArcs,
|
||||||
|
getCloudPath,
|
||||||
|
getDrawHeartPath,
|
||||||
|
getHeartParts,
|
||||||
|
getHeartPath,
|
||||||
|
getRoundedInkyPolygonPath,
|
||||||
|
getRoundedPolygonPoints,
|
||||||
|
inkyCloudSvgPath,
|
||||||
|
} from '../geo-shape-helpers'
|
||||||
import { getLines } from '../getLines'
|
import { getLines } from '../getLines'
|
||||||
import { DashStyleCloud } from './DashStyleCloud'
|
|
||||||
import { DashStyleEllipse } from './DashStyleEllipse'
|
|
||||||
import { DashStyleOval } from './DashStyleOval'
|
|
||||||
import { DashStylePolygon } from './DashStylePolygon'
|
|
||||||
import { DrawStyleCloud } from './DrawStyleCloud'
|
|
||||||
import { DrawStylePolygon } from './DrawStylePolygon'
|
|
||||||
import { SolidStyleCloud } from './SolidStyleCloud'
|
|
||||||
import { SolidStyleEllipse } from './SolidStyleEllipse'
|
|
||||||
import { SolidStyleOval } from './SolidStyleOval'
|
|
||||||
import { SolidStylePolygon } from './SolidStylePolygon'
|
|
||||||
|
|
||||||
export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
|
export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
|
const theme = useDefaultColorTheme()
|
||||||
const { id, props } = shape
|
const { id, props } = shape
|
||||||
const { w, color, fill, dash, growY, size } = props
|
const { w, color, fill, dash, growY, size } = props
|
||||||
const strokeWidth = STROKE_SIZES[size]
|
const strokeWidth = STROKE_SIZES[size]
|
||||||
|
@ -22,85 +25,194 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
|
||||||
switch (props.geo) {
|
switch (props.geo) {
|
||||||
case 'cloud': {
|
case 'cloud': {
|
||||||
if (dash === 'solid') {
|
if (dash === 'solid') {
|
||||||
|
const d = getCloudPath(w, h, id, size)
|
||||||
return (
|
return (
|
||||||
<SolidStyleCloud
|
<>
|
||||||
color={color}
|
<ShapeFill theme={theme} d={d} color={color} fill={fill} />
|
||||||
fill={fill}
|
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
||||||
strokeWidth={strokeWidth}
|
</>
|
||||||
w={w}
|
|
||||||
h={h}
|
|
||||||
id={id}
|
|
||||||
size={size}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
} else if (dash === 'dashed' || dash === 'dotted') {
|
|
||||||
return (
|
|
||||||
<DashStyleCloud
|
|
||||||
color={color}
|
|
||||||
fill={fill}
|
|
||||||
strokeWidth={strokeWidth}
|
|
||||||
w={w}
|
|
||||||
h={h}
|
|
||||||
id={id}
|
|
||||||
size={size}
|
|
||||||
dash={dash}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
} else if (dash === 'draw') {
|
} else if (dash === 'draw') {
|
||||||
|
const d = inkyCloudSvgPath(w, h, id, size)
|
||||||
return (
|
return (
|
||||||
<DrawStyleCloud
|
<>
|
||||||
color={color}
|
<ShapeFill theme={theme} d={d} color={color} fill={fill} />
|
||||||
fill={fill}
|
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const innerPath = getCloudPath(w, h, id, size)
|
||||||
|
const arcs = getCloudArcs(w, h, id, size)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ShapeFill theme={theme} d={innerPath} color={color} fill={fill} />
|
||||||
|
<g
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
w={w}
|
stroke={theme[color].solid}
|
||||||
h={h}
|
fill="none"
|
||||||
id={id}
|
pointerEvents="all"
|
||||||
size={size}
|
>
|
||||||
|
{arcs.map(({ leftPoint, rightPoint, center, radius }, i) => {
|
||||||
|
const arcLength = center
|
||||||
|
? radius *
|
||||||
|
canonicalizeRotation(
|
||||||
|
canonicalizeRotation(Vec.Angle(center, rightPoint)) -
|
||||||
|
canonicalizeRotation(Vec.Angle(center, leftPoint))
|
||||||
|
)
|
||||||
|
: Vec.Dist(leftPoint, rightPoint)
|
||||||
|
|
||||||
|
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||||
|
arcLength,
|
||||||
|
strokeWidth,
|
||||||
|
{
|
||||||
|
style: dash,
|
||||||
|
start: 'outset',
|
||||||
|
end: 'outset',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<path
|
||||||
|
key={i}
|
||||||
|
d={
|
||||||
|
center
|
||||||
|
? `M${leftPoint.x},${leftPoint.y}A${radius},${radius},0,0,1,${rightPoint.x},${rightPoint.y}`
|
||||||
|
: `M${leftPoint.x},${leftPoint.y}L${rightPoint.x},${rightPoint.y}`
|
||||||
|
}
|
||||||
|
strokeDasharray={strokeDasharray}
|
||||||
|
strokeDashoffset={strokeDashoffset}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
})}
|
||||||
|
</g>
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
case 'ellipse': {
|
case 'ellipse': {
|
||||||
if (dash === 'solid') {
|
const geometry = editor.getShapeGeometry(shape)
|
||||||
return <SolidStyleEllipse strokeWidth={strokeWidth} w={w} h={h} color={color} fill={fill} />
|
const d = geometry.getSvgPathData(true)
|
||||||
} else if (dash === 'dashed' || dash === 'dotted') {
|
|
||||||
return (
|
if (dash === 'dashed' || dash === 'dotted') {
|
||||||
<DashStyleEllipse
|
const perimeter = geometry.length
|
||||||
id={id}
|
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||||
strokeWidth={strokeWidth}
|
perimeter < 64 ? perimeter * 2 : perimeter,
|
||||||
w={w}
|
strokeWidth,
|
||||||
h={h}
|
{
|
||||||
dash={dash}
|
style: dash,
|
||||||
color={color}
|
snap: 4,
|
||||||
fill={fill}
|
closed: true,
|
||||||
/>
|
}
|
||||||
)
|
)
|
||||||
} else if (dash === 'draw') {
|
|
||||||
return <SolidStyleEllipse strokeWidth={strokeWidth} w={w} h={h} color={color} fill={fill} />
|
return (
|
||||||
|
<>
|
||||||
|
<ShapeFill theme={theme} d={d} color={color} fill={fill} />
|
||||||
|
<path
|
||||||
|
d={d}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
fill="none"
|
||||||
|
stroke={theme[color].solid}
|
||||||
|
strokeDasharray={strokeDasharray}
|
||||||
|
strokeDashoffset={strokeDashoffset}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const geometry = editor.getShapeGeometry(shape)
|
||||||
|
const d = geometry.getSvgPathData(true)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ShapeFill theme={theme} d={d} color={color} fill={fill} />
|
||||||
|
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
break
|
|
||||||
}
|
}
|
||||||
case 'oval': {
|
case 'oval': {
|
||||||
if (dash === 'solid') {
|
const geometry = editor.getShapeGeometry(shape)
|
||||||
return <SolidStyleOval strokeWidth={strokeWidth} w={w} h={h} color={color} fill={fill} />
|
const d = geometry.getSvgPathData(true)
|
||||||
} else if (dash === 'dashed' || dash === 'dotted') {
|
if (dash === 'dashed' || dash === 'dotted') {
|
||||||
|
const perimeter = geometry.getLength()
|
||||||
|
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||||
|
perimeter < 64 ? perimeter * 2 : perimeter,
|
||||||
|
strokeWidth,
|
||||||
|
{
|
||||||
|
style: dash,
|
||||||
|
snap: 4,
|
||||||
|
start: 'outset',
|
||||||
|
end: 'outset',
|
||||||
|
closed: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashStyleOval
|
<>
|
||||||
id={id}
|
<ShapeFill theme={theme} d={d} color={color} fill={fill} />
|
||||||
|
<path
|
||||||
|
d={d}
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
w={w}
|
fill="none"
|
||||||
h={h}
|
stroke={theme[color].solid}
|
||||||
dash={dash}
|
strokeDasharray={strokeDasharray}
|
||||||
color={color}
|
strokeDashoffset={strokeDashoffset}
|
||||||
fill={fill}
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ShapeFill theme={theme} d={d} color={color} fill={fill} />
|
||||||
|
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'heart': {
|
||||||
|
if (dash === 'dashed' || dash === 'dotted') {
|
||||||
|
const d = getHeartPath(w, h)
|
||||||
|
const curves = getHeartParts(w, h)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ShapeFill theme={theme} d={d} color={color} fill={fill} />
|
||||||
|
{curves.map((c, i) => {
|
||||||
|
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||||
|
c.length,
|
||||||
|
strokeWidth,
|
||||||
|
{
|
||||||
|
style: dash,
|
||||||
|
snap: 1,
|
||||||
|
start: 'outset',
|
||||||
|
end: 'outset',
|
||||||
|
closed: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<path
|
||||||
|
key={`curve_${i}`}
|
||||||
|
d={c.getSvgPathData()}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
fill="none"
|
||||||
|
stroke={theme[color].solid}
|
||||||
|
strokeDasharray={strokeDasharray}
|
||||||
|
strokeDashoffset={strokeDashoffset}
|
||||||
|
pointerEvents="all"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
} else if (dash === 'draw') {
|
})}
|
||||||
return <SolidStyleOval strokeWidth={strokeWidth} w={w} h={h} color={color} fill={fill} />
|
</>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const d = getDrawHeartPath(w, h, strokeWidth, shape.id)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ShapeFill d={d} color={color} fill={fill} theme={theme} />
|
||||||
|
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
break
|
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
const geometry = editor.getShapeGeometry(shape)
|
const geometry = editor.getShapeGeometry(shape)
|
||||||
|
@ -109,36 +221,112 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
|
||||||
const lines = getLines(shape.props, strokeWidth)
|
const lines = getLines(shape.props, strokeWidth)
|
||||||
|
|
||||||
if (dash === 'solid') {
|
if (dash === 'solid') {
|
||||||
|
let d = 'M' + outline[0] + 'L' + outline.slice(1) + 'Z'
|
||||||
|
|
||||||
|
if (lines) {
|
||||||
|
for (const [A, B] of lines) {
|
||||||
|
d += `M${A.x},${A.y}L${B.x},${B.y}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SolidStylePolygon
|
<>
|
||||||
fill={fill}
|
<ShapeFill theme={theme} d={d} color={color} fill={fill} />
|
||||||
color={color}
|
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
||||||
strokeWidth={strokeWidth}
|
</>
|
||||||
outline={outline}
|
|
||||||
lines={lines}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
} else if (dash === 'dashed' || dash === 'dotted') {
|
} else if (dash === 'dashed' || dash === 'dotted') {
|
||||||
|
const innerPath = 'M' + outline[0] + 'L' + outline.slice(1) + 'Z'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashStylePolygon
|
<>
|
||||||
dash={dash}
|
<ShapeFill theme={theme} d={innerPath} fill={fill} color={color} />
|
||||||
fill={fill}
|
<g
|
||||||
color={color}
|
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
outline={outline}
|
stroke={theme[color].solid}
|
||||||
lines={lines}
|
fill="none"
|
||||||
|
pointerEvents="all"
|
||||||
|
>
|
||||||
|
{Array.from(Array(outline.length)).map((_, i) => {
|
||||||
|
const A = Vec.ToFixed(outline[i])
|
||||||
|
const B = Vec.ToFixed(outline[(i + 1) % outline.length])
|
||||||
|
const dist = Vec.Dist(A, B)
|
||||||
|
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||||
|
dist,
|
||||||
|
strokeWidth,
|
||||||
|
{
|
||||||
|
style: dash,
|
||||||
|
start: 'outset',
|
||||||
|
end: 'outset',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<line
|
||||||
|
key={i}
|
||||||
|
x1={A.x}
|
||||||
|
y1={A.y}
|
||||||
|
x2={B.x}
|
||||||
|
y2={B.y}
|
||||||
|
strokeDasharray={strokeDasharray}
|
||||||
|
strokeDashoffset={strokeDashoffset}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
})}
|
||||||
|
{lines &&
|
||||||
|
lines.map(([A, B], i) => {
|
||||||
|
const dist = Vec.Dist(A, B)
|
||||||
|
|
||||||
|
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||||
|
dist,
|
||||||
|
strokeWidth,
|
||||||
|
{
|
||||||
|
style: dash,
|
||||||
|
start: 'skip',
|
||||||
|
end: 'skip',
|
||||||
|
snap: dash === 'dotted' ? 4 : undefined,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<path
|
||||||
|
key={`line_fg_${i}`}
|
||||||
|
d={`M${A.x},${A.y}L${B.x},${B.y}`}
|
||||||
|
stroke={theme[color].solid}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
fill="none"
|
||||||
|
strokeDasharray={strokeDasharray}
|
||||||
|
strokeDashoffset={strokeDashoffset}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</g>
|
||||||
|
</>
|
||||||
|
)
|
||||||
} else if (dash === 'draw') {
|
} else if (dash === 'draw') {
|
||||||
|
const polygonPoints = getRoundedPolygonPoints(
|
||||||
|
id,
|
||||||
|
outline,
|
||||||
|
strokeWidth / 3,
|
||||||
|
strokeWidth * 2,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
let d = getRoundedInkyPolygonPath(polygonPoints)
|
||||||
|
|
||||||
|
if (lines) {
|
||||||
|
for (const [A, B] of lines) {
|
||||||
|
d += `M${A.toFixed()}L${B.toFixed()}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const innerPolygonPoints = getRoundedPolygonPoints(id, outline, 0, strokeWidth * 2, 1)
|
||||||
|
const innerPathData = getRoundedInkyPolygonPath(innerPolygonPoints)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DrawStylePolygon
|
<>
|
||||||
id={id}
|
<ShapeFill d={innerPathData} fill={fill} color={color} theme={theme} />
|
||||||
fill={fill}
|
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
||||||
color={color}
|
</>
|
||||||
strokeWidth={strokeWidth}
|
|
||||||
outline={outline}
|
|
||||||
lines={lines}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
import { TLGeoShape, TLShapeId } from '@tldraw/editor'
|
|
||||||
import * as React from 'react'
|
|
||||||
import { ShapeFill, useDefaultColorTheme } from '../../shared/ShapeFill'
|
|
||||||
import { cloudSvgPath } from '../cloudOutline'
|
|
||||||
|
|
||||||
export const SolidStyleCloud = React.memo(function SolidStyleCloud({
|
|
||||||
fill,
|
|
||||||
color,
|
|
||||||
strokeWidth,
|
|
||||||
w,
|
|
||||||
h,
|
|
||||||
id,
|
|
||||||
size,
|
|
||||||
}: Pick<TLGeoShape['props'], 'fill' | 'color' | 'w' | 'h' | 'size'> & {
|
|
||||||
strokeWidth: number
|
|
||||||
id: TLShapeId
|
|
||||||
}) {
|
|
||||||
const theme = useDefaultColorTheme()
|
|
||||||
const path = cloudSvgPath(w, h, id, size)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ShapeFill theme={theme} d={path} fill={fill} color={color} />
|
|
||||||
<path d={path} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})
|
|
|
@ -1,26 +0,0 @@
|
||||||
import { TLGeoShape } from '@tldraw/editor'
|
|
||||||
import * as React from 'react'
|
|
||||||
import { ShapeFill, useDefaultColorTheme } from '../../shared/ShapeFill'
|
|
||||||
|
|
||||||
export const SolidStyleEllipse = React.memo(function SolidStyleEllipse({
|
|
||||||
w,
|
|
||||||
h,
|
|
||||||
strokeWidth: sw,
|
|
||||||
fill,
|
|
||||||
color,
|
|
||||||
}: Pick<TLGeoShape['props'], 'w' | 'h' | 'fill' | 'color'> & { strokeWidth: number }) {
|
|
||||||
const theme = useDefaultColorTheme()
|
|
||||||
const cx = w / 2
|
|
||||||
const cy = h / 2
|
|
||||||
const rx = Math.max(0, cx)
|
|
||||||
const ry = Math.max(0, cy)
|
|
||||||
|
|
||||||
const d = `M${cx - rx},${cy}a${rx},${ry},0,1,1,${rx * 2},0a${rx},${ry},0,1,1,-${rx * 2},0`
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ShapeFill d={d} color={color} fill={fill} theme={theme} />
|
|
||||||
<path d={d} stroke={theme[color].solid} strokeWidth={sw} fill="none" />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})
|
|
|
@ -1,47 +0,0 @@
|
||||||
import { TLGeoShape } from '@tldraw/editor'
|
|
||||||
import * as React from 'react'
|
|
||||||
import { ShapeFill, useDefaultColorTheme } from '../../shared/ShapeFill'
|
|
||||||
|
|
||||||
export const SolidStyleOval = React.memo(function SolidStyleOval({
|
|
||||||
w,
|
|
||||||
h,
|
|
||||||
strokeWidth: sw,
|
|
||||||
fill,
|
|
||||||
color,
|
|
||||||
}: Pick<TLGeoShape['props'], 'w' | 'h' | 'fill' | 'color'> & {
|
|
||||||
strokeWidth: number
|
|
||||||
}) {
|
|
||||||
const theme = useDefaultColorTheme()
|
|
||||||
const d = getOvalIndicatorPath(w, h)
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ShapeFill d={d} color={color} fill={fill} theme={theme} />
|
|
||||||
<path d={d} stroke={theme[color].solid} strokeWidth={sw} fill="none" />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export function getOvalIndicatorPath(w: number, h: number) {
|
|
||||||
let d: string
|
|
||||||
|
|
||||||
if (h > w) {
|
|
||||||
const offset = w / 2
|
|
||||||
d = `
|
|
||||||
M0,${offset}
|
|
||||||
a${offset},${offset},0,1,1,${offset * 2},0
|
|
||||||
L${w},${h - offset}
|
|
||||||
a${offset},${offset},0,1,1,-${offset * 2},0
|
|
||||||
Z`
|
|
||||||
} else {
|
|
||||||
const offset = h / 2
|
|
||||||
d = `
|
|
||||||
M${offset},0
|
|
||||||
L${w - offset},0
|
|
||||||
a${offset},${offset},0,1,1,0,${offset * 2}
|
|
||||||
L${offset},${h}
|
|
||||||
a${offset},${offset},0,1,1,0,${-offset * 2}
|
|
||||||
Z`
|
|
||||||
}
|
|
||||||
|
|
||||||
return d
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
import { TLGeoShape, VecLike } from '@tldraw/editor'
|
|
||||||
import * as React from 'react'
|
|
||||||
import { ShapeFill, useDefaultColorTheme } from '../../shared/ShapeFill'
|
|
||||||
|
|
||||||
export const SolidStylePolygon = React.memo(function SolidStylePolygon({
|
|
||||||
outline,
|
|
||||||
lines,
|
|
||||||
fill,
|
|
||||||
color,
|
|
||||||
strokeWidth,
|
|
||||||
}: Pick<TLGeoShape['props'], 'fill' | 'color'> & {
|
|
||||||
outline: VecLike[]
|
|
||||||
lines?: VecLike[][]
|
|
||||||
strokeWidth: number
|
|
||||||
}) {
|
|
||||||
const theme = useDefaultColorTheme()
|
|
||||||
let path = 'M' + outline[0] + 'L' + outline.slice(1) + 'Z'
|
|
||||||
|
|
||||||
if (lines) {
|
|
||||||
for (const [A, B] of lines) {
|
|
||||||
path += `M${A.x},${A.y}L${B.x},${B.y}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ShapeFill d={path} fill={fill} color={color} theme={theme} />
|
|
||||||
<path d={path} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})
|
|
588
packages/tldraw/src/lib/shapes/geo/geo-shape-helpers.ts
Normal file
588
packages/tldraw/src/lib/shapes/geo/geo-shape-helpers.ts
Normal file
|
@ -0,0 +1,588 @@
|
||||||
|
import {
|
||||||
|
CubicBezier2d,
|
||||||
|
EASINGS,
|
||||||
|
HALF_PI,
|
||||||
|
PI,
|
||||||
|
PI2,
|
||||||
|
TLDefaultSizeStyle,
|
||||||
|
Vec,
|
||||||
|
VecModel,
|
||||||
|
centerOfCircleFromThreePoints,
|
||||||
|
getPointOnCircle,
|
||||||
|
getPointsOnArc,
|
||||||
|
perimeterOfEllipse,
|
||||||
|
rng,
|
||||||
|
toDomPrecision,
|
||||||
|
} from '@tldraw/editor'
|
||||||
|
import { getStrokePoints } from '../shared/freehand/getStrokePoints'
|
||||||
|
import { getSvgPathFromStrokePoints } from '../shared/freehand/svg'
|
||||||
|
|
||||||
|
/* ---------------------- Oval ---------------------- */
|
||||||
|
|
||||||
|
export function getOvalPerimeter(h: number, w: number) {
|
||||||
|
if (h > w) return (PI * (w / 2) + (h - w)) * 2
|
||||||
|
else return (PI * (h / 2) + (w - h)) * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------- Heart --------------------- */
|
||||||
|
|
||||||
|
export function getHeartPath(w: number, h: number) {
|
||||||
|
return (
|
||||||
|
getHeartParts(w, h)
|
||||||
|
.map((c, i) => c.getSvgPathData(i === 0))
|
||||||
|
.join(' ') + ' Z'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDrawHeartPath(w: number, h: number, sw: number, id: string) {
|
||||||
|
const o = w / 4
|
||||||
|
const k = h / 4
|
||||||
|
const random = rng(id)
|
||||||
|
const mutDistance = sw * 0.75
|
||||||
|
const mut = (v: Vec) => v.addXY(random() * mutDistance, random() * mutDistance)
|
||||||
|
|
||||||
|
const A = new Vec(w / 2, h)
|
||||||
|
const B = new Vec(0, k * 1.2)
|
||||||
|
const C = new Vec(w / 2, k * 0.9)
|
||||||
|
const D = new Vec(w, k * 1.2)
|
||||||
|
|
||||||
|
const Am = mut(new Vec(w / 2, h))
|
||||||
|
const Bm = mut(new Vec(0, k * 1.2))
|
||||||
|
const Cm = mut(new Vec(w / 2, k * 0.9))
|
||||||
|
const Dm = mut(new Vec(w, k * 1.2))
|
||||||
|
|
||||||
|
const parts = [
|
||||||
|
new CubicBezier2d({
|
||||||
|
start: A,
|
||||||
|
cp1: new Vec(o * 1.5, k * 3),
|
||||||
|
cp2: new Vec(0, k * 2.5),
|
||||||
|
end: B,
|
||||||
|
}),
|
||||||
|
new CubicBezier2d({
|
||||||
|
start: B,
|
||||||
|
cp1: new Vec(0, -k * 0.32),
|
||||||
|
cp2: new Vec(o * 1.85, -k * 0.32),
|
||||||
|
end: C,
|
||||||
|
}),
|
||||||
|
new CubicBezier2d({
|
||||||
|
start: C,
|
||||||
|
cp1: new Vec(o * 2.15, -k * 0.32),
|
||||||
|
cp2: new Vec(w, -k * 0.32),
|
||||||
|
end: D,
|
||||||
|
}),
|
||||||
|
new CubicBezier2d({
|
||||||
|
start: D,
|
||||||
|
cp1: new Vec(w, k * 2.5),
|
||||||
|
cp2: new Vec(o * 2.5, k * 3),
|
||||||
|
end: Am,
|
||||||
|
}),
|
||||||
|
new CubicBezier2d({
|
||||||
|
start: Am,
|
||||||
|
cp1: new Vec(o * 1.5, k * 3),
|
||||||
|
cp2: new Vec(0, k * 2.5),
|
||||||
|
end: Bm,
|
||||||
|
}),
|
||||||
|
new CubicBezier2d({
|
||||||
|
start: Bm,
|
||||||
|
cp1: new Vec(0, -k * 0.32),
|
||||||
|
cp2: new Vec(o * 1.85, -k * 0.32),
|
||||||
|
end: Cm,
|
||||||
|
}),
|
||||||
|
new CubicBezier2d({
|
||||||
|
start: Cm,
|
||||||
|
cp1: new Vec(o * 2.15, -k * 0.32),
|
||||||
|
cp2: new Vec(w, -k * 0.32),
|
||||||
|
end: Dm,
|
||||||
|
}),
|
||||||
|
new CubicBezier2d({
|
||||||
|
start: Dm,
|
||||||
|
cp1: new Vec(w, k * 2.5),
|
||||||
|
cp2: new Vec(o * 2.5, k * 3),
|
||||||
|
end: A,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
return parts.map((c, i) => c.getSvgPathData(i === 0)).join(' ') + ' Z'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHeartPoints(w: number, h: number) {
|
||||||
|
const points = [] as Vec[]
|
||||||
|
const curves = getHeartParts(w, h)
|
||||||
|
for (let i = 0; i < curves.length; i++) {
|
||||||
|
for (let j = 0; j < 20; j++) {
|
||||||
|
points.push(CubicBezier2d.GetAtT(curves[i], j / 20))
|
||||||
|
}
|
||||||
|
if (i === curves.length - 1) {
|
||||||
|
points.push(CubicBezier2d.GetAtT(curves[i], 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHeartParts(w: number, h: number) {
|
||||||
|
const o = w / 4
|
||||||
|
const k = h / 4
|
||||||
|
return [
|
||||||
|
new CubicBezier2d({
|
||||||
|
start: new Vec(w / 2, h),
|
||||||
|
cp1: new Vec(o * 1.5, k * 3),
|
||||||
|
cp2: new Vec(0, k * 2.5),
|
||||||
|
end: new Vec(0, k * 1.2),
|
||||||
|
}),
|
||||||
|
new CubicBezier2d({
|
||||||
|
start: new Vec(0, k * 1.2),
|
||||||
|
cp1: new Vec(0, -k * 0.32),
|
||||||
|
cp2: new Vec(o * 1.85, -k * 0.32),
|
||||||
|
end: new Vec(w / 2, k * 0.9),
|
||||||
|
}),
|
||||||
|
new CubicBezier2d({
|
||||||
|
start: new Vec(w / 2, k * 0.9),
|
||||||
|
cp1: new Vec(o * 2.15, -k * 0.32),
|
||||||
|
cp2: new Vec(w, -k * 0.32),
|
||||||
|
end: new Vec(w, k * 1.2),
|
||||||
|
}),
|
||||||
|
new CubicBezier2d({
|
||||||
|
start: new Vec(w, k * 1.2),
|
||||||
|
cp1: new Vec(w, k * 2.5),
|
||||||
|
cp2: new Vec(o * 2.5, k * 3),
|
||||||
|
end: new Vec(w / 2, h),
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------------------- Ellipse -------------------- */
|
||||||
|
|
||||||
|
function getEllipseStrokeOptions(strokeWidth: number) {
|
||||||
|
return {
|
||||||
|
size: 1 + strokeWidth,
|
||||||
|
thinning: 0.25,
|
||||||
|
end: { taper: strokeWidth },
|
||||||
|
start: { taper: strokeWidth },
|
||||||
|
streamline: 0,
|
||||||
|
smoothing: 1,
|
||||||
|
simulatePressure: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEllipseStrokePoints(id: string, width: number, height: number, strokeWidth: number) {
|
||||||
|
const getRandom = rng(id)
|
||||||
|
|
||||||
|
const rx = width / 2
|
||||||
|
const ry = height / 2
|
||||||
|
const perimeter = perimeterOfEllipse(rx, ry)
|
||||||
|
|
||||||
|
const points: Vec[] = []
|
||||||
|
|
||||||
|
const start = PI2 * getRandom()
|
||||||
|
const length = PI2 + HALF_PI / 2 + Math.abs(getRandom()) * HALF_PI
|
||||||
|
const count = Math.max(16, perimeter / 10)
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const t = i / (count - 1)
|
||||||
|
const r = start + t * length
|
||||||
|
const c = Math.cos(r)
|
||||||
|
const s = Math.sin(r)
|
||||||
|
points.push(
|
||||||
|
new Vec(
|
||||||
|
rx * c + width * 0.5 + 0.05 * getRandom(),
|
||||||
|
ry * s + height / 2 + 0.05 * getRandom(),
|
||||||
|
Math.min(
|
||||||
|
1,
|
||||||
|
0.5 +
|
||||||
|
Math.abs(0.5 - (getRandom() > 0 ? EASINGS.easeInOutSine(t) : EASINGS.easeInExpo(t))) / 2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return getStrokePoints(points, getEllipseStrokeOptions(strokeWidth))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEllipseDrawIndicatorPath(
|
||||||
|
id: string,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
strokeWidth: number
|
||||||
|
) {
|
||||||
|
return getSvgPathFromStrokePoints(getEllipseStrokePoints(id, width, height, strokeWidth))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEllipsePath(w: number, h: number) {
|
||||||
|
const cx = w / 2
|
||||||
|
const cy = h / 2
|
||||||
|
const rx = Math.max(0, cx)
|
||||||
|
const ry = Math.max(0, cy)
|
||||||
|
return `M${cx - rx},${cy}a${rx},${ry},0,1,1,${rx * 2},0a${rx},${ry},0,1,1,-${rx * 2},0`
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------------------- Polygon -------------------- */
|
||||||
|
|
||||||
|
import { VecLike, precise } from '@tldraw/editor'
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export function getRoundedInkyPolygonPath(points: VecLike[]) {
|
||||||
|
let polylineA = `M`
|
||||||
|
|
||||||
|
const len = points.length
|
||||||
|
|
||||||
|
let p0: VecLike
|
||||||
|
let p1: VecLike
|
||||||
|
let p2: VecLike
|
||||||
|
|
||||||
|
for (let i = 0, n = len; i < n; i += 3) {
|
||||||
|
p0 = points[i]
|
||||||
|
p1 = points[i + 1]
|
||||||
|
p2 = points[i + 2]
|
||||||
|
|
||||||
|
polylineA += `${precise(p0)}L${precise(p1)}Q${precise(p2)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
polylineA += `${precise(points[0])}`
|
||||||
|
|
||||||
|
return polylineA
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export function getRoundedPolygonPoints(
|
||||||
|
id: string,
|
||||||
|
outline: VecLike[],
|
||||||
|
offset: number,
|
||||||
|
roundness: number,
|
||||||
|
passes: number
|
||||||
|
) {
|
||||||
|
const results: VecLike[] = []
|
||||||
|
|
||||||
|
const random = rng(id)
|
||||||
|
let p0 = outline[0]
|
||||||
|
let p1: VecLike
|
||||||
|
|
||||||
|
const len = outline.length
|
||||||
|
|
||||||
|
for (let i = 0, n = len * passes; i < n; i++) {
|
||||||
|
p1 = Vec.AddXY(outline[(i + 1) % len], random() * offset, random() * offset)
|
||||||
|
|
||||||
|
const delta = Vec.Sub(p1, p0)
|
||||||
|
const distance = Vec.Len(delta)
|
||||||
|
const vector = Vec.Div(delta, distance).mul(Math.min(distance / 4, roundness))
|
||||||
|
results.push(Vec.Add(p0, vector), Vec.Add(p1, vector.neg()), p1)
|
||||||
|
|
||||||
|
p0 = p1
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------- Cloud --------------------- */
|
||||||
|
|
||||||
|
type PillSection =
|
||||||
|
| {
|
||||||
|
type: 'straight'
|
||||||
|
start: VecModel
|
||||||
|
delta: VecModel
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'arc'
|
||||||
|
center: VecModel
|
||||||
|
startAngle: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPillPoints(width: number, height: number, numPoints: number) {
|
||||||
|
const radius = Math.min(width, height) / 2
|
||||||
|
const longSide = Math.max(width, height) - radius * 2
|
||||||
|
const circumference = Math.PI * (radius * 2) + 2 * longSide
|
||||||
|
const spacing = circumference / numPoints
|
||||||
|
|
||||||
|
const sections: PillSection[] =
|
||||||
|
width > height
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
type: 'straight',
|
||||||
|
start: new Vec(radius, 0),
|
||||||
|
delta: new Vec(1, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'arc',
|
||||||
|
center: new Vec(width - radius, radius),
|
||||||
|
startAngle: -PI / 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'straight',
|
||||||
|
start: new Vec(width - radius, height),
|
||||||
|
delta: new Vec(-1, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'arc',
|
||||||
|
center: new Vec(radius, radius),
|
||||||
|
startAngle: PI / 2,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
type: 'straight',
|
||||||
|
start: new Vec(width, radius),
|
||||||
|
delta: new Vec(0, 1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'arc',
|
||||||
|
center: new Vec(radius, height - radius),
|
||||||
|
startAngle: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'straight',
|
||||||
|
start: new Vec(0, height - radius),
|
||||||
|
delta: new Vec(0, -1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'arc',
|
||||||
|
center: new Vec(radius, radius),
|
||||||
|
startAngle: PI,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
let sectionOffset = 0
|
||||||
|
|
||||||
|
const points: Vec[] = []
|
||||||
|
for (let i = 0; i < numPoints; i++) {
|
||||||
|
const section = sections[0]
|
||||||
|
if (section.type === 'straight') {
|
||||||
|
points.push(Vec.Add(section.start, Vec.Mul(section.delta, sectionOffset)))
|
||||||
|
} else {
|
||||||
|
points.push(
|
||||||
|
getPointOnCircle(section.center, radius, section.startAngle + sectionOffset / radius)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
sectionOffset += spacing
|
||||||
|
let sectionLength = section.type === 'straight' ? longSide : PI * radius
|
||||||
|
while (sectionOffset > sectionLength) {
|
||||||
|
sectionOffset -= sectionLength
|
||||||
|
sections.push(sections.shift()!)
|
||||||
|
sectionLength = sections[0].type === 'straight' ? longSide : PI * radius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return points
|
||||||
|
}
|
||||||
|
|
||||||
|
const SIZES: Record<TLDefaultSizeStyle, number> = {
|
||||||
|
s: 50,
|
||||||
|
m: 70,
|
||||||
|
l: 100,
|
||||||
|
xl: 130,
|
||||||
|
}
|
||||||
|
|
||||||
|
const BUMP_PROTRUSION = 0.2
|
||||||
|
|
||||||
|
export function getCloudArcs(
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
seed: string,
|
||||||
|
size: TLDefaultSizeStyle
|
||||||
|
) {
|
||||||
|
const getRandom = rng(seed)
|
||||||
|
const pillCircumference = getOvalPerimeter(width, height)
|
||||||
|
const numBumps = Math.max(
|
||||||
|
Math.ceil(pillCircumference / SIZES[size]),
|
||||||
|
6,
|
||||||
|
Math.ceil(pillCircumference / Math.min(width, height))
|
||||||
|
)
|
||||||
|
const targetBumpProtrusion = (pillCircumference / numBumps) * BUMP_PROTRUSION
|
||||||
|
|
||||||
|
// if the aspect ratio is high, innerWidth should be smaller
|
||||||
|
const innerWidth = Math.max(width - targetBumpProtrusion * 2, 1)
|
||||||
|
const innerHeight = Math.max(height - targetBumpProtrusion * 2, 1)
|
||||||
|
const innerCircumference = getOvalPerimeter(innerWidth, innerHeight)
|
||||||
|
|
||||||
|
const distanceBetweenPointsOnPerimeter = innerCircumference / numBumps
|
||||||
|
|
||||||
|
const paddingX = (width - innerWidth) / 2
|
||||||
|
const paddingY = (height - innerHeight) / 2
|
||||||
|
const bumpPoints = getPillPoints(innerWidth, innerHeight, numBumps).map((p) => {
|
||||||
|
return p.addXY(paddingX, paddingY)
|
||||||
|
})
|
||||||
|
const maxWiggleX = width < 20 ? 0 : targetBumpProtrusion * 0.3
|
||||||
|
const maxWiggleY = height < 20 ? 0 : targetBumpProtrusion * 0.3
|
||||||
|
|
||||||
|
// wiggle the points from either end so that the bumps 'pop'
|
||||||
|
// in at the bottom-right and the top-left looks relatively stable
|
||||||
|
// note: it's important that we don't mutate here! these points are also the bump points
|
||||||
|
const wiggledPoints = bumpPoints.slice(0)
|
||||||
|
for (let i = 0; i < Math.floor(numBumps / 2); i++) {
|
||||||
|
wiggledPoints[i] = Vec.AddXY(
|
||||||
|
wiggledPoints[i],
|
||||||
|
getRandom() * maxWiggleX,
|
||||||
|
getRandom() * maxWiggleY
|
||||||
|
)
|
||||||
|
wiggledPoints[numBumps - i - 1] = Vec.AddXY(
|
||||||
|
wiggledPoints[numBumps - i - 1],
|
||||||
|
getRandom() * maxWiggleX,
|
||||||
|
getRandom() * maxWiggleY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const arcs: Arc[] = []
|
||||||
|
|
||||||
|
for (let i = 0; i < wiggledPoints.length; i++) {
|
||||||
|
const j = i === wiggledPoints.length - 1 ? 0 : i + 1
|
||||||
|
const leftWigglePoint = wiggledPoints[i]
|
||||||
|
const rightWigglePoint = wiggledPoints[j]
|
||||||
|
const leftPoint = bumpPoints[i]
|
||||||
|
const rightPoint = bumpPoints[j]
|
||||||
|
|
||||||
|
// when the points are on the curvy part of a pill, there is a natural arc that we need to extends past
|
||||||
|
// otherwise it looks like the bumps get less bumpy on the curvy parts
|
||||||
|
const distanceBetweenOriginalPoints = Vec.Dist(leftPoint, rightPoint)
|
||||||
|
const curvatureOffset = distanceBetweenPointsOnPerimeter - distanceBetweenOriginalPoints
|
||||||
|
const distanceBetweenWigglePoints = Vec.Dist(leftWigglePoint, rightWigglePoint)
|
||||||
|
const relativeSize = distanceBetweenWigglePoints / distanceBetweenOriginalPoints
|
||||||
|
const finalDistance = (Math.max(paddingX, paddingY) + curvatureOffset) * relativeSize
|
||||||
|
|
||||||
|
const arcPoint = Vec.Lrp(leftPoint, rightPoint, 0.5).add(
|
||||||
|
Vec.Sub(rightPoint, leftPoint).uni().per().mul(finalDistance)
|
||||||
|
)
|
||||||
|
if (arcPoint.x < 0) {
|
||||||
|
arcPoint.x = 0
|
||||||
|
} else if (arcPoint.x > width) {
|
||||||
|
arcPoint.x = width
|
||||||
|
}
|
||||||
|
if (arcPoint.y < 0) {
|
||||||
|
arcPoint.y = 0
|
||||||
|
} else if (arcPoint.y > height) {
|
||||||
|
arcPoint.y = height
|
||||||
|
}
|
||||||
|
|
||||||
|
const center = centerOfCircleFromThreePoints(leftWigglePoint, rightWigglePoint, arcPoint)
|
||||||
|
const radius = Vec.Dist(
|
||||||
|
center ? center : Vec.Average([leftWigglePoint, rightWigglePoint]),
|
||||||
|
leftWigglePoint
|
||||||
|
)
|
||||||
|
|
||||||
|
// todo: could use Arc2d here
|
||||||
|
|
||||||
|
arcs.push({
|
||||||
|
leftPoint: leftWigglePoint,
|
||||||
|
rightPoint: rightWigglePoint,
|
||||||
|
arcPoint,
|
||||||
|
center,
|
||||||
|
radius,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return arcs
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Arc {
|
||||||
|
leftPoint: Vec
|
||||||
|
rightPoint: Vec
|
||||||
|
arcPoint: Vec
|
||||||
|
center: Vec | null
|
||||||
|
radius: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cloudOutline(
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
seed: string,
|
||||||
|
size: TLDefaultSizeStyle
|
||||||
|
) {
|
||||||
|
const path: Vec[] = []
|
||||||
|
|
||||||
|
const arcs = getCloudArcs(width, height, seed, size)
|
||||||
|
|
||||||
|
for (const { center, radius, leftPoint, rightPoint } of arcs) {
|
||||||
|
path.push(...getPointsOnArc(leftPoint, rightPoint, center, radius, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCloudPath(
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
seed: string,
|
||||||
|
size: TLDefaultSizeStyle
|
||||||
|
) {
|
||||||
|
// const points = cloudOutline(width, height, seed, size)
|
||||||
|
// {
|
||||||
|
// let path = `M${toDomPrecision(points[0].x)},${toDomPrecision(points[0].y)}`
|
||||||
|
// for (const point of points.slice(1)) {
|
||||||
|
// path += ` L${toDomPrecision(point.x)},${toDomPrecision(point.y)}`
|
||||||
|
// }
|
||||||
|
// return path
|
||||||
|
// }
|
||||||
|
|
||||||
|
const arcs = getCloudArcs(width, height, seed, size)
|
||||||
|
let path = `M${arcs[0].leftPoint.toFixed()}`
|
||||||
|
|
||||||
|
// now draw arcs for each circle, starting where it intersects the previous circle, and ending where it intersects the next circle
|
||||||
|
for (const { leftPoint, rightPoint, radius, center } of arcs) {
|
||||||
|
if (center === null) {
|
||||||
|
// draw a line to rightPoint instead
|
||||||
|
path += ` L${rightPoint.toFixed()}`
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// use the large arc if the center of the circle is to the left of the line between the two points
|
||||||
|
const arc = Vec.Clockwise(leftPoint, rightPoint, center) ? '0' : '1'
|
||||||
|
path += ` A${toDomPrecision(radius)},${toDomPrecision(radius)} 0 ${arc},1 ${rightPoint.toFixed()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
path += ' Z'
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
const DRAW_OFFSETS: Record<TLDefaultSizeStyle, number> = {
|
||||||
|
s: 0.5,
|
||||||
|
m: 0.7,
|
||||||
|
l: 0.9,
|
||||||
|
xl: 1.6,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inkyCloudSvgPath(
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
seed: string,
|
||||||
|
size: TLDefaultSizeStyle
|
||||||
|
) {
|
||||||
|
const getRandom = rng(seed)
|
||||||
|
const mutMultiplier = DRAW_OFFSETS[size]
|
||||||
|
const arcs = getCloudArcs(width, height, seed, size)
|
||||||
|
const avgArcLengthSquared =
|
||||||
|
arcs.reduce((sum, arc) => sum + Vec.Dist2(arc.leftPoint, arc.rightPoint), 0) / arcs.length
|
||||||
|
const shouldMutatePoints = avgArcLengthSquared > (mutMultiplier * 15) ** 2
|
||||||
|
const mutPoint = shouldMutatePoints
|
||||||
|
? (p: Vec) => p.addXY(getRandom() * mutMultiplier * 2, getRandom() * mutMultiplier * 2)
|
||||||
|
: (p: Vec) => p
|
||||||
|
let pathA = `M${arcs[0].leftPoint.toFixed()}`
|
||||||
|
let leftMutPoint = mutPoint(arcs[0].leftPoint)
|
||||||
|
let pathB = `M${leftMutPoint.toFixed()}`
|
||||||
|
|
||||||
|
for (const { leftPoint, center, rightPoint, radius, arcPoint } of arcs) {
|
||||||
|
if (center === null) {
|
||||||
|
// draw a line to rightPoint instead
|
||||||
|
pathA += ` L${rightPoint.toFixed()}`
|
||||||
|
const rightMutPoint = mutPoint(rightPoint)
|
||||||
|
pathB += ` L${rightMutPoint.toFixed()}`
|
||||||
|
leftMutPoint = rightMutPoint
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const arc = Vec.Clockwise(leftPoint, rightPoint, center) ? '0' : '1'
|
||||||
|
pathA += ` A${toDomPrecision(radius)},${toDomPrecision(radius)} 0 ${arc},1 ${rightPoint.toFixed()}`
|
||||||
|
const rightMutPoint = mutPoint(rightPoint)
|
||||||
|
const mutArcPoint = mutPoint(arcPoint)
|
||||||
|
const mutCenter = centerOfCircleFromThreePoints(leftMutPoint, rightMutPoint, mutArcPoint)
|
||||||
|
|
||||||
|
// handle situations where the points are colinear (this happens when the cloud is very small)
|
||||||
|
if (!Number.isFinite(mutCenter.x) || !Number.isFinite(mutCenter.y)) {
|
||||||
|
// draw a line to rightMutPoint instead
|
||||||
|
pathB += ` L${rightMutPoint.toFixed()}`
|
||||||
|
leftMutPoint = rightMutPoint
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutRadius = Math.abs(Vec.Dist(mutCenter, leftMutPoint))
|
||||||
|
pathB += ` A${toDomPrecision(mutRadius)},${toDomPrecision(
|
||||||
|
mutRadius
|
||||||
|
)} 0 ${arc},1 ${rightMutPoint.toFixed()}`
|
||||||
|
leftMutPoint = rightMutPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathA + pathB + ' Z'
|
||||||
|
}
|
|
@ -1,34 +0,0 @@
|
||||||
import { perimeterOfEllipse } from '@tldraw/editor'
|
|
||||||
|
|
||||||
export function getOvalSolidPath(w: number, h: number) {
|
|
||||||
if (h > w) {
|
|
||||||
const offset = w / 2
|
|
||||||
|
|
||||||
return `
|
|
||||||
M0,${offset}
|
|
||||||
a${offset},${offset},0,1,1,${offset * 2},0
|
|
||||||
L${w},${h - offset}
|
|
||||||
a${offset},${offset},0,1,1,-${offset * 2},0
|
|
||||||
Z`
|
|
||||||
}
|
|
||||||
|
|
||||||
const offset = h / 2
|
|
||||||
|
|
||||||
return `
|
|
||||||
M${offset},0
|
|
||||||
L${w - offset},0
|
|
||||||
a${offset},${offset},0,1,1,0,${offset * 2}
|
|
||||||
L${offset},${h}
|
|
||||||
a${offset},${offset},0,1,1,0,${-offset * 2}
|
|
||||||
Z`
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getOvalPerimeter(h: number, w: number) {
|
|
||||||
if (h > w) {
|
|
||||||
const offset = w / 2
|
|
||||||
return perimeterOfEllipse(offset, offset) + (h - offset * 2) * 2
|
|
||||||
}
|
|
||||||
|
|
||||||
const offset = h / 2
|
|
||||||
return perimeterOfEllipse(offset, offset) + (w - offset * 2) * 2
|
|
||||||
}
|
|
|
@ -22,13 +22,8 @@ import {
|
||||||
import { ShapeFill, useDefaultColorTheme } from '../shared/ShapeFill'
|
import { ShapeFill, useDefaultColorTheme } from '../shared/ShapeFill'
|
||||||
import { STROKE_SIZES } from '../shared/default-shape-constants'
|
import { STROKE_SIZES } from '../shared/default-shape-constants'
|
||||||
import { getPerfectDashProps } from '../shared/getPerfectDashProps'
|
import { getPerfectDashProps } from '../shared/getPerfectDashProps'
|
||||||
import { getDrawLinePathData } from '../shared/polygon-helpers'
|
|
||||||
import { getLineDrawPath, getLineIndicatorPath } from './components/getLinePath'
|
import { getLineDrawPath, getLineIndicatorPath } from './components/getLinePath'
|
||||||
import {
|
import { getDrawLinePathData } from './line-helpers'
|
||||||
getSvgPathForBezierCurve,
|
|
||||||
getSvgPathForEdge,
|
|
||||||
getSvgPathForLineGeometry,
|
|
||||||
} from './components/svg'
|
|
||||||
|
|
||||||
const handlesCache = new WeakCache<TLLineShape['props'], TLHandle[]>()
|
const handlesCache = new WeakCache<TLLineShape['props'], TLHandle[]>()
|
||||||
|
|
||||||
|
@ -254,7 +249,7 @@ function LineShapeSvg({ shape }: { shape: TLLineShape }) {
|
||||||
key={i}
|
key={i}
|
||||||
strokeDasharray={strokeDasharray}
|
strokeDasharray={strokeDasharray}
|
||||||
strokeDashoffset={strokeDashoffset}
|
strokeDashoffset={strokeDashoffset}
|
||||||
d={getSvgPathForEdge(segment as any, true)}
|
d={segment.getSvgPathData(true)}
|
||||||
fill="none"
|
fill="none"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -283,7 +278,7 @@ function LineShapeSvg({ shape }: { shape: TLLineShape }) {
|
||||||
}
|
}
|
||||||
// Cubic style spline
|
// Cubic style spline
|
||||||
if (shape.props.spline === 'cubic') {
|
if (shape.props.spline === 'cubic') {
|
||||||
const splinePath = getSvgPathForLineGeometry(spline)
|
const splinePath = spline.getSvgPathData()
|
||||||
if (dash === 'solid') {
|
if (dash === 'solid') {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -314,7 +309,7 @@ function LineShapeSvg({ shape }: { shape: TLLineShape }) {
|
||||||
key={i}
|
key={i}
|
||||||
strokeDasharray={strokeDasharray}
|
strokeDasharray={strokeDasharray}
|
||||||
strokeDashoffset={strokeDashoffset}
|
strokeDashoffset={strokeDashoffset}
|
||||||
d={getSvgPathForBezierCurve(segment as any, true)}
|
d={segment.getSvgPathData()}
|
||||||
fill="none"
|
fill="none"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { getStrokeOutlinePoints } from '../../shared/freehand/getStrokeOutlinePo
|
||||||
import { getStrokePoints } from '../../shared/freehand/getStrokePoints'
|
import { getStrokePoints } from '../../shared/freehand/getStrokePoints'
|
||||||
import { setStrokePointRadii } from '../../shared/freehand/setStrokePointRadii'
|
import { setStrokePointRadii } from '../../shared/freehand/setStrokePointRadii'
|
||||||
import { getSvgPathFromStrokePoints } from '../../shared/freehand/svg'
|
import { getSvgPathFromStrokePoints } from '../../shared/freehand/svg'
|
||||||
import { getSvgPathForLineGeometry } from './svg'
|
|
||||||
|
|
||||||
function getLineDrawFreehandOptions(strokeWidth: number) {
|
function getLineDrawFreehandOptions(strokeWidth: number) {
|
||||||
return {
|
return {
|
||||||
|
@ -58,5 +57,5 @@ export function getLineIndicatorPath(
|
||||||
return getSvgPathFromStrokePoints(strokePoints)
|
return getSvgPathFromStrokePoints(strokePoints)
|
||||||
}
|
}
|
||||||
|
|
||||||
return getSvgPathForLineGeometry(spline)
|
return spline.getSvgPathData()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
import {
|
|
||||||
CubicBezier2d,
|
|
||||||
CubicSpline2d,
|
|
||||||
Edge2d,
|
|
||||||
Polyline2d,
|
|
||||||
Vec,
|
|
||||||
toDomPrecision,
|
|
||||||
} from '@tldraw/editor'
|
|
||||||
|
|
||||||
export function getSvgPathForEdge(edge: Edge2d, first: boolean) {
|
|
||||||
const { start, end } = edge
|
|
||||||
if (first) {
|
|
||||||
return `M${toDomPrecision(start.x)},${toDomPrecision(start.y)} L${toDomPrecision(
|
|
||||||
end.x
|
|
||||||
)},${toDomPrecision(end.y)} `
|
|
||||||
}
|
|
||||||
return `${toDomPrecision(end.x)},${toDomPrecision(end.y)} `
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSvgPathForBezierCurve(curve: CubicBezier2d, first: boolean) {
|
|
||||||
const { a, b, c, d } = curve
|
|
||||||
|
|
||||||
if (Vec.Equals(a, d)) return ''
|
|
||||||
|
|
||||||
return `${first ? `M${toDomPrecision(a.x)},${toDomPrecision(a.y)}` : ``}C${toDomPrecision(
|
|
||||||
b.x
|
|
||||||
)},${toDomPrecision(b.y)} ${toDomPrecision(c.x)},${toDomPrecision(c.y)} ${toDomPrecision(
|
|
||||||
d.x
|
|
||||||
)},${toDomPrecision(d.y)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSvgPathForCubicSpline(spline: CubicSpline2d, isClosed: boolean) {
|
|
||||||
let d = spline.segments.reduce((d, segment, i) => {
|
|
||||||
return d + getSvgPathForBezierCurve(segment, i === 0)
|
|
||||||
}, '')
|
|
||||||
|
|
||||||
if (isClosed) {
|
|
||||||
d += 'Z'
|
|
||||||
}
|
|
||||||
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSvgPathForPolylineSpline(spline: Polyline2d, isClosed: boolean) {
|
|
||||||
let d = spline.segments.reduce((d, segment, i) => {
|
|
||||||
return d + getSvgPathForEdge(segment, i === 0)
|
|
||||||
}, '')
|
|
||||||
|
|
||||||
if (isClosed) {
|
|
||||||
d += 'Z'
|
|
||||||
}
|
|
||||||
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSvgPathForLineGeometry(spline: CubicSpline2d | Polyline2d, isClosed = false) {
|
|
||||||
if (spline instanceof Polyline2d) {
|
|
||||||
return getSvgPathForPolylineSpline(spline, isClosed)
|
|
||||||
} else {
|
|
||||||
return getSvgPathForCubicSpline(spline, isClosed)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,58 +1,5 @@
|
||||||
import { Vec, VecLike, precise, rng } from '@tldraw/editor'
|
import { Vec, VecLike, precise, rng } from '@tldraw/editor'
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export function getRoundedInkyPolygonPath(points: VecLike[]) {
|
|
||||||
let polylineA = `M`
|
|
||||||
|
|
||||||
const len = points.length
|
|
||||||
|
|
||||||
let p0: VecLike
|
|
||||||
let p1: VecLike
|
|
||||||
let p2: VecLike
|
|
||||||
|
|
||||||
for (let i = 0, n = len; i < n; i += 3) {
|
|
||||||
p0 = points[i]
|
|
||||||
p1 = points[i + 1]
|
|
||||||
p2 = points[i + 2]
|
|
||||||
|
|
||||||
polylineA += `${precise(p0)}L${precise(p1)}Q${precise(p2)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
polylineA += `${precise(points[0])}`
|
|
||||||
|
|
||||||
return polylineA
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export function getRoundedPolygonPoints(
|
|
||||||
id: string,
|
|
||||||
outline: VecLike[],
|
|
||||||
offset: number,
|
|
||||||
roundness: number,
|
|
||||||
passes: number
|
|
||||||
) {
|
|
||||||
const results: VecLike[] = []
|
|
||||||
|
|
||||||
const random = rng(id)
|
|
||||||
let p0 = outline[0]
|
|
||||||
let p1: VecLike
|
|
||||||
|
|
||||||
const len = outline.length
|
|
||||||
|
|
||||||
for (let i = 0, n = len * passes; i < n; i++) {
|
|
||||||
p1 = Vec.AddXY(outline[(i + 1) % len], random() * offset, random() * offset)
|
|
||||||
|
|
||||||
const delta = Vec.Sub(p1, p0)
|
|
||||||
const distance = Vec.Len(delta)
|
|
||||||
const vector = Vec.Div(delta, distance).mul(Math.min(distance / 4, roundness))
|
|
||||||
results.push(Vec.Add(p0, vector), Vec.Add(p1, vector.neg()), p1)
|
|
||||||
|
|
||||||
p0 = p1
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function getDrawLinePathData(id: string, outline: VecLike[], strokeWidth: number) {
|
export function getDrawLinePathData(id: string, outline: VecLike[], strokeWidth: number) {
|
||||||
let innerPathData = `M ${precise(outline[0])}L`
|
let innerPathData = `M ${precise(outline[0])}L`
|
|
@ -70,16 +70,15 @@ export function getPerfectDashProps(
|
||||||
dashCount -= dashCount % snap
|
dashCount -= dashCount % snap
|
||||||
|
|
||||||
if (dashCount < 3 && style === 'dashed') {
|
if (dashCount < 3 && style === 'dashed') {
|
||||||
if (totalLength / strokeWidth < 5) {
|
if (totalLength / strokeWidth < 4) {
|
||||||
dashLength = totalLength
|
dashLength = totalLength
|
||||||
dashCount = 1
|
dashCount = 1
|
||||||
gapLength = 0
|
gapLength = 0
|
||||||
} else {
|
} else {
|
||||||
dashLength = totalLength * 0.333
|
dashLength = totalLength * (1 / 3)
|
||||||
gapLength = totalLength * 0.333
|
gapLength = totalLength * (1 / 3)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dashCount = Math.max(dashCount, 3)
|
|
||||||
dashLength = totalLength / dashCount / (2 * ratio)
|
dashLength = totalLength / dashCount / (2 * ratio)
|
||||||
|
|
||||||
if (closed) {
|
if (closed) {
|
||||||
|
|
|
@ -78,6 +78,7 @@ export const STYLES = {
|
||||||
{ value: 'cloud', icon: 'geo-cloud' },
|
{ value: 'cloud', icon: 'geo-cloud' },
|
||||||
{ value: 'x-box', icon: 'geo-x-box' },
|
{ value: 'x-box', icon: 'geo-x-box' },
|
||||||
{ value: 'check-box', icon: 'geo-check-box' },
|
{ value: 'check-box', icon: 'geo-check-box' },
|
||||||
|
{ value: 'heart', icon: 'geo-heart' },
|
||||||
],
|
],
|
||||||
arrowheadStart: [
|
arrowheadStart: [
|
||||||
{ value: 'none', icon: 'arrowhead-none' },
|
{ value: 'none', icon: 'arrowhead-none' },
|
||||||
|
|
|
@ -41,13 +41,12 @@ const CurrentState = track(function CurrentState() {
|
||||||
shape && path.includes('select.')
|
shape && path.includes('select.')
|
||||||
? ` / ${shape.type || ''}${
|
? ` / ${shape.type || ''}${
|
||||||
'geo' in shape.props ? ' / ' + shape.props.geo : ''
|
'geo' in shape.props ? ' / ' + shape.props.geo : ''
|
||||||
} / [${Vec.ToFixed(editor.getPointInShapeSpace(shape, editor.inputs.currentPagePoint), 0)}]`
|
} / [${Vec.ToInt(editor.getPointInShapeSpace(shape, editor.inputs.currentPagePoint))}]`
|
||||||
: ''
|
: ''
|
||||||
const ruler =
|
const ruler =
|
||||||
path.startsWith('select.') && !path.includes('.idle')
|
path.startsWith('select.') && !path.includes('.idle')
|
||||||
? ` / [${Vec.ToFixed(editor.inputs.originPagePoint, 0)}] → [${Vec.ToFixed(
|
? ` / [${Vec.ToInt(editor.inputs.originPagePoint)}] → [${Vec.ToInt(
|
||||||
editor.inputs.currentPagePoint,
|
editor.inputs.currentPagePoint
|
||||||
0
|
|
||||||
)}] = ${Vec.Dist(editor.inputs.originPagePoint, editor.inputs.currentPagePoint).toFixed(0)}`
|
)}] = ${Vec.Dist(editor.inputs.originPagePoint, editor.inputs.currentPagePoint).toFixed(0)}`
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
|
|
|
@ -18,12 +18,12 @@ export function DefaultToolbarContent() {
|
||||||
<EllipseToolbarItem />
|
<EllipseToolbarItem />
|
||||||
<TriangleToolbarItem />
|
<TriangleToolbarItem />
|
||||||
<DiamondToolbarItem />
|
<DiamondToolbarItem />
|
||||||
<CloudToolbarItem />
|
|
||||||
<StarToolbarItem />
|
|
||||||
<HexagonToolbarItem />
|
<HexagonToolbarItem />
|
||||||
<OvalToolbarItem />
|
<OvalToolbarItem />
|
||||||
<TrapezoidToolbarItem />
|
|
||||||
<RhombusToolbarItem />
|
<RhombusToolbarItem />
|
||||||
|
<StarToolbarItem />
|
||||||
|
<CloudToolbarItem />
|
||||||
|
<HeartToolbarItem />
|
||||||
<XBoxToolbarItem />
|
<XBoxToolbarItem />
|
||||||
<CheckBoxToolbarItem />
|
<CheckBoxToolbarItem />
|
||||||
<ArrowLeftToolbarItem />
|
<ArrowLeftToolbarItem />
|
||||||
|
@ -54,204 +54,159 @@ export function useIsToolSelected(tool: TLUiToolItem) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function SelectToolbarItem() {
|
export function ToolbarItem({ tool }: { tool: string }) {
|
||||||
const tools = useTools()
|
const tools = useTools()
|
||||||
const isSelected = useIsToolSelected(tools['select'])
|
const isSelected = useIsToolSelected(tools[tool])
|
||||||
return <TldrawUiMenuItem {...tools['select']} isSelected={isSelected} />
|
return <TldrawUiMenuItem {...tools[tool]} isSelected={isSelected} />
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export function SelectToolbarItem() {
|
||||||
|
return <ToolbarItem tool="select" />
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function HandToolbarItem() {
|
export function HandToolbarItem() {
|
||||||
const tools = useTools()
|
return <ToolbarItem tool="hand" />
|
||||||
const isSelected = useIsToolSelected(tools['hand'])
|
|
||||||
return <TldrawUiMenuItem {...tools['hand']} isSelected={isSelected} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function DrawToolbarItem() {
|
export function DrawToolbarItem() {
|
||||||
const tools = useTools()
|
return <ToolbarItem tool="draw" />
|
||||||
const isSelected = useIsToolSelected(tools['draw'])
|
|
||||||
return <TldrawUiMenuItem {...tools['draw']} isSelected={isSelected} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function EraserToolbarItem() {
|
export function EraserToolbarItem() {
|
||||||
const tools = useTools()
|
return <ToolbarItem tool="eraser" />
|
||||||
const isSelected = useIsToolSelected(tools['eraser'])
|
|
||||||
return <TldrawUiMenuItem {...tools['eraser']} isSelected={isSelected} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function ArrowToolbarItem() {
|
export function ArrowToolbarItem() {
|
||||||
const tools = useTools()
|
return <ToolbarItem tool="arrow" />
|
||||||
const isSelected = useIsToolSelected(tools['arrow'])
|
|
||||||
return <TldrawUiMenuItem {...tools['arrow']} isSelected={isSelected} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function TextToolbarItem() {
|
export function TextToolbarItem() {
|
||||||
const tools = useTools()
|
return <ToolbarItem tool="text" />
|
||||||
const isSelected = useIsToolSelected(tools['text'])
|
|
||||||
return <TldrawUiMenuItem {...tools['text']} isSelected={isSelected} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function NoteToolbarItem() {
|
export function NoteToolbarItem() {
|
||||||
const tools = useTools()
|
return <ToolbarItem tool="note" />
|
||||||
const isSelected = useIsToolSelected(tools['note'])
|
|
||||||
return <TldrawUiMenuItem {...tools['note']} isSelected={isSelected} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function AssetToolbarItem() {
|
export function AssetToolbarItem() {
|
||||||
const tools = useTools()
|
const tools = useTools()
|
||||||
const isSelected = useIsToolSelected(tools['asset'])
|
return <TldrawUiMenuItem {...tools['asset']} />
|
||||||
return <TldrawUiMenuItem {...tools['asset']} isSelected={isSelected} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function RectangleToolbarItem() {
|
export function RectangleToolbarItem() {
|
||||||
const tools = useTools()
|
return <ToolbarItem tool="rectangle" />
|
||||||
const isSelected = useIsToolSelected(tools['rectangle'])
|
|
||||||
return <TldrawUiMenuItem {...tools['rectangle']} isSelected={isSelected} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function EllipseToolbarItem() {
|
export function EllipseToolbarItem() {
|
||||||
const tools = useTools()
|
return <ToolbarItem tool="ellipse" />
|
||||||
const isSelected = useIsToolSelected(tools['ellipse'])
|
|
||||||
return <TldrawUiMenuItem {...tools['ellipse']} isSelected={isSelected} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function DiamondToolbarItem() {
|
export function DiamondToolbarItem() {
|
||||||
const tools = useTools()
|
return <ToolbarItem tool="diamond" />
|
||||||
const isSelected = useIsToolSelected(tools['diamond'])
|
|
||||||
return <TldrawUiMenuItem {...tools['diamond']} isSelected={isSelected} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function TriangleToolbarItem() {
|
export function TriangleToolbarItem() {
|
||||||
const tools = useTools()
|
return <ToolbarItem tool="triangle" />
|
||||||
const isSelected = useIsToolSelected(tools['triangle'])
|
|
||||||
return <TldrawUiMenuItem {...tools['triangle']} isSelected={isSelected} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function TrapezoidToolbarItem() {
|
export function TrapezoidToolbarItem() {
|
||||||
const tools = useTools()
|
return <ToolbarItem tool="trapezoid" />
|
||||||
const isSelected = useIsToolSelected(tools['trapezoid'])
|
|
||||||
return <TldrawUiMenuItem {...tools['trapezoid']} isSelected={isSelected} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function RhombusToolbarItem() {
|
export function RhombusToolbarItem() {
|
||||||
const tools = useTools()
|
return <ToolbarItem tool="rhombus" />
|
||||||
const isSelected = useIsToolSelected(tools['rhombus'])
|
|
||||||
return <TldrawUiMenuItem {...tools['rhombus']} isSelected={isSelected} />
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export function HexagonToolbarItem() {
|
|
||||||
const tools = useTools()
|
|
||||||
const isSelected = useIsToolSelected(tools['hexagon'])
|
|
||||||
return <TldrawUiMenuItem {...tools['hexagon']} isSelected={isSelected} />
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export function CloudToolbarItem() {
|
|
||||||
const tools = useTools()
|
|
||||||
const isSelected = useIsToolSelected(tools['cloud'])
|
|
||||||
return <TldrawUiMenuItem {...tools['cloud']} isSelected={isSelected} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function PentagonToolbarItem() {
|
export function PentagonToolbarItem() {
|
||||||
const tools = useTools()
|
return <ToolbarItem tool="pentagon" />
|
||||||
const isSelected = useIsToolSelected(tools['pentagon'])
|
}
|
||||||
return <TldrawUiMenuItem {...tools['pentagon']} isSelected={isSelected} />
|
|
||||||
|
/** @public */
|
||||||
|
export function HeartToolbarItem() {
|
||||||
|
return <ToolbarItem tool="heart" />
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export function HexagonToolbarItem() {
|
||||||
|
return <ToolbarItem tool="hexagon" />
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export function CloudToolbarItem() {
|
||||||
|
return <ToolbarItem tool="cloud" />
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function StarToolbarItem() {
|
export function StarToolbarItem() {
|
||||||
const tools = useTools()
|
return <ToolbarItem tool="star" />
|
||||||
const isSelected = useIsToolSelected(tools['star'])
|
|
||||||
return <TldrawUiMenuItem {...tools['star']} isSelected={isSelected} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function OvalToolbarItem() {
|
export function OvalToolbarItem() {
|
||||||
const tools = useTools()
|
return <ToolbarItem tool="oval" />
|
||||||
const isSelected = useIsToolSelected(tools['oval'])
|
|
||||||
return <TldrawUiMenuItem {...tools['oval']} isSelected={isSelected} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function XBoxToolbarItem() {
|
export function XBoxToolbarItem() {
|
||||||
const tools = useTools()
|
return <ToolbarItem tool="x-box" />
|
||||||
const isSelected = useIsToolSelected(tools['x-box'])
|
|
||||||
return <TldrawUiMenuItem {...tools['x-box']} isSelected={isSelected} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function CheckBoxToolbarItem() {
|
export function CheckBoxToolbarItem() {
|
||||||
const tools = useTools()
|
return <ToolbarItem tool="check-box" />
|
||||||
const isSelected = useIsToolSelected(tools['check-box'])
|
|
||||||
return <TldrawUiMenuItem {...tools['check-box']} isSelected={isSelected} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function ArrowLeftToolbarItem() {
|
export function ArrowLeftToolbarItem() {
|
||||||
const tools = useTools()
|
return <ToolbarItem tool="arrow-left" />
|
||||||
const isSelected = useIsToolSelected(tools['arrow-left'])
|
|
||||||
return <TldrawUiMenuItem {...tools['arrow-left']} isSelected={isSelected} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function ArrowUpToolbarItem() {
|
export function ArrowUpToolbarItem() {
|
||||||
const tools = useTools()
|
return <ToolbarItem tool="arrow-up" />
|
||||||
const isSelected = useIsToolSelected(tools['arrow-up'])
|
|
||||||
return <TldrawUiMenuItem {...tools['arrow-up']} isSelected={isSelected} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function ArrowDownToolbarItem() {
|
export function ArrowDownToolbarItem() {
|
||||||
const tools = useTools()
|
return <ToolbarItem tool="arrow-down" />
|
||||||
const isSelected = useIsToolSelected(tools['arrow-down'])
|
|
||||||
return <TldrawUiMenuItem {...tools['arrow-down']} isSelected={isSelected} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function ArrowRightToolbarItem() {
|
export function ArrowRightToolbarItem() {
|
||||||
const tools = useTools()
|
return <ToolbarItem tool="arrow-right" />
|
||||||
const isSelected = useIsToolSelected(tools['arrow-right'])
|
|
||||||
return <TldrawUiMenuItem {...tools['arrow-right']} isSelected={isSelected} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function LineToolbarItem() {
|
export function LineToolbarItem() {
|
||||||
const tools = useTools()
|
return <ToolbarItem tool="line" />
|
||||||
const isSelected = useIsToolSelected(tools['line'])
|
|
||||||
return <TldrawUiMenuItem {...tools['line']} isSelected={isSelected} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function HighlightToolbarItem() {
|
export function HighlightToolbarItem() {
|
||||||
const tools = useTools()
|
return <ToolbarItem tool="highlight" />
|
||||||
const isSelected = useIsToolSelected(tools['highlight'])
|
|
||||||
return <TldrawUiMenuItem {...tools['highlight']} isSelected={isSelected} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function FrameToolbarItem() {
|
export function FrameToolbarItem() {
|
||||||
const tools = useTools()
|
return <ToolbarItem tool="frame" />
|
||||||
const isSelected = useIsToolSelected(tools['frame'])
|
|
||||||
return <TldrawUiMenuItem {...tools['frame']} isSelected={isSelected} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function LaserToolbarItem() {
|
export function LaserToolbarItem() {
|
||||||
const tools = useTools()
|
return <ToolbarItem tool="laser" />
|
||||||
const isSelected = useIsToolSelected(tools['laser'])
|
|
||||||
return <TldrawUiMenuItem {...tools['laser']} isSelected={isSelected} />
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,6 +68,7 @@ export type TLUiIconType =
|
||||||
| 'geo-cloud'
|
| 'geo-cloud'
|
||||||
| 'geo-diamond'
|
| 'geo-diamond'
|
||||||
| 'geo-ellipse'
|
| 'geo-ellipse'
|
||||||
|
| 'geo-heart'
|
||||||
| 'geo-hexagon'
|
| 'geo-hexagon'
|
||||||
| 'geo-octagon'
|
| 'geo-octagon'
|
||||||
| 'geo-oval'
|
| 'geo-oval'
|
||||||
|
@ -209,6 +210,7 @@ export const iconTypes = [
|
||||||
'geo-cloud',
|
'geo-cloud',
|
||||||
'geo-diamond',
|
'geo-diamond',
|
||||||
'geo-ellipse',
|
'geo-ellipse',
|
||||||
|
'geo-heart',
|
||||||
'geo-hexagon',
|
'geo-hexagon',
|
||||||
'geo-octagon',
|
'geo-octagon',
|
||||||
'geo-oval',
|
'geo-oval',
|
||||||
|
|
|
@ -512,7 +512,7 @@ export const frameShapeProps: {
|
||||||
};
|
};
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const GeoShapeGeoStyle: EnumStyleProp<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "cloud" | "diamond" | "ellipse" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">;
|
export const GeoShapeGeoStyle: EnumStyleProp<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "cloud" | "diamond" | "ellipse" | "heart" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const geoShapeMigrations: TLPropsMigrations;
|
export const geoShapeMigrations: TLPropsMigrations;
|
||||||
|
@ -524,7 +524,7 @@ export const geoShapeProps: {
|
||||||
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
|
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
|
||||||
fill: EnumStyleProp<"none" | "pattern" | "semi" | "solid">;
|
fill: EnumStyleProp<"none" | "pattern" | "semi" | "solid">;
|
||||||
font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
|
font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
|
||||||
geo: EnumStyleProp<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "cloud" | "diamond" | "ellipse" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">;
|
geo: EnumStyleProp<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "cloud" | "diamond" | "ellipse" | "heart" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">;
|
||||||
growY: T.Validator<number>;
|
growY: T.Validator<number>;
|
||||||
h: T.Validator<number>;
|
h: T.Validator<number>;
|
||||||
labelColor: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
labelColor: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
||||||
|
|
|
@ -37,6 +37,7 @@ export const GeoShapeGeoStyle = StyleProp.defineEnum('tldraw:geo', {
|
||||||
'arrow-down',
|
'arrow-down',
|
||||||
'x-box',
|
'x-box',
|
||||||
'check-box',
|
'check-box',
|
||||||
|
'heart',
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue