Add cloud shape (#1708)
![Kapture 2023-07-04 at 16 36 31](https://github.com/tldraw/tldraw/assets/1242537/bcb19959-ac66-46fa-92ea-50fe4692a96c) ### Change Type - [x] `minor` — New feature [^1]: publishes a `patch` release, for devDependencies use `internal` [^2]: will not publish a new version ### Test Plan 1. Make some cloud shapes, try different sizes, colors, fills. 2. Export cloud shapes to images. - [ ] Unit Tests - [ ] End to end tests ### Release Notes - Adds a cloud shape.
This commit is contained in:
parent
910be6073f
commit
83a391b46b
33 changed files with 752 additions and 32 deletions
|
@ -14,7 +14,7 @@ const clickableShapeCreators = [
|
|||
{ tool: 'ellipse', shape: 'geo' },
|
||||
{ tool: 'triangle', shape: 'geo' },
|
||||
{ tool: 'diamond', shape: 'geo' },
|
||||
{ tool: 'pentagon', shape: 'geo' },
|
||||
{ tool: 'cloud', shape: 'geo' },
|
||||
{ tool: 'hexagon', shape: 'geo' },
|
||||
// { tool: 'octagon', shape: 'geo' },
|
||||
{ tool: 'star', shape: 'geo' },
|
||||
|
@ -40,7 +40,7 @@ const draggableShapeCreators = [
|
|||
{ tool: 'ellipse', shape: 'geo' },
|
||||
{ tool: 'triangle', shape: 'geo' },
|
||||
{ tool: 'diamond', shape: 'geo' },
|
||||
{ tool: 'pentagon', shape: 'geo' },
|
||||
{ tool: 'cloud', shape: 'geo' },
|
||||
{ tool: 'hexagon', shape: 'geo' },
|
||||
// { tool: 'octagon', shape: 'geo' },
|
||||
{ tool: 'star', shape: 'geo' },
|
||||
|
|
6
assets/icons/icon/geo-cloud.svg
Normal file
6
assets/icons/icon/geo-cloud.svg
Normal file
|
@ -0,0 +1,6 @@
|
|||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="path-1-inside-1_2338_126559" fill="white">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3232 12.3709C26.7125 12.788 29.3367 15.6771 29.3367 19.1792C29.3367 22.9679 26.2653 26.0393 22.4766 26.0393H7.52351C3.73481 26.0393 0.663452 22.9679 0.663452 19.1792C0.663452 15.6772 3.28755 12.7881 6.67673 12.3709C6.78558 7.86748 10.4703 4.25024 15 4.25024C19.5296 4.25024 23.2143 7.86747 23.3232 12.3709Z"/>
|
||||
</mask>
|
||||
<path d="M23.3232 12.3709L21.3238 12.4192L21.3655 14.145L23.0789 14.3559L23.3232 12.3709ZM6.67673 12.3709L6.92109 14.3559L8.63443 14.145L8.67615 12.4192L6.67673 12.3709ZM23.0789 14.3559C25.478 14.6511 27.3367 16.6996 27.3367 19.1792H31.3367C31.3367 14.6546 27.9469 10.9248 23.5675 10.3858L23.0789 14.3559ZM27.3367 19.1792C27.3367 21.8634 25.1607 24.0393 22.4766 24.0393V28.0393C27.3699 28.0393 31.3367 24.0725 31.3367 19.1792H27.3367ZM22.4766 24.0393H7.52351V28.0393H22.4766V24.0393ZM7.52351 24.0393C4.83938 24.0393 2.66345 21.8634 2.66345 19.1792H-1.33655C-1.33655 24.0725 2.63024 28.0393 7.52351 28.0393V24.0393ZM2.66345 19.1792C2.66345 16.6997 4.52205 14.6512 6.92109 14.3559L6.43237 10.3859C2.05304 10.925 -1.33655 14.6547 -1.33655 19.1792H2.66345ZM8.67615 12.4192C8.75882 8.99859 11.5587 6.25024 15 6.25024V2.25024C9.38196 2.25024 4.81233 6.73637 4.67731 12.3226L8.67615 12.4192ZM15 6.25024C18.4412 6.25024 21.2411 8.99858 21.3238 12.4192L25.3226 12.3225C25.1875 6.73636 20.6179 2.25024 15 2.25024V6.25024Z" fill="black" mask="url(#path-1-inside-1_2338_126559)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -141,6 +141,7 @@
|
|||
"geo-style.hexagon": "Hexagon",
|
||||
"geo-style.octagon": "Octagon",
|
||||
"geo-style.oval": "Oval",
|
||||
"geo-style.cloud": "Cloud",
|
||||
"geo-style.pentagon": "Pentagon",
|
||||
"geo-style.rectangle": "Rectangle",
|
||||
"geo-style.rhombus-2": "Rhombus 2",
|
||||
|
|
|
@ -101,6 +101,7 @@ import iconsGeoArrowLeft from './icons/icon/geo-arrow-left.svg'
|
|||
import iconsGeoArrowRight from './icons/icon/geo-arrow-right.svg'
|
||||
import iconsGeoArrowUp from './icons/icon/geo-arrow-up.svg'
|
||||
import iconsGeoCheckBox from './icons/icon/geo-check-box.svg'
|
||||
import iconsGeoCloud from './icons/icon/geo-cloud.svg'
|
||||
import iconsGeoDiamond from './icons/icon/geo-diamond.svg'
|
||||
import iconsGeoEllipse from './icons/icon/geo-ellipse.svg'
|
||||
import iconsGeoHexagon from './icons/icon/geo-hexagon.svg'
|
||||
|
@ -311,6 +312,7 @@ export function getAssetUrlsByImport(opts) {
|
|||
'geo-arrow-right': formatAssetUrl(iconsGeoArrowRight, opts),
|
||||
'geo-arrow-up': formatAssetUrl(iconsGeoArrowUp, opts),
|
||||
'geo-check-box': formatAssetUrl(iconsGeoCheckBox, opts),
|
||||
'geo-cloud': formatAssetUrl(iconsGeoCloud, opts),
|
||||
'geo-diamond': formatAssetUrl(iconsGeoDiamond, opts),
|
||||
'geo-ellipse': formatAssetUrl(iconsGeoEllipse, opts),
|
||||
'geo-hexagon': formatAssetUrl(iconsGeoHexagon, opts),
|
||||
|
|
|
@ -97,6 +97,7 @@ export function getAssetUrls(opts) {
|
|||
'geo-arrow-right': formatAssetUrl('./icons/icon/geo-arrow-right.svg', opts),
|
||||
'geo-arrow-up': formatAssetUrl('./icons/icon/geo-arrow-up.svg', opts),
|
||||
'geo-check-box': formatAssetUrl('./icons/icon/geo-check-box.svg', opts),
|
||||
'geo-cloud': formatAssetUrl('./icons/icon/geo-cloud.svg', opts),
|
||||
'geo-diamond': formatAssetUrl('./icons/icon/geo-diamond.svg', opts),
|
||||
'geo-ellipse': formatAssetUrl('./icons/icon/geo-ellipse.svg', opts),
|
||||
'geo-hexagon': formatAssetUrl('./icons/icon/geo-hexagon.svg', opts),
|
||||
|
|
1
packages/assets/types.d.ts
vendored
1
packages/assets/types.d.ts
vendored
|
@ -87,6 +87,7 @@ export type AssetUrls = {
|
|||
'geo-arrow-right': string
|
||||
'geo-arrow-up': string
|
||||
'geo-check-box': string
|
||||
'geo-cloud': string
|
||||
'geo-diamond': string
|
||||
'geo-ellipse': string
|
||||
'geo-hexagon': string
|
||||
|
|
|
@ -286,6 +286,10 @@ export function getAssetUrlsByMetaUrl(opts) {
|
|||
new URL('./icons/icon/geo-check-box.svg', import.meta.url).href,
|
||||
opts
|
||||
),
|
||||
'geo-cloud': formatAssetUrl(
|
||||
new URL('./icons/icon/geo-cloud.svg', import.meta.url).href,
|
||||
opts
|
||||
),
|
||||
'geo-diamond': formatAssetUrl(
|
||||
new URL('./icons/icon/geo-diamond.svg', import.meta.url).href,
|
||||
opts
|
||||
|
|
|
@ -863,7 +863,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
onBeforeCreate: (shape: TLGeoShape) => {
|
||||
props: {
|
||||
growY: number;
|
||||
geo: "arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "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" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box";
|
||||
labelColor: "black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow";
|
||||
color: "black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow";
|
||||
fill: "none" | "pattern" | "semi" | "solid";
|
||||
|
@ -893,7 +893,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
onBeforeUpdate: (prev: TLGeoShape, next: TLGeoShape) => {
|
||||
props: {
|
||||
growY: number;
|
||||
geo: "arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "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" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box";
|
||||
labelColor: "black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow";
|
||||
color: "black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow";
|
||||
fill: "none" | "pattern" | "semi" | "solid";
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { canolicalizeRotation, SelectionEdge, toDomPrecision } from '@tldraw/primitives'
|
||||
import { canonicalizeRotation, SelectionEdge, toDomPrecision } from '@tldraw/primitives'
|
||||
import {
|
||||
getDefaultColorTheme,
|
||||
TLFrameShape,
|
||||
|
@ -69,7 +69,7 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
|
|||
g.appendChild(rect)
|
||||
|
||||
// Text label
|
||||
const pageRotation = canolicalizeRotation(this.editor.getPageRotationById(shape.id))
|
||||
const pageRotation = canonicalizeRotation(this.editor.getPageRotationById(shape.id))
|
||||
// rotate right 45 deg
|
||||
const offsetRotation = pageRotation + Math.PI / 4
|
||||
const scaledRotation = (offsetRotation * (2 / Math.PI) + 4) % 4
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { canolicalizeRotation, SelectionEdge, toDomPrecision } from '@tldraw/primitives'
|
||||
import { canonicalizeRotation, SelectionEdge, toDomPrecision } from '@tldraw/primitives'
|
||||
import { TLShapeId } from '@tldraw/tlschema'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useEditor } from '../../../../hooks/useEditor'
|
||||
|
@ -18,7 +18,7 @@ export const FrameHeading = function FrameHeading({
|
|||
}) {
|
||||
const editor = useEditor()
|
||||
|
||||
const pageRotation = canolicalizeRotation(editor.getPageRotationById(id))
|
||||
const pageRotation = canonicalizeRotation(editor.getPageRotationById(id))
|
||||
const isEditing = useIsEditing(id)
|
||||
|
||||
const rInput = useRef<HTMLInputElement>(null)
|
||||
|
|
|
@ -38,11 +38,15 @@ import { HyperlinkButton } from '../shared/HyperlinkButton'
|
|||
import { SvgExportContext } from '../shared/SvgExportContext'
|
||||
import { TextLabel } from '../shared/TextLabel'
|
||||
import { useForceSolid } from '../shared/useForceSolid'
|
||||
import { cloudOutline, cloudSvgPath } from './cloudOutline'
|
||||
import { DashStyleCloud, DashStyleCloudSvg } from './components/DashStyleCloud'
|
||||
import { DashStyleEllipse, DashStyleEllipseSvg } from './components/DashStyleEllipse'
|
||||
import { DashStyleOval, DashStyleOvalSvg } from './components/DashStyleOval'
|
||||
import { DashStylePolygon, DashStylePolygonSvg } from './components/DashStylePolygon'
|
||||
import { DrawStyleCloud, DrawStyleCloudSvg } from './components/DrawStyleCloud'
|
||||
import { DrawStyleEllipseSvg, getEllipseIndicatorPath } from './components/DrawStyleEllipse'
|
||||
import { DrawStylePolygon, DrawStylePolygonSvg } from './components/DrawStylePolygon'
|
||||
import { SolidStyleCloud, SolidStyleCloudSvg } from './components/SolidStyleCloud'
|
||||
import { SolidStyleEllipse, SolidStyleEllipseSvg } from './components/SolidStyleEllipse'
|
||||
import {
|
||||
getOvalIndicatorPath,
|
||||
|
@ -142,6 +146,9 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
const cy = h / 2
|
||||
|
||||
switch (shape.props.geo) {
|
||||
case 'cloud': {
|
||||
return cloudOutline(w, h, shape.id, shape.props.size)
|
||||
}
|
||||
case 'triangle': {
|
||||
return [new Vec2d(cx, 0), new Vec2d(w, h), new Vec2d(0, h)]
|
||||
}
|
||||
|
@ -358,6 +365,48 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
const h = props.h + growY
|
||||
|
||||
switch (props.geo) {
|
||||
case 'cloud': {
|
||||
if (dash === 'solid' || (dash === 'draw' && forceSolid)) {
|
||||
return (
|
||||
<SolidStyleCloud
|
||||
color={color}
|
||||
fill={fill}
|
||||
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 === 'dashed' ? dash : size === 's' && forceSolid ? 'dashed' : dash}
|
||||
/>
|
||||
)
|
||||
} else if (dash === 'draw') {
|
||||
return (
|
||||
<DrawStyleCloud
|
||||
color={color}
|
||||
fill={fill}
|
||||
strokeWidth={strokeWidth}
|
||||
w={w}
|
||||
h={h}
|
||||
id={id}
|
||||
size={size}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case 'ellipse': {
|
||||
if (dash === 'solid' || (dash === 'draw' && forceSolid)) {
|
||||
return (
|
||||
|
@ -471,7 +520,8 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
|
||||
indicator(shape: TLGeoShape) {
|
||||
const { id, props } = shape
|
||||
const { w, h, growY, size } = props
|
||||
const { w, size } = props
|
||||
const h = props.h + props.growY
|
||||
|
||||
const forceSolid = useForceSolid()
|
||||
const strokeWidth = STROKE_SIZES[size]
|
||||
|
@ -479,13 +529,16 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
switch (props.geo) {
|
||||
case 'ellipse': {
|
||||
if (props.dash === 'draw' && !forceSolid) {
|
||||
return <path d={getEllipseIndicatorPath(id, w, h + growY, strokeWidth)} />
|
||||
return <path d={getEllipseIndicatorPath(id, w, h, strokeWidth)} />
|
||||
}
|
||||
|
||||
return <ellipse cx={w / 2} cy={(h + growY) / 2} rx={w / 2} ry={(h + growY) / 2} />
|
||||
return <ellipse cx={w / 2} cy={h / 2} rx={w / 2} ry={h / 2} />
|
||||
}
|
||||
case 'oval': {
|
||||
return <path d={getOvalIndicatorPath(w, h + growY)} />
|
||||
return <path d={getOvalIndicatorPath(w, h)} />
|
||||
}
|
||||
case 'cloud': {
|
||||
return <path d={cloudSvgPath(w, h, id, size)} />
|
||||
}
|
||||
|
||||
default: {
|
||||
|
@ -602,6 +655,50 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'cloud': {
|
||||
switch (props.dash) {
|
||||
case 'draw':
|
||||
svgElm = DrawStyleCloudSvg({
|
||||
id,
|
||||
strokeWidth,
|
||||
w: props.w,
|
||||
h: props.h,
|
||||
color: props.color,
|
||||
fill: props.fill,
|
||||
size: props.size,
|
||||
theme,
|
||||
})
|
||||
break
|
||||
|
||||
case 'solid':
|
||||
svgElm = SolidStyleCloudSvg({
|
||||
strokeWidth,
|
||||
w: props.w,
|
||||
h: props.h,
|
||||
color: props.color,
|
||||
fill: props.fill,
|
||||
size: props.size,
|
||||
id,
|
||||
theme,
|
||||
})
|
||||
break
|
||||
|
||||
default:
|
||||
svgElm = DashStyleCloudSvg({
|
||||
id,
|
||||
strokeWidth,
|
||||
w: props.w,
|
||||
h: props.h,
|
||||
dash: props.dash,
|
||||
color: props.color,
|
||||
fill: props.fill,
|
||||
theme,
|
||||
size: props.size,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
default: {
|
||||
const outline = this.editor.getOutline(shape)
|
||||
const lines = getLines(shape.props, strokeWidth)
|
||||
|
|
301
packages/editor/src/lib/editor/shapes/geo/cloudOutline.ts
Normal file
301
packages/editor/src/lib/editor/shapes/geo/cloudOutline.ts
Normal file
|
@ -0,0 +1,301 @@
|
|||
import { PI, Vec2d, getPointOnCircle, shortAngleDist } from '@tldraw/primitives'
|
||||
import { TLDefaultSizeStyle, Vec2dModel } from '@tldraw/tlschema'
|
||||
import { rng } from '@tldraw/utils'
|
||||
|
||||
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: Vec2dModel
|
||||
delta: Vec2dModel
|
||||
}
|
||||
| {
|
||||
type: 'arc'
|
||||
center: Vec2dModel
|
||||
startAngle: number
|
||||
}
|
||||
|
||||
export 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 Vec2d(radius, 0),
|
||||
delta: new Vec2d(1, 0),
|
||||
},
|
||||
{
|
||||
type: 'arc',
|
||||
center: new Vec2d(width - radius, radius),
|
||||
startAngle: -PI / 2,
|
||||
},
|
||||
{
|
||||
type: 'straight',
|
||||
start: new Vec2d(width - radius, height),
|
||||
delta: new Vec2d(-1, 0),
|
||||
},
|
||||
{
|
||||
type: 'arc',
|
||||
center: new Vec2d(radius, radius),
|
||||
startAngle: PI / 2,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
type: 'straight',
|
||||
start: new Vec2d(width, radius),
|
||||
delta: new Vec2d(0, 1),
|
||||
},
|
||||
{
|
||||
type: 'arc',
|
||||
center: new Vec2d(radius, height - radius),
|
||||
startAngle: 0,
|
||||
},
|
||||
{
|
||||
type: 'straight',
|
||||
start: new Vec2d(0, height - radius),
|
||||
delta: new Vec2d(0, -1),
|
||||
},
|
||||
{
|
||||
type: 'arc',
|
||||
center: new Vec2d(radius, radius),
|
||||
startAngle: PI,
|
||||
},
|
||||
]
|
||||
|
||||
let sectionOffset = 0
|
||||
|
||||
const points: Vec2d[] = []
|
||||
for (let i = 0; i < numPoints; i++) {
|
||||
const section = sections[0]
|
||||
if (section.type === 'straight') {
|
||||
points.push(Vec2d.Add(section.start, Vec2d.Mul(section.delta, sectionOffset)))
|
||||
} else {
|
||||
points.push(
|
||||
getPointOnCircle(
|
||||
section.center.x,
|
||||
section.center.y,
|
||||
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)
|
||||
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 bumpPoints = getPillPoints(innerWidth, innerHeight, numBumps).map((p) => {
|
||||
return p.addXY(paddingX, paddingY)
|
||||
})
|
||||
|
||||
const maxWiggle = targetBumpProtrusion * 0.3
|
||||
|
||||
const adjustedBumpPoints = bumpPoints.map((p) => {
|
||||
return Vec2d.AddXY(p, getRandom() * maxWiggle, getRandom() * maxWiggle)
|
||||
})
|
||||
|
||||
const arcs: Arc[] = []
|
||||
|
||||
for (let i = 0; i < adjustedBumpPoints.length; i++) {
|
||||
const leftPoint = adjustedBumpPoints[i]
|
||||
const rightPoint = adjustedBumpPoints[i === adjustedBumpPoints.length - 1 ? 0 : i + 1]
|
||||
|
||||
arcs.push(getCloudArc(leftPoint, rightPoint, Math.max(paddingX, paddingY), width, height))
|
||||
}
|
||||
|
||||
return arcs
|
||||
}
|
||||
|
||||
export function getCloudArc(
|
||||
leftPoint: Vec2d,
|
||||
rightPoint: Vec2d,
|
||||
padding: number,
|
||||
width: number,
|
||||
height: number
|
||||
) {
|
||||
const midPoint = Vec2d.Average([leftPoint, rightPoint])
|
||||
const offsetAngle = Vec2d.Angle(leftPoint, rightPoint) - Math.PI / 2
|
||||
const arcPoint = Vec2d.Add(midPoint, Vec2d.FromAngle(offsetAngle, padding))
|
||||
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(leftPoint, rightPoint, arcPoint)
|
||||
const radius = Vec2d.Dist(center, leftPoint)
|
||||
|
||||
return {
|
||||
leftPoint,
|
||||
rightPoint,
|
||||
center,
|
||||
radius,
|
||||
}
|
||||
}
|
||||
|
||||
type Arc = ReturnType<typeof getCloudArc>
|
||||
|
||||
function getCenterOfCircleGivenThreePoints(a: Vec2d, b: Vec2d, c: Vec2d) {
|
||||
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 Vec2d.Average([a, b, c])
|
||||
}
|
||||
|
||||
return new Vec2d(x, y)
|
||||
}
|
||||
|
||||
export function cloudOutline(
|
||||
width: number,
|
||||
height: number,
|
||||
seed: string,
|
||||
size: TLDefaultSizeStyle
|
||||
) {
|
||||
const path: Vec2d[] = []
|
||||
|
||||
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 arcs = getCloudArcs(width, height, seed, size)
|
||||
let path = `M${arcs[0].leftPoint.x},${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 { rightPoint, radius } of arcs) {
|
||||
path += ` A${radius},${radius} 0 0,1 ${rightPoint.x},${rightPoint.y}`
|
||||
}
|
||||
|
||||
path += ' Z'
|
||||
return path
|
||||
}
|
||||
|
||||
export function inkyCloudSvgPath(
|
||||
width: number,
|
||||
height: number,
|
||||
seed: string,
|
||||
size: TLDefaultSizeStyle
|
||||
) {
|
||||
const getRandom = rng(seed)
|
||||
const mut = (n: number) => {
|
||||
const multiplier = size === 's' ? 0.5 : size === 'm' ? 0.7 : size === 'l' ? 0.9 : 1.6
|
||||
return n + getRandom() * multiplier * 2
|
||||
}
|
||||
const arcs = getCloudArcs(width, height, seed, size)
|
||||
let pathA = `M${arcs[0].leftPoint.x},${arcs[0].leftPoint.y}`
|
||||
let pathB = `M${mut(arcs[0].leftPoint.x)},${mut(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 { rightPoint, radius, center } of arcs) {
|
||||
pathA += ` A${radius},${radius} 0 0,1 ${rightPoint.x},${rightPoint.y}`
|
||||
const mutX = mut(rightPoint.x)
|
||||
const mutY = mut(rightPoint.y)
|
||||
const mutRadius = Vec2d.Dist(center, { x: mutX, y: mutY })
|
||||
|
||||
pathB += ` A${mutRadius},${mutRadius} 0 0,1 ${mutX},${mutY}`
|
||||
}
|
||||
|
||||
return pathA + pathB + ' Z'
|
||||
}
|
||||
|
||||
export function pointsOnArc(
|
||||
startPoint: Vec2dModel,
|
||||
endPoint: Vec2dModel,
|
||||
center: Vec2dModel,
|
||||
radius: number,
|
||||
numPoints: number
|
||||
): Vec2d[] {
|
||||
const results: Vec2d[] = []
|
||||
|
||||
const startAngle = Vec2d.Angle(center, startPoint)
|
||||
const endAngle = Vec2d.Angle(center, endPoint)
|
||||
|
||||
const l = shortAngleDist(startAngle, endAngle)
|
||||
|
||||
for (let i = 0; i < numPoints; i++) {
|
||||
const t = i / (numPoints - 1)
|
||||
const angle = startAngle + l * t
|
||||
const point = getPointOnCircle(center.x, center.y, radius, angle)
|
||||
results.push(point)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
import { Vec2d, canonicalizeRotation } from '@tldraw/primitives'
|
||||
import { TLDefaultColorTheme, TLGeoShape, TLShapeId } from '@tldraw/tlschema'
|
||||
import * as React from 'react'
|
||||
import {
|
||||
ShapeFill,
|
||||
getShapeFillSvg,
|
||||
getSvgWithShapeFill,
|
||||
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 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 angle = canonicalizeRotation(
|
||||
canonicalizeRotation(Vec2d.Angle(center, rightPoint)) -
|
||||
canonicalizeRotation(Vec2d.Angle(center, leftPoint))
|
||||
)
|
||||
const arcLength = radius * angle
|
||||
|
||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||
arcLength,
|
||||
strokeWidth,
|
||||
{
|
||||
style: dash,
|
||||
start: 'outset',
|
||||
end: 'outset',
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<path
|
||||
key={i}
|
||||
d={`M${leftPoint.x},${leftPoint.y}A${radius},${radius},0,0,1,${rightPoint.x},${rightPoint.y}`}
|
||||
strokeDasharray={strokeDasharray}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</g>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export function DashStyleCloudSvg({
|
||||
dash,
|
||||
fill,
|
||||
color,
|
||||
theme,
|
||||
strokeWidth,
|
||||
w,
|
||||
h,
|
||||
id,
|
||||
size,
|
||||
}: Pick<TLGeoShape['props'], 'dash' | 'fill' | 'color' | 'w' | 'h' | 'size'> & {
|
||||
id: TLShapeId
|
||||
strokeWidth: number
|
||||
theme: TLDefaultColorTheme
|
||||
}) {
|
||||
const innerPath = cloudSvgPath(w, h, id, size)
|
||||
const arcs = getCloudArcs(w, h, id, size)
|
||||
|
||||
const strokeElement = document.createElementNS('http://www.w3.org/2000/svg', 'g')
|
||||
strokeElement.setAttribute('stroke-width', strokeWidth.toString())
|
||||
strokeElement.setAttribute('stroke', theme[color].solid)
|
||||
strokeElement.setAttribute('fill', 'none')
|
||||
|
||||
for (const { leftPoint, rightPoint, center, radius } of arcs) {
|
||||
const angle = canonicalizeRotation(
|
||||
canonicalizeRotation(Vec2d.Angle(center, rightPoint)) -
|
||||
canonicalizeRotation(Vec2d.Angle(center, leftPoint))
|
||||
)
|
||||
const arcLength = radius * angle
|
||||
|
||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(arcLength, strokeWidth, {
|
||||
style: dash,
|
||||
start: 'outset',
|
||||
end: 'outset',
|
||||
})
|
||||
|
||||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
||||
path.setAttribute(
|
||||
'd',
|
||||
`M${leftPoint.x},${leftPoint.y}A${radius},${radius},0,0,1,${rightPoint.x},${rightPoint.y}`
|
||||
)
|
||||
path.setAttribute('stroke-dasharray', strokeDasharray.toString())
|
||||
path.setAttribute('stroke-dashoffset', strokeDashoffset.toString())
|
||||
strokeElement.appendChild(path)
|
||||
}
|
||||
|
||||
// Get the fill element, if any
|
||||
const fillElement = getShapeFillSvg({
|
||||
d: innerPath,
|
||||
fill,
|
||||
color,
|
||||
theme,
|
||||
})
|
||||
|
||||
return getSvgWithShapeFill(strokeElement, fillElement)
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import { TLDefaultColorTheme, TLGeoShape, TLShapeId } from '@tldraw/tlschema'
|
||||
import * as React from 'react'
|
||||
import {
|
||||
ShapeFill,
|
||||
getShapeFillSvg,
|
||||
getSvgWithShapeFill,
|
||||
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 d={path} fill={fill} color={color} />
|
||||
<path d={path} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export function DrawStyleCloudSvg({
|
||||
fill,
|
||||
color,
|
||||
strokeWidth,
|
||||
theme,
|
||||
w,
|
||||
h,
|
||||
id,
|
||||
size,
|
||||
}: Pick<TLGeoShape['props'], 'fill' | 'color' | 'w' | 'h' | 'size'> & {
|
||||
strokeWidth: number
|
||||
theme: TLDefaultColorTheme
|
||||
id: TLShapeId
|
||||
}) {
|
||||
const pathData = inkyCloudSvgPath(w, h, id, size)
|
||||
|
||||
const strokeElement = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
||||
strokeElement.setAttribute('d', pathData)
|
||||
strokeElement.setAttribute('stroke-width', strokeWidth.toString())
|
||||
strokeElement.setAttribute('stroke', theme[color].solid)
|
||||
strokeElement.setAttribute('fill', 'none')
|
||||
|
||||
// Get the fill element, if any
|
||||
const fillElement = getShapeFillSvg({
|
||||
d: pathData,
|
||||
fill,
|
||||
color,
|
||||
theme,
|
||||
})
|
||||
|
||||
return getSvgWithShapeFill(strokeElement, fillElement)
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import { TLDefaultColorTheme, TLGeoShape, TLShapeId } from '@tldraw/tlschema'
|
||||
import * as React from 'react'
|
||||
import {
|
||||
ShapeFill,
|
||||
getShapeFillSvg,
|
||||
getSvgWithShapeFill,
|
||||
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 d={path} fill={fill} color={color} />
|
||||
<path d={path} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export function SolidStyleCloudSvg({
|
||||
fill,
|
||||
color,
|
||||
strokeWidth,
|
||||
theme,
|
||||
w,
|
||||
h,
|
||||
id,
|
||||
size,
|
||||
}: Pick<TLGeoShape['props'], 'fill' | 'color' | 'w' | 'h' | 'size'> & {
|
||||
strokeWidth: number
|
||||
theme: TLDefaultColorTheme
|
||||
id: TLShapeId
|
||||
}) {
|
||||
const pathData = cloudSvgPath(w, h, id, size)
|
||||
|
||||
const strokeElement = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
||||
strokeElement.setAttribute('d', pathData)
|
||||
strokeElement.setAttribute('stroke-width', strokeWidth.toString())
|
||||
strokeElement.setAttribute('stroke', theme[color].solid)
|
||||
strokeElement.setAttribute('fill', 'none')
|
||||
|
||||
// Get the fill element, if any
|
||||
const fillElement = getShapeFillSvg({
|
||||
d: pathData,
|
||||
fill,
|
||||
color,
|
||||
theme,
|
||||
})
|
||||
|
||||
return getSvgWithShapeFill(strokeElement, fillElement)
|
||||
}
|
|
@ -81,7 +81,11 @@ export class Pointing extends StateNode {
|
|||
if (!shape) return
|
||||
|
||||
const bounds =
|
||||
shape.props.geo === 'star' ? getStarBounds(5, 200, 200) : new Box2d(0, 0, 200, 200)
|
||||
shape.props.geo === 'star'
|
||||
? getStarBounds(5, 200, 200)
|
||||
: shape.props.geo === 'cloud'
|
||||
? new Box2d(0, 0, 300, 180)
|
||||
: new Box2d(0, 0, 200, 200)
|
||||
const delta = this.editor.getDeltaInParentSpace(shape, bounds.center)
|
||||
|
||||
this.editor.select(id)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {
|
||||
canolicalizeRotation,
|
||||
canonicalizeRotation,
|
||||
EPSILON,
|
||||
PI,
|
||||
PI2,
|
||||
|
@ -332,8 +332,8 @@ describe('When resizing mulitple shapes...', () => {
|
|||
.pointerMove(rotateEnd.x, rotateEnd.y)
|
||||
.pointerUp()
|
||||
|
||||
expect(canolicalizeRotation(shapeA.rotation) % Math.PI).toBeCloseTo(
|
||||
canolicalizeRotation(rotation) % Math.PI
|
||||
expect(canonicalizeRotation(shapeA.rotation) % Math.PI).toBeCloseTo(
|
||||
canonicalizeRotation(rotation) % Math.PI
|
||||
)
|
||||
expect(editor.getPageRotation(shapeB)).toBeCloseTo(rotation + rotationB)
|
||||
expect(editor.getPageRotation(shapeC)).toBeCloseTo(rotation + rotationB)
|
||||
|
@ -589,7 +589,7 @@ describe('Reisizing a selection of multiple shapes', () => {
|
|||
editor.pointerUp(20, 20, { shiftKey: false })
|
||||
jest.advanceTimersByTime(200)
|
||||
|
||||
expect(editor.getShapeById(ids.boxB)!.rotation).toBeCloseTo(canolicalizeRotation(-PI / 2))
|
||||
expect(editor.getShapeById(ids.boxB)!.rotation).toBeCloseTo(canonicalizeRotation(-PI / 2))
|
||||
|
||||
editor.select(ids.boxA, ids.boxB)
|
||||
// shrink
|
||||
|
@ -2326,7 +2326,7 @@ describe('snapping while resizing a shape that has been rotated by multiples of
|
|||
expect(editor.getPageBoundsById(ids.boxX)!.w).toBeCloseTo(60)
|
||||
expect(editor.getPageBoundsById(ids.boxX)!.h).toBeCloseTo(60)
|
||||
expect(editor.getShapeById(ids.boxX)!.rotation).toEqual(
|
||||
canolicalizeRotation(((PI / 2) * times) % (PI * 2))
|
||||
canonicalizeRotation(((PI / 2) * times) % (PI * 2))
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { canolicalizeRotation, Matrix2d, Vec2d } from '@tldraw/primitives'
|
||||
import { canonicalizeRotation, Matrix2d, Vec2d } from '@tldraw/primitives'
|
||||
import { isShapeId, TLShape, TLShapePartial } from '@tldraw/tlschema'
|
||||
import { structuredClone } from '@tldraw/utils'
|
||||
import { Editor } from '../editor/Editor'
|
||||
|
@ -83,7 +83,7 @@ export function applyRotationToSnapshotShapes({
|
|||
Matrix2d.Inverse(parentTransform),
|
||||
newPagePoint
|
||||
)
|
||||
const newRotation = canolicalizeRotation(shape.rotation + delta)
|
||||
const newRotation = canonicalizeRotation(shape.rotation + delta)
|
||||
|
||||
return {
|
||||
id: shape.id,
|
||||
|
|
|
@ -130,7 +130,7 @@ export class Box2d {
|
|||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export function canolicalizeRotation(a: number): number;
|
||||
export function canonicalizeRotation(a: number): number;
|
||||
|
||||
// @public
|
||||
export function clamp(n: number, min: number): number;
|
||||
|
@ -718,6 +718,8 @@ export class Vec2d {
|
|||
// (undocumented)
|
||||
static From({ x, y, z }: Vec2dModel): Vec2d;
|
||||
// (undocumented)
|
||||
static FromAngle(r: number, length?: number): Vec2d;
|
||||
// (undocumented)
|
||||
static FromArray(v: number[]): Vec2d;
|
||||
// (undocumented)
|
||||
static Len(A: VecLike): number;
|
||||
|
|
|
@ -55,7 +55,7 @@ export {
|
|||
angleDelta,
|
||||
approximately,
|
||||
areAnglesCompatible,
|
||||
canolicalizeRotation,
|
||||
canonicalizeRotation,
|
||||
clamp,
|
||||
clampRadians,
|
||||
degreesToRadians,
|
||||
|
|
|
@ -484,6 +484,10 @@ export class Vec2d {
|
|||
return r
|
||||
}
|
||||
|
||||
static FromAngle(r: number, length = 1) {
|
||||
return new Vec2d(Math.cos(r) * length, Math.sin(r) * length)
|
||||
}
|
||||
|
||||
static ToArray(A: VecLike) {
|
||||
return [A.x, A.y, A.z!]
|
||||
}
|
||||
|
|
|
@ -88,7 +88,7 @@ export function perimeterOfEllipse(rx: number, ry: number): number {
|
|||
* @returns A number between 0 and 2 * PI
|
||||
* @public
|
||||
*/
|
||||
export function canolicalizeRotation(a: number) {
|
||||
export function canonicalizeRotation(a: number) {
|
||||
a = a % PI2
|
||||
if (a < 0) {
|
||||
a = a + PI2
|
||||
|
|
|
@ -457,14 +457,14 @@ export const frameShapeProps: {
|
|||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export const GeoShapeGeoStyle: EnumStyleProp<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "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" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const geoShapeMigrations: Migrations;
|
||||
|
||||
// @public (undocumented)
|
||||
export const geoShapeProps: {
|
||||
geo: EnumStyleProp<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "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" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">;
|
||||
labelColor: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow">;
|
||||
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow">;
|
||||
fill: EnumStyleProp<"none" | "pattern" | "semi" | "solid">;
|
||||
|
|
|
@ -15,7 +15,7 @@ import { arrowShapeMigrations } from './shapes/TLArrowShape'
|
|||
import { bookmarkShapeMigrations } from './shapes/TLBookmarkShape'
|
||||
import { drawShapeMigrations } from './shapes/TLDrawShape'
|
||||
import { embedShapeMigrations } from './shapes/TLEmbedShape'
|
||||
import { geoShapeMigrations } from './shapes/TLGeoShape'
|
||||
import { GeoShapeVersions, geoShapeMigrations } from './shapes/TLGeoShape'
|
||||
import { imageShapeMigrations } from './shapes/TLImageShape'
|
||||
import { noteShapeMigrations } from './shapes/TLNoteShape'
|
||||
import { textShapeMigrations } from './shapes/TLTextShape'
|
||||
|
@ -707,6 +707,22 @@ describe('Migrate GeoShape legacy horizontal alignment', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('adding cloud shape', () => {
|
||||
const { up, down } = geoShapeMigrations.migrators[GeoShapeVersions.AddCloud]
|
||||
|
||||
test('up does nothing', () => {
|
||||
expect(up({ props: { geo: 'rectangle' } })).toEqual({
|
||||
props: { geo: 'rectangle' },
|
||||
})
|
||||
})
|
||||
|
||||
test('down converts clouds to rectangles', () => {
|
||||
expect(down({ props: { geo: 'cloud' } })).toEqual({
|
||||
props: { geo: 'rectangle' },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Migrate NoteShape legacy horizontal alignment', () => {
|
||||
const { up, down } = noteShapeMigrations.migrators[3]
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import { ShapePropsType, TLBaseShape } from './TLBaseShape'
|
|||
export const GeoShapeGeoStyle = StyleProp.defineEnum('tldraw:geo', {
|
||||
defaultValue: 'rectangle',
|
||||
values: [
|
||||
'cloud',
|
||||
'rectangle',
|
||||
'ellipse',
|
||||
'triangle',
|
||||
|
@ -72,11 +73,14 @@ const Versions = {
|
|||
AddCheckBox: 4,
|
||||
AddVerticalAlign: 5,
|
||||
MigrateLegacyAlign: 6,
|
||||
AddCloud: 7,
|
||||
} as const
|
||||
|
||||
export { Versions as GeoShapeVersions }
|
||||
|
||||
/** @internal */
|
||||
export const geoShapeMigrations = defineMigrations({
|
||||
currentVersion: Versions.MigrateLegacyAlign,
|
||||
currentVersion: Versions.AddCloud,
|
||||
migrators: {
|
||||
[Versions.AddUrlProp]: {
|
||||
up: (shape) => {
|
||||
|
@ -202,5 +206,21 @@ export const geoShapeMigrations = defineMigrations({
|
|||
}
|
||||
},
|
||||
},
|
||||
[Versions.AddCloud]: {
|
||||
up: (shape) => {
|
||||
return shape
|
||||
},
|
||||
down: (shape) => {
|
||||
if (shape.props.geo === 'cloud') {
|
||||
return {
|
||||
...shape,
|
||||
props: {
|
||||
...shape.props,
|
||||
geo: 'rectangle',
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -55,6 +55,7 @@ export const STYLES = {
|
|||
geo: [
|
||||
{ value: 'rectangle', icon: 'geo-rectangle' },
|
||||
{ value: 'ellipse', icon: 'geo-ellipse' },
|
||||
{ value: 'cloud', icon: 'geo-cloud' },
|
||||
{ value: 'triangle', icon: 'geo-triangle' },
|
||||
{ value: 'diamond', icon: 'geo-diamond' },
|
||||
{ value: 'pentagon', icon: 'geo-pentagon' },
|
||||
|
|
|
@ -61,8 +61,8 @@ export function ToolbarSchemaProvider({ overrides, children }: TLUiToolbarSchema
|
|||
toolbarItem(tools['triangle']),
|
||||
toolbarItem(tools['trapezoid']),
|
||||
toolbarItem(tools['rhombus']),
|
||||
toolbarItem(tools['pentagon']),
|
||||
toolbarItem(tools['hexagon']),
|
||||
toolbarItem(tools['cloud']),
|
||||
// toolbarItem(tools['octagon']),
|
||||
toolbarItem(tools['star']),
|
||||
toolbarItem(tools['oval']),
|
||||
|
|
|
@ -145,6 +145,7 @@ export type TLUiTranslationKey =
|
|||
| 'geo-style.hexagon'
|
||||
| 'geo-style.octagon'
|
||||
| 'geo-style.oval'
|
||||
| 'geo-style.cloud'
|
||||
| 'geo-style.pentagon'
|
||||
| 'geo-style.rectangle'
|
||||
| 'geo-style.rhombus-2'
|
||||
|
|
|
@ -145,6 +145,7 @@ export const DEFAULT_TRANSLATION = {
|
|||
'geo-style.hexagon': 'Hexagon',
|
||||
'geo-style.octagon': 'Octagon',
|
||||
'geo-style.oval': 'Oval',
|
||||
'geo-style.cloud': 'Cloud',
|
||||
'geo-style.pentagon': 'Pentagon',
|
||||
'geo-style.rectangle': 'Rectangle',
|
||||
'geo-style.rhombus-2': 'Rhombus 2',
|
||||
|
|
|
@ -79,6 +79,7 @@ export type TLUiIconType =
|
|||
| 'geo-arrow-right'
|
||||
| 'geo-arrow-up'
|
||||
| 'geo-check-box'
|
||||
| 'geo-cloud'
|
||||
| 'geo-diamond'
|
||||
| 'geo-ellipse'
|
||||
| 'geo-hexagon'
|
||||
|
@ -243,6 +244,7 @@ export const iconTypes = [
|
|||
'geo-arrow-right',
|
||||
'geo-arrow-up',
|
||||
'geo-check-box',
|
||||
'geo-cloud',
|
||||
'geo-diamond',
|
||||
'geo-ellipse',
|
||||
'geo-hexagon',
|
||||
|
|
|
@ -3,6 +3,7 @@ import fetch from 'cross-fetch'
|
|||
import { assert } from 'node:console'
|
||||
import { parse } from 'semver'
|
||||
import { exec } from './lib/exec'
|
||||
import { BUBLIC_ROOT } from './lib/file'
|
||||
import { nicelog } from './lib/nicelog'
|
||||
import { getLatestVersion, publish, setAllVersions } from './lib/publishing'
|
||||
import { getAllWorkspacePackages } from './lib/workspace'
|
||||
|
@ -57,7 +58,12 @@ async function main() {
|
|||
packageJsonFilesToAdd.push(`${workspace.relativePath}/package.json`)
|
||||
}
|
||||
}
|
||||
await exec('git', ['add', 'lerna.json', ...packageJsonFilesToAdd])
|
||||
await exec('git', [
|
||||
'add',
|
||||
'lerna.json',
|
||||
...packageJsonFilesToAdd,
|
||||
BUBLIC_ROOT + '/packages/*/src/version.ts',
|
||||
])
|
||||
|
||||
// this creates a new commit
|
||||
await auto.changelog({
|
||||
|
|
Loading…
Reference in a new issue